您的位置:首页 > 编程语言 > C语言/C++

Effective C++(条款26-31)

2016-01-10 13:02 295 查看
条款26:尽可能延后变量定义式的出现时间

std::string encryptPassword(const std::string& password)
{
...                   //可能抛出异常的部分,比如长度的检查
std::string encrypted(password);
//通过复制构造函数定义并初始化
encrypt(encrypted);
return encrypt;
}


“尽可能延后”的意义是不只是应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。为避免无意义的默认构造行为,应该在变量定义的同时进行初始化。

有循环的变量的定义是应该放在循环外定义还是循环内定义比较好呢?

//A:定义在
循环外
widget w;
for(int i=0;i<n;i++)
{
w=取决于i的某个值;
...
}
/*在widget函数内部,这种写法的成本是:1个构造函数+1个析构函数+n个赋值操作*/

//B:定义在循环内
for(int i=0;i<n;i++)
{
widget w(取决于i的某个值);
...
}
/*这种写法的成本:n个构造函数+n个析构函数*/


因此,除非(1)你知道赋值成本比“构造+析构”成本低(2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B。B中变量的作用域范围更小,有时对程序的可理解性和易维护性更好。

条款27:尽量少做转型动作

先了解下旧式风格的转型操作:

(T)expression;
T(expression);//将expression转型为T


C++提供四种新式转型:

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


const_cast 通常被用来将对象的常量性转除 ,它是这几种里唯一有这种能力的转型操作符。

dynamic_cast 主要用来执行“安全向下转型, 用来决定某对象是否归属继承体系中的某个类型。之所以需要这种类型,通常是想用指向基类对象的指针或引用来处理继承类的对象 , dynamic_cast可能耗费重大的运行成本,在注重效率的代码中应避免使用 。我们一般有两种做法避免这种问题:

1、使用容器并在其中存储直接指向继承类对象的智能指针,示例如下:

class window{...};
class specialwindow:public window{
public:
void blink();
...
};

typedef std::vector<std::trl::shared_ptr<specialwindow>> vpsw;
vpsw winPtrs;
...
for(vpsw::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter)
(*iter)->blink();//在继承体系中,只有specialwindow支持blink()的闪烁效果


2、在基类内提供virtual函数做对各个派生类做的事,可以实现通过基类接口处理派生类的对象

class window{
public:
virtual void blink(){}
...
};
class specialwindow:public window{
public:
virtual void blink(){...};
...
};

typedef std::vector<std::trl::shared_ptr<window>> vpw;
vpw winPtrs;
...
for(vpw::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter)
(*iter)->blink();


reinterpret_cast执行低级转型,实际动作及结果可能取决于编译器,这也就表示它不可以移植。例如将一个指向int的指针转换为int。

用来强迫进行隐式转换,例如将non-const转换为const对象、或将int转换为double等等,也可以执行上述多种转换的反向转换,但是它无法将const对象转换为non-const对象,这个只有const_cast可以办到。

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

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

宁可使用C++类型的转型,不要使用旧式转型。前者很容易辨识出来,而且也比较分门别类的掌职。

条款28:避免返回handles指向对象内部成分

引用、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handles,随之而来的是“降低对象封装性”的风险,同时,它可能导致“虽然调用const成员函数却造成对象状态被改变”。下面例子体现了这个问题。

class Point{
public:
Point(int x,int y);
...
void setX(int newval);
void setY(int newval);
...
};

struct RectData{
Point ulhc;
Point lrhc;
};

class Rectangle{
public:
...
const Point& upperLeft() const{return pData->ulhc;}  //在返回类型上加const就可以轻松去除下面将要提到的两个问题
const Point& lowerRight() const{return pData->lrhc;
...
};

//调用
Point c1(0,0);
Point c2(100,100);
const Rectangle rec(c1,c2);
rec.upperLeft().setX(50);//错误,客户只能读取Point,不能改写它


上面例子提到的两个问题是:

1、成员变量的封装性最多只能等于“返回引用”的函数的访问级别。在本例中,public成员函数返回类中的私有成员变量,让私有成员的访问权限等同于公有的

2、const成员函数传出来一个引用,后者所指的数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

所谓虚吊号码牌:常见的来源就是函数返回值,函数返回的指针或引用是函数体内部的临时变量,在函数体结束时,临时变量会被析构,最终导致指针或引用指向一个不再存在的对象。

条款29:为“异常安全”而努力是值得的

异常安全函数即使异常发生也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

基本型:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。

强烈型:如果异常被抛出,程序状态不变。如果函数成功,就是完全成功,若果函数失败,程序会回到“调用函数之前”的状态。

不抛掷保证:承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能。作用于内置类型身上的所有操作都提供不抛掷异常的保证。这是异常安全码中必不可少的关键基础材料。

异常安全码必须提供上述三种保证之一,如果他不这样做,他就不具备异常安全性。函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的”异常安全保证“中的最弱者。

”强烈保证“往往能够以copy-and-swap实现出来,但强烈安全保证并非对所有函数都可实现或具备现实意义。copy-and-swap策略是为你打算修改的对象(原件)做出一份副本,然后在副本上做一切需要的修改,如有任何修改动作抛出异常,原对象仍保持未改变的状态。待所有改变都成功后,再讲修改过的副本和原对象在一个不抛出异常的操作中置换。

在攥写新码或修改旧码时,请仔细想想怎样让它更具备异常安全性。首先,”以对象管理资源“可以阻止资源泄露,然后,挑选三个”异常安全保证“中的一个实施于你所写的每一个函数身上。

条款30:透彻了解inline的里里外外

inline函数背后的整体观念是:将”对此函数的每一次调用“都是以函数本体替代。将大多数inlining限制在小型、被频繁调用的函数身上,这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使升级的速度提升机会最大化。

inline函数通常一定被置于头文件内,因为编译器在编译期间必须要知道那个函数长什么样子。inlining在大多数C++程序中是编译器行为。模板通常也被置于头文件内,不要因为函数模板出现在头文件中,就将他们声明为inline。

所有对virtual函数的调用都会使inlining落空,因为virtual意味着”等待,在运行期才能确定调用哪个版本的函数“,而inline意味着”执行前,先将调用动作替换为被调用函数的本体“。

策略:一开始不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定要成为inline”或”十分平淡无奇“的函数身上。慎重使用inline函数便是对日后使用调试器带来帮助。不要忘记80-20法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上头。

条款31:将文件间的编译依存关系降至最低

现象: 假设你对C++程序的某个class实现文件做了些轻微的修改。(而且,这里修改的并不是class接口,而是实现,而且只改private成分。

然后重新建置这个程序,你会发现所有的东西都需要重新编译和连接。

原因:问题出在C++并没有把”将接口从实现中分离”做的很好。

Class的定义式不只详细叙述了class接口,还包括十足的实现细目。

#include <string>
#include "date.h"
#include "address.h"
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::string theName;
Date theBirthDate;
Address theAddress;
};


这样一来就在Person定义文件和其含入文件之间形成了一种编译依存关系。

这将会导致如果这些头文件中任何一个被改变,那么每一个含入Person class的文件都需要重新编译,任何使用Person class的文件也要重新编译。

分离的关键在于——用“声明的依存性”替换“定义的依存性”。

这也正是 编译依存性 最小化的本质现实中,让头文件尽可能的自我满足,万一做不到,则让它与其他文件内的声明式(非定义式)相依。

对此,有一个简单的设计策略:

如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。

你可以只靠一个类型声明式就定义出指向该类型的reference和pointer;但如果定义某类型的object,就需要用到该类型的定义式。

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

当声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以 by value方式传递该类型的参数(或返回值)亦然:

class Date;        // class声明式
Date today();        // 没问题,这里并不需要
void clearAppointments(Date d);        // Date的定义式


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

根据上面的准则,我们需要两个头文件,一个用来声明,一个用来定义。而且这两个文件必须保持一致性,因此程序库客户应该总是#include一个生命稳健而非前置声明若干函数,程序库作者也应该提供这两个头文件。

此外,关于 handle classinterface class

1.像Person这样使用 pimpl idiom 的class,往往被称为 handle classes。

然而,handle class如何工作呢?

#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 birthDate() const;
std::address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;  /*main class(Person)只内含一个指针成员(这里使用了 tr1::shared_ptr),指向其实现类(PersonImpl)。这样的设计常被称为 pimpl idiom(就是 pointer to implementation)。*/
};


① 它们的所有函数转交给相应的实现类并由后者完成实际的工作。

比如:

#include "Person.h"
#include "PersonImpl.h"

Person::Person(const std::string& name,const Date& birthday,const Address& addr) : pImpl(new PersonImpl(name,birthday,addr)
{}

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


② 令Person成为一种特殊的 abstract base class(抽象基类),称为Interface class。

这种class是用来 描述 derived class的接口,因此它通常不带成员变量,也没有构造函数,只有一个 virtual析构函数以及一组 pure virtual函数,用来叙述整个接口。

class Person  {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
};


这个class的客户必须以 Person 的 pointers 和 reference 来撰写应用程序,因为它不可能针对“内含pure virtual函数”的Person class 具体出实体。就像Hnadle clas的客户一样,除非Interface class的接口被修改否则其客户不需要重新编译。

Interface class的客户必须有办法为这种class创建新对象。

方法1:通常调用一个特殊函数,此函数扮演“真正将被具现化”的那个derived class的构造函数角色。这样的函数通常称为factory函数或virtual构造函数。它们返回指针,指向动态分配所得对象,而对该对象支持Interface class的接口。这样的函数又往往在Interface class内被声明为static:

class Person  {
public:
...
static std::tr1::shared_ptr<Person>     // 返回一个tr1::shared_ptr,指向一个新的Person,并以给定之参数初始化。条款18 告诉你,为什么返回tr1::shared_ptr
create(const std::string& name,
const Date& birthday,
const Address& addr);
...
};


客户会这样使用它们:

std::string name;
Date dateOfBirth;
Address address;
...
// 创建一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name,datefBirth,address);

...
std::cout<<pp->name()            // 通过Person接口使用这个对象
<<" was born on "
<<pp->birthDate()
<<" and now lives at "
<<pp->address();
...                    // 当pp离开作用域,对象会被自动消除


请记住

1.支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle class 和 Interface class

2.程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及template 都适用
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: