(五).实现
____________________________________________________________________________________________________________________________________
条款30:透彻了解inlining 的里里外外
#1.inline是将“对此函数的每一个调用”都以函数本体来替换之,所以inline函数应保证函数本体的短小,这样,既可以导致较小的目标码,又可以导致较高的指令
高速缓存集中率。
#2.大多数建置环境在编译期间进行inlining,所以Inline函数通常一定被置于头文件中,
Templates通常也被置于头文件中,因为编译器要在编译期间将其具现化。
#3.一个Inline函数是否真的被inlined取决于建置环境,但主要取决于编译器。
(大多数编译器无法将你要求的函数Inline化时,会给你一个警告信息。)
(1).所有对virtual函数的调用都会使Inlining落空,因为virtual意味着到运行期
才决定调用哪个函数。
(2).编译器通常不对“通过函数指针而进行的调用“inlining,例如:
inline void f(){...} //假设编译器有意愿inline "对f的调用”
void (*pf)()=f; //pf指向f
...
f(); //这个调用将被inlined,因为它是一个正常调用
pf(); //这个调用或许不被inlined,因为它通过函数指针达成
(3).有时候编译器会生成构造函数和析构函数的副本,如此一来它们就可以获得指针
指向那些函数,在array内部元素的构造和析构中使用。
(4).Derived class 构造函数也许不被inlined,因为即便是空构造,它也会调用
base classes构造函数和进行异常安全检查,致使代码块增大,从而不适合inline.
#4.不要因为function templates 出现在头文件,就将它们声明为inline。
#5.先将inline限制在那些平淡无奇到只有一句的函数身上或干脆不inline,可以
方便日后的调试过程,不过这样也等于将自己推向手工最优化之路,但毕竟这样更
符合80-20法则。
(虽然某些建置环境勉力支持对inlined函数的调试,其他许多建置环境仅仅只能
"在调试版程序中禁止发生inlining".)
#6.inline带来的一个弊端是,一旦inline函数被修改,依靠它的客户端都必须重新
编译,毕竟inline会插入函数本体,所以请保证inline使用在短小明确的代码上,这
可为日后升级提供便利。
____________________________________________________________________________________________________________________________________
条款31:将文件间的编译依存关系降至最低
#1.标准程序库不该作前置声明,而应该使用#includes来完成目的。
理由1:标准程序库的前置声明比较复杂,实际上并不需要你这么做。
理由2:标准程序库意味着它成为了公用的标准,所以极少会做改动,从而
要求重新编译,特别是当它作为预编译头使用时,所以请放心使用#includes.
#2.支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。因为
编译器对于定义式需要确定其实现的大小,以致于增加了编译依存性。
(如果可以,请让头文件尽可能自我满足,做不到则考虑声明式)
三个声明式的简单策略:
(1).如果使用 object references 或 object pointers 可以完成任务,就不要使用
objects。
(2).如果能够,尽量以 class 声明式替换 class 定义式。
//注意,当你声明一个函数而它用到某个class时,你并不需要该 class 定义;
//纵使函数以 by value 方式传递该类型的参数(或返回值):
class Date; //Date声明式
Date today();
void clearAppointments(Date d); //Date定义式
(3).为声明式和定义式(对同一对象的说法)提供不同的头文件
(用于声明式较多时,为了组织结构的统一)
例如:
#include"datefwd.h" //其中声明 class Date;
Date today();
void clearAppointments(Date d);
//"datefwd.h"命名方式取决于C++标准程序库文件的<iosfwd>,<iosfwd>带来的
//另一个彰显是,本条款也使用于templates,毕竟也有些建置环境允许将templates
//的定义式放在”非头文件“内,这样一来就可以将只含声明式的头文件提供给templates
#3.基于声明式的两个手段是 Handle classes 和 Interface classes。
//Handle classes示例:
#include<string>
#include<memory>
//Person.h
class PersonImpl;
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;
};
//Person.cpp
#include"PersonImpl.h"
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();
}
//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> //工厂函数(或称为virtual构造函数)
create(const std::string& name, //创建一个派生类对象,并返回基类指针
const Date& birthday,
const Address& addr);
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));
...
};
class RealPerson:public Person{
public:
RealPerson(const std::string& name, const Date& 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 theBirthDate;
Address theAddress;
};
{}
//客户可以这样使用:
std::string name;
Date dateOfBirth;
Address address;
...
//创建一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout<<pp->name(),
<<"was born on"
<<pp->birthDay()
<<"and now lives at"
<<pp->address();
... //pp离开作用域对象会自动消除
对Handle classes和Interface classes的评价:
使用Handle classes 或 Interface classes,虽然降低了文件间的编译依存性,
但却会使代码和目标码增多,降低简洁性,另外,动态内存分配也会带来额外的
开销,并带来遭遇bad_alloc的可能性。此外,Interface classes 中virtual函数
的调用还会导致间接跳跃(indirect jump)成本,除此之外,vpr的存在也会使类
对象变大,其变大大小取决于base classes 和 dervied classes中不同的virtual
函数个数。因此,你应该以渐进式方式来考虑使用这些技术。
____________________________________________________________________________________________________________________________________
(六).继承与面向对象设计
____________________________________________________________________________________________________________________________________
#1."public继承"意味着is-a。适用于base classes 身上的每一件事情一定也适用
于derived classes 身上,因为每一个derived class 对象也是一个base class
对象。
例如:
class Bird{
public:
virtual void fly(); //鸟会飞
...
};
class Penguin:public Bird{ //企鹅是一种鸟
...
};
//这里的企鹅是一种鸟使用了public继承,
//但这里仍然有一个问题,”企鹅不会飞!“,
//而此处的继承关系却表明企鹅会飞,之所以
//出错的原因在于并不是所有的鸟类都会飞。
//我们可以更改接口使它运行期产生错误:
void error(const std::string& msg); //定义于另外某处
class Penguin:public Bird{
public:
virtual void fly() { error("Attempt to make a penguin fly!"); }
...
};
//如此便可以在运行期表明,“企鹅会飞,但尝试这么做却是一种错误!”
//而好的接口应该可以防止无效的代码通过编译,因此我们应该采取
//另一种在“在编译器拒绝企鹅飞行”的设计,如下:
class Bird{
...
};
class FlyingBird: public Bird{
public:
virtual void fly();
...
};
class Penguin: public Bird{
...
};
//此时,因为行为的正确合理,你让企鹅飞,编译器肯定会抱怨:
Penguin p;
p.fly(); //编译期错误
#2.public继承适合于不会对base classes 原有形态和行为产生束缚的derived class:
例如:
//考虑以下代码:
class Renctangle{
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const; //返回当前值
virtual int width() const;
...
};
void makeBigger(Rectangle& r) //这个函数用以增加r的面积
{
int oldHeight = r.height();
r.setWidth(r.width() + 10); //为r的宽度增加10
assert(r.height() == oldHeight);//判断r的高度是否未曾改变
}
//显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度;r的
//高度从未改变。
//现考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:
class Square: public Rectangle {...};
Square s;
...
assert(s.width() == s.height()); //这对正方形一定为真
makeBigger(s); //由于继承,s是一种(is-a)矩形,
//所以我们可以增加其面积。
assert(s.width() == s.height()); //对所有正方形应该仍然为真。
//但我们遇到了一个问题,当makeBigger(s)之后,第二个assert就不为真了
//其原因是Square改变了base class-Rectangle的形态,对行为产生了约束,
//因为它要求了长等于宽,这也正验证了本条款的这句话:public继承适合于
//不会对base classes 原有形态和行为产生束缚的derived class
//从另一个角度来看,任何施用于base class的事情都应该施用于dervid class
//而base class要求长可以不等于宽,但dervid class却无法适应该要求,因此,
//咋看之下,正方形和长方形应该用public继承,但实则不然。
____________________________________________________________________________________________________________________________________
条款33:避免遮掩继承而来的名称
#1.C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称类型相不相同
并不重要。(意思是只看名称,忽略名称类型,参数类型,参数个数,等其他东西。)
#2.C++编译器遵循的名称查找规则为局部到全局。
例如,查找一个函数,其顺序为:
class->base class->内含base class的namespace(s)->global
如果在某处查找到该名称,则不会继续往下查找。
#3.derived classes内的名称会遮掩base classes内的名称。在public继承下从来
没有人希望如此。(这是#2中的一个特例)
#4.为了让遮掩的名称再见天日,可使用
(1).using声明式(使某作用域内所有该名称曝光)
(2).转交函数(forwarding functions).
(使某作用域内特定名称在另一作用域内延续)
例如:
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived:public Base{
public:
virtual void mf1();
void mf3();
void mf4();
...
};
Derived d;
int x;
...
d.mf1(); //没问题,调用Derived::mf1
d.mf1(x); //错误!因为 Dervid::mf1遮掩了Base::mf1
d.mf2(); //没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); //错误!因为 Derived::mf3遮掩了Base::mf3
//【使用using声明式的方法】:
class Derived:public Base{
public:
using Base::mf1; //让 Base class内名为mf1 和 mf3的所有东西
using Base::mf3; //在 Derived 作用域内都可见(并且public)
virtual void mf1();
void mf3();
void mf4();
...
};
//现在再看以下改变:
d.mf1(); //没问题,调用Derived::mf1
d.mf1(x); //现在没问题,调用Base::mf1
d.mf2(); //没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); //现在没问题,调用Base::mf3
//现假设我们唯一想要继承Base::mf1()
//因此我们可用【转交函数】的手法使其可见,
//并使用该特定手法:
class Derived:private {
public:
virtual void mf1(){Base::mf1();} //转交函数暗自成为inline
...
};
Derived d;
int x;
d.mf1(); //很好,调用的是 Derived::mf1
d.mf1(x); //错误! Base::mf1(x)被遮掩了
____________________________________________________________________________________________________________________________________
条款34:区分接口继承和实现继承
#1.接口继承和实现继承不同。在public继承之下,derived classes总是继承
base classes的接口。
#2.三种函数继承方式:
<1>.声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
(带有强制性实现含义,但其实现码具有参考意义。)
<2>.声明简朴的(非纯)impure virtual函数的目的,是让 derived classses 继承
该函数的接口和缺省实现。(可默认继承接口及缺省实现。)
<3>.声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份
强制性实现。
(non-vritual函数代表的含义是其不变性凌驾于其特异性。)
//考虑以下代码:
class Shape {
public:
virtual void draw() const = 0; //<1>的形式
virtual void error(const std::string& msg); //<2>的形式
int objectID() const; //<3>的形式
...
};
class Rectangle:public Shape{...};
class Ellipse:public Shape{...};
其中<1>的设计思想是:你必须提供一个draw函数,但我不干涉你怎么实现它。
其中<2>的设计思想是:你必须支持一个error函数,但如果你不想自己写一个,
可以使用Shape class提供的版本。
其中<3>的设计思想是:objectID适用于base class和任何想使用它的
derived class,如果有需要,你只要继承并使用它就好了。
#3.pure virtual函数可以实现,但调用它的唯一用途是”调用时明确指出其class
类型“,其好处是可以借助编译期明确指出的错误来避免犯下缺省继承的错误。
//考虑以下代码:
class Airport{...};
class Airplane{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Ariport& destination);
{
//缺省实现,将飞机飞至指定的目的地
}
class ModelA:public Airplane{...};
class ModelB:public Airplane{...};
//现假设该飞机场引进了C型飞机,但此C型飞机不走默认路线,
//而是走新的航线,于是该航空公司程序员为其添加了一些代码,
//但由于一时心急,却忘了添加新的fly函数:
class ModelC:public Airplane{...}; //未声明fly函数
//但此代码编译行的通,运行也没问题,于是C型飞机仍走默认路线,
//因此便酿成了该航空公司的信用悲剧
//那如果我们改用pure virtual函数,并予以实现码呢?
//就像这样:
class Airplane{
public:
virtual void fly(const Airport& destination)=0;
...
};
void Airplane::fly(const Airport& destination){
...
};
//那么程序员就必须为新型飞机强制型提供实现函数,
//而其实现码便成为了默认的参考版本
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination)
{
Ariplane::fly(destination);
}
}
class ModelB:public Airplane{
public:
virtual void fly(const Airport& destination)
{
Ariplane::fly(destination);
}
}
class ModelC:public Airplane{
public:
virtual void fly(const Airport& destination);
}
void ModleC::fly(const Airport& destination)
{
//将C型飞机按新路线开往目的地
}
//这样一来,如果程序员不为每个型号的飞机提供fly函数,
//编译器便会发出警告,从而避免犯下此类错误,虽然它降低
//一些简洁性,但却增加了一些明确性。另外,由于含有pure
//virtual的class只能成为抽象类,不能实例化,因此,它的
//使用场合也被添加了一些限制。
____________________________________________________________________________________________________________________________________