第 9 章 派 生 类
这 一 章 解 释 如 何 用 派 生 类 产 生 可 扩 展 的 程 序 。本 章 包 含 如 下 一 些 主 题 :
-
派 生 类 综 述
-
多 重 基 类
-
虚 拟 函 数
-
抽 象 类
-
范 围 规 则 总 结
派 生 类 概 述
利 用 继 承 机 制 , 新 的 类 可 以 从 已 有 的 类 中 派 生 ( 有 关 继 承 见 下 一 节 “ 单 一 继 承 ” 的 开 始 ) 。 那 些 用 于 派 生 的 类 称 为 这 些 特 别 派 生 出 的 类 的 “ 基 类 ”。 派 生 类 的 说明 可 以 用 下 面 的 语 法 。
语 法
基 类 说 明 :
: 基 类 表基 类 表 :
基 类 说 明 符
基 类 表 , 基 类 说 明 符基 类 说 明 符 :
完 全 类 名 称
virtual 访 问 说 明 符 opt 完 全 类 名 称
访 问 指 示 符 virtual opt 完 全 类 名 称访 问 指 示 符 :
private
protected
public
单 一 继 承
在 “ 单 一 继 承 ” 这 种 最 普 通 的 形 式 中 , 派 生 类 仅 有 一 个 基 类 , 考 虑 如 图 9.1 所 示的 关 系 。
图 9.1 简 单 单 一 继 承 图
注 意 图 9.1 中 的 从 一 般 到 特 殊 的 过 程 。 在 类 的 层 次 设 计 中 , 可 以 发 现 一 些 普 遍 的特 性 , 即 派 生 类 总 是 同 基 类 有 “ kind of ” 关 系 。 在 图 9.1 中 书 是 一 种 印 刷 好 的文 档 而 一 本 平 装 书 是 一 种 书 。
在 图 9.1 中 的 另 一 个 值 得 注 意 点 是 Book 既 是 派 生 类 ( 从 PrintedDo c ument 中 派生 ), 也 是 基 类 (PaperbackBook 是 从 Book 派 生 的 ) 。 下 面 的 例 子 是 这 种 类 层 次 的一 个 轮 廓 性 的 说 明 。
class PrintedDocument
{
// 成 员 表
};
//Book 是 从 PrintedDo c ument 中 派 生 的
class Book:public PrintedDocument
{
// 成 员 表
};
//PaperbackBook 是 从 Book 中 派 生class PaperbackBook: public Book
{
// 成 员 表
};
PrintedDocument 作 为 Book 的 直 接 基 类 , 它 同 时 也 是 PaperbackBook 的 非 直 接基 类 。 直 接 基 类 和 非 直 接 基 类 的 区 别 在 于 直 接 基 类 出 现 在 类 说 明 的 基 类 表 中 , 而非 直 接 基 类 不 出 现 在 基 类 表 中 。
每 个 派 生 类 的 说 明 是 在 基 类 的 说 明 之 后 说 明 的 , 因 此 对 于 基 类 仅 只 给 出 一 个 前向 引 用 的 说 明 是 不 够 的 , 必 须 是 完 全 的 说 明 。
在 前 面 的 例 子 中 , 使 用 的 访 问 说 明 符 是 public 。 公 有 继 承 、 私 有 继 承 以 及 保 护的 继 承 在 第 10 章 “ 成 员 访 问 控 制 ” 中 讲 述 。
- 个 类 可 以 作 为 很 多 特 别 类 的 基 类 , 如 图 9.2 所 示 。
图 9.2 有 向 无 环 图 的 例 子
在 图 9.2 中 的 图 叫 “ 有 向 无 环 图 ” (DAG) 。 有 一 些 类 是 多 个 派 生 类 的 基 类 。 但 反过 来 不 是 真 的 : 对 于 任 意 给 的 派 生 类 仅 有 一 个 直 接 基 类 。 图 9.2 描 绘 了 单 一 继 承的 结 构 。
注 意 : 有 向 无 环 图 并 不 仅 用 于 单 一 继 承 。 它 们 也 可 以 用 于 多 重 继 承 图 。 这 一 主 题将 在 下 一 节 中 的 “ 多 重 继 承 ” 中 论 述 。
在 继 承 中 , 派 生 类 含 有 基 类 的 成 员 加 上 任 何 你 新 增 的 成 员 。 结 果 派 生 类 可 以 引 用基 类 的 成 员 ( 除 非 这 些 成 员 在 派 生 类 中 重 定 义 了 ) 。 当 在 派 生 类 中 重 定 义 直 接 基类 或 间 接 基 类 的 成 员 时 , 可 以 使 用 范 围 分 辨 符 (::) 引 用 这 些 成 员 。 考 虑 下 面 的 代码 :
class Document
{
public:
char * Name;// 文 档 名 称
// 实 现 类 Document 的 PrintNameOf 函 数void Document::PrintNameOf()
{
cout << Name << end ;
}
class Book:public Document
{
public:
Book(char *name, long pagecount); private:
long PageCount;
};
//class Book 构 造 函 数
Book::Book (char *name, long pagecount)
{
Name=mew char [strlen(name)+1];
strcpy (Name,name);
注 意 , Book 的 构 造 函 数 (Book::Book) 具 有 对 数 据 成 员 Name 的 访 问 权 。 在 程 序中 可 以 按 如 下 方 式 创 建 Book 类 对 象 并 使 用 之 。
// 创 建 一 个 Book 类 的 新 对 象 , 这 将 激 活 构 造 函 数 Book:Book Book LibraryBook ("Programming Windows,2nd Ed",994);
...
// 使 用 从 Document 中 继 承 的 函 数 PrintNameOf. LibraryBook.PrintNameOf();
如 前 面 例 子 所 示 , 类 成 员 和 继 承 的 数 据 与 函 数 以 一 致 的 方 式 引 用 。 如 果 类 Book 所 调 用 的 PrintNameOf 是 由 类 Book 重 新 定 义 实 现 的 , 则 原 来 属 于 类 Document 的PrintNameOf 函 数 只 能 用 范 围 分 辩 符 (::) 才 能 使 用 :
class Book:public Document
{
Book(char *name,long pagecount);
void PrintNameOf();
long PageCount;
};
void Book::PrintNameOf()
{
cout<<"Name of Book:";
Document::PrintNameOf();
}
只 要 有 一 个 可 访 问 的 、 无 二 义 性 的 基 类 , 派 生 类 的 指 针 和 引 用 可 以 隐 含 地 转 换 为它 们 基 类 的 指 针 和 引 用 。 下 面 的 例 子 证 实 了 这 种 使 用 指 针 的 概 念 ( 同 样 也 适 用 于引 用 ):
#include <iostream.h>
void main()
{
Document * DocLib[10]; //10 个 文 档 的 库
for (int i=0; i<10; ++i)
{
cout<<"Type of document:"
<<"P)aperback,M)agazine,H)elp File,C)BT"
<< endl;
char CDocType;
cin >>CDocType;
switch(tolower(CDocType))
{
case 'p':
DocLib[i]=new PaperbackBook;
break;
case 'm':
DocLib[i]=new Magazine;
break;
case 'h':
DocLib[i]=new HelpFile;
break;
case 'c':
DocLib[i]=new ComputerBasedTraining;
break;
default:
--i;
break;
}
}
for (i=0; i<10; ++i)
DocLib[i]->PrintNameOf();
}
在 前 面 例 子 的 SWITCH 语 句 中 , 创 建 了 不 同 类 型 的 对 象 。 这 一 点 依 赖 于 用 户 对CDocType 对 象 所 作 出 的 说 明 。 然 而 这 些 类 型 都 是 从 类 Document 中 派 生 出 来 的 , 故 可 以 隐 含 地 转 换 为 Document* 。 结 果 是 DocLib 成 为 一 个 “ 相 似 链 表 ” (heterogeneous list) 。 此 链 表 所 包 含 的 是 不 同 种 类 的 对 象 , 其 中 的 所 有 对 象 并不 是 有 相 同 的 类 型 。
因 为 Document 类 有 一 个 PrintNameOf 函 数 。 因 此 它 能 够 打 印 图 书 馆 中 每 本 书 的名 称 , 但 对 于 Document 类 型 来 说 有 一 些 信 息 会 省 略 掉 了 ( 如 :Book 的 总 页数 ,HelpFile 的 字 节 数 等 ) 。
注 意 : 强 制 基 类 去 实 现 一 个 如 PrintNameOf 的 函 数 , 通 常 不 是 一 个 很 好 的 设 计 , 本章 后 面 的 “ 虚 拟 函 数 ” 中 提 供 了 一 个 可 替 换 的 设 计 方 法 。
多 重 继 承
C++ 的 后 期 的 一 些 版 本 为 继 承 引 入 了“ 多 重 继 承 ” 模 式 。 在 一 个 多 重 继 承 的 图 中 , 派 生 类 可 以 有 多 个 直 接 基 类 。 考 虑 图 9.3 。
图 9.3 简 单 多 重 继 承 图
如 图 9.3 所 示 的 图 中 , 显 示 了 一 个 CollectibleString 类 。该 类 既 像 Collectible 类 ( 一 种 可 包 容 聚 集 的 类 ), 又 像 String 类 。 对 于 派 生 类 需 要 多 个 基 类 的 属 性 的问 题 , 多 重 继 承 是 一 种 很 好 的 解 决 办 法 。 因 而 也 很 容 易 派 生 出CollectibleCustomer 和 CollectibleWindow 等 等 。
对 于 一 个 特 定 的 程 序 如 果 每 个 类 的 属 性 并 不 是 全 部 要 求 使 用 , 则 每 个 类 可 以 单 独使 用 或 者 同 别 的 类 联 合 在 一 起 使 用 。 因 此 把 图 9.3 所 描 绘 的 类 层 次 作 为 基 础 , 用户 很 容 易 组 织 出 不 可 收 集 的 字 符 串 或 可 收 集 的 非 字 符 串 。 对 于 使 用 单 一 继 承 , 则没 有 这 种 便 利 性 。
虚 基 类 层 次
有 一 些 类 层 次 很 庞 大 , 但 有 很 多 东 西 很 普 遍 。 这 些 普 遍 的 代 码 在 基 类 中 实 现 了 , 然 而 在 派 生 类 中 又 实 现 了 特 殊 的 代 码 。
对 于 基 类 来 说 重 要 的 是 建 立 一 种 机 制 , 通 过 这 种 机 制 派 生 类 能 够 完 成 大 量 的 函 数机 能 。
这 种 机 制 通 常 是 用 虚 函 数 来 实 现 的 。 有 时 , 基 类 为 这 些 函 数 提 供 了 一 个 缺 省 的 实现 。 如 在 图 9.2 的 Document 类 层 次 中 , 两 个 重 要 的 函 数 是 Identify 和 WhereIs 。当 调 用 Identify 函 数 时 , 返 回 一 个 正 确 的 标 识 。 对 于 各 种 文 档 来 说 正 确 的 是 : 对于 Book, 调 用 如 doc->Identify() 的 函 数 必 须 返 回 ISBN 编 号 ; 而 对 于 一 个HelpFile 返 回 产 品 名 和 版 本 号 更 合 理 一 些 。 同 样 ,WhereIs 函 数 对 于 一 本 书 来 说应 该 返 回 行 和 书 架 号 , 但 对 于 HelpFile 就 应 该 返 回 它 的 磁 盘 位 置 , 也 许 是 一 个 目录 和 名 称 。
了 解 到 所 有 的 Identify 和 WhereIs 的 函 数 实 现 返 回 的 是 同 种 类 型 的 信 息 , 这 一点 很 重 要 。
在 这 个 例 子 中 , 恰 好 是 一 种 描 述 性 字 符 串 。
这 些 函 数 可 以 作 为 虚 拟 函 数 来 实 现 , 然 后 用 指 向 基 类 的 指 针 来 调 用 , 对 于 实 际 代码 的 联 结 将 在 运 行 时 决 定 , 以 选 择 正 确 的 Identify 和 WhereIs 函 数 。
类 协 议 的 实 现
类 可 以 实 现 为 要 强 制 使 用 某 些 协 议 。 这 些 类 称 为 “ 抽 象 类 ” , 因 为 不 能 为 这 种 类类 型 创 建 对 象 。 它 们 仅 仅 是 为 了 派 生 别 的 类 而 存 在 。
当 一 个 类 中 含 有 纯 虚 拟 函 数 或 当 他 们 继 承 了 某 些 纯 虚 拟 函 数 却 又 没 有 为 它 们 提供 一 个 实 现 时 , 该 类 称 为 抽 象 类 。 纯 虚 拟 函 数 是 用 纯 说 明 符 定 义 的 虚 拟 函 数 。 如下 :
virtual char *Identify()=0;
基 类 Document 把 如 下 一 些 协 议 强 加 给 派 生 类 。
-
为 Identify 函 数 提 供 一 个 合 适 的 实 现
-
为 WhereIs 函 数 提 供 一 个 合 适 的 实 现
在 设 计 Document 类 时 , 通 过 说 明 这 种 协 议 , 类 设 计 者 可 以 确 保 如 不 提 供 Identify 和 WhereIs 函 数 则 不 能 实 现 非 抽 象 类 。 因 而 Document 类 含 有 如 下 说 明 :
class Document
{
public:
...
// 对 派 生 类 的 要 求 , 它 们 必 须 实 现 下 面 这 些 函 数
virtual char *Identify()=0;
virtual char *WhereIs()=0;
...
};
基 类
如 前 面 讨 论 的 , 继 承 过 程 创 建 的 新 的 派 生 类 是 由 基 类 的 成 员 加 上 由 派 生 类 新 加 的成 员 组 成 。 在 多 重 继 承 中 , 可 以 构 造 层 次 图 , 其 中 同 一 基 类 可 以 是 多 个 派 生 类 的一 部 分 。 图 9.4 显 示 了 这 种 图 。
图 9.4 单 个 基 类 的 多 重 实 例
在 图 9.4 中 以 图 的 形 象 表 达 了 CollectibleString 和 CollectibleSortable 的组 成 。 然 而 , 基 类 Collectible 通 过 路 径 CollectibleSortable 以 及CollectibleString 到 达 类 CollectibleSortableString 。 为 了 消 除 这 种 冗 余 , 当 这 些 类 被 继 承 时 , 可 以 说 明 为 虚 拟 基 类 。
有 关 说 明 虚 拟 基 类 以 及 带 有 虚 拟 基 类 的 对 象 是 如 何 组 成 的 , 见 本 章 后 面 的 “ 虚 拟
基 类 ”。
多 重 基 类
如 同 多 重 继 承 中 所 描 述 的 , 一 个 类 可 以 从 多 个 基 类 中 派 生 出 来 。 在 派 生 类 由 多 个基 类 派 生 出 来 的 多 重 继 承 模 式 中 , 基 类 是 用 基 类 表 语 法 成 份 来 说 明 的 ( 见 本 章 开始 的“ 概 述 ” 中 的 语 法 ) 。例如 :CollectionOfBook 类 是 由 类 Collection 和 Book 类 派 生 的 , 可 按 如 下 进 行 说 明 :
class CollectionOfBook:public Book,public Collection
{
// 新 成 员
};
基 类 的 说 明 顺 序 一 般 没 有 重 要 的 意 义 , 除 非 在 某 些 情 况 下 要 调 用 构 造 函 数 和 析 构函 数 的 时 候 。 在 这 些 情 况 下 , 基 类 的 说 明 顺 序 会 对 下 面 所 列 的 有 影 响 。
-
由 构 造 函 数 引 起 的 初 始 化 发 生 的 顺 序 。 如 果 你 的 代 码 依 赖 于CollectionOfBook 的 Book 部 分 要 在 Collection 部 分 之 前 初 始 化 , 则 此 说 明 顺 序 将 很 重 要 。 初 始 化 是 按 基 类 表 中 的 说 明 顺 序 进 行 初 始化 的 。
-
激 活 析 构 函 数 以 作 清 除 工 作 的 顺 序 。 同 样 , 当 类 的 其 它 部 分 正 在 被清 除 时 , 如 果 某 些 特 别 部 分 要 保 留 , 则 该 顺 序 也 很 重 要 。 析 构 函 数 的调 用 是 按 基 类 表 说 明 顺 序 的 反 向 进 行 调 用 的 。
注 意 : 基 类 的 说 明 顺 序 会 影 响 类 的 存 储 器 分 布 。 不 要 对 基 类 成 员 在 存 储 器 中 的 顺
序 作 出 任 何 编 程 的 决 定 。
在 你 说 明 基 类 表 时 , 不 能 把 同 一 类 名 称 说 明 多 次 。 但 是 对 于 一 个 派 生 类 而 言 , 其非 直 接 基 类 可 以 有 多 个 相 同 的 。
虚 拟 基 类
因 为 一 个 类 可 以 多 次 作 为 一 个 派 生 类 的 非 直 接 基 类 。 C++ 提 供 了 一 个 办 法 去 优 化这 种 基 类 的 工 作 。 研 究 图 9.5 中 的 类 层 次 , 它 显 示 了 一 个 模 拟 的 午 餐 线 。
图 9.5 模 拟 午 餐 线 图
在 图 9.5 中 ,Queue 是 CashierQueue 和 LunchQueue 的 基 类 。 但 是 当 这 两 个 类 联合 在 一 起 形 成 LunchCashierQueue 时 , 下 面 的 问 题 就 产 生 了 : 新 的 类 包 含 有 两 个Queue 类 型 的 子 对 象 , 一 个 来 自 于 CachierQueue, 另 一 个 来 自 于 LunchQueue 。 图
9.6 给 出 了 一 个 概 念 上 的 存 储 器 分 布 ( 实 际 的 内 容 公 布 可 能 会 进 行 优 化 ) 。
图 9.6 模 拟 的 午 餐 线 对 象
注 意 ,在 LunchCashierQueue 对 象 中 , 有 两 个 Queue 子 对 象 。下 面 的 代 码 说 明 Queue 为 虚 拟 基 类 :
class Queue
{
// 成 员 表
};
class CashierQueue:virtual public Queue
{
// 成 员 表
};
class LunchQueue: virtual public Queue
{
// 成 员 表
};
class LunchCashierQueue:public LunchQueue, public CashierQueue
{
// 成 员 表
};
关 键 字 virtual 确 保 了 仅 有 一 个 Queue 对 象 的 拷 贝 ( 见 图 9.7) 。
图 9.7 带 有 虚 拟 基 类 的 模 拟 午 餐 线 对 象
- 个 类 对 于 给 定 的 类 型 既 可 以 有 虚 拟 的 组 成 部 分 , 也 可 以 有 非 虚 拟 的 组 成 部 分 。这 种 情 况 发 生 在 图 9.8 所 示 的 情 况 下 。
图 9.8 同 一 个 类 的 虚 拟 的 和 非 虚 拟 的 组 成 部 分
在 图 9.8 中 ,CachierQueue 和 LunchQueue 用 Queue 作 为 虚 拟 基 类 。 但 是TakeoutQueue 仅 说 明 Queue 为 基 类 , 并 不 是 虚 拟 基 类 。 因 此LunchTakeoutCashierQueue 有 两 个 Queue 子 对 象 :
- 个 是 从 包 括 LunchCashierQueue 的 路 径 中 继 承 而 来 的 , 另 一 个 则 是 从 包 括TakeoutQueue 的 路 径 中 而 来 , 图 9.9 显 示 了 这 一 点 。
图 9.9 带 有 虚 拟 和 非 虚 拟 继 承 的 对 象 的 分 布
注 意 : 虚 拟 继 承 同 非 虚 拟 继 承 相 比 具 有 大 小 上 的 好 处 , 然 而 它 也 引 入 了 额 外 的 运行 开 销 。
如 果 一 个 派 生 类 重 载 了 一 个 从 虚 拟 基 类 中 继 承 的 虚 拟 函 数 , 而 且 该 派 生 类 以 指 向虚 拟 基 类 的 指 针 调 用 这 些 构 造 函 数 和 析 构 函 数 时 , 编 译 器 会 引 入 一 个 附 加 的 隐 含的 “ vtordisp ” 域 到 带 有 虚 拟 基 类 的 类 中 。 /vd0 编 译 器 选 项 禁 止 了 这 个 增 加 的隐 含 vtordisp 构 造 / 析 构 位 置 成 员 。 /vd1 选 项 ( 缺 省 ), 使 得 在 需 要 时 可 以 解 除禁 止 。 只 有 在 你 确 信 所 有 类 的 构 造 函 数 或 析 构 函 数 都 虚 拟 地 调 用 了 虚 拟 函数 ,vtordisp 才 可 以 关 掉 。
/vd 编 译 器 选 项 会 影 响 全 局 编 译 模 式 。 使 用 vtordisp 编 译 指 示 可 以 在 基 于 类 方
式 上 打 开 或 禁 止 vtordisp 域 : #pragma vtordisp(off)
class GetReal:virtual public{...}; # pragma vtordisp(on)
名 称 的 二 义 性
多 重 继 承 使 得 从 不 同 的 路 径 继 承 成 员 名 称 成 为 可 能 。 沿 着 这 些 路 径 的 成 员 名 称并 不 必 然 是 唯 一 的 。 这 些 名 称 的 冲 突 称 为 “ 二 义 性 ”。
任 何 引 用 类 成 员 的 表 达 式 必 须 使 用 一 个 无 二 义 性 的 引 用 。 下 面 的 例 子 显 示 了 二义 性 是 如 何 发 生 的 。
// 说 明 两 个 基 类 A 和 B class A
{
public:
unsigned a;
unsigned b();
};
class B
{
public:
unsigned a(); // 注 意 类 A 也 有 一 个 成 员 "a" 和 一 个 成 员 "b"
int b();
char c;
};
// 定 义 从 类 A 和 类 B 中 派 生 出 的 类 C
class C : public A, public B
{
};
按 上 面 所 给 出 的 类 说 明 , 如 下 的 代 码 就 会 引 出 二 义 性 , 因 为 不 清 楚 是 引 用 类 A 的b 呢 , 还 是 引 用 类 B 的 b:
C *pc=new C; pc->b();
考 虑 一 下 上 面 的 代 码 , 因 为 名 称 a 既 是 类 A 又 是 类 B 的 成 员 , 因 而 编 译 器 并 不 能区 分 到 底 调 用 哪 一 个 a 所 指 明 的 函 数 。 访 问 一 个 成 员 , 如 果 它 能 代 表 多 个 函 数 、对 象 、 类 型 或 枚 举 则 会 引 起 二 义 性 。
编 译 器 通 过 下 面 的 顺 序 执 行 以 检 测 出 二 义 性 :
-
如 果 访 问 的 名 称 是 有 二 义 性 的 ( 如 前 述 ), 则 产 生 一 条 错 误 信 息 。
-
如 果 重 载 函 数 是 无 二 义 性 的 , 它 们 就 没 有 什 么 问 题 了
( 有 关 重 载 函 数 二 义 性 的情 况 , 见 第 12 章 “ 重 载 ” 中 的 “ 参 量 匹 配 ” ) 。
-
如 果 访 问 的 名 称 破 坏 了 成 员 访 问 许 可 , 则 产 生 一 条 错 误 信 息 ( 有 关 信 息 详 见 第
10 章 “ 成 员 访 问 控 制 ” ) 。
在 一 个 表 达 式 产 生 了 一 个 通 过 继 承 产 生 的 二 义 性 时 , 通 过 用 类 名 称 限 制 发 生 问 题的 名 称 即 可 人 工 解 决 二 义 性 , 要 使 前 面 的 代 码 以 无 二 义 性 地 正 确 编 译 , 要 按 如 下使 用 代 码 :
C *pc = new C; pc->B::a();
注 意 : 在 类 C 说 明 之 后 , 在 C 的 范 围 中 引 用 B 就 会 潜 在 地 引 起 错 误 。 但 是 , 直 到 在C 的 范 围 中 实 际 使 用 了 一 个 对 B 的 无 限 定 性 的 引 用 , 才 会 产 生 错 误 。
二 义 性 和 虚 拟 基 类
如 果 使 用 了 虚 拟 基 类 、 函 数 、 对 象 、 类 型 以 及 枚 举 可 以 通 过 多 重 继 承 的 路 径 到达 , 但 因 为 只 有 一 个 虚 拟 基 类 的 实 例 , 因 而 访 问 这 些 名 称 时 , 不 会 引 起 二 义 性 。
图 9.10 显 示 了 采 用 虚 基 类 和 非 虚 基 类 的 对 象 的 组 成 。
图 9.10 虚 拟 的 及 非 虚 拟 的 派 生
在 图 9.10 中 , 访 问 任 何 类 A 的 成 员 , 通 过 非 虚 拟 基 类 访 问 则 会 引 起 二 义 性 ; 因 为编 译 器 没 有 任 何 信 息 以 解 释 是 使 用 同 类 B 联 系 在 一 起 的 子 对 象 , 还 是 使 用 同 类 C 联 系 在 一 起 的 子 对 象 , 然 而 当 A 说 明 为 虚 拟 基 类 时 , 则 对 于 访 问 哪 一 个 子 对 象 不存 在 问 题 了 。
支 配
通 过 继 承 图 可 能 有 多 个 名 称 ( 函 数 的 、 对 象 的 、 枚 举 的 ) 可 以 达 到 。 这 种 情 况 视为 非 虚 拟 基 类 引 起 的 二 义 性 。 但 虚 拟 基 类 也 可 以 引 起 二 义 性 , 除 非 一 个 名 称 “ 支配 ” (dominate) 了 其 它 的 名 称 。
- 个 名 称 支 配 其 它 的 名 称 发 生 在 该 名 称 定 义 在 两 个 类 中 , 其 中 一 个 是 由 另 一 个 派生 的 , 占 支 配 地 位 的 名 称 是 派 生 类 中 的 名 称 , 在 此 名 称 被 使 用 的 时 候 , 相 反 不 会 产生 二 义 性 , 如 下 面 的 代 码 所 示 :
class A
{
public:
int a;
};
class B: public virtual A
{
public:
int a();
};
class C: public virtual A
{
...
class D: public B,public C
{
public:
D() {a();} // 不 会 产 生 二 义 性 ,B::a() 支 配 了 A::a
};
转 换 的 二 义 性
显 式 地 或 隐 含 地 对 指 向 类 类 型 的 指 针 或 引 用 的 转 换 也 可 引 起 二 义 性 。 图 9.11 显示 了 如 下 几 点 :
-
说 明 了 一 个 类 型 D 的 对 象 。
-
把 取 地 址 运 算 符 用 于 此 对 象 效 果 。 注 意 , 地 址 运 算 符 总 是 支 持 对 象的 基 类 地 址 。
-
显 式 地 把 取 地 址 运 算 符 得 到 的 指 针 转 换 为 指 向 基 类 类 型 A 的 指 针 。注 意 , 把 对 象 的 地 址 造 型 转 换 成 类 型 A*, 通 常 并 未 给 编 译 器 提 供 足够 的 信 息 以 确 定 到 底 是 选 择 哪 一 个 A 类 型 的 子 对 象 。 在 此 情 况 下 , 存 在 着 两 个 A 类 型 的 子 对 象 。
对 于 到 A* 的 转 换 是 有 二 义 性 的 , 因 为 没 有 办 法 分 辨 出 哪 一 个 A 类 型 的 子 对 象 是正 确 的 。
注 意 只 要 显 式 地 说 明 你 想 要 使 用 的 是 哪 一 个 子 对 象 , 如 下 : (A*)(B*)&d // 使 用 B 的 子 对 象
图 9.11 有 二 义 性 的 指 向 基 类 的 指 针 的 转 换
虚 拟 函 数
虚 拟 函 数 可 以 确 保 在 一 个 对 象 中 调 用 正 确 的 函 数 , 而 不 管 用 于 调 用 函 数 的 表 达式 。
假 设 一 个 基 类 含 有 一 个 说 明 为 虚 拟 函 数 同 时 一 个 派 生 类 定 义 了 同 名 的 函 数 。 派生 类 中 的 函 数 是 由 派 生 类 中 的 对 象 调 用 的 , 甚 至 它 可 以 用 指 向 基 类 的 指 针 和 引 用来 调 用 。 下 面 的 例 子 显 示 了 一 个 基 类 提 供 了 一 个 PrintBalance 函 数 的 实 现 :
{
public:
Account(double d); // 构 造 函 数
virtual double GetBalance(); // 获 得 平 衡
virtual void PrintBalance(); // 缺 省 实 现private:
double _balance;
};
// 构 造 函 数 Account 的 实 现
double Account::Account(double d)
{
_balance=d;
}
//Account 的 GetBalance 的 实 现double Account::GetBalance()
{
return _balance;
}
//PrintBalance 的 缺 省 实 现void Account::PrintBalance()
{
cerr<<"Error.Balance not avai la ble for base type".
<<endl;
}
两 个 派 生 类 CheckingAccount 和 SavingsAccount 按 如 下 方 式 创 建 : class CheckingAccount:public Account
{
public:
void PrintBalance();
};
//CheckingAccount 的 PrintBalance 的 实 现void CheckingAccount::PrintBalance()
{
cout<<"Checking account balance:"
<< GetBalance();
}
class SavingsAccount:public Account
{
public:
void PrintBalance();
};
//SavingsAccount 中 的 PrintBalance 的 实 现void SavingsAccout::PrintBalance()
{
cout<<"Savings account balance:"
<< GetBalance();
}
函 数 PrintBalance 在 派 生 类 中 是 虚 拟 的 , 因 为 在 基 类 Account 中 它 是 说 明 为 虚拟 的 , 要 调 用 如 PrintBalance 的 虚 拟 函 数 , 可 以 使 用 如 下 的 代 码 :
// 创 建 类 型 CheckingAccount 和 SavingsAccount 的 对 象CheckingAccount *pChecking=new CheckingAccount(100.00); SavingsAccount *pSavings=new SavingsAcco u nt(1000.00);
// 用 指 向 Account 的 指 针 调 用 PrintBalance Account *pAccount=pChecking;
pAccount->PrintBalance();
// 使 用 指 向 Account 的 指 针 调 用 PrintBalance pAccount=pSavings;
pAccount->PrintBalance();
在 前 面 的 代 码 中 , 除 了 pAccount 所 指 的 对 象 不 同 , 调 用 PrintBalance 的 代 码 是相 同 的 。
因 为 PrintBalance 是 虚 拟 的 , 将 会 调 用 为 每 个 对 象 所 定 义 的 函 数 版 本 , 在 派 生 类CheckingAccount 和 SavingsAccount 中 的 函 数 “ 覆 盖 ” 了 基 类 中 的 同 名 函 数 。如 果 一 个 类 的 说 明 中 没 有 提 供 一 个 对 PrintBalance 的 覆 盖 的 实 现 , 则 将 采 用 基类 Account 中 的 缺 省 实 现 。
派 生 类 中 的 函 数 重 载 基 类 中 的 虚 拟 函 数 , 仅 在 它 们 的 类 型 完 全 相 同 时 才 如 此 。 派生 类 中 的 函 数 不 能 仅 在 返 回 值 上 同 基 类 中 的 虚 拟 函 数 不 同 ; 参 量 表 也 必 须 不 同 。当 指 针 或 引 用 调 用 函 数 时 , 要 遵 循 如 下 规 则 :
-
对 虚 拟 函 数 调 用 的 解 释 取 决 于 调 用 它 们 的 对 象 所 基 于 的 类 型 。
-
对 非 虚 函 数 调 用 的 解 释 取 决 于 调 用 它 们 的 指 针 或 引 用
的 类 型 。下 面 例 子 显 示 了 在 使 用 指 针 调 用 虚 拟 或 非 虚 拟 函 数 时 它 们 的 行 为 : #include <iostream.h>
// 说 明 一 个 基 类class Base
{
public:
virtual void NameOf(); // 虚 拟 函 数
void InvokingClass(); // 非 虚 拟 函 数
};
// 两 个 函 数 的 实 现void Base::NameOf()
{
cout<<"Base::NameOf\n";
}
void Base::InvokingClass()
{
cout<<"Invoke d by Base\n";
}
// 说 明 一 个 派 生 类
class Derived:public Base
{
public:
void NameOf(); // 虚 拟 函 数
void InvokingClass(); // 非 虚 拟 函 数
};
// 两 个 函 数 的 实 现
void Deri v ed::NameOf()
{
cout<<"Derived::NameOf\n";
}
void Derived::InvokingClass()
{
cout<<"Invoked by Derived\n";
}
void main()
{
// 说 明 一 个 Derived 类 型 的 对 象
Derived aDerived;
// 说 明 两 个 指 针 , 一 个 是 Derived* 型 的 , 另 一 个 是 Base* 型 的 , 并 用
//aDerived 初 始 化 它 们 。
Derived *pDerived=&aDerived;
Base *pBase =&aDerived;
// 调 用 这 个 函 数
pBase->NameOf(); // 调 用 虚 拟 函 数
pBase->InvokingClass();// 调 用 非 虚 拟 函 数
pDerived->NameOf();// 调 用 虚 拟 函 数
pDerived->InvokingClass(); // 调 用 非 虚 拟 函 数
}
该 程 序 的 输 出 是 :
Derived::NameOf Invoked by Base Derived::NameOf Invoked by Derived
注 意 , 不 管 调 用 NameOf 函 数 的 指 针 是 通 过 指 向 基 类 的 指 针 还 是 指 向 派 生 类 的 指针 , 它 调 用 的 函 数 是 派 生 类 的 。 因 为 NameOf 是 虚 拟 函 数 , 而 且 pBase 和 pDerived 指 向 的 对 象 都 是 派 生 类 的 , 故 而 调 用 函 数 是 派 生 类 的 。
因 为 虚 拟 函 数 只 能 为 类 类 型 的 对 象 所 调 用 , 所 以 你 不 能 把 一 个 全 局 的 或 静 态 函 数说 明 为 虚 拟 的 。
在 派 生 类 中 说 明 一 个 重 载 函 数 时 可 以 用 virtual 关 键 字 , 但 是 这 并 不 是 必 须 的 , 因 为 重 载 一 个 虚 拟 函 数 , 此 函 数 就 必 然 是 虚 拟 函 数 。
基 类 中 的 虚 拟 函 数 必 须 有 定 义 , 除 非 它 们 被 说 明 为 纯 的 ( 有 关 纯 虚 拟 函 数 的 详 情见 下 节 “ 抽 象 类 ” ) 。
虚 拟 函 数 调 用 机 制 可 以 用 范 围 分 辨 符 (::) 明 确 地 限 定 函 数 名 称 的 方 法 来 加 以 限制 。 考 虑 前 面 的 代 码 , 用 下 面 的 代 码 调 用 基 类 的 PrintBalance 。CheckingAccount *pChecking=new CheckingAccount(100.00) ;
pChecking->Account::PrintBalanc e (); // 明 确 限 定
Account *pAccount=pChecking; // 调 用 Account::PrintBalance
pAccount->Account::PrintBalance();// 明 确 限 定
上 面 例 子 中 的 两 个 对 PrintBalance 的 调 用 都 限 制 了 虚 拟 函 数 的 调 用 机 制 。
抽 象 类
抽 象 类 就 像 一 个 一 段 意 义 上 的 说 明 , 通 过 它 可 以 派 生 出 特 有 的 类 。 你 不 能 为 抽 象类 创 建 一 个 对 象 , 但 你 可 以 用 抽 象 类 的 指 针 或 引 用 。
至 少 含 有 一 个 纯 虚 拟 函 数 的 类 就 是 抽 象 类 。 从 抽 象 类 中 派 生 出 的 类 必 须 为 纯 虚拟 函 数 提 供 实 现 , 否 则 它 们 也 是 抽 象 类 。
把 一 个 虚 拟 函 数 说 明 为 纯 的 , 只 要 通 过 纯 说 明 符 语 法 ( 见 本 章 前 面 的 “ 类 协 议 的实 现 ” ), 考 虑 一 下 本 章 早 些 时 候 在 “ 虚 拟 函 数 ” 中 提 供 的 例 子 。 类 Account 的意 图 是 提 供 一 个 通 常 意 义 的 函 数 功 能 ,Account 类 型 的 对 象 太 简 单 而 没 有 太 多 用处 。 因 此 Account 是 作 为 抽 象 类 的 一 个 很 好 的 候 选 :
class Account
{
public:
Account(double d); // 构 造 函 数
virtual double GetBalance();// 获 得 平 衡
virtual void PrintBalance()=0; // 纯 虚 拟 函 数
Private:
double _balance;
};
这 里 的 说 明 同 前 一 次 的 说 明 的 唯 一 不 同 是 PrintBalance 是 用 纯 说 明 符 说 明 的 。
使 用 抽 象 类 的 限 制
抽 象 类 不 能 用 于 如 下 用 途 :
-
变 量 或 成 员 数 据
-
参 量 类 型
-
函 数 的 返 回 类 型
-
明 确 的 转 换 类 型
另 外 一 个 限 制 是 如 果 一 个 抽 象 类 的 构 造 函 数 调 用 了 一 个 纯 虚 拟 函 数 , 无 论 是 直 接还 是 间 接 的 , 结 果 都 是 不 确 定 的 。 但 抽 象 类 的 构 造 函 数 的 析 构 函 数 可 以 调 用 其 它成 员 函 数 。
抽 象 类 的 纯 虚 拟 函 数 可 以 有 定 义 , 但 它 们 不 能 用 下 面 语 法 直 接 调 用 : 抽 象 类 名 称 ::函 数 名 称 ()
在 设 计 基 类 中 含 有 纯 虚 拟 析 构 函 数 的 类 层 次 时 , 这 一 点 很 有 用 。 因 为 在 销 毁 一 个对 象 的 过 程 中 通 常 都 要 调 用 基 类 的 析 构 函 数 , 考 虑 下 面 的 例 子 :
#include <iostream.h>
// 说 明 一 个 带 有 纯 虚 拟 析 构 函 数 的 抽 象 类class b ase
{
public:
base() { }
virtual ~ base()=0;
};
// 提 供 一 个 析 构 函 数 的 定 义
base:: ~ base()
{
};
class derived:public base
{
public:
derived() { };
~ derived() { };
};
void main()
{
derived *pDerived=new derived;
delete pDerived;
}
当 一 个 由 pDerived 所 指 的 对 象 销 毁 的 时 候 , 会 调 用 类 derived 的 析 构 函 数 , 进 而
调 用 基 类 base 中 的 析 构 函 数 。 纯 虚 拟 函 数 的 空 的 实 现 保 证 了 该 函 数 至 少 存 在 着一 些 操 作 。
注 意 : 在 前 面 例 子 中 , 纯 虚 拟 函 数 base:: ~ base 是 在 derived:: ~ derived 中 隐含 调 用 的 。 当 然 明 确 地 用 全 限 定 成 员 函 数 名 称 去 调 用 纯 虚 拟 函 数 是 可 能 的 。
范 围 规 则 总 结
这 一 节 补 充 一 些 有 关 类 的 新 的 概 念 :
-
二 义 性
-
全 局 名 称
-
名 称 和 限 定 名
-
函 数 的 参 量 名 称
-
构 造 函 数 初 始 化 器
二 义 性
名 称 的 使 用 在 其 范 围 中 必 须 是 无 二 义 性 的 ( 直 到 名 称 的 重 载 点 ) 。 如 果 这 个 名 称表 示 了 一 个 函 数 , 那 么 这 个 函 数 必 须 是 关 于 参 量 的 个 数 和 类 型 是 无 二 义 性 的 。 如果 名 称 存 在 着 二 义 性 , 则 要 运 用 成 员 访 问 规 则 。
全 局 名 称
- 个 对 象 、 函 数 或 枚 举 的 名 称 如 果 在 任 何 函 数 、 类 之 外
引 入 或 前 缀 有 全 局 单 目
范 围 操 作 符 (::), 并 同 时 没 有 同 任 何 下 述 的 双 目 操 作 符 连 用 。
-
范 围 分 辨 符 (::)
-
对 象 和 引 用 的 成 员 选 择 符 (.)
-
指 针 的 成 员 选 择 符 (->)
名 称 及 限 定 名
同 双 目 的 范 围 分 辨 符 (::) 一 起 使 用 的 名 称 叫 “ 限 定 名 ”。 在 双 目 范 围 分 辨 符 之 后说 明 的 名 称 必 须 是 在 该 说 明 符 左 边 所 说 明 的 类 的 成 员 或 其 基 类 的 成 员 。
在 成 员 选 择 符 (. 或 ->) 后 说 明 的 名 称 必 须 是 在 该 说 明 符 左 边 所 说 明 的 类 类 型 对 象的 成 员 或 其 基 类 的 成 员 。 在 成 员 选 择 符 的 右 边 所 说 明 的 名 称 可 以 是 任 何 类 类 型对 象 , 只 要 该 说 明 符 的 左 边 是 一 个 类 类 型 对 象 , 而 且 该 对 象 的 类 定 义 了 一 个 重 载的 成 员 选 择 符 (->), 它 把 指 针 所 指 的 对 象 变 为 特 殊 的 类 类 型 ( 这 一 规 定 , 在 第 12 章 “ 重 载 ” 中 的 类 成 员 访 问 中 详 细 讨 论 ) 。
编 译 器 按 下 面 的 顺 序 搜 索 一 个 名 称 , 发 现 以 后 便 停 止 :
-
如 果 名 称 是 在 函 数 中 使 用 , 则 在 当 前 块 范 围 中 搜 索 , 否 则 在 全 局 范 围中 搜 索 。
-
向 外 到 每 一 个 封 闭 块 范 围 中 搜 索 , 包 括 最 外 面 函 数 范 围 ( 这 将 包 括 函数 的 参 量 ) 。
-
如 果 名 称 在 一 个 成 员 函 数 中 使 用 , 则 在 该 类 的 范 围 中
搜 索 该 名 称 。
-
在 该 类 的 基 类 中 搜 索 该 名 称 。
-
在 外 围 嵌 套 类 范 围 ( 如 果 有 ) 或 其 基 类 中 搜 索 , 这 一 搜 索 一 直 到 最 外 层包 裹 的 类 的 范 围 搜 索 之 后 。
-
在 全 局 范 围 中 搜 索 。
然 而 你 可 以 按 如 下 方 式 改 变 搜 索 顺 序 :
-
如 果 名 称 的 前 面 有 ::, 则 强 制 搜 索 在 全 局 范 围 之 中 。
-
如 果 名 称 的 前 面 有 class 、 struct 和 union 关 键 字 , 将 强 制 编 译 器 仅搜索 class , struct 或 union 名 称 。
-
在 范 围 分 辨 符 的 左 边 的 名 称 , 只 能 是 class , struct 和
union 的 名 称 。
如 果 在 一 个 静 态 成 员 函 数 中 引 用 了 一 个 非 静 态 的 成 员 名 , 将 会 产 生 一 条 错 误 消息 。 同 样 地 , 任 何 引 用 包 围 类 中 的 非 静 态 组 员 会 产 生 一 条 错 误 消 息 , 因 为 被 包 围的 类 没 有 包 围 类 的 this 指 针 。
函 数 参 量 名 称
函 数 的 参 量 名 称 在 函 数 的 定 义 中 视 为 在 函 数 的 最 外 层 块 的 范 围 中 。 因 此 , 它 们 是局 部 名 称 并 且 在 函 数 结 束 之 后 , 范 围 就 消 失 了 。
函 数 的 参 量 名 称 是 在 函 数 说 明 ( 原型 ) 的 局 部 范 围 中 , 并 且 在 说 明 结 束 以 后 的 范 围中 消 失 。
缺 省 的 参 量 名 称 是 在 参 量 ( 它 们 是 缺 省 的 ) 范 围 中 , 如 前 面 两 段 描 述 的 , 然 而 它 们不 能 访 问 局 部 变 量 和 非 静 态 类 成 员 。 缺 省 参 量 值 的 确 定 是 在 函 数 调 用 的 时 候 , 但它 们 的 给 定 是 在 函 数 说 明 的 原 始 范 围 中 。 因 此 成 员 函 数 的 缺 省 参 量 总 是 在 类 范围 中 的 。
构 造 函 数 初 始 化 器
构 造 函 数 初 始 化 器 ( 在 第 11 章 “ 特 殊 成 员 函 数 ” 中 的 “ 初 始 化 基 类 和 成 员 ” 中详 述 ) 在 构 造 函 数 说 明 的 最 后 层 块 范 围 中 求 值 。 因 此 它 们 可 以 使 用 构 造 函 数 的 参量 名 称 。