c++ 多态,就是利用了一个二级指针(指针数组),数组里的每个元素都指向了,用virtual修饰的成员函数。

既然提到了指针,那就让我们用内存地址来证明一下吧。

为了证明,我们必须要取到成员函数的首地址。利用下面的函数取得成员函数的地址

template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
return *static_cast<dst_type*>(static_cast<void*>(&src));
}

调用上面函数的方法:

void* p1 = pointer_cast<void*>(&类名::成员方法名);

1,首先我们先看看非多态的成员方法的内存布局,通过下面的证明代码:

#include "stdio.h"
#include "string.h"
#include <iostream> template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
return *static_cast<dst_type*>(static_cast<void*>(&src));
} class A
{
private:
int a;
char b;
short c;
public:
A(int m,char n,short t)
{
a = m;b = n;c = t;
}
void funca(){
std::cout << "A::func()" << std::endl;
}
};
class B:public A
{
private:
char d;
public:
B(int m,char n,short t,char q):A(m,n,t)
{
d = q;
}
void funcb(){
std::cout << "B::func()" << std::endl;
}
}; int main()
{
A x(1,2,3);
B y(1,2,3,4); A* m = new B(1,2,3,4);
void* p1 = pointer_cast<void*>(&A::funca);
void* p2 = pointer_cast<void*>(&B::funcb);
void(*a)() = (void(*)())p1;
a();
return 0;
}

实验环境:gcc version 7.3.0。ubuntu18.04

下图是GDB的调试截图

gdb里的操作步骤1,先用【info line 22】命令去得到A类的成员函数funca的地址,22是成员函数funca所在的行号。

gdb里的操作步骤2,因为p1是指向A类的成员函数funca的指针,所以【p p1】的结果就是A类的成员函数funca的地址。

结论:通过步骤1和步骤2的结果来看,得出的两个地址是相同的。

说明了什么呢?说明了非多态的成员函数的地址偏移量,是在编译阶段,就固定好了的。

地址偏移量是啥?

当运行程序时,先从硬盘把程序载入到内存中,这个程序就成为了一个运行中的进程,操作系统会给进程分配虚拟内存空间,为了能够调用到函数,必须知道函数在的虚拟内存空间的地址。这个地址调用侧怎么知道的呢,在编译阶段,编译器自动把被调用函数相对于进程首地址的偏移量算出来了,告诉了调用测,所以调用侧才能找到被调用函数的地址。

2,我们再来看看多态的成员方法的内存布局,通过下面的证明代码:

#include "stdio.h"
#include "string.h"
#include <iostream> template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
return *static_cast<dst_type*>(static_cast<void*>(&src));
} class A
{
private:
int a;
char b;
short c;
public:
A(int m,char n,short t)
{
a = m;b = n;c = t;
}
virtual void func(){
std::cout << "A::func()" << std::endl;
}
virtual void func1(){
std::cout << "A::func1()" << std::endl;
}
};
class B:public A
{
private:
char d;
public:
B(int m,char n,short t,char q):A(m,n,t)
{
d = q;
}
virtual void func(){
std::cout << "B::func()" << std::endl;
}
virtual void func1(){
std::cout << "B::func1()" << std::endl;
}
}; int main()
{
A x(1,2,3);
B y(1,2,3,4); A* m = new B(1,2,3,4);
void* p1 = pointer_cast<void*>(&A::func);
void* p2 = pointer_cast<void*>(&B::func);
m->func();//调用的是B的func
m->func1();//调用的是B的func1
//void(*a)() = (void(*)())p1;
//a();
return 0;
}

下图是GDB的调试截图

指针p1指向的类A的func成员函数;

指针p2指向的类B的func成员函数;

但是从gdb的结果来看,他们指向的地址都是0x1,也就说明他们没有正确的指向类的成员函数。

那么类的成员函数的地址在哪里呢?看下面的gdb截图

通过【p x】查看对象x,发现x里面多了个_vptr的东西。这个东西就是最开始说的二级指针(指针数组)。

步骤1:先用【info line 22】命令去得到A类的成员函数func的地址.

步骤2:【p *(void**)0x555555755d48】,先把_vptr指针转成void型的二级指针,然后再用【*】取得地址里的内容,发现地址类存放的就是类A的成员函数func的地址0x555555554cc4。

结论:_vptr指向的就是所有虚函数中的第一个虚函数的地址。

问题来了,如何得到第二个呢?因为64位系统指针所占8个字节,所以(_vptr + 8)就是第二个虚函数的地址。

下面就让多态的真相浮出水面

当有如下代码:用父类的指针指向子类的对象(多态的最终目的:面对抽象类编程),然后调用子类和父类完全相同的函数(必须是虚函数)。让人迷惑,到底调用的是哪个。

A* m = new B(1,2,3,4);//B是A的子类
m->func();
m->func1();

用父类的指针调用虚函数时,先去它指向的内存中(子类所占用的内存)找_vptr,然后从_vptr里找函数的地址。非虚函数的地址不在_vptr里。

步骤1:【p *m】,发现m是类A的对象

步骤2:【set print object on】,含义是显示对象真正的类型

步骤3:【p *m】,发现m原来不是类A的对象,而是类B的对象。

