Bootstrap

【C语言】字符+字符串函数精讲

前言

● 从我们第一个C程序——Hello world 的诞生,到字符串的拷贝、比较等各种操作的实现。从中不难发现:我们在处理C语言时对字符字符串的处理很是频繁,因此学习字符及字符串的各种操作函数尤显其必要性。

● C语言本身是没有字符串类型的,字符串通常放在 常量字符串 中或者 字符数组 中。字符串常量适用于那些对它不做修改的字符串函数。

● 补充:本章所讲解的函数均为C语言的库函数,如果想了解更多关于C语言库函数的内容,这里给大家推荐一个官网cplusplus.com大家可以参照官网里的文档学习库函数。

废话少说,上干货。Let’s go!

文章目录

一、求字符串长度-strlen

🍑1.函数声明

size_t strlen ( const char * str );

注释:

1.字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。最终返回一个无符号的整形,表示字符串的长度。
2.由于strlen 只是计算字符串的长度,不能对原字符串进行更改,所以参数部分采用const修饰。
2.参数指向的字符串必须要以 ‘\0’ 结束,否则返回随机值。
3.注意函数的返回值为size_t 即 unsigned int是无符号的( 易错 )

🍑2.strlen函数使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)
		printf("abc>abcdef\n");
	else
		printf("abc<abcdef\n");

	return 0;
}

注释:

我们已知strlen返回的是字符串中‘\0’之前出现的字符个数,即strlen(“abc”)=3,strlen(“abcdef”)=6,但是strlen的返回值为size_t类型,即无符号整数(这里是一个坑),因此无符号整数3-6相当于3的补码加上-6的补码,结果为无符号整数,即一个非常大的数字。因此条件满足,输出abc>abcdef。

🍑3.strlen函数的模拟实现

📝思路一:计数器+遍历查找

#include<assert.h>
size_t mystrlen(const char* str)
{
	assert(str);
	int count = 0;
	while (*str != 0)//找到字符‘\0’的地址
	{
		str++;
		count++;//找到‘\0’之前计数器每次加1
	}
	//遍历结束,返回字符串长度
	return count;
}

📝思路二:递归

#include<assert.h>
size_t mystrlen(const char* str)
{
	assert(str);
	if (*str != 0)//递归条件
		return 1 + mystrlen(str + 1);
	else
		return 0;
}

📝思路三:指针-指针

#include<assert.h>
size_t mystrlen(const char* str)
{
	assert(str);
	const char* start = str;//首字符地址
	const char* end = str;//尾字符地址
	while (*end != 0)//找到字符‘\0’的地址
	{
		end++;
	}
	//即‘\0’的地址-首字符地址,指针相减返回之间元素个数
	return end - start;
}

二、长度不受限制的字符串函数

顾名思义,这一类函数对字符串进行操作的时候总是以’\0’为基准进行的,而不是以字符串的长度为基准。而长度受限制的字符串函数受长度n的限制。

🍑1.字符串拷贝函数-strcpy

🌳(1)函数声明

char* strcpy(char * destination, const char * source );

注释:

1.会将源字符串中的内容拷贝到目标空间中,包括’\0’。最终返回目标字符串的地址。
2.源字符串必须以 ‘\0’ 结束。
3.目标空间必须足够大,以确保能存放源字符串。
4.目标空间必须可变,即不能为常量字符串。

🌳(2)strcpy函数使用

//使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "abc";
	const char arr2[] = "hello world!";
	strcpy(arr1, arr2);//将arr2的内容拷贝到arr1中
	printf("arr1=%s\n", arr1);
	printf("arr2=%s\n", arr2);
	return 0;
}

🌳(3)strcpy函数的模拟实现

#include<assert.h>
char* my_strcpy(char*dest,const char*src)
{
	assert(dest && src);
	char* ret = dest;//记录目标空间的起始位置
	while (*dest++ = *src++)//拷贝,当*src=0时停止
	{
		;
	}
	//拷贝完毕,返回目标空间起始位置
	return ret;
}

🍑2.字符串拼接函数-strcat

🌳(1)函数声明

char * strcat ( char * destination, const char * source );

注释:

1.将源字符串的副本附加到目标字符串中。目标字符串中的‘\0’字符将被源字符串的第一个字符覆盖,并且在连接形成的新字符串的末尾包含一个‘\0’字符。最终返回目标字符串的地址。(可以将strcat理解为字符串追加函数,将源字符串追加到目标字符串末尾)
2.源字符串必须以 ‘\0’ 结束。
3.目标空间必须有足够的大,能容纳下源字符串的内容。
4.目标空间必须可修改。

