目录
写在前面:
今天学习到了继承中的多态,这个知识点我已經学了不下10次了,每一次几乎都会讲到这个知识点,去年我在上武汉理工大学汪洋博士的Openfoam Programming 2306的时候手动实现了一次,但是其实当时有一些小bug并没有解决,理解了大概意思就过了(那门课程的节奏有点顶)。今天正好赶到Rock老师讲到这里了,我之前的记忆也差不多还给汪博了,于是我决定这一次直接彻底把他搞透(学习之前还是有些抵触的,差点暂时跳过),于是有了这一篇文章(除了搞懂了这个基本上今天啥也没干)。网上也有很多博主讲过这个东西了,老八股文了,大佬们多见谅。
具体实现:
-
多态的实现:继承机制(
inheritance
)+虚函数机制(virtual function
), 虚函数只需要加载在父类中,但是在子类中也给出声明可以增加代码的可读性。 -
为什么我们需要多态?
-
提高代码的可重用性,利用多态可以实现不同派生类继承于相同基类,这样避免了大量的重复代码,使得代码更加简洁。
-
实现一致的接口,对于不同类型的派生类我们可以通过多态分别定义其实现,这样用户不需要过度关心接口的区分,这也提升了代码内在实现的隐秘性。
-
提高代码可扩展性,如果使用多态,那么想实现一个新的派生类,只需要修改部分接口即可对代码进行相应的扩展。
-
动态绑定,运行时多态机制使得我们定义一个函数不需要给各种子类分别定义,编译器会自动查询虚表进而实现不同类型对象调用各自内部定义的函数。
-
说人话其实就是人们常说的父类引用或指针为形参,传入子类的对象之后,在函数内部调用的所有方法都是子类重写的方法(如果有继承关系且声明为虚函数并在子类中重写)
-
-
多态虚函数的原理:
虚函数表
-
说完了原因,那么这个运行时多态的机制到底是什么呢?其实我们作为程序员只需要了解他的使用就可以了,但是对于面试来说虚函数以及运行时多态的原理是绕不过去的一个话题。为此就引出了我们运行时多态的始作俑者:虚函数表和虚函数。
-
虚函数:
-
虚函数指在一个函数的基类中被声明为
virtual
的函数,这个函数会在派生类中被重写。当基类指针或引用调用这个函数时,编译器会根据对象的不同匹配相应的函数定义,实现多态。
-
-
虚函数表(vtable):
-
为了实现运行时多态,编译器使用虚函数表的结构,他为每一个类创建一个虚函数表,这个表是一组函数指针,每一个指针指向一个虚函数的实现。
-
当一个类对象被创建时,其内部会包涵一个虚指针(vptr),指向其对应类的虚函数表,虚函数表中包含了类内所有的虚函数的地址。
-
当调用一个虚函数,编译器会调用这个对象虚指针指向的类当中定义的相应的虚函数,编译器也可以通过基类的指针或者引用直接访问派生类中重写的虚函数版本,这种机制使得函数的行为可以根据对象的实际类型而变化也是运行时多态的原理。
-
-
虚函数指针(vptr):
-
每一个包含虚函数的类的对象都会包含一个隐藏成员,虚函数指针,通常情况下vptr是对象内存布局中的第一个对象,即vptr的地址通常就是对象的地址
-
-
为此,借用rock老师的思路我画了一个虚函数表的示意图,如上面提到的虚函数表是编译时为每个含有虚函数的类创建的,而每一个新生成的对象内部包含一个虚指针,他指向了对象创建时所属类的虚函数表,如此一来实现了多态。
-
-
我们再来总结一下, 一个对象生成时,(在大多数编译器中)他的内存分配的第一个位置保存的隐藏成员就是vptr,这个vptr指向了他的所属类别的vtbl,他的vtbl+偏移量指向的就是相应的虚函数: 即
vptr
=&obj
>>vtbl
=*vptr
>>func
=vtbl[offset]
>>func = (*&obj)[offset]
, 所以对于func
来说,vtbl
是一级指针*,那么vptr就是二级指针**(也就是对象的地址),注意偏移量不等于字节数,不能直接在vtbl指针的基础上+1,2,3。 -
下面我们举一个例子,就一个基类和Base一个派生类Derived,基类里面有三个虚函数,子类中实现了前两个,第三个不实现。然后实现就是直接打印出相应的函数位置,再加上一个普通成员函数,两个成员变量以及一个静态成员变量,由此用于说明类对象的内存分布。
-
定义类的代码如下:
#include<iostream> #include<cstdint> class Base{ public: //vptr = 8 in 64 bit os //vptr = 4 in 32 bit os virtual void func1(){ std::cout << "in Base::func1" << std::endl; } virtual void func2(){ std::cout << "in Base::func2" << std::endl; } virtual void func3(){ std::cout << "in Base::func3" << std::endl; } void func4(){ std::cout << "in non-virtual func4" << std::endl; } private: int x = 200;//4 int y = 300;//4 static int z; }; class Derived:public Base{ public: virtual void func1(){ std::cout << "in son::func1" << std::endl; } virtual void func2(){ std::cout << "in son::func2" << std::endl; } };
-
我们先来在main函数中看一看这个Base创建的对象的内存大小吧,按照我们的想法,所有的虚函数应该存放在一个vtable中在64bit系统中他的大小应该是8,然后就是我们的两个int型的成员变量每个是4, 注意函数都存在代码段中,所以另外一个非虚函数不存在于对象中,虚函数也是如此,存在对象中的是一个指向vtable的指针(又说了废话)。还有就是静态成员变量也不在对象中,这个可以自己去拓展。所以他的大小就是4+4+8=16
int Base::z = 0; int main(){ Base father; Derived son; // 4 + 4 + 8 = 16 std::cout << "sizeof(father) == " << sizeof(father) << std::endl; std::cout << "sizeof(son) == " << sizeof(son) << std::endl; }
-
最后的输出结果如我们所想:
/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 sizeof(father) == 16 sizeof(son) == 16
-
现在我们开始真正的用vptr激活我们的多态吧,为了方便调用,我们定义一个函数指针类型(func),依旧在main函数中生成两个对象,我们尝试使用vptr来激活多态。
-
首先我们按照我们上面所叙述的原理硬解一下这个函数多态的实现,那么在这里我们由于变量类型的实现问题需要用到几个关键字(博主的工具链ubuntu & g++,直接用int指针或者unsigned int或者
longlong都直接报了断错误,这里需要更正一下,long long 是完全可以的,当时我的测试有问题,只要能够容纳相关内容即可)。-
第一个就是一个来自
<cstdint>
的uintptr_t
,这是一种无符号整型,能够包含所有类型的指针而不丢失信息,此外他可以自动匹配不同的操作系统,可以在32位和64位系统上自动转换大小。(注意:不管你用什么,指针的级别一定要搞清楚,即*的个数) -
第二个就是
reinterpret_cast<>
,这是一种类型转换运算符,用于进行低级别的、不安全的类型转换。它能够在不同类型之间进行转换,即使这些类型之间本来没有明显的关联,这个运算符编译器不推荐,但是我试过static_cast<>
是不好用的。 -
注:以上两个关键字我是从武汉理工大学汪洋博士的
Openfoam Programming 2306
中的课程学到的,在此感谢汪洋博士,顺便吹一波汪哥的课,他真的付出了很多的辛苦在这门课的迭代上,搞CFD的兄弟姐妹们可以了解一下 。
-
-
那么现在所有的工具都齐全了我们直接一条代码实现这个调用,两种方式,uintptr_t**, long long**均可以。
-
//...other codes given above typedef void (*func)(); int Base::z = 0; int main(){ Base father; Derived son; //from <cstdint> uintptr_t an unsigned-int able to contain all type of pointer without losing information //this method doesnot work vvvv //reinterpret_cast<func>((*(reinterpret_cast<uintptr_t**>(&father)))+1)(); //this is the achievement of polymophism using (vptr->vtbl)[offset] for(int i=0; i<3; ++i){ std::cout << "polymorphism achievement of father" << std::endl; // reinterpret_cast<func>((*(reinterpret_cast<long long**>(&father)))[i])(); reinterpret_cast<func>((*(reinterpret_cast<uintptr_t**>(&father)))[i])(); std::cout << "polymorphism achievement of son" << std::endl; // reinterpret_cast<func>((*(reinterpret_cast<long long**>(&son)))[i])(); reinterpret_cast<func>((*(reinterpret_cast<uintptr_t**>(&son)))[i])(); } }
-
输出结果如下:是不是很酷?为什么func3还是在Base?因为func3没有实现,所以这个就很重要了,在未定义子类成员方法的时候,你至少有一个父类的方法可以使用,这样就会很安全。
-
/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 polymorphism achievement of father in Base::func1 polymorphism achievement of son in son::func1 polymorphism achievement of father in Base::func2 polymorphism achievement of son in son::func2 polymorphism achievement of father in Base::func3/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 in Base::func1 in Base::func2 in Base::func3 in son::func1 in son::func2 in son::func3 polymorphism achievement of son in Base::func3
-
那么这其中到底发生了什么呢?速度太快看不见?
-
我们把这个实现变成一个函数吧,函数返回一个定义好的函数指针类型func,然后传入对象的地址,依旧地(两种方式,uintptr_t**, long long*)具体实现如下所示:
-
//...other codes given above typedef void (*func)(); int Base::z = 0; /* //This is method using longlong* func convert(Base* father, int offset){ long long** vptr = reinterpret_cast<long long**> (father); long long* vtbl = *vptr; long long function = vtbl[offset]; return reinterpret_cast<func> (function); } */ func convert(Base* father, int offset){ uintptr_t** vptr = reinterpret_cast<uintptr_t**> (father); uintptr_t* vtbl = *vptr; uintptr_t function = vtbl[offset]; return reinterpret_cast<func> (function); } int main() { Base father; Derived son; //from <cstdint> uintptr_t an unsigned-int able to contain all type of pointer without losing information for(int i=0; i<3; ++i){ std::cout << "polymorphism achievement of father" << std::endl; convert(&father,i)(); std::cout << "polymorphism achievement of son" << std::endl; convert(&son,i)(); } return 0; }
-
和之前一模一样,输出结果如下:
-
/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 polymorphism achievement of father in Base::func1 polymorphism achievement of son in son::func1 polymorphism achievement of father in Base::func2 polymorphism achievement of son in son::func2 polymorphism achievement of father in Base::func3 polymorphism achievement of son in Base::func3
-
再有一点,如果我的子类多定义了一个父类没有的函数呢?我们添加一个public的第五个函数,注意这个函数父类没有。
-
class Derived:public Base{ public: void func5(){ std::cout << "in son::func5" << std::endl; } };
-
相应的main函数中调用一下,偏移量为3了
-
int main() { Base father; Derived son; convert(&son,3)();//non-virtual function cannot be called by the vptr return 0; }
-
报了段错误,这说明虚指针没办法访问非虚成员函数
-
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
-
现在我们在把这个声明成虚函数,仍然,他不在我们的父类里
-
class Derived:public Base{ public: virtual void func5(){ std::cout << "in son::func5" << std::endl; } };
-
使用相同的main函数调用试一下
-
int main() { Base father; Derived son; convert(&son,3)(); return 0; }
-
奇迹出现了,输出成功
-
/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 in son::func5
-
这说明,vtable包含的虚函数不一定全部来源于父类,在子类中重新调用,还是可以通过父类访问的,这个真的很强了。
-
大家可以好好看一看这个函数中的实现,其实就是一个指针指向另一个然后解引用,以上就是多态的机理了,不知道大家有没有看懂?
-
注意,这个实现很不安全,大家理解机理最重要,千万不要在实际项目使用这种操作。
-
-
为什么不可以是int类型**?
-
为什么这里使用int**不可以呢?我第一时间会思考,int**在64位操作系统上他也是一个64位系统的指针啊?为什么就不可以定义呢?
-
其实不是类型问题,而是经过解引用后他已经不是一个指针了,那个时候就涉及到了寻址的问题。
-
如果使用int** 解引用两次就会变成int类型,这个时候变量指向vtbl中的函数,依旧是一个指针,而int型无法完全寻址64位操作系统,会在32位时截断,导致访问到的内容未定义。
-
我们再来看,我继续使用int类型,但是我依旧可以完成多态的调用,但是我要保证在他解引用至虚表中的函数时,他依旧是一个指针而不是一个标准的integer,那么这个时候我一定可以完成实现,因为不管在什么系统上,不同类型的指针的大小都是一样的,不存在sizeof(int*) < sizeof(long*) < sizeof(long long*)
-
看一下我的实现吧,确实可以用int但是最后一次保证他是指针,所以我们就是三级指针了,注意cpp不建议二级指针以上,我只是在此为了理解这个困扰我的问题而实践了一下。用这么多指针确实是无比丑陋。看我的代码定义吧,我把所有的实现都放在这了:
-
typedef void (*func)(); /* func convert(Base* father, int offset){ //int *** vptr = reinterpret_cast<int ***> (father); long long** vptr = reinterpret_cast<long long**> (father); long long* vtbl = *vptr; long long function = vtbl[offset]; return reinterpret_cast<func> (function); } */ /* func convert(Base* father, int offset){ uintptr_t ** vptr = reinterpret_cast<uintptr_t **> (father); uintptr_t* vtbl = *vptr; uintptr_t function = vtbl[offset]; return reinterpret_cast<func> (function); } */ func convert(Base* father, int offset){ int *** vptr = reinterpret_cast<int ***> (father); int ** vtbl = *vptr; int* function = vtbl[offset]; return reinterpret_cast<func> (function); }
-
测试代码如下:
-
int Base::z = 0; int main() { Base father; Derived son; std::cout << "using the designed function to activate the polymorphism" << std::endl; for(int i=0; i<3; ++i){ std::cout << "polymorphism achievement of father" << std::endl; convert(&father,i)(); std::cout << "polymorphism achievement of son" << std::endl; convert(&son,i)(); } return 0; }
-
最后的代码运行结果,依旧完美运行:
-
/media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week02/day05/project02/cmake-build-debug/project02 using the designed function to activate the polymorphism polymorphism achievement of father in Base::func1 polymorphism achievement of son in son::func1 polymorphism achievement of father in Base::func2 polymorphism achievement of son in son::func2 polymorphism achievement of father in Base::func3 polymorphism achievement of son in Base::func3 Process finished with exit code 0
-
总结
- 这个多态的实现我用了两天的时间,里面的每一个点都抠了很久才明白,我觉得这个真的不是茴香豆写法,而是如果你真的想理解一个东西,你应该是有任何一个说不过取得点都会觉得有问题的。就好比这个指针,他既然是指针了,都在一个os里怎么会存在一个所谓的寻址范围大小问题呢?
- 希望大家遇到问题尽快地问题弄清楚,如果在知识点中遗留问题,何谈掌握呢?还有就是希望这篇文章真的对你有用,至少这是我目前写过的我认为最有意义的一篇文章。
- 我记得我的好朋友武汉理工大学的汪博说过最多的一句话:想进步一定要dirty your hand。
- 最后以南京大学蒋炎炎老师的一句话结尾最合适不过了:“ 编译器永远是对的,没有debug的代码永远是错的。”
致谢
- 继续感谢奇牛学院Rock老师的课程。
- 感谢汪洋博士的课程
Openfoam Programming 2306
,搞cfd的同仁们可以关注一下。 - 大家共同进步,共勉。