C 语言的字符串复制踩坑率极高,一个没检查的目标缓冲区长度就能让程序崩溃。本文用最小可编译示例,讲清 strcpy、strncpy、memcpy 和手写循环四种写法的边界差异,重点说明各自会在什么情况下写越界、以及该怎么选。核心结论是:复制前必须先确认目标缓冲区足够大,四种写法里只有 strncpy 与手写循环能显式设上限,其余两种都靠开发者自觉。下面每段代码都给出逐行注释和失败场景。先确认缓冲区大小,再决定用哪种写法。

一、先看结论:四种写法怎么选
下面这张表给出四种写法的适用场景与风险。选择依据只有一个:你能不能确定目标缓冲区一定放得下。
| 写法 | 复制上限 | 主要风险 |
|---|---|---|
strcpy |
无 | 源字符串过长直接写越界 |
strncpy |
指定 n |
n 大于源长度时残留旧数据,不加 \\0 |
memcpy |
指定 n |
按字节复制,可能不带字符串结尾 |
| 手写循环 | 由自己控制 | 代码长,容易漏写结尾符 |

需要提醒的是,除 strcpy 外,没有一个函数会自动帮你在结尾补 \\0,所以「复制完是不是合法字符串」这件事始终要你自己负责。想在线对比不同写法的实际效果,可用 在线代码实例 验证实际结果。
二、环境确认与最小示例
先写一份最小示例,覆盖最常见也最容易出错的场景:把短字符串复制进刚好容得下的缓冲区。示例基于 C99 标准,用 GCC 或 Clang 均可编译;不同编译器对越界的报错表现不同,但结论一致。除第一份完整程序外,后面各节代码均为局部片段,省略了头文件与 main 函数,按片段理解即可。
# 查看编译器版本并编译下面的示例
gcc --version
gcc -o demo demo.c
#include <stdio.h>
#include <string.h>
int main(void) {
char src[] = \"hello\"; // 源字符串,含结尾符共 6 字节
char dst[16]; // 目标缓冲区,留足余量
strcpy(dst, src); // 库函数复制,自动带上结尾符
printf(\"%s\\n\", dst); // 预期输出 hello
return 0;
}
上面这份能跑通,但 strcpy 的风险就在「能跑通」这三个字上。一旦把 dst 改成 char dst[4],同样的代码就会写越界,而且是否立刻崩溃取决于编译器和操作系统,属于典型的未定义行为。写代码前先确认缓冲区大小,比事后调试重要得多。想系统补齐 C 语言基础,可以看 C 语言教程。
三、写法一:strcpy 不带长度检查
strcpy 从源地址逐个复制到结尾符为止,不做任何长度检查。它只适合你百分百确定源字符串短于目标缓冲区的场景。
#include <string.h>
char dst[16];
char src[] = \"hello\";
strcpy(dst, src); // 安全:源 5 字符 + 结尾符,目标有 16 字节
// 危险:目标只有 4 字节,源却要 6 字节,多出的两个字节写到了缓冲区外
char too_small[4];
// strcpy(too_small, src); // 若真执行,属于未定义行为,可能崩溃也可能静默通过
这里的失败特点是:不报错、不崩溃,但内存已经被破坏。修复方向只有两个,一是把目标缓冲区放大到能容纳最长可能输入,二是改用带长度上限的写法。判断标准也很简单:源字符串长度在运行前能不能确定,不能就别用 strcpy。
四、写法二:strncpy 带长度上限
strncpy 限制复制的最大字节数,是当前最常被推荐的写法。但它有两个容易忽略的细节:结尾符不一定被写入,缓冲区过大时残留旧数据。
#include <string.h>
char dst[16];
char src[] = \"hi\";
strncpy(dst, src, sizeof(dst) - 1); // 最多复制 15 字节,为目标留一个位置
dst[sizeof(dst) - 1] = \'\\0\'; // 手动补结尾符,保证是合法字符串
第二条补 \\0 不是可选项。如果源字符串比 n 短,strncpy 会用 0 填满剩余部分;如果源更长且 n 恰好用完,n 位置就不会有结尾符,此时缓冲区里是一段没有结尾的数据。更隐蔽的问题是残留:缓冲区里原有内容可能仍在结尾符之后被打印出来。这两个坑都属于「代码看着对,输出不对」的典型。
五、写法三与写法四:memcpy 与手写循环
memcpy 按字节复制,不认字符串结尾符,适合明确知道长度的场景。手写循环最灵活,上限完全由自己控制。
#include <string.h>
char dst[16];
char src[] = \"hello\";
// 写法三:memcpy 按字节复制,长度必须自己算准,且不使用字符串结尾符
memcpy(dst, src, strlen(src) + 1); // +1 把结尾符一起复制过去
// 写法四:手写循环,逐字节复制并在结尾补 \'\\0\',上限完全可控
size_t i;
for (i = 0; i < sizeof(dst) - 1 && src[i] != \'\\0\'; i++) {
dst[i] = src[i]; // 逐字节赋值,过程透明
}
dst[i] = \'\\0\'; // 循环结束后补结尾符
memcpy 的判定标准是长度是否已知。已知就快且安全,未知就别用。手写循环的优势是把边界写在代码里,哪一步停都由自己决定,代价是多写几行。这四种写法都要求一件事:目标缓冲区必须事先确认足够大,这不是函数能替你做的事。常见排错按下面清单走:
| 现象 | 常见原因 | 修复方向 |
|---|---|---|
| 打印结果后面跟着乱码 | 复制完没写结尾符 \\0 |
手动补 dst[n-1] = \'\\0\' 或改用 strlen(src)+1 |
| 输出是新旧数据拼接 | 缓冲区过大且未清空,残留旧内容 | 补结尾符,或用 memset 先清空目标 |
| 运行崩溃且位置随机 | strcpy 写越界,破坏相邻内存 |
改用 strncpy 或手写循环,并放大目标缓冲区 |
用 memcpy 后打印异常 |
只复制了字节,字符串结尾符没到位 | 长度计算包含 +1,或复制后手动补 \\0 |
以上失败与修复均来自真实边界场景,未在本机执行,按 C 标准与库函数文档推断。想进一步了解字符串结尾符的作用,可读 C 语言指针与字符串。把复制限制在明确的字节范围内,是这类代码唯一能长期稳住的做法。
总结
C 语言字符串复制,先记住一件事:所有复制函数都不会替你检查目标缓冲区,也基本不会自动补结尾符。四种写法的取舍很清楚,strcpy 只在源长度确定且缓冲区留足余量时才安全;strncpy 是默认推荐,但要手动补结尾符并注意残留数据;memcpy 适合长度已知的场景,别忘了 +1;手写循环最灵活,边界完全自己控制。无论选哪种,动手前先确认缓冲区够不够,这一步不做,后面四种写法都救不回来。
延伸学习
想在编程狮系统学 C 语言字符串与数组,可以顺着下面三篇深入:
常见问题
Q:strcpy 和 strncpy 到底该用哪个?
A:默认用 strncpy,并手动补结尾符。只有在你百分百确定源字符串长度可控、且目标缓冲区远大于它时,才考虑 strcpy。写越界的代价是未定义行为,可能延迟到很久以后才暴露。
Q:加了 strncpy 就安全了吗?
A:不一定。strncpy 只限制复制上限,不保证结尾符存在,也不清空目标缓冲区的残留内容。安全的前提是三点都做到:上限算对、结尾符补上、缓冲区大小确认够。
Q:为什么复制完打印会出现乱码?
A:因为没有结尾符 \'\\0\'。字符串以结尾符为边界,缺了它,打印会一直读到内存里别的字节。修复方向是复制后手动补 dst[上限] = \'\\0\'。
Q:memcpy 能直接用来复制字符串吗?
A:可以,但长度为 strlen(src) + 1,要把结尾符一起复制。如果只传 strlen(src),结尾符不会过去,打印时就会出现乱码,这是最容易漏的一处。

TRAE-AI编程



