当前位置: 首页 > news >正文

【C++】多态深度解析:虚函数表与动态绑定的奥秘

在这里插入图片描述

💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 C++。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:C++ 炼 魂 场:从 青 铜 到 王 者 的 进 阶 之 路
✨代 码 趣 语:多 态 就 像 遥 控 器,按 同 一 个 按 钮,不 同 设 备 做 不 同 事,因 为 每 个 设 备 都 有 自 己 的 “功 能 表”(虚 表)。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee

在这里插入图片描述

文 章 目 录

  • 多 态
    • 定 义
    • 虚 函 数
    • 虚 函 数 的 重 写
      • 定 义
      • 特 殊 情 况
    • 构 成 条 件
      • 条 件
      • 代 码 展 示
      • 结 论
    • override 和 final
    • 重 载、覆 盖(重 写)、隐 藏(重 定 义)的 对 比
  • 抽 象 类
  • 多 态 的 原 理
    • 虚 函 数 表
    • 结 论
    • 动 态 绑 定 与 静 态 绑 定
  • 多 继 承 中 的 虚 函 数 表
  • 总 结

         多 态 是 C++ 面 向 对 象 的 核 心 特 性,是 实 现 代 码 灵 活 扩 展 的 关 键,但 学 习 中 常 遇 “概 念 混 淆、原 理 抽 象” 问 题。本 文 从 虚 函 数、重 写 规 则 等 基 础 入 手,拆 解 多 态 构 成 条 件,深 入 虚 表 与 动 态 绑 定 底 层,帮 你 从 “会 用” 到 “理 解” 多 态。


多 态

定 义

         多 态 就 是 多 种 形 态,当 不 同 的 对 象 去 完 成 某 个 行 为, 会 产 生 出 不 同 的 状 态。


虚 函 数

         被 virtual 修 饰 的 类 成 员 函 数 称 为 虚 函 数。

class Person 
{
public://虚函数virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

         全 局 函 数 不 能 构 成 虚 函 数,虚 函 数 必 须 是 成 员 函 数。

virtual void BuyTicket()
{cout << "买票-全价" << endl;
}

在这里插入图片描述


虚 函 数 的 重 写

定 义

         派 生 类 中 有 一 个 跟 基 类 完 全 相 同 的 虚 函 数(即 派 生 类 虚 函 数 与 基 类 虚 函 数 的 返 回 值 类 型、函 数 名 字、参 数 类 型 完 全 相 同),称 子 类 的 虚 函 数 重 写 了 基 类 的 虚 函 数。


特 殊 情 况

         重 写 的 条 件 是 虚 函 数 + 三 同(返 回 值 类 型、函 数 名 字、参 数 列 表 )。

