动态绑定,多态(带你从本质了解多态)

article/2025/9/22 17:26:22

在上一章节中,我们讲述了虚函数和虚函数表,我们知道了不管在类中有多少个虚函数,都不会使类的大小扩大,在this指针中,只会多出一个虚函数表的地址,是this指针的第一个内容,在虚函数表中,函数是根据虚函数定义的顺序排列的,在这一章节中,我们将通过深入解析虚函数表,从而从本质上理解多态。

文章目录

  • 一.深入探索虚函数表
            • 1.单继承无函数覆盖下的虚函数表
            • 2.单继承有函数覆盖下的函数虚表
            • 3.多继承无函数覆盖下的虚函数表
  • 二.前期绑定和后期绑定
  • 三.多态

一.深入探索虚函数表

我们知道在虚函数表中存储的是该函数的地址,那么我们该如何验证?
我们可以通过函数指针的方式来调用存储在虚函数表中的函数:

#include "stdafx.h"class Base{
public:int a;int b;Base(){a = 1;b = 2;}void Function_1(){printf("Base:Function_1...\n");}virtual void Function_2(){printf("Base:Function_2...\n");}virtual void Function_3(){printf("Base:Function_3...\n");}
};int main(int argc, char* argv[])
{typedef void (*Function)(void);Base b;int* p;p = (int*)&b;int* function;function = (int*)(*p);Function pFn;for(int i=0;i<2;i++){pFn = (Function)*(function+i);pFn();}return 0;
}

这里我们通过函数指针来调用虚函数表中的地址,发现虚函数表中存的确实是虚函数的地址,且顺序是按照定义虚函数的顺序排列的。

1.单继承无函数覆盖下的虚函数表

我们知道在虚函数表中存的只有虚函数的地址,所以我们在写代码的时候不再写构造函数和析构函数,我们只写虚函数

#include "stdafx.h"class Base{
public:int a;int b;virtual void Function_1(){printf("Base:Function_1...\n");}virtual void Function_2(){printf("Base:Function_2...\n");}
};class Sub1:public Base{
public:int c;virtual void Sub_1(){printf("Sub1:Sub_1...\n");}virtual void Sub_2(){printf("Sub2:Sub_2...\n");}
};int main(int argc, char* argv[])
{typedef void (*Function)(void);Sub1 b;int* p;p = (int*)&b;int* function;function = (int*)(*p);Function pFn;for(int i=0;i<4;i++){pFn = (Function)*(function+i);pFn();}return 0;
}

我们看看程序输出窗口:
单继承无函数覆盖
我们观察代码就能知道,我们通过函数指针调用函数的时候,是根据虚函数表的顺序调用的,所以我们得出结论:
当继承父类的时候,父类的虚函数会在派生类虚函数的上面,且它们的顺序都为定义虚函数的顺序。

2.单继承有函数覆盖下的函数虚表
#include "stdafx.h"class Base{
public:int a;int b;virtual void Function_1(){printf("Base:Function_1...\n");}virtual void Function_2(){printf("Base:Function_2...\n");}
};class Sub1:public Base{
public:int c;virtual void Function_1(){printf("Sub1:Function_1...\n");}virtual void Sub_1(){printf("Sub1:Sub_1...\n");}virtual void Sub_2(){printf("Sub2:Sub_2...\n");}
};int main(int argc, char* argv[])
{typedef void (*Function)(void);Sub1 b;int* p;p = (int*)&b;int* function;function = (int*)(*p);Function pFn;for(int i=0;i<5;i++){pFn = (Function)*(function+i);pFn();}return 0;
}

这里注意一个细节,在通过函数指针调用函数的时候,我写了一个for循坏,并且次数为5,但是在程序运行的时候,它弹出来一个窗口告诉我该地址不可访问,则说明虚函数表里的函数肯定比五个少。
我们来看看程序输出窗口:
单继承有函数覆盖
这里打印出的函数顺序,实际上就是虚函数表里的函数顺序。
这时候,我们应该记得在课堂上,“铁男“说过一句话:”覆盖的是哪个,就在那个表里“。这里我解释一下:子类Function_1覆盖的是父类的虚函数,那么这个函数就出现在”父类的虚函数表“里(注意这里我们只是形象地称为父类的虚函数表,实际上这里只有一张虚函表哦),但是函数的具体功能是我们后定义的那个函数的功能。

3.多继承无函数覆盖下的虚函数表

看完了单继承,我们来看看多继承的虚函数表:

#include "stdafx.h"class Base1{
public:int a;int b;virtual void Base1_1(){printf("Base1:Function_1...\n");}virtual void Base1_2(){printf("Base1:Function_2...\n");}
};class Base2{
public:int c;int d;virtual void Base2_1(){printf("Base2:Function_1...\n");}virtual void Base2_2(){printf("Base2:Function_2...\n");}
};class Sub1:public Base1,Base2{
public:int e;virtual void Sub_1(){printf("Sub1:Sub_1...\n");}virtual void Sub_2(){printf("Sub1:Sub_2...\n");}
};int main(int argc, char* argv[])
{typedef void (*Function)(void);Sub1 b;int* p;p = (int*)&b;int* function;function = (int*)(*p);Function pFn;for(int i=0;i<6;i++){pFn = (Function)*(function+i);pFn();}return 0;
}

根据我们上面的讲解,应该在虚函数表中有6个函数地址,但是程序在运行的时候照样提醒我:该地址不允许访问,说明在虚函数表中,不足6个函数。
我们看看程序输出窗口:
程序输出窗口1

那么到底哪里出了问题?父类Base2中的虚函数去哪了?
我们先来看一下Sub的大小:

#include "stdafx.h"class Base1{
public:int a;int b;virtual void Base1_1(){printf("Base1:Function_1...\n");}virtual void Base1_2(){printf("Base1:Function_2...\n");}
};class Base2{
public:int c;int d;virtual void Base2_1(){printf("Base2:Function_1...\n");}virtual void Base2_2(){printf("Base2:Function_2...\n");}
};class Sub1:public Base1,Base2{
public:int e;virtual void Sub_1(){printf("Sub1:Sub_1...\n");}virtual void Sub_2(){printf("Sub1:Sub_2...\n");}
};int main(int argc, char* argv[])
{printf("%d",sizeof(Sub1));return 0;
}

我们可以看到程序输出窗口输出了28,我们来看看Sub的成员:继承了Base1的a和b,继承了Base2的c和d,自己的成员e,还有一张虚表,应该一共是24,可是它为什么输出了28?
其实通过课堂上老师的讲解我们已经知道:多重继承函数时会有多张虚表,而Base2的虚表就存在于this指针的第二个成员,他是Base2的虚表。

二.前期绑定和后期绑定

我们知道当程序调用函数的时候,有两种调用方式,一种是直接调用函数的地址,这种地址在程序编译的时候就已经写死了,另一种是通过一个地址,间接调用函数。
这里介绍一个名词:绑定,将函数与地址链接在一起的过程,叫做绑定。
直接调用函数的方式,在编译时就已将函数与地址绑定,我们称为(前期)编译期绑定
间接调用函数的方式,在运行的时候才进行绑定,我们称这种方式为(运行期)动态绑定或者晚绑定
注意:
只有virtual函数是动态绑定

三.多态

了解了前面的过程,多态的概念这里一句话就明白了:动态绑定还有另一个名字:多态。
这里给出多态的书面定义:
C++中的多态分为静态多态和动态多态。静态多态是函数重载,在编译阶段就饿能够确定调用哪个函数。动态多态是由继承产生的,指同一个属性或行为在基类和各派生类中具有不同的语义,不同的对象根据所接受的消息做出不同的响应,这种现象称为多态。
多态的实现需要满足三个条件:
(1)基类中声明虚函数
(2)派生类重写基类的虚函数
(3)将基类指针指向派生类对象,通过基类指针访问虚函数
我们来看看多态的具体实现,看看多态到底是什么:

#include "stdafx.h"class Base{
public:int x;Base(){x=100;}virtual void Base_1(){printf("Base:Function_1...\n");}virtual void Base_2(){printf("Base:Function_2...\n");}
};class Sub:public Base{
public:int e;Sub(){x=200;}virtual void Base_1(){printf("Sub1:Sub_1...\n");}
};void Test(Base* p){int n = p->x;printf("%d\n",n);p->Base_1();p->Base_2();
}int main(int argc, char* argv[])
{Base b;Base* p = &b;Test(p);return 0;
}

首先我们定义了一个基类对象,并且通过基类指针去访问函数,我们来看看程序输出框:
多态实现
我们定义了基类对象,并且通过基类指针去访问函数,当然是没有任何问题的。
接下来我们看看多态的实现:
创建一个基类,再创建一个派生类,将基类函数覆盖,通过基类指针访问派生类:

#include "stdafx.h"class Base{
public:int x;Base(){x=100;}virtual void Base_1(){printf("Base:Function_1...\n");}virtual void Base_2(){printf("Base:Function_2...\n");}
};class Sub:public Base{
public:int e;Sub(){x=200;}virtual void Base_1(){printf("Sub1:Sub_1...\n");}
};void Test(Base* p){int n = p->x;printf("%d\n",n);p->Base_1();p->Base_2();
}int main(int argc, char* argv[])
{Sub b;Base* p = &b;Test(p);return 0;
}

我们来看看程序输出窗口:
多态实现
我们可以看到,基类中属性的值也被改变,并且基类中函数也被覆盖,这就是我们所说的多态,同一个属性或行为在基类和各派生类中具有不同的语义,不同的对象根据所接受的消息做出不同的响应。


http://chatgpt.dhexx.cn/article/xHVIL6Hb.shtml

相关文章

数据结构——哈希表(Hash表)、哈希碰撞

1.概述 哈希表&#xff08;也叫散列表&#xff09;&#xff0c;是根据键&#xff08;Key&#xff09;直接访问在内存存储位置的数据结构。就是一种以 键-值(key-value) 存储数据的结构&#xff0c;我们只要输入key&#xff0c;就可查找到其对应的值。 hash函数就是根据key计算…

24-哈希碰撞攻击是什么?

24-哈希碰撞攻击是什么&#xff1f; 最近哈希表碰撞攻击&#xff08;Hashtable collisions as DOS attack&#xff09;的话题不断被提起&#xff0c;各种语言纷纷中招。本文结合PHP内核源码&#xff0c;聊一聊这种攻击的原理及实现。 哈希表碰撞攻击的基本原理 哈希表是一种…

hashcode及哈希碰撞

数据结构中&#xff1a; 用来映射元素关键字(能唯一标识该元素&#xff0c;类似数据库中的主键可以唯一标识一条记录)和元素的内存地址的关系(解决树&#xff0c;线程表等结构中元素和位置无确定关系&#xff0c;查找时需要进行不断比较的问题。顺序查找的比较结果是和不等。树…

hash和hash碰撞以及解决方案

hash&#xff1a; Hash&#xff0c;一般翻译做“散列”&#xff0c;也有直接音译为“哈希”的&#xff0c;就是把任意长度的输入&#xff08;又叫做预映射&#xff0c; pre-image&#xff09;&#xff0c;通过散列算法&#xff0c;变换成固定长度的输出&#xff0c;该输出就是…

哈希碰撞攻击与防范机制

1.引子 哈希表的原理是用数组来保存键值对&#xff0c;键值对存放的位置&#xff08;下标&#xff09;由键的哈希值决定&#xff0c;键的哈希值可以在参数时间内计算出来&#xff0c;这样哈希表插入、查找和删除的时间复杂度为O(1)&#xff0c;但是这是理想的情况下&#xff0…

HashMap的实现原理及hash冲突(碰撞)解决方法

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时&#xff0c;系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法&#xff0c;都可通过该方法获得它的 hashCode 值。得到这…

解决Hash碰撞冲突方法总结

Hash碰撞冲突 我们知道&#xff0c;对象Hash的前提是实现equals()和hashCode()两个方法&#xff0c;那么HashCode()的作用就是保证对象返回唯一hash值&#xff0c;但当两个对象计算值一样时&#xff0c;这就发生了碰撞冲突。如下将介绍如何处理冲突&#xff0c;当然其前提是一…

哈希表碰撞攻击的基本原理

原文地址&#xff1a;http://blog.jobbole.com/11516/ 来源&#xff1a;张洋 最近哈希表碰撞攻击&#xff08;Hashtable collisions as DOS attack&#xff09;的话题不断被提起&#xff0c;各种语言纷纷中招。本文结合PHP内核源码&#xff0c;聊一聊这种攻击的原理及实现。 …

hash碰撞处理方法

目录 哈希表 哈希冲突 解决碰撞方法 1、开放定址法 a)、线性探测法 a)、二次探测法 c&#xff09;伪随机探测 2、再哈希法 3、拉链法 4、建立公共溢出区 哈希表 是一种实现关联数组抽象数据类型的数据结构&#xff0c;这种结构可以将关键码映射到给定值。 简单来说…

通俗讲解哈希表,哈希碰撞问题!

哈希表是个啥&#xff1f; 小白&#xff1a; 庆哥&#xff0c;什么是哈希表&#xff1f;这个哈希好熟悉&#xff0c;记得好像有HashMap和HashTable之类的吧&#xff0c;这是一样的嘛&#xff1f;&#x1f60a; 庆哥&#xff1a; 这个哈希确实经常见&#x1f602;&#xff0c;足…

哈希碰撞是个什么鬼?

什么是哈希算法&#xff1f; 哈希算法&#xff0c;也叫哈希函数&#xff0c;散列函数&#xff0c;是将任意长度的二进制值映射为较短的固定长度的二进制值&#xff0c;即哈希值。哈希算法是一种只能加密&#xff0c;不能解密的特殊算法。 什么是哈希碰撞&#xff1f; 如果不…

哈希值与哈希碰撞

哈希碰撞 一、什么是哈希&#xff1f; 哈希&#xff08;hash&#xff09;就是讲不同的输入&#xff0c;映射成独一无二、固定长度的值&#xff0c;既哈希值。 我们可以理解为商品的条形码。任何商品都会有一个固定长度而又固定的条码。它的作用就类似于哈希。 哈希值长度可…

【Java】哈希冲突(哈希碰撞)

文章目录 为什么发生哈希冲突&#xff08;哈希碰撞&#xff09;能否完全避免哈希冲突常用处理哈希冲突的方法1.开放地址法1.1线性探测再散列缺点&#xff1a;二次聚集 1.2二次探测再散列1.3伪随机探测再散列 2.再哈希地址法3.链地址法4.建立公共溢出区 为什么发生哈希冲突&…

什么是哈希,哈希表,哈希函数,哈希碰撞?

什么是哈希&#xff1f; 比方我有个原始值&#xff0c;S[“老铁双击666”,‘感谢老铁送的飞机’]&#xff0c; 通过某种算法&#xff08;比如java的hasecode(获得变量的物理地址)&#xff09;得到的666这个就是“哈希码“&#xff08;将字符串转换成尽可能不重复的int类型数字…

解决哈希碰撞的方法

什么是hash表 根据设定的哈希函数H(key)和处理冲突的方法将一组关键字映像到一个有限的连续的地址集&#xff08;区间&#xff09;上&#xff0c;并以关键字在地址集中的“像”作为记录在表中的存储位置&#xff0c;这种表便称为哈希表&#xff0c;这一映像过程称为哈希造表或…

公网IP/内网IP:

转自&#xff1a;http://hi.baidu.com/qkjzsjqsehailte/item/1042151cc0959f426926bbb4 IP地址分配 IP地址标识着网络中一个系统的位置。我们知道每个IP地址都是由两部分组成的&#xff1a;网络号和主机号。其中网络号标识一个物理的网络&#xff0c;同一个网络上所有主机需要同…

公网ip、内网ip

首先解释一下“内网”与“外网”的概念&#xff1a; 内网&#xff1a;即所说的局域网&#xff0c;比如学校的局域网&#xff0c;局域网内每台计算机的IP地址在本局域网内具有互异性&#xff0c;是不可重复的。但两个局域网内的内网IP可以有相同的。 外网&#xff1a;即互联网&a…

什么是公网IP和内网IP?

1、引言 搞网络通信应用开发的程序员&#xff0c;可能会经常听到外网IP&#xff08;即互联网IP地址&#xff09;和内网IP&#xff08;即局域网IP地址&#xff09;&#xff0c;但他们的区别是什么&#xff1f;又有什么关系呢&#xff1f;另外&#xff0c;内行都知道&#xff0c…

内网地址与公网地址及作用

下个礼拜就要过年喽 每天离假期更近了一步&#xff0c;就充满了动力 大家回家路上也要注意防护安全哦 ———————————————————— 一般内网指的就是我们园区内网&#xff0c;用的地址一般都是私有地址 私有地址在RFC1918草案中被提到&#xff0c;指的就是10…

java如何获得内网ip、外网ip、以及如何根据ip查询地址

今天突发奇想地想要用java写一个小的工具类。 用来实现如何获得本机的内网ip&#xff0c;外网ip和根据ip获得相应的地址。 花了几个小时才弄清&#xff0c;然后整理了一下&#xff0c;希望有用。 首先要明白以下三种ip地址的区别&#xff1a; &#xff08;1&#xff09;127.0.0…