C标准库字符串函数复现

引言:为什么需要复现标准库字符串函数?

在C语言开发中,``提供的字符串函数(如strlenstrcpy)是最常用的工具之一。但这些函数的底层实现逻辑你真的清楚吗?

  • 学习价值:复现标准库函数能帮你深入理解字符串操作的底层逻辑(如空终止符的作用、内存复制的安全性);
  • 工程实践:在嵌入式开发、操作系统内核等场景中,可能因内存限制或安全要求无法直接使用标准库,需自定义实现;
  • 避坑指南:了解标准库函数的潜在问题(如strcpy的缓冲区溢出风险),能帮助你在实际开发中写出更安全的代码。

今天,我们就通过复现6个核心字符串函数(strlenstrcpystrncpystrcatstrncatstrcmp),彻底掌握字符串操作的底层原理!


复现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结尾。

实现原理

  1. 保存dest的起始地址(用于返回);
  2. 逐个复制src的字符到dest,直到遇到src\0
  3. 手动在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填充剩余空间。

实现原理

  1. 遍历源字符串的前n个字符(或直到遇到\0);
  2. 若源字符串长度不足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结尾。

实现原理

  1. 找到dest的末尾(空终止符位置);
  2. src的字符逐个复制到dest末尾,直到src结束;
  3. 手动在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结尾。

实现原理

  1. 找到dest的末尾(空终止符位置);
  2. 复制源字符串的前n个字符到dest末尾(若源字符串提前结束则停止);
  3. 手动在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)。

实现原理

逐个比较str1str2的字符,直到遇到不同的字符或其中一个字符串结束:

  • 若字符值不同,返回它们的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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stddef.h> // 用于size_t类型

/*
复现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;
}