C++ 智能指针详解
引言
在 C++ 编程中,动态内存管理始终是开发者面临的核心挑战之一。手动使用 new 分配内存、delete 释放内存的模式,不仅需要开发者时刻关注内存生命周期,更可能因疏忽导致内存泄漏(忘记调用 delete)、二次释放(重复调用 delete),或是在异常抛出时因执行流跳转跳过 delete 语句等问题。
为解决这一痛点,C++ 标准库引入了智能指针这一核心工具。它基于'资源获取即初始化'(RAII)的设计思想,将动态内存资源封装为对象的成员,利用 C++ 对象自动调用析构函数的特性,实现内存的'自动释放',从根本上减少手动管理内存的负担与风险。
本文将系统梳理智能指针的核心作用、实现原理,并针对 C++ 标准库中不同类型的智能指针展开详细解析,包括它们的设计逻辑、模拟实现代码、适用场景,以及如何规避典型问题。
智能指针的作用
作用:如果正确使用了智能指针的话,就不用自己手动
delete了。自己手动
delete容易忽略的地方:在抛异常时容易跳过自己写的delete或者自己忘记delete了。
智能指针的实现和原理
原理:资源交给对象管理,对象生命周期内,资源有效;对象生命周期到了,释放资源——用的是 RAII 思想。
实现要求:让对象的用法像指针一样。
template <class T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {}
~SmartPtr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
使用示例:
SmartPtr<string> sp(new string("renshen"));
这种基础智能指针有个问题:没有处理拷贝语义,可能导致双重释放。
标准库中的智能指针
不需要拷贝的场景,一般使用
unique_ptr。需要拷贝的场景,一般使用
shared_ptr,但要小心循环引用。
std::auto_ptr
这是 C++98 实现的智能指针,原理是管理权的转移。在拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象,被拷贝对象悬空。
这个智能指针有很大的弊端:如果管理权被转移了,之前那个指针就会变成空指针。因此很多公司明令禁止使用。
auto_ptr 的模拟实现
template <class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
~auto_ptr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 拷贝构造函数,转移所有权
auto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) {
ap._ptr = nullptr;
}
// 赋值运算符也会转移所有权
auto_ptr& operator=(auto_ptr<T>& ap) {
if (this != &ap) {
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
std::unique_ptr
unique_ptr, shared_ptr 和 weak_ptr 是 Boost 库先实现的,后来 C++11 也支持这几个智能指针的使用了。
unique_ptr 直接让智能指针不能拷贝和赋值,且不支持管理权转移(除非显式移动)。这个智能指针也是基于 RAII 思想实现的。
unique_ptr 的模拟实现
template <class T>
class unique_ptr {
public:
unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 禁止拷贝
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr& operator=(const unique_ptr<T>&) = delete;
// 允许移动
unique_ptr(unique_ptr<T>&& other) noexcept : _ptr(other._ptr) {
other._ptr = nullptr;
}
unique_ptr& operator=(unique_ptr<T>&& other) noexcept {
if (this != &other) {
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
引申:一般拷贝不让实现的话,赋值也不能实现。
std::shared_ptr
这个智能指针是最全面的,一般都是用的这个,然后有循环引用的时候搭配 weak_ptr 用。
原理:通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。
shared_ptr 的模拟实现
template <class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}
template <class D>
shared_ptr(T* ptr, D del) : _ptr(ptr), _pcount(new int(1)), _del(del) {}
~shared_ptr() {
if (--(*_pcount) == 0) {
_del(_ptr);
delete _pcount;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr == sp._ptr) return *this;
if (--(*_pcount) == 0) {
_del(_ptr);
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
T* get() const { return _ptr; }
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr){ delete ptr; };
};
用法:
shared_ptr<A> sp1(new A(1));
注意:shared_ptr<A> 跟 A 可不是一个类型。
编译器对越界访问的检查一般不是很彻底的。
shared_ptr 的一个弊端
shared_ptr 在遇到循环引用的时候也是会内存泄漏的。
std::weak_ptr
weak_ptr 这个智能指针不是 RAII 思想的,它的唯一作用就是解决 shared_ptr 的循环引用问题的。
这个智能指针可以访问资源,但是不参与资源释放的解决。
注意:weak_ptr 库里面实现了让它可以和 shared_ptr 相互转换。
weak_ptr 的模拟实现
template <class T>
class weak_ptr {
public:
weak_ptr() : _ptr(nullptr) {}
weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp.get();
return *this;
}
T* get() const { return _ptr; }
private:
T* _ptr;
};
删除定制器
在智能指针使用过程中,可能会有 new[] 或者 malloc 出来的空间,这些空间用 delete 根本不行,此时就需要删除定制器了。
库里面的话,是给 unique_ptr 和 shared_ptr 配备了删除定制器的。
这里的话,自己模拟实现一下 shared_ptr 的删除定制器(在上面的模拟实现里面已经写了)。
总结
掌握智能指针是安全 C++ 开发的关键。根据所有权需求选择 unique_ptr 或 shared_ptr,配合 weak_ptr 避免循环引用,并使用自定义删除器处理特殊内存分配,能有效杜绝内存泄漏。


