网站首页 > 技术文章 正文
“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 & 反汇编窥探
- 查看段信息 objdump -h a.out | grep .rodata
可见 "hello" 存在 .rodata 段。 - 动态断点 p &s1 // 栈地址,如 0x7fff... p s2 // 指针值,通常在 0x400000 附近(程序映像内)
- 写操作验证 set {char}0x4005e4 = 'H' // 立即触发保护错误
通过调试你会直观感受两个写法的本质区别,这在排查越界或悬垂指针问题时非常关键。
七、工程化最佳实践
- 优先使用常量指针: const char *msg = "error";
明确不可写,减少误用。 - 需要可变内容时拷贝: char buf[64];
strncpy(buf, msg, sizeof(buf)); - 关注生命周期:指向字面量的指针在函数返回后仍有效;栈数组随栈帧销毁。
- 避免隐式退化陷阱:函数形参若需数组大小,应额外传递长度。
- 开启编译警告:-Wwrite-strings 可提示误写字面量风险。
结语
字符数组与字符串字面量只是 C 语言的一对“双胞胎”,却牵动着内存、安全与性能的全局。写好一行字符串声明,看似基础,却是对语言深度理解的体现。掌握两者存储差异,合理选型,才能在开发与调试中游刃有余,让每一次字符串操作都稳健可靠。
猜你喜欢
- 2025-10-02 别让 uint8 毁了你的字符串:C++ 中uint8转字符串指南
- 2025-10-02 面试懵了:StringBuilder 为什么线程不安全?
- 2025-10-02 源码分享:在pdf上加盖电子签章_pdf添加电子印章的格式
- 2025-10-02 为什么?为什么StringBuilder是线程不安全的?
- 2025-10-02 Java25的新特性_java特性有哪些
- 2025-10-02 hashCode使用指南_hashcode算法
- 2025-10-02 String、StringBuilder、StringBuffer源码分析
- 2024-08-08 Rust 与 C 之间,传递字符串的 7 种方式
- 2024-08-08 C语言将十六进制数据转换为字符串
- 2024-08-08 C语言快速入门——字符串生成(c语言程序设计字符串)
- 10-02基于深度学习的铸件缺陷检测_如何控制和检测铸件缺陷?有缺陷铸件如何处置?
- 10-02Linux Mint 22.1 Cinnamon Edition 搭建深度学习环境
- 10-02AWD-LSTM语言模型是如何实现的_lstm语言模型
- 10-02NVIDIA Jetson Nano 2GB 系列文章(53):TAO模型训练工具简介
- 10-02使用ONNX和Torchscript加快推理速度的测试
- 10-02tensorflow GPU环境安装踩坑日记_tensorflow配置gpu环境
- 10-02Keye-VL-1.5-8B 快手 Keye-VL— 腾讯云两卡 32GB GPU保姆级部署指南
- 10-02Gateway_gateways
- 最近发表
-
- 基于深度学习的铸件缺陷检测_如何控制和检测铸件缺陷?有缺陷铸件如何处置?
- Linux Mint 22.1 Cinnamon Edition 搭建深度学习环境
- AWD-LSTM语言模型是如何实现的_lstm语言模型
- NVIDIA Jetson Nano 2GB 系列文章(53):TAO模型训练工具简介
- 使用ONNX和Torchscript加快推理速度的测试
- tensorflow GPU环境安装踩坑日记_tensorflow配置gpu环境
- Keye-VL-1.5-8B 快手 Keye-VL— 腾讯云两卡 32GB GPU保姆级部署指南
- Gateway_gateways
- Coze开源本地部署教程_开源canopen
- 扣子开源本地部署教程 丨Coze智能体小白喂饭级指南
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)