(Effective C++)第五章 实现(Implementation)

7.1 条款26:尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)

只要你定义一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你得承受构造成本;当这个变量离开作用域时,你得承受析构成本。
//这个函数过早定义变量”encrypted”
std:: string encryptPassword(const std::string & password)
{
use namespace std;
string encrypted;
if (password.length() < PASSWD_MIN_LEN)
{
    Throw logic_error("Password is too short");
}
 …
return encrypted;
}
示例7-1-1  加密函数过早定义变量
如果有个异常抛出出去,他就真的没有被使用,但是仍然付出了encrypted的构造和析构成本。更受欢迎的做法是:
std:: string encryptPassword(const std::string & password)
{
。。。 //检查长度
string encrypted(password); //过个copy构造函数定义并初始化
encrypt(encrypted)
return encrypted;
}
示例7-1-2  加密函数的改进
这段代码有两处改进:一是通过制作在构造时指定初值,比先构造对象再赋值效率高;二是延后定义,省去了检查长度抛异常的构造和析构成本。
但是循环咋办?
// 方法A:定义于循环体外
Widget w;
for (int i = 0; i < n; i++)
{
w = 取决于i的取值; //

}    // 方法B:定义于循环体内

for (int i = 0; i < n; i++)
{
Widget w(取决于i的取值); //

}
示例7-1-3  循环体与变量定义
做法A:1个构造函数+1个析构函数+n个赋值操作
做法B:n构造函数+n个析构函数
尤其是当n值很大士,做法A大体上比较高效。

7.2 条款27:尽量少做转型动作 (Minimize casting)

C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casting)破坏了类型系统(type system)。
C风格的转型动作:
(T)expression           //将expression转型为T
函数风格的转型动作:
T(expression)          //将expression转型为T
两种形式并无差别,我们称此为“旧式转型”(old-sytle casts)。
C++还提供四种新式转型:
const_cast(T)( expression )
dynamic_cast(T)( expression )
reinterpret _cast(T)( expression )
static_cast(T)( expression )
各自不同的目的:
?    const_cast通常被用来将对象的常量性转换(cast away the constness)。是唯一有此能力的C++ Sytle转型操作符。
?    dynamic主要是直线“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。耗费最大的转型动作。
?    reinterpret意图直线低级转型,实际动作可鞥呢取决于编译器,这也就表示不可移植。
?    static_cast用来强迫隐式转换(implicit conversion),例如,将non-const转为cosnt对象,或将int转换为double等等。它可以执行上述多种转换的反向转换,例如,将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但是无法将const转为non-const——这个只有const-cast才办得到。

旧式转换仍然合法,但是新式转换更受欢迎。
使用旧式转换的时机是,当我调用一个explicit构造函数将一个对象传递给一个函数时。例如:
class Widget
{
 public:
    explicit Widget(int size);
};
void doSomeWork(const Widget &w);
doSomeWork(Widget(15));  //以一个int加上函数风格的转型动作创建一个Widget
doSomeWork(static_cast<Widget>(15));  ///以一个int加上C++风格的转型动作创建一个Widget
示例7-2-1  explicit构造函数转
任何一个类型转换往往真的令编译器编译出运行期间运算的码。
class Base{…};
class Derived : public Base {…};
Derived d;
Base *pb = &d;      //隐式将Derived*转换为Base* //隐患
这个例子表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如,“以Base*指向它”时的地址和“以Derived*指向它”时的地址)。即使在单一继承中也可能发生。对象的布局方式和它们的地址计算方式随编译器不同而不同,那意味着,在某一平台行得通,而在其他平台可鞥行不通。
class Window
{
 public:
    virtual void onReszie() {…}
};
class SpecialWindow:public Window
{
 public:
    virtual void onReszie()
{
    Stat          static_cast<Window>(*this).onResize();  //将*this转型为Window,调用其
onResz                                                      onReszie
     …    //这里进SpecialWindow专属行为
}
};
示例7-2-2  错误的转型动作
上述例子表明,它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象之base class成分”的暂时副本身上的onResize!它是在“当前对象之base class成分”的副本上调用Window::onResize,然后再当前对象身上执行SpecialWindow的专属动作。如果Window:: onReszie修改了对象内容,当前对象其实没有改动,改动的是副本。然而SpecialWindow:: onReszie如果也修改对象,当前对象真的会被修改。这种当前对象进入一种“伤残”状态:其base clas成分的更改没有落实,而derived class的成分修改倒是落实了。
解决之道是拿掉转型动作。
class SpecialWindow:public Window
{
 public:
    virtual void onReszie()
{
    Stat          Window::onResize();  //调用Window::onResize作用于*this身上           
     …    //这里进SpecialWindow专属行为
}
};
示例7-2-3  拿到错误的转型动作
之所以需要dynamic_cast,通常是因为你想在一个认定为derived class对象身上执行derived class操作函数,但是你手上只有一个指向base的pointer货reference,你只能靠他们来处理对象。有两个一般性做法可以避免这个问题:
第一,使用容器并在其中存储直接指向derived class对象的制作,如此便消除了“通过base class接口处理对象”的需要。
class Window {。。。};
class SpecialWindow:public Window
{
 public:
    void blink();
    …
 };
