C++进阶01 继承与派生

   

图源:文心一言

听课笔记简单整理,供小伙伴们参考~🥝🥝

  • 第1版:听课的记录代码~🧩🧩

编辑:梅头脑🌸

审核:文心一言


目录

🐳课程来源

🐳继承

🐋7.1 继承的基本概念与语法

🐋7.1.1 单继承与多继承

🐋7.1.2 公有继承、私有继承与保护继承

🐋7.2 派生类的构造、析构函数

🐋7.2.1 派生类的构造函数

🐋7.2.2 派生类的析构函数

🐳冗余与二义性问题

🐋7.3 多继承问题1:同名隐藏问题1

🐋7.4 多继承问题1:同名隐藏问题2

🐋7.5 二义性问题

🐋7.5.1 二义类问题1

🐋7.5.2 二义类问题2

🐋7.6 虚基类

🐋7.6.1 虚基类

🐋7.6.2 有虚基类的构造函数

🔚结语


🐳课程来源

  • 郑莉李超老师的公开课:🌸C++语言程序设计进阶 - 学堂在线 (xuetangx.com)

🐳继承

🐋7.1 继承的基本概念与语法

📇相关概念

继承

在实际编程中,有时我们会需要扩展模块的功能,这时,我们通常不会从头开始编写代码。这不仅很麻烦,其次也很玄学:谁也不知道这堆尸米山代码动哪根柱子就塌了。每当提及到此,讨论主题总能够从技术领域上升到哲学领域。

还好我们不需要讨论得这样深刻——我们仅讨论如何使用继承的功能,以尽可能小的代价改动程序框架,让这堆脆弱又庞大的尸米山Run起来,以此维护我们更脆弱的饭碗~

🐋7.1.1 单继承与多继承

在写代码时,我们看到现有代码或者网络的数据库哪个类不错,可以选择继承它,并且在此基础上再添加一些自己的函数;有时,如果我们如果能发现很多父类的条件都具有我们所需要的功能,也不用辛苦地做抉择,可以同时都继承过来,既要又要(你可能想说:“三姓家奴休走!燕人张飞在此!”)~

以下代码,展示了类Derived继承Base,并增加自己的构造与析构函数的语法框架。具体的代码会在后面的习题中详细展示~

⌨️单继承代码

class Derived : public Base {   // 派生类名 Derived,继承方式 public, 基类名Base
public:                         // 除了基类以外,派生类新增的成员Derived();~Derived();
};

⌨️多继承代码

class Derived : public Base1, private Base2 {   // 派生类名 Derived,继承了基类Base1的public 与 Base2 的private;
public:                                         // 除了基类以外,派生类新增的成员Derived();~Derived();
};

🐋7.1.2 公有继承、私有继承与保护继承

继承的方式分为:公有继承,私有继承与保护继承。其中:

  • 公有继承:如将父类的公有成员继承为派生类的公有成员。
  • 私有继承:将父类的公有和保护成员继承为派生类的私有成员。
  • 保护继承:将父类的公有和保护成员继承为派生类的保护成员。

三种继承方式中,公有继承是最常用的,因为它使用起来是最方便的,以下展示三种继承的代码:

⌨️公有继承代码

// Point.h

//Point.h
#ifndef _POINT_H_
#define _POINT_H_class Point {
public:void initPoint(float x = 0, float y = 0) { this->x = x, this->y = y; }void move(float offX, float offY) { x += offX, y += offY; }float getX() const { return x; }float getY() const { return y; }
private:float x, y;
};#endif // !_POINT_H_

// Rectangle.h

// Rectangle.h
#ifndef _RECTANGLE_H_
#define _RECTANGLE_H_#include "Point.h"class Rectangle : public Point {    // 派生类定义部分
public:     // 新增公有函数成员void initRectangle(float x, float y, float w, float h) {    initPoint(x, y);            // 调用基类公有成员函数this->w = w;this->h = h;}float getH() const { return h; }float getW() const { return w; }
private:    // 新增私有函数成员float w, h;
};#endif // !_RECTANGLE_H_

