Derived classes
任何类类型(无论使用 class-key class 还是 struct 声明)都可以被声明为从一个或多个 基类 派生 ,这些基类又可以派生自它们自己的基类,从而形成继承层次结构。
目录 |
语法
基类列表由
类声明语法
中的
基类子句
提供。
基类子句
由字符
:
后接一个或多个以逗号分隔的
基类说明符
组成。
| attr (可选) class-or-computed | (1) | ||||||||
attr
(可选)
virtual
class-or-computed
|
(2) | ||||||||
| attr (可选) access-specifier class-or-computed | (3) | ||||||||
attr
(可选)
virtual
access-specifier
class-or-computed
|
(4) | ||||||||
attr
(可选)
access-specifier
virtual
class-or-computed
|
(5) | ||||||||
virtual
和
access-specifier
可以以任意顺序出现。
| attr | - | (since C++11) 任意数量的 属性 序列 | ||||
| access-specifier | - | private 、 public 或 protected 之一 | ||||
| class-or-computed | - |
以下之一
|
一个 详细类型说明符 由于语法限制不能直接作为 class-or-computed 出现。
|
base-specifier 在 base-clause 中可以是 包展开 。
被声明为
|
(C++11 起) |
如果省略 access-specifier ,对于使用 struct 声明的派生类,其默认访问说明符为 public ;对于使用 class 声明的派生类,其默认访问说明符为 private 。
struct Base { int a, b, c; }; // 每个 Derived 类型的对象都包含 Base 作为子对象 struct Derived : Base { int b; }; // 每个 Derived2 类型的对象都包含 Derived 和 Base 作为子对象 struct Derived2 : Derived { int c; };
在 base-clause 中列出的、由 class-or-computed 表示的类称为直接基类。这些基类所继承的类称为间接基类。同一个类不能被多次指定为直接基类,但同一个类可以同时作为直接基类和间接基类存在。
每个直接和间接基类都以 基类子对象 的形式,存在于派生类的对象表示中,其偏移量由ABI决定。由于 空基类优化 ,空基类通常不会增加派生对象的大小。基类子对象的构造函数由派生类的构造函数调用:可以通过 成员初始化器列表 向这些构造函数提供参数。
虚拟基类
对于每个被指定为 virtual 的不同基类, 最终派生对象 仅包含该类型的一个基类子对象,即使该类在继承层次结构中多次出现(只要每次继承时都使用 virtual 继承)。
struct B { int n; }; class X : public virtual B {}; class Y : virtual public B {}; class Z : public B {}; // 每个 AA 类型的对象包含一个 X、一个 Y、一个 Z 以及两个 B 子对象: // 一个是 Z 的基类,另一个由 X 和 Y 共享 struct AA : X, Y, Z { AA() { X::n = 1; // 修改虚继承 B 子对象的成员 Y::n = 2; // 修改同一虚继承 B 子对象的成员 Z::n = 3; // 修改非虚继承 B 子对象的成员 std::cout << X::n << Y::n << Z::n << '\n'; // 输出 223 } };
标准库中的iostreams继承层次结构是虚基类继承的一个示例: std::istream 和 std::ostream 通过虚继承从 std::ios 派生。 std::iostream 同时继承自 std::istream 和 std::ostream ,因此每个 std::iostream 实例都包含一个 std::ostream 子对象、一个 std::istream 子对象,以及仅一个 std::ios 子对象(因而也只有一个 std::ios_base )。
所有虚基类子对象都在任何非虚基类子对象之前初始化,因此只有 最终派生类 在其 成员初始化列表 中调用虚基类的构造函数:
struct B { int n; B(int x) : n(x) {} }; struct X : virtual B { X() : B(1) {} }; struct Y : virtual B { Y() : B(2) {} }; struct AA : X, Y { AA() : B(3), X(), Y() {} }; // AA的默认构造函数会调用X和Y的默认构造函数 // 但这些构造函数不会调用B的构造函数,因为B是虚基类 AA a; // a.n == 3 // X的默认构造函数会调用B的构造函数 X x; // x.n == 1
在涉及虚继承时,类成员的非限定名称查找存在 特殊规则 (有时被称为支配规则)。
公有继承
当类使用 public 成员访问说明符 从基类继承时,基类的所有公共成员可作为派生类的公共成员进行访问,基类的所有受保护成员可作为派生类的受保护成员进行访问(基类的私有成员除非被设为友元,否则永远不可访问)。
公有继承模拟了面向对象编程中的子类型关系:派生类对象 IS-A 基类对象。指向派生对象的引用和指针应当能被任何期望指向其任意公有基类的引用或指针的代码所使用(参见 LSP ),或者用 DbC 术语来说,派生类应当维护其公有基类的类不变式,不应强化其 重写 成员函数的任何前置条件,也不应弱化任何后置条件。
#include <iostream> #include <string> #include <vector> struct MenuOption { std::string title; }; // Menu 是 MenuOption 的向量:选项可被插入、删除、重新排序... // 并且具有标题。 class Menu : public std::vector<MenuOption> { public: std::string title; void print() const { std::cout << title << ":\n"; for (std::size_t i = 0, s = size(); i < s; ++i) std::cout << " " << (i + 1) << ". " << at(i).title << '\n'; } }; // 注意:Menu::title 没有问题,因为其作用独立于基类。 enum class Color { WHITE, RED, BLUE, GREEN }; void apply_terminal_color(Color) { /* 操作系统相关 */ } // 这是错误的做法! // ColorMenu 是一个每个选项都有自定义颜色的菜单。 class ColorMenu : public Menu { public: std::vector<Color> colors; void print() const { std::cout << title << ":\n"; for (std::size_t i = 0, s = size(); i < s; ++i) { std::cout << " " << (i + 1) << ". "; apply_terminal_color(colors[i]); std::cout << at(i).title << '\n'; apply_terminal_color(Color::WHITE); } } }; // ColorMenu 需要满足以下无法通过公开继承 Menu 来保证的不变式,例如: // - ColorMenu::colors 和 Menu 必须具有相同数量的元素 // - 为了保持逻辑,调用 erase() 时应当同时从 colors 中移除元素, // 以便让选项保持其颜色 // 基本上每个对 std::vector 方法的非常量调用都会破坏 ColorMenu 的不变式, // 并且需要用户通过正确管理颜色来修复。 int main() { ColorMenu color_menu; // 这个类的主要问题是我们必须保持 ColorMenu::colors // 与 Menu 同步。 color_menu.push_back(MenuOption{"Some choice"}); // color_menu.print(); // 错误!print() 中的 colors[i] 越界 color_menu.colors.push_back(Color::RED); color_menu.print(); // 正确:colors 和 Menu 具有相同数量的元素 }
保护继承
当类使用 protected 成员访问说明符 从基类继承时,基类的所有公共成员和保护成员均可作为派生类的保护成员进行访问(基类的私有成员除非被设为友元,否则永远不可访问)。
保护继承可用于"受控多态性":在派生类的成员中,以及所有进一步派生类的成员中,派生类都是一个基类:在需要基类引用和指针的地方,可以使用派生类的引用和指针。
私有继承
当一个类使用 private 成员访问说明符 从基类继承时,基类的所有公共和受保护成员在派生类中均可作为私有成员访问(基类的私有成员除非被设为友元,否则永远不可访问)。
私有继承常用于基于策略的设计,因为策略类通常是空类,将其用作基类既能实现静态多态,又能利用 空基类优化 。
私有继承也可用于实现组合关系(基类子对象是派生类对象的实现细节)。使用成员变量能提供更好的封装性,通常更推荐这种方式,除非派生类需要访问基类的保护成员(包括构造函数)、需要重写基类的虚成员、需要让基类在其他基类子对象之前构造之后析构、需要共享虚基类或需要控制虚基类的构造。在从 参数包 进行多重继承,或基类身份通过模板元编程在编译时确定的情况下,使用成员实现组合的方式也不适用。
与受保护继承类似,私有继承也可用于受控多态性:在派生类成员(但不包括进一步派生的类)内部,派生类 IS-A 基类。
template<typename Transport> class service : private Transport // 通过私有继承自传输策略 { public: void transmit() { this->send(...); // 使用提供的任意传输方式进行发送 } }; // TCP传输策略 class tcp { public: void send(...); }; // UDP传输策略 class udp { public: void send(...); }; service<tcp> service(host, port); service.transmit(...); // 通过TCP进行传输
成员名称查找
类成员的限定与非限定名称查找规则详见 名称查找 。
关键词
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 1710 | C++98 | class-or-decltype 的语法使得无法从需要 template 消歧符的依赖类派生 | 允许使用 template 消歧符 |