  1. 子 类 可 以 不 加 virtual,父 类 必 须 加,在 这 里 建 议 都 加 上。原 因 是 子 类 已 经 继 承 了 父 类 的 虚 函 数 特 性。
  2. 协 变,返 回 的 值 可 以 不 同,但 是 要 求 返 回 值 必 须 是 父 子 关 系 指 针 和 引 用,不 一 定 是 当 前 父 子 关 系,父 子 必 须 同 时 是 指 针 或 者 引 用。协 变 的 使 用 场 景 很 少。

协 变

#include<iostream>
using namespace std;
class Person 
{
public:virtual Person* BuyTicket(){ cout << "买票-全价" << endl;return 0;}
};
class Student : public Person 
{
public:virtual Student* BuyTicket() { cout << "买票-半价" << endl; return 0;}/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

在这里插入图片描述
3. 析 构 函 数 加 virtual,是 虚 函 数 重 写,因 为 类 析 构 函 数 都 被 处 理 成 destructor 这 个 统 一 的 名 字,让 它 们 构 成 重 写。
情 景 1

#include<iostream>
using namespace std;
class Person 
{
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person 
{
public:virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{Person p;Student s;return 0;
}

在这里插入图片描述
         这 里 父 类 被 析 构 了 两 次 是 因 为 父 类 调 用 完 成 后 被 析 构 。然 后 子 类 对 象 中 有 父 类 对 象,先 析 构 子 类 中 的 父 类,然 后 析 构 子 类。
情 景 2
析 构 函 数 是 虚 函 数 吗?为 什 么 要 一 定 是 虚 函 数?

#include<iostream>
using namespace std;
class Person 
{
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person 
{
public:~Student() { cout << "~Student()" << endl; delete[] ptr;}
private:int* ptr = new int[10];
};
int main()
{//Person p;//Student s;Person* p = new Person;delete p;p = new Student;delete p;//p->destructor() + operator delete(p)//这里期望p->destructor()是一个多态调用,而不是普通调用return 0;
}

在这里插入图片描述
         这 段 代 码 中 只 调 用 了 父 类 的 析 构 函 数,没 有 调 用 子 类 的 析 构 函 数,多 态 调 用 看 的 是 指 向 的 对 象。普 通 对 象 看 当 前 调 用 对 象 的 类 型。在 p = new Student; 这 句 代 码 中,p 的 类 型 是 Person*,这 里 p 被 当 成 了 普 通 对 象,没 有 调 用 子 类 的 析 构 函 数,而 发 生 了 内 存 泄 漏。
         在 C++ 中,当 通 过 基 类 指 针 删 除 派 生 类 对 象 时,如 果 基 类 析 构 函 数 不 是 虚 函 数,只 会 调 用 基 类 析 构 函 数,而 不 会 调 用 派 生 类 析 构 函 数。所 以,需 要 将 析 构 函 数 改 成 重 写 防 止 内 存 泄 漏。
         这 里 建 议 只 要 子 类 要 继 承 父 类,建 议 写 出 子 类 的 析 构 函 数,并 将 父 子 类 前 面 加 上 virtual 改 成 虚 函 数


情 景 3
         虚 函 数 重 写 改 变 的 是 函 数 的 实 现 内 容。虚 函 数 重 写 的 本 质 是 在 派 生 类 中,用 一 个 新 的 函 数 体,替 换 掉 基 类 中 虚 函 数 的 实 现。当 通 过 基 类 指 针 或 引 用 调 用 该 虚 函 数 时,会 根 据 对 象 的 实 际 类 型(运 行 时 类 型)调 用 对 应 的 派 生 类 版 本。

#include<iostream>
using namespace std;
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl;}virtual void test() { func();//this*->func(),this*指的是A*,调用的是父类的func函数}
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

在这里插入图片描述
         这 里 使 用 的 是 基 类 的 函 数 名,派 生 类 的 函 数 体。这 也 就 是 为 什 么 可 以 省 略 派 生 类 的 virtual
在这里插入图片描述


构 成 条 件

条 件

         多 态 是 在 不 同 继 承 关 系 的 类 对 象,去 调 用 同 一 函 数,产 生 了 不 同 的 行 为。那 么 在 继 承 中 要 构 成 多 态 还 有 两 个 条 件:
(1)必 须 通 过 父 类 的 指 针 或 者 引 用 调 用 虚 函 数。
为 什 么 必 须 是 父 类 的 指 针 或 引 用?
         在 C++ 中,动 态 绑 定(多 态)是 通 过 虚 函 数 表(vtable)和 虚 表 指 针(vptr) 实 现 的:

  1. 每 个 包 含 虚 函 数 的 类 编 译 时 会 生 成 一 张 虚 函 数 表(vtable),里 面 存 放 着 虚 函 数 的 地 址。
  2. 每 个 对 象 会 有 一 个 虚 表 指 针(vptr),指 向 自 己 所 属 类 的 vtable。
  3. 当 你 通 过 父 类 的 指 针 或 引 用 调 用 虚 函 数 时,编 译 器 会:
    (1)找 到 该 对 象 的 vptr
    (2)根 据 vptr 找 到 对 应 的 vtable
    (3)在 vtable 中 找 到 真 正 要 调 用 的 函 数 地 址 并 执 行。

         这 样,即 使 指 针 是 父 类 型,但 实 际 对 象 是 子 类 型 时,也 会 调 用 子 类 的 虚 函 数。


为 什 么 不 能 是 子 类?
         如 果 你 用 子 类 指 针,它 只 能 指 向 子 类 对 象,编 译 器 在 编 译 期 就 可 以 确 定 调 用 的 是 子 类 的 函 数(静 态 绑 定),不 需 要 动 态 绑 定。多 态 的 意 义 在 于 用 父 类 指 针 指 向 不 同 子 类 对 象,调 用 同 一 个 方 法,行 为 不 同。如 果 用 子 类 指 针,就 失 去 了 这 种 灵 活 性。


为 什 么 不 能 是 父 类 的 对 象?
         如 果 你 直 接 用 父 类 对 象,那 它 就 是 一 个 纯 粹 的 父 类 对 象,它 的 vptr 指 向 父 类 的 vtable。
         当 你 把 子 类 对 象 赋 值 给 父 类 对 象 时,会 发 生 对 象 切 片:

  1. 子 类 对 象 中 属 于 父 类 的 部 分 会 被 复 制,子 类 特 有 的 成 员 和 vptr 会 被 “切 掉”。
  2. 这 个 父 类 对 象 的 vptr 依 然 指 向 父 类 的 vtable,调 用 虚 函 数 时 只 会 调 用 父 类 版 本。这 就 是 为 什 么 对 象 切 片 会 破 坏 多 态。
    首 先 派 生 类 会 将 父 类 的 虚 表 拷 贝 下 来,然 后 去 重 写 覆 盖 对 应 的 地 址。虚 函 数 表 没 有 拷 贝。
    对 象 的 切 片 和 引 用 的 切 片 是 不 同 的。子 类 赋 值 给 父 类 切 片,不 会 拷 贝 虚 表,如 果 拷 贝 虚 表,则 不 能 确 定 父 类 的 对 象 中 虚 表 是 父 类 还 是 子 类 的 虚 函 数。

(2)被 调 用 的 函 数 必 须 是 虚 函 数 且 派 生 类 必 须 对 基 类 的 虚 函 数 进 行 重 写。

代 码 展 示

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
class Student : public Person 
{
public://虚函数的重写virtual void BuyTicket() { cout << "买票-半价" << endl; }/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

在这里插入图片描述


结 论

  1. 对 象 中 不 存 在 成 员 函 数,只 存 在 成 员 变 量。
  2. 多 态 调 用 看 的 是 指 向 的 对 象。普 通 对 象 看 当 前 调 用 对 象 的 类 型。
  3. 2 个 条 件 缺 一 不 可。派 生 类 发 生 了 切 片。

override 和 final

final:修 饰 虚 函 数,表 示 该 虚 函 数 不 能 再 被 重 写。final 添 加 在 基 类 后 面。

#include<iostream>
using namespace std;
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl;}
};
int main()
{Benz b;b.Drive();return 0;
}

在这里插入图片描述
override: 检 查 派 生 类 虚 函 数 是 否 重 写 了 基 类 某 个 虚 函 数,如 果 没 有 重 写 编 译 报 错。

#include<iostream>
using namespace std;
class Car 
{
public:virtual void Drive() {cout << "virtual void Drive() " << endl;}
};
class Benz :public Car 
{
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{Benz b;b.Drive();return 0;
}

在这里插入图片描述


重 载、覆 盖(重 写)、隐 藏(重 定 义)的 对 比

在这里插入图片描述


抽 象 类

多 态 的 原 理

虚 函 数 表

过 程
         编 译 器 在 编 译 时,为 每 个 有 虚 函 数 的 类 生 成 一 个 独 立 的 虚 函 数 表,这 是 一 个 静 态 数 组,存 的 是 函 数 指 针。
         基 类 有 自 己 的 vtable。
         派 生 类 也 有 自 己 的 vtable。


派 生 类 的 vtable 构 建 方 式

  1. 先 把 基 类 的 虚 函 数 列 表 内 容 “继 承” 过 来(不 是 运 行 时 拷 贝,而 是 编 译 时 直 接 生 成 新 的 表)。
  2. 如 果 派 生 类 重 写 了 某 个 虚 函 数,就 在 自 己 的 vtable 中 替 换 掉 对 应 位 置 的 函 数 指 针,指 向 新 的 实 现。
  3. 如 果 派 生 类 新 增 了 虚 函 数,就 会 把 这 些 新 函 数 的 指 针 追 加 到 vtable 末 尾。

         对 象 里 只 有 一 个 vptr(虚 表 指 针),指 向 所 属 类 的 vtable。

  1. 构 造 对 象 时,编 译 器 自 动 插 入 代 码,让 vptr 指 向 该 类 的 vtable。

         虚 函 数 放 在 代 码 段 里,虚 函 数 表 里 存 放 着 虚 函 数 的 地 址。

#include<iostream>
using namespace std;
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func1()" << endl;}     void Func3(){cout << "Func1()" << endl;}
private:char _b = 1;
};
int main()
{cout << sizeof(Base) << endl;Base b;return 0;
}

在这里插入图片描述

结 论

  1. 派 生 类 对 象 d 中 也 有 一 个 虚 表 指 针,d 对 象 由 两 部 分 构 成,一 部 分 是 父 类 继 承 下 来 的 成 员,虚 表 指 针 也 就 是 存 在 部 分 的 另 一 部 分 是 自 己 的 成 员。
  2. 基 类 b 对 象 和 派 生 类 d 对 象 虚 表 是 不 一 样 的,这 里 我 们 发 现 Func1 完 成 了 重 写,所 以 d 的 虚 表 中 存 的 是 重 写 的 Derive::Func1,所 以 虚 函 数 的 重 写 也 叫 作 覆 盖,覆 盖 就 是 指 虚 表 中 虚 函 数 的 覆 盖。重 写 是 语 法 的 叫 法,覆 盖 是 原 理 层 的 叫 法。
  3. 另 外 Func2 继 承 下 来 后 是 虚 函 数,所 以 放 进 了 虚 表,Func3 也 继 承 下 来 了,但 是 不 是 虚 函 数,所 以 不 会 放 进 虚 表。
  4. 虚 函 数 表 本 质 是 一 个 存 虚 函 数 指 针 的 指 针 数 组,一 般 情 况 这 个 数 组 最 后 面 放 了 一 个 nullptr。

在这里插入图片描述

  1. 派 生 类 的 虚 表 生 成
    (1)先 将 基 类 中 的 虚 表 内 容 拷 贝 一 份 到 派 生 类 虚 表 中 (2)如 果 派 生 类 重 写 了 基 类 中 某 个 虚 函 数,用 派 生 类 自 己 的 虚 函 数 覆 盖 虚 表 中 基 类 的 虚 函 数
    (3)派 生 类 自 己 新 增 加 的 虚 函 数 按 其 在 派 生 类 中 的 声 明 次 序 增 加 到 派 生 类 虚 表 的 最 后。
  2. 虚 表 存 储 在 代 码 段,虚 表 存 的 是 虚 函 数 指 针,不 是 虚 函 数,虚 函 数 和 普 通 函 数 一 样 的,都 是 存 在 代 码 段 的,只 是 他 的 指 针 又 存 到 了 虚 表 中。另 外 对 象 中 存 的 不 是 虚 表,存 的 是 虚 表 指 针。同 类 型 的 对 象 共 用 虚 表。

代 码 验 证 虚 表 存 在 代 码 段

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void Func1(){}virtual void Func2(){}
protected:int _a = 0;
};
class Student : public Person 
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
protected:int _b = 1;
};
int main()
{int a = 0;printf("栈:%p\n", &a);static int b = 0;printf("静态区:%p\n", &b);int* c = new int;printf("堆区:%p\n", &c);const char* d = "hello world";printf("常量区(代码段):%p\n", d);Person ps1;Student st1;printf("虚表1:%p\n", *((int*)&ps1));printf("虚表2:%p\n", *((int*)&st1));return 0;
}

在这里插入图片描述
找 到 隐 藏 的 Func3 函 数

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void Func1(){cout << "Person:Func1()" << endl;}virtual void Func2(){cout << "Person:Func2()" << endl;}
//protected:int _a = 0;
};
class Student : public Person 
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}virtual void func3(){cout << "Student:Func3()" << endl;}
protected:int _b = 1;
};
void Func(Person& p)
{p.BuyTicket();
}
//打印函数指针数组
typedef void(*FUNC_PIR) ();
//void PrintVFT(FUNC_PIR table[])
void PrintVFT(FUNC_PIR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PIR f = table[i];f();}printf("\n");
}
int main()
{Person ps;Student st;int vft1 = *((int*)&ps);//取出4个字节PrintVFT((FUNC_PIR*)vft1);int vft2 = *((int*)&st);PrintVFT((FUNC_PIR*)vft2);return 0;
}

