【C++】不能两次杀死同一条鱼 - 浅述shared_ptr智能指针的使用方法

在C++里,动态对象的创建是通过new操作符进行的,在恰当的时候通过delete操作符释放动态对象的空间并执行其析构函数是程序员的职责。遗憾的是,多数新手程序员都做不好这项工作,相关的疏失导致了巨量的软件缺陷:

  • 未能释放动态对象,导致内存泄漏。
  • 在内存释放后再次访问指针所指向的动态对象。在释放指针所指向的动态对象后,及时将指针置为空对避免该问题的发生有帮助。
  • 多次释放同一个动态对象。这种情况多发生在两个以上的指针指向同一个动态对象时。

本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

以少许效率损失为代价,智能指针可以部分解决此问题。本节以shared_ptr为例,简要描述智能指针的使用方法及基本工作原理。请阅读下述C++代码。

//Project - SharedPointer
#include <iostream>
#include <memory>
using namespace std;

class Fish {
public:
    string sName;
    Fish(const string& name){
        sName = name;
        cout << "Fish Constructor called: " << sName << endl;
    }

    void sayHello(){
        cout << "Aloha: " << sName << endl;
    }

    ~Fish(){
        cout << "Fish Destructor called:  "  << sName << endl;
    }
};

void sayHello(shared_ptr<Fish> f){
    f->sayHello();      //对智能指针使用指向操作符->
}

void sayHello(Fish& f){
    f.sayHello();
}

int main(){
    shared_ptr<Fish> dora1(new Fish("Dora"));
    shared_ptr<Fish> tom1 = make_shared<Fish>("Tom");
    cout << "-----------------------------------------" << endl;

    sayHello(tom1);
    auto tom2 = tom1;   //智能指针对象的复制
    sayHello(*tom2);    //对智能指针使用解引用操作符*
    cout << "-----------------------------------------" << endl;

    dora1->sayHello();
    Fish* dora2 = dora1.get();   //获取智能指针内的原始指针
    dora2->sayHello();
    cout << "-----------------------------------------" << endl;
    return 0;
}

上述程序的执行结果为:

Fish Constructor called: Dora
Fish Constructor called: Tom
-----------------------------------------
Aloha: Tom
Aloha: Tom
-----------------------------------------
Aloha: Dora
Aloha: Dora
-----------------------------------------
Fish Destructor called:  Tom
Fish Destructor called:  Dora

头文件引入了共享型智能指针模板类shared_ptr。

🎯
要点shared_ptr表示一个指向T类型对象的智能指针对象。智能指针对象不是一个平凡的指针,而是一个包含平凡指针的对象,它通过引用计数来记录指针所指向的对象的被引用次数,当被指向对象的引用计数降到0时(意味着动态对象不再被需要),智能指针对象会通过delete操作符或者指定deleter函数释放动态对象。

🚩第6 ~ 21行:为了演示智能指针所管理的动态对象的生命周期,我们设计了Fish类。Fish的构造及析构函数都会向控制台报告构造或析构的消息。

🚩第23 ~ 25行:sayHello()函数接受一个智能指针对象f为参数,然后对f使用指向操作符访问Fish对象的sayHello()方法。请注意,f是一个智能指针对象,此处的参数传递为传值;第24行的指向操作符事实上执行的是f对象的重载operator->()函数。

🚩第27 ~ 29行:函数名重载的sayHello()接受Fish的引用f作为参数,然后执行f的sayHello()成员函数。

🚩第32行:定义并构建了指向Fish对象的智能指针对象dora1,以动态Fish对象"Dora"的地址作为参数。该行代码执行过程包含如下几步。

  • dora1是一个自动对象,在栈内为其分配空间;
  • new Fish(“Dora”)在堆内分配对象空间并构造初始化,返回“Dora鱼”的地址;
  • 以new Fish(“Dora”)返回的地址为参数,执行dora1的构造函数。该构造函数将动态对象的地址保存在dora1内部,并将引用计数置为1,以表明该动态对象当前被1个智能指针对象所“引用”。

执行结果的第1行对应"Dora鱼"的构造输出。

