• c++动态内存管理与智能指针


    程序使用三种不同的内存

    1. 静态内存:static成员以及任何定义在函数之外的变量
    2. 栈内存:一般局部变量
    3. 堆内存(自由空间):动态分配的对象

    静态内存和栈内存中的变量由编译器产生和销毁,动态分配的对象在我们不再使用它时要由程序员显式地销毁

    1|0一、介绍

    动态分配内存

    1. new():为对象分配空间,并返回指向该对象的指针
    2. delete:销毁对象,并释放与之相关的内存

    使用智能指针:定义在头文件memory

    1. shared_ptr:允许多个指针指向同一个对象
    2. unique_ptr:“独占”所使用的对象
    3. weak_ptr:伴随类,弱引用,指向shared_ptr所管理的对象

    和容器一样,只能指针也是一种模板,需要给它传入一个参数来指定类型

    2|0二、shared_ptr类

    声明shared_ptr:

    shared_ptr<string> p1; //shared_ptr,可以指向string shared_ptr<list<int>> p2; //shared_ptr,可以指向list<int>

    使用方式与普通指针一致,解引用返回它所指向的对象,在条件表达式中检查是否为空

    //若p1不为空且指向一个空string if(p1 && p1->empty()){ *p1 = "hi"; //对p1重新赋值 }

    image-20220228105731951

    2|1make_shared函数

    make_shared<typename>(arguments)

    在动态内存中分配并初始化一个对象

    返回指向此对象的shared_ptr指针

    //指向一个值为42的int的shared_ptr shared_ptr<int> p1 = make_shared<int>(42); //指向一个值为"999"的string的shared_ptr shared_ptr<string> p2 = make_shared<string>(3, '9'); //指向一个值为0的int的shared_ptr shared_ptr<int> p3 = make_shared<int>();

    没有传入参数时,进行值初始化

    auto p4 = make_shared<string>(); //p4指向空string

    2|2shared_ptr的拷贝和引用

    每个share_ptr都有一个关联的计数器

    • 当拷贝shared_ptr时,计数器会递增
    • 当shared_ptr被赋予新值或者shared_ptr被销毁(如一个局部的shared_ptr离开其作用域),计数器会递减
    • 当一个shared_ptr的计数器==0时,内存会被释放
    auto r = make_shared<int>(42); r = q; //给r赋值,使它指向另一个地址 //递增q指向的对象的引用计数 //递减r指向的对象的引用计数 //如果计数器为0,自动释放

    2|3shared_ptr自动销毁所管理的对象…

    和其他类一样,shared_ptr类型也有析构函数

    shared_ptr的析构函数会

    1. 递减指针所指向的对象的引用计数
    2. 当对象的引用计数为0时,销毁对象并释放内存

    2|4…shared_ptr还会自动释放相关联对象的内存

    举例:

    //factory返回一个share_ptr,指向一个动态分配的对象 shared_ptr<Foo> factory(T arg){ //对arg的操作 return make_shared<Foo>(arg); } void ues_factory(T arg){ shared_ptr<Foo> p = factory(arg); //使用p } //p离开了作用域,由于引用计数由1减到0,对象被销毁,内存释放

    如果有其他引用计数也指向该对象,则对象内存不会被释放掉

    //factory和上述一致 //ues_factory返回shared_ptr的拷贝 void use_factory(T arg){ shared_ptr<Foo> p = factory(arg); //使用p return p; //返回p的拷贝,此时递增了计数器,引用数为2 }//p离开作用域,对象计数器引用2-1=1,对象内存没有释放

    return shared_ptr时,如果不是返回引用类型,则会进行拷贝,shared_ptr的计数器+1后-1,最终shared的计数器不变

    由于在最后一个shared _ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。

    share_ptr 在无用之后仍然保留的一种可能情况是,你将shared _ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。

    如果你将shared ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

    2|5使用动态生存期的资源的类

    程序使用动态内存的三种原因

    1. 程序不知道自己需要使用多少对象
    2. 不知道所需对象的准确类型
    3. 需要在多个对象间共享数据

    容器类常出于第一种原因使用动态内存,在15章会看见出于第二种原因的例子,本节讨论第三种原因

    先考虑这么一种情况:

    我们要定义一个Blob类,当该类型的对象拷贝时,对象共享底层数据。

    如b2 = b1时,b2,b1共享底层数据,对b2的操作也会印象到b1,且销毁b2时,b1的仍指向原数据

    Blob<string> b1; //空Blob { //新作用域 Blob<string> b2 = {"a","an","the"}; b1 = b2; //b1和b2共享数据 }//b2离开作用域,被销毁了,但b2的数据不能被销毁 //b1指向b2的原数据

    2|6应用举例:Blob类

    1|0定义Blob类

    最终,我们希望将Blob定义为一个模板类,但现在我们先将其定义为StrBlob,即底层数据是vector<string>的Blob

    class StrBlob{ public: //拷贝控制 StrBlob();//默认构造函数 StrBlob(initializer_list<string> il); //列表初始化 StrBlob(const StrBlob& strb); //查询 int size() const {return data->size();} bool empty() const {return data->empty();} //添加和删除元素 void push_back(const string &t) {data->push_back(t);} void pop_back() {data->pop_back();} //访问元素 string& front(); string& back(); private: shared_ptr<vector<string>> data; //如果data[i]不合法,抛出异常 void check(int i, const string &msg) const; };

    1|0StrBlob的构造函数

    StrBlob::StrBlob() : data(make_shared<vector<string>>()) {cout<<"in StrBlob dafault"<<endl;}; StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) {cout<<"in StrBlob initializer_list"<<endl;}

    1|0元素访问成员函数

    在访问时必须保证容器非空,定义check函数进行检查

    void StrBlob::check(int i, const string& msg) const{ if(i >= data->size()) throw out_of_range(msg); }

    元素访问成员函数:

    string& StrBlob::front(){ //如果vector为空,check会抛出一个异常 check(0, "front on empty StrBlob"); return data->front(); } string& StrBlob::back(){ check(0, "back on empty StrBlob"); return data->back(); }

    1|0StrBlob的拷贝、赋值和销毁

    StrBlob使用默认的拷贝、赋值和析构函数对此类型的对象进行操作

    当我们对StrBlob对象进行拷贝、赋值和销毁时,它的shared_ptr成员也会默认地进行拷贝、赋值和销毁

    //由于data是private的 //在StrBlob中设置一个接口look_data //look_data返回data的引用 class StrBlob{ public: //... shared_ptr<vector<string>>& look_data() {return data;} //返回引用,避免对象拷贝 private: //其余部分都不变 };

    测试程序:

    //测试程序 int main(){ StrBlob b1; {//新作用域 StrBlob b2 = {"first element","second element"}; cout<<"before assignment : " <<b2.look_data().use_count()<<endl; b1 = b2; cout<<"after assignment : " <<b2.look_data().use_count()<<endl; }//b2被销毁,计数器递减 //b1仍指向b2的原数据 cout<<b1.front()<<endl; //打印此时b1的计数器 cout<<"b2 has been dstoryed : " <<b1.look_data().use_count()<<endl; return 0; }

    输出结果:

    image-20220228163045670

    如果look_data值返回,而不是引用返回,那么会存在拷贝【见6.2.2节笔记】,所有计数器的值会+1

    image-20220228163205679

    3|0三、直接管理内存

    3|1使用new分配内存

    • new分配动态内存
    • delete销毁动态内存

    new和delete与智能指针不同,类对象的拷贝、赋值和销毁操作都不会默认地对动态分配的对象进行管理,无论是对象的创建还是销毁,都需要程序员显式地操作,在大型的应用场景中会十分复杂。

    在熟悉C++拷贝控制之前,尽量只使用智能指针,而不是本节的方法管理动态内存

    1|0使用new动态分配和初始化对象

    new type_name:返回一个指向该对象的指针

    //pi指向一个动态分配,默认初始化的无名对象 int *pi = new int; //*pi的值是未定义的 cout<<*pi<<endl;

    对象是默认初始化这意味着:

    1. 指向的是:内置类型和组合类型对象。对象的值是未定义的

    2. 指向的是:类类型对象。调用默认构造函数

    可以直接初始化动态分配的对象

    • 直接调用构造函数
    • 列表初始化
    //pi指向对象的值为42 int *pi = new int(42); //"9999999999" string *ps = new string(10, '9'); //vector有5个元素,依次为0,1,2,3,4 vector<int> *pv = new vector<int>{0,1,2,3,4};

    也可以值初始化

    string *ps1 = new string(); //值初始化为空string string *ps = new string; //默认初始化为空string int *pi1 = new int; //默认初始化,值未定义 int *pi = new int(); //值初始化,*pi = 0;

    所以,初始化动态分配的对象是一个好习惯

    1|0动态分配const对象

    new可以分配const对象

    和其他const对象一样,动态分配的const对象必须被初始化

    //分配并初始化const int const int *pi = new const int(1024); //分配并默认初始化const string const string *ps = new const string;

    1|0内存耗尽

    如果new分配动态内存失败,返回一个空指针,并报出std::bad_alloc异常

    int *p1 = new int; //返回空指针,抛出异常 int *p2 = new (nothrow) int; //如果分配失败,new返回空指针

    我们第二种形式的new为定位new (placement new),其原因我们将在19.1.2节(第729页)中解释。

    定位new表达式允许我们向new传递额外的参数

    在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的 new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。

    3|2使用delete释放内存

    1|0基本介绍

    delete():接受一个指针,指向我们想要销毁的对象

    执行两个操作

    • 销毁对象
    • 释放对应的内存

    注意点:

    1. 保证只传给delete动态分配的指针,将一般指针传给delete,其行为是未定义的
    2. 同一块内存不能释放两次
    3. 不要忘记delete内存
    4. 不要使用已经delete的对象
    int i, *pi = &i; int *pd = new int(); delete pd; //正确:释放pd内存 pd = nullptr; //好习惯:指出pd不再指向动态内存 delete pi; //未定义:pi没有指向动态分配的内存 delete pd; //未定义:pd内存已经被释放

    保证以上两点是程序员的责任,编译器并不会检查以上错误

    1|0举例

    在被显式地delete前,用new动态分配的内存一直存在

    Foo* factory(T arg){ //处理arg return new Foo(arg); }//调用者负责释放 void ues_factory(T arg){ Foo *p = factory(arg); //使用p但不delete它 }//p离开了作用域,但它所指向的内存没有被释放!!

    use_factory返回时,局部变量p被销毁。但此变量是一个内置指针,而不是一个智能指针,所以p所指向的内存并没有被销毁

    这样就产生了一块无名的内存块,存在又无法删除。

    这也体现了智能指针与普通指针的区别:智能指针在离开自己的作用域,自己的变量名失效时,销毁指向的对象并释放关联内存;而new产生的指针不会。

    修改use_factory:

    void use_factory(T arg){ Foo *p = factory(arg); //使用p delete p; //记得释放p }

    坚持使用智能指针,可以避免上述的绝大部分问题

    4|0四、shared_ptr和new结合使用

    4|1new直接初始化share_ptr

    可以用new返回的指针初始化share_ptr

    构造函数是explicit

    所以,不存在new产生的指针向shared_ptr的隐式类型转换,必须采用直接初始化,而不是拷贝初始化或者赋值

    shared_ptr<int> p1(new int(42)); //正确:使用直接初始化 shared_ptr<int> p2 = new int(30);//错误:new产生的指针

    同理,返回shared_ptr的函数不能返回new产生的指针

    shared_ptr<int> clone(int p){ //错误:构造函数为explicit,无法转换 //return new int(p); //正确:显式地用int*构造shared_ptr<int> return shared_ptr<int>(new int(p)); }

    如对隐式类型转换有疑问查看 7-5笔记第三点”隐式类类型转换”

    4|2初始化时传入可调用对象代替delete

    默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete。我们将在12.1.4节介绍如何定义自己的释放操作。

    image-20220228230530493

    image-20220228230537447

    5|0五、unique_ptr

    和shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象

    5|1基本操作

    必须采用直接初始化

    unique_ptr<double> p1; //可以指向double的一个unique_ptr unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int

    unique_ptr不支持拷贝与赋值

    unique_ptr<string> p1(new string("hello")); unique_ptr<string> p2(p1); //错误:不支持拷贝 unique_ptr<string> p3; p3 = p1; //错误:不支持赋值

    unique_ptr支持的操作

    image-20220301083622870

    可以使用release和reset将指针的所有权从一个(非const)unique_ptr转移到另一个unique_ptr

    //将所有权从p1,转移到p2 unique_ptr<string> p1(new string("hello")); unique_ptr<string> p2(p1.release()); //release将p1置空 cout<<*p2<<endl; //输出 hello unique_ptr<string> p3(new string("world")); //p2绑定的对象被释放,p3置空,p2指向p3原来指向的对象 p2.reset(p3.release()); cout<<*p2<<endl; //输出: world

    5|2传递和返回unique_ptr

    不能拷贝unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

    unique_ptr<int> clone(int p){ //正确:从int*创建一个unique_ptr<int> return unique_ptr<int>(new int(p)); }

    还可以返回一个局部变量的拷贝

    unique_ptr<int> clone(int p){ unique_ptr<int> ret(new int(p)); return ret; }

    对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,我们将在13.6.2节(移动构造函数和移动运算符)中介绍它。

    5|3向unique_ptr传递删除器

    //p指向一个类型为objT的对象 //并使用一个类型为delT的可调用对象释放objT //p会使用一个名为fcnd的delT对象来删除objT unique_ptr<objT, delT> p(new objT, fcn);

    作为一个更具体的例子,我们将写一个连接程序,用unique_ptr来代替shared_ptr,如下所示:

    void f(destination &d /*其他需要的参数*/) { connection c = connect(&d);//打开链接 unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection); //使用链接 //当f退出时(即使是由于异常而退出) //connection会调用end_connection正常退出 }

    注意decltype(end_connection)返回一个函数类型,而函数类型不能作为参数,函数指针可以
    所以要加上*表示函数指针
    p(&c, end_connection)中,类似于数组名表示指针一样,函数名实际上就表示函数指针
    所以也可写作p(&c, &end_connection),但没必要。【前一个&表示引用传递,后一个&表示取址得到指针】


    __EOF__

    本文作者萌之上
    本文链接https://www.cnblogs.com/timothy020/p/15948748.html
    关于博主:评论和私信会在第一时间回复。或者直接私信我。
    版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
    声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
  • 相关阅读:
    十三、vite项目中无法使用minio的解决方案
    CTO与CIO选型数据中台的几大建议
    java ssm空巢老年人社区服务系统
    java基于微信小程序的社区疫情防控系统 uniapp 小程序
    6161. 从字符串中移除星号 Java解决
    ResNet网络架构
    Promise封装Ajax请求
    SpringBoot 快速入门(保姆级详细教程)
    ChatGPT AIGC总结Excel中Vlookup,lookup,xlookup的区别
    亲测可用:Axios携带自定义的Cookie解决方案
  • 原文地址:https://www.cnblogs.com/timothy020/p/15948748.html