Skip to content

18.4 虚构造函数,虚赋值,重写虚函数

By Alex on February 1st, 2008 | last modified by nascardriver on June 26th, 2020

翻译 by dashjay 2020-12-21 | last modified at 2020-12-21

虚构造函数

尽管C++会在你不提供结构函数时默认提供一个,但是你有时要体用自己的结构函数(尤其是需要分配内存的类)。你应该总是将析构函数,设置为虚函数,在处理集成关系的时候。考虑下面的例子:

#include <iostream>
class Base
{
public:
    ~Base() // note: not virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};

class Derived: public Base
{
private:
    int* m_array;

public:
    Derived(int length)
      : m_array{ new int[length] }
    {
    }

    ~Derived() // note: not virtual (your compiler may warn you about this)
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};

int main()
{
    Derived *derived { new Derived(5) };
    Base *base { derived };

    delete base;

    return 0;
}

注意:如果你编译了上面的例子,你的编译器可能提示你有关非虚解构函数(故意在例子中呈现的)。你也可以禁用编译器的 flag,这样可以将 warnings 视为报错来执行。

因为 base 是一个 Base 指针,当 base 被删除的时候,程序检查 Base 解构函数是虚函数。而它并不是,所以它假设它只需要调用 Base 类的析构函数,我们听过下面的输出来了解这个:

Calling ~Base()

However, we really want the delete function to call Derived’s destructor (which will call Base’s destructor in turn), otherwise m_array will not be deleted. We do this by making Base’s destructor virtual:

#include <iostream>
class Base
{
public:
    virtual ~Base() // note: virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};

class Derived: public Base
{
private:
    int* m_array;

public:
    Derived(int length)
      : m_array{ new int[length] }
    {
    }

    virtual ~Derived() // note: virtual
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};

int main()
{
    Derived *derived { new Derived(5) };
    Base *base { derived };

    delete base;

    return 0;
}

这个程序产生下列效果:

Calling ~Derived()
Calling ~Base()

规则:无论你何时处理继承,你应该显式的设置析构函数为虚函数

与普通虚函数成员一样,如果一个基类函数是虚函数,所有的继承重写类将会被当做虚函数,无论你是否指定。如果派生类的解构函数不为空,就请标注为虚函数。

虚赋值方法

可以将赋值操作符设为虚函值,不像析构函数那样,设置为虚构函数总是一个好主意,将赋值操作符设置为虚拟化就容易引起很多bug,并且进入在这个教程之外的话题。因此,为了简单起见,我们建议你目前先让虚函数为非虚函数。

忽略虚函数

有时候你想要忽略一个函数的虚化,例如,看看下面的代码:

class Base
{
public:
    virtual const char* getName() const { return "Base"; }
};

class Derived: public Base
{
public:
    virtual const char* getName() const { return "Derived"; }
};

在这个例子中,你也许想要使用指向派生类的基类指针调用 Base::getName() 而不是 Derived::getName()。为了这样,只需要简单的使用范围解析操作符:

#include <iostream>
int main()
{
    Derived derived;
    const Base &base { derived };
    // Calls Base::GetName() instead of the virtualized Derived::GetName()
    std::cout << base.Base::getName() << '\n';

    return 0;
}

你也许不会经常这样使用,但知道也还是不错的。

我们是否应该使所有结构函数为虚函数

这是一个新手常问的问题。正如上方例子所说,如果基类结构函数没有标注为虚函数,程序员稍后删除指向派生类的基类指针时程序有内存泄露的风险。一个避免的方式就是将所有析构函数标注为虚函数,但是你应该那样做么?

说”是“很容易,然后你稍后使用任何类作为一个基类 —— 但这样做存在性能代价(一个虚指针会添加到每个类的实例中)。因此你不得不平衡开销,和你的意图。

传统的智慧(最初由 Herb Sutter 提出,一个很重要的 C++ 老师)建议避免如下的集中非虚函数结构函数内存泄露情况,”一个基类结构函数应该既是public的又是虚函数,或者 是protected 且非虚函数。“,一个带有 protected 解构函数的类实例不能被通过指针来删除,因此阻止了当一基类有非虚析构函数的时候,意外的通过基类指针删除一个派生类。不幸的是,这也意味着基类不能被通过一个基类指针删除,基本上意味着类不能被动态分配,或者如期被派生类删除。这也排除了在这个类上使用智能指针(例如 std::unique_ptrstd::shared_ptr),限制了那个规则(我们将在之后的课程中介绍智能指针)的可用性。它也意味着基类不能被在栈上分配。那是一个相当重的代价。

(原文太长)Conventional wisdom (as initially put forth by Herb Sutter, a highly regarded C++ guru) has suggested avoiding the non-virtual destructor memory leak situation as follows, “A base class destructor should be either public and virtual, or protected and nonvirtual.” A class with a protected destructor can’t be deleted via a pointer, thus preventing the accidental deleting of a derived class through a base pointer when the base class has a non-virtual destructor. Unfortunately, this also means the base class can’t be deleted through a base class pointer, which essentially means the class can’t be dynamically allocated or deleted except by a derived class. This also precludes using smart pointers (such as std::unique_ptr and std::shared_ptr) for such classes, which limits the usefulness of that rule (we cover smart pointers in a later chapter). It also means the base class can’t be allocated on the stack. That’s a pretty heavy set of penalties.

既然 final 描述符已经被引入到这个语言,我们的建议如下:

  • 如果你的类打算被继承,请确保析构函数是虚函数。
  • 如果你的类不打算被继承,将你的类标注为 final,这将会阻止其他类一开始就从该类派生,但是不会对该类本身带来任何限制