虽然iOS中对C++的要求并不是必须的,但是很多基础都是用C++写的。我们都说C++难学,除了众多的特性以及为了兼容底层语言的一些妥协,导致特性极其复杂,虽然经过C++11之后的标准,已经更加完善和易用了,但我们还是需要去了解一些C++的实现原理及特性,才能避免掉入坑中。
这里将不会一一说明C++的基础知识,我相信有很多人应该比我说的更好。
虚函数
C++的虚函数实现是通过虚函数表,这是大家都知道的。那么虚函数表是一个什么样的结构呢?
这里我们用类C语言的描述来说明一下。
1 | class A { |
以上用C结构表示大概是这样的:
1 | struct vtable_A { |
编译器会在类中自动生成一个指向虚函数表的指针,并在构造类的时候帮我们把对应的虚函数指针填上。那么如果我们重写了某个基类的虚函数的时候,我们的虚函数表相对应的虚函数会改写为子类的函数指针,如下:
1 | vtable_A: | A::f() | |
所以我们从表中查找出来的将会是B::f。这也解释了为什么C++类,只含有虚函数的class,size也是需要有一个指针的大小。
当有多个基类的时候,这时候为了分辨多个来源的时候,将会采用多个虚函数表结构:
1 | class A { |
这时候的C的虚函数表将会是这种感觉:
1 | vtable_A: | C::f() | |
而更复杂的情况也是这目前的扩展,这里就不赘述了。
函数参数
我们都知道,C++能和C无缝链接,其实就是对C语言的一个扩展,其大部分机制,比如参数传递都是和C语言一模一样的。而C语言是不支持this这种语法的,那么C++是如何传递参数的呢?
虽然我们看来A::f()
就是该函数定义,但实际上我们是有一个this的隐式参数,也就是说A::f(A *this)
才是该函数的真正定义。
结合上节,那么我们就可以动态的取出vtable内的函数指针,进行调用,实现类似“动态语言”的某些特性。
1 | typedef void (*Fun)(void*); |
多基类的this指针
上节我们知道this指针将会是一个隐式参数,那么出现多个基类的时候,又是如何处理的呢?
1 | class A { |
如果我们调用
1 | C c; |
此时调用的是子类的C::f()还是自己的B::f()呢?
按照我们对面向对象语言的理解和要求,最终调用的肯定是C::f()。但是按照vtable的布局要求,B::f()中this所指向的是&c+8
的地址,那么基类又是怎么知道最终需要调用C::f()并且找到子类的地址&c
呢?
以上代码的打印为:
1 | class C |
可以看到this指针的确是真实的vtable指针,概括为C语言大概是这样的:
1 | struct C c; |
要知道编译器具体为我们做了什么魔法操作,那么我们就需要看看其汇编结果了。
在this->f()
单步跟踪,我们会发现他会进入一个奇怪的函数。
1 | 0x100001620 <+0>: pushq %rbp |
说明我们虚函数表里面的C::f()
并不是我们所写的那个方法,而是编译器帮我们封装了一层,专门进行了this-8
这样的还原指针地址的操作。
可以说这一顿操作真的是很厉害,我们平时用的时候都不曾注意这些,也不曾深入了解为什么C++中有些方法必须是虚函数,或者有些时候虚函数存在设计缺陷。
不过在debug的过程中,xcode中this指针永远是显示的子类的指针,不知道做了如何的处理。
最后
C++依然是一门比较复杂的语言,虽然我个人还是比较喜欢C++的特性,拥有非常大的灵活性,但是不得不说要用好还是非常难的。