学而时习之:C++中的智能指针

79 阅读10分钟

C++ 智能指针,指针用于保存内存地址,在动态分配中不可或缺,但也容易出错。下面代码就演示了典型的内存泄漏:

while (1) {
    int* ptr = new int;   // 只申请,从不释放
}

每次循环都丢失一块堆内存,程序最终会因耗尽内存而崩溃。

一. 智能指针

为了杜绝这类问题,C++ 提供了“智能指针”:它们封装裸指针,自动释放所管理的资源,从根本上避免泄漏。所有智能指针都定义在头文件 <memory> 中,采用模板实现,可托管任意类型。

标准库给出的智能指针种类:

  • auto_ptr(C++11 起废弃,C++17 移除)
  • unique_ptr——独占式所有权
  • shared_ptr——共享式所有权
  • weak_ptr——非拥有式“弱”引用

1. auto_ptr(已废弃)

auto_ptr 是 C++ 早期提供的智能指针,它在离开作用域时会自动 delete 所指向的对象,从而防止内存泄漏。

语法

auto_ptr<类型> 名字;

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto_ptr<int> ptr1(new int(10));  // 托管一块 int
    cout << *ptr1 << endl;            // 输出 10

    auto_ptr<int> ptr2 = ptr1;        // 所有权转移给 ptr2
    cout << *ptr2;                    // 仍输出 10
    // ptr1 现已为空,再解引用会崩溃
    return 0;
}

注意
auto_ptr 在 C++11 起被弃用,C++17 开始彻底移除。 现代代码请改用 unique_ptrshared_ptr

2. unique_ptr 独占式所有权

unique_ptr 同一时间独占一份指针,禁止拷贝。 如需转移托管权,必须显式使用 std::move() 把所有权交给另一个 unique_ptr

unique_ptr 是 C++11 引入的智能指针,定义在头文件 <memory> 中,用于管理动态分配的对象。核心要点如下:

  • 独占所有权:同一时刻只能有一个 unique_ptr 拥有指定对象。
  • 仅可移动,不可拷贝:通过 std::move 可把所有权转移给另一个 unique_ptr
  • 自动释放:当 unique_ptr 离开作用域或被重置时,会自动 delete 所指向的对象,无需手动释放。

2.1 语法

unique_ptr<A> ptr1 (new A);    //"A为数据类型"

执行时会在上创建对象,并让 指针名 成为其唯一所有者;指针销毁时对象同步被销毁。

自 C++14 起,推荐用更安全的方式创建:

unique_ptr<A> ptr1 = make_unique<A>();  //"A为数据类型"

2.2 unique_ptr 示例

2.2.1 创建并使用
  1. 先定义一个结构体 A,里面有个成员函数 printA() 用来打印提示文字。
  2. main() 里用 unique_ptr<A> 创建动态对象,p1 是唯一拥有者。
  3. 通过 p1->printA() 调用成员函数;p1.get() 可取出裸指针地址。

代码:

#include <iostream>
#include <memory>
using namespace std;

struct A {
    void printA() {
        cout << "A struct...." << endl;
    }
};

int main() {
    unique_ptr<A> p1(new A);  // 创建动态对象,p1 独占
    p1->printA();             // 调用成员函数

    cout << p1.get();         // 输出裸指针地址(如 0x3e9dec20)
    return 0;
}

输出:

A struct....
0x3e9dec20
2.2.2 试图拷贝 unique_ptr 所拥有对象
#include <iostream>
#include <memory>
using namespace std;

struct A {
    void printA() {
        cout << "A struct...." << endl;
    }
};

int main() {
    
    unique_ptr<A> p1(new A);
    p1->printA();

    //显示所指向指针的地址
    cout << p1.get() << endl;

    // 会导致编译时错误
    unique_ptr<A> p2 = p1;
    p2->printA();
    return 0;
}
main.cpp: In function ‘int main()’:
main.cpp:18:24: error: use of deleted functionstd::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)
[with _Tp = A; _Dp = std::default_delete]’
   18 |     unique_ptr<A> p2 = p1;
      |                        ^~
In file included from /usr/include/c++/11/memory:76,
                 from main.cpp:3:
/usr/include/c++/11/bits/unique_ptr.h:468:7: note: declared here
  468 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~

上面的代码会产生编译错误,因为我们不能对 unique_ptr 进行普通拷贝赋值。要让 p2 接管 p1 的对象,必须使用移动语义,如下所示:

2.2.3 转移 unique_ptr 的所有权
#include <iostream>
#include <memory>
using namespace std;

struct A {
    void printA() {
        cout << "A struct...." << endl;
    }
};

