字符串函数和内存函数
- 字符函数和内存函数
- 字符函数
- 求字符串长度
- strlen
- 长度不受限制的字符串函数
- strcpy
- strcat
- strcmp
- 长度受限制的字符串函数
- strncpy
- strncat
- strncmp
- 字符串查找
- strstr
- strtok
- 错误信息报告
- strerror
- 字符函数:
- 内存函数
- memcpy
- memmove
- memcmp
- memset
- 库函数的模拟实现
- 模拟实现strlen
- 第一种方法:
- 第二种方法:
- 第三种方法:
- 模拟实现strcpy
- 模拟实现strcat
- 模拟实现strcmp
- 模拟实现strstr
- 模拟实现memcpy
- 模拟实现memmove
字符函数和内存函数
本节内容重点就是学会使用一些字符函数和内存函数,并且去模拟几个重要的函数,了解和体会这个函数是怎么样去实现的。不知道各位寒假在家学习的怎样,一定要坚持学习啊。好了,话不多说,我们直接开始学习我们的函数吧。
字符函数
求字符串长度
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;
}
效果:
我猜你想说的一定是-3肯定小于0,打印<=嘛,如果你是这样想的,那么恭喜你,你就错了。实际上我们的结果是输出>,这时候我们就要思考为什么了,实际上还是因为库里面strlen返回值是size_t的问题,两个无符号整型相减得到的一定是无符号整型,将-3的二进制序列看做一个无符号数的话,那值可就大了去了。所以最终结果输出的是>,所以模拟实现strlen的话,返回值设计为int还是size_t没有就绝对的好坏,要根据具体情况来判断。
长度不受限制的字符串函数
strcpy
字符串拷贝函数,我们先看它的函数参数和返回类型,
char * strcpy ( char * destination, const char * source );
然后看一下它的信息:
可以看到使用其实就是将source(源头)字符串的地址和destination(目标)字符串的地址传过去,然后它会帮你拷贝,最后返回目标字符串的起始地址。
使用strcpy需要注意的点:
- 源字符串必须以 ‘\0’ 结束。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
使用的一个简单举例:
int main()
{
char arr1[20] = "xxxxxxxxxxxxxxx";
char* arr2 = "abcdef";//注意常量字符串不能被修改
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
效果:
strcat
首先看函数参数和返回类型,
char * strcat ( char * destination, const char * source );
然后看信息:

可以看到实际上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;
}
效果:
到这里看起来一切合情合理,非常顺利,但是其实是有问题的,strcat这函数是不能用来字符串自己给自己追加的,因为我们是从\0开始的,你上来直接把\0结束标志给干掉了,然后后面就会一直追加,程序就停不下来了。
strcmp
先看函数参数和返回类型,字符串比较函数,
int strcmp ( const char * str1, const char * str2 );
再看函数信息:
传给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;
}
效果:
长度受限制的字符串函数
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;
}
效果:
当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;
}
效果:
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;
}
效果:
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;
}
效果:
字符串查找
strstr
先看函数参数和返回值,字符串查找函数。
const char * strstr ( const char * str1, const char * str2 );
char * strstr ( char * str1, const char * str2 );
再看函数信息:
传递两个字符串地址,在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;
}
输出:
strtok
这个函数可以说是一个很奇葩的函数,它的作用是将一个字符串分割出来。
char * strtok ( char * str, const char * delimiters );
看函数描述:
还是比较长的,我们来解释一下,就是将一个字符串中的分隔符单独放到一个数组里面,将该数组作为第二个函数参数传给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;
}
输出:
但是我们如果每次都这么写确实有点挫,事实上我们通常会巧妙的利用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 );
再看一下函数详细信息:
这个函数就是给它一个错误码作为函数参数,然后返回一个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;
}
输出:
这样我们只是简单举了这样一个例子来解释strerror这个函数,但是在实际情况下不是这样使用的,我们需要用的errno这个宏,
为了实际展示strerror是怎么用的,我们再来简单了解一个函数:文件打开函数fopen,
FILE * fopen ( const char * filename, const char * mode );
这里我们简单看一下即可,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;
}
输出:
可以看到错误信息是目录中没有文件,因为我们当前目录下是没有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来看一下:
也是可以的,一个是直接就打印,所以有时候你并不想将错误信息打印出来这个函数就不是特别好了,没有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 );
再看函数信息:
传递目标地址和源头地址,传递要拷贝的内容大小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
实际上是memcpy一样的功能,和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。 参考下面模拟实现memcpy有详细解释。
memcmp
和strcmp类似,也是一个比较函数,只是通过每一个字节依次比较,
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
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;
}
输出:
memset
内存设置函数,先看函数参数和返回值:
void * memset ( void * ptr, int value, size_t num );
该函数以字节为单位来将内存中的值来修改为你想要的内容。
简单使用举例:
/*memset example*/
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello world!!";
memset(arr, 'x', 5);
printf("%s\n", arr);
return 0;
}
输出:
但是这个函数有几个需要注意的点,例如下面情况:
可以看到似乎出问题了,并没有像我们想象的一样全部初始化为1,一定要牢记:memset是以字节为单位进行修改的,我们打开内存看一下你就明白了:
可以看一下内存中的值:
每一个字节都是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
要去模拟这个函数其实不是特别容易,我们需要考虑两种情况:
一种情况较为简单,没有任何重复的元素,直接寻找一遍即可;另一种情况有些复杂,如果中间有重复的元素,则需要用多个指针来实现。
我们可以通过画图来解释:
第一种情况用指针去寻找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;
}
我们可以测试一下:
当我们这样两组单独的数据去测试的时候看起来没有任何问题,我们再换一种情况,假设我们要将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的问题解决,所以我们这两个最好都要记住。