为什么C语言中的复合字面量是可修改的?

18

通常人们将“不可修改的”与字面量联系起来

char* str = "Hello World!";
*str = 'B';  // Bus Error!

然而,当使用复合文字时,我很快发现它们是完全可修改的(并且查看生成的机器代码,您会看到它们被推送到堆栈上):

char* str = (char[]){"Hello World"};
*str = 'B';  // A-Okay!

我正在使用clang-703.0.29进行编译。这两个示例不应该生成完全相同的机器代码吗?如果可修改,复合字面量仍然是字面量吗?

编辑:一个更短的示例:

"Hello World"[0] = 'B';  // Bus Error!
(char[]){"Hello World"}[0] = 'B';  // Okay!

我甚至不确定它是否是未定义行为,我从未真正查看过官方的语言标准,但在6.5.2.5的第12点中,它说(char[]){"abc"}被设计为可修改的。 - hgiesel
请注意,上面的示例实际上并没有展示lvalue-literal行为(更清晰的示例可能是类似于(int){1} = 2;的东西)-你两个片段之间的主要区别在于第一个中有一个真正的字符串字面量,而在第二个中你使用一个字符串字面量初始化了一个本地数组 -如果你只是修改第一个来读取char str[] = ...,你将会得到相同的行为。 - Alex Celeste
@Leushenko 我需要吗?在复合字面量的情况下,我使用指向其第一个字符的指针初始化 str。当我说 char str[] = … 时,我初始化了一个不可修改的 char 字面量,并将其内容复制到堆栈上的数组 str 中。 - hgiesel
@hgiesel:你说得对,实际上这是一个"匿名对象"。你可以使用 const 修饰符来明确告诉编译器你的意图。需要注意的是,在C语言中,不破坏这个约定是程序员的责任。即使对于字符串字面值,也不能保证写入操作不起作用(并且C明确允许这种实现扩展)。所以,不出现错误并不意味着这是定义良好的行为。如果有疑问,请阅读标准文档。 - too honest for this site
@Olaf 我现在唯一的问题是,当我说 char str[] = (char[]){"Hello"}; 时,我是否实际上在堆栈上初始化了两次。 - hgiesel
2
@hgiesel:不是这样的。1)C语言甚至没有强制使用堆栈(顺便说一下,也没有堆),有一些实现是不用的。2)有一个对象被分配在某个地方,加上字符串字面值来初始化它。但是为什么应该分配两次相同的对象呢?你使用字符串的例子并不好,因为你可以直接使用字面量,但仍然是有效的。 - too honest for this site
2个回答

17

复合字面量是可被赋值的左值,在其元素的值可修改。在以下情况下,

char* str = (char[]){"Hello World"};
*str = 'B';  // A-Okay!  

您正在修改一个合法的复合字面值。

C11-§6.5.2.5/4:

 

如果类型名称指定了未知大小的数组,则大小由初始化列表指定,复合字面值的类型为完成的数组类型。否则(当类型名称指定对象类型时),复合字面量的类型是由类型名称指定的。 在任何情况下,结果都是lvalue

正如可以看到的那样,复合字面值的类型是一个完整的数组类型,并且是lvalue,因此与字符串字面值不同,它是可修改的。

标准还提到:

§6.5.2.5/7:

 

字符串文字和带有const限定类型的复合文字不需要指定不同的对象。101

此外,它说:

 

11 EXAMPLE 4 可以通过以下结构指定只读复合字面量:

(const float []){1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6}   

12例5:下列三个表达式有不同的含义:

"/tmp/fileXXXXXX"
(char []){"/tmp/fileXXXXXX"}
(const char []){"/tmp/fileXXXXXX"}

第一个对象始终具有静态存储期,并且具有char数组类型,但不需要是可修改的; 当它们出现在函数体内时,后两个对象具有自动存储期,并且这两个对象中的第一个对象是可修改的

13 示例6 像字符串字面值一样,带const限定符的复合字面值可以放置在只读内存中,甚至可以共享。例如,

(const char []){"abc"} == "abc"

如果字面值的存储被共享,可能会产生1。


1
请注意,此问题已标记为C99。 - nalzok
@sunqingyao;是的。但对于这种情况,规则几乎与C11相同。 - haccks
我觉得很烦人的是(就我所知),没有const-static复合字面量的语法,因为字符串字面量很少是唯一具有单一使用点的静态数据类型。 - supercat

4
复合字面量语法是一种缩写表达式,相当于一个带有初始化程序的本地声明,后跟对所声明的未命名对象的引用。
char *str = (char[]){ "Hello World" };

等同于:

char __unnamed__[] = { "Hello world" };
char *str = __unnamed__;

__unnamed__ 具有自动存储功能,并且被定义为可修改,可以通过指向它的指针 str 进行修改。

char *str = "Hello World!"; 的情况下,指向的对象不应该被修改。实际上,尝试修改它会导致未定义的行为。

C标准本可以将这样的字符串字面值定义为类型 const char[] 而不是 char[],但这会在遗留代码中生成许多警告和错误。

然而,建议向编译器传递一个标志,使这样的字符串字面值隐式地成为 const,并将整个项目定义为 const 正确,即:将所有用于修改其对象的指针参数定义为 const。对于 gccclang,命令行选项是 -Wwrite-strings。我还强烈建议启用更多的警告并使用 -Wall -W -Werror 将它们变成致命性错误。


对于我写的这段代码来说,很遗憾,“复合字面量语法是等同于本地声明的缩写表达式”并不正确。例如,您可以执行<type> foo [<n>]; 在当前作用域中获得 <n>*sizeof(<type>) 字节的未初始化堆栈内存,但没有办法拥有仅分配而未初始化的复合文字, (<type> [<n>]) {} 是非标准的,大多数编译器将其视为 {0}。如果有方法,请告诉我。这将非常有用。 - user426
@user426:这是一个不同的问题。顺便说一下,你从我的回答中引用的内容是不完整的:我写道*复合字面量语法是一个简写表达式,相当于带有初始化器的本地声明[...] *。如果你想在堆栈上分配一些未初始化的空间,可以在支持它的系统上使用alloca() - chqrlie

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