【C++进阶】继承
目录​编辑本节学习目标1. 继承的概念及定义1.1 什么是继承1.2 继承定义语法基类成员在派生类的访问规则1.3 继承模板类模板父类2. 基类与派生类对象转换切片 / 切割 slicing3. 继承体系下作用域与隐藏name‑hiding同名成员变量隐藏示例同名成员函数隐藏重点选择题4. 派生类的默认 6 个成员函数派生类默认成员函数执行规则4.2 禁止类被继承两种写法5. 继承与友元6. 继承与静态成员7. 多继承、菱形继承与虚继承 virtual7.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 私有成员无论哪种继承方式在派生类内部都不可见。注意不是没有继承到子类对象中只是语法禁止访问对象内存里面依然存在这份数据。 如果希望子类可以访问但是外部不能访问成员设置为protectedprotected 就是为继承场景诞生。简单口诀派生类中访问权限 min(基类成员权限继承方式)权限优先级public protected private表格基类成员public 继承protected 继承private 继承publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可见不可见不可见语法细节使用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 { templateclass T class stack : public std::vectorT { public: void push(const T x) { // 必须写 vectorT::push_back(x); // push_back(x); //直接写会编译报错模板按需实例化找不到名字 vectorT::push_back(x); } void pop() { vectorT::pop_back(); } const T top() { return vectorT::back(); } bool empty() { return vectorT::empty(); } }; } int main() { bit::stackint 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 禁止类被继承两种写法C11final关键字修饰类该类不能被继承推荐class Base final { public: void func5() { cout Base::func5 endl; } protected: int a 1; }; // class Derive : public Base{}; //编译报错Base被final禁止继承C98 旧方式把基类构造函数设置为 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. 多继承、菱形继承与虚继承 virtual7.1 单继承、多继承单继承一个派生类只有一个直接基类。多继承一个派生类拥有两个及以上直接基类。对象内存排布继承列表从左到右先继承的基类放在内存低地址后继承放后面派生类自己成员放最后。7.2 菱形继承钻石继承—— 多继承特殊坑继承关系Assistant助理同时继承Student和TeacherStudent、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‑apublic 继承is‑a 是一个Student is‑a PersonBMW is‑a Car。 继承属于白箱复用基类内部细节对子类可见父子类耦合度很高基类改动会强烈影响子类。组合has‑a 拥有一个Car has‑a Tire汽车拥有轮胎。把别的类作为自己的成员变量。 组合属于黑箱复用只使用对方公开接口内部细节不可见耦合低维护性更好。✨开发经验优先使用组合只有满足 is‑a 语义再用继承实现多态必须使用继承。示例对比 stack 两种实现//方式1继承 vector is‑a不推荐 templateclass T class stack : public std::vectorT {}; //方式2组合 has‑a推荐容器适配器真实实现思路 templateclass T class stack { public: std::vectorT _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继承和组合怎么选Ais‑a 是继承has‑a 优先组合组合耦合更低。