Bootstrap

【C语言】字符串函数详解


在这里插入图片描述

Ⅰ. strcpy – 字符串拷贝

1、函数介绍

char *strcpy(char *Destination, const char *Source);

strcpy 函数是一个用于拷贝字符串的函数,即将一个字符串中的内容拷贝到另一个字符串中(会覆盖原字符串内容)。它的参数是两个指针,第一个指向的是拷贝字符串的目的地的起始位置,即要将字符串拷贝到什么地方;第二个指向的是要拷贝字符串的内容的起始位置,即需要拷贝的字符串。它的返回值是目标空间的起始位置。

💥注意:

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

​ 举个例子,比如我们要将 arr2 数组中的 "def" 拷贝到 arr1 数组中。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[10] = "abc";
	char arr2[] = "def";
	strcpy(arr1, arr2);
	printf("%s", arr1);
	return 0;
}

// 运行结果
def

注意: 拷贝结束后 arr1 数组中只有 "def",因为 "abc" 被覆盖了。

2、模拟实现

char* my_strcpy(char* dest, const char* src) 
{
    if (dest == NULL || src == NULL)
        return NULL;

    char* p = dest;
    while (*p++ = *src++) // 当遇到\0赋值之后判断为0则退出循环,达到了我们的目的
    {}

    return dest;
}

​ 在这个代码中,我们首先添加了一个错误处理机制,以防止传递给 strcpy 函数的参数是 NULL 指针。如果参数是 NULL 指针,则函数将返回 NULLmain 函数将输出一条错误消息并退出程序。

​ 此外,我们还使用了 const 关键字来指示源字符串 src 是只读的,这可以帮助我们防止无意中修改源字符串。

Ⅱ. strcat – 字符串追加

1、函数介绍

char *strcat(char *Destination, const char *Source);

strcat 函数是一个用于追加字符串的函数,即将一个字符串中的内容追加到另一个字符串后面(不会覆盖原字符串内容)。它的参数是两个指针,第一个指向的是追加字符串的目的地的起始位置,即要将字符串追加到什么地方;第二个指向的是要追加字符串的内容的起始位置,即需要追加的字符串。它的返回值是目标空间的起始位置。

💥注意:

  • 源字符串必须以 \0 结束。
  • 目标空间必须足够大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • 字符串不能给自己追加( \0 被覆盖,无终止条件)。

​ 举个例子,比如我们要将 arr2 数组中的 “liren!” 追加到 arr1 数组的后面。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "hello ";
	char arr2[] = "liren!";
	strcat(arr1, arr2);
	printf("%s", arr1);
	return 0;
}

// 运行结果:
hello liren!

2、模拟实现

char* my_strcat(char* dest, const char* src) 
{
    char* p = dest;
    while (*p != '\0') // 找到目标空间的'\0'
    {
        p++;
    }
    while (*p++ = *src++) // 当遇到\0赋值之后判断为0则退出循环,达到了我们的目的
    {}
    return dest;
}

​ 这个实现中,首先定义了两个指针,一个指向目标字符串的末尾,一个指向源字符串的开头。然后,使用 while 循环将源字符串中的每个字符逐个复制到目标字符串的末尾,直到源字符串末尾为止。最后,在目标字符串的末尾添加一个 NULL 字符,表示字符串的结束。

​ 需要注意的是,在本函数的实现中,目标字符串必须具有足够的空间来容纳源字符串,否则会导致缓冲区溢出和未定义行为。此外,源字符串必须是一个 const char* 类型的指针,以防止在函数内部修改源字符串的内容。

Ⅲ. strcmp – 字符串比较

1、函数介绍

int strcmp(const char *string1, const char *string2);

strcmp 函数是一个用于比较两个字符串内容的函数。它的参数是两个指针,指针分别指向两个待比较字符串的起始位置。它的返回值是一个整型数字。当 string1 大于 string2 的时候返回一个大于 0 的数;当 string1 等于 string2 的时候返回 0;当 string1 小于 string2 的时候返回一个小于 0 的数。

💥注意:

  • 字符串比较的不是字符串长度的大小,而是两个字符串中对应位置字符的 ASCII 值。

​ 举个例子,比如比较字符串 "hello world!" 和字符串 "hello liren!" 的大小。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "hello world!";
	char arr2[20] = "hello csdn!";
	printf("%d\n", strcmp(arr1, arr2));
	printf("%d\n", strcmp(arr2, arr1));
	return 0;
}

// 运行结果:
1
-1

2、模拟实现

int my_strcmp(const char *s1, const char *s2) 
{
    while (*s1 == *s2) 
    {
        if(*s1 == '\0')
            return 0;
        s1++;
        s2++;
    }
    return *(const unsigned char*)s1 - *(const unsigned char*)s2;
}

