成员函数指针

成员函数指针

  函数指针中的函数都是全局作用域内的函数,而成员函数指针指向的函数往往都是某个类的非静态成员函数。使用如下代码,编译器会报非法转换的错误。报这个错有俩个原因

  1. 首先类内的函数是在类作用域中的,而函数指针指向的函数是在全局作用域的。
  2. 成员函数有隐含的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()
}
成员函数指针类型转化
非虚函数情况下
基类与派生类之间的转化

  基类与派生类的成员函数指针发生隐式转换有俩个前提

  1. public继承
  2. 俩个函数指针对应函数的返回值和参数类型必须一样

  在这俩个前提下,基类与派生类的函数指针可以互相转化。(不像赋值兼容规则只是单向的基类到派生类)

#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我认为是虚表指针在对象模型中的位置。但是修改了下代码,查看了下汇编还是虚表指针编译器自己会索引相关偏移量找到根本不需要这个值。所以我认为它不是整个派生类对象中某个虚表的偏移量而是单个对象模型(单个基类)中虚表的偏移量,比如一个类(没有继承)中虚表可能在底部也可能在顶部。

总结
  1. 虚函数的情况下,成员函数指针保存的是虚函数在虚表中的偏移量
  2. 成员函数指针的调用本质上是使用obj对象的this指针,ptr中的函数地址或虚函数偏移量。如果是虚函数的成员指针,那么调用时通过obj对象this指针找到相应虚表根据偏移量调用。非虚函数根据ptr的函数地址,obj的this指针进行调用。
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
成员函数指针可以作为类型使用,可以用它来定义函数指针变量、成员变量、函数参数等。 例如,假设有一个类`MyClass`,其中有一个成员函数`int add(int a, int b)`,可以定义一个函数指针类型`FuncPtr`,如下所示: ``` class MyClass { public: int add(int a, int b) { return a + b; } }; using FuncPtr = int (MyClass::*)(int, int); // 定义函数指针类型 void func(FuncPtr pFunc, MyClass& obj, int a, int b) { int result = (obj.*pFunc)(a, b); // 调用成员函数 // do something with result } int main() { MyClass obj; FuncPtr pFunc = &MyClass::add; // 定义函数指针变量 func(pFunc, obj, 1, 2); // 调用函数 return 0; } ``` 在上面的代码中,首先定义了一个`MyClass`对象`obj`,然后定义了一个成员函数指针类型`FuncPtr`,并使用`using`关键字定义了一个别名。接着定义了一个函数`func`,其中有一个参数是`FuncPtr`类型的成员函数指针变量`pFunc`。在`main`函数中,将`&MyClass::add`赋值给`pFunc`,并调用`func`函数,传入`pFunc`、`obj`和参数1、2。 需要注意的是,在定义成员变量时,需要使用指向成员函数指针类型来定义,例如: ``` class MyClass { public: int add(int a, int b) { return a + b; } private: int (MyClass::*pFunc)(int, int); // 定义成员变量 }; ``` 在定义函数参数时,需要使用指向成员函数指针类型来定义,例如: ``` class MyClass { public: int add(int a, int b) { return a + b; } }; void func(int (MyClass::*pFunc)(int, int), MyClass& obj, int a, int b) { int result = (obj.*pFunc)(a, b); // 调用成员函数 // do something with result } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值