深度探索C++对象模型<九>

Member的各种调用方式

Nonstatic Member Functions

由于C++的一个设计标准就是使得nonstatic member function要有和一般的nonmember function有相同的效率;

这就带来了一个变化,就是编译器内部会将nonstatic member function转换为等价的nonmember function实体;

转化步骤:

  • 改写函数的原型,安插一个新的参数,即this指针;
  • 将每一个对nontatic data member的存取操作改写为通过this指针的操作;
  • 将member function改写成一个外部函数,修改函数名称;

例如一个这样的函数:

1
2
3
4
5
6
7
8
Point3d Point3d::normalize() const
{
register float mag = magnitude();
Point3d normal;
normal.x = x/mag;
normal.y = y/mag;
normal.z = z/mag;
}

将可能会改写成:

1
2
3
4
5
6
void normalize_7Point3dFv(register const Point3d *const this, Point3d &__result)
{
register float mag = this->magnitude();
__result.Point3d::Point3d(this->x/mag, this->y/mag, this->z/mag);
return;
}

特别的,编译器内部会对名称进行mangling处理,虽然目前处理方式没有统一的标准,但一般来说会在member名称前面加上类名,甚至为了保证重载的操作,会加上函数参数的类型,当然如果声明了extern C,就会抑制了这种效果,这也是extern关键字的一个重要功能。

有时我们看到的编译器报错,显示了非常奇怪的函数名称报错,往往就是因为name mangling的原因。

Virtual member functions

如果normalize()是一个虚函数,那么以下调用会转化为:

1
2
ptr->normalize();
(*ptr->vptr[1])(ptr);

1就是virtual function slot的索引值,关联到normalize()函数。

而如果里面的magnitude()函数也是虚函数,那么由于normalize会先调用,决定了object的类型,所以编译器会使用更加明确的调用方式,而不是(*ptr->vptr[2])(ptr);例如:

1
register float mag = Point3d::magnitude();

Static Member Functions

如果normalize是一个静态函数,那么这两种调用会变为:

1
2
3
4
obj.normalize();
prt->normalize();
//会转为
normalize_7Point3dSFv();

由于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
2
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

那么无论是通过prt去调用,还是通过pmf的间接调用,虚拟机制都是有效的:

1
2
ptr->z();
(ptr->*pmf)();

上一节提到过,对一个nonstatic member function取地址,得到的是该函数在内存中的地址,然而对于一个virtual function来说,其地址在编译器是位置的,因此如果对这样的函数取地址,返回的将会是一个索引值。

1
2
3
4
5
6
7
8
9
class Point
{
public:
virtual ~Point();
float x();
float y();
virtual float z();
//...
};

进行各种取地址操作:

1
2
3
4
&Point::~Point; //1
&Point::x();//返回的是这两个函数在内存中的地址
&Point::y();
&Point::z();//2

多重继承下,指向member function的指针

为了让指向member functions的指针能够支持多重继承和虚拟继承,c++设计了这样一个结构:

1
2
3
4
5
6
7
8
struct __mptr{
int delta;//this指针的offset值
int index;//virtual table的索引
union {
ptrtofunc faddr;//nonvirtual member function地址
int v_offset;//virtual base class的vptr的位置
};
};

Inline Functions

一个inline函数,在被编译器处理的过程中,有两个阶段:

  • 分析函数定义,判断函数的intrinsic inlien ability。如果函数因其复杂性,或者内部的构建问题,被判断为不可inline,那么它会被转为一个static函数,然后在被编译的模块中产生相应的函数定义;
  • 真正的inline函数扩展是在调用的那一点上,这会产生参数的求值操作和临时性的对象管理。

formal arguments

在inline函数扩展期间,每一个形参都被实参所代替,但如果参数有副作用,就不能简单地替换,因为这可能会导致实参多次求值。为了避免这个问题,通常会引进临时对象。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline int min(int i, int j)
{
return i < j ? i : j;
}

inline int bar()
{
int minval;
int val1 = 1024;
int val2 = 2048;
/* 1 */ minval = min(val1, val2);
/* 2 */ minval = min(1024, 2048);
/* 3 */ minval = min(foo(), bar()+1);
return minval;
}

//第一个 参数直接替换
minval = val1 < val2 ? val1 : val2;
//第二个 直接使用常量
minval = 1024;
//第三个 引入临时对象
int t1, t2;
minval = (t1 = foo()), (t2 = bar()+1), t1<t2 ? t1 : t2;

局部变量

如果inline函数有局部变量,为了避免命名冲突,inline函数的每一个局部变量都必须被封装在函数调用的一个scope中,拥有独一无二的名称。

inline函数是#define的一个安全替代品,避免了宏中出现副作用参数的问题。但如果一个inline函数被调用多次,也会产生大量的扩展码。