2、构造/析构/赋值运算

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

当你自己没有编写时,编译器会默认为你的类创建defult构造函数、析构函数、拷贝构造函数和copy assignment操作符。
如:

class Empty{};

实际上,相当于

class Empty{
public:
	Empty(){...}  //defualt构造函数
	Empty(const Empty& other){...} //copy构造函数
	~Empty(...)   //析构函数

	Empty& opreate=(const Empty& other){...}  //copy assignment函数
};

当你自己写了构造函数后,编译器就不会为你生成defualt 构造函数了。
有两种情况下,编译器会拒绝执行copy assignment操作。

template<class T>
class NamedObject {
public:
	NamedObject(std::string& name, const T& value);
	...
private:
	std::string& nameValue;
	const T objectValue;
};
std::string NewDog("PersePhone");
std::string OldDog("Satch");
NameObject<int> p(newDog, 2);
NameObject<int> s(OldDog, 36);
p = s; //此过程编译器会报错

因为nameValue是引用类型,C++不允许引用类型指向其他对象。所以当p.nameValue已经赋值引用对象后,不能更改,所以 p=s 操作便会被编译器拒绝执行。
同样的const成员不能更改,所以p=s操作时尝试更改p.objectValue,编译器也会拒绝执行。
另外,如果base classes将copy assignment声明为private时,编译器为拒绝为derived classes生成copy assignment操作符。


编译器可以暗自为class创建defalut构造函数、copy构造函数、copy assignment操作符以及析构函数

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

如果不想使用编译器支持copy或赋值等功能,可以将copy构造函数和copy assignment操作符声明为private。

class HomeForSale{
private:
	HomeForSale(const HomeForSale& );
	HomeForSale& operator=(const HomeForSale&);
};

但是这样写会出现问题,比如在member函数或friend函数中这么做时,会引起不必要的错误。所以更合适的写法是定义一个不可复制到base class,然后继承它,如下:

class Uncopyable{
protected:
	Uncopyable();
	~Uncopyable();
private:
	Uncopyable(const Uncopyable& );
	Uncopyable& opreator=(const Uncopyable&);	
};

class HomeForSale : private Uncopyable{
};

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

如果不这么做,会造成派生类没有被析构,产生不可预估的错误。

#include <iostream>
class Base{
public:
        Base(){}
        virtual void Hello(){std::cout << "base hello \n";}
        ~Base(){std::cout << "~Base\n";}
};
class Derived : public Base{
public:
        Derived(){}
        void Hello() override{std::cout << "derived hello\n";}
        ~Derived(){std::cout << "~Derived\n";}
};
int main(){
        Base* base = new Derived();
        base->Hello();
        delete base;

        return 0;
}

输出

derived hello
~Base

可以看到,派生类没有被析构,如果派生类里需要在析构函数里释放内存,就是造成内存泄漏。

但是不是说所有的类都要把析构函数定义为虚函数,因为虚函数需要维持一个虚表,可能会带来额外的内存使用。

带多态性质的基类应该声明一个virtual析构函数
如果class设计的目的不是作为基类或多态性,就不该声明virtual析构函数

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

class Widget{
public:
	...
	~Widget(){}   //假设这里会吐出一个异常
};
void  dosomthing(){
	std::vector<Widget> v;
	...      //v 在函数结束时自动销毁
}

这样就会造成vector里的Widget析构时产生多个异常,而C++在两个异常同时存在时会结束执行或产生未知错误。
最好的做法就是把异常交给客户处理,并且在析构函数里上双保险,捕获异常。

class DBConn{
public:
...
void close(){
	db.close();
	closed = true;
}
~DBConn(){
	if(!closed){
		try{
			db.close();
		}catch(...){
			//记录异常并关闭程序或吞下异常
		}
	}
}
private:
	DBConnction db;
	bool closed;
};

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获它并处理。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。

条款09:绝不在构造或析构函数中调用virtual函数

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类

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

int x,y,z;
x=y=z=10;

为了实现连锁赋值,赋值操作符需要返回一个reference执行操作符的左侧实参。

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

令assignment操作符返回一个reference to *this

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

class Bitmap{...};
class Widget{
public:
	...
	Widget& operator=(const widget& rhs){
		delete pb;
		pb = new Bitmap(*rhs.pb);
		return *this;
	}
private:
	Bitmap* pb;
};

如果参数rhs等于this,也就是发生了自我赋值时,就会造成pb在第一步就被释放掉了。最终this返回的对象指向了一个空指针,发生未知错误。
可以修改为这样

class Bitmap{...};
class Widget{
public:
	...
	Widget& operator=(const widget& rhs){
		if(this == &rhs) return *this;
		delete pb;
		pb = new Bitmap(*rhs.pb);
		return *this;
	}
private:
	Bitmap* pb;
};

如果是自我赋值则直接返回。
或者

class Bitmap{...};
class Widget{
public:
	...
	Widget& operator=(const widget& rhs){
		Bitmap* temp = pb;
		pb = new Bitmap(*rhs.pb);
		delete temp;
		return *this;
	}
private:
	Bitmap* pb;
};

运用临时变量,先报错原来的指针,重新生成对象后,再删除原来的指针。

再或者使用swap

class Bitmap{...};
class Widget{
public:
	...
	Widget& operator=(const Widget& rhs){
		Widget temp(rhs);
		swap(temp);   //将*this 数据与temp进行交换
		return *this;
	}
private:
	Bitmap* pb;
};

确保当对象自我赋值时operator=有良好的行为
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

条款12:复制对象时勿忘其每一个成分

比如:

class Customer{
public:
	...
	Customer(const Customer& rhs):name(rhs.name){}
	Customer& operator= (const Customer& rhs){
		name = rhs.name;
		return *this;
	}
private:
	std::string name;
};

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

这样在派生类被调用拷贝时,会忘记复制基类的成员变量,造成错误。
正确的做法是:

class Customer{
public:
	...
	Customer(const Customer& rhs):name(rhs.name){}
	Customer& operator= (const Customer& rhs){
		name = rhs.name;
		return *this;
	}
private:
	std::string name;
};

class PriorityCustomer : public Customer{
public:
	...
	PriorityCustomer(const PriorityCustomer& rhs):
		Customer(rhs),
		priority(rhs.priority){}
	PriorityCustomer& operator=(const PriorityCustomer& rhs){
		Customer::operator=(rhs);
		priority = rhs.priority;
		return *this;
	}
};

Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
不要尝试以某个Copying函数实现另一个Copying函数。应该将共同机能放进第三个成员函数中,并由两个Copying函数共同调用。

上一篇:智能指针实现Impl模式


下一篇:[CF1182E] Product Oriented Recurrence - 矩阵快速幂