【Effective C++】构造/析构/赋值运算
几乎你写的每一个class都会有一个或多个构造函数、一个析构函数,一个copy assignment操作符。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及你的整个classes。所以确保它们行为正确时生死攸关的大事。本章提供的引导可让你把这些函数良好地集结在一起,形成classes的脊柱。
条款05:了解C++默默编写并调用哪些函数
如果你自己没有声明,编译器就会声明
- 默认构造函数
- copy构造函数 //单纯地将来源对象的每一个non-static成员变量拷贝到目标对象
- copy assignment操作符 //同上
- 析构函数 //是个non-virtual
唯有这些函数被需要(被调用),它们才会被编译器创建出来,下面代码造成上述每一个函数被编译器产出:
Empty e1; //默认构造函数 //析构函数 Empty e2(e1); //拷贝构造函数 e2=e1; //copy assignment操作符
注意:
如果生成的代码不合法或者没有意义时,编译器会拒绝为class生成operate=:
(1)不合法
#include <string> #include <iostream> using namespace std; class Dog { public: Dog(string& namevalue, int agevalue):name(namevalue),age(agevalue){ //name = namevalue; 当需要初始化const修饰的类成员/引用成员数据应使用成员初始化列表 //age = agevalue; } void show() { cout << name << " is " << age << " years old." << endl; } private: string& name; const int age; }; int main() { string s1("Persephone"); string s2("Satch"); Dog d1(s1, 2); Dog d2(s2, 36); //Dog d1("Persephone", 2); //Dog d2("Satch", 36); //d1 = d2;//无法引用 函数 "Dog::operator=(const Dog &)" (已隐式声明) -- 它是已删除的函数
d1.show();
d2.show();
getchar();
}
由于C++不允许“让引用改指向不同的对象”,编译器没有自动生成operate=,d1=d2就会报错。
(2)没有意义
如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。毕竟编译器为derived classes所生的copy assignment操作想象中可以处理base class成分(条款12)。
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
有些类,你不想它的对象被拷贝,但如果你不声明拷贝构造函数,编译器会自动给你生成。这时候我们可以,
1)将拷贝构造函数和copy assignment操作符声明为private(此时member函数和friend函数还是可以调用你的private函数),并且不去定义它。
2)写一个base class,它有私有的拷贝构造函数和copy assignment操作符,然后去继承它,根据上一条,编译器将拒绝为其生成一个copy assignment操作符。(可能会导致多重继承
3)使用Boost提供的版本。(还没学到
条款07:为多态基类声明virtual析构函数
比如在使用工厂函数时,工厂函数会返回一个base class指针,指向新生成的derived对象。被返回的对象位于heap,因此为了避免泄露内存和其他资源,需要delete该对象。
BasicCamera* pb = CreateCamera(); ... delete pb;
指针指向的时子类对象,但却经由一个base class指针来删除,而目前的base class有一个non-virtual析构函数(条款05指出,编译器自动生成的析构函数是non-virtual的)。这会引来灾难,因为实际执行时,通常发生的是,对象的derived成分没被销毁,base class成分通常会被销毁,造成一个局部销毁的对象。
解决方法:
给base class一个virtual析构函数
注意:
- 任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
- 如果class不含virtual函数,通常表示它不意图被用作一个base class,此时令其析构函数为virtual,会导致其对象的体积增大,不能传递至其他语言的函数。
- 总而言之,只有当class内含至少一个virtual函数,才为它声明virtual析构函数
- 有时候抽象类不想被实体化,比如说BasicCamera创建对象没有意义,你可以有一个pure virtual函数,这时候可以声明一个pure virtual的析构函数。
class BasicCamera { public: virtual ~BasicCamera() = 0; };
加上virtual,加上“=0”,就成为pure virtual函数了,但是书上说“必须为这个pure virtual析构函数提供一份定义”???由子类继承后定义可以吗?
试了一下,好像确实如此。
#include<iostream> using namespace std; class BasicCamera { public: virtual ~BasicCamera()=0;//如果此处为~BasicCamera(),只会调用父类的析构 }; BasicCamera::~BasicCamera() { cout << "调用了BasicCamera的析构函数" << endl; }//不加会报错,必须要定义 class Hik :public BasicCamera { public: ~Hik() { cout << "调用了hik的析构函数" << endl; } }; class Factory { public: BasicCamera* CreateCamera() { return new Hik(); } }; int main() { Factory fac = Factory(); BasicCamera* camera = fac.CreateCamera(); delete camera; getchar(); return 0; }
运行结果:
可以看出,析构函数的运作方式是,最深层派生的那个class(此处为子类hik)的那个析构函数最先被调用,然后是每一个base class的析构函数被调用。编译器会在子类的析构函数中调用父类的析构函数,所以必须提供一份定义。
条款08:别让异常逃离析构函数
如果析构函数吐出异常程序可能过早结束或出现不明确行为。比如,HikCamera类在析构时抛出异常:
#include<iostream> using namespace std; class HikCamera { public: ~HikCamera() { throw 1; closed = true; } private: bool closed = false; }; int main() { { HikCamera cam; } getchar(); return 0; }
上图可以看出,调用了abort函数终止了程序。但如果析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?
1. 如果抛出异常就结束程序(通常通过abort完成):
~HikCamera() { try { throw 1; closed = true; } catch (int i) { abort(); } }
如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用abort可以抢先置“不明确行为”于死地。
2.吞下因调用close而引发的异常
~HikCamera() { try { throw 1; closed = true;} catch (int i) { cout << "close函数中有异常" << endl; } }
3. 将异常放在析构函数之外
提供一个close函数,赋予客户机会对可能出现的问题作出反应。同时可以在析构函数中追踪,由析构函数关闭之。
class HikCamera { public: void close() { throw 1; closed = true; } ~HikCamera() { if(!closed){ try { close(); } catch (int i) { cout << "close函数中有异常" << endl; }} } private: bool closed = false; };
条款09:绝不在构造和析构过程中调用virtual函数
假如在构造函数中调用了virtual函数:
class BasicCamera { public: BasicCamera() { open();//调用了virtual函数 } virtual void open() { cout << "打开了相机" << endl; } }; class HikCamera:BasicCamera{ public: HikCamera() { cout << "创建了hik相机" << endl; } void open() { cout << "打开了海康相机" << endl; } };
运行结果:
在创建子类对象时,不会创建父类对象,只是初始化子类中属于父类的成员。父类的构造函数会被调用,运行父类构造函数时,里面的父类版本的virtual函数被调用了,不是子类中的版本。
原因
1. 当基类的构造函数执行时,子类的成员变量尚未初始化,如果virtual函数(open函数)调用了子类成员变量,这会导致不明确行为。
2. 基类构造期间,对象类型为基类。不只是virtual函数被编译器解析至基类,若使用运行期类型信息,也会把对象视为基类。
同样的道理也适用于析构函数。一旦子类的析构函数开始执行,子类成员变量就会呈现未定义值,进入基类析构函数后对象成为基类对象。
想要确保每一次有子类被建立就调用对应的open(),
解决方法
把父类的virtual函数改为不virtual的,然后子类的构造函数传递必要信息给父类的构造函数,父类的构造函数就可以调用non-virtual的open()了。像这样:
#include<iostream> using namespace std; class BasicCamera { public: BasicCamera(const string& info) { open(info); } void open(const string& info) { cout << "打开了" << info << "相机" << endl; }; }; class HikCamera:BasicCamera{ public: HikCamera(string s):BasicCamera(s) { } }; int main() { HikCamera cam("Hik"); getchar(); return 0; }
换句话说,无法使用virtual函数从基类向下调用,在构造期间,你可以“令子类将必要的构造信息向上传递至基类构造函数”。
条款10:令operate=返回一个reference to *this
关于赋值,可以将它们写成连锁的形式:
int x, y, z; x = y = z = 15; //赋值连锁形式
因为赋值所采用的是右结合律,因此,上面的连锁赋值可以解析为:
x = (y = (z = 15));
为了实现这样的“连锁赋值”,赋值操作符必须返回一个reference,指向操作符的左侧的实参,这也是为classes实现赋值操作符是应该遵守的协议:
class Widget { public: ... Widget& operator=(const Widget& rhs) //返回类型是一个reference,指向当前的对象 { ... return* this; //返回左侧的对象 } ... };
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算。例如:+=,*=,-=等。需要注意的是,这只是一个协议,并不是强制的。但是,最好还是这样做,因为这份协议被所有的内置类型和标准程序库提供的类型如string,vector,complex,tr1::shared_ptr或即将提供的类型所共同遵守的。
条款11:在operate=中处理“自我赋值”
自我赋值
“自我赋值”发生在对象被赋值给自己时:
class Widget{ ... }; Widget w; ... w = w; //赋值给自己
看着有些愚蠢,但它合法,不要以为大家绝对不这么做。以下为某些不明显“自我赋值”产生的情况:
(1)指针/引用指向同一个对象
a[i] = a[j]; //当i = j时 *px = *py // 当两个指针指向同一个东西时
(2)父类的指针和子类的指针指向同一个对象
class Base { ... }; class Derived: public Base { ... }; void doSomething(const Base& rb, Derived* pd); //rb和pd可能是同一个对象
自我赋值的后果
在我们打算自行管理资源时,自我赋值的情况可能会使我们掉进“在停止使用资源之前意外释放了它”的陷阱。假设建立一个class,来保存一个指针指向一块动态分配的位图(bitmap):
class Bitmap{ ... }; class Widget{ ... private: Bitmap* pb; //指针,指向一个从heap分配而得的对象 };
下面是operate=的实现代码,
Widget& operate=(const Widget& rhs){ delete pb; //停止使用当前的bitmap pb = new Bitmap(rhs.pb); //使用rhs‘s bitmap的副本(不new的话,就指向一个bitmap了 return *this }
表面合理,但自我赋值出现时不安全:
Widget rhs; Widget w = rhs; //这是ok的 rhs = rhs; //第一步,将rhs.pb指向的对象删掉
解决方法
想要阻止这种做法,有以下方法:
(1)增加“证同测试”
Widget& operate=(const Widget& rhs){ if (this == &rhs) return *this; //如果是自我赋值,不要做任何事 delete pb; pb = new Bitmap(rhs.pb); return *this }
这样做行得通,具备了“自我赋值安全性”,但是还不具备“异常安全性”。如果”new Bitmap“导致了异常,Widget最终还是有一个指针指向一块被删除的Bitmap。这样的指针有害,你无法安全地删除它们,甚至无法安全地读取它们。
(2)在复制pb所指东西之前不要删除pb:
Widget& operate=(const Widget& rhs){ Bitmap* pOrig = pb; //pb和pOrig指向一个对象 pb = new Bitmap(rhs.pb); //pb指向rhs.pb的副本 delete pOrig ; //利用pOrig删去旧对象 return *this }
同时具备了“自我赋值安全性”和“异常安全性”。
(3)copy and swap技术
swap(Widget& rhs){ ... } //交换*this和rhs的数据 Widget& operate=(const Widget& rhs){ Widget temp(rhs); //为rhs的数据做一个复件 swap(temp); //将*this数据和上述复件的数据交换 return *this }
如果创建指针的话,需要delete掉,但是创建对象的话,析构函数会delete掉指针。temp拷贝rhs的成功说明new Bitmap没有异常,然后swap。
更激进版:
swap(Widget& rhs){ ... } //交换*this和rhs的数据 Widget& operate=(Widget rhs){ swap(rhs); //将*this数据和上述复件的数据交换 return *this }
传入值时,会自动生成复件。此时的rhs就是原rhs的拷贝。
条款12:复制对象时勿忘其每一个成分
copy构造函数和copy assignment操作符我们称之为copying函数,编译器会自动为我们的classes创建copying函数,将拷贝对象的所有成员变量都做一份拷贝。如果你声明自己的copying函数,可能会导致:
1. 如果你漏了一个成员变量没有复制,大多数编译器不会告诉你
2. 继承时,子类的copying函数只复制了子类的成员,而父类的成员变量会被默认的构造函数初始化
PriorityCustomer的copying函数没有复制Customer成员变量,PriorityCustomer的copy构造函数并没有指定实参传给其base class构造函数,因此PriorityCustomer对象的Customer成分会被不带实参的父类default构造函数初始化(上面的是伪代码,父类必须要有默认构造函数)。父类的default构造函数将对name和lastTransaction执行缺省的初始化动作。
base class 的成分往往是private, 所以你无法直接访问他们,你应该让derived class的copying函数调用相应的base class函数:
#include<iostream> using namespace std;
//--------父类-------------------------- class BasicCamera { public: BasicCamera() {}//父类必须要又默认构造函数,如果子类没有在拷贝构造的过程拷贝父类成员,那么子类会调用父类的默认构造函数,对父类的成员变量执行缺省的初始化。 BasicCamera(const BasicCamera& rhs) { name = rhs.name; } BasicCamera& operator=(const BasicCamera& rhs) {} void setname(string s) { name = s; } void show() { cout << "name is " << name << endl; } protected: private: std::string name; };
//---------------子类-------------- class HikCamera : public BasicCamera { public: HikCamera() {}; HikCamera(const HikCamera& rhs) :BasicCamera(rhs), price(rhs.price) { };//调用基类的copy构造函数 HikCamera& operator=(const HikCamera& rhs) { BasicCamera::operator =(rhs);//对基类成分进行赋值 price = rhs.price; return *this; }; void set(int x, string s) { BasicCamera::setname(s); price = x; } void show() { BasicCamera::show(); cout << "price is " << price << endl; } protected: private: int price; }; int main() { HikCamera cam; string s = "hik"; cam.set(100, s); HikCamera cam1(cam); cam1.show(); getchar(); }
运行结果
如果没有BasicCamera(rhs),运行结果:
总结:
当你编写一个copying函数,请确保
1.复制所有的local成员变量,
2.调用所有的base class内的适当的copying函数。
当这两个copying函数有近似相同的实现本体,令一个copying函数调用另一个copying函数无法让你达到你想要的目标。
总结:
05:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数。
08:析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
08:如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非析构函数中)执行该操作。
10:令赋值操作符返回一个reference to *this。
确保当对象自我赋值时operate=有良好行为,其中技术包括比较“来源对象”和目标对象“的地址、精心周到的语句顺序、以及cpoy and swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12:Copying函数应该确保复制“对象内的所有成员变量”及”所有base class成分“。
12:不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
参考:
1. 《Effective C++》P34-60