C++虚函数与虚表

有一定面向对象知识的朋友对继承与多态一定很熟悉,C++想实现继承的话就要使用虚函数,那么什么是虚函数,其原理是什么,下面尽量给大家分析一下C++中其运行机制:

 

首先,基础,什么是虚函数,什么是多态?

答:被virtual关键字修饰的成员函数,就是虚函数。虚函数用来实现多态性(Polymorphism),将接口与实现进行分离;简单来说就是实现共同的方法,但因个体差异而采用不同的策略。

举例来说,我有一个动物的基类,这个类里面包含“跑”(virtual  void  run())的一个成员方法,这个基类有两个派生类——猫和袋鼠,我想让猫和袋鼠都可以使用跑的方法,但是很明显他们两个跑的方式是不一样的。这样,由于个体的不同而实现同一方法的不同效果就是多态。

情况1:

class A{
public:
void Show(){ cout<<”I am A”<<endl;}
};
class B:public A{
public:
void Show (){ cout<<”I am B”<<endl;}
};
int main(){
A a;
B b;
a. Show ();
b. Show ();
}


 

这里为了演示方便就声明了AB两个类,B继承了A

这样打印出来的语句肯定是,i am A,I am B。不过这并不是多态,B在声明Show()方法的时候完全是自己的

Show(),他们调用的完全两个对象的方法,所以并不是多态(其实这是一种隐藏)。如何才是多态?一切用指向基类的指针或引用来操作对象是多态的基本特征,也就是说得有指针而且还是指向基类的指针。

那改一下吧~

情况2

int main(){
A a;
B b;
A *p1=&a;
A *p2=&b;
p1-> Show ();
p2-> Show ();
}


那现在结果呢?I am AI am A。还是不对,虽然p2已经指向b,但是还是调用的AShow()。

所以,我们不妨按照虚函数的定义,试一下,把Show前面加一个virtual

情况3

class A{
public:
virtual void Show(){ cout<<”I am A”<<endl;}
};
class B:public A{
public:
virtual void Show (){ cout<<”I am B”<<endl;}
};


现在重新运行main的代码,这样输出的结果就是I am AI am B了。

简单总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数

必须是基类指针指向派生类对象,派生类指针不能指基类。所以派生类只能操作父类有的属性及函数。对于那些父类没有的属性,必须将父类指针强制转化为子类指针后才可使用。

那么为什么会根据不同的类对象,调用不同版本的函数呢?

 

—————————————————————————————类的指针与对象分析———————————————————————————————————

写到这里,我想先放一放,非常建议大家和我一起研究一下类的指针和类的对象,这对之后的理解有很大的帮助。

A a;                          //类的对象

A *p1=&a;或者A*p1=newA();    //类的指针

A*p1=NULL;                   //类的指针,是可以先行定义的

B b;                         //类的对象

(这里要说明一下,用new创建的指针(必须赋予指针)需要结束之后调用delete,才能执行析构函数。而B b不需要手动释放,可以自动调用构造函数与析构函数)

 

类的指针(用 ->操作符):他是一个内存地址值,他指向内存中存放的类对象(包括一些成员变量所赋的值),如果用new声明的话那么他使用的是内存,是个永久变量,除非你释放它(否则也是在栈中). 并且没有调用构造函数。
类的对象(用 . 操作符):他是利用类的构造函数在内存中分配一块内存(包括一些成员变量所赋的值,在运行时就分配了对应大小的内存),成员函数的地址是全局已知的,所以其内存无需保存在对象实例中(关于类占用内存的大小,请参考另一篇博客XXXX)。类的对象使用的是内存,是个局部的临时变量。

 

理解 当类是有虚函数的基类,假如Show是它的一个虚函数,则调用Show:   
       
类的对象:调用的是它自己的Show;   
      
类的指针:调用的是分配给它空间时那种类的Show父类的指针可以指向子类的对象

       (我们使用基类的引用或指针调用函数的时候(p2-> Show ();)并不清楚该函数真正调用的对象是什么类型,如果是虚函数的话,就只能在运行时才决定(如果不是虚函数,我们认为编译时就可以定下来了)~不过这里大家肯定还会有疑问?既然它调用的是指针所指向的对象,那不加virtual的函数为什么就没有效果呢?继续往下看)

————————————————————————————————————————————————————————————————————————

 

 

 

好了,下面我们继续学习虚函数,对上面的例子再进行分析!

在上面的情况2下(没有虚函数的情况),我们给A类型指针传递了不同类的地址,按常理来说我们希望这个指针能够区分是基类还是派生类,然而结果却是都当做派生类来处理。所以我们自然想改变这种情况,虚函数也就诞生了~并且实现了多态的效果

 

虚函数这样的特点到底如何实现?答案就是虚函数表!   Virtual Table简称为V-table(虚表)

 

在虚函数存在的类中,编译器就会为他们创建一个vptr指针,这个指针指向的就是虚表(V-Table)。

这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

 

下面举代码例子:

class A{
public:
virtual void show(){ cout<<”Show A”<<endl;}
virtual void print(){cout<<”print  A”<<endl;}
};
 
typedef void (*Fun)(void);  //指向返回void类型并且无参数的函数的指针,类比typedef void (*) (void) Fun;
A a;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&a) << endl; //取a的地址之后强转成int*,这样获取的就是虚函数表的地址(其实就是vptr)
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&a) << endl;  //对vptr解引用(等价于*vptr),强转成int*就是第一个虚函数的地址
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&a));
pFun();


 

 

结果打印:

虚函数表地址:0012FED4

虚函数表  第一个函数地址:0044F148

A::Show

 

 

下面我们再声明两个类:

class B:public A{
public:
virtual void show(){ cout<<”Show B”<<endl;}
virtual void printB(){cout<<”print  B”<<endl;}  //注意这里没有覆盖A的方法
};
class C: public A {
public:
virtual void showC(){ cout<<”Show C”<<endl;}
virtual printC(){cout<<”print  C”<<endl;}
};


这里C没有覆盖A的方法,而B覆盖了A的方法

我们看一下不同情况下的虚函数的表是什么样的

对于无覆盖的虚函数

A  a;


我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

对于有覆盖的虚函数表 ( 没错,其实这就是我们常说的覆盖,而博客一开始的情况一就是隐藏的例子 )


我们可以看到下面几点:

1)覆盖的show()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数位置不变。

这样,我们就可以看到对于下面这样的程序,

A *a = new B();
a->show();
a 所指的内存中的虚函数表的 show() 的位置已经被 B:: show () 函数地址所取代,于是在实际调用发生时,是 B:: show () 被调用了。多态就这样实现了 ~

这里附一张多继承的效果图,B继承与A1,A2,A3、

 

补充:

是基类虚表中有的,在派生类中都有,并根据派生类自己的虚函数进行了扩展

虽然上面例子的a可以访问show(),但是却不能访问printB,即使a虚表里面拥有(父类不可以访问子类自己的虚函数)

如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,我们还是有办法访问到。

 

 

 

最后再说明几点:

1.      静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。 

2.      内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。 

3.      构造函数不能是虚函数,因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。 

4.      析构函数可以是虚函数,而且通常声名为虚函数,继承关系下,派生类的实例中会为基类的成员变量申请相应的内存。析构的时候,我们需要析构基类以及派生类的所有占用空间,不用虚函数的话,就会只调用基类的析构函数

 

说了这么多,应该对虚函数与多态有一个全新的了解了吧~

参考了 wswifth 的博客(感谢!)

你可能感兴趣的:(覆盖,虚函数,多态,虚表,C++类)