提问



在C中,可以在声明中使用字符串文字,如下所示:


char s[] = "hello";


或者像这样:


char *s = "hello";


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

最佳参考


这里的区别在于


char *s = "Hello world";


"Hello world"放在内存的只读部分中,并使s成为指针,使得该内存上的任何写操作都是非法的。


做的时候:


char s[] = "Hello world";


将文字字符串放在只读内存中,并将字符串复制到堆栈上新分配的内存中。从而制作


s[0] = 'J';


法律。

其它参考1


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


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

其它参考2


本声明:


char s[] = "hello";


创建一个对象 - 一个大小为6的char数组,称为s,用值'h', 'e', 'l', 'l', 'o', '\0'初始化。此数组在内存中分配的位置,以及它的生存时间取决于声明的显示位置。如果声明在一个函数内,它将一直存在到声明它的块的结尾,并且几乎肯定会在栈上分配;如果它在函数外部,它可能存储在初始化数据段中,该初始化数据段在程序运行时从可执行文件加载到可写存储器中。


另一方面,这个声明:


char *s ="hello";


创建两个对象:



  • 包含值'h', 'e', 'l', 'l', 'o', '\0'的6 char只读数组,其中没有名称且静态存储持续时间(含义它在整个生命周期中存在);和

  • 一个类型为pointer-to-char的变量,名为s,它使用该未命名的只读数组中第一个字符的位置进行初始化。



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

其它参考3


鉴于声明


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


这行


char *s0 = "hello world";


s0定义为char的指针,具有自动存储持续时间(意味着变量s0仅存在于声明它的范围内)并复制地址字符串文字(在本例中为0x00008000)。请注意,由于s0指向字符串文字,因此不应将其用作试图修改它的任何函数的参数(例如,strtok()strcat()strcpy()]]等)。


这行


char s1[] = "hello world";


s1定义为具有自动存储持续时间的char的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


C99 N1256草案


数组文字有两种完全不同的用法:



  1. 初始化char[]:


    char c[] = "abc";      
    


    这是更神奇的,并在 6.7.8/14初始化中描述:



    ??可选地,字符类型数组可以由字符串文字初始化
    ??用括号括起来。字符串文字的连续字符(包括
    ??如果有空间或数组的大小未知,则终止空字符)初始化
    ??数组的元素。



    所以这只是一个捷径:


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


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

  2. 其他地方:它产生一个:



    • 无名

    • char数组C和C ++中字符串文字的类型是什么?

    • 带静态存储

    • 如果修改了UB,则为



    所以当你写:


    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初始化给出了一个直接的例子:



??例8:声明


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

??
??定义plainchar数组对象st,其元素用字符串文字初始化。

??
??该声明与之相同


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

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


char *p = "abc";

??
??使用类型指向char的指针定义p并将其初始化为指向类型为array of char且长度为4的对象,其元素使用字符串文字初始化。如果尝试使用p修改数组的内容,则行为未定义。



GCC 4.8 x86-64 ELF实施


程序:


#include 

int main() {
    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部分,[[strong>不存储在.text中。


如果我们为char[]做同样的事情:


 char s[] = "abc";


我们获得:


17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)


所以它存储在堆栈中(相对于%rbp)。


但请注意,默认链接描述文件将.rodata.text放在同一段中,该段具有执行但没有写入权限。这可以通过以下方式观察:


readelf -l a.out


其中包含:


 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata 

其它参考5


char s[] = "hello";


s声明为char的数组,其长度足以保存初始值设定项(5 + 1 char s)并通过将给定字符串文字的成员复制到数组中来初始化数组阵列。


char *s = "hello";


声明s是指向一个或多个(在这种情况下更多)char的指针,并将其直接指向包含文字"hello"的固定(只读)位置。

其它参考6


char s[] = "Hello world";


这里,s是一个字符数组,如果我们愿意,可以覆盖它们。


char *s = "hello";


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

其它参考7


作为补充,考虑到,对于只读目的,两者的使用是相同的,您可以通过使用[]*( + )索引来访问char
格式:


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


和:


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


显然,如果你试图这样做


*(x + 1) = 'a';


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

其它参考8


只是添加:您还可以获得不同的尺寸值。


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


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

其它参考9


char *str = "Hello";


上面的设置指向文本值Hello,它在程序的二进制映像中被硬编码,在内存中被标记为只读,意味着此字符串文字中的任何更改都是非法的并且会抛出分段故障。


char str[] = "Hello";


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


means str[0] = 'M';


将把str改为Mello。


有关详细信息,请查看类似问题:


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

其它参考10


如果是:


char *x = "fred";


x是左值 - 可以分配给它。但在以下情况下:[108]


char x[] = "fred";


x不是左值,它是右值 - 你不能分配它。

其它参考11


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


这可能是指出const正确性是好事的好机会。无论何时何地,都可以使用const关键字来保护您的代码,而不是轻松的调用者或程序员,这些指针通常在指针发挥作用时最放松。


足够的情节剧,这是用const装饰指针时可以实现的。
(注意:必须从右到左读取指针声明。)
以下是使用指针时保护自己的3种不同方法:


const DBJ* p means "p points to a DBJ that is const" 


- 也就是说,DBJ对象不能通过p进行更改。


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