Error[8]: Undefined offset: 156, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:[+++]VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,[+++]判断依据为目标字符地址与源字符地址的大小。

[+++]

  图解从前往后拷贝与从后往前拷贝的区别:

[+++]

🌃[+++]

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 157, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,[+++]判断依据为目标字符地址与源字符地址的大小。

[+++]

  图解从前往后拷贝与从后往前拷贝的区别:

[+++]

🌃[+++]

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 158, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

[+++]

  图解从前往后拷贝与从后往前拷贝的区别:

[+++]

🌃[+++]

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 159, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

[+++]

🌃[+++]

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 160, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃[+++]

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 161, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,[+++]确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 162, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

[+++]
[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 163, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 164, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

  • [+++]
  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 165, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

  • [+++]
  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 166, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

  • [+++]

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 167, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,[+++]

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 168, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃[+++] 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 169, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,[+++]因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 170, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

[+++]

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 171, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

使用注意事项:

  • [+++]
  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 172, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

使用注意事项:

  • [+++]
  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 173, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

使用注意事项:

  • [+++]


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 174, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

使用注意事项:


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。[+++]对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
C语言进阶——字符串函数&&内存函数_C_内存溢出

C语言进阶——字符串函数&&内存函数

C语言进阶——字符串函数&&内存函数,第1张

🌇🌆🌃本文已收录至:C语言——梦想系列_Yohifo的博客-CSDN博客

更多知识尽在此专栏中!

目录

🌇前言

🌇正文

🌆字符串函数

🌉长度不可控的字符串函数

🌃strlen 长度统计

🌃strcpy 拷贝

🌃strcmp 比较

🌃strcat 追加

🌉长度可控的字符串 *** 作函数

🌃strncpy 可控拷贝

🌃strncmp 可控比较

🌃strncat 可控追加

🌉特殊字符串函数

🌃strstr 寻找

🌃strtok 分割

🌃strerror 报错 

🌆字符分类函数

🌃isdigit 十进制判断

🌃isxdigit 十六进制判断

🌃isupper 大写判断

🌃islower 小写判断

🌃toupper 转为大写

🌃tolower 转为小写

🌆内存函数

🌃memcpy 拷贝

🌃memmove 移动

🌃memcmp 比较

🌃memset 设置 

🌇总结


🌇前言
题目描述及其要求

  这是牛客网上的一道简单题:判断输入字符是否为字母,一般的解决方法是通过ASCII码判断,不过这样做的话判断表达式较长,此时我们可以利用C语言中的库函数isalpha(判断是否为字母) 来完成这个题目,不仅代码量少,而且通俗易懂。要实现这种效果,就需要学习C语言中的各种库函数,而本文会列出大多数字符串函数和内存函数的使用及其实现,如果你想学习C语言库函数或对字符串、内存有好奇之心,不妨仔细来看看吧!🎉🎉🎉

此文章分为三部分:字符串函数、 字符分类函数、内存函数。

通过库函数简化后的代码

🌇正文

  首先我们从字符串函数开始介绍,顾名思义,字符串函数就是为字符串设计的函数,我们比较熟悉的有字符串长度统计函数 strlen、字符串比较函数 strcmp,除此之外还有很多实用的字符串函数,比如字符串追加、字符串分割、字符串寻找等等,话不多说,让我们直接进入主题:

🌆字符串函数 🌉长度不可控的字符串函数

下面介绍的是对目标字符串 *** 作长度不可控的函数,使用场景相对有限。

🌃strlen 长度统计

  strlen 是用来统计字符串长度的一个库函数,因为字符串有个重要特征:以'

这是 strlen 的标准格式
'作为结束标志。strlen 正是根据这一特点,从首地址开始,逐个比对统计,得出此字符串的长度。当然如果没有结束标记,strlen 就会一直往后找,得出一个随机值。

strlen 计算字符串长度
strlen 统计的是 使用时,必须包含结束标志 之前出现的字符个数

 

使用注意事项:

  • 返回值是 size_t (unsigned int 无符号整型)
  • 我们可以使用一个临时指针记录起始位置,当我们的源指针指向结束标志时,循环结束,将两个指针相减,就能得到元素个数(关于指针 - 指针得到元素个数) ,也就是字符串长度。
  • //strlen 计算字符串长度 size_t myStrlen(const char* p) { assert(p);//断言,防止空指针 char* tmp = p;//记录起始位置 while (*p) { p++;//在循环内指向+1 *** 作,避免位置出错 } return (size_t)(p - tmp);//指针 - 指针得到元素个数 } int main() { char* pa = "Hello World!"; //size_t len = strlen(pa); //printf("库函数实现结果:\n%zu\n", len); size_t len = myStrlen(pa);//此时调用我们写的函数 printf("模拟函数实现结果:\n%zu\n", len); return 0; }

模拟实现 strlen

  这个函数的工作原理我们已经清楚了,可以试着模仿库函数的方式,写出一个属于自己的 strlen。

  既然是模仿库函数,那么在返回类型、参数类型等方面要和库函数一致,在统计长度前,strcpy 拷贝下面来看看具体代码实现吧:

数组空间也要足够大,不然装不下源字符串就尴尬了。

  同样的,我们使用之前的示例来验证此函数的可行性 ,可以看到结果与库函数一致。

🌃
strcpy 标准格式

  字符串拷贝需要两个字符串,字符数组 dest(目标) 与字符串 src(源),strcpy 中只需要这两个参数,即把源字符串内容拷贝到目标字符数组中(源字符串中的结束标志也会拷贝),其中要确保字符数组  dest 可改变,源字符串中必须包含 源字符串中的 目标空间必须足够大,能够装下源字符串 会拷贝到目标字符数组中

目标空间必须是可修改的 

使用注意事项:

  • 当源字符串中的首元素拷贝到目标字符数组中后仍然位于首位置,也就是说两个字符串元素拷贝位置是同步的,既然源字符串中的结束标志也要拷贝过去,
  • //strcpy 字符串拷贝 char* myStrcpy(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录起始位置 //当*src 为结束标志并赋给 *dest时,整体为假, //循环终止,目标数组也拿到了结束标志 while (*dest++ = *src++) { ;//空语句 } return tmp;//返回起始地址 } int main() { char arr1[20] = "xxxxxxxxx"; char arr2[] = "Hello!"; //printf("库函数实现结果:\n%s\n", strcpy(arr1, arr2)); printf("模拟函数实现结果:\n%s\n", myStrcpy(arr1, arr2)); return 0; }
  • strcmp 比较
  • strcmp 标准格式

模拟实现 strcpy 

  同样的,我们可以对这个函数进行模拟实现,拷贝的本质就是赋值,

strcmp 的返回值
那么我们就可以将其和赋值写进一个循环判断条件中(这样会构成一段非常奇妙的代码),这样一来我们整个程序的可读性就很不错了。

字符串大小比较时,与长度无关

  当然,我们的模拟函数也能实现需求 

🌃从首字符开始,逐字符比较

  字符串比较函数是从首字符开始比较,通过字符对应的ASCII码值对比,就能知道大小,如果str1>str2,返回1,如果str1因为比较并不需要改变值,所以使用常量字符串也能比较。

通过字符对应的ASCII码值做对比
即指向 str1 的指针 dest、指向 str2 的指针 src,对两个指针解引用后的值进行比较,如果相同就同时向后偏移,直到找到不同的值或移动到结束标志处停止,

使用注意事项:

  • strcat 追加
  • 追加,就是在目标字符数组的末尾(strcat 无法自己给自己追加,因为在追加过程中,目标字符数组结束标志会被覆盖掉,处)添加源字符串的值,
  • strcat 标准格式

模拟实现 strcmp 

  我们可以通过指针的移动来模拟实现这个函数,源字符串和目标字符数组中都必须有目标空间必须足够大最后再分情况确定返回值就行了。

//strcmp 字符串比较
int myStrcmp(const char* dest, const char* src)
{
	assert(dest && src);//断言
	//当找到不同数或移动到\0处时,循环停止
	while (*dest == *src && (*dest || *src))
	{
		dest++;//没有找到不同的元素
		src++;//需要向后偏移
	}
	//分情况判断,确定返回值
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	printf("库函数实现结果:\n%d\n", strcmp(str1, str2));
	//printf("模拟实现函数结果:\n%d\n", myStrcmp(str1, str2));
	return 0;
}

  使用模拟函数通过测试用例: 

🌃目标空间必须可修改,所以是字符数组 

  就需要把指向首地址处的指针 dest 移向尾地址,比如目标字符串数组中为abcd,源字符串为1234,经过追加后,字符数组就变为了abcd1234。值得一提的是,将此时的尾地址看作首地址2,将源字符串中的元素从此处开始拷贝至目标字符数组中,导致源字符串(其实就是目标字符,因为是自己给自己追加)中的结束标志也消失了,追加过程会无法停止。

//strcat 字符串追加 char* myStrcat(char* dest, const char* src) { assert(dest && src);//断言 char* tmp = dest;//记录目标字符数组首地址 while (*dest) { dest++;//将指针dest移动至尾元素处 } //类似 strcpy 拷贝 *** 作 while (*src) { //判断条件用 *src就行了 *dest++ = *src++;//确保源字符串中的每个元素都能追加上 } return tmp;//返回首地址 } int main() { char arr1[20] = "ABCD"; char arr2[] = "1234"; //printf("库函数实现结果:\n%s\n", strcat(arr1,arr2)); printf("模拟函数实现结果:\n%s\n", myStrcat(arr1, arr2)); return 0; }

使用注意事项:

  • strncpy 可控拷贝
  • n 的含义是长度可控,即在传递参数时,需要一个额外变量控制拷贝的字节数,
  • 且如果选定的n字节中没有包含结束标志
    strncpy 标准格式
    ,则在拷贝结束后,strncpy 会自动添加一个使用注意事项:

模拟实现 strcat

  既然是在目标字符数组的末尾处追加字符,目标空间必须足够大当然在移动前要保存此地址,目标空间必须可修改这样就完成了追加的 *** 作,最后再返回之前记录的首地址就行了。

源字符串中不一定要有循环终止条件标为 k,在循环结束后,还要对 *dest 再一次进行赋值,即 //strncpy n个字符串拷贝
char* myStrncpy(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//循环k次,确保拷贝足够的字节数
	while (k--)
	{
		*dest++ = *src++;//类似拷贝的 *** 作
	}
	*dest = 'strncmp 可控比较';//再次给目标字符数组中的元素赋值
	return tmp;//返回起始地址
}
int main()
{
	char arr1[20] = "xxxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncpy(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncpy(arr1, arr2, 5));
	return 0;
}(函数会自己添加)

🌉长度可控的字符串 *** 作函数

下面开始介绍字符串 *** 作函数的升级版,这些函数使用起来更加自由方便。

🌃当然控制长度不能超过源字符串的长度,不然是无意义的

  相比于前面的 strcpy,strncpy 多了一个字母n,

strncmp 标准格式
相较于前面的固定拷贝,这里的可控拷贝更为灵活,
返回值与 strcmp 完全一致

与 strcmp 基本一致

控制比较字节数不能为负数 

  • strncat 可控追加
  • 同所有可控家族成员一样,strncat 也会自动添加结束标志 
    strncat 标准格式
  • 目标字符数组中必须有目标空间必须足够大

模拟实现 strncpy

  这个模拟实现也比较简单,注意两点就行了,目标空间必须可修改这样一来就能和 strncpy 一样了。

源字符串中可以不包含1.循环判断条件 2.最后 //strncat n个字符串追加
char* myStrncat(char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	char* tmp = dest;//记录起始位置
	//使指针移向目标字符数组的末尾处
	while (*dest)
	{
		dest++;
	}
	//判断条件为k,即传入的控制字节数
	while (k--)
	{
		*dest++ = *src++;//确保每个元素都能追加
	}
	return tmp;//返回目标字符数组的起始地址
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[] = "Hello World!";
	//printf("库函数实现结果:\n%s\n", strncat(arr1, arr2, 5));
	printf("模拟函数实现结果:\n%s\n", myStrncat(arr1, arr2, 5));
	return 0;
} 的添加

🌃strstr 寻找

  同样的,strncmp 也能控制比较长度,如果出现则返回第一次其在目标字符串中第一次出现的地址,如果没有出现,则返回一个空指针。

ststr 标准格式
只要传入的字符串地址就行了

使用注意事项:

  • 这个函数没有什么需要特别注意的事项
  • 需要用到多个指针,不断记录位置、移动位置、刷新位置,

模拟实现 strncmp

  这个模拟实现也比较简单,大体思路与 strcmp 的模拟一样,只是循环判断条件变为了 k 和 *dest (当对比到结束标志处,循环也会停止)。

//strncmp n个字符串比较
int myStrncmp(const char* dest, const char* src, size_t k)
{
	assert(dest && src);//断言
	//为何使用前置--?
	//因为这样能有效避免多判断一次的情况
	while (--k && *dest == *src)
	{
		dest++;//确保每位都能对比到
		src++;
	}
	//分情况返回
	if (*dest - *src > 0)
		return 1;
	else if (*dest - *src < 0)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BATZ";
	char* str2 = "BAT";
	//printf("库函数实现结果:\n%d\n", strncmp(str1, str2, 3));
	printf("模拟函数实现结果:\n%d\n", myStrncmp(str1, str2, 3));
	return 0;
}

🌃//strstr 字符串寻找 char* myStrstr(const char* dest, const char* src) { assert(dest && src);//断言 //特殊情况之一,如果源字符串为空,则返回目标字符串 if (!*src) return (char*)dest;//强制类型转换 const char* str1 = dest;//使用替身指针 const char* str2 = src;//避免影响源字符串和目标字符串 char* p = (char*)dest;//记录字符串、判断字符串指针 while (*p) { char* s1 = (char*)str1;//比较部分的指针 char* s2 = (char*)str2; //这是主要判断部分,当两个字符相等,并且不为strtok 分割 //才有继续往后走的资格 while (*s1 && *s2 && *s2 == *s1) { s1++;//往后移动,逐字比较 s2++; } //如果是因为子串的如果字符串中有多个分隔符,在第一次分割时传入的是首字符地址,第二次及后续分割需要传递一个空指针,因为 strtok 有记忆功能,当第一次分割结束后,它会记录下此地址,为下次分割做准备,因此需要传递有一个空指针。结束的,就找到了目标字符串 if (*s2 == '
strtok 标准格式
') return p;//直接返回记录指针即可 str1++;//此时没找到,目标字符串指针需要向后移动 p = (char*)str1;//记录指针矫正 } return NULL;//如果走到这一步,说明没找到,返回空指针 } int main() { char* str1 = "abcdef"; char* str2 = "cdef"; //printf("库函数实现结果:\n%s\n", strstr(str1, str2)); printf("模拟函数实现结果:\n%s\n", myStrstr(str1, str2)); return 0; }

  可控追加,旨在控制源字符串中字符追加数,比如目标字符数组为abcd,源字符串为1234,我们传递字节数为2,当追加结束后,目标字符数组变为abcd12,目标字符数组中要包含结束标志因此利用 strncat 自己给自己追加,能够很好的完成任务,避免结束标志吞噬问题。

第一次传递的是字符串首地址

使用注意事项:

  • 如果针对同一个字符串,第二次需要传递一个空指针 
  • 在使用此函数前,一般会创建一个临时变量存储目标数组值,避免分割对目标字符串造成影响
  • //strtok 字符串分割 int main() { char str1[] = "12345678@gmail.com"; char* str2 = "@."; char buf[100] = { 0 }; strcpy(buf, str1); char* p; for (p = strtok(buf, str2); p; ) { printf("%s\n", p); p = strtok(NULL, str2); } return 0; }
  • strerror 报错

模拟实现 strncat

  代码大体与模拟 strcat 的一致,只不过有两个地方需要注意:C语言中有大约141个错误码。如果直接将错误码放入 strerror 中并打印,会出现相应的错误信息;

当程序运行出错后,errno 会获取当前的错误码,

🌉特殊字符串函数 🌃
strerror 标准格式

  字符串寻找函数,作用是在目标字符串中寻找是否出现过源字符串,

errno 标准格式

strerror 中的参数必须是整型,字符型会按ASCII码处理

使用注意事项:

  • error 在使用时需要包含头文件 errno.h
  • ctype.h

模拟实现 strstr

  这个函数实现起来就比较复杂了,isdigit 十进制判断当然我们这里模拟实现的是效率比较低的算法,如果想要追求时间,可以参考参考KMP算法, 提高寻找效率。

isxdigit 十六进制判断

🌃isupper 大写判断

  字符串分割函数,顾名思义就是对字符串进行分割 *** 作,比如字符串abcd&1234,我们把&视为分隔符,再把字符串首地址和分隔符传给 strtok 函数,就能分别得到字符串 abcd 和字符串 1234 的首地址。字符串分割函数有个值得注意的点:islower 小写判断当然如果想要分割其他字符串,只需传递其他字符串的首地址就行了,此时记忆块会刷新。

toupper 转为大写

使用注意事项:

  • tolower 转为小写
  • 函数
  • 当条件满足时(即所传递参数符合条件时)返回真
  • iscntrl

任何控制字符
🌃isspace 

  这个特殊字符串函数需要配合错误码使用;错误码:是指包含各种错误信息的数字代码,比如数字0表示没有错误,经过博主测试,空白字符,比如空格、换页、换行、回车、制表符等当然,C语言中有一个专门的函数记录错误码,即 errno,需要引出头文件 errno.h,isdigit这样一来,strerror(errno) 搭配就能很好的打印出错误信息。 

十进制数字 0~9
isxdigit

使用注意事项:

  • 十六进制数字 0~f (或0~F都行)
  • islower
//strerror 字符串报错
#include
int main()
{
	//打开不存在的文件,使程序报错
	FILE* pf = fopen("test.txt", "r");
	if (!pf)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
🌆字符分类函数

  这部分的字符串函数相对简单,无非就是通过ASCII码判断,不过前人已经打包成函数了,我们只需要调用小写字母 a~z这个头文件就能直接使用。

🌃isupper

  如果成立,返回1,否则返回0。

//isdigit 判断十进制
#include
int main()
{
	if (isdigit('8'))
		printf("字符8是十进制数字");
	else
		printf("字符8不是十进制数字");
	return 0;
}
🌃大写字母 A~Z

  如果成立,返回1,否则返回0。

//isxdigit 判断十六进制
#include
int main()
{
	if (isxdigit('f'))
		printf("字符f是十六进制数字");
	else
		printf("字符f不是十六进制数字");
	return 0;
}
🌃isalpha

  如果成立,返回1,否则返回0。

//isupper 判断大写
#include
int main()
{
	if (isupper('f'))
		printf("字符f大写字母");
	else
		printf("字符f不是大写字母");
	return 0;
}
🌃任意字母,即 a~z 或 A~Z

  如果成立,返回1,否则返回0。

//islower 判断小写
#include
int main()
{
	if (islower('f'))
		printf("字符f是小写字母");
	else
		printf("字符f不是小写字母");
	return 0;
}
🌃isalnum

  返回类型为整型,对应ASCII码值

//toupper 小写转大写
#include
int main()
{
	char c = 'a';
	printf("%c\n", toupper(c));
	return 0;
}
🌃字母或数字,即 a~z 、A~Z 或 0~9

  返回类型为整型,对应ASCII码值

//tolower 大写转小写
#include
int main()
{
	char c = 'Z';
	printf("%c\n", tolower(c));
	return 0;
}

  因为这几个函数都比较简单,所以就没有详细讲解,其实不用这些函数,我们自己也能写出解法,不过会麻烦一些,比如开头提到的那题,下面给大家做了一个汇总表格,让大家看看有哪些现成可用的库函数:

memcpy 拷贝全能版的 strcpy
memcpy 标准格式
目标空间必须足够大目标空间必须可修改传入的字节数需要慎重考虑,这里推荐 sizeof(类型)*想传入的元素数需要把字节控制数 num 作为判断依据,需要强制类型转化为 char* 型再解引用赋值,确保所有数据都能进行拷贝,同样在移动时也需要进行强制类型转化,当我们自己给自己拷贝,并且拷贝空间与目标空间重叠时,自己设计的函数会出问题,会有值被覆盖掉,因为它默认从前往后拷贝(会产生覆盖现象)。库函数中的 memcpy 是个满分拷贝。//memcpy void* myMemcpy(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //判断依据同样是传入的控制字节数 while (num--) { *(char*)dest = *(char*)src;//需要进行强制类型转化 ((char*)dest)++;//转化后移动,确保步长为1字节 ((char*)src)++; } return tmp;//返回目标空间地址 } int main() { int arr1[20] = { 0 }; int arr2[] = { 1,2,3,4,5 }; //memcpy(arr1, arr2,sizeof(int)*5); myMemcpy(arr1,arr2,sizeof(int)*5);//传入时推荐这种写法 int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[20] = "xxxxxxxxx"; char arr4[] = "Hello World"; //printf("%s\n", (char*)memcpy(arr3, arr4, sizeof(char) * 5));//经测试,不会自己添加memmove 移动 //printf("%s\n", (char*)myMemcpy(arr4+6, arr4, sizeof(char) * 5)); char arr5[] = "abcdefg123456"; //printf("%s\n", (char*)memcpy(arr5 + 3, arr5, sizeof(char) * 5));//测试自己拷贝自己时 //printf("%s\n", (char*)myMemcpy(arr5 + 3, arr5, sizeof(char) * 5));//出现重叠的情况 return 0; }移动可以看作拷贝,memcpy 是男人,memmove 是帅气的男人。
memmove 标准格式
目标空间必须足够大目标空间必须可修改传入字节数要慎重考虑
ispunct标点符号,即不属于数字或字母的圆形字符
isgraph任何图形字符
isprint任何可打印的字符,包括图形字符和空白字符
toupper、tolower除ASCII码为0外的任何字符
🌆内存函数

  内存 *** 作函数比较高端,它们更像是不可控字符串函数的Pro版,因为内存函数的 *** 作对象是所有类型,而字符串函数只是面向字符串设计的,话不多说,让我们一起看看内存函数。

🌃memcpy 实现不重叠的拷贝,memmove 实现的是有重叠的拷贝,

  相当于只需要在拷贝前判断是需要从前往后还是从后往前进行拷贝就好了,

//memmove void* myMemmove(void* dest, const void* src, size_t num) { assert(dest && src);//断言 void* tmp = dest;//记录 //情况1,目标地址在源地址之前 //此时执行的就是memcpy的 *** 作 //从前往后拷贝就行了 if (dest < src) { while (num--) { //从前往后赋值 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } } //情况2,需要从后往前拷贝 //此时可以把num当作偏移量 else { while (num--) { //从后往前赋值 *((char*)dest + num) = *((char*)src + num); } } return tmp; } int main() { int arr1[20] = { 0 }; int arr2[10] = { 6,7,8,9,10 }; memmove(arr1, arr2, sizeof(int) * 5); //myMemmove(arr1, arr2, sizeof(int) * 5); int i = 0; for (i = 0; i < 5; i++) { printf("%d ", arr1[i]); } char arr3[] = "abcdefg123456"; //printf("%s\n", (char*)memmove(arr3 + 3, arr5, sizeof(char) * 5));//模拟自己拷贝自己且重叠 //printf("%s\n", (char*)myMemmove(arr3 + 3, arr5, sizeof(char) * 5));//同上,测试模拟函数 return 0; }

使用注意事项:

  • 注:为了简化讲解,使用的是另一个示例
  • memcmp 比较
  • 我们在接收参数时会使用空指针进行接收,比较时会转化为字符型指针进行解引用比较,
    memcmp 标准格式

模拟实现 memcpy

 跟当时我们模拟实现 strncpy 一样,

memcmp 返回值及其意义
不过因为这是全能型的拷贝函数,传递参数时,要传地址(指针)与 strncpy 不同的是它不会自动补\0,这样就对字符串不太友好了,所以如果是专门处理字符串,还是使用字符串函数比较合适。

 值得一提的是,返回参数类型为整型但是微软在设计 memcpy 时留了一手,就是库函数 memcpy 也能完成这个任务,原因很简单,他们在设计时考虑到了这个问题,所以在函数执行前加了个判断,以决定是从前往后拷贝,还是从后往前拷贝。其实这个任务应该交给 memmove 完成的,但是 memcpy 的设计者实现了类 memmove 的 *** 作,所以说传入字节数要慎重考虑 至于如何实现这个功能,下面会介绍到。

1.判断条件中要包含 num 2.判断时要逐字节进行判断,即强制类型转换为 char* 后解引用。
🌃memset 设置

  内存移动函数,memset 可用于某些数据的初始化,当然内存设置这个函数也适用于所有类型的数据,memmove 包含 memcpy,能实现更多 *** 作,可以这样比喻,

memset 标准格式
微软在VS中重写了 memcpy,使得 memcpy 也能实现 memmove 的功能,使其变成了一个满分拷贝。

参数1要为指针,如果不是指针类型,就传入地址

使用注意事项:

  • 参数2为整型,代表在内存中设置后的具体值
  • 参数3需要慎重考虑,不能超过原数据的大小 
  • 知其然,并知其所以然,

模拟实现 memmove

   memmove 是在 memcpy 的基础进行改进的,两者在设计之初就分工明确:VS中的 memcpy 做了升级,也可以实现有重叠的拷贝。其实要实现这一点也不难,判断依据为目标字符地址与源字符地址的大小。

 
 

  图解从前往后拷贝与从后往前拷贝的区别:

🌃

  内存比较函数,有点像 strncmp 的升级版,为了适用于所有数据,确保每位都能对比到,返回值和 strncmp 一样。

使用注意事项:

模拟实现 memcmp

  模拟实现的话也比较简单,抓住两个点就行了,

//memcmp
int myMemcmp(const void* dest, const void* src, size_t num)
{
	assert(dest && src);
	if (!*(char*)src && *(char*)dest)
		return 1;//当源空间为空,且目标空间不为空时
	if (!*(char*)src && !*(char*)dest)
		return 0;//当源空间与目标空间都为空时,避免无效循环
	//类似于 strncmp 的判断条件
	while (--num && *(char*)dest == *(char*)src)
	{
		((char*)dest)++;//逐字符比较
		((char*)src)++;
	}
	//分流判断返回
	if (*(char*)dest > *(char*)src)
		return 1;
	else if (*(char*)dest < *(char*)src)
		return -1;
	else
		return 0;
}
int main()
{
	char* str1 = "BBAZ";
	char* str2 = "BBA";
	//printf("%d\n", memcmp(str1, str2, sizeof(char) * 2));
	printf("%d\n", myMemcmp(str1, str2, sizeof(char) * 2));
	return 0;
}

🌃 

  内存设置函数,顾名思义就是对数据在内存中的存储值做修改,因为这个函数实现起来也比较简单,无非就是逐字节进行修改,类似于 memcpy 吧,不过源字符串为传入的固定值,因此这个函数我们就进行模拟实现了。

使用注意事项:


🌇总结

  本次字符串函数&&内存函数的学习就到此为止了,回顾全文,我们从字符串类型 *** 作开始,延伸到全数据类型的 *** 作,中间还介绍了几个字符分类函数,这些库函数就像一件件实用的工具,是前人智慧的结晶,所以我们不仅要会用,还要用好,并将它们运用到代码中。对这些库函数的模拟实现能让我们更为全面的了解实现原理,并避免犯错。 🎊🎊🎊

 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正。

相关文章推荐

C语言进阶——数据在内存中的存储_Yohifo的博客-CSDN博客 

C语言初阶——实用调试技巧_Yohifo的博客-CSDN博客

【算法】KMP算法——解决字符串匹配问题_榶曲的博客-CSDN博客

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/2991513.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-23
下一篇 2022-09-23

发表评论

登录后才能评论

评论列表(0条)

保存