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. Pycharm2019最新激活注册码(pycharm激活教程)

    给大家分享一下PyCharm2019最新可用的激活注册码.激活Pycharm专业版的方法有很多,这里主要给大家分享最有效的两种,一种是使用最新可用的注册激活码,一种是使用破解补丁的方法,这种方法虽然麻 ...

  2. bay——安装_Oracle 12C-单实例-Centos7.txt

    安装Oracle12C 总结笔记 IP:10.20.4.214 ---------------------------------------------用户和密码: root/bayaimbayai ...

  3. nvprof 使用记录; 以及使用 nvprof 查看tensorflow-gpu 核函数运行记录

    最近需要使用 nvprof 此时cuda 程序运行的性能,下面对使用过程进行简要记录,进行备忘: 常用使用命令:nvprof --unified-memory-profiling off python ...

  4. Memcached 基本语法 记录

    set 命令:命令将value数值存储在指定的key中: set key flags exptime bytes [noreply] value key:键值 key-value 结构中的 key,用 ...

  5. 以太网驱动的流程浅析(五)-mii_bus初始化以及phy id的获取【原创】

    以太网驱动的流程浅析(五)-mii_bus初始化以及phy id的获取 Author:张昺华 Email:920052390@qq.com Time:2019年3月23日星期六 此文也在我的个人公众号 ...

  6. WINDOWS代理服务器搭建 - Apache httpd

    1.检查电脑系统类型 检查电脑版本是为 32位操作系统 还是 64位操作系统 2.下载安装Apache Httpd 下载地址:https://www.apachehaus.com/cgi-bin/do ...

  7. http返回头中content-length与Transfer-Encoding: chunked的问题释疑

    先说说问题出现的背景:        公司服务器与手机客户端交互,客户端请求一个动态生成的XML文件,在用firebug查看http响应头的时候,有时候发现有content-length属性,有时候没 ...

  8. Vue入门(二)

    1.vue3.0安装 cnpm install -g @vue/cli 或者 yarn global add @vue/cli //创建项目 vue create hello-world //运行 n ...

  9. 解决Fiddler在win7系统下的安全证书问题

    如果电脑win7系统,一直解决不了Fiddler的证书安装问题,可以试试下面的办法. 在安装证书的时候一直遇到这个问题. 这是因为win7系统的在.net Framework4.0上面的bug引起的. ...

  10. 用nodejs快速实现websocket服务端(带SSL证书生成)

    有不少公司将nodejs的socket.io作为websocket的解决方案,很遗憾的是socket.io是对websocket的封装,并不支持html5原始的websocket协议,微信小程序使用的 ...