字符串函数和内存函数

本文详细介绍了C语言中常用的字符串和内存管理函数,如strlen用于计算字符串长度,strcpy和strcat分别用于字符串拷贝和连接,还有strcmp进行字符串比较。此外,还讨论了内存函数memcpy、memmove、memcmp和memset的功能和使用方法,以及它们在处理内存重叠时的差异。文章强调了理解和正确使用这些函数的重要性,以及在编程中需要注意的安全问题。
摘要由CSDN通过智能技术生成

在c语言中,有非常多的库函数,比如我们经常使用的scanf和printf,除此之外,还有一些非常实用的函数,比如我们之前使用的qsort排序函数,可以对任意数据类型进行排序,strlen函数,可以计算字符串的长度,这次,我们就来了解一些和字符串以及内存相关的函数,掌握了这些函数,可以让我们在写代码时变得非常方便

目录

1.字符串函数

1.1strlen

1.2.strcpy

1.3.strcat 

1.4.strcmp

1.5.strncpy

1.6.strncat

1.7.strncmp

1.8.strstr

1.9.strtok

1.10.strerror

1.11.字符分类函数

1.12.字符转换函数

2.内存函数

2.1.memcpy

2.2memmove

2.3.memcmp

2.4.memset


1.字符串函数

1.1strlen

strlen函数是用来求字符串长度的,在我们日常写代码里也经常使用,我们之前也模拟写过strlen函数,有三种方法实现,我们先来看看这三种方法的区别

int my_strlen1(const char* str) {
	assert(str != NULL);
	int count = 0;
	while (*str != '\0') {
		count++;
		str++;
	}
	return count;
}

这种实现方法非常简单,就是遍历一遍字符串,数个数,就不多介绍

int my_strlen2(const char* str) {
	assert(str != NULL);
	if (*str != '\0') {
		return 1 + my_strlen2(str + 1);
	}
	else {
		return 0;
	}
}

第二种方法就是使用递归,如果当前位置不为\0,就返回1+strlen(下一个位置),为\0时返回0即可

int my_strlen3(const char* str) {
	const char* start = str;
	assert(str != NULL);
	while (*str) {
		str++;
	}
	return str - start;
}

第三种方法就是指针减指针了,指针减指针是两个地址之间的元素个数

大家会发现,我们的三种方法,返回值类型都是int,我们看看库里边的实现是什么

我们可以看到,库里边的返回值类型为size_t,size_t是无符号整形,strlen是求字符串长度,求出的长度是不可能为负数,所以使用了size_t,不过这也有利有弊,我们看个例子

 

我们发现,这是一个非常诡异的现象,明明abc比abcdef要短,为什么会输出>呢? 

这是因为3-6=-3,但是strlen函数返回值为size_t,两个无符号数相减也会被当做一个无符号数,而-3看做无符号数时,是一个非常大的正数,所以是大于0的,但如果这里是int,就不会出现这种问题,但是库函数这么选择,也有他的道理,我们在设计函数时,要想清楚他应用的场景,要想清楚这些关系,不要一味的追求哪一种更好,要分析清楚利害关系

我们来看看strlen的一些细节

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

1.2.strcpy

strcpy也是我们熟悉的一个函数,是字符串拷贝,我们来看看他的一些细节

我们可以看到,用strcpy拷贝字符串时,会把arr2里的\0也拷贝到arr1里

 

当我们提前放入\0时,字符串拷贝也会提前停止,并不会将\0之后的内容也拷贝进去

 

使用strcpy函数必须保证目标空间足够大

 

 当我们把字符串拷贝到指针时程序也会发生崩溃,这是因为p所指向的字符串是常量字符串,常量是不能被修改的,我们必须保证目标是可修改的,所以我们使用的是数组

我们来看看strcpy的库函数

接着我们来模拟实现strcpy

char* my_strcpy(char* dest,const char* src) {
	char* ret = dest;
	assert(dest && src);
	while (*dest++ = *src++) {
		;
	}
	return ret;
}

dest代表目标空间,src代表原空间,因为我们要把src拷贝到dest里,所以我们不需要改变src,于是我们加上const,断言可以帮助我们判断是否为空,接着就是进行拷贝,拷贝完后,因为dest的起始位置发生变化,所以我们需要一个变量来记录dest的起始位置,然后我们把他返回即可

我们来总结一下strcpy的细节

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

1.3.strcat 

strcat可能就有人不知道了,这是字符串连接函数,我们先看看他的使用

 strcat可以把arr2的内容拼接到arr1之后,我们来看看他的库函数

