Effective C++ 读书笔记(2.构造、析构、赋值运算)

条款05:了解C++默认编写并调用哪些函数

  • 编译器会为一个空类声明:copy构造函数copy assignment操作符和一个析构函数,如果你没有声明任何构造函数则编译器会为你声明一个default构造函数
class Empty{
     };

class Empty{
     
public:
	Empty(){
     }
	~Empty(){
     }
	Empty(const Empty& rhs){
     }

	Empty& operator=(const Empty& rhs){
     }
};
  1. default构造函数和析构函数(非虚函数):放置幕后代码,像是调用基类或者成员变量的构造和析构函数。
  2. copy构造函数和copy assignment操作符,单纯的将每一个non-static成员变量拷贝到目标对象。
  3. 对内含const、reference成员的classes需要自己定义copy assignment。
template <class T>
class NamedObject{
     
public:
	//不再接收一个const名称,因为nameValue现在是一个 reference-to-non-const string
	NamedObeject(std::string& name,const T& value);
	...
private:
	std::string& nameValue;
	const T objectValue;
}

std::string newDog("Persephone");
std::string oldDog("Stach");

NamedObject<int>p(newDog,2);
NamedObject<int>s(oldDog,36);
p=s;
//p对象中的引用会指向s中的引用吗?c++并不允许引用改指不同的对象,编译器拒绝编译

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

  • 问题:如果有特殊需要,需要禁止copy函数和copy assignment操作符,如果你不在类中声明它们,那么编译器会为你声明它们。
  1. 解决1:因为所有编译器产出的函数都是public,所以我们直接将copy函数和copy assignment声明为private成员,阻止编译器创建自己的版本。问题:member函数和friend函数拷贝这个对象那么就会出问题。

  2. 解决2:同样是声明在private中,但是用一个专门为了阻止copying动作而设计的base class。为了阻止一个类对象被拷贝,可以继承这一个bass class。

    class Uncopyable{
           
    protected:
    	Uncopyable(){
           }
    	~Uncopyable(){
           }//允许derived对象构造和析构
    private:
    	Uncopyable(const Uncopyable&);//但阻止copy
    	Uncopyable& operator=(const Uncopyable&);
    }
    
    class HomeForSale:private Uncopyable{
           
    	...
    }
    

    ==总结:==为了驳回编译器自动提供的机能,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

条款07:为多态基类声明virtual析构函数

  • 设计一个TimeKeeper base class和一些derived classes作为不同的计时方法:

    class TimeKeeper{
           
    public:
    	TimeKeeper();
    	~TimeKeeper();
    	...
    }
    
    class AtomicClock:public TimeKeeper{
           };
    class WaterClock:public TimeKeeper{
           };
    class WristWatch:public TimeKeeper{
           };
    
  • 可以设计Factory函数,返回指针指向一个计时对象。

    TimeKeeper* getTimeKeeper();//返回一个指针,指向派生类的动态分配对象
    TimeKeeper* ptk=getTimeKeeper();//从基类继承体系,获得一个动态分配对象
    ...//运用它
    delete ptk;//释放它避免资源泄露
    

    注意!!!
    getTimeKeeper返回的指针指向派生类,但是删除这个类却通过一个基类指针,而这个基类指针只有non-virtual析构函数。这就导致了delete后调用基类的析构函数来析构派生类,那么结果就是只能析构派生类中bass class成分,而drived class的部分则得不到释放,导致内存泄漏。
    解决方法
    给bass class一个virtual析构函数。

    class TimeKeeper{
           
    public:
    	TimeKeeper();
    	virtual ~TimeKeeper();
    	...
    }
    TimeKeeper* ptk=getTimeKeeper();
    ...//运用它
    delete ptk;//释放它避免资源泄露
    
  • 任何class只要带有virtual函数几乎确定也应该拥有一个virtual析构函数。反之,如果一个class没有virtual函数,通常表示这个class不被企图当做base class,令其析构函数为virtual往往是一个馊主意。实现virtual往往需要vptr(指向一个由函数指针构成的数组)即vtbl,增加对象大小达到50%~100%。

  • 有时候令class带上一个pure virtual析构函数会颇为便利,导致abstract classes(不能被实体化)==必须为这个pure virtual析构函数提供一份定义。

    class AWOV{
           
    publicvirtual ~AWOV()=0;
    }
    
    AWOV::~AWOV(){
           }
    

