C++RAII与智能指针

发布时间 2023-11-11 02:27:07作者: 橙皮^-^

一、RAII概念

Resource acquisition is initialization (RAII)[1]--由C++之父Bjarne Stroustrup提出,即获取资源即初始化。具体实践:使用一个对象,在其构造时获取资源,在对象生命期控制对资源访问始终有效,最后在对象析构的时候释放资源(这里的资源指 网络套接字、互斥锁、文件句柄、内存等)。
操作:

  • 1、在构造函数初始化(获取)资源
  • 2、在析构函数中释放资源

二、例子

2.1 标准库锁中RAII的应用

#include <mutex>
#include <fstream>
int WriteFile(int argc, char*argv[])
{
  static std::mutex mutex;
	//将锁资源交给本地变量对象管理
	//在构造时获取锁资源,在离开作用域销毁变量时释放资源
	//而不必去操心分支或异常处理情况
  std::lock_guard<std::mutex> lock(mutex);

  std::ofstream file("example.txt");
  if(!file.is_open()) {
	  // throw std::runtime_error("open file err");
	  // 这里若抛出异常, 离开函数作用域,销毁lock对象同时释放锁
    return -1;
  }
  //do something
  return 0;
}

同样的,可以通过作用域来管理本地变量生命周期,从而管理锁的粒度

int WriteFile(int argc, char*argv[])
{
	static std::mutex mutex;
	{
		std::lock_guard<std::mutex> lock(mutex);
		//操作临界资源
	}//离开作用域释放锁
	//其他操作
}

三、智能指针

智能指针是一类用来管理资源的类型,常用来管理堆分配内存资源,定义在头文件中std命名空间,也是一种对RAII技术的典型应用。C++11提供了三种类型智能指针

  • std::shared_ptr 共享指针
  • std::unique_ptr 独占指针
  • std::weak_ptr 弱指针

3.1 std::shared_ptr 共享指针

  • 考虑这样一个问题:有一个内存指针被多个对象引用,那么什么时候进行delete释放内存才是正确。这个时候手动释放内存就会变得复杂而难以维护。std::shared_ptr 使用引用计数[2],被每一个引用时,引用数加1,每删除一个引用时,引用数减少1,当引用数为0,自动释放内存资源。

  • 成员函数
    use_count() //获取当前引用计数
    get() //返回原指针
    ->或* //访问封装指针
    reset() // 减少一个引用计数

  • 使用例子

#include <memory>
#include <iostream>
class A
{
public:
  A(){
    std::cout << "construct A" << std::endl;
  }
  void Foo(){
    std::cout << "call Foo" << std::endl;
  }
  ~A(){
    std::cout << "destroy A" << std::endl;
  }
};
int main(int argc, char* argv[])
{
  // std::shared_prt<A> ptr_a(new A()); // 使用new 初始
  std::shared_ptr<A> ptr_a = std::make_shared<A>();
 // 使用std::make_shared 模板方法更好, 避免显示使用new
  //construct A
  ptr_a->Foo(); // -> 访问成员 call Foo
  {
    auto ptr1_a = ptr_a; // 赋值引用计数加1
    std::cout << ptr1_a.use_count() << std::endl; //获取当前计数 2
  }//退出作用域,销毁ptr1_a是减少引用计数1
  std::cout << ptr_a.use_count() << std::endl; // 1
  return 0;
} //退出ptra_a作用域,引用计数减少1到0,释放A destroy A
  • 当出现俩个对象互相引用的时候称为循环引用,会出现什么现象?
#include <memory>
#include <iostream>

class A;
class B;

class A
{
public:
  std::shared_ptr<B> ptr_b_;
  A(){
    std::cout << "construct A" << std::endl;
  }
  ~A(){
    std::cout << "destroy A" << std::endl;
  }
};

class B
{
public:
  std::shared_ptr<A> ptr_a_;
  B(){
    std::cout << "construct B" << std::endl;
  }
  ~B(){
    std::cout << "destroy B" << std::endl;
  }
};

