C 初学者 - 结构中的字符串不需要 Malloc

Beginner to C - Malloc Not Required For String In Struct

提问人:Nathaniel Woodbury 提问时间:7/29/2023 最后编辑:Nathaniel Woodbury 更新时间:7/30/2023 访问量:114

问:

为什么不需要在结构中的字符串上调用 Malloc?

嗨,我这周刚开始学习 C,我发现了一个我无法解释的奇怪不一致。

据我所知,当函数结束时,“堆栈”中分配的项目将从内存中删除。如果我们希望一个变量持续更长时间(当作用域结束时),我们使用 malloc。

这对结构来说是完全有意义的,并且是一致的。如果您创建一个结构,则尝试打印其创建函数范围之外的值 - 除非您恶意定位该结构,否则将导致错误。

但是,我发现如果我尝试创建一个结构并使用默认值(结构的大小)对其进行 malloc,然后添加一个 char 指针(字符串)作为结构成员,它会接收该值并且在尝试打印它时不会出现任何错误 - 我认为我需要在结构成员本身上调用 malloc, 然后将字符串复制到其中?

知道这里发生了什么吗??

这将打印结构成员,而不会出现任何问题。

typedef struct person {
  char *name;
} person;

// print struct fields
void print_person(person *input) { printf("Name: %s", input->name); }

person *person_generator(char *name) {
  my_struct *person = malloc(sizeof(my_struct));
  // assign given char pointer to struct field
  person->name = name;
  return person;
}

person *create_default_person() {
  person *new_person = person_generator("Alfred Hitchcock");
  return new_person;
}

int main() {
  person *default_person = create_default_person();
  print_person(default_person);
  return 0;
}

我很困惑为什么下面的代码不是强制性的?

typedef struct person {
  char *name;
} person;

// print struct fields
void print_person(person *input) { printf("Name: %s", input->name); }

person *person_generator(char *name) {
  my_struct *person = malloc(sizeof(my_struct));
  // didnt need to do this?
  person->name = malloc(strlen(name) + 1);
  strcpy(person->name, name);
  return person;
}

char *get_string() {
  char *item = "Alfred Hitchcock";
  return item;
}

person *create_default_person() {
  char *value = get_string();
  person *new_person = person_generator(value);
  return new_person;
}

int main() {
  person *default_person = create_default_person();
  print_person(default_person);
  return 0;
}

我期望当“create_default_person()”函数结束时,它创建的指针会从其内部的堆栈中被销毁,那么结构成员应该指向垃圾数据,对吗?

编辑

附加问题:

我被告知答案是字符串文字在应用程序的生命周期内存储。让我们再举一个例子。从文件中读取字符串,然后将其分配到 struct 字段中,而不调用 malloc。

为什么这仍然正确打印

typedef struct person {
  char *name;
} person;

void print_person(person *input) { printf("Name: %s", input->name); }

person *person_generator(char *name) {
  my_struct *person = malloc(sizeof(my_struct));
  person->name = name;
  return person;
}

person *create_default_person() {
  FILE *fp;
  fp = fopen("./data.txt", "r");
  char buffer[255];

  if (fp == NULL) {
    printf("Couldnt open\n");
    return NULL;
  }

  fgets(buffer, 255, fp);
  fclose(fp);

  person *new_person = person_generator(buffer);
  return new_person;
}

int main() {
  person *human = create_default_person();
  if (human == NULL) {
    printf("Failed to find human");
    return 0;
  }
  print_person(human);
  return 0;
}
struct malloc c-strings

评论

2赞 Chris 7/29/2023
注意:您尚未检查任何一个调用是否成功。您可能还希望使用 .mallocperson->name = strdup(name);
1赞 chux - Reinstate Monica 7/29/2023
@Nathaniel伍德伯里,还要考虑你会怎么做。如果该函数被分配了,则该函数可能会被分配,而如果它的来源是 ,则该函数不会被释放。所以最好分配:在所有情况下,所以以后的代码可以在所有情况下释放。delete_person(default_person)free(person->name);person->namemalloc()person->name = name;person->name = malloc(strlen(name) + 1); strcpy(person->name, name);
1赞 Jeremy Friesner 7/29/2023
在您的后续问题中,示例程序在尝试取消引用指向该缓冲区消失后的指针时,会调用内部未定义的行为。当一个程序调用未定义的行为时,任何事情都可能发生,包括输出与你对正确程序的期望相匹配——但这只是盲目的运气(即还没有任何东西覆盖堆栈的那部分),你不能依赖它以这种方式工作。(酒店管理层不会对你满意:stackoverflow.com/a/6445794/131930print_person()char buffer[255] )
0赞 Nathaniel Woodbury 7/29/2023
@JeremyFriesner 这么清楚的答案。非常感谢

