欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

C]字符和字符串函数

最编程 2024-10-19 13:16:17
...

写在前面

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

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

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


目录

写在前面

一、strlen

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

2.1、strcpy

2.2、strcat

2.3、strcmp

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

3.1strncpy

3.2strncat

3.3strncmp

四、字符串查找函数

4.1 strstr

4.2 strtok

五、错误信息报告

5.1 strerror

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有三种方法:指针-指针法,循环法,递归法(不创建变量计数器)。这里不才展示循环法。

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

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' 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • 字符串自己给自己追加时,必须保证存储空间足够大。

模拟实现:

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的数字

模拟实现:

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个限定条件。

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个限定条件。

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个限定条件。

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算法

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.2 strtok

字符串分割

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

 函数的定义

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

五、错误信息报告

5.1 strerror

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

函数的定义

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有任何的重叠,复制的结果都是未定义的。

模拟实现:

        

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时,我们使用从前向后拷贝也就没有问题啦。????

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个字节

返回值如下:

模拟实现:

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>!拿捏!????