C进阶:字符函数和内存函数

字符函数和内存函数

本节内容重点就是学会使用一些字符函数和内存函数,并且去模拟几个重要的函数,了解和体会这个函数是怎么样去实现的。不知道各位寒假在家学习的怎样,一定要坚持学习啊。好了,话不多说,我们直接开始学习我们的函数吧。

字符函数

求字符串长度

strlen

strlen其实是我们的老朋友了,但是今天我们还是来复习一下它,我们首先来看函数的参数和返回类型,

size_t strlen ( const char * str );

使用strlen需要注意的点:

  • 字符串中一定要有\0,因为字符串是以\0作为结束标志。
  • strlen统计的是字符串中\0之前的字符个数

关于strlen的简单使用就不再举例了。

但是我们还发现一个问题就是,库里面的strlen返回类型实际上是size_t,我们知道size_t其实就是unsigned int(无符号整型),我们可以猜测一下,在函数设计时,设计者可能想的是strlen来求字符串长度,长度大小那么一定为正数,所以设计成了size_t。但是我们在模拟strlen的时候,我们一般将返回类型设为int,这是为什么呢?我们来看下面这样一段代码:

#include <stdio.h>
#include <string.h>
int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)
	{
		printf(">\n");
	}
	else
	{
		printf("<=\n");
	}
	return 0;
}

效果:image-20230111125653304

我猜你想说的一定是-3肯定小于0,打印<=嘛,如果你是这样想的,那么恭喜你,你就错了。实际上我们的结果是输出>,这时候我们就要思考为什么了,实际上还是因为库里面strlen返回值是size_t的问题,两个无符号整型相减得到的一定是无符号整型,将-3的二进制序列看做一个无符号数的话,那值可就大了去了。所以最终结果输出的是>,所以模拟实现strlen的话,返回值设计为int还是size_t没有就绝对的好坏,要根据具体情况来判断。

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

strcpy

字符串拷贝函数,我们先看它的函数参数和返回类型,

char * strcpy ( char * destination, const char * source );

然后看一下它的信息:

image-20230111121156563

可以看到使用其实就是将source(源头)字符串的地址和destination(目标)字符串的地址传过去,然后它会帮你拷贝,最后返回目标字符串的起始地址。

使用strcpy需要注意的点:

  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。

使用的一个简单举例:

