继承和派生
面向对象编程的四个基本概念
-
抽象:通过抽象类或接口定义对象的核心特性。
-
封装:用类来封装数据和操作,隐藏内部实现,提供公共接口。
-
继承:通过继承复用已有类的特性,构造新的类。
-
多态(动态绑定):通过动态绑定在运行时决定调用的是基类还是派生类的函数。
继承的本质
继承允许我们定义类之间的关系,继承公共的部分,只在派生类中特化不同的部分。通过继承:
-
吸收:派生类可以直接使用基类的功能。
-
改造:派生类可以重写基类的成员函数,实现多态。
-
新增:派生类可以添加新的成员和方法。
继承种类
-
按父类个数:单继承、多继承。
-
按继承方式:公有继承、保护继承、私有继承。
继承语法:
class Derived : public Base { |
继承方式与访问权限
继承方式:控制类之间的关系(public
、protected
、private
),影响派生类对基类成员的可见性。
继承方式 | 基类 public 成员 |
基类 protected 成员 |
基类 private 成员 |
---|---|---|---|
public |
public |
protected |
不可访问 |
**protected ** |
protected |
protected |
不可访问 |
private |
private |
private |
不可访问 |
访问权限:控制类内成员的访问权限,表现类的封装性(同样使用 public
、protected
、private
关键字)。
访问权限 | public | protected | private |
---|---|---|---|
对本类 | 可见 | 可见 | 可见 |
对子类 | 可见 | 可见 | 不可见 |
对外部(调用方) | 可见 | 不可见 | 不可见 |
派生类构造函数
基类构造函数不被继承:派生类需要重新定义自己的构造函数。
构造函数初始化列表:用于基类和派生类成员的初始化,执行顺序是先初始化基类,再初始化派生类成员。
class Base { |
派生类构造函数连锁调用
派生类的构造函数初始化顺序为:先调用基类构造函数,再初始化派生类成员。通过初始化列表传递参数。
(这个列表并不决定类中成员初始化的顺序,因为初始化顺序总是按照成员在类定义中的声明顺序进行)
class Base { |
对象结构
普通继承中的对象结构
继承中的访问控制: 在普通继承中,子类继承了父类的 private
成员,但这些成员对子类来说是不可访问的。虽然子类不能直接操作这些 private
成员,但它们仍然是子类对象的一部分,可以通过查看子类的大小来验证。
对象结构: 在继承关系中,子类对象包含父类的成员,即使是私有成员也会占据内存空间。因此,子类对象的内存结构是由父类的成员和子类自己的成员共同构成的。
基类与派生类的赋值切换
对象切割(Slicing): 当派生类的对象赋值给基类的对象、指针或引用时,只会保留基类的成员,派生类特有的成员会被切割掉。这就是所谓的对象切割。
赋值规则:
-
派生类可以赋值给基类:派生类对象可以赋值给基类对象、指针或引用,赋值时派生类的额外成员会被切割。
-
基类对象不能赋值给派生类:基类对象不能直接赋值给派生类对象,因为基类缺少派生类特有的成员。
指针转换:
-
强制类型转换:基类的指针可以通过强制类型转换赋值给派生类的指针,但这要求基类的指针实际指向派生类对象,否则会导致越界访问。
-
安全转换:如果基类是多态类型,可以使用
dynamic_cast
来进行类型转换,确保转换是安全的,并避免非法访问派生类的成员。
多继承中的对象结构
底层子类对象中,分别继承了中间层父类从顶层父类继承而来的成员变量,因此内存模型中含有两份底层父类的成员变量。
菱形继承
菱形继承模型: 在菱形继承中,一个父类被两个子类继承,而这两个子类又被一个新的子类继承。这种继承关系构成了一个菱形的结构图。
二义性问题: 当两个子类继承了同一个基类的成员时,在最下层的子类中访问这些共同的成员会产生二义性。此时,需要通过域运算符(::
)来明确指定使用的是哪个基类的成员。
虚继承解决二义性: 为了解决菱形继承中的二义性问题,可以通过虚继承来强制指定基类为虚基类。在定义派生类时,使用 virtual
关键字标记继承的基类,这样在最终的子类中,这些共同的基类成员只会有一份拷贝,避免了二义性。
虚继承与多态: 关于虚继承的详细机制以及与多态的关系,可以参考C++多态。
组合与继承的区别
组合(Composition): 组合是一种 has-a
的关系,即一个对象包含另一个对象。组合通过将多个类组合在一起,实现代码复用。例如,类B中包含类A的对象,表示类B包含了类A的一部分功能。组合通常通过接口暴露功能,而隐藏实现细节,因此依赖关系较弱,耦合度低。
继承(Inheritance): 继承是一种 is-a
的关系,表示派生类是基类的一种具体化。继承的依赖关系较强,耦合度较高,因为派生类需要了解并使用基类的部分实现细节,甚至可能会修改基类的行为。当基类发生变化时,所有的派生类都会受到影响。因此,继承在维护性和封装性方面可能不如组合。
选择依据:
-
组合优先:当几个类之间的关联不大,且只需要使用其中的一部分功能时,优先选择组合。组合的低耦合性使得代码更具封装性和可维护性。
-
继承适用场景:当存在强关联关系,且需要在子类中修改或扩展基类的功能时,可以选择继承。继承在多态实现中也不可或缺。
-
综合考虑:如果两者都可以选择,优先选择组合,因为它在保证代码独立性和模块化方面更具优势。