Chapter 2 构造,构析,赋值
Item 5 知道C++自动生成和调用什么
编译器可能会隐式生成一个类的初始构造函数,拷贝构造函数,拷贝赋值函数和构析函数。
Item 6 明令禁止你不想要编译器生成的函数
书中介绍了如何避免类被拷贝的两种方法:将其拷贝构造及赋值函数声明为private(无法防止friend函数调用), 或者将类继承自一个Uncopyable类,实际上现在的C++11直接把不想要的函数声明为delete就行了。
Item 7 在多态体系中声明虚构析函数
也是常识了,直接说明不在继承体系里把基类构析函数声明为virtual
的后果:
#include<iostream>
#include<vector>
using namespace std;
class Person{
public:
Person(){ cout << "Person constructed" << endl; };
~Person(){ cout << "Person destroyed" << endl; };
private:
string name;
string address;
};
class Student : public Person{
public:
Student(){ cout << "Student constructed" << endl; };
~Student(){ cout << "Student destroyed" << endl; };
private:
string schoolName;
string schoolAddress;
};
int main(){
Person *stu = new Student();
delete stu;
}
Person constructed
Student constructed
// 派生类没销毁!!
Person destroyed
Item 8 构析时检查异常
为了避免异常导致内存泄漏,建议在类的构析函数中做异常处理(书中以一个数据库类设计为例,但我想一般情况下用不到。。)
Item 9 别在构造或析构时调用虚函数
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)
换句话说:在构造函数和析构函数中,virtual函数失去多态性。
试想一下,假设此时在构造函数和析构函数中,virtual函数没有失去多态性,会出现什么问题。我们知道构造次序吧,先构造base类部分,再构造derived部分。
那么在base类构造过程中,derived的参数都没有被初始化。virtual函数一旦具有多态性,调用的就是派生类的函数,那么它使用的派生类中数据成员将是未初始化的,必然导致未定义行为。
所以,一句话。在构造函数和析构函数中,virtual函数和普通函数没任何区别。
举个例子:
class Transaction{
public:
Transaction();
virtual void logTransaction()const//virtual function
{
//log the Transaction
std::cout<<"This is Transaction logTransaction"<<std::endl;
}
};
Transaction::Transaction()
{
logTransaction();//called in Ctor
}
class BuyTransaction:public Transaction{
public:
virtual void logTransaction()const
{
std::cout<<"This is BuyTransaction logTransaction"<<std::endl;
}
};
class SellTransaction:public Transaction{
public:
virtual void logTransaction()const
{
std::cout<<"This is SellTransaction logTransaction"<<std::endl;
}
};
BuyTransaction b;
输出结果是:This is Transaction logTransaction
不是想要的This is BuyTransaction logTransaction。
在构造BuyTransaction对象b的时候,首先调用BuyTransaction类的构造函数(构造函数执行顺序请看这里:【关于基类和派生类的构造函数和析构函数的执行顺序问题】),在这个构造函数中,再调用基类Transaction的构造函数,在这里调用了logTransaction()函数。这样就输出了所显示的内容。在基类构造期间,不会下降到派生类去调用派生类的虚函数。
base class的构造函数先于derived class的构造函数。在base class构造函数期间,derived class的对象还没有构建,如果derived class的virtual用到了local变量,这时如果真的调用了derived class的virtual函数,会使用为初始化的变量,会有不明确的行为。所以C++不让你走这条路。
还有一个理由就是,在base class构造期间,对象类型是base class,不是derived class。virtual函数会被编译器解析(resolve to)base class。如果使用了运行期类型信息(runtime type information),编译器也会把它视为base class类型。
相同的道理同样适用于析构函数。析构过程和构造过程相反。先析构派生类部分,再析构基类部分。析构到基类时,派生类中的变量就是为初始化的,对象类型是基类类型。
Item 10 重载赋值运算符来返回this
Item 11 在=运算符函数中处理自赋值问题
- 设计operator=函数时要确保其exception-safe 和 self-assignment-safe,技巧包括比较赋值参数和this的地址,谨慎检查语句顺序和copy and swap:
class Bitmap { ... };
class Widget { ...
private:
Bitmap *pb;
};
比较赋值参数以处理自赋值情况:
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs)
return *this;
delete pb;
pb = new Bitmap(*rhs.pb); // 缺陷:Bitmap的构造可能会发生异常,造成指针悬挂
return *this;
}
确保已经赋值成功了再删除旧指针:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap *pOrig = pb; //pOrig指向原来的BitMap
pb = new Bitmap(*rhs.pb); //pb指向rhs的BitMap
delete pOrig; //若new成功,删除原来的BitMap
return *this;
}
copy and swap:
class Widget { ...
void swap(Widget& rhs); ...
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
Item 12 要拷贝就拷贝对象的一切
如果你自己设计类的拷贝构造及拷贝赋值函数,编译器是不会告诉你有没有完全拷贝类的数据的,因此设计时就要检查拷贝函数是否将所有数据成员拷贝过去,这点在设计多态时更加重要,记住设计派生类的构造函数和拷贝函数时也要注意构造(拷贝)其基类部分。
另外,如果拷贝构造函数和拷贝赋值函数的代码有冗余,不要尝试用一个拷贝函数调用另一个拷贝函数的方法来避免冗余代码,而是将公共实现部分封装为额外的函数。