头图

前言

大家好,我是小康。

在上一篇文章中,我们聊了聊 RAII 的魔力,如何通过简单的类设计解决了资源泄漏问题,比如自动管理数据库连接、网络连接等。RAII 就像一个贴心的小助手,帮你在构造时搞定资源分配,在析构时自动清理资源,让你轻松避免手动管理资源的“坑”。

不过,讲到这,有的朋友可能会问:“这些例子很好,但每次都得手写一个类,岂不是很麻烦?有没有一种现成的解决方案,可以更方便地管理像内存这样的资源?” 这就是我们今天的主题—智能指针

智能指针是RAII思想的完美实践,它不仅帮助我们管理动态分配的内存,还能提高代码的健壮性和可读性。今天我们就来聊聊C++里的“神器”之一:Boost库中的智能指针。放心,我会用最简单的大白话帮你把它讲明白,看完保准让你眼前一亮。

微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。

什么是智能指针?

先问你一个问题:在C++中动态分配的内存,你还记得释放吗?
如果答案是“不太记得”,那么恭喜你,成功掉进了初学者的大坑之一—内存泄漏

普通指针需要你手动delete,但说实话,没人能保证100%记得释放。如果某个delete忘了写,或者写错地方,内存泄漏就找上你了。

而智能指针的作用就是帮你托管这些内存,让你“new”的内存不用再操心“delete”。它自动帮你管理资源生命周期,真正做到开开心心用内存,轻轻松松不泄漏

auto_ptr——智能指针的先驱

在C++98/03时代,C++标准并没有提供直接的智能指针类,而是通过auto_ptr来实现智能指针的初步功能。auto_ptr在早期版本的C++中非常常见,但它也存在一些问题,尤其是在所有权转移方面。

auto_ptr的特点

auto_ptr最重要的特点是它会在离开作用域时自动释放内存。它通过RAII思想,确保在auto_ptr对象销毁时自动调用delete来释放资源。就像一个“自动清洁工”,让你不用担心忘记释放内存。

使用示例

#include <iostream>
#include <memory> // auto_ptr

void testAutoPtr() {
    std::auto_ptr<int> ptr(new int(42)); // 使用auto_ptr分配内存
    std::cout << "AutoPtr value: " << *ptr << std::endl;
    // 离开作用域时,ptr会自动释放内存
}

auto_ptr的问题

虽然auto_ptr的设计理念是好的,但它的 所有权转移 问题使得它在实际使用中很容易引发错误。因为当auto_ptr被拷贝时,所有权会转移给新的auto_ptr,原本的auto_ptr就变成了空指针。

#include <iostream>
#include <memory>

void testAutoPtr() {
    std::auto_ptr<int> p1(new int(10));
    std::auto_ptr<int> p2 = p1;  // 转移所有权,p1为空
    std::cout << "p1: " << (p1.get() ? "not null" : "null") << std::endl;
    std::cout << "p2: " << *p2 << std::endl;
}

这种行为在很多场景下是非常危险的,尤其是在需要保留原始指针或多个指针共享资源时。因此,auto_ptr在C++11中被标记为弃用(deprecated),并逐渐被更为安全的智能指针取代。

Boost库中的智能指针家族

为了弥补auto_ptr的不足,Boost库提供了更为强大和灵活的智能指针实现。Boost的智能指针不仅能更好地管理内存,还能支持复杂的内存管理需求,比如共享所有权弱引用等。 而且,C++11及以后标准中的智能指针(如std::shared_ptrstd::unique_ptr)的设计,正是借鉴了Boost库中智能指针的思想和实现方式。

接下来,我们逐个认识一下 boost 库的这些成员:

1、boost::scoped_ptr

1.1 简单理解:

boost::scoped_ptr 是最简单的智能指针,它的生命周期和作用域绑定,离开作用域时自动释放内存,就像个“定时清理工”。

1.2 使用场景:

它适合那些不需要共享、不需要转移所有权的对象。比如函数内临时分配的内存。

1.3 使用示例:

#include <boost/scoped_ptr.hpp>
#include <iostream>

void testScopedPtr() {
    boost::scoped_ptr<int> p(new int(42)); // 构造时分配内存
    std::cout << "ScopedPtr value: " << *p << std::endl;
    // 离开作用域,自动释放内存
}

注意:scoped_ptr不支持拷贝和赋值,因为它的所有权是唯一且不可转移的。

  • 不支持拷贝