在这里插入图片描述
         监 视 窗 口 没 有 显 示 Func3,经 过 内 存 查 找 得 到 了 Func3 的 地 址。


动 态 绑 定 与 静 态 绑 定

  1. 静 态 绑 定 又 称 为 前 期 绑 定 (早 绑 定),在 程 序 编 译 期 间 确 定 了 程 序 的 行 为,也 称 为 静 态 多 态,比 如:函 数 重 载
  2. 动 态 绑 定 又 称 后 期 绑 定(晚 绑 定),是 在 程 序 运 行 期 间,根 据 具 体 拿 到 的 类 型 确 定 程 序 的 具 体 行 为,调 用 具 体 的 函 数,也 称 为 动 态 多 态。

多 继 承 中 的 虚 函 数 表

#include<iostream>
using namespace std;
class Base1 
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << "虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf("第%d个虚函数地址:0X%x->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;Base1 b1;Base2 b2;cout << "sizeof(d):" << sizeof(d) << endl;int vft1 = *((int*)&d);PrintVTable((VFPTR*)vft1);//方法1//int vft2 = *((int*)((char*)&d + sizeof(Base1)));//方法2Base2* ptr = &d;int vft2 = *((int*)ptr);PrintVTable((VFPTR*)vft2);return 0;
}

