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 上更严格,稍不注意就是段错误。记住一句话:字面量是常量,不是变量

警告:不要试图用强制类型转换来绕过这个限制。即使你通过某种方式成功修改了,结果也是未定义的——可能这次能跑,下次就不行了;可能在这个编译器上能跑,换个编译器就崩了。

字符串字面量内存布局 char *p = "hello"; p 在栈上,指向只读段 char arr[] = "hello"; arr 在栈上,内容可修改 .rodata 只读段 "hello" 常量 栈区(可读写) "hello" 副本 关键区别 • char *p:p 本身可修改(可以指向别处),但指向的内容不可修改 • char arr[]:arr 本身不可修改(数组名是常量),但内容可以修改 • 推荐做法:用 const char *p 声明指向字面量的指针,让编译器帮你把关
公众号:蓝海资料掘金营,微信 deep3321