在上一课有关指针和对派生对象基类的引用的过程中,我们看了一些示例,其中使用指针或对基类的引用有可能简化代码。但是,在每种情况下,我们都遇到了基本指针或引用只能调用函数的基本版本而不是派生版本的问题。 以下是此行为的简单示例:
class Base { public: const char* getName() { return "Base"; } }; class Derived: public Base { public: const char* getName() { return "Derived"; } }; int main() { Derived derived; Base &rBase = derived; std::cout << "rBase is a " << rBase.getName() << '\n'; }此示例打印结果:
rBase is a Base
因为rBase是Base引用,所以它调用Base :: getName(),它实际上引用了Derived对象的Base部分。
在本课中,我们将展示如何使用虚函数解决此问题。
虚函数和多态
虚拟函数是一种特殊类型的函数,调用它时,解析到该基类和派生类之间存在的函数的最底部的派生类。此函数称为多态。如果派生函数具有相同的label(名称,参数类型以及是否为const)并且返回类型作为函数的基本版本,则将其视为匹配项。这些函数称为覆盖。
要使函数成为虚函数,只需在函数声明之前放置**“virtual”**关键字即可。
以下是带有虚函数的上述示例:
class Base { public: virtual const char* getName() { return "Base"; } // 注意添加虚拟关键字 }; class Derived: public Base { public: virtual const char* getName() { return "Derived"; } }; int main() { Derived derived; Base &rBase = derived; std::cout << "rBase is a " << rBase.getName() << '\n'; return 0; }此示例打印结果:
rBase is a Derived
因为rBase是对Derived对象的Base部分的引用,所以当计算rBase.getName()时,它通常会解析为Base :: getName()。但是,Base :: getName()是虚拟的,它告诉程序去看看Base和Derived之间是否有更多派生的函数版本。在这种情况下,它将解析为Derived :: getName()!
我们来看一个稍微复杂的例子:
class A { public: virtual const char* getName() { return "A"; } }; class B: public A { public: virtual const char* getName() { return "B"; } }; class C: public B { public: virtual const char* getName() { return "C"; } }; class D: public C { public: virtual const char* getName() { 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 <string> #include <iostream> class Animal { protected: std::string m_name; // 我们正在使这个构造函数受到保护 ,因为 // 我们不希望人们直接创建Animal对象, // 但是我们仍然希望派生类能够使用它。 Animal(std::string name) : m_name(name) { } public: std::string getName() { return m_name; } const char* speak() { return "???"; } }; class Cat: public Animal { public: Cat(std::string name) : Animal(name) { } const char* speak() { return "Meow"; } }; class Dog: public Animal { public: Dog(std::string name) : Animal(name) { } const char* speak() { return "Woof"; } }; void report(Animal &animal) { std::cout << animal.getName() << " says " << animal.speak() << '\n'; } int main() { Cat cat("Fred"); Dog dog("Garbo"); report(cat); report(dog); }这打印:
Fred says ??? Garbo says ???
这是使用speak()函数虚拟的等效类:
#include <string> class Animal { protected: std::string m_name; // 我们正在使这个构造函数受到保护 ,因为 // 我们不希望人们直接创建Animal对象, // 但是我们仍然希望派生类能够使用它。 Animal(std::string name) : m_name(name) { } public: std::string getName() { return m_name; } virtual const char* speak() { return "???"; } }; class Cat: public Animal { public: Cat(std::string name) : Animal(name) { } virtual const char* speak() { return "Meow"; } }; class Dog: public Animal { public: Dog(std::string name) : Animal(name) { } virtual const char* speak() { return "Woof"; } }; void report(Animal &animal) { std::cout << animal.getName() << " says " << animal.speak() << '\n'; } int main() { Cat cat("Fred"); Dog dog("Garbo"); report(cat); report(dog); }该程序产生结果:
Fred says Meow Garbo says Woof
有用!
当评估animal.speak()时,程序注意到Animal :: speak()是一个虚函数。在动物引用Cat对象的Animal部分的情况下,程序会查看Animal和Cat之间的所有类,以查看它是否可以找到更多派生函数。在这种情况下,它会找到Cat :: speak()。在动物引用Dog对象的Animal部分的情况下,程序解析对Dog :: speak()的函数调用。
请注意,我们没有使Animal :: GetName()成为虚拟。这是因为GetName()永远不会在任何派生类中被覆盖,因此没有必要。
同样,以下数组示例现在按预期工作:
Cat fred("Fred"), misty("Misty"), zeke("Zeke"); Dog garbo("Garbo"), pooky("Pooky"), truffle("Truffle"); // 设置一个指向动物的指针数组,并将这些指针设置为我们的Cat和Dog对象 Animal *animals[] = { &fred, &garbo, &misty, &pooky, &truffle, &zeke }; for (int iii=0; iii < 6; ++iii) std::cout << animals[iii]->getName() << " says " << animals[iii]->speak() << '\n';产生结果:
Fred says Meow Garbo says Woof Misty says Meow Pooky says Woof Truffle says Woof Zeke says Meow
即使这两个例子只使用Cat和Dog,我们从Animal派生的任何其他类也可以使用我们的report()函数和animal数组而无需进一步修改!这可能是虚函数的最大好处 - 能够以这样的方式构造代码:新派生的类将自动使用旧代码而无需修改!
**警告:**派生类函数的label必须与基类虚函数的签名完全匹配,才能使用派生类函数。如果派生类函数具有不同的参数类型,程序可能仍然可以正常编译,但虚函数将无法按预期解析。
使用virtual关键字
如果函数被标记为虚拟,则所有匹配的覆盖也被视为虚拟,即使它们未明确标记为虚拟。但是,在派生函数上使用关键字virtual不会受到影响,并且它可以作为一个有用的提示,即函数是虚函数而不是普通函数。因此,对于派生类中的虚拟化函数使用virtual关键字通常是个好主意,即使它并非绝对必要。
返回虚函数的类型
class Base { public: virtual int getValue() { return 5; } }; class Derived: public Base { public: virtual double getValue() { return 6.78; } };在这种情况下,Derived :: getValue()不被视为Base :: getValue()的匹配覆盖(它被认为是一个完全独立的函数)。
不要从构造函数或析构函数中调用虚函数
这是另一个经常搞毫无戒心的新程序员的问题。您不应该从构造函数或析构函数中调用虚函数。为什么?
请记住,在创建Derived类时,首先构造Base部分。如果您要从Base构造函数调用虚函数,并且尚未创建该类的Derived部分,则它将无法调用该函数的Derived版本,因为Derived函数没有Derived对象可以工作。在C ++中,它将调用Base版本。
析构函数存在类似的问题。如果在Base类析构函数中调用虚函数,它将始终解析为函数的Base类版本,因为类的Derived部分已经被销毁。
规则:永远不要从构造函数或析构函数中调用虚函数
虚函数的缺点
由于大多数时候您都希望您的功能是虚拟的,为什么不将所有功能都虚拟化?答案是因为它效率低下 - 解析虚函数调用所花费的时间比解析常规函数要长。此外,编译器还必须为具有一个或多个虚函数的每个类对象分配一个额外的指针。我们将在本章的后续课程中更多地讨论这个问题。
Quiz Time:
1)以下程序打印什么?本练习旨在通过检查完成,而不是通过编译器编译示例。
1A)
class A { public: virtual const char* getName() { return "A"; } }; class B: public A { public: virtual const char* getName() { return "B"; } }; class C: public B { public: // 注意:这里没有getName()函数 }; class D: public C { public: virtual const char* getName() { return "D"; } }; int main() { C c; A &rBase = c; std::cout << rBase.getName() << '\n'; return 0; }解决方案
B. rBase是指向C对象的A引用。通常rBase.getName()会调用A :: getName(),但是A :: getName()是虚拟的,所以它调用A和C之间派生最多的匹配函数。那就是B :: getName(),它打印B 。 图1b)
class A { public: virtual const char* getName() { return "A"; } }; class B: public A { public: virtual const char* getName() { return "B"; } }; class C: public B { public: virtual const char* getName() { return "C"; } }; class D: public C { public: virtual const char* getName() { return "D"; } }; int main() { C c; B &rBase = c; // 注意:这次rBase是B std::cout << rBase.getName() << '\n'; return 0; }解决方案
C.这非常简单,因为C :: getName()是B类和C类之间派生最多的匹配调用。
1C)
class A { public: const char* getName() { return "A"; } // 注意:不是虚拟的 }; class B: public A { public: virtual const char* getName() { return "B"; } }; class C: public B { public: virtual const char* getName() { return "C"; } }; class D: public C { public: virtual const char* getName() { return "D"; } }; int main() { C c; A &rBase = c; std::cout << rBase.getName() << '\n'; return 0; }解决方案
答:由于A不是虚拟的,当调用rBase.getName()时,会调用A :: getName()。
1D)
class A { public: virtual const char* getName() { return "A"; } }; class B: public A { public: const char* getName() { return "B"; } // 注意:不是虚拟的 }; class C: public B { public: const char* getName() { return "C"; } //注意:不是虚拟的 }; class D: public C { public: const char* getName() { return "D"; } // 注意:不是虚拟的 }; int main() { C c; B &rBase = c; // 注意:这次rBase是B std::cout << rBase.getName() << '\n'; return 0; }解决方案
C.即使B和C未标记为虚函数,A :: getName()也是虚拟的,B :: getName()和C :: getName()都是覆盖。因此,B :: getName()和C :: getName()被认为是隐式虚拟的,因此对rBase.getName()的调用解析为C :: getName(),而不是B :: getName()。
1E)
class A { public: virtual const char* getName() const { return "A"; } // 注意:函数是const }; class B: public A { public: virtual const char* getName() { return "B"; } }; class C: public B { public: virtual const char* getName() { return "C"; } }; class D: public C { public: virtual const char* getName() { return "D"; } }; int main() { C c; A &rBase = c; std::cout << rBase.getName() << '\n'; return 0; }解决方案
答:这个有点棘手。rBase是对C对象的引用,因此rBase.getName()通常会调用A :: getName()。但是A :: getName()是虚拟的,所以它调用A和C之间函数的最派生版本。那就是A :: getName()。因为B :: getName()和c :: getName()不是const,所以它们不被视为覆盖!因此,该程序打印A. 1F)
#include <iostream> class A { public: A() { std::cout << getName(); } //注意添加构造函数 virtual const char* getName() { return "A"; } }; class B : public A { public: virtual const char* getName() { return "B"; } }; class C : public B { public: virtual const char* getName() { return "C"; } }; class D : public C { public: virtual const char* getName() { return "D"; } }; int main() { C c; return 0; }解决方案
A.另一个棘手的问题。当我们创建一个C对象时,首先构造一个A部分。当调用A构造函数来执行此操作时,它会调用虚函数getName()。因为类的B和C部分尚未设置,所以这解析为A :: getName()。