【C++对象模型探索】系列之构造/析构函数详解

内容概述

  想必使用C++的程序员们,或多或少都听过或者抱怨过C++语言过于复杂,同时编译器备着程序员们做了很多事情。例如:类中的构造函数就存在很多种变化情况,什么时候编译器会生成默认构造函数,成员变量在构造函数中初始化,有哪些成员函数与成员变量属于类或者对象,拷贝构造函数的使用注意事项,析构函数在继承时候为什么需要虚函数模式等等。但是,当你对这些C++类与对象知识熟悉之后,你就会慢慢应用自如。下面会对c++类中的构造函数等进行逐一介绍。

构造函数区分

  C++对象模型探索中,构造函数、析构函数、拷贝构造函数、移动构造函数等。对于C++类与对象模型来说,构造函数、拷贝构造函数、析构函数使用频率最高,同时里面的一些知识点也较多,下面会逐个进行主要说明。

  • 默认构造函数:主要分为两种:编译期间默认生成的构造函数;或者是定义的无参构造函数;(如果定义了构造函数,那么系统编译期间就不会生成默认的构造函数。但是,没有定义构造函数,系统编译也不一定生成默认构造函数;)
  • 显示构造函数:显示定义构造函数的调用方式,避免构造函数的隐式转换操作,使用更加严格;在构造函数前面添加关键字explicit
  • 委托构造函数:参见下面委托构造函数实现代码部分,浅显易懂;
  • 拷贝构造函数:主要解决类对象直接的拷贝操作;当然这里面有深拷贝与浅拷贝区分,同时拷贝构造函数也存在与默认构造函数同样情况,系统在没有定义拷贝构造函数情况下,是否会生成拷贝构造函数;
  • 析构函数:主要是在对象执行结束,资源进行回收释放操作;当存在继承时候,最佳定义成为虚析构函数模式;
  • 移动构造函数:避免拷贝构造函数中存在深拷贝等效率低下的情况,当然也会对被移动构造的对象权限转移。
什么情况下编译生成默认构造函数

  关于没有声明类的构造函数时候,在以下几种情况下编译器会帮助生成默认构造函数;不同的编译器可能有些许差别,这里只验证了windows平台的visual studio 2017和linux下的gcc编译。

下面表示的为默认Base与Apple都是个空类,添加如下任何一个条件编译器会帮助生成默认构造函数:

  • 类Base里面存在虚函数;(无继承关系)
  • 类Base里面有个其它类(例如:Apple类)成员变量,且Apple类里面声明构造函数;(无继承关系)
  • 假设类Base继承Apple类,且Apple类里面声明构造函数;
windows与linux平台如何确认生成构造函数查询方法
平台windows10Ubuntu
编译器Visual Studio 2019GCC 7.4.0

  为了更有效的演示如何确认系统是否生成默认构造函数,下面贴一小段测试代码:

#include <iostream>

class Base
{
public:
  virtual void calc()
  {
      std::cout << "virtual function go." << std::endl;
  }
private:
    int m_variable;
};

int main(void)
{
    Base b;

    std::cout << "the program running success." << std::endl;
    return 0;
}

  windows平台下,如何测试系统是否生成默认构造函数:可以通过两种方式来进行确认。

  第一种,通过调试程序添加断点与查看反汇编方式:

将上述代码Base类中的虚函数注释掉,进行反汇编调试查询的结果如下图:

  通过上述对比,明显发现注释掉虚函数后,Base类没有被编译期间生成默认构造函数。(这里顺带提一下:反汇编查找,在visual studio 上是调试–> 窗口–>反汇编)

  第二种方式:打开visual studio 2019的终端,进入到你的工程Debug目录下,然后输入如下命令:(c_plus_plus_constructor是创建的工程名称)

dumpbin /all c_plus_plus_constructor.obj > 1.txt

  然后,你会在你的工程Debug目录下看到生成的1.txt。然后,拖拽到VS编译环境中,通过查找来看是否存在Base构造函数。

  linux平台下,如何测试系统是否生成默认构造函数:

