关于C#:char s[]和char*s有什么区别?

What is the difference between char s[] and char *s?

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

1
char s[] ="hello";

或者像这样:

1
char *s ="hello";

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


不同之处在于

1
char *s ="Hello world";

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

做的时候:

1
char s[] ="Hello world";

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

1
s[0] = 'J';

合法的。


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

1
2
void foo(char *x);
void foo(char x[]); // exactly the same in all respects

在其他上下文中,char *分配指针,而char []分配数组。你会问,前一种情况下绳子会往哪里去?编译器秘密地分配一个静态匿名数组来保存字符串文本。所以:

1
2
3
4
char *x ="Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] ="Foo";
char *x = (char *) __secret_anonymous_array;

请注意,千万不要试图通过此指针修改此匿名数组的内容;效果未定义(通常意味着崩溃):

1
x[1] = 'O'; // BAD. DON'T DO THIS.

使用数组语法直接将其分配到新内存中。因此,修改是安全的:

1
2
char x[] ="Foo";
x[1] = 'O'; // No problem.

然而,数组的寿命只与它的包含范围一样长,因此,如果在函数中执行此操作,则不要返回或泄漏指向该数组的指针-而是使用strdup()或类似工具进行复制。当然,如果数组是在全局范围内分配的,没有问题。


本声明:

1
char s[] ="hello";

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

另一方面,本声明:

1
char *s ="hello";

创建两个对象:

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

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


给出了声明

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

假设以下假设内存映射:

1
2
3
4
5
6
7
8
9
                    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元数组的EDOCX1×1(EDCOX1,2,C++),具有静态存储持续时间,这意味着当程序启动并分配到程序结束时,它的内存被分配。试图修改字符串文字的内容会调用未定义的行为。

线

1
char *s0 ="hello world";

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

线

1
char s1[] ="hello world";

s1定义为具有自动存储持续时间的char的12元素数组(长度取自字符串文字),并将文字内容复制到数组。从内存映射中可以看到,我们有两个字符串"hello world"的副本;不同的是,您可以修改s1中包含的字符串。

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

1
2
3
4
5
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以指向不同的数组。


C99 N1256草案

字符串文本有两种不同的用途:

  • 初始化char[]

    1
    char c[] ="abc";

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

    An array of character type may be initialized by a character string literal, optionally
    enclosed in braces. Successive characters of the character string literal (including the
    terminating null character if there is room or if the array is of unknown size) initialize the
    elements of the array.

    所以这只是一个捷径:

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

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

  • 其他任何地方:它生成:

    • 未命名的
    • char数组:C和C++中字符串字的类型是什么?
    • 带静态存储器
    • 如果修改的话会得到ub

    所以当你写:

    1
    char *c ="abc";

    这类似于:

    1
    2
    3
    /* __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 In translation phase 7, a byte or code of value zero is appended to each multibyte
    character sequence that results from a string literal or literals. The multibyte character
    sequence is then used to initialize an array of static storage duration and length just
    sufficient to contain the sequence. For character string literals, the array elements have
    type char, and are initialized with the individual bytes of the multibyte character
    sequence [...]

    6 It is unspecified whether these arrays are distinct provided their elements have the
    appropriate values. If the program attempts to modify such an array, the behavior is
    undefined.

  • 6.7.8/32"初始化"给出了一个直接示例:

    EXAMPLE 8: The declaration

    1
    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

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

    The contents of the arrays are modifiable. On the other hand, the declaration

    1
    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实现

    程序:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>

    int main(void) {
        char *s ="abc";
        printf("%s
    "
    , s);
        return 0;
    }

    编译和反编译:

    1
    2
    gcc -ggdb -std=c99 -c main.c
    objdump -Sr main.o

    输出包含:

    1
    2
    3
    4
     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段。

    如果我们对char[]也这样做:

    1
     char s[] ="abc";

    我们得到:

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

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

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

    1
    readelf -l a.out

    其中包含:

    1
    2
    3
     Section to Segment mapping:
      Segment Sections...
       02     .text .rodata


    1
    char s[] ="hello";

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

    1
    char *s ="hello";

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


    1
    char s[] ="Hello world";

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

    1
    char *s ="hello";

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


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

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

    还有:

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

    很明显,如果你想这么做

    1
    *(x + 1) = 'a';

    当您试图访问只读内存时,可能会出现分段错误。


    只需添加:您还可以获得不同大小的值。

    1
    2
    3
    4
    printf("sizeof s[] = %zu
    "
    , sizeof(s));  //6
    printf("sizeof *s  = %zu
    "
    , sizeof(s));  //4 or 8

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


    1
    char *str ="Hello";

    上述设置str指向程序二进制图像中硬编码的文本值"hello",该值在内存中标记为只读,这意味着该字符串文本中的任何更改都是非法的,这将引发分段错误。

    1
    char str[] ="Hello";

    将字符串复制到堆栈上新分配的内存中。因此,任何变更都是合法的。

    1
    means str[0] = 'M';

    将str更改为"mello"。

    有关详细信息,请回答类似问题:

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


    根据这里的评论,很明显:char*s="hello";是个坏主意,应该在很窄的范围内使用。

    这可能是一个指出"常量正确性"是"好事"的好机会。无论何时何地,都可以使用"const"关键字来保护代码不受"轻松"调用方或程序员的影响,这些调用方或程序员通常在使用指针时最"轻松"。

    足够的情节,这里是一个人可以达到什么装饰与"const"指针。(注意:必须从右向左读取指针声明。)以下是三种不同的方法来保护你自己玩指针:

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

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

    1
    DBJ* const p means"p is a const pointer to a DBJ"

    -也就是说,可以通过p更改dbj对象,但不能更改指针p本身。

    1
    const DBJ* const p means"p is a const pointer to a const DBJ"

    -也就是说,不能更改指针p本身,也不能通过p更改dbj对象。

    与尝试的常量突变相关的错误在编译时被捕获。常量没有运行时空间或速度惩罚。

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

    ——DJ


    在以下情况下:

    1
    char *x ="fred";

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

    1
    char x[] ="fred";

    x不是左值,它是右值——不能赋值给它。