为什么我们需要C ++的虚函数?
我正在学习C ++,而我正在进入虚拟function。
从我读过的(在书中和在线中),虚拟函数是基类中的函数,您可以在派生类中重写。
但是在本书的前面,在学习基本inheritance时,我能够在派生类中重写基本函数而不使用virtual
。
那么我在这里错过了什么? 我知道有更多的虚拟function,这似乎是重要的,所以我想清楚它到底是什么。 我只是无法在网上find一个直接的答案。
我自己是一个C ++新手,但这里是我不仅了解什么是虚函数,而且为什么他们是必需的:
假设你有这两个类:
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."; } };
在你的主要function:
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(); }
现在我们的主要function是:
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*
? 如果你必须从动物中获得更多的动物,他们都需要自己的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."
完成。
没有“虚拟”,你会得到“早期绑定”。 该方法的哪个实现将在编译时根据所调用的指针的types决定。
与“虚拟”你得到“晚绑定”。 该方法的哪个实现是在运行时根据指向对象的types – 它最初构造的是什么来决定的。 这不一定基于指向该对象的指针的types。
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* obj = new Derived (); // Note - constructed as Derived, but pointer stored as Base* obj->Method1 (); // Prints "Base::Method1" obj->Method2 (); // Prints "Derived::Method2"
编辑 – 看到这个问题 。
另外 – 本教程将介绍C ++中的早期和晚期绑定。
你需要至less1级的inheritance和沮丧来certificate它。 这是一个非常简单的例子:
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 cout << d->Says(); // always Woof cout << a->Says(); // Woof or ?, depends on virtual }
如果基类是Base
,派生类是Der
,那么可以有一个Base *p
指针,它实际上指向了Der
一个实例。 当你调用p->foo();
,如果foo
不是虚拟的,那么Base
的版本会执行,忽略p
实际上指向Der
的事实。 如果foo 是虚拟的,则p->foo()
执行p->foo()
的“最叶”覆盖,充分考虑指向项目的实际类别。 所以虚拟和非虚拟之间的区别实际上非常重要:前者允许运行时多态 ,OO编程的核心概念,而后者则不允许。
需要虚拟function解释[易懂]
#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.
但是有了虚拟function:
#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.
因此,使用虚函数可以实现运行时多态性。
你需要虚拟的方法来安全的向下转换 , 简单和简洁 。
这就是虚拟方法所做的事情:它们安全地倒下,显然简单明了的代码,避免了不必要的手工强制转换成更复杂和冗长的代码。
非虚方法⇒静态绑定
以下代码有意“不正确”。 它不会将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; }
在注释为“bad”的行中,由于静态已知types (编译时已知的types)是Expression
,并且value
方法不是虚拟的,因此将调用Expression::value
方法。
虚拟方法⇒dynamic绑定。
在静态已知typesExpression
中将value
声明为virtual
可确保每次调用都将检查实际的对象types,并调用该dynamictypes的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
,因为虚拟方法被虚拟调用 。 这也被称为呼叫的dynamic绑定 。 执行一些检查,find对象的实际dynamictypes,并调用该dynamictypes的相关方法实现。
相关的实现是最具体(最派生)类中的实现。
请注意,这里派生类中的方法实现不是标记为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; }
看这个的一个积极的方法就是,如果遇到不安全的向下转换,如上所述的复杂性和冗长性,那么通常一个虚拟方法或多个方法可以真正起作用。
你必须区分重载和重载。 如果没有virtual
关键字,只能重载一个基类的方法。 这意味着隐藏。 假设您有一个基类Base
和一个派生类Specialized
,它们都实现了void foo()
。 现在你有一个指向Base
的指针指向一个Specialized
的实例。 当你调用foo()
时,你可以观察到virtual
的不同之处:如果方法是虚拟的,则使用Specialized
的实现,如果缺失,则selectBase
的版本。 最好不要从基类中重载方法。 做一个非虚拟的方法是它的作者的方式告诉你,它在子类中的扩展是不打算的。
为什么我们需要C ++中的虚拟方法?
快速回答:
- 它为我们提供了面向对象编程所需的“成分” 1 。
在Bjarne Stroustrup C ++编程:原理与实践(14.3):
虚函数提供了在基类中定义函数的能力,并且在用户调用基类函数时调用的派生类中具有相同名称和types的函数。 这通常称为运行时多态性 , dynamic调度或运行时调度,因为调用的函数是在运行时基于所使用的对象的types确定的。
- 如果你需要虚拟函数调用 2,它是最快速的更高效的实现。
为了处理虚拟呼叫,需要一个或多个与派生对象 3相关的数据。 通常做的方式是添加函数表的地址。 该表通常被称为虚拟表或虚拟function表 ,其地址通常被称为虚拟指针 。 每个虚函数在虚表中都有一个插槽。 根据调用者的对象(派生)types,虚拟函数依次调用相应的覆盖。
1.使用inheritance,运行时多态和封装是面向对象编程的最常见的定义。
2.无法使代码function更快,或者使用其他语言function使用更less的内存来在运行时select其他语言。 Bjarne Stroustrup C ++编程:原理与实践(14.3.1) 。
3.当我们调用包含虚函数的基类时,会告诉哪个函数真的被调用。
在基类中有函数时,可以在派生类中重Redefine
或Override
它。
重新定义方法 :在派生类中给出了基类方法的新实现。 不利于Dynamic binding
。
覆盖方法 : Redefining
派生类中的基类的virtual method
。 虚拟方法有助于dynamic绑定 。
所以当你说:
但是在本书的前面,在学习基本inheritance时,我能够在不使用“虚拟”的情况下重写派生类中的基本方法。
你不是在压倒它,因为基类中的方法不是虚拟的,而是你正在重新定义它
如果你知道底层的机制,它会有所帮助。 C ++将C程序员使用的一些编码技术正式化,“类”被“覆盖”replace – 具有公共头部分的结构将被用于处理不同types的对象,但是具有一些共同的数据或操作。 通常,覆盖层(公共部分)的基础结构具有指向function表的指针,该表指向每个对象types的一组不同的例程。 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关键字告诉编译器不要在编译时做出(函数绑定的)决定,而是把它作为运行时间来推迟“ 。
-
您可以通过在基类声明中关键字
virtual
前面来创build虚函数。 例如,class Base { virtual void func(); }
-
当基类具有虚拟成员函数时,从基类inheritance的任何类都可以用完全相同的原型 重新定义函数,即只能重新定义函数,而不是函数的接口。
class Derive : public Base { void func(); }
-
基类指针可以用来指向基类对象以及派生类对象。
- 当通过使用Base类指针调用虚拟函数时,编译器在运行时决定调用哪个版本的函数(即Base类版本或重写的Derived类版本)。 这被称为运行时多态性 。
虚拟关键字告诉编译器不应该执行早期绑定。 相反,它应该自动安装执行后期绑定所需的所有机制。 为了达到这个目的,典型的compiler1为每个包含虚拟函数的类创build一个表(称为VTABLE)。编译器将该特定类的虚拟函数的地址放在VTABLE中。 在每个具有虚函数的类中,它秘密地放置一个叫vpointer(缩写为VPTR)的指针,该指针指向该对象的VTABLE。 当通过基类指针进行虚拟函数调用时,编译器悄悄地插入代码来获取VPTR并在VTABLE中查找函数地址,从而调用正确的函数并导致后期绑定发生。
在这个链接中的更多细节http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
我想添加另一个虚拟函数的用法,尽pipe它使用了与上述答案相同的概念,但我想它值得一提。
虚拟破坏者
考虑下面这个程序,没有声明基类析构函数为虚拟的; 猫的记忆可能无法清理。
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; }
输出:
删除动物
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; }
输出:
删除动物名称猫
删除动物
(对不起,我是初学者,这个答案可能会有错误,但我会和你分享我的研究成果)我用对话的forms写了一个更好的阅读:
………………………………………….. ……………………..
为什么我们需要虚拟function?
由于多态性。
什么是多态?
基指针也可以指向派生types对象。
这个多态性的定义如何导致对虚拟function的需求?
那么,通过早期的约束 。
什么是早期绑定?
C ++中的早期绑定(编译时绑定)意味着一个函数调用在程序执行之前是固定的。
所以…?
因此,如果使用基本types作为函数的参数,那么编译器只会识别基本接口,如果使用派生类的任何参数调用该函数,它将被切掉,这不是您想要发生的事情。
如果不是我们想要发生的事情,为什么这是允许的?
因为我们需要多态!
多晶现在有什么好处呢?
你可以使用一个基类指针作为单个函数的参数,然后在你的程序运行时,你可以毫无问题地访问每个派生types接口(例如它们的成员函数),使用这个单引用基指针。
我仍然不知道什么虚拟function对…有好处! 这是我的第一个问题!
好吧,这是因为你太早问你的问题了!
为什么我们需要虚拟function?
假设你用一个基指针调用了一个函数,该基指针的派生类中有一个对象的地址。 正如我们上面讨论的那样,在运行时,这个指针被取消引用,但是,我们期望一个方法(==一个成员函数)“来自我们的派生类”被执行! 然而,一个相同的方法(一个具有相同头文件的方法)已经在基类中定义了,那么为什么你的程序会费心select另一个方法呢? 换句话说,我的意思是,你怎么能把这个场景从我们之前看到的事情中解脱出来呢?
简单的答案是“一个虚拟的基础成员函数”,稍微长一点的答案是“在这一步,如果程序在基类中看到一个虚函数,它就知道(意识到)你正在尝试使用多态性“,所以派生类(使用v-table ,后期绑定的forms)来find另一个具有相同头的方法 ,但是预期具有不同的实现。
为什么不同的实现?
你kn头! 去读一本好书 !
好的,等待等待,当他/她可以简单地使用派生types指针时,为什么还要费心使用基本指针呢? 你是法官,这难道是头疼吗? 看看这两个片段:
// 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();
而实际上,这也可能是一个人为的例子!
关于效率, 虚拟函数与早期绑定函数相比效率略低。
“这个虚拟调用机制几乎和”正常函数调用“机制一样高效(在25%以内),它的空间开销是一个虚拟函数的类的每个对象中的一个指针加上每个这样的类的一个vtbl”[ A Bjarne Stroustrup 的C ++游览 ]
虚拟方法用于界面devise。 例如在Windows中,有一个叫做IUnknown的接口,如下所示:
interface IUnknown { virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef () = 0; virtual ULONG Release () = 0; };
这些方法留给接口用户来实现。 它们对于必须inheritanceIUnknown的某些对象的创build和销毁至关重要。 在这种情况下,运行时间意识到三种方法,并期望它们在调用它们时执行。 所以从某种意义上说,它们是对象本身和任何使用该对象的对象之间的契约。
我们需要虚拟的方法来支持“运行时多态性”。 当您使用指针或对基类的引用来引用派生类对象时,可以调用该对象的虚函数并执行派生类的函数版本。
virtual关键字强制编译器select在对象类中定义的方法实现,而不是在指针的类中。
Shape *shape = new Triangle(); cout << shape->getName();
在上面的例子中,Shape :: getName将被默认调用,除非getName()在基类Shape中被定义为虚拟。 这迫使编译器在Triangle类而不是在Shape类中寻找getName()实现。
虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。 这也被称为dynamic调度,并有一些相关的开销。
最后,为什么在C ++中甚至需要虚拟化,为什么不把它作为Java中的默认行为呢?
- C ++基于“零开销”和“支付你所用的东西”的原则。 所以它不会尝试为您执行dynamic调度,除非您需要它。
- 为接口提供更多的控制。 通过使函数非虚拟化,接口/抽象类可以控制其所有实现中的行为。
为什么我们需要虚拟function?
虚拟函数避免了不必要的types转换问题,我们中的一些人可以辩论,为什么我们需要虚函数的时候,我们可以使用派生类的指针来调用派生类中特定的函数!答案是 – 它使大系统中的inheritance开发中,非常希望有单个指针基类对象。
下面我们来比较两个简单的程序来理解虚函数的重要性:
没有虚拟function的程序:
#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; }
OUTPUT:
Fathers age is 50 years Fathers age is 50 years son`s age is 26 years
具有虚拟function的程序:
#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; }
OUTPUT:
Fathers age is 50 years son`s age is 26 years son`s age is 26 years
通过仔细分析两个产出,就能理解虚拟function的重要性。