字符串字面量详解
1. 什么是字符串字面量?
字符串字面量就是直接写在代码里的字符串,用双引号括起来:
"Hello, World!" // 这是一个字符串字面量
"ABC" // 这也是
"123" // 这还是
"" // 空字符串字面量
2. 内存中的位置
字符串字面量存储在内存的只读数据段(通常称为.rodata段):
内存布局示例 :
代码段 ← 存放程序指令
(.text)
只读数据段 ← 存放字符串字面量
(.rodata) "Hello"、"ABC"等在这里
已初始化数据 ← 全局变量、静态变量
(.data)
未初始化数据段 ← 未初始化的全局变量
(.bss)
堆 ← malloc分配的内存
(heap)
栈 ← 局部变量、函数参数
(stack)
3. 关键特性
特性1:只读性
char *str = "Hello"; // "Hello"在.rodata段,只读
str[0] = 'h'; // 运行时错误!试图修改只读内存
// Segmentation fault (core dumped)
特性2:生命周期
// 字符串字面量的生命周期是整个程序运行期间
char* get_greeting() {
return "Hello"; // 可以返回,因为"Hello"在.rodata中
} // 不会像局部变量那样被销毁
// 对比:局部数组
char* get_bad_greeting() {
char local[] = "Hello"; // 在栈上创建数组
return local; // 危险!函数返回后local被销毁
}
返回局部数组的危险性将在文章的尾部做介绍。
特性3:唯一性(可能被合并)
char *s1 = "Hello";
char *s2 = "Hello";
// 编译器可能让s1和s2指向同一个地址
printf("s1地址: %p\n", s1); // 例如: 0x4005f4
printf("s2地址: %p\n", s2); // 也是: 0x4005f4
// 因此:
printf("%d\n", s1 == s2); // 可能是(相同地址)
4. 字符串字面量 vs 字符数组
示例1:字符串字面量
char *str_literal = "Hello"; // 指向只读内存
// "Hello"在.rodata段
// 内存布局:
// rodata段: 'H' 'e' 'l' 'l' 'o' '\0'
// str_literal → 指向这个位置
示例2:字符数组char str_array[] = "Hello"; // 在栈上创建可修改的副本
// 等价于:char str_array[] = {'H','e','l','l','o','\0'};
// 内存布局:
// 栈上: str_array[0]='H', str_array[1]='e', ...
5. 可视化对比
#include <stdio.h>
int main() {
// 情况1:字符串字面量(只读)
char *literal1 = "Hello";
char *literal2 = "Hello";
// 情况2:字符数组(可修改)
char array1[] = "Hello";
char array2[] = "Hello";
printf("=== 地址对比 ===\n");
printf("literal1: %p\n", literal1); // 相同地址(指向.rodata)
printf("literal2: %p\n", literal2); // 相同地址(指向.rodata)
printf("array1: %p\n", array1); // 不同地址(在栈上)
printf("array2: %p\n", array2); // 不同地址(在栈上)
printf("\n=== 可修改性测试 ===\n");
// 尝试修改
array1[0] = 'h'; // 可以,因为array1在栈上
printf("修改array1后: %s\n", array1); // 输出: hello
// literal1[0] = 'h'; // 取消注释会崩溃!
// printf("%s\n", literal1);
printf("\n=== 大小测试 ===\n");
printf("sizeof(literal1) = %zu\n", sizeof(literal1)); // 8(指针大小)
printf("sizeof(array1) = %zu\n", sizeof(array1)); // 6(数组大小:"Hello" + '\0' = 6字节)
return 0;
}
可能的输出:=== 地址对比 ===
literal1: 0x4005f4
literal2: 0x4005f4 ← 相同地址!
array1: 0x7ffd1234abcd
array2: 0x7ffd1234abcb ← 不同地址!
=== 可修改性测试 ===
修改array1后: hello
=== 大小测试 ===
sizeof(literal1) = 8
sizeof(array1) = 6
6. const的重要性
最佳实践:使用const
const char *str = "Hello"; // 好习惯:加上const
str[0] = 'h'; // 编译错误!编译器会报错
为什么const重要?// 没有const - 容易犯错
char *str = "Hello"; // 看起来str指向可修改内存
str[0] = 'h'; // 运行时才崩溃
// 有const - 编译器保护
const char *str = "Hello"; // 明确告诉编译器:这是只读的
str[0] = 'h'; // 编译时就报错,不会等到运行时
7. 常见的坑和错误
错误1:返回局部字符串字面量的指针(没问题)
// 这是可以的
const char* get_error_message(int code) {
switch(code) {
case 1: return "File not found";
case 2: return "Permission denied";
default: return "Unknown error";
}
}
// 字符串字面量在.rodata,不会被销毁
错误2:修改字符串字面量//错误!
char *filename = "config.txt";
filename[0] = 'C'; // 尝试改成"Config.txt" - 会崩溃!
//正确做法:
char filename[] = "config.txt"; // 创建副本
filename[0] = 'C'; // 修改副本
错误3:比较字符串字面量char *input = get_user_input(); // 假设用户输入"Hello"
//错误:比较地址而不是内容
if (input == "Hello") { // 比较指针地址
// 可能永远不会成立
}
//正确:比较内容
if (strcmp(input, "Hello") == 0) {
// 比较字符串内容
}
9. 总结
特性 字符串字面量 char *p = "abc" 字符数组 char a[] = "abc"
内存位置 只读数据段(.rodata) 栈(局部)或.data段(全局)
可修改性 不可修改 可修改
sizeof 指针大小(通常8字节) 数组大小(字符数+1)
赋值 赋值的是地址 不能直接赋值给其他数组
相同字面量 可能共享同一内存 总是独立副本
生命周期 整个程序运行期 作用域内
推荐声明 const char *p = "abc" char a[] = "abc"
"双引号里是字面量,只读内存别乱改"
"想要修改用数组,或者malloc动态开"
记住:字符串字面量就像刻在石碑上的字,你只能读不能改。如果想要修改,需要自己准备一张纸(数组或动态内存)来抄写它。
ps:返回局部数组的危险性
示例1:立即使用可能"正常"
#include <stdio.h>
char* get_bad_greeting() {
char local[] = "Hello";
printf("函数内:local地址 = %p, 内容 = %s\n", local, local);
return local; // 危险!
}
int main() {
char *ptr = get_bad_greeting();
//立即访问 - 可能看起来"正常"(但实际上危险)
printf("函数外:ptr地址 = %p, 内容 = %s\n", ptr, ptr);
return 0;
}
可能输出 :函数内:local地址 = 0x7ffd1234abcd, 内容 = Hello
函数外:ptr地址 = 0x7ffd1234abcd, 内容 = Hello ← 看起来正常!
示例2:调用其他函数后被破坏#include <stdio.h>
#include <string.h>
char* get_bad_greeting() {
char local[] = "Hello";
return local;
}
void innocent_function() {
char buffer[100];
strcpy(buffer, "这段文字会覆盖栈上的内容!");
printf("Innocent: %s\n", buffer);
}
int main() {
char *ptr = get_bad_greeting();
// 调用另一个函数
innocent_function();
// 现在再看ptr指向的内容
printf("Main: ptr内容 = %s\n", ptr); // 可能输出乱码!
return 0;
}
可能的输出 :Innocent: 这段文字会覆盖栈上的内容!
Main: ptr内容 = @#$%^&* ← 乱码!
示例3:多次调用产生奇怪结果#include <stdio.h>
char* get_bad_greeting() {
char local[] = "Hello";
return local;
}
char* get_another_greeting() {
char local[] = "World";
return local; // 同样危险!
}
int main() {
char *ptr1 = get_bad_greeting();
char *ptr2 = get_another_greeting();
// 观察奇怪的现象
printf("ptr1 = %s\n", ptr1); // 可能输出"World"!
printf("ptr2 = %s\n", ptr2); // 可能输出"World"
// 更奇怪的是:
printf("ptr1 == ptr2 ? %s\n", ptr1 == ptr2 ? "是" : "否");
// 可能输出"是"!两个指针指向相同地址
return 0;
}
可能的输出:ptr1 = World ← 应该是Hello,但被覆盖了!
ptr2 = World
ptr1 == ptr2 ? 是 ← 两个函数返回相同地址!
正确做法1:返回字符串字面量(只读)
const char* get_greeting() {
return "Hello"; //安全:字符串字面量在.rodata段
}
// 使用:
const char *str = get_greeting();
printf("%s\n", str); //正常
// str[0] = 'h'; //编译错误(因为有const)
正确做法1:使用静态变量
char* get_static_greeting() {
static char greeting[] = "Hello"; // 在.data段,不在栈上
return greeting; // 安全:静态变量生命周期是整个程序
}
// 注意:静态变量会被所有调用共享
char *s1 = get_static_greeting();
strcpy(s1, "Hi"); // 修改了静态变量
char *s2 = get_static_greeting();
printf("%s\n", s2); // 输出"Hi",不是"Hello"!
正确做法2:动态分配内存
char* get_dynamic_greeting() {
char *greeting = malloc(6 * sizeof(char)); // 在堆上分配
if (greeting != NULL) {
strcpy(greeting, "Hello");
}
return greeting; // 安全:堆内存持续存在
}
// 使用后必须释放!
char *str = get_dynamic_greeting();
if (str != NULL) {
printf("%s\n", str);
free(str); // 非常重要!
}
核心要点
1.栈内存是临时工:函数返回就"解雇"
2.返回栈地址就像给朋友你租的酒店房间钥匙:你退房后,朋友拿钥匙开门的后果不可预测
3.悬空指针的危害:
- 读取:得到垃圾数据
- 写入:可能破坏其他数据或导致崩溃
- 安全隐患:可能泄露敏感信息
这种bug特别危险,因为:
- 有时"正常"工作,掩盖了问题
- 在开发环境可能正常,生产环境才出问题
- 可能被黑客利用来读取敏感数据或执行恶意代码