Function 语意学
Member Function的各种调用方式
Nonstatic Member Functions
C++的设计准则之一就是:成员函数要和一般函数拥有同样的效率,成员函数不应该带来额外的负担,这也就是为什么每一个成员函数的参数列表都隐含一个this指针的原因
那么member function是如何转化为nonmember function的呢?具体如下
1 | /*1.改写函数的signature,为的是把this指针作为参数传入*/ |
名称的特殊处理(Name Mangling)
其实在上面所述的步骤中,比较复杂的一点在于Name Mangling,生成独一无二的命名
之前在讨论函数重载和返回值的关系之时,也曾略微讨论过这个话题
其实到目前为止,编译器并没有统一的编码方法,不过大概的规则也就是函数名+参数个数+参数类型,因此只需知道存在此步骤即可
Static Member Functions
以下是static member functions的特性
- 不能直接存取class中的nonstatic members
- 不能被声明为const, volatile, virtual
- 不需要经由class object才被调用
使用member selection语法(使用class object调用)是一种符号上的便利,实际上会转化为直接的调用操作
1 | if (foo().object_count() > 1) {} |
如下所示,static member function的地址类型其实是nonstatic function
1 |
|
因此,可以作为回调函数,因为该函数不涉及具体的class object,可作为一般函数指针传入
Virtual Member Functions
我们都知道,对于Virtual Member Functions的调用实际上是通过vptr所指向的vtbl进行决议的
1 | //假设foo是一个virtual member function |
通过vptr所指向的vtbl,再通过索引,获得对应函数指针并进行调用
但是如果是经由一个class object调用一个virtual function,这种操作总会被编译器当作一般的nonstatic member function,所进行的转化就如之前所述
为了支持virtual function 机制,首先需要对多态的对象有某种执行期类型判断法,也就是说以下的调用操作需要ptr执行期的某些相关信息
1 | ptr->z(); |
我们需要判断这个调用是否涉及到多态,如果涉及到,应该怎么决议才能调用正确的z()
首先考虑,假设将以下信息和指针放在一起
- 所参考的对象的地址
- 对象类型的某种编码,用来正确决议出
z()
如果是这样做的话,那么
- 会增加空间成本
- 不再兼容C
因此,可以考虑将额外的信息放在对象本身,因为在C++中并没有polymorphic
之类的关键字,因此判断一个class
是否支持多态,唯一合适的方法就是看看它是否有任何的virtual funciton
,如果持有,那么它就需要这份额外的信息来支持多态机制
知道了额外信息应该放到什么地方,下一步就是决定应该存放何种额外信息
试想,如何才能正确决议一个virtual function
的实例?我们应该需要什么信息才能决议?
- ptr所指的对象的真实类型
- z()实例的位置
为了得知以上信息,我们需要在class object中增添
- 一个字符串/数字,标示class的类型
- 一个指针(vptr),指向某个表格(vtbl),表格中存放
virtual function
的执行期地址
object中存放指向表格的指针,然后为了找到函数地址,每一个virtual function被指派一个表格索引,这些工作都是在编译期中完成的
首先我们来理一下
- object持有的是vptr
- class持有的是vtbl,在单继承每一个class只有一个vtbl
然后,通过下面的例子,我们很容易得出结论
- 我们不知道
p
所指向的对象的类型,但是可以通过p
存取到该对象的vptr,继而通过vptr获得该对象类型(class)的vtbl- 因为虚函数需要通过vptr进行存取,然后vptr是存放在对象中的,对象是通过构造函数进行构建的,因此,vptr的生成和赋值是在构造函数中的,这就是为什么构造函数不能是
vitrual
的理由
- 因为虚函数需要通过vptr进行存取,然后vptr是存放在对象中的,对象是通过构造函数进行构建的,因此,vptr的生成和赋值是在构造函数中的,这就是为什么构造函数不能是
- 我们不知道哪一个
say()
会被调用,但是知道每一个say()
的位置都是在表中固定的位置的,对于同一个virtual function
,在父类和子类的虚函数表中的索引都是一样的
1 |
|
Q1
接下来回答一个问题:为什么我们不知道p所指向的对象的类型呢?看都能看出来啊,既然p=&y
,y是T2
类型的,那么p指向的对象类型肯定是T2
啊
就我个人理解,在程序编译时的解析中,变量的类型已经绑定好了,例如会生成如下的信息
1 | <T1 *, p>; |
p所指的内存空间,仍会解析为T1类型,因此,p不能访问到T2的方法,但是可以访问到T2和T1共有的成员变量,如vptr
我们不知道p所指向的对象的类型,其实是从编译器的角度来说的,编译器无法直接生成确定的调用,如下
1 | p->say(); |
这也就是为什么称之为动态绑定的原因
Q2
打印vtbl
1 |
|
多重继承下的Vitrual Functions
在多重继承的模型下,一个class可能会拥有多个vtbl
如下的继承关系
1 | class Base; |
所进行的构造顺序为Base->Derive->DD,所进行的析构顺序为DD->Derive->Base
1 |
|
根据以上的测试代码可知,DD含有多个VTBL
pdd所在的位置存在一个vptr,指向Base所拥有的vtbl
pdd+Base得到另一个vptr,指向Derive所拥有的vtbl,因此在调用Derive的析构函数的时候会对指针进行调整
thunk技术
所谓thunk是一小段的汇编代码,用来
- 以适当的offset调整this指针
- 跳到virtual function中去
虚继承下的Virtual Functions
虚拟继承是多重继承中特有的概念
虚拟基类是为解决多重继承而出现的,如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数
为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类
然后,不要在virtual base class中声明nonstatic data members,因为存在共享的数据
假设在A中存在nonstatic data member,然后我调用B1的方法将其修改,就会影响到B2的访问,这就是深渊!
指向Member Function的指针
取一个不绑定与任何object的nonstatic data member的地址,得到的是在class布局中的bytes位置,如果想存取该data member,需要将其绑定到具体的class object上
1 |
|
取一个 nonstatic member function的地址,如果该函数是nonvirtual,得到的结果是它在内存中真正的地址,但是如果不将其绑定到某个class object上,就无法通过它来调用该函数(因为nonstatic函数需要对具体的data member进行操作,因此需要绑定具体的object来进行调用,例如需要使用test.func()而不能通过Test::func()来调用func)
如果是static member function则可以看作是普通的函数,使用一般的函数指针
1 | class Test |
在上面的例子中,(t.*pmf)()
中,t作为*this
指针的提供者,只有这样才能通过对应的指针调用nonstatic member function