C++幕后故事(六)--函数我来调你了

这个章节咱们会学到如下3个知识点:html

1.不一样的类型函数是怎么调用的。
2.成员函数指针各个模型实现的原理是什么以及各个指针的效率如何。
3.inline函数的注意事项。
linux

1.普通成员调用

看下面的代码,这里面我分别调用了类成员和全局函数设计模式

class NormalCall {
public:
    void Add(int number) {
        number+m_add;
    }
    static void ClassStatic() {
        cout << "ClassStatic" << endl;
    }
    virtual void ClassVirutal() {
        cout << "ClassVirutal" << endl;
    }
public:
    int m_add;
};

void Add(NormalCall *nc, int number) {
    nc->m_add + number;
}

void test_normal_call() {
    NormalCall nc;
    nc.Add(1);
    // 其实被编译器转成__ZN6NormalCall6AddEi(&nc, 1)
    // 编译器在调用类的普通成员函数时,会在函数的参数中隐式添加了一个this指针,这个指针
    // 就是当前生成对象的首地址。同时对普通成员变量的存取也是经过this指针
    Add(&nc, 1);     // 调用全局函数
}

int main() {
    return 0;
}
复制代码

把上面的源代码保存文件main.cpp,而后在linux平台上用g++ -Wall –g main.cpp –o main,再用nm main,就会导出main里面的符号等等其余东西。我把重要的东西拿出来看下。函数

080486f0 T __x86.get_pc_thunk.bx
08048820 T _Z16test_normal_callv
0804881b T _Z3AddP10NormalCalli
08048888 t _Z41__static_initialization_and_destruction_0ii
U _ZdlPv@@GLIBCXX_3.4
08048bea W _ZN10NormalCall12ClassVirutalEv
08048be4 W _ZN10NormalCall3AddEi

从这里咱们这里能够看出,咱们写代码的时候名字就是Add,可是编译完以后名称全变了。_Z3AddP10NormalCalli咱们能够猜想下就是咱们写的Add(NormalCall, int)原型。_ZN10NormalCall3AddEi应该就是咱们的NormalCall成员函数Add(int)原型。你可能会奇怪,为何C++编译器编译出来的名称都变了,这种作法叫作name mangling(命名粉碎),实际上是为了支持C++另一个特性,就是函数重载的特性。同时,也是C++确保调用普通成员函数,和调用全局函数的在效率上是一致的。工具

静态成员函数调用

void test _static_call()
{
    NormalCall *pNC = new NormalCall();
    pNC->ClassVirutal();

    NormalCall NC;
NC.ClassStatic();

    pNC->ClassStatic();
    NormalCall::ClassStatic();

}
复制代码

; 上面三种调用static函数的生成的反汇编代码是一致的。
22C7D4 call NormalCall::ClassStatic (0221550h)
22C7D9 call NormalCall::ClassStatic (0221550h)
22C7DE call NormalCall::ClassStatic (0221550h)
布局

总结:学习

1.静态成员函数,没有this指针。
没法直接存取类中普通的非静态成员变量。
3.调用方式能够向类中普通的成员函数,也能够用ClassName::StaticFunction。
4.能够将静态的成员函数在某些环境下当作回调函数使用。
5.静态的成员函数不可以被声明为const、volatile和virtual。
测试

3.虚函数调用

关于虚函数在第四章作了专门的介绍,这里就不在啰嗦了。ui

4.成员函数指针

从字面意思看,有两点内容。this

1.是个指针
2.指向的类的成员函数

4.1普通成员函数指针

class Car {
public:
    void Run() { cout << "Car run" << endl; }
    static void Name() { cout << "lexus" << endl; }
};

void test_function_pointer() {
    // void (Car::*fRun)() = &Car::Run;
    // 能够将成员函数指针分红四步看
    void        // 1.返回值
    (Car::*      // 2.哪一个类的成员函数指针
    fRun)       // 3.函数指针名称
    ()           // 4.参数列表
    = &Car::Run;

    Car car;
    (car.*fRun)();
}
复制代码

从上面的代码中看出,定义一个成员函数指针,只须要注意四步就行:

1.返回值。
2.哪一个类的成员函数指针。
3.函数指针名称。
4.参数列表。

