0基础C语言积跬步之字符函数与字符串函数(上)

目录

一、字符分类函数

(1)字符分类函数解析

(2)代码练习

二、字符转换函数

(1)tolower和toupper的使用

[三、strlen 的使用和模拟实现](#三、strlen 的使用和模拟实现)

(1)strlen的使用

(2)strlen的模拟实现

[四、strcpy 的使用和模拟实现](#四、strcpy 的使用和模拟实现)

(1)strcpy的使用

(2)strcpy的模拟实现

[五、strcat 的使用和模拟实现](#五、strcat 的使用和模拟实现)

(1)strcat的使用

(2)strcat的模拟实现

[六、strcmp 的使用和模拟实现](#六、strcmp 的使用和模拟实现)

(1)strcmp的使用

(2)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~zA~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,刚好符合图片

感谢大家的观看,新人求互三,关注我必回关!下章见!

相关推荐
hhb_6183 小时前
Swift核心技术难点与实战案例解析
开发语言·ios·swift
一楼的猫3 小时前
从工具链视角对比:番茄作家助手 vs 第三方写作辅助方案
java·服务器·开发语言·前端·学习·chatgpt·ai写作
程序leo源3 小时前
Qt窗口详解
开发语言·数据库·c++·qt·青少年编程·c#
東隅已逝,桑榆非晚3 小时前
字符函数和字符串函数
c语言·笔记
likerhood3 小时前
Java static 关键字从浅入深
java·开发语言
猫猫的小茶馆4 小时前
【Python】函数与模块化编程
linux·开发语言·arm开发·驱动开发·python·stm32
计算机安禾4 小时前
【c++面向对象编程】第38篇:设计原则(二):里氏替换、接口隔离与依赖倒置
开发语言·c++
_院长大人_4 小时前
Java Excel导出:如何实现自定义表头与字段顺序的完全控制
java·开发语言·后端·excel
code_whiter4 小时前
C++1进阶(继承)
开发语言·c++