一.strstr函数
1.1strstr函数的作用
strstr函数的用途就是从字符串1中寻找字符串2.
1.2strstr函数的格式
char *strstr( const char *string, const char *strCharSet );
参数:两个字符型指针,第一个参数是一个字符串的地址,第二个参数是需要在第一个字符串里寻找的那个字符串的地址
返回值:返回的也是char*类型的指针,这个指针指向的是在第一个字符串找到第二个字符串后的那个位置的地址。比如说第一个字符串“abcdefgdef”,第二个字符串是“def”,返回的就是第一个字符串中指向第一个d的地址。如果在第一个字符串里没有找到第二个字符串则返回NULL。如果第二个字符串是一个长度为0的字符串则直接返回第一个字符串。
头文件:<string.h>
1.3strstr函数的用法
int main()
{
char str[] = "abcdefgefgabcefg";
printf("%s\n", strstr(str, "gef"));
if (strstr(str, "gefabc") == NULL)
{
printf("不存在");
}
printf("%s\n", strstr(str, ""));
return 0;
}
返回的就是第一次发现gef这个字符串时g的位置,再打印的时候就从这个位置开始向后打印,直到遇见\0。如果没有找到第二个字符串则返回NULL。如果第二个字符串什么都没有,就直接返回第一个字符串。
1.4strstr函数的模拟实现
这个函数模拟实现起来会有些麻烦。我们先来分析一下。
假设两个参数分别是:“abbbbcdef”,“bcd”。现在我们要通过自己写的函数从"abbbbcdef"中找到"bcd"。
我们可以先定义两个指针s1,s2分别指向这两个字符串首元素的地址,在定义一个指针p,然后判断是否相等。如果不相等就p++,但如果相等s1,s2都++。大概思路是这样的,我来说明一下原因:
先将str1,s1,p指向第一个字符串的起始位置。str2,s2指向第二个字符串的起始位置。我们先将p的值赋给s1,然后s1,s2指向的元素比较。
如果相等说明p指向的这个位置有可能是我们要找的那个第二个字符串的首元素,此时s1,s2指针分别自加1,找第二个元素,依此循环,知道s2指向了第二个字符串的末尾\0,跳出循环,然后返回指针p。
但是不相等又会怎样呢?,不相等说明p此时指向的元素不是我们要找的那个字符串的首元素,所以我们将p++,让指针p指向下一个元素,把p的值赋给s1,让s1重新与s2的第一个元素比较(所以s2=str2).
相等和不相等有关的都解释完了,我们不可能一次就能找到这个字符串,肯定要把上面说的放在一个循环里,但是循环什么时候结束呢?
首先在循环里面,我们发现如果s1,s2一直自加1,有可能会出现s2指向了第二个字符串的末尾,这时候说明s2已经遍历完了,也说明我们找到了这个字符串。所以我们直接返回此时p的位置(因为我们每次都假设p是我们要找的字符串的首地址)。
循环条件我们可以设置成判断p是否指向了第一个字符串的末尾,因为我们可以想想,如果第一个字符串已经遍历完了都没有找到第二个字符串,这不就说明第一个字符串里没有第二个字符串了吗?此时我们就可以跳出循环,然后返回NULL.
我们来看代码:
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 == *s2) && (*s1 != '\0') && (*s2 != '\0'))
//在这里增加了一个条件(*s1 != '\0') && (*s2 != '\0')
//因为又可能s1,s2在自加1时指向了字符串的末尾
{
s1++;
s2++;
}
p++;
if (*s2 == '\0')
return p;
}
return NULL;
}
二.strtok函数
2.1strtok函数的作用
strtok函数有两个字符串的参数,这个函数作用就是在第一个字符串里找第二个字符串里包含的元素,如果找到一个,就把第一个字符串里的这个元素替换成\0,然后返回第一个字符串的首地址。
2.2strtok函数的格式
char *strtok( char *strToken, const char *strDelimit );
参数:第一个参数是一个字符串首元素的地址。第二个参数,第二个参数也是一个字符串首元素的地址。
返回值:返回第一个字符串首元素的地址。
头文件:<string.h>
2.3strtok函数的用法
看完上面这些你们可能会有点懵,这有什么用?
我们先看看代码:
int main()
{
char str[] = "2499093082@qq.com";
printf("%s\n", strtok(str, "@."));
return 0;
}
我们将第二个字符串里的元素当成分隔符,然后把第一个字符串里的元素当成包含数个分隔符的一个字符串。
strtok函数就是在第一个字符串里找是否有第二个字符串里的元素,然后把它替换成\0。我们只替换第一次遇见分隔符的元素。
我们看到@的位置变成了\0,而’.'的位置没有变化。我们再来看最终打印的结果:
我们这里从第一个字符串的首地址开始打印。但是这就完了吗?当然不是。strtok函数的第一个参数不为空指针时,在找到第一个分隔符并把它改成\0之后,这个函数会记住此时的位置。(我们刚才传的参数是str不是空指针)。但如果第一个参数是空指针会怎么样呢?
我们来看代码:
int main()
{
char str[] = "2499093082@qq.com";
printf("%s\n", strtok(str, "@."));
printf("%s\n", strtok(NULL, "@."));
printf("%s\n", strtok(NULL, "@."));
return 0;
}
再来看结果:
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
但是我们再加一行代码:
int main()
{
char str[] = "2499093082@qq.com";
printf("%s\n", strtok(str, "@."));
printf("%s\n", strtok(NULL, "@."));
printf("%s\n", strtok(NULL, "@."));
printf("%s\n", strtok(NULL, "@."));
return 0;
}
最后一行发现是返回的空指针,所以如果字符串中不存在更多的标记,则返回 NULL 指针。当然我们可以给上面的代码改简单一点:
int main()
{
char str[] = "2499093082@qq.com";
char* str1 = "@.";
char* str2 = NULL;
for (str2 = strtok(str, str1); str2 != NULL; str2 = strtok(NULL, str1))
{
printf("%s\n", str2);
}
return 0;
}
2.4strtok函数的模拟实现
char* my_strtok(char* strToken, const char* strDelimit)
{
assert(strToken && strDelimit)
//strToken指向的第一个元素与strDelimit的第一个元素比较
//相同就改成\0,不同strDelimit++.
//直到strDelimit指到字符串末尾\0。然后strToken++,重新循环上面的步骤
while(*strToken)
{
//第一次循环
while (*strDelimit)
{
if (*strToken == *strDelimit)
{
*strToken = '\0';
return strToken;
}
else
strDelimit++;
}
strToken++;
}
return NULL;
}
这是我们的初步模拟,但是我们仔细看会发现,strtok函数只是在第一次调用时传的是一个字符串的地址,后面几次传的都是NULL空指针。既然后面产的参数都不一样了,strtok函数**又是怎么知道我们要切割的什么函数呢?**这里我们大胆的猜想一下,可能这个函数内部有个static定义的静态量地址保存了我们切割后\0的地址,这样我们第二次调用后就能准确无误的找到了之后的地址。
char* my_strtok(char* strToken, const char* strDelimit)
{
assert(strToken && strDelimit);
static char* ptr = NULL;//用static定义一个指针
if (strToken == NULL)//假设我们这个函数已经调用了一次,此时就可以进入这个判断,将我们上个函数保存的地址传给strToken,后面的操作就和原来一样就行。
strToken = ptr;
while (*strToken)
{
while (*strDelimit)
{
if (*strToken == *strDelimit)
{
*strToken = '\0';
ptr = strToken;//这是一次函数执行完之前先把当前\0的位置保存下来以便后来用
return strToken;
}
else
strDelimit++;
}
strToken++;
}
return NULL;
}
这是我改进之后的代码。
三.strerror函数
3.1strerror函数的作用
strerror函数是将错误码转换成错误信息并返回。
3.2strerror函数的格式
char *strerror( int errnum );
参数:错误码
返回值:错误信息
头文件:<string.h>
3.3strerror函数的用法
可能有些人刚看到错误码,错误信息这些东西可能会听不懂。C语言中的错误码跟我们有时候在网站上有时候看到的404,类似。我们来看代码:
int main()
{
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));
return 0;
}
像0,1,2,3,4这些就是C语言自带的错误码
我们再来看结果:
返回的值就是以字符串形式表示的错误信息。
但是我们平时在写代码的时候,又怎么知道我们出错的代码里他的错误码是多少呢?
这里我们就要用到一个变量errno,这是C语言自带的一个全局变量,我们只需要**包含一个头文件<errno.h>**即可。我们写的错误会自动记录到这个变量里边。我们通过代码来体现:
int main()
{
FILE* pf = fopen("test.txt", "r");
//这行代码就是读一个test.txt的文件,如果我这个电脑里没这个文件就会返回空指针
//既然打不开,肯定就会报错,他就会把这个错误码放到errno里
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
return 0;
}
四.perror函数
perror函数的功能和strerror函数类似
我们直接看代码:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("错误");
}
return 0;
}
我们再看打印结果:
我们发现他也是打印错误信息,但是他高级一点,他直接帮你打印出来了并且,你还可以在前面自定义加上一些话。
你需要添加的字符,这个函数在打印是自动打印出来并在后面加上:和空格。
五.字符分类函数
头文件:<ctype.h>
六.字符转换函数
int tolower ( int c ); //将大写字母转成小写
int toupper ( int c );//将小写字母转成大写
头文件:<ctype.h>
我们来通过代码理解一下:
int main()
{
char arr[] = "HELLOabcdef";
int i = 0;
for (i = 0; i < 5; i++)
//我们将这个数组的前5个都改成小写,后面的改成大写
{
if (isupper(arr[i]) != 0)//判断是否为大写,如果是大写就返回真(就是一个非0的数)
arr[i] = tolower(arr[i]);//如果是大写就换成小写
printf("%c ", arr[i]);//打印出来
}
for (i = 5; i < 11; i++)
//我们将这个数组后面的改成大写
{
if (islower(arr[i]) != 0)//判断是否为小写,如果是小写就返回真
arr[i] = toupper(arr[i]);//如果是大写就换成大写
printf("%c ", arr[i]);//打印出来
}
return 0;
}
七.memcpy函数
7.1memcpy函数的作用
memcpy也是拷贝一个数据,但是和strncpy函数不同。strcpy函数只能拷贝n个字符。memcpy要更高级写,它不止可以拷贝字符串,他还能拷贝整型,浮点型,结构体…
7.2memcpy函数的格式
void *memcpy( void *dest, const void *src, size_t count );
参数:
和strncpy相比,他的参数,返回值都变成了void类型的。**void类型的指针说明他什么样子类型的指针都可以接收**,这就保证了他什么类型的数据都可以拷贝。
size_t count里的count不是代表需要拷贝count个数据,而是代表拷贝count个字节。因为这个函数不知道要拷贝的是什么类型的数据,所以只能通过要拷贝多少字节来完成任务。
返回值:
返回值也是void*类型,返回的是第一个参数的地址
头文件:<string.h>
7.3memcpy函数的用法
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr1[10] = { 0 };
memcpy(arr1, arr, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
这个代码表示,将arr里的20个字节的数据拷贝到arr1中,要知道,我们这个数组是Int型的数组,一个int类型是4个字节,也就是把前5个元素拷贝过去。
我们来看结果:
7.4memcpy函数的模拟实现
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest && src);
while(count--)
{
//我们在这里一个字节一个字节的拷贝过去
//因为dest,src都是void*类型的指针,这个指针虽然什么类型的值都能接收,但是不能直接使用
//既然我们是一个字节一个字节的拷贝,所以我们把这个指针强制类型转换成char*类型在进行解引用
//因为char正好是一个字节
*(char*)dest = *(char*)src;
//((char*)dest)++;
//((char*)src)++;//这样写,可能会有些编译器不能编译
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
好,到了这里我们已经模拟一半了。有些人可能会有人疑问了,这怎么才一半。我们现在通过代码来看看我们写的和原来的还有什么区别:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
memcpy(arr + 2, arr, 20);
my_memcpy(arr1 + 2, arr1, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
在这里我想将1,2,3,4,5拷贝到3,4,5,6,7的位置上,但是效果如何呢?
第一行是原函数,成功实现了我的问题,但是我们自己模拟的却不是很理想,这是为什么呢?
因为我们将第一个数拷贝到第三个数,第二个数拷贝到第四个数时,原来的第三个数,第四个数就已经变了,所以当第三个数拷贝到低五个数时本来是3拷贝过去,其实变成了1.所以最终展现的结果是1,2,1,2,1…
但是要如何解决这种问题呢?其实也很容易想到:我们从后往前打印。我们先将第5个数拷贝到第7个数里,再后来第三个数拷贝到第五个数时就不怕原来第5个数被覆盖了。这样我们就好写代码了:将代码分成两部分,一部分从前向后打印,另一部分从后向前打印。现在我们的任务是什么时候从后向前,什么时候从前向后。
在这两幅图中,我们可以看到memcpy(str1,str2,num);如果str2的地址小于str1的地址就从后往前打印,str2的地址大于str1时则相反。
现在再看看我们改进的代码:
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest && src);
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);
//每次循环count都在自减1
}
}
return ret;
}
我们再来看看结果:
这样就可以和原函数一模一样了。
八.memmove函数
memmove函数和memcpy类似,和memcpy的差别就memmove函数处理的源内存块和目标内存块是可以重叠的,什么意思呢?我们可以看看我们刚才模拟实现memcpy函数时,有两种写法,第一种就是不能实现重叠的内存拷贝,第二种则可以实现。其实我们第二种就是memmove函数的模拟实现
但是为什么memcpy函数也可以有和memmove一样的作用呢?其实memcpy只是在有些编译器里可以这样做,所以,我们如果想实现内存空间重叠时的拷贝,最好还是用memmove这个函数。
九.memcmp函数
9.1memcmp函数的作用
memcmp函数的作用和strncmp函数类似,也是比较,只是memcmp函数能比较的不只是字符串,他什么类型的都能比较,但前提是相同类型,因为比一个整型和一个结构体的大小没什么意义。
9.2memcmp函数的格式
int memcmp( const void *buf1, const void *buf2, size_t count );
参数:
memcmp函数也是一个字节一个字节的比较,总共比较count个字节。
buf1,buf2是需要比较两个元素的首地址
返回值:
buf1>buf2返回一个大于0的数;
buf1<buf2返回一个小于0的数;
buf1=buf2返回一个等于0的数;
头文件:<string.h>
9.3memcmp函数的用法
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,3,4,6,6,7,8,9,10 };
printf("%d\n", memcmp(arr1, arr2, 16));
printf("%d\n", memcmp(arr1, arr2, 20));
return 0;
}
我们比较的是数组里元素的大小,当比较前16个字节,也就是4个元素的时候应该返回0,比较前20个元素时,应该返回一个小于0的数,因为两个数组前4个元素都是一样的,在第五个元素时5<6。我们来看看结果:
9.4memcmp函数的模拟实现
int my_memcmp(const void* buf1, const void* buf2, size_t count)
{
assert(buf1 && buf2);
while (count--)
{
if (*(char*)buf1 == *(char*)buf2)
//先判断是否相等,相等就指针+1,一直到不相等或者count为0
{
buf1 = (char*)buf1 + 1;
buf2 = (char*)buf2 + 1;
}
else
//此时说明有元素不相等,我们只需返回二者相减的值即可
return *(char*)buf1 - *(char*)buf2;
}
//如果代码能运行到这里,说明在循环完之前需要比较的元素全部相等,所以直接返回0
return 0;
}
十.memset函数
10.1memset函数的作用
memset是将一个给定大小的空间内容设置自己希望变成的内容。
10.2memset函数的格式
void *memset( void *dest, int c, size_t count );
参数:
dest指向的是你需要改的那一块空间的起始地址。
count是你需要改多少个字节。
c是你需要将改的这些字节改成什么值。
返回值:返回的是dest,也就是你需要更改的那块空间的起始地址。
头文件:<string.h>
10.3memset函数的用法
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
memset(arr, 0, 12);
int i = 0;
for (i = 0; i < 7; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
这个代码就是说明我们希望将这个数组的前12个字节设置成0.
我们再来看看结果:
我们确实将前12个字节的内容设置成0了。
10.4memset函数的模拟实现
void* my_memset(void* dest, int c, size_t count)
{
assert(dest);
char* ret = dest;
while (count--)
{
*(char*)dest = c;
//将c赋值给每个字节
dest = (char*)dest + 1;
}
return ret;
}
10.5一个小的知识点(小端字节序存储)
我们先看代码:
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
memset(arr, 0, 13);
int i = 0;
for (i = 0; i < 7; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
这里我们是将前13个字节设置成0.但是一个整型是4个自己,如果将第13个字节设置成0,第四个元素有什么变化呢?
我们看结果:
我们发现第四个元素也变成0了。
这是因为我这个编译器他是小端字节序存储,就是说一个数据在内存中存储时,低位存到低地址上,高位存到高地址上。因为数组里的元素是从低地址到高地址开始存储的。
所以里面元素应该是这样存的:
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00…
在这里一个字节用两个数字表示,一个数字代表4个比特位。我们会发现第13个字节是04,所以将04设置成00,第四个元素自然而然变成了0.