在注意调用的方式(car.fRun)(),它比日常调用car.Run()时候多了个

这个背后实现的原理:

每一个成员函数都一个固定的地址,把普通成员函数的地址赋值给一个函数指针,而后在调用函数指针的时候再把this指针当作参数传递进去。这个就和普通成员函数调用的原理是一致的。

4.2 静态成员函数指针

void test_function_pointer() {
    void (*fName)() = &Car::Name;
    fName();
}
复制代码

注意到没有咱们在定义静态成员函数时,没有加上类名Car。这是由于静态函数里面没有this指针,因此就形成了不须要加上类名Car,同时也形成静态成员函数不能直接使用类的普通成员变量和函数。你可能发现类的静态成员函数,和全局函数很是相似,其实本质上是同样的,都是一个可调用的地址。

4.3 虚拟成员函数指针

上面的两节,咱们看了普通成员函数指针和静态成员函数指针,以为比较简单。接下来的重头戏虚拟成员函数指针,这里的套路更深也更难理解,且听我一步步道来。

4.3.1单继承模型下调用

class Animal {
public:
    virtual ~Animal() { cout << "~Animal" << endl; }
    virtual void Name() { cout << "Animal" << endl; }
};

class Cat : public Animal
{
public:
    virtual ~Cat() { cout << "~Cat" << endl; }
    virtual void Name() { cout << "Cat Cat" << endl; }
};

class Dog : public Animal
{
public:
    virtual ~Dog() { cout << "~Dog" << endl; }
    virtual void Run() { cout << "Dog Run" << endl; }
    virtual void Name() { cout << "Dog Dog" << endl; }
};

void test_virtual_fucntion_pointer() {
    Animal *animal = new Cat();
    void (Animal::*fName)() = &Animal::Name;
    printf("fName address %p\n", fName);
    // fName address 00FD1802
    (animal->*fName)();
    // Cat Cat

    // 打印Cat的虚表中的Name地址
    Cat *cat = new Cat();
    long *vtable_address = (long *)(*((long *)(cat)));
    printf("virtual Name address %p\n", (long *)vtable_address[1]);
    // virtual Name address 00FD1429

    // 编译器在语法层面阻止咱们获取析构函数地址
    // 可是咱们知道的在虚函数章节里面,咱们能够经过虚表的地址间接获取析构函数地址
    // void (Animal::*Dtor)() = &Animal::~Animal;
    // (animal->*Dtor)();

    printf("fName address %p\n", fName);
    // fName address 00FD1802
    animal = new Dog();
    (animal ->*fName)();
    // Dog Dog

    // 打印Dog的虚表中的Name地址
    Dog *dog = new Dog();
    long *dog_vtable_address = (long *)(*((long *)(dog)));
    printf("virtual Name address %p\n", (long *)dog_vtable_address[1]);
    // virtual Name address 00FD1672
}
复制代码

在代码中咱们定义了一个变量fName。

void (Animal::*fName)() = &Animal::Name;

并赋值为&Animal:Name;咱们再打印出Name的地址0x009F1802。

咱们先思考这个地址到底指向谁?

这个地址就是虚函数的地址?若是是,那么它的地址是父类的?仍是子类,若是那么编译器又是怎么我指向的是哪一个虚函数地址?若是不是,那么又是个什么地址?接下里咱们一步步的经过汇编代码验证猜测。

咱们在VS的调试模式下,将鼠标移动fName变量上就会显示一串信息。

image

显示的什么thunk,vcall{4…},都是什么玩意看不懂。反汇编走一遍,究竟是个什么锤子。

如下是关键的汇编代码:

(animal->*fName)();
00FDD66F  mov         esi,esp 
; 是否是条件反射了,将this指针地址放到ecx中
00FDD671  mov         ecx,dword ptr [animal]  
00FDD674  call        dword ptr [fName]  

function_semantic::Animal::`vcall'{4}':
00FD1802  jmp         function_semantic::Animal::`vcall'{4}' (0FD73BCh) 

; 拿到虚表首地址
00FD73BC  mov         eax,dword ptr [ecx]  
; 偏移地址,找到正确的虚函数地址
00FD73BE  jmp         dword ptr [eax+4] 

