《Effective C++》学习笔记(四)

原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38845319

前言

今天给自己订的任务是将《Effective C++》第二章看完,一口气看下来发现量并不大,这一章剩下的内容都较为简短,来看看今天的条款吧。

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

如同条款的字面意思,不要让析构函数中抛出异常,这样会使程序出现不明确行为。

举个例子:有一个Widget的自定义类的vector。

vector<Widget> v;

当它在呗销毁的时候,它需要销毁掉里面含有的所有Widget,如果里面有10个Widget,在析构第一个元素期间,有个异常抛出,其它九个依旧得销毁,而不是跳过。

用书上的话说就是C++不喜欢析构函数吐出异常。

书上还举了一个例子,假设你使用一个class负责数据库连接:

class DBConnection{
public:
	...
	static DBConnection create();
	void close();
};

为了防止客户使用完DBConnection以后忘记调用close(),我们可以创建一个新类DBConn来管理这个对象,我们可以在这个新类的析构函数中调用该DBConnection对象的close()

class DBConn{
public:
	~DBConn()
	{
		db.close();
	}
	...
private:
	DBConnection& db;
};

我们就可以这样来用它:

{
	DBConn connection(*(DBConnection.create()));
	...
}

这里出现个问题就是如果db.close(),要是吐出异常怎么办?通常我们用以下解决方案:

一是只要抛出异常就结束程序:

DBConn::~DBConn(){
	try
	{
		db.close();
	} catch(...)
	{
		....
		std::abort();//结束程序,可以强制"不明确行为"死掉
	}
}

二是吞掉异常:

DBConn::~DBConn(){
	try
	{
		db.close();
	}catch(...)
	{
		...
	}
}

一般而言吞掉异常不是很好的处理发式,但总比为"粗暴的结束程序"或"出现不明确行为"担负代价和风险要好,这样即使程序遭遇了一个错误或者异常情况下都可以继续运行,在另一方面提高了软件的健状性。而这些解决方案都存在一个问题:客户不能对"close失败的异常情况"做出反应,为了解决这个问题,这里我们可以将独立出来一个新的close接口:

class DBConn{
public:
	void close()
	{
		db.close();
		isClosed = true;
	}
	~DBConn(){
		if(!isClosed){//使用以上两种解决方案之一来进行解决.
		}
	}
	...
private:
	DBConnection db;
	bool isClosed;
};

这样我们来重新审理这段代码,一个新的接口close(),提供给客户调用,如果出现异常客户可以第一时间来进行处理,如果可以确定这里不会出现异常的话,也可以不处理,客户还可以选择不调用close(),放弃对可能出现的异常处理,选择让析构函数自动调用。

总结:

1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

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

看到这个条款的时候,我有一点疑惑,为什么不能再构造函数和析构函数中调用virtual函数,看完条款,我的困惑逐步消散了。

先看看一段代码:

class A
{
	public:
	A();
	virtual void func() const = 0;
	...
}

A::A(){
	...
	func();
}

class B : public A
{
public:
	virtual void fund() const;
	...
}

如果我们要创建一个B对象,那么肯定会调用B的构造函数,但是在调用B()之前,A()一定会被优先调用,是的,派生类对象的基类部分会在派生类自身成分被构造之前先构造妥当。

在A()构造函数的最后一行调用了virtual函数func,这时调用的是A中的func,而不是B中的func。

在基类构造期间,virtual函数不是virtual函数。

为什么会这样呢?第一,派生类的函数里几乎必然取用了local成员变量,而那些成员变量尚未初始化。C++是不会让你这么做的。

第二,也是更根本的原因,在派生类对象的基类构造期间,对象的类型是基类而不是派生类,不只是virtual函数会被编译器解析至基类,若使用运行期类型信息(如dynamic_cast、typeid),也会把对象视作基类类型。

总结:

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。

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

经常可以看到连锁赋值,形如:

int x, y, z;
x = y = z = 15;

连锁赋值其实采用的是右结合律,所以上述连锁赋值可以解析为:

x = (y = (z = 15));

为了让自定义类也实现如此“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。

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

这个协议不仅适用于标准赋值形式,也包括+=,==,*=等所有赋值相关运算。

注意:这只是一个协议,没有强制性,但是所有内置类型和标准程序库提供的类型或者即将提供的类型都共同遵守。所以除非你有个标新立异的好理由,不然还是乖乖随众吧。

总结:

令赋值操作符返回一个reference to *this。

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

“自我赋值”发生在对象被赋值给自己时,比如:

Widget w;
w = w;

这看上去有点愚蠢,但它是合法的,所以一定不要认为客户不会这么做。其实还有其他形式,让人不易一眼识别出来。

a[i] = a[j]; // i = j时,潜在的自我赋值
*px = *py; // px与py恰好指向同一东西时,潜在的自我赋值

我们来看一个因为“自我赋值”而导致的不安全示例吧:

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

如果this = &rhs,那么这份代码必定十有问题的,所以我们可以这么改一下:

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

我们叫这个方法为“证同测试”,这样做是能避免“自我赋值”导致的不安全行为的,但是如果直接让operator=拥有“异常安全性”,它同时也会有“自我赋值安全性”,一石二鸟。

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

当然,除了这个方法之外,还有一个确保代码不但“异常安全”而且“自我赋值安全”的方法:

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

&#16;当然,这个代码还可以简化成下面这样:

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

总结:

1)确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

一个良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象的拷贝,对的,他们就是copy构造函数和copy assignment操作符,统称为copying函数。

编译器会为我们默认生成,你也可以自定义一个版本,但一定要细致谨慎。

一定要给所有的成员变量在copying函数中进行复制。不要可能会照成不明确行为。

