Th5.3:智能指针(shared_ptr)之详述

本小节回顾学习的知识点分别是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表达式的文章,以便我们学习哈:

C++中的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的路上~

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fanfan21ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值