若没有另外说明,都是运行在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指针。