为什么引入智能指针?
内存泄漏问题
C 在堆上申请内存后,需要手动对内存进行释放。随着代码日趋复杂和协作者的增多,很难保证内存都被正确释放,因此很容易导致内存泄漏。
在上述代码中,FunctionWithMemoryLeak()
函数动态分配了一个整型对象的内存,并在结束时没有释放该内存。这就导致了内存泄漏,因为没有机制来释放这块分配的内存。
void FunctionWithMemoryLeak() {
int* ptr = new int(5); // 在堆上动态分配内存
// 没有释放内存,造成内存泄漏
}
int main() {
FunctionWithMemoryLeak();
// ...
return 0;
}
多线程下的对象析构问题
在多线程环境下,对象的析构问题需要特别注意,因为多个线程可能同时访问和操作同一个对象。如果多个线程同时尝试析构同一个对象,可能会导致对象被多次删除。因此稍有不慎就会导致程序崩溃。
代码语言:javascript复制#include <iostream>
#include <thread>
using namespace std;
class Resource {
public:
Resource() {
cout << "Resource acquired." << endl;
}
~Resource() {
cout << "Resource released." << endl;
}
};
void ThreadFunc(Resource* resource) {
// 模拟对资源的使用
this_thread::sleep_for(std::chrono::seconds(2));
cout << "Thread: Using resource." << endl;
// 在多线程环境下,资源的析构可能由其他线程执行
delete resource;
}
int main() {
Resource* sharedResource = new Resource();
thread t(ThreadFunc, sharedResource);
// 在主线程中,早期销毁了资源
delete sharedResource;
t.join();
cout << "Main: Finished." << endl;
return 0;
}
在上述代码中,我们创建了一个共享资源Resource
的实例,并在主线程和另一个线程中对其进行操作。主线程在启动另一个线程后早期销毁了资源,而另一个线程仍在使用已经销毁的资源。这会导致未定义行为,访问无效的内存,可能导致崩溃或数据损坏。
而智能指针设计的初衷就是可以帮助我们管理堆上申请的内存,即开发者只需要申请,释放内存的任务交给智能指针。用于确保程序不存在内存和资源泄漏且是异常安全的。
智能指针的使用
下面是一个原始指针和智能指针比较的示例代码
代码语言:javascript复制// 原始指针
void rawptr(){
// 使用原始指针
Obj *rawptr = new Obj("raw pointer");
// dosomething
rawptr->doSomething();
// 释放内存
delete rawptr;
}
// 智能指针
void smtptr(){
// 使用智能指针
unique_ptr<Obj> smtptr(new Obj("smart pointer"));
// dosomething
smtptr->doSomething();
// 自动释放资源
}
智能指针通过封装指向堆分配对象的原始指针,并提供自动的内存管理和资源释放机制,帮助避免内存泄漏和资源管理错误。
智能指针的特点包括:
- 拥有权管理:智能指针拥有其所指向的对象,负责在适当的时机释放内存。这意味着当智能指针超出作用域或不再需要时,它会自动调用析构函数来释放内存。
- 析构函数处理:智能指针的析构函数中通常包含了对所拥有对象的内存释放操作,确保在智能指针被销毁时,关联的资源也会被释放。这种自动化的资源管理有助于避免内存泄漏和资源泄漏。
- 异常安全性:智能指针在异常情况下能够保证资源的正确释放。即使发生异常,智能指针也会在其作用域结束时被销毁,并调用析构函数来释放资源。
智能指针封装了指向堆分配对象的原始指针,因此智能指针通常提供直接访问其原始指针的方法。 C 标准库智能指针拥有一个用于此目的的get
成员函数。
// 智能指针
void smtptr(){
// 使用智能指针
unique_ptr<Obj> smtptr(new Obj("smart pointer"));
// dosomething
smtptr->doSomething();
// 获取智能指针的原始指针
Obj *obj = pLarge.get()
// 自动释放资源
}
智能指针的类型
目前C 11主要支持的智能指针为以下几种:
- unique_ptr
- shared_ptr
- weak_ptr
unique_ptr
std::unique_ptr
是 C 标准库提供的智能指针之一,用于管理动态分配的对象。它提供了独占所有权的语义,即同一时间只能有一个std::unique_ptr
拥有对对象的所有权。当std::unique_ptr
被销毁或重置时,它会自动释放所拥有的对象,并回收相关的内存。
std::unique_ptr
支持所有权的转移,可以通过move
将一个std::unique_ptr
实例的所有权转移到另一个实例。这种所有权转移可以通过移动构造函数和移动赋值运算符来实现。
// 创建一个 std::unique_ptr
unique_ptr<Resource> uResource1 = make_unique<Resource>();
// 使用移动构造函数将所有权转移到另一个 std::unique_ptr
unique_ptr<Resource> uResource2 = move(uResource1);
可以通过下图来描述
其基本用法
代码语言:javascript复制unique_ptr<Obj> a1(new Obj());
// 获取原始指针
Obj *raw_a = a1.get();
/*
std::unique_ptr 类型提供了一个名为 operator bool() 的成员函数,
用于将 std::unique_ptr 对象转换为布尔值。
该函数用于检查 std::unique_ptr 是否持有有效的指针
*/
if(a1)
{
// a1 拥有指针
}
// release释放所管理指针的所有权,返回原生指针。下面两条语句等价
unique_ptr<Obj> a2(a1.release());
a2 = move(a1);
// reset释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
a2.reset(new Obj());
// 没有参数,以下两条语句等价
a2.reset();
//释放并销毁原有对象
a2 = nullptr;
参考官方文档:如何:创建和使用 unique_ptr 实例
shared_ptr
std::shared_ptr
用于管理动态分配的对象。与std::unique_ptr
不同,std::shared_ptr
允许多个智能指针共享对同一个对象的所有权,通过引用计数来跟踪资源的使用情况。当最后一个std::shared_ptr
对象销毁时,资源会被释放。也就是说多个std::shared_ptr
可以拥有同一个原生指针的所有权。
在初始化一个shared_ptr
之后,可以复制它,将其分配给其他shared_ptr
实例。 所有实例均指向同一个对象,并共享资源与一个控制块。每当新的shared_ptr
添加、超出范围或重置时增加和减少引用计数,当引用计数达到零时,控制块将删除内存资源和自身。
可以通过下图来描述
代码语言:javascript复制// 第一次创建内存资源时,请使用make_shared
auto sptr = make_shared<Obj>()
shared_ptr<Obj> a1(new Obj());
// 与unique_ptr不同,允许共享
shared_ptr<Obj> a2 = a1;
// 获得原生指针
Obj *raw_a = a1.get();
/*
std::unique_ptr 类型提供了一个名为 operator bool() 的成员函数,
用于将 std::unique_ptr 对象转换为布尔值。
该函数用于检查 std::unique_ptr 是否持有有效的指针
*/
if(a1)
{
// a1 拥有指针
}
// 如果引用计数为 1,则返回true,否则返回false
if(a1.unique())
{
// 如果返回true,引用计数为1
}
// use_count() 返回引用计数的大小
int cnt = a1.use_count();
参考官方文档:如何:创建和使用 shared_ptr 实例
weak_ptr
循环引用的情况是指两个或多个
std::shared_ptr
对象相互持有对方的所有权,形成死锁,导致引用计数无法降为零,从而std::shared_ptr
无法被释放造成内存泄漏。
std::weak_ptr
用于解决std::shared_ptr
可能引发的循环引用和内存泄漏问题。std::weak_ptr
允许跟踪一个由std::shared_ptr
管理的对象,而不会增加引用计数。它本身是一个弱指针,所以它本身是不能直接调用原生指针的方法的。如果想要使用原生指针的方法,需要将其先转换为一个std::shared_ptr
。
weak_ptr
可以通过一个shared_ptr
创建。
shared_ptr<Obj> a1(new Obj());
weak_ptr<Obj> weak_a1 = a1; //不增加引用计数,避免循环引用
// expired()判断所指向的原生指针是否被释放,如果被释放了返回true,否则返回false
if(weak_a1.expired())
{
//如果为true,weak_a1对应的原生指针已经被释放了
}
// 返回原生指针的引用计数
int cnt = weak_a1.use_count();
/*
lock()返回shared_ptr,如果原生指针没有被释放,
则返回一个非空的shared_ptr,否则返回一个空的shared_ptr
*/
if(shared_ptr<Obj> shared_a = weak_a1.lock())
{
//此时可以通过shared_a进行原生指针的方法调用
}
//将weak_a1置空
weak_a1.reset();
参考官方文档:如何:创建和使用 weak_ptr 实例
智能指针使用实践
writing
参考文章:C 智能指针最佳实践&源码分析