《Effective C++》构造/析构/赋值 函数:条款5-条款9

每一个类中都有构造函数、析构函数、赋值操作符。这几个函数是一个类最根本的函数,它控制着创建对象并初始化、对象消亡时的清理以及摆脱旧值赋新值。这样函数如果有问题,那么影响极为严重。

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

加入编写一个空类,那么经过编译之后,C++默认编写了哪些函数。

class Empty{
};

经过编译器处理后会有默认构造函数、复制构造函数、赋值操作符和析构函数。这些函数都是public且inline。

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

当需要这些函数时,这些函数会被编译器创建,这几个是最常见的、也是用的最多的。那么编译器创建的这些函数都是做了什么?

首先,如果没有构造函数,编译器将会创建一个默认构造函数,由它来调用基类和non-static成员变量的构造函数。

析构函数是否是虚函数,继承基类,如果没基类,那么默认是non-virtual。析构函数会调用基类和non-static成员变量的析构函数。

编译器创建的复制构造函数和赋值构造函数是个浅复制。浅复制可以参考这里

在编译器创建的复制构造函数和赋值操作符中,给成员变量初始化或赋值,会调用成员变量的赋值构造函数和赋值操作符。

对于赋值操作符,有些情况下编译器是不会合成的。只有当生成的代码合法且有适当机会证明它有意义,才会合成赋值操作符。英文原文是:the resulting code is both legal and has a reasonable chance of make sense.

例如一个类包含不可更改(重新赋值成员变量)

template<typename T>
class NamedObject{
public:
	NamedObject(const char* name, const T& value);
	NamedObject(const std::string& name, const T& value);

private:
	std::string& nameValue;
	const T objectValue;
};

两个成员变量,一个是引用:初始化后不能更改,一个是常量:也是初始化后不能更改。

编译器合成赋值操作符后,在调用赋值操作符时能更改这两个变量的值吗?显然不可以。编译器会拒绝编译重新赋值的动作。

如果自己编写赋值操作符,且合法,编译不会出错。

如果其基类的赋值操作符是private,编译器将拒绝为派生类合成赋值操作符。

关于这三个函数的一些内容,还可以参考这里

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

假设有一个中介卖房子,房子是一个类

class HomeForSale
{
……
};

天下没有两个一模一样的房子,因此复制构造函数和赋值操作符不应该使用。

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);//应该出错
h1=h2;//应该出错

但是编译器默认帮我们合成了这两个函数。而且编译器合成的函数都访问都是public。

如果我们想阻止使用这两个函数,我们可以声明它们为private。

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

只声明,不实现。如果普通的调用会在编译阶段出现错误(无法访问private),但是友元和member函数却可以访问,这样的话错误会发生的链接阶段,因为我们只是声明,没有实现。

将错误提前到编译阶段是最好的,毕竟越早出现错误越好。可以通过继承来实现,在这设计一个不可以复制的类

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

让其他类来继承此类即可

class HomeForSale:public Uncopyable
{
……
};

这样一来,在HomeForSale类中如果生产对应的赋值或复制构造函数,会调用基类对应的函数,其基类对应函数为private,会出现编译错误。

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

在创建有层次的类时,往往要把基类的析构函数声明为虚函数。

这是因为在使用这些类时,往往是通过基类指针或者引用使用的(类的实例在堆上),如果是析构对象时,通过delete +指针,这时如果析构函数不是虚函数,将不会调用当前指针指向对象的析构函数。这是多态的原理。

同理可知,要实现多态的函数,在基类也要声明为虚函数。

当一个类不用做基类时,如果把其析构函数声明为虚函数是个馊主意。因为虚构函数是通过虚函数表调用的,在调用虚函数时多一步指针操作;除此之外,其对象占用的内存空间也会多一个虚函指针。

一个类不含虚函数,一般不适合做基类。例如string类不含任何的虚函数,但是有时程序员还是会这样用

class SpecialString:public std::string{
……
};

这时如果通过基类指针使用,在delete时,可能会造成内存泄漏。要记住,STL中的容器都没有虚的析构函数。

有时一个类含有纯虚函数。这样的类叫做抽象类,不能被实例化。

class AWOV
{
public:
	virtual ~AWOV()=0;
};

如果把它当作基类,会有问题,因为其析构函数只有声明。析构函数的调用时从派生类到基类,如果没定义,会发生链接错误,这是要定义个空的析构函数

AWOV::~AWOV(){}

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

