文章目录
前言
在C语言中提供了很多方便的库函数,比如strlen strcpy strcat 等等一系列的库函数。作为初学者不仅要会正确的使用这些库函数,还可以试着去模拟实现一些库函数。可以加深印象,更好的掌握C语言知识。
一、库函数是什么?
百度百科给出的定义是:库函数(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。
在早期的时候,不同程序员经常要大量使用一些相同功能的方法函数。比如进行数据的输出或输出等,为了避免重复繁琐实现相关功能函数,于是不同编译器厂商封装实现了一些经常使用的函数。这就是早期库函数,尽管在实现方法上会有细微的差异,但是使用的方法保持高度相同。库函数的使用
要包含对应的头文件。
对于新手初学者来说,学习库函数可以试着去查阅官方文档,这样不仅会了解学习了库函数,同时还会印象更加深刻。推荐网址C语言库函数网址查阅
二、试着自己实现一些简单的库函数
1.实现strlen
strlen函数求字符串长度,实现思路,因为字符串是以\0结尾的,可以将字符串每个字符挨个遍历同时记录次数,直到找见\0结束遍历。同时函数返回记录次数
#include<stdio.h>
int my_strlen(char* str)//求字符串长度函数
{
int count = 0;
while (*str++)
{
count++;
}
return count;
}
int main()
{
char str[] = { "hello" };
int ret = my_strlen(str);
printf("%d", ret);
return 0;
}
因为\0的对应的ascii值就是0,所以将传进函数的字符串地址不断前移同时解引用,一旦遇见\0解引用后就是0直接跳出循环。
如果想提升一点效率的话,可以不用计数的方法来统计个数求长度,可以先将字符串的起始地址保存在一个指针变量中,同时找到字符串\0的地址,用该地址减去字符串起始地址就是字符串的长度。
因为(*str++)是后置++,所以while循环结束时是指向\0后面的一个位置,所以采用while( *str){str++;}
使得循环结束时str刚好指向\0;同时这里加上const修饰是希望被指向的内容不被改变,使得函数更加安全,如果还想做优化可以利用assert函数来断言一下,让my_strlen函数不能传进去空指针。
断言要引入头文件#include<assert.h>头文件。它使得函数指针不能为空指针,否则就警告报错。它有点类似于if语句,但是它本质上是个宏。想要详细的了解它的具体情况,请查阅官方文档,这里就不做过多的赘述。
2.实现strcpy和strncpy
字符串复制函数,作用是将一个字符串的内容复制到另一个字符串中。实现思路还是和上述类似,利用指针找到想复制的字符串地址和将要被改变的字符串地址,然后挨个交换复制,\0也要被复制交换。这样就使得最后输出结果的时候刚好是复制的内容。
代码如下(示例):
char* my_strcpy( char* str1, const char* str2)//str1是被复制的字符指针,str2是复制内容
{
assert(*str1 && *str2);//断言 str1和str2不能为空
char* tem = str1;
while ( *str1++=*str2++)
{
}
return tem;
}
利用const和assert使得函数更加安全一点,因为在循环的过程中str1指向的位置会发生变化,所以需要提前保存好str1的位置,因为最后需要返回原来字符串的起始位置。但是使用这样字符串需要考虑,字符数组空间是不是足够的,当要复制的内容的大小,超出原来字符数组的大小,这就会有问题了。
于是引入另一个库函数strncpy,相比strcpy多了一个整型参数,用来限制字符串的复制大小。具体详情,请移步官网查寻文档。这里主要讨论strncpy实现,关于实现思路大致如下
其实在先前strcpy的基础上进行改造即可,传入一个整型,限制字符复制个数。整个实现的前提还是挨个找对应字符的位置,所以只需要n写入while循环即可,因为可能输入的n是小于复制的字符串长度的。所以可能在遇到\0之前就已经跳出了循环,这个时候需要在对应的字符指针位置加上一个\0,来保证字符串正常结束。
char* my_strncpy(char* str1, const char* str2, int n)
{
char* tem = str1;
while ((n--) && (*str1++ = *str2++))
{
}
*str1 = '\0';
return tem;
}
将字符指针的遍历和并n用&&来连接,当n为0时如果还没遇到\0依旧可以跳出循环,当n大于复制内容的字符串长度依旧可以跳出循环,比如str1=“abcde”,str2=“qwe” n=2时,在str1在2次循环后,就自动跳出循环了,如果字符串结尾没有\0,所以打印结果就会是qwcde,因为\0没有加上去,就会把目标地址的字符串原有的后续字符打印出来,因此需要加上一个\0.
3.实现strcmp和strncmp
1.strcmp函数比较字符串大小,比较原理是比较对应字符的ASCII码值,从起始位置挨个比较,如果某个位置字符ASCII值大于另一个就返回大于 0的整数数,如果是小于就是返回小于0的整数,如果是每个字符相等就返回0. 比如:strcmp(abc , abdefgrt) 比较,就会返回大于0的整数,因为abc的第三个字符c对应的ASCII值大于abdefgrt的第三个字符d的ASCII值。如果abdefgrt第一个字符改成b,那么在第一次首字符比较的时候就会返回大于0的整数。
实现思路,还是挨个遍历,经行比较字符,如果中途有了比较结果就返回比较结果,如果没有结果就接着比较直到遇见\0就停止下来。
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);//断言 指针不能为空
while (*str1==*str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}
如果字符内容相同就依此往下接着遍历比较,如果比较到\0还是字符相同,直接返回0.
上述介绍了strncpy函数,对于strcmp函数来说也是有strncpy来限制比较个数。
实现思路如下
引入一个整型参数限制字符的遍历,在原有的strcmp基础上可以将整型参数放在循环条件中限制循环次数。
int my_strncmp(const char* str1, const char* str2,int n)
{
assert(str1 && str2);
while ((*str1 == *str2)&&n--)
{
str1++;
str2++;
}
if (*str1 == '\0'||*str2=='\0')
{
return 0;
}
else
{
return *str1 - *str2;
}
}
这里做了一点改进,因为两个字符串长短不一,如果当字符串为 hello 和hell n=4 时这个时候其实正确的返回值应该是返回0,但是先前定义实现的strcmp函数,判断语句是定义在循环体内部的,所以当跳出循环的时候 str1是没有指向\0的,所以就是直接返回两个指针解引用相减了。这样就不会返回0,所以因此需要将判断定义在循环体外部。因为字符串的长度不一,无法确定字符串的到底是谁先到\0,所以就用或者来连接str1 str2 是否为\0。
4.实现strcat和strncat
C 库函数 char *strcat(char *dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
dest指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
src – 指向要追加的字符串,该字符串不会覆盖目标字符串。
该函数返回一个指向最终的目标字符串 dest 的指针。
大致实现思路,先找到目标字符串\0的位置,在从\0的位置开始覆盖要追加的字符串\0也要追加进去。最后返回目标字符串的起始位置。
char* my_strcat(char* dest, const char* src)
{
char* tem = dest;
while (*dest)//找到\0的位置
{
dest++;
}
while (*dest++=*src++)//追加字符串内容
{
}
*dest = '\0';//最后加上\0
return tem;
}
需要注意的是strcat函数不能自己追加自己,因为第一步就是找到字符串\0,接着覆盖它,此时原来字符串中已经没有\0了。while将陷入死循环,一直追加。
实现了strcat还有strncat,关于strncat还是和上述类似是用来限制追加的字符个数。基于strcat来实现strncat,大体思路如下
引入整型参数来限制循环次数,进行字符串的追加。
char* my_strncat(char* dest, const char* src,int n)
{
char* tem = dest;
while (*dest)
{
dest++;
}
while ((n--)&&(*dest++ = *src++))
{
;
}
*(dest)= '\0';
return tem;
}
最后的\0不加的也行,因为n=0时直接跳出循环并不影响结果,但是如果这样做这个追加后的字符串结尾处是无\0的,为了避免造成不必要的麻烦,还是最后在结尾处加上\0。
5.模拟strstr
先在官网文档上去查阅一下这个函数
其实这个函数的作用就是在字符串str1中查找字符串str2是不是str1的子串,如果是就返回str1中对应字串的首地址,如果不是就返回空指针。
既然要实现这个这个函数,我们先梳理一下大致思路
字符串str1从第一个位置开始和字符串比较,如果相同,str1和str1同时往后移动,如果在一次比较之后两者不同的话,就直要判断str2此时是否为\0,如果是\0,说明已经比较到结尾了,str2就是str1的子串,直接返回str1的起始比较地址,如果不等于\0,说明第一次匹配失败。需要从str1的第二个位置开始再挨个和str2比较,然后重复上述判断。直到从str1最后一个字符开始匹配都没有成功的话那就说明,str2不是str1的子串,直接返回null。从这个过程中我们看到指向str1的位置是要不断更新的,指向的str1位置也是要更新的,所以需要再引入两个变量,将传递给形参的str1 str2的首地址,再赋值给两个新变量,让这两个新变量不断匹配比较,配备失败后,将指向str1首地址的形参往前移动再重新赋值给对应的变量,另一个变量只需要重置一下,接着比较匹配即可,一旦成功直接返回指向str1的形参即可,这思路中有不断的比较更新,比较失败后还得差值,所以应该是双重循环。基于这个思路,代码大致就有了
代码如下
#include<assert.h>
char* my_strstr(const char* str1, const char* str2)
{
assert(str1);//指针不能为空,断言
assert(str1);
while (*str1)
{
char* s1 = str1;
char* s2 = str2;
while (*s1 == *s2)
{
s1++;
s2++;
}
if (*s2 =='\0')
{
return str1;
}
else
str1++;
}
return NULL;
}
我将s1 和 s2都定义在循环体内部,来实现匹配失败后的实时更新,同时str1也在更新。因为是查找,所以实参字符串内容并没有改变,如果不想动形参指针str1可以在循环体外定义一个新的指针变量,将str1值赋给这个变量,将循环体内所有的str1改成这个变量即可。其实这个不相等跳出循环有点像strcmp函数,不同的是,需要不断地更新指针匹配比较。
6.模拟memcpy
不了解这个函数还是可以去官网查阅文档,这里简单介绍一下这个函数。
void * memcpy ( void * destination, const void * source, size_t num );
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 ‘\0’ 的时候并不会停下来。
这个函数有点像strcpy但是不同的是这个函数什么类型的数据都可以拷贝。而且有第三个参数限制拷贝的内容。
可以看到这个函数的返回值类型和参数类型都给出了,这点是也是可以在官网文档中查到的。
大致思路如下。因为可以对任意类型数据进行拷贝,所以用void* 指针来接收地址和返回地址。
因为用void*来接收地址,肯定是要强制类型转换对对地址操作,因为是按字节来操作的,所以强制类型转换成char * 即可,在进行拷贝复制数据即可。
代码示例如下
#include<assert.h>
void* my_memcpy(void* dest, void* src,int size_num)
{
assert(dest);//防止空指针
assert(src);
void* tem = dest;
while (size_num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return tem;
}
这个函数虽然可以拷贝任意数据,但是也有一个问题。就像strcat追加字符串函数一样,当它追加的时候就会把原有的\0的给覆盖掉,从而陷入死循环。虽然memcpy不会用纠结\0和死循环的问题,但是如果拷贝的源头和目标地址有重和的话,那么势必会会造成数据覆盖,达不到预期的结果。C语言库函数有一个memmove,这个函数和memcpy功能一样都是拷贝,但是它遇到上述情况也能正常拷贝,那接着就实现一下这个memmove函数吧
7.模拟memmove
上面提到了覆盖的情况,我们先举个简单的例子分析一一下吧
基于上述思路,写出如下代码
#include<stdio.h>
#include<assert.h>
void* my_memmove(void* dest, void* src,int size_num)
{
assert(dest);//防止空指针
assert(src);
void* tem = dest;
if (dest > src)
{
while (size_num--)
{
*((char*)dest + size_num) = *((char*)src + size_num);
}
}
else
{
while (size_num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
return tem;
}
倒序拷贝的时候可以利用第三个形参,当作偏移量。来移动指针指向的位置进行拷贝。
memcpy 和memmove拷贝的过程中实际上是改变了字符串的,如果不想改变原字符串的内容,可以将原字符串内容赋值给一个字符数组即可。
总结
1.适当利用const来修饰函数参数,来增加函数的安全性。为了避免传入空指针,可以用断言来提前进行限制。
2.while(* str1++ =*str2++)可以先把 ++ 拿走去试着理解 这样就变成了 *str1= *str2,在后置++进行遍历。同时在使用不同的++时要注意指针最终指向的位置。避免产生错误操作。
3.以上的库函数都是封装好的,可以直接调用,但是如果自己试着去实现就会对其有更加深刻的认识。
4.关于实现时的函数参数 返回值设置,可以看看官方文档中对其的定义说明,然后仿照即可。同时注意函数名不能原来的库函数一致。切记不能随意改变参数和返回值类型以及改变参数的个数,因为我们是模拟库函数,不是创造新函数。
5.以上内容如有错误,欢迎指出。谢谢指教!