目录
上一篇讲了:
一、求字符串长度
1.1strlen
1.2注意:
二、长度不受限制的字符串函数
2.1strcpy
2.2strcat
2.3strcmp
三、长度受限制的字符串函数
3.1strncpy
3.2strncat
3.3strncmp
详见: 大龄失业学生转码日记:2022-12-03(字符函数与字符串函数一)
一、字符串查找
1.1strstr
字符串查找函数,作用是:两个字符串str1、str2;在str1中找str2这个子串,如果在str1中找到str2这个子串了,就返回子串的起始地址;如果在str1中多次找到str2,这个子串,则返回的是第一次找到子串的起始位置;如果找不到str2这个字符串则函数就返回空指针。
char * strstr ( const char *str1, const char * str2);
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdefabcdef";
char arr2[] = "bcd";
//在arr1中找arr2,返回值类型是char*
char* ret= strstr(arr1, arr2);
if (NULL == ret)
{
printf("没找到\n");
}
else
{
//如果能找到就返回b的地址,则打印字符串就会打印bcdefabcdef这一串
printf("%s\n", ret);//从ret地址向后打印这个内容
}
return 0;
}
结果

#include <stdio.h>
int main()
{
char arr1[] = "abcdefabcdef";
char arr2[] = "bcdq";
char* ret= strstr(arr1, arr2);
if (NULL == ret)
{
printf("没找到\n");
}
else
{
printf("%s\n", ret);
}
return 0;
}
结果

注意:子串一定是连续的,strstr()函数是找连续完整的子串。
模拟实现strstr()函数:
思路:
让s1和s2向后走,str记住它整个字符串的起始位置;substr记住它整个子串字符串的起始位置。cur指针最开始指向str的起始位置。
首先认为str从起始位置有可能找到子串(bbc)所以把cur赋给s1,即s1指向a,s就赋给子串的起始位置。
s1和s2各自指向的是a和b并不相等,意味着这个位置开始是不肯能匹配成功,所以cur不用记住这个位置,则让cur指针向后走指向b,此时就假设从这个位置有可能匹配成功,就把cur赋给s1,这里s2回到起始位置指向b,s1指向的b和s2指向的相等,让s1和s2均各自向后走,二者均指向的是b,又相等,则二者均各自向后走一步,发现s1此时指向的是b,而s2指向的是c,二者不相等,说明从这一次cur的位置开始匹配是不可能成功的,所以让cur向后走一步指向b,此时让s1回到cur指向的位置处,s2还回到整个子串的起始位置处指向b,s1和s2指向的相等,二者均各走向后走一步,都是b相等,二者均各自又向后走,都是c相等,二者均各自向后走一步,发现s2遇到\0,说明已经找到子串。
在cur不断向后走的过程中,cur指向的字符串还没空(还没指向\0,或者说cur还不是空指针)说明还有字符可以被查找,还有可能匹配成功。
while循环停止的条件:s1和s2不相等;s2中遇到\0(找到子串,应该停下);s1遇到\0(被查找的字符串已经查找完,不可能找到子串了)。
s1和s2比较的前提是二者均不是(二者均还没遇到)\0。
#include <stdio.h>
#include <assert.h>
char* my_strstr(const char* str, const char* substr)
{
const char* s1 = str;
const char* s2 = substr;
const char* cur = str;
assert(str && substr);
if (*substr == '\0')
{
return (char*)str;
}
while (*cur)
{
s1 = cur;
s2 = substr;
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
//这里也可以写成:while(*s1 && *s2 && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return (char*)cur;
}
cur++;
}
return NULL;
}
int main()
{
char arr1[] = "abbbcdef";
char arr2[] = "bbc";
char* ret = my_strstr(arr1, arr2);
if (NULL == ret)
{
printf("没找到\0");
}
else
{
printf("%s\n", ret);
}
return 0;
}
结果

