虽然iOS中对C++的要求并不是必须的,但是很多基础都是用C++写的。我们都说C++难学,除了众多的特性以及为了兼容底层语言的一些妥协,导致特性极其复杂,虽然经过C++11之后的标准,已经更加完善和易用了,但我们还是需要去了解一些C++的实现原理及特性,才能避免掉入坑中。

这里将不会一一说明C++的基础知识,我相信有很多人应该比我说的更好。

虚函数

C++的虚函数实现是通过虚函数表,这是大家都知道的。那么虚函数表是一个什么样的结构呢?

这里我们用类C语言的描述来说明一下。

class A {
public:
  virtual void f() {}
};

class B : public A {
  void f() override {}
  virtual void g() {}
};

以上用C结构表示大概是这样的:

struct vtable_A {
  void f(A *this) {}
}

struct A {
  vtable_A *__vtable;
};

struct vtable_B {
  vtable_A _super;
  void g(B *this) {}
}

struct B {
  vtable_B *__vtable;
};

编译器会在类中自动生成一个指向虚函数表的指针,并在构造类的时候帮我们把对应的虚函数指针填上。那么如果我们重写了某个基类的虚函数的时候,我们的虚函数表相对应的虚函数会改写为子类的函数指针,如下:

vtable_A:  | A::f() |
如果没有重写f(),B的虚函数表应该如下:
vtable_B:  | A::f() | B::g() |
如果重写了f(),B的虚函数表则会变为:
vtable_B:  | B::f() | B::g() |

所以我们从表中查找出来的将会是B::f。这也解释了为什么C++类,只含有虚函数的class,size也是需要有一个指针的大小。

当有多个基类的时候,这时候为了分辨多个来源的时候,将会采用多个虚函数表结构:

class A {
  virtual void f() {}
};

class B {
  virtual void f() {}
  virtual void g() {}
};

class C {
  void f() override {}
}

这时候的C的虚函数表将会是这种感觉:

vtable_A:  | C::f() |
vtable_B:  | C::f() | B::g() |
class C {
  vtable_A *__vtable_a;
  vtable_B *__vtable_b;
}

而更复杂的情况也是这目前的扩展,这里就不赘述了。

函数参数

我们都知道,C++能和C无缝链接,其实就是对C语言的一个扩展,其大部分机制,比如参数传递都是和C语言一模一样的。而C语言是不支持this这种语法的,那么C++是如何传递参数的呢?

虽然我们看来A::f()就是该函数定义,但实际上我们是有一个this的隐式参数,也就是说A::f(A *this)才是该函数的真正定义。

结合上节,那么我们就可以动态的取出vtable内的函数指针,进行调用,实现类似“动态语言”的某些特性。

typedef void (*Fun)(void*);
C c;
uintptr_t cptr = reinterpret_cast<uintptr_t> (&c);
uintptr_t *vtable = reinterpret_cast<uintptr_t *> (cptr);
Fun *vtc = reinterpret_cast<Fun *>(*(vtable + 0));
Fun f = *(vtc);
f(&c);

多基类的this指针

上节我们知道this指针将会是一个隐式参数,那么出现多个基类的时候,又是如何处理的呢?

class A {
public:
    virtual void f() {
        std::cout << __PRETTY_FUNCTION__ << " this: " << this << std::endl;
    }
    virtual void g() {
        std::cout << __PRETTY_FUNCTION__ << " this: " << this << std::endl;
    }
};

class B {
public:
     virtual void f() {
         std::cout << __PRETTY_FUNCTION__ << " this: " << this << std::endl;
     }
    virtual void k() {
        this->f();
        std::cout << __PRETTY_FUNCTION__ << " this: " << this << std::endl;
    }
};

class C : public A, public B {
public:
    void f() override {
        std::cout << __PRETTY_FUNCTION__ << " this: " << this << std::endl;
    }
};

如果我们调用

C c;
c.k()

此时调用的是子类的C::f()还是自己的B::f()呢?

按照我们对面向对象语言的理解和要求,最终调用的肯定是C::f()。但是按照vtable的布局要求,B::f()中this所指向的是&c+8的地址,那么基类又是怎么知道最终需要调用C::f()并且找到子类的地址&c呢?

以上代码的打印为:

class C
virtual void C::f() this: 0x7ffeefbff560
virtual void B::k() this: 0x7ffeefbff568

可以看到this指针的确是真实的vtable指针,概括为C语言大概是这样的:

struct C c;
B::k( &c + 8 /* sizeof(B::f) */ );

// 而k()中的调用可以解释为
void B::k(B *this) {
  func_ptr p = vtable_get(this, B::f);
  p(this);
}

要知道编译器具体为我们做了什么魔法操作,那么我们就需要看看其汇编结果了。

this->f()单步跟踪,我们会发现他会进入一个奇怪的函数。

0x100001620 <+0>:  pushq  %rbp
0x100001621 <+1>:  movq   %rsp, %rbp
0x100001624 <+4>:  movq   %rdi, -0x8(%rbp)
0x100001628 <+8>:  movq   -0x8(%rbp), %rdi
0x10000162c <+12>: addq   $-0x8, %rdi
0x100001630 <+16>: popq   %rbp
0x100001631 <+17>: jmp    0x100001540               ; C::f at main.cpp:103

说明我们虚函数表里面的C::f()并不是我们所写的那个方法,而是编译器帮我们封装了一层,专门进行了this-8这样的还原指针地址的操作。

可以说这一顿操作真的是很厉害,我们平时用的时候都不曾注意这些,也不曾深入了解为什么C++中有些方法必须是虚函数,或者有些时候虚函数存在设计缺陷。

不过在debug的过程中,xcode中this指针永远是显示的子类的指针,不知道做了如何的处理。

最后

C++依然是一门比较复杂的语言,虽然我个人还是比较喜欢C++的特性,拥有非常大的灵活性,但是不得不说要用好还是非常难的。