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


506

在C语言中,可以在这样的声明中使用字符串文字:

char s[] = "hello";

或像这样:

char *s = "hello";

那么区别是什么呢?我想知道在编译和运行时在存储持续时间方面实际发生了什么。



8
char * s =“ hello”,这里s可以在运行时指向任何其他字符串,我的意思是它不是常量指针,可以在运行时分配另一个值p =“ Nishant”,而s []这里s是常量指针。 ..它不能重新分配另一个字符串,但是我们可以在s [index]分配另一个字符值。
Nishant Kumar 2012年

Answers:


540

区别在于

char *s = "Hello world";

将被放置"Hello world"存储器只读部分中,并使其s指向该存储器的任何写操作都是非法的。

这样做时:

char s[] = "Hello world";

将文字字符串放入只读内存中,并将该字符串复制到堆栈上新分配的内存中。因此使

s[0] = 'J';

法律。


22
"Hello world"在两个示例中,文字字符串都位于“内存的只读部分”中。带有数组的示例指向那里,带有数组的示例将字符复制到数组元素。
09年

28
pmg:在第二种情况下,文字字符串不一定完全作为单个连续对象存在于内存中-它只是一个初始化程序,编译器可以相当合理地发出一系列“加载立即字节”指令,这些指令包含嵌入其中的字符值他们。
caf

10
该字符数组例子并不能一定会将堆栈上的字符串-如果它出现在文件级别,它可能会在某种初始化的数据段,而不是。
caf

9
我想指出的是字符S =“XX”不具有要在只读存储器(一些实现没有MMU的,例如)。n1362 c1x草案仅声明修改此类数组会导致未定义的行为。但是无论如何+1,因为依靠那种行为是一件很愚蠢的事情。
paxdiablo

3
我对仅包含char msg[] = "hello, world!"; 字符串的文件进行了干净的编译,该文件最终位于初始化的数据部分中。当声明char * const结束于只读数据部分时。gcc-4.5.3
gcbenison 2012年

152

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

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


72

此声明:

char s[] = "hello";

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

另一方面,此声明:

char *s ="hello";

创建两个对象:

  • 一个6 s 的只读数组,其中char包含values 'h', 'e', 'l', 'l', 'o', '\0',该数组没有名称,并且具有静态存储持续时间(意味着它在程序的整个生命周期内都有效);和
  • 一个指针类型为char的变量,称为s,使用该未命名只读数组中第一个字符的位置进行初始化。

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


1
在这两个“ hello”声明中,内存是在适当的时间分配的。还有另一件事char * p =“ hello”,这里的“ hello”存储在文本段中,如您在回答中所述……以及char s []呢? =“ hello”还将首先存储在文本段部分中,并且在运行时将按照Rickard在那里的回答中的说明复制到堆栈中。请澄清这一点。
Nishant Kumar 2012年

2
@Nishant:在这种char s[] = "hello"情况下,"hello"只是一个初始化程序,告诉编译器应如何初始化数组。它可能会或可能不会在文本段中产生相应的字符串-例如,如果s具有静态存储持续时间,则很可能唯一的实例"hello"将在初始化的数据段中-对象s本身。即使s具有自动存储期限,也可以通过一系列文字存储而不是副本(例如movl $1819043176, -6(%ebp); movw $111, -2(%ebp))来初始化它。
caf 2012年

更精确地说,GCC 4.8将其放入.rodata其中,然后链接脚本将其转储到与相同的段中.text。看我的回答
西罗Santilli郝海东冠状病六四事件法轮功

@caf在Rickard的第一个答案中,它被编写为char s[] = "Hello world";将文字字符串放入只读内存中,并将该字符串复制到堆栈上新分配的内存中。但是,您的答案仅涉及放在只读存储器中的原义字符串,而跳过句子的第二部分:copies the string to newly allocated memory on the stack。因此,由于未指定第二部分,您的答案是否不完整?
毕马威(KPMG)

1
@AjaySinghNegi:正如我在其他评论(针对此答案和Rickard的答案)中所述,该字符串char s[] = "Hellow world";只是一个初始化程序,不一定存储为单独的只读副本。如果s具有静态存储持续时间,则字符串的唯一副本很可能位于的位置的读写段中s,即使没有,编译器也可以选择使用立即加载指令或类似指令初始化数组,而不是复制来自只读字符串。关键是在这种情况下,初始化程序字符串本身不存在运行时。
caf

60

给出声明

char *s0 = "hello world";
char s1[] = "hello world";

