- 作者: 雪山肥鱼
- 时间:20211211 16:25
- 目的: 复习智能指针
内容来源: <<深入应用C++11>>
第4章 使用C++11解决内存泄漏的问题
引言
只能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域,自动的销毁动态分配的对象,放置内存泄漏。它的一种通用实现技术是使用引用计数。每使用一次,内部引用计数+1,每析构一次,内部引用计数-1,减为0时, 删除所指向的堆内存。
C++11 实现了3种智能指针:
- std::shared_ptr
- std::uniq_ptr
- std::weak_ptr
引入的头文件 <memory>
1. shared_ptr 共享的智能指针
shared_ptr 使用引用计数,每一个shared_ptr 拷贝都指向相同的内存。直到最后一个shared_ptr 析构的时候,内存才会被释放。计数归0.
1.1 shared_ptr的基本用法
- 初始化
//shared_ptr 初始化方式
shared_ptr<int> p (new int(1));
shared_ptr<int> p2 = p;
//未初始化的智能指针可以通过 reset 初始化
shared_ptr<int> ptr;
ptr.reset(new int(1)); //reset 是的 引用计数-1
if(ptr) {
cout<<"ptr is not null"<<endl;
}
auto t = std::make_shared<int>(10);
不能将一个原始指针,直接赋值给一个智能指针
shared_ptr<int> p = new int (1); //编译错误,不允许直接赋值
智能指针的用法和普通指针用法相似,只不过,不用管理自己分配的内存。
shared_ptr 不能通过直接将原始指针赋值来初始化,需要通过构造函数和辅助方法来初始化
由代码可知,智能指针可以通过重载的bool 来判断是否为空
- 获取原始指针
shared_ptr<int> ptr(new int(1));
int * p = ptr.get();
- 指定删除器
void DeleteIntPtr(int *p) {
delete p;
}
shared_ptr<int> p(new int, DeleteIntPtr);
//删除器也可以用lambda
shared_ptr<int> p(new int, [ ](int * p){delete p;});
但是 shared_ptr 管理动态数组的时候,需要指定删除器, 也就是说并不完美的支持 数组
//方法1:
shared_ptr<int> p (new int[10], [](int*p){delete [ ] p;});
//方法2:
shared_ptr<int> p (new int [10], default_delete<int[ ]>);
//方法3 封装 make_shared_array 方法,让 shared_ptr支持数组
template<typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
测试代码如下:
shared_ptr<int> p = make_shared_array<int>(10);
shared_ptr<char> p = make_shared_array<char>(10);
shared_ptr 应该注意的问题
- 不要用一个原始指针初始化多个shared_ptr
int * ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //logic error
- 不要在函数实参种创建shared_ptr
function(shared_ptr<int>(new int), g( )); //有缺陷
因为C++ 的函数参数计算顺序在不同的编译器,不同的调用约定下,调用顺序不同,一般从右到左,也可能从左到右。
如果先new int, 然后调g( ), 如果g( ) 发生异常,而 shared_ptr<int> 还没创建,则int 内存泄漏。
正确写法:
shared_ptr<int> p (new int());
f(p, g() );
- 通过shared_from_this() 返回 this 指针,不要将this 指针作为shared_ptr 返回出来,因为 this 本质上是一个裸指针,非常有可能导致重复析构。
struct A {
shared_ptr<A> GetSelf() {
return shared_ptr<A>(this);
}
};
int main(int argc, char **argv) {
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
用同一个 this 指针 构造了 两个智能指针 sp1, sp2, 而两者之间又毫无瓜葛。所以 离开mian 函数后 会各自析构,导致重复析构的错误。
正确返回this 的 shared_ptr 做法是: 让目标类通过派生 std::enable_shared_from_this<T>类,然后使用基类成员 shared_from_this 返回this 的shared_ptr。
class A: public std::enable_shared_from_this<A> {
shared_ptr<A> GetSlef() {
return shared_from_this();
}
}
shared_ptr<A> spy(new A);
shared_ptr<A> p = spy->GetSelf();
手动的让两者建立联系
- 避免循环引用。智能指针的最大一个陷阱就是循环引用,循环引用会导致内存泄漏。
struct A;
struct B;
struct A {
shared_ptr<B> bptr;
~A() {
cout<<"A is deleted!"<<endl;
}
};
struct B {
shared_ptr<A> aptr;
~B() {
cout<<"B is deleted!"<<endl;
}
}
void TestPtr() {
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
}
循环引用导致 ap, bp 的计数均为 2, 脱离作用域后,为1,不会减少到0.导致两个指针不会被析构,插死你哼内存泄漏。解决办法为后续的weak_ptr.
2. unique_ptr 独占的智能指针
unique_ptr 是一个独占的智能指针,不允许其他智能指针共享其内部的指针。
unique_ptr<T> myPtr(new T);
unique_ptr<T> myOtherPtr = myPtr; //错误 不可复制
但可以通过std::move, 转移其他unique_ptr,这样它本身就不再拥有原来指针的所有权了。
unique_ptr<T> myPtr(new T);
unique_ptr<T> myOtherPtr = std::move(myPtr);
//错误
unique_ptr<T> ptr = myPtr;
unique_ptr 不像shared_ptr 可以通过make_shared 方法来创建智能指针。至少c++11,没有,c++14 有类似的make_unique来创建unique_ptr.
template<class T, class... Args> inline
typename enable_if<!is_array<T>::value, unique_ptr<T>>::type
make_unique(Args&&... args)
{
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
template<class T> inline
typename enable_if<is_array<T>::value && extent<T>::value == 0, unique_ptr<T>>::type
make_unique(size_t size)
{
typedef typename remove_extent<T>::type U;
return unique_ptr<T>(new U[size]());
}
template<class T, class... Args>
typename enable_if<extent<T>::value !=0, void>::type
make_unique(Args&&...) = delete;
- 不同点 1
- 不是数组,直接创建 unique_ptr
- 是数组
2.1 先判断是否为定长数组,如果是定长数组则编译不通过
因为不能这样调用make_unique<T[10]>(10), 应该 make_unique<T[ ]>(10)
2.2 若非定长数组,则获取数组种的元素类型,再根据入参size 创建动态数组的unique_ptr
- 不同点2
unique_ptr 和 shared_ptr 相比,unique_ptr 除了独占性这个特点外,还可以指向一个数组。
std::unique_ptr<int [ ]> ptr(new int[10]);
ptr[9] = 9;
注意:
shared_ptr<int[ ]>ptr(new int[10]) 是不合法的
不同点3
删除器指定方式不同
std::shared_ptr<int> ptr(new int(1), [ ](int *p){delete p;}); //正确
std::unique_ptr<int> ptr(new int(1), [ ](int* p){delete p;}); //错误
unique_ptr 指定删除器的时候,需要确定删除器的类型,所以不能像shared_ptr 那样直接指定删除器
std::unique_ptr<int, void(*)(int *)>ptr(new int(1), [ ](int* p){delete p;});
带了 lambda ,如果没有捕获变量,是正确的,如果捕获变量,则会编译报错。
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [ &](int* p){delete p;});
lambda在没有捕获变量的情况下是可以转换为函数指针的,一旦捕获了就无法转换了,因为你在类型里指定的是函数指针
如果希望删除器 支持 lambda,
std::unique_str<int, std::function<void(int*)>>ptr(new int(1), [&](int* p){delete p;});
换句话,lambda如果捕获了变量,则转换为 function 对象
当然也可以自定义删除器 - 仿函数
#include <memory>
#include <functional>
using namespace std;
struct MyDeleter
{
void operator()(int*p)
{
cout<<"delete"<<endl;
delete p;
}
};
int main() {
std::unique_ptr<int, MyDeleter> p (new int(1));
return 0;
}
非常明显,如果希望只有一个智能指针管理资源或管理数组,就用unique_ptr, 如果希望多个智能指针管理同一个资源,就用 shared_ptr
3. weak_ptr 弱引用的智能指针
weak_ptr 可以视为 是 shared_ptr 的辅助,并不管理shared_ptr内部内容,主要是为了监视 shared_ptr的生命周期。
weak_ptr 没有重载 * 和 -> ,不共享指针,不能操作资源,主要是通过shared_ptr 获得资源的监控权。不增加引用计数,析构也不会减少引用计数。
weak_ptr 可以用来返回 this 指针和 解决循环引用的问题
3.1 weak_ptr 基本用法
- 通过 use_count()方法获得当前资源的引用计数:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout<<wp.use_count()<<endl; //1
- 通过 expired() 方法来判断所观测的资源是否已经被释放
shared_ptr<int > sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
std::cout<<"invalid weakptr, original ptr has been released"<<std::endl;
else
std::cout<< "valid weakptr"<<endl;
- 通过lock 方法获取所监视的 shared_ptr
std::weak_ptr<int> gw;
void f() {
if(gw.expired()) {
std::cout<<"gw is expired\n";
}
else {
auto spt = gw.lock();
std::cout<<*spt<<std::endl;
}
}
int main(int argc ,char **argv) {
{
auto sp = std::make_shared<int>(42);
gw = sp;
f();
}
f();
}
//output
42
gw is expired
3.2 weak_ptr 返回 this 指针
不能直接将 this 指针 返回为 shared_ptr,需要通过派生 std::enable_shared_from_this类,并通过其方法 shared_from_this 返回 智能指针。
原因:
std::enable_shared_from_this类中 有一个 weak_ptr,这个weak_ptr 用来观测 this指针。
调用shared_from_this() 方法时,会调用内部 weak_ptr的lock()方法,将所观测的shared_ptr 返回。
一个是直接返回 this, 一个是 返回shared_ptr,就是两者的不同。
struct A : public std::enable_shared_from_this<A> {
std::shared_ptr<A> GetSelf() {
return shared_from_this();
}
~A() {
cout << "A is deleted"<<endl;
}
}
std::shared_ptr<A> spy(new A);
std::shared_ptr<A> p = spy->GetSelf();
以上的操作都是安全的。shared_from_this(),调用的是内部的weak_ptr的 lock(),返回的智能指针。在离开作用域之后,spy的引用计数为0. A对象会被析构,不会造成重复析构的问题。
注意,获取自身智能指针的函数,仅在shared_ptr<T> 的构造函数执行完成后才能使用,因为 enbale_shared_from_this 内部 的 weak_ptr 只有通过shared_ptr 才能构造。因为存在继承的关系嘛
3.3 weak_ptr解决循环引用问题
问题:
struct A;
struct B;
struct A {
shared_ptr<B> bptr;
~A() {
cout<<"A is deleted!"<<endl;
}
};
struct B {
shared_ptr<A> aptr;
~B() {
cout<<"B is deleted!"<<endl;
};
void TestPtr() {
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
}
}
上述代码中 ,由于循环引用 ap, bp 的引用计数分别为2. 俩开作用域后,都只减1. 会造成内存泄漏。
结局方案:只要将A 或 B 的任意一个成员改为 weak_ptr即可。
struct A;
struct B;
struct A {
shared_ptr<B> bptr;
~A() {
cout<<"A is deleted!"<<endl;
}
};
struct B {
weak_ptr<A> aptr;//修正为weak_ptr
~B() {
cout<<"B is deleted!"<<endl;
}
};
void TestPtr() {
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp (new B);
ap-> bptr = bp;
bp->aptr = ap;
}
}
在对B 的成员赋值时,即执行bp->aptr = ap 时,由于 aptr 是 weakptr,所以 ap 的引用计数为1.
所以 ap 析构干净后,为0, 那么ap 中的 bp 析构一次,bp 自己再析构一次。则 不会发生内存泄漏。
4. 通过智能指针管理第三方库的分配内存
用智能指针来管理第三方库分配的内存是比较稳妥的。
通常下面的代码是存在风险的
void *p = GetHandle() -> Create();
// do sth...
Gethandle() -> Release(p);
总会有人忘记 Release.
void * p = GetHandle()->Create();
shared_ptr<void> sp (p, [this](void * p) {
GetHandle() -> Release(p); //为什么会挂上this,我不太理解
})
上述代码,有些繁琐,因为每次 create() 后都要写这么一长串。 可以提炼出一个公共函数
shared_ptr<void> Guard(void * p) {
return shared_ptr<void> sp( p, [this](void * p) {Gethandle() -> Release(p);
}
}
void *p = GetHandle() -> Create();
auto sp = Guard(p);
//do sth...
上述代码虽然做了简化,但是仍然有隐患,如果没用变量去接 Guard(p) ,该行结束,则p 会被当作临时对象释放。
会很容易造成野指针的出现。
所以用宏是最安全的
#define GUARD(p) std::shared_ptr<void> p##p(p, [ ](void *p) {GetHandle()->Release(p) });
void*p = GetHandle()->Create();
GUARD(p);
会用p 初始化一个 新的 shared_ptr<int> pp,所以是较为安全的
通过宏定义的方式可以保证不会忘记智能指针没有赋值,即方便又安全。
4 总结
智能指针是为没有GC的语言解决可能的内存泄漏问题。但是实际应用中,智能指针有一些需要注意的地方,好在这些问题都可以解决。
- shared_ptr 和 uniq_ptr 使用时 如何选择
- weak_ptr 是 shared_ptr 的 助手。只是监视shared_ptr 而已。解决shared_ptr 的循环引用和this 指针问题。
网友评论