【问题标题】:What is the difference between char s[] and char *s?char s[] 和 char *s 有什么区别?
【发布时间】:2010-12-14 20:01:24
【问题描述】:

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

char s[] = "hello";

或者像这样:

char *s = "hello";

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

【问题讨论】:

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

标签: c string char constants


【解决方案1】:

这里的区别在于

char *s = "Hello world";

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

在做的时候:

char s[] = "Hello world";

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

s[0] = 'J';

合法的。

【讨论】:

  • 在两个示例中,文字字符串 "Hello world" 都位于“内存的只读部分”中。带有数组的示例 points 在那里,带有数组的示例 复制 字符到数组元素。
  • pmg:在第二种情况下,文字字符串不一定作为单个连续对象存在于内存中 - 它只是一个初始化程序,编译器可以相当合理地发出一系列“加载立即字节”包含嵌入其中的字符值的指令。
  • char 数组示例 not 一定要将字符串放在堆栈上 - 如果它出现在文件级别,它可能会在某种初始化的数据段中。
  • 我想指出 char s = "xx" 没有必须在只读内存中(例如,某些实现没有 MMU) . n1362 c1x 草案只是声明修改这样的数组会导致未定义的行为。但无论如何 +1,因为依赖这种行为是一件愚蠢的事情。
  • 我对仅包含 char msg[] = "hello, world!"; 的文件进行了干净编译,该字符串最终出现在初始化的数据部分中。当声明 char * const 以在只读数据部分结束时。 gcc-4.5.3
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    此声明:

    char s[] = "hello";
    

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

    另一方面,这个声明:

    char *s ="hello";
    

    创建两个对象:

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

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

    【讨论】:

    • 在“hello”的两个声明中,内存是在comiple时间分配的?。另一件事 char *p = "hello" 这里的“hello” 存储在文本段中,如您在回答中所述.. . 那么 char s[] = "hello" 它也会首先存储在文本段部分中,并且在运行时它将复制到堆栈中,正如 Rickard 在那里的回答中所说的那样。请澄清这一点。
    • @Nishant:在char s[] = "hello" 的情况下,"hello" 只是一个初始化器,告诉编译器应该如何初始化数组。它可能会或可能不会在文本段中产生相应的字符串——例如,如果s 具有静态存储持续时间,那么"hello" 的唯一实例可能会在初始化的数据段中——对象@987654335 @ 本身。即使s 具有自动存储持续时间,它也可以通过一系列文字存储而不是副本来初始化(例如movl $1819043176, -6(%ebp); movw $111, -2(%ebp))。
    • 更准确地说,GCC 4.8 将其放入.rodata,然后链接描述文件将其转储到与.text 相同的段中。见my answer
    • @caf 在 Rickard 的第一个回答中,写到 char s[] = "Hello world"; 将文字字符串放入只读内存中,并将字符串复制到堆栈上新分配的内存中。但是,您的答案只涉及放入只读存储器的文字字符串,并跳过了句子的第二部分:copies the string to newly allocated memory on the stack。那么,您的答案是否不完整,因为没有指定第二部分?
    • @AjaySinghNegi:正如我在其他 cmets 中所述(对此答案和 Rickard 的答案),char s[] = "Hellow world"; 中的字符串只是一个初始化程序,不一定存储为单独的只读完全复制。如果s 具有静态存储持续时间,则字符串的唯一副本可能位于s 位置的读写段中,即使不是,编译器也可能选择使用 load-immediate 初始化数组指令或类似的,而不是从只读字符串复制。关键是在这种情况下,初始化字符串本身不存在运行时。
    【解决方案4】:

    鉴于声明

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

    【讨论】:

    • 在你的内存映射中,4列不应该是0x00 0x01 0x02 0x03而不是0x01 0x02 0x03 0x04吗?否则它看起来像 s0 指向 0x00008000 但第一个字母在 0x00008001。同样,不清楚 0x00008004 是第二个“l”还是“o”的地址。
    【解决方案5】:

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

    示例 8:声明

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

    定义“普通”字符数组对象st,其元素使用字符串字面量进行初始化。

    此声明与

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

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

    char *p = "abc";
    

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

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

    【讨论】:

      【解决方案6】:
      char s[] = "hello";
      

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

      char *s = "hello";
      

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

      【讨论】:

      • 如果 s 不会改变,在函数中使用什么方法更可取,f(const char s[]) 还是 f(const char *s)?
      • @psihodelia:在函数声明中没有区别。在这两种情况下,s 都是指向 const char 的指针。
      【解决方案7】:
      char s[] = "Hello world";
      

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

      char *s = "hello";
      

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

      【讨论】:

      • @bo Persson 为什么在第二种情况下不能更改字符块?
      【解决方案8】:

      另外,考虑到对于只读目的,两者的使用是相同的,您可以通过使用[]*(&lt;var&gt; + &lt;index&gt;) 进行索引来访问字符 格式:

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

      还有:

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

      显然,如果您尝试这样做

      *(x + 1) = 'a';
      

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

      【讨论】:

      • 这与x[1] = 'a'; 没有任何不同,后者也会出现段错误(当然,取决于平台)。
      【解决方案9】:

      只是补充一点:您还可以获得不同的尺寸值。

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

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

      【讨论】:

        【解决方案10】:
        char *str = "Hello";
        

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

        char str[] = "Hello";
        

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

        means str[0] = 'M';
        

        将 str 更改为“Mello”。

        更多详情,请查看类似问题:

        Why do I get a segmentation fault when writing to a string initialized with "char *s" but not "char s[]"?

        【讨论】:

          【解决方案11】:
          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
          

          【讨论】:

            【解决方案12】:

            差异示例:

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

            在第一种情况下,指针算法正在工作(传递给函数的数组衰减为指针)。

            【讨论】:

            • 虽然此代码可能会回答问题,但提供有关此代码为何和/或如何回答问题的额外上下文可提高其长期价值。
            【解决方案13】:

            如果是:

            char *x = "fred";
            

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

            char x[] = "fred";
            

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

            【讨论】:

            • 从技术上讲,x 是一个不可修改的左值。不过,在几乎所有情况下,它都会计算出指向其第一个元素的指针,并且 那个 值是一个右值。
            【解决方案14】:

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

            这可能是指出“const 正确性”是“好事”的好机会。无论何时何地您都可以使用“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-ant 突变相关的错误在编译时被捕获。 const 没有运行时空间或速度损失。

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

            --DBJ

            【讨论】:

            • 这都是正确的,但与问题无关。至于你对 C++ 编译器的假设,这个问题被标记为 C,而不是 C++。
            • char *s = "const string"; 没有什么不好的;
            猜你喜欢
            相关资源
            最近更新 更多