typedef std::vector<std::tr1:shared_ptr<Windows> > VPW;
VPW winPtrs;

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end();iter++)
{
   // 不希望使用dynamic_cast
   if (SpecialWindow *psw = dynamic_cast< SpecialWindow *> (iter->get()))
{
    psw->blink();
}
}
// 应该改而这样做
typedef std::vector<std::tr1:shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end();iter++)
{
     (*iter)->blink();  //OK
}
示例7-2-3  使用容器存储derived对象指针
第二,让你通过base class接口处理“所有可能之各种Windows派生类”,那就是在base class内提供virtual函数做你想对各个Window派生类做的事。
class Window
{
public:
  virtual void bink() {}
};
class SpecialWindow:public Window
{
 public:
    virtual void blink(){};
 };
typedef std::vector<std::tr1:shared_ptr<Windows> > VPW;
VPW winPtrs;

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end();iter++)
{
   // 这里没有dynamic_cast
 (*iter)->blink();

}
示例7-2-4  使用虚函数避免转型动作
绝对必须避免的一件事是所谓的“连串(cascading) dynamic_cast”,如下

class Window {};
class SpecialWindow1:public Window{};

typedef std::vector<std::tr1:shared_ptr<Windows> > VPW;
VPW winPtrs;

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end();iter++)
{
   // 连串dynamic_cast
 if (SpecialWindow *psw1 = dynamic_cast< SpecialWindow1 *> (iter->get()))
{}
else if(SpecialWindow *psw2 = dynamic_cast< SpecialWindow2 *> (iter->get()))
{}
else if(SpecialWindow *psw2 = dynamic_cast< SpecialWindow2 *> (iter->get()))
{}

}
示例7-2-5  连串dynamic_cast转型动作

7.3 条款28:避免返回Handles指向对象内部成分 (Avoid returning “handles” to object internals)

?    注意事项
避免返回handles(包括reference,指针,迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
假设存在这样的矩形:

class Point {  //这个类表示点
public:
Point (int x, int y);
void setX();
void setY();
};
struct RectData{
 Point ulhc;   //左上角(upper left-hand corner)
Point lrlhc;   //右下角(lower right-hand corner)
}
class Rectangle {
Point & upperLeft() const {return pData->ulhc;}
Point & lowerRight() const { return pData->lrhc;}
private:
std::tr1::shared_ptr< RectData > pData;
};

//客户这样使用
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1, coord2);  // 从(0,0)到(100,100)
rec.upperLeft().setX(50);   //从(50,0)到(100,100)
示例7-3-1  非正常封装
这里请注意,upperLeft的调用者能够使用被返回的reference来更改成员,但是rec其实应该是不可变的(const)。给我们的教训,第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。第二,如果 const成员函数传出一个reference(non-const),后者所指数据域对象自身有关联,而它有存储于对象之外,那么这个函数的调用者可以修改那个数据。
正确的做法:
class Rectangle {
cosnt Point & upperLeft() const {return pData->ulhc;}
cosnt Point & lowerRight() const { return pData->lrhc;}
private:
std::tr1::shared_ptr< RectData > pData;
};
示例7-3-2  正常封装
即便如此,upperLeft和lowerRight还是返回了“代表对象内部(成员变量和不被公开的成员函数)”的handles,有可能导致dangling handles(空悬的句柄)。例如:

class GUIObject {…};
const Rectangle boundingBox(const GUIObject &obj);
GUIObject *pgo;

