C++ 智能指针
由于 C++ 没有提供自动内存回收机制,因此每个 new 出来的对象在不需要时都要手动 delete 掉,否则就会造成内存泄漏(memory leak)。但由于种种原因,例如程序流程太复杂,或者程序员忘记 delete,从而导致内存泄漏,影响程序运行甚至异常退出。因此,在现代 C++ 编程中,标准库定义了智 能指针(Smart Pointer),这些智能指针用于帮助确保程序不会出现内存泄漏,并具有异常安全。
什么是智能指针
简单来说,C++ 中的智能指针就是一种用于自动化管理动态分配内存的机制。它是 C++ 提供的一种高级工具,可以自动地管理指向堆内存的指针,并在不需要时将其释放。这些指针使用一个计数器来跟踪有多少个指针引用了同一个对象。当计数器归零时,智能指针会自动释放对象。
使用智能指针,程序员在使用 new 分配内存后不再需要跟踪指向该内存的指针,也不需要手动 delete 释放该内存。有效地避免了因程序流程太复杂,或者忘记 delete 导致内存泄漏或野指针等问题。
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放由它管理的堆内存。所有智能指针都重载了 operator-> 操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用 . 操作符。
访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以 if (my_smart_object) 永远为真,要判断智能指针的裸指针是否为空,应该使用如下方式判断。
if (my_smart_object.get())
智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
智能指针在 std 命名空间中定义 ,下面我们将逐一介绍四种智能指针。
智能指针的 种类
在 C++ 中,主要有三种智能指针:
unique_ptr:唯一所有权指针,一个对象只能由一个unique_ptr指向,当unique_ptr被销毁时,它所管理的对象也被销毁。shared_ptr:共享所有权指针,允许多个shared_ptr指向同一对象。它使用一个引用计数器来跟踪有多少个shared_ptr指向同一个对象,并在所有指向该对象的shared_ptr都被销毁时释放该对象。weak_ptr:弱引用指针,允许共享一个对象的所有权,但不会增加引用计数。weak_ptr主要用于避免shared_ptr的循环引用问题。
使用智能指针可以避免手动管理动态内存的复杂性,并减少内存泄漏和悬挂指针等错误的风险。但需要注意的是,智能指针并不能完全消除所有的内存管理问题,还需要根据具体情况综合使用。
unique_ptr
unique_ptr 定义在头文件 <memory> 中,它持有对对象的独有权 —— 两个 unique_ptr 不能指向同一个对象,即 unique_ptr 不共享它所管理的对象。
unique_ptr 无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。你只能移动 unique_ptr,即对资源管理权限可以实现转移。这意味着,内存 资源所有权可以转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源。
实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于存 C++ 对象时,可使用 unique_ptr,构造 unique_ptr 时,可使用 make_unique Helper 函数。
下图演示了两个 unique_ptr 实例之间的所有权转换。

