智能指针

首先理解什么是RAII,RAII(Resource Acquisition Is Initialization)是C++中用来管理资源的生命周期的一种技术。
在 RAII 中,资源的获取和释放是在对象的构造函数和析构函数中完成的。当对象被创建时,它的构造函数被调用,从而获取资源;当对象超出作用域时,其析构函数被调用,从而释放资源。这样就确保了资源在对象的生命周期内始终是有效的,即使在发生异常或其他错误的情况下也能正确地释放资源

不带引用计数的智能指针

下面实现一个非常简单的智能指针

#include <iostream>
#include <stdexcept>
template <typename T>
class SmartPointer
{
public:
SmartPointer(T *ptr) : _ptr(ptr) {}
~SmartPointer()
{
delete _ptr;
}
T *operator->() { return _ptr; }
T &operator*() { return *_ptr; }
private:
T *_ptr;
};
class Bar
{
public:
~Bar() { std::cout << "destructor" << std::endl; }
void func() { std::cout << "func()" << std::endl; }
};
int main()
{
{
SmartPointer<Bar> f(new Bar());
f->func();
(*f).func();
}
}

代码的运行结果如下:
func()
func()
destructor

可以看到,这个智能指针非常简单,就是在析构函数中正确释放资源,然后提供了->*运算符重载函数。
智能指针就是对普通指针进行了一层封装,然后提供了对应的运算符重载函数,使其接口用起来像普通指针,同时在智能指针对象出作用域时会自动调用析构函数。
只要保证析构函数中,正确释放了资源,那么就能保证资源在出作用域时自动释放。

一般不将智能指针创建在堆上,创建在堆上SmartPointer<Bar> *f = new SmartPointer<Bar>(new Bar());,仍需要手动delete,才能访问智能指针的析构函数从而释放资源。这种做法与直接使用裸指针无异。

带引用计数的智能指针

上面的版本,不带引用计数,所以有一个很严重的问题。

int main()
{
{
SmartPointer<Bar> sp(new Bar());
SmartPointer<Bar> sp2(sp); // 拷贝构造
}
}

上面这个代码片段的执行结果为:
destructor
destructor
free(): double free detected in tcache 2
Aborted (core dumped)

可以发现,程序对同一个内存执行了两次delete。这是因为,SmartPointer<Bar> sp2(sp);执行的是浅拷贝,故两个智能指针对象中存放的是同一个内存地址,所以会有double free。
那么,能不能把这个浅拷贝改成深拷贝呢?
答案显然是不能的,改成深拷贝意味着每次执行拷贝都会重新开辟内存空间,然后拷贝数据,但本来的意图可能只是需要传递一个指针而已,也就是两个指针指向同一个内存。

那么,如何解决这个问题呢?从是否共享所有权的角度来分类,有两种办法:

  1. 不共享所有权
    • 拒绝拷贝的发生,直接把拷贝(移动)构造,拷贝(移动)赋值函数删除,对应的是std::scoped_ptr
    • 每次拷贝都是移动,都把资源的所有权转移出去,也就是只有最后一个智能指针拥有所有权,其它智能指针中都置为nullptr,对应的是std::auto_ptr
    • 允许移动构造、移动赋值操作,不允许拷贝构造、拷贝赋值,也就是c++11中的std::unique_ptr,通过std::unique_ptr<T> uptr2(std::move(uptr1))来创建新对象。

std::scoped_ptr只能用于单个对象,不能拷贝移动,std::auto_ptr自动转移所有权,容易出错,比如再次使用交出所有权的对象,这俩个都不推荐使用。

std::unique_ptr可以移动构造新对象,而且是显式的发生所有权的转移的。

  1. 共享所有权
    利用引用计数,通过引用计数是否为0,来判断是否真的执行delete操作。当发生拷贝时,引用计数加一,当有一个智能指针出作用域,对应的引用计数减一,当引用计数减为0时,才真正执行delete操作。
    这样就能保证,不发生double free,同时最后一个出作用域的智能指针能够正确释放资源。

尝试实现一个带引用技术的智能指针

