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


286

以下代码在第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测试。


1
它很有趣-但是在Visual Studio开发人员命令提示符上使用Windows编译器(cl)时,它实际上可以编译并完美运行。让我有些困惑...
David Refaeli

Answers:


240

请参阅C FAQ,问题1.32

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

:字符串文字(C源代码中双引号字符串的正式术语)可以以两种略有不同的方式使用:

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

一些编译器具有控制字符串文字是否可写的开关(用于编译旧代码),而某些编译器可能具有将字符串文字形式上视为const char数组的选项(以更好地捕获错误)。


7
其他几点:(1)段故障按所述发生,但它的出现是运行环境的函数;如果同一代码在嵌入式系统中,则写入可能无效,或者实际上可能将s更改为z。(2)因为字符串文字是不可写的,所以编译器可以通过将两个“ string”实例放在同一位置来节省空间。或者,如果代码中的其他地方有“另一个字符串”,则一个内存块可以同时支持两种文字。显然,如果随后允许代码更改这些字节,则可能会发生奇怪而困难的错误。
greggo 2011年

1
@greggo:好点。在具有MMU的系统上,还有一种方法可以通过mprotect波动只读保护来实现(请参阅此处)。

因此char * p =“ blah”实际上创建了一个临时数组?weird。
rahul tyagi 2014年

1
经过2年的C ++ ... TIL编写
zeboidlund 2014年

@rahultyagi是什么意思?
Suraj Jain

105

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

在第二个例子中,字符串"string"复制从其只读家里的编译器str[]阵列。然后允许更改第一个字符。您可以通过打印每个地址来进行检查:

printf("%p", str);

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

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

13
每当在printf上使用“%p”时,都应将指针强制转换为void *,如printf(“%p”,(void *)str);一样。使用printf打印size_t时,如果使用最新的C标准(C99),则应使用“%zu”。
克里斯·杨

4
另外,只有在获取类型的大小时才需要带sizeof的括号(然后参数看起来像强制类型转换)。请记住,sizeof是运算符,而不是函数。
放松


34

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

人们所指的“只读内存”是ASM术语中的文本段。指令在内存中位于同一位置。出于安全性等明显原因,这是只读的。当您创建一个初始化为字符串的char *时,字符串数据将被编译到文本段中,并且程序会初始化指针以指向该文本段。因此,如果您尝试更改它,请kaboom。Segfault。

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


但是,是否存在允许修改“只读内存”的实现,这不是真的吗?
Pacerier

以数组形式编写时,如果初始化的字符串数据是静态的或全局的,则编译器会将其放置在数据段中。否则(例如对于常规自动数组),它将放置在堆栈上,位于函数main的堆栈框架中。正确?
SE

26

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

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类型为“ 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

如果我们这样做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

17

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

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

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

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


1
在堆栈中,或者在数据段中(如果str是global或)static
Gauthier

12

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

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


8

要了解此错误或问题,您应该首先了解b / w与指针和数组的区别,因此在这里我首先向您解释b / w与他们的区别

字符串数组

 char strarray[] = "hello";

在内存数组中存储在连续存储单元中,按[h][e][l][l][o][\0] =>[]1个字符字节大小的存储单元存储,并且此连续存储单元可以通过名为strarray的名称访问。strarray本身包含初始化为它的所有字符串。在"hello" 这种情况下,我们可以通过按索引值访问每个字符来轻松更改其内存内容

`strarray[0]='m'` it access character at index 0 which is 'h'in strarray

并且其值更改为 'm' strarray值更改为"mello" ;

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

指针

众所周知,指针指向内存中的内存位置,未初始化的指针指向随机的内存位置,因此在初始化之后指向特定的内存位置

char *ptr = "hello";

此处指针ptr初始化为字符串"hello",该字符串是存储在只读存储器(ROM)中的常量字符串,因此"hello"不能更改,因为它存储在ROM中

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

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

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

ptr="new string"; is valid

7
char *str = "string";  

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

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


6
char *str = "string";

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

char str[] = "string";

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


我们可以int *b = {1,2,3) 像写一样写char *s = "HelloWorld"吗?
Suraj Jain

6

的C常见问题是@matli挂提到它,但没有一个人在这里还没有,所以澄清:如果一个字符串(在源双引号字符串)用于任何地方以外初始化字符数组(即:@ Mark的第二个示例正确运行),该字符串由编译器存储在特殊的静态字符串表中,该类似于创建本质上是匿名的全局静态变量(当然是只读的)(没有变量“ name”) ”)。该只读部分是重要组成部分,也是为什么@马克的第一个代码示例段错误。


我们可以int *b = {1,2,3) 像写一样写char *s = "HelloWorld"吗?
Suraj Jain

4

 char *str = "string";

行定义了一个指针,并将其指向文字字符串。文字字符串不可写,因此在执行以下操作时:

  str[0] = 'z';

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

该行:

char str[] = "string";

分配一个字符数组并将文字字符串复制到该数组中,这是完全可写的,因此后续更新没有问题。


我们可以int *b = {1,2,3) 像写一样写char *s = "HelloWorld"吗?
Suraj Jain

3

诸如“字符串”之类的字符串文字可能会作为只读数据(赋予或接受您的编译器)分配在可执行文件的地址空间中。当您触摸它时,它会吓到您在其泳衣区域,并会通过段错误告知您。

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


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. 

1

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

其次,您要创建一个数组,该数组是您具有完全访问权限的内存,并使用对其进行初始化"string"。您正在创建一个char[7](六个字母,一个用于结尾的“ \ 0”),然后您可以随便使用它。


@Ferruccio ,?是的const前缀使变量为只读
EsmaeelE

在C字符串文字中,其类型为char [N],不是const char [N],因此没有警告。(您至少可以通过传递来更改gcc中的值-Wwrite-strings。)
melpomene

0

假设字符串是

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

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

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


-1

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


-2

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.