介绍

前面讲了一点类与结构体的反汇编分析,这篇简单介绍其构造函数与析构函数的相关知识。

构造函数和析构函数都是类中特殊的成员函数,构造函数支持重载,析构函数只能是一个无参函数,且他们都不可定义返回值。调用构造函数后,返回值为对象首地址,也就是this指针。

构造函数出现时机

对象定义时,构造函数同时调用。因此知道了对象的生命周期,其构造函数便可推断。我们按照生命周期不同分以下几种对象来讨论。

局部对象

编译器隐藏了构造函数的调用过程,我们试分析以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

class Person {
public:
Person() { //无参构造函数
age = 20;
}
int age;
};

int main(int argc, char* argv[]) {
Person person; //类对象定义
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.text:0000000140001530 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000140001530 public main
.text:0000000140001530 main proc near ; CODE XREF: __tmainCRTStartup+210↑p
.text:0000000140001530 ; DATA XREF: .pdata:000000014000506C↓o
.text:0000000140001530
.text:0000000140001530 var_4 = byte ptr -4
.text:0000000140001530 arg_0 = dword ptr 10h
.text:0000000140001530 arg_8 = qword ptr 18h
.text:0000000140001530
.text:0000000140001530 push rbp
.text:0000000140001531 mov rbp, rsp
.text:0000000140001534 sub rsp, 30h
.text:0000000140001538 mov [rbp+arg_0], ecx
.text:000000014000153B mov [rbp+arg_8], rdx
.text:000000014000153F call __main
.text:0000000140001544 lea rax, [rbp+var_4]
.text:0000000140001548 mov rcx, rax ; this
.text:000000014000154B call _ZN6PersonC1Ev ; Person::Person(void)
.text:0000000140001550 mov eax, 0
.text:0000000140001555 add rsp, 30h
.text:0000000140001559 pop rbp
.text:000000014000155A retn
.text:000000014000155A main endp


.text:0000000140002860 ; Person *__fastcall Person::Person(Person *__hidden this)
.text:0000000140002860 public _ZN6PersonC1Ev
.text:0000000140002860 _ZN6PersonC1Ev proc near ; CODE XREF: main+1B↑p
.text:0000000140002860 ; DATA XREF: .pdata:000000014000521C↓o
.text:0000000140002860
.text:0000000140002860 arg_0 = qword ptr 10h
.text:0000000140002860
.text:0000000140002860 push rbp
.text:0000000140002861 mov rbp, rsp
.text:0000000140002864 mov [rbp+arg_0], rcx
.text:0000000140002868 mov rax, [rbp+arg_0]
.text:000000014000286C mov dword ptr [rax], 14h
.text:0000000140002872 nop
.text:0000000140002873 pop rbp
.text:0000000140002874 retn
.text:0000000140002874 _ZN6PersonC1Ev endp

进入对象作用域时调用构造函数。因为是成员函数,同样使用cx寄存器传递this指针。调用结束后,将对象地址作为返回值。

堆对象

有如下实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

class Person {
public:
Person() {
age = 20;
}
int age;
};

int main(int argc, char* argv[]) {
Person *p = new Person;
//为了突出本节讨论的问题,这里没有检查new运算的返回值
p->age = 21;
printf("%d\n", p->age);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.text:0000000140001530 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000140001530 public main
.text:0000000140001530 main proc near ; CODE XREF: __tmainCRTStartup+210↑p
.text:0000000140001530 ; DATA XREF: .pdata:000000014000A06C↓o
.text:0000000140001530
.text:0000000140001530 var_18 = qword ptr -18h
.text:0000000140001530 arg_0 = dword ptr 10h
.text:0000000140001530 arg_8 = qword ptr 18h
.text:0000000140001530
.text:0000000140001530 push rbp
.text:0000000140001531 push rbx
.text:0000000140001532 sub rsp, 38h
.text:0000000140001536 lea rbp, [rsp+30h]
.text:000000014000153B mov [rbp+10h+arg_0], ecx
.text:000000014000153E mov [rbp+10h+arg_8], rdx
.text:0000000140001542 call __main
.text:0000000140001547 mov ecx, 4 ; unsigned __int64
.text:000000014000154C call _Znwy ; operator new(ulong long)
.text:0000000140001551 mov rbx, rax
.text:0000000140001554 mov rcx, rbx ; this
.text:0000000140001557 call _ZN6PersonC1Ev ; Person::Person(void)
.text:000000014000155C mov [rbp+10h+var_18], rbx
.text:0000000140001560 mov rax, [rbp+10h+var_18]
.text:0000000140001564 mov dword ptr [rax], 15h
.text:000000014000156A mov rax, [rbp+10h+var_18]
.text:000000014000156E mov eax, [rax]
.text:0000000140001570 mov edx, eax
.text:0000000140001572 lea rax, aD ; "%d\n"
.text:0000000140001579 mov rcx, rax ; char *
.text:000000014000157C call _Z6printfPKcz ; printf(char const*,...)
.text:0000000140001581 mov eax, 0
.text:0000000140001586 add rsp, 38h
.text:000000014000158A pop rbx
.text:000000014000158B pop rbp
.text:000000014000158C retn
.text:000000014000158C main endp

使用Person *p = new Person()这样的方式申请堆对象时,会调用Person类的无参构造函数。使用vs编译器时,若堆空间申请失败,则会避开函数调用。

另外new和malloc区别很大,malloc不负责触发构造函数,也不是运算符,无法进行运算符重载。

参数对象

返回对象

在使用指针作为参数和返回值时,函数内没有对构造函数的调用。以此可以分辨参数或返回值是对象还是对象的指针。

全局对象与静态对象

全局对象与静态对象构造时机相同,被隐藏在深处。windows位于vs2019的启动函数mainCRTStartup中的_cinit函数中实现了全局对象的构造函数初始化。

默认构造函数

有些情况下编译器不会提供默认构造函数。甚至在优化编译后简单的类会被转化为连续定义的变量,完全不需要构造函数。那么什么情况下编译器会提供默认构造函数?

  • 本类和本类中定义的成员对象或者父类中存在虚函数
    • 这种情况下必须存在构造函数对虚表指针进行隐式初始化,所以没有定义构造函数的情况下编译器会添加默认的构造函数来进行虚表的初始化。
  • 父类或本类中定义的成员对象带有构造函数
    • 若为派生类,构造顺序是先构造父类再构造自身。因此这种情况下编译器会添加默认构造函数并在构造函数中完成父类构造函数的调用。成员对象带有构造函数的情况相同。
    • 若本类、父类、成员对象都没有定义构造函数,也不存在虚函数,那么默认构造函数将会失去意义,因此编译器不会提供默认构造函数。

析构函数出现时机

类似的,对象根据对象所在的作用域,当程序流程执行到作用域结束时,会释放该作用域内的所有对象,在释放过程中会调用对象的析构函数。

  • 局部对象:作用域结束前调用析构函数
  • 堆对象:释放堆空间前调用析构函数
  • 参数对象:退出函数前,调用参数对象的析构函数。
  • 返回对象:如无对象引用定义,退出函数后,调用返回对象的析构函数,否则与对象引用的作用域一致。
  • 全局对象、静态对象:main()函数返回后调用析构函数。
    • 在VS中其构造函数在mainCRTStartup的_initterm调用中被构造
    • main()函数结束后使用exit终止程序,exit()函数内的_execute_onexit_table实现对全局对象的析构函数调用。
⬆︎TOP