Skip to content

18.2 — 虚函数和多态

By Alex on January 30th, 2008 | last modified by Alex on January 23rd, 2020 翻译 by dashjay Dec 20th 2020

在之前的关于指向派生类中基类部分的指针和引用的课程中,我们看了很多例子使用指针或者引用指向基类,有可能优化代码。然而,在每个例子中,我们遇到的问题都是基类指针或者引用只能调用基类版本的函数,而不是派生类版本的。

这有一个简单的例子:

#include <iostream>
#include <string_view>

class Base
{
public:
    std::string_view getName() const { return "Base"; }
};

class Derived: public Base
{
public:
    std::string_view getName() const { return "Derived"; }
};

int main()
{
    Derived derived;
    Base &rBase{ derived };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

例子输出:

rBase is a Base

因为 rBase 是一个 Base 的引用,它调用了 Base::getName(),即便它实际上引用的是派生类对象的基类部分。

在这节课中,我们将会展示如何使用虚函数去解决这种问题。

虚函数和多态

一个虚汗是是一种特殊的函数,当被调用时,解析到存在于基类和派生类中的最后的派生版本的函数。这个能力被叫做多态。我们认为一个派生函数匹配同样的签名(名字,参数类型,是否为 const 函数,和他的返回值类型都要和基类版本一样),这样的函数被叫做重写。

为了编写一个虚函数,简单的在函数申明前放置一个 virtual 的关键词即可。

这是一个虚函数的例子:

#include <iostream>
#include <string_view>

class Base
{
public:
    virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
};

class Derived: public Base
{
public:
    virtual std::string_view getName() const { return "Derived"; }
};

int main()
{
    Derived derived;
    Base &rBase{ derived };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

这个例子输出:

rBase is a Derived

因为 rBase 是一个派生类对象的基类部分的引用,当 rBase.getName() 被调用时,他将会解析到 Base::getName()。然而,Base::getName() 是一个虚函数,将会告诉程序去检查是否有更外层的派生版本的函数(在基类和派生类之间)。在这个例子中,会解析到 Derived::getName()

让我们看一个稍稍复杂的例子:

#include <iostream>
#include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    virtual std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
    virtual std::string_view getName() const { return "C"; }
};

class D: public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    A &rBase{ c };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

你认为他会输出什么?

让我们来看看它是如何工作的。首先我们实例化了一个C类对象,rBase是一个 A 类的引用,我们将引用指向 C 对象的中的 A 基类部分。最后,我们调用 rBase.getName()rBase.getName() 取值到 A::getName()。然而 A::getName() 是一个虚函数,因此编译器将调用最外层的派生版本(介于 A 和 C 之间)。在这个例子中,就是会调用 C::getName()。注意,它不会去调用 D::getName() 因为我们原始对象是一个 C 类对象,而不是 D,因此只有 A 和 C 之间的函数会被考虑。

结果就是,我们的程序输出:

rBase is a C

一个更加复杂的例子

让我们来看一个之前课上提到的 Animal 的例子。这里是之前的代码:

# include <iostream>
# include <string>
# include <string_view>

class Animal
{
protected:
    std::string m_name;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(const std::string &name)
        : m_name{ name }
    {
    }

public:
    const std::string &getName() const { return m_name; }
    std::string_view speak() const { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(const std::string& name)
        : Animal{ name }
    {
    }

    std::string_view speak() const { return "Meow"; }
};

class Dog: public Animal
{
public:
    Dog(const std::string &name)
        : Animal{ name }
    {
    }

    std::string_view speak() const { return "Woof"; }
};

void report(const Animal &animal)
{
    std::cout << animal.getName() << " says " << animal.speak() << '\n';
}

int main()
{
    Cat cat{ "Fred" };
    Dog dog{ "Garbo" };

    report(cat);
    report(dog);

    return 0;
}

程序输出:

Fred says ???
Garbo says ???

下面是等效的代码,其中不同的将 speak() 声明为虚函数:

# include <iostream>
# include <string>
# include <string_view>

class Animal
{
protected:
    std::string m_name;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(const std::string &name)
        : m_name{ name }
    {
    }

public:
    const std::string &getName() const { return m_name; }
    virtual std::string_view speak() const { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(const std::string &name)
        : Animal{ name }
    {
    }

    virtual std::string_view speak() const { return "Meow"; }
};

class Dog: public Animal
{
public:
    Dog(const std::string& name)
        : Animal{ name }
    {
    }

    virtual std::string_view speak() const { return "Woof"; }
};

void report(const Animal &animal)
{
    std::cout << animal.getName() << " says " << animal.speak() << '\n';
}

int main()
{
    Cat cat{ "Fred" };
    Dog dog{ "Garbo" };

    report(cat);
    report(dog);

    return 0;
}

程序打印结果:

Fred says Meow
Garbo says Woof

成功了!

animal.speak() 被取值(evaluated)的时候,程序 Animal:speak() 是一个虚函数。在这个例子中,animal 引用了 Cat 类对象的 Animal 部分,程序就会寻找所有在 AnimalCat 之间的类,来检查有没有靠后(more-derived)的派生函数版本。在这个例子中,它找到了 Cat::speak()。在这个例子中, animal 又引用了 Dog 类对象中的 Animal 部分,这个程序解析到了 Dog::speak() 并调用。

注意到我们没有让 Animal::getName() 成为一个虚函数。这是因为 getName() 不会在任何派生类中被重写,没有那样做的必要

相似的是,下面的数组例子也按照期望工作了:

Cat fred{ "Fred" };
Cat misty{ "Misty" };
Cat zeke{ "Zeke" };

Dog garbo{ "Garbo" };
Dog pooky{ "Pooky" };
Dog truffle{ "Truffle" };

// Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
Animal *animals[]{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };

for (const auto *animal : animals)
    std::cout << animal->getName() << " says " << animal->speak() << '\n';

产生结果:

Fred says Meow
Garbo says Woof
Misty says Meow
Pooky says Woof
Truffle says Woof
Zeke says Meow

即便这两个例子仅仅使用了 CatDog,但是其他的派生自 Animal 的类也会在 report() 函数起作用,甚至是 animal 数组也可以!这也许就是虚函数最大的好处了吧 —— 有能力使新派生的类能够在不进行修改的情况下自动使用旧代码来构造代码!

提醒一句:为了使派生类的函数被调用,派生类的签名必须正好和基类的一样。如果派生类的函数有不同的参数类型,这个程序也会被调用,但是虚函数实际上不会按照期望被调用。

virtual 关键词的使用

如果一个函数被标记为虚函数,那么所有重写的函数也会被认为是虚函数,即便没有像那样显式的申明。然而,派生类有一个 virtual 的关键词也无伤大雅,并且也作为一个函数是虚函数而不是普通函数的提示。因此,对派生类中的虚函数使用 virtual 关键词也是一个不错的习惯,尽管这不是严格必须的。

一个虚函数的返回类型

在常规的情况下,重写虚函数的返回值必须和虚函数保持一致,看如下例子:

class Base
{
public:
    virtual int getValue() const { return 5; }
};

class Derived: public Base
{
public:
    virtual double getValue() const { return 6.78; }
};

在这个例子中, Derived::getValue() 不会被当做 Base::getValue() 的重写(它被认为是一个不同的函数)。

不要在构造函数和解构函数中调用虚函数

这有一个其他的问题经常为新手程序员代码不可预料的问题。你不应该从构造函数和解构函数中调用虚函数。为什么?

记住当一个派生类被构造时,基类部分先被构造,如果你在基类的构造函数调用虚函数,并且派生类部分至今并没有被创造出来,就不能调用到派生版本的函数,因为没有派生对象能够支持派生类函数的调用,在 C++ 中,它会先调用基类版本的函数。

在解构函数中有一个类似的问题。如果你调用一个虚函数在基类的结构函数中,他将总是解析到基类版本的函数中,因为派生类版本的函数早就被销毁了。

规定:绝对不从构造函数和解构函数中调用虚函数。

虚函数的缺点

大多数的时候,你希望你的函数成为一个虚函数,为什么不让所有函数都是虚函数呢?答案就是因为它不高效——解析一个虚函数的调用会消耗更长的时间,相比解析普通的函数。而且,编译器需要为有一个或者多个虚函数的类的对象分配额外的指针,我们将会在接下来的课程中讨论这个问题。

Quiz time

1) What do the following programs print? This exercise is meant to be done by inspection, not by compiling the examples with your compiler.

1a)

# include <iostream>
# include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    virtual std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
// Note: no getName() function here
};

