深入C++成员函数及虚函数表

本文深入探讨了C++中成员函数的内存布局,包括静态与非静态成员函数指针的差异。重点讲解了虚函数表的工作原理,如何通过成员函数指针解析对象内存,以及在多继承下的复杂情况。通过对C++对象内存、成员函数指针和虚函数表的分析,揭示了C++如何处理对象成员和虚函数调用的细节。
摘要由CSDN通过智能技术生成

深入C++成员函数及虚函数表

大家好!这次逗比老师要和大家分享的是C++中的成员函数,我们会深入解析C++处理对象成员时的方式,还有关于成员函数指针、虚函数表等问题的深入研究。

简单对象的内存布局

在介绍其他问题之前,咱们先来研究一下,一个C++对象在内存中的存储布局。首先,如果是POD类型的对象,那么布局方式和C中的结构体相同,按照定义的顺序排布所有成员,并且会在适宜的时候进行内存对齐。例如下面例程我们写了一个简单的用来打印一个对象内部结构(十六进制方式)的代码:

#include <iostream>
#include <iomanip>

class C1 {
public:
    char m1;
    // pad 7 Bytes
    uint32_t m2[5];
};

template <typename T>
void ShowMemory(const T &ref, const std::string &name = "no name") {
    std::cout << "=====begin=====" << std::endl;
    auto base = reinterpret_cast<const uint8_t *>(&ref);
    auto size = sizeof(typename std::remove_reference<T>::type);
    
    std::cout << "name: " << name << std::endl;
    std::cout << "size: " << size << " Byte(s)" << std::endl;
    
    std::cout << "  |";
    for (int i = 0; i < 16; i++) {
        std::cout << std::setw(2) << std::hex << i << "|";
    }
    std::cout << std::endl;
    
    int i = 0;
    for (const uint8_t *ptr = base; ptr < base + size; ptr++) {
        if (i % 16 == 0) {
            std::cout << " " << std::hex << i / 16 << "|";
        }
        i++;
        std::cout << std::setw(2) << std::setfill('0') << std::hex << uint16_t{*ptr} << "|";
        if (i % 16 == 0) {
            std::cout << std::endl;
        }
    }
    std::cout << std::endl << "======end======" << std::endl;
}

#define SHOW(obj) ShowMemory(obj, #obj)

int main(int argc, const char * argv[]) {
    C1 c1;
    c1.m1 = 44;
    c1.m2[0] = 88;
    SHOW(c1);
    return 0;
}

示例的输出结果如下:

=====begin=====
name: c1
size: 24 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|2c|00|00|00|58|00|00|00|00|00|00|00|00|00|00|00|
 0|00|00|00|00|00|00|00|00|
======end======

相信这一部分大家都很熟悉了,不再啰嗦。

接下来我们要研究的是,非POD类型中,C++到底都“偷偷”在对象中做了什么。首先我们先看一下简单继承的方式,假如有B继承自A,那么B中是如何布局的呢?请看例程:

// ShowMemory相关内容省略,参考之前的例程即可
class A {
public:
    uint16_t m1, m2;
    uint8_t m3;
};

class B : public A {
public:
    uint16_t m4;
};

int main(int argc, const char * argv[]) {
    B b;
    b.m1 = 0x1234;
    b.m2 = 0x4567;
    b.m3 = 0xef;
    b.m4 = 0x789a;
    SHOW(b);
    return 0;
}

输出如下:

=====begin=====
name: b
size: 8 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|34|12|67|45|ef|00|9a|78|
======end======

看得出,地址0x00和0x01是m1,0x02和0x03是m2,0x04是m3,然后0x05是一个字节的内存对齐。也就是说,0x00~0x05其实就是一个完整的A类型对象,也就是父类集成到的内容。然后0x06和0x07是m4,也就是后面排的是子类的扩展内容。

我们知道,数据类型仅仅是处理数据的方式,然而数据本身都只是相同的二进制数罢了,如果知道了一个对象的实际内存布局,那么我们其实也可以反过来直接构造一个对象。请看下面历程:

