【C语言进阶】从入门到入土(字符串和内存函数第二篇)

前言:
本篇继续介绍处理字符和字符串的库函数的使用和注意事项,让我们更好的使用字符串和字符串库函数,去理解运用一些库函数或自定义函数。

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第1张图片

发文时间正值是国庆假期,虽迟但到,祝祖国母亲生日快乐,此生无悔入华夏!

字符串和内存函数:

  • 一.字符串查找
    • 1.strstr
    • 2.strtok
  • 二.错误信息报告
    • 1.strerror
  • 三. 字符分类函数
  • 四.内存操作函数
    • 1.memcpy
    • 2.memmove
    • 3.memset
    • 4.memcmp

一.字符串查找

1.strstr

前面的strcpy是str+cpy的意思是字符串拷贝,那么strstr是字符串字符串,有什么作用呢?我们看一下:
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第2张图片

实际上这是一个查找字符串的函数,就是在一个字符串中查找另一个字符串(子字符串)是否存在的作用。我们可以详细看一下定义:

Returns a pointer to the first occurrence of str2 in str1, or a null,pointer if str2 is not part of str1.(返回一个指针,存储的是str2str1中第一次出现的位置,如果没有找到,则返回空指针。)

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第3张图片 【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第4张图片 【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第5张图片

查!哪里跑!

代码:

int main()
{
	char arr1[] = "hello world,i love world";
	char arr2[] = "china";
    //查找arr1在arr2中第一次出现的位置

	char* ret = strstr(arr1, arr2);
	if (ret == NULL)
	{
		printf("找不到");
	}
	else
	{
		printf("%s", ret);
	}
}

当我们运行起来,得到的结果就是world,i love world,因为我们查到的是第一次出现的地址,所以ret中存的就是第一次 world的地址,然后再打印出来。而由于存储的是第一个world的地址,所以打印出来的时候也是一直到字符串结束的'\0'才打印结束。


然后我们进行自定义环节:

首先我们来分析一下,自定义函数首先需要的就是先摸清这个函数的定义和有多少种情况,然后再一一实现它,所以我们先分析,大致分为三种可能:

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第6张图片

所以我们的构思应该是:

  1. 结束标志应该是s1s2其中一个出现'\0'。所以要记录下s1s2的地址,当其等于'\0'则返回值。
  2. 当对比一样后重新不一样,需返回对比第一个相同的下一个开始进行比较,所以需要用到指针cp记录下地址。
  3. 当对比一样的时候,s1s2两个指针同时++,继续对比。
  4. 最后还要注意当传过来的就只有\0的指针,应该直接返回。

然后我们来写代码:

char* my_strstr(const char*str1, const char* str2)
{
	assert(str1 && str2);//断言,保证不是空指针
	char* s1;//用于对比的指针指向s1中的字符
	char* s2;//用于对比的指针指向s2中的字符
	char* cp = str1;//记录地址,为第二种情况准备
	
	if (*str2 == '\0')//如果一开始就是\0,则直接返回
		return str1;

	while (*cp)//当记录地址读到\0也就是已经对比完整个字符串
	{
		s1 = cp;//放入首元素地址
		s2 = str2;//放入首元素地址

		while (*s1 && *s2 && *s1 == *s2)
		//当s1和s2都不是\0时,且对比字符相同时
		{
			s1++;//s1和s2同时++
			s2++;
		}
		
		if (*s2 == '\0')
		//当s2遇到/0,证明在字符串中已经找到完整的子字符串
		{
			return cp;//返回找到的字符串的首元素地址
		}
		
		cp++;
		//如果上面布置没有返回出去,证明判断错误
		//则就是情况2,记录地址+1然后继续开始比较
	}
	
	return NULL;//当上面的代码运行结束,仍未返回,就是找不到,返回空指针
}

int main()
{
	char arr1[] = "abbbcdef";
	char arr2[] = "bbc";
	//查找arr1中arr2第一次出现的位置
	char *ret = my_strstr(arr1, arr2);

	if (ret == NULL)//返回空指针就是没有找到
	{
		printf("找不到\n");
	}
	else
	{
		printf("%s\n", ret);//找到并打印
	}
	return 0;
}

2.strtok

strtok这个函数就有点东西了,上面既然有追加字符串的,那选择我们也有切断字符串的,具体是什么样子呢,我们来看一下:
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第7张图片

