1. This指针

This指针只有在调用class中的method的时候才会被当成参数传进去。假设我们有一个Person类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
public:
int age = 10;
int height = 180;

void WashFace() {
int a = age;
int b = height;
cout << "Age: " << age << ", Height: " << height;
}

private:

};

我们调用一下washFace(),然后反调试看一下底层到底发生了什么:

0.png

在washFace()的call之前,我们可以看到编译器把ebp-10的值放到ecx中了,其实现在ecx的值也就相当于第一个参数,有点__fastcall的意思。这个ebp-10很明显就是我们创建Person这个对象的地方,也就是Main中的第一个局部变量的地址。这个局部变量的地址就是this指针。

我们step in这个function看一看:

1.png

我们可以看到在填充完缓冲区之后,直接把ecx(即传进来的第一个参数)放到了[ebp-8],也就是第一个局部变量里面中。那么现在WashFace()这个函数的第一个局部变量就变成了this指针。之后获取成员和改变成员的操作也是通过这个this指针(即Main函数中创建对象的地址)进行修改的。

2. 构造函数

如果没有初始化你的成员,而且直接使用成员,这是很危险的。因为编译器不会给你自动初始化,那些成员现在的值都是stack或者heap中的垃圾。所以定义一个类,你一定要初始化成员。

第一种情况:在栈中创建的对象

假设我们有如下的class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
public:
int age;
int height;

Person(int age, int height) {
this->age = age;
this->height = height;
}

void WashFace() {
int a = age;
int b = height;
cout << "Age: " << age << ", Height: " << height;
}

private:

};

第一种情况:假设我们再栈中创建的对象:

我们底层看一下构造函数何时被调用:

3.png

我们创建的时候直接call了constructor。并且传入了三个参数:this 指针, 12h 和 0BEh。

我们看一下构造函数内部怎么做的:

4.png

还是一样的,把this指针里面的地址放到构造函数的第一个局部变量里。构造函数接下来使用this指针对Main中我们创建的对象修改。

所以本质上,我们修改成员变量都是通过this指针在创建对象的stack里面对成员变量进行修改。

就拿我们的例子来说,成员变量其实一直都在Main函数的Stack中。这些成员变量本质上其实是Main函数中连续的局部变量。

同样的,如果我们在Heap中创建对象,那么这个对象的成员其实都在heap中,不管怎么改成员都是通过this指针来修改heap。

第二种情况:使用new在堆中创建对象

底层代码如下:

6.png

我们可以看到首先调用了new这个方法(其实new最终调用的Win32API 为HeapAlloc)。之后再调用了构造函数。

我们可以看到调用了new之后还有个if…else… 这个判断就是如果内存分配成功执行构造函数,如果没有分配成功就跳过。因为可以看到cmp dword ptr [ebp-110h],0 ,这个里面的[ebp-110h]就来自调用new之后的返回值(第CE560E)的eax中,new返回的eax中放的其实就是开辟的地址,如果开辟成功就是开辟的地址,如果开辟失败则eax为0。

所以用new在堆中创建对象可以分为两个步骤

一, malloc再堆中开辟内存

二, 判断是否malloc成功,如果成功则执行构造函数;如果malloc失败则不执行构造函数

3. 析构函数

第一种情况: 对象在栈中

大家都知道析构函数是在对象被销毁的时候被调用。这种概念很模棱两可,什么时候才是销毁的时候呢?

我们可以通过底层来看看析构函数到底是什么时候被调用的。

假设我们有如下的析构函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person
{
public:
int age;
int height;

Person(int age, int height) {
this->age = age;
this->height = height;
}

~Person() {
cout << "I am destoried\n";
}

void WashFace() {
int a = age;
int b = height;
cout << "Age: " << age << ", Height: " << height;
}

private:

};

我们反汇编一下看一下到底在哪调用~Person这个析构函数。

5.png

我们可以看到,如果我们在main函数里面创建了Person对象,那么Main函数结束时就会马上调用析构函数,并且还是传入this指针。

所以如果是在某个函数中创建了对象,那么析构函数会在这个函数结束时被调用。

如果是在全局区创建了对象,那么析构函数会在整个程序关闭的同时被调用。

第二种情况:对象在堆中,使用delete摧毁对象

7.png

首先需要call一个scalar deleting destructor的函数,这个函数第二个参数。我们可以看到push 1其实就是需要执行删除的次数。

我们跳进去看一下:

8.png

所以呢,delete在这里实际上也做了两件事情:

一, 调用所有对象的析构函数

二, free堆中的内存