库函数详解
介绍处理字符和字符串的库函数还有内存操作函数的使用和注意事项。
C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。
字符串常量适用于那些对它不做修改的字符串函数。
求字符串长度函数
strlen
size_t strlen ( const char * str );
-
字符串已经 ‘\0’ 作为结束标志,
strlen
函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。 -
参数指向的字符串必须要以 ‘\0’ 结束。
-
注意函数的返回值为
size_t
,是无符号的(易错)。#include <stdio.h> #include <string.h> int main() { const char*str1 = "abcdef"; const char*str2 = "bbb"; if(strlen(str2)-strlen(str1)>0) { printf("str2>str1\n"); } else { printf("srt1>str2\n"); } return 0; }
- 上图所示:竟然返回了
str2>str1
的结果,这就是因为strlen
的返回值size_t
是一个无符号数,当我们用较小的数减去较大的数得到一个负数,而无符号数会它当做正数来看待,结果大于0。 - 本题来看
3 - 6 = -3
-3的二进制
1000 0000 0000 0000 0000 0000 0000 0011 - 原码
1111 1111 1111 1111 1111 1111 1111 1100 - 反码
1111 1111 1111 1111 1111 1111 1111 1101 - 补码
-
上图就是实际算出的结果,所以肯定是大于0的,使用
strlen
比较字符串长度时,一定要注意不要用减法来实现,可以直接使用比较运算符。 -
strlen
函数的模拟实现。#include<stdio.h> #include<assert.h> //my_strlen模拟实现strlen size_t my_strlen(const char* str) { assert(str); const char* start = str; const char* end = str; while (*end != '\0') { end++; } return end - start; } //测试 int main() { const char* str1 = "abcdef"; printf("%d", my_strlen(str1)); }
长度不受限制的字符串函数
strcpy
将源指向的C字符串复制到目标指向的数组中,包括终止空字符(并在该点停止)。
char* my_strcpy(char * destination, const char * source );
-
源字符串必须以
'\0'
结束。 -
会将源字符串中的
'\0'
拷贝到目标空间。#include<stdio.h> #include<string.h> int main() { char arr[10] = "xxxxxxxxx"; char arr2[] = { 'h', 'e','\0', 'l', 'l', 'o'}; strcpy(arr, arr2); printf("%s\n", arr); return 0; }
- 我们调试可以看到,当复制到目标数组中时,会将终止符一起复制过去,并在该处停止。
-
目标空间必须足够大,以确保能存放源字符串。
#include<stdio.h> #include<string.h> int main() { char arr[3] = {0}; char arr2[] = "abcdef"; strcpy(arr, arr2); printf("%s\n", arr); return 0; }
- 可以看到如果目标空间不够大,则会报运行时检查失败2-变量“arr”周围的堆栈已损坏的错误,也就是数组发生了越界。
-
但最后仍然能够输出,这取决于字符串的输出方式,找到第一元素的地址往后遍历即可,直到遇到
'\0'
。 -
目标空间必须可变。
#include<stdio.h> #include<string.h> //错误的示范 int main() { char* p = "hello world"; //常量字符串 char arr2[] = "abcdef"; strcpy(p, arr2); printf("%s\n", p); return 0; }
-
如果将目标空间设置为常量字符串等不可变空间,则会引发异常。
-
strcpy函数的模拟实现。
#include<stdio.h> #include<assert.h> char* my_strcpy(char* dest, const char* src) { assert(dest && src); char* ret = dest; while (*dest++ = *src++) { ; } return ret; } //测试 int main() { char arr1[20] = "abc"; char arr2[] = "hello world"; //返回的是目标空间首地址char* printf("%s\n", my_strcpy(arr1, arr2)); return 0; }
strcat
将源字符串的副本附加到目标字符串。
在目标字符串中的
\0
,由源的第一个字符覆盖。两个串联形成的新字符串,结尾必须有
\0
,也就是源字符串的\0
也被复制过去。
char * strcat ( char * destination, const char * source );
-
源字符串必须以 ‘\0’ 结束。
-
目标空间必须有足够的大,能容纳下源字符串的内容。
-
目标空间必须可修改。
-
字符串自己给自己追加。
#include<stdio.h> #include<string.h> int main() { char arr1[20] = "hello"; printf("%s\n", strcat(arr1, arr1)); return 0; }
-
之所以会报错是因为追加时,将目标字符串结尾的
\0
覆盖了,后续遍历自己的时候不会有终止的时刻,造成死循环,然后发生访问冲突。 -
strcat函数的模拟实现。
#include<stdio.h> #include<assert.h> char* my_strcat(char* dest, const char* src) { assert(dest && src); // 1.找目标空间的'\0' char* ret = dest; while (*dest != '\0') { dest++; } // 2.拷贝源数据到'\0'以及之后的空间 while (*dest++ = *src++) { ; } return ret; } //测试 int main() { char arr1[20] = "hello"; char arr2[] = " world"; printf("%s\n", my_strcat(arr1, arr2)); return 0; }
strcmp
此函数先开始比较每个字符串的第一个字符,如果它们彼此相等,往后比较直到字符不同或到达
\0
。strcmp比较的是对应位置上字符的大小,而非长度。
int strcmp ( const char * str1, const char * str2 );
标准规定:
- 第一个字符串大于第二个字符串,则返回大于0的数字。
- 第一个字符串等于第二个字符串,则返回0。
- 第一个字符串小于第二个字符串,则返回小于0的数字。
strcmp函数的模拟实现。
#include<stdio.h>
#include<assert.h>
int my_strcmp(const char* s1, const char* s2) {
assert(s1 && s2);
while (*s1 == *s2) {
if (*s1 == '\0') {
return 0;
}
s1++;
s2++;
}
//源码的实现方式
/*if (*s1 > *s2) {
return 1;
}
else {
return -1;
}*/
return *s1 - *s2;
}
不同的测试用例分析
-
找到不同字符停止
//测试1 int main(){ char arr1[] = "abcdef"; char arr2[] = "abq"; int ret = my_strcmp(arr1, arr2); if (ret < 0) printf("arr1<arr2\n"); else if(ret>0) printf("arr1>arr2\n"); else printf("arr1==arr2\n"); printf("%d\n", ret); return 0; }
-
可以看出
arr1<arr2
,因为比较到字符串第三位时,'c'<'q'
停止往后遍历,得到’c'-'q'
的值-14。 -
遇到
'\0'
前的字符都一样
//测试2
int main(){
char arr1[] = "abcd";
char arr2[] = "abc";
int ret = my_strcmp(arr1, arr2);
if (ret < 0)
printf("arr1<arr2\n");
else if(ret>0)
printf("arr1>arr2\n");
else
printf("arr1==arr2\n");
printf("%d\n", ret);
return 0;
}
-
当其中一个字符串已经访问到
'\0'
也会停止,然后依旧用对应位置相减,'d'-'\0'
的结果为100,也就是'd'
的ASCII码值。(字符串结束符'\0'
的ASCII是0) -
当两个字符串都没有
'\0'
结尾,且前面字符都一样。//测试3 int main(){ char arr1[] = { 'a', 'b', 'c' }; char arr2[] = { 'a', 'b', 'c' }; int ret = my_strcmp(arr1, arr2); if (ret < 0) printf("arr1<arr2\n"); else if(ret>0) printf("arr1>arr2\n"); else printf("arr1==arr2\n"); printf("%d\n", ret); return 0; }
- 我们观察内存如图所示,按照规则,该函数会一直往后找寻,直到遇见不一样的字符或者
'\0'
。
- 最后得出的结果会变得随机,无法判断。这次是因为遇到内存中的90和CC停止后相减的结果。
长度受限制的字符串函数
此类函数于长度不受限制的区别在于多传入了一个size_t类型的参数,用于确定拷贝,追加,比较的个数是多少,也就是多了一个停止的条件。
strncpy
拷贝num个字符从源字符串到目标空间。
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加
'\0'
,直到num个。
char * strncpy ( char * destination, const char * source, size_t num );
strncpy函数的模拟实现。
#include<assert.h>
char * my_strncpy ( char * dest, const char * src, size_t num ){
assert(dest && src);
char* start = dest;
while(num && (*dest++ = *src++) != '\0'){
num--;
}
if(num){
while(--num){
*dest++ = '\0';
}
}
return start;
}
strncat
同样的方式考虑长度后面补
'\0'
即可。追加是不仅仅是把其中设定的字符放入,还有末尾的'\0'
也会补上。
char * strncat ( char * destination, const char * source, size_t num );
使用strncat实现自己追加自己
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abc";
strncat(arr1, arr1, 3);
printf("%s\n", arr1);
return 0;
}
- 成功实现,弥补了strcat不能自己追加自己的遗憾。
strncat函数的模拟实现。
#include<assert.h>
char * my_strncat ( char * dest, const char * src, size_t num ){
assert(dest && src);
char* start = dest;
while(*dest != '\0'){
dest++;
}
while(num--){
if((*dest++ = *src++) != '\0'){
return start;
}
}
*dest = '\0';
return start;
}
strncmp
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
int strncmp ( const char * str1, const char * str2, size_t num );
strncmp函数的模拟实现。
- 第一种
#include<assert.h>
int my_strncmp ( const char * str1, const char * str2, size_t num ){
while(num-- && *str1 && *str2 && (*str1==*str2)){
str1++;
str2++;
}
return (*(unsigned char*)str1 - *(unsigned char*)str2);
}
- 第二种
#include<assert.h>
int my_strncmp(const char *s1, const char *s2, size_t n){
while(n-- && *s1 && *s2){
if(*s1 != *s2){
return (unsigned char)*s1 - (unsigned char)*s2;
}
s1++;
s2++;
}
//当while语句不成立,即n、*s1、*s2中任一一个为0,或者*s1和*s2同时为0, while循环条件不成立
if(!n) //比较位数截止,相等。
return 0;
if(*s1) //s2为0,s1一定大于s2。
return 1;
if(*s1 == *s2 ) //两者同时截止遇到\0,相等。
return 0;
return -1; //s1为0,s1一定小于s2。
}
字符串查找函数
strstr
在一个主串中找子串是否存在。如果存在返回子串第一次出现位置(地址);否则返回NULL。
char * strstr ( const char *str1, const char * str2);
用法:
#include<stdio.h>
#include<string.h>
int main() {
char arr1[] = "abcdefbcd";
char arr2[] = "bcd";
char* p = strstr(arr1, arr2);
if (p == NULL) {
printf("不存在!\n");
}
else {
printf("%s\n", p);
}
return 0;
}
- 从上面的答案可以看出,找到子串后,返回第一次出现的地址,如果对其进行输出,将从该地址往后遍历主串输出。
strstr函数的模拟实现。
- 暴力求解
#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;
if(*str1 == '\0'){
return str1;
}
while (*p) {
while (*s1 != '\0' && s2 != '\0' && (*s1 == *s2)) {
s1++;
s2++;
}
if (*s2 == '\0') {
return (char* )p;
}
p++;
s1 = p;
s2 = str2;
}
return NULL;
}
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
void getNext(char* sub, int* next, int lenSub) {
next[0] = -1;
next[1] = 0;
//当前i的下标
int i = 2;
//存储的前一项的k值。
int k = 0;
while (i < lenSub) {
if (k == -1 || sub[i - 1] == sub[k]) {
next[i] = k + 1;
i++;
k++;
}
else {
k = next[k];
}
}
}
char* my_strstr(char* str, char* sub) {
assert(str && sub);
int lenStr = strlen(str);
int lenSub = strlen(sub);
if (lenStr == 0 || lenSub == 0) {
return NULL;
}
//求next数组
int* next = (int*)malloc(sizeof(int) * lenSub);
assert(next);
getNext(sub, next, lenSub);
//遍历主串
int i = 0;
//遍历子串
int j = 0;
while (i < lenStr && j < lenSub) {
if (j == -1 || str[i] == sub[j]) {
i++;
j++;
}
else {
j = next[j];
}
}
free(next);
if (j >= lenSub) {
return &str[i-lenSub];
}
else {
return NULL;
}
}
strtok
char * strtok ( char * str, const char * sep );
sep
参数是个字符串,定义了用作分隔符的字符集合。- 第一个参数指定一个字符串,它包含了0个或者多个由
sep
字符串中一个或者多个分隔符分割的标记。 strtok
函数找到str
中的下一个标记,并将其用\0
结尾,返回一个指向这个标记的指针。(注:strtok
函数会改变被操作的字符串,所以在使用strtok
函数切分的字符串一般都是临时拷贝的内容并且可修改。)strtok
函数的第一个参数不为NULL
,函数将找到str
中第一个标记,strtok
函数将保存它在字符串中的位置。strtok
函数的第一个参数为NULL
,函数将在同一个字符串中被保存的位置开始,查找下一个标记。- 如果字符串中不存在更多的标记,则返回
NULL
指针。
代码举例
#include <stdio.h>
int main()
{
char *p = "zhangsan@hello.com";
const char* sep = ".@";
char arr[30];
char *str = NULL;
//将数据拷贝一份,处理arr数组的内容
strcpy(arr, p);
//初始第一个参数不为NULL, 后面第一个参数为NULL,这样就会接着上次的位置继续往后寻找标记,直到没有找寻标记完毕。
for(str=strtok(arr, sep); str != NULL; str=strtok(NULL, sep))
{
printf("%s\n", str);
}
}
- 上述方式就可以完成特定分隔符对该字符串进行分割,之所以后续使用NULL指针,是因为strtok函数内部有存储上次标记位置的参数,所以直接使用NULL指针再次调用即可往后遍历。
错误信息报告函数
strerror
返回错误码,所对应的错误信息。
char * strerror ( int errnum );
- 我们可以看到这里输出了错误码对应的错误信息,而这些错误码是c语言库函数报错的时候自带的错误码。
使用方法
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main() {
//当我们调用库函数错误时
//错误码将被记录到错误码变量中
//errno -> #include<errno.h>
FILE* pf = fopen("text.txt", "r");
if (pf == NULL) {
printf("%s\n", strerror(errno));
return 1;
}
//读文件
fclose(pf);
pf = NULL;
return 0;
}
- 当库函数错误时,会将对应的错误码放在errno变量中,这个arrno = 2,所以输出了这个结果。
perror
它的使用类似于printf + strerror函数,参数为加入前面的自定义信息,然后输出。
void perror ( const char * str );
使用方法
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main() {
//当我们调用库函数错误时
//错误码将被记录到错误码变量中
//errno -> #include<errno.h>
FILE* pf = fopen("text.txt", "r");
if (pf == NULL) {
perror("zyx:");
return 1;
}
//读文件
fclose(pf);
pf = NULL;
return 0;
}
字符操作函数
字符分类函数
函数 | 如果参数符合下列条件就返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符:空格’ ‘,换页’\f’,换行’\n’,回车’\r’,制表符’\t’或垂直制表符’\v’ |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母af,大写字母AF |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
isalpha | 字母az或AZ |
isalnum | 字母或者数字,az,AZ,0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
使用举例
#include <ctype.h>
#include<stdio.h>
int main()
{
char ch1 = 'A';
int ret1 = isxdigit(ch1);
int ret2 = islower(ch1);
printf("%d\n", ret1);
printf("%d\n", ret2);
}
输出结果
- 我们可以看到第一个函数
isxdigit
可以判断该数是否为16进制的字符。如果是就返回128(非0);如果不是就返回0。 - 第二个函数
islower
可以判断字符是否为小写字母。如果是就返回非0;不是就返回0。
字符转换函数
int tolower ( int c );
int toupper ( int c );
使用举例
#include<stdio.h>
#include <ctype.h>
int main()
{
char arr[] = "Are you ok?";
char* p = arr;
while (*p)
{
if (islower(*p))
{
*p = toupper(*p);
}
p++;
}
printf("%s\n", arr);
return 0;
}
- 遍历字符数组,判断是否为小写字母,如果是就改为大写字母。字符串每个字母都改为了大写字母。
内存操作函数
内存操作时,传入的指针类型是任意的。
memcpy
void * memcpy ( void * destination, const void * source, size_t num );
-
函数
memcpy
从source
的位置开始向后复制num
个字节的数据到destination
的内存位置。 -
这个函数在遇到
'\0'
的时候并不会停下来。 -
如果
source
和destination
有任何的重叠,复制的结果都是未定义的。-
所以当有重叠的时候,当
destination
空间在source
的前面时,应该从前往后拷贝;反之则从后往前拷贝。
-
使用举例
#include<stdio.h>
int main()
{
//初始化时后缀f编译器才会认为是float类型,默认为double类型。
float arr3[] = { 1.0f,2.0f,3.0f,4.0f };
float arr4[5] = { 0.0 };
memcpy(arr4, arr3, sizeof(arr3));
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%.1f ", arr4[i]);
}
}
输出结果
- 内存操作函数可以接收任何类型内存块,如上面所示,一个浮点数的数组,同样可以实现内存的覆盖操作。
memcpy
函数的模拟实现
因为接收的指针类型不固定,当我们想要遍历内存的时候,最好使用char*类型,一个字节一个字节的拷贝即可。(类似于回调函数章节
qsort
函数里面compar
函数的自定义实现)
#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;
}
- 前面介绍了
memcpy
的弊端,我们再看下面这个函数,它就弥补了这一点。
memmove
void * memmove ( void * destination, const void * source, size_t num );
- 和
memcpy
的差别就是memmove
函数处理的源内存块和目标内存块是可以重叠的。 - 如果源空间和目标空间出现重叠,就得使用
memmove
函数处理。
memmove
函数的模拟实现
#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
}
memset
void * memset ( void * ptr, int value, size_t num );
- 用指定的数据类型填充,指向的内存空间,并指定内存块填充的大小。(一个字节一个字节的填充)
- 返回填充后的该内存块。
memcmp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
-
比较从ptr1和ptr2指针开始的
num
个字节 -
-
大概意思与
strcmp
类似,直到找到不同内存块的内容,比较大小判定返回值。如果前面大于后面,返回>0的值;前面小于后面,返回<0的值;若两个内存块遍历完相等,返回0。
使用举例
/* memcmp example */
#include <stdio.h>
#include <string.h>
int main ()
{
char buffer1[] = "DWgaOtP12df0";
char buffer2[] = "DWGAOTP12DF0";
int n;
n=memcmp ( buffer1, buffer2, sizeof(buffer1) );
if (n>0) printf ("'%s' is greater than '%s'.\n",buffer1,buffer2);
else if (n<0) printf ("'%s' is less than '%s'.\n",buffer1,buffer2);
else printf ("'%s' is the same as '%s'.\n",buffer1,buffer2);
return 0;
}