C++——智能指针详解及实现
1. 概述
在 C++ 的早期版本中,程序员主要使用原生指针来管理动态内存。例如,使用 new 关键字分配内存,使用 delete 释放内存。然而,这种方式很容易出错。例如,当程序员忘记释放内存时,就会导致内存泄漏;当释放内存后仍然使用指向该内存的指针时,就会产生悬空指针问题。智能指针的出现就是为了解决这些问题,它通过自动管理内存,让程序员可以更安全、更方便地使用动态内存。
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。
C++ 标准库提供了几种智能指针类型,主要包括 std::unique_ptr、std::shared_ptr和std::weak_ptr。它们各自有不同的用途和特性。
2. std::unique_ptr
std::unique_ptr 是 C++11 引入的智能指针类型,属于标准库 memory 中的内容。
特点:
- 独占所有权:std::unique_ptr 表示对动态分配对象的独占所有权。也就是说,一个 std::unique_ptr 对象在任何时刻只能拥有一个所有者。
- 它不允许复制(拷贝构造函数和拷贝赋值运算符被删除),但可通过 std::move 转移所有权(移动构造函数和移动赋值运算符被定义)。
std::unique_ptr<int> p1 = std::make_unique<int>(10); // 创建并拥有一个 int
std::unique_ptr<int> p2 = p1; // ❌ 错误:不能拷贝
std::unique_ptr<int> p3 = std::move(p1); // ✅ 正确:通过 move 转移所有权
- 当 unique_ptr 离开作用域,它所拥有的对象会被自动释放,不需要手动 delete。
{
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 离开这个作用域后,内存会自动被释放
}
- 适用于一个资源只被一个对象拥有的情况。
- 性能接近原始指针。
unique_ptr 的拷贝构造函数和赋值重载函数
适用场景: 单一所有权的资源管理。
常见方法:
p.get(); // 返回原始指针
p.release(); // 释放所有权,不删除对象
p.reset(); // 删除当前对象并可指向新对象
p.swap(p2); // 交换两个 unique_ptr
示例:
#include <iostream>
#include <memory>
int main()
{
// 创建一个unique_ptr,指向一个动态分配的int对象
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl; // 输出10
// unique_ptr不允许复制
// std::unique_ptr<int> ptr2 = ptr; // 错误
// 但可以移动
std::unique_ptr<int> ptr2 = std::move(ptr);
std::cout << *ptr2 << std::endl; // 输出10
// 此时ptr不再拥有所有权,ptr.get()返回nullptr
return 0;
}
3. std::shared_ptr
特点:
- 共享所有权:std::shared_ptr 允许多个 shared_ptr 对象共享同一个动态分配的对象。它通过内部的引用计数机制来管理对象的生命周期。当最后一个 shared_ptr 被销毁时,它所管理的内存才会被释放。
- 线程安全:引用计数操作是线程安全的,这使得 shared_ptr 可以在多线程环境中安全地使用。
- 会造成循环引用,需要搭配 weak_ptr 使用
适用场景: 需要共享所有权的复杂场景。
#include <memory>
#include <iostream>
struct Test {
Test() { std::cout << "Test created\n"; }
~Test() { std::cout << "Test destroyed\n"; }
};
int main() {
std::shared_ptr<Test> p1 = std::make_shared<Test>();
{
std::shared_ptr<Test> p2 = p1; // 引用计数 +1
std::cout << p1.use_count() << "\n"; // 输出 2
} // p2 析构,计数 -1
std::cout << p1.use_count() << "\n"; // 输出 1
}
4. std::weak_ptr
特点:
- 不拥有资源,不增加引用计数
- 必须调用 .lock() 转换为 shared_ptr 后使用
- 用于观察 shared_ptr 管理的对象,解决循环引用问题
- 和shared_ptr一样,weak_ptr的操作也是线程安全的。
由于 shared_ptr 是通过引用计数来管理对象的生命周期,如果不小心形成了循环引用,就会导致内存泄漏。循环引用(Circular Reference)是指两个或多个智能指针相互引用,形成一个环形结构,导致它们永远无法释放资源,即使已经超出了作用域。例如:
#include <memory>
#include <iostream>
struct B; // 前向声明
struct A {
std::shared_ptr<B> bptr;
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::shared_ptr<A> aptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->bptr = b;
b->aptr = a;
// main函数结束时,看似 a 和 b 超出作用域会销毁
}
❌ 问题:
-
a 和 b 出了作用域后,它们的引用计数都为 1
-
所以资源不会释放,析构函数不会被调用,造成内存泄漏
为了解决循环引用问题,可以使用 std::weak_ptr。
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::weak_ptr<A> aptr; // 改为 weak_ptr
~B() { std::cout << "B destroyed\n"; }
};
👍 这样做的效果:
- shared_ptr 管理资源(增加引用计数)
- weak_ptr 观察资源(不增加引用计数)
- 所以当 a 和 b 出了作用域后:
- a 的 bptr 是 shared_ptr,计数归 0,释放
- b 的 aptr 是 weak_ptr,不阻止 a 被销毁
- 成功释放,析构函数被调用