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 动态分配内存
    数据查询语言 DQL
    数据操纵语言 ,DML, 增删改
    Convert Sorted List to Binary Search Tree
    Longest Consecutive Sequence
    Binary Tree Postorder Traversal
    Triangle
    4Sum
    3Sum Closest
  • 原文地址:https://www.cnblogs.com/yeqluofwupheng/p/6876100.html
Copyright © 2011-2022 走看看