我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
当前回答
virtual关键字强制编译器选择对象类中定义的方法实现,而不是指针类中的方法实现。
Shape *shape = new Triangle();
cout << shape->getName();
在上面的示例中,默认情况下将调用Shape::getName,除非getName()在基类Shape中定义为virtual。这迫使编译器在Triangle类而不是Shape类中查找getName()实现。
虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也被称为动态调度,并且存在一些与之相关的开销。
最后,为什么在C++中甚至需要虚拟,为什么不将其作为Java中的默认行为?
C++基于“零开销”和“按需付费”的原则。因此,除非您需要,否则它不会尝试为您执行动态调度。为界面提供更多控制。通过使函数非虚拟化,接口/抽象类可以控制其所有实现中的行为。
其他回答
对虚拟函数的解释存在的问题是,它们没有解释如何在实践中使用它,以及它如何有助于维护。我创建了一个虚拟函数教程,人们已经发现它非常有用。此外,它基于战场前提,这让它更令人兴奋:https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html.
考虑这个战场应用:
#include "iostream"
//This class is created by Gun1's company
class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};
//This class is created by Gun2's company
class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};
//We create an abstract class to interface with WeaponController
class WeaponsInterface {
public:
virtual void shootTarget() = 0;
};
//A wrapper class to encapsulate Gun1's shooting function
class WeaponGun1 : public WeaponsInterface {
private:
Gun1* g;
public:
WeaponGun1(): g(new Gun1()) {}
~WeaponGun1() { delete g;}
virtual void shootTarget() { g->fire(); }
};
//A wrapper class to encapsulate Gun2's shooting function
class WeaponGun2 : public WeaponsInterface {
private:
Gun2* g;
public:
WeaponGun2(): g(new Gun2()) {}
~WeaponGun2() { delete g;}
virtual void shootTarget() { g->shoot(); }
};
class WeaponController {
private:
WeaponsInterface* w;
WeaponGun1* g1;
WeaponGun2* g2;
public:
WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;}
~WeaponController() {delete g1; delete g2;}
void shootTarget() { w->shootTarget();}
void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically
switch(gunNumber) {
case 1: w = g1; break;
case 2: w = g2; break;
}
}
};
class BattlefieldSoftware {
private:
WeaponController* wc;
public:
BattlefieldSoftware() : wc(new WeaponController()) {}
~BattlefieldSoftware() { delete wc; }
void shootTarget() { wc->shootTarget(); }
void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); }
};
int main() {
BattlefieldSoftware* bf = new BattlefieldSoftware();
bf->shootTarget();
for(int i = 2; i > 0; i--) {
bf->changeGunTo(i);
bf->shootTarget();
}
delete bf;
}
我鼓励您首先阅读博客上的文章,了解包装器类创建的原因。
如图所示,有各种火炮/导弹可以连接到战场软件,并且可以向这些武器发出命令,以进行射击或重新校准等。这里的挑战是能够在不必更改蓝色战场软件的情况下更改/更换火炮/导弹,并且能够在运行时切换武器,而无需更改代码并重新编译。
上面的代码显示了问题是如何解决的,以及具有精心设计的包装类的虚拟函数如何封装函数并帮助在运行时分配派生类指针。WeaponGun1类的创建确保了你将Gun1的处理完全分离到类中。无论你对Gun1做了什么改变,你只需要在WeaponGun1中做出改变,并有信心其他职业不会受到影响。
由于WeaponsInterface类,您现在可以将任何派生类分配给基类指针WeaponsIterface,并且因为它的函数是虚拟的,所以当您调用WeaponsIInterface的shootTarget时,派生类shootTarget将被调用。
最好的部分是,您可以在运行时更改枪(w=g1和w=g2)。这是虚拟函数的主要优势,这也是我们需要虚拟函数的原因。
因此,在更换枪支时,不再需要在不同的地方注释代码。现在,这是一个简单而干净的过程,添加更多的枪类也更容易,因为我们只需要创建一个新的WeaponGun3或WeaponGun 4类,我们可以确信它不会破坏BattlefieldSoftware的代码或WeaponGun1/WeaponGun2的代码。
我认为您所指的是这样一个事实:一旦方法被声明为virtual,您就不需要在重写中使用“virtual”关键字。
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
如果在Base的foo声明中不使用“virtual”,那么Derived的foo将只是隐藏它。
以下是我如何理解虚拟函数的含义,以及为什么需要它们:
假设您有以下两个类:
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."
完成。
关于效率,虚拟函数的效率略低于早期绑定函数。
“这种虚拟调用机制的效率几乎可以与“正常函数调用”机制一样高(25%以内)。它的空间开销是一个具有虚拟函数的类的每个对象中的一个指针加上每个此类类的一个vtbl”[Bjarne Stroustrup的C++教程]
为什么我们需要虚拟功能?
虚拟函数避免了不必要的类型转换问题,我们中的一些人会争论,当我们可以使用派生类指针来调用派生类中特定的函数时,为什么需要虚拟函数!答案是,它否定了在大型系统开发中继承的全部思想,因为在大型系统中,非常需要使用单指针基类对象。
让我们比较以下两个简单的程序,以了解虚拟函数的重要性:
无虚拟功能的程序:
#include <iostream>
using namespace std;
class father
{
public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
输出:
Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years
具有虚拟功能的程序:
#include <iostream>
using namespace std;
class father
{
public:
virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
输出:
Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years
通过仔细分析这两个输出,可以理解虚拟函数的重要性。