从零开始理解 C 语言库函数模拟:一次很有收获的练习
学习 C 语言时,我们经常会使用很多库函数,比如 strlen、strcpy、strcmp、memcpy 等。刚开始学习时,我只知道它们“能用”,但并不清楚它们内部到底是怎么实现的。
后来我发现,模拟实现库函数是学习 C 语言非常好的练习方式。它不仅能帮助我们理解函数的底层逻辑,还能锻炼指针、数组、字符串、内存操作等基础知识。
这篇博客就从初学者的角度,记录我学习库函数模拟的一些过程、代码示例和个人感悟。
一、为什么要模拟实现库函数?
在 C 语言中,库函数已经被封装好了,我们可以直接调用。例如:
C
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello";
printf("%zu\n", strlen(arr));
return 0;
}
这段代码可以直接输出字符串长度。
但是对于初学者来说,如果只会调用函数,而不了解它们的实现过程,就容易停留在“会用”的层面。
模拟实现库函数有几个好处:
- 加深对字符串和数组的理解
- 提高指针使用能力
- 熟悉函数封装思想
- 理解标准库函数的设计逻辑
- 为后续学习数据结构和底层编程打基础
我觉得模拟库函数的过程,就像是把别人写好的工具拆开来看一看:它为什么能工作?边界情况怎么处理?参数为什么这样设计?
二、模拟实现 strlen 函数
strlen 函数用于求字符串长度,不包含字符串末尾的 '\0'。
标准库中的使用方式:
C
strlen("hello");
返回值是 5。
1. 基本思路
字符串在 C 语言中以 '\0' 作为结束标志,所以我们只需要从头开始遍历字符串,直到遇到 '\0' 为止。
2. 模拟代码
C
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "hello";
printf("%zu\n", my_strlen(arr));
return 0;
}
3. 代码分析
C
assert(str != NULL);
这句代码是为了防止传入空指针。如果 str 是 NULL,继续访问 *str 就会造成程序崩溃。
C
while (*str != '\0')
这表示只要当前字符不是字符串结束标志,就继续循环。
C
str++;
这里体现了指针的移动。每次让指针指向下一个字符。
通过模拟 strlen,我第一次比较清楚地感受到:字符串其实就是一段连续的字符空间,而指针可以一个一个地访问它们。
三、模拟实现 strcpy 函数
strcpy 用于字符串拷贝,把源字符串复制到目标空间中。
标准库使用方式:
C
strcpy(dest, src);
1. 基本思路
把源字符串中的字符一个一个复制到目标空间,直到把 '\0' 也复制过去。
2. 模拟代码
C
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char src[] = "hello world";
char dest[20] = { 0 };
my_strcpy(dest, src);
printf("%s\n", dest);
return 0;
}
3. 代码分析
这段代码中最关键的是:
C
while (*dest++ = *src++)
刚开始看到这句代码时,我觉得非常难理解。后来拆开来看,其实它相当于:
C
*dest = *src;
dest++;
src++;
循环条件是赋值表达式的结果。
当复制普通字符时,表达式结果不为 0,循环继续。
当复制到 '\0' 时,'\0' 的值是 0,循环结束。
这里要注意,strcpy 会把字符串末尾的 '\0' 也复制过去,否则目标字符串就不是一个完整的 C 字符串。
4. 为什么要保存 ret?
C
char* ret = dest;
因为后面 dest 会不断向后移动。如果不提前保存起始地址,最后就找不到目标字符串的开头了。
所以函数最后返回:
C
return ret;
这也让我理解了为什么标准库中的 strcpy 返回值是目标空间的起始地址。
四、模拟实现 strcmp 函数
strcmp 用于比较两个字符串。
它的返回值规则是:
- 第一个字符串大于第二个字符串,返回大于
0的数 - 第一个字符串等于第二个字符串,返回
0 - 第一个字符串小于第二个字符串,返回小于
0的数
1. 基本思路
从两个字符串的第一个字符开始比较:
- 如果字符相等,就继续比较下一个字符
- 如果字符不相等,就返回两个字符 ASCII 值的差
- 如果两个字符串同时遇到
'\0',说明字符串相等
2. 模拟代码
C
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char arr1[] = "abc";
char arr2[] = "abd";
int ret = my_strcmp(arr1, arr2);
printf("%d\n", ret);
return 0;
}
3. 代码分析
比如比较 "abc" 和 "abd":
'a' == 'a',继续'b' == 'b',继续'c' != 'd',返回'c' - 'd'
因为 'c' 的 ASCII 值比 'd' 小,所以返回负数。
这个练习让我明白,字符串比较并不是比较字符串长度,也不是整体一次性比较,而是从左到右逐字符比较。
五、模拟实现 strcat 函数
strcat 用于字符串追加,把源字符串拼接到目标字符串的末尾。
1. 基本思路
实现 strcat 分两步:
- 找到目标字符串末尾的
'\0' - 从这个位置开始复制源字符串
2. 模拟代码
C
#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (*dest != '\0')
{
dest++;
}
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[30] = "hello ";
char arr2[] = "world";
my_strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
3. 注意事项
使用 strcat 时,目标空间必须足够大。
例如:
C
char arr1[10] = "hello ";
char arr2[] = "world";
这就可能出现问题,因为 "hello world" 加上 '\0' 一共需要 12 个字符空间,而 arr1 只有 10 个。
所以模拟实现库函数时,我也逐渐意识到:C 语言很多错误不是语法错误,而是内存空间使用不当导致的错误。
六、模拟实现 memcpy 函数
前面几个函数主要处理字符串,而 memcpy 用于内存拷贝,可以拷贝任意类型的数据。
1. 基本思路
memcpy 的参数通常是:
C
void* memcpy(void* dest, const void* src, size_t count);
它的作用是从 src 指向的位置拷贝 count 个字节到 dest。
因为它按字节拷贝,所以通常会把指针转换成 char* 类型。
2. 模拟代码
C
#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (count--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 0 };
my_memcpy(arr2, arr1, sizeof(arr1));
for (int i = 0; i < 5; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
3. 代码分析
C
*(char*)dest = *(char*)src;
这里把 void* 转换成 char*,是因为 void* 不能直接解引用,也不能直接进行指针加减。
而 char 类型大小是 1 字节,所以用 char* 可以实现逐字节拷贝。
通过这个例子,我对 void* 有了更清楚的认识:它可以接收任意类型的地址,但真正操作数据时,还是需要转换成具体类型。
七、学习过程中的几点感悟
1. 指针没有想象中可怕
刚开始学指针时,我觉得它很抽象。尤其是看到 *p++、*dest++ = *src++ 这样的写法时,很容易懵。
但在模拟库函数的过程中,我发现指针其实就是“地址变量”。只要搞清楚:
- 指针指向哪里
- 解引用访问什么
- 指针移动后指向哪里
很多问题就会变得清晰。
2. 字符串的核心是 '\0'
C 语言字符串最重要的标志就是 '\0'。
无论是 strlen、strcpy、strcmp 还是 strcat,它们都离不开 '\0'。
如果忘记处理 '\0',程序就可能继续访问未知内存,造成错误。
所以我觉得,学习 C 语言字符串时,一定要牢记:字符串不是由长度决定结束,而是由 '\0' 决定结束。
3. 写函数时要考虑非法参数
以前写代码时,我经常只考虑正常情况,比如传入的指针一定有效。
但模拟库函数时,我开始注意到:
C
assert(str != NULL);
这种写法可以帮助我们在调试阶段发现问题。
虽然 assert 不是万能的,但它提醒我:写函数不能只考虑“理想输入”,也要考虑错误情况。
4. C 语言很注重细节
模拟库函数让我感受到,C 语言的很多细节都非常重要:
- 数组空间是否足够
- 指针是否为空
- 是否拷贝了
'\0' - 是否发生越界访问
- 源空间和目标空间是否重叠
这些问题在高级语言中可能被封装起来了,但在 C 语言中需要程序员自己负责。
这也是 C 语言难学但值得学的地方。
八、总结
通过模拟实现常见库函数,我不仅学会了这些函数的使用方法,更重要的是理解了它们背后的实现思想。
这次练习让我收获最大的是:
strlen帮助我理解字符串结束标志strcpy帮助我理解字符串拷贝和返回值设计strcmp帮助我理解逐字符比较strcat帮助我理解字符串拼接和空间问题memcpy帮助我理解内存按字节操作
对于 C 语言初学者来说,库函数模拟是非常值得练习的内容。它不像复杂项目那样难以上手,但又能覆盖很多核心知识点。
我觉得学习 C 语言不能只停留在“会调用函数”,更要尝试去理解“函数为什么这样实现”。当我们能自己写出这些库函数时,对 C 语言的理解也会更加深入。
更多推荐


所有评论(0)