【重学C++】【指针】C++智能指针不止shared_ptr,还有weak_ptr和unique_ptr

大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。

重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。


智能指针是C++中的一种高级内存管理工具,用来提高程序的安全性和可维护性。它可以进行自动化内存管理,可以在一定程度上减少内存泄漏和悬挂指针等问题。C++11中的智能指针不只有shared_ptr,还有与之配套的weak_ptr,以及独占所有权的unique_ptr等,本文我们来一起学习下C++11中的智能指针。

1. std::shared_ptr

允许多个指针共享同一个对象,内部使用引用计数来跟踪对象的引用次数。当引用计数为0时,内存会被释放。

1.1 基本使用

写以下测试程序:

#include <iostream>
#include <string>
#include <memory>

class X1 {
public:
    int* i_p;
    X1(int a) {
        i_p = new int(a);
        std::printf("call x1 constructor\n----------------------\n");
    }
    void print()
    {
        std::printf("int value is %d, this: %p\n", *i_p, this);
    }
};

int main(int argc, char *argv[])
{
    std::shared_ptr<X1> x1_1 = std::make_shared<X1>(10);
    x1_1->print();
    std::printf("1 use count: %d\n", x1_1.use_count());

    std::shared_ptr<X1> x1_2 = x1_1;
    x1_2->print();
    std::printf("2 use count: %d\n", x1_2.use_count());

    x1_1.reset();
    std::printf("x1 after reset: %p\n", x1_1.get());
    std::printf("3 use count: %d\n", x1_2.use_count());

    x1_2->print();

    return 0;
}

执行后输出结果如下:

在这里插入图片描述

1.2 代码解释

(1)x1_1为智能指针,因为现在只有它自己指向X1的实例,因此其 use_count 为1

(2)x1_2为智能指针,与x1_1相等,即x1_2x1_1指向相同的位置,之后该实例的引用计数 use_count 变为了2,同时,根据打印也可以看到,其打印的 this 地址与 x1_1 相同,指向相同位置。

(3)x1_1.reset() 表示清除该智能指针,清除之后x1_1为空了,原来实例的引用计数 use_count 也变为了1。

(4)因为引用计数还不是0,因此这块的内存空间还是在的,原来的实例还是在的,因此还是能够使用 x1_2 打印出这个实例的数据和地址。

(5)注意智能指针在打印输出时,使用的是 get 方法,get方法是将智能指针转换成裸指针的方法:x1_1.get()

1.3 线程安全性

指针和引用计数是线程安全的(引用计数的加减是原子的),但指针所指对象中的操作就需要自己做控制,并不是线程安全的。

1.4 补充知识 - 使用注意事项

1.4.1 reset函数

  • reset()不带参数时,若该智能指针是唯一指向该对象的指针,则释放,并置空。若智能指针不是唯一指向该对象的指针,则引用计数减一,同时将自身置空。

  • reset()带参数时,若智能指针是唯一指向该对象的指针,则释放并指向新的对象。若智能指针不是唯一指向该对象的指针,则引用计数减一,并指向新的对象

std::shared_ptr<int> ptr = make_shared<int>(10);
ptr.reset(new int(20));

1.4.2 优先使用make_shared来构造智能指针,因为它更高效。

优先使用make_shared来构造智能指针,因为它更高效。

1.4.3 慎用 get 方法得到的裸指针

(1)不要delete get到的这个裸指针,否则会导致2次delete

(2)get 到的裸指针,不会增加或减少原智能指针的引用计数,是完全独立的,但是由于都对应一块内存,所以对内存的操作会很难控制。所以,一般不要使用get到的原始指针操作内存。

1.4.4 不要用一个原始指针初始化多个shared_ptr

int *ptr=new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);//逻辑错误

1.4.5 通过shared_from_this返回this指针

不要将this指针作为shared_ptr返回回来,因为this指针本质上是一个裸指针,如下代码错误:

#include <iostream>
#include <memory>

using namespace std;

class MyClass
{
public:
	shared_ptr<MyClass> GetSelf() {
		return shared_ptr<MyClass>(this);//不要这样做
	}
	MyClass() {
		cout << "MyClass()" << endl;
	};
	~MyClass() {
		cout << "~MyClass()" << endl;
	};


};

int main()
{
	shared_ptr<MyClass> sp1(new MyClass);
	shared_ptr<MyClass> sp2 = sp1->GetSelf();
	return 0;
}

运行后结果:

MyClass()
~MyClass()
~MyClass()
free(): double free detected in tcache 2

因为这个 sp1 和 sp2之间没有任何关系,会各自析构,从而导致二次释放。应该改成下面这样,继承std::enable_shared_from_this类,然后使用shared_from_this()返回thisshared_ptr

#include <iostream>
#include <memory>

using namespace std;

class MyClass: public std::enable_shared_from_this<MyClass>
{
public:
	shared_ptr<MyClass> GetSelf() {
		return shared_from_this();//不要这样做
	}
	MyClass() {
		cout << "MyClass()" << endl;
	};
	~MyClass() {
		cout << "~MyClass()" << endl;
	};


};

