目录
背景
裸指针
智能指针
原理
智能指针
auto_ptr
unique_ptr
1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)
1.1 C++提供了标准库函数move()
1.2.如果unique_ptr是一个临时右值
2. unique_ptr可用于数组
shared_ptr
环状引用问题
weak_ptr
注意:
总结
背景
-
裸指针
如果编程中直接使用裸指针,就需要程序员手动的去allocate和release资源了。假设有这样一段代码:
void f(){
int* a = new int(100); //申请资源
....
delete a; //release a所指向的对象
}
这段程序乍看起来没有什么问题,new和delete有成对地出现,应该不会发生内存泄漏的情况吧。那如果程序员忘记写delete了;或者是像上面一样有加delete,但这new和delete中间的"...."区域内是否有可能出现过早的return语句呢,亦或是这块"...."区域的语句抛出了异常,无论是哪一种原因,最后其实都没有做delete,那这种情况下内存泄露就发生了。
那怎么避免这种问题呢?最直接的想法可能就是“既然前面说的问题是因为程序可能会提前退出导致的,那就在程序每一个可能会退出的地方,都加上一个delete,以进行全面的防守”。这样子做当然没有问题,但是越到后面,可能就越难维护了,假设后面的人,在这段程序里又加了一些内容,使得程序可能又新增了一个会提前退出的地方,但是后面的人不知道这个函数里还有着对资源管理的事情要做,而忘记了加delete,还是会引入问题。所以单纯地依赖在f()里总是会手动地执行其delete语句不是一个很好的做法。
-
智能指针
那前面说的问题还有其他更好的解决办法吗?答案是:有。这个方法就是采用智能指针(smart pointer).
智能指针能够解决前面说的“因为忘记delete或者是提前退出导致的内存泄漏问题”,在介绍智能指针是如何使用之前,还是需要了解一下智能指针是怎么解决掉前面说的那些问题的,它能解决这个问题的原理是什么。
-
原理
智能指针是基于这样一个思想:以对象来管理资源。简单来说就是,把资源放进一个对象里面,将资源释放的动作放在析构函数里,我们便可以依赖C++的析构函数自动调用机制确保资源被释放。
“以对象来管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)”,这里面有两个关键的想法:
- 获得资源后立刻放进管理对象内:每一笔资源在获得的同时立刻被放进管理对象当中
- 管理对象运用析构函数确保资源被释放:不论流程上如何控制,一旦管理对象被销毁,其析构函数自然会被调用,其所管理的资源也就会被释放掉
没错,智能指针正是一个类(模板类,因为指针类型很多),它所管理的资源就是指针,可以简单地理解为就是在这个类的析构函数里有做一个delete的动作。智能指针这个模板类重载了一些指针相关的运算操作符(比如“*”,“->”),让程序员在操作这个对象时就像是操作普通指针一样。所以智能指针不仅使得人们可以像使用普通指针那样去使用它,还可以防止对内存的不当使用。
这里再多说几句:以对象来管理资源,其实不仅仅是内存是资源,其他的比如:互斥锁(Mutex)、文件描述器(File descriptor) 等其实也是资源,一旦你用了它,将来就必须要还给系统,如果不这样,就会发生各种事故。资源交给对象来管理,只是不同的资源所对应的管理对象不一样,比如互斥锁的管理对象一般使用的是lock_guard、unique_lock这些类的对象(C++ 多线程 (mutex & conition_variable篇)),但是基本原理是一样的,就是利用了C++对象的构造函数和析构函数的自动调用机制,防止资源泄漏发生。
智能指针
前面介绍了智能指针的大概原理,接下来看看C++里提供的智能指针有哪些,以及用法。
C++提供的智能指针有如下几种:auto_ptr(C++11中已经摒弃)、shared_ptr、weak_ptr和unique_ptr,就像前面说的,它们都是模板类。关于这些智能指针:
- 要创建智能指针对象,必须包含头文件<memory>;
- 所有的指针类都有一个explicit构造函数,该函数将指针作为参数
- 可以通过智能指针类提供的get()方法获取智能指针所指向的原始资源
- 使用通常的模板语法来实例化所需类型的指针(weak_ptr的构造方法稍微特殊点,后面会介绍),比如:
auto_ptr<int> a(new int(100));
unique_ptr<int> a(new int(100));
shared_ptr<int> a(new int(100));
weak_ptr<int> b = a;
为什么会有多种不同类型的智能指针呢?接下来介绍一下这些不同类型的智能指针的差异
auto_ptr
auto_ptr是C++ 98提出来的,C++11中已经摒弃了。它的用法如下:
void f(){
auto_ptr<int> a(new int(100));
....
//后面就不用再手动去加delete了
}
当通过copy构造函数或者时赋值运算符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权
void f(){
auto_ptr<int> a(new int(100)); //a指向new int返回的地址
auto_ptr<int> b(a); //现在b指向对象,a被置为NULL
a = b; //现在a指向对象,b置为NULL
}
这意味着受auto_ptr管理的资源没有一个以上的auto_ptr可以同时指向它。但是在auto_ptr放弃了对象的所有权后,这样就留下了一个悬挂指针,使用者后面还有可能使用它来访问该对象(比如上面的例子中,最后再做一个“*b”的操作),但显然这会导致问题.
unique_ptr
独占式指针,即同一时刻只能有一个指针指向同一个对象。如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。
这一点看起来和auto_ptr是一样的,都是对于一个资源来说,只有一个指针指向它。但是它们之间有如下差别:
1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)
对于unique_ptr来说,编译器会认为下面#3这一步是非法操作,直接编译阶段就会报错,这样就避免了a不再指向有效数据的问题,所以从这一点来讲,unique_ptr比auto_ptr更安全。
void f_unique() {
unique_ptr<int> a(new int(100)); //#1
unique_ptr<int> b; //#2
b = a; //#3 not allowed
}
但如果真的想把unique_ptr赋给另一个的话,其实也不是不行,如下两种情况可以做到将一个unique_ptr赋给另一个:
1.1 C++提供了标准库函数move()
比如想将上面的a赋给b,则可以使用move()
void f_unique() {
unique_ptr<int> a(new int(100));
cout << "Address: " << a.get() << "; *a = " << *a << endl;
unique_ptr<int> b;
b = move(a); //not allowed
cout << "Address: " << b.get() << "; *b = " << *b << endl;
cout << "Address: " << a.get() << endl;
}
1.2.如果unique_ptr是一个临时右值
如果unique_ptr是一个临时右值,编译器允许这样做;如果unique_ptr将存在一段时间,编译器将禁止这样做。
如下面这段示例,demo返回的是一个临时的unique_ptr,然后a接管了原本返回的unique_ptr所拥有的对象,而返回的unique_ptr被销毁。这样的话,没有机会使用返回的临时unique_ptr对象来访问无效的数据,这样的赋值行为想象上也应该要是合理的
unique_ptr<int> demo(int a) {
unique_ptr<int> res(new int(a));
return res;
}
void testDemo() {
unique_ptr<int> a;
a = demo(10);
cout << "Address: " << a.get() << "; *a = " << *a << endl;
}
2. unique_ptr可用于数组
unique_ptr<int []> a(new int(100));
但是auto_ptr不可以,因为auto_ptr里使用的是delete而没有使用delete [],所以只能与new一起使用,无法与delete []一起使用,所以无法用于数组。而unique_ptr里有使用new []和delete []的版本。
shared_ptr
shared_ptr是计数型智能指针,同一时刻可以有多个指针指向同一个对象,并在无人指向它时自动删除该资源。它的大概原理是:shared_ptr内部有一个计数器,每当新增一个指向同一个对象的指针时,那么这个计数器会加1;反之,每有一个指针被释放,计数器减1,如果计数器为0时,shared_ptr就会释放指向的对象。
所以可见shared_ptr和前面所说的auto_ptr、unique_ptr最明显的不同就是:shared_ptr允许多个指针同时指向一个对象,而auto_ptr、unique_ptr都只允许同一时刻只能有一个指针指向同一个对象。
如果程序要使用多个指向同一个对象的指针,则应该选择shared_ptr。如下这段程序所示, a,b,c这三个shared_ptr都同时指向了同一块内存
void f() {
shared_ptr<int> a(new int(100));
cout << "Adress: " <<a.get() << "; *a = " << *a << endl;
shared_ptr<int> b(a);
cout << "Adress: " << b.get() << "; *b = " << *b << endl;
shared_ptr<int> c = a;
cout << "Adress: " << c.get() << "; *c = " << *c << endl;
return;
}
环状引用问题
环状引用是指两个或多个对象之间相互引用,形成一个闭环的情况。在面对环状引用时,shared_ptr无法打破这种情况,会引起内存泄漏。
假设有这样一段代码:
class A;
class B;
class A {
public:
shared_ptr<B> b_ptr;
};
class B {
public:
shared_ptr<A> a_ptr;
};
void testCycles() {
shared_ptr<A> a = make_shared<A>(); //#1
shared_ptr<B> b = make_shared<B>(); //#2
a->b_ptr = b; //#3
b->a_ptr = a; //#4
return;
}
1.经过上面的#1和#2后,指向a和b的shared_ptr引用计数各自为1,即当前各有一个shared_ptr有分别指向a和b
2. 经过#3和#4后,a->b_ptr指向了b, b->a_ptr指向a, 因为a和b有互指,则各自的引用计数变为2,如下图所示:
3. 当a和b都脱离作用域,各自调用自己的析构函数后,a和b各自的引用计数减为1,但是a和b的资源还没有release,a还有指向b,b也还有指向a.
4. a释放,就需要b的a_ptr释放,b释放,就需要a的b_ptr释放。因此a和b互相约束着对方的析构,最后都没法析构,导致内存泄漏。
这有点儿“死锁”的味道了。所以环状引用会导致对象的引用计数无法减为0,从而导致内存泄漏。为了解决环状引用的问题,可以使用weak_ptr来打破环状引用。weak_ptr是一种弱引用,它不会增加对象的引用计数,也不会阻止对象的销毁。下面介绍一下weak_ptr。
weak_ptr
weak_ptr是用来解决shared_ptr相互引用导致的内存泄漏问题,是shared_ptr的辅助类。具体来说,就是在环状引用中,将其中一个对象的指针使用weak_ptr来引用另一个对象,而不是使用shared_ptr。这样,在两个对象相互引用时,当所有的shared_ptr都释放了所指向的对象,即使还有weak_ptr指向该对象,对象也会被销毁,不会导致引用计数无法降为0,从而避免了内存泄漏的问题
因此,上面那段示例代码可以将A or B里的某一方改为weak_ptr:
class A;
class B;
class A {
public:
shared_ptr<B> b_ptr;
};
class B {
public:
weak_ptr<A> a_ptr;
};
void testCycles() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return;
}
注意:
1. weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,如下的语法是非法的
weak_ptr<int> a(new int(100));
2. weak_ptr没有operator*函数和operator->成员函数,不具有一般指针的行为。
总结
在实际中,应尽量避免直接使用裸指针,而应优先选择智能指针,它们利用对象的生命周期来管理资源,避免了资源泄漏这些问题。并且针对不同的需求选用合适类型的智能指针。