目录
[2.auto_ptr 简介与管理权转移机制🌼](#2.auto_ptr 简介与管理权转移机制🌼)
[1. 构造函数](#1. 构造函数)
[2. 拷贝构造函数](#2. 拷贝构造函数)
[3. 赋值运算符](#3. 赋值运算符)
[4. 析构函数](#4. 析构函数)
[5. 指针操作符](#5. 指针操作符)
[问题分析:为什么说 auto_ptr 是失败的设计🤣🤣](#问题分析:为什么说 auto_ptr 是失败的设计🤣🤣)
[3. unique_ptr:独占型智能指针](#3. unique_ptr:独占型智能指针)
[4.shared_ptr 和 weak_ptr 指针](#4.shared_ptr 和 weak_ptr 指针)
[5. 循环引用问题和 weak_ptr介绍](#5. 循环引用问题和 weak_ptr介绍)
[weak_ptr 的作用](#weak_ptr 的作用)
1.🫢🫢🫢什么是内存泄漏和智能指针?
在 C++ 编程中,内存泄漏 是一个常见且严重 的问题,尤其是在需要手动管理内存 的场景下。如果程序分配了一块内存,却在不再需要它时没有正确释放,那么这块内存就会永远无法被回收,导致系统资源被浪费。这种现象被称为内存泄漏。
为了帮助程序员更好地管理内存,避免内存泄漏和其他内存管理错误,C++ 标准库引入了智能指针 (Smart Pointer)。智能指针是一种封装了原始指针的类模板,它可以在适当的时候**自动释放资源,**极大地简化了内存管理工作,同时提升了代码的安全性和可维护性。
内存管理问题的背景
-
手动内存管理的复杂性
在传统 C++ 编程中,动态分配内存通常通过
new
和delete
进行管理。然而,这种手动管理方法非常容易出错,例如:- 忘记释放内存,导致内存泄漏。
- 多次释放同一块内存,导致程序崩溃。
- 释放后继续使用已释放的内存,导致未定义行为。
-
循环引用的隐患
在复杂的数据结构(例如图、树)中,对象之间可能会互相引用,导致即使它们已经没有实际用途,仍然无法被正确销毁。例如两个对象分别持有对方的指针时,就可能形成循环引用,从而导致内存无法被释放。
-
可维护性问题
手动管理内存增加了代码的复杂性和错误率。在大型项目中,维护这些代码可能会变得极其困难。
智能指针的引入
智能指针是C++ 标准库(从 C++11 开始 )中提供的工具,用于自动管理动态分配的内存。它通过 RAII(资源获取即初始化)的机制,确保资源在生命周期结束时自动释放,从而避免了内存泄漏和其他内存管理问题。
C++ 提供了以下几种智能指针:
std::auto_ptr :(C++98,已废弃,漏洞太大)
std::unique_ptr
:用于独占资源的所有权。
std::shared_ptr
:用于共享资源的所有权,使用引用计数管理内存。
std::weak_ptr
:辅助 std::shared_ptr
,用于解决循环引用问题。
智能指针的作用
-
自动管理内存
智能指针会在它们的生命周期结束时自动释放所管理的资源**,避免了内存泄漏的** 发生。例如,
std::unique_ptr
会在它超出作用域时自动调用delete
,而**std::shared_ptr
** 会在最后一个引用被销毁时释放资源。 -
提升代码安全性
智能指针通过 RAII 保证资源的释放时机明确,减少了手动调用
delete
的错误。此外,它们还提供了明确的所有权语义,减少了资源管理中的歧义。 -
解决复杂的引用问题
通过使用
std::weak_ptr
,智能指针可以轻松应对复杂的数据结构中的循环引用问题,保证资源能够被正确回收。
智能指针与内存泄漏
智能指针与内存泄漏的关系主要体现在以下几个方面:
-
防止忘记释放内存
手动管理内存时,忘记调用
delete
是导致内存泄漏的主要原因。智能指针会在其生命周期结束时自动释放资源,无需程序员显式管理。 -
解决循环引用问题
在使用
std::shared_ptr
时,可能会因为循环引用导致内存泄漏。这种情况下,std::weak_ptr
提供了一个有效的解决方案,通过弱引用打破循环。 -
提高代码的健壮性
使用智能指针能够显著减少内存管理错误,如双重释放、悬挂指针(dangling pointer)等,从而使代码更加安全和稳定。
下面我们来介绍一下这些智能指针:😁😁
2.auto_ptr
简介与管理权转移机制🌼
auto_ptr
是 C++98 中引入的一个智能指针类,其主要功能是自动管理动态分配的内存,避免显式调用 delete
。它的核心特性是 管理权转移 ,即当一个 auto_ptr
对象被复制或赋值时 ,原有的 auto_ptr
会将其管理的内存所有权转移给新的 auto_ptr
,自己变为"空悬状态" (dangling)。这一设计虽然初衷良好**,但存在严重缺陷**,因此在现代 C++ 中被废弃。
以下是代码解析以及其原理的详细介绍:
1. 构造函数
auto_ptr
的构造函数接受一个原始指针,表明它将管理这块内存:
cpp
auto_ptr(T* ptr) : _ptr(ptr) {}
这个指针 _ptr
是**auto_ptr
** 类管理的资源。当 auto_ptr
对象销毁 时,_ptr
会被释放。
2. 拷贝构造函数
auto_ptr
的拷贝构造函数实现了管理权转移:
cpp
auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) {
sp._ptr = nullptr; // 原指针悬空
}
新的**auto_ptr
(如 sp2
)** 获取原对象(如 sp1
)所管理的内存。
原对象的指针被设置为 nullptr
,避免多次释放同一块内存。
问题就出现在这里:sp2把管理的权限给了sp1自己空了,但是sp2并没有销毁,如果再次使用sp2,就会越界访问。
3. 赋值运算符
赋值运算符同样实现了管理权转移
cpp
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if (this != &ap) {
if (_ptr) delete _ptr; // 释放当前对象的资源
_ptr = ap._ptr; // 获取新资源
ap._ptr = NULL; // 原对象悬空
}
return *this;
}
检查是否是自赋值(this != &ap
),避免错误。
释放当前 auto_ptr
对象持有的资源。
转移新资源的管理权,并将原对象悬空。
4. 析构函数
在**auto_ptr
**对象生命周期结束时,自动释放所管理的内存:
cpp
~auto_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
5. 指针操作符
auto_ptr
支持像普通指针一样访问所管理的资源:
cpp
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
解引用操作符(*
)返回资源内容。
箭头操作符(->
)返回资源的原始指针。
以下是一个完整的 C++98 代码示例,演示了 std::auto_ptr
的管理权转移机制以及其带来的问题,同时展示了如何通过 std::auto_ptr
管理内存。
cpp
#include <iostream>
namespace bit {
// 模拟 auto_ptr
template <class T>
class auto_ptr {
public:
// 构造函数,接管原始指针的管理权
auto_ptr(T* ptr) : _ptr(ptr) {}
// 拷贝构造函数,转移管理权
auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) {
// 将原指针置为 nullptr,避免多重释放
sp._ptr = nullptr;
}
// 赋值运算符,转移管理权
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if (this != &ap) { // 防止自赋值
// 释放当前资源
if (_ptr) {
delete _ptr;
}
// 转移资源
_ptr = ap._ptr;
// 将原指针置为 nullptr,避免多重释放
ap._ptr = nullptr;
}
return *this;
}
// 析构函数,自动释放资源
~auto_ptr() {
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
// 解引用操作符
T& operator*() {
return *_ptr;
}
// 箭头操作符
T* operator->() {
return _ptr;
}
private:
T* _ptr; // 原始指针
};
}
cpp
int main() {
// 动态分配内存并由 auto_ptr 管理
bit::auto_ptr<int> sp1(new int(20)); // sp1 拥有动态分配的内存
std::cout << "sp1 points to: " << *sp1 << std::endl; // 输出 20
// 拷贝构造函数触发管理权转移
bit::auto_ptr<int> sp2(sp1); // sp2 获取 sp1 的管理权
std::cout << "sp2 points to: " << *sp2 << std::endl; // 输出 20
// 注意:sp1 已经悬空,不再拥有内存管理权
// 以下代码会导致未定义行为,因为 sp1 已悬空
// std::cout << "sp1 points to: " << *sp1 << std::endl; // 错误!sp1 悬空
return 0;
}
cpp
int main() {
std::auto_ptr<int> sp1(new int); // 动态分配内存,sp1 管理资源
std::auto_ptr<int> sp2(sp1); // 管理权转移,sp1 被悬空
// sp1 悬空,不能使用
*sp2 = 10; // 修改资源内容
cout << *sp2 << endl; // 输出 10
cout << *sp1 << endl; // 未定义行为,可能崩溃
return 0;
}
行为解析
-
std::auto_ptr<int> sp1(new int);
- 分配动态内存,并由
sp1
管理。 - 内存内容尚未初始化。
- 分配动态内存,并由
-
std::auto_ptr<int> sp2(sp1);
sp2
调用拷贝构造函数,获取sp1
的资源管理权。sp1
的指针被置为空。
-
*sp2 = 10;
- 使用
sp2
修改资源内容。
- 使用
-
cout << *sp2 << endl;
- 输出资源的内容,结果为
10
。
- 输出资源的内容,结果为
-
cout << *sp1 << endl;
sp1
已悬空,解引用其指针会导致未定义行为(程序可能崩溃)。
问题分析:为什么说 auto_ptr
是失败的设计🤣🤣
1.管理权转移的副作用
管理权转移会导致原 auto_ptr
对象悬空。如果在使用悬空对象时未检查指针状态,会导致未定义行为。
程序员需要额外注意每个 auto_ptr
的状态,违背了智能指针"降低内存管理复杂性"的初衷。
2.不支持标准容器
标准容器(如 std::vector
)要求元素支持拷贝,而 auto_ptr
的管理权转移机制导致元素拷贝后原来的 auto_ptr
被置空,无法满足容器的语义需求。
3.多次释放问题
如果误操作导致多个 auto_ptr
持有同一资源,会引发双重释放问题,程序崩溃。
4.现代替代方案
C++11 引入了 std::
unique_ptr****和 std::shared_ptr
, 分别通过禁用拷贝和引用计数机制提供了更安全的内存管理方式。
从 C++17 开始,auto_ptr
被正式移除。
总结
-
std::auto_ptr
的特点- 自动管理动态分配的内存。
- 支持管理权转移,但会导致原对象悬空。
-
缺陷
- 易用性差,容易导致悬空指针和未定义行为。
- 不适合标准容器。
- 管理机制设计过于简单,难以满足复杂场景需求。
-
现代替代
- 使用
std::unique_ptr
替代独占所有权的场景。 - 使用
std::shared_ptr
替代共享所有权的场景。 - 避免使用
auto_ptr
,它是一个过时且被废弃的设计。
- 使用
下面我们来介绍unique_ptr指针😁😁😁
3. unique_ptr
:独占型智能指针
std::unique_ptr
是 C++11 中引入的一种独占所有权的智能指针,用于安全地管理动态分配的内存。它的核心特性是 独占性 ,即同一时间只能有一个**unique_ptr
** 对象管理某一块资源。它通过禁止拷贝和赋值操作确保这一点,并允许通过移动语义转移资源所有权。
以下是对代码的解析及其功能介绍:
构造函数
cpp
unique_ptr(T* ptr)
: _ptr(ptr) {}
- 接受一个原始指针
ptr
,表明unique_ptr
将独占管理这块内存。 - 一旦资源被**
unique_ptr
** 接管**,就无需手动调用delete
。**
析构函数
cpp
~unique_ptr()
{
if (_ptr)
{ cout << "delete:" << _ptr << endl;
delete _ptr; }
}
在 unique_ptr
生命周期结束时(例如超出作用域) ,析构函数会自动释放所管理的内存,防止内存泄漏。
禁止拷贝
cpp
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
- 禁止拷贝构造和拷贝赋值操作,确保每块资源只被一个
unique_ptr
对象独占管理。 - 如果尝试拷贝,会导致编译错误。
指针操作符
cpp
T& operator*()
{ return *_ptr; }
T* operator->()
{ return _ptr; }
支持解引用(*
)和箭头(->
)操作,使得 unique_ptr
使用方式类似于原始指针。
完整代码展示:
cpp
#include <iostream>
#include <memory> // 标准库 unique_ptr 所在头文件
using namespace std;
// 模拟 C++11 的 unique_ptr 实现
namespace bit {
template <class T>
class unique_ptr {
public:
// 构造函数:接管资源
unique_ptr(T* ptr) : _ptr(ptr) {}
// 析构函数:释放资源
~unique_ptr() {
if (_ptr) {
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
// 禁止拷贝构造和拷贝赋值
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
// 移动构造函数:转移资源所有权
unique_ptr(unique_ptr<T>&& sp) noexcept : _ptr(sp._ptr) {
sp._ptr = nullptr;
}
// 移动赋值运算符:转移资源所有权
unique_ptr<T>& operator=(unique_ptr<T>&& sp) noexcept {
if (this != &sp) {
// 释放当前资源
if (_ptr) {
delete _ptr;
}
// 转移资源
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
// 解引用操作符
T& operator*() {
return *_ptr;
}
// 箭头操作符
T* operator->() {
return _ptr;
}
// 获取原始指针
T* get() const {
return _ptr;
}
// 释放资源
void reset(T* ptr = nullptr) {
if (_ptr) {
delete _ptr;
}
_ptr = ptr;
}
private:
T* _ptr; // 管理的资源
};
}
cpp
#include <iostream>
#include <memory> // 需要包含此头文件
using namespace std;
int main() {
// 创建 unique_ptr 管理资源
bit::unique_ptr<int> uptr1(new int(10));
cout << "Value: " << *uptr1 << endl; // 输出 10
// 禁止拷贝,以下代码会报错
// bit::unique_ptr<int> uptr2 = uptr1; // 错误:拷贝构造被删除
// 禁止赋值,以下代码会报错
// bit::unique_ptr<int> uptr3;
// uptr3 = uptr1; // 错误:赋值运算符被删除
return 0; // 资源会自动释放
}
转移管理权
尽管 unique_ptr
禁止拷贝,但可以通过移动语义转移管理权:
cpp
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> uptr1(new int(20));
cout << "uptr1 points to: " << *uptr1 << endl; // 输出 20
// 转移管理权到 uptr2
unique_ptr<int> uptr2 = std::move(uptr1);
cout << "uptr2 points to: " << *uptr2 << endl; // 输出 20
// uptr1 现在为空
if (!uptr1) {
cout << "uptr1 is now null." << endl;
}
return 0;
}
设计理念:独占所有权
-
核心特性
- 每块动态分配的内存只能由一个
unique_ptr
对象管理。 - 禁止拷贝,确保资源独占。
- 可通过
std::move
转移所有权,避免不必要的资源拷贝。
- 每块动态分配的内存只能由一个
-
优势
- 自动释放资源,避免内存泄漏。
- 独占管理,防止多个对象同时释放同一资源(双重释放问题)。
- 使用简单,减少手动内存管理的复杂性。
-
局限性
- 无法共享资源,如果需要多个指针共享同一资源,应使用
std::shared_ptr
。
- 无法共享资源,如果需要多个指针共享同一资源,应使用
总结
-
std::unique_ptr
的优点- 独占所有权,禁止拷贝,安全高效。
- 自动释放所管理资源,防止内存泄漏。
- 支持移动语义,可灵活转移资源管理权。
-
适用场景
- 需要动态分配资源,并确保每块资源只由一个指针管理。
- 希望简化内存管理,避免忘记
delete
。
-
与其他智能指针的比较
std::shared_ptr
:适用于多个指针共享同一资源的场景,通过引用计数实现。std::weak_ptr
:避免循环引用,协助std::shared_ptr
管理资源。std::unique_ptr
:适用于资源独占的场景,轻量级且高效。
现代 C++ 项目中,std::unique_ptr
是推荐的智能指针之一,尤其适合独占资源的管理需求。
下面来介绍share_ptr和weak_ptr指针💕💕
4.shared_ptr
和 weak_ptr
指针
在 C++ 中,shared_ptr
和 weak_ptr
是两种常用的智能指针 ,它们用于共享和管理动态分配的资源 ,尤其适用于多个对象共享相同资源的场景 。它们与 unique_ptr
不同,shared_ptr
允许多个智能指针指向同一块内存 ,通过引用计数来管理内存的释放。而 weak_ptr
则是为了避免循环引用问题而设计的,它并不增加引用计数。
share_ptr的解释:
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
代码分解与分析
构造函数
cpp
share_Ptr(T* ptr)
: _ptr(ptr),
_pcount(new int(1))
{}
功能 :初始化智能指针,管理传入的资源 ptr
,并初始化引用计数为 1
。
作用 :第一个持有资源的 share_Ptr
对象会负责创建引用计数 _pcount
。
析构函数
cpp
~share_Ptr() {
if (--(*_pcount) == 0) {
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
- 功能 :
- 每个
share_Ptr
对象销毁时,引用计数减 1。 - 如果引用计数降为 0,说明没有其他
share_Ptr
对象引用该资源,释放资源和引用计数。
- 每个
- 关键点:防止资源泄漏,同时避免过早释放资源。
拷贝构造函数
cpp
share_Ptr(share_Ptr<T> &a)
: _ptr(a._ptr)
,_pcount(a._pcount)
{
(*_pcount)++;
}
功能:
用现有的 share_Ptr
对象初始化新的 share_Ptr
对象。
共享资源指针 _ptr
和引用计数 _pcount
,并将引用计数加 1。
用途:支持智能指针对象的拷贝操作。
赋值运算符
cpp
share_Ptr& operator=(share_Ptr<T>& a) {
if (_ptr == a._ptr) { // 防止自己给自己赋值
return *this;
}
if (--(*_pcount) == 0) { // 释放当前资源
delete _ptr;
delete _pcount;
}
// 共享新资源
_ptr = a._ptr;
_pcount = a._pcount;
(*_pcount)++;
return *this;
}
功能:
- 如果是给同一资源赋值,直接返回当前对象。
- 如果是不同资源,当前对象的引用计数减 1,若引用计数降为 0,则释放当前对象的资源。
- 将当前对象共享到目标资源,并增加目标资源的引用计数。
- 用途:支持智能指针对象间的赋值操作。
查询引用计数(精华😊)
cpp
int use_count()
{
return *_pcount;
}
- 功能:返回当前资源的引用计数。
- 用途:方便用户查询资源的共享情况。
模拟原生指针操作
cpp
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
功能:
operator->
:提供指针访问资源成员的能力。
operator*
:返回引用,支持访问资源的内容。
用途 :使**share_Ptr
**的使用与原生指针类似。
使用案例:
cpp
int main() {
// 创建两个资源,并分别用 share_Ptr 管理
share_Ptr<int> sp1(new int(10));
share_Ptr<int> sp2(new int(20));
cout << "sp1 use_count: " << sp1.use_count() << endl; // 输出 1
cout << "sp2 use_count: " << sp2.use_count() << endl; // 输出 1
// 创建共享资源的对象 sp3 和 sp4
share_Ptr<int> sp3 = sp1; // sp3 与 sp1 共享资源
share_Ptr<int> sp4 = sp2; // sp4 与 sp2 共享资源
// 创建更多共享对象 sp5 和 sp6
share_Ptr<int> sp5 = sp1; // sp5 与 sp1、sp3 共享资源
share_Ptr<int> sp6 = sp2; // sp6 与 sp2、sp4 共享资源
// sp5 和 sp6 离开作用域,引用计数减少
cout << "\nAfter sp5 and sp6 go out of scope:\n";
cout << "sp1 use_count: " << sp1.use_count() << endl; // 输出 2
cout << "sp2 use_count: " << sp2.use_count() << endl; // 输出 2
cout << "sp3 use_count: " << sp3.use_count() << endl; // 输出 2
cout << "sp4 use_count: " << sp4.use_count() << endl; // 输出 2
// sp3 和 sp4 离开作用域,引用计数减少
cout << "\nAfter sp3 and sp4 go out of scope:\n";
cout << "sp1 use_count: " << sp1.use_count() << endl; // 输出 1
cout << "sp2 use_count: " << sp2.use_count() << endl; // 输出 1
// 程序结束,sp1 和 sp2 离开作用域,资源被释放
return 0;
}
本示例展示了多个智能指针对象共享同一块资源时,如何通过引用计数有效管理资源。
代码避免了重复释放资源或内存泄漏的问题,同时通过引用计数的变化清晰体现资源的生命周期管理。
5. 循环引用问题和 weak_ptr介绍
循环引用问题的引入
在使用 s
hared_ptr****管理资源时,如果两个对象通过 shared_ptr
相互引用 ,会导致 循环引用问题 。这种情况下,即使没有任何其他外部对象持有这些资源,引用计数也永远不会降为 0,导致资源无法释放,从而引发 内存泄漏。
背景介绍
在链表或类似结构中,节点通常会有指向其他节点的指针(例如 _next
和 _prev
),而使用智能指针管理这些指针时,如果采用 shared_ptr
,容易造成 循环引用问题,导致内存泄漏。
案例代码说明
该代码模拟了一个双向链表节点 Node
的场景,展示了如何通过 weak_ptr
来打破 shared_ptr
的循环引用。
结构体定义
cpp
struct Node {
int _val; // 存储节点数据
shared_ptr<Node> _next; // 指向下一个节点的 shared_ptr
shared_ptr<Node> _prev; // 指向前一个节点的 shared_ptr
};
cpp
int main() {
shared_ptr<Node> sp1(new Node); // 创建第一个节点
shared_ptr<Node> sp2(new Node); // 创建第二个节点
cout << sp1.use_count() << endl; // 输出:1
cout << sp2.use_count() << endl; // 输出:1
sp1->_next = sp2; // sp1 的 `_next` 指向 sp2
sp2->_prev = sp1; // sp2 的 `_prev` 指向 sp1
// 设置双向链接后,两个节点互相持有对方的 shared_ptr,导致引用计数都不为零
cout << sp1.use_count() << endl; // 输出:2
cout << sp2.use_count() << endl; // 输出:2
return 0;
}
问题分析:
-
引用计数的问题:
- 在
sp1->_next = sp2
和sp2->_prev = sp1
之后,两个节点相互持有对方的shared_ptr
。 sp1
持有sp2
的shared_ptr
,而sp2
又持有sp1
的shared_ptr
。- 这使得
sp1
和sp2
的引用计数各自增加到 2,而不是 1。
- 在
-
循环引用的形成:
sp1
和sp2
各自持有对方的shared_ptr
,导致它们永远无法释放。- 即使
sp1
和sp2
离开作用域,它们的引用计数依旧不会降到 0。 - 因此,这两个节点的内存永远不会被释放,从而导致 内存泄漏。
weak_ptr
的作用
- 避免循环引用 :
weak_ptr
不增加引用计数,避免相互依赖的shared_ptr
对象形成循环引用。 - 安全访问资源 :
weak_ptr
可通过lock()
方法转化为一个shared_ptr
,只有在资源有效时才能访问,从而避免访问悬空资源。
weak_ptr
的基本实现
以下是一个简化版的 weak_ptr
实现:
cpp
template<class T>
class weak_ptr {
public:
weak_ptr() : _ptr(nullptr) {}
// 从 shared_ptr 创建 weak_ptr
weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
// 赋值操作
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp.get();
return *this;
}
// 安全访问资源
T* lock() const {
return _ptr;
}
// 检查资源是否有效
bool expired() const {
return _ptr == nullptr;
}
private:
T* _ptr; // 仅仅观察资源,不影响引用计数
};
正确结构体定义:
cpp
struct Node {
A _val; // 节点数据
bit::weak_ptr<Node> _next; // 指向下一个节点的 weak_ptr
bit::weak_ptr<Node> _prev; // 指向前一个节点的 weak_ptr
};
1._next
和 _prev
使用 weak_ptr
:
- 避免
shared_ptr
的循环引用问题。 weak_ptr
仅观察_next
和_prev
指向的资源,不增加引用计数。
cpp
int main() {
bit::shared_ptr<Node> sp1(new Node);
bit::shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl; // 输出:1
cout << sp2.use_count() << endl; // 输出:1
sp1->_next = sp2; // sp1 的 `_next` 指向 sp2
sp2->_prev = sp1; // sp2 的 `_prev` 指向 sp1
cout << sp1.use_count() << endl; // 输出:1,sp2 的 weak_ptr 不增加 sp1 的引用计数
cout << sp2.use_count() << endl; // 输出:1,sp1 的 weak_ptr 不增加 sp2 的引用计数
return 0;
}
运行过程详解
-
创建两个
Node
对象的shared_ptr
,sp1
和sp2
。sp1
和sp2
的引用计数初始为 1。- 此时资源仅被
sp1
和sp2
各自管理。
-
设置**
_next
和_prev
:**sp1->_next = sp2
:sp1
的_next
指向sp2
。sp2->_prev = sp1
:sp2
的_prev
指向sp1
。
-
由于
_next
和_prev
使用的是weak_ptr
,它们不会增加对sp1
和sp2
的引用计数。 -
引用计数查询:
- 设置
_next
和_prev
后,sp1.use_count()
和sp2.use_count()
的值仍为 1。
- 设置
-
退出作用域:
-
当
sp1
和sp2
离开作用域时,shared_ptr
的引用计数变为 0,资源被正常释放。
总结
-
-
循环引用问题:
- 由于
shared_ptr
的引用计数机制,双向依赖会导致资源无法释放。
- 由于
-
weak_ptr
的作用:- 通过弱引用观察资源,不影响引用计数。
- 打破循环依赖,使资源能够被正常释放。
-
案例核心:
- 将链表节点中
_next
和_prev
指针从shared_ptr
换为weak_ptr
。 - 避免了循环引用,保证程序在退出时能够正确释放资源。
- 将链表节点中
6.智能指针不能管理连续的地址
智能指针主要设计用于管理堆内存中的资源,它通过封装原始指针来自动管理内存的生命周期,确保内存的正确释放。智能指针的机制通常依赖于引用计数或资源管理策略,如std::unique_ptr
和std::shared_ptr
。
然而,智能指针不能直接管理连续的地址(比如数组或固定大小的内存块),原因有几个:
-
内存分配方式不同 :智能指针通常用于管理动态分配的单个对象,而连续地址通常涉及动态分配的数组。C++中的智能指针如**
std::unique_ptr
和std::shared_ptr
是为单个对象设计的**,虽然可以通过特定的定制化删除器来支持数组,但其行为并不完全符合标准智能指针的设计意图。 -
**删除操作不适用:对于连续内存(例如通过
new[]
分配的数组),需要调用delete[]
来释放内存,而std::unique_ptr
和std::shared_ptr
默认会调用delete
,**这会导致释放数组时出现问题。虽然可以自定义删除器,但这会增加复杂度。 -
内存管理复杂性:智能指针管理的是对象的生命周期,通常假设每个对象的大小和位置是独立的。对于连续内存块,内存管理更加复杂,需要考虑到整个数组的生命周期,而不仅仅是单个对象的销毁。因此,直接使用智能指针来管理这样的内存会涉及额外的管理逻辑,增加了程序的复杂度和出错的风险。
为了管理连续内存,可以使用std::vector
或std::array
(对于已知大小的静态数组),它们是C++标准库中专门为这种场景设计的容器,可以自动管理内存并提供灵活的内存访问。😊😊
总结:😊
智能指针是 C++ 中的重要工具,极大地简化了内存管理,帮助开发者避免了许多由裸指针带来的问题,如内存泄漏、悬挂指针和双重释放等问题。它们通过 RAII(资源获取即初始化)机制,自动管理内存和资源的释放,使得代码更加简洁和安全。
通过本文的分析,我们可以总结出以下几个关键点:
unique_ptr
:它是一个独占所有权的智能指针,确保资源的唯一拥有者,避免了资源的重复释放。由于它不支持拷贝,只能通过转移所有权来实现资源管理,因此它非常适用于动态资源的管理。
shared_ptr
:支持多个所有者共享资源,通过引用计数来确保资源在最后一个持有者离开时被正确释放。
shared_ptr
使得多对象共享同一资源变得简单,但也引入了循环引用的问题,特别是在涉及复杂数据结构时。
weak_ptr
:用于打破
shared_ptr
引发的循环引用问题,它提供了一种不增加引用计数的方式来观察资源。通过weak_ptr
,我们能够避免不必要的内存泄漏,并确保资源在没有任何有效引用时被及时释放。智能指针的优势:
自动内存管理 :智能指针自动管理资源的生命周期,避免了手动调用
delete
的繁琐,也减少了错误发生的可能性。提高代码安全性:通过 RAII 原则,智能指针确保资源能够在作用域结束时自动清理,防止内存泄漏和不必要的资源占用。
易于使用:智能指针在 C++ 中实现了类似引用的语法,可以像普通指针一样使用,极大地提升了代码的可读性和可维护性。
挑战与注意事项:
性能开销 :虽然智能指针大大简化了内存管理,但它们可能引入一些性能开销,特别是
shared_ptr
的引用计数操作和mutex
锁的使用。循环引用 :如前所述,
shared_ptr
会在某些情况下导致循环引用问题,需要用weak_ptr
来解决这一问题。理解语义:开发者需要理解每种智能指针的语义及其使用场景,以确保选择最合适的工具来管理资源。
总的来说,智能指针在 C++ 中扮演着至关重要的角色,极大地提高了程序的安全性和可维护性。了解并合理使用这些智能指针,可以让我们编写更安全、更高效的代码,同时避免许多常见的内存管理错误。