假设以下假设内存映射:

                    0x01 0x02 0x03 0x04
        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个元素的数组charconst char在C ++中),具有静态存储持续时间,这意味着它的内存在程序启动时分配,并保持分配状态,直到程序终止。尝试修改字符串文字的内容会调用未定义的行为。

线

char *s0 = "hello world";

定义s0为指向char具有自动存储持续时间的指针(意味着该变量s0仅在声明它的作用域中存在),并将字符串文字的地址0x00008000在此示例中)复制到该地址。注意,因为s0指向一个字符串字面量,它不应该被用来作为一个参数,将试图修改它的任何功能(如strtok()strcat()strcpy(),等)。

线

char s1[] = "hello world";

定义s1为具有12个元素的数组char(长度从字符串文字中获取)并具有自动存储持续时间,并将文字内容复制到该数组中。从内存映射中可以看到,我们有两个字符串副本"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以指向其他数组。


2
我认为假设的内存映射使它易于理解!
midnightBlue

32

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],那么您还修改了__unnamedUB。

    在6.4.5“字符串文字”中有记录:

    5在转换阶段7中,一个或多个字符串文字产生的每个多字节字符序列将附加一个零值的字节或代码。然后,多字节字符序列用于初始化一个足以包含该序列的静态存储持续时间和长度数组。对于字符串文字,数组元素的类型为char,并使用多字节字符序列的各个字节进行初始化[...]

    6如果这些数组的元素具有适当的值,则不确定这些数组是否不同。如果程序尝试修改这样的数组,则行为是不确定的。

6.7.8 / 32“初始化”给出了一个直接的示例:

例8:声明

char s[] = "abc", t[3] = "abc";

定义“普通” char数组对象st其元素用字符串文字初始化。

此声明与

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

数组的内容是可修改的。另一方面,声明

char *p = "abc";

定义p类型为“ 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放在同一段中,该具有执行但没有写权限。这可以通过以下方式观察到:

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)。


15
char s[] = "hello";

声明s是一个阵列的char哪个是长到足以保持初始化(5个+ 1 charS)和由给定字符串文字的成员复制到阵列初始化数组。

char *s = "hello";

声明s是一个或多个(在这种情况下为多个)chars 的指针,并将其直接指向包含literal的固定(只读)位置"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 2014年

3

另外,请考虑一下,出于只读目的,两者的使用是相同的,因此可以通过[]或编制索引来访问char *(<var> + <index>)

printf("%c", x[1]);     //Prints r

和:

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

显然,如果您尝试这样做

*(x + 1) = 'a';

当您尝试访问只读内存时,您可能会遇到分段错误。


这与段错误也没有什么不同x[1] = 'a';(当然取决于平台)。
glglgl

3

只需添加:它们的大小也会得到不同的值。

printf("sizeof s[] = %zu\n", sizeof(s));  //6
printf("sizeof *s  = %zu\n", sizeof(s));  //4 or 8

如上所述,对于数组,'\0'将分配为最后一个元素。


2
char *str = "Hello";

上面的设置str指向文本值“ Hello”,该值在程序的二进制映像中进行了硬编码,并在内存中标记为只读,这意味着对该String文本进行的任何更改都是非法的,并且会引发分段错误。

char str[] = "Hello";

将字符串复制到堆栈上新分配的内存中。因此,允许对其进行任何更改都是合法的。

means str[0] = 'M';

将str更改为“ Mello”。

有关更多详细信息,请经历类似的问题:

为什么在写入以“ char * s”而不是“ char s []”初始化的字符串时出现分段错误?


0

如果是:

char *x = "fred";

x是一个左值 -可以分配给它。但在以下情况下:

char x[] = "fred";

x不是左值,而是右值-您无法分配给它。


3
从技术上讲,x是不可修改的左值。但是,在几乎所有情况下,它都会求值一个指向其第一个元素的指针,并且值是一个右值。
caf

0
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

-1

根据这里的评论,应该显而易见的是:char * s =“ hello”; 这是一个坏主意,应在非常狭窄的范围内使用。

这可能是一个很好的机会,指出“常量正确性”是“好事”。无论何时何地,都可以使用“ 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没有运行时空间或速度损失。

(当然,假设您正在使用C ++编译器?)

--DBJ


这都是正确的,但与问题无关。就您对C ++编译器的假设而言,该问题被标记为C,而不是C ++。
Fabio说恢复莫妮卡

char * s =“ const string”没有什么坏处;
保罗·史密斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.