C++11:包装器,智能指针(AI优化版)
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
后果: n1 和 n2 作用域结束后,计数均只减为 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)来优雅地判断智能指针是否为空(即是否持有合法资源)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)