1. 为什么这两个函数总被人放在一起比较
1.1 一个从面试到Code Review都绕不开的话题
有一次我给团队做Code Review,看到一位同事直接用strcpy拷贝一段从网络协议包里解析出来的字段,我当时就拦了下来。他有点不服气,说"都是拷贝,为什么不能用?"这个问题其实特别典型——strcpy和memcpy大概是C语言里被问得最多、也用得最混乱的两个函数。网上关于两者区别的帖子满天飞,但大部分回答都停在"一个是拷贝字符串,一个是拷贝内存"这种表面层次,真到写代码的时候该怎么选、为什么不安全、边界条件是什么,很多人其实没有吃透。
这篇文章想把这两个函数彻底讲明白。适合谁看?准备校招社招面试的C/C++方向同学、刚入门系统编程的新人、以及写了不少年代码但对底层行为不太较真的开发者。看完之后你会知道:面试官问这个问题时真正想考什么,以及实际工程中碰到字符串拷贝、内存拷贝该怎么决策。
1.2 名字和声明越像,越容易混淆语义
先看这两个函数在标准库里的声明:
c复制char *strcpy(char *dest, const char *src);
void *memcpy(void *dest, const void *src, size_t n);
从表面看,它们的模式几乎一样:都是一个目标地址、一个源地址,一个隐含长度、一个显式长度。这是它们被放在一起比较的第一个原因——"长得很像"。但strcpy处理的对象是"字符串",它工作的前提是src必须以'\0'结尾;而memcpy处理的对象是"一段内存",它根本不在乎里面有没有'\0',只关心你要拷贝多少个字节。
这个差异往深了说,是C语言里"字符串"和"内存块"两种抽象的区别。字符串是一种以'\0'为终止标记的字符序列,它的长度是运行时才能确定的隐式信息;而内存块是一段指定长度的连续字节,它的边界由使用者显式给出。strcpy服务于前者,memcpy服务于后者。理解了这一层,你就能明白为什么这两个函数的使用场景几乎完全不能互相替换。
1.3 C23废弃strcpy:行业在用脚投票
聊到这里有必要提一个背景:最新的C23标准已经将strcpy标记为废弃(deprecated)。这是官方在明确表态——在安全敏感的系统级编程当中,strcpy这种无法告知目标缓冲区大小的函数已经不符合现代工程的要求。
为什么还会在面试题里反复出现?因为存量代码太多、教学体系惯性太大,而且"理解strcpy为什么危险"本身就是理解C语言内存模型的一把钥匙。面试官问这个问题,很多时候不是真的指望你用strcpy写生产代码,而是看你对"字符串终止""缓冲区边界""未定义行为"这些底层概念有没有真正建立直觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 底层工作机制:一个在找路牌,一个在搬货
2.1 strcpy的本质:对'\0'的逐字节扫描
strcpy(dest, src)的行为可以用一句话概括:从src地址开始,一个字节一个字节地往dest拷贝,每次拷贝完检查当前字符是不是'\0',如果是就停止。这个过程的循环次数不受调用者控制,完全取决于src字符串的长度。
用伪代码表示大概是这样的:
c复制char *strcpy(char *dest, const char *src) {
char *p = dest;
while ((*p++ = *src++))
;
return dest;
}
注意这个while条件:先赋值,再判断赋进去的值。也就是说,'\0'也会被拷贝进dest,然后循环才停下来。这就是很多人容易忽略的一点——strcpy会把终止符一起拷贝走,目标缓冲区如果只预留了字符串内容的长度,就会溢出1个字节。别小看这1个字节,在堆内存布局里它可能恰好覆盖相邻对象的元数据,是大量堆溢出漏洞的源头。
从工作方式上看,strcpy像一个沿着路找路牌的人,路牌就是'\0',什么时候看到路牌什么时候停。它没办法提前知道要走多远,只能一步步走。
2.2 memcpy的本质:闭着眼睛按长度搬运
memcpy(dest, src, n)的行为就简单直接得多:从src拷贝n个字节到dest,循环执行n次,不管里面有没有'\0'。
c复制void *memcpy(void *dest, const void *src, size_t n) {
unsigned char *d = dest;
const unsigned char *s = src;
while (n--) {
*d++ = *s++;
}
return dest;
}
当然,这是示意实现。现代glibc里,memcpy会把n大于一定阈值的情况从逐字节循环切换成机器字长拷贝,甚至走SIMD指令,一次搬16字节、3
