C++智能指针基础用法详解

C++智能指针基础用法详解

更多精彩内容
👉个人内容分类汇总 👈

1、概述

C++智能指针是一类对象,它们封装了原始指针,并提供了一种机制来自动释放所指向的对象。

以下是一些关于C++智能指针的详细用法:

  1. shared_ptr

    • 这是一种引用计数智能指针,它允许多个指针拥有同一个对象
    • 当最后一个拥有该对象的shared_ptr被销毁时,它会自动释放所指向的内存。
    • 使用std::make_shared可以高效地创建一个shared_ptr实例。
  2. unique_ptr

    • 这是一种独占所有权的智能指针,它不允许拷贝构造,但支持移动语义 move
    • 这意味着同一时间只能有一个unique_ptr指向给定的资源。
    • 这避免了多个指针指向同一块内存的情况,减少了内存泄漏的风险。
  3. weak_ptr

    • 它是一种与shared_ptr配合使用的智能指针,主要用于解决shared_ptr循环引用的问题。
    • weak_ptr不会增加引用计数,因此可以用来打破潜在的循环引用,防止内存无法释放。
    • 在访问所引用的对象前必须先转换为 std::shared_ptr。
    • std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。

智能指针是C++中用于管理动态分配资源的强大工具,通过正确使用智能指针,可以显著减少内存泄漏的可能性,并提高代码的安全性和可维护性。

头文件

#include <memory>

2 std::unique_ptr

到 std::shared_ptr所管理对象的弱引用

1.1 管理对象

  • 使用unique_ptr构造函数管理指针:unique_ptr<int> ptr1(new int(123));
  • 使用unique_ptr构造函数管理动态分配的数组:unique_ptr<int[]> ptr2(new int[5]);
  • 使用make_unique创建管理一个新对象的独占指针:unique_ptr<Test> t1 = make_unique<Test>(1, 2);
    • ()中为Test构造函数的参数。
    • >=C++14支持。
  • 使用make_unique创建管理动态分配的数组:unique_ptr<Test[]> t3 = make_unique<Test[]>(5);
    • ()中为数组的大小。
    • >=C++14支持。

1.2 判断是否为空

  1. unique_ptr<int> ptr;
  2. 使用if(ptr){} :true:占有指针,false:不占有;
  3. 使用if(ptr == nullptr)判断;

1.3 访问管理的指针

  • 单对象
    • 使用*解引用管理的指针;
    • 使用->调用管理指针对象的函数;
  • 数组
    • 使用ptr[i]或者ptr.get()[i]访问被管理的对象。

1.4 返回管理的裸指针

  • pointer get():返回指向被管理对象的指针,如果无被管理对象,则为 nullptr 。

1.5 修改智能指针所有权

  • T* release():返回当前管理的对象的指针(如果没有则返回nullptr),并释放所有权(ptr会为空);
  • void reset(pointer p = pointer()):重置管理的指针对象;
    • 如果不为空,则将已经管理的指针对象释放;
    • 重新管理传入的指针对象;
    • 如果传入参数为nullptr或者不传入参数则只是将ptr置空。
  • void swap(unique_ptr& other):将this和传入的other交换管理的指针对象;

