我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
当前回答
接口设计采用虚拟方法。例如,在Windows中有一个名为IUnknown的界面,如下所示:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
这些方法留给界面用户来实现。它们对于创建和销毁必须继承IUnknown的某些对象至关重要。在这种情况下,运行时知道这三个方法,并期望在调用它们时实现它们。所以在某种意义上,它们充当了对象本身和使用该对象的任何事物之间的契约。
其他回答
以下是我如何理解虚拟函数的含义,以及为什么需要它们:
假设您有以下两个类:
class Animal
{
public:
void eat() { std::cout << "I'm eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."; }
};
在主功能中:
Animal *animal = new Animal;
Cat *cat = new Cat;
animal->eat(); // Outputs: "I'm eating generic food."
cat->eat(); // Outputs: "I'm eating a rat."
到目前为止还不错,对吧?动物吃普通食物,猫吃老鼠,都没有虚拟食物。
现在让我们稍微改变一下,以便通过一个中间函数调用eat()(本例中的一个普通函数):
// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }
现在我们的主要功能是:
Animal *animal = new Animal;
Cat *cat = new Cat;
func(animal); // Outputs: "I'm eating generic food."
func(cat); // Outputs: "I'm eating generic food."
哦哦。。。我们把一只猫传给func(),但它不会吃老鼠。是否应该重载func()以使其使用Cat*?如果你必须从Animal派生出更多的动物,它们都需要自己的func()。
解决方案是使Animal类中的eat()成为一个虚拟函数:
class Animal
{
public:
virtual void eat() { std::cout << "I'm eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."; }
};
主要内容:
func(animal); // Outputs: "I'm eating generic food."
func(cat); // Outputs: "I'm eating a rat."
完成。
当基类中有函数时,可以在派生类中重新定义或重写它。
重新定义方法:派生类中给出了基类方法的新实现。不便于动态绑定。
重写方法:在派生类中重新定义基类的虚拟方法。虚拟方法有助于动态绑定。
所以当你说:
但在书的早些时候,当我了解基本遗传时能够重写派生类中的基方法,而不使用“虚拟”。
因为基类中的方法不是虚拟的,所以您不是在重写它,而是在重新定义它
OOP答案:亚型多态性
在C++中,需要虚拟方法来实现多态性,如果应用维基百科中的定义,则更准确地说是子类型或子类型多态性。
维基百科,分类,2019-01-09:在编程语言理论中,子类型化(也称为子类型多态性或包含多态性)是类型多态性的一种形式,其中子类型是通过某种可替代性概念与另一个数据类型(父类型)相关的数据类型,这意味着程序元素(通常是子例程或函数),编写为对父类型的元素进行操作也可以对子类型的元素执行操作。
注意:子类型表示基类,子类型表示继承类。
关于亚型多态性的进一步阅读
https://en.wikipedia.org/wiki/Subtypinghttps://en.wikipedia.org/wiki/Polymorphism_(computer_science)#子类型
技术答案:动态调度
如果您有一个指向基类的指针,那么方法的调用(声明为虚拟)将被分派到所创建对象的实际类的方法。这就是亚型多态性是如何在C++中实现的。
进一步阅读C++中的多态性与动态调度
http://www.cplusplus.com/doc/tutorial/polymorphism/https://en.cppreference.com/w/cpp/language/virtual
实现答案:创建vtable条目
对于方法上的每个修饰符“virtual”,C++编译器通常会在声明方法的类的vtable中创建一个条目。这就是常见的C++编译器实现动态调度的方式。
进一步阅读vtables
https://en.wikipedia.org/wiki/Virtual_method_table
示例代码
#include <iostream>
using namespace std;
class Animal {
public:
virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
virtual ~Animal(){};
};
class Cat : public Animal {
public:
virtual void MakeTypicalNoise()
{
cout << "Meow!" << endl;
}
};
class Dog : public Animal {
public:
virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
cout << "Woof!" << endl;
}
};
class Doberman : public Dog {
public:
virtual void MakeTypicalNoise() {
cout << "Woo, woo, woow!";
cout << " ... ";
Dog::MakeTypicalNoise();
}
};
int main() {
Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };
const int cnAnimals = sizeof(apObject)/sizeof(Animal*);
for ( int i = 0; i < cnAnimals; i++ ) {
apObject[i]->MakeTypicalNoise();
}
for ( int i = 0; i < cnAnimals; i++ ) {
delete apObject[i];
}
return 0;
}
示例代码输出
Meow!
Woof!
Woo, woo, woow! ... Woof!
代码示例的UML类图
跟进@user6359267的回答,C++范围层次结构是
global -> namespace -> class -> local -> statement
因此,每个类都定义了一个范围。如果不是这样的话,子类中的重写函数实际上会在同一范围内重新定义函数,而链接器不允许这样做:
在每个翻译单元中使用之前必须声明函数,并且一个函数只能在整个程序(跨所有翻译单元)的给定范围内定义一次
由于每个类都定义了自己的作用域,因此被调用的函数是在调用该函数的对象的类中定义的函数。所以
#include <iostream>
#include <string>
class Parent
{
public:
std::string GetName() { return "Parent"; }
};
class Child : public Parent
{
public:
std:::string GetName() { return "Child"; }
};
int main()
{
Parent* parent = new Parent();
std::cout << parent->GetName() << std::endl;
Child* child = new Child();
std::cout << child->GetName() << std::endl;
*parent = child;
std::cout << child->GetName() << std::endl;
return 0;
}
输出
Parent
Child
Parent
因此,我们需要一种方法来告诉编译器应该在运行时而不是编译时确定要调用的函数。这就是虚拟关键字的作用。
这就是为什么函数重载被称为编译时多态(或早期绑定),而虚拟函数重写被称为运行时多态(或者后期绑定)。
细节:
在内部,当编译器看到一个虚拟函数时,它会创建一个类成员指针,该指针使用.*和->*运算符一般指向该类的成员(而不是对象中该成员的特定实例)。他们的工作是允许您访问一个类的成员,该成员具有指向该成员的指针。这些很少被程序员直接使用(也许除非你正在编写一个编译器来实现“虚拟”)。
我认为您所指的是这样一个事实:一旦方法被声明为virtual,您就不需要在重写中使用“virtual”关键字。
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
如果在Base的foo声明中不使用“virtual”,那么Derived的foo将只是隐藏它。