跳转到内容

C++ 编程/类/多态

来自 Wikibooks,开放世界中的开放书籍

动态多态(重写)

[编辑 | 编辑源代码]

到目前为止,我们已经了解到可以通过继承在类中添加新的数据和函数。但是,如果我们希望派生类继承基类的方法,但拥有不同的实现方式呢?这就是我们在谈论多态性的时候,它是 OOP 编程中的一个基本概念。

如之前在 编程范式部分 所见,多态性 分为两个概念:静态多态性动态多态性。本节重点介绍动态多态性,它在派生类重写基类中声明的函数时适用于 C++。

我们通过在派生类中重新定义方法来实现这个概念。但是,我们在这样做时需要考虑一些因素,因此现在我们必须介绍动态绑定、静态绑定和虚方法的概念。

假设我们有两个类,ABB派生自A,并重新定义了存在于类A中的方法c()的实现。现在假设我们有一个类B的对象b。指令b.c()应该如何解释?

如果b在堆栈中声明(不是作为指针或引用声明),编译器会应用静态绑定,这意味着它会解释(在编译时)我们指的是存在于B中的c()的实现。

但是,如果我们将b声明为类A的指针或引用,编译器在编译时无法知道要调用哪个方法,因为b可以是AB类型。如果这在运行时解决,则将调用存在于B中的方法。这称为动态绑定。如果这在编译时解决,则将调用存在于A中的方法。这同样是静态绑定。

虚成员函数

[编辑 | 编辑源代码]

成员函数相对简单,但经常被误解。这个概念是设计类层次结构中子类化类的一个重要组成部分,因为它决定了特定上下文中的重写方法的行为。

虚成员函数是类成员函数,可以在从声明它们的类派生的任何类中被重写。然后,成员函数体被替换为派生类中的一组新的实现。

注意
重写虚函数时,可以更改派生类成员函数的私有、受保护或公共状态访问状态。

通过在方法声明之前放置关键字,我们表明当编译器必须决定应用静态绑定还是动态绑定时,它将应用动态绑定。否则,将应用静态绑定。

注意
虽然在子类定义中使用虚拟关键字不是必需的(因为如果基类函数是虚拟的,所有子类对其的重写也将是虚拟的),但在为将来的重复利用(用于在同一项目之外使用)生成代码时,这样做是一种很好的风格。

再次,这应该用一个例子来解释

class Foo
{
public:
  void f()
  {
    std::cout << "Foo::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Foo::g()" << std::endl;
  }
};
 
class Bar : public Foo
{
public:
  void f()
  {
    std::cout << "Bar::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Bar::g()" << std::endl;
  }
};
 
int main()
{
  Foo foo;
  Bar bar;

  Foo *baz = &bar;
  Bar *quux = &bar;

  foo.f(); // "Foo::f()"
  foo.g(); // "Foo::g()"
 
  bar.f(); // "Bar::f()"
  bar.g(); // "Bar::g()"

  // So far everything we would expect...
 
  baz->f();  // "Foo::f()"
  baz->g();  // "Bar::g()"

  quux->f(); // "Bar::f()"
  quux->g(); // "Bar::g()"
 
  return 0;
}

我们对两个对象的第一次调用f()g()很简单。然而,当我们使用指向 Foo 类型的指针 baz 时,事情变得有趣起来。

f()不是,因此对f()的调用将始终调用与指针类型关联的实现 - 在这种情况下是来自 Foo 的实现。

注意
记住,重载重写是不同的概念。

虚函数调用在计算上比普通函数调用更昂贵。虚函数使用指针间接寻址,调用,并且需要比普通成员函数多几条指令。它们还要求任何包含虚函数的类/结构的构造函数初始化一个指向其虚成员函数的指针表。

所有这些特性都将在性能和设计之间产生权衡。应该避免在没有现有结构需求的情况下预先声明虚函数。请记住,仅在运行时解析的虚函数无法内联。


Clipboard

要做
虚函数和内联问题的示例。


注意
使用类模板可以解决使用虚函数的一些需求。当我们介绍模板时,将对此进行介绍。

纯虚成员函数

[编辑 | 编辑源代码]

还有一种有趣的可能性。有时我们不想提供函数的任何实现,而是希望要求对我们的类进行子类化的人自己提供实现。这就是虚函数的情况。

要指示一个纯函数而不是实现,我们只需在函数声明之后添加一个"= 0"。

再次 - 一个例子

class Widget
{
public:
   virtual void paint() = 0;
};

