写在前面
在头文件 <string.h>中,有着大量的函数方便程序猿🐵来处理字符串,本篇笔记介绍了大多常用的库函数。
点击每个小节的标题都可以跳转到官方文旦对应的链接
为了方便理解,每个库函数不才都自我模拟实现了一遍,方便各位看官理解~~😆😆
目录
[4.1 strstr](#4.1 strstr)
[4.2 strtok](#4.2 strtok)
[5.1 strerror](#5.1 strerror)
[5.2 perror](#5.2 perror)
一、strlen
计算字符串长度
函数的定义:size_t strlen ( const char * str );
- 字符串已经'\0'作为结束标志,strlen函数返回的是在字符串中'\0'前面出现的字符个数(不包含'\0')。
- 参数指向的字符串必须要以'\0'结束。
- 注意函数的返回值为size_t,是无符号的(易错)
- size_t 类型是: typedef unsigned __int64 size_t; 是类型重定义后的 unsigned int
**模拟实现:**my_strlen有三种方法:指针-指针法,循环法,递归法(不创建变量计数器)。这里不才展示循环法。
cpp
size_t my_strlen(const char* arr) {
char* dst = arr;
int num = 0;
while(*dst++) {
num++;
}
return num;
}
int main() {
char arr[] = "12344545";
printf("%d\n", my_strlen(arr));
return 0;
}
- strlen函数是计算字符串的长度,字符串中的结束标志是 '\0' ,那咱们只需要循环计算出数组起始地址到 '\0' 中有多少个字符即可模拟实现出strlen。
- 循环条件:*dst++中的 *dst。的解读 ,根据优先级,**dst是先与解引用操作符*结合后,dst再++。**我们知道循环判断是为真进入循环,为假结束循环。我们字符串结束表示为 '\0' ,即我们读到数组结尾标识符时候,我们的 *dst 的值为:0。在C语言中,0为假,所以直接使用*dst作为判断语句的表达式即可。
- 循环条件:*dst++中的后置++。的解读 ,后置++,是先使用值再++,根据优先级,dst是先与解引用操作符*结合后,dst再++。 当我们*dst当前的值不为空,那咱们dst++后进入循环,循环结束后就可以判断原先*dst的后一位的值,若此时*dst为 '\0' 那么结束循环,在结束循环后,我们num计算的值就是我们数组长度,就算pts加加了也对num的计算没有影响。
二、长度不受限制的字符串函数
2.1、strcpy
字符串拷贝
将源指向的C字符串复制到目标指向的数组中,包括 终止空字符即' \0 '(并在该点停止)。
函数的定义:char* strcpy(char * destination, const char * source );
- 源字符串必须以 '\0' 结束。
- 会将源字符串中的 '\0' 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
**模拟实现:**my_strcpy
cpp
char* my_strcpy(char* dst, const char* src) {
assert(dst && src);
int i = 0;
while (dst[i] = src[i]) {
i++;
}
return dst;
}
int main() {
char arr1[20] = "12344545";
char arr2[] = "abc";
my_strcpy(arr1, arr2);
for(int i = 0; arr1[i] != '\0'; i++)
printf("%c\n", arr1[i]);
return 0;
}
- my_strcpy是字符串拷贝函数,我们使用一个循环函数进行拷贝。
- 解析循环条件:dst[i] = src[i] 。我们知道循环判断是为真进入循环,为假结束循环。在字符串拷贝中,我们需要把src数组j结束标志 '\0' 也一同拷贝过去。
- 若此时src[i]的值不为 '\0' ,那把对应的值 赋值给dst[i]后,循环条件判断dst[i]是否为假,此时为真,进入循环语句,i++。
- 若此时src[i]的值为 '\0' ,那把 '\0' 的值赋值 给 dst[i] 后,循环条件判断dst[i]是否为假,此时为假,结束循环,结束循环后我们也把src包含 '\0' 前的字符也都拷贝到了dst数组中了。
2.2、strcat
字符串追加
将源字符串的副本附加到目标字符串。目标中的终止空字符被源的第一个字符覆盖,并且在目标中 由两者连接形成的新字符串末尾包含一个空字符**。**
函数的定义:char * strcat ( char * destination, const char * source );
- 源字符串必须以 '\0' 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
- 字符串自己给自己追加时,必须保证存储空间足够大。
模拟实现:
cpp
char* my_strcat(char* dst, const char* src) {
assert(dst, src);
char* end = dst;
while (*dst) {
dst++;
}
int i = 0;
while (dst[i] = src[i]) {
i++;
}
return end;
}
int main() {
char arr1[20] = "12344545";
char arr2[] = "abc";
my_strcat(arr1, arr2);
for(int i = 0; arr1[i] != '\0'; i++)
printf("%c\n", arr1[i]);
return 0;
}
- my_strcat是字符串追加函数,其思想结合了my_strlen与my_strcpy。
- 首先找到dst的结尾即 '\0' (my_strlen),在dst的 '\0' 处添加src的字符(my_strcpy)
2.3、strcmp
字符串比较
此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续使用以下成对字符,直到字符不同或达到终止空字符。
函数的定义:int strcmp ( const char * str1, const char * str2 );
- 第一个字符串大于第二个字符串,则返回大于0的数字
- 第一个字符串等于第二个字符串,则返回0
- 第一个字符串小于第二个字符串,则返回小于0的数字
模拟实现:
cpp
int my_strcmp(const char* str1, const char* str2) {
assert(str1 && str2);
int i = 0;
while (str1[i] == str2[i]) {
if (str1[i] == '\0') {
return 0;
}
i++;
}
return str1[i] - str2[i];
}
int main() {
char arr1[20] = "abc";
char arr2[] = "abc";
int n = my_strcmp(arr1, arr2);
if (n == 0) {
printf("数组相等\n");
}
else {
printf("数组不相等\n");
}
return 0;
}
- my_strcmp是字符串比较函数,需要遍历字符串每个元素比较是否相等。
- 字符在内存中是以ASCII码值进行存储的,在内存中其本质还是整形,所以我们直接判断两个字符串相同下标的ASCII码是否相等就可以判断出两个字符串是否相等了。
- 在循环语句中在两个字符串相等的情况下,我们判断一下是否已经是字符串结尾,若是结尾则说明字符串相等。
- 若判断出字符串不相等,我们返回 str1[i] - str2[i] 即可以得出:第一个字符串大于第二个字符串,则返回大于0的数字 或 第一个字符串小于第二个字符串,则返回小于0的数字 的结果。
三、长度受限制的字符串函数
可以根据使用者的需求来规定函数访问范围
3.1strncpy
字符串拷贝
把源字符串的前num个元素拷贝到目标字符串中。
函数的定义:
char * strncpy ( char * destination, const char * source, size_t num );
- 拷贝num个字符从源字符串到目标空间。
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
- 如果源字符串长于num,则不会在目标末尾隐式附加空字符。在这种情况下,目标字符串不应被视为以空结尾的C字符串(这样读取会溢出)。
模拟实现:其本质还是strcpy,只不过多加了只能拷贝num个限定条件。
cpp
char* my_strncpy(char* dst, const char* src, int num) {
assert(dst && src);
int i = 0;
for (i = 0; i < num; i++) {
dst[i] = src[i];
}
return dst;
}
int main() {
char arr1[20] = "12345";
char arr2[] = "aabcd";
my_strncpy(arr1, arr2, 2);
for (int i = 0; arr1[i] != '\0'; i++) {
printf("%c\n", arr1[i]);
}
return 0;
}
3.2strncat
字符串追加
把源字符串的前num个元素追加到目标字符串中
函数的定义:
char * strncat ( char * destination, const char * source, size_t num );
- 将源的第一个数字字符附加到目标,再加上一个终止空字符。
- 如果源代码中字符串的长度小于num,则只复制直到终止空字符的内容。
- 如果源代码中字符串的长度大于num,则复制到对应的num位置后停止复制,并自动在末尾添加 '\0'。
**模拟实现:**其本质还是strcat,只不过多加了只能追加num个限定条件。
cpp
char* my_strncat(char* dst, const char* src, int num) {
assert(dst, src);
char* end = dst;
while (*dst) {
dst++;
}
int i = 0;
for (i = 0; i < num; i++) {
dst[i] = src[i];
}
dst[i] = '\0';
return end;
}
int main() {
char arr1[20] = "12345";
char arr2[] = "aabcd";
my_strncat(arr1, arr2, 1);
for (int i = 0; arr1[i] != '\0'; i++) {
printf("%c\n", arr1[i]);
}
return 0;
}
- 本质还是strcat,但是在上面的for循环中是没有进行 '\0'的赋值的,需要在结束for循环后进行'\0' 的赋值。
- 在for循环中 i++后不满足条件才会结束循环,此时的 dst + i 已经是追加结束后的下一个地址了,此时 dst[i] 赋值'\0' ,即把dst增加了正常的结束符标志。
3.3strncmp
字符串比较
将源字符串的最多num个字符与目标字符串的num个字符进行比较。
函数的定义:
int strncmp ( const char * str1, const char * str2, size_t num );
- 比较到出现另一个字符不一样或者一个字符串结束或者num个字符全部比较完。
模拟实现:其本质还是strcat,只不过多加了只能比较前num个限定条件。
cpp
int my_strncmp(const char* str1, const char* str2,int num) {
assert(str1 && str2);
int i = 0;
//可以把判断语句放进for循环的循环条件中,但是可读性下降
/*for (i = 0; i < num && str1[i] == str2[i]; i++) {
;
}*/
for (i = 0; i < num; i++) {
if (str1[i] != str2[i]) {
break;
}
}
if (i == num) {
return 0;
}
else {
return str1[i] - str2[i];
}
}
int main() {
char arr1[20] = "12345";
char arr2[] = "aabcd";
int n = my_strncmp(arr1, arr2, 1);
if (n == 0) {
printf("数组相等\n");
}
else {
printf("数组不相等\n");
}
return 0;
}
- 本质还是strcmp函数逻辑,区别是循环是否执行了num次。
- 若执行了num次,说明两个字符串相等。
- 若指向次数小于num次,说明两个字符串不相等。
四、字符串查找函数
4.1 strstr
字符串查找
查找源字符串在目标字符串中出现的位置。
函数的定义:
const char * strstr ( const char * str1, const char * str2 );
- 返回一个指向str1中str2第一次出现的指针,如果str2不是str1的一部分,则返回一个空指针。
- 匹配过程不包括终止空字符,但查找到str2中的'\0'后结束strstr函数并返回指针。
模拟实现:这篇笔记使用暴力查找(可以使用KMP算法:【算法】KMP算法)
cpp
char* my_strstr(const char* str1, const char* str2) {
assert(str1 && str2);
char* src2 = NULL;
char* src1 = NULL;
while (*str1++) {
if (*str1 == *str2) {
src1 = str1 + 1;
src2 = str2 + 1;
while (*src1++ == *src2++) {
if (*src2 == '\0') {
return str1;
}
}
}
}
return NULL;
}
int main() {
char arr1[20] = "12222345";
char arr2[] = "2223";
char* n = my_strstr(arr1, arr2, 1);
if (n != NULL) {
printf("找到了:>%s\n", n);
}
else {
printf("找不到!\n");
}
return 0;
}
- 在my_strstr函数中我们核心思想其实也是strcmp ,也是两个字符串比较,只不过在strstr中我们需要返回一个指向str1中str2第一次出现的指针。
- 我们首先使用循环来遍历str1数组,若str1已经是字符串结束标识符,那说明没有找到,结束循环。
- 在每个不是字符串结束标识符时,我们都与str2的内容进行比较,若str1与str2的首元素内容相同时,我们就使用两个临时指针来进行我们后续元素的比较,定义的两个临时指针,是用于避免str1和str2的移动,导致比较失败时候返回不了原比较地址。
- 若比较已经走到了src2为 '\0' 时,说明str2已经结束,说明比较成功。
- 若比较失败,那我们str1继续往后移动,继续比较,直到str1为 '\0' 后返回 NULL。
4.2strtok
字符串分割
通过提供的分割关键字,在字符串中查找到对应的分割关键字后进行字符串的分割
函数的定义:
char * strtok ( char * str, const char * sep );
- sep参数是个字符串,定义了用作分隔符的字符集合
- 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
- strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改 变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
- strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
- strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
五、错误信息报告
5.1strerror
返回错误码,所对应的错误信息。可以获取所以的错误信息。
函数的定义:
char * strerror ( int errnum );
- 解释errnum的值,生成一个字符串,其中包含一条描述错误条件的消息,就像库的函数将其设置为errno一样。
5.2 perror
打印错误消息
与5.1的功能类似,都是打印错误信息,perror可以自定义信息。但是perror只支持库函数内的错误提取,程序猿自己的语法错误是无法获取的。
函数的定义:
void perror ( const char * str );
- 自动接收错误信息并打印
- 将errno的值解释为错误消息,并将其打印到stderr(标准错误输出流,通常是控制台),可以选择在它前面加上str中指定的自定义消息。
- errno是一个整型变量,其值描述了调用库函数时产生的错误条件或诊断信息(C标准库的任何函数都可以为errno设置值,即使在此引用中没有明确指定,即使没有发生错误),有关更多信息,请参阅errno。
- perror产生的错误消息取决于平台。
如果参数str不是空指针,则打印str,后跟冒号(:)和空格。然后,无论str是否为空指针,都会打印生成的错误描述,后跟一个换行符("\n")。 - 应在错误产生后立即调用perror,否则它可能会被调用其他函数覆盖。
六、内存操作函数
6.1memcpy
内存拷贝
此拷贝不考虑连续空间中的内部拷贝问题,strcpy只针对数组中的拷贝,而memcpy支持所有内容的拷贝。
函数的定义:
void * memcpy ( void * destination, const void * source, size_t num );
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到 '\0' 的时候并不会停下来。
- 如果source和destination有任何的重叠,复制的结果都是未定义的。
模拟实现:
cpp
void* my_memcpy(void* dst, void* src, int num) {
assert(dst && src);
void* str1 = dst;
for (int i = 0; i < num; i++) {
(*((char*)dst)) =( *((char*)src));
(char*)dst = ((char*)dst) + 1;
(char*)src = ((char*)src) + 1;
}
return str1;
}
int main() {
int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
int arr2[] = { 9,8,7,6,5,4,3,2,1, '\0'};
my_memcpy(arr1, arr2, sizeof(int) * 5);
for (int i = 0; arr1[i] != '\0'; i++) {
printf("%d ", arr1[i]);
}
return 0;
}
- void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
- 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。
- 理解解释一后,剩下的就是strncpy的思想了。轻松拿捏😎😆
在模拟实现中,我们尝试自己拷贝自己,把arr1中的123456789改变为121234589,即为:memcpy(arr1 + 2, arr, 5);
但是我们得到的结果却为:
上图看到,实际结果为:121212189
我们画图后清晰的看出:
my_memcpy是从前往后排拷贝,在有任何的重叠的情况下,我们往后需要拷贝的结果就不是我们希望的结果。所以在有重叠的情况下我们使用memmove。
ps :在Visual Studio中把memcpy改写成了memmove,即使我们使用的是memcpy在底层调用的memmove。但不是所有的编译器都改写了。
6.2memmove
内存拷贝,为了解决memcpy起点终点有重叠后导致未定义的内容,从而推出了memmove,但是memmove的运行效率比memcpy低。
此拷贝考虑连续空间中的内部拷贝问题,strcpy只针对数组中的拷贝,而memmove支持所有内容的拷贝。
函数的定义:
void * memmove ( void * destination, const void * source, size_t num );
- 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
- 为避免溢出,目标和源参数指向的数组大小应至少为num字节。
模拟实现:
为了实现重叠的情况下我们可以正常得到我们的结果。我们需要分为dst > src的情况与dst =< src的情况。
在dst > src的情况下,我们使用从前往后拷贝就会出现我们意料之外的结果。
当我们使用从后往前拷贝时,我们就可以得到我们想要的结果。
在dst =< src时,我们使用从前向后拷贝也就没有问题啦。😎
cpp
void* my_memmove(void* dst, void* src, int num) {
assert(dst && src);
void* str1 = dst;
if (dst > src) {
(char*)dst = ((char*)dst) + (num - 1);
(char*)src = ((char*)src) + (num - 1);
for (int i = num - 1; i >= 0; i--) {
(*((char*)dst)) = (*((char*)src));
(char*)dst = ((char*)dst) - 1;
(char*)src = ((char*)src) - 1;
}
}
else {
for (int i = 0; i < num; i++) {
(*((char*)dst)) = (*((char*)src));
(char*)dst = ((char*)dst) + 1;
(char*)src = ((char*)src) + 1;
}
}
return str1;
}
//内存操作main:
int main() {
int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
int arr2[] = { 9,8,7,6,5,4,3,2,1, '\0'};
my_memmove(arr1 , arr1 + 2, sizeof(int) * 5);
for (int i = 0; arr1[i] != '\0'; i++) {
printf("%d ", arr1[i]);
}
printf("\n\n");
return 0;
}
- 核心思想与memcpy函数相同,不过只增加了分类dst > src的情况与dst =< src的情况 。
- void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
- 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。
6.3memcmp
内存比较
功能与strcmp相似,strcmp只支持字符串比较,mencmp支持所有内容比较(需相同类型)
函数的定义:
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
- 比较从ptr1和ptr2指针开始的num个字节
返回值如下:
模拟实现:
cpp
int my_memcmp(const void* ptr1, const void* ptr2, int num) {
assert(ptr1 && ptr2);
int i = 0;
for (i = 0; i < num; i++) {
if ((*(char*)ptr1) != (*(char*)ptr2)) {
break;
}
(char*)ptr1 = ((char*)ptr1) + 1;
(char*)ptr2 = ((char*)ptr2) + 1;
}
if (i == num) {
return 0;
}
else {
return (*(char*)ptr1) - (*(char*)ptr2);
}
}
//内存操作main:
int main() {
int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
int arr2[] = { 1,8,7,6,5,4,3,2,1, '\0'};
int n = my_memcmp(arr1 , arr2, sizeof(int) * 2);
if (n == 0) {
printf("数组相等\n");
}
else {
printf("数组不相等\n");
}
return 0;
}
- 核心思想与strncmp相同。只需留言void*问题即可。
- void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
- 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。
七、字符分类函数
上图都为C语言中的字符分类函数用法相对简单,不才这里不一一举栗了。
以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 若有帮助不要吝啬点赞哟~~💖💖
ps:表情包来自网络,侵删🌹
若是看了这篇笔记彻底拿捏<string.h>头文件的就可以在评论区打出:小小<string.h>!拿捏!😎