美文网首页程序员
《深入探索C++对象模型》笔记 Chapter4

《深入探索C++对象模型》笔记 Chapter4

作者: 朱明代月 | 来源:发表于2018-08-30 21:17 被阅读2次

    《深入探索C++对象模型》笔记 Chapter2
    《深入探索C++对象模型》笔记 Chapter3

    第4章 函数

    4.1 总述

    非静态成员函数

    C++的设计准则之一就是,非静态成员函数至少必须要和普通函数有相同的效率。

    编译器会将成员函数转换为了对等的普通函数。转换步骤如下:

    • 改写函数原型(signature),传入参数添加一个this指针
    • 将对成员变量(非静态)的操作改为通过this指针来存取
    • 对成员函数的函数名称做命名重整(mangling)

    上述所说的 mangling 过程,通常函数名会被加上class名称的前缀,以及参数类型的后缀,从而确保每个函数名都是独一无二的。

    虚函数

    //虚函数调用
    ptr->func();
    //转为为
    (*ptr->vptr[1])(ptr);
    

    以上的转换,vptr表示指向虚函数表的指针;1是虚函数表中该函数的索引值,和func()函数关联;函数参数中的ptr表示this指针。

    静态成员函数

    首先来看看为什么需要静态成员函数。

    在引入静态成员函数之前,C++规定所有成员函数都必须由对象来调用,然而有些成员函数不对成员变量操作,也就没必要传入this指针,而C++并不能辨识这种情况。

    于是,如果将静态成员变量声明为private,就必须提供成员函数来调用它,也就必须要实例化一个对象才能调用,以至出现了以下奇葩的写法:
    ((ClassName*) 0 ) -> get_staticmember_func();
    将0强转成class指针,然后调用成员函数返回一个静态成员变量。

    而有了静态成员函数,才解决了上述所说的问题。静态成员函数的主要特性是没有this指针,以此延伸出了其他特性:

    • 不能存取非静态成员
    • 不能被声明为 const volatile virtual
    • 不需要经由对象才能被调用

    由于静态成员函数没有this指针,所以它在内存上的存储类似于普通函数。

    4.2 虚函数

    在C++中,多态表示以一个 public base class 的指针(或 reference),寻址出一个 derived class object 的意思。

    单继承

    单继承内存模型

    如上图, Point3d 继承 Point2d , Point2d 继承 Point 。

    virtual table 在编译期就已经确定,virtual table 的每一个函数地址称之为一个 slot ,派生类定义虚函数会 overriding 基类相应函数的 slot 。这样 ptr->z() ,不管 ptr 指向的对象是基类还是派生类,它调用 z() 时函数地址一定是放在第四个 slot ,于是编译器转换代码为 (*ptr->vptr[4])(ptr)

    对于纯虚函数,slot 里放置的是 pure_virtual_called() 函数,这个函数如果被意外调用,通常会结束掉这个程序。可以利用这个特性做运行期异常处理。

    多继承

    多继承内存模型

    虚继承

    虚继承内存模型

    4.3 效率

    经过测试,普通函数、非静态成员函数、静态成员函数的效率很相近。inline成员函数效率惊人。

    4.4 指向成员函数的指针

    A::* 意为指向类A成员的指针,我们可以用 void (A::*pmf)() = &A::func 表示指向A中 func() 成员函数的指针。对于非虚函数,这个指针指向一个函数地址,对于虚函数,由于地址在编译时期是未知的,所以存放的是该函数在虚函数表中的索引值。(个人以为,此时再说 pmf 是个指针就比较牵强了,它只是个记录函数相对位置的int值)
    于是,(ptr->*pmf)() 就会被编译器翻译为 (*ptr->vptr[(int)pmf])(ptr) 。可以写个demo做个验证:

    #include <iostream>
    #include <stdio.h>
    using namespace std;
    
    class A {
        public:
        void common(){}
        virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
    };
    class B :public A{
        public:
        virtual void foo() { printf("B::foo(): this = 0x%p\n", this); }
        virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
        virtual void foo(int i) { printf("B::bar(): this = 0x%p\n", this); }
    };
    
    int main(){
        A* a = new A;
        B* b = new B;
        printf("A::commont: %x\n",pcommon);
        printf("A::foo: %x\n",pafoo);
        printf("B::foo: %x\n",pbfoo);
        printf("B::bar: %x\n",pbbar);
    }
    

    输出如下:

    A::commont: 31425c9e
    A::foo: 1
    B::foo: 1
    B::bar: 9
    

    至于为什么索引值从1开始,书上没有说,不过想来和 3.6指向成员函数的指针 所说的类似,为了区分指向成员函数的空指针。

    而索引值按8递增,那是因为我的云主机是64位,一个指针8字节。

    单继承的情况,一个索引值就足够调用不同虚函数了,但是考虑到多继承呢?一个派生类继承自A和B,调用它的虚函数时,this指针作为传入参数可能是A,也可能是B,所以多继承还需要考虑this指针的偏移。我本来想仿照此篇博客 C/C++杂记:深入理解数据成员指针、函数成员指针 打印出this指针的偏移,但是事与愿违,偏移值始终为0,也只能归因于不同编译器的实现方式不同了。

    4.5 内联函数

    inline关键字并不能强迫将任何函数都变成 inline ,而是给编译器一个建议。至于编译器是否采纳这个建议,需要一系列复杂的测试,包括计算赋值、调用函数、调用虚函数的次数,每种操作都会有一个权值,这些操作与权值的乘积之和就是函数的复杂度。

    对于inline函数,如果传入的参数是变量或者常量,那么直接对函数体内代码执行参数替换就可以了,但是如果传入的参数是个表达式呢?难道函数体内的代码每次碰到这个表达式都要算一遍?显示编译器不会这么傻。针对这种情况,编译器会产生一个临时对象,以避免重复求值。

    再考虑一种情况,如果inline函数中有局部变量会怎么样?如果直接把代码扩展到调用inline函数的函数,万一后者有同名的变量呢?所以需要把局部变量放在封闭区段中,让它们有自己的作用域。这个过程依旧会产生局部变量。

    可以看到,局部变量和表达式参数都会使inline函数产生临时对象,所以使用inline不当会产生大量临时对象反而降低效率。

    参考文章

    C/C++杂记:深入理解数据成员指针、函数成员指针

    相关文章

      网友评论

        本文标题:《深入探索C++对象模型》笔记 Chapter4

        本文链接:https://www.haomeiwen.com/subject/ayqoiftx.html