C++ 多态之虚函数表并通过虚函数表调用虚函数

一 前言引入

  虚函数在多态中可以说非常重要,因此关于虚函数的底层实现,我们有必要了解。首先我们来看以下代码。

class Animal {
public :
    Animal() = default;
    Animal(int x, double y) : x(x), y(y) {}
    void run() {
        cout << "I don't know how to run" << endl;
    }

private :
    int x;
    double y;
};

class Dog : public Animal {
public : 
    Dog() : Animal() {};
    Dog(int x, double y) : Animal(x, y) {}
    void run() {
        cout << "I can run with four legs" << endl;
    }
};


int main() {
    cout << "sizeof(Animal): " << sizeof(Animal) << endl;
    cout << "sizeof(Dog): " << sizeof(Dog) << endl;
    return 0;
}

  如代码所示,定义了父类Animal,子类Dog公有继承自Animal类,现在我们输出两个类实例化出来的对象在内存中的大小,代码输出如下:

在这里插入图片描述
  关于输出结果大小为16字节,以下有几点相关的说明:

  1. 这个答案显然是正确的,并且这个大小是成员变量在内存中的大小,不包含成员方法在内存中的大小
  2. 若各位看官不理解为什么对象大小不是4 + 8 = 12个字节,而是16个字节,建议大家上网搜索如何判断结构体的大小
  3. 对于某一个类,实例化对象时,若对每一个对象都为其成员变量和成员方法分配内存,这样显然是不好的,因为这些从同一个类实例化而来的对象中的成员方法都一样,为此在C++中,每一个对象分配的内存空间在中只有成员变量,而成员方法是另外开辟一片公共的内存空间,所有对象都可以访问并调用其中的方法。

  现在我对原来的代码进行一些修改,将run()方法变为虚函数(修改较小,看官可自行操作,这里就不再粘贴代码了),再次运行代码,输出的结果会是什么呢?

在这里插入图片描述
  我们会发现,此时大小变为24个字节,相对于之前的16个字节增加了8个字节,那为什么会这样呢?是因为引入了一个虚函数,对象所占内存大小就会增加8字节吗?如果真是这样,那在类中再定义一个虚函数,大小是不是会变成30呢?如果不是这样,那是别的什么原因呢?
  首先前者猜想肯定是错误的,我们可以在类中多定义几个虚函数,之后输出大小,会发现增加虚函数个数后对象后的大小并没有变化。这样的话,就是说,类中定义虚拟函数后,相比没有定义虚函数,增加的大小恒为8字节那着8字节会是什么呢?我们平时编程中什么东西的大小是8字节,然后放在此处有些依据呢? 答案就是“指针” ,在64位操作系统中,任意类型的指针变量的大小都是8字节。指针既然是一个地址,那是谁的地址呢?答案就是“虚函数表在内存中的地址”
  若存在虚函数时,则对象在内存中头8个字节存储的就是虚函数表的地址,此虚函数表是和类挂钩的,一个类就对应一张虚函数表,换句话说,不管这个类实例化出多少个对象,这些对象在内存中头8个字节的内容都是一致的,都指向虚函数表在内存中的地址。


二 验证虚函数表的存在并调用

  知晓虚函数表的存在,那么我们尝试着来验证虚函数表的存在。

  1. 首先我们来验证,Dog类实例化出来的所有对象,其首部的8个字节的内容是否相同,主要代码如下:
// 从地址q开始,打印向后n个字节的内容
void output_raw_data(void *q, int n) {
    unsigned char *p = (unsigned char *)q;
    for (int i = 0; i < n; i++) {
        printf("%02x ", p[i]);  //不足2位的前面补0
    }
    printf("\n");
}

int main() {
    Dog a, b;
    output_raw_data(&a, sizeof(Dog));
    output_raw_data(&b, sizeof(Dog));
    return 0;
}

  代码输出如下:

在这里插入图片描述
  观察输出我们发现对象a和对象b前8个字节的内容是一致的,符合预期猜想。


  1. 通过对象中的虚函数表地址调用虚函数
      首先我们知道,虚函数表中存储的是虚函数在内存中的地址,也就是函数指针,我们假设函数指针的类型为func,那么指向这个虚函数表的指针类型为func *,而对象首部8字节中存储的就是虚函数表的地址,所以我们需要将对象的地址转换 为 func **
      这一点要是不好理解的话,可以参考字符串数组:每一个字符的类型是char,多个字符组成字符串,指向字符串的指针类型为char *,若干个字符串组合成字符串数组,所以指向字符串数组的指针类型为char **,虽然两者之间不完全相同,但是作为类比解释还是可以的。关键代码如下:
// 定义函数指针
typedef void (* func)();

int main() {
    Dog a, b;

    cout << " call virtual function through virtual table: " << endl << endl;
    ((func **)(&a))[0][0]();
    ((func **)(&a))[0][1]();
    
    return 0;
}

  执行结果如下:

在这里插入图片描述
  从截图可以看出,显然调用是成功的。
在这里插入图片描述


三 this指针做为成员方法的隐藏参数

  现在我们修改一下虚函数,向函数中传入一个int类型的参数,并在函数中打印出来,我们试试这样到底行不行。

class Dog : public Animal {
public :
    Dog() : Animal() {};
    Dog(int x, double y) : Animal(x, y) {}
    void run(int x) override {
        cout << "this: " << this << endl;
        cout << "x: " << x << endl;
        cout << "I can run with four legs" << endl;
    }
    void say() override {
        cout << "Wang~ Wang~ Wang~" << endl;
    }
};

typedef void (* func)(int);

int main() {
    Dog a;
    ((func **)(&a))[0][0](145);
    return 0;
}

  如代码所示,run()函数中传入 int x 作为函数参数,在run()函数中输出了this指针和x的值,注意修改函数指针类型。代码输出如下:
在这里插入图片描述
  我们发现,输出的x的值是错误的,如果我们再算一下 this 指针输出的值,我们会发现9 * 16 + 1 = 145,这不就是在调用函数时,我们传入的参数吗?这是怎么回事?
  其实这就是因为,this参数是成员函数的隐藏参数,并且这个参数在参数列表的第一个位置,所以我们传入的参数,实际上是传给了this指针,这个this指针的值显然是错误的,如果我们通过该this值访问对象中的成员变量一定会报错。
  为了函数正确执行,需要修改函数指针的类型,同时将this指针(对象的地址)传入即可。
  对应代码如下:

class Animal {
public :
    Animal() = default;
    Animal(int x, double y) : x(x), y(y) {}
    virtual void run(int x) {
        cout << "I don't know how to run" << endl;
    }
    virtual void say() {
        cout << "I don't know how to say" << endl;
    }

private :
    int x;
    double y;
};


class Dog : public Animal {
public :
    Dog() : Animal() {};
    Dog(int x, double y) : Animal(x, y) {}
    void run(int x) override {
        cout << "this:         " << this << endl;
        cout << "x: " << x << endl;
        cout << "I can run with four legs" << endl;
    }
    void say() override {
        cout << "Wang~ Wang~ Wang~" << endl;
    }
};

typedef void (* func)(void *, int);

int main() {
    Dog a;
    cout << "address of a: " << &a << endl;
    ((func **)(&a))[0][0](&a, 145);
    return 0;
}

  执行结果如下:

在这里插入图片描述

  当然,在平时情况,我们通过变量本身(或引用)直接调用或通过指针间接调用 对象的成员函数,直接传参即可,没必要参数列表前面再加上对象的地址。因为编译器已经帮我们完成了这项工作,这也从另一个方面解释了为什么只有在成员方法 (类方法除外) 中可以使用this关键字,this指针是函数的参数,函数内部当然就可以调用了。

  加油,路漫漫其修远兮,吾将天天敲代码!!!

©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页