C++开发中级


978 浏览 5 years, 10 months

6.5 virtual的真相

版权声明: 转载请注明出处 http://www.codingsoho.com/

virtual的真相

在本章前面讲述重写方法时,曾经说过只有虚方法才能够被正确地重写。我们使用定语“正确”的原因是如果方法不是虚的,也可以试着重写这个方法,但是这样做会导致微妙的错误。

1.隐藏而不是重写

下面的代码显示了一个超类以及一个子类,每个类都有一个方法。子类试图重写超类的方法,但是在超类中没有将这个方法声明为虚的。

class Super
{
    public:
        void go(){ cout << "go() called on Super" << endl; }
};

class Sub : public Super
{
    public:
        void go(){ cout << "go() called on Sub" << endl; }
};

试着用用Sub对象调用go()方法好像没有问题。

Sub mySub;
mySub.go();

正如预期的那样,Sub调用了go()。然而,由于这个方法不是虚的,因此实际上没有被重写。

相反,Sub类创建了一个新的方法,名称也是go(),这个方法与Super类的go()方法完全没有关系。为了证实这一点,只需要用Super的指针或者引用调用这个方法:

Sub mySub;
Super& ref = mySub;
ref.go();

您可能希望输出是go() called on Sub,但是实际上,输出是go() called on Super。这是因为ref变量是一个Super引用,并且省略了virtual关键字。当调用go()方法时,只是执行了Super的go()方法。由于不是虚方法,就不需要考虑子类是否重写了这个方法。

提示: 试图重写非虚方法将“隐藏”超类定义的方法,并且这个重写的方法只能在子类环境中使用。

如何实现virtual

为了理解为什么会隐藏方法,需要了解virtual关键字的真正作用。在C++编译类的时候,会创建一个包含类所有数据成员以及方法的二进制对象。在非虚情况下,将控制交给正确方法的代码是硬编码,此时会根据编译时类型调用方法。

如果方法被声明为虚的,会使用名为虚表(vtable)的特定内存区域调用正确的实现。为了更好地理解虚表是如何让方法重写变为可能,考虑下面的Super以及Sub类:

每个具有一个或者多个虚方法的类都有一张虚表,虚表中包含了指向虚方法实现的指针。通过这种方法,当使用某个对象调用方法的时候,指针也进入虚表,然后根据实际的对象类型执行正确版本的方法。

class Super
{
    public:
        virtual void func1() {}
        virtual void func2() {}
};

class Sub : public Super
{
    public:
        virtual void func2() {}
};

对于这个示例,考虑下面的两个实例

Super mySuper;
Sub mySub;

下图显示了这两个实例虚表的高级视图。mySuper对象包含了指向虚表的一个指针,虚表有两项,一项是func1(),另一项是func2()。这两个项指向了Super::func1()以及Super::func2()的实现。

mySub也包含了指向虚表的一个指针,这个虚表也包含两项,一项func1(),一项func2()。mySub虚表的func1()项指向Super::func1(),因为Sub没有重写func1 n。但是mySub虚表的func2()项指向Sub::func2()。

3. 使用virtual的理由

建议您将所有方法都声明为Virtual,这一事实可能让您感到迷惑,既然这样为什么要使用virtual关键字呢?编译器不能自动将所有方法都声明为virtual吗?答案是可以。许多人认为C++语言应该将所有方法都声明为virtual, Java语言就是这么做的。

由于虚表的开销问题,有观点反对将一切都声明为virtual,这也是使用这个关键字的主要原因。为了调用虚方法,程序需要执行额外的操作,那就是对指针解除引用以执行正确的代码。

在多数情况下,这样做会轻微地影响性能,但是C++的设计者认为最好让让程序员决定是否有必要影响性能。如果方法永远不会被重写,就没必要将其声明为virtual从而影响性能。然而对于当今的CPU而言,性能的影响可以用十亿分之一秒来度量,并且随着CPU的发展时间会进一步缩短。

在多数应用程序中,您无法察觉到使用虚方法以及不使用虚方法带来的性能差别,因此应该遵循建议将所有方法声明为virtual,包括析构函数。

virtual对于代码大小也有影响。为了实现虚方法,每个对象都有一个指针,这个指针会占用一点空间。

虚析构函数的需求

即使认为不应该将所有方法都声明为virtual的程序员,也坚持认为应该将析构函数声明为virtual。原因是如果析构函数未声明为virtual,很容易在销毁对象的时候不释放内存。

class Super
{
    public:
        Super();
        ~Super();
};

class Sub : public Super
{
    public:
        Sub() { mString = new Char[30]; }
        ~Sub() { delete [] mString }
    protected:
        char* mString;
};

int main()
{
    Super* ptr = new Sub(); // mString is allocated here.
    delete ptr; // ~Super() is called, but not ~Sub because the destructor is not virtual
    return 0;
}

提示: 除非有特别原因,否则强烈建议将所有方法(包括析构函数,构造函数除外)声明为virtual。构造函数不需要也无法声明为virtual,因为在创建对象的时候总是会明确地指定类