int main()
{
	char arr1[20] = "xxxxxxxxxxxxxxx";
	char* arr2 = "abcdef";//注意常量字符串不能被修改
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

效果:image-20230111125607715

strcat

首先看函数参数和返回类型,

char * strcat ( char * destination, const char * source );

然后看信息:

image-20230111125212763

可以看到实际上strcat和strcpy还是很类似的,都是将目标地址和源头地址传过去,最后将目标字符串起始地址返回。只是效果不同,strcat是在目标字符串的末尾去进行追加。

使用strcat需要注意的点:

  • 源字符串必须以 ‘\0’ 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。
  • !!!strcat这函数是不能用来字符串自己给自己追加的

使用简单举例:

//strcat——追加字符串
#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[20] = "hello ";//注意要保证目标空间足够大
	char arr2[] = "world!!";
	strcat(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

效果:image-20230111125716927

到这里看起来一切合情合理,非常顺利,但是其实是有问题的,strcat这函数是不能用来字符串自己给自己追加的,因为我们是从\0开始的,你上来直接把\0结束标志给干掉了,然后后面就会一直追加,程序就停不下来了。

strcmp

先看函数参数和返回类型,字符串比较函数,

int strcmp ( const char * str1, const char * str2 );

再看函数信息:

image-20230111132041283

传给strcmp两个字符串的地址,如果第一个字符串小于第二个字符串返回小于0的数字,相等返回0,大于返回大于0的数字。

需要注意的点:

  • 标准规定:
    第一个字符串大于第二个字符串,则返回大于0的数字
    第一个字符串等于第二个字符串,则返回0
    第一个字符串小于第二个字符串,则返回小于0的数字
  • strcmp比较两个字符串是根据第一个不同字符的ASCII码值来比较大小的,千万不要认为是字符串长度。

简单使用举例:

//strcmp——字符串比较
#include <stdio.h>
#include <string.h>
int main()
{
	char* arr1 = "abcdef";
	char* arr2 = "abq";//c小于q
	printf("%d\n",strcmp(arr1, arr2));
	return 0;
}

效果:image-20230111132906577

长度受限制的字符串函数

strncpy

strncpy实际上是同strcpy基本类似,只是最后多了一个函数参数代表个数:

char * strncpy ( char * destination, const char * source, size_t num );

需要注意的是:

  • 拷贝num个字符从源字符串到目标空间。
  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

简单使用举例:

当num小于源字符串长度

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

效果:image-20230111135822542

当num大于源字符串长度:

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

效果:image-20230111135956321

strncat

类似的,strncat也是增加了一个参数代表追加个数:

char * strncat ( char * destination, const char * source, size_t num );

需要注意的点是:当追加完要求的三个时,字符串后面会自动放一个\0。

例如:

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

效果:image-20230111141004043

strncmp

同理,strncmp也是比strcmp多了一个函数参数num表示比较前num个字符:

int strncmp ( const char * str1, const char * str2, size_t num );

简单使用举例:

/*strncmp  example*/
#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "abcdq";
	printf("%d\n", strncmp(arr1, arr2, 5));
	printf("%d\n", strncmp(arr1, arr2, 4));
	return 0;
}

效果:image-20230111182734869

字符串查找

strstr

先看函数参数和返回值,字符串查找函数。

const char * strstr ( const char * str1, const char * str2 );
      char * strstr (       char * str1, const char * str2 );

再看函数信息:

image-20230111184353699

传递两个字符串地址,在arr1中查找arr2是否存在,若存在则返回arr1中第一次找到arr2的地址,若没找到则返回空指针。

简单使用举例:

/*strstr  example*/

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

int main()
{
	char* arr1 = "abbcdef";
	char* arr2 = "bbc";
	char* arr3 = "bbcq";
	printf("%s\n", strstr(arr1, arr2));
	printf("%s\n", strstr(arr1, arr3));
	return 0;
}

输出:image-20230111185344987

strtok

这个函数可以说是一个很奇葩的函数,它的作用是将一个字符串分割出来。

char * strtok ( char * str, const char * delimiters );

看函数描述:

image-20230111211204473

还是比较长的,我们来解释一下,就是将一个字符串中的分隔符单独放到一个数组里面,将该数组作为第二个函数参数传给strtok,第一个是想要分割的数组,这样strtok函数能够找到字符串中的分隔符改为\0,将这一部分分割出来,返回这一部分的起始地址。如果没有找到分隔符的话,则返回空指针(NULL)。

需要注意的点:

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

简单使用举例:

/*strtok  example*/
#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "lzuobing@handsome.net";
	char* p = "@.";
	char buf[50] = { 0 };
	strcpy(buf, arr1);
	printf("%s\n", strtok(buf, p));
	printf("%s\n", strtok(NULL, p));
	printf("%s\n", strtok(NULL, p));
	printf("%s\n", strtok(NULL, p));
	return 0;
}

输出:image-20230111212059599

但是我们如果每次都这么写确实有点挫,事实上我们通常会巧妙的利用for循环来实现输出,代码如下:

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

int main()
{
	char arr1[] = "lzuobing@handsome.net";
	char* p = "@.";
	char buf[50] = { 0 };
	strcpy(buf, arr1);
	for (char* ret = strtok(buf, p); ret != NULL; ret = strtok(NULL, p))
	{
		printf("%s\n", ret);
	}
	return 0;
}

错误信息报告

strerror

strerror这个函数说实话也是一个比较特别的函数,我们先来看一下函数参数和返回值:

char * strerror ( int errnum );

再看一下函数详细信息:

image-20230112144533602

这个函数就是给它一个错误码作为函数参数,然后返回一个char*的指针,用来翻译成错误信息,

简单使用举例:

#include <stdio.h>
#include <string.h>
//c库函数在使用是出错的话,会返回错误码
//strerror可以将错误码翻译为错误信息
int main()
{
	printf("%s\n", strerror(1));
	printf("%s\n", strerror(2));
	printf("%s\n", strerror(3));
	printf("%s\n", strerror(4));
	printf("%s\n", strerror(5));
	return 0;
}

输出:image-20230112145106215

这样我们只是简单举了这样一个例子来解释strerror这个函数,但是在实际情况下不是这样使用的,我们需要用的errno这个宏,

为了实际展示strerror是怎么用的,我们再来简单了解一个函数:文件打开函数fopen,

FILE * fopen ( const char * filename, const char * mode );

image-20230112150441016

这里我们简单看一下即可,fopen函数就是用来打开一个文件,如果打开成功返回一个FILE*的一个指针,如果打开失败则返回一个空指针。

我们来让它打开失败一次用strerror来看一下错误信息:

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

输出:image-20230112151455258

可以看到错误信息是目录中没有文件,因为我们当前目录下是没有test.txt文件的,如果你创建一个再去运行就没有这个报错了。