在这里插入图片描述
在这里插入图片描述
         寄 存 器 ecx 存 储 的 是 this 指 针 的 地 址。Derive 和 Base1 的 地 址 的 开 始 重 叠。ptr1 的 this 指 针 指 向 的 是 Derive 对 象 的 开 始。ptr2 的 this 指 针 指 向 的 是 Base2 的 开 始,距 离 Base1 的 开 始 有 8 个 字 节。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


总 结

         多 态 的 核 心 是 “运 行 时 确 定 函 数 调 用”,依 赖 虚 表(vtable)与 虚 表 指 针(vptr):子 类 继 承 基 类 虚 表 后,重 写 函 数 覆 盖 表 项,通 过 vptr 实 现 动 态 绑 定。需 牢 记 析 构 函 数 设 虚 函 数、用 override/final 规 范 重 写 等 细 节。建 议 结 合 案 例 改 代 码、自 定 义 类 验 证 虚 表,通 过 实 践 掌 握 多 态,写 出 高 可 维 护 的 C++ 代 码。

http://www.dtcms.com/a/516217.html

相关文章:

  • 腾讯云网站建设教程企业名录app
  • 重庆做网站有哪些医疗网站建设
  • 语音识别技术之科大讯飞在线API
  • 从案例到实践:仓颉编程语言入门核心知识点全解析
  • VR环境中的概念
  • 闽侯县住房和城乡建设局官方网站猪八戒官网做网站专业吗
  • 十个app制作网站wordpress目录插件
  • PHP全电发票OFD生成实战
  • 利用DuckDB SQL求解集合数学题
  • 做新闻h5网站专业网站建设费用报价
  • 个人网站开发的环境海南省建设网站的公司电话号码
  • C++学习:C++11关于类型的处理
  • LayoutManager
  • 网站建设公司盈利分析网站建设需要哪些的ps
  • QML学习笔记(四十六)QML与C++交互:Q_PROPERTY宏映射
  • 培训学校 网站费用购物商城网站建设方案
  • 黑马商城day5-服务保护和分布式事务
  • 【实证分析】地市人才及资本创新要素流动数据集-含代码(2003-2023年)
  • 【学习系列】SAP RAP 16:RAP应用部署集成至Fiori Launchpad 【On-Premise】
  • 01-JavaScript基础
  • 万亿国债助力应急行业-多链路聚合通信路由在应急项目中的解决方案和技术需求
  • CSS3 超实用属性:pointer-events (可穿透图层的鼠标事件)
  • 企业做网站公司有哪些wordpress 积分支付
  • Java线程阻塞状态
  • 网站优化排名软件哪些最好99企业邮箱
  • dify之Web 前端工作流编排(Workflow Builder)
  • 环境变量进阶:本地变量、内建命令与全局属性的深度解析
  • 《图解技术体系》Wonderful talk AI ~~Google AI
  • 咸阳网站建设培训学校国外网站 国内访问速度
  • 建设一个网站的工作方案企业信息公开网查询