C++11:论构对象的构建和释放

目录

什么是构造&析构函数?

构造函数

拷贝构造函数

委托构造函数

移动构造函数

析构函数

何时会调用析构函数

析构函数的功能

成员变量的释放顺序

总结


对象的构建和释放,在C++中占据着举足轻重地位,而构造函数和析构函数分别负责对象的构建和释放,因此详细分析阐述对象的构造函数和析构函数就显得尤为重要和必要了,本博客将从默认构造,拷贝构造,移动构造,委托构造等角度分析对象的构建;从析构的调用时机,功能以及成员变量释放顺序三个角度分析对象的释放。

什么是构造&析构函数?

《C++ Primer》给构造函数进行了下述定义:每个类都分别定了它的对象被初始化的方式,类通过定义一个或几个特殊的成员函数来控制对象的初始化过程,这些函数叫做构造函数;构造函数的任务是初始化对象的数据成员,无论何时只要创建类对象,就会执行构造函数。由上述定义,可得知:(1)构造函数是一个特殊的函数;(2)每个类都会存在至少一个构造函数;(3)构造函数在对象创建是调用。而且构造函数的名称与类名完全一样;除无返回值之外,构造函数与普通函数完全相同。

而对于析构函数,《C++ Primer》给出的定义则是:当一个对象释放时会执行的特殊成员函数,它的作用是释放对象使用的资源并销毁对象中的非static数据成员;析构函数可以自动调用,也可以显示调用;而且析构函数名称和类名完全相同,只是前面多了一个(~)前缀,它无任何输入参数,也无返回值。

为了让大家对构造函数和析构函数,有更直观认识,我们定义HsString类同时实现对应的各类构造函数,其中HsString() : HsString("")为委托构造;HsString(const HsString& hsString)为拷贝构造函数,HsString(const char* s)普通构造函数,HsString(HsString&& hsString)为移动构造函数,virtual ~HsString() 则为类的析构函数。

class HsString
{
public:
	HsString() : HsString("")
	{
	}

	HsString(const HsString& hsString)
	{
		int len = std::strlen(hsString.m_data);
		m_data = new char[len + 1]{ 0 };
		strcpy(m_data, hsString.m_data);
        hsString.m_data = nullptr;
	}

	HsString(const char* s): m_data(nullptr)
	{
		int len = std::strlen(s);
		m_data = new char[len + 1]{ 0 };
		strcpy(m_data, s);
	}

	HsString(HsString&& hsString)
	{
		m_data = hsString.m_data;
		hsString.m_data = nullptr;
	}

	virtual ~HsString()
	{
		if (nullptr != m_data)
		{
			delete[] m_data;
		}
		m_data = nullptr;
	}

private:
	char*  m_data;
};

构造函数的特点

  • 构造函数的名字和类名相同;
  • 构造函数没有返回值,这是构造函数与普通函数最显著的区别
  • 除此之外与其他函数一样,都会包含一个(可空)的函数参数列表和一个(可空)的函数体;
  • 构造函数不能声明为const类型,也不能声明为virtual

析构函数特点

  • 析构函数名称与类名一致,只是前面多了一个~前缀
  • 析构函数无输入参数,也无返回值
  • 析构函数可声明为virtual

本博客余下部分,将深入讨论构造函数,析构函数相关的话题。

构造函数

普通构造函数是C++98/03标准就支持的构造形式,也是最常用的一种类构造方式。普通构造函数包含一个(可空)的初始化列表和构造函数体;委托构造是C++11新引入的特性,目的是把把当前构造函数的全部或部分功能委托其他构造函数完成;移动构造是基于移动语义的构造函数,目的是为了降低深拷贝的性能开销。本节我们将会讨论委托构造、移动构造以及继承构造。

拷贝构造函数

拷贝构造函数是C++98标准引入的一种对象构造方式。基于拷贝构造函数,编译器允许用户根据自己的需要定制自己的对象拷贝。拷贝构造的一般声明方式为const &,例如 HsString的拷贝构造函数HsString(const HsString& hsString),采用这种方式是为了满足右值对象的拷贝构造。因为右值无法赋值给左值引用,但是可以赋值常量左值引用。

HsString orgInstance = HsString();

拷贝构造的说明

  • 拷贝构造的参数最好声明为常量左值引用 const &,只有const &的形参才可以接收右值。

委托构造函数

委托构造函数和其他构造函数一样,同样包含一个成员初始化列表和一个函数体;不同的是委托构造的成员初始化列表只能包含一个唯一的入口,就是类名本身;类名后面紧跟圆括号括起来的参数列表,参数列表必须去其他可重载的构造函数匹配。除此之外委托构造不能出现循环委托,即此构造函数不能直接或简介委托给自己;所以HsString的默认构造函数HsString()就是一个合法的委托构造函数;也许在某些情况下你会写出类似下面这样的代码,而这种委托构造,却是不合法的。因为委托构造的只能包含一个初始化列表成员,一个可重载的其他构造函数而且参数列表必须和可重载的构造函数完全匹配。

