2.5.2、virtual的评判
在像Java这样的有些语言中,所有的成员函数自动为virtual,所以它们可以被恰当地重载。在c++中不是这样。关于在c++中反对使一切皆为virtual的争论,以及一开始就有了这个关键字的原因,与vtable的开销有关。调用一个virtual成员函数,程序需要执行额外的操作去间接引用指向合适代码的指针去执行。在大部分情况下对性能的影响是微乎其微的,但是c++的设计者认为,至少在当时是这样,让程序员自己去决定其性能影响是必要的,会更好一些。如果成员函数从来不会被重载,就没有必要使其为virtual,去承担性能的损失。然而,在今天的CPU的情况下,性能的影响是以纳秒计的,对于未来的CPU影响会更小。在大部分应用程序中,在使用virtual成员函数与避免使用它们之间测量不出性能的不同。
但是,在特定的情况下,性能开销可能会比较大,可能需要有可以避免的选项。例如,假设你有一个Point类,该类有一个vitual成员函数。如果有另外一个数据结构保存了上百万甚至上十亿的Point,在第一个point的virtual成员函数的调用会产生巨大的开销。在这种情况下,在Point类中避免任何virtual成员函数就可能是明智的了。
每个对象对内存的使用也是一个小小的影响。在成员函数实现之外,每个对象也需要一个指向它的vtable的指针,它占用了很小量的空间。在绝大部分情况下这不是一个问题。然而,有时候确实也有问题。再举刚才的那个Point类的例子,容器保存了上十亿的Point。在这种情况下,这额外的内存要求也变得很严重。
2.5.3、virtual析构函数的需要
析构函数几乎总是virtual。使析构函数non-virtual很容易地导致内存没有被对象析构函数释放掉的情况发生。只有把类标记成final才能使得它的析构函数non-virtual。
例如,如果一个继承类在构造函数中使用内存动态分配,在析构函数中删除,如果析构函数从来不会被调用的话,它就不会被释放。同样地,如果继承类有在类实例被破坏时自动删除的成员,例如std::unique_ptr,那么如果析构函数不被调用的话,那些成员也不会被删除。
如下代码所示,如果它是non-virtual的话,是很容易的“糊弄”编译器,让它忽略对析构函数的调用的。
import std;
using namespace std;
class Base
{
public:
Base() = default;
~Base() {}
};
class Derived : public Base
{
public:
Derived()
{
m_string = new char[30];
println("m_string allocated");
}
~Derived()
{
delete[] m_string;
println("m_string deallocated");
}
private:
char* m_string;
};
int main()
{
Base* ptr{ new Derived{} }; // m_string is allocated here.
delete ptr; // ~Base is called, but not ~Derived because the destructor
// is not virtual!
}
从下面的输出可以看出来,Derived对象从来没有被调用过,也就是说,“m_string deallocated”信息从来没有输出过:
m_string allocated
从技术上讲,上面代码中的delete调用的行为没有被标准定义。在这种没有定义的情况下c++编译器可以做它想做的任何事儿。然而,大部分编译器只是调用基类的析构函数,而不是继承类的析构函数。
修正措施就是在基类中标记析构函数为virtual。如果你不想在析构函数中做额外的工作,只是想使其为virtual,可以显式地缺省它。下面是例子:
class Base
{
public:
Base() = default;
virtual ~Base() = default;
};
这样改了以后,输出就与预想的一致了:
m_string allocated
m_string deallocated
记住从c++11以后,拷贝构造函数与拷贝赋值操作符的生成如果类有一个用户定义的析构函数的话会失效。基本上,一旦你有了一个用户声明的析构函数,五规则就来了。这意味着你需要声明拷贝构造函数,拷贝赋值操作符,move构造函数,与move赋值操作符,也可以显式地缺省它们。本章的例子中没有这样做,是由于保持准确到位的考虑。
警告:除非是有特别的原因不去做,或者类被标识为了final,析构函数都应该被标记为virtual。构造函数不可以也没有必要为virtual,因为总是要在生成对象时指定确切的类构建。
在本章的一开始,建议在成员函数上使用override关键字,意思是重载基类的成员函数。在析构函数上使用override关键字也是可能的。这确保了在基类中的析构函数不是virtual的情况下编译器会触发错误。可以将virtual,override,与default组合在一起,举例如下:
class Derived : public Base
{
public:
virtual ̃Derived() override = default;
};
2.6、防止重载
除了把整个类标记为final,c++也允许将单个的成员函数标记为final。这样的成员函数不能在继承类中被重载。例如,在DerivedDerived中的Derived类中的someFunction()重载会导致编译错误:
class Base
{
public:
virtual ̃Base() = default;
virtual void someFunction();
};
class Derived : public Base
{
public:
void someFunction() override final;
};
class DerivedDerived : public Derived
{
public:
void someFunction() override; // Compilation error.
};