【C++】动态内存管理(五)使用STL容器进行大量的动态内存管理

来源:互联网 发布:网络搞笑剧有哪些 编辑:程序博客网 时间:2024/05/29 12:56

相信我们平时使用C++的时候都会用到STL里面的很多容器,可以发现,对于这些容器里面的数据,当容器的生命周期到达结束的时候,里面的数据就会被很好地释放掉。
但是对于处于堆上和堆栈的不同数据,使用的方法也有所不同。
我们以以下的对象作为被操作者:

    class Simple    {    public:        string name;    private:        size_t value;    public:        Simple(const Simple& rhs) :value(rhs.value), name("none")        {            cout << "creat simple" << endl;        };        Simple() :value(), name("none")        {            cout << "creat simple" << endl;        };        explicit Simple(size_t v) :value(value), name("none")        {            cout << "creat simple" << endl;        };        void DoSomething()        {            cout << "hello" << name << endl;        }        ~Simple()        {            cout << "delete Simple " << name << endl;        }    };

然后运行如下代码,进行测试:

    void test()    {        vector<Simple>datas;        Simple s(1,"s1");        datas.push_back(s);    }

输出如下:

creat simplecreat simpledelete Simple s1delete Simple s1

可以看到,我们push_back()的对象被成功的析构了,而且从输出我们还可以得到一个事情,就是:vector采用的是一种拷贝构造的方式进行的push_back(),所以我们每传入一个对象的实例,vector都会执行一次新对象的内存安排和对象的构造。然后在vector到达生命周期的时候就会将其内部的对象全部析构然后释放内存空间。

刚才我们的测试是对在栈上的,有生命周期的对象。
对于new出来的对象,我们该如何进行处理呢?
首先,我们对new出来的对象进行一些测试:

    void test()    {        vector<Simple>datas;        for (int i = 0; i < 2; i++)        {            Simple* p = new Simple(i, "p");            datas.push_back(*p);            cout << "vector size : " << datas.size() << endl;            cout << "vector capacity : " << datas.capacity() << endl;        }    }

输出如下:

creat simple 0creat simple 0vector size : 1vector capacity : 1creat simple 1creat simple 1creat simple 0delete Simple p 0vector size : 2vector capacity : 2delete Simple p 0delete Simple p 1

我相信,很多人对这样的输出都会感到非常费解,为什么new出对象,然后添加了两次对象之后,根据输出,却执行了五次的构造函数和仅仅三次的析构函数呢?

原因如下:

  • vector使用的内存并不是一开始就是一块很大的内存,一开始如果没有指定,那么其可使用的内存只能存放一个对象。
  • 然后当第二个对象添加的时候,它就会开始寻找一块新的更大的连续内存,然后将刚才的对象移动过来,释放之前的内存空间,然后再添加。之后的内存方案也是如此,内存不足就再寻找新的连续内存空间。
  • vectorpush_back()并不是将对象移动到其内部的内存空间,而是通过拷贝构造的方式在其内存空间上执行placement new构造和你要添加的对象一样的对象副本,从此,源对象和要被添加的对象就没有关系了。
  • 所以,如果你用new构造的对象想要用vector管理的话,要不然就传入指针,要不然就在push_back()之后将之前的对象delete

那我们使用vectorpush_back()普通指针数据(指向真正的对象)怎么样?
在换到更大的内存空间的时候就不会有大量的析构和构造函数的调用也不会有大量的数据被迁移和复制,我们需要移动的只是保存这个对象的几个字节的指针。
听起来很美好,但是让我们测试一下:

    void test()    {        vector<Simple*>datas;        for (int i = 0; i < 2; i++)        {            Simple* p = new Simple(i, "p");            datas.push_back(p);        }    }

大家可以猜想一下输出是怎样的?对象是否被成功的析构了呢?
输出如下:

creat simple 0creat simple 1

可以看到,对象并没有被析构,当vector生命周期结束,被释放内存空间的只是存储对象内容地址的指针,其指向的对象并没有被析构。这当然是合乎情理的,但是难道我们要这样去处理吗:

void test()    {        vector<Simple*>datas;        for (int i = 0; i < 2; i++)        {            Simple* p = new Simple(i, "p");            datas.push_back(p);        }        ......        for (int i = 0; i < datas.size(); i++)        {            datas[i]->~Simple();        }    }

诚然,这非常的奇怪而且容易导致错误,而且这样也并不能体现容器的优越性,如何处理呢?

用智能指针


智能指针只是在普通指针外层进行了一定量的抽象,其大小和复杂度都和普通的指针是一样的,但是其在析构的时候却会完整的释放掉其指向的资源对象。
可以看出,直接在vector中存放智能指针好像很不错,内存泄漏和拷贝构造的耗时都解决了。
我们可以试着将new出来的对象放入智能指针,然后将这个智能指针push_back()vector
如下:

    void test()    {        vector<shared_ptr<Simple>>datas;        for (int i = 0; i < 5; i++)        {            shared_ptr<Simple> p(new Simple(i, "p"));            datas.push_back(p);        }    }

输出如下:

creat simple 0creat simple 1creat simple 2creat simple 3creat simple 4delete Simple p 0delete Simple p 1delete Simple p 2delete Simple p 3delete Simple p 4

可以看到所有的资源都被很好的装载和释放了。

注意,我们这里是不能使用auto_ptr智能指针的,因为auto_ptr是资源支配权转移的智能指针,而vector中的拷贝构造和移动实施的过于频繁,可能会导致资源所有权的遗失。新标准已经明确禁止在vector中存放auto_ptr

所以这里使用shared_ptr来存放和管理资源,shared_ptr在非环状数据结构中可以很好的发挥其特性,防止资源泄露,让资源都能得到很好的释放和管理。

所以在最后,做一下总结,说明为什么最好要在vector中放shared_ptr

  • 防止直接放对象时,push_back() 一个new出来的源对象之后,忘记去delete掉源对象导致内存泄漏
  • shared_ptr可以很好的保证你new出来的内容都能很好的被释放掉
  • shared_ptr采用引用计数的方式,就算被push_back()的源shared_ptr已经析构掉了,但是其保留的资源还存在于vector的内部shared_ptr指针上,不用担心资源被不小心释放
  • shared_ptr可以实现多态,而放普通对象不行,普通指针则不好管理内存的分配和释放
原创粉丝点击