【C++】从基类到派生类:关于C++继承的务实梳理

发布时间:2026/10/12 3:52:16
【C++】从基类到派生类:关于C++继承的务实梳理
浮世尘弦个人主页⭐个人专栏《C语言》、《数据结构与算法》、《C》非淡薄无以明志非宁静无以致远前言在 C 中继承是面向对象编程的重要机制之一。它允许我们基于已有的类定义新的类从而复用现有代码并在原有基础上扩展新的属性和行为。被继承的类通常称为基类或父类继承得到的新类称为派生类或子类。不过继承并不只是“把成员拿过来”这么简单。继承方式会直接影响父类成员在子类中的访问权限对象的构造和析构也有固定顺序。理解这些规则是正确使用 C 继承的基础。接下来让我们详细学习一下继承的相关内容吧目录一继承的介绍1引入继承2继承的概念二基类访问方式变化1访问变化表2stack继承底层容器三基类和派生类之间的转换四子类的默认成员函数1默认成员函数2实现一个不能被继承的类五继承中地友元和静态成员1继承与友元函数2继承与静态成员六多继承及菱形继承问题1继承模式2虚继承3多继承中的指针偏移问题七继承和组合一继承的介绍1引入继承首先我们看一下下面这个案例定义这两个类时这两个类中有许多重复的部分成员和函数。这一部分的存在时冗余的。有什么方法可以帮助我们去除这些重复的部分就像之前的函数模版类模版一样C提出了他的方法继承测试设计一个Person类把重复的部分都放进去左边的Person为父类右边的student和Teacher为子类子类直接继承父类的成员属性和函数不用在单独进行实现。去除了重复的部分。接下来重新来详细认识一下继承2继承的概念继承是面向对象编程OOP的三大核心特征之一封装继承多态。允许我们在一个已有类父类/基类上增加成员函数和成员变量再创建一个新类子类/派生类。定义格式二基类访问方式变化1访问变化表父类成员访问权限public继承protected继承private继承父类的public成员子类的public成员子类的protected成员子类的private成员父类的protected成员子类的protected成员子类的protected成员子 类的private成员父类的private成员❌ 在子类中不可见❌ 在子类中不可见❌ 在子类中不可见1,基类private成员在派⽣类中无论以什么⽅式继承都是不可见的。这⾥的不可见是指基类的私有成员无论在子类中还是在子类外都是无法进行访问使用但其还是被继承到了派⽣类对象中。即子类中还是有这一个成员的。2子类中继承到的成员变量的访问方式取决于Min(父类的成员访问限定方式子类继承方式public protected private3使用关键字class时默认的继承方式是private使用关键字struct时默认的继承方式是public可以进行省略但是建议写出来。4实际使用时使用最多的时public继承protected和private继承使用到的很少。因此实际重要的只有public继承的2个。补充父类和子类中的成员函数函数名相同调用子类同名函数时父类同名函数就会隐藏。一般调用的是子类B中的fun如果想要在子类对象中调用父类对象当中的同名函数需要用对象.父类名::函数名()这种形式。代码演示:class Person { protected: string name 小明; string ID 2331323; }; class Student : public Person { public: void Print() { cout name endl; //这里需要指定一下才能访问父类同名成员 cout Person::name endl; //直接进行访问 cout ID endl; } protected: string name 超正经小李; }; int main() { Student st1; st1.Print(); return 0; }运行结果最好还是不要在子类起同名的函数。2stack继承底层容器之前直接在stackT,container在container处直接调用容器这种方式称为组合。现在可以使用继承的方式使用容器适配器stack演示一下//之前使用stack底层的模板的方式叫做组合 // 这种叫继承 //#define CON deque //#define CON list #define CON vector namespace att { //stack继承底层容器 templateclass T class stack : public CONT //stack继承底层容器 { public: void push(const T x) { CONT::push_back(x);//需要指定容器实例化 } //不指定实例化就找不到实例化的push_back(); void pop() { CONT::pop_back(); } const T top() { return CONT::back(); } bool empty() { return CONT::empty(); } }; } int main() { att::stackint st; st.push(3); st.push(6); st.push(9); st.push(2); while (!st.empty()) { cout st.top() ; st.pop(); } return 0; }运行结果三基类和派生类之间的转换public继承出来的子类对象可以赋值给父类的对象/指针/引用。这种方式称为切片。反过来父类对象不能赋值给子类原因子类对象中可能存在父类对象中不存在的成员而父类的成员子类则是全部继承所以可以赋值。代码演示四子类的默认成员函数1默认成员函数1子类的构造函数必须调用父类的构造函数。如果父类没有默认构造函数则需要在子类构造函数中的初始化列表中显式调用。构造时先调用父类构造再调用子类。2子类的拷贝构造函数必须调用父类的拷贝构造完成初始化。3子类的赋值重载operator必须调用父类的operator完成父类的复制。由于重名的隐藏问题需要指定作用域。4子类的析构函数会在清理完自己独有的成员后自动调用父类析构函数清理共有的父类中的成员。析构时先调用子类析构再调用父类析构。5子类的析构和父类的析构构成隐藏关系。代码演示父类class Person { public: Person(const string _name 雀巢)//默认构造 :name(_name) { cout Person(const string _name) endl; } //拷贝构造 Person(const Person p1) :name(p1.name) { cout Person(const Person p1) endl; } Person operator(const Person p1) { cout Person operator(const Person p1) endl; if (this ! p1) { name p1.name; } return *this; } ~Person() { cout ~Person() endl; } protected: string name; };子类构造函数 public: //如果使用默认构造函数 //1内置类型不确定 //2自定义类型调用默认构造 //3继承父类的成员看成一个整体调用父类构造 //构造函数 Student(const string name, int _age, const string _address) :Person(name) //使用父类初始化 ,age(_age) ,address(_address) {}拷贝构造//拷贝构造函数--可以不写使用默认的就可以 //但是有资源的开辟深拷贝就要自己写 Student(const Student s1) :Person(s1) ,age(s1.age) ,address(s1.address) { //深拷贝 }赋值重载operator://赋值重载函数--可以不写使用默认的就可以 //但是有资源的开辟深拷贝就要自己写 Student operator(const Student s) { if (this ! s) { Person::operator(s); age s.age; address s.address; } return *this; }析构函数//Student()析构默认生成的就够用了只有有资源要释放才手动写 ~Student() { //子类析构和父类析构构成隐藏关系 //子类调用后会自动调用父类析构 }测试案例和结果结果分析打印的结果为什么调用了两次拷贝构造 四次析构明明只有三个对象原因出在父类上的赋值重载上2实现一个不能被继承的类1C98做法让父类构造函数私有由于子类的构造函数必须调用父类的构造函数父类构造 函数私有化以后子类看不见就不能进行调用无法实例化对象。2C11新增加一个关键字finalfinal放在父类名字的后面这样子类就不能进行继承。代码演示 //实现一个不能被继承的类 //1父类构造函数私有子类的构造函数必须调用父类的构造函数C98) //2C11,final关键字final修改父类派生类就不能继承了 class A //final //第二种方法 { public: void Func1() { cout adls endl; } private: //第一种方法 //A() //{} }; class B : public A { public: void Func2() { cout Func2 endl; } }; int main() { B b1; b1.Func1(); return 0; }运行结果A类中除了中文去掉任意一个注释B类都不能正常初始化 。补充一个有趣的现象按需实例化当主函数不进行实例化时如下图左边展开final依旧会报错但是右边使用第一种方法私有化却不会进行报错这就是一种按需实例化。原因解释不进行实例化 编译器就不会进行仔细地检查只会大致检查是否有明显地错误。五继承中地友元和静态成员1继承与友元函数友元关系不能进行继承基类友元不能访问子类私有和保护成员代码演示class D //需要提前进行声明否则不知道C中的D是什么 //继承与友元 //友元关系不能进行继承基类友元不能访问子类私有和保护成员 class C { public: friend void play(const C s1, const D s2); protected: string name; }; class D : public C { public: //friend void play(const C s1, const D s2); protected: int age; }; void play(const C s1, const D s2) { cout s1.name endl; cout s2.age endl; } int main() { return 0; }这里友元函数play中的age会报错解决方法让子类当中也包括上友元函数2继承与静态成员父类定义了 staic静态成员整个继承体系中只有这一个静态成员 。无论继承出多少子类都只有这一个staic成员实例化。代码演示//静态成员与继承 class Person { public: string name; static int age; }; int Person::age 18; class Student : public Person { protected: string ID; }; int main() { Person p1; Student s2; //name是不同的父子类各用一份 cout p1.name endl; cout s2.name endl; //age是同一个父子类共用一个 cout p1.age endl; cout s2.age endl; //public前提下可以直接指定类域进行访问 cout Person::age endl; cout Student::age endl; Person::age; cout p1.age endl; cout s2.age endl; return 0; }运行展示六多继承及菱形继承问题1继承模式单继承一个子类只有一个直接父类这样的继承关系称之为单继承。多继承一个子类有两个或两个以上的直接父类这种继承关系称为多继承。先继承的父类在前面后继承的父类在后面。菱形继承菱形继承是多继承的一种特殊情况。菱形继承有数据冗余和二义性的问题。实践一般不使用菱形继承。结构如下所示解释Student和Person继承了PersonAssistant有继承了Student和Tercher那么从Person中继承到Assistant中的成员就有了两份造成了数据冗余和二义性。2虚继承虚继承是为了解决菱形继承专门设计的一种机制。在继承前面加上virtual代码演示//菱形继承与虚函数 class Person { public: Person(const string name) :_name(name) {} string _name; }; class Student : virtual public Person { public: Student(const string name,int num) :Person(name) ,_num(num) {} private: int _num;//学号 }; class Teacher : virtual public Person { public: Teacher(const string name,const string Id) :Person(name) ,ID(Id) {} private: string ID;//工号 }; class Assistant :public Student, public Teacher { public: //对象的构造顺序永远是虚基类Person - 非虚基类(Student和Tercher) - 自身成员变量 Assistant(int num,const string id,const string name) :Person(name)//编译器只认这里的name ,Student(name,num)//虚继承后这里的name会被编译器忽略 ,Teacher(name,id)//同样被忽略 {} private: int age; };运行测试总结与补充1只要直接的父类是同一个就加上virtual2,假设有五层继承A-B-C-D-E如果全是普通继承E的初始化列表中只能有D的初始化和自己成员的初始化。如果发生了虚继承需要初始化虚基类B上层直接父类自身成员。虚基类并不是一种特殊的类而是在虚继承关系中被virtual关键字修饰的那个共享基类。E中的初始化列表中会包含B,C,D以及自身成员的初始化。3多继承中的指针偏移问题结果C分析直接父类在子类中的初始化的顺序和继承的顺序相同。先继承Base1再继承Base2因此Base1在前Base2在后。七继承和组合1public继承是⼀种is-a的关系。每个子类对象都是⼀个父类对象。2组合是⼀种has-a的关系。假设B组合了A每个B对象中都有⼀个A对象。继承允许你根据基类的实现来定义派生类的实现。这种通过生成派⽣类的复用通常被称为白箱复用。继承⼀定程度破坏了基类的封装基类的改变对派生类有很大的影响。派生类和基类间的依 赖关系很强耦合度高。白箱在继承方式中基类的内部细节对派生类可见。对象组合实现新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用被称为黑箱复用。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系耦合度低。优先使用对象组合有助于保持每个类被封装。黑箱对象的内部细节是不可见的。总结1黑箱测试相对简单不用了解底层实现从功能的角度测试白箱测试相对较难要了解底层实现从代码运行逻辑测试。2优先使用组合而不是继承。实际尽量多去用组合组合的耦合度低代码维护性好。不过也不太 那么绝对类之间的关系就适合继承那就⽤继承另外要实现多态也必须要继承。类之间的关系既适合用继承也适合组合就用组合。结尾本篇文章中所涉及到的代码gitee链接继承1 菱形继承与虚继承以上就是本文的全部内容了如果对你有所帮助的话请大佬不妨给博主来个“一键三连”这对我是大大的支持也能让博主产出更优质的内容。往期回顾把值写进类型C 模板进阶的非类型参数、特化与分离编译总结本文系统讲解C继承机制涵盖继承概念、访问控制变化、基类与派生类转换、默认成员函数处理、友元与静态成员特性重点剖析多继承及菱形继承问题并引入虚继承解决方案。强调继承与组合的对比。⚡把上面的内容吃透就休息一下吧⚡