总结
1. polymorphic(带有多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,那么他就应该拥有一个virtual析构函数。
2. classes的设计目的如果不是为了作为一个base classes使用,或者不是为了具备多态性,就不该声明virtual析构函数。

条款08:别让异常逃离析构函数

问题
假设一个class中的析构函数会吐出异常,且定义一个容器实例v存放这个class的许多实例,当销毁这个v时就会调用多次析构函数,如果抛出多个异常程序可能会过早结束或者出现不明确行为。

  • 例子:使用一个class负责数据库连接
    class DBConnection{
           
    public:
    	...
    	static DBConnection create();//返回对象
    
    	void close();//关闭联机,失败则抛出异常
    }
    
    为了使用户不忘记在DBconnection中调用close,一个合理的想法就是创建一个用来管理DBconnection的class,在这个class的析构函数中调用DBConnection::close()函数。
    class DBConn{
           
    public:
    	...
    	~DBConn()
    	{
           
    		db.close();
    	}
    	
    private:
    	DBConnection db;
    }
    
    //这样客户就可以写出这样的代码
    {
           //开启一个区块。
    	DBConn dbc(DBConnection::create());//建立DBConnection对象并交给DBConn对象以便管理,通过dbc结构管理DBConnection对象,在区块结束的时候DBConn自动销毁,因而自动为DBConnection对象调用close
    	...
    }
    
    如果:调用close函数异常那么DBConn的析构函数就会传播这个异常,允许异常抛出析构函数。
    解决办法
  1. close抛出异常就结束程序,用abort阻止异常传播。
    DBConn::~DEConn()
    {
           
    	try(db.close();)
    	catch(...){
           
    		std::abort();
    }
    }
    
  2. 吞下因调用close而发生的异常
    DBConn::~DEConn()
    {
           
    	try(db.close();)
    	catch(...){
           
    }
    }
    
    以上两种方法都无法对“导致close抛出异常”的情况做出反应。
  3. 重新设计DBConn的接口
    class DBConn{
           
    public:
    	void close()//供客户使用的新函数
    	{
           
    		db.close();
    		closed=true;
    	}
    	~DBConn()
    	{
           
    		if(!closed)
    		{
           
    			try(db.close())//关闭连接(如果没有调用close的话)
    			catch(...){
           
    			//制作运转记录,记录下对close的调用失败;
    			...
    			}
    		}
    	}
    private:
    	DBConnection db;
    	bool closed;
    }
    

总结

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下(不传播)或结束程序。
  2. 如果客户需要对某个操作函数运行期间抛出的异常作反应,那么class应该提供一个普通函数(而不是在析构中)执行该操作。

条款09:决不在构造函数和析构过程中调用virtual函数

例子
假设你有一个class继承体系,用来模拟股市交易,买入和卖出一定要有审计,每当创建一个交易对象在审计日记中都需要有记录。

class Transaction{
     
public:
	Transaction();
	virtual void logTransaction() const =0;//做出一份因类型不同而不同的日志记录,纯虚函数
}
Transaction::Transaction()
{
     
	...
	logTransaction();
}
class BuyTransaction:public Transaction{
     
public:
	virtual void logTransaction() const;
	...
}
class SellTransaction:public Transaction{
     
public:
	virtual void logTransaction() const;
	...
}

现在,当执行以下操作时会发生什么事情?

BuyTransaction b;
  1. 派生类需要先调用基类的构造函数完成派生类中的基类成分的初始化。而基类Transcation的构造函数最后一行调用logTransaction则会引发问题,这时候调用的时基类中的纯虚函数而不是派生类中用来覆盖这个纯虚函数的版本。
  2. 调用析构函数也是如此,一旦析构函数被调用,那么对象内的派生类成员变量首先呈现未定义值,进入基类的析构函数,这时候c++就视这个对象为基类对象。
  3. 通常上述例子很容易被编译器发出警告
  • 编译器不总能够检测出构造函数或者析构函数是否调用virtual函数:
    如果Transcation有多个构造函数,每个都需要执行某些相同的工作,那么避免将代码重复的优秀做法就是把共同的初始化代码(其中包括对logTransaction的调用)放进一个初始化函数中。

    class Transaction{
           
    public:
    	Transaction()
    	{
           init();}//调用non-virtual
    	virtual void logTransaction() const = 0;
    	...
    private:
    	void init()
    	{
           
    		...
    		logTransaction();
    	}
    };
    

上述方法会让编译器和连接器没有任何报警行为。但运行时会调用纯虚函数导致系统中止程序。
问题
那么如何保证每次有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?
解决方法
一种做法是将这个纯虚函数改为non_virtual函数,然后要求派生类的构造函数传递必要的信息给基类构造函数。而后那个构造函数就可以安全的调用logTransaction了

class Transaction{
     
	explicit Transaction(const std::string& logInfo);
	void logTransaction(const std::string& logInfo) const;
	...
}
Transaction::Transaction(const std::string& logInfo)
{
     
	...
	logTransaction(logInfo);
}
class BuyTransaction{
     
public:
	BuyTransaction(parameters):Transaction(createLogString(parameters))//将log信息传递给base class构造函数
	{
     ...}
	...
private:
	static std::string createLogString(parameters);
}

总结
你无法使用virtual函数从基类向下调用,但是在构造期间你可以用派生类将必要的构造信息向上传递至基类的构造函数。但是要注意,必要的构造信息由一个静态成员函数提供,静态成员函数保证了传入的不会是尚未初始化的成员变量

  • 对类的静态成员变量和成员函数作个总结:
    (原文链接:https://blog.csdn.net/MoreWindows/article/details/6721430)
  1. 静态成员函数中不能调用非静态成员。
  2. 非静态成员函数中可以调用静态成员。因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的。
  3. 静态成员变量使用前必须先初始化(如int MyClass::m_nNumber = 0;),否则会在linker时出错。

条款10:令operator= 返回一个reference to *this

  • 对于所有的赋值相关运算,都可以返回一个绑定在*this上的引用。

    class Widget{
           
    public:
    	...
    	Widget& operator=(const Widget&rhs)
    	{
           
    		...
    		return *this;
    	}
    };
    

条款11:在operator=中处理“自我赋值”

问题
当对象被赋值给自己的时候,自我赋值就发生了。

class Widget{
     ...};
Widget w;
...
w=w;//看起来很显而易见的错误
a[i]=a[j];
*px=*py;/
class Base{
     ...};
class Derived:public  Base{
     ...};
void doSomething(const Base&rb,Derived* pd);//基类引用或指针都可以指向一个Derived class对象。

自我赋值时可能会发生“在停止使用资源前意外释放了它”的陷阱。
假设你建立了一个指针指向一块动态分配的位图(bitmap):

class Bitmap{
     ...};
class Widget(
...
private:
	Bitmap *pb;
};
  1. 自我赋值时并不安全的代码

    Widget& Widget::operator=(const Widget& rhs)
    {
           
    	delete pb;
    	pb=new Bitmap(*rhs.pb);
    	return *this;
    }
    
  2. 避免这种错误,传统做法是在代码最前面加上一个证同测试达到自我赋值的检验目的
    这种方法可以保证自我赋值安全性,但还不能保证异常安全性(分配时的内存不粗或者是Bitmap的copy构造函数发生异常)

    Widget& Widget::operator=(const Widget& rhs)
    {
           
    	if(this==&rhs)return *this;//证同测试
    	delete pb;
    	pb=new Bitmap(*rhs.pb);
    	return *this;
    }
    
  3. 将解决问题的重点放在异常安全上,跳过证同测试

    Widget& Widget::operator=(const Widget& rhs)
    {
           
    	Bitmap* pOrig=pb;//记住以前的pb地址
    	pb=new Bitmap(*rhs.pb);//令pb指向*pb的一个副本 会出现异常的地方
    	delete pOrig;//释放以前的pb地址
    	return *this;
    }
    
  4. 第三种方案的替代方案“copy and swap”技术

    Widget& Widget::operator=(const Widget& rhs)
    {
           
    	Widget temp(rhs);//为rhs做一份数据复件
    	swap(temp);//交换temp和*this的数据,temp会在函数结束之后自动释放内存
    	return *this;
    }
    
  5. copy and swap技术的另一种实现基于以下两点:
    (1)某class的copy assignment操作符可能会被声明为by value方式
    (2)以by value方式传递值会造成一份拷贝
    基于以上两点我们写下另一种实现:

    Widget& Widget::operator=(Widget rhs)
    {
           
    	swap(rhs);
    	return *this;
    }
    

    牺牲了清晰性,但是将copy动作从函数本体挪到函数参数构造阶段却可令编译器有时生成更高效的代码。

条款12:赋值对象时勿忘其每一个成分

如果你自己写出copy函数,那么在你添加新的成员变量的时候编译器不会提醒你。
一旦发生继承,那么派生类构造阶段调用基类构造函数时就会被不带实参的缺省构造函数替代,而你自己的那个版本就会失效。
确保(1)复制所有的local成员变量。(2)调用所有base classes内的适当的copying函数。

  • 如果你发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的办法就是建立一个新的成员变量供这两个函数调用。

你可能感兴趣的:(effective,C++)