C++11 :包装器与智能指针

一、 包装器 (std::function)

std::function 是一个极其强大的类模板,它可以统一包装和存储 C++ 中各种各样的“可调用对象”(包括:函数指针、仿函数、Lambda 表达式、类的成员函数等)。

核心优势: 消除不同可调用对象的类型差异,提供统一的调用签名。若调用空的 std::function,会抛出 std::bad_function_call 异常。

1. 基础用法与类型统一

#include <iostream>
#include <functional>
using namespace std;

int f(int a, int b) { return a + b; } 

struct Functor {
    int operator() (int a, int b) { return a + b; }
};

int main() {
    // 统一包装三种不同的可调用对象
    function<int(int, int)> f1 = f;                        // 1. 函数指针
    function<int(int, int)> f2 = Functor();                // 2. 仿函数对象
    function<int(int, int)> f3 = [](int a, int b) { return a + b; }; // 3. Lambda

    cout << f1(1, 2) << endl;
    cout << f2(1, 3) << endl;
    cout << f3(1, 4) << endl;
    return 0;
}

装类的成员函数 (静态与非静态)

包装普通成员函数时,必须处理隐藏的 this 指针,std::function 极其灵活,支持传递对象指针、对象本体或右值。

class Plus {
public:
    Plus(int n = 10) : _n(n) {}
    static int plusi(int a, int b) { return a + b; }
    double plusd(double a, double b) { return (a + b) * _n; }
private:
    int _n;
};

int main() {
    // 包装静态成员函数(需要 & 获取地址)
    function<int(int, int)> f4 = &Plus::plusi;

    // 包装普通成员函数(必须带有类类型参数,作为 this 指针)
    function<double(Plus*, double, double)> f5 = &Plus::plusd;
    Plus pd;
    cout << f5(&pd, 1.1, 2.2) << endl; // 传指针

    function<double(Plus, double, double)> f6 = &Plus::plusd;
    cout << f6(pd, 2.2, 3.3) << endl;  // 传左值对象

    function<double(Plus&&, double, double)> f7 = &Plus::plusd;
    cout << f7(Plus(), 2.2, 3.3) << endl; // 传右值/临时对象
    return 0;
}

二、 智能指针与 RAII 思想

1. 裸指针带来的异常安全问题

在复杂的业务逻辑中,如果使用了裸指针并遭遇了异常抛出,很容易导致执行流直接跳过 delete,造成内存泄漏。

void fun1() {
    int* a1 = new int[10];
    try {
        ProcessData(); // 如果抛出异常,控制流跳走
        delete[] a1;   // 永远不会执行,导致严重内存泄漏!
    } catch (...) { throw; }
}

2. RAII 思想

RAII (Resource Acquisition Is Initialization,资源获取即初始化):利用 C++ 局部对象在生命周期结束时自动调用析构函数的特性,将获取到的裸资源委托给一个类对象管理。保障了无论代码是正常结束还是因为异常栈展开而结束,资源都能被安全释放。


三、 C++ 标准库三大智能指针

标准库智能指针均包含在 <memory> 头文件中。

1. auto_ptr (C++98 遗留,已在 C++17 废除)

  • 特性:拷贝时进行控制权转移

  • 致命缺陷:拷贝后原对象会被静默置空(悬空状态),极易导致意外的空指针访问崩溃。现代 C++ 强烈禁止使用。

2. unique_ptr (唯一指针)

  • 特性独占资源,严格禁止拷贝构造和拷贝赋值。

  • 转移权:仅支持移动语义 (std::move)。移动后,原指针明确悬空,强制程序员对控制权转移保持清醒。

unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 编译报错,不支持拷贝
unique_ptr<Date> up2(move(up1)); //  支持移动,up1 资源被合法掠夺

3. shared_ptr (共享指针)

  • 特性:支持拷贝和移动,允许多个指针共享同一块资源。

  • 原理:底层采用引用计数 (Reference Counting)。析构时,如果计数减到 0,才真正 delete 资源。

shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
cout << sp1.use_count() << endl;  // 输出 2

四、 智能指针的核心原理与手写实现

对于 shared_ptr,要实现共享,最难的是引用计数存在哪

  • int _count;(每个对象独立计数,无法同步)

  • static int _count;(所有 shared_ptr 实例全局共享,导致不同资源的计数混乱)

  • int* _pcount;(堆上动态开辟计数器):随资源一并分配,确保同一块资源的管理者共享同一个计数器。

核心源码

namespace xuan {
    // ==========================================
    // 1. unique_ptr 实现
    // ==========================================
    template<class T>
    class unique_ptr {
    public:
        unique_ptr(T* ptr) : _ptr(ptr) {}
        ~unique_ptr() { delete _ptr; }

