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

C++默认调用的函数

当你写一个空类的时候,编译器会默认声明一个编译器版本的copy构造函数,copy assignment操作符、一个析构函数和默认构造函数(都是inline的与public)。

//当声明一个空类的时候编译器默认声明以下函数
//当函数调用的时候,编译器才会创建这些函数
class Empty(){
public:
	Empty(){...};								//默认构造函数
	Empty(const Empty& rhs){...};				//copy构造函数
	~Empty(){...};								//析构函数,non-virtual
	Empty& operator= (const Empty& rth) {...};	//copy assignment操作符
};

//如果类的成员是常量或是引用的话,默认的赋值操作是拒绝编译的
//因为C++不允许reference指向不同的对象,更改常量也是不合法的。
//还有一种情况是基类的将copy assignment操作符定义为private的话
//派生类是无法生成默认的copy assgnment操作符函数的(derived class没有权限处理base class的成分)

明确拒绝所不需要的编译器默认生成的函数

书上介绍了两个方法,以copy函数为例,一个是在主动声明private的copy成员,但是却不实现它。另一个就类似上一部分内容说的一个情况,构建一个专门阻止copy的类(用第一个方法),让其他需要阻止copy的类继承于该base class。

class MyClass{
protected:
	MyClass(){...};						//允许默认构建构造函数
	~MyClass(){...};					//允许默认构建析构函数
private:
	MyClass(const MyClass);				//只进行声明,但是不实现,此时MyClass也不允许copy操作
};

class A : private MyClass{
	//A不允许构建copy函数
};

为多态基类声明virtual析构函数

主要讲如果基类里面有virtual(基本上该类就是多态性质了),那么其析构函数几乎是必须定义为virtual,如果不想让某个类成为base class(并具有多态的性质),那么定义其析构函数没什么意义(反倒导致对象体积增加,不再具有移植性)。当然不是所有的基类都要virtual 析构函数,只有多态基类才需要声明virtual析构函数。(如上面所述的阻止copy的base class就不是多态用途,只是继承,并不需要指派虚析构函数)

多态中子类指针可以赋值给父类指针,继承是指子类可以使用父类的功能。

一些标准容器是(如string)是没有virtual函数的,所以,要是继承的话,可能会导致不确定行为(将派生类指针赋予基类指针并释放)。

class TimerKeeper{
public:
	TimerKeeper();
	~TimerKeeper();					//non-virtual的析构函数
};

class ALittleTimer : TimerKeeper{
	...
};
//设计工厂函数(工厂模式)用来返回实例。
TimerKeeper* getTimerKeeper(){...};

//在调用的时候,如果使用delete的时候(书上建议不要delete,用RAII机制)
//delete的时候可能base成分销毁了,但derived成分没有被销毁

TimerKeeper* ptk = getTimerKeeper();
//getTimerKeeper()可能是返回ALittleTimer对象
...

delete ptk;						//此时会导致局部销毁

如果class中有纯虚函数的话(会导致抽象类),析构函数应该是声明为纯虚析构函数,但是由于析构函数的运作是从最derived class的析构开始调用的,然后逐渐调用到base class的析构函数,此时需要对base class析构函数进行定义(纯虚函数通常来说不被定义,在抽象类中声明,派生类中实现),连接器才不会报错。

class AWOV{
public:
	virtual ~AWOV() = 0;		//声明纯虚的析构函数
};

AWOV::~AWOV(){}					//pure virtual析构函数的定义

禁止析构函数吐出异常

禁止析构函数吐出异常,利用try-catch来捕捉异常并吞下或是使用abort来结束程序。

异常的处理机制:在某个函数A中出现异常,并不进行处理,抛出一个异常给A的调用者,这样一层一层的抛出(如果每一层都不进行处理),直至main函数,如果也不进行处理的话,则程序中止。

析构函数如果抛出异常的话,在调用析构函数的时候,程序总会强制性停止,不会返回main函数中止程序,而是在析构函数中直接中止程序。(构造函数抛出异常时则会导致内存泄露)。

//class.cpp
#include "Class.h"
#include <iostream>

Class::~Class() {
	std::cout << "i dead" << std::endl;
	throw ("ss");
}
void Class::Error() {
	throw("error");
}

//main.c

#include <iostream>
#include "Class.h"

using namespace std;

int main()
{
    Class* s = new Class();
    try {
        delete s;					//在此处直接中止程序
        s->Error();					//如果把上一行注释掉,就不会中止程序,Error产生的异常跳到外层进行处理
    }
    catch(...){
        cout << "you die" << endl;
    }
}

