一、裸指针的常见问题
在深入智能指针之前,我们先回顾一下**裸指针(Raw Pointer)**的痛点。正是这些风险,催生了智能指针的诞生。
C++ 智能指针通过 RAII 机制实现内存自动管理。本文详解 unique_ptr 独占所有权、shared_ptr 引用计数共享所有权及 weak_ptr 解决循环引用的原理与用法。涵盖定制删除器、类型转换及性能优化技巧,对比裸指针的内存泄漏、野指针等风险,提供最佳实践以避免常见错误。

在深入智能指针之前,我们先回顾一下**裸指针(Raw Pointer)**的痛点。正是这些风险,催生了智能指针的诞生。
内存泄漏是指程序分配的内存空间在使用完毕后,没有被正确释放,导致这部分内存永远无法被再次使用。
// 反面示例:裸指针导致的内存泄漏
void func() {
int* p = new int(10);
// 业务逻辑处理...
if (some_condition) {
return; // 提前返回,忘记释放 p
}
delete p;
}
在上述代码中,如果 some_condition 为 true,函数会提前返回,delete p 语句就永远不会执行,导致堆内存泄漏。
二次释放是指对同一块内存进行多次 delete 操作。这会破坏堆内存的完整性,导致程序崩溃或未定义行为。
// 反面示例:裸指针导致的二次释放
void func() {
int* p = new int(20);
delete p; // 第一次释放
// ... 中间经过复杂的逻辑,忘记 p 已经被释放
delete p; // 第二次释放,程序崩溃
}
野指针是指指向已释放内存或非法内存地址的指针。访问野指针会导致程序崩溃、数据损坏等不可预测的结果。
// 反面示例:裸指针导致的野指针问题
int* func() {
int x = 10;
return &x; // 返回栈内存地址,形成野指针
}
当程序发生异常时,正常的执行流程会被打断,可能导致裸指针无法被释放。
// 反面示例:异常导致的内存泄漏
void func() {
int* p = new int(30);
try {
throw std::runtime_error("something wrong");
} catch (...) {
throw; // 重新抛出异常,p 未释放
}
delete p; // 永远不会执行
}
面对裸指针的种种问题,智能指针的核心设计思想应运而生:将指针的生命周期管理与对象的生命周期绑定,通过 RAII(资源获取即初始化)机制,实现内存的自动释放。
简单来说,智能指针是一个 '包装器类',它封装了裸指针,并在其析构函数中自动执行 delete 操作。由于 C++ 的对象生命周期遵循 '出作用域即析构' 的规则,当智能指针对象离开作用域时,析构函数会自动调用,从而保证内存被正确释放。
C++11 标准库提供了三种核心智能指针:unique_ptr、shared_ptr和weak_ptr。它们各自有着不同的设计理念和适用场景。
unique_ptr是最简单、最高效的智能指针,它的核心特性是独占所有权—— 同一时间,只能有一个 unique_ptr 指向一块内存。当 unique_ptr 对象被销毁时,它所指向的内存也会被自动释放。
unique_ptr的底层实现非常简洁:
封装一个裸指针;禁用拷贝构造函数和拷贝赋值运算符,确保所有权无法被复制;支持移动构造函数和移动赋值运算符,允许所有权的 '转移';析构函数中调用
delete释放内存。
我们可以通过一个简化版的 unique_ptr 来理解其原理:
// 简化版 unique_ptr 实现(仅演示核心逻辑)
template <typename T>
class MyUniquePtr {
private:
T* ptr;
public:
explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}
~MyUniquePtr() { delete ptr; ptr = nullptr; }
MyUniquePtr(const MyUniquePtr& other) = delete;
MyUniquePtr& operator=(const MyUniquePtr& other) = delete;
MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) { other.ptr = nullptr; }
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr; }
return *this;
}
T* operator->() const { return ptr; }
T& operator*() const { return *ptr; }
T* get() const { return ptr; }
T* release() { T* temp = ptr; ptr = nullptr; return temp; }
void reset(T* p = nullptr) { delete ptr; ptr = p; }
};
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) { cout << "Test(" << id_ << ") 构造" << endl; }
~Test() { cout << "Test(" << id_ << ") 析构" << endl; }
void show() { cout << "Test id: " << id_ << endl; }
private:
int id_;
};
void test_unique_ptr_basic() {
// 方式 1:通过 make_unique 创建(推荐)
unique_ptr<Test> up1 = make_unique<Test>(1);
up1->show();
// 方式 2:通过 new 创建(不推荐)
unique_ptr<Test> up2(new Test(2));
// 错误:不允许拷贝构造
// unique_ptr<Test> up3 = up1;
// 正确:移动构造(转移所有权)
unique_ptr<Test> up5 = move(up1);
up5->show();
// 重置指针
up5.reset(new Test(5));
}
unique_ptr。vector<unique_ptr<T>> 是常见用法。最佳实践:优先使用 make_unique 创建 unique_ptr;避免手动调用 get()、release();管理数组时指定数组类型 unique_ptr<T[]>。
unique_ptr的独占性虽然高效,但无法满足 '多个指针共享同一块内存' 的场景。此时,shared_ptr应运而生 —— 它支持共享所有权,多个 shared_ptr 可以指向同一块内存,当最后一个 shared_ptr 被销毁时,内存才会被释放。
shared_ptr的核心机制是引用计数(Reference Counting):
每个
shared_ptr都封装了一个 '数据指针'和一个 '控制块指针';控制块中存储了引用计数、弱引用计数以及对象的析构器等信息;当创建一个新的shared_ptr指向对象时,引用计数加 1;当引用计数减为 0 时,控制块会调用析构器释放对象内存。
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) { cout << "Test(" << id_ << ") 构造" << endl; }
~Test() { cout << "Test(" << id_ << ") 析构" << endl; }
void show() { cout << "Test id: " << id_ << endl; }
private:
int id_;
};
void test_shared_ptr_basic() {
shared_ptr<Test> sp1 = make_shared<Test>(1);
cout << "sp1 use_count: " << sp1.use_count() << endl; // 1
// 拷贝构造:引用计数加 1
shared_ptr<Test> sp2 = sp1;
cout << "sp1 use_count after copy: " << sp1.use_count() << endl; // 2
// 移动构造:引用计数不变
shared_ptr<Test> sp4 = move(sp1);
cout << "sp1 use_count after move: " << sp1.use_count() << endl; // 0
}
循环引用是指两个或多个 shared_ptr 互相持有对方的引用,导致它们的引用计数永远无法减为 0,从而造成内存泄漏。这是 shared_ptr 的致命缺陷。
weak_ptr是一种 '弱引用' 智能指针,它的核心特性是不拥有对象的所有权,仅能观察 shared_ptr 所管理的对象。weak_ptr不会增加 shared_ptr 的引用计数,因此不会导致循环引用问题。
weak_ptr的底层依赖 shared_ptr 的控制块:
weak_ptr仅存储控制块的指针,不存储数据指针;创建weak_ptr时,会将控制块的弱引用计数加 1;weak_ptr无法直接访问对象,必须通过lock()方法获取一个shared_ptr,才能访问对象。
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) { cout << "Test(" << id_ << ") 构造" << endl; }
~Test() { cout << "Test(" << id_ << ") 析构" << endl; }
void show() { cout << "Test id: " << id_ << endl; }
weak_ptr<Test> wp_self; // 弱引用
private:
int id_;
};
void test_weak_ptr_solve_cycle() {
shared_ptr<Test> sp1 = make_shared<Test>(100);
shared_ptr<Test> sp2 = make_shared<Test>(200);
// 用 weak_ptr 代替 shared_ptr,避免循环引用
sp1->wp_self = sp2;
sp2->wp_self = sp1;
cout << "sp1 use_count: " << sp1.use_count() << endl; // 1
cout << "sp2 use_count: " << sp2.use_count() << endl; // 1
// 通过 lock() 访问对方
if (auto sp = sp1->wp_self.lock()) {
cout << "sp1 access sp2: ";
sp->show();
}
}
shared_ptr 的循环引用问题:将循环引用中的一方改为 weak_ptr。weak_ptr,避免主题被观察者 '绑架'。weak_ptr,当对象被销毁时,缓存自动失效。最佳实践:不单独使用 weak_ptr;使用 lock() 前先检查 expired();避免长期持有 lock() 返回的 shared_ptr。
默认情况下,智能指针会使用 delete 释放内存。但在某些场景下,我们需要自定义释放逻辑(例如,释放动态数组、文件句柄等),此时可以使用 '定制删除器'。
#include <memory>
#include <iostream>
using namespace std;
void close_file(FILE* fp) {
if (fp) fclose(fp);
}
void test_custom_deleter_unique() {
FILE* fp = fopen("test.txt", "w");
unique_ptr<FILE, decltype(&close_file)> up2(fp, close_file);
}
C++ 标准库提供了专门的类型转换函数:
static_pointer_cast:对应 static_cast。dynamic_pointer_cast:对应 dynamic_cast(支持运行时类型检查)。const_pointer_cast:对应 const_cast。注意:dynamic_pointer_cast 仅对多态类型有效;类型转换主要适用于 shared_ptr,unique_ptr 不支持。
unique_ptr:性能几乎与裸指针一致。make_shared 减少内存分配:一次性分配对象和控制块的内存。shared_ptr 拷贝:通过 const& 传递。weak_ptr::lock():短暂持有,尽快释放。shared_ptr 的循环引用:导致内存泄漏(已通过 weak_ptr 解决)。weak_ptr 访问已过期的对象:未检查 expired() 或 lock() 返回的 shared_ptr 是否为空。unique_ptr<T> 而非 unique_ptr<T[]>:导致析构时调用 delete 而非 delete[]。unique_ptr 的拷贝操作:unique_ptr 禁用拷贝,强行拷贝会编译失败。get() 返回的裸指针创建智能指针:导致二次释放。make_shared 和 make_unique:避免直接使用 new,减少内存泄漏风险。get()、release() 的滥用:仅在必要时使用。shared_ptr 避免循环引用:将循环引用中的一方改为 weak_ptr。unique_ptr 管理数组时指定 T[]:确保析构时调用 delete[]。weak_ptr 访问对象前检查有效性:通过 expired() 或 lock() 返回的 shared_ptr 是否为空判断。选择合适的智能指针:
unique_ptr(优先选择,性能最优)。shared_ptr(配合 weak_ptr 解决循环引用)。weak_ptr(不拥有所有权,解决循环引用)。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online