动态内存与智能指针
在C/C++中,动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时忘记释放内存就会出现内存泄露的问题;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存问题。
新标准库提供了两种智能指针类型来管理动态对象,智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr
允许多个指针指向同一个对象;unique_str
则“独占”所指向的对象。标准库还定义了一个名为weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr
所管理的对象。
1、shared_ptr类
智能指针是模板,因此我们创建一个智能指针的时候,必须提供额外的信息——指针可以指向的类型:
shared_ptr<string> p1; //指向string
shared_ptr<list<int>> p2; //指向int的list
默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用中智能指针,效果就是检测它是否为空:
//如果p1不为空,检查它是否指向一个空string
if (p1 && p1->empty())
*p1 = "hi"; //如果p1指向一个空string,则解引用p1,将一个新值赋予string
shared_ptr
与unique_str
都支持的操作:
shared_ptr<T> sp //空智能指针,可以指向类型为T的对象
unique_ptr<T> up
p //将p作为一个条件判断,若p指向一个对象,则为true
*p //解引用p,获得它指向的对象
p->mem //等价于(*p).mem
p.get() //返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q) //交换p、q中的指针
p.swap(q)
share_ptr
独有的操作:
make_shared<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p (q) //p是shared_ptr的拷贝,q中的指针必须能转化为T*
p = q //p和q都是shared_ptr,所保存的指针必须能互换。此操作会递减p的引用计数器,递增q的引用计数器;若p的引用计数器变为0,则会将其管理的原内存释放
p.unique() //若p.use_count()为1,返回true,否则返回false
p.use_count() //返回与p共享对象的智能指针数;可能很慢,主要用于调试
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared
的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。定义在头文件memory
中。
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//指向一个值为"999"的string
shared_ptr<string> p4 = make_shared<string>(3, '9');
//指向一个值初始化的int,即值为0
shared_ptr<int> p5 = make_shared<int>();
类似顺序容器的emplace
成员,make_shared
用其参数来构造给定类型的对象。例如,调用make_shared<string>
时传递的参数必须与string
的某个构造函数相匹配。如果不传递任何参数,对象就会进行值初始化。
用auto
定义一个对象来保存make_shared
的结果,这种方式比较简单:
//p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr的拷贝与赋值
当进行拷贝或赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象:
auto p = make_shared<int>(42); //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
我们可以认为每个shared_ptr
都有一个关联的计数器,通常称其为引用计数。拷贝一个shared_ptr
时,计数器就会递增。例如,当我们用一个shared_ptr
初始化另一个shared_ptr
,或将他作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr
赋予一个新值或是shared_ptr
被销毁(例如一个局部的shared_ptr
离开其作用域)时,计数器就会递减。一旦一个shared_ptr
的计数器变为0,它就会自动释放字节所管理的对象:
auto r = make_shared<int>(42); //r指向的int只有一个引用者
r = q; //给r赋值,令它指向另一个地址;递增q指向的对象的引用计数;递减r原来指向的对象的引用计数;r原来指向的对象已没有引用者,会自动释放。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr
被销毁时,shared_ptr
类会自动销毁此对象。它是通过析构函数来完成销毁工作的。shared_ptr
的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr
的析构函数就会自动销毁对象,并释放它占用的内存。
shared_ptr自动释放相关联的内存
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
......
} //p离开了作用域,它指向的内存会被自动释放掉
此例中,p是唯一引用factory
返回的内存的对象。由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存将会释放。但如果有其他的shared_ptr
也指向这块内存,他就不会被释放掉:
shared_ptr<Foo> use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
......
return p; //当我们返回p时,引用计数器进行了递增操作
} //p离开了作用域,但它指向的内存不会被释放掉
由于在最后一个shared_ptr
销毁钱内存都不会释放,保证shared_ptr
在无用之后就不在保留就非常重要了。
程序使用动态内存一般处于一下三种原因之一:
- 1、程序不知道自己需要使用多少对象
- 2、程序不知道所需对象的准确类型
- 3、程序需要在多个对象间共享数据
2、shared_ptr和new结合使用
接受指针参数的智能指针构造函数是explicit
的,因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化的形式来初始化一个指针。
shared_ptr<int> p1 = new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int> p1(new int(1024)); //正确:使用了直接初始化形式
p1的初始化隐式的要求编译器用一个new
返回的int*
来创建一个shared_ptr
。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条语句是错误的。出于相同的原因,一个返回shared_ptr
的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> clone(int p) {
return new int(p); //错误:隐式转换为shared_ptr<int>
return shared_ptr<int>(new int(p)); //正确:显式的用int*创建shared_ptr<int>
}
不要混合使用普通指针和智能指针,也不要用get初始另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为get
的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要想不能使用智能指针的代码传递一个内置指针。使用get
返回的指针的代码不能delete
此指针。
为了正确使用智能指针,我们必须坚持一些基本规范:
- 不使用相同的内置指针初始化多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
3、unique_ptr
与shared_ptr
不同,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。当我们定一个unique_ptr
时,需要将其绑定到一个new
返回的指针上。类似shared_ptr
,初始化unique_ptr
必须采用直接初始化形式:
unique_ptr<double> p1; //可以指向一个double的unique_str
unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int
由于一个unique_ptr
拥有它指向的对象,因此unique_ptr
不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("hello world"));
unique_ptr<string> p2(p1); //错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; //错误:unique_ptr不支持赋值
unique_ptr
独有的操作:
//空unique_ptr,可以指向类型为T的对象。u1使用delete来释放他的指针;u2会使用一个类型为D的可调用对象来释放他的指针。
unique_ptr<T> u1
unique_ptr<T, D> u2
unique_ptr<T, D> u(d) //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr //释放u指向的对象,将u置为空
u.release() //u放弃对指针的控制权,将u置为空
u.reset() //释放u指向的对象
u.reset(q) //如果提供内置指针q,令u指向这个对象;否则将u置为空
u.reset(nullptr)
虽然我们不能拷贝或赋值unique_ptr
,但是可以通过release
或reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个unique_ptr
:
//将所有权从p1(指向string "hello world")转移给p2
unique_ptr<string> p2(p1.reslease()); //release将p1置空
unique_ptr<string> p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); //reset释放了p2原来指向的内存
release
成员返回unique_ptr
当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置空。
reset
成员接收一个可选的指针参数,令unique_str
重新指向给定的指针。如果unique_ptr
不为空,它原来指向的对象被释放。因此,对p2
调用reset
释放了用hello world
初始化的string
所使用的内存,将p3
对指针的所有权转移给p2
,并将p3
置空。
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr
的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr
。最常见的例子是从函数返回一个unique_ptr
:
unique_ptr<int> clone(int p) {
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
......
return ret;
}
对于两段代码,编译器都知道要范湖IDE对象将要被销毁,在此情况下,编译器执行一种特殊的“拷贝”。
向unique_ptr传递删除器
unique_ptr
默认情况下用delete
释放它指向的对象,但是我们可以重载unique_ptr
中默认的删除器。与重载关联容器的比较操作类似,我们必须尖括号中unique_ptr
指向类型之后提供删除器类型。创建或reset
一个这种unique_ptr
类型的对象时,必须提供一个指定类型的可调用对象(删除器):
//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p(new objT, fcn);
//下面是个更具体的例子
void f(destination &d /*需要其他的参数*/) {
connection c = connect(&d); //打开连接
//当p被销毁时,连接将会关闭
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭。
}
4、weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。将一个weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也会被释放,因此,weak_ptr
的名字抓住了这种智能指针“弱”共享对象的特点。当我们创建一个weak_ptr
时,要用一个shared_ptr
来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //弱共享p,p的引用计数未改变
由于对象可能不存在,我们不能使用weak_ptr
直接访问对象,而必须调用lock
。此函数检查weak_ptr
指向的对象是否仍存在。如果存在,lock
返回一个指向共享对象的shared——ptr
。与任何其他shared_ptr
类似,只要此shared_ptr
存在,它所指向的底层对象也就会一直存在:
if (shared_ptr<int> np = wp.lock()) {
//在if中,np与p共享对象
......
}