当字符数组从字符串字面值初始化时会发生什么?

5
据我理解,下面的代码起到以下作用:
char* cptr = "Hello World";

"

\"Hello World\" 存储在程序内存的 .rodata 部分。字符串字面量\"Hello World\" 返回一个指向该字符串基地址的指针,或者所谓“数组”的第一个元素的地址,因为字符在内存中是按顺序排列的,所以它将是 'H'。这是我想象中字符串字面量存储在内存中的小图示:

"
0x4 : 'H'
0x5 : 'e'
0x6 : 'l'
0x6 : 'l'
0x7 : 'o'
0x8 : ' '
0x9 : 'W'
0xa : 'o'
0xb : 'r'
0xc : 'l'
0xd : 'd'
0xe : '\0'

因此,上面的声明变为:
char* cptr = 0x4;

现在cptr指向字符串文字。我只是编造地址。
0xa1 : 0x4

现在我们来看一下这段代码是如何工作的?
char cString[] = "Hello World";

我假设在之前的情况下,"Hello World" 也会退化成 'H' 和 0x4 的地址。

char cString[] = 0x4;

我将“=”翻译为重载赋值运算符,当它与char数组的初始化一起使用时。我理解,仅在C字符串初始化时,它会从给定基地址开始逐个复制字符到C字符串,直到复制最后一个字符'\0'为止。它还为所有字符分配足够的内存。由于重载运算符实际上只是函数,我认为它的内部实现类似于strcpy()。
我希望有经验的C程序员能确认我的代码如何工作的假设。这是我将字符串文字中的字符复制到C字符串后对C字符串的可视化:
0xb4 : 'H'
0xb5 : 'e'
0xb6 : 'l'
0xb6 : 'l'
0xb7 : 'o'
0xb8 : ' '
0xb9 : 'W'
0xba : 'o'
0xbb : 'r'
0xbc : 'l'
0xbd : 'd'
0xbe : '\0'

再次强调,地址是任意的,重点是堆栈中的C字符串与内存中.rodata部分中的字符串字面量不同。

我想做什么?我试图使用char指针暂时保存字符串字面量的基地址,并使用相同的char指针(字符串字面量的基地址)来初始化C字符串。

char* cptr = "Hello World";
char cString[] = cptr;

我假设"Hello World"会被评估为它的基础地址0x4。因此,这段代码应该是这样的:
char* cptr = 0x4;
char cString[] = 0x4;

我认为这应该与char cString[] = "Hello World";没有区别,因为"Hello World"会被解释为其基础地址,并且这就是存储在char指针中的内容!但是,gcc却给了我一个错误:
error: invalid initializer
char cString[] = cptr;
                 ^
  1. 为什么不能使用char指针作为临时占位符来存储字符串字面值的基地址?
  2. 这段代码如何工作?我的假设正确吗?
  3. 在代码中使用字符串字面值会将存储在内存中的字符的“数组”基地址返回吗?

数组不是指针。在 Stack Overflow 和互联网上有很多关于这个问题的解释。是的,你可以分配一个指针,使其指向一个数组。但是你不能分配一个数组,使其指向任何东西(因为它不是一个指针)。初始化char cString[] = "Hello World"不涉及任何指针——符号cString最终成为一个由“Hello World”中的字符初始化的包含12个字符的数组的地址。 - Steve Summit
你把“所谓的‘数组’”写得好像有什么可疑的地方一样。实际上并没有。在C语言中,“数组”这个术语已经被明确定义了,标准明确规定将每个字符串字面量的结尾添加一个空字符,并使用生成的字节序列初始化静态数组,以使其足够长。没有“好像”或“类似”的说法——在C源代码中出现的字符串字面量对应于程序中的真正的数组。 - John Bollinger
我不理解“真实的”这个词。我的观点是字符串文字似乎是存储在程序的.rodata部分中的数组。但是,它不是一个程序员在代码中明确声明的数组:char array[]; - Galaxy
3个回答

6
您对内存布局的理解基本正确。但您遇到的问题是C语言中的初始化语义问题。
在这里,声明中的“=”符号并不是赋值运算符。相反,它是指定正在实例化的变量的初始化器的语法。通常情况下,T x = y;T x; x = y;不同。
有一条语言规则,字符数组可以从字符串字面值初始化。(在这种情况下,字符串字面值不会“求其基地址”)。但是,并没有一条语言规则指出数组可以从指向要复制到数组中的元素的指针进行初始化。
为什么规则是这样的?因为“历史原因”。

3
第二个定义 char cString[] = "Hello World"; 是这个等价定义的简写形式:
char cString[12] = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0' };

如果这个定义是在全局范围内或具有静态存储,cString将位于可执行映像中的.data段中,并保留初始内容。如果它出现在具有自动存储的函数作用域内,编译器将为数组分配自动存储(在堆栈帧上或等效地保留空间),并生成代码以在运行时执行初始化。

在抽象机中,有一个字符串字面量被复制到cString中。就可观察行为而言,你提供的这两个代码是相同的。实现它的选项可以描述为优化问题。 - M.M
1
@M.M:确实,但复制字符串字面值有些模糊,它并没有精确地描述另一个例子char cString[12] = { "Hello" };等同于char cString[12] = { 'H','e','l','l','o','\0','\0','\0','\0','\0','\0','\0' };,而不是char cString[12]; strcpy(cString, "Hello"); - chqrlie

3
我假设与之前的情况一样,"Hello World" 也会降级为地址 'H'0x4

不是的:cString[] 在内存中得到了一个全新的地址。编译器将12个字符分配给它,并用 "Hello World" 字符串字面值中的内容进行初始化。

我假设 "Hello World" 会计算出其基地址 0x4。在代码中使用字符串字面值是否会将基地址返回到存储字符数组的内存中?

cString 可以在后面被转换为 char*,从而得到其基地址,但在常规上下文中它仍然是一个数组。特别地,如果调用 sizeof(cString),则会得到数组的大小而非指针的大小。

为什么不能使用 char 指针作为临时占位符来存储字符串字面值的基地址呢?

可以这样做。然而,一旦将字符串字面值赋值给 char *,它就不再是字符串字面值,至少在编译器看来是这样。它变成了一个 char * 指针,与其他 char * 指针没有区别。

请注意,现代 C 编译器将相同的字符串字面值组合在一起作为优化,因此如果您写入:

#define HELLO_WORLD "Hello World"
...
char* cptr = HELLO_WORLD;
char cString[] = HELLO_WORLD;

当开启优化并打开编译器时,编译器会消除字符串文字的重复副本。

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