Effective C++学习笔记五:实现

二十六:Postpone variable definitions as long as possible. 尽可能延后变量定义式的出现时间。

定义一个变量而其类型带有构造或者析构函数,那么程序的控制流到达这个变量定义式,就会有构造成本。

所以延迟定义,在需要的时候去定义它。

例子:

std::string encryptPassword(const std::string& password)
{
	using namespace std;
	string encrypted;
	if (password.length() < MinimumPasswordLength)
	{
		throw logic_error("Password is too short");
	}

	return encrypted;
}

如果中途throw了,encrypted就没有用到,所以可以延迟string的定义。

对于关于循环的操作

Widget w;
for (int i = 0; i < n; ++i)
{
	w = ...;
}

for (int i = 0; i < n; ++i)
{
	Widget w(...);
}

对于第一种,一个构造 一个析构 n个赋值操作

对于第二种,n个构造,n个析构。

如果一个赋值操作成本低于一组构造+析构成本,a大体好些,尤其是n很大的时候,否则就是b好。

除非1.赋值成本比构造+析构成本低2.正在处理代码中效率高度敏感的部分,否则应该使用做法B。

总结:

尽可能延后变量定义式的出现。这样可增加程序的清晰度并改善程序效率。

二十七:Minimize casting.尽量少做转型动作。

c风格的转型动作:

(T)expression

函数风格的转型动作:

T(expression)

c++的四种转型:

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

例子:

class Window
{
public:
	virtual void OnResize() {}
};

class SpecialWindow :public Window {
public:
	virtual void OnResize()
	{
		static_cast<Window>(*this).OnResize();//这边有问题
	}
};

这边并非在当前对象上调用Window::onResize,而是在当前对象的base class成分的副本上调用Window::Onresize。

如果Window::OnResize修改了对象的内容,当前对象没有被改动,改动的是副本。如果SpecialWindow::OnResize内如果也修改对象,当前对象会改动。所以会出现baseclass更改没有落实而derivedclass成分的更改落实了。

可以用

Window::OnResize();代替上面的转型操作。

尽量避免dynamic_cast 消耗可能会更大

试着不要这样做:

class Window {};
class SpecialWindow : public Window {
public:
	void blink();
};

typedef std::vector<std::tr1::shared_ptr<Window> > VPM;
VPM winPtrs;

for (VPM::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
	{
		psw->blink();
	}
}

改为:

typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSM;
VPSM winPtrs;
for (VPSM::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	(*iter)->blink();
}

注:尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进自己的代码里。

宁可使用新式转型。

二十八:Avoid returning "handles" to object internals。避免返回handles指向对象内部成分

refrences, 指针和迭代器统统是所谓的handles(用来获取某个对象)。而返回一个“代表对象内部数据”的handle,随之而来

的便是“降低对象封装性”的风险。

不应该令成员函数返回一个指针指向“访问级别较低”的成员函数,如果这么做,后者的实际访问级别就会提高如同前者,如果要这么做,注意const的使用。

指向内部数据还可能带来空数据的问题(悬吊指针等等)。需要注意判空。

注:

避免返回handles指向对象内部。遵循这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。

二十九:Strive for exception-safe code. 为“异常安全”而努力是值得的。

例子:

class PrettyMenu
{
public:
	void ChangeBackground(std::istream imgSrc); //改变背景图像
private:
	Mutex mutex;//互斥器
	Image* bgImage;//目前的背景图像
	int imageChanges;//背景图像被改变的次数。
};

//实现一:
void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	lock(&mutex);//取互斥器
	delete bgImage;//
	++imageChanges;
	bgImage = new Image(imgSrc);
	unlock(&mutex);//释放互斥器
}

这个实现在异常安全性上很糟糕。

“异常安全”有两个条件:

1.不泄露任何资源。如果上面的new Image(ImgSrc)导致异常,对unlock的调用绝不会执行,于是互斥器永远被把持住了。

2.不允许数据败坏。

确保锁被释放:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	Lock m1(&mutex);
	delete bgImage;
	++imageChanges;
	bgImage = new Image(imgSrc);
}

关于数据破坏问题:

class PrettyMenu
{
	std::tr1::shared_ptr<Image> bgImage;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	Lock m1(&mutex);
	bgImage.reset(new Image(imgSrc));

	++imageChanges;
}

