内存泄漏和智能指针

目录

1.🫢🫢🫢什么是内存泄漏和智能指针?

[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 指针)

share_ptr的解释:

构造函数

析构函数

拷贝构造函数

赋值运算符

查询引用计数(精华😊)

模拟原生指针操作

[5. 循环引用问题和 weak_ptr介绍](#5. 循环引用问题和 weak_ptr介绍)

循环引用问题的引入

[weak_ptr 的作用](#weak_ptr 的作用)

总结:😊

智能指针的优势:

挑战与注意事项:


1.🫢🫢🫢什么是内存泄漏和智能指针?

在 C++ 编程中,内存泄漏 是一个常见且严重 的问题,尤其是在需要手动管理内存 的场景下。如果程序分配了一块内存,却在不再需要它时没有正确释放,那么这块内存就会永远无法被回收,导致系统资源被浪费。这种现象被称为内存泄漏。

为了帮助程序员更好地管理内存,避免内存泄漏和其他内存管理错误,C++ 标准库引入了智能指针 (Smart Pointer)。智能指针是一种封装了原始指针的类模板,它可以在适当的时候**自动释放资源,**极大地简化了内存管理工作,同时提升了代码的安全性和可维护性。

内存管理问题的背景

  1. 手动内存管理的复杂性

    在传统 C++ 编程中,动态分配内存通常通过 newdelete 进行管理。然而,这种手动管理方法非常容易出错,例如:

    • 忘记释放内存,导致内存泄漏。
    • 多次释放同一块内存,导致程序崩溃。
    • 释放后继续使用已释放的内存,导致未定义行为。
  2. 循环引用的隐患

    在复杂的数据结构(例如图、树)中,对象之间可能会互相引用,导致即使它们已经没有实际用途,仍然无法被正确销毁。例如两个对象分别持有对方的指针时,就可能形成循环引用,从而导致内存无法被释放。

  3. 可维护性问题

    手动管理内存增加了代码的复杂性和错误率。在大型项目中,维护这些代码可能会变得极其困难。

智能指针的引入

智能指针是C++ 标准库(从 C++11 开始 )中提供的工具,用于自动管理动态分配的内存。它通过 RAII(资源获取即初始化)的机制,确保资源在生命周期结束时自动释放,从而避免了内存泄漏和其他内存管理问题。

C++ 提供了以下几种智能指针:

std::auto_ptr :(C++98,已废弃,漏洞太大)

std::unique_ptr:用于独占资源的所有权。

std::shared_ptr:用于共享资源的所有权,使用引用计数管理内存。

std::weak_ptr :辅助 std::shared_ptr,用于解决循环引用问题。

智能指针的作用

  1. 自动管理内存

    智能指针会在它们的生命周期结束时自动释放所管理的资源**,避免了内存泄漏的** 发生。例如,std::unique_ptr 会在它超出作用域时自动调用 delete ,而**std::shared_ptr** 会在最后一个引用被销毁时释放资源。

  2. 提升代码安全性

    智能指针通过 RAII 保证资源的释放时机明确,减少了手动调用 delete 的错误。此外,它们还提供了明确的所有权语义,减少了资源管理中的歧义。

  3. 解决复杂的引用问题

    通过使用 std::weak_ptr,智能指针可以轻松应对复杂的数据结构中的循环引用问题,保证资源能够被正确回收。

智能指针与内存泄漏

智能指针与内存泄漏的关系主要体现在以下几个方面:

  1. 防止忘记释放内存

    手动管理内存时,忘记调用 delete 是导致内存泄漏的主要原因。智能指针会在其生命周期结束时自动释放资源,无需程序员显式管理。

  2. 解决循环引用问题

    在使用 std::shared_ptr 时,可能会因为循环引用导致内存泄漏。这种情况下,std::weak_ptr 提供了一个有效的解决方案,通过弱引用打破循环。

  3. 提高代码的健壮性

    使用智能指针能够显著减少内存管理错误,如双重释放、悬挂指针(dangling pointer)等,从而使代码更加安全和稳定。

下面我们来介绍一下这些智能指针:😁😁

2.auto_ptr 简介与管理权转移机制🌼

auto_ptrC++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;
}

行为解析

  1. std::auto_ptr<int> sp1(new int);

    • 分配动态内存,并由 sp1 管理。
    • 内存内容尚未初始化。
  2. std::auto_ptr<int> sp2(sp1);

    • sp2 调用拷贝构造函数,获取 sp1 的资源管理权。
    • sp1 的指针被置为空。
  3. *sp2 = 10;

    • 使用 sp2 修改资源内容。
  4. cout << *sp2 << endl;

    • 输出资源的内容,结果为 10
  5. 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 被正式移除。

总结

  1. std::auto_ptr 的特点

    • 自动管理动态分配的内存。
    • 支持管理权转移,但会导致原对象悬空。
  2. 缺陷

    • 易用性差,容易导致悬空指针和未定义行为。
    • 不适合标准容器。
    • 管理机制设计过于简单,难以满足复杂场景需求。
  3. 现代替代

    • 使用 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;
}
设计理念:独占所有权
  1. 核心特性

    • 每块动态分配的内存只能由一个 unique_ptr 对象管理
    • 禁止拷贝,确保资源独占。
    • 可通过 std::move 转移所有权,避免不必要的资源拷贝。
  2. 优势

    • 自动释放资源,避免内存泄漏。
    • 独占管理,防止多个对象同时释放同一资源(双重释放问题)。
    • 使用简单,减少手动内存管理的复杂性。
  3. 局限性

    • 无法共享资源,如果需要多个指针共享同一资源,应使用 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_ptrweak_ptr 指针

