9、动态字符串的处理:用malloc处理字符串、strdup函数的实现

字符串在C语言里,说白了就是一块连续的内存,末尾带个'\0'。静态字符串好处理,但动态字符串——嗯,这里才是真正考验功底的地方。我见过太多新手在字符串上栽跟头,今天就聊聊怎么用malloc把字符串玩明白。

9.1 为什么需要动态字符串?

你想想看,如果写一个函数要拼接两个字符串,或者从文件里读一行不定长的文本,用固定大小的数组行不行?

char buffer[256];  // 万一输入超过256呢?

我在项目中遇到过这样的bug:用户输入了一个超长的名字,直接导致缓冲区溢出,程序崩溃。从那以后,我养成了一个习惯——只要字符串长度不确定,就用动态内存。

核心原则:静态数组适合已知最大长度的场景;动态分配适合长度可变或未知的场景。

9.2 用malloc处理字符串的基本操作

9.2.1 分配与复制

假设你要复制一个字符串,不能直接赋值指针——那只是复制了地址。你得自己分配内存,再把内容拷过去。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    const char *src = "Hello, dynamic world!";
    
    // 分配空间:长度+1(给'\0'留位置)
    char *dst = (char *)malloc(strlen(src) + 1);
    if (dst == NULL) {
        fprintf(stderr, "内存分配失败\n");
        return 1;
    }
    
    // 复制内容
    strcpy(dst, src);
    
    printf("复制结果: %s\n", dst);
    
    free(dst);
    return 0;
}

我的习惯:每次malloc后立刻检查返回值,不为NULL才继续。这个习惯帮我省了无数次调试时间。

9.2.2 字符串拼接

拼接字符串时,很多人会犯一个错误——忘记给'\0'留空间。我曾经在代码审查时看到这样的写法:

// 错误示范
char *result = (char *)malloc(strlen(s1) + strlen(s2));
strcpy(result, s1);
strcat(result, s2);  // 这里会越界!

正确的做法是:

char *concat_strings(const char *s1, const char *s2) {
    size_t len1 = strlen(s1);
    size_t len2 = strlen(s2);
    
    char *result = (char *)malloc(len1 + len2 + 1);
    if (result == NULL) return NULL;
    
    strcpy(result, s1);
    strcat(result, s2);
    
    return result;
}

9.3 strdup函数的实现

strdup是个很实用的函数,但它是POSIX标准里的,不是C标准库的一部分。说白了,有些平台可能没有它。所以我建议你自己实现一个,这样代码移植性更好。

strdup的功能很简单:传入一个字符串,返回一个动态分配的新副本。

char *my_strdup(const char *s) {
    if (s == NULL) return NULL;  // 防御性编程
    
    size_t len = strlen(s);
    char *copy = (char *)malloc(len + 1);
    
    if (copy != NULL) {
        strcpy(copy, s);
    }
    
    return copy;
}

注意:调用者必须负责free返回的指针。我曾经见过有人strdup后忘了free,导致内存泄漏——这种bug最难查。

9.3.1 为什么不用strcpy直接返回?

你可能会问:为什么不直接返回一个指向原字符串的指针?因为原字符串可能被修改或释放。strdup创建了一个完全独立的副本,互不影响。

9.3.2 一个更健壮的版本

我个人习惯在实现时加一些额外的安全检查:

char *safe_strdup(const char *s) {
    if (s == NULL) {
        return NULL;
    }
    
    size_t len = strlen(s);
    if (len == 0) {
        // 空字符串也要分配一个字节放'\0'
        char *empty = (char *)malloc(1);
        if (empty) *empty = '\0';
        return empty;
    }
    
    char *copy = (char *)malloc(len + 1);
    if (copy == NULL) {
        fprintf(stderr, "safe_strdup: 内存分配失败,请求大小 %zu\n", len + 1);
        return NULL;
    }
    
    memcpy(copy, s, len + 1);  // 比strcpy更快
    return copy;
}

小技巧:用memcpy代替strcpy可以避免strcpy每次复制时检查'\0'的开销,因为我们已经知道长度了。

9.4 动态字符串的常见陷阱

陷阱 后果 解决方法
忘记给'\0'留空间 缓冲区溢出 总是分配 strlen + 1
忘记free 内存泄漏 成对使用malloc/free
对NULL指针strcpy 程序崩溃 调用前检查是否为NULL
多次free同一指针 未定义行为 free后置NULL

9.5 知识体系图

动态字符串处理知识体系 动态字符串 内存分配 malloc + 1 检查NULL 字符串操作 strcpy/strcat memcpy(推荐) strdup实现 分配+复制 防御性检查 常见陷阱 忘记'\0'空间 | 忘记free | 对NULL操作 | 多次free

9.6 实战建议

嗯,最后说几点我这些年积累的经验:

  • 成对出现:每次malloc都要想好对应的free在哪里。我习惯在函数注释里写明"调用者负责释放返回值"。
  • 初始化指针:声明指针时初始化为NULL,这样即使忘记分配,free(NULL)也是安全的。
  • 避免重复计算:如果多次用到字符串长度,提前算好存起来,别每次都strlen。
  • 考虑realloc:如果字符串会不断增长,用realloc比每次重新malloc+free更高效。

一句话总结:动态字符串处理的核心就三件事——算对长度、检查返回值、记得释放。做到这三点,基本不会出大问题。

公众号:蓝海资料掘金营,微信deep3321