🌳(2)strcat函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "hello ";
	const char arr2[] = "world!";
	printf("追加前:%s\n", arr1);
	printf("追加前:%s\n", arr2);
	strcat(arr1, arr2);
	
	printf("追加后: % s\n",arr1 );
	return 0;
}

🌳(3)strcat函数模拟实现

char* my_strcat(char* dest, const char* src)
{
 assert(dest&&src);
	char* ret = dest;//记录目标空间首元素地址
	while (* dest!='\0')//找到目标空间dest中‘\0’的地址
	{
		dest++;
	}
	//从目标字符串的‘\0’地址处开始拷贝字符串,包括结尾的'\0'
	//此过程类似于strcpy函数
	while (*dest++ = *src++)
	{
		;
	}
	return ret;//返回目标空间首元素的地址
}

🌳(4)思考:strcat能否实现字符串给自己追加?

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "hello";
	strcat(arr1,arr1);
	printf("%s\n", arr1);
	return 0;
}

调试结果:

结论:

strcat在追加时,将末尾的‘\0’字符覆盖,因此在此后追加过程中永远找不到’\0’,即不会正常停止,最终报错:写入冲突。所以strcat函数不能实现自己给自己追加。

🍑3.字符串比较函数-strcmp

🌳(1)函数声明

int strcmp ( const char * str1, const char * str2 );

注释:

1.strcmp函数开始比较两个字符串的第一个字符。如果第一个字符相等,则继续向后比较,直到字符不同或达到终止的‘\0’字符,比较停止。
2.strcmp函数,比较对应位置上的字符大小,而非长度。
3.标准规定:
(1)第一个字符串大于第二个字符串,则返回大于0的数字
(2)第一个字符串等于第二个字符串,则返回0
(3)第一个字符串小于第二个字符串,则返回小于0的数字

🌳(2)strcmp函数使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "abq";
	printf("%d\n",strcmp(arr1, arr2));
	//c>q即arr>arr2。vs下返回 -1

	char arr3[] = "abcd";
	char arr4[] = "abc";
	printf("%d\n",strcmp(arr3, arr4));
	//d>'\0'即arr3>arr4。vs下返回 1

	char arr5[] = "abc";
	char arr6[] = "abc";
	printf("%d\n",strcmp(arr5, arr6));
	//a=a,b=b,c=c,'\0'='\0'即arr5=arr6。vs下返回 0

	return 0;
}

🌳(3)strcmp函数模拟实现

#include<assert.h>
int my_strcmp(const char* s1, const char* s2)
{
	assert(s1 && s2);
	while (*s1 == *s2)//字符串对应字符相同
	{
		if (*s1 == '\0')
		{
			//如果同时为‘\0’,说明两字符串完全相同,返回0
			return 0;
		}
		//否则同时向后偏移,进行下一对字符的比较
		s1++;
		s2++;
		
	}
	//如果不满足对应字符相同则返回二者差值(巧妙地满足标准规定)
	return *s1 - *s2;
}

三、长度受限制的字符串函数介绍

🍑1.strncpy

🌳(1)函数声明

char * strncpy ( char * destination, const char * source, size_t num );

注释:

1.表示把source所指向的字符串中以source地址开始的前num个字节复制到destination所指的数组中,并返回被复制后的destination的地址。
2.如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
3.destination必须具有足够大的空间可以容纳拷贝后的字符串。

🌳(2)strncpy函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "xxxxxxxxx";
	char arr2[] = "123456";
	//当num小于字符串arr2的长度时,拷贝前num个字符到arr1
	strncpy(arr1, arr2, 3);

	char arr3[] = "xxxxxxxxx";
	char arr4[] = "123456";
	//当num大于字符串arr4的长度时,超出的部分自动拷贝0,直到达到num个为止
	strncpy(arr3, arr4, 7);

	char arr5[] = "xxxxxxxxx";
	char arr6[] = "123\0ddd";
	//当arr6中出现\0时,以\0为arr6字符串的结束标志,超出部分会自动补0
	strncpy(arr5, arr6, 4);

	printf("arr1=%s\n",arr1);
	printf("arr3=%s\n",arr3);
	printf("arr5=%s\n",arr5);
	return 0;
}

🍑2.strncat

🌳(1)函数声明

char * strncat ( char * destination, const char * source, size_t num );

注释:

1.把source所指字符串的前num个字符添加到destination所指字符串的结尾处,并覆盖destination所指字符串结尾的’\0’,从而实现字符串的连接。最终返回目标字符串的地址。
2.strncat追加后会自动在最后补上’\0’。
3.如果num大于字符串source的长度,那么仅将source指向的字符串内容追加到destination的尾部。
4.目标字符串必须要有足够的空间容纳追加后的新目标字符串。

