类和继承(五):不应当使用虚函数的情况

关于是否应该所有成员函数缺省为虚函数的争论

只有涉及继承时,才需要考虑与此相关的问题

  • 适用的情况
    • 基类派生出的子类需要重写(or 覆盖,override),基类中相应的成员和函数应声明为虚函数
  • 不适用的情况
    • 虚函数代价并不是非常高,当时会带来一定的额外开销

有些情况下非虚函数能够正确运行而虚函数不能

有些类并非为继承而设计(设计时并不兼容被继承)

效率

程序调用显式提供的对象的虚拟成员函数,优秀的编译器不带来额外的开销(与调用非虚函数相同)

  • 如:
1
2
T x;
x.f();
  • 在这里,f是否虚函数应该没有影响;产生对T::f的直接调用

    • 若所有对成员函数的调用都是通过显式指定的对象进行的则成员函数是否是虚函数就无关紧要了
    • 一旦通过指针或引用进行调用就是有意义的
    • 虚函数会产生额外的开销

用内存引用(memory reference)计数衡量大概的开销:
随着微处理器的速度越来越快,内存应用耗时占比会越高,此项指标更精确;
但高速缓存的更广泛更大量的使用也使得这项粗糙的估计更不准确;
但在这儿这样估计是必要的。

  • 如:
1
2
3
4
5
int& IntArray::operator[](unsigned n) {
if (n >= arraysize)
throw "subscript out of range";
return data[n];
}
  • 设函数为内联函数,好的实现在直接通过对象使用operator[]时不引入新开销

  • 通过指针或引用调用operator[]的开销与三个内存引用相关:

      1. 对指针本身
      1. 为此成员函数初始化this指针
      1. 用于调用返回序列
  • 调用虚函数则通常需要多出另外的三个内存应用:

      1. 从对象取出描述对象类型的表的地址值
      1. 取出虚函数的地址
      1. 在可能的较大外围对象中,取出本对象的偏移量

成员函数很大时,这样的开销远小于函数执行所需开销,额外的开销不成问题

  • 一个潜在开销很大的例子及改进策略:
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
class InputBuffer {
public:
//...
virtual int get();
//...
};
// all derived class of InputBuffer can override get()
int countlines(InputBuffer& b) {
int n = 0;
int c;
// get() would be called a lot of times
while ((c = b.get()) != EOF) {
if (c == '\n') ++n;
}
return n;
}
// imroved version
class InputBuffer {
public:
//...
int get() {
// call a virtual function only in specified condition
if (next >= limit)
return refill();
return *next++;
}
protected:
// read in plenty of characters
virtual int refill();
private:
char* next;
char* limit;
};

行为

当派生类并不严格扩展基类行为时,成员函数定义为虚函数会导致不正确的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class IntArray {
public:
IntArray(unsigned);
int& operator[](unsigned);
unsigned size() const;
//...
};
class IntBlock: public Int Array {
public:
IntBlock(int l, int h): low(l), high(h), IntArray(l > h ? 0 : h - l + 1) {}
int& operator[](int n) {
return IntArray::operator[](n - low);
}
private:
int low, high;
};
int sum(IntArray& x) {
int result = 0;
for (int i = 0; i < x.size(); ++i)
result += x[i];
return result;
}
  • 当将一个实际类型为IntBlock的对象传给sum()时,只有operator[]为非虚函数才有正确的行为

有些函数只为特定有限制的用途而设计

  • 类的接口可以有两种用户:使用该类对象的人和从这个类派生新类的人

  • 有的类会故意不考虑其他人如何通过继承改变它的行为

虚析构函数

  • 有需要自定义的析构函数
    • 存在此种情形:指向基类的指针或引用都有其静态类型,并实际上都指向派生类的对象

《C++沉思录(Cplusplus Thinking)》笔记