笔记20-1(C语言进阶 字符串函数介绍)

目录

注:

 长度受限制的字符串函数

strlen

工作原理

实例

模拟实现strlen函数

strcpy

 工作原理

解析工作原理

strcat - 字符串追加/连接

工作原理

实例

模拟实现

注意

strcmp - 字符串比较函数

工作原理

实例

模拟实现

优化my_strcmp函数

长度受限制的字符串函数

strncpy

实例

strncat

实例

strncmp

实例

strstr - 寻找字符串的子串

实例

模拟实现

strtok - 切割字符串

实例

strerror

实例

 存在一个与strerror函数相关的函数 - perror函数

实例


注:

 本笔记参考B站up鹏哥C语言的视频


 长度受限制的字符串函数

strlen

工作原理

  1. 字符串 '\0' 作为结束标志,strlen函数返回的是字符串中 '\0' 前面出现的字符串个数(不包含 '\0' )。
  2. 参数指向的字符串必须要以 '\0' 结束。
  3. 注意函数的返回值是size_t,是无符号的(unsigned int)

解释第3点

int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)
	{
		printf(">\n");
	}
	else
	{
		printf("<=\n");
	}
	return 0;
}

打印结果为:> 

分析:这里是无符号整型相减,虽然 3 - 6 = -3 ,但这里的 -3 的补码会直接被解析成原码,故-3在被解析后就是一个很大的正数。

实例

#include<stdio.h>
#include<string.h>

int main()
{
	char arr[] = "abc";
	int len = strlen(arr);

	printf("%d\n", len);

	return 0;
}

打印结果是 3 。此时没有计算 '\0'

而如果将数组arr改为:char arr[] = { 'a', 'b', 'c' };  此时数组内没有放入 '\0' ,打印结果就是随机值

这里之所以是74,是因为strlen函数在内存中持续往后查找,在某个位置找到了 '\0'

模拟实现strlen函数

提供一种写法

#include<stdio.h>
#include<assert.h>

int my_strlen(const char* str)//加上const,使代码更加“健壮”
{
	assert(str != NULL);//记得断言
	int count = 0;//计算器
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	char arr[] = "abc";
	int len = my_strlen(arr);

	printf("%d\n", len);

	return 0;
}

strcpy

 工作原理

把指针变量source所指向的空间内的数据拷贝到指针变量destination所指向的空间内。

  1. 源字符串(source)必须以 '\0' 结束。
  2. 会将源字符串(source)中的 '\0' 拷贝到目标空间。
  3. 目标空间必须足够大,以确保能存放源字符串(source)
  4. 目标空间必须可变。

存放字符串的错误示范

int main()
{
	char arr[20] = { 0 };
	arr = "Hello";
	return 0;
}

这是行不通的,因为 arr 是数组首元素的地址。

解析工作原理

  • 源字符串必须以 '\0' 结束。
  • 会将源字符串中的 '\0' 拷贝到目标空间。

使用strcpy的正确示范

#include<stdio.h>
#include<string.h>

int main()
{
	char arr[20] = "#########";
	strcpy(arr, "Hello");//字符串"Hello"在被使用时,使用的是"H"的地址
	printf("%s\n", arr);
	return 0;
}

打印结果:

执行调试,看见

这里 '\0' 也被带过去了。

---

如果被拷贝的字符串没有 '\0' 结尾,如:

#include<stdio.h>
#include<string.h>

int main()
{
	char arr_1[20] = "#########";
	char arr_2[] = { 'a', 'b', 'c' };

	strcpy(arr_1, arr_2);

	printf("%s\n", arr_1);
	return 0;
}

执行代码,发现

 程序挂了。

分析:数组arr_2最后没有 '\0' 结尾,strcpy函数会从数组arr_2的首地址出发,寻找 '\0' ,在找到 '\0' 之前,无法确认strcpy函数在内存中找到了什么。

---

  • 目标空间必须足够大,以确保能存放源字符串。

还有一种情况

#include<stdio.h>
#include<string.h>

int main()
{
	char arr_1[5] = "#####";
	char* p = "Hello World";
	strcpy(arr_1, p);
	printf("%s\n", arr_1);
	return 0;
}

这种情况执行代码会出现

这是因为目标数组(arr_1)空间太小,强行将字符串拷贝后发生溢出,但是程序崩溃了 - 数组arr_1被撑爆了。

---

  • 目标空间必须可变。
#include<stdio.h>
#include<string.h>

int main()
{
	char* str = "xxxxxxxxxxxxxxxx";
	char* p = "Hello World";
	strcpy(str, p);
	printf("%s\n", str);
	return 0;
}

执行代码,发现:

程序崩溃了。

分析:目标空间必须可以修改。指针变量str内存放的是一个常量字符串的地址,常量字符串无法修改

strcat - 字符串追加/连接

工作原理

把指针变量source所指向的空间内的数据追加到指针变量destination所指向的空间内。

  1. 源字符串(source)必须以 '\0' 结束。
  2. 目标空间必须足够大,能容纳下源字符串(source)的内容。
  3. 目标空间必须可以修改。
  4. 字符串无法自己追加自己。

实例

#include<stdio.h>
#include<string.h>

int main()
{
	char arr_1[20] = "Hello ";
	char arr_2[] = "World";
	strcat(arr_1, arr_2);//字符串追加/连接
	printf("%s\n", arr_1);
	return 0;
}

打印结果为:

运行调试,发现

注意,原本的数组arr_1内存放的那个 '\0' 没有了。

分析:

一种测试方法(测试是否会追加arr_2中的 '\0'

将数组arr_1改为 char arr_1[20] = "Hello \0##########";  注意:arr_1内原本就有一个 '\0' ,这里多放了一个 '\0' 进去,并且在后面追加了#号,如果strcat函数执行,如果拿取了arr_2的 '\0' ,则arr_1中的一个#号会被覆盖掉

现在开始调试,发现:

说明数组arr_2的 '\0' 也会被抓取,同时,这也说明指针变量source所指向的空间内必须存在 '\0'

模拟实现

分析:

参考库函数写法

void my_strcat(char* dest,const char* src)

src指向的空间不需要改变,加入const 。

注意:

字符数组被初始化后,如果存在没有被初始化的部分,这部分会自动被放入 '\0'

对于指针变量destsrc,存在

要把src指向的 'W' 追加到dest指向的 '\0' 的位置上:

  1. 找到目标字符串(arr_1)中的'\0'
  2. 源数据(arr_2)追加过去,包含'\0'
  3. 注意返回类型,返回目标空间(destion)的起始地址。

#include<stdio.h>
#include<assert.h>
char* my_strcat(char* dest,const char* src)
{
	char* ret = dest;
	assert(dest && src);
	//找到目标字符串(arr_1)中的'\0'
	while (*dest)
	{
		dest++;
	}
	//while循环停下时,dest指向 '\0',即0

	//源数据(arr_2)追加过去,包含'\0'
	while (*dest++ = *src++)
	{
		;
	}
	//当*++dest = *src++拿取'\0'时,表达式的结果就是'\0'

	return ret;//返回目标空间的起始地址
}

int main()
{
	char arr_1[20] = "Hello \0##########";
	char arr_2[] = "World";
	printf("%s\n", my_strcat(arr_1, arr_2));
        //由于返回值(char*),所以可以直接打印

	return 0;
}

库函数的实现参考

注意

对strcat函数而言,无法将目标字符串拷贝到其自身后面,如:

int main()
{
	char arr[20] = "abcd";
	strcat(arr, arr);
	printf("%s\n", arr);
	return 0;
}

运行代码,发现

代码挂掉了。

原因:数组arr内原本被放入的是 'a' 'b' 'c' 'd' '\0' ,在追加字符串时,字符'a'会把原本的 '\0' 覆盖掉,导致最后无法找到 '\0' ,又开始拿取 字符'a',陷入死循环。

(ps:笔者在VS2022上用64位环境测试该库函数时,代码没有挂掉。而如果测试自己编写的my_strcat函数,发现代码挂掉了,推测这里是编译器本身导致的问题。)

以下为一些推测

开始调试,打开反汇编和寄存器

当执行到这一步时,在寄存器rdx和rcx内部分别存入了一个数组arr首元素的地址

接下来继续执行反汇编

此时再看寄存器

此时寄存器RCX存储的地址变成了0000000000000004,而寄存器RDX存储的地址与原本相比,向后了4个字节,打开内存

ASCII码的61对应的数值就是字符'a',而原本字符'a'所在的位置应该在往前4个字节,

这说明此时已经复制成功。再看寄存器

发现寄存器R11存储了数组arr首元素的地址,结合源代码,也就是说,R11应该对应返回的地址。

而此时寄存器R9和R10也被调用了(如果使用my_strcat函数,这两个寄存器是不会被调用的。)

查询资料分析寄存器R10被用作数据存储,在使用之前会保存原值。

------

猜测:在函数strcat被调用时,编译器会先在寄存器R10内保存数组arr内的数据,在数组arr被改变时,source实际是调用了寄存器R10内的数据,被改变的数组本身则在寄存器R11内被存储起来,这样就达成了函数执行的目的。

strcmp - 字符串比较函数

工作原理

字符串比较和长度无关,两个字符串从首地址开始比较:

▲如果字符相同,比较下一位;

▲如果字符不同,则认为该位字符的ASCII值大的字符串更大。

而如果两个字符串完全相同,则当比较完 '\0' 后,函数结束,认为两个字符串相等。

  • 标准规定:

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

注意

int main()
{
	char* p = "OBC";
	char* q = "ABCDEF";
	if (p > q)
		printf(">\n");
	else
		printf("<=\n");
	return 0;
}

这种写法是不行的。p和q分别是指向两块不同空间的指针:

 调试可以看出地址的不同。

上述代码是在比较两个地址的大小,并不是比较字符串的大小。

同理,if ("OBC" > "ABCDEF") 这种写法也是在比较字符串首地址大小,也不行。

实例

#include<stdio.h>
#include<string.h>
int main()
{
	int ret = strcmp("abbb", "abc");
	printf("%d\n", ret);

	return 0;
}

程序执行结果:-1


#include<stdio.h>
#include<string.h>
int main()
{
	char* p = "abcdef";
	char* q = "abbb";
	int ret = strcmp(p, q);
	if (ret > 0)
	{
		printf("p > q\n");
	}
	else if (ret < 0)
	{
		printf("p < q\n");
	}
	else
	{
		printf("p == q\n");
	}
	return 0;
}

程序执行结果:

模拟实现

#include<stdio.h>
#include<assert.h>
int my_strcmp(const char* s1, const char* s2)
{
	assert(s1 && s2);
	while (*s1 == *s2)
	{
		if (*s1 == '0')
		{
			return 0;
		}
		s1++;
		s2++;
	}
	if (*s1 > *s2)
	{
		return 1;
	}
	else
	{
		return -1;
	}
}

int main()
{
	char* p = "abcdef";
	char* q = "abbb";
	int ret = my_strcmp(p, q);
	if (ret > 0)
	{
		printf("p > q\n");
	}
	else if (ret < 0)
	{
		printf("p < q\n");
	}
	else
	{
		printf("p == q\n");
	}
	return 0;
}

分析:

当函数内部传入数据时,指针s1和s2指向首字符的地址。

 首先首字符'a'进行比较,发现相同,向后寻找;字符'b'也相同;再找,找到字符'b'和'c'不同,开始比较。

优化my_strcmp函数

my_strcmp函数中

if (*s1 > *s2)
{
	return 1;
}
else
{
	return -1;
}

可以被改为 return *s1 - *s2;

库函数的实现参考

长度受限制的字符串函数

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

  • strcpy
  • strcat
  • strcmp

与之对应,存在长度受限制的字符串函数

  • strncpy
  • strncat
  • strncmp

strncpy

拷贝source内开始(num)的字符到destination内。(存在字符限制 - num限制了可以拷贝的字符的个数)

三个参数

  • 目标字符串(destination)的首元素地址;
  • 源字符串(source)的首元素地址;
  • 要拷贝的字符数目(num)。

这个函数的好处是可以控制拷贝字符的个数,可以使字符数组不容易被撑爆,不容易出现警告。

当源字符串(source)长度不满足字符数目(num)的要求时,会自动补上 '\0'

实例

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "abcdef";
	char arr2[] = "qwer";
	strncpy(arr1, arr2, 2);
	printf("%s\n", arr1);
	return 0;
}

打印结果:

注意

如果strncpy函数内部变成 strncpy(arr1, arr2, 6); 的形式,开始调试,发现:

这里确实拷贝了6个字符,最后两个是 '\0'

库函数实现参考

strncat

追加字符串,从源字符串(source)的第一个字符开始,追加到目标字符串(destination)的末尾。

如果源字符串(source)的长度(num)少于要求,只追加到源字符串(source) '\0' 的位置。

实例

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = "Hello ";
	char arr2[] = "World";
	strncat(arr1, arr2, 3);
	printf("%s\n", arr1);
	return 0;
}

打印结果:

同时,如果把长度(num)改为6,也可以正常打印:

库函数实现参考

strncmp

从指针str1str2的指向字符串的首字符的地址开始比较。存在三种情况使函数结束:

  • 比较字符,发现字符不同;
  • 找到终止空字符 '\0'
  • 被比较的字符数目和指定的比对数目(num)匹配。

实例

#include<stdio.h>
#include<string.h>
int main()
{
	char* p = "abcdef";
	char* q = "abcqwert";
	int ret = strncmp(p, q, 3);

	printf("%d\n", ret);

	return 0;
}

打印结果:

而如果把比对数目(num)改为4,打印结果:

strstr - 寻找字符串的子串

  • 如果str2str1的子串,则返回str1中第一次出现子串str2的地址;
  • 如果str2不是str1的子串,则返回一个空指针。

实例

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abcdefabcdef";
	char arr2[] = "bcd";

	//在arr1中查找是否包含arr2数组
	char* ret = strstr(arr1, arr2);

	if (ret == NULL)
		printf("没找到\n");
	else
		printf("找到了:%s\n", ret);

	return 0;
}

打印结果:

模拟实现

#include<stdio.h>
#include<assert.h>
char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	//存储起始位置
	const char* s1 = NULL;
	const char* s2 = NULL;

	char* cp = str1;//指针cp一开始是维护str1的

#include<assert.h>
char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	//存储起始位置
	const char* s1 = NULL;
	const char* s2 = NULL;

	const char* cp = str1;//指针cp一开始是维护str1的

	//匹配
	if (*str2 == '\0')
	{
		return (char*)str1;
	}
	while (*cp)
	{
		//赋值与回正
		s1 = cp;
		s2 = str2;

		while (*s1 && *s2 && (*s1 == *s2))
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return cp;
		}
		cp++;//cp向后找一个元素
	}

	return NULL;
}
int main()
{
	char arr1[] = "abbbcde";
	char arr2[] = "bbc";

	//在arr1中查找是否包含arr2数组
	char* ret = my_strstr(arr1, arr2);

	if (ret == NULL)
		printf("没找到\n");
	else
		printf("找到了:%s\n", ret);

	return 0;
}

分析:

对于传来的两个地址,函数内部只进行比较,加上 const 保护数据。

在比较过程中,存在两种情况:

情况一

  1. 比较字符'a'和字符'c',发现字符不相等,str1继续寻找下一个元素;
  2. 比较字符'b'和'c',又发现不相等,继续向后寻找;
  3. 比较字符'c''c',发现相等;
  4. str1str2分别向后一个元素并进行比对,发现相等;
  5. 继续向后比对……;
  6. str2找到 '\0' ,说明查找完毕,说明找到子串。

