探究C++中的成员函数指针和虚函数表

来源:互联网 发布:sql 双竖线是什么意思 编辑:程序博客网 时间:2024/05/22 04:38

say something

相信对C++对象有一定了解的话,应该都会知道,在C++中对象的实现中,成员函数和成员变量是分离的
所以我们所谈到的非静态成员函数其实只是一个普通的函数不过被编译器所隐藏,必须绑定到特定的对象上才能执行
静态成员函数实际上就真的就是一个普通的函数,独立于整个对象之外,不过被编译器加上了一堆修饰避免重命名,和对象无关
普通非静态成员函数的实现是通过传入this指针的方式去绑定到特定的对象上,然后执行特化的操作

我们先定义一个测试对象,下面的所有操作都是基于这个类

class Base {private:    int a;public:    Base():a(0) {}    void test() { cout << "call Base ::test()" << endl; }//没有操作成员变量    void testMember() { a = 6; }//操作了成员变量    static void testStatic() { cout << "call Base::testStatic"<<endl; }//静态成员函数    virtual void test1() { cout << "call Base ::test1()" << endl; }//虚成员函数1    virtual void test2()const { cout << "call Base ::test2()" << endl; }//const虚成员函数2    virtual void test3(int a) { cout << "call Base ::test3(int a)" << endl; }//带参数的虚成员函数3};

非静态成员函数指针

我们可以通过如下的方式去声明以及定义这个对象的非静态成员函数指针

    void(Base::*func1)();//定义一个名为func1的指向Base对象的函数指针,未初始化    //func1 = &Base::testStatic;//报错,这里提示testStatic是一个类型为void (void)的函数,验证了我的观点    func1 = &Base::test;//绑定到指定成员函数上去,现在其值为一个实例化函数的地址

定义完成,如何去使用这个函数指针?
我们可以有多种方式去调用,不过无论哪种方式方式都需要将函数指针绑定到实例化的对象上:

    Base src;    (src.*func1)();//直接调用    Base* ptr = &src;    (ptr->*func1)();//指针调用    //实际上,无论如何调用,编译器都会将上面的调用转换成下面的调用方式    (*func1)(&src);    (*func1)(ptr);    //当然,你直接这么写是不行的,只有编译器可以这么做

静态成员函数指针

前面已经说了,在编译器的眼里,静态成员函数实际上和普通函数没什么不同,上面的成员函数可能被编译器修饰成如下模样并直接放在全局:

extern void _Base_testStaticVoid_1() { cout << "call Base::testStatic" << endl; }

我们调用的时候就直接被编译器解释成这个模样:

    Base::testStatic();    //解释成如下:    _Base_testStaticVoid_1();

这也和静态函数的作用有关,静态成员函数并不需要和特定对象进行联系,而是只能操作静态成员和局部以及全局变量。
所以,我们如何定义一个静态成员函数指针?——就像定义普通函数指针那样

typedef void(*Func)();//简单的typedef,将Func表示为一个void(void)类型的函数的指针类型Func f1 = &Base::testStatic;//直接定义和赋值

就像操作普通函数指针那样

虚函数表

要说到虚函数表的话,希望大家已经知道C++为每一个定义有虚函数的对象都会构建一个虚函数表,虚函数表中存储了每一个虚函数的实例函数的地址
一般当我们定义一个对象:

Base *a = new Base();

这时候,编译器做了什么呢?不仅仅是做了为我们的对象分配内存空间然后按照构造函数去构建对象的操作,还做了一些额外的操作:

Base():a(0) {}会被拓展成:Base():a(0) {  this->vptr=Base::vptr;  ...  //为每一个含有构造函数的成员变量进行一一的构建  ...  //自己写的操作}

它会多分配四个字节或者八个字节的空间去存储一个指针变量,这个指针变量指向虚函数表的头部,一般编译器会将这个指针放在对象的头部。然后在每次构建的时候去修改这个虚函数指针的值,确保多态的正确执行。
然后在每次调用虚函数的时候都会找到相应类型的对象的虚函数表指针对应的虚函数表中寻找对应的那一个虚函数

我们可以试着去探寻一下我说的是否正确:

void TestVirtualTable() {    Base *a = new Base();    int* vptr = reinterpret_cast<int*>(a);//获得虚函数表的指针,指向虚函数表的第一个函数    int p = *vptr;    Func* funcT = reinterpret_cast<Func*>(p);//将函数表的第一个函数的实例转换成函数指针    funcT[0]();    funcT[1]();    Func1 f1 = reinterpret_cast<Func1>(funcT[2]);    f1(1);     //可以输出,但是会报错}

输出如下:

call Base ::test1()call Base ::test2()call Base ::test3(int a)

reinterpret_cast我就不再赘述了,可以看到,的确如我所说,虚函数表指针就放在Base*指向的位置。
那么,我们该如何定义一个虚函数指针呢?
其实和普通成员函数指针有点类似:

    void (Base::*func)();    func = &Base::test1;

我们已经知道,对一个非静态成员函数取其地址,将获得这个函数在内存中的地址,但是对一个虚函数取地址的话,由于其地址在编译时期是不能确定的(多态),所以我们只能获得一个索引值

比如针对上面的代码,我们输出&Base::test1的话,会发现其值为1(当然我们是不能直接进行输出),&Base::test2的话,输出为2,这个索引值代表了其在虚函数表中的位置

所以如果我们通过一个虚函数指针“去执行函数的话:

void (Base::*func)()= &Base::test1;Base bs;(bs.*func)();

(bs.*func)();这一行代码会被修饰成如下形式:

(bs.vptr[(int)func])(&bs);

只有这样,才能很好的支持继承的关系

所以,我们可以自己思考一下,多态是怎么通过这种方式去实现的?