int main(int argc, char* argv[])
{
  auto ptr_a = std::make_shared<A>(); // construct A
  auto ptr_b = std::make_shared<B>(); // construct B

  ptr_a->ptr_b_ = ptr_b; // b引用计数为2
  ptr_b->ptr_a_ = ptr_a; // a引用计数为2
  return 0;
  //程序退出并没有调用AB析构函数
  //按照栈释放内存顺序,先进后出,依次销毁ptr_a, A引用数减少1,
 //此时A对象仍然被B对象引用, 引用计数为1,因此没有释放A对象,
 //然后销毁ptr_b,B引用数减少1,
 //这时同样B对象被A对象引用,也是没能销毁。
 //这个时候就会导致内存泄漏。
}

运行上面的代码例子,可以观察到,只调用了A和B的构造函数,并没有调用A和B的析构函数,也就是A和B对象内存资源都没能正确释放。这也是std::shared_ptr的缺陷。为了解决这个问题,C++引入了std::weak_ptr弱指针。

3.2 std::weak_ptr 弱指针

  • 结合std::shared_ptr指针使用的智能指针,提供一个对像的引用,但不参与引用计数,处理循环引用问题。
  • 弱指针没有 * 和 -> 运算符,不能直接对资源进行操作,一般访问流程,先调用expired检查资源是否已释放->若资源存在调用lock方法返回另一个std::shared_ptr指针, 不存在返回nullptr,使用后者进行资源操作。
    用std::weak_ptr处理上面循环引用问题
#include <memory>
#include <iostream>

class A;
class B;

class A
{
public:
  std::shared_ptr<B> ptr_b_;
  A(){
    std::cout << "construct A" << std::endl;
  }
  void Foo(){
    std::cout << "call Foo()" << std::endl;
  }
  ~A(){
    std::cout << "destroy A" << std::endl;
  }
};

class B
{
public:
  std::weak_ptr<A> ptr_a_;
  B(){
    std::cout << "construct B" << std::endl;
  }
  ~B(){
    std::cout << "destroy B" << std::endl;
  }
};

int main(int argc, char* argv[])
{
  auto ptr_a = std::make_shared<A>(); // construct A
  auto ptr_b = std::make_shared<B>(); // construct B

  ptr_a->ptr_b_ = ptr_b;
  ptr_b->ptr_a_ = ptr_a;
  //弱指针访问流程
  if(!ptr_b->ptr_a_.expired()) {
    //资源未被释放
    std::shared_ptr<A> sh_ptr = ptr_b->ptr_a_.lock();//返回另一个指向A的共享指针
    sh_ptr->Foo();//访问A资源
  }
  return 0;
}
//destroy A
//destroy B

3.3 std::unique_ptr 独占指针

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,只允许移动操作。

#include <memory>
#include <iostream>

class A;
class B;

class A
{
public:
  std::shared_ptr<B> ptr_b_;
  A(){
    std::cout << "construct A" << std::endl;
  }
  void Foo(){
    std::cout << "call Foo()" << std::endl;
  }
  ~A(){
    std::cout << "destroy A" << std::endl;
  }
};

int main(int argc, char* argv[])
{
  std::unique_ptr<A> ptr0_a(nullptr);
  {
    auto ptr1_a = std::make_unique<A>();
    //std::make_unique 需要C++14
    //auto ptr = ptr1_a; 编译错误 不允许赋值操作
    auto ptr0_a = std::move(ptr1_a); //只允许移动操作
    if(ptr1_a) {
      //ptr1_a被移动
      ptr1_a->Foo();
    }
    //移动过后ptr1_a还可以直接访问吗?
    ptr1_a->Foo(); //实际上还是可以直接访问的。需要注意
  }//A的生命周期由ptr0_a控制
  ptr0_a->Foo();
  return 0;
}

四、参考资料


  1. https://www.stroustrup.com/bs_faq2.html#finally ↩︎

  2. https://en.wikipedia.org/wiki/Reference_counting ↩︎