实用指南:C++智能指针 -- 通俗易懂&全面的快速讲解
目录
八、weak_ptr 的特殊函数 -- expired( )

一、为什么学习智能指针
有的智能指针可以在生命周期结束时可以自动释放申请的内存,这就避免了内存泄漏的问题。
有的智能指针通过计数来自动释放申请的内存,避免内存泄漏。(不懂计数下面会讲)
二、引入智能指针
智能指针分为四个,它们分别是auto_ptr、unique_ptr、shared_ptr、weak_ptr。
看似很难很多,但是十分简单。
它们共用一个头文件,如下->:
#include
三、智能指针的基本用法 & auto_ptr
在掌握一种智能指针后,其他的智能指针基本都是照搬
1、智能指针的第一个用处,自动释放内存空间
智能指针的用法如下->:
T为类型
auto_ptr p1;
auto_ptr p1(new T);
使用方法如下->:
class A
{
};
int main()
{
auto_ptr p1;
auto_ptr p2(new A());
}
我们上面提到过,智能指针会自动释放内存,我们验证一下,看看是否会自动调用析构函数

可以看到,智能指针确实会自动释放内存
智能指针除了自动释放内存还有其他用处吗?
2、智能指针的第二个用处,可以像普通指针一样使用
智能指针可以调用类中的公开的函数或者对象,如下->:

智能指针之所以可以像普通指针一样使用,是因为它重载了 -> 和 *
3、智能指针的三个常用函数
1、get( ) -- 获取指针管理的内存空间
class A { public: A() { cout << " 构造 " << endl; } ~A() { cout << " 析构 " << endl; } }; int main() { auto_ptrp1;//没有管理的内存,不调用析构 auto_ptr p2(new A()); A* tep = p2.get(); }
当然也可以用p1.get( ),来取出这块内存空间的地址并打印出来
2、release( ) -- 取消智能指针对动态内存的托管,但不会主动释放内存

这里我们可以看到并没有调用析构函数,说明智能指针失去了对这块空间的管理,就不会自动调用析构函数了
3、reset( ) -- 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

可以看到,reset确实是这样的
auto_ptr 的注意事项
auto_ptr在使用时,最好不要使用复制 或者 赋值运算符(=),因为复制或者赋值都会改变资源的所有权
我们代码举例如下->:
#define _CRT_SECURE_NO_WARNINGS #include#include using namespace std; class A { public: A() { cout << " 构造 " << endl; } ~A() { cout << " 析构 " << endl; } void get(){ cout << " get() " << endl; } int a; }; int main() { auto_ptr p1;//没有管理的内存,不调用析构 auto_ptr p2(new A()); cout << " 原地址 " << endl; cout << p1.get() << endl; cout << p2.get() << endl; cout << endl; cout << " 赋值后的地址 " << endl; p1 = p2; cout << p1.get() << endl; cout << p2.get() << endl; }

本来我们的p2是有内存的,但是赋值给p1后,p1接管了p2管理的内存,p2本身放弃了这个内存块的所有权
复制也是同理

在 auto_ptr 中,不支持对象数组的内存管理
但在 unique_ptr,shared_ptr 支持
auto_ptr p1(new int[4]);//不支持,编译错误
auto_ptr p1(new int[4]);//支持,但调用的是delete,会内存泄漏
//以下两个智能指针下面会讲到
unique_ptr p2(new int[4]);
shared_ptr p3(new int[4]);
auto_ptr被淘汰的原因
auto_ptr 已经被淘汰了现在,原因有两个
1、 首先就是我们刚才所提到的,复制 或者 赋值 会失去对内存空间的管理权。这是一种极其危险的事情。
2、STL容器内的数据必须支持互相之间可以 复制 和 赋值 ,但是 auto_ptr 在STL容器中却不支持
vector> vec;
auto_ptr p3(new string("I'm P3"));
auto_ptr p4(new string("I'm P4"));
// 必须使用std::move修饰成右值,才可以进行插入容器中
vec.push_back(std::move(p3));
vec.push_back(std::move(p4));
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
// 风险来了:
vec[0] = vec[1]; // 如果进行赋值,问题又回到了上面一个问题中。
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
3、 不支持对象数组的内存管理
auto_ptr p1(new int[4]);//不支持,编译错误
auto_ptr p1(new int[4]);//支持,但调用的是delete,会内存泄漏
//以下两个智能指针下面会讲到
unique_ptr p2(new int[4]);
shared_ptr p3(new int[4]);
四、unique_ptr
unique_ptr 是 auto_ptr 的进化版本,它包含我们在 auto_ptr 中讲解的所有功能如有,如 : 自动释放内存,可以像普通指针使用,可以使用常用的三个函数,不能直接 复制 或者 赋值。使用这些的语法都是一样的
从功能上来讲,唯一不同的就是支持对象数组的内存管理
我们直接拿出不同点来讲解,很少,很简单。
本质上来讲,unique_ptr 说是 auto_ptr 的进化版本,其实就是语法更加严格了。
使用 auto_ptr 时,程序员可能不小心赋值出错了,这很严重。
那么 unique_ptr 就为了限制这种情况,直接不允许编译。
auto_ptrp1; auto_ptr p2(new A()); p1 = p2;//编译正常,运行正常 unique_ptr p3; unique_ptr p4(new A()); p3 = p4; //编译不通过,不允许这样写 //------------------------------------------------------------ auto_ptr tep1(p1);//编译允许,运行允许 unique_ptr tep2(p3); //编译不通过,不允许这样写
但是严格归严格,并不是完全禁止。
使用右值转换可以实现 复制 或者 赋值 。
auto_ptrp1; auto_ptr p2(new A()); p1 = p2;//编译正常,运行正常 unique_ptr p3; unique_ptr p4(new A()); p3 = move(p4); //------------------------------------------------------------ auto_ptr tep1(p1);//编译允许,运行允许 unique_ptr tep2(move(p3));

unique_ptr 进行右值转换后的效果跟 使用auto_ptr执行p1 = p2是一样的,move后的值还是会失去内存空间的管理权,并让新的指针进行管理。所以unique_ptr 只是让程序员明确这样使用的代价,不要误操作。
我们来验证一下

其次,就是对象数组内存管理的区别
auto_ptr p1(new int[4]);//auto_ptr 不支持对象数组的内存管理
unique_ptr p2(new int[4]);//unique_ptr 支持对象数组的内存管理 自动调用 delete[]
对象数组的内存管理可以让我们直接访问下标,如下->:

五、计数智能指针 shared_ptr
这个指针不太一样,它是通过计数来决定要不要释放内存空间的。
什么意思?
use_count( )函数
我们先认识一个 shared_ptr 的独享函数 use_count( ) ,这个函数可以知道 shared_ptr 这个智能指针管理的计数是多少,代码例子如下->:

可以看到打印了1,这个1代表的是p1所管理的这块内存空间现在有1个shared_ptr指针管理着
那么如果再多一个 shared_ptr 指针管理这块内存空间,计数器就会变成2
为了让其他的 shared_ptr 管理这个内存块,shared_ptr 就会支持赋值运算符(=)
shared_ptr 的赋值运算符的意思是共享所有权,因此不会失去对这块内存空间的控制
我们代码验证如下->:

可以看到,计数确实变成了2
这代表有2个 shared_ptr 的智能指针在管理这块内存空间
注意,我说的是 shared_ptr 在管理这块内存空间,不是普通指针,普通指针是不会改变计数的,原因我们下面讲

可以看到,普通指针确实不会改变计数,这是因为->:
shared_ptr 是根据计数来决定要不要释放内存的,当 shared_ptr 的生命周期到了的时候,这个计数就会减 1 。拿我们的代码说明,有一块内存空间被 p1 和 p2 两个智能指针管理,计数为 2,当 p1 的生命周期结束,计数减 1 ,从 2 变为 1 。然后 p2 的生命周期结束,计数减 1 ,从 1 变为 0 。计数一旦变为 0 ,这个内存就自动释放掉了。
那么为什么普通指针不加计数呢,因为普通指针生命周期结束并不会影响计数器,我们之前学习或者使用普通指针的时候,从不考虑计数的问题。因为普通指针根本就没有计数器。
需要注意的是,shared_ptr 是一个极其负责的智能指针,因此 shared_ptr 在智能指针的三个常用函数中并不支持 release 函数的使用

shared_ptr 支持主动释放内存,主需要把 shared_ptr 指针置为空,就可以主动释放内存
shared_ptrr up1(new int(10));
up1 = nullptr ; // int(10) 的引用计数减1,计数归零内存释放
// 或
up1 = NULL; // 作用同上
一些常见用法,比如支持对象数组内存管理,以及swap交换管理的内存空间
//注意要加上[],否则调用的是 delete 会内存泄漏 unique_ptr p1(new A[5]{1,2,3,4,5}); shared_ptr p2(new A[5]{6,7,8,9,0}); unique_ptrp3(new A); unique_ptr p4(new A); swap(p3, p4);//可以交互内部管理的内存空间,shared_ptr 同理,引用计数也会随之改变
六、shared_ptr的使用陷阱
看下面这段代码,这是一个死循环代码
#include
#include
#include
using namespace std;
class Girl;
class Boy {
public:
Boy() {cout << "Boy 构造函数" << endl;}
~Boy() {cout << "~Boy 析构函数" << endl;}
void setGirlFriend(shared_ptr _girlFriend) {
this->girlFriend = _girlFriend;
}
private:
shared_ptr girlFriend;
};
class Girl {
public:
Girl() {cout << "Girl 构造函数" << endl;}
~Girl() {cout << "~Girl 析构函数" << endl;}
void setBoyFriend(shared_ptr _boyFriend) {
this->boyFriend = _boyFriend;
}
private:
shared_ptr boyFriend;
};
void useTrap() {
shared_ptr spBoy(new Boy());
shared_ptr spGirl(new Girl());
// 陷阱用法
spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}
int main(void) {
useTrap();
system("pause");
return 0;
}


spboy 和 grilfriend 指向同一块空间,spgirl 和 boyfriend 指向同一块空间
spboy 释放了,所以 spboy 和 girlfriend 的计数都变为 1
spgirl 再释放,spgirl和boyfriend计数也都为 1
最后都为 1,谁都没释放掉这块空间
所以在使用 shared_ptr 智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
只要注释掉其中一个,就可以正常释放了
void useTrap() {
shared_ptr spBoy(new Boy());
shared_ptr spGirl(new Girl());
// 单方获得管理
//spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
}
1、首先释放 spBoy ,但是因为girl对象里面的智能指针还托管着 boy ,boy 的引用计数为2,所以释放spBoy时,引用计数减1,boy 的引用计数为 1 ;
2、在释放 spGirl ,girl 的引用计数减 1,为零,开始释放 girl 的内存,因为 girl 里面还包含有托管 boy 的智能指针对象,所以也会进行 boyFriend 的内存释放,boy 的引用计数减 1,为零,接着开始释放 boy 的内存。最终所有的内存都释放了。
七、weak_ptr
1、weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造
2、它的构造和析构不会引起引用记数的增加或减少
3、同时 weak_ptr 没有重载 * 和 -> 但可以使用 lock( ) 获得一个可用的 shared_ptr 对象
weak_ptrp1(new int);//编译错误,weak_ptr 只可以从 weak_ptr 或者 shared_ptr 构造 shared_ptr p2(new A()); weak_ptr p3(p2); weak_ptr p4(p3);

可以看到计数还是为1,weak_ptr 的构造或者析构是不影响计数的
weak_ptr 没有重载 * 和 ->,但 weak_ptr 支持lock函数,这个函数可以返回一个 shared_ptr 对象


实战使用 ->: 刚才 shared_ptr 的使用陷阱,使用 weak_ptr 解决
#include
#include
#include
using namespace std;
class Girl;
class Boy {
public:
Boy() {cout << "Boy 构造函数" << endl;}
~Boy() {cout << "~Boy 析构函数" << endl;}
void setGirlFriend(shared_ptr _girlFriend) {
this->girlFriend = _girlFriend;
// 在必要的使用可以转换成共享指针
shared_ptr sp_girl;
sp_girl = this->girlFriend.lock();
cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;
}
private:
weak_ptr girlFriend;
};
class Girl {
public:
Girl() {cout << "Girl 构造函数" << endl;}
~Girl() {cout << "~Girl 析构函数" << endl;}
void setBoyFriend(shared_ptr _boyFriend) {
this->boyFriend = _boyFriend;
}
private:
shared_ptr boyFriend;
};
void useTrap() {
shared_ptr spBoy(new Boy());
shared_ptr spGirl(new Girl());
spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
}
int main(void) {
useTrap();
system("pause");
return 0;
}
八、weak_ptr 的特殊函数 -- expired( )
expired( ):判断当前 weak_ptr 智能指针是否还有托管的对象,有则返回 false,无则返回 true


浙公网安备 33010602011771号