C++基础之使用构造和析构函数保证成对出现

本文探讨如何利用C++的构造和析构函数保证资源的正确管理,例如线程同步的临界区操作,数据库的登录登出,网络连接的建立断开。通过创建特定的类,在构造时执行如EnterCriticalSection的操作,析构时执行LeaveCriticalSection,确保即使在异常或多出口情况下也能避免资源泄漏和死锁。
 

如果想要某些事情成对出现,使用构造函数和析构函数。

 

为保证线程同步/互斥,经常使用信号量、互斥量、临界区和事件。以临界区 CRITICAL_SECTION 为例,需要

InitializeCriticalSection 和 DeleteCriticalSection

EnterCriticalSection 和 LeaveCriticalSection

成对配合使用,否则可能出现资源泄漏甚至死锁的情况。

考虑以下程序片段,中间的代码可能出现异常,导致控制转移。或者函数有多个出口,就需要在每个出口前调用LeaveCriticalSection。

CRITICAL_SECTION _cs;
void SomeFunc()
{
	EnterCriticalSection(&_cs);

	//可能抛出异常

	LeaveCriticalSection(&_cs);
}


我们希望EnterCriticalSection(或者其他lock 函数)和 LeaveCriticalSection(或者其他unlock 函数)成对出现,最好的方法是用class来包装,且分别在构造函数和析构函数中实现。

#include <Windows.h>

class Lock
{
public:
	Lock();
	~Lock();
	void Enter(int id = 0);
	void Leave();
private:
	CRITICAL_SECTION _cs;
	DWORD dwOwningThread;
	int iEnterCount;
	int iId;
};

class AutoLock
{
public:
	AutoLock(Lock* target, int id = 0);
	~AutoLock();
private:
	Lock* _target;
};

 

#include "AutoLock.h"

Lock::Lock(): dwOwningThread(0), iEnterCount(0), iId(0)
{
	InitializeCriticalSection(&_cs);
}

Lock::~Lock()
{
	DeleteCriticalSection(&_cs);
}

void Lock::Enter(int id)
{
	DWORD currentThread = GetCurrentThreadId();
	EnterCriticalSection(&_cs);
	dwOwningThread = currentThread;
	iEnterCount++;
	iId = id;
}

void Lock::Leave()
{
	if (--iEnterCount == 0) {
		dwOwningThread = 0;
		iId = 0;
	}
	LeaveCriticalSection(&_cs);
}

AutoLock::AutoLock(Lock* target, int id) : _target(target)
{
	_target->Enter(id);
}

AutoLock::~AutoLock()
{
	_target->Leave();
}


直接使用 AutoLock 类,而不用考虑释放临界区的问题。

	Lock* workBufferLock;
	//...
	AutoLock lock(workBufferLock);
	//...


这个方法应用在很多场合,比如网络访问中建立连接和断开连接,数据库访问中的登录和登出,测量函数平均运行耗时等。

 

【参考】

Solmyr 的小品文系列之六:成对出现

