C++继承

继承和派生

面向对象编程的四个基本概念

  1. 抽象:通过抽象类或接口定义对象的核心特性。

  2. 封装:用类来封装数据和操作,隐藏内部实现,提供公共接口。

  3. 继承:通过继承复用已有类的特性,构造新的类。

  4. 多态(动态绑定):通过动态绑定在运行时决定调用的是基类还是派生类的函数。

继承的本质

继承允许我们定义类之间的关系,继承公共的部分,只在派生类中特化不同的部分。通过继承:

  1. 吸收:派生类可以直接使用基类的功能。

  2. 改造:派生类可以重写基类的成员函数,实现多态。

  3. 新增:派生类可以添加新的成员和方法。

继承种类

  1. 按父类个数:单继承、多继承。

  2. 按继承方式:公有继承、保护继承、私有继承。

继承语法

class Derived : public Base {
// 新增成员声明
};

继承方式与访问权限

继承方式:控制类之间的关系(publicprotectedprivate),影响派生类对基类成员的可见性。

继承方式 基类 public 成员 基类 protected 成员 基类 private 成员
public public protected 不可访问
**protected ** protected protected 不可访问
private private private 不可访问

访问权限:控制类内成员的访问权限,表现类的封装性(同样使用 publicprotectedprivate 关键字)。

访问权限 public protected private
对本类 可见 可见 可见
对子类 可见 可见 不可见
对外部(调用方) 可见 不可见 不可见

派生类构造函数

基类构造函数不被继承:派生类需要重新定义自己的构造函数。

构造函数初始化列表:用于基类和派生类成员的初始化,执行顺序是先初始化基类,再初始化派生类成员。

class Base {
public:
Base(int x) { std::cout << "Base constructor: " << x << "\n"; }
};

class Derived : public Base {
int y;
public:
Derived(int x, int y_val) : Base(x), y(y_val) {
std::cout << "Derived constructor: " << y << "\n";
}
};

派生类构造函数连锁调用

派生类的构造函数初始化顺序为:先调用基类构造函数,再初始化派生类成员。通过初始化列表传递参数。

(这个列表并不决定类中成员初始化的顺序,因为初始化顺序总是按照成员在类定义中的声明顺序进行)

class Base {
public:
Base(int x) { std::cout << "Base constructor: " << x << "\n"; }
};

class Intermediate : public Base {
public:
Intermediate(int x) : Base(x) {
std::cout << "Intermediate constructor\n";
}
};

class Final : public Intermediate {
int z;
public:
Final(int x, int z_val) : Intermediate(x), z(z_val) {
std::cout << "Final constructor: " << z << "\n";
}
};

对象结构

普通继承中的对象结构

继承中的访问控制: 在普通继承中,子类继承了父类的 private 成员,但这些成员对子类来说是不可访问的。虽然子类不能直接操作这些 private 成员,但它们仍然是子类对象的一部分,可以通过查看子类的大小来验证。

对象结构: 在继承关系中,子类对象包含父类的成员,即使是私有成员也会占据内存空间。因此,子类对象的内存结构是由父类的成员和子类自己的成员共同构成的。

基类与派生类的赋值切换

对象切割(Slicing): 当派生类的对象赋值给基类的对象、指针或引用时,只会保留基类的成员,派生类特有的成员会被切割掉。这就是所谓的对象切割。

赋值规则

  • 派生类可以赋值给基类:派生类对象可以赋值给基类对象、指针或引用,赋值时派生类的额外成员会被切割。

  • 基类对象不能赋值给派生类:基类对象不能直接赋值给派生类对象,因为基类缺少派生类特有的成员。

指针转换

  • 强制类型转换:基类的指针可以通过强制类型转换赋值给派生类的指针,但这要求基类的指针实际指向派生类对象,否则会导致越界访问。

  • 安全转换:如果基类是多态类型,可以使用 dynamic_cast 来进行类型转换,确保转换是安全的,并避免非法访问派生类的成员。

多继承中的对象结构

底层子类对象中,分别继承了中间层父类从顶层父类继承而来的成员变量,因此内存模型中含有两份底层父类的成员变量。

菱形继承

菱形继承模型: 在菱形继承中,一个父类被两个子类继承,而这两个子类又被一个新的子类继承。这种继承关系构成了一个菱形的结构图。

二义性问题: 当两个子类继承了同一个基类的成员时,在最下层的子类中访问这些共同的成员会产生二义性。此时,需要通过域运算符(::)来明确指定使用的是哪个基类的成员。

虚继承解决二义性: 为了解决菱形继承中的二义性问题,可以通过虚继承来强制指定基类为虚基类。在定义派生类时,使用 virtual 关键字标记继承的基类,这样在最终的子类中,这些共同的基类成员只会有一份拷贝,避免了二义性。

虚继承与多态: 关于虚继承的详细机制以及与多态的关系,可以参考C++多态

组合与继承的区别

组合(Composition): 组合是一种 has-a 的关系,即一个对象包含另一个对象。组合通过将多个类组合在一起,实现代码复用。例如,类B中包含类A的对象,表示类B包含了类A的一部分功能。组合通常通过接口暴露功能,而隐藏实现细节,因此依赖关系较弱,耦合度低。

继承(Inheritance): 继承是一种 is-a 的关系,表示派生类是基类的一种具体化。继承的依赖关系较强,耦合度较高,因为派生类需要了解并使用基类的部分实现细节,甚至可能会修改基类的行为。当基类发生变化时,所有的派生类都会受到影响。因此,继承在维护性和封装性方面可能不如组合。

选择依据

  • 组合优先:当几个类之间的关联不大,且只需要使用其中的一部分功能时,优先选择组合。组合的低耦合性使得代码更具封装性和可维护性。

  • 继承适用场景:当存在强关联关系,且需要在子类中修改或扩展基类的功能时,可以选择继承。继承在多态实现中也不可或缺。

  • 综合考虑:如果两者都可以选择,优先选择组合,因为它在保证代码独立性和模块化方面更具优势。

------------------------------- 本文结束啦❤感谢您阅读-------------------------------
赞赏一杯咖啡