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

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

char s[] = "hello";

或者像这样:

char *s = "hello";

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

387053 次浏览

不同之处在于

char *s = "Hello world";

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

虽然做的事情:

char s[] = "Hello world";

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

s[0] = 'J';

合法的。

char s[] = "hello";

声明schar的数组,长度足够容纳初始化式(5 + 1 chars),并通过将给定字符串文字的成员复制到数组中来初始化数组。

char *s = "hello";

声明s是一个或多个(在本例中是多个)chars的指针,并将其直接指向包含文字"hello"的固定(只读)位置。

此声明:

char s[] = "hello";

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

另一方面,这个声明:

char *s ="hello";

创建两个对象:

  • 一个由6个# eyz0组成的只读数组,包含值'h', 'e', 'l', 'l', 'o', '\0',它没有名字,有静态存储持续时间(意味着它在程序的整个生命周期中都存在);而且
  • 一个类型为指针到字符的变量,称为s,它是用该未命名的只读数组中第一个字符的位置初始化的。

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

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

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

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

char *s = "hello";

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

在下列情况下:

char *x = "fred";

x是左值——它可以被赋值给。但在这种情况下:

char x[] = "fred";

X不是一个左值,它是一个右值——你不能给它赋值。

给出声明

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仅存在于声明它的作用域),并将字符串字面量的地址(本例中为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以指向不同的数组。

根据这里的注释,应该很明显:char * s = "hello"; 是一个坏主意,应该在非常狭窄的范围内使用。< / p >

这可能是指出“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没有运行时空间或速度损失。

(当然,假设你使用的是c++编译器?)

——日本

补充一点:它们的大小也会有不同的值。

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

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

c99n1256草案

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

  1. < p >初始化# EYZ0:

    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 "String literals"中有说明:

    在转换阶段7中,值为0的字节或代码被附加到每个多字节 由一个或多个字符串字面值产生的字符序列。多字节字符 然后使用Sequence初始化一个静态存储持续时间和长度的数组 足以包含序列。对于字符串字面量,数组元素具有 类型为char,并用多字节字符的单个字节进行初始化 序列[…]< / p > 如果数组的元素具有属性,则不指定这些数组是否不同 适当的值。如果程序试图修改这样的数组,行为为 定义。< / p > 李< /引用> < / >

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

例8:声明

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

定义“普通”字符数组对象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)。

另外,考虑到对于只读目的,两者的使用是相同的,您可以通过索引[]*(<var> + <index>)来访问一个char 格式:< / p >
printf("%c", x[1]);     //Prints r

和:

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

很明显,如果你试图这么做

*(x + 1) = 'a';

你可能会得到一个分割错误,因为你试图访问只读内存。

char *str = "Hello";

上面设置str指向“Hello”字面值,该字面值是硬编码在程序的二进制图像中,在内存中被标记为只读,这意味着这个String字面值的任何更改都是非法的,并且会抛出分割错误。

char str[] = "Hello";

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

means str[0] = 'M';

将str更改为“Mello”。

如欲了解更多详情,请浏览类似问题:

为什么我得到一个分割错误时写入一个字符串初始化"char * "但不是"char s[]" ?< / >

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

举个例子来说明区别:

printf("hello" + 2); //llo
char a[] = "hello" + 2; //error

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