当前位置: 首页 > news >正文

cpp自学 day26(智能指针)

C++ 智能指针学习笔记

智能指针是 C++11 引入的重要特性,旨在自动管理动态分配的内存,有效解决传统裸指针(raw pointer)可能导致的内存泄漏、野指针、重复释放等问题。它们的核心思想是RAII (Resource Acquisition Is Initialization),即资源在对象构造时获取,在对象析构时自动释放。


🎯 智能指针的类型

C++ 主要提供了三种智能指针:unique_ptrshared_ptrweak_ptr

1. unique_ptr (独占所有权)

  • 概念: 独占性指针。unique_ptr 管理的内存只能由一个智能指针拥有。当 unique_ptr 对象被销毁时,它所指向的内存会被自动释放。
  • 特性:
    • 独占性: 任何时候,只有一个 unique_ptr 可以指向给定的内存地址。
    • 无法复制: unique_ptr 不能被复制(= 操作符),但可以被移动move())。这意味着所有权可以转移。
    • 轻量: 大小与裸指针相同(通常),性能开销极小。
  • 何时使用:
    • 当你确定某块内存资源只会被一个所有者管理时。
    • 作为函数返回值(通过移动语义安全返回动态分配的对象)。
    • 用作类成员,表示该类独占某个资源。

示例:

#include <iostream>
#include <memory> // 包含智能指针的头文件
#include <utility> // 包含 moveusing namespace std; // 使用命名空间,以便直接使用 cout, endl, unique_ptr 等class MyClass {
public:MyClass() { cout << "MyClass 构造" << endl; }~MyClass() { cout << "MyClass 析构" << endl; }void greet() { cout << "Hello from MyClass!" << endl; }
};void processUniquePtr(unique_ptr<MyClass> ptr) {if (ptr) {ptr->greet();}// ptr 在这里离开作用域,MyClass 对象被自动销毁
}int main() {// 1. 创建 unique_ptr// (1)推荐使用 make_unique 来创建,更安全、更高效unique_ptr<MyClass> p1 = make_unique<MyClass>(); if (p1) {p1->greet();}//(2)第二种创建 unique_ptr的方式//unique_ptr<MyClass> p1(new MyClass());// 2. 所有权转移 (移动)// unique_ptr<MyClass> p2 = p1; // 编译错误!不能复制unique_ptr<MyClass> p2 = move(p1); // p1 现在为空,p2 拥有所有权if (p1) {cout << "p1 仍然有效" << endl;} else {cout << "p1 已经为空" << endl; // 输出此行}if (p2) {p2->greet(); }// 3. 作为函数参数传递 (所有权转移)cout << "\n--- 调用 processUniquePtr ---" << endl;unique_ptr<MyClass> p3 = make_unique<MyClass>();processUniquePtr(move(p3)); // p3 的所有权转移到函数参数 ptr// p3 在这里也变为空cout << "\n--- main 函数结束 ---" << endl; // p2 离开作用域,MyClass 析构return 0;
}

make_unique与unique_ptr(new T())创建unique_ptr指针的区别

  • 安全性(防范内存泄漏):

    • make_unique 更安全。 即使在某些复杂表达式中发生异常,它也能确保内存不会泄漏。它把内存分配和智能指针绑定做成了一个“原子操作”。
    • unique_ptr(new T()) 在极少数情况下(比如在函数参数求值时),如果 new T() 后、unique_ptr 绑定前发生异常,可能导致内存泄漏。
  • 效率(内存分配次数):

    • make_unique 通常更高效。 它通常只需要一次内存分配,同时为对象和管理指针的数据分配空间。
    • unique_ptr(new T()) 涉及两次内存分配:一次给 new T() 的对象,一次给 unique_ptr 自身。

