%% # 纲要 > 主干纲要、Hint/线索/路标 1. "继承" 是如何实现的? 2. "派生类与基类"之前的关系, - 派生类继承了基类的哪些部分? - 派生类需要完成什么任务?——初始化基类的部分? - 相互转换? # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** --- # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 继承 C++中的**继承**(inheritance)是面向对象编程(OOP)的一个核心概念,允许**创建基于现有类的新类**。 "继承"机制提供了代码重用的手段,并能建立起类之间的层次关系。 继承的层级表现为: - **基类**(base class):提供通用的属性和方法。 - **派生类**(derived class):可添加新的属性和方法,也可以重写继承来的方法。 派生类通过 "**==类派生列表==**" (class derivation list)来明确指出其从哪个**基类**继承而来,可以继承自**多个基类**。 每个派生类都会继承其 "==**直接基类**==(派生列表中指定的)" 的所有成员,因此也**包括了其==所有 "间接基类"== 的部分**。 一个派生类同时也可以作为其他派生类的 "基类"。 <br><br> # 派生访问说明符 在 "类派生列表" 中需要指明 "**继承方式**",C++支持**三种类型的继承**,由 "派生访问说明符"指定。 "**派生访问说明符**" 控制的是 **基类的 "==公有成员==" 与 "==受保护成员=="** 在**派生类中**所呈现的**对外的 "访问权限"**: - `public`:公有继承 - **基类的 `public` 与 `protected` 成员** 在派生类中**仍然分别为 `public`、`protected`**,保持不变; - `protected`:受保护的继承 - **基类的 `public` 与 `protected` 成员** 在派生类中==**全变为 "`protected`"**==。 - `private`:私有继承 - **基类的 `public` 与 `protected` 成员**在派生类中==**全变为 "`private`"**== 总结: | 继承方式 | 基类的 `public` 成员在派生类中 | 基类的 `protected` 成员在派生类中 | | -------------- | -------------------- | ----------------------- | | `public` 继承 | `public` | `protected` | | `protected` 继承 | `protected` | `protected` | | `private` 继承 | `private` | `private` | > [!example] 说明示例: > > ```cpp title:derived_ways.cpp > class Base { > public: > int publicMember; > protected: > int protectedMember; > private: > int privateMember; > }; > > class PublicDerived : public Base { > // publicMember 仍是公有的 > // protectedMember 仍是保护的 > // privateMember 在 Derived 中不可访问 > }; > > class ProtectedDerived : protected Base { > // publicMember 和 protectedMember 在 Derived 中都变成受保护的 > // privateMember 在 Derived 中不可访问 > }; > > class PrivatedDerived: private Base { > // publicMember 和 protectedMember 在 Derived 中都变成私有的 > // privateMember 在 Derived 中不可访问 > }; > ``` > > [!caution] 无论哪种派生方式,**==基类的 private 成员==均无法在派生类内 "直接访问"** > > - 派生类**从基类继承来的 `public` 和 `protected` 成员**能够在**派生类的成员函数(及友元)** 中直接访问。 > - 派生类**从基类继承来的 `private` 成员**在派生类中**不可直接访问**,派生类只能通过其继承的 "**基类的成员函数**" 来修改 **基类的 `private` 成员**。 > > [!caution] > 派生类只对**其自身 "==从基类继承来的、基类部分 public 和 protected 成员== "具有直接访问权限**, > 但是**不能直接通过 "基类" 或 "基类对象" 去访问基类的 protected 成员**。 > > ```cpp > class Base { > protected: > int prot_mem; > }; > > class Derived : public Base { > public: > friend void visit (Derived&); // 可以访问 Derived::prot_mem, 这是派生类继承来的、其自身中的基类部分的成员. > friend void visit (Base&); // 不能访问 Base::prot_mem, 这不属于派生类本身, 而是"基类". > }; > ``` > > > [!NOTE] `class` 的默认继承方式为 `private`,而 `struct` 默认继承方式为 `public` > > ```cpp > class Base {}; > class D1: Base {}; // 默认为`private`继承 > struct D2: Base {}; // 默认为`public`继承 > ``` > > <br><br> # 派生类的作用域 **派生类的作用域** 嵌套在 **==基类的作用域==** 之内。 因此,如果一个名字在**派生类的作用域**内无法正确解析,则编译器将继续**在外层的基类作用域**中寻找该名字的定义。 **派生类可以 ==重用== 定义在其直接基类或间接基类,以及更外层作用域中的名字**, 此时**定义在==派生类内层作用域中的名字==将隐藏定义在==外层作用域的名字==**。可使用**作用域运算符`::`** 明确地**使用外层作用域中的名称**。 <br><br> # 基类的静态成员 基类中的静态成员始终**只存在一个实体**,无论从基类中派生出多少个派生类。 如果基类的静态成员是**非 `private` 的**,则也可以**通过派生类来访问**。 ```cpp struct Base { static void stkmem(); } struct Derived : public Base { //... } Base::stkmem(); Derived::stkmem(); // 派生类调用基类的静态成员函数 ``` <br><br> # 派生类与基类之间的"初始化构造"以及"拷贝控制"关系 > 参见 [^3] 当创建**派生类对象**时,首先会调用其 **==直接基类==及==虚基类==的构造函数**来初始化**派生类对象中的基类以及部分**。 同样,对于派生类的**拷贝控制操作**,**除析构函数以外**,都需要**对其基类部分显式地进行处理**: - 在派生类的**拷贝/移动构造函数**中,应在 **==成员初始化列表==** 中显式调用**基类的拷贝/移动构造函数**来初始化对象的基类部分; - 在派生类的**拷贝/移动赋值操作**中,应通过 **==类作用域运算符==** 显式调用**基类的拷贝/移动赋值运算符**来正确地赋值基类部分。 - 在派生类的**析构函数**中,**只需要销毁派生类自己分配的资源**,**而==基类部分的析构函数==将被==自动隐式调用**==。 对于一个派生类,其编译器自动提供的 "**合成版本(Synthesized)**"的**拷贝控制操作**,都会**自动调用直接基类中的对应操作**。 > [!caution] 继承体系下,编译器为派生类提供 "合成版本" 拷贝控制操作的规则 [^2] > > > - 若**基类的==默认构造函数==、==拷贝控制操作== 是`private` 或 `=delete`**,则**派生类中的对应成员将是 `= deleted`**。 > - 因为**编译器无法使用==基类成员==来对==派生类对象中的基类部分==进行处理**。 > > - 若**基类的析构函数是`private` 或 `=delete`**,则派生类中 **"合成"的默认构造函数与拷贝、移动构造函数将是 `= deleted`**; > - 因为**无法对基类部分进行销毁,也就不能创建基类部分**。 > > - 若**基类的析构函数是虚函数**(虚函数即意味着必须定义,例如声明 `=default`),则**编译器不会为其生成合成的移动操作**, > - 故**编译器也不能为派生类合成移动操作**。 派生类继承示例: ```cpp title:inheritance.cpp class Base { public: Base() = default; Base(int value, char ch) : baseValue(value), baseChar(ch) { // 基类的构造函数 } virtual ~Base() = default; protected: int baseValue; char baseChar; }; class Derived: public Base { public: // 派生类的默认构造函数, 调用基类的默认构造函数 Derived() : Base() {}; // 派生类的构造函数, 在成员初始化列表中首先调用"基类的构造函数`Base(int, char)`" Derived(int b_v, char b_ch, int derivedV) : Base(b_v, b_ch), derivedValue(derivedV) {} // 派生类的拷贝构造函数, 在调基类的拷贝构造体 Derived(const Derived& other) : Base(other) { derivedValue = other.derivedValue; } // 派生类的拷贝赋值运算符, 调用基类的拷贝赋值运算符 Derived& operator=(const Derived& other){ if (this != &other) { Base::operator=(other); // 调用基类的拷贝赋值运算符 // ...派生类特有的拷贝逻辑 derivedValue = other.derivedValue; } return *this; } // 派生类的移动构造函数, 调用基类的移动构造函数 Derived(Derived&& other) noexcept : Base(std::move(other)) { derivedValue = other.derivedValue; } // 派生类的移动赋值运算符, 调用基类的移动赋值运算符 Derived& operator=(Derived&& other) noexcept { if (this != &other) { Base::operator=(std::move(other)); // 调用基类的移动赋值运算符 // 派生类特有的移动赋值逻辑 // ... derivedValue = other.derivedValue; } return *this; } private: int derivedValue; }; ``` <br><br> ### 派生类中对基类部分的初始化 **派生类**的**成员初始化列表**中不能直接初始化基类的数据成员,**必须在其 "==成员初始化列表==" 中调用 ==基类的构造函数==来初始化其 "==基类部分=="**。 成员初始化列表中**对基类构造函数的调用必须是==第一个操作==**,确保**基类成员**最先被初始化。 > [!caution] 如果派生类的构造函数**未显式调用基类的构造函数**,则将**自动调用基类的==默认构造函数==**。 > 在该情况下,如果 **==基类没有无参的默认构造函数==(不存在、或属于 `private` 或被声明为 `=delete`)**,则会编译错误。 > [!note] > **派生类应当==遵循基类的接口==**,通过**调用基类的构造函数来初始化从基类中继承而来的成员**。 > 即使从语法上可以在**派生类的构造函数体内**为其公有或受保护的基类成员赋值,也 **==不应当这么做==**。 <br> #### 派生类中基类部分的构造与析构顺序 对一个派生类,其基类的构造顺序**与 "派生列表" 中基类的出现顺序保持一致**,而**与派生类构造函数的成员初始化列表中的调用顺序无关**。 具体来说,将根据派生类的 "派生列表" 中**各个==直接基类==** 的声明顺序: - 首先**构造所有==虚基类==**:编译器将**依次检查每个==直接基类==是否含有虚基类**,最先完成所有虚基类的构造。 - 然后**根据声明顺序逐一构造==非虚的直接基类==**,每个直接基类再分别完成各自基类的构造。 **析构顺序**则与构造顺序**恰好相反**,最先被构造的基类会最后被销毁。 > [!NOTE] > - 当**构造**一个派生类对象时,其**虚基类部分将==首先==被构造**,然后是其他非虚基类的部分,最后才会初始化派生类特有的成员; > - 当**析构**一个派生类对象时,其**虚基类部分将==最后==被销毁**,首先销毁派生类特有部分,然后销毁非虚基类的部分。 > <br><br> ### 使用派生类对象对基类对象进行"初始化或赋值" C++中**可以使用==派生类对象==对一个==基类对象==初始化或赋值**,将会调用**基类的拷贝/移动构造函数**、拷贝/移动赋值运算符。 其中,**只有该派生类对象中的基类部分会被拷贝、移动或赋值**,而**派生类特有的部分将会被忽略掉**,即发生 **==对象切片==问题**(sliced down)。 ```cpp title:use_derived_obj_init_or_assign_a_base_obj.cpp struct Base { Base() = default; Base(const Base& other) { cout << "Invoke Base's copy constructor\n"; } Base(Base&& other) { cout << "Invoke Base's move constructor\n"; } Base& operator=(const Base& other) { if (this != &other) { cout << "Invoke Base's operator=()\n"; } return *this; } }; struct Derived : public Base{ Derived() = default; }; int main() { Derived obj_d; // 使用派生类来初始化基类对象. Base obj_b1 = obj_d; // 调用基类的拷贝构造函数 Base obj_b2 = std::move(obj_d); // 调用基类的移动构造函数 // 使用派生类来为基类赋值. obj_b1 = obj_d; // 调用基类的拷贝赋值运算符 obj_b1 = std::move(obj_d); // 调用基类的移动赋值运算符 } ``` <br><br> # final 禁止继承与禁止重写 ![[02-开发笔记/01-cpp/类与对象/cpp-类成员函数的基本说明#final 说明符|cpp-类的成员函数#final 说明符]] <br><br> # using 声明在继承中的三个作用 ### 使用 `using` 声明修改派生访问权限 派生类可通过 **`using` 声明**来修改**对 ==从基类继承来的某个可访问成员==的 "派生访问权限"** ,将其添加到**派生类的特定访问权限域中**。 **`using` 声明语句中的==名字/标识符的访问权限==**,由**该 `using` 声明语句之前的==访问说明符**==来决定。 ```cpp title:change_derived_access class Base { public: int public_mem = 5; protected: void prot_method() {}; }; class Derived : private Base { // 私有继承, 派生类从基类继承来的所有基类部分变成 "派生类的私有成员". public: // 派生类在其"public"访问说明符下通过`using`语句将继承来的"private成员"变为 "public成员" using Base::public_mem; using Base::prot_method; }; int main() { Derived obj; obj.public_mem; obj.prot_method(); } ``` ### 使用 `using` 声明引入基类中被隐藏的同名函数 > 参见 [^1] 派生类中的函数会 **"==隐藏==" 基类中的所有同名函数**,而不构成重载。 如果希望**将基类中的同名函数引入,与派生类定义的版本同时形成重载**,可使用 `using` 声明引入,如下所示: ```cpp struct Base { void memfcn(); virtual void memfcn(int); }; struct Derived : Base { void memfcn(string); // 将 "隐藏"基类的所有memfcn, 而不是构成重载. using Base::memfcn; // 通过`using`声明, 将原先基类中被隐藏的同名函数全部引入 // 此时, 派生类中存在着三个重载版本. }; int main() { Derived d; d.memfcn("hello"); d.memfcn(); // error: 基类的`memfcn()`已经被隐藏, 而不构成重载 d.memfcn(10); // error: 基类的`memfcn(int)`已经被隐藏, 而不构成重载 } ``` ### 使用 `using` 声明继承构造函数 > 这是 `using` 声明用于 "**基类构造函数**" 时的特殊作用,相当于**语法糖**,旨在简化派生类构造函数的编写。 派生类可以通过 `using` 声明 **重用/继承** 其 **==直接基类==** 定义的构造函数[^4]。 "**==重用/继承==**" 是指对于**基类的每个构造函数**(除默认、拷贝、移动构造函数外), 编译器都将**在派生类中生成一个与之对应的、==形参列表完全相同的构造函数**==,形式为 `Derived(params) : Base(args) {};` 。 其中,派生类自己的数据成员将被 "**默认初始化**"。 由此,**可以直接初始化派生类对象**,而无需在派生类中显式地定义这些"派生类的构造函数"。 > [!caution] `using` 声明用于继承 **==构造函数==**的注意事项 > - ** `using` 声明不会继承基类的==默认、拷贝、移动构造函数==**。 <br>如果派生类没有自定义,则编译器将可能根据对应情况自动提供 "合成版本"。 > > > - **`using` 声明不会继承基类中 "==与派生类已有构造函数 '同签名'== 的基类构造函数"** > > > - **`using` 声明不会改变继承的基类构造函数的==访问级别==**,无论 `using` 声明语句位于派生类定义中的哪个访问说明符下。(这与 `using` 声明用于其他名称时不同) > > > - **`using` 声明不会继承基类的构造函数中的"==默认实参=="**。<br>对于**一个具有默认实参基类构造函数**,派生类将继承得到**多个**无默认实参的版本: > - 一个版本包含完整形参列表,但都没有默认实参; > - 其余多个版本,每个版本**分别去除掉一个 "原本具有默认实参" 的形参**。 > > > - 在涉及**多重继承**时,如果派生类的**多个直接基类**中**存在 '==同签名==' 的构造函数**,则派生类必须为该构造函数定义其自己版本而不能继承,否则编译错误。 ```cpp title:derive_constructor_of_base_class.cpp class Base { public: Base(int x) { cout << "Invoke `Base(int)` " << endl; } Base(char ch) { cout << "Invoke `Base(char)` " << endl; } Base(double db) { cout << "Invoke `Base(double)` " << endl; } }; class Derived : public Base { public: // 通过`using`声明继承基类的构造函数(除默认、拷贝、移动构造函数) using Base::Base; }; int main() { // Derived自己未定义构造函数, 而是通过`using`声明重用/继承了基类的构造函数. Derived d1(10); // 相当于调用`Derived(int) : Base(int)`; Derived d2(3.16); // 相当于调用`Derived(double) : Base(double)`; Derived d3('F'); // 相当于调用`Derived(char) : Base(char)`; } ``` <br><br><br> # 派生类与基类的类型转换 ![[02-开发笔记/01-cpp/类型相关/cpp-类型转换#向上转型与向下转型|cpp-类型转换#向上转型与向下转型]] <br><br><br> # 虚继承 "**虚继承"**(virtual derived)机制用于解决**多重继承**时,**一个派生类==通过不同路径间接地多次继承同一个基类==导致派生类中==存在多个基类实例副本**== 的问题[^5]。 **虚继承**通过在 "派生列表" 中使用 **`virtual` 关键字** 指定,被 "**虚继承**" 的基类称为 "**==虚基类==**(virtual base class)**"。 > [!NOTE] **虚继承**不影响该**派生类**本身,只影响 "**其派生类进一步派生出来的类,即==间接子类==**"。 > > "虚继承" 意味着 **"该派生类的派生类" 将可以 "==共享" 该虚基类==,即只持有==虚基类的一份实例副本==**。 > [!example] 虚继承示例 > 对于下列继承关系中,通过**对 `Derived1` 和 `Derived2` 声明为"虚继承"自 `Base`**,则当另一个派生类同时继承自 `Derived1` 和 `Derived2` 时,其**间接基类 `Base` 的部分将只存在一份示例**。 > > ![[_attachment/02-开发笔记/01-cpp/类与对象/cpp-继承.assets/IMG-cpp-继承-FBFFC63BC18B3024691B54F9C1A39ACD.png|153]] > > 实现示例: > > ```cpp title: virtual_inherite. cpp > class Base { > public: > // Base 类成员 > }; > > class Derived1 : virtual public Base {}; // 声明虚继承 > class Derived2 : virtual public Base {}; // 声明虚继承 > > // `Derived`中将只持有一份间接基类`Base`的实例。 > class Derived : public Derived1, public Derived2 {}; > ``` > > <br> ## 虚基类的成员 虚继承中,被共享的**虚基类部分只存在==唯一的实例==**(`Base::xxx`)。 如果**虚基类的成员**未被任何派生类成员**覆盖或隐藏**,则最底层派生类中**将可以==直接访问==虚基类成员**。 > [!example] > ![[_attachment/02-开发笔记/01-cpp/类与对象/cpp-继承.assets/IMG-cpp-继承-13F6002369EA15912BA8C2AF304EE18E.png|905]] 说明示例: - 对 VIM 而言,同时存在变量 `Derived1::ival` 以及 `Base::ival`,**后者被前者隐藏**。 - 对 VIM 而言,同时存在函数 `Derived1::bar(char)` 以及 `Base::bar(int)`,**后者被前者隐藏**.。 ```cpp struct Base { void bar(int) { cout << "Base::bar(int)" << endl; } int ival = 1; double dval = 3.14; }; struct Derived1 : virtual public Base { void bar(char) { cout << "Derived1::bar(char)" << endl; } void foo(char) { cout << "Derived1::foo(char)" << endl; } char cval = 'A'; }; struct Derived2 : virtual public Base { void foo(char) { cout << "Derived2::foo(char)" << endl; } int ival = 2; char cval = 'B'; }; struct VIM : public Derived1, public Derived2 { void test() { // foo('G'); // 二义性错误, 在其直接基类Derived1与Derived2中均存在 Derived1::foo('G'); // cout << cval << endl; // 二义性错误 cout << Derived1::cval << endl; // 尽管Derived2中也有继承自基类的`Base::bar(int)`, 但被VIM的直接基类中的`Derived1::bar(char)`隐藏. bar('G'); // Derived1::bar(char). // 尽管Derived1中也有继承自基类的`Base::ival`, 但被VIM的直接基类中的`Derived2::ival`隐藏. cout << ival << endl; // Derived2::ival // 虚基类Base::dval, 未被其任何派生类同名成员覆盖或隐藏, 故在间接子类中可以被直接访问. cout << dval << endl; } }; ``` <br> ## 虚继承中的构造顺序 在虚继承中,由"**==最底层的"派生类==** 负责 **==构造虚基类的部分==**,**最底层派生类的构造函数**需要**调用==虚基类的构造函数==**。 > [!caution] 虚基类只能由最底层派生类初始化。对虚基类的初始化如果出现在 "中间基类"中,则这些初始值将被忽略。 - 编译器按照 "**直接基类**" 的**派生声明顺序**依次检查,如果其中存在 **直接基类之上存在==虚基类==**,则先**构造虚基类**。 - **所有虚基类**构造完成后,再按照声明顺序逐一构造 "**非虚基类**"。 > [!NOTE] 在普通继承中,**一个派生类只能初始化其 "==直接基类==",调用直接基类的构造函数**。 示例: ```cpp class ZooAnimal {}; class Bear : virtual public ZooAnimal {}; class Character {}; class BookCharacter : public Character {}; class ToyAnimal {}; class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal { public: // 由最底层派生类初始化其"虚基类" // 可显式调用其 "虚基类" 的构造函数, 若未显式调用则对其虚基类进行"默认初始化". TeddyBear() : ToyAnimal(), ZooAnimal() {} // 虚基类构造顺序与此处调用顺序无关, 只取决于派生声明列表. }; int main() { TeddyBear obj; // 构造顺序为: 首先构造完所有虚基类, 然后才是非虚基类. // ZooAnimal(); // Toynimal(); // Character(); // BookCharacter(); // Bear(); // TeddyBear(); } ``` <br><br> # 参考资料 # Footnotes [^1]: 《C++ Primer》P551 [^2]: 《C++ Primer》P552 [^3]: 《C++ Primer》P552-556 [^4]: 《C++ Primer》P557 [^5]: 《C++ Primer》P718