五.实现 


条款26:尽可能延后变量定义式的出现时间

  如果你定义了一个变量且该类型带一个构造函数或析构函数,当程序到达该变量时,你要承受构造成本,而离开作用域时,你要承受析构成本。为了减少这个成本,最好尽可能延后变量定义式的出现时间。举例说明:

string encryptPassword(const string& password)
{
string encrypted; //(1)
if (password.length() < MINIMUM_PASSWORD_LENGTH) {
throw logic_error("Password is too short");
}
//进行必要的操作,将口令的加密版本放进encrypted之中; string encrypted; //(2)
return encrypted;
}

  encrypted应该在(2)处定义,因为如果在(1)处定义,如果抛出异常,那么encrypted的构造和析构还是要执行,浪费系统资源!

  通过默认构造函数构造出一个对象然后对它赋值”比“直接在构造函数时指定初值”效率差。“尽可能延后”的真正意义应该是:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

    //方法A:定义循环外
Widget w;
for (int i = ; i < n; ++i)
{
w = some value dependent on i;
...
}//1个构造函数+1个析构函数+n个赋值操作;
//方法B:定义循环外
for (int i = ; i < n; ++i)
{
Widget w(some value dependent on i);
...
}//n个构造函数+n个析构函数

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

    请记住:

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

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

  C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
  C风格的转型动作看起来像这样:
     (T)expression    //将expression转型为T  

  函数风格的转型动作看起来像这样:
     T(expression)    //将expression转型为T
  

  C++还提供四种新式转型:
  const_cast:通常被用来将对象的常量性转除;即去掉const。
  dynamic_cast:主要用来执行“安全向下转型”,即:基类指针/引用到派生类指针/引用的转换。如果源和目标类型没有继承/被继承关系,编译器会报错;否则必须在代码里判断返回值是否为NULL来确认转换是否成功。有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL)
  reinterpret_cast:意图执行低级转型,将数据从一种类型的转换为另一种类型,也就是说将数据以二进制存在的形式进行重新解释。实际动作可能取决于编译器,这也就表示它不可移植
  static_cast:用来静态类型转换,强制类型转换,运行时不做类型检查,因而可能是不安全的。例如将non-const转型为const,int转型为double等等。 static_cast也可以进行基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。(基类和子类之间的动态类型转换建议用dynamic_cast)

  upcast:Just same as dynamic_cast. 由于不用做runtime类型检查,效率比dynamic_cast高;

  downcast:不安全。不建议使用。

#include <iostream>
using namespace std;
class Base
{
public:
virtual int foo(){
cout<<"Base"<<endl;
return ;
};
}; class Derived:public Base
{
public:
int foo(){
cout<<"Derived"<<endl;
return ;
}
};
int main()
{
Base *b=new Base;
Derived *d1=static_cast<Derived*>(b);
d1->foo(); // 正确
Derived *d2=dynamic_cast<Derived*>(b);
d2->foo(); //错误
}

尽量使用新式转型:

  • 它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程。
  • 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

    请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。

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

class Point
{
public:
Point(int x, int y);
void SetX(int newVal);
void SetY(int newVal);
private:
int x_cor;
int y_cor;
}; struct RectData
{
Point ulhc; // 矩形左上角的点
Point lrhc; // 矩形右下角的点
};
class Rectangle
{
private:
shared_ptr<RectData> pData;
public:
Point& upperLeft()const{ return pData->lrhc; }
Point& lowerRight()const{ return pData->ulhc; }
};

  上面的代吗中Point是表示坐标系中点的类,RectData表示一个矩形的左上角与右下角点的点坐标。Rectangle是一个矩形的类,包含了一个指向RectData的指针。

我们可以看到了uppLeft和lowerRight是两个const成员函数,它们的功能只是想向客户提供两个Rectangle相关的坐标点,而不是让客户修改Rectangle。但是两个函数却都返回了references指向了private内部数据,调用者于是可以通过references更改内部数据

  这给了我们一些警示:成员变量的封装性只等于“返回其reference”的函数的访问级别;如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

  handles(号码牌,用于取得某个对象)指reference、指针和迭代器,它们返回一个“代表对象内部数据”的handle。

  解决方法:我们可以对上面的成员函数返回类型上加上const

public:
const Point& upperLeft()const{ return pData->lrhc; }
const Point& lowerRight()const{ return pData->ulhc; }

  但是函数返回一个handle代表对象内部成分还总是危险的,因为可能会造成dangling handles(空悬的号牌)。比如某个函数返回GUI对象的外框(bounding box)。

