下面我将从最基础、最安全到最贴近标准库实现的方式,分步为你讲解如何用 C 语言实现 strcpy。
最基础的实现(不安全)
这是最直接、最容易理解的实现,但它存在严重的安全隐患。
代码示例:
#include <stdio.h>
/**
* @brief 不安全的 strcpy 实现
* @param dest 目标缓冲区
* @param src 源字符串
* @return 指向目标缓冲区的指针
*/
char* my_strcpy_unsafe(char* dest, const char* src) {
char* original_dest = dest; // 保存起始地址,用于返回
// 循环直到 src 指向的字符是 '\0' (字符串结束符)
while (*src != '\0') {
*dest = *src; // 将 src 的值赋给 dest
dest++; // dest 指针后移
src++; // src 指针后移
}
// 循环结束后,*src 是 '\0',所以这里需要手动在 dest 末尾添加 '\0'
*dest = '\0';
return original_dest;
}
int main() {
char source[] = "Hello, C!";
char destination[20]; // 确保目标缓冲区足够大
my_strcpy_unsafe(destination, source);
printf("Source: %s\n", source);
printf("Destination: %s\n", destination);
return 0;
}
代码解析:
-
函数签名:
char* my_strcpy_unsafe(char* dest, const char* src)dest: 目标缓冲区的指针,我们修改这个指针指向的内容。src: 源字符串的指针,我们只读取它指向的内容,所以用const修饰,防止意外修改。char*: 返回类型,按照惯例,strcpy返回指向目标缓冲区起始位置的指针。
-
while循环:while (*src != '\0'):循环条件是检查src当前指向的字符是否是字符串的结束符\0。*dest = *src:核心操作,将src指向的字符复制到dest指向的位置。dest++和src++:指针递增,指向下一个字符。
-
添加结束符:
- 当
while循环结束时,src指向了\0。dest指向的是复制的最后一个字符的下一个位置,我们必须在这里手动写入\0,以确保dest也是一个合法的 C 字符串。
- 当
-
返回值:
return original_dest:返回指向目标缓冲区起始地址的指针,方便链式调用或后续使用。
为什么不安全?
这个实现最大的问题是缓冲区溢出。src 字符串的长度超过了 dest 缓冲区的大小(sizeof(dest)),while 循环会继续写入,覆盖掉 dest 缓冲区之外的内存,导致程序崩溃或安全漏洞。
安全的实现(增加长度检查)
为了避免缓冲区溢出,我们需要在复制前检查目标缓冲区的大小。
代码示例:
#include <stdio.h>
/**
* @brief 安全的 strcpy 实现
* @param dest 目标缓冲区
* @param src 源字符串
* @param dest_size 目标缓冲区的总大小 (包括 '\0')
* @return 指向目标缓冲区的指针
*/
char* my_strcpy_safe(char* dest, const char* src, size_t dest_size) {
if (dest_size == 0) {
return dest; // 缓冲区为0,无法写入任何内容
}
size_t i;
for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++) {
dest[i] = src[i];
}
// 循环结束后,确保 dest 总是以 '\0'
dest[i] = '\0';
return dest;
}
int main() {
char source[] = "This is a very long string that might cause overflow.";
char destination[10]; // 故意设置一个很小的缓冲区
printf("Attempting to copy a long string into a small buffer...\n");
// 调用安全版本
my_strcpy_safe(destination, source, sizeof(destination));
printf("Source: %s\n", source);
printf("Destination: %s\n", destination); // 只复制了 "This is"
return 0;
}
代码解析:
- 新增参数:
size_t dest_size,表示目标缓冲区的总容量(包括用于存放\0的一个字节)。 - 循环条件:
for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++)i < dest_size - 1:这是关键!它确保我们最多只写入dest_size - 1个字符,为最后的\0预留空间。src[i] != '\0':如果源字符串提前结束,循环也会停止。
- 添加结束符:
dest[i] = '\0';无论循环是因为哪种情况结束,我们都保证dest是一个合法的、以\0结尾的字符串。
这个版本可以防止缓冲区溢出,是更健壮的实现。
最优化的实现(指针运算)
标准库中的 strcpy 通常使用指针运算来实现,因为它比数组索引(dest[i])在某些编译器和架构下效率更高。
代码示例:
#include <stdio.h>
/**
* @brief 优化的 strcpy 实现(使用指针运算)
* @param dest 目标缓冲区
* @param src 源字符串
* @param dest_size 目标缓冲区的总大小 (包括 '\0')
* @return 指向目标缓冲区的指针
*/
char* my_strcpy_optimized(char* dest, const char* src, size_t dest_size) {
if (dest_size == 0) {
return dest;
}
char* d = dest;
const char* s = src;
// 复制字符,直到遇到 '\0' 或缓冲区满
while (--dest_size > 0 && (*d++ = *s++) != '\0') {
// 循环体为空,所有操作都在条件中完成
}
// dest_size 变为 0,说明缓冲区已满,需要手动添加 '\0'
// 如果因为 *s == '\0' 退出,循环已经将 '\0' 复制给了 *d
if (dest_size == 0) {
*d = '\0'; // 确保在缓冲区满时也添加结束符
}
return dest;
}
int main() {
char source[] = "Optimized copy";
char destination[20];
my_strcpy_optimized(destination, source, sizeof(destination));
printf("Destination: %s\n", destination);
return 0;
}
代码解析:
- 指针别名:使用
char* d = dest;和const char* s = src;创建指针别名,使代码更清晰,也避免了重复解引用dest和src。 - 精妙的循环条件:
while (--dest_size > 0 && (*d++ = *s++) != '\0')--dest_size > 0:在循环开始时先dest_size减 1,然后判断是否还大于 0,这相当于我们最多可以复制dest_size - 1个字符。(*d++ = *s++) != '\0':这是一个赋值表达式,它先执行*d = *s,d++和s++,整个表达式的值是赋值后的*d的值,如果赋值的是\0,表达式为假,循环结束。- 这个循环将复制、指针移动、判断是否结束三个操作巧妙地合并在一行,非常高效。
- 处理边界情况:如果循环因为
dest_size变为 0 而退出(即缓冲区已满,但源字符串还没结束),我们需要手动在d的当前位置(即缓冲区的末尾)写入\0,以保证字符串的正确性。
总结与对比
| 特性 | my_strcpy_unsafe |
my_strcpy_safe |
my_strcpy_optimized |
|---|---|---|---|
| 安全性 | 不安全,可能溢出 | 安全,防止溢出 | 安全,防止溢出 |
| 核心逻辑 | 简单的 while 循环 |
for 循环加长度检查 |
while 循环加指针运算 |
| 效率 | 较低 | 较低 | 高,贴近标准库实现 |
| 适用场景 | 学习理解,绝不可用于生产代码 | 需要基本安全性的场景 | 推荐在生产代码中使用的模式 |
| 标准库对应 | 无 | strncpy (但 strncpy 行为略有不同) |
strcpy (需确保缓冲区足够大) |
重要提示:在现代 C 编程中,如果你已经知道目标缓冲区足够大,直接使用标准库的 <string.h> 中的 strcpy 是最简单、最高效的选择,如果你不确定缓冲区大小,应该使用 strncpy 或者更安全的 snprintf。
#include <string.h>
#include <stdio.h>
int main() {
char src[] = "Standard Library";
char dest1[50];
char dest2[10];
// 安全使用,因为 dest1 足够大
strcpy(dest1, src);
printf("dest1: %s\n", dest1);
// 危险! dest2 太小,会导致缓冲区溢出
// strcpy(dest2, src); // 不要这样做!
// 安全的替代方案
strncpy(dest2, src, sizeof(dest2) - 1);
dest2[sizeof(dest2) - 1] = '\0'; // 确保手动添加结束符
printf("dest2: %s\n", dest2);
return 0;
}
