C 语言 字符串复制:4 种写法一次讲清,从最小示例讲到边界

猿友 2026-09-28 10:41:04 浏览数 (22)
反馈

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

C 语言字符串复制四种写法对比示意图

一、先看结论:四种写法怎么选

下面这张表给出四种写法的适用场景与风险。选择依据只有一个:你能不能确定目标缓冲区一定放得下。

写法 复制上限 主要风险
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),结尾符不会过去,打印时就会出现乱码,这是最容易漏的一处。

0 人点赞