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

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

提问人:Markus 提问时间:10/3/2008 最后编辑:Antti Haapala -- Слава УкраїніMarkus 更新时间:10/26/2023 访问量:101089

问:

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

分段故障 C 串

评论

4赞 Maverick Meerkat 9/13/2016
这很有趣 - 但是在Visual Studio开发人员命令提示符上使用Windows编译器(cl)时,这实际上可以完美地编译和运行。让我困惑了一会儿......

答:

12赞 Mike F 10/3/2008 #1

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

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

120赞 Greg Hewgill 10/3/2008 #2

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

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

printf("%p", str);

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

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

评论

17赞 Chris Young 10/3/2008
每当在 printf 上使用 “%p” 时,您都应该将指针转换为 void *,就像在 printf(“%p”, (void *)str);使用 printf 打印size_t时,如果使用最新的 C 标准 (C99),则应使用“%zu”。
6赞 unwind 11/25/2008
此外,只有在取类型的大小时才需要带有 sizeof 的括号(然后参数看起来像强制转换)。请记住,sizeof 是一个运算符,而不是一个函数。
1赞 phuclv 4/11/2017
并使用 %zu 打印size_t
0赞 john 2/26/2021
警告:格式 [-Wformat=] 中的未知转换类型字符“z”:/
9赞 DougN 10/3/2008 #3
char *str = "string";  

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

尝试写入应用程序的只读代码也是如此。不过,我猜这可能取决于编译器。str[0]=

18赞 Andru Luvisi 10/3/2008 #4

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

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

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

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

评论