1.2strtok
可以把一个字符串切割成几个字符串。切割是按照分割符切的。
举例:wudi@xiao.lei
这一串字符串是由@和.这两个符号分割开的;
若想现在从这一串字符串中提取非分割字符:
提取wudi;
提取xiao;
提取lei。
此时就用strtok()这个函数。
char * strtok ( char * str, const char * sep );
- sep参数是个字符串,是一个char*的指针,指向了一个字符串,定义了用作分隔符的字符集合,如例子中的@和.把这两者放在一起,即分割符的集合就是“@.”(无所谓顺序,包含起来就可以)。
#include <stdio.h>
int main()
{
const char* p = "@.";
//调用strtok()函数时:
strtok(, p);//第二个参数就可以传p,p指向的就是分隔符的集合
return 0;
}
- 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
#include <stdio.h>
#include <string.h>
int main()
{
const char* p = "@.";
//调用strtok()函数时:
char arr[] = "wudi@xiao.lei";//arr数组中的字符串是由@和.这两个分隔符分开的各个字符串
strtok(arr, p);//第二个参数就可以传p,p指向的就是分隔符的集合
return 0;
}
}
"wudi@xiao.lei"中“keli”是一个标记,这个字符串里可以有一个标记也可以有多个标记也可以没有标记,如空字符串也行,标记是由一个或多个分隔符分割开的。这里arr就是strtok()函数的第一个参数。
- strtok()函数的功能:
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。
如例子中的wudi是一个标记,xiao是一个标记,lei是一个标记(也可以理解为子段),当strtok()函数第一次作用于wudi@xiao.lei这个字符串的时候,它会找到其中一个标记,如第一个标记“wudi”,strtok()函数会把后面的@符号改成\0,并且返回一个指向这个标记的指针,即返回的是w的地址。
所以第一次调用strtok()函数返回k的地址并且把@置换成\0,即从w的地址向后wudi就可以打印出来——拿到了一个字符串。
- strtok函数会改变被操作的字符串,(如例子中把@改成\0)所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。意思是如本身想处理arr数组“wudi@xiao.lei”,从里面提取各个字段(标记),一般不会直接处理arr本身,一般会创建一个临时数组如buf,把arr中的内容拷贝放到临时数组buf中,接下来的操作对象就是buf,buf数组里放的就是“wudi@xiao.lei”这个内容,可以修改buf里面的内容,并且修改buf里面的内容不会影响到arr数组。
#include <stdio.h>
#include <string.h>
int main()
{
const char* p = "@.";
//调用strtok()函数时:
char arr[] = "wudi@xiao.lei";
char buf[50] = { 0 };
strcpy(buf, arr);
strtok(arr, p);
return 0;
}
-
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
如例子中strtok()函数传参传的是arr,是"wudi@xiao.lei",则此时arr是一个有效的地址,不是空指针,strtok()函数就会找到arr数组中的第一个标记:wudi,strtok()函数就会记住wudi这个标记的位置。 -
strtok()函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
意思是:有可能给arr数组传的是一个NULL空指针,strtok()函数记住wudi这个标记的位置后就会从wudi后面找下一个标记。而当strtok()函数的第一个参数是空指针时还是会从拷贝的buf同一个字符串里面找被保存位置开始查找下一个标记,遇到.就找到了标记,找到之后也会把.置换成\0,然后返回y的地址,则就可以打印出xiao这个子段。
这是第一个参数是空指针时会往后走。
strtok()函数第一个参数不是空指针时,strtok找到的是第一个标记,从第二个标记往后的标记要查找的话,第一个参数就要为空指针,因为当第一个参数为空指针时函数将在同一个字符串被保存位置开始找下一个标记,当把下一个标记找到时,就会把这个标记位置保存好然后下一次再调用strtok()函数时传第一个参数时,第一个参数为空时就会从被保存位置再找下一个标记,这样才能把后面所有的标记找出来。
如果字符串中不存在更多的标记,则返回 NULL 指针。
综上:
strtok()函数的使用习惯:
strtok()函数找第一个标记的时候,函数的第一个参数不是NULL;
strtok()函数找非第一个标记的时候,函数的第一个参数是NULL。
#include <stdio.h>
#include <string.h>
int main()
{
const char* p = "@.";
char arr[] = "wudi@xiao.lei";
char buf[50] = { 0 };
strcpy(buf, arr);
char* str = NULL;//创建一个char*的空指针,初始化为NULL
for (str = strtok(buf, p); str != NULL; str = strtok(NULL, p))
{
printf("%s\n", str);//打印str地址所指向的内容
}
return 0;
}
结果

