More Effective C++ 读书笔记(2)

条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

区别1:当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。 

你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

istream operator>>(istream& s, Widget& w);
void passAndThrowWidget()
{
    Widget localWidget;
    cin >> localWidget; //传递 localWidget 到 operator>>
    throw localWidget; // 抛出 localWidget 异常
}
当传递 localWidget 到函数 operator>>里,不用进行拷贝操作,而是把 operator>>内的引用类型变量 w 指向 localWidget,任何对 w 的操作实际上都施加到 localWidget 上。
不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行 localWidget 的拷贝操作,也就说传递到 catch子句中的是 localWidget 的拷贝。必须这么做,因为当 localWidget 离开了生存空间后,其析构函数将被调用。如果把 localWidget 本身(而不是它的拷贝)传递给 catch 子句,这个子句接收到的只是一个被析构了的 Widget,一个 Widget 的“尸体”。这是无法使用的。因此 C++规范要求被做为异常抛出的对象必须被复制。

区别2:抛出异常运行速度比参数传递要慢


条款13:通过引用(reference)捕获异常


条款14:审慎使用异常规格


条款15:了解异常处理的系统开销

开销1:编译器必须支持异常,默认情况下带有的开销

当你不用异常处理时你不能让编译器生产商消除这方面的开销,因为程序一般由多个独立生成的目标文件(object files)组成,只有一个目标文件不进行异常处理并不能代表其他目标文件不进行异常处理。

开销2:try块

粗略地估计,如果你使用 try 块,代码的尺寸将增加 5%-10%并且运行速度也同比例减慢。
为了减少开销,你应该避免使用无用的 try 块。

开销3:异常规格

编译器为异常规格生成的代码与它们为 try 块生成的代码一样多,所以一个异常规格一般花掉与 try 块一样多的系统开销。

开销4:抛出异常

与一个正常的函数返回相比,通过抛出异常从函数里返回可能会慢三个数量级。

条款16:牢记 80-20 准则(80-20 rule)

软件整体的性能取决于代码组成中的一小部分

条款17:考虑使用lazy evaluation(懒惰计算法)

推迟计算工作直到系统需要这些计算的结果

除非你确实需要,不去为任何东西制作拷贝。

条款18:分期摊还期望的计算

over-eager evaluation(过度热情计算法):如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销。

条款19:理解临时对象的来源

在 C++中真正的临时对象是看不见的,它们不出现在你的源代码中。

建立临时对象的环境:

1.使函数成功调用而建立临时对象
当传送给函数的对象类型与参数类型不匹配时会产生这种情况。

// 返回 ch 在 str 中出现的次数
size_t countChar(const string& str, char ch);
char buffer[MAX_STRING_LEN];
char c;
cout << countChar(buffer, c)<< buffer << endl;

buffer类型为char数组,但countChar的参数类型为const string&,仅当消除类型不匹配后,才能成功进行这个调用。这时编译器会进行隐式转换,方法是建立一个 string 类型的临时对象。通过以 buffer 做为参数调用 string 的构造函数来初始化这个临时对象。countChar 的参数 str 被绑定在这个临时的 string 对象上。当 countChar 返回时,临时对象自动释放。

PS:仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。

void uppercasify(string& str);
char subtleBookPlug[] = "Effective C++";
uppercasify(subtleBookPlug); //ERROR

假设建立一个临时对象,那么临时对象将被传递到 upeercasify 中,其会修改这个临时对象,把它的字符改成大写。但是对 subtleBookPlug 函数调用的真正参数没有任何影响;仅仅改变了临时从 subtleBookPlug 生成的 string 对象。无疑这不是程序员所希望的。程序员传递 subtleBookPlug 参数到 uppercasify 函数中,期望修改 subtleBookPlug 的值。当程序员期望修改非临时对象时,对非常量引用(references-to-non-const)进行的隐式类型转换却修改临时对象。 

2.函数返回对象时

const Number operator+(const Number& lhs,
                       const Number& rhs);

这个函数的返回值是临时的,因为它没有被命名;它只是函数的返回值。你必须为每次调用 operator+构造和释放这个对象而付出代价。

条款20:协助完成返回值优化

返回constructor argument而不是直接返回对象,能让编译器消除临时对象的开销。

const Rational operator*(const Rational& lhs,
                         const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(),
                    lhs.denominator() * rhs.denominator());
}
PS:但这种做法也会带来其他开销: 你仍旧必须为在函数内临时对象的构造和释放而付出代价,你仍旧必须为函数返回对象的构造和释放而付出代价。

条款21:通过重载避免隐式类型转换

class A
{
public:
    A();
    A(int);
};
A a1,a2;
A a3 = a1 + a2;
//将10隐式转换为A(10)即可完成下列操作
a3 = a1 + 10;
a3 = 10 + a2;

除了使用隐式类型转换,还可以通过重载的方式完成。

const A operator +(const A& lhs,const A& rhs);
const A operator +(const A& lhs,const int rhs);
const A operator +(const int lhs,const A& rhs);
 
 
 
 

PS:在 C++中,一个重载的 operator 必须带有一个用户定义类型(user-defined type)的参数。 因此 不能重载operator +(int ,int)这种情况