// main.cpp

#include <iostream>
using namespace std;
#include "Rectangle.h"int main()
{Rectangle rect;                     // 定义Rectangle类的对象rect.initRectangle(2, 3, 20, 10);   // 设置矩形的数据rect.move(3, 2);                    // 移动矩形的位置,rectangle中虽然没有写move,但是它继承了类point,因此可以通过类rect间接调用类point的成员函数movecout << "The data of rect(x, y, w, h)" << endl;cout << rect.getX() << ", " << rect.getY() << ", " << rect.getW() << ", " << rect.getH() << endl;   // 输出矩阵的特征参数return 0;
}

📇公有继承执行结果

📇公有继承代码说明

(在含有继承的情况下,构造函数的限制较多,因此Rectangle类在这里没有写显式的构造函数,而是由编译器生成构造函数,并由普通成员函数initRectangle进行初始化~)

这里展示了一个简单的例子,其中Rectangle类公有继承了Point类,并添加了宽度w和高度h属性。通过公有继承,Rectangle类可以访问Point类的公有成员函数和属性。

需要初始化的4个成员(x, y, w, h)中,仅变量w, h是Rectangle类可以初始化的,而由于公有继承,Point类的私有变量x, y并未直接继承到rect,需要通过在Point类中定义的构造函数initPoint去访问与初始化;

从调试窗口可以看到,对象rect中内嵌point对象与变量w, h:

⌨️私有继承代码

// Point.h:同 公有继承代码;

// Rectangle.h:新增公有函数成员“float getX() const”,“float getY() const”,其中包括需要被调用的Point类的公有成员接口,否则在私有继承的前提下,无法调用Point类的函数;

// Rectangle.h
#ifndef _RECTANGLE_H_
#define _RECTANGLE_H_#include "Point.h"class Rectangle : private Point {    // 派生类定义部分
public:     void initRectangle(float x, float y, float w, float h) {initPoint(x, y);            // 调用基类公有成员函数this->w = w;this->h = h;}void move(float offX, float offY) { Point::move(offX, offY); }float getX() const { return Point::getX(); }float getY() const { return Point::getY(); }float getH() const { return h; }float getW() const { return w; }
private:    // 新增私有函数成员float w, h;
};#endif // !_RECTANGLE_H_

 // main.cpp:同 公有继承代码;

📇私有继承代码说明

在私有继承中,派生类只能访问其直接基类的公有成员和保护成员作为自己的私有成员。

私有继承相比公有继承,不会再默认继承所有函数,想使用任何函数都必须提前声明,并且这个函数在派生类中将成为私有函数(私有继承只是改变了基类成员在派生类中的访问级别)。

因此,如果需要在派生类中使用这些基类的函数,通常需要在派生类中提供相应的公有成员函数来调用它们。例如,如果注释掉“float getX() const”,“float getY() const”,编译器就没办法为X,Y坐标初始化了~

⌨️保护继承代码

class A {
protected:int x;
};class B : public A {
public:void function();
};
void B :: function() {x = 5;   // 正确,继承与派生类可以正确访问保护类型成员
}int main() {A a;a.x = 5; // 错误,成员A::x不可以访问,因为保护成员在类外看就像private
}

📇保护继承代码说明

通过静态编译报错,我们可以简单地获得以下结论:

  • class A的protected成员变量x,在main()函数中直接访问是不可以的,因为在派生类外,变量x被视作私有变量;
  • class B通过公有继承class A, 属于派生类因此可以访问x与为x赋值~

保护继承的实际作用范围,可能是中小型团队自己书写的项目,保护性很好;如果是大型团队的项目,难以避免代码不是符合规范的写法,而是一起粘粘贴贴拼拼凑凑做成的,用保护类型可能会造成访问的不便~

⌨️多继承代码