Const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
示例7-3-3  空悬的句柄
我们假设boundingBox(*pgo)的调用者获得一个新的、暂时的Rectangle对象,姑且叫做temp。但是在执行temp.upperLeft()后,temp将被销毁,而那间接导致temp的Points析构。最终导致pUpperLeft指向一个存在的对象,变成了空悬、虚吊。

7.4 条款29:为” 异常安全”而努力是值得的 (Strive for exception-safe code)

假设有个class用来表现夹带背景图案的GUI菜单,用于多线程环境。所以,他有个互斥器:

class PrettyMenu {
public:

void changeBackground(std::istream &imgSrc);

private:
Mutex mutex;  //
Image * bgImage;  //当前的背景图像
int imageChanges;  //背景图像被改变次数
};
void PrettyMenu:: changeBackground(std::istream &imgSrc)
{
  Lock(&mutex);  //取得互斥器(见条款14)
  Delete bgImage;
  ++ imageChanges;
  bgImage = new Image(imgSrc);
  unlock(&mutex);
}
示例7-4-1  非异常安全函数
从异常安全的观点来看,这个函数很糟。
当异常安全抛出时,带有异常安全性的函数会:
?    不泄露任何数据: 上述没有做到这点,因为一旦new Image(imgSrc)导致的异常,对unlock的调研就绝对不会执行,于是互斥器就永远把持住了。
?    不允许数据败坏:如果new Image(imgSrc)抛出异常,bgImage就是执行一个已经被删除的对象,imageChanges也被累加

异常安全函数(Exception-safe functions)提供以下三个保证之一:
?    基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
?    强烈保证:如果异常被抛出,程序状态不变。
?    不抛掷(nothrow)保证:承诺绝不会抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等)身上的所有操作都提供nothrow保证。

因此,我们的改进地方,如下:
class PrettyMenu {

private:
Mutex mutex;  //
std::tr1::shared_ptr<Image> bgImage;  //当前的背景图像
int imageChanges;  //背景图像被改变次数
};
void PrettyMenu:: changeBackground(std::istream &imgSrc)
{
  Lock m1(&mutex);  //资源管理器类(见条款14)
//以new Image的执行接口设定bgImage内部指针
  bgImage.reset(new Image(imgSrc));  
  ++ imageChanges;
}
示例7-4-2  基本异常安全函数
注意:这里不再手动删除旧图像,因为这个动作已经由智能指针完成。此外,删除动作只发生在新图像被成功创建之后。tr1::shared_ptr::reset函数只有在其参数被成功生成之后才会被调用。delete只在reset函数内部被使用,所以如果从未进入那个函数也就绝不对使用delete。
但是美中不足的是参数imgSrc。如果Image构造函数抛出异常,有可能输入流(input stream)的读取记号已被移走。
一般化的设计策略是copy and swap。原则很简单:为你打算修改的对象做出一份副本,然后在那副本身上做一切必要的修改。实际上讲所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,执行那个所谓的实现对象(implementation object,即副本)。这种手法被称为pimpl idiom,条款31详细介绍。典型写法如下:
struct PMImpl {  //PMImpl=PrettyMenu Impl
std::tr1::shared_ptr<Image> bgImage;  //当前的背景图像
int imageChanges;  //背景图像被改变次数
};
class PrettyMenu {

private:
Mutex mutex;  //
std::tr1::shared_ptr<PMImpl> pImpl;  
};
void PrettyMenu:: changeBackground(std::istream &imgSrc)
{
  using std::swap;  //见条款25
  Lock m1(&mutex);  //资源管理器类(见条款14)
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  //
  pNew->bgImage.reset(new Image(imgSrc));  //修改副本
  ++ pNew->imageChanges;
swap(pImpl, pNew);   //置换数据,释放mutex
}
示例7-4-3  强烈异常安全函数
此例之中选择让PMImpl成为一个struct而不是一个class,这是因为PrettyMenu的数据封装性已经由于“pImpl是private”而获得了保证。

7.5 条款30:透切了解inlining的里里外外 (Understand the ins and outs of inlining)

编写内联函数就像现实生活一样,没有白吃的午餐。inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。这样做增加你的目标码大小。如果在一台有限内存机器上,过度热衷inlining会造成程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀会导致额外的换页行为,降低指令高速缓存装置的击中率,从而降低效率。
记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。

