【C语言】字符函数和字符串函数

写在前面

在头文件 <string.h>中,有着大量的函数方便程序猿🐵来处理字符串,本篇笔记介绍了大多常用的库函数。

点击每个小节的标题都可以跳转到官方文旦对应的链接

为了方便理解,每个库函数不才都自我模拟实现了一遍,方便各位看官理解~~😆😆


目录

写在前面

一、strlen

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

2.1、strcpy

2.2、strcat

2.3、strcmp

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

3.1strncpy

3.2strncat

3.3strncmp

四、字符串查找函数

[4.1 strstr](#4.1 strstr)

[4.2 strtok](#4.2 strtok)

五、错误信息报告

[5.1 strerror](#5.1 strerror)

[5.2 perror](#5.2 perror)

六、内存操作函数

6.1memcpy

6.2memmove

6.3memcmp

七、字符分类函数


一、strlen

计算字符串长度

函数的定义:size_t strlen ( const char * str );

  • 字符串已经'\0'作为结束标志,strlen函数返回的是在字符串中'\0'前面出现的字符个数(不包含'\0')。
  • 参数指向的字符串必须要以'\0'结束。
  • 注意函数的返回值为size_t,是无符号的(易错)
  • size_t 类型是: typedef unsigned __int64 size_t; 是类型重定义后的 unsigned int

**模拟实现:**my_strlen有三种方法:指针-指针法,循环法,递归法(不创建变量计数器)。这里不才展示循环法。

cpp 复制代码
size_t my_strlen(const char* arr) {
	char* dst = arr;
	int num = 0;
	while(*dst++) {
		num++;
	}

	return num;
}

int main() {
	char arr[] = "12344545";
	printf("%d\n", my_strlen(arr));
	return 0;
}
  • strlen函数是计算字符串的长度,字符串中的结束标志是 '\0' ,那咱们只需要循环计算出数组起始地址到 '\0' 中有多少个字符即可模拟实现出strlen。
  • 循环条件:*dst++中的 *dst。的解读 ,根据优先级,**dst是先与解引用操作符*结合后,dst再++。**我们知道循环判断是为真进入循环,为假结束循环。我们字符串结束表示为 '\0' ,即我们读到数组结尾标识符时候,我们的 *dst 的值为:0。在C语言中,0为假,所以直接使用*dst作为判断语句的表达式即可。
  • 循环条件:*dst++中的后置++。的解读 ,后置++,是先使用值再++,根据优先级,dst是先与解引用操作符*结合后,dst再++。 当我们*dst当前的值不为空,那咱们dst++后进入循环,循环结束后就可以判断原先*dst的后一位的值,若此时*dst为 '\0' 那么结束循环,在结束循环后,我们num计算的值就是我们数组长度,就算pts加加了也对num的计算没有影响

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

2.1、strcpy

字符串拷贝

将源指向的C字符串复制到目标指向的数组中,包括 终止空字符即' \0 '(并在该点停止)。

函数的定义:char* strcpy(char * destination, const char * source );

  • 源字符串必须以 '\0' 结束。
  • 会将源字符串中的 '\0' 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。

**模拟实现:**my_strcpy

cpp 复制代码
char* my_strcpy(char* dst, const char* src) {
	assert(dst && src);
	int i = 0;
	while (dst[i] = src[i]) {
		i++;
	}
	return dst;
}

int main() {
	char arr1[20] = "12344545";
	char arr2[] = "abc";
	my_strcpy(arr1, arr2);
	for(int i = 0; arr1[i] != '\0'; i++)
		printf("%c\n", arr1[i]);
	return 0;
}
  • my_strcpy是字符串拷贝函数,我们使用一个循环函数进行拷贝。
  • 解析循环条件:dst[i] = src[i] 。我们知道循环判断是为真进入循环,为假结束循环。在字符串拷贝中,我们需要把src数组j结束标志 '\0' 也一同拷贝过去。
  • 若此时src[i]的值不为 '\0' ,那把对应的值 赋值给dst[i]后,循环条件判断dst[i]是否为假,此时为真,进入循环语句,i++。
  • 若此时src[i]的值为 '\0' ,那把 '\0' 的值赋值 给 dst[i] 后,循环条件判断dst[i]是否为假,此时为假,结束循环,结束循环后我们也把src包含 '\0' 前的字符也都拷贝到了dst数组中了。

2.2、strcat

字符串追加

将源字符串的副本附加到目标字符串。目标中的终止空字符被源的第一个字符覆盖,并且在目标中 由两者连接形成的新字符串末尾包含一个空字符**。**

函数的定义:char * strcat ( char * destination, const char * source );

  • 源字符串必须以 '\0' 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • 字符串自己给自己追加时,必须保证存储空间足够大。

模拟实现:

cpp 复制代码
char* my_strcat(char* dst, const char* src) {
	assert(dst, src);
	char* end = dst;
	while (*dst) {
		dst++;
	}
	int i = 0;
	while (dst[i] = src[i]) {
		i++;
	}
	return end;
}

int main() {
	char arr1[20] = "12344545";
	char arr2[] = "abc";
	my_strcat(arr1, arr2);
	for(int i = 0; arr1[i] != '\0'; i++)
		printf("%c\n", arr1[i]);
	return 0;
}
  • my_strcat是字符串追加函数,其思想结合了my_strlen与my_strcpy。
  • 首先找到dst的结尾即 '\0' (my_strlen),在dst的 '\0' 处添加src的字符(my_strcpy)

2.3、strcmp

字符串比较

此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续使用以下成对字符,直到字符不同或达到终止空字符。

函数的定义:int strcmp ( const char * str1, const char * str2 );

  • 第一个字符串大于第二个字符串,则返回大于0的数字
  • 第一个字符串等于第二个字符串,则返回0
  • 第一个字符串小于第二个字符串,则返回小于0的数字

模拟实现:

cpp 复制代码
int my_strcmp(const char* str1, const char* str2) {
	assert(str1 && str2);
	int i = 0;
	while (str1[i] == str2[i]) {
		if (str1[i] == '\0') {
			return 0;
		}
		i++;
	}
	return str1[i] - str2[i];
}

int main() {
	char arr1[20] = "abc";
	char arr2[] = "abc";
	int n = my_strcmp(arr1, arr2);
	if (n == 0) {
		printf("数组相等\n");
	}
	else {
		printf("数组不相等\n");

	}
	return 0;
}
  • my_strcmp是字符串比较函数,需要遍历字符串每个元素比较是否相等。
  • 字符在内存中是以ASCII码值进行存储的,在内存中其本质还是整形,所以我们直接判断两个字符串相同下标的ASCII码是否相等就可以判断出两个字符串是否相等了。
  • 在循环语句中在两个字符串相等的情况下,我们判断一下是否已经是字符串结尾,若是结尾则说明字符串相等。
  • 若判断出字符串不相等,我们返回 str1[i] - str2[i] 即可以得出:第一个字符串大于第二个字符串,则返回大于0的数字 第一个字符串小于第二个字符串,则返回小于0的数字 的结果。

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

可以根据使用者的需求来规定函数访问范围

3.1strncpy

字符串拷贝

把源字符串的前num个元素拷贝到目标字符串中。

函数的定义

复制代码
char * strncpy ( char * destination, const char * source, size_t num );
  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
  • 如果源字符串长于num,则不会在目标末尾隐式附加空字符。在这种情况下,目标字符串不应被视为以空结尾的C字符串(这样读取会溢出)。

模拟实现:其本质还是strcpy,只不过多加了只能拷贝num个限定条件。

cpp 复制代码
char* my_strncpy(char* dst, const char* src, int num) {
	assert(dst && src);
	int i = 0;
	for (i = 0; i < num; i++) {
		dst[i] = src[i];
	}
	return dst;
}

int main() {
	char arr1[20] = "12345";
	char arr2[] = "aabcd";
	my_strncpy(arr1, arr2, 2);
	for (int i = 0; arr1[i] != '\0'; i++) {
		printf("%c\n", arr1[i]);
	}
	return 0;
}

3.2strncat

字符串追加

把源字符串的前num个元素追加到目标字符串中

函数的定义

复制代码
char * strncat ( char * destination, const char * source, size_t num );
  • 将源的第一个数字字符附加到目标,再加上一个终止空字符。
  • 如果源代码中字符串的长度小于num,则只复制直到终止空字符的内容。
  • 如果源代码中字符串的长度大于num,则复制到对应的num位置后停止复制,并自动在末尾添加 '\0'。

**模拟实现:**其本质还是strcat,只不过多加了只能追加num个限定条件。

cpp 复制代码
char* my_strncat(char* dst, const char* src, int num) {
	assert(dst, src);
	char* end = dst;
	while (*dst) {
		dst++;
	}
	int i = 0;
	for (i = 0; i < num; i++) {
		dst[i] = src[i];
	}
	dst[i] = '\0';
	return end;
}

int main() {
	char arr1[20] = "12345";
	char arr2[] = "aabcd";
	my_strncat(arr1, arr2, 1);
	for (int i = 0; arr1[i] != '\0'; i++) {
		printf("%c\n", arr1[i]);
	}
	return 0;
}
  • 本质还是strcat,但是在上面的for循环中是没有进行 '\0'的赋值的,需要在结束for循环后进行'\0' 的赋值。
  • 在for循环中 i++后不满足条件才会结束循环,此时的 dst + i 已经是追加结束后的下一个地址了,此时 dst[i] 赋值'\0' ,即把dst增加了正常的结束符标志。

3.3strncmp

字符串比较

将源字符串的最多num个字符与目标字符串的num个字符进行比较。

函数的定义

复制代码
int strncmp ( const char * str1, const char * str2, size_t num );
  • 比较到出现另一个字符不一样或者一个字符串结束或者num个字符全部比较完。

模拟实现:其本质还是strcat,只不过多加了只能比较前num个限定条件。

cpp 复制代码
int my_strncmp(const char* str1, const char* str2,int num) {
	assert(str1 && str2);
	int i = 0;

    //可以把判断语句放进for循环的循环条件中,但是可读性下降
	/*for (i = 0; i < num && str1[i] == str2[i]; i++) { 
		; 
	}*/

	for (i = 0; i < num; i++) {
		if (str1[i] != str2[i]) {
			break;
		}
	}
	if (i == num) {
		return 0;
	}
	else {
		return str1[i] - str2[i];
	}
}


int main() {
	char arr1[20] = "12345";
	char arr2[] = "aabcd";
	int n = my_strncmp(arr1, arr2, 1);
	if (n == 0) {
		printf("数组相等\n");
	}
	else {
		printf("数组不相等\n");

	}
	return 0;
}
  • 本质还是strcmp函数逻辑,区别是循环是否执行了num次。
  • 若执行了num次,说明两个字符串相等。
  • 若指向次数小于num次,说明两个字符串不相等。

四、字符串查找函数

4.1 strstr

字符串查找

查找源字符串在目标字符串中出现的位置。

函数的定义

复制代码
const char * strstr ( const char * str1, const char * str2 );
  • 返回一个指向str1中str2第一次出现的指针,如果str2不是str1的一部分,则返回一个空指针。
  • 匹配过程不包括终止空字符,但查找到str2中的'\0'后结束strstr函数并返回指针。

模拟实现:这篇笔记使用暴力查找(可以使用KMP算法:【算法】KMP算法

cpp 复制代码
char* my_strstr(const char* str1, const char* str2) {
    assert(str1 && str2);
	char* src2 = NULL;
	char* src1 = NULL;
	while (*str1++) {
		if (*str1 == *str2) {
			src1 = str1 + 1;
			src2 = str2 + 1;
			while (*src1++ == *src2++) {
				if (*src2 == '\0') {
					return str1;
				}
			}
		}
	}
	return NULL;
}

int main() {
	char arr1[20] = "12222345";
	char arr2[] = "2223";
	char* n = my_strstr(arr1, arr2, 1);
	if (n != NULL) {
		printf("找到了:>%s\n", n);
	}
	else {
		printf("找不到!\n");
	}
	return 0;
}
  • 在my_strstr函数中我们核心思想其实也是strcmp ,也是两个字符串比较,只不过在strstr中我们需要返回一个指向str1中str2第一次出现的指针。
  • 我们首先使用循环来遍历str1数组,若str1已经是字符串结束标识符,那说明没有找到,结束循环。
  • 在每个不是字符串结束标识符时,我们都与str2的内容进行比较,若str1与str2的首元素内容相同时,我们就使用两个临时指针来进行我们后续元素的比较,定义的两个临时指针,是用于避免str1和str2的移动,导致比较失败时候返回不了原比较地址。
  • 若比较已经走到了src2为 '\0' 时,说明str2已经结束,说明比较成功。
  • 若比较失败,那我们str1继续往后移动,继续比较,直到str1为 '\0' 后返回 NULL。

4.2strtok

字符串分割

通过提供的分割关键字,在字符串中查找到对应的分割关键字后进行字符串的分割

函数的定义

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

五、错误信息报告

5.1strerror

返回错误码,所对应的错误信息。可以获取所以的错误信息。

函数的定义

复制代码
char * strerror ( int errnum );
  • 解释errnum的值,生成一个字符串,其中包含一条描述错误条件的消息,就像库的函数将其设置为errno一样。

5.2 perror

打印错误消息

与5.1的功能类似,都是打印错误信息,perror可以自定义信息。但是perror只支持库函数内的错误提取,程序猿自己的语法错误是无法获取的。

函数的定义

复制代码
void perror ( const char * str );
  • 自动接收错误信息并打印
  • 将errno的值解释为错误消息,并将其打印到stderr(标准错误输出流,通常是控制台),可以选择在它前面加上str中指定的自定义消息。
  • errno是一个整型变量,其值描述了调用库函数时产生的错误条件或诊断信息(C标准库的任何函数都可以为errno设置值,即使在此引用中没有明确指定,即使没有发生错误),有关更多信息,请参阅errno。
  • perror产生的错误消息取决于平台。
    如果参数str不是空指针,则打印str,后跟冒号(:)和空格。然后,无论str是否为空指针,都会打印生成的错误描述,后跟一个换行符("\n")。
  • 应在错误产生后立即调用perror,否则它可能会被调用其他函数覆盖。

六、内存操作函数

6.1memcpy

内存拷贝

此拷贝不考虑连续空间中的内部拷贝问题,strcpy只针对数组中的拷贝,而memcpy支持所有内容的拷贝。

函数的定义:

复制代码
void * memcpy ( void * destination, const void * source, size_t num );
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 '\0' 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

模拟实现:

cpp 复制代码
void* my_memcpy(void* dst, void* src, int num) {
	assert(dst && src);
	void* str1 = dst;
	for (int i = 0; i < num; i++) {
		(*((char*)dst)) =( *((char*)src));
		(char*)dst = ((char*)dst) + 1;
		(char*)src = ((char*)src) + 1;
	}
	return str1;
}

int main() {
	int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
	int arr2[] = { 9,8,7,6,5,4,3,2,1, '\0'};
	my_memcpy(arr1, arr2, sizeof(int) * 5);
	
	for (int i = 0; arr1[i] != '\0'; i++) {
		printf("%d ", arr1[i]);
	}
	return 0;
}
  • void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
  • 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。
  • 理解解释一后,剩下的就是strncpy的思想了。轻松拿捏😎😆

在模拟实现中,我们尝试自己拷贝自己,把arr1中的123456789改变为121234589,即为:memcpy(arr1 + 2, arr, 5);

但是我们得到的结果却为:

上图看到,实际结果为:121212189

我们画图后清晰的看出:

my_memcpy是从前往后排拷贝,在有任何的重叠的情况下,我们往后需要拷贝的结果就不是我们希望的结果。所以在有重叠的情况下我们使用memmove。

ps在Visual Studio中把memcpy改写成了memmove,即使我们使用的是memcpy在底层调用的memmove。但不是所有的编译器都改写了。


6.2memmove

内存拷贝,为了解决memcpy起点终点有重叠后导致未定义的内容,从而推出了memmove,但是memmove的运行效率比memcpy低。

此拷贝考虑连续空间中的内部拷贝问题,strcpy只针对数组中的拷贝,而memmove支持所有内容的拷贝。

函数的定义:

复制代码
void * memmove ( void * destination, const void * source, size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
  • 为避免溢出,目标和源参数指向的数组大小应至少为num字节。

模拟实现:

为了实现重叠的情况下我们可以正常得到我们的结果。我们需要分为dst > src的情况与dst =< src的情况。

在dst > src的情况下,我们使用从前往后拷贝就会出现我们意料之外的结果。

当我们使用从后往前拷贝时,我们就可以得到我们想要的结果。

在dst =< src时,我们使用从前向后拷贝也就没有问题啦。😎

cpp 复制代码
void* my_memmove(void* dst, void* src, int num) {
	assert(dst && src);
	void* str1 = dst;
	if (dst > src) {
		(char*)dst = ((char*)dst) + (num - 1);
		(char*)src = ((char*)src) + (num - 1);
		for (int i = num - 1; i >= 0; i--) {
			(*((char*)dst)) = (*((char*)src));
			(char*)dst = ((char*)dst) - 1;
			(char*)src = ((char*)src) - 1;
		}
	}
	else {
		for (int i = 0; i < num; i++) {
			(*((char*)dst)) = (*((char*)src));
			(char*)dst = ((char*)dst) + 1;
			(char*)src = ((char*)src) + 1;
		}
	}
	return str1;
}


//内存操作main:
int main() {
	int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
	int arr2[] = { 9,8,7,6,5,4,3,2,1, '\0'};
	my_memmove(arr1 , arr1 + 2, sizeof(int) * 5);
	
	for (int i = 0; arr1[i] != '\0'; i++) {
		printf("%d ", arr1[i]);
	}

	printf("\n\n");
	return 0;
}
  • 核心思想与memcpy函数相同,不过只增加了分类dst > src的情况与dst =< src的情况 。
  • void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
  • 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。

6.3memcmp

内存比较

功能与strcmp相似,strcmp只支持字符串比较,mencmp支持所有内容比较(需相同类型)

函数的定义:

复制代码
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
  • 比较从ptr1和ptr2指针开始的num个字节

返回值如下:

模拟实现:

cpp 复制代码
int my_memcmp(const void* ptr1, const void* ptr2, int num) {
	assert(ptr1 && ptr2);
	int i = 0;
	for (i = 0; i < num; i++) {
		if ((*(char*)ptr1) != (*(char*)ptr2)) {
			break;
		}
		(char*)ptr1 = ((char*)ptr1) + 1;
		(char*)ptr2 = ((char*)ptr2) + 1;
	}
	if (i == num) {
		return 0;
	}
	else {
		return (*(char*)ptr1) - (*(char*)ptr2);
	}


}

//内存操作main:
int main() {
	int arr1[20] = { 1,2,3,4,5,6,7,8,9,'\0'};
	int arr2[] = { 1,8,7,6,5,4,3,2,1, '\0'};
	int n = my_memcmp(arr1 , arr2, sizeof(int) * 2);
	
	if (n == 0) {
		printf("数组相等\n");
	}
	else {
		printf("数组不相等\n");
		
	}
	return 0;
}
  • 核心思想与strncmp相同。只需留言void*问题即可。
  • void* 可以接收所有类型的一级指针,利用char*访问一个字节的特性,把void*强制类型转换成 char*,使得可以交换所有我们想交换的字节个数。
  • 在void*中,不能使用自增与自减,只能使用赋值的方式使得指针移动。

七、字符分类函数

上图都为C语言中的字符分类函数用法相对简单,不才这里不一一举栗了。


以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 若有帮助不要吝啬点赞哟~~💖💖

ps:表情包来自网络,侵删🌹

若是看了这篇笔记彻底拿捏<string.h>头文件的就可以在评论区打出:小小<string.h>!拿捏!😎

相关推荐
halo14161 分钟前
uni-app 界面TabBar中间大图标设置的两种方法
开发语言·javascript·uni-app
双手插兜-装高手9 分钟前
Linux - 线程基础
linux·c语言·笔记
froginwe1110 分钟前
Go 语言数组
开发语言
瑞雨溪10 分钟前
python中的OS模块的基本使用
开发语言·python
醒过来摸鱼15 分钟前
【Golang】协程
开发语言·后端·golang
时光追逐者16 分钟前
.NET 9 中 LINQ 新增功能实操
开发语言·开源·c#·.net·.netcore·linq·微软技术
Leuanghing17 分钟前
使用Python生成F分布表并导出为Excel文件
开发语言·python·excel·f分布
IT枫斗者19 分钟前
如何解决Java EasyExcel 导出报内存溢出
java·服务器·开发语言·网络·分布式·物联网
XiaoCCCcCCccCcccC38 分钟前
Linux环境下的基础开发工具 -- 包管理器,vim,gcc/g++,make/makefile,git,gdb/cgdb
linux·c语言·gdb
sun_weitao1 小时前
括号匹配算法
开发语言·python