我正在学习 C++,我刚刚进入虚拟函数。
从我读到的(在书中和在线)中,虚函数是基类中的函数,您可以在派生类中覆盖它们。
但是在本书的前面,在学习基本继承时,我能够在派生类中重写基函数,而无需使用virtual
.
那么我在这里错过了什么?我知道虚函数还有更多,而且它似乎很重要,所以我想弄清楚它到底是什么。我只是无法在网上找到一个简单的答案。
我正在学习 C++,我刚刚进入虚拟函数。
从我读到的(在书中和在线)中,虚函数是基类中的函数,您可以在派生类中覆盖它们。
但是在本书的前面,在学习基本继承时,我能够在派生类中重写基函数,而无需使用virtual
.
那么我在这里错过了什么?我知道虚函数还有更多,而且它似乎很重要,所以我想弄清楚它到底是什么。我只是无法在网上找到一个简单的答案。
以下是我不仅理解什么是virtual
功能,而且理解为什么需要它们的方式:
假设你有这两个类:
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."
到目前为止一切顺利,对吧?动物吃普通食物,猫吃老鼠,都没有virtual
。
现在让我们稍微改变一下,以便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()
。
解决方案是eat()
从Animal
类中创建一个虚函数:
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."
完毕。
没有“虚拟”,您将获得“早期绑定”。使用该方法的哪个实现在编译时根据您调用的指针的类型来决定。
使用“虚拟”,您将获得“后期绑定”。使用该方法的哪个实现在运行时根据所指向对象的类型来决定——它最初被构造为什么。根据指向该对象的指针的类型,这不一定是您所想的。
class Base
{
public:
void Method1 () { std::cout << "Base::Method1" << std::endl; }
virtual void Method2 () { std::cout << "Base::Method2" << std::endl; }
};
class Derived : public Base
{
public:
void Method1 () { std::cout << "Derived::Method1" << std::endl; }
void Method2 () { std::cout << "Derived::Method2" << std::endl; }
};
Base* basePtr = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*
basePtr->Method1 (); // Prints "Base::Method1"
basePtr->Method2 (); // Prints "Derived::Method2"
编辑- 看到这个问题。
此外 -本教程涵盖 C++ 中的早期和后期绑定。
您需要至少 1 级继承和向上转换来证明它。这是一个非常简单的例子:
class Animal
{
public:
// turn the following virtual modifier on/off to see what happens
//virtual
std::string Says() { return "?"; }
};
class Dog: public Animal
{
public: std::string Says() { return "Woof"; }
};
void test()
{
Dog* d = new Dog();
Animal* a = d; // refer to Dog instance with Animal pointer
std::cout << d->Says(); // always Woof
std::cout << a->Says(); // Woof or ?, depends on virtual
}
虚函数用于支持运行时多态性。
也就是说,virtual关键字告诉编译器不要在编译时做出(函数绑定的)决定,而是将它推迟到运行时”。
virtual
您可以通过在其基类声明中的关键字前面来使函数虚拟。例如,
class Base
{
virtual void func();
}
当基类具有虚成员函数时,任何从基类继承的类都可以用完全相同的原型重新定义函数,即只能重新定义功能,不能重新定义函数的接口。
class Derive : public Base
{
void func();
}
基类指针可用于指向基类对象以及派生类对象。
当使用基类指针调用虚函数时,编译器在运行时决定调用哪个版本的函数——即基类版本或重写的派生类版本。这称为运行时多态性。
您需要虚拟方法来实现安全向下转换、简单和简洁。
这就是虚拟方法的作用:它们安全地向下转换,代码显然简单而简洁,避免了不安全的手动转换,否则您将拥有更复杂和冗长的代码。
以下代码故意“不正确”。它没有将value
方法声明为virtual
,因此会产生意想不到的“错误”结果,即 0:
#include <iostream>
using namespace std;
class Expression
{
public:
auto value() const
-> double
{ return 0.0; } // This should never be invoked, really.
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const
-> double
{ return number_; } // This is OK.
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const
-> double
{ return a_->value() + b_->value(); } // Uhm, bad! Very bad!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
在注释为“坏”的行Expression::value
中,调用了该方法,因为静态已知类型(编译时已知的类型)是Expression
,并且该value
方法不是虚拟的。
value
以virtual
静态已知类型声明Expression
可确保每次调用都会检查这是什么实际对象类型,并value
为该动态类型调用相关实现:
#include <iostream>
using namespace std;
class Expression
{
public:
virtual
auto value() const -> double
= 0;
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const -> double
override
{ return number_; }
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const -> double
override
{ return a_->value() + b_->value(); } // Dynamic binding, OK!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
这里的输出6.86
应该是这样的,因为 virtual 方法被调用了 virtual 。这也称为调用的动态绑定。执行一点检查,找到对象的实际动态类型,并调用该动态类型的相关方法实现。
相关实现是最具体(最派生)类中的实现。
请注意,此处派生类中的方法实现没有被标记virtual
,而是被标记override
。它们可以被标记virtual
,但它们是自动虚拟的。该override
关键字确保如果某些基类中没有这样的虚拟方法,那么您将收到错误(这是可取的)。
没有virtual
人将不得不实现一些自己动手做的动态绑定版本。这通常涉及不安全的手动向下转换、复杂性和冗长。
对于单个函数的情况,就像这里一样,将函数指针存储在对象中并通过该函数指针调用就足够了,但即便如此,它也会涉及一些不安全的向下转换、复杂性和冗长,即:
#include <iostream>
using namespace std;
class Expression
{
protected:
typedef auto Value_func( Expression const* ) -> double;
Value_func* value_func_;
public:
auto value() const
-> double
{ return value_func_( this ); }
Expression(): value_func_( nullptr ) {} // Like a pure virtual.
};
class Number
: public Expression
{
private:
double number_;
static
auto specific_value_func( Expression const* expr )
-> double
{ return static_cast<Number const*>( expr )->number_; }
public:
Number( double const number )
: Expression()
, number_( number )
{ value_func_ = &Number::specific_value_func; }
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
static
auto specific_value_func( Expression const* expr )
-> double
{
auto const p_self = static_cast<Sum const*>( expr );
return p_self->a_->value() + p_self->b_->value();
}
public:
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{ value_func_ = &Sum::specific_value_func; }
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
看待这一点的一种积极方式是,如果您遇到上述不安全的向下转换、复杂性和冗长,那么通常一个或多个虚拟方法可以真正提供帮助。
如果基类是Base
,而派生类是Der
,则您可以有一个Base *p
指针,它实际上指向Der
. 当您调用 时p->foo();
,如果foo
不是虚拟的,则执行它的版本,忽略实际指向Base
的事实。如果 foo是虚拟的,则执行 的“最叶”覆盖,充分考虑指向项的实际类。所以虚拟和非虚拟之间的区别实际上是非常关键的:前者允许运行时多态,这是 OO 编程的核心概念,而后者不允许。p
Der
p->foo()
foo
我想添加虚拟函数的另一种用法,尽管它使用与上述答案相同的概念,但我想它值得一提。
虚拟破坏者
考虑下面的这个程序,没有将基类析构函数声明为虚拟;Cat 的内存可能无法清理。
class Animal {
public:
~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat() {
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
输出:
Deleting an Animal
class Animal {
public:
virtual ~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat(){
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
输出:
Deleting an Animal name Cat Deleting an Animal
需要虚拟功能解释【通俗易懂】
#include<iostream>
using namespace std;
class A{
public:
void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B; // Create a base class pointer and assign address of derived object.
a1->show();
}
输出将是:
Hello from Class A.
但使用虚函数:
#include<iostream>
using namespace std;
class A{
public:
virtual void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
virtual void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B;
a1->show();
}
输出将是:
Hello from Class B.
因此,使用虚函数可以实现运行时多态性。
您必须区分覆盖和重载。如果没有virtual
关键字,您只会重载基类的方法。这意味着隐藏。假设您有一个基类Base
和一个派生类Specialized
,它们都实现了void foo()
. 现在你有一个Base
指向Specialized
. 当你调用foo()
它时,你可以观察到不同之处virtual
:如果方法是虚拟的,Specialized
将使用的实现,如果它缺失,Base
将选择版本。最好的做法是永远不要重载基类中的方法。使方法成为非虚拟方法是其作者告诉您其在子类中的扩展不是有意的方式。
为了更好地阅读,我以对话的形式给出了答案:
为什么我们需要虚函数?
因为多态。
什么是多态性?
基指针也可以指向派生类型对象这一事实。
这种多态性的定义如何导致对虚函数的需求?
好吧,通过早期绑定。
什么是早期绑定?
C++ 中的早期绑定(编译时绑定)是指在程序执行之前固定函数调用。
所以...?
因此,如果您使用基类型作为函数的参数,编译器将仅识别基接口,并且如果您使用派生类中的任何参数调用该函数,它将被切掉,这不是您想要发生的。
如果这不是我们想要发生的,为什么允许这样做?
因为我们需要多态!
那么多态有什么好处呢?
您可以使用基类型指针作为单个函数的参数,然后在程序的运行时,您可以访问每个派生类型接口(例如它们的成员函数)而没有任何问题,使用该单个函数的解引用基指针。
我还是不知道虚函数有什么用……!这是我的第一个问题!
好吧,这是因为你问得太早了!
为什么我们需要虚函数?
假设您使用基指针调用函数,该指针具有来自其派生类之一的对象的地址。正如我们在上面讨论过的,在运行时,这个指针被取消引用,到目前为止一切都很好,但是,我们希望“来自我们的派生类”的方法(== 成员函数)被执行!但是,在基类中已经定义了相同的方法(具有相同标头的方法),那么您的程序为什么还要费心选择其他方法呢?换句话说,我的意思是,你怎么能把这种情况与我们过去经常看到的情况区分开来呢?
简短的回答是“基类中的一个虚拟成员函数”,而稍长一点的回答是,“在这一步,如果程序在基类中看到一个虚函数,它就知道(意识到)你正在尝试使用多态性”等到派生类(使用v-table,一种后期绑定的形式)来找到另一个具有相同标头的方法,但 - 预期 - 具有不同的实现。
为什么不同的实现?
你这个笨蛋!去读一本好书!
好吧,等等等等,当他/她可以简单地使用派生类型指针时,为什么还要费心使用基指针?你是法官,这一切的头痛值得吗?看看这两个片段:
//1:
Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();
//2:
Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();
好的,虽然我认为1仍然比2好,你也可以这样写1:
//1:
Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();
此外,您应该知道,这只是对我迄今为止向您解释的所有内容的人为使用。取而代之的是,假设您的程序中有一个函数分别使用每个派生类的方法(getMonthBenefit()):
double totalMonthBenefit = 0;
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
totalMonthBenefit += x -> getMonthBenefit();
}
现在,尝试重新编写这个,不要头疼!
double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();
实际上,这也可能是一个人为的例子!
为什么我们需要 C++ 中的虚拟方法?
在 Bjarne Stroustrup C++ 编程:原理与实践,(14.3):
虚函数提供了在基类中定义函数并在用户调用基类函数时调用的派生类中具有相同名称和类型的函数的能力。这通常称为运行时多态性、动态分派或运行时分派,因为调用的函数是在运行时根据所使用对象的类型确定的。
为了处理虚拟调用,需要一个或多个与派生对象 3相关的数据。通常的做法是添加函数表的地址。该表通常称为虚表或虚函数表,其地址通常称为虚指针。每个虚函数在虚表中都有一个槽。根据调用者的对象(派生)类型,虚函数依次调用相应的覆盖。
1.继承、运行时多态和封装的使用是面向对象编程最常见的定义。
2. 您无法将功能编码得更快或使用更少的内存使用其他语言功能在运行时选择备选方案。Bjarne Stroustrup C++ 编程:原理与实践。(14.3.1)。
3. 当我们调用包含虚函数的基类时,告诉我们真正调用了哪个函数。
当您在基类中有一个函数时,您可以Redefine
或Override
它在派生类中。
重新定义方法:派生类中给出了基类方法的新实现。不方便Dynamic binding
。_
覆盖方法:
派生类中基类的Redefining
方法virtual method
虚拟方法有助于动态绑定。
所以当你说:
但是在本书的前面,当学习基本继承时,我能够在不使用“虚拟”的情况下覆盖派生类中的基本方法。
您没有覆盖它,因为基类中的方法不是虚拟的,而是您正在重新定义它
如果您了解底层机制,它会有所帮助。C++ 形式化了 C 程序员使用的一些编码技术,用“覆盖”替换“类”——具有公共标题部分的结构将用于处理不同类型但具有一些公共数据或操作的对象。通常,覆盖层的基本结构(公共部分)有一个指向函数表的指针,该函数表指向每个对象类型的一组不同的例程。C++ 做同样的事情,但隐藏了机制,即 C++ptr->func(...)
中 func 是虚拟的,就像 C 一样(*ptr->func_table[func_num])(ptr,...)
,派生类之间的变化是 func_table 的内容。[非虚拟方法 ptr->func() 只是转换为 mangled_func(ptr,..)。]
这样做的结果是,您只需要了解基类即可调用派生类的方法,即如果例程了解类 A,您可以将派生类 B 指针传递给它,然后调用的虚拟方法将是那些的 B 而不是 A,因为您通过函数表 B 指向。
关键字 virtual 告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。为此,典型的编译器 1 为每个包含虚函数的类创建一个表(称为 VTABLE)。编译器将特定类的虚函数地址放在 VTABLE 中。在每个带有虚函数的类中,它都会偷偷地放置一个指针,称为 vpointer(缩写为 VPTR),它指向该对象的 VTABLE。当您通过基类指针进行虚函数调用时,编译器会悄悄地插入代码以获取 VPTR 并在 VTABLE 中查找函数地址,从而调用正确的函数并导致后期绑定发生。
此链接中的更多详细信息 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
virtual关键字强制编译器选择定义在对象类而不是指针类中的方法实现。
Shape *shape = new Triangle();
cout << shape->getName();
在上面的例子中,Shape::getName 将默认被调用,除非 getName() 在基类 Shape 中被定义为 virtual。这迫使编译器在 Triangle 类而不是 Shape 类中查找 getName() 实现。
虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也称为动态调度,并且有一些与之相关的开销。
最后,为什么在 C++ 中甚至需要 virtual,为什么不像 Java 那样让它成为默认行为呢?
在 C++ 中,如果您应用维基百科的定义,则需要虚拟方法来实现多态性,更准确地说是子类型化或子类型多态性。
维基百科,子类型,2019-01-09:在编程语言理论中,子类型(也称为子类型多态性或包含多态性)是类型多态性的一种形式,其中子类型是通过某种概念与另一个数据类型(超类型)相关的数据类型可替代性,意味着程序元素,通常是子例程或函数,被编写为对超类型的元素进行操作,也可以对子类型的元素进行操作。
注意:子类型表示基类,子类型表示继承类。
如果您有一个指向基类的指针,那么该方法的调用(即声明为虚拟的)将被分派给所创建对象的实际类的方法。这就是C++ 实现子类型多态的方式。
对于方法上的每个修饰符“virtual”,C++ 编译器通常在声明该方法的类的 vtable 中创建一个条目。这就是普通 C++ 编译器如何实现Dynamic Dispatch的。
示例代码
#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类图
为什么我们需要虚函数?
虚函数避免了不必要的类型转换问题,我们中的一些人可能会争论,当我们可以使用派生类指针调用派生类中特定的函数时,为什么我们需要虚函数!答案是 - 它使大型系统中继承的整个想法无效开发,其中非常需要具有单指针基类对象。
让我们比较下面两个简单的程序来理解虚函数的重要性:
没有虚函数的程序:
#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
通过仔细分析这两种输出,我们可以理解虚函数的重要性。
对虚函数的解释的问题在于,它们没有解释它在实践中是如何使用的,以及它如何有助于可维护性。我创建了一个虚拟功能教程,人们已经发现它非常有用。另外,它是基于战场前提的,这使它更令人兴奋: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
类,您现在可以将任何派生类分配给基类指针WeaponsInterface
,并且因为它的函数是虚拟的,所以当您调用WeaponsInterface
'sshootTarget
时,派生类shootTarget
会被调用。
w=g1
最好的部分是,您可以在运行时(和)更换枪w=g2
。这是虚函数的主要优点,这也是我们需要虚函数的原因。
所以在换枪时不再需要在各个地方注释掉代码。它现在是一个简单而干净的过程,添加更多的枪类也更容易,因为我们只需要创建一个新的WeaponGun3
orWeaponGun4
类,我们可以确信它不会弄乱BattlefieldSoftware
's 代码或WeaponGun1
/ WeaponGun2
's 代码。
关于效率,虚函数的效率不如早期绑定函数。
“这种虚拟调用机制几乎可以与“普通函数调用”机制一样高效(在 25% 以内)。它的空间开销是具有虚拟函数的类的每个对象中的一个指针加上每个此类的一个 vtbl” [ A Bjarne Stroustrup的 C++ 之旅]
虚拟方法用于界面设计。例如,在 Windows 中有一个名为 IUnknown 的接口,如下所示:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
这些方法留给界面用户来实现。它们对于创建和销毁必须继承 IUnknown 的某些对象至关重要。在这种情况下,运行时知道这三个方法,并期望在调用它们时实现它们。因此,从某种意义上说,它们充当了对象本身与使用该对象的任何事物之间的契约。
这是一个完整的例子,说明了为什么使用虚拟方法。
#include <iostream>
using namespace std;
class Basic
{
public:
virtual void Test1()
{
cout << "Test1 from Basic." << endl;
}
virtual ~Basic(){};
};
class VariantA : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantA." << endl;
}
};
class VariantB : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantB." << endl;
}
};
int main()
{
Basic *object;
VariantA *vobjectA = new VariantA();
VariantB *vobjectB = new VariantB();
object=(Basic *) vobjectA;
object->Test1();
object=(Basic *) vobjectB;
object->Test1();
delete vobjectA;
delete vobjectB;
return 0;
}
我认为您指的是一旦方法被声明为虚拟的事实,您就不需要在覆盖中使用“虚拟”关键字。
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
如果您不在 Base 的 foo 声明中使用“virtual”,那么 Derived 的 foo 只会遮蔽它。
这是前两个答案的 C++ 代码的合并版本。
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
#ifdef VIRTUAL
virtual string says() { return "??"; }
#else
string says() { return "??"; }
#endif
};
class Dog: public Animal
{
public:
string says() { return "woof"; }
};
string func(Animal *a)
{
return a->says();
}
int main()
{
Animal *a = new Animal();
Dog *d = new Dog();
Animal *ad = d;
cout << "Animal a says\t\t" << a->says() << endl;
cout << "Dog d says\t\t" << d->says() << endl;
cout << "Animal dog ad says\t" << ad->says() << endl;
cout << "func(a) :\t\t" << func(a) << endl;
cout << "func(d) :\t\t" << func(d) << endl;
cout << "func(ad):\t\t" << func(ad)<< endl;
}
两个不同的结果是:
如果没有 #define virtual,它会在编译时绑定。Animal *ad 和 func(Animal *) 都指向 Animal 的 say() 方法。
$ g++ virtual.cpp -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says ??
func(a) : ??
func(d) : ??
func(ad): ??
使用 #define virtual,它在运行时绑定。Dog *d、Animal *ad 和 func(Animal *) 指向/引用 Dog 的 says() 方法,因为 Dog 是它们的对象类型。除非[Dog's say() "woof"] 方法没有定义,否则它将是在类树中最先搜索的方法,即派生类可以覆盖其基类[Animal's say()] 的方法。
$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
有趣的是,Python 中的所有类属性(数据和方法)实际上都是虚拟的。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字 virtual。下面是 Python 的代码版本:
class Animal:
def says(self):
return "??"
class Dog(Animal):
def says(self):
return "woof"
def func(a):
return a.says()
if __name__ == "__main__":
a = Animal()
d = Dog()
ad = d # dynamic typing by assignment
print("Animal a says\t\t{}".format(a.says()))
print("Dog d says\t\t{}".format(d.says()))
print("Animal dog ad says\t{}".format(ad.says()))
print("func(a) :\t\t{}".format(func(a)))
print("func(d) :\t\t{}".format(func(d)))
print("func(ad):\t\t{}".format(func(ad)))
输出是:
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
这与 C++ 的虚拟定义相同。请注意,d和ad是两个不同的指针变量,它们引用/指向同一个 Dog 实例。表达式 (ad is d) 返回 True 并且它们的值是相同的 < main .Dog object at 0xb79f72cc>。
你熟悉函数指针吗?虚函数是一个类似的想法,除了您可以轻松地将数据绑定到虚函数(作为类成员)。将数据绑定到函数指针并不容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为……多态性!”
我们需要支持“运行时多态性”的虚拟方法。当您使用指针或对基类的引用来引用派生类对象时,您可以为该对象调用虚函数并执行派生类的函数版本。
底线是虚拟功能让生活更轻松。让我们使用 M Perry 的一些想法并描述如果我们没有虚函数而只能使用成员函数指针会发生什么。在没有虚函数的正常估计中,我们有:
class base {
public:
void helloWorld() { std::cout << "Hello World!"; }
};
class derived: public base {
public:
void helloWorld() { std::cout << "Greetings World!"; }
};
int main () {
base hwOne;
derived hwTwo = new derived();
base->helloWorld(); //prints "Hello World!"
derived->helloWorld(); //prints "Hello World!"
好的,这就是我们所知道的。现在让我们尝试使用成员函数指针来实现:
#include <iostream>
using namespace std;
class base {
public:
void helloWorld() { std::cout << "Hello World!"; }
};
class derived : public base {
public:
void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
void(derived::*hwBase)();
void helloWorld() { std::cout << "Greetings World!"; }
};
int main()
{
base* b = new base(); //Create base object
b->helloWorld(); // Hello World!
void(derived::*hwBase)() = &derived::helloWorld; //create derived member
function pointer to base function
derived* d = new derived(); //Create derived object.
d->displayHWDerived(hwBase); //Greetings World!
char ch;
cin >> ch;
}
虽然我们可以用成员函数指针做一些事情,但它们不像虚函数那样灵活。在类中使用成员函数指针很棘手;成员函数指针几乎,至少在我的实践中,总是必须在主函数中或从成员函数中调用,如上例所示。
另一方面,虚函数虽然可能有一些函数指针开销,但确实大大简化了事情。
编辑:eddietree 还有另一种类似的方法:c++ virtual function vs member function pointer (performance comparison)。
跟进@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 关键字的作用。
这就是为什么函数重载被称为编译时多态性(或早期绑定)而虚函数覆盖被称为运行时多态性(或后期绑定)的原因。
细节:
.*
在内部,当编译器看到一个虚函数时,它会创建一个类成员指针,该指针通常使用and运算符指向类的成员(而不是对象中该成员的特定实例)->*
。他们的工作是允许您访问给定指向该成员的指针的类的成员。这些很少被程序员直接使用(也许除非你正在编写一个编译器来实现“虚拟”)。