// 省略A和B的定义,请参考上面历程
int main(int argc, const char * argv[]) {
    // 直接构造二进制数据
    uint8_t data[] = {0x34, 0x12, 0x67, 0x45, 0xef, 0x00, 0x9a, 0x78};
    // 用对象方式解析数据
    B *ptr = reinterpret_cast<B *>(data);
    // 尝试读取m2和m4
    std::cout << std::hex << ptr->m2 << ", " << ptr->m4 << std::endl;
    return 0;
}

输出结果如下:

4567, 789a

看起来,通过二进制数据来反向构造对象,到目前为止还是可行的。

请大家先消化上面的内容,我们再一起来往下看。

成员函数指针

我们了解到,C++其实本质还是C语言,只不过做了很多语法糖,使得语法更为高级,更加适合用高等思维去设计。但语法并不改变语义,C++的高级语法其实都可以等价翻译为C语言语法,例如函数重载,其本质是编译器在函数名前后加上了前后缀用以区分的。

那么成员函数也是一样的,虽然我们把它写在类当中,但本质上,它仍是函数,和普通函数一样,它的指令也会存入一块内存,我们也可以设法找到这片内存。

先来看一个静态成员函数的例子:

class C1 {
public:
    static void test() {std::cout << "C1::test" << std::endl;}
};

int main(int argc, const char * argv[]) {
    // 静态成员函数指针
    void (*pf1)() = C1::test;
    // 打印地址的值
    std::cout << reinterpret_cast<void *>(pf1) << std::endl;
    // 直接调用
    pf1();
    return 0;
}

执行结果:

0x100003074
C1::test

这里的0x100003074其实就是C1::test函数保存的跳转地址。所以这里我们看到,其实静态成员函数就是普通的函数而已,语义上来说,和写在外面的函数没什么区别,这里的类名其实与命名空间几乎无异了。只是语法上来说,它在C1内,那我们自然是要写和C1相关的内容。

但如果是非静态成员函数呢?请看例程:

class C1 {
public:
    int m1;
    void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
};

int main(int argc, const char * argv[]) {
    // 定义对象
    C1 c1;
    // 成员赋值
    c1.m1 = 1234;
    // 成员函数指针
    void (C1::*pf1)(int) = &C1::test;
    // pf1的长度
    std::cout << sizeof(pf1) << std::endl;
    // 调用
    (c1.*pf1)(5);
    return 0;
}

输出如下:

16
C1::test, a=5, m1=1234

非静态成员函数指针的用法大家应该不陌生,但似乎让我们很诧异的是这个16,照理讲,在64位环境下,指针的大小都是8字节,可pf1却很个性地来了个16,这是为什么?

先不急,我们还是把pf1的二进制内容先打印出来看看:

int main(int argc, const char * argv[]) {
    C1 c1;
    c1.m1 = 1234;
    void (C1::*pf1)(int) = &C1::test;
    
    SHOW(pf1);
    
    return 0;
}

/* 输出结果:
=====begin=====
name: pf1
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|80|28|00|00|01|00|00|00|00|00|00|00|00|00|00|00|

======end======
*/

后面一长串都是0,而前面这部分看上去有点像是地址,不免让人猜测,是否前8字节才是真正的函数地址呢?我们来做个实验便好:

class C1 {
public:
    int m1;
    void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
    void test2() {std::cout << "C1::test2" << std::endl;}
};

int main(int argc, const char * argv[]) {
    C1 c1;
    c1.m1 = 1234;
    void (C1::*pf1)(int) = &C1::test;
    void (C1::*pf2)() = &C1::test2;
    
    auto test_ppf = reinterpret_cast<void (**)()>(&pf2);
    (*test_ppf)();
    
    return 0;
}

/*输出结果:
C1::test2
*/