答:

3赞 Jeremy Friesner 7/29/2023 #1

在 C 语言中,字符串文字 (like ) 作为进程静态数据的一部分静态存储"Alfred Hitchcock"

因此,它们可以保证在进程的整个生命周期内保持有效,并且不需要手动分配或释放其存储空间。正如你所发现的,一个简单的指向它们的指针就足以使用它们。

另一方面,如果你在运行时随机编造新名称,则需要为这些运行时生成的字符串显式分配空间,并使用 /etc 管理它们的生存期。malloc()/free()

我期望当“create_default_person()”函数结束时,它创建的指针会从其内部的堆栈中被销毁,那么结构成员应该指向垃圾数据,对吗?

不完全是 -- 函数结束时,指针会被销毁,但指针指向的数据不会。在这种情况下,字符串文本本身无限期地保持有效,无论指向或不指向它。

评论

0赞 Barmar 7/29/2023
您可能想提到第二个版本更好,因为它不需要调用方保证参数字符串具有永久生存期。
0赞 Eric Postpischil 7/29/2023
回复“当函数结束时指针被销毁”:不确定您说的是哪个函数,但在 OP 的代码中没有指被销毁。 将 的地址(第一个字符)传递给 。 将其存储在 中并返回 ,这是指向它分配的内存的指针。 使用返回的地址初始化并返回它...create_default_person"Alfred Hitchcock"person_generatorperson_generatorperson->namepersoncreate_default_personnew_person
0赞 Eric Postpischil 7/29/2023
...如果某个函数确实返回指向其本地自动对象之一的指针,则该对象将在 C++ 意义上被“销毁”。在 C 语言中,我们说它的生存期结束,这意味着内存不再为它保留。除此之外,大多数 C 实现不会进行任何“破坏”活动。返回的地址不会在任何意义上被销毁,尽管根据 C 标准的定义,其值是不确定的。
0赞 Nathaniel Woodbury 7/29/2023
很好的答案。我添加了一个额外的 EDIT 跟进问题供任何人回答。谢谢!
0赞 arfneto 7/30/2023 #2

据我所知,当函数结束时,“堆栈”中分配的项目将从内存中删除。如果我们希望一个变量持续更长时间(当作用域结束时),我们使用 malloc。

只要将代码加载到内存中,函数中声明的任何值都将存在。也许你不能再接受它了,但价值会一直存在。在对同一函数的下一次调用中,它们将具有相同的值和地址。这对于字符串文字是一样的,如 。staticitemchar *item = "Alfred Hitchcock"

通过调用在函数内部分配的区域不会被神奇地删除:这是 的任务,你的代码负责:必须由你的代码分配的区域,通过在从函数返回之前调用,或者使用程序逻辑和返回的指针。否则,该地区将永远消失。这称为内存泄漏malloc()freemallocfree

但是,我发现如果我尝试创建一个结构并使用默认值(结构的大小)对其进行 malloc,然后添加一个 char 指针(字符串)作为结构成员,它会接收该值并且在尝试打印它时不会出现任何错误 - 我认为我需要在结构成员本身上调用 malloc, 然后将字符串复制到其中?

关于这个

这是struct

typedef struct person
{
    char* name;
} person;

person只是指向 char 的指针的容器。并且具有固定的尺寸。你不需要对它做任何事情。它可以指向一个 1 GB 的区域,它可以指向一个 .它可以指向 。如果你需要一些东西,你需要把它放在那里。doubleNULL

有点跑题了,但是,正如您正在学习的那样,您会看到 .并且有一个无声的约定,保留第一个大写字母来命名你定义的名字,所以你可以更好地作为structCperson

typedef struct
{
    char* name;
} Person;

另请注意,编译器并不关心代码的对齐方式。缩进仅用于阅读。符号是一种宗教信仰,我们很少可以选择如何在源代码中声明或对齐事物:这是由您的学校、公司或客户决定的。通常,我们甚至无法决定使用哪种语言。

