智能指针是什么?能否为?

摘要:基本概念 问题背景 在 C++ 中,手动管理资源(内存、文件描述符、互斥锁、数据库连接等)时,容易因异常、提前返回等意外情况导致资源泄漏。例如: class A { int size; char *p;
基本概念 问题背景 在 C++ 中,手动管理资源(内存、文件描述符、互斥锁、数据库连接等)时,容易因异常、提前返回等意外情况导致资源泄漏。例如: class A { int size; char *p; public: A(int s=1):size(s){p = new char[s];} ~A(){delete [] p;} // 析构函数释放堆内存 }; void someFunction() { A *p = new A(100); // 分配资源 // 若此处提前返回或抛出异常,delete p 无法执行,导致内存泄漏 ... delete p; // 手动释放资源,可靠性低 } 这种直接指向资源、无自动管理能力的指针称为原始指针(raw pointer),其核心缺陷是:无法保证资源在任意场景下被妥善释放。 智能指针核心定义 智能指针是 C++ 提供的类模板,用于自动化管理资源,核心特征: 本质是栈对象(局部变量),而非真正的指针; 内部封装了指向资源的原始指针; 离开作用域时,自动调用析构函数释放资源(无需手动操作); 重载 operator-> 和 operator* 运算符,用法与普通指针一致。 智能指针对象(栈上) ├─ 成员:原始指针 *p(指向堆/资源) ├─ 重载:operator->、operator* └─ 析构函数:自动释放 *p 指向的资源 ↓ 资源(堆内存/文件描述符/互斥锁等) 智能指针分类详解 auto_ptr(C++17 已废弃) 基本用法 std::auto_ptr 是早期智能指针,核心作用是自动释放资源,需包含头文件 <memory>: #include <memory> #include <iostream> using namespace std; class A { int size; char *p; public: A(int s=1):size(s){cout<<"构造"<<endl; p = new char[s];} ~A(){cout<<"析构"<<endl; delete [] p;} void resize(int newSize) { // 重新分配堆内存 size = newSize; delete [] p; p = new char[size]; } void info(void){cout << size << endl;} // 输出大小 }; void someFunction(void) { auto_ptr<A> ap(new A(20)); // 封装原始指针 ap->info(); // 等价于 (*ap).info(),输出 20 ap->resize(100); // 操作资源 ap->info(); // 输出 100 } // 离开作用域,ap 析构,自动释放 A 对象资源 // 执行结果: // 构造 // 20 // 100 // 析构 核心缺陷(废弃原因) auto_ptr 支持拷贝构造和赋值运算,但会导致原指针失去资源控制权(逻辑与语法矛盾),编译器无语法限制,极易踩坑: void someFunction(void) { auto_ptr<A> sp1(new A(20)); auto_ptr<A> sp2(sp1); // 拷贝构造:sp1 失去控制权 sp1->info(); // 异常!sp1 已失效 auto_ptr<A> sp3; sp3 = sp2; // 赋值运算:sp2 失去控制权 sp2->info(); // 异常!sp2 已失效 sp3->resize(666); // 仅最后一个指针有效 sp3->info(); // 正常输出 666 } 结论:auto_ptr 仅能单独使用,禁止拷贝构造和赋值操作,因设计缺陷被 C++17 废弃,不推荐使用。 shared_ptr(共享所有权智能指针) shared_ptr 是 C++11 引入的核心智能指针,支持多指针共享同一资源,通过引用计数解决重复释放问题,语法与逻辑自洽。 基本用法 #include <memory> #include <iostream> using namespace std; void someFunction(void) { shared_ptr<A> sp1(new A(30)); // 构造智能指针,引用计数=1 shared_ptr<A> sp2(sp1); // 拷贝构造,引用计数=2 sp1->info(); // 正常输出 30 sp2->info(); // 正常输出 30 shared_ptr<A> sp3; sp3 = sp1; // 赋值运算,引用计数=3 sp1->info(); // 正常输出 30 sp2->info(); // 正常输出 30 sp3->info(); // 正常输出 30 sp1->resize(100); // 修改资源,所有共享指针均受影响 sp1->info(); // 100 sp2->info(); // 100 sp3->info(); // 100 } // 作用域结束:sp3、sp2、sp1 依次析构,引用计数降至 0,释放资源 核心原理:引用计数 shared_ptr 内部维护一个静态引用计数器,核心逻辑: 当智能指针关联资源时,计数器 +1; 智能指针析构(离开作用域)时,计数器 -1; 计数器降至 0 时,自动调用资源的析构函数释放资源。 资源(A 对象) └─ 引用计数器:3 ├─ shared_ptr sp1(关联) ├─ shared_ptr sp2(关联) └─ shared_ptr sp3(关联) 常见问题与解决方案 问题 1:重复关联导致重复释放 直接用同一原始指针构造多个 shared_ptr,会导致计数器独立,析构时重复释放资源: void someFunction(void) { A *p = new A(20); shared_ptr<A> sp1(p); // 计数器=1 shared_ptr<A> sp2(p); // 错误!计数器=1(独立计数) // 析构时 sp1 和 sp2 均释放 p,导致双重释放崩溃 } 解决方案:多个 shared_ptr 共享资源时,通过拷贝构造或赋值操作创建,而非直接用原始指针重复构造: shared_ptr<A> sp1(new A(20)); shared_ptr<A> sp2(sp1); // 正确:拷贝构造,计数器=2 shared_ptr<A> sp3 = sp1; // 正确:赋值,计数器=3 问题 2:循环引用导致内存泄漏 两个类互相持有对方的 shared_ptr,会导致引用计数无法降至 0,资源永久无法释放: class B; // 前置声明 class A { public: shared_ptr<B> spb; // A 持有 B 的 shared_ptr A(int s){cout<<"A 构造"<<endl;} ~A(){cout<<"A 析构"<<endl;} }; class B { public: shared_ptr<A> spa; // B 持有 A 的 shared_ptr B(int s){cout<<"B 构造"<<endl;} ~B(){cout<<"B 析构"<<endl;} }; void someFunction(void) { shared_ptr<A> spa(new A(100)); // 计数器=1 shared_ptr<B> spb(new B(200)); // 计数器=1 spa->spb = spb; // A 的 spb 关联 B,B 计数器=2 spb->spa = spa; // B 的 spa 关联 A,A 计数器=2 } // 析构时:spa 计数器=1,spb 计数器=1,均不释放,内存泄漏 shared_ptr spa → A 对象 → shared_ptr spb ↑ ↓ shared_ptr spb → B 对象 → shared_ptr spa 解决方案:weak_ptr(弱引用指针) weak_ptr 是 shared_ptr 的辅助指针,核心特性: 仅能通过 shared_ptr 或其他 weak_ptr 构造; 构造/析构不影响引用计数; 无法直接访问资源,需通过 lock() 转换为 shared_ptr 后操作。 修改代码如下: class B; class A { public: weak_ptr<B> spb; // 改为 weak_ptr A(int s){cout<<"A 构造"<<endl;} ~A(){cout<<"A 析构"<<endl;} }; class B { public: weak_ptr<A> spa; // 改为 weak_ptr B(int s){cout<<"B 构造"<<endl;} ~B(){cout<<"B 析构"<<endl;} }; void someFunction(void) { shared_ptr<A> spa(new A(100)); shared_ptr<B> spb(new B(200)); spa->spb = spb; // weak_ptr 不增加 B 的计数器(仍为1) spb->spa = spa; // weak_ptr 不增加 A 的计数器(仍为1) // 访问资源:先判断是否过期,再 lock() 转换 if(!spa->spb.expired()) { // expired():判断资源是否释放 spa->spb.lock()->info(); // lock():返回 shared_ptr,访问资源 } } // 析构时:spa 计数器=0(释放 A),spb 计数器=0(释放 B),无泄漏 shared_ptr spa → A 对象 → weak_ptr spb(虚线,不计数) ↑ ↓ shared_ptr spb → B 对象 → weak_ptr spa(虚线,不计数) 问题 3:多线程访问失序 shared_ptr 本身的引用计数是线程安全的,但资源的访问并非线程安全,多线程并发读写会导致数据竞争: #include <thread> #include <mutex> using namespace std; class A { public: int x; A(int x=0):x(x){} void setX(int x){this->x = x;} }; mutex m; // 互斥锁 void routine1(shared_ptr<A> sp) { while(1) { m.lock(); // 加锁保证原子操作 if (sp->x % 2 == 0) cout << sp->x << "是偶数" << endl; m.unlock(); // 解锁 usleep(10*1000); } } void routine2(shared_ptr<A> sp) { srand(time(NULL)); while(1) { m.lock(); // 加锁保证原子操作 sp->setX(rand()%1000); m.unlock(); // 解锁 } } int main() { shared_ptr<A> sp(new A); thread t1(routine1, sp); thread t2(routine2, sp); t1.detach(); t2.detach(); pthread_exit(NULL); } 解决方案:对共享资源的访问添加互斥锁(mutex),保证同一时间只有一个线程操作资源。 unique_ptr(独占所有权智能指针) unique_ptr 是 C++11 引入的轻量级智能指针,核心特性:资源独占,禁止拷贝构造和赋值操作,性能优于 shared_ptr。 基本用法 #include <memory> #include <iostream> using namespace std; void someFunction(void) { unique_ptr<A> up(new A(20)); // 独占资源 up->info(); // 正常输出 20 up->resize(200); up->info(); // 正常输出 200 // 错误!禁止拷贝构造 // unique_ptr<A> up2(up); // 错误!禁止赋值操作 // unique_ptr<A> up3; up3 = up; } // 离开作用域,up 析构,释放资源 核心原理:屏蔽拷贝与赋值 unique_ptr 通过 = delete 显式删除拷贝构造和赋值运算符,从语法上禁止资源共享: template <typename T> class myUniquePtr { T *p; public: myUniquePtr(T *p=nullptr):p(p){} ~myUniquePtr(){delete p;} // 析构释放资源 // 显式删除拷贝构造和赋值运算符 myUniquePtr(const myUniquePtr &r) = delete; myUniquePtr &operator=(const myUniquePtr &r) = delete; // 重载运算符,支持指针用法 T *operator->(){return p;} T &operator*(){return *p;} // 辅助接口 T *release() { // 释放所有权,返回原始指针 T *temp = p; p = nullptr; return temp; } void reset(T *newP=nullptr) { // 重置资源 delete p; p = newP; } }; 拓展:移动语义(C++11+) unique_ptr 虽禁止拷贝,但支持移动语义(std::move),可将资源所有权转移给另一个 unique_ptr: void someFunction(void) { unique_ptr<A> up1(new A(20)); unique_ptr<A> up2 = move(up1); // 移动语义:up1 失去所有权,up2 独占 up2->info(); // 正常输出 20 // up1->info(); 错误!up1 已失效 } 拓展(新增) 智能指针的选择策略 场景 推荐智能指针 核心原因 资源独占,无需共享 unique_ptr 性能最优,语法禁止共享,无引用计数开销 资源需多指针共享 shared_ptr + weak_ptr 支持共享所有权,weak_ptr 解决循环引用 兼容旧代码(C++17 前) 无(避免 auto_ptr) auto_ptr 设计缺陷,已废弃 进阶技巧 使用 make_shared 构造 shared_ptr(推荐) std::make_shared 是构造 shared_ptr 的更安全方式,避免原始指针暴露,且内存分配更高效(一次分配资源和计数器): // 推荐:make_shared 构造 shared_ptr<A> sp = make_shared<A>(20); // 不推荐:直接用原始指针构造 shared_ptr<A> sp(new A(20)); 自定义删除器 默认情况下,智能指针使用 delete/delete[] 释放资源,可自定义删除器处理特殊资源(如数组、文件描述符): // 示例:管理数组(默认 delete 不适用数组,需自定义删除器) shared_ptr<int> sp(new int[10], [](int *p){delete[] p;}); // 示例:管理文件描述符 #include <fcntl.h> #include <unistd.h> shared_ptr<int> fd_ptr(new int(open("test.txt", O_RDONLY)), [](int *fd){close(*fd); delete fd;}); weak_ptr 的其他接口 use_count():返回关联资源的引用计数(仅作参考,非原子操作); lock():若资源未释放,返回非空 shared_ptr;否则返回空 shared_ptr; reset():解除与资源的关联。 线程安全性补充 shared_ptr 的引用计数是线程安全的(原子操作); shared_ptr 指向的资源并非线程安全,需手动加锁保护; unique_ptr 无共享场景,线程安全取决于资源的访问方式。