通过只能指针管理资源,并且将计数放在资源设置之后。

一种新的策略:

copy and swap :为你打算修改的对象做出一份副本,然后再副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未修改状态。待所有改变都成功后,再将修改后的那个副本和原对象在一个不跑出异常的操作中置换。

struct PMImp1 {
	std::tr1::shared_ptr<Image> bgImage;
	int imgaeChanges;
};

class PrettyMenu {
private:
	Mutex mutex;
	std::tr1::shared_ptr<PMImp1> pImp1;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	using std::swap;
	Lock m1(&mutex);
	std::tr1::shared_ptr<PMImp1> pNew(new PMImp1(*pImp1));

	pNew->bgImage.reset(new Image(imgSrc));
	++pNew->imageChanges;

	swap(pImp1, PNew);
}

三十:Understand the ins and outs of inlining透彻了解inlining的里里外外

inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内。

class Person
{
public:
	int age() const { return theAge; }//一个隐喻的inline申请:age被定义在class定义式内

private:
	int theAge;
};

inlining在大多数c++程序中是编译期行为,少部分是运行期行为,所以Inline函数通常一定被置于头文件内。Templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。

Template的具现化于inlining无关,如果所有根据此template具现化出来的函数都应该inlined,可以将此template声明为inline,

否则就不要声明为inline。

对virtual函数调用会使inlining落空,因为virtual意味着运行期间才知道调用哪个。

编译器通常不对“通过函数指针而进行的调用”实施inlining,所以调用方式也会影响到inline。

inline void f() {}
void(*pf)() = f;


f();//这个调用将被inlined,因为它是一个正常调用
pf();//这个调用或许不被inlined,因为它通过函数指针达成。

inline函数无法随着程序库的升级而升级,如果inline函数修改了,所有用到f的客户端程序必须重新编译。

注:

将大多数inlining限制在小型,被频繁调用的函数上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

不要只因为function tempaltes出现在头文件,就将它们声明为inline。

三十一:Minimize compilation dependencies between files. 将文件间的编译依存关系降至最低。

#include

如果头文件中有任何一个改变,或这些头文件所依赖的其他头文件有任何改变,相关的文件都会重新编译。

考虑一种设计:

#include <string>
#include <memory>
class PersonImp1;
class Date;         
class Address;

class Person
{
public:
	Person(const std::string& name, const Date& birthday, const Address& addr);

	std::string name() const;
	std::string birthDate() const;
	std::string address() const;

private:
	std::tr1::shared_ptr<PersonImp1> pImp1; //指针, 指向实现物。 std::tr1::shared_ptr
};

这里主类 Person内只含有一个指针成员指向其实现类。这种设计一般被称为 “pointer to implementation”.

这样Person的客户就完全与Dates, Addresses以及Persons的实现细目分离了,那些classes的任何实现修改都不需要Person客户端重新编译。此外由于客户无法看到Person的实现细目,也就不可能写出什么“取决于那些细目”代码,实现接口与实现分离。

(貌似 UE4 的umg实现是这种结构,umg类封装slate类的指针。)

注:

如果使用objects references 或 object pointers可以完成任务,就不要使用objects。因为后者需要定义式。

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

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

例如标准程序库头文件的<iosfwd>。 <iosfwd>内含iostream各组件的声明式,其对应定义分布在若干不同的头文件内,包括

<sstream>, <streambuf>, <fstream>和<iostream>

最后:

#include "Person.h"
#include "PersonImp1.h"

Person::Person(const std::string& name, const Data& birthday, const Address& addr)
	:pImp1(new PersonImp1(name, birthday, addr))
{

}

std::string Person::name() const {
	return pImp1->name();
}

像Person这样使用pimpl idiom的classes, 往往被称为Handle classes.

另一种制作Handle class的办法是,令Person成为一种特殊的abstract base class,称为 Interface class。

一个针对Person而写的Interface class或许看起来像这样

class Person
{
public:
	virtual ~Person();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
	virtual std::string address() const = 0;
};
	static std::tr1::shared_ptr<Person>
		create(const std::string& name, const Date& birthday, const Address& addr);

提供一个工厂函数。

std::string name;
Date dateOfBirth;
Address address;

//...
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
发布了35 篇原创文章 · 获赞 5 · 访问量 409

猜你喜欢

转载自blog.csdn.net/qq_33776188/article/details/103123915