Effective C++ 读书笔记(5)—— 实现

尽可能延后变量定义式的出现时间

当一个变量的类型带有构造函数与析构函数的时候,控制流到达这个定义式就得承担构造成本,当离开作用域就必须承担析构成本,如果最后这个变量从未使用,仍然需要这些成本(在抛出异常的时候,就可能出现未被使用的情况)。

尽可能延后的真正意义是:你不单单应该延后变量的定义直到使用这个变量的那一刻,而且应该延后这份定义知道能给他初值实参。

对于循环来说,要判断他的构造析构成本与赋值成本。

//一个构造,一个析构,n个赋值
Widget w;
for(int i = 0; i < n; ++i){
	w = ...}
//n个构造,n个析构
for(int i = 0; i < n; ++i){
	Widget w(...);
}

尽量少做转型动作

C++的四种新式转型

const_cast<T> (expression)	//用于将对象的常量性移除
dynamic_cast<T> (expression)//用于安全向下转型,用于对象是否归属于继承体系中的某一个类型(旧式语法无法执行,会耗费大量的成本)
reinterpret<T> (expression)	//执行低级转型,如把pointer to int 转型为int)
static_cast<T> (expression)	//强迫隐式转换,比如non-const到const,int 到 double,到那时不能const 到non-const(要使用const_cast)

一般使用旧式转换的时候只有在调用explicit构造函数将对象传递给一个函数时。

class W{
public: 
	explicit W(int size);
	...
};
void dosome(const W& w);
dosome(W(15));		//使用旧式转型
dosome(static_cast<W> (15));	//使用新式转型

任何一个类型转换往往令编译器编译出运行期间执行的代码。

一个有意思的事情是

class Base{...};
class Derived: public Base{...};
Derived a;
Base* b = &a;	//此时的指针b与指针a是有一个偏移量的,这里隐喻的将Derived*转换为Base*

书上还提到一件事情,进行转型的时候返回的是个副本

class W{
public:
	virtual void onR() {..}
}class M : public W{
public:
	virtual void onR(){
	static_cast<W> (*this).onR();		//将this转换为W,然后调用onR
	...			//M的专属操作
	}
};
//这里面的转型是调用当前对象的base class的副本,再进行M的专属操作的,此时base class并没有改变,derived反倒改变了

class M : public W{
public:
	virtual void onR(){
		W::onR();		//此时调用的是base的版本
	}
};

书的后面探讨的是dynamic_cast的问题,dynamic_cast通常是用来在一个你认为是derived class的对象执行derived class的操作函数,但是你手上只有一个指向base class的pointer或reference。由于dynamic_cast运行效率很慢(对于深度继承与多重继承的成本更高),有两个一般性的做法可以避免这个问题。

一个是使用容器储存直接指向derived class的指针,通常是智能指针,这样就消除了通过base class处理对象的需要。

class W{...}class Spublic W{
publicvoid blink();
	...
};

typedef std::vector<std::tr1::shared_ptr<W>> VPM;
VPM ww;
for(VPM::iterator iter = ww.begin(); iter != ww.end(); ++iter)
{
	if(S* s = dynamic_cast<S>(iter->get()))		//最好不用dynami_cast
	{
		s->blink();
	}
}
//替换方式
typedef std::vector<std::tr1::shared_ptr<S>> VPSM;
VPSM ss;
for(VPSM::iterator iter = ss.begin(); iter != ss.end(); ++iter)
{
	(*iter)->blink();
}

另外的一种做法使用virtual,这边就不给代码了。

总之,有替代方案就不要使用dynamic_cast了。

然后绝对是要避免连串的使用dynamic_cast。

if(...)
	dynamic_cast...
else
	dynamic_cast...
//这种情况绝对要避免,因为产生出来的代码又大又慢,而且基本不稳。

避免返回handles指向对象内部成分

这里的handle可以是指针或迭代器或引用。

//这个类来表示点
class Point{
public: 
	Point(int x, int y);
	...
	void setX(int newV);
	void setY(int newV);
	...
};

