C++ - 对象模型之 成员函数调用

C++对象模型目录

C++ - 对象模型之 编译器何时才会自行添加构造函数

C++ - 对象模型之 内存布局

C++ - 对象模型之 成员函数调用

C++ - 对象模型之 构造和析构函数都干了什么

C++ - 对象模型之 类对象在执行时是如何生成的

C++ - 对象模型之 模板、异常、RTTI的实现


C++ - 对象模型之 成员函数调用


C++ 类的函数共有三种,

nonestatic member function:如果调用该函数,必须实例化一个对象,然后通过对象调用,那么调用的函数处理的就是该对象。

static member function:该函数不属于任何一个对象,即使没有对象也能够调用该函数。

virtual member function:多态,根据继承关系来确定对象具体调用哪个类的函数。


nonestatic member function

class C1{
public:
void func1(){}
};
C1 c1,c2;
c1.func1();
c2.func1();

我们知道c1和c2共用func1函数段,那么func1函数是如何区分调用者是c1还是c2呢?其实编译器在编译的时候,大体将func1函数和代码修改成了如下形式:
void func1(C1 * const this){
}
func1(&c1);
func1(&c2);
func1函数就是通过传入的对象指针来区分调用者是哪个对象的。
所以,C++的nonestatic member function的参数在编译后都会增加一个,就是this指针。

static member function

该类型函数和nonestatic member function函数区别在于它的参数不会带有this指针,它和普通的C函数一样,可以直接调用。由于这个特点,所以它有以下特点和用途:

1. 不能直接调用和处理nonestatic member data,因为data属于具体的某个对象,static函数无法获得对象的指针,所以无法获得该对象的data;

2. 不能使const、virtual,因为const是保护member data,既然static函数无法直接调用member data,所以没有必要声明为const,另外不管怎么继承,static函数无法获得继承后对象,所以更谈不上virtual;

3. 由于和C的函数基本一样,所以可以作为回调函数;


virtual member function

对于单一继承、多重继承、虚拟继承下的virtual function调用是有区别的,所以,我们分类说明,在说明这些问题的时候,要和《C++ - 对象模型之 内存布局》的案例对照说明。


 单一继承机制

如《C++ - 对象模型之 内存布局》的“单一继承&多态”部分的代码,里面三个类GrandFather、Father、Child的vtbl布局如下

GrandFather:

|GrandFather::who()|

|GrandFather::fishing()|

|GrandFather::hungry()|

Father:

|Father::who()|

|GrandFather::fishing()|

|Father::hungry()|

|Father::cutting()|

Child:

|Child::who()|

|GrandFather::fishing()|

|Child::hungry()|

|Father::cutting()|

|Child::studying()|


我们可以得出以下几点结论:

1. 一个class不管有多少实例,这些实例可以共用一个vtbl,因为同类型的class的vtbl布局是一样的;

2. 为了在编译器,就能够确定virtual function对应的slot索引值,所以virtual function的排列顺序是先遍历base class,然后再遍历derived class,并且,一个virtual function不会被索引两次;

3. 每个class 实例有自己的vptr,该vptr指向vtbl;


多重继承机制

如《C++ - 对象模型之 内存布局》的“多重继承&多态”部分的代码,里面五个类GrandFather、Father、Grandad、Mother、Child的vtbl布局如下

GrandFather:

|GrandFather::who()|

|GrandFather::fishing()|

|GrandFather::hungry()|

Father:

|Father::who()|

|GrandFather::fishing()|

|Father::hungry()|

|Father::cutting()|

Grandad:

|Grandad::who()|

|GrandFather::chessing()|

|GrandFather::hungry()|

Mother:

|Mother::who()|

|GrandFather::chessing()|

|Mother::hungry()|

|Mother::sewing()|

Child:

vptr1:

|Child::who()|

|GrandFather::fishing()|

|Child::hungry()|

|Father::cutting()|

|Child::studying()|

vptr2:

|Child::who()|

|GrandFather::chessing()|

|Child::hungry()|

|Mother::sewing()|

我们得到如下的结论:

1. Child有两个vptr,一个指向第一个base,一个指向第二个base,有多少base,就有多少vptrs;

2. Child的studying()函数只在第一个vptr中有索引,这样可以节省空间,这也就引出了第一个base是主要实体,后继的base都是次要实体;

3. 如果用Child指针或Father指针或GrandFather指针处理Child,包括析构,都没有区别,因为这个时候指针真正指向的Child Object的起始地址;

4. 如果用Mother指针或者Grandad指针处理Child,就必须调整this指针了。

如Mother * m = &child;

m的地址其实是&child+sizeof(Father),一般的处理也不会有什么大的问题,但是当出现delete m时,必须将调整m地址指向child的起始地址,但是这个时候,如何才能知道m实际指向的是Mother实例还是Child实例呢。







你可能感兴趣的