智能指针是一个模板函数,为我们封装好了一系列的方法来进行内存的自动管理。本质上我们也可以通过编写自己的模板类来实现和智能指针类似的功能,后续会提到相关的内容。
在使用智能指针之前,我们可以回顾一下以前我们在使用指针对内存进行操作。
比如现在我们编写了一个Test类,定义如下:
class Test
{
public:
int TestGet() {
return Num;
}
void showNum() {
cout << Num << endl;
}
void TestSet(int num) {
this->Num = num;
}
Test() {
Num = 0;
cout << "调用默认构造函数Test()" << endl;
}
Test(int Num) {
this->Num = Num;
cout << "调用构造函数Test(num)" << endl;
}
virtual ~Test() {
cout << "调用析构函数~Test()" << endl;
}
private:
int Num;
};
之后我们new一块内存,并用一个指针指向它
Test* ptr1 = new Test;
下面我们来分析可能造成的三种情况:
1.没有使用delete语句
2.内存泄漏
ptr1 = nullptr;
就会出现内存泄漏的情况,此时没有任何指针指向之前申请出来的内存,那么这块内存就无法被我们正常delete释放了。3.多次析构
浅复制
的方法,即两个指针指针指向了同一块内存空间:析构了两次
,即:这里可能会有疑问:啊,为啥我要用两个指针指向同一个内存呢,我直接重新深复制new一块出来不就行了?雀氏帅!
但假设现在有两个场景:
进程
(或者简单点说:不同的程序),每个进程只是想访问这块公共资源而已,如果每个进程都要重新复制一份,那开销就非常大了。所以有的时候不可避免的要这么做,最后经过一堆大佬的商讨和出手之后,在C++11之后我们就迎来了智能指针了(我们也有垃圾回收了,以后JAVA这方面喷不了把,哈哈哈哈(开个玩笑,都是好语言))
OK,那现在分析智能指针的优点就非常容易了:
OK,那智能指针这么牛皮,那它是怎么实现的呢?——下面分析一波shared_ptr
雀氏这玩意源码优点复杂,我们就简单看看它的实现好了。
前面都在说智能指针,现在看看shared_ptr
作为智能指针中的一种,有什么神奇的地方。和unique_ptr
不同,shared_ptr
既然名字中都有个share
,所以它可以被多个指针指向,那么它是如何实现当最后一个指向这块内存的指针不再指向这块内存后,这块内存才被释放的呢?————通过引用计数
。
这个和操作系统里面的信号量机制其实挺像的:
template<class T>
class shared_ptr
{
public:
T* get() const; //获取原始指针
T use_count() const; //获取引用计数
返回类型 operator->(参数); //重载运算符->
...
private:
T* px;
shared_count pn;
};
假如这块内存有三个指针指着,那么pn就等于3,这个T* px就是T类型的原始指针,可以通过get方法获取,同时这个模板重载了->
运算符,这样可以使得我们使用智能指针可以像使用普通指针那样使用。
还是重点说说引用计数,shared_ptr
提供的引用计数非常厉害,好像是线程安全的,当然我们现在理解的话可以先简单点理解它就是记录有多少个指针指向这块内存的。当有新的指针指向这块内存,pn就加1;当旧指针不再指向这块内存的时候,pn就减1。
(猛的童鞋可以看看源码解析)
为了加深理解,下面可以对shared_ptr
进行简单的测试使用。
shared_ptr
主要有以下种方法:(不限于此)
1.使用share_ptr
的构造函数进行初始化
shared_ptr<Test> ptr1(new Test);
2.使用share_ptr
的复制构造函数进行初始化(右值版本只是改变使用权,详解请见C++11移动语义
的相关内容)
shared_ptr<Test> ptr2(ptr1);
// 右值版本
//使用移动语义调用右值版本的复制构造函数
shared_ptr<Test> ptr3 = std::move(ptr1);
3.使用std::make_shared
模板进行初始化
//括号里的内容是初始化的值
shared_ptr<Test> ptr4 = make_shared<Test>(8);
4.使用reset方法
//将指针的原先控制权限释放,即ptr3不再拥有访问原来资源的权限
//同时ptr3指向一块新的内存(也可以不new)ptr3.reset()单纯释放权限
ptr3.reset(new Test(30));
shared_ptr
调用对应类的函数直接通过->调用即可,如:
ptr1->TestSet(20);
cout << ptr1->TestGet() << endl;
shared_ptr
生成数组的问题我们知道,在自己申请一个数据的时候,释放需要用到delete[]才能将对应内存释放干净,即:
Test* pp = new Test[4];
delete[] pp;
但是如果只使用shared_ptr
构造函数种默认的删除器
,则做不到这一点。什么叫删除器呢?
删除器就是释放内存的时候采取什么方式进行释放,默认情况是直接delete。
//如果定义一个数组,不重新定义删除器就会析构错误
//shared_ptr ptr4(new Test[4]);
//使用lambda表达式构造一个匿名函数更加方便
//其中[]是要在内部定义的变量,()里的为参数,{}里的为函数体内容
shared_ptr<Test> ptr5(new Test[4], [](Test* tmp) {
cout << "这是ptr5的删除器" << endl;
delete[] tmp;
}); //通过自己定义的lambda表达式即可重新定义删除器
//当然,也可以使用C++提供的std::default_delete()
shared_ptr<Test> ptr6(new Test[3], std::default_delete<Test[]>());
shared_ptr
的注意事项这里涉及两种情况:
shared_ptr
,之后通过get函数取出原始指针后进行strcpy_s
函数调用://直接用智能指针作为函数参数是不行的
shared_ptr<char> ptr1(new char[20],std::default_delete<Test[]>());
char a[10] = "abcdefghi";
strcpy_s(ptr1, a, 10);
//但是用这个之后,智能指针就会指向一块新的内存了
//再用ptr1.use_count()就会无效
strcpy_s(ptr1.get(),10, a); //会改变内部指针的指向
cout << ptr1 << endl; //error
//test
shared_ptr<Test> ptr1(new Test);
shared_ptr<Test> ptr2(ptr1);
delete(ptr2.get()); //在出作用域后报错,析构两次同一块内存
为什么会出现这样的情况?
还是一样,先定义一个ptr1的智能指针,之后通过移动语义
将ptr1对这块内存的使用权限转移给ptr3,即:
shared_ptr<Test> ptr1(new Test);
//使用移动语义调用右值版本的复制构造函数
shared_ptr<Test> ptr3(std::move(ptr1));
之后再通过->运算符调用类方法就会报错:
cout << ptr1->TestGet() << endl; //error
ptr1->TestSet(20); //error
但是呢会发现一个很有趣的现象,你依然可以用ptr1模板的成员函数use_count()去查看这块内存的引用计数情况:
cout << ptr1.use_count() << endl; //值为0
其实很好理解,因为智能指针本身是模板,只不过通过类的封装和运算符的重载让他看起来像个指针而已,但本质上它依然是个模板。
当使用 -> 运算符的时候,它会调用模板成员函数获得指针指向的内存对象的this指针,但前面使用移动语义的时候,指针会向编译器声明不再使用这块内存,因此获取到的this指针为nullptr,因此无法通过this指针访问类的方法。
而通过 . 运算符访问的是智能指针的模板函数,因此是可以的。