class Button : public Widget
{
public:
   void paint() // is virtual because it is an override
   {
       // do some stuff to draw a button
   }
};

因为paint()是一个纯函数在 Widget类中,我们必须在所有具体子类中提供实现。如果我们没有提供,编译器会在构建时给出错误。

这有助于提供接口 - 我们期望基于特定层次结构的所有对象的行为,但当我们想忽略实现细节时。

那么这为什么有用呢?

让我们以我们之前的例子为例,我们有一个纯用于绘制。在许多情况下,我们希望能够对小部件执行操作,而不必担心它是哪种小部件。绘制是一个简单的例子。

想象一下,我们的应用程序中有一些东西会在小部件变得活动时重新绘制小部件。它只适用于指向小部件的指针 - 例如Widget *activeWidget() const可能是一个可能的函数签名。因此,我们可能执行以下操作

Widget *w = window->activeWidget();
w->paint();

我们希望实际调用适合“真实”小部件类型的 paint 成员函数 - 而不是Widget::paint()(这是一个“纯”,如果使用虚拟调度调用,会导致程序崩溃)。通过使用一个函数,我们确保子类的成员函数实现 -Button::paint()在本例中 - 将被调用。


Clipboard

要做
提及接口类


协变返回类型

[编辑 | 编辑源代码]

协变返回类型是派生类中的虚函数能够返回指向自身实例的指针或引用,如果基类中的方法版本也这样做。例如

class base
{
public:
  virtual base* create() const;
};

class derived : public base
{
public:
  virtual derived* create() const;
};

这允许避免强制转换。

注意
一些较旧的编译器不支持协变返回类型。对于此类编译器,存在变通方法。

虚构造函数

[编辑 | 编辑源代码]

存在一个类层次结构,基类为Foo。给定层次结构中的一个对象bar,希望能够执行以下操作

  1. 创建一个对象bazbar(例如,类Bar)使用该类的默认构造函数初始化。通常使用的语法是
    Bar* baz = bar.create();
  2. 创建一个对象bazbar这是bar的副本。通常使用的语法是
    Bar* baz = bar.clone();

在类Foo中,方法Foo::create()Foo::clone()声明如下

class Foo
{
    // ...

    public:
        // Virtual default constructor
        virtual Foo* create() const;

        // Virtual copy constructor
        virtual Foo* clone() const;
};

如果Foo要用作抽象类,则这些函数可以被设置为纯虚函数

class Foo
{
   // ...

    public:
        virtual Foo* create() const = 0;
        virtual Foo* clone() const = 0;
};

为了支持创建默认初始化的对象,以及创建复制对象,每个类Bar在层次结构中必须具有公共默认构造函数和复制构造函数。Bar的虚构造函数定义如下

class Bar : ... // Bar is a descendant of Foo
{
    // ...

    public:
    // Non-virtual default constructor
    Bar ();
    // Non-virtual copy constructor
    Bar (const Bar&);

    // Virtual default constructor, inline implementation
    Bar* create() const { return new Foo (); }
    // Virtual copy constructor, inline implementation
    Bar* clone() const { return new Foo (*this); }
};

上面的代码使用了协变返回类型。如果你的编译器不支持Bar* Bar::create(),请使用Foo* Bar::create()代替,同样适用于clone().

在使用这些虚构造函数时,你必须手动通过调用delete baz;来释放创建的对象。如果使用智能指针(例如std::unique_ptr<Foo>) 用于返回类型而不是普通的Foo*.

请记住,无论是否Foo使用动态分配的内存,您必须定义析构函数virtual ~Foo ()并将其设为负责使用指向祖先类型的指针来释放对象。

虚拟析构函数

[edit | edit source]

务必记住,即使在任何基类中定义空虚拟析构函数也很重要,因为如果不这样做会导致编译器生成的默认析构函数出现问题,它将不会是虚拟的。

在派生类中重新定义时,不会覆盖虚拟析构函数,每个析构函数的定义是累积的,它们从最后一个派生类开始,一直到第一个基类。

纯虚拟析构函数

[edit | edit source]

每个抽象类都应该包含纯虚拟析构函数的声明。

纯虚拟析构函数是纯虚拟函数的一种特殊情况(意在在派生类中被覆盖)。它们必须始终被定义,并且该定义应该始终为空。

class Interface {
public:
  virtual ~Interface() = 0; //declaration of a pure virtual destructor 
};

Interface::~Interface(){} //pure virtual destructor definition (should always be empty)
华夏公益教科书