C++中用new来动态分配内存,delete手动释放内存来达到动态管理内存的目的。因为保证在正确的时间释放内存是非常困难的,忘记释放内存就会产生内存泄露。
为了更安全、便捷的使用动态内存,C++11标准库提供了新的智能指针类来管理动态内存。智能指针在行为上和普通的指针是一样的,只不过它可以保证内存在适当的时候自动释放。
新的标准库提供了两种智能指针(在头文件
中),区别在于管理底层指针的方式。
本文仅介绍shared_ptr的相关用法。
类似vector,智能指针也是一种模板,因此创建智能指针时,我们需要提供额外的信息表明指针指向的类型。
shared_ptr
shared_ptr
> p2;//创建一个空指针p2,可以指向list
这仅仅是创建了一个智能指针,并没有给它分配内存。
通常有两种方式给shared_ptr分配内存
最安全的分配和使用动态内部的方式是调用名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
shared_ptr<int> p1 = make_shared<int>(42);
//创建指向int的shard_ptr并且用42初始化
shared_ptr<string> p2 = make_shared<string>(10, '9');
//创建一个指向string的shared_ptr并且调用string的(n,char)构造函数初始化
shared_ptr p3 = make_shared();
//创建指向my_class的shared_ptr并且调用my_class的默认构造函数初始化
类似于容器操作的emplace,make_shared函数用其参数来构造给定类型的对象。所以说如果是构造一个类的shared_ptr,一定要注意初始化时必须和某个构造函数匹配。
前边提到过,shared_ptr允许多个指针指向同一个对象。所以我们可以认为每一个shared_ptr都有一个和它关联的计数器,称为引用计数器。当满足下述条件时,引用计数器递增。
初始化shared_ptr: shared_ptr
此时p1指向的对象只有p1一个引用者,引用计数为1
拷贝shared_ptr:shared_ptr
此时q和p指向同一个对象,该对象的引用计数为2
将shared_ptr当做一个函数的参数传递:
shared_ptr<int> p1 = make_shared<int>(42);
printCount(p1);
void printCount(shared_ptr<int> p)
{
cout << p.use_count() << endl;//返回p指向对象的引用计数
}
输出结果是2,因为p1被当做了参数传递
shared_ptr<vector<int>> getVector()
{
return make_shared<vector<int>>();
}
shared_ptr<vector<int>> p = getVector();
cout << p.use_count() << endl;
输出结果是1,虽然初始化的时候没有显式的用make_shared函数给p分配内存,但是调用了getVector()函数,返回了一个shard_ptr,这样p指向的对象引用计数器增加。
当一个对象的引用计数减为0后,shared_ptr会自动销毁对象,释放内存。
当以下两种情况发生时,shared_ptr的引用计数会递减
auto p1 = make_shared<int>(42);
p1 = p2;
给p1赋值使它指向p2指向的对象,会递增p2指向的对象的引用计数,递减p1指向对象的引用计数,此时p1指向对象的引用计数为0,被自动销毁。
void printCount(shared_ptr<int> p)
{
shared_ptr<int> x = make_shared<int>();
cout << x.use_count() << endl;
}
x是局部shared_ptr,当函数结束时,x指向的对象引用计数减为0,x指向的对象被销毁,释放占用的内存。
如前所说,如果不初始化一个智能指针,那么它就会被初始化为一个空指针。除了用make_shared函数我们还可以用new返回指针初始化智能指针。
例如shared_ptr
该语句实际上调用了智能指针的构造函数,该构造函数的参数是普通指针。这种构造函数是显式的,因此shared_ptr
这种方式是错误的。
同样的理由,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。
shared_ptr<int> clone(int p)
{
return new int(p);
//错误,内置指针不能被隐式的转换为shared_ptr
}
正确写法应当是:
shared_ptr<int> clone(int p)
{
return shared_ptr<int> (new int(p));
}
还有,必须要强调的一点是:
默认情况下,一个用来初始化shared_ptr的普通指针必须指向动态分配的内存,这是因为智能指针默认用delete释放它关联的内存。
如果出于一些目的,必须用不指向动态分配内存的指针来初始化智能指针,就必须自定义一种释放内存的方式(可以理解为重载delete)。
详细例子在3.shared_ptr支持的操作中介绍。
操作 | 解释 |
---|---|
shared_ptr< T > sp | 创建空智能指针,可以指向T类型的对象 |
sp.get() | 返回sp中保存的指针。PS:一定要小心使用,如果智能指针释放了对象,返回的指针指向的对象也将消失 |
sp.reset() | 若sp是唯一指向该对象的shared_ptr,reset释放该对象 |
sp.reset(p) | sp不在指向原来它指向的对象,指向内置指针p指向的对象,这里p是被new动态分配内存的 |
sp.reset(p,d) | sp不在指向原来它指向的对象,指向内置指针p指向的对象,这里p将会被可调用对象d释放 |
sp.use_count() | 返回sp指向对象的引用计数 |
sp.unique() | 如果sp.use_count() == 1,返回true否则返回false |
swap(sp1,sp2) | 交换两个智能指针 |
shared_ptr< T > sp(p) | sp管理内置指针p,p必须是被new动态分配内存的,而且能转换为T*类型 |
shared_ptr< T > sp(p,d) | sp接管内置指针p指向对象的所有权,p必须能准换为T*类型,sp将使用可调用对象d代替delete |
shared_ptr< T > sp1(sp2,d) | sp1是shared_ptr sp2的拷贝,使用可调用对象d代替delete |
上述操作表中,引入了一种新的做法,使用可调用对象d来释放内置指针。
这基于这样的假设:
内置指针p指向的对象不是由new动态分配的,然而我们还是用了p初始化了sp(shared_ptr),此时就必须用“重载”delete函数,顺利释放p的内存。
这样是很麻烦的,具体的例子可以看这里:
C++ Primer 第五版 练习12.4
C++ Primer建议不要混合使用普通指针和智能指针,更加推荐使用智能指针。
为此,介绍几个例子:
假如说存在函数:
void process(shared_ptr<int> ptr)
{
//do something
}//ptr离开作用域,被ptr被销毁,但是它指向的内存不会被释放
此函数的正确使用方法传递shared_ptr:
shared_ptr<int> ptr(new int(42));
//ptr指向对象引用计数为1
process(ptr);
//process中ptr指向的对象引用计数为2
int i = *ptr;
//正确,引用计数为2
典型的错误用法如下:
int * x(new int(1024));
process(x);
//错误,内置指针不能隐式的转换为shared_ptr
process(shared_ptr<int> (x));
//正确,process中ptr.use_count() == 1,process结束时ptr指向的内存会被释放!
int i = *x;
//未定义行为,x是空悬指针!
首先,语句shared_ptr
相当于一个强制转换,得到一个临时变量shared_ptr,调用process时,相当于void process(shared_ptr
,那么当process结束的时候,ptr指向的对象的引用计数变为0,内存被释放,也就是x指针指向的内存被释放,x变为了一个空悬指针。
记住:当shared_ptr绑定到内置指针时。我们就把内存的管理责任教给了shared_ptr,不要再用内置指针访问该内存。
出于近乎相同的理由,不要使用get返回的内置指针初始化其他的智能指针!