何时为char *分配内存?

22

当我想要为一个char *分配内存时,我有点困惑该何时将它指向一个const字符串。

是的,如果我希望修改字符串,我需要为其分配内存。

但是在我不希望修改所指向的字符串并且只需要传递值的情况下,我是否可以像下面这样做?与使用malloc分配内存相比,以下步骤有什么缺点?

char *str = NULL;

str = "This is a test";

str = "Now I am pointing here";

这些字符串在可执行映像的数据部分的只读部分在编译时分配。在这种情况下,无需动态分配内存(在运行时)。 - barak manos
1
你要开始使用-Wextra-Wall,否则我会找到你并杀了你。 - Alec Teal
1
@barakmanos:如何在编译时“分配”一些东西? - dhein
@Zaibis:你可以让编译器为你完成。 - barak manos
@barakmanos:不,编译器并不是分配内存的工具。编译器只是一个翻译单元。编译器将您的代码转换为机器可理解的形式。但是,分配内存的人是计算单元,也就是您的CPU。编译器所做的一切只是创建二进制文件,这些文件可以被它知道需要分配内存的单元理解。如果编译器分配了内存,那么编译后的二进制文件可能包含RAM,这是自相矛盾的。 - dhein
显示剩余5条评论
7个回答

21

让我们再尝试一下您的示例,使用-Wwrite-strings编译器警告标志,您将会看到一个警告:

warning: initialization discards 'const' qualifier from pointer target type

这是因为“这是一个测试”的类型是const char *,而不是char *。因此,当您将文字地址分配给指针时,会丢失常量信息。
由于历史原因,编译器允许在非常量变量中存储字符串字面值,但这是一种不良行为,我建议您始终使用-Wwrite-strings
如果您想亲自证明它,请尝试修改该字符串:
char *str = "foo";
str[0] = 'a';

这个程序的行为是未定义的,但在许多系统上可能会看到分段错误。 使用 Valgrind 运行此示例,您会看到以下内容:
Process terminating with default action of signal 11 (SIGSEGV)
  Bad permissions for mapped region at address 0x4005E4

问题在于编译器生成的二进制代码会将字符串字面量存储在只读内存位置。试图在其中写入会导致段错误

重要的是要理解,你正在处理两个不同的系统:

  1. C类型系统用于帮助你编写正确的代码,并且可以很容易地进行“静音”(通过强制转换等)

  2. 内核内存页面权限用于保护您的系统,并且必须始终受到尊重。

同样,由于历史原因,这是1和2不一致的一个点。更明确地说,1比2宽松得多(导致内核终止了您的程序)。

所以不要被编译器欺骗,你声明的字符串字面量是真正的常量,你无法改变它!

考虑到您的指针str的读写是正确的。但是,为了编写正确的代码,应该使用const char *而不是char *。通过以下更改,您的示例将成为有效的C代码:

const char *str = "some string";
str = "some other string";

(const char *指向一个常量字符串的指针)

在这种情况下,编译器不会发出任何警告。一旦代码执行后,你所写的内容和内存中的内容将匹配。

注意:指向常量字符串的常量指针为const char *const

const char *const str = "foo";

手指头的规则是:尽可能保持恒定不变。
如果需要修改字符串,请使用动态分配(malloc()或更好的一些高级字符串操作函数,例如来自libc的strdup等),如果不需要,请使用字符串字面值。

2
否则回答很好,但我认为证明部分是误导性的。写入字面量是未定义行为,并且您不一定在所有系统上都会得到segfault。在C中,您永远不能信任测试,因为UB不能保证始终不正确地工作。您需要知道代码是正确的。这就是为什么在使用C时启用所有编译器警告非常重要的原因。 - user694733
1
@Andrew稍微修改了一下答案,但如果你在C语言中进行字符串操作,无论如何都必须注意自己在做什么;) - Thomas Moulard
@user694733 修改了答案,指出这是未定义行为。毕竟,在现今的认知中,在拥有读写权限的页面上加载字符串字面量会被视为安全漏洞... - Thomas Moulard
1
我们的进程禁止使用malloc()函数。 - tehnyit
1
@tehnyit:我完全理解并同意这一点。正如我已经在聊天中与安德鲁讨论过的那样,我没有看到原帖或回答与嵌入式世界有关的地方,所以只是读到人们建议尽量避免它。但是有些工作(即使计划得很好)也无法在没有它们的情况下完成。例如,在规划视频游戏的开发时,您无法真正了解每个场景中将有多少顶点要渲染和存储的精度。因此,我只是看到有人建议避免一些不必在每种情况下都避免的事情。明白我的意思吗? - dhein
显示剩余16条评论

5
如果您知道str将始终是只读的,为什么不将其声明为只读?
char const * str = NULL;
/* OR */
const char * str = NULL;

其实,有一个原因可能会让这个过程变得困难 - 当你将字符串传递给一个没有声明自己为只读函数的函数时。假设你正在使用一个声明了这个函数的外部库:

int countLettersInString(char c, char * str);
/* returns the number of times `c` occurs in `str`, or -1 if `str` is NULL. */

这个函数有很好的文档说明,你知道它不会尝试更改字符串str - 但是如果你用一个常量字符串调用它,你的编译器可能会给你一个警告!你知道这样做没有什么危险,但是你的编译器并不知道。

