原型:void*memcpy(void*dest, const void*src,unsigned int count);

功能:由src所指内存区域复制count个字节到dest所指内存区域。

说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。

举例

//   memcpy.c
#include   <stdlib.h>
#include   <string.h>
main()
{                 char *s= "Golden  Global   View ";  char d[20];  clrscr();  memcpy(d,s,strlen(s));  d[strlen(s)]=0;  printf( "%s ",d);  getchar();  return   0;
}  

下面自行实现这个函数

程序清单 1 V0.1版程序

void MyMemMove(char *dst,char *src,int count)
{   while(count--)   *dst++ = *src++;
}  

程序清单 2 测试V0.1用例

void Test()
{   char p1[256] = ”hello,world!”;   char p2[256] = {0};   MyMemMove(p2,p1,strlen(p1));   printf(“%s”,p2);
}   

客观地讲,相比那些交白卷或者函数声明都不会写的同学来说,能够写出这段代码的同学已经非常不错了,至少在C语言这门课程上已经达到了现行高校的教育目标,但是离企业的用人要求还有一定的距离。我们不妨将上面的程序称为V0.1版本,看看还有没有什么地方可以改进。 
        首先我们看看函数声明是否合理,V0.1版的程序将源地址和目的地址都用char *来表示,这样当然也没有什么问题,但是让其他人使用起来却很不方便,假如现在要将count个连续的结构体对象移动到另外一个地方去,如果要使用v0.1的程序的话,正确的写法如下:

 MyMemMove((char *)dst,(char *)src,sizeof(TheStruct)*count); 

也就是说我们需要将结构体指针强制转换成char * 才能够正常工作,这样除了字符串以外其它的类型都不可避免地要进行指针强制转换,否则编译器就会呱呱叫,比如在VC++2008下就会出现这样的错误:

error C2664: 'MyMemMove' : cannot convert parameter 1 from 'TheStruct *'to 'char *' ;

那么如何解决这个问题呢?其实很简单,我们知道有一种特别的指针,任何类型的指针都可以对它赋值,那就是void *,所以应该将源地址目的地址都用void*来表示。当然函数体的内容也要作相应的改变,这样我们就得到了V0.2版的程序。 
程序清单 3 V0.2版程序

void MyMemMove(void *dst,void *src,int count)
{   while (count--)   {   *(char *)dst = *(char *)src;   dst = (char *)dst + 1;   src = (char *)src + 1;   }
}   

有的同学可能会问,这里面不是还有指针强制转换吗?只不过是换了地方。没错,强制指针转换确实是从使用者的代码转移到了库的代码里,但我们可以将 MyMemMove理解为库,而将Test理解为使用者,事实上通过调整之后的效果却有天壤之别,V0.1是一逸永劳,而V0.2是一劳永逸! 
        还有几个细节需要注意,为了实现链式表达式,我们应该将返回值也改为void *。此外,如果我们不小心将“*(char *)dst = *(char *)src;”写反了,写成“*(char *)src =*(char *)dst;”编译照样通过,而为了找出这个错误又得花费不少时间。注意到src所指向的内容在这个函数内不应该被改变,所有对src所指的内容赋值都应该被禁止,所以这个参数应该用const修饰,如果有类似的错误在编译时就能够被发现:

error C3892: 'src' : you cannot assign to a variable that is const ;

作为程序员犯错误在所难免,但是我们可以利用相对难犯错误的机器,也就是编译器来降低犯错误的概率,这样我们就得到了V0.3版的程序。 
程序清单 4 V0.3版程序

void * MyMemMove(void *dst,const void *src,int count)
{   void *ret=dst;   while (count--)   {   *(char *)dst = *(char *)src;   dst = (char *)dst + 1;   src = (char *)src + 1;   }   return ret;
}   

现在再来考虑这样一种情况,有使用者这样调用库:MyMemMove(NULL,src, count),这是完全可能的,因为一般来说这些地址都是程序计算出来的,那就难免会算错,出现零地址或者其它的非法地址也不足为奇。可以预料的是,如果出现这种情况的话,则程序马上就会down掉,更糟糕的是你不知道错误出在哪里,于是不得不投入大量的精力在浩瀚的代码中寻找bug。解决这类问题的通用办法是对输入参数作合法性检查,也就是V0.4版程序。 
程序清单 5 V0.4版程序

void * MyMemMove(void *dst,const void *src,int count)
{   void *ret=dst;   if (NULL==dst||NULL ==src)   {   return dst;   }   while (count--)   {   *(char *)dst = *(char *)src;   dst = (char *)dst + 1;   src = (char *)src + 1;   }   return ret;
} 

