文章目录
- 求字符串长度
- 一、strlen()
- 长度不受限制的字符串函数
- 一、strcpy()
- 二、strcat()
- 三、strcmp()
- 长度受限制的字符串函数
- 一、引入
- 二、strncpy()
- 三、strncat()
- 四、strncmp()
- 字符串查找函数
- 一、strstr()
- 二、strtok()
- 错误信息报告函数
- 一、strerror()
- 字符操作函数
- 内存操作函数
- 一、memcpy()
- 二、memmove()
- 三、memset()
- 四、memcmp()
- 总结与提炼
求字符串长度
一、strlen()
1. 函数原型
size_t strlen ( const char * str );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,求一下下面这个字符串的长度
int main(void)
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("len = %d\n", len);
return 0;
}
- 为什么算出来的是【6】呢?通过调试我们可以看到,对于
strlen()
来说,计算的是从字符串开头到字符串末尾的\0
为止一共有多少字符,那数一下就可以知道有6个
4. 注意事项
接下去我们来说说有关这个函数的一些注意事项
① 参数指向的字符串必须要以 ‘\0’ 结束
- 可以看到,若此时我将arr字符数组初始化成这样的话,计算出来的长度就不正确了,因为这样去初始化的话其实根本不能算作是一个字符串,它的结尾是没有
\0
的
- 通过调试观察就可以发现,字符数组arr末尾是没有
\0
的,这才印证了为什么上面这个很奇怪的数,对于这个字符数组,它在内存中的布局是这样的:[][][][][a][b][c][][][]
,编译器为它在内存中随机分配了一块空间,既然没有\0
的话,它在内存中前后有什么东西就是不确定的,是随机的,我们去计算它的地址并不存在什么意义
② 注意函数的返回值为size_t,是无符号的( 易错 )
- 请问下面这段代码的运行结果是多少?会进入哪个if分支呢?
int main(void)
{
size_t t;
if (strlen("abc") - strlen("abcdef") > 0)
{
printf(">\n");
}
else
{
printf("<=\n");
}
return 0;
}
- 可以看到,最后的结果出人意料地为【>】,因为上面说到了strlen()函数计算的是字符串末尾的
\0
之前的字符个数,那么if()条件中即为3 - 6 = -3 < 0
,那一定会进入第二个分支,打印出来的结果就是【<=】,但为什么最后的结果是【>】呢?
此时,请你再回到第二小节,看一下strlen()函数的返回值,为size_t
- 可以去库里看看它的定义,就发现它的原型是【unsigned int】 —— 无符号整型。在深度挖掘数据在计算机内部的存储一文中,我们有说到在计算机内部对于一个负数来说它会被当成一个无符号整型来进行处理,那它就会是一个非常大的正数,所以最后的结果 > 0就是这么出来的
5. 模拟实现
接下去的话我们就来模拟实现这个strlen()函数,这里我介绍三种方法
方法1:计数器
- 首先第一种就是采用计数器的形式,最简单直观
size_t my_strlen1(const char* str)
{
int count = 0;
while (*str)
{
str++;
count++;
}
return count;
}
方法2:递归
- 第二种便是采用递归的形式,具体的分析可以看看C语言的函数章节
/*
* a b c d e f \0
* 1 + b c d e f \0
* 1 + 1 + c d e f \0
* 1 + 1 + 1 + d e f \0
* 1 + 1 + 1 + 1 + e f \0
* 1 + 1 + 1 + 1 + 1 + f \0
* 1 + 1 + 1 + 1 + 1 + 1 + \0
*/
size_t my_strlen2(const char* str)
{
if (*str == '\0')
return 0;
return 1 + my_strlen2(str + 1);
}
方法3:指针相减【计算的就是二者之间相差的元素个数】
- 在C语言指针章节,我也有详细得说到过两个指针相减计算的就是它们之间相差的个数,因此我们可以先记录一下首字符的地址,直到指针偏移到末尾的
\0
时,将两个地址一减最后的结果便是字符串的长度
size_t my_strlen3(const char* str)
{
const char* tmp = str;
while (*tmp)
{
tmp++;
}
return tmp - str;
}
长度不受限制的字符串函数
一、strcpy()
1. 函数原型
char * strcpy ( char * destination, const char * source );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,将一个字符串拷贝到另一个字符串中
char name[20] = { 0 };
strcpy(name, "zhangsan");
printf("%s\n", name);
- 首先简单一些,定义初始化了一个字符数组name,里面可以容纳20个元素,使用
strcpy()
就可以将“zhangsan”拷贝到这个字符数组中
- 也可以通过调试来看看最后有没有拷贝过去
4. 注意事项
接下去我们来说说有关这个函数的一些注意事项
① 源字符串必须以 ‘\0’ 结束,因为源字符串中的 ‘\0’ 会被拷贝到目标空间
- 继续定义两个字符数组进行拷贝的工作测试,为了能够看得更清楚,str1中我使用的都是
*
char str1[] = "**************";
char str2[] = "hello world";
strcpy(str1, str2);
printf("%s\n", str1);
- 但结果是不是和你想象得有所不同,可能以为的是
hello world***
,但是d
后面并没有任何东西,原因其实就在于字符串最后面的\0
,str2里面存放的是个字符串,最后面是带有\0
的, 通过strcpy()进行拷贝的时候,会将末尾的\0
也一起拷贝过去 - 又因为
%s
打印字符串的时候也是以末尾的\0
作为结束的标志,因此打印到此处就结束了,不会再打印后面的***
- 但此时若是我将原字符串改为末尾不带
\0
,会发生什么呢?我们运行起来看看
- 可以看到,程序发生了奔溃,因为原字符串的末尾没有
\0
,所以在拷贝的时候编译器完全不知道什么时候停下来,所以在一直拷贝的过程中就会发生【越界访问】的问题
——> 所以需要拷贝的原字符串一定要以\0
结尾,否则会出现问题
② 目标空间必须足够大,以确保能存放源字符串
- 不仅是源头有限制,目标字符串也需要有一定的限制,不可以过随意。例如说下面要将字符数组中的
abcdef
拷贝到空间只有3的字符数组str1中去,会发生什么呢?
char str1[3] = { 0 };
char str2[] = "abcdef";
strcpy(str1, str2);
printf("%s\n", str1);
- 可以看到,str2虽然拷贝过去了,但是str1这个字符数组却被破坏了,原因就在于str1数组的容量太小了,不足以容纳
abcdef
——> 所以我们在拷贝字符串的时候也要考虑到目标字符串的空间是否足够容纳原字符串
③ 目标空间必须可变
- 不过目标空间除了要有足够大的空间之外,还要保证可以变,因为将源字符串拷贝过去的时候,肯定会修改目标空间的内容,若是目标空间不可以修改的话,那就是无稽之谈了😆
- 如果你有认真看过指针章节的话,应该对指针p并不陌生,它存放的就是字符串
abcdef
中【a】的首元素地址,我们知道对于一个字符串来说为一个常量,是不可修改的,所以定义指针p最标准的写法还是const char* p = "abcdef"
,这是一个常量指针,表示指针p所指向的那块空间中的内存是不可修改的,因此将"bit"
拷贝过去的话便是非法的
char* p = "abcdef";
char* str = "bit";
strcpy(p, str);
printf("%s\n", p);
- 通过调试可以看出,对内存中一块只读的空间进行修改的时候就会发生【访问冲突】的问题
5. 模拟实现
接下去的话我们就来模拟实现这个strcpy()函数,细致讲解可以看看模拟实现库函数strcpy之梅开n度
- 这里我就给出最后最优的版本,首先在进入函数的时候要先
assert()
断言一下,不过别忘了包含头文件哦!因为在字符拷贝的过程中指向目标空间的dest
指针会偏移到最末尾\0
的位置,但我们最后要返回拷贝完后指向目标空间起始地址的指针,因此在一开始要先做一个保存才行 - 再下去的话就是拷贝的逻辑,随着
*dest++ = *src++
的不断进行,最终源头中的\0
会被拷贝到目标空间中,那此时就作为这个while判别式的条件,为0即终止循环,此时src中的所有内容都拷贝过来了,返回我们一开始保存的目标空间的起始地址即可
char* m_strcpy(char* dest, const char* src)
{
assert(dest && src);
char* begin = dest; //保存一下目标空间字符串的起始地址
while (*dest++ = *src++)
{
;
}
return begin;
}
二、strcat()
1. 函数原型
char * strcat ( char * destination, const char * source );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,拼接一下两个字符串
char arr1[20] = "hello ";
printf("%s\n", strcat(arr1, "world"));
那既然是拼接,是从什么地方开始拼接的呢?这里猜测一波是\0
😁
- 通过调试观察可以发现,
world
就是从arr1的\0
处开始拼接的,而且也会将自己的\0
拷贝过去
4. 注意事项
接下去我们来说说有关这个函数的一些注意事项,与
strcpy
类似
① 源字符串必须以 ‘\0’ 结束
- 可以看到,若是将源字符串初始化为无
\0
的,在拷贝的过程中就会出现问题
char arr1[] = "hello \0********";
char arr2[] = { 'a', 'b', 'c' };
printf("%s\n", strcat(arr1, arr2));
- 可以看到,虽然是拼接了,但是因为在字符串的末尾没有
\0
,所以在打印的时候编译器就会一直去寻找\0
继而导致访问冲突的问题
② 目标空间必须有足够的大,能容纳下源字符串的内容
char arr1[3] = { 0 };
char arr2[] = "abcdef";
printf("%s\n", strcat(arr1, arr2));
- 也是一样,不仅是源头有要求,目标空间也有一定的要求,如果没有足够大空间的话也放不下想要拼接过来的内容
③ 目标空间必须可修改
char* p = "abcdef";
char arr2[] = "ghijkl";
printf("%s\n", strcat(p, arr2));
- 一样,若是目标空间不可修改的话,拼接也是【无稽之谈】,会造成访问冲突的问题
④ 不可以给自己做追加
char arr1[20] = "abcdef";
printf("%s\n", strcat(arr1, arr1));
- 还有一点要说明的是不可以自己给自己做追加,因为源字符串是在目标字符串的
\0
位置开始拼接的,也就是说这个\0
会被覆盖掉,那么在想要追加自己原本的\0
时,却找不到了,即自己在给自己追加的时候会把自己的内容破坏,使得自己在停下来的时候没有\0
了
- 通过调试再来看一下
5. 模拟实现
接下去的话我们就来模拟实现这个strcat()函数,和strcpy()很类似
- 因为其进行拼接的时候是从
\0
的位置开始的,因此我们在模拟实现的时候就要先去找到目标字符串中的\0
才行,保存一下【dest】就可以出发了,一直寻找直到找到\0
为止停下来 - 接下去的逻辑就和
strcpy()
一样了,把源字符串拷贝到目标字符串的\0
处
char* m_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest; //保存一下目标字符串的起始地址
//1.寻找目标字符串中的\0
while (*dest != '\0')
{
dest++;
}
//2.从目标字符串的\0开始拷贝源字符串
while (*dest++ = *src++)
{
;
}
return ret;
}
三、strcmp()
1. 函数原型
int strcmp ( const char * str1, const char * str2 );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,比较一下两个字符串
int main(void)
{
char arr1[] = "zhangsanfeng";
char arr2[] = "zhangsanfeng";
int ret = strcmp(arr1, arr2);
if (ret == 1)
printf(">\n");
else if(ret == -1)
printf("<\n");
else
printf("==\n");
return 0;
}
下面是strcmp()
函数的比较规则:
- ptr1所指向小于ptr2,返回 < 0的数【VS下是-1】
- ptr1所指向等于ptr2,返回 0
- ptr1所指向大于ptr2,返回 > 0的数【VS下是1】
4. 模拟实现
接下去的话我们就来模拟实现这个strcmp()这函数
- 可以看到,主体就是在比较
*str1
和*str2
,若是它们相同的话就一直++,若是不相同的话便跳出循环继续比较谁大谁小,那么判断二者完全相同的逻辑就只能写在循环内部了,判断*str == '\0'
就可以看出它是不是走到了字符串的末尾,而且还没有跳出循环,此时就可以return 0;
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0; //二者相同且为'\0',return 0
str1++;
str2++; //否则向后继续查找
}
if (*str1 < *str2)
return -1;
else
return 1;
}
- 那既然
*str1
和*str2
我们都知道是两个字符了,直接相减判断其ASCLL码即可
return *str1 - *str2;
长度受限制的字符串函数
讲完了长度不受限制的字符串函数,接下去我们再来说说长度受限制的字符串函数,和上面的一组函数很像,可以指定长度大小的字符串进行操作
一、引入
💬 那有同学就问了:既然有了一组这样的函数了,为什么还要再大费周章地搞出来一组呢?
- 还记得我们在将
strcpy()
的时候说到在拷贝的时候目标字符串要有足够大的空间来容纳源字符串吗?但是你仔细去观察的话是可以发现,虽然目标空间有时候放不下,但是编译器还是把它拷贝过去了,然后才报出来Error❌
- 不过编译器其实是有一些Warning⚠的,因为在计算机内部有个东西叫做【缓冲区】,计算机从外设中读入的东西首先是要放到缓冲区中的,这个缓冲区在内存中,然后CPU再去内存中的缓冲区里拿东西,这里稍微拓展一下,想要多了解一点可以看看冯诺依曼体系结构与操作系统
- 看到这里报出警告说【缓冲区内可写入4个字节,但是实际的操作却写入了7个字节】
其实对于上面的这种越界写入是很危险的事情,正常来说编译器应该要爆出错误,而不是只警告一下,原因就在于我在首部加上了这句
#define _CRT_SECURE_NO_WARNINGS 1
- 将其去掉之后就可以看到爆出了下面这样的错误。所以其实就是因为上面这句话才使得编译器没有报出错误,其实编译器是很严谨的👈
要知道,C语言很早就被设计出来了,多多少少存在着一些缺陷和不完整性,那我们也不能去怪设计语言的人,毕竟【人有失手,马有失蹄】,【人非圣贤,孰能无过】呢!
那接下去呢就让我们来看看下面的这几组函数
二、strncpy()
1. 函数原型
char * strncpy ( char * destination, const char * source, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,把arr2中的n个字符拷贝到arr1中
int main(void)
{
char arr1[10] = { 0 };
char arr2[] = "hello world";
strncpy(arr1, arr2, 5);
printf("%s\n", arr1);
return 0;
}
- 我们通过调试来观察一下n个字符是否有被拷贝过去了
- 但是呢,有些时候会出现像下面这样的场景,即源字符串中只有3个字符,但是拷贝过去却要拷5个的情况,由运行结果我们可以看到,确实是拷贝过去了,也没有出现任何的问题
- 通过调试也可以看出,确实原封不动地拷贝过去了,但是这样看不出最后的
\0
到底有没有过去,我们将目标字符串做一个修改
- 通过对目标字符串做一个修改,然后再去进行一个拷贝就可以发现,在在首先拷贝了原先的【b】【i】【t】【\0】后,又在后面补上了一个
\0
,这样就凑足了5个
- 若是需要拷贝7个过去的话也是同理,会在后面补充3个
\0
4. 模拟实现
接下去的话我们就来模拟实现这个strncpy()函数
- 思路很简单,首先第一块逻辑就是将原字符串中num个字符拷贝过去,拷一个
num--
,直到num个字符拷贝完为止。 - 接着第二块逻辑,就是去判断一下num是否 > 0,若是的话那就表示
num > 原字符串的长度
,此时就需要再做【补充\0的工作】,不过while循环中的条件要写--num
,否则的话就会多进入一次,那后面就会多出一个\0
char* my_strncpy(char* dest, const char* src, size_t num)
{
assert(dest && src);
char* start = dest;
while (num && (*dest++ = *src++))
{
num--;
}
//若是跳出循环后num > 0,表示num > 原字符串的长度
if (num)
{
while (--num)
{
*dest++ = '\0'; //再补充num个'\0'
}
}
return start;
}
三、strncat()
1. 函数原型
char * strncat ( char * destination, const char * source, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,拼接一下指定的字符个数
int main(void)
{
char arr1[20] = "hello ";
char arr2[] = "world wide web";
strncat(arr1, arr2, 5);
printf("%s\n", arr1);
return 0;
}
- 一样,我们通过调试来看看
- 不过这看不出最后的
\0
有没有过去,我们将目标字符串做个修改,自己手动加上一个\0
,然后在后面加上******
,继续通过调试来观察可以发现,源字符串中的\0
是会被拷贝过去的
还是一样,对于
strncat()
来说也会出现需要拷贝的字符个数 > 源字符串原先的个数,那此时也会和strncpy()
一样在后面补充\0
吗?我们继续通过调试来看看
- 可以看到,原本的
hello
加上8个最后的arr1长度应该为13,即数组下标12的地方为\0
,但是在调试看来却不是这样,d
的后面还是只有一个\0
,编译器并没有做过多的补充,那么这也就印证了我们原先解读函数时说的那些东西
5. 模拟实现
接下去的话我们就来模拟实现这个strncat()函数
- 前面的思路还是和
strcat()
一样,让【dest】先移动到\0
的位置,然后第二块逻辑,就是从从\0
的位置开始拷贝src中的num个字符 - 内部是一个拷贝逻辑,不过在我测试了多次后,这个拷贝的逻辑和判断是否到达
\0
的逻辑必须放在一起,即从源头拷贝过来\0
的那一瞬间就立马返回,因为*dest++
这是一个后置++,当这句代码执行完后dest又会往后进行偏移,此时就不对了,要在拷贝到\0
立马返回当前目标字符串的起始地址- 当然上述的灵感也是来自于官方的库中,否则也很难想到这一点
- 最后的话若是在循环内部没有找到
\0
的话就需要自己手动去加上了,保证一个字符串的完整性,最后也是一样返回目标字符串的起始地址
char* my_strncat(char* dest, const char* src, size_t num)
{
assert(dest && src);
char* start = dest;
//1.首先让dest先移动到\0的位置
while (*dest != '\0')
{
dest++;
}
//2.从\0开始拷贝src中的num个字符
while (num--)
{
if((*dest++ = *src++) == '\0')
return start; //碰到\0直接返回,不再补充\0
}
*dest = '\0'; //最后在目标字符串的末尾处添上\0
return start;
}
四、strncmp()
1. 函数原型
int strncmp ( const char * str1, const char * str2, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,比较一下两个字符串中固定字符的大小
int main(void)
{
char arr1[] = "abcdef";
char arr2[] = "abcz";
int ret = strncmp(arr1, arr2, 3);
if (ret == 0) {
printf("==\n");
}
else if(ret < 0) {
printf("<\n");
}
else{
printf(">\n");
}
return 0;
}
- 首先是比较两个字符串中的前3个,可以看到
abc
与abc
是相同的
- 首先是比较两个字符串中的前4个,可以看到
abcd
是小于abcz
的
- 将
abcz
换成abcc
后,结果又会有所不同
💬 不过呢,要注意这里的返回值ret,不可以用== 1
或== -1
这样去判断
- 通过运算我们可以发现,在VS下若是前者小于后者返回的结果便是【-1】,但是在其他编译器上可不一定,如果你有仔细看过
strcmp()
的话就可以知道它返回的只是>/</== 0
的数字,而不是具体的数值,因此我们不能将值写死,否则在其他编译器例如gcc上就跑不过去了
4 模拟实现
对于strncmp()的实现比较复杂,这里就不做详解了,有兴趣的同学可以自行阅读一下库里提供的源码
int __cdecl strncmp
(
const char *first,
const char *last,
size_t count
)
{
size_t x = 0;
if (!count)
{
return 0;
}
/*
* This explicit guard needed to deal correctly with boundary
* cases: strings shorter than 4 bytes and strings longer than
* UINT_MAX-4 bytes .
*/
if( count >= 4 )
{
/* unroll by four */
for (; x < count-4; x+=4)
{
first+=4;
last +=4;
if (*(first-4) == 0 || *(first-4) != *(last-4))
{
return(*(unsigned char *)(first-4) - *(unsigned char *)(last-4));
}
if (*(first-3) == 0 || *(first-3) != *(last-3))
{
return(*(unsigned char *)(first-3) - *(unsigned char *)(last-3));
}
if (*(first-2) == 0 || *(first-2) != *(last-2))
{
return(*(unsigned char *)(first-2) - *(unsigned char *)(last-2));
}
if (*(first-1) == 0 || *(first-1) != *(last-1))
{
return(*(unsigned char *)(first-1) - *(unsigned char *)(last-1));
}
}
}
/* residual loop */
for (; x < count; x++)
{
if (*first == 0 || *first != *last)
{
return(*(unsigned char *)first - *(unsigned char *)last);
}
first+=1;
last+=1;
}
return 0;
}
字符串查找函数
一、strstr()
1. 函数原型
const char * strstr ( const char * str1, const char * str2 );
char * strstr ( char * str1, const char * str2 );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,看一下str2这个子串在str1中
int main(void)
{
char str1[] = "abcdefabcdef";
char str2[] = "def";
char* substr = strstr(str1, str2);
printf("%s\n", substr);
return 0;
}
- 可以看到,最后返回的结果是子串
def
在主串abcdefabcdef
中出现的第一个位置,我们使用%s
去打印的话就会从这个位置开始往后打印后面的字符串
- 但我若是去更换一下str2的话,它就不存在于str1中了
5. 模拟实现
接下去的话我们就来模拟实现这个strstr()函数,比较复杂一些,要集中注意力哦!
情况①:匹配一次就成功
- 首先是第一种情况,那就是子串在和主串匹配的时候一次就能匹配成功了
情况②:匹配多次才成功
- 接下去第二种情况,就是需要匹配多次才能成功,可以看到一开始前面出现了
b b b
,但是我们要匹配的子串是b b c
,所以在匹配到第三个b的时候就需要进行重新匹配 - 那若是要重新匹配的话就需要让【s1】和【s2】进行重新置位的操作,【s2】的话很简单,直接回到初始的位置即可,但是对于【s1】的话其实没有必要,我们可以设置一个【p】记录子串在主串中的位置,如果在匹配的过程中失配了,只需要让【s1】回到
p + 1
的位置即可,因为从【p】的位置开始已经不可以匹配成功了,具体地我在下面讲述代码的时候细说
首先给出整体代码可以先看看
const char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
const char* s1 = str1;
const char* s2 = str2;
const char* p = str1;
while (*p)
{
s1 = p;
s2 = str2;
while (s1 != '\0' && s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return p; //此时p的位置即为子串s2在s1中出现的第一个位置
}
p++;
}
return NULL; //若是主串遍历完了还是没有找到子串,表明其不在主串中,返回NULL
}
细说一下:
- 首先我们看到开头的三个指针定义,因为在失配的时候需要指针回到字符串的起始位置,所以【str1】和【str2】的位置我们不可以去动它,那两个指针另外做移动,然后再拿一个【p】记录位置
const char* s1 = str1;
const char* s2 = str2;
const char* p = str1;
- 在while循环内存,最主要的还是这段匹配的逻辑,若是
*s1
和*s2
z中的存放的字符相同的话,就继续往后查找,但是呢它们不能一直无休止地往后查找,总有停下来的时候,那也就是当指针所指向的内容为\0
时,就需要跳出循环
while (s1 != '\0' && s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
- 若只是二者不相同跳出来了,此时
p++
即可,然后回到循环判断*p
是否为\0
,若还没有碰到主串末尾的话,就需要更新s1
和s2
的位置,继续进行匹配的逻辑
p++;
s1 = p;
s2 = str2;
- 若是
*s2 == '\0'
的话,此时就表示子串已经匹配完成了,都到达末尾了,那么这个时候我们应该返回【子串在主串中出现的第一个位置】,这也是strstr()
的本质,那么这个位置在哪里呢?因为我们是哪p
去记录位置的,那就可以说在主串中从指针p所指向的这个位置开始直到*s2
到末尾时,即为匹配成功子串的一个位置
if (*s2 == '\0')
{
return p; //此时p的位置即为子串s2在s1中出现的第一个位置
}
匹配过程解说:
看完匹配的过程相信你对strstr()这个函数应该非常清楚了,但其实它的效率并不是很高,在我们看来它只是一个【暴搜】的过程,若是想要追求更加高效的匹配过程,可以看看KMP算法
二、strtok()
1. 函数原型
char * strtok ( char * str, const char * delimiters );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,求一下下面这个字符串的长度
int main()
{
char sep[] = "@.";
char email[30] = "2350045583@qq.com";
char* ret = strtok(email, sep);
if (ret != NULL)
printf("%s\n", ret);
ret = strtok(NULL, sep);
if (ret != NULL)
printf("%s\n", ret);
ret = strtok(NULL, sep);
if (ret != NULL)
printf("%s\n", ret);
return 0;
}
- 本函数也可以叫做【字符串分割函数】,根据所传入的
seq
分割字符数组,来确定要以何种字符来进行分割,这里我采用的是@
和.
,那么在这个函数执行的时候,就会根据这两个字符来进行分割 - 细心的同学应该可以发现我两次在传递参数的时候是不一样的,只有第一次传递了
email
字符串,但第二、三次传递的都是NULL,如果你有认真阅读过这个函数,就知道为什么了我这样做了,- strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置
- strtok函数的第一个参数为 NULL ,函数将在同一个字符串中
被保存的位置开始
,查找下一个标记 - 如果字符串中不存在更多的标记,则返回 NULL 指针
- 有了上面的这些规则,相信你一定能理解这个函数了
- 可以看到,我在获取到分割的子串后去打印时都会判断一下它是否为空,因为原文中有写到
If a token is found, a pointer to the beginning of the token.Otherwise, a null pointer.
所以它是有可能返回一个空指针的,对于一个空指针来说,我们就无需去打印了
代码优化:
因为strtok函数会改变被操作的字符串,所以我们一般不会对原字符串进行操作,而会去选择临时拷贝一份
- 这个时候就可以使用到我们前面所学的
strcpy
,此时再去操作的话原字符串就不会被修改了
char cp[30];
strcpy(cp, email); //临时拷贝一份
char* ret = strtok(cp, sep);
if (ret != NULL)
printf("%s\n", ret);
ret = strtok(NULL, sep);
if (ret != NULL)
printf("%s\n", ret);
ret = strtok(NULL, sep);
if (ret != NULL)
printf("%s\n", ret);
但你是否觉得上面这样判断一次打印一次很麻烦,这种代码要是给你上司看到的话指不定会被骂成什么样,我们不要写重复的逻辑,尽量将其进行封装,那对于上面的重复工作,其实我们可以使用【循环】来做一个优化
- 就像下面这样,我们可以将这些逻辑写到for循环中去,如果你有看过C语言分支和循环语言的话,就可以知道对于for循环来说第一个表达式是只会被执行一次的,也就是一开始进来出初始化的时候,而我们传递参数给
strtok()
的时候也是只在第一次传递字符串给第一个参数,后面的话就都传递NULL了 - 因此后面的传值改变我们可以写在循环变量调整的位置,即第三个表达式处。那第二个表达式我们最熟悉了,就是写for循环的终止条件,因为我们始终拿的就是
ret
去接收每一次分割后的返回值然后去打印,那么最后的话当分割到字符串结尾的时候没有了就会返回NULL,那此时我们将其作为结束条件来判断即可
for (ret = strtok(cp, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
完整代码如下:
int main()
{
char sep[] = "@.";
char email[30] = "2350045583@qq.com";
char cp[30];
strcpy(cp, email); //临时拷贝一份
char* ret = NULL;
for (ret = strtok(cp, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
return 0;
}
错误信息报告函数
一、strerror()
1. 函数原型
char * strerror ( int errnum );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,求一下下面这个字符串的长度
int main(void)
{
printf("%s\n", strerror(0));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
printf("%s\n", strerror(4));
printf("%s\n", strerror(5));
return 0;
}
- 可以看到,这里我打印了一些错误信息,也就是每种数字所示对应的【错误信息】
当然这个函数不是这么用的,我们可以在实际的场景中来试试,比方说这里要打开一个文件,那么打开文件的话就一定存在打开失败的情况,此时我们就可以使用
strerror()
去给出一些错误信息
- 在这里看到我给这个函数内部传入了一个东西叫做【errno】,它是一个错误变量,里面记录了很多常见的错误,我们若是不知道要传入哪个数字来显示错误信息的话,只需要传入这个变量即可,它是C语言设置的一个全局的错误码存放的变量
- 只不过你要只用的话需要包含一下
#include <errno.h>
这个头文件才可以
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
else {
printf("文件打开正常\n");
}
return 0;
}
- 可以看到,此时我在当前目录下创建了一个
test.text
的文本文件,然后通过fopen()
函数去打开它,如果不清楚这个函数的话可以看看C语言文件操作指南
- 但若是我将文件的文件名改换一下,此时文件一定是打开失败的,那么就会通过
strerror(erron)
这个函数去打印一些相关的错误信息
这个函数了解一下即可,不用过于深究
字符操作函数
📚下面给出一起有关字符操作的函数,它们都可以在cplusplus这个网站中搜到,感兴趣的同学可以去多了解一点
函数 | 如果他的参数符合下列条件就返回真 |
---|---|
iscntrl | 任何控制字符 |
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 | 任何可打印字符,包括图形字符和空白字符 |
- 下面演示两个比较常用的
isupper()
判断是否为大写字母,以及tolower()
将大写字母转为小写
#include <stdio.h>
#include <ctype.h>
int main()
{
int i = 0;
char str[] = "Test String.\n";
char c;
while (str[i])
{
c = str[i];
if (isupper(c))
c = tolower(c);
putchar(c);
i++;
}
return 0;
}
内存操作函数
一、memcpy()
1. 函数原型
void * memcpy ( void * destination, const void * source, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,求一下下面这个字符串的长度
- 可以看到,我们要为
memcpy()
传入的前两个参数就是目的地址和源地址,最后一个参数的话就是要拷贝的字节数,记住,这里是【字节数】而不是【元素个数】,所以可以看到我是用sizeof(int)
首先求出了数组中每个元素的字节数,然后在乘上数组元素个数,就是整个数组所占的字节数
int main(void)
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int arr2[10] = { 0 };
memcpy(arr2, arr1, sizeof(int) * sz);
return 0;
}
- 除了整型数据,
memcpy()
也可以拷贝浮点型的数据,上去仔细看看原函数就可以知道目标地址和原地址的类型都是void*
,表明它们可以接收任意类型的地址,即可以拷贝任意类型的数据
int main(void)
{
float arr1[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
float arr2[5] = { 0 };
memcpy(arr2, arr1, sizeof(int) * sz);
return 0;
}
5. 模拟实现
接下去的话我们就来模拟实现这个memcpy()函数
void* my_memcpy(void* dest, const void* src, int num)
{
assert(dest && src);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
- 这里主要讲一下的就是这个内部的拷贝逻辑,之前我们在使用
strcpy()
的时候是直接用【*dest = *src】的,但是这里的话我们不能这么去操作,上面讲到过两个目标指针和源指针都是void*
类型的,这种指针类型是不可以直接进行解引用的,而是要在内部对其进行强制类型转换 - 那转成什么类型的指针呢?
int*
、float*
、double*
吗?不,这些都不可以,设想我们传入的字节数是28,那使用int*
类型的指针去拷贝确实可以做到,但若是我传入的总字节数为27呢?不是一个4字节或者8字节的整数倍,那要怎么去拷贝呢? - 但是有一个类型的指针却可以做到,那就是
char*
,无论你要我拷多少字节的数据,反正我解引用每次只能拷贝1个字节的数据,那么就一个个拷过去就行了,虽然效率上来说是低了一些,但是容错率下降了,就不会出现什么大问题
- 当单个字节的数据拷贝完成后,指针就向后偏移指向下一个要拷贝的数据,那也强转为
char*
类型即可,便可以一次访问4个字节,但是这里尽量不要直接写成(char*)dest++
,因为这里面涉及到【隐式类型转换】,在中间会产生一个临时对象,我们对临时对象去++的话并没有什么意义,所以这里还是规规矩矩地写就行 - 如果不太清楚【隐式类型转换】的话,可以看看C语言操作符章节中的内容,有讲到这一块的知识点
dest = (char*)dest + 1;
src = (char*)src + 1;
💬 看到上面这样一个个拷贝过去太累了,如果我不想拷贝所有的数据,而是只拷贝一半的数据呢?这可以不可以做到
- 这当然是可以的,我们只需要指定拷贝的字节数就可以了,现在数组的大小是40个字节,一般数据的话就是20个字节,那就像下面这样去进行拷贝即可
int main(void)
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
my_memcpy(arr2, arr1, 20);
for (int i = 0; i < 10; ++i)
{
printf("%d ", arr2[i]);
}
return 0;
}
- 可以看到,最后就只拷贝了一半的数据过去
💬 不过我觉得,从一个数组拷贝到另外一个数组太麻烦了,可以直接在自己本身上进行操作吗?
- 这当然也是可以的,比方说现在我想把arr1数组中前面20个字节的数据,即前5个元素【1 2 3 4 5】拷贝到【3 4 5 6 7】这个位置中,那最后的结果是否会是【1 2 1 2 3 4 5 8 9 10】呢
int main(void)
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr1 + 2, arr1, 20);
for (int i = 0; i < 10; ++i)
{
printf("%d ", arr1[i]);
}
return 0;
}
- 通过运行可以看出,似乎并没有拷贝过去,而且数组前面的元素变成了【1 2 1 2 1 2 1】,这是为何呢?
- 我们一起来看一下下面这张图,仔细观察就可以发现,当前两个数拷贝完之后想要去拷贝3的时候,此时我们拿到的还是【1】,当想要去拷贝4的时候,拿到的便是【2】,依次类推,这就是为什么打印出来拷贝位置的结果是【1 2 1 2 1】
💬 那该怎么办呀😲有什么其他办法吗?
- 对与
memcpy()
来说,它只负责拷贝两块独立空间中的数据,但是对于一个数组的元素,它们都是连续存放的,若是擅自去进行拷贝的话会造成覆盖的情况,此时我们可以使用memmove()
这个函数,它可以用来专门拷贝重叠内存的数据
二、memmove()
1. 函数原型
void * memmove ( void * destination, const void * source, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,来移动一下两个重叠的内存块
int main(void)
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr1 + 2, arr1, 20);
for (int i = 0; i < 10; ++i)
{
printf("%d ", arr1[i]);
}
return 0;
}
5. 模拟实现
接下去的话我们就来模拟实现这个memmove()函数
- 对于这个函数的实现来说,比较复杂,要分为三种情况进行讨论
流程图示:
分析:
- 对于数组的空间排布来说,前面是低地址,后面是高地址,如果你自己看下图的话,就可以发现它被分成了三块区域,对于
dest
来说,一个是在src前面,需要从前往后进行拷贝,一个是在src
后面,需要从后往前进行拷贝,还有一个便是两块内存空间不会进行覆盖, 但还是存在与一个连续的空间即数组中,这个时候无论是【从前往后】还是【从后往前】都是可以的,那这样分成三个区域太麻烦了,这里我推荐分成两块区域,通过地址的大小进行比较- 当
dest < src
时,我们从前往后进行逐一字节的拷贝 - 当
dest >= src
时,我们从后往前进行逐一字节的拷贝
- 当
动画图解:
代码展示:
void* my_mommove(void* dest, const void* src, size_t num)
{
assert(dest && src);
char* start = dest;
if (dest < src)
{
//memcpy()的拷贝逻辑
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else //dest >= src
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return start;
}
代码分析:
- 我主要来讲一下从后往前拷的这段逻辑,因为一个整型元素是四个字节,我们这里把它做一个分割就可以看出,若是我们要拷贝20个字节的数据的话,最后的末尾自己便是20,往前一个字节就是需要拷贝的实际数据,以此类推,一个个字节往前数就可以拷贝完所有的数据,不仅是对于整型元素,浮点型元素也是类同
- 那我们要如何去获取到这个19,18,17,16,15个字节呢?很简单,只需要把
dest
和src
强转为char*
类型的地址即可,此时再加上一个【num】便可以偏移到指定的位置处,随着【num】的不断变化,就可以将数据从后往前进行一一拷贝
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
三、memset()
1. 函数原型
void * memset ( void * ptr, int value, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,初始化一下arr这个数组
int main(void)
{
int arr[10];
int sz = sizeof(arr) / sizeof(arr[0]);
memset(arr, 0, sizeof(int) * sz);
return 0;
}
- 再去打印一下这个数组就可以发现确实是可以做到初始化的功能
4. 注意事项
💬 但是可别高兴得太早,若是我现在想要将数组中的数据都初始化成【1】呢,此时还能成功吗?
- 可以看到,似乎数组的每个值并没有初始化成功,而是变成了一个很大的数,这是为什么呢?
- 我们可以通过【内存】的形式去观察一下。此时就可以观察到每一个字节都被初始化成了1,那么4个字节的话其实就不再是1了,而是一个很大的数,回想
memset()
的特性,是以字节为单位去进行一个初始化,那就可以看出问题出在哪里了
💬 所以我们在使用memset()
的时候一定要注意以上这一点
四、memcmp()
接下去就是本文的最后一个内部函数 ——
memcmp()
1. 函数原型
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
2. 函数解读
原文链接
3. 功能演示
解读完函数后,我们来用一用它,比较一下两个内存块中的num个字节
int main(void)
{
int arr1[10] = { 1,2,3,4,5 };
int arr2[10] = { 1,3,2 };
int ret = memcmp(arr1, arr2, 12);
printf("%d\n", ret);
return 0;
}
- 这里可以看到,我比较了两个数组的前12个字节,即数组的前3个元素,然后返回的是-1,这是为何呢?它是如何去进行比较的呢?
- 对于这个函数的返回值来说,和
strcmp()
一样,为< 0、= 0或者> 0的数值
- 那我们现在可以来看一下它们在内存中的样子,对于VS来说是小端存放,因此数组arr1存放在内存中便是
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
- 数组arr2存放在内存中为
01 00 00 00 03 00 00 00 02 00 00 00
- 要知道,
memcmp()
可是一个字节一个字节进行比较,那么此时当他们比较到【02】和【03】的时候就已经不相等了,因为前一个小于后一个,所以便会返回 < 0的数字
- 但此时若我将arr2数组去做一个变化的话,返回的便是 = 0的值
总结与提炼
最后来总结一下本文所学习的内容📖
- 在本文中,我们总共学习了四种函数,分别是字符串函数、字符串查找函数、错误信息报告函数、字符操作函数、内存函数,现在我们再来回顾一下
一、字符串函数
- strlen() —— 可以求解字符串的长度
- 参数指向的字符串必须要以 ‘\0’ 结束
- 函数的返回值为size_t,是无符号的
- 三种模拟实现的方式
- strcpy() —— 可以将一个字符串拷贝到另一个字符串中
- 源字符串必须以
‘\0’
结束,因为源字符串中的 ‘\0’ 会被拷贝到目标空间 - 目标空间必须足够大,以确保能存放源字符串
- 目标空间必须可变
- 源字符串必须以
- strcat() —— 可以拼接两个字符串
- 源字符串必须以
‘\0’
结束 - 目标空间必须有足够的大,能容纳下源字符串的内容
- 目标空间必须可修改
- 不可以给自己做追加
- 源字符串必须以
- strcmp() —— 可以比较两个字符串
- 记住比较的规则👇
- ptr1所指向小于ptr2,返回 < 0的数【VS下是-1】
- ptr1所指向等于ptr2,返回 0
- ptr1所指向大于ptr2,返回 > 0的数【VS下是1】
- 学会模拟实现
- 记住比较的规则👇
- strncpy() —— 可以进行指定字符的拷贝
- 若拷贝的num个数 > 源字符串长度,则拷贝过去后不够的补
\0
- 尝试模拟实现
- 若拷贝的num个数 > 源字符串长度,则拷贝过去后不够的补
- strncat() —— 可以追加n个字符
- 源字符串中有几个就追加几个,追加完后补
\0
, - 若是拷贝的num个数 > 源字符串长度也是一样,不会再多追加
\0
- 源字符串中有几个就追加几个,追加完后补
- strncmp() —— 可以比较n个字符
- 学会使用,了解即可
二、字符串查找函数
- strstr() —— 定位子字符串
- 清楚如何在主串中找到子串,学会模拟实现
- strtok() —— 切割字符串
- 根据给出的切割字符数组,将原字符串进行一个切割划分
- 注意传参的时候字符串只在第一次进行传递,后面几次均传递NULL即可,会从上一次切割之后的位置继续查找
三、错误信息报告函数
- strerror() —— 获取指向错误消息字符串的指针
- 学会使用即可,传递
errno
这个存放全局错误码的变量
- 学会使用即可,传递
四、字符操作函数
- 会使用即可,遇到时进行查询
五、内存操作函数
- memcpy() —— 拷贝内存块
- 可以完成任意数据类型的拷贝工作
- 无法实现自己给自己拷贝,会出现覆盖的问题,只能拷贝两块独立空间中的数据
- 掌握模拟实现
- memmove() —— 移动内存块
- 可以完成重叠内存块的拷贝
- memset() —— 填充内存块
- 可以进行内存设置,以字节为单位初始化
- memcmp() —— 比较内存块
- 可以作用于比较任意类型的数据,但要注意是一个字节一个字节进行比较
以上就是本文要介绍的所有内容,对于这些函数大家可以自行到库中去阅读,如有解读错误敬请指出,感谢您的阅读🌹