在 C++ 中,shared_ptrweak_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. 如果是给同一资源赋值,直接返回当前对象。
  2. 如果是不同资源,当前对象的引用计数减 1,若引用计数降为 0,则释放当前对象的资源。
  3. 将当前对象共享到目标资源,并增加目标资源的引用计数。
  • 用途:支持智能指针对象间的赋值操作。
查询引用计数(精华😊)
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介绍

循环引用问题的引入

在使用 shared_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;
}

问题分析:

  1. 引用计数的问题:

    • sp1->_next = sp2sp2->_prev = sp1 之后,两个节点相互持有对方的 shared_ptr
    • sp1 持有 sp2shared_ptr,而 sp2 又持有 sp1shared_ptr
    • 这使得 sp1sp2 的引用计数各自增加到 2,而不是 1。
  2. 循环引用的形成:

    • sp1sp2 各自持有对方的 shared_ptr,导致它们永远无法释放。
    • 即使 sp1sp2 离开作用域,它们的引用计数依旧不会降到 0。
    • 因此,这两个节点的内存永远不会被释放,从而导致 内存泄漏

weak_ptr 的作用
  1. 避免循环引用weak_ptr 不增加引用计数,避免相互依赖的 shared_ptr 对象形成循环引用。
  2. 安全访问资源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;
}

运行过程详解

  1. 创建两个 Node 对象的 shared_ptrsp1sp2

    • sp1sp2 的引用计数初始为 1。
    • 此时资源仅被 sp1sp2 各自管理。
  2. 设置**_next_prev:**

    • sp1->_next = sp2sp1_next 指向 sp2
    • sp2->_prev = sp1sp2_prev 指向 sp1
  3. 由于 _next_prev 使用的是 weak_ptr,它们不会增加对 sp1sp2 的引用计数。

  4. 引用计数查询:

    • 设置 _next_prev 后,sp1.use_count()sp2.use_count() 的值仍为 1。
  5. 退出作用域:

    • sp1sp2 离开作用域时,shared_ptr 的引用计数变为 0,资源被正常释放。


      总结

  • 循环引用问题

    • 由于 shared_ptr 的引用计数机制,双向依赖会导致资源无法释放。
  • weak_ptr 的作用

    • 通过弱引用观察资源,不影响引用计数。
    • 打破循环依赖,使资源能够被正常释放。
  • 案例核心

    • 将链表节点中 _next_prev 指针从 shared_ptr 换为 weak_ptr
    • 避免了循环引用,保证程序在退出时能够正确释放资源。

6.智能指针不能管理连续的地址

智能指针主要设计用于管理堆内存中的资源,它通过封装原始指针来自动管理内存的生命周期,确保内存的正确释放。智能指针的机制通常依赖于引用计数或资源管理策略,std::unique_ptrstd::shared_ptr

然而,智能指针不能直接管理连续的地址(比如数组或固定大小的内存块),原因有几个:

  1. 内存分配方式不同 :智能指针通常用于管理动态分配的单个对象,而连续地址通常涉及动态分配的数组。C++中的智能指针如**std::unique_ptrstd::shared_ptr是为单个对象设计的**,虽然可以通过特定的定制化删除器来支持数组,但其行为并不完全符合标准智能指针的设计意图。

  2. **删除操作不适用:对于连续内存(例如通过new[]分配的数组),需要调用delete[]来释放内存,而std::unique_ptrstd::shared_ptr默认会调用delete,**这会导致释放数组时出现问题。虽然可以自定义删除器,但这会增加复杂度。

  3. 内存管理复杂性:智能指针管理的是对象的生命周期,通常假设每个对象的大小和位置是独立的。对于连续内存块,内存管理更加复杂,需要考虑到整个数组的生命周期,而不仅仅是单个对象的销毁。因此,直接使用智能指针来管理这样的内存会涉及额外的管理逻辑,增加了程序的复杂度和出错的风险。

为了管理连续内存,可以使用std::vectorstd::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++ 中扮演着至关重要的角色,极大地提高了程序的安全性和可维护性。了解并合理使用这些智能指针,可以让我们编写更安全、更高效的代码,同时避免许多常见的内存管理错误。

相关推荐
qincjun35 分钟前
文件I/O操作:C++
开发语言·c++
星语心愿.39 分钟前
D4——贪心练习
c++·算法·贪心算法
汉克老师1 小时前
2023年厦门市第30届小学生C++信息学竞赛复赛上机操作题(三、2023C. 太空旅行(travel))
开发语言·c++
single5941 小时前
【c++笔试强训】(第四十一篇)
java·c++·算法·深度优先·图论·牛客
yuanbenshidiaos1 小时前
C++-----函数与库
开发语言·c++·算法
Lenyiin2 小时前
3354. 使数组元素等于零
c++·算法·leetcode·周赛
爱打APEX的小李2 小时前
拷贝构造和赋值运算符重载
c++
霁月风3 小时前
设计模式——工厂方法模式
c++·设计模式·工厂方法模式
夜阳朔3 小时前
《C++ Primer》第三章知识点
c++·编程语言
sjyioo3 小时前
【C++】类和对象.1
c++