int main()
{
	shared_ptr<MyClass> sp1(new MyClass);
	shared_ptr<MyClass> sp2 = sp1->GetSelf();
	return 0;
}

2. std::unique_ptr

独占所有权,确保在其生命周期内只有一个指针可以指向该对象。不能进行拷贝构造或赋值操作,但可以进行移动操作。

2.1 基本使用

在上一段代码基础上,我们增加以下测试代码:

// 创建一个 std::unique_ptr,它将独占所有权一个 X1 对象
std::unique_ptr<X1> x1_3 = std::make_unique<X1>(10);
//    std::unique_ptr<X1> x1_3(new X1(10)); // ok 也可以这样定义
x1_3->print();

// 尝试创建另一个指向同一对象的 std::unique_ptr 会导致编译错误
//     std::unique_ptr<X1> x1_4 = x1_3; // 错误!不能进行拷贝构造或赋值操作
std::unique_ptr<X1> x1_4 = std::move(x1_3);
// 现在 x1_3 不再指向任何对象
if (!x1_3) {
    std::cout << "x1_3 is now empty." << std::endl;
}
x1_4->print();

// 使用 release() 方法释放指针所有权,并将其指向的对象的指针返回
X1* rawPtr = x1_4.release();
// 现在 x1_4 不再指向任何对象
if (!x1_4) {
    std::cout << "x1_4 is now empty." << std::endl;
}

// 在不再需要时手动释放内存
delete rawPtr;

输出结果如下:

在这里插入图片描述

2.2 代码解释

(1)刚开始创建了一个 x1_3 作为 unique_ptr 指向X1的一个实例,后面无法通过赋值或拷贝构造来定义 x1_4,这就是 unique_ptr 的独占性。报错如下:

在这里插入图片描述

(2)注意下 unique_ptr 的初始化方式,从C++14开始,unique_ptr 可以使用方法 std::make_unique 来初始化

(3)unique_ptr 无法通过赋值和复制的方式构造,但可以通过 std::move 来构造。因为 std::move 是将内存的所有权转移,还是能保证这块内存只有一个指针在访问。通过输出可以看到,在std::move之后,x1_3为空了,x1_4拿走了所有权。

(4)unique_ptr的释放通过 release() 方法来实现。注意:这里的释放不是指释放内存,而是释放独占属性,表示这块区域可以由其它指针来使用了。release 方法返回的是这块内存区域的裸指针:X1* rawPtr = x1_4.release()。释放之后,x1_4 就为空了。

(5)注意最后需要手动释放这块内存区域:delete rawPtr;

3. std::weak_ptr

std::shared_ptr的观察者,不会增加引用计数。主要用于解决std::shared_ptr的循环引用问题,提供一种非拥有的观察方式。

3.1 基本使用

再写下面的测试代码:

std::shared_ptr<X1> x1_1 = std::make_shared<X1>(10);
x1_1->print();
std::printf("1 use count: %d\n", x1_1.use_count());

std::weak_ptr<X1> x1_weak = x1_1;
std::printf("2 use count: %d\n", x1_1.use_count());
std::printf("3 use count: %d\n", x1_weak.use_count());

// 检查 x1_weak 是否指向有效的对象
if (auto ptr = x1_weak.lock()) {
    ptr->print();
} else {
    std::cout << "Weak pointer is expired." << std::endl;
}

// 销毁 std::shared_ptr,释放资源
x1_1.reset();

// 再次检查 weakPtr 是否指向有效的对象
if (auto ptr = x1_weak.lock()) {
    ptr->print();
} else {
    std::cout << "Weak pointer is expired." << std::endl;
}

运行结果如下:

在这里插入图片描述

3.2 代码解释

(1)根据引用计数的打印,weak_ptr 是不会新增引用计数的。

(2)weak_ptr 在使用时,需要 x1_weak.lock() 之后才能访问内存数据。lock 方法是将 weak_ptr 转化成 shared_ptr。为了验证这一点,可以在 lock 后再打印 x1_weak的引用计数,你会看到引用计数增加了:

std::printf("4 use count: %d\n", x1_weak.use_count()); // 引用计数为 2

3.3 解决shared_ptr的循环引用导致内存泄露问题

本文中我们一直在说 shared_ptr 有循环引用问题,weak_ptr 是为了解决 shared_ptr 的循环引用而设计的。

3.3.1 shared_ptr 循环引用导致内存泄露

看下面的示例代码:

#include <iostream>
#include <memory>
 
class A;
class B;
 
class A {
public:
    std::shared_ptr<B> ptr_b;
    ~A() { std::cout << "A is destroyed." << std::endl; }
};
 
class B {
public:
    std::shared_ptr<A> ptr_a;
    ~B() { std::cout << "B is destroyed." << std::endl; }
};
 