// 将cpp文件使用gcc进行编译生成可执行文件
g++ c_plus_plus_constructor.cpp -o test_c_plus_plus_constructor
// 执行生成的文件,确认正确执行
./test_c_plus_plus_constructor
// 终端输入命令进行查看
nm test_c_plus_plus_constructor | c++filt

  执行完上述指令后,你会在终端看到很多输出;如下图一个示例,你能够看到系统编译期间生成默认构造函数:

  无论是windows还是linux平台,查看如果没有发现Base::Base()这个构造函数,则说明系统编译期间并未生成默认构造函数。另外,这里说明一下,即使编译器合成出默认构造函数,也并不是会对成员变量进行初始化,这和不同编译器的实现方式有关。可以理解为,有些编译器合成出来的默认构造函数,只是一个没有什么用的构造函数。

继承时候构造函数调用顺序

这里简单介绍一下继承情况下构造函数的执行顺序,后续关于继承的知识再单独介绍分析。

  • 单继承:假设GrandDerive继承DeriveDerive继承Base,都是公有继承方式,那么声明一个GrandDerive对象,调用的构造函数顺序与析构函数执行顺序如下图;U型顺序分别先执行基类的构造函数,析构函数正好与构造函数顺序相反;
  • 多继承:先执行基类的构造函数,执行顺序由子类继承的顺序来决定;
class Derive : public Base, public Base2
{
	// 这里调用构造函数顺序为,先Base()再Base2()最后Devive();析构函数执行顺序正好相反
};
  • 虚继承:主要为了解决多层继承时候,重复继承TopBase(下图)的问题,给子类(Derive)带来使用TopBase类的成员函数或者成员变量上面的二义性问题;下图为引入虚继承来有效避免重复继承以及子类使用导致的二义性问题。
构造函数的相关使用注意事项

关于经常定义使用构造函数时候,我们需要注意的一些知识点如下。

  • 初始化成员变量顺序问题:在初始化列表中一定注意初始化成员变量的顺序只与你定义的成员变量顺序一致;
class A
{
public:
   A():m_i(5), m_j(m_i) // error use, m_j(m_i), m_i(5)
   {
   // 上面将m_j(m_i), m_i(5)也是对的,
   // 主要是说明初始化顺序只与private下面定义m_i与m_j的顺序有关
   }
private:
   int m_i;
   int m_j;
};
  • 构造函数内部不能调用虚函数;不能有任何返回值;构造函数与类同名;
  • 关于类成员变量必须使用初始化列表的有:const 成员变量,引用类型;原因在于初始化列表会在构造函数执行前进行操作;
  • 构造函数初始化成员变量时,尽量使用初始化列表方式(特别是在类成员变量存在其它类对象时候,这样会更加高效);
拷贝构造函数

  拷贝构造函数主要解决类对象之间的数据拷贝问题,当然这里就会涉及到一个最为重要的问题:拷贝构造函数的使用时候深拷贝与浅拷贝概念以及注意事项。深拷贝:开辟一段新的内存将要拷贝对象的数据存入里面;浅拷贝:只拷贝对象的地址,特别是这里说到类成员变量是指针的情况;深拷贝与浅拷贝的主要区别:是否真正获取一个对象的复制实体,而不只是一个地址;

  • 拷贝构造函数的深拷贝与浅拷贝示例代码:
#include <iostream>
using namespace std;

class Apple
{
public:
	explicit Apple(int m_num, int size): _m_num(m_num), _size(size)
	{
		cout << "Apple::Apple()." << endl;
		if (nullptr != _ptr)
		{
			delete[] _ptr;
		}
		// 对成员变量指针_ptr进行开辟内存,并且赋值操作
		_ptr = new int[_size];
		for (int i = 0; i < _size; ++i)
		{
			_ptr[i] = i + 1; // _ptr志向的内存数据为:[1,2,3,4,5]
		}
	}

	Apple(const Apple& other):_m_num(other._m_num), _size(other._size)
	{
		cout << "Apple::Apple(const Apple& other)." << endl;
		if (this == &other)
		{   // 避免自我拷贝
			return ;
		}
		//-------------浅拷贝操作-----------------//
		//this->_ptr = other._ptr; // error

		// -------------深拷贝操作----------------//
		// 确认拷贝前_ptr指针为空,不为空则释放以前的内存
		if (nullptr != _ptr)
		{
			delete[] _ptr;
		}
		// 计算输入的other._ptr的内存大小
		const int _len = other._size; // 获取指针所指向的内存大小
		_ptr = new int[_len]; // 分配内存
		memcpy(_ptr, other._ptr, _len * sizeof(int));
	}