情况二


     1.字符'a'和'b'不相等,str1往后寻找一个元素;
     2.str1找到字符'b',字符'b'和字符'b'相等,开始向后匹配,找到:

      3.发现字符'b''c'不相等,接下来str1会向前一个元素 - 回正(即原本开始匹配位置的下一个位置),再进行一次比对(此时str2也要回正):

      4.循环步骤,最后发现str2指向的字符串不是str1指向的字符串。

所以如果想要回正,就需要新的指针s1s2。但是这样是不够的,因为这样只是解决了起始位置的问题,但是str1的回正并不一定是回到起始位置,所以还需要一个指针cp

故函数开始时,存在:

库函数实现参考

strtok - 切割字符串

  • sep参数是个字符串,定义了用作分隔符的字符集合;
  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中的一个或者多个分隔符分割的标记;
  • strtok函数找到str中的下一个标记,并将其用 '\0' 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分字符串时,一般都是临时拷贝的内容,并且该内容可以被修改。)
char tmp[20] = { 0 };
strcpy(tmp, arr);
char* ret = strtok(tmp, p);

其中arr就是原本的字符串。

三种情况

  1. strtok函数的第一个参数不为NULL,函数找到str中的一个标记,strtok函数将保存它在字符串中的位置
  2. strtok函数的第一个参数NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  3. 如果字符串中不存在更多的标记,则返回NULL指针。

注意:strtok函数需要记录 '\0' 的位置,即需要有记录功能(静态变量 - static 修饰局部变量)。

实例

借用cplusplus的例子

#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

代码执行的结果:

例子2

#include<stdio.h>
#include<string.h>

int main()
{
	char arr[] = "255@255.255-255";
	char* p = "@.-";
	char tmp[20] = { 0 };
	strcpy(tmp, arr);

	char* ret = NULL;

	for (ret = strtok(tmp, p);
		ret != NULL; 
		ret = strtok(NULL, p))
	{
		printf("%s\n", ret);
	}
	return 0;
}

打印结果:

分析:

数组tmp内原本存放:"255@255.255-255"

  1. 第一次使用strtok函数,数组tmp内发生改变:"255\0255.255-255"
  2. 第二次使用strtok函数:"255\0255\0255-255"
  3. 第三次使用strtok函数:"255\0255\0255\0255"

strerror

使用库函数的时候,有可能出现调用失败
调用失败时,都会设置错误码
一般地,错误码都会被放入变量 errno (int errno)
errno中一般存储着整数,需要使用函数strerror进行翻译才能变成可以查看的信息

注意:errno是一个全局的错误码。想要使用,需要引用头文件<errno.h>

实例

例1

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	printf("%s\n", strerror(0));
	printf("%s\n", strerror(1));
	printf("%s\n", strerror(2));
	printf("%s\n", strerror(3));
	printf("%s\n", strerror(4));
	return 0;
}

打印结果:

例2

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	FILE* pf = fopen("test.txt", "r");
	//打开文件test.txt 以只读的形式打开

	if (pf == NULL)//如果文件存在,返回一个有效指针;如果文件不存在,pf就是有一个空指针
	{
		printf("%s\n", strerror(errno));
                return 1;
	}
        //读文件
	//...
	fclose(pf);//关闭文件
	pf = NULL;

	return 0;
}

打印结果:

如果对应文件夹下没有文件test.txt,则

 存在一个与strerror函数相关的函数 - perror函数

  1. 该函数直接拿取errno内的错误码,
  2. 打印错误信息,不用通过错误码转换。

与strerror函数相比,可以加入自定义信息。

实例

#include<string.h>

int main()
{
	FILE* pf = fopen("test.txt", "r");
	//打开文件test.txt 以只读的形式打开

	if (pf == NULL)//如果文件不存在,pf就是有一个空指针
	{
		perror("fopen");
		return 1;
	}
	//...
	fclose(pf);//关闭文件
	pf = NULL;

	return 0;
}

打印结果:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值