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

时间:2008-10-02 19:45:22

标签: c segmentation-fault c-strings

以下代码在第2行接收seg错误:

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进行测试。

18 个答案:

答案 0 :(得分:219)

请参阅C FAQ,Question 1.32

  

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

     

A :字符串文字(正式用语   对于C中的双引号字符串   来源)可以稍微使用两个   不同的方式:

     
      
  1. 作为char数组的初始值设定项,如char a[]的声明中所述,它指定初始值   该数组中的字符(和,   如果有必要,它的大小)。
  2.   
  3. 在其他地方,它变成一个未命名的静态字符数组,   并且可以存储该未命名的数组   在只读内存中,以及哪些内存   因此不一定如此   改性。在表达式上下文中,   数组一次转换为a   指针,像往常一样(见第6节),所以   第二个声明初始化p   指向未命名的数组的第一个   元件。
  4.         

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

答案 1 :(得分:93)

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

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

printf("%p", str);

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

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

答案 2 :(得分:31)

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

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

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

答案 3 :(得分:20)

  

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

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如果这些数组的元素具有,则未指定这些数组是否是不同的   适当的价值观如果程序试图修改这样的数组,则行为是   未定义。

  3. 6.7.8 / 32"初始化"给出了一个直接的例子:

      

    示例8:声明

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

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

         

    此声明与

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

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

    char *p = "abc";
    
         

    定义p类型"指向char"并初始化它以指向一个类型为#34的对象; char"长度为4,其元素用字符串文字初始化。如果尝试使用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

    如果我们对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
    

答案 4 :(得分:17)

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

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

的简写
char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '\0' };

“str”是在堆栈上分配的数组,可以自由修改。

答案 5 :(得分:12)

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

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

答案 6 :(得分:8)

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

字符串数组

 char strarray[] = "hello";

存储器阵列存储在连续存储器单元中,存储为[h][e][l][l][o][\0] =>[]是1个字节大小的存储器单元,这个连续存储器单元可以通过名称为strarray来访问。这里是字符串数组{{1} }本身包含初始化为它的字符串的所有字符。在这种情况下strarray 所以我们可以通过索引值

访问每个字符来轻松更改其内存内容
"hello"

并将其值更改为`strarray[0]='m'` it access character at index 0 which is 'h'in strarray ,因此strarray值更改为'm';

这里需要注意的一点是,我们可以通过逐个字符更改字符串数组的内容,但不能直接将其他字符串初始化为"mello"无效

指针

我们都知道指针指向内存中的内存位置, 未初始化的指针指向随机内存位置,因此在初始化后指向特定的内存位置

strarray="new string"

这里指针ptr被初始化为字符串char *ptr = "hello"; ,这是存储在只读存储器(ROM)中的常量字符串,因此"hello"无法更改,因为它存储在ROM中

和ptr存储在堆栈部分并指向常量字符串"hello"

所以ptr [0] ='m'无效,因为你无法访问只读存储器

但是ptr可以直接初始化为其他字符串值,因为它只是指针,所以它可以指向其数据类型变量的任何内存地址

"hello"

答案 7 :(得分:7)

char *str = "string";  

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

因此str[0]=正在尝试写入应用程序的只读代码。我猜这可能是编译器依赖的。

答案 8 :(得分:6)

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

答案 9 :(得分:6)

char *str = "string";

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

char str[] = "string";

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

答案 10 :(得分:4)

 char *str = "string";

行定义指针并将其指向文字字符串。当您执行以下操作时,文字字符串是不可写的:

  str[0] = 'z';

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

该行:

char str[] = "string";

将字符数组和拷贝文字字符串分配到该数组中,该数组是完全可写的,因此后续更新没有问题。

答案 11 :(得分:3)

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

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

答案 12 :(得分:2)

// 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\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array. 

答案 13 :(得分:1)

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

第二,你要创建一个数组,这是你可以完全访问的内存,并用"string"初始化它。你正在创建一个char[7](六个用于字母,一个用于终止'\ 0'),你用它做任何你想做的事。

答案 14 :(得分:0)

假设字符串是

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

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

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

答案 15 :(得分:0)

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

<块引用>

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

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

amessage 是一个数组,大小刚好可以容纳字符序列和初始化它的 '\0'。数组中的单个字符可能会更改,但 amessage 将始终引用相同的存储。另一方面,pmessage 是一个指针,初始化为指向一个字符串常量;该指针随后可能会被修改为指向其他地方,但如果您尝试修改字符串内容,则结果未定义。

答案 16 :(得分:-1)

首先是一个不能修改的常量字符串。第二个是具有初始化值的数组,因此可以进行修改。

答案 17 :(得分:-2)

当您访问无法访问的内存时,会导致

分段错误。

char *str是指向不可修改的字符串的指针(获取seg错误的原因)..

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