字符串函数和内存操作函数详解及模拟实现

字符串和内存操作函数的介绍以及模拟实现——strlen 、strcmp、strcpy、strcat、strncmp、strncpy、strncat、strstr、strtok、strerror、memcpy、memste、memcmp


前言

本篇对字符串和内存操作函数进行介绍并对其进行模拟实现。内容涵盖:strlen 、strcmp、strcpy、strcat、strncmp、strncpy、strncat、strstr、strtok、strerror、memcpy、memste、memcmp

标题

一、strlen

strlen这个函数用来求字符串长度,它的判断字符串结束的标准是‘\0’,返回‘\0’前出现的字符个数,不包括‘\0’。并且函数的返回类型为size_t,是无符号的。
在对其进行模拟实现之前,先讲几个需要额外注意的点
1、以\0为结束标志,这就意味着,如果你传入的字符串没有以\0结尾,那么会返回一个随机值
如上图,len_a为一个随机值

2、函数的返回类型为size_t,即一个无符号整数,而不是我们想象 的int。关于什么是size_t,我们可以转到定义查看
可选中size_t并右击,里面有转到定义的选项
这其实会导致一个问题的产生。例如

在这里插入图片描述
我问大家,这段代码的运行结果会是什么?
答案是

在这里插入图片描述
很惊讶吧?明明a的长度小于b的长度呀,那么二者相减结果为-4,应该打印“哈哈”才对呀
因为它是无符号整数相减,答案也是个无符号整数,所以对应-4的二进制的符号位1被当成数据进行计算了
ok,下面我们对该函数进行模拟实现
我们这里讲两个方法,一是计数器实现法,二是递归实现法
1、计数器实现法

size_t my_strlen(const char* a)
{
	assert(a != NULL);
	size_t count = 0;
	while (*a != '\0')
	{
		count++;
		a++;
	}
	return count;

}

这个十分简单,先初始化计数器为0,只要*a不等于0,就对计数器++,并对a++以判断下一个元素是否为0。一旦为0,就代表着字符串结束了,返回此时计数器的值,就是字符串长度
2、递归实现法

size_t my_strlen(const char* a)
{
	if (*a == '\0')
		return 0;
	else
		return 1 + my_strlen(a+1);
}

任何递归都需要结束条件。这个递归的结束条件为*a=’\0’,(实际也就是0,因为’\0’的assic码为0,二者在这里没有区别)
如果不等于,就返回a+1对应的字符串的长度再+1
我画图说明一下
在这里插入图片描述
大家来看这个字符串,它的长度是不是等于第一个字符的长度(也就是1)加上其余字符的长度
也就是1+蓝色方框的长度
在这里插入图片描述
而蓝色方框长度等于1+红色方框长度
在这里插入图片描述
红色方框的长度等于1+绿色方框长度
在这里插入图片描述
那么这些方框长度怎么求呢,很简单,把指向对应方框起始元素的指针传到我们的my_strlen中就行了
这就是递归的底层逻辑
递归法的妙处在于它没有创建任何一个临时变量(不信你仔细观察上面代码,没有任何一个临时变量哟)

二、strcmp

在这里插入图片描述strcmp的参数类型两个都为const char*,这意味着这个函数不改变字符串内容,仅比较了二者的大小。那么,它是怎样去比的呢?
其实,它的比较方式是按照字典序来进行比较的。即:先比较第一个元素,如果第一个元素的大小比较出来,那么整个字符串的大小就比较出来了,不用去管后面的(比如“b”这个字符串大于“azzzzzzz”这个字符串,因为b的assic码值大于a)如果第一个相等,那就比第二个,如果第二个大小比较出来,那么整个字符串的大小就比较出来了,不用去管第三第四个了。以此类推。如果比到\0二者依然相等(即二者的指针都指向\0),说明这两个字符串相等。
那么,它是如何返回的呢?
MSDN上是这样描述的:

在这里插入图片描述
即如果第一个字符串大于第二个字符串,就返回一个大于0的数,如果第一个字符串小于第二个字符串,就返回一个小于0 的数。如果第一个字符串等于第二个,则返回0。

简单介绍过后,我们来对其进行模拟实现

int my_strcmp(const char* a,const char* b)
{
	assert(a&&b);//保证传入的两个指针不为NULL
	while (*a == *b)
	{
		if (*a == '\0')//如果*a='\0',因为已经
			//进入该循环,即*a=*b=‘\0’,即二者比较
			//到‘\0’依然没有比出大小,此时
			//判定二者大小相等,返回0
		{
			return 0;
		}
		a++;
		b++;
	}
	//从while循环出来的条件只有一个,即二者大小
	//不相等。此时说明大小已经比较出来
	return *a - *b;//*a>*b,返回大于0的数
	               //*a<*b,返回小于0的数
}

注释解释的十分详细

三、strcpy

strcpy函数有两个参数,第一个为目标字符串的首元素地址,第二个为源字符串的首元素地址。它的功能是将目标字符串的内容更改为源字符串的内容。这就意味着,它把\0也给拷贝过去了
拷贝前:
在这里插入图片描述
拷贝后:
在这里插入图片描述
还有一点需要额外注意的是,strcpy的返回类型并非为空,而是char*,也就是说,strcpy把目标字符串的首元素地址返回,这就说明,可以直接对其进行打印(在后续的字符串介绍中,我们会经常用到这种操作)
在这里插入图片描述
ok,下面对其进行实现

