Item26:Postpone variable definitions as long as possible.尽可能延后变量定义的出现时间
这里所说的变量是variable of type,它需要调用constructor和destructor。
简单情况下,如果函数抛出异常,导致变量没有被使用到,但是由于已经定义过,其实还是需要调用它的constructor和destructor。
std::string encryptPassword(const std::string& password)
{
…
std::string encrypted; //这是错误的写法,这个定义已经需要调用构造函数了。
encrypted = password;
encrypt(encrypted);
return encrypted;
}
正确的写法:
std::string encryptPassword(const std::string& password)
{
…
std::string encrypted(password); //正确的写法。
encrypt(encrypted);
return encrypted;
}
postpone有两层含义:
1 postpone a variable’s definition until right before you have to use the variable。
2 postpone the definition until you have initialization arguments for it.
循环怎么办?
最常见的写法一种是在循环体里面定义变量,另一种是在循环体执行前定义一个变量。
|
A
|
B
|
approach
|
Widget w;
for(int i=0; i<n;++i)
{
w=取决于i的某个值;
…
}
|
for(int i=0; i<n;++i)
{
Widget w(取决于i的某个值);
…
}
|
开销
|
1 constructor + 1 destructor + n assignment
if assignment小于 constructor-destructor对,这种写法比较有效
|
n constructor + n destructor
|
defect
|
这种方法定义的变量在较大的范围都是可见的,不好。
|
|
结论:除非你明确知道approach A的开销小于approach B,否则尽量使用approach B。
Item 27:Minimize casting尽量少做转型动作
C++提供的4中cast形式
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
尽量使用这些新风格的转型,最起码的它好认啊。
cast,到底做了什么?
基类和派生类的指针有时是不相等的,存在一个偏移量offset。single object might have more than one address。为啥说有时呢,因为这取决于Compiler。所以不能想当然在基类指针上加个值就得到什么。
dynamic_cast
cast并不是啥都没有做的,它会创建一个临时的基类的copy,然后调用的是这个临时类的方法,和当前被cast类无关哦。
同时多级的继承关系里面cast也要通过字符串比较来确定匹配的基类,因此如果对性能有要求,就尽量少使用cast。
解决方法
有两种:
1 使用type-safe的容器
这种方法的缺陷是容器里面只能允许有1种类型的指针
Example见P121
2 在基类里面定义虚函数
class Window{
public:
virtual void blink();
…
};
class SpecialWindow : public Window {
public:
virtual void blink();
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
…
for(VPW::iterator iter=winPtrs.begin(); iter!=winPtrs.end(); ++iter)
(*iter).blink(); //注意这里就不需要dynamic_cast
Item 28:Avoid returning "handles" to object internals.避免返回handles指向对象内部成分 123
handles
handles可以理解为句柄,或者说是一个“把柄”,握着这个“把柄”,你可以修改对象的数据成员。
handle包括reference,pointer,iterator。
问题出现的背景
很简单,就是看上去函数返回的是一个const对象,但是由于是handle,导致数据还是可以被client修改的。这种情况,debug时很难发现,而且给人的感觉是系统不稳定,后果很严重,不可轻视。
struct rectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
public:
…
Point& upperLeft() const {return pData->ulhc; }
private:
std::tr1::shared_ptr<RecData> pData;
};
const member function:不能修改数据成员。
client调用:
const Rectangle rec(Point (0,0), Point(100,100));
rec.upperLeft().setX(50);//坐标就这么不知不觉中被修改了
结论
避免返回指向对象内部的handles,这种handles包括:pointer,reference,iterator。
如果某个函数的访问级别低,禁止其它成员函数返回指向这个函数的指针。因为如果这样,就可以通过函数指针来调用这个函数了。
Item 29:Strive for exception-safe code.为“异常安全”而努力是值得的 127
class PrettyMenu{
public:
void PrettyMenu::changeBackgroud(std::istream& imgSrc) ;
private:
Mutex mutex;
int imageChanges;
Image * bgImage;
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
Lock m1(&mutex); //Item13,对象管理资源的典型应用
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
异常安全性函数应该提供以下3种保证之一:
基本保证
如果异常抛出,程序里所有的东西都保持有效的状态。
增强保证
如果异常抛出,程序的状态保持不变。
不抛出异常保证
决不抛出异常。因为总能够完成希望要做的事情。
这同时也是一种理想状态。
一般情况下,我们的选择是在basic和strong异常保证之间。
重新写changeBackgroud,提供basic异常保证:
class PrettyMenu{
…
std::tr1::shared_ptr<Image> bgImage;
…
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
Lock m1(&mutex); //Item13,对象管理资源的典型应用
bgImage.reset(new Image(imgSrc) );
++imageChanges;
}
strong异常保证的设计策略说出来也很简单,就是copy and swap。具体描述见P131 L5开始的描述。
strong异常保证
基本思路就是copy and swap。
struct PMImpl {
std::tr1::shared_prt<Image> bgImage;
int imageChanges;
};
class PrettyMenu{
…
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackgroud(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex); //Item13,对象管理资源的典型应用
std::tr1:: shared_prt< PMImpl > pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
++pNew-> imageChanges;
swap(pImpl, pNew); //置换
}
trade-off折中考虑异常保证
并不是一定要提供strong异常保证就是好的,这里是需要一个trade-off。strong异常保证的代价一定是很大的,efficiency,complexity都是要考虑的因素。
同时如果函数中调用了没有异常保证的其它函数,这个函数也不能是异常保证的。
void someFunc()
{
…
f1();
f2();
…
}
这引申出一个结论:如果代码中调用了没有异常保证的C的传统代码,这样的函数,就大可不必还要提供strong的异常保证了
Item 30:Understand the ins and outs of inlining.透彻了解inlining的里里外外 134
什么是内联函数inline
将函数指定为 inline 函数,(通常)就是将它在程序中每个调用点上“内联地”展开。函数本体Function body是不存在的。
inline是对Compiler的请求,而不是对命令的。
The inline specification is only a request to the compiler. The compiler may choose to ignore this request.
一般来说,内联机制适用于优化小的、只有几行的而且经常被调用的函数。
大多数的编译器都不支持递归函数的内联。
复杂函数也是不可以的,一个 1200 行的函数也不太可能在调用点内联展开。
虚函数也是不可以的。
inline与函数模板Function Template
template<typename T>
inline const T& std::max(const T& a, const T& b)
{ return a<b?b:a; }
inline函数大多数都是定义在头文件了,这是因为编译器必须知道inline函数是什么样子。
Template也是定义在头文件里,因为Compiler需要知道Template是什么样子,为了初始化Template。
Template独立于inline。这是不同的概念,当然函数模板也可以声明成inline。
函数指针和内联函数
函数指针指向的内联函数一定不会被inline,Compiler会生成这个函数的本体function body,函数指针就是指向的这个函数本体。
inline void f() {…}
void (*pf)() = f;
…
f();
pf();//这个调用就不会是inline,因为这是一个函数指针。
构造函数、析构函数和内联函数
构造函数和析构函数不要做内联。
简单的说,如果base constructor内联,派生类中从基类继承来的data member被初始化2次,一次是来自内联,一次来自C++执行基类的构造函数。
同理,析构函数,也就会被释放2次L惨了,但是实际上,只有一个对象啊。不死等什么。
内联函数修改的影响
内联函数的机制决定调用内联函数,就是把代码插入调用的位置,因此如果内联函数修改了,所有调用该函数的地方都需要重新编译。如果这个函数不是内联的,修改后,只要重新链接就可以了。
记住
inline仅仅适用于小型的,被频繁调用的函数。
不要因为函数模板定义在头文件里就把它声明成incline,Function Template和inline这是2个不同的概念。
Item 31:Minimize compilation dependencies between files.将文件间的编译依存关系降至最低 140
前向声明forward declaration
1. 首先前向声明仅仅是声明了一个类
class Date; //forward declaration
class Address;
2. 其次前向声明是一种不完全类型
只能以有限的方式使用。只能用于定义指向该类型的指针,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。不能定义该类型的对象。
pimpl:pointer to implementation
#include <string>
#include <memory>
class PersonImpl;
class date;
class Address;
class Person {
public:
Person(const std::string& name, const date& birthday, const Address& addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
…
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //point to implementation
};
基本解决之道
replacement of dependencies on definitions with dependencies on declarations.以声明的依赖性替换定义的依赖性
尽量让头文件做到自包含,万一做不到,则让它和其它文件内的声明相互依赖,而不是定义。
规则
如果使用对象引用活对象指针可以解决问题,就避免使用对象,
取代类定义,依赖于类声明
头文件要成对出现,一个是声明,一个是定义。
两种具体的操作方法
Handle Classes
定义Handle class
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const=0;
std::string birthday() const=0;
std::string address() const=0;
…
private:
std::tr1:shared_ptr<PersonImpl> pImpl;
};
std::string Person::name() const
{
return pImpl->name();
}
定义实现类PersonImpl
Handle Classes
这里给出的Handle Classes的定义太精辟了。喜欢J
Classes like Person that employ the pimpl idiom are often called Handle classes.
Making Person a Handle class doesn’t change what Person does, it just changes the way it does it.
Interface Classes
定义接口
class Person {
public:
virtual ~Person();
virtual std::string name() const=0;
virtual std::string birthday() const=0;
virtual std::string address() const=0;
…
static std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr );
};
定义实现
class RealPerson:public Person {
public:
realPerson(const std::string& name, std::string& birthday, const Address& addr)
:theName(name),theBirthdate(birthday), theAddress(addr)
{}
virtual ~RealPerson()
std::string name() const;
std::string birthday () const;
std::string address () const;
private:
std::string theName;
date theBirdate;
Address theAddress;
};
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr )
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));
}
结论
减少耦合行的惯用的思路是用声明依赖替代定义依赖。有2种方法:Interface classes和Handle classes.
任何减少耦合的做法都是会带来负面影响的:占用内存增加,间接调用。但是关键还是在于引用本身是不是对这些额外的开销敏感。