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

普通成员函数的调用

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;
}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 9,478评论 0 45
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,505评论 1 51
  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,566评论 0 13
  • 一、书目介绍 作者:由比尔.康纳狄和拉姆.查兰,两位共同撰写。比尔.康纳狄是通用电气前高级HR总监,拉姆.查兰是全...
    任璐璐阅读 7,506评论 0 1
  • 001【定位】 艾·李斯&杰克·特劳特 1、孙子云:先胜而后求战。 2、任何在顾客心智中没有位置的品牌,终将从现实...
    翊銘阅读 467评论 0 0