好家伙,知道为什么说他有点意思了吧,说明都比别人长一半。我们首先还是先看他的参数 char * str, const char * delimiters,其实这里的意思就是,前面的str是待宰的羔羊,也就是要切割的字符串。而后面的delimiters就是切割符,就是当遇到这个符号的时候,就需要切开。

这个函数还有一些细节:

  1. 第一个参数str指定一个字符串,它包含了0个或者多个由delimiters字符串中一个或者多个分隔符分割的标记。
  2. delimiters参数是个字符串,定义了用作分隔符的字符集合。可放置多个分割字符,只要遇到放置中有的或者连续的都会切割。
  3. strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  4. strtok的返回值是一个指针。
  5. strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  6. strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  7. 如果字符串中不存在更多的标记,则返回 NULL 指针。
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第8张图片

看定义看的头晕,那还是直接用代码来碰一碰吧:

#include 
int main()
{

	char arr1[] = "gitee.com/happyiucmooo";
	//我们要分割的字符串
	
	char arr2[100] = { 0 };

	char sep[] = "/cm.";//分隔符分割的标记
	strcpy(arr2, arr1);
	char* ret = NULL;//定义一个指针接收返回值

	for (ret = strtok(arr2, sep); ret != NULL; ret = strtok(NULL, sep))
	//for循环中的三个条件 初始化;判断;调整 刚好可以利用起来
	
	//ret = strtok(arr2, sep)   引进第一个参数
	//ret != NULL               判断是否完成分割
	//ret = strtok(NULL, sep)   每一个分割完后继续调整分割
	
	{
		printf("%s\n", ret);
	}

	return 0;
}

最后得到的结果是:

gitee      由分隔符'.'分割
o          由分隔符'c''m'分割
happyiu    由分隔符'\'和"cm"分割
ooo        由分隔符'cm'分割

当第二次调用strtok函数的时候,我们只需要传参NULL就可以,是因为当第一次分隔的时候,strtok除了把分隔符变成'\0'外,还记住了下一个字符的地址。

strtok里面创建存储这个值的变量是什么呢?如果是局部变量的话,当strtok函数运行完就销毁了,根本不可能带入下一次strtok函数中,所以这个变量应该是静态变量或者全局变量,静态变量和全局变量直到程序彻底结束才销毁或者说是回收。

这就是strtok函数的内容和代码啦。


二.错误信息报告

1.strerror

strerror是一个错误信息报告函数。当我们在写代码过程中有错误,c语言会返回错误码,比如:

int main()
{
    return 0//显然没加;
}
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第9张图片

这里我们就可以看见返回的error就是错误码,而后面的解释就是错误码解析出来的信息。每一个错误码表示一个错误信息。而我们的strerror就是报告错误的一个函数,具体我们看代码:

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

	return 0;
}

打印结果:
No error
Operation not permitted
No such file or directory
No such process

也就是说,strerror函数就是接收错误信息的错误码,从而读取得到错误信息并接收。而实际上这些错误是C语言库函数调用失败的时候,会把错误码存储到errno的变量中,errno是C语言本身内部的一个变量。

比如说下面这个代码尝试:

//strerror - 可以返回C语言内置的错误码对应的错误信息
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//fopen是打开文件函数,不成功返回值是NULL
	//括号内容是以读的形式打开"test.txt"

	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		printf("打开文件成功\n");
	}
	return 0;
}

这里运行得到的就是No such file or directory,因为我没有创建这样的文本文件。所以返回的值就是NULL,最后打印错误信息。

在这里我们又又可以用到另一个函数perror,实际上可以理解为打印+strerror的作用。我们用代码来展现:

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("测试");
		//打印+strerror
	}
	else
	{
		printf("打开文件成功\n");
	}
	return 0;
}

实际上打印结果:测试: No such file or directory。所以perror函数就是打印内容+上strerror(errno)的作用。而这个函数的缺点就是他一定会打印错误信息,而不能得到错误信息不打印。


三. 字符分类函数

字符分类函数有:

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

在这里就不一一介绍了,就选第一个说明一下吧,首先说明一下控制字符。

比如说 printf("%d",x) 中的 %d 就是控制字符,它控制输出变量的格式 总之,控制字符就是控制 语句、格式、条件等的字符

然后我们用一段代码来说明:

#include 
#include 
int main ()
{
  int i=0;
  char str[]="first line \n second line \n";
  while (!iscntrl(str[i]))
  {
    putchar (str[i]);
    i++;
  }
  return 0;
}