	~Apple()
	{
		cout << "Apple::~Apple()." << endl;
		delete[] _ptr;
		_ptr = nullptr;
	}

	int* GetPtr()
	{
		return _ptr;
	}

	int GetSize()
	{
		return _size;
	}
private:
	int _m_num;
	int _size;
	int* _ptr;
};

void printMessage(Apple& apple)
{
	// 获取内存指针
	int* ptr = apple.GetPtr();
	// 确认调用拷贝构造函数的输出是否正确
	const int len = apple.GetSize();
	for (int i = 0; i < len; ++i)
	{
		cout << " " << ptr[i];
	}
	cout << endl;
}

int main(void)
{	// 构造函数初始化
	Apple a(3, 5);
	// 拷贝构造函数调用:深拷贝
	Apple b = a;

	printMessage(a);
	cout << "------------------------" << endl;
	printMessage(b);
	return 0;
}

  我们重点关注一下上述的拷贝构造函数实现,里面对成员变量指针_ptr所指向的内存数据进行开辟内存拷贝过去,这属于深拷贝操作;如果只是简单的复制other._ptr的地址,将会导致异常错误。主要原因在于前一个对象的指针也指向该块内存数据,导致两个对象a,b分别执行结束后都调用自己的析构函数,释放两次内存,导致崩溃。

委托构造函数

  委托构造函数:同一个类中构造函数委托另一个构造函数进行成员变量初始化操作,当然被选择委托构造函数尽量初始化较多成员变量。派生类也可以调用基类的构造函数作为委托构造函数。

下面为委托构造函数的示例代码:

#include <iostream>

using namespace std;

class Rectangle
{
public:
	Rectangle() : Rectangle(0, 0, 0, 0, 0)
	{
		cout << "Rectangle()被调用." << endl;
	}

	Rectangle(float center_x, float center_y) : Rectangle(center_x, center_y, 0, 0, 0)
	{
		cout << "Rectangle(float, float)被调用." << endl;
	}

	Rectangle(float center_x, float center_y, int width, int height, float area):
		_center_x(center_x)
		, _center_y(center_y)
		, _width(width)
		, _height(height)
		, _area(area)
	{
		cout << "Rectangle(float, float, int, int, float)被调用." << endl;
	}

public:
	float _center_x;
	float _center_y;
	int _width;
	int _height;
	float _area;
};


int main(void)
{
	Rectangle rect;
	cout << "rect._center_x: " << rect._center_x 
		<< ", rect._center_y: " << rect._center_y 
		<< ", rect._width: " << rect._width << ", rect._height: " 
		<< rect._height << ", rect._area: " << rect._area << endl;

	cout << "---------------------------------------------------------" << endl;
	Rectangle rect2(1.2, 2.4);
	cout << "rect._center_x: " << rect2._center_x
		<< ", rect._center_y: " << rect2._center_y
		<< ", rect._width: " << rect2._width << ", rect._height: "
		<< rect2._height << ", rect._area: " << rect2._area << endl;

	cout << "--------------------------------------------------------" << endl;

	Rectangle rect3(1.7, 4.4, 5, 6, 10);
	cout << "rect._center_x: " << rect3._center_x
		<< ", rect._center_y: " << rect3._center_y
		<< ", rect._width: " << rect3._width << ", rect._height: "
		<< rect3._height << ", rect._area: " << rect3._area << endl;

	return 0;
}

程序输出结果如下:

Rectangle(float, float, int, int, float)被调用.
Rectangle()被调用.
rect._center_x: 0, rect._center_y: 0, rect._width: 0, rect._height: 0, rect._area: 0
---------------------------------------------------------
Rectangle(float, float, int, int, float)被调用.
Rectangle(float, float)被调用.
rect._center_x: 1.2, rect._center_y: 2.4, rect._width: 0, rect._height: 0, rect._area: 0
--------------------------------------------------------
Rectangle(float, float, int, int, float)被调用.
rect._center_x: 1.7, rect._center_y: 4.4, rect._width: 5, rect._height: 6, rect._area: 10

  • 当构造函数委托另一个构造函数时候,这个构造函数不能再使用成员列表初始化方式;
  • 存在继承关系时候,派生类默认调用基类构造函数;当然,你也可以显示委托调用基类的构造函数;