二、错误信息报告
2.1streror
返回错误码所对应的错误信息。
char * strerror ( int errnum );
参数是整型错误码,返回值是char*类型指针。
在C语言中规定了一些错误信息,错误信息由两部分组成。一个是错误码,一个错误码对应一个错误信息,如0在C语言中代表No Error,1代表一个意思,2代表一个意思,3代表一个意思……C语言中规定了很多错误信息,0是错误码,"No Error"是0所对应的错误信息。把错误码翻译成它所对应的错误信息就会便于理解。
strerror()函数就可以把错误码翻译成错误信息。
strerror()函数的使用方法:
给一个错误码就会返回错误码所对应错误信息的起始地址。如给0返回的就是No Error这个字符串的起始地址。
用strerror()函数测试0-9的错误信息:
#include <stdio.h>
#include <string.h>
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%s\n", strerror(i));
}
return 0;
}
结果

错误码——错误信息
0 —— No error
1 —— Operation not permitted
2 —— No such file or directory
3 —— No such process
4 —— Interrupted function call
5 —— Input/output error
6 —— No such device or address
7 —— Arg list too long
8 —— Exec format error
9 —— Bad file descriptor
错误信息本来就是常量字符串。
使用场景:
C语言可以操作文件:打开文件有fopen()函数:
fopen()函数:
参数分别是:文件名、打开文件的方式;
返回方式是FILE*类型的指针。
如果使用这个函数,则需要确定打开的文件和打开文件的方式和返回值。
#include <stdio.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
//检测:
//打开失败:
if (NULL == pf)
{
//pf指向空指针则说明出错,但若是不知道什么原因(文件不存在或权限不够……)
//打印出错的原因:
//strerror();//把错误码翻译成错误信息,错误码是什么?
//当库函数使用的时候,发生错误会把errno这个全局变量的错误信息设置为本次执行库函数产生的错误码
//这里调用fopen()函数失败就会把errno这个全局变量的错误信息设置为本次执行函数产生的错误码
//errno是C语言提供的全局变量,可以直接使用,放在errno.h文件中,所以使用errno需要包含头文件<errno>
printf("%s\n",strerror(errno));//返回的是char*
return 0;
}
//打开成功:
//则读文件
//……
// ……
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//No such file or directory(这里打印的是发生错误的原因)
//因为没有创建这个文件夹
//因为在工程路径下并没有text.txt这个文件,则以“r”的方式打印一定会
//打印失败,因为fopen()函数调用失败时会返回空指针
//若此时去创建这个文件则读取成功运行成功(不打印什么)
当一个函数调用完之后,分析错误原因时就可以在函数调用完之后,在后面用strerror()函数,传错误码,就会返回这个错误码所对应的错误信息,然后就会打印这个错误信息。
注意errno是全局变量,是全局共用的,当检测一个函数调用完之后的错误之后,同一个项目中检测下一个函数若发生错误就会更新errno,就是其他函数(程序)的错误提示了。
三、字符操作
3.1字符分类函数
函数 如果它的参数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’
isdigit 十进制数字 0~9
isxdigit 十六进制数字,包括所有十进制数字,小写字母af,大写字母AF
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母az或AZ
isalnum 字母或者数字,a-z,0-9,A-Z
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
这些函数可以判断一个字符的种类,头文件均是<ctype.h>。
举例:isspace()函数
——判断一个字符是否是空白字符,即在屏幕上看不见打印了什么,比如空格就是空白字符。
int isspace( int c );
参数就是所判断的字符,得到的是该字符的ASCII码值,返回类型是int。
isspace returns a non-zero value if c is a white-space character (0x09 – 0x0D or 0x20). iswspace returns a non-zero value if c is a wide character——参考MSDN部分
如果参数c是一个空白字符时,isspace()函数就返回一个非0值;
如果参数c不是一个空白字符时,isspace()函数就会返回一个0。
#include <stdio.h>
#include <ctype.h>
int main()
{
printf("%d\n", isspace(' '));//空白字符
return 0;
}
结果