int main() {
    
    unique_ptr<A> p1(new A);
    p1->printA();
  
    // displays address of the containing pointer
    cout << p1.get() << endl;

    // now address stored in p1 shpould get copied to p2
    unique_ptr<A> p2 = move(p1);

    p2->printA();
    cout << p1.get() << endl;
    cout << p2.get();
    return 0;
}

输出结果:

A struct....
0x3514c20
A struct....
0
0x3514c20

代码演示了如何用 `std::move``p1` 的托管权转给 `p2`1. 一开始 `p1` 指向动态创建的 `A` 对象,地址为 `0x3514c20`2. 执行   “unique_ptr<A> p2 = move(p1)” 后,所有权被转移:  
   - `p1.get()` 变成 `0`(空指针)  
   - `p2.get()` 仍保持原地址 `0x3514c20`
结论:通过移动语义,`p1` 把地址“交出去”后自身置空,`p2` 成为新的唯一所有者。

2.3 什么时候用 unique_ptr?

当你需要独占资源所有权时就用它。 unique_ptr 保证同一时刻只有一个指针拥有这份资源,禁止拷贝,只允许通过 std::move 转移所有权;对象作用域结束时会自动 delete,彻底杜绝内存泄漏。

3. shared_ptr 共享式所有权

使用 shared_ptr 可以让多个指针同时指向同一个对象,并通过内部的引用计数机制(可用use_count() 查看)来跟踪当前有多少个 shared_ptr 共享该资源;当最后一个指针销毁时,对象才会被自动 delete

C++ 中的 shared_ptr。std::shared_ptr 是 C++11 引入的智能指针之一。与裸指针不同,它内部带有一个控制块,用来记录被管理对象的引用计数。所有指向同一对象的 shared_ptr 副本共享这份计数,确保当最后一个副本销毁时才自动 delete 对象,实现安全的内存管理。 shared_ptr1.png

3.1 语法

声明一个管理类型 Tshared_ptr

std::shared_ptr<T> 指针名;

3.2 初始化方式

(1)用新对象初始化

std::shared_ptr<T> ptr(new T());          // 接管裸指针
std::shared_ptr<T> ptr = std::make_shared<T>();  // 推荐:一次性分配对象+控制块

(2)用已有指针初始化

std::shared_ptr<T> ptr(already_existing_pointer);       // 接管裸指针

3.3 shared_ptr 的常用成员方法

方法说明
reset()释放当前对所管对象的所有权,并将 shared_ptr 置为空。
use_count()返回当前共享同一对象的 shared_ptr 实例数量(引用计数)。
unique()判断是否只有一个 shared_ptr 拥有该对象(即 use_count() == 1)。
get()返回保存的裸指针。使用时需小心,切勿手动 delete
swap(shr_ptr2)与另一个 shared_ptr 交换所管对象的所有权。

3.4 shared_prt 示例

示例1:

// C++ 程序演示 shared_ptr 的用法
#include <iostream>
#include <memory>
using namespace std;

class A {
public:
    void show() { cout << "A::show()" << endl; }
};

int main()
{
    shared_ptr<A> p1(new A);  // 创建共享指针并访问对象
    cout << p1.get() << endl;  // 打印被管理对象的地址
    p1->show();

    shared_ptr<A> p2(p1); // 创建新的共享指针,共享所有权
    p2->show();
  
    // 打印 p1 和 p2 的地址
    cout << p1.get() << endl;
    cout << p2.get() << endl;
  
    // 返回指向同一被管理对象的 shared_ptr 对象数量
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;
  
    // 放弃 p1 对对象的所有权,指针变为 NULL
    p1.reset();
    cout << p1.get() << endl; // 这将打印 nullptr 或 0
    cout << p2.use_count() << endl;
    cout << p2.get() << endl;
    //这些行演示了 p1 不再管理对象(get() 返回 nullptr),但 p2 仍然管理同一个对象,因此其引用计数为 1。
    return 0;
}
0x1365c20
A::show()
A::show()
0x1365c20
0x1365c20
2
2
0
1
0x1365c20

示例2:

// C++ 程序演示 make_shared 的用法
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    shared_ptr<int> shr_ptr1 = make_shared<int>(42); // 使用 std::make_shared 创建共享指针
    shared_ptr<int> shr_ptr2 = make_shared<int>(24);
    cout << "Value 1: " << *shr_ptr1 << endl; // 使用解引用操作符 (*) 访问值
    cout << "Value 2: " << *shr_ptr2 << endl;
    shared_ptr<int> shr_ptr3 = shr_ptr1;  // 使用赋值运算符 (=) 共享所有权
    // 检查共享指针 1 和共享指针 3 是否指向同一对象
    if (shr_ptr1 == shr_ptr3) {
        cout << "shared pointer 1 and shared pointer 3 "  "point to the same object." << endl;
    }
    shr_ptr2.swap(shr_ptr3);  // 交换共享指针 2 和共享指针 3 的内容
    cout << "Value 2 (after swap): " << *shr_ptr2 << endl;  // 检查交换后的值
    cout << "Value 3 (after swap): " << *shr_ptr3 << endl;
    // 使用逻辑运算符检查共享指针是否有效
    if (shr_ptr1 && shr_ptr2) {
        cout << "Both shared pointer 1 and shared pointer "  "2 are valid."  << endl;
    }
    // 重置共享指针
    shr_ptr1.reset();
}
Value 1: 42
Value 2: 24
shared pointer 1 and shared pointer 3 point to the same object.
Value 2 (after swap): 42
Value 3 (after swap): 24
Both shared pointer 1 and shared pointer 2 are valid.