移动构造函数

  我们知道上面的拷贝构造函数内部实现涉及深拷贝操作,会带来效率上的降低。因此c++有个移动构造函数来进行右值引用操作;移动构造函数其实主要就是对内存拷贝,特别是某个对象的成员变量指针在堆上的数据进行权限转移,这样能更加高效。但是,需要注意的是被转移的对象后续已经不再拥有操作该堆上数据的权限。看下面的代码,对象b经过移动构造函数调用后,不可以在通过指针b._ptr来对堆上内存数据进行操作,并且该对象b._ptr指针已经赋空。

#include <iostream>
using namespace std;

class Apple
{
public:
	explicit Apple(int m_num, int size): _m_num(m_num), _size(size)
	{
		cout << "Apple::Apple()." << endl;
		if (nullptr != _ptr)
		{
			delete[] _ptr;
		}
		// 对成员变量指针_ptr进行开辟内存,并且赋值操作
		_ptr = new int[_size];
		for (int i = 0; i < _size; ++i)
		{
			_ptr[i] = i + 1; // _ptr志向的内存数据为:[1,2,3,4,5]
		}
	}

	Apple(const Apple& other):_m_num(other._m_num), _size(other._size)
	{
		cout << "Apple::Apple(const Apple& other)." << endl;
		if (this == &other)
		{   // 避免自我拷贝
			return ;
		}
		//-------------浅拷贝操作-----------------//
		//this->_ptr = other._ptr; // error

		// -------------深拷贝操作----------------//
		// 确认拷贝前_ptr指针为空,不为空则释放以前的内存
		if (nullptr != _ptr)
		{
			delete[] _ptr;
		}
		// 计算输入的other._ptr的内存大小
		const int _len = other._size; // 获取指针所指向的内存大小
		_ptr = new int[_len]; // 分配内存
		memcpy(_ptr, other._ptr, _len * sizeof(int));
	}

	// 移动构造函数
	Apple(Apple&& other) :_m_num(other._m_num), _size(other._size), _ptr(other._ptr)
	{
		cout << "Apple::Apple(const Apple&& other)." << endl;
		// 这里将原始指针赋值nullptr,这样在other对象析构时候,不会释放指向的内存
		// 相当于将该段内存的指针是否操作权限移交给_ptr
		other._ptr = nullptr;
	}

	~Apple()
	{
		cout << "Apple::~Apple()." << endl;
		delete[] _ptr;
		_ptr = nullptr;
	}

	int* GetPtr()
	{
		return _ptr;
	}

	int GetSize()
	{
		return _size;
	}
private:
	int _m_num;
	int _size;
	int* _ptr;
};

void printMessage(Apple& apple)
{
	// 获取内存指针
	int* ptr = apple.GetPtr();
	// 确认调用拷贝构造函数的输出是否正确
	const int len = apple.GetSize();
	for (int i = 0; i < len; ++i)
	{
		cout << " " << ptr[i];
	}
	cout << endl;
}

int main(void)
{	// 构造函数初始化
	Apple a(3, 5);
	// 拷贝构造函数调用:深拷贝
	Apple b = a;
	// 调用移动构造函数进行指针地址拷贝,提高效率
	Apple c = std::move(b);
	
	printMessage(a);
	cout << "------------------------" << endl;
	printMessage(c);
	return 0;
}
小结

  C++11类与对象中构造函数、拷贝构造函数、析构函数是程序员使用频率较高且接触较多的。当然,关于虚函数、拷贝运算符、赋值运算符等后续再做介绍。

  • 构造函数初始化类成员变量最好使用初始化列表的方式,这样在成员变量是另一个类对象时候会更加高效;
  • 构造函数成员变量为const或者引用类型,必须使用初始化列表进行初始化;
  • 构造函数初始化列表进行初始化成员变量顺序只与类声明中的成员变量顺序有关;
  • 委托构造函数尽量选择被委托的构造函数成员变量初始化最多的那个;
  • 拷贝构造函数定义时候,一定注意区分深浅拷贝的问题,不然可能会造成异常;
  • 如需自己定义构造函数,建议使用显示构造函数方法;即:添加关键字explicit;以此来避免构造函数被隐式转换调用;
  • 存在继承关系时候,建议基类的析构函数声明为虚析构函数模式;
参考

构造函数
拷贝构造函数
析构函数
移动构造函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值