strcat的参数和返回类型,和strcpy是一样的 ,都是将原数据xx到目标数据xx,我们来看strcat的细节

strcat的拼接,是将原字符串从目标字符的\0开始替换,然后将剩余的拼接上去

源字符串必须以 '\0' 结束

目标空间必须足够大,能容纳下源字符串的内容

目标空间必须得有\0

目标空间必须可修改

 接着我们来模拟实现strcat

char* my_strcat(char* dest,const char* src) {
	assert(dest && src);
	char* ret = dest;
	while (*dest) {
		dest++;
	}
	while (*dest++ = *src++) {
		;
	}
	return ret;
}

我们要将src连接到dest后面,src不需要修改,所以加上const,我们先要找到dest的末尾,也就是\0的位置,所以第一个while就是找到末尾,第二个while就是追加,我们还需要一个变量来记录dest的起始位置,然后返回即可,如果dest里有多个\0,我们会在遇到的第一个\0开始进行追加

另外,我们是可以将字符串常量连接到数组后的

1.4.strcmp

strcmp是字符串比较,我们先来看他的库函数说明

 

strcmp会对两个字符串进行比较,如果相等返回0,第一个大于第二个会返回>0的数,小于的话返回<0的数,我们来看个例子

 

a和a是相等,b和b相等,q大于c,所以arr1是<arr2的,会输出<

 在vs环境下,会返回的数为1,0,-1,但不是所有的环境下返回值都是这样的

知道了这些,我们来模拟实现一下strcmp

int my_strcmp(const char* str1,const char* str2) {
	assert(str1 && str2);
	while (*str1 == *str2) {
		if (*str1 == '\0') {
			return 0;
		}
		str1++;
		str2++;
	}
	if (*str1 > *str2) {
		return 1;
	}
	else {
		return -1;
	}
}

因为是字符串比较,两个字符串都不用修改,所以我们加上const,while循环,我们用来判断str1和str2是否相等,当他俩一值相等的情况下,走到\0的位置,说明这两个字符串相等,我们返回0,如果中途有不相等的情况,会跳出循环,接着我们根据大小返回1或者-1即可

因为是这个函数并没有规定一定要返回1和-1,所以我们可以把if else语句改为

return *str1 - *str2;

在有些编译器里,就是这样做的,切记返回值是大于0的数,小于0的数和0,而不是1,0,-1

我们上面介绍的这些函数,都是长度不受限制的字符串函数,比如字符串拷贝,他会把第二个参数全部拷贝到第一个参数里,一值到\0为止,而因为这个原因,这些函数会让人感觉不安全,比如目标空间不够大时,他们也会继续执行

所以我们接下来介绍长度受限制的字符串函数

1.5.strncpy

我们来看strncpy,是strcpy的升级版,我们先来看他的库函数

 strncpy相比strcpy多了一个参数,是我们要拷贝几个字符,我们来看例子

 我们发现,我们指定几个字符,strncpy就拷贝几个字符,我们的拷贝完hello后,并没有在hello后加上\0

 我们的arr2只有五个字符,但我们指定10的话,不够的位置会补\0

1.6.strncat

同样的,strncat就是strcat的升级版,也是多了一个参数

 我们来看例子

 我们发现,在第二个图片里,在追加时,在追加结束后,会在后边加上\0

1.7.strncmp

和前面两个一样,strncmp是strcmp的升级版,多了一个参数

我们来看例子

 这个函数是比较简单的

1.8.strstr

接着我们来介绍一个特殊的函数,strstr是查找子串函数,我们先来看看他的库函数

 strstr是在第一个str里找第二个str,他会返回str2在str1里第一次出现的位置,如果没有找到,会返回一个空指针

接着我们来模拟实现一下strstr

char* my_strstr(const char* str1, const char* str2) {
	assert(str1 && str2);
	if (*str2 == '\0') {
		return str1;
	}
	const char* s1 = str1;
	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 cp;
		}
		cp++;
	}
	return NULL;
}

因为是查找字符串子串,我们不需要修改,所以加上const,第一个if语句用来判断特殊情况,如果要查找的子串是空串,我们直接返回str1,cp用来记录每次匹配的起始位置,也就是从str1的起始位置一直走到\0,用cp的位置开始匹配str2,s1和s2用来进行匹配,我们不能直接动用cp进行匹配,否则位置为发现错误,所以借助s1和s2,然后每次匹配完我们把cp赋给s1,str2赋给s2,接着就是匹配,我们在匹配时,s1和s2都可能走到\0,所以我们要加上这个条件,当内层while跳出后,如果s2到了\0,说明匹配成功,我们返回cp即可,否则cp++,如果cp到了\0都没有匹配成功,说明失败,退出外层while,返回NULL,当然我们还有很多细节没有实现,效率不高,我们是暴力求解,但是这样就可以完成strstr的功能了,这样写可能会有警告,如果感觉麻烦,可以在判断特殊情况的if里把str1强制转换为char*,返回的cp也要强制转换为char*