​ 这个实现中,使用 while 循环比较两个字符串中的每个字符。如果两个字符相等,则继续比较下一个字符;如果不相等,则返回它们的 ASCII 码之差。需要注意的是,由于有符号和无符号字符之间的比较会产生未定义行为,因此我们将两个指针都转换为 const unsigned char * 类型。

​ 如果两个字符串完全相等,则返回值为 0。如果 s1 小于 s2,则返回一个负整数,反之则返回一个正整数。

​ 需要注意的是,在使用 strcmp 函数比较字符串时,一定要确保两个字符串都以 NULL 字符结尾。如果没有以 NULL 字符结尾,会导致函数在比较过程中访问到未知的内存区域,从而导致未定义行为。

Ⅳ. strncpy、strncat、strncmp – 可限制操作长度

​ 我们发现 strcpy 是将一个字符串全部拷贝到另一个字符串,strcat 是将一个字符串全部追加到另一个字符串后面,strcmp 也是比较两个字符串的全部内容,这类操作函数称为长度不受限制的字符串操作函数。

​ 那么我们如果操作字符串时并不想操作整个字符串,而只想操作字符串的一部分怎么办呢❓❓❓

​ 库函数中的 strncpy、strncat、strncmp 便解决了这个问题。

char *strncpy(char *Dest, const char *Source, size_t count);
char *strncat(char *Dest, const char *Source, size_t count);
int strncmp(const char *string1, const char *string2, size_t count);

​ 这里就不细讲用法了,比较简单,自行尝试!

Ⅴ. strlen – 求字符串长度

1、函数介绍

size_t strlen( const char *string );

strlen 函数是一个用于求字符串长度的库函数。它的参数是被求长度的字符串的起始地址,返回值是一个无符号整型。

💥注意:

  • 参数指向的字符串要以 \0 结束。
  • strlen 返回的是在字符串中 \0 之前出现的字符个数(不包含 \0 )。
  • 注意函数的返回值为 size_t,是无符号的(易错)。

​ 举个例子,比如我们要求字符串 "abcdef" 的长度。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abcdef";
	size_t ret = strlen(arr);
	printf("%d\n", ret);
	return 0;
}

// 运行结果:
6

2、模拟实现(三种方式)

① 计数器的方式

​ 我们定义一个变量为 count,如果传入的指针指向的内容不是 \0,那么 count++,同时指针后移一位,循环往复,直到找到 \0 时返回 count 即可。

size_t my_strlen(const char* str)
{
	size_t count = 0; // 计数器
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}

② 递归的方式

​ 我们一进入函数体就判断传入指针指向的内容是否为 \0,如果是就返回 0,不是就返回 1 + my_strlen(str+1),如此进行下去,直到递归到内层时找到 \0,这时再一步步将值返回回来即可。

size_t my_strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}

③ 指针-指针的方式

​ 进入函数体时,我们事先定义一个指针变量将传入的指针保存下来,然后将传入的指针向后移,直到遇到 \0 时,我们返回当前指针与保存的指针的差值即可。(指针与指针的差的绝对值是两个指针之间的元素个数)

size_t my_strlen(const char* str)
{
	const char* p = str; // 保存起始位置
	while (*str != '\0')
		str++;
	return str - p;
}

Ⅵ. strstr – 字符串查找

1、函数介绍

char *strstr(const char *string, const char *strCharSet);

strstr 函数可以在一个字符串(字符串1)中查找另一个字符串(字符串2),如果字符串2存在于该字符串1中,那么就返回被字符串2在字符串1中第一次出现的起始位置,如果在字符串1中找不到字符串2,那么就返回空指针(NULL)。它的第一个参数是字符串1的起始位置,第二个参数是字符串2的起始位置。

💥注意:

  • 若字符串2为空字符串,则返回字符串1的起始位置。

​ 举个例子,比如我们在字符串 “abcdefbcd” 中查找字符串 “bcd”。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abcdefbcd";
	char arr2[] = "bcd";
	char* ret = strstr(arr1, arr2); // 在arr1中查找arr2字符串第一次出现的位置
	if (ret != NULL)
		printf("%s\n", ret);
	else
		printf("找不到\n");
	return 0;
}

// 运行结果:
bcdefbcd

​ 注意:strstr 函数的返回值是字符串 "bcd" 在字符串 "abcdefbcd"第一次出现的位置的起始位置,而不是出现几次就返回几个起始位置。

2、模拟实现

strstr 函数的模拟实现相对复杂,在实现过程中我们需要设置3个指针变量来辅助实现函数功能。

  • cp指针: 记录每次开始匹配时的起始位置,当从该位置开始匹配时就找到了目标字符串,便于返回目标字符串出现的起始位置;当从该位置开始没有匹配成功时,则从cp++处开始下一次的匹配。
  • p1p2指针: 通过判断p1和p2指针解引用后是否相等来判断每个字符是否匹配成功,若成功,则指针后移比较下一对字符;若失败,p1指针返回cp指针处,p2指针返回待查找字符串的起始位置。

