这个章节咱们会学到如下3个知识点:html
1.不一样的类型函数是怎么调用的。
2.成员函数指针各个模型实现的原理是什么以及各个指针的效率如何。
3.inline函数的注意事项。
linux
看下面的代码,这里面我分别调用了类成员和全局函数设计模式
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。
测试
关于虚函数在第四章作了专门的介绍,这里就不在啰嗦了。ui
从字面意思看,有两点内容。this
1.是个指针
2.指向的类的成员函数
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指针当作参数传递进去。这个就和普通成员函数调用的原理是一致的。
void test_function_pointer() {
void (*fName)() = &Car::Name;
fName();
}
复制代码
注意到没有咱们在定义静态成员函数时,没有加上类名Car。这是由于静态函数里面没有this指针,因此就形成了不须要加上类名Car,同时也形成静态成员函数不能直接使用类的普通成员变量和函数。你可能发现类的静态成员函数,和全局函数很是相似,其实本质上是同样的,都是一个可调用的地址。
上面的两节,咱们看了普通成员函数指针和静态成员函数指针,以为比较简单。接下来的重头戏虚拟成员函数指针,这里的套路更深也更难理解,且听我一步步道来。
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变量上就会显示一串信息。
显示的什么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)
复制代码
画了一张图解释下:
首先,咱们不看蓝色虚线的部分。此时并非直接找到虚函数地址,而是经过一个中间层(黑色虚框部分)去找到。这种技术,在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),根据不一样的虚表就能找到不一样的虚函数。
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方法,和上面的没啥区别。
提早预警,这里的模型更复杂了,你们必定要耐心看下去。
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。打印出以下内容:
增长了虚继承以后,内存的模型复杂度立立刻升了一个档次。上述表格看的不明显,我花了几张图方便你们观看。
好了,咱们画图Tiger相关的内存模型图。接下来咱们看看这是指向虚成员函数指针是如何实现的。
咱们看下面这段代码的执行流程。
// 1.测试代码
// void (Animal::*fName)() = &Animal::Name;
// 下面这句和上面注释的一句是等价的
void (BigTiger::*fName)() = &Animal::Name;
Tiger *temptiger2 = new Tiger();
BigTiger *bigtiger = temptiger2;
(bigtiger->*fName)();
复制代码
你们可能在第二步调整this指针的时候会很奇怪的,可是根据我debug模式下跟下来,vtordisp for vbase Animal 这个位置的值为0。
那么ecx = ecx-[ecx-4]等价于ecx=ecx-0仍是等于ecx自己,ecx里面就保存了this指针的地址,最后再调用虚函数。这里我也很好奇为何这里还有个调整this地址的问题。 还有个关于vtordisp的,我也没有理解,从调试的过程看下来,就知道他参与了最后一次的this指针调整。这里我贴出网上的一个地址讨论这个的
那么上面的调用过程就是:
1.根据thunk找到正确的虚函数地址。
2.调整this指针的偏移,再调用第一步找到的虚函数地址。
成员函数指针有两种形态:
1.和C语言中同样的函数指针。
2.thunk vcall的技术,就是几行汇编代码:1.以调整this的地址。
2.能够协助找到真正的虚函数地址。
不知道你们有没有感受,这个thunk很是像桥接模式的思路,将桥的两边变化都隔离开,就是解耦,各自能够随意变化。
你们可能对学习了这节的成员函数指针以为没啥用处,其实这节的用处可大了。想一想C++11中的functional,bind是怎么实现的。后面有机会的话经过functional重写观察者设计模式,让你感叹这个的强大。
同时这里面还有其余的模式组合(好比:虚继承普通成员函数),我这里就没有一一的探讨了,但愿读者对本身感兴趣的部分动手实践,或者和我讨论也能够。
最后咱们在比较下各类函数指针的效率如何:
inline函数调用的过程当中,须要注意两点:
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;
复制代码
这样作的话,其实会形成大量的临时对象构造。若是你的对象须要大量的初始化操做,会带来效率问题。
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展开的时候也会临时对象的构造,就和上面的影响是同样的,形成的效率损失。