2. shared_ptr (共享所有权)

  • 概念: 共享性指针。shared_ptr 允许多个智能指针共同拥有同一块内存资源。它通过引用计数 (reference count) 来管理内存,只有当所有指向该内存的 shared_ptr 都被销毁时,内存才会被释放。
  • 特性:
    • 共享性: 可以被复制,多个 shared_ptr 可以指向同一个对象。
    • 引用计数: 内部维护一个引用计数,记录有多少个 shared_ptr 指向同一个对象。
    • 自动释放: 当引用计数归零时,内存自动释放。
    • 循环引用问题: 存在循环引用(Cyclic Reference)的风险,可能导致内存泄漏(两个或多个 shared_ptr 相互引用,导致引用计数永远不为零)。
  • 何时使用:
    • 当一个内存资源可能被多个模块或对象共享时。
    • 工厂方法返回对象时。
    • 实现观察者模式或事件系统。

示例:

#include <iostream>
#include <memory> // 包含智能指针的头文件using namespace std; // 使用命名空间,以便直接使用 cout, endl, shared_ptr 等class MyClass {
public:MyClass() { cout << "MyClass 构造" << endl; }~MyClass() { cout << "MyClass 析构" << endl; }void greet() { cout << "Hello from MyClass!" << endl; }
};void showSharedPtr(shared_ptr<MyClass> ptr) { // 参数是 shared_ptr 副本cout << "  进入 showSharedPtr 函数,引用计数: " << ptr.use_count() << endl;if (ptr) {ptr->greet();}cout << "  离开 showSharedPtr 函数" << endl;// ptr 离开作用域,引用计数 -1
}int main() {// 1. 创建 shared_ptr// 推荐使用 make_shared 来创建,更安全、更高效shared_ptr<MyClass> sp1 = make_shared<MyClass>(); cout << "sp1 创建后,引用计数: " << sp1.use_count() << endl; // 输出: 1// 2. 复制 shared_ptr (共享所有权)shared_ptr<MyClass> sp2 = sp1; // 复制,引用计数增加cout << "sp2 复制后,引用计数: " << sp1.use_count() << endl; // 输出: 2// 3. 作为函数参数传递 (引用计数增加)cout << "\n--- 调用 showSharedPtr ---" << endl;showSharedPtr(sp1); // sp1 复制给函数参数 ptrcout << "--- showSharedPtr 返回后,sp1 引用计数: " << sp1.use_count() << endl; // 输出: 2 (因为函数参数的副本已销毁)// 4. 一个 shared_ptr 离开作用域{shared_ptr<MyClass> sp3 = sp1; // 又一个副本,引用计数增加到 3cout << "sp3 在作用域内,引用计数: " << sp1.use_count() << endl; // 输出: 3} // sp3 离开作用域,引用计数减少到 2cout << "sp3 离开作用域后,引用计数: " << sp1.use_count() << endl; // 输出: 2sp1.reset(); // 显式释放 sp1 持有的对象,引用计数 -1 (变为 1)cout << "sp1 reset 后,引用计数: " << sp2.use_count() << endl; // 输出: 1cout << "\n--- main 函数结束 ---" << endl; // sp2 离开作用域,引用计数 -1 (变为 0),MyClass 对象析构return 0;
}

3. weak_ptr (非拥有性引用)

  • 概念: weak_ptr 是一种非拥有性的智能指针。它不增加所指向对象的引用计数,因此不会阻止对象被释放。
  • 特性:
    • 不拥有: 不拥有对象的管理权,因此不会增加引用计数。
    • 解决循环引用: 主要用于解决 shared_ptr 的循环引用问题。
    • 安全性: weak_ptr 可以检查它所指向的对象是否仍然存在。如果对象已被销毁,weak_ptr 会变为空。
    • 使用前必须转换为 shared_ptr 无法直接通过 weak_ptr 访问对象,必须先通过 lock() 方法获取一个 shared_ptr
  • 何时使用:
    • 打破 shared_ptr 之间的循环引用。
    • 观察者模式中,观察者持有被观察对象的 weak_ptr,避免被观察者无法被释放。
    • 缓存机制中,缓存项可以持有资源的 weak_ptr,允许资源在内存不足时被清除。

示例(解决循环引用):

