【问题标题】:Why do I get a segmentation fault when writing to a "char *s" initialized with a string literal, but not "char s[]"?为什么在写入使用字符串文字初始化的 \"char *s\" 而不是 \"char s[]\" 时出现分段错误?
【发布时间】:2022-12-08 23:24:08
【问题描述】:

以下代码在第 2 行收到段错误:

char *str = "string";
str[0] = 'z';  // could be also written as *str = 'z'
printf("%s\n", str);

虽然这非常有效:

char str[] = "string";
str[0] = 'z';
printf("%s\n", str);

使用 MSVC 和 GCC 进行测试。

【问题讨论】:

  • 这很有趣——但是当在 visual studio 开发人员命令提示符下使用 Windows 编译器 (cl) 时,这实际上可以完美地编译和运行。让我困惑了一会儿......

标签: c segmentation-fault c-strings


【解决方案1】:

请参阅 C 常见问题解答,Question 1.32

: 这些初始化有什么区别?
char a[] = "string literal";
char *p = "string literal";
如果我尝试为 p[i] 分配一个新值,我的程序就会崩溃。

一个:字符串文字(正式术语 对于 C 中的双引号字符串 source)可以用在两个咯 不同的方法:

  1. 作为 char 数组的初始值设定项,如 char a[] 的声明,它指定初始值 该数组中的字符(以及, 如有必要,它的大小)。
  2. 在其他任何地方,它都会变成一个未命名的静态字符数组, 这个未命名的数组可以存储 在只读存储器中,其中 因此不一定是 修改的。在表达式上下文中, 该数组立即转换为 指针,像往常一样(见第 6 节),所以 第二个声明初始化 p 指向未命名数组的第一个 元素。

    一些编译器有一个开关 控制字符串文字是否 是否可写(用于编译旧的 代码),有些人可能有选择 导致字符串文字正式化 被视为 const char 的数组(对于 更好的错误捕获)。

【讨论】:

  • 其他几点:(1)段错误如所述发生,但它的发生是运行环境的函数;如果相同的代码在嵌入式系统中,写入可能没有效果,或者它实际上可能将 s 更改为 z。 (2) 因为字符串文字是不可写的,编译器可以通过将两个“string”实例放在同一个地方来节省空间;或者,如果代码中的其他地方有“另一个字符串”,那么一块内存可以支持这两种文字。显然,如果随后允许代码更改这些字节,可能会出现奇怪而棘手的错误。
  • @greggo:好点。还有一种方法可以在具有 MMU 的系统上执行此操作,方法是使用 mprotect 来发送只读保护(请参阅 here)。
  • 所以 char *p="blah" 实际上创建了一个临时数组?很奇怪。
  • 在用 C++ 编写 2 年后...TIL
  • @rahul tyagi,不是临时数组。恰恰相反,它是寿命最长的阵列。它由编译器创建并在可执行文件本身中找到。从上面你应该明白的是,这是一个共享必须被视为的数组只读(实际上可能是只读的)。
【解决方案2】:

通常,字符串文字在程序运行时存储在只读内存中。这是为了防止您意外更改字符串常量。在您的第一个示例中,"string" 存储在只读存储器中,*str 指向第一个字符。当您尝试将第一个字符更改为 'z' 时会发生段错误。

在第二个例子中,字符串"string"复制由编译器从它的只读主页到 str[] 数组。然后允许更改第一个字符。您可以通过打印每个地址来检查这一点:

printf("%p", str);

此外,在第二个示例中打印 str 的大小将向您显示编译器已为其分配了 7 个字节:

printf("%d", sizeof(str));

【讨论】:

  • 每当在 printf 上使用“%p”时,您应该将指针转换为 void *,如 printf("%p", (void *)str);使用 printf 打印 size_t 时,如果使用最新的 C 标准 (C99),则应使用“%zu”。
  • 此外,只有在获取类型的大小时才需要带有 sizeof 的括号(然后参数看起来像一个强制转换)。请记住 sizeof 是一个运算符,而不是一个函数。
  • 警告:格式为 [-Wformat=] 的未知转换类型字符“z”:/
【解决方案3】:

大多数这些答案都是正确的,但只是为了增加一点清晰度......

人们所说的“只读存储器”是ASM术语中的文本段。它与加载指令的内存位置相同。出于安全等显而易见的原因,这是只读的。当您创建一个初始化为字符串的 char* 时,字符串数据被编译到文本段中,程序将指针初始化为指向文本段。因此,如果您尝试更改它,那就好。段错误。

当写成一个数组时,编译器将初始化的字符串数据放在数据段中,这与您的全局变量等所在的位置相同。该内存是可变的,因为数据段中没有指令。这一次,当编译器初始化字符数组(它仍然只是一个 char*)时,它指向数据段而不是文本段,您可以在运行时安全地更改它。