但现实情况是,当你在 in 中声明时,你实际上是在声明你的 IDE 或编译器可以告诉你的 a,所以你这样写可能更容易namechar* namenamechar*

typedef struct
{
    char* name;

}   Person;

而且从不

typedef struct
{
    char *name;

}   Person;

这里是 .它被宣布为这样。如果 是 那么显然是 : 这就是 这里 的意思。这是声明的结果,而不是声明本身的结果。namechar*namechar**namechar*C

以这种方式编写代码可以使以后更容易阅读和理解代码。

关于第一个代码

void print_person(Person* input)
{
    printf("Name: %s", input->name);
}

没关系,但检查一个是明智的NULLinput

person *person_generator(char *name) {
  my_struct *person = malloc(sizeof(my_struct));
  // assign given char pointer to struct field
  person->name = name;
  return person;
}

这在某种程度上也没关系,但正如我之前告诉过你的那样,最好按原样写

Person* person_generator(char* name)
{
    Person* one = malloc(sizeof(Person));
    one->name = name; // can be NULL
    return one;
}

这里的问题是,以这种方式写作,你无法控制你在里面放什么。请参阅下面的代码,我稍后会谈到这一点struct

这里

person *create_default_person() {
  person *new_person = person_generator("Alfred Hitchcock");
  return new_person;
}

没关系,只是一个默认值,但请考虑

Person* create_default_person()
{
    return person_generator("Alfred Hitchcock");
}

它更容易阅读。

返回person_generator

如果不将字符串内容复制到指向的新分配区域,则会遇到麻烦。如果不这样做,则无法安全地释放此区域。如果被召唤,你最终可能会打电话,而且它是无害的。但是,如果被调用,就像你在 中所做的那样,这个字符串是静态的,一个文字,不能是自由的,所以如果你释放 .更好的是始终拥有您的数据,就像在nameperson_generator (NULL)free(NULL)person_generator("Alfred Hitchcock")create_default_person()PersonstructPerson

Person* create_person(char* name)
{
    Person* one = (Person*)malloc(sizeof(Person));
    if (name == NULL) return one;
    one->name = (char*)malloc(sizeof(strlen(name) + 1));
    if (name == NULL) return one;  // if malloc failed
    strcpy(one->name, name);       // copy all data
    printf("\tAllocating \"%s\"\n", one->name);
    return one;
}

[名称改为create_person()person_generator]

很多人会告诉你不要结果。这是 90 年代的东西,基于 95 年的一本书和几年后的 C 常见问题解答文档。 这在古代的东西中:不要相信它。正如你在这里看到的,即使在上面这个适度的 10 行代码中,有时你也需要在一页代码中做很多事情,而转换返回的指针会作为对你和你的员工的文档或警告。 只是返回,并且通过不让转换隐含,您可以为自己省去很多麻烦。今天我们知道,隐性的东西不是这么好的......castmallocmallocmallocvoid*

另外,当你编写代码来创建一个动态的东西时,请习惯于立即编写代码来删除它。如

Person* erase_person(Person* one)
{
    if (one == NULL) return NULL;
    if (one->name != NULL)
        printf("\tDeleting \"%s\"\n", one->name);
    free(one->name);
    free(one);
    return NULL;
}

并测试它。

一个完整的例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct
{
    char* name;
} Person;

Person* create_default_person();
Person* create_person(char*);
Person* erase_person(Person*);
void    print_person(Person*);

int main(void)
{
    Person* five[5] = {0};
    for (int i = 0; i < 5; i += 1)
        five[i] = create_default_person();
    printf("    Persons:\n");
    for (int i = 0; i < 5; i += 1) print_person(five[i]);
    printf("    Deleting all Persons\n");
    five[0] = erase_person(five[0]);
    for (int i = 0; i < 5; i += 1)
        five[i] = erase_person(five[i]);
    five[0] = create_person("Isac Asimov");
    print_person(five[0]);
    five[0] = erase_person(five[0]);
    return 0;
}

Person* create_default_person()
{
    return create_person("Alfred Hitchcock");
}

Person* erase_person(Person* one)
{
    if (one == NULL) return NULL;
    if (one->name != NULL)
        printf("\tDeleting \"%s\"\n", one->name);
    free(one->name);
    free(one);
    return NULL;
}