为什么?因为就编译器而言,也许这个函数确实尝试修改字符串的内容,这会导致程序崩溃。也许你非常依赖这个库,并且有许多函数都像这样行事。那么也许最好在第一次声明字符串时不要将其声明为const,但是接下来你需要确保不要尝试修改它。

另一方面,如果你是编写countLettersInString函数的人,则只需通过使用const声明来确保编译器知道你不会修改字符串:

int countLettersInString(char c, char const * str);

这样它将能够无障碍地接受常量和非常量字符串。


4
使用字符串字面量的一个缺点是它们有长度限制。因此,你应该记住来自ISO/IEC:9899文档的以下内容(我强调):
5.2.4.1 翻译限制 1 实现应能够翻译和执行至少包含以下每个限制中至少一个实例的程序: [...] — 字符串字面量或宽字符串字面量中的4095个字符(在连接后)
因此,如果你的常量文本超过这个数量(有时可能会出现这种情况,特别是如果你在C中编写动态Web服务器),如果你想保持系统独立性,你就不能使用字符串字面量方法。

什么???4095个字符的限制是针对单个字符串常量的,而不是所有常量的总和。这与问题有什么关系? - Tonny
@Tonny:我何时说过不同的话?我的意思是,我引用了它只适用于单个字符字符串文字,你为什么要告诉我呢?但是试着构建一个包含2或3个常量部分的复杂动态HTML页面,将它们连接成一个HTML文件。我敢打赌,如果你尊重这个限制,你不会走得太远!那么它与问题有什么关系呢?嗯,他问的是与malloc相比的缺点是什么。其中之一是,如果你不能遵守这个限制(在某些工作空间中很可能),你不能使用文字概念而不是malloc来保持系统独立性。就是这样。 - dhein
现在我理解了你的意图,你说得很有道理。但是从你的回答本身来看并不那么清晰。(我更关注代码中的语法问题而不是大小限制)。如果我没有“领会”它,那么还会有很多其他人也错过了它。 - Tonny
@Tonny:你能解释一下这个误解是怎么产生的吗?这样我就可以把它编辑掉了。对我来说,听起来很清楚,我不知道我应该改变什么来防止这种情况。 - dhein
我会在开头加上一行:“使用const字符串的一个缺点是它们有长度限制”。然而,我仍然不太清楚这在实践中有多大的影响:如果您需要非常长的字符串,您必须动态分配它们,但您仍然需要从某个地方填充这些字符串内容。根据定义,这不能是源代码中的另一个const字符串。您将从文件或其他资源动态加载它们。 - Tonny
@Tonny 谢谢,我会加上的。那么,这与我所说的有何冲突?也许我表述得太过严厉了,你所写的正是我想要表达的。我既不赞成使用动态内存,也不赞成通常的字符串字面值(因为两者都有情境上的优缺点)。我只是想提出这个限制,因为之前没有人提到过它。 - dhein

3
只要不打算修改该字符串的内容,您的代码就没有问题。另外,这种字符串字面值的内存会在程序的整个生命周期中保留。使用malloc分配的内存是可读可写的,因此您可以操作该内存的内容。

2

如果您有一个不想修改的字符串字面量,那么您正在做的是正确的:

char *str = NULL;
str = "This is a test";
str = "Now I am pointing here";

在这里,指针str指向一块内存。在第二行,您将"This is a test"写入该内存中,然后在第三行再次写入"Now I am pointing here"。这在C语言中是合法的。请注意,保留了HTML标签。

您可能会觉得有些矛盾,但是您无法修改以下形式的字符串:

str[0]='X' // will give a problem.

然而,如果你想修改它,或者将其用作缓冲区来保存输入行等,那么请使用malloc

char *str=malloc(BUFSIZE);   // BUFSIZE size what you want to allocate
free(str);                   // freeing memory

在编译时不知道需要多少内存的情况下,请使用malloc()


如果你真的,真的必须使用malloc(),请确保你验证返回值(malloc在错误时返回NULL)和errno的状态。 - Andrew
@Andrew 为什么你建议检查 malloc() 错误? - mucaho
如果malloc()失败,它会返回NULL - 这意味着你没有分配内存。不检查返回状态是动态内存分配的最大问题之一! - Andrew
@Andrew 当动态内存分配失败时,你有哪些选择?使用 exit(EXIT_FAILURE) 退出应用程序吗?这是我无法想象的事情。你是否遇到过 malloc 失败的情况? - mucaho
1
@mucaho - malloc()和任何其他动态内存分配都可能失败...至于您的选项-您应始终检查指针是否为NULL并采取适当的措施...那将取决于您的设计。退出程序是“如果所有其他方法都失败”的操作,对于嵌入式应用程序而言不太合适! - Andrew

1
它将正常运行,但编译器可能会发出警告,因为您正在指向一个常量字符串。
附注:仅当您不修改它时才能正常运行。因此,不使用malloc的唯一缺点是无法修改它。

1

很不幸,在C语言中这是合法的,但是任何试图通过指针修改字符串字面量的尝试都会导致未定义行为。

比如说

str[0] = 'Y'; //No compiler error, undefined behavior

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接