上面之所以写成“if(NULL==dst||NULL ==src)”而不是写成“if (dst == NULL || src == NULL)”,也是为了降低犯错误的概率。我们知道,在C语言里面“==”和“=”都是合法的运算符,如果我们不小心写成了“if (dst = NULL || src = NULL)”还是可以编译通过,而意思却完全不一样了,但是如果写成“if (NULL=dst||NULL =src)”,则编译的时候就通不过了,所以我们要养成良好的程序设计习惯:常量与变量作条件判断时应该把常量写在前面V0.4版的代码首先对参数进行合法性检查,如果不合法就直接返回,这样虽然程序down掉的可能性降低了,但是性能却大打折扣了,因为每次调用都会进行一次判断,特别是频繁的调用和性能要求比较高的场合,它在性能上的损失就不可小觑。如果通过长期的严格测试,能够保证使用者不会使用零地址作为参数调用MyMemMove函数,则希望有简单的方法关掉参数合法性检查。我们知道宏就有这种开关的作用,所以V0.5版程序也就出来了。

程序清单 6 V0.5版程序

void * MyMemMove(void *dst,const void *src,int count)
{   void *ret=dst;
#ifdef DEBUG   if (NULL==dst||NULL ==src)   {   return dst;   }
#endif   while (count--)   {   *(char *)dst = *(char *)src;   dst = (char *)dst + 1;   src = (char *)src + 1;   }   return ret;
}   

如果在调试时我们加入“#defineDEBUG”语句,增强程序的健壮性,那么在调试通过后我们再改为“#undef DEBUG”语句,提高程序的性能。事实上在标准库里已经存在类似功能的宏:assert,而且更加好用,它还可以在定义DEBUG时指出代码在那一行检查失败,而在没有定义DEBUG时完全可以把它当作不存在。assert(_Expression)的使用非常简单,当_Expression为0时,调试器就可以出现一个调试错误,有了这个好东西代码就容易多了。 
程序清单 7 V0.6版程序

void * MyMemMove(void *dst,const void *src,int count)
{   assert(dst);   assert(src);   void *ret=dst;   while (count--)   {   *(char *)dst = *(char *)src;   dst = (char *)dst + 1;   src = (char *)src + 1;   }   return ret;
}

到目前为止,在语言层面上,我们的程序基本上没有什么问题了,那么是否真的就没有问题了呢?这就要求程序员从逻辑上考虑了,这也是优秀程序员必须具备的素质,那就是思维的严谨性,否则程序就会有非常隐藏的bug,就这个例子来说,如果用户用下面的代码来调用你的程序。 
程序清单 8 重叠的内存测试。

void Test()
{   char p [256]= "hello,world!";   MyMemMove(p+1,p,strlen(p)+1);   printf("%s\n",p);
}   

如果你身边有电脑,你可以试一下,你会发现输出并不是我们期待的“hello,world!”(在“hello world!”前加个h),而是“hhhhhhhhhhhhhh”,这是什么原因呢?原因出在源地址区间和目的地址区间有重叠的地方,V0.6版的程序无意之中将源地址区间的内容修改了!有些反映快的同学马上会说我从高地址开始拷贝。粗略地看,似乎能解决这个问题,虽然区间是重叠了,但是在修改以前已经拷贝了,所以不影响结果。但是仔细一想,这其实是犯了和上面一样的思维不严谨的错误,因为用户这样调用还是会出错:

MyMemMove( p, p+1, strlen(p)+1); 

所以最完美的解决方案还是判断源地址和目的地址的大小,才决定到底是从高地址开始拷贝还是低地址开始拷贝,所以V0.7顺利成章地出来了。 
程序清单 9 V0.7版程序

void * MyMemMove(void *dst,const void *src,int count)
{   assert(dst);   assert(src);   void * ret = dst;   if (dst <= src || (char *)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);
}  

  经过以上7个版本的修改,我们的程序终于可以算是“工业级”了。回头再来看看前面的测试用例,就会发现那根本就算不上是测试用例,因为它只调用了最正常的一种情况,根本达不到测试的目的。有了上面的经历,测试用例也就相应地出现了,我们不妨用字符数组来模拟内存。 
程序清单 10 相对全面的测试用例 。

void Test()
{   char p1[256] = "hello,world!";   char p2[256] = {0};   MyMemMove(p2,p1,strlen(p1)+1);   printf("%s\n",p2);   MyMemMove(NULL,p1,strlen(p1)+1);   MyMemMove(p2,NULL,strlen(p1)+1);   MyMemMove(p1+1,p1,strlen(p1)+1);   printf("%s\n",p1);   MyMemMove(p1,p1+1,strlen(p1)+1);   printf("%s\n",p1);
} void * memcpy ( void * dst,const void * src,size_t count)
{  void * ret = dst;  while (count--) {  *(char *)dst = *(char *)src;  dst = (char *)dst + 1;  src = (char *)src + 1;  }  return(ret);
}
char *strcpy(char *des, const char *src){  assert((des != NULL) && (src != NULL));  char *ret = des; // 防止改变des的地址  while ((*des++ = *src++) !='\0') ;  return ret;
}  