(这里怕有些读者看晕,我稍微多解释一下。由于void (C1::*)()这种类型是16字节长度的,并不是普通的指针,因此我们不能直接转换成void *或普通函数指针。而我们现在要做的是把pf2的前8个字节拿出来,再按照一个普通的函数指针去读取。因此,我们先取pf2的地址,然后把这个地址按照二级指针进行解指针,得到一个指针,而这个指针的值其实就是pf2的前8个字节。所以刚才那几行代码如果详细一点来写就是这样:

void (C1::*pf2)() = &C1::test2;
void *ppf2 = reinterpret_cast<void *>(&pf2); // ppf2是pf2的地址
// 但此时ppf2应该是个二级指针,解指针后应该得到一个8字节的数
uintptr_t pf_addr = *reinterpret_cast<uintptr_t *>(ppf2);
// pf_addr的值,应该就是我们想到的函数的地址的值了,还需要再转换成函数指针类型
void (*pf)() = reinterpret_cast<void (*)()>(pf_addr);
// 按照函数方式调用
pf();

转义之后相信大家应该更容易看得懂了。)

果然如此,前8个字节解出来的地址,还真的是个可调用的函数。但到目前为止我们都没有出现任何问题,是因为C1::test2是无参的,并且内部也与成员变量无关。如果把相同的操作用给C1::test的话就会core dump,有兴趣的读者可以自行尝试。

既然是非静态的成员函数,我们都只要正常操作都是用对象来调用的,这个对象会作为函数的一个隐藏参数(也就是this)来传入,所以,我们其实少传了一个this参数。例如C1::test的操作应该是这样的:

class C1 {
public:
    int m1;
    void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << m1 << std::endl;}
};

int main(int argc, const char * argv[]) {
    C1 c1;
    c1.m1 = 1234;
    void (C1::*pf1)(int) = &C1::test;
    
    // 对象作为第一个参数传进去,其他参数跟在后面,即可转换为普通函数
    auto test_ppf = reinterpret_cast<void (**)(C1 *, int)>(&pf1);
    (*test_ppf)(&c1, 5);
    // 引用其实本质是指针的语法糖,所以也可以改写成引用类型
    auto test_ppf2 = reinterpret_cast<void (**)(C1 &, int)>(&pf1);
    (*test_ppf2)(c1, 5);
    
    return 0;
}

/*调用结果:
C1::test, a=5, m1=1234
C1::test, a=5, m1=1234
*/

看来确实是这样了,pf1的前8个字节真的就是一个普通的函数,只不过有一个隐藏的this参数罢了。我们也就把obj->func(arg)的形式,成功改写成了func(obj ,arg)的形式。那后8个字节到底是干什么的呢?先别急,后面就知道了。在解释后8个字节的作用之前,我们不妨先换换脑子,看另一个问题。

成员变量的本质是内存偏移量和数据类型的记录

这个小标题可能会让读者有点摸不着头脑,不过没关系,很快你就会明白,我们先来看一段例程:

class C1 {
public:
    int m1;
    // 为了方便观察,这里我用十六进制打印m1
    void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << std::hex << m1 << std::endl;}
};

int main(int argc, const char * argv[]) {
    // 这是随意写的一段数据
    uint8_t data[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc};
    
    void (C1::*pf1)(int) = &C1::test;
    // 注意这里,我将隐藏参数改为了void *
    auto test_ppf = reinterpret_cast<void (**)(void *, int)>(&pf1);
    auto f = *test_ppf;
    f(data, 8);
    
    return 0;
}
/*输出结果:
C1::test, a=8, m1=78563412
*/

这通操作相当大胆,f是C1::test所对应的实际函数,照理说,第一个参数是要传一个C1类型的对象的,但此时我传入了一个随意的二进制数据,程序竟然可以正常运行。并且我们观察运行结果,0x78563412正好是data的前4个字节。这也就是说,程序把data当做了C1类型来处理,取的m1,就是取这个对象(或数据)的前4个字节,并且当做整数来处理。

为了验证这个说法,我们不妨再多定义几个变量:

class C1 {
public:
    int m1;
    char m2;
    short m3;
    void test(int a) {std::cout << "C1::test, a=" << a << ", m1=" << std::hex << m1 << ", m2=" << m2 << ", m3=" << m3 << std::endl;}
};

int main(int argc, const char * argv[]) {
    // 这是随意写的一段数据
    uint8_t data[] = {0x12, 0x34, 0x56, 0x78, 0x3c, 0xbc, 0x11, 0xaa, 0xcc, 0x55};
    
    void (C1::*pf1)(int) = &C1::test;
    // 注意这里,我将隐藏参数改为了void *
    auto test_ppf = reinterpret_cast<void (**)(void *, int)>(&pf1);
    auto f = *test_ppf;
    f(data, 8);
    
    return 0;
}

/*输出结果:
C1::test, a=8, m1=78563412, m2=<, m3=aa11
*/

没问题,成员都是按照对首地址的偏移,以及定义的类型来解析的,比如这里m2,应当取的是0x3c所对应的ASCII码,自然是'<'。

那么此时我们在回头看一眼这一节的小标题,有没有恍然大悟呢?

虚函数表

我们再来看看,如果一个类(或父类)拥有虚函数,会变成什么样。请看下面例程:

// 省略SHOW相关代码,请参考前面例程
class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << m1 << std::endl;}
};

int main(int argc, const char * argv[]) {
    C1 c1;
    SHOW(c1);
    
    return 0;
}
/*输出结果:
=====begin=====
name: c1
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|38|40|00|00|01|00|00|00|34|12|00|00|00|00|00|00|

======end======
*/

看起来m1跑到了0x08的位置,那么0x00~0x07位置应当就是虚函数表指针了。将这个指针解开以后,就可以得到虚函数表,虚函数表其实就是一个指针数组,每一个元素指向一个函数。为了验证这个说法,我们不妨再做个实验,请看例程:

class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << std::hex << m1 << std::endl;}
    virtual void test2() {std::cout << "test2" << std::endl;}
};

int main(int argc, const char * argv[]) {
    C1 c1;
    // 由于虚函数表指针是最初始的成员,偏移量为0,所以和对象首地址相同
    void *pvfl = static_cast<void *>(&c1);
    // 解pvfl应当得到一个数组,但是由于无法确定数组大小(也就是虚函数个数),因此用数组元素指针偏移来完成
    void **vfl = *static_cast<void ***>(pvfl); // vfl是虚函数表,也就是个数组,里面的元素都是指针,所以vf1是void **类型,而pvfl是这个数组的指针,所以pvf1是void ***类型(如果实在想不通,把最里面的一层void *定义为func_t,vfl就是func_t[]类型,然后pvfl就是func_t (*)[]类型,所以*pvfl就是func_t[],再把数组替换成指针,把func_t替换成void *得到前面代码)
    // 尝试取出第一个元素
    void *vf1 = vfl[0];
    // 将这个元素转化为函数指针,然后调用
    void (*f1)(C1 &) = reinterpret_cast<void (*)(C1 &)>(vf1);
    f1(c1);
    
    return 0;
}
/*调用结果:
1234
*/

我们成功通过虚函数表访问到了成员函数。验证一下,我们来尝试取出test2对应函数地址:

class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << std::hex << m1 << std::endl;}
    virtual void test2() {std::cout << "test2" << std::endl;}
};

int main(int argc, const char * argv[]) {
    C1 c1;
    void *pvfl = static_cast<void *>(&c1);
    void **vfl = *static_cast<void ***>(pvfl);
    void *vf2 = vfl[1];
    void (*f2)(C1 &) = reinterpret_cast<void (*)(C1 &)>(vf2);
    f2(c1);
    
    return 0;
}

/*调用结果:
test2
*/

没有问题,看来虚函数表,就是普通函数指针的指针,正常按照指针大小偏移即可。

虚函数指针及其调用过程

