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 知识体系图
9.6 实战建议
嗯,最后说几点我这些年积累的经验:
- 成对出现:每次malloc都要想好对应的free在哪里。我习惯在函数注释里写明"调用者负责释放返回值"。
- 初始化指针:声明指针时初始化为NULL,这样即使忘记分配,free(NULL)也是安全的。
- 避免重复计算:如果多次用到字符串长度,提前算好存起来,别每次都strlen。
- 考虑realloc:如果字符串会不断增长,用realloc比每次重新malloc+free更高效。
一句话总结:动态字符串处理的核心就三件事——算对长度、检查返回值、记得释放。做到这三点,基本不会出大问题。