news 2026/9/13 23:56:28

智能指针的使用及其原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智能指针的使用及其原理

目录:

  • 1. 智能指针的使用
  • 2. RAII和智能指针
  • 3. C++标准库智能指针的使用
    • 3.1 auto_ptr
    • 3.2 unique_ptr
    • 3.3 shared_ptr
    • 3.4 weak_ptr
    • 3.5 make_shared
    • 3.6 explicit 修饰构造函数
  • 4. 删除器
    • 4.1 数组的特化版本
    • 4.2 自定义删除器

1. 智能指针的使用

只要是堆上 new 出来的资源,不想手动管理 delete,优先考虑智能指针;能用unique_ptr就不用shared_ptr

int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (const std::exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }
void Func() { int* array1 = new int[10]; int* array2 = new int[10]; // ⚠️重点:这一行有可能抛出异常! try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; //除0抛异常 } catch (...) { //释放两块内存,重新抛出异常 delete[] array1; delete[] array2; throw; } //正常执行路径释放 delete[] array1; delete[] array2; }

两个场景分析:

场景 1:Divide()函数抛出异常(除 0)

进入catch(...),执行delete[] array1、array2,内存正常释放。 👉 这个场景看起来没问题。

场景 2:int* array2 = new int[10];这里直接抛出异常(内存分配失败)

new分配内存失败,会抛出bad_alloc异常!

  1. array1 = new int[10]成功,内存分配出来;

  2. 执行 array2 的 new 时抛异常;

  3. 函数立刻终止,直接跳出 Func;

  4. 还没有进入 try 块!catch 捕获不到!

  5. array1指向的内存没有释放 → 内存泄漏!

使用 unique_ptr 改造:

#include <memory> void Func() { // 全部用智能指针管理 std::unique_ptr<int[]> array1 = std::make_unique<int[]>(10); std::unique_ptr<int[]> array2 = std::make_unique<int[]>(10); int len, time; cin >> len >> time; cout << Divide(len, time) << endl; // 不需要任何try-catch手动释放! }

分析两种异常情况

  1. array2 构造时 new 抛异常array1是局部栈对象,已经构造完成。发生异常,函数退出,array1 自动调用析构释放内存。
  2. Divide 函数抛异常 函数提前退出,array1array2都是局部对象,离开作用域自动析构,两块内存全部释放。

2. RAII和智能指针

RAII 全称:Resource Acquisition Is Initialization,资源获取即初始化。 它是 C++ 管理资源最核心的设计思想。

核心思想一句话概括:

把资源交给局部对象托管:对象构造时获取资源,对象析构时释放资源;依靠对象生命周期,保证资源一定会释放。

智能指针就是应用 RAII 思想封装裸指针的类模板。 只靠 RAII 还不够,我们希望它用起来像普通指针,所以还要重载运算符模拟指针行为。

两大关键点:

  1. 遵循 RAII:构造接收裸指针,析构执行delete自动释放内存;
  2. 重载运算符operator*operator->operator[],让智能指针拥有和裸指针一样的访问语法。

简易智能指针示例:

template<class T> class SmartPtr { public: // 构造函数:获取资源,接收new出来的裸指针(RAII第一步) SmartPtr(T* ptr) :_ptr(ptr) {} // 析构函数:释放资源(RAII核心) ~SmartPtr() { cout << "delete[] " << _ptr << endl; delete[] _ptr; } // 重载解引用 *sp T& operator*() { return *_ptr; } // 重载箭头 ->sp T* operator->() { return _ptr; } // 重载下标 [] ,支持数组访问 sp[i] T& operator[](size_t i) { return _ptr[i]; } private: T* _ptr; // 私有成员:封装裸指针 };
  1. template<class T>类模板:支持任意类型指针int*自定义类型*
  2. 私有成员T* _ptr封装裸指针,资源保存在对象内部;
  3. 构造函数:接收外部new出来的地址,完成资源获取
  4. 析构函数:对象销毁自动执行delete[]自动释放堆内存
  5. 三个运算符重载:对外提供和原生指针一模一样的使用方式。

3. C++标准库智能指针的使用

智能指针概览:

C++ 标准库提供了多种智能指针,除了weak_ptr外,它们都符合 RAII 行为。它们的主要区别在于拷贝时的思路不同

  1. auto_ptr(C++98):拷贝时转移管理权(已废弃)。

  2. unique_ptr(C++11):独占所有权,不支持拷贝,只支持移动。

  3. shared_ptr(C++11):共享所有权,基于引用计数。

  4. weak_ptr(C++11):弱引用,不控制生命周期,用于解决循环引用问题。

3.1 auto_ptr

auto_ptr是 C++98 设计出来的智能指针。它的特点是:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象

致命缺陷:
这是一个非常糟糕的设计。当发生拷贝时,原对象(被拷贝者)会被置空(悬空)。如果后续继续访问原对象,就会导致程序崩溃。

代码示例:

#include <iostream> #include <memory> using namespace std; struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { cout << "~Date()" << endl; } }; int main() { auto_ptr<Date> ap1(new Date); // 拷贝时,管理权限转移,被拷贝对象 ap1 悬空 auto_ptr<Date> ap2(ap1); // 空指针访问,ap1 对象已经悬空,下面代码会崩溃 // ap1->_year++; return 0; }

3.2 unique_ptr

unique_ptr翻译过来是“唯一指针”。它的特点非常鲜明:不支持拷贝,只支持移动

  • 适用场景:如果不需要拷贝的场景,非常建议使用它。

  • 移动语义:虽然不支持拷贝,但支持std::move移动。注意,移动后原对象也会悬空,所以使用移动要谨慎。

示例:

#include <iostream> #include <memory> using namespace std; struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { cout << "~Date()" << endl; } }; int main() { unique_ptr<Date> up1(new Date); // 不支持拷贝 // unique_ptr<Date> up2(up1); // 编译报错 // 支持移动,但是移动后 up1 也悬空 unique_ptr<Date> up3(move(up1)); return 0; }

3.3 shared_ptr

shared_ptr是“共享指针”。它的特点是支持拷贝,也支持移动。底层是通过引用计数的方式实现的。

  • 机制:每当有一个新的shared_ptr指向同一块资源,引用计数 +1;每当一个shared_ptr析构,引用计数 -1。当引用计数为 0 时,释放资源。

  • 适用场景:如果需要拷贝的场景,就需要使用它。

示例:

int main() { shared_ptr<Date> sp1(new Date); // 支持拷贝,引用计数增加 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); cout << sp1.use_count() << endl; // 输出 3 sp1->_year++; cout << sp1->_year << endl; // 输出 2 cout << sp2->_year << endl; // 输出 2 cout << sp3->_year << endl; // 输出 2 // 支持移动,但是移动后 sp1 也悬空 shared_ptr<Date> sp4(move(sp1)); return 0; }

3.4 weak_ptr

weak_ptr是“弱指针”。它完全不同于上面的智能指针。

  • 特点:不支持 RAII,意味着不能用它直接管理资源。它不增加引用计数。

  • 产生本质:是为了解决shared_ptr循环引用导致内存泄漏的问题。

weak_ptr 的核心价值:通常发生在两个对象互相持有对方的 shared_ptr 时,导致引用计数永远无法归零。

错误版本:

#include <iostream> #include <memory> struct Node { int val; std::shared_ptr<Node> next; // shared_ptr互相引用 Node(int v) : val(v) { std::cout << "构造 Node " << val << "\n"; } ~Node() { std::cout << "析构 Node " << val << "\n"; } }; int main() { auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2); a->next = b; b->next = a; // 循环引用! // main函数结束,a、b离开作用域 // 但是a、b内部互相持有shared_ptr,引用计数 != 0 // 析构函数不会调用 → 内存泄漏! return 0; }

weak_ptr修复版本:

#include <iostream> #include <memory> struct Node { int val; std::weak_ptr<Node> next; // weak_ptr 弱指针,不增加引用计数 Node(int v) : val(v) { std::cout << "构造 Node " << val << "\n"; } ~Node() { std::cout << "析构 Node " << val << "\n"; } }; int main() { auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2); a->next = b; b->next = a; // weak_ptr使用:lock() 尝试提升为shared_ptr if (auto sp = a->next.lock()) { std::cout << "a 的 next 值:" << sp->val << "\n"; } // main结束,a,b销毁;weak_ptr不影响计数,对象正常释放 return 0; }

3.5 make_shared

shared_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。

template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);

基本用法:

#include <memory> // 直接使用构造函数 std::shared_ptr<Foo> p1(new Foo(1, 2)); // 使用 make_shared(推荐) auto p2 = std::make_shared<Foo>(1, 2);

make_shared将参数完美转发给Foo的构造函数,返回值是一个shared_ptr<Foo>

优势:

  1. 内存分配次数少:直接构造shared_ptr需要两次内存分配(对象本身 + 控制块),而make_shared只分配一次(对象和控制块在同一块内存)。

  2. 异常安全:避免因参数求值顺序导致的内存泄漏。

int main() { // 直接构造 shared_ptr<Date> sp1(new Date(2024, 9, 11)); // 使用 make_shared 构造 shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11); // 配合 auto 使用更简洁 auto sp3 = make_shared<Date>(2024, 9, 11); return 0; }

3.6 explicit 修饰构造函数

shared_ptrunique_ptr的构造函数都使用了explicit修饰。这是为了防止普通指针隐式类型转换成智能指针对象。

#include <iostream> #include <memory> struct Date { int _year; Date(int year = 1) : _year(year) {} ~Date() { std::cout << "~Date()" << std::endl; } }; void takeSmartPtr(std::shared_ptr<Date> sp) { std::cout << "管理权已移交,当前引用计数: " << sp.use_count() << std::endl; } int main() { // 错误:禁止隐式转换 (编译报错) // shared_ptr<Date> sp1 = new Date(); // takeSmartPtr(new Date()); // 正确:必须显式调用构造函数 std::shared_ptr<Date> sp2(new Date()); takeSmartPtr(sp2); // 正确:使用 make_shared (更加推荐) auto sp3 = std::make_shared<Date>(); takeSmartPtr(sp3); return 0; }

4. 删除器

智能指针析构时默认是进行delete释放资源。这就意味着,如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。

为了解决这个问题,智能指针支持在构造时给定一个删除器。删除器本质是一个可调用对象(仿函数、函数指针、Lambda),在智能指针析构时会调用它来释放资源。

4.1 数组的特化版本

因为new[]经常使用,为了简洁,unique_ptrshared_ptr都特化了一份[]的版本。这样在析构时就会自动调用delete[]

// 如果没有特化,直接这样写会崩溃(因为默认 delete 不匹配 new[]) // unique_ptr<Date> up1(new Date[10]); // 使用特化版本,管理 new[] 的资源 unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]);

4.2 自定义删除器

unique_ptrshared_ptr支持删除器的方式有所不同:

  • unique_ptr:是在类模板参数中支持的。

  • shared_ptr:是在构造函数参数中支持的。

方法1:

template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; // unique_ptr 需要将删除器类型作为模板参数 unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); // shared_ptr 只需要在构造函数中传入删除器对象 shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

方法2:

// 函数指针做删除器 template<class T> void DeleteArrayFunc(T* ptr) { delete[] ptr; } // unique_ptr 传入函数指针类型 unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); // shared_ptr 直接传函数指针 shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); // Lambda 表达式做删除器 auto delArrOBJ = [](Date* ptr) { delete[] ptr; }; // unique_ptr 使用 decltype 推导 Lambda 类型 unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); // shared_ptr 直接传 Lambda shared_ptr<Date> sp4(new Date[5], delArrOBJ);
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 23:52:26

PyCharm高效开发:Yi-Coder-1.5B Python环境配置指南

开展高效开发, 关于Yi - Coder - 1.5B环境配置指南之一, 包含为何会选择Yi - Coder - 1.5B来进行配合。在平常的开发里面, 我们时常会需要迅速地生成代码片段, 去理解繁杂的逻辑, 补全函数调用, 甚至于依据注释自动生成实现。 Yi-Coder-1.5B身为一款专门为编程而优化的开源模型…

作者头像 李华
网站建设 2026/9/13 23:52:24

普通人学python有什么用 ?学好了能干什么

这种语言对普通人而言是很适宜去学习的, 在学习完毕以后, 能够从事的数据挖掘以及分析工作, 还有游戏开发工作, 自动化测试工作, 网站开发爬虫工作等等等。1普通人学习的好处运用学习, 能够提升工作效率, 借助几十行代码去进行一个简单爬虫工具的撰写, 几分钟即可自动抓取特定网…

作者头像 李华
网站建设 2026/9/13 23:52:17

谷歌 Gemini Advanced 更新,可直接在线编辑和运行 Python 代码

2月20日, IT之家传来消息, 2月8日时, 谷歌宣称要把Bard AI聊天机器人进行更名, 还打造并推出专门针对安卓的App, 这个App里搭载的Ultra 1.0模型版本, 是需要进行注册, 而后还要订阅的, 在逻辑推理这方面, 在此App中该模型版本比其他情况更具优势, 在执行指令方面, 它表现得更为…

作者头像 李华