<think> 我们正在讨论C++中的空构造函数析构函数。根据提供的引用,我们可以总结如下: 引用[1]提到空类会被编译器加入一个char成员,以便实例化的对象占有空间。但这里更关心的是构造函数析构函数。 引用[2][3]详细说明了构造函数析构函数的基本概念: - 构造函数:在创建对象时初始化成员属性,由编译器自动调用。 - 析构函数:在对象销毁前执行清理工作,由编译器自动调用。 - 如果没有显式定义,编译器会生成默认的(空实现的)构造函数析构函数。 引用[4]展示了构造函数析构函数的具体使用示例,其中构造函数初始化了成员变量并分配了内存,析构函数则释放了内存。 引用[5]则说明了基类析构函数如果不是虚函数,则通过基类指针删除派生类对象时,派生类的析构函数不会被调用,这可能导致资源泄漏。 现在,针对用户的问题:空构造函数析构函数的作用。 1. 空构造函数(即函数体为空的构造函数): - 如果没有定义任何构造函数,编译器会生成一个默认的空构造函数(如果没有任何其他构造函数被定义的话)。但是,如果定义了其他构造函数,则编译器不会生成默认构造函数,除非显式地使用`=default`。 - 空构造函数的作用: - 允许创建对象而不需要任何参数(即无参构造函数)。例如,当创建对象数组时,每个元素都会调用无参构造函数。 - 即使函数体为空,编译器仍然会执行一些必要的操作,比如调用成员对象基类的构造函数(如果有的话)。所以,即使构造函数体为空,成员变量可能是由它们自己的默认构造函数初始化的(如果成员是类类型的话)。 - 对于内置类型的成员变量,如果没有在构造函数的初始化列表中显式初始化,它们将保持未初始化(即随机值)。因此,空构造函数可能不会初始化内置类型的成员变量,这需要注意。 2. 空析构函数(即函数体为空的析构函数): - 如果没有定义析构函数,编译器会生成一个默认的空析构函数。 - 空析构函数的作用: - 在对象销毁时,编译器会调用析构函数。如果析构函数为空,则不会执行额外的清理操作。 - 但是,即使析构函数体为空,编译器也会自动调用成员对象基类的析构函数(按照构造的逆序)。因此,如果成员是类类型,它们的析构函数会被正确调用。 - 如果类中没有需要手动释放的资源(如动态分配的内存、文件句柄等),那么空析构函数就是合适的。 重要注意事项: - 如果类中包含了动态分配的资源(如通过`new`分配的内存),则必须定义非空的析构函数来释放这些资源,否则会导致内存泄漏。 - 在继承体系中,基类的析构函数通常应该声明为虚函数(除非该类不会被继承)。引用[5]说明了如果基类析构函数不是虚函数,那么通过基类指针删除派生类对象时,派生类的析构函数不会被调用,从而可能导致资源泄漏。 总结: - 空构造函数:主要用于允许默认构造,编译器会调用成员基类的构造函数(如果有),但不会初始化内置类型(除非在初始化列表中指定)。 - 空析构函数:编译器会调用成员基类的析构函数,适用于没有需要手动释放资源的类。但要注意继承体系中的虚析构函数问题。 相关问题: 1. 什么情况下需要为类定义非空的构造函数? 2. 在C++中,基类的析构函数为什么应该声明为虚函数? 3. 默认构造函数(空构造函数)在什么时候会被编译器自动生成? 4. 如果一个类没有定义析构函数,那么编译器生成的默认析构函数会做什么? </think> ### C++中空构造函数析构函数的作用分析 #### 1. 空构造函数的作用 - **对象默认初始化** 当类未声明任何构造函数时,编译器会自动生成空构造函数(默认构造函数),用于创建对象实例。例如: ```cpp class MyClass { public: MyClass() {} // 显式定义的空构造函数 }; ``` 此时`MyClass obj;`能正常创建对象[^3]。 - **兼容容器操作** STL容器(如`vector`)要求元素类型必须有默认构造函数,空构造函数满足此要求: ```cpp std::vector<MyClass> vec(10); // 需要调用默认构造函数 ``` - **成员默认初始化** 若类包含类类型成员(如`std::string`),空构造函数会触发这些成员的默认构造: ```cpp class Widget { std::string name; // 调用std::string的默认构造函数 public: Widget() {} // 触发name的默认构造 }; ``` > **注意**:内置类型(如`int`)在空构造函数中不会被初始化,值为随机数[^4]。 #### 2. 空析构函数的作用 - **资源自动释放** 即使析构函数为空,编译器也会自动销毁所有成员变量(按声明逆序): ```cpp class Resource { std::unique_ptr<int> ptr; // 智能指针自动释放 public: ~Resource() {} // 空析构函数仍会调用ptr的析构 }; ``` - **支持继承体系** 在继承链中,派生类析构会自动调用基类空析构函数: ```cpp class Base { public: virtual ~Base() {} // 空虚析构支持多态销毁 }; class Derived : public Base { FileHandle file; // 资源句柄 ~Derived() override { file.close(); } // 先执行派生析构 }; // 后自动调用Base::~Base() ``` - **防止资源泄漏(占位)** 显式定义空析构函数可防止编译器生成默认析构函数,为后续添加资源释放逻辑预留位置。 #### 3. 关键特性对比 | 特性 | 空构造函数 | 空析构函数 | |---------------------|------------------------------|--------------------------------| | **编译器自动生成** | 未声明构造函数时生成 | 未声明析构函数时生成 | | **内置类型处理** | 不初始化(随机值) | 无操作 | | **类成员处理** | 调用其默认构造函数 | 调用其析构函数 | | **虚函数意义** | 构造函数不能为虚函数 | 虚析构函数支持多态销毁[^5] | #### 4. 使用场景 - **适用空构造** - POD(Plain Old Data)类型的数据传输对象 - 仅包含智能指针或RAII对象的类 - 需要STL容器兼容的类 - **适用空析构** - 不含手动分配资源的类(内存、文件句柄等) - 基类声明虚析构函数(即使函数体为空) - 作为接口类的默认实现 > **陷阱**:空析构函数在涉及多态时,若基类未声明`virtual`,会导致派生类资源泄漏[^5]: > ```cpp > Base* obj = new Derived(); > delete obj; // 若基类析构非虚,仅调用~Base() > ``` --- ### 相关问题 1. 为什么C++编译器会为没有构造函数的类生成默认构造函数? 2. 在哪些场景下必须手动定义非空的析构函数? 3. 虚析构函数如何解决多态对象销毁时的资源泄漏问题? 4. 空类(无成员变量)的默认构造函数析构函数有什么特殊行为?[^1] 5. 构造函数初始化列表相比在构造函数体内赋值有什么优势? [^1]: 空类会被编译器插入1字节占位符以保证实例可寻址 [^2]: 析构函数不可重载且自动调用 [^3]: 编译器强制要求初始化清理 [^4]: 构造函数初始化成员,析构函数释放资源 [^5]: 虚析构函数确保调用完整析构链
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值