前言: 本篇继续介绍处理字符和字符串的库函数的使用和注意事项,让我们更好的使用字符串和字符串库函数,去理解运用一些库函数或自定义函数。
发文时间正值是国庆假期,虽迟但到,祝祖国母亲生日快乐,此生无悔入华夏!
一.字符串查找
1.strstr
前面的strcpy是str+cpy的意思是字符串拷贝,那么strstr是字符串字符串,有什么作用呢?我们看一下:
实际上这是一个查找字符串的函数,就是在一个字符串中查找另一个字符串(子字符串)是否存在的作用。我们可以详细看一下定义:
Returns a pointer to the first occurrence of str2 in str1, or a null,pointer if str2 is not part of str1.(返回一个指针,存储的是str2 在str1 中第一次出现的位置,如果没有找到,则返回空指针。)
查!哪里跑!
代码:
int main()
{
char arr1[] = "hello world,i love world";
char arr2[] = "china";
char* ret = strstr(arr1, arr2);
if (ret == NULL)
{
printf("找不到");
}
else
{
printf("%s", ret);
}
}
当我们运行起来,得到的结果就是world,i love world ,因为我们查到的是第一次出现的地址,所以ret 中存的就是第一次 world 的地址,然后再打印出来。而由于存储的是第一个world 的地址,所以打印出来的时候也是一直到字符串结束的'\0' 才打印结束。
然后我们进行自定义环节:
首先我们来分析一下,自定义函数首先需要的就是先摸清这个函数的定义和有多少种情况,然后再一一实现它,所以我们先分析,大致分为三种可能:
所以我们的构思应该是:
- 结束标志应该是
s1 或s2 其中一个出现'\0' 。所以要记录下s1 和s2 的地址,当其等于'\0' 则返回值。 - 当对比一样后重新不一样,需返回对比第一个相同的下一个开始进行比较,所以需要用到指针
cp 记录下地址。 - 当对比一样的时候,
s1 和s2 两个指针同时++,继续对比。 - 最后还要注意当传过来的就只有
\0 的指针,应该直接返回。
然后我们来写代码:
char* my_strstr(const char*str1, const char* str2)
{
assert(str1 && str2);
char* s1;
char* s2;
char* cp = str1;
if (*str2 == '\0')
return str1;
while (*cp)
{
s1 = cp;
s2 = str2;
while (*s1 && *s2 && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cp;
}
cp++;
}
return NULL;
}
int main()
{
char arr1[] = "abbbcdef";
char arr2[] = "bbc";
char *ret = my_strstr(arr1, arr2);
if (ret == NULL)
{
printf("找不到\n");
}
else
{
printf("%s\n", ret);
}
return 0;
}
2.strtok
strtok 这个函数就有点东西了,上面既然有追加字符串的,那选择我们也有切断字符串的,具体是什么样子呢,我们来看一下:
好家伙,知道为什么说他有点意思了吧,说明都比别人长一半。我们首先还是先看他的参数 char * str , const char * delimiters ,其实这里的意思就是,前面的str 是待宰的羔羊,也就是要切割的字符串。而后面的delimiters 就是切割符,就是当遇到这个符号的时候,就需要切开。
这个函数还有一些细节:
- 第一个参数
str 指定一个字符串,它包含了0个或者多个由delimiters 字符串中一个或者多个分隔符分割的标记。 delimiters 参数是个字符串,定义了用作分隔符的字符集合。可放置多个分割字符,只要遇到放置中有的或者连续的都会切割。strtok 函数找到str 中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok 函数会改变被操作的字符串,所以在使用strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改。)strtok 的返回值是一个指针。strtok 函数的第一个参数不为 NULL ,函数将找到str 中第一个标记,strtok 函数将保存它在字符串中的位置。strtok 函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。- 如果字符串中不存在更多的标记,则返回 NULL 指针。
看定义看的头晕,那还是直接用代码来碰一碰吧:
#include <string.h>
int main()
{
char arr1[] = "gitee.com/happyiucmooo";
char arr2[100] = { 0 };
char sep[] = "/cm.";
strcpy(arr2, arr1);
char* ret = NULL;
for (ret = strtok(arr2, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
return 0;
}
最后得到的结果是:
gitee 由分隔符'.'分割
o 由分隔符'c'和'm'分割
happyiu 由分隔符'\'和"cm"分割
ooo 由分隔符'cm'分割
当第二次调用strtok 函数的时候,我们只需要传参NULL就可以,是因为当第一次分隔的时候,strtok 除了把分隔符变成'\0' 外,还记住了下一个字符的地址。
那strtok 里面创建存储这个值的变量是什么呢?如果是局部变量的话,当strtok 函数运行完就销毁了,根本不可能带入下一次strtok 函数中,所以这个变量应该是静态变量或者全局变量,静态变量和全局变量直到程序彻底结束才销毁或者说是回收。
这就是strtok函数的内容和代码啦。
二.错误信息报告
1.strerror
strerror 是一个错误信息报告函数。当我们在写代码过程中有错误,c语言会返回错误码,比如:
int main()
{
return 0
}
这里我们就可以看见返回的error就是错误码,而后面的解释就是错误码解析出来的信息。每一个错误码表示一个错误信息。而我们的strerror 就是报告错误的一个函数,具体我们看代码:
int main()
{
printf("%s\n", strerror(0));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
return 0;
}
打印结果: No error Operation not permitted No such file or directory No such process
也就是说,strerror 函数就是接收错误信息的错误码,从而读取得到错误信息并接收。而实际上这些错误是C语言库函数调用失败的时候,会把错误码存储到errno 的变量中,errno 是C语言本身内部的一个变量。
比如说下面这个代码尝试:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
printf("打开文件成功\n");
}
return 0;
}
这里运行得到的就是No such file or directory ,因为我没有创建这样的文本文件。所以返回的值就是NULL,最后打印错误信息。
在这里我们又又可以用到另一个函数perror ,实际上可以理解为打印+strerror 的作用。我们用代码来展现:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("测试");
}
else
{
printf("打开文件成功\n");
}
return 0;
}
实际上打印结果:测试: No such file or directory 。所以perror函数就是打印内容+上strerror(errno) 的作用。而这个函数的缺点就是他一定会打印错误信息,而不能得到错误信息不打印。
三. 字符分类函数
字符分类函数有:
函数 | 如果他的参数符合下列条件就返回真 |
---|
iscntrl | 任何控制字符 | isspace | 空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’ | isdigit | 十进制数字 0~9 | isxdigit | 十六进制数字,包括所有十进制数字,小写字母af,大写字母AF | islower | 小写字母a~z | isupper | 大写字母A~Z | salpha | 字母a ~ z或A~Z | isalnum | 字母或者数字,a ~ z,A ~ Z,0~9 | ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) | isgraph | 任何图形字符 | isprint | 任何可打印字符,包括图形字符和空白字符 |
在这里就不一一介绍了,就选第一个说明一下吧,首先说明一下控制字符。
比如说 printf("%d",x) 中的 %d 就是控制字符,它控制输出变量的格式 总之,控制字符就是控制 语句、格式、条件等的字符
然后我们用一段代码来说明:
#include <stdio.h>
#include <ctype.h>
int main ()
{
int i=0;
char str[]="first line \n second line \n";
while (!iscntrl(str[i]))
{
putchar (str[i]);
i++;
}
return 0;
}
打印结果是first line 。在这里,我们知道iscntrl 函数当参数符合条件就返回真,然后我们第一个元素f 不是控制字符,所以返回值为假,在while中加上不等于修饰,就是可以运行打印,i++。然后一直打印,直到\n ,他是一个控制字符,所以返回值为真,然后while中加上不等于修饰,就停止循环,停止打印了。
四.内存操作函数
1.memcpy
既然字符串可以拷贝,那其他的类型呢,整形,结构体呢,其实这里有一个函数就是考虑到这些而诞生的,它就是memcpy :
memcpy就是一个拷贝这些类型的一个函数,而他的参数是void * memcpy ( void * destination, const void * source, size_t num ); 中,第一个目的地址,第二个源地址,第三个拷贝元素个数。在这里源和目的地址都是void类型,是因为我们不清楚要拷贝的是什么类型,而void空类型就是万能类型,可以接受其他的类型。
① 我们来打一段代码看看:
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[10] = { 0 };
memcpy(arr2, arr1, 5 * sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
所以函数memcpy 就是从source的位置开始向后复制num 个字节的数据到destination的内存位置。
自定义memcpy 函数:
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest && src);
while (count--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[5] = { 1,2,3,4,5};
int arr2[10] = { 0 };
my_memcpy(arr2, arr1, 5*sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
② 这时候我们另一个想法,如果自己复制自己会怎么样:
如果source 和destination 有任何的重叠,复制的结果都是未定义的。
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest && src);
while (count--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr1+2, arr1, 4 * sizeof(int));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
我们的代码正常情况下应该就是把1 2 3 4 覆盖掉3 4 5 6 的地方,也就是打印的是1 2 1 2 3 4 7 8 9 10 ,但是我们的打印结果是1 2 1 2 1 2 7 8 9 10 .原因就在于当我们一个一个的拷贝的时候,本来是3覆盖掉5的地方时,3的位置在前面已经被覆盖成1了,所以5 6的位置拷贝3 4 地方的值时,拷贝过来的是已经拷贝了1 2 的3 4位置。
所以这里做的话是有问题的,如果我们想实现这样的移动式的拷贝,我们可以使用另一个函数:memmove 。
PS: 实际上在vs2019 中,使用memcpy 是可以达到这种拷贝效果的,只不过是我们的自定义函数没有达到这种效果。但在C语言库中memcpy 的效果也确实只需要达到这样就可以了,只是在vs2019 中效果变得更好了。
2.memmove
上面的代码我们使用memmove就可以实现我们想要的效果了:
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
memmove(arr1+2, arr1, 16);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
同样我们看一下或者直接对比一下memmove 和memcpy :
memmove 函数顾名思义就是移动字符串的一个函数,其实它和memcpy 有着很多的相同点,都是可以完成拷贝的内容的作用。和memcpy 的差别就是memmove 函数处理的源内存块和目标内存块是可以重叠的。也就是说在拷贝考试中,memcpy 只能考60分,但memmove 能考100分。
而且这两个函数的参数都是一样的:
然后我们尝试一下实现这一个100分函数的自定义,先分析思路:
然后根据分析我们得到:
对于memmove 的自定义,需要分区域讨论:
- 当
dest <src 时,也就是拷贝内容在后,存放区在前的时候,应该从前往后拷贝元素。 - 当
dest =src 的时候,随意。 - 当
dest >src 且不超处拷贝内容的范围时,也就是拷贝内容在前,存放区在后,但两者还有重叠的时候,应该从后往前拷贝。 - 当
dest >src 且两者无重叠范围的时候,随意。
总结下来我们可以选择分为两大区域,以dest =src 为界限,前面的就前往后拷贝元素,后面的从后往前拷贝。
也就是:
void* my_memmove(void* dest, const void* src, size_t count)
{
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
my_memmove(arr1+2, arr1, 16);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
然后我们逐一完善就是:
前面部分:
如果是dest < src 才进入。需要一个一个的拷贝覆盖,这里还是使用char * 类型好,然后我们先将void * 类型强制类型转换为char * ,再将其赋值,赋值完之后往前走一个字符大小,继续赋值,直到count为0才退出。
if (dest < src)
{
while (count--)
{
*(char*)dest = *(char*)src;
dest = *(char*)dest + 1;
src = *(char*)src + 1;
}
}
后面部分:
如果不是前面部分的情况,就是后面部分的情况了。然后对于后面部分的拷贝覆盖,需要从后往前,所以我们要先得到拷贝内容和拷贝到存储的位置的末尾地址,往前覆盖。刚好count 进入时是后置- -,我们在赋值的时候先+上一个count 就是最末端开始赋值了,然后再执行count- - 时,就相当于往前覆盖了。
else
{
while (count--)
{
*((char*)dest + count) = *((char*)src + count);
}
}
最后完整的代码就是:
void* my_memmove(void* dest, const void* src, size_t count)
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (count--)
{
*(char*)dest = *(char*)src;
dest = *(char*)dest + 1;
src = *(char*)src + 1;
}
}
else
{
while (count--)
{
*((char*)dest + count) = *((char*)src + count);
}
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
my_memmove(arr1+2, arr1, 16);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
这就是memmove 的自定义函数的实现了,也是在vs2019底下的memcpy 自定义函数的实现,但memcpy 不是在每一个编译器上都能做出100分的答卷,所以一般还是认为是memmove 函数更胜一筹。
3.memset
既然能移动,那能不能修改呢,答案是可以的,这就要看我们的memset 函数了。memset ,意思就是设置内存。我们来看一下定义:
void * memset ( void * ptr, int value, size_t num );
由memset 的参数我们可以理解,首先是需要修改的地址内容,然后是修改值,最后是需要修改多少。
代码:
int main()
{
int arr[] = { 1,2,3,4,5 };
int i = 0;
printf("改变前:");
for(i=0;i<5;i++)
{
printf("%d ", arr[i]);
}
memset(arr, 0, 20);
printf("\n改变后:");
for (i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
同时我们也可以调试以内存中查看变化,来理解函数作用:
这就是我们的memset 函数。
4.memcmp
接下来是memcmp 函数
int memcmp ( const void * ptr1,
const void * ptr2,
size_t num );
熟悉的意思,熟悉的参数。既然一个一个的类型可以比较,那我们想比较更细的内存时,就用到这一个函数了。是不是又想起来一个函数是strcmp ,它比较只是比较字符串的内容,而且遇到'\0' 就停止了。而我们这里的memcmp 不一样,什么数据都可以比较,反正是一个一个的字节比较。
同样的是它比较的返回值是一样的:
我们来代码尝试一下:
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3,6,7 };
int ret1 = memcmp(arr1, arr2, 12);
int ret2 = memcmp(arr1, arr2, 13);
printf("%d\n", ret1);
printf("%d\n", ret2);
return 0;
}
好啦,本篇的内容就先到这里,关于字符串和内存函数也就到这里了噢,还请继续关注,一起努力学习!。关注一波,互相学习,共同进步。
还有一件事:
|