unique_ptr 与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器运行效率很高,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。
unique_ptr 指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过 reset 方法重新指定、通过 release 方法释放所有权、通过移动语义转移所有权,unique_ptr 还可能没有对象,这种情况被称为 empty。
//智能指针的创建
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //绑定动态对象
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d); //创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代 默认的删除器 delete
//所有权的变化
int *p_i = u_i2.release(); //释放所有权
unique_ptr<string> u_s(new string("abc"));
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针”
u_s2.reset(u_s.release()); //所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价
下面是一个使用 unique_ptr 的简单示例程序,展示了如何创建 unique_ptr 对象、将其作为函数参数传递以及如何使用 unique_ptr 管理动态内存:
#include <iostream>
#include <memory> // 包含 unique_ptr
void print(std::unique_ptr<int>& ptr) { // 接受 unique_ptr 的引用
std::cout << "value: " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr(new int(42)); // 创建 unique_ptr 对象
print(ptr); // 将 unique_ptr 作为函数参数传递
*ptr = 43; // 可以像常规指针一样使用箭头或星号操作符
std::cout << "new value: " << *ptr << std::endl;
return 0; // 在 main 函数结束时,unique_ptr 对象自动释放内存
}
在这个示例程序中,我们首先创建了一个指向 int 类型的 unique_ptr 对象,并通过 new 运算符在堆上分配了一个整型对象。然后,我们将 unique_ptr 对象作为函数参数传递给 print 函数,并输出指向的整数值。接着,我们通过星号操作符修改了指向的整数值,并输出新值。
在程序结束时,unique_ptr 对象自动释放了它所管理的内存,因此我们无需手动调用 delete 操作符来释放分配的内存。
shared_ptr
shared_ptr是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,定义在 <memory> 文件中,命名空间为 std。shared_ptr 最初实现于 Boost 库中,后由 C++11 引入到 C++ STL。
shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。像 shared_ptr 这种智能指针,在《Effective C++》中被称为“引用计数型智能指针”(reference-counting smart pointer,RCSP)。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销 —— shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针。而时间上的开销主要在初始化和拷贝操作上, * 和 -> 操作符重载的开销跟 auto_ptr 是一样。
开销并不是我们不使用 shared_ptr 的理由,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
int i;
A(int n):i(n) { };
~A() { cout << i << " " << "destructed" << endl; }
};
int main()
{
shared_ptr<A> sp1(new A(2)); // A(2)由sp1托管
shared_ptr<A> sp2(sp1); // A(2)同时交由sp2托管
shared_ptr<A> sp3;
sp3 = sp2; // A(2)同时交由sp3托管
cout << sp1->i << "," << sp2->i << "," << sp3->i << endl;
A * p = sp3.get(); // get返回托管的指针,p 指向 A(2)
cout << p->i << endl; // 输出 2
sp1.reset(new A(3)); // reset导致托管新的指针, 此时sp1托管A(3)
sp2.reset(new A(4)); // sp2托管A(4)
cout << sp1->i << endl; // 输出 3
sp3.reset(new A(5)); // sp3托管A(5),A(2)无人托管,被delete
cout << "end" << endl;
return 0;
}
下面是一个使用 shared_ptr 的简单示例程序,展示了如何创建 shared_ptr 对象、使用引用计数以及如何在不同的作用域中共享动态内存:
#include <iostream>
#include <memory> // 包含 shared_ptr
void func(std::shared_ptr<int> ptr) // 接受 shared_ptr 的拷贝
{
std::cout << "func: " << *ptr << std::endl;
}
int main()
{
std::shared_ptr<int> ptr1(new int(42)); // 创建 shared_ptr 对象
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出引用计数
{
std::shared_ptr<int> ptr2 = ptr1; // 复制 shared_ptr,共享同一块内存
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出引用计数
std::cout << "ptr2 count: " << ptr2.use_count() << std::endl; // 输出引用计数
} // 离开作用域时,ptr2 被销毁,但内存不被释放
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出引用计数
func(ptr1); // 将 shared_ptr 作为函数参数传递,函数内部共享同一块内存
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出引用计数
return 0; // 在 main 函数结束时,shared_ptr 对象自动释放内存
}
在这个示例程序中,我们首先创建了一个指向 int 类型的 shared_ptr 对象,并通过 new 运算符在堆上分配了一个整型对象。然后,我们输出 shared_ptr 对象的引用计数,发现此时计数为 1。
接着,我们创建了一个新的作用域,并将 shared_ptr 对象复制给另一个 shared_ptr 对象,共享同一块内存。我们输出两个 shared_ptr 对象的引用计数,发现此时计数为 2。当离开这个作用域时,其中一个 shared_ptr 对象被销毁,但内存不被释放。
然后,我们将 shared_ptr 对象作为函数参数传递给 func 函数,并输出 shared_ptr 对象的引用计数,发现此时计数为 2,这是因为 func 函数内部也共享同一块内存。
在程序结束时,shared_ptr 对象自动释放了它所管理的内存,因此我们无需手动调用 delete 操作符来释放分配的内存。