前言:
本篇继续介绍处理字符和字符串的库函数的使用和注意事项,让我们更好的使用字符串和字符串库函数,去理解运用一些库函数或自定义函数。
发文时间正值是国庆假期,虽迟但到,祝祖国母亲生日快乐,此生无悔入华夏!
前面的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";
//查找arr1在arr2中第一次出现的位置
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;//用于对比的指针指向s1中的字符
char* s2;//用于对比的指针指向s2中的字符
char* cp = str1;//记录地址,为第二种情况准备
if (*str2 == '\0')//如果一开始就是\0,则直接返回
return str1;
while (*cp)//当记录地址读到\0也就是已经对比完整个字符串
{
s1 = cp;//放入首元素地址
s2 = str2;//放入首元素地址
while (*s1 && *s2 && *s1 == *s2)
//当s1和s2都不是\0时,且对比字符相同时
{
s1++;//s1和s2同时++
s2++;
}
if (*s2 == '\0')
//当s2遇到/0,证明在字符串中已经找到完整的子字符串
{
return cp;//返回找到的字符串的首元素地址
}
cp++;
//如果上面布置没有返回出去,证明判断错误
//则就是情况2,记录地址+1然后继续开始比较
}
return NULL;//当上面的代码运行结束,仍未返回,就是找不到,返回空指针
}
int main()
{
char arr1[] = "abbbcdef";
char arr2[] = "bbc";
//查找arr1中arr2第一次出现的位置
char *ret = my_strstr(arr1, arr2);
if (ret == NULL)//返回空指针就是没有找到
{
printf("找不到\n");
}
else
{
printf("%s\n", ret);//找到并打印
}
return 0;
}
strtok
这个函数就有点东西了,上面既然有追加字符串的,那选择我们也有切断字符串的,具体是什么样子呢,我们来看一下:
好家伙,知道为什么说他有点意思了吧,说明都比别人长一半。我们首先还是先看他的参数 char * str
, const char * delimiters
,其实这里的意思就是,前面的str
是待宰的羔羊,也就是要切割的字符串。而后面的delimiters
就是切割符,就是当遇到这个符号的时候,就需要切开。
这个函数还有一些细节:
str
指定一个字符串,它包含了0个或者多个由delimiters
字符串中一个或者多个分隔符分割的标记。delimiters
参数是个字符串,定义了用作分隔符的字符集合。可放置多个分割字符,只要遇到放置中有的或者连续的都会切割。strtok
函数找到str
中的下一个标记,并将其用 \0
结尾,返回一个指向这个标记的指针。(注:strtok
函数会改变被操作的字符串,所以在使用strtok
函数切分的字符串一般都是临时拷贝的内容并且可修改。)strtok
的返回值是一个指针。strtok
函数的第一个参数不为 NULL ,函数将找到str
中第一个标记,strtok
函数将保存它在字符串中的位置。strtok
函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。看定义看的头晕,那还是直接用代码来碰一碰吧:
#include
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))
//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函数的内容和代码啦。
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语言本身内部的一个变量。
比如说下面这个代码尝试:
//strerror - 可以返回C语言内置的错误码对应的错误信息
int main()
{
FILE* pf = fopen("test.txt", "r");
//fopen是打开文件函数,不成功返回值是NULL
//括号内容是以读的形式打开"test.txt"
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("测试");
//打印+strerror
}
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
#include
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中加上不等于修饰,就停止循环,停止打印了。
既然字符串可以拷贝,那其他的类型呢,整形,结构体呢,其实这里有一个函数就是考虑到这些而诞生的,它就是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]);
}
//打印结果:1 2 3 4 5 0 0 0 0 0
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;
//由于是void类型,所以要强制类型转换才能计算,下同
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
中效果变得更好了。
上面的代码我们使用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]);
}
//1 2 1 2 3 4 7 8 9 10
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);
//my_memmove(arr1,arr+2,16);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
这就是memmove
的自定义函数的实现了,也是在vs2019底下的memcpy
自定义函数的实现,但memcpy
不是在每一个编译器上都能做出100分的答卷,所以一般还是认为是memmove
函数更胜一筹。
既然能移动,那能不能修改呢,答案是可以的,这就要看我们的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;
//打印结果:
//改变前:1 2 3 4 5
//改变后:0 0 0 0 0
}
同时我们也可以调试以内存中查看变化,来理解函数作用:
这就是我们的memset
函数。
接下来是memcmp
函数
int memcmp ( const void * ptr1,
const void * ptr2,
size_t num );
熟悉的意思,熟悉的参数。既然一个一个的类型可以比较,那我们想比较更细的内存时,就用到这一个函数了。是不是又想起来一个函数是strcmp
,它比较只是比较字符串的内容,而且遇到'\0'
就停止了。而我们这里的memcmp
不一样,什么数据都可以比较,反正是一个一个的字节比较。
我们来代码尝试一下:
int main()
{
int arr1[] = { 1,2,3,4,5 };
//在内存中:01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ....
int arr2[] = { 1,2,3,6,7 };
//在内存中:01 00 00 00 02 00 00 00 03 00 00 00 06 00 00 00 ....
int ret1 = memcmp(arr1, arr2, 12);
int ret2 = memcmp(arr1, arr2, 13);
printf("%d\n", ret1);//0
printf("%d\n", ret2);//-1
return 0;
}
好啦,本篇的内容就先到这里,关于字符串和内存函数也就到这里了噢,还请继续关注,一起努力学习!。关注一波,互相学习,共同进步。
还有一件事: