好久不见呀,最近系统学习了一下常会用到的库函数,比如字符串函数strlen、strcmp、strcpy字符函数以及内存函数memcpy、memmove等等。其实前面也陆陆续续介绍过,只是一直没有作一个总结,这篇博客就把它们整理介绍一下,顺便用自己的方法去模拟实现,有兴趣的小伙伴来看看咩(预警!!!篇幅较长,可先收藏后看!!!)
库函数介绍及相关模拟实现
一、字符串函数
1、求字符串长度的函数
(1)介绍
字符串中使用 strlen 函数来求字符串的长度,下面来看一下函数声明:
size_t strlen( const char *string );
这个函数的头文件是<string.h>,返回值为无符号整型 size_t ,形参为 const 修饰的字符串首元素地址。字符串以 ’ \0 ’ 为结束标志,返回的就是 ’ \0 ’ 前的字符个数,注意 ’ \0 ’ 不包括在内。形参指向的字符串必须要以 ’ \0 ’ 结束。
下面举一个实例:
int main()
{
char arr[] = { "abcdef" }; //双引号引住的字符串会自动生成 '\0'
printf("%d\n", strlen(arr));
return 0;
}
(2)模拟实现
- count计数器实现
int str_len(const char* str)
{
assert(str != NULL);
int count = 0;
while (*str++)
{
count++;
}
return count;
}
- 函数递归实现
int str_len(const char* str)
{
assert(str != NULL);
while (*str++)
{
return 1 + str_len(str++);
}
return 0;
}
- 函数指针的运算实现
int str_len(const char* str)
{
assert(*str != NULL);
const char* ret = str;
while (*ret++)
{
;
}
return ret - str - 1;
}
2、长度不受限制的字符串函数
(1)strcpy
-
函数声明:
char *strcpy( char *strDestination, const char *strSource );
头文件使用 <string.h> ,函数功能是将源字符串中的字符拷贝到目的字符串中(包括 ’ \0 ’ ),返回值返回拷贝后的目的字符串。
-
注意事项
源字符串必须以 ’ \0 ’ 结尾,防止程序陷入死循环;目标字符串必须空间足够大,并且是可修改的(为了把源字符串赋过来) -
实例:
int main() { char str1[20]; char str2[] = "zuoyawen"; char* ret = strcpy(str1, str2); printf("%s\0", str1); return 0; }
-
模拟实现
char* str_cpy(char* dest, const char* src) { assert(dest != NULL); assert(src != NULL); char* ret = dest; while (*ret++ = *src++) { ; } return dest; }
(2)strcat
-
函数声明
char *strcat( char *strDestination, const char *strSource );
头文件使用 <string.h> ,函数功能是将源字符串中的字符追加到目的字符串 ’ \0 ’ 之后(会覆盖目的字符串本来的结束标志),追加时会把源字符串的结束标志一起追加过来。返回值返回追加后的目的字符串。
-
注意事项
源字符串和目的字符串必须以 ’ \0 ’ 结束,防止函数陷入死循环;目标字符串必须空间足够大,并且是可修改的(为了把源字符串赋过来);这个函数不可以实现字符串自身的追加(后面会讲strncat,这个函数可以实现),因为会在一开始就覆盖掉结束标志,陷入死循环。 -
实例
int main() { char str1[20] = "hello "; char* str2 = "xiaozuo!"; char* ret = strcat(str1, str2); printf("%s\n", ret); return 0; }
-
模拟实现
大体思路就是先把目的字符串遍历,找到结束标志,再采用strcpy的模拟实现方法实现即可。char* str_cat(char* dest, const char* src) { assert(dest != NULL); assert(src != NULL); char* ret = dest; while (*ret) { ret++; } while (*ret++ = *src++) { ; } return dest; }
(3)strcmp
-
函数声明
int strcmp( const char *string1, const char *string2 );
头文件使用 <string.h> ,函数功能是比较两个字符串的ASCII码值。返回值返回比较后的结果,string1 > string2 时,返回一个正值;string1 = string2 时,返回0; string1 < string2 时,返回一个负值。
-
注意事项
在VS编译器下,该函数的返回值分别是1,0,-1,但是这并不是说明只有这一种情况,所以在使用时条件判断不要用 ==,而是采用大于小于这样的方式去判断,下面看一个实例。 -
实例
int main() { char* str1 = "hello "; char* str2 = "xiaozuo!"; int ret = strcmp(str1, str2); if (ret > 0) { printf("%s > %s\n", str1, str2); } else if (ret = 0) { printf("%s = %s\n", str1, str2); } else { printf("%s < %s\n", str1, str2); } return 0; }
-
模拟实现:找不同的字符,若未找到返回0,找到返回差值即可
-
方法一
int str_cmp(const char* str1, const char* str2) { while (*str1 == *str2) { if (*str1 == '\0') { return 0; } str1++; str2++; } //*str1 != *str2退出循环,跳到这里,返回两个ASCII的差值就好 return *str1 - *str2; }
-
方法二
int str_cmp(const char* str1, const char* str2) { assert(str1 != NULL); assert(str2 != NULL); //判空不可以直接指针通过NULL判空,因为结束标志所在的地方不一定为空指针 while (*str1 != '\0' && *str2 != '\0') { if (*str1 != *str2) { return *str1 - *str2; } str1++; str2++; } return 0; }
-
方法三
int str_cmp(const char* str1, const char* str2) { assert(str1 != NULL); assert(str2 != NULL); int ret = 0; while (*str1 && !(ret = *str1 - *str2)) { str1++; str2++; } return ret; }
这种是小左认为最简单的一种~ 前两种都是循环嵌套一个条件判断,这种只需要做循环判断就好。怎么样,是不是很巧妙?
3、长度受限制的字符串函数
对于长度不受限制的字符串函数可能会出现安全性方面的问题,比如strcpy 、strcat 这样的函数,若是目的字符串开辟的内存空间不够大就可能会导致溢出的问题,所以就出现了这样一些长度受限制的字符串操作函数,规定操作的长度就可以规避这个问题。
由于处理的思想同无长度限制的字符串操作函数基本一致,这里就不再进行模拟实现,只进行实例演示。
(1)strncpy
-
函数声明
char *strncpy( char *strDest, const char *strSource, size_t count );
同 strcpy 一样头文件是 <string.h> ,有目的操作数和源操作数,返回值为目的操作数首元素地址,区别是加了一个形参 count 。功能是把源操作数的前 count 个字符拷贝到目的字符串。
-
注意事项
若需要拷贝的字符个数 count 小于源字符串本身的个数num,只拷贝count个字节,不会主动追加结束标志 ’ \0 ’ ;若 count 大于 num 时则该函数会自动在拷贝完源字符串之后,在目的字符串后面追加 count - num 个 ’ \0 ’ -
实例
int main() { char str1[20] = {0}; char* str2 = "hello xiaozuo!"; strncpy(str1, str2, 10); printf("%s\n", str1); return 0; }
(2)strncat
-
函数声明
char *strncat( char *strDest, const char *strSource, size_t count );
同 strcat 一样头文件是 <string.h> ,有目的操作数和源操作数,返回值为目的操作数首元素地址,区别是加了一个形参 count 。功能是把源操作数的前 count 个字符追加到目的字符串。
-
注意事项
追加时,不会追加 ’ \0 ',开辟空间的时候源字符串会自己将未赋值的内存空间全部默认为 ’ \0 ’ 。
-
实例
int main() { char str1[20] = "hello "; char* str2 = "xiaozuo!"; strncat(str1, str2, 8); printf("%s\n", str1); return 0; }
(3)strncmp
-
函数声明
int strncmp( const char *string1, const char *string2, size_t count );
同 strcmp 一样头文件是 <string.h> ,有两个操作数,返回值为比较后的结果,区别是加了一个形参 count 。功能是将两个操作数的前 count 个字符进行比较。
-
实例
int main() { char arr[][5] = { "R2D2","C3P0","R2A6" }; int i = 0; for (i = 0; i < 3; i++) { if (strncmp(arr[i], "R2xx", 2) == 0) { printf("find it! -> %s\n",arr[i]); } } return 0; }
4、其余字符串函数
(1)strstr
-
函数声明
char *strstr( const char *string, const char *strCharSet );
函数头文件是 <string.h> ,形参为两个 const 修饰的字符串,其中第一个为原字符串,第二个需要判断是不是第一个的子字符串,返回值为 char* 类型的指针,若是找到了该子串,返回找到第一个子字符串的原字符串的地址;若是未找到,返回一个NULL即可。
-
注意事项
当原字符串中有多个子字符串时,只返回第一个所在的地址,若要继续找后面的,可以以第一次返回的字字符串串为新的原字符串,再去寻找。
-
实例
这里使用该函数实现判断字符串是否旋转,基本思想:利用strncat()函数构造一个新的字符串,通过strstr()函数直接找到子字串,能找到说明就是旋转得到的。int judge_spin(const char* str, const char* spin_str) { int num = strlen(str); char* str1 = strncat(str,str, num); if (strstr(str1, spin_str) != NULL) { return 1; } return 0; } int main() { char str1[30] = "abcde"; char str2[30] = "eabcd"; int ret = judge_spin(str1, str2); if (ret) { printf("yes\n"); } else { printf("no\n"); } return 0; }
(2)strtok
-
函数声明
char *strtok( char *strToken, const char *strDelimit );
函数头文件是 <string.h> ,形参为两个字符串,其中第一个是被分隔的字符串,第二个是分隔的字符集合。当使用该函数找到下一个分隔标记,将其替换为 ’ \0 ’ ,并返回指向这个分隔标记的指针(即下一个分隔标记前的内容);当第一部分参数为NULL时,直接从下一个开始;若再无标记时,返回NULL。
-
注意事项
该函数会改变原字符串内容,所以使用时都是放到临时拷贝的内存中,同时保证其可修改。
-
实例
由于这一个函数的实现过程比较复杂,口述不够形象,小左就把它的执行过程截图附到后面,方便理解。
代码1:int main() { char str[] = ".xiao.zuo.xiao.chen"; char* pstr; pstr = strtok(str, "."); while (pstr != NULL) { printf("%s\n", pstr); //固定格式。第一次使用原字符串名,后面就直接NULL就好 pstr = strtok(NULL, "."); } return 0; }
执行过程:
代码2:有了代码1的基础,代码2就不再展示执行过程,基本同1一致,只是通过 for 循环去实现,具体见注释。
int main() { char* ip = "192.168.0.1"; const char* sign = "."; //创建临时变量来进行切分,防止改变被操作数 char str1[30]; strcpy(str1, ip); //创建一个指向每部分字符串的指针 char* str = strtok(str1, sign); //循环走完所有的分隔符除了上面初始化用原字符串,其余使用这个函数时,原字符串为承接上一个的NULL for (str; str != NULL; str = strtok(NULL, sign)) { printf("%s\n", str); } return 0; }
(3)strerror
-
函数声明
char *strerror( int errnum );
函数头文件是 <string.h> ,形参为整型变量错误序号,使用时要包含头文件 <errno.h>,返回值是一个指向错误信息字符串的指针。
-
实例
int main () { FILE * pFile; //定义一个文件 pFile = fopen ("unexist.doc","r"); //以只读的方式打开该文件 if (pFile == NULL) { printf("Error information -> %s\n", strerror(errno)); //perror("Error information"); } return 0; }
-
改良——perror( )
直接把错误提示信息以字符串的形式写到形参上,也不需要写错误信息编号,一定程度上优化了代码,所以实际编程时都写的是 perror( ) 函数。
二、字符函数
注意字符函数都需要一个头文件 <ctype.h>,这部分内容没有什么技术含量,所以只做总结,不作具体实现。
1、字符分类函数
函数 | 参数符合条件返回真 |
---|---|
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 | 任何可打印字符,包括图形字符和空白字符 |
2、字符转换函数
注意,转换只是返回改变后的 ASCII 值,并不会改变字符本身!!
int tolower ( int c ); //大写字母转换成小写
int toupper ( int c ); //小写字母转换成大写
三、内存函数
内存函数这部分,它的头文件 也是<string.h> ,实现的功能其实也和字符串函数一样,而它的优势是操作数不再是字符串,而是内存单元。这就意味着它可以把这些功能实现在更多的数据类型中。内存函数只说四个使用频率较高的,其余小伙伴们可自行学习,这里就不作介绍~
1、memcpy
-
函数声明
void *memcpy( void *dest, const void *src, size_t count );
函数形参是两个 void* 的指针(分别指向目的操作数,源操作数)以及一个 size_t 的值 count (表示需要拷贝的字符字节数);返回值也是 void* 的指针,返回一个指向目的操作数的指针。
-
注意事项
这个函数在遇到 ‘\0’ 的时候并不会停下来;只能实现非重叠拷贝(先拷贝的数据可能会覆盖后拷贝的数据)
-
实例
#include <stdio.h> #include <string.h> //实现整数数组的拷贝 int main() { int arr1[10] = { 1,2,3,4,5 }; int arr2[20]; memcpy(arr2,arr1,12); int i = 0; for (i = 0; i < 3; i++) { printf("%d ", arr2[i]); } return 0; } //实现结构体的拷贝 struct human { char name[20]; int age; }; int main() { struct human h1 = { "xiaozuo",21 }; struct human h2; memcpy(&h2, &h1, sizeof(h1)); printf("name:%s age:%d \n", h2.name, h2.age); return 0; }
-
模拟实现
void* mem_cpy(void* dest, const void* src, size_t count) { assert(dest); assert(src); void* ret = dest; while (count--) { *((char*)dest)++ = *((char*)src)++; } return ret; }
2、memmove
-
函数声明
void *memmove( void *dest, const void *src, size_t count );
这个函数就弥补了 memcpy 不可以实现重叠拷贝的缺点,函数形参是两个 void* 的指针(分别指向目的操作数,源操作数)以及一个 size_t 的值 count (表示需要拷贝的字符字节数);返回值也是 void* 的指针,返回一个指向目的操作数的指针。
-
注意事项
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的;如果源空间和目标空间出现重叠,就得使用memmove函数处理。
-
实例
//整数数组的重叠拷贝 int main() { int arr1[10] = { 1,2,3,4,5 }; memmove(arr1, arr1 + 2, 12); int i = 0; int sz = sizeof(arr1) / sizeof(arr1[0]); for (i = 0; i < sz; i++) { printf("%d ", arr1[i]); } return 0; } //字符串的重叠拷贝 int main() { char str[30] = "abcdefg hijkiloveyou"; memmove(str, str + 12, 9); puts(str); return 0; }
-
模拟实现
由于这个函数一般是专门来实现重叠拷贝的,为了防止未拷贝数据就被覆盖,具体还需要分情况讨论:
情况1:目的空间地址在源空间地址之前有交叠,从前向后拷贝; 情况2:目的空间地址在源空间地址之后有交叠,从后向前拷贝; 情况3:当两个地址空间无交叠或者完全重叠时,不存在覆盖问题,拷贝顺序都可。
所以实现方式有两种,本篇文章做模拟实现的时候采用(情况1 + 情况2、3)这种,下面看实现:
void* mem_move(void* dest, const void* src, size_t count) { assert(dest); assert(src); void* ret = dest; //从前往后拷贝,和memcpy一样 if (dest < src) { while (count--) { *((char*)dest)++ = *((char*)src)++; } } //其余情况都采用从后往前拷贝 else { while (count--) { *((char*)dest + count) = *((char*)src + count); } } return ret; }
3、memset
-
函数声明
void *memset( void *dest, int c, size_t count );
函数形参是一个 void* 的指针(指向目的操作数),一个准备设置的字符 c 以及一个 size_t 的值 count (表示需要设置的字符字节数);返回值也是 void* 的指针,返回一个指向目的操作数的指针。
-
注意事项
虽然这个函数可以设置字符,但对于数字而言只能设置 0 ,因为它是按字节统一去设置,无法实现四个字节为单位的一致。
-
实例
int main() { char str[30]; memset(str, 'z', 3); str[5] = 0;//为了看是不是只设置了3个 puts(str); return 0; }
4、memcmp
int memcmp( const void *buf1, const void *buf2, size_t count );
函数形参是两个const void* 的指针(分别指向两个需要比较的操作数)以及一个 size_t 的值 count (表示需要比较的字符字节数);返回值是 int 型的,通过返回的值确定大小。功能基本和strncmp类似,这里就不再做实现~
综上,这次的分享又结束了~欢迎评论区讨论指正!!!