C++进阶:(十六)从裸指针到智能指针,C++ 内存管理的 “自动驾驶” 进化之路

目录

前言

[一、裸指针的 "血泪史":为什么我们需要智能指针?](#一、裸指针的 “血泪史”:为什么我们需要智能指针?)

[1.1 内存泄漏:最常见的 "噩梦"](#1.1 内存泄漏:最常见的 “噩梦”)

[1.2 二次释放:致命的 "双重打击"](#1.2 二次释放:致命的 “双重打击”)

[1.3 野指针:潜伏的 "幽灵"](#1.3 野指针:潜伏的 “幽灵”)

[1.4 异常安全:被忽略的 "隐形杀手"](#1.4 异常安全:被忽略的 “隐形杀手”)

[1.5 智能指针的核心使命](#1.5 智能指针的核心使命)

[二、智能指针的 "三驾马车":unique_ptr、shared_ptr、weak_ptr](#二、智能指针的 “三驾马车”:unique_ptr、shared_ptr、weak_ptr)

[2.1 unique_ptr:独占所有权的 "独行侠"](#2.1 unique_ptr:独占所有权的 “独行侠”)

[2.1.1 unique_ptr 的核心原理](#2.1.1 unique_ptr 的核心原理)

[2.1.2 unique_ptr 的基本使用](#2.1.2 unique_ptr 的基本使用)

[2.1.3 unique_ptr 的使用场景与最佳实践](#2.1.3 unique_ptr 的使用场景与最佳实践)

[2.2 shared_ptr:共享所有权的 "社交达人"](#2.2 shared_ptr:共享所有权的 “社交达人”)

[2.2.1 shared_ptr 的核心原理:引用计数](#2.2.1 shared_ptr 的核心原理:引用计数)

[2.2.2 shared_ptr 的基本使用](#2.2.2 shared_ptr 的基本使用)

[2.2.3 循环引用:shared_ptr 的 "阿喀琉斯之踵"](#2.2.3 循环引用:shared_ptr 的 “阿喀琉斯之踵”)

[2.3 weak_ptr:打破循环的 "旁观者"](#2.3 weak_ptr:打破循环的 “旁观者”)

[2.3.1 weak_ptr 的核心原理](#2.3.1 weak_ptr 的核心原理)

[2.3.2 weak_ptr 的基本使用与循环引用解决方案](#2.3.2 weak_ptr 的基本使用与循环引用解决方案)

[2.3.3 weak_ptr 的使用场景与最佳实践](#2.3.3 weak_ptr 的使用场景与最佳实践)

[三、智能指针的 "进阶技巧":定制删除器、类型转换与性能优化](#三、智能指针的 “进阶技巧”:定制删除器、类型转换与性能优化)

[3.1 定制删除器:处理特殊资源释放](#3.1 定制删除器:处理特殊资源释放)

[3.1.1 unique_ptr 的定制删除器](#3.1.1 unique_ptr 的定制删除器)

[3.1.2 shared_ptr 的定制删除器](#3.1.2 shared_ptr 的定制删除器)

[3.2 智能指针的类型转换](#3.2 智能指针的类型转换)

[3.3 智能指针的性能优化](#3.3 智能指针的性能优化)

[3.3.1 优先使用 unique_ptr](#3.3.1 优先使用 unique_ptr)

[3.3.2 使用 make_shared 减少内存分配](#3.3.2 使用 make_shared 减少内存分配)

[3.3.3 避免不必要的 shared_ptr 拷贝](#3.3.3 避免不必要的 shared_ptr 拷贝)

[3.3.4 合理使用 weak_ptr 的 lock ()](#3.3.4 合理使用 weak_ptr 的 lock ())

[四、智能指针的 "避坑指南":常见错误与最佳实践总结](#四、智能指针的 “避坑指南”:常见错误与最佳实践总结)

[4.1 常见错误](#4.1 常见错误)

[4.2 最佳实践总结](#4.2 最佳实践总结)

总结


前言

在 C++ 的世界里,有一个让无数开发者 "谈虎色变" 的话题 ------ 内存管理。指针作为 C++ 的 "灵魂",赋予了开发者直接操作内存的强大能力,但也埋下了内存泄漏、野指针、二次释放等一系列 "定时炸弹"。你是否也曾因为一个忘记释放的new,调试了一下午的内存泄漏?是否也曾被野指针导致的程序崩溃搞得怀疑人生?

幸运的是,C++ 标准库为我们提供了一套 "自动驾驶" 级别的解决方案 ------ 智能指针。它就像给裸指针装上了 "自动刹车" 和 "自动泊车" 系统,让内存管理变得安全、高效且省心。本文将从底层原理到实际应用,全方位拆解智能指针的设计精髓,带你彻底掌握unique_ptrshared_ptrweak_ptr的使用技巧,从此和内存问题说再见!下面就让我们正式开始吧!


一、裸指针的 "血泪史":为什么我们需要智能指针?

在深入智能指针之前,我们先回顾一下**裸指针(Raw Pointer)**的 "坑"。正是这些痛点,催生了智能指针的诞生。

1.1 内存泄漏:最常见的 "噩梦"

内存泄漏是指程序分配的内存空间在使用完毕后,没有被正确释放,导致这部分内存永远无法被再次使用。尤其在复杂的程序逻辑中,一个疏忽就可能造成内存泄漏。

cpp 复制代码
// 反面示例:裸指针导致的内存泄漏
void func() {
    int* p = new int(10); // 分配堆内存
    // 业务逻辑处理...
    if (some_condition) {
        return; // 提前返回,忘记释放p
    }
    // 其他操作...
    delete p; // 正常路径下的释放,但异常路径会跳过
}

int main() {
    while (true) {
        func(); // 循环调用,每次都会泄漏4字节内存
        sleep(1);
    }
    return 0;
}

在上述代码中,如果some_conditiontrue,函数会提前返回,delete p语句就永远不会执行,导致堆内存泄漏。如果这段代码在循环中执行,内存会持续增长,最终导致程序崩溃。

1.2 二次释放:致命的 "双重打击"

二次释放是指对同一块内存进行多次delete操作。这会破坏堆内存的完整性,导致程序崩溃或未定义行为。

cpp 复制代码
// 反面示例:裸指针导致的二次释放
void func() {
    int* p = new int(20);
    delete p; // 第一次释放
    // ... 中间经过复杂的逻辑,忘记p已经被释放
    delete p; // 第二次释放,程序崩溃
}

这种问题在多人协作或复杂代码中尤为常见 ------ 当一个指针被传递到多个函数后,很难追踪它是否已经被释放。

1.3 野指针:潜伏的 "幽灵"

野指针是指指向已释放内存或非法内存地址的指针。访问野指针会导致程序崩溃、数据损坏等不可预测的结果。

cpp 复制代码
// 反面示例:裸指针导致的野指针问题
int* func() {
    int x = 10; // 栈内存,函数返回后会被销毁
    return &x; // 返回栈内存地址,形成野指针
}

int main() {
    int* p = func();
    cout << *p << endl; // 访问野指针,行为未定义(可能输出乱码或崩溃)
    return 0;
}

栈内存的生命周期与函数作用域绑定,函数返回后栈内存会被系统回收,此时返回的指针就成了野指针。

1.4 异常安全:被忽略的 "隐形杀手"

当程序发生异常时,正常的执行流程会被打断,可能导致裸指针无法被释放。

cpp 复制代码
// 反面示例:异常导致的内存泄漏
void func() {
    int* p = new int(30);
    try {
        // 模拟抛出异常
        throw runtime_error("something wrong");
    } catch (...) {
        // 未处理p的释放,导致内存泄漏
        throw; // 重新抛出异常
    }
    delete p; // 永远不会执行
}

即使我们小心谨慎地处理了所有正常路径,异常也可能成为 "漏网之鱼",导致内存泄漏。

1.5 智能指针的核心使命

面对裸指针的种种问题,智能指针的核心设计思想应运而生:将指针的生命周期管理与对象的生命周期绑定,通过 RAII(资源获取即初始化)机制,实现内存的自动释放

简单来说,智能指针是一个**"包装器类"** ,它封装了裸指针,并在其析构函数中自动执行delete操作。由于 C++ 的对象生命周期遵循 "出作用域即析构" 的规则,当智能指针对象离开作用域时,析构函数会自动调用,从而保证内存被正确释放,从根本上避免了内存泄漏、二次释放等问题。

二、智能指针的 "三驾马车":unique_ptr、shared_ptr、weak_ptr

C++11 标准库提供了三种核心智能指针:unique_ptr、shared_ptr和weak_ptr。它们各自有着不同的设计理念和适用场景,共同构成了 C++ 内存管理的 "主力军"。

2.1 unique_ptr:独占所有权的 "独行侠"

unique_ptr是最简单、最高效的智能指针,它的核心特性是独占所有权 ------ 同一时间,只能有一个unique_ptr指向一块内存。当unique_ptr对象被销毁时,它所指向的内存也会被自动释放。

2.1.1 unique_ptr 的核心原理

unique_ptr的底层实现非常简洁:

  1. 封装一个裸指针(T ptr*);
  2. 禁用拷贝构造函数和拷贝赋值运算符(C++11 中通过**= delete**实现),确保所有权无法被复制;
  3. 支持移动构造函数和移动赋值运算符,允许所有权的 "转移";
  4. 析构函数中调用delete(或delete[],针对数组类型)释放内存。

我们可以通过一个简化版的unique_ptr来理解其原理:

cpp 复制代码
// 简化版unique_ptr实现(仅演示核心逻辑)
template <typename T>
class MyUniquePtr {
private:
    T* ptr; // 封装的裸指针

public:
    // 构造函数:接收裸指针
    explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}

    // 析构函数:自动释放内存
    ~MyUniquePtr() {
        delete ptr; // 核心:自动delete
        ptr = nullptr;
    }

    // 禁用拷贝构造(独占所有权,不允许复制)
    MyUniquePtr(const MyUniquePtr& other) = delete;

    // 禁用拷贝赋值
    MyUniquePtr& operator=(const MyUniquePtr& other) = delete;

    // 移动构造:转移所有权
    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr; // 原指针置空,避免二次释放
    }

    // 移动赋值:转移所有权
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr; // 释放当前指针指向的内存
            ptr = other.ptr; // 接收对方的指针
            other.ptr = nullptr; // 原指针置空
        }
        return *this;
    }

    // 重载->运算符,支持指针访问语法
    T* operator->() const { return ptr; }

    // 重载*运算符,支持解引用
    T& operator*() const { return *ptr; }

    // 获取裸指针(谨慎使用)
    T* get() const { return ptr; }

    // 释放所有权(返回裸指针,智能指针不再管理该内存)
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }

    // 重置指针(释放当前内存,指向新地址)
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }
};

从简化实现可以看出,unique_ptr通过禁用拷贝、支持移动,确保了所有权的独占性,同时通过析构函数自动释放内存,实现了 "零泄漏" 的保证。

2.1.2 unique_ptr 的基本使用

unique_ptr的使用非常直观,以下是常见操作示例:

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

class Test {
public:
    Test(int id) : id_(id) {
        cout << "Test(" << id_ << ") 构造" << endl;
    }
    ~Test() {
        cout << "Test(" << id_ << ") 析构" << endl;
    }
    void show() {
        cout << "Test id: " << id_ << endl;
    }
private:
    int id_;
};

// 1. 基本初始化与使用
void test_unique_ptr_basic() {
    cout << "=== test_unique_ptr_basic ===" << endl;
    // 方式1:通过make_unique创建(推荐,更安全)
    unique_ptr<Test> up1 = make_unique<Test>(1);
    up1->show(); // 调用成员函数
    cout << "up1 get: " << up1.get() << endl; // 获取裸指针

    // 方式2:通过new创建(不推荐,可能导致内存泄漏)
    unique_ptr<Test> up2(new Test(2));
    up2->show();

    // 错误:不允许拷贝构造
    // unique_ptr<Test> up3 = up1; 

    // 错误:不允许拷贝赋值
    // unique_ptr<Test> up4;
    // up4 = up1;

    // 正确:移动构造(转移所有权)
    unique_ptr<Test> up5 = move(up1);
    up5->show();
    cout << "up1 get after move: " << up1.get() << endl; // up1变为nullptr

    // 正确:移动赋值(转移所有权)
    unique_ptr<Test> up6;
    up6 = move(up2);
    up6->show();
    cout << "up2 get after move: " << up2.get() << endl; // up2变为nullptr

    // 重置指针(释放当前内存,指向新对象)
    up5.reset(new Test(5));
    up5->show();

    // 释放所有权(up6不再管理该内存,需手动释放)
    Test* raw_ptr = up6.release();
    delete raw_ptr; // 必须手动delete,否则内存泄漏

    cout << "=== test_unique_ptr_basic end ===" << endl;
}

// 2. 管理数组(需指定数组类型)
void test_unique_ptr_array() {
    cout << "\n=== test_unique_ptr_array ===" << endl;
    // 管理int数组(注意模板参数为int[])
    unique_ptr<int[]> up_arr(new int[5]{10, 20, 30, 40, 50});
    for (int i = 0; i < 5; ++i) {
        cout << up_arr[i] << " "; // 支持[]运算符
    }
    cout << endl;
    // 析构时会自动调用delete[],无需手动释放
    cout << "=== test_unique_ptr_array end ===" << endl;
}

// 3. 作为函数返回值(自动移动,无需显式调用move)
unique_ptr<Test> create_test(int id) {
    return make_unique<Test>(id); // 编译器自动优化为移动语义
}

void test_unique_ptr_return() {
    cout << "\n=== test_unique_ptr_return ===" << endl;
    unique_ptr<Test> up = create_test(10);
    up->show();
    cout << "=== test_unique_ptr_return end ===" << endl;
}

int main() {
    test_unique_ptr_basic();
    test_unique_ptr_array();
    test_unique_ptr_return();
    return 0;
}

运行结果如下:

复制代码
=== test_unique_ptr_basic ===
Test(1) 构造
Test id: 1
up1 get: 0x7f8b4a405a00
Test(2) 构造
Test id: 2
Test id: 1
up1 get after move: 0x0
Test id: 2
up2 get after move: 0x0
Test(5) 构造
Test id: 5
Test(2) 析构
Test(5) 析构
=== test_unique_ptr_basic end ===

=== test_unique_ptr_array ===
10 20 30 40 50 
=== test_unique_ptr_array end ===

=== test_unique_ptr_return ===
Test(10) 构造
Test id: 10
=== test_unique_ptr_return end ===
Test(10) 析构

从运行结果可以看出:

  • unique_ptr对象离开作用域时,所指向的Test对象会自动析构;
  • 移动操作后,原unique_ptr会被置空,避免二次释放;
  • 管理数组时,析构函数会自动调用delete[],无需手动处理。

2.1.3 unique_ptr 的使用场景与最佳实践

unique_ptr是最常用的智能指针,适用于以下场景:

  1. 独占资源所有权 :当一块内存只需要被一个指针管理时,优先使用unique_ptr
  2. 作为函数参数 / 返回值:传递临时对象的所有权(通过移动语义);
  3. 管理局部动态对象:替代裸指针,避免函数退出时忘记释放内存;
  4. 容器元素 :**vector<unique_ptr<T>>**是常见用法,避免容器元素的拷贝开销。

最佳实践:

  • 优先使用make_unique创建unique_ptrmake_unique是 C++14 引入的函数,它能避免直接使用new,减少内存泄漏风险(例如make_unique<Test>(1)unique_ptr<Test>(new Test(1))更安全);
  • 避免手动调用get()release() :这些函数会暴露裸指针,可能破坏unique_ptr的所有权管理,仅在必要时使用;
  • 管理数组时指定数组类型 :使用unique_ptr<T[]>而非unique_ptr<T>,确保析构时调用delete[]

2.2 shared_ptr:共享所有权的 "社交达人"

unique_ptr的独占性虽然高效,但无法满足 "多个指针共享同一块内存" 的场景(例如,多个对象需要引用同一个资源)。此时,shared_ptr应运而生 ------ 它支持共享所有权 ,多个shared_ptr可以指向同一块内存,当最后一个shared_ptr被销毁时,内存才会被释放。

2.2.1 shared_ptr 的核心原理:引用计数

shared_ptr的核心机制是引用计数(Reference Counting)

  1. 每个shared_ptr都封装了一个**"数据指针"**(指向实际对象)和一个 "控制块指针"(指向控制块);
  2. 控制块中存储了引用计数(当前指向该对象的shared_ptr数量)、弱引用计数 (当前指向该对象的weak_ptr数量)以及对象的析构器等信息;
  3. 当创建一个新的shared_ptr指向对象时,引用计数加 1;
  4. shared_ptr被销毁(析构)或指向其他对象(赋值)时,引用计数减 1;
  5. 当引用计数减为 0 时,控制块会调用析构器释放对象内存,随后释放控制块本身。

· 我们可以通过简化版的shared_ptr理解其原理:

cpp 复制代码
// 简化版shared_ptr实现(仅演示核心逻辑)
template <typename T>
class MySharedPtr {
private:
    T* data_ptr; // 指向实际对象的指针
    struct ControlBlock {
        int ref_count; // 引用计数
        int weak_count; // 弱引用计数(为weak_ptr预留)
        T* obj_ptr; // 指向对象的指针

        ControlBlock(T* p) : ref_count(1), weak_count(0), obj_ptr(p) {}

        ~ControlBlock() {
            delete obj_ptr; // 释放对象内存
        }
    };
    ControlBlock* control_block; // 指向控制块的指针

    // 减少引用计数,必要时释放控制块
    void decrement_ref_count() {
        if (control_block) {
            control_block->ref_count--;
            // 引用计数为0,且弱引用计数也为0时,释放控制块
            if (control_block->ref_count == 0) {
                if (control_block->weak_count == 0) {
                    delete control_block;
                } else {
                    // 弱引用存在时,仅释放对象,不释放控制块
                    control_block->obj_ptr = nullptr;
                }
            }
            control_block = nullptr;
            data_ptr = nullptr;
        }
    }

public:
    // 构造函数:创建新的控制块
    explicit MySharedPtr(T* p = nullptr) : data_ptr(p) {
        if (p) {
            control_block = new ControlBlock(p);
        } else {
            control_block = nullptr;
        }
    }

    // 析构函数:减少引用计数
    ~MySharedPtr() {
        decrement_ref_count();
    }

    // 拷贝构造函数:共享控制块,引用计数加1
    MySharedPtr(const MySharedPtr& other) {
        data_ptr = other.data_ptr;
        control_block = other.control_block;
        if (control_block) {
            control_block->ref_count++;
        }
    }

    // 拷贝赋值运算符:先减少当前引用计数,再共享新的控制块
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            decrement_ref_count(); // 释放当前资源
            data_ptr = other.data_ptr;
            control_block = other.control_block;
            if (control_block) {
                control_block->ref_count++;
            }
        }
        return *this;
    }

    // 移动构造函数:转移所有权,不修改引用计数
    MySharedPtr(MySharedPtr&& other) noexcept {
        data_ptr = other.data_ptr;
        control_block = other.control_block;
        other.data_ptr = nullptr;
        other.control_block = nullptr;
    }

    // 移动赋值运算符:转移所有权
    MySharedPtr& operator=(MySharedPtr&& other) noexcept {
        if (this != &other) {
            decrement_ref_count();
            data_ptr = other.data_ptr;
            control_block = other.control_block;
            other.data_ptr = nullptr;
            other.control_block = nullptr;
        }
        return *this;
    }

    // 重载->和*运算符
    T* operator->() const { return data_ptr; }
    T& operator*() const { return *data_ptr; }

    // 获取引用计数
    int use_count() const {
        return control_block ? control_block->ref_count : 0;
    }

    // 获取裸指针
    T* get() const { return data_ptr; }
};

从简化实现可以看出,shared_ptr的核心是控制块和引用计数。多个shared_ptr通过共享同一个控制块,实现了引用计数的同步更新,从而保证了共享所有权的正确性。

2.2.2 shared_ptr 的基本使用

shared_ptr的使用方式与unique_ptr类似,但支持拷贝操作。以下是常见操作示例:

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

class Test {
public:
    Test(int id) : id_(id) {
        cout << "Test(" << id_ << ") 构造" << endl;
    }
    ~Test() {
        cout << "Test(" << id_ << ") 析构" << endl;
    }
    void show() {
        cout << "Test id: " << id_ << ", use_count: " << sp_self->use_count() << endl;
    }
    // 持有自身的shared_ptr(用于演示循环引用)
    shared_ptr<Test> sp_self;
private:
    int id_;
};

// 1. 基本初始化与拷贝
void test_shared_ptr_basic() {
    cout << "=== test_shared_ptr_basic ===" << endl;
    // 方式1:通过make_shared创建(推荐)
    shared_ptr<Test> sp1 = make_shared<Test>(1);
    cout << "sp1 use_count: " << sp1.use_count() << endl; // 1

    // 拷贝构造:引用计数加1
    shared_ptr<Test> sp2 = sp1;
    cout << "sp1 use_count after copy: " << sp1.use_count() << endl; // 2
    cout << "sp2 use_count after copy: " << sp2.use_count() << endl; // 2

    // 拷贝赋值:引用计数加1
    shared_ptr<Test> sp3;
    sp3 = sp1;
    cout << "sp1 use_count after assign: " << sp1.use_count() << endl; // 3

    // 移动构造:引用计数不变
    shared_ptr<Test> sp4 = move(sp1);
    cout << "sp1 use_count after move: " << sp1.use_count() << endl; // 0(sp1已置空)
    cout << "sp4 use_count after move: " << sp4.use_count() << endl; // 2(sp2和sp3仍在)

    // 重置指针:引用计数减1
    sp2.reset();
    cout << "sp2 reset, sp3 use_count: " << sp3.use_count() << endl; // 1(仅sp3和sp4)

    sp3.reset(new Test(2));
    cout << "sp3 reset to new Test, use_count: " << sp3.use_count() << endl; // 1

    cout << "=== test_shared_ptr_basic end ===" << endl;
    // sp4、sp3离开作用域,Test(1)和Test(2)会被析构
}

// 2. 共享所有权的实际场景
void test_shared_ptr_share() {
    cout << "\n=== test_shared_ptr_share ===" << endl;
    shared_ptr<Test> sp = make_shared<Test>(10);

    // 多个函数共享同一个Test对象
    auto func1 = [&]() {
        shared_ptr<Test> sp1 = sp;
        cout << "func1: ";
        sp1->show();
    };

    auto func2 = [&]() {
        shared_ptr<Test> sp2 = sp;
        cout << "func2: ";
        sp2->show();
    };

    func1();
    func2();
    cout << "main: sp use_count: " << sp.use_count() << endl; // 1(func1和func2的sp1、sp2已析构)

    cout << "=== test_shared_ptr_share end ===" << endl;
}

// 3. 循环引用问题(shared_ptr的致命缺陷)
void test_shared_ptr_cycle() {
    cout << "\n=== test_shared_ptr_cycle ===" << endl;
    shared_ptr<Test> sp1 = make_shared<Test>(100);
    shared_ptr<Test> sp2 = make_shared<Test>(200);

    // 循环引用:sp1持有sp2,sp2持有sp1
    sp1->sp_self = sp2;
    sp2->sp_self = sp1;

    cout << "sp1 use_count: " << sp1.use_count() << endl; // 2(sp1和sp2->sp_self)
    cout << "sp2 use_count: " << sp2.use_count() << endl; // 2(sp2和sp1->sp_self)

    // sp1和sp2离开作用域时,引用计数减为1(而非0),导致Test对象无法析构
    cout << "=== test_shared_ptr_cycle end ===" << endl;
}

int main() {
    test_shared_ptr_basic();
    test_shared_ptr_share();
    test_shared_ptr_cycle();
    return 0;
}

运行结果如下:

复制代码
=== test_shared_ptr_basic ===
Test(1) 构造
sp1 use_count: 1
sp1 use_count after copy: 2
sp2 use_count after copy: 2
sp1 use_count after assign: 3
sp1 use_count after move: 0
sp4 use_count after move: 2
sp2 reset, sp3 use_count: 1
Test(2) 构造
sp3 reset to new Test, use_count: 1
=== test_shared_ptr_basic end ===
Test(1) 析构
Test(2) 析构

=== test_shared_ptr_share ===
Test(10) 构造
func1: Test id: 10, use_count: 2
func2: Test id: 10, use_count: 2
main: sp use_count: 1
=== test_shared_ptr_share end ===
Test(10) 析构

=== test_shared_ptr_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 2
sp2 use_count: 2
=== test_shared_ptr_cycle end ===

从运行结果可以发现一个关键问题:test_shared_ptr_cycle函数结束后,Test(100)Test(200)并没有被析构!这就是shared_ptr的致命缺陷 ------循环引用

2.2.3 循环引用:shared_ptr 的 "阿喀琉斯之踵"

循环引用是指两个或多个shared_ptr互相持有对方的引用,导致它们的引用计数永远无法减为 0,从而造成内存泄漏。

test_shared_ptr_cycle中:

  1. sp1指向Test(100)sp2指向Test(200),初始引用计数均为 1;
  2. sp1->sp_self = sp2:Test(100)sp_self持有sp2sp2的引用计数变为 2;
  3. sp2->sp_self = sp1:Test(200)sp_self持有sp1sp1的引用计数变为 2;
  4. 函数结束时,sp1sp2离开作用域,它们的引用计数各减 1,变为 1;
  5. 此时,Test(100)sp_self仍持有sp2Test(200)sp_self仍持有sp1,引用计数无法减为 0,两个Test对象永远无法析构,造成内存泄漏。

为了解决循环引用问题 ,C++ 标准库引入了第三种智能指针 ------weak_ptr

2.3 weak_ptr:打破循环的 "旁观者"

weak_ptr是一种**"弱引用"智能指针,它的核心特性是不拥有对象的所有权** ,仅能观察shared_ptr所管理的对象。weak_ptr不会增加shared_ptr的引用计数,因此不会导致循环引用问题。

2.3.1 weak_ptr 的核心原理

weak_ptr的底层依赖shared_ptr的控制块:

  1. weak_ptr仅存储控制块的指针,不存储数据指针;
  2. 创建weak_ptr时,会将控制块的弱引用计数加 1;
  3. weak_ptr无法直接访问对象,必须通过lock()方法获取一个shared_ptr(此时引用计数加 1),才能访问对象;
  4. shared_ptr的引用计数减为 0 时,对象会被析构,但控制块会保留到弱引用计数也为 0 时才释放;
  5. weak_ptr可以通过**expired()**方法判断所观察的对象是否已被析构。

我们可以通过简化版的weak_ptr理解其原理:

cpp 复制代码
// 简化版weak_ptr实现(与MySharedPtr配套)
template <typename T>
class MyWeakPtr {
private:
    typename MySharedPtr<T>::ControlBlock* control_block; // 仅指向控制块

public:
    // 默认构造
    MyWeakPtr() : control_block(nullptr) {}

    // 从shared_ptr构造:弱引用计数加1
    MyWeakPtr(const MySharedPtr<T>& sp) {
        control_block = sp.control_block;
        if (control_block) {
            control_block->weak_count++;
        }
    }

    // 拷贝构造
    MyWeakPtr(const MyWeakPtr& other) {
        control_block = other.control_block;
        if (control_block) {
            control_block->weak_count++;
        }
    }

    // 析构函数:弱引用计数减1
    ~MyWeakPtr() {
        if (control_block) {
            control_block->weak_count--;
            // 引用计数和弱引用计数均为0时,释放控制块
            if (control_block->ref_count == 0 && control_block->weak_count == 0) {
                delete control_block;
            }
            control_block = nullptr;
        }
    }

    // 拷贝赋值
    MyWeakPtr& operator=(const MyWeakPtr& other) {
        if (this != &other) {
            // 释放当前弱引用
            if (control_block) {
                control_block->weak_count--;
                if (control_block->ref_count == 0 && control_block->weak_count == 0) {
                    delete control_block;
                }
            }
            // 指向新的控制块
            control_block = other.control_block;
            if (control_block) {
                control_block->weak_count++;
            }
        }
        return *this;
    }

    // 从shared_ptr赋值
    MyWeakPtr& operator=(const MySharedPtr<T>& sp) {
        // 释放当前弱引用
        if (control_block) {
            control_block->weak_count--;
            if (control_block->ref_count == 0 && control_block->weak_count == 0) {
                delete control_block;
            }
        }
        // 指向sp的控制块
        control_block = sp.control_block;
        if (control_block) {
            control_block->weak_count++;
        }
        return *this;
    }

    // 检查对象是否已过期(被析构)
    bool expired() const {
        return !control_block || control_block->ref_count == 0;
    }

    // 获取shared_ptr,用于访问对象
    MySharedPtr<T> lock() const {
        if (expired()) {
            return MySharedPtr<T>(nullptr); // 对象已过期,返回空shared_ptr
        }
        // 引用计数加1,返回shared_ptr
        return MySharedPtr<T>(control_block->obj_ptr, control_block);
    }

    // 获取弱引用计数
    int use_count() const {
        return control_block ? control_block->ref_count : 0;
    }
};

从简化实现可以看出,weak_ptr不直接管理对象内存,仅通过控制块的弱引用计数跟踪shared_ptr的状态。通过lock()方法,weak_ptr可以安全地获取shared_ptr来访问对象,避免了悬空指针问题。

2.3.2 weak_ptr 的基本使用与循环引用解决方案

weak_ptr不能单独使用,必须与shared_ptr配合。以下是其常见用法,重点演示如何解决循环引用问题:

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

class Test {
public:
    Test(int id) : id_(id) {
        cout << "Test(" << id_ << ") 构造" << endl;
    }
    ~Test() {
        cout << "Test(" << id_ << ") 析构" << endl;
    }
    void show() {
        cout << "Test id: " << id_ << endl;
    }
    // 将shared_ptr改为weak_ptr,打破循环引用
    weak_ptr<Test> wp_self;
private:
    int id_;
};

// 1. weak_ptr的基本操作
void test_weak_ptr_basic() {
    cout << "=== test_weak_ptr_basic ===" << endl;
    shared_ptr<Test> sp = make_shared<Test>(1);

    // 从shared_ptr创建weak_ptr
    weak_ptr<Test> wp = sp;
    cout << "wp use_count: " << wp.use_count() << endl; // 1(引用计数,weak_ptr不影响)
    cout << "wp expired: " << wp.expired() << endl; // false(对象未析构)

    // 通过lock()获取shared_ptr,访问对象
    if (auto sp1 = wp.lock()) {
        cout << "lock success: ";
        sp1->show();
        cout << "sp1 use_count: " << sp1.use_count() << endl; // 2(sp和sp1)
    } else {
        cout << "object has been destroyed" << endl;
    }

    // 重置shared_ptr,对象析构
    sp.reset();
    cout << "sp reset, wp expired: " << wp.expired() << endl; // true(对象已析构)

    // 再次lock(),返回空shared_ptr
    if (auto sp2 = wp.lock()) {
        sp2->show();
    } else {
        cout << "lock failed: object has been destroyed" << endl;
    }

    cout << "=== test_weak_ptr_basic end ===" << endl;
}

// 2. 解决循环引用问题
void test_weak_ptr_solve_cycle() {
    cout << "\n=== test_weak_ptr_solve_cycle ===" << endl;
    shared_ptr<Test> sp1 = make_shared<Test>(100);
    shared_ptr<Test> sp2 = make_shared<Test>(200);

    // 用weak_ptr代替shared_ptr,避免循环引用
    sp1->wp_self = sp2; // wp_self是weak_ptr,不增加sp2的引用计数
    sp2->wp_self = sp1; // 同理,不增加sp1的引用计数

    cout << "sp1 use_count: " << sp1.use_count() << endl; // 1(仅sp1)
    cout << "sp2 use_count: " << sp2.use_count() << endl; // 1(仅sp2)

    // 通过lock()访问对方
    if (auto sp = sp1->wp_self.lock()) {
        cout << "sp1 access sp2: ";
        sp->show();
    }

    cout << "=== test_weak_ptr_solve_cycle end ===" << endl;
    // sp1和sp2离开作用域,引用计数减为0,Test对象被析构
}

// 3. weak_ptr的其他应用场景:观察者模式
class Subject; // 前向声明

// 观察者类(弱引用主题,避免循环引用)
class Observer {
public:
    Observer(const shared_ptr<Subject>& subject) : wp_subject(subject) {}
    void update() {
        if (auto sp_subject = wp_subject.lock()) {
            cout << "Observer update: subject state = " << sp_subject->getState() << endl;
        } else {
            cout << "Subject has been destroyed" << endl;
        }
    }
private:
    weak_ptr<Subject> wp_subject; // 弱引用主题
};

// 主题类(持有观察者的shared_ptr)
class Subject {
public:
    void setState(int state) {
        state_ = state;
        notifyObservers();
    }
    int getState() const { return state_; }
    void addObserver(const shared_ptr<Observer>& observer) {
        observers_.push_back(observer);
    }
private:
    void notifyObservers() {
        for (auto& observer : observers_) {
            observer->update();
        }
    }
    int state_;
    vector<shared_ptr<Observer>> observers_;
};

void test_weak_ptr_observer() {
    cout << "\n=== test_weak_ptr_observer ===" << endl;
    shared_ptr<Subject> subject = make_shared<Subject>();

    // 创建观察者(持有主题的弱引用)
    shared_ptr<Observer> observer1 = make_shared<Observer>(subject);
    shared_ptr<Observer> observer2 = make_shared<Observer>(subject);

    subject->addObserver(observer1);
    subject->addObserver(observer2);

    // 主题状态变化,通知观察者
    subject->setState(10);
    subject->setState(20);

    // 主题被销毁
    subject.reset();
    cout << "After subject reset:" << endl;
    observer1->update(); // 输出"Subject has been destroyed"

    cout << "=== test_weak_ptr_observer end ===" << endl;
}

int main() {
    test_weak_ptr_basic();
    test_weak_ptr_solve_cycle();
    test_weak_ptr_observer();
    return 0;
}

运行结果如下:

复制代码
=== test_weak_ptr_basic ===
Test(1) 构造
wp use_count: 1
wp expired: 0
lock success: Test id: 1
sp1 use_count: 2
sp reset, wp expired: 1
lock failed: object has been destroyed
Test(1) 析构
=== test_weak_ptr_basic end ===

=== test_weak_ptr_solve_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 1
sp2 use_count: 1
sp1 access sp2: Test id: 200
=== test_weak_ptr_solve_cycle end ===
Test(200) 析构
Test(100) 析构

=== test_weak_ptr_observer ===
Observer update: subject state = 10
Observer update: subject state = 10
Observer update: subject state = 20
Observer update: subject state = 20
After subject reset:
Subject has been destroyed
=== test_weak_ptr_observer end ===

从运行结果可以看出:

  • weak_ptr不会增加shared_ptr的引用计数,因此不会导致循环引用;
  • 通过lock()方法可以安全地获取shared_ptr,访问对象前会检查对象是否已析构;
  • 在观察者模式中,weak_ptr可以避免观察者和主题之间的循环引用,同时允许主题被正常销毁。

2.3.3 weak_ptr 的使用场景与最佳实践

weak_ptr的使用场景主要包括:

  1. 解决shared_ptr的循环引用问题 :这是weak_ptr最核心的用途,将循环引用中的一方改为weak_ptr即可;
  2. 观察者模式 :观察者持有主题的weak_ptr,避免主题被观察者 "绑架"(即使有观察者,主题也能正常销毁);
  3. 缓存场景 :缓存对象的weak_ptr,当对象被销毁时,缓存自动失效,避免悬空指针。

最佳实践:

  • 不单独使用weak_ptrweak_ptr必须与shared_ptr配合使用,无法直接访问对象;
  • 使用lock()前先检查expired() :虽然**lock()会返回空指针,但提前检查expired()**可以提高代码可读性;
  • 避免长期持有lock()返回的shared_ptrlock()返回的shared_ptr会增加引用计数,长期持有会导致对象无法及时析构。

三、智能指针的 "进阶技巧":定制删除器、类型转换与性能优化

除了基本用法,智能指针还有一些进阶技巧,可以应对更复杂的场景。

3.1 定制删除器:处理特殊资源释放

默认情况下,智能指针会使用delete(或delete[])释放内存。但在某些场景下,我们需要自定义释放逻辑(例如,释放动态数组、文件句柄、网络连接等),此时可以使用 "定制删除器"。

3.1.1 unique_ptr 的定制删除器

unique_ptr的定制删除器是模板参数的一部分,语法如下:

cpp 复制代码
#include <memory>
#include <iostream>
#include <fstream>
using namespace std;

// 1. 释放动态数组(unique_ptr<T[]>已支持,此处仅为示例)
void delete_array(int* p) {
    cout << "Custom deleter: delete[] array" << endl;
    delete[] p;
}

// 2. 释放文件句柄
void close_file(FILE* fp) {
    if (fp) {
        cout << "Custom deleter: close file" << endl;
        fclose(fp);
    }
}

// 3. lambda表达式作为删除器(更简洁)
auto delete_lambda = [](Test* p) {
    cout << "Custom deleter (lambda): delete Test(" << p->id_ << ")" << endl;
    delete p;
};

void test_custom_deleter_unique() {
    cout << "=== test_custom_deleter_unique ===" << endl;

    // 1. 函数指针作为删除器
    unique_ptr<int, decltype(&delete_array)> up1(new int[5]{1,2,3,4,5}, delete_array);

    // 2. 释放文件句柄
    FILE* fp = fopen("test.txt", "w");
    unique_ptr<FILE, decltype(&close_file)> up2(fp, close_file);

    // 3. lambda表达式作为删除器
    unique_ptr<Test, decltype(delete_lambda)> up3(new Test(10), delete_lambda);

    cout << "=== test_custom_deleter_unique end ===" << endl;
}

运行结果:

复制代码
=== test_custom_deleter_unique ===
Test(10) 构造
=== test_custom_deleter_unique end ===
Custom deleter (lambda): delete Test(10)
Custom deleter: close file
Custom deleter: delete[] array

3.1.2 shared_ptr 的定制删除器

shared_ptr的定制删除器是构造函数的参数,语法更灵活

cpp 复制代码
void test_custom_deleter_shared() {
    cout << "\n=== test_custom_deleter_shared ===" << endl;

    // 1. 函数指针作为删除器
    shared_ptr<int> sp1(new int[5]{1,2,3,4,5}, [](int* p) {
        cout << "Custom deleter (shared): delete[] array" << endl;
        delete[] p;
    });

    // 2. 释放文件句柄
    FILE* fp = fopen("test.txt", "w");
    shared_ptr<FILE> sp2(fp, close_file);

    // 3. 成员函数作为删除器
    class FileDeleter {
    public:
        void operator()(FILE* fp) {
            if (fp) {
                cout << "Custom deleter (member function): close file" << endl;
                fclose(fp);
            }
        }
    };
    shared_ptr<FILE> sp3(fopen("test2.txt", "w"), FileDeleter());

    cout << "=== test_custom_deleter_shared end ===" << endl;
}

运行结果:

复制代码
=== test_custom_deleter_shared ===
=== test_custom_deleter_shared end ===
Custom deleter (shared): delete[] array
Custom deleter: close file
Custom deleter (member function): close file

3.2 智能指针的类型转换

裸指针可以通过static_castdynamic_cast等进行类型转换,但智能指针不能直接使用这些运算符。C++ 标准库提供了专门的类型转换函数:

  • static_pointer_cast:对应static_cast,用于静态类型转换;
  • dynamic_pointer_cast:对应dynamic_cast,用于动态类型转换(支持运行时类型检查);
  • const_pointer_cast:对应const_cast,用于去除const限定。

示例代码:

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() { cout << "Base show" << endl; }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void show() override { cout << "Derived show" << endl; }
    void derived_func() { cout << "Derived specific function" << endl; }
};

void test_pointer_cast() {
    cout << "=== test_pointer_cast ===" << endl;

    // 1. dynamic_pointer_cast(动态类型转换)
    shared_ptr<Base> sp_base = make_shared<Derived>();
    if (auto sp_derived = dynamic_pointer_cast<Derived>(sp_base)) {
        sp_derived->show(); // Derived show
        sp_derived->derived_func(); // Derived specific function
    } else {
        cout << "dynamic_cast failed" << endl;
    }

    // 2. static_pointer_cast(静态类型转换)
    shared_ptr<Base> sp_base2 = static_pointer_cast<Base>(make_shared<Derived>());
    sp_base2->show(); // Derived show

    // 3. const_pointer_cast(去除const限定)
    shared_ptr<const Base> sp_const = make_shared<Base>();
    auto sp_non_const = const_pointer_cast<Base>(sp_const);
    sp_non_const->show(); // Base show

    cout << "=== test_pointer_cast end ===" << endl;
}

int main() {
    test_pointer_cast();
    return 0;
}

运行结果:

复制代码
=== test_pointer_cast ===
Derived show
Derived specific function
Derived show
Base show
=== test_pointer_cast end ===

注意:

  • dynamic_pointer_cast仅对多态类型(含有虚函数)有效,非多态类型会编译失败;
  • 类型转换仅适用于shared_ptrunique_ptr不支持(因为unique_ptr的所有权独占,转换会破坏安全性)。

3.3 智能指针的性能优化

智能指针的性能开销主要来自两个方面:

  1. shared_ptr引用计数操作(原子操作,有一定开销);
  2. 控制块的内存分配(shared_ptr的控制块需要额外分配内存)。

3.3.1 优先使用 unique_ptr

unique_ptr的性能几乎与裸指针一致(无额外开销),因此在不需要共享所有权时,优先使用unique_ptr,而非shared_ptr

3.3.2 使用 make_shared 减少内存分配

make_shared会一次性分配对象和控制块的内存,而**shared_ptr<T>(new T())**会分两次分配(一次分配对象,一次分配控制块)。因此,make_shared不仅更安全,还能减少内存分配次数,提高性能。

cpp 复制代码
// 推荐:一次内存分配(对象+控制块)
auto sp1 = make_shared<Test>(1);

// 不推荐:两次内存分配(对象和控制块分开)
auto sp2 = shared_ptr<Test>(new Test(2));

3.3.3 避免不必要的 shared_ptr 拷贝

shared_ptr的拷贝会增加引用计数(原子操作),频繁拷贝会影响性能。如果仅需要临时访问对象,可通过const&传递shared_ptr,避免拷贝。

cpp 复制代码
// 推荐:传递const引用,避免拷贝
void func(const shared_ptr<Test>& sp) {
    sp->show();
}

// 不推荐:拷贝shared_ptr,增加引用计数
void func(shared_ptr<Test> sp) {
    sp->show();
}

3.3.4 合理使用 weak_ptr 的 lock ()

weak_ptr::lock()会返回shared_ptr,增加引用计数。如果仅需要短暂访问对象,应尽快释放lock()返回的shared_ptr,避免引用计数长期不为 0。

cpp 复制代码
// 推荐:短暂持有lock()返回的shared_ptr
if (auto sp = wp.lock()) {
    sp->show();
} // sp离开作用域,引用计数减1

// 不推荐:长期持有
auto sp = wp.lock();
// 长时间操作...
sp->show();

四、智能指针的 "避坑指南":常见错误与最佳实践总结

智能指针虽然强大,但如果使用不当,仍可能导致内存问题。以下为大家总结一下常见错误和最佳实践。

4.1 常见错误

  1. 将裸指针交给多个智能指针管理:导致二次释放。

    cpp 复制代码
    // 错误
    int* p = new int(10);
    unique_ptr<int> up(p);
    shared_ptr<int> sp(p); // 错误:p被up和sp同时管理,会二次释放
  2. 使用get()返回的裸指针创建智能指针:同样导致二次释放。

    cpp 复制代码
    // 错误
    shared_ptr<int> sp = make_shared<int>(10);
    int* p = sp.get();
    shared_ptr<int> sp2(p); // 错误:sp和sp2都管理p,会二次释放
  3. shared_ptr的循环引用 :导致内存泄漏(已通过weak_ptr解决)。

  4. unique_ptr的拷贝操作unique_ptr禁用拷贝,强行拷贝会编译失败。

    cpp 复制代码
    // 错误
    unique_ptr<int> up1 = make_unique<int>(10);
    unique_ptr<int> up2 = up1; // 编译失败:拷贝构造被禁用
  5. 管理数组时使用unique_ptr<T>而非unique_ptr<T[]> :导致析构时调用delete而非delete[],造成内存泄漏。

    cpp 复制代码
    // 错误
    unique_ptr<int> up(new int[5]); // 析构时调用delete,内存泄漏
    
    // 正确
    unique_ptr<int[]> up(new int[5]); // 析构时调用delete[]
  6. weak_ptr访问已过期的对象 :未检查expired()lock()返回的shared_ptr是否为空,导致访问空指针。

    cpp 复制代码
    // 错误
    weak_ptr<int> wp;
    {
        auto sp = make_shared<int>(10);
        wp = sp;
    }
    *wp.lock(); // 错误:wp已过期,lock()返回空指针,解引用崩溃

4.2 最佳实践总结

  1. 优先使用智能指针,替代裸指针:除了极少数场景(如与 C 语言兼容),应尽量使用智能指针管理动态内存;
  2. 选择合适的智能指针
    • 独占所有权unique_ptr(优先选择,性能最优);
    • 共享所有权shared_ptr(配合weak_ptr解决循环引用);
    • 观察对象weak_ptr(不拥有所有权,解决循环引用);
  3. 优先使用make_sharedmake_unique :避免直接使用new,减少内存泄漏风险,提高性能;
  4. 不混合使用裸指针和智能指针:避免二次释放和内存泄漏;
  5. 避免get()release()的滥用:仅在必要时使用,使用后确保裸指针的生命周期不超过智能指针;
  6. shared_ptr避免循环引用 :将循环引用中的一方改为weak_ptr
  7. unique_ptr管理数组时指定T[] :确保析构时调用delete[]
  8. weak_ptr访问对象前检查有效性 :通过expired()lock()返回的shared_ptr是否为空判断。

总结

从裸指针的 "步步惊心" 到智能指针的 "自动驾驶",C++ 的内存管理经历了一次革命性的进化。智能指针通过 RAII 机制,将内存管理的责任从开发者转移到编译器,从根本上解决了内存泄漏、二次释放、野指针等经典问题。

掌握智能指针的使用及其原理,不仅能让你的代码更安全、更高效,还能让你深刻理解 C++ 的 RAII 设计思想。在实际开发中,应遵循 "能⽤智能指针就不用裸指针" 的原则,合理选择合适的智能指针,让内存管理不再成为你的 "痛点",而是你的 "底气"。

最后,希望大家能记住一句话:智能指针不是银弹,但它是 C++ 内存管理最强大的武器。用好它,你就能在 C++ 的世界里 "畅行无阻"

相关推荐
努力学习的小廉1 小时前
【QT(二)】—— 初识QT
开发语言·qt
爱学习的小邓同学1 小时前
C++ --- map/set的使用
开发语言·c++
weixin_421133411 小时前
JShielder
开发语言
MSTcheng.1 小时前
【C++进阶】继承(下)——挖掘继承深处的奥秘!
开发语言·c++
RisunJan1 小时前
【HarmonyOS】鸿蒙开发语言的选择
开发语言·华为·harmonyos
学困昇1 小时前
Linux基础开发工具(上):从包管理到“进度条”项目实战,掌握 yum/vim/gcc 核心工具
linux·运维·开发语言·数据结构·c++·vim
雨落在了我的手上1 小时前
C语言入门(二十五):自定义类型:结构体
c语言·开发语言
Yan-英杰1 小时前
openEuler 25.09 VM虚拟机实测:性能与安全双维度测评
服务器·开发语言·科技·ai·大模型
兩尛1 小时前
HJ52 计算字符串的编辑距离
java·开发语言·算法