为什么我在写入一个“char * "初始化字符串文字,但不是"char s[]"?

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

95177 次浏览

因为在第一个例子的上下文中,"whatever"的类型是const char *(即使你将它赋值给一个非const char*),这意味着你不应该尝试写它。

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

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

在第二个例子中,字符串"string"被编译器从其只读归属数组str[]转换为复制。然后允许更改第一个字符。你可以通过打印每个地址来检查:

printf("%p", str);

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

printf("%d", sizeof(str));
char *str = "string";

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

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

在第一个代码中,"string"是一个字符串常量,字符串常量永远不应该被修改,因为它们通常被放置在只读内存中。"str"是一个用来修改常量的指针。

在第二段代码中,"string"是一个数组初始化器,类似于

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

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

char *str = "string";

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

char str[] = "string";

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

像“String”这样的字符串文字可能在可执行文件的地址空间中作为只读数据分配(通过编译器)。当你去触摸它时,它会害怕你在它的泳衣区,并让你知道一个隔离错误。

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

 char *str = "string";

Line定义了一个指针,并将其指向一个字面值字符串。字面值字符串是不可写的,所以当你这样做:

  str[0] = 'z';

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

线:

char str[] = "string";

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

参见C FAQ, 问题1.32

这些初始化之间的区别是什么?< br > char a[] = "string literal"; < br > char *p = "string literal"; < br > 如果我试图给p[i]赋一个新值,程序就会崩溃

一个:字符串字面量(正式术语 对于C语言中的双引号字符串 源)可以用在两个稍微 不同的方式:< / p >

  1. 作为char数组的初始化式,如在char a[]的声明中,它指定了初始值 该数组中的字符(并且, 如果需要,它的大小)。
  2. 在其他地方,它会变成一个未命名的静态字符数组, 这个未命名的数组可以被存储 在只读存储器中 因此不一定是 修改。在表达式上下文中, 数组立即转换为a 指针,像往常一样(参见第6节),因此 第二个声明初始化p 指向未命名数组的第一个 元素。李< / >
一些编译器有一个开关 控制是否字符串字面量 是否可写(用于编译旧的 代码),有些可能有选项 使字符串字面量正式 作为const char (for

首先,str是一个指向"string"的指针。编译器允许将字符串字面量放在内存中不能写入,但只能读取的地方。(这实际上应该触发一个警告,因为你将const char *赋值给char *。你是禁用了警告,还是忽略了它们?)

第二,你正在创建一个数组,它是你有完全访问权的内存,并使用"string"初始化它。你正在创建一个char[7](六个字母,一个结尾的'\0'),你可以对它做任何你喜欢的事情。

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

这些答案大部分都是正确的,但为了更清楚一点……

人们所说的“只读内存”是ASM术语中的文本段。它是内存中加载指令的同一个地方。出于安全等明显的原因,这是只读的。当创建一个初始化为字符串的char*时,字符串数据被编译到文本段中,程序初始化指向文本段的指针。所以如果你想改变它,就死定了。段错误。

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

// 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.

当您试图访问不可访问的内存时,会导致分割错误。

char *str是一个指向不可修改的字符串的指针(这是得到segfault的原因)。

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

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

要理解这个错误或问题,你应该首先知道指针和数组的差异 所以在这里,我首先要解释你的差异b/w他们

字符串数组

 char strarray[] = "hello";
在内存数组中存储在连续存储单元中,存储为[h][e][l][l][o][\0] =>[]是1个char字节大小的存储单元,并且这个连续存储单元可以通过名为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

为什么我得到一个分割错误时写入字符串?

c99n1256草案

字符串字面量有两种不同的用法:

  1. < p >初始化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 "String literals"中有说明:

    在转换阶段7中,值为0的字节或代码被附加到每个多字节 由一个或多个字符串字面值产生的字符序列。多字节字符 然后使用Sequence初始化一个静态存储持续时间和长度的数组 足以包含序列。对于字符串字面量,数组元素具有 类型为char,并用多字节字符的单个字节进行初始化 序列[…]< / p > 如果数组的元素具有属性,则不指定这些数组是否不同 适当的值。如果程序试图修改这样的数组,行为为 定义。< / p > 李< /引用> < / >

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";

定义类型为“指向char的指针”的p,并将其初始化为指向长度为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

假设字符串是,

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

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

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

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

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

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

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

不变的记忆

由于字符串字面量在设计上是只读的,所以它们存储在内存的不变的一部分中。存储在那里的数据是不可变的,即不能被更改。因此,在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。,最好的做法是使用const关键字,如const *s = "hello"。这样可读性更强,并且当它被违反时,编译器将提供更好的帮助。然后它会抛出类似error: assignment of read-only location ‘*s’的错误,而不是seg错误。编辑器中的linter也可能在手动编译代码之前发现错误。