美文网首页
C++对象模型4——函数的调用

C++对象模型4——函数的调用

作者: 漫游之光 | 来源:发表于2018-12-20 14:09 被阅读0次

    普通成员函数的调用

    C++的设计准则之一就是:普通成员函数的调用至少和全局函数有相同的效率。而事实上,C++编译器的实现是把普通成员函数转化为对全局函数的调用,请看下面的代码:

    #include<iostream>
    using namespace std;
    
    class A{
    public:
        const char *name = "class A";
        int val = 100;
        void print(){
            cout << "name = " << name << " val = " << val << endl;
        }
    };
    
    void printA(A *a){
        cout << "name = " << a->name << " val = " << a->val << endl;
    }
    
    int main(){
        A a;
        a.print();
        printA(&a);
        return 0;
    }
    

    这种转化,编译器需要做三件事:

    1. 改变函数参数,把this指针当做第一个参数传入函数。
    2. 对成员的访问都通过this指针。
    3. 把成员函数重新写成一个外部函数。

    要验证这一点,可以查看汇编代码:

        a.print();
    00007FF7C69C461D  lea         rcx,[a]  
    00007FF7C69C4622  call        A::print (07FF7C69C123Ah)  
        printA(&a);
    00007FF7C69C4627  lea         rcx,[a]  
    00007FF7C69C462C  call        printA (07FF7C69C1221h)
    

    可以看出,明明print函数没有参数,但是还是传入了一个this指针,和printA函数的调用方式一样。

    静态成员函数

    静态成员函数是C++后面才引入的,在没有引入之前,也有类似的函数调用方法。下面是一个例子:

    #include<iostream>
    using namespace std;
    
    class A{
    public:
        int val;
        static void print(){
            cout << "hello world" << endl;
        }
    };
    
    int main(){
        ((A *)0)->print();
        return 0;
    }
    

    可以想象,这样调用时没有任何问题的,因为C++编译器把成员函数改为了全局函数,这样也会传入this指针,并且没有访问成员的值,所以这样调用是没有问题的。这也引出了静态全局函数和普通成员函数的最大差别,没有this指针。总的来说,静态成员函数有以下的特点:

    1. 静态成员函数没有this指针,也就是说,无法访问类中的非静态成员函数。
    2. 静态成员函数不能使用const(没有this指针)修饰,也不能使用virtual(无法访问虚函数表指针)修饰。
    3. 可以使用类对象调用,但是无法访问类对象中的成员,也可以使用类名调用。
    4. 静态成员函数等同于非成员函数,有的需要提供回调函数的场合,可以将静态成员函数作为回调函数。

    虚函数的调用

    虚函数其实和普通成员函数的调用差不多,不同的是,虚函数的调用需要通过虚函数表,请看下面一个例子:

    #include<iostream>
    using namespace std;
    
    class A{
    public:
        void * vptr;
        const char *name = "class A";
        int val = 100;
        virtual void print_name(){
            cout << "name = " << name << endl;
        }
        virtual void print_val(){
            cout << "val = " << val << endl;
        }
    };
    
    void a_name(A *a){
        cout << "name = " << a->name << endl;
    }
    
    void a_val(A *a){
        cout << "val = " << a->val << endl;
    }
    
    typedef void(*func)(A*);
    
    int main(){
        A *a = new A();
        void * vt[2] = {(void *)a_name,(void *)a_val};
        a->vptr = vt;
        a->print_name();
        a->print_val();
        (*((func *)(a->vptr)))(a);
        (((func *)a->vptr)[1])(a);
        (*((func *)a->vptr)[1])(a);
        (*((func *)(a->vptr) + 1))(a);
        return 0;
    }
    

    这里有一些值得注意的东西,第一,就是数组名相当于对数组取了地址,也就是说,char *name[],使用name就相当于使用char **。第二个就是[i]操作符相当于*(name + i)。第三个就是对于函数指针,可以不适用*解引用,也可以不使用;大多数时候都是不使用的。

    搞清楚了上面的问题,代码就比较好理解了,上面的代码手动模拟了编译器的处理,生成虚函数表和虚函数指针,然后使用虚函数指针取调用虚函数,这样就可以实现多态。如果要验证,还是要看看汇编代码,调用print_val的汇编代码如下:

        a->print_val();
    00007FF6D83F5256  mov         rax,qword ptr [a]  //rax = a  
    00007FF6D83F525B  mov         rax,qword ptr [rax]  //rax = *a = vptr
    00007FF6D83F525E  mov         rcx,qword ptr [a]  //rcx = a
    00007FF6D83F5263  call        qword ptr [rax+8]  //vptr指向第二个函数
    

    可以看出,对于虚函数,还是需要传入this指针,并且还需要传入虚函数在虚函数表中的索引。

    对于继承体系来说,还有可能需要调整this指针,这种情况比较复杂,就不展开讨论了,就简单说一下在多继承体系下不适用虚析构函数会出现问题。

    #include<iostream>
    using namespace std;
    
    class A{
    public:
        int a_val;
    };
    
    class B{
    public:
        int b_val;
    };
    
    class C:public A,public B{
    public:
        int c_val;
    };
    
    int main(){
        B *b = new C();
        delete b;
        return 0;
    }
    

    其实这就是this指针调整出现的问题,在C语言中我们就知道,free函数必须是malloc分配的指针,并且这个指针不能改变;而这里出现的错误,就是这个this指针并没有指向开始分配的内存(new在底层会调用malloc),导致free(delete在底层调用了free)的调用失败。这里,如果想要程序运行正常的话,只需要把父类的析构函数声明会虚函数。这个例子说明,在继承体系中,父类的析构函数最好声明为虚函数。

    指向成员函数的指针

    和指向类成员的指针一样,C++也有指向成员函数的指针,这个个人感觉可以了解,平时用得很少。

    #include<iostream>
    using namespace std;
    
    class A{
    public:
        int val;
        void f1(){
            cout << "A::f1()" << endl;
        }
    
        virtual void f2(){
            cout << "A::f2()" << endl;
        }
    };
    
    class B :public A{
    public:
        virtual void f2() override{
            cout << "B::f2()" << endl;
        }
    };
    
    int main(){
        void(A::*p1)() = &A::f1;
        void(A::*p2)() = &A::f2;
        printf("%p %p\n");
        A *a = new B();
        (a->*p1)();
        (a->*p2)();
        return 0;
    }
    

    这种调用之所以比较奇怪,是因为它必须要绑定一个对象才能调用,在语法上会有点奇怪。

    相关文章

      网友评论

          本文标题:C++对象模型4——函数的调用

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