我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
我正在学习C++,我刚刚开始学习虚拟函数。
从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。
但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。
那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。
当前回答
如果你知道潜在的机制,这会有所帮助。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的虚拟方法。
其他回答
您需要虚拟方法来实现安全的下变频、简单和简洁。
这就是虚拟方法所做的:它们安全地向下转换,使用明显简单而简洁的代码,避免了在更复杂和冗长的代码中进行不安全的手动转换。
以下代码故意“不正确”。它没有将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方法不是虚拟的。
在静态已知类型表达式中将值声明为virtual可确保每次调用都会检查这是什么实际类型的对象,并调用该动态类型的值的相关实现:
#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,而是标记为override。它们可以被标记为虚拟,但它们是自动虚拟的。override关键字确保如果某个基类中没有这样的虚拟方法,那么您将得到一个错误(这是可取的)。
如果没有虚拟绑定,则必须实现一些自己动手版本的动态绑定。这通常涉及不安全的手动降级、复杂性和冗长。
对于单个函数的情况,如这里所示,将函数指针存储在对象中并通过该函数指针进行调用就足够了,但即使如此,它也会涉及一些不安全的下变频、复杂性和冗长性,即:
#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;
}
看待这一点的一种积极方式是,如果您遇到了如上所述的不安全的下变频、复杂性和冗长,那么通常一个或多个虚拟方法确实会有帮助。
您需要至少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关键字强制编译器选择对象类中定义的方法实现,而不是指针类中的方法实现。
Shape *shape = new Triangle();
cout << shape->getName();
在上面的示例中,默认情况下将调用Shape::getName,除非getName()在基类Shape中定义为virtual。这迫使编译器在Triangle类而不是Shape类中查找getName()实现。
虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也被称为动态调度,并且存在一些与之相关的开销。
最后,为什么在C++中甚至需要虚拟,为什么不将其作为Java中的默认行为?
C++基于“零开销”和“按需付费”的原则。因此,除非您需要,否则它不会尝试为您执行动态调度。为界面提供更多控制。通过使函数非虚拟化,接口/抽象类可以控制其所有实现中的行为。
关键字virtual告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。为了实现这一点,典型的编译器1为每个包含虚拟函数的类创建一个表(称为VTABLE)。编译器将该特定类的虚拟函数的地址放在VTABLE中。在每个具有虚拟函数的类中,它都会秘密地放置一个指针,称为vpointer(缩写为VPTR),该指针指向该对象的VTABLE。当您通过基类指针进行虚拟函数调用时,编译器会悄悄地插入代码以获取VPTR并在VTABLE中查找函数地址,从而调用正确的函数并导致延迟绑定。
此链接中的详细信息http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
解释了虚拟功能的需求[易于理解]
#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.
因此,使用虚拟函数可以实现运行时多态性。