字符函数与字符串函数
导言
大家好,很高兴又和大家见面了!!!
从咱们学习C语言的开始,我们就接触了一个数据类型——字符类型。并且在之后的学习过程中,我们经常与这一类型的元素打交道,如字符变量、字符数组、字符指针……
与这些类型密切相关的就是字符与字符串,我们经常要对这些字符和字符串进行一些操作,如字符小写转大写、判断是不是小写字符、计算字符串长度……
为了方便程序猿来处理这些字符和字符串,C语言为程序猿提供了一系列的库函数,这就是我们今天要介绍的字符函数与字符串函数;
一、字符分类函数
字符,可以简单的理解为只要是键盘上能敲出来的都是字符,前面我们有介绍一个内容——ASCII码表。
从表中我们可以看到这里面的字符有各式各样的,这些字符分为两大类——控制字符与打印字符。而打印字符又分为数字字符、标点符号、小写字符、大写字符……对于这些字符,C语言提供了一类专门用于进行字符分类的函数,如下所示:
iscntrl——判断是不是控制字符
isspace——判断是否为空白字符
isdigit——判断是否是数字字符
isxdigit——判断是否是十六进制数字字符
islower——判断是否是小写字母
isupper——判断是否是大写字母
isalpha——判断是否是字母
isalnum——判断是否是字母或者数字
ispunct——判断是否是标点符号
isgraph——判断是否是图形字符
isprint——判断是否是可打印字符,包括图形字符和空白字符
这些函数都是收录在头文件<ctype.h>
中,所以我们在使用这些函数时,需要引用这个头文件。
1.1 字符分类函数的用法
这些函数的用法十分相似,使用的基本逻辑就是在传入想要分类的字符后,通过函数的返回值来判断是否为对应的函数类型:
- 符合条件返回非零值;
- 不符合条件返回0;
这里我们以islower
函数为例来说明这类函数的用法。我们先打开MSDN来看看这个函数:
islower
函数的原型如下所示:
int islower(int c);
其他的字符分类函数的原型和islower
函数一样,都是返回类型为整型,参数为整型的库函数。之所以参数为整型,是因为这些函数是根据这些字符的ASCII码值来进行分类判断的,如小写字符的ASCII码值是在97-122,如果我们不使用islower
,我们就可以如下进行编码:
int main()
{
char a = 0;
scanf("%c", &a);
if (a >= 97 && a <= 122)
printf("%c的ASCII码值为%d,是小写字母\n", a, a);
else
printf("%c的ASCII码值为%d,不是小写字母\n", a, a);
return 0;
}
如果我们使用islower
函数,我们则可以写成:
int main()
{
char a = 0;
scanf("%c", &a);
if(islower(a))
printf("%c的ASCII码值为%d,是小写字母\n", a, a);
else
printf("%c的ASCII码值为%d,不是小写字母\n", a, a);
return 0;
}
下面我们来运行一下这个代码:
其他的字符分类函数的用法和islower
如出一辙,大家只要在使用时别忘记头文件<ctype.h>
就行了。
二、字符转换函数
与字符分类函数相同,C语言为程序猿提供了两个用来进行字母大小写转换的函数——tolower
、toupper
。我们同样通过MSDN来认识一下这两个函数:
从这里的介绍可以看到,tolower
与toupper
这两个函数与前面的字符分类函数一致,都是一个返回类型为整型,参数类型为整型的库函数,它们的作用就是进行字符的大小写转换。下面我们就来看一下它们的用法;
2.1 字符转换函数的用法
我们通过ASCII码表可以知道,大写字母与其对应的小写字母的ASCII码值相差32,也就是说,如果我们不借用这个字符转换函数的话,我们可以通过字母±32来达到转换大小写的目的,如下所示:
int main()
{
char a = 0;
scanf("%c", &a);
printf("转换前字符为%c,对应的ASCII码值为%d\n", a, a);
if (a >= 'A' && a <= 'Z')
a += 32;
else if (a >= 'a' && a <= 'z')
a -= 32;
printf("转换后字符为%c,对应的ASCII码值为%d\n", a, a);
return 0;
}
如果我们通过函数来进行转换,我们就可以编写:
int main()
{
char a = 0;
scanf("%c", &a);
int b = 0;//接收转换后的值
if (islower(a))
{
b = toupper(a);
}
else if (isupper)
{
b = tolower(a);
}
printf("转换前字符为%c,对应的ASCII码值为%d\n", a, a);
printf("转换后字符为%c,对应的ASCII码值为%d\n", b, b);
return 0;
}
根据MSDN的描述,此时转换后的值是字符a的一个副本,也就是说,字符转换函数并未改变a的值,下面我们就来测试一下:
从测试结果中我们可以得到以下信息:
- 字符转换函数不是直接对操作对象进行转换,而是额外生成了一个副本;
- 当操作对象不符合条件时,不会对其进行任何操作;
字符分类函数与字符转换函数都是比较简单的函数,相信大家现在都能理解并会使用这两个函数了,这里我还是要提醒大家一下几点:
- 别忘记引用对应的头文件
<stdlib.h>
和<ctype.h>
; - 字符转换函数的返回值别忘记使用变量来接收,否则会报错。
三、字符串函数
字符串我们已经介绍过多次了,它的定义是由双引号引起的一个或多个字符就叫做字符串。我们在前面的学习中,对字符串掌握了以下知识点:
'\0'
是字符串的结束标志;- 字符串自带一个
'\0'
; ""
这个是空字符串,字符串里的元素只有一个'\0'
;
对于字符串,我们也需要对它进行一些操作,如计算字符串长度、两个字符串之间比较大小……C语言为了提高程序猿的编程效率,它为程序猿提供了一系列用来对字符串进行操作的函数,简称字符串函数。这些函数都位于头文件<string.h>
中,我们在使用这些字符串函数时,需要引用这个头文件;
3.1 成员
下面我们通过网站cplusplus.com来看一下在<string.h>这个头文件中有哪些库函数:
今天我们将介绍这其中的部分字符串函数,按它们各自的功能分类,有以下几类:
- 求字符串长度——
strlen
- 字符串拷贝——
strcpy
、strncpy
- 追加字符串——
strcat
、strncat
- 字符串比较——
strcmp
、strncmp
- 查找子字符串——
strstr
- 拆分字符串为标记——
strtok
- 获取错误信息字符串——
strerror
下面我们将一一介绍这些字符串函数;
3.2 strlen函数
strlen
的全称是string length
——字符串长度,这个函数是专门用来求取字符串长度的库函数。
为了更详细的介绍strlen
,这里我借助MSDN来获取strlen
函数的相关信息:
在使用strlen
函数时,通过这个介绍,我们可以得到以下几点信息:
strlen
函数是通过读取\0
进而计算\0
前的字符个数(不包含'\0'
);strlen
的返回值是size_t类型的值;
这里可能就会有朋友有疑问了,size_t
是一个什么类型,下面我们一起来探讨一下;
3.2.1 size_t类型
这里是通过C++图书馆的网站上找到的解释,可以看到,它是一种无符号整型,也就是说,这种类型的整数的二进制没有符号位,下面我们来测试一下:
有没有发现,同样是3-6,但是得出来的结果却截然不同,下面我们来通过二进制序列分析一下:
//-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——补码
前面我们有介绍过,整型在内存中都是通过补码进行存储的,此时计算机拿到的是-3的补码:
- 对于有符号整型来说,计算机在拿到-3的补码之后,会将其转化为原码输出;
- 但是对于无符号整型来说,原码=反码=补码,所以,计算机直接将其进行输出;
将其转化为十进制的话就能得到式子:
S
32
=
2
0
+
2
1
+
2
2
+
…
…
+
2
29
+
2
30
+
2
31
−
2
1
S_{32}=2^0+2^1+2^2+……+2^{29}+2^{30}+2^{31}-2^1
S32=20+21+22+……+229+230+231−21
根据等比数列的求和公式:
S
n
=
a
1
∗
(
1
−
q
n
)
/
(
1
−
q
)
S_n=a_1*(1-q^n)/(1-q)
Sn=a1∗(1−qn)/(1−q)
我们很容易的得到式子:
S
32
−
2
1
=
a
1
∗
(
1
−
q
32
)
/
(
1
−
q
)
−
2
1
=
1
∗
(
1
−
2
32
)
/
(
1
−
2
)
−
2
=
2
32
−
3
S_{32}-2^1=a_1*(1-q^{32})/(1-q)-2^1=1*(1-2^{32})/(1-2)-2=2^{32}-3
S32−21=a1∗(1−q32)/(1−q)−21=1∗(1−232)/(1−2)−2=232−3
最终我们就得到了测试结果。相信大家经过这个探讨,应该都能理解什么事无符号整型了。下面我们继续介绍strlen
;
3.2.2 strlen的易错点
因为strlen
的返回值是一个无符号整型,很多朋友可能都会忘记这个点,所以容易导致一些错误,如下所示:
//strlen的易错点
int main()
{
if (strlen("abc") - strlen("abcd") < 0)
printf("小于\n");
else
printf("大于\n");
return 0;
}
字符串"abc"的长度为3,字符串"abcd"的长度为4,将这两个长度作差,我们可以得到的是什么结果呢?
可以看到,此时得到的是大于,这就是因为strlen
的返回值为无符号整型,正常我们进行整型运算3-4得到的是-1,但是在无符号整型进行运算的时候会得到一个很大的数字。
strlen
函数我们在之前有过简单的介绍它的使用,今天我们来详细介绍一下;
3.2.2 strlen的使用
strlen
的使用比较简单,它的参数是一个字符指针,既然是指针,我们对其传参时可以是字符数组的数组名、也可以是字符串,还可以是存储常量字符串的字符指针,它会计算字符串中字符的数量,但是不包括'\0'
,如下所示
下面我们来使用一下strlen
计算字符串、字符指针和字符数组:
有一点我们一定要注意:
- 参数指向的字符串必须以
'\0'
结束,否则strlen
将会返回一个随机值;
如下所示:
可以看到,在数组ch1
中并没有'\0'
,此时strlen
会对数组进行越界访问,直到找到一个'\0'
才会停止。
现在大家应该都直到如何使用strlen
了,不过有一个操作符——sizeof——计算操作数所占内存空间大小
,可能有朋友容易将他俩搞混,下面我们来介绍一下它们之间的区别;
3.2.3 strlen与sizeof
现在我们来测试一下strlen
和sizeof
:
我们通过表格的形式来更加直观的介绍它们之间的区别:
区别 | sizeof | strlen |
---|---|---|
性质不同 | sizeof是一个操作符,不需要引用头文件 | strlen是一个库函数需要引用头文件<string.h> |
工作原理不同 | sizeof是计算操作数所占内存空间大小,单位是字节 | strlen是计算字符串中’\0’之前的字符个数,单位是个 |
操作对象不同 | sizeof不关注计算的是什么对象 | strlen的操作对象必须是字符类型,并且还需要关注操作对象是否有’\0’ |
现在strlen
我们就已经介绍完了,下面我们继续看下一类函数——字符串拷贝函数——strcpy
、strncpy
;
3.3 strcpy函数和strncpy函数
这个两个函数都是用来进行字符串拷贝的,我们先来看一下这两个函数的介绍:
从介绍中我们可以看到,strcpy
和strncpy
这两个函数都是将第二个参数的字符串内容拷贝到第一个参数中,但是strncpy
相比于strcpy
多了一个参数——拷贝字符串的个数,也就是说,strcpy
它是直接将整个字符串拷贝过来,但是strncpy
会根据具体的个数进行拷贝;
我们继续来看一下这两个函数是怎么使用的;
3.3.1 strcpy和strncpy的使用
对于这两个函数的使用,我们可以简单的理解为;
strcpy
是将整个字符串包括’\0’拷贝到目标字符串的指定位置,所以源对象需要有’\0’;strncpy
是将指定数量的字符拷贝到目标字符串中,源对象不一定要有’\0’;- 不管是
strcpy
还是strncpy
它们的操作对象都不能有重叠;
接下来我们通过不同的测试来进一步介绍它们的用法;
- 原目标有无
'\0'
对strcpy
和strncpy
的影响
下面我们来对这两个函数进行第一次测试,源目标有’\0’和没有’\0’的区别:
这次测试结果很好的说明了一个问题,strcpy
函数和strlen
函数一样,也是通过寻找’\0’从而停止函数的继续运行,而strncpy
只关心需要拷贝的字符数量,并不关心源对象是否有'\0'
;
strncpy
的拷贝数量与源对象的字符数量不相等
下面我们测试一下strncpy
在拷贝的对象数量不等于源对象字符数量时会怎么处理:
从这次的测试结果来看,strncpy
在拷贝小于源对象字符个数的字符时是不关心'\0'
的,但是在拷贝大于源对象字符个数的字符时,就有区别了:
- 源对象有
'\0'
,则多出的字符个数用'\0'
继续填补; - 源对象没有
'\0'
,则多出的字符个数用'?'
继续填补;
strcpy
与strncpy
对指定的位置进行拷贝
接下来我们来测试一下对strcpy
与strncpy
指定的位置进行拷贝:
这次测试结果证明了strcpy
和strncpy
这两个函数都是可以指定位置进行拷贝的,既可以指定拷贝源对象的起始点,也可以指定拷贝目标对象的起始点;
- 拷贝的目标对象空间小于源对象
接下来我们测试一下如果目标对象的空间小于源对象的空间时,函数又该如何处理:
可以看到此时如果当拷贝的字符数量超过目标空间的大小时,就会造成目标空间的堆栈损坏,所以目标空间需要足够大,至少能放入需要拷贝的所有字符才行;
- 字符指针之间的字符串拷贝
接下来我们来测试一下如果对象为字符指针时,strcpy
和strncpy
又是如何处理的:
可以看到当源对象为指针时,是不影响函数进行拷贝的,但是当目标为指针时,此时指针如果被赋予了不可修改的值,如这里的空指针和常量字符串,此时函数也是无法进行拷贝的;
- 将源对象的字符拷贝给源对象
接下来我们来测试一下能不能实现自我拷贝:
这次测试我们可以看到,两个函数都是能够进行自我拷贝的,但是,我们会发现它们结果与我们所想的结果有点不太一样,会出现这种情况也是因为这两个函数对于拷贝空间有重叠的情况是标准未定义的,虽然此时是能正常输出,但是结果却是错误的,所以最好不要使用这两个函数来进行自我拷贝;
3.3.2 小结
经过前面的介绍,对于strcpy和strncpy这两个函数,我们可以做一个小结:
- strcpy是以字符串终止标志’\0’作为拷贝的结点,将指定起点的整个字符串拷贝到目标对象中;
- strncpy是以给定的拷贝字符的个数为标准进行拷贝,此时会出现以下情况:
- 给定的个数小于或等于源对象字符个数,此时函数正常进行拷贝;
- 给定的个数大于源对象字符个数,此时源对象有’\0’则多出的字符通过’\0’填补;
- 给定的个数大于源对象字符个数,此时源对象没有’\0’则多出的字符通过’?'填补;
- 源对象需要有’\0’;
- 目标对象的空间需要足够大,至少能放入拷贝的字符个数;
- 目标对象需要能够被修改;
- 源对象与目标对象的空间不能够重叠;
3.4 strcat函数和strncat函数
现在我们介绍的这两个函数是用来进行字符串追加的,它们与字符串拷贝不同,但又有相似之处。下面我们来看一下这两个函数的介绍:
可以看到strcat
与strcpy
的参数一样,strncat
和strncpy
的参数是一样的,也就是说,它们的用法也很相似,只不过是功能不同而已:
strcpy
和strncpy
是将源对象的字符拷贝到目标对象中;strcat
和strncat
是将源对象的字符添加到目标对象中;
这时有朋友可能就会好奇了,一个拷贝,一个增加,到底有什么区别呢?下面我们就来看看strcat
和strncat
的用法;
3.4.1 strcat和strncat的使用
从函数的介绍中,我们可以得到几点信息:
- 源对象和目标对象都需要有’\0’,追加后的字符串也会使用’\0’作为结束标志;
- 追加的实现是将源对象的第一个字符覆盖目标对象的’\0’来实现追加;
- 两个字符串的空间不能够重叠;
strcat
是将源对象添加到目标对象中;strncat
是将指定的字符个数添加到目标对象中;
下面我们来测试一下这两个函数的用法;
'\0'
对函数的影响
在测试'\0'
对函数的影响时我们为了保证目标对象有足够的空间来接收源对象的内容,因此我们无法测试目标对象中没有'\0'
的情况。
从测试结果中我们可以看到,当源对象和目标对象中都存在'\0'
时,此时两个函数都是能够正常使用的,但是当源对象没有'\0'
时,strcat
函数则无法正常使用;而strncat
函数在进行追加时,因为是根据我们传入的字符个数来进行追加的,所以当我们追加的字符个数与源对象的长度一致时并不影响函数的正常运行;
- 空间重叠对函数的影响
从测试结果中可以看到,对于指定追加个数的strncat
函数来说,在空间有重叠的情况下函数依旧能够正确的完成追加,但是对于strcat
函数而言,由于它在追加时是需要将源对象的'\0'
一并追加到目标对象中,当函数进行自我追加时,原先拥有的'\0'
被追加的内容给覆盖掉了,导致函数始终无法找到源目标的'\0'
,最后得到的结果就是一直进行追加。
- 追加的数量对
strncat
函数的影响
在这次的测试中,我们通过追加0~4个字符数量对函数strncat
进行了探讨,从监视窗口中不难看出:
- 当数量为0时函数不会执行任何操作;
- 当数量大于0小于或等于源对象的长度时,函数会在目标对象的末尾新增一个
'\0'
; - 当数量大于源对象的长度时,函数会按照源对象的长度对目标对象进行追加,并在追加完字符后新增一个
'\0'
;
- 不同起点对函数的影响
这一次我们分别测试了改变目标对象的起点与源对象的起点。从测试结果中我们可以看到当我们在移动目标对象的起点后,函数返回的也是移动后的目标对象;当我们移动了源对象的起点后,目标对象中追加的内容也是源对象移动后的内容。
3.4.2 小结
经过上面的探讨,我们可以得到以下结论:
strcat
和strncat
是用于将源对象追加到目标对象末尾的库函数;strcat
与strncat
在进行追加时是通过使用源对象的第一个字符覆盖目标对象的第一个'\0'
来进行追加的strcat
在进行追加时受源对象中的'\0'
的影响,当追加的对象空间重叠时可能会导致字符串中的'\0'
被覆盖而进入追加的死循环;strncat
在进行追加时受指定的字符数量的影响:- 当追加字符数量为0时函数不进行任何操作;
- 当追加字符数量大于0小于或等于源对象的长度是,函数正常追加并在目标对象的末尾新增一个
'\0'
; - 当追加字符数量大于源对象的长度时,函数会按源对象的长度进行追加,并在目标对象末尾新增一个
'\0'
;
- 函数在进行追加操作时会根据传参时的对象进行操作:
- 当源对象的地址发生改变时,会提取改变后的源对象中的内容;
- 当目标对象的地址发生改变时,追加操作会根据改变后的起始点开始寻找第一个
'\0'
进行追加并在完成追加后返回传入的目标对象的地址;
3.5 strcmp
函数和strncmp
函数
对于两个字符串来说,它们也是能够进行大小比较的,比较的依据并不是根据字符串的长度,而是根据字符串中同位序字符的ASCII码值进行比较的。下面我们就来看一下C语言给我们提供的两个用于进行字符串大小比较的函数strcmp
和strncmp
:
从函数的介绍中我们可以这两个函数的功能是一样的,但是还是有些许区别:
strcmp
比较的就是两个字符串strncmp
比较的是两个字符串中的字符,也就是两个字符串的子串
3.5.1 strcmp和strncmp的使用
为了弄清它们的用法,我们继续往下看;
这里的用法介绍可能有点不好理解,我给大家解释一下:
strcmp
是按照字母表的顺序将两个字符串中的字符进行挨个比较,并根据比较的结果返回对应的值;strncmp
是按照字母表的顺序将两个字符串中的子串进行挨个比较,并根据比较的结果返回对应的值;
大家需要注意的是介绍中说的字母表的顺序并不真的就是英文的字母表,这里的字母表更多的是指计算机中的ASCII码表。这里我们通过对同一个字母a的大小写进行测试,来说明比较函数比较时的底层逻辑:
对于字符'a'
和'A'
来说小写字符的ASCII码值是要比大写字母大32的,但由这两个字符组成的两个字符串进行比较时就会有下面三种情况:
- 用字符串
"a"
与字符串"A"
进行比较,对应的ASCII码值的比较结果为 97 > 65 97>65 97>65 也就是字符串1大于字符串2,我们可以看到函数返回的是1; - 用字符串
"a"
与字符串"a"
进行比较,对应的ASCII码值的比较结果为 97 = 97 97=97 97=97 也就是字符串1等于字符串2,我们可以看到函数返回的是0; - 用字符串
"A"
与字符串"a"
进行比较,对应的ASCII码值的比较结果为 65 < 97 65<97 65<97 也就是字符串1小于字符串2,我们可以看到函数返回的是-1;
相信大家看到这里应该就能明白了,其实不管是strcmp
还是strncmp
,它们在进行比较时就是从给定的字符串的起点开始挨个往后进行比较,如果比较过程中所有的字符都相等,那么返回的结果就是0,当出现不相等时,就会根据比较的结果返回对应的值。两个函数的区别就在于后者是根据给定的字符个数进行比较,如下所示:
可以看到,对于同样的字符串,只因为我们通过strncmp
时比较的是4个字符,最终得到的是两个结果,这也进一步说明了几个问题:
- 函数在进行比较时,
strcmp
是比较整个字符串,strncmp
是比较指定的字符数量; - 字符串比较的底层逻辑是对比两个字符串中同位序的字符的ASCII码值的大小;
- 函数的返回值是根据第一次出现的不同字符的比较结果进行返回,当两个字符串的字符完全相同才返回0;
对于strcmp
和strncmp
这两个函数来说,还是有些地方需要我们仔细的探讨一下的。
- 有无
'\0'
对函数的影响
对于这两个函数,我们从介绍中并未看到函数在使用时提及字符串终止符,因此'\0'
的有无对函数的运行有无影响就是需要我们关注的第一个问题:
这里我们测试了3中情况,由测试结果可以看到:
- 对于
strcmp
来说,它在执行字符串比较时关注的是给定的字符个数,因此有无'\0'
对它来说并不重要; - 对于
strcmp
来说,它比较的是整个字符串,当字符串中没有’\0’时,如果已存在的字符都是相等的,这时函数并未找到字符串终止符,函数会继续往后查找,这时查找的结果就变成不可预测的了。就像test4
函数中,函数查找到第5个字符时,ch1
的字符为'\0'
,对应的ASCII码值为0,而字符串ch2
中的字符是未知的,从结果上看,它对应的ASCII码值肯定是大于0的;在test5
函数中两个字符串的已知字符都相等,因此函数会继续查找第5个字符,这时两个字符串中的第5个字符都是未知的,从结果上可以看到此时ch2
后面的字符对应的ASCII码值是大于ch1
后面的字符对应的ASCII码值的;
- '\0’的位置对函数的影响
由上一个测试可知,'\0'
的有无对函数strcmp
来说是有很大的影响的,现在我比较好奇的是如果在比较的字符串中都有'\0'
但是'\0'
的位置不一定在字符串末尾,这时对两个函数又会有什么影响呢?
下面我们来分析一下这次的三个测试用例:
- 在
test6
中,虽然字符数组中'\0'
后的内容并不相同,但是得到的比较结果两个函数都为0,也就是说两个函数都是比较到'\0'
就停止了。从这次测试中我们可以得到一个结论——两个函数在比较字符时都是以'\0'
作为结束标志; - 在
test7
和test8
中,当'\0'
位于不同位置时,'\0'
的位序靠前的对象会小一点。这个测试结果其实不难理解,我们知道这两个函数都是一个字符一个字符的进行比较,当比较到第三个字符时,ch1
的字符为'\0'
对应的ASCII码值为0,ch2
的字符只要不是'\0'
那它的ASCII码值肯定大于0,因此函数就不会继续往后比较了。从这两次测试中我们可以得到一个结论——当两个长度不相等的字符串进行比较时,字符串中第一个'\0'
之前的相同位序上的元素都相等,那么长字符串大于短字符串;
3.5.2 小结
从上面的介绍中,我们可以对这两个函数总结以下结论:
strcmp
和strncmp
在进行字符串比较时的底层逻辑是对相同位序上的字符的ASCII码值进行比较;- 函数在对两个字符串进行比较时,会以第一个不相等的字符的比较结果作为两个字符串的比较结果:
- ASCII码值大的字符所在字符串大于ASCII码值小的字符所在字符串;
- 当两个字符串中所有的字符都相等时,这两个字符串才相等;
strcmp
和strncmp
在进行比较时都是以'\0'
作为结束标志;- 在已有元素相同且顺序相同的情况下,当两个字符数组中
'\0'
的位置不相等时,'\0'
的位序靠前的字符数组小于'\0'
的位序靠后的字符数组; - 在两个元素相同但长度不同的字符串中,长字符串大于短字符串;
3.6 查找子字符串——strstr
3.6.1 字符串的基本概念
在介绍这个strstr
函数之前我们需要先了解几个字符串的基本概念:
- 主串:源字符串
- 子串:源字符串中能够找到的任意多个连续的字符组成的子序列
- 字符在串中的位置:字符在串中的序号
- 子串在主串中的位置:子串的第一个字符在主串中出现的位置
为了方便大家更好的理解,这里我们以字符串"hello"
为例,来说明这些概念:
字符串
"hello"
它就是一个主串;
字符串"hell"
、"ell"
、"lo"
、"o"
、""
等能够在主串中找到的这些任意多个连续字符组成的子序列都是它的子串;(PS:这里的多个是个泛指,不要较真哦!!!)
在主串中的这些字符'h'
、'e'
、'l'
、'l'
、'o'
所对应的序号为1、2、3、4、5,这些序号也就是字符在串中的位置;(PS:字符串中的序号是从1开始,字符串中字符的下标是从0开始,二者相差1)
对于子串"ell"
来说,它第一次出现在主串中的位置就是字符'e'
在串中的位置,也就是2。
在很多时候,我们都会遇到需要我们在某个字符串中查找子串的位置,这种定位子串的操作我们将其称为字符串的匹配模式。
3.6.2 strstr
C语言在头文件<string.h>
中提供了一个专门用来定位子串的库函数——strstr
。这是一个非常重要的库函数,下面我们来认识一下这个函数:
从函数的原型中我们可以看到strstr
这个函数有两个参数,第一个参数主串的类型为char*
,第二个参数子串的类型为const char*
类型,函数的返回值也是一个char*
的指针;
从函数返回值的介绍中我们可以看到strstr
这个函数返回的是子串在主串中第一次出现的地址,而字符串的地址就是字符串第一个字符的地址;
3.6.3 strstr的使用
了解了函数的基本信息后,下面我们就可以来测试一下函数的用法了,如下所示:
这里大家一定要注意字符在字符串中的下标和字符在字符串中的序号的区别。从这里的用法演示中我们可以看到strstr
这个函数的使用并不困难,关于函数的用法我就不再多加赘述了,下面我们来了解一下该函数的底层逻辑。
3.6.4 strstr的底层逻辑
strstr函数在运行时,实际上就是通过将子串中的字符与主串中的字符一个一个的进行比较,当子串中的字符全部都能在主串中找到时,那就说明主串中存在该子串,此时函数就会返回主串中子串的首字符的地址,如下所示:
通过这个动图相信大家也能更好的理解strstr
函数的一个工作原理。当然这里展示的是字符串的朴素匹配模式,strstr
函数实际在运行时的效率会更高。
字符串匹配模式算法包含朴素匹配模式算法和KMP匹配模式算法,相关的知识点我会在【数据结构】专栏中详细介绍,这里我就不再展开介绍了,大家如果对该内容感兴趣的话可以关注该专栏。
3.7 拆分字符串为标记——strtok
在日常生活中,我们可能会遇到将一条信息拆分成多条信息的情况,就比如我现在需要大家将出生年月日xxxx-xx-xx
分别提取出来,大家此时会怎么做呢?
有朋友会说,这还不简单,我直接化身成CV工程师,一下就解决了。这个也确实不失为一种方法,但是如果此时是需要提取100个人的信息、1000个人的信息、10000个人的信息……这时CV工程师还可行吗?
很显然在这种数据量庞大的情况下CV工程师并不是一个好的解决方式。为了更加高效的完成提取工作,C语言在头文件<string.h>
中给我们提供了一个用来拆分字符串的函数strtok
。下面我们就来一起了解一下这个库函数:
从函数的介绍中我们可以看到这个函数是用来找到字符串中的下一个标记的,至于这个标记是什么,我们还不清楚;
在函数的原型中可以看到这个函数有两个参数一个是char*
类型的参数,一个是const char*
类型的参数,函数的返回类型也是char*
;
在函数的返回值介绍中我们可以看到,这些函数是用来在strToken
中找到标记并返回指向该标记的指针,如果找不到标记就返回NULL;当找到标记时就会用NULL来替换每个分隔符;
看到这个介绍我们还是比较懵,这里又是提到了标记又是提到了分隔符,这些都是什么意思呢?别着急我们继续往下看;
3.7.1 strtok的使用
为了弄清楚什么是标记什么是分割符,我们需要来看一下这个函数具体时如何使用的:
从Remarks介绍中我们可以得到以下信息:
- 函数所提到的标记是位于
strToken
这个字符串中的; - 在隔符字符串
strDelimit
中的这些分割符也能够在strToken
中找到
因此我们可以大胆推测strToken
这个字符串的结构应该是由"标记"
和"分割符"
组成。
现在我们再来看一下年月日的字符串结构"xxxx-xx-xx"
,在这个字符串中,将年、月、日给隔开的字符为'-'
。按照strtok
函数的介绍来看,在"xxxx-xx-xx"
这个字符串中,年、月、日这些信息就是函数提到的标记,而将它们隔开的字符'-'
就是分割符。
在下面的函数使用介绍中我们可以得到以下信息:
- 第一次调用
strtok
时,函数会跳过strToken
中的前导分割符并返回第一个标记的地址,并修改strToken
这个字符串; - 当我们想获取后面的标记时,我们需要将
strToken
这个参数的值改为空指针; - 分隔符字符集Delimit在不同的调用中可以接收不同的分隔符,以便字符串中的分隔符发生变化;
为了更好的理解strtok
这个函数的用法,下面我们可以做一个测试:
从这次测试中我们可以得到以下结论:
strtok
函数在每次调用时能且只能找到一个标记并返回;strtok
函数在第一次调用时会改变第一次调用传入的字符串;- 在后续的调用中如果传入字符串为NULL,则可以继续查找被修改过的字符串的后续标记;
- 当被修改过的字符串中已经没有标记时,则返回空指针;
- 当传入的字符串中无法找到分隔符集中的分隔符时,返回该字符串;
这里需要注意的是传入的字符串应该是可被修改的,如果传入的是常量字符串,则函数无法正常运行,如下所示:
可以看到当我们将ch1
的类型由字符数组类型改为字符指针类型后,此时的ch1
就变成了一个内容不可修改的常量字符串,这时我们再来调用strtok
函数时,因为函数会对ch1
中的内容进行修改,所以就出现了写入冲突的错误。
在函数的使用中有提到分隔符字符集中的内容在不同的调用中也是可以进行修改的,那具体能不能修改呢?我们来测试一下:
从这次的测试中我们可以得到以下结论:
strtok
在第一次调用时会跳过前导分隔符;- 在后续的调用中分隔符字符集中的内容是可以被修改的;
3.7.2 小结
经过前面对strtok
函数的介绍,关于strtok
函数的用法,我们可以总结为以下几点:
strtok
函数的参数分别是待分割的字符串strToken
和字符串中的分隔符集Delimit
;strToken
字符串必须是能够被修改的字符串;Delimit
字符集中的分隔符可以被修改;strtok
在第一次调用时会跳过strToken
中的前导分隔符;strtok
函数在调用时,函数会对不同的情况做出不同的处理:- 如果字符串
strToken
中存在标记和分隔符,会将标记末尾的分隔符修改为'\0'
并返回一个指向该标记的指针; - 如果字符串
strToken
中不存在分隔符,则会返回指向字符串strToken
的指针; - 如果字符串
strToken
中不存在标记,则会返回NULL;
- 如果字符串
strtok
每次调用只能查找一个标记;- 在第一次调用中,如果字符串
strToken
中存在分割符,则函数会修改字符串; - 在后续的调用中,如果想要继续查找被修改的字符串
strToken
中后续的标记,需要将参数strToken
改为NULL;
3.8 获取错误信息字符串——strerror
3.8.1 strerror
下面我们要介绍的strerror
这个库函数它并不是用来对字符串进行操作的库函数,它的作用是当系统出现错误时,获取系统错误信息的库函数。下面我们借助cplusplus网站来认识一下这个库函数:
网站中对这个库函数解释的很详细了,这个函数的具体作用我们简单的理解就是:
- 当程序发生错误时,会产生一个错误号码,当我们将对应的错误号码传给函数时,函数会生成一个不可修改的字符串,这个字符串就是用来解释这个错误号码的含义的。
3.8.2 errno
在上面的介绍中还提到了一个由库函数设置的errno
,这个errno
是什么呢?我们继续借助网站来认识一下;
从errno
的介绍中我们可以得到以下信息:
- 它是被定义的一个宏常量;
- 这个宏常量在程序启动时会被设置为0;
- 这个宏常量的值可以被C标准库中的任意库函数修改为不同于0的值;
- 该宏常量位于头文件
<errno.h>
中; errno
不同的值对应的是不同的错误,我们可以通过strerror
来获取对应的错误信息,也可以通过perror
来打印错误信息;
通过这个介绍我相信大家对errno
已经有了一个初步的印象了,下面我们就来做个简单的测试,来看一下errno
中的不同的值会对应哪些错误信息:
可以看到这里我们测试的10个整型值都有其对应的错误信息。
3.8.3 perror
在errno
的介绍中还提到了一个函数perror
,下面我们就来看一下这个库函数的相关内容:
从函数的介绍中,我们可以获取以下信息:
perror
函数是用来打印错误信息的;perror
的参数为空指针时,只打印错误信息;perror
的参数为非空指针时,会先打印字符串中的内容,并在后面加上冒号和空格后再打印错误信息;perror
应该在错误产生时立即调用,否则会被其它的信息给覆盖;
3.8.4 strerror、errno和perror的使用
下面我们通过一个简单的例子来说明一下strerror和perror这两个库函数以及errno这个宏常量应该如何使用,如下所示:
在这次的测试中,我们仅仅执行了打印操作,此时程序在运行中是没有发生错误的,因此,errno
的值为0,由strerror
和perror
这两个函数打印出来的错误信息来看,都是无错误;
这次我们通过calloc
函数向内存申请了1000000000个大小为整型大小的空间,可以看到,此时errno
的值被calloc
函数修改为了12,对应的错误信息为"Not enough space"
——没有足够的空间。
从这两个例子中我们可以得到结论:
errno
的作用就是用来实时监测程序运行的情况,当程序运行的过程中发生错误时,errno
的值就会被修改;- 我们可以通过
strerror
来获取对应的错误信息字符串,如果要将这个信息打印出来,则需要借助输出函数来进行输出; perror
的作用就是自动获取错误信息并将错误信息打印在控制台上;
可见,相比于strerror
和error
,perror
不仅能够完成它们俩的工作,还能额外完成打印函数的工作。
结语
在今天的内容中,我们详细介绍了字符函数和字符串函数的相关知识点。下面我们简单的给今天的内容做个总结:
- 在头文件
<ctype.h>
中包含了一系列的字符分类函数和字符转换函数; - 在头文件
<string.h>
中包含了一系列的字符串函数; - 字符串函数绝大部分都是来操作字符串的函数,如:
- 求字符串长度的函数——
strlen
- 进行字符串拷贝的函数——
strcpy
和strncpy
- 进行字符串拼接的函数——
strcat
和strncat
- 进行字符串比较的函数——
strcmp
和strncmp
- 查找子串的函数——
strstr
- 进行字符串拆分的函数——
strtok
- 求字符串长度的函数——
- 字符串函数中也有不是用来操作字符串而是用来获取错误信息字符串的函数——
strerror
; - 包含在头文件
<errno.h>
中的宏常量errno
可以获取错误信息; - 包含在头文件
<stdio.h>
中的库函数perror
可以打印错误信息的;
今天的内容到这里就全部结束了,希望今天的内容能够掌握如何利用这些库函数更加高效的解决字符和字符串的问题。如果大家喜欢博主的内容,大家可以点赞、收藏加评论支持一下博主。当然也可以转发给身边需要的朋友。最后感谢各位的支持,咱们下一篇内容见!!!