【C++】继承
目录一、初识继承语法格式继承方式与访问权限二、基类和派生类之间的赋值转换基本特性基本特性的代码演示三、重定义隐藏示例①成员变量同名示例②成员函数同名四、派生类的默认成员函数基本特性关于父类中默认成员函数是否需要显式调用五、继承与友元六、继承与静态成员1.基类静态成员变量派生类共享同一实例2.基类静态成员函数派生类可继承且共享实现3.派生类对基类静态成员的 “隐藏”而非覆盖总结七、多继承1.单继承2.多继承3.菱形继承4.菱形继承的解决方法虚继承八、对象组合对象组合的形式①严格组合②聚合③关联对象组合的优势总结一、初识继承继承机制是面向对象程序设计中使代码可以复用的重要手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称派生类。继承是类设计层次的复用。类名解释原有类称基类、父类、超类在原有类基础上扩展而来的类称派生类、子类语法格式// 基类父类 class Base { public: int publicMem; // 公有成员 protected: int protectedMem;// 保护成员 private: int privateMem; // 私有成员 }; // 派生类子类继承自Base class Derived : public Base { // 派生类可以访问基类的public和 protected成员 // 基类的private成员不可直接访问 };结构解释继承方式与访问权限基类成员权限public 继承protected 继承private 继承publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可访问不可访问不可访问示例//基类 class Base { public: void pubFunc() {} protected: void proFunc() {} private: void priFunc() {} }; // public继承 class DerivedPublic : public Base { void test() { pubFunc(); // 可访问public - public proFunc(); // 可访问protected - protected // priFunc(); // 错误基类private成员不可访问 } }; // protected继承 class DerivedProtected : protected Base { void test() { pubFunc(); // 可访问public - protected proFunc(); // 可访问protected - protected } }; // private继承 class DerivedPrivate : private Base { void test() { pubFunc(); // 可访问public - private proFunc(); // 可访问protected - private } };注意①基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。②基类private成员在派生类中是不能被访问如果基类成员不想在类外直接被访问但需要在派生类中能访问就定义为protected。可以看出保护成员限定符是因继承才出现的。③ 使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显式写出继承方式。④在实际运用中一般都是使用public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强。二、基类和派生类之间的赋值转换基本特性①派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。②基类对象不能赋值给派生类对象。③基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用但必须是基类的指针是指向派生类对象时才安全对基本特性①中切片的理解注子类虽然是继承了父类中的成员但父类和子类中的 _name、_sex、_age 是不同的只能说子类包含了两部分的成员继承的父类成员和父类原本的成员是独立的。基本特性的代码演示父类和子类1、派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用2、基类对象不能赋值给派生类对象3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用但必须是基类的指针是指向派生类对象时才安全情况①基类指针指向派生类对象安全pp虽然是基类指针但它实际指向的是一个完整的Student对象包含基类部分age和派生类部分_No。强制转换后ps1正确识别了对象的完整类型访问_No时不会越界。情况②基类指针指向基类对象危险p_obj是基类对象内存中只包含age成员根本没有_No成员的存储空间。强制转换后ps2错误地将基类对象当作派生类对象处理访问_No时会操作p_obj内存之外的非法区域导致未定义行为可能崩溃、数据错乱等。三、重定义隐藏在继承体系中基类和派生类都有独立的作用域。子类和父类中如果存在同名成员子类成员将屏蔽对父类同名成员的直接访问这种情况叫隐藏也叫重定义。在子类成员函数中可以使用 父类::父类成员 显式访问父类成员注①成员函数只需要函数名相同就构成隐藏。②在实际中继承体系里面最好不要定义同名的成员。示例①成员变量同名头文件测试文件可以看到没有指定打印哪个 _num 时优先打印的是子类中的 _num这里打印_num时优先到局部域中找找不到再依次到子类域、父类域、全局域找一处找到后就不再向后查询。示例②成员函数同名头文件测试文件可以看到此处调用 fun( ) 函数如果不传参数就报错因为父类中的fun( )被隐藏了调用到的是子类中的fun( )函数传参后报错消失隐藏通俗点理解就是当父类和子类出现同名成员时默认先访问子类的同名成员就好像父类中的同名成员被隐藏起来了一样。四、派生类的默认成员函数基本特性派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的operator必须要调用基类的operator完成基类的复制。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。总结①派生类继承自基类的那部分其构造、拷贝构造、赋值重载以及析构都需要通过派生类调用基类的来完成。以析构函数为例头文件测试文件这里先析构子类然后自动调用父类析构目的是去析构子类中继承自父类的那部分内容而不是析构父类自身。②先构造基类再构造派生类③先析构派生类再析构基类关于父类中默认成员函数是否需要显式调用父类函数类型是否需要显式调用显式调用位置普通构造函数仅当父类无默认构造函数时需要子类构造函数的初始化列表拷贝构造函数必须显式调用否则父类部分用默认构造初始化子类拷贝构造的初始化列表赋值重载函数必须显式调用子类赋值运算符的函数体内部1. 父类的普通构造函数①必须显式调用的情况如果父类没有默认构造函数即没有无参构造函数或默认构造函数被删除 / 不可访问子类必须在初始化列表中显式调用父类的某个有参构造函数。②可隐式调用的情况如果父类有默认构造函数无参构造函数子类初始化列表中若不显式调用父类构造函数编译器会自动隐式调用父类的默认构造函数。2. 父类的拷贝构造函数必须显式调用子类的拷贝构造函数不会自动调用父类的拷贝构造函数而是默认调用父类的默认构造函数。因此若需要用父类对象的成员初始化子类中继承的父类部分必须在子类拷贝构造函数的初始化列表中显式调用父类的拷贝构造函数。若不显式调用Parent(other)则父类部分会被Parent()默认构造函数初始化导致父类成员未正确拷贝。3. 父类的赋值运算符重载必须显式调用子类的赋值运算符重载不会自动调用父类的赋值运算符需要在子类赋值运算符的函数体内部显式调用父类的赋值运算符不能在初始化列表中调用因为赋值运算发生在对象构造之后。若不显式调用Parent::operator(other)父类部分的成员不会被赋值导致赋值不完整。五、继承与友元友元关系不能继承即父类的友元不能访问子类的私有和保护成员即使是子类继承的成员也不行因为父类的友元不是子类的友元。示例头文件测试文件六、继承与静态成员1.基类静态成员变量派生类共享同一实例基类的静态成员变量在内存中仅存在一份实例所有派生类及基类自身的对象共享这一份数据不会因继承而产生新的副本头文件测试文件内存唯一基类静态成员变量仅在全局区分配一次内存派生类不会为其额外分配内存。共享修改无论是基类对象还是派生类d1 对象修改的都是同一份static_val修改后所有访问者都会看到最新值。访问方式静态成员变量可通过 “类名静态成员”推荐无需创建对象或 “对象.静态成员” 访问派生类也可通过自身类名访问如Derived::static_val。2.基类静态成员函数派生类可继承且共享实现基类的静态成员函数同样属于类本身派生类会继承该函数的实现且所有派生类共享同一函数实例不会因继承产生新的函数副本。静态成员函数的核心限制只能访问静态成员变量 / 静态成员函数无法访问非静态成员因为非静态成员依赖具体对象而静态函数无this指针。头文件测试文件继承实现派生类无需重写即可直接调用基类的静态成员函数如Derived::setStaticVal本质是调用Base::setStaticVal。无多态性静态成员函数不依赖this指针也无法被声明为virtual虚函数因此不支持多态。即使派生类重写了静态函数也只是 “隐藏” 基类的静态函数而非覆盖。示例若Derived重写setStaticVal调用时会根据 “调用者的类名” 而非 “对象实际类型” 选择函数。3.派生类对基类静态成员的 “隐藏”而非覆盖若派生类定义了与基类同名的静态成员变量或函数会隐藏基类的静态成员即派生类范围内优先访问自身的静态成员但不会覆盖基类的静态成员基类的静态成员仍存在且可访问。隐藏规则派生类同名静态成员仅在自身作用域内隐藏基类成员基类成员仍独立存在如Base::static_val与Derived::static_val是两个不同的静态变量各有一份内存。无多态即使基类指针指向派生类对象静态成员函数的调用仍由 “指针的类类型”Base决定而非 “对象实际类型”Derived这与虚函数的多态性完全不同。总结特性基类静态成员变量基类静态成员函数继承后是否复制不复制所有派生类共享同一实例不复制所有派生类共享同一实现访问方式类名静态变量 / 对象。静态变量类名静态函数 / 对象。静态函数能否访问非静态成员不能静态成员独立于对象不能无 this 指针多态性无静态变量无多态概念无不能声明为 virtual无覆盖派生类同名处理隐藏基类变量独立内存隐藏基类函数无覆盖七、多继承1.单继承前面举的例子基本都是单继承这里就不展开了。2.多继承一般不建议设计出多继承。头文件测试文件Derived 同时拥有 Base1 和 Base2 的成员可直接调用两个基类的函数。继承方式的作用与单继承相同如 public 继承保持基类成员的访问权限3.菱形继承首先声明一定不要设计出菱形继承否则在复杂度及性能上都可能有问题。菱形继承结构会导致派生类中出现数据冗余和访问歧义头文件测试文件数据冗余FinalDerived 继承了 Derived1 和 Derived2而两者都包含 Base 的成员 x因此 FinalDerived 中存在两份 x分别来自 Derived1 和 Derived2。访问歧义直接访问 fd.x 时编译器无法确定要访问哪一份 x导致编译错误。4.菱形继承的解决方法虚继承虚继承确保最终派生类中只保留共同基类的一份实例。仅在菱形继承场景下使用虚继承其它情况不要使用。示例头文件测试文件单一实例虚继承确保共同基类Base在最终派生类FinalDerived中只存在一份实例消除数据冗余。初始化责任转移虚基类Base的初始化不再由直接派生类Derived1、Derived2负责而是由最终派生类FinalDerived在初始化列表中直接初始化。避免歧义由于只有一份共同基类成员直接访问如fd.x不会产生歧义。注意若多个基类有同名成员非来自共同基类派生类访问时需用基类名限定以消除歧义若多个基类有同名虚函数派生类重写时需确保函数签名一致否则可能导致多态行为异常底层原理虚继承基本内存布局// 虚继承菱形结构 class Base { public: int x; }; class Derived1 : virtual public Base { public: int y; }; // 虚继承 class Derived2 : virtual public Base { public: int z; }; // 虚继承 class FinalDerived : public Derived1, public Derived2 { public: int w; };Base实例被移到内存末尾成为所有虚继承类的共享实例即只有一份Base实例Derived1和Derived2中各增加一个虚基类指针vbptr, Virtual Base Pointer用于指向Base实例的位置通常存储偏移量虚基类指针vbptr虚继承的类如Derived1、Derived2会在内存中增加一个隐藏的指针成员vbptr该指针指向虚基类表vbtable, Virtual Base Table。虚基类表vbtable虚基类表是一个存储偏移量的数组记录当前类的成员与虚基类Base成员之间的内存距离。例如Derived1的 vbtable 中存储Derived1成员到Base::x的偏移量。Derived2的 vbtable 中存储Derived2成员到Base::x的偏移量。访问虚基类成员的过程当通过Derived1或Derived2的对象访问Base::x时先通过vbptr找到 vbtable。从 vbtable 中获取偏移量计算出Base::x在内存中的实际地址。最终访问到共享的Base::x实例。最终派生类的初始化责任虚基类Base的构造函数由最终派生类FinalDerived负责调用而非中间派生类Derived1、Derived2。这确保Base只被初始化一次避免多份实例。注Base 仅被 FinalDerived 初始化一次避免了普通继承中的重复初始化。性能代价增加内存开销每个虚继承的类需存储vbptr额外占用指针大小的内存通常 8 字节。访问效率降低访问虚基类成员需通过vbptr和 vbtable 间接计算地址比直接访问多一次内存读取。仅在菱形继承场景下使用虚继承如需要多个类共享同一个基类实例时。非菱形结构的多继承无需使用虚继承否则会徒增性能开销八、对象组合通过在一个类中声明另一个类的对象作为成员实现功能组合示例#include iostream // 引擎类 class Engine { public: void run() { std::cout 引擎运行中 std::endl; } }; // 汽车类组合引擎 class Car { private: Engine engine; // 组合汽车有一个引擎 public: // 汽车启动调用引擎功能 void start() { std::cout 汽车启动 std::endl; engine.run(); // 使用组合的引擎对象 } }; int main() { Car myCar; myCar.start(); // 输出 // 汽车启动 // 引擎运行中 return 0; }Car类通过成员变量engine组合了Engine类的功能体现了 has-a 关系汽车有一个引擎Car可以直接使用Engine的run()方法但无需继承Engine成员对象engine的生命周期与Car对象一致对象组合的形式①严格组合成员对象完全依赖于所属对象存在如 “汽车和发动机”成员对象不能被多个所属对象共享。②聚合成员对象可独立存在只是被所属对象 “引用”如 “公司和员工”。通常通过指针或引用实现成员对象的生命周期不受所属对象控制class Employee { /* ... */ }; class Company { private: Employee* employees[100]; // 聚合员工可独立于公司存在 public: // 仅存储指针不负责员工对象的创建/销毁 void addEmployee(Employee* e) { /* ... */ } };③关联两个类通过指针 / 引用建立临时联系如 “客户和订单”关联关系可动态建立或解除。对象组合的优势低耦合组合类与成员对象之间仅通过接口交互修改成员对象的内部实现不会影响组合类符合 “开闭原则”。高灵活性可通过替换不同的成员对象如不同型号的发动机在不修改Car类的情况下改变其功能。避免菱形继承问题当需要复用多个类的功能时组合不会像多继承那样导致歧义或数据冗余。总结在实际开发中应遵循 “优先使用组合而非继承”Favor Composition Over Inheritance的设计原则以构建更健壮、易维护的代码。感谢阅读本文如有错漏之处烦请各位斧正。