> 文档中心 > C++进阶之多态

C++进阶之多态

目录

一,多态的概念

二,多态的定义及实现

(1)多态的构成条件 

(2)虚函数

(3)虚函数的重写  

虚函数重写的两个例外

(4)C++11 override 和 fifinal

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

​三、抽象类

(1)概念

(2)接口继承和实现继承

四、多态的原理

(1)虚函数表

(2)多态的原理

多态真正意义上是如何实现的呢?

虚函数的重写为啥吗也叫覆盖呢?

多态为什么只能的是基类指针和引用呢?对象为什么不行?

多态与非多态调用函数时的区别

同类型的虚表指针是一样的吗?

普通函数和虚函数存储的位置是否一样?

问题解析

(3)动态绑定与静态绑定

五、单继承和多继承关系的虚函数表

监视窗口观察虚函数表

(1)单继承

​(2)多继承

打印虚函数表

(1)单继承

(2)多继承

六、继承和多态常见的面试问题

问答题

七、虚继承中的虚函数(扩展)


一,多态的概念

概念: 多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。 举个栗子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。 多态的分类静态多态与动态多态:

静态的多态:函数重载,看起来调用同一个函数有不同的行为。静态指的是在编译时实现的。 eg:流提取运算符的重载,自动识别不同的类型 动态的多态:一个父类的引用或者指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态就是指运行时实现的。本质:不同的人去做同一件事情,结果不同 实现运行时多态性的机制称为动态绑定也叫晚期绑定。 eg:

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 ps;Student st;Func(ps);Func(st); //可以给子类对象是因为切割。return 0;}

 注意:这里虽然两个函数名相同,但是它俩却不构成隐藏。原因则是如果子类中满足三同(函数名,返回值,参数)的虚函数,叫做重写也叫覆盖。

二,多态的定义及实现

(1)多态的构成条件 

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person 。Person对象买票全价, Student 对象买票半价。

那么在继承中要 构成多态还有两个条件 1. 必须通过基类的指针或者引用调用虚函数 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

eg1:函数参数传对象就不构成多态

构成多态,与p的类型没有关系,与传过去的对象有关,传的是哪个类型的对象,调用的就是这个类型的虚函数 --跟对象有关。
不构成多态,与p的类型有关,调用的就是p类型的函数 --跟类型有关。

(2)虚函数

虚函数:即被virtual修饰的类的非静态成员函数称为虚函数。ps:其他函数不能成为虚函数,虚函数在类中声明和类外定义时候,virtual关键字只在声明时加上,在类外实现时不能加

class Person {public: virtual void BuyTicket() { cout << "买票-全价" << endl;}};

(3)虚函数的重写  

虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

eg: 不是虚函数,构成的是隐藏 ,这里的BuyTicket调用的是父类中的是因为p的类型是Person,与子类的隐藏并没有联系

eg:参数不同,不构成多态

 两个函数都有参数

 eg:返回值不同,直接报错

虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写(其实这个是C++不是很规范的地方,我们建议两个都写上virtual) eg:子类没写virtual但是构成了多态(虽然子类没写virtual,但是它是先继承了父类的虚函数属性,再完成重写,那么它也算是虚函数)

eg:子类函数的访问限定符是私有仍然可以构成多态,原因:和上面一样,子类继承了父类虚函数的属性,重写的是内容。

本质上,子类重写的虚函数,可以不加virtual是因为析构函数,父类析构函数加上virtual,那么就不存在不构成多态,没调用子类析构函数,内存泄漏场景。建议,我们自己写的时候都加上virtual

虚函数重写的两个例外

1. 协变 ( 基类与派生类虚函数返回值类型不同 )

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变(简单来说就是两个虚函数的返回值也可以不同,要求就是返回值是父子关系的指针或者引用)

 eg:返回值是父子类的指针,仍然构成多态

 eg:只要是父子类的返回值就可以

2. 析构函数的重写(基类与派生类析构函数的名字不同)

析构函数是虚函数,同样构成重写,因为析构函数名被特殊处理化了,处理成了destructor。 对于普通对象,析构函数是否是虚函数,是否完成重写,都会正确调用

eg: 析构函数不是虚函数

 析构函数是虚函数

析构函数构成多态的使用场景:

对于动态申请出来的父子对象,如果给了父类指针管理,多态就发生了作用,就需要析构函数是虚函数。

首先明白:new对于自定义类型--operator new+构造函数

                  delete对于自定义类型---析构函数+析构函数

                  析构函数名被特殊处理化了,处理成了destructor

 以该代码为例:

class Person {public:  ~Person() { cout << "~Person()" << endl; }};class Student : public Person {public:  ~Student() { cout << "~Student()" <destructor()delete p1; //析构函数 + operator deletedelete p2;    //p2->destructor()return 0;}

运行结果如下:

对于析构函数来说,p1,p2都是调用的Person的析构函数,原因就是他俩都是Person类型的。对于p1来说,调用Person的析构函数是没有任何问题的,因为它new的就是Person类型。但是对于p2来讲,它new的是Student类型,它里面包含Student部分与Person部分,析构函数只会调用Person的析构函数,进而p2里面的Student部分就得不到清理,就可能存在内存泄漏问题。

我们自己的目的就是让它指向父类调父类,指向子类调子类,那么该如何解决该问题呢?我们可以通过多态来实现。

第一个条件:指针或者引用 ,这个地方已经满足指针了。

第二个条件:函数名,返回值,参数都相同,且都是虚函数,已经满足

 运行结果如下:符合我们的预期结果

那为什么析构函数不直接叫做destructor呢?

语言是一步一步产生的,最开始产生类,又完成了构造和析构,当完成多态后,又发现在上述场景下,析构函数也需要多态,所以编译器又进行了特殊处理,可以认为编译器为早期设计做了个补丁

ps:并不是动态申请出来的对象就一定需要多态,要取决于动态申请出来的父子对象,是给了父类指针管理,还是父类与子类指针各管各的

(4)C++11 override fifinal

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此: C++11 提供了 override fifinal 两个关键字,可以帮助用户检测是否重写。

如何设计一个不能被子类继承的类?

方法一:C++98通过将父类的构造函数设置成私有的实现该目的--间接实现

原理:子类构造不出对象来了,因为子类初始化父类只有一种方式就是调用父类的构造函数 ,但是父类的构造函数是私有的,在子类中不可用,所以子类就没办法构造了。   

间接限制:自类构造函数无法调用父类构造函数初始化成员,没办法实例化对象   

 存在的问题:现在确实不能继承了,但是我们用A同样也实例化不出对象来了,因为A的构造函数是私有的,类外访问不到。

那么该如何解决这个问题呢?我们可以设置一个静态成员函数解决这个问题

class A{//类里面还是能用A的构造函数的private:A(int a=0):_a(a){}public:static A CreateOBj(int a = 0){return A(a);}protected:int _a;};

为什么要设置这个静态成员函数呢?

原因就是:

  • 静态成员函数可直接用类名+::+静态成员函数名访问,而其他成员函数必须要用对象调用
  • A这个类不能实例化出对象,因为它的构造函数是私有的

方法二:C++11 通过final实现--直接限制

final就是最终类,表示该类不能被继承,简洁明了。

final的其他功能

修饰虚函数,表示该虚函数不能再被重写,如果不想一个函数被重写,我们就可以加上final 

override关键字

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错 放在子类重写的虚函数后面,检查是否完成重写。

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

三、抽象类

(1)概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类 不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。(纯虚函数的类,本质上强制子类去完成虚函数重写,override只是在语法上检查是否完成重写)抽象--现实世界中没有对应的实物
一个类型,如果一般在现实世界中,没有具体的对应实物就定义成抽象类比较好。

PS:纯虚函数只声明,不实现;纯虚函数是可以实现的,但是它的实现却没有价值

 

new一个Car也是不行的 

子类继承后,同样new不出来,因为子类将父类的纯虚函数继承下来后,子类也是一个抽象类。

重写之后,可以进行实例化,这里调用的是子类的Drive是因为多态,指向谁调用谁。

从这个角度来看,实现纯虚函数没有意义,因为没有人调用,父类实例化不出对象,本身指针调用了又会崩溃。

(2)接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

为什么说纯虚函数的实现没有价值呢?首先看下面这个例子

虽然抽象类实例化不出对象来,但是可以定义指针,但是指针调用这个纯虚函数编译时不会报错,运行时结果就会崩溃。

有人说是空指针解引用的问题,但是对于一个普通的类来说,这个使用时没有任何问题的。所以这里与空指针无关。

那么原因是什么呢?我们先往下面学习,随后进行解释 

四、多态的原理

(1)虚函数表

//问:32位下sizeof(Base)是多少?class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;char _ch = 'A';};int main(){cout << sizeof(Base) << endl;}

一般情况下我们认为是8,因为int4个字节,char1个字节,函数在公共代码区,所以最大对齐数是8,但是答案是12个字节,就说明该虚函数不存放在公共代码区。

通过监视我们发现对象中多了一个指针_vfptr(指针的大小是4个字节,所以对齐数是12),叫做虚函数表指针,指向的这个表中存的是虚函数,符合多态的时候,调用虚函数要到虚表中去找。这个表就可以认为是个数组。

方便演示,增加一个虚函数

学到这上面遗留下的问题也迎刃而解:

f()不报错是因为它是普通函数,存在公共代码区,指针虽然是空指针,但是不会去指针指向的对象里去找这个f()函数,直接就找到了,只是要把空指针传给this。

Drive() 符合多态的场景,指针p就会在对象里找虚函数表指针,虚函数表指针又会在虚表里面去找调用函数的地址,这个p是空指针,在它去找虚函数表指针的时候,就直接崩溃了。

(2)多态的原理

我们以下面这段代码展开讨论

class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }void f(){cout << "f()" << endl;}protected:int _a = 0;};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-半价" << endl; }protected:int _b = 0;};void Func(Person& p) {p.BuyTicket();p.f();}int main(){Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;}

还记得这里 Func 函数传 Person 调用的Person::BuyTicket,传 Student 调用的是 Student::BuyTicket

首先我们画下对象模型:

当Func传的是父类对象,那就到父类对象的虚表里面找到父类的虚函数,所以调用父类的虚函数。

当Func传的是子类对象,发生了切片,这时候P就是子类对象中父类那一部分的别名,对p而言看到的还是父类对象,不同的是到父类对象的虚表里面找到的是重写的虚函数,也就是子类的。

决定多态里面调的是什么,是与对象的虚函数表里面存的是什么相关的,存的是什么调的就是什么。

从指令的角度来说他们执行的完全是相同的指令,只是因为传的是不同的对象,则去对象的虚表里面找到不同的虚函数。

p第一次是Mike的别名,第二次p是Johnson的别名,Johnson是一个子类对象,是子类对象中父类的那部分别名,两次都去找虚函数指针,找到的虚函数是不同的,所以调用的是不同的函数。

ps:从这我们可以得出,调用普通函数比虚函数更快,因为普通函数直接就是一个地址,虚函数则要去虚表里面找。

多态真正意义上是如何实现的呢?

依赖虚函数的重写,虚函数重写了以后对象的虚表里面就是不同的虚函数,父类对象是父类的虚函数,子类对象是子类的虚函数。多态的原理:基类的指针或引用,指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用。

虚函数的重写为啥吗也叫覆盖呢?

虚函数的重写也叫覆盖,重写是语法层的概念,覆盖是原理层的概念:子类继承了父类的虚函数,与父类用的不是同一个虚表,而是自己搞了一个虚表,可以认为把父类的虚表拷贝到自己的虚表上了,如果没重写虚函数,可以认为是一个,如果重写了虚函数,就要对这个位置进行覆盖,覆盖成子类重写的虚函数。

多态为什么只能的是基类指针和引用呢?对象为什么不行?

对象虽然也能切片,但是对象切片和引用或指针切片是有区别的

引用切片,r1就是Johnson中父类这部分的别名,虚表的指针是一样的。

如果是对象切片,p只会把Johnson中父类这部分的值拷贝过去,虚表指针不会拷贝过去。因为如果父类的虚表指针指向子类的虚表那就非常的麻烦了,那就不确定这时候父类对象中到底存的是子类的虚表还是父类的虚表。

换个角度说:如下图

p1,p2都是只拷贝成员不拷贝虚表指针,虚表指针都是父类的虚表指针,我这个对象确实能接收父类,也能接收子类。但是对象里面放的虚表指针始终是父类的虚表指针,放的是父类的虚函数,这个时候就实现不出来多态。

有人说编译器在子类切片的时候把子类的虚表指针拷贝过来不就能让对象也实现多态吗?

但是这个时候就乱了,一个父类的对象里面到底存的是父类的虚表指针,还是子类的虚表指针就无法确定。eg:这个时候去调用析构函数就有可能调错,因为此时父类的对象里面可能存的是子类的析构函数。所以为了不发生混乱,我们就要保证父类的对象里面一定是父类的虚表,子类对象里面一定是子类的虚表,进而对象在切片的时候就不会拷贝子类的虚函数表指针。因此,对象就无法实现出多态。

指针和引用可以就是因为他俩不拷贝。他俩就变成子类对象中父类那一部分的别名或者指针。

对象调用虚函数也不会到虚表里面去找,对象是在编译时就确定地址。换一个角度,就算他去虚表里面找,找到的也都是父类的。

多态与非多态调用函数时的区别

不是多态:在编译的时候确定地址

构成多态:在运行时确定地址,注意看这里call的是eax,但是我们并不知道eax里面是啥,得让他在表里面找才能知道,所以称之为运行时多态

同类型的虚表指针是一样的吗?

答:是一样的,同类型的虚表指针是相同的指向同一张虚表,所以一个类按理来说只有一张虚表

普通函数和虚函数存储的位置是否一样?

答:他们是一样的,都在代码段,只是虚函数要把地址存一份到虚表中去,方便实现多态

学到这以后,我们再来讨论下为什么子类继承父类后,将子类虚函数的访问限定符设置成私有以后还可以实现多态。

