第16.1章 继承是什么:基类子对象

English

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{};
};

这个关系从派生类一侧写出来,而基类名前面那个访问说明符不是装饰 —— 它是 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. 经验法则