char s[]和char *s有什么区别?

581

在 C 语言中,可以像这样在声明中使用字符串字面量:

char s[] = "hello";

或者像这样:

char *s = "hello";

那么它们有什么区别呢?我想知道在编译时和运行时的存储持续时间实际上发生了什么。


12
好的,我会尽力以最简单明了的方式翻译这些内容。下面是需要翻译的网页:http://c-faq.com/aryptr/index.html这个网页解释了指针和数组在C语言中的基本概念和用法,包括如何声明、初始化、访问和释放内存等。http://c-faq.com/charstring/index.html这个网页解释了C语言中字符和字符串的基本概念和用法,包括如何声明、初始化、赋值、比较、连接和格式化输出等。 - Sinan Ünür
12
char *s = "hello",这里的 s 在运行时可以指向另一个字符串,我指的是它不是常量指针,您可以在运行时分配另一个值 p = "Nishant"。而对于 s[],这里的 s 是一个常量指针,不能重新分配另一个字符串,但我们可以在 s[index] 处分配另一个字符值。 - Nishant Kumar
@Nishant 你的意思是..你可以在运行时分配另一个值给 s = "Nishant",而 s[] ... 你是这个意思吗?否则,在上下文中有点令人困惑。 - Yifangt
14个回答

613
这里的区别在于:
char *s = "Hello world";

"Hello world"放置在内存的只读部分,并将s指向该内存,这将使得对该内存的任何写操作非法。

执行此操作时:

char s[] = "Hello world";

将字面字符串放入只读内存并将该字符串复制到新分配的堆栈内存中。从而使

s[0] = 'J';

合法的。


33
两个例子中,字面字符串 "Hello world" 存储在内存的“只读区域”。对于使用数组 points 的例子,该字符串仅被引用;而对于使用数组 copies 的例子,该字符串则被复制到了数组元素中。 - pmg
38
在第二种情况下,字面字符串并不一定作为单个连续对象存在于内存中,它只是一个初始化器。编译器可以合理地发出一系列“加载立即字节”指令,其中包含嵌入它们的字符值。 - caf
15
字符数组的示例不一定将字符串放在堆栈上 - 如果它出现在文件级别,那么它很可能会在某种初始化的数据段中。 - caf
11
我想指出 char s = "xx" 不一定要存储在只读内存中(例如,一些实现没有MMU)。n1362 c1x 草案只是陈述修改这样的数组将导致未定义的行为。无论如何加分,因为依赖这种行为是一件愚蠢的事情。 - paxdiablo
3
在一个只包含char msg[] = "hello, world!";语句的文件中,我可以顺利进行编译,该字符串最终存储在初始化数据段中。但如果将其声明为 char * const,则会存储在只读数据段中。这是使用gcc-4.5.3时的情况。 - gcbenison
显示剩余13条评论

180

首先,在函数参数中,它们是完全等效的:

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()或类似方法进行复制。如果数组是在全局范围内分配的,则没有问题。


89

这个声明:

char s[] = "hello";

创建一个对象 - 大小为6的char数组,名为s,初始化为'h','e','l','l','o','\0'。该数组在内存中的分配位置和生命周期取决于声明出现的位置。如果声明在函数内部,则它将存在于声明所在块的末尾,并且几乎肯定会在堆栈上分配;如果声明在函数外部,则它可能存储在“初始化数据段”中,该数据段在程序运行时从可执行文件加载到可写内存中。

另一方面,这个声明:

char *s ="hello";

创建两个对象:

  • 一个包含'h','e','l','l','o','\ 0'这6个字符的只读数组,没有名称,并具有静态存储期(这意味着它在整个程序的生命周期内都存在);以及
  • 一个char指针类型的变量,名为s,它被初始化为那个未命名的只读数组中第一个字符的位置。

未命名的只读数组通常位于程序的“text”段中,这意味着它与代码本身一起从磁盘加载到只读内存中。指针变量s在内存中的位置取决于声明出现的位置(就像第一个示例中一样)。


1
在“hello”的声明中,内存是否在编译时分配?另外,char *p =“hello”中,“hello”像您在答案中所述一样存储在文本段中。那么char s [] =“hello”也会首先存储在文本段中,在运行时将其复制到堆栈中,就像Rickard在他们的答案中所述一样。请澄清这一点。 - Nishant Kumar
2
@Nishant:在 char s[] = "hello" 的情况下,"hello" 只是一个初始化器,告诉编译器如何初始化数组。它可能会导致相应的字符串出现在文本段中,也可能不会 - 例如,如果 s 具有静态存储期,则 "hello" 的唯一实例很可能在已初始化数据段中 - 对象 s 本身。即使 s 具有自动存储期,它也可以通过一系列字面量存储来初始化,而不是通过复制(例如 movl $1819043176, -6(%ebp); movw $111, -2(%ebp))。 - caf
更准确地说,GCC 4.8将其放入.rodata中,链接器脚本然后将其转储到与.text相同的段中。请参见我的答案 - Ciro Santilli OurBigBook.com
在Rickard的第一个答案中,写道char s[] = "Hello world";会将字面字符串放入只读内存,并将字符串复制到栈上新分配的内存中。但是,你的答案只涉及将字面字符串放入只读内存,并跳过了句子的第二部分,即“将字符串复制到栈上新分配的内存中”。因此,你的答案是否不完整,因为没有指定第二部分? - ajaysinghnegi
2
@AjaySinghNegi:正如我在其他评论中所述(针对此答案和Rickard的答案),char s[] = "Hellow world"; 中的字符串只是一个初始化器,并不一定会作为单独的只读副本存储。如果s具有静态存储期,则该字符串的唯一副本可能位于S位置的可读写段中,即使不是这样,编译器也可以选择使用load-immediate指令或类似指令来初始化数组,而不是从只读字符串复制。重点是,在这种情况下,初始化程序字符串本身没有运行时存在。 - caf

72

考虑如下变量声明:

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";

定义s1char的12元素数组(长度从字符串文字中获得),其自动存储持续时间,并将文字内容复制到数组中。从内存映射中可以看出,我们有两个字符串"hello world"的副本,但不同之处在于您可以修改包含在s1中的字符串。

s0s1在大多数情况下是可以互换的;以下是例外情况:

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重新指向不同的数组。


在你的内存映射中,4列不应该是0x01 0x02 0x03 0x04而是0x00 0x01 0x02 0x03吗?否则看起来s0指向0x00008000,但第一个字母在0x00008001。同样,不清楚0x00008004是第二个“l”的地址还是“o”的地址。 - Fabio says Reinstate Monica

41

C99 N1256 草案

字符串字面量有两种不同的用法:

  1. 初始化 char[]:

    char c[] = "abc";      
    

    这是“更多的魔法”,并在6.7.8/14“初始化”中描述:

    字符类型的数组可以通过字符字符串文字进行初始化,可选地包含在大括号中。 字符串文字的连续字符(包括终止空字符,如果有空间或数组大小未知)初始化数组的元素。

    所以这只是一个快捷方式:

    char c[] = {'a', 'b', 'c', '\0'};
    

    像任何其他常规数组一样,c可以被修改。

  2. 在其他任何地方:它生成一个:

    因此,当您编写:

    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 and t 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 use p 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

结论:GCC将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)。


17
char s[] = "hello";

声明s为一个char数组,足够长以容纳初始化程序(5 + 1个char),并通过将给定字符串文字的成员复制到数组中来初始化该数组。

char *s = "hello";
声明指针s指向一个或多个(在此情况下为多个)char,并直接将它指向包含文字"hello"的固定(只读)位置。

1
如果 s 不会被改变,哪种函数中的方法更可取,在 f(const char s[]) 和 f(const char *s) 中呢? - psihodelia
1
@psihodelia:在函数声明中没有区别。在两种情况下,s都是指向const char的指针。 - CB Bailey

4
char s[] = "Hello world";

这里,s是字符数组,如果需要,可以进行覆盖操作。

char *s = "hello";

字符串字面值用于在内存中创建这些字符块,该指针 s 指向此处。我们可以通过更改它指向的对象来重新分配,但只要它指向一个字符串字面值,它所指向的字符块就不能被更改。


@bo Persson 为什么在第二种情况下字符块不能被更改? - Pankaj Mahato

3
作为补充,请注意,对于只读目的而言,两者的使用是相同的,您可以通过使用[]*(<var> + <index>)来索引一个字符。格式:
printf("%c", x[1]);     //Prints r

"并且:"
printf("%c", *(x + 1)); //Prints r

显然,如果你试图去做
*(x + 1) = 'a';

您可能会遇到分段错误,因为您正在尝试访问只读内存。

这与 x[1] = 'a'; 没有任何区别,后者也会导致段错误(当然,这取决于平台)。 - glglgl

3
一个区别的例子:
printf("hello" + 2); //llo
char a[] = "hello" + 2; //error

在第一种情况下,指针算术是有效的(传递给函数的数组会衰变为指针)。

2
虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - Donald Duck

3
只是补充一下:它们的大小也有不同的值。
printf("sizeof s[] = %zu\n", sizeof(s));  //6
printf("sizeof *s  = %zu\n", sizeof(s));  //4 or 8

正如上文所提到的,对于数组,'\0'将被分配为最后一个元素。

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