int main() {
    std::shared_ptr<A> ptr_a = std::make_shared<A>();
    std::shared_ptr<B> ptr_b = std::make_shared<B>();
 
    ptr_a->ptr_b = ptr_b;
    ptr_b->ptr_a = ptr_a;
 
    // 当ptr_a和ptr_b离开作用域时,它们的引用计数将变为1,而不是0
    // 因此,它们的析构函数不会被调用,发生内存泄漏
 
    return 0;
}

代码中,ptr_a 中有 ptr_bptr_b 中有 ptr_a,这就是循环引用,这时候在 return 0 之前,ptr_aptr_b 的引用计数都是2,而离开二者的作用域后,循环计数只会下降到1,不会到0,所以导致 ptr_aptr_b 的内存一直无法被自动释放,从而会造成内存泄露的问题。

3.3.2 weak_ptr 解决 shared_ptr 循环引用的问题

将其中一个shared_ptr修改成weak_ptr

class B {
public:
    std::weak_ptr<A> ptr_a;  // 使用 std::weak_ptr
    ~B() { std::cout << "B is destroyed." << std::endl; }
};

这样修改后,再执行下面的main函数:

int main() {
    std::shared_ptr<A> ptr_a = std::make_shared<A>();
    std::shared_ptr<B> ptr_b = std::make_shared<B>();

    ptr_a->ptr_b = ptr_b;
    ptr_b->ptr_a = ptr_a;  // 使用 std::weak_ptr

    std::printf("ptr_a 的引用计数: %d\n", ptr_a.use_count());
    std::printf("ptr_b 的引用计数:%d\n", ptr_b.use_count());

    return 0;
}

输出如下:

在这里插入图片描述

可以看到循环引用已经没有了,ptr_aptr_b都被释放了。原因是:weak_ptr不会增加引用计数,因此ptr_a的引用计数为1,离开作用域后引用计数变为0,调用析构函数销毁。销毁后,其引用的ptr_b引用计数减一,变为1,而ptr_b离开作用域引用计数再减一,变为0,所以ptr_b也被销毁。

4. std::auto_ptr(C++98已废弃)

独占所有权,不能被复制,但可以被移动。已被std::unique_ptr取代。

这个ptr已经不用了,所以在这里就不介绍了,需要的可以自己去查查。

5. 总结

本文介绍了三种智能指针:unique_ptrshared_ptrweak_ptr。 每种智能指针都有各自的使用场景:

(1)unique_ptr独占对象的所有权,由于没有引用计数,性能较好。
(2)share_ptr共享对象的所有权,但性能略差。
(3)weak_ptr配合share_ptr,解决循环引用问题。

同时,本文也对智能指针的一些使用注意事项做了总结,以帮助大家规避一些使用中常见的坑。

6. 参考

  • https://mp.weixin.qq.com/s/EQpghtU8IfV8CN4o0t8-9g
  • https://mp.weixin.qq.com/s/9kqUjR2dF98eysk-9pG4mQ

提醒一句:一定要动手去实践一下!没有任何一篇文章看了之后就能彻底搞懂指针,必须亲身体验才能加深印象!

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~


  • 大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例
  • 欢迎 点赞 + 关注 👏,持续学习持续干货输出
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜同学小张 🙏

本站文章一览:

在这里插入图片描述

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++中,`unique_ptr`、`shared_ptr`、`weak_ptr`是三种常用的智能指针,用于管理动态分配的内存,避免内存泄漏和悬空指针等问题。 1. `unique_ptr`:是一种独占式智能指针,表示一个对象的所有权只能被一个`unique_ptr`持有,不能被其他指针或引用所共享。当`unique_ptr`超出作用域或被显式释放时,它所指向的对象将被自动销毁。例如: ```cpp std::unique_ptr<int> p(new int(10)); ``` 2. `shared_ptr`:是一种共享智能指针,表示一个对象的所有权可以被多个`shared_ptr`共享。每个`shared_ptr`维护一个引用计数器,当引用计数器变为0时,它所指向的对象将被自动销毁。`shared_ptr`还支持自定义删除器,可以在对象销毁时执行特定的操作。例如: ```cpp std::shared_ptr<int> p = std::make_shared<int>(10); std::shared_ptr<int> q = p; ``` 3. `weak_ptr`:是一种弱引用智能指针,不能直接访问所指向的对象,只能通过调用`lock()`方法获得一个指向所指对象的`shared_ptr`。当`weak_ptr`所指向的对象已经被销毁时,`lock()`方法将返回一个空的`shared_ptr`。`weak_ptr`主要用于解决`shared_ptr`的循环引用问题,避免内存泄漏。例如: ```cpp std::shared_ptr<int> p = std::make_shared<int>(10); std::weak_ptr<int> q = p; std::shared_ptr<int> r = q.lock(); ``` 这些智能指针都定义在`<memory>`头文件中,并且都是模板类,可以用于管理各种类型的动态分配内存。在实际开发中,应尽量使用智能指针来管理内存,避免手动管理内存所带来的麻烦和风险。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

同学小张

如果觉得有帮助,欢迎给我鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值