C++ 继承详解:从语法到底层原理,一文搞懂面向对象复用
继承是 C++ 面向对象编程的三大特性之一(封装、继承、多态)。如果说类与对象解决了"数据和操作封装在一起"的问题,那继承解决的问题就是代码复用——在已有类的基础上创建新类,复用基类的代码,同时添加新的功能。本篇博客基于课堂笔记和课件,系统讲解 C++ 继承的全部核心知识。
一、前言
1.1 为什么需要继承?
写代码时经常遇到这种情况:你有一个 Person 类,里面有姓名、年龄等属性和一些基本操作。现在你要写一个 Student 类和一个 Teacher 类——它们都有姓名、年龄,但 Student 还有学号,Teacher 还有工号。
没有继承时,你只能把共有的代码再抄一遍:
// 没有继承——重复代码
class Person {
string _name;
int _age;
};
class Student {
string _name; // 又写一遍
int _age; // 又写一遍
int _stuNum; // 学号
};
class Teacher {
string _name; // 再写一遍
int _age; // 再写一遍
int _jobId; // 工号
};
这明显不合理——代码大量重复,维护困难。如果你要改 Person 的属性(比如加一个性别字段),所有类都要改。
继承可以解决这个问题:
class Student : public Person {
int _stuNum; // 只需要关注 Student 特有的成员
};
class Teacher : public Person {
int _jobId; // 只需要关注 Teacher 特有的成员
};
Student 和 Teacher 自动拥有 Person 的所有成员,不需要再写一遍。
1.2 面向对象三大特性
| 特性 | 作用 | 说人话 |
|---|---|---|
| 封装 | 数据和操作绑定,控制访问权限 | 类把数据和函数打包,告诉你哪些能用、哪些不能用 |
| 继承 | 在已有类的基础上创建新类,复用代码 | 子类自动拥有父类的成员,不需要重新造轮子 |
| 多态 | 同一操作在不同对象上表现出不同行为 | 同一个函数,传学生指针和传普通人指针,结果不一样 |
继承是连接"封装"和"多态"的桥梁——没有继承就没有多态。
1.3 本篇博客主要内容
- 继承的概念与语法——怎么定义一个类继承另一个类
- 访问权限——public/protected/private 继承的区别
- 对象切片(赋值兼容)——子类对象能不能当父类用
- 作用域与隐藏——同名成员会怎样
- 派生类的默认成员函数——构造/拷贝构造/赋值/析构的顺序和写法
- 静态成员和友元在继承中的表现
- 多继承和菱形继承——C++ 继承最复杂的地方
- 虚拟继承——如何解决菱形继承的数据冗余和二义性
- 组合 vs 继承——两种代码复用的设计取舍
- 常见错误和面试问题
二、继承的概念与定义
2.1 基本语法
class Person {
public:
void Print() {
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
};
// Student 继承 Person
class Student : public Person {
public:
void func() {
cout << _name << endl; // ✅ 可以访问父类的 protected 成员
// cout << _age << endl; // ❌ 不能直接访问父类的 private 成员
Print(); // ✅ 可以间接调用父类的 public 函数
}
protected:
int _stuid; // 学号
};
这里 Student 继承了 Person,我们说:
Person是基类(base class),也叫父类Student是派生类(derived class),也叫子类Student从Person继承了_name、_age、Print()等成员
💡 课堂比喻:爸爸(Person)有名字、年龄、买东西的技能。儿子(Student)自动继承这些——不需要重新"学"。但爸爸的钱包(private 成员)儿子不能直接碰。
2.2 三种继承方式
| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
|---|---|---|---|
| public 继承 | 仍是 public | 仍是 protected | 不可见 |
| protected 继承 | 变为 protected | 仍是 protected | 不可见 |
| private 继承 | 变为 private | 变为 private | 不可见 |
规则:派生类中成员的访问权限 = min(基类中该成员的访问限定符, 继承方式)。
class Person {
public:
string _name;
protected:
int _age;
private:
string _privateInfo; // 私房钱
};
class Student1 : public Person {
// _name 仍是 public
// _age 仍是 protected
// _privateInfo 不可见(不能访问)
};
class Student2 : protected Person {
// _name 变成 protected
// _age 仍是 protected
// _privateInfo 不可见
};
class Student3 : private Person {
// _name 变成 private
// _age 变成 private
// _privateInfo 不可见
};
❗ 最常用的是 public 继承,它表达了"is-a"的关系——Student 是一个 Person。protected 和 private 继承用得很少。
2.3 class 和 struct 的继承默认方式
// class 默认继承方式是 private
class Student : Person { ... }; // 等价于 class Student : private Person
// struct 默认继承方式是 public
struct Student : Person { ... }; // 等价于 struct Student : public Person
三、基类和派生类之间的赋值转换——对象切片
这是继承中最基础也最重要的概念之一。派生类对象可以当作基类对象来使用。
class Person {
public:
string _name = "peter";
int _age = 18;
};
class Student : public Person {
public:
int _stuNum = 1;
};
3.1 三种赋值兼容方式
Student st;
// 1. 派生类对象赋值给基类对象——切片(slicing)
Person p = st; // ✅ 只拷贝 Person 部分,_stuNum 被"切掉"
// 2. 派生类对象初始化基类引用
Person& ref = st; // ✅ ref 引用的是 st 中的 Person 部分
// 3. 派生类对象的地址赋给基类指针
Person* ptr = &st; // ✅ ptr 指向 st 中的 Person 部分

课堂笔记:派生类到基类的切片转换关系。子类对象赋值给父类对象时,子类特有的成员被"切掉"
3.2 反过来不行
Person p;
// Student st = p; // ❌ 基类对象不能赋值给派生类
// Student& ref = p; // ❌ 基类引用不能绑定到派生类
// Student* ptr = &p; // ❌ 基类指针不能直接赋给派生类
为什么不行? 基类对象中没有派生类特有的成员。如果允许父类隐式转成子类,访问 st._stuNum 时就会访问到不存在的数据——这是未定义行为。
3.3 切片后的操作
Student st;
Person p = st;
Person& ref = st;
Person* ptr = &st;
// 通过基类引用/指针操作,只能访问基类部分
ref._name += 'x'; // ✅ 修改的是 st 中的 _name
ptr->_name += 'y'; // ✅ 也是修改 st 中的 _name
// ref._stuNum; // ❌ 基类引用看不到派生类特有的成员
四、继承中的作用域
4.1 基类和派生类是独立的作用域
class Person {
protected:
string _name = "peter";
int _num = 0; // Person 的 _num
};
class Student : public Person {
public:
void func() {
// Student 中也有 _num
cout << _num << endl; // 默认访问子类的 _num
cout << Person::_num << endl; // 显式访问父类继承来的 _num
}
protected:
int _num = 1; // Student 自己的 _num
};
输出:
1 // 子类的 _num
0 // 父类的 _num
这里两个 _num 是不同的变量——一个在 Person 的作用域中,一个在 Student 的作用域中。Student 中的 _num 隐藏(hides)了 Person 中的 _num。
4.2 同名成员函数隐藏
同名成员函数也会被隐藏——而且与参数列表无关(这和函数重载不同)。
class A {
public:
void fun() {
cout << "func()" << endl;
}
};
class B : public A {
public:
void fun(int i) { // 隐藏了 A 的 fun()
cout << "func(int i)->" << i << endl;
}
};
int main() {
B bb;
// bb.fun(); // ❌ 编译错误!B 的 fun(int) 隐藏了 A 的 fun()
bb.fun(1); // ✅ 调用 B 自己的 fun(int)
bb.A::fun(); // ✅ 显式指定作用域,调用 A 的 fun()
return 0;
}
🚨 重点强调:派生类中定义了一个和基类同名的函数(即使参数不同),基类的所有同名函数都会被隐藏。这和函数重载(在同一作用域中参数不同构成重载)完全是两码事。
五、派生类的默认成员函数
这一节是面试高频考点。派生类有自己的一套构造、拷贝构造、赋值、析构规则。

课堂笔记:派生类默认成员函数——构造/拷贝/赋值/析构的调用顺序
5.1 构造函数
class Person {
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p) {
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person() {
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person {
public:
// 派生类构造函数——必须调用基类构造函数初始化基类部分
Student(int num, const char* str, const char* name)
: Person(name) // ✅ 显式调用基类构造函数
, _num(num)
, _str(str)
{
cout << "Student()" << endl;
}
// ...
protected:
int _num;
string _str;
};
构造顺序:
Person() ← 先构造基类部分
Student() ← 再构造派生类部分
如果你不显式调用基类构造函数:
Student(int num, const char* str, const char* name)
// 没有 Person(...) —— 编译器会尝试调用 Person 的默认构造
// 但 Person 没有默认构造——编译错误!
, _num(num)
, _str(str)
{}
💡 规则:派生类的构造函数中,基类的构造函数是必须被调用的。如果不显式调用,编译器会调用基类的默认构造函数(如果存在的话)。基类没有默认构造时,你必须显式调用。
5.2 拷贝构造函数
class Student : public Person {
public:
// s2(s1)——派生类拷贝构造
Student(const Student& s)
: Person(s) // ✅ 调用基类的拷贝构造(s 切片成 Person)
, _num(s._num)
, _str(s._str)
{}
};
这里 Person(s) 是把 Student 对象 s 传给 Person(const Person& p)——因为 Student 可以切片成 Person,所以这是合法的。
5.3 赋值运算符重载
class Student : public Person {
public:
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // ✅ 必须显式调用基类的 operator=
_num = s._num;
_str = s._str;
}
return *this;
}
};
💡
Person::operator=(s)一定要加Person::,否则operator=(s)会递归调用自己——死循环。
5.4 析构函数
class Student : public Person {
public:
~Student() {
cout << _name << endl;
cout << "~Student()" << endl;
// ⚠️ 这里不需要手动调用 Person::~Person()
// 子类析构函数结束后,会自动调用父类析构函数
}
};
析构顺序和构造相反:
~Student() ← 先析构派生类部分
~Person() ← 再析构基类部分(自动调用)
为什么必须是先子后父?
假设派生类中有一个指针成员指向了基类成员管理的数据。如果先析构基类,基类的资源被释放了,派生类析构时再去访问就崩了。
main() {
Student s1(1, "xxxx", "张三");
Student s2(s1); // 拷贝构造
// 离开作用域时:
// 1. ~Student() —— 析构子类部分
// 2. ~Person() —— 析构父类部分(自动)
}
5.5 构造和析构顺序总结
构造顺序:基类构造 → 派生类构造
析构顺序:派生类析构 → 基类析构(自动)
💡 课堂上的一个形象理解:建造房子时,先打地基(父类),再盖上层(子类)。拆除时,先拆上层(子类),再拆地基(父类)——你不会把地基先拆了然后再拆上面的墙。
六、继承与静态成员
6.1 基类的静态成员被所有派生类共享
class Person {
public:
Person() { ++_count; }
Person(const Person& p) { ++_count; }
protected:
string _name;
public:
static int _count; // 统计人的个数
};
int Person::_count = 0; // 类外定义静态成员
class Student : public Person {
protected:
int _stuNum;
};
class Graduate : public Student {
protected:
string _seminarCourse;
};
int main() {
Person p;
Student s1;
Student s2;
Student s3(s1);
Graduate g1;
cout << Person::_count << endl; // 5
cout << Student::_count << endl; // 5 —— 同一份!
cout << &Person::_count << endl; // 某个地址
cout << &Student::_count << endl; // 完全相同的地址
return 0;
}
💡 关键:静态成员在整个继承体系中只有一份。Person 的
_count和 Student 的_count指的是同一个变量。不管是Person::_count还是Student::_count,访问的都是同一个地址上的同一个值。
七、继承与友元
7.1 友元关系不能被继承
class Person {
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person {
// friend void Display(const Person& p, const Student& s);
// ⚠️ 即使取消注释,Display 也只能访问 Student 继承自 Person 的 protected 成员
// 不能访问 Student 自己的 protected 成员(比如 _stuNum)
// 除非在 Student 中再声明一次 friend
protected:
int _stuNum;
};
// 即使 Person 声明了 friend,Display 也不能访问 Student::_stuNum
void Display(const Person& p, const Student& s) {
cout << p._name << endl; // ✅ Person 的 friend,可以访问 Person 的 _name
// cout << s._stuNum << endl; // ❌ Student 没有声明 Display 为友元
}
规则:
- 基类的友元不能访问派生类的私有/保护成员
- 派生类的友元不能访问基类的私有/保护成员
- 基类友元访问派生类成员时,只能访问派生类中从基类继承来的成员
八、多继承
8.1 什么是多继承
C++ 允许一个派生类有多个基类——这叫多继承(Multiple Inheritance)。
class Student {
protected:
int _num;
};
class Teacher {
protected:
int _id;
};
// Assistant 同时继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
多继承的语法:class 派生类 : 继承方式 基类1, 继承方式 基类2, ...
8.2 多继承中的同名字段
如果多个基类中有同名的成员,访问时会产生二义性:
class Student {
public:
string _name;
};
class Teacher {
public:
string _name;
};
class Assistant : public Student, public Teacher {
};
int main() {
Assistant a;
// a._name = "peter"; // ❌ 二义性!不知道用 Student 的还是 Teacher 的
a.Student::_name = "peter"; // ✅ 显式指定
a.Teacher::_name = "mary"; // ✅ 显式指定
return 0;
}
九、菱形继承
9.1 菱形继承的结构
多继承会引出一个经典问题——菱形继承(也叫钻石继承)。
Person
/ \
Student Teacher
\ /
Assistant
class Person {
public:
string _name;
};
class Student : public Person {
protected:
int _num;
};
class Teacher : public Person {
protected:
int _id;
};
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
9.2 菱形继承的问题
int main() {
Assistant a;
// a._name = "peter"; // ❌ 二义性!哪一份 _name?
a.Student::_name = "xxx"; // ✅ 指定 Student 路线继承的 _name
a.Teacher::_name = "yyy"; // ✅ 指定 Teacher 路线继承的 _name
return 0;
}
菱形继承有两个严重问题:
问题 1:数据冗余
Assistant 对象中有两份 _name——一份从 Student 继承(Student 又从 Person 继承),一份从 Teacher 继承(Teacher 也从 Person 继承)。
// 内存布局(示意):
// Assistant 对象:
// [Student部分]
// [Person部分]
// _name ← 第一份
// _num
// [Teacher部分]
// [Person部分]
// _name ← 第二份(冗余!)
// _id
// _majorCourse
问题 2:二义性
访问 a._name 时不知道访问的是 Student 路线继承来的还是 Teacher 路线继承来的,必须显式指定 a.Student::_name 或 a.Teacher::_name。但即使显式指定,数据冗余问题依然存在——两个 _name 是不同的变量,分别赋值互不影响。
十、虚拟继承
10.1 virtual 关键字
虚拟继承(Virtual Inheritance)用来解决菱形继承的数据冗余和二义性问题:
class Person {
public:
string _name;
};
// 加上 virtual
class Student : virtual public Person {
protected:
int _num;
};
// 也加上 virtual
class Teacher : virtual public Person {
protected:
int _id;
};
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
虚拟继承后:
int main() {
Assistant a;
a._name = "peter"; // ✅ 不再有二义性!只有一份 _name
cout << a._name << endl; // "peter"
// 当然,显式指定也可以
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
cout << a._name << endl; // "yyy"——因为 Student 和 Teacher 访问的是同一份
return 0;
}
虚拟继承之后,Assistant 对象中的 _name 只有一份,既没有数据冗余,也没有二义性。
10.2 虚拟继承的底层原理
虚拟继承是如何做到"只有一份"的?答案是虚基表(vbtable)。
class A {
public:
int _a;
};
class B : virtual public A {
public:
int _b;
};
class C : virtual public A {
public:
int _c;
};
class D : public C, public B {
public:
int _d;
};

课堂笔记:虚基表和虚基表指针的示意图——D 对象中 B 和 C 部分各有一个 vbptr 指向虚基表,通过偏移量找到公共的 A 部分
对象布局(简化版):
D 对象内存布局(虚拟继承):
+------------------+
| B 部分: |
| vbptr | ← 指向 B 的虚基表(记录 B 到 A 的偏移量)
| _b |
| _b1 |
| _b2 |
+------------------+
| C 部分: |
| vbptr | ← 指向 C 的虚基表(记录 C 到 A 的偏移量)
| _c |
+------------------+
| D 部分: |
| _d |
+------------------+
| A 部分: |
| _a | ← 共享的 A,只有一份!
+------------------+
虚基表中存放的是偏移量——告诉你从当前对象的某个位置到共享基类(A)的偏移距离。
D d;
d._a = 0; // ✅ 没有二义性——只有一份 A::_a
d._b = 3;
d._c = 4;
d._d = 5;
// 切片时,指针会做偏移调整
B* pb = &d; // pb 指向 D 中的 B 部分
C* pc = &d; // pc 指向 D 中的 C 部分
// 即使 pb 和 pc 指向 D 中不同位置,
// 通过它们访问 _a 时,会通过 vbptr + 偏移量找到同一个 _a
pb->_a++; // _a 变为 1
pc->_a++; // _a 变为 2(和 pb 操作的是同一个 _a)
💡 理解关键:虚拟继承是通过间接寻址(加上一层偏移量计算)来实现的。代价是访问虚基类成员的效率比普通继承稍低(多一次间接寻址),换来了"数据不冗余"和"无二义性"。
十一、组合 vs 继承
11.1 两种复用方式
继承是代码复用的方式之一,但不是唯一的方式。另一种常见的方式是组合(Composition)。
// 继承——is-a 关系
class A { /* ... */ };
class B : public A { /* ... */ }; // B "是一个" A
// 组合——has-a 关系
class A { /* ... */ };
class B {
A _aa; // B "有一个" A
int _b;
};
11.2 组合示例
class Person {
public:
Person(int num, const char* str, const char* name)
: _p(name)
, _num(num)
, _str(str)
{}
private:
string _name; // Person 自己管理 name
int _age;
};
// 组合方式——B 中包含 A 对象
class BB {
public:
BB(int num, const char* str, const char* name)
: _p(name) // 初始化组合的对象
, _num(num)
, _str(str)
{}
private:
Person _p; // 组合:BB "有一个" Person
int _num;
string _str;
};
11.3 继承 vs 组合:什么时候用什么?
| 对比维度 | 继承(is-a) | 组合(has-a) |
|---|---|---|
| 关系 | Student 是 Person | Car 有 Engine |
| 耦合度 | 高——派生类依赖基类的实现 | 低——通过接口使用组件 |
| 代码复用 | 自动继承基类成员 | 通过包含对象使用其功能 |
| 可维护性 | 基类改变可能影响所有派生类 | 组件替换不影响整体 |
| 适用场景 | 层次关系明确,子类确实是一种父类 | 功能模块组合 |
设计原则:优先使用组合,而不是继承。
但继承在某些场景下是不可替代的:
- 需要多态(虚函数)——组合不能实现多态
- 清晰的层次关系——「人 → 学生 → 研究生」这种层次用继承最自然
- 需要访问基类的 protected 成员——组合不能访问私有或保护成员
十二、继承和多态的关系
继承是多态(Polymorphism)的前提。简单说:没有继承,就没有多态。
多态的核心是虚函数(virtual function):
class Person {
public:
virtual void BuyTicket() {
cout << "Person->买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() { // 重写(override)基类的虚函数
cout << "Student->买票-半价" << endl;
}
};
// 多态:指向谁,调用谁的虚函数
void Func(Person* p) {
p->BuyTicket(); // 取决于 p 实际指向什么对象
}
int main() {
Person ps;
Student st;
Func(&ps); // 输出:Person->买票-全价
Func(&st); // 输出:Student->买票-半价
return 0;
}
多态的两个条件:
- 父子类完成虚函数重写(三同:函数名、参数、返回值)
- 用父类的指针或引用调用虚函数
“指向谁,调用谁”——这就是多态。基类指针可以指向不同的派生类对象,调用同一个虚函数却表现出不同的行为。
多态的内容非常丰富,本篇只做简单引入。后续会有专门的博客详细讲解虚函数、抽象类、多态的底层原理(虚函数表等)。
十三、常见易错点总结
| # | 易错点 | 说明 |
|---|---|---|
| 1 | 派生类不能继承基类的友元关系 | 友元是单向授予,不能传递和继承 |
| 2 | 派生类不能继承基类的静态成员 | 静态成员是共享的——只有一个副本 |
| 3 | 派生类中访问同名成员默认访问自己的 | 想访问基类的需要加 基类:: |
| 4 | 派生类函数隐藏基类同名函数 | 即使参数不同也是隐藏,不是重载 |
| 5 | 派生类构造函数必须调用基类构造函数 | 不显式调用则调用基类的默认构造 |
| 6 | 派生类拷贝构造必须调用基类拷贝构造 | Person(s) 利用切片 |
| 7 | 派生类 operator= 必须显式调用基类 operator= | 否则基类部分不会被赋值 |
| 8 | 析构顺序和构造相反:先子后父 | 编译器自动保证,不要手动调用父类析构 |
| 9 | 切片(slicing)只发生在派生类→基类 | 基类不能隐式转换为派生类 |
| 10 | 菱形继承会产生数据冗余和二义性 | 用虚拟继承解决 |
| 11 | 虚拟继承使用虚基表(vbtable) | 通过偏移量找到共享的基类部分 |
| 12 | class 类默认 private 继承,struct 默认 public | 注意区别 |
| 13 | protected 成员不能被外部访问,但能被派生类访问 | 和 private 的区别就在这里 |
| 14 | 不要总用继承,优先考虑组合 | 耦合度更低,更灵活 |
十四、面试 / 学习常见问题
1. 什么是继承?为什么需要继承?
继承是面向对象三大特性之一,允许在已有类(基类)的基础上创建新类(派生类),派生类自动拥有基类的成员。继承的核心目的是代码复用——避免重复编写相同的代码,同时便于维护。
2. C++ 有哪几种继承方式?各有什么区别?
三种:public 继承、protected 继承、private 继承。继承方式决定了基类成员在派生类中的访问权限变化。最常用的是 public 继承。基类的 private 成员在任何继承方式下都不可见。
3. 什么是对象切片?
派生类对象赋值给基类对象时,派生类特有的成员被"切掉",只保留基类部分。派生类对象可以赋值给基类对象、初始化基类引用、赋给基类指针。反过来不行。
4. 派生类的作用域和基类的关系是什么?
派生类和基类有各自独立的作用域。派生类中定义了和基类同名的成员时,基类的成员被隐藏(也叫重定义)。注意是隐藏不是重载——函数隐藏与参数列表无关。
5. 派生类的构造函数有什么注意事项?
必须调用基类的构造函数来初始化从基类继承来的成员。如果不显式调用,编译器会调用基类的默认构造函数。如果基类没有默认构造,就必须显式调用。构造顺序:先基类后派生类。
6. 派生类的拷贝构造怎么写?
Student(const Student& s) : Person(s), _num(s._num), _str(s._str) {}。必须显式调用基类的拷贝构造,Person(s) 利用切片将 s 的基类部分传给基类拷贝构造。
7. 派生类的赋值重载有什么注意事项?
必须显式调用基类的 operator=:Person::operator=(s)。如果漏掉,基类部分的数据不会被赋值。还要处理自赋值判断。
8. 派生类的析构顺序是什么?
先调用派生类析构函数,再自动调用基类析构函数(编译器保证)。析构顺序和构造顺序相反。不要手动调用基类析构函数。
9. 静态成员在继承中有什么特性?
基类中定义的静态成员,在整个继承体系中只有一份。通过基类或任何派生类访问的都是同一个变量。静态成员属于类,不属于对象,所有派生类共享。
10. 友元关系可以继承吗?
不能。基类的友元函数不能访问派生类的私有/保护成员。派生类的友元函数也不能访问基类的私有/保护成员。友元是单向授予的,不能传递。
11. 什么是菱形继承?有什么问题?
菱形继承是指一个派生类通过两个中间基类间接继承同一个基类(如 Assistant → Student/Teacher → Person)。问题一:数据冗余——派生类对象中有多份顶层基类的成员。问题二:二义性——访问顶层基类成员时不知道该走哪条路径。
12. 虚拟继承是如何解决菱形继承问题的?
通过 virtual 关键字:class Student : virtual public Person。虚拟继承后,派生类对象中只保留一份顶层基类的成员。底层通过**虚基表(vbtable)**存储偏移量,通过 vbptr(虚基表指针)找到共享基类的位置。代价是访问效率略低(多一次间接寻址)。
13. 组合和继承有什么区别?各有什么优缺点?
继承是 is-a 关系(Student is a Person),组合是 has-a 关系(Car has an Engine)。继承耦合度高,基类改变影响所有派生类;组合耦合度低,组件可替换。设计上优先考虑组合,但多态必须使用继承。
14. 继承和多态有什么关系?
继承是多态的前提。多态通过基类指针或引用调用虚函数,根据实际指向的对象类型表现出不同的行为。条件是:虚函数重写 + 基类指针/引用调用。
15. 派生类中如果定义了和基类同名的函数会发生什么?
基类的所有同名函数都会被隐藏(无论参数是否相同)。这和函数重载不同——重载发生在同一作用域中。想调用基类的隐藏函数需要加 基类::。
十五、最后总结
继承的核心思想
继承解决的核心问题是代码复用——站在已有代码的肩膀上构建新类。
继承的本质:复用 + 扩展
┌────────────────┐
│ 已有的基类 │ ← 复用已有的代码
│ (Person) │
└────────┬───────┘
│ 继承
┌────────▼───────┐
│ 新增的派生类 │ ← 在复用基础上添加新功能
│ (Student) │
└────────────────┘
本篇的关键知识点
- 继承语法:
class 派生类 : public 基类,最常用的是 public 继承 - 切片:派生类可以当作基类使用(赋值、引用、指针),反之不行
- 隐藏规则:派生类同名成员会隐藏基类的同名成员
- 构造/析构顺序:先父后子构造,先子后父析构
- 菱形继承:多继承带来的数据冗余和二义性问题,用 virtual 继承解决
- 虚拟继承:通过虚基表(vbtable)和偏移量实现共享基类
- 组合优先:能用组合就别用继承,但多态必须用继承
后续学习衔接
- 本篇讲了继承的语法和原理
- 下一站:多态——虚函数、虚函数表、抽象类、运行时类型识别
- 再往后:C++11 新特性、异常处理、IO 流等
继承是理解 C++ 面向对象的"第二阶段"——第一阶段是类与对象(封装),第二阶段就是继承,第三阶段是多态。三个阶段层层递进,缺一不可。
Person → Student不只是代码上的继承,也是 C++ 面向对象设计思想的延续。理解继承,你就理解了"在已有基础上扩展"的编程思维——这种思维不仅适用于 C++,也适用于一切面向对象语言。
笔记整理自课堂学习(Lesson01–继承)和老师课件代码。代码示例基于课堂 Person/Student/Teacher/Assistant 系列。如有错误或疏漏,欢迎指正。
更多推荐
所有评论(0)