目录
1、什么是智能指针?
2、为什么需要智能指针
3、RAII思想及智能指针的原理
4、智能指针的发展
4.1 auto_ptr
4.2 unique_ptr
6、循环引用问题
1、什么是智能指针?
智能指针是一种用于管理动态分配的内存的 C++ 类。它们提供了对堆内存的自动分配和释放,以防止内存泄漏和悬挂指针的情况。常见的智能指针包括 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。它们提供不同的所有权和生命周期管理模型,以满足不同的需求。
std::unique_ptr: 代表独占所有权的智能指针。它允许一个对象拥有对动态分配的内存的唯一所有权,当其超出作用域时,会自动释放所管理的内存。它不能被复制或赋值,因为复制会导致所有权转移,从而破坏唯一性。
std::shared_ptr: 代表共享所有权的智能指针。多个 shared_ptr 对象可以共享同一块动态分配的内存。内部维护一个引用计数,当所有 shared_ptr 对象都释放了对内存的引用时,才会释放内存。这种指针适用于需要多个所有者的情况。
std::weak_ptr: 代表弱引用的智能指针。它不会增加引用计数,也不会影响内存的释放。通常与 shared_ptr 搭配使用,用于避免循环引用导致的内存泄漏问题。可以通过 lock() 方法获取一个 shared_ptr,如果所指向的对象还存在的话。
2、为什么需要智能指针
我们来看一下这段代码:
#include<iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
1、在
Func()
函数中,如果new int
抛出异常,导致内存分配失败,那么p1
指针会成为悬空指针,没有办法释放动态分配的内存,因为没有对应的delete
调用。这会导致内存泄漏。2、同样地,如果
p2
的new int
抛出异常,会导致p2
成为悬空指针,同样会造成内存泄漏。3、如果在
div()
函数中,当用户输入的b
为 0 时,会抛出一个invalid_argument
异常。而在Func()
中调用div()
,如果这个异常被抛出,那么new int
分配的内存将无法被释放,同样会导致内存泄漏。智能指针可以很好地解决这些问题。通过使用智能指针,我们可以确保在发生异常或函数退出时,动态分配的内存会得到正确的释放,从而避免内存泄漏。因为智能指针的析构函数会在对象超出作用域时自动调用
delete
操作,确保资源的正确释放。因此,将p1
和p2
替换为std::unique_ptr<int>
或std::shared_ptr<int>
将会更安全和可靠。
3、RAII思想及智能指针的原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这样做有两大好处:
无需显式释放资源: 通过 RAII,资源的释放被嵌入到对象的析构函数中,因此不需要在代码中显式地释放资源。这样可以避免忘记释放资源而导致的内存泄漏或资源泄漏问题。
资源的自动管理: 对象的生命周期由语言的自动对象生存期规则控制,因此当对象超出作用域时,资源会自动释放,从而确保资源在不再需要时被正确释放,避免资源泄漏和错误使用资源的问题。
智能指针设计:
#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
}
~SmartPtr()
{
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
这段代码定义了一个简单的模板类 SmartPtr
,用于管理动态分配的内存。然后,在 Func
函数中创建了两个 SmartPtr<int>
对象,分别管理两个动态分配的整型变量,当对象超出作用域资源就会自动释放,然后调用了 div
函数进行除法运算。如果除数为零,则抛出 invalid_argument
异常。
仅仅释放资源还不能称为智能指针,所以需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
4、智能指针的发展
4.1 auto_ptr
// std::auto_ptr<int> sp1(new int);// std::auto_ptr<int> sp2(sp1); // 管理权转移 这个时候sp1已经失效了
auto_ptr
采用了独占所有权的模型,意味着当一个 auto_ptr
被赋值给另一个 auto_ptr
后,原始 auto_ptr
将失去对资源的所有权。这导致在代码中进行所有权转移时很容易出现问题,例如可能会导致资源多次释放或悬空指针。auto_ptr
存在诸多弊端,因此不建议在现代 C++ 中使用它。
自 C++ 11 起,此类模板已弃用。unique_ptr 是一个具有类似功能的新工具,但具有更高的安全性
4.2 unique_ptr
既然auto_ptr的拷贝存在问题,那么unique_ptr直接禁用了赋值拷贝。
我们先来看一下
移动赋值运算符 (1): 接受一个右值引用参数,用于将资源所有权从一个
unique_ptr
转移到另一个。这种赋值运算符用于实现资源的移动语义,通常在转移所有权时使用。赋值为 null 指针 (2): 接受一个
nullptr_t
参数,用于将unique_ptr
赋值为 null 指针,即释放当前持有的资源,并将unique_ptr
设置为 null。类型转换赋值运算符 (3): 接受一个右值引用参数,用于将资源所有权从一个
unique_ptr
转移到另一个,但允许指定不同的模板参数类型和删除器类型。这种赋值运算符用于允许更灵活的类型转换和删除器设置。拷贝赋值运算符 (4): 这个赋值运算符是被删除的,意味着不能直接对
unique_ptr
进行拷贝赋值操作。这是因为unique_ptr
是独占所有权的智能指针,不允许多个指针共享同一个资源,因此拷贝赋值在语义上是不合适的,所以被明确删除。
unique_ptr不允许多个指针共享一个资源,虽然还有使用场景,但明显场景受限制。这个时候有了更加优化的share_ptr.
4.3 share_ptr
share_ptr允许多个指针共享一个资源,采用引用计数的思想,只有当计数为一时才会析构,避免了同一块空间多次释放的问题。由于share_ptr比较完善,所以实际应用场景也会更多一些。
5、share_ptr的模拟实现
shared_ptr
的基本思想:共享同一个指针,但维护一个引用计数来追踪有多少个 shared_ptr
指向相同的资源。每次智能指针被复制时,引用计数增加;每次智能指针被销毁时,引用计数减少。如果引用计数减少到 0,智能指针会释放所持有的资源。
template <class T>
class share_ptr
{
public:
share_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{
}
share_ptr(const share_ptr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
//拷贝时引用计数++
++(*_count);
}
share_ptr<T>& operator=(const share_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();//释放可能存在的资源
_ptr = sp._ptr;
_count = sp._count;
//拷贝时引用计数++
++(*_count);
}
return *this;
}
void release()
{
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
}
~share_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
int main()
{
share_ptr<int> sp;
return 0;
}
share_ptr还需要注意循环引用问题
6、循环引用问题
循环引用是指两个或多个对象之间相互引用,导致它们的引用计数永远无法归零,从而导致内存泄漏。这在使用智能指针时是一个常见的问题,特别是在使用 shared_ptr
时更容易出现。
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
};
class B {
public:
std::shared_ptr<A> a_ptr;
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
类 A
拥有一个指向类 B
对象的 shared_ptr
,而类 B
拥有一个指向类 A
对象的 shared_ptr
。当 a
和 b
超出作用域时,它们的引用计数永远不会归零,因为彼此都持有对方的指针,而只有指针释放时所指向的对象才会释放,但是指针是对象的成员,所以循环引用谁也不会释放,从而导致内存泄漏。
解决方案:
#include <memory>
class B;
class A {
public:
std::weak_ptr<B> b_weak_ptr; // 将其中一个指针设计为 weak_ptr
};
class B {
public:
std::shared_ptr<A> a_ptr;
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_weak_ptr = b; // 使用 weak_ptr
b->a_ptr = a;
// 在使用前检查 weak_ptr
if (auto shared_b = a->b_weak_ptr.lock()) {
// 如果对象仍然存在,则可以安全地使用 shared_b
std::cout << "Object B is still alive." << std::endl;
} else {
std::cout << "Object B is expired or deleted." << std::endl;
}
return 0;
}
使用
weak_ptr
打破循环引用:将其中一个指针设计为weak_ptr
,这样它不会增加对象的引用计数。但是需要在使用前对weak_ptr
进行检查,以确保对象仍然存在。
7、share_ptr中的自定义删除器
如何根据不同的对象来执行特定的清理操作。
shared_ptr
允许指定一个自定义的删除器(deleter),并且提供了对应的构造函数
#include <iostream>
#include <memory>
// 自定义资源
struct MyResource {
MyResource() { std::cout << "资源已分配。" << std::endl; }
~MyResource() { std::cout << "资源已释放。" << std::endl; }
void CustomCleanup() { std::cout << "自定义清理操作。" << std::endl; }
};
// 删除器函数对象
struct CustomDeleter {
void operator()(MyResource* ptr) const {
if (ptr) {
ptr->CustomCleanup();
delete ptr;
}
}
};
int main() {
// 创建 shared_ptr,并指定删除器
std::shared_ptr<MyResource> ptr(new MyResource(), CustomDeleter());
// 使用 shared_ptr
// ...
// 当 shared_ptr 超出作用域时,将会调用删除器来释放资源
return 0;
}
MyResource
是我们要管理的自定义资源,CustomDeleter
是我们定义的删除器函数对象。在创建 shared_ptr
时,我们将资源指针和删除器一起传递给 shared_ptr
的构造函数。当 shared_ptr
超出作用域时,删除器将被调用来执行特定的清理操作,例如执行自定义的清理函数 CustomCleanup()
并释放资源。
我们可以根据不同的场景使用不同的删除器
std::shared_ptr<A> sp(new A[10], [](A* p){delete[] p; });
这里创建了一个 shared_ptr
,它指向一个包含 10 个 A
对象的动态数组。在这里,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数负责释放数组内存,使用 delete[]
来释放动态数组的内存。
std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* p){fclose(p); });
这里创建了一个 shared_ptr
,它指向通过 fopen
打开的文件指针。同样地,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数使用 fclose
来关闭文件。
这种方式允许我们在 shared_ptr
不再需要资源时执行特定的清理操作,确保资源被正确释放,避免内存泄漏或资源泄漏。