第16.1章 继承是什么:基类子对象
1. 派生对象含有一个基类对象
struct labeled_point : point 并不会把 point 的代码拷进
labeled_point。它让每个 labeled_point
含有一个 point,作为自己的一部分,并把这个部分叫作
基类子对象。关于继承的其他一切,都是从这一句话推出来的。
struct point
{
::std::int_least32_t x{}, y{};
[[nodiscard]] ::std::int_least32_t sum() const noexcept { return x + y; }
};
struct labeled_point : point
{
::std::string_view label{};
};
p.x与p.y在labeled_point上照样能用: 它们是基类子对象的成员,名字查找找得到。p.sum()也能用:sum是在point里声明的, 被继承了。并没有为它在labeled_point里生成什么。sizeof(labeled_point)至少是sizeof(point)加上新增的成员 —— 在这台机器上point是8、::std::string_view是16,所以结果是24。- 一个
labeled_point就是 一个point: 可以把point const&绑定到它上面 (第16.5章)。
这个关系从派生类一侧写出来,而基类名前面那个访问说明符不是装饰 ——
它是 public、protected 或 private,
并且会改变程序其余部分能对这个关系做什么
(第16.3章)。
2. is-a 测试
public 继承是 C++ 能对类型做出的最强断言:它说派生类型就是基类型, 对任何用途、永远成立。把这句话读出来,然后检查:
| 候选 | “一个 D 是一个 B 吗” |
结论 |
|---|---|---|
labeled_point : point |
带标签的点是一个点 | 是 —— 标签是附加信息,点承诺的一切仍然成立 |
point : labeled_point |
一个点是一个带标签的点 | 不是 —— 普通点没有标签 |
car : engine |
一辆车是一台发动机 | 不是 —— 车有一台发动机 (第16.8章) |
square : rectangle |
正方形是矩形 | 看着像、其实错 —— 矩形可以被改成 2×3,正方形不能,
所以把 square& 当 rectangle& 传出去就会破坏它 |
最后一行是严格要求的经典理由。public 继承意味着基类的承诺对派生对象对所有调用者 成立,包括那些在派生类存在之前就写好的调用者。“is-a”不是现实世界的事,而是可替换性的事。
3. 布局:基类在最前面
在不涉及 virtual 继承时,基类子对象位于派生对象的最前面,这正是“指向派生对象的
基类引用不需要任何调整”的原因:
| 表达式 | 类型 | 值 |
|---|---|---|
sizeof(point) |
::std::size_t |
8 |
sizeof(labeled_point) |
::std::size_t |
24 |
sizeof(std::string_view) |
::std::size_t |
16 |
继承本身不额外占一个字节的填充:没有新增数据成员的派生类和它的基类一样大。 这属于“空基类优化”的话题,也是本章倒数第二个惊喜 (第16.8章)。
4. 完整程序
#include <fast_io.h>
#include <cstdint>
#include <string_view>
struct point
{
::std::int_least32_t x{}, y{};
[[nodiscard]] ::std::int_least32_t sum() const noexcept { return x + y; }
};
struct labeled_point : point
{
::std::string_view label{};
};
int main()
{
try
{
labeled_point p{};
p.x = 3;
p.y = 4;
p.label = "corner";
::fast_io::io::println(p.label, " ", p.sum());
::fast_io::io::println("sizeof(point)=", sizeof(point), " sizeof(labeled_point)=", sizeof(labeled_point));
point const &base{p};
::fast_io::io::println("base.sum()=", base.sum());
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
输出:
corner 7
sizeof(point)=8 sizeof(labeled_point)=24
base.sum()=7
第三行才是有意思的:base 是绑定到 p 内部那个
point 子对象的引用,不是拷贝,而且它没有经过任何强制转换。
5. 经验法则
- 写下冒号之前,先把 is-a 那句话读出来。如果它对每个调用者都不是字面为真的,就用成员。
- 显式写
: public base;在struct里public是隐含的, 而在class里隐含的是相反的,所以这个词能防止有人把struct改成class时含义悄悄变化。 - 让基类小而自洽。你放进基类的一切都会被它下面的所有类型继承,包括未来代码必须维持的不变式。
- 不要用继承来复用代码。基类是一个类型,不是宏;复用是函数和模板的事。
- 不要继承你不拥有的类型,除非它的作者写明了它是基类(公开且非虚的析构函数通常意味着相反的意思)。