Effective C++ 第二版 13) 初始化列表 14) 虚析构函数

来源:互联网 发布:卖py什么意思网络用语 编辑:程序博客网 时间:2024/06/12 21:14

条款 13: 初始化列表中成员列出的顺序和他们在类中声明的顺序相同 

Pascal和Ada可以任意设定数组下标上下限, 数组下标可以是10到20, 不一定是0到10; C语言则习惯坚持从0开始计数;

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T>
class Array {
public:
    Array(int lowBound, int highBound);
...
private:
    vector<T> data; // 数组数据存储在vector 对象中关于 vector 模板参见条款49
    size_t size; // 数组中元素的数量
    int lBound, hBound; // 下限,上限
};
template<class T>
Array<T>::Array(int lowBound, int highBound) : size(highBound - lowBound + 1),
lBound(lowBound), hBound(highBound),data(size){}

>size_t是unsigned int, 构造函数会进行合法性检查, 保证highBound大于等于lowBound; 

即使数组上下限合法, 也没人知道data里到底有多少个元素;

Note 类成员是按照他们在类里被声明的顺序进行初始化的, 和成员初始化列表中的顺序没有关系; [写类的时候要注意成员顺序]

>上例中的data总是先初始化, 然后是size, lBound和hBound; 所以data(size)将是unexpected;


C++这么做的理由:

1
2
3
4
5
6
7
8
9
class Wacko {
public:
    Wacko(const char *s): s1(s), s2(0) {}
    Wacko(const Wacko& rhs): s2(rhs.s1), s1(0) {}
private:
    string s1, s2;
};
Wacko w1 = "Hello world!";
Wacko w2 = w1;

>如果按照初始化列表顺序初始化, 那w1和w2的数据成员被创建的顺序会不同;

对一个对象的成员来说, 析构函数被调用的顺序总是和他们在构造函数里被创建的顺序相反; [先构造的后析构] 如果允许构造顺序按照初始化列表顺序, 编译器就要为每一个对象跟踪成员初始化的顺序, 以保证析构的顺序正确, 这样带来的是昂贵的开销; 
所以为了避免开销, 同一种类型的对象在构造和析构过程中对成员的处理顺序都是相同的, 不在乎成员在初始化列表中的顺序;

只有非静态数据成员的初始化遵守以上规则;

静态数据成员的行为像全局和名字空间对象, 只会被初始化一次; 

Note 基类数据成员总是在派生类数据成员之前被初始化, 在继承时, 要把基类的初始化列在成员初始化列表的最前面; [基类的构造放在初始化列表最前面]

如果使用多继承[不推荐], 基类被初始化的顺序和他们被派生类继承的顺序一致, 在成员初始化列表中的顺序会被忽略;

要保证初始化列表中成员的顺序和类内声明的顺序一致;


条款14: 确定基类有虚析构函数

如果类想跟踪有多少个对象, 一个简单的方法是创建静态成员来统计对象个数, 构造函数+1, 析构函数-1;

敌方目标类:

1
2
3
4
5
6
7
8
9
10
11
12
class EnemyTarget {
public:
    EnemyTarget() { ++numTargets; }
    EnemyTarget(const EnemyTarget&) { ++numTargets; }
    ~EnemyTarget() { --numTargets; }
    static size_t numberOfTargets() { return numTargets; }
    virtual bool destroy(); // 摧毁EnemyTarget对象后返回成功
private:
    static size_t numTargets; // 对象计数器
};
// 类的静态成员要在类外定义; // 缺省初始化为0
size_t EnemyTarget::numTargets;

坦克:

1
2
3
4
5
6
7
8
9
10
class EnemyTank: public EnemyTarget {
public:
    EnemyTank() { ++numTanks; }
    EnemyTank(const EnemyTank& rhs) : EnemyTarget(rhs) { ++numTanks; }
    ~EnemyTank() { --numTanks; }
    static size_t numberOfTanks() { return numTanks; }
    virtual bool destroy();
private:
    static size_t numTanks; // 坦克对象计数器
};

如果程序在某处new了EnemyTank对象, 然后delete掉:  EnemyTarget *targetPtr = new EnemyTank; ... delete targetPtr;

看起来两个类在析构函数里都对构造函数做的操作进行了清除, new出来的对象也用delete删除了, 但实际上程序的行为是不可预测的;

Note 当通过基类的指针去删除派生类的对象, 基类没有虚析构函数时, 结果是不可确定的; (派生类的析构可能永远不会调用)

>将EnemyTarget的析构函数声明为virtual, 对象内存释放时, EnemyTank和EnemyTarget的析构函数都会被调用;

Note 一般基类都包含虚函数, 如果一个类不准备作为基类, 虚函数将是一个坏主意;

1
2
3
4
5
6
7
8
// 一个表示2D 点的类
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

> 如果short int占16位, Point对象刚好放入一个32位的寄存器中; Point对象可以作为32位数据传给C或其他语言写的函数中; 如果Point的析构函数是virtual, Point对象将大于32位;

实现虚函数需要对象附带额外信息, 来确定对象在运行时该调用哪个虚函数, 大多数编译器使用vptr(虚函数表指针); vptr指向称为vtbl(虚函数表)的函数指针数组; 每个有虚函数的类都附带一个vtbl, 当对象的某个虚函数进行请求调用时, 实际被调用的函数是根据指向vtbl的vptr在vtbl中找到相应的函数指针来确定的;

>如果Point类包含虚函数, 对象大小从32位变成, 32 + 32(vptr); C++中的Point对象和其他语言比如C中声明的结构变得不同, 用其他语言写的函数也不能传递Point了, 导致代码无法移植;


可命名的数组:[扩展型继承也有虚析构的需求]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T> // 基类模板
class Array { // (来自条款13)
public:
    Array(int lowBound, int highBound);
    ~Array();
private:
    vector<T> data;
    size_t size;
    int lBound, hBound;
};
template<class T>
class NamedArray: public Array<T> {
public:
    NamedArray(int lowBound, int highBound, const string& name);
...
private:
    string arrayName;
};

>如果在某个地方将指向NamedArray类型的指针换成了Array类型的指针, 用delete来删除Array指针, 会出现不确定的行为;

1
2
3
4
5
6
NamedArray<int> *pna = new NamedArray<int>(10, 20, "Impending Doom");
Array<int> *pa;
...
pa = pna; // NamedArray<int>* -> Array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayName会造成泄漏,因为*pa 的NamedArray永远不会被删除


纯虚函数将产生抽象类--不能实例化的类; 抽象类是准备作为基类的, 所以必须要有虚析构函数, 在想要成为抽象类的类里声明一个纯虚析构函数;

1
2
3
4
class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
    virtual ~AWOV() = 0; // 声明一个纯虚析构函数
};

Note 必须提供纯析构函数的定义: AWOV::~AWOV() {} [纯虚函数可以有定义]

因为析构函数的工作方式是: 最底层的派生类的析构先调用, 然后各个基类的析构函数被调用; 即使是抽象类, 编译器也要产生对~AWOV的调用, 所以要有函数体;

如上所示, 抽象类析构函数可能什么都没做, 将析构函数声明为内联函数, 可以避免对空函数的调用产生开销;

作为析构函数, 地址会进入到类的vtbl, 但内联函数不是作为独立的函数存在的[类似#define宏, 代码直接替代函数], 所以必须用特殊的方法得到地址: 如果声明虚析构函数为inline, 将会避免调用它们时产生的开销, 但编译器还是会产生一个此函数的拷贝;

原创粉丝点击