class A {
public:void setA(int);void showA() const;
private:int a;
};
class B {
public:void setB(int);void showB() const;
private:int b;
};
class C : public A, private B {     // C类多继承,以公有方式继承A,以私有方式继承B
public:void setC(int, int, int);void showC() const;
private:int c;
};void  A::setA(int x) {a = x;
}
void B::setB(int x) {b = x;
}
void C::setC(int x, int y, int z) {setA(x);    // c需要调用A、B的公有函数为 a, b赋值setB(y);c = z;      // c只能为自己新增的成员赋值
}//其他函数实现略int main() {C obj;obj.setA(5);obj.showA();obj.setC(6, 7, 9); obj.showC();// obj.setB(6);  // 错误:B是私有继承的,B的公有函数到C中也变为了私有函数,不能通过类外直接访问// obj.showB();  // 错误return 0;
}

📇多继承代码说明

这里展示了class C 公有继承 class A,私有继承 classB之后的一些注意事项。例如,我们可以通通过C的对象去访问A,但是无法再通过C的对象去访问B,因为B是私有继承,其基类的函数在派生类是私有函数~ 

🐋7.2 派生类的构造、析构函数

🐋7.2.1 派生类的构造函数

以下展示B为基类,C为公有继承派生类如何调用自己的构造函数~

⌨️代码

#include <iostream>
using namespace std;class B {
public:B();        // 默认构造函数B(int i);   // 含参数的构造函数~B();       // 系统构造函数void print() const;
private:int b;
};B::B(){b = 0;cout << "B's default constructor called." << endl;
}
B::B(int i) {b = i;cout << "B's constructor called." << endl;
}
B::~B() {cout << "B's destructor called." << endl;
}
void B::print() const {cout << b << endl;
}class C : public B {
public:C();C(int i, int j);~C();void print() const;
private:int c;
};C::C() {c = 0;cout << "C's default constructor called." << endl;
}
C::C(int i, int j):B(i),c(j) {cout << "C's constructor called." << endl;
}
C::~C() {cout << "C's destructor called." << endl;
}
void C::print() const {cout << c << endl;
}int main()
{C obj(5, 6);obj.print();return 0;
}

📇执行结果

📇代码解释

在C的构造函数“C(int i, int j)”中,使用初始化列表为B类的构造函数传递参数i,并为C类的成员变量c传递j~

初始化执行的顺序为:

  • 首先,通过对象obj调用C类的构造函数。  
  • 在C类的构造函数中,通过初始化列表调用B类的构造函数,传递参数i。B类构造函数执行完成后,控制返回C类构造函数。  
  • 接着,在C类构造函数中初始化成员变量c为j。 

析构时执行的顺序恰好相反:

  • 首先调用派生类C的析构函数。  
  • 然后调用基类B的析构函数。  

此时,如果在C类的构造函数中没有显式地为B类构造函数传递参数i,那么B类将使用自己的默认构造函数进行初始化。同样地,如果在C类的构造函数中没有为成员变量c提供初始化值,那么c将被默认初始化。

例如,在本例如果增加未含有初始化值的对象obj2,则其坐标(i, j)会被C与B的默认构造函数初始化为0(注意,此处如果我们没有写默认构造函数,那么编译器就会开始报错)。

int main()
{C obj(5, 6); C obj2;        // 调用默认构造函数obj.print();obj2.print();return 0;
}

那么输出也会随之更改~

🐋7.2.2 派生类的析构函数

下面展示稍微复杂一些的初始化:派生类除了自身的构造函数,也增加了私有变量。那么初始化的顺序是怎样的呢?

⌨️代码

