Effective C++ 改善程序与设计的55个具体做法(二)
二、构造/析构/赋值运算
5. 了解C++默默编写并调用哪些函数
- 对于一个类,如果你没声明构造函数,编译器会默认的为他声明一个默认构造函数、一个copy构造函数、一个拷贝赋值函数、以及一个析构函数。对于编译器生成的copy构造和copy赋值函数都只是对来源对象进行一个简单的拷贝。
- 如果你声明了一个有参的一般构造函数,编译器就不会再为他创建默认构造函数.
- 当一个类内含有引用成员时,编译器会拒绝编译默认生成的赋值操作(引用不允许修改了,如下代码所示)。当含有引用变量初始化后又被拷贝赋值,但是编译器是不允许让一个引用值指向不同对象。对于常量也是,常量不允许被修改,编译器不知道如何进行操作。
class NamedObject{
public:
NamedObject(string &_name,int _age){
...//赋值
}
private:
string &name;
const int age;
}
//考虑如下:
string newDog("Persephone");
string oldDog("stach");
NamedObject p(newDog,22);
NamedObject s(oldDog,23);
p = s;
编译器可以暗自为class创建default构造函数,拷贝构造函数,拷贝赋值操作符以及析构函数。
6. 若不想使用编译器自动生成的函数,就该明确拒绝
- 当你不想让你的类支持拷贝构造或者拷贝赋值时,对于其他函数可以通过不声明进行操作,但是对于这两个函数,编译器会自动生成,所以你可以将他们声明在private中。但是尽管如此,友元函数以及成员函数依旧可以调用它们。所以你可以对他们不进行定义来解决这个问题。
- 可以专门设计一个防止拷贝的base类:
class Uncopyable {
public:
Uncopyable() {
} //允许derived对象构造和析构
~Uncopyable() {
}
private:
Uncopyable(const Uncopyable&); //阻止拷贝
Uncopyable& operator=(const Uncopyable&);
为驳回编译器自动暗自提供的机能,可以将相应的成员函数声明为private并且不允实现。像上述base类也是一种做法。
7. 为多态基类声明virtual析构函数
- 当一个继承类对象经由基类指针被删除,而该基类带有一个非虚的析构函数,其结果会导致对象的继承类的成分不会被销毁。虚函数的目的是允许继承类的实现得以客制化(动态多态)。所以最好不要让一个没有虚函数的类做基类。
- 并且对于一个不做基类的类,如果让其带有虚函数也是个不太好的决定。因为如果有虚函数,那编译器会为他多声明一个隐藏变量的虚函数表指针vptr,用于指向当前类的虚函数表,这就会导致该类比原有类要大一点(一个指针大小)。所以原本刚好的一个刚好可以塞进缓存器的类可能会因为这一点多出来的内存无法被塞进去。
- 当一类有虚函数的时候,将其析构函数也设置成虚函数,也不会增加大小,也对于后续的继承友好。
- 拥有纯虚函数的类为抽象类,无法被实例化!当你需要抽象类作为base类,但是又找不到合适的纯虚函数的时候,可以考虑让析构函数变为纯虚函数(你需要为他提供定义)。析构函数的调用方式为最深层的那个派生类的析构函数最先被调用,然后逐个调用每个base类。所以最后调用最终base类时,你需要给这个析构一个定义。
1.带多态性质的base类应该声明一个虚析构函数。如果这个类不带有任何的虚函数,他就应该拥有一个虚析构函数。
2.Classes的设计目的不是作为base classes使用,或不是为了具备多态性,就不该声明虚析构函数。
8. 别让异常逃离析构函数
- 虽然并不禁止在析构函数时突出异常,但是并不鼓励这么做。只要析构吐出异常,即使不是使用容器或者数组,也会由于过早结束产生不明确行为。
- 如果你非要在析构时候,进行异常检测操作,你可以进行如下操作避免不确定行为:
1. abort()操作: 直接结束当前进程,对异常程序直接终止,抢先对不明确行为置于死地。
~DBConnn{
try {
db.close() }
catch (...) {
记录运转记录,记录对close调用失败;
std::abort();
}}
2.吞下因调用close而发生的异常。
~DBConnn{
try {
db.close() }
catch (...) {
记录运转记录,记录对close调用失败;
}}
但是以上两个方法都没法对异常进行反应,只能避免不确定的行为。
- 较好的策略是重新设计DBConn接口。DBConnn可以自己提供一个close函数,并在事先确定是否close的后在进行是否析构。但是如果否,那么问题又回到之前的不明确行为问题。
public:
...
void close(){
db.close();
closed = true;
}
~DBConnn{
if(!closed){
//已经释放,则不用在进行下面的风险行为。
try {
db.close()}
catch (...) {
记录运转记录,记录对close调用失败;}
}
}
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下它们(不传播)或者结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9: 绝不在构造和析构过程中调用virtual函数
- base类构造期间virtual函数对不会下降到继承类阶层。你可以理解为派生类在构造时候,会调用base类的构造函数,此时base类中的虚函数,不被当成虚函数。并且如果他要是纯虚函数,那更问题大了,他要在派生类中实现,此时派生类中的成员尚未被初始化。析构函数也同理,当派生类析构结束后,对象中的派生成员变量就呈现未定义值,进入base类进行析构的时候,就应该把当前类当成base类。
- 侦测构造函数或者析构函数运行期间是或否调用虚函数并不轻松,因此当一个类有多个构造函数的时候,可以将相同的工作形成一个初始化函数如init()中。如果只是虚函数的话,父类构造函数中不会发生多态,这可能会和你想要的结果不一样。
- 解决方法:1.取消其虚函数资格,使其传递正常信息。2.令他为static函数,这样就不会发生base构造期间虚函数无法下降到派生类的问题。
在构造和析构期间不要调用虚函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)
10. 令operator=返回一个reference to *this
- 为了实现连锁赋值,赋值操作符必须返回一个指向操作符的左侧实参的引用。
int x,y,z;
x = y = z;
11 在operator中处理“自我赋值”
w = w;
a[i] = a[j]; //i==j 潜在自我赋值
*px = *py;
- 当有这么一个类,用于保存一个指针指向一个动态内存。
class Bitmap{
...};
class Widget(){
private:
Bitmap * pb;
}
下面的赋值看似很合理:
Widget& operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但是当自我赋值的时候你就会发现问题来了,他在赋值之前,先把自己给delete了,赋值结束后,发现自己指向一个被删除的对象了,这就是问题所在。
- 解决办法可以如下
- 证同测试:
if(this ==&rhs) return *this;
- 虽然上述操作有赋值安全性,但是没有解决异常安全性问题。如当他两确实不相等时,但是delete后,分配空间时导致异常(内存不足等情况),则也会导致指向删除的内存。因此你可以将删除滞后:
Widget& operator=(const Widget& rhs){
Bitmap* pOrig = pb; // 先保存原先的pb
pb = new Bitmap(*rhs.pb); //就算异常,pb也未被删除。
delete pOrig; //删除那块内存内容。
return *this;
}
- copy and swap技术
class Bitmap{
...};
class Widget(){
private:
...
void swap(Widget& rhs); //交换*this 和rhs的数据
...
}
Widget& operator=(const Widget& rhs){
Widget temp(rhs); //为rhs数据制作一份附件
swap(temp); //将*this数据和上述附件的数据交换
return *this; //结束调用析构函数自动删除。
}
1.确保当对象自我赋值时operator=有良好行为。其中技术包括比较来源对象和目标对象的地址、精心周到的语句顺序(copy后在delete)、copy and swap。
2.确定任何函数如果操作一个以上对象,而其中多个对象是同一个对象时,其行为仍然正确。
12 复制对象时无忘其每一个成分
- 当你完成一个类的时候,你的拷贝构造函数已经写好了,但是当你突然给该类新增一个成员变量的时候,你的拷贝构造函数或者赋值函数并不会报错。但是事实上这不是你要的结果。或者如下情况:
class son:public father{
public:
son(const son& rhs):p(rhs.p){
}
son& operator=(const operator& rhs){
p = rhs.p;
return *this;
}
private:
int p;
}
上述代码看似没有问题,但是当你完成是,你会发现,父类的信息并没有被拷贝过去,因为你只考虑了子类中的成员的拷贝,rhs中父类的信息你没有考虑,父类则只调用了无参的默认构造参数进行拷贝(如果没有默认怎会报错)。你需要把父类也考虑进去。
class son:public father{
public:
son(const son& rhs):p(rhs.p),father(rhs){
}
son& operator=(const operator& rhs){
father::operator=(rhs); //对父类进行复制
p = rhs.p;
return *this;
}
private:
int p;
}
- 你可能为了代码的不重复,觉得拷贝构造和拷贝复制的代码似乎类似,你就想在拷贝赋值中调用拷贝构造或者相反操作。这问题很大,构造函数用来初始化,而赋值函数用于作用于已初始化对象上。在一个已初始化的对象做初始化事情,没有意义,相反也是。但是你可以尝试将两者相同的代码放入一个函数内(init())。通过调用这个函数实现代码的缩减。
- 拷贝函数应该确保复制“对象内所有成员变量”以及“所有base class 成分”。
- 不要尝试以某个拷贝函数实现另一个拷贝函数。应该将共同技能放入第三个函数中,并由两个coping函数共同调用。