PPS:没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。

条款22:考虑用运算符的赋值形式(op=)取代其单独形式(op)

operator+ 可以利用operat+=来实现

条款23:考虑变更程序库

在效率方面,iostream 程序库总是不如 stdio,因为 stdio 产生的执行文件与 iostream 产生的执行文件相比尺寸小而且执行速度快。

条款24:理解虚拟函数、 多继承、虚基类和RTTI所需的代价

一、虚函数的代价:

1.你必须为每个包含虚函数的类的 virtual table(vtbl)留出空间

类的 vtbl 的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。每个类应该只有一个 virtual table。

vtbl会被编译器放在哪里?

(1)一种干脆的方法是为每一个可能需要 vtbl 的 object 文件生成一个 vtbl 拷贝。连接程序然后去除重复的拷贝,在最后的可执行文件或程序库里就为每个 vtbl 保留一个实例。

(2)更普通的设计方法是采用启发式算法来决定哪一个 object 文件应该包含类的 vtbl。通常启发式算法是这样的:要在一个 object 文件中生成一个类的 vtbl,要求该 object 文件包含该类的第一个非内联、非纯虚拟函数(non-inline non-pure virual function)定义(也就是类的实现体)。因此上述 C1 类的 vtbl 将被放置到包含 C1::~C1 定义的 object 文件里(不是内联的函数),C2 类的 vtbl 被放置到包含 C1::~C2 定义的 object 文件里(不是内联函数)。

PS:避免把虚函数声明为内联函数

2.在每个包含虚函数的类的对象里,你必须为额外的指针付出代价(vptr)


void makeACall(C1 *pC1)
{
    pC1->f1();
}

上述函数的操作过程:

1.通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内
哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到vptr)和一个指针的间接寻址(以得到 vtbl)
2. 找到对应 vtbl 内的指向被调用函数的指针(在上例中是 f1)。这也是很简单的,
因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内
的一个偏移。

如果我们假设每个对象有一个隐藏的数据叫做 vptr,而且 f1 在 vtbl 中的索引为 i,此语句 pC1->f1();

3. 调用第二步找到的的指针所指向的函数。

生成的代码就是这样的 :

(*pC1->vptr[i])(pC1); 
//调用被 vtbl 中第 i 个单元指向的函数,
//而 pC1->vptr指向的是 vtbl;pC1 被做为this 指针传递给函数。
 
 

3.你实际上放弃了使用内联函数

因为“内联”是指“在编译期间用被调用的函数体本身来代替函数调用的指令”,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能知道为什么它不会内联该函数的调用。

二、运行时类型识别(RTTI)的代价 

RTTI 能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询。这些信息被存储在类型为 type_info 的对象里,你能通过使用 typeid 操作符访问一个类的 type_info 对象。

RTTI被设计为在类的 vtbl 基础上实现:例如,vtbl 数组的索引 0 处可以包含一个 type_info 对象的指针,这个对象属于该 vtbl相对应的类。

使用这种实现方法,RTTI 耗费的空间是在每个类的 vtbl 中的占用的额外单元再加上存储 type_info 对象的空间。 

条款25:将构造函数和非成员函数虚拟化

虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。

class NLComponent { //用于 newsletter components的抽象基类
public: 
    ... //包含至少一个纯虚函数
};
class TextBlock: public NLComponent {
public:
    ... // 不包含纯虚函数
};
class Graphic: public NLComponent {
public:
    ... // 不包含纯虚函数
};
class NewsLetter { // 一个 newsletter 对象
public: //  NLComponent 对象
    ... // 的链表组成
private:
    list<NLComponent*> components;
};

class NewsLetter {
public:
    ...
private:
    // 为建立下一个 NLComponent 对象从 str 读取数据,
    // 建立 component 并返回一个指针。
    static NLComponent * readComponent(istream& str);
};
NewsLetter::NewsLetter(istream& str)
{
    while (str) {
        //  readComponent 返回的指针添加到 components 链表的最后,
        // "push_back" 一个链表的成员函数,用来在链表最后进行插入操作。
        components.push_back(readComponent(str));
    }
}

考虑一下 readComponent 所做的工作。它根据所读取的数据建立了一个新对象,或是TextBlock 或是 Graphic。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。

虚拟拷贝构造函数返回一个指针,指向调用该函数的对象的新拷贝。

class NLComponent {
public:
    // declaration of virtual copy constructor
    virtual NLComponent * clone() const = 0;
};
class TextBlock: public NLComponent {
public:
    virtual TextBlock * clone() const // virtual copy
    { return new TextBlock(*this); } // constructor
};
class Graphic: public NLComponent {
public:
    virtual Graphic * clone() const // virtual copy
    { return new Graphic(*this); } // constructor
};

类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此“拷贝”的含义与真正的拷贝构造函数相同。如果真正的拷贝构造函数只做了简单的拷贝,那么虚拟拷贝构造函数也做简单的拷贝。如果真正的拷贝构造函数做了全面的拷贝,那么虚拟拷贝构造函数也做全面的拷贝。

PS:被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。
















猜你喜欢

转载自blog.csdn.net/sinat_25394043/article/details/80161978