1.6 make_unique初始化智能指针的优缺点

  • 优点:

    1. 类型安全std::make_unique 在创建 std::unique_ptr 实例时不需要显式指定类型,编译器可以根据赋值的对象类型自动推导出来,这减少了代码冗余,同时也避免了类型匹配错误。

      auto t5 = make_unique<Test>();
      
    2. 异常安全:使用 std::make_unique 比直接使用 new 操作符更加安全,特别是在函数调用需要分配多个资源时。std::make_unique 确保在动态分配内存失败(可能抛出异常)时不会泄露资源,这是因为它是在表达式中即完成了资源的分配又完成了资源的绑定,符合 C++ 的 RAII(资源获取即初始化)原则。

    3. 简洁明了std::make_unique 的使用使得代码更加简洁和易于理解。它隐藏了动态内存分配的细节,让代码的意图更明显。

    4. 改进代码维护:由于不需要显式指定类型,当对象类型变化时,减少了需要修改的地方,从而简化了代码的维护。

  • 缺点:

    1. 自定义删除器不可用:与 std::unique_ptr 直接构造函数不同的是,使用 std::make_unique 时不能指定自定义删除器。如果需要自定义删除逻辑,就必须直接使用 std::unique_ptr 的构造函数。

      unique_ptr( pointer p, /* see below */ d1 ) noexcept;    // 构造函数参数2可用传入自定义删除器
      
    2. 较低版本的C++支持std::make_unique 是在 C++14 中引入的,这意味着在 C++11 或更早的标准中不可用。对于依然使用旧标准的代码库或项目,这可能是一个限制。

    3. 不适用于数组的直接初始化:虽然 std::unique_ptr 支持管理动态数组,但是 std::make_unique 并不直接支持创建拥有初始值的动态数组。如果需要初始化数组,可能需要额外的步骤或者使用其他技术。

    4. 性能考虑:在某些非常关注性能的场合,std::make_unique 的便利性可能与直接使用 new 操作符相比,有轻微的性能开销。然而,这种差异通常非常小,而且安全性和代码清晰度的好处远远超过了性能的微小损失。

1.7 详细使用代码示例

#include <iostream>
#include <memory>
using namespace std;

class Test {
public:
	Test(int a = -1, int b = -2) 
	{
		m_a = a;
		m_b = b;
		cout << "构造Test" << endl;
	}
	~Test() {
		cout << "~test" << endl;
	}
	void print()
	{
		cout << this <<":"<< m_a <<" " << m_b << endl;
	}
private:
	int m_a, m_b;
};

/**
 * @brief 智能指针作为返回值
 * @return 
 */
unique_ptr<Test> fun1()
{
	unique_ptr<Test> t = make_unique<Test>(222, 123);
	cout << "函数中创建:";
	t->print();
	
	return t;
}

/**
 * @brief 智能指针作为参数传入(会将所有权转移到函数形参)
 * @param t 
 */
void fun2(unique_ptr<Test> t)
{
	cout << "智能指针作为函数参数:"; t->print();
}

/**
 * @brief 智能指针作为参数传入(以引用传入不会转移所有权)
 * @param t 
 */
void fun3(const unique_ptr<Test>& t)
{
	cout << "智能指针引用作为函数参数:"; t->print();
}

int main()
{
	unique_ptr<int> ptr1(new int(123));           // 使用构造函数创建
	cout << *ptr1 << endl;                        // 使用解引用访问
	
	unique_ptr<int[]> ptr2(new int[5]{1, 2, 3, 11, 22});           // 使用构造函数创建数组
	for(int i = 0; i < 5; i++)
	{
		cout << ptr2[i] <<" ";
	}
	cout << endl << endl;
	
	// C++14开始支持make_unique
	// 使用默认构造函数。
	unique_ptr<Test> t1 = make_unique<Test>();
	cout <<"t1 "; t1.get()->print();             // 使用get返回管理的指针
	// 使用release返回管理的指针,并释放所有权
	Test* ptrT1 = t1.release();
	cout <<"t1 "; ptrT1->print();         
	if(t1.get() == nullptr)
	{
		cout << "调用release后释放t1所有权" << endl;
	}
	
	// 使用匹配这些参数的构造函数
	unique_ptr<Test> t2 = make_unique<Test>(1, 2);
	cout <<"t2 "; t2->print();               // 使用指针方式访问
	// 使用传入的指针替换旧的指针,并释放旧指针
	t2.reset(new Test(2, 3));
	cout << "t2 reset传参 "; t2->print();
	t2.reset();      // 如果不传参数则会置空
	cout << "t2 reset不传参 " << t2.get() << endl;
	
	cout << endl;
	// 创建指向 5 个元素数组的 unique_ptr 
	unique_ptr<Test[]> t3 = make_unique<Test[]>(5);
	cout <<"t3 " << endl; 
	for(int i = 0; i < 5; i++)
	{
//		t3[i].print();
		t3.get()[i].print();        // [i] == get()[i]  数组中不是指针
	}
	cout << endl;
	
	auto t4 = make_unique<Test>(11, 22);        // 使用auto简化
	cout <<"t4 "; t4.get()->print();
	auto t5 = make_unique<Test>();              // 使用auto简化
	t5.swap(t4);                                // 交换两个指针所有权
	cout << "swap t4 "; t4->print();
	cout << "swap t5 "; t5->print();
	
	auto t6 = move(t5);                         // 使用移动赋值转移指针所有权
	if(!t5)
	{
		cout << "t5 nullptr" << endl;
	}
	cout <<"t6 "; t6.get()->print();
	
	// 作为返回值返回局部unique_ptr
	auto t7 = fun1();
	cout << "t7 "; t7->print();
	
	// 以引用传入
	fun3(t7);    
	cout << "引用传入:" << t7.get() << endl;
	// 作为函数参数传入,不能使用fun2(t7)方式调用
	fun2(move(t7));
	cout << "移动传入:" << t7.get() << endl;
	
	return 0;
}
123
1 2 3 11 22

