前言
在 C 语言的学习过程中,字符与字符串的处理是一块绕不开的基础内容。本文围绕两个最常用的头文件展开:ctype.h 中的字符分类与字符转换函数,以及 string.h 中的常用字符串函数。
对于 ctype.h 部分,我们会介绍 isdigit、islower、ispunct 等字符分类函数,以及 tolower、toupper 两个字符转换函数,并结合练习演示它们的实际用法。
对于 string.h 部分,我们会依次讲解 strlen、strcpy、strcat、strcmp、strncpy、strncat、strncmp、strstr、strtok、strerror 等函数。每个函数都会先说明函数原型、功能、参数与返回值,再通过代码演示其使用方式;其中 strlen、strcpy、strcat、strcmp、strstr 还会进一步给出模拟实现的思路与代码,帮助大家理解这些函数在底层是如何工作的。
本文适合已经掌握 C 语言基本语法(变量、指针、数组、函数)的读者阅读,尤其适合正在学习 C 语言库函数、准备面试或想深入理解字符串函数实现原理的同学。建议按章节顺序阅读:先看第 1、2 节的字符函数打好基础,再进入第 3 节之后的字符串函数;对于带「模拟实现」的小节,可以先自己动手写一遍,再对照文中的思路与代码进行验证,效果会更好。
目录
- 1.字符分类函数
- 2.字符转换函数
- 3.strlen的使用和模拟实现
- 4.strcpy的使用和模拟实现
- 5.strcat的使用和模拟实现
- 6.strcmp函数的使用
- 7.strncpy函数的使用
- 8.strncat函数的使用
- 9.strncmp函数的使用
- 10.strstr函数
- 11.strtok函数的使用
1.字符分类函数
在C语言中有一类函数是专门对字符进行分类的,就是一个字符属于什么类型的字符。
在使用这些函数之前,必须要包含对应的头文件:#include <ctype.h>
那么这些函数具体有哪些呢?如下图所示:

若参数符合函数对应的条件,则会返回一个非0数,不符合返回0。
对上面的一些概念做一下解释:

这些函数对字符的类型做出了分类,下面我们就简单用几个函数做一个演示:
isdigit函数原型:
c
int isdigit(int c);
根据函数原型,我们可以将字符填入到函数的参数部分:

islower函数原型:
c
int islower(int c);
演示:

ispunct函数原型:
c
int ispunct(int c);
演示:

在演示完几个函数之后,我们对字符分类函数有了一定的了解了,我们就可以来小试牛刀一下。
练习:
写一个代码,将字符串中的小写字母转大写,其他字符不变。
我们先来捋一下思路:

根据上面的思路,我们就可以尝试去写出以下代码了:

2.字符转换函数
C语言提供了2个字符转换函数:
c
int tolower(int c);//将参数传进去的大写字母转成小写,只对大写字母有作用
int toupper(int c);//将参数传进去的小写字母转成大写,只对小写字母有作用
我先来演示一下:

大小写转换就刚好可以去对上面练习的代码进一步的优化:

以下字符串函数(函数模拟实现除外)均要包含头文件:#include <string.h>
3.strlen的使用和模拟实现
strlen函数在之前的讲解中已经使用过了很多次,相信大家已经再熟悉不过了,但是在今天我们还是要带着大家在重新系统性的学一遍strlen函数相关的知识,让我们开始吧!
strlen函数原型:
c
size_t strlen(const char* str);
strlen函数的功能:统计在字符串中'\0'前面出现的字符个数(不包括'\0'),将统计到的个数作为返回值。
注意事项:
1.字符串以**'\0'作为结束标志,参数指向的字符串必须要以 '\0'结束。
2.使用strlen函数时,需要包含头文件: #include <string.h>**
3.函数的返回值为size_t类型的,是无符号整型,因为字符串的长度是非负数。
学习过以上知识之后,我们还是来一道练习题来巩固一下,代码见下图:

提问:这段代码的运行结果是什么?
我们知道str2的字符串长度是3,str1的字符串长度是6,3-6得到-3,那么是不是输出str1>str2呢?
我们来看一下运行结果:

令我们没有想到的是,它输出的竟然是str2>str1,这到底是什么原因呢?下面我来为大家解释一下:
首先strlen函数的返回值是无符号整型,我们在计算到结果为-3时,由于-3在内存中是以补码的形式存储,但是打印在屏幕上的是原码,我们将-3转化成2进制的原码,通过换算得到补码,因为-3是无符号整型,不存在最高位是符号位,所有位都是数值位,所以它的原反补码相同,无符号整型减去无符号整型还是无符号整型,计算出来的结果始终是大于0的数。

那么代码怎么改进呢?其实是由2种方法的,首先第一种方法是直接让strlen函数进行大小比较:

第二种方法就是将strlen函数的返回值进行强制类型转化,让它变成有符号整型,进行正常运算。

这道题讲完之后,紧接着我们来讲一下关于strlen函数的模拟实现:在之前我们已经讲过了2种方法,如下:
方式1:

方式2:

但是如果题目改成不能在函数内部创建临时变量,怎么求字符串的长度?
接下来,我讲一种新方法,这里涉及函数递归,我们先来分析一下思路:

通过以上思路,我们可以尝试写出代码来:

在这里解释一下,为什么不能用str++呢?
不能使用str++的原因:str++是先使用,再++,所以my_strlen函数会先把str存的地址传过去,然后在原来的位置上在再进行++,传过去str是原来的地址,指向的对象不变,没有经过++的运算,递归可能会陷入死循环。
在这里,++str是可以使用的,先进行str++,改变指针的指向,再将str传过去,是可以实现传过去的指针发生变化的,但是在某些场景下可能会存在风险,因为str原来的地址也会被改变。我们所写的代码中,str+1可以保证str存的地址不变的基础上,可以将str+1的新地址传给下一个my_strlen。
4.strcpy的使用和模拟实现
strcpy函数原型:
c
char* strcpy(char* destination, const char* source);
strcpy函数的功能:字符串拷贝,将源字符串逐字符复制到目标字符串中,包括结束符'\0'。
参数 :
destination:指针,指向目标空间
source:指针,指向源头数据
返回值 :
strcpy函数返回的目标空间的起始地址
学习了这些知识之后,你也许还不懂是什么意思,没关系的,接下来我们会通过举例来说明,结合知识去进一步的理解:
假设有2个字符串分别叫arr1、arr2,arr1中的字符串想要拷贝到arr2中去,我们会怎么做?
是直接把arr2赋给arr1吗?肯定不是

所以才有了strcpy函数,帮助我们去实现字符串的拷贝,演示一下,如下图:
使用strcpy函数完成拷贝,利用目标空间arr2的地址传给printf函数去打印:

另一种写法:将strcpy函数的返回值传给printf函数去打印,其实返回值也是目标空间arr2的地址

运行结果:

注意事项 :
1.源字符串必须以'\0'结束,才能知道拷贝到那里结束
2.会将源字符串中的'\0'拷贝到目标空间
3.目标空间必须足够大,以确保能放得下源字符串
4.目标空间必须可修改
对于注意事项的第4点,如果目标空间存放的是常量字符串,能实现字符串的拷贝吗?

肯定是不能实现的,因为常量字符串是不能被修改的,将源字符串拷贝到目标空间中,就要求目标空间是可以修改的,因为是常量字符串,所以不能被修改。
对于注意事项的第2点,拷贝时会将源字符串的'\0'拷贝到目标空间去一个验证:

我们在调试过程中去观察,发现确实会将源字符串的'\0'拷贝到目标空间中。
那么我还有一个问题,就是当源字符串中含有'\0'时,它还会将处于'\0'之后的字符拷贝到目标空间中吗?我们来验证一下:

结果是不会拷贝'\0'之后的字符了,strcpy函数在拷贝时只管把'\0'前(包括'\0')的所有字符拷贝到目标空间中。
接下来,我们就要去模拟实现一下strcpy函数,首先我们先来捋一下strcpy函数的思路:

通过上面的思路,我们可以尝试去写一下代码:

对于这段代码来说,其实还存在优化的空间,那么哪些部分可以优化呢?
我们先来对while循环内的循环体进行优化:

这样我们可以将循环体中解引用部分和++部分做了一个很好的优化,但是'\0'打印的部分没有进行优化,接着继续优化,就产生了我们下一个版本:

另外一种写法:

在循环过程中,要对指针解引用,是不是要保证2个指针不是空指针:

我们希望源字符串不能被修改,目标空间可以被修改:

至此,我们完成了这段代码的优化。
5.strcat的使用和模拟实现
strcat函数原型:
c
char* strcat(char* destination,const char* source);
strcat函数的功能:字符串追加,把source指向的源字符串中的所有字符都追加到destination指向的空间中。
参数 :
destination:指针,指向目标空间
source:指针,指向源头数据
返回值 :
strcat函数返回的目标空间的起始地址
接下来,我们先来演示一下:

运行结果:

注意事项 :
1.源字符串必须以'\0'结束,否则不知道追加到源字符串的哪里结束追加。
2.目标字符串也得有'\0',否则没办法知道从目标空间的哪里开始追加。
3.目标空间必须要足够大,能容纳下源字符串的内容。
4.目标空间必须可修改。
我们想知道这个函数也会将源字符串的'\0'追加到目标空间吗?验证如下:

我们可以观察到arr2的变化,'\0'从源字符串追加到了arr2的空间里头,说明了我们的猜测是对的。
接下来,我们来模拟实现一下这个函数:
思路分析:

依据上面的思路,我们来尝试写一下代码:

6.strcmp函数的使用
接下来,我们来学习一个字符串的比较函数,首先是函数的原型:
c
int strcmp(const char* str1,const char* str2);
**功能:**用来比较str1和str2指针分别指向的字符串,从两个字符串的第一个字符开始比较,如果两个字符的ASCLL码值相等,就比较下一个字符,直到遇到不相等的2个字符,或者字符串结束。
参数 :
str1:指针,指向要比较的第一个字符串的首字符
str2:指针,指向要比较的第二个字符串的首字符
返回值:
标准规定:第一个字符串大于第二个字符串,返回大于0的数,相等返回0,小于返回小于0的数。
接下来,我用以上函数的基本信息来简单写一段代码:

或者直接将字符串作为参数,直接写进括号里面:

对函数的使用有了一定的了解后,我来模拟实现一下strcmp函数:
思路分析:

依据上面的思路,我可以尝试写出一下代码:

我查看一下运行的结果:

以上我学习的这组字符串函数,是对字符串长度没有限制的函数,那么肯定是由对字符串长度有限制的函数了,我就继续学习了。
7.strncpy函数的使用
先来看一下函数的原型:
c
char* strncpy(char* destination,char* source,size_t num);
**功能:**字符串拷贝,将source指向的字符串拷贝到destination指向的空间中,最多拷贝num个字符
参数:
destination:指针,指向目标空间
source:指针,指向源头数据
num:从source指向的字符串中最多拷贝的字符个数
返回值:
strncpy函数返回的目标空间的起始地址
接下来,我用上面的知识写一段简单的代码:

运行结果:

假如我拷贝字符串超过源字符串的字符个数,目标空间字符串会发生什么变化呢?

调试截图:

首先它会拷贝过去源字符串的所有字符串,想要拷贝的字符个数与实际源字符串的个数差了几个,在拷贝过去的源字符串后补几个'\0'
**总结:**拷贝num个字符从源字符串到目标空间,如果源字符串的长度小于num,则拷贝源字符串之后,在目标的后边追加几个'\0',直到补充到num个
比较strcpy和strncpy函数 :
· strcpy函数拷贝到'\0'为止,如果目标空间不够的话,容易出现越界行为。
· strncpy函数指定了拷贝的长度,源字符串不一定要有'\0',同时在设计参数的时候,就会多一层思考:目标空间的大小是否够用,strncpy相对strcpy函数更加安全。
8.strncat函数的使用
strncat的函数原型:
c
char* strncat(char* destination,const char* source,sizt_t num);
**功能:**字符串追加;将source指向的字符串的内容,追加到destination指向的空间,最多追加num个字符。
参数:
destination:指针,指向了目标空间
source:指针,指向了源头数据
num:最多追加的字符的个数
**返回值:**返回的是目标空间的起始地址
学习了函数的相关知识之后,我来尝试简单的写一段代码:

运行结果:

但是在追加的时候是否会把'\0'拷贝到目标空间内,我来验证一下:

调试截图:

但是这能说明'\0'是从源字符串拷贝过来的吗?如果num小于源字符串的个数,那还会有'\0'拷贝过来吗?将num的值从3改为2,我来测试一下:

结果是这个'\0'不是源字符串拷贝过来的 ,而是在追加到num个源字符串的字符时,在末尾自动会补一个'\0'作为追加结束的。
如果num大于源字符串的字符个数,那么会发生什么情况呢?我们在上段代码的基础上,将num的值从2改为5,我来测试一下:

结果追加到源字符串末尾后,直接加一个'\0',作为追加结束的标志,不会用'\0'去真正追加到5个字符,就只是追加完源字符串的所有字符就行(不包括'\0')。
strcat和strncat对比:
· 参数不同,strncat多了一个参数
· strcat函数在追加的时候要将源字符串的所有内容,包括'\0'都追加过去,但是strncat函数指定了追加的长度。
· strncat函数中源字符串不一定要有'\0'了
· strncat更加灵活,也更加安全
9.strncmp函数的使用
strncat函数的原型:
c
int strncmp(const char* str1,const char* str2,size_t num);
**功能:**字符串比较;比较str1和str2指向的两个字符串的内容,最多比较num个字符。
参数:
str1:指针,指向一个比较的字符串
str2:指针,指向另一个比较的字符串
num:最多比较的字符个数
**返回值:**若第一个字符串大于第二个字符串,则返回大于0的数;相等返回0;小于返回小于零的数。
学习了函数的相关知识之后,我来尝试写一段简单的代码:

运行结果:

只比较前5个字符,两个字符串确实是相等的
strcmp和strncmp比较:
· 参数不同
· strncmp可以比较任意长度
· strncmp函数更加灵活,更加安全
10.strstr函数
strstr函数的原型:
c
char* strstr(const char* str1,const char* str2);
功能: strstr函数,查找str2指向的字符串在str1指向的字符串中出现的位置。简单来说,就是在一个字符串中找另一个字符串第一次出现的位置。
参数:
str1:指针,指向了被查找的字符串
str2:指针,指向了要查找的字符串
返回值:
· 如果str1指向的字符串中存在str2指向的字符串,那么返回第一次出现位置的指针
· 如果str1指向的字符串不存在str2指向的字符串,那么返回NULL
学习了函数的相关知识,我来尝试写一段简单的代码:

接下来,我来尝试模拟实现一下strstr函数:
思路分析:
由题目要求画出以下图:

只要被查找的字符串的指针指向'\0'时,就说明没有找到,这个可以作为循环的条件
第一次比较:

第二次比较:

第三次比较:

改进:

第四次比较:

第五次比较:

第六次比较:


依据上面的思路分析,我可以尝试去写出代码:

在特殊场景下的处理,当要查找的字符串是空字符串时:

11.strtok函数的使用
strtok函数的原型:
c
char* strtok(char* str,const char* delim);
功能:
· 分割字符串: 根据delim参数中指定的分隔符,将输入的字符串str拆分成多个子字符串。
· 修改原始字符串: strtok会直接在原始字符串中插入'\0'终止符,替换分隔符的位置,因此原始字符串会被修改。
参数:
1.str:首次调用时传入待分割的字符串;后续调用传入NULL,表示继续分割同一个字符串。
2.delim:包含所有可能分隔符的字符串(每个字符均视为独立的分隔符)。
返回值:
· 成功时返回指向当前子字符串的指针。
· 没有更多子字符串时返回NULL。
使用步骤
1.首次调用: 传入待分割字符串和分隔符。
2.后续调用: 传入NULL和相同的分隔符,继续分割。
3.结束条件: 当返回NULL时,表示分割完成。
看完使用步骤,是不是看起来有点困难,不用担心,接下来我来写一段代码,相信能够帮助你去更好的理解:

运行结果:

但实际上在分割完字符串的时候,返回的空指针并不能去打印,这里其实像是报了一个错误警告了。
事实上,strtok函数并不是这样用的,它可以借助for循环,无论要分割几段字符串,它都能一次性完成分割,参考代码如下:

注意事项
· 破坏性操作: strtok会直接修改原始字符串,将其中的分隔符替换为'\0'。如果需要保留原字符串,应先拷贝一份。
· 连续分隔符: 多个连续的分隔符会被视为单个分隔符,不会返回空字符串。
` 空指针处理: 如果输入的str为NULL且没有前序调用,行为未定义。
12.strerror函数的使用
strerror函数的原型:
c
char* strerror(int errnum);
功能:
1.strerror函数可以通过参数部分的errnum表示错误码,得到对应的错误信息,并且返回这个错误信息字符串首字符的地址。
2.strerror函数只针对标准库中的函数发生错误后设置的错误码转换。

参数:
errnum:表示错误码
这个错误码一般传递的是errno这个变量的值,在C语言有一个全局变量叫:errno,当库函数的调用发生错误的时候,就会将本次错误的错误码存放到errno这个变量中,使用这个全局变量需要包含一个头文件errno.h 。
返回值:
函数返回通过错误码得到的错误信息字符串的首字符的地址。
学习完函数的相关知识后,我来尝试写一段简单的代码,帮助大家更好的理解这个函数的使用:

运行结果:

后续我们还会见到库函数调用的一些错误,就不再过多在本文讲解了。
总结
本文围绕 ctype.h 与 string.h 两个头文件,梳理了 C 语言中最常用的一批字符与字符串函数,下面用要点列表做一次归纳。
一、ctype.h 字符分类与转换函数
- 字符分类函数 (
isdigit、islower、isupper、isalpha、isalnum、isspace、ispunct等):用于判断一个字符属于哪一类,参数符合条件时返回非 0 值,不符合返回 0。 - 字符转换函数 (
tolower、toupper):只对相应大小写的字母生效,其他字符原样返回,常与分类函数配合完成大小写转换。 - 注意事项 :参数类型为
int,传入的应当是unsigned char范围内的值或EOF;函数只做判断与转换,不会修改原字符,需要接收返回值。
二、string.h 字符串函数
strlen:统计'\0'之前的字符个数,返回size_t(无符号);字符串必须以'\0'结束。strcpy:把源字符串连同'\0'一起拷贝到目标空间;目标空间必须足够大且可修改。strcat:把源字符串追加到目标字符串末尾,并追加'\0';目标空间必须足够大且可修改。strcmp:按字符 ASCII 码值逐字符比较,返回大于 0、等于 0、小于 0 三种结果。strncpy:最多拷贝num个字符;源串长度不足num时用'\0'补齐,相对strcpy更安全。strncat:最多追加num个字符,追加结束后自动补一个'\0';源串不要求以'\0'结尾。strncmp:最多比较num个字符,比strcmp更灵活、更安全。strstr:在str1中查找str2第一次出现的位置,找到返回该位置指针,找不到返回NULL。strtok:按分隔符拆分字符串,会直接修改原串(把分隔符替换为'\0'),首次传字符串、后续传NULL。strerror:把错误码(通常取errno)转换成对应的错误信息字符串,需包含errno.h。
三、学习建议
- 先手写模拟实现,再对照标准库 :对
strlen、strcpy、strcat、strcmp、strstr这类函数,先自己按思路写一遍,再对照标准库的行为与边界处理,理解会更牢固。 - 注意
size_t的无符号陷阱 :strlen等函数返回无符号整型,两个无符号数相减不会得到负数,比较时容易出错,必要时先强制转换为有符号类型或直接比较。 - 优先使用带
n的安全版本 :在长度不确定的场景下,优先选择strncpy、strncat、strncmp,并始终确认目标空间大小,避免越界。
参考资料
- C 标准库文档(
ctype.h、string.h各函数的原型与行为说明) - cppreference 上关于 C 标准库头文件
ctype.h、string.h的条目,可查阅各函数的详细定义与边界情况 - 各编译器(如 GCC、MSVC)自带的库函数手册与调试实践