HsString() : HsString(""), m_data(nullptr)
{
}

委托构造的特点

  • 委托构造是一类特殊的构造函数,它特殊的地方在于可以把自己的职责,部分或全部委托给其他构造函数完成;
  • 委托构造函数只能包含一个初始化成员列表,而且此列表成员的参数列表必须和可重载的构造函数完全匹配;
  • 委托构造不能直接或间接依赖自己,就是说不能最终在委托给自己,形成一个死循环。

移动构造函数

移动构造函数,是基于移动语义(右值引用)的构造函数,它是C++11标准提出的新型构造方式;C++11之前,如果要将源对象的资源转移到目标对象只能通过拷贝构造,在某些情况下,我们没有必要复制对象,从而降低软件的运行性能;但由于C++11语义的引入,低性能的状况发生了很多的变化,移动语义允许在不进行资源复制的情况下完成资源控制权的迁移。这些描述也许比较拗口,我们以HsString为例,为大家深入分析移动构造和拷贝构造的区别。

HsString orgInstance = HsString();
HsString copyInstance = orgInstance;
HsString movInstance = std::move(orgInstance);
移动构造与拷贝构造对比
图一   拷贝构造和移动构造

如图一所示,左边部分表示HsString copyInstance = orgInstance的实现过程,右半部分表示HsString movInstance = std::move(orgInstance)的实现过程,movInstance没有自己重新申请堆内存而是直接控制了orgInstance申请的堆内存。而orgInstance也释放了堆堆内存的控制权。

对比图一左右两方,我们可以深入的理解move构造的实现方式以及与拷贝构造的差异,但是我们还有两个问题没有解决:(1)目标对象movInstance是怎么获取控制权的;(2)原始orgInstance是怎么释放控制权的。

图二  移动构造的步骤及原理

图二应该可以很好的回答上述两个问题,m_data = hsString.m_data完成移动赋值的权限迁移,hsString.m_data = nullptr完成原始对象对资源管理权的释放。

移动构造的特殊说明

  • 目标对象堆指针通过原始对象指针赋值,以此实现原始对象到目标对象权限的迁移;
  • 原始对象通过对所持有的堆内存指针赋值为nullptr,以此完成对资源管理权的释放;
  • 堆资源权限变更后,原始对象所持有的堆内存指针必须赋值为nullptr,否则将会导致内存重复释放

析构函数

与构造函数一样,析构函数负责对象的释放,同样是对象生命周期管理的重要组成部分,本章我们将从析构函数的调用时机和功能两个角度分析析构函数。

何时会调用析构函数

无论何时,只要一个对象被销毁时,就会自动调用对象的析构函数:

  • 变量在离开其作用域时被销毁
  • 当一个对象被销毁时,其成员被销毁
  • 容器被销毁时,其元素被销毁
  • 对于动态分配的对象,当指向它的指针应用delete运算符被销毁时
  • 对于临时对象,当创建它的完整表达式结束时被销毁

析构函数的功能

C++中析构函数赋值对象的释放工作,具体包括如下:

  • 对象释放时,对象持有的资源将会首先得到释放
  • 对象所持的资源释放完毕后,对象才会真正消失

以HsString为例,当HsString释放时将会调用~HsString()实现对象的析构动作,然后HsString对象才会消失。

成员变量的释放顺序

在《C++11:深刻理解成员变量初始化顺序》我们有这样的结论:类成员变量通过初始化列表初始化时,与构造函数中的初始化列表中的变量顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存的次序有关系,而变量的内存排列顺序,早在编译期就根据变量的定义次序决定了。同样,如果析构函数没有明确说明成员对象的释放顺序,那么C++编译器将会按照成员定义的逆序进行释放。

举个例子:CUser类包含两个成员变量

  • 通过构造函数列表初始化,m_strName将会得到优先初始化,然后m_nAge才会初始化;
  • 析构函数调用时,m_nAge会得到优先释放,然后才是m_strName,即成员变量的定义顺序的逆序。
class CUser
{
public:
    CUser(string strName, int nAge)
    : m_strName(strName)
    , m_nAge(nAge) 
    {}
    ~CUser()
    {}
private:
   string  m_strName;
   int     m_nAge;
};

成员变量释放顺序

  • 析构函数明确说明了成员变量的释放顺序,编译器将会按照析构函数中说明的顺序释放
  • 析构函数没有明确说明成员变量的释放顺序,编译器将会按照变量定义的逆序方式释放

总结

在本博客中,我们详细的探讨了对象的构建和对象的释放,C++通过构造函数实现对象的构建,通过析构函数实现对象的释放。而负责构建的构造函数由可分为普通构造函数,委托构造,移动构造等方式;负责对象析构的只有一个析构函数(定义格式:名称与类名一致,只是前面多了一个~前缀)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值