        // 核心:强制删除拷贝控制函数
        unique_ptr(const unique_ptr<T>&) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

        // 提供移动构造和移动赋值
        unique_ptr(unique_ptr<T>&& up) : _ptr(up._ptr) {
            up._ptr = nullptr;
        }

        unique_ptr<T>& operator=(unique_ptr<T>&& up) {
            if (this != &up) {
                delete _ptr;
                _ptr = up._ptr;
                up._ptr = nullptr;
            }
            return *this;
        }

        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
    private:
        T* _ptr = nullptr;
    };

    // ==========================================
    // 2. shared_ptr 实现
    // ==========================================
    template<class T>
    class shared_ptr {
    public:
        shared_ptr(T* ptr) : _ptr(ptr), _pcount(new int(1)) {}

        // 拷贝构造:共享计数器,计数 +1
        shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {
            ++(*_pcount);
        }

        ~shared_ptr() { release(); }

        // 拷贝赋值
        shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
            if (_ptr != sp._ptr) {    // 防御自己给自己赋值
                release();            // 释放旧资源
                _ptr = sp._ptr;       // 接入新资源
                _pcount = sp._pcount;
                ++(*_pcount);         // 注意:必须先解引用,再自增!不能写 *_pcount++
            }
            return *this;
        }

        void release() {
            if (--(*_pcount) == 0) {
                delete _ptr;
                delete _pcount;
            }
        }

        size_t use_count() { return *_pcount; }
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }

    private:
        T* _ptr;
        int* _pcount;
    };
}

五、 定制删除器 (Custom Deleter)

智能指针默认使用 delete ptr 释放资源。如果我们管理的资源不是普通的 new 出来的(如 new[] 数组、FILE* 文件指针等),默认的析构行为会导致程序崩溃。

解决策略:传入一个可调用对象(仿函数、Lambda)作为定制删除器。

提示:对于 new[] 数组,标准库已经特化了 unique_ptr<T[]>shared_ptr<T[]> 版本,可以直接使用。

语法差异对比:

  • unique_ptr:删除器类型是模板参数的一部分。推荐使用仿函数decltype(lambda)

  • shared_ptr:删除器是构造函数参数。底层用类型擦除实现,推荐直接传入 Lambda 表达式

// unique_ptr: 仿函数作为模板参数
template<class T>
struct DeleteArray {
    void operator()(T* ptr) { delete[] ptr; }
};
unique_ptr<Date, DeleteArray<Date>> up1(new Date[5]);

// shared_ptr: Lambda 作为构造参数
shared_ptr<Date> sp1(new Date[5], [](Date* ptr) { delete[] ptr; });

六、 循环引用与 weak_ptr

1. 循环引用的产生 (Cyclic Reference)

在双向链表或父子树状结构的场景中,如果相互持有对方的 shared_ptr,会导致致命的内存泄漏。

struct ListNode {
    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;  
};

shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);

n1->_next = n2; // n2 引用计数变 2
n2->_prev = n1; // n1 引用计数变 2

后果: n1n2 作用域结束后,计数均只减为 1。n1 等待 n2 销毁,n2 等待 n1 销毁,形成逻辑死锁,导致永久内存泄漏。

2. weak_ptr

weak_ptr 是为了辅助 shared_ptr 而生的弱指针。它不参与资源管理(不支持 RAII),不增加强引用计数

struct ListNode {
    // 使用弱引用
    weak_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;  
};

3. weak_ptr 核心操作与底层控制块

由于 weak_ptr 不控制资源生死,它访问资源时必须确认资源是否还存活。

  • wp.expired():检查资源是否已销毁。

  • wp.lock():原子的获取一个存活的 shared_ptr(如果资源已销毁,返回空指针)。

底层揭秘:控制块 (Control Block) shared_ptr 实际上在堆上开辟了一个包含强引用计数弱引用计数的控制块。当强引用计数归零时,实际的资源对象被销毁。但只要还有 weak_ptr 存在(弱计数不为零),控制块本身就不会销毁。因此 weak_ptr 依然可以去控制块里读取强计数的状态,进而判断出 expired()


七、 补充机制

  • make_shared 函数:推荐使用 make_shared<T>(...) 初始化 shared_ptr。它将“资源对象”和“控制块”合并在一次动态内存分配中完成,既提高了性能,又有效减少了堆内存碎片。

  • explicit 构造:智能指针的单参构造函数通常带有 explicit 关键字,严格禁止裸指针到智能指针的隐式类型转换(例如禁止 shared_ptr<int> sp = new int(10); 这种危险写法)。

  • operator bool 支持:智能指针重载了 bool 类型转换符。可以直接用 if (sp) 来优雅地判断智能指针是否为空(即是否持有合法资源)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