C指针存储字符串为何不能修改内容

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>是前面的一大篇东西。

相关推荐
bruce_哈哈哈3 小时前
go语言初认识
开发语言·后端·golang
xiaolongmeiya3 小时前
P7082 [NWRRC 2013] Dwarf Tower 完全背包
c++
2401_876221343 小时前
因数个数、因数和、因数积
c++·算法
十五年专注C++开发3 小时前
VS2019编译的C++程序,在win10正常运行,在win7上Debug正常运行,Release运行报错0xC0000005,进不了main函数
开发语言·c++·报错c0x0000005
一条咸鱼_SaltyFish3 小时前
[Day13] 微服务架构下的共享基础库设计:contract-common 模块实践
开发语言·人工智能·微服务·云原生·架构·ai编程
隐退山林3 小时前
JavaEE:多线程初阶(一)
java·开发语言·jvm
C_心欲无痕3 小时前
ts - 模板字面量类型与 `keyof` 的魔法组合:`keyof T & `on${string}`使用
linux·运维·开发语言·前端·ubuntu·typescript
fy zs3 小时前
网络编程套接字
linux·服务器·网络·c++
最贪吃的虎3 小时前
Redis其实并不是线程安全的
java·开发语言·数据库·redis·后端·缓存·lua