C++RAII用法

news/2025/2/9 5:42:10/

思维导图

在这里插入图片描述

为什么要引入RAII

有一个简单的服务器例子。在Windows系统上写一个C++程序,在客户端请求连接时,给客户端发一条"Hello World"消息,然后关闭连接。不需要保证客户端一定能收到。

程序实现流程

  1. 创建socket
  2. 绑定IP地址和端口号
  3. 在该IP地址和端口号上启动监听,循环等待客户端连接。客户端连接成功后,发消息,然后断开连接。

实现版本一

Windows上使用网络通信API时,需要通过 WSAStartup 函数初始化 WinSock 库,并在程序结束时使用 WSACleanup 进行清理。代码中充斥着用于避免出错的重复资源清理逻辑closesocket(sockSrv)WSACleanup()

#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib")int main() {// 初始化 WinSock 库,设置使用版本为 2.2WORD version = MAKEWORD(2, 2);WSADATA wsaData;int ret = WSAStartup(version, &wsaData);if (ret != 0) {// 初始化失败,清理并退出WSACleanup();return -1;}// 检查是否成功获得所请求的版本if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {// 如果不匹配,清理并退出WSACleanup();return -2;}// 创建服务器套接字,指定使用 IPv4 协议、TCP 协议SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET) {// 创建套接字失败,清理并退出WSACleanup();return -3;}// 设置服务器地址结构SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 绑定到所有可用的网络接口addrSrv.sin_family = AF_INET; // 使用 IPv4 协议addrSrv.sin_port = htons(6000); // 设置监听端口为 6000// 绑定套接字到指定地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0) {// 绑定失败,清理并退出closesocket(sockSrv);WSACleanup();return -4;}// 开始监听连接,最大连接数为 15if (listen(sockSrv, 15) != 0) {// 监听失败,清理并退出closesocket(sockSrv);WSACleanup();return -5;}// 客户端连接地址结构SOCKADDR_IN addrClient;int len = sizeof(SOCKADDR);// 进入服务器主循环,等待并接受客户端连接while (true) {// 接受客户端连接SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET) {// 接受连接失败,跳出循环break;}// 向客户端发送 "hello world" 字符串const char* message = "hello world";send(sockClient, message, strlen(message), 0);// 关闭与客户端的连接closesocket(sockClient);}// 关闭服务器套接字closesocket(sockSrv);// 清理 WinSock 库WSACleanup();return 0;
}

先分配资源,再进行相关操作,在任意中间步骤出错时都对响应的资源进行回收,如果中间部分没有出错,就在资源使用完毕后对其进行回收。

这样编写代码容易出错,而且会造成大量代码重复。

实现版本二

使用goto语句跳转到统一的清理点进行资源清理操作。

#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib")int main() {SOCKADDR_IN addrSrv;         // 服务器地址结构SOCKET sockSrv;              // 服务器套接字SOCKADDR_IN addrClient;      // 客户端地址结构int len = sizeof(SOCKADDR);  // 地址结构的大小const char* message = "hello world"; // 发送的消息// 初始化 WinSock 库,使用 2.2 版本WORD version = MAKEWORD(2, 2);WSADATA wsaData;int ret = WSAStartup(version, &wsaData);if (ret != 0) {  // 如果初始化失败,跳转到 cleanup2 清理资源goto cleanup2;return -1;  // 返回初始化失败的错误码}// 检查所初始化的 WinSock 版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {goto cleanup2;return -2;  // 如果版本不匹配,清理资源并返回错误码}// 创建一个 TCP 套接字sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET) {  // 如果创建套接字失败,跳转到 cleanup2 清理资源goto cleanup2;return -3;  // 返回创建套接字失败的错误码}// 填充服务器地址结构addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  // 设置为任意 IP 地址addrSrv.sin_family = AF_INET;   // 使用 IPv4addrSrv.sin_port = htons(6000); // 监听端口 6000// 绑定套接字到指定的地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0) {  goto cleanup1;  // 如果绑定失败,跳转到 cleanup1 清理服务器套接字return -4;  // 返回绑定失败的错误码}// 开始监听客户端连接,最多允许 15 个客户端排队if (listen(sockSrv, 15) != 0) {goto cleanup1;  // 如果监听失败,跳转到 cleanup1 清理服务器套接字return -5;  // 返回监听失败的错误码}// 接受客户端连接while (true) {SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET) {  // 如果接受连接失败,跳出循环break;}// 向客户端发送消息send(sockClient, message, strlen(message), 0);closesocket(sockClient);  // 关闭客户端套接字}// 跳转到 cleanup1 清理服务器套接字goto cleanup1;cleanup1:closesocket(sockSrv);  // 关闭服务器套接字
cleanup2:WSACleanup();  // 清理 WinSock 库资源return 0;  // 程序结束
}

goto语句要慎用,会使得程序结构混乱。

实现版本三

使用do{...}while(0)循环的break特性将资源回收集中到一个地方。

#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib")  // 链接 ws2_32 库,这是 WinSock 编程所必需的int main() {WORD version = MAKEWORD(2, 2);  // 定义要使用的 WinSock 版本 2.2WSADATA wsaData;                // 用于保存 WinSock 数据int ret = WSAStartup(version, &wsaData);  // 初始化 WinSock 库if (ret != 0) {  // 如果初始化失败return -1;   // 返回错误码 -1}SOCKET sockSrv = -1;  // 定义服务器套接字,初始化为 -1 表示未初始化do {// 检查 WinSock 版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)break;  // 如果版本不匹配,跳出循环sockSrv = socket(AF_INET, SOCK_STREAM, 0);  // 创建一个 TCP 套接字if (sockSrv == INVALID_SOCKET)  // 如果套接字创建失败break;  // 跳出循环SOCKADDR_IN addrSrv;  // 服务器地址结构addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  // 绑定到所有可用的网络接口addrSrv.sin_family = AF_INET;  // 使用 IPv4 协议addrSrv.sin_port = htons(6000);  // 设置端口为 6000SOCKADDR_IN addrClient;  // 客户端地址结构int len = sizeof(SOCKADDR);  // 地址结构的大小const char* message = "hello world";  // 发送的消息内容// 绑定套接字到指定的地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0)break;  // 绑定失败,跳出循环// 开始监听客户端连接,最大排队 15 个连接请求if (listen(sockSrv, 15) != 0)break;  // 监听失败,跳出循环// 持续接受客户端连接while (true) {SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);  // 等待客户端连接if (sockClient == INVALID_SOCKET)  // 如果接受连接失败break;  // 跳出循环send(sockClient, message, strlen(message), 0);  // 向客户端发送消息closesocket(sockClient);  // 关闭客户端套接字}} while (0);  // 结束 do-while 循环if (sockSrv != -1)  // 如果服务器套接字有效closesocket(sockSrv);  // 关闭服务器套接字WSACleanup();  // 清理 WinSock 库return 0;  // 程序成功结束
}

很巧妙,但C++有更好的写法。

实现版本四

使用RAII(资源获取就是初始化),资源在我们拿到时就初始化,一旦不需要改资源,就自动释放资源。

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib, "ws2_32.lib")// 创建一个 ServerSocket 类,封装了网络编程的各个步骤
class ServerSocket {
public:// 构造函数,初始化成员变量ServerSocket() {m_ListenSocket = -1;  // 初始化监听套接字为无效值}// 析构函数,释放资源~ServerSocket() {// 关闭监听套接字if (m_ListenSocket != -1)::closesocket(m_ListenSocket);}// 初始化 WinSock 库和套接字static bool DoInit() {if (m_bInit) return true;  // 如果已初始化,直接返回 trueWORD version = MAKEWORD(2, 2);  // 设置所需的 WinSock 版本 2.2WSADATA wsaData;                // 用于保存 WinSock 数据int ret = ::WSAStartup(version, &wsaData);  // 初始化 WinSockif (ret != 0)  // 如果初始化失败,返回 falsereturn false;// 检查实际使用的版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)return false;  // 版本不匹配,返回 falsem_bInit = true;  // 成功初始化,标记为 truereturn true;}// 清理 WinSock 库static void DoCleanup() {if (m_bInit) {::WSACleanup();m_bInit = false;}}// 创建并初始化监听套接字bool DoListen(const char* ip, short port = 6000) {if (!DoInit()) return false;  // 如果 WinSock 初始化失败,返回 false// 创建一个 TCP 套接字m_ListenSocket = ::socket(AF_INET, SOCK_STREAM, 0);if (m_ListenSocket == INVALID_SOCKET)  // 如果套接字创建失败,返回 falsereturn false;SOCKADDR_IN addrSrv;  // 服务器地址结构addrSrv.sin_addr.S_un.S_addr = inet_addr(ip);  // 设置 IP 地址addrSrv.sin_family = AF_INET;  // 使用 IPv4 协议addrSrv.sin_port = htons(port);  // 设置端口(默认 6000)// 绑定套接字到指定地址和端口if (::bind(m_ListenSocket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0)return false;  // 绑定失败,返回 false// 启动监听,backlog 是请求队列的最大长度(默认为 15)if (::listen(m_ListenSocket, 15) != 0)return false;  // 监听失败,返回 falsereturn true;  // 成功开始监听,返回 true}// 接受客户端连接并发送消息void DoAccept() {SOCKADDR_IN addrClient;  // 客户端地址结构int len = sizeof(SOCKADDR);  // 地址结构的大小const char* message = "hello world";  // 发送的消息while (true) {// 接受客户端连接请求SOCKET sockClient = ::accept(m_ListenSocket, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET)break;  // 如果连接失败,跳出循环// 向客户端发送消息::send(sockClient, message, strlen(message), 0);// 关闭客户端套接字::closesocket(sockClient);}}private:static bool m_bInit;  // 是否初始化的标志位,静态成员SOCKET m_ListenSocket;  // 监听套接字
};// 静态成员初始化
bool ServerSocket::m_bInit = false;int main() {ServerSocket serverSocket;  // 创建 ServerSocket 对象// 绑定服务器 IP 和端口,若失败则退出if (!serverSocket.DoListen("0.0.0.0", 6000))return false;// 接受客户端连接并发送消息,若失败则退出serverSocket.DoAccept();// 清理 WinSock 库ServerSocket::DoCleanup();return 0;  // 程序正常结束
}

