13、字符串字面量:为什么不能修改 char *p = "hello"; ?
这个问题,我几乎每次面试新人都会问。十个人里有八个会答错,或者答得模棱两可。说实话,我自己刚入行时也在这上面栽过跟头。
先看一段代码:
#include <stdio.h>
int main() {
char *p = "hello";
p[0] = 'H'; // 试图修改第一个字符
printf("%s\n", p);
return 0;
}
这段代码能编译通过,但运行时大概率会崩溃。为什么?
字符串字面量的本质
当你写下 "hello" 时,编译器做了两件事:
- 在程序的只读数据段(.rodata)分配一块内存,存放这6个字节(包括结尾的 '\0')
- 把这个内存地址返回给你
说白了,"hello" 是一个常量。它被放在只读区域,就像你写在纸上的合同条款——你可以看,但不能改。
核心要点:字符串字面量存储在只读内存中,任何修改操作都是未定义行为。
为什么 C 语言要这么设计?
你想想看,如果字符串字面量可以修改,会出什么问题?
- 两个不同的指针指向同一个字符串字面量,改一个会影响另一个
- 编译器无法做字符串合并优化(多个相同的字面量共用一份内存)
- 程序的安全性大打折扣
我记得有一次,我在一个嵌入式项目里看到有人这么写:
char *get_status_msg(int code) {
if (code == 0) return "OK";
if (code == 1) return "ERROR";
return "UNKNOWN";
}
然后另一个函数试图修改返回的字符串。结果程序跑着跑着就莫名其妙地崩溃了,而且不是每次都崩,是偶发性的。查了两天才找到原因——嗯,就是这种对字符串字面量的非法修改。
char *p 和 char p[] 的区别
很多人搞混这两个声明,其实它们完全不同:
| 声明方式 | 存储位置 | 能否修改 | 本质 |
|---|---|---|---|
char *p = "hello"; |
p 在栈上,指向只读数据段 | 不能修改字符串内容 | 指针指向常量 |
char p[] = "hello"; |
数组在栈上,内容从只读段拷贝过来 | 可以修改 | 数组是变量的副本 |
看个例子就明白了:
char *p = "hello";
char arr[] = "hello";
// 下面这行会崩溃
p[0] = 'H';
// 下面这行没问题
arr[0] = 'H';
为什么 arr[] 可以改?因为 "hello" 这个字面量只是用来初始化数组的,初始化完成后,数组里存的是自己的副本,跟原来的字面量没关系了。
个人习惯:我一般用 const char *p = "hello"; 来声明指向字符串字面量的指针。这样编译器会在编译阶段就帮你检查,防止误修改。
避坑指南
我曾经在一个通信协议解析模块里踩过这个坑。当时需要把收到的数据包转成可读的字符串,我图省事直接用了指针指向字面量:
char *parse_packet(uint8_t *data) {
if (data[0] == 0xAA) return "TYPE_A";
if (data[0] == 0xBB) return "TYPE_B";
return "UNKNOWN";
}
// 后来需求变更,需要在返回的字符串后面追加信息
char *result = parse_packet(data);
strcat(result, " - v1.0"); // 崩溃!
正确的做法是:如果需要修改,就用数组或者动态分配内存。
// 方案1:用数组
char result[32] = "TYPE_A";
// 方案2:动态分配
char *result = malloc(32);
strcpy(result, "TYPE_A");
现代 C 编译器的警告
好消息是,现在的编译器越来越智能了。如果你用 gcc -Wall 编译上面的代码,会收到警告:
warning: ISO C++ forbids converting a string constant to 'char*'
但注意,这只是警告,不是错误。很多老代码里充斥着这种写法,编译时也不会报错。所以靠编译器是不够的,你得自己心里有数。
总结一下
字符串字面量不能修改,根本原因在于它被放在了只读内存区域。用 char *p 指向它,相当于你手里拿着一张照片——你可以看,但不能涂改。如果你真想改,那就自己复印一份(用数组或动态内存),想怎么改都行。
这个知识点看似简单,但实际项目中出问题的频率相当高。尤其是做嵌入式开发,内存布局和权限管理比 PC 上更严格,稍不注意就是段错误。记住一句话:字面量是常量,不是变量。
警告:不要试图用强制类型转换来绕过这个限制。即使你通过某种方式成功修改了,结果也是未定义的——可能这次能跑,下次就不行了;可能在这个编译器上能跑,换个编译器就崩了。