 从语法上来说重写是一种接口继承,子类在调用的时候编译器检查不出来。因为p是一个父类的引用,编译器会在父类中去检查,发现这个BuyTicket是公有的,转换成代码是跟普通函数不一样,是到虚表里面去找,只要找到就可以调用,所以私有的限制就不起作用了。

所以C++的访问限定符也不一定是绝对的安全(private一定调用不到),因为虚函数放到虚表中去,我想办法拿到这个虚函数的地址,还是可以调的到。

问题解析

通过观察和测试,我们发现了以下几点问题: 以该段代码为例解释:

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;}

1. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

2.子类d的虚表会先拷贝一份父类的,不同的是子类重写的Func1会覆盖,没重写的保持不变。

3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

我们在子类中增加一个Func4(),透过内存窗口进行观察,(监视窗口是看不到的)

6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多人都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

我们自己通过以下代码观察:

int main(){int* p = (int*)malloc(4);printf("堆:%p\n", p);int a = 0;printf("栈:%p\n", &a);//静态区大归类属于数据段static int b = 0;printf("数据段:%p\n", &b);const char* str = "aaaa";printf("常量区:%p\n", str);printf("代码段:%p\n", &Base::Func1);//虚函数的地址存在虚函数表指针头上4个字节32位下,64位下存在头8个字节Base bs;printf("虚函数表:%p\n", *((int*)&bs));//&bs是Base*,强转成int*,在解引用一下,就取到头上4个字节}

通过观察我们发现虚函数表指针的地址跟接近与常量区,常量区只是在C语言下这样叫,从操作系统来看,就是放在代码段的。

(3)动态绑定与静态绑定

1. 静态绑定又称为前期绑定 ( 早绑定 ) 在程序编译期间确定了程序的行为 也称为静态多态 ,比如:函数重载 2. 动态绑定又称后期绑定 ( 晚绑定 ) ,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

五、单继承和多继承关系的虚函数表

监视窗口观察虚函数表

(1)单继承

监视窗口中虚表函数的地址不是函数真正的地址,存储的是jmp指令的地址。 接下来通过反汇编帮助大家理解。

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;}virtual void Func4(){cout << "Derive::Func4()" <Func1();Base* p2 = &d;p2->Func1();return 0;}

(2)多继承

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" <func1();Base2* p2 = &d;p2->func1();}

多继承了后Derive就有两个虚表,func1重写,func2没重写,又新增了func3,那么func3放在那个虚表呢?

d里面有两张虚表。

 通过内存寻找func3,发现func3在Base1的虚表里面。(主要是看继承的先后顺序,如果先继承的是Base2,那么就是放在Base2里面的)

除此之外这里还存在着一个问题:Derive重写了func1,但是它里面的两个虚表中的func的地址不一样,这又是为什么呢?

