做题家系列 —— 手撕内存拷贝函数 memmove、memcpy
memcpy
??C 和 C++ 使用的内存拷贝函数,函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,即从源 source 中拷贝 n 个字节到目标 destin 中。
函数原型
#include <string.h>
#include <cstring>
void *memcpy(void *str1, const void *str2, size_t n)
参数说明 str1 —— 指向用于存储复制内容的内存起始地址,函数调用时类型强制转换为 void* 指针 str2 —— 指向要复制的数据源的内存起始地址,函数调用时类型强制转换为 void* 指针 n ?? —— 要被复制的内存长度(字节数) ? 关于 void* 指针 ??void* 指针,即不定类型指针。C 语言中指针仅占 4 个字节,即存储了一个内存地址,因此编译器在编译指针时必须知道它的类型(即 * 号前的部分),才能决定读取数据的长度。如 char* 指针,即 char 类型指针,编译器会从它指向的内存地址起只读取 1 个字节进行编译;而对于 int* 指针,即 int 类型指针,编译器则会从它指向的内存地址起读取 4 个字节。 ??在 C 语言中 void 一般表示 “空”、“无” 的意思,因此,void* 指针即为 “无类型指针”,正式称呼为 “不定类型指针”。大家也可以将其理解为 C 语言中指针的默认类型。因为不知道指针类型,编译器无法直接对 void* 指针操作,程序员在使用 void* 指针时必须将其转换为其他类型的指针来使用,具体用法可以看下面的代码示例。 ??至于为什么 memcpy 参数和返回值要用 void* 指针,个人理解一方面是为了标准化,规范化,提高普适性,统一输入输出接口;另一方面也是防止程序员忘记进行类型转换吧,毕竟返回的 void* 不转类型就会报错。 ? 关于 size_t 类型 ??简单理解,C语言中,它底层就是一个 unsigned int 。使用 size_t 主要还是为了提高代码的可移植性、有效性或者可读性。在不同的语言中,不同的平台上,size_t 的定义也许不一样,但这都不是当前程序员关心的问题,我只需要开放接口给后来人就行了。
实现思路
- 函数接口:
· 用两个 void* 指针接收目标内存地址和源内存地址,一个无符号整型接收内存拷贝长度 · 注意给源内存地址的形参加 const(加在数据类型左边,保护指向的内容),防止篡改原始数据 · 返回值类型也是 void* 指针(保证接口的通用性) · 得到函数接口 void* memcpy(void* dst, const void* src, unsigned int count) - 主体部分:
· 先定义返回值 void* ret = dst; return ret;(返回的是目标内存区域的首地址) · 拷贝顺序就用简单的正序拷贝实现,即从头开始,逐字节拷贝 count 次 · 用 while 循环实现迭代操作,while ( count-- ) 正好可以跑 count 次 · 循环内部:先赋值,再偏移。( void* 无法直接运算或解引用,需要先转换类型) · 赋值操作:(char *)dst = *(char *)src; · 偏移操作:dst = (char *)dst + 1; src = (char *)src + 1;
手撕代码
void* memcpy(void* dst, const void* src, unsigned int count)
{
void* ret = dst;
while ( count-- )
{
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
return ret;
}
面试注意要点
- 第 1 行:函数接口必须不能错!尤其是 src 前必须加 const 修饰,进行保护。
- 第 3 行:先写返回值,即将目标内存地址保存下来用于返回。
- 第 4 行:循环条件,根据拷贝长度逐字节循环拷贝,必须用 count-- 不能 --count。
- 第 6 行:void* 指针每次使用时要先进行类型转换。
- 第 7-8 行:指针偏移操作,这也是为什么第 3 行要先写返回值。
memmove
函数原型
#include <string.h>
#include <cstring>
void *memmove(void *str1, const void *str2, size_t n)
memmove 和 memcpy 的区别
- memmove 会检查内存重叠是否,但 memcpy 不会
- 当源内存区域的尾部与目标内存区域的头部重叠时,memmove 会采用倒序拷贝的策略
- 即如下图所示,前两种情况 memmove 采用正序拷贝,第三种情况采用倒序拷贝
实现思路
- 一句话概括:检测内存是否重叠,无重叠则正序逐字节拷贝,重叠则倒序逐字节拷贝。
- 函数接口:
· 跟 memcpy 思路一样,void* memmove(void* dst, const void* src, unsigned int count) - 内存重叠判断:
- 主体部分:
·
手撕代码
void* memmove(void* dst, const void* src, unsigned int count)
{
void* ret = dst;
if ( dst <= src || dst >= (char *)src + count )
{
while (count--)
{
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
}
else
{
dst = (char *)dst + count - 1;
src = (char *)src + count - 1;
while ( count-- )
{
*(char *)dst = *(char *)src;
dst = (char *)dst - 1;
src = (char *)src - 1;
}
}
return ret;
}
面试注意要点
- 第 4 行:内存重叠的检查条件必须不能写错!
- 其余手撕代码重点与 memcpy 相同
?
总结
??memcpy 和 memmove 是程序员面试的经典考题,属于底层代码的复现,外企尤其爱考这些题。这两个函数代码非常短,思想也很简单,短短几行却包含了很多细节考点,很容易考察出程序员的基本功。建议务必背熟,最好能做到行云流水,一气呵成!
|