//为了让Rectangle尽可能小,就把Point数据存储在struct里面,再用指针指向struct
struct RectData{
	Point ulhc;
	Point lrhc;
};

class Rectangle{
	...
public:
	//返回一个得知相关坐标点的函数,为了提高效率,采用by reference
	Point& upperLeft() const {return pData->ulhc;}
	Point& lowerRight() const {return pData->lrhc;}
	...
private:
	std::tr1::share_ptr<RectData> pData;
};
//上述的代码是可以通过编译,但是,代码的含义却是自相矛盾的
//因为一方面upperLeft和lowerRight是一个const成员函数,但是他却返回一个引用
//这就代表了用户可以使用这个引用来修改Point的值

//可以进行下面的修改
class Rectangle{
	...
public:
	const Point& upperLeft() const {return pData->ulhc;}
	const Point& lowerRight() const {return pData->lrhc;}
	...
};

但是,返回一个handle的问题还远不在此,返回handle之后,你就必须使所指对象比handle更加长寿。

class G{...};
const Rectangle bB(const G& obj);	//以值传递返回一个矩形

G* pgo;			
const Point* pp = &(bB(*pgo).upperLeft());	
//在这个语句结束之后,bB返回的值就会自动销毁,但是pp依然指向一个Point值,这个值是不存在的,未定义行为!

为异常安全努力

异常安全,就是要在异常抛出的时候不泄露任何资源,不允许数据败坏。
异常安全的三个保证,满足其中之一即可。
基本承诺:异常抛出,任何事物都可以保持在有效的情况下。
强烈保证:函数成功,就完全成功,失败就回到原来调用之前的状态。可以使用copy-and-swap。
不抛掷承诺:承诺绝不抛掷异常。可以使用noexcept修饰符。
copy-and-swap策略:

struct P{
	std::tr1::shared_ptr<Image> bgImage;
	int imageChanges;
};
class P{
	...
private:
	 Mutex mutex;
	 std::tr1::shared_ptr<P> pImpl; 
};

void P::changeBackgroud(std::istream& imgSrc)
{
	using std::swap;
	Lock m1(&mutex);		//复制mutex一个副本
	std::tr1::shared_ptr<P> pNew(new P(*pImpl));
	pNew->bgImage.reset(new Image(imgSrc));
	++pNew->imageChanges;
	swap(pImpl,pNew);		//交换数据,并释放mutex
}

笔记写的代码实在简洁,有兴趣真的可以去看下书。

C++11提供了个新的noexcept修饰符可以来确保函数不抛出异常。

void fuc() noexcept { throw(); }
//调用这个函数的时候会直接终止程序进而避免异常传播
//C++11出于安全考虑默认把delete函数设为noexcept,同样类的析构函数也是默认为noexcept的

话说这本书可能有的条款有些过时了。

了解内联

调用inline函数可以不产生调用函数的开销,动作也像函数,但是也会造成代码膨胀和效率损失。

inline一般都是置于头文件中,因为编译器要知道他的样子。

然后注意一点,Template也一般在头文件中,而且如果template的所有函数都应该为inline的话,请将这个template声明为inline,但是如果不是所有都需要inline,请不要声明inline,因为内联需要成本,会导致代码膨胀。

inline只是对编译器的一个申请,不是强制的命令,可以通过将函数定义于class定义式中来隐喻声明。

大多数的编译器拒绝将太过复杂的函数inline,而对于virtual函数的调用也都会使得inline落空,因为inline意味着执行前,先将调用动作替换为调用函数的本体,而virtual意味着等待,直到运行期才确定调用哪个函数。

然后构造函数与析构函数往往是内联的糟糕的候选人。因为C++对于对象被创建与被销毁时发生了什么做了各式各样的保证,这些保证有时候就放在构造函数与析构函数中间。

