逝水流年

This is a blog to record my life, my work, my feeling …

More Effective C++读书笔记19

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

如果是自定义类型进行隐式的类型转换,肯定会调用构造和析构函数,这样就一定会有一定的开销,那么如何避免这类隐式类型转换呢?21小节给出一个方式就是通过重载函数避免进行隐式类型转换。
1
2
3
4
5
6
7
class UInt
{
    public:
        const UInt operator+(const UInt& lrs, const UInt& hrs);
};
UInt a, b, c;
a = b + 10;
在计算b+10的时候,编译器会将10隐式的转为UInt类型进行计算,这种情况下可以通过重载操作法来避免出现隐式转换的开销。当然是必要时候才会采取这种方法,如果不是性能瓶颈,那么有可能造成重载函数过多,而维护起来会很不方便。

注意一种情况const UPInt operator+(int lhs, int rhs);   这种情况是错误的。在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型(user-defined type)的参数。

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

在这一小节我个人来说学到了一点,那就是操作符重载中尽量能让实现能“集成”,不要所有实现都重新实现一遍,这样可以减少维护费用。

More Effective C++读书笔记18

Item 20:协助完成返回值优化

返回对象时的开销会比较大,会调用对象的构造和析构函数,但是当一个函数必须要返回对象时,这种构造和析构造成的开销是无法消除的。那么还能优化么? 以某种方法返回对象,能让编译器消除临时对象的开销,这样编写函数通常是很普遍的。这种技巧是返回constructor argument而不是直接返回对象。你可以这样做:

1
2
3
4
5
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}
仔细观察被返回的表达式。它看上去好象正在调用Rational的构造函数,实际上确是这样。你通过这个表达式建立一个临时的Rational对象,Rational(lhs.numerator() * rhs.numerator(),  lhs.denominator() * rhs.denominator()); 并且这是一个临时对象,函数把它拷贝给函数的返回值。返回constructor argument而不出现局部对象,这种方法还会给你带来很多开销,因为你仍旧必须为在函数内临时对象的构造和释放而付出代价,你仍旧必须为函数返回对象的构造和释放而付出代价。但是你已经获得了好处。C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。因此如果你在如下的环境里调用operator*:
1
2
3
Rational a = 10;
Rational b(1, 2);
Rational c = a * b;                          // 在这里调用operator*
编译器就会被允许消除在operator*内的临时变量和operator*返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*的临时对象的开销就是零:没有建立临时对象。你的代价就是调用一个构造函数――建立c时调用的构造函数。而且你不能比这做得更好了,因为c是命名对象,命名对象不能被消除(参见条款M22)。不过你还可以通过把函数声明为inline来消除operator*的调用开销。

看完这篇突然认识到,理解编译器优化是非常重要的,对编译器了解构思,代码的效率自然有保证。路漫漫其修远兮,吾将上下而求索!

More Effective C++读书笔记17

这里提到一个概念Over-eager evaluation,可以理解为超前计算,主要有两种方法caching和 prefething,都是以空间换时间来提高效率。

看到这章,让我想起了我以前做的两个项目,一个是给某直辖市做的全市联网XX系统,一个是给某发电厂做的监控环境系统。

1.全市联网系统,由于数据量非常大,导致的性能瓶颈就是每次查询数据库都会很慢,导致用户体验非常不好,而实际情况是数据库中的内容并不是总是频繁改变,所以我采用了catching方式,将数据库中内容预存到内存中,每次读取先从catch中查询,如果有就直接返回,如果没有则再查询数据库,通过这种方式,大大提高了查询效率,这是一个典型的用空间换取时间的方案。

2.发电厂的监控项目是一个需要频繁查询的系统,要实时的更新系统数据,并计算出当时的最大值,最小值和平均值,还要将采集到的数据记录到数据库中以备查询。这里没最麻烦的是计算max,min和ave,因为这些值会根据时长不停的变化,每次查询时都要重新计算,有可能还需要查询数据库,这就导致程序运行非常慢,当时提出一个方案是采用catching方式,提前计算出来结果,并实时更新这些结果,需要的时候,可以直接取。这个方案实施起来非常麻烦,所以我改了下将每次采集的数值计算好,将这些值直接保存到数据库中,然后每次查询都直接读取,效率提高了很多。这种方式可以归结为上述的prefetching方式里。

More Effective C++读书笔记16

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

首先说说使用lazy evaluation的优点,主要是能提高程序效率。延时计算,有可能避免不必要的计算,或者是只需用计算部分结果,而不需要全部结果,这些都可以称为lazy evaluation。C++本身是early evaluation,它需要程序员自己实现lazy evaluation,这样大大增加了程序员的灵活性,可以根据实际情况使用lazy evaluation。

书中提到可以在具体4个地方使用lazy evaluation:

1.引用计数

1
2
3
  class string {};
  string s1 = "hello";
  string s2 = s1;

s2=s1,这句应该调用string的拷贝构造函数,通过重新分配内存,拷贝字符串到s2实现。但是有可能s2根本不会被修改,只是用作输出,这样就可以使用lazy evaluatin,不用分配内存和拷贝字符串,而只要将s2指向s1同一个内存空间即可。

2.区别对待读取和写入

同上一个例子

3.Lazy Fetching(懒惰提取)

如一个类有很多成员,不一定要在构造函数中获取全部的成员变量的初值,尤其是需要I/O操作的成员,延后到使用时在读取不失为一个提高效率的解决方案。

4.Lazy Expression Evaluation(懒惰表达式计算)

More Effective C++读书笔记15

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

80-20 准则说的是大约 20%的代码使用了 80%的程序资源;大约 20%的代码耗用了大约 80%的运行时间;大约 20%的代码使用了 80%的内存;大约 20%的代码执行 80%的磁盘访问;80%的维护投入于大约 20%的代码上;通过无数台机器、操作系统和应用程序上的实验这条准则已经被再三地验证过。80-20 准则不只是一条好记的惯用语,它更是一条有关系统性能的指导方针,它有着广泛的适用性和坚实的实验基础。

当想到 80-20 准则时,不要在具体数字上纠缠不清,一些人喜欢更严格的 90-10 准则,而且也有一些试验证据支持它。不管准确地数字是多少,基本的观点是一样的:软件整体的性能取决于代码组成中的一小部分。

本章主要讲解如何找到影响性能瓶颈的20%的代码的位置。提高效率并不难,难得是如何找到性能的正确瓶颈。方法不外乎有两种,一是猜测或屏经验判断,二是通过profile工具准确测算,这两种方法,当然是第二种方法更科学,更可信。利用好profile工具,提供最有效的数据进行测试,找到程序的瓶颈是一个程序员需要掌握的高级技巧之一。

More Effective C++读书笔记14

Item 14:审慎使用异常规格(exception specifications)

何为异常规格,通俗的理解就是对异常的规范的说明。它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制。

不过在通常情况下,美貌只是一层皮,外表的美丽并不代表其内在的素质。函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的程序其缺省的行为就是halt(停止运行)。在激活的栈中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。

一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,但因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格  译者注)编译器不对此种情况进行检测,并且语言标准也禁止编译器拒绝这种调用方式(尽管可以显示警告信息)。

虽然防止抛出unexpected异常是不现实的,但是C++允许你用其它不同的异常类型替换unexpected异常,你能够利用这个特性。例如你希望所有的unexpected异常都被替换为UnexpectedException对象。你能这样编写代码:

1
2
3
4
5
6
7
class UnexpectedException {};          // 所有的unexpected异常对象被
                                       //替换为这种类型对象
void convertUnexpected()               // 如果一个unexpected异常被
{                                      // 抛出,这个函数被调用
    throw UnexpectedException();
}
set_unexpected(convertUnexpected);

通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。 下面看另外一个书中例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Session {
public:
    ~Session();
  ...
private:
    static void logDestruction(Session *objAddr) throw();
};
Session::~Session()
{
   try
   {
       logDestruction(this);
   }
   catch (...) {  }
}

session的析构函数调用logDestruction记录有关session对象被释放的信息,它明确地要捕获从logDestruction抛出的所有异常。但是logDestruction的异常规格表示其不抛出任何异常。现在假设被logDestruction调用的函数抛出了一个异常,而logDestruction没有捕获。我们不会期望发生这样的事情,但正如我们所见,很容易就会写出违反异常规格的代码。当这个异常通过logDestruction传递出来,unexpected将被调用,缺省情况下将导致程序终止执行。这是一个正确的行为,但这是session析构函数的作者所希望的行为么?作者想处理所有可能的异常,所以好像不应该不给session析构函数里的catch块执行的机会就终止程序。如果logDestruction没有异常规格,这种事情就不会发生(一种防止的方法是如上所描述的那样替换unexpected)。

More Effective C++读书笔记13

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

为什么要通过引用捕获异常,引用捕获异常相对值和指针捕获有何优点?

通过Item12学习可以看到首先,值捕获异常会调用拷贝构造函数2次,而引用捕获只有一次,效率方面,引用要高于值。可以按效率高低进行下排序,由高到低:指针->;引用->;值。

其次值传递在继承体系中会导致slice问题,即子类会被切割为基类。通过这两点就可以完全排除值捕获异常的可能性了。那么指针和引用相比,为什么要采用引用呢?

如果要是通过指针捕获异常的话,那么传递过来的指针是堆栈分配的?还是静态指针?还是栈对象的取地址操作获取的指针?根据不同的情况,会有不同的结果。如果是堆栈分配的指针,那么需要释放内存,如果通过局部对象的地址获取的指针,有可能在捕获到后,已经超过作用域,而指向一个已经销毁的对象,所以对catch来说,通过指针捕获异常,对异常的处理要复杂得多,也难以保持统一。而通过引用捕获异常却没有这些问题,所以当然最终选择最优的方案,通过引用捕获异常。

More Effective C++读书笔记12

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

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

把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。

第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。

最后一点,catch 子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的 catch 将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>;
using namespace std;
class Base
{
public:
    Base() { cout << "constructor" << endl; }
    ~Base()  { cout << "destructor" << endl; }
    Base(const Base& m) { cout << "copy constructor" << endl; pInt = m.pInt; }
    void Message() {cout << "Base::Message" <<endl;}
private:
    int pInt;
};
class SubClass: public Base
{
public:
    SubClass() { cout << "constructor_sub" << endl; }
    ~SubClass(){ cout << "destructor_sub" << endl; }
    SubClass(const SubClass& ) { cout << "copy constructor_sub" << endl; }
    void Message() {cout << "SubClass::Message" <<endl;}
private:
};
int main() {
    int iTemp = 0;
    try
    {
        throw iTemp;
    }
    catch (double d)
    {
        cout << "double" << endl;
    }
    catch (int i)
    {
        cout << "int" << endl;
    }
    int* piTemp = NULL;
    try
    {
        throw piTemp;
    }
    catch (void* e)
    {
        cout << "void*" << endl;
    }
    catch (int* i)
    {
        cout << "int* " << endl;
    }
    SubClass n;
    try
    {
        throw n;
    }
    catch (Base& e)
    {
        e.Message();
        //throw;
        //throw e;
    }
    catch (SubClass& ex)
    {
        e.Message();
    }
    Base m;
    //Base* m = new Base();
    try
    {
        throw m;
    }
    catch (Base e)
    {
        cout << "Base" << endl;
    }
//    catch (Base& e)
//    {
//        cout << "Base&" << endl;
//    }
//    catch (const Base& e)
//    {
//        cout << "const Base&" << endl;
//    }
//    catch (Base* e)
//    {
//        e->;Message();
//        delete e;
//    }
}

第一部分throw iTemp;输出:cout << “int” << endl; 对应上面的第二种情况,异常传递的类型如果是基础类型的话不能进行隐式转换。

第二部分throw piTemp;输出:cout << “void” << endl;对应上面的第二种情况,说明如果是异常抛出指针的话,catch参数可以被隐式转换为void指针。

第三部分throw n;输出:

1
2
3
4
5
6
7
  constructor
  constructor_sub
  constructor
  copy constructor_sub
  Base::Message
  destructor_sub
  destructor

对应上面的第二种情况,说明在异常捕获中,子类异常可以通过基类参数捕获到。对应上面的第三种情况,说明捕获的顺序是代码中出现的顺序。把注释掉的//throw; //throw e;两句分别打开,运行结果确是一样的,跟书中描述不尽相同。理论上讲throw应该抛出的时subclass类型的异常。 最后一部分主要是验证异常参数传值,传引用和传指针的区别。通过程序结果可以看出,不论是传值还是传引用,都会进行拷贝构造,不同之处为传值会进行两次拷贝构造。不过在传指针时,确没有进行拷贝构造函数的调用。和函数参数穿指针一样。但是需要注意的是,异常处理有可能会跟异常抛出位置不是同一个作用域,那样抛指针异常的时候,有可能会造成异常泄露,所以我在最后显示调用delete e;释放资源。

