外贸网站 中英,wordpress 左侧菜单栏,老王传奇新开网站,企业邮箱在哪里查看目录
前言
一、shared_ptr
1. 基本用法和构造方法
2. 引用计数机制
3. weak_ptr 解决循环引用
二、unique_ptr
1. 基本用法和构造方法
2. 独占性
3. 所有权转移
1#xff09;unique_ptr :: release()
2#xff09;移动语义 和 move()
三、 对比 shared_ptr 和 un…目录
前言
一、shared_ptr
1. 基本用法和构造方法
2. 引用计数机制
3. weak_ptr 解决循环引用
二、unique_ptr
1. 基本用法和构造方法
2. 独占性
3. 所有权转移
1unique_ptr :: release()
2移动语义 和 move()
三、 对比 shared_ptr 和 unique_ptr
unique_ptr:
shared_ptr:
自定义删除器
默认删除器
总结 前言 智能指针的主要目的是自动管理内存分配和释放以减少程序员错误和减轻程序员的负担。它是通过一个特殊的类封装了一个指针并添加一些额外的语义来实现的。使用智能指针的好处在于它可以帮助程序员避免常见的内存错误如内存泄漏和使用已被释放的内存。智能指针还可以使代码更加清晰和易于理解因为它们使得内存分配和所有权转移变得显式和清晰。现我们将从概念和实际场景对共享指针和独占指针进行理解和使用掌握如何高效的使用智能指针来便捷整个项目的编码操作。
一、shared_ptr
1. 基本用法和构造方法
对其而言有两种常用的构造方法具体使用方法和普通指针类似下面给出示例
构造方法
// 1.创建一个 shared_ptr指向 int 类型的对象
std::shared_ptrint sharedInt std::make_sharedint(42);// 2.创建一个 shared_ptr指向一个动态分配的对象
std::shared_ptrdouble sharedDouble(new double(3.14));
访问指针使用
// 1.视作普通指针进行访问
*sharedInt 10;
std::cout Value of sharedInt: *sharedInt std::endl;
// 2.调用get()方法取到普通指针再访问
*sharedInt.get() 20;
std::cout Value of sharedInt: *sharedInt.get() std::endl;
通过上面的代码示例我们了解到 shared_ptr 的常用构造方法有两种
1使用 make_shared()是推荐的创建 shared_ptr 的方法它在单次分配中同时创建对象和控制块效率更高。
2直接使用 “new” 出的对象初始化可以直接使用 new 关键字创建动态分配的对象并将其传递给 shared_ptr 构造函数。但这样做可能导致性能损失因为需要额外的内存用于控制块。
对指针的访问方法明明可以直接对智能指针变量进行访问那 get() 方法存在的必要性是什么
当我们需要将指针传给接受普通指针的函数或功能模块时智能指针是不能被函数接收后自动隐式类型转换为所需类型的所以 get() 方法很好地解决了这个问题下面给出示例代码
print_int(sharedInt); // 编译报错
print_int(sharedInt.get()); 2. 引用计数机制
shared_ptr 特性 提供共享所有权的智能指针。使用引用计数来追踪资源的所有者数量。当最后一个 shared_ptr 指向资源销毁时资源被释放。 至于共享所有权为了便于对比理解本文后面再阐述这里先来理解引用计数的概念
引用计数
share_ptr 使用一个控制块control block来管理引用计数和其他信息。控制块是在内存中动态分配的包含引用计数和指向实际对象的指针。当创建新的 shared_ptr 时会为对象分配一个新的控制块并将引用计数初始化为 1。当共享指针被复制或赋值时引用计数递增。当共享指针被销毁超出作用域时引用计数递减。当引用计数变为零时说明没有任何指针指向该对象因此对象和控制块的内存都会被释放。
shared_ptr :: use_count() shared_ptr :: reset() 为了便于理解下面利用 shared_ptr 内置的 use_count() 方法使用代码和运行结果帮助感受引用计数变化的过程
// 首次创建一个 shared_ptr引用计数初始为 1
std::shared_ptrint sp1 std::make_sharedint(42);
// 打印引用计数
cout sp1.use_count() endl;// 创建另一个 shared_ptr引用计数增加为 2
std::shared_ptrint sp2 sp1;
cout sp2.use_count() endl;// 创建另一个 shared_ptr引用计数增加为 3
std::shared_ptrint sp3 sp2;
cout sp3.use_count() endl;// 销毁一个 shared_ptr 对象引用计数减 1变为 2
sp2.reset();
cout sp3.use_count() endl;
运行结果 要注意的是使用引用来引用智能指针变量并不会引起变量引用计数的变化
// 为了避免代码冗杂下面代码直接续在前面代码后不再重复给出
auto ref_sp sp1;
cout ref_sp.use_count() endl; 运行结果 ref_sp 仅仅是智能指针 sp1 的一个引用它们共享相同的引用计数因此不会增加引用计数。这只是一个别名没有创建新的智能指针对象。
3. weak_ptr 解决循环引用
weak_ptr 主要用于解决 shared_ptr 的循环引用问题。循环引用可能导致对象无法正常释放因为 shared_ptr 的引用计数永远不会变为零。通过使用 weak_ptr可以打破循环引用允许对象在不再被引用时正常释放。
具体什么是循环引用下面给出简单示例
class ObjectB; // 提前声明class ObjectA {
public:std::shared_ptrObjectB objectB; // 注意这里是 shared_ptrObjectA() {std::cout ObjectA constructed std::endl;}~ObjectA() {std::cout ObjectA destructed std::endl;}
};class ObjectB {
public:std::weak_ptrObjectA objectA; // 注意这里是 weak_ptrObjectB() {std::cout ObjectB constructed std::endl;}~ObjectB() {std::cout ObjectB destructed std::endl;}
};
考虑两个对象相互引用的情况其中 ObjectA 持有 shared_ptrObjectB而 ObjectB 持有 shared_ptrObjectA。这样的循环引用会导致对象永远无法释放。在此案例中只需要将两者其中之一改为 weak_ptr 即可解决他俩相互推脱都不释放的循环引用造成的问题。
测试用例
// 创建 shared_ptr 和 weak_ptr
std::shared_ptrObjectA sharedA std::make_sharedObjectA();
std::shared_ptrObjectB sharedB std::make_sharedObjectB();// 建立关联
sharedA-objectB sharedB;
sharedB-objectA sharedA;
运行结果 当我们正常创建并初始化上面两类对象时通过观察控制台窗口打印的构造和析构函数内容发现 weak_ptr 成功使得两对象被系统释放并调用各自的析构函数。
weak_ptr :: lock() 由于 lock() 函数具有返回其保留的对象的能力所以我们可以利用其特性在以 weak_ptr 为类成员的类中自建一个功能用于判断该 weak_ptr 包含的对象即 shared_ptr 是否存在有没有被释放掉不妨我们将其命名为 is_exist() 将其作为类成员函数方便我们利用类对象调用从而了解以 weak_ptr 为类型的成员变量包含的对象是否被释放。
class ObjectB {
public:std::weak_ptrObjectA objectA;// 构造函数和析构函数bool is_exist() // 提供访问方法{// 使用 lock() 获取 shared_ptrif (auto sharedPtr objectA.lock()) {// 对象存在可以安全地使用 sharedPtrstd::cout Object exist! std::endl;return true;}else {// 对象已经被销毁std::cout Object has been released std::endl;return false;}}
};
调用测试 is_exist() 功能
void test3()
{// 创建 shared_ptr 和 weak_ptrstd::shared_ptrObjectA sharedA std::make_sharedObjectA();std::shared_ptrObjectB sharedB std::make_sharedObjectB();// 建立关联sharedA-objectB sharedB;sharedB-objectA sharedA;sharedB-is_exist(); // 测试B类中的 weak_ptr// 1.reset作用于sharedBsharedB.reset(); // 销毁sharedB 使其恢复刚构造完成的状态// 在使用 sharedB 之前检查是否为空if (sharedB) {sharedB-is_exist();}else {std::cout sharedB is null. std::endl;}
}
运行结果 接着我们尝试销毁ObjectB类型对象内部的 weak_ptr 指针包含的对象
void test3()
{// 创建 shared_ptr 和 weak_ptrstd::shared_ptrObjectA sharedA std::make_sharedObjectA();std::shared_ptrObjectB sharedB std::make_sharedObjectB();// 建立关联sharedA-objectB sharedB;sharedB-objectA sharedA;sharedB-is_exist(); // reset()销毁前作对比用// 2.reset作用于ObjectB类型对象内部的weak_ptr指针包含的对象sharedB-objectA.reset();sharedB-is_exist();
}
运行结果 我们发现实现的 is_exist() 功能成功反映了 weak_ptr 对象是否被销毁的状态以便我们进一步安全操作由于 reset() 后指针变为悬空指针通过检测避免出现访问悬空指针的情况。
二、unique_ptr
1. 基本用法和构造方法
std::unique_ptr 是 C11 引入的智能指针用于管理动态分配的对象它独占unique ownership所指向的对象。与 shared_ptr 不同std::unique_ptr 不使用引用计数因此每个 std::unique_ptr 拥有对对象的唯一所有权。这意味着当 std::unique_ptr 被销毁或通过 std::move 转移所有权时它所管理的对象会被销毁。
构造方法
比如需要创建一个已有类的智能指针下面给出示例类的声明
class MyClass {
public:MyClass() {std::cout MyClass constructed std::endl;}~MyClass() {std::cout MyClass destructed std::endl;}void DoSomething() {std::cout Doing something... std::endl;}
}; 具体构造语句
// 创建 unique_ptr
std::unique_ptrMyClass uniquePtr1 std::make_uniqueMyClass();
std::unique_ptrMyClass uniquePtr2(new MyClass);
使用方法
// 使用 unique_ptr
uniquePtr1-DoSomething();
uniquePtr1.get()-DoSomething();
uniquePtr2-DoSomething();
uniquePtr2.get()-DoSomething();
运行结果 对于 unique_ptr 而言与 shared_ptr 一致的是存在 get() 方法可以取到普通指针。不同的是没有引用计数独占所拥有的对象那独占性又是如何体现呢
2. 独占性
std::unique_ptr 是一种独占所有权的智能指针它确保一个对象只能由一个 std::unique_ptr 拥有。这意味着任何时候只有一个 std::unique_ptr 指向一个特定的动态分配对象。这是通过禁止复制构造函数和赋值运算符来实现的因为这些操作会导致多个 std::unique_ptr 指向同一个对象破坏了独占性。 注意这里的独占性是仅仅允许一对一的情况存在不可多对一和一对多 不妨我们下面采用各种方式来验证其独占性的规则
// 创建 unique_ptr
std::unique_ptrMyClass uniquePtr1 std::make_uniqueMyClass();// 1. 尝试复制构造
// std::unique_ptrMyClass uniquePtr2 uniquePtr1; // 编译错误// 2. 尝试赋值运算符
// std::unique_ptrMyClass uniquePtr3;
// uniquePtr3 uniquePtr1; // 编译错误
具体编译报错形式注意编译器检查标红 另外我们还有通过其他方法验证独占性由于部分概念还未阐述具体操作会在后面给出。
3. 所有权转移 出于编程过程中实际需要我们定义的 unique_ptr 不得不在其他地方被延续使用下去但是出于一对一的独占性要求unique_ptr 是不可复制的包括上面验证的复制构造函数、赋值运算符。此时应运而生的就是 “所有权转移” 既然不能复制那就将自身拥有的对象转移给别的对象进行管理。
下面给出案例便于理解 “所有权转移” 的使用方法和存在意义
1unique_ptr :: release() 现在假定我们需要使用该指针作为函数参数实现功能而接受的参数类型为C语音风格的普通指针这时候就有两种选择 1利用unique_ptr :: get() 2利用unique_ptr :: release() 通过上图我们了解到 release() 的本质是转移其拥有指针的所有权以返回值传递权限通过接受该函数返回值实现所有权接收要注意的是 release() 后unique_ptr 会自动置空。这里需要区分一个概念 release() 仅仅是将 unique_ptr 对象的指针置为 nullptr并不会影响已经转移的资源或对象
这意味着上面两方法的本质区别就在于 release() 该行以后的代码都应避免访问和使用 release() 后的 unique_ptr 指针因为 release() 后的指针此时变为悬空指针再次访问会报错或产生未定义行为。
为了便于反映问题我们对 MyClass 类稍作修改使其具有一个成员变量 n 类声明如下
class MyClass {
public:MyClass() {std::cout MyClass constructed std::endl;}~MyClass() {std::cout MyClass destructed std::endl;}void DoSomething() {std::cout Doing something... std::endl;}int n{}; // 自主初始化空
};
我们将以普通指针作函数参数的功能命名为use_uniquePtr_1(MyClass* mc)实际实现如下
void use_uniquePtr_1(MyClass* mc)
{cout use_uniquePtr_1(MyClass* mc) endl;mc-DoSomething();
} 来看如下代码示例调用上方函数
// ************* 普通指针 -- unique_ptr.release() **************
std::unique_ptrMyClass up std::make_uniqueMyClass();use_uniquePtr_1(up.get());
if (!up) { cout (1) up is nullptr endl; }use_uniquePtr_1(up.release());
if (!up) { cout (2) up is nullptr endl; }
运行结果 我们看到运行结果最后并没有打印执行析构函数正是因为 release() 将 up 的所有权转交给use_uniquePtr_1(up.release()); 函数然而在该函数内并未在结束时对 up 存储的对象进行释放处理导致此函数后面执行部分也不能释放这块内存也就自然造成了内存泄漏为程序后续更大的灾难性错误奠定了基础。
如果我再给代码后面加一句访问 up 的操作会报错吗
std::unique_ptrMyClass up std::make_uniqueMyClass();
use_uniquePtr_1(up.get());
if (!up) { cout (1) up is nullptr endl; }use_uniquePtr_1(up.release());
if (!up) { cout (2) up is nullptr endl; }up-DoSomething(); // ### 注意此行 ###
if (!up) { cout (3) up is nullptr endl; }
运行结果 我们看到程序依旧没有报错成功结束整个调试过程但是我们明明在 release() 后 up 变为了空指针为什么在调用类内成员函数 up-DoSomething(); 时没有报错呢结束了但是不代表没有进行非法操作将同样的代码在部分编译器和平台就会运行报错为什么这里程序正常结束没有报错访问空指针
在 C 中调用空指针的成员函数并不一定会导致运行时错误。在这种情况下DoSomething() 函数可能是一个非虚函数而非虚函数在调用时并不会引发空指针解引用导致的崩溃。当然也有前提就是不通过this指针访问成员变量比如我下面给出论证
std::unique_ptrMyClass up std::make_uniqueMyClass();
use_uniquePtr_1(up.get());
if (!up) { cout (1) up is nullptr endl; }
cout up-n endl; // 注意此行use_uniquePtr_1(up.release());
if (!up) { cout (2) up is nullptr endl; }
cout up-n endl; // 注意此行up-DoSomething();
if (!up) { cout (3) up is nullptr endl; }
运行上面代码 嘿嘿报错了吧不急不急访问this指针才报错是吧如果将 release() 后的cout代码行换为访问类内静态成员的值还会报错吗
代码如下
std::unique_ptrMyClass up std::make_uniqueMyClass();
use_uniquePtr_1(up.get());
if (!up) { cout (1) up is nullptr endl; }
//cout up-n endl;
cout up-s_n endl; // 注意此行use_uniquePtr_1(up.release());
if (!up) { cout (2) up is nullptr endl; }
//cout up-n endl;
cout up-s_n endl; // 注意此行
运行结果 具体该部分访问空指针的问题可以参考本人另外一篇文章悬空指针 ---- 未定义行为
2移动语义和 move() 我们了解利用move()实现功能的本质还是实行对象所有权的转移那么我们不妨以此方法来解决和上面 unique_ptr :: release() 情境下的问题但是我们注意到 move() 返回值类型与参数类型一致所以仅需改变函数功能要求将参数从C语言普通指针转换为智能指针类型即可实现 move() 测试将外部智能指针通过参数传递方式实现所有权转移。
为了便于理解 move() 的功能下面给出简单示例
void uniquePointer_move()
{std::unique_ptrMyClass up1 std::make_uniqueMyClass();std::unique_ptrMyClass up2 move(up1); // 通过move()使得up2接管了up1拥有的对象// move()后只会释放 up2 管理的对象而不会释放 up1 的对象// ### up1 在构造时调用构造函数, up2 在析构时调用析构函数 ###
}
运行结果 可以看到正如代码注释部分预测的情况一致并且没有发生内存泄漏的情况。
为了与上面 release() 情形对应将函数功能名称定义为use_uniquePtr_2(unique_ptrMyClass mc)这里需要对函数参数为智能指针引用类型并非值类型的原因作出解释unique_ptr 是独占所有权的智能指针它的移动构造函数将转移所有权的同时使原指针为空。因此在 use_uniquePtr_2 调用后实参将不再拥有对象的所有权实参的值将变成 nullptr。这就导致在 use_uniquePtr_2 函数内部mc(形参) 是一个空的 nullptr而在尝试使用 mc-DoSomething(); 时会导致空指针解引用从而编译器报错。
具体测试实现如下
void uniquePointer_move()
{std::unique_ptrMyClass up1 std::make_uniqueMyClass();std::unique_ptrMyClass up2 move(up1);if (up1) // 试图对 move() 转移后丢失对象所有权的智能指针访问{cout up1 is not nullptr endl;use_uniquePtr_2(up1);}use_uniquePtr_2(up2);
}
运行结果 通过结果发现 use_uniquePtr_2(up1); 并未被调用说明经 move() 函数转移对象所有权后智能指针up1被置空还原为初始默认状态为空指针。如果后面代码在未对 up1 判空的情况下使用就会造成悬空指针的风险发生程序未定义行为或程序崩溃。
三、 对比 shared_ptr 和 unique_ptr unique_ptr: 独占所有权 unique_ptr 独占其所指向的对象的所有权。一个特定的 unique_ptr 是唯一能够拥有和管理其指向对象的智能指针。 轻量级 由于独占所有权unique_ptr 通常比 shared_ptr 更轻量级因为它不需要维护引用计数。 移动语义 支持移动语义可以通过移动转移所有权避免复制开销。 适用场景 当你有一个明确的所有权关系且对象不需要被多个智能指针共享时使用unique_ptr。 shared_ptr: 共享所有权 shared_ptr允许多个智能指针共享对同一对象的所有权通过引用计数来追踪对象的引用次数。 相对重量级 由于需要维护引用计数shared_ptr相对于unique_ptr来说可能更重量级。 循环引用问题 当存在循环引用时shared_ptr需要谨慎使用因为它可能导致内存泄漏。 适用场景 当需要多个智能指针共享同一个对象并且对象的生命周期不容易预测时使用shared_ptr。 自定义删除器 众所周知C中类内的析构函数可以自定义实现释放资源时要进行的额外特定操作虽然智能指针也是类但是我们无法修改其析构函数以帮助我们实现释放资源时进行的相关操作于是我们可以通过构建自定义删除器来指定创建的智能指针对象在析构时要额外执行的指令通俗讲自定义删除器本质上也是一种仿函数。现在利用仿函数自定义 free() 对C语音风格函数返回的指针处理并将该规则传给智能指针删除器简单实现如下
class FreeDeleter // 自定义删除器
{
public:void operator()(void* p){free(p);}
};
假定返回一个堆区指针其解引用所得值为100函数定义如下
int* some_c_function()
{int* p (int*)malloc(sizeof(int));*p 100;return p;
}
在该函数外利用智能指针接受返回值实现自动管理返回指针指向的内存空间请注意如下两种写法的异同
void do_work_1() // unique_ptr 接收
{auto p unique_ptrint, FreeDeleter(some_c_function());printf(%d\n, *p.get());
}
void do_work_2() // shared_ptr 接收
{auto p shared_ptrint(some_c_function(), FreeDeleter());printf(%d\n, *p.get());
}
注意在 shared_ptr 的构造函数中提供的删除器是用于释放资源的而在 unique_ptr 的模板参数中提供的删除器是用于释放资源的因此使用时需要小心确保删除器的正确性。
那 shared_ptr 和 unique_ptr 两者对象绑定自定义删除器的方式可以互换吗 这是运行前的编译器报错显示所以答案显然是不可以的。
默认删除器
其实如果想要利用智能指针来接管已有指针的指向的内存或包含的对象更容易想到的是利用默认删除器来实现
void do_work_3()
{auto p unique_ptrint(some_c_function());printf(%d\n, *p.get());
}
评价 简洁性更为简洁省略了提供自定义删除器的步骤适用于一般情况。 默认删除器使用默认的删除器这是 unique_ptr 的默认行为会调用 delete 进行内存释放。(这就意味着无论处理的是 malloc 还是 new 产生的堆区指针都用 delete 释放) 总体来说选择取决于你对内存释放的需求和是否有额外的清理操作。在一般情况下使用默认删除器的 do_work_3 更为简洁而提供自定义删除器的 do_work_1/2 更适用于需要特定清理操作的情况。 总结 易错点和注意事项 std::shared_ptr 需要小心循环引用。避免裸指针操作尽可能使用智能指针的接口。避免混合使用 new/delete 和 std::make_shared/std::make_unique。不要将 std::shared_ptr 转为 std::unique_ptr除非确保没有其他 shared_ptr 指向同一资源注意对智能指针部分操作可能带来的悬空指针问题 在选择智能指针时根据对象的所有权需求和额外的清理操作来决定使用 shared_ptr 还是 unique_ptr。shared_ptr 适用于多个指针共享对象的情况而 unique_ptr 更适合独占所有权的场景。通过灵活运用它们的特性可以提高代码的安全性和效率。希望本文对正处于学习智能指针阶段的探索者能有一定的指导作用为日常实际项目的运用中提供有用的建议。