学习 C 语言时,我们经常会使用很多库函数,比如 strlenstrcpystrcmpmemcpy 等。刚开始学习时,我只知道它们“能用”,但并不清楚它们内部到底是怎么实现的。

后来我发现,模拟实现库函数是学习 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);

这句代码是为了防止传入空指针。如果 strNULL,继续访问 *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 分两步:

  1. 找到目标字符串末尾的 '\0'
  2. 从这个位置开始复制源字符串

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'

无论是 strlenstrcpystrcmp 还是 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 语言的理解也会更加深入。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