class GUIObject{
//..
};
const Rectangle boundingBox(const GUIObject &obj);
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

  对boundingBox返回的是一个新的,暂时的Rectangle对象temp,而pUpperLeft指向的是temp的Points。

  当const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());执行完之后,temp将会被销毁,从而导致temp内的Points析构,最终将导致pUpperLeft 指向一个不存在的对象,从而造成悬空、虚吊。

 请记住:

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

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

 

  Inline(内联)函数,多棒的点子!它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码
  如果 inline 函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
     记住,inline 只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式钱加上关键字inline。
     Inline函数通常一定被置于头文件内,因为大多数建置环境(building environment)在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
     Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
     Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
     一个表面上看似inline的函数是否真实inline,取决于你的建置环境(building environment),主要取决于编译器。
     有的时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。比如,当程序要取某个inline函数的地址,编译器通常必须为此函数生成一个oulined函数本体,因为编译器没有能力让一个指针指向一个并不存在的函数。

inline void f(){..}
void (*pf)()=f;
..
f(); //这个将被inline
pf(); //这个或许不被inline,因为它通过函数指针完成

  对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。
  这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。

总结:

1. inline函数的调用,是对函数本体的调用,是函数的展开,使用不当会造成代码膨胀。

2. 大多数C++程序的inline函数都放在头文件,inlining发生在编译期。

3. inline函数只代表“函数本体”,并没有“函数实质”,是没有函数地址的(内联优化从目标文件中去掉了该函数的入口点,符号表中也没有该函数的名称)。

4. inlining在大多数编译器中编译期行为, inline 函数无法随着程序库的升级而升级。换句话说如果f 是程序库内的一个inline 函数,客户将”f 函数本体”编进其程序中,一旦程序库设计者决定改变f ,所有用到f 的客户端程序都必须重新编译。但如果f是non-inline函数,当修改f后,客户端只需要重新连接就好了。

值得注意的是:

1. 构造函数与析构函数往往不适合inline。因为这两个函数都包含了很多隐式的调用,而这些调用付出的代价是值得考虑的。可能会有代码膨胀的情况。

2. inline函数无法随着程序库升级而升级。因为大多数都发生在编译期,升级意味着重新编译。

3. 大部分调试器(VS2010可以)是不能在inline函数设断点的。因为inline函数没有地址。

     请记住:

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
  • 另外,对function templates的inline也要慎重,保证其所有实现的函数都应该inlined后再加inline。

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

  这个问题产生是源于希望编译时影响的范围尽量小,编译效率更高,维护成本更低,这一需求。

  实现这个目标首先第一个想到的就是,声明与定义的分离,用户的使用只依赖声明,而不依赖定义(也就是具体实现)。

  但C++的Class的定义式却不仅仅只有接口,还有实现细目(这里指实现接口需要的私有成员)。而有时候我们需要修改的通常是接口的实现方法,而这一修改可能需要添加私有变量,但这个私有变量对用户是不应该可见的。但这一修改却放在了定义式的头文件中,从而造成了,使用这一头文件的所有代码的重新编译。

  于是就有了pimpl(pointer to implementation)的方法。用pimpl把实现细节隐藏起来,在头文件中只需要一个声明就可以,而这个poniter则作为private成员变量供调用。

  这里会有个有意思的地方,为什么用的是指针,而不是具体对象呢?这就要问编译器了,因为编译器在定义变量时是需要预先知道变量的空间大小的,而如果只给一个声明而没有定义的话是不知道大小的,而指针的大小是固定的,所以可以定义指针(即使只提供了一个声明)。

  这样把实现细节隐藏了,那么实现方法的改变就不会引起别的部分代码的重新编译了。而且头文件中只提供了impl类的声明,而基本的实现都不会让用户看见,也增加了封装性。

  结构应该如下:

class PersonImpl;
class Person {
public:
...
private:
std::tr1::shared_ptr<PersonImpl> PersonImpl;
};

  这一种类也叫handle class

  另一种实现方法就是用带factory函数的interface class。就是把接口都写成纯虚的,实现都在子类中,通过factory函数或者是virtual构造函数来产生实例。

  声明文件时这么写:

class Person
{
public:
static shared_ptr<Person> create(const string&,
const Data&,
const Adress&);
};

  定义实现的文件这么写

class RealPerson :public Person
{
public:
RealPerson(...);
virtual ~RealPerson(){}
//...
private:
// ...
};

  有了RealPerson之后,就可以写出Person::create函数了:

std::tr1::shared_ptr<Person> Person::create(const string& name
const Data& birthday
<span style="white-space:pre"> </span> const Address& adrr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));
}

请记住:

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。
  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用

