Inside the C++ Object Model 系列笔记 四 -- The Semantics of Function

c++支持三种类型的成员函数,分别为 static,nostatic,virtual。每一种调用方式都不尽相同。

nonstatic member function

C++的设计准则之一就是:nonstatic member function 至少必须和一般的 nonmember function 有相同的效率。
实际上,nonstatic member function 会被编译器进行如下的转换,变成一个普通函数:

1
Type1 X::foo(Type2 arg1) { ... }

会被转换为如下的普通函数:

1
void foo(X *const this, Type1 &__result, Type2 arg1) { ... }

改写函数原型,在参数中增加 this 指针,对每一个”nonstatic data member 的存取操作”改为由 this 指针来存取

将 member function 重写为一个外部函数,经过”mangling”处理(不需要处理的加上 extern “C”)

实际上,普通函数、普通成员函数、静态成员函数到最后都会变成与 C 语言函数类似的普通函数,只是编译器在这些不同类型的函数身上做了不同的扩展,并放在不同的 scope 里面而已。

编译器内部会将成员函数等价转换为非成员函数,具体是这样做的:

1.改写成员函数的签名,使得其可以接受一个额外参数,这个额外参数即是 this 指针:

1
2
3
float Point::X();
//成员函数X被插入额外参数this
float Point:: X(Point* this );

当然如果成员函数是 const 的,插入的参数类型将为 const Point* 类型。

2.将每一个对非静态数据成员的操作都改写为经过 this 操作。

3.将成员函数写成一个外部函数,对函数名进行“mangling”处理,使之成为独一无二的名称。

可以看出,将一个成员函数改写成一个外部函数的关键在于两点,一是给函数提供一个可以直接读写成员数据的通道;
二是解决好有可能带来的名字冲突。第一点通过给函数提供一个额外的指针参数来解决,第二点则是通过一定的规则将名字转换,使之独一无二。

于是在 VC 中对于上面的例子中的成员函数的调用将发生如下的转换:

1
2
3
4
//p->X();被转化为
?X@Point@@QAEMXZ(p);
//obj.X();被转化为
?X@Point@@QAEMXZ(&obj);

覆盖(override)、重载(overload)、隐藏(hide, overwrite)的区别:

  • 覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样(函数签名一样),只是函数的实现体不一样。
  • 重载是指 在同一个类中 不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。
  • 隐藏是指派生类中的函数把基类中相同名字的函数屏蔽掉了。隐藏与另外两个概念表面上看来很像,很难区分,其实他们的关键区别就是在多态的实现上。

C++多态(polymorphism)表示”以一个 public base class 的指针(或者 reference),寻址出一个 derived class object”

我专门写了一篇关于这些容易弄混的概念的文章:Override Overload Overwrite

Virtual Member Function

如果 function()是一个虚拟函数,那么用指针或引用进行的调用将发生一点特别的转换——一个中间层被引入进来。例如:

1
2
3
// p->function()
//将转化为
(*p->vptr[1])(p);
  • 其中 vptr 为指向虚函数表的指针,它由编译器产生。vptr 也要进行名字处理,因为一个继承体系可能有多个 vptr。
  • 1 是虚函数在虚函数表中的索引,通过它关联到虚函数 function().

何时发生这种转换?答案是在必需的时候 -- 一个再熟悉不过的答案。当通过指针调用的时候,要调用的函数实体无法在编译期决定,必需待到执行期才能获得,所以上面引入一个间接层的转换必不可少。但是当我们通过对象(不是引用,也不是指针)来调用的时候,
进行上面的转换就显得多余了,因为在编译器要调用的函数实体已经被决定。此时调用发生的转换,与一个非静态成员函数(Nonstatic Member Functions)调用发生的转换一致。p.function()的处理就跟非静态成员函数一样了。

Static Member Function

  • 不能够直接存取其类中的非静态成员(nostatic members),包括不能调用非静态成员函数(Nonstatic Member Functions)。
  • 不能声明为 const、volatile 或 virtual
  • 参数没有 this
  • 可以不用对象访问,直接 类名::静态成员函数 访问,当然,通过对象调用也被允许

需要注意的是通过一个表达式或函数对静态成员函数进行调用,被 C++ Standard 要求对表达式进行求值。如:

1
2
(a+=b).static_fuc();
func().static_fuc();

虽然省去对 a+b 求值对于 static_fuc()的调用并没有影响,但是程序员肯定会认为表达式 a+=b 已经执行,一旦编译器为了效率省去了这一步,很难说会浪费多少程序员多少时间去查找这个 bug。这无疑是一个明智的规定。func()返回一个对象。

vtable 的内容:

  • virtual class offset(有虚基类才有)
  • topoffset
  • typeinfo
  • 继承基类所声明的虚函数实例,或者是覆盖(override)基类的虚函数
  • 新的虚函数(或者是纯虚函数占位)

虚函数表的构造挺简单的:

虚函数表的构造

从内存布局的角度看,类对象继承基类的时候只把基类的 nonstatic data member 和 member function(函数入口,也可以说是函数指针) 放进自己内存里,static data member 和 static function 都在 global address 里面。然后就是虚函数表是复制了一份基类的虚函数表,然后把 virtual 实现了的部分替换掉,没实现的就不改,依然用父类的。然后虚函数表指针自然也要不一样,毕竟指向的内存地址不一样,对吧。