五.实现 


条款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. 封装cookie组件

    var Cookie = { // 读取 get: function(name){ var cookieStr = "; "+document.cookie+"; &qu ...

  2. dede定义全局变量(include/common.inc.php)及调用方式

    dede定义全局变量的文件include/common.inc.php及使用   在include/common.inc.php文件里,dede定义了大量的全局变量,详细自己去看看   dede模板里 ...

  3. 如何在Android Studio中使用Gradle发布项目至Jcenter仓库

    简述 目前非常流行将开源库上传至Jcenter仓库中,使用起来非常方便且易于维护,特别是在Android Studio环境中,只需几步配置就可以轻松实现上传和发布. Library的转换和引用 博主的 ...

  4. STM32学习笔记——USART串口(向原子哥和火哥学习)

    一.USART简介 通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换.USART利用分数波特率发生器提供宽范围的波特率选择. S ...

  5. BZOJ 1055 玩具取名

    Description 某人有一套玩具,并想法给玩具命名.首先他选择WING四个字母中的任意一个字母作为玩具的基本名字.然后他会根据自己的喜好,将名字中任意一个字母用“WING”中任意两个字母代替,使 ...

  6. oracle利用merge更新一表的某列数据到另一表中

    假设你有两张表 t1 表 -------------------------- id |    name   |   pwd 1  |      n1     | t2 表 ------------- ...

  7. Promise 让异步更优

    每个异步方法都返回一个Promise 更优雅. then方法 每一个Promise  都有一个叫then 的方法, 接受一对callback    被解决时调用,resolve, 被拒绝   reje ...

  8. IHS怎么通过80端口连接WAS——<转>

    IHS如何通过80端口连接WAS? 经常看到有朋友问这个问题,所以简单总结一下基本步骤:   IHS和WAS之间是通过Plugin来实现的,一般的Plugins和IHS安装在同一物理机器上,WAS安装 ...

  9. 除了创建时指定窗口位置之外,还有3种移动窗口位置的办法(移动的同时往往可以改变窗口大小)(SetWindowPos最有用,它有许多标志位)

    首先,在创立窗口对象的时候,CreateWindowEx就可以指定窗口的位置.除此之外,还有三种方法可以改变窗口的位置: procedure TWinControl.CreateWindowHandl ...

  10. linux内核空间与用户空间信息交互方法

    linux内核空间与用户空间信息交互方法     本文作者: 康华:计算机硕士,主要从事Linux操作系统内核.Linux技术标准.计算机安全.软件测试等领域的研究与开发工作,现就职于信息产业部软件与 ...