1.9.strtok

strtok也是一个比较奇特的函数,相比strstr,他更加特别,我们来看看他是怎么使用的

我们有12345@qq.com这样一个字符串,他是12345,qq,和com以及@和. 构成,我们想拿到前面三个字符串,就可以使用strtok函数,strtok是字符串分割函数

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

 sep参数是个字符串,定义了用作分隔符的字符集合,比如我们上边字符串,@和.就可以看做分割符,第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记

 strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:
strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容
并且可修改。)

意思就是说,strtok会在arr里查找@符,会把@变为\0,然后会返回12345这个字符串的首元素地址,因为strtok会直接修改字符串内容,所以使用时一定要注意

我们可以先用strcpy进行拷贝,然后切割 

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

 意思就是说,strtok的第一个参数可以为空,也可以不为空,不为空时会寻找第一个标记,为空时他会从保存的位置向后寻找下一个标记

所以我们后续要分割时,要传NULL,他会自己记录一个位置,和\0有关,当找不到标记时会返回NULL

我们这样写太麻烦了,如果字符串很长的话,我们不可能这样复制粘贴那么多次,所以我们要这样写

我们使用循环来写,当ret不为空时,就会持续分割打印 

1.10.strerror

char * strerror ( int errnum );

strerror的作用是返回错误码,所对应的错误信息

c语言的库函数在运行时,如果发生错误,就会将错误码存在一个变量里,这个变量是:errno,是一个全局变量,错误码是一些数字,比如1,2,3,4,5,每一个错误码都对应一个错误信息,我们需要将错误码翻译成信息,我们看几个例子

 0对应的就是没有错误,1对应的是操作被拒绝,即没有权限,2对应的是没有这个文件,3对应的是没有这个进程,4对应的是函数被打断,5对应的是输入输出错误,每一个错误码对应一个错误信息的字符串,strerror是返回首字符的地址,用%s即可打印出来,通过错误信息我们就可以明白代码哪里有错误,实际应用里不是这样使用的,我们来看看我们该如何使用

我们用fopen这个函数举例,这是打开文件函数,如果打开成功,返回有效指针,打开失败返回NULL,当他打开失败时,是因为什么原因失败,我们就可以通过strerror知道

fopen如果没有指定位置,会在当前文件夹里寻找文件,我当前的文件夹里并没有text2023.txt这个文件,所以肯定会打开失败,fclose是关闭文件

 

 假如我们这里不知道为什么会打开失败,该怎么办呢?

我们可以把错误信息打印出来,错误码来自于errno,是全局变量,想使用errno需要引入头文件errno.h,我们看此时的错误信息,是没有这个文件,此时我们就明白了为什么会打开错误

接着我们在当前文件下创建test2023.txt这个文件,我们再来打开看看

 

这次就成功打开了 ,这就是strerror的使用方法,如果有多种错误,我们也可以多次打印,他会按错误顺序打印出来

除了strerror,还有一个函数更加方便

 我们直接来看例子

 他会先将我们输入的错误信息打印出来,然后会打印一个冒号,然后才会打印错误信息,perror里的内容是我们自定义的错误信息,他会先打印出自定义错误信息,我们可以认为perror是printf加strerror两个函数的组合,这个函数虽然方便,但是他不灵活,无论如何,他都会直接打印出错误信息,所以使用时要注意区别

1.11.字符分类函数

字符分类函数的数量非常多,大家使用里进行查询即可,我们来看看有哪些字符分类函数

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

 我们来看几个例子

我们来使用判断是否为小写 ,他会接收一个字符,或者ASCII码值,如果是小写字符,会返回一个非0的数字,否则返回0

使用这个函数,需要引入头文件ctype.h 

 其他函数也都是这样的设计,符合条件返回非0的数,不符合返回0

1.12.字符转换函数

int tolower ( int c );
int toupper ( int c );

这两个函数就非常简单,一个是小写转换为大写,一个是大写转换为小写

 非常简单,我们就不多介绍

2.内存函数