打印结果是first line。在这里,我们知道iscntrl函数当参数符合条件就返回真,然后我们第一个元素f不是控制字符,所以返回值为假,在while中加上不等于修饰,就是可以运行打印,i++。然后一直打印,直到\n,他是一个控制字符,所以返回值为真,然后while中加上不等于修饰,就停止循环,停止打印了。


四.内存操作函数

1.memcpy

既然字符串可以拷贝,那其他的类型呢,整形,结构体呢,其实这里有一个函数就是考虑到这些而诞生的,它就是memcpy

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第10张图片

memcpy就是一个拷贝这些类型的一个函数,而他的参数是void * memcpy ( void * destination, const void * source, size_t num ); 中,第一个目的地址,第二个源地址,第三个拷贝元素个数。在这里源和目的地址都是void类型,是因为我们不清楚要拷贝的是什么类型,而void空类型就是万能类型,可以接受其他的类型。

① 我们来打一段代码看看:


int main()
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[10] = { 0 };

	memcpy(arr2, arr1, 5 * sizeof(int));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}
    //打印结果:1 2 3 4 5 0 0 0 0 0
	return 0;
}

所以函数memcpy就是从source的位置开始向后复制num个字节的数据到destination的内存位置。


自定义memcpy函数:

void* my_memcpy(void* dest, const void* src, size_t count)
{
	void* ret = dest;
	assert(dest && src);

	while (count--)
	{
		*(char*)dest = *(char*)src;
		//由于是void类型,所以要强制类型转换才能计算,下同
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

int main()
{
	int arr1[5] = { 1,2,3,4,5};
	int arr2[10] = { 0 };

	//拷贝的是整型数据
	my_memcpy(arr2, arr1, 5*sizeof(int));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}

	return 0;
}

② 这时候我们另一个想法,如果自己复制自己会怎么样:

如果sourcedestination有任何的重叠,复制的结果都是未定义的。

void* my_memcpy(void* dest, const void* src, size_t count)
{
	void* ret = dest;
	assert(dest && src);

	while (count--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };

	my_memcpy(arr1+2, arr1, 4 * sizeof(int));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}

	return 0;
}

我们的代码正常情况下应该就是把1 2 3 4覆盖掉3 4 5 6的地方,也就是打印的是1 2 1 2 3 4 7 8 9 10,但是我们的打印结果是1 2 1 2 1 2 7 8 9 10.原因就在于当我们一个一个的拷贝的时候,本来是3覆盖掉5的地方时,3的位置在前面已经被覆盖成1了,所以5 6的位置拷贝3 4 地方的值时,拷贝过来的是已经拷贝了1 2 的3 4位置。

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第11张图片

所以这里做的话是有问题的,如果我们想实现这样的移动式的拷贝,我们可以使用另一个函数:memmove

PS:
实际上在vs2019中,使用memcpy是可以达到这种拷贝效果的,只不过是我们的自定义函数没有达到这种效果。但在C语言库中memcpy的效果也确实只需要达到这样就可以了,只是在vs2019中效果变得更好了。


2.memmove

上面的代码我们使用memmove就可以实现我们想要的效果了:

int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
	
	memmove(arr1+2, arr1, 16);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}
    //1 2 1 2 3 4 7 8 9 10
	return 0;
}

同样我们看一下或者直接对比一下memmovememcpy
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第12张图片

memmove函数顾名思义就是移动字符串的一个函数,其实它和memcpy有着很多的相同点,都是可以完成拷贝的内容的作用。和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。也就是说在拷贝考试中,memcpy只能考60分,但memmove能考100分。

而且这两个函数的参数都是一样的:

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第13张图片

然后我们尝试一下实现这一个100分函数的自定义,先分析思路:
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第14张图片
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第15张图片

然后根据分析我们得到:

对于memmove的自定义,需要分区域讨论:

  1. dest<src时,也就是拷贝内容在后,存放区在前的时候,应该从前往后拷贝元素。
  2. dest=src的时候,随意。
  3. dest>src且不超处拷贝内容的范围时,也就是拷贝内容在前,存放区在后,但两者还有重叠的时候,应该从后往前拷贝。
  4. dest>src且两者无重叠范围的时候,随意。

总结下来我们可以选择分为两大区域,以dest=src为界限,前面的就前往后拷贝元素,后面的从后往前拷贝。