class D: public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    A &rBase{ c };
    std::cout << rBase.getName() << '\n';

    return 0;
}

B. rBase is an A reference pointing to a C object. Normally rBase.getName() would call A::getName(), but A::getName() is virtual so it instead calls the most derived matching function between A and C. That is B::getName(), which prints B.

1b)

# include <iostream>
# include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    virtual std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
    virtual std::string_view getName() const { return "C"; }
};

class D: public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    B &rBase{ c }; // note: rBase is a B this time
    std::cout << rBase.getName() << '\n';

    return 0;
}

C. This is pretty straightforward, as C::getName() is the most derived matching call between classes B and C.

1c)

# include <iostream>
# include <string_view>

class A
{
public:
    // note: no virtual keyword
    std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    virtual std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
    virtual std::string_view getName() const { return "C"; }
};

class D: public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    A &rBase{ c };
    std::cout << rBase.getName() << '\n';

    return 0;
}

A. Since A is not virtual, when rBase.getName() is called, A::getName() is called.

1d)

# include <iostream>
# include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    // note: no virtual keyword in B, C, and D
    std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
    std::string_view getName() const { return "C"; }
};

class D: public C
{
public:
    std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    B &rBase{ c }; // note: rBase is a B this time
    std::cout << rBase.getName() << '\n';