构造Test
t1 0x1b1198c1ab0-1 -2
t1 0x1b1198c1ab0-1 -2
调用release后释放t1所有权
构造Test
t2 0x1b1198c1ad01 2
构造Test
~test
t2 reset传参 0x1b1198c1af02 3
~test
t2 reset不传参 0

构造Test
构造Test
构造Test
构造Test
构造Test
t3
0x1b1198c1ad8-1 -2
0x1b1198c1ae0-1 -2
0x1b1198c1ae8-1 -2
0x1b1198c1af0-1 -2
0x1b1198c1af8-1 -2

构造Test
t4 0x1b1198c1b1011 22
构造Test
swap t4 0x1b1198c5c70-1 -2
swap t5 0x1b1198c1b1011 22
t5 nullptr
t6 0x1b1198c1b1011 22
构造Test
函数中创建:0x1b1198c5c90222 123
t7 0x1b1198c5c90222 123
智能指针引用作为函数参数:0x1b1198c5c90222 123
引用传入:0x1b1198c5c90
智能指针作为函数参数:0x1b1198c5c90222 123
~test
移动传入:0
~test
~test
~test
~test
~test
~test
~test

3 std::shared_ptr

拥有共享对象所有权语义的智能指针

1.1 管理对象

  • 使用shared_ptr构造函数管理指针:shared_ptr<Test> t1(new Test(11, 1));
  • 使用shared_ptr构造函数管理动态分配的数组:unique_ptr<int[]> ptr2(new int[5]);
  • 使用make_shared创建管理一个新对象的共享指针:auto t7 = make_shared<Test>(123, 5);
    • ()中为Test构造函数的参数。
    • >=C++11支持。

1.2 判断是否为空

  1. make_shared<int> ptr;
  2. 使用if(ptr){} :true:占有指针,false:不占有;
  3. 使用if(ptr == nullptr)判断;
  4. 使用long use_count()函数 返回 shared_ptr 所指对象的引用计数。

1.3 访问管理的指针

  • 单对象
    • 使用*解引用管理的指针;
    • 使用->调用管理指针对象的函数;
  • 数组
    • 使用ptr[i]或者ptr.get()[i]访问被管理的对象。

1.4 返回管理的裸指针

  • pointer get():返回指向被管理对象的指针,如果无被管理对象,则为 nullptr 。

1.5 修改智能指针所有权

  • void reset(pointer p = pointer()):重置管理的指针对象;
    • 释放被管理对象的所有权;
    • p 所指向对象替换被管理对象;
  • void swap(shared_ptr& other):将this和传入的other交换管理的指针对象;

