一文彻底读懂C++的继承机制

发布时间:2026/5/19 6:17:20

一文彻底读懂C++的继承机制 一、继承的概念及定义1.1 继承的概念在没有接触继承之前我们要设计两个类Student和TeacherStudent和Teacher都有姓名/地址/电话/年龄等成员变量都有identity身份认证的成员函数设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数比如老师独有成员变量是职称学生的独有成员变量是学号学生生独有的成员函数是学习老师独有的成员函数是教授。123456789101112131415161718192021222324252627282930313233343536373839classStudent{public:voidStudy(){//学习}voididentity(){//身份认证}private:string _name;//姓名string _address;//地址string _tel;//电话int_age;//年龄int_stuid;//学号};classTeacher{public:voidteach(){//教授}voididentity(){//身份认证}private:string _name;//姓名string _address;//地址string _tel;//电话int_age;//年龄int_title;//职工号};利用继承的方法我们可以将两个类中的公共部分提取出来封装成单独一个类Person再使Teacher/Student分别继承Person。这样Teacher/Student中既有自己特有的成员变量函数也有公共的成员变量函数大大避免了代码的冗余。1234567891011121314151617181920212223242526272829303132333435#includestring.hclassPerson{public:voididentity(){//身份认证}string _name;//姓名string _address;//地址string _tel;//电话int_age;//年龄};classStudent:publicPerson//Stuedent继承Person类{public:voidStudy(){//学习}private:int_stuid;//学号};classTeacher:publicPerson//Teacher继承Person类{public:voidteach(){//教授}private:int_title;//职工号};继承机制是面向对象程序设计使代码可以复用的最重要的手段它允许我们在保持原有类特性的基础上进行扩展增加方法成员函数和属性成员变量这样产生新的类叫做派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到到复杂的认知过程。以前我们接触的都是函数层面的复用继承是类设计层次的复用。1.2 继承的定义1.2.1 定义格式下面Person是基类也称作父类Student是派生类也称作子类。1.2.2 继承基类成员访问方式的变化基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。基类private成员在派生类中不能访问如果基类不想在类外直接访问但还需要在派生类中可以访问就定义成protected。可以看出保护成员限定符是因继承才出现的。实际上面的表格我们总结一下就会发现基类的私有成员在派生类中都是不可见的。基类的其他成员在派生类中的访问方式Min(成员在基类的访问限定符继承方式)publicprotectedprivate。使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显式地写出继承方式。在实际运用中一般使用的都是public继承几乎很少使用protected/private继承也不提倡使用protected/private继承因为protected/private继承下来的成员都只能在派生类中使用实际中扩展维护性不强。1.3 继承类模板在 C 中类模板可以被继承这种方式称为 继承类模板。通过继承类模板我们可以创建更具体或更特殊化的类同时复用模板的通用逻辑。继承类模板主要有两种场景普通类继承类模板的实例化版本类模板继承另一个类模板1.普通类继承类模板的实例化版本当一个普通类继承自一个已经实例化的类模板时需要指定模板参数12345678910111213141516171819// 定义一个类模板templatetypenameTclassContainer {protected:T data;public:Container(T val) : data(val) {}T get()const{returndata; }voidset(T val) { data val; }};// 普通类继承实例化的类模板classIntContainer :publicContainerint {public:IntContainer(intval) : Containerint(val) {}// 新增方法voidincrement() { data; }};2.类模板继承另一个类模板类模板可以继承另一个类模板此时可以使用自身的模板参数作为基类模板的参数12345678910111213141516171819202122232425262728293031323334353637#include iostream// 基类模板templatetypenameTclassBase {protected:T value;public:Base(T v) : value(v) {}voidprint()const{std::cout Base value: value std::endl;}};// 派生类模板继承基类模板templatetypenameT,typenameUclassDerived :publicBaseT {private:U extra;public:// 注意初始化列表中需要显式指定基类模板Derived(T v, U e) : BaseT(v), extra(e) {}voidshow()const{// 访问基类成员时可能需要使用this指针或BaseT::限定std::cout Derived value: this-value, Extra: extra std::endl;}};intmain() {Derivedint, std::string obj(42,example);obj.print();// 调用基类方法obj.show();// 调用派生类方法return0;}注意事项访问基类成员在派生类模板中访问基类模板的成员时可能需要使用this-指针或BaseT::限定符帮助编译器识别成员。模板参数传递派生类模板可以将自身的模板参数传递给基类模板也可以使用固定类型1234templatetypenameTclassDerived :publicBasedouble {// 固定使用double类型// ...};二、基类和派生类间的转化在 C 中public 继承的派生类对象与基类之间存在三种常见的赋值 / 关联方式派生类对象赋值给基类指针、派生类对象赋值给基类引用、派生类对象直接赋值给基类对象。这三种方式的本质和效果有显著区别核心在于是否保留派生类的特性以及是否触发多态行为。1.派生类对象赋值给基类指针Base* ptr derivedObj;本质基类指针指向派生类对象的基类部分但指针可以间接访问派生类的完整信息配合虚函数实现多态。特性指针的静态类型是Base*但动态类型是Derived*指向对象的实际类型。可以通过指针调用基类的成员包括虚函数若调用虚函数会根据动态类型触发动态绑定调用派生类的重写版本。不能直接访问派生类新增的成员需显式类型转换如dynamic_cast。1234567891011121314151617classBase {public:virtualvoidfunc() { cout Base::func() endl; }};classDerived :publicBase {public:voidfunc() override { cout Derived::func() endl; }voidderivedFunc() { cout Derived独有的函数 endl; }};intmain() {Derived d;Base* ptr d;// 基类指针指向派生类对象ptr-func();// 调用Derived::func()动态绑定// ptr-derivedFunc(); // 错误不能直接访问派生类新增成员return0;}这里需要注意的是基类对象不能赋值给派生类对象。2. 派生类对象赋值给基类引用Base ref derivedObj;本质基类引用绑定到派生类对象的基类部分引用是对象的别名不产生新对象。特性引用的静态类型是Base动态类型是Derived。行为与基类指针类似可调用基类成员调用虚函数时触发动态绑定。不能直接访问派生类新增成员需显式转换。123456intmain() {Derived d;Base ref d;// 基类引用绑定派生类对象ref.func();// 调用Derived::func()动态绑定return0;}3. 派生类对象直接赋值给基类对象Base baseObj derivedObj;本质发生对象切片Object Slicing—— 仅将派生类对象中的基类部分复制到基类对象中派生类独有的成员被 切片 丢弃。特性生成一个全新的基类对象与原派生类对象无关。该对象的静态类型和动态类型都是Base不触发多态调用函数时仅使用基类的实现。无法访问派生类的任何特性即使显式转换也不行因为派生类部分已丢失。123456intmain() {Derived d;Base baseObj d;// 对象切片仅复制基类部分baseObj.func();// 调用Base::func()无动态绑定return0;}方式操作本质是否产生新对象动态类型是否支持多态动态绑定能否访问派生类新增成员派生类→基类指针指针指向派生类的基类部分否派生类类型是调用虚函数时否需显式转换派生类→基类引用引用绑定派生类的基类部分否派生类类型是调用虚函数时否需显式转换派生类→基类对象直接赋值复制基类部分切片派生部分是生成基类对象基类类型否否已被切片丢弃三、隐藏机制3.1 继承中的隐藏机制派生类和基类中有同名成员派生类成员将屏蔽基类对同名成员的直接访问这种情况叫隐藏。在派生类成员函数中要是需要访问基类中同名成员可以使用基类::基类成员显式访问12345678910111213141516171819202122classPerson{protected:int_num 111;string _name ⼩李⼦;};classStudent :publicPerson{public:voidPrint(){cout _numendl;//这里打印结果为999因为Person中的_num被隐藏了}protected:int_num 999;};intmain(){Student s1;s1.Print();return0;};需要注意的是对于成员函数来讲只要函数名相同就构成了隐藏。也就是说如果派生类与基类中的成员函数名相同通过派生类的实例化对象调用该函数默认调用派生类中定义的重名函数需要调用基类中的重名函数时需要指定类名12345678910111213141516171819202122classPerson{public:voidPrint(){cout 我是基类中的Print函数 endl;}};classStudent :publicPerson{public:voidPrint(){cout 我是派生类中的Print函数 endl;}};intmain(){Student s1;s1.Print();return0;};要是想显式调用基类中的Print函数需要指定Person::Print();如下示例123456intmain(){Student s1;s1.Person::Print();return0;};这也从侧面说明对于继承关系中的隐藏机制基类中的重名的成员函数或者成员变量确确实实被派生类继承下来了只是由于隐藏机制默认访问或调用的是派生类中的成员变量或函数需要访问基类中的时需要指明类域。3.2 重载与隐藏这里我们试着想一想下面基类与派生类func函数之间的关系A重载 B隐藏12345678910111213141516171819202122classA{public:voidfun(){cout func() endl;}};classB :publicA{public:voidfun(inti){cout func(int i) i endl;}};intmain(){B b;b.fun(10);return0;};这里我们发现基类和派生类中都有一重名的func函数根据上述提到的隐藏机制的形成条件基类的func函数与派生类中的func函数构成隐藏。

相关新闻