目录
编辑
本节学习目标
1. 继承的概念及定义
1.1 什么是继承
1.2 继承定义语法
基类成员在派生类的访问规则
1.3 继承模板类(模板父类)
2. 基类与派生类对象转换(切片 / 切割 slicing)
3. 继承体系下作用域与隐藏(name‑hiding)
同名成员变量隐藏示例
同名成员函数隐藏(重点选择题)
4. 派生类的默认 6 个成员函数
派生类默认成员函数执行规则
4.2 禁止类被继承两种写法
5. 继承与友元
6. 继承与静态成员
7. 多继承、菱形继承与虚继承 virtual
7.1 单继承、多继承
7.2 菱形继承(钻石继承)—— 多继承特殊坑
7.3 虚继承 virtual 解决菱形继承
8. 继承与组合
8.1 is‑a vs has‑a
📔本篇核心考点总结
本节学习目标
- 理解继承的概念,解决代码冗余复用问题,分清父类 (基类)/ 子类 (派生类)
- 掌握三种继承方式
public / protected / private,掌握基类成员在派生类中的访问规则 - 理解派生类对象切片,基类派生类指针引用转换
- 继承体系作用域、成员隐藏(name‑hiding),区分隐藏和重载
- 派生类 6 个默认成员函数:构造、拷贝构造、赋值重载、析构的执行规则
- final 禁止继承,继承与友元、继承与静态成员
- 理解多继承、菱形继承带来二义性与数据冗余,掌握虚继承
virtual解决菱形问题 - 分清
is‑a继承 和has‑a组合,理解白箱复用、黑箱复用,开发优先组合
1. 继承的概念及定义
1.1 什么是继承
继承是面向对象实现代码复用最重要机制。 在保留原有类(基类 / 父类)全部特性的基础之上,扩展出新的类(派生类 / 子类)。
场景:学生 Student、老师 Teacher。两者都包含姓名、地址、电话、年龄、身份认证
identity();学生独有学号、学习;老师独有职称、授课。 不使用继承,两份类中重复写相同成员,代码大量冗余。
把公共属性、方法抽出来放到Person父类;Student、Teacher继承 Person,直接复用父类成员,只写自己独有的内容。
#include <iostream> #include <string> using namespace std; //父类(基类) class Person { public: void identity() { cout << "void identity() " << _name << endl; } protected: string _name = "张三"; string _address; string _tel; int _age = 18; }; //子类(派生类) public继承Person class Student : public Person { public: void study() { } protected: int _stuid; //学号 }; class Teacher : public Person { public: void teaching() { } protected: string _title; //职称 }; int main() { Student s; Teacher t; s.identity(); //复用父类方法 t.identity(); return 0; }1.2 继承定义语法
class 派生类名 : 继承方式 基类名 { //派生类自己新增的成员 };继承三种方式:public、protected、private
基类成员在派生类的访问规则
- 基类的 private 私有成员:无论哪种继承方式,在派生类内部都不可见。
注意:不是没有继承到子类对象中,只是语法禁止访问,对象内存里面依然存在这份数据。 如果希望子类可以访问,但是外部不能访问,成员设置为
protected,protected 就是为继承场景诞生。
- 简单口诀:
派生类中访问权限 = min(基类成员权限,继承方式)权限优先级:public > protected > private
表格
| 基类成员 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可见 | 不可见 | 不可见 |
- 语法细节:
- 使用
class定义类,默认继承方式是 private; - 使用
struct定义类,默认继承方式是 public; - ✅工程开发绝大多数场景只用
public继承;protected/private继承极少使用,扩展性差。
示例演示三种继承:
class Person { public: void Print() { cout << _name << endl; } protected: string _name; private: int _age; }; // class Student : protected Person; // class Student : private Person; class Student : public Person { protected: int _stunum; };1.3 继承模板类(模板父类)
如果父类是类模板,子类继承的时候,必须带上基类模板域,否则编译器找不到基类成员。 例:stack 继承 vector,模拟栈。
namespace bit { template<class T> class stack : public std::vector<T> { public: void push(const T& x) { // 必须写 vector<T>::push_back(x); // push_back(x); //直接写会编译报错,模板按需实例化找不到名字 vector<T>::push_back(x); } void pop() { vector<T>::pop_back(); } const T& top() { return vector<T>::back(); } bool empty() { return vector<T>::empty(); } }; } int main() { bit::stack<int> st; st.push(1); st.push(2); st.push(3); while(!st.empty()) { cout << st.top() << " "; st.pop(); } return 0; }2. 基类与派生类对象转换(切片 / 切割 slicing)
只针对public 公有继承
- 派生类对象可以赋值给基类对象、基类指针、基类引用
切片(切割):把派生类对象里面属于基类的那一部分切出来,交给基类。派生类独有的成员直接被切掉丢弃。
class Person { protected: string _name; string _sex; int _age; }; class Student : public Person { public: int _No; //学号 }; int main() { Student sobj; //1.派生类 → 基类指针 Person* pp = &sobj; //2.派生类 → 基类引用 Person& rp = sobj; //3.派生类赋值给基类对象(发生对象切片,调用基类拷贝构造) Person pobj = sobj; // ❌基类对象不能直接赋值给派生类对象,编译报错 // sobj = pobj; return 0; }- 基类普通对象不能直接赋值给派生类对象。
- 基类指针如果本来指向派生类对象,可以强制转换为派生类指针;安全转换后面多态章节学习
dynamic_cast。
3. 继承体系下作用域与隐藏(name‑hiding)
- 基类、派生类拥有相互独立的作用域。
- 成员隐藏(遮蔽):派生类和基类出现同名成员,派生类成员会屏蔽基类同名成员。
注意:只要函数名字一样就构成隐藏,参数不一样也隐藏,不是重载!重载只发生在同一个作用域!
- 如果需要在子类访问被隐藏的基类成员,使用
基类::成员名显式指定类域。
工程编码建议:尽量不要写同名成员,可读性差容易踩坑。
同名成员变量隐藏示例
class Person { protected: string _name = "小李子"; int _num = 111; //身份证号 }; class Student : public Person { public: void Print() { cout << "姓名:" << _name << endl; cout << "身份证号:" << Person::_num << endl; //显式指定基类域访问被隐藏成员 cout << "学号:" << _num << endl; } protected: int _num = 999; //学号,和父类_num同名,发生隐藏 }; int main() { Student s1; s1.Print(); return 0; }同名成员函数隐藏(重点选择题)
class A { public: void fun() { cout << "A::fun()" << endl; } }; class B : public A { public: //函数名字相同,参数不同,构成【隐藏】,不是重载! void fun(int i) { cout << "B::fun(int i)" << i << endl; } }; int main() { B b; b.fun(10); //调用B::fun(int) // b.fun(); //编译报错,子类作用域把父类fun()隐藏了! b.A::fun(); //想要调用父类版本,必须显式指定类域 return 0; }考点:重载必须同一个类同一个作用域;父子类之间同名函数,不管参数是否一样,都是隐藏。
4. 派生类的默认 6 个成员函数
编译器在满足条件时自动生成 6 个默认成员函数:构造、拷贝构造、赋值运算符重载、析构、取地址两个重载。
派生类默认成员函数执行规则
- 派生类构造函数:初始化列表阶段必须调用基类构造函数初始化继承过来的基类部分; 如果基类没有无参默认构造,派生类必须在初始化列表显式调用对应基类构造。
对象构造顺序:先基类构造,再派生类构造。
- 派生类拷贝构造:必须调用基类拷贝构造,完成基类部分拷贝。
- 派生类 operator = 赋值重载:必须调用基类的赋值重载;派生类会隐藏基类的 operator=,需要显式
基类::operator=(s)调用。 - 派生类析构函数:派生类析构执行完毕,编译器自动调用基类析构;
对象析构顺序:先派生类析构,再基类析构,和构造顺序相反。
- 析构函数名字会被编译器特殊处理为
destructor(),基类和派生类析构函数名字不一样,普通继承下构成隐藏,不是重写;多态章节 virtual 虚析构才会重写。
完整示例代码:
#include <iostream> #include <string> using namespace std; class Person { public: Person(const char* name = "peter") :_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(const char* name, int num) : Person(name) , _num(num) { cout << "Student()" << endl; } //派生类拷贝构造,调用基类拷贝构造 Student(const Student& s) : Person(s) , _num(s._num) { cout << "Student(const Student& s)" << endl; } Student& operator=(const Student& s) { cout << "Student& operator=(const Student& s)" << endl; if(this != &s) { //显式调用基类赋值重载,派生类会隐藏基类operator= Person::operator=(s); _num = s._num; } return *this; } ~Student() { cout << "~Student()" << endl; //编译器自动在函数末尾调用 ~Person(),不需要手写 } protected: int _num; }; int main() { Student s1("jack",18); Student s2(s1); Student s3("rose",17); s1 = s3; return 0; }4.2 禁止类被继承两种写法
- C++11
final关键字修饰类,该类不能被继承(推荐)
class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; }; // class Derive : public Base{}; //编译报错,Base被final禁止继承- C++98 旧方式:把基类构造函数设置为 private。派生类实例化对象必须调用基类构造,基类构造私有子类无法访问,子类不能实例化。
5. 继承与友元
❗友元关系不会继承。基类的友元函数,不能直接访问派生类的 protected/private 成员。
#include <iostream> #include <string> using namespace std; class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; }; class Student : public Person { protected: int _stuNum; }; void Display(const Person& p, const Student& s) { cout << p._name << endl; // cout << s._stuNum << endl; //编译报错!友元不继承,不能访问Student的_stuNum } int main() { Person p; Student s; // Display(p,s); //报错 return 0; }解决:需要把 Display 也设置为 Student 的友元。
6. 继承与静态成员
基类里面的 static 静态成员,整个继承体系之内只有唯一一份,所有派生类对象共享同一份静态成员。 可以通过基类名、派生类名访问 public 静态成员。
#include <iostream> #include <string> using namespace std; class Person { public: string _name; static int _count; }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; }; int main() { Person p; Student s; cout << &p._name << endl; //普通成员每个对象独立一份,地址不同 cout << &s._name << endl; cout << &p._count << endl; cout << &s._count << endl; //静态成员地址完全相同,共享同一份 cout << Person::_count << endl; cout << Student::_count << endl; return 0; }7. 多继承、菱形继承与虚继承 virtual
7.1 单继承、多继承
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类拥有两个及以上直接基类。
对象内存排布:继承列表从左到右,先继承的基类放在内存低地址,后继承放后面,派生类自己成员放最后。
7.2 菱形继承(钻石继承)—— 多继承特殊坑
继承关系:Assistant助理同时继承Student和Teacher;Student、Teacher 都继承 Person。
问题:
- 数据冗余:Assistant 对象里面会存在两份 Person 基类数据(一份来自 Student 路径,一份来自 Teacher 路径);
- 二义性:直接访问
Assistant._name编译器不知道走哪一条继承路径,编译报错。
#include <iostream> #include <string> using namespace std; class Person { public: string _name; }; class Student : public Person { protected: int _num; //学号 }; class Teacher : public Person { protected: int _id; //职工编号 }; //助理,多继承 Student、Teacher class Assistant : public Student, public Teacher { protected: string _majorCourse; }; int main() { Assistant a; // a._name = "peter"; //编译报错,二义性!两份_name不知道选哪一个 //强制指定路径可以访问,但是不能解决数据冗余 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; return 0; }7.3 虚继承 virtual 解决菱形继承
在中间层继承的时候写
virtual public Person虚继承;公共父类 Person 在子类对象里面只保存唯一一份,消除冗余和二义性。
#include <iostream> #include <string> using namespace std; class Person { public: string _name; }; //虚继承 class Student : virtual public Person { protected: int _num; }; //虚继承 class Teacher : virtual public Person { protected: int _id; }; class Assistant : public Student, public Teacher { protected: string _majorCourse; }; int main() { Assistant a; a._name = "peter"; //不再二义性,只有唯一一份Person return 0; }⚠️注意:虚继承会引入虚基表,底层实现复杂,有一定性能开销;工程尽量不要设计菱形继承。Java 直接取消多继承从根源规避这个问题。
扩展:C++ 标准 IO 库
ios_base就是典型虚继承菱形结构。
8. 继承与组合
8.1 is‑a vs has‑a
- public 继承:is‑a 是一个
Student is‑a Person;BMW is‑a Car。 继承属于白箱复用:基类内部细节对子类可见;父子类耦合度很高,基类改动会强烈影响子类。
- 组合:has‑a 拥有一个
Car has‑a Tire(汽车拥有轮胎)。把别的类作为自己的成员变量。 组合属于黑箱复用,只使用对方公开接口,内部细节不可见;耦合低,维护性更好。
✨开发经验:优先使用组合;只有满足 is‑a 语义再用继承;实现多态必须使用继承。
示例对比 stack 两种实现:
//方式1:继承 vector is‑a(不推荐) template<class T> class stack : public std::vector<T> {}; //方式2:组合 has‑a(推荐,容器适配器真实实现思路) template<class T> class stack { public: std::vector<T> _v; };📔本篇核心考点总结
- 继承实现类层次代码复用;三种继承方式
public/protected/private;基类 private 成员子类永远不可见;口诀min(成员权限,继承方式)。开发只用 public 继承。 - 派生类对象切片:派生类可以赋值给基类指针 / 引用 / 对象;基类不能直接赋值给派生类。
- 父子类同名成员构成隐藏 name‑hiding,不是重载,重载仅限同一个作用域;
基类::成员访问被隐藏成员。 - 默认成员函数:构造先基类后派生类;析构先派生类后基类;拷贝构造、赋值重载要处理基类部分。final 修饰类禁止继承。
- 友元不能继承;static 静态成员继承体系共享同一份。
- 多继承会带来菱形继承:两份公共基类,数据冗余 + 访问二义性;
virtual虚继承解决;尽量避免菱形继承。 - is‑a 用继承,has‑a 优先用组合;组合耦合低,优先组合。
面试简答
Q:基类 private 成员子类能不能继承?能不能访问?
A:内存上继承到子类对象,但是语法不可访问;子类需要访问就设置为 protected。
Q:成员隐藏和重载区别?
A:重载在同一个作用域,函数名相同参数不同;父子不同作用域只要名字相同就是隐藏,和参数无关。
Q:菱形继承问题怎么解决?
A:中间层使用 virtual 虚继承;最好设计上避免菱形继承。
Q:继承和组合怎么选?
A:is‑a 是继承;has‑a 优先组合,组合耦合更低。