速通C语言系列
速通C语言第一站 一篇博客带你初识C语言 http://t.csdn.cn/N57xl
速通C语言第二站 一篇博客带你搞定分支循环 http://t.csdn.cn/Uwn7W
速通C语言第三站 一篇博客带你搞定函数 http://t.csdn.cn/bfrUM
速通C语言第四站 一篇博客带你学会数组 http://t.csdn.cn/Ol3lz
速通C语言第五站 一篇博客带你详解操作符 http://t.csdn.cn/OOUBr
速通C语言第六站 一篇博客带你掌握指针初阶 http://t.csdn.cn/7ykR0
速通C语言第七站 一篇博客带你掌握数据的存储 http://t.csdn.cn/qkerU
速通C语言第八站 一篇博客带你掌握指针进阶 http://t.csdn.cn/m95FKe
速通C语言第八.五站 指针进阶题目练习 http://t.csdn.cn/wWC2x
感谢佬们支持!
上篇博客我们讲解了指针的进阶,这篇博客给大家介绍一些函数的用法、细节以及模拟实现
相比较上一篇会相对轻松一些,那就让我们开始吧
字符串函数大体分为以下几类
求长度 | strlen |
长度不受限制的 | strcpy、strcat、strcmp |
长度受限制的 | strncpy、strncat、strncmp |
字符串查找 | strstr、strtok |
错误信息报告 | strerror |
我们一个一个来搞
strlen
功能介绍
用于计算字符串长度
使用方法、细节
- 字符串以’\0’ 作为结束标志,strlen函数返回的是在字符串中’\0’ 前面出现的字符个数(不包含’\0’ )。
- 参数指向的字符串必须要以’\0’ 结束。(否则strlen会自动向后找'\0'找到后返回这个‘、0’之前的字符个数)
- 注意函数的返回值为size_t,即无符号的
这个函数我们之前已经多次使用了,这里就不在举例子了,
我们直接来模拟实现
那这里有同学要问了,为什么要模拟实现?
大佬本贾杭曾说:模拟实现不是为了造出一个更好的轮子,而是为了通过实现对使用有更好的理解
模拟实现
首先我们有三种思路
1 计数器
2 递归
3 指针-指针
这里我们采用计数器的思想,大家可以自己试试后面两种
首先我们的返回类型和原函数一样采用size_t类型,
由于只是计算长度,并不改变字符串,我们加上const修饰
size_t my_strlen(const char* str)
使用指针的好习惯是使用前先断言一下
assert(str != NULL);
当*str不为NULL时就往下迭代
完整代码如下
size_t my_strlen(const char* str)
{
assert(str != NULL);
size_t count = 0;
while (*str != '\0')
{
str++;
count++;
}
return count;
}
strcpy
功能介绍
source:源头,destination:目的地,即将源头的字符串拷到目的地上
用于拷贝字符串
使用方法、细节
- 源字符串必须以’\0’ 结束。
- 会将源字符串中的’\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
我们浅浅的使用一下
char arr[20] = { 0 };
//将hello拷至arr
strcpy(arr, "hello");
printf("%s\n", arr);
return 0;
运行一下
啊但是!如果我们的“source字符串”没有’\0‘会如何?
char arr[20] = { 0 };
char arr2 []= { 'a','b','c' };
strcpy(arr, arr2);
printf("%s\n", arr);
退出时代码不是0,可见程序崩溃了
再演示一下 destination 空间不够的情况
char arr5[5] = { 0 };
char* p = "hello YiGang";
strcpy(arr5,p);
printf("%s\n", arr5);
我们发现,拷是拷过去了,但是还是崩溃了
模拟实现
这个函数的模拟也十分简单,遍历一下,一次拷一个即可,我们直接上代码
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
while (*dest++ = *src++)
{
;
}
return ret;
}
strcat
字符串追加
使用方法、细节
源字符串必须以’\0’ 结束,否则会往后一直找\0,在\0的位置追加,可能会导致追加空间不足的情况,若找不到\0则会一直向后访问,可能会有结果,但是已经越界访问了。
目标字符串也要有'\0',否则会一直拷,直到找到'\0'为止
目标空间必须足够大,能容纳下源字符串的内容。
目标空间必须可修改。
源字符串的\0会被一起拷贝到目标字符串中。
浅浅的使用一下
char arr1[20] = "hello";
//在后面追加一个YiGang -》hello YiGang
char arr2[] = "YiGang";
strcat(arr1, arr2);
printf("%s\n", arr1);
运行一波
再来演示一下source和destination字符串分别没有’\0‘的情况
char arr1[20] = "hello";
char arr2[] = { 'Y','i','G','a','n','g'};
strcat(arr1, arr2);
printf("%s\n", arr1);
直接崩溃
char arr3[] = "wenxiao";
strcat(arr2, arr3);
printf("%s\n", arr2);
找不到’\0‘就会一直找,找到’\0'才会拷,所以就出现了我们经典的 烫烫烫烫烫
模拟实现
这个函数的核心逻辑就两个
一是找到目标字符串的'\0',而是在这个‘\0'之后拷源字符串,边拷边找'\0',找到后停止。
由刚才的查阅我们可知,strcat的返回类型是char*,要返回目标空间的地址,但是经过我们的拷贝后这个地址肯定变了,所以我们要用一个临时变量保存一下,并以此为返回值
char* ret = dest;
直接上代码
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
//找目标空间的\0
while (*dest)
{
dest++;
}
//拷贝
while (*dest++ = *src++)
{
;
}
return ret;
}
模拟实现了之后,我们来思考一下
一个字符串能否用strcat给自己追加?
假设有字符串abcd
strcat找到了abcd的'\0'后开始追加,
看起来没什么问题,啊但是!追加完d后本来该追加 \0 了,但是abcd的\0已经被追加的a给覆盖了,所以找不到 \0 了,所以这波会出问题。
char arr1[] = "abcd";
strcat(arr1, arr1);
printf("%s\n", arr1);
所以这就是模拟实现的好处,通过对模拟实现的分析,我们才能更好的理解使用的一些细节
另外,通过模拟实现来看,strcat的效率非常低,所以后期我们很少用这个函数
strcmp
比较两个字符串
- 比较的是字符串的内容,不是字符串的长度
- 比较时内容相同则比较下一对,直到不同或都遇到\0
- 第一个字符串大于第二个字符串,则返回大于0的数字
- 第一个字符串等于第二个字符串,则返回0
- 第一个字符串小于第二个字符串,则返回小于0的数字
注:在VS环境下,大于0的数为1,小于0的数为-1
我们浅浅的使用一下
char* q = "abbb";
char* p = "abc";
int ret = strcmp(q, p);
printf("%d\n", ret);
由于c>b,显然q
模拟实现
遍历两个字符串,如果相等就往后迭代,*s1取到\0说明相等,返回0.否则说明不相等
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 == *s2)
{
if (*s1 == '\0')
{
//相等
return 0;
}
s1++;
s2++;
}
//不相等
return *s1 - *s2;
}
此处不相等时我们为了简略可以直接返回*s1-*s2,而不用if~else
以上几种均为长度不受限制的函数,不安全;对应与这些函数,我们有长度受限的版本
strncpy
拷贝num个字符到目标字符串
使用方法、细节
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
- \0不算做字符串内容,num为\0之前出现的字符个数。
我们借助例子来看一下
char arr1[20] = "abcdef";
char arr2[] = "qwer";
strncpy(arr1, arr2, 2);//将arr2的2个字符拷过去
本来arr1是这样
运行strncpy那行后
(ab被替换为了qw)
如果我们要拷6个字符呢?
strncpy(arr1, arr2, 6);//将arr2的6个字符拷过去
多余的两个会补成 \0
模拟实现
模拟了strcpy后,这个的模拟就更简单了
我们只需额外处理一下拷贝个数大于源字符串个数的情况即可
char* my_strncpy(char* dest, const char* src, size_t count)
{
assert(dest && src);
char* start = dest;
while (count && (*dest++ = *src++) != '\0')
count--;
//处理操作长度大小>源字符串长度的情况
if (count)
{
while (--count)
{
//补 \0
*dest++ = '\0';
}
}
return (start);
}
strncmp
比较num长度的字符串的大小
使用方法、细节
num为比较个数,而比较的规则和strcmp一样
我们使用一波
char arr1[20] = "abcdef";
char arr2[20] = "abcdeq";
int ret=strncmp(arr1, arr2, 3);//比较前3个字符
printf("%d\n", ret);
由于二者前3个字母相同,所以应该返回0
模拟实现
我们比较时要考虑二者相等且不为 \0
由于第一个字母在循环中就会比较一次,所以操作num个数我们需循环num-1次
int my_strncmp(char* str1, char* str2, size_t num)
{
assert(str1 && str2);
while (!((*str1 - *str2))
&& *str1 && *str2
&& --num)//用前置--实现num-1次循环
{
str1++;
str2++;
}
return *str1 - *str2;
}
strncat
追加num个字符
使用方法、细节
如果源字符串的长度小于num,只会追加字符串,其余不会操作,并不会自动追加\0。
相比之下,strcpy则是硬凑到num个
我们来举个例子
char arr1[20] = "hello";
char arr2[] = "world";
strncat(arr1, arr2, 6);
模拟实现
char* my_strncat(char* dest, char* src, size_t num)
{
char* start = dest;
while (*dest)
{
dest++;
}
while (num--)//count为真
{
if ((*dest++ = *src++) == 0)
{
//操作长度大于源字符串,不进行补\0,直接返回
return(start);
}
}
*dest = '\0';//追加完毕没有返回,则手动加上\0
return (start);
}
接下来再介绍3个别的功能的函数
strstr
查找子串
使用方法、细节
查找时返回子串第一次出现位置的首地址,找不到返回空指针
我们浅浅使用一下
char arr1[] = "abcdefabcdef";
char arr2[] = "bcd";
char* ret = strstr(arr1, arr2);
if (NULL == ret)
{
printf("找不到\n");
}
else
{
printf("找到了\n");
printf("%s\n", ret);
}
模拟实现
这次的模拟实现需要考虑一些特殊情况
首先,一般的情况为
当s1不等于s2时,s1++,……s1++到c时,s1=s2,然后s1、s2再分别++,最后返回s1中c的指针
但也可能是下面这种情况
如果仅从第一个b开始匹配是不符合的,要从第三个b开始,所以我们要提前存储初始位置
同时,我们要防止s2是空串,所以应该加个判断
上波代码
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
const char* s1 = str1;//拷贝地址
const char* s2 = str2;
const char* cp = str1;//拷贝str1地址,记录当前位置
//特殊情况
if (*str2 == '\0')
{
return str1;//如果字串为空串,直接返回\0
}
while (*cp)
{
s1 = cp;
s2 = str2;
//*s1、*s2不等于0,说明二者均为查完
while (*s1 && *s2 && (*s1 == *s2))
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return (char*)cp;
}
//cp++,为了防止所给的第二种情况
cp++;
}
return NULL;
}
strtok
以第二个参数为分隔符来切割字符串
使用方法、细节
delimiter参数是个字符串,定义了用作分隔符的字符集合,第一个参数指定一个字符串,它包含了0个或者多个由delimiter字符串中一个或者多个分隔符分割的标记。
strtok函数找到str中的下一个标记,并将其用\0 结尾,返回一个指向这个标记的指针。
strtok函数的第一个参数不为NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第一个参数为NULL ,函数将在同一个字符串中被保存的位置(\0)开始查找下一个标记。如果字符串中不存在更多的标记,则返回NULL 指针。
strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。
即
char tmp[30]={0};
strcpy(arr,tmp);
浅浅使用一波
char arr[] = "Wenxiao#Yigang";
char tmp[30]={0};
strcpy(arr,tmp);
//用p储存分隔符
char* p = "#";
char*ret=strtok(tmp, p);
printf("%s", ret);
运行一下
那如果我们要重复切割怎么办?
char arr[] = "Wenxiao#Yi#gang";
//用p储存分隔符
char* p = "#";
for (char* ret = strtok(arr, p); ret != NULL; ret = strtok(NULL, p))
{
printf("%s\n", ret);
}
strerror
使用库函数时可能会调用失败,此时会设置一个错误码,strerror会将这个错误码输出为对应的错误信息。
使用方法、细节
使用时需引头文件 errno.h
errno是错误码,是一个全局的整形变量,当库函数调用失败会把相应错误码放到errno中,把错误码传给strerror函数,会返回char* 类型地址,为错误信息的首元素地址,通过printf可以打印出相应错误信息。
使用一波
int* p = (int*)malloc(INT_MAX);
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
perror和sterror
perror
通俗来讲,perror == printf +strerror
即 perror 的使用方式比strerror更加方便,如果给出自定义提示信息,它会自动补上:和空格并且直接打印出错误信息;若不给提示信息,则会直接打印错误信息。perror在得到库函数调用失败的错误码后,会根据错误码打印对应的错误信息。
使用一波
int* p = (int*)malloc(INT_MAX);
if (NULL == p)
{
perror("malloc");
}
1 字符分类函数
这部分函数使用频率不高,大家做个了解即可
函数 | 作用(符合条件就返回真,否则返回假) |
incntrl | 任何控制字符 (0~31及127(共33个)为控制字符或通讯专用字符,如换行,回车,换页,删除等控制字符,SOH(文头)、EOT(文尾)、ACK(确认)等通讯专用字符)。 |
isspace |
空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’ |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母 a~f,大写字母 A~F |
islower | 小写字母 a~z |
isupper | 大写字母 A~Z |
isalpha | 字母 a~z或 A~Z |
isalnum | 字母或数字 a~z, A~Z, 0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 (除空格以外的任何可打印字符。可以理解为就是检测一个字符是否是一个可见字符) |
isprint | 任何可打印字符,包括图形字符和空白字符 |
2 字符转换函数
tolower
大写字母转小写字母,其他不操作
例:
char ch1 = '0';
char ch2 = 'A';
char ch3 = 'a';
printf("%c\n", tolower(ch1));
printf("%c\n", tolower(ch2));
printf("%c\n", tolower(ch3));
toupper
小写字母转大写字母,其他不操作
例:
char ch1 = '0';
char ch2 = 'A';
char ch3 = 'a';
printf("%c\n", toupper(ch1));
printf("%c\n", toupper(ch2));
printf("%c\n", toupper(ch3));
memcpy
相比于strcpy只能拷贝字符串,memcpy可以对任何单位数据进行拷贝
使用方法、细节
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到’\0’ 的时候并不会停下来,只有当元素拷贝完成后才会停下来。而strcpy遇到"\0"会停下来
- memcpy需要拷贝所有的类型的数据,所以用void*指针。
- 你可以拷任意字节的数据
使用一波
int arr1[20] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[20] = { 0 };
//将arr1前5个元素传至arr2?
memcpy(arr2, arr1, 20);//5*4=20
我们通过调试来观察
运行memcpy后,
当然,我们也可以拷17、18、19个字节
模拟实现
为了实现一个字节一个字节拷贝,我们使用经典操作,将void*指针强转为char*类型指针
每复制一个字节后要写加1而不能写++,要不然就没有强制类型转换的效果了
上代码
void* my_memcpy(void* dst, const void* src, size_t num)
{
assert(dst && src);
//记住先拷贝一份地址,便于返回
void* ret = dst;
while (num--)
{
*(char*)dst = *(char*)src;
//写 +1 而非 ++
dst = (char*)dst + 1;
src = (char*)src + 1;
}
return ret;//返回void*指针
}
现在我们想改需求怎么办?
如果将 12345 放到 34567 的区间,能否实现?
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr1 + 2, arr1, 20);
调试看看
刚开始为
运行my_memcpy后
很明显,我们写的my_memcpy自己给自己拷贝时重叠了
当我们拷贝前两个数时没问题
但当我们准备拷3时,发现3已经被我们之前拷过去的1覆盖了,所以拷3的过程中实际上拷贝的是1,后面的拷过去的数也是同理
所以应对这种拷贝重叠的问题时,我们一般用下面这个函数memmove
memmove
对任何单位数据进行拷贝,重叠和不重叠的都能搞定。
使用方法、细节
可以解决所谓拷贝重复的问题
我们用memmove解决上面这个例子
memmove(arr1 + 2, arr1, 20);
运行之后
(直接拿捏)
模拟实现
模拟实现memmove的关键一点就是解决拷贝重复的情况
当遇到这种情况时,我们可以用一种”倒着拷“的思路,(这种思路非常重要,在我们做题时遇到类似问题,也会用这种思路)
刚才我们是从1开拷的,这次我们从5开始拷。
当我们实际画图思考之后,发现确实没有拷贝重叠的情况发生了,非常奈斯
但是!
如果我们由想把 34567 拷到 12345 所在位置,你再倒着拷,会怎样呢?
竟然又出现了拷贝重复的问题,而如果正着拷就不会出现问题,由此可见,我们并不能无脑“倒着拷”
通过观察可知,当src的字符串和dst的字符串有重合时,src在dst右边时,我们正着拷,而src在dst右边时,我们反着拷,而当src的字符串和dst的字符串有重合时,我们就无所谓怎么拷了
为了方便,我们没有重合时统一从后往前拷
代码实现一波
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
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;
}
注:需要注意的是,VS中的库函数memcpy既可以拷贝重叠,又可以拷贝不重叠,即可以完成memmove的以上操作,太牛逼啦
memcmp
对指定单位内容进行比较
使用方法、细节
基本细节于strcmp保持一致,即
- 第一组数据的一个字节内容若小于第二组数据的一个字节内容返回小于0的值
- 第一组数据的一个字节内容若等于第二组数据的一个字节内容返回0
- 第一组数据的一个字节内容若大于第二组数据的一个字节内容返回大于0的值
- 但是他比较的是字节数,即num是几,他就比较几个字节
使用一波
float arr1[] = { 1.0,2.0,3.0,4.0 };
float arr2[] = { 1.0,3.0 };
int ret=memcmp(arr1, arr2,6);
printf("%d\n", ret);
运行之后
模拟实现
相比上两个函数这个基本没什么变化,所以非常容易模拟实现
int my_memcmp(const void* arr1, const void* arr2, size_t num)
{
assert(arr1 && arr2);
while (num--)
{
if (*(char*)arr1 != *(char*)arr2)
{
return (*(char*)arr1) - (*(char*)arr2);
}
arr1 = (char*)arr1 + 1;
arr2 = (char*)arr2 + 1;
}
return 0;
}
memset
将ptr所指空间的前num个字节设置成指定的value值
使用方法、细节
- 内存设置的元素相同
- 接收数据的最大值为ff,也就是一个字节的最大数据
浅浅使用一波
int arr[10] = { 0 };
memset(arr, 1, 20);
运行之后
思考:由于memset操作的是字节,将20个字节单位改成1,那么设置的元素就不是1,而是16进制位0x01010101的元素。
所以由于他改成了16进制数,我们打印出来的数将会很大
for (int i = 0; i < 10; ++i)
{
printf("%d ",arr[i]);
}
模拟实现
void* my_memset(void* dest, int c, size_t num)
{
assert(dest);
void* ret = dest;
while (num--)
{
*(char*)dest = c;
dest = (char*)dest + 1;
}
return ret;
}
做总结,今天向大家介绍了字符相关函数和内存函数的使用细节及模拟实现,介绍这些函数的同时不仅是让大家学会用,也算是为将来学习C++中的string类稍微的铺垫一下
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。