boost::scoped_ptr<int> p1(new int(10));
boost::scoped_ptr<int> p2 = p1;  // 错误!scoped_ptr 不允许拷贝
  • 不支持赋值
boost::scoped_ptr<int> p1(new int(10));
boost::scoped_ptr<int> p2;
p2 = p1;  // 错误!scoped_ptr 不支持赋值

一句话总结:scoped_ptr 就是单人版的智能指针,简单省心,离开作用域自动清理内存。

2. boost::shared_ptr

2.1 简单定义:

boost::shared_ptr 是一个智能指针,它实现了 共享所有权 的概念。就是说,多个 shared_ptr 可以一起管理同一个对象,直到最后一个 shared_ptr 被销毁时,这个对象的内存才会被释放。它通过 引用计数 来跟踪有多少个 shared_ptr 指向同一个对象,引用计数是一个整数,表示有多少个指针正在“持有”这个对象。

比方说,你和你的朋友们一起合租一套房子,你们每个人都是这个房子的 "共享所有者"。如果你们都同意,当最后一个人搬走时,房子就不再使用了(也就是销毁)。这里的“最后一个人”就是最后一个指向房子的 shared_ptr

2.2 基本用法使用示例:

#include <boost/shared_ptr.hpp>
#include <iostream>

void testSharedPtr() {
    boost::shared_ptr<int> p1(new int(10));  // p1 管理一个值为 10 的整型对象
    boost::shared_ptr<int> p2 = p1;  // p2 和 p1 共享所有权
    std::cout << "SharedPtr count: " << p1.use_count() << std::endl; // 输出 2,因为 p1 和 p2 都指向同一个对象

    p2.reset();  // p2 释放所有权
    std::cout << "SharedPtr count: " << p1.use_count() << std::endl; // 输出 1,因为 p1 依然指向该对象
}

解释:

  • 在这段代码里,我们创建了一个 boost::shared_ptr<int>,它指向一个 int 类型的对象,值为 10。
  • 然后我们创建了第二个 shared_ptrp2,它和 p1 指向同一个对象。此时,引用计数变成了 2,因为 p1p2 都共享这个对象的所有权。
  • p2.reset() 被调用时,p2 释放了对这个对象的所有权,引用计数减少到 1,表示只有 p1 仍然拥有该对象。
  • 最后,当 p1 离开作用域时,对象的引用计数为 0,shared_ptr 会自动释放对象的内存。

2.3 特点:

  • 通过引用计数管理资源,内存会在最后一个 shared_ptr 销毁时自动释放。
  • 需要注意 循环引用 问题,因为若两个对象互相持有 shared_ptr,引用计数永远不会为零,最终导致内存泄漏。

2.4 实际使用场景:缓存数据的共享

假设你有一个缓存(比如配置数据),这个缓存需要在多个地方使用:

  • A 模块 需要访问缓存数据。
  • B 模块 也需要访问同一个缓存数据。

如果用 shared_ptr,多个模块都能安全地“共享”这份缓存:

  1. 只要还有模块需要用这个缓存,内存就不会被释放。
  2. 只有所有模块都不再使用它时,内存才会被自动释放。

来看个具体例子:

#include <boost/shared_ptr.hpp>
#include <iostream>

void ModuleA(boost::shared_ptr<int> data) {
    std::cout << "Module A is using data: " << *data << std::endl;
}

void ModuleB(boost::shared_ptr<int> data) {
    std::cout << "Module B is using data: " << *data << std::endl;
}

void testSharedPtr() {
    boost::shared_ptr<int> cache(new int(42));  // 缓存数据

    ModuleA(cache);  // A 模块使用缓存
    ModuleB(cache);  // B 模块使用缓存

    std::cout << "SharedPtr use_count: " << cache.use_count() << std::endl;  // 输出引用计数
}

int main() {
    testSharedPtr();
    // 只有这里 cache 离开作用域,引用计数归零,内存才会被释放
    return 0;
}

2.5 引用计数是什么?

引用计数 就是指有多少个 shared_ptr 对象指向同一个资源。每次创建一个新的 shared_ptr 来指向已经存在的对象时,它会增加该对象的引用计数。每次一个 shared_ptr 被销毁或者 reset 后,引用计数就会减少。

你可以把引用计数想象成房子(对象)的“合租人数量”。每当一个人加入(创建新的 shared_ptr)或者搬走(销毁 shared_ptr),合租人数都会变化。只有当最后一个人搬走时,房子才会被清理掉。

