【C++】继承的那些事儿~

发布时间:2026/7/21 8:04:45
【C++】继承的那些事儿~ 目录1.继承的概念及定义1.1 继承的概念1.2 继承的定义1.3继承类模板2.基类和派生类的转换3.继承中的作用域3.1 隐藏4.派生类的默认成员函数4.1 四个常见的默认成员函数4.2 如何实现一个不能被继承的类5.多继承和菱形继承问题5.1 继承模型5.2 虚继承5.3 虚继承的原理5.4 多继承的指针偏移问题1.继承的概念及定义1.1 继承的概念继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段它允许我们在保持原有类特性的基础上进行扩展增加方法和属性这样产生新的类称派生类。什么时候用继承呢我们看这样一个场景假设我们设计了两个类Teacher和Student他们都有的属性如姓名、居住地址、电话、年龄等也都有Identity身份认证的成员函数这样的话设计在两个类里面就显得冗余了。当然他们也会有自己的特性老师授课和学生学习等。像这样两个类具有相同的一些属性和方法时我们就可以考虑使用继承手段。我们将上述这个场景中相同的属性和方法放在Person这个类当中。具体代码实现如下class Person { public: // ⾝份认证 void identity() { cout void identity() _name endl; } protected: string _name; //姓名 string _address; //地址 string _tel; //电话 int _age 18; //年龄 }; class Student : public Person //共有继承 { public: //学习 void Study() { //... } protected: int _stuid; //学号 }; class Teacher : public Person { public: // 授课 void teaching() { //... } protected: string title; // 职称 };1.2 继承的定义上面的继承中Person是基类也称作父类Student是派生类也称作子类。继承方式一共有三种public、protected、private不同的继承方式访问方式也不同。类成员/继承方式public继承protected继承private继承基类的public成员派生类public成员派生类的protected成员派生类private成员基类的protected成员派生类protected成员派生类的protected成员派生类private成员基类的private成员派生类中不可见派生类中不可见派生类中不可见通过上表我们可以发现基类成员在派生类里所处的地位是Min基类里的地位派生类里的地位总是取小的那一种。private protected public基类private成员无论以什么样的继承方式在派生类当中都是不可见的。这里的不可见指的是派生类对象不管在类外还是类里面都不能去访问但还是继承到了派生类对象。但如果基类成员不想在类外直接被访问但是想要在派生类中被范围就可以定义成protected。继承方式不写的话class默认继承方式是privatestruct默认是public这里的class和struct指的是派生类。实际中一般都是public继承。另外说明有元关系不能继承也就是说基类的有元函数不能访问派生类的私有和保护成员如果基类定义了static静态成员那么整个继承体系里面也只会有这样一个成员无论有多少个派生类都只有一个static成员实例。1.3继承类模板派生类访问基类时建议指定类域否则会编译报错。必须指定类域的三种场景a.访问基类中的类型成员typedef、usingb.访问基类中的静态成员或枚举c.访问基类中的成员函数依赖模板参数时templatetypename T class Base { public: void func() { cout Base endl; } }; templatetypename T class Derived : public BaseT { public: void test() { // func(); // 某些编译器可能报错依赖查找失败 BaseT::func(); // 明确告诉编译器去基类中找 this-func(); // 另一种方式通过 this 指针 } };2.基类和派生类的转换通常情况下我们把一个类型的对象赋值给另一个类型的引用或者指针时存在类型转换中间有临时变量产生需要加const但public继承中就有一个例外派生类对象可以赋值给基类的指针/引用而不需要加const这里绑定的时派生类对象中的基类部分也就是说基类的指针或引用既可以指向基类对象也可以指向派生类对象。基类对象不能赋值给派生类对象。派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成这个过程就像派生类自己定义部分成员切掉了⼀样所以也被叫做切割或者切片。基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型可以使用RTTI(Run-Time TypeInformation)的dynamic_cast 来进行识别后进行安全转换。3.继承中的作用域3.1 隐藏在继承体系中基类和派生类都有独立的作用域。如果派生类和基类中有同名成员派生类成员将屏蔽对基类同名成员的直接访问这种情况叫隐藏。在派生类成员函数中可以使同基类::基类成员 显示访问需要注意的是如果是成员函数的隐藏只需要函数名相同就构成隐藏。因为多态中⼀些场景析构函数需要构成重写重写的条件之⼀是函数名相同。编译器会对析构函数名进行特殊处理统一处理成destructor()所以基类析构函数不加virtual的情况下派生类析构函数和基类析构函数构成隐藏关系。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; // 学号 }; int main() { Student s1; s1.Print(); return 0; };4.派生类的默认成员函数和类的默认成员函数差不多这里还是着重讲前四个常用的。4.1 四个常见的默认成员函数派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。派生类对象初始化先调用基类构造再调派生类构造。派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的operator必须要调用基类的operator完成基类的复制。需要注意的是派生类的operator隐藏了基类的operator所以显示调用基类的operator需要指定基类作用域。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。class Person { public: Person(const char* name) : _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 18, const char* address 成都) :Person(name) , _num(num) , _address(address) { cout Student() endl; } Student(const Student s) :Person(s) ,_num(s._num) ,_address(s._address) { // 涉及深拷贝需要自己实现 } Student operator(const Student s) { if(this ! s) { Person::operator(s); _num s._num; _address s._address; } return *this; // 涉及深拷贝需要自己实现 } ~Student() { // 不用显示调用基类析构编译器会在派生类析构结束后自动调用析构 // Person::~Person(); //... cout ~Student() endl; } protected: int _num; // 学号 string _address; // 地址 }; // 派生类一般要自己实现构造不需要显示写析构、拷贝构造赋值重载除非派生类有深拷贝的资源需要处理 int main() { Student s1; return 0; }Student(const Student s):Person(s) 为什么这里直接传s就可以了呢因为 s 是 Student 对象当它被传递给 Person 的构造函数时编译器会自动将 Student 对象“切片”为 Person 对象只提取 Person 基类部分即 _name来构造。C允许派生类引用/指针隐式转换为基类引用/指针向上转型Upcasting。4.2 如何实现一个不能被继承的类方法一基类的构造函数私有派生类的构成必须调用基类的构造函数但是基类的构成函数私有化以后派生类看不见就不能调用了那么派生类就无法实例化出对象。方法二C11新增了一个关键字finalfinal修饰基类派生类就不能继承了。// C11的⽅法 class Base final { public: void func5() { cout Base::func5 endl; } protected: int a 1; private: // C98的⽅法 /*Base() {}*/ };5.多继承和菱形继承问题5.1 继承模型单继承⼀个派生类只有⼀个直接基类时称这个继承关系为单继承。也就是说哪怕是连续多个只要一个继承的派生类也依然是单继承。多继承一个派生类有两个或以上直接基类时称这个继承关系为多继承多继承对象在内存中的模型是先继承的基类在前面后继承的基类在后面派生类成员在放到最后。菱形继承菱形继承是多继承的⼀种特殊情况。从下面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题在Assistant的对象中Person成员会有两份。5.2 虚继承对于菱形继承出现的问题就需要虚继承来解决菱形虚拟继承的底层实现会比较复杂性能也会有所损失。class Person { public: string _name; // 姓名 /*int _tel; int _age; string _gender; string _address;*/ // ... }; // 使⽤虚继承Person类 class Student : virtual public Person { protected: int _num; //学号 }; // 使⽤虚继承Person类 class Teacher : virtual public Person { protected: int _id; // 职⼯编号 }; // 教授助理 class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 }; int main() { // 使⽤虚继承可以解决数据冗余和⼆义性 Assistant a; a._name peter; return 0; }5.3 虚继承的原理通过监视窗口看到Student和Teacher中也把Person“继承”下来了不利于分析接下来我们从内存窗口来看。为方便研究我们讲上述问题简化其本质还是一样的。class A { public: int _a 0; }; class B : virtual public A { public: int _b 1; }; class C : virtual public A { public: int _c 2; }; class D : public B , public C { public: int _d 3; }; int main() { D d; d.B::_a 1; d.C::_a 2; d._a 3; d._b 4; d._c 5; d._d 6; B* p2 b; B* p1 d; return 0; }接下来我们就深入分析上面这个菱形继承。调内存窗口我们可以看见p1 和 p2 里存储的偏移量就是来访问共同拥有的_a这个变量。也就是说被虚继承的类在vs里面会放在末尾各对象通过基类的指针指向的地址中存储的偏移量来访问这就避免了数据冗余和二义性了。5.4 多继承的指针偏移问题下面说法正确的是A p1 p2 p3 B.p1 p2 p3 C.p1 p3 ! p2 D.p1 ! p2 ! p3class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 d; Base2* p2 d; Derive* p3 d; return 0; }答案C,通过上面的菱形继承原理很容易就分析出答案了。完~