在析构函数不得不产生异常的时候,必须将异常吞掉。

不产生异常就好了,但有时候程序员明白异常产生的原因(但不知道怎么处理还是不要catch,直接中止吧),可以在catch中恢复那些程序员可以弥补的数据,如果不能弥补,再进行数据保存。不过异常安全问题实在麻烦,使用异常机制也会有损性能,noexcept 可能是个好选择(拒绝抛出异常)。

例子是在RAII类中的析构函数里面处理异常,如果客户需要对某个操作函数运行期间抛出的异常进行反应,那么应该提供一个普通函数对其进行操作。

//DBConnection中有个函数close可能会出现异常
class DBConnection{
public: 
	...
	void close();
};

class DBConn{
public:
	void close()				//给客户机会使用函数
	{
		db.close();
		close = true;
	}
	~DBConn()
	{
		if(!closed){
			try					//如果发生异常时吞下异常
			{
				db.close();
			}
			catch(...)
			{
				//制作运转记录,记录下对close调用的失败
				std::abort();	//结束程序
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

不要在构造与析构过程中调用virtual函数

在创建derived class对象的时候,base class 构造函数的执行先于derived class,所以说,如果base class 的构造函数调用了virtual函数的话,会导致多态失效,该函数是base class的函数,而不是derived class的(此时virtual函数不是virtual函数)。

不单单是virtual函数,dynamic_cast和typeid也会有一样的效果。

同样在析构函数中使用这些函数时,他们也不会变成一个derived class对象(只会属于base class)。

为了避免重复代码,又不允许使用虚函数,书上提出一个解决方案,利用derived class将必要的构造信息向上传递至base class构造函数中(因为你无法使用virtual函数向下进行调用)。

//
class Transaction{
public: 
	explicit Transaction(const std::string& logInfo);			//拒绝隐式转化
	void logTransaction(const std::string& logInfo) const;		//const成员函数,说明不会修改数据类型
	...
};

Transaction::Transaction(const std::string& logInfo){
	...
	logTransaction(logInfo);
}

class BuyTranction : public Tranction{
public:
	BuyTransaction(parameters)
	: Transaction(createLogString(parameters))					//将参数传给基类构造函数
	{...}
private:
	static std::string createLogString(parameters);				//辅助函数通过参数parameters来返回不同的string值。
};

在构建派生类对象的时候,由于基类构造函数肯定是首先构造的,如果不指定调用构造函数,会进行隐式调用基类的构造函数,为了向上向基类传递信息,通过显示调用基类的构造函数并传递某些参数信息。

有个部分不太明白:书上说定义createLogString为static的话,也就不可能意外指向“初期未成熟的BuyTransaction对象内部尚未初始化的成员变量”,猜测的一个原因是static修饰的成员函数是无法访问非static的成员变量的。

令operator= 返回一个reference to *this

理由是随众。不return *this 编译也是能通过的。

处理operator = 中的自我赋值的问题

如果不进行自我赋值的处理的话,当两个指针指向同一个对象的时候,对其中的一个进行销毁,那么另外一个就会指向一个删除了的对象。

书上建议采用证同试验来进行检验是否自我赋值。

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;			//此时留有一个副本,后面发现异常时,可以进行弥补,返回副本
	pb = new Bitmap(*rhs.pb);	
	delete pOrig;
	return *this;
}

//使用copy and swap技术进行异常安全处理
//在待赋值的原对象复制一个副本,对副本进行修改,如果出现异常则返回原对象,没有异常就交换副本与原对象
class Widget{
	...
	void swap(Widget& rhs);
	...
};

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

复制对象时勿忘记其每一个成分

当你自己声明copying(copy函数和copy assignment操作符)函数的时候,编译器在你忘记复制某些变量的时候很有可能不报错。

在新增加一个成员变量的时候,就要修改copying函数。如果发生继承的话,就必须将连带复制基类的成员变量。

class A{
public:
	A(const A& rhs);
	A& operator= (const A& rhs);
private:
	int a;
};
A::A(const A& rhs) : a(rhs.a)
{...}
A& A::operator= (const A& rhs)
{...}

class B : public A{
public:
	B(const B& rhs);
	B& operator= (const B& rhs);
private:
	int b;
};

class B::B(const PriorityCustomer& rhs)
	:b(rhs.b),A(rhs)			//调用A的复制构造函数	
{...}

B& B::operator= (const B& rhs){
	A::operator= (rhs);			//必须对A的成分进行赋值
	...
}

不允许copy函数与copy assignment操作符之间相互调用,如果是为了消除重复代码,建议建立第三个函数来减少代码重复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值