优秀的编程知识分享平台

网站首页 > 技术文章 正文

【C语言·025】字符数组与字符串字面量的存储区别

nanyue 2025-10-02 04:34:57 技术文章 1 ℃

char s[] = "hi";char *p = "hi"; 有什么不同?”——这是面试里常见的“送命题”。表面看只是两行代码,实际上隐含了内存模型、编译期优化、运行期安全性等多维知识。本文带你从底层存储、可写性、sizeof 语义到调试技巧,全面拆解两者的差异与最佳实践。


一、问题的入口:两个看似相同的写法

char s1[] = "hello";
char *s2  = "hello";
  • s1:编译器会在栈(或全局/静态区)开辟 6 字节空间(含 \0),并把 "hello" 的内容逐字节拷贝进去。s1真数组,生命周期与其作用域绑定。
  • s2"hello" 被放入只读数据段(.rodata),s2 仅保存字面量首地址。它不是数组,而是一个指针变量

两行代码透露了四个关键词:存储位置、可写性、生命周期、类型退化。下面逐一分析。


二、存储位置:栈 & 静态存储区的“天然分工”

写法

数据区

指针/数组本身

char s[]

栈 / 数据段

与数据同一块内存

char *p

只读数据段(.rodata)

指针在栈/数据段

  • 字符串字面量只存一份:多个指针变量指向同一个 "hello",编译器常做常量合并(string pooling),节省空间。
  • 数组各自拷贝:每次声明 char a[] = "hello"; 都会生成新副本,占用额外栈空间,适合需要局部可变副本的场景。

三、可写性:一行代码决定“生死”

  • 数组可写 s1[0] = 'H'; // 合法
    修改发生在栈或静态数据区,可安全写入。
  • 指针所指字面量不可写 s2[0] = 'H'; // 未定义行为,常见 SIGSEGV
    现代编译器把只读段页面设为 r--,写操作触发 CPU 保护。

记忆法:指向字符串字面量的指针默认“只读”,数组拷贝的才“可写”。


四、初始化细节与数组退化

  • 数组初始化char s[] = "hi"; 等价于 char s[3] = {'h','i','\0'};,长度由编译器推导。
  • 退化规则:函数参数接收 char s[ ] 会退化为 char *, 但在定义语境仍是数组。
  • void foo(char a[]) { // 实际参数类型为 char*
    sizeof(a); // 结果是指针大小,而非数组大小
    }

五、sizeof 与 strlen:两个“坑位”

sizeof(s1)   // 6
strlen(s1)   // 5

sizeof(s2)   // 指针大小:32 位=4, 64 位=8
strlen(s2)   // 5
  • sizeof 数组得到总字节数(含终止符),指针只给指针大小
  • strlen 必须遍历遇到 \0,即运行期开销且要求字符串正确终止。

六、调试实战:GDB & 反汇编窥探

  1. 查看段信息 objdump -h a.out | grep .rodata
    可见 "hello" 存在 .rodata 段。
  2. 动态断点 p &s1 // 栈地址,如 0x7fff... p s2 // 指针值,通常在 0x400000 附近(程序映像内)
  3. 写操作验证 set {char}0x4005e4 = 'H' // 立即触发保护错误

通过调试你会直观感受两个写法的本质区别,这在排查越界或悬垂指针问题时非常关键。


七、工程化最佳实践

  1. 优先使用常量指针 const char *msg = "error";
    明确不可写,减少误用。
  2. 需要可变内容时拷贝 char buf[64];
    strncpy(buf, msg, sizeof(buf));
  3. 关注生命周期:指向字面量的指针在函数返回后仍有效;栈数组随栈帧销毁。
  4. 避免隐式退化陷阱:函数形参若需数组大小,应额外传递长度。
  5. 开启编译警告-Wwrite-strings 可提示误写字面量风险。

结语

字符数组与字符串字面量只是 C 语言的一对“双胞胎”,却牵动着内存、安全与性能的全局。写好一行字符串声明,看似基础,却是对语言深度理解的体现。掌握两者存储差异,合理选型,才能在开发与调试中游刃有余,让每一次字符串操作都稳健可靠。

最近发表
标签列表