C++RAII与智能指针
一、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::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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App