2.6 循环引用问题

shared_ptr 通过引用计数来管理对象的生命周期,但它有一个常见问题:循环引用。当两个对象互相持有对方的 shared_ptr 时,引用计数永远不会归零,导致内存无法被释放,从而引发内存泄漏。

举个简单的例子:

假设我们有两个类AB,它们互相持有对方的 shared_ptr

#include <boost/shared_ptr.hpp>
#include <iostream>

class A;
class B;

class A {
public:
    boost::shared_ptr<B> b_ptr;  // A 持有 B 的 shared_ptr
};

class B {
public:
    boost::shared_ptr<A> a_ptr;  // B 持有 A 的 shared_ptr
};

void testCircularReference() {
    boost::shared_ptr<A> a(new A);  // 创建 A
    boost::shared_ptr<B> b(new B);  // 创建 B

    a->b_ptr = b;  // A 持有 B
    b->a_ptr = a;  // B 持有 A

    // 此时 A 和 B 会互相持有对方的 shared_ptr,引用计数永远不会为零
}

int main() {
    testCircularReference();
    // 即使 testCircularReference 函数结束,A 和 B 仍然不会被释放
}

为什么会发生循环引用?

先来看张图:

解释一下:

1. 初始化对象 A 和 B

boost::shared_ptr<A> a(new A);  // 创建 A
boost::shared_ptr<B> b(new B);  // 创建 B
  • 这里分别创建了 shared_ptr<A>shared_ptr<B>
  • 此时的引用计数:

    • a 的引用计数 ref_a_count = 1
    • b 的引用计数 ref_b_count = 1

2. A 持有 B(a->b_ptr = b)

a->b_ptr = b;  // A 持有 B
  • a 内部的 b_ptr 指向了 b
  • b 的引用计数增加了 1,因为现在 a->b_ptr 也指向了 b
  • 此时引用计数变化:

    • a 的引用计数 ref_a_count = 1(没有变)
    • b 的引用计数 ref_b_count = 2

图中的标注ref_b_count++,ref_b_count = 2

3. B 持有 A(b->a_ptr = a)

b->a_ptr = a;  // B 持有 A
  • b 内部的 a_ptr 指向了 a
  • a 的引用计数增加了 1,因为现在 b->a_ptr 也指向了 a
  • 此时引用计数变化:

    • a 的引用计数 ref_a_count = 2
    • b 的引用计数 ref_b_count = 2

图中的标注ref_a_count++,ref_a_count = 2

4. 问题出现了!为什么会发生循环引用?

现在 ab 互相持有彼此的 shared_ptr,构成了一个循环引用。具体情况是:

  • a 的引用计数 = 2

    • 1 是由 boost::shared_ptr<A> a 持有的。
    • 1 是由 b->a_ptr 持有的。
  • b 的引用计数 = 2

    • 1 是由 boost::shared_ptr<B> b 持有的。
    • 1 是由 a->b_ptr 持有的。

问题就在这里:

  • testCircularReference() 函数结束时,ab 两个 shared_ptr 会离开作用域,它们的引用计数会分别变成 1 ,但是永远不会变为 0。从而导致内存无法释放。

结果:内存泄漏!

微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。

3、使用 boost::weak_ptr 解决循环引用

为了避免 shared_ptr 引起的循环引用问题,我们可以使用 boost::weak_ptr。但在讲解weak_ptr 如何解决循环引用之前,我们先来了解下 weak_ptr

3.1 什么是 weak_ptr?

weak_ptr 是一种不增加引用计数的指针,它只做“观察者”角色,而不拥有对象。因此,即使两个对象互相持有对方的 shared_ptr,只要其中一个持有 weak_ptr,引用计数就不会增加,最终对象的内存会被正确释放。

3.2 weak_ptrshared_ptr 有什么区别?

  • shared_ptr拥有型 指针,它会增加引用计数,表示你在“持有”这个对象。
  • weak_ptr 就是一个“”指针,它不会改变引用计数,只是用来 观察 对象是否还存在。

3.3 weak_ptr 的基本使用:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <iostream>

void testWeakPtr() {
    boost::shared_ptr<int> p1(new int(20));  // p1 持有一个 int(20)
    boost::weak_ptr<int> wp = p1;  // wp 只是观察 p1,但不增加引用计数

    // 通过 lock() 将 weak_ptr 转回 shared_ptr,检查对象是否还在
    if (boost::shared_ptr<int> sp = wp.lock()) {
        std::cout << "WeakPtr value: " << *sp << std::endl;  // 输出: WeakPtr value: 20
    }
}