1.6 线程安全问题

  • std::shared_ptr<T>::use_count 方法返回一个 std::shared_ptr 实例管理的对象的引用计数。这个引用计数表示有多少个 std::shared_ptr 对象共享同一个被管理对象。尽管 std::shared_ptr 本身是为了提供一个安全共享资源的方法,但是在多线程环境中使用 use_count 时,确实存在一些需要注意的问题。
  • 多线程环境下的问题
    1. 非原子操作use_count 方法本身不保证原子操作。这意味着在一个线程中调用 use_count 获取引用计数值的同时,另一个线程可能正修改这个计数(例如,通过创建或销毁 std::shared_ptr 对象)。这种情况下,use_count 返回的值可能立即就过时了。
    2. 竞态条件:由于 use_count 不能保证操作的原子性,在并发编程中,基于 use_count 的逻辑可能导致竞态条件。例如,一个线程检查 use_count 是否为1,意图判断是否只有一个 shared_ptr 指向对象,基于此进行某些操作,但在检查和操作之间,另一个线程可能已经改变了实际的引用计数。
    3. 性能影响:虽然 std::shared_ptr 的实现通常会尽量减少多线程环境下的锁竞争,但频繁地在多线程环境下查询 use_count 可能会影响性能,特别是在引用计数频繁变化的情况下。
  • 建议的用法
  • 避免依赖 use_count 进行逻辑判断:在多线程程序中,尽量不要基于 use_count 的返回值来进行逻辑判断或控制流程,因为这个值在多线程环境下难以保证其准确性和时效性。
  • 使用线程安全的设计:如果确实需要根据共享资源的所有权情况来做逻辑处理,考虑使用更加线程安全的设计模式,比如使用互斥锁(std::mutex)来保护整个操作过程,或者使用原子操作来管理共享状态。
  • 考虑使用 std::atomic_loadstd::atomic_store:C++11 引入了对 std::shared_ptr 的原子操作函数,如 std::atomic_loadstd::atomic_store,它们可以在多线程环境中安全地加载和存储 std::shared_ptr 对象。虽然这些函数不直接解决 use_count 相关的问题,但它们提供了一种安全处理 std::shared_ptr 的方法。

1.7 详细使用代码示例

#include <iostream>
#include <memory>
using namespace std;

class Test {
public:
	Test()
	{
		cout << "构造:" << this << endl;
	}
	Test(int a, int id) 
	{
		m_a = a;
		m_id = id;
		cout << "构造:" << id << endl;
	}
	~Test() {
		cout << "析构:" << m_id << endl;
	}
	void print()
	{
		cout <<"---显示:" << m_a <<" "<< m_id << endl;
	}
private:
	int m_a;
	int m_id;
};

void fun1(shared_ptr<Test> t)
{
	t->print();
	cout << "函数参数传入:" << t.use_count() << endl;
}

int main()
{
	shared_ptr<Test> t1(new Test(11, 1));    // 使用new创建智能指针(可能导致内存泄漏)
	shared_ptr<Test> t2(t1);                 // 使用shared_ptr初始化另一个shared_ptr
	shared_ptr<Test> t3 = t2;                // 使用复制构造初始化
	shared_ptr<Test> t4;                     // 未管理对象的智能指针
	cout << "未管理:" << t4.use_count() <<" " << t4 << endl;
	
	t1->print();
	t2->print();
	t3->print();
	cout << "引用计数:" << t3.use_count() << endl << endl;
	
	/**
	 * 1.使用新的指针替换已有的指针,如果已有指针计数为1,则替换后会被释放;
	 * 2.注意:方法本身不保证原子操作。这意味着在一个线程中调用 use_count 获取引用计数值的同时,另一个线程可能正修改这个计数
	 */
	shared_ptr<Test> t5(new Test(11, 2));
	t5.reset(new Test(11, 3));               
	cout << "引用计数:" << t5.use_count() << endl << endl;  
	
	// 交换两个指针
	shared_ptr<Test> t6(new Test(11, 4));
	t6.swap(t5);
	cout << "t6交换后内容:"; 
	t6->print();
	cout << endl;
	
	/**
	 * 1.使用make_shared创建智能指针;
	 * 2.使用auto 简化代码;
	 * 3.使用get()返回存储的指针;
	 * 4.使用 * 解引用存储的指针。
	 */
	auto t7 = make_shared<Test>(123, 5);
	t7.get()->print();
	(*t7).print();
	cout << "引用计数:" << t7.use_count() << endl << endl;  
	
	fun1(t7);
	cout << "函数返回后引用计数:" << t7.use_count() << endl << endl;  
	

	shared_ptr<Test[]> t8(new Test[5]);           // 使用构造函数创建数组
	t8[0].print();
	cout << endl;
//	shared_ptr<int[]> t9 = make_shared<int[]>(3); // 不支持
	
	return 0;
}
构造:1
未管理:0 0
---显示:11 1
---显示:11 1
---显示:11 1
引用计数:3

构造:2
构造:3
析构:2
引用计数:1

