前言
● 从我们第一个C程序——
Hello world
的诞生,到字符串的拷贝、比较等各种操作的实现。从中不难发现:我们在处理C语言时对字符
和字符串
的处理很是频繁,因此学习字符及字符串的各种操作函数尤显其必要性。
● C语言本身是没有字符串类型的,字符串通常放在常量字符串
中或者字符数组
中。字符串常量适用于那些对它不做修改的字符串函数。
● 补充:本章所讲解的函数均为C语言的库函数,如果想了解更多关于C语言库函数的内容,这里给大家推荐一个官网cplusplus.com大家可以参照官网里的文档学习库函数。
废话少说,上干货。Let’s go!
一、求字符串长度-strlen
🍑1.函数声明
size_t strlen ( const char * str );
注释:
1.字符串以
'\0'
作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。最终返回一个无符号的整形,表示字符串的长度。
2.由于strlen 只是计算字符串的长度,不能对原字符串进行更改,所以参数部分采用const
修饰。
2.参数指向的字符串必须要以 ‘\0’ 结束,否则返回随机值。
3.注意函数的返回值为size_t 即 unsigned int是无符号的( 易错 )
🍑2.strlen函数使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
if (strlen("abc") - strlen("abcdef") > 0)
printf("abc>abcdef\n");
else
printf("abc<abcdef\n");
return 0;
}
注释:
我们已知strlen返回的是字符串中‘\0’之前出现的字符个数,即strlen(“abc”)=3,strlen(“abcdef”)=6,但是strlen的返回值为size_t类型,即无符号整数(这里是一个坑),因此无符号整数3-6相当于3的补码加上-6的补码,结果为无符号整数,即一个非常大的数字。因此条件满足,输出abc>abcdef。
🍑3.strlen函数的模拟实现
📝思路一:计数器+遍历查找
#include<assert.h>
size_t mystrlen(const char* str)
{
assert(str);
int count = 0;
while (*str != 0)//找到字符‘\0’的地址
{
str++;
count++;//找到‘\0’之前计数器每次加1
}
//遍历结束,返回字符串长度
return count;
}
📝思路二:递归
#include<assert.h>
size_t mystrlen(const char* str)
{
assert(str);
if (*str != 0)//递归条件
return 1 + mystrlen(str + 1);
else
return 0;
}
📝思路三:指针-指针
#include<assert.h>
size_t mystrlen(const char* str)
{
assert(str);
const char* start = str;//首字符地址
const char* end = str;//尾字符地址
while (*end != 0)//找到字符‘\0’的地址
{
end++;
}
//即‘\0’的地址-首字符地址,指针相减返回之间元素个数
return end - start;
}
二、长度不受限制的字符串函数
顾名思义,这一类函数对字符串进行操作的时候总是以’\0’为基准进行的,而不是以字符串的长度为基准。而长度受限制的字符串函数受长度n的限制。
🍑1.字符串拷贝函数-strcpy
🌳(1)函数声明
char* strcpy(char * destination, const char * source );
注释:
1.会将源字符串中的内容拷贝到目标空间中,包括’\0’。最终返回目标字符串的地址。
2.源字符串必须以 ‘\0’ 结束。
3.目标空间必须足够大,以确保能存放源字符串。
4.目标空间必须可变,即不能为常量字符串。
🌳(2)strcpy函数使用
//使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abc";
const char arr2[] = "hello world!";
strcpy(arr1, arr2);//将arr2的内容拷贝到arr1中
printf("arr1=%s\n", arr1);
printf("arr2=%s\n", arr2);
return 0;
}
🌳(3)strcpy函数的模拟实现
#include<assert.h>
char* my_strcpy(char*dest,const char*src)
{
assert(dest && src);
char* ret = dest;//记录目标空间的起始位置
while (*dest++ = *src++)//拷贝,当*src=0时停止
{
;
}
//拷贝完毕,返回目标空间起始位置
return ret;
}
🍑2.字符串拼接函数-strcat
🌳(1)函数声明
char * strcat ( char * destination, const char * source );
注释:
1.将源字符串的副本附加到目标字符串中。目标字符串中的‘\0’字符将被源字符串的第一个字符覆盖,并且在连接形成的新字符串的末尾包含一个‘\0’字符。最终返回目标字符串的地址。(可以将strcat理解为字符串追加函数,将源字符串追加到目标字符串末尾)
2.源字符串必须以 ‘\0’ 结束。
3.目标空间必须有足够的大,能容纳下源字符串的内容。
4.目标空间必须可修改。
🌳(2)strcat函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "hello ";
const char arr2[] = "world!";
printf("追加前:%s\n", arr1);
printf("追加前:%s\n", arr2);
strcat(arr1, arr2);
printf("追加后: % s\n",arr1 );
return 0;
}
🌳(3)strcat函数模拟实现
char* my_strcat(char* dest, const char* src)
{
assert(dest&&src);
char* ret = dest;//记录目标空间首元素地址
while (* dest!='\0')//找到目标空间dest中‘\0’的地址
{
dest++;
}
//从目标字符串的‘\0’地址处开始拷贝字符串,包括结尾的'\0'
//此过程类似于strcpy函数
while (*dest++ = *src++)
{
;
}
return ret;//返回目标空间首元素的地址
}
🌳(4)思考:strcat能否实现字符串给自己追加?
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "hello";
strcat(arr1,arr1);
printf("%s\n", arr1);
return 0;
}
调试结果:
结论:
strcat在追加时,将末尾的‘\0’字符覆盖,因此在此后追加过程中永远找不到’\0’,即不会正常停止,最终报错:写入冲突。所以strcat函数不能实现自己给自己追加。
🍑3.字符串比较函数-strcmp
🌳(1)函数声明
int strcmp ( const char * str1, const char * str2 );
注释:
1.strcmp函数开始比较两个字符串的第一个字符。如果第一个字符相等,则继续向后比较,直到字符不同或达到终止的‘\0’字符,比较停止。
2.strcmp函数,比较对应位置上的字符大小,而非长度。
3.标准规定:
(1)第一个字符串大于第二个字符串,则返回大于0
的数字
(2)第一个字符串等于第二个字符串,则返回0
(3)第一个字符串小于第二个字符串,则返回小于0
的数字
🌳(2)strcmp函数使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abq";
printf("%d\n",strcmp(arr1, arr2));
//c>q即arr>arr2。vs下返回 -1
char arr3[] = "abcd";
char arr4[] = "abc";
printf("%d\n",strcmp(arr3, arr4));
//d>'\0'即arr3>arr4。vs下返回 1
char arr5[] = "abc";
char arr6[] = "abc";
printf("%d\n",strcmp(arr5, arr6));
//a=a,b=b,c=c,'\0'='\0'即arr5=arr6。vs下返回 0
return 0;
}
🌳(3)strcmp函数模拟实现
#include<assert.h>
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 == *s2)//字符串对应字符相同
{
if (*s1 == '\0')
{
//如果同时为‘\0’,说明两字符串完全相同,返回0
return 0;
}
//否则同时向后偏移,进行下一对字符的比较
s1++;
s2++;
}
//如果不满足对应字符相同则返回二者差值(巧妙地满足标准规定)
return *s1 - *s2;
}
三、长度受限制的字符串函数介绍
🍑1.strncpy
🌳(1)函数声明
char * strncpy ( char * destination, const char * source, size_t num );
注释:
1.表示把source所指向的字符串中以source地址开始的前num个字节复制到destination所指的数组中,并返回被复制后的destination的地址。
2.如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
3.destination必须具有足够大的空间可以容纳拷贝后的字符串。
🌳(2)strncpy函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "xxxxxxxxx";
char arr2[] = "123456";
//当num小于字符串arr2的长度时,拷贝前num个字符到arr1
strncpy(arr1, arr2, 3);
char arr3[] = "xxxxxxxxx";
char arr4[] = "123456";
//当num大于字符串arr4的长度时,超出的部分自动拷贝0,直到达到num个为止
strncpy(arr3, arr4, 7);
char arr5[] = "xxxxxxxxx";
char arr6[] = "123\0ddd";
//当arr6中出现\0时,以\0为arr6字符串的结束标志,超出部分会自动补0
strncpy(arr5, arr6, 4);
printf("arr1=%s\n",arr1);
printf("arr3=%s\n",arr3);
printf("arr5=%s\n",arr5);
return 0;
}
🍑2.strncat
🌳(1)函数声明
char * strncat ( char * destination, const char * source, size_t num );
注释:
1.把source所指字符串的前num个字符添加到destination所指字符串的结尾处,并覆盖destination所指字符串结尾的’\0’,从而实现字符串的连接。最终返回目标字符串的地址。
2.strncat追加后会自动在最后补上’\0’。
3.如果num大于字符串source的长度,那么仅将source指向的字符串内容追加到destination的尾部。
4.目标字符串必须要有足够的空间容纳追加后的新目标字符串。
🌳(2)strncat函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "xxxxxxxxx";
char arr2[] = "123456";
//当num小于arr2字符串长度时,在arr1后追加前num个字符
strncat(arr1, arr2, 3);
printf("arr1 = % s\n", arr1);
char arr3[20] = "xxxxxxxxx";
char arr4[] = "123456";
//当num大于arr4字符串长度时,在arr3后追加arr4字符串
strncat(arr3, arr4, 7);
printf("arr3 = % s\n", arr3);
return 0;
}
补充:与strcat不同,由于strncat追加后会自动在最后补上’\0’,所以可以实现自己给自己追加。
#include<stdio.h>
#include<string.h>
int main()
{
//注意:arr1需要有足够大的空间可以容纳追加后的字符串
char arr1[50] = "12345";
strncat(arr1, arr1,5 );
printf("arr1 = % s\n", arr1);
return 0;
}
🍑3.strncmp
🌳(1)函数声明
int strncmp ( const char * str1, const char * str2, size_t num );
注释:
1.比较直到出现对应位置上的字符不一样,或者一个字符串结束,或者num个字符全部比较完。
2.strncmp函数,同样比较对应位置上的字符大小,而非长度。
3.标准规定:
(1)第一个字符串大于第二个字符串,则返回大于0的数字
(2)第一个字符串等于第二个字符串,则返回0
(3)第一个字符串小于第二个字符串,则返回小于0的数字
🌳(2)strncmp函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abce";
char arr2[] = "abcdefg";
//比较前三个字符,a=a,b=b,c=c,即前三个字符相同,返回0
printf("%d\n",strncmp(arr1, arr2, 3));
char arr3[20] = "aca";
char arr4[] = "abcdefg";
//比较前2个字符,a=a,a<b,即arr3第2个字符小,vs下返回1
printf("%d\n", strncmp(arr3, arr4, 2));
char arr5[20] = "a";
char arr6[] = "abcdefg";
//由于num>strlen(arr1)+1,比较前2个字符,a=a,'\0'<b,即arr1的第2个字符小,vs下返回-1
printf("%d\n", strncmp(arr5, arr6, 3));
return 0;
}
四、字符串查找与分割
🍑1.字符串查找函数-strstr
🌳(1)函数声明
char * strstr ( const char *str1, const char * str2);
注释:
1.判断字符串str2是否是str1的子串。如果是,则该函数返回 str1字符串从 str2第一次出现的位置开始到 str1结尾的字符串;否则,返回
NULL
。
2.strstr函数在查找时,大小写会被认为是不同的字符串。
🌳(2)strstr函数的使用
📝用于判断一个字符串中是否含有目标字符子串
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "bcd";
char* p = strstr(arr1,arr2);
if (p == NULL)
{
printf("不存在\n");
}
else
{
printf("%s\n", p);
}
return 0;
}
🌳(3)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 (*str2 == '\0')//特殊处理
{
//如果str2是空串,直接返回str1
return str1;
}
while (*p)//判断str1是否遍历完
{
s1 = p;
s2 = str2;
while (*s1 != '\0' && *s2 != '\0' && (*s1 == *s2))
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return (char*)p;//找到了,返回在str1中的开始地址
}
p++;//上一次循环没找到,查找位置向后偏移
}
//str1中不存在str2,找不到子串
return NULL;
}
🍑2.字符串分割函数-strtok
🌳(1)函数声明
char * strtok ( char * str, const char * sep );
注释:
1.sep参数是一个字符串,定义了用作分隔符的字符集合。
2.第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标 记。
3.strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容 并且可修改。)
4.strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置。
5.strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标 记。
6.如果字符串中不存在更多的标记,则返回 NULL 指针。
🌳(2)strtok函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char buf[50] = { 0 };
char arr[] = "www.The calf wants to turn [email protected]";
strcpy(buf, arr);//将数据拷贝一份,不改变源数据
char* flag = ".@ ";//分隔符集合
int count = 0;
//根据strtok函数特点:
//第一次第一个参数不为NULL,向后查找分隔符,找到改为'\0',返回分隔符前的子字符串,并保存这一次strtok走到的位置。
//第二次令第一个参数为NULL,strtok会从上一次保存的位置开始向后查找分隔符,找到改为'\0'并返回第二个子字符串的地址。
//第三次……
for (char* str = strtok(buf, flag); str != NULL; str = strtok(NULL, flag))
{
count++;
printf("分割%d=%s\n",count,str);
}
return 0;
}
五、错误信息报告
🍑1.错误报告函数-strerror
🌳(1)函数声明
char * strerror ( int errnum );
注释:
1.返回错误码,所对应的错误信息。
2.与错误码变量errno
搭配使用。(errno 是记录系统的最后一次错误代码)
🌳(2)strerror函数使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
//使用错误码变量需要包含头文件
#include<errno.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
//库函数调用失败之后,会把错误码记录到错误码变量中——errno
if (pf == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
拓展:与strerror函数类似,使用perror
函数也可显示相应错误信息。perror相当于printf+strerror,同时参数可添加提示词。
代码展示
#include<errno.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("错误信息");
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
六、字符操作函数
🍑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 | 任何图形字符 |
🍑2.字符操作函数的简单使用
//注意:使用库函数需要包含头文件
#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;
}
这些字符操作函数针对单个字符进行操作,它们的参数返回值以及功能都非常简单。这里就不再一一介绍了。
七、内存操作函数
🍑1.内存拷贝函数-memcpy
🌳(1)函数声明
void * memcpy ( void * destination, const void * source, size_t num );
注释:
1.函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
2.这个函数在遇到 ‘\0’ 的时候并不会停下来。
3.如果source和destination有任何的重叠,复制的结果都是未定义的。(即:memcpy并不支持内存空间有重叠的复制)
🌳(2)memcpy函数的使用
//拷贝arr1中的前8个字节到arr2中
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
float arr1[] = { 1.0f,2.0f,3.0f,4.0f };
float arr2[5] = { 0.0f };
memcpy(arr2, arr1,8);
return 0;
}
🌳(3)memcpy函数模拟实现
#include<assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
while (num--)//拷贝字节数
{
//每次拷贝1个字节,所以将其转换为(char*)指针
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
🍑2.内存拷贝函数-memmove(可重叠)
🌳(1)函数声明
void * memmove ( void * destination, const void * source, size_t num );
注释:
1.和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
2.如果源空间和目标空间出现重叠,就得使用memmove函数处理。
🌳(2)memmove函数的使用
//从arr中拷贝20个字节到arr+2
//注意:使用库函数需要包含头文件
#include<string.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr+2,arr,20);
return 0;
}
🌳(3)memmove函数的模拟实现
分析:
📝代码实现
#include<assert.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest);
assert(src);
if (dest < src)//src前——>后拷贝
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
else//src后——>前
{
while(num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
🍑3.内存比较函数-memcmp
🌳(1)函数声明
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
注释:
1.比较从ptr1和ptr2指针开始的num个字节
2.标准规定:
(1)ptr1对应的字节内容>ptr2对应字节内容,返回大于0的数字
(2)两个内存块内容相同,返回0
(3)ptr1对应的字节内容<ptr2对应字节内容,返回小于0的数字
🌳(2)memcmp函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {1,2,3,0,0};
int ret = memcmp(arr1, arr2, 13);
//分析:前12个字节内容相同,第13个字节内容00000100>00000000
printf("%d\n", ret);
return 0;
}
🍑4.内存设置函数-memset
🌳(1)函数声明
void * memset ( void * ptr, int value, size_t num );
注释:
1.将ptr中当前位置后面的num个字节用 value 替换并返回ptr。
2.注意是以字节(byte
)为单位。ptr必须具有足够的空间容纳替换后的ptr。
🌳(2)menset函数的使用
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
char str[] = "xxxxxxxxxx";
memset(str,'0', 5);//表示将str的前5个字节设置成字符0
printf("%s\n", str);
return 0;
}
总结
本章主要对字符串以及字符操作函数进行了一个较为全面的介绍,篇幅较长,但干货满满。建议大家反复观看,慢慢食用😊