代码解释:

  1. p1 是一个 shared_ptr,它持有一个 int(20) 的对象,并管理这个对象的生命周期。
  2. wp 是一个 weak_ptr,它 不拥有 对象,只是观察 p1
  3. 使用 wp.lock()weak_ptr 转回 shared_ptr,如果对象还存在(即 p1 还活着),就能成功创建一个新的 shared_ptr,否则返回一个空指针。
  4. 通过 *sp 我们可以打印出对象的值。

3.4 适用场景:

weak_ptr 适合用在需要“观察”对象,但又不希望影响对象生命周期的场景,比如:

  • 解决 shared_ptr 的循环引用问题。
  • 在缓存系统中观察资源状态,不影响资源的销毁。

简单来说,weak_ptr 就像一个 旁观者,它不插手对象的生死,只是用来看看对象还在不在。

3.5 如何解决 shared_ptr 的循环引用问题?

weak_ptr 的基本用法我们已经了解了,接下来我们来看看weak_ptr 是怎么解决循环引用。

使用 weak_ptr 解决循环引用:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <iostream>

class A;
class B;

class A {
public:
    boost::shared_ptr<B> b_ptr;  // A 持有 B 的 shared_ptr
};

class B {
public:
    boost::weak_ptr<A> a_ptr;  // B 使用 weak_ptr 来引用 A
};

void testCircularReference() {
    boost::shared_ptr<A> a(new A);  // 创建 A
    boost::shared_ptr<B> b(new B);  // 创建 B

    a->b_ptr = b;  // A 持有 B
    b->a_ptr = a;  // B 使用 weak_ptr 来引用 A

    // 这样就避免了引用计数的循环增加
}

int main() {
    testCircularReference();
    // 此时,当 testCircularReference 函数结束,A 和 B 会正确释放
}

代码分析:

Step 1:a->b_ptr = b

  • b_ptrA 类中的 shared_ptr<B>
  • b 被赋值给 a->b_ptrB 的引用计数 +1,变为 2
  • 此时,ba->b_ptr 共同持有 B 对象。

Step 2:b->a_ptr = a

  • a_ptrB 类中的 weak_ptr<A>
  • weak_ptr 不会增加 A 的引用计数,所以 A 的引用计数依然为 1
  • b->a_ptr 只是“观察” A,不会阻止 A 被释放。

作用域结束时的引用计数如何变化?

testCircularReference() 函数结束时:

1. a 离开作用域:

  • ashared_ptr<A>,它管理 A 对象的生命周期。
  • a 被销毁时,A 的引用计数从 1 变为 0
  • A 对象被释放,同时它的成员变量 b_ptrshared_ptr<B>)也被销毁。

2. b_ptr 被销毁:

  • b_ptr 是 A 的成员变量,它持有 B 对象。
  • b_ptr 被销毁时,B 的引用计数从 2 变为 1(因为 b_ptr 不再持有 B)。

3.b 离开作用域:

  • bshared_ptr<B>,它管理 B 对象的生命周期。
  • b 被销毁时,B 的引用计数从 1 变为 0
  • B 对象被释放。

weak_ptr 的作用:

正是因为 weak_ptr 的存在,打破了 A 和 B 之间的循环引用。虽然 B 持有对 A 的弱引用,但它不会增加 A 的引用计数,因此在 A 的生命周期结束时,A 可以正常被销毁。

如果没有 weak_ptr,而是使用 shared_ptr 来互相引用,那么 A 和 B 的引用计数会互相依赖,永远不会归零,导致内存泄漏。通过将 B 持有的 shared_ptr<A> 替换为 weak_ptr<A>,循环引用被成功打破,内存也得到了正确释放。

小结一下:

  • weak_ptr 的核心作用是打破循环引用,它不增加引用计数,只是“观察”对象。
  • 在上面的代码中,A 持有 B 的 shared_ptr,而 B 只通过 weak_ptr 引用 A,从而避免了引用计数的死循环。
  • 作用域结束后,A 和 B 的引用计数逐步归零,内存被正确释放。

4. boost::intrusive_ptr(不常用,了解即可)

4.1 简单理解

boost::intrusive_ptr 是一种轻量级智能指针,和 shared_ptr 不一样,它 不自己维护引用计数,而是依赖对象本身来管理引用计数。也就是说,intrusive_ptr 更像是一个工具,用来帮助你管理那些已经自带引用计数的对象。