function_semantic::Cat::Name:
; 真正的虚函数地址
00FD1429  jmp         function_semantic::Cat::Name (0FD8FB0h)  
复制代码

画了一张图解释下:

image

首先,咱们不看蓝色虚线的部分。此时并非直接找到虚函数地址,而是经过一个中间层(黑色虚框部分)去找到。这种技术,在microsoft编译器中被包装了一个高大上的名词叫作thunk。

咱们再看整张图,你会发现和之前调用虚函数的方式(蓝色虚线箭头)相比,是否是就是多了一个thunk的调用过程。可是为啥要多个中间层,那不意味着效率又下降了?首先引入thunk是为了寻找虚函数地址增长强大的灵活性。其次须要认可的是效率的确降低了,可是没有降低的那么厉害,这几行代码都是汇编级别的代码,因此执行的效率仍是很高。 接下来我详细解释下是如何增长灵活性,仔细观察上面的黄色高亮的代码块,为了方便查看我摘抄下来。第一次看到下面的代码,总以为很是的别扭。声明的类型是父类的成员函数指针,最后调用的倒是子类重写的虚函数打印的结果分别是Cat Cat,Dog Dog,非常神奇,并且fName是个变量在这个过程是不变化的,这是怎么作到的。这背后就是thunk的功劳了。

Animal *animal = new Cat();
void (Animal::*fName)() = &Animal::Name;

(animal->*fName)();
// Cat Cat

animal = new Dog();
(animal ->*fName)();
// Dog Dog
复制代码

那么thunk到底什么?

从汇编层看,thunk就是那么几行代码。干了一件很简单的事,就是根据传递过来的ecx指针,找到虚表地址,在根据偏移量(这里偏移为4byte)找到正确的虚函数地址。因此ecx里面就是保存了对象的首地址(也就是包括了vptr),根据不一样的虚表就能找到不一样的虚函数。

4.3.2 多继承模型下调用

class Fly {
public:
    virtual ~Fly() { cout << "~Fly" << endl; }
    virtual void CanFly() { cout << "Fly" << endl; }
    void Distance() { cout << "Fly distance" << endl; }
};

class Fish : public Animal, public Fly
{
public:
    virtual ~Fish() { cout << "~Fish" << endl; }
    virtual void Name() { cout << "Fish" << endl; }
    virtual void CanFly() { cout << "Fish Fly" << endl; }
};

void test_mult_inherit_vir_fun_pointer() {
    void (Animal::*fName)() = &Animal::Name;
    
    void (Fly::*fFly)() = &Fly::CanFly;
    Fish *fish = new Fish();
    Fly *fishfly = fish; 
    (fishfly->*fFly)();

    Animal *animal = fish;
    (animal->*fName)();
}
复制代码

这里从反汇编的角度看,在单继承下面都是调用thunk方法,和上面的没啥区别。

4.3.3 虚拟继承模型下调用

提早预警,这里的模型更复杂了,你们必定要耐心看下去。

class Animal {
public:
    virtual ~Animal() { cout << "~Animal" << endl; }
    virtual void Name() { cout << "Animal" << endl; }
    void Size() { cout << "Animal Size" << endl; }
};

class BigTiger: public virtual Animal
{
public:
    virtual ~BigTiger() { cout << "~Big Tiger" << endl; }
    virtual void Name() { cout << "Big Tiger" << endl; }
};

class FatTiger: public virtual Animal
{
public:
    virtual ~FatTiger() { cout << "~Fat Tiger" << endl; }
    virtual void Name() { cout << "Fat Tiger" << endl; }
};

class Tiger: public BigTiger, public FatTiger
{
public:
    virtual ~Tiger() { cout << "~Tiger" << endl; }
    virtual void Name() { cout << "Tiger" << endl; }
    virtual void CanFly() { cout << "Tiger Fly" << endl; }
};

void test_virtual_mult_inherit_vir_fun_pointer() {
// 1.测试代码
    // void (Animal::*fName)() = &Animal::Name;
    // 下面这句和上面注释的一句是等价的
    void (BigTiger::*fName)() = &Animal::Name;
    Tiger *temptiger2   = new Tiger();
    BigTiger *bigtiger   = temptiger2;
(bigtiger->*fName)();
// 打印出:Tiger

// 2.测试代码
    // void (FatTiger::*fFatName)() = &Animal::Name;
    // 下面这句和上面注释的一句是等价的
    void (FatTiger::*fFatName)() = &FatTiger::Name;
    Tiger *temptiger = new Tiger();
    FatTiger *fattiger = temptiger;
(fattiger->*fFatName)();
// 打印出:Tiger
 }

