C++智能指针的使用

在传统 C++ 中,我们使用 newdelete 手动管理堆内存。这非常容易出错:

  1. 内存泄漏 :忘了 delete。导致内存被无效占用。
  2. 悬空指针 :提前 delete 了还在使用的内存,指针还在,指向的内存无效。
  3. 双重释放 :对同一块内存 delete 两次,可能导致程序崩溃。

智能指针其核心思想是:

谁最后一个使用这片内存,谁就负责释放它

C++11 在 <memory> 头文件中提供了三种主要的智能指针:

1. std::unique_ptr (唯一指针)

同一时间只能有一个 unique_ptr 拥有某个资源。无法被复制(copy),只能被移动(move),如果被move了,

原来的unique_ptr就没有这片资源了。离开作用域时,会自动释放这片空间。 当你不需要共享资源所有权时,优先使用它。

示例

cpp 复制代码
 #include <memory>

int main() {
    // 为该对象创建了一片内存空间,并独家占有它 (创建一个独占指针)
    std::unique_ptr<int> myBalloon = std::make_unique<int>(42);

    // 你可以使用该对象
    *myBalloon = 100;

    // 尝试复制?不行!编译器会报错。
    // std::unique_ptr<int> copy = myBalloon; // 错误!

    // 但是你可以把它移动给其他指针
    std::unique_ptr<int> friendBalloon = std::move(myBalloon); // 所有权转移

    // 现在 myBalloon 是空的了,你不能再用它了
    // *myBalloon = 200; // 错误!崩溃

    // 其他指针现在拥有该对象
    *friendBalloon = 200; // 没问题

    return 0;
} // 函数结束,friendBalloon 离开作用域,其所对应的内存自动释放
  // myBalloon 离开作用域,但它是空的,所以啥也不做

2. std::shared_ptr (共享指针)

多个 shared_ptr 可以共同拥有同一个对象。它使用引用计数 来跟踪有多少个 shared_ptr ,指向同一对象。当一个新的
shared_ptr 指向该资源时,引用计数增加。当一个 shared_ptr 被销毁或重置时,引用计数减少。当引用计数变为零时,管理的资源

空间被自动删除。
使用场景

当多个部分都需要使用同一个对象,并且没有明确的单一所有者时。例如:放在容器中的对象、多个类实例共享同一个公共资源、缓存等。

示例

cpp 复制代码
 #include <iostream>
    #include <memory>

    class Resource {
    public:
        Resource() { std::cout << "Resource acquired\n"; }
        ~Resource() { std::cout << "Resource destroyed\n"; }
    };

    int main() {
        // 1. 创建 shared_ptr (推荐使用 std::make_shared)
        std::shared_ptr<Resource> sp1 = std::make_shared<Resource>();
        std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 1

        {
            // 2. 复制:共享所有权,引用计数增加
            std::shared_ptr<Resource> sp2 = sp1;
            std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 2
            // sp2 离开作用域,析构,引用计数减 1
        }

        std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 1

        // 3. 原始指针陷阱:不要用同一个原始指针创建多个独立的 shared_ptr
        Resource* raw_ptr = new Resource();
        std::shared_ptr<Resource> sp3(raw_ptr);
        // std::shared_ptr<Resource> sp4(raw_ptr); // 灾难!两个独立的 sp3 和 sp4
                                                   // 会尝试删除同一块内存,导致未定义行为。

        // 正确做法:如果要再创建一个,应该复制 sp3
        std::shared_ptr<Resource> sp4 = sp3; // 正确!引用计数增加。

        return 0;
    } // sp1, sp3, sp4 离开作用域,引用计数降为0,资源空间被销毁。
    // 输出:
    // Resource acquired (sp1)
    // Use count: 1
    // Use count: 2
    // Use count: 1
    // Resource acquired (raw_ptr)
    // Resource destroyed (sp3/sp4)
    // Resource destroyed (sp1)

3. std::weak_ptr (弱指针)

用于解决 shared_ptr循环引用问题。

什么是循环引用?

两个或多个对象使用 shared_ptr 互相引用,形成一个闭环。导致它们的引用计数永远无法降到零,形成了一个死圈。当你设计类关系,

发现两个类需要互相指向对方时,就要高度警惕循环引用。通常,应该将其中"不属于核心所有权"的那个指针改为 weak_ptr。

cpp 复制代码
#include <iostream>
#include <memory>

class SnakeB; // 前向声明

class SnakeA {
public:
    std::shared_ptr<SnakeB> b_ptr; // 蛇A的肚子里装着蛇B
    ~SnakeA() { std::cout << "SnakeA 被销毁了!太好了!\n"; }
};

