C语言进阶之字符串函数,内存函数
1.前言
我们在日常编程中,会用到像memset,strlen,strcmp,strcat,memerror等等库函数,我们将str开头的称之为字符串函数,mem开头的称为内存函数,那么接下来我就向大家介绍一下这两大类的函数,顺便跟大家一起模拟实现这些库函数
2.字符串函数
使用字符串函数的重要一点就是要引用其相应的头文件**<string.h>**
在介绍后面的字符串函数之前,我先为大家介绍一下strlen这个库函数
我们大家都知道strlen这个库函数可以帮助我们数字符串的长度,就像下面这样:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[]="abcdef";
printf("%d\n",strlen(arr));
return 0;
}
在这里我们就能看到结果是:6,这个库函数的原理就是当指针指向的地址解引用为'\0'时,数数结束,这个我们可以验证一下
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef\0abcd";
printf("%d\n",strlen(arr));
return 0;
}
这里就很明显能够看到当指针指向的地址解引用后的元素'\0'时,数数就会结束
根据这一点,我们可以开始对strlen进行一个模拟操作:
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* arr)
{
assert(arr);//断言,防止arr为NULL
int count=0;//计数
while(*arr)//当*arr为'\0'时结束
{
arr++;
count++;
}
return count;
}
int main()
{
char arr[]="abcdef";
printf("%d\n",my_strlen(arr));
return 0;
}
通过结果,我们就可以知道我们的strlen的模拟已经实现了
而我们使用的日常中的字符串函数分为两类:
不受长度限制的字符串函数:strcmp,strcat,strcpy
受长度限制的字符串函数:strncmp,strncat,strncpy
(1)不受长度限制的字符串函数
不受长度限制的字符串函数很好理解,就是函数一路运行到底,只会根据字符串的结束而结束函数,日常中用到的主要是:
strcpy,strcat,strcmp
i.strcpy
这个库函数名字很好理解,就是字符串(string)的覆盖(copy)
对于其用法,我们可以通过cplusplus.com知道
在这里我们就能够大体知道其用法
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20]={ 0 };
char arr2[]="hello world";
strcpy(arr1,arr2);
printf("%s\n",arr1);
return 0;
}
输出结果如下:
既然我们知道了其用法,那我们就得知道其原理:其工作原理大概就是通过两个不同的指针来指向目标数组和源数组,然后再对目标数组中的几个目标元素进行一个覆盖的操作,当这个源数组达到了'\0'就意味着结束了,这个我们可以进行验证一下:
使用前:
使用后:
在这里我们就能看到了这个覆盖操作,而且会将源数组的**'\0'**也带过去,当然我们也可以将目标数组改为当前数组的首元素后面的元素作为我们的目标数组,例如:
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "XXXXXXXXXXXXXXXX";
char arr2[] = "hello world";
strcpy(arr1+3, arr2);
printf("%s\n", arr1);
return 0;
}
输出的结果如下:
通过上面的说明,我们也可以对这个函数进行模拟操作:
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest,const char* src)
{
//const char* src是为了防止我们对源数组进行更改
assert(dest && src);//防止dest和src为空
char* ret = dest;//因为dest需要移动,所以在这里先对dest的首元素地址进行记录
while(*dest++ = *src++);//进行覆盖操作和保证当*src为'\0'时停止覆盖的操作
return ret;
}
int main()
{
char arr1[30]="abdesf";
char arr2[]="sdsdfgsdf";
my_strcpy(arr1,arr2);
printf("%s\n",arr1);
return 0;
}
得到的结果:
这样就能够测试出我们的模拟strcpy成功了
但此时或许有人会问:不需要对arr1的内存进行判断能否放得下arr2中的内容吗?
我的回答是:不需要,因为模拟函数就是对原本的函数进行模仿即可,而不是改善,而且这个函数也确实达到了我们想要的功能,至于我们传参的内存问题,这是我们自己的问题,我们需要对这种东西进行判断
ii.strcmp
这个库函数也很好理解,就是字符串(string)的比较(compare),那我们就先开始了解其用法吧
通过cplusplus.com的网站,我们知道了
下面我就对这用法进行一个解释:
在此之外,我先的说明一点,当str1<str2时,输出的是一个<0的数字,相等时,输出的是0,str1>str2时,输出的是一个>0的数字
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "abcd";
char str2[] = "bbcd";
int ret = strcmp(str1,str2);
printf("%d\n",ret);
if(ret>0)
{
printf(">\n");
}
else if(ret==0)
{
printf("=\n");
}
else
{
printf("<\n");
}
return 0;
}
结果是:
有人或许这时候会对这库函数的比较原理感到好奇,所以接下来,就是对其工作原理进行讲解:
其比较是通过两个指针指向两个不同的数组,这两个指针同时移动,然后再对这两个指针指向的地址所对应的元素的ASCII进行比较,若相同就比下一位,直到出现不同的两位,然后返回相应的值,或是直到两个字符中某一位遇到了'\0',此时也会结束,说明此时两个字符串相同
直到其工作原理后,接下来就是对这个函数进行模拟的操作:
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* str1,const char* str2)
{
//这里只是对其进行比较的操作,用const可以防止不小心对这两个字符串进行改变
assert(str1 && str2);//防止str1和str2为NULL
while(*str1 == *str2)
{
//不将++放到判断中,是因为即使条件不成立,此时也会进行一次++的操作
//假如此时*str1为'\0'了,但是还是进行了一次++操作,此时就会发生越界访问
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char str1[] = "acdes";
char str2[] = "acdef";
int ret = my_strcmp(str1 , str2);
if(ret < 0)
printf("<\n");
else if(ret == 0)
printf("=\n");
else
printf(">\n");
return 0;
}
结果:
这里也检测出我们的模拟strcmp成功了
iii.strcat
strcat有可能之前都没怎么见过,这个库函数的功能其实就是在一个字符串后再加一个字符串,用法在cplusplus.com可以查到
下面就对这个库函数的操作进行解释一下:
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char dest[30]="hello ";
char src[]="world";
strcat(dest,src);
printf("%s\n",dest);
return 0;
}
代码运行后的结果:
那么接下来我就为大家讲解该库函数的工作原理:
先将一个指针指向目标字符串,移动该指针,等到这个指针指向的是'\0'时,再通过另一个新指针将源字符串的内容一个一个的覆盖到目标字符串上
这个我们也可以验证一下:
使用前:
使用后:
这里我们就大概能对这个工作原理有个基本理解了
接下来就是我们的模拟strcat了
#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest,const char* src)
{
//src作用只是提供字符串,所以其不需要发生改变,所以加上const
assert(dest && src);//这里的dest和src不能是空指针
char* ret=dest;
while(*dest)
{
dest++;
}
while(*dest++ = *src++);
return ret;
}
int main()
{
char dest[30]="hello ";
char src[]="world";
my_strcat(dest,src);
printf("%s\n",dest);
return 0;
}
运行结果:
从运行结果,我们就可以知道我们的模拟的目的达成了
(2)受长度限制的字符串函数
根据上面的不受长度限制的字符串函数,我们也能推出受长度限制的字符串函数大概就是有一个长度限制的要求,达到这个长度要就会停止函数后面的任务,那事实呢?
事实就是这样
那其分类跟受长度限制的差不多
strncpy,strncmp,strncat
i.strncpy
其用法跟上面的strcpy差不多,就是多了一个参数,用来确定需要覆盖的字符数量,用cplusplus.com可以知道:
下面就对这个用法解释一下:
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char dest[30]="abcdefg";
char src[]="XXXX";
strncpy(dest,src,2);
printf("%s\n",dest);
return 0;
}
输出结果:
其工作原理与上面的strcpy相似,就不再提了,就是多了一个用于拷贝源字符串的限制
那下面就来到模拟这个strncpy了
#include <stdio.h>
#include <assert.h>
char* my_strncpy(char* dest, const char* src, size_t num)
{
//src只是提供字符串,不需要改变,所以加上一个const
assert(dest && src);//防止dest和src为NULL
char* ret = dest;
while(*src&&num--)
{
//*src是防止我们的num写的很大,但是我们的src很小,导致最后越界访问了
//num--是为了控制拷贝次数,这样就能达到控制拷贝src的字符数的最多量
*dest++ = *src++;
}
return ret;
}
int main()
{
char dest[30]="abcdef";
char src[]="XXXX";
my_strncpy(dest,src);
printf("%s\n", dest, 2);
return 0;
}
运行结果:
这里就能够证明我们的strncpy的功能模拟成功了
ii.strncmp
用法跟上面的差不多,也是多了一个用来限制比较的字符数量,通过cplusplus.com,我们可以知道:
下面就对这个用法进行简单的解释:
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char str1[]="abcde";
char str2[]="abcee";
int ret=strncmp(str1, str2, 3);
if(ret < 0)
printf("<\n");
else if(ret==0)
printf("=\n");
else
printf(">\n");
return 0;
}
运行结果:
这里我们就能很好地看出strncmp和strcmp之间的区别了,假如是strcmp的话,这里就是**<**了
工作原理也是跟strcmp差不多,这里就不再多讲了
接下来就到了模拟strncmp
#include <stdio.h>
#include <assert.h>
int my_strncmp(const char* str1,const char* str2,size_t num)
{
assert(str1 && str2);//防止str1和str2是NULL
num--;//得先减一次,不然最后返回的时候减的目标字符位置的后一位
//从而导致函数的运行结果错误
while(*str1 && *str2 && num-- && *str1 == *str2)
{
//*str1&&*str2是防止这两个字符串先遇到了'\0',从而导致了越界访问
//num--是为了达到比较字符串中字符数量
//只有当*str1==*str2时,才能继续往下走
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char str1[]="abcde";
char str2[]="abcee";
int ret = my_strncmp(str1, str2, 3);
if(ret < 0)
printf("<\n");
else if(ret==0)
printf("=\n");
else
printf(">\n");
return 0;
}
运行结果:
当num为4时,运行结果:
通过这两个进行对比,同时也验证了我们的模拟strncmp成功了
iii.strncat
其用法与strcat差不多,当然我们也可以通过cplusplus.com进行一次搜索:
下面为大家解释一下:
例子:
#include <stdio.h>
#include <string.h>
int main()
{
char dest[30]="abcd";
char src[]="XXXXX";
strncat(dest,src,3);
printf("%s\n",dest);
return 0;
}
运行结果:
其工作原理与strcat差不多,这里就不多赘述,但是其还是遇到'\0'就会开始添加源字符串
使用前:
使用后:
接下来就是模拟strncat了
#include <stdio.h>
#include <assert.h>
char* my_strncat(char* dest, const char* src, int num)
{
//const char* src是因为src只要提供字符串即可,不需要改变
assert(dest && src);//保证dest和src不为NULL
char* ret = dest;
while(*dest)
{
//使dest来到'\0'的地址
dest++;
}
while(*src && num--)
{
//*src是防止num过大,导致最后越界访问
//num--是为了达到源字符串最多能作为被添加的字符数量
*dest++ = *src++;
}
return ret;
}
int main()
{
char dest[30]="abcd\0ef";
char src[]="XXXXXX";
my_strncat(dest,src,3);
return 0;
}
使用前:
使用后:
这样就证明了我们的模拟strncat成功了
3.内存函数
内存函数可以说是字符串函数的升级版,因为字符串能做的事情,内存函数也能做到,当然它也包括在了**<string.h>**的这个头文件中,所以我们使用它的库函数时,先要引用这个头文件,我们正常用到的大概有这几个:
memcpy,memmove,memset,memcmp,malloc
当然malloc虽然是内存函数,但是由于其功能是分配空间,所以我将其放于动态内存管理中再讲,因为那里才是它的主场
在介绍内存函数之前,我们需要知道void*的数据类型,可以叫其为泛型数据类型,它可以用来接受任何的数据类型,但其本身没法直接使用,要使用的话,要进行强制类型转化
(1)memcpy
memcpy的用法和工作原理和上面讲到的strncpy都差不多,只不过其可以接受所有类型的数据
下面就简单地解释一下:
下面就给大家看个例子:
#include <stdio.h>
#include <string.h>
int main()
{
int dest[30]={ 1,2,3,4,5 };
int src[]={ 6,7,8 };
memcpy(dest,src,8);
for(int i=0;i<5;i++)
{
printf("%d ",dest[i]);
}
printf("\n");
return 0;
}
运行结果:
关于其运行的原理,因为跟strncpy的相似,所以这里就不再多描述,跟strncpy相比就是多了能接收所有类型的数组
那下面就跟大家一起模拟memcpy
#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest,const void* src, size_t num)
{
assert(dest && src);//防止dest和src为NULL
void* ret = dest;
while(num--)
{
//这里用一个字节一个字节的修改,这样就可以适合所有的情况
*(char*)dest = *(char*)src;//
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int dest[30]={ 1, 2, 3, 4, 5 };
int src[]={ 5, 6, 7};
my_memcpy(dest, src, 8);
for(int i=0 ; i<5; i++)
{
printf("%d ",dest[i]);
}
printf("\n");
return 0;
}
运行结果:
这样也就能证明我们的模拟memcpy成功了
这时有人就会想,那我能够自己覆盖自己吗?
那我们就来测试一下自己覆盖自己的情况:
#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest,const void* src, size_t num)
{
//由于src不需要发生改变,所以用const对其进行修饰
assert(dest && src);//防止dest和src为NULL
void* ret = dest;
while(num--)
{
//这里用一个字节一个字节的修改,这样就可以适合所有的情况
*(char*)dest = *(char*)src;//
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int dest[30]={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
my_memcpy(dest+2, dest, 20 );
for(int i=0 ; i<10; i++)
{
printf("%d ",dest[i]);//预期结果:1 2 1 2 3 4 5 8 9 10
}
printf("\n");
return 0;
}
此时的运行结果如下:
那有人会问那库函数本身能不能做到呢?
那我们就来试一下:
#include <stdio.h>
#include <string.h>
int main()
{
int dest[30]={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
memcpy(dest+2, dest, 20 );
for(int i=0 ; i<10; i++)
{
printf("%d ",dest[i]);//预期结果:1 2 1 2 3 4 5 8 9 10
}
printf("\n");
return 0;
}
运行结果:
运行结果与我们的预期相同
这时候有人就会开始发出疑问:那博主你的模拟函数应该错了吧!
但我想说的是:不对
因为这个函数本身的任务就只是将源数组覆盖给目标数组,而不是自己覆盖自己的功能,所以我们的模拟的效果达到了,只不过没有做到像库函数中做的这么完美,而且这个库函数后期也经过了改良,不然下面的memmove为何能存在呢?
(2)memmove
我们就先介绍其用法:
下面我就对这张图中的内容进行简单地解释一下:
下面就给大家展示两个例子:
正常的使用:
#include <stdio.h>
#include <string.h>
int main()
{
int dest[30]={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int src[]={ 11, 12, 13 };
memmove(dest, src, 8);
for(int i=0; i<10; i++)
{
printf("%d ",dest[i]);
}
printf("\n");
return 0;
}
运行结果:
自己覆盖自己:
#include <stdio.h>
#include <string.h>
int main()
{
int dest[30]={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
memmove(dest+2, dest, 20);
for(int i=0; i<10; i++)
{
printf("%d ",dest[i]);//预期结果:1 2 1 2 3 4 5 8 9 10
}
printf("\n");
return 0;
}
运行结果:
这里就达到了我们的预期结果,所以从某种意义上,可以将memmove理解为memcpy的双胞胎兄弟
那接下来就直接来进行我们的模拟操作吧,当然我的模拟不会使用临时拷贝,然后借助这个临时拷贝来完成覆盖的操作,首先,我得明确一点就是这种模拟方式当然是可行的,但是由于又要再内存中重新拷贝一份,这会导致我们的内存使用增大,从而导致我们的性能下降,所以我的模拟方式是直接对数组本身进行操作
#include <stdio.h>
#include <string.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);//防止dest和src为NULL
void* ret = dest;
if(dest < src)
{
while(num--)
{
//从前往后开始覆盖
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while(num--)
{
//从后往前开始覆盖
*((char*)dest+num) = *((char*)src+num);
}
}
return ret;
}
int main()
{
int dest[30]={ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int src[]={ 11, 12, 13 };
my_memmove(dest+2, src, 8);
for(int i=0; i<10; i++)
{
printf("%d ",dest[i]);
}
printf("\n");
return 0;
}
正常用法运行结果:
自己覆盖自己的dest在src后面:
my_memmove(dest+2, dest, 20);//预期结果:1 2 1 2 3 4 5 8 9 10
运行结果:
自己覆盖自己的dest在src前面:
my_memmove(dest, dest+2, 20);//预期结果:3 4 5 6 7 6 7 8 9 10
运行结果:
那下面我就为大家讲解一下这里模拟memmove的原理:
dest>src的情况:
dest<src的情况:
至于dest和src没有重叠部分以及两者完全重叠的时候两种方法都可以,所以这里就不在继续解释了
这些就是我的模拟memmove的工作原理
(3)memcmp
memcmp其用法和strncmp相似
下面就对这个进行解释一下:
例子:
#include <stdio.h>
#include <string.h>
int main()
{
int str1[]={ 3, 4, 5, 6, 3 };
int str2[]={ 3, 4, 5, 7, 4 };
int ret = memcmp( str1, str2, 12 );
if(ret < 0)
printf("<\n");
else if(ret == 0)
printf("=\n");
else
printf(">\n");
return 0;
}
运行结果:
至于其工作原理因为跟strncmp相似,这就不解释了
接下来就是模拟函数了
#include <stdio.h>
#include <assert.h>
int my_memcmp(const void* str1, const void* str2, size_t num)
{
assert(str1 && str2);//防止str1和str2为NULL
num--;//先减一,防止最后比较的是目标位置的下一位
while(num-- && *(char*)str1 == *(char*)str2)
{
//要想要想等,那在内存中的安排一定要是相同的
str1 = (char*)str1 + 1;
str2 = (char*)str2 + 1;
}
return *(char*)str1 - *(char*)str2;
}
int main()
{
int str1[] = { 1, 3, 5, 6, 2 };
int str2[] = { 1, 3, 4, 6, 3 };
int ret = my_memcmp( str1, str2, 12 );
if(ret < 0)
printf("<\n");
else if(ret == 0)
printf("=\n");
else
printf(">\n");
return 0;
}
运行结果:
int ret = my_memcmp( str1, str2, 8 );
此时运行结果:
这里就很好的符合了我们的预期,也证明了模拟成功了
(4)memset
接下来这个在我们日常的初始化数组之类的中非常常用的一个库函数:memset
通过cplusplus.com我们知道其用法
下面我就对这个稍加解释:
下面我就举个例子:
#include <stdio.h>
#include <string.h>
int main()
{
int arr[4]={ 0 };
memset(arr, 1, 8);
for(int i=0; i < 4; i++)
{
printf("%d\n",arr[i]);
}
return 0;
}
运行结果:
对于这个结果,我们当然也可以通过看内存来知道来理解这个结果
刚开始初始化:
使用memset初始化后:
注意这里看到是16进制下的内存空间
通过计算机,我们知道了0x01 01 01 01 十进制下为16,843,009,也就是我们输出的结果
当然我们也可以对这个memset进行一次模拟操作
#include <stdio.h>
#include <assert.h>
void* my_memset(void* ptr, int value, size_t num)
{
assert( ptr );//防止ptr为NULL
void* ret = ptr;
while(num--)
{
*(char*)ptr = value;
ptr = (char*)ptr + 1;
}
return ret;
}
int main()
{
int ptr[4]={ 0 };
my_memset(ptr, 1, 8);
for(int i=0; i < 4; i++)
{
printf("%d\n",ptr[i]);
}
return 0;
}
运行结果:
这个结果跟上面原来的memset一致,也能证明我们的模拟成功了
4.总结
这样一来,我们就学会了字符串函数和内存函数,而且也能感觉到内存函数相当于字符串函数的改良版,它有字符串函数的功能外,还有更大的拓展性,那么期待我们在下一站继续相遇!