在 C 语言中,可以像这样在声明中使用字符串字面量:
char s[] = "hello";
或者像这样:
char *s = "hello";
那么它们有什么区别呢?我想知道在编译时和运行时的存储持续时间实际上发生了什么。
在 C 语言中,可以像这样在声明中使用字符串字面量:
char s[] = "hello";
或者像这样:
char *s = "hello";
那么它们有什么区别呢?我想知道在编译时和运行时的存储持续时间实际上发生了什么。
char *s = "Hello world";
将"Hello world"
放置在内存的只读部分,并将s
指向该内存,这将使得对该内存的任何写操作非法。
执行此操作时:
char s[] = "Hello world";
将字面字符串放入只读内存并将该字符串复制到新分配的堆栈内存中。从而使
s[0] = 'J';
合法的。
char msg[] = "hello, world!";
语句的文件中,我可以顺利进行编译,该字符串最终存储在初始化数据段中。但如果将其声明为 char * const
,则会存储在只读数据段中。这是使用gcc-4.5.3时的情况。 - gcbenison首先,在函数参数中,它们是完全等效的:
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";
创建一个对象 - 大小为6的char
数组,名为s
,初始化为'h','e','l','l','o','\0'
。该数组在内存中的分配位置和生命周期取决于声明出现的位置。如果声明在函数内部,则它将存在于声明所在块的末尾,并且几乎肯定会在堆栈上分配;如果声明在函数外部,则它可能存储在“初始化数据段”中,该数据段在程序运行时从可执行文件加载到可写内存中。
另一方面,这个声明:
char *s ="hello";
创建两个对象:
'h','e','l','l','o','\ 0'
这6个字符的只读数组,没有名称,并具有静态存储期(这意味着它在整个程序的生命周期内都存在);以及s
,它被初始化为那个未命名的只读数组中第一个字符的位置。未命名的只读数组通常位于程序的“text”段中,这意味着它与代码本身一起从磁盘加载到只读内存中。指针变量s
在内存中的位置取决于声明出现的位置(就像第一个示例中一样)。
char s[] = "hello"
的情况下,"hello"
只是一个初始化器,告诉编译器如何初始化数组。它可能会导致相应的字符串出现在文本段中,也可能不会 - 例如,如果 s
具有静态存储期,则 "hello"
的唯一实例很可能在已初始化数据段中 - 对象 s
本身。即使 s
具有自动存储期,它也可以通过一系列字面量存储来初始化,而不是通过复制(例如 movl $1819043176, -6(%ebp); movw $111, -2(%ebp)
)。 - cafchar s[] = "Hello world";
会将字面字符串放入只读内存,并将字符串复制到栈上新分配的内存中。但是,你的答案只涉及将字面字符串放入只读内存,并跳过了句子的第二部分,即“将字符串复制到栈上新分配的内存中”。因此,你的答案是否不完整,因为没有指定第二部分? - ajaysinghnegichar s[] = "Hellow world";
中的字符串只是一个初始化器,并不一定会作为单独的只读副本存储。如果s
具有静态存储期,则该字符串的唯一副本可能位于S
位置的可读写段中,即使不是这样,编译器也可以选择使用load-immediate指令或类似指令来初始化数组,而不是从只读字符串复制。重点是,在这种情况下,初始化程序字符串本身没有运行时存在。 - caf考虑如下变量声明:
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"
是一个由12个char
元素(在C++中是const char
)组成的数组,具有静态存储期,这意味着在程序启动时分配了它的内存并一直保留到程序终止。试图修改字符串字面值的内容会导致未定义行为。
下面这行代码:
char *s0 = "hello world";
将s0
定义为自动存储期(意味着变量s0
仅在声明它的作用域内存在)的指向char
的指针,并将字符串字面值(例如此示例中的0x00008000
)的地址复制到它中。请注意,由于s0
指向字符串字面值,因此不应将其用作尝试修改它的任何函数的参数(例如strtok()
、strcat()
、strcpy()
等)。
该行
char s1[] = "hello world";
定义s1
为char
的12元素数组(长度从字符串文字中获得),其自动存储持续时间,并将文字内容复制到数组中。从内存映射中可以看出,我们有两个字符串"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
重新指向不同的数组。
0x01 0x02 0x03 0x04
而是0x00 0x01 0x02 0x03
吗?否则看起来s0
指向0x00008000,但第一个字母在0x00008001。同样,不清楚0x00008004是第二个“l”的地址还是“o”的地址。 - Fabio says Reinstate MonicaC99 N1256 草案
字符串字面量有两种不同的用法:
初始化 char[]
:
char c[] = "abc";
这是“更多的魔法”,并在6.7.8/14“初始化”中描述:
字符类型的数组可以通过字符字符串文字进行初始化,可选地包含在大括号中。 字符串文字的连续字符(包括终止空字符,如果有空间或数组大小未知)初始化数组的元素。
所以这只是一个快捷方式:
char c[] = {'a', 'b', 'c', '\0'};
像任何其他常规数组一样,c
可以被修改。
在其他任何地方:它生成一个:
因此,当您编写:
char *c = "abc";
这类似于:
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
请注意从char []
到char *
的隐式转换,这始终是合法的。
然后,如果您修改c [0]
,则还会修改__unnamed
,这是UB。
这在6.4.5“字符串文字”中有记录:
5 在第7个翻译阶段,将值为零的字节或代码附加到由字符串文字或文字产生的每个多字节字符序列。 然后使用多字节字符序列初始化具有静态存储期和长度的数组,该数组刚好足以包含该序列。 对于字符字符串文字,数组元素具有char类型,并使用多字节字符序列的各个字节进行初始化[...]
6 未指定这些数组是否不同,只要它们的元素具有适当的值即可。 如果程序尝试修改此类数组,则行为是未定义的。
6.7.8/32 "Initialization" 给出了一个直接的例子:
EXAMPLE 8: The declaration
char s[] = "abc", t[3] = "abc";
defines "plain" char array objects
s
andt
whose elements are initialized with character string literals.This declaration is identical to
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
The contents of the arrays are modifiable. On the other hand, the declaration
char *p = "abc";
defines
p
with type "pointer to char" and initializes it to point to an object with type "array of char" with length 4 whose elements are initialized with a character string literal. If an attempt is made to usep
to modify the contents of the array, the behavior is undefined.
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
char*
存储在.rodata
部分,而不是.text
部分。.rodata
和.text
放在同一个段中,该段具有执行权限但没有写入权限。可以通过以下方式观察到这一点: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
),并通过将给定字符串文字的成员复制到数组中来初始化该数组。
char *s = "hello";
声明指针s
指向一个或多个(在此情况下为多个)char
,并直接将它指向包含文字"hello"
的固定(只读)位置。s
都是指向const char
的指针。 - CB Baileychar s[] = "Hello world";
这里,s
是字符数组,如果需要,可以进行覆盖操作。
char *s = "hello";
字符串字面值用于在内存中创建这些字符块,该指针 s
指向此处。我们可以通过更改它指向的对象来重新分配,但只要它指向一个字符串字面值,它所指向的字符块就不能被更改。
[]
或*(<var> + <index>)
来索引一个字符。格式:printf("%c", x[1]); //Prints r
printf("%c", *(x + 1)); //Prints r
*(x + 1) = 'a';
x[1] = 'a';
没有任何区别,后者也会导致段错误(当然,这取决于平台)。 - glglglprintf("hello" + 2); //llo
char a[] = "hello" + 2; //error
printf("sizeof s[] = %zu\n", sizeof(s)); //6
printf("sizeof *s = %zu\n", sizeof(s)); //4 or 8
'\0'
将被分配为最后一个元素。