因为这里的地址不是真实的函数地址是一个jmp指令,最终他们都会jmp到同一个函数上去了。我们还是通过反汇编观察。

多继承是子类重写了Base1和Base2的虚函数func1,但是虚函数表重写的func1的地址不一样,但是没关系,他们最终调用到的还是同一个函数,这里进行封装地址的主要目的就是修正this指针的地址。(ecx就是寄存器,存的是this指针)

借助汇编理解:

vs监视窗口中我们发现看不见子类自己写的虚函数 。这里是编译器的监视窗口故意隐藏了这些函数,也可以认为是他的一个小bug 。上面我们通过内存窗口进行了观察 虚表,下面我们使用代码打印出虚表中的函数。

虚表指针是在构造函数的进行初始化列表,初始化的(初始化列表不写,也会进行执行)

打印虚函数表

(1)单继承

对下面这段代码,我们就可以借助一些方法打印出它的虚函数表。因为虚函表是一个函数指针数组。我们自己建立的虚函数的类型又是void(*)()的;

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;};
typedef void(*VF_PTR)();//函数指针要把别名定义在中间-----void(*)()  VF_PTR//void PrintVFTable(VF_PTR* table)//打印虚函数表中的内容void PrintVFTable(VF_PTR table[]){for (int i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}}
int main(){Base  b;//传参的时候要传虚表指针的头四个字节,并要将其转换void(*)()类型PrintVFTable((VF_PTR*)(*(int*)&b));Derive d;//传参的时候要传虚表指针的头四个字节,并要将其转换void(*)()类型PrintVFTable((VF_PTR*)(*(int*)&d));}

这样我们就可以清楚的看到每个类自己的虚表了。

小问题:以上操作我们都是在32位下完成的,假如我们在64位下又该怎么做呢?(64位下我们就需要去取到头上的8个字节)

这里有三种方法: 方法一:直接用long long类型的因为它就是8字节

PrintVFTable((VF_PTR*)(*(long long*)&b));

方法二:利用二级指针--32位下指针是4个字节,64位下是8个字节,这种写法不管是在32位还是64位都可以完美解决,因为一个二级指针解引用后是个一级指针,一个指针不管是什么类型都始终是4/8个字节。

PrintVFTable((VF_PTR*)(*(void**)&b));

方法三:利用宏定义

#ifdef _WIN64PrintVFTable((VF_PTR*)(*(long long*)&b));#elsePrintVFTable((VF_PTR*)(*(int*)&b));#endif // !_WIN64

(2)多继承

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" <", i, table[i]);VF_PTR f = table[i];f();}}
int main(){Base1 b1;PrintVFTable((VF_PTR*)(*(void**)&b1));cout << endl;Base2 b2;PrintVFTable((VF_PTR*)(*(void**)&b2));cout << endl;Derive d;PrintVFTable((VF_PTR*)(*(void**)&d));cout << endl;}

但在d里面我们看不到Base2,我们可以通过以下两种方法进行观察

方法一:通过加Base1大小的方式加到Base2

PrintVFTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));

 这里为啥要强转成char*呢?

PrintVFTable((VF_PTR*)(*(void**)(&d + sizeof(Base1))));//这是错误写法//一个Base1是8个字节,int*+1 代表加4个字节,char*+1代表加1个字节,//Base*+1就是加8个字节,所以这要强转成char*类型

-------------------------------------------------------------------------------------------------------------------------

方法二:利用指针偏移,原理:Base2切片的时候,指针是会偏移一下的,不会像Base1一样直接指向d;

Base2* p = &d;PrintVFTable((VF_PTR*)(*(void**)p));

 原理解释:

六、继承和多态常见的面试问题

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 类使用的不是同一张虚表

答案:

1. A   2.D  3.C  4. A   5. B    6. D   7.D

8.下面程序输出结果是什么 ? ()

#includeusing namespace std;class A {public:A(const char* s) { cout << s << endl; }~A() {}};class B :virtual public A {public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }};class C :virtual public A {public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }};class D :public B, public C {public:D(const char* s1, const char* s2, const char* s3, const 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 答案:A   。解释:B,C虚继承A,A就只有一份,就会被放到一个公共位置,编译器就会将A的三个初始化合并成一个初始化的顺序与初始化列表的顺序没有关系,而是与声明的顺序 这里声明的顺序就是继承的顺序 有关,D先继承B再继承C就会先调B,再调C。而A是最先被继承的所以A就排在第一位去初始化。并且A只会在D里面进行初始化,B与C里面的都是没用的,因为A只有一份,A既不属于B也不属于C,所以B,C里面对A进行初始化是不管的。eg:D先继承C,再继承B,答案改变。证明初始化顺序确实与继承顺序有关

