目标:在 不破坏性能 的前提下,写出边界安全、可维护的字符串代码;给出 strcpy/strcat 的替代方案、容量与长度的双指标思维、以及 UTF-8 等多字节编码的注意事项。
1)为什么 strcpy/strcat 不安全?
- 不感知容量:一旦源比目标长就越界。
- 不自动加边界:错误难以暴露,常在上线后“随机崩”。
- 多字节编码下更难排查。
2)容量与长度的双指标
- 容量(capacity):目标缓冲区能容纳的最大字节数 cap。
- 长度(length):当前字符串有效字节数,不含终止符 '\0'。
规则:任何写入都必须保证 len < cap 且写完 buf[last]='\0'。
3)替代方案速查
- 组合拳(标准可移植):snprintf + strnlen + memcpy/memmove。
- 可选增强(平台相关):strlcpy/strlcat(BSD/部分平台)、asprintf(GNU 扩展)。
安全拷贝模板(标准库即可)
// 把 src 拷贝进 dst,确保以 '\0' 结尾且不越界
void safe_copy(char *dst, size_t cap, const char *src){
if (cap == 0) return;
size_t n = strnlen(src, cap-1);
memcpy(dst, src, n);
dst[n] = '\0';
}
安全拼接模板
size_t safe_cat(char *dst, size_t cap, const char *suffix){
size_t dlen = strnlen(dst, cap);
if (dlen == cap) return dlen; // 已被截断
size_t room = cap - dlen - 1;
size_t n = strnlen(suffix, room);
memcpy(dst + dlen, suffix, n);
dst[dlen + n] = '\0';
return dlen + n;
}
格式化优先用 snprintf
snprintf(buf, sizeof buf, "%s-%d", tag, id); // 自动截断 + 结尾 '\0'
4)重叠区域:memmove 胜于 memcpy
// 原地删除前缀 prefix(若存在)
void drop_prefix(char *s, const char *prefix){
size_t n = strlen(prefix);
if (strncmp(s, prefix, n)==0) memmove(s, s+n, strlen(s+n)+1);
}
memcpy 对重叠未定义;memmove 处理重叠安全。
5)动态拼接:可增长“字符串构建器”
struct sbuf { char *p; size_t len, cap; };
int sbuf_append(struct sbuf *b, const char *s){
size_t add = strlen(s);
if (b->len + add + 1 > b->cap) {
size_t need = b->len + add + 1;
size_t newcap = b->cap? b->cap*2 : 32;
if (newcap < need) newcap = need;
char *tmp = realloc(b->p, newcap);
if (!tmp) return -1;
b->p = tmp; b->cap = newcap;
}
memcpy(b->p + b->len, s, add+1);
b->len += add;
return 0;
}
6)编码注意:UTF-8 ≠ 宽字符
- UTF-8 的“长度”是字节数,不是字符数;一个“字”可能占 3 字节。
- 需要“字符级”裁剪/截断时,必须沿着 码点边界 处理,或转成 宽字符(wchar_t)/库处理。
- 混合中文/表情符时,避免按字节随意 substr,容易“半个字”变乱码。
7)输入读取:拒绝 gets,谨慎 scanf("%s")
- gets 已被移除。
- scanf("%s", buf) 无边界 → 用 "%15s" 这种限制宽度,或用 fgets。
- 文本行读取常用:fgets(buf, sizeof buf, fp),随后去掉末尾 '\n'。
8)自检清单
- 始终带容量参数
- 写完保证 '\0' 终止
- 重叠用 memmove
- 动态构建器按倍数扩容
- UTF-8 操作遵循码点边界
- 输入函数限制宽度或用 fgets
9)迷你练习
1)把 strcpy(dst, src) 改写成容量安全的版本(见 §3)。
2)写 trim_right(char *s):去掉行尾 \r\n\t 空格,不越界。
3)写 join3(buf,cap,a,b,c):把三个字符串按顺序拼接入 buf。
