1.智能指针的引入
观察下列程序,正常情况下,程序new的对象我们能正常释放,但是当抛异常出现后,后⾯的delete没有得到执行,所以内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出(将注释部分取消即可)。
代码语言:javascript
AI代码解释
double Divide(int a, int b) { // throw "Divide by zero condition!"; if (b == 0) { throw "Divide by zero condition!"; } else { return (double)a / (double)b; } } void func() { int * arr1= new int[10]; int * arr2 = new int[10]; /*try {*/ int x; int y; cin >> x >> y; cout << Divide(x, y) << endl; /*}*/ //catch (...) { // cout << "delete []" << arr1 << endl; // cout << "delete []" << arr2 << endl; // delete[] arr1; // delete[] arr2; // throw;//异常重新抛出 //} cout << "delete []" << arr1 << endl; delete[] arr1; cout << "delete []" << arr2 << endl; delete[] arr2; } int main() { try { func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }但是因为new本身也可能抛异常,两个new和Divide同时有问题呢,那我们就要套很多个try---catch语句,就很麻烦,因此C++引入了智能指针这位重量级人物。
2.RAII与智能指针的设计思路
在C++编程中,资源管理是一个至关重要的问题,尤其是涉及动态内存分配、文件操作、网络连接和线程同步等场景时。如果资源没有被正确释放,就会导致内存泄漏或资源占用过长的问题。为了解决这个问题,RAII(Resource Acquisition Is Initialization,资源获取即初始化)设计思想应运而生,并成为智能指针设计的核心理念之一。
2.1 RAII(资源获取即初始化)
2.1.1 RAII 的核心概念
RAII 是一种管理资源的 C++ 编程思想,其核心原则是利用对象的生命周期来管理资源的申请与释放,确保资源不会被错误地释放或泄露。RAII 主要包含以下几个关键点:
- 在对象构造时获取资源(即资源的获取和初始化绑定在一起)。
- 在对象析构时释放资源(当 RAII 对象离开作用域时,资源会被自动释放)。
- 资源在对象生命周期内始终保持有效,不会因为异常或程序流程问题导致资源泄漏。
2.1.2 RAII 的典型应用
RAII 主要用于管理需要手动释放的资源,如:
- 动态内存管理(new/delete, malloc/free)
- 文件操作(文件打开/关闭)
- 互斥锁(lock/unlock)
- 网络连接(连接/断开)
2.1.3 代码示例:RAII 管理文件句柄
代码语言:javascript
AI代码解释
#include <iostream> #include <fstream> class FileGuard { private: std::fstream file; public: // 构造函数打开文件 FileGuard(const std::string& filename) { file.open(filename, std::ios::out); if (!file.is_open()) { throw std::runtime_error("文件打开失败"); } } // 提供文件流操作 std::fstream& get() { return file; } // 析构函数关闭文件 ~FileGuard() { if (file.is_open()) { file.close(); std::cout << "文件已关闭\n"; } } }; int main() { try { FileGuard fg("example.txt"); fg.get() << "Hello, RAII!"; } catch (const std::exception& e) { std::cerr << "异常:" << e.what() << std::endl; } return 0; }代码解析:
FileGuard类在构造时自动打开文件,在析构时自动关闭文件。- 即使
main函数中抛出异常,FileGuard也能保证文件被正确关闭,避免资源泄漏。 - 通过RAII 方式,不需要手动
close()文件,降低了出错的可能性。
3.1 智能指针的设计思路
C++ 的智能指针(Smart Pointer)是RAII 思想的典型应用,用于管理动态分配的内存,避免手动new/delete可能导致的内存泄漏。
3.1.1 智能指针的额外需求
相比于 RAII 的一般资源管理,智能指针除了需要在析构时释放资源之外,还需要:
- 模仿原生指针的行为,即可以像指针一样访问和操作对象。
- 提供安全的引用计数(shared_ptr),支持多个智能指针共享同一块内存。
- 支持独占管理(unique_ptr),避免多个指针同时管理同一块内存。
3.1.2 智能指针的核心机制
智能指针类通过重载运算符来模拟原生指针的行为:
operator*允许解引用智能指针,访问内部对象。operator->允许使用ptr->成员语法访问内部对象。- 构造函数负责初始化并绑定资源。
- 析构函数负责在合适的时机释放资源。
3.1.3 代码示例:简单实现一个智能指针
代码语言:javascript
AI代码解释
#include <iostream> // 自定义智能指针 template<typename T> class SmartPointer { private: T* _ptr; public: explicit SmartPointer(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPointer() { delete _ptr; std::cout << "资源已释放\n"; } // 重载 * 和 -> 访问对象 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } }; class Test { public: void show() { std::cout << "智能指针测试\n"; } }; int main() { SmartPointer<Test> sp(new Test()); sp->show(); // 使用 -> 访问 Test 的方法 return 0; // 离开作用域时,资源自动释放 }代码解析:
SmartPointer通过重载operator*和operator->,使其行为类似于普通指针。- 构造时传入
new分配的对象,析构时自动释放内存,避免手动delete带来的错误。
接下来我们对引入的例子进行修改(这里也是简单实现一个智能指针)
代码语言:javascript
AI代码解释
class SmartPtr { public: //模仿RAII SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout<< "delete " << _ptr << endl; delete[] _ptr; } //重载运算符,模拟指针行为,方便访问资源 T* operator ->() { return _ptr; } T& operator *() { return *_ptr; } T& operator [](int index) { return _ptr[index]; } private: T* _ptr; }; double Divide(int a, int b) { // throw "Divide by zero condition!"; if (b == 0) { throw "Divide by zero condition!"; } else { return (double)a / (double)b; } } void func() { SmartPtr<int> sp1=new int[10]; SmartPtr<int> sp2=new int[10]; SmartPtr<pair<int, int> > sp3=new pair<int, int>[10]; for (int i = 0; i < 10; i++) { sp1[i] = sp2[i] = i; } for (int i = 0; i < 10; i++) { cout << sp1[i] << " "; } int x, y; cin >> x >> y; cout << Divide(x, y) << endl; }3.C++ 标准库智能指针的使用
在 C++ 标准库中,智能指针位于<memory>头文件中,因此包含<memory>头文件后就可以使用智能指针。智能指针的设计目标是自动管理动态分配的资源,避免手动new/delete可能导致的内存泄漏和悬空指针问题。
除了weak_ptr之外,其他标准库智能指针都符合RAII(资源获取即初始化)原则,并且支持像原生指针一样访问资源。不同类型的智能指针,主要的区别在于拷贝语义的处理方式。
1.auto_ptr(C++98,已废弃)
auto_ptr是C++98设计的智能指针,其拷贝行为存在严重问题:- 在拷贝时,它会转移资源的管理权,导致原来的
auto_ptr变成悬空指针,容易引发访问非法内存的错误。 - 由于这个设计缺陷,C++11引入新的智能指针后,强烈建议不要使用
auto_ptr,甚至在 C++17被移除。
- 在拷贝时,它会转移资源的管理权,导致原来的
- 许多公司在 C++11 之前,就已经明确禁止使用
auto_ptr了。
2.unique_ptr(C++11 引入)
unique_ptr代表独占所有权,即:- 不支持拷贝,只允许移动(
std::move())。 - 适用于不需要拷贝的场景,资源只能被一个对象管理。
- 离开作用域时,自动释放资源。
- 不支持拷贝,只允许移动(
示例:
代码语言:javascript
AI代码解释
#include <iostream> #include <memory> class Test { public: void show() { std::cout << "使用 unique_ptr\n"; } }; int main() { std::unique_ptr<Test> ptr = std::make_unique<Test>(); ptr->show(); // std::unique_ptr<Test> ptr2 = ptr; // 错误!unique_ptr 不允许拷贝 std::unique_ptr<Test> ptr2 = std::move(ptr); // 允许移动 return 0; // 资源自动释放 }3.shared_ptr(C++11 引入)
shared_ptr代表共享所有权,即:- 支持拷贝和移动,多个
shared_ptr可以共享同一个资源。 - 底层使用引用计数来管理资源,当最后一个
shared_ptr释放时,资源才被释放。 - 适用于需要多个对象共享同一资源的场景。
- 支持拷贝和移动,多个
示例:
代码语言:javascript
AI代码解释
#include <iostream> #include <memory> class Test { public: void show() { std::cout << "使用 shared_ptr\n"; } }; int main() { std::shared_ptr<Test> p1 = std::make_shared<Test>(); // 推荐使用 make_shared std::shared_ptr<Test> p2 = p1; // p1 和 p2 共享同一资源 p1->show(); std::cout << "引用计数:" << p1.use_count() << std::endl; // 输出 2 return 0; // 只有当 p1 和 p2 都析构,资源才会释放 }代码语言:javascript
AI代码解释
class Date { public: Date(int year=2025, int month=3, int day=15) :_year(year), _month(month), _day(day) { cout << "Date()" << endl; } ~Date() { cout << "~Date()" << endl; } int _year; int _month; int _day; }; #include <memory> int main() { //拷贝后,p1悬空了 /*auto_ptr<Date> p1(new Date(2018, 1, 1)); auto_ptr<Date> p2(p1); unique_ptr<Date> p3(new Date(2018, 1, 1));*/ //unique_ptr<Date> p4(p3)--error; //不支持拷贝,支持移动构造,移动后p3悬空 //unique_ptr<Date> p4(move(p3)); shared_ptr<Date> sp1(new Date); // ⽀持拷贝 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); cout << sp1.use_count() << endl; sp1->_year++; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; // ⽀持移动,但是移动后sp1也悬空 shared_ptr<Date> sp4(move(sp1)); cout<<sp1.get() << endl; return 0; }4.weak_ptr(C++11 引入)
weak_ptr代表弱引用,主要用于解决shared_ptr循环引用的问题。weak_ptr不会影响引用计数,所以不能直接访问资源,必须通过lock()方法转换为shared_ptr才能使用。
示例:
代码语言:javascript
AI代码解释
#include <iostream> #include <memory> class Test { public: void show() { std::cout << "使用 weak_ptr\n"; } }; int main() { std::shared_ptr<Test> sp = std::make_shared<Test>(); std::weak_ptr<Test> wp = sp; // 不影响引用计数 if (auto ptr = wp.lock()) { // 需要转换为 shared_ptr 才能访问 ptr->show(); } else { std::cout << "对象已释放\n"; } return 0; }5. 智能指针的删除器
- 智能指针默认使用
delete释放资源,因此不能直接用于管理非new分配的资源,否则会导致delete释放非法内存。 unique_ptr和shared_ptr支持自定义删除器,即在构造时传入一个可调用对象(函数、lambda、仿函数),来指定资源释放方式。- 例如
new[]需要delete[]释放,因此unique_ptr和shared_ptr也特化了数组版本。
代码语言:javascript
AI代码解释
//特化版本 unique_ptr<Date[]> p1(new Date[5]); shared_ptr<Date[]> p2(new Date[5]);代码语言:javascript
AI代码解释
struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout << "~Date()" << endl; } }; template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; template<class T> void DeleteFunc(T* ptr) { delete[] ptr; } class Fclose { public: void operator()(FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); } }; int main() { //程序崩溃 //unique_ptr<Date> up1(new Date[10]); //shared_ptr<Date> sp1(new Date[10]); // --------特化版本 //unique_ptr<Date[]> p1(new Date[5]); // shared_ptr<Date[]> p2(new Date[5]); //仿函数对象 /// unique_ptr<Date, DeleteArray<Date>> p3(new Date[10],DeleteArray<Date>()); // unique_ptr和shared_ptr支持删除器的方式有所不同 // unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的 // 使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤ /*unique_ptr<Date, DeleteArray<Date>> p3(new Date[10]); shared_ptr<Date> p4(new Date[10], DeleteArray<Date>());*/ //函数指针版本 //unique_ptr<Date, void(*)(Date*)> p1(new Date[3], DeleteFunc<Date>); //shared_ptr<Date> p2(new Date[3], DeleteFunc<Date>); //lambda版本 auto Delete = [](Date* ptr) { delete[] ptr; }; //decltype(Delete) 获取 lambda 的类型,并作为 unique_ptr 的删除器类型。 unique_ptr<Date, decltype(Delete)> p1(new Date[3], Delete); shared_ptr<Date> p2(new Date[3], [](Date* ptr) { delete[] ptr; }); // 实现其他资源管理的删除器 shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); }); return 0; }6.make_shared(推荐使用)
shared_ptr除了可以直接用new资源构造,还可以使用make_shared(),这样更高效:make_shared()直接在shared_ptr的引用计数控制块中分配资源,减少一次new开销,提高性能。make_shared()更安全,避免shared_ptr(new T())可能导致的内存泄漏(如果new T()之后shared_ptr构造失败,可能导致T资源泄漏)。
示例:
代码语言:javascript
AI代码解释
#include <iostream> #include <memory> int main() { std::shared_ptr<int> sp = std::make_shared<int>(42); // 直接构造对象 std::cout << "值:" << *sp << std::endl; return 0; }7. 智能指针的bool转换
shared_ptr和unique_ptr都支持operator bool,用于检查智能指针是否为空:
代码语言:javascript
AI代码解释
std::unique_ptr<int> up; if (up) { /* 不为空 */ } else { /* 为空 */ }这个特性可以直接用于if语句,简化空指针判断。
8. 防止隐式转换
shared_ptr和unique_ptr的构造函数都使用explicit修饰,防止普通指针隐式转换为智能指针:
代码语言:javascript
AI代码解释
void func(std::unique_ptr<int> ptr) {} int main() { // func(new int(10)); // ❌ 错误,不能隐式转换 func(std::make_unique<int>(10)); // ✅ 正确,必须显式转换 return 0; }总结
auto_ptr(已废弃):拷贝会导致原指针悬空,不安全。unique_ptr(推荐):独占所有权,不支持拷贝,支持移动,适用于不需要共享资源的情况。shared_ptr(推荐):共享所有权,支持拷贝和移动,底层使用引用计数。weak_ptr:弱引用,用于解决shared_ptr的循环引用问题。- 推荐使用
make_shared(),更安全更高效。 - 支持自定义删除器,适用于
new[]和非new资源。