本小节回顾学习的知识点分别是shared_ptr常用的操作、计数、自定义删除器。
今天总结的知识分为以下2个大点:
(1)shared_ptr引用计数的增加和减少
(1.1)引用计数的增加
(1.2)引用计数的减少
(2)shared_ptr指针的常用操作
(2.1).use_count();
(2.2).unique();
(2.3) .reset();
(2.4)*解引用;
(2.5).get();
(2.6).swap();
(2.7)= nullptr
(2.8)智能指针名字作为判断条件
(2.9)指定删除器以及数组问题
(1)shared_ptr引用计数的增加和减少:
每个shared_ptr都会记录有多少个其他的shared_ptr指向这个相同的内存。因此我们才可以观察到引用计数的增加和减少!
注意:所有的shared_ptr都是强引用(strong references),记住这一点即可了!因为引用类型本来就是指针常量~,这个智能指针这么强,你不妨就这样记住它是强大引用即可啦哈~
(1.1)引用计数的增加:
在如下case下,all指向这个对象的shared_ptr的引用计数都会增加:
a)用已经定义了的shared_ptr来初始化别的shared_ptr时,引用计数会增加!
请看以下代码:
//每个shared_ptr都会记录有多少个其他的shared_ptr指向这个相同的内存
auto p1 = make_shared<int>(100);
运行结果:
把代码修改为如下:
//每个shared_ptr都会记录有多少个其他的shared_ptr指向这个相同的内存
auto p1 = make_shared<int>(100);
auto p2(p1);//引用计数+1
//p1和p2都指向heap区的值为100的int型变量的内存地址
从上述的运行调试结果图中,我们可以发现,当有2个shared_ptr都指向同一个内存地址时,引用计数的确是增加了的!
b)把智能指针当做实参往函数中传递时,函数的非引用类型的shared_ptr形参会让引用计数+1
(当调用完该函数时,引用计数会自动减1,因为此时该shared_ptr形参所指向的内存会给自动释放掉)
(当然,你用const & 类型do函数都形参时,传入shared_ptr实参也不会导致引用计数增加/减少,因为引用类型的形参不会导致编译器给我们创建新的临时shared_ptr对象导致引用计数的增加!若不信你可以自己敲一敲代码实验一下哈~)
void testfunc(shared_ptr<int> sp) {
cout << *sp << endl;
}
int main(void) {
auto p1 = make_shared<int>(100);
auto p2(p1);
testfunc(p1);//引用计数先+1 后-1
return 0;
}
运行结果:
shared_ptr实参传入到函数的shared_ptr形参中时,引用计数+1:
函数调用结束后,引用计数-1;
c)当shared_ptr作为函数的返回值并用一个对应的shared_ptr接住该函数返回值时,引用计数会增加!
(当然,如果你没有用一个shared_ptr来接住这个函数返回值的话,该返回的临时shared_ptr指针对象就会给编译器释放掉!这样一增一减引用计数就相当于不变了)
请看以下代码:
shared_ptr<int> createShared_ptr(shared_ptr<int>& sp) {
return sp;
}
int main(void) {
auto p1 = make_shared<int>(100);
auto p2(p1);
auto p3 = createShared_ptr(p1);//引用计数+1
createShared_ptr(p1);//引用计数先+1 后-1 ==> 引用计数不变了!
//因为你没有用一个shared_ptr指针来接住这个函数的返回值!
return 0;
}
运行结果:
小总结-引用计数增加的case为:
a)用已经定义了的shared_ptr来初始化别的shared_ptr时,引用计数会增加!
b)把智能指针当做实参往函数中传递时,函数的非引用类型的shared_ptr形参会让引用计数会增加!
(当调用完该函数时,引用计数会自动减1,因为此时该shared_ptr形参所指向的内存会给自动释放掉)
(当然,你用const & 类型do函数都形参时,传入shared_ptr实参也不会导致引用计数增加/减少,因为引用类型的形参不会导致编译器给我们创建新的临时shared_ptr对象导致引用计数的增加!)
c)当shared_ptr作为函数的返回值并用一个对应的shared_ptr接住该函数返回值时,引用计数会增加!
(当然,如果你没有用一个shared_ptr来接住这个函数返回值的话,该返回的临时shared_ptr指针对象就会给编译器释放掉!这样一增一减引用计数就相当于不变了)
(1.2)引用计数的减少:
注意:当shared_ptr指针对象的引用计数从1-》0时,该指针所指向的原对象的内存空间就会给释放掉~
a)让已经定义了的shared_ptr指向一个新的对象时,引用计数会减少!
请看以下代码:
auto p1 = make_shared<int>(100);
auto p2(p1);
auto p3 = createShared_ptr(p1);
p3 = make_shared<int>(888);
//让p3重新指向新的对象,此时p3这个shared_ptr的引用计数就为1
//而p1和p2这2个shared_ptr的引用计数就减为2(原本为3)
p2 = make_shared<int>(88);
//让p2重新指向新的对象,此时p2这个shared_ptr的引用计数就为1
//而p1这个shared_ptr的引用计数就减为1(原本为3)
p1 = make_shared<int>(8);
//让p1重新指向新的对象,此时p1这个shared_ptr的引用计数就为1
//而p1这个shared_ptr的指向的原对象的内存就会给释放掉
//为什么会释放呢?
//答:因为当shared_ptr指针对象的引用计数从1-》0时,该指针所指向的原对象的内存空间就会给释放掉~
运行结果:
执行完auto p3 = createShared_ptr(p1);这一行代码的调试结果:
执行完p3 = make_shared<int>(888);这一行代码的调试结果:
p1和p2这2个shared_ptr的引用计数减为2(3-1=2):
执行p1 = make_shared<int>(8);之前这一行代码的调试结果:
执行p1 = make_shared<int>(8);之后这一行代码的调试结果:
标红表明这块内存空间已经被释放掉了!
可见,以上运行调试结果都符合我在代码块中注释的内容,若有不懂的话请自己敲一敲代码试一试哈~
b)局部的shared_ptr离开其作用域时,引用计数会减少!
(就比如在上述引用计数增加时候shared_ptr用作函数都非引用类型的形参时,调用函数结束后,该非引用类型的shared_ptr形参的引用计数就会减1~,这一点比较简单,这里就不多赘述了!)
c)当一个shared_ptr的引用计数从1-》0时,则该智能指针会自动释放自己所管理(指向)的对象
auto ps1 = make_shared<int>(10);//ps1指向一个对象
auto ps2 = make_shared<int>(10);//ps2指向另一个对象
ps1 = ps2;
//此时,给ps1赋值会让ps1指向ps2
//此时,ps1和ps2都指向同一个对象了,那么此时这2个shared_ptr指针的引用计数就为2
//ps1原来所指向的对象的引用计数就会从1-》0,因为你不指向原对象了,就自动给你释放掉了哈!
小总结-引用计数减少的case为:
a)让已经定义了的shared_ptr指向一个新的对象时,引用计数会减少!
b)局部的shared_ptr离开其作用域时,引用计数会减少!
(就比如在上述引用计数增加时候shared_ptr用作函数都非引用类型的形参时,调用函数结束后,该非引用类型的shared_ptr形参的引用计数就会减1~,这一点比较简单,这里就不多赘述了!)
c)当一个shared_ptr的引用计数从1-》0时,则该智能指针会自动释放自己所管理(指向)的对象
(2)shared_ptr指针的一些常用操作:
(2.1).use_count():
.use_count()的作用:返回指向某对象的智能指针的个数,也即返回引用计数的值。(主要用于调试代码)
请看以下代码:
auto p1 = make_shared<int>(1);
auto p2(p1);
auto p3(p2);
cout << "当前有" << p1.use_count() << "个shared_ptr智能指针指向同一个对象!" << endl;
cout << "当前有" << p2.use_count() << "个shared_ptr智能指针指向同一个对象!" << endl;
cout << "当前有" << p3.use_count() << "个shared_ptr智能指针指向同一个对象!" << endl;
运行结果:
(2.2).unique():
.unique()的作用:用于判断该智能指针是否独占某个指向的对象(该对象值不能为空)
(也即只有一个shared_ptr指针是指向该对象的 <==> 此时该智能指针的引用计数为1的意思)
请看以下代码:
auto p1 = make_shared<int>(1);
if (p1.unique())cout << "p1 is unique!" << endl;
else cout << "p1 isn't unique!" << endl;
auto p2(p1);
if (p1.unique())cout << "p1 is unique!" << endl;
else cout << "p1 isn't unique!" << endl;
运行结果:
(2.3) .reset():恢复(复位/重置),
①当reset()不带参数时,此时reset()函数的功能即:删除某个shared_ptr指针的意思。下面分情况讨论之:
---若该shared_ptr指针是唯一指向某对象的指针,那么就立刻释放其所指向的内存(<==>引用计数减为0了),并将该shared_ptr指针置为空nullptr。
---若该shared_ptr指针不是唯一指向某对象的指针,那么就不释放其所指向的内存(因为别的shared_ptr指针还需要用到!),但将其所指向对象的引用计数-1,同时将该shared_ptr指针置为空nullptr。
(这句很拗口的话其实就是这个意思:既然该shared_ptr指针并不是唯一指向同一个对象的,那么我将该shared_ptr指针变量删除了也不会影响别的shared_ptr指针的使用,只要我不去释放其所指向的内存就行~)
请看以下代码:
shared_ptr<int> pi(make_shared<int>(100));
pi.reset();
//此时pi并是唯一一个指向某对象的shared_ptr指针
//因此在删除它时必须要释放其所指向对象的内存(这里是智能指针自动给你do了这一步哈)
if (pi == nullptr)cout << "pi被置空!" << endl;
//result:pi被置空!
shared_ptr<int> pi(make_shared<int>(100));
auto pi2(pi);
pi.reset();
//此时pi并不是唯一一个指向某对象的shared_ptr指针
//因此在删除它时不用释放其所指向对象的内存
if (pi == nullptr)cout << "pi被置空!" << endl;
//result:pi被置空!
②当reset()带参数时(且该参数一般是一个new出来的指针对象),此时reset()函数的功能即:重置该shared_ptr指针。下面分情况讨论之:
---当该shared_ptr指针是唯一指向某对象的指针时,则立刻释放其所指向的内存,并让该shared_ptr指针指向reset(参数中的新对象)!
---当该shared_ptr指针不是唯一指向某对象的指针时,则不会释放其所指向的内存(因为别的shared_ptr指针还需要用到!),但将其所指向的对象的引用计数-1,并将该shared_ptr指针指向reset(参数中的新对象)!
请看以下代码:
shared_ptr<int> pi(new int(100));
pi.reset(new int(8));
//pi是唯一的指向100这个对象内存的shared_ptr指针
//因此当reset它时,释放掉原指向的内存,并将其指向新的对象8(开辟新内存)
//也即:释放原内存,并指向新的内存
shared_ptr<int> pi(new int(100));
auto pi2(pi);//引用计数变为2
pi.reset(new int(8));
//pi不是唯一的指向100这个对象内存的shared_ptr指针
//因此当reset它时,不会释放掉原指向的内存,但将其指向的原对象的引用计数-1,并
//将它指向新的对象8(开辟新内存)
//也即:不释放原内存,但指向新的内存
if (pi.unique())cout << "pi is unique!" << endl;
else cout << "pi isn't unique!" << endl;
if (pi2.unique())cout << "pi2 is unique!" << endl;
else cout << "pi2 isn't unique!" << endl;
运行结果:
注意:空的shared_ptr指针也可以通过reset()来重新进行初始化!
shared_ptr<int> p;//shared_ptr p本来就是个空的指针!
p.reset(new int(12));//那么此时p.reset后就不需要释放p所指向的内存了
//而是直接让p指向新对象12(开辟新内存)!
(2.4)*解引用:
这里的解引用,其实就是和普通指针的解引用一样,作用:取得指针所指向对象的值!
请看以下代码:
auto sptr = shared_ptr<int>(new int(198));
cout << "*sptr = " << *sptr << endl;
//result: *sptr = 198;
(2.5).get():
.get()的作用:用于返回shared_ptr智能指针中保存/指向的对象指针(裸指针)。
注意①:请务必小心使用get()函数,如果该shared_ptr智能指针释放了所指向的对象(内存),那么这个get函数返回的裸指针也会给释放掉(变得毫无意义)!
请看以下代码:
auto sptr = shared_ptr<int>(new int(198));
auto p = sptr.get();
sptr = make_shared<int>(998);
//让sptr指针指向别的对象时,那么get函数返回的指针所指向的内存也会给释放
运行结果:
让原shared_ptr不动:(因为它指向一个int型对象,所以p是一个int*)
让原shared_ptr指向新的内存:(此时智能指针原所指向的内存被释放了)
注意②:请务必小心使用get()函数,你不能对该裸指针do事情!因为裸指针是归于shared_ptr帮你自动管理的,你若一个不小心delete了就会造成程序崩溃!
(可能有同学会疑惑,为什么都用智能指针来管理动态内存了,还需要给shared_ptr一个get函数用于获取其所指向的裸指针呢?这不是又破又立吗?答:因为一些Cpp的第三方库的函数中,需要传入的是裸指针的参数,而不是智能指针的参数,因此就不得不引入get函数以达到此目的!)
请看以下代码:
auto sptr = shared_ptr<int>(new int(198));
auto p = sptr.get();
delete p;//千万不要这么干!你外面直接管理释放掉裸指针,那你前面定义的shared_ptr不就没用了么是吧?
//你一旦这么干了就会报异常,从而产生不可预料的结果!
sptr = make_shared<int>(998);
运行结果:
(2.6).swap():(不常用!)
.swap()的作用:交换2个智能指针所指向的对象。
请看以下代码:
shared_ptr<string> ps1 = make_shared<string>("I Love China1!");
shared_ptr<string> ps2 = make_shared<string>("I Love China2!");
运行结果:
当加上这一行代码时:
ps1.swap(ps2);
运行结果:
可见,.swap()这个shared_ptr指针模板中所内置的函数起了交换2个智能指针的作用。
当加上这一行代码时:
std::swap(ps1, ps2);
运行结果:
可见,当我用C++标准库中的std:swap()函数将2个智能指针do再一次的交换时,ps1和ps2又指向其原本所指向的对象了!
(2.7)= nullptr:
=nullptr的作用:
1---先将该shared_ptr指针所指向的对象的引用计数减去1(若该引用计数减为0了,就释放该指针所指向对象的内存。若引用计数没有减为0,因为还有别的指针要用嘛,就不释放该内存)
2---再将该shared_ptr智能指针置为空。
请看以下代码:
shared_ptr<string> ps1 = make_shared<string>("I Love China1!");
shared_ptr<string> ps2(ps1);
ps1 = nullptr;
运行结果:
当未执行到ps1 = nullptr;这行代码时的运行调试结果:
当执行完ps1 = nullptr;这行代码后的运行调试结果:
结果充分体现了shared_ptr智能指针=nullptr后的效果。
(2.8)智能指针名字作为判断条件:
其实就是智能指针和普通指针一样,其名字可以用于if-else等判断语句do条件的判断!
请看以下代码:
shared_ptr<string> ps1 = make_shared<string>("I Love China1!");
ps1 = nullptr;
if (ps1)cout << "ps1指向一个对象!" << endl;
else cout << "ps1为空!" << endl;
//result : "ps1为空!"
(2.9)指定删除器以及数组问题:
我们都知道,shared_ptr智能指针可以在引用计数减为0时,帮我们自动delete所指向的对象。
a)指定删除器:缺省case下,shared_ptr模板类中就是使用delete运算符作为默认的资源析构方式。但你也可以用自己指定的删除器来覆盖系统提供的默认的删除器。(我们自己可指定这个删除器,比如指定该删除器的功能是:打印一些日志输出语句再delete。)这样,当该指针需要删除所指向的对象时,编译器就会为我们自动调用自指定的删除其来do资源析构的工作。
注意:删除器可以是一个函数/lambda表达式(后续会学)/default_delete。shared_ptr中指定删除器的方式很简单,即:在创建该智能指针时在参数表中添加上具体的删除器函数名即可。
这里给出一篇大佬总结的lambda表达式的文章,以便我们学习哈:
格式:
//删除器必须是一个函数对象!
//1-Func类型的删除器
void myDeleteName(pointerType* pvar){
//...
delete pvar;//这句话是必须存在的!
//delete[] pvar;//new数组时用这句话释放资源!
//因为既然你自己决定要自己来管理这段智能指针的内存,就必须有义务手动释放该内存(当引用计数变为0时)
}
//2-Lambda表达式(函数)类型的删除器
auto myDeleteName = [](pointerType* pvar){
//...
delete pvar;//这句话是必须存在的!
//delete[] pvar;//new数组时用这句话释放资源!
}
//3-函数符类型的删除器
class Mydel{
public:
void operator()(int* p){
if(p){
delete p;//这句话是必须存在的!
//delete[] pvar;//new数组时用这句话释放资源!
}
cout<<"调用了自定义的删除器!"<<endl;
}
};
请看以下代码:(用函数来写shared_ptr的删除器)
void myintDelete(int* p) {//自指定的删除器:删除整型指针
cout << "自己指定的删除器void myDelete(int* p)起作用啦~" << endl;
delete p;//这句话是必须的!
}
void mydoubleDelete(double* p) {//自指定的删除器:删除整型指针
cout << "自己指定的删除器void myDelete(double* p)起作用啦~" << endl;
delete p;//这句话是必须的!
}
int main(void) {
shared_ptr<int> pi(new int(100), myintDelete);
pi = nullptr;
shared_ptr<double> pi2(new double(100.88), mydoubleDelete);
pi2 = nullptr;
return 0;
}
运行结果:
再看以下代码:(用lambda表达式来写shared_ptr的删除器)
shared_ptr<int> pi(new int(100), [](int* p)mutable->void{
cout << "lambda表达式形式的shared_ptr指针给释放啦!" << endl;
delete p; }) ;
shared_ptr<int> pi2(pi);
pi.reset();//删除指针pi,但不释放所指向对象的内存(因为引用计数只变为1而已)
pi2.reset();//删除指针pi2,并释放所指向对象的内存(因为引用计数变为0了)
运行结果:
可能有同学会有疑惑,我就只让shared_ptr智能指针的默认删除器帮我管理内存不就好了吗?为啥我还需要自己指定删除器呢?这不又是一件既破又立的事儿吗?
答:是的,一般情况下shared_ptr指针的默认删除器都可以很好地释放内存,但是有些特殊的情况下,默认删除器do的不是很好,因此此时就需要我们提供自己指定的删除器了!
那么什么情况下会需要提供自指定的删除器呢?
答:当使用shared_ptr来管理动态数组时,就必须要我们自己提供自指定的删除器!
请看以下代码:
class A {
public:
A() {}
~A() {}
};
int main(void){
shared_ptr<int> ps(new int[10], [](int* p)mutable->void {
cout << " [](int* p)mutable->void这个指定的删除器work了!" << endl;
delete[] p;
});
shared_ptr<A> pA(new A[10], [](A* p)mutable->void {
cout << " [](A* p)mutable->void这个指定的删除器work了!" << endl;
delete[] p;
});
return 0;
}
运行结果:
default_delete是标准库中的模板类(当然,这个模板类内部也是用delete来do的!):在指定shared_ptr的删除器时,可用default_delete作为指定的删除器!
请看以下代码:
shared_ptr<int> ps(new int[10],std::default_delete<int[]>());
//int[]告知std::default_delete这个模板类,我要删除的是int类型的数组!
shared_ptr<A> pA(new A[10],std::default_delete<A[]>());
//A[]告知std::default_delete这个模板类,我要删除的是A类型的数组!
当然,为了解决以上叙述中用shared_ptr智能指针的删除器删除数组类型的指针变量很不方便的问题(释放指向数组类型的指针时),C++17引入了在尖括号内可加入[]的格式!
格式:
shared_ptr<pointerType[]> pointerName(new pointerType[nums]);
请看以下代码:
shared_ptr<A> pA(new A[10]);//×!会报异常!正常运行!
shared_ptr<int> pA2(new int[10]);//×!
//虽然编译器不会报异常,因为这是delete内置的int数据类型的指针,此时虽然delete p不符合语法,但是编译器还是会给内置的数据类型以delete[] p的方式释放内存!
//但是总的来说非常不推荐这么写!
请看以下代码:
shared_ptr<A[]> pA(new A[10]);//✓!C++17引入的!正常运行!
shared_ptr<int[]> pA2(new int[10]);//✓!C++17引入的!
这样我们在尖括号<>中加个[]就可以让shared_ptr正确地帮助我们删除指向数组的指针了!
补充:以后见到用函数模板来封装shared_ptr数组时,不要发懵!
请看以下代码:
class A {
public:
A() {}
~A() {}
};
template<typename T>
shared_ptr<T> make_array_shared_ptr(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
//创建一个指向大小为size,类型为T的数组的shared_ptr指针
}
shared_ptr<int> pintArr = make_array_shared_ptr<int>(5);
//创建一个指向int型大小为5的数组的shared_ptr指针
shared_ptr<A> pintArr = make_array_shared_ptr<A>(3);
//创建一个指向A型大小为3的数组的shared_ptr指针
b)指定删除器额外的说明:
就算有2个shared_ptr指针分别指定了不同的删除器,只要他们所指向的对象类型相同,我们就认为这2个指针是同属于一个类型的!!!
shared_ptr<int> p1(new int(100), lambdaDelete1);
shared_ptr<int> p2(new int(200), lambdaDelete2);
p2 = p1;
//p2先调用lambdaDelete2先把p2所指向的对象释放,然后再指向p1所指向的对象。此时p1指向的对象引用计数为2
//当整一个main函数执行完毕后,还会调用lambdaDelete1来释放p1和p2所共同指向对象。
既然同属于一个类型,就表明可以将这2个shared_ptr指针都放到元素类型为该对象类型的容器中!
shared_ptr<int> p1(new int(100), lambdaDelete1);
shared_ptr<int> p2(new int(200), lambdaDelete2);
vector<shared_ptr<int>> vec{ p1,p2 };
cout << *vec[0] << endl;//100
cout << *vec[1] << endl;//200
补充之前说的make_shared知识点:
std::make_shared()这个标准库函数是我们提倡的生成shared_ptr的方法。但是,使用该函数来生产shared_ptr智能指针时,我们就没有办法提供自己指定的删除器了!这是该函数的一大缺点!
以上就是我总结的关于shared_ptr常用的操作、计数、自定义删除器的笔记。希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~