以下代码在第 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 测试。
以下代码在第 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 常见问题解答,问题 1.32
问:这些初始化有什么区别?
char a[] = "string literal";
char *p = "string literal";
如果我尝试将新值分配给p[i]
.答:字符串文字(C 源代码中双引号字符串的正式术语)可以以两种略有不同的方式使用:
- 作为 char 数组的初始值设定项,就像在 的声明中一样
char a[]
,它指定了该数组中字符的初始值(如果需要,还指定它的大小)。- 在其他任何地方,它都会变成一个未命名的静态字符数组,并且这个未命名的数组可能存储在只读存储器中,因此不一定可以修改。在表达式上下文中,数组像往常一样立即转换为指针(参见第 6 节),因此第二个声明初始化 p 以指向未命名数组的第一个元素。
一些编译器有一个开关来控制字符串文字是否可写(用于编译旧代码),有些编译器可能有选项可以使字符串文字被正式视为 const char 数组(以便更好地捕获错误)。
通常,字符串文字在程序运行时存储在只读内存中。这是为了防止您意外更改字符串常量。在您的第一个示例中,"string"
存储在只读内存中并*str
指向第一个字符。当您尝试将第一个字符更改为'z'
.
在第二个示例中,编译器将字符串"string"
从其只读主目录复制str[]
到数组中。然后允许更改第一个字符。您可以通过打印每个地址来检查:
printf("%p", str);
此外,在第二个示例中打印的大小str
将显示编译器已为其分配了 7 个字节:
printf("%d", sizeof(str));
这些答案中的大多数都是正确的,但只是为了增加一点清晰度......
人们所指的“只读存储器”是 ASM 术语中的文本段。它与内存中加载指令的位置相同。出于安全等明显原因,这是只读的。当您创建一个初始化为字符串的 char* 时,字符串数据被编译到文本段中,程序初始化指针以指向文本段。所以如果你试图改变它,kaboom。段错误。
当编写为数组时,编译器会将初始化的字符串数据放在数据段中,这与您的全局变量等存在的位置相同。该内存是可变的,因为数据段中没有指令。这一次,当编译器初始化字符数组(仍然只是一个 char*)时,它指向的是数据段而不是文本段,您可以在运行时安全地更改它。
为什么写入字符串时会出现分段错误?
C99 N1256 草案
字符串字面量有两种不同的用法:
初始化char[]
:
char c[] = "abc";
这是“更神奇”,并在 6.7.8/14“初始化”中描述:
字符类型的数组可以由字符串字面量初始化,可选地用大括号括起来。字符串文字的连续字符(如果有空间或数组大小未知,则包括终止的空字符)初始化数组的元素。
所以这只是一个捷径:
char c[] = {'a', 'b', 'c', '\0'};
像任何其他常规数组一样,c
可以修改。
在其他任何地方:它会生成:
所以当你写:
char *c = "abc";
这类似于:
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
char[]
请注意从to的隐式转换char *
,这始终是合法的。
那么你修改了c[0]
,你也修改__unnamed
了,也就是UB。
这记录在 6.4.5“字符串文字”中:
5 在翻译阶段 7 中,将一个字节或零值代码附加到每个由一个或多个字符串文字产生的多字节字符序列。然后使用多字节字符序列来初始化一个静态存储持续时间和长度刚好足以包含该序列的数组。对于字符串文字,数组元素的类型为 char,并使用多字节字符序列的各个字节初始化 [...]
6 如果这些数组的元素具有适当的值,则未指定这些数组是否不同。如果程序尝试修改这样的数组,则行为未定义。
6.7.8/32“初始化”给出了一个直接的例子:
例 8:声明
char s[] = "abc", t[3] = "abc";
定义“普通”字符数组对象
s
,t
其元素用字符串字面量初始化。此声明与
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
section 中,而不是.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
在第一个代码中,“string”是一个字符串常量,并且永远不应该修改字符串常量,因为它们通常被放置在只读内存中。“str”是用于修改常量的指针。
在第二个代码中,“string”是一个数组初始值设定项,是
char str[7] = { 's', 't', 'r', 'i', 'n', 'g', '\0' };
“str”是分配在栈上的数组,可以自由修改。
因为在"whatever"
第一个示例的上下文中的类型是const char *
(即使您将其分配给非常量字符 *),这意味着您不应该尝试写入它。
编译器通过将字符串放入内存的只读部分来强制执行此操作,因此写入它会产生段错误。
char *str = "string";
上面的设置str
指向"string"
在程序的二进制图像中硬编码的文字值,这可能在内存中被标记为只读。
str[0]=
尝试写入应用程序的只读代码也是如此。我猜这可能是编译器依赖的。
要理解这个错误或问题,您应该首先了解指针和数组的区别,所以首先我在这里向您解释它们的区别
char strarray[] = "hello";
在内存数组中存储在连续的内存单元中,按[h][e][l][l][o][\0] =>[]
1 char 字节大小的内存单元存储,并且可以通过此处名为 strarray 的名称访问此连续的内存单元。所以这里的字符串数组strarray
本身包含初始化到它的字符串的所有字符。在此case 在这里"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
char *str = "string";
分配一个指向字符串文字的指针,编译器将其放入可执行文件的不可修改部分;
char str[] = "string";
分配并初始化一个可修改的局部数组
@matli 链接到的 C 常见问题解答提到了它,但这里还没有其他人,所以为了澄清:如果在初始化字符数组之外的任何地方使用字符串文字(源代码中的双引号字符串)(即:@马克的第二个例子,它工作正常),该字符串由编译器存储在一个特殊的静态字符串表中,这类似于创建一个本质上是匿名的全局静态变量(当然是只读的)(没有变量“名称”)。只读部分是重要部分,这也是@Mark 的第一个代码示例出现段错误的原因。
这
char *str = "string";
line 定义一个指针并将其指向一个文字字符串。文字字符串是不可写的,所以当你这样做时:
str[0] = 'z';
你得到一个段错误。在某些平台上,文字可能位于可写内存中,因此您不会看到段错误,但无论如何它都是无效代码(导致未定义的行为)。
该行:
char str[] = "string";
分配一个字符数组,并将字面量字符串复制到该数组中,该数组是完全可写的,所以后续更新没有问题。
像“string”这样的字符串文字可能作为只读数据分配在可执行文件的地址空间中(给予或接受你的编译器)。当你去触摸它时,它会吓坏你在它的泳衣区,并让你知道一个段错误。
在您的第一个示例中,您将获得指向该 const 数据的指针。在您的第二个示例中,您正在使用 const 数据的副本初始化一个包含 7 个字符的数组。
// 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.
首先,str
是一个指向 的指针"string"
。允许编译器将字符串文字放在内存中您无法写入但只能读取的位置。(这确实应该触发警告,因为您将 a 分配const char *
给 a char *
。您是否禁用了警告,或者您只是忽略了它们?)
其次,您正在创建一个数组,这是您可以完全访问的内存,并使用"string"
. 您正在创建一个char[7]
(六个用于字母,一个用于终止 '\0'),并且您可以随心所欲地使用它。
假设字符串是,
char a[] = "string literal copied to stack";
char *p = "string literal referenced by p";
在第一种情况下,当 'a' 进入范围时,将复制文字。这里 'a' 是在堆栈上定义的数组。这意味着将在堆栈上创建字符串,并将其数据从代码(文本)内存中复制,该内存通常是只读的(这是特定于实现的,编译器也可以将此只读程序数据放入可读写内存中)。
在第二种情况下,p 是在堆栈(本地范围)上定义的指针,并引用存储在其他位置的字符串文字(程序数据或文本)。通常修改这样的内存不是好的做法,也不鼓励。
首先是一个不能修改的常量字符串。其次是具有初始化值的数组,因此可以对其进行修改。
Section 5.5 Character Pointers and Functions
ofK&R
还讨论了这个话题:
这些定义之间有一个重要区别:
char amessage[] = "now is the time"; /* an array */
char *pmessage = "now is the time"; /* a pointer */
amessage
是一个数组,大到足以容纳字符序列并'\0'
对其进行初始化。数组中的单个字符可能会更改,但amessage
始终引用相同的存储。另一方面,pmessage
是一个指针,初始化为指向一个字符串常量;指针可能随后被修改为指向别处,但如果您尝试修改字符串内容,则结果未定义。
当您尝试访问无法访问的内存时会导致分段错误。
char *str
是指向不可修改的字符串的指针(出现段错误的原因)。
而是char str[]
一个数组,可以修改..