#include <iostream>
using namespace std;
class Base1 {
public:Base1(int i){cout << "Constructing Base1 " << i << endl;}~Base1() { cout << "Destructing Base1" << endl; }
};
class Base2 {
public:Base2(int j){cout << "Constructing Base2 " << j << endl;}~Base2() { cout << "Destructing Base2" << endl; }
};
class Base3 {
public:Base3() { cout << "Constructing Base3 *" << endl; }~Base3() { cout << "Destructing Base3" << endl; }
};class Derived : public Base2, public Base1, public Base3 {
public:Derived(int a, int b, int c, int d) : Base1(a), member2(d), member1(c), Base2(b){ }
private:Base1 member1;Base2 member2;Base3 member3;
};int main() {Derived obj(1, 2, 3, 4);return 0;  
}

📇代码解释

在Derived的构造函数“Derived(int a, int b, int c, int d) : Base1(a), member2(d), member1(c), Base2(b)”中,初始化的变量分别为:“基类Base1的构造函数,基类Base2的私有成员,基类Base2的构造函数,基类Base1的私有成员” 传值~

根据之前打代码的经验,假设的初始化顺序会不会是这样呢?

  • 基类Base1的构造函数使用 int a 初始化;
  • 基类Base2的私有成员member 2 使用 int b 初始化;
  • 基类Base2的构造函数使用 int c 初始化;
  • 基类Base1的私有成员member 1 使用 int c 初始化;

实际上完全不一样好的嘛,初始化顺序与构造函数定义的初始化顺序没有关系~

构造函数初始化顺序:

  • 首先,按照基类在继承列表中出现的顺序“class Derived : public Base2, public Base1, public Base3 ”从左到右进行初始化,也就是:Base2, Base1, Base3;
  • 其次,按照成员对象在类定义中出现的顺序“private: Base1 member1; Base2 member2; Base3 member3;”进行初始化,也就是:member1, member2, member3; 

关于Base3和member3的初始化:为什么Base3 和 member3 明明没有传值,也会被初始化呢?

  • 因为它是Derived的一个基类。如果Base3没有定义任何构造函数,或者定义了一个或多个构造函数但其中至少有一个是默认构造函数(即可以无参数调用的构造函数),则编译器会自动调用Base3的默认构造函数进行初始化;
  • 类似地,如果Derived类中包含一个名为member3的Base3类型成员对象,并且没有在Derived构造函数初始化列表中显式初始化它,那么member3也会通过调用Base3的默认构造函数进行初始化。

析构函数调用顺序

  • 首先调用成员对象的析构函数,按照它们在类定义中出现的相反顺序析构:member3,member2,member1; 
  • 然后调用继承类的析构函数,也按照它们在继承列表中出现的相反顺序析构:Base3,Base1,Base2;

另外,AI特别强调了:在实际编程中,应该确保代码的可读性和可维护性。特别是打乱基类和成员对象的初始化顺序这样的做法是不推荐的,因为它可能会导致混淆和错误。正确的做法是遵循C++的初始化顺序规则,并按照规范的代码风格编写代码。


🐳冗余与二义性问题

🐋7.3 多继承问题1:同名隐藏问题1

📇相关概念

在上面多继承的片段中,我们得知公有继承、私有继承对象访问函数与变量的权限。

现在讨论一种特殊的情况:如果我们的代码中,从原始基类对应的每一个派生类,都有相同的函数名字,那么我们在访问这些函数时,是不是也可以从类名区分基类的函数与派生类的函数呢?

⌨️代码

