class A
{
int i;
};
class B
{
A *p;
public:
B(){p=new A;}
~B(){delete p;}
/*
B(const B &temp)
{
p=temp.p;
}
*/
};
void sayHello(B x)
{
}
int main()
{
B b;
sayHello(b);
return 0;
}
//程序崩溃!这里的错误原因是编译器在生成默认拷贝构造函数的时候执行了浅拷贝。上面被注释掉的程序就是编译器自动添加的部分。从而导致在sayHello中向x传递值时,调用了执行浅拷贝的默认拷贝构造函数使得x对象和b对象中的值完全一致,包括p指针的值,在x离开作用域(也就是sayHello函数结束),x发生析构,调用delete销毁了指针p,同时在main函数结束的时候,析构b时又会调用一次delete删除指针p。也就是本程序会delete一个已经被delete的指针。可以做如下改进,来修复程序:
class B
{
A *p;
public:
B(){p=new A;}
~B(){delete p;}
B(const B &temp)
{
p=new A;
*p=*(temp.p);
}
};
拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
拷贝构造函数:
1.如果一个构造函数的第一个参数是
自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。(拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果其参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。)
2.什么地方调用?(在我们用=定义变量时会发生;将一个对象作为实参传递给一个非引用类型的形参;从一个返回类型为非引用类型的函数返回一个对象;用花括号列表初始化一个数组中的元素或一个聚合类中的成员。)
拷贝赋值运算符:
1.赋值运算符通常应该返回一个指向其左侧运算对象的引用。
析构函数:
1.没有返回值,也不接受参数。(由于不接受参数,因此它不能被重载。对一个给定类,只会有唯一一个析构函数。)
(重点:三/五法则)
1.需要析构函数的类也需要拷贝和赋值操作。
2.需要拷贝操作的类也需要赋值操作,反之亦然。(例子:考虑一个类为每个对象分配一个独有的、唯一的序号。这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的、独一无二的序号。除此之外,这个拷贝构造函数从给定对象拷贝所有其他数据成员。这个类还需要自定义拷贝赋值运算符来避免将序号赋予目的对象。但是,这个类不需要自定义析构函数。)
(=default)
显示地要求编译器生成合成的版本。
(=delete)
通知编译器(以及我们代码的读者),我们不希望定义这些成员。(不能删除析构函数)
重点:希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为private的。(新标准之前这样做)
对象移动(在对象拷贝后就立即被销毁的情况下,移动而非拷贝对象会大幅度提升性能):
1.右值引用:&&,只能绑定到一个将要销毁的对象。
2.移动构造函数
3.移动赋值运算符