C++学习笔记DAY4
C++ 面向对象二 —— 继承 · 详细学习笔记
每天我就这样补补补,,,
继承在我看来有点像便捷的CV工程(当然还是本质设计不是这样的hhh)
实际上继承的核心目的只有一个,建模‘is-a'关系+多态替换,就有点像dog is animal而不是dog cpoies Animal
具体的我们后面再看吧
目录
- 继承的基本概念
- 权限修饰符详解
- 三种继承方式
- 继承中的对象模型
- 继承中的构造与析构顺序
- 继承中的同名成员处理
- 继承中的同名静态成员处理
- 多继承语法
- 菱形继承问题
- 虚继承——菱形继承的解决方案
- 虚继承内存模型深度剖析
- 综合案例:图形类
- 常见陷阱与要点
- 进阶拓展
- 速查表
1. 继承的基本概念
1.1 为什么需要继承
当多个类有相同的属性和方法时,会出现大量代码重复。继承可以提取共性到父类中,子类复用父类代码。
问题场景:
Dog 类 Cat 类
├ 属性:姓名 ├ 属性:姓名
├ 属性:年龄 ├ 属性:年龄
├ 属性:颜色 ├ 属性:品种
├ 行为:吃饭 ├ 行为:吃饭
├ 行为:睡觉 ├ 行为:睡觉
└ 行为:看家 └ 行为:抓老鼠
重复!重复!重复!
解决方案:提取共性到 Animal 父类
Animal(父类 / 基类 / 超类)
├ 属性:姓名
├ 属性:年龄
├ 行为:吃饭
└ 行为:睡觉
│
┌────────┴────────┐
│ │
Dog(子类) Cat(子类)
├ 属性:颜色 ├ 属性:品种
└ 行为:看家 └ 行为:抓老鼠
← 复用 + 扩展 → ← 复用 + 扩展 →
1.2 继承的定义
继承:允许一个类(派生类/子类)继承另一个类(基类/父类)的属性和行为,并可以添加自己的新成员。
基本语法:
class 子类 : 继承方式 父类 {
// 子类新增的成员
};
完整入门案例:
#include <iostream>
#include <string>
using namespace std;
// ============ 父类(基类) ============
class Animal {
public:
string m_Name; // 姓名
int m_Age; // 年龄
void eat() {
cout << m_Name << " 在吃饭" << endl;
}
void sleep() {
cout << m_Name << " 在睡觉" << endl;
}
};
// ============ 子类1:Dog ============
class Dog : public Animal {
public:
string m_Color; // Dog 特有属性
void watchHouse() { // Dog 特有行为
cout << m_Name << " 在看家" << endl;
}
};
// ============ 子类2:Cat ============
class Cat : public Animal {
public:
string m_Breed; // Cat 特有属性
void catchMouse() { // Cat 特有行为
cout << m_Name << " 在抓老鼠" << endl;
}
};
int main() {
Dog dog;
dog.m_Name = "旺财"; // 从父类继承的属性
dog.m_Age = 3; // 从父类继承的属性
dog.m_Color = "黄色"; // 子类自己的属性
dog.eat(); // 从父类继承的方法
dog.sleep();
dog.watchHouse(); // 子类自己的方法
cout << "---" << endl;
Cat cat;
cat.m_Name = "咪咪";
cat.m_Age = 2;
cat.m_Breed = "橘猫";
cat.eat();
cat.sleep();
cat.catchMouse();
return 0;
}
1.3 继承的作用
| 作用 | 说明 |
|---|---|
| 代码复用 | 子类自动获得父类的所有成员,避免重复编码 |
| 扩展性 | 子类可以添加新的属性和方法,或重写父类方法 |
| 层次化设计 | 类之间形成层次结构,便于理解和维护 |
| 多态基础 | 继承是实现多态的前提条件 |
1.4 继承使用场景
当多个类之间有相同的属性和方法时,将这些共同部分抽取到基类中,其他类通过继承复用。
案例分析:教师与学生
Person(父类)
├ 姓名
├ 年龄
├ 性别
├ 吃饭()
└ 睡觉()
│
├── Teacher(子类)
│ └ 工号、教书()
│
└── Student(子类)
└ 学号、学习()
class Person {
public:
string m_Name;
int m_Age;
string m_Gender;
void eat() { cout << m_Name << " 在吃饭" << endl; }
void sleep() { cout << m_Name << " 在睡觉" << endl; }
};
class Teacher : public Person {
public:
int m_TeacherId;
void teach() { cout << m_Name << " 在教书" << endl; }
};
class Student : public Person {
public:
int m_StudentId;
void study() { cout << m_Name << " 在学习" << endl; }
};
1.5 继承的扩展——重写
当父类的功能不能满足子类需求时,子类可以重写父类的方法:
class Animal {
public:
virtual void speak() {
cout << "动物在叫" << endl;
}
};
class Dog : public Animal {
public:
void speak() override { // 重写父类方法
cout << "旺旺叫!" << endl;
}
};
注意:重写是多态的前提。这里用到了虚函数,仅占位,后面还会详细展开
2. 权限修饰符详解
2.1 三种权限修饰符
C++ 有三种访问权限修饰符,控制类成员的访问范围:
| 修饰符 | 本类 | 子孙类(派生类) | 外部类(类外) | 权限大小 |
|---|---|---|---|---|
public |
✓ | ✓ | ✓ | 最高 |
protected |
✓ | ✓ | ✗ | 中等 |
private |
✓ | ✗ | ✗ | 最低 |
权限大小:private < protected < public
私藏自用,保护传家,公有共享
2.2 代码验证
#include <iostream>
using namespace std;
class Person {
public:
int m_A; // 公共权限——任意位置可访问
protected:
int m_B; // 保护权限——类内和子类可访问
private:
int m_C; // 私有权限——仅类内可访问
public:
void func() {
m_A = 10; // 本类内可以访问 public
m_B = 20; // 本类内可以访问 protected
m_C = 30; // 本类内可以访问 private
}
};
// 子类
class Student : public Person {
public:
void test() {
m_A = 100; // ✓ 子类可以访问父类的 public
m_B = 200; // ✓ 子类可以访问父类的 protected
// m_C = 300; // ✗ 子类不能访问父类的 private
}
};
int main() {
Person p;
p.m_A = 1; // ✓ 类外可以访问 public
// p.m_B = 2; // ✗ 类外不能访问 protected
// p.m_C = 3; // ✗ 类外不能访问 private
return 0;
}
2.3 protected 的特殊价值
protected 是继承体系中最重要的权限修饰符:
┌─────────────────────────────────────────────────┐
│ protected 的意义 │
├─────────────────────────────────────────────────┤
│ 对子类可见(像 public) │
│ 对外部不可见(像 private) │
│ │
│ → 专门为继承设计! │
│ → 父类中希望子类能访问但不希望外部访问的成员 │
│ 用 protected 修饰 │
└─────────────────────────────────────────────────┘
实际应用:
class BankAccount {
public:
void deposit(double amount) { // 存款——外部可用
m_Balance += amount;
}
double getBalance() { // 查询余额——外部可用
return m_Balance;
}
protected:
double m_Balance; // 余额——子类可用,外部不可直接修改
void applyInterest() { // 计算利息——子类可用
m_Balance *= 1.03;
}
private:
int m_AccountPassword; // 密码——只有本类可用
};
class SavingsAccount : public BankAccount {
public:
void yearlyUpdate() {
applyInterest(); // ✓ 子类可以调用 protected 方法
// m_AccountPassword = 123; // ✗ 子类不能访问 private
}
};
int main() {
SavingsAccount sa;
sa.deposit(1000); // ✓ public
// sa.m_Balance = 0; // ✗ protected
// sa.applyInterest(); // ✗ protected
return 0;
}
2.4 struct vs class 的默认权限
| 关键字 | 默认权限(不写修饰符时) |
|---|---|
class |
private |
struct |
public |
class A {
int x; // 默认 private
};
struct B {
int x; // 默认 public
};
// 继承的默认也是一样的:
class C : A {}; // 默认 private 继承
struct D : B {}; // 默认 public 继承
最佳实践:永远显式写明权限修饰符,不依赖默认值。
3. 三种继承方式
3.1 继承方式概述
C++ 支持三种继承方式,它们的区别在于:父类成员在子类中的最高访问权限是什么。
| 继承方式 | 父类 public 成员 | 父类 protected 成员 | 父类 private 成员 | 效果 |
|---|---|---|---|---|
| public 继承 | 子类中仍为 public |
子类中仍为 protected |
子类中不可访问 | 权限不变(最常用) |
| protected 继承 | 子类中降为 protected |
子类中仍为 protected |
子类中不可访问 | public → protected |
| private 继承 | 子类中降为 private |
子类中降为 private |
子类中不可访问 | 全部降为 private |
3.2 核心规则——权限降级
继承方式决定了「降级上限」:
public 继承 → 权限不变(最宽松)
protected 继承 → public 降为 protected,其余不变
private 继承 → 全部降为 private(最严格)
记忆口诀:「取小值」
父类成员权限 和 继承方式,取两者中更严格的那个。
权限降级图解:
=== public 继承(全部保持)===
父类成员 子类中
public ──→ public (不变)
protected ──→ protected (不变)
private ──→ 不可访问 (隐藏)
=== protected 继承(public 降级)===
父类成员 子类中
public ──→ protected (降级!)
protected ──→ protected (不变)
private ──→ 不可访问 (隐藏)
=== private 继承(全部降级)===
父类成员 子类中
public ──→ private (降级!)
protected ──→ private (降级!)
private ──→ 不可访问 (隐藏)
这个逻辑应该挺好理顺的
3.3 公有继承(public)
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son : public Base {
public:
void test() {
m_A = 10; // public 成员,继承后仍为 public
m_B = 20; // protected 成员,继承后仍为 protected
m_C = 30; // 这里会报错,private 成员,子类不可访问
}
};
int main() {
Son s;
s.m_A = 1; // 子类中 m_A 仍是 public,类外可访问
s.m_B = 2; // protected,类外不可访问
return 0;
}
公有继承表示 "is-a" 关系:子类就是一种父类。这是最常用的继承方式。
3.4 保护继承(protected)
class Son : protected Base {
public:
void test() {
m_A = 10; // ✓ public 成员,继承后降为 protected
m_B = 20; // ✓ protected 成员,继承后仍为 protected
// m_C = 30; // ✗ private,不可访问
}
};
int main() {
Son s;
// s.m_A = 1; // ✗ m_A 降为 protected,类外不可访问了!
return 0;
}
3.5 私有继承(private)
class Son : private Base {
public:
void test() {
m_A = 10; // ✓ public 成员,继承后降为 private
m_B = 20; // ✓ protected 成员,继承后降为 private
// m_C = 30; // ✗ private,不可访问
}
};
// 再往下继承一层
class GrandSon : public Son {
public:
void test() {
// m_A = 10; // ✗ m_A 已降为 private,孙类不可访问
// m_B = 20; // ✗ m_B 已降为 private,孙类不可访问
}
};
私有继承表示 "has-a" / "用...实现" 关系:子类用父类来实现自己的功能,但不暴露继承关系。
3.6 三种继承方式对比总结
┌──────────────────────────────────────────────────────────────┐
│ 继承方式对比速查表 │
├──────────┬──────────┬──────────────┬──────────────┬──────────┤
│ 继承方式 │ 语义关系 │ public→子类 │ protected→子类 │ 使用频率 │
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
│ public │ is-a │ public │ protected │ ★★★★★ │
│ protected│ 实现继承 │ protected │ protected │ ★☆☆☆☆ │
│ private │ has-a │ private │ private │ ★★☆☆☆ │
└──────────┴──────────┴──────────────┴──────────────┴──────────┘
最佳实践:95% 以上的场景使用
public继承。只有在需要限制接口暴露时才考虑 protected/private 继承。
4. 继承中的对象模型
4.1 子类对象的内存布局
子类对象包含了从父类继承的所有成员(包括 private),加上自己的新增成员。
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son : public Base {
public:
int m_D;
};
内存布局:
Son 对象内存:
┌─────────────┐
│ m_A (4字节) │ ← 从 Base 继承(public)
├─────────────┤
│ m_B (4字节) │ ← 从 Base 继承(protected)
├─────────────┤
│ m_C (4字节) │ ← 从 Base 继承(private,隐藏但存在!)
├─────────────┤
│ m_D (4字节) │ ← Son 自己的成员
└─────────────┘
sizeof(Son) = 16(4 个 int)
4.2 关键结论:private 成员也被继承了
#include <iostream>
using namespace std;
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son : public Base {
public:
int m_D;
};
int main() {
cout << "sizeof(Base) = " << sizeof(Base) << endl; // 12
cout << "sizeof(Son) = " << sizeof(Son) << endl; // 16
// Son 中包含了 m_A + m_B + m_C + m_D = 16 字节
// 说明 m_C(private)也被继承了!只是编译器隐藏了访问
return 0;
}
输出:
sizeof(Base) = 12
sizeof(Son) = 16
重要结论:父类中私有成员也是被子类继承下去了,只是由编译器隐藏后访问不到。它在内存中确实存在,只是编译器在语法层面禁止访问。
4.3 内存对齐
编译器可能会在成员之间插入填充字节,使成员地址对齐到自然边界(通常是其大小的倍数):
class Base {
char m_C; // 1字节,但会填充到 sizeof(int)
int m_A; // 4字节
};
class Son : public Base {
double m_D; // 8字节
};
// sizeof(Son) 可能不是简单的 1+4+8=13
// 而是由于对齐变成更大的值
内存对齐规则因编译器和平台而异,使用
sizeof运算符获取实际大小。
4.4 查看类内存布局的工具
使用编译器命令查看类的内存布局(以 GCC/Clang 为例):
# GCC
g++ -fdump-lang-class=layout.txt main.cpp
# Clang
clang++ -Xclang -fdump-record-layouts main.cpp
# MSVC (Visual Studio)
# 项目属性 → C/C++ → 命令行 → 添加 /d1reportSingleClassLayoutSon
5. 继承中的构造与析构顺序
5.1 构造与析构顺序
子类对象创建时,会先调用父类的构造函数,再调用子类的构造函数。析构顺序与构造相反。
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base 构造函数!" << endl; }
~Base() { cout << "Base 析构函数!" << endl; }
};
class Son : public Base {
public:
Son() { cout << "Son 构造函数!" << endl; }
~Son() { cout << "Son 析构函数!" << endl; }
};
int main() {
cout << "=== 创建 Son 对象 ===" << endl;
Son s;
cout << "=== main 函数执行中 ===" << endl;
cout << "=== Son 对象即将销毁 ===" << endl;
return 0;
}
输出:
=== 创建 Son 对象 ===
Base 构造函数! ← 先构造父类
Son 构造函数! ← 再构造子类
=== main 函数执行中 ===
=== Son 对象即将销毁 ===
Son 析构函数! ← 先析构子类
Base 析构函数! ← 再析构父类
5.2 顺序图解
构造顺序(创建对象时):
Base() ──→ Son()
父类构造 子类构造
(先) (后)
析构顺序(销毁对象时):
~Son() ──→ ~Base()
子类析构 父类析构
(先) (后)
总结:析构顺序与构造完全相反
5.3 多层继承的构造析构顺序
class Grandpa {
public:
Grandpa() { cout << "Grandpa 构造" << endl; }
~Grandpa() { cout << "Grandpa 析构" << endl; }
};
class Father : public Grandpa {
public:
Father() { cout << "Father 构造" << endl; }
~Father() { cout << "Father 析构" << endl; }
};
class Son : public Father {
public:
Son() { cout << "Son 构造" << endl; }
~Son() { cout << "Son 析构" << endl; }
};
int main() {
Son s;
return 0;
}
输出:
Grandpa 构造 ← 最顶层父类先构造
Father 构造 ← 中间层
Son 构造 ← 最底层子类最后构造
Son 析构 ← 最底层子类先析构
Father 析构 ← 中间层
Grandpa 析构 ← 最顶层父类最后析构
规律:构造从最顶层父类开始,逐层向下到子类;析构从子类开始,逐层向上到最顶层父类。
5.4 为什么先构造父类?
子类对象的完整性依赖于父类部分先被正确初始化。子类构造函数可能需要使用父类的成员,如果父类还没构造,子类就可能访问到未初始化的数据。
这里也能感受到看到可以在子类修改父类public的内容
比喻:
建房子时,必须先打地基(父类),再建框架(子类)
拆房子时,必须先拆上层(子类),再拆地基(父类)
5.5 子类构造函数中调用父类构造函数
class Base {
public:
int m_A;
Base(int a) : m_A(a) { // 父类有参构造
cout << "Base(" << a << ") 构造" << endl;
}
};
class Son : public Base {
public:
int m_B;
// 通过初始化列表调用父类有参构造
Son(int a, int b) : Base(a), m_B(b) {
cout << "Son(" << a << ", " << b << ") 构造" << endl;
}
};
int main() {
Son s(10, 20);
cout << "m_A = " << s.m_A << ", m_B = " << s.m_B << endl;
return 0;
}
注意:如果父类没有默认构造函数(无参构造),子类构造函数必须在初始化列表中显式调用父类的有参构造。
6. 继承中的同名成员处理
6.1 同名成员问题
当子类与父类出现同名成员变量或同名成员函数时,子类会隐藏父类的同名成员。
6.2 同名成员变量
#include <iostream>
using namespace std;
class Base {
public:
int m_A = 100; // 父类的 m_A
};
class Son : public Base {
public:
int m_A = 200; // 子类的 m_A(同名!隐藏了父类的 m_A)
};
int main() {
Son s;
// 直接访问 → 访问子类的同名成员
cout << "Son 中的 m_A = " << s.m_A << endl; // 200
// 加父类作用域 → 访问父类的同名成员
cout << "Base 中的 m_A = " << s.Base::m_A << endl; // 100
return 0;
}
输出:
Son 中的 m_A = 200
Base 中的 m_A = 100
6.3 同名成员函数
#include <iostream>
using namespace std;
class Base {
public:
void func() {
cout << "Base::func() 调用" << endl;
}
void func(int x) { // 重载版本
cout << "Base::func(int) 调用, x = " << x << endl;
}
};
class Son : public Base {
public:
void func() { // 同名!隐藏了父类所有 func 版本
cout << "Son::func() 调用" << endl;
}
};
int main() {
Son s;
// 直接调用 → 调用子类的同名函数
s.func(); // Son::func() 调用
// 加父类作用域 → 调用父类的同名函数
s.Base::func(); // Base::func() 调用
// 注意:子类的同名函数会隐藏父类所有同名函数(包括重载版本)
// s.func(10); // 编译错误!父类的 func(int) 被隐藏了
// 需要加作用域才能调用被隐藏的父类重载版本
s.Base::func(10); // Base::func(int) 调用, x = 10
return 0;
}
6.4 同名成员处理总结
┌──────────────────────────────────────────────┐
│ 继承中同名成员访问规则 │
├──────────────────────────────────────────────┤
│ 访问子类同名成员: │
│ 对象名.成员名 (直接访问) │
│ │
│ 访问父类同名成员: │
│ 对象名.父类名::成员名 (加作用域 │
└──────────────────────────────────────────────┘
6.5 using 声明恢复被隐藏的父类成员
如果子类同名函数隐藏了父类的所有重载版本,可以用 using 声明恢复:
class Base {
public:
void func() { cout << "Base::func()" << endl; }
void func(int x) { cout << "Base::func(" << x << ")" << endl; }
};
class Son : public Base {
public:
using Base::func; // 恢复父类所有 func 重载版本!
void func() { // 子类自己的版本
cout << "Son::func()" << endl;
}
};
int main() {
Son s;
s.func(); // Son::func()
s.func(10); // Base::func(10) ← using 恢复了!
s.Base::func(); // Base::func()
return 0;
}
7. 继承中的同名静态成员处理
7.1 静态成员的特点
- 静态成员变量在类内声明,类外初始化
- 静态成员属于类级别,所有对象共享
- 静态成员存储在全局区(静态区),不属于某个具体对象
这里定义的地方必须是全局作用域。当然C++17可以使用inline static在类内直接定义与初始化,这里涉及到多线程安全的问题
7.2 同名静态成员的访问
处理方式和非静态成员一样:直接访问子类的,加作用域访问父类的。但静态成员多了类名访问的方式。
#include <iostream>
using namespace std;
class Base {
public:
static int m_A; // 静态成员变量——类内声明
};
int Base::m_A = 100; // 静态成员变量——类外初始化
class Son : public Base {
public:
static int m_A; // 同名静态成员
};
int Son::m_A = 200; // 类外初始化
int main() {
Son s;
// ===== 方式一:通过对象访问 =====
cout << "通过对象访问:" << endl;
cout << "Son 中的 m_A = " << s.m_A << endl; // 200
cout << "Base 中的 m_A = " << s.Base::m_A << endl; // 100
// ===== 方式二:通过类名访问 =====
cout << "通过类名访问:" << endl;
cout << "Son 中的 m_A = " << Son::m_A << endl; // 200
cout << "Base 中的 m_A = " << Son::Base::m_A << endl; // 100
// 也可以:
cout << "Base 中的 m_A = " << Base::m_A << endl; // 100
return 0;
}
7.3 同名静态成员函数
class Base {
public:
static void func() {
cout << "Base::static func()" << endl;
}
};
class Son : public Base {
public:
static void func() {
cout << "Son::static func()" << endl;
}
};
int main() {
// 通过对象名访问
Son s;
s.func(); // Son::static func()
s.Base::func(); // Base::static func()
// 通过类名访问
Son::func(); // Son::static func()
Son::Base::func(); // Base::static func()
return 0;
}
7.4 同名静态成员总结
同名静态成员处理方式和非静态完全一样:
访问子类:直接访问
访问父类:加作用域
唯一区别:静态成员可以用「类名::成员名」方式访问
子类类名::成员名 → 子类的
子类类名::父类名::成员名 → 父类的
父类名::成员名 → 父类的
8. 多继承语法
8.1 多继承概念
C++ 允许一个类同时继承多个父类。
class 子类 : 继承方式 父类1, 继承方式 父类2, ... {
// 子类成员
};
注意:C++ 实际开发中不建议频繁使用多继承,因为容易产生二义性问题。
8.2 多继承示例
#include <iostream>
using namespace std;
class Base1 {
public:
int m_A = 10;
void func1() { cout << "Base1::func1()" << endl; }
};
class Base2 {
public:
int m_B = 20;
int m_A = 30; // 注意:和 Base1 有同名成员!
void func2() { cout << "Base2::func2()" << endl; }
};
// 多继承
class Son : public Base1, public Base2 {
public:
int m_C = 40;
};
int main() {
Son s;
cout << "sizeof(Son) = " << sizeof(Son) << endl; // 16(4个int)
// 访问不同父类的成员
cout << "m_A (Base1) = " << s.Base1::m_A << endl; // 10
cout << "m_B (Base2) = " << s.m_B << endl; // 20
cout << "m_A (Base2) = " << s.Base2::m_A << endl; // 30
cout << "m_C (Son) = " << s.m_C << endl; // 40
// 调用不同父类的方法
s.func1();
s.func2();
return 0;
}
8.3 多继承中的同名问题
当多个父类中有同名成员时,子类使用时必须加作用域区分:
class Base1 {
public:
int m_A = 10;
};
class Base2 {
public:
int m_A = 20; // 和 Base1 同名
};
class Son : public Base1, public Base2 {};
int main() {
Son s;
// cout << s.m_A << endl; // 编译错误!二义性!
cout << s.Base1::m_A << endl; // 10
cout << s.Base2::m_A << endl; // 20
return 0;
}
8.4 多继承的构造顺序
class Base1 {
public:
Base1() { cout << "Base1 构造" << endl; }
};
class Base2 {
public:
Base2() { cout << "Base2 构造" << endl; }
};
// 继承顺序决定构造顺序
class Son : public Base1, public Base2 {
public:
Son() { cout << "Son 构造" << endl; }
};
int main() {
Son s;
return 0;
}
输出:
Base1 构造 ← 按继承列表顺序
Base2 构造
Son 构造
构造顺序由继承列表中父类的出现顺序决定(从左到右),与初始化列表中的顺序无关。
8.5 多继承的争议
多继承的问题:
1. 二义性 —— 多个父类有同名成员时需要加作用域
2. 菱形继承 —— 两个父类继承自同一个祖父类,导致数据重复
3. 复杂性 —— 继承关系难以理解和维护
Java 和 C# 的解决方案:
→ 不支持多继承(类层面),只支持多实现(接口层面)
C++ 的解决方案:
→ 允许多继承,但用虚继承解决菱形问题
→ 实际开发中,更推荐用「组合」替代多继承
9. 菱形继承问题
9.1 什么是菱形继承
菱形继承(Diamond Inheritance),又称钻石继承,是指:
- 两个派生类继承同一个基类
- 又有某个类同时继承这两个派生类
Animal(祖父类)
/ \
/ \
Sheep Tuo (两个中间类)
\ /
\ /
SheepTuo (最终子类)
◇ 菱形结构
9.2 代码示例
#include <iostream>
using namespace std;
// 祖父类
class Animal {
public:
int m_Age; // 年龄
};
// 父类1:羊
class Sheep : public Animal {};
// 父类2:驼
class Tuo : public Animal {};
// 子类:羊驼(多继承)
class SheepTuo : public Sheep, public Tuo {};
int main() {
SheepTuo st;
// 问题1:二义性!
// st.m_Age = 10; // 编译错误!不确定是 Sheep 的 m_Age 还是 Tuo 的 m_Age
// 只能加作用域访问
st.Sheep::m_Age = 8; // 羊的年龄 = 8
st.Tuo::m_Age = 28; // 驼的年龄 = 28
cout << "Sheep 的 m_Age = " << st.Sheep::m_Age << endl; // 8
cout << "Tuo 的 m_Age = " << st.Tuo::m_Age << endl; // 28
// 问题2:数据冗余!
// 羊驼只需要一个年龄,但继承了两份!
// sizeof(SheepTuo) = 8(两份 m_Age)
cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;
return 0;
}
9.3 菱形继承的两大问题
问题一:二义性(Ambiguity)
SheepTuo 继承了 Sheep 的 m_Age 和 Tuo 的 m_Age
访问 st.m_Age 时编译器不知道选哪个
→ 编译错误!
问题二:数据冗余(Redundancy)
羊驼只需要一个年龄属性
但继承了两份 m_Age(分别来自 Sheep 和 Tuo)
→ 浪费内存,且两个值可能不一致
9.4 菱形继承的内存模型
=== 普通菱形继承的内存结构 ===
SheepTuo 对象:
┌─────────────────────────┐
│ Sheep 部分 │
│ └ m_Age (4字节) │ ← 从 Animal 继承,第一份
├─────────────────────────┤
│ Tuo 部分 │
│ └ m_Age (4字节) │ ← 从 Animal 继承,第二份
├─────────────────────────┤
│ SheepTuo 自己的成员 │
│ (无) │
└─────────────────────────┘
sizeof(SheepTuo) = 8
两份 m_Age → 二义性 + 数据冗余!
10. 虚继承——菱形继承的解决方案
10.1 虚继承语法
在中间类(Sheep 和 Tuo)继承 Animal 时,加上 virtual 关键字:
// 祖父类
class Animal {
public:
int m_Age;
};
// 父类1:虚继承 Animal
class Sheep : virtual public Animal {};
// 父类2:虚继承 Animal
class Tuo : virtual public Animal {};
// 子类:羊驼
class SheepTuo : public Sheep, public Tuo {};
10.2 虚继承后的效果
int main() {
SheepTuo st;
// 不再有二义性!直接访问即可
st.m_Age = 10;
cout << "m_Age = " << st.m_Age << endl; // 10
cout << "Sheep::m_Age = " << st.Sheep::m_Age << endl; // 10(同一份数据)
cout << "Tuo::m_Age = " << st.Tuo::m_Age << endl; // 10(同一份数据)
cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;
return 0;
}
10.3 虚继承原理
虚继承通过 虚基类指针(vbptr) 和 虚基类表(vbtable) 实现:
vbptr — Virtual Base Pointer(虚基类指针)
→ 每个虚继承的类对象中都有一个
→ 指向虚基类表
vbtable — Virtual Base Table(虚基类表)
→ 记录虚基类成员相对于对象起始地址的偏移量
→ 通过偏移量找到共享的那一份数据
10.4 虚继承后的内存模型
=== 虚继承后的内存结构(64位系统)===
SheepTuo 对象:
┌──────────────────────────────────┐
│ Sheep 部分 │
│ └ vbptr_Sheep (8字节) │ ← 虚基类指针
├──────────────────────────────────┤
│ Tuo 部分 │
│ └ vbptr_Tuo (8字节) │ ← 虚基类指针
├──────────────────────────────────┤
│ Animal 部分(共享!只有一份!) │
│ └ m_Age (4字节) │ ← 只有一份!
└──────────────────────────────────┘
内存对齐后: sizeof(SheepTuo) ≈ 24字节
vbptr_Sheep → vbtable → 偏移量 → 找到 m_Age
vbptr_Tuo → vbtable → 偏移量 → 找到同一个 m_Age
┌─────────────────────┐
│ 两个 vbptr 指向的是 │
│ 同一份 Animal::m_Age │
│ → 不再二义!不再冗余!│
└─────────────────────┘
10.5 虚继承完整代码
#include <iostream>
using namespace std;
// 祖父类
class Animal {
public:
int m_Age;
Animal() : m_Age(0) {}
};
// 虚继承!
class Sheep : virtual public Animal {};
// 虚继承!
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {
public:
void setAge(int age) {
m_Age = age; // 直接访问,不再有二义性
}
int getAge() {
return m_Age;
}
};
int main() {
SheepTuo st;
st.m_Age = 10; // 直接访问!没有二义性!
cout << "st.m_Age = " << st.m_Age << endl;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; // 同一份
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; // 同一份
cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;
return 0;
}
11. 虚继承内存模型深度剖析
11.1 各类内存大小对比
#include <iostream>
using namespace std;
class Animal {
public:
int m_Age; // 4字节
};
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
int main() {
cout << "sizeof(Animal) = " << sizeof(Animal) << endl;
cout << "sizeof(Sheep) = " << sizeof(Sheep) << endl;
cout << "sizeof(Tuo) = " << sizeof(Tuo) << endl;
cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;
return 0;
}
输出(64位系统,GCC编译器):
sizeof(Animal) = 4
sizeof(Sheep) = 16
sizeof(Tuo) = 16
sizeof(SheepTuo) = 24
11.2 内存大小分析
Animal:4字节
┌──────────┐
│ m_Age │ 4字节
└──────────┘
Sheep:16字节
┌──────────────┐
│ vbptr_Sheep │ 8字节(虚基类指针)
├──────────────┤
│ m_Age │ 4字节(从 Animal 继承)
├──────────────┤
│ 对齐填充 │ 4字节(对齐到8的倍数)
└──────────────┘
总计:16字节
SheepTuo:24字节
┌──────────────┐
│ vbptr_Sheep │ 8字节(Sheep 的虚基类指针)
├──────────────┤
│ vbptr_Tuo │ 8字节(Tuo 的虚基类指针)
├──────────────┤
│ m_Age │ 4字节(只有一份!共享的 Animal 数据)
├──────────────┤
│ 对齐填充 │ 4字节
└──────────────┘
总计:24字节
11.3 虚基类指针工作原理
SheepTuo 对象起始地址:0x1000
┌──────────────┐ 0x1000
│ vbptr_Sheep │ ──→ vbtable_Sheep
├──────────────┤ 0x1008 │ 偏移量 = 16 │
│ vbptr_Tuo │ ──→ vbtable_Tuo
├──────────────┤ 0x1010 │ 偏移量 = 8 │
│ m_Age │ ←── 两个 vbptr 通过偏移量都指向这里!
└──────────────┘ 0x1014
vbptr_Sheep 查表得到偏移量 16 → 0x1000 + 16 = 0x1010 → m_Age
vbptr_Tuo 查表得到偏移量 8 → 0x1008 + 8 = 0x1010 → m_Age
→ 同一个 m_Age!不会二义,不会冗余!
11.4 虚继承 vs 普通继承内存对比
=== 普通菱形继承 === === 虚继承 ===
SheepTuo (8字节) SheepTuo (24字节)
┌────────────┐ ┌──────────────┐
│ Sheep::m_Age│ 4字节 │ vbptr_Sheep │ 8字节
├────────────┤ ├──────────────┤
│ Tuo::m_Age │ 4字节 │ vbptr_Tuo │ 8字节
└────────────┘ ├──────────────┤
两份 m_Age │ m_Age │ 4字节
二义 + 冗余 └──────────────┘
一份 m_Age
无二义 无冗余
但多了指针开销
| 对比项 | 普通菱形继承 | 虚继承 |
|---|---|---|
| m_Age 份数 | 2份 | 1份 |
| 二义性 | 有 | 无 |
| 数据冗余 | 有 | 无 |
| 额外开销 | 无 | vbptr 指针 |
| 内存大小 | 较小(8字节) | 较大(24字节) |
| 访问方式 | 需加作用域 | 直接访问 |
虚继承的作用是:在多继承体系中,确保共享的基类子对象只有一份,从而解决菱形继承带来的数据冗余和访问歧义;但它会引入额外的内存和运行成本,因此在业务代码中应尽量避免,优先使用组合或接口继承。
12. 综合案例:图形类
12.1 案例描述
编写图形类 Shape,有方法 draw() 画出图形、area() 计算面积;编写具体图形类 Circle(圆)和 Square(正方形),均继承自 Shape,分别实现自己的 draw() 和 area()。
12.2 完整代码
#include <iostream>
#include <cmath>
using namespace std;
// ============ 图形基类(抽象类) ============
class Shape {
public:
// 画图形
virtual void draw() {
cout << " 画一个抽象图形" << endl;
}
// 计算面积
virtual double area() {
return 0.0;
}
// 虚析构
virtual ~Shape() {}
};
// ============ 圆形 ============
class Circle : public Shape {
private:
double m_Radius;
public:
Circle(double r) : m_Radius(r) {}
void draw() override {
cout << " 画一个圆形(半径=" << m_Radius << ")" << endl;
cout << " *** " << endl;
cout << " * * " << endl;
cout << " * * " << endl;
cout << " * * " << endl;
cout << " *** " << endl;
}
double area() override {
return 3.14159 * m_Radius * m_Radius;
}
};
// ============ 正方形 ============
class Square : public Shape {
private:
double m_Side;
public:
Square(double s) : m_Side(s) {}
void draw() override {
cout << " 画一个正方形(边长=" << m_Side << ")" << endl;
cout << " +-------+" << endl;
cout << " | |" << endl;
cout << " | |" << endl;
cout << " +-------+" << endl;
}
double area() override {
return m_Side * m_Side;
}
};
// ============ 拓展:三角形 ============
class Triangle : public Shape {
private:
double m_Base;
double m_Height;
public:
Triangle(double b, double h) : m_Base(b), m_Height(h) {}
void draw() override {
cout << " 画一个三角形(底=" << m_Base << ", 高=" << m_Height << ")" << endl;
cout << " /\\ " << endl;
cout << " / \\ " << endl;
cout << " / \\ " << endl;
cout << " /______\\ " << endl;
}
double area() override {
return 0.5 * m_Base * m_Height;
}
};
// ============ 多态展示函数 ============
void showShape(Shape &shape) {
shape.draw();
cout << " 面积 = " << shape.area() << endl;
cout << " ----------" << endl;
}
int main() {
Circle c(5.0);
Square sq(4.0);
Triangle t(6.0, 3.0);
cout << "===== 图形展示 =====" << endl;
showShape(c);
showShape(sq);
showShape(t);
return 0;
}
12.3 拓展:图形类层次设计
Shape(抽象基类)
├── draw() = 0 (纯虚函数)
├── area() = 0 (纯虚函数)
└── ~Shape() virtual
│
┌────────────┼────────────┐
│ │ │
Circle Square Triangle
├── m_Radius ├── m_Side ├── m_Base
├── draw() ├── draw() ├── m_Height
└── area() └── area() ├── draw()
└── area()
13. 常见陷阱与要点
13.1 继承的常见陷阱
陷阱1:误以为 private 成员不被继承
class Base {
private:
int m_A = 10;
};
class Son : public Base {
public:
void test() {
// m_A = 20; // 编译错误,不可访问
// 但 m_A 在内存中确实存在!sizeof(Son) 包含了它
}
};
// sizeof(Son) = 4 → m_A 被继承了,只是访问不到
陷阱2:子类同名函数隐藏父类所有重载版本
class Base {
public:
void func() { cout << "func()" << endl; }
void func(int x) { cout << "func(int)" << endl; }
};
class Son : public Base {
public:
void func() { cout << "Son::func()" << endl; }
};
Son s;
s.func(); // Son::func()
// s.func(10); // 编译错误!父类 func(int) 被隐藏了!
陷阱3:构造函数不能被继承
class Base {
public:
Base(int x) { cout << x << endl; }
};
class Son : public Base {
// Son 不继承 Base(int) 构造函数!
// 如果 Base 没有默认构造,Son 必须在初始化列表中调用
public:
Son() : Base(0) {} // 必须显式调用
};
陷阱4:友元不能被继承
class Base {
friend void friendFunc(Base &b); // 友元函数
};
class Son : public Base {
// friendFunc 不能直接访问 Son 的私有成员!
// 友元关系不被继承
};
陷阱5:静态成员属于类,不属于对象
class Base {
public:
static int s_Count;
};
int Base::s_Count = 0;
class Son : public Base {};
Son s1, s2;
s1.s_Count = 10;
cout << s2.s_Count << endl; // 10,所有对象共享同一份
cout << Base::s_Count << endl; // 10
cout << Son::s_Count << endl; // 10
13.2 高频问题
Q1: 继承的优缺点?
| 优点 | 缺点 |
|---|---|
| 代码复用 | 父类变化影响所有子类(耦合) |
| 扩展性强 | 继承层次过深难以理解 |
| 多态的基础 | 破坏封装(protected 暴露细节) |
| 层次化设计 | 编译期绑定,灵活性不如组合 |
Q2: 继承 vs 组合,如何选择?
继承(is-a):Dog is an Animal → 使用继承
组合(has-a):Car has an Engine → 使用组合
优先使用组合,而非继承(Effective C++ 条款 32)
class Engine { ... };
class Car {
private:
Engine m_Engine; // 组合!Car has an Engine
};
Q3: 菱形继承怎么解决?
虚继承:在中间层类继承顶层基类时加 virtual 关键字,使最终子类只持有一份顶层基类的数据。
Q4: 虚继承的原理是什么?
虚继承通过虚基类指针(vbptr)和虚基类表(vbtable)实现。vbptr 存储在对象中,指向 vbtable,vbtable 记录虚基类成员相对于对象起始地址的偏移量,通过偏移量访问共享的虚基类成员。
Q5: 什么是对象切片?
class Base { public: virtual void f() {} };
class Derived : public Base { public: int m_Extra; };
Derived d;
Base b = d; // 对象切片!Derived 部分被截断
// b.m_Extra; // 不存在
值传递时,子类特有的部分会被"切掉",只保留父类部分。
14. 进阶拓展
14.1 继承与组合的对比
// ===== 继承方式(is-a)=====
class Stack : public vector<int> {
// Stack 是一种 vector → 语义上不太准确
// 而且 vector 的所有 public 方法都暴露了
public:
void push(int val) { push_back(val); }
int pop() { int v = back(); pop_back(); return v; }
};
// ===== 组合方式(has-a)—— 更好的设计 =====
class Stack {
private:
vector<int> m_Data; // Stack 有一个 vector
public:
void push(int val) { m_Data.push_back(val); }
int pop() {
int v = m_Data.back();
m_Data.pop_back();
return v;
}
bool empty() const { return m_Data.empty(); }
// 只暴露需要的方法,不暴露 vector 的所有接口
};
14.2 私有继承实现组合
私有继承在语义上等同于组合,但不如直接使用成员变量直观:
// 方式一:私有继承(has-a 的另一种实现)
class Stack : private vector<int> {
public:
void push(int val) { push_back(val); }
int pop() { int v = back(); pop_back(); return v; }
using vector<int>::empty; // 有选择地暴露基类方法
};
// 方式二:成员变量(推荐)
class Stack {
private:
vector<int> m_Data;
public:
void push(int val) { m_Data.push_back(val); }
int pop() { int v = m_Data.back(); m_Data.pop_back(); return v; }
};
原则:能用组合就用组合,只有需要访问基类的 protected 成员或需要虚函数时才考虑私有继承。
14.3 using 改变继承的访问权限(C++11)
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son : private Base { // 私有继承
public:
using Base::m_A; // 将 m_A 恢复为 public
// using Base::m_B; // 可以恢复为 public(原来是 protected)
// using Base::m_C; // 编译错误!private 成员不能用 using 恢复
};
int main() {
Son s;
s.m_A = 10; // ✓ m_A 被恢复为 public
return 0;
}
14.4 final 禁止继承(C++11)
class FinalClass final { // 这个类不能被继承
public:
void func() {}
};
// class Derived : public FinalClass {}; // 编译错误!
14.5 继承中的默认构造函数
class Base {
public:
Base() = default; // C++11,让编译器生成默认构造
Base(int x) : m_X(x) {}
int m_X;
};
// C++11: using 继承父类构造函数
class Son : public Base {
public:
using Base::Base; // 继承父类所有构造函数!
};
Son s1; // 调用 Base()
Son s2(10); // 调用 Base(int)
14.6 CRTP — 奇异递归模板模式
// CRTP:编译期多态(静态多态)
template <typename Derived>
class ShapeBase {
public:
void draw() {
static_cast<Derived*>(this)->drawImpl();
}
double area() {
return static_cast<Derived*>(this)->areaImpl();
}
};
class Circle : public ShapeBase<Circle> {
public:
void drawImpl() { cout << "画圆" << endl; }
double areaImpl() { return 3.14 * 5 * 5; }
};
// 零开销抽象,编译期确定调用,无需虚函数表
15. 速查表
15.1 继承核心知识速查
| 知识点 | 关键内容 |
|---|---|
| 继承语法 | class 子类 : 继承方式 父类 {} |
| 三种继承方式 | public / protected / private |
| 常用继承 | public(95%以上场景) |
| 构造顺序 | 父类 → 子类(从上到下) |
| 析构顺序 | 子类 → 父类(从下到上,与构造相反) |
| 同名成员 | 子类隐藏父类,加作用域访问父类 |
| 静态同名 | 同上,多了类名访问方式 |
| 多继承 | class C : public A, public B {} |
| 菱形继承 | 两个子类继承同一父类,再被同一孙类继承 |
| 虚继承 | class Sheep : virtual public Animal {} |
| 虚继承原理 | vbptr + vbtable,共享一份数据 |
15.2 权限修饰符速查
| 修饰符 | 本类 | 子类 | 外部 |
|---|---|---|---|
| public | ✓ | ✓ | ✓ |
| protected | ✓ | ✓ | ✗ |
| private | ✓ | ✗ | ✗ |
15.3 三种继承方式速查
| 继承方式 | public 成员→子类 | protected 成员→子类 | private 成员→子类 |
|---|---|---|---|
| public | public | protected | 不可访问 |
| protected | protected | protected | 不可访问 |
| private | private | private | 不可访问 |
15.4 同名成员访问速查
同名非静态成员:
访问子类: 对象名.成员名
访问父类: 对象名.父类名::成员名
同名静态成员:
对象名访问:
访问子类: 对象名.成员名
访问父类: 对象名.父类名::成员名
类名访问:
访问子类: 子类名::成员名
访问父类: 子类名::父类名::成员名 或 父类名::成员名
15.5 菱形继承速查
问题:
Animal ← Sheep ← SheepTuo
Animal ← Tuo ← SheepTuo
→ SheepTuo 有两份 Animal 数据 → 二义性 + 冗余
解决:
class Sheep : virtual public Animal {} ← 虚继承
class Tuo : virtual public Animal {} ← 虚继承
→ SheepTuo 只有一份 Animal 数据 → 无二义 无冗余
原理:
vbptr(虚基类指针)→ vbtable(虚基类表)→ 偏移量 → 共享数据
内存开销:
普通菱形继承:8字节(两份 m_Age)
虚继承: 24字节(2个 vbptr + 1份 m_Age + 对齐)
→ 空间换正确性
15.6 继承 vs 组合
| 继承 | 组合 | |
|---|---|---|
| 关系 | is-a(是一种) | has-a(有一个) |
| 耦合 | 高(编译期绑定) | 低(运行时可替换) |
| 灵活性 | 低 | 高 |
| 封装性 | 弱(暴露 protected) | 强(只暴露需要的方法) |
| 推荐度 | 在 is-a 时使用 | 优先使用 |
15.7 构造析构顺序速查
单继承:
构造:父 → 子
析构:子 → 父
多继承:
构造:按继承列表从左到右
析构:从右到左
多层继承:
构造:Grandpa → Father → Son
析构:Son → Father → Grandpa

浙公网安备 33010602011771号