关闭

条款二十四:了解virtual functions、multiple inheritance、virtual base class、runtime type identification的成本

标签: C++虚函数表效率
326人阅读 评论(0) 收藏 举报
分类:

条款二十四:了解virtual functions、multiple inheritance、virtual base class、runtime type identification的成本

1. 虚函数

  在C++中因为虚函数的存在使得多态机制可以让用户类的设计和使用时有更大的灵活性,而虚函数如何管理和调用对于程序的效率来说很是重要。类中的虚函数的实现原理是函数指针也就是虚函数指针(virtual table pointres简写为vptrs),而这些虚函数指针存储在一个虚函数表(virtual tables简写为vtbls)中。vtbl通常是一个有函数指针组成的数组,但是有些编译器也会选择使用链表来管理虚函数,但是策略基本相同,就是使用线性表来管理函数指针。程序中的每一个class一旦声明或者继承了虚函数,那么这个类一定有一个vtbl,而虚函数表中保存的就是这个对象的个中虚函数的指针。如下代码:


    class father
    {
    private:
        int _num;
    public:
        int _fuck;
    public:
        virtual int fun_rst()
        {
            std::cout << "fun_rst used!\n";
            return 1;
        }
        virtual int fun_thd()
        {
            std::cout << "fun_thd used!\n";
            return 3;
        }
        virtual int fun_snd()
        {
            std::cout << "fun_snd used!\n";
            return 3;
        }

        void print_p()
        {
            printf("_fuck的地址     %p\n", &this->_fuck);
            printf("_num的地址      %p\n", &this->_num);
        }
    };

  在vs的命令行工具下输入cl /d1 reportSingleClassLayoutBase1 yuan.cpp命令来查看函数中类的虚函数表其中 reportSingleClassLayoutBase1 Base1是指定类的名字,yuan.cpp是对应的源文件必须经过编译。如图为上述代码的虚函数表:
虚函数表

  从打印出的内容可以看出类的数据成员和函数是分开的,非虚函数是不会保存在虚函数表中,而且虚函数表中的虚函数的排列顺序是和代码的顺序相关的。从图中可以看出类的大小为12个字节除了两个成员外又多了4个字节也就是一个int类型的长度,多余的字节就是虚函数表的指针,他指向虚函数表。我们可以通过虚函数表指针来访问虚函数,代码如下:


    father tmp;
    typedef int(*FUN)();
    FUN ptr = NULL;
    int* p_vtbl = (int*)&(tmp);               //虚函数表地址
    int* p_vtbl_function = (int*)*p_vtbl;     //虚函数表中第一个虚函数的地址
    for (int i = 0; i < 3; i++)
    {
        ((int (*)())*((int*)*((int*)&(tmp)) + i))();           //函数调用的展开
        ptr = (FUN)*(p_vtbl_function + i);
        ptr();
    }

  从图中可以很容易的看出类中虚函数的调用原理((int (*)())*((int*)*((int*)&(tmp)) + i))();是将类的地址转换为对应虚函数的地址,i为偏移量。我们通过打印虚函数的地址和成员函数的地址可以猜测,类中的成员函数和数据成员的存储方式是虚函数表的指针然后才是数据成员,也就是说对象对应的指针就是虚函数表的地址,下图只是一个示意图不保证完全正确,因为不同编译器的行为不同:

  上面说的是简单的一个类,下面代码展示一个继承的类:


    class son :public father
    {
    public:
        virtual void son_fun()
        {
            std::cout << "son fun\n";
        }

        virtual int fun_snd()
        {
            std::cout << "function in the son!\n";
        }
    };

  图中显示的是继承类的虚函数表,可以看出未被类重写的父类虚函数仍旧保存在虚函数表中,而被重写的虚函数被重写的函数替代。

  因此每一个类中最少有一个虚函数表(多重继承有多个),这个虚函数表一定会占用一定的内存空间,而这个虚函数表的大小取决于类中虚函数的多少。这个虚函数表的存储位置取决于编译器。

  这种存储策略分为两种阵营,第一种是暴力式做法就是在每一个需要虚函数表的目标文件中产生一个虚函数表的副本,最后再由链接器剥离重复的副本,使得最终的程序库或者可执行文件中只有一个虚函数表的实体。

  另一种策略是探勘式做法,决定哪一个目标文件应该内含某个class的虚函数表。基本的做法是:类的虚函数表被产生于“内含第一个non-inline,non-pure虚函数定义式”的目标文件中,所以不要将虚函数声明为inline否则可能为虚函数表的创建产生麻烦。

  从上面的虚函数原理看来虚函数会带来两个成本问题:第一,虚函数表的保存必须耗费一定的内存空间,这取决于你的虚函数的多少;第二类成员中必须维护一个虚函数表的指针。

  因为虚函数的调用就是利用函数指针进行调用,所以虚函数的调用基本不构成性能上的瓶颈。

2. 多重继承

  在C++中多继承往往将问题复杂化,在多重继承中类中一般有多个虚函数表(每个基类对应一个虚函数表),而且定位相关的虚函数表也比较复杂。成本方面多重继承相对于单继承的成本增加和继承的类相关,内存空间的压力增加了一些,运行期的调用成本也有一定的增加。

  而且多重继承也会增加另一个麻烦,当孙子继承于某两个父类而这两个类有继承自同一父类时,孙子中就会存在两个祖先的成员数据,这可以通过虚继承和虚基类来解决。但是会在类中增加隐藏的指针,这些指针用来防止基类对象的复制。内存图如下(内存分布一把取决于编译器这里只说一种):


    class grad_father{};
    class father_rst:virtual public grad_father{};  
    class father_snd:virtual public grad_father{};
    class son:public father_rst,public father_snd{};

  增加指针只是一些编译器的做法而另一些编译器并不会增加相应的指针而将这份工作交由虚函数表来做。图中只是为了理解一种处理方式而已。如果加上虚函数表的话就是下面这样:

  图中阴影部分为编译器为你的类增加的部分,而且类中只有三个虚函数表不是四个,这种做法可以有效的减少程序的开销。

3. RTTI(runtime type identification)

  在运行时类的信息会被存储在type_info对象中,我们可以使用type_id来获取类的相关信息。拥有虚函数的类为了管理这个type_info对象可能将他保存在虚函数表中,因此每个类都要为type_info的对象负责。但这并不不意味着你的程序会因为RTTI机制而付出更多的效率代价。RTTI让你可以在程序中实时的获取类的类型等方面的信息,这比你自己管理你的类来说可能更加有效,因为你无法保证你自己的管理能够让你的类在任何地方都被识别。因此我们需要了解他的成本而不是摒弃。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:16165次
    • 积分:687
    • 等级:
    • 排名:千里之外
    • 原创:55篇
    • 转载:0篇
    • 译文:1篇
    • 评论:2条
    文章分类
    最新评论