字符函数
C语言中对字符和字符串的处理特别频繁,但C语言本身没有字符串类型,字符串通常放在常量字符串或字符数组中。字符串以“\0”作为结束标志。
求字符串长度
strlen
int main()
{
int len = strlen("abcdef");
printf("%d\n", len);
char arr[] = { 'a','b','c','d','e','f'}; //错误写法
len = strlen(arr);
printf("%d\n", len);
return 0;
}
6
42
::: tip strlen求字符串长度时,将字符串每一个字符和“\0”比较,如果不相等,计数器++,直到找到"\0",计数停止。要求字符串中必须有“\0”。 ::: 模拟实现strlen的三种方法:
- 计数器法
- 递归法
- 指针-指针
#include <assert.h> int my_strlen(const char* str) //使用指针接收,函数只求长度不改字符串, { assert(str); int count = 0; while (*str != '\0') //whilei循环条件也可以写成*str。如果*str不是0,条件为真,进入循环 { count++; str++; } return count; } int main() { int len = my_strlen("abcdef"); //实际传到自定义函数的是字符“a”的地址 printf("%d\n", len); return 0; }
注意
int main() { int len = strlen("abcdef"); if (strlen("abc") - strlen("abcdef") > 0) printf("A"); else printf("B"); return 0; }
库函数strlen的返回类型是size_t,上述代码的返回类型是int。 ::: warningB
系统把无符号整型重命名为size_t,即size_t == unsigned int。 ::: 上述代码中无符号的3减去无符号的6,结果-3当成无符号数将补码存在内存中,是一个大于0的数字。
长度不受限制的字符串函数
字符串的长度不受限制,以“\0”作为结束标志。 存在的问题是在拷贝追加的过程中,不考虑目的字符串的大小,可能会发生上限越界。
strcpy-字符串拷贝
::: warning 源字符串不能是char arr[] = {'a','b','c'}的形式。原因是这种写法字符串内没有“\0”。 目的字符串不能是char * arr = "abc"的形式。原因是这种写法中arr指向的是常量字符串,不能被修改。 ::: strcmp对字符串进行拷贝后,希望返回目的空间的起始位置,类型是char。 *如上图,strcmp在字符串拷贝的时候,将源字符串的全部内容包括“\0”都拷贝过去,目标字符串中原有的内容除了被替换的字符外其他内容不变。对字符串的操作是遇到“\0”停止。**
strcpy模拟实现
#include <assert.h>
char* my_strcmp(char* dest, const char* src)
{
//先保证指针有效性
assert(dest);
assert(src != NULL);
char* ret = dest;
//保存目的空间的起始位置,后面dest会改变
//while判断的第一种写法
while (*src)
{
//拷贝“world”
*dest = *src;
dest++;
src++;
}*dest = *src; //拷贝“\0”
//while判断的第二种写法
while (*src)
{
*dest++ = *src++; //拷贝“world”
//dest++;
//src++;
}*dest = *src; //拷贝“\0”
//while判断的第三种写法
while (*dest++ = *src++)
{
;
}
return ret; //返回目的空间的起始位置
}
int main()
{
char arr1[] = "hello world";
char arr2[] = "world";
strcpy(arr1, arr2);
my_strcmp(arr1, arr2);
printf("%s\n",arr1);
return 0;
}
::: tip 上述代码第行while ( * dest++ = * src++)代码分析: 当 * src的值“w”赋给 * dest时,表达式 * dest++ = * src++ 的结果是“w”,不等于0,进入循环, * src和 * dest后置++后将下一个字符“o”赋值给 * dest,表达式的结果变为“o”,进入循环。代码在产生赋值的同时做了判断,又执行了自增,循环直到 * src将“\0”拷贝给 * dest,替换了 * dest的“ ”,赋值导致表达式的结果变为“\0”,“\0”的ASCII值是0,表达式为假,循环停止。此时数组arr2中的所有内容都被拷贝过去。 :::
strcat-字符串追加
上图中表示字符串拷贝时是从目的字符串的第一个“\0”处开始向后追加(覆盖后面的“\0”),源字符串的“\0”也拷贝到目的字符串中。 即目的字符串中的“\0”作用是提示源字符串追加时的起始位置,源字符串中的“\0”的作用是提示追加时停止的位置。 字符数组后面空余的空间全部填充“\0”。 ::: warning 目的字符串不能是char arr1[] = "hello "的写法。原因是不指定数组大小时,数组根据初始化来确定大小。拷贝时应确保目的数组空间足够大。 源字符串中必须有“\0”,数组拷贝时以“\0”作为拷贝结束的标志。 :::
strcat模拟实现
#include <assert.h>
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
//遍历找到目的字符串的“\0”
while (*dest != '\0')
{
dest++;
}
//追加源字符串(相当于从目的字符串的“\0”处开始拷贝源字符串)
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[15] = "hello ";
char arr2[] = "world";
my_strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
strcmp-字符串比较
当常量字符串abc作为一个表达式存在的时候,产生的值不是字符串,而是字符串首字符a的地址。所以字符串比较时不能直接使用比较运算符。 strcmp比较的是对应字符的ASCII值,字符从前向后比较,小的字符对应的字符串小于另外一个字符串,对应字符相等时比较下一个对应字符。同时遇到“\0”时比较结束,输出0。
strcmp模拟实现
#include <assert.h>
int my_strcmp(const char* str1, const char* str2)
{
assert(str1, str2);
//解法1:
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
if (*str1 > *str2)
{
return 1;
}
else
{
return -1;
}
}
//解法2:
// do
// {
// if (*str1 > *str2)
// {
// return 1;
// }
// else if (*str1 < *str2)
// {
// return -1;
// }
// else
// {
// return 0;
// }
// str1++;
// str2++;
// } while (*str1 != '\0');
//}
int main()
{
char* p1 = "abcdef";
char* p2 = "abcqwe";
int ret = my_strcmp(p1, p2);
if (ret == 0)
{
printf("相等");
}
else if (ret > 0)
{
printf("大于");
}
else
{
printf("小于");
}
return 0;
}
长度受限制的字符串函数
strncpy-拷贝n个字符
strncpy严格按照n的值执行复制: strncpy拷贝时,如果n大于源字符串的长度,源字符串拷贝过去后补0到n:
strncpy模拟实现
#include <string.h>
#include <assert.h>
void my_strncpy(char* str1, char* str2, int num)
{
assert(str1, str2);
if (num < strlen(str2))
{
while (num != 0)
{
*str1++ = *str2++;
num--;
}
}
else if (num > strlen(str2))
{
while (*str2 != '\0')
{
*str1++ = *str2++;
}
int i = num - strlen(str2);
while (i != 0)
{
*str1++ = '\0';
i--;
}
}
}
int main()
{
char arr1[20] = "abcdejjnmgknfukygk";
char arr2[] = "hello";
my_strncpy(arr1, arr2, 8);
printf("%s\n", arr1);
return 0;
}
(技术不精代码写的自己都觉得垃圾...有大佬可以给个思路就更好了)
strncat-追加n个字符
strncat严格按照n的值进行追加,如果追加后没有“\0”,strncat希望追加后的内容也是字符串,所以strncat会在追加后再添加一个“\0”: strncat追加时,如果n大于源字符串的长度,只追加源字符串内的内容,多出来的长度不处理:
strncmp-比较n个字符
#include <string.h>
int main()
{
const char arr1[] = "abcdef";
const char arr2[] = "abcqwer";
//int ret = strcmp(arr1, arr2); //-1
//int ret = strncmp(arr1, arr2,3); //0
int ret = strncmp(arr1, arr2, 10); //-1
printf("%d\n", ret);
return 0;
}
如上代码,strncmp比较到“\0”停止或者比较num个元素后停止。
字符串查找
strstr-查找字符串
文档中NULL表示空指针,NUL和Null表示“\0”。
#include <string.h>
int main()
{
char* p1 = "abcdefgh";
char* p2 = "def";
char* ret = strstr(p1, p2);
//在str1中查找str2是否存在,如果存在,返回找到的首字符地址,如果不存在,返回0
if (ret == NULL)
{
printf("不存在\n");
}
else
{
printf("%s\n", ret); //defgh
//ret是字符串名,存储的是找到的字符串的首字符地址,打印ret得到字符串(“\0”为止)
}
return 0;
}
假设目标字符串中包含多个要查找的字符串,strstr函数找到的是第一个查找到的字符串的地址。如下:是从第一个查找到的字符串的地址向后打印直到“\0”。
#include <string.h>
int main()
{
char* p1 = "abcdefabcdef";
char* p2 = "def";
char* ret = strstr(p1, p2);
printf("%s\n", ret); //defabcdef-
return 0;
}
strstr模拟实现
::: tip 代码思路:要查找的字符串首字符p2和目标字符串的首字符p1比较,如果不相等,p2不变,p1++,向后移动一个字符继续比较。如果相等,p1和p2同时++,比较两个字符串的下一位是否相等。 ::: ::: tip 内层while循环结束标志是:
- *p1=='\0'(目标字符串全部查找没有找到)
- *p2=='\0'(要查找的字符串全部查找已经找到)
- *p!= * p2。(此时还没有找到,如abcmef中查找bcd,b和c查找到,m不等于d,没有找到) :::
- p1和p2的值不能任意改变,原因是如果要查找的字符串是bbc,目标字符串是abbbcde,当对比到字符串c不相等时,需要被查找字符串回到起始位置b,目标字符串回到第一次 * p== * p2时p1的下一位,即第三个字符b处,继续下一轮查找(bbb匹配失败,从下一位开始匹配bbc)。需要使用一个变量cur记录目标字符串中此时的位置。*
#include <string.h> #include <assert.h> char* my_strstr(const char* p1, const char* p2) //返回的是空指针或有效地址 { assert(p1, p2); //保证p1和p2的有效性 char* s1 = p1; char* s2 = p2; char* cur = p1; //用来记录第一次*p1==*p2时的位置 if (*p2 == '\0') { return p1; } while (*cur) //为假则目标字符串匹配结束,匹配失败,返回空指针 { s1 = cur; //目标字符串回到第一次* p == *p2时p1的下一位 s2 = p2; //s2回归,重新查找 while ((*s1 != '\0') && (*s2 != '\0') && (*s1 == *s2)) //主要的循环判断程序 { s1++; s2++; } if (*s2 == '\0') //为真则要查找的字符串匹配结束,匹配成功 { return cur; //cur此时为p1内匹配成功时的位置,即字符串地址 } cur++; } return NULL; }
int main() { char* p1 = "abbbcdef"; char* p2 = "bbc"; char* ret = my_strstr(p1, p2); if (ret == NULL) { printf("不存在\n"); } else { printf("%s\n", ret); } return 0; }
**上述代码中,p2可以是空字符串,p1不能是空字符串。**
::: warning
**在函数实现中,当p2是空字符串时,将空字符串的首字符的地址传进函数中。(空字符串中只有一个字符是结尾的“\0”)。返回p1的首字符地址。**
:::
::: tip
**KMP算法是strstr算法的进阶形式。**
:::
------------------------------------
##### strtok-字符串分隔
strtok函数使用场景:某些字符串是由一个或多个字符分隔成多个段,需要拿出各个段或某个段时,使用strtok函数操作。如IP地址,邮箱等。
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/4beceeec99120cd24ce2e0995bb8a7b7.png)
**strtok切割的字符串是原字符串的临时拷贝。**
::: tip
**strtok函数第一次调用时,str是字符串地址(首字符地址),找到str中的分隔符后,会将分隔符改为“\0”,然后返回str的地址,即返回str中第一段的内容。
再次调用strtok函数时,str置为空指针NULL,函数会从刚才“\0”的位置的后一位开始继续向后找,找到下一个分隔符时,将其改为“\0”,返回此次查找起始位置的地址。
以此向后,直到找到str末尾的“\0”,返回最后一次查找时起始的位置。函数再继续向后查找,str为空字符串,找不到分隔符,返回NULL。**
:::
#include <string.h> int main() { char arr[] = "https://www.helloworld.net/80662724"; char* p = ":/."; char buf[100] = { 0 }; strcpy(buf, arr); char* ret = NULL; for (ret = strtok(arr, p); ret != NULL; ret = strtok(NULL, p)) { printf("%s\n", ret); } return 0; }
https www helloworld net 80662724
::: danger
很少用警告...大写加粗来一个
**代码运行时候能消除warning时候尽量消除,如果没有消除,代码执行异常,一定要先去把warning搞掉。**
上面这个代码我没引头文件<string.h>,vs报warning,按照惯例忽略警告,F5执行成功不输出结果,F11运行到一半报错,还考虑了是不是NULL没有定义,最后才发现是头文件的锅...**吃了经验主义的亏**。
:::
**上述代码第9行strtok可以传入字符串也可以传入NULL空指针,在后续调用时strtok可以从上次调用时结束的位置后再输出内容,可以推测strtok内有一个静态变量,第一次strtok调用完毕后不销毁。**
------------------------------------
#### 错误信息报告
strerror-错误报告函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/2870f26504ec790fc6be2e3072bbf197.png)
**strerror的作用是翻译错误信息。**
#include <string.h> int main() { char* str1 = strerror(0); printf("%s\n", str1); //No error char* str2 = strerror(1); printf("%s\n", str2); //Operation not permitted char* str3 = strerror(2); printf("%s\n", str3); //No such file or directory char* str4 = strerror(3); printf("%s\n", str4); //No such process return 0; }
上述代码中strerror中的数字是自己放进去的,真实在代码中出现的是如下情况:
#include <string.h> #include <errno.h> int main() { char* str = strerror(errno); printf("%s\n", str); //No error return 0; }
::: tip
**errno是全局的错误码变量,当C语言的库函数在执行过程中发生错误,就会把对应的错误码赋值给errno变量中。所以只需要检测errno中错误码的值,就可以输出错误信息锁定错误原因。
errno的值由库函数维护。**
:::
使用场景如下:
#include <string.h> #include <errno.h> int main() { FILE* p = fopen("测试", "r"); //表示打开“测试”文件并执行“读”操作并将地址赋给p,fopen函数调用失败会返回空指针 if (p == NULL) { printf("%s\n", strerror(errno)); } else { printf("open file success"); } return 0; }
No such file or directory
上述代码的所在路径下没有名为“测试”的文件,输出结果如上。
在代码路径下新建了名为“测试”的文件后再次执行命令,输出结果如下:
Permission denied
------------------------------------
### 字符操作
#### 字符分类函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/f36de9d902e9aed1e933d870672a4b9d.png)
::: tip
**字符分类函数中传入的是小写字母,返回非0值,不是小写字母则返回0。**
:::
#include <ctype.h> int main() { printf("%d\n", islower('a')); //2 printf("%d\n", isspace('0')); //0 return 0; }
------------------------------------
#### 字符转换函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/b0dbe54924a2fdaf490c1c123a9b6380.png)
**小写转大写时,ASCII值-32;反之+32。**
#include <ctype.h> int main() { printf("%c\n", tolower('A')); //a printf("%c\n", toupper('a')); //A return 0; }
大写转小写:
int main() { char arr[] = "I Love Yaner"; int i = 0; while (arr[i] != '\0') { if (isupper(arr[i])) { arr[i] = tolower(arr[i]); } i++; } printf("%s\n", arr); return 0; }
i love yaner
------------------------------------
### 内存操作函数
**strcpy、strcat、strcmp、strncpy、strncat、strncmp函数的操作对象是字符串,操作时遇到“\0”停止,不能用来操作整型、浮点型、结构体等其他类型。**
#### memcpy-内存拷贝函数
**memcpy可以操作任意类型的数据,使用无类型指针void * ,size_t类型的num用来标明拷贝的字节数。**
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/b6c74fd0811dea9750bd6a2b449440b1.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/2690587bb8d8f12c9e69157c35b0e8fd.png)
##### my_memcpy函数实现
**代码思路:void * 类型的数据没有类型,不能进行运算,也不能解引用。已知num的大小,即src的字节数,可以逐字节的拷贝,将dest和src强制类型转换成(char * )类型,每次拷贝一个字节,满足题目需要。**
#include <string.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest); assert(src); int i = 0; for (i = 0; i <= num; i++) { (char)dest = (char)src; ++(char)dest; ++(char)src; }
}
struct S { char name[4]; int age; }; int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[5] = { 0 }; my_memcpy(arr2, arr1, sizeof(arr1)); struct S arr3[] = { {"张三",22} ,{"李四",20} ,{"王五",18}}; struct S arr4[2] = { 0 }; my_memcpy(arr4, arr3, sizeof(arr3)); return 0; }
::: tip
**上述代码中自定义函数my_memcpy中的for循环可以改成while(num--)。**
:::
::: warning
**上述函数实现过程中应该返回dest的地址,可以在自定义函数中先把dest暂存在void*ret中,函数最后再return ret。**
:::
当拷贝的源空间和目的空间有交集时,memcpy仍可以正确输出结果:
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; memcpy(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }
1 2 1 2 3 4 5 8 9
**但是根据C语言标准,有内存空间重叠的拷贝,使用memmove函数。**
------------------------------------
#### memmove内存移动函数
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/4ec62eb012ad27672470e2b6d7dd5a02.png)
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; memmove(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }
1 2 1 2 3 4 5 8 9
::: tip
在同一个内存空间内进行有内存重叠的拷贝操作时,为了避免需要拷贝的数据被覆盖,需要考虑拷贝的顺序。
:::
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/cb6ad8709e6d5c53d0246c9151fba8a4.png)
下图是123拷贝到345时,3->5的逐字节拷贝示意:
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/6629b37cd4620162521f26fe6b9bea83.png)
如上,拷贝时需要找到最后一个字节,即(char*)src+num-1和(char*)dest+num-1。
::: tip
**当dest起始位置在src起始位置的左边时,选择从前向后拷贝;当dest起始位置在src起始位置的右边,src结束位置的左边时,选择从后先前拷贝。**
**当dest的结束位置在src的左边,和dest起始位置在src结束位置的右边时,内存空间无重叠,无需考虑拷贝顺序。**
:::
#include <string.h> #include <assert.h> void* my_memmove(void* dest, const void* src, size_t num) { assert(dest); assert(src); if (dest < src) { int i = 0; for (i = 0; i <= num; i++) { (char)dest = (char)src; ++(char)dest; ++(char)src; } } else { while (num--) { ((char)dest + num) = ((char)src + num); } } }
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; my_memmove(arr + 2, arr, 5 * 4); //将arr中的12345拷贝到34567的位置 int i = 0; for (i = 0; i < 9; i++) { printf("%d ", arr[i]); } return 0; }
::: warning
**上述函数实现过程中应该返回dest的地址,可以在自定义函数中先把dest暂存在void*ret中,函数最后再return ret。**
:::
------------------------------------
#### memset-内存设置
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/3ea3042f04b72e3fd2b86fb649c21f0d.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/82dc904d9614e22cfff5ef172fd69fe1.png)
**memset中的参数num单位是字节,操作非char类型的内存空间时需要注意:**
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/29954fbf4368de968f6919a95636b0f2.png)
#### memcmp-内存比较
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/30b8b3902236de7e06e8459c29107ef8.png)
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/9e55c7fd509e1f004feb3637f17849ad.png)
int main() { int arr1[] = { 1,2,3,4,5,6,7,8,9 }; int arr2[] = { 1,2,6,5,4,8,7,3,9 }; int ret = memcmp(arr1, arr2, 8); printf("%d\n", ret); //0 return 0; }
上述代码比较内存中前8个字节,完全相等。当比较前9个字节时,返回值为-1,如下:
![image](https://img-hello-world.oss-cn-beijing.aliyuncs.com/imgs/ebe4310435fc0ce198bf62f15ab36211.png)