class B{
public:
	...
private:
	std::string bm1, bm2;
}class A: public B{
public:
	A(){}
	...
private:
	std::string dm1, dm2, dm3;
};
//看上去A的构造函数似乎没有任何代码,
//其实C++编译器会为我们做出如下的观念性实现
A::A(){
	B::B();					//初始化B
	try{ dm1.std::string::string(); }	//试图创建dm1
	catch(...){
		B::~B();			//销毁B
		throw;				//抛出异常
	}
	try{ dm2.std::string::string(); }	//试图创建dm2
	catch(...){
		dm1.std::string::~string();		//销毁dm1
		B::~B();			//销毁B
		throw;
	}
	try{ dm3.std::string::string(); }
	catch(...){
		dm1.std::string::~string();
		dm2.std::string::~string();		//销毁dm2
		B::~B();
		throw;
	}
}

//同样也适用于base class,如果B的构造函数被内联了,那么继承与B的A的构造函数被调用的时候也还是会调用B的构造函数。

如果有内联函数的话对于调试也会是一个很大的麻烦,因为不知道如何在一个不存在的函数里面设置断点。

内联的话一般都是内联一些频繁使用、代码量较小的函数。

然后,编程的时候,先不要设置内联,要在进行优化的时候再设置内联函数,来使得潜在的代码膨胀问题最小化并且使得速度提升的机会最大化。

2-8法则:一个程序的80%的执行时间是花费在20%的代码上面的。

将文件的编译依存关系降至最低

C++没有把将接口从实现中分离做的很好。class的定义式里面不仅叙述了class接口,还包括十足的实现细目。

所以如果对C++的某个实现文件做出轻微修改(不是接口,只是实现部分,而且只改private的部分),就会重新编译与连接。

#include "date.h"			//使用头文件使得Person的定义文件与含入的文件之间形成了一个编译依存关系
#include <string>			//只要一个改变,所有相关的都要进行重新编译

class Person{
public:
	...
private:
	Date ss;		//实现细目
};
//这些连串的相互依存关系会对许多项目造成难以形容的灾难

//如果使用前置声明的话,是会避免这种现象,但是有两个问题
namespace std{
	class string//不正确!string不是个类!!
}
class Date//前置声明
class Person{...}//第一个问题是string不是个类,他只是个typedef,第二个是由于编译器必须在编译期间知道对象的大小。
int main(){
	int x;
	Person p(..);		//编译器必须知道p有多大
}
//当然在java上面并没有这个问题,上面代码等同于
int main(){
	int x;
	Person* p;			//编译器只分配一个指针的空间给对象
}
//这是真正的接口与实现分离

分离的关键在于以声明的依存性来替换定义的依存性,这就是依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,就让他与其他文件内的声明式(不是定义式)相依。这个相依的策略是:

如果使用object reference或object pointer可以完成任务,就不要使用object。

如果能够,尽量以class的声明式来替换class的定义式。

为声明式和定义式提供不同的头文件。

//像上述的Person使用pimpl的类往往称为Handle class,要让这种类真正的实现一些东西,就是要将所有的函数转交给相应的实现类并由后者完成实际工作
//下面就是一种Handle class的实现
#include "Person.h"			//正在实现Person类,必须用到定义式
#include "PersonImpl.h"		//必须使用PersonImpl的定义式,否则无法调用其成员函数
							//Person与PersonImpl有着完全相同的成员函数,两者的接口完全相同。
Person::Person(const std::string& name, const Date& bir)
	:pImpl(new PersonImpl(name,bir))
{}

std::string Person::name() const
{
	return pImpl->name();
}
//上面就是让Person构造函数以new来调用PersonImpl构造函数,以及使用Person::name函数来调用PersonImpl::name

//另一种做法就是把Person作为一个抽象的接口,interface class,然后用子类进行真正的具体实现除实体,工厂模式。

//使用Handle class可以解除接口与实现之间的耦合关系,降低文件间的编译依存性
//当然这样也会在运行期间丧失一些速度,为每个对象超额付出一些内存

Handle class与Interface class正是用来设计隐藏实现细节,如函数本体。程序库头文件应该以完全且仅有声明式的形式存在,不管是否涉及template。

然后就是书上有个误译:
参考
所有调用inline的函数如果做出改变就会有必须重新编译。
参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值