智能指针
Published on
C++11标准废弃了原来的智能指针auto_ptr,同时引入了三个新的智能指针:unique_ptr, shared_ptr与weak_ptr。它们基于RAII原则进行内存管理。
RAII(资源获取即初始化)
C++中可以在栈上或堆上申请内存,栈上申请的内存会在超出作用域时自动释放,但是堆上申请的内存需要手动释放,否则会出现内存泄漏。在现代C++中,通常基于RAII原则编写代码,以避免出现内存泄漏。
RAII的基本原则是:尽可能在栈上申请资源,如果由于资源太大或其它原因必须在堆上申请时,则该资源需要由一个栈上的对象持有;在该对象初始化时即获取该资源,同时在该对象的析构函数中释放该资源。在超出持有资源对象的作用域时,其析构函数会被自动调用,进而释放所持有的资源,从而避免了内存泄漏。
unique_ptr
特点
unique_ptr对象持有一个底层指针,在unique_ptr对象析构时会自动释放该指针。通过禁用拷贝构造函数与拷贝赋值操作符,unique_ptr保证此底层指针只有一个持有者。如需转移底层指针的所有权,可以使用移动语义,移动后原unique_ptr失效。
在确定同时只能有一个所有者的情况下,使用unique_ptr可以在编译期间即保证此约束,从而避免运行期间出现错误,同时可以简化代码逻辑并提高运行效率。
实现
在unique_ptr类中,至少需要实现以下功能:
- 普通构造函数:保存传入的指针
- 析构函数:释放持有的指针
- 拷贝构造函数/赋值操作符:禁止使用,避免多个持有者
- 移动构造函数/赋值操作符:实现底层指针所有权的转移
- *与->操作符:实现与普通指针相同的操作
一个简单的实现如下:
template <typename T>
class unique_ptr
{
public:
// 普通构造函数,保存传入的指针
unique_ptr(T *p = nullptr) : ptr_(p) {}
// 析构函数
~unique_ptr()
{
// 底层指针非空则释放
if (ptr_)
{
delete ptr_;
ptr_ = nullptr;
}
}
// 禁用拷贝构造函数与赋值操作符,保证只有一个所有者
unique_ptr(const unique_ptr &) = delete;
unique_ptr &operator=(const unique_ptr &) = delete;
// 移动构造函数
unique_ptr(unique_ptr &&p)
{
// 保存底层指针
ptr_ = p.ptr_;
// 将原unique_ptr中的底层指针置空,避免同时有两个所有者
p.ptr_ = nullptr;
}
// 移动赋值操作符,与移动构造函数做法相同
unique_ptr &operator=(unique_ptr &&p)
{
ptr_ = p.ptr_;
p.ptr_ = nullptr;
return *this;
}
// 实现*与->操作符,使智能指针的使用与普通指针相同
T &operator*() const noexcept
{
return *ptr_;
}
T *operator->() const noexcept
{
return ptr_;
}
// 实现其它所需方法
// ......
private:
// 底层指针
T *ptr_;
};
使用
struct A
{
// ......
};
int main()
{
// 建立unique_ptr对象,传入需要管理的底层指针(对传入的指针不要再做任何操作)
std::unique_ptr<A> pa = std::unique_ptr<A>(new A);
// 错误,禁止复制unique_ptr
std::unique_ptr<A> pb(pa);
std::unique_ptr<A> pb = pa;
// 正确,可以移动所有权,移动后通过pb可以访问所管理指针,通过pa不再能访问
std::unique_ptr<A> pb = std::move(pa);
// 推荐使用make_unique函数建立unique_ptr(C++14添加),此方式更加简洁,效率更高,且保证异常安全
std::unique_ptr<A> pc = std::make_unique<A>();
} // 退出作用域后,所有智能指针与底层指针都被自动释放
shared_ptr
特点
与unique_ptr不同,在shared_ptr中,一个底层指针可以有多个持有者,因此shared_ptr可以进行拷贝构造或赋值。shared_ptr通过添加一个引用计数来记录持有者的数量,当最后一个持有者对象析构时,会自动释放底层指针,从而保证没有内存泄漏。
shared_ptr适合应用在同时可能有多个持有者操作指针,但是并不知道哪个持有者最后释放的场景中。
实现
在shared_ptr类中,至少需要实现以下功能:
- 普通构造函数:保存传入的指针,引用计数设为1
- 析构函数:引用计数-1,如果归0,则释放所持有的指针
- 拷贝构造函数/赋值操作符:复制底层指针,引用计数+1
- *与->操作符:实现与普通指针相同的操作
一个简单的实现如下:
template <typename T>
class shared_ptr
{
public:
// 普通构造函数,保存传入的指针,初始化引用计数为1
shared_ptr(T *p = nullptr) : ptr_(p), rc_(new int(1)) {}
// 析构函数
~shared_ptr()
{
// 释放当前资源
release();
}
// 拷贝构造函数
shared_ptr(const shared_ptr &p) : ptr_(p.ptr_), rc_(p.rc_)
{
// 拷贝数据后引用计数+1
++(*rc_);
}
// 拷贝赋值操作符
shared_ptr &operator=(const shared_ptr &p)
{
// 如果不指向同一底层指针
if (ptr_ != p.ptr_)
{
// 释放当前资源
release();
// 拷贝数据
ptr_ = p.ptr_;
rc_ = p.rc_;
// 引用计数+1
++(*rc_);
}
return *this;
}
// 实现*与->操作符,使智能指针的使用与普通指针相同
T &operator*() const noexcept
{
return *ptr_;
}
T *operator->() const noexcept
{
return ptr_;
}
// 实现其它所需方法
// ......
private:
// 释放当前资源
void release()
{
// 引用计数-1
--(*rc_);
// 如果引用计数归0,释放底层指针与引用计数指针
if (*rc_ == 0)
{
delete ptr_;
delete rc_;
ptr_ = nullptr;
rc_ = nullptr;
}
}
// 底层指针
T *ptr_;
// 引用计数
int *rc_;
};
使用
struct A
{
// ......
};
int main()
{
// 建立shared_ptr对象,传入需要管理的底层指针(对传入的指针不要再做任何操作)
std::shared_ptr<A> pa = std::shared_ptr<A>(new A);
// 可以复制与赋值,引用计数增加
std::shared_ptr<A> pb(pa);
std::shared_ptr<A> pb = pa;
// 同样推荐使用make_shared函数建立shared_ptr
std::shared_ptr<A> pc = std::make_shared<A>();
} // 退出作用域后,所有智能指针被自动释放(如果没有其它持有者,则底层指针也被释放)
weak_ptr
特点
weak_ptr是弱化版本的shared_ptr,它仅用于监视shared_ptr中管理的资源是否存在,以避免出现循环引用等问题。weak_ptr并不影响引用计数,也不能通过weak_ptr直接使用底层指针。
作用
shared_ptr对象未释放前,其持有的底层指针不会释放,如果出现循环引用的情况,会造成内存泄漏。例如:
struct B;
struct A
{
std::shared_ptr<B> b;
};
struct B
{
std::shared_ptr<A> a;
};
int main()
{
std::shared_ptr<A> pa = std::make_shared<A>(); // 建立A指针,引用计数为1
std::shared_ptr<B> pb = std::make_shared<B>(); // 建立B指针,引用计数为1
pa->b = pb; // B指针引用计数为2
pb->a = pa; // A指针引用计数为2
} // 两个底层指针引用计数仍为1,因此没有释放,造成内存泄漏
上面的例子中,pa与pb相互持有对方的一个shared_ptr,因此存在循环引用。即使在主函数退出后,两个指针的引用计数仍然都为1,因此不能释放,导致内存泄漏。修改方法是将循环引用中的任意一个shared_ptr改为weak_ptr,例如:
struct B
{
std::weak_ptr<A> a;
};
使用
int main()
{
// weak_ptr必须使用shared_ptr初始化
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp1;
// 通过expired函数检查是否失效
if (!wp.expired())
{
// 通过lock函数生成shared_ptr才能使用
std::shared_ptr<int> sp2 = wp.lock();
*sp2 = 100;
}
}
其它
自定义删除器(deleter)
unique_ptr与shared_ptr默认使用delete语句释放指针,用户可以在建立智能指针时指定一个函数/仿函数/lambda表达式执行自定义释放操作。
std::unique_ptr<D, void (*)(D *)> p1(new D, [](D *ptr) { delete ptr; });
std::shared_ptr<D> p2(new D, [](D *ptr) { delete ptr; });
数组的智能指针
如果将数组作为底层指针传入智能指针,由于默认使用delete对底层指针进行释放,此时会产生未定义行为。可以使用自定义删除器或模板特化的方法避免此问题。
// 错误,使用默认删除器会产生未定义行为
std::shared_ptr<int> sp1(new int[10]());
// 正确,使用自定义删除器
std::shared_ptr<int> sp2(new int[10](), std::default_delete<int[]>());
// 正确,使用模板特化(C++17中添加),同时实现了[]操作符,可以将智能指针作为数组使用
std::shared_ptr<int[]> sp3(new int[10]());
sp3[0] = 10;