🚩第33行:该行代码的执行包含如下几步。

  • tom1是一个自动对象,在栈内为其分配空间;
  • make_shared(“Tom”)函数创建并构造一个Fish类型的堆对象(以"Tom"为参数),然后构造并返回一个指向该动态对象的shared_ptr类型的智能指针;
  • 上述返回的智能指针被拷贝构造给tom1。
📍
注意由于编译器优化的原因,编译器有可能会省掉先构造智能指针再拷贝构造智能指针的不必要步骤,而直接执行tom1的构造函数,以动态Fish对象的指针作为参数。

执行结果的第2行对应"Tom鱼"的构造输出。

🚩第36行:将智能指针对象tom1传值给sayHello()函数(第23行)的形参f,该函数对f应用指向操作符,执行“Tom鱼”的sayHello()方法。可以想象,当f对象被拷贝构造时,“Tom鱼”的引用计数将由1变2,因为此时tom1和f两个智能指针都指向“Tom鱼”;在sayHello()函数结束执行前,局部对象f被析构,“Tom鱼”的引用计数则会由2变1。本行的输出见执行结果的第4行。

🚩第37行:从tom1拷贝构造tom2。“Tom鱼”的引用计数将由1变2,因为智能指针tom1和tom2都指向“Tom鱼”。

🚩第38行:第27行的sayHello()函数接受一个Fish&作为参数,通过对tom2使用解引用操作符*,可以获得Fish类型的对象。tom2的类型可以视为Fish*,*tom2的类型则为Fish。事实上,tom2是通过执行tom2对象的operator()操作符函数来实现“解引用”的功能的。本行的输出见执行结果的第5行。

🚩第41行:对智能指针dora1使用指向操作符,执行“Dora鱼”的sayHello()方法。如前所述,该指向操作符事实上是通过dora1的operator->()操作符函数发挥作用的。本行的输出见执行结果的第7行。

🚩第42 ~ 43行:执行dora1对象的get()成员函数获取智能指针内部的“原始”指针,然后通过原始指针执行“Dora鱼”的sayHello()方法。成员操作符“.”证实,dora1是一个对象,而不是一个平凡的指针。相关输出见执行结果的第8行。

到了main()函数的结尾,自动对象tom2的析构导致“Tom鱼”的引用计数由2变1、tom1的析构则进一步导致引用计数由1变0,这表明“Tom鱼”不再被任何智能指针所引用。在tom1的析构函数里,delete操作符被执行,“Tom鱼”动态对象被释放。执行结果的第10行可见“Tom鱼”的析构输出。

同理,dora1的析构导致“Dora鱼”的释放,执行结果的第11行可见“Dora鱼”的析构输出。

智能指针的使用简化了动态对象的生命周期管理,程序员不必再手动释放动态对象。当最后一个指向该动态对象的智能指针对象被析构时,该动态对象会被释放。大多数情况下,由此所导致的性能损失是可以接受的。

shared_ptr<Fish> p1(new Fish("1"));  //1号鱼及其指针p1
auto p2 = make_shared<Fish>("2");    //2号鱼及其指针p2
p2 = p1;       //p2与原引用对象解绑,改为绑定p1所指向的对象
p1.reset();    //p1与对象解绑
if (p1.get() == nullptr)   //解绑后的p1是空指针
     cout << "p1 is nullptr." << endl;
p1.reset(p2.get());        //p1与原对象解绑,改为绑定p2所指向的对象

上述代码进一步演示了shared_ptr的使用方法。

🚩第3行:将p1赋值给p2,这将导致如下结果。

  • p2与原对象解除绑定,本例中p2是指向原对象的唯一智能指针,原对象即2号鱼被释放;
  • p2改为指向p1所指向的对象。

🚩第4行:p1的reset()成员函数将解除p1与对象的绑定,解绑后,p1成为“空指针”。

🚩第7行:p1.reset(p2.get())的执行导致如下结果。

  • p1与原绑定对象解绑;
  • p1改为指向p2所指向的对象。
💥
警告智能指针仅适用于动态(堆)对象,不要对栈对象或者静态对象创建智能指针,因为栈对象和静态对象的生命周期是由编译器自动管理的。不要通过动态对象的原始指针创建多个互不相关的智能指针。智能指针的get()方法所返回的原始指针只可使用,不可应用delete进行释放。如果这样做了,当智能指针析构时,会对同一个动态对象进行第2次释放。

