C++ 虚函数实现多态浅析

这几天深入学习了一下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;

}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-10 16:12:20

C++ 虚函数实现多态浅析的相关文章

C++中的重载,隐藏,虚函数,多态浅析

直到今日,才发现自己对重载的认识长时间以来都是错误的.幸亏现在得以纠正,真的是恐怖万分,雷人至极.一直以来,我认为重载可以发生在基类和派生类之间,例如: 1 class A { 2 public: 3 void test(int); 4 }; 5 class B : public A { 6 public: 7 void test(int, int); 8 }; 9 10 void main() 11 { 12 B b; 13 14 b.test(5);  //错误,应该b.A::test(5)

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

C++中虚函数和多态

1.C++中的虚函数 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Ta

你好,C++(37)上车的人请买票!6.3.3 用虚函数实现多态

6.3.3  用虚函数实现多态 在理解了面向对象的继承机制之后,我们知道了在大多数情况下派生类是基类的“一种”,就像“学生”是“人”类中的一种一样.既然“学生”是“人”的一种,那么在使用“人”这个概念的时候,这个“人”可以指的是“学生”,而“学生”也可以应用在“人”的场合.比如可以问“教室里有多少人”,实际上问的是“教室里有多少学生”.这种用基类指代派生类的关系反映到C++中,就是基类指针可以指向派生类的对象,而派生类的对象也可以当成基类对象使用.这样的解释对大家来说是不是很抽象呢?没关系,可以

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr).C++对象模型 一.虚函数表指针(vptr)及虚基类表指针(bptr) C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括: virtual function机制:用以支持一个有效率的"执行期绑定": virtual base class:用以实现多次在继承体系中的基类,有一个单一而被共享的实体. 1.虚函数表指针 C++中,有两种数据

c++ 内存分配 虚函数实现多态等

看到了很好的解释,先mark下,有空整理. 虚函数实现多态与多重继承乌索普的回答 很有收获,毕竟我觉得多态才是面向对象的灵魂. 这样的设计也让人得以理解为什么虚函数会像成员变量一样与对象绑定. C++成员函数在内存中的存储方式 原文地址:https://www.cnblogs.com/zsl96/p/8732662.html

&lt;C++&gt; 类(3):初始化列表 常函数和常量对象 虚函数与多态(包括纯虚函数)

一.初始化列表(初始化列表中必须有的两个内容) 1.类中const的成员变量: ①特点:不能修改 必须初始化 在构造函数后面加冒号 格式为:":变量名(值)" 也就是说 常量必须在初始化列表中初始化 ②执行顺序:构造函数先执行初始化列表 然后执行函数中的内容 1 #include<iostream> 2 using namespace std; 3 4 class CPerson 5 { 6 public: 7 const int a; 8 public: 9 CPerso

一道理解虚函数(多态)机制的题目

一道理解虚函数(多态)机制的题目(摘抄) 以下程序输出为 class Base { public: Base(int j): i(j) {} virtual~Base() {} void func1() { i *= 10; func2(); } int getValue() { return i; } protected: virtual void func2() { i++; } protected: int i; }; class Child: public Base { public: