目录
1.引言
2.字符分类函数与字符转换函数
3.strlen 函数
4.strcpy 函数与 strncpy 函数
5.strcat 函数与 strncat 函数
6.strcmp 函数与 strncmp 函数
7.strstr 函数
8.strtok 函数
9.strerror 函数
10.结语
1.引言
我们前面已经学过了字符串的基础知识,也知道了 strlen 函数可用来统计字符串的字符个数,那除了我们熟知的 strlen 函数,在C语言的 string.h 头文件下,还提供了许多其他与字符串相关的函数,所以本篇博客就来详细讲解一下与字符串相关的各种函数的作用及使用语法。
2.字符分类函数与字符转换函数
在C语言中有这么一类函数是专门用来做字符分类的,也就是判断一个字符属于什么类型。那此类函数的使用方法非常相似,也都是由 ctype.h 头文件提供。我们这里就着重展示两个,剩下的就靠小萌新们自己去探索了。
那在C语言中还有一类函数是专门用来做字符转换的,也就是把字符从一个类型转为另一个类型。那此类函数的使用方法与前面的字符分类函数一致,也同样由 ctype.h 头文件提供。字符转换函数只有下面这两个,tolower 函数主要用于大写字符的小写转换,而 toupper 函数主要用于小写字符的大写转换。关于这里的返回值类型与参数类型为什么都是 int 类型,是因为所有的字符在实际储存时存的都是对应的 ASCII 值。
在没有学习这些函数之前,我们想要实现字符的大小写转换,可能会像图一那样书写代码。那如果我们掌握了可以判断字符大小写类型的字符分类函数与可以转换字符大小写类型的字符转换函数,就能将代码改成图二那样。
3.strlen 函数
关于 strlen 函数的用法及作用我们前面已经讲过了,这里主要再补充两个点。第一个点就是 strlen 函数的返回值类型为 size_t,但由于 size_t 表示的是无符号整型,我们在对数据类型为size_t 的变量或表达式做结果判断就要注意负数的判断情况,在 size_t 类型下的负数实际上表示的是一个极大的正数。
第二个点就是 strlen 函数的模拟实现,我们之前是使用指针-指针的方式实现的,那如果要不额外创建变量完成对字符串的字符个数统计,我们就可以考虑使用函数递归的方式,具体代码如下。关于这个函数的具体运行逻辑就不再解释了,看不懂的小萌新建议再回去看一遍函数递归的知识讲解。
4.strcpy 函数与 strncpy 函数
然后我们来学习一下 strcpy 函数的作用及其使用语法。strcpy 函数可用来拷贝字符串,也就是将一串字符串的内容拷贝到另一串字符串里,完成对应内容的替换。此时有小萌新可能会问,我们前面学过指针知识,知道可以通过指针获取地址,然后解引用操作完成值的修改,那为什么还要使用 strcpy 函数?这是因为字符串及数组与普通变量不同,我们通过指针得到的是字符串的首地址或数组的首地址,解引用操作后得到的也是这个地址中的值,那进行内容修改时也只会修改这个地址中的值,我们可以看下图的代码演示。
所以想要完整替换字符串的内容,就要使用 strcpy 函数。那 strcpy 函数的使用语法如下图所示。这个函数的返回值类型为 char* 指针类型,且返回的是目标字符串的首地址,所以我们也可以创建一个指针变量接收 strcpy 函数的返回值,然后借助这个指针变量访问目标字符串的内容。strcpy 函数的参数有两个,分别是目标字符串的首地址与源头字符串的首地址,且一定是目标字符串的首地址在前,源头字符串的首地址在后,因为是从目标字符串的首地址开始将源头字符串的内容拷贝进去。
那除此之外还要注意,在拷贝时是将源头字符串的 '\0' 也一起拷贝过去的,所以源头字符串一定要以 '\0' 结尾,不然会出问题。且目标字符串必须是可修改的,所开辟的内存空间也要足够大,这样才能确保正常完成拷贝操作,不然就会发生越界访问,同样会出问题。
那在清楚了 strcpy 函数的作用与使用方法后,我们来模拟一下 strcpy 函数。根据刚刚我们对 strcpy 函数的理解,就能写出下图代码。首先通过 tmp 临时指针变量储存目标字符串的首地址,这样就能在函数结束时正常传回目标字符串的首地址。然后通过 while 循环来实现字符串的拷贝操作,因为源头字符串的拷贝是到 '\0' 结束的,那就可以通过判断当下字符是否为 '\0' 来控制循环的进行。而每次在完成 *p2=*p1 的拷贝操作后,都要进行相应的指针偏移操作。值得注意的是,在循环结束时我们还要再进行一次拷贝操作,因为源头字符串的 '\0' 也要拷贝到目标字符串里。
虽然我们已经模拟实现了 strcpy 函数,但我们还要再深入思考一下,代码是否可以进一步改进。首先从安全性上来看,我们只是完成代码的拷贝操作,那源头字符串就不能被修改,为了保护源头字符串,我们可以在前面使用 const 修饰;而且在使用指针前应先判断指针是否为空,那我们就可以使用 assert 断言,记得在前面引入头文件 assert.h。然后再观察刚刚的代码,我们发现最后还要再进行一次拷贝操作,那能不能直接完成?其实是可以的,我们只要将 while 循环体里的代码改写为 *p2++ = *p1++ 就能实现字符拷贝与指针偏移操作。然后将这个表达式作为 while 循环的判断条件,就能在源头字符串拷贝到 '\0' 时终止循环且直接完成 '\0' 的字符拷贝。我们简单解释一下就是,表达式原本是完成字符拷贝操作,作为判断语句时 while 循环就会判断刚刚拷贝的字符是否为 '\0',因为 '\0' 的 ASCII值为 0,如果是则循环终止,而且 '\0' 的字符拷贝会在循环终止前完成。值得注意的是,while 循环体的代码不能为空,所以最后的 ; 不可忘记,这表示执行的是空语句。
那理解了 strcpy 函数之后,再来理解 strncpy 函数就相当容易了。前面的 strcpy 函数是长度不受限的字符串函数,也就是说我们并不能控制字符串的拷贝长度,利用 strcpy 函数只能将源头字符串完全拷贝到目标字符串里。而后面的 strncpy 函数则是长度受限制的字符串函数,也就是说我们可以通过参数来控制字符串的拷贝长度,利用 strncpy 函数就可以只拷贝源头字符串的一部分到目标字符串里。那从前面的对比中我们就知道了 strncpy 函数与 strcpy 函数的区别,也知道了strncpy 函数应该有三个参数,除了目标字符串的首地址与源头字符串的首地址外,还有至多拷贝的字符个数。
然后我们来看下图代码。
通过调试我们可以看到,当我们规定的拷贝长度小于源头字符串长度时,只会拷贝源头字符串的一部分,且不会将源头字符串的 '\0' 字符拷贝过来。所以在使用 strncpy 函数时,不再要求源头字符串必须以 '\0' 结尾。
那当我们修改 strncpy 函数的参数大小后,再次运行代码,结果有所不同。
通过调试我们可以看到,当我们规定的拷贝长度大于源头字符串长度时,除了将源头字符串全部拷贝过来外,还在末尾补了多个 '\0' 字符。因为我们规定了拷贝长度,那当源头字符串长度不够时就会通过在末尾补 '\0' 字符的方式来凑齐所需的长度。
然后我们也模拟一下 strncpy 函数,具体代码如下。关于 strncpy 函数的模拟就不细致讲解了,感兴趣的小萌新可以自己研究一下。
5.strcat 函数与 strncat 函数
然后我们来学习一下 strcat 函数的作用及其使用语法。strcat 函数可用来拼接字符串,也就是将一串字符串的内容拼接到另一串字符串的后面,完成对应内容的追加。strcat 函数的使用方法与strcpy 函数很像,返回值类型也为 char* 指针类型,返回的是目标字符串的首地址;函数的参数也是两个,一个是目标字符串的首地址,另一个是源头字符串的首地址。我们直接来看下图的代码演示。
那除此之外还要注意,除了源头字符串一定要以 '\0' 结尾外,目标字符串也一定要以 '\0' 结尾,不然就无法获取字符串拼接的起始地址。同样的,目标字符串必须是可修改的,所开辟的内存空间也要足够大。
那在清楚了 strcat 函数的作用与使用方法后,我们来模拟一下 strcat 函数。根据上图的拼接结果提示与刚刚我们对 strcat 函数的理解,就能写出下图代码。其实 strcat 函数也是在做字符串的内容修改操作,只是与 strcpy 函数修改的起始地址不同,strcpy 函数是从目标字符串的首地址开始修改,而 strcat 函数要从目标字符串结尾的 '\0' 字符的地址开始修改。所以我们先要找到目标字符串的 '\0' 字符所在地址,至于后续操作则与 strcpy 函数的模拟逻辑相同,这里就不再重复了。
strcat 函数与 strncat 函数的区别也在于拼接字符个数是否受限。利用 strcat 函数只能将源头字符串全部拼接到目标字符串的后面,而利用 strncat 函数就可以只拼接源头字符串的一部分到目标字符串的后面。那 strncat 函数同样有三个参数,除了目标字符串的首地址与源头字符串的首地址外,还有至多拼接的字符个数。
然后我们来看下图代码。
通过调试我们可以看到,当我们规定的拼接长度小于源头字符串长度时,只会拼接源头字符串的一部分,但此时会在末尾自动补上一个 '\0' 字符,以保证拼接完后的字符串依旧是一个合法的字符串,这点与 strncpy 函数不同。
那当我们修改 strncat 函数的参数大小后,再次运行代码,结果有所不同。
通过调试我们可以看到,当我们规定的拼接长度大于源头字符串长度时,就可以将整个源头字符串拼接到目标字符串的后面,且不会像 strncpy 函数那样,通过在末尾补 '\0' 字符的方式来凑齐所需的长度,所以此时的 strncat 函数就等同于 strcat 函数。
然后我们也模拟一下 strncat 函数,具体代码如下。关于 strncat 函数的模拟就不细致讲解了,感兴趣的小萌新可以自己研究一下。
6.strcmp 函数与 strncmp 函数
然后我们来学习一下 strcmp 函数的作用及其使用语法。strcmp 函数可用来比较字符串,他所比较的是两个字符串的内容是否相同。strcmp 函数的使用方法与前面两个类似,这里就不再重复了。不过要说明两点,首先就是 strcmp 函数的返回值类型为 int 类型,因为最后要根据返回值与 0的大小关系来判断两个字符串的大小关系。如果返回值 >0,说明第一个字符串大于第二个字符串;如果返回值 ==0,说明第一个字符串等于第二个字符串;如果返回值 <0,说明第一个字符串小于第二个字符串。其次字符串的大小比较是按照字典序来的,也就是比较对应位置上字符的 ASCII 值的大小。
然后我们来看下图代码。
第一行代码的结果证明,字符串是按字典序比较大小的,因为第一个字符串的首字符 'a' 的ASCII 值为 97,而第二个字符串的首字符 'b' 的 ASCII 值为 98,通过字符 'a' 与字符 'b' 的 ASCII 值比较,那返回值就是 <0 的数,说明第一个字符串小于第二个字符串。
第二行代码的结果证明,当两个字符串长度不同且一个字符串为另一个字符串的一部分时,也能正常判断两个字符串的大小,因为字符串结尾有隐藏的 '\0' 字符,通过字符 'e' 与字符 '\0' 的ASCII 值比较,那返回值就是 >0 的数,说明第一个字符串大于第二个字符串。
第三行代码的结果证明,在将带有 '\0' 字符的字符串存入字符数组时,虽然能正常储存所有字符,但 '\0' 是字符串结束的标志,所以在有关字符串的运算中,第二个字符串会被认为是字符串 'bcd',此时与第一个字符串 'bcd' 相比,内容就是完全一样的,那返回值就 ==0,说明第一个字符串等于第二个字符串。
那在清楚了 strcmp 函数的作用与使用方法后,我们来模拟一下 strcmp 函数。通过刚刚的代码讲解,我们知道字符串的比较方式是从两个字符串的首地址开始,对应位置一位一位的向下比较两个字符的 ASCII 值,当两个字符串第一次出现不相同的字符时,直接结束比较并返回对应的比较结果,此时返回值 >0 则说明第一个字符串更大,返回值 <0 则说明第二个字符串更大;若两个字符串完成比较,也就是都读取到 '\0' 字符且此前的字符完全相同,此时返回值 ==0 说明两个字符串相同。那基于刚刚的原理分析,我们就能写出下图代码。我们可以利用 while 循环来判断两个字符串是否存在不同的字符,如果存在,此时直接返回差值,这同样符合返回值的规定。反之两个字符串就是相同的,此时只要判断其中一个字符串获取的字符是否为 '\0' 就能知道比较是否完成,一旦完成就返回 0。
strcmp 函数与 strncmp 函数的区别也在于比较字符个数是否受限。利用 strcmp 函数就是完整比较两个字符串的内容是否相同,而利用 strncmp 函数就可以只比较两个字符串的一部分内容是否相同。所以利用 strcmp 函数比较结果为不同的两个字符串,利用 strncmp 函数比较时就可能是相同的。那 strncmp 函数同样有三个参数,除了第一个字符串的首地址与第二个字符串的首地址外,还有至多比较的字符个数。
然后我们来看下图代码。
我们来分析一下结果。对于第一行代码而言,我们通过参数控制比较字符个数为 4个,那 arr1 中所存的字符串就取出 "aabc" 这一部分,而 arr2 中所存的字符串就取出 "aabc" 这一部分,此时两个字符串的部分内容就是相同的,返回值就为 0。对于第二行代码而言,我们通过参数控制比较字符个数为 7个,那 arr1 中所存的字符串就取出 "aabcdeg" 这一部分,而 arr2 中所存的字符串就取出 "aabcdcg" 这一部分,此时第一个字符串中字符 'e' 的 ASCII 值大于第二个字符串中字符 'c' 的 ASCII 值,返回一个 >0 的值。
然后我们也模拟一下 strncmp 函数,具体代码如下。关于 strncmp 函数的模拟就不细致讲解了,感兴趣的小萌新可以自己研究一下。
7.strstr 函数
然后我们来学习一下 strstr 函数的作用及其使用语法。strstr 函数可用来查找子串,也就是在一个字符串中查找另一个字符串第一次出现的位置。这个函数的返回值类型为 char* 指针类型,返回结果有两种,如果在母串中找到子串则返回子串第一次出现位置的指针,如果没有在母串中找到子串则返回 NULL 指针。我们可以创建一个指针变量来接收 strstr 函数的返回结果,并通过结果判断得知是否在母串中找到子串。strstr 函数的参数有两个,分别是被查找的母串的首地址与要查找的子串的首地址,且母串的首地址在前,子串的首地址在后。
然后我们来看一下 strstr 函数的具体使用。首先 arr1 数组中存放的就是被查找的母串,arr2 与 arr3 数组中存放的都是要查找的子串。当我们在母串中查找子串 "abc" 时,可以看到是从母串的第五个字符开始的,所以最后结果就是将母串的首地址向后偏移四个单位后所得的地址。而当我们在母串中查找子串 "cfg" 时,可以看到母串中并没有这个子串,所以最后结果就是 NULL 指针,NULL 本来表示 0 值,那作为地址时在 x86 环境下就是 00000000。
那在清楚了 strstr 函数的作用与使用方法后,我们来模拟一下 strstr 函数。那在书写代码前我们先要了解一下 strstr 函数查找子串的逻辑。如果母串中能找到子串,那子串就一定得是母串的一部分,也就是说母串的部分内容应与子串完全相同。所以当子串的首字符与母串的某个字符相同时,我们就开始比对接下来的字符内容,如果完全一样则说明找到子串,此时就返回母串中最开始匹配上的字符地址。但如果整个母串走完了也没能与子串匹配上则说明母串中找不到子串,此时就返回 NULL 指针。所以我们应该设计三个指针变量,第一个用来遍历母串的每一个字符,同时用来保存与子串首字符匹配的字符地址,第二个用来遍历匹配成功后母串的剩余字符,第三个用来遍历匹配成功后子串的剩余字符。这样在我们找到子串时就可以直接返回第一个指针变量的值,因为第一个指针变量指向的就是与子串首字符匹配上的字符地址,而如果始终没能找到子串,就在最后返回 NULL 指针。至此,我们就能写出下图代码来模拟 strstr 函数的运行逻辑。
上述结果是没有问题的,但整体代码还可以做优化改进。首先我们要知道,空字符串是任意字符串的子串,因为只要是合法的字符串就一定会有 '\0' 字符,而空字符串就是只有 '\0' 字符的字符串。那我们在进入 while 循环前就可以先判断子串是否是空串,如果是此时直接返回母串的首地址。除此之外,对于上图样例中 arr4 数组所存的子串,我们可以看到母串的一部分是可以匹配子串的前半部分的,但在匹配子串的后半部分时母串已经没有多余字符可以继续匹配了,此时就可以直接返回 NULL 指针,因为无论后续再怎么匹配,也无法在母串中找到子串。最终我们的代码就变成了下图这样。
8.strtok 函数
然后我们来学习一下 strtok 函数的作用及其使用语法。strtok 函数可用来分割字符串,也就是根据分隔符集合中可能的分隔符,在字符串中通过插入 '\0' 字符替代分隔符的方式将一个大字符串分割为几个小字符串。这个函数的返回值类型为 char* 指针类型,返回结果有两种,每次成功完成分割操作后就会返回当前分割出的子串的首地址;当分割操作无法继续,也就是不能再分割出新的子串时就会返回 NULL 指针。我们可以创建一个指针变量来接收 strtok 函数的返回结果,并通过结果判断得知是否可以继续进行分割操作,通常配合循环语句使用。参数有两个,分别是被分割的字符串的首地址与分隔符集合的首地址。不过 strtok 函数有一个特性,只有在首次调用时需要传入被分割的字符串的首地址,后续调用时直接传入 NULL 指针就行,表示继续分割同一个字符串。
然后我们来看一下 strtok 函数的具体使用。首先 arr1 数组中存放的就是要分割的字符串,arr2 数组中存放的就是分隔符的集合,这里的分隔符只有 '@' 与 '/',所以在进行分割时字符串中的 '.' 字符不会被视作分隔符,最后的结果也证明了这一点。
关于为什么要使用 strcpy 函数将 arr1 数组中的字符串拷贝到 arr3 数组中后再对 arr3 使用strtok 函数,是因为 strtok 函数具有破坏性,每次做分割操作时都会将原有的分隔符改为 '\0' 字符。所以当我们想要保留原字符串或之后还要使用原字符串时,就要先进行拷贝操作,然后对拷贝字符串做分割操作。
strtok 函数在处理多个连续分隔符时,会将这些分隔符视作一个分隔符,也就只会进行一次修改操作,将最前面的分隔符改为 '\0' 字符后完成本次分割操作。下一次进行分割操作时会直接略过这些分隔符,从下一个非分隔符的字符开始往后找新的分隔符继续做分割操作,所以并不会返回空字符串。
同时,关于 strtok 函数的 NULL 指针使用情况,必须保证前面有非空指针的调用,不然属于未定义行为,是会引发报错问题的。
那 strtok 函数除了最开始的写法外,我们还能使用 for 循环的方式,如下图。因为 for 循环语句的三个表达式分别为初始条件,终止条件与步进条件,所以我们可以把首次调用作为初始条件,而后续调用作为步进条件,当返回值为 NULL 时说明无法再做分割操作,所以最后的终止条件就是P == NULL。
9.strerror 函数
在讲解 strerror 函数前,我们先要了解一下错误码的相关知识。在不同系统和C语言标准库的实现中其实都规定了一些错误码,一般是放在 errno.h 这个头文件中。那在C语言程序启动时,是会使用一个全局变量 errno 来记录当前程序的错误码,只不过在程序最开始启动时 errno 为 0,也就是没有任何错误发生,而当我们在使用标准库中的函数发生某种错误时,就会将对应的错误码存放到 errno 里。错误码本质上就是对各种错误进行数字编号,编译器能够通过数字知道发生了什么错误,我们却难以直接理解,但每个错误码都有对应的错误信息,我们通过 strerror 函数就可以将错误码对应的错误信息字符串的首地址返回,从而知道发生了什么错误。
通过 Everything 搜索 errno.h 这个头文件后打开就能看到各种错误信息以及对应的错误码。
在清楚了错误码是什么之后,我们就来学习一下 strerror 函数的作用及其使用语法。关于 strerror 函数的作用其实刚刚我们也说过了,就是根据参数部分传入的错误码,找到对应的错误信息,并返回这个错误信息字符串的首地址。要注意的是,strerror 函数仅仅是针对于标准库中的函数在使用时发生错误后,根据所设置的错误码进行信息转换。且使用这个函数同样需要包含头文件 string.h。而参数部分一般就是传递 errno 这个变量的值,那要使用这个全局变量就需要包含头文件 errno.h。
然后我们来看一下 strerror 函数的具体使用。下图列举了一个大家平时可能会遇到的例子,当我们以只读的形式去打开一个不在当前路径下的文件时就会出现文件打开失败的情况。那这里所使用的文件操作我们还没讲过,关于文件的相关内容同样会在后续博客里详细介绍,感兴趣的小萌新可以先去了解一下,这里主要是借这个例子来演示 strerror 函数的使用场景。当文件打开失败传回 NULL 指针后,我们将 errno 变量传入到 strerror 函数里,再创建一个指针变量接收 strerror 函数的返回值并打印输出。那从最后的输出结果我们知道,这个错误的错误码为 2,错误信息是找不到该文件或文件夹。
然后我们顺带讲一下 perror 函数的使用语法。perror 函数可以认为是 printf 函数与 strerror 函数的组合,因为他既能像 printf 函数那样打印输出字符串,又能像 strerror 函数那样将错误码转换为对应的错误信息字符串。但 perror 函数打印是有限制的,我们只能通过参数传递一个字符串,当字符串为空串时直接打印输出错误信息字符串;但当字符串不为空串时,就会先打印输出传入的字符串,然后在字符串结尾加上一个冒号与一个空格后,再打印输出错误信息字符串。
10.结语
本篇博客到这里就结束了,希望能够帮到各位刚接触C语言的小萌新们,下篇博客将继续讲解C语言的相关知识。