C语言--字符串函数2


本篇文章我们将继续学习相关字符串函数以及内存函数

strtok

我们先来看strtok库函数的书写格式

char * strtok ( char * str, const char * sep );
//其中str是要被分隔的字符串,sep是一个若干个分隔符组成的字符集合。

strtok的具体用法如下:

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

可能只看抽象的用法解释比较难理解,我们用具体的几个例子来搞清楚这个函数具体怎么使用的和效果如何,

int main()
{
    
    
	char arr[] = "192.168.3.212";
	char buf[30] = {
    
    0};
	strcpy(buf, arr);

	const char* p = ".";
	char* str = strtok(buf, p);//
	printf("%s\n", str);

	str = strtok(NULL, p);//
	printf("%s\n", str); 
	
	str = strtok(NULL, p);//
	printf("%s\n", str);

	str = strtok(NULL, p);//
	printf("%s\n", str);

	return 0;
}

运行结果如图1
图1
我们结合图像来分析,如图2
图2

但是我们发现,如果只有几个分隔符还好,如果有成千上万个分隔符的时候,难道我们要写几千几万次重复的内容吗?很显然不可能,所以我们可以用一个巧妙的for循环来解决这个问题。
看下面这段代码:

int main()
{
    
    
	char arr[] = "[email protected]";
	char buf[30] = {
    
     0 };
	strcpy(buf, arr);


	const char* p = "@.";
	char* str = NULL;
	for (str = strtok(buf, p); str != NULL; str=strtok(NULL, p))
	{
    
    
		printf("%s\n", str);
	}

	return 0;
}

运行结果如图3
图3
因为每次调用函数后str都被赋值为指向分隔符位置的指针,并把它作为下一个数组的首元素地址然后执行后续打印等操作,直到最后一次找不到标记时,strtok会返回一个空指针,str!=NULL判定为假,从而跳出了循环

由于这个函数的使用范围比较小,所以本篇文章就不对其进行模拟实现了。

strerror

我们先来看strerror库函数的书写格式

char * strerror ( int errnum );

strerror返回的是一个错误码,每个错误码对应不同的错误信息并将其打印出来
注意,要使用这个库函数需要引用头文件#include <errno.h>
看下面这段代码

int main()
{
    
    
	char* p = strerror(0);
	printf("%s\n", p);

	p = strerror(1);
	printf("%s\n", p);

	p = strerror(2);
	printf("%s\n", p);

	p = strerror(3);
	printf("%s\n", p);
	return 0;
}

运行结果如图4
图4
我们可以发现,不同的整型数字对应不同的错误,至于具体值是多少,这就是电脑自行判断给出的值了,我们以打开一个文件为例

int main()
{
    
    
	//打开文件
	//打开文件的时候,如果文件的打开方式是"r"
	//文件存在则打开成功,文件不存在打开失败
	//打开文件失败的话,会返回NULL
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
    
    
		printf("打开文件失败,原因是:%s\n", strerror(errno));
		return 1;
	}
	//读写文件
	//...
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

运行结果如图5
图5

根据注释内容,由于我的电脑上并没有这个test.txt文件,所以打开失败,返回NULL,并且error的值就为3(看整形3对应的错误信息)

perror

这个库函数实际意义上就是prinf+strerror

int main()
{
    
    
	//打开文件
	//打开文件的时候,如果文件的打开方式是"r"
	//文件存在则打开成功,文件不存在打开失败
	//打开文件失败的话,会返回NULL
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
    
    
		perror("打开文件失败");
		//printf + strerror
		return ;
	}
	//读写文件
	//...
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

运行结果和图5一样,如图6

图6

字符分类函数

我们来看一些字符分类函数

这些函数都是用来判断相关数据的,我们举几个例子
需要引用头文件**#include <ctype.h>**

void test7()
{
    
    
	printf("%d\n", isdigit('6'));	
	printf("%d\n", isspace('2'));
	printf("%d\n", islower('x'));
	printf("%c\n", toupper('x'));
	printf("%c\n", tolower('X'));
}

打印结果如图

这里的toupper,tolower根据字面意思也能理解,作用就是大小写的转换
我们就以这个大小写转换函数为例

void test8()
{
    
    
	char arr[128] = {
    
    0};
	gets(arr);
	int i = 0;
	while (arr[i])
	{
    
    
		if (isupper(arr[i]))
		{
    
    
			arr[i] = tolower(arr[i]);
		}
		printf("%c", arr[i]);
		i++;
	}
}

运行结果如下

可见,如果我们可以灵活运用这些库函数来实现许多功能,就可以大大简化代码复杂度。

