C++ 手撕shared_ptr

发布于:2025-05-17 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.成员变量

shared_ptr有两个成员变量 1.指向管理的资源T*ptr 2.引用计数 因为指向同一块资源的指针共享同一个计数器 所以用*对同一块空间++ -- (如果用static静态成员变量 那整个类共享计数器 也就是说管理不同对象用的是同一个计数器)

因为涉及到对计数器++ -- 但操作并不是原子性的,可以用mutex互斥锁进行同步,也可以用原子变量atomic<>让计数器变为原子变量 通过fetch_add(1) fetch_sub(1)进行原子操作

2.release() 引用计数--

但shared_ptr不指向 对象时 ,计数器-- 如果为0了就析构资源。

fetch_sub(1)进行-1操作 并返回减之前的值 所以判断是否==1

如果减之前值==1 就要delete析构资源

注意:要先判断ref_count是否存在再进行 操作 

3.构造函数

1.默认构造

2.显示构造

传入指向对象的指针 

如果p指向空 计数器也为空 ,存在就创建计数器并置为1

注意一定要加 explicit 禁止隐式类型转换。

如果不加explicit ,构造函数就允许隐式转换 

eg.

void foo(shared_ptr<int> p) {
    std::cout << *p << std::endl;
}

int main() {
    foo(new int(42));  // ❗合法!但你可能没意识你创建了一个 shared_ptr
}

可能会在传参、赋值时无意识地创建智能指针对象,导致资源提前释放或重复管理。

3.拷贝构造

和传入的shared_ptr指针一样 指向同一个对象  计数器++ fetch_add(1)

记得带上模板参数shared_ptr<T>

4.移动构造

不再由other指向对象 而由新的shared指针指向 -- ++正好抵消。

注意:

1.一定要把other的对象置为空,不能让它再指向资源。

在 shared_ptr 实现中,如果移动构造/赋值后没有置空原对象,就可能导致其析构时再次释放已经被转移的资源,从而发生严重错误。正确做法是在资源转移后清空原对象指针,防止二次释放。

2.移动构造/移动赋值 加上noexcept表示不会抛异常

如果你不给移动构造加 noexcept,STL 不会使用 move,因为一旦抛异常就会导致崩溃。

只有在移动构造是 noexcept 时,容器在执行元素移动、扩容时,才敢放心地使用移动语义来搬移对象;

所以,加了 noexcept = 让容器用移动替换拷贝,提升性能。

一般移动构造函数、移动赋值函数和析构函数只是做资源的转移或释放工作,不涉及分配内存或抛出异常的逻辑。所以会加上noexcept表示不会抛异常。

4.赋值函数

1.拷贝赋值

放弃指向当前资源release() 指向别的资源fetch_add(1)。

要保证不是自己给自己赋值 如果是就返回自己就行了

注意:

1.返回值shared_ptr<T>& 采用引用的形式。为什么加&?

        1.防止产生中间变量 造成不必要的构造和析构

        2.支持a=b=c 链式表达式 =>自右往左赋值 1.先b=c 2.再a=(b=c的返回值) a直接用返回值进行operator= 完成赋值操作 就不用再创建临时对象传入 

2.判断是否为自己时 不要写成*this!=other 因为我shared类中并没有写operator!=的重载

        建议直接this!=&other

2.移动赋值

1.noexcept

2.other指向nullptr

5.析构函数

析构 计数器--

5.*解引用 ->运算符

对shared_ptr对象* 就是要该资源对象 所以返回T&

-> 访问该资源对象的成员变量 要返回该对象的地址T*

6.get()获取裸指针

7.获取引用计数

先判断计数器是否存在 load()获取计数器值

8.operator bool 类型转换

shared_ptr类型可以类型转换为bool 判断是否存在

9.重置

放弃管理当前资源 指向新资源

总结:

1.显示构造函数一定要explicit 禁止隐式类型转换,避免用户无意中传入原始指针而生成 shared_ptr

2.移动构造/赋值 加noexcept表示不会抛异常 否则STL在扩容、搬移元素时不会选择移动语义而是拷贝 导致效率降低

3.赋值函数 要先判断是否为自身 this!=&other