C++之多态【详细总结】
前言
想必大家都知道面向对象的三大特征:封装,继承,多态。封装的本质是:对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承的本质是为了复用,复用基类的数据成员和方法。对于多态而言,多态的实现要求必须是公有继承作为前提,这也是我们的学习顺序。那么这篇文章就带领大家一起学习多态!
目录
前言
Ⅰ.多态的概念
Ⅱ.多态的定义及实现
Ⅲ.抽象类
Ⅳ.多态的原理
Ⅴ.单继承和多继承关系的虚函数表
Ⅵ.继承和多态常见的面试问题
Ⅰ.多态的概念
多态的概念通俗来说:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
下面我们通过一个例子进行理解,同为动物的小猫咪和小狗发出的不同声音。
☆有一个基类(Animal),它有两个派生类(Cat,Dog),在Animal中有个方法(Say),Cat和Dog都是继承于Animal。当Cat调用Say时会发出“喵~喵~喵~喵~”的声音,当Dog调用Say时会发出“汪~汪~汪~汪~”的声音,这就是多态的实现。

☆再简单的举一个例子:张三和李四都是学生,他们都想报考法律专业的学校,张三成绩比较优异就报了一个知名法律大学,李四的成绩就不是非常的拔尖,就随便报了一个学校。张三和李四去完成报考的行为,但是他们完成时就就产生不同的状态。
Ⅱ.多态的定义及实现
多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Cat和Dog继承了Animal。Animal对象Say-动物语言,Cat对象Say-喵,Dog对象Say-汪。
那么在继承中构成多态的两个条件:
☆必须通过基类的指针或者引用调用虚函数
☆被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Animal {
public:virtual void Say(){cout << " 动物语言 " << endl; }
};
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Cat : public Animal {
public:virtual void Say() {cout << "喵~喵~喵~喵~" << endl; }
};class Dog : public Animal {
public:virtual void Say() {cout << "汪~汪~汪~汪~" << endl; }
};

注意:在重写基类虚函数时,派生类的虚函数在不加 virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
/*virtual*/ void Say() {cout << "汪~汪~汪~汪~" << endl; }
当任何一个条件破坏,就会变成隐藏。


指针调用虚函数

注意:普通调用跟调用对象有关,多态调用是跟(指针/引用)指向的对象有关。
协变(基类与派生类虚函数返回值类型不同)
子类重写基父类函数时,子类中有一个跟父类完全相同的虚函数,与父类虚函数返回值类型不同,但是要求返回值必须是一个父子类关系的指针或引用,称为协变。
class A{
};class B : public A {
};class Person {
public:virtual A* f() { return new A; }
};class Student : public Person {
public:virtual B* f() { return new B; }
};
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:virtual ~Person() {cout << "~Person()" << endl; }
};class Student : public Person {
public:/virtual ~Student() {cout << "~Student()" << endl; }
};void Test()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;
}
只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。加 virtual关键字。

如果未加virtual关键字。通过观察发现,发生了内存泄漏。原本还需要释放Student类,但是这里没有。因为delete是使用指针调用析构-operator delete(ptr),这里未加virtual关键字就是普通调用,普通调用和对象类型有关,普通调用会发生隐藏关系,是什么类型就调用什么析构函数,所以就会调用两次Person。

C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
☆final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

实现一个不被继承的类
♢构造私有,c++98抽象类
class A
{
private:A(){}
};class Benz :public A
{};
♢定义时加 final,c++11
class A final
{
private:A(){}
};class Benz :public A
{};
☆override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:virtual void Drive(){}
};class Benz :public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

重载、覆盖(重写)、隐藏(重定义)的对比

Ⅲ.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public://纯虚函数virtual void Drive() = 0;
};class Benz :public Car
{
public://对纯虚函数进行重写virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};void Test()
{//Car p;//出错Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
为了更好的理解接口继承,下面我们通过试题进行深究。以下程序输出结果是什么()?
class A
{
public:virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }virtual void test(){ func(); }
};class B : public A
{
public:void func(int val = 0){ std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B*p = new B;p->test();return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
参考答案:B: B->1,过程如图下

Ⅳ.多态的原理
虚函数表
这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:char _c = 'a';int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}
参考答案:12,过程如下图

通过观察测试我们发现p对象是12bytes,除了_b,_c成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析。
针对上面的代码我们做出以下改造,我们增加一个派生类Derive去继承Base ,Derive中重写Func1 ,Base再增加一个虚函数Func2和一个普通函数Func3。
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}

通过观察和测试,我们发现了以下几点问题:
☆ 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
☆基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
☆另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,因为不是虚函数,所以不会放进虚表。
☆ 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
☆总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
☆这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多时候都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。
多态的原理
相信大家经过上面的理解,对于多态原理的面纱几乎已经解开了,对于多态的理解最重要是理解虚指针(_vfptr),虚函数(父子类被virtual关键字声明的函数),虚表(虚函数的类都有一个一维的虚函数表)。父子类通过继承关系的虚函数,通过虚指针指向虚表,当发生切片后,当子类虚函数重写了父类的虚函数,这时就会发生覆盖。
下面主要是对覆盖在进行深度理解,最后再通过汇编代码的角度来理解虚表,虚函数,虚函数指针。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Jack;Func(Jack);Student Tom;Func(Tom);return 0;
}

通过汇编的角度理解虚表,我们发现在父类中调用自己函数,因为是普通调用,不管是通过虚函数指针调用虚表,还是直接调用虚表,都是在编译时已经从符号表确认了函数的地址,直接call。
void Func(Person* p)
{p->BuyTicket();
}int main()
{Person Jack;Func(&Jack);Jack.BuyTicket();return 0;
}
在汇编代码中,声明成员Jack,Jack调用Func函数,相当于把Jack对象头4个字节(虚表指针)移动到了edx,Jack直接调用类中的BuyTicket函数,相当于把虚表中的头4字节存的虚函数指针移动到了eax 。
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是Jack对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把Jack对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA call eax
001940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是Jack是对象,不满足多态的条件,所以这里是地址普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}
当满足多态调用在编译时确定的,是运行起来以后到对象的中取找的。
void Func(Person* p)
{p->BuyTicket();
}int main()
{Student Tom;Func(&Tom);Tom.BuyTicket();return 0;
}
void Func(Person* p)
00DA458E mov eax,dword ptr [p]
void Func(Person* p)
00DA4591 mov edx,dword ptr [eax]
00DA4593 mov esi,esp
00DA4595 mov ecx,dword ptr [p]
00DA4598 mov eax,dword ptr [edx]
00DA459A call eax
00DA459C cmp esi,esp
动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
不管虚函数的虚指针是指向父类虚表,还是子类的虚表其实都是一样的,该有虚表该有成员都是一样的,不一样的是子函数的虚表中的虚函数(可能发生了重写/覆盖)。
int main()
{//普通调用--编译时/静态 绑定Student Tom;Func(&Tom);//多态调用--运行时/动态 绑定Person Jack;Func(&Jack);return 0;
}

总结:多态的原理实质就是,虚表是提前写好的,对象指向谁就调用谁的虚表,多态调用就是依靠虚表一系列的动作。指向父类调用父类的虚函数,指向子类调用子类的虚函数(可能覆盖),运行起来后去虚表中找。
虚表的存放区域?
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Test()
{int a = 0;cout << "栈" << &a << endl;int* p1 = new int;cout << "堆" << p1 << endl;const char* str = "hello world";cout << "代码段/常量区" << (void*)str << endl;static int b = 0;cout << "静态区/数据段" << &b << endl;Student s;cout << "虚表:" << (void*)*((int*)&s1) << endl;
}
通过观察,我们发现代码段与虚表的地址是挨的最近,所以我们得出结论虚表是存放在代码段。
同一个类下,虚表是用一个。
void Test()
{Student s1;cout << "虚表1:" << (void*)*((int*)&s1) << endl;Student s2;cout << "虚表2:" << (void*)*((int*)&s2) << endl;
}

理解: (void*)*((int*)&s1)

Ⅴ.单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类 的虚表模型前面我们已经看过了,没什么需要特别研究的
单继承中的虚函数表
class Base {public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }private:int a;
};class Derive :public Base {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }private:int b;
};

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印 出虚表中的函数。
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr
●先取b的地址,强转成一个int*的指针
●再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
●再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
●虚表指针传递给PrintVTable进行打印虚表
●需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

注意:
☆平时使用 typedef定义一个别名是直接加在类型后面,例如:typedef long double REAL;这里 typedef void(*) ()VFPTR是错误的,函数指针的语法规定是将VFPTR别名放入函数指针类型()中
☆在传参时,PrintVTable中参数是函数指针的数组,就不能只传对象4byte的地址,那么就应该传函数指针(VFPTR*)(*(int*)&b)。
//函数指针:通过地址调用函数
typedef void(*VFPTR) ();//函数指针的数组:虚表地址是连续的,通首地址连续查看虚函数地址
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Base b;Derive d;VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}
通过打印发现func3和func4是在对象d的虚表中,d对象的func1函数也在虚表中覆盖了父类的funcl函数。下图也通过不同颜色区分出不同对象对应的虚拟地址空间。

为了能够同时测试32位和64位下的虚表地址,32位下是4byte,64为下是8byte,又该如何呢?
这里就通过传参时,传入二级指针(void**),当32位下传int*时解引用是4byte,但是在64下传int*时解引用是8byte,显然这时候是行不通的,但是传二级指针不管是32位还是64位下解引用都是地址,地址不管怎么样都是4byte,这里也可以写成(int**),(double**)等等都是可以的,但通常情况下是写成(void**)。
//32位和64位同时可以打印虚表地址
VFPTR* vTableb = (VFPTR*)(*(void**)&b);PrintVTable(vTableb);
多继承中的虚函数表
在多继承下,子类会继承两个父类的虚函数表。

子类继承了两个父类的虚函数表,而且都会与两个父类的虚函数进行重写。
注意:为了打印出第二个父类虚函数表的地址,需要取第一个父类地址的偏移量例如:(char*)&d + sizeof(Base1)或者 Base2* ptr2 = &d;
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }private:int b2;
};class Derive : public Base1, public Base2 {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int d1;
};typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Base1 b1;Base2 b2;PrintVTable((VFPTR*)(*(int*)&b1));PrintVTable((VFPTR*)(*(int*)&b2));Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//PrintVTable(vTableb2);Base2* ptr2 = &d;PrintVTable((VFPTR*)(*(int*)&ptr2));return 0;
}
最后通打印我们发现,子类会与两个父类发生重写,但是子类未重写的虚函数放在第一个继承父类部分的虚函数表中。

菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗,在实际中也很少用。对于学习知识来说是可以见见的,过于深究会头昏脑胀。
☆菱形继承
菱形继承就是重复继承,在继承的时候不加virtual关键字。

class Base {
public:virtual void func() { cout << "Base1::func" << endl; }
private:int b;
};class Base1 : public Base {
public:virtual void func1() { cout << "Base1::func1" << endl; }private:int b1;
};class Base2 : public Base{
public:virtual void func2() { cout << "Base2::func2" << endl; }private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func2() { cout << "Derive::func3" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int d1;
};

☆菱形虚拟继承
为了解决菱形继承的二义性和数据冗余问题,用过加virtual关键字形成菱形继承。由于前面的“菱形继承”中的类的内部数据和接口都是完全一样的,为了解决冗余,只是我们采用了虚拟继承:其省略后的源码如下所示:
class Base {……};
class Base1 : virtual public B{……};
class Base2: virtual public B{……};
class Derive : public B1, public B2{ …… };
菱形虚拟继承其实结构与菱形继承是一样的,不一样是的加了虚拟后,会单独形成一个虚基表进行记录变量的偏移量,这里只需要明白的是虚函数表和虚基表是不同的,要讨论的结构如下:


Ⅵ.继承和多态常见的面试问题
概念查考
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关, 而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封 装性的表现
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
8. 下面程序输出结果是什么? ()
#include<iostream>
using namespace std;class A{
public:A(char *s) { cout<<s<<endl; }~A(){}
};class B:virtual public A
{
public:B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};class C:virtual public A
{
public:C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};class D:public B,public C
{
public:D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1){ cout<<s4<<endl;}
};int main() {D *p=new D("class A","class B","class C","class D");delete p;return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
9. 多继承中指针偏移问题?下面说法正确的是( )
class 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;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
10. 多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public: int _b1; };class Base2 { public: int _b2; };class Derive : public Base2, public Base1 { public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
A:p1 == p2 == p3 B:p1 > (p2 = p3) C:p1 == p3 != p2 D:p1 != p2 != p3

问答题
1. 什么是多态?
答:多态分为两种,一种是静态的多态是静态绑定,在程序编译期间确定了程序的行为,函数重载;一种动态的多态是动态绑定,通过继承下虚函数重写,父类的指针指向在父类的虚函数进行动态绑定,运行起来去虚表中找到对应的虚函数,指向子类调用子类。
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:函数重载发生在同一作用域并且只需要函数名相同;重写是子类的虚函数对父类的虚函数进行覆盖,在不同的作用域且在继承关系下函数需要三同(参数,函数名,返回值)和父类函数被关键字virtual修饰 ;重定义是子类继承父类下,在不同作用域,满足函数名相同就构成隐藏。
3. 多态的实现原理?
答:通过继承下虚函数重写,父类的指针指向在父类的虚函数进行动态绑定,运行起来去虚表中找到对应的虚函数,指向子类调用子类。
4. inline函数可以是虚函数吗?
答:不能的,但是在语法上是可以的,只是编译器会忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。当不构成多态是可以具有内联属性,构成多态不具有内联属性。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的构造函数初始化列表中初始化【虚表指针】,一般情况 下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:菱形继承会产生二义性和数据冗余。虚基表指针在本类中找到虚基表,本类的虚基表通过偏移量计算找到该虚基类中变量的值。注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?
答:在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;目的是为了重写,达成 多态,继承的是接口;抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
☺ [ 作者 ] includeevey
📃 [ 日期 ] 2023/2/1
📜 [ 声明 ] 到这里就该说再见了,若本文有错误和不准确之处,恳望读者批评指正!
有则改之无则加勉!若认为文章写的不错,一键三连加关注!

相关文章:
C++之多态【详细总结】
前言 想必大家都知道面向对象的三大特征:封装,继承,多态。封装的本质是:对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承的本质是为了复用&#x…...
ThingsBoard-RPC
1、使用 RPC 功能 ThingsBoard 允许您将远程过程调用 (RPC) 从服务器端应用程序发送到设备,反之亦然。基本上,此功能允许您向/从设备发送命令并接收命令执行的结果。本指南涵盖 ThingsBoard RPC 功能。阅读本指南后,您将熟悉以下主题: RPC 类型;基本 RPC 用例;RPC 客户端…...
java分治算法
分治算法介绍 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或 相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题 的解的合并。这个技…...
【Flutter】【Unity】使用 Flutter + Unity 构建(AR 体验工具包)
使用 Flutter Unity 构建(AR 体验工具包)【翻译】 原文:https://medium.com/potato/building-with-flutter-unity-ar-experience-toolkit-6aaf17dbb725 由于屡获殊荣的独立动画工作室 Aardman 与讲故事的风险投资公司 Fictioneers&#x…...
MC0108白给-MC0109新河妇荡杯
MC0108白给 小码哥和小码妹在玩一个游戏,初始小码哥拥有 x的金钱,小码妹拥有 y的金钱。 虽然他们不在同一个队伍中,但他们仍然可以通过游戏的货币系统进行交易,通过互相帮助以达到共赢的目的。具体来说,在每一回合&a…...
求职(JAVA程序员的面试自我介绍)
背景 在找工作的过程中,在面试的环节,大多数面试官首先都会叫你自我介绍一下。一般是3到5分钟内。不过经过我面试的无数的公司还有曾经也面试过大多数的求职者。国内很多的程序员面试都极其不专业。有一种很随心所欲的感觉。所以经常遇到求职者吐槽遇到了…...
金三银四季节前端面试题复习来了
vue3和vue2的区别有哪些 Diff算法的改进Tree Sharing优化主要的API双向绑定改为es6的proxy原生支持tscomposition API移除令人头疼的this 说说CSS选择器以及这些选择器的优先级 !important 内联样式(1000) ID选择器(0100) 类选…...
【C/C++基础练习题】简单语法使用练习题
🍉内容专栏:【C/C要打好基础啊】 🍉本文内容:简单语法使用练习题(复习之前写过的实验报告) 🍉本文作者:Melon西西 🍉发布时间 :2023.2.10 目录 1、输入三个数…...
堆排序
章节目录:一、相关概述1.1 基本介绍1.2 排序思想二、基本应用2.1 步骤说明2.2 代码示例三、结束语一、相关概述 1.1 基本介绍 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序。它的最坏最好平均时间复杂度均为 O(nlogn)&#x…...
PLC是什么?PLC相关知识小科普
欢迎各位来到东用知识小课堂1.PLC是什么:●PLC就是可编程控制器,它应用于工业环境,必须具有很强的抗干扰能力、广泛的适应能力和应用范围。●PLC是“数字运算操作的电子系统”,也是一种计算机,它是“专为在工业环境下应…...
BERT简介
BERT: BERT预训练模型训练步骤: 使用Masked LM方式将语料库中的某一部分的词语掩盖住,模型通过上下文预测被掩盖的信息,从而训练出初步的语言模型在语料库中选出连续的上下语句,并使用Tranformer模块识别语句的连续性通…...
OpenStack云平台搭建(5) | 部署Nova
目录 1、登录数据库配置 2、安装nova 3、计算节点上安装nova 4、在controller节点上 nova组件是用来建虚拟机的(功能:负责响应虚拟机创建请求、调度、销毁云主机) nova主要组成: (1).nova api service------安装在controlle…...
【重要】2023年上半年有三AI新课程规划出炉,讲师持续招募中!
2023年正式起航,想必大家都已经完全投入到了工作状态中,有三AI平台今年将在已有内容的基础上,继续进行新课程开发,本次我们来介绍今年上半年的课程计划,以及新讲师招募计划。2023年新上线课程我们平台的课程当前分为两…...
【正点原子FPGA连载】第八章UART串口中断实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南
1)实验平台:正点原子MPSoC开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id692450874670 3)全套实验源码手册视频下载地址: http://www.openedv.com/thread-340252-1-1.html 第八章UART串口中…...
【云原生】解读Kubernetes三层网络方案
在上一篇文章中,我以网桥类型的 Flannel 插件为例,为你讲解了 Kubernetes 里容器网络和 CNI 插件的主要工作原理。不过,除了这种模式之外,还有一种纯三层(Pure Layer 3)网络方案非常值得你注意。其中的典型…...
elasticsearch8.3.2搭建部署
Elasticsearch8.3.2搭建部署详细步骤 0.过往文章 ES-6文章: Elasticsearch6.6.0部署、原理和使用介绍: https://blog.csdn.net/wt334502157/article/details/119515730 ES-7文章: Elasticsearch7.6.1部署、原理和使用介绍: https://blog.csdn.net/wt…...
MySQL_InnoDB引擎
InnoDB引擎 逻辑存储结构 表空间(ibd文件),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。 段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollba…...
json-server使用
文章目录json-server使用简介安装json-server启动json-server操作创建数据库查询数据增加数据删除数据修改数据putpatch配置静态资源静态资源首页资源json-server使用 简介 github地址 安装json-server npm install -g json-server启动json-server json-server --watch db…...
实现mint操作(参考pancake)
区块链发展越来越好,nft已经火了很久,今天写一下如何用js、web3js、调用合约,实现mint nft。简单的调用://引入一些依赖 (根据需要,有一些是其他功能的) import useActiveWeb3React from ./web3…...
Linux进程信号
目录 一、认识信号 1.1 生活角度的信号 1.2 技术角度的信号 1.3 信号的发送与记录 1.4 常见信号处理方式 二、产生信号 2.1 通过终端按键产生信号(核心转储) 2.2 通过系统函数向进程发送信号 2.2.1 kill()函数 2.2.2 raise()函数 2.2.3 abort()函数 2.3 因软件条件…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...
听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
Go语言多线程问题
打印零与奇偶数(leetcode 1116) 方法1:使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...
