这几天深入学习了一下c++多态,趁此总结了一下多态中的虚函数,先看一下c++多态中的定义
多态定义:
父类指针指向子类对象,通过父类指针或引用可以调用到正月版本的函数。
而本文主要尝试解释:为什么父类指针指向子类对象,通过父类指针或引用可以调用到正月版本的函数?
如有大牛有更好解释,还望共同探讨。废话不说,直接进入正题
先定义四个类 如下:
//
// main.cpp
// project13
//
// Created by 就不告诉你我是谁 on 15-8-7.
// Copyright (c) 2015年 xuqigang. All rights reserved.
//
#include <iostream>
#include <cstdio>
using namespace std;
class Base{
public:
int num;
public:
void fun(){ std::cout<<"Base->fun()\n" ;};
void print(){ std::cout<<"Base->print()\n";};
void cir(){ std::cout<<"Base->cir()\n";};
};
class Base1{
public:
int num;
public:
virtualvoid fun1(){std::cout<<"Base1->virtual fun1()\n";};
void print1(){std::cout<<"Base1->print1()\n";};
void cir1(){std::cout<<"Base1->cir1()\n";};
};
class Derivted:public Base{
public:
int der;
public:
void fun(){std::cout<<"Derivted->fun()\n";};
void print(){std::cout<<"Derivted->print()\n";};
void get(){std::cout<<"Derivted->print()\n";};
};
class Derivted1:public Base1{
public:
int der;
public:
virtualvoid fun1(){std::cout<<"Derivted1->fun1()\n";};
void print1(){std::cout<<"Derivted1->print1()\n";};
void get1(){std::cout<<"Derivted1->get1()\n";};
};
int main(int argc, const char * argv[])
{
// insert code here...
//首先我们先从一个类,来认识一个类指针(未经初始化的),
// 对于一个不含虚函数的类指针:
Base *p;
// std::cout << p->num<<endl;//程序运行出错
p->num; //这里语句可以执行,但没有结果,直接输出的话,会报停
p->fun();
p->print();
p->cir();
/*
运行结果:
Base->fun()
Base->print()
Base->cir()
*/
// 对于一个含虚函数的类指针
Base1 *p1;
p1->num;
// p1->fun1();//fun1()为虚函数,此时程序无法执行该条语句 直接报停
p1->print1();
p1->cir1();
/*
运行结果:
Base1->print1()
Base1->cir1()
*/
// 综上可知:一个未经初始化的类指针,可以访问普通成员函数,和变量。但无法访问虚函数。访问普通函数,在这里可以理解为这是类指针的一个天生的一种功能。就像鸭子天生会游泳一样。重点解释为什么一个未初始化的类指针,无法访问类中的虚函数。
// 这是因为当一个成员函数被定义为虚函数后,类经编译器编译后,会创建一个虚函数表。该表有相应的地址。而我们定义的虚函数的地址则被保存在这个虚函数表中,也就是说,(这里仅是个人推测)普通成员函数与虚函数的所存放的位置不同。一个未经初始化的类指针,不知道该虚函数表的地址,因此也就无法访问到虚函数表中存放的虚函数,如果一个类指针知道该虚函数表的地址,是不是就可以访问虚函数了呢?答案是:YES 那么如何获得虚函数表的地址呢?请看下面这个例子
// 在一个类对象中,没有虚函数时,的内存布局
Base b;
std::cout <<"类中没有虚函数时,对象b的地址为:";
printf("%p\n",&b);
std::cout <<"类中没有虚函数时,对象b中首个成员变量的地址为:";
printf("%p\n",&b.num);
/*
运行结果:
类中没有虚函数时,对象b的地址为:0x7fff5fbff800
类中没有虚函数时,对象b中首个成员变量的地址为:0x7fff5fbff800
此时我们发现,这两地址相同。于是得出这样的结论:在一个无虚函数的类对象中,对象的地址即是对象中首个成员变量的地址
那么,如果一个类有虚函数呢?请看下面这个例子
*/
Base1 b1;
std::cout << "类中有虚函数时,对象b1的地址为:"<< (int*)(&b1) << endl;//该语句等价于 printf("%p\n",&b1);
std::cout << "类中有虚函数时,对象b1中首个成员变量的地址为:" <<(int*)(&b1.num) << endl;
/*
运行结果:
类中有虚函数时,对象b1的地址为:0x7fff5fbff7f0
类中有虚函数时,对象b1中首个成员变量的地址为:0x7fff5fbff7f8
此时我们发现,这两地址不再相同,并且仔细发现,这两个地址的差值刚好是8个字节,也就是一个指针变量的大小。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置。所以,在类对象的内存布局中,首先是该类的虚函数表指针(里面存放着首个虚函数的地址),然后才是对象数据。也就是说 &b1 的值 得到的就是虚函数表指针的地址。既然这样,在一个有虚函数的类中,我们可以通过取一个类实例化对象的地址即&b1的方式 获得一个类中的虚函数表指针地址
既然虚函数表在类中,只有一份,那么通过同一个类的不同对象,获得的虚函数表的首地址应该也相同,
下面看这个例子:
*/
Base1 b2;
std::cout << "对象b1的虚函数表指针地址:" << (int*)(&b1) << endl;
std::cout << "对象b2的虚函数表指针地址:" << (int*)(&b2) << endl;
std::cout << "通过对象b1获得虚函数表 —第一个虚函数地址:" << (int*)*(int*)(&b1) << endl;
std::cout << "通过对象b2获得虚函数表 —第一个虚函数地址:" << (int*)*(int*)(&b2) << endl;
/*
运行结果:
对象b1的虚函数表指针地址:0x7fff5fbff7d0
对象b2的虚函数表指针地址:0x7fff5fbff7c0
通过对象b1获得虚函数表 —第一个虚函数地址:0x30d0
通过对象b2获得虚函数表 —第一个虚函数地址:0x30d0
通过比较发现,通过对象b1 获得的虚函数表第一个函数地址,与通过b2获得的虚函数表第一个虚函数地址相同。由于虚函数表指针是保留每个对象内存布局的头部,所以不同对象的虚函数表指针地址不同
结论得到验证:即在一个类中,虚函数表只有一个,通过通过同一个类实例化出的不同对象,获得的虚函数表的第一个虚函数地址相同。
此时我们再回过头看下之前定义的 Base1 *p1指针,由于指针p1不知道某个虚函数表指针的地址,进而也就,无法得到虚函数表指针中存放的首个虚函数地址。
由上面可知,可以通过取对象地址,即&b1获得虚函数表指针地址值,如果执行 p1 = &b1 这条语句是不是就可以让指针p1 获得虚函数表指针的地址了呢?
如果获得了虚函数表指针的地址值,那么指针p1是不是就可以获得虚函数表中的首个虚函数地址,通过查找虚函数表,来调用想要调用的虚函数了?请看下面这个例子进行验证:\n";
*/
p1 = &b1;
/*这条语句很多书上说是,将一个指针指向一个对象。这句话没有错。在这里我们应该从另一个角度解释,由于对象b1的类型中有虚函数,因此应真实理解为把虚函数表指针的地址值赋值给指针变量 p1,此刻p1便有了虚函数表指针的地址,然后再通过转型、解引用,就可以得到首个虚函数的地址;值得注意的是,我们试想:虚函数表指针的地址,肯定需要用一个二级指针才能存放,而我们定义的p1显然是一个一级指针,但仍能赋值,这里有两种理解:一种是,&b1我们得到的只是指针的地址值。第二种,这里发生了一次隐式类型转换。*/
std::cout <<"指针p1中存放虚函数表指针地址:" << (int *)p1 <<endl;
std::cout << "通过p1 获得虚函数表 —第一个虚函数地址:" << (int*)*(int*)p1 << endl;
p1 -> fun1();//fun1()在类中定义的是虚函数,未初始化时,无法执行
/*
运行结果:
指针p1 中存放虚函数表指针地址:0x7fff5fbff7d0
通过p1 获得虚函数表 —第一个虚函数地址:0x30d0
Base1->virtual fun1()
程序顺利执行虚函数表中的虚函数 由实验可知:结论得到验证\n";
通过以上知识,我们可以知道,一个类指针如果想访问虚函数表中的虚函数需要满足的必要条件:类指针中存放的有虚函数表指针的地址,至于这个地址是如果获得的,编译器不关心,我们可以通过取对象地址的方式获得,也可以通过其它方式获得,如果有的话。
接下来,我们终于可以解释在一个类中,定义的有虚函数,(多态)当一个父类指针指向一个子类对象的时候,为什么可以通过父类指针调用到正确版本的函数?
同样 我们先从一个没有虚函数的子类对象入手,来了解一下子类对象的内存布局
示例:*/
Derivted d;
std::cout<<"没有虚函数的子类对象d的地址:" <<&d<<endl;/* 获取无虚函数的子类对象的地址*/
std::cout<<"没有虚函数的子类对象d的从父类继承过来的首个成员变量的地址:" <<&d.num<<endl;/* 获取无虚函数的子类对象的从父类继承过来的首个成员变量的地址*/
std::cout<<"没有虚函数的子类对象d的从父类继承过来的首个成员变量的地址:" <<&d.der<<endl;/* 获取无虚函数的子类对象中由子类扩展来的首个成员变量的地址*/
/*
运行结果:
没有虚函数的子类对象d 的地址:0x7fff5fbff7f0
没有虚函数的子类对象d 的从父类继承过来的首个成员变量的地址:0x7fff5fbff7f0
没有虚函数的子类对象d 的从父类继承过来的首个成员变量的地址:0x7fff5fbff7f4
由结果可以看出,对子类对象直接取地址与对子类对象的从父类继承的首个成员变量取地址得到的地址值相同,从而得出结论:子类对象地址值也就是子类对象的从父类继承的首个成员变量地址值,注意我这里强调的是地址值而非地址
对于一个有虚函数存在的子类对象,其内存布局又是如何呢?
*/
Derivted1 d1;
std::cout<< "有虚函数的子类对象d1 的地址:" <<&d1<<endl;/* 获取有虚函数的子类对象的地址*/
std::cout<<"有虚函数的子类对象d1的从父类继承过来的首个成员变量的地址:" <<&d1.num<<endl;/* 获取无虚函数的子类对象的从父类继承过来的首个成员变量的地址*/
std::cout<<"有虚函数的子类对象d1的从父类继承过来的首个成员变量的地址:" <<&d1.der<<endl;/* 获取无虚函数的子类对象中由子类扩展来的首个成员变量的地址*/
/*
运行结果:
有虚函数的子类对象d1 的地址:0x7fff5fbff7a8
有虚函数的子类对象d1 的从父类继承过来的首个成员变量的地址:0x7fff5fbff7b0
有虚函数的子类对象d1 的从父类继承过来的首个成员变量的地址:0x7fff5fbff7b4
可以发现,这里三个地址都不相同,那么&d1 得到的地址是什么意义呢? 我们在研究Base1 类时,提到了c++标准参考手册,同理,这里的&d1的值也是虚函数表指针的地址值;即 &d1我们可以得到的是虚函数表的地址值;
下面我们再来了解一下子类指针的一些特性,首先了解一下不含虚函数的子类指针
*/
Derivted *P;
P->fun();
P->print();
P->cir();
P->get();
/*
程序运行结果如下:
Derivted->fun()
Derivted->print()
Base->cir()
Derivted->get()
这是未经初始化的指针P 所能访问到的函数,被覆盖掉得父类覆盖掉得函数fun() print() 子类指针无法访问即类指针的特性,通俗的说,这是类指针天生的一种本领(特性),这里不进行解释;
而对于含有虚函数的子类指针,又具备哪些天生本领(可以调用哪些函数)呢?接着请看下面的例子:
*/
Derivted1 *P1;
// P1->fun1(); fun1()为虚函数,此时程序无法执行该条语句直接报停
P1->print1(); //从运行结果可知,此时调用的是子类覆盖父类print1()后的函数
P1->cir1(); //从父类继承过来的方法
P1->get1(); //子类扩展的方法
/*
程序运行结果如下:
Derivted1->print1()
Base1->cir1()
Derivted1->get1()
这是未经初始化的指针P1 所能访问到的函数,即类指针的特性,通俗的说,这是类指针P1天生的一种本领(可以调用哪些函数),这里不进行解释;在这里再次证明了,未经初始化的类指针,只能调用普通成员函数,而虚函数存储在虚函数表里,类指针没有虚函数表指针的地址,因此也就无法访问到虚函数表中的虚函数,自然而然,也就P1也就无法调用虚函数fun1(),同样那如果指针P1中存放的有子类虚函数表的地址,是不是就可以访问(调用)到虚函数fun1()啦?答案是:YES下面我们就来验证一下;
*/
P1 = &d1;//通过对象d1获得虚函数表指针的地址值,并赋值给变量P1;
P1 -> fun1();
/*
运行结果:
Derivted1->fun1()
根据运行结果可知,子类中的虚函数fun1()成功的到调用;
饶了这么一大圈,终于可以正式谈谈,为什么在一个含有虚函数的类中,父类指针指向子类对象后,通过父类指针可以调用到正确版本的函数?请看下面的例子
*/
Base1 *pp1;
Derivted1 dd1;
pp1 = &dd1;
/*通过对象dd1获得子类Derivted1的虚函数表的地址,并赋值给(存放到)指针变量pp1中 ,不得不提一下,这里发生了一次隐式的指针类型转型 即&dd1 后,先把地址强制转换为Base1 类型的指针地址 然后再赋值过去,因为对象dd1的类型是Derivited1类型,&dd1后也是Derived类型的;
既然指针pp1获得了子类的虚函数表地址,理所当然就可以访问(调用)子类虚函数表中的所有虚函数;*/
pp1->fun1();
/*调用子类虚函数表中的虚函数,在这里你可能会问,为什么调用的不是父类中的fun1()?当然是因为pp1指针指向的是子类的虚函数表,子类虚函数表中没有父类fun1()这个虚函数啦。原因是因为,子类的虚函数fun1()把从父类中继承的虚函数fun1()覆盖掉,所以无法调用,反过来,如果从父类继承过来的虚函数,在子类虚函数表中没有被覆盖,当然可以被调用;*/
/*
运行结果如下:
Derivted1->fun1()
多态的中心点就是:虚函数表指针地址,通过对虚函数表指针地址解引用我们可以得到首个虚函数地址,进行 &dd1+1操作,可以得到对象中首个成员变量地址。
到这里,终于说完了,是不是明白为什么当一个父类指针指向一个子类对象时,可以通过父类指针可以调用到正确版本的函数了吧!如果诸位有更好的解释,欢迎一起交流分享。如有转载请注明出处——xiao gang最后,掌声鲜花的有没有?
*/
return 0;
}