eg:在B,C里面改变A的初始化,答案不变,说明就不会执行B,C里面A的初始化。

 eg:去掉虚继承,就符合子类构造函数的调用规则,先初始化父类在初始化子类。同时D就不能初始化A了,因为D是子类调用D的初始化函数回去父类调用父类的构造函数,而这时D的父类B,C都不能去初始化这个A,对于D来说A就不算它的父类或者父类的成员。语法规定不允许使用间接非虚基类。

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 答案:C 解释:

10. 以下程序输出结果是什么()

class A {public: virtual void func(int val = 1){ std::cout<"<< val <<std::endl;} virtual void test(){ func();}};class B : public A {public: void func(int val=0){ std::cout<"<< val <test(); return 0; }

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确 答案:B 解释:p的指针调用test(),子类的指针调用一个虚函数是不构成多态的,子类没有写test(),但是继承下父类的test(),父类中test()的this指针是A*this ,p->test()就是把p传给了A*this,而这里发生A*this 调用func(),发生了隐形的多态,父类的指针A*this接收的是子类的指针,所以func调用的是子类重写的func(),貌似按照现在的逻辑答案是D,但是很坑的一点是:普通函数的继承是一个实现继承,把函数及函数的实现都继承下来了;但是虚函数的继承是接口的继承,只是把父类的接口继承下来了(接口说人话就是函数),重写了它的实现,所以子类的func没有加virtual也是重写,所以将父类的接口继承下来,这里的缺省参数用的是父类的,所以这里无论是父类还是子类val都是1. eg:所以这个子类val写啥都不管用。

问答题

1. 什么是多态?答:参考博客内容 2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 ) ?答:参考博客内容 3. 多态的实现原理?答:参考博客内容 4. inline 函数可以是虚函数吗? 答:可以,调用时如果不构成多态,这个函数就保持内联属性;如果构成多态,这个函数就没有inline属性了, 因为调用是到对象的虚函数表中找到虚函数的地址,实现调用无法使用inline属性。 eg:

5. 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

eg:

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。构造函数成为虚函数没有价值,虚函数的意义是构成多态调用,那么多态调用要去虚函数表中查找虚函数,对象中的虚函数表指针,是在构造函数初始化列表阶段才初始化的。        

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考博客内容。8. 对象访问普通函数快还是虚函数更快? 答:如果不构成多态,都是编译器确定调用函数的地址,那么他们一样快。 如果构成多态,那么是虚函数调用是运行时去虚函数表中确定函数地址,普通函数是编译时直接确定地址,那么普通函数更快。9. 虚函数表是在什么阶段生成的,存在哪的? 答:虚函数表是在编译阶段就生成的,一般情况下存在代码段( 常量区 ) 的。 10. C++ 菱形继承的问题?虚继承的原理?答:参考博主继承博客。注意这里不要把虚函数表和虚基表搞混了。 11. 什么是抽象类?抽象类的作用?答:参考博客内容 。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

七、虚继承中的虚函数(扩展)

//虚继承包含虚函数后是什么样子呢class A {public:virtual void f(){}public:int _a;};// class B : public Aclass B : virtual public A {public:int _b;};// class C : public Aclass C : virtual public A {public:int _c;};class D : public B, public C {public:int _d;};int main(){D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5; return 0;}

B,C重写了虚函数f1,又有自己的一个虚函数是怎样的?

class A {public:virtual void f1(){}public:int _a;};// class B : public Aclass B : virtual public A {public:virtual void f1(){}virtual void f2(){}public:int _b;};// class C : public Aclass C : virtual public A {public:virtual void f1(){}virtual void f2(){}public:int _c;};class D : public B, public C {public://D必须重写因为B重写了,C也重写了,A现在是虚继承是一份公共的A,那到底用谁重写的呢?这个地方就存疑virtual void f1(){}public:int _d;};int main(){D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5; return 0;}

组词