CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章C++惯用法与ModernC++篇

    • 1.1 C++ RAII惯用法
      • 1.2 pimpl 惯用法
      • 1.3 拥抱C++新变化(C++11/14/17新增的实用特性)
      • 1.4 统一的类成员初始化语法与 std::initializer_list<T>
      • 1.5 C++17注解标签(attributes)
      • 1.6 final/override/=default/=delete语法
      • 1.7 auto关键字的前尘后事
      • 1.8 Range-based 循环语法
      • 1.9 C++17结构化绑定
      • 1.10 stl容器新增的实用方法介绍
      • 1.11 stl中的智能指针类详解
    • 第2章C++开发工具与调试进阶

    • 第3章C++多线程编程从入门到进阶

    • 第4章C++网络编程重难点解析

    • 第5章网络通信故障排查常用命令

    • 第6章网络通信协议设计

    • 第7章高性能服务结构设计

    • 第8章Redis 网络通信模块源码分析

    • 第9章 服务其他模块设计

    • C++后端开发进阶
    • 第1章C++惯用法与ModernC++篇
    zhangxf
    2023-04-03
    目录

    1.1 C++ RAII惯用法

    什么是RAII?容我先卖个关子,给大家讲个故事。

    # 1.1.1 版本一 最初的写法

    我在刚学习服务器开发的时候公司安排给新人一个练习:在Windows系统上写一个C++程序,该程序的功能是实现一个简单的服务,当客户端连接上来时,给客户端发一条 “HelloWorld” 消息后关闭连接,不用保证客户端一定能收到。

    如果你熟悉基础网络编程知识,你会觉得这很容易嘛,这个程序就是TCP网络通信的基本流程,实现流程如下:

    1. 创建socket;

    2. 绑定ip地址和端口号;

    3. 在该ip地址和端口号上启动侦听,然后循环等待客户端连接的到来,当客户端连接成功后,发送一条 “HelloWorld” 消息,然后断开连接。

      在Windows操作系统上,使用网络通信API之前,需要使用WSAStartup函数初始化一下socket库,在程序结束时,需要使用WSACleanup函数清理下socket库。

    我很快就将程序写出来了:

    #include <winsock2.h>
    #include <stdio.h>
    
    //链接Windows的socket库
    #pragma comment(lib, "ws2_32.lib")
    
    int main(int argc, char* argv[])
    {
        //初始化socket库
        WORD wVersionRequested = MAKEWORD(1, 1);
        WSADATA wsaData;
        int err = WSAStartup(wVersionRequested, &wsaData);
        if (err != 0)
            return 1;
    
        if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
        {
            WSACleanup();
            return 1;
        }
    
        //创建用于监听的套接字
        SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
        if (sockSrv == -1)
        {
            WSACleanup();
            return 1;
        }
    
        SOCKADDR_IN addrSrv;
        addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        addrSrv.sin_family = AF_INET;
        addrSrv.sin_port = htons(6000);
        //绑定套接字,监听6000端口
        if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == -1)
        {
            closesocket(sockSrv);
            WSACleanup();
            return 1;
        }
    
        //启动监听,准备接受客户请求
        if (listen(sockSrv, 15) == -1)
        {
            closesocket(sockSrv);
            WSACleanup();
            return 1;
        }
    
        SOCKADDR_IN addrClient;
        int len = sizeof(SOCKADDR);
        char msg[] = "HelloWorld";
        while (true)
        {
            //等待客户请求到来,如果有客户端连接,则接受连接
            SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
            if (sockClient == -1)
            	break;
            	
            //给客户端发送”HelloWorld“消息
            send(sockClient, msg, strlen(msg), 0);
            closesocket(sockClient);
        }// end while-loop
    
        closesocket(sockSrv);
        WSACleanup();
    
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69

    上面的代码虽然满足了任务要求,但是有些地方令人不太满意,代码中到处充斥着重复的、为了考虑出错情形的资源清理逻辑:

    closesocket(sockSrv);
    
    1

    和

    WSACleanup();
    
    1

    这样的场景我们在实际开发中经常遇到,例如下面这段伪码:

    char* p = new char[1024];
    
    if (操作1不成功)
    {
        delete[] p;
        p = NULL;
        return;
    }
    
    if (操作2不成功)
    {
        delete[] p;
        p = NULL;
        return;
    }
    
    if (操作3不成功)
    {
        delete[] p;
        p = NULL;
        return;
    }
    
    delete[] p;
    p = NULL;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    这种情形我们可以归纳成:先分配资源,然后进行相关操作,在任一中间步骤中操作出错时对资源进行回收,如果中间步骤没有错误,我们在资源使用完毕之后也要回收相应的资源。上述伪代码片段中释放资源的重要性不言而喻,因为分配了堆内存,否则会造成内存泄露。但是实际在编写这样的代码时太容易出错了,我们必须时刻保持警惕,在任何一个出错步骤中都要记得回收资源,因此也造成了大量重复逻辑。那么有没有方法解决这类问题呢?有,使用goto语句。

    # 1.1.2 版本二 使用goto语句

    还是以前面网络通信的代码为例,如果使用goto语句,上述代码可以简化成如下形式:

    #include <winsock2.h>
    #include <stdio.h>
    
    //链接Windows的socket库
    #pragma comment(lib, "ws2_32.lib")
    
    int main(int argc, char* argv[])
    {
        //由于goto语句不能跳过变量定义,
        //所以提前定义下文需要用到的变量
        SOCKET sockSrv;
        SOCKADDR_IN addrSrv;
        SOCKADDR_IN addrClient;
        int len = sizeof(SOCKADDR);
        char msg[] = "HelloWorld";
        
        //初始化socket库
        WORD wVersionRequested = MAKEWORD(1, 1);
        WSADATA wsaData;
        int err = WSAStartup(wVersionRequested, &wsaData);
        if (err != 0)
            return 1;
    
        if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
        {
            goto cleanup2;
            return 1;
        }
    
        //创建用于监听的套接字
        sockSrv = socket(AF_INET, SOCK_STREAM, 0);
        if (sockSrv == -1)
        {
            goto cleanup2;
            return 1;
        }
    
        addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        addrSrv.sin_family = AF_INET;
        addrSrv.sin_port = htons(6000);
        //绑定套接字,监听6000端口
        if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == -1)
        {
            goto cleanup1;
            return 1;
        }
    
        //启动监听,准备接受客户请求
        if (listen(sockSrv, 15) == -1)
        {
            goto cleanup1;
            return 1;
        }
    
        while (true)
        {
            //等待客户请求到来,如果有客户端连接,则接受连接
            SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
            if (sockClient == -1)
                break;
    
            //给客户端发送”HelloWorld“消息
            send(sockClient, msg, strlen(msg), 0);
            closesocket(sockClient);
        }// end while-loop
    
    cleanup1:
        closesocket(sockSrv);
    
    cleanup2:
        WSACleanup();
    
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74

    使用了goto语句之后,我们一旦中间某个步骤出错,则跳转到统一的清理点处进行资源清理操作。

    但是,这样的代码还是令我们很忧伤,当年在学编程语言的第一堂课时老师就告诉我们慎用“goto”语句,它是魔鬼,会让我们的程序结构变得混乱和难以维护,各种编程书籍在介绍goto语句时也重复表达着同样的意思。这里姑且不评论老师的话和编程书籍上不建议使用goto语句到底是不是必须遵守的金科玉律。如果不用goto语句,有没有更好的实现方式呢?有的,且看版本三。

    # 1.1.3 版本三 使用do...while(0)循环

    使用do...while(0)循环改进后的代码:

    #include <winsock2.h>
    #include <stdio.h>
    
    //链接Windows的socket库
    #pragma comment(lib, "ws2_32.lib")
    
    int main(int argc, char* argv[])
    {
        //初始化socket库
        WORD wVersionRequested = MAKEWORD(1, 1);
        WSADATA wsaData;
        int err = WSAStartup(wVersionRequested, &wsaData);
        if (err != 0)
            return 1;
    
        SOCKET sockSrv = -1;
        do
        {
            if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
                break;
    
            //创建用于监听的套接字
            sockSrv = socket(AF_INET, SOCK_STREAM, 0);
            if (sockSrv == -1)
                break;
    
            SOCKADDR_IN addrSrv;
            addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
            addrSrv.sin_family = AF_INET;
            addrSrv.sin_port = htons(6000);
            //绑定套接字,监听6000端口
            if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == -1)
                break;
    
            //启动监听,准备接受客户请求
            if (listen(sockSrv, 15) == -1)
                break;
    
            SOCKADDR_IN addrClient;
            int len = sizeof(SOCKADDR);
            char msg[] = "HelloWorld";
            while (true)
            {
                //等待客户请求到来,如果有客户端连接,则接受连接
                SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
                if (sockClient == -1)
                    break;
    
                //给客户端发送”HelloWorld“消息
                send(sockClient, msg, strlen(msg), 0);
                closesocket(sockClient);
            }// end inner-while-loop
        } while (0); //end outer-while-loop
    
        if (sockSrv != -1)
            closesocket(sockSrv);
    
        WSACleanup();
    
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61

    上述代码利用do...while(0)循环中的break特性巧妙地将资源回收操作集中到一个地方,使用for循环也能达到一样的效果。我们同样可以使用do...while(0)形式去改造上文中堆内存分配与释放的示例伪代码:

    char* p = NULL;
    do
    {
        p = new char[1024];
        if (操作1不成功)
            break;
    
        if (操作2不成功)
            break;
    
        if (操作3不成功)
            break;
    } while (0);
    
    delete[] p;
    p = NULL;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这是do...while(0)的一个妙用,当我刚参加工作时第一次在项目中遇到这种写法时很费解,等搞明白后觉得这种写法很巧妙。我们的故事到这里还没完结呢,在C++语言中,我们有更好的写法来代替do...while(0),这就是本节我们要介绍的RAII惯用法。哎呀,绕了一个大圈子,主角终于上场了。那什么是RAII惯用法呢?

    # 1.1.4 版本四 RAII惯用法

    RAII是英文Resource Acquisition Is Initialization的缩写,翻译成中文是“资源获取就是初始化”,这个翻译仍然令人费解。通俗地说,所谓RAII就是资源在你拿到时就已经初始化好了,一旦你不再需要这个资源,该资源可以自动释放。

    对于C++语言来说,即资源在构造函数中初始化(可以在构造函数中调用单独的初始化函数),在析构函数中释放或清理。常见的情形就是函数调用中,创建C++对象时分配资源,当C++对象出了作用域时会自动被清理和释放(不管这个这个对象是如何出作用域的——中间某个步骤不满足提前return掉还是正常走完全部流程后 return)。

    还是以上面网络通信的例子来说,程序初始化时我们需要分配两种资源:

    • 资源一: 初始化好Windows socket网络库;
    • 资源二: 创建一个用于侦听的socket。

    而我们在程序结束时,我们需要清理上述两种资源。

    使用RAII惯用法改进后的代码如下:

    #include <winsock2.h>
    #include <stdio.h>
    
    //链接Windows的socket库
    #pragma comment(lib, "ws2_32.lib")
    
    class ServerSocket
    {
    public:
        ServerSocket()
        {
            m_bInit = false;
            m_ListenSocket = -1;
        }
    
        ~ServerSocket()
        {
            if (m_ListenSocket != -1)
                ::closesocket(m_ListenSocket);
    
            if (m_bInit)
                ::WSACleanup();
        }
    
        bool DoInit()
        {
            //初始化socket库
            WORD wVersionRequested = MAKEWORD(1, 1);
            WSADATA wsaData;
            int err = WSAStartup(wVersionRequested, &wsaData);
            if (err != 0)
                return false;
    
            if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
                return false;
                
            m_bInit = true;
    
            //创建用于监听的套接字
            m_ListenSocket = socket(AF_INET, SOCK_STREAM, 0);
            if (m_ListenSocket == -1)
                return false;
    
            return true;
        }
    
        bool DoBind(const char* ip, short port = 6000)
        {
            SOCKADDR_IN addrSrv;
            addrSrv.sin_addr.S_un.S_addr = inet_addr(ip);
            addrSrv.sin_family = AF_INET;
            addrSrv.sin_port = htons(port);
            if (::bind(m_ListenSocket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == -1)
                return false;
    
            return true;
        }
    
        bool DoListen(int backlog = 15)
        {
            if (listen(m_ListenSocket, backlog) == -1)
                return false;
    
            return true;
        }
    
        bool DoAccept()
        {
            SOCKADDR_IN addrClient;
            int len = sizeof(SOCKADDR);
            char msg[] = "HelloWorld";
            while (true)
            {
                //等待客户请求到来,如果有客户端连接,则接受连接
                SOCKET sockClient = accept(m_ListenSocket, (SOCKADDR*)&addrClient, &len);
                if (sockClient == -1)
                    break;
    
                //给客户端发送”HelloWorld“消息
                send(sockClient, msg, strlen(msg), 0);
                closesocket(sockClient);
            }// end inner-while-loop
    
            return false;
        }
    
    private:
        bool    m_bInit;
        SOCKET  m_ListenSocket;
    };
    
    int main(int argc, char* argv[])
    {
        ServerSocket serverSocket;
        if (!serverSocket.DoInit())
            return false;
    
        if (!serverSocket.DoBind("0.0.0.0", 6000))
            return false;
    
        if (!serverSocket.DoListen(15))
            return false;
    
        if (!serverSocket.DoAccept())
            return false;
    
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108

    上述代码中,我们没有在构造函数中分配资源,而是单独使用一个DoInit()方法来初始化资源,并在析构函数中回收相应的资源。这样在main函数中,我们不用担心任何中间步骤的失败忘记释放资源,因为一旦main函数调用结束,serverSocket对象会自动调用其析构函数回收相应资源。这就是RAII惯用法的原理!

    严格来说,上述代码中ServerSocket的成员变量m_bInit应该被设计成类静态成员,调用WSAStartup和WSACleanup的函数应该被设计成类的静态方法,因为它们分别只需要在程序初始化和退出时调用一次就可以了。

    我希望你能理解这种C++惯用法,因为它在C++中实在太常用了。我们也可以使用RAII惯用法再次改写上文中分配堆内存的示例伪代码:

    class HeapObjectWrapper
    {
    public:
        HeapObjectWrapper(int size)
        {
            m_p = new char[size];
        }
    
        ~HeapObjectWrapper()
        {
            delete[] m_p;
            m_p = NULL;
        }
    
    private:
        char*  m_p;
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    HeapObjectWrapper heapObj(1024);
    if (操作1不成功)
    	return;
    
    if (操作2不成功)
    	return;
    
    if (操作3不成功)
    	return;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    上述代码中heapObj对象一旦出了其作用域,会自动调用其析构函数释放堆内存。当然,RAII惯用法中的对资源的”分配“和”释放“的含义可以延伸出成各种外延和内涵来,如对多线程锁的获取和释放,我们在实际开发中也常常遇到以下情景:

    void SomeFunction()
    {
        得到某把锁;
        if (条件1)
        {
            if (条件2)
            {
                某些操作1
                释放锁;
                return;
            }
            else (条件3)
            {
                某些操作2
                释放锁;
                return;
            }
    	}
    
        if (条件3)
        {
            某些操作3
            释放锁;
            return;
        }
    
        某些操作4
        释放锁;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    这是一段非常常见的逻辑,为了避免死锁,我们必须在每个可能退出的分支上释放锁。随着逻辑写得越来越复杂,我们忘记在某个退出分支加上释放锁的代码的可能性也越来越大。而RAII惯用法正好解决了这个问题,我们可以将锁包裹成一个对象,在构造函数中获取锁,在析构函数中释放锁,伪码如下:

    class SomeLockGuard
    {
    public:
        SomeLockGuard()
        {
            //加锁
            m_lock.lock();
        }
    
        ~SomeLockGuard()
        {
            //解锁
            m_lock.unlock();
        }
    
    private:
        SomeLock  m_lock;
    };
    
    void SomeFunction()
    {
        SomeLockGuard lockWrapper;
        if (条件1)
        {
            if (条件2)
            {
                某些操作1
                return;
            }
            else (条件3)
            {
                某些操作2
                return;
            }
        }
    
        if (条件3)
        {
            某些操作3
            return;
        }
    
        某些操作4
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44

    使用了RAII惯用法之后,我们再也不必在每个函数出口处加上释放锁的代码了,因为锁会在函数调用结束后自动释放。

    对于上面的代码,有经验的读者可能一眼就看出来了:这不就是C++11 std::lock_guard和boost::mutex::scoped_lock的原理嘛,确实是这样,后面的章节我们会详细介绍操作系统和C++11各种锁的用法。

    # 1.1.5 小结

    资源泄露和死锁等问题具有非常强的隐蔽性,如果将来在生产环境出现,难以复现不说且一旦出现也不太容易排查与定位问题。理解并熟练使用RAII惯用法不仅能让你的代码更加简洁和模块化,而且可以在开发阶段就能避免一部分资源泄漏、死锁问题。

    上次更新: 2025/04/01, 20:53:14
    1.2 pimpl 惯用法

    1.2 pimpl 惯用法→

    最近更新
    01
    C++语言面试问题集锦 目录与说明
    03-27
    02
    第四章 Lambda函数
    03-27
    03
    第二章 关键字static及其不同用法
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式