简单不先于复杂,而是在复杂之后。
目录
0. 前言
C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。
字符串常量适用于那些对它不做修改的字符串函数。
1. 函数介绍
1.1 strlen
1.1.1 介绍
计算字符串长度。
- 字符串以 '\0' 为结束标志, strlen 函数返回的是在字符串中 '\0' 前面的字符个数(不包含 \0)。
- 参数指向的字符串必须要以 \0 结束。
- 注意函数的返回类型是 size_t 是无符号的。
- 学会 strlen 的模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { //char arr[] = "abcdef"; //a b c d e f \0 char arr[] = { 'w','o','w' }; //[][][][][][][][][w][o][w][][][][][][][][] int len = strlen(arr);//随机值 printf("%d\n", len); return 0; }
1.1.2 strlen 函数模拟实现
1.1.2.1 计数器方法
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> size_t my_strlen(const char* str) { assert(*str); size_t count = 0; while (*str != '\0') { str++; count++; } return count; } int main() { char arr[] = "abcdef"; size_t n = my_strlen(arr); printf("%d\n", n); return 0; }
1.1.2.2 递归方法
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> size_t my_strlen(const char* str) { if (*str == '\0') { return 0; } else { return 1 + my_strlen(str + 1); } } int main() { char arr[] = "abcdef"; size_t n = my_strlen(arr); printf("%d\n", n); return 0; }
1.1.2.3 指针 - 指针方法
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> size_t my_strlen(const char* str) { assert(*str); char* start = str; while (*str != '\0') { str++; } return str - start; } int main() { char arr[] = "abcdef"; size_t n = my_strlen(arr); printf("%d\n", n); return 0; }
1.2 strcpy
1.2.1 介绍
char* strcpy(char* destination, const char* source);
字符串拷贝。
- Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).
- 源字符串必须以 '\0' 结束
- 会将源字符串中的 '\0' 拷贝到目标空间
- 目标空间必须足够大,以确保能存放源字符串
- 目标空间必须可变
- 学会模拟实现
以上为正确写法。
因为目标地址指向一个常量字符串,这个区域不可修改,所以会引发异常,故要求目标空间必须可变。
1.2.2 strcpy 函数模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> //strcpy 返回目标空间的起始地址 char* my_strcpy(char* dest, const char *src) { //实现字符串拷贝只有需要解引用操作才能找到字符串内容 //故两个指针不可为空指针,要对其断言 assert(dest); assert(src); char* ret = dest; while (*src) { *dest++ = *src++; } *dest = *src; return ret; } int main() { char arr1[] = { "abcdef" }; char arr2[20] = { 0 }; my_strcpy(arr2, arr1); printf("%s\n", arr2); return 0; }
以上代码不够简练,可以优化:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> //strcpy 返回目标空间的起始地址 char* my_strcpy(char* dest, const char* src) { //实现字符串拷贝只有需要解引用操作才能找到字符串内容 //故两个指针不可为空指针,要对其断言 assert(dest && src); char* ret = dest; while (*dest++ = *src++) { ; } return ret; } int main() { char arr1[] = { "abcdef" }; char arr2[20] = { 0 }; my_strcpy(arr2, arr1); printf("%s\n", arr2); return 0; }
这样的话 *dest++ = *src++ 这个语句就做到了既赋值又判断。
1.3 strcat
1.3.1 介绍
字符串追加。
- Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.
- 源字符串必须以’\0‘结束。
- 目标空间必须足够大,能容纳下源字符串的内容.
- 目标空间必须可修改。
- 字符串自己给自己追加,如何?
#define _CRT_SECURE_NO_WARNINGS 1 #include<string.h> #include<stdio.h> //字符串追加 int main() { char arr1[20] = "hello"; strcat(arr1, "world"); printf("%s\n", arr1); return 0; }
1.3.2 strcat 函数模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> //字符串追加 char* my_strcat(char* dest, const char* src) { assert(dest && src); char* ret = *dest; while (*dest != '\0') { dest++; } while (*dest++ = *src++) { ; } return ret; } int main() { char arr1[20] = "hello "; my_strcat(arr1, "world"); printf("%s\n", arr1); return 0; }
如果自己给自己追加,程序会崩溃。
因为当把源字符串的字符逐个拷贝到目标字符串后面的时候,直到该拷贝‘\0’的时候,源字符串的‘\0‘被第一个拷贝过来的字符覆盖掉了,会陷入死循环。
所以我们要尽量避免字符串自己给自己追加。
1.4 strcmp
1.4.1 介绍
- 比较两个字符串是否相等。
- This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.
- 标准规定:
1. 第一个字符串大于第二个字符串,则返回大于0的数字
2. 第一个字符串等于第二个字符串,则返回0
3. 第一个字符串小于第二个字符串,则返回小于0的数字
这种写法是在比较两个数组首元素的地址。
两个字符串比较是否相等,应该使用 strcmp
一对字符在比较的时候比较的是 ASCII 码值。
1.4.2 strcmp 函数模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> int my_strcmp(const char* str1,const char* str2) { assert(str1 && str2); while (*str1 == *str2) { if (*str1 == '\0') return 0; str1++; str2++; } if (*str1 > *str2) return 1; else return -1; } int main() { char arr1[20] = "abc"; char arr2[20] = "abc"; //比较一下两个字符串是否相等 int ret = my_strcmp(arr1, arr2); if (ret < 0) printf("<\0"); else if (ret == 0) printf("==\0"); else printf(">\0"); //arr1 和 arr2 是数组名,是数组首元素的地址,必然不相等 //if (arr1 == arr2) //{ // printf("==\0"); //} //else //{ // printf("!=\0"); //} return 0; }
函数还可以简化:
int my_strcmp(const char* str1,const char* str2) { assert(str1 && str2); while (*str1 == *str2) { if (*str1 == '\0') return 0; str1++; str2++; } return (*str1 > *str2); }
当目标空间不够时,会产生越界访问导致程序崩溃,但是函数仍然可以将大小超出目标空间的字符串拷贝。
这是潜在的安全隐患。
1.5 strncpy
1.5.1 函数介绍
- Copies the first num characters of source to destination. If the end of the source C string(which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.
- 拷贝num个字符从源字符串到目标空间。
- 如果源字符串小于num,在拷贝完源字符串时,在目标后面追加0,直到num个。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char arr1[] = "abcdef"; char arr2[] = "hello world"; strncpy(arr1, arr2, 5); printf("%s\n", arr1); return 0; }
因为在源字符串后面追加了0,也就是\0, 所以在源字符串小于num时,数组里存放的虽然追加两个0,后面有字符‘f’,但是打印字符串时到第一个‘\0’ 就已经终止了,只能打印源字符串的内容。
1.6 strncat
1.6.1 函数介绍
char * strncat ( char * destination, const char * source, size_t num );
- Appends the first num characters of source to destination, plus a terminating null-character.
- If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char arr1[20] = "hello\0xxxxxxxx "; char arr2[] = "world"; strncat(arr1, arr2, 3); return 0; }
如果 num 大于源字符串,也不会像 strncpy 一样多追加几个 \0 。
1.7 strncmp
1.7.1 函数介绍
int strncmp ( const char * str1, const char * str2, size_t num );
- 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
#define _CRT_SECURE_NO_WARNINGS 1 #include<string.h> #include<stdio.h> int main() { char arr1[] = "abcdef"; char arr2[] = "abc"; int ret = strncmp(arr1, arr2, 3); if (ret == 0) printf("=="); else if (ret < 0) printf("<"); else printf(">"); return 0; }
我们以后在使用字符串函数的时候尽量使用带n的版本,更严谨一些。
1.8 strstr
1.8.1 函数介绍
char * strstr ( const char *str1, const char * str2);
- Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.
- 查找子串(str2 是子串)
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char email[] = "lxz@code.com"; char substr[] = "code"; char* ret = strstr(email, substr); if (ret == NULL) { printf("子串不存在"); } else { printf("%s\n", ret); } return 0; }
1.8.2 strstr 函数模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> 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 != '\0' && s2 != '\0' && *s1 == *s2) { s1++; s2++; } if (*s2 == '\0') { return (char*)p; } p++; } return NULL; } int main() { char email[] = "lxz@code.com"; char substr[] = "code"; char* ret = my_strstr(email, substr); if (ret == NULL) { printf("子串不存在"); } else { printf("%s\n", ret); } return 0; }
这样找子串是不够高效的,有一个 KMP 算法,用来实现在一个字符串中查找子字符串,效率高但是实现难度大。
这里不做赘述。
1.9 strtok
1.9.1 函数介绍
char * strtok ( char * str, const char * sep );
- 切割字符串
- sep 参数是个字符串,定义了用作分隔符的字符集合。
- 第一个参数指定一个字符串,它包含了 0 个或者多个由 sep 字符串中一个或多个分隔符分割的标记。
- strtok 函数找到 str 中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok 函数会改变被操作的字符串,所以在使用 strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改。)
- strtok 函数的第一个参数不为 NULL,函数将找到 str 中第一个标记, strtok 函数将保存它在字符串中的位置。
- strtok 函数的第一个参数为 NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { const char* sep = "@."; char email[] = "lxz@code.com"; char cp[30] = { 0 }; strcpy(cp, email); char* ret = strtok(cp, sep); printf("%s\n", ret); ret = strtok(NULL, sep); printf("%s\n", ret); ret = strtok(NULL, sep); printf("%s\n", ret); return 0; }
因为我们不知道字符串中有几个字段,所以不知道应该调用几次 strtok 函数,所以换一种写法。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { const char* sep = "@."; char email[] = "lxz@code.com"; char cp[30] = { 0 }; strcpy(cp, email); char* ret = NULL; for (ret = strtok(cp, sep); ret != NULL; ret = strtok(NULL, sep)) { printf("%s\n", ret); } //char* ret = strtok(cp, sep); //printf("%s\n", ret); //ret = strtok(NULL, sep); //printf("%s\n", ret); //ret = strtok(NULL, sep); //printf("%s\n", ret); return 0; }
这样写利用 for 循环可以适应任意数量字段的字符串。
1.10 sterror
1.10.1 函数介绍
char * strerror ( int errnum );
- 返回错误码,所对应的错误信息
C语言的库函数,在执行失败的时候,都会设置错误码。
字符分类函数:
函数 如果他的参数符合下列条件就返回真 iscntrl 任何控制字符 isspace 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v' isdigit 十进制数字 0~9 isxdigit 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F islower 小写字母a~z isupper 大写字母A~Z isalpha 字母a~z或A~Z isalnum 字母或者数字,a~z,A~Z,0~9 ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印) isgraph 任何图形字符 isprint 任何可打印字符,包括图形字符和空白字符
这些函数用法都很简单,不做赘述。
字符转换:
int tolower ( int c );//转小写 int toupper ( int c );//转大写
1.11 memcpy
1.11.1 函数介绍
void * memcpy ( void * destination, const void * source, size_t num );
- 函数 memcpy 从 source 的位置开始向后复制 num 个字节的数据到 destination 的内存位置。
- 这个函数在遇到 ‘\0’ 的时候并不会停下来。
- 如果 source 和 destination 有任何的重叠,复制的结果都是未定义的。
memcpy
函数可以用于复制任何类型的数据,无论是字符、整数、浮点数还是自定义数据结构。它不关心数据的具体类型,只是按照指定的字节数进行复制。由于
memcpy
的参数是void*
类型,可以轻松地将不同数据类型的数据复制到目标内存中,而不需要进行类型转换。这使得代码更加灵活和可维护。
1.11.2 memcpy 函数模拟实现
C 语言中
void*
类型的指针不能直接解引用的原因是因为void*
是一种通用的指针类型,它可以用来存储任何数据类型的地址,但它本身并不知道指向的是什么类型的数据。因此,直接解引用void*
指针是不允许的,因为编译器无法确定要访问多少字节的内存以及如何解释这些字节。为了解引用
void*
指针,需要将其转换为一个特定的数据类型的指针,这通常涉及到类型转换。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> void* my_memcpy(void* dest, const void* src, size_t num) { assert(dest && src); void* ret = dest; while (num--) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } return ret; } int main() { int arr1[] = { 1,2,3,4,5,6,7 }; int arr2[10] = { 0 }; my_memcpy(arr2, arr1, 28); return 0; }
memcpy 负责拷贝两块独立空间中的数据,对于重叠的空间,比如将自身空间中的数据拷贝到自身空间中,是不支持的。
重叠空间的拷贝,要用到 memmove
1.12 memmove
1.12.1 函数介绍
void * memmove ( void * destination, const void * source, size_t num );
- 和 memcpy 的差别就是 memmove 函数处理的源内存块和目标内存块是可以重叠的。
- 如果源空间和目标空间出现重叠, 就得使用 memmove 函数处理。
1.12.2 memmove 函数模拟实现
![]()
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<assert.h> void* my_memmove(void* dest, const void* src, size_t num) { assert(dest && src); void* ret = dest; if (dest < src) { //前->后 while (num--) { *(char*)dest = *(char*)src; dest = (char*)dest + 1; src = (char*)src + 1; } } else { //后->前 while (num--) { *((char*)dest + num) = *((char*)src + num); } } return ret; } void test() { int arr1[] = { 1,2,3,4,5,6,7,8,9,10 }; my_memmove(arr1 + 2, arr1, 20); int i = 0; for (i = 0; i < 10; i++) { printf("%d ", arr1[i]); } } int main() { test(); return 0; }
1.13 memcmp
1.13.1 函数介绍
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
- 比较从ptr1和ptr2指针开始的num个字节
- 返回值如下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> 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 05 00 00 00 int arr2[] = { 1,3,2 }; //01 00 00 00 03 00 00 00 02 00 00 00 int ret = memcmp(arr1, arr2, 12); printf("%d\n", ret); return 0; }
strcmp 只能比较字符串
memcmp 可以比较任意类型的数据
1.14 memset
- 内存设置
这个函数以字节为单位初始化内容,会把每个字节改变为要改的值。
int main() { int arr[10] = { 0 }; memset(arr, 1, 40); int i = 0; for (i = 0; i < 10; i++) { printf("%d\n", arr[i]); } return 0; }