刚才我们用通过手动来控制指针偏移,找到了对应的虚函数并调用。编译器也可按照同样的方式,在成员定义列表中找到虚函数的位置,数出它是第几个,然后去虚函数表中找。但倘若我把虚函数的函数指针单独拿出来,该怎么办呢?(因为此时没法通过变量名来判断这是第几个虚函数了。)玄机,就在函数指针当中。

请看下面例程:

// 省略SHOW相关实现,请参考前面例程
class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << std::hex << m1 << std::endl;}
    virtual void test2() {std::cout << "test2" << std::endl;}
};

int main(int argc, const char * argv[]) {
    void (C1::*pf1)() = &C1::test;
    SHOW(pf1);
    
    return 0;
}

/*调用结果:
=====begin=====
name: pf1
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
*/

我们可以看到,虚函数的函数指针中保存得不再是实际的函数地址(因为实际的保存在虚函数表中),而是字节的偏移量,注意这里偏移量是1起始,因此实际在虚函数表中的偏移量比这个数值少1(主要是由于0用来表示空指针了)。

验证一下,我们取test2即可:

class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << std::hex << m1 << std::endl;}
    virtual void test2() {std::cout << "test2" << std::endl;}
    virtual void test3() {}
};

int main(int argc, const char * argv[]) {
    void (C1::*pf2)() = &C1::test;
    SHOW(pf2);
    
    return 0;
}
/*调用结果:
=====begin=====
name: pf2
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
*/

OK,如果有继承关系会怎样呢?

class C1 {
public:
    int m1 = 0x1234;
    virtual void test() {std::cout << std::hex << m1 << std::endl;}
    virtual void test2() {std::cout << "test2" << std::endl;}
};

class C2 : public C1 {
public:
    virtual void test3() {}
};


int main(int argc, const char * argv[]) {
    // 这里一定不可以用auto,因为C2中没有override这个函数,所以auto会推导出void (C1::*)()而不是void (C2::*)()
    void (C2::*pf1)() = &C2::test;
    SHOW(pf1);
    
    void (C2::*pf2)() = &C2::test2;
    SHOW(pf2);
    
    void (C2::*pf3)() = &C2::test3;
    SHOW(pf3);
    
    return 0;
}

/*执行结果:
=====begin=====
name: pf1
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
=====begin=====
name: pf2
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
=====begin=====
name: pf3
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|11|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
*/

也就是说,继承时,父类的虚函数表也会继承过来,新的虚函数会续写在父类的虚函数表之后。

多继承和那神秘的高8字节

单继承的,虚函数表顺延续写看起来理所应当,可多继承呢?

多继承时,C++会将第一个继承类作为主父类,而其他的父类虚函数表将单独继承,不再合并。也就是说,如果一个类有N个父类的话,就会有N个虚函数表。

为了验证,请看例程:

struct A {
    uint8_t pad[4] {1, 2, 3, 4};
    virtual void f1() {}
};
struct B {
    uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
    virtual void f2() {}
};
struct C : A, B {};

int main(int argc, const char * argv[]) {
    C c;
    SHOW(c);
    
    return 0;
}

/*执行结果:
=====begin=====
name: c
size: 32 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|50|40|00|00|01|00|00|00|01|02|03|04|00|00|00|00|
 1|68|40|00|00|01|00|00|00|01|02|03|04|05|06|07|08|

======end======
*/

可以看到,0x00~0x07是一个虚函数表指针,也是主的(C和A拼接后的),而0x10~0x17是另一个虚函数表(从B直接继承下来的),读者可以自行验证该说法。

接下来我们做一个操作,在三个类中的函数里分别打印出this,请看例程:

struct A {
    uint8_t pad[4] {1, 2, 3, 4};
    virtual void f1() {std::cout << "f1, this=" << this << std::endl;}
};
struct B {
    uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
    virtual void f2() {std::cout << "f2, this=" << this << std::endl;}
};
struct C : A, B {
    int m = 5;
    virtual void f3() {std::cout << "f3, this=" << this << std::endl;}
};