构造:4
t6交换后内容:---显示:11 3

构造:5
---显示:123 5
---显示:123 5
引用计数:1

---显示:123 5
函数参数传入:2
函数返回后引用计数:1

构造:0x1d926a45c98
构造:0x1d926a45ca0
构造:0x1d926a45ca8
构造:0x1d926a45cb0
构造:0x1d926a45cb8
---显示:648282448 473

析构:0
析构:0
析构:0
析构:0
析构:473
析构:5
析构:3
析构:4
析构:1

3 std::weak_ptr

到 std::shared_ptr所管理对象的弱引用

1.1 创建引用

  • 使用weak_ptr构造函数创建shared_ptr引用:

    auto t2 = make_shared<Test>(123, 1);
    weak_ptr<Test> t4(t2);     // 使用构造函数(不增加引用计数)
    
  • 使用weak_ptr赋值构造函数创建shared_ptr引用:

    auto t2 = make_shared<Test>(123, 1);
    t1 = t2;       // 使用复制构造(不增加引用计数)
    

1.2 判断引用状态

  1. 使用long use_count() 函数返回管理该对象的 shared_ptr 对象数量;
  2. 使用bool expired()函数检查被引用的对象是否已删除;

1.3 访问管理的指针

  • weak_ptr需要访问管理的指针需要先使用std::shared_ptr<T> lock()函数转换为 std::shared_ptr。

1.7 详细使用代码示例

#include <iostream>
#include<memory>
using namespace std;


class Test {
public:
	Test()
	{
		cout << "构造:" << this << endl;
	}
	Test(int a, int id) 
	{
		m_a = a;
		m_id = id;
		cout << "构造:" << id << endl;
	}
	~Test() {
		cout << "析构:" << m_id << endl;
	}
	void print()
	{
		cout <<"---显示:" << m_a <<" "<< m_id << endl;
	}
private:
	int m_a;
	int m_id;
};


int main()
{
	weak_ptr<Test> t1;
	{
		auto t2 = make_shared<Test>(123, 1);
		cout << "1 t2引用:" << t2.use_count() << endl;
		t1 = t2;       // 使用复制构造(不增加引用计数)
		
		auto t3 = t1.lock();       // 创建管理被引用的对象的shared_ptr(增加引用计数)
		
		weak_ptr<Test> t4(t2);     // 使用构造函数(不增加引用计数)
		cout << "1 t4引用:" << t3.use_count()<< endl;
		
		cout << "2 t2引用:" << t2.use_count()<< endl;
		cout  << std::boolalpha << "1 t1引用:" << t1.use_count() <<",是否被释放:" << t1.expired()<< endl;
	}
	// 离开作用域后t2被释放
	cout  << std::boolalpha << "2 t1引用:" << t1.use_count() <<",是否被释放:" << t1.expired()<< endl;
	cout << endl;
	
	auto t5 = make_shared<Test>(123, 2);
	weak_ptr<Test> t6(t5);
	cout  << std::boolalpha << "1 t6引用:" << t6.use_count() <<",是否被释放:" << t6.expired()<< endl;
	t6.reset();       // 释放被管理对象的所有权。
	cout  << std::boolalpha << "2 t6引用:" << t6.use_count() <<",是否被释放:" << t6.expired()<< endl;
	cout << "t5引用计数:" << t5.use_count() << endl;
	cout << endl;
	
	/**
	 * 1.使用swap交换管理的指针;
	 * 2.weak_ptr要访问管理的对象,需要先使用lock转换为shared_ptr指针;
	 */
	weak_ptr<Test> t7(t5);
	auto t8 = make_shared<Test>(123, 3);
	weak_ptr<Test> t9(t8);
	t9.lock()->print();
	t9.swap(t7);
	t9.lock()->print();
	cout << endl;
	
	
	return 0;
}
构造:1
1 t2引用:1
1 t4引用:2
2 t2引用:2
1 t1引用:2,是否被释放:false
析构:1
2 t1引用:0,是否被释放:true

构造:2
1 t6引用:2,是否被释放:false
2 t6引用:0,是否被释放:true
t5引用计数:2

构造:3

析构:3
析构:2
  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

mahuifa

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值