在C++中,析构函数可以抛出异常,但是不建议这么做。

class Widget{
public:
……
~Widget(){……}
};

void doSomething()
{
	std::vector<Widget> v;
	……
	//v要析构,会调用Widget的析构函数
}

当容器销毁时会调用析构函数,这时如果析构函数抛出异常,容器中剩余的元素还是应该被销毁,否则可能会有内存泄露。这时如果继续销毁其他元素,又出现异常的话,会同时存在两个异常。两个异常同时存在会导致不明确的行为。使用标准库的其他容器或TR1的任何容器或者array,也会遇到类似的问题。

但是有时必须在析构函数中执行一些动作,而这些动作可能会抛出异常。例如一个类负责数据库连接

class DBConnection
{
public:
……
void close();//关闭数据库
};

为了确保用户在使用后关闭连接,在析构函数中关闭连接。

class DBConnection
{
public:
……
	~DBConn()//析构函数关闭连接
	{
		db.close();
	}
private:
	DBConnection db;
};

如果调用close成功,则一切都美好。但是如果出现异常,DBConn会抛出异常,也就是允许这个异常离开析构函数,这样会传播异常。

有两个方法可以避免这个问题:

1、如果close抛出异常,就终止这个程序。通常通过调用abort完成。

通常当“于析构期间发生的错误”后无法继续执行,终止程序是个合理的选项。毕竟这样可以确保异常从析构函数传播出去。

~DBConn()//析构函数关闭连接
	{
		try{
			db.close();
		}
		catch(……)
		{
			//记录下对close调用的失败
			std::abort();//退出
		}

	}

2、吞下这个异常。

~DBConn()//析构函数关闭连接
	{
		try{
			db.close();
		}
		catch(……)
		{
			//记录下对close调用的失败
		}

	}

吞下这个异常也不是个好主意,它压制了某些失败动作的重要信息。

一个比较好的策略是重新设计DBCoon接口,是客户能对可能出现的异常做出反应。例如DBConn可以自己提供一个close函数,可以给客户一个机会来处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已经关闭,并在答案为否的情况下由其析构函数关闭,这样可以防止遗失数据库连接。但是如果DBConnection的析构函数调用close失败,问题又回到了起点。

把调用close的责任从DBConn析构函数手上转移到DBConn客户手上,这样会多出一个保险。客户自己调用close函数并不会给它们带来负担,而且给了他们一个处理错误的机会,否则他们没机会响应。

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

在构造或析构函数中调用virtual函数不会呈现出多态。看下面一个例子,假设一个class继承体系,用来记录买进和卖出的订单。每笔交易都要有记录,基类这样写

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
}

在买进和卖出的类中,可以重写logTransaction()函数。

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;

输出结果却是:This is Transaction logTransaction

不是想要的This is BuyTransaction logTransaction。

在构造BuyTransaction对象b的时候,首先调用BuyTransaction类的构造函数,在这个构造函数中,再调用基类Transaction的构造函数,在这里调用了

logTransaction()函数。这样就输出了所显示的内容。在基类构造期间,不会下降到派生类去调用派生类的虚函数。

base class的构造函数先于derived class的构造函数。在base class构造函数期间,derived class的对象还没有构建,如果derived class的virtual用到了local变量,这时如果真的调用了derived class的virtual函数,会使用为初始化的变量,会有不明确的行为。所以C++不让你走这条路。

还有一个理由就是,在base class构造期间,对象类型是base class,不是derived class。virtual函数会被编译器解析(resolve to)base class。如果使用了运行期类型信息(runtime type information),编译器也会把它视为base class类型。

相同的道理同样适用于析构函数。析构过程和构造过程相反。先析构派生类部分,再析构基类部分。析构到基类时,派生类中的变量就是为初始化的,对象类型是基类类型。

上面代码中,比较容易看出在构造函数中调用了virtual函数,如果改写为下面的形式:

class Transaction{
public:
	Transaction();
	void Init()
	{
		logTransaction();
	}
	virtual void logTransaction()const//virtual function
	{
		//log the Transaction
		std::cout<<"This is Transaction logTransaction"<<std::endl;
	}
};
Transaction::Transaction()
{
	Init();
}

就没那么容易看出问题了。构造函数调用了Init()函数,在Init()函数中调用了virtual函数。

那么怎么才可以实现上面的要求呢?在构造对象时记录下相应的记录。

一种方法是:在构造函数时,传递信息给logTransaction()函数,从derived class构造函数传递给base class构造函数。