class Person {
public:
int age() const{ return theAge}  //一个隐喻的inline申请,age被定义于class内
private:
int ethAge;  
};
示例7-5-1  一个隐喻的inline申请
这样的函数通常是成员函数,但是条款46说friend函数也可被定义于class内,如果真是这样,他们也是被隐喻声明为inline。
下面是标准的max template(来自<algorithm>)的实现:
template<typename T>
inline const T& std::max(const T& a, const T& b) {
  return a < b? b:a;
};
示例7-5-2  一个显示的inline申请
Template的具体化与inlining无关。
一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别:如果他们无法将你要求的函数inline话,会给你一个警告信息(见条款53)。
如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。例如:
inline void f() {…}        //假设编译器有意愿inline“对f的调用”
void (*pf)() = f;           //pf指向f

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

程序库设计者必须评估“将函数声明为inline”的冲击,inline函数无法随着程序库的升级而升级。一旦程序库设计中决定改变f,所用用到f的客户端程序都必须重新编译。
不要忘记80-20经验法则:平均而言,一个程序往往将80%的执行时间花费在20%的代码上。这是一个重要法则。

7.6 条款31:将文件间的编译依存关系降至最低 (Minimize compilation dependencies between files)

C++并没有把“将接口从实现中分离”这事做得很好。Class的定义式不只详细叙述了class接口,还包括十足的实现细目,例如:

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;      //实现细目
};
示例7-6-1  接口与实现未分离
这里的class person无法通过编译——除非取得其实现代码所用到的class string,Date和Address的定义式。这样的定义式通常有#include指示符提供,比如如下的东西:
#include <string>
#include “date.h”
#include “address.h”
针对Person我们可以这样做:把Person分割为两个classes,一个只提供接口(Person),另一个赋值实现该接口(PersonImpl)。

#include <stirng>  //标准程序库组件不该被前置声明
#include <memory>
Class PersonImpl; //Person实现类的前置声明
Class Date;      //Person接口用到的classes的前置声明
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<PersonImpl> pImpl;  //指针,指向实现类,见条款13
};
示例7-6-2  接口与实现分离(Handle Classes)
这就是真正的“接口与实现分离”。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质。其他每一件事都源自这个简单的设计策略:
?    如果使用Object reference或Object pointers可以完成任务,就不要使用objects。
?    如果能够,尽量以class声明式替换class定义式。例如
class Date;
Date today();
Void clearAppointments(Date d);
声明today和clearAppointments函数而无需定义Date。
?    为声明式和定义式提供不同的头文件。当然这些文件必须保持一致,如果有个声明式被改变了,两个文件都得改变。程序库客户应该总是#inlucde一个声明文件。
C++也提供关键字export,允许将template声明式和template定义式分割与不同的文件。不幸的是,支持该关键字的编译器非常少。
像Person这样使用pimpl idiom的classes,被称为Handle classes。而另一个制作Handle classes办法是,令Person成为一致特殊的abstract base class,称为Interface class。如下
class Person{
public:
Person(const std::string &name, const Date &birthday, const Address &addr);
static std::trl::shared_ptr<Person>  //条款18和条款13,factory函数
create(const std::string &name,
const Date &birthday,
 const Address &addr);
virtual ~Person();
virtual std::string name const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
};
std::trl::shared_ptr<Person>  //条款18和条款13,factory函数
Person::create(const std::string &name,
const Date &birthday,
 const Address &addr)
{
return
std::trl::shared_ptr<Person> (new RealPerson(name, birthday,address));
}
class RealPerson : public Person{
public:
RealPerson(const std::string &name, const Date &birthday,
 const Address &addr):theName(name),theBirthDay(birthday),
theAddress(addr)
{
}
virtual ~RealPerson();
std::string name const;
std::string birthDate() const;
std::string address() const;
};

//客户这样使用
std::string name;
Date dateOfBirth;
Address address;
//创建一个对象
std::trl::shared_ptr<Person> pp(Person::create(name, birthday,address));
std::cout<<pp->name()<<endl;
示例7-6-3  接口与实现分离(Interface Classes)
当然支持Interface class接口的那个具体类(concrete classes)必须被定义出来,而且真正的构造函数必须被调用。RealPerson示范实现Interface class的两个最常见的机制之一:从Interface class继承接口规格,然后实现出接口所覆盖的函数。第二个实现设计多重继承,条款40探索的主题。
Handle Classes和Interface Classes解除了 接口与实现之间的耦合关系,从而降低了文件间的编译依存性。


你可能感兴趣的:(C++,String,Class,interface,reference,编译器)