《C++反汇编与逆向分析技术揭秘》笔记-第10章构造函数和析构函数

若没有另外说明,都是运行在VS2022,x86,Debug下。

本章使用以下代码。

#include<iostream>
#include<string>
using namespace std;


class Person
{
private:
	char* name;

public:
	Person() :name(nullptr) {}  //构造函数

	Person(const Person& other) //复制构造函数(深拷贝)
	{
		int len = strlen(other.name);
		this->name = new char[len + 1];
		if (this->name != nullptr)
			strcpy(this->name, other.name);
	}

	Person& operator=(const Person& other) //赋值运算符重载(深拷贝)
	{
		int len = strlen(other.name);
		if (this->name != nullptr)
			delete[] this->name;

		this->name = new char[len + 1];
		if (this->name != nullptr)
			strcpy(this->name, other.name);

		return *this;
	}

	~Person() //析构函数
	{
		if (name != nullptr)
		{
			delete[] name;
			name = nullptr;
		}
	}

public:
	void setName(const char* name) //设置数据成员(name)
	{
		int len = strlen(name);
		if (this->name != nullptr)
			delete[] this->name;

		this->name = new char[len + 1];
		if (this->name != nullptr)
			strcpy(this->name, name);
	}

	const char* getName() { return name; } //获取数据成员(name)

};

//对象作为参数
void show(Person obj) { 
	printf(obj.getName()); 
}

//对象作为返回值
Person getPerson()
{
	Person obj;
	obj.setName("Hello");
	return obj;
}

//全局对象
Person g_person1;

10.1构造函数的出现时机
1.构造函数属于成员函数,所以调用过程中同样需要传递this指针。构造函数调用结束后,会将this指针作为返回值。


2.在函数调用时传递参数对象,参数会进行复制,形参是实参的副本,相当于复制构造了一个全新的对象。由于定义了新对象,因此会触发复制构造函数,完成两个对象间数据的复制。如果没有定义复制构造函数,编译器会对原对象与复制对象中的各数据成员直接进行数据复制,称为默认复制构造函数,这种复制方式属于浅拷贝。当类中有资源申请,并以数据成员来保存这些资源时,需要使用者提供一个复制构造函数,要处理的不仅是原对象的各数据成员,还有它们指向的资源数据,避免资源释放错误。
调用如下。


没有复制构造函数时,即将上述代码的复制构造函数注释掉【会重复释放同一堆空间而触发中断】

有复制构造函数时。


3.在函数返回时需要对返回对象进行复制,因此同样会使用复制构造函数。区别在,参数对象在进入函数前使用复制构造函数,而返回对象在函数返回时使用复制构造函数。
调用如下。


没有复制构造函数时,即将上述代码的复制构造函数注释掉【会重复释放同一堆空间而触发中断】


有复制构造函数时。


4.返回值和参数是对象指针时,不会调用复制构造函数,而是直接使用指针保存对象首地址。
5.全局对象或静态对象定义,反汇编如下。分析:全局构造函数的调用是在初始化函数内完成的。在执行每个全局对象构造代理函数时都会先执行对象的构造函数,然后使用atexit注册析构代理函数。


6.跟踪全局对象的构造函数如下。
在类的数据成员处断点,调试运行程序,等待调用构造函数。打开调用堆栈窗口(栈回溯)。


mainCRTStartup->__scrt_common_main-> __scrt_common_main_seh->_initterm。


_initterm的定义。分析:
(1)因为构造函数可以重载,所以其参数的类型、个数和顺序都无法预知,也就无法预先定义构造函数。函数参数如何匹配?如何保证栈顶平衡?最简洁的办法就是使用代理函数。编译器为每个全局对象分别生成构造代理函数,由代理函数调用对应构造函数。因为代理函数的类型被统一指定为_PVFV,所以能通过数组统一地管理和执行。
(2)能不能取消代理函数,直接在main()函数前调用构造函数?不能,上述(1)是原因。


7.使用x32dbg跟踪全局对象的构造函数(下一篇)。


10.2每个对象是否都有默认的构造函数
1.在没有定义构造函数的情况下,当类中没有虚函数存在,父类和子类对象也没有定义构造函数时,提供默认的构造函数已没有任何意义,只会降低程序的执行效率,因此编译器没有对这种情况的类提供默认的构造函数。

10.3析构函数的出现时机
1.局部对象:作用域结束前调用析构函数。
2.堆对象:释放堆空间前调用析构函数。
多个堆对象的申请与释放,调用如下。分析:
(1)构造代理函数的作用:根据数组中对象的总个数,从堆数组中的第一个对象首地址开始,依次向后遍历数组中每个对象,逐个调用构造函数。
(2)析构代理函数的作用:根据数组中对象的总个数,从堆数组中的最后一个对象首地址开始,依次向前遍历数组中每个对象,逐个调用析构函数。

反汇编如下。


3.参数对象:退出函数前,调用参数对象的析构函数。
4.返回对象:分两种情况,调用如下。分析:
(1)第一种定义时赋初值,把person对象地址作为隐藏参数传递给getPerson(),在getPerson()内部触发person的复制构造函数,退出函数前,调用返回对象的析构函数。
(2)第二种不是定义时赋初值,person2先调用构造函数,再把临时对象地址作为隐藏参数传递给getPerson(),在getPerson()内部触发临时对象的复制构造函数,退出函数前,调用返回对象的析构函数。【注意】person2调用赋值运算符重载,对临时对象进行复制,若无赋值运算符重载而进行浅拷贝【会重复释放同一堆空间而触发中断】, 调用临时对象的析构函数。

 反汇编如下。

5.全局对象或静态对象:main()函数返回后调用析构函数。
在__scrt_common_main_seh里,如下。分析:
(1)在main()函数调用结束后,由exit终止程序,exit会调用由atexit注册的回调函数。
(2)编译器为每个全局对象生成析构代理函数,用于传入全局对象的this指针。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值