More Effective C++读书笔记11

Item 11:禁止异常信息(exceptions)传递到析构函数外

书中所言,会有两种情况调用析构函数。第一种是在正常情况下删除一个对象,例如对象超出作用域或者显示delete。第二种是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象。在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用 terminate 函数。

为什么要防止在析构函数中的异常传递到函数外,除了上面说的第二种情况外,还有就是将异常传递出去后,程序转到异常处理程序,析构函数中异常之后的程序将不会再被执行,这有可能会导致资源的泄露。

这里对stack unwinding说明下,堆栈展开是C++的一个概念,每个函数都回有它的堆栈,调用函数时会对函数参数和局部成员进行入栈,而函数结束时会按入栈相反的顺序进行出栈。举个例子说下的上面的第二种情况:

1
2
3
4
5
void func(string s)
{
    object obj(s);
    obj.dosomething();
}

如果在dosomething()中发生某些异常,那么异常将传递出去,直到遇到异常捕获函数为止。而随着异常的传递,函数func已经结束,其局部变量obj会被析构,如果在析构的过程中再次出现异常,那么C++将调用 terminate 函数,程序直接终止。

More Effective C++读书笔记10

Item 10:在构造函数中防止资源泄漏

该条款讲述的内容跟上一条款很相似,应该都可以归属为编写异常安全代码范畴。在构造函数中尽量处理简单的代码,防止抛异常,因为C++并没有对构造函数异常做析构的动作,并不会在构造函数发生异常的情况下,调用析构函数去释放资源。这是考虑到效率问题。

我们还是举个例子更直观的描述如何在构造函数中编写异常安全的代码,要做个通讯录类,内容包括姓名,地址,图片和一段声音。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Image
{
public:
    Image(string name);
private:
    string imageName;
};
class Audio
{
public:
    Audio(string name);
private:
    string audioName;
};
class BookEntry
{
public:
    BookEntry(string name, string adress, Image* image, Audio* audio);
    ~BookEntry();
private:
    string m_name;
    string m_adress;
    Image* m_image;
    Audio* m_audio;
};
BookEntry::BookEntry(string name, string adress, string imageName, string audioName)
: m_name(name), m_adress(adress),m_image(NULL),m_audio(NULL)
{
   m_image = new Image(imageName);
   m_audio = new Audio(audioName);
}
BookEntry::~BookEntry()
{
    delete m_image;
    delete m_audio;
}

因为C++ delete删除空指针是不做任何操作的,所以析构函数中并没有对指针进行判断。

我们考虑正常情况下构造函数全部指向完毕(完全构造),当销毁BookEntry的时候,会调用析构函数释放资源,没有任何问题。但是如果是构造函数中出现异常情况怎么办?最简单的办法就是万能的try catch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BookEntry::BookEntry(string name, string adress, string imageName, string audioName)
: m_name(name), m_adress(adress),m_image(NULL),m_audio(NULL)
{
    try
    {
        m_image = new Image(imageName);
        m_audio = new Audio(audioName);
    }
    catch(...)
    {
        delete m_image;
        delete m_audio;
    }
}

OK, 没有问题了,但是看起来catch中的代码跟析构函数的代码一样,OK,利用refactoring method我们可以合并代码,abstract a public method。

那么如果我们再修改下代码,将成员变量改为const常量

1
2
Image* const m_image;
Audio* const m_audio;

那么就只能在成员初始化列表中对指针进行初始化了。

1
2
3
BookEntry::BookEntry(string name, string adress, string imageName, string audioName)
: m_name(name), m_adress(adress),m_image((iamgeName.empty()?NULL:new Image(imageName))),m_audio((audioName.empty()?NULL:new Audio(audioName)))
{}

在成员初始化函数列表中,是不可以包含语句的,只能使用表达式,那么try catch就无法再使用了,那么该如何解决呢?估计看过前一章的一定会想到的,对就是使用智能指针。

1
2
3
4
5
6
7
8
9
const share_ptr<Image>; m_image;
const share_ptr<Audio>; m_audio;
BookEntry::BookEntry(string name, string adress, string imageName, string audioName)
: m_name(name), m_adress(adress),m_image((iamgeName.empty()?NULL:new Image(imageName))),m_audio((audioName.empty()?NULL:new Audio(audioName)))
{
}
BookEntry::~BookEntry()
{
}

连析构函数也省了。