char* my_strcpy(char* dest, const char* src)
{
	assert(dest != NULL);
	assert(src != NULL);//对传进来的指针进行安全性检查
	char* ret = dest;//因为下面要对dest进行改变,而该函数要求返回目标字符串
	//的首地址,故可直接返回ret,即改变前的dest
	while (*src != '\0')
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = '\0';//while 循环在检测到*src为\0时就终止了,故
	//需对其额外赋值
	return ret;

还可以对它进行精简

char* my_strcpy(char* dest, const char* src)
{
	assert(dest != NULL);
	assert(src != NULL);//对传进来的指针进行安全性检查
	char* ret = dest;//因为下面要对dest进行改变,
	//dest不再指向第一个元素
	//而该函数要求返回目标字符串的首地址,
	//故需额外设置一个指针ret,即改变前的dest
	//方便对这个字符串的首地址进行返回
	
	while (*dest++ = *src++);//当dest*被赋值为\0时,表达式值为左式,即假,while循环终止
	return ret;
}

几乎每一行都用注释解释过了。

char* ret = dest;//因为下面要对dest进行改变,
	//dest不再指向第一个元素
	//而该函数要求返回目标字符串的首地址,
	//故需额外设置一个指针ret,即改变前的dest
	//方便对这个字符串的首地址进行返回
	

这点需要额外注意,后续我们会多次进行类似的操作,其根本目的都是保存传来字符串的首地址方便返回
使用strcpy时,有以下几点注意事项
1、源字符串必须以\0结尾
2、目标空间必须足够大,能容下源字符串
3、目标空间必须可变(不能是char*)

四、strcat

在这里插入图片描述
该函数的功能是,从目标字符串的\0位置处开始,将源字符串拼接到目标字符串后方(包括\0)
实现方法十分简单,如下所示

char* my_strcat(char* dest, const char* src)
{
	assert(dest&&src);//断言二者都不为NULL,保证程序的安全性
	char* ret = dest;//作为返回值,保留目标字符串的起始地址
	while (*dest != '\0')
		dest++;//找到目标字符串\0的位置,便于从此处进行拼接
	while (*dest++ = *src++);//进行字符串拼接操作
	return ret;
}

使用该函数时,有以下几点注意事项
1、目标空间必须足够大,可以容纳拼接后的字符
2、目标空间必须可以修改(不能为char*)
3、源字符串和目标字符串必须包含\0
4、不能用该函数自己拼接自己
针对第四点,我想解释一下
如果你自己拼接自己,即两个参数都传入同一个字符串
那么刚开始的情形是这样的
在这里插入图片描述
dest++找到\0之后是这样的
在这里插入图片描述
然后问题来了,进行拼接时,将dest赋值为src,结束条件是*src=\0,而一旦自己拼接自己,会将\0破坏掉,这样将永远找不到那个\0
在这里插入图片描述

上面我介绍的函数都是 “长度不受限制”的函数,即他们的结束标志都是\0,比较就是最多比较到\0,连接就是连接到源字符串的\0为止,拷贝就是拷贝到\0为止。下面我来介绍几组具有长度限制的函数

五、strncpy

该函数功能与strcmp类似。但不同的是,它多了一个参数:拷贝的个数,单位是字节,如下所示
在这里插入图片描述
需要注意的是,如果你规定拷贝6个过去,但源字符串只有三个字符(带上\0一共3个),那么该函数将这三个字符拷贝过去后,自动在其后面补3个\0,这样加上原来的三个就是你规定的6个字节。
下面图片的说法正确吗?
在这里插入图片描述
错误!这个\0是a这个字符串本身的,不是补过去的。
在这里插入图片描述
上面这个图片是正确的。
好了,下面我们来对其进行模拟实现

char* my_strncpy(char* dest, const char* src,size_t n)
{
	assert(dest != NULL);
	assert(src != NULL);//对传进来的指针进行安全性检查
	char* ret = dest;//因为下面要对dest进行改变,而该函数要求返回目标字符串
	//的首地址,故可直接返回ret,即改变前的dest
	while (n && (*dest++ = *src++))//当dest*被赋值为\0时,表达式值为左式,即假,while循环终止。
		                           //当n等于0,即达到规定的拷贝个数,循环终止。
		n--;  
	if (n)//规定的个数没有拷贝完,补\0直至拷贝n个
	{
		while (--n)
		*dest = '\0';
		dest++;
		
	}
	return ret;
}

六、strncmp

这个函数的作用是比较前n个字符的大小。注释解释的十分详细,在此不作赘述

int my_strncmp(const char* a,const char* b,size_t n)
{
	assert(a&&b);//保证传入的两个指针不为NULL
	while (*a == *b&&n)
	{
		n--;
		if (*a == '\0')//如果*a='\0',因为已经
			//进入该循环,即*a=*b=‘\0’,即二者比较
			//到‘\0’依然没有比出大小,此时
			//判定二者大小相等,返回0
		{
			return 0;
		}
		a++;
		b++;
	}
		//从while循环出来的条件有两个,即二者大小
		//不相等或者n=0。此时说明大小已经比较出来或者达到要求比较个数
	if (n != 0)//大小已经比较出来
		return *a - *b;//*a>*b,返回大于0的数
	                   //*a<*b,返回小于0的数
	else//达到要求比较个数,由于n--使得n=0时,该while没有进行完,
		//依然继续,会对a,b进行++操作使得指针移至n+1的位置,故需-1
		return *(a - 1) - *(b - 1);
	}

七、strncat

这个函数和strncpy不太一样。
当你需要追加的字符个数大于你源字符串的字符个数时,它不会补\0至你需要的个数,而是追加到源字符串结束为止(包括\0),就停下来了,不去理会你多要求追加的个数。
当你需要追加的字符个数小于你的源字符串的字符个数时,它会追加规定个数的字符并且自己补一个\0(因为它要保证追加完毕后你依旧是个以\0结尾 的字符串)

char* my_strncat(char* dest, const char* src, size_t n)
{
	assert(dest&&src);//断言二者都不为NULL,保证程序的安全性
	char* ret = dest;//作为返回值,保留目标字符串的起始地址
	while (*dest != '\0')
		dest++;//找到目标字符串\0的位置,便于从此处进行拼接
	while (n&&(*dest++ = *src++))
		n--;
	if (n == 0)
	{
		*dest = '\0';
	}
	return ret;
}

八、strstr

strstr这个函数用来查找目标字符串里是否存在源字符串这样一个字串,即源字符串是否包含于目标字符串,如果存在,则返回源字符串第一次出现位置的指针,若不存在,则返回NULL
在这里插入图片描述
下面我来举例说明
在这里插入图片描述
str1中存在str2这个字串,那么就返回str2第一次出现位置的指针,如果str2出现多次依然返回第一次出现位置的指针
在这里插入图片描述
str1中不存在str2这个字串,那么返回null。
我们来对其进行模拟实现

char* my_strstr(const char* a1, const char* a2)
{
	assert(a1&&a2);
	const char* p1 = a1;
	const char* p2 = a2;
	const char* cur = a1;
	while (1)
	{
		p1 = cur;
		while (*p1 != '\0'&&*p2 != '\0'&&*p1 == *p2)
		{
			p1++;
			p2++;
		}
		if (*p2 == '\0')
		{
			return cur;
		}
		else if (*p1 == '\0')
		{
			return NULL;
		}
		else
		{
			cur++;
			p2 = a2;
		}
	}
}

下面我来对这个代码进行解释
cur的含义是字串开始的位置(如果有的话),这相当于遍历,因为我们假设每个字符都有可能是字串开始的位置,如果cur到达\0依旧没有找到那就返回NULL。

在这里插入图片描述
这是刚开始的状态。可以看到,p1和 p2并不 相等,说明即便有字串也不是从这个地方开始的,所以cur++。p2回到刚开始的位置便于重新判断,即p2=a2,这也是我们设置a2的意义所在。当又一次while循环开始时,我们另p1=cur,表明从当前cur位置进行字串存在判断。那么就变成下面这个状态
在这里插入图片描述
继续判断,发现条件还是不符合,直到d的时候条件开始符合
在这里插入图片描述
此时大循环里面的小while循环条件成立,进入让p1,p2分别++
在这里插入图片描述
注意,cur的位置没变
然后
p1依然等于
p2,那么循环继续
在这里插入图片描述
p1依旧等于p2,继续
在这里插入图片描述
这时,p2=\0,不满足条件,跳出循环
跳出循环的情况有三种,一是
p2等于\0,此时说明字串确实是从cur位置开始的,直接返回cur。第二种情况是p1=\0,既然已经进入到了第二个if说明p2一定不是\0,那么说明判断到最后依旧没有找到字串,返回NULL。第三种情况是跳出循环是由于*p1!=p2,这时说明字串不是从当前cur所在位置开始的,那么进入else里面。这次的跳出明显属于第一种,即p2=\0,那么返回cur
在这里插入图片描述

九、strtok

strtok是一个字符串分割函数,它的作用是分割字符串
例如,在生活中我们会有这样的场景:给你一串符号:“1234,4532,4563,34”我们需要从这串符号中提取数字部分,那么这就用到了字符串分割函数。它有两个参数,第一个参数是char* str1,是你要分割的函数,第二个参数是const char* str2,是分割符的集合
我先解释一下什么是分割符的集合,比如你要分割这个字符串:1234。3455123& 那么你把这个字符串当作第一个参数传入,然后第二个参数传入 。& 并不是说以“。&”这个字符串为分割符而是以“。”,“”,“&”这三个字符为分割符,因此分割到最后应该得到的是1234 3455 123
下面我来介绍一下它的使用方法。
它的使用方法稍微复杂。当你第一次调用这个函数时,它会返回指向遇到第一个分割符之前的字符串的指针,同时保留这个位置。当年第二次调用这个函数并在第一个参数的位置传入NULL,它会从上一次保留的位置开始继续分割。
结合图片的解释如下:
在这里插入图片描述
上面这张图是使用一次
当你再次使用并将第一个参数传入NULL时,它会按照上一次保留的位置开始继续分割
在这里插入图片描述
在这里插入图片描述
它的模拟实现非常复杂,vs2013版本的编译器给出了它的模拟实现,有兴趣的可以看一下

/***
*strtok.c - tokenize a string with given delimiters
*
*       Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
*       defines strtok() - breaks string into series of token
*       via repeated calls.
*
*******************************************************************************/

#include <cruntime.h>
#include <string.h>
#ifdef _SECURE_VERSION
#include <internal.h>
#else  /* _SECURE_VERSION */
#include <mtdll.h>
#endif  /* _SECURE_VERSION */

/***
*char *strtok(string, control) - tokenize string with delimiter in control
*
*Purpose:
*       strtok considers the string to consist of a sequence of zero or more
*       text tokens separated by spans of one or more control chars. the first
*       call, with string specified, returns a pointer to the first char of the
*       first token, and will write a null char into string immediately
*       following the returned token. subsequent calls with zero for the first
*       argument (string) will work thru the string until no tokens remain. the
*       control string may be different from call to call. when no tokens remain
*       in string a NULL pointer is returned. remember the control chars with a
*       bit map, one bit per ascii char. the null char is always a control char.
*
*Entry:
*       char *string - string to tokenize, or NULL to get next token
*       char *control - string of characters to use as delimiters
*
*Exit:
*       returns pointer to first token in string, or if string
*       was NULL, to next token
*       returns NULL when no more tokens remain.
*
*Uses:
*
*Exceptions:
*
*******************************************************************************/

#ifdef _SECURE_VERSION
#define _TOKEN *context
#else  /* _SECURE_VERSION */
#define _TOKEN ptd->_token
#endif  /* _SECURE_VERSION */

#ifdef _SECURE_VERSION
char * __cdecl strtok_s (
        char * string,
        const char * control,
        char ** context
        )
#else  /* _SECURE_VERSION */
char * __cdecl strtok (
        char * string,
        const char * control
        )
#endif  /* _SECURE_VERSION */
{
        unsigned char *str;
        const unsigned char *ctrl = control;

        unsigned char map[32];
        int count;

#ifdef _SECURE_VERSION

        /* validation section */
        _VALIDATE_RETURN(context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(string != NULL || *context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(control != NULL, EINVAL, NULL);

        /* no static storage is needed for the secure version */

#else  /* _SECURE_VERSION */

        _ptiddata ptd = _getptd();

#endif  /* _SECURE_VERSION */

        /* Clear control map */
        for (count = 0; count < 32; count++)
                map[count] = 0;

        /* Set bits in delimiter table */
        do {
                map[*ctrl >> 3] |= (1 << (*ctrl & 7));
        } while (*ctrl++);

        /* Initialize str */

        /* If string is NULL, set str to the saved
         * pointer (i.e., continue breaking tokens out of the string
         * from the last strtok call) */
        if (string)
                str = string;
        else
                str = _TOKEN;

        /* Find beginning of token (skip over leading delimiters). Note that
         * there is no token iff this loop sets str to point to the terminal
         * null (*str == '\0') */
        while ( (map[*str >> 3] & (1 << (*str & 7))) && *str )
                str++;

        string = str;

        /* Find the end of the token. If it is not the end of the string,
         * put a null there. */
        for ( ; *str ; str++ )
                if ( map[*str >> 3] & (1 << (*str & 7)) ) {
                        *str++ = '\0';
                        break;
                }

        /* Update nextoken (or the corresponding field in the per-thread data
         * structure */
        _TOKEN = str;

        /* Determine if a token has been found. */
        if ( string == str )
                return NULL;
        else
                return string;
}

十、strerror

strerror 是错误检测函数。它的参数只有一个,即错误码。当你传给它一个错误码时,它会返回对应的错误信息。
在这里插入图片描述
当你传入0这个错误码,它会打印相应的错误信息,可以看出,0这个错误码对应的错误信息是“No error”,即没有出错。
我们可以尝试多打印几个错误码看看
在这里插入图片描述
那么这个函数具体该怎么用呢,显然光像这样传入错误码是很难体现它的作用的。它的作用需要一个特殊的变量来体现:errno 使用这个变量前,需要包含一个头文件
#include<errno.h> 这个变量记录着C库函数的错误码,当你程序发生相应错误时,这个变量会被赋予不同的错误码
举个例子
在这里插入图片描述
这段代码的意思是以只读的方式打开一个叫做joijo的txt文件
而我当前的路径底下是没有这个文件的(注意,是你的代码源文件所在路径)
在这里插入图片描述
那么我运行结果会是这样
在这里插入图片描述
看,我并没有传入一个错误码,但它通过errno检测到了错误信息。
我现在创建一个这样的文件
在这里插入图片描述
再次运行代码
在这里插入图片描述
成功打开。
如何查看错误码都有哪些呢?
在这里插入图片描述
右击errno,转到定义

#define EPERM           1
#define ENOENT          2
#define ESRCH           3
#define EINTR           4
#define EIO             5
#define ENXIO           6
#define E2BIG           7
#define ENOEXEC         8
#define EBADF           9
#define ECHILD          10
#define EAGAIN          11
#define ENOMEM          12
#define EACCES          13
#define EFAULT          14
#define EBUSY           16
#define EEXIST          17
#define EXDEV           18
#define ENODEV          19
#define ENOTDIR         20
#define EISDIR          21
#define ENFILE          23
#define EMFILE          24
#define ENOTTY          25
#define EFBIG           27
#define ENOSPC          28
#define ESPIPE          29
#define EROFS           30
#define EMLINK          31
#define EPIPE           32
#define EDOM            33
#define EDEADLK         36
#define ENAMETOOLONG    38
#define ENOLCK          39
#define ENOSYS          40
#define ENOTEMPTY       41

/* Error codes used in the Secure CRT functions */

#ifndef RC_INVOKED
#if !defined (_SECURECRT_ERRCODE_VALUES_DEFINED)
#define _SECURECRT_ERRCODE_VALUES_DEFINED
#define EINVAL          22
#define ERANGE          34
#define EILSEQ          42
#define STRUNCATE       80
#endif  /* !defined (_SECURECRT_ERRCODE_VALUES_DEFINED) */
#endif  /* RC_INVOKED */

/* Support EDEADLOCK for compatibility with older MS-C versions */
#define EDEADLOCK       EDEADLK

/* POSIX SUPPLEMENT */
#define EADDRINUSE      100
#define EADDRNOTAVAIL   101
#define EAFNOSUPPORT    102
#define EALREADY        103
#define EBADMSG         104
#define ECANCELED       105
#define ECONNABORTED    106
#define ECONNREFUSED    107
#define ECONNRESET      108
#define EDESTADDRREQ    109
#define EHOSTUNREACH    110
#define EIDRM           111
#define EINPROGRESS     112
#define EISCONN         113
#define ELOOP           114
#define EMSGSIZE        115
#define ENETDOWN        116
#define ENETRESET       117
#define ENETUNREACH     118
#define ENOBUFS         119
#define ENODATA         120
#define ENOLINK         121
#define ENOMSG          122
#define ENOPROTOOPT     123
#define ENOSR           124
#define ENOSTR          125
#define ENOTCONN        126
#define ENOTRECOVERABLE 127
#define ENOTSOCK        128
#define ENOTSUP         129
#define EOPNOTSUPP      130
#define EOTHER          131
#define EOVERFLOW       132
#define EOWNERDEAD      133
#define EPROTO          134
#define EPROTONOSUPPORT 135
#define EPROTOTYPE      136
#define ETIME           137
#define ETIMEDOUT       138
#define ETXTBSY         139
#define EWOULDBLOCK     140

十一、memcpy

在这里插入图片描述
这个函数叫做内存拷贝函数,是一个功能比之strcpy更为强大的函数。因为strcpy的参数以及实现方式就决定了它只能拷贝字符串。那么有没有一种函数,能够拷贝int,拷贝double,拷贝数组,甚至拷贝结构体呢?
答案是有,那便是这个函数。
先介绍一下参数。第一个参数是void* dest,是目标内存的地址。第二个参数是void* src,即源地址。第三个参数是你要拷贝的字节个数,即你要拷贝多少个字节。那么为什么前两个参数是void类型的呢,道理很简单。void表示这个函数不知道你要穿过来一个什么类型的指针,同时也表示你可以随意传任何类型的指针。但是void*有一个致命的缺陷,那便是它无法进行解引用,自加,自减操作,因为它是个void类型的,无法明确加一或者减一要跳过多少个字节。对于这点,我们有相应的对策,到模拟实现部分我们在详细介绍。
下面我来演示一下它的使用
我们来模拟最复杂的情况:结构体数组
先来创建两个结构体数组
在这里插入图片描述
第一个数组peoples_1有两个成员,分别是Linda女士的信息和Merry女士的信息。第二个数组peoples_2是个空数组,被初始化为0;内存拷贝之前他们的内存是这样子的
在这里插入图片描述
下面进行内存拷贝
在这里插入图片描述
拷贝成功!
下面我们来实现一下这个函数

void* my_memcpy(void* dest, void* src, size_t size)
{
	void* ret = dest;
	assert(dest&&src);
	while (size > 0)
	{
		size--;
		*(char*)dest = *(char*)src;
		((char*)dest)++;
		((char*)src)++;
	}
	return ret;
}

这个代码有一点需要额外注意一下
为什么要把dest和src转化成char*
因为dest和src传进来的是void类型,无法进行++操作。
而我们拷贝内存都是一个字节一个字节的拷贝,char
类型的指针+1刚好就是跳过一个字节。故这里将其强制类型转换为char*方便逐字节拷贝

十二、memmove

memmove这个函数是一个加强版的memcpy。为什么这么说呢,因为memcpy无法解决内存重叠问题而memmove可以。
我先来解释一下什么是内存重叠
众所周知,内存拷贝函数是通过将源指针指向的内存逐字节拷贝到目标指针指向的内存。这就存在一个问题:
在这里插入图片描述
比方说我想拷贝一个数组中的元素,我想把从arr[1]开始的12个字节的元素拷贝到从arr[1]+2开始的内存位置。如上所示,红色方框代表源指针开始12个字节的内存。蓝方框代表目标指针开始的12个字节的内存。我们期望拷贝完后数组内部应该是12323478.按照memcpy的做法,我先把2拷贝到4的位置

然后指针移动
在这里插入图片描述
再把3拷贝到5的位置
在这里插入图片描述
指针移动
在这里插入图片描述
发现问题没,原本4的位置被2覆盖了,这样做会把2拷贝到6的位置

在这里插入图片描述
结果与我们的期望不符,12323278
这种情况我们称之为 内存覆盖
C语言标准规定,memcpy不负责处理这块问题,也就是说,memecpy不负责处理内存覆盖的情况。内存一旦覆盖,你要交给另一个函数处理,即我们要实现的memmove。然而,vs2013版本的编译器的memcpy也可以实现内存覆盖下的拷贝,这不符合C语言标准的规定,因为这样会导致vs2013的编译器memcpy和memmove的实现完全一样。我们不在此做过多计较,只需理解:当你需要内存覆盖下的拷贝是,选择memmmove。
那么是不是说,只要内存发生重叠,就必须使用memmove呢?一定可以这样做,但不一定非得这样做。因为有一种内存重叠的情况是不会发生内存覆盖的。如下图
在这里插入图片描述
像这种目标指针的位置在源指针前面的内存重叠按照我们memcpy的方法是不会发生内存覆盖的
说了这么多,怎么破解这个问题呢?
我们可以采用倒序拷贝法
在这里插入图片描述
还是第一种情况,即源指针在目标指针的前面。这时按照memcpy的做法会发生内存覆盖。这次我们采用倒序拷贝
可以这样想,先把指针移动到结尾
在这里插入图片描述
再进行拷贝,每拷贝一次,指针减一
拷贝
在这里插入图片描述
指针减一
在这里插入图片描述
拷贝
在这里插入图片描述
指针减一
在这里插入图片描述
拷贝
在这里插入图片描述
得到了我们想要的12323478
需要注意的是,这种方法只适用于源指针在目标指针前面内存重叠现象。如果你不是内存重叠,就不可能发生内存覆盖。如果你源指针在目标指针后面,只需按照memcpy的方法正向拷贝即可
代码实现如下

void* my_memmove(char* dest, const char* src, size_t size)
{
	assert(dest&&src);
	void* ret = dest;
	if(((char*)dest) - ((char*)src) < 0 && ((char*)src) - ((char*)dest) < size )
		//内存重叠,且目标指针在源指针前面
	{
		while (size > 0)
				{
					size--;
					*(char*)dest = *(char*)src;
					((char*)dest)++;
					((char*)src)++;
				}
				return ret;
	}
	else if (((char*)dest) - ((char*)src) > 0 && ((char*)dest) - ((char*)src) < size )
	{
		//内存重叠,且源指针在目标指针前面
		while (size > 0)
		{
			size--;
			*(char*)(dest+size) = *(char*)(src+size);
			
		}
		return ret;
	}
	else
		//无内存重叠
	{
		while (size > 0)
		{
			size--;
			*(char*)dest = *(char*)src;
			((char*)dest)++;
			((char*)src)++;
		}
		return ret;
	}
}

十三、memcmp

内存比较函数。它和字符串比较函数的不同之处有两点。一是它比较的是内存空间中的元素的大小。二是它规定了比较的字节个数,即你要比较多少个字节
举例来说
在这里插入图片描述数组arr1的前八个字节和数组arr2的前八个字节相等,故返回0
在这里插入图片描述
而如果比较前16个字节,则会返回一个大于0的数,vs2013的编译器选择直接返回1,而C标准中规定只要返回大于0的数即可。
那么有一个问题,就是如果我比较不是4的倍数的字节该如何比呢?
比如,我比较13个字节,如何?

实践出真知,我们跑一下看看
在这里插入图片描述
为什么呢?我们需要看问题的本源:内存比较函数,归根结底比较的是内存。我们把内存调出来看一下
arr1的内存
arr2的内存
arr1的第13个字节为05,arr2的第13个字节为04,而05>04,故返回1
下面我们来对它进行模拟实现

int my_memcmp(const void* a1,const void* a2,size_t size)
{
	assert(a1&&a2);
	while (*((char*)a1) == *((char*)a2)&&size>0)
	{
		((char*)a1)++;
		((char*)a2)++;
		size--;
	}
	return *((char*)a1) - *((char*)a2);
}

十四、memset

在这里插入图片描述
内存设置函数。它的功能是把指定大小的空间的内存统统改为你规定的字符
int c的这个c的含义是你要设定的字符是什么
在这里插入图片描述
我们来举例说明
在这里插入图片描述

这段代码的含义是,把a这块空间的7个字节统统设置为’x’
那么为什么c的类型是 int 呢?可以把它理解为assic码值
在这里插入图片描述
这里a这个数组的前四个字节统统被设置为x的assic码值,为78.
在这里插入图片描述
打印结果再次验证了我们的猜测,因为2021161080这个数就是十六进制的78 78 78 78转化为十进制
在这里插入图片描述
下面我们来对其进行模拟实现

void* my_memset(void* a, int c, size_t size)
{
	assert(a);
	void* ret = a;
	while (size > 0)
	{
		*((char*)a) = c;
		((char*)a)++;
		size--;
	}
	return ret;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值