💜 C++ 底层矩阵 · 代码永不停歇
| 👤 作者主页 | 🔥 C++ 核心专栏 |
|---|---|
| 💾 算法题解仓库 | 📁 代码仓库 |
C++继承全解------从基础语法到对象模型暗坑全吃透
你以为继承只是简单的代码复用?
当你遇到对象切片、名字隐藏、菱形继承数据冗余、虚基表编译报错、多态析构内存泄漏 时,才会明白:
C++ 继承的本质,是对对象模型、内存布局、访问控制的终极考验。
本文带你从零开始,吃透 C++ 继承的所有语法、底层原理与暗坑。
一、继承的本质 ------ 从代码冗余到层次化复用
1.1 为什么需要继承?
描述 Student 和 Teacher 时,都会用到姓名、年龄等公共属性。不使用继承,会出现大量重复代码,维护成本极高。
无继承:代码冗余,难以维护
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
string studentId;
void study() { cout << name << " 正在学习" << endl; }
};
class Teacher {
public:
string name;
int age;
string course;
void teach() { cout << name << " 正在授课" << endl; }
};
继承的解决方案 :
把公共成员抽取到基类(父类) ,Student/Teacher 作为**派生类(子类)**复用基类成员。
cpp
// 继承:代码复用 + 层次清晰
class Person {
public:
string name;
int age;
};
// 派生类 : 继承方式 基类
class Student : public Person {
public:
string studentId;
void study() { cout << name << " 正在学习" << endl; }
};
class Teacher : public Person {
public:
string course;
void teach() { cout << name << " 正在授课" << endl; }
};
1.2 继承基本语法
cpp
class 派生类名 : 继承方式 基类名 {
// 派生类新增成员
};
二、访问控制 ------ 权限的「木桶效应」
继承的核心难点:基类成员在派生类中的最终权限,不是继承方式说了算,也不是原权限说了算,而是取更小的那个。
2.1 核心公式(背会这一条就够)
最终权限 = min(基类成员权限, 继承方式)
权限强度:public > protected > private
| 继承方式 | 基类 public | 基类 protected | 基类 private |
|---|---|---|---|
| public | public | protected | 不可访问 |
| protected | protected | protected | 不可访问 |
| private | private | private | 不可访问 |
两个关键理解【面试高频】
- 不可访问 ≠ 不存在
基类private成员依然会被派生类对象包含(占内存),只是语法上禁止直接访问。 - protected 是为继承而生
private:自己能用,子类不能用;protected:自己能用,子类能用,外部不能用。
2.2 默认继承规则
class派生:默认 private 继承struct派生:默认 public 继承
2.3 可运行代码示例
cpp
#include <iostream>
using namespace std;
class Base {
public:
int pub = 1;
protected:
int pro = 2;
private:
int pri = 3; // 子类不可直接访问
};
// public 继承
class PubDerive : public Base {
public:
void test() {
cout << pub << endl; // ok
cout << pro << endl; // ok
// cout << pri << endl; // error
}
};
// protected 继承
class ProDerive : protected Base {
public:
void test() {
cout << pub << endl; // ok,内部可访问
cout << pro << endl; // ok
}
};
// private 继承
class PriDerive : private Base {
public:
void test() {
cout << pub << endl; // ok,内部可访问
cout << pro << endl; // ok
}
};
int main() {
PubDerive p;
p.pub; // ok
// p.pro; // error
ProDerive pr;
// pr.pub; // error,外部无法访问
PriDerive pv;
// pv.pub; // error,外部无法访问
return 0;
}
三、内存视角:对象切片(最容易踩的暗坑)
3.1 向上转型
派生类对象可以赋值/初始化给:
- 基类对象
- 基类指针
- 基类引用
这叫向上转型(安全、隐式)。
cpp
#include <string>
using namespace std;
class Person {
public:
int _age;
string _name;
};
class Student : public Person {
public:
string _studentId;
};
int main() {
Student s;
// 赋值给基类对象 → 发生切片
Person p = s;
// 指针/引用 → 不切片
Person* pp = &s;
Person& rp = s;
return 0;
}
3.2 底层真相:切片是什么?
派生类对象内存布局:
基类子对象在前,派生类新增成员在后
Student:[ Person(_name, _age) ][ _studentId ]
Person p = s:只拷贝基类部分 ,切掉子类独有成员 → 对象切片- 切片后,
p彻底变成Person,无法访问子类任何成员
【致命暗坑】
多态必须用指针/引用,绝对不能用对象赋值,否则切片会破坏多态。
四、作用域隐藏:同名就藏,不看参数
4.1 核心规则
基类和派生类是两个独立作用域 。
只要名字相同 ,派生类成员会隐藏基类所有同名成员。
⚠️ 不是重载!不是覆盖!是隐藏!
- 重载要求:同一作用域 + 同名不同参;
- 隐藏要求:不同作用域 + 同名即可。
4.2 变量隐藏
cpp
#include <iostream>
using namespace std;
class Base {
public:
int num = 100;
};
class Derived : public Base {
public:
int num = 200; // 隐藏基类 num
void test() {
cout << num << endl; // 200(优先子类)
cout << Base::num << endl; // 100(指定作用域)
}
};
int main() {
Derived d;
d.test();
return 0;
}
4.3 函数隐藏
只要函数名相同,无论参数是否一致,基类所有重载函数都会被隐藏。
cpp
#include <iostream>
using namespace std;
class Base {
public:
void func() { cout << "Base func" << endl; }
void func(int x) { cout << "Base func(int)" << endl; }
};
class Derived : public Base {
public:
// 隐藏基类两个 func
void func() { cout << "Derived func" << endl; }
};
int main() {
Derived d;
d.func(); // ok
// d.func(10); // error!基类func(int)被隐藏
d.Base::func(10); // ok,指定作用域
return 0;
}
【面试题】
基类有多个重载函数
fun(),子类只要定义一个同名函数,基类所有重载版本全部被隐藏。
4.4 隐藏 / 重写 / 重载 极简对比
| 特性 | 重载 | 隐藏 | 重写(多态) |
|---|---|---|---|
| 作用域 | 同一作用域 | 不同作用域 | 不同作用域 |
| 函数名 | 相同 | 相同 | 相同 |
| 参数 | 不同 | 可同可不同 | 必须相同 |
| virtual | 不需要 | 不需要 | 必须有 |
| 绑定类型 | 静态绑定 | 静态绑定 | 动态绑定 |
五、派生类生命周期:构造 & 析构
5.1 构造函数规则
- 派生类必须初始化基类子对象
- 若不显示调用基类构造,编译器会隐式调用基类默认构造
- 基类无默认构造 → 直接编译报错
cpp
#include <string>
using namespace std;
class Person {
public:
// 无默认构造函数
Person(const string& name) : _name(name) {}
private:
string _name;
};
class Student : public Person {
public:
// 正确:必须显式调用基类构造
Student(const string& name, const string& id)
: Person(name), _id(id) {}
private:
string _id;
};
【暗坑】
基类构造调用顺序 = 继承列表顺序,与初始化列表顺序无关。
5.2 析构函数:内存泄漏的重灾区
- 调用顺序:先析构子类 → 再析构父类
- 普通析构:不构成多态,基类指针删除子类对象 → 只调用基类析构 → 内存泄漏
错误示例
cpp
#include <iostream>
using namespace std;
class Base {
public:
~Base() { cout << "基类析构" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "子类析构" << endl; }
};
int main() {
Base* ptr = new Derived;
delete ptr;
// 输出:仅 基类析构 → 子类资源泄漏!
return 0;
}
正确写法
基类析构函数必须加 virtual
cpp
class Base {
public:
virtual ~Base() { cout << "基类析构" << endl; }
};
【铁律】
一个类只要可能被继承,析构函数就必须是 virtual。
六、多继承与菱形继承:底层灾难与虚继承
6.1 多继承语法
cpp
class Derived : public Base1, public Base2 {};
6.2 菱形继承的模型图
Person
/ \
Student Teacher
\ /
Assistant
cpp
#include <string>
using namespace std;
class Person { public: string _name; };
class Student : public Person {};
class Teacher : public Person {};
class Assistant : public Student, public Teacher {};
两大致命问题
- 数据冗余 :
Assistant包含两份Person - 访问二义性 :
a._name编译报错,不知道用哪一份
6.3 虚继承:解决菱形继承
在中间派生类 加上 virtual:
cpp
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};
底层原理
- 增加 vbptr(虚基表指针)
- 指向 vbtable(虚基表)
- 表中存放:当前指针到唯一虚基类子对象的偏移
最终:整个继承体系中只有一份 Person
工程建议
尽量避免菱形继承,逻辑复杂、性能略低、可读性差。
七、类模板继承
7.1.类模板继承需要指定类域
我们以stack继承vector为例子
代码实现:
cpp
#include<iostream>
#include<vector>
using namespace std;
template<class T>
class Mystack:pubilc vector<T>{
void push(const T& data){
//push_back(data) error:找不到push_back,因为编译器在没有实例化之前是不会进入到类模板中的,即访问不到vector内部的函数和成员,需要指定类域,要求编译器去vector里面寻找
this->push_back(data);
//或
vector<T>::push_back(data)
}
};
7.2.模板与虚函数
模板函数是不允许继承的,因为虚函数在继承之前就需要计算虚函数表的大小,而模板导致无法计算
八、继承 vs 组合
8.1.定义
- 继承(is-a):Derived是一种Base,例如狗是一种动物
- 组合(has-a):Class包含另一个类作为成员,例如车有引擎
8.2.优先使用组合
- 白箱 vs 黑箱 :继承是一种白箱调用,派生类能够访问基类的public 成员,甚至protected 成员,导致基类的实现细节暴露,基类一变动,可能影响所有的派生类,而组合是一种黑箱调用,只能通过特点的函数接口访问成员变量,屏蔽了底层细节,耦合度较低
- 继承存在滥用:某些类的关系可能并不符合is-a的情况,比如stack和vector,stack并不支持insert等等接口,可能会破坏类的特点
九、如何终结继承
有些类可能并不期望被继承,那么应该怎么做呢?
9.1.C++11方案:final
在不期望被继承的类和函数后面加上final关键字
cpp
class Nonderived final{
//
}
// class Bad : public NonDerived {}; // ❌ 编译错误
final也能修饰函数,让派生类无法重写
示例代码:
cpp
class Base {
public:
virtual void show() final { // 虚函数 + final
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
// 错误!无法重写被 final 修饰的虚函数
void show() override {
cout << "Derived" << endl;
}
};
9.2.C++98方案:构造函数私有
原理:派生类在构造时必定要调用基类的构造函数,基类的构造函数私有化就导致派生类构造不了,这样也就没办法继承了,不如final直观,较为麻烦
cpp
class Base{
private:
Base(){}
public:
//让外面能够实例化
static Base* create(){
return new Base();
}
}
// Base b; error
// Base d = Base::create();ok
// class D = Base(); error
结语:给读者的总结
继承不只是把代码抄一遍,它涉及到了:
-
作用域屏蔽(隐藏规则)
-
构造析构顺序(先父后子,相反析构)
-
内存布局与切片(对象赋值丢失派生部分)
-
多态与虚表(virtual 的神奇之处)
-
菱形继承与虚基表(偏移量寻址)
-
模板继承中的查找问题(依赖型名称)
掌握了这些底层原理,你才能在面对C++复杂的对象模型时游刃有余,写出健壮、高效、易维护的面向对象程序!