Person* create_person(char* name)
{
    Person* one = (Person*)malloc(sizeof(Person));
    if (name == NULL) return one;
    one->name = (char*)malloc(sizeof(strlen(name) + 1));
    if (name == NULL) return one;  // if malloc failed
    strcpy(one->name, name);       // copy all data
    printf("\tAllocating \"%s\"\n", one->name);
    return one;
}

void print_person(Person* input)
{
    if (input == NULL) return;
    printf("\tName: %s\n", input->name);
}

这是输出

        Allocating "Alfred Hitchcock"
        Allocating "Alfred Hitchcock"
        Allocating "Alfred Hitchcock"
        Allocating "Alfred Hitchcock"
        Allocating "Alfred Hitchcock"
    Persons:
        Name: Alfred Hitchcock
        Name: Alfred Hitchcock
        Name: Alfred Hitchcock
        Name: Alfred Hitchcock
        Name: Alfred Hitchcock
    Deleting all Persons
        Deleting "Alfred Hitchcock"
        Deleting "Alfred Hitchcock"
        Deleting "Alfred Hitchcock"
        Deleting "Alfred Hitchcock"
        Deleting "Alfred Hitchcock"
        Allocating "Isac Asimov"
        Name: Isac Asimov
        Deleting "Isac Asimov"

该代码只是创建一个指针数组,用它做一些事情并删除每一个。看看如何使用这些东西可能会有所帮助。Person

从第二个代码

char *get_string() {
  char *item = "Alfred Hitchcock";
  return item;
}

person *create_default_person() {
  char *value = get_string();
  person *new_person = person_generator(value);
  return new_person;
}

我期望当“create_default_person()”函数结束时,它创建的指针会从其内部的堆栈中被销毁,那么结构成员应该指向垃圾数据,对吗?

退货何时结束其生命周期。但是 a 会返回给调用方。该指针指向一个静态字符串文字“Alfred Hitchcock”,它将永远存在。get_string()itemchar*

当结束并到达他们的一生时。但 指针将返回给调用方。该指针指向一个有效的指针,其中有一个指向内部声明的静态数据的指针,该字符串literal用于初始化。create_default_person()valuenew_personPersonstructget_string()item

关于附加问题

这个程序x.c

char* alfred = "alfred";
int main(void){ return 0; }

生成此程序集代码

.LC0:
        .string "alfred"
alfred:
        .quad   .LC0
main:
        push    rbp
        mov     rbp, rsp
        mov     eax, 0
        pop     rbp
        ret

当从此代码生成可执行文件时,指令和字符串的数据肯定会记录到 .当您运行程序时,会加载到内存中,并包含一个区域,一个带有字符串的区域,如您在可执行文件中看到的:x.exex.exex.exestaticdump

0010760    0000    0000    0000    0000    0000    0000    0000    0000
         \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0011020    6c61    7266    6465    0000    0000    0000    0000    0000
          a   l   f   r   e   d  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0011040    ffff    ffff    ffff    ffff    ffff    ffff    ffff    ffff
        377 377 377 377 377 377 377 377 377 377 377 377 377 377 377 377
0011060    0140    0000    0000    0000    0000    0000    0000    0000
          @ 001  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0

您会在 address 上看到一个预期的字符串。当加载到内存中时,字符串将具有一个固定的静态地址,该地址相对于代码加载到由操作系统运行的地址的开头,这将是您可以使用 访问的地址。11020alfred&alfred[0]

更改示例

#include <stdio.h>
#include <string.h>
const char* alfred = "alfred";

int main(void)
{
    // alfred[0] = 'A'; running this crashes the program
    //                  as the area can not be changed.
    //                  Declaring it 'const' makes the
    //                  compiler work for you and do not
    //                  allow the program to run and crash
    printf(
        "    \"alfred\" is at 0x%X. Last byte is %d\n",
         &alfred[0], alfred[strlen(alfred)]);
    const char* p = alfred;
    printf(
        "    pointer points to 0x%X, value there is "
        "\"%s\" Last byte is %d\n",
        p, p, *(p + strlen(p)));
    return 0;
}

代码如下:

  • 打印加载字符串的地址,并显示最后一个字节为 a。null
  • 声明指向此地址的指针,并使用它来打印相同的内容。在第二个 printf 上,您会看到指针算术作为获取字符串最后一个字节的替代方法C
  • 请注意,该区域被声明为 and undestest,这对您有好处,因此编译器不会让您出错并尝试更改静态文本,而是如果以后只是使您的代码崩溃const char*