步骤4:查看_vptr里第一个指针,发现指向的是B的func;加8后得到第二个指针,发现发现指向的是B的func1.

让虚函数们的地址存入_vptr里面的代码肯定是由编译器给我们加上的,这些代码加到哪里了呢?

加到了构造函数里

c/c++ 学习互助QQ群:877684253

本人微信:xiaoshitou5854

c++ 多态的内幕的更多相关文章

  1. PHP面向对象编程——深入理解方法重载与方法覆盖(多态)

    什么是多态? 多态(Polymorphism)按字面的意思就是“多种状态”.在面向对象语言中,接口的多种不同的实现方式即为多态.引用Charlie Calverts对多态的描述——多态性是允许你将父对 ...

  2. PHP面向对象编程之深入理解方法重载与方法覆盖(多态)

    这篇文章主要介绍了PHP面向对象编程之深入理解方法重载与方法覆盖(多态)的相关资料,需要的朋友可以参考下: 什么是多态? 多态(Polymorphism)按字面的意思就是"多种状态" ...

  3. 【解惑】Java动态绑定机制的内幕

    在Java方法调用的过程中,JVM是如何知道调用的是哪个类的方法源代码? 这里面到底有什么内幕呢? 这篇文章我们就将揭露JVM方法调用的静态(static binding) 和动态绑定机制(auto ...

  4. 封装,capsulation,&&继承,Inheritance,&&多态,polymorphism

    Inheritance&&polymorphism 层次概念是计算机的重要概念.通过继承(inheritance)的机制可对类(class)分层,提供类型/子类型的关系.C++通过类派 ...

  5. C++多态有哪几种方式?

    C++多态方式: (1)静态多态(重载,模板) 是在编译的时候,就确定调用函数的类型. (2)动态多态(覆盖,虚函数实现) 在运行的时候,才确定调用的是哪个函数,动态绑定.运行基类指针指向派生类的对象 ...

  6. 多态、继承、this、super

    先放一下多态的定义: (360词典上的哈) 多态(Polymorphism)按字面的意思就是"多种状态".在面向对象语言中,接口的多种不同的实现方式即为多态.引用Charlie C ...

  7. Java基础之多态和泛型浅析

    Java基础之多态和泛型浅析 一.前言: 楼主看了许多资料后,算是对多态和泛型有了一些浅显的理解,这里做一简单总结 二.什么是多态? 多态(Polymorphism)按字面的意思就是“多种状态”.在面 ...

  8. 洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法

    "为什么这个功能用不了?" 程序员:"清一下缓存" 上篇洞悉系列文章给大家详细介绍了MySQL的存储内幕:洞悉MySQL底层架构:游走在缓冲与磁盘之间.既然聊过 ...

  9. SQL Server技术内幕笔记合集

    SQL Server技术内幕笔记合集 发这一篇文章主要是方便大家找到我的笔记入口,方便大家o(∩_∩)o Microsoft SQL Server 6.5 技术内幕 笔记http://www.cnbl ...

随机推荐

  1. Cesium专栏-裁剪效果(基于3dtiles模型,附源码下载)

    Cesium Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精 ...

  2. SAP OB52会计年度变式

    Var.(Posting Period Variant) 记帐区间变式,每个公司代码对应一个记帐期间变式,多个公司代码可以使用一个相同的记帐期间变式 A(Performance Assistant) ...

  3. Android Service 启动流程

    执行顺序 : startService -> bindService -> unbindService -> stopService 回调的结果为: 执行顺序 : startServ ...

  4. Word List 1

    前言 图片均来源网络 文章目录 前言 1.1 Super computer 1.2 Mainframe 1.3 Server 1.4 Desktop PC 1.5 Notebook or Laptop ...

  5. Linux下使用 github+hexo 搭建个人博客01-hexo搭建

    为什么要搭建自己的博客系统? 原因有好几个吧,归类如下:1.自己搭建博客系统很有成就感,可以自己选定页面风格和页面排版: 2.自己搭建博客系统可以根据自己的需要添加各种插件功能,因此整体上比网上的第三 ...

  6. IT宝塔安装,Centos系统

    宝塔安装地址:https://www.bt.cn/btcode.html 本文链接地址:https://www.cnblogs.com/wannengachao/p/12036716.html 版权声 ...

  7. 使用EA将源码转化为类图

    EA简介: EA 全名 Enterprise Architect,是一款著名的 UML 建模软件.在软件架构设计与逆向工程中具有重要作用.软件十分小巧(< 60MB ),并且有汉化破解版.推荐大 ...

  8. 防止ssh暴力破解的小工具denyhosts

    DenyHosts 简介 DenyHosts 是 Python 语言写的一个程序软件,运行于 Linux 上预防 SSH 暴力破解的,它会分析 sshd 的日志文件(/var/log/secure), ...

  9. Python extend函数解读

    num = [1,2] print('将1迭代2次') num.extend([1]*2) print(num) print('将2迭代3次') num.extend([2] * 3) print(n ...

  10. pymongo和mongoengine安装和使用教程 包含常用命令行和代码示例 | pymongo and mongoengine tutorial on ubuntu 16.04

    本文首发于个人博客https://kezunlin.me/post/e88f04e5/,欢迎阅读最新内容! pymongo and mongoengine tutorial on ubuntu 16. ...