Member的各种调用方式
Nonstatic Member Functions
由于C++的一个设计标准就是使得nonstatic member function要有和一般的nonmember function有相同的效率;
这就带来了一个变化,就是编译器内部会将nonstatic member function转换为等价的nonmember function实体;
转化步骤:
- 改写函数的原型,安插一个新的参数,即this指针;
- 将每一个对nontatic data member的存取操作改写为通过this指针的操作;
- 将member function改写成一个外部函数,修改函数名称;
例如一个这样的函数:
1 | Point3d Point3d::normalize() const |
将可能会改写成:
1 | void normalize_7Point3dFv(register const Point3d *const this, Point3d &__result) |
特别的,编译器内部会对名称进行mangling处理,虽然目前处理方式没有统一的标准,但一般来说会在member名称前面加上类名,甚至为了保证重载的操作,会加上函数参数的类型,当然如果声明了extern C,就会抑制了这种效果,这也是extern关键字的一个重要功能。
有时我们看到的编译器报错,显示了非常奇怪的函数名称报错,往往就是因为name mangling的原因。
Virtual member functions
如果normalize()是一个虚函数,那么以下调用会转化为:
1 | ptr->normalize(); |
1就是virtual function slot的索引值,关联到normalize()函数。
而如果里面的magnitude()函数也是虚函数,那么由于normalize会先调用,决定了object的类型,所以编译器会使用更加明确的调用方式,而不是(*ptr->vptr[2])(ptr);例如:
1 | register float mag = Point3d::magnitude(); |
Static Member Functions
如果normalize是一个静态函数,那么这两种调用会变为:
1 | obj.normalize(); |
由于static member function没有this指针,所以:
- 不能直接存取其中class的nonstatic members;
- 不能被声明为const, volatile或者virtual;
- 不需要经由对象调用(但是如果static member是一个private,就很可能要依赖于对象);
如果要取一个static member function的地址,那么其类型会是:unsigned int (*)();
Virtual Member Functions
为了支持virtual function机制,必须要有某种策略能够在运行期进行类型判断。考虑这样的一个调用:
1 | ptr->z() |
这样的一个调用需要两个信息:
- 指针指向的对象的地址;
- 对象类型的某种编码或者某种结构的地址;
为了提高效率,不支持多态的类是不需要这些额外的信息的。因此我们可以通过类中是否含有virtual functions判断是否支持多台。
那么virtual function是如何被构建的呢?每一个类都会有一个virtual table,而每一个table内含其对应的类对象所有虚函数的地址,每一个虚函数都会被指派一个索引值。而每个类对象都会被编译器安插一个指针,指向该virtual table。
一个类继承基类的时候:
- 继承所有的虚函数实体,将这些函数实体的地址拷贝到派生类的虚函数表的slot中;
- 填写自定义的虚函数地址到slot中;
- 新加的虚函数,新加一个slot;
多重继承下的virtual function
考虑这样的继承关系:
derived : base1, base2
这种多重继承的关键在于base2 subobject的身上:假如为派生对象指派一个base2的指针:
1 | Base2 *pbase2 = new Derived; |
那么在编译器需要调整指针的指向,使其指向base2 subobject的位置。不然是无法通过指针调用。
指向member function的指针
前面我们提到过,取一个nonstatic data menber的地址,如果该函数是nonvirtual的,则得到的是它在内存中的地址,但是这个地址也是依赖于对象地址的。
事实上,一个member function的指针,如果不用于virtual function这些情况的话,它的效率并不会比使用一个nonmember function的指针更高
支持指向virtual member functions的指针
考虑这样的一个程序片段:
1 | float (Point::*pmf)() = &Point::z; |
那么无论是通过prt去调用,还是通过pmf的间接调用,虚拟机制都是有效的:
1 | ptr->z(); |
上一节提到过,对一个nonstatic member function取地址,得到的是该函数在内存中的地址,然而对于一个virtual function来说,其地址在编译器是位置的,因此如果对这样的函数取地址,返回的将会是一个索引值。
1 | class Point |
进行各种取地址操作:
1 | &Point::~Point; //1 |
多重继承下,指向member function的指针
为了让指向member functions的指针能够支持多重继承和虚拟继承,c++设计了这样一个结构:
1 | struct __mptr{ |
Inline Functions
一个inline函数,在被编译器处理的过程中,有两个阶段:
- 分析函数定义,判断函数的intrinsic inlien ability。如果函数因其复杂性,或者内部的构建问题,被判断为不可inline,那么它会被转为一个static函数,然后在被编译的模块中产生相应的函数定义;
- 真正的inline函数扩展是在调用的那一点上,这会产生参数的求值操作和临时性的对象管理。
formal arguments
在inline函数扩展期间,每一个形参都被实参所代替,但如果参数有副作用,就不能简单地替换,因为这可能会导致实参多次求值。为了避免这个问题,通常会引进临时对象。举个例子:
1 | inline int min(int i, int j) |
局部变量
如果inline函数有局部变量,为了避免命名冲突,inline函数的每一个局部变量都必须被封装在函数调用的一个scope中,拥有独一无二的名称。
inline函数是#define的一个安全替代品,避免了宏中出现副作用参数的问题。但如果一个inline函数被调用多次,也会产生大量的扩展码。