澳门新葡新京:C++中的虚函数(virtual function)
1.简介
虚函数是C++顶用于实现多态(polymorphism)的机制。核生理念便是经由过程基类造访派生类定义的函数。假设我们有下面的类层次:
class A
{
public:
virtual void foo() { cout
那么,在应用的时刻,我们可以:
A * a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,然则被调用的函数(foo)却是B的!
这个例子是虚函数的一个范例利用,经由过程这个例子,大概你就对虚函数有了一些观点。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时候被确定的,而是在运行时候被确定的。因为编写代码的时刻并不能确定被调用的是基类的函数照样哪个派生类的函数,以是被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果,假如是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
{
A澳门新葡新京 a;
a.foo(); // A::foo()被调用
}1.1 多态
在懂得了虚函数的意思之后,再斟酌什么是多态就很轻易了。仍旧针对上面的类层次,然则应用的措施变的繁杂了一些:
void bar(A * a)
{
a->foo(); // 被调用的是A::foo() 照样B::foo()?
}
由于foo()是个虚函数,以是在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()照样B::foo(),然则可以肯定的说:假如a指向的是A类的实例,则A::foo()被调用,假如a指向的是B类的实例,则B::foo()被调用。
这种同一代码可以孕育发生不合效果的特征,被称为“多态”。
1.2 多态有什么用?
多态这么神奇,然则能用来做什么呢?这个命题我难以用一两句话概括,一样平常的C++教程(或者其它面向工具说话的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,假如你不知道这个例子,随便找本书应该都有先容。我试图从一个抽象的角度描述一下,转头再结合那个画图的例子,大概你就更轻易理解。
在面向工具的编程中,首先会针对数据进行抽象(确定基类)和承袭(确定派生类),构成类层次。这个类层次的应用者在应用它们的时刻,假如仍旧在必要基类的时刻写针对基类的代码,在必要派生类的时刻写针对派生类的代码,就即是类层次完全裸露在应用者眼前。假如这个类层次有任何的改变(增添了新类),都必要应用者“知道”(针对新类写代码)。这样就增添了类层次与其应用者之间的耦合,有人把这种环境列为法度榜样中的“bad smell”之一。
多态可以使法度榜样员离开这种困境。再转头看看1.1中的例子,bar()作为A-B这个类层次的应用者,它并不知道这个类层次中有若干个类,每个类都叫什么澳门新葡新京,然则一样可以很好的事情,当有一个C类从A类派生出来后,bar()也不必要“知道”(改动)。这完全归功于多态--编译器针对虚函数孕育发生了可以在运行时候确定被调用函数的代码。
1.3 若何“动态联编”
编译器是若何针对虚函数孕育发生可以再运行时候确定被调用函数的代码呢?也便是说,虚函数实际上是若何被编译器处置惩罚的呢?Lippman在深度探索C++工具模型[1]中的不合章节讲到了几种要领,这里把“标准的”要领简单先容一下。
我所说的“标准”要领,也便是所谓的“VTABLE”机制。编澳门新葡新京译器发明一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也便是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有若干个实例。派生类有自己的VTABLE,然则派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时刻,编译器还会在每个实例的内存结构中增添一个vptr字段,该字段指向本类的VTABLE。经由过程这些手段,编译器在看到一个虚函数调用的时刻,就会将这个调用改写,针对1.1中的例子:
2.2 纯虚函数
如下声明表示一个函数为纯虚函数:
class A
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行径,实际上便是所谓的“接口”。它奉告应用者,我的派生类都邑有这个函数。
2.3 虚析构函数
析构函数也可所以虚的,以致是纯虚的。例如:
class A
{
public:
virtual ~A()=0; // 纯虚析构函数澳门新葡新京
};
当一个类盘算被用作其它类的基类时,它的析构函数必须是虚的。斟酌下面的例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A *澳门新葡新京 a = new B;
delete a;
}
在这个例子中,法度榜样大概不会象你想象的那样运行,在履行delete a的时刻,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可骇?
假如将上面A::~A()改为virtual,就可以包管B::~B()也在delete a的时刻被调用了。是以基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么感化,是虚的就够了。平日只有在盼望将一个类变成抽象类(不能实例化的类),而这个类又没有相宜的函数可以被纯虚化的时刻,可以应用纯虚的析构函数来达到目的。