【C++进阶】继承
2026/9/24 18:00:47 网站建设 项目流程

目录

​编辑

本节学习目标

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

📔本篇核心考点总结

本节学习目标

  1. 理解继承的概念,解决代码冗余复用问题,分清父类 (基类)/ 子类 (派生类)
  2. 掌握三种继承方式public / protected / private,掌握基类成员在派生类中的访问规则
  3. 理解派生类对象切片,基类派生类指针引用转换
  4. 继承体系作用域、成员隐藏(name‑hiding),区分隐藏和重载
  5. 派生类 6 个默认成员函数:构造、拷贝构造、赋值重载、析构的执行规则
  6. final 禁止继承,继承与友元、继承与静态成员
  7. 理解多继承、菱形继承带来二义性与数据冗余,掌握虚继承virtual解决菱形问题
  8. 分清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 派生类名 : 继承方式 基类名 { //派生类自己新增的成员 };

继承三种方式:publicprotectedprivate

基类成员在派生类的访问规则
  1. 基类的 private 私有成员:无论哪种继承方式,在派生类内部都不可见

注意:不是没有继承到子类对象中,只是语法禁止访问,对象内存里面依然存在这份数据。 如果希望子类可以访问,但是外部不能访问,成员设置为protected,protected 就是为继承场景诞生。

  1. 简单口诀:派生类中访问权限 = min(基类成员权限,继承方式)权限优先级:public > protected > private

表格

基类成员public 继承protected 继承private 继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可见不可见不可见
  1. 语法细节:
  • 使用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 公有继承

  1. 派生类对象可以赋值给基类对象、基类指针、基类引用

切片(切割):把派生类对象里面属于基类的那一部分切出来,交给基类。派生类独有的成员直接被切掉丢弃。

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)

  1. 基类、派生类拥有相互独立的作用域
  2. 成员隐藏(遮蔽):派生类和基类出现同名成员,派生类成员会屏蔽基类同名成员。

注意:只要函数名字一样就构成隐藏,参数不一样也隐藏,不是重载!重载只发生在同一个作用域!

  1. 如果需要在子类访问被隐藏的基类成员,使用基类::成员名显式指定类域。

工程编码建议:尽量不要写同名成员,可读性差容易踩坑。

同名成员变量隐藏示例

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 个默认成员函数:构造、拷贝构造、赋值运算符重载、析构、取地址两个重载。

派生类默认成员函数执行规则

  1. 派生类构造函数:初始化列表阶段必须调用基类构造函数初始化继承过来的基类部分; 如果基类没有无参默认构造,派生类必须在初始化列表显式调用对应基类构造。

对象构造顺序:先基类构造,再派生类构造

  1. 派生类拷贝构造:必须调用基类拷贝构造,完成基类部分拷贝。
  2. 派生类 operator = 赋值重载:必须调用基类的赋值重载;派生类会隐藏基类的 operator=,需要显式基类::operator=(s)调用。
  3. 派生类析构函数:派生类析构执行完毕,编译器自动调用基类析构;

对象析构顺序:先派生类析构,再基类析构,和构造顺序相反

  1. 析构函数名字会被编译器特殊处理为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 禁止类被继承两种写法

  1. C++11final关键字修饰类,该类不能被继承(推荐)
class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; }; // class Derive : public Base{}; //编译报错,Base被final禁止继承
  1. 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助理同时继承StudentTeacher;Student、Teacher 都继承 Person。

问题:

  1. 数据冗余:Assistant 对象里面会存在两份 Person 基类数据(一份来自 Student 路径,一份来自 Teacher 路径);
  2. 二义性:直接访问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

  1. public 继承:is‑a 是一个

Student is‑a Person;BMW is‑a Car。 继承属于白箱复用:基类内部细节对子类可见;父子类耦合度很高,基类改动会强烈影响子类。

  1. 组合: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; };

📔本篇核心考点总结

  1. 继承实现类层次代码复用;三种继承方式public/protected/private;基类 private 成员子类永远不可见;口诀min(成员权限,继承方式)。开发只用 public 继承。
  2. 派生类对象切片:派生类可以赋值给基类指针 / 引用 / 对象;基类不能直接赋值给派生类。
  3. 父子类同名成员构成隐藏 name‑hiding,不是重载,重载仅限同一个作用域;基类::成员访问被隐藏成员。
  4. 默认成员函数:构造先基类后派生类;析构先派生类后基类;拷贝构造、赋值重载要处理基类部分。final 修饰类禁止继承。
  5. 友元不能继承;static 静态成员继承体系共享同一份。
  6. 多继承会带来菱形继承:两份公共基类,数据冗余 + 访问二义性;virtual虚继承解决;尽量避免菱形继承。
  7. is‑a 用继承,has‑a 优先用组合;组合耦合低,优先组合。

面试简答

Q:基类 private 成员子类能不能继承?能不能访问?

A:内存上继承到子类对象,但是语法不可访问;子类需要访问就设置为 protected。

Q:成员隐藏和重载区别?

A:重载在同一个作用域,函数名相同参数不同;父子不同作用域只要名字相同就是隐藏,和参数无关。

Q:菱形继承问题怎么解决?

A:中间层使用 virtual 虚继承;最好设计上避免菱形继承。

Q:继承和组合怎么选?

A:is‑a 是继承;has‑a 优先组合,组合耦合更低。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询