一、 C++多继承基础语法与同名成员处理
1.1 多继承语法
C++允许一个类继承多个类。
class Base1 { public: int m_A; };
class Base2 { public: int m_B; };
// 多继承语法:class 子类 : 继承方式 父类1, 继承方式 父类2 …
class Son : public Base1, public Base2 { public: int m_C; };
1.2 同名成员引发的二义性
如果父类 Base1 和 Base2 中存在同名成员(例如都有 int m_A;),子类 Son 访问该成员时,编译器无法确定是哪个父类的成员,就会产生二义性(Ambiguity)。
-
解决方式:访问时必须加上作用域限定符 ::。
Son s;
// s.m_A = 100; // 报错:引用不明确(二义性)
s.Base1::m_A = 100; // 成功,指定访问 Base1 的 m_A
s.Base2::m_A = 200; // 成功,指定访问 Base2 的 m_A
1.3 同名成员函数的隐藏
如果子类出现了和父类同名的成员函数,子类中的同名函数会隐藏掉父类中所有的同名成员函数(包括重载版本)
class Base {
public:
void func() { cout << "Base func" << endl; }
void func(int a) { cout << "Base func(int)" << endl; }
};
class Son : public Base {
public:
void func() { cout << "Son func" << endl; }
};
// 调用
Son s;
s.func(); // 调用子类的 func()
// s.func(100); // 报错!父类带参的 func(int) 被隐藏了
s.Base::func(100); // 成功,必须加作用域
补充:静态成员(static)遇到同名情况,处理方式与非静态成员完全一致。
1.4 继承方式对权限的影响
-
公有继承 (public):父类成员权限保持不变。
-
保护继承 (protected):父类中 public 和 protected 成员到子类中全变为 protected。
-
私有继承 (private):父类中 public 和 protected 成员到子类中全变为 private。
-
注意:无论哪种继承方式,父类中的 private 成员子类始终无法访问(编译器隐藏了,但实际上确实被继承了下来,占用内存)。
二、 继承中的对象模型与构造析构顺序
2.1 构造与析构顺序
-
构造顺序:先调用父类构造函数,再调用子类构造函数。
-
析构顺序:与构造相反,先调用子类析构函数,再调用父类析构函数。
2.2 查看对象模型
可以使用VS自带的开发人员命令提示工具查看类的内存布局:
cl /d1 reportSingleClassLayout类名 文件名.cpp
通过查看对象模型可知,父类中所有的非静态成员属性都会被继承到子类中。子类对象的大小(sizeof)通常包含了所有父类非静态数据成员加上子类自身数据成员的总和(注意内存对齐)。
三、 菱形继承(钻石继承)与虚继承
3.1 什么是菱形继承?
两个派生类继承同一个基类,同时又有某个类同时继承这两个派生类。形状如同钻石。
Animal (m_Age)
/ \\
Sheep Tuotuo
(m_Age) (m_Age)
\\ /
SheepTuo
引发的问题:
数据冗余:SheepTuo 继承了 Sheep 和 Tuotuo,而它们各自有一份 Animal 的数据,导致 SheepTuo 中保存了两份 m_Age。
二义性:访问 st.m_Age 时,编译器不知道取哪一份。
3.2 虚继承(virtual inheritance)
利用虚继承可以解决菱形继承问题。在继承前加上 virtual 关键字,此时 Animal 被称为虚基类。
class Animal { public: int m_Age; };
class Sheep : virtual public Animal {}; // 虚继承
class Tuotuo : virtual public Animal {}; // 虚继承
class SheepTuo : public Sheep, public Tuotuo {};
注意:要解决菱形继承的二义性,所有通向公共基类的路径都必须使用虚继承。如果只有 Sheep 虚继承,而 Tuotuo 普通继承,编译器依然无法合并数据,依旧存在两份 m_Age,直接访问会产生二义性。
3.3 虚继承的底层实现(vbptr 与 vbtable)
虚继承底层依赖:
-
vbptr (Virtual Base Pointer):虚基类指针。含有虚基类的对象中会多出一个指针(通常位于对象内存布局的最顶部或尾部),指向虚基类表。
-
vbtable (Virtual Base Table):虚基类表。记录了当前类到虚基类的偏移量。
-
访问虚基类成员时,需要通过 vbptr 查表计算地址,会有轻微的性能开销。
四、 虚继承下的初始化规则(最派生类负责制)
这是虚继承中最容易踩坑的地方:在虚继承中,虚基类的初始化由“最终派生类(Most Derived Class)”全权负责。中间类构造函数中对虚基类的初始化请求会被编译器强制忽略。
场景演示:
class Animal {
public:
int mAge;
Animal(int age) : mAge(age) { cout << "Animal构造, age=" << age << endl; } // 没有默认构造
};
class Sheep : virtual public Animal {
public:
Sheep() : Animal(18) { cout << "Sheep构造" << endl; } // 被忽略!
};
class Tuotuo : virtual public Animal {
public:
Tuotuo() : Animal(28) { cout << "Tuotuo构造" << endl; } // 被忽略!
};
class Student : public Sheep, public Tuotuo {
public:
// 【核心】必须显式初始化虚基类 Animal
// 传入的值将决定最终的 mAge
Student() : Animal(99), Sheep(), Tuotuo() {
cout << "Student构造, 最终 mAge=" << mAge << endl;
}
};
运行结果分析:
构造顺序:虚基类优先初始化 -> 然后按声明顺序初始化非虚基类。
首先执行 Student 构造函数中的 Animal(99),此时 mAge 被初始化为 99。
接着执行 Sheep(),内部尝试 Animal(18),但发现 Animal 已经被最派生类 Student 初始化了,编译器直接跳过。
接着执行 Tuotuo(),内部尝试 Animal(28),同样跳过。
最终 Student 对象中的 mAge 为 99。
关于“如果两个路径赋不同的值,子类继承哪一个”的解答:
都不会继承! 子类中的 mAge 值完全取决于最派生类(Student)构造函数中显式传递的参数。
-
如果最派生类没有显式初始化会怎样?
-
如果虚基类 Animal 有默认构造函数,则使用默认值(如 0)。
-
如果虚基类 Animal 没有默认构造函数(只提供了带参构造),则代码编译报错。提示 constructor for 'Student' must explicitly initialize the base class 'Animal' which does not have a default constructor。
-
补充:在虚继承机制下修改共享数据
因为只有一份 m_Age 数据,所以以下操作修改的是同一块内存:
Student st;
st.Sheep::mAge = 18; // 给共享的 mAge 赋值为 18
st.Tuotuo::mAge = 28; // 给同一个 mAge 赋值为 28
// 由于操作的是同一份数据,后一次赋值会覆盖前一次,最终 st.mAge == 28
五、 建议总结
实际开发不建议使用多继承。多继承会让代码结构极其复杂,容易引发二义性和内存布局混乱。
谨慎使用虚继承。虚继承虽然解决了数据冗余和二义性,但付出了性能代价(多了一个 vbptr 指针的空间开销,以及运行时通过查表间接寻址的时间开销)。且虚基类的初始化规则较难把控。
现代C++设计原则:优先使用组合(Composition)或接口抽象(Interface)来代替多重继承。如果确实需要多继承,建议只继承纯虚函数的接口类(类似于Java的interface),避免继承带有数据成员的实体类。



