深拷贝从“new”说起

来源:互联网 发布:苹果ai软件下载 编辑:程序博客网 时间:2024/05/18 18:18

    在设计类时,有时必须提供“深拷贝”函数。一个常见的情形就是需要在类中动态管理内存。一个常见的错误就是误以为C++的new会与C的malloc一样,在申请内存失败后,返回空指针,于是就对返回值进行判断以确定下一步的操作。

㈠ new的基本知识:

 

先说说new操作符:new有三种:newoperator newplacement newnew是通过调用operator new这个库函数来实现内存分配的。new申请内存失败的话,会抛出异常std::bad_alloc,且返回值不为0,当然也可以指定不抛异常的new

 

直接new一个对象,对象是否初始化,与定义一个同类对象相似:

new X;      //调用类X的默认构造函数

new X(x)    //调用类X的构造函数用x来初始化。

new int      //变量未被初始化

new int(5)   //变量初始化为5

new X[5]     //会调用类X的默认构造

 

可以在类型名后加上一对(),使用默认值初始化:

new int()     //变量初始化为0

new int[5]()  //数组的5个元素都初始化为0

new int[5](2) //错误,不允许按指定值进行初始化

 

new int[0];      //可以申请长度为0的数组 

delete (int*)0;  //删除0指针是安全的

 

 

㈡ 什么时候需要“深拷贝”函数:

 

对下面的类:

class Test {

  int *arr;

  size_t sz;

 

 public:

  Test(size_t sz_=0): arr(new int[sz_]()), sz(sz_) { }

  ~Test()  { delete [] arr; }

  // other functions

};

 

这个类成员有一个是指针变量,构造函数和析构函数都涉及对内存的申请和释放。当使用下面代码时,就会出问题。

 

Test a;

Test b(a)   //等价于 Test b=a; 调用与Test::Test(Test&)类似的拷贝构造函数

Test c;

c = a;      //调用与Test::Test& operator=(Test&)类似的赋值函数

 

上面的代码在定义对象b时,会调用拷贝构造函数Test::Test(Test&)由于Test类没有提供类似的拷贝构造函数,编译器就默认生成一个,将类的成员逐个进行简单的赋值(这就是“浅拷贝”)。于是 b.arr a.arr 都指向同一块内存,对b.arr的操作,会破坏a.arr所指向的数据,而当b结束生存期,被析构时,b.arr 所指向的内存(同时也是a.arr所指向的内存)被释放。当对a.arr进行操作时,由于a.arr所指向的内存可能已经被回收或者重新分配给其它对象,因而会有无法估计的错误发生,而当a结束生存期,又a.arr所指向的内存再次执delete操作,可能会造成程序崩溃,甚至更严重。(这与newdelete的具体实现相关,有的实现版本是将分配好的内存信息存放在一个表中,delete时根据查表结果再操作;有的实现则是简单的多申请一点空间,储存用户所申请的内存大小信息,delete时根据该信息再操作。前者安全但效率低,后者高效,但不够安全。)

 

对对象c的赋值,除了会有类似的结果,还存在对象c原来所占用的内存未被释放,造成内存泄漏。

 

要避免上述情况发生,最简单的方法,是将要调用的函数声明为私有成员。如:

private:

  Test& operator=(Test&) ;   //: Test& operator=(const Test&)

  Test(Test&)                //:  Test(const Test&) 

这两个函数的声明,参数类型用值和引用都可以,加不加const都可以。

 

 

㈢ 自定义“深拷贝”函数:

 

① 对 拷贝构造函数(Test(Test&)),其参数肯定是采用引用而不是值。因为如果传值的话, Test (Test tmp),在构造tmp时,需要调用拷贝构造函数,也就是说会调用自身,因而会限入死循环。参数一般采用常引用,一方面是为了防止引用的对象被修改,另一方面因为常引用可以引用临时变量(比如进行隐式转换时生成的临时变量),而引用则不行,举个简单的例子:

void ff(std::string& other);        // 不允许 ff("hi"); 方式调用

void gg(const std::string& other);  // 可以   gg("hi");

因为调用时会执行std::string tmp("hi")生成一个临时对象,然后才是对这个对象进行引用。

 

 

Test::Test(const Test& other):arr(new int[other.sz]), sz(other.sz)

{

    std::copy(other.arr, other.arr + sz, arr);

}

 

② 对赋值函数,则要特别注意两点:检查是否对自身赋值;在出现异常时,要保证原来的对象不被修改。另外,operator=最好返回引用,这样对x=y=z,可以避免额外的临时对象产生。

 

Test& Test::operator=(const Test &other)

{

//先分配新内存,再释放旧内存,保证异常安全   

  int *tmp = new int[other.sz]; 

  std::copy(other.arr, other.arr + other.sz, tmp);

  delete [] arr;

  arr = tmp;

  sz = other.sz;

  return *this;

}

 

另外一种保证异常安全的做法是:先用other构造一个临时对象tmp,再将tmp的成员和原来的交换。

Test& Test::operator=(const Test &other)

{

  Test tmp(other);

  swap (tmp);   //swap为自定义的交换成员函数,并且不抛出异常

  return *this;

}

 

也可以写做:

Test& Test::operator=(Test other)

{

  swap (other);   //swap为自定义的交换成员函数,并且不抛出异常

  return *this;

}

 

这样写不仅代码更简洁,而且性能可能更好,在传入一个临时对象(比如说某个函数的返回值),少了一次拷贝构造函数的调用和一次析构函数的调用。

 

swap函数的实现可以采用:

void Test::swap(Test& other) throw()

{

  std::swap(arr, other.arr);

  std::swap(sz, other.sz);

}

 

第二种方法,先构造一个临时对象再交换,看似比第一种方法麻烦不少,但更易于维护:资源都是在构造函数内分配,在析构函数内回收。

 

自定义一个swap函数不仅方便两个对象间的数据交换,而且可以快速的释放某个对象所占用的内存(新建一个临时对象再与其交换即可,对vector等容器推荐使用交换的方法来释放所占用的内存。)

 

上面的交换函数用到了标准库的swap函数,该函数是内联的。调用库函数而不是自己手写的代码,一方面使代码更简洁,另一方面可以给编译器更多的优化空间,在经过编译器优化后,甚至比手写的代码性能要高。(比如说开启C++ 0x后交换两个类对象,可以采用std::move。再比如说,intelCPU有一个xchg指令可以直接交换两个变量,编译器可以(当然,这只是可以)针对这个指令进行优化。)

 

 

㈣ 类中指针指向自定义类型时的“深拷贝”函数

 

如果类A中的指针是指向自定义类B的话,则深度拷贝函数可以直接调用类B对应的拷贝函数。

 

class Test2 {

  Test *test;

 

 public:

  Test2(): test(new Test) { }

  Test2(const Test2& other): test(new Test(*other.test)) { }

  Test2 &operator=(const Test2& other) { *test = *other.test; return *this; }

  ~Test2() { delete test; }

};