类指针 调用虚函数的时候,会使用 vptr 找虚函数表。
在使用 函数指针 调用成员虚函数的时候会使用到vcall。如果是vcall代码段,则vcall代码会应道编译器找出正确的虚函数表中的虚函数地址进行调用。
一 指向类成员函数的指针,类静态函数,普通全局函数
从前面的学习我们已经知道如何给 定义一个类成员函数指针了.
那么成员函数也是可以通过这种方式定义的,不过有些细节上的不同个,参见代码
class Teacher26base {
public:Teacher26base() {}void Teacher26basefun() {cout << "Teacher26basefun called" << endl;}
};class Teacher26son : public Teacher26base {
public:Teacher26son() {}void Teacher26sonfun() {cout << "Teacher26sonfun called" << endl;}int static Teacher26sonstaticfunc(string str) {cout << "Teacher26sonstaticfunc called str = " << str << endl;return 10;}
};void quanjuTeacher26() {cout << "全局quanjuTeacher26 called" << endl;
}void main() {//旧的回忆,对于全局函数的访问,可以通过定义一个函数指针类型//1.定义一个 函数指针 类型 QuanJuFuncTypetypedef void(*QuanJuFuncType)();//2.使用类型 定义 变量 func,由于是一个指针,QuanJuFuncType func = quanjuTeacher26;func();//3.直接定义一个函数指针类型void(*jutiQuanJuFunc)();jutiQuanJuFunc = &quanjuTeacher26;//jutiQuanJuFunc = quanjuTeacher26;//对于全局函数,前面加不加&都一样jutiQuanJuFunc();//4.对于类成员函数,我们可以这么干吗,是的,也可以这么干,不过前面要加上类命名空间void(Teacher26son::*jutiQuanJuFuncTeacher26son)();//4.1 但是在类成员指针的赋值的时候,一定要加上&jutiQuanJuFuncTeacher26son = &Teacher26son::Teacher26sonfun;//4.2 调用时,需要一个类对象,//注意调用的时候,是(对象.*xxx)(),这是为了和类成员函数 tea.xxx区分Teacher26son tea;(tea.*jutiQuanJuFuncTeacher26son)();//5.如果是类静态成员函数int(*staticfunc)(string tempvalue) = &Teacher26son::Teacher26sonstaticfunc;//静态成员也是一样的,不同的是,需要加上类命名空间,注意的是和 全局函数一样,前面加不加 &都可以,加不加& 是历史原因。int result = staticfunc("abc");cout << "result = " << result << endl;}
二 指向虚成员函数的指针以及vcall进一步谈
如果要弄一个 指向类成员虚函数呢?
class Teacher26base {
public:Teacher26base() {}void Teacher26basefun() {cout << "Teacher26basefun called" << endl;}int virtual Teacher26basevirtualfunc(double dou) {cout << "Teacher26basevirtualfunc called dou = " << dou << endl;int inttemp = (int)dou;return inttemp;}int virtual Teacher26basevirtualfunc2(double dou) {cout << "Teacher26basevirtualfunc2 called dou = " << dou << endl;int inttemp = (int)dou * 2;return inttemp;}
};class Teacher26son : public Teacher26base {
public:Teacher26son() {}void Teacher26sonfun() {cout << "Teacher26sonfun called" << endl;}int static Teacher26sonstaticfunc(string str) {cout << "Teacher26sonstaticfunc called str = " << str << endl;return 10;}int virtual Teacher26basevirtualfunc(double dou) {cout << "Teacher26sonvirtualfunc called dou = " << dou << endl;int inttemp = (int)dou;return inttemp;}int virtual Teacher26basevirtualfunc2(double dou) {cout << "Teacher26sonvirtualfunc2 called dou = " << dou << endl;int inttemp = (int)dou * 2;return inttemp;}};void quanjuTeacher26() {cout << "全局quanjuTeacher26 called" << endl;
}void main() {//旧的回忆,对于全局函数的访问,可以通过定义一个函数指针类型//1.定义一个 函数指针 类型 QuanJuFuncTypetypedef void(*QuanJuFuncType)();//2.使用类型 定义 变量 func,由于是一个指针,QuanJuFuncType func = quanjuTeacher26;func();//3.直接定义一个函数指针类型void(*jutiQuanJuFunc)();jutiQuanJuFunc = &quanjuTeacher26;//jutiQuanJuFunc = quanjuTeacher26;//对于全局函数,前面加不加&都一样jutiQuanJuFunc();//对于类成员函数,我们可以这么干吗,是的,也可以这么干,不过前面要加上类命名空间void(Teacher26son::*jutiQuanJuFuncTeacher26son)();jutiQuanJuFuncTeacher26son = &Teacher26son::Teacher26sonfun;//但是在类成员指针的赋值的时候,一定要加上&//调用时,需要一个类对象,Teacher26son tea;(tea.*jutiQuanJuFuncTeacher26son)();//注意调用的时候,是(对象.*xxx)(),这是为了和类成员函数 tea.xxx区分//如果是静态成员函数int(*staticfunc)(string tempvalue) = &Teacher26son::Teacher26sonstaticfunc;//静态成员也是一样的,不同的是,需要加上类命名空间,注意的是和 全局函数一样,前面加不加 &都可以,加不加& 是历史原因。int result = staticfunc("abc");cout << "result = " << result << endl;//5.如果是虚函数呢?由于虚函数也是依赖于 类对象调用的,也就是也需要this指针// 虚函数是如下的两个:int virtual Teacher26sonvirtualfunc(double dou) // int virtual Teacher26sonvirtualfunc2(double dou) //5.1 定义如下int(Teacher26base::*vir)(double tempdou) = &Teacher26base::Teacher26basevirtualfunc;//5.2 使用 类指针 调用 虚函数指针类型Teacher26base *ptea26 = new Teacher26son();result = (ptea26->*vir)(3.14);00A6F46E mov esi,esp
00A6F470 sub esp,8
00A6F473 movsd xmm0,mmword ptr [__real@40091eb851eb851f (0A79F78h)]
00A6F47B movsd mmword ptr [esp],xmm0
00A6F480 mov ecx,dword ptr [ptea26]
00A6F483 call dword ptr [vir]
00A6F486 cmp esi,esp
00A6F488 call __RTC_CheckEsp (0A61596h)
00A6F48D mov dword ptr [result],eaxcout << "result1 = " << result << endl;//5.3 使用 类对象 调用 虚函数指针类型Teacher26base tea26;result = (tea26.*vir)(78.8);00A6F4D7 mov esi,esp
00A6F4D9 sub esp,8
00A6F4DC movsd xmm0,mmword ptr [__real@4053b33333333333 (0A79F98h)]
00A6F4E4 movsd mmword ptr [esp],xmm0
00A6F4E9 lea ecx,[tea26]
00A6F4EC call dword ptr [vir]
00A6F4EF cmp esi,esp
00A6F4F1 call __RTC_CheckEsp (0A61596h)
00A6F4F6 mov dword ptr [result],eax cout << "result2 = " << result << endl;//5.4 通过 类指针 正常调用 虚函数ptea26->Teacher26basevirtualfunc(3.14);00A6F538 mov esi,esp
00A6F53A sub esp,8
00A6F53D movsd xmm0,mmword ptr [__real@40091eb851eb851f (0A79F78h)]
00A6F545 movsd mmword ptr [esp],xmm0
00A6F54A mov eax,dword ptr [ptea26]
00A6F54D mov edx,dword ptr [eax]
00A6F54F mov ecx,dword ptr [ptea26]
00A6F552 mov eax,dword ptr [edx]
00A6F554 call eax
00A6F556 cmp esi,esp
00A6F558 call __RTC_CheckEsp (0A61596h) //5.5 通过 类对象 正常调用 虚函数tea26.Teacher26basevirtualfunc(23);00A6F55D sub esp,8
00A6F560 movsd xmm0,mmword ptr [__real@4037000000000000 (0A79F88h)]
00A6F568 movsd mmword ptr [esp],xmm0
00A6F56D lea ecx,[tea26]
00A6F570 call Teacher26base::Teacher26basevirtualfunc (0A61442h)//5.6 通过类对象 正常调用普通函数tea26.Teacher26basefun();00A6F575 lea ecx,[tea26]
00A6F578 call Teacher26base::Teacher26basefun (0A61749h) //5.6 那么这个5.2, 5.3, 5.4有区别吗?debug看反汇编的代码cout << "断点在这里" << endl;
}
结论:
1. 不管是 类对象,还是 类指针 调用 虚函数指针 都是会走 虚函数指针 查找 虚函数表。
但是这个两种方式调用 虚函数的具体步骤 会有不同。
类指针 调用虚函数的时候,会使用 vptr 找虚函数表。
在使用函数指针调用成员虚函数的时候会使用到vcall。
//5.1 定义如下int(Teacher26base::*vir)(double tempdou) = &Teacher26base::Teacher26basevirtualfunc;//5.2 使用 类指针 调用 虚函数指针类型Teacher26base *ptea26 = new Teacher26son();result = (ptea26->*vir)(3.14);00A6F46E mov esi,esp
00A6F470 sub esp,8
00A6F473 movsd xmm0,mmword ptr [__real@40091eb851eb851f (0A79F78h)]
00A6F47B movsd mmword ptr [esp],xmm0
00A6F480 mov ecx,dword ptr [ptea26]
00A6F483 call dword ptr [vir]
00A6F486 cmp esi,esp
00A6F488 call __RTC_CheckEsp (0A61596h)
00A6F48D mov dword ptr [result],eaxcout << "result1 = " << result << endl;//5.3 使用 类对象 调用 虚函数指针类型Teacher26base tea26;result = (tea26.*vir)(78.8);00A6F4D7 mov esi,esp
00A6F4D9 sub esp,8
00A6F4DC movsd xmm0,mmword ptr [__real@4053b33333333333 (0A79F98h)]
00A6F4E4 movsd mmword ptr [esp],xmm0
00A6F4E9 lea ecx,[tea26]
00A6F4EC call dword ptr [vir]
00A6F4EF cmp esi,esp
00A6F4F1 call __RTC_CheckEsp (0A61596h)
00A6F4F6 mov dword ptr [result],eax
看到: result = (ptea26->*vir)(3.14); 通过 指向类虚函数指针的方法 调用该函数,也会有vcall的出现
00A6F4EC call dword ptr [vir] //在这一步的时候,按F11 就会发现004D187F jmp Teacher26base::`vcall'{0}' (04D776Eh) ,然后再F11,就会发现会走到这里:004D1799 jmp Teacher26son::Teacher26basevirtualfunc (04D7C60h)
2. 类指针 调用虚函数 会走 虚函数指针 查找 虚函数表
//5.4 通过 类指针 正常调用 虚函数ptea26->Teacher26basevirtualfunc(3.14);00A6F538 mov esi,esp
00A6F53A sub esp,8
00A6F53D movsd xmm0,mmword ptr [__real@40091eb851eb851f (0A79F78h)]
00A6F545 movsd mmword ptr [esp],xmm0
00A6F54A mov eax,dword ptr [ptea26]
00A6F54D mov edx,dword ptr [eax]
00A6F54F mov ecx,dword ptr [ptea26]
00A6F552 mov eax,dword ptr [edx]
00A6F554 call eax
00A6F556 cmp esi,esp
00A6F558 call __RTC_CheckEsp (0A61596h)
3. 类对象调用虚函数 和 类对象调用 普通函数 会有不同个,但是都不会走
//5.5 通过 类对象 正常调用 虚函数tea26.Teacher26basevirtualfunc(23);00A6F55D sub esp,8
00A6F560 movsd xmm0,mmword ptr [__real@4037000000000000 (0A79F88h)]
00A6F568 movsd mmword ptr [esp],xmm0
00A6F56D lea ecx,[tea26]
00A6F570 call Teacher26base::Teacher26basevirtualfunc (0A61442h)//5.6 通过类对象 正常调用普通函数tea26.Teacher26basefun();00A6F575 lea ecx,[tea26]
00A6F578 call Teacher26base::Teacher26basefun (0A61749h)
4. 类对象调用虚函数没有 继承。
发现是和有继承的情况下是一样的。都是5步完成。
public:int virtual Teacher27virtualfunc(double dou) {cout << "Teacher27virtualfunc called dou = " << dou << endl;int inttemp = (int)dou;return inttemp;}
};void main() {Teacher27 tea;tea.Teacher27virtualfunc(3.14);0028A37A sub esp,8
0028A37D movsd xmm0,mmword ptr [__real@40091eb851eb851f (0299F78h)]
0028A385 movsd mmword ptr [esp],xmm0
0028A38A lea ecx,[tea]
0028A38D call Teacher27::Teacher27virtualfunc (0281906h) cout << "end" << endl;
}
5. 在如下的代码中打印 虚函数的地址。
也会看到 vcall的使用
printf("Teacher26base:: %p\n",&Teacher26base::Teacher26basevirtualfunc);// 00E8F5C6 push offset Teacher26base::`vcall'{0
//}' (0E8187Fh)
//00E8F5CB push offset string "Teacher26base:: %p\n" (0E991E8h)
//00E8F5D0 call _printf(0E81091h)
//00E8F5D5 add esp, 8
总结:
什么时候会使用到vcall
在使用函数指针调用成员虚函数的时候会使用到vcall
三 vcall 在继承中的体现
int(Teacher26base::*vir)(double tempdou) = &Teacher26base::Teacher26basevirtualfunc;
int(Teacher26son::*vir1)(double tempdou) = &Teacher26son::Teacher26basevirtualfunc;
如上的两行代码,都是定义一个 函数指针,且是类的虚函数指针,到这里,我们应该有个意识,这两个里面都会使用到vcall代码段,vcall里面应该都存储的有 偏移,且由于Teacher26basevirtualfunc是第一个虚函数,因此偏移应该是0才对。
那么再调用的时候,我们一定要一个类指针,或者类引用
Teacher26base *ptea26 = new Teacher26son();
result = (ptea26->*vir)(3.14);
cout << "result1 = " << result << endl;
指针和引用由于是动态绑定,因此等号右边是啥,ptea26就会指向的是啥。
因此当前case下,this就是 Teacher26son,有了this,有了偏移,就能找到对应的真正的函数,调用一下就可以找到真正的函数地址了。