字符串与字符指针:存储方式、数组与指针的区别、常用函数实现

字符串这东西,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,安全可控。

知识体系结构图

字符串与字符指针知识体系 存储方式 栈上存储(字符数组) 只读区存储(字符串常量) 全局区存储(静态数组) 数组 vs 指针 sizeof 结果不同 赋值行为不同 可修改性不同 生命周期不同 常用函数实现 strlen:遍历计数 strcpy:逐字节拷贝 strcmp:逐字节比较 strcat:末尾拼接 核心:理解内存布局,避免缓冲区溢出

避坑指南

我总结几个实战中容易踩的坑:

  • 忘记留'\0'的位置:char s[5] = "Hello"; 这行代码会溢出,因为需要6字节。我曾经在串口通信协议里犯过这个错,数据包尾部总是多一个乱码。
  • 用==比较字符串:if (str1 == str2) 比较的是地址,不是内容。要用strcmp。我记得有个同事调试了两小时,才发现是这个问题。
  • 返回局部数组的指针:函数里定义char buf[100]; return buf; 返回后栈空间被回收,指针变野指针。用静态数组或动态分配。
  • strcpy不检查长度:这是缓冲区溢出的头号元凶。我建议用strncpy或snprintf替代。

我的个人建议:在嵌入式项目中,尽量不用动态内存分配(malloc/free)。用固定大小的数组+长度检查,虽然死板,但可靠。字符串操作时,始终假设输入可能非法,加一层保护。

字符串这东西,看似简单,实则暗藏杀机。理解了存储方式和指针的区别,你就能写出更健壮的代码。嗯,今天就聊到这,下次见。

专注资料整理