C++之多态

目录

1、为什么要用多态?

2、虚函数的定义

3、虚函数的实现机制

4、哪些函数不能被设置为虚函数?

5、虚函数的访问

5.1、指针访问

5.2、引用访问

5.3、对象访问

5.4、成员函数中访问

5.5、构造函数和析构函数中访问

6、纯虚函数

7、抽象类

8、虚析构函数

9、重载、隐藏、覆盖

10、测试虚表的存在

11、带虚函数的多基派生

12、虚拟继承

12.1、虚拟继承时派生类对象的构造和析构

12.2、菱形继承

13、效率分析


多态性(polymorphism)是面向对象设计语言的基本特征之一。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态性是面向对象的精髓。多态性可以简单地概括为“一个接口,多种方法”
多态按字面意思就是多种形态。比如说:警车鸣笛,普通人反应一般,但逃犯听见会大惊失色,拔腿就跑。又比如说,...通常是指对于同一个消息、同一种调用,在不同的场合,针对不同的对象下,执行不同的行为 。总结一句话就是:对于同一种指令(警车鸣笛),针对不同对象(警察、普通人、嫌疑犯),产生不一样的行为(行动、正常行走、藏起来)。

1、为什么要用多态?

我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性。
如果项目耦合度很高的情况下,维护代码时修改一个地方会牵连到很多地方,会无休止的增加开发成本。而降低耦合度,可以保证程序的扩展性。而多态对代码具有很好的可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

C++支持两种多态性:编译时多态和运行时多态。
编译时多态:也称为静态多态,我们之前学的函数重载、运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为先期联编(early binding)
运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding)。
C++通过虚函数来实现动态联编。
接下来,我们提到的多态,不做特殊说明,指的就是动态多态。

2、虚函数的定义

什么是虚函数呢?虚函数就是在基类中被声明为virtual,并在一个或多个派生类中被重新定义的成员函数。

概念:是一个成员函数,并且在前面加上virtual关键字
性质:如果在基类中定义类虚函数,那么在派生类中该函数就是虚函数,即使在派生类中没有加virtual

重定义(重写、覆盖):派生类要保证该虚函数的名字与基类相同,函数的返回类型也要相同,函数的参数列表也要相同(包括参数的个数、参数的类型、参数的顺序),言外之意,唯一可以不一样的,就是函数体。

其形式如下:

// 类内部
class 类名
{
virtual 返回类型 函数名(参数表){//...}
};
//类之外
virtual 返回类型 类名::函数名(参数表)
{//...
}

如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。派生类要对虚函数进行中可根据需重定义,重定义的格式有一定的要求:

1.与基类的虚函数有相同的参数个数;
2.与基类的虚函数有相同的参数类型;
3.与基类的虚函数有相同的返回类型。

class Base
{
public:virtual void display(){cout << "Base::display()" << endl;}virtual void print(){cout << "Base::print()" << endl;}
};
class Derived
: public Base
{
public:virtual void display(){cout << "Derived::display()" << endl;}
};
void test(Base *pbase)
{pbase->display();
}
int main()
{Base base;Derived derived;test(&base);test(&derived);return 0;
}

上面的例子中,对于test()函数,如果不管测试的结果,从其实现来看,通过类Base的指针pbase只能调用到Base类型的display函数;但最终的结果是25行的test调用,最终会调用到Derived类的display函数,这里就体现出虚函数的作用了,这是怎么做到的呢,或者说虚函数底层是的怎么实现的呢?

3、虚函数的实现机制

虚函数的实现是怎样的呢?简单来说,就是通过一张虚函数表(Virtual Fucntion Table)实现的。具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针(vfptr),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址.

当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖(override). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖

虚函数原理总结:当基类定义了虚函数,就会在该类创建的对象的存储布局的前面,新增一个虚函数指针,该指针指向虚函数表(简称虚表),虚表中存的是虚函数的入口地址(有多少虚函数都会入虚表)。当派生类继承基类的时候,会满足吸收的特点,那么派生类也会有该虚函数,所以派生类创建的对象的布局的前面,也会新增一个虚函数指针,该指针指向派生类自己的虚函数表(简称虚表),虚表中存的是派生类的虚函数的入口地址(有多少虚函数都会入虚表),如果此时派生类重写了从基类这里吸收过来的虚函数,那么就会用派生类自己的虚函数的入口地址覆盖从基类这里吸收过来的虚函数的入口地址。

上诉代码的内存布局图:

虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?从上面的例子,可以得出结论:
1、基类定义虚函数
2、派生类重写(重定义、覆盖)该虚函数
3、创建派生类的对象
4、用基类的指针指向(引用绑定)派生类的对象
5、使用基类的指针(引用)调用该虚函数

4、哪些函数不能被设置为虚函数?

1、普通函数(自由函数、全局函数):虚函数必须是成员函数,而普通函数是非成员函数。
2、内联成员函数:内联成员函数进行函数替换的时候,发生时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;如果将内联函数设置为虚函数,那此时就会失去内联的含义。
3、静态成员函数:静态成员函数发生时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;静态成员函数是共享的,被该类的所有对象共享。没有this指针
4、友元函数:如果友元函数本身是一个普通函数,那么友元函数不能被设置为虚函数。如果友元函数本身是另外一个类的成员函数,是可以被设置为虚函数的。友元关系不能被继承。
5、构造函数:构造函数不能被继承,但是虚函数是可以被继承的;构造函数发生的时机在编译的时候,而虚函数要体现动态多态,发生的时机在运行的时候;如果构造函数被设置为虚函数,那么要体现出多态,就需要放在虚表中,需要使用虚函数指针找到虚表,而如果构造函数都不调用,那对象是没有完全创建出来的,对象都不完整,此时有没有虚函数指针都不一定。(重要)

5、虚函数的访问

5.1、指针访问

使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;
使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。

5.2、引用访问

使用引用访问虚函数,与使用指针访问虚函数类似,表现出动态多态特性。不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。因此在使用上有一定限制,但这在一定程度上提高了代码的安全性,特别体现在函数参数传递等场合中,可以将引用理解成一种“受限制的指针”

5.3、对象访问

和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。通过对象名访问虚函数时, 调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。

5.4、成员函数中访问

在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。

5.5、构造函数和析构函数中访问

构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用“this->虚函数名”的形式来调用,编译器仍将其解释为静态联编的“本类名::虚函数名”。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。

#include<iostream>
using namespace std;
class Grandpa
{
public:Grandpa(){cout << "Grandpa()" << endl;}~Grandpa(){cout << "~Grandpa()" << endl;}virtualvoid func1(){cout << "Grandpa::func1()" << endl;}virtualvoid func2(){cout << "Grandpa::func2()" << endl;}
}; class Father: public Grandpa
{
public:Father(){cout << "Father()" << endl;func1();}~Father(){cout << "~Father()" << endl;func2();}virtualvoid func1(){cout << "Father::func1()" << endl;}virtualvoid func2(){cout << "Father::func2()" << endl;}
};
class Son: public Father
{
public:Son(){cout << "Son()" << endl;}~Son(){cout << "~Son()" << endl;}virtual void func1(){cout << "Son::func1()" << endl;}virtual void func2(){cout << "Son::func2()" << endl;}
};
int main()
{Son son;
}

运行结果:

接下来,我们看一个练习,大家思考一下,会输出什么呢?

#include<iostream>
using namespace std;
//主要考虑点在绑定时机
class A
{
public:virtualvoid func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
private:long _a;
};
class B: public A
{
public:virtualvoid func(int val = 10){cout << "B->" << val << endl;}
private:long _b;
};
int main(void)
{B b;A *p1 = (A*)&b;B *p2 = &b;p1->func();p2->func();return 0;
}

输出结果:

6、纯虚函数

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的格式如下:

class 类名
{
public:virtual 返回类型 函数名(参数包) = 0;
};

设置纯虚函数的意义,就是让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”

class Base
{
public:virtual void display() = 0;
};
class Derived
: public Base
{
public:virtual void display(){cout << "Derived::display()" << endl;}
};

声明纯虚函数的目的在于,提供一个与派生类一致的接口。

纯虚函数的应用实例:

class Figure
{
public:virtual void display() const = 0;virtual double area() const = 0;
};
class Circle
: public Figure
{
public:explicit Circle(double radius): _radius(radius){}void display() const{cout << "Circle";}double area() const{return 3.14159 * _radius * _radius;}
private:double _radius;
};
class Rectangle
: public Figure
{
public:Rectangle(double length, double width): _length(length), _width(width){}void display() const{cout << "Rectangle";}double area() const{return _length * _width;}
private:double _length;double _width;
};
class Triangle
: public Figure
{
public:Triangle(double a, double b, double c): _a(a), _b(b), _c(c){}void display() const{cout << "Triangle";}//海伦公式计算三角形的面积double area() const{double p = (_a + _b + _c) / 2;return  sqrt(p * (p - _a) * (p - _b) * (p - _c));}  
private:double _a;double _b;double _c;
};

7、抽象类

一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数,该类便为抽象类。一个抽象类只能作为基类来派生新类,不能创建抽象类的对象。
和普通的虚函数不同,在派生类中一般要对基类中纯虚函数进行重定义。如果该派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。这说明只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类才不再是抽象类。


除此以外,还有另外一种形式的抽象类。对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中作为其对象成员都不能创建该类的对象,但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类。

#include<iostream>
using namespace std;
class Base
{
protected:Base(long base): _base(base){cout << "Base()" << endl;}
protected:long _base;
};
class Derived: public Base
{
public:Derived(long base, long derived): Base(base), _derived(derived){cout << "Derived(long, long)" << endl;}void print() const{cout << "_base:" << _base<< ", _derived:" << _derived << endl;}
private:long _derived;
};
void test()
{Base base(1);//errorDerived derived(1, 2);
}

8、虚析构函数

虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。

class Base
{
public:Base(const char *pbase): _pbase(new char[strlen(pbase) + 1]()){cout << "Base(const char *)" << endl;strcpy(_pbase, pbase);}/*virtual*/~Base(){if(_pbase){delete [] _pbase;_pbase = nullptr;}cout << "~Base()" << endl;}private:char *_pbase;
};
class Derived
: public Base
{
public:Derived(const char *pbase, const char *pderived): Base(pbase), _pderived(new char[strlen(pderived) + 1]()){cout << "Derived(const char *, const char *)" << endl;strcpy(_pderived, pderived);}~Derived(){cout << "~Derived()" << endl;if(_pderived){delete [] _pderived;_pderived = nullptr;}}
private:char *_pderived;
};
void test()
{Base *pbase = new Derived("hello", "wuhan");pbase->print();delete pbase;
}

如上,在例子中,如果基类Base的析构函数没有设置为虚函数, 则在执行delete pbase语句时,不会调用派生类Derived的析构函数,这样就会造成内存泄漏。此时,将基类Base的析构函数设置为虚函数,就可以解决该问题。
如果有一个基类的指针指向派生类的对象,并且想通过该指针delete派生类对象,系统将只会执行基类的析构函数,而不会执行派生类的析构函数。为避免这种情况的发生,往往把基类的析构函数声明为虚的,此时,系统将先执行派生类对象的析构函数,然后再执行基类的析构函数。
如果基类的析构函数声明为虚的,派生类的析构函数也将自动成为虚析构函数,无论派生类析构函数声明中是否加virtual关键字。

9、重载、隐藏、覆盖

重载:在同一个作用域中,函数的名字相同,但是参数列表不一样(包括参数的个数、参数类型、参数顺序)
重定义(重写、覆盖):发生在基类与派生类中,必须是虚函数函数名字相同参数列表也相同
隐藏:发生在基类与派生类中,派生中的函数与基类中的函数名字相同(至于是不是虚函数,至于参数列表是不是一样的没有关系),派生类的数据成员也可以隐藏基类中的同名数据成员。

//针对于隐藏的例子
class Base
{
public:Base(int m): _member(m){cout << "Base(int)" << endl;}void func(int x){cout << "Base::func(int)" << endl;}~Base(){cout << "~Base()" << endl;}
protected:int _member;
};
class Derived
: public Base
{
public:Derived(int m1, int m2): Base(m1), _memeber(m2){cout << "Derived(int, int)" << endl;}void func(int *){
cout << "_member: " << _member << endl;cout << "Derived::func(int*)" << endl;}~Derived(){cout << "~Derived()" << endl;}
private:int _member;
};

10、测试虚表的存在

我们已经知道虚表的存在,但之前都是理论的说法,我们是否可以通过程序来验证呢?答案是肯定的。接下来我们看看下面的例子:

#include<iostream>
using namespace std;
class Base
{
public:Base(long data1) : _data1(data1){}virtualvoid func1(){cout << "Base::func1()" << endl;}virtualvoid func2(){cout << "Base::func2()" << endl;}virtualvoid func3(){cout << "Base::func3()" << endl;}
protected:long _data1;
};
class Derived: public Base
{
public:Derived(long data1, long data2): Base(data1), _data2(data2){}virtualvoid func1(){cout << "Derived::func1()" << endl;}virtualvoid func2(){cout << "Derived::func2()" << endl;}
private:long _data2;
};

11、带虚函数的多基派生

#include <iostream>using std::cout;
using std::endl;class A
{
public:virtualvoid a(){cout << "virtual void A::a()" << endl;}virtualvoid b(){cout << "virtual void A::b()" << endl;}virtualvoid c(){cout << "virtual void A::c()" << endl;}
};class B
{
public:virtualvoid a(){cout << "virtual void B::a()" << endl;}virtualvoid b(){cout << "virtual void B::b()" << endl;}void c(){cout << "virtual void B::c()" << endl;}void d(){cout << "virtual void B::d()" << endl;}
};class C: public A, public B
{
public:virtualvoid a(){cout << "virtual void C::a()" << endl;}void c(){cout << "virtual void C::c()" << endl;}void d(){cout << "virtual void C::d()" << endl;}
};class D: public C
{
public:void c(){cout << "D::c()" << endl;}
};void test()
{cout << "sizeof(A) = " << sizeof(A) << endl;//cout << "sizeof(B) = " << sizeof(B) << endl;//cout << "sizeof(C) = " << sizeof(C) << endl << endl;//C c;printf("&c = %p\n", &c);cout << endl;A *pa = &c;printf("pa = %p\n", pa);pa->a();//C::a()pa->b();//A::b()pa->c();//C::c()cout << endl;B *pb = &c;printf("pb = %p\n", pb);pb->a();//C::a()pb->b();//B::b()pb->c();//B::c()pb->d();//B::d()cout << endl;C *pc = &c;printf("pc = %p\n", pc);pc->a();//C::a()/* pc->b();//二义性 */pc->A::b();pc->B::b();pc->c();//C::c()pc->d();//隐藏C::d()cout << endl;D d;pc = &d;pc->c();//D::c多态,虚函数}int main()
{test();return 0;
}

结论:多重继承(带虚函数)
//1. 每个基类都有自己的虚函数表
//2. 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中
//3. 内存布局中,其基类的布局按照基类被声明时的顺序进行排列

//4. 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是
// 真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的
// 对应的虚函数的地址,而只是一条跳转指令

12、虚拟继承

在虚函数之前,首先问大家一个问题:从语义上来讲,为什么动态多态和虚继承都使用virtual关键字来表示?
virtual在词典中的解释有两种:
1. Existing or resulting in essence or effect though not in actual fact, form, or name.实质上的,实际上的:虽然没有实际的事实、形式或名义,但在实际上或效果上存在或产生的;
2. Existing in the mind, especially as a product of the imagination. Used in literary criticism of text. 虚的,内心的:在头脑中存在的,尤指意想的产物,用于文学批评中。
C++中的virtual关键字采用第一个定义,即被virtual所修饰的事物或现象在本质上是存在的,但是没有直观的形式表现,无法直接描述或定义,需要通过其他的间接方式或手段才能够体现出其实际上的效果。关键就在于存在、间接和共享这三种特征:

1.虚函数是存在的
2.虚函数必须要通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数
3.共享性表现在基类会共享被派生类重定义后的虚函数

虚拟继承又是如何表现这三种特征的呢?

1.存在即表示虚继承体系和虚基类确实存在
2.间接性表现在当访问虚基类的成员时同样也必须通过某种间接机制来完成(通过虚基表来完成)
3.共享性表现在虚基类会在虚继承体系中被共享,而不会出现多份拷贝

虚拟继承是指在继承定义中包含了virtual关键字的继承关系。虚基类是指在虚继承体系中的通过virtual继承而来的基类。语法格式如下:

class Baseclass;
class Subclass
: public/private/protected virtual Baseclass
{
public:  //...
private: //...
protected://...
};
//其中Baseclass称之为Subclass的虚基类, 而不是说Baseclass就是虚基类

接下来,我们看看例子:

#include <iostream>using std::cout;
using std::endl;class A
{
public:A(): _ia(0){cout << "A()" << endl;}A(int ia): _ia(ia){cout << "A(int)" << endl;}~A(){cout << "~A()" << endl;}
protected:int _ia;
};class B: virtual public A
{
public:B(): A(), _ib(0){cout << "B()" << endl;}B(int ia, int ib): A(ia), _ib(ib){cout << "B(int, int)" << endl;}~B(){cout << "~B()" << endl;}
protected:int _ib;
};class C: public B
{
public:C(int ia, int ib, int ic): A(ia)//显示将虚基类A的数据成员进程初始化, B(ia, ib), _ic(ic){cout << "C(int, int, int)" << endl;}void show() const{cout << "_ia = " << _ia << endl<< "_ib = " << _ib << endl<< "_ic = " << _ic << endl;}~C(){cout << "~C()" << endl;}
private:int _ic;
};int main(int argc, char **argv)
{C c(1, 2, 3);c.show();return 0;
}

// 结论一:单个虚继承,不带虚函数
// 虚继承与继承的区别
// 1. 多了一个虚基指针
// 2. 虚基类位于派生类存储空间的最末尾

// 结论二:单个虚继承,带虚函数

// 1. 如果派生类没有自己的虚函数,此时派生类对象不会产生虚函数指针
// 2. 如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针,
//   并且该虚函数指针位于派生类对象存储空间的开始位置

12.1、虚拟继承时派生类对象的构造和析构

在普通的继承体系中,比如A派生出B,B派生出C,则创建C对象时,在C类构造函数的初始化列表中调用B类构造函数,然后在B类构造函数初始化列表中调用A类的构造函数,即可完成对象的创建操作。但在虚拟继承中,则有所不同。

class A
{
public:
A(){cout << "A()" << endl;}A(int ia): _ia(ia){cout << "A(int)" << endl;}
void f(){cout << "A::f()" << endl;}
protected:
int _ia;
};
class B
: virtual public A
{
public:
B(){cout << "B()" << endl;}B(int ia, int ib): A(ia), _ib(ib){cout << "B(int,int)" << endl;}
protected:
int _ib;
};
class C
: public B
{
public:C(int ia, int ib, int ic): B(ia, ib), _ic(ic){cout << "C(int,int,int)" << endl;
}void show() const{cout << "_ia: " << _ia << endl<< "_ib: " << _ib << endl<< "_ic: " << _ic << endl;}
private:int _ic;
};
void test()
{C c(10, 20, 30);c.show();
}

从最终的打印结果来看,在创建对象c的过程中,我们看到C带三个参数的构造函数执行了,同时B带两个参数的构造函数也执行了,但A带一个参数的构造函数没有执行,而是执行了A的默认构造函数。这与我们的预期是有差别的。如果想要得到预期的结果,我们还需要在C的构造函数初始化列表最后,显式调用A的相应构造函数。那为什么需要这样做呢?
在 C++ 中,如果继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。即我们需要显式调用虚基类的构造函数来完成初始化,如果不显式调用,则编译器会调用虚基类的缺省构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。如果虚基类中没有定义的缺省构造函数,则会编译错误。因为如果不这样做,虚基类部分会在存在的多个继承链上被多次初始化。很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们要保证它们能够得到正确的初始化。这种情况在菱形继承中非常明显, 我们接下来看看这种情况

#include <iostream>using std::cout;
using std::endl;class A
{
public:A(): _ia(0){cout << "A()" << endl;}A(int ia): _ia(ia){cout << "A(int)" << endl;}~A(){cout << "~A()" << endl;}
protected:int _ia;
};class B: virtual public A
{
public:B(): A(), _ib(0){cout << "B()" << endl;}B(int ia, int ib): A(ia), _ib(ib){cout << "B(int, int)" << endl;}~B(){cout << "~B()" << endl;}
protected:int _ib;
};class C: public B
{
public:C(int ia, int ib, int ic): A(ia)//显示将虚基类A的数据成员进程初始化, B(ia, ib), _ic(ic){cout << "C(int, int, int)" << endl;}void show() const{cout << "_ia = " << _ia << endl<< "_ib = " << _ib << endl<< "_ic = " << _ic << endl;}~C(){cout << "~C()" << endl;}
private:int _ic;
};int main(int argc, char **argv)
{C c(1, 2, 3);c.show();return 0;
}

12.2、菱形继承

/虚基指针所指向的虚基表的内容
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移


class A
{
public:int m_a = 1;
};
class B :virtual public A
{
public:int m_b = 2;
};
class C :virtual public A
{
public:int m_c = 3;
};
class D :public B, public C
{
public:int m_d = 4;
};
void main()
{D d;d.m_d = 40;d.m_c = 30;d.m_b = 20;d.m_a = 100;//  让B C虚拟继承 A 加关键字virtual
}

如果是在若干类层次中,从虚基类直接或间接派生出来的派生类的构造函数初始化列表均有对该虚基类构造函数的调用,那么创建一个派生类对象的时候只有该派生类列出的虚基类的构造函数被调用,其他类列出的将被忽略,这样就保证虚基类的唯一副本只被初始化一次。即虚基类的构造函数只被执行一次。
对于虚继承的派生类对象的析构,析构函数的调用顺序为:
1.先调用派生类的析构函数;
2.然后调用派生类中成员对象的析构函数;
3.再调用普通基类的析构函数;
4.最后调用虚基类 的析构函数。

13、效率分析

通过以上的学习我们可以知道,多重继承和虚拟继承对象模型较单一继承复杂的对象模型,造成了成员访问低效率,表现在两个方面:对象构造时vptr的多次设定,以及this指针的调整。对于多种继承情况的效率比较如下:

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

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

相关文章

串变换dfs

分析&#xff1a; DFS,注意判断是否遍历结束&#xff0c;返回false 代码示例&#xff1a; #include<bits/stdc.h> using namespace std; int n,k; string s,t; struct {int op;int x;int y; }cha[10]; int vis[10]; bool dfs(int dep){if(st)return true;if(depk1)retu…

Qt教程 — 3.3 深入了解Qt 控件:Input Widgets部件(2)

目录 1 Input Widgets简介 2 如何使用Input Widgets部件 2.1 QSpinBox组件-窗口背景不透明调节器 2.2 DoubleSpinBox 组件-来调节程序窗口的整体大小 2.3 QTimeEdit、QDateEdit、QDateTimeEdit组件-编辑日期和时间的小部件 Input Widgets部件部件较多&#xff0c;将分为三…

滑动窗口最大值(leetcode hot100)

给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a;nums [1,3,-1,-3,5,3,6,7], k 3 输…

C++:菱形继承与虚继承

看下面这个示例代码 class A{ public: int num10; A(){cout<<"A构造"<<endl;} virtual void fun(){cout<<"A虚函数"<<endl;} };class B:public A{ public: B(){cout<<"B构造"<<endl;} void fun(){cout<…

基于Matlab的车牌识别算法,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

高可用系统有哪些设计原则

1.降级 主动降级&#xff1a;开关推送 被动降级&#xff1a;超时降级 异常降级 失败率 熔断保护 多级降级2.限流 nginx的limit模块 gateway redisLua 业务层限流 本地限流 gua 分布式限流 sentinel 3.弹性计算 弹性伸缩—K8Sdocker 主链路压力过大的时候可以将非主链路的机器给…

telnet命令使用

window启用telnet telnet命令连接服务端 启动netty服务端后&#xff0c;使用如下cmd命令连接服务端&#xff0c;按enter&#xff0c;将连接到netty服务端 再按CTRL ]&#xff0c;进入命令交互界面 输入 help&#xff0c;查看命令介绍 发送消息&#xff0c;再断开连接&…

一起学数据分析_2

写在前面&#xff1a;代码运行环境为jupyter&#xff0c;如果结果显示不出来的地方就加一个print()函数。 一、数据基本处理 缺失值处理&#xff1a; import numpy as np import pandas as pd#加载数据train.csv df pd.read_csv(train_chinese.csv) df.head()# 查看数据基本…

Python环境安装及Selenium引入

Python环境安装 环境下载 Download Python | Python.org 环境安装 需使用管理员身份运行 查看环境是否安装成功 python --version 如果未成功则检查环境变量配置 安装 Selenium 库 pip install selenium Selenium 可以模拟用户在浏览器中的操作&#xff0c;如点击按钮、填写…

Springboot 整合 Elasticsearch(五):使用RestHighLevelClient操作ES ②

&#x1f4c1; 前情提要&#xff1a; Springboot 整合 Elasticsearch&#xff08;三&#xff09;&#xff1a;使用RestHighLevelClient操作ES ① 目录 一、Springboot 整合 Elasticsearch 1、RestHighLevelClient API介绍 1.1、全查询 & 分页 & 排序 1.2、单条件查询…

接口幂等性问题和常见解决方案

接口幂等性问题和常见解决方案 1.什么是接口幂等性问题1.1 会产生接口幂等性的问题1.2 解决思路 2.接口幂等性的解决方案2.1 唯一索引解决方案2.2 乐观锁解决方案2.3 分布式锁解决方案2.4 Token解决方案(最优方案) 3 Token解决方案落地3.1 token获取、token校验3.2 自定义注解,…

java过滤器Filter相关知识点汇总

1.Filter概述 Servlet Filter又称Servlet过滤器&#xff0c;它是在Servlet2.3规范中定义的&#xff0c;能够对Servlet容器传给Web资源的request对象和response对象执行检查和修改。 Filter不是Servlet&#xff0c;不能直接访问&#xff0c;其本身也不能生成request对象和resp…

第十三届蓝桥杯(C/C++ 大学B组)

目录 试题 A: 九进制转十进制 试题 B: 顺子日期 试题 C: 刷题统计 试题 D: 修剪灌木 试题 E: X 进制减法 试题 F: 统计子矩阵 试题 G: 积木画 试题 H: 扫雷 试题 I: 李白打酒加强版 试题 J: 砍竹子 试题 A: 九进制转十进制 九进制正整数 ( 2022 )转换成十进制等于多…

Java后端面试经验分享,~纯分享

本文将从面试、工作、学习三个方面分享最近面试的一些心得以及以后发展的一些规划&#xff0c;仅供参考&#xff0c;哈哈&#xff0c;毕竟本人也很菜&#xff0c;因为菜才要多学习。一会儿也会分享两本Java面试题库&#xff08;题库是b站大学找的&#xff0c;一会儿我也会分享出…

开发知识点-python-Tornado框架

介绍 Tornado是一个基于Python语言的高性能Web框架和异步网络库&#xff0c;它专注于提供快速、可扩展和易于使用的网络服务。由于其出色的性能和灵活的设计&#xff0c;Tornado被广泛用于构建高性能的Web应用程序、实时Web服务、长连接的实时通信以及网络爬虫等领域。 Torna…

java组合模式揭秘:如何构建可扩展的树形结构

组合模式&#xff08;Composite Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许将对象组合成树形结构以表示整体/部分层次结构。组合模式使得客户端可以统一对待单个对象和组合对象&#xff0c;从而使得客户端可以处理更复杂的结构。 组合模式的主要组成部分包括&…

spring boot3登录开发-微信小程序用户登录设计与实现

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 &#x1f30a;山高路远&#xff0c;行路漫漫&#xff0c;终有归途 目录 写在前面 登录流程 流程解析 具体实现 相关代码 说明 服务端 小程序端 写在最后 写在前面 本文介绍了springb…

20双体系Java学习之数组的toString类

Arrays.toString ★小贴士 数组内容字符串表示形式由数组的元素列表组成&#xff0c;括在方括号&#xff08;"[]"&#xff09;中。相邻元素用字符 ", "&#xff08;逗号加空格&#xff09;分隔。 使用toString()方法可方便地输出数组的内容&#xff0c;避免…

前端跨平台开发框架:简化多端开发的利器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

代码随想录 贪心算法-难度题目-其他题目

目录 53.最大子数组和 134.加油站 968.监控二叉树 53.最大子数组和 53. 最大子数组和 中等 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个…