【c++中内存拷贝函数(C++ memcpy)详解】相关推荐

  1. c++中内存拷贝函数(C++ memcpy)详解

    原型:void*memcpy(void*dest, const void*src,unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域. 说明 ...

  2. 【内存拷贝函数:memcpy与memmove】

    文章目录 前言 一.memcpy函数(内存拷贝)的实验 二.memcpy函数的模拟实现 三.memmove函数(内存移动)的实验 四.memmove函数的模拟实现 总结 前言 c和c++使用的内存拷贝 ...

  3. 【C语言】字符串拷贝函数(strcpy)与内存拷贝函数的不同及内存拷贝函数(memcpy)的模拟实现

    目录 字符串拷贝函数(strcpy) 函数原型 应用实例 我们将一个存放"abcdefg"的字符数组arr的内容拷贝到字符数组brr之中. 缺点 内存拷贝函数(memcpy) 函数 ...

  4. python时间函数报错_python3中datetime库,time库以及pandas中的时间函数区别与详解...

    1介绍datetime库之前 我们先比较下time库和datetime库的区别 先说下time 在 Python 文档里,time是归类在Generic Operating System Servic ...

  5. python获取系统时间函数_python3中datetime库,time库以及pandas中的时间函数区别与详解...

    1介绍datetime库之前 我们先比较下time库和datetime库的区别 先说下time 在 Python 文档里,time是归类在Generic Operating System Servic ...

  6. python for item in items_Python中items()系列函数的用法详解

    这篇文章主要介绍了Python中dictionary items()系列函数的用法,很实用的函数,需要的朋友可以参考下 本文实例讲述了Python中dictionary items()系列函数的用法, ...

  7. php copy array,ES6中Array.copyWithin()函数用法的详解

    ES6为Array增加了copyWithin函数,用于操作当前数组自身,用来把某些个位置的元素复制并覆盖到其他位置上去.下面重点给大家介绍ES6中Array.copyWithin()函数的用法,需要的 ...

  8. MySQL中的sum函数用法实例详解

    今天分享一下mysql中的sum函数使用.该函数已经成为大家操作mysql数据库中时常用到的一个函数,这个函数统计满足条件行中指定列的和,想必肯定大家都知道了,本身就没什么讲头了,这篇文章主要是通过几 ...

  9. python3中datetime库,time库以及pandas中的时间函数区别与详解

    1介绍datetime库之前 我们先比较下time库和datetime库的区别 先说下time 在 Python 文档里,time是归类在Generic Operating System Servic ...

最新文章

  1. 工作中InnoDB引擎数据库主从复制同步心得
  2. 解决 Virtual PC 在 XP PAE模式下无法运行的问题
  3. 计算机网络·用户代理和浏览器 发送邮件分别需要的协议
  4. 本土链雷达网_走向本土设计
  5. IBM沃森为存储系统开发人员带来的启发
  6. windows nginx c++读取请求数据_轻松应对百万并发的Nginx,搞懂LinuxC/C++这些技术栈升职加薪...
  7. leetcode13. 罗马数字转整数
  8. 动态路由选择协议(二)距离矢量路由选择协议
  9. 接上电就工作的单片机
  10. vRealize Operations Manager 安装部署
  11. 岗位理解_如何正确理解策划岗位
  12. NBU查看备份集大小
  13. “JEPG”到“TXT”,魔幻的Loot
  14. linux命令日志抓取,linux抓取某条日志记录的命令
  15. win10计算机右键属性打不开,win10电脑系统属性打不开的解决方法
  16. 在浏览器拉起应用的方式
  17. No module named ‘pip._internal.vcs.git
  18. 每个人来到世上,都是匆匆过客
  19. 手机号如何批量导入通讯录?
  20. 东方财富choice金融终端研究笔记

热门文章

  1. pytorch-多卡GPU训练
  2. windows下安装与使用pix2tex(mathpix免费替代版)
  3. 预告:揭秘7*24小时用数学解码交易的神秘玩家,量化交易者——TokenInsight对话首席...
  4. 爬取携程景点可能遇到的问题
  5. 在HTML网页中怎样写大于号和小于号
  6. python openssl 证书加解密过程感觉是这样
  7. Linux内核(十五)sysrq 详解 I —— 使用手册
  8. 根据目标检测结果裁剪bbox保存到本地,python,opencv
  9. win8运行matlab7.0,Win8.1系统中matlab7.0不兼容的解决方法
  10. MySQL- 经典面试题