​ 例如,在字符串"abbbcdef"中查找字符串"bbc":
​ 刚刚开始时3个指针的指向如图所示:

在这里插入图片描述

​ 若p1与p2匹配不成功,则cp指针后移,接着将cp指针赋值给p1指针:

在这里插入图片描述

​ 此时,p1与p2匹配成功,那么cp指针不动,p1和p2指针后移继续比较:

在这里插入图片描述

​ 当p1与p2匹配不成功时,cp指针后移一位,p1返回cp位置,p2返回待查找字符串起始位置:

在这里插入图片描述

​ 从此位置开始下一轮的比较:

在这里插入图片描述

​ 直到当p2指向的内容为\0时,便说明待查找字符串中的字符已经被找完,也说明了从当前cp位置开始匹配能够找到目标字符串,所以此时返回指针cp即可。

char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 != NULL); // 断言,当str1为空指针报错
	assert(str2 != NULL); // 断言,当str2为空指针报错
	const char* cp = str1; // 记录开始匹配时的起始位置
	if (*str2 == '\0') // 要查找的字符串为空字符串
		return (char*)str1;
	while (*cp)
	{
		const char* p1 = cp;
		const char* p2 = str2;
		while ((*p1!='\0') && (*p2!='\0') && (*p1 == *p2))
		{
			p1++;
			p2++;
		}
		if (*p2 == '\0') // 目标字符串已被查找完
			return (char*)cp;
		cp++;
	}
	return NULL; // 找不到目标字符串
}

​ 但是这种算法的时间复杂度比较高,优化方法就是 KMP 算法,具体可以看 KMP 算法的笔记!

Ⅶ. strtok – 字符串分割

char *strtok(char *strToken, const char *strDelimit);

strtok 函数能通过给定的一系列字符将一个字符串分割成许多子字符串的函数。它的第一个参数是需要被分割的字符串的首地址;第二个参数是一个字符串的首地址,该字符串是用作分隔符的字符集合。返回值是查找到的标记的首地址。

💥注意:

  • 找到 strToken 中的一个标记时,会将其用 \0 结尾并返回这个标记的首地址。
  • strtok 函数是会改变 strToken 的,所以在使用 strtok 函数切分的字符串都是临时拷贝的内容并且可修改。
  • 第一个参数不为 NULL 时,函数将找到 strToken 中的第一个标记,并保存它在字符串中的位置。
  • 第一个参数为 NULL 时,函数将从同一个字符串中被保存的位置开始查找它的下一个标记。
  • 若字符串中不存在更多的标记,则返回 NULL 指针。

​ 举个例子,比如我们要将字符串"[email protected]"以"@“字符和”."字符分割开。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "[email protected]"; // 待分割字符串
	char arr2[] = "@."; // 分隔符的字符集合
	char arr3[20] = { 0 };
	strcpy(arr3, arr1); // 将数据拷贝一份使用,防止原数据被修改
	char* token = strtok(arr3, arr2); // 第一次传参需传入待分割字符串首地址
	while (token != NULL) // 说明还未分割完
	{
		printf("%s\n", token);
		token = strtok(NULL, arr2); // 对同一个字符串进行分割,第二次及以后的第一个参数为NULL
	}
	return 0;
}

// 运行结果:
2916776007
qq
com

注意:strtok 函数找到第一个标记时,将其后的 ’@‘ 字符改为 ’\0’ 并返回第一个标记的首地址,所以我们以返回的地址为首地址开始打印字符串的时候就只会打印出 2916776007,第二次再对该字符串调用 strtok 函数时将从 ’@’ 字符后面开始寻找下一个标记。

Ⅷ. strerror、perror – 错误报告函数

char *strerror(int errnum);

strerror 函数可以把错误码转换为对应的错误信息,返回错误信息对应字符串的起始地址。

void perror(const char *string);

perror 函数可以打印一个错误信息,无返回值。

​ 我们需要知道,库函数在使用的时候如果发生错误,都会有对应的错误码,而这些错误码都会被存放在 errno 这个全局变量中,如果要使用这个全局变量,我们需要引其对应的头文件:#include<errno.h>

​ 举个例子:(注:fopen函数的功能是打开一个文件,当其执行成功时会返回打开文件的首地址,执行失败时会返回一个空指针)

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");//打开test.txt文件阅读
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		perror("fopen");
	}
	return 0;
}

// 运行结果:
No such file or directory
fopen: No such file or directory

当我们要打开一个不存在的文件(test.txt)来阅读的时候,显然fopen函数会执行失败,于是pf指针接收的便是空指针(NULL)。

  • strerror: 只负责将错误码转换为对应的错误信息,不打印。
  • perror: 直接打印错误信息,并且我们可以自己加上注释来明确错误来源于哪个库函数。

在这里插入图片描述

;