#include <stdio.h>
#include <ctype.h>
int main()
{
printf("%d\n", isspace('!'));
return 0;
}
结果

用处——判断获得的字符是不是空白字符:
#include <ctype.h>
#include <stdio.h>
int main()
{
char ch = 'X';
//空白字符就进入:
if (isspace(ch))
{
printf("wudi\n");
}
//不是空白字符:
else
{
printf("wudi+1\n");
}
return 0;
}
结果

其他函数的使用和isspace()函数类似。
判断一个数字是否是数字字符:
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = '0';
if (isdigit(ch))
{
printf("wudi\n");
}
else
{
printf("wudi+1\n");
}
return 0;
}
结果

3.2字符转换函数——tolower、toupper
tolower()函数
——把大写字母转换为小写
int tolower ( int c );
toupper()函数
——把小写字母转换为大写
int toupper ( int c );
这两个函数的头文件均是<ctype.h>
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = 0;
ch = 'w';//获取一个字符放到ch中
//ch如果是小写就转大写,如果是大写就转小写:
if (islower(ch))//是小写
{
//转大写:
ch = toupper(ch);
}
else
{
//转小写:
ch = tolower(ch);
}
printf("%c\n", ch);
return 0;
}
结果

四、内存操作函数
4.1memcpy
从源数据拷贝num个数据到目标空间中。从source的位置开始向后复制num个字节的数据到destination的内存位置。
void * memcpy ( void * destination, const void * source, size_t num );
num是指拷贝字节的个数,单位是字节,类型是无符号整型。
拷贝的对象可能是整型、字符串、结构体等数据,void*的指针可以接收任意数据的地址。
#include <stdio.h>
#include <string.h>
int main()
{
/*char arr1[] = "abcdef";
char arr2[] = { 0 };*/
//把arr1中的内容拷贝放到arr2中——用字符串拷贝函数
//strcpy()函数是用来拷贝字符串的,拷贝字符串时找\0
/*strcpy(arr2, arr1);*/
int arr3[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr4[5] = { 0 };
//把arr3中的前5个元素拷贝放到arr4中
//因为arr3是整型数据,不是字符串,不能用拷贝字符串的strcpy函数
//——memcpy()函数不关心拷贝的数据类型
//拷贝5个整型,则是20个字节
//memcpy(arr4, arr3, 20);//可以计算20的:
memcpy(arr4, arr3, 5 * sizeof(arr3[0]));
//看arr4中的内容:
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", arr4[i]);
}
return 0;
}//1 2 3 4 5
//若拷贝arr3中的6,7,8,9,10则需要把arr3中6的地址作为源数据,则是:
//memcpy(arr4, arr3+5, 5 * sizeof(arr3[0]));
//arr3+0指向的是1,传的参数是数组名,是首元素地址
结果

这个函数在遇到 ‘\0’ 的时候并不会停下来。
如果source和destination有任何的重叠,复制的结果都是未定义的。
注意memcpy()函数不能倒着拷贝,只要是内存块中的数据都可以用此函数拷贝。
若把char类型的数组放到整型数组中,存放数据的都是内存块可以拷贝,一个int4个字节,1个char1个字节,则拷贝过去就相当于一个整型空间里放了四个字符,这4个字符的ASCII码值合起来组合成一个整型,按照整型的方式解读出来就是一个整型的值。
4.2memmove
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
void * memmove ( void * destination, const void * source, size_t num );
#include <stdio.h>
#include <string.h>
void test1()
{
int arr3[] = { 1,2,3,4,5,6,7,8,9,10 };
//my_memcpy(arr3+2, arr3, 5 * sizeof(arr3[0]));
memmove(arr3 + 2, arr3, 5 * sizeof(arr3[0]));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr3[i]);
}
}
int main()
{
test1();
return 0;
}
结果