#include <iostream>
#include <stdexcept>
template <typename T>
class RefCnt
{
public:
RefCnt(T *ptr) : ptr(ptr)
{
if (ptr)
cnt = 1;
}
void increRef()
{
cnt++;
}
int decreRef()
{
return --cnt;
}
private:
T *ptr;
int cnt; // 不是线程安全的
};
template <typename T>
class SharedPtr
{
public:
SharedPtr(T *ptr) : ptr(ptr)
{
rc = new RefCnt<T>(ptr);
}
SharedPtr(const SharedPtr &sp) : ptr(sp.ptr), rc(sp.rc)
{
if (ptr)
rc->increRef();
}
SharedPtr &operator=(const SharedPtr &sp)
{
if (this == &sp)
return *this;
if (0 == rc->decreRef())
{
delete ptr;
}
ptr = sp.ptr;
rc = sp.rc;
rc->increRef();
return *this;
}
~SharedPtr()
{
if (0 == rc->decreRef())
{
delete ptr;
delete rc;
}
}
T *operator->()
{
return ptr;
}
T &operator*()
{
return *ptr;
}
private:
T *ptr;
RefCnt<T> *rc;
};
int main()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
SharedPtr<int> sp3(nullptr);
sp3 = sp2;
*sp1 = 42;
std::cout << *sp2 << " " << *sp3 << std::endl;
}

shared_ptr,weak_ptr

shared_ptr: 强智能指针 可以改变资源的引用计数
weak_ptr:弱智能指针 不会改变资源的引用计数(一般用于解决强智能指针的循环引用问题,以及多线程环境下访问共享对象的线程安全问题)

shared_ptr

  • shared_ptr 用于共享资源的所有权,可以有多个 shared_ptr 指向相同的内存资源。
  • 每当有一个新的 shared_ptr 指向对象时,该对象的引用计数会增加。(通过将已有的shared_ptr拷贝或者赋值给新的shared_ptr对象)
  • 当引用计数为零时,即没有任何 shared_ptr 指向对象时,对象会被自动销毁。

weak_ptr

  • weak_ptr 可以由 shared_ptr构造而来,如std::weak_ptr<int> weak = shared;
  • weak_ptr 用于解决 shared_ptr 的循环引用问题。它不会增加引用计数,因此不会影响对象的生命周期。
  • weak_ptr 允许你观察一个 shared_ptr 指向的对象,但不拥有它。如果所有指向对象的 shared_ptr 都被销毁,weak_ptr 会自动失效,避免悬挂指针问题。
  • weak_ptr 可以通过 lock() 方法转换为一个 shared_ptr,如果被观察的对象仍然存在,就返回一个有效的 shared_ptr;如果对象已被销毁,则返回一个空的 shared_ptr

weak_ptr可以用于在多线程环境下,对不拥有所有权的资源的安全访问,通过这种弱引用,能够保证,不影响被观察的对象的生命周期,同时又能观察到该目标。
而如果用shared_ptr则会修改目标对象的生命周期。

#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
void worker(std::weak_ptr<int> weak) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一段时间后观察
if (auto shared = weak.lock()) {
std::cout << "shared_ptr is still valid. Value: " << *shared << std::endl;
} else {
std::cout << "shared_ptr has been released." << std::endl;
}
}
int main() {
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::thread observer(worker, std::weak_ptr<int>(shared));
std::this_thread::sleep_for(std::chrono::seconds(3));
shared.reset();
observer.join();
return 0;
}

weak_ptr还能用于解决shared_ptr导致的循环应用问题

#include <iostream>
#include <memory>
class B; // 前置声明
class A {
public:
A() { std::cout << "A constructor\n"; }
~A() { std::cout << "A destructor\n"; }
void setB(std::shared_ptr<B> b) { m_b = b; }
private:
std::shared_ptr<B> m_b;
};
class B {
public:
B() { std::cout << "B constructor\n"; }
~B() { std::cout << "B destructor\n"; }
void setA(std::weak_ptr<A> a) { m_a = a; }
private:
std::weak_ptr<A> m_a;
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 创建循环引用
a->setB(b);
b->setA(a);
// 释放智能指针
a.reset();
b.reset();
// 此时 A 和 B 对象会正确地被销毁,避免了循环引用导致的内存泄漏
return 0;
}

自定义删除器

使用 std::shared_ptrstd::unique_ptr 来管理动态分配的内存时,可以使用自定义的删除器(deleter)来指定如何释放资源,这在编写各种线程池、连接池、文件资源管理(使用自定义删除器来调用 fclose 关闭文件)时比较常用。

void custom_deleter(int* ptr) {
std::cout << "Custom deleter called\n";
delete ptr;
}
int main() {
int* raw_ptr = new int(42);
// 当然,除了使用函数指针,还可以使用函数对象或者lambda来实现。
std::shared_ptr<int> ptr(raw_ptr, custom_deleter);
return 0;
}
posted @   EricLing0529  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示