从 内 存 布 局 深 入 剖 析 继 承 与 多 态 匡 翠 芸 武 汉 科 技 大 学 计 算 机 学 院, 湖 北 武 汉 (430065) E-mail: kuciyun@126.com 摘 要 : 在 面 向 对 象 程 序 设 计 中, 使 派 生 类 继 承 基 类 的 虚 函 数, 再 定 义 一 个 指 向 派 生 类 对 象 的 基 类 指 针 可 以 轻 松 实 现 动 态 绑 定 程, 那 么 编 译 器 是 如 何 做 到 动 态 绑 定 以 及 基 类 的 析 构 函 数 为 什 么 要 申 明 为 虚 析 构 函 数 呢. 本 文 避 开 普 通 的 调 试 方 法, 通 过 精 巧 设 计 实 例, 安 插 指 针 来 对 面 向 对 象 的 类 的 继 承 与 多 态 的 研 究, 从 内 存 的 角 度 深 入 剖 析 了 具 有 虚 函 数 的 基 类 和 派 生 类 对 象 的 内 存 布 局 和 结 构 组 成, 分 析 动 态 绑 定 的 实 现 原 理, 从 而 在 编 写 程 序 的 时 候 出 现 不 必 要 的 错 误, 使 设 计 出 来 的 类 更 合 理, 避 免 类 拥 肿. 关 键 词 : 多 态 性 ; 继 承 性 ; 虚 函 数 ; 虚 函 数 表 ; 虚 拟 指 针 ; 动 态 联 编 ; 内 存 布 局 1. 引 言 在 C++ 面 向 对 象 程 序 设 计 中, 继 承 与 多 态 是 OOP 最 重 要 的 两 个 特 性 继 承 的 目 的 是 复 用, 继 承 复 用 包 括 两 方 面 的 复 用 : 抽 象 ( 接 口 ) 复 用 和 实 现 ( 过 程 ) 复 用 多 态 的 目 的 是 要 将 抽 象 复 用 及 实 现 复 用 剥 离 开 来 那 么 派 生 类 是 如 何 实 现 继 承 基 类 的 属 性 和 方 法, 虚 函 数 是 如 何 实 现 多 态? 庖 丁 解 牛 的 功 力 不 在 其 刀 的 锋 利, 而 在 对 牛 筋 骨 脉 络 的 理 解 一 般 的 研 究 方 法 是 通 过 [1] 调 试 从 汇 编 代 码 去 分 析, 但 有 其 不 足 : 一 是 不 懂 汇 编 的 人 无 法 深 入 了 解, 二 是 不 能 从 整 体 上 把 握 对 象 继 承 与 多 态 的 内 存 布 局 本 文 正 是 从 内 存 布 局 作 为 切 入 点, 精 心 设 计 C++ 代 码 来 从 整 体 上 深 入 剖 析 这 两 大 特 性, 使 OOP 人 员 对 继 承 与 多 态 的 筋 骨 脉 络 了 如 指 掌, 在 实 际 程 序 设 计 中 做 到 心 中 有 数, 有 的 放 矢 2. 设 计 C++ 代 码 分 析 内 存 布 局 2.1 虚 拟 指 针 的 存 在 Class A1{int a;}; Class A2{int a; Virtual void f1(){} Virtual f2(){} }; sizeof(a1) = 4; sizeof(a2) = 8;A1 的 字 节 数 为 4 在 我 们 的 意 料 之 中, 而 在 A2 中 我 们 增 加 了 两 个 虚 函 数, 这 时 的 字 节 数 却 为 8. 原 来 编 译 器 对 每 个 包 含 虚 函 数 的 类 创 建 一 个 虚 表 [2] ( 称 为 VTable), 此 VTable 由 类 的 所 有 对 象 所 共 用, 同 时 编 译 器 也 会 为 这 个 类 的 每 个 类 对 象 秘 密 地 嵌 入 一 个 变 量, 即 一 个 指 向 自 己 虚 表 的 指 针 称 为 虚 拟 指 针 (VPTR) 正 是 因 为 有 了 VPTR 的 存 在, 所 以 后 者 的 所 占 字 节 为 8. 2.2 设 计 实 例 由 上 面 的 分 析 可 知, 对 于 有 虚 函 数 的 类 在 实 例 化 (Instance) 时 都 有 一 个 VPTR, 那 么, 对 象, 虚 表 的 结 构 是 由 什 么 组 成, 结 构 如 何? 编 译 器 是 怎 么 实 现 动 态 联 编? 为 此 我 们 设 计 两 个 类 CBase 与 CDeri, 其 中 后 者 派 生 于 前 者, 为 了 看 清 楚 动 态 的 真 面 目, 我 特 意 在 CBase 中 定 义 两 个 虚 函 数, 其 中 只 有 一 个 在 在 派 生 类 中 重 写. 而 在 派 生 类 中 再 加 一 个 虚 函 数 用 来 分 析 VTable 的 情 况. 具 体 如 下 : class CBase{ public: CBase(){nBaseData = 10;} ~CBase(){} Void Function0(){cout<<"<2> Address="<<&nBaseData<<" CBase::nBaseData= "<<nbasedata<<endl;} - 1 -
virtual void VFunction1(){cout<<" CBase::VFunction1< >"<<endl;} virtual void VFunction2(){cout<<" CBase::VFunction2< >"<<endl;} private: int nbasedata; }; class CDeri : public CBase{ public: CDeri(){nDeriData = 20;} ~CDeri(){} void Function0(){cout<<"<3> Address= "<<&nderidata<<" CDeri::nDeriData= "<<nderidata<<endl;} virtual void VFunction2(){cout<<" CDeri::VFunction2< >"<<endl;} virtual void VFunction3(){cout<<" CDeri::VFunction3< >"<<endl;} private: int nderidata; }; 类 设 计 好 了. 要 清 楚 VTable 的 结 构, 可 以 通 过 获 取 VPTR, 因 为 VPTR 指 VTable, 依 上 面 的 分 析 知 道, 在 每 个 有 虚 函 数 的 对 象 中 编 译 器 都 会 放 个 一 个 VPTR,. 为 此 定 义 函 数 指 针 : typedef void (*pfun)(); pfun fun; 并 设 计 获 取 函 数 指 针 的 函 数 CallMemberFunction() 如 下 : void CallMemberFunction(unsigned long *pmemberfunction, const int &noffset) { fun = (pfun)((*(unsigned long *)(pmemberfunction + noffset))); cout<<"<"<<(noffset+1)<<"> "<<(pmemberfunction + noffset)<< --> <<fun; (*fun)(); } 同 时 为 了 明 晰 对 象 的 结 构, 为 此 设 计 函 数 SetMemberData() 用 来 设 置 成 员 变 量 的 值, 如 下 : void SetMemberData(unsigned long *pdatavalue, const int &noffset, const int &ndatavalue) { int *pdata = (int *)(pdatavalue + noffset); *pdata = ndatavalue; } 最 后 设 计 主 函 数, 在 其 中 先 实 例 化 基 类 对 象, 然 后 实 例 派 生 类 对 象, 目 的 在 于 对 比 基 类 与 派 生 类 在 内 存 中 的 布 局. 如 下 : int main(int argv, char * argc[ ]) { CBase *pbase = new CBase; unsigned long *VPTR1 = (unsigned long *)(pbase); cout<<"<1> Address= "<<VPTR1<<" Pointer of Virtl Fun"<<endl; unsigned long *pvmemfun1 = (unsigned long *)(*VPTR1); pbase->function0(); for (int j = 0; j < 2; j ++) CallMemberFunction(pVMemFun1, j); CBase *pderi = new CDeri; unsigned long *VPTR = (unsigned long *)(pderi); cout<<"<1> Address= "<<VPTR<<" Pointer of Virtl Fun"<<endl; unsigned long *pvmemfun = (unsigned long *)(*VPTR); SetMemberData(VPTR, 1, 40); SetMemberData(VPTR, 2, 80); pderi->function0(); ((CDeri *)pderi)->function0(); for (int i = 0; i < 3; i ++) CallMemberFunction(pVMemFun, i); delete pbase; delete pderi; return 0;} - 2 -
2.3. 深 入 分 析 (1) 在 主 函 数 中 先 定 义 基 类 的 对 象 指 针 pbase, 再 进 行 强 制 类 型 转 化 为 (unsigned long *) 目 的 是 取 VPTR, 因 为 VPTR 指 向 VTable, 所 以 (*VPTR) 就 是 VTable 的 首 地 址 [3], 然 后 取 地 址 加 偏 移 量 强 制 转 化 为 函 数 指 针 类 型, 从 下 面 图 1 输 出 内 存 结 果 可 知, 在 对 象 中 首 地 址 中 存 放 的 是 VPTR( 地 址 :00A91BD8), 后 紧 跟 基 类 的 成 员 变 量 ( 地 址 :00A91BDC). 而 在 VTable 中, 按 声 明 的 先 后 次 序 存 放 的 是 基 类 的 虚 函 数 地 址, 由 此 可 画 出 对 应 在 内 存 的 分 布 如 : 图 2. 图 1: 基 类 的 内 存 结 果 图 2: 基 类 的 对 应 内 存 结 构 (2) 然 后 在 主 函 数 中 定 义 派 生 类 的 对 象 指 针 pderi, 同 样 对 其 进 行 强 制 类 型 转 换 从 而 得 到 VPTR, 再 调 用 SetMemberData() 去 修 改 基 类 与 派 生 类 的 私 有 成 员 变 量, 调 用 CallMemberFunction() 去 调 用 所 有 的 虚 函 数, 这 个 函 数 先 输 出 VTable 中 每 一 项 表 的 地 址 及 所 存 放 的 函 数 地 址. 从 下 图 3 输 出 内 存 结 果 可 知, 在 对 象 中 首 地 址 同 样 是 VPTR( 地 址 : 00A90930), 其 后 是 基 类 的 成 员 变 量 ( 地 址 : 00A90934), 然 后 才 是 派 生 类 成 员 变 量 ( 地 址 : 00A90938). 对 于 VTable 而 言, 在 VTable 的 第 一 个 地 址 中 所 指 向 的 函 数 地 址 没 有 变 ( 地 址 : 004011E5), 那 是 因 为 在 派 生 类 中 没 有 重 写 VFunction1, 而 在 派 生 类 中 重 写 了 VFunction2 且 在 第 二 个 地 址 所 指 向 的 VFunction 的 地 址 改 变 了 ( 地 址 : 0040119A 变 为 地 址 : 004010A0), 第 三 个 是 派 生 类 新 加 的 虚 函 数 地 址. 由 此 知 在 VTable 中, 放 置 了 这 个 类 中 或 是 它 的 基 类 中 所 有 虚 函 数 的 地 址, 这 些 虚 函 数 的 顺 序 都 是 一 样 的, 每 个 虚 函 数 都 在 VTable 中 占 了 了 个 表 项, 保 存 着 一 条 跳 转 到 它 的 入 口 地 址 的 指 令, 所 以 通 过 偏 移 量 可 以 容 易 地 找 到 所 需 的 函 数 体 的 地 址 如 果 派 生 类 没 有 重 写 虚 函 数, 那 么 VTable 中 保 留 基 类 虚 函 数 的 地 址, 如 果 重 写 了 相 应 的 虚 函 数, 那 么 VTable 中 的 地 址 就 会 改 变 成 指 向 派 生 类 的 虚 函 数 地 址 如 果 派 生 类 有 自 己 的 虚 函 数, 那 么 VTable 中 就 会 添 加 该 项 正 是 由 于 第 二 个 地 址 里 面 的 内 容 改 变, 从 而 实 现 动 态 联 编 技 术. 调 用 虚 函 数 的 时 候, 它 先 根 据 Vtable 找 到 入 口 地 址 再 执 行, 从 而 实 现 了 动 态 联 编. 于 是 可 画 出 对 应 内 存 分 布 如 : 图 4. 图 3: 派 生 类 的 内 存 结 果 图 4: 派 生 类 对 应 内 存 结 构 - 3 -
3. 结 论 (1) 如 果 该 类 没 有 虚 拟 函 数, 则 不 存 在 虚 拟 函 数 表. 当 一 个 类 含 有 虚 函 数 时, 编 译 器 就 会 为 这 个 类 生 成 一 个 VTable, 编 译 器 另 外 还 为 每 个 类 的 对 象 提 供 了 一 个 虚 拟 指 针 (VPTR), 这 个 指 针 指 向 了 对 象 所 属 类 的 虚 表, 并 且 这 个 VPTR 在 对 象 的 头 部. (2) 对 象 ( 含 虚 函 数 的 类 ) 是 由 VPTR 和 成 员 变 量 组 成, 无 论 有 多 少 个 虚 函 数, 只 有 一 个 VPTR, 与 成 员 函 数 ( 如 :Function0) 无 关, 因 此 对 象 的 大 小 由 一 个 指 针 的 字 节 大 小 加 上 成 员 变 量 所 占 字 节 的 大 小 之 和, 有 时 候 为 了 存 取 的 效 率, 还 要 加 上 字 节 对 齐 (Align). (3) 虚 函 数 表 (VTable) 的 结 构 实 质 是 数 组,( 如 在 基 类 中 的 地 址 为 : 0047016C, 00470170) 在 派 生 类 中 地 址 为 004701DC, 004701E0, 004701E4) 而 这 个 数 组 中 所 存 储 的 是 指 向 函 数 地 址 的 指 针, 即 VTable 的 结 构 是 函 数 指 针 数 组 的 结 构 [4]. 因 此 只 要 取 到 VPTR, 然 后 采 用 偏 移 量 很 容 易 实 现 函 数 的 调 用. (4) 在 派 生 类 中, 如 果 没 有 重 写 (Override) 基 类 的 虚 函 数, 那 么 在 VTable 中 保 留 基 类 虚 函 数 的 地 址 ( 如 :VFunction1), 如 果 派 生 类 重 写 了, 则 在 VTable 中 把 基 类 对 应 虚 函 数 地 址 用 派 生 类 虚 函 数 地 址 覆 盖 ( 如 :VFunction2). 如 果 在 派 生 类 中 加 入 新 的 虚 函 数, 则 在 VTable 中 后 面 加 入 对 应 虚 函 数 的 地 址 ( 如 :VFunciton3). 即 :VTable 按 照 类 中 虚 函 数 声 明 的 顺 序 一 一 填 入 函 数 地 址, 派 生 类 会 继 承 基 类 的 VTable( 当 然 还 会 有 其 它 可 继 承 的 成 员 ), 当 在 派 生 类 中 修 改 虚 函 数 时, 同 时 派 生 类 中 虚 表 中 的 内 容 也 随 之 被 修 改, 表 中 相 应 的 元 素 已 经 不 是 基 类 的 函 数 地 址, 而 是 派 生 类 的 函 数 地 址, 虚 表 可 以 继 承, 如 果 派 生 类 没 有 重 写 虚 函 数, 那 么 基 类 虚 表 中 仍 然 会 有 该 函 数 的 地 址, 只 不 过 这 个 地 址 指 向 的 是 基 类 的 虚 函 数 实 现. (5) 在 对 象 或 者 在 VTable 的 结 构 中, 基 类 的 组 成 在 前, 派 生 类 在 后 ( 如 : 在 pderi 对 象 中 :nbasedata 在 前,nDeriData 在 后, 在 VTable 中,VFunction1,VFunction2 在 前, 而 VFunction3 在 后 ) 而 在 每 个 类 中 都 是 按 声 明 的 先 后 次 序 分 配 内 存. (6) 从 上 面 的 对 象 结 构 分 析 可 知, 普 通 成 员 函 数 在 对 象 中 不 占 内 存, 调 用 时 简 单 地 跳 转 到 一 个 固 定 地 址 ( 编 译 时 分 配 好 ); 同 样 静 态 成 员 函 数 ( 这 里 没 有 举 例 ) 也 不 占, 因 为 静 态 成 员 函 数 只 属 于 类, 为 所 有 对 象 共 用. 不 属 于 某 个 特 定 的 对 象. (7) 本 例 中 利 用 SetMemberData() 函 数 实 现 了 类 体 外 对 私 有 变 量 值 的 修 改, 这 也 提 供 了 一 个 修 改 私 有 变 量 的 方 法, 访 问 效 率 较 高, 尤 其 对 一 些 经 过 多 重 或 多 层 继 承 得 到 的 类 对 象 的 数 据 成 访 问, 使 用 该 方 法 需 要 对 类 成 员 的 布 局 十 分 清 楚 ( 尤 其 是 有 字 节 对 齐 时 ), 否 则 很 容 易 产 生 非 法 操 作, 甚 至 导 致 系 统 崩 溃, 这 种 方 法 也 说 明 了 类 封 装 性 是 不 完 全 可 靠, 从 此 也 可 以 得 出, 基 类 的 成 员 变 量, 派 生 类 是 可 以 继 承 的, 只 是 在 派 生 类 中 的 成 员 函 数 跟 对 象 都 不 能 访 问 而 已 ( 编 译 器 不 能 通 过 ) 4. 高 效 应 用 毫 无 疑 问, 有 了 继 承 性, 使 得 代 码 重 复 利 用 率 很 高, 明 显 可 以 缩 短 开 发 周 期 但 由 上 的 分 析 知 道, 派 生 类 的 对 象 中 会 继 承 基 类 中 的 所 有 成 员 变 量, 包 括 私 有 成 员 变 量, 这 样 会 带 来 空 间 开 销 同 样 有 了 虚 函 数 使 类 的 设 计 更 灵 活 也 更 强 大, 同 样 时 间 和 空 间 的 开 销 不 可 避 免 也 大 了 ( 不 是 直 接 跳 转 到 编 译 时 分 配 的 固 定 地 址, 而 是 要 先 得 到 VPTR, 然 后 经 过 偏 移 量 取 得 虚 函 数 地 址, 然 后 再 调 用 ). 所 以 在 设 计 类 的 时 候 除 非 十 分 明 确 以 后 这 个 类 会 被 继 承 且 里 面 的 函 数 会 被 改 写, 才 加 关 键 字 Virtual 因 为 如 果 继 承 的 层 次 比 较 深, 那 么 所 有 直 接 和 间 接 基 类 的 数 据 成 员, 虚 函 数 都 会 在 派 生 类 的 对 象 中, 虽 然 在 对 象 中 无 论 多 少 个 VF, 只 增 加 一 个 VPTR, - 4 -
但 VTable 会 变 得 很 庞 大 所 以 在 实 际 OOP 中, 我 们 就 可 以 做 到 心 中 有 数, 有 的 放 矢 5. 结 束 语 (1) 以 上 结 论 的 运 行 环 境 : 系 统 为 Windows XP 编 译 器 为 Visual Studio C++ 6.0. 对 于 其 他 的 编 译 器, 具 体 实 现 并 不 完 全 相 同, 但 都 大 同 小 异.( 如 Linux 平 台 上 的 GNU C++ 编 译 器 就 把 指 向 VTable 的 虚 拟 指 针 (VPTR) 放 在 对 象 尾 部 而 不 是 头 部, 而 且 VTable 中 仅 仅 存 放 虚 函 数 的 入 口 地 址, 而 不 是 跳 转 到 虚 函 数 的 指 令. (2) 以 上 征 对 仅 仅 是 简 单 的 单 继 承 的 情 况, 对 于 横 向 ( 如 : 多 继 承 ), 纵 向 ( 如 虚 继 承, 更 深 层 次 继 承 ) 等 更 复 杂 的 情 况, 由 于 篇 幅 所 限 在 此 不 加 以 深 入, 但 剖 析 思 想 大 同 小 异. 参 考 文 献 [1] 蓝 雯 飞.C++ 语 言 的 多 态 性 应 用 研 究 [J] 计 算 机 时 代 1998(19)6 [2] 候 捷 译. 深 入 探 索 C++ 对 象 模 型 [M] 武 汉 : 华 中 科 技 大 学 出 版 社 [3] 张 宇. 淺 谈 多 态 中 的 虚 拟 指 针 (VPTR)[J] 武 汉 市 教 育 科 学 研 究 院 学 报 2006.11.4 [4] 彭 建 盛. 多 态 性 在 C++ 面 向 对 象 程 序 设 计 中 的 实 现 [J] 河 池 学 院 学 报 2004(20)4 Inside Analysis the Inheritance and Polymorophism from the Memory Layout KUANG Cui-Yun College of Computer Science and Technology WuHan University of Science and Technology WuHan (430065) Abstract In OOP, the derived class inherited the basic class that have a virtual function, and then define a basic pointer that point to the object of derive class can easily achieve dynamic binding.the compiler is how to achieve dynamic binding and why the destructed function of the base class must defined used by virtual keyword. The paper avoid this debugging method, research the inheritance and polymorophism of Object-Orient programming through the exquisitely designing example and insert the pointer, Thorough analysis the memoy layout and composition of object and Virtual Function Table, the analysis the principle of dynamic binding, avoid makeing the unnecessary mistakes and a lots of inderited class,can designing reasonable class while programming. Keywords: Polymorphism; Inheritance Virtual Function; Virtual Function Table; Virtual Pointer; Dynamic Binding; Memory Layout - 5 -