C++RAII用法

思维导图

在这里插入图片描述

为什么要引入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.2
	WORD 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;
	}

	// 开始监听连接,最大连接数为 15
	if (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.2
    if (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;   // 使用 IPv4
    addrSrv.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.2
    WSADATA wsaData;                // 用于保存 WinSock 数据
    int ret = WSAStartup(version, &wsaData);  // 初始化 WinSock 库
    if (ret != 0) {  // 如果初始化失败
        return -1;   // 返回错误码 -1
    }

    SOCKET sockSrv = -1;  // 定义服务器套接字,初始化为 -1 表示未初始化

    do {
        // 检查 WinSock 版本是否为 2.2
        if (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);  // 设置端口为 6000

        SOCKADDR_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;  // 如果已初始化,直接返回 true

        WORD version = MAKEWORD(2, 2);  // 设置所需的 WinSock 版本 2.2
        WSADATA wsaData;                // 用于保存 WinSock 数据
        int ret = ::WSAStartup(version, &wsaData);  // 初始化 WinSock
        if (ret != 0)  // 如果初始化失败,返回 false
            return false;

        // 检查实际使用的版本是否为 2.2
        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
            return false;  // 版本不匹配,返回 false

        m_bInit = true;  // 成功初始化,标记为 true
        return 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)  // 如果套接字创建失败,返回 false
            return 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;  // 监听失败,返回 false

        return 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值