【讨论】:

  • 但是不是真的可以有允许修改“只读内存”的实现吗?
  • 当写成数组时,编译器将初始化的字符串数据放在数据段中(如果它们是静态的或全局的)。否则(例如对于一个普通的自动数组)它放在堆栈上,在函数 main 的堆栈帧中。正确的?
  • @SE 是的,我想 Bob Somers 在编写“数据段”时指的是堆栈、堆和静态(包括静态和全局变量)。一个本地数组放在堆栈上,所以你是对的:)
  • 对不起,但你在这里可能是正确的,数据段是专用于初始化全局或静态变量的内存部分,但如果数组是本地的,也可以将其放在堆栈上,如你所写。
【解决方案4】:

为什么在写入字符串时会出现分段错误?

C99 N1256草案

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

  1. 初始化char[]

    char c[] = "abc";      
    

    这是“更神奇的”,并在 6.7.8/14“初始化”中进行了描述:

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

    所以这只是一个快捷方式:

    char c[] = {'a', 'b', 'c', '
【解决方案5】:

在第一段代码中,“string”是一个字符串常量,字符串常量永远不要修改,因为它们经常被放入只读内存中。 “str”是用于修改常量的指针。

在第二个代码中,“string”是一个数组初始值设定项,是

char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '
【解决方案6】:

因为在第一个示例的上下文中 "whatever" 的类型是 const char *(即使您将其分配给非常量 char*),这意味着您不应该尝试写入它。

编译器通过将字符串放入内存的只读部分来强制执行此操作,因此写入它会产生段错误。

【讨论】:

    【解决方案7】:
    char *str = "string";  
    

    上面的设置 str 指向文字值 "string",它在程序的二进制映像中是硬编码的,它可能在内存中被标记为只读。

    所以 str[0]= 正试图写入应用程序的只读代码。我猜这可能是依赖于编译器的。

    【讨论】:

      【解决方案8】:

      要了解此错误或问题,您应该首先了解指针和数组的区别 所以在这里首先我向你解释了它们之间的差异

      字符串数组

       char strarray[] = "hello";
      

      在内存中数组存储在连续的存储单元中,存储为[h][e][l][l][o][

      【解决方案9】:
      char *str = "string";
      

      分配一个指向字符串文字的指针,编译器将其放入可执行文件的不可修改部分;

      char str[] = "string";
      

      分配并初始化一个可修改的本地数组

      【讨论】:

      • 我们可以像写char *s = "HelloWorld"那样写int *b = {1,2,3)吗?
      • @SurajJain 不,我们不能那样做,因为这将是无效的转换( int 到 int* )。我们也不能写 char* ptr = { 'a', 'b'}; 这也是无效的转换(char 到 char*)。
      【解决方案10】:

      @matli 链接到的 C FAQ 提到了它,但这里还没有其他人提到它,所以为了澄清:如果在任何地方使用字符串文字(源代码中的双引号字符串)以外初始化字符数组(即:@Mark 的第二个示例,它工作正常),该字符串由编译器存储在一个特殊的静态字符串表,这类似于创建一个本质上是匿名的(没有变量“名称”)的全局静态变量(当然是只读的)。这只读部分是重要的部分,这就是为什么 @Mark 的第一个代码示例出现段错误的原因。

      【讨论】:

      • 我们可以像写char *s = "HelloWorld"那样写int *b = {1,2,3)吗?
      【解决方案11】:

       char *str = "string";
      

      line 定义了一个指针并将其指向文字字符串。文字字符串不可写,所以当你这样做时:

        str[0] = 'z';
      

      你得到一个段错误。在某些平台上,文字可能位于可写内存中,因此您不会看到段错误,但无论如何它都是无效代码(导致未定义的行为)。

      该行:

      char str[] = "string";
      

      分配一个字符数组和副本将文字字符串放入该数组中,该数组是完全可写的,因此后续更新是没有问题的。

      【讨论】:

      • 我们可以像写char *s = "HelloWorld"那样写int *b = {1,2,3)吗?
      【解决方案12】:

      像“string”这样的字符串文字可能作为只读数据分配在您的可执行文件的地址空间中(提供或接受您的编译器)。当你去触摸它时,它会吓坏你在它的泳衣区,并让你知道有段错误。

      在您的第一个示例中,您将获得指向该 const 数据的指针。在您的第二个示例中,您正在使用 const 数据的副本初始化一个包含 7 个字符的数组。

      【讨论】:

        【解决方案13】:
        // create a string constant like this - will be read only
        char *str_p;
        str_p = "String constant";
        
        // create an array of characters like this 
        char *arr_p;
        char arr[] = "String in an array";
        arr_p = &arr[0];
        
        // now we try to change a character in the array first, this will work
        *arr_p = 'E';
        
        // lets try to change the first character of the string contant
        *str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.
        
        
        /*-----------------------------------------------------------------------------
         *  String constants can't be modified. A segmentation fault is the result,
         *  because most operating systems will not allow a write
         *  operation on read only memory.
         *-----------------------------------------------------------------------------*/
        
        //print both strings to see if they have changed
        printf("%s
        ", str_p); //print the string without a variable
        printf("%s
        ", arr_p); //print the string, which is in an array. 
        

        【讨论】:

          【解决方案14】:

          首先,str 是指向"string" 的指针。允许编译器将字符串文字放在内存中您无法写入但只能读取的位置。 (这真的应该触发警告,因为您将 const char * 分配给 char *。您是否禁用了警告,或者只是忽略了它们?)

          其次,您正在创建一个数组,这是您可以完全访问的内存,并使用 "string" 对其进行初始化。您正在创建一个 char[7](六个用于字母,一个用于终止 '

          【解决方案15】:

          假设字符串是,

          char a[] = "string literal copied to stack";
          char *p  = "string literal referenced by p";
          

          在第一种情况下,文字将在“a”进入范围时被复制。这里 'a' 是在堆栈上定义的数组。这意味着字符串将在堆栈上创建,其数据从代码(文本)内存复制,代码(文本)内存通常是只读的(这是特定于实现的,编译器也可以将此只读程序数据放入可读写内存中).

          在第二种情况下,p 是在堆栈(本地范围)上定义的指针,并引用存储在其他位置的字符串文字(程序数据或文本)。通常修改这样的内存不是好的做法,也不鼓励。

          【讨论】:

            【解决方案16】:

            K&RSection 5.5 Character Pointers and Functions也讨论了这个话题:

            这些定义之间有一个重要的区别:

            char amessage[] = "now is the time"; /* an array */
            char *pmessage = "now is the time"; /* a pointer */

            amessage 是一个数组,刚好足以容纳字符序列和 '

            【解决方案17】:

            常量记忆

            由于字符串文字在设计上是只读的,因此它们存储在常数部分的记忆。存储在那里的数据是不可变的,即不能更改。因此,在 C 代码中定义的所有字符串文字都在这里获得一个只读内存地址。

            栈内存

            堆栈部分内存是局部变量地址所在的地方,例如,函数中定义的变量。


            正如@matli 的回答所暗示的那样,有两种方法可以使用 string 这些常量字符串。

            1. 指向字符串文字的指针

            当我们定义一个指向字符串文字的指针时,我们正在创建一个指针变量,它位于栈内存.它指向底层字符串文字所在的只读地址。

            #include <stdio.h>
            
            int main(void) {
              char *s = "hello";
              printf("%p
            ", &s);  // Prints a read-only address, e.g. 0x7ffc8e224620
              return 0;
            }
            

            如果我们尝试通过插入来修改s

            s[0] = 'H';
            

            我们得到一个Segmentation fault (core dumped)。我们正在尝试访问我们不应该访问的内存。我们正在尝试修改只读地址 0x7ffc8e224620 的值。

            2.字符数组

            为了示例,假设存储在常量内存中的字符串文字"Hello" 具有与上面相同的只读内存地址0x7ffc8e224620

            #include <stdio.h>
            
            int main(void) {
              // We create an array from a string literal with address 0x7ffc8e224620.
              // C initializes an array variable in the stack, let's give it address
              // 0x7ffc7a9a9db2.
              // C then copies the read-only value from 0x7ffc8e224620 into 
              // 0x7ffc7a9a9db2 to give us a local copy we can mutate.
              char a[] = "hello";
            
              // We can now mutate the local copy
              a[0] = 'H';
            
              printf("%p
            ", &a);  // Prints the Stack address, e.g. 0x7ffc7a9a9db2
              printf("%s
            ", a);   // Prints "Hello"
            
              return 0;
            }
            

            笔记:在 1. 中使用指向字符串文字的指针时,最佳做法是使用 const 关键字,例如 const *s = "hello"。这更具可读性,编译器将在违反时提供更好的帮助。然后它会抛出类似 error: assignment of read-only location ‘*s’ 的错误,而不是段错误。在您手动编译代码之前,编辑器中的 Linters 也可能会发现错误。

            【讨论】:

              【解决方案18】:

              第一个是一个不可修改的常量字符串。第二个是一个带有初始化值的数组,所以它可以被修改。

              【讨论】:

                【解决方案19】:

                当您尝试访问无法访问的内存时,会导致分段错误。

                char *str 是指向不可修改的字符串的指针(出现段错误的原因)。

                char str[] 是一个数组并且可以修改..

                【讨论】:

                  猜你喜欢
                  • 2021-03-16
                  • 2021-03-15
                  • 2019-12-16
                  • 2019-03-04
                  • 1970-01-01
                  • 2018-05-09
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多