模拟实现memmove()函数:
举例:
①若把1, 2, 3, 4, 5拷贝放到3, 4, 5, 6, 7中:
思路:
不用额外创建空间,正不行则倒着放:
把5放到7位置上,把4放到6位置上,把3放到5位置上,把2放到4位置上,把1放到3置上,即完成了1,2,3,4,5放到3,4,5,6,7上。
拷贝顺序:
拷贝5,拷贝4,拷贝3,拷贝2,拷贝1(倒顺序的源数据)
②若把3,4,5,6,7拷贝放到1,2,3, 4,5中:
此时倒着拷贝就会内存重叠,需要正着从前向后拷:
拷贝3,拷贝4,拷贝5,拷贝6,拷贝7(正顺序的源数据)
则不同情况不同的分析:

注意:是源数据拷贝到目标空间中,内存重叠的这块空间既可能是源空间又可能是目标空间,但是const修饰的是指针,限制的是不能通过指针改变所指向的空间内容(内存)而不是限制内存。
dest没有被const修饰限制,即dest没有被保护,所以可以通过dest指针去改变它指向空间的内容。
const修饰src指针,限制的是src,所以是不能通过src指针去改变所指向空间内容的任何数据,但是那块空间内容没有被限制死。
若思路是临时创建一块空间,则需要先拷贝,后拷贝回来,操作麻烦,时间和空间都浪费,所以没必要单独增加一块空间。
现在分析:什么情况下从前向后拷贝,什么情况下从后向前拷贝?

4.3memset
设置内存。把一块内存以字节为单位设置成自己想要的值。
void *memset( void *dest, int c, size_t count );
dest——指针指向的目的地;
c——要被设置的字符,以字节为单位;
count——字符的个数。
#include <stdio.h>
#include <string.h>
int main()
{
//memset()函数针对字符数组:
char arr[15] = { 0 };
//把从arr开始,把arr的前10个字节改成字符x:
memset(arr, 'x', 10);
//memset()函数针对整型数组:
int brr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//把从arr开始,把arr的前10个字节全部改成0:
memset(brr, 0, 10);//改了3的前两个字节
int crr[10] = { 1,2,3,4,5,6,7,8,9,10 };
memset(crr, 'x', 10);
return 0;
}//78是十六进制,写成十进制是120,是小写的x的ASCII码值
memset()可以改整型为字符,根据需求都可以设置。
4.4memcmp
内存比较,从ptr1和ptr2指针开始向后比较num个字节,一对字节一对字节地比较,最多比较num对。
int memcmp ( const void * ptr1,const void * ptr2,size_t num );
与strcmp()相似,但是strcmp()函数比较到\0就停止了,memcmp并不在乎有没有\0,执行的就是比较num个字符,比较num个字节。
memcmp()函数的返回值:<0、>0、=0的数字,和strcmp()函数一样。
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3,4,5 };
int ret = memcmp(arr1, arr2, 6);//比较前6个字节,2里面比较一半
int cmp = memcmp(arr1, arr2, 8);//比较的是前8个字节,即比较的是1和2
printf("%d %d\n", ret, cmp);
return 0;
}
结果

#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,7,4,5 };
int arr2[] = { 1,2,3,4,5 };
int ret = memcmp(arr1, arr2, 8);//比较的是前8个字节,即比较的是1和2
int cmp = memcmp(arr1, arr2, 9);//比较的是前9个字节
printf("%d %d\n", ret, cmp);
return 0;
}
结果

比较的是:在内存中一对字节中放的数据谁大谁小。
五、总结
memcpy()函数——拷贝num个字节
memmove()函数——拷贝num个字节
memset()函数——设置num个字节
memcmp()函数——比较num个字节
这四个函数都是以字节为单位的,头文件均是<string.h>。
本文介绍了C语言中字符串操作的基础知识,包括字符串查找、切割、错误信息报告等,并详细讲解了字符分类与转换函数。此外,还深入探讨了常用的内存操作函数,如memcpy、memmove、memset和memcmp的使用方法及注意事项。

被折叠的 条评论
为什么被折叠?