int main() {
   test_virtual_mult_inherit_vir_fun_pointer();
}
复制代码

上述的测试代码,咱们先看看Tiger的内存布局是什么样的。

把代码copy拿出来保存为main.cpp,在vs2013命令行工具中,cd到main.cpp所在的目录,运行指令cl /d1 reportSingleClassLayoutTiger main.cpp。打印出以下内容:

image

image

增长了虚继承以后,内存的模型复杂度立立刻升了一个档次。上述表格看的不明显,我花了几张图方便你们观看。

image

image

image

好了,咱们画图Tiger相关的内存模型图。接下来咱们看看这是指向虚成员函数指针是如何实现的。

咱们看下面这段代码的执行流程。

// 1.测试代码
// void (Animal::*fName)() = &Animal::Name;
// 下面这句和上面注释的一句是等价的
void (BigTiger::*fName)() = &Animal::Name;
Tiger *temptiger2   = new Tiger();
BigTiger *bigtiger   = temptiger2;
(bigtiger->*fName)();
复制代码

image

你们可能在第二步调整this指针的时候会很奇怪的,可是根据我debug模式下跟下来,vtordisp for vbase Animal 这个位置的值为0。

image

那么ecx = ecx-[ecx-4]等价于ecx=ecx-0仍是等于ecx自己,ecx里面就保存了this指针的地址,最后再调用虚函数。这里我也很好奇为何这里还有个调整this地址的问题。 还有个关于vtordisp的,我也没有理解,从调试的过程看下来,就知道他参与了最后一次的this指针调整。这里我贴出网上的一个地址讨论这个的

www.cnblogs.com/fanzhidongy…

那么上面的调用过程就是:

1.根据thunk找到正确的虚函数地址。
2.调整this指针的偏移,再调用第一步找到的虚函数地址。

4.4 成员函数指针总结

成员函数指针有两种形态:

1.和C语言中同样的函数指针。
2.thunk vcall的技术,就是几行汇编代码:

1.以调整this的地址。
2.能够协助找到真正的虚函数地址。

不知道你们有没有感受,这个thunk很是像桥接模式的思路,将桥的两边变化都隔离开,就是解耦,各自能够随意变化。

你们可能对学习了这节的成员函数指针以为没啥用处,其实这节的用处可大了。想一想C++11中的functional,bind是怎么实现的。后面有机会的话经过functional重写观察者设计模式,让你感叹这个的强大。

同时这里面还有其余的模式组合(好比:虚继承普通成员函数),我这里就没有一一的探讨了,但愿读者对本身感兴趣的部分动手实践,或者和我讨论也能够。

最后咱们在比较下各类函数指针的效率如何:

image

5.inline函数调用

inline函数调用的过程当中,须要注意两点:

5.1函数参数列表:

inline int max(int left, int right) {
    return left > right ? left : right;
}
复制代码
// 调用方式
max(foo(), bar()+1)
复制代码
// inline 被扩展以后
int t1;
int t2;
maxvale = (t1=foo()),(t2=bar()+1), t1 > t2 ? t1 : t2;
复制代码

这样作的话,其实会形成大量的临时对象构造。若是你的对象须要大量的初始化操做,会带来效率问题。

5.2局部变量

inline int max(int left, int right) {
// 添加临时变量max_value
    int max_value = left > right ? left : right;
    return max_value;
}
复制代码
{
    // 调用方式
    int local_var;
    int maxval;
    maxval = max(left, right);
}
复制代码
// inline 被扩展以后
// max里面的max_value会被mangling,如今假设为__max_maxval
int __max_maxval;
maxval = (__max_maxval = left > right ? left : right), __max_maxval;
复制代码

在inline函数中增长了临时变量,看到最后inline展开的时候也会临时对象的构造,就和上面的影响是同样的,形成的效率损失。

6.总结:

image
相关文章
相关标签/搜索