如果这个类同时作为基类,切记一定要在copying函数中调用对应的基类copying构造函数。

简单点说:1)复制所有local成员变量,2)调用所有base classes内的适当copying函数。

不要试图让copy构造函数调用copy assignment,也不要试图让copy assignment调用copy构造函数。

如果两者之间有相似的代码,应该将他们提出来,放到一个新的函数里,任copy assignment和copy构造函数调用。

总结:

1)Copying函数应该确保复制“对象内的所有成员变量”及“所有base class 成分”。

2)不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

结语

看了下目录,第三章也是五个条款,总页数大概17页,明天争取看完五个条款。

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

时间: 2024-10-12 22:03:43

《Effective C++》学习笔记(四)的相关文章

python cookbook第三版学习笔记四:文本以及字符串令牌解析

文本处理: 假设你存在一个目录,下面存在各种形式的文件,有txt,csv等等.如果你只想找到其中一种或多种格式的文件并打开该如何办呢.首先肯定是要找到满足条件的文件,然后进行路径合并在一一打开. path=r'D:\test_source' filenames=os.listdir(path) print filenames ret=[name for name in filenames if name.endswith('.txt')] print ret direct_path=[os.pa

python cookbook第三版学习笔记十二:类和对象(三)创建新的类或实例属性

先介绍几个类中的应用__getattr__,__setattr__,__get__,__set__,__getattribute__,. __getattr__:当在类中找不到attribute的时候,会调用__getattr__,并执行其中的自定义代码.所有在类中定义的属性都包含在__dict__中,也就是说如果在__dict__中找不到对应的属性名,则__getattr__被触发. class get_try(object):     def __init__(self,value):   

python cookbook第三版学习笔记十三:类和对象(三)描述器

__get__以及__set__:假设T是一个类,t是他的实例,d是它的一个描述器属性.读取属性的时候T.d返回的是d.__get__(None,T),t.d返回的是d.__get__(t,T).说法比较绕,我们来看一个实例: class Descriptor(object):     def __get__(self, instance, owner):         return 'get',self,instance,owner class T(object):     d=Descri

python cookbook第三版学习笔记六:迭代器与生成器

假如我们有一个列表 items=[1,2,3].我们要遍历这个列表我们会用下面的方式 For i in items:   Print i 首先介绍几个概念:容器,可迭代对象,迭代器 容器是一种存储数据的数据结构,容器将所有数据保存在内存中,典型的容器有列表,集合,字典,字符数组等.如items就是一个列表容器.   可迭代对象:这个对象是否可迭代.如items也是一个可迭代对象.简单来说如果可以用for循环的对象都称为可迭代对象.如果要判断是否是一个可迭代的对象.可以用print isinsta

python cookbook第三版学习笔记七:python解析csv,json,xml文件

CSV文件读取: Csv文件格式如下:分别有2行三列. 访问代码如下: f=open(r'E:\py_prj\test.csv','rb') f_csv=csv.reader(f) for f in f_csv:     print f 在这里f是一个元组,为了访问某个字段,需要用索引来访问对应的值,如f[0]访问的是first,f[1]访问的是second,f[2]访问的是third. 用列索引的方式很难记住.一不留神就会搞错.可以考虑用对元组命名的方式 这里介绍namedtuple的方法.

python cookbook第三版学习笔记二:字典

一般来说字典中是一个键对应一个单值的映射,如果想一个键值映射多个值,那么就需要将这些值放到另外的容器中,比如列表或者集合. 比如d={'a':[1,2]} Collections中的defaultdict模块会自动创建这样的字典.如下 d=defaultdict(list) d['a'].append(1) d['a'].append(2) d['b'].append(3) defaultdict(<type 'list'>, {'a': [1, 2], 'b': [3]}) 下面再来看下字典

python cookbook第三版学习笔记十一:类和对象(二)调用父类的方法

在子类中调用父类的方法,可以下面的A.spam(self)的方法. class A(object):     def spam(self):         print 'A.spam' class B(A):     def spam(self):         print 'B.spam'         A.spam(self) if __name__=='__main__':     b=B()     b.spam() 但是上面的代码有一个问题,如果B的父类变更了,而且有很多子类的父

python cookbook第三版学习笔记九:函数

接受任意数量参数的函数. 当传入函数的参数个数很多的时候,在函数定义的时候不需要为每一个参数定义一个变量,可以用*rest的方式来包含多余的参数. 如下面的代码,*rest包含了2,3,4这3个参数.且可以迭代访问.在这个例子中,rest其实就是其他位置参数组成的一个元组 def avg(first,*rest):     for i in rest:         print i     average=(first+sum(rest))/(1+len(rest))     print av

python cookbook第三版学习笔记十三:类和对象(五)代理类以及内存回收

代理类: 代理类的作用其实有继承有些类似,如果你想将某个实例的属性访问代理到内部另外一个实例中去,可以用继承也可以用代理.来看下代理的应用: class A:     def spam(self,x):         print 'in Class A x=%d' % x     def foo(self):         print 'in Class A:foo()' class B1:     def __init__(self):         self._a=A()       

《Linux命令、编辑器与shell编程》第三版 学习笔记---003

Shell准备 1.识别Shell类型 echo  $0 echo $BASH echo $SHELL 上边三个命令结果都是: /bin/bash 2.终端常用操作 a.删除单个字符 c+h或退格键 b.删除单个单词 c+w c.删除单个行 c+u d.重复编辑命令行 arrowUp或arrowDown e.中断命令执行c+C 3.切换为root用户 a.su命令后,输入root密码 b.sudo命令,输入用户密码,需执行该命令的用户在sudo组中 4.文档查看 a.使用man 1).用户命令