#include <iostream>
using namespace std;class Base1 {
public:void display() const {      // 基类Base1定义cout << "Base1::display()" << endl;}
};
class Base2: public Base1{      // 公有派生类Base2定义
public:void display() const {cout << "Base2::display()" << endl;}
};
class Derived :public Base2 {   // 公有派生类Derived定义
public:void display() const {cout << "Derived::display()" << endl;}
};
void fun(Base1* ptr) {          // 参数为指向基类对象的指针ptr->display();             // 尝试调用各个类所对应对象自身的display函数
}int main()
{Base1 base1;Base2 base2;Derived derived;fun(&base1);        // 用Base1对象的指针调用fun函数;fun(&base2);        // 用Base2对象的指针调用fun函数;fun(&derived);      // 用Derived对象的指针调用fun函数;return 0;
}

📇代码解释

我们定义了不同类的各自的对象,用来访问本类中的display()函数。理想状态下的输出结果:

  •     fun(&base1); // 用Base1对象的指针调用fun函数;Base1的fun函数被调用;
  •     fun(&base2); // 用Base2对象的指针调用fun函数;Base2的fun函数被调用;
  •     fun(&derived); // 用Derived对象的指针调用fun函数;Derived的fun函数被调用;

实际状态下的输出结果:

  •     fun(&base1); // 用Base1对象的指针调用fun函数;Base1的fun函数被调用;
  •     fun(&base2); // 用Base2对象的指针调用fun函数;Base1的fun函数被调用;
  •     fun(&derived); // 用Derived对象的指针调用fun函数;Base1的fun函数被调用;

我们可以发现,display()这个函数调用来调用去,最后还最原始的基类在工作。这是因为,fun()函数中的指针是基类指针,它只能调用基类中的成员函数。当使用基类指针指向派生类对象并调用一个非虚函数时,会发生静态绑定,即编译时就已经确定了要调用的函数是基类中的版本。

而且,基类指针很少隐式地转换为派生类指针(C++存在此功能,但是通常不建议这样做,这是不安全的),而派生类指针不能隐式地转换为基类指针(除非进行显式的类型转换)。

因此,不要重新定义继承而来的非虚函数,会使代码的可读性变得很差。实际上,如果希望在派生类中重写基类的函数,并希望通过基类指针或引用来调用派生类中的版本,应该将该函数声明为虚函数。这样,就会发生动态绑定,即在运行时确定要调用的函数版本。

以下是使用虚函数的代码,将Base1类中的display函数声明为虚函数~

⌨️方法一:虚函数代码

#include <iostream>  
using namespace std;  class Base1 {  
public:  virtual void display() const {      // 基类Base1定义,将display声明为虚函数  cout << "Base1::display()" << endl;  }  
};  class Base2: public Base1 {      // 公有派生类Base2定义  
public:  void display() const override {     // 使用override关键字明确表示重写基类中的虚函数  cout << "Base2::display()" << endl;  }  
};  class Derived: public Base2 {   // 公有派生类Derived定义  
public:  void display() const override {     // 同样使用override关键字  cout << "Derived::display()" << endl;  }  
};  void fun(Base1* ptr) {          // 参数为指向基类对象的指针  ptr->display();             // 现在将调用各个类所对应对象自身的display函数  
}  int main() {  Base1 base1;  Base2 base2;  Derived derived;  fun(&base1);        // 用Base1对象的指针调用fun函数,将输出 "Base1::display()"  fun(&base2);        // 用Base2对象的指针调用fun函数,将输出 "Base2::display()"  fun(&derived);      // 用Derived对象的指针调用fun函数,将输出 "Derived::display()"  return 0;  
}

当然,如果你在调用时已经知道了类的结构,那么你也可以通过以下方式调用函数:

⌨️方法二:类名调用代码

通过类名调用代码如下~

int main()
{Base1 base1;Base2 base2;Derived derived;base1.Base1::display();base2.Base2::display();derived.Derived::display();return 0;
}

📇执行结果

🐋7.4 多继承问题1:同名隐藏问题2

📇相关概念

与上一个问题相关,在多继承的条件下,通过对象去调用各个函数的代码如下~

⌨️代码

#include <iostream>
using namespace std;
class Base1 {
public:int var;void fun() { cout << "Member of Base1" << endl; }
};
class Base2 {
public:int var;void fun() { cout << "Member of Base2" << endl; }
};
class Derived : public Base1, public Base2 {    // Derived继承了Base1、Base2,同时,3个函数分别定义了类的变量var;
public:int var;void fun() { cout << "Member of Derived" << endl; }
};int main() {Derived d;Derived* p = &d;//访问Derived类成员;通过对象名.成员名标识访问d.var = 1;d.fun();//访问Base1基类成员;作用域分辨标识符,通过基类名访问d.Base1::var = 2;d.Base1::fun();//访问Base2基类成员;作用域分辨标识符,通过指针访问p->Base2::var = 3;p->Base2::fun();return 0;
}

📇执行结果

📇代码解释

在这个例子中,Derived类继承了Base1Base2类,并且所有三个类都有一个名为var的成员变量和一个名为fun的成员函数。由于同名隐藏规则,Derived类中的varfun会隐藏基类中的同名成员。

这时,如果通过Derived访问函数fun(),那么它会默认访问Derived成员。如果想让其访问Base1 基类成员,Base 2基类成员,就需要借用通过使用作用域分辨符(::)访问被隐藏的基类成员。

🐋7.5 二义性问题

🐋7.5.1 二义类问题1

📇相关概念

在上一道题的基础上,我们考虑得更复杂一些:在多继承的条件下,如果我想通过派生类的对象访问函数,此时多个基类有同名的成员函数,而恰好派生类又没有这个函数,那该怎么办呢?

class A {
public:void f();
};
class B {
public:void f();void g();
};
class C : public A, public B {
public:void g();void h();
};

如果定义:C  c1;

  • 则 c1.f():具有二义性,因为编译器会不知道调用A的f()函数还是B的f()函数;
  • 而 c1.g():无二义性(同名隐藏),因为即使基类B有g(),如果派生类C中也有g(),编译器会优先调用类C的g();

解决c1.f()二义性的方法有2个:

  • 使用类名限定: 使用c1.A::f()或C1.B::f()区分访问A或B的类;
  • 同名隐藏 :在C类中声明同名成员c1.f(),f()在根据需要调用c1.A::f()或C1.B::f()。

🐋7.5.2 二义类问题2

还有一种问题:作为孙子类想访问爷爷类的成员时,如果两个父类都继承了该成员,那么可以孙子可以直接通过访问爷爷类的成员吗?

⌨️代码 

#include <iostream>
using namespace std;class Base0 {   //定义基类Base0
public:int var0;void fun0() { cout << "Member of Base0" << endl; }
};
class Base1 : public Base0 { //定义派生类Base1 
public: //新增外部接口int var1;
};
class Base2 : public Base0 { //定义派生类Base2 
public: //新增外部接口int var2;
};
class Derived : public Base1, public Base2 {
public:int var;void fun(){cout << "Member of Derived" << endl;}
};

以下,我们通过3种途径更改var的值:(1)父类;(2)父类的父类;(3)子类。

(1)事实上,如果我们想访问var0,可以通过父类Base1或者父类Base2访问:

int main() {  Derived d;d.Base1::var0 = 2;d.Base1::fun0();d.Base2::var0 = 3;d.Base2::fun0();return 0;
}

这两个调用语句都可以显示正确的执行结果~

(2)甚至,如果想通过爷爷类Base0的接口访问var0,也是可以的。虽然老师的教案(10年前的版本)应该是不可以的,但我估计编译器又默默优化,它不会迷路了~

int main() {  Derived d;d.Base0::var0 = 3;d.Base0::fun0();return 0;
}

不过有个小问题,d 含有两个构造类函数 Base1和Base2,其中Base1和Base2分别包含着Base0。在赋值完成以后,只有Base1的var0被初始化了,而Base2的var0还是乱七八糟的数据~

(3)但是,如果试图通过孙子类Derived直接访问var0,编译器会报错的,它会显示: "Derived::var0" 不明确。 可能编译器不知道应该通过哪个路径赋值给var0。

int main() {  Derived d;d.var0 = 2;d.fun0();return 0;
}

为了解决这个问题,我们可以使用虚继承~

🐋7.6 虚基类

🐋7.6.1 虚基类

📇相关概念

虚继承是C++中的一种继承方式,用于解决多重继承时的二义性和数据冗余问题。通过虚继承,可以确保在继承结构中只有一个共享的基类子对象,一定程度上解决二义性与冗余的问题~

⌨️虚继承代码

#include <iostream>
using namespace std;
class Base0 {
public:int var0;void fun0() { cout << "Member of Base0" << endl; }
};
class Base1 : virtual public Base0 {  
public:int var1;
};
class Base2 : virtual public Base0 {
public:int var2;
};class Derived : public Base1, public Base2 {//定义派生类Derived 
public:int var;void fun() {cout << "Member of Derived" << endl;}
};int main() {Derived d;d.var0 = 2;   //直接访问虚基类的数据成员d.fun0();     //直接访问虚基类的函数成员return 0;
}

在上述代码中,Base1和Base2虚继承了Base0。因此,Derived类中只有一份Base0的子对象,解决了数据冗余问题。现在可以通过Derived类的对象d直接访问Base0的成员var0和fun0(),而不会产生二义性~

通过与上一个通过爷爷类Base0 的截图进行比较,在赋值的过程中,直接更改了路径上所有的var0的值~

需要注意的是,虚继承并不是解决所有多继承二义性问题的万能药。它可能不适合规模很大的项目,因为虚继承会增加一些额外的开销。此外,虚继承还会影响构造函数的调用顺序和析构函数的释放顺序,需要特别注意。

🐋7.6.2 有虚基类的构造函数

⌨️代码

#include <iostream>
using namespace std;class Base0 {
public:Base0(int var) : var0(var) { }int var0;void fun0() { cout << "Member of Base0" << endl; }
};
class Base1 : virtual public Base0 {
public:Base1(int var) : Base0(var) { } // 为虚基类的最远构造函数传递参数int var1;
};
class Base2 : virtual public Base0 {
public:Base2(int var) : Base0(var) { }int var2;
};class Derived : public Base1, public Base2 {
public:Derived(int var) : Base0(var), Base1(var), Base2(var) { }int var;void fun(){cout << "Member of Derived" << endl;}
};int main() {    //程序主函数Derived d(1);d.var0 = 2; //直接访问虚基类的数据成员d.fun0();   //直接访问虚基类的函数成员return 0;
}

📇代码解释

参数列表:这部分内容与AI审核有歧义,AI认为Base0的构造函数被调用了2次,不过通过跟踪调试和测试,发现确实只调用了1次~

  • 如果是直接继承,只写Base1,Base2的传参就好;

  • 如果是虚继承,需要为虚基类Base0的最远构造函数传递参数,该过程只需要1次,不会通过Base1与Base2反复调用~

在有虚基类的继承结构中,构造函数的调用顺序会发生变化。

  • 首先调用虚基类的构造函数;

  • 然后按照非虚基类的声明顺序调用它们的构造函数;

  • 最后调用派生类的构造函数。

析构函数的调用顺序:与构造函数的调用顺序相反。


🔚结语

博文到此结束了,写得模糊或者有误之处,期待小伙伴留言讨论与批评,督促博主优化内容{例如有错误、难理解、不简洁、缺功能}等,博主会顶锅前来修改~~😶‍🌫️😶‍🌫️

我是梅头脑,本片博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下,感谢点赞小伙伴对于博主的支持~~🌟🌟

同系列的博文:🌸数据结构_梅头脑_的博客-CSDN博客

同博主的博文:🌸随笔03 笔记整理-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/527044.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoo的火车订票管理系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

使用scrapy爬取蜻蜓FM

创建框架和项目 ### 1. 创建虚拟环境 conda create -n spiderScrapy python3.9 ### 2. 安装scrapy pip install scrapy2.8.0 -i https://pypi.tuna.tsinghua.edu.cn/simple### 3. 生成一个框架并进入框架 scrapy startproject my_spider cd my_spider### 4. 生成项目 scrapy …

一个用libcurl多线程下载断言错误问题的排查

某数据下载程序&#xff0c;相同版本的代码&#xff0c;在64位系统中运行正常&#xff0c;但在32位系统中概率性出现断言错误。一旦出现&#xff0c;程序无法正常继续&#xff0c;即使重启亦不行。从年前会上领导提出要追到根&#xff0c;跟到底&#xff0c;到年后的今天&#…

数据标注专业团队

数据标注服务 在跟一些淘宝、多多商家老板合作后&#xff0c;客户一般付款后&#xff0c;中介是有20%左右的提成&#xff0c;我们主要是希望可以实现数据标注无中介化&#xff0c;有需求可以直接联系数据标注团队直接负责人&#xff0c; 若有意愿请添加添加v&#xff1a;shu_ju…

全网最最最详细-bash: wget: 未找到命令如何解决教程

在基于 Red Hat 的系统中&#xff0c;包管理器是 yum 或者在更新的版本中是 dnf。 如果你的系统是 CentOS 7 或更早版本&#xff0c;你应该使用 yum 命令来安装 wget&#xff1a; sudo yum install wget如果你的系统是 CentOS 8 或者 Fedora&#xff0c;你可能需要使用 dnf 命…

SPI读取TLE5012B编码器角度数据_ACM32G103

买到一个带编码器的电机&#xff0c;卖家留出来读取编码器数据的接口有SPI 具体的原理图如下&#xff1a; P2标注的是SPI的接口&#xff0c;其中MISO和MOSI是硬件连接到一起的 使用ACM32G103的硬件SPI2去读取其角度数据 原理大概如下&#xff1a; 1、先发送读取数据的指令&…

Java二叉树 (2)

&#x1f435;本篇文章将对二叉树的一些基础操作进行梳理和讲解 一、操作简述 int size(Node root); // 获取树中节点的个数int getLeafNodeCount(Node root); // 获取叶子节点的个数int getKLevelNodeCount(Node root,int k); // 获取第K层节点的个数int getHeight(Node r…

浅谈2024 年 AI 辅助研发趋势!

目录 ​编辑 引言 一、AI辅助研发现状 1. 技术发展 2. 工具集成 3. 应用场景 二、AI辅助研发趋势 1. 更高的自动化程度 2. 更高的智能化程度 3. 更多的领域应用 4. 更高的重视度 三、结论 四. 完结散花 悟已往之不谏&#xff0c;知来者犹可追 创作不易&#xff…

阿里云服务器多少钱一年?价格表新鲜出炉(2024)

2024阿里云服务器优惠活动政策整理&#xff0c;阿里云99计划ECS云服务器2核2G3M带宽99元一年、2核4G5M优惠价格199元一年&#xff0c;轻量应用服务器2核2G3M服务器61元一年、2核4G4M带宽165元1年&#xff0c;云服务器4核16G10M带宽26元1个月、149元半年&#xff0c;云服务器8核…

SpringCloudGateway理论与实践

文章目录 网关介绍为什么需要网关Gateway 使用gateway pom依赖yml 配置重启测试总结 断言过滤器工厂路由过滤器的种类请求头过滤器默认过滤器全局过滤器总结 Gateway解决跨域 网关介绍 Spring Cloud Gateway 是一个基于Spring Framework 5&#xff0c;由Spring Cloud团队开发的…

如何解决新版的anaconda notebook 打不开浏览器

1 安装nodejs 先安装nodejs&#xff0c;里面有很多需要用node来启动服务 2 一片空白 安装jupyter以后启动&#xff0c; 结果就得到了如下&#xff0c;在chrome里面打开以后&#xff0c;一片空白 3 列出环境 conda create --name pytorch python3.9 conda env list cond…

Kubernetes-4

文章目录 Kubernetes-41、pod的生命周期2、pod的中止过程3、强制终止pod4、查看资源类型4.1、kubectl get 后面接的都是资源类型4.2、kubectl api-resources 查看目前有哪些资源类型 5、容器的状态5.1、总结5.2、Pod 状态和 Pod 内部容器状态5.3、容器的重启策略 6、探针probe6…