class SnakeB {
public:
    std::shared_ptr<SnakeA> a_ptr; // 蛇B的肚子里装着蛇A
    ~SnakeB() { std::cout << "SnakeB 被销毁了!太好了!\n"; }
};

int main() {
    std::cout << "游戏开始!创建两条蛇...\n";

    // 创建两条蛇的智能指针
    auto a = std::make_shared<SnakeA>();
    auto b = std::make_shared<SnakeB>();

    std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 1
    std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 1

    std::cout << "现在让它们互相引用...\n";
    a->b_ptr = b; // 蛇A 吃掉 蛇B -> b 的引用计数 +1
    b->a_ptr = a; // 蛇B 反过来也吃掉 蛇A -> a 的引用计数 +1

    std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 2
    std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 2

    std::cout << "main函数结束了,该清理内存了...\n";
    return 0;
    // 此时,局部变量 a 和 b 的生命周期结束,它们应该被销毁。
    // a 被销毁:蛇A 的引用计数从 2 -> 1
    // b 被销毁:蛇B 的引用计数从 2 -> 1

    // 问题来了:现在还有谁在引用蛇A?是蛇B肚子里的 shared_ptr!
    //           现在还有谁在引用蛇B?是蛇A肚子里的 shared_ptr!

    // 因为它们的引用计数都是1,而不是0,所以系统永远不会去调用它们的析构函数。
    // 两条蛇永远僵持在那里,谁也无法销毁谁。内存泄漏发生了!
}

现在,我们派 weak_ptr 上场。它的核心思想是:"我只观察,我不拥有"。

我们修改一下代码,把其中一条蛇肚子里的 shared_ptr 改成 weak_ptr。

cpp 复制代码
#include <iostream>
#include <memory>

class SnakeB;

class SnakeA {
public:
    // 蛇A的肚子里改成"弱指针"指向蛇B。这意味着:我知道蛇B在哪,但我不会"拥有"它。
    std::weak_ptr<SnakeB> b_weak_ptr; // 关键修改:shared_ptr -> weak_ptr
    ~SnakeA() { std::cout << "SnakeA 被销毁了!太好了!\n"; }
};

class SnakeB {
public:
    std::shared_ptr<SnakeA> a_ptr;
    ~SnakeB() { std::cout << "SnakeB 被销毁了!太好了!\n"; }
};

int main() {
    auto a = std::make_shared<SnakeA>();
    auto b = std::make_shared<SnakeB>();

    // 建立关系
    a->b_weak_ptr = b; // 弱指针赋值,不会增加 b 的引用计数!
    b->a_ptr = a;      // shared_ptr 赋值,会增加 a 的引用计数。

    std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 2 (被 b->a_ptr 引用)
    std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 1 (a->b_weak_ptr 是弱引用,不增加计数!)

    std::cout << "main函数结束了...\n";
    return 0;
    // 1. 局部变量 b 被销毁:蛇B 的引用计数从 1 -> 0。
    //    因为计数为0,系统立即销毁 SnakeB 对象。打印 "SnakeB 被销毁了!太好了!"
    // 2. 因为 SnakeB 被销毁了,它内部的成员 a_ptr 也随之被销毁。
    //    这意味着指向 SnakeA 的引用减少了1。
    // 3. 局部变量 a 被销毁:蛇A 的引用计数从 2 -> 1 -> (因为上一步) 现在变成了 0?
    //    让我们仔细算:a 最初的计数是1,被 b->a_ptr 引用后变成2。
    //    b 被销毁时,b->a_ptr 这个引用没了,所以 a 的计数从2变回1。
    //    然后局部变量 a 被销毁,a 的计数从1变为0。所以 SnakeA 也被销毁!打印 "SnakeA 被销毁了!太好了!"
}

运行修改后的代码,你会看到两条蛇的析构消息都成功打印了! 内存泄漏被解决了。

黄金法则

  1. 默认使用 std::unique_ptr:除非明确需要共享所有权,否则这是最安全、最快速的选择。
  2. 使用 std::make_uniquestd::make_shared :它们更高效(一次分配内存,同时存放对象和控制块)。更安全
    (避免了因为异常导致的内存泄漏)。make_unique 是 C++14 引入的,C++11 可以自己简单实现一个。
  3. 避免使用原始指针进行内存管理 :将所有 new 的结果立即放入智能指针中。
  4. 不要用同一个原始指针初始化多个智能指针:这是导致双重释放的常见原因。
  5. 使用 weak_ptr 来解决或避免循环引用:当你需要"观察"但不"拥有"资源时。

通过遵循这些原则,你可以极大地减少 C++ 程序中的内存管理错误,写出更安全、更清晰的代码。