- 课程
- 教程
- 面试准备
视频2022年7月7日 | 2.12万次观看
C++,字符串 保存 分享 点赞
描述
你好!在这个章节中,让我们一起来深入探讨 C/C++ 中一个非常基础但至关重要的标准库函数——strcpy()。无论你是刚刚接触 C 语言的初学者,还是正在准备技术面试的资深开发者,彻底理解字符串拷贝的底层机制都是必不可少的。我们将从最基本的概念开始,一步步解析如何将一个字符串完整地复制到另一个位置,并在这个过程中,我们将探讨 C 语言中内存管理的精髓。
为什么我们需要自定义字符串拷贝?
你可能会问,既然标准库已经提供了 strcpy,为什么我们还需要学习它的实现,甚至自己动手写一个呢?这是一个非常好的问题。通过实现这个函数,我们将能够更深刻地理解以下概念:
- 指针与数组的密切关系:理解如何在内存中连续地操作数据。
- 内存安全的重要性:认识到缓冲区溢出等常见漏洞的成因。
- C 语言字符串的约定:深入理解 C 语言中“以空字符结尾”这一核心规则。
函数原型与语法解析
首先,让我们来看看标准库中 INLINECODE835fa0bc 的定义。在 C 语言中,它定义在 INLINECODE0034d86f 头文件里;而在 C++ 中,它则包含在 中。
char* strcpy(char* dest, const char* src);
让我们仔细剖析一下这个函数签名,这包含了很多设计上的巧思:
- 返回值:函数返回一个
char*指针。具体来说,它返回的是目标内存块的起始地址。这使得我们可以进行链式调用,或者在函数执行完毕后方便地确认目标地址。 - dest (目标):这是一个指向目标数组的指针,内容将被复制到这里。注意:调用者必须确保 INLINECODE119077d3 指向的内存空间足够大,足以容纳 INLINECODEb203d47e 中的内容(包括结束符),否则会导致程序崩溃或安全漏洞。
- src (源):这是源字符串,它被 INLINECODE63527593 修饰。这意味着在 INLINECODE2dcaecbe 函数内部,我们承诺不会修改源字符串的内容,这不仅保护了数据,也使得编译器能帮我们进行更好的类型检查。
让我们动手实现:从逻辑到代码
现在,让我们把书本上的知识转化为实际的代码。我们将分步实现一个功能完善的字符串拷贝函数。
#### 方法一:标准循环实现(最直观)
这是最常见的实现方式,清晰地展示了“遍历直到遇到结束符”的逻辑。
#include
#include // 用于测试验证
// 函数实现:my_strcpy
// 功能:将 src 指向的字符串复制到 dest 指向的内存中
char* my_strcpy(char* dest, const char* src) {
// 1. 保存 dest 的原始地址
// 这非常重要,因为我们需要在循环结束时返回它
char* original_dest = dest;
// 2. 开始循环复制
// *src 取出当前字符,赋值给 *dest
// 表达式 (*dest++ = *src++) 的值是赋值后的字符
// 只要该字符不是 ‘\0‘ (即非假),循环就继续
while ((*dest++ = *src++) != ‘\0‘) {
// 循环体为空,所有工作都在条件判断中完成
// 这种写法简洁且高效,是C语言的经典风格
}
// 3. 当循环结束时,src 已经指向了 ‘\0‘
// 并且 ‘\0‘ 也已经被复制到了 dest 的上一个位置
// 4. 返回目标字符串的起始地址
return original_dest;
}
// 测试代码
int main() {
char source[] = "Hello, System Programming!";
char destination[50]; // 确保目标数组足够大
// 调用我们的函数
my_strcpy(destination, source);
std::cout << "源字符串: " << source << std::endl;
std::cout << "目标字符串: " << destination << std::endl;
// 验证返回值是否正确
assert(destination == my_strcpy(destination, source));
std::cout << "测试通过:函数返回了正确的目标地址。" << std::endl;
return 0;
}
#### 代码深度解析
让我们暂停一下,仔细看看这一行看似魔法的代码:while ((*dest++ = *src++) != ‘\0‘)。这里是整个逻辑的核心,也是新手最容易感到困惑的地方。
- 赋值即表达式:在 C/C++ 中,赋值操作 INLINECODE64c80a5a 是有返回值的,它的值就是赋值后左侧变量的值。所以,INLINECODEb72c7b38 不仅把字符复制过去了,还把这个字符本身“带”回来了。
- 后置自增 (INLINECODE26bbf4ca):INLINECODE44b3cc0a 的意思是“先使用 dest 的当前值,然后再将指针加 1”。这使得我们能够在一个表达式内完成“复制数据”和“移动指针”两个动作。
- 终止条件:当 INLINECODEa7db3b59 指向字符串结束符 INLINECODE076000a8 时,复制操作依然发生(INLINECODEd1952d97 被复制到了 INLINECODEc0f2d7a9),但表达式的结果变成了整数 0(即 INLINECODEde655c81),循环因此终止。这是一个非常优雅的设计,因为它自然地处理了字符串结束符,不需要我们在循环后再写 INLINECODE4abbb8df。
#### 方法二:不检查赋值结果的写法(更简洁)
你可能会在开源项目或旧的代码库中看到这种写法,它更短,但逻辑是一样的。
char* my_strcpy_v2(char* dest, const char* src) {
char* ptr = dest;
// 当 *src 为 ‘\0‘ 时,其值为 0,循环条件为假,停止循环
while (*src) {
*dest = *src;
dest++;
src++;
}
// 注意!这种方法需要在循环结束后手动添加结束符
*dest = ‘\0‘;
return ptr;
}
实战中的陷阱与最佳实践
虽然我们学会了如何实现 strcpy,但在实际工作中,直接使用它或类似的操作时,有几个你必须时刻警惕的“坑”。
#### 1. 缓冲区溢出
这是 C 语言中最臭名昭著的安全漏洞。如果你目标数组的大小小于源字符串的大小,strcpy 会毫不犹豫地继续写入,覆盖掉相邻内存中的数据。
// 危险示例!
char small_buffer[5]; // 只能存 4 个字符 + 1 个 ‘\0‘
char large_str[] = "This is a very long string";
// 这将导致栈破坏 或段错误
// strcpy(small_buffer, large_str); // 绝对不要这样做!
解决方案:
- 使用
strncpy:这是一个更安全的变体,它允许你指定最大复制的字符数。 - 最佳实践:在使用 INLINECODEb6506793 后,最好手动检查并添加结束符,因为 INLINECODE0fff02e9 在长度达到限制时不会自动添加
\0。
// 更安全的做法
strncpy(small_buffer, large_str, sizeof(small_buffer) - 1);
small_buffer[sizeof(small_buffer) - 1] = ‘\0‘; // 强制以 null 结尾
#### 2. 内存重叠
如果源字符串和目标字符串在内存中发生了重叠(例如,你在同一个字符串内部进行移动),标准的 strcpy 行为是未定义的。因为它先读取了数据,可能在写入之前就覆盖了还没读取的数据。
char buffer[] = "HelloWorld";
// 想要把 buffer 中的 "World" 移到开头
// strcpy(buffer, buffer + 5); // 危险!可能导致数据损坏
解决方案:在这种情况下,应使用 INLINECODEde7b63ef。INLINECODEf2112cb3 的内部实现会检查内存重叠,并自动决定是从头复制还是从尾复制,从而保证数据的正确性。
性能优化思考
作为一个追求极致的开发者,你可能会好奇:上面的 while 循环实现是最快的吗?
- 现代 CPU 优化:现代 CPU 处理字符串处理非常快。标准的库实现(如 glibc 中的 INLINECODEce1e0c89)通常针对特定架构进行了高度汇编级优化。它们可能一次读写 4 个或 8 个字节(利用 INLINECODE53c64035 类型操作),而不是一个字节一个字节地拷贝,只有当遇到字符串边界时才切换为字节操作。
- 循环展开:为了减少循环跳转的开销,我们也可以尝试简单的循环展开,但编译器通常能帮我们做这件事。
对于应用程序开发来说,清晰和正确的实现往往比微秒级的优化更重要,除非你正在编写底层库。
总结与进阶
在这篇文章中,我们一起深入探讨了 strcpy 的方方面面。我们不仅学习了如何从零开始实现它,理解了指针运算的精妙之处,还探讨了缓冲区溢出和内存重叠等在实际开发中必须面对的安全问题。
通过亲手编写 my_strcpy,我相信你对 C 语言的内存模型有了更直观的认识。这种对底层的掌控感正是 C/C++ 语言魅力的所在。
下一步建议:
现在你可以尝试挑战自己:
- 编写一个
strcat函数,用于将两个字符串连接起来。 - 实现一个
strcmp函数,用于比较两个字符串的大小。
推荐内容
<img src="/videos/cpp-program-to-find-the-sum-of-all-digits-of-a-number" alt="视频缩略图10:14" />
<img src="/videos/cpp-program-to-find-the-sum-of-all-digits-of-a-number" alt="视频缩略图" /> 7.96万 观看 | 2025/01/11 …求一个数字所有数位之和的程序
<img src="/videos/graham-scan-algorithm" alt="视频缩略图10:37" />
<img src="/videos/graham-scan-algorithm" alt="视频缩略图" /> 2.93万 观看 | 2025/01/09 …Graham 扫描算法
<img src="/videos/primality-test-introduction-and-school-method" alt="视频缩略图03:53" />
<img src="/videos/primality-test-introduction-and-school-method" alt="视频缩略图" /> 2.29万 观看 | 2025/01/08 …素性测试(简介及学校方法)
<img src="/videos/cpp-program-to-find-roots-of-a-quadratic-equation" alt="视频缩略图04:41" />
<img src="/videos/cpp-program-to-find-roots-of-a-quadratic-equation" alt="视频缩略图" /> 1.04万 观看 | 2025/01/08 …求解一元二次方程根的 C++ 程序
<img src="/videos/cpp-program-on-multiplication-of-two-matrices" alt="视频缩略图08:23" />
<img src="/videos/cpp-program-on-multiplication-of-two-matrices" alt="视频缩略图" /> 7200 观看 | 2025/01/03 …两个矩阵相乘的 C++ 程序
<img src="/videos/cpp-program-to-check-if-two-strings-are-anagrams" alt="视频缩略图08:00" />
<img src="/videos/cpp-program-to-check-if-two-strings-are-anagrams" alt="视频缩略图" /> 3800 观看 | 2025/01/03 …检查两个字符串是否为变位词的 JavaScript 程序
<img src="/videos/cpp-program-to-return-maximum-occurring-character-in-an-input-string" alt="视频缩略图03:36" />
<img src="/videos/cpp-program-to-return-maximum-occurring-character-in-an-input-string" alt="视频缩略图" /> 3100 观看 | 2024/08/16 …返回输入字符串中出现频率最高的字符的 C++ 程序
<img src="/videos/c-program-to-find-gcd" alt="视频缩略图03:12" />
<img src="/videos/c-program-to-find-gcd" alt="视频缩略图" /> 11.0万 观看 | 2024/07/18 …求最大公约数(GCD)的 C++ 程序
![视频缩略图10:49](https://medi