内存拷贝函数的编写整理(很流行的面试题)

原创 2013年12月04日 23:00:21
很多公司都喜欢出关于内存拷贝的这么一道题,下面是本人整理的关于内存拷贝函数编写的过程,仅供参考。
以下是我在一次模拟笔试中写的程序:
void MyMemcpy(char *dst,char *src,int count)
{
    while(count--)
    {
        *dst++ = *src++;
    }
}
很显然,这样的程序是不合格的。首先,定死了源地址和目标地址的类型,都用了char *型,使得人们使用时,都要通过类型强制转换来解决,很麻烦。改成下面这样的程序:
void MyMemcpy(void *dst,void *src,int count)
{
     while (count--)
     {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
      }
}
这样,把强制转换的步骤移到了库的代码中,使得使用者方便了,而且一劳永逸。
另外,还有几个细节要注意,为了实现链式表达式,将返回值改为void *。此外,如果将*(char *)dst = *(char *)src;写反了,编译也是照样通过的,而找这个错误又要花费很多时间。如果你注意到src所指向的内容在函数内是不应该被改变的,所有对src所指内容的改变都必须被禁止,所以这个参数要用const修饰。改得代码如下:
void * MyMemcpy(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;
}
此时,有经验的程序员会发现,如果dst传入了空指针,那么,程序马上会挂掉。如果出现这样的错误,程序马上挂掉,很可能你回找不出错误在哪里,而花费大量时间在代码里寻找bug。解决这类问题的方法如下代码所示:

void * MyMemcpy(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)”,则编译的时候就通不过了,所以我们要养成良好的程序设计习惯:常量与变量作条件判断时应该把常量写在前面。
上面的代码对参数的合法性首先进行了检查,这样使得程序挂掉的几率降低了,但是性能就打折扣了,因为每次调用都会进行一次判断,特别是频繁调用和性能比较高的场合,它的性能上的损失就不可忽略了。
如果长期严格测试,能够保证使用者不会使用零地址作为参数调用此函数,则希望有简单的方法关掉参数合法性检查。我们知道宏就有某种开关的作用,所系新版程序如下:
void * MyMemcpy(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;
}
如果在调试时,我们加入“#define DEBUG"语句,增强程序的健壮性,那么在调试过后,我们再改为”#undef DEBUG“语句,提高程序的性能。而事实上,标准库中已经存在了类似功能的宏:assert,而且更加好用,它可以在定义DEBUG时,指出代码在哪一行检查失败,而在没有定义DEBUG时,完全可以把它当做不存在。assert(_expression_r_r)的使用非常简单,当_expression_r_r为0时,调试器就出现一个调试错误,有了这个,代码就容易多了。
void * MyMemcpy(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。举例这个例子来说,用下列代码来调试上述程序。
void Test()//重叠的内存测试
{
    char p [256]= "hello,world!";
    MyMemcpy(p+1,p,strlen(p)+1);
    printf("%s\n",p);
}
如果你身边有电脑,你可以试一下,你会发现输出并不是我们期待的“hhello,world!”(在“hello world!”前加个h),而是“hhhhhhhhhhhhhh”,这是什么原因呢?原因出在源地址区间和目的地址区间有重叠的地方,V0.6版的程序无意之中将源地址区间的内容修改了!有些反映快的同学马上会说我从高地址开始拷贝。粗略地看,似乎能解决这个问题,虽然区间是重叠了,但是在修改以前已经拷贝了,所以不影响结果。但是仔细一想,这其实是犯了和上面一样的思维不严谨的错误,因为用户这样调用还是会出错:
    MyMemcpy( p, p+1, strlen(p)+1);
    所以最完美的解决方案还是判断源地址和目的地址的大小,才决定到底是从高地址开始拷贝还是低地址开始拷贝。最终代码如下:

void * MyMemcpy(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);
}
我们的程序终于完成了,大家回头看看第一个程序,发现第一个真的是弱爆了。所以,编写程序一定要严谨。
下面附上上面程序的测试程序:
void Test()
{
    char p1[256] = "hello,world!";
    char p2[256] = {0};
    MyMemcpy(p2,p1,strlen(p1)+1);
    printf("%s\n",p2);
    MyMemcpy(NULL,p1,strlen(p1)+1);
    MyMemcpy(p2,NULL,strlen(p1)+1);
    MyMemcpy(p1+1,p1,strlen(p1)+1);
    printf("%s\n",p1);
    MyMemcpy(p1,p1+1,strlen(p1)+1);
    printf("%s\n",p1);
}