🌳(2)strncat函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "xxxxxxxxx";
	char arr2[] = "123456";
	//当num小于arr2字符串长度时,在arr1后追加前num个字符
	strncat(arr1, arr2, 3);
	printf("arr1 = % s\n", arr1);

	char arr3[20] = "xxxxxxxxx";
	char arr4[] = "123456";
	//当num大于arr4字符串长度时,在arr3后追加arr4字符串
	strncat(arr3, arr4, 7);
	printf("arr3 = % s\n", arr3);
	return 0;
}

补充:与strcat不同,由于strncat追加后会自动在最后补上’\0’,所以可以实现自己给自己追加。

#include<stdio.h>
#include<string.h>
int main()
{
	//注意:arr1需要有足够大的空间可以容纳追加后的字符串
	char arr1[50] = "12345";
	strncat(arr1, arr1,5 );
	printf("arr1 = % s\n", arr1);
	
	return 0;
}

🍑3.strncmp

🌳(1)函数声明

int strncmp ( const char * str1, const char * str2, size_t num );

注释:

1.比较直到出现对应位置上的字符不一样,或者一个字符串结束,或者num个字符全部比较完。
2.strncmp函数,同样比较对应位置上的字符大小,而非长度。
3.标准规定:
(1)第一个字符串大于第二个字符串,则返回大于0的数字
(2)第一个字符串等于第二个字符串,则返回0
(3)第一个字符串小于第二个字符串,则返回小于0的数字

🌳(2)strncmp函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "abce";
	char arr2[] = "abcdefg";
	//比较前三个字符,a=a,b=b,c=c,即前三个字符相同,返回0
	printf("%d\n",strncmp(arr1, arr2, 3));

	char arr3[20] = "aca";
	char arr4[] = "abcdefg";
	//比较前2个字符,a=a,a<b,即arr3第2个字符小,vs下返回1
	printf("%d\n", strncmp(arr3, arr4, 2));

	char arr5[20] = "a";
	char arr6[] = "abcdefg";
	//由于num>strlen(arr1)+1,比较前2个字符,a=a,'\0'<b,即arr1的第2个字符小,vs下返回-1
	printf("%d\n", strncmp(arr5, arr6, 3));
	
	return 0;
}

四、字符串查找与分割

🍑1.字符串查找函数-strstr

🌳(1)函数声明

char * strstr ( const char *str1, const char * str2);

注释:

1.判断字符串str2是否是str1的子串。如果是,则该函数返回 str1字符串从 str2第一次出现的位置开始到 str1结尾的字符串;否则,返回NULL
2.strstr函数在查找时,大小写会被认为是不同的字符串。

🌳(2)strstr函数的使用

📝用于判断一个字符串中是否含有目标字符子串

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>

int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "bcd";
	char* p = strstr(arr1,arr2);
	if (p == NULL)
	{
		printf("不存在\n");
	}
	else
	{
		printf("%s\n", p);
	}
	return 0;
}

🌳(3)strstr函数模拟实现

查找流程如图所示:

仿照以上思路,代码实现如下:

#include<assert.h>
char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	const char* s1 = str1;
	const char* s2 = str2;

	const char* p = str1;//用于记录字符串查找开始的位置

	if (*str2 == '\0')//特殊处理
	{
		//如果str2是空串,直接返回str1
		return str1;
	}
	while (*p)//判断str1是否遍历完
	{
		s1 = p;
		s2 = str2;
		while (*s1 != '\0' && *s2 != '\0' && (*s1 == *s2))
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return (char*)p;//找到了,返回在str1中的开始地址
		}
		p++;//上一次循环没找到,查找位置向后偏移
	}
	//str1中不存在str2,找不到子串
	return NULL;
}

🍑2.字符串分割函数-strtok

🌳(1)函数声明

char * strtok ( char * str, const char * sep );

注释:

1.sep参数是一个字符串,定义了用作分隔符的字符集合。
2.第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标 记。
3.strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容 并且可修改。)
4.strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置。
5.strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标 记。
6.如果字符串中不存在更多的标记,则返回 NULL 指针。

🌳(2)strtok函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char buf[50] = { 0 };
	char arr[] = "www.The calf wants to turn [email protected]";
	strcpy(buf, arr);//将数据拷贝一份,不改变源数据
	char* flag = ".@ ";//分隔符集合
	int count = 0;
	//根据strtok函数特点:
	//第一次第一个参数不为NULL,向后查找分隔符,找到改为'\0',返回分隔符前的子字符串,并保存这一次strtok走到的位置。
	//第二次令第一个参数为NULL,strtok会从上一次保存的位置开始向后查找分隔符,找到改为'\0'并返回第二个子字符串的地址。
	//第三次……
	for (char* str = strtok(buf, flag); str != NULL; str = strtok(NULL, flag))
	{
		count++;
		printf("分割%d=%s\n",count,str);
	}
	return 0;
}

