本文为 C++ 学习笔记,参考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代码大全》第 2 版。
继承是一种复用,不同抽象层次的对象可以复用相同的特性。继承通常用于说明一个类(派生类)是另一个类(基类)的特例。继承的目的在于,通过“定义能为两个或更多个派生类提供共有元素的基类”的方式写出更精简的代码。
1. 继承基础
本节以公有继承为例,说明继承中的基础知识。
日常生活中的继承示例:
基类 | 派生类 |
---|---|
Fish(鱼) | Goldfish(金鱼)、 Carp(鲤鱼)、 Tuna(金枪鱼,金枪鱼是一种鱼) |
Mammal(哺乳动物) | Human(人)、 Elephant(大象)、 Lion(狮子)、 Platypus(鸭嘴兽,鸭嘴兽是一种哺乳动物) |
Bird(鸟) | Crow(乌鸦)、 Parrot(鹦鹉)、 Ostrich(鸵鸟)、 Platypus(鸭嘴兽,鸭嘴兽也是一种鸟) |
Shape(形状) | Circle(圆)、 Polygon(多边形,多边形是一种形状) |
Polygon(多边形) | Triangle(三角形)、 Octagon(八角形,八角形是一种多边形,而多边形是一种形状) |
1.1 继承与派生
基类(比如鱼类)派生出派生类(比如金枪鱼类),派生类继承基类。公有继承中,派生类是基类的一种,比如,我们可以说,金枪鱼是鱼的一种。
阅读介绍继承的文献时,“从…继承而来(inherits from)”和“从…派生而来(derives from)”术语的含义相同。同样,基类(base class)也被称为超类(super class);从基类派生而来的类称为派生类(derived class),也叫子类(sub class)。
1.2 构造函数的“继承”与覆盖
在 C++11 新标准中,派生类能够重用其直接基类定义的构造函数,这些构造函数并不是通常意义上的继承,它只是一种代码复用,为方便起见,我们暂称为“继承”。一个类只初始化其直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。
派生类继承直接基类的构造函数的方法是在派生类中使用 using 声明语句,如下:
class Base
{
public:
Base() {}; // 1. 默认、拷贝、移动构造函数不能被继承和覆盖
Base(int a) {}; // 2. 被派生类中的构造函数覆盖
Base(int a, int b) {}; // 3. 被派生类中的构造函数继承
Base(int a, string b) {}; // 3. 被派生类中的构造函数继承
};
class Derived: public Base
{
public:
using Base::Base; // 声明继承基类中的构造函数,若无此声明是不继承构造函数的
Derived(int a) {}; // 覆盖基类中的构造函数
};
通常情况下,using 声明只是令某个名字在当前作用域可见。而用于构造函数时,using 声明语句将令编译器生成代码。
对于基类的每个可被继承的构造函数,编译器都在派生类中生成一个形参列表完全相当的构造函数。不过有两种例外情况,第一种:如果派生类构造函数与基类构造函数参数表一样,则相当于派生类构造函数覆盖了基类构造函数,这种情况被覆盖的基类构造函数无法被继承;第二种:默认、拷贝、移动构造函数不会被继承。根据这些规则,上例代码由编译器生成的派生类构造函数形式如下:
class Derived: public Base
{
public:
Derived(int a, int b) : Base(a, b) {};
Derived(int a, string b) : Base(a, b) {};
};
1.3 在派生类中调用基类构造函数
派生类调用基类构造函数有三种形式:
- 如果基类有默认构造函数,派生类构造函数会隐式调用基类默认构造函数,这由编译器实现,不需编写调用代码;
- 如果基类没有默认构造函数,即基类提供了重载的构造函数,则派生类构造函数通过初始化列表来调用基类构造函数,这属于显式调用。这种方式是必需的,否则编译器会试图调用基类默认构造函数,而基类并无默认构造函数,编译会出错;
- 在派生类构造函数中,使用
::Base()
形式显示调用基类构造函数。和基类普通函数的调用方式不同,派生类中调用基类普通函数的形式为Base::Function()
(需要指定类名)。虽然这种方式和第 2 种方式实现功能基本一样,但如果只使用这种方式而缺少第 2 种方式,编译会出错。这种方式似乎没有什么意义。??此处出自哪里,怎么找不到??
如果基类包含重载的构造函数,需要在实例化时给它提供实参,创建派生类对象时如何实例化这样的基类呢?这是上述三种形式中的第二种形式工,方法是使用初始化列表,并通过派生类的构造函数调用合适的基类构造函数。
class Base
{
public:
Base(int a) { m = a };
private:
int m;
};
class Derived: public Base
{
public:
Derived(): Base(25) {}; // 基类构造函数被调用一次,最终 Base::m 值为 25
Derived(): Base(25) { ::Base(36) }; // 基类构造函数被调用两次,最终 Base::m 值为 25
Derived() { ::Base(36) }; // 编译器试图调用基类默认构造函数 Base::Base(),编译出错
};
1.4 构造顺序与析构顺序
Tuna 继承自 Fish,则创建 Tuna 对象时的构造顺序为:1. 先构造 Tuna 中的 Fish 部分;2. 再构造 Tuna 中的 Tuna 部分。实例化 Fish 部分和 Tuna 部分时,先实例化成员属性,再调用构造函数。析构顺序与构造顺序相反。示例程序如下:
#include <iostream>
using namespace std;
class FishDummy1Member
{
public:
FishDummy1Member() { cout << "Fish.m_dummy1 constructor" << endl; }
~FishDummy1Member() { cout << "Fish.m_dummy1 destructor" << endl; }
};
class FishDummy2Member
{
public:
FishDummy2Member() { cout << "Fish.m_dummy2 constructor" << endl; }
~FishDummy2Member() { cout << "Fish.m_dummy2 destructor" << endl; }
};
class Fish
{
private:
FishDummy1Member m_dummy1;
FishDummy2Member m_dummy2;
public:
Fish() { cout << "Fish constructor" << endl; }
~Fish() { cout << "Fish destructor" << endl; }
};
class TunaDummyMember
{
public:
TunaDummyMember() { cout << "Tuna.m_dummy constructor" << endl; }
~TunaDummyMember() { cout << "Tuna.m_dummy destructor" << endl; }
};
class Tuna: public Fish
{
private:
TunaDummyMember m_dummy;
public:
Tuna() { cout << "Tuna constructor" << endl; }
~Tuna() { cout << "Tuna destructor" << endl; }
};
int main()
{
Tuna tuna;
}
为了帮助理解成员变量是如何被实例化和销毁的,定义了两个毫无用途的空类:FishDummyMember 和 TunaDummyMember。程序输出如下:(//后不是打印内容,是说明语句)
Fish.m_dummy1 constructor // 基类数据成员1实例化
Fish.m_dummy2 constructor // 基类数据成员2实例化
Fish constructor // 基类构造函数
Tuna.m_dummy constructor // 派生类数据成员实例化
Tuna constructor // 派生类构造函数
Tuna destructor // 派生类析构函数
Tuna.m_dummy destructor // 派生类数据成员销毁
Fish destructor // 基类析构函数
Fish.m_dummy2 destructor // 基类数据成员2销毁
Fish.m_dummy1 destructor // 基类数据成员1销毁
1.5 基类方法的覆盖与隐藏
这里的覆盖与隐藏,指的是基类中的方法在派生类对象中的可见性问题。
实现中方法的覆盖与隐藏,参考注释 1.1 1.2 1.3。
调用派生类中的覆盖方法,参考注释 2.1 2.2。
调用基类中被覆盖的方法,参考注释 3.1 3.2。
调用基类中被隐藏的方法,参考注释 4.1 4.2 4.3。
调用基类中的其他方法,参考注释 5.1 5.2。
#include <iostream>
using namespace std;
class Fish
{
private:
bool isFreshWaterFish;
public:
Fish(bool IsFreshWater) : isFreshWaterFish(IsFreshWater){}
void Swim() // 1.1 此方法被派生类中的方法覆盖
{
if (isFreshWaterFish)
cout << "Fish::Swim() Fish swims in lake" << endl;
else
cout << "Fish::Swim() Fish swims in sea" << endl;
}
void Swim(bool freshWater) // 1.3 此方法被派生类中的方法隐藏,因为派生类中的覆盖函数隐藏了基类中 Swim 的所有重载版本
{
if (freshWater)
cout << "Fish::Swim(bool) Fish swims in lake" << endl;
else
cout << "Fish::Swim(bool) Fish swims in sea" << endl;
}
void Fly()
{
cout << "Fish::Fly() Joke? A fish can fly?" << endl;
}
};
class Tuna: public Fish
{
public:
Tuna(): Fish(false) {}
// using Fish::Swim; // 4.2 解除对基类中所有 Swim() 重载版本的隐藏
void Swim() // 1.2 覆盖基类中的方法
{
cout << "Tuna::Swim() Tuna swims real fast" << endl;
}
};
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
void Swim() // 1.2 覆盖基类中的方法
{
cout << "Carp::Swim() Carp swims real slow" << endl;
Fish::Swim(); // 3.2 在派生类中调用基类方法(继承得到)
Fish::Fly(); // 5.2 在派生类中调用基类方法(继承得到)
}
/*
void Swim(bool freshWater) // 4.3 覆盖基类中 Swim(bool) 方法
{
Fish::Swim(freshWater); // 4.3 调用基类中被覆盖的 Swim(bool) 方法
}
*/
};
int main()
{
Carp carp;
Tuna tuna;
carp.Swim(); // 2.1 调用派生类中的覆盖方法
tuna.Swim(); // 2.2 调用派生类中的覆盖方法
tuna.Fish::Swim(); // 3.1 调用基类中被覆盖的方法
tuna.Fish::Swim(false); // 4.1 调用基类中被隐藏的方法
tuna.Fly(); // 5.1 调用基类中的其他方法(继承得到)
return 0;
}
运行结果如下:
Carp::Swim() Carp swims real slow
Fish::Swim() Fish swims in lake
Fish::Fly() Joke? A fish can fly?
Tuna::Swim() Tuna swims real fast
Fish::Swim() Fish swims in sea
Fish::Swim(bool) Fish swims in sea
Fish::Fly() Joke? A fish can fly?
2. 访问权限与类的继承方式
访问权限有三种:公有 (public)、保护 (protected) 和私有 (private),这三个关键字也称访问限定符。访问限定符出现在两种场合:一个是类的成员的访问权限,类有公有成员、保护成员和私有成员;一个是类的继承方式,继承方式有公有继承、保护继承和私有继承三种。
这两种场合的访问权限组合时,编译器采用最严格的策略,确保派生类中继承得到的基类成员具有最低的访问权限。例如,基类的公有成员遇到私有继承时,就变成派生类中的私有成员;基类的保护成员遇到公有继承时,就变成派生类中的保护成员;基类的私有成员派生类不可见。
注意一点,基类的私有成员派生类不可见,但派生类对象里实际包含有基类的私有成员信息,只是它没有权限访问而已。参 3.1 节。
2.1 类成员访问权限
类的成员有三种类型的访问权限:
public: public 成员允许在类外部访问。类外部访问方式包括通过类的对象访问,通过派生类的对象访问以及在派生类内部访问。
protected: protected 成员允许在类内部、派生类内部和友元类内部访问,禁止在继承层次结构外部访问。
private: private 成员只能在类内部访问。
类的内部包括类的声明以及实现部分,类的外部包括对当前类的调用代码以及其它类的声明及实现代码。
2.2 公有继承
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原来的状态。基类的公有成员在派生类中也是公有成员,基类的保护成员在派生类中也是保护成员,基类的私有成员派生类不可见。
公有继承用于“是一种(is-a)”的关系。is-a 表示派生类是基类的一种,比如金枪鱼(派生类)是鱼(基类)的一种。
2.3 私有继承
私有继承的特点是基类的公有成员和保护成员都变成派生类的私有成员。基类的私有成员仍然为基类所私有,派生类不可见。
私有继承使得只有派生类才能使用基类的属性和方法,因此表示“有一部分”(has-a)关系。has-a 表示基类是派生类的一部分,派生类包含基类,比如发动机(基类)是汽车(派生类)的一部分。
2.4 保护继承
保护继承的特点是基类的公有成员和保护成员都变成派生类的保护成员。基类的私有成员仍然为基类所私有,派生类不可见。
与私有继承类似,保护继承也表示 has-a 关系。不同的是,基类的公有和保护成员变为派生类中的保护成员,能够被派生类及派生类的子类访问。
2.5 总结
下表中,表头部分表示基类的三种成员,表格正文部分表示不同继承方式下,对应的基类成员在派生类中的访问权限。以表格第四行第二列为例,表示在私有继承方式下,基类的 public 成员将成为派生类中的 private 成员。
基类成员 | public 成员 | protected 成员 | private 成员 |
---|---|---|---|
共有继承 | public 成员 | protected 成员 | 不可见 |
保护继承 | protected 成员 | protected 成员 | 不可见 |
私有继承 | private 成员 | private 成员 | 不可见 |
注意:慎用私有或保护继承。对于大多数使用私有继承的情形(如汽车和发动机之间的私有继承),更好的选择是,将基类对象作为派生类的一个成员属性,使用组合,而不是继承。
3. 基类对象与派生类对象的赋值关系
3.1 派生类对象与基类的关系
#include <iostream>
using namespace std;
class Base
{
private:
int x = 1;
int y = 2;
const static int z = 3;
};
class Derived : public Base
{
private:
int u = 11;
int v = 22;
const static int w = 33;
};
int main()
{
Base base;
Derived derived;
cout << "sizeof(Base) = " << sizeof(Base) << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
return 0;
}
程序输出为:
sizeof(Base) = 8
sizeof(Derived) = 16
类里的 static 成员属于整个类,而不属于某一个对象,不计入类的 sizeof。sizeof(类名) 等于 sizeof(对象名),因此 sizeof(Base) 值是 8。对于派生类 Derived,其 sizeof 运算结果为基类数据成员占用空间大小加上派生类数据成员占用空间大小,因此值为 16。
注意一点,派生类对象所在的内存空间里含有基类数据成员信息,包括基类私有数据成员,但派生类没有权限访问基类私有数据成员,编译器在语法上不支持。
使用 gdb 调试程序,打印出基类对象和派生类对象的值,可得到如下信息:
(gdb) p base
$1 = {x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}
3.2 切除问题
将派生类对象复制给基类对象有如下两种情况:
第一种:通过赋值操作将派生类对象复制给基类对象
Derived objDerived;
Base objectBase = objDerived;
第二种:通过传参方式将派生类对象复制给基类对象
void UseBase(Base input);
...
Derived objDerived;
UseBase(objDerived); // copy of objDerived will be sliced and sent
这两种情况下,编译器都是只复制派生类对象的基类部分,而不是复制整个对象。这种无意间裁减数据,导致 Derived 变成 Base 的行为称为切除(slicing)。
要避免切除问题,不要按值传递参数,而应以指向基类的指针或 const 引用的方式传递。参《C++ 多态》笔记第 1 节。
3.3 赋值关系
如下三条关系的根本原因在 3.1 节中已讲述。
派生类对象可以赋值给基类对象,反之则不行。
因为派生类对象数据成员比基类对象数据成员多。将派生类对象赋值给基类对象,基类对象能够得到所有数据成员的值。反过来,将基类对象赋值给派生类对象,派生类对象中部分数据成员无法取得合适的值,因此赋值失败。
派生类指针可以赋值给基类指针,反之则不行。
因为派生类指针所指向内存块比基类指针所指向内存块大。基类指针可以指向派生类对象,取基类大小的内存即可。反过来,派生类指针若指向基类对象,势必会造成内存越界。
派生类对象可以赋值给基类引用,反之则不行。
因为派生类对象比基类对象空间大。将派生类对象赋值给基类引用,基类引用表示派生类对象中的基类部分,多余部分舍弃即可。反过来,显然不行。
如下:
Base base;
Derived derived;
base = derived; // 正确
derived = base; // 错误
Base *pbase = &derived; // 正确
Derived *pderived = &base; // 错误
Base &rbase = derived; // 正确
Derived &rderived = base; // 错误
4. 多继承
派生类继承多个基类的特征称作多继承。如对鸭嘴兽来说。鸭嘴兽具备哺乳动物、鸟类和爬行动物的特征,那么鸭嘴兽可以继承哺乳动物、鸟类和爬行动物这三个基类。代码形如:
class Platypus: public Mammal, public Reptile, public Bird
{
// ... platypus members
};
5. 禁止继承
从 C++11 起,编译器支持限定符 final。被声明为 final 的类不能用作基类,因此禁止继承。
修改记录
2019-05-20 V1.0 初稿
2020-02-29 V1.1 修改例程错误