在 C 中,可以在声明中使用字符串文字,如下所示:
char s[] = "hello";
或像这样:
char *s = "hello";
那么区别是什么呢?我想知道在存储持续时间方面实际发生了什么,无论是在编译时还是运行时。
这里的区别在于
char *s = "Hello world";
会将 "Hello world"
放置在内存的只读部分中,并使 s
成为指向该指针的指针,从而使对该内存的任何写入操作都是非法的。
在做的时候:
char s[] = "Hello world";
将文字字符串放入只读内存并将字符串复制到堆栈上新分配的内存。从而使
s[0] = 'J';
合法的。
首先,在函数参数中,它们是完全等价的:
void foo(char *x);
void foo(char x[]); // exactly the same in all respects
在其他情况下,char *
分配一个指针,而 char []
分配一个数组。你问,在前一种情况下,字符串在哪里?编译器秘密分配一个静态匿名数组来保存字符串字面量。所以:
char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;
请注意,您绝不能尝试通过此指针修改此匿名数组的内容;效果未定义(通常意味着崩溃):
x[1] = 'O'; // BAD. DON'T DO THIS.
使用数组语法直接将其分配到新内存中。因此修改是安全的:
char x[] = "Foo";
x[1] = 'O'; // No problem.
但是,该数组仅在其包含范围内存在,因此,如果您在函数中执行此操作,请不要返回或泄漏指向该数组的指针 - 使用 strdup()
或类似内容创建副本。如果数组是在全局范围内分配的,当然没问题。
本声明:
char s[] = "hello";
创建 one 对象 - 一个名为 s
的大小为 6 的 char
数组,使用值 'h', 'e', 'l', 'l', 'o', '\0'
进行初始化。该数组在内存中的分配位置以及它的生存时间取决于声明出现的位置。如果声明在一个函数中,它将一直存在到声明它的块的末尾,并且几乎肯定会在堆栈上分配;如果它在函数之外,它可能存储在“初始化数据段”中,当程序运行时,该数据段从可执行文件加载到可写内存中。
另一方面,这个声明:
char *s ="hello";
创建两个对象:
包含值 'h'、'e'、'l'、'l'、'o'、'\0' 的 6 个字符的只读数组,它没有名称并且具有静态存储持续时间(意味着它存在在程序的整个生命周期内);和
一个指向字符类型的变量,称为 s,它使用该未命名的只读数组中的第一个字符的位置进行初始化。
未命名的只读数组通常位于程序的“文本”段中,这意味着它与代码本身一起从磁盘加载到只读内存中。 s
指针变量在内存中的位置取决于声明出现的位置(就像在第一个示例中一样)。
char s[] = "hello"
的情况下, "hello"
只是一个初始化器,告诉编译器应该如何初始化数组。它可能会或可能不会在文本段中产生相应的字符串 - 例如,如果 s
具有静态存储持续时间,那么 "hello"
的唯一实例可能会在初始化的数据段中 - 对象 {3 } 本身。即使 s
具有自动存储持续时间,它也可以通过一系列文字存储而不是副本来初始化(例如 movl $1819043176, -6(%ebp); movw $111, -2(%ebp)
)。
char s[] = "Hello world";
将文字字符串放入只读内存并将字符串复制到堆栈上新分配的内存。但是,您的答案仅涉及放入只读存储器的文字字符串,并跳过了句子的第二部分:copies the string to newly allocated memory on the stack
。那么,由于没有指定第二部分,您的答案是否不完整?
char s[] = "Hellow world";
中的字符串只是一个初始化程序,根本不一定存储为单独的只读副本。如果 s
具有静态存储持续时间,则字符串的唯一副本可能位于 s
位置的读写段中,即使不是,编译器也可能选择使用 load-immediate 初始化数组指令或类似的,而不是从只读字符串复制。关键是在这种情况下,初始化字符串本身不存在运行时。
鉴于声明
char *s0 = "hello world";
char s1[] = "hello world";
假设以下假设的内存映射(列表示从给定行地址偏移 0 到 3 处的字符,因此例如右下角的 0x00
位于地址 0x0001000C + 3
= 0x0001000F
):
+0 +1 +2 +3 0x00008000: 'h' 'e' 'l' 'l' 0x00008004: 'o' ' ' 'w' 'o' 0x00008008: 'r' 'l' 'd' 0x00 ... s0: 0x00010000: 0x00 0x00 0x80 0x00 s1: 0x00010004: 'h' 'e' 'l' 'l' 0x00010008: 'o' ' ' 'w' 'o' 0x0001000C: 'r' 'l' 'd' 0x00
字符串字面量 "hello world"
是具有静态存储持续时间的 char
(C++ 中的 const char
)的 12 元素数组,这意味着它的内存在程序启动时分配并保持分配状态直到程序终止。尝试修改字符串文字的内容会调用未定义的行为。
线
char *s0 = "hello world";
将 s0
定义为指向具有自动存储持续时间的 char
的指针(意味着变量 s0
仅在声明它的范围内存在)并复制字符串文字的 地址 ({ 4} 在这个例子中)。请注意,由于 s0
指向字符串文字,因此不应将其用作任何尝试修改它的函数的参数(例如,strtok()
、strcat()
、strcpy()
等)。
线
char s1[] = "hello world";
将 s1
定义为具有自动存储持续时间的 char
(长度取自字符串文字)的 12 元素数组,并将文字的 contents 复制到数组中。从内存映射中可以看出,我们有两个字符串 "hello world"
副本;不同之处在于您可以修改 s1
中包含的字符串。
s0
和 s1
在大多数情况下可以互换;以下是例外情况:
sizeof s0 == sizeof (char*)
sizeof s1 == 12
type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char
您可以重新分配变量 s0
以指向不同的字符串文字或另一个变量。您不能重新分配变量 s1
以指向不同的数组。
0x00 0x01 0x02 0x03
而不是 0x01 0x02 0x03 0x04
?否则看起来 s0
指向 0x00008000,但第一个字母位于 0x00008001。同样,不清楚 0x00008004 是第二个“l”还是“o”的地址。
C99 N1256 草案
字符串字面量有两种不同的用法:
初始化 char[]: char c[] = "abc";这是“更神奇”,并在 6.7.8/14“初始化”中进行了描述:字符类型的数组可以由字符串字面量初始化,可选地用大括号括起来。字符串文字的连续字符(如果有空间或数组大小未知,则包括终止的空字符)初始化数组的元素。所以这只是一个快捷方式: char c[] = {'a', 'b', 'c', '\0'};像任何其他常规数组一样,可以修改 c。其他任何地方:它生成一个:未命名的 char 数组 C 和 C++ 中的字符串文字类型是什么?使用静态存储,如果修改会提供 UB 所以当你写的时候: char *c = "abc";这类似于: /* __unnamed 很神奇,因为修改它会得到 UB。 */ 静态字符 __unnamed[] = "abc";字符 *c = __unnamed;请注意从 char[] 到 char * 的隐式转换,这始终是合法的。那么如果你修改c[0],你也修改了__unnamed,也就是UB。这在 6.4.5 “字符串文字”中有记录: 5 在翻译阶段 7 中,将一个字节或值为零的代码附加到由一个或多个字符串文字产生的每个多字节字符序列。然后使用多字节字符序列来初始化一个静态存储持续时间和长度刚好足以包含该序列的数组。对于字符串文字,数组元素具有 char 类型,并使用多字节字符序列的各个字节进行初始化 [...] 6 如果它们的元素具有适当的值,则未指定这些数组是否不同。如果程序尝试修改这样的数组,则行为未定义。
6.7.8/32“初始化”给出了一个直接的例子:
例 8:声明 char s[] = "abc", t[3] = "abc";定义“普通”字符数组对象 s 和 t,其元素用字符串字面量初始化。这个声明等同于 char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };数组的内容是可修改的。另一方面,声明 char *p = "abc";定义 p 类型为“pointer to char”,并将其初始化为指向长度为 4 的类型为“char 数组”的对象,其元素使用字符串字面量进行初始化。如果尝试使用 p 修改数组的内容,则行为未定义。
GCC 4.8 x86-64 ELF 实现
程序:
#include <stdio.h>
int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}
编译和反编译:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
输出包含:
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
结论:GCC 将 char*
存储在 .rodata
部分,而不是 .text
。
但是请注意,默认链接描述文件将 .rodata
和 .text
放在同一个 segment 中,它具有执行但没有写入权限。这可以通过以下方式观察到:
readelf -l a.out
其中包含:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
如果我们对 char[]
做同样的事情:
char s[] = "abc";
我们获得:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
所以它被存储在堆栈中(相对于 %rbp
)。
char s[] = "hello";
将 s
声明为 char
的数组,该数组的长度足以容纳初始化程序 (5 + 1 char
s),并通过将给定字符串文字的成员复制到数组中来初始化数组。
char *s = "hello";
将 s
声明为指向一个或多个(在本例中为多个)char
的指针,并将其直接指向包含文字 "hello"
的固定(只读)位置。
s
都是指向 const char
的指针。
char s[] = "Hello world";
这里,s
是一个字符数组,如果我们愿意,可以覆盖它。
char *s = "hello";
字符串文字用于在该指针 s
指向的内存中的某处创建这些字符块。我们可以在这里通过改变它重新分配它所指向的对象,但是只要它指向一个字符串字面量,它所指向的字符块就不能改变。
另外,考虑到对于只读目的,两者的使用是相同的,您可以通过使用 []
或 *(<var> + <index>)
格式索引来访问 char:
printf("%c", x[1]); //Prints r
和:
printf("%c", *(x + 1)); //Prints r
显然,如果你试图做
*(x + 1) = 'a';
当您尝试访问只读内存时,您可能会遇到分段错误。
x[1] = 'a';
没有任何不同,后者也会出现段错误(当然,取决于平台)。
只是要补充一点:您还可以为它们的大小获得不同的值。
printf("sizeof s[] = %zu\n", sizeof(s)); //6
printf("sizeof *s = %zu\n", sizeof(s)); //4 or 8
如上所述,对于数组 '\0'
将被分配为最终元素。
char *str = "Hello";
上面将 str 设置为指向在程序的二进制图像中硬编码的字面值“Hello”,在内存中被标记为只读,这意味着此 String 字面量的任何更改都是非法的,并且会引发分段错误。
char str[] = "Hello";
将字符串复制到堆栈上新分配的内存。因此,对其进行任何更改都是允许且合法的。
means str[0] = 'M';
将 str 更改为“Mello”。
有关更多详细信息,请查看类似问题:
差异的一个例子:
printf("hello" + 2); //llo
char a[] = "hello" + 2; //error
在第一种情况下,指针算法正在工作(传递给函数的数组衰减为指针)。
char *s1 = "Hello world"; // Points to fixed character string which is not allowed to modify
char s2[] = "Hello world"; // As good as fixed array of characters in string so allowed to modify
// s1[0] = 'J'; // Illegal
s2[0] = 'J'; // Legal
如果是:
char *x = "fred";
x 是一个 lvalue - 它可以被分配给。但在以下情况下:
char x[] = "fred";
不是左值,它是右值——你不能分配给它。
x
是不可修改的左值。不过,在几乎所有情况下,它都会计算出指向其第一个元素的指针,并且 that 值是一个右值。
根据这里的评论,很明显: char * s = "hello" ;是个坏主意,应该在非常狭窄的范围内使用。
这可能是指出“const 正确性”是“好事”的好机会。无论何时何地您都可以使用“const”关键字来保护您的代码,使其免受“放松”的调用者或程序员的影响,这些调用者或程序员通常在指针发挥作用时最“放松”。
足够的情节剧,这是用“const”装饰指针时可以达到的效果。 (注意:必须从右到左阅读指针声明。)以下是在玩指针时保护自己的 3 种不同方法:
const DBJ* p means "p points to a DBJ that is const"
— 即不能通过 p 更改 DBJ 对象。
DBJ* const p means "p is a const pointer to a DBJ"
——也就是说,你可以通过 p 改变 DBJ 对象,但不能改变指针 p 本身。
const DBJ* const p means "p is a const pointer to a const DBJ"
——也就是说,你不能改变指针p本身,也不能通过p改变DBJ对象。
与尝试的 const-ant 突变相关的错误在编译时被捕获。 const 没有运行时空间或速度损失。
(假设您使用的是 C++ 编译器,当然?)
--DBJ
"Hello world"
都位于“内存的只读部分”中。数组 points 的示例在那里,数组的示例 copy 将字符复制到数组元素。char msg[] = "hello, world!";
的文件进行了干净的编译,该字符串最终出现在初始化的数据部分中。当声明char * const
以在只读数据部分结束时。 gcc-4.5.3