深拷贝从“new”说起
来源:互联网 发布:苹果ai软件下载 编辑:程序博客网 时间:2024/05/18 18:18
在设计类时,有时必须提供“深拷贝”函数。一个常见的情形就是需要在类中动态管理内存。一个常见的错误就是误以为C++的new会与C的malloc一样,在申请内存失败后,返回空指针,于是就对返回值进行判断以确定下一步的操作。
㈠ new的基本知识:
先说说new操作符:new有三种:new、operator new和placement new。 new是通过调用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操作,可能会造成程序崩溃,甚至更严重。(这与new和delete的具体实现相关,有的实现版本是将分配好的内存信息存放在一个表中,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。再比如说,intel的CPU有一个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; }
};
- 深拷贝从“new”说起
- 从 +new Date 说起,Javascript的一元操作符
- c++的默认拷贝构造函数,从深度拷贝和浅拷贝说起
- 从中国平安收购深发展说起
- 从Object_oriented 说起
- 从AFX_MANAGE_STATE(AfxGetStaticModuleState())说起
- 从“芙蓉姐姐”现象说起
- 不知从何说起
- 从jira说起
- 从AFX_MANAGE_STATE(GetStaticModuleState())说起
- 从辞职说起
- 从"爱因斯坦圆"说起
- 从PDCA说起
- 从被点到说起......
- 从大乘“六度法”说起
- 从AFX_MANAGE_STATE(AfxGetStaticModuleState())说起
- 从Proxy.newProxyInstance说起
- 从中医经络说起
- 端午假期结束了
- Oracle优化方式之RBO&CBO
- Oracle重建索引
- PCTFree&PCTUsed
- 静夜思
- 深拷贝从“new”说起
- 编程思想
- gdgd
- .NET/Framework/CLR/C#/VS 版本一览
- 简单的文件查看编辑器
- Eclipse添加注释的快捷键alt+shift+j,在菜单中是source->generate element comment
- DisplayTag应用所需包,css文件及使用指南(转载)
- 来站感言
- staic 方法在继承时重写的一点认识