RAII的其他用途

分配堆内存

把堆内存包裹成对象,构造函数分配堆内存,析构函数释放堆内存。

多线程锁的获取和释放

把锁包裹成对象,构造函数获取锁,析构函数释放锁。

推荐一下

https://github.com/0voice


http://www.ppmy.cn/news/1570510.html

相关文章

nas-群晖docker查询注册表失败解决办法(平替:使用SSH命令拉取ddns-go)

目录 前言必读 一、遇到问题 二、操作步骤 &#xff08;一&#xff09;打开群晖系统的SSH服务? &#xff08;二&#xff09;Windows电脑本地下载安装putty? 输入登录账号密码 开启root权限 例子&#xff1a;使用命令行下载ddns-go? 前言必读 读者手册&#xff08;必…

【JVM详解一】类加载过程与内存区域划分

一、简介 1.1 概述 JVM是Java Virtual Machine&#xff08;Java虚拟机&#xff09;的缩写&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关…

MoviePy,利用Python自动剪辑tiktok视频

Python剪辑视频是非常强大的&#xff0c;而且能流水线批量操作&#xff0c;可以使用MoviePy库实现。 最近看到一个Github项目&#xff0c;作者利用Python写了一个自动生成tiktok视频的脚本&#xff0c;受到热捧。 现在像抖音、tiktok上有很多流水线生产的视频&#xff0c;不少…

RNN-day1-NLP基础

NLP基础 一、基本概念 自然语言处理&#xff1a;Natural Language Processing,主要目标是让计算机能够理解、解释和生成人类语言的数据。 1 基本概念 1.1NLP概念 语言&#xff1a;人类沟通的机构化系统&#xff0c;包括声音、书写符号、手势 自然语言&#xff1a;自然进化…

实现数组的扁平化

文章目录 1 实现数组的扁平化1.1 递归1.2 reduce1.3 扩展运算符1.4 split和toString1.5 flat1.6 正则表达式和JSON 1 实现数组的扁平化 1.1 递归 通过循环递归的方式&#xff0c;遍历数组的每一项&#xff0c;如果该项还是一个数组&#xff0c;那么就继续递归遍历&#xff0c…

Web 音视频(四)在浏览器中处理音频

前言 为什么单独介绍音频处理? 网络上缺乏音频处理的资料&#xff0c;绝大多数示例都是针对视频而略过音频&#xff0c;很多人在网上寻找音频处理的示例对前端开发者来说&#xff0c;音频处理相对视频略微复杂一些 所以&#xff0c;本文专门针对音频数据&#xff0c;汇总讲…

代理模式的作用

一、代理模式 代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问&#xff0c;这样就可以在不修改原目标对象的前提下&#xff0c;提供额外的功能操作&#xff0c;扩展目标对象的功能。 代理模式的主要作用是扩展目标对…

ubuntu20.04+RTX4060Ti大模型环境安装

装显卡驱动 这里是重点&#xff0c;因为我是跑深度学习的&#xff0c;要用CUDA&#xff0c;所以必须得装官方的驱动&#xff0c;Ubuntu的附件驱动可能不太行. 进入官网https://www.nvidia.cn/geforce/drivers/&#xff0c;选择类型&#xff0c;最新版本下载。 挨个运行&#…