4. weak_ptr 非拥有式“弱”引用

weak_ptr 是一种不拥有对象的智能指针,它只提供对某个资源的“弱”引用。 与 shared_ptr 类似,但它不参与引用计数
因此,它不会阻止对象被销毁,专门用来打破两个或多个对象互相用 shared_ptr 指向对方而形成的循环引用

weak_ptr 是一种智能指针,相比裸指针风险更低。它可以指向由 shared_ptr 管理的资源,但不拥有该资源,即只建立“非拥有性”引用,不影响引用计数。

为什么需要 weak_ptr: shared_ptr 的典型问题是循环引用
若对象 A 用 shared_ptr 持有 B,对象 B 又用 shared_ptr 持有 A,双方引用计数永远不为 0,导致内存泄漏。

weak_ptr 用来打破这种循环:
把其中一条(或两条)持有关系改为 weak_ptr,对象就能正常释放,同时仍可安全地访问对方(需先提升成 shared_ptr)。

4.1 语法

std::weak_ptr<类型> 名字 ;     其中 `类型` 是它要指向的数据类型。

4.2 std::weak_ptr 常用成员函数

序号函数功能
1reset()清空当前 weak_ptr,不再引用任何资源。
2swap(weak_ptr2)与另一个 weak_ptr 交换所观察的对象。
3expired()检查所观察的资源是否已被释放(返回 true 表示资源不存在)。
4lock()若资源仍存在,返回一个持有该资源的 shared_ptr;否则返回空的 shared_ptr
5use_count()返回当前有多少个 shared_ptr 仍在共享该资源。

4.3 weak_ptr 的主要用途

 weak_ptr 的强大之处!它让你能够"观察"对象而不"拥有"对象。

  1. 防止循环引用
    当对象 A 需要引用对象 B,但又不拥有 B 时,用 weak_ptr 可避免双方用 shared_ptr 互相引用造成的内存泄漏。
  2. 缓存系统
    缓存往往需要临时保存对象引用,但又不能阻碍对象被销毁。weak_ptr 正好提供“非拥有”的观察方式,对象不再被需要时可立即回收。

4.4 weak_ptr 示例

// C++ 程序演示 weak_ptr 的用法
#include <iostream>
#include <memory>

using namespace std;

// 声明一个虚拟对象
class Object {
public:
    Object(int value) : data(value){
        cout << "Object created with value: " << data << endl;
    }
    ~Object(){
        cout << "Object destroyed with value: " << data << endl;
    }
    int data;
};
int main() // 主
{
    shared_ptr<Object> sharedObjectA = make_shared<Object>(42); // 创建拥有资源所有权的共享指针
    weak_ptr<Object> weakObjectA = sharedObjectA; // 为之前创建的共享对象创建弱指针
    // 使用 weak_ptr 访问对象
    if (!weakObjectA.expired()) {
        cout << "The value stored in sharedObjectA:" << (*weakObjectA.lock()).data << endl;
    }
    // 删除对象
    sharedObjectA.reset();
    cout << "End of the Program";
    return 0;
}

二、普通指针存在的问题

  • 内存泄漏:当程序重复分配内存但从未释放时就会发生这种情况。这会导致过度消耗内存,最终致使系统崩溃。
  • 野指针:从未使用有效对象或地址进行初始化的指针称为野指针。
  • 悬空指针:如果一个指针指向的内存已在程序中被提前释放,则该指针称为悬空指针。

三、指针与智能指针

指针和智能指针之间的区别如下:

指针智能指针
指针是一个变量,它保存一个内存地址以及关于该内存位置的数据类型信息。指针是一个指向内存中某个东西的变量。简而言之,智能指针是包装了指针的类,或称作用域指针。
当指针超出其作用域时,它不会被以任何形式销毁。当智能指针超出其作用域时,它会自行销毁。
指针效率相对较低,因为它们不支持其他特性。智能指针更高效,因为它们具有内存管理的附加特性。
它们非常依赖人工/手动管理。它们在本质上是自动化/预编程的。