成员函数指针
成员函数指针
函数指针中的函数都是全局作用域内的函数,而成员函数指针指向的函数往往都是某个类的非静态成员函数。使用如下代码,编译器会报非法转换的错误。报这个错有俩个原因
- 首先类内的函数是在类作用域中的,而函数指针指向的函数是在全局作用域的。
- 成员函数有隐含的this指针参数而函数指针的函数却没有
class Foo {
void Fun()
{
cout << " Foo" << endl;
}
};
typedef void (*pFunc)();
pFunc ptr = &Foo::Fun; 错误
基于上面这个原因,我们需要定义成员函数指针去访问成员函数,而由于this指针的缘故所以成员函数的调用往往都伴随着相应的对象,C++ 定义了 .* 与 ->* 来通过使用成员函数指针去访问成员函数,由于.* 与 ->*的优先级比较低所以使用的时候往往需要加上大括号。
Return_Type (Class_Name::* pointer_name) (Argument_List);
class Foo {
void Fun()
{
cout << " Foo" << endl;
}
};
typedef void (*pFunc)();
typedef void(Foo::*pFooFunc)();
pFunc ptr = &Foo::Fun; 错误
pFooFunc ptr2 = &Foo::Fun; 正确
Foo obj;
Foo * p = new Foo;
(obj.*ptr2)(); //正确调用
(p->*ptr2)(); //正确调用
静态成员函数指针
由于静态成员函数的调用往往都是脱离于对象的,所以c++把它当做普通的函数来处理。如果我们使用一个成员函数指针去执行一个静态成员函数会报错。
#include <iOStream>
#include <string>
using namespace std;
class Foo {
public:
static int f(string str) {
cout << "Foo::f()" << endl;
return 1;
}
};
int main(int argc, char *argv[]) {
// int (Foo::*fptr) (string) = &Foo::f; // 错误
int (*fptr) (string) = &Foo::f; // 正确
(*fptr)("str"); // 调用 Foo::f()
}
成员函数指针类型转化
非虚函数情况下
基类与派生类之间的转化
基类与派生类的成员函数指针发生隐式转换有俩个前提
- public继承
- 俩个函数指针对应函数的返回值和参数类型必须一样
在这俩个前提下,基类与派生类的函数指针可以互相转化。(不像赋值兼容规则只是单向的基类到派生类)
#include <iostream>
class Foo {
public:
int f(char *c = 0) {
std::cout << "Foo::f()" << std::endl;
return 1;
}
};
class FooDervied : public Foo {
public:
int f(char *c = 0) {
std::cout << "FooDerived::f()" << std::endl;
return 1;
}
};
int main(int argc, char *argv[]) {
typedef int (Foo::*FPTR) (char*);
typedef int (FooDervied::*FDPTR) (char*);
FPTR fptr = &Foo::f;
FDPTR fdptr = &FooDervied::f;
fdptr = static_cast<int(FooDervied::*)(char*)>(fptr); // 正确,逆赋值兼容规则
fptr = static_cast<int(Foo::*)(char *)>(fdptr); //正确 双向转化
}
不同类之间的成员函数指针的强转
不同类型的成员函数指针之间可以通过强转调用,但是调用并不会生效,执行的还是原来逻辑的函数,如下图代码所示。这是因为函数的地址在编译期就确定了,那么这个时候即使强转指针变量中保存的函数地址也是不会改变的,所以执行的还是强转之前的函数逻辑,但是this指针确是调用对象的this指针。
比如下面代码中A对象通过强转B对象的成员函数指针,想去调用A类的成员函数。但是即使传递的形参都是A类函数的参数,但是结果却还是只能调用B类的成员函数,但是this指针却用的是A的this指针。那么这样就形成了一个怪异的调用,使用A类的对象去调用了B类的成员函数并且this指针却是A对象的this指针。
#include <iostream>
class Foo {
public:
void f(char *c = 0) {
std::cout << "Foo::f()" << std::endl;
std::cout << *(int *)this << std::endl;
}
};
class Bar {
public:
Bar()
:a_(0)
{}
void b() {
std::cout << "Bar::b()" << std::endl;
}
int a_;
};
int main(int argc, char *argv[]) {
typedef void (Foo::*FPTR) (char*);
typedef void (Bar::*BPTR) ();
FPTR fptr = &Foo::f;
BPTR bptr = &Bar::b;
Bar obj;
( obj.*(BPTR) fptr )(); // 虽然调用的参数是按bar::b 的函数传的,但是却调用的是 Foo::f()
return 0;
}
output:
Foo::f()
0
虚函数情况下
基类与派生类之间的转化
从下面的代码可以看出,在加入虚函数机制下,原本 Foo::f 的调用变为了 Dervied::f 函数的调用,这主要是因为虚函数的调用是通过this指针访问虚表,在通过 Dervied对象的this指针访问虚表的时候,Foo::f函数已被Dervied::f函数重写,所以调用的时候发生了多态,导致Dervied::f 函数被调用。
#include <iostream>
class Foo {
public:
virtual int f(char *c = 0) {
std::cout << "Foo::f()" << std::endl;
return 1;
}
};
class FooDervied : public Foo {
public:
virtual int f(char *c = 0) {
std::cout << "FooDerived::f()" << std::endl;
return 1;
}
};
int main(int argc, char *argv[]) {
typedef int (Foo::*FPTR) (char*);
typedef int (FooDervied::*FDPTR) (char*);
FPTR fptr = &Foo::f;
FDPTR fdptr = &FooDervied::f;
fdptr = static_cast<int(FooDervied::*)(char*)>(fptr); // 正确,逆赋值兼容规则
Dervied obj;
(obj.*fdptr)(1); // Dervied::f 调用
(obj.*fptr)(1);//Dervied::f 调用 并且发生了隐式转化
}
不同类之间成员函数指针的强转
如下原本在非虚函数情况下强转失效输出Dervied::f() 却变成了 Bar::b() ,还记得上面的强转并不生效吗?即使强转,调用的还是强转前的成员函数。但是在俩边成员函数指针所指向的成员函数都是虚函数的情况下,却可以成功发生强转想要的结果,也就是通过Derived类的成员函数指针却调用了Base的成员函数。
造成这个的主要原因在于对虚函数来讲,其成员函数指针保存的不是函数地址而是指向的虚函数在虚表中的偏移量。那么下面中的 (obj.*(BPTR)fptr)() 这段代码表示,虚表指针使用的是obj对象的虚表指针,偏移量使用的是fptr记录的偏移量,把它们俩个结合起来调虚函数。
虽然看起来强转调用的是bar::b ,但是fptr偏移量记录的是第二个函数的偏移量,所以结果实际调用的却是 bar::c 函数。
#include <iostream>
class Foo_base{
int a_;
};
class Foo {
public:
virtual int f(char *c=0) {
std::cout << "Foo::f()" << std::endl;
++a_;
return 1;
}
virtual void Fun()
{
std::cout << "Foo::Fun()" << std::endl;
}
int a_;
};
class Bar {
public:
virtual void b() {
std::cout << "Bar::b()" << std::endl;
}
virtual void c(int a) {
std::cout << "Bar::c()"<< a << std::endl;
}
};
class FooDerived : public Foo_base , public Foo {
public:
virtual int f(char *c = 0) {
std::cout << "FooDerived::f()" << std::endl;
return 1;
}
};
int main(int argc, char *argv[]) {
typedef void (FooDerived::*FPTR) ();
typedef void (Bar::*BPTR) ();
FPTR fptr = &FooDerived::Fun;
BPTR bptr = &Bar::b;
std::cout << fptr << std::endl; // 输出9
std::cout << bptr << std::endl; //输出1
Bar obj;
(obj.*(BPTR)fptr)();
return 0;
}
汇编分析
既然上面讲了(obj.*(BPTR)fptr)() 这段代码表示使用 obj对象的虚表指针与fptr记录的偏移量结合起来调用的虚函数,那么为什么这么说请看下面的汇编分析,下面的汇编是上面的代码去除了俩个输出语句的汇编结果
main:
pushq %rbp
movq %rsp, %rbp // 进入main函数
subq $64, %rsp // 使用栈指针开辟空间 开辟了64字节空间
movl %edi, -52(%rbp) //把main函数的参数压栈
movq %rsi, -64(%rbp)//把main函数的参数压栈
movq $9, -16(%rbp)// FPTR fptr = &FooDerived::Fun 的执行
movq $0, -8(%rbp)// 多开辟8字节用来记录对应的虚表指针在对象模型中的位置
movq $1, -32(%rbp)// BPTR bptr = &Bar::b 的执行
movq $0, -24(%rbp)// 记录 Bar类型中虚表指针位置在Bar对象模型中的位置
leaq -48(%rbp), %rax// 相当于 把 rbp-48的内容保存到 rax ,这里其实是Bar的地址
movq %rax, %rdi // rax 把 内容传递给 rdi ,那么rdi其实相当于this指针它是构造函数的第一个参数
call _ZN3BarC1Ev //调用Bar构造函数
leaq -16(%rbp), %rax // 取fptr的地址放入rax
movq (%rax), %rax // 去fptr的内容放到 rax 现在 rax内容为9
andl $1, %eax // rax -1 , rax内容为8
testb %al, %al // 测试eax的低8位是否为0
je .L8 // 为0 jmp 到L8 ,但是显然不为0,里面是9逻辑继续往下执行
leaq -16(%rbp), %rax // 逻辑走到这里了,继续将 &fptr 存入rax
movq 8(%rax), %rax // 相当于 *((char*)&(fptr)+8) 代码,也就是把Bar中虚表指针在对象模型的偏移量存到 rax
movq %rax, %rdx // 备份到 rdx中,偏移量为0,表示虚表指针在Bar初始处
leaq -48(%rbp), %rax // &bar 存到 rax
addq %rdx, %rax // 寻找 存放虚表指针的地址 , &bar + 0 的执行
movq (%rax), %rdx // *(&bar) 存到rdx ,相当于取虚表的地址存到rdx中
leaq -16(%rbp), %rax // 这里相当于 取 &fptr 放到 rax
movq (%rax), %rax // 取 fptr指向的虚函数偏移量 放入 rax中,现在rax 为9
subq $1, %rax // 对rax 减一,rax 内容为8
leaq (%rdx,%rax), %rax // rdx 存的是虚表地址,这里相当于 虚表地址向下偏移8字节然后存到rax中
movq (%rax), %rax // 对rax指向的空间解引用,这里相当于取到了虚表中第二个函数的地址,也就是Bar::c的地址
movq %rax, %rdx// 把 Bar::c 的地址存到 rdx中
jmp .L9
.L8:
leaq -16(%rbp), %rax
movq (%rax), %rdx
.L9:
leaq -16(%rbp), %rax // 取 &fptr 到 rax
movq 8(%rax), %rax // 取FooDervied 中虚表指针的在对象模型中的偏移量
movq %rax, %rcx // 取偏移量到 rcx 偏移量为0
leaq -48(%rbp), %rax // 取 bar地址到rax
addq %rcx, %rax // 通过 偏移量 与 &Bar 相加 取到 虚表指针变量的地址
movq %rax, %rdi // 把虚表变量的地址存到 rdi中,其实也就是Bar对象的this指针存到rdi里面然后调用 Bar::c
call *%rdx
movl $0, %eax
leave
ret
上面这段汇编,为什么偏移量是 1 和 9 ,这为了区分 ptr指针指向的是 NULL 指针还是 保存的是偏移量为0。如果都存0 ,程序没法判断到底是偏移量为0 还是 保存的是NULL 指针,所以都往上调整了1
第二个 每个ptr前8个字节都保存了一个0,这个0我认为是虚表指针在对象模型中的位置。但是修改了下代码,查看了下汇编还是虚表指针编译器自己会索引相关偏移量找到根本不需要这个值。所以我认为它不是整个派生类对象中某个虚表的偏移量而是单个对象模型(单个基类)中虚表的偏移量,比如一个类(没有继承)中虚表可能在底部也可能在顶部。
总结
- 虚函数的情况下,成员函数指针保存的是虚函数在虚表中的偏移量
- 成员函数指针的调用本质上是使用obj对象的this指针,ptr中的函数地址或虚函数偏移量。如果是虚函数的成员指针,那么调用时通过obj对象this指针找到相应虚表根据偏移量调用。非虚函数根据ptr的函数地址,obj的this指针进行调用。
相关阅读
1、成员变量和局部变量简述 在Java语言里,根据定义变量位置的不同,可以将变量分成两大类:成员变量(存在于堆内存中,和类一起创建)和局
很多人都认为java中属性就是成员变量,其实不然;那他们有什么区别呢?让我们通过以下代码来理解: public class Person { private Str
完整源码下载 点击下载完整源码如果对你有用,请给个Star,你的支持,是我最大的动力 1 获取所有QQ 这里主要通过抓包,抓取QQ空间中的
谈营销光谈战略战术不行,再牛逼的方案也需要依靠人去落实,才能得到更好的结果,如何找到更多优秀的人来合作也是我的日常思考范畴之一
日前,腾讯AI加速器二期成员名单正式曝光,清锋时代CEO姚志锋、享阅教育CEO赵梓淳、通用微科技CEO王云龙、猫酷科技CEO王永、一面网络