在c语言中,有非常多的库函数,比如我们经常使用的scanf和printf,除此之外,还有一些非常实用的函数,比如我们之前使用的qsort排序函数,可以对任意数据类型进行排序,strlen函数,可以计算字符串的长度,这次,我们就来了解一些和字符串以及内存相关的函数,掌握了这些函数,可以让我们在写代码时变得非常方便
目录
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*,大家不了解的话可以去看看
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是按字节为单位来修改的原因,我们再通过内存来看
以上就是我们的全部内容,希望大家可以有所收获
如有错误,还请指正