C++智能指针基础用法详解
文章目录
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
1、概述
C++智能指针是一类对象,它们封装了原始指针,并提供了一种机制来自动释放所指向的对象。
以下是一些关于C++智能指针的详细用法:
shared_ptr
- 这是一种引用计数智能指针,它允许多个指针拥有同一个对象。
- 当最后一个拥有该对象的
shared_ptr
被销毁时,它会自动释放所指向的内存。- 使用
std::make_shared
可以高效地创建一个shared_ptr
实例。unique_ptr
- 这是一种独占所有权的智能指针,它不允许拷贝构造,但支持移动语义 move。
- 这意味着同一时间只能有一个
unique_ptr
指向给定的资源。- 这避免了多个指针指向同一块内存的情况,减少了内存泄漏的风险。
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 判断是否为空
unique_ptr<int> ptr
;- 使用
if(ptr){}
:true:占有指针,false:不占有; - 使用
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初始化智能指针的优缺点
-
优点:
-
类型安全:
std::make_unique
在创建std::unique_ptr
实例时不需要显式指定类型,编译器可以根据赋值的对象类型自动推导出来,这减少了代码冗余,同时也避免了类型匹配错误。auto t5 = make_unique<Test>();
-
异常安全:使用
std::make_unique
比直接使用new
操作符更加安全,特别是在函数调用需要分配多个资源时。std::make_unique
确保在动态分配内存失败(可能抛出异常)时不会泄露资源,这是因为它是在表达式中即完成了资源的分配又完成了资源的绑定,符合 C++ 的 RAII(资源获取即初始化)原则。 -
简洁明了:
std::make_unique
的使用使得代码更加简洁和易于理解。它隐藏了动态内存分配的细节,让代码的意图更明显。 -
改进代码维护:由于不需要显式指定类型,当对象类型变化时,减少了需要修改的地方,从而简化了代码的维护。
-
-
缺点:
-
自定义删除器不可用:与
std::unique_ptr
直接构造函数不同的是,使用std::make_unique
时不能指定自定义删除器。如果需要自定义删除逻辑,就必须直接使用std::unique_ptr
的构造函数。unique_ptr( pointer p, /* see below */ d1 ) noexcept; // 构造函数参数2可用传入自定义删除器
-
较低版本的C++支持:
std::make_unique
是在 C++14 中引入的,这意味着在 C++11 或更早的标准中不可用。对于依然使用旧标准的代码库或项目,这可能是一个限制。 -
不适用于数组的直接初始化:虽然
std::unique_ptr
支持管理动态数组,但是std::make_unique
并不直接支持创建拥有初始值的动态数组。如果需要初始化数组,可能需要额外的步骤或者使用其他技术。 -
性能考虑:在某些非常关注性能的场合,
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 0x1b1198c1ad0:1 2
构造Test
~test
t2 reset传参 0x1b1198c1af0:2 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 0x1b1198c1b10:11 22
构造Test
swap t4 0x1b1198c5c70:-1 -2
swap t5 0x1b1198c1b10:11 22
t5 nullptr
t6 0x1b1198c1b10:11 22
构造Test
函数中创建:0x1b1198c5c90:222 123
t7 0x1b1198c5c90:222 123
智能指针引用作为函数参数:0x1b1198c5c90:222 123
引用传入:0x1b1198c5c90
智能指针作为函数参数:0x1b1198c5c90:222 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 判断是否为空
make_shared<int> ptr
;- 使用
if(ptr){}
:true:占有指针,false:不占有; - 使用
if(ptr == nullptr)
判断; - 使用
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
时,确实存在一些需要注意的问题。- 多线程环境下的问题
- 非原子操作:
use_count
方法本身不保证原子操作。这意味着在一个线程中调用use_count
获取引用计数值的同时,另一个线程可能正修改这个计数(例如,通过创建或销毁std::shared_ptr
对象)。这种情况下,use_count
返回的值可能立即就过时了。 - 竞态条件:由于
use_count
不能保证操作的原子性,在并发编程中,基于use_count
的逻辑可能导致竞态条件。例如,一个线程检查use_count
是否为1,意图判断是否只有一个shared_ptr
指向对象,基于此进行某些操作,但在检查和操作之间,另一个线程可能已经改变了实际的引用计数。 - 性能影响:虽然
std::shared_ptr
的实现通常会尽量减少多线程环境下的锁竞争,但频繁地在多线程环境下查询use_count
可能会影响性能,特别是在引用计数频繁变化的情况下。
- 非原子操作:
- 建议的用法
- 避免依赖
use_count
进行逻辑判断:在多线程程序中,尽量不要基于use_count
的返回值来进行逻辑判断或控制流程,因为这个值在多线程环境下难以保证其准确性和时效性。 - 使用线程安全的设计:如果确实需要根据共享资源的所有权情况来做逻辑处理,考虑使用更加线程安全的设计模式,比如使用互斥锁(
std::mutex
)来保护整个操作过程,或者使用原子操作来管理共享状态。 - 考虑使用
std::atomic_load
和std::atomic_store
:C++11 引入了对std::shared_ptr
的原子操作函数,如std::atomic_load
和std::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 判断引用状态
- 使用
long use_count()
函数返回管理该对象的shared_ptr
对象数量; - 使用
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