尽可能延后变量定义式的出现时间
当一个变量的类型带有构造函数与析构函数的时候,控制流到达这个定义式就得承担构造成本,当离开作用域就必须承担析构成本,如果最后这个变量从未使用,仍然需要这些成本(在抛出异常的时候,就可能出现未被使用的情况)。
尽可能延后的真正意义是:你不单单应该延后变量的定义直到使用这个变量的那一刻,而且应该延后这份定义知道能给他初值实参。
对于循环来说,要判断他的构造析构成本与赋值成本。
//一个构造,一个析构,n个赋值
Widget w;
for(int i = 0; i < n; ++i){
w = ...;
}
//n个构造,n个析构
for(int i = 0; i < n; ++i){
Widget w(...);
}
尽量少做转型动作
C++的四种新式转型
const_cast<T> (expression) //用于将对象的常量性移除
dynamic_cast<T> (expression)//用于安全向下转型,用于对象是否归属于继承体系中的某一个类型(旧式语法无法执行,会耗费大量的成本)
reinterpret<T> (expression) //执行低级转型,如把pointer to int 转型为int)
static_cast<T> (expression) //强迫隐式转换,比如non-const到const,int 到 double,到那时不能const 到non-const(要使用const_cast)
一般使用旧式转换的时候只有在调用explicit构造函数将对象传递给一个函数时。
class W{
public:
explicit W(int size);
...
};
void dosome(const W& w);
dosome(W(15)); //使用旧式转型
dosome(static_cast<W> (15)); //使用新式转型
任何一个类型转换往往令编译器编译出运行期间执行的代码。
一个有意思的事情是
class Base{...};
class Derived: public Base{...};
Derived a;
Base* b = &a; //此时的指针b与指针a是有一个偏移量的,这里隐喻的将Derived*转换为Base*
书上还提到一件事情,进行转型的时候返回的是个副本
class W{
public:
virtual void onR() {..}
};
class M : public W{
public:
virtual void onR(){
static_cast<W> (*this).onR(); //将this转换为W,然后调用onR
... //M的专属操作
}
};
//这里面的转型是调用当前对象的base class的副本,再进行M的专属操作的,此时base class并没有改变,derived反倒改变了
class M : public W{
public:
virtual void onR(){
W::onR(); //此时调用的是base的版本
}
};
书的后面探讨的是dynamic_cast的问题,dynamic_cast通常是用来在一个你认为是derived class的对象执行derived class的操作函数,但是你手上只有一个指向base class的pointer或reference。由于dynamic_cast运行效率很慢(对于深度继承与多重继承的成本更高),有两个一般性的做法可以避免这个问题。
一个是使用容器储存直接指向derived class的指针,通常是智能指针,这样就消除了通过base class处理对象的需要。
class W{...};
class S: public W{
public:
void blink();
...
};
typedef std::vector<std::tr1::shared_ptr<W>> VPM;
VPM ww;
for(VPM::iterator iter = ww.begin(); iter != ww.end(); ++iter)
{
if(S* s = dynamic_cast<S>(iter->get())) //最好不用dynami_cast
{
s->blink();
}
}
//替换方式
typedef std::vector<std::tr1::shared_ptr<S>> VPSM;
VPSM ss;
for(VPSM::iterator iter = ss.begin(); iter != ss.end(); ++iter)
{
(*iter)->blink();
}
另外的一种做法使用virtual,这边就不给代码了。
总之,有替代方案就不要使用dynamic_cast了。
然后绝对是要避免连串的使用dynamic_cast。
if(...)
dynamic_cast...
else
dynamic_cast...
//这种情况绝对要避免,因为产生出来的代码又大又慢,而且基本不稳。
避免返回handles指向对象内部成分
这里的handle可以是指针或迭代器或引用。
//这个类来表示点
class Point{
public:
Point(int x, int y);
...
void setX(int newV);
void setY(int newV);
...
};
//为了让Rectangle尽可能小,就把Point数据存储在struct里面,再用指针指向struct
struct RectData{
Point ulhc;
Point lrhc;
};
class Rectangle{
...
public:
//返回一个得知相关坐标点的函数,为了提高效率,采用by reference
Point& upperLeft() const {return pData->ulhc;}
Point& lowerRight() const {return pData->lrhc;}
...
private:
std::tr1::share_ptr<RectData> pData;
};
//上述的代码是可以通过编译,但是,代码的含义却是自相矛盾的
//因为一方面upperLeft和lowerRight是一个const成员函数,但是他却返回一个引用
//这就代表了用户可以使用这个引用来修改Point的值
//可以进行下面的修改
class Rectangle{
...
public:
const Point& upperLeft() const {return pData->ulhc;}
const Point& lowerRight() const {return pData->lrhc;}
...
};
但是,返回一个handle的问题还远不在此,返回handle之后,你就必须使所指对象比handle更加长寿。
class G{...};
const Rectangle bB(const G& obj); //以值传递返回一个矩形
G* pgo;
const Point* pp = &(bB(*pgo).upperLeft());
//在这个语句结束之后,bB返回的值就会自动销毁,但是pp依然指向一个Point值,这个值是不存在的,未定义行为!
为异常安全努力
异常安全,就是要在异常抛出的时候不泄露任何资源,不允许数据败坏。
异常安全的三个保证,满足其中之一即可。
基本承诺:异常抛出,任何事物都可以保持在有效的情况下。
强烈保证:函数成功,就完全成功,失败就回到原来调用之前的状态。可以使用copy-and-swap。
不抛掷承诺:承诺绝不抛掷异常。可以使用noexcept修饰符。
copy-and-swap策略:
struct P{
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
class P{
...
private:
Mutex mutex;
std::tr1::shared_ptr<P> pImpl;
};
void P::changeBackgroud(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex); //复制mutex一个副本
std::tr1::shared_ptr<P> pNew(new P(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl,pNew); //交换数据,并释放mutex
}
笔记写的代码实在简洁,有兴趣真的可以去看下书。
C++11提供了个新的noexcept修饰符可以来确保函数不抛出异常。
void fuc() noexcept { throw(); }
//调用这个函数的时候会直接终止程序进而避免异常传播
//C++11出于安全考虑默认把delete函数设为noexcept,同样类的析构函数也是默认为noexcept的
话说这本书可能有的条款有些过时了。
了解内联
调用inline函数可以不产生调用函数的开销,动作也像函数,但是也会造成代码膨胀和效率损失。
inline一般都是置于头文件中,因为编译器要知道他的样子。
然后注意一点,Template也一般在头文件中,而且如果template的所有函数都应该为inline的话,请将这个template声明为inline,但是如果不是所有都需要inline,请不要声明inline,因为内联需要成本,会导致代码膨胀。
inline只是对编译器的一个申请,不是强制的命令,可以通过将函数定义于class定义式中来隐喻声明。
大多数的编译器拒绝将太过复杂的函数inline,而对于virtual函数的调用也都会使得inline落空,因为inline意味着执行前,先将调用动作替换为调用函数的本体,而virtual意味着等待,直到运行期才确定调用哪个函数。
然后构造函数与析构函数往往是内联的糟糕的候选人。因为C++对于对象被创建与被销毁时发生了什么做了各式各样的保证,这些保证有时候就放在构造函数与析构函数中间。
class B{
public:
...
private:
std::string bm1, bm2;
};
class A: public B{
public:
A(){}
...
private:
std::string dm1, dm2, dm3;
};
//看上去A的构造函数似乎没有任何代码,
//其实C++编译器会为我们做出如下的观念性实现
A::A(){
B::B(); //初始化B
try{ dm1.std::string::string(); } //试图创建dm1
catch(...){
B::~B(); //销毁B
throw; //抛出异常
}
try{ dm2.std::string::string(); } //试图创建dm2
catch(...){
dm1.std::string::~string(); //销毁dm1
B::~B(); //销毁B
throw;
}
try{ dm3.std::string::string(); }
catch(...){
dm1.std::string::~string();
dm2.std::string::~string(); //销毁dm2
B::~B();
throw;
}
}
//同样也适用于base class,如果B的构造函数被内联了,那么继承与B的A的构造函数被调用的时候也还是会调用B的构造函数。
如果有内联函数的话对于调试也会是一个很大的麻烦,因为不知道如何在一个不存在的函数里面设置断点。
内联的话一般都是内联一些频繁使用、代码量较小的函数。
然后,编程的时候,先不要设置内联,要在进行优化的时候再设置内联函数,来使得潜在的代码膨胀问题最小化并且使得速度提升的机会最大化。
2-8法则:一个程序的80%的执行时间是花费在20%的代码上面的。
将文件的编译依存关系降至最低
C++没有把将接口从实现中分离做的很好。class的定义式里面不仅叙述了class接口,还包括十足的实现细目。
所以如果对C++的某个实现文件做出轻微修改(不是接口,只是实现部分,而且只改private的部分),就会重新编译与连接。
#include "date.h" //使用头文件使得Person的定义文件与含入的文件之间形成了一个编译依存关系
#include <string> //只要一个改变,所有相关的都要进行重新编译
class Person{
public:
...
private:
Date ss; //实现细目
};
//这些连串的相互依存关系会对许多项目造成难以形容的灾难
//如果使用前置声明的话,是会避免这种现象,但是有两个问题
namespace std{
class string; //不正确!string不是个类!!
}
class Date; //前置声明
class Person{...};
//第一个问题是string不是个类,他只是个typedef,第二个是由于编译器必须在编译期间知道对象的大小。
int main(){
int x;
Person p(..); //编译器必须知道p有多大
}
//当然在java上面并没有这个问题,上面代码等同于
int main(){
int x;
Person* p; //编译器只分配一个指针的空间给对象
}
//这是真正的接口与实现分离
分离的关键在于以声明的依存性来替换定义的依存性,这就是依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,就让他与其他文件内的声明式(不是定义式)相依。这个相依的策略是:
如果使用object reference或object pointer可以完成任务,就不要使用object。
如果能够,尽量以class的声明式来替换class的定义式。
为声明式和定义式提供不同的头文件。
//像上述的Person使用pimpl的类往往称为Handle class,要让这种类真正的实现一些东西,就是要将所有的函数转交给相应的实现类并由后者完成实际工作
//下面就是一种Handle class的实现
#include "Person.h" //正在实现Person类,必须用到定义式
#include "PersonImpl.h" //必须使用PersonImpl的定义式,否则无法调用其成员函数
//Person与PersonImpl有着完全相同的成员函数,两者的接口完全相同。
Person::Person(const std::string& name, const Date& bir)
:pImpl(new PersonImpl(name,bir))
{}
std::string Person::name() const
{
return pImpl->name();
}
//上面就是让Person构造函数以new来调用PersonImpl构造函数,以及使用Person::name函数来调用PersonImpl::name
//另一种做法就是把Person作为一个抽象的接口,interface class,然后用子类进行真正的具体实现除实体,工厂模式。
//使用Handle class可以解除接口与实现之间的耦合关系,降低文件间的编译依存性
//当然这样也会在运行期间丧失一些速度,为每个对象超额付出一些内存
Handle class与Interface class正是用来设计隐藏实现细节,如函数本体。程序库头文件应该以完全且仅有声明式的形式存在,不管是否涉及template。