当前位置: 首页 > news >正文

低价车网站建设/公司优化是什么意思

低价车网站建设,公司优化是什么意思,淘宝联盟做的好的网站,重庆市安全建设工程信息前言 想必大家都知道面向对象的三大特征:封装,继承,多态。封装的本质是:对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承的本质是为了复用&#x…

前言

        想必大家都知道面向对象的三大特征:封装,继承,多态。封装的本质是:对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承的本质是为了复用,复用基类的数据成员和方法。对于多态而言,多态的实现要求必须是公有继承作为前提,这也是我们的学习顺序。那么这篇文章就带领大家一起学习多态!

目录

前言

Ⅰ.多态的概念

Ⅱ.多态的定义及实现

Ⅲ.抽象类

Ⅳ.多态的原理

Ⅴ.单继承和多继承关系的虚函数表

Ⅵ.继承和多态常见的面试问题


Ⅰ.多态的概念

多态的概念通俗来说:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

下面我们通过一个例子进行理解,同为动物的小猫咪和小狗发出的不同声音。

☆有一个基类(Animal),它有两个派生类(Cat,Dog),在Animal中有个方法(Say),Cat和Dog都是继承于Animal。当Cat调用Say时会发出“喵~喵~喵~喵~”的声音,当Dog调用Say时会发出“汪~汪~汪~汪~”的声音,这就是多态的实现。

☆再简单的举一个例子:张三和李四都是学生,他们都想报考法律专业的学校,张三成绩比较优异就报了一个知名法律大学,李四的成绩就不是非常的拔尖,就随便报了一个学校。张三和李四去完成报考的行为,但是他们完成时就就产生不同的状态。

Ⅱ.多态的定义及实现

多态的构成条件 

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Cat和Dog继承了AnimalAnimal对象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++之多态【详细总结】

前言 想必大家都知道面向对象的三大特征&#xff1a;封装&#xff0c;继承&#xff0c;多态。封装的本质是&#xff1a;对外暴露必要的接口&#xff0c;但内部的具体实现细节和部分的核心接口对外是不可见的&#xff0c;仅对外开放必要功能性接口。继承的本质是为了复用&#x…...

ThingsBoard-RPC

1、使用 RPC 功能 ThingsBoard 允许您将远程过程调用 (RPC) 从服务器端应用程序发送到设备,反之亦然。基本上,此功能允许您向/从设备发送命令并接收命令执行的结果。本指南涵盖 ThingsBoard RPC 功能。阅读本指南后,您将熟悉以下主题: RPC 类型;基本 RPC 用例;RPC 客户端…...

java分治算法

分治算法介绍 分治法是一种很重要的算法。字面上的解释是“分而治之”&#xff0c;就是把一个复杂的问题分成两个或更多的相同或 相似的子问题&#xff0c;再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解&#xff0c;原问题的解即子问题 的解的合并。这个技…...

【Flutter】【Unity】使用 Flutter + Unity 构建(AR 体验工具包)

使用 Flutter Unity 构建&#xff08;AR 体验工具包&#xff09;【翻译】 原文&#xff1a;https://medium.com/potato/building-with-flutter-unity-ar-experience-toolkit-6aaf17dbb725 由于屡获殊荣的独立动画工作室 Aardman 与讲故事的风险投资公司 Fictioneers&#x…...

MC0108白给-MC0109新河妇荡杯

MC0108白给 小码哥和小码妹在玩一个游戏&#xff0c;初始小码哥拥有 x的金钱&#xff0c;小码妹拥有 y的金钱。 虽然他们不在同一个队伍中&#xff0c;但他们仍然可以通过游戏的货币系统进行交易&#xff0c;通过互相帮助以达到共赢的目的。具体来说&#xff0c;在每一回合&a…...

求职(JAVA程序员的面试自我介绍)

背景 在找工作的过程中&#xff0c;在面试的环节&#xff0c;大多数面试官首先都会叫你自我介绍一下。一般是3到5分钟内。不过经过我面试的无数的公司还有曾经也面试过大多数的求职者。国内很多的程序员面试都极其不专业。有一种很随心所欲的感觉。所以经常遇到求职者吐槽遇到了…...

金三银四季节前端面试题复习来了

vue3和vue2的区别有哪些 Diff算法的改进Tree Sharing优化主要的API双向绑定改为es6的proxy原生支持tscomposition API移除令人头疼的this 说说CSS选择器以及这些选择器的优先级 !important 内联样式&#xff08;1000&#xff09; ID选择器&#xff08;0100&#xff09; 类选…...

【C/C++基础练习题】简单语法使用练习题

&#x1f349;内容专栏&#xff1a;【C/C要打好基础啊】 &#x1f349;本文内容&#xff1a;简单语法使用练习题&#xff08;复习之前写过的实验报告&#xff09; &#x1f349;本文作者&#xff1a;Melon西西 &#x1f349;发布时间 &#xff1a;2023.2.10 目录 1、输入三个数…...

堆排序

章节目录&#xff1a;一、相关概述1.1 基本介绍1.2 排序思想二、基本应用2.1 步骤说明2.2 代码示例三、结束语一、相关概述 1.1 基本介绍 堆排序是利用堆这种数据结构而设计的一种排序算法&#xff0c;堆排序是一种选择排序。它的最坏最好平均时间复杂度均为 O(nlogn)&#x…...

PLC是什么?PLC相关知识小科普

欢迎各位来到东用知识小课堂1.PLC是什么&#xff1a;●PLC就是可编程控制器&#xff0c;它应用于工业环境&#xff0c;必须具有很强的抗干扰能力、广泛的适应能力和应用范围。●PLC是“数字运算操作的电子系统”&#xff0c;也是一种计算机&#xff0c;它是“专为在工业环境下应…...

BERT简介

BERT&#xff1a; BERT预训练模型训练步骤&#xff1a; 使用Masked LM方式将语料库中的某一部分的词语掩盖住&#xff0c;模型通过上下文预测被掩盖的信息&#xff0c;从而训练出初步的语言模型在语料库中选出连续的上下语句&#xff0c;并使用Tranformer模块识别语句的连续性通…...

OpenStack云平台搭建(5) | 部署Nova

目录 1、登录数据库配置 2、安装nova 3、计算节点上安装nova 4、在controller节点上 nova组件是用来建虚拟机的&#xff08;功能&#xff1a;负责响应虚拟机创建请求、调度、销毁云主机&#xff09; nova主要组成&#xff1a; (1).nova api service------安装在controlle…...

【重要】2023年上半年有三AI新课程规划出炉,讲师持续招募中!

2023年正式起航&#xff0c;想必大家都已经完全投入到了工作状态中&#xff0c;有三AI平台今年将在已有内容的基础上&#xff0c;继续进行新课程开发&#xff0c;本次我们来介绍今年上半年的课程计划&#xff0c;以及新讲师招募计划。2023年新上线课程我们平台的课程当前分为两…...

【正点原子FPGA连载】第八章UART串口中断实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第八章UART串口中…...

【云原生】解读Kubernetes三层网络方案

在上一篇文章中&#xff0c;我以网桥类型的 Flannel 插件为例&#xff0c;为你讲解了 Kubernetes 里容器网络和 CNI 插件的主要工作原理。不过&#xff0c;除了这种模式之外&#xff0c;还有一种纯三层&#xff08;Pure Layer 3&#xff09;网络方案非常值得你注意。其中的典型…...

elasticsearch8.3.2搭建部署

Elasticsearch8.3.2搭建部署详细步骤 0.过往文章 ES-6文章&#xff1a; Elasticsearch6.6.0部署、原理和使用介绍: https://blog.csdn.net/wt334502157/article/details/119515730 ES-7文章&#xff1a; Elasticsearch7.6.1部署、原理和使用介绍: https://blog.csdn.net/wt…...

MySQL_InnoDB引擎

InnoDB引擎 逻辑存储结构 表空间&#xff08;ibd文件&#xff09;&#xff0c;一个mysql实例可以对应多个表空间&#xff0c;用于存储记录、索引等数据。 段&#xff0c;分为数据段&#xff08;Leaf node segment&#xff09;、索引段(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)

区块链发展越来越好&#xff0c;nft已经火了很久&#xff0c;今天写一下如何用js、web3js、调用合约&#xff0c;实现mint nft。简单的调用&#xff1a;//引入一些依赖 &#xff08;根据需要&#xff0c;有一些是其他功能的&#xff09; 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.7 Web学生管理系统

1.定义通讯协议基于前面介绍过的 FLask Web 网站 与 urlib 的访问网站的方法&#xff0c;设计一个综合应用实例。它是一个基于 Web 的学生记录管理程序。学生的记录包括 id(学号) 、name(姓名) 、grade(成绩)&#xff0c;服务器的作用是建立与维护一个Sqllite 的学生数据库 stu…...

前端教学视频分享(视频内容与市场时刻保持紧密相连,火热更新中。。。)

⚠️获取公众号 本次要想大家推荐一下本人的公众号&#xff0c;在微信中搜索公众号 李帅豪在对话框中输入前端视频四个字即可立即获取所有视频&#xff0c;不收费无广告&#xff01;&#xff01;&#xff01; 本公众号收集了近两年来前端最新最优秀的学习视频&#xff0c;涵盖…...

Docker-consul的容器服务更新与发现

一.Consul概述1.1 什么是服务注册与发现服务注册与发现是微服务架构中不可或缺的重要组件。起初服务都是单节点的&#xff0c;不保障高可用性&#xff0c;也不考虑服务的压力承载&#xff0c;服务之间调用单纯的通过接口访问。直到后来出现了多个节点的分布式架构&#xff0c;起…...

Java笔记-线程中断

线程的中断 1.应用场景&#xff1a; 假设从网络下载一个100M的文件&#xff0c;如果网速很慢&#xff0c;用户等得不耐烦&#xff0c;就可能在下载过程中点“取消”&#xff0c;这时&#xff0c;程序就需要中断下载线程的执行。 2.常用中断线程的方法&#xff1a; 1.使用标…...

js中的自调用表达式

自调用表达式 由函数表达式创建的函数可以自调用&#xff0c;称之为自调用表达式。 语法 由函数表达式创建函数: const myFn function () {let a 100console.log(a);return a } myFn() //调用后执行&#xff0c;输出100表达式后面紧跟 ( ) 则会自动调用: const myFn fu…...

Python操作的5个坏习惯,你中了几个呢?

很多文章都有介绍怎么写好 Python&#xff0c;我今天呢相反&#xff0c;说说写代码时的几个坏习惯。有的习惯会让 Bug 变得隐蔽难以追踪&#xff0c;当然&#xff0c;也有的并没有错误&#xff0c;只是个人觉得不够完美。 注意&#xff1a;示例代码在 Python 3.6 环境下编写 …...

C++并发与多线程编程(3)---线程间共享数据

主要内容&#xff1a;共享数据带来的问题使用互斥量保护数据数据保护的替代方案共享数据带来的问题当涉及到共享数据时&#xff0c;问题可能是因为共享数据修改所导致。如果共享数据是只读的&#xff0c;那么只读操作不会影响到数据&#xff0c;更不会涉及对数据的修改&#xf…...

洞察:2022年医疗行业数据安全回顾及2023年展望

过去的2022年&#xff0c;统筹安全与发展&#xff0c;在医疗信息化发展道路中&#xff0c;数据安全不可或缺。这一年&#xff0c;实施五年多的《网络安全法》迎来首次修改&#xff0c;《数据安全法》、《个人信息保护法》实施一周年&#xff0c;配套的《数据出境安全评估办法》…...

多传感器融合定位十五-多传感器时空标定(综述)

多传感器融合定位十五-多传感器时空标定1. 多传感器标定简介1.1 标定内容及方法1.2 讲解思路2. 内参标定2.1 雷达内参标定2.2 IMU内参标定2.3 编码器内参标定2.4 相机内参标定3. 外参标定3.1 雷达和相机外参标定3.2 多雷达外参标定3.3 手眼标定3.4 融合中标定3.5 总结4. 时间标…...

开发微服务电商项目演示(三)

一&#xff0c;nginx动静分离第1步&#xff1a;通过SwitchHosts新增二级域名&#xff1a;images.zmall.com第2步&#xff1a;将本次项目的易买网所有静态资源js/css/images复制到nginx中的html目录下第3步&#xff1a;在nginx的核心配置文件nginx.conf中新增二级域名images.zma…...