200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > C++ 面向对象(二)多态 : 虚函数 多态原理 抽象类 虚函数表 继承与虚函数表

C++ 面向对象(二)多态 : 虚函数 多态原理 抽象类 虚函数表 继承与虚函数表

时间:2020-10-07 23:48:45

相关推荐

C++ 面向对象(二)多态 : 虚函数 多态原理 抽象类 虚函数表 继承与虚函数表

目录

多态多态的概念多态的构成条件虚函数虚函数的重写协变(返回值不同)析构函数的重写(函数名不同)final和overridefinaloverride重载, 重写, 重定义对比抽象类多态的原理虚函数表虚函数表的存储位置动态绑定和静态绑定继承与虚函数表单继承与虚函数表多继承与虚函数表

多态

多态的概念

什么是多态呢?就是一种事物,多种形态。就是对于同一个行为,不同的对象去完成就会产生不同的结果。

举个生活中的例子,当你去旅游景点游玩时,不同的身份买票的价格也不一样。比如对于普通人是原价购买,而对于学生和孩子则是半价购买,对于军人则是优先购买。明明同样是购买,不同身份带来的不同结果,就是多态的作用。

在C++中,多态就是对于同一个函数,当调用的对象不同,他的操作也不同。就是指针和引用指向指向哪一个对象,就调用哪一个对象的虚函数

例如:

class Human{public:virtual void print(){cout << "i am a human" << endl;}};class Student : public Human{public:virtual void print(){cout << "i am a student" << endl;}};class Teacher : public Human{public:virtual void print(){cout << "i am a teacher" << endl;}};void ShowIdentity(Human& human){human.print();}int main(){Human h;Teacher t;Student s;ShowIdentity(h);ShowIdentity(t);ShowIdentity(s);}

多态的构成条件

这里先给出条件,底下的原理解析那一块会具体讲原因

多态是继承体系中的一个行为,如果要在继承体系中构成多态,需要满足两个条件

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,并且派生类必须要对继承的基类的虚函数进行重写

虚函数

虚函数就是被virtual修饰的类成员函数(这里的virtual和虚继承的virtual虽然是同一个关键字,但是作用不一样)

如:

class Human{public:virtual void print(){cout << "i am a human" << endl;}};

虚函数的重写

当派生类中有一个和基类完全相同的虚函数(函数名,返回值,参数完全相同),则说明子类的虚函数重写了基类的虚函数(只重写了函数实现)

如:

class Human{public:virtual void print(){cout << "i am a human" << endl;}};class Student : public Human{public:virtual void print(){cout << "i am a student" << endl;}};void ShowIdentity(Human &human){human.print();}int main(){Human h;Student s;ShowIdentity(h);ShowIdentity(s);}

如果不满足上面的条件,例如参数不同则会变成重定义。

注意:

#include <iostream>class Base{public:virtual void Show(int n = 10)const{//提供缺省参数值std::cout << "Base:" << n << std::endl;}};class Base1 : public Base{public:virtual void Show(int n = 20)const{//重新定义继承而来的缺省参数值std::cout << "Base1:" << n << std::endl;}};int main(){Base* p1 = new Base1; p1->Show(); return 0;}

此时输出的是Base1:10, 这是出自Effective C++中的一个问题

如果子类重写了缺省值,此时的子类的缺省值是无效的,使用的还是父类的缺省值

原因是因为多态是动态绑定,而缺省值是静态绑定。对于P1,他的静态类型也就是这个指针的类型是Base,所以这里的缺省值是Base的缺省值,而动态类型也就是指向的对象是Base1,所以这里调用的虚函数则是Base1中的虚函数,所以这里就是Base1中的虚函数,Base中的缺省值,也就是Base1:10。

或者可以更简单的一句话描述,虚函数的重写只重写函数实现,不重写缺省值

这道题最近考试做错了,就拿出来讲了一下

但是也存在两种例外的情况。

协变(返回值不同)

当基类和派生类的返回值类型不同时,如果基类对象返回基类对象的引用或者指针,派生类对象也返回的是派生类对象的引用或者指针时,就会引起协变。协变也能完成虚函数的重写

例如:

class Human{public:virtual Human& print(){cout << "i am a human" << endl;return *this;}};class Student : public Human{public:virtual Student& print(){cout << "i am a student" << endl;return *this;}};

如果返回值不是引用或者指针则不会构成协变

class Student : public Human{public:virtual Student print(){cout << "i am a student" << endl;return *this;}};

析构函数的重写(函数名不同)

析构函数虽然函数名不同,但是也能构成重写,因为编译器为了让析构函数实现多态,会将它的名字处理成destructor,这样就能也能构成重写。

为什么编译器要通过这种方式让析构函数也能构成重写呢?

假设存在这种情况,我用一个基类指针或者引用指向派生类对象,如果不构成多态会怎样

class Human{public:~Human(){cout << "~Human()" << endl;}};class Student : public Human{public:~Student(){cout << "~Student()" << endl;}};int main(){Human* h = new Student;delete h;return 0;}

可以看到,如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数,这也就导致了一种情况,如果派生类的析构函数中有资源释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为destructor的原因

final和override

final和override是C++11中提供给用户用来检测是否进行重写的两个关键字。

final

使用final修饰的虚函数不能被重写。

如果某一个虚函数不想被派生类重写,就可以用final来修饰这个虚函数

class Human{public:virtual void print() final{cout << "i am a human" << endl;}};class Student : public Human{public:virtual void print(){cout << "i am a student" << endl;}};

override

override关键字是用来检测派生类虚函数是否构成重写的关键字。

在我们写代码的时候难免会出现些小错误,如基类虚函数没有virtual或者派生类虚函数名拼错等问题,这些问题不会被编译器检查出来,发生错误时也很难一下子锁定,所以C++增添了override这一层保险,当修饰的虚函数不构成重写时就会编译错误。

class Human{public:void print(){cout << "i am a human" << endl;}};class Student : public Human{public:virtual void print() override{cout << "i am a student" << endl;}};

重载, 重写, 重定义对比

重载:

1.在同一作用域

2.函数名相同,参数的类型、顺序、数量不同。

重写(覆盖):

1.作用域不同,一个在基类一个在派生类

2.函数名,参数,返回值必须相同(协变和析构函数除外)

3.基类和派生类必须都是虚函数(派生类可以不加virtual,基类的虚函数属性可以继承,但是最好要加上virtual)

重定义(隐藏):

1.作用域不同,一个在基类一个在派生类

2.函数名相同

3.派生类和基类同名函数如果不构成重写那就是重定义

抽象类

如果在虚函数的后面加上 =0,并且不进行实现,这样的虚函数就叫做纯虚函数。而包含纯虚函数的类,也叫做抽象类或者接口类。抽象类不能实例化出对象,因为他所具有的信息不足以描述一个对象,派生类继承后也只有在重写纯虚函数后才能实例化出对象。

抽象类就像是一个蓝图,为派生类描述好一个大概的架构,派生类必须实现完这些架构,至于要在这些架构上面做些什么,增加什么,就属于派生类自己的问题。

例如:

class Human{public:virtual void print() = 0;};class Student : public Human{public:virtual void print(){cout << "i am a student" << endl;}};class Teacher : public Human{public:virtual void print(){cout << "i am a teacher" << endl;}};

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的

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

多态的原理

虚函数表

这里还是用这两个类举例子

class Human{public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}void test2(){cout << "1test1" << endl;}int _age;};class Student : public Human{public:virtual void print() {cout << "i am a student" << endl;}void test2(){cout << "2test2" << endl;}int _stuNum;};

还是和上次一样,首先看看h的大小,按照正常情况,因为h中只有一个成员变量_age,大小应该是四个字节。

但是这里却是8个。

打开监视窗口观察

可以看到里面除了_age以外,还有个指针_vfptr,这个指针指向了一个函数指针数组,这个函数指针数组也就是虚函数表,其中的每一个成员指向的都是之前我们实现的虚函数,这个_vfptr也被称为虚函数表指针

而不是虚函数的test2则没有被放入表中。

多态的实现也正是借助了这个虚函数表。

首先观察这个虚函数表,我们可以看到,如果派生类实现了某个虚函数的重写,那么在派生类的虚函数表中,重写的虚函数就会覆盖掉原有的函数,如Student::print。而没有完成重写的test1则依旧保留着从基类继承下来的虚函数Human::test1。

为了进一步验证基类和派生类虚函数表的关系,我将派生类所有的虚函数重写去掉

结合上面的内容可以发现,派生类会继承基类的虚函数表,如果派生类完成了重写,则会将重写的虚函数覆盖掉原有的函数。所以指针或引用指向哪一个对象,就调用对象中虚函数表中对应位置的虚函数,来实现多态。

这也就是为什么需要派生类函数也为虚函数,并且必须要重写才能实现的原因

继续分析构成多态的另一个条件,为什么必须要指针或者引用才能构成多态。

int main(){Student s;Human h1 = s;Human* h2 = &s;}

这里可以看到,如果将派生类对象赋值给基类对象,会因为对象切割,导致他的内存布局整个被修改,完全转换为基类对象的类型,虚函数表也与基类相同,所以不能实现多态。

而如果用基类指针或者引用指向派生类对象,虽然指向的是派生类对象,但是他们的内存布局是兼容的,他不会像赋值一样改变派生类对象的内存结构,所以派生类对象的虚函数表得到了保留,所以他可以通过访问派生类对象的虚函数表来实现多态。

总结一下派生类虚函数表的生成过程:

1.首先派生类会将基类的虚函数表拷贝过来

2.如果派生类完成了对虚函数的重写,则用重写后的虚函数覆盖掉虚函数表中继承下来的基类虚函数

3.如果派生类自己又新增了虚函数,则添加在虚函数表的最后面

常见问题解析:

内联函数可以是虚函数吗?

不可以,内联函数没有地址,无法放进虚函数表中。

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