五、错误信息报告

🍑1.错误报告函数-strerror

🌳(1)函数声明

char * strerror ( int errnum );

注释:

1.返回错误码,所对应的错误信息。
2.与错误码变量errno搭配使用。(errno 是记录系统的最后一次错误代码)

🌳(2)strerror函数使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
//使用错误码变量需要包含头文件
#include<errno.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//库函数调用失败之后,会把错误码记录到错误码变量中——errno
	if (pf == NULL)
	{
		printf("%s\n",strerror(errno));
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

拓展:与strerror函数类似,使用perror函数也可显示相应错误信息。perror相当于printf+strerror,同时参数可添加提示词。
代码展示

#include<errno.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("错误信息");
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

六、字符操作函数

🍑1.字符操作函数的分类

字符操作函数如果它的参数符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’
isdigit十进制数字 0~9
isxdigit十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower小写字母a~z
isupper大写字母A~Z
isalpha字母a~z或A~Z
isalnum字母或者数字,a~z,A~Z,0~9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符

🍑2.字符操作函数的简单使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<ctype.h>
int main()
{
	char arr[] = "Are you ok?";
	char* p = arr;
	while (*p)
	{
		if (islower(*p))
		{
			*p = toupper(*p);
		}
		p++;
	}
	printf("%s\n", arr);
	return 0;
}

这些字符操作函数针对单个字符进行操作,它们的参数返回值以及功能都非常简单。这里就不再一一介绍了。

七、内存操作函数

🍑1.内存拷贝函数-memcpy

🌳(1)函数声明

void * memcpy ( void * destination, const void * source, size_t num );

注释:

1.函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
2.这个函数在遇到 ‘\0’ 的时候并不会停下来。
3.如果source和destination有任何的重叠,复制的结果都是未定义的。(:memcpy并不支持内存空间有重叠的复制)

🌳(2)memcpy函数的使用

//拷贝arr1中的前8个字节到arr2中
//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	float arr1[] = { 1.0f,2.0f,3.0f,4.0f };
	float arr2[5] = { 0.0f };
	memcpy(arr2, arr1,8);
	return 0;
}

🌳(3)memcpy函数模拟实现

#include<assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	assert(dest && src);
	while (num--)//拷贝字节数
	{
		//每次拷贝1个字节,所以将其转换为(char*)指针
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

🍑2.内存拷贝函数-memmove(可重叠)

🌳(1)函数声明

void * memmove ( void * destination, const void * source, size_t num );

注释:

1.和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
2.如果源空间和目标空间出现重叠,就得使用memmove函数处理。

🌳(2)memmove函数的使用

//从arr中拷贝20个字节到arr+2
//注意:使用库函数需要包含头文件
#include<string.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	memmove(arr+2,arr,20);
	return 0;
}

🌳(3)memmove函数的模拟实现

分析:

📝代码实现

#include<assert.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	assert(dest);
	assert(src);
	if (dest < src)//src前——>后拷贝
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	else//src后——>前
	{
		while(num--)
		{
			*((char*)dest + num) = *((char*)src + num);
		}
	}
	return ret;
}

🍑3.内存比较函数-memcmp

🌳(1)函数声明

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

注释:

1.比较从ptr1和ptr2指针开始的num个字节
2.标准规定:
(1)ptr1对应的字节内容>ptr2对应字节内容,返回大于0的数字
(2)两个内存块内容相同,返回0
(3)ptr1对应的字节内容<ptr2对应字节内容,返回小于0的数字

🌳(2)memcmp函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	int arr1[] = {1,2,3,4,5};
	int arr2[] = {1,2,3,0,0};
	int ret = memcmp(arr1, arr2, 13);
	//分析:前12个字节内容相同,第13个字节内容00000100>00000000
	printf("%d\n", ret);
	return 0;
}

🍑4.内存设置函数-memset

🌳(1)函数声明

void * memset ( void * ptr, int value, size_t num );

注释:

1.将ptr中当前位置后面的num个字节用 value 替换并返回ptr。
2.注意是以字节(byte)为单位。ptr必须具有足够的空间容纳替换后的ptr。

🌳(2)menset函数的使用

//注意:使用库函数需要包含头文件
#include<stdio.h>
#include<string.h>
int main()
{
	char str[] = "xxxxxxxxxx";
	memset(str,'0', 5);//表示将str的前5个字节设置成字符0
	printf("%s\n", str);
	return 0;
}

总结

本章主要对字符串以及字符操作函数进行了一个较为全面的介绍,篇幅较长,但干货满满。建议大家反复观看,慢慢食用😊

;