实际上还有一个函数叫perror,这个函数是能够直接打印出错误信息,而strerror是先将错误码转换为错误信息然后自己去实现打印

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

我们还是上面这个例子,直接用perror来看一下:image-20230112153655574

也是可以的,一个是直接就打印,所以有时候你并不想将错误信息打印出来这个函数就不是特别好了,没有strerror灵活。

字符函数:

这些函数看一下知道即可,有时候用这些函数可能会比较方便,例如判断是否是大写字符或小写字符。

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

内存函数

memcpy

内存拷贝函数,可以拷贝任意类型的内容,先看函数参数和返回类型:

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

再看函数信息:

image-20230112170906048

传递目标地址和源头地址,传递要拷贝的内容大小num个字节,memcpy会帮你把内存拷贝过去最后返回目标起始地址。

简单使用举例:

/*memcpy example*/
#include <stdio.h>
#include <string.h>

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

memmove

image-20230112184013641

实际上是memcpy一样的功能,和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。 参考下面模拟实现memcpy有详细解释。

memcmp

和strcmp类似,也是一个比较函数,只是通过每一个字节依次比较,

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

image-20230112192856656

1中的内容小于2中的内容,返回小于0的数;相等返回0;大于则返回大于0的数。

简单使用举例:

/*memcmp example*/
#include <stdio.h>
#include <string.h>
int main()
{
	int arr1[] = { 1,2,3 };
	int arr2[] = { 1,2,3 };
	int arr3[] = { 1,2,9 };
	printf("%d\n", memcmp(arr1, arr3,12));
	printf("%d\n", memcmp(arr1, arr2,12));
	printf("%d\n", memcmp(arr3, arr2,12));
	return 0;
}

输出:image-20230112193048180

memset

内存设置函数,先看函数参数和返回值:

void * memset ( void * ptr, int value, size_t num );

image-20230112194044052

该函数以字节为单位来将内存中的值来修改为你想要的内容。

简单使用举例:

/*memset example*/
#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "hello world!!";
	memset(arr, 'x', 5);
	printf("%s\n", arr);
	return 0;
}

输出:image-20230112194228208

但是这个函数有几个需要注意的点,例如下面情况:

image-20230112194458741

可以看到似乎出问题了,并没有像我们想象的一样全部初始化为1,一定要牢记:memset是以字节为单位进行修改的,我们打开内存看一下你就明白了:

可以看一下内存中的值:image-20230112194721423

每一个字节都是1,当然是一个很大的数字了。

另外还需要注意的是函数第二个参数ASCII码值不能超过255,因为是以字节为单位进行修改的,8个比特位即使是无符号数最大也就255。

通常来说我们一般初始化是用的最多的情况,也就是每一个字节修改成0;

所以使用函数时要注意每一个函数,看使用合不合理。

库函数的模拟实现

模拟实现strlen

我们模拟strlen有三种思路,一是计数器直接计数,二是使用递归的方式,三是指针相减。

第一种方法:

这种方法是最简单的方法,也是最容易理解的方法。

//第一种方法——计数器
#include <assert.h>
int my_strlen(const char* str)
{
	assert(str!=NULL);//判断指针合法性
	int count = 0;//计数器
	while (*str)
	{
		count++; 
		str++;
	}
	return count;
}

第二种方法:

这种递归法如果你是第一次看可能理解不了,可以看之前我写过的这篇文章,里面有详细解释:

(C语言底层逻辑剖析函数篇(其三),函数递归与迭代超详解,斐波那契数列递归经典例题,汉诺塔问题,青蛙跳台阶_比昨天强一点就好的博客-CSDN博客

//第二种方法——递归
#include <assert.h>
int my_strlen(const char* str)
{
	assert(str != NULL);
	if (*str!='\0')
	{
		return 1 + my_strlen(str + 1);
	}
	else
	{
		return 0;
	}
}

第三种方法:

这种方法唯一需要注意的就是理解一下,两指针相减,得到的是中间的元素个数。

//第三种方法——指针相减
#include <assert.h>
int my_strlen(const char* str)
{
	assert(str != NULL);
	const char* start = str;//记录起始地址
	while (*str)
	{
		str++;
	}
	return (int)(str - start);//指针相减得到的是中间元素个数
}

模拟实现strcpy

一个字符一个字符拷贝即可,一直到源头字符串的\0拷贝过去,需要注意的点就是最后要返回目标字符串的起始地址,所以要提前记录一下。

//模拟实现strcpy
#include <assert.h>
char* my_strcpy(char* des, const char* source)
{
	assert(des && source);//判断指针合法性
	char* ret = des;//记录目标字符串起始地址
	while (*des++ = *source++)//拷贝
	{
		;
	}
	return ret;//返回目标起始地址
}

模拟实现strcat

关键是要想清楚要从目标字符串的末尾\0开始追加,一直追加到原字符串的末尾\0。

//模拟实现strcat
#include <assert.h>
char* my_strcat(char* des, const char* source)
{
	assert(des && source);//判断指针合法性
	char* ret = des;//记录起始地址
	//1.找到目标字符串\0
	while (*des)
	{
		des++;
	}
	//2.追加
	while (*des++ = *source++)
	{
		;
	}
	return ret;
}

模拟实现strcmp

按照顺序一个字符一个字符依次比较其ASCII码值即可。

//模拟strcmp
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* arr1,const char* arr2)
{
    assert(arr1&&arr2);
	while (*arr1==*arr2)
	{
		if (*arr1 == '\0')
		{
			return 0;
		}
		arr1++;
		arr2++;
	}
	/*if (*arr1 < *arr2)
		return -1;
	else
		return 1;*/
	return *arr1 - *arr2;
}

模拟实现strstr

要去模拟这个函数其实不是特别容易,我们需要考虑两种情况:

一种情况较为简单,没有任何重复的元素,直接寻找一遍即可;另一种情况有些复杂,如果中间有重复的元素,则需要用多个指针来实现。

我们可以通过画图来解释:

image-20230111200546714

第一种情况用指针去寻找bbc过程中其实容易出现问题,当我们找到第三个b时,发现不是我们要找的c,但是这时候指针已经往后走了,怎么办,所以这时候我们最后委托两个指针去向后遍历,并且还需要一个指针cp来记录一下开始判断的位置。

//模拟strstr

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

char* my_strstr(const char* str1,const char* str2)
{
	assert(str1 && str2);//判断指针合法性
	if (*str2 == '\0')
	{
		return (char*)str1;//str2为空字符串不做任何处理,直接返回str1
	}
	const char* s1 = str1;//委托两个指针s1,s2
	const char* s2 = str2;
	const char* cp = str1;//记录开始判断的地址
	//重点是理解下面思路
	while (*cp)
	{
		s1 = cp;
		s2 = str2;
		while (*s1 !='\0' && *s2 !='\0' && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return (char*)cp;
		}
		cp++;
	}
	return NULL;
}

模拟实现memcpy

最简单的思路:一个字节一个字节拷贝即可,

#include <assert.h>
void* my_memcpy(void* des, const void* source, int num)
{
	assert(des && source);//判断指针合法性
	void* ret = des;//记录目标起始地址
	while (num--)
	{
		*(char*)des = *(char*)source;
		des = (char*)des + 1;//一定注意这里的写法,不要写成*des++,强制类型转换是临时性的
		source = (char*)source + 1;//
	}
	return ret;
}

我们可以测试一下:image-20230112173014852

当我们这样两组单独的数据去测试的时候看起来没有任何问题,我们再换一种情况,假设我们要将12345拷贝放到34567处,有重复情况时:

看起来似乎就有问题了,并不是我们想要的结果,所以我们上面最简单的思路其实是存在一些问题的。哪里有问题呢,我们来分析一下:

左图是问题分析,右边是解决办法的分情况讨论,

我们这样详细分析清楚之后,实际上真正想要引出的是memmove这个函数,因为memmove实际上就是解决了这个重叠的问题。

模拟实现memmove

我们可以根据以上分析的思路来实现memmove:

//模拟实现memmove
#include <assert.h>

void* my_memmove(char* des, const char* source, size_t num)
{
	assert(des && source);
	char* ret = des;
	if (des < source)
	{
		//前-->后
		while (num--)
		{
			*((char*)des) = *((char*)source);
			des = (char*)des + 1;
			source = (char*)source + 1;
		}
	}
	else
	{
		//后-->前
		while (num--)
		{
			*((char*)des + num) = *((char*)source + num);
		}
	}
	return ret;
}

我相信肯定会有人有疑问啊,memmmove看起来完全就是memcpy的升级版,那么memcpy有什么存在必要呢,这些函数其实都是很多年前设计出的了,我们现在也只能猜测,也许当时是先出的memcpy,后来有人发现了重叠的问题,然后设计出一个memmove,但其实,现在有的平台上的memcpy已经将重叠的问题解决了,例如VS,gcc等,所以其实两种都可以的,但是还有一些环境并没有将memcpy的问题解决,所以我们这两个最好都要记住。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝不过海呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值