Effective C++ 读书笔记(二)
2、 构造/析构/赋值运算
条款05 :了解C++默认编写并调用哪些函数
空类经过编译器处理后会有默认构造函数、复制构造函数、赋值操作符和析构函数。这些函数都是public且inline
- 默认构造函数,由它来调用基类和non-static成员变量的构造函数
- 析构函数是否是虚函数,继承基类,如果没基类,那么默认是non-virtual,析构函数会调用基类和non-static成员变量的析构函数。
- 复制构造函数和赋值操作符中,给成员变量初始化或赋值,会调用成员变量的赋值构造函数和赋值操作符。他们都是浅拷贝
- 赋值操作符,有些情况下编译器是不会合成的,例如
- 两个成员变量,一个是引用:初始化后不能更改,一个是常量:也是初始化后不能更改,因此不可以用赋值更改变量,此时编译器不会合成
- 基类的赋值操作是private的,派生类不会生成赋值运算符
条款 06 :若不想使用编译器自动生成的函数,就该明确拒绝
-
房子是个类,天下没有一样的房子,所以拷贝与赋值都不能使用,将其设置为私有(只声明不定义)就可阻止使用这两个函数
注意:普通调用会在编译阶段出错(private),友元和成员函数可以访问错误会发生在链接阶段(没有定义),错误出现越早越好,可以用继承来实现
class Uncopyable{ { protected: Uncopyable(){} ~Uncopyable(){}; private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&);
其他类来继承就行了
这样继承的类中如果生产对应的拷贝与赋值构造函数,就会调用基类对应的函数,会发生编译错误
条款 07 :为多态基类声明为virtual析构函数
-
创建有层次的类时,将基类的析构函数声明为虚函数
原因:当基类指针(引用)指向子类对象时,如果析构对象通过delete 指针的方式,只会调用基类的析构函数,不会调用子类的析构函数。可能会造成内存泄漏
-
但是当一个类不做基类时,不要将析构函数弄成虚函数,因为调用过程中会多一步指针操作,同时对象也多了一个虚函数指针,
-
一个类不含虚函数,不适合做基类,STL中的容器没有虚析构函数,一个类中至少有个虚函数,析构函数才将弄为虚函数
-
一个类含有纯虚函数,抽象类不能被实例化
class AWOV { public: virtual ~AWOV()=0; }; AWOV::~AWOV(){}//这一步是必要的
如果把这个当做是基类,会有问题,析构函数只有声明没有定义,析构函数从派生类到基类的调用时,会发生链接错误。因此需要定义(空定义)
条款 08 :别让异常逃离析构函数
-
析构函数可以抛出异常,但是不建议这么做;例如:
容器销毁会调用析构函数,如果抛出异常,剩下的元素没有被销毁,会造成内存泄漏。如果继续销毁,会存在两个异常,两个异常会导致不明确的行为
-
有时候又必须在析构函数中执行一些动作,这些动作可能会导致异常,如果调用这些动作不成功会抛出异常,使得异常传播。解决方法如下:
-
动作函数抛出错误,就终止程序,调用abort函数
~DBConn()//析构函数关闭连接 { try{ db.close(); } catch(……) { //记录下对close调用的失败 std::abort();//退出 } }
-
吞下这个异常,它会压制某些失败动作的重要信息。比较好的是重新设计接口,使得客户能对可能的异常做出反应。
~DBConn()//析构函数关闭连接 { try{ db.close(); } catch(……) { //记录下对close调用的失败 } }
-
条款 09 : 绝不再构造和析构函数中调用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 时,会输出父类的函数内容,这是因为基类先构造,在基类构造期间,不会下降到派生类去调用派生类的虚函数,所以调用的是基类的虚函数,此时不表现出多态的性质。
解决方法:将父类的那个函数设置成非虚函数,从derived class构造函数传递参数给base class构造函数
#include<iostream> class Transaction{ public: explicit Transaction(const std::string& parameter); void logTransaction(const std::string& parameter)const//no-virtual function { //log the Transaction std::cout<<"This is "<<parameter<<" logTransaction"<<std::endl; } }; Transaction::Transaction(const std::string& parameter) { logTransaction(parameter);//called in Ctor } class BuyTransaction:public Transaction{ public: BuyTransaction() :Transaction(CreatPamameter()) { } private: static std::string CreatPamameter() { return "BuyTransaction"; } }; class SellTransaction:public Transaction{ public: SellTransaction() :Transaction(CreatPamameter()) { } private: static std::string CreatPamameter() { return "SellTransaction"; } }; int main() { BuyTransaction b; SellTransaction s; return 0; }
-
网上很多解释说: 链接
当构造派生类对象时,先调用基类的构造函数,此时派生类还没有被构造出来,所以调用的是基类的虚函数。
而析构时,派生类已经析构掉了,所以基类析构时仍调用的是基类的虚函数。错!
实际上,无论派生类有没有被构造出来,还是已经析构了。在构造、析构函数中一定只会调用本类中的虚函数。 因为在函数进入构造、析构函数时,一定会把虚指针填充为当前类虚表的首地址
条款10 :令operator=返回一个reference to *this
- 为了实现连锁赋值,操作符必须返回一个reference指向操作符左侧的实参。其实,如果operator=不返回一个引用,返回一个临时对象,照样可以实现连锁赋值(但是这个临时对象会调用一个拷贝构造函数)
- 与之类似的有+=、-=等改变左侧操作符的运损,就当做是个协议,我们都去遵守吧
条款11 :在operator=中实现“自我赋值”
-
如果自己管理资源,可能会“在停止使用资源之前意外释放了它”
class Widget { public: Widget& operator=(const Widget& rhs) { delete p;//如果p之前就已经释放掉了,再次释放会被报错 p=new int(ths.p); return *this; } int *p; };
防止以上的方法就是“证同测试”,判断当前判断是不是赋值
class Widget { public: Widget& operator=(const Widget& rhs) { if(this==&rhs)//证同测试 return *this; delete p; p=new int(rhs.p); return *this; } int *p; };
-
还有一个方案是copy与swap技术,用来解决异常安全问题,条款29 详细说明
如果是引用传递
class Widget { public: void swap(const Widget& rhs);//交换rhs和this Widget& operator=(const Widget& rhs) { Widget tmp(rhs);//赋值一份数据 swap(tmp)//交换 return *this;//临时变量会自动销毁 } int *p; };
如果是值传递,则不需要新建临时变量,直接使用函数参数即可
class Widget { public: void swap(const Widget& rhs);//交换rhs和this Widget& operator=(const Widget rhs) { swap(rhs) return *this; } int *p; };
条款 12 : 复制对象时勿忘其每一个部分
- 一旦给类添加变量,自己写的copying函数(拷贝与赋值构造函数)也要修改,因为编译器不会提醒你;
- 在派生类层次中,派生类中的构造函数没有初始化的基类部分是通过默认构造函数初始化的(没有就会报错)但是在赋值操作符中,不会调用基类的默认构造函数。因为赋值操作只是给对象赋值,不是初始化,因此不会调用基类的构造函数
- 赋值操作符与拷贝构造函数不能相互调用,因为拷贝构造函数是构造一个不存在的对象,而操作符是给一个存在的对象重新赋值。消除重复的代码就是写一个init();