C指针存储字符串为何不能修改内容
本文核心问题:C语言指针存储字符串为何不能修改内容。
我们看下面程序:
c++
# include <stdio.h>
int main(void){
const char *str = 0;
str = "Hello";
str = "Hi";
printf("%s \n", str);
}
有人可能奇怪,这前面加个const,怎么还能"修改"呢,不过这篇代码是没问题的,指针可以重定向,你可以让 str 指向别的字符串。这个const只是让指针指向的内容不能修改,而不是指针本身不能再指向别的地址,如果你要声明不能修改指针指向的地址,也不能修改指向的对象的指针,可以像下面这样:
c++
const char* const a = "Hello";
再看下面代码:
c++
# include <stdio.h>
int main(void){
char *str = 0;
str = "Hello";
str[0] = 'F';
printf("%s \n", str);
}
程序报错了,执行结果是:[1] 7379 bus error ./a.out ,Xcode中运行效果如下:

也就是说,这样的指针可以重定向,但是内容是不能修改的,const只是禁止修改指针内容,而且就算没有const,你也不能修改这种情况下指针指向的内容,写const只是更安全。如果没有const保护,后面代码真不小心修改了,就会导致奇奇怪怪未定义的问题,例如段错误。
我画了张图来演示内存模型:

C语言中存储字符串通常有两种方法:
- 第一种是
char str[6]="Hello";,数组内容是可以修改的。 - 第二种就是
char *str = "Hello";在一些比较古老的代码上能见到这种写法,因为ANSI C之前没有const。
第一种是利用一个字符类型的数组去存储Hello,数组里面的结构是'H', 'e', 'l', 'l', 'o', '\0'
第二种是写法中,"Hello" 是个字符串字面量,存储在程序的只读区域,a 是个指向"字符串字面量"的指针,也就是说,a 是指向 Hello 的。
那么为什么第二种方法存储的字符串不能修改呢?这要从多个方面说起,涉及安全性、内存优化、性能、操作系统加载程序的原理,后面我分成四点,一个一个说。
原因之一:安全性
看下面代码:
c++
const char *a = "Hello";
const char *b = "Hello"; // 编译器可能优化为同一个地址
const char *c = "Hello";
编译器在优化的时候会优化成同一个地址,这样就不用存储三遍 "Hello" 了,节省空间。
如果允许修改会发生什么?把上面的代码后面加几句:
c++
const char *a = "Hello";
const char *b = "Hello"; // 编译器可能优化为同一个地址
const char *c = "Hello";
a[0] = 'h'; // 会同时影响b和c
printf("%s\n", b); // 输出"hello",这显然不是预期的
编译器把abc三个指针指向了同一个 Hello ,程序中总共只存储了一遍,当a修改内容的时候,b和c的内容也会被改掉,这是不安全的,因为a的变量会被"莫名其妙"的改掉,程序会变得混乱。
原因之二:内存优化
c++
// 编译器可以合并相同的字符串
const char *msg1 = "Error";
const char *msg2 = "Error";
const char *msg3 = "Error";
// 在内存中只存储一份"Error"
// 三个指针指向同一个地址,节省内存
这是前面说过的,字符串字面量"Error"可以合并为一个,而无需在程序以及内存中存放三份副本。
原因之三:性能
只读数据段(.rodata)可以被操作系统标记为只读。CPU可以缓存这部分内存,因为它知道内容不会改变。允许更激进的内存优化。
原因之四:操作系统在加载程序时的处理
这里我用一张图来演示:

实际验证
我使用下面的代码来演示:
c++
# include <stdio.h>
int main(void){
char *str = 0;
str = "Hello";
printf("%s \n", str);
}
macOS和Linux内存模型不一样,macOS是Mach-O段结构,我在命令行里面编译并查看 __cstring 段的内容,这个段相当于前面说的Linux中的 .rodata 段。

可以看到我代码中的"Hello"被放在了__cstring段里面(推测缩写是constant string),这正是可执行文件中的只读段。
macOS中Mach-O段结构如下图:

cc -S main.c看看汇编里面怎么写的:

最后放张图给大家看看源代码文件在预处理以后长什么样:

可以看到最后那里就是我们写的程序,前面的# include <stdio.h>是前面的一大篇东西。