🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客
更多知识尽在此专栏中!
目录
这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉
此文章分为三部分:字符串函数、 字符分类函数、内存函数。
首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:
下面介绍的是对目标字符串操作长度不可控的函数,使用场景相对有限。
strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'\0'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。
使用注意事项:
- strlen 统计的是 \0 之前出现的字符个数
- 使用时,必须包含结束标志
- 返回值是 size_t (unsigned int 无符号整型)
模拟实现 strlen
这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。
既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。下面来看看具体代码实现吧:
//strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1操作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。
字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组 dest 可改变,数组空间也要足够大,不然装不下源字符串就尴尬了。
使用注意事项:
- 源字符串中必须包含 \0
- 源字符串中的 \0 会拷贝到目标字符数组中
- 目标空间必须足够大,能够装下源字符串
- 目标空间必须是可修改的
模拟实现 strcpy
同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。
//strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }当然,我们的模拟函数也能实现需求
字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1
因为比较并不需要改变值,所以使用常量字符串也能比较。 使用注意事项:
- 字符串大小比较时,与长度无关
- 从首字符开始,逐字符比较
- 通过字符对应的ASCII码值做对比
模拟实现 strcmp
我们可以通过指针的移动来模拟实现这个函数,即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,最后再分情况确定返回值就行了。
//strcmp 字符串比较 int myStrcmp(const char* dest, const char* src) { assert(dest && src);//断言 //当找到不同数或移动到\0处时,循环停止 while (*dest == *src && (*dest || *src)) { dest++;//没有找到不同的元素 src++;//需要向后偏移 } //分情况判断,确定返回值 if (*dest - *src > 0) return 1; else if (*dest - *src < 0) return -1; else return 0; } int main() { char* str1 = "BATZ"; char* str2 = "BAT"; printf("库函数实现结果:\n%d\n", strcmp(str1, str2)); //printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2)); return 0; }使用模拟函数通过测试用例:
追加,就是在目标字符数组的末尾(\0处)添加源字符串的值,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。
使用注意事项:
- 源字符串和目标字符数组中都必须有\0
- 目标空间必须足够大
- 目标空间必须可修改,所以是字符数组
模拟实现 strcat
既然是在目标字符数组的末尾处追加字符,就需要把指向首地址处的指针 dest 移向尾地址,当然在移动前要保存此地址,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,这样就完成了追加的操作,最后再返回之前记录的首地址就行了。
//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝操作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }
下面开始介绍字符串操作函数的升级版,这些函数使用起来更加自由方便。
相比于前面的 strcpy,strncpy 多了一个字母n,n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,相较于前面的固定拷贝,这里的可控拷贝更为灵活,且如果选定的n字节中没有包含结束标志\0,则在拷贝结束后,strncpy 会自动添加一个\0。
使用注意事项:
- 目标空间必须足够大
- 目标空间必须可修改
- 源字符串中不一定要有\0(函数会自己添加)
模拟实现 strncpy
这个模拟实现也比较简单,注意两点就行了,循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 \0 。这样一来就能和 strncpy 一样了。
//strncpy n个字符串拷贝 char* myStrncpy(char* dest, const char* src, size_t k) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //循环k次,确保拷贝足够的字节数 while (k--) { *dest++ = *src++;//类似拷贝的操作 } *dest = '\0';//再次给目标字符数组中的元素赋值 return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxx"; char arr2[] = "Hello World!"; //printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5)); printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5)); return 0; }
同样的,strncmp 也能控制比较长度,当然控制长度不能超过源字符串的长度,不然是无意义的
使用注意事项:
- 与 strcmp 基本一致
- 控制比较字节数不能为负数
模拟实现 strncmp
这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。
//strncmp n个字符串比较 int myStrncmp(const char* dest, const char* src, size_t k) { assert(dest && src);//断言 //为何使用前置--? //因为这样能有效避免多判断一次的情况 while (--k && *dest == *src) { dest++;//确保每位都能对比到 src++; } //分情况返回 if (*dest - *src > 0) return 1; else if (*dest - *src < 0) return -1; else return 0; } int main() { char* str1 = "BATZ"; char* str2 = "BAT"; //printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3)); printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3)); return 0; }
可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,同所有可控家族成员一样,strncat 也会自动添加结束标志 \0。因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。
使用注意事项:
- 目标字符数组中必须有\0
- 目标空间必须足够大
- 目标空间必须可修改
- 源字符串中可以不包含\0
模拟实现 strncat
代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:1.循环判断条件 2.最后 \0 的添加
//strncat n个字符串追加 char* myStrncat(char* dest, const char* src, size_t k) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //使指针移向目标字符数组的末尾处 while (*dest) { dest++; } //判断条件为k,即传入的控制字节数 while (k--) { *dest++ = *src++;//确保每个元素都能追加 } *dest = '\0'; return tmp;//返回目标字符数组的起始地址 } int main() { char arr1[20] = "xxxxxxx"; char arr2[] = "Hello World!"; //printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5)); printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5)); return 0; }
字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。
使用注意事项:
- 只要传入的字符串地址就行了
- 这个函数没有什么需要特别注意的事项
模拟实现 strstr
这个函数实现起来就比较复杂了,需要用到多个指针,不断记录位置、移动位置、刷新位置,当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。
//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为\0 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的\0结束的,就找到了目标字符串 if (*s2 == '\0') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }
字符串分割函数,顾名思义就是对字符串进行分割操作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。
使用注意事项:
- 目标字符数组中要包含结束标志
- 第一次传递的是字符串首地址
- 如果针对同一个字符串,第二次需要传递一个空指针
- 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
//strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,当程序运行出错后,errno 会获取当前的错误码,这样一来,strerror(errno) 搭配就能很好的打印出错误信息。
使用注意事项:
- strerror 中的参数必须是整型,字符型会按ASCII码处理
- error 在使用时需要包含头文件 errno.h
//strerror 字符串报错 #include int main() { //打开不存在的文件,使程序报错 FILE* pf = fopen("test.txt", "r"); if (!pf) { printf("%s\n", strerror(errno)); return -1; } fclose(pf); pf = NULL; return 0; }
这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用 ctype.h 这个头文件就能直接使用。
如果成立,返回1,否则返回0。
//isdigit 判断十进制 #include int main() { if (isdigit('8')) printf("字符8是十进制数字"); else printf("字符8不是十进制数字"); return 0; }
如果成立,返回1,否则返回0。
//isxdigit 判断十六进制 #include int main() { if (isxdigit('f')) printf("字符f是十六进制数字"); else printf("字符f不是十六进制数字"); return 0; }
如果成立,返回1,否则返回0。
//isupper 判断大写 #include int main() { if (isupper('f')) printf("字符f大写字母"); else printf("字符f不是大写字母"); return 0; }
如果成立,返回1,否则返回0。
//islower 判断小写 #include int main() { if (islower('f')) printf("字符f是小写字母"); else printf("字符f不是小写字母"); return 0; }
返回类型为整型,对应ASCII码值
//toupper 小写转大写 #include int main() { char c = 'a'; printf("%c\n", toupper(c)); return 0; }
返回类型为整型,对应ASCII码值
//tolower 大写转小写 #include int main() { char c = 'Z'; printf("%c\n", tolower(c)); return 0; }
因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:
函数 | 当条件满足时(即所传递参数符合条件时)返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符,比如空格、换页、换行、回车、制表符等 |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字 0~f (或0~F都行) |
islower | 小写字母 a~z |
isupper | 大写字母 A~Z |
isalpha | 任意字母,即 a~z 或 A~Z |
isalnum | 字母或数字,即 a~z 、A~Z 或 0~9 |
ispunct | 标点符号,即不属于数字或字母的圆形字符 |
isgraph | 任何图形字符 |
isprint | 任何可打印的字符,包括图形字符和空白字符 |
toupper、tolower | 除ASCII码为0外的任何字符 |
内存操作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的操作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。
相当于全能版的 strcpy
使用注意事项:
- 目标空间必须足够大
- 目标空间必须可修改
- 传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数
模拟实现 memcpy
跟当时我们模拟实现 strncpy 一样,需要把字节控制数 num 作为判断依据,不过因为这是全能型的拷贝函数,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。
值得一提的是,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的操作,所以说库函数中的 memcpy 是个满分拷贝。至于如何实现这个功能,下面会介绍到。
//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加\0 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }
内存移动函数,移动可以看作拷贝,memmove 包含 memcpy,能实现更多操作,可以这样比喻,memcpy 是男人,memmove 是帅气的男人。微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。
使用注意事项:
- 目标空间必须足够大
- 目标空间必须可修改
- 传入字节数要慎重考虑
模拟实现 memmove
memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,判断依据为目标字符地址与源字符地址的大小。
//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的操作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }图解从前往后拷贝与从后往前拷贝的区别:
注:为了简化讲解,使用的是另一个示例
内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,确保每位都能对比到,返回值和 strncmp 一样。
使用注意事项:
- 传递参数时,要传地址(指针)
- 返回参数类型为整型
- 传入字节数要慎重考虑
模拟实现 memcmp
模拟实现的话也比较简单,抓住两个点就行了,1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
//memcmp int myMemcmp(const void* dest, const void* src, size_t num) { assert(dest && src); if (!*(char*)src && *(char*)dest) return 1;//当源空间为空,且目标空间不为空时 if (!*(char*)src && !*(char*)dest) return 0;//当源空间与目标空间都为空时,避免无效循环 //类似于 strncmp 的判断条件 while (--num && *(char*)dest == *(char*)src) { ((char*)dest)++;//逐字符比较 ((char*)src)++; } //分流判断返回 if (*(char*)dest > *(char*)src) return 1; else if (*(char*)dest < *(char*)src) return -1; else return 0; } int main() { char* str1 = "BBAZ"; char* str2 = "BBA"; //printf("%d\n", memcmp(str1, str2, sizeof(char) * 2)); printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2)); return 0; }
内存设置函数,顾名思义就是对数据在内存中的存储值做修改,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。
使用注意事项:
- 参数1要为指针,如果不是指针类型,就传入地址
- 参数2为整型,代表在内存中设置后的具体值
- 参数3需要慎重考虑,不能超过原数据的大小
本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型操作开始,延伸到全数据类型的操作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。知其然,并知其所以然,对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊
如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。
相关文章推荐
C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客