#include<iostream>

class Transaction{
public:
	explicit Transaction(const std::string& parameter);

	void logTransaction(const std::string& parameter)const//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;
}

这样就解决了开始提出的问题。在derived类中使用了private static函数来创建参数。这样增强了代码的可读性,另外static函数不会指向未初始化的derived类中的变量。

时间: 2024-10-06 22:32:33

《Effective C++》构造/析构/赋值 函数:条款5-条款9的相关文章

《Effective C++》构造/析构/赋值 函数:条款10-条款12

条款10:令operator=返回一个reference to *this 赋值操作符运算是由右向左运算的.例如一个连锁赋值 <span style="font-size:14px;">int x, y, z; x=y=z=15;</span> 编译器解释时时这样的: x=(y=(z=15)); 先给z赋值,用赋值后的z再给y赋值,用赋值后的y再给x赋值. 为了实现连锁赋值,操作符必须返回一个reference指向操作符左侧的实参. 其实,如果operator=

[Effective C++]构造/析构/赋值运算

条款05:了解C++默默编写了并调用了那些函数 请记住: 编译器可以暗自为class 创建default构造函数,copy构造函数,copy assignment 操作符,以及析构函数 class Empty { public: Empty(){...} //default constructor Empty(const Empty& rhs){...} //copy constructor ~Empty(){...} //destructor Empty& operator=(const

c++笔记:const、初始化、copy构造/析构/赋值函数

构造函数 Default构造函数:可被调用而不带任何实参的构造函数,没有参数或每个参数都有缺省值.如: class A { public: A(); }; 将构造函数声明为explicit,可阻止它们被用来执行隐式类型转换,但仍可用来进行显示类型转换.如: class B { public: explicit B(int x = 0, bool b = ture); }; copy构造函数:用于以同型对象初始化自我对象,以passed by value的方式传递对象:· copy assignm

Effective C++ —— 构造/析构/赋值运算(二)

条款05 : 了解C++默默编写并调用哪些函数 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费 条款02 : 尽量以const,enum,inline 替换#define 水电费

Effective C++ -- 构造析构赋值运算

05.了解C++默默编写并调用哪些函数 编译产生的析构函数时non-virtual,除非这个类的基类析构函数为virtual 成员变量中有引用和const成员时,无法自动生成copy assignment函数 基类将copy assignment操作符声明为private时,编译器拒绝为其derived classes生成一个copy assignment操作符. 06.若不想使用编译器自动生成的函数,就该明确拒绝 将自动生成的默认构造函数,拷贝构造函数,copy assignment声明为pr

C++构造/析构/赋值函数

在编写C++程序的时候,我们会为特定某一类对象申明类类型,几乎我们申明的每一个class都会有一个或多个构造函数.一个析构函数.一个赋值运算符重载=.以及拷贝构造函数.这些函数控制着类对象的基础操作,确保新定义的对象的初始化.完成对象撤销时的清理工作.赋予对象新值.如果这些函数的操作出错,则会导致严重的后果,所以确保这些函数的操作行为正常是非常重要的. 一.编译器默认生成的函数 如果我们编写一个空类,编译器会为我们默认生成构造函数.析构函数.赋值运算符.拷贝构造函数. 例如当我们定义 class

Effective C++笔记:构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数 默认构造函数.拷贝构造函数.拷贝赋值函数.析构函数构成了一个类的脊梁,只有良好的处理这些函数的定义才能保证类的设计良好性. 当我们没有人为的定义上面的几个函数时,编译器会给我们构造默认的. 当成员变量里有const对象或引用类型时,编译器会不能合成默认的拷贝赋值函数:当一个基类把它的拷贝赋值函数定义为private时,它的派生类也不无生成默认的拷贝赋值函数,因为它无法完成基类成份的赋值. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝 将拷贝构

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(2)-读书笔记 <Effective C++>第8章 定制new和delete-读书笔记 条款09:绝不在构造和析构过程中调用virtual函数 你不该在构造和析构函数期间调用virtual函数,因为这样的调用不会带来你预期的结果. (1)在der

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effective C++>第8章 定制new和delete-读书笔记 条款05:了解C++默默编写并调用哪些函数 当C++处理过一个空类后,编译器就会为其声明(编译器版本的):一个拷贝构造函数.一个拷贝赋值运算符和一个析构函数.如果你没有声明任何构造函数,编译器还会声明一个默认构造函数.所有这些函数都被声明为pub