上面我们介绍了字符函数,比如字符串拷贝,字符串连接等等,但他们只能对字符串使用,我们在使用中,会有很多别的情况,比如我们需要拷贝一个整形数据,就不能使用strcpy进行拷贝,这时我们就需要内存函数

2.1.memcpy

memcpy是内存拷贝函数,我们来看看他的函数原型

 我们发现,他的参数是void*,返回值也是void*,这是因为我们可能对任何数据进行拷贝,所以使用了void*,我在上一期博客里详细介绍了void*,大家不了解的话可以去看看

(5条消息) 万字讲解!进阶指针!_KLZUQ的博客-CSDN博客

memcpy和strncpy参数很像,strncpy最后一个参数是拷贝字符的数量,memcpy的最后一个参数是拷贝字节的数量,我们来看个例子

我们将arr1里的5个元素拷贝到arr2里,因为数组是整形, 拷贝5个元素,5*4=20,所以我们拷贝20个字节,我们还可以跳着拷贝,比如我们拷贝3,4,5,6,7到arr2里

我们想怎么拷贝,就怎么拷贝,根据实际需求来看,我们也可以只拷贝17个字节

 

因为大小端的关系,所以这里数据没有影响

接着我们来模拟实现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;
}

因为void*指针不能直接使用,所以我们需要强制转换,又因为不确定类型,所以我们只能一个字节一个字节拷贝,所以我们强转为char*类型,然后让dest和src+1,+1时也需要进行强制转换,最后我们再返回起始地址即可

我们还可以这样写,用前置++来完成dest和src的+1操作,但是不能后置++,在某些编译器下是不行的

我们来看看memcpy的一些细节

函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 '\0' 的时候并不会停下来。
如果source和destination有任何的重叠,复制的结果都是未定义的。

知道了这些内容,那我们该想想我们代码的一些问题了,如果我们要将arr1里的34567拷贝到arr1的12345的位置,会发生什么呢?

此时就发生了问题,因为1先替换3,2先替换4,我们再要拷贝3时,3已经变成了1,4已经变成了2,那我们该怎么解决呢? 

我们可以倒着拷贝,1,2,3,4,5,6,7,8,9,我们先把5拷贝到7的位置,再把4拷贝到6的位置,然后是3到5的位置,依此类推,这样就不会出现问题了,那我们之后拷贝都从后往前拷贝行吗?答案是不行的,如果我们想把3,4,5,6,7拷贝到1,2,3,4,5的位置就会出现我们最开始的问题,所以我们要根据情况的不同,来选择不同的拷贝方式

我们发现当dest的起始位置在src起始位置的左边时,我们需要从前向后拷贝,而其他情况我们则可以从后向前拷贝,而实现这个方法的就是我们的memmove函数

2.2memmove

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

和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。

 我们来模拟实现memmove函数

void* my_memmove(void* dest, const void* src, size_t num) {
	void* ret = dest;
	assert(src && dest);
	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);
		}
	}
}

我们先判断src和dest的位置情况,然后进行if-eles选择,从前向后拷贝和memcpy的逻辑是一样的,从后向前拷贝,我们需要找到末尾位置,while循环的num会先进行-1,比如20变成19,然后再进行拷贝,而起始位置加上此时的num刚好是末尾位置,+1是第二个位置,+19自然是第20的位置,当前在使用src和dest前我们需要将他们强制转换,然后加上num,再解引用即可,此时我们不需要对dest和src进行操作,因为我们是对他们加上了num

其实在vs的环境下,memcpy也是可以对重叠空间进行操作的,效果和memmove是一样的,但是在其他编译器下就不一定了

2.3.memcmp

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

从名字就可以看出,这是内存比较函数,使用方法也是一样的,最后一个参数是字节数,和strcmp是类似的,我们来看个例子

 前8个字节是相等的,返回0

 当我们比较第9个字节后,arr2是比arr1大的,所以返回<0的数(因为大小端的关系,比较9和比较12是一样的)

2.4.memset

memset是内存设置函数 ,他可以把我们的内存设置为我们想要的内容,但是也是以字节来进行设置的,所以出现的效果和我们想象中的是不一样的,我们来看个例子

比如这样,我们就是把arr数组的前5个字符改为x

 

接着我们把world改为y 

我们还可以用0填充,不过改为0后就什么都看不到了

 

 我们要把数组里10个0全部改为1,10个整形40个字节,我们这样写会出现这种情况,我们把数字改为16进制大家就明白了

 这就是我们说的memset是按字节为单位来修改的原因,我们再通过内存来看

 以上就是我们的全部内容,希望大家可以有所收获

如有错误,还请指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值