引言:为什么需要复现标准库字符串函数?
在C语言开发中,``提供的字符串函数(如strlen
、strcpy
)是最常用的工具之一。但这些函数的底层实现逻辑你真的清楚吗?
- 学习价值:复现标准库函数能帮你深入理解字符串操作的底层逻辑(如空终止符的作用、内存复制的安全性);
- 工程实践:在嵌入式开发、操作系统内核等场景中,可能因内存限制或安全要求无法直接使用标准库,需自定义实现;
- 避坑指南:了解标准库函数的潜在问题(如
strcpy
的缓冲区溢出风险),能帮助你在实际开发中写出更安全的代码。
今天,我们就通过复现6个核心字符串函数(strlen
、strcpy
、strncpy
、strcat
、strncat
、strcmp
),彻底掌握字符串操作的底层原理!
复现1:my_strlen
——计算字符串长度
功能说明
my_strlen
用于计算字符串的有效字符数(不包含空终止符\0
)。
实现原理
从字符串起始地址开始遍历,每遇到一个非\0
字符计数加1,直到遇到\0
停止。
1 2 3 4 5 6 7 8
| size_t my_strlen(const char *p) { size_t count = 0; while (*p != '\0') { // 遍历直到空终止符 count++; p++; // 移动到下一个字符 } return count; }
|
关键点
- 使用
size_t
类型避免负数问题(长度不可能为负);
- 时间复杂度为O(n)(n为字符串长度),空间复杂度为O(1)(仅用计数器)。
测试用例
1 2
| char p[30] = "122345"; printf("%zu\n", my_strlen(p)); // 输出6(字符'1','2','2','3','4','5')
|
复现2:my_strcpy
——复制字符串内容
功能说明
my_strcpy
将源字符串(src
)的内容复制到目标字符串(dest
),并确保dest
以\0
结尾。
实现原理
- 保存
dest
的起始地址(用于返回);
- 逐个复制
src
的字符到dest
,直到遇到src
的\0
;
- 手动在
dest
末尾添加\0
(确保目标字符串合法)。
1 2 3 4 5 6 7 8
| char *my_strcpy(char *dest, const char *src) { char *tmp = dest; // 保存目标起始地址 while (*src != '\0') { // 复制直到源字符串结束 *dest++ = *src++; // 逐个字符复制(后置++避免覆盖) } *dest = '\0'; // 手动添加空终止符 return tmp; // 返回目标起始地址 }
|
关键点
- 必须确保
dest
有足够空间容纳src
的内容(否则会导致缓冲区溢出);
- 返回
dest
的起始地址是为了支持链式调用(如strcpy(dest, strcpy(tmp, src))
)。
测试用例
1 2 3
| char p[30] = "122345"; char q[] = "12234"; printf("%s\n", my_strcpy(p, q)); // 输出"12234"(p被覆盖为q的内容)
|
复现3:my_strncpy
——安全复制(限制长度)
功能说明
my_strncpy
将源字符串的前n
个字符复制到目标字符串,若源字符串长度小于n
,则用\0
填充剩余空间。
实现原理
- 遍历源字符串的前
n
个字符(或直到遇到\0
);
- 若源字符串长度不足
n
,继续用\0
填充目标字符串至n
长度。
1 2 3 4 5 6 7 8 9 10
| char *my_strncpy(char *dest, const char *src, size_t n) { size_t i; for (i = 0; i < n && src[i] != '\0'; i++) { // 复制前n个有效字符 dest[i] = src[i]; } for (; i < n; i++) { // 填充剩余空间为\0 dest[i] = '\0'; } return dest; }
|
关键点
- 若
n
大于源字符串长度,目标字符串末尾会被填充\0
(避免未终止);
- 若
n
小于源字符串长度,仅复制前n
个字符(目标字符串不会以\0
结尾!需额外处理)。
测试用例
1 2 3
| char p[30] = "122345"; char a[] = "128757"; printf("%s\n", my_strncpy(p, a, 4)); // 输出"1287"(复制前4个字符)
|
复现4:my_strcat
——拼接字符串
功能说明
my_strcat
将源字符串(src
)的内容追加到目标字符串(dest
)的末尾,并确保dest
以\0
结尾。
实现原理
- 找到
dest
的末尾(空终止符位置);
- 将
src
的字符逐个复制到dest
末尾,直到src
结束;
- 手动在
dest
末尾添加\0
。
1 2 3 4 5 6 7 8 9
| char *my_strcat(char *dest, const char *src) { char *tmp = dest; // 保存目标起始地址 while (*dest != '\0') { // 找到dest的末尾 dest++; } while ((*dest++ = *src++) != '\0'); // 复制src到dest末尾 *dest = '\0'; // 手动添加空终止符(防止src未终止导致dest越界) return tmp; // 返回目标起始地址 }
|
关键点
- 必须确保
dest
有足够空间容纳src
的内容(否则会导致缓冲区溢出);
- 若
dest
原本为空字符串(""
),则直接复制src
。
测试用例
1 2 3
| char p[30] = "122345"; char a[] = "128757"; printf("%s\n", my_strcat(p, a)); // 输出"122345128757"(p被扩展为两字符串拼接结果)
|
复现5:my_strncat
——安全拼接(限制长度)
功能说明
my_strncat
将源字符串的前n
个字符追加到目标字符串(dest
)的末尾,并确保dest
以\0
结尾。
实现原理
- 找到
dest
的末尾(空终止符位置);
- 复制源字符串的前
n
个字符到dest
末尾(若源字符串提前结束则停止);
- 手动在
dest
末尾添加\0
。
1 2 3 4 5 6 7 8 9 10 11
| char *my_strncat(char *dest, const char *src, size_t n) { char *tmp = dest; // 保存目标起始地址 while (*dest != '\0') { // 找到dest的末尾 dest++; } for (size_t i = 0; i < n && *src != '\0'; i++) { // 复制前n个字符(或源提前结束) *dest++ = *src++; } *dest = '\0'; // 手动添加空终止符 return tmp; // 返回目标起始地址 }
|
关键点
- 若
n
大于源字符串长度,仅复制源的全部内容并在末尾添加\0
;
- 若
n
小于源字符串长度,复制前n
个字符后强制终止dest
。
测试用例
1 2 3
| char p[30] = "122345"; char a[] = "128757"; printf("%s\n", my_strncat(p, a, 4)); // 输出"1223451287"(p末尾追加前4个字符)
|
复现6:my_strcmp
——比较字符串大小
功能说明
my_strcmp
逐个比较两个字符串的字符,返回它们的字典序关系(负数、0、正数分别表示str1 < str2
、相等
、str1 > str2
)。
实现原理
逐个比较str1
和str2
的字符,直到遇到不同的字符或其中一个字符串结束:
- 若字符值不同,返回它们的ASCII差值(
*(unsigned char *)str1 - *(unsigned char *)str2
);
- 若全部字符相同且同时结束,返回0。
1 2 3 4 5 6 7 8 9
| int my_strcmp(const char *str1, const char *str2) { while (*str1 && *str2 && *str1 == *str2) { // 字符相同且未结束则继续 str1++; str2++; } return *(unsigned char *)str1 - *(unsigned char *)str2; // 返回差值(处理符号问题) }
|
关键点
- 使用
unsigned char
强制转换避免符号扩展问题(如char
为有符号类型时,\xff
会被视为-1);
- 若两个字符串完全相同但长度不同(如
"abc"
和"abcd"
),会在较短字符串的\0
处停止比较,返回\0 - 'd'
(负数)。
测试用例
1 2 3
| char a[] = "128757"; char q[] = "12234"; printf("%d\n", my_strcmp(a, q)); // 输出正数("128757" > "12234")
|
主函数测试:验证所有函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| int main(void) { char p[30] = "122345"; // 可修改的数组 char q[] = "12234"; char a[] = "128757";
// 测试 strcmp printf("%d\n", my_strcmp(a, q)); // 输出正数("128757" > "12234")
// 测试 strlen printf("%zu\n", my_strlen(p)); // 输出6(字'1','2','2','3','4','5')
// 测试 strcpy printf("%s\n", my_strcpy(p, q)); // 输出"12234"(p被覆盖为q的内容)
// 测试 strcmp(比较修改后的p和q) printf("%d\n", my_strcmp(p, q)); // 输出0(两者内容相同)
// 测试 strncpy(复制前4个字符) printf("%s\n", my_strncpy(p, a, 4)); // 输出"1287"(p前4位被覆盖)
// 测试 strcat(拼接a到p末尾) printf("%s\n", my_strcat(p, a)); // 输出"1287128757"(p末尾追加a的内容)
// 测试 strncat(拼接前4个字符) printf("%s\n", my_strncat(p, a, 4)); // 输出"128712871287"(p末尾追加前4个字符)
// 测试 strcpy(再次覆盖p) printf("%s\n", my_strcpy(p, q)); // 输出"12234"(p被覆盖为q的内容)
return 0; }
|
总结:复现标准库函数的价值
通过手动实现C语言标准库的字符串函数,我们不仅掌握了底层内存操作的细节(如空终止符的作用、缓冲区溢出的风险),更深入理解了这些函数的设计逻辑和潜在陷阱。这对提升代码的安全性(如避免strcpy
的缓冲区溢出)、优化性能(如strncpy
的按需复制)以及调试复杂问题(如字符串越界)都有重要意义。
下次遇到字符串操作需求时,不妨尝试自己实现核心逻辑——这不仅能加深对C语言的理解,更能让你在工程实践中写出更健壮的代码!
完整源代码:复现C语言标准库字符串函数
以下是所有复现函数的完整实现,包含详细注释与测试用例,可直接复制编译运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
|
/* 复现C语言中的库函数 * 1.size_t my_strlen(const char *str); * 2.char *my_strcpy(char *dest, const char *src); * 3.char *my_strncpy(char *dest, const char *src, size_t n); * 4.char *my_strcat(char *dest, const char *src); * 5.char *my_strncat(char *dest, const char *src, size_t n); * 6.int my_strcmp(const char *str1, const char *str2); */
// 1. 实现 strlen 求字符的长度 size_t my_strlen(const char *p) { size_t count = 0; while (*p != '\0') { // 遍历直到空终止符 count++; p++; // 移动到下一个字符 } return count; }
// 2. 实现 strcpy 复制字符串的值 char *my_strcpy(char *dest, const char *src) { char *tmp = dest; // 保存目标起始地址 while (*src != '\0') { // 复制直到源字符串结束 *dest++ = *src++; // 逐个字符复制(后置++避免覆盖) } *dest = '\0'; // 手动添加空终止符 return tmp; // 返回目标起始地址 }
// 3. 更安全的复制(带长度限制) char *my_strncpy(char *dest, const char *src, size_t n) { size_t i; for (i = 0; i < n && src[i] != '\0'; i++) { // 复制前n个有效字符 dest[i] = src[i]; } for (; i < n; i++) { // 填充剩余空间为\0 dest[i] = '\0'; } return dest; }
// 4. 实现 strcat 拼接字符串 char *my_strcat(char *dest, const char *src) { char *tmp = dest; // 保存目标起始地址 while (*dest != '\0') { // 找到dest的末尾(空终止符位置) dest++; } while ((*dest++ = *src++) != '\0'); // 复制src到dest末尾 *dest = '\0'; // 手动添加空终止符(防止src未终止导致dest越界) return tmp; // 返回目标起始地址 }
// 5. 实现 strncat 安全拼接(限制长度) char *my_strncat(char *dest, const char *src, size_t n) { char *tmp = dest; // 保存目标起始地址 while (*dest != '\0') { // 找到dest的末尾 dest++; } for (size_t i = 0; i < n && *src != '\0'; i++) { // 复制前n个字符(或源提前结束) *dest++ = *src++; } *dest = '\0'; // 手动添加空终止符 return tmp; // 返回目标起始地址 }
// 6. 实现 strcmp 比较字符串大小 int my_strcmp(const char *str1, const char *str2) { while (*str1 && *str2 && *str1 == *str2) { // 字符相同且未结束则继续 str1++; str2++; } // 返回差值(转换为unsigned char避免符号扩展问题) return *(unsigned char *)str1 - *(unsigned char *)str2; }
// 主函数:测试所有复现函数 int main(void) { char p[30] = "122345"; // 可修改的数组(初始长度6) char q[] = "12234"; // 自动推导长度(5+1=6) char a[] = "128757"; // 自动推导长度(6+1=7)
// 测试 my_strcmp(比较a和q) printf("my_strcmp(a, q) = %d\n", my_strcmp(a, q)); // 输出正数("128757" > "12234")
// 测试 my_strlen(计算p的长度) printf("my_strlen(p) = %zu\n", my_strlen(p)); // 输出6(字符'1','2','2','3','4','5')
// 测试 my_strcpy(将q复制到p) printf("my_strcpy(p, q) = %s\n", my_strcpy(p, q)); // 输出"12234"(p被覆盖为q的内容)
// 测试 my_strcmp(比较修改后的p和q) printf("my_strcmp(p, q) = %d\n", my_strcmp(p, q)); // 输出0(两者内容相同)
// 测试 my_strncpy(将a的前4个字符复制到p) printf("my_strncpy(p, a, 4) = %s\n", my_strncpy(p, a, 4)); // 输出"1287"(p前4位被覆盖)
// 测试 my_strcat(将a拼接至p末尾) printf("my_strcat(p, a) = %s\n", my_strcat(p, a)); // 输出"1287128757"(p末尾追加a的内容)
// 测试 my_strncat(将a的前4个字符拼接至p末尾) printf("my_strncat(p, a, 4) = %s\n", my_strncat(p, a, 4)); // 输出"128712871287"(p末尾追加前4个字符)
// 测试 my_strcpy(再次将q复制到p) printf("my_strcpy(p, q) = %s\n", my_strcpy(p, q)); // 输出"12234"(p被覆盖为q的内容)
return 0; }
|