1. 指针基础

指针全名为指针变量,是C++中用于存储内存地址的变量。计算机在存储数据时采用有序存放的方式,为了能够准确访问每个数据的位置,需要使用地址来区分,指针变量就是专门用来存放这些地址的变量。

2. 智能指针的本质与实现原理

智能指针本质上是封装了原始C++指针的类模板,是为了确保动态内存安全性而产生的。其实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。

智能指针是C++11引入的最重要特性之一,用于自动管理动态分配的内存,防止内存泄漏和悬空指针问题。它们遵循RAII(Resource Acquisition Is Initialization)原则,确保资源在不再需要时被正确释放。

3. 智能指针类型

C++标准库提供了三种主要的智能指针:

3.1 std::unique_ptr(独占所有权指针)

  • 特点:独占所指向的对象,不允许复制,但可以移动
  • 用途:用于管理独占所有权的资源,替代原始指针的首选
  • 内存开销:几乎为零(通常与原始指针大小相同)

3.2 std::shared_ptr(共享所有权指针)

  • 特点:多个shared_ptr可以共享同一个对象的所有权,使用引用计数
  • 用途:用于需要多个所有者共享同一资源的场景
  • 内存开销:需要存储引用计数(通常是指针大小的两倍)

3.3 std::weak_ptr(弱引用指针)

  • 特点:不增加引用计数,用于解决shared_ptr的循环引用问题
  • 用途:观察shared_ptr管理的对象,但不拥有所有权
  • 内存开销:与shared_ptr类似

4. 关键知识点

4.1 创建智能指针

优先使用std::make_uniquestd::make_shared

//(C++14起)
auto ptr1 = std::make_unique<int>(42);
auto ptr2 = std::make_shared<std::string>("Hello");

// 传统方式
std::unique_ptr<int> ptr3(new int(42));
std::shared_ptr<std::string> ptr4(new std::string("Hello"));

4.2 所有权转移

// unique_ptr 的所有权转移
std::unique_ptr<int> source = std::make_unique<int>(42);
std::unique_ptr<int> destination = std::move(source); // source 变为空

// shared_ptr 的所有权共享
std::shared_ptr<int> shared1 = std::make_shared<int>(42);
std::shared_ptr<int> shared2 = shared1; // 引用计数增加

5. shared_ptr的所有权共享机制

当多个shared_ptr共享同一对象时:

std::shared_ptr<int> shared1 = std::make_shared<int>(42);
std::shared_ptr<int> shared2 = shared1; // 共享同一对象

底层机制详解

  1. 第一行:std::make_shared<int>(42)

    • 在堆上分配内存并构造一个值为42的int对象
    • 同时创建一个控制块,其中包含引用计数(初始为1)和其他管理信息
    • shared1包含两个指针:一个指向int对象,一个指向控制块
  2. 第二行:shared2 = shared1

    • shared2复制了shared1的两个指针:指向同一int对象和同一控制块
    • 控制块中的引用计数增加(从1变为2)

6. 循环引用问题及解决方案

6.1 什么是循环引用?

循环引用发生在两个或多个对象通过shared_ptr相互引用时,形成一个闭环,导致它们的引用计数永远不会降为零,从而无法被自动销毁。

6.2 基本示例

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    
    // 形成循环引用
    a->b_ptr = b;
    b->a_ptr = a;
    
    std::cout << "A use count: " << a.use_count() << std::endl;
    std::cout << "B use count: " << b.use_count() << std::endl;
    return 0;
    // 程序结束时不会打印"A destroyed"和"B destroyed"
}

6.3 循环引用的工作原理

  1. 创建a时,A对象的引用计数为1
  2. 创建b时,B对象的引用计数为1
  3. 当执行a->b_ptr = b时:
    • B对象的引用计数增加为2
    • 现在有两个shared_ptr指向B对象:b和a->b_ptr
  4. 当执行b->a_ptr = a时:
    • A对象的引用计数增加为2
    • 现在有两个shared_ptr指向A对象:a和b->a_ptr
  5. 当main函数结束时:
    • 局部变量a和b被销毁,它们的引用计数减1
    • A对象的引用计数变为1(来自b->a_ptr)
    • B对象的引用计数变为1(来自a->b_ptr)
    • 由于引用计数不为零,A和B对象都不会被销毁
    • 但没有任何外部指针可以访问这些对象了,导致内存泄漏

6.4 解决方案:使用std::weak_ptr

std::weak_ptr是一种不控制对象生命周期的智能指针,它指向一个由shared_ptr管理的对象,但不会增加引用计数。

修复循环引用

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 使用weak_ptr而不是shared_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a; // weak_ptr不会增加引用计数
    
    std::cout << "A use count: " << a.use_count() << std::endl;
    std::cout << "B use count: " << b.use_count() << std::endl;
    return 0;
    // 程序结束时会打印"B destroyed"和"A destroyed"
}

7. weak_ptr的计数方式与内存分配

7.1 weak_ptr真的不计数吗?

  • weak_ptr不参与强引用计数:不会阻止对象被销毁
  • weak_ptr参与弱引用计数:用于管理控制块的生命周期

7.2 内存分配方式

  • 使用std::make_shared:对象和控制块在同一内存块中分配
  • 直接使用new:对象和控制块分开分配

7.3 控制块内容

控制块包含:

  • 强引用计数
  • 弱引用计数
  • 其他管理信息

7.4 生命周期管理

  • 对象生命周期:当强引用计数变为0时,对象被销毁(调用析构函数)
  • 控制块生命周期:当强引用计数和弱引用计数都变为0时,控制块被释放

7.5 可视化示例

时间线:
1. shared_ptr 创建: [控制块: strong=1, weak=0] → [对象存在]
2. weak_ptr 创建: [控制块: strong=1, weak=1] → [对象存在]
3. shared_ptr 重置: [控制块: strong=0, weak=1] → [对象销毁!]
4. weak_ptr 检查: 对象已销毁,但控制块存在
5. weak_ptr 销毁: [控制块: strong=0, weak=0] → [控制块销毁]

7.6 形象比喻

想象一个热气球(对象)和它的锚点(控制块):

  1. shared_ptr是抓住气球绳子的人
    • 每个shared_ptr都用力拉着绳子(强引用计数+1)
    • 只要还有人拉着,气球就不会飞走(对象不会被销毁)
  2. weak_ptr是旁边看气球的人
    • 他们只观察气球,并不拉绳子(不增加强引用计数)
    • 但他们需要知道这个锚点位置是否存在(弱引用计数+1)

发生了什么?
当最后一个shared_ptr松手(强引用数归零):

  • 气球立刻飞走销毁(对象内存被释放)
  • 但锚点还在地上(控制块还在)
  • 因为还有weak_ptr观察者需要知道"这里曾经有个气球"

当最后一个weak_ptr也离开:

  • 锚点被移除(控制块内存最终释放)

weak_ptr不阻止对象被销毁(不计强引用数),但需要控制块活着来告诉你对象是否还存在(计弱引用数)。这就是它既能解决循环引用,又能安全检查对象状态的原因


本文基于学习笔记整理

Logo

一座年轻的奋斗人之城,一个温馨的开发者之家。在这里,代码改变人生,开发创造未来!

更多推荐