zoukankan      html  css  js  c++  java
  • C++基础之动态内存

    C++支持动态分配对象,它的生命周期与它们在哪里创建无关,只有当显示的被释放时,这些对象才会被销毁。分配在静态或栈内存中的对象由编译器自动创建和销毁。

    new在动态内存中为对象分配空间并返回一个指向该对象的指针,并调用构造函数构造对象;delete接受一个动态对象指针,调用析构函数销毁该对象,并释放与之相关的内存。

    那么new、delete和malloc、free有什么区别和联系呢?

     1、new/delete是C++的操作符,而malloc与free是C++/C 语言的标准库函数,不在编译器控制权限之内。

    2、new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。

    3、new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。

    4、new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。

    5、new自动计算需要分配的空间,而malloc需要手工计算字节数
    6、new是类型安全的,而malloc不是,比如:

    int* p = new float[2]; // 编译时指出错误
    int* p = malloc(2*sizeof(float)); // 编译时无法指出错误

    new operator 由两步构成,分别是 operator new 和 construct

    7、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
    8、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
    9、malloc/free要库文件支持,new/delete则不要。

    new动态分配内存

    new无法为其分配的对象命名,而是返回一个指向该对象的指针;默认情况下,动态分配的对象是默认初始化,因此内置对象和组合对象是未初始化的。

    int *p1 = new int;//动态分配一个未初始化的无名对象
    string *p2 = new string;//默认初始化为空string
    View Code

    养成一个为动态分配的对象初始化的好习惯。

    如果提供括号包围的初始化器,则可以通过初始化器来推断对象类型,就可以使用auto,但是这是当括号中仅有单一初始化器的时候才能使用auto。

    动态分配一个const对象必须进行初始化。

    auto p1 = new auto("haha");
    const int *p2 = new const int(10);

    默认情况下,new操作符时内存不足会抛出一个bad_alloc的异常。

    但是还有定位new的形式,使得此时不抛出异常,而返回一个空指针。定位new允许我们想new中传递额外的参数。

    int *p = new (nothrow) int;//如果失败,返回空指针

    delete释放内存

    通常,编译器是不能分辨一个指针是指向的静态对象还是动态对象,所以下面的操作是错的,但是编译期检查不出来。

    int i = 10;
    int *p = &i;
    delete p;//释放局部变量,错误

    注意:new和delete经常出现的三种错误

    1. 忘记释放内存;
    2. 使用已经释放的内存;
    3. 同一块内存释放两次。

    delete之后重置指针为空是一个好习惯,悬空指针很危险,通常上面的第二个错误就是悬空指针引起的;但是这样也只是提供有限的保护。

    一、智能指针

    动态分配内存容易出现问题,因为确保在正确的时间释放内存很困难,我们可能经常忘记释放的内存。于是C++11的标准库中提供了两种智能指针来管理动态对象,它们自己负责动态释放所指向的对象。

    分别是:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

    shared_ptr和unique_ptr都支持的操作如下:

    操作 描述

    shared_ptr<T> sp

    unique_ptr<T> up

    空智能指针,可以指向类型为T的对象
    p 将p用作一个条件判断,若p指向一个对象,则为true
    *p 解引用p,获得它指向的对象
    p->mem 等价于(*p).mem
    p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。

    swap(p,q)

    p.swap(q) 

    交换p和q中的指针

    1.shared_ptr类

    智能指针也是模板,需要提供指向的类型,且默认初始化时,智能指针保存着一个空指针。

    shared_ptr<string>sp;//可以指向string的空智能指针

    相关操作如下:

    操作 描述
    make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象。
    make_shared<T>p(q)  p是shared_ptr  q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
    p = q  p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
    p.unique()   若p.use_count()为1,返回true;否则返回false
    p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于测试

    最安全的分配和使用动态内存的方法是使用make_shared的函数,它和顺序容器的emplace成员类似,它的参数和指向的对象的某个构造函数匹配。

    通常可以使用auto来推断类型,方便使用。

    shared_ptr<string>sp = make_shared<string>(10,'9');//指向一个值为“9999999999”的string
    auto spInt = make_shared<int>(10);

    每个shared_ptr都会记录有多少个其他的shared_ptr志向相同的对象。因此,可以认为每个shared_ptr都有一个与之关联的计数器,通常称为引用计数。当一个shared_ptr的引用计数为0,它就会自动释放自己所管理的对象。

    auto r = make_shared<int>(10);
    r = q;//给r赋值,是它指向另一个对象;则q指向的对象的引用计数会递增,r原来指向的对象的引用计数会递减,如果减为0,则释放该对象。

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

    下面的三种情况下使用动态内存:

    1. 程序不知道自己需要多少个对象;
    2. 程序不知道需要的对象准确类型;
    3. 程序需要在多个对象之间共享数据。
    #include <iostream>  
    #include <string>  
    #include <memory>             //智能指针和动态分配内存  
    #include <vector>  
    #include <initializer_list>   //初始值列表  
    #include <stdexcept>  
      
    class StrBlob  
    {  
        public:  
            typedef std::vector<std::string>::size_type size_type;  
            StrBlob();  
            StrBlob(std::initializer_list<std::string>il);  
            size_type size()const{ return data->size(); }  
            bool empty() { return data->empty(); }  
            //添加删除元素  
            void push_back(const std::string &s){ data->push_back(s); }  
            void pop_back();  
            //访问元素  
            std::string& front();  
            std::string& back();  
            const std::string& front()const;   
            const std::string& back() const;  
      
        private:  
            std::shared_ptr<std::vector<std::string>> data;  
            //private 检查函数。  
            void check(size_type i, const std::string &msg)const;  
    };  
      
    //默认构造函数  
    StrBlob::StrBlob():  
        data(std::make_shared<std::vector<std::string>>()) { }  
    //拷贝构造函数  
    StrBlob::StrBlob(std::initializer_list<std::string>il):  
        data(std::make_shared<std::vector<std::string>>(il)) { }  
      
      
    void StrBlob::check(size_type i, const std::string &msg)const  
    {  
        if(i >= data->size())  
            throw std::out_of_range(msg);  
    }  
      
    const std::string& StrBlob::back()const  
    {  
        check(0, "back on empty StrBlob");  
        return data->back();  
    }  
    
    //避免代码重复和编译时间问题,用non-const版本调用const版本  
    //在函数中必须先调用const版本,然后去除const特性  
    //在调用const版本时,必须将this指针转换为const,注意转换的是this指针,所以<>里面是const StrBlob* 是const的类的指针。  
    //调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,否则调用的是non-const版本,non-const调用non-const会引起无限递归。  
    //return时,const_cast抛出去除const特性
      
    std::string& StrBlob::back()  
    {  
        const auto &s = static_cast<const StrBlob*>(this)->back(); //<span style="color:#FF0000;">auto前面要加const,因为auto推倒不出来const。</span>  
        return const_cast<std::string&>(s);  
    }  
      
    const std::string& StrBlob::front()const  
    {  
        check(0, "front on empty StrBlob");  
        return data->front();  
    }  
      
    std::string& StrBlob::front()  
    {  
        const auto &s = static_cast<const StrBlob*>(this)->front();  
        return const_cast<std::string&>(s);  
    }  
      
    void StrBlob::pop_back()  
    {  
        check(0, "pop_back on empty StrBlob");  
        data->pop_back();  
    }  
      
    int main()  
    {  
        std::shared_ptr<StrBlob>sp;  
        StrBlob s({"wang","wei","hao"});  
        StrBlob s2(s);//共享s内的数据  
        std::string st = "asd";  
        s2.push_back(st);  
        //s2.front();  
        std::cout << s2.front() << std::endl;  
        std::cout << s2.back() << std::endl;  
    }  

    还可以使用new返回指针来初始化智能指针,此时智能指针的构造函数是explicit。

    shared_ptr<int>p1(new int(10));//正确:使用new初始化智能指针
    shared_ptr<int>p2 = new int(10);//错误:必须显示的初始化

    但是注意不要将普通的指针和智能指针混合使用,很容易出错。

    get()操作

    get可以讲指针的权限给代码,但是注意,必须保证代码不会delete指针,才能使用get。永远不要用get去初始化另一个智能指针或给它赋值。

    reset()操作

    reset会更新引用计数,所以在多个shared_ptr共享对象时,常与unique()一起使用。

    异常

    使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放;但是普通指针就不会。

    void fun( )  
    {  
            int *p = new int(42);  
            //如果这时抛出一个异常且未捕获,内粗不会被释放,但是智能指针就可以释放。  
            delete p;  
    }  

    标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源,可以使用智能指针来解决这个问题。

    /*有问题*/
    connection connect(*destination);  
    void disconnect(connect);  
    void f(destination &d)  
    {  
            connection c  = connect(&d);  
            disconnect(d);//如果没有调用disconnect,那么永远不会断开连接。  
    }
    //使用智能指针优化,等于自己定义了delete代替本身的delete  
    connection connect(*destination);  
    void end_disconnect(connection*p) {disconnect(p);}   
    void f(destination &d)  
    {  
            connection c = connect(&d);  
            shared_ptr<connection>p(&d, end_connect);//定义自己的删除器
            //f退出时,会自动调用end_connect。  
    }

    总结:

    智能指针陷阱

    1. 不使用相同的内置指针值初始化(或reset)多个智能指针;//多个智能指针还是单独的指向内置指针的内存,use_count分别为1
    2. 不delete get( )返回的指针;//两次delete释放,智能指针内部也会delete
    3. 不使用get( )初始化或reset另一个智能指针;//free( ): invalid pointer:也是多次释放
    4. 如果你使用get( )返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
    5. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(删除函数向上面的disconnect( ))。

    二、unique_ptr类

    由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值。

    但是,可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。                

    操作 描述

    unique_ptr<T> u1 

    unique_ptr<T,p> u2

    空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针
    unique_ptr<T,p> u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
    u = nullptr 释放u所指向的对象,将u置为空
    u.release() u放弃对指针的控制权,返回指针,并将u置为空
    u.reset() 释放u指向的对象

    u.reset(q)

    u.reset(nullptr)

    如果提供了内置指针q,令u指向这个对象;否则将u置为空

    但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。

    unique_ptr<int> clone(int p){
        return unique_ptr<int>(new int(p));//返回一个unique_ptr
    }

    在早的版本中提供了auto_ptr的类,它有unique_ptr 的部分特性,但是不能在容器中保存auto_ptr, 也不能在函数中返回 auto_ptr, 编写程序时应该使用unique_ptr。

    向unique_ptr 传递删除器

    #include <memory>  
    #include <iostream>  
      
    using namespace std;  
      
    typedef int connection;  
      
    connection* connect(connection *d)  
    {  
        cout << "正在连接..." << endl;  
        d = new connection(40);  
        return d;  
    }  
      
    void disconnect(connection *p)  
    {  
        cout << "断开连接..." << endl;  
    }  
      
    int main()  
    {  
        connection *p,*p2;  
        p2 = connect(p);  
        cout << p << endl;   
        cout << *p2 << endl;  
        unique_ptr<connection, decltype(disconnect)*>q(p2,disconnect);  
        //在尖括号中提供类型,圆括号内提供尖括号中的类型的对象。  
        //使用decltype()关键字返回一个函数类型,所以必须添加一个*号来指出我们使用的是一个指针  
    }  

    三、weak_ptr

    weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数,且最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。

    weak_ptr操作

    操作 描述
    weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
    weak_ptr<T> w(sp) 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型。
    w = p p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
    w.reset() 将w置为空
    w.use_count() 与w共享对象的shared_ptr的数量
    w.expired() 若w.use_count()为0,返回true,否则返回false
    w.lock() 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

    当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。weak_ptr 不会更改shared_ptr 的引用计数。

    不能直接使用weak_ptr访问对象,而必须调用lock();引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉。
    std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
    std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
    此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。

    四、动态数组

    动态数组并不是数组类型,而是得到一个数组元素类型的指针。

    大多数应用应该使用标准库容器而不是动态分配的数组,因为使用容器更简单、更安全。

    int *p = new int[get_siae()];//方括号中的大小必须是整数,但不必是常量
    int *p = new int[]{0,1,2,3,4,5,6,7,8,9};//C++11中可以使用列表初始化
    string *sp = new string[10]{"ad","fd","xbv"};//初始化器中的元素较少时,剩下的进行值初始化;初始化器中的元素过多时,new失败,不会分配任何内存。

    不能用auto分配数组。

    动态分配一个空数组是合法的,但是不能解引用。

    指向数组的unique_ptr

    指向数组的unique_ptr不支持成员访问运算符

    操作 描述
    unique_ptr<T[]> u u可以指向一个动态分配的数组,数组元素类型为T
    unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
    u[i] 返回u拥有的数组中位置i处的对象,u必须指向一个数组

    shared_ptr管理动态数组要定义自己的删除器,而且访问元素的时候要是用get()获取内置指针。

    shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});
    sp.reset();//使用上面的lambda表达式释放数组
    
    for(size_t i = 0;i != 10;++i)
        *(sp.get() + i) = i;

    五、allocator类

    new它将内存分配和对象构造结合到了一起,而allocator类允许我们将分配和初始化分离。

    标准库allocator类定义在头文件 <memory>中。它帮助我们将内存分配和构造分离开来,它分配的内存是原始的、未构造的。

    类似vector,allocator也是一个模板类,我们在定义一个allocator类类型的时候需要制定它要分配内存的类型,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

    allocator<string> alloc;
    auto const p = alloc.allocate(n); // 分配n个未初始化的string

    allocator类的操作:

    allocator类及其算法
    allocator<T> a 定义了一个名为a的allocator对象,可以为类型为T的对象分配内存
    a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象
    a.deallocate(p, n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate成员函数返回的指针,且n必须是创建时候的大小,在destroy之前,用户必须在这块内存上调用destroy函数
    a.construct(p, args) p必须是一个类型为T*的指针,只想一块原始内存,args被传递给类型为T的构造函数
    a.destroy(p) p为T*类型的指针,此算法对p执行析构函数

     allocator分配的内存是未构造的,construct成员函数接受一个指针和零个或多个额外参数,在给定的位置构造一个元素,额外的参数用来初始化对象。这些额外参数必须和类型相匹配的合法的初始化器。 为了使用allocate返回的内存,我们必须用construct构造对象。

    auto q = p;
    alloc.construct(q++); // *q为空字符串
    alloc.construct(q++, 10'c'); // *q为cccccccccc
    alloc.construct(q++, "hi"); // *q为hi

    还未构造对象的情况下或者是使用原始内存是错误的:

    cout << *p << endl; // 正确,使用string的输出运算符
    cout << *q << endl; // 错误,q指向未构造的内存

    在这些对象使用结束后,我们使用destroy来销毁这些元素:

    while (q != p)
        alloc.destroy(--q);

    元素被销毁后,如果需要将内存归还给系统,就需要调用deallocate函数:

    alloc.deallocate(p, n);

    传递给deallocate的指针不能为空,必须指向由allocate分配的内存,而且,n必须为allocate分配时的大小。

    标准库为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象:
    allocator的算法
    uninitialized_copy(b, e, b2) 从迭代器b和3
    指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中,b2指向的内存必须足够大,能容下输入序列中的元素的拷贝
    uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的原始内存中
    uninitialized_fill(b, e, t) 在迭代器b和e指定的原始内存范围中创建对象,值均为t的拷贝
    uninitialized_fill_n(b, n, t) 从迭代器b指向的原始内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能容纳给定数量的对象

    vector<int> vec{0, 1, 2, 3, 4, 5};
    auto p = alloc.allocate(vec.size() * 2);
    auto q = uninitialized_copy(vec.begin(), vec.end(), p);
    uninitialize_fill_n(q, vec.size(), 42);

    uninitialized_copy在给定位置构造元素,函数返回递增后的目的位置迭代器。因此,一个uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。

  • 相关阅读:
    C. Shaass and Lights 解析(思維、組合)
    D. Binary String To Subsequences(队列)(贪心)
    CodeForces 1384B2. Koa and the Beach (Hard Version)(贪心)
    CodeForces 1384B1. Koa and the Beach (Easy Version)(搜索)
    CodeForces 1384C. String Transformation 1(贪心)(并查集)
    CodeForces 1384A. Common Prefixes
    POJ-2516 Minimum Cost(最小费用最大流)
    POJ3261-Milk Patterns(后缀数组)
    HDU-1300 Pearls(斜率DP)
    HDU-4528 小明系列故事-捉迷藏(BFS)
  • 原文地址:https://www.cnblogs.com/yeqluofwupheng/p/6876100.html
Copyright © 2011-2022 走看看