类成员函数
thiscall
约定
特定于 Microsoft
__thiscall
的调用约定用于 x86 体系结构上的 C++ 类成员函数。 它是成员函数使用的默认调用约定,该约定不使用变量参数(vararg
函数)。在
__thiscall
下,被调用方清理堆栈,这对于vararg
函数是不可能的。 自变量将从右到左推送到堆栈中。 指针this
通过注册 ECX 传递,而不是在堆栈上传递。

类成员构造函数,会将类指针写入eax
返回,而普通成员函数不会返回。

// 在汇编层面上,函数签名大致为:Class* __thiscall foo(Class* cl);
识别成员函数
构造函数
thiscall
约定。- 返回
this
指针。(eax
返回)
局部作用域

new
堆作用域

new
作用域更容易识别构造函数,因为构造函数的代码一定在检查之后,即使内联。
构造函数的返回值必须是
this
指针,局部作用域返回值在外部没用到。堆作用域,返回值要在外部进行检查,如果为空,那就不调用构造函数。
析构函数
thiscall
约定- 无参
- 无返回值
- 通过析构代理函数调用,通过参数决定是否调用释放
debug局部变量析构
与Release
版的关闭内联的汇编一致。

Debug手动析构
delete
会检查空指针。- 先调用析构,再调用
delete
释放内存。
显示调用析构不释放内存。
析构代理
// 函数签名void scalar_deleting_destructor(bool is_free); // 控制是否释放内存

push 1
表示释放内存push 0
表示仅析构,不释放内存。
无参成员函数

- 将结构体地址传递到
ecx
寄存器。 - 调用函数。
有参成员函数

- 通过栈传递参数。
- 通过
ecx
传递this
指针。 - 调用函数。
特殊
可以手动修改成员函数的调用约定。
手动修改成员函数调用约定在进行
FFI
的时候非常有用。比如想让rust
使用c++
类中的函数。

类数组
Person a[10];
类数组在初始化的时候要批量调用构造函数,还需要让delete[]
能够释放这一整块内存。所以类数组在申请内存的时候会多申请4
个字节,用来保存长度。

类构造迭代器
当使用Person ps[10];
这种方式定义类数组时,编译器将优化为使用类构造迭代器来进行循环构造。

void vector_constructor_iterator(void* ary_address, size_t objcet_size, size_t object_num, void (*)(void *), constructor);// 它还有一个eh vector constructor iterator 这个包含一个析构函数的参数void eh_vector_constructor_iterator(void* ary_address, size_t objcet_size, size_t object_num, void (*)(void *) constructor, void (*)(void *) destructor);