下述代码演示了一些常见的智能指针的错误使用方法:

Fish* f = new Fish("Peter");
shared_ptr<Fish> p1(f);
auto p2 = p1;            //正确
shared_ptr<Fish> p3(f);  //错误

🚩第3行:正确,p2的出现仅会导致动态对象引用计数的增加,当且仅当p2和p1都被析构时,“Peter鱼”才会被释放。

🚩第4行:错误,智能指针p3与p1/p2互不相关,它会为“Peter鱼”创建一个新的引用计数。本例中,p1(以及p2)会释放“Peter鱼”,p3也会释放“Peter鱼”,显然,我们不可以杀死同一条鱼两次。
在这里插入图片描述

Fish* p = p3.get();     //p3是个指向Fish对象的智能指针
delete p;               //错误

🚩第2行:错误,释放p指针所指向的对象后,智能指针p3以及他的表兄弟们,还会再释放一次。同理,不能两次杀死同一条鱼。

读者如果对智能指针的工作机制感到疑惑,请仔细阅读下一节 - 智能指针的实现。

普通的指针可以指向一个使用new []操作符创建的动态数组,智能指针也可以。

shared_ptr<float> a(new float[1024]);

无论是new float,还是new float[1024],所得到的都是float*,也就是说,上述代码中的智能指针a并不知晓其指向的是一个动态对象,还是由多个动态对象构成的数组。根据第8章的讨论,new []所返回的指针必须通过delete []操作符进行释放,显然,上述智能指针a并不知道它所指向是一个动态数组,它只能使用delete而不是delete []来释放对象,这样做有风险。

💥
警告当使用shared_ptr管理动态对象数组时,要么指定类型为对象数组,要么提供一个删除者(deleter)函数通过delete []释放数组,该函数将在智能指针释放对象时被调用。

下述C++代码演示了确保智能指针安全释放对象数组的方法。

//Project - SharedPtrArray
#include <iostream>
#include <memory>
using namespace std;

template <typename T>
void delete_array(T* p){
    cout << "delete_array" << endl;
    delete[] p;
}

class Fish{
public:
    ~Fish(){ cout << "Fish::Fish~()" << endl; }
};

int main(){
    shared_ptr<Fish[]> a(new Fish[4]);
    a = nullptr;   //a指针指向的数组被释放

    shared_ptr<Fish> b(new Fish[2],delete_array<Fish>);
    b.reset();     //b指针指向的数组被释放

    shared_ptr<float> c(new float[512],
        [](float*p){cout << "lambda function\n"; delete[] p;});
    *c = 4.4F;
    //c++;           //错误:智能指针不支持指针运算
    //c[1] = 99.2F;  //错误:智能指针不支持[]操作符
    cout << *c << endl;
    return 0;
}

上述代码的执行结果为:

Fish::Fish~()
Fish::Fish~()
Fish::Fish~()
Fish::Fish~()
delete_array
Fish::Fish~()
Fish::Fish~()
4.4
lambda function

🚩第18 ~ 19行:智能指针a的类型中的模板参数被指定为Fish [],这相当于告知a对象,其所管理的是一个元素类型为Fish的动态数组。第19行给指针a赋值为nullptr,将导致a指针释放对象数组。从执行结果的第1 ~ 4行可见,共有4次Fish对象的析构函数执行,这间接说明,对象数组的释放是通过delete []操作符进行的。

🚩第21 ~ 22行:智能指针b的构造函数的第2个参数为自定义的删除者函数。该函数是一个通用的模板函数,其通过delete []操作符释放对象数组。执行结果的第5 ~ 7行显示,该函数成功执行,并析构了两个Fish对象。

🚩第24 ~ 25行:程序为智能指针c提供了一个匿名函数【C++ 11】▲作为删除者函数。执行结果的第9行对应该匿名函数的执行输出。自动对象c在main()函数的结尾被释放并引发了删除者函数的执行。

🚩第27行:如注释所言,智能指针不支持象普通指针那样的指针运算。如果确实需要,只能通过get()方法获取原始指针后进行。

🚩第28行:同样地,智能指针也不支持普通指针那样的[]操作符。

为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

简洁的C及C++
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
Python编程基础及应用
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造

如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

Python编程基础及应用

Python编程基础及应用实验教程
在这里插入图片描述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值