zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【代码质量】RAII在C++编程中的必要性

C++编程代码 质量 必要性
2023-09-14 09:07:07 时间


1 前言

  C/C++相比其他高级编程语言,具有指针的概念,指针即是内存地址。C/C++可以通过指针来直接访问内存空间,效率上的提升是不言而喻的,是其他高级编程语言不可比拟的;比如访问内存一段数据,通过指针可以直接从内存空间读取数据,避免了中间过程函数压栈、数据拷贝甚至消息传输等等。


  内存交给程序员管理,是存在隐患的;申请了内存,因为意外原因而没有释放,即导致内存泄漏;系统长期运行后因申请不到可用内存而导致异常,甚至崩溃。为了避免内存泄漏,C/C++程序员发明各类方法以检测或者避免内存泄漏。RAII就是一个C++规范标准,遵循该标准,以尽可能避免内存泄漏。


2 什么是RAII

  RAII全称为Resource Acquisition Is Initialization,由C++发明者Bjarne Stroustrup提出的设计理念;中文可直译为资源获取即为初始化,是C++语言的一种管理资源、避免泄漏的方法标准。RAII基本原则是,资源与对象的生命周期绑定,利用C++类将由程序员管理的资源间接转换为由系统管理,程序员不需显式地释放资源,以从根本上避免内存泄漏,基本步骤包括:

  • 自动申请资源
  • 使用资源
  • 自动释放资源

  RAII实现过程易于理解,在创建一个对象是,构造函数用于申请资源空间,在对象生命周期内,资源可用正常访问;对象超出作用域或者生命周期结束后(释放),系统调用析构函数释放已申请的资源。


3 为什么用RAII

  • 将由程序员管理的资源转换为由系统管理
  • 避免内存泄漏
  • 良好的编程约束标准

4 RAII应用

  RAII典型的应用例子就是C++11引入的智能指针、类模板锁,以解决内存泄漏、死锁问题。

  • 智能指针
  • 类模板lock_guard

  以一个“申请—释放”内存的最常用过程为例:

  • 常规写法,确保函数所有出口都释放申请的内存
int main(int argc, char * * argv)
{ 
    int *p = NULL;
    bool condition0 = false;
    bool condition1 = false; 
    
    p = new int();

    /* todo */

    if (condition0)
    {
        delete p;
        return -1;    
    }

    if (condition1)
    {
        delete p;
        return -1; 
    }

   delete p;

   return 0;
}

  • 因为中途退出函数,(忘记)未释放内存,导致内存泄漏
int main(int argc, char * * argv)
{ 
    int *p = NULL;
    bool condition0 = false;
    bool condition1 = false;
        
    p = new int();

    /* todo */

    if (condition0)
    {
        return -1;     /* 可能导致内存泄漏 */
    }

    if (condition1)
    {
        return -1 ;     /* 可能导致内存泄漏 */
    }
    
    delete p;

    return 0;
}

  • 引入RAII类,将内存交给系统管理
class new_raii
{  
public:  
    explicit new_raii(std::function<void()> fun):delete_fun(fun)
    {
        std::cout << "call constrcutor fun" << std::endl;
    }
    
    ~new_raii() 
    { 
        std::cout << "call destrcutor fun" << std::endl;
        delete_fun();   /* 调用释放资源函数 */
    }
private:  
    std::function<void()> delete_fun; 
}; 

  • RAII完整示例
#include <iostream>
#include <functional>

class new_raii
{  
public:  
    explicit new_raii(std::function<void()> fun):delete_fun(fun)
    {
        std::cout << "call constrcutor fun" << std::endl;
    }
    
    ~new_raii() 
    { 
        std::cout << "call destrcutor fun" << std::endl;
        delete_fun();   /* 调用释放资源函数 */
    }
private:  
    std::function<void()> delete_fun; 
}; 

int main(int argc, char * * argv)
{ 
    int *p = NULL;
    bool condition0 = true;
    bool condition1 = false; 
    
    p = new int();

	/* 将申请内存交给raii类对象管理 */
    new_raii([&]
        {
            std::cout << "call delete fun" << std::endl;
            delete p;
        }	/* 指定删除资源函数 */
    );	
    
    /* todo */
    if (condition0)
    {
        return -1;    
    }

    if (condition1)
    {
        return -1; 
    }

   return 0;
}

  编译执行结果:

acuity@ubuntu:/home/RAII$ g++ raii.cpp -o raii -std=c++11
acuity@ubuntu:/home/RAII$ ./raii
call constrcutor fun
call destrcutor fun
call delete fun
acuity@ubuntu:/home/RAII$ 

  从结果看,无论函数何时退出,都无需显示调用delete释放申请的内存。只要类对象生命周期结束,即调用析构函数释放内存。


5 小结

  RAII本质是将资源和对象生命周期绑定,将资源管理任务转化为对象管理任务,资源的申请和释放由系统自动调用构造和析构函数实现;将程序员管理的资源间接转为由系统管理。

  内存是系统资源之一,除此之外,其他系统资源如申请了,没有及时释放,同样会导致“资源泄漏”。对于其他系统资源,也可以参考RAII原则,确保系统的稳定性。内存是最直接接触的资源,不限于内存,常用系统资源及异常现象包括:

  • 文件描述符fd
      open了文件描述符,没有及时close,导致系统文件描述符用尽。

  • 互斥锁
      互斥锁资源的使用不当,导致死锁产生。

  • socket套接字
      socket套接字用尽,导致创建socket失败。

  • 端口、进程、线程、文件等等有限的资源