本文整理于周立功先生的《卓越的教练是如何训练高手的?》。
版权声明:本文为博主原创文章,未经博主允许不得转载。

QT中的内存管理一个小经验

最近在看Qwt给的示例的源码,遇到这么一句代码: dataCurve->setSymbol(new QwtSymbol(QwtSymbol::Ellipse, QBrush( Qt::red ), Q...
  • zhouguoqionghai
  • zhouguoqionghai
  • 2015年10月03日 16:20
  • 2908

C/C++面试题:编写类String的构造函数、析构函数和赋值函数。

考点:构造函数、析构函数和赋值函数的编写方法 出现频率:☆☆☆☆☆ 已知类String的原型为:         class String         {         public:...
  • zz460833359
  • zz460833359
  • 2015年06月26日 16:26
  • 1493

C语言面试题---字符串拷贝函数与内存拷贝函数

原创2017-12-25创新教育研究中心TeachPlus C语言面试题---字符串拷贝函数与内存拷贝函数 看看下面的这题: strcpy()和memcpy()的区别? 答案: strcpy是字...
  • study_plus
  • study_plus
  • 2017年12月25日 16:43
  • 63

基于visual Studio2013解决面试题之0902内存拷贝

题目解决代码及点评/* 用 C 语言实现函数 void * memmove(void *dest,const void *src,size_t n) memmove 函数的功能是...
  • yincheng01
  • yincheng01
  • 2013年12月20日 01:17
  • 1442

调用拷贝构造函数的几种情况(附面试题)

1. 深拷贝和浅拷贝(拷贝构造函数的使用)   有时候需要自己定义拷贝构造函数,以避免浅拷贝问题。 在什么情况下需要用户自己定义拷贝构造函数: 一般情况下,当类中成员有指针变量、类中有动态内...
  • zzwdkxx
  • zzwdkxx
  • 2016年11月30日 16:24
  • 2345

面试题-基础-字符串转整数&字符串拷贝函数

/* ---------字符串转换为整数(类似与atoi函数)------------ 描述:程序本没有那么复杂,如果只是简单的其实不到10行就可以搞定,但是要考虑不同的情况的话,就稍微复杂点。 下...
  • sd396791336
  • sd396791336
  • 2013年09月14日 21:44
  • 884

Java面试题- java中函数传参传递的是引用的拷贝

身边有些做Java开发的朋友,找工作时常常被考到一道关于字符串的题目。题目倒是很基础,然而根据朋友们事后的描述,有理由认为有的面试官自己都没有完全搞清楚这个问题。此外,在CSDN论坛中我也多次看到一些...
  • jiale0815
  • jiale0815
  • 2011年10月24日 09:40
  • 606

44. 腾讯面试题: 使用函数的参数指针,分配内存空间问题

面试官写了一段程序: void fun(int* a) { a = new int[100]; cout
  • hhh3h
  • hhh3h
  • 2014年05月28日 20:21
  • 1498

(面试题)c++内存分配函数有哪些?

http://wenku.baidu.com/link?url=_LNZMoT8_E5yEEhX5z9iTQ0dTCQudFSV6SyFYdTXuQ6oFqcLP3rIK0Ux_f9hsi7WtrnH...
  • zjytc26
  • zjytc26
  • 2015年01月23日 23:53
  • 302

内存拷贝函数编写

今年阿里实习面试时,面试官叫我写出内存拷贝函数
  • zhonghua18517
  • zhonghua18517
  • 2014年06月02日 15:43
  • 598
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:内存拷贝函数的编写整理(很流行的面试题)
举报原因:
原因补充:

(最多只允许输入30个字)