1赞 Gauthier 3/24/2016
在堆栈上,或者数据段(如果是全局或 .strstatic
6赞 Rob Walker 10/3/2008 #5
char *str = "string";

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

char str[] = "string";

分配并初始化一个可修改的局部数组

评论

0赞 Suraj Jain 12/27/2016
我们能像写一样写吗?int *b = {1,2,3)char *s = "HelloWorld"
0赞 Jitu DeRaps 9/19/2022
@SurajJain不,我们不能这样做,因为它将是无效的转换( int 到 int* )。此外,我们不能写这也将是无效的转换( char to char*)。char* ptr = { 'a', 'b'};
3赞 Jurney 10/3/2008 #6

像“string”这样的字符串文字可能被分配到可执行文件的地址空间中作为只读数据(提供或接受编译器)。当你去触摸它时,它会吓坏你在其泳衣区域,并用 seg 故障通知您。

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

4赞 Michael Burr 10/3/2008 #7

 char *str = "string";

line 定义一个指针并将其指向文本字符串。文字字符串是不可写的,因此当您这样做时:

  str[0] = 'z';

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

该行:

char str[] = "string";

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

评论

0赞 Suraj Jain 12/27/2016
我们能像写一样写吗?int *b = {1,2,3)char *s = "HelloWorld"
283赞 matli 10/3/2008 #8

参见 C 常见问题解答,问题 1.32

:这些初始化之间有什么区别?


如果我尝试为 分配一个新值,我的程序会崩溃。
char a[] = "string literal";char *p = "string literal";p[i]

:字符串文字(正式术语 用于 C 中的双引号字符串 source)可分两用 不同的方式:

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

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

评论

10赞 greggo 8/27/2011
其他几点:(1)段错误如所描述的那样发生,但它的发生是运行环境的函数;如果相同的代码在嵌入式系统中,则写入可能不起作用,或者实际上可能会将 S 更改为 Z。 (2)由于字符串文字是不可写的,编译器可以通过将两个“字符串”实例放在同一个位置来节省空间;或者,如果代码中的其他位置有“另一个字符串”,那么一个内存块可以同时支持这两个文本。显然,如果允许代码更改这些字节,可能会出现奇怪而困难的错误。
1赞 5/2/2013
@greggo:说得好。在具有 MMU 的系统上,还有一种方法可以通过使用 to wave 只读保护来做到这一点(请参阅此处)。mprotect
0赞 rahul tyagi 12/3/2014
所以 char *p=“blah” 实际上创建了一个临时数组 ?weird。
3赞 zeboidlund 12/28/2014
经过 2 年的 C++ 写作...直到
3赞 ikegami 11/15/2019
@rahul tyagi,不是临时数组。恰恰相反,它是寿命最长的数组。它由编译器创建,可在可执行文件本身中找到。从上面你应该明白的是,它是一个共享数组,必须被视为只读(实际上可能是只的)。
1赞 David Thornley 10/3/2008 #9

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

其次,你要创建一个数组,这是你拥有完全访问权限的内存,并使用 初始化它。你正在创建一个(六个用于字母,一个用于终止“\0”),然后你用它做任何你喜欢的事情。"string"char[7]

评论

0赞 EsmaeelE 12/9/2017
@Ferruccio,?是,前缀使变量为只读const
0赞 melpomene 6/13/2018
在 C 语言中,字符串文字的类型为 ,而不是 ,因此没有警告。(您至少可以通过传递 在 gcc 中更改它。char [N]const char [N]-Wwrite-strings
6赞 rpj 10/3/2008 #10

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

评论

0赞 Suraj Jain 12/27/2016
我们能像写一样写吗?int *b = {1,2,3)char *s = "HelloWorld"
45赞 Bob Somers 10/3/2008 #11

这些答案中的大多数都是正确的,但只是为了更清楚一点......

人们所指的“只读内存”是 ASM 术语中的文本段。它与内存中加载指令的位置相同。出于显而易见的原因,例如安全性,这是只读的。创建初始化为字符串的 char* 时,字符串数据将编译到文本段中,程序初始化指向文本段的指针。所以如果你试图改变它,kaboom。Segfault。

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

评论

1赞 Pacerier 9/21/2013
但是,难道不是真的可以有允许修改“只读内存”的实现吗?
0赞 S E 12/4/2019
当编写为数组时,编译器会将初始化的字符串数据放在数据段中(如果它们是静态的或全局的)。否则(例如,对于普通的自动数组),它放置在堆栈上,在函数 main 的堆栈帧中。正确?
0赞 Olov 12/27/2020
@SE是的,我想 Bob Somers 在编写“数据段”时指的是堆栈、堆和静态(包括静态和全局变量)。并且将本地数组放在堆栈上,因此您是正确的:)
0赞 Olov 12/27/2020
对不起,但你在这里可能是对的,数据段是专用于初始化的全局或静态变量的内存部分,但如果数组是本地的,也可以放在堆栈上,正如你所写的那样。
1赞 puppydrum64 12/13/2022
@Pacerier有。如果这是来自 8 位微型软盘的代码,您绝对可以修改它。它在 RWX 标志意义上是“只读”的,而不是 RAM 与 .ROM 意义上的。
2赞 jokeysmurf 1/20/2012 #12
// 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赞 Raghu Srikanth Reddy 1/4/2013 #13

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

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

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

0赞 libralhb 10/11/2013 #14

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

8赞 user2426842 12/7/2013 #15

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

字符串数组

 char strarray[] = "hello";

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

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

其值更改为 因此 strarray 值更改为'm'"mello";

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

指针

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

char *ptr = "hello";

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

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

所以 ptr[0]='m' 是无效的,因为你不能访问只读内存

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

ptr="new string"; is valid
36赞 Ciro Santilli OurBigBook.com 6/5/2015 #16

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

C99 N1256 草案

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

  1. 初始化:char[]

    char c[] = "abc";      
    

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

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

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

    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;
    

    请注意隐式转换 from to ,这始终是合法的。char[]char *

    那么如果你修改 ,你也会修改 ,这就是 UB。c[0]__unnamed

    这记录在 6.4.5 “字符串文字”中:

    5 在转换阶段 7 中,每个多字节都会附加一个值为零的字节或代码 由一个或多个字符串文本生成的字符序列。多字节字符 然后,序列用于初始化静态存储持续时间和长度的数组 足以包含序列。对于字符串文字,数组元素具有 键入 char,并使用多字节字符的单个字节进行初始化 序列 [...]

    6 没有具体说明这些数组是否是不同的,前提是它们的元素具有 适当的值。如果程序尝试修改此类数组,则行为为 定义。

6.7.8/32 “初始化”给出了一个直接的例子:

示例 8:声明

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

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

此声明等同于

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

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

char *p = "abc";

使用类型“指向 char”进行定义,并将其初始化为指向类型为“array of char”且长度为 4 的对象,其元素使用字符串文字初始化。如果尝试用于修改数组的内容,则行为是未定义的。pp

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 将其存储在 section 中,而不是 .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

评论

0赞 puppydrum64 12/13/2022
换句话说,字符串太短了,在我们的例子中,字节被解释为 一个 并以这种方式推送到堆栈上。int
0赞 Ciro Santilli OurBigBook.com 12/15/2022
@puppydrum64它与字符串大小无关。必须始终使用堆栈(因为它可以根据 C 标准进行修改)。另一个可以放在堆栈或文本上,但文本效率更高(因为您不需要在每次函数调用时都不断加载新副本)。
1赞 Venki 2/4/2019 #17

假设字符串是,

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

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

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

1赞 Hari 8/1/2021 #18

Section 5.5 Character Pointers and FunctionsOF 还讨论了这个话题:K&R

这些定义之间存在重要区别:

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

amessage是一个数组,刚好足够大以容纳字符序列并对其进行初始化。数组中的单个字符可能会更改,但将始终引用同一存储。另一方面,是一个指针,初始化为指向字符串常量;指针随后可能会被修改为指向其他位置,但如果尝试修改字符串内容,则结果未定义。'\0'amessagepmessage

2赞 Tim Skov Jacobsen 9/1/2021 #19

恒定内存

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

堆栈内存

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


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

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

当我们定义一个指向字符串文字的指针时,我们正在创建一个存在于堆栈内存中的指针变量。它指向基础字符串文本所在的只读地址。

#include <stdio.h>

int main(void) {
  char *s = "hello";
  printf("%p\n", &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\n", &a);  // Prints the Stack address, e.g. 0x7ffc7a9a9db2
  printf("%s\n", a);   // Prints "Hello"

  return 0;
}

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

0赞 raddevus 10/26/2023 #20

第一个的含义与你想象的不同:

char *str = "string";

这意味着,

  1. 存储我的字符串 ->“string”
  2. 在地址(由编译器选取)
  3. 将“string”存储的地址保存在名为 的变量中。str

接下来,尝试更改存储字符串“string”的地址

str[0] = 'z';  // could be also written as *str = 'z'

尝试一个简单的程序

您可以通过简单地稍微更改您的程序来证明这是真的。

尝试编译并运行以下命令:

#include <stdio.h>
int main()
{
    char *str = "string";
    // str[0] = 'z';  // could be also written as *str = 'z'
    // PRINT the value contained in str (it's an address - a pointer which points to "string")
    printf("%p\n", str); 
}

在我的机器上,结果输出为:0x55e789c1d004

当您尝试使用 str[0] = 'z' 写入时,您尝试将地址 'z' 存储在地址上str[0]0x55e789c1d004