六.继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系

  以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。
  如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
     在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)

好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
    
     请记住:

  • “public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。

条款33:避免遮掩继承而来的名称

C++的名称遮掩规则所做的唯一事情就是:遮掩名称。只要Derived class有和base class中相同的函数名,不管其返回值,参数类型,参数个数是否相同,也不管成员函数是纯虚函数,非纯虚函数或非虚函数。只要名称相同就发生覆盖。派生类的作用域嵌套在基类的作用域内。
     (1)derived classes内的名称会遮掩base classes内的名称(即使变量的类型不同,或者函数的参数不同)。如下面代码所示:

class Base {
private:
int x;
public:
virtual void mfl() = ;
virtual void mfl(int);
virtual void mf2();
void mf3 ();
void mf3(double);
}; class Derived: public Base {
public:
virtual void mfl();
void mf3 ();
void mf4 ();

}; Derived d;
int x; d.mfl(); //正确,调用Derived::mfl
d.mfl(x); //错误,名称被覆盖
d.mf2(); //正确,调用Base::mf2
d.mf3(); //正确,调用Derived::mf3
d.mf3(x); //错去,名称被覆盖</span>

  (2) 为了让被遮掩的名称再见天日,可使用using 声明式或转交函数( forwarding functions) 。

  (a) 使用using声明式

class Base {
private:
int x;
public:
virtual void mfl() = ;
virtual void mfl(int);
virtual void mf2();
void mf3 ();
} void mf3(double); class Derived: public Base {
public:
using Base::mfl; //使用using 声明式
using Base: :mf3; //使用using 声明式
virtual void mfl();
void mf3 ();
void mf4();
} Derived d
int x;
d.mf1 () ; //仍调用Derived: :mfl
d.mf1 (x); //调用Base: :mfl
d.mf2 () ; //调用Base: :mf2
d.mf3 ();//调用Derived: :mf3
d.mf3 (x); //调用Base: :mf3</span>

  (b) 使用转交函数

class Derived: private Base (
public:
virtual void mfl () //转变函数(forwading function) ,
{ Base:: mfl ( );} //暗自成为inline
}
Derived d;
int x;
d.mfl(); //正确,调用Derived::mfl
d.mfl(x); //错误,名称被遮掩

请记住:

  • derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。

 条款34:区分接口继承和实现继承

  本条款主要讲函数接口继承(也就是声明)和函数实现继承,以及pure virtual 函数、simple(impure) virtual 、non-virtual函数之间的差异。
(1)接口继承和实现继承不同。在public 继承之下, derived classes 总是继承base class的接口。
(2) pure virtual 函数只具体指定接口继承。(要求继承者必须重新实现该接口)
(3) 简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承(继承者可自己实现该接口也可使用缺省实现)。
(4) non-virtual 函数具体指定接口继承以及强制性实现继承。(继承者必须使用该接口的实现)

class Shape {
public:
virtual void draw( ) const = ; //pure virtual 函数
virtual void error(const std::string& msg); //简朴的(非纯) impure virtual 函数
int objectID ( ) const;// non-virtual 函数
};

请记住:

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体制定接口继承。
  • 简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
  • non-virtual函数具体制定接口继承以及强制性实现继承。

    条款35:考虑virtual函数以外的其它选择

   

  本条款告诉程序员,当需要使用virtual 函数时,可以考虑其他选择。

  Virtual函数的替代方案是:
  (1) 使用non-virtual interface(NVI)手法。思想是:将virutal函数放在private中,而在public中使用一个non-virtual函数调用该virtual函数。优点是:可以做一些预处理、后处理工作。

class GameCharacter {
public:
int healthValue() const{ // 1. 子类不能重定义
... // 2. preprocess
int retVal = doHealthValue(); // 2. 真正的工作放到虚函数中
... // 2. postprocess
return retVal;
}
private:
virtual int doHealthValue() const { // 3. 子类可重定义
...
}
};

  例如:

class Base {
public:
Base(int i):val(i){}; int healthValue() const
{
int retVal = doHealthValue();
return retVal;
}
private:
virtual int doHealthValue() const
{
return val;
} int val;
};
class Derived: public Base {
public:
Derived(int i,int j):Base(i),val(j){};
private:
virtual int doHealthValue() const
{
return val;
}
int val;
}; int main()
{
Derived d(,);
cout<<d.healthValue()<<endl;
return ;
}
  输出:2
  (2)将virtual函数替换为“函数指针成员变量”(这是Strategy设计模式中的一种表现形式),见下面代码。优点是每个对象拥有自己的函数实现,也可在运行时改变计算函数;缺点是:该函数不能访问类中的私有成员(若要访问,必须由公有成员提供接口)

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc); // default algorithm
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{ }
int healthValue() const {
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
  (3) 以tr1::function成员变量替换virtual函数,这允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。这种方式比上面的函数指针更灵活、限制更少:
    [1]返回值不一定是int,与其兼容即可; 
    [2]可以是function对象; 
    [3]可以是类的成员函数。
  
  (4) 继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。这种方式最大的优点是:可以随时添加新的算法。举例:

class GameCharacter;  

class HealthCalcFunc {  

 public:  

  ...  

  virtual int calc(const GameCharacter& gc) const  

   { ... }  

  ...  

};  

HealthCalcFunc defaultHealthCalc;  

class GameCharacter {  

 public:  

  explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)  

   : pHealthCalc(phcf)  

    {}  

  int healthValue() const {  

   return pHealthCalc->calc(*this);  

 }  

  ...  

private:  

HealthCalcFunc *pHealthCalc;  

};  

 条款36:绝不重新定义继承而来的non-virtual函数

//类的定义
class B{
public:
void func(){ cout<<“B”;}
virtual void func2(){ cout<<“B”;}
}; class D:public B{
public:
void func() { cout<<“D”;}
virtual void func2(){ cout<<“D”;}
}; //下面是对B和D的使用
D dObject;
B* bPtr = &dObject;
D* dPtr = &dObject; //下面这两种调用方式:
bPtr->func(); //调用B::func
dPtr->func(); //调用D::func
bPtr->func2(); //调用D::func
dPtr->func2(); //调用D::func
  解释:在C++继承中,virtual函数是动态绑定的,调用的函数跟指针或者引用实际绑定的对象有关,而non-virtual函数是静态绑定的,调用的函数只跟声明的指针或者引用的类型相关。
 
  • 不要重新定义继承而来的non-virtual函数。

条款37:绝不重新定义继承而来的缺省参数值


    对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数系动态绑定(dynamically bound),而缺省参数值确实静态绑定(statically bound)。意思是你可能会在“调用一个定义于派生类内的虚函数”的同时,却使用基类为它所指定的缺省参数值。

class Shape{  

 public:  

  enum Color{RED,GREEN,BLUE};  

  virtual void draw(Color color = RED)const = ;  

  ...  

};  

class Circle:public Shape{  

 public:  

  //竟然改变缺省参数值  

  virtual void draw(Color color = GREEN)const{ ... }  

};  

Shape* pc = new Circle; //静态绑定为RED
pc->draw(); //注意调用的是: Circle::draw(RED),也就是说,此处的draw函数是基类和派生类的“混合物”
Circle* pc = new Circle; //静态绑定为GREEN

  为什么缺省参数是静态绑定而不是动态绑定呢?主要原因是运行效率。如果缺省参数采用动态绑定,那么编译器就必须有某种方法在运行期为virtual函数决定适当的缺省参数,这样会使程序的执行效率低下并且实现机理更加复杂。

  聪明的做法是考虑替代设计,如条款35中的一些virutal函数的替代设计,其中之一是NVI手法,令base class内的一个public non-virtual函数调用private virtual函数。

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue };
virtual void draw(ShapeColor color = Red)const
{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color)const = ;
};
class Rectangle :public Shape
{
private:
virtual void doDraw(ShapeColor color)const;
};

请记住:

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

条款38:通过符合塑模出has-a或“根据某物实现出”

 

  复合或包含意味着has-a。如果我们想设计一个自己的set,我们思考后觉得可以用list来实现它,但是如果我把它设计出list的一个派生类,就会有问题,因为父类的所有行为在派生类都是被允许的,而list允许元素重复,而set则显然不行,所以set与list之间不符合is-a关系,我们可以把list设计为set的一个成员,即包含关系(has-a)。


条款39:通过符合塑模出has-a或“根据某物实现出”

 

  明智而审慎地使用private继承
  (1)如果class之间的继承关系是private。编译器不会自动将一个derived class对象转化为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性。

class Person {..}
class Student:private Person{..} void eat(const Person&);
Person p;
Student s; eat(p); //正确
eat(s); //错误

  (2)private继承“并不存在is-a关系”的classes,而是has-a或is-implemented-in-terms-of的继承模型。如果我们只想利用base class的一部分功能,而又不想把base class暴露给其他外部对象使用,可以使用private继承。

主要有三种使用场合:

(a)其中一个derived class 需要访问base class的protected成员;

(b)derived class 需要重新定义base class中一个或多个virtual函数。

(c)需要对empty classes的空间最优化,如下面的代码:

class Empty{ }; //empty class
class HoldsAnyInt{
private:
int x;
Empty e;
};//sizeof(HoldsAnyInt) =8。//对于大小为0的独立对象,通常C++会在对象内需要安插一个char, 并且有位对齐要求。 class HoldsAnyInt::private Empty{
private:
int x;
}; //sizeof(HoldsAnyInt) == sizeof(int),这个就是EBO(empty based optimization 空白基类优化)。

 条款40:明智而审慎地使用多重继承

  使用多重继承就要考虑歧义的问题(成员变量或者成员函数的重名)。最简单的情况的解决方案是显式的调用(诸如item.Base::f()的形式)。

  复杂一点的,就可能会出现“钻石型多重继承”,以File为例:

class File { ... }
class InputFile : public File { ... }
class OutputFile : public File { ... }
class IOFile : public InputFile, public OutputFile { ... }

  这里的问题是,当File有个filename时,InputFile与OutputFile都是有的,那么IOFile继承后就会复制两次,就有两个filename,这在逻辑上是不合适的。解决方案就是用virtual继承:

class File { ... }
class InputFile : virtual public File { ... }
class OutputFile : virtual public File { ... }
class IOFile : public InputFile, public OutputFile { ... }
  这样InputFile与OutputFile共享的数据就会在IOFile中只保留一份了。

  但是virtual继承并不常用,因为:

  1. virtual继承会增加空间与时间的成本。

  2. virtual继承会非常复杂(编写成本),因为无论是间接还是直接地继承到的virtual base class都必须承担这些bases的初始化工作,无论是多少层的继承都是。

Effective C++ 总结(三)的更多相关文章

  1. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  2. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  3. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. Effective Java 第三版——7. 消除过期的对象引用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  5. Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版——10. 重写equals方法时遵守通用约定

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——11. 重写equals方法时同时也要重写hashcode方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——12. 始终重写 toString 方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——14.考虑实现Comparable接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  10. Effective Java 第三版——18. 组合优于继承

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. Node 之 Express 学习笔记 第二篇 Express 4x 骨架详解

    周末,没事就来公司加班继续研究一下Express ,这也许也是单身狗的生活吧. 1.目录结构: bin, 存放启动项目的脚本文件 node_modules, 项目所有依赖的库,以及存放 package ...

  2. 入门4:PHP 语法基础1

    一.PHP标记符 1.PHP页面中 以<?php  开头, ?>结尾,纯PHP页面结尾可以不写?> 2.在HTML页面插入PHP代码,必须有?>结尾.代码如下: <!DO ...

  3. C#.NET快速开发框架-企业版V4.0截图打包下载

    C/S系统开发框架-企业版 V4.0 (Enterprise Edition) http://www.csframework.com/cs-framework-4.0.htm 其它图片打包下载: ht ...

  4. python 装饰器、生成器、迭代器

    # 装饰器'''由高阶函数(把一个函数名当作实参传递给另一个函数,返回值中包含函数名)和嵌套函数(函数中嵌套函数)组成功能:在不更改原函数的代码和调用方式的前提下添加新的功能装饰器本身就是一个函数.使 ...

  5. SVN 使用的简单整理

    1. 在SVN服务器上创建存储Dir,并和个人主机建立联系.      现在SVN服务器上创建一个存储文件夹svn_storeDir.然后在个人电脑上建立一个本地文件夹local_Dir.    进入 ...

  6. 传值 UI考试知识点

    传值: 1. 属性传值:从前往后 2. 代理传值:从后往前 3. block: 4. 单例:普通写法和GCD写法 5 . 通知 NSNotification GCD 单例: static PlayMu ...

  7. iOS: 获取文件路径

    iOS: 获取文件路径   // 例如 - (NSString *)applicationDocumentsDirectory { return [NSSearchPathForDirectories ...

  8. Unity NGUI 3.0.4版本 制作网络版斗地主

    Unity NGUI 3.0.4版本 @by 灰太龙  开发环境 Win7旗舰版 Unity 4.2.1f4 本文就写个开门篇,告诉大家怎么用NGUI,第一步导入NGUI 3.0.4版本! 1.启动U ...

  9. 模态运行EXE程序

    function ExecShowModal(APath: PChar; ACmdShow: Integer; ATimeout: Longword): Integer; var vStartupIn ...

  10. n数码问题, 全排列哈希

    转载了一篇关于全排列的哈希函数,Poj1077就是应用了全排列的哈希: 我们经常使用的数的进制为“常数进制”,即始终逢p进1.例如,p进制数K可表示为    K = a0*p^0 + a1*p^1 + ...