int main(int argc, const char * argv[]) {
    C c;
    c.f1();
    c.f2();
    c.f3();
    SHOW(c);
    
    return 0;
}
/*调用结果:
f1, this=0x16fdff448
f2, this=0x16fdff458
f3, this=0x16fdff448
=====begin=====
name: c
size: 32 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|50|40|00|00|01|00|00|00|01|02|03|04|00|00|00|00|
 1|70|40|00|00|01|00|00|00|01|02|03|04|05|06|07|08|
 2|05|00|00|00|00|00|00|00|

======end======
*/

this指针的不同,其实也证实了上面的说法,A和C中函数都是正常的this(实际对象的首地址),而B中的函数打印出的this却发生了偏移。其实,C++的多继承,从第二个父类开始,就会转换成类的组合来处理。相当于在C类中先放了一个B的对象,因此我们观察对象c的内存布局,首先0x00~0x0f是从A类继承来的内容,然后0x10~0x1f是一个完整的B,最后0x20~0x27是C中新增的成员。

由于C的虚函数直接续写在了从A继承来的虚函数表后面,因此,这两个类中的虚函数传入的this都是对象的首地址,而B类中的虚函数的this则要传入C类中B类继承来位置的首地址,可以看得出偏移量是0x10,正好上面验证f2的this比f1和f3的this向后偏移了0x10。

现在我们再来打印一下三个函数指针:

struct A {
    uint8_t pad[4] {1, 2, 3, 4};
    virtual void f1() {std::cout << "f1, this=" << this << std::endl;}
};
struct B {
    uint8_t pad[8] {1, 2, 3, 4, 5, 6, 7, 8};
    virtual void f2() {std::cout << "f2, this=" << this << std::endl;}
};
struct C : A, B {
    int m = 5;
    virtual void f3() {std::cout << "f3, this=" << this << std::endl;}
};

int main(int argc, const char * argv[]) {
    void (C::*p1)() = &C::f1;
    SHOW(p1);
    void (C::*p2)() = &C::f2;
    SHOW(p2);
    void (C::*p3)() = &C::f3;
    SHOW(p3);
    return 0;
}
/*调用结果:
=====begin=====
name: p1
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|01|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
=====begin=====
name: p2
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|01|00|00|00|00|00|00|00|10|00|00|00|00|00|00|00|

======end======
=====begin=====
name: p3
size: 16 Byte(s)
  | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| a| b| c| d| e| f|
 0|09|00|00|00|00|00|00|00|00|00|00|00|00|00|00|00|

======end======
*/

终于,成员函数指针神秘的高8字节作用解开了,就是这个this的偏移量,这里的0x10表示要向后偏移16字节。这就是成员函数指针是2个指针长度的原因所在了,第8字节是函数指针,高8字节是this的偏移量。

总结,完整的虚函数指针的调用方式如下:

1.取低8字节,表示虚函数表的偏移量

2.取高8字节,表示this的偏移量

3.根据this偏移量找到虚函数表

4.根据虚函数表偏移量找到函数

5.将调用者向后偏移对应的位置,作为实际调用者,传入函数的第一个参数中

例如:(obj.*vf1)() 【obj是对象,vf1是一个虚函数指针】

1.取vf1低8字节,记为f1

2.取vf2高8字节,记为adj

3.将&obj向后偏移adj字节,这是虚函数表指针,记为vpfl

4.vpfl向后偏移f1 * 指针大小,这是实际的函数指针,记为rf

5.调用rf,第一个参数是&obj偏移adj字节,其他参数递补。

结语

C++确实很难,因为它用了很基础的C作为底层支撑,却提供了很多高级的语法和功能,但如果我们可以把握本质,揭开它神秘面纱以后,发现其实也不过如此。

关于C++成员函数指针的相关问题就讲解到这里,如果读者有疑问,欢迎留言!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

borehole打洞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值