#include <iostream>
#include <memory>using namespace std; // 使用命名空间class B; // 前向声明class A {
public:shared_ptr<B> b_ptr; // A 拥有 BA() { cout << "A 构造" << endl; }~A() { cout << "A 析构" << endl; }void set_b(shared_ptr<B> b) { b_ptr = b; }
};class B {
public:weak_ptr<A> a_ptr; // B 弱引用 A,不增加 A 的引用计数B() { cout << "B 构造" << endl; }~B() { cout << "B 析构" << endl; }void set_a(shared_ptr<A> a) { a_ptr = a; }void access_a() {if (shared_ptr<A> temp_a = a_ptr.lock()) { // 尝试获取 shared_ptrcout << "  B 成功访问 A (引用计数: " << temp_a.use_count() << ")" << endl;} else {cout << "  B 无法访问 A,A 已被销毁" << endl;}}
};int main() {cout << "--- 正常情况下(无循环引用)---" << endl;{shared_ptr<A> pa_normal = make_shared<A>();shared_ptr<B> pb_normal = make_shared<B>();// pa_normal 持有 pb_normal,但 pb_normal 不持有 pa_normalpa_normal->set_b(pb_normal); cout << "pa_normal 引用计数: " << pa_normal.use_count() << endl; // 1cout << "pb_normal 引用计数: " << pb_normal.use_count() << endl; // 2 (pa_normal 和 pb_normal 都持有)} // pa_normal, pb_normal 离开作用域,引用计数归零,对象正常析构cout << "\n--- 解决循环引用(使用 weak_ptr)---" << endl;// 这是演示 weak_ptr 解决 shared_ptr 循环引用的关键代码块{shared_ptr<A> pa = make_shared<A>(); // A 构造shared_ptr<B> pb = make_shared<B>(); // B 构造pa->set_b(pb); // A 强引用 B (b_ptr 是 shared_ptr)pb->set_a(pa); // B 弱引用 A (a_ptr 是 weak_ptr,不增加 A 的引用计数)cout << "pa 引用计数: " << pa.use_count() << endl; // 1 (只有 pa 自己)cout << "pb 引用计数: " << pb.use_count() << endl; // 2 (pa->b_ptr 和 pb 自己)pb->access_a(); // B 能够通过 weak_ptr 临时获取 A 的 shared_ptr 并访问} // pa, pb 离开作用域,引用计数归零,A 和 B 都正常析构cout << "\n--- main 函数结束 ---" << endl;return 0;
}

unique_ptr 常用方法清单

  • unique_ptr():创建空指针。
  • unique_ptr(ptr):接管裸指针 ptr 的所有权。
  • unique_ptr(unique_ptr&& other):移动构造,从 other 转移所有权。
  • operator=(unique_ptr&& other):移动赋值,转移所有权。
  • operator=(nullptr_t):将指针置空。
  • get():返回存储的裸指针。
  • release():释放所有权,返回裸指针并置空自身(返回的裸指针需手动管理)。
  • reset(ptr = nullptr):释放当前对象,接管新裸指针 ptr 的所有权(或置空)。
  • swap(other):与另一个 unique_ptr 交换对象。
  • operator bool():判断指针是否为空。
  • operator*():解引用,获取所指对象。
  • operator->():箭头运算符,访问所指对象成员。

shared_ptr 常用方法清单

  • shared_ptr():创建空指针。
  • shared_ptr(ptr):接管裸指针 ptr 的所有权。
  • shared_ptr(const shared_ptr& other):复制构造,增加引用计数。
  • shared_ptr(shared_ptr&& other):移动构造,转移所有权(不改变引用计数)。
  • shared_ptr(weak_ptr wp):从 weak_ptr 构造 shared_ptr(如果对象存在)。
  • operator=(const shared_ptr& other):复制赋值,增加引用计数。
  • operator=(shared_ptr&& other):移动赋值。
  • operator=(nullptr_t):将指针置空。
  • use_count():返回当前指向同一对象的 shared_ptr 数量。
  • get():返回存储的裸指针。
  • reset(ptr = nullptr):释放当前对象,接管新裸指针 ptr 的所有权(或置空)。
  • swap(other):与另一个 shared_ptr 交换对象。
  • operator bool():判断指针是否为空。
  • operator*():解引用,获取所指对象。
  • operator->():箭头运算符,访问所指对象成员。
  • owner_before(other):比较拥有者次序(用于排序)。

weak_ptr 常用方法清单

  • weak_ptr():创建空指针。
  • weak_ptr(const shared_ptr& sp):从 shared_ptr 构造 weak_ptr
  • weak_ptr(const weak_ptr& wp):复制构造。
  • weak_ptr(weak_ptr&& wp):移动构造。
  • operator=(const shared_ptr& sp):从 shared_ptr 赋值。
  • operator=(const weak_ptr& wp):复制赋值。
  • operator=(weak_ptr&& wp):移动赋值。
  • lock():尝试获取一个 shared_ptr。如果对象存在则返回非空 shared_ptr,否则返回空。
  • expired():检查所指对象是否已被销毁。
  • use_count():返回所观察对象的 shared_ptr 引用计数。
  • reset():将 weak_ptr 置空。
  • swap(other):与另一个 weak_ptr 交换对象。
  • owner_before(other):比较拥有者次序。

💡 智能指针的优势

  • 自动内存管理: 无需手动 delete,避免内存泄漏。
  • 避免野指针: 当智能指针离开作用域时,资源会被释放,指向该资源的智能指针也会失效。
  • 异常安全: 即使在函数发生异常时,智能指针也能保证内存的正确释放。
  • 代码简洁: 减少了手动管理内存的复杂性,使代码更清晰。

⚠️ 使用注意事项

  • 总是使用 make_uniquemake_shared 这是创建智能指针的最佳实践,比直接使用 new 更安全(防止异常和资源泄漏)和更高效。
  • 避免裸指针和智能指针混用: 尽量不要将智能指针管理的对象的裸指针暴露出去,或在两者之间频繁转换,这容易造成混乱和错误。如果需要获取裸指针,使用 get() 方法,但要谨慎使用。
  • unique_ptr 不可复制,只能移动: 如果你需要转移所有权,请使用 move()
  • shared_ptr 的循环引用问题: 这是最常见的陷阱。当两个对象互相持有对方的 shared_ptr 时,它们的引用计数永远不会归零,导致内存泄漏。使用 weak_ptr 来打破循环引用
  • 不要用同一个裸指针初始化多个智能指针: 这会导致重复释放和未定义行为。

相关文章:

  • Linux中《进程控制》详细介绍
  • Java并发编程-理论基础
  • 算法题(165):汉诺塔问题
  • 华为OD机考-机房布局
  • 合成数据:国内外创新企业的崛起与突破
  • UE的AI行为树Selector和Sequence如何理解
  • day6 cpp:c中处理字符串,c++string
  • CSP信奥赛C++常用系统函数汇总
  • Linux 下 DMA 内存映射浅析
  • Effective Java 第三版 第二三章总结
  • 【JVM】Java虚拟机(三)——类加载与类加载器
  • [创业之路-410]:经济学 - 国富论的核心思想和观点,以及对创业者的启发
  • Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
  • 2025-06-02-IP 地址规划及案例分析
  • OD 算法题 B卷【反转每对括号间的子串】
  • Secs/Gem第八讲(基于secs4net项目的ChatGpt介绍)
  • 剑指offer19_链表中倒数第k个节点
  • Netty集群搭建
  • python打卡day48
  • PandasAI使用
  • 电脑哪里做模板下载网站/seo赚钱暴利
  • 怎样建官方网站/sem和seo哪个工作好
  • 无锡做网站好/app开发公司排名
  • 黄州做网站的/2021十大网络舆情案例
  • 网站备案名称更改/营销技巧有哪些
  • 陕西省交通建设集团西长分公司网站/外贸接单平台哪个最好