4.2 使用场景

  • 如果对象本身已经有引用计数的机制(比如一些框架中的对象),intrusive_ptr 就很合适。
  • 它更轻量,性能更高,适合那些对性能要求比较高的场景。

4.3 怎么用?

用起来比 shared_ptr 要麻烦一点,因为你需要自己告诉它怎么增加和减少引用计数。

4.4 示例代码:

#include <boost/intrusive_ptr.hpp>
#include <iostream>

class MyClass {
private:
    int ref_count;  // 自己维护引用计数

public:
    MyClass() : ref_count(0) {}
    
    void add_ref() { ++ref_count; }  // 增加引用计数
    void release() {
        if (--ref_count == 0) {  // 如果引用计数为 0,释放内存
            delete this;
        }
    }

    void show() const { std::cout << "MyClass instance" << std::endl; }
};

// 必须告诉 intrusive_ptr 如何增加和减少引用计数
void intrusive_ptr_add_ref(MyClass* obj) {
    obj->add_ref();  // 调用对象的增加引用计数函数
}

void intrusive_ptr_release(MyClass* obj) {
    obj->release();  // 调用对象的减少引用计数函数
}

void testIntrusivePtr() {
    boost::intrusive_ptr<MyClass> p1(new MyClass());  // 创建对象,引用计数 = 1
    boost::intrusive_ptr<MyClass> p2 = p1;  // 共享所有权,引用计数 = 2

    p1->show();
    p2.reset();  // p2 释放所有权,引用计数 = 1
    p1.reset();  // p1 释放所有权,引用计数 = 0,对象被销毁
}

int main() {
    testIntrusivePtr();
    return  0;
}

代码解释:

1. 对象自己管理引用计数

  • MyClass 里有一个 ref_count,用来记录当前有多少个指针指向它。
  • add_ref()release() 是手动实现的增加和减少引用计数的函数。

2. 两个必要的函数

  • intrusive_ptr_add_ref(MyClass* obj):当 intrusive_ptr 增加一个引用时调用。
  • intrusive_ptr_release(MyClass* obj):当 intrusive_ptr 释放一个引用时调用。如果引用计数变为 0,释放对象。

3. 智能释放

  • 当最后一个 intrusive_ptr 离开作用域时,引用计数降到 0,对象会自动释放内存。

4.5 为什么要用它?

  • 性能高
    它不需要额外分配内存来存储引用计数,一切都交给对象自己管理,减少了开销。
  • 更灵活
    如果你已经有一个类本身实现了引用计数(比如 add_ref()release()),用 intrusive_ptr 会更方便。

4.6 适合什么场景?

  • 性能要求高:比如游戏引擎、图形处理这种场景,用 intrusive_ptr 能减少内存开销。
  • 自定义引用计数逻辑:当你需要对引用计数的增减有更细粒度的控制时。

一句话总结:boost::intrusive_ptr 是一个更轻量的智能指针,但你需要自己实现引用计数的逻辑。它适合用在对象已经有引用计数机制的场景,性能高,灵活性强,但也需要更细心的管理。

总结:什么时候用哪个智能指针?

  • scoped_ptr:简单、作用域内管理的资源。
  • shared_ptr:需要共享所有权时使用。
  • weak_ptr:解决shared_ptr的循环引用问题。
  • intrusive_ptr:高性能场景,由用户控制引用计数。

看完这些,是不是感觉Boost库的智能指针很贴心?它们的设计灵感都来源于RAII的思想,只不过为我们预先封装好了繁琐的实现细节。无论是小白还是老鸟,掌握这些工具,绝对能让你的C++代码变得更优雅、更安全。

尾声

今天,我们从 RAII 一路聊到 Boost智能指针,让资源管理不再是负担,而变成一种享受。希望这篇文章能让你对智能指针的理解更上一层楼!如果觉得有用,别忘了点赞、收藏、关注!,或分享给更多对 C++ 编程感兴趣的小伙伴!😊

也欢迎关注我的公众号「跟着小康学编程」,获取更多有趣又实用的技术干货。有问题?评论区等你,咱们一起讨论学习!技术路上不孤单,一起成长!

怎么关注我的公众号?

扫下方公众号二维码即可关注。

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!


小康
33 声望4 粉丝

一枚分享编程技术和 AI 相关的程序员 ~