目录
[三、strlen 的使用和模拟实现](#三、strlen 的使用和模拟实现)
[四、strcpy 的使用和模拟实现](#四、strcpy 的使用和模拟实现)
[五、strcat 的使用和模拟实现](#五、strcat 的使用和模拟实现)
[六、strcmp 的使用和模拟实现](#六、strcmp 的使用和模拟实现)
一、字符分类函数
(1)字符分类函数解析
C语言中有一系列函数,是专门用来将字符分类的函数
使用它们需要包括头文件#include <ctype.h>
| 函数 | 返回真的条件 |
|---|---|
iscntrl |
任何控制字符(不可打印,ASCII 0~31、127) |
isspace |
空白字符:空格 、换页 \f、换行 \n、回车 \r、水平制表符 \t、垂直制表符 \v |
isdigit |
十进制数字字符 '0' ~ '9' |
isxdigit |
十六进制数字:0~9、a~f、A~F |
islower |
小写字母 a~z |
isupper |
大写字母 A~Z |
isalpha |
字母 a~z 或 A~Z |
isalnum |
字母 或 数字(a-z、A-Z、0-9) |
ispunct |
标点符号:可打印、非数字非字母的图形字符、非空格 |
isgraph |
任何图形字符(可打印、非空白) |
isprint |
任何可打印字符(图形字符 + 空白字符) |
我们来看看这些函数是如何使用的,所以符合条件,则返回非0的数,如果不符合,则返回0
cpp
#include <stdio.h>
#include <ctype.h>
int main()
{
char arr[] = "A 9!\n\tc"; // 大写字母、空格、数字、标点、换行、制表、控制字符(响铃)
int i = 0;
while (arr[i] != '\0')
{
char ch = arr[i];
printf("字符:'%c' ASCII:%d\n", ch, ch);
printf(" iscntrl : %d(控制字符)\n", iscntrl(ch));
printf(" isspace : %d(空白字符)\n", isspace(ch));
printf(" isdigit : %d(十进制数字)\n", isdigit(ch));
printf(" isxdigit : %d(十六进制数字)\n", isxdigit(ch));
printf(" islower : %d(小写字母)\n", islower(ch));
printf(" isupper : %d(大写字母)\n", isupper(ch));
printf(" isalpha : %d(字母)\n", isalpha(ch));
printf(" isalnum : %d(字母/数字)\n", isalnum(ch));
printf(" ispunct : %d(标点符号)\n", ispunct(ch));
printf(" isgraph : %d(图形字符)\n", isgraph(ch));
printf(" isprint : %d(可打印字符)\n", isprint(ch));
printf("------------------------------------\n");
i++;
}
return 0;
}
运行截图:我们来一个截图一个截图分析
iscntrl:ASCLL码0‑31、127 → 控制字符=不可打印,用来控制设备、格式、光标isprint:ASCLL码32‑126 → 可打印字符 = 空格 + 图形字符isgraph:ASCLL码33‑126 → 图形字符 = 可打印,可看到,不包括空格)
:
字符'A'是十六进制其中的表示10的一个字母,而且是大写字母,因为可以看见,所以同时也是一个图形字符,也是可打印字符,所以在isxdigit,isupper,isalpha,isalnum,isgraph,isprint这几个函数中都会返回一个非0的数,其它返回0
:
字符' '中是一个空白字符,而且是可以打印出来能看到的,所以isspace和isprint这两个函数会返回一个非0的数,其它返回0
:
字符'9',是十进制数字,同时也可以表示十六进制数字,同时因为可以看见,而且不是空格,所以也是图形字符,也是可打印字符,所以isxdigit,isalnum,isgraph,isprint都会返回一个非0的数,其它返回0
:
'!'是标点符号,也是图形符号,也是可打印字符。所以ispunct,isgraph,isprint都会返回一个非0的数,其它返回0
:
'\n',\n被隐藏直接对格式产生效果,'\n'不管是以字符形式打印,还是放在函数中,都会直接产生效果,只要输出 \n,都会立刻执行换行动作,所以上面图片我们看到的就是起了换行效果后的,'\n'是控制字符,因为看不见,所以同时也是空白字符,所以iscntrl,isspace会返回一个非0的数,其它返回0
:
ASCII为9的字符是水平制表符'\t',也是看不见的控制字符,所以也是空白字符,iscntrl,isspace会返回一个非0的数,其它返回0
这里我们可能会有一个疑惑'\t'不是占八格吗,在控制台中,为何这里打印出来只占了一格?我们来解惑一下:
'\t'是以每八个字符为一条制表线,八个字符八个字符分割开
'\t'的规则是每次移动,都是直接跳到下个制表线,不管前面有几个字符,都会直接跳到下个制表线的位置,中间无字符的位置就补空格,比如我们来看一串代码:
cpp
#include <stdio.h>
int main()
{
// 用 | 标记每一列,方便看空格
printf("12345678|2345678|2345678|\n");
printf("a\tb\n"); // a在第1列,跳到第9列 → 空7格
printf("abc\tb\n"); // abc在第3列,跳到第9列 → 空5格
printf("abcd\tb\n"); // abcd在第4列,跳到第9列 → 空4格
printf("abcdefgh\tb\n"); // 第8列,直接跳到下一个8的倍数
printf("abcdefgh\tb\tc\n");
return 0;
}
运行截图:

不管'\t'前面有几个字符,比如第三句printf("abcd\tb\n")打印出的结果,'\t'不是从d开始往后计算八个空格的,而是从第一个字符就开始计算了,八个字符为一个制表位,所以b就会打印在8的后面,因为这里刚好是第一个制表位的位置;接着我们再看printf("abcdefgh\tb\n")打印出的结果,前面的abcdefgh已经占满了八个字符,所以第一个制表位之前已经被占满了,所以此时我们就会直接跳到下个制表位,中间就会空八个空白字符,然后再打印b;printf("abcdefgh\tb\tc\n")这句话中,前面和上一句一样,我们已经打印了b在第二个制表位的位置,制表位其实是不占字符的,是我们虚拟出来的,所以上图中b对应的最上方的那条线,其实就是我们省略的1,因为在控制台中|也占一个字符,我们无法省略,实际上制表位的位置应该是12345678|12345678|12345678|这样的,所以这个b其实就是排在上个制表符紧接着的,已经占了一个字符,到下个制表符还差七个字符,所以我们中间又跳过了七个字符去打印了c,这就是'\t'打印的规则
所以在printf("字符:'%c' ASCII:%d\n", ch, ch)中,首先我们要知道一个中文字会占两个字符,所以前面的字符两字已经占了四个字符,:是中文的标点符号,也占两个字符的位置,紧接着'占一个字符,此时已经占了七个字符的位置了,我们再用%c打印'\t',此时距离下一个制表符就只剩下一个字符的位置,又因为'\t'是无法打印在屏幕上看见的,所以我们就只打印出一个空白字符,就跳到了下一个制表线的位置,然后打印了我们的' ASCII:%d\n,如图所示,只打印出一个空白字符:
:
这个打印的字符是'\x07',这个字符的意思就是将十六进制07转化成十进制,然后找到对应的ASCLL码值的字符,所以'\x07',07转化为十进制就是7,ASCLL码值为7的字符是'\a'字符,这是个控制字符(响铃符),打印的时候电脑会鸣一声,所以iscntrl会返回一个非0的数,其它返回0
(2)代码练习
我们来一串代码练习,写一串代码,将小写字母全部转化成大写字母:
char str[] = "xxx":在栈区上开一块内存,将静态区的字符串复制进去,可修改char *str = "xxx":字符串放在只读常量区(静态区中),指针只是指向它,不能改
cpp
#include <stdio.h>
#include <ctype.h>
int main()
{
int i = 0;
char str[] = "Test String.\n";
int sz = sizeof(str) / sizeof(str[0]);
while (str[i]!='\0')
{
if (islower(str[i]))
{
str[i] -= 32;
}
i++;
}
printf("%s\n", str);
return 0;
}
我们用islower判断字符是否是小写字符,字符实际上存储的就是整型数组ASCLL码值,是的话我们就将小写字符的ASCLL码值减32得到大写字母,最后打印出修改后的字符串
运行截图:

二、字符转换函数
(1)tolower和toupper的使用
C语言中提供了两个字符转换函数:使用它们也需要包括头文件#include <ctype.h>
cpp
int tolower ( int c ); 将参数传进去的⼤写字母转⼩写
int toupper ( int c ); 将参数传进去的⼩写字母转⼤写
那么此时对于上面我们将小写字母转化成大写字母的代码,就可以进行优化改造:
cpp
#include <stdio.h>
#include <ctype.h>
int main()
{
int i = 0;
char str[] = "Test String.\n";
int sz = sizeof(str) / sizeof(str[0]);
while (str[i] != '\0')
{
if (islower(str[i]))
{
str[i] = toupper(str[i]);
}
i++;
}
printf("%s\n", str);
return 0;
}
运行截图:

但是我们要注意一点的是:toupper和tolower函数,并不会真正的将里面的参数原来的字符修改,而是把修改后的值当做返回值返回回来,如果不接收,那相当于把修改后的值直接丢弃不去使用,所以我们用str[i] = toupper(str[i])将修改后的值接收,才能将修改的结果覆盖原字符
三、strlen 的使用和模拟实现
(1)strlen的使用
strlen库函数的原型与作用:
cpp
size_t strlen(const char* str);
- 用于统计字符串'\0'之前的字符个数,并将这个字符个数当做返回值返回
- 注意返回值的类型是size_t类型
- 使用strlen需要包括头文件#include <string.h>
下面我们来看一下strlen函数的一个使用场景:用于比较字符个数,有坑!!!
我们先自己看看下面代码,思考运行结果,再去看下面的运行截图
cpp
#include <stdio.h>
#include <string.h>
int main()
{
const char* str1 = "abcdef";
const char* str2 = "bbb";
if (strlen(str2) - strlen(str1) > 0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
这是一个很坑的**C 语言经典陷阱题,**很容易让人误以为(strlen(str2) - strlen(str1))<0,然后打印"srt1>str2\n",但实际上不是这样子的,我们要注意,strlen函数的返回值是size_t类型,是一个无符号整型,两个无符号整型的数相减,得到的还是一个无符号的数,上述代码中因为strlen(str2)==3,strlen(str1)==6,所以strlen(str2) - strlen(str1)==-3,但因为-3是一个size_t类型,所以-3会被看做成一个很大的正数,因为它的补码中所有位包括原来的符号位都被看作成有效数值位,就会变成一个很大的正数
运行截图:

我们来看在32位电脑下,-3的原反补码:
- -3的原码:10000000 00000000 00000000 00000011
- -3的反码:11111111 11111111 11111111 11111100
- -3的补码:11111111 11111111 11111111 11111101
当-3被当做无符号正型来看待时,-3的补码就会被全当成有效数值位,最前方的1不再表示负数,当无符号位时,我们默认这串二进制表示的数是正数,所以就会得到一个非常巨大的正数
-3的补码:11111111 11111111 11111111 11111101全部是有效数值位的时候,得到的数是4294967293,我们来写串代码验证一下:
cpp
#include <stdio.h>
int main()
{
size_t a = 3, b = 6;
size_t ret = a - b;
printf("%zu\n", ret);
return 0;
}
运行截图:

所以最上方我们的代码思考题中,strlen(str2) - strlen(str1)的得到的结果,确实是个大于0的数
(2)strlen的模拟实现
(a)方法一:计数器的实现
cpp
#include <stdio.h>
#include <assert.h>
//计数器方式实现strlen
size_t my_strlen(const char* str)
{
int count = 0;
assert(str); //assert断言,保证指针不为空
while (*str)
{
count++;
str++;
}
return count;
}
//主函数
int main()
{
char* str = "abcde";
size_t ret = my_strlen(str);
printf("%zu\n", ret);
return 0;
}
利用count计数,count初始化为0,用while循环从str的起始地址开始往后遍历,如果不是'\0',count就++,str也++,继续判断下一个字符是否为'\0',不是的话就重复前面的动作,是的话就终止循环,count和str都不在++,将count的值返回,count就是我们'\0'之前的字符的个数
运行截图:

(b)方法二:不能创建临时变量计数器
cpp
#include <assert.h>
#include <stdio.h>
//递归实现 strlen
int my_strlen(char* str)
{
assert(str);
if (*str == '\0') // 递归出口:遇到结束符,返回0
return 0;
return 1 + my_strlen(str + 1); // 1 + 剩余字符串长度
}
int main()
{
char* str = "abcde";
size_t ret = my_strlen(str);
printf("%zu\n", ret);
return 0;
}
我们采取递归的方法,首先我们先判断一下str是否为空指针,因为我们没法对空指针进行解引用加减等操作,程序会直接崩溃,所以为了防止我们不小心传了一个空指针,我们首先进行了一个assert断言,接着我们采取递归的方法,如果该字符是'\0',就返回0,如果不是就返回1+my_strlen(str + 1),也就是1加上剩余字符串的长度,就这样反复递推出去,然后再回归计算,最终会得到'\0'字符之前的字符个数
运行截图:
这里我们补充一个知识点:'\0'不等于空指针,不要误以为指向'\0'就等于指向空指针,'\0'是ASCLL码值为0的合法字符,指针可以指向此;空格' '也是ASCLL码为32的合法字符,指针也可以指向此,只有指针变量存储的值是NULL,才是空指针
(c)方法三:指针-指针的方式
cpp
#include <assert.h>
#include <stdio.h>
//指针-指针方式实现strlen
int my_strlen(char* str)
{
assert(str); // 1. 防止空指针
char* start = str; // 2. 记录【字符串起始地址】
while (*str) // 3. 遍历到末尾 '\0'
{
str++;
}
char* finish = str; // 4. 记录【字符串结束地址】
return finish - start; // 5. 【指针 - 指针】= 元素个数
}
int main()
{
char* str = "abcde";
size_t ret = my_strlen(str);
printf("%zu\n", ret); // 输出:5
return 0;
}
我们采取指针-指针的方式,因为我们知道,指针-指针得到的就是这两个地址之间,相差的元素个数,那我们只要知道了起始地址,和'\0'的地址,再相减,得到的就是我们'\0'之前的字符个数,所以我们先用指针变量start记录起始位置,再用while循环,让str指向'\0',然后我们用指针变量finish来储存指向'\0'的地址,最终用finish - start,两个地址相减,就得到了'\0'和起始位置之间的元素个数,也就是'\0'之前的字符个数
运行截图:
四、strcpy 的使用和模拟实现
(1)strcpy的使用
strcpy库函数的原型与作用:
cpp
char* strcpy(char* destination, const char* source);
- 用于将源字符串拷贝到目标空间,目标空间必须可修改,而且足够大,需要放的下源字符串(包括'\0')
- 会覆盖 destination 原有内容,连同source中的 '\0' 一起复制
- 函数返回值返回的是destination的起始地址
- 源字符串必须以
\0结尾,如果没有,就会一直往后拷贝,造成越界访问 - 不能自己拷贝自己(源和目标需是同一块空间),属于未定义行为
- 使用strcpy需要头文件#include <string.h>
我们来看看具体的使用方法:
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char dest[20]="!!!!!!!!!!"; // 足够大的可修改数组
char src[] = "hello";
strcpy(dest, src);
printf("%s\n", dest); // hello
return 0;
}
运行截图:
但我们思考一下,使用 strcpy 拷贝字符串时,目标空间 dest 是被全部覆盖,还是仅覆盖源字符串 src 长度范围内的字符?下面我们来调试看一下:

可以看到,我们仅仅是将src内的字符串hello'\0'复制了进去,覆盖了该范围内的字符,但是dest空间原有未被覆盖的字符'!'依然存在
(2)strcpy的模拟实现
cpp
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest,const char* src)
{
assert(dest && src); //检查两个指针都不为空
char* start = dest;
while (*dest++ = *src++)
;
return start;
}
int main()
{
char dest[20]; // 足够大的可修改数组
char src[] = "hello";
char* ret = my_strcpy(dest, src);
printf("%s\n", ret);
return 0;
}
因为我们在拷贝src的字符串去dest时,dest的地址也会自增,就不会指向dest的初始起始地址了,所以我们用一个char*类型的指针变量start,去存放我们dest的初始起始地址,接着我们写了一个循环while (*dest++ = *src++),将src字符串中的字符逐个拷贝进dest字符数组中,当*src为'\0'时,整个 (*dest++ = *src++)表达式的结果就为'\0',退出循环,此时已经将src中的字符串包括'\0'全部拷贝进dest,然后我们再返回dest的起始地址start,就成功做到模拟strcpy的实现
运行截图:

五、strcat 的使用和模拟实现
(1)strcat的使用
strcat库函数的原型与作用:
cpp
char* strcat(char* dest, const char* src);
- 用于将字符串拼接到目标字符串中,目标空间必须足够大,而且可以修改
- 两个参数dest和src都必须以 \0 结尾
- 不能直接自己拼接自己,必须先备份原字符串,再拼接
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char a[50] = "Hello "; // 数组要足够大
char b[] = "World";
strcat(a, b); // 把b拼接到a后面
printf("%s\n", a); // Hello World
return 0;
}
运行截图:

可以看到,确实是拼接到了Hello 的后面,但是如何拼接的呢,让我们来调试一下看看:

上面是刚开始未拼接,未使用strcat函数时,a数组中存放的值,可见我们存放了'H','e','l','l','o',' ','\0'和若干个'\0',下面我们再来看看拼接之后的数组a:

可见我们找到了目标数组a中字符串末尾的'\0',并用数组b中的字符串给覆盖掉了,所以我们拼接的规则是,覆盖掉目标空间的'\0',再将src中的字符串包括'\0'进行拼接
但我们要注意一点!!!strcat拼接字符串函数,我们C语言规定,这个函数不能用于自己拼接自己,当我们自己拼接自己时,我们首先找到了dest末尾的'\0',并从'\0'开始用src的字符逐个复制覆盖,但当我们复制完一遍时准备复制'\0'时,发现'\0'已经被我们修改成src的首个字符了,因为src=dest,所以src就找不到'\0'了,程序就会死循环,一直往后越界访问,直到找到'\0'
总而言之,记住:C语言规定!!!strcat函数不能用于自己拼接自己,这是规定,规定!!!
(2)strcat的模拟实现
cpp
#include <stdio.h>
#include <assert.h>
char* my_strcat(char* str1,const char* str2)
{
assert(str1&&str2);
char* start = str1;
while (*str1)
{
str1++;
}
while (*str1++ = *str2++);
return start;
}
int main()
{
char a[50] = "Hello "; // 数组要足够大
char b[] = "World";
char* ret = my_strcat(a, b);
printf("%s\n", a);
return 0;
}
我们首先把str1的起始地址储存给start指针,然后while循环,找到让str1指向目标字符串的'\0',然后我们用while (*str1++ = *str2++)进行从str1的'\0'处开始覆盖,覆盖完成后最终返回start指针,完成打印
运行截图:

六、strcmp 的使用和模拟实现
(1)strcmp的使用
strcmp库函数的原型与作用:
cpp
int strcmp(const char* str1, const char* str2);
- 用于比较两串字符串是否相同,两串字符串都是不能被修改的字符串
- 比较规则:从起始位置开始,逐个字符比较ASCLL码的值大小,如果str1>str2,则返回一个大于0的数;如果str1==str2,则返回0;如果str1<str2,则返回一个小于0的数
- 在vs中,str1>str2,返回1; str1==str2,返回0; str1<str2,返回-1
- 使用strcmp库函数必须包含头文件:#include <stdio.h>
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char a[] = "Hello bit";
char b[] = "Hello";
printf("%d\n", strcmp(a, b));
return 0;
}
a数组字符串比b数组字符串大,所以strcmp会返回1
运行截图:

(2)strcmp的模拟实现
cpp
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char a[] = "Hello";
char b[] = "Hello bit";
printf("%d\n", my_strcmp(a, b));
return 0;
}
我们写了一个while循环,如果*str1 == *str2,两个字符串的一对字符如果相等,我们就让str1++,str2也++,当我们*str1='\0'时,如果还能进while (*str1 == *str2)循环,就说明*str2就是等于*str1等于'\0',所以我们写个这时我们写个if语句,if (*str1 == '\0'),我们就return0,证明两字符串相等,如果从while循环出来了,就说明两个字符串的这对字符是不相等的,我们返回*str1 - *str2后的值,如果*str1>*str2,那么*str1 - *str2就会>0;如果*str1<*str2,*str1 - *str2就会<0,这个值具体等于多少,取决于它们ASCLL码值相减的结果
运行截图:

比较到a的'\0'和b的'空格'时发现两串字符串不一样,返回'\0'减去'空格'的ASCLL码值,也就是0-32=-32,刚好符合图片
感谢大家的观看,新人求互三,关注我必回关!下章见!
