一、为什么需要智能指针?
智能指针基于RAII(资源获取即初始化)机制,自动管理堆内存,核心解决裸指针的内存泄漏问题:把堆内存的生命周期绑定到栈上的智能指针对象,当智能指针对象超出作用域时,析构函数会自动释放绑定的堆内存,无需手动
delete。C++ 中的裸指针(
int* p = new int;)需要手动管理内存:
- 忘记
delete会导致内存泄漏;- 多次
delete会导致未定义行为;- 异常场景下(比如
delete前抛出异常),内存也无法释放。
二、C++ 智能指针的分类(C++11 及以后)
C++ 标准库在
<memory>头文件中提供了三种核心智能指针:unique_ptr、shared_ptr、weak_ptr,其中auto_ptr已被废弃(设计缺陷)。
unique_ptr是独占式智能指针(轻量、高效),shared_ptr是共享式(引用计数),weak_ptr辅助shared_ptr解决循环引用;- 使用智能指针的核心原则:优先选
unique_ptr,避免裸指针与智能指针混用,警惕shared_ptr的循环引用。
1. std::unique_ptr(独占式智能指针)
核心特点:独占对堆内存的所有权,同一时刻只能有一个
unique_ptr指向同一个资源,不支持拷贝(copy),仅支持移动(move)。基本用法(代码示例)
#include <iostream> #include <memory> // 必须包含的头文件 // 自定义类,用于演示 class MyClass { public: MyClass(int val) : value(val) { std::cout << "MyClass 构造:" << value << std::endl; } ~MyClass() { std::cout << "MyClass 析构:" << value << std::endl; } void show() { std::cout << "值:" << value << std::endl; } private: int value; }; int main() { // 1. 创建unique_ptr,管理堆内存 // unique_ptr<指针类型> 指针变量名(需要管理的堆内存); std::unique_ptr<MyClass> ptr1(new MyClass(10)); // 推荐写法(C++14+):make_unique,更安全(避免异常安全问题) auto ptr2 = std::make_unique<MyClass>(20); // 2. 使用智能指针(和裸指针用法类似) ptr1->show(); // 输出:值:10 (*ptr2).show(); // 输出:值:20 // 3. 移动语义(转移所有权) std::unique_ptr<MyClass> ptr3 = std::move(ptr1); // ptr1变为空,ptr3拥有资源 if (ptr1 == nullptr) { std::cout << "ptr1 已为空" << std::endl; // 会执行这行 } ptr3->show(); // 输出:值:10 // 4. 手动释放资源(一般不需要,析构会自动释放) ptr3.reset(); // 释放资源,ptr3变为空 // 5. 管理数组(unique_ptr专门支持数组) std::unique_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[0] = 100; std::cout << arr_ptr[0] << std::endl; // 输出:100 // 6. 作为函数返回值(自动移动,无需手动std::move) auto get_ptr() { return std::make_unique<MyClass>(30); } auto ptr4 = get_ptr(); ptr4->show(); // 输出:值:30 return 0; // 函数结束,所有智能指针析构,自动释放堆内存 }关键说明
std::make_unique是 C++14 引入的,比直接new更安全(避免 “内存泄漏 + 异常” 的场景);unique_ptr不支持拷贝(std::unique_ptr<MyClass> ptr4 = ptr3;编译报错),只能通过std::move转移所有权;unique_ptr是轻量级的(大小和裸指针一样),性能几乎和裸指针无差别,是首选的智能指针(除非需要共享所有权)。
2. std::shared_ptr(共享式智能指针)
核心特点:允许多个
shared_ptr共享同一个资源的所有权,底层通过引用计数实现:
- 每新增一个
shared_ptr指向该资源,引用计数 + 1;- 每销毁一个
shared_ptr,引用计数 - 1;- 当引用计数变为 0 时,自动释放资源。
基本用法(代码示例)
#include <iostream> #include <memory> class MyClass { public: MyClass(int val) : value(val) { std::cout << "MyClass 构造:" << value << std::endl; } ~MyClass() { std::cout << "MyClass 析构:" << value << std::endl; } void show() { std::cout << "值:" << value << std::endl; } private: int value; }; int main() { // 1. 创建shared_ptr(推荐用make_shared,更高效) std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10); std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1 // 2. 拷贝,引用计数+1 std::shared_ptr<MyClass> ptr2 = ptr1; std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2 std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2 // 3. 多个指针共享资源 ptr1->show(); // 输出:值:10 ptr2->show(); // 输出:值:10 // 4. 重置指针,引用计数-1 ptr1.reset(); std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1 // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new) std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[1] = 200; std::cout << arr_ptr[1] << std::endl; // 输出:200 return 0; // ptr2析构,引用计数变为0,资源释放 }关键问题:循环引用
shared_ptr的最大陷阱是循环引用:两个shared_ptr互相指向对方,导致引用计数永远无法变为 0,最终内存泄漏。循环引用示例(错误):
#include <iostream> #include <memory> class B; // 前向声明 class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "A 析构" << std::endl; } }; class B { public: std::shared_ptr<A> a_ptr; ~B() { std::cout << "B 析构" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; // A引用B b->a_ptr = a; // B引用A // 循环引用:a和b的引用计数都是2,析构时各减1,变为1,永远不会释放 return 0; // 不会输出"A 析构"和"B 析构",内存泄漏 }
3. std::weak_ptr(弱引用智能指针)
核心特点:专门解决
shared_ptr的循环引用问题,是shared_ptr的 “辅助指针”:
- 不拥有资源的所有权,不会增加引用计数;
- 可以观察
shared_ptr管理的资源是否还存在;- 必须通过
lock()方法转换成shared_ptr才能访问资源(避免访问已释放的资源)。解决循环引用的示例(正确):
#include <iostream> #include <memory> class B; class A { public: std::weak_ptr<B> b_ptr; // 改为weak_ptr ~A() { std::cout << "A 析构" << std::endl; } }; class B { public: std::weak_ptr<A> a_ptr; // 改为weak_ptr ~B() { std::cout << "B 析构" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; // weak_ptr不增加引用计数 b->a_ptr = a; // weak_ptr不增加引用计数 // 检查资源是否存在 if (auto temp = a->b_ptr.lock()) { // lock()返回shared_ptr,若资源存在则有效 std::cout << "B资源存在" << std::endl; } return 0; // a和b析构,引用计数变为0,资源释放(输出"A 析构"和"B 析构") }关键说明
weak_ptr不能直接访问资源(没有->和*运算符),必须通过lock()获取shared_ptr后才能访问;expired()方法可以判断weak_ptr指向的资源是否已释放(返回true表示已释放);weak_ptr的大小和shared_ptr相同(因为要存储引用计数的指针)。
三、智能指针的使用原则
- 优先使用
unique_ptr(高效、无额外开销),仅在需要共享所有权时使用shared_ptr; - 避免用同一个裸指针创建多个智能指针(会导致重复释放);
- 不要手动
delete智能指针管理的裸指针(智能指针析构时会再次delete); shared_ptr的循环引用必须用weak_ptr解决;- 优先使用
make_unique/make_shared创建智能指针(异常安全、更高效)。
四、内存泄漏案例以及改良方案
内存泄漏的核心本质是:堆内存被分配后,失去了对它的所有引用,导致程序无法再释放这块内存,直到程序退出(系统会回收,但长期运行的程序如服务器会持续占用内存)。
常见的内存泄漏场景
1. 最基础:忘记释放手动分配的内存
这是新手最易犯的错误 —— 用new/malloc分配堆内存后,未调用delete/free释放,尤其是在分支、循环等复杂逻辑中更容易遗漏。
示例代码(错误):
#include <iostream>
using namespace std;
void func() {
// 分配堆内存
int* p = new int(10);
string name = "test";
// 分支逻辑导致忘记释放
if (name == "test") {
cout << "分支返回,遗漏delete" << endl;
return; // 直接返回,p指向的内存永远无法释放
}
// 只有走else才会释放(本例不会执行)
delete p;
}
int main() {
func(); // 执行后内存泄漏(4字节int)
return 0;
}
避免方法:
- 优先使用智能指针(
unique_ptr/shared_ptr)替代裸指针; - 若必须用裸指针,遵循 “分配即规划释放” 原则,在分配内存时就确定释放的位置。
修复后的完整代码
#include <iostream>
#include <memory> // 必须包含智能指针的头文件
using namespace std;
void func() {
// 用unique_ptr替代裸指针,make_unique是创建unique_ptr的推荐方式
unique_ptr<int> p = make_unique<int>(10);
string name = "test";
if (name == "test") {
cout << "分支返回,智能指针自动释放内存" << endl;
return; // 即使提前返回,p也会析构并释放内存
}
// 无需手动delete!智能指针超出作用域时会自动释放
// 原来的delete p 可以完全删除
}
int main() {
func(); // 执行后无内存泄漏
return 0;
}
2. 异常导致的内存泄漏
new分配内存后,delete执行前抛出异常,导致delete语句无法执行,进而泄漏内存。这是比 “忘记释放” 更隐蔽的问题。
示例代码(错误):
#include <iostream>
#include <stdexcept>
using namespace std;
void riskyFunc() {
throw runtime_error("突发异常"); // 抛出异常
}
void func() {
int* p = new int(20); // 分配内存
riskyFunc(); // 抛出异常,后续代码全部跳过
delete p; // 永远执行不到,内存泄漏
}
int main() {
try {
func();
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
return 0;
}
避免方法:
- 核心方案:使用智能指针(RAII 机制),即使抛出异常,智能指针对象析构时仍会自动释放内存;
- 兜底方案:用
try-catch包裹,但代码冗余且易遗漏,不如智能指针可靠。
修复后的代码:
#include <iostream>
#include <stdexcept>
#include <memory> // 智能指针头文件
using namespace std;
void riskyFunc() {
throw runtime_error("突发异常");
}
void func() {
unique_ptr<int> p = make_unique<int>(20); // 智能指针
riskyFunc(); // 抛异常也不影响,p析构时自动释放
}
int main() {
try {
func();
} catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
return 0; // 无内存泄漏
}
3. shared_ptr 的循环引用(进阶陷阱)
这是使用智能指针时的高频错误 —— 两个或多个shared_ptr互相持有对方的引用,导致引用计数永远无法归 0,内存无法释放。
示例代码(错误):
#include <iostream>
#include <memory>
using namespace std;
class B; // 前向声明
class A {
public:
shared_ptr<B> b_ptr; // A持有B的shared_ptr
~A() { cout << "A 析构" << endl; } // 不会执行
};
class B {
public:
shared_ptr<A> a_ptr; // B持有A的shared_ptr
~B() { cout << "B 析构" << endl; } // 不会执行
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b; // 循环引用开始
b->a_ptr = a;
// a和b的引用计数都是2,析构时各减1变为1,永远不会释放
return 0; // 无析构输出,内存泄漏
}
避免方法:
- 将循环引用中的一方或双方的
shared_ptr替换为weak_ptr(弱引用,不增加引用计数); - 修复后的代码可参考上一轮讲解智能指针时的
weak_ptr示例。
4. 容器存储裸指针未清理
vector/list/map等容器存储裸指针时,清空容器(如clear())仅会删除指针本身(容器内的元素),但不会释放指针指向的堆内存。
示例代码(错误):
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int*> vec;
// 向容器添加堆内存指针
vec.push_back(new int(1));
vec.push_back(new int(2));
vec.push_back(new int(3));
vec.clear(); // 仅清空容器,3个int的堆内存未释放,泄漏
return 0;
}
避免方法:
- 容器中存储智能指针(如
vector<unique_ptr<int>>),清空时自动释放内存; - 若必须存裸指针,清空容器前遍历 delete 每个元素。
修复后的代码:
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
int main() {
vector<unique_ptr<int>> vec;
vec.push_back(make_unique<int>(1));
vec.push_back(make_unique<int>(2));
vec.push_back(make_unique<int>(3));
vec.clear(); // 自动释放所有堆内存,无泄漏
return 0;
}
5. 动态数组释放错误(delete vs delete [])
用new[]分配的数组,若误用delete(而非delete[])释放:
- 对于类对象数组:仅调用第一个元素的析构函数,其余元素的析构函数不执行,导致内存泄漏;
- 对于内置类型数组(int/char 等):看似无泄漏,但属于 “未定义行为”,可能引发其他问题。
示例代码(错误):
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() { cout << "MyClass 构造" << endl; }
~MyClass() { cout << "MyClass 析构" << endl; }
};
int main() {
// 分配对象数组
MyClass* arr = new MyClass[3]; // 输出3次构造
delete arr; // 错误!仅调用第一个对象的析构,后2个泄漏
// 正确写法:delete[] arr;
return 0;
}
避免方法:
- 严格遵循 “
new配delete,new[]配delete[]” 的规则; - 优先使用
vector或unique_ptr<T[]>管理动态数组(无需手动释放)。
6. 全局 / 静态指针的内存泄漏
全局或静态指针指向堆内存时,若程序结束前未释放:
- 虽然程序退出后操作系统会回收内存,但长期运行的程序(如服务器、后台服务)会持续占用内存,最终导致内存耗尽;
- 不符合 “资源用完即释放” 的编程规范。
示例代码(错误):
#include <iostream>
using namespace std;
// 全局指针
int* g_ptr = new int(100);
int main() {
// 程序运行期间未释放g_ptr,直到退出才被系统回收
cout << *g_ptr << endl;
// 遗漏:delete g_ptr;
return 0;
}
避免方法:
- 用全局智能指针(如
static unique_ptr<int> g_ptr = make_unique<int>(100)); - 在程序退出前(如 main 结束前)显式释放全局 / 静态裸指针。
7. 第三方库资源未释放
使用第三方库的 API 分配资源(如自定义句柄、内存、句柄)时,未调用库提供的 “释放函数”,导致泄漏(这类泄漏常被忽略)。
示例场景(伪代码):
// 第三方库API示例
void* create_obj(); // 分配资源,返回指针
void destroy_obj(void* p); // 释放资源
int main() {
void* obj = create_obj(); // 分配资源
// 业务逻辑...
destroy_obj(obj); // 忘记调用,资源泄漏
return 0;
}
避免方法:
- 封装成 RAII 类,析构函数中调用释放函数;
- 记录所有 “分配 - 释放” API 对,确保成对调用。
624

被折叠的 条评论
为什么被折叠?



