字符串与字符指针:存储方式、数组与指针的区别、常用函数实现
字符串这东西,C语言里没有专门的类型。嗯,听起来有点奇怪对吧?但这就是C的设计哲学——一切从简。字符串其实就是字符数组,末尾加个'\0'。我当年刚学的时候,总觉得这设计太原始了。后来做嵌入式项目,才明白这种设计有多灵活。
字符串的存储方式
字符串在内存里怎么存的?说白了就两种方式:栈上存、只读区存。
栈上存储(字符数组):
char str1[] = "Hello"; // 栈上分配6字节(含'\0')
char str2[10] = "Hello"; // 栈上分配10字节,多余空间填0
只读区存储(字符串常量):
char *str3 = "Hello"; // "Hello"在只读区,str3指向它
我在项目中遇到过一个问题:用指针指向字符串常量,然后试图修改它。程序直接崩溃了。为什么?因为只读区不能写。你想想看,如果所有字符串都能随便改,那多个指针指向同一个常量时,一个改了全乱套。
| 存储方式 | 位置 | 可修改 | 生命周期 |
|---|---|---|---|
| 字符数组 | 栈/全局区 | 是 | 作用域内 |
| 字符串常量 | 只读数据区 | 否 | 程序全程 |
字符数组与字符指针的区别
这两个东西,看起来差不多,用起来天差地别。我见过不少新手在这上面栽跟头。
核心区别一:sizeof 的结果不同
char arr[] = "Hello";
char *ptr = "Hello";
printf("%zu\n", sizeof(arr)); // 6(包含'\0')
printf("%zu\n", sizeof(ptr)); // 8(64位系统,指针大小)
为什么会这样?因为arr是数组名,代表整个数组。ptr只是个指针,存的是地址。我个人习惯用sizeof(arr)/sizeof(arr[0])来算数组长度,而不是用strlen——strlen要遍历,效率低。
核心区别二:赋值行为不同
char arr[] = "Hello";
char *ptr = "Hello";
// arr = "World"; // 编译错误!数组名不能赋值
ptr = "World"; // 可以,ptr指向新的常量
// arr[0] = 'h'; // 可以,修改数组内容
// ptr[0] = 'h'; // 危险!修改只读区,未定义行为
我曾经在调试一个通信协议栈时,发现数据总是不对。查了半天,原来有人用指针指向了常量字符串,然后试图修改它做缓冲区。嗯,这种bug最难找,因为不是每次都崩溃,而是偶尔出现乱码。
常用字符串函数实现
标准库提供了
strlen:计算字符串长度
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++; // 遍历到'\0'
return p - s; // 指针相减得长度
}
注意:不包含'\0'。如果s是NULL,直接崩溃。所以调用前要检查。
strcpy:字符串拷贝
char *my_strcpy(char *dest, const char *src) {
char *ret = dest;
while ((*dest++ = *src++)); // 先赋值,再判断,最后自增
return ret;
}
我曾经在项目中用strcpy,结果缓冲区溢出,把栈上的返回地址给覆盖了。程序跑飞,查了两天。从那以后,我坚持用strncpy或者自己写带长度检查的版本。
strcmp:字符串比较
int my_strcmp(const char *s1, const char *s2) {
while (*s1 && (*s1 == *s2)) {
s1++;
s2++;
}
return *(unsigned char *)s1 - *(unsigned char *)s2;
}
返回值:相等为0,s1小于s2为负,s1大于s2为正。注意用unsigned char比较,避免符号扩展问题。
strcat:字符串拼接
char *my_strcat(char *dest, const char *src) {
char *ret = dest;
while (*dest) dest++; // 找到dest末尾
while ((*dest++ = *src++)); // 拼接src
return ret;
}
实际项目中,我很少用strcat。为什么?因为不知道dest还剩多少空间。我更喜欢用snprintf,安全可控。
知识体系结构图
避坑指南
我总结几个实战中容易踩的坑:
- 忘记留'\0'的位置:char s[5] = "Hello"; 这行代码会溢出,因为需要6字节。我曾经在串口通信协议里犯过这个错,数据包尾部总是多一个乱码。
- 用==比较字符串:if (str1 == str2) 比较的是地址,不是内容。要用strcmp。我记得有个同事调试了两小时,才发现是这个问题。
- 返回局部数组的指针:函数里定义char buf[100]; return buf; 返回后栈空间被回收,指针变野指针。用静态数组或动态分配。
- strcpy不检查长度:这是缓冲区溢出的头号元凶。我建议用strncpy或snprintf替代。
我的个人建议:在嵌入式项目中,尽量不用动态内存分配(malloc/free)。用固定大小的数组+长度检查,虽然死板,但可靠。字符串操作时,始终假设输入可能非法,加一层保护。
字符串这东西,看似简单,实则暗藏杀机。理解了存储方式和指针的区别,你就能写出更健壮的代码。嗯,今天就聊到这,下次见。