也就是:

void* my_memmove(void* dest, const void* src, size_t count)
{
	//前面部分:
	
	//后面部分:
	
}


int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
	
	my_memmove(arr1+2, arr1, 16);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}

	return 0;
}

然后我们逐一完善就是:

前面部分:

如果是dest < src才进入。需要一个一个的拷贝覆盖,这里还是使用char * 类型好,然后我们先将void *类型强制类型转换为char *,再将其赋值,赋值完之后往前走一个字符大小,继续赋值,直到count为0才退出。

if (dest < src)
	{
		//前面部分:
		while (count--)
		{
			*(char*)dest = *(char*)src;
			dest = *(char*)dest + 1;
			src = *(char*)src + 1;
		}
	}

后面部分:

如果不是前面部分的情况,就是后面部分的情况了。然后对于后面部分的拷贝覆盖,需要从后往前,所以我们要先得到拷贝内容和拷贝到存储的位置的末尾地址,往前覆盖。刚好count进入时是后置- -,我们在赋值的时候先+上一个count就是最末端开始赋值了,然后再执行count- -时,就相当于往前覆盖了。

else
	{

		//后面部分:
		while (count--)
		{
			*((char*)dest + count) = *((char*)src + count);
		}
	}

最后完整的代码就是:

void* my_memmove(void* dest, const void* src, size_t count)
{
	assert(dest && src);
	void* ret = dest;

	if (dest < src)
	{
		//前面部分:
		while (count--)
		{
			*(char*)dest = *(char*)src;
			dest = *(char*)dest + 1;
			src = *(char*)src + 1;
		}
	}
	
	else
	{

		//后面部分:
		while (count--)
		{
			*((char*)dest + count) = *((char*)src + count);
		}
	}

	return ret;
}


int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7,8 ,9,10};
	
	my_memmove(arr1+2, arr1, 16);
	//my_memmove(arr1,arr+2,16);

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}

	return 0;
}

这就是memmove的自定义函数的实现了,也是在vs2019底下的memcpy自定义函数的实现,但memcpy不是在每一个编译器上都能做出100分的答卷,所以一般还是认为是memmove函数更胜一筹。


3.memset

既然能移动,那能不能修改呢,答案是可以的,这就要看我们的memset函数了。memset,意思就是设置内存。我们来看一下定义:

void * memset ( void * ptr, int value, size_t num );
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第16张图片

memset的参数我们可以理解,首先是需要修改的地址内容,然后是修改值,最后是需要修改多少。

代码:

int main()
{
	int arr[] = { 1,2,3,4,5 };
	int i = 0;
	
	printf("改变前:");
	for(i=0;i<5;i++)
	{
		printf("%d ", arr[i]);
	}
	memset(arr, 0, 20);
	
	printf("\n改变后:");
	for (i = 0; i < 5; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
   //打印结果:
   
   //改变前:1 2 3 4 5
   //改变后:0 0 0 0 0
}

同时我们也可以调试以内存中查看变化,来理解函数作用:

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第17张图片

这就是我们的memset函数。


4.memcmp

接下来是memcmp函数

【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第18张图片
int memcmp ( const void * ptr1, 
 const void * ptr2, 
 size_t num ); 

熟悉的意思,熟悉的参数。既然一个一个的类型可以比较,那我们想比较更细的内存时,就用到这一个函数了。是不是又想起来一个函数是strcmp,它比较只是比较字符串的内容,而且遇到'\0'就停止了。而我们这里的memcmp不一样,什么数据都可以比较,反正是一个一个的字节比较。

同样的是它比较的返回值是一样的:
【C语言进阶】从入门到入土(字符串和内存函数第二篇)_第19张图片

我们来代码尝试一下:

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	//在内存中:01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ....
	int arr2[] = { 1,2,3,6,7 };
	//在内存中:01 00 00 00 02 00 00 00 03 00 00 00 06 00 00 00 ....

	int ret1 = memcmp(arr1, arr2, 12);
	int ret2 = memcmp(arr1, arr2, 13);

	printf("%d\n", ret1);//0
	printf("%d\n", ret2);//-1
	return 0;
}

好啦,本篇的内容就先到这里,关于字符串和内存函数也就到这里了噢,还请继续关注,一起努力学习!。关注一波,互相学习,共同进步。

还有一件事:

你可能感兴趣的:(C语言进阶,c语言)