不可以,静态成员函数没有this指针,无法访问虚函数表。

构造函数可以是虚函数吗?

不可以,虚函数表指针也是对象的成员之一,是在构造函数初始化列表初始化时才生成的,不可能是虚函数

析构函数可以是虚函数吗?

可以,上面有写,最好把基类析构函数声明为虚函数,防止使用基类指针或者引用指向派生类对象时,派生类的析构函数没有调用,可能导致内存泄漏。

对象访问虚函数快还是普通函数快?

如果不构成多态的话,虚函数和普通函数的访问是一样快的,但是如果构成多态,调用虚函数就得到虚函数表中查找,就会导致速度变慢,所以普通函数更快一些。

虚函数表的存储位置

从上面的观察可以看出来,虚函数存于虚函数表中,那么虚函数又存储在哪里呢?

这里就来验证一下

int main(){Student s1;int a = 0;int* p1 = &a;char* p2= "helloworld";int* p3 = new int;printf("栈变量:%p\n", p1);printf("代码段常量:%p\n", p2);printf("堆变量:%p\n", p3);printf("普通函数地址:%p\n", ShowIdentity);printf("虚函数地址:%p\n", &Student::print);printf("虚函数表地址:%p\n", *(int*)&s1);}

通过对比可以看到,虚函数表与常量,函数一样存储于代码段中。

所以得出结论,虚函数表在编译阶段生成,存储于代码段。

动态绑定和静态绑定

对象的静态类型:对象在声明时采用的类型。是在编译期确定的。(比如下面的h1,Human也就是他原本的类型就是静态类型,而他指向的对象的类型Student也就是动态类型)

对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改

静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。

动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

接着我们通过汇编代码,来观察多态是在哪个阶段实现的, 就可以知道它是静态还是动态。

int main(){Student s1;Human& h1 = s1;Human h2 = s1;h1.print();h2.print();return 0;}

可以看到h1的print是满足多态的,这里调用的函数是在

这一阶段中找到eax中存储的虚函数指针,所以可以发现,满足多态的调用是在运行的时候,到对象中的找到虚函数指针来完成的调用

而下面h2的print则不满足多态,所以是直接在编译时从符号表中找到函数的地址后调用。

所以可以得出的结论是,满足多态的函数调用时在运行的时候调用的,也就是动态多态。而之前重载那一章节也曾经说过重载也是一种多态的表现,只不过重载是在编译的时候完成的调用,所以也被静态多态

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

继承与虚函数表

单继承与虚函数表

class Human{public:virtual void print(){cout << "Human::print" << endl;}int _age;};class Student : public Human{public:virtual void print(){cout << "Student::print" << endl;}virtual void test1(){cout << "Student::test1" << endl;}int _stuNum;};

对于单继承的虚函数表,他会直接继承基类的虚函数表,如果完成了重写,则会覆盖掉原来的虚函数,如果有新的虚函数test1(),则会加在基类虚函数表的尾部。但是由于编译器的问题所以这里并不会显示出来。

所以可以通过代码直接从内存中查看

typedef void(*vfPtr) ();void Print(vfPtr vfTable[]){cout << " 虚表地址>" << vfTable << endl;for (int i = 0; vfTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vfTable[i]);vfPtr f = vfTable[i];f();}cout << endl;}int main(){Human b;Student d;vfPtr* vfTable = (vfPtr*)(*(int*)& b);Print(vfTable);vfPtr* vfTable = (vfPtr*)(*(int*)& d);Print(vfTable);return 0;}

可以看到,如果派生类有新的虚函数,则会加在虚函数表的尾部。

多继承与虚函数表

class Human{public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}int _age;};class Student : public Human{public:virtual void print() {cout << "i am a student" << endl;}virtual void test1(){cout << "2test1" << endl;}int _stuNum;};class Test : public Human, public Student{public:virtual void print(){cout << "i am a Test" << endl;}virtual void test2(){cout << "2test2" << endl;}};

对于多继承来说,派生类会拷贝两个基类的虚函数表

同样的,编译器无法显示,所以继续用代码从内存中读取。

typedef void(*vfPtr) ();void Print(vfPtr vfTable[]){cout << " 虚表地址>" << vfTable << endl;for (int i = 0; vfTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vfTable[i]);vfPtr f = vfTable[i];f();}cout << endl;}int main(){Test t;vfPtr* vfTableb = (vfPtr*)(*(int*)& t);Print(vfTableb);vfPtr* vfTabled = (vfPtr*)(*(int*)& t + sizeof(Human));Print(vfTabled);return 0;}

同样的,重写的虚函数会覆盖原有虚函数,而派生类未重写的虚函数test2()则会放到第一个继承基类部分的虚函数表中,也就是这里的Human的虚函数表中。

对于多态和对象模型这一部分的问题,我还有很多地方理解的不够好,可以参考一些陈皓大佬的这几篇博客来进一步学习。

C++ 虚函数表解析

C++ 对象的内存布局(上)

C++ 对象的内存布局(下)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。