    return 0;
}

C. Even though B and C aren’t marked as virtual functions, A::getName() is virtual and B::getName() and C::getName() are overrides. Therefore, B::getName() and C::getName() are considered implicitly virtual, and thus the call to rBase.getName() resolves to C::getName(), not B::getName().

1e)

# include <iostream>
# include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    // Note: Functions in B, C, and D are non-const.
    virtual std::string_view getName() { return "B"; }
};

class C: public B
{
public:
    virtual std::string_view getName() { return "C"; }
};

class D: public C
{
public:
    virtual std::string_view getName() { return "D"; }
};

int main()
{
    C c;
    A &rBase{ c };
    std::cout << rBase.getName() << '\n';

    return 0;
}

A. This one is a little trickier. rBase is an A reference to a C object, so rBase.getName() would normally call A::getName(). But A::getName() is virtual, so it calls the most derived version of the function between A and C. And that is A::getName(). Because B::getName() and c::getName() are not const, they are not considered overrides! Consequently, this program prints A.

1f)

# include <iostream>
# include <string_view>

class A
{
public:
 A() { std::cout << getName(); } // note addition of constructor

 virtual std::string_view getName() const { return "A"; }
};

class B : public A
{
public:
 virtual std::string_view getName() const { return "B"; }
};

class C : public B
{
public:
 virtual std::string_view getName() const { return "C"; }
};

class D : public C
{
public:
 virtual std::string_view getName() const { return "D"; }
};

int main()
{
 C c;

 return 0;
}

A. Another tricky one. When we create a C object, the A part is constructed first. When the A constructor is called to do this, it calls virtual function getName(). Because the B and C parts of the class aren’t set up yet, this resolves to A::getName().