#include <cstdio>class Person{public: Person(){ std::printf("Person\n"); } ~Person(){ std::printf("~Person()\n"); }};int main(){ Person ps[10]; return 0;}
对象传递
类对象作为参数
特征:
- 函数外构造。
- 函数内析构。
不包含拷贝构造
#include <cstdio>class Person{
public: int age; char name[25]; Person(){ std::printf("Person\n"); } Person(int age){ std::printf("Person(int age)\n"); this->age = age; } ~Person(){ std::printf("~Person()\n"); } // 拷贝构造 // Person(const Person& other){ // this->age = other.age; // printf("Person(const Person& other)\n"); // }};void show_age(Person p){ printf("show_age\n"); printf("age: %d\n", p.age);}
int main(){ Person p1(13); show_age(p1); // 不产生新对象 show_age(12); // 产生一个临时对象 return 0;}
- 因为类不包含拷贝构造,所以传递的时候,
show_age(p1);
不进行新对象的产生。传递时,栈上需要用到类中的所有数据。 - 而
show_age(12)
参数是int
类型,所以需要在show_age(12)
这一行进行一次构造,构造后,不清理栈,此时栈上数据仍是类构造的数据。

包含拷贝构造
// 拷贝构造Person(const Person& other){ this->age = other.age; printf("Person(const Person& other)\n");}
- 拷贝构造的时机在函数调用之前。拷贝后,不清理堆栈。
-
例外
临时对象函数外构造、函数外析构。

类对象作为返回值
特征:
函数内构造。
函数外析构。
retrun Object
匿名形式
#include <cstdio>class Person{ int age;public: Person(){ std::printf("Person\n"); } Person(int age){ std::printf("Person(int age)\n"); this->age = age; } ~Person(){ std::printf("~Person()\n"); } // 拷贝构造 Person(const Person& other){ this->age = other.age; printf("Person(const Person& other)\n"); }};Person return_Person(int age){ printf("return_Person\n"); return Person(age);}
int main(){ return_person(12); Person p1 = return_person(15); const Person& p_ref = return_person(16); return 0;}
1. return_person(12)

首先,栈中没有分配空间。临时对象的生命周期在本行消失,也就是本行释放。
2. Person p1 = return_person(15)

返回值保存到栈中的局部对象。此时传参时,将ecx
作为this
指针提前放置,在构造时,使用局部变量的地址构造,省去一次拷贝构造。
3. const Person& p_ref = return_person(16)

临时变量延长生命周期,局部存在引用的空间。但是不发生拷贝构造,表现形式与上面相同。
虚函数
纯虚函数
class A{ // 至少拥有一个纯虚函数 => 抽象类 public: virtual void foo() = 0; // 纯虚函数};
pure-specifier
微软自己搞得语法,可以让纯虚析构函数,带有实现。派生类继承的时候,析构函数可以实现也可以不实现。
class Base{public: Base(); virtual void fun1() = 0; virtual void fun2() = 0; virtual ~Base() = 0 { // 带有默认实现的纯虚析构 微软自己搞得
};};
纯虚函数在虚表中的表现是一个__purecall
指针

当项目使用静态链接的时候,其中的实现如下:

核心代码是在这个函数中抛出一个异常,可以通过这个来识别纯虚函数的调用。

虚表
虚表存在.rdata
常量区,大小未知,只能通过偏移来猜大小。

当存在RTTI
时,常量区可以看到类的名称信息与作用域。

带有虚函数的构造
构造中需要填入虚表指针,虚表指针位置在类对象的首地址。

无内联

内联

如果基类的构造为默认,在内联时将被优化。
带有虚函数的析构
如果派生类带有虚析构,虚表指针中指向析构代理函数。
析构时,为了避免在析构函数中使用本类方法,所以需要先将虚表指针重新指向为本类虚表地址。
// Circle:Shapevirtual ~Circle(){ // 先调用派生类构造 this->xxx();}virtual ~Shape(){ // 再调用基类构造 // 如果不重置虚表指针,此时this->xxx()指向派生类xxx方法,因为派生类已经被析构,所以造成误解 this->xxx();}

继承
- 不带虚表的继承,难以通过构造函数分辨继承顺序。
- 调基类构造有可能是:基类成员对象变量初始化。

构造顺序
带有虚表的构造函数,以虚表指针填写为界限
- 虚表填写上方为基类构造。
- 下方为本类构造中的代码。

识别RTTI
继承顺序
虚表指针
->RTTI
->RTTI第一项
。



多继承
假设我们有A、B类,我们现在让AB
类多继承A
、B
。
class AB: public A, public B;
AB类对象的内存结构
+-----------------------+ <-- 对象起始地址 (例如 &objAB, ptrA, ptrAB)| AB::vptr (for A) | 指向 AB 类针对 A 基类的虚函数表 (vtable_AB_for_A) 的指针+-----------------------+| dataA | 来自基类 A 的数据成员 (int)+-----------------------+| AB::vptr (for B) | 指向 AB 类针对 B 基类的虚函数表 (vtable_AB_for_B) 的指针+-----------------------+| dataB | 来自基类 B 的数据成员 (int)+-----------------------+| dataAB | 来自派生类 AB 自身的数据成员 (int)+-----------------------+ <-- 对象内存末尾
构造特征
构造顺序:
- 使用A的虚表构造A
- 使用B的虚表构造B
- 填写AB类对A基类的虚表指针,填写AB类对B基类的虚表指针,来构造AB。

在构造AB的时候,虚表是AB对A或者AB对B的虚表,和A的虚表、B的虚表不是一个东西。
如果基类A存在虚函数`fun1`,派生类AB覆盖`fun1`,此时:A的虚表:{ A::fun1 }AB对A的虚表:{ AB::fun1() }两者不同,需要注意
注意与成员变量的区分
如果让AB类存在A、B两个类的成员。
此时内存结构:
+-----------------------+ <-- 对象起始地址 (例如 &objAB, ptrA, ptrAB)| A::vptr | 指向 A 基类的虚函数表 的指针+-----------------------+| dataA | 来自基类 A 的数据成员 (int)+-----------------------+| B::vptr | 指向 AB 类针对 B 基类的虚函数表 (vtable_AB_for_B) 的指针+-----------------------+| dataB | 来自基类 B 的数据成员 (int)+-----------------------+| dataAB | 来自派生类 AB 自身的数据成员 (int)+-----------------------+ <-- 对象内存末尾
内存结构的分布差不多,但是这里因为虚表指向的基类本身的,并不会重新覆盖。

还有一点:
派生类填写本身虚表之前为继承关系,之后为构造代码。
这里明显看到填写自身虚表之前不存在继承关系。
析构

虚继承
#include <iostream>#include <cstdio>using namespace std;// 多重继承class A{ int a = 10;public: A(){ std::cout << "A()" << std::endl; } virtual ~A(){ std::cout << "A::~A()" << std::endl; } virtual void fun1() = 0;};class B: public A{ int b = 20;public: B(){ std::cout << "B()" << std::endl; } virtual void fun1(){ printf("B::fun1()\n"); } virtual void funB(){ printf("B::funB()\n"); } virtual ~B(){ std::cout << "B::~B()" << std::endl; }};
class C: public A { int b = 20;public: C(){ std::cout << "C()" << std::endl; } virtual void fun1(){ printf("C::fun1()\n"); } virtual void funC(){ printf("C::funC()\n"); } virtual ~C(){ std::cout << "C::~C()" << std::endl; }};
class BC:public B, public C { int bc = 66;public: BC(){ std::cout << "BC()" << std::endl; } virtual void fun1(){ printf("BC::fun1()\n"); } virtual void funB(){ printf("BC::funB()\n"); } virtual void funC(){ printf("BC::funC()\n"); } virtual void funBC(){ printf("BC::funBC()\n"); } virtual ~BC(){ std::cout << "BC::~BC()" << std::endl; }};
int main(){ BC* bc = new BC(); __asm{ nop; nop; nop; nop; } bc->funC(); bc->funB(); bc->funBC(); delete bc; return 0;}
菱形继承
A / \ B C \ / BC
当出现菱形继承时,对BC
来说,同时存在两份A
的实例(由B
和C
分别提供)。
此时BC
的构造链:
- 按照继承顺序先构造
B::B()
B::B()
中调用A::A()
,构造一个A
对象。
- 然后构造
C::C()
C::C()
也由构造A::A()
,出现第二个A
对象。
- 最后填写
BC::vfptr(for B)
和BC::vfptr(for C)
构造自身BC::BC()
。
最终BC
的内存结构如下:
+-----------------------+ <-- 对象起始地址 (例如 &objA, ptrA)| BC::vfptr (for B) | BC对B的虚表指针+-----------------------+| B 子对象部分 || +---------------------+| | A对象部分 | 来自B中的A对象部分| +---------------------++-----------------------+| BC::vfptr (for C) | BC对C的虚表指针+-----------------------+| C 子对象部分 || +---------------------+| | A对象部分 | 来自C中的A对象部分| +---------------------++-----------------------+| BC 自身内部成员变量 | BC派生类自身成员变量部分+-----------------------+
此时出现冗余的A
对象,为了解决这个问题,我们可以让B
、C
虚继承A
。
class B:virtual public A;class C:virtual public A;
虚继承后的内存结构
- 虚基类仅构造一次,由最远的派生类构造。
虚继承的结构假设class B: virtual public A
+-----------------------+ <-- 对象起始地址 (例如 &objA, ptrA)| vbptr (for B) | 虚基类偏移表指针 (Virtual Base Pointer)+-----------------------+ - 指向 A 类针对虚基类 B 的虚基类表 (vbtable_A_for_B)| vptr (for A) | 虚函数表指针 (Virtual Table Pointer)+-----------------------+ - 指向 A 类自身的虚函数表 (vtable_A_for_A)| dataA_Derived | 派生类 A 自身的数据成员 (int)+-----------------------+| (padding) | 可能存在填充字节,为了内存对齐+-----------------------+| 虚基类 B 子对象部分 | **共享的虚基类 B 子对象 (在 A 对象内部间接包含)**| +---------------------+| | dataB_Base | 来自虚基类 B 的数据成员 (int)| +---------------------+| | vptr (for B) | 可能存在虚函数表指针 (vtable_A_for_B') for the B subobject within A (取决于编译器优化)| +---------------------++-----------------------+ <-- 对象内存末尾
虚表结构:
A: 0x0 vfptr{A::~A(), A::fun1()} 0x4 int A::a
B:A 0x0 vfptr{B::~B(), B::fun1(), B::funB()} 0x4 int A::a 0x8 int B::b

经过虚继承后:
A: 0x0 vfptr {A::~A(), A::fun1()} 0x4 int A::a
B:A 0x0 vfptr{B::~B(), B::fun1(), B::funB()} 0x4 int A::a 0x8 int B::b 0x0 vfptr(for B) {B::funB(void)} 0x4 vbptr B::vbtable 0x8 int B::b (B::member) 0xc B::`vftable'{for `A'} {B::~B(), B::fun1()} 0x10 int A::a (A::member)
虚继承后:
A
不变B
增加B::vbtable
和B::vftable (for A)
。B::vbtable
中存放B::vftable (for A)
在类中的偏移-
B
中对A
虚函数的覆盖,会写入到B::vftable (for A)
B
新增加的虚函数会增加一个B::vftable (for B)
的虚表指针,并在其中写入虚函数地址。
BC
的内存结构
A: 0x0 A::vftable {A::~A(), A::fun1()} 0x4 int A::a
B:A0x0 B::vftable (for B) {B::funB(void)}0x4 vbptr B::vbtable (for A) { 0xc }0x8 B::member { int B::b }0xc 0000 0000 // 分割 有可能有,也可能没有-------------- A 虚基类起始0x10 vftable B::vftable (for A) {B::~B(), B::fun1()}0x14 int A::a (A::member)-------------- A 虚基类结束
C:A0x0 vftable C::vftable (for C) {C::funC(void)}0x4 vbptr C::vbtable (for A) { 0xc }0x8 C::memeber { int C::c }0xc 0000 0000 // 分割 有可能有,也可能没有-------------- A 虚基类起始0x10 vftable C::vftable (for A) {C::~C(), C::fun1()}0x14 int A::a (A::member)-------------- A 虚基类结束
BC:B,A--------------B 基类开始0x0 vftable BC::vftable(for B) {BC::funBC(void)}0x4 vbptr B::vbtable { 0x1c } // 存储从此处到A虚基类的偏移量0x8 B member {int B::b}--------------B 基类结束--------------C 基类开始0xc vftable BC::vftable(for C)0x10 vbptr C::vbtable { 0x10 } // 存储从此处到A虚基类的偏移量0x14 C member { int C::c }--------------C 基类开始0x18 BC member { int BC::bc }0x1c 0000 0000 // 分割 有可能有,也可能没有--------------A 虚基类开始0x20 vftable A::vftable {}0x24 A member {int A::a}--------------A 虚基类结束


重要结论
- 虚基类一定在类的最后。
- 虚偏移表中存放的是从此处到虚基类的距离。
- 通过虚偏移表,我们可以计算出虚基类和派生类的空间大小。(仅限单继承)
虚继承后构造顺序
- 填写
BC::vbtable (for B)
,填写BC::vbtable (for C)
。 - 构造基类
- 由最后的派生类构造虚基类
A
。 - 构造
B
。- 如果
B
中有重写A
中的虚函数,则先覆盖A
虚表。 - 覆盖
B
虚表,调用B
构造。
- 如果
- 构造
C
。- 如果
C
中有重写A
中的虚函数,则先覆盖A
虚表。 - 覆盖
C
虚表,调用C
构造。
- 如果
- 构造
BC
- 如果
BC
中有对B
、C
中虚函数的覆盖,则覆盖B
、C
的虚表。 - 如果
BC
中有对A
中虚函数的覆盖,则覆盖A
的虚表。 - 调用构造代码。
- 如果
- 由最后的派生类构造虚基类
- 填写偏移表:
- 构造虚基类
A
。 - 构造
B
,因为不再构造A
,所以下面两个顺序可以乱。- 覆盖
A
中虚表。 - 覆盖自身虚表,构造自身。
- 覆盖
- 构造
C
,顺序也可以乱。- 覆盖
A
中虚表。 - 覆盖自身虚表,并构造自身。
- 覆盖
- 构造
BC
。- 覆盖
B
的虚函数表,覆盖C
的虚函数表。 - 覆盖
A
中虚表。 - 构造自身。
- 覆盖