讲到这里,我们回过去思考一个问题,我们前面学的内容都是对char类型字符串进行相关操作,那么如果是一个整型数组,浮点型数组又该怎么办呢?貌似用str为前缀的相关函数就行不通了,所以为了操作这些类型的数组,我们引入了使用范围更为广泛的内存函数

memcpy

我们还是先来看memcpy库函数的书写格式

void * memcpy ( void * destination, const void * source, size_t num );

具体的使用方法如下

  1. 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置
  2. 这个函数在遇到 ‘\0’ 的时候并不会停下来
  3. 如果source和destination有任何的重叠,复制的结果都是未定义的(这个后面和memmove比较再来讲)
    我们
    先来以复制一个整型数组为例,看下面这段代码
void test1()
{
    
    
	int arr1[] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	int arr2[8] = {
    
     0 };
	//把arr1中的前5个数据拷贝到arr2中
	memcpy(arr2, arr1, 20);
}

这里要特别注意!
这个size_t num是无符号整型,单位是字节!!!
因为这个函数并不知道我们传递的参数是什么类型,所以如果我们传递的是整型数组,整型是4个字节,所以如果要拷贝五个数据,就应该输入4*5=20.

memcpy的模拟实现

我们现在来模拟实现一下memcpy库函数的相关功能,看下面这段代码:

void* my_memcpy(void* dest, const void* src, size_t num)
{
    
    
	void* ret = dest;
	assert(dest && src);//断言
	while (num--)
	{
    
    
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

因为memcpy的创建者并不知道我们这些使用者将来会用memcpy来操作什么类型的数据,所以用void*来书写,由于size_t num的单位是字节,为了方便循环次数对的上,我们就将dest和src指针都强制类型转换为char类型

memcpy的局限性

我们发现,当用memcpy来拷贝同一数组的元素时可能会出现bug,我们通过图来分析
图7
所以为了避免这种情况的发生,我们首先要对dest和src的位置先后进行一个判断,如果出现上述图中这种情况,就应该从后向前拷贝来避免覆盖,反之则从前向后拷贝
这其实就是接下来要讲的memmove库函数了

memmove

memmove库函数在memcpy函数的基础上又有优势,和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。如果源空间和目标空间出现重叠,就得使用memmove函数处理。
比如下面这段代码

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

	memmove(arr1+2, arr1, 20);

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

运行结果如图8
图8
我们可以发现被覆盖的部分在拷贝的时候并没有受到影响。

memmove的模拟实现

其实memmove函数的模拟实现就是在memcpy的模拟实现上优化了拷贝顺序
具体的代码修改如下:

void* my_memmove(void* dest, const void* src, size_t num)
{
    
    
	void* ret = dest;
	assert(dest && src);
	if (dest < src)
	{
    
    
		//前->后
		while (num--)
		{
    
    
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
    
    
		//后->前
		while (num--)
		{
    
    
			*((char*)dest + num) = *((char*)src + num);
			//num越来越小,从后向前拷贝
		}
	}
	return ret;
}

所以我们来总结一下
1. C语言中,memcpy拷贝不重叠内存
2. 重叠的就交给memmove
3. 在功能性上memmove>memcpy。

memcmp

类比strcmp,memcmp就是一个可以实现比较任意类型数据是否相等功能的库函数
举个例子

void test5()
{
    
    
	int arr1[] = {
    
     1,2,3,4,7};//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 07 00 00 00
	int arr2[] = {
    
     1,2,3,4,6};//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 06 00 00 00
	int ret = memcmp(arr1, arr2, 16);
	printf("%d\n", ret);
}

如果比较的是16个字节,那么就是相等的,但如果比较的是17个字节 07是大于06的,所以返回值是大于0的类比strcmp的返回值

memset

我们还是先来看书写格式

void *memset(void *s, int c, unsigned long n);

其中s是指向要操作的数组或字符串的首元素地址,c是要修改的值,n是要修改的字节长度
看下面这段代码

void test6()
{
    
    
	char arr[] = "hello world";
	memset(arr, 'x', 5);
	printf("%s\n", arr);
}

打印结果如图9
图9
我们再来看一段代码

	int arr[10] = {
    
     0 };

	memset(arr, 1, sizeof(arr));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%x ", arr[i]);
	}

运行结果如图10
图10
我们发现每个元素的值并没有被修改为1,这是因为memset是以字节为单位来修改数据的,但是int类型每个元素显然不是一个字节,所以这种写法是无法将数据的每个元素设置为1的。

以上就是关于字符串函数以及内存函数全部内容了,如有出入,欢迎指正。

猜你喜欢

转载自blog.csdn.net/m0_75233943/article/details/129544104