C++继承
在 C++ 中,继承(Inheritance) 是面向对象编程的三大特性之一(封装、继承、多态),它允许一个类(派生类/子类)继承另一个类(基类/父类)的成员(成员变量和成员函数),并可以在此基础上添加新成员或重写原有成员,实现代码复用和功能扩展。
一、继承的基本概念
- 基类(Base Class):被继承的类,也称为父类。
- 派生类(Derived Class):继承基类的类,也称为子类。
- 继承关系:派生类拥有基类的非私有成员(根据访问修饰符规则),同时可以定义自己的新成员。
二、继承的语法
通过 class 派生类名 : 继承方式 基类名 声明继承关系,其中继承方式决定基类成员在派生类中的访问权限。
// 基类(父类) |
三、继承方式与访问权限
继承方式(public、protected、private)会影响基类成员在派生类中的访问权限,规则如下:
| 基类成员权限 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | 派生类中为 public | 派生类中为 protected | 派生类中为 private |
| protected | 派生类中为 protected | 派生类中为 protected | 派生类中为 private |
| private | 派生类中不可访问 | 派生类中不可访问 | 派生类中不可访问 |
四、派生类的构造与析构
派生类构造函数最重要的职责之一是确保其基类子对象得到正确的初始化。
A. 显式调用基类构造函数(推荐)
如果基类没有默认构造函数(即基类定义了带参数的构造函数,但没有无参构造函数),或者你需要传递特定参数给基类,那么派生类必须在初始化列表中显式调用基类的构造函数。class Base {
public:
Base(int val) {
std::cout << "Base constructed with value: " << val << std::endl;
}
// 注意:如果 Base 只有这一个构造函数,Derived 必须显式调用它
};
class Derived : public Base {
public:
Derived(int x, int y)
// 显式调用基类构造函数,并传入所需的参数 x
: Base(x), m_data(y)
{
std::cout << "Derived constructed." << std::endl;
}
private:
int m_data;
};
// 调用:Derived obj(10, 20);
// 构造顺序:Base(10) -> m_data(20) -> Derived body
B. 隐式调用基类默认构造函数
如果基类有可访问的默认构造函数(无参数的构造函数),并且派生类没有在初始化列表中显式调用任何基类构造函数,那么编译器将自动调用基类的默认构造函数。class Base {
public:
Base() {
std::cout << "Base default constructed." << std::endl;
}
};
class Derived : public Base {
public:
// 派生类没有显式调用 Base()
Derived() {
std::cout << "Derived constructed." << std::endl;
}
// 编译器会自动在初始化列表中添加 : Base()
};
// 调用:Derived obj;
// 构造顺序:Base() -> Derived body
五、虚拟继承
虚拟继承是 C++ 中一种特殊的继承机制,主要目的是解决多重继承环境下出现的菱形继承问题。
1. 菱形继承问题
假设有四个类,它们形成了如下的继承结构(像一个菱形):
- 顶层基类 (Grandparent):
Base - 中间派生类 (Parents):
Derived1和Derived2,它们都继承自Base。 - 底层派生类 (Child):
FinalDerived,它同时继承自Derived1和Derived2。Base
/ \
Derived1 Derived2
\ /
FinalDerived
问题所在:
如果 Base 类中有一个成员变量 data 或成员函数 func():
FinalDerived通过Derived1继承了一份Base::data。FinalDerived又通过Derived2继承了另一份Base::data。
结果是,FinalDerived 对象中包含了两份 Base 子对象,导致:
- 内存浪费: 存储了两份相同的基类数据。
- 歧义性: 当尝试通过
FinalDerived对象访问data时,编译器不知道应该访问哪一份Base::data,从而产生歧义错误。
2. 虚拟继承的作用与语法
虚拟继承就是解决这个问题的机制。它确保无论基类在继承体系中出现多少次,派生类的对象中只包含一个该基类的子对象。
在派生类继承基类时,使用 virtual 关键字。class Base {
public:
int data;
Base(int d) : data(d) { /* ... */ }
};
// Derived1 虚拟继承 Base
class Derived1 : virtual public Base {
public:
Derived1(int d) : Base(d) { /* ... */ }
};
// Derived2 虚拟继承 Base
class Derived2 : virtual public Base {
public:
Derived2(int d) : Base(d) { /* ... */ }
};
// FinalDerived 正常继承 Derived1 和 Derived2
class FinalDerived : public Derived1, public Derived2 {
public:
// 构造函数必须显式调用 Base 的构造函数 (见下方 3.C)
FinalDerived(int d) : Base(d), Derived1(d), Derived2(d) { /* ... */ }
};
在使用了虚拟继承后,FinalDerived 对象中将只包含一个 Base 子对象。
3. 虚拟继承的内存和构造机制
虚拟继承虽然解决了歧义性,但也引入了特殊的内存布局和构造规则:
A. 内存布局
虚拟继承的基类(例如 Base)被称为虚基类。
- 虚基类子对象会被放置在派生类对象内存的一个特殊区域(通常是对象的末尾)。
- 派生类(例如
Derived1和Derived2)会通过一个虚基类指针或 虚基类表来间接访问这个唯一的共享Base子对象。
B. 构造规则的核心变化
在虚拟继承体系中,为了确保虚基类只被构造一次,C++ 引入了特殊的构造规则:
虚基类(例如 Base)的构造,总是由继承体系中最底层的派生类(例如 FinalDerived)负责调用。
C. 构造函数的职责
- 虚基类的直接派生类 (
Derived1,Derived2): 它们仍然需要在初始化列表中调用虚基类(Base)的构造函数。但是,当创建FinalDerived对象时,编译器会忽略这些中间类的调用。 - 最底层派生类 (
FinalDerived): 必须显式地在自己的初始化列表中调用虚基类(Base)的构造函数,否则编译器会尝试调用Base的默认构造函数。这是唯一真正执行构造Base的调用。
总结: 最底层派生类负责提供虚基类所需的所有构造参数,以保证它只被构造一次。
六.多继承的构造与析构顺序
1.构造顺序规则
- 虚基类 的构造:
- 最优先构造。无论虚基类出现在继承列表中哪个位置,它们总是最先被构造。
- 如果存在多个虚基类,它们的构造顺序取决于它们在继承列表中的声明顺序。
- 注意: 虚基类只会被最底层的派生类构造一次。
- 非虚基类的构造:
- 按照它们在派生类的继承列表中出现的顺序依次构造。
- 与派生类构造函数的初始化列表中的顺序无关。
- 派生类成员对象的构造:
- 按照它们在派生类定义中的声明顺序依次构造。
- 与派生类构造函数的初始化列表中的顺序无关。
- 派生类自身构造:
- 最后,执行派生类构造函数花括号
{}内部的代码。
- 最后,执行派生类构造函数花括号
示例:
// --- 虚基类 1 ---
class Engine {
public:
Engine(int hp) { std::cout << "1. Engine(" << hp << ") constructed." << std::endl; }
~Engine() { std::cout << "10. Engine destructed." << std::endl; }
};
// --- 虚基类 2 ---
class Chassis {
public:
Chassis(const std::string& type) { std::cout << "2. Chassis(" << type << ") constructed." << std::endl; }
~Chassis() { std::cout << "9. Chassis destructed." << std::endl; }
};
// --- 成员类 ---
class Tire {
public:
Tire() { std::cout << "6. Tire constructed." << std::endl; }
~Tire() { std::cout << "5. Tire destructed." << std::endl; }
};
// --- 中间类 1 (Car) ---
class Car : virtual public Engine, virtual public Chassis { // 虚继承两个基类
public:
// 必须调用虚基类构造函数,但只有最低层会真正执行
Car(int hp, const std::string& type) : Engine(hp), Chassis(type) {
std::cout << "3. Car constructed." << std::endl;
}
~Car() { std::cout << "8. Car destructed." << std::endl; }
};
// --- 中间类 2 (Truck) ---
class Truck : virtual public Engine, virtual public Chassis {
public:
Truck(int hp, const std::string& type) : Engine(hp * 2), Chassis(type) { // 注意不同的参数
std::cout << "4. Truck constructed." << std::endl;
}
~Truck() { std::cout << "7. Truck destructed." << std::endl; }
};
// --- 最终派生类 ---
// 继承列表顺序:先 Truck, 后 Car
class Amphibian : public Truck, public Car {
private:
Tire m_tires[4]; // 成员对象
public:
// 最底层类必须调用所有虚基类的构造函数!
Amphibian()
// 关键点 1: 虚基类构造 (按继承列表 VBase1, VBase2 顺序)
: Engine(300),
Chassis("Floating"),
// 关键点 2: 非虚基类构造 (按继承列表 Truck, Car 顺序)
Truck(150, "Heavy"),
Car(75, "Light")
{
std::cout << "7. Amphibian constructed." << std::endl;
}
~Amphibian() { std::cout << "4. Amphibian destructed." << std::endl; }
};
int main() {
std::cout << "--- Starting Construction ---\n";
Amphibian vehicle;
std::cout << "--- Starting Destruction ---\n";
return 0;
}
输出:--- Starting Construction ---
1. Engine(300) constructed. <-- VBase1 (Amphibian调用)
2. Chassis(Floating) constructed. <-- VBase2 (Amphibian调用)
3. Truck constructed. <-- Non-VBase1 (继承列表顺序)
4. Car constructed. <-- Non-VBase2 (继承列表顺序)
5. Tire constructed. (x4) <-- 成员对象 (按声明顺序)
6. Amphibian constructed. <-- 派生类自身
2.析构顺序规则
- 派生类自身析构:
- 首先执行派生类析构函数花括号
{}内部的代码。
- 首先执行派生类析构函数花括号
- 派生类成员对象析构:
- 按照它们在派生类定义中的声明顺序的逆序依次析构。
- 非虚基类的析构:
- 按照它们在派生类继承列表中的逆序依次析构。
- (在上面的例子中,先析构 Base2,后析构 Base1)。
- 虚基类的析构:
- 最后析构。虚基类总是在所有非虚基类和成员之后才被析构。
上述示例输出:--- Starting Destruction ---
4. Amphibian destructed. <-- 派生类自身
5. Tire destructed. (x4) <-- 成员对象 (逆序)
6. Car destructed. <-- Non-VBase2 (继承列表逆序)
7. Truck destructed. <-- Non-VBase1 (继承列表逆序)
8. Chassis destructed. <-- VBase2 (虚基类逆序)
9. Engine destructed. <-- VBase1 (虚基类逆序)