c++容器入门分析(上)

来源:互联网 发布:ubuntu中如何安装vim 编辑:程序博客网 时间:2024/06/04 21:16

        最近搞了一段时间的容器,是时候总结一下了。以前java也研究过一段时间的容器,和c++的容器大同小异。容器在我们的运用中有着不可取代的作用。java转c++还真不容易,相比之下,c++更偏底层一些,而且很多东西java已经封装好了,只要直接调用,而c++就要自己动手干。

        首先,先理解一下什么是容器,这个定义是在别人博客看见的,这里copy一下。在c++中容器被定义为:在数据存储上,有一种对象类型,它可以持有其他对象或者指向其他对象的指针,这种对象类型就叫做容器。

        怎么样,是不是乍看之下不知所谓,所以说专家就是一天吃饱饭了没事做,专门研究这些概念,定义一些很文雅的东西,显得很厉害的样子。就像中学做的语文题,划线的这句话表达作者的什么意思,难道作者写这句话的时候真是这么想的?据说语文教材有几处错误,人民教育出版社都被告上法庭了,我只能呵呵,因为我的语文成绩一直垫底,一直抓不住作者的心态,高考勉强及格,啊多么痛的领悟。说通俗点不是更好么,在我的理解,容器就是存储同一类对象的集合,当然容器也是一种对象。就跟生活中的杯子一样,它就是一个容器,它可以存储水也可以是别的,当然也可以在大杯子中放一个小杯子,小杯子里再存储水或别的。这个意思就是容器里也可以存储容器。如果连通俗的都搞不清楚,还搞毛文雅的。文雅是衣食无忧的人追求的东西,我们连饭都吃不饱哪有精力追求文雅。

        但是这里要强调一下,容器中存储的只是对象的副本,也就是对象的值,而不是对象本身,当改变容器中的元素时是不会改变原来的对象,这里看下例子吧。

内置对象:

vector<int> ve;int a = 10, b = 20;//添加两个元素ve.push_back(a);ve.push_back(b);//修改第一个元素ve[0] = 100;cout << ve[0] << " " << ve[1] << " " << a << " " << b << endl;
打印结果为:100 20 10 20

        可以看出原来a还是没变。

自定义对象:

//Student是自定义类,两个属性id和namevector<Student> ve;Student stu(1, "aaa");ve.push_back(stu);cout << "改变前:" << endl;cout << "集合中的对象id:" << ve[0].id << ",name:" << ve[0].name << ",原来对象id:" << stu.id << ",name:" << stu.name << endl;//修改容器中对象的属性ve[0].id = 100;ve[0].name = "000";cout << "改变后:" << endl;cout << "集合中的对象id:" << ve[0].id << ",name:" << ve[0].name << ",原来对象id:" << stu.id << ",name:" << stu.name << endl;
打印结果为:改变前:
                        集合中的对象id:1,name:aaa,原来对象id:1,name:aaa
                        改变后:
                        集合中的对象id:100,name:000,原来对象id:1,name:aaa

        可以看出来原来对象stu还是没有变。

        这里又想到了函数调用传参的问题,相信刚接触的时候都会有这样的问题,以后再总结。

容器分为顺序性容器,关联式容器和适配器容器。

        标准库提供了3种顺序性容器:vector,list和deque。顺序性容器根据位置来存储和访问元素,而在容器中元素的排列次序是和元素的值无关的,只和添加到容器的次序有关。

        vector其实就是一种动态的数组,它可以不给定大小,在添加元素的时候,如果容量不足,就会自动增加容量,然后添加元素,是不是棒。其实则不然,我们用数组,除了来存储数据之外,最大的用处就是用来访问,而且是可以随机访问,这是数组最大的优势,而vector则扩展了数组的功能,不仅可以随机访问,而且可以动态的添加元素,这是一把双刃剑,优点就是可以通过下标随机访问,像数组一样,而且不需要知道大小,有多少存多少,缺点就是这样的开销是很大的。因为创建vector的时候,会分配一块连续的内存空间,当再添加元素的时候,会重新分配更大的连续内存空间,把原来的数据拷贝过来,添加新元素,然后将原来的内存释放掉,这样一个过程将会严重影响到vector的性能。而且当添加的元素越多,系统自动分配的内存空间越大。这里我写了一个小程序测试的。

vector<int> ve;int ca = ve.capacity();cout << "开始,集合size:" << ve.size() << ",集合capacity:" << ca << endl;for(int i = 0;i!=100;++i){ve.push_back(i);if(ve.capacity() != ca){ca = ve.capacity();cout << "分配大小,集合size:" << ve.size() << ",集合capacity:" << ca << endl;}}cout << "结束,集合size:" << ve.size() << ",集合capacity:" << ca << endl;

打印结果:开始,集合size:0,集合capacity:0
                    分配大小,集合size:1,集合capacity:1
                    分配大小,集合size:2,集合capacity:2
                    分配大小,集合size:3,集合capacity:4
                    分配大小,集合size:5,集合capacity:8
                    分配大小,集合size:9,集合capacity:16
                    分配大小,集合size:17,集合capacity:32
                    分配大小,集合size:33,集合capacity:64
                    分配大小,集合size:65,集合capacity:128
                    结束,集合size:100,集合capacity:128

        capacity表示的是可用空间,而size是实际已用空间,其实vector就是这样,当空间不足的时候,重新分配其实是分配的capacity,新元素就添加到没有用到的部分。

            

        这里我给vector添加了100个元素,每次添加一个,从程序的运行结果可用看出,刚开始size和capacity都为0,因为我定义了一个空的vector,当添加第一个元素的时候,系统分配了空间大小为1(其实是4个字节,这里只是为了表示)的内存,当添加第二个元素的时候,分配了大小为2的内存,都还只增多1,当添加第三个元素的时候,分配了大小为4的内存,这里增多了2,所以当添加第四个元素的时候就不用在重新分配内存空间了,往后是不是看出来,当添加元素越多的时候,重新分配的内存空间是成2的指数增长。如果这个vector只要65个元素的时候,系统已经分配了大小为128的内存空间,那么剩下63大小内存就浪费了,而且这样的每次分配是需要很大的开销。所以这种方式还是很不提倡的。虽然说提供了reserve(int)方法,让系统每次为vector增加固定大小的内存空间,但是频繁的重新开辟新的内存空间和销毁原来的内存空间是很耗性能的。在插入元素方面,如果在上图一a和b之间插入一个元素x,就会变成图二。而且在删除元素方面,如果把上图中的元素b删除后,就会变成图三。你会发现,添加x的时候,后面的b,c和d都会往后移一个位置,删除b的时候,后面的c和d都会往前移一个位置。这就是vector的劣势。

        list是一个链表,是链式结构,是由很多个节点构成的,每个节点都有一个数据块,一个前驱指针指向前一个节点,一个后驱指针指向后一个节点,但是头节点没有前驱指针,尾节点没有后驱指针。它不用分配指定大小的内存空间,也不是连续的内存空间。当在两个节点之间添加一个节点的时候,只要将前面节点的后驱指针指向该节点,将该节点的前驱指针指向前节点,然后将该节点的后驱指针指向后节点,将后节点的前驱指针指向该节点就可以了,不用重新分配新的内存空间,而删除节点,也是同样的原理,因此在添加元素和删除元素方面是它的强项,但是在访问上面,就不能向数组一样访问,必须要从头节点开始访问,然后根据后驱指针一个一个的访问,这也就是list的致命弱点。下图一为一个list,增加节点为图二,删除节点为图三。

            

        list的这种访问方式就需要迭代器了iterator了,当然vector也可以用迭代器来访问。迭代器也就是一种特殊的指针,list<int>::iterator it = li.begin(),要指向下一个元素++it,如果当访问当某一元素时,然后把该元素删掉,那么我们就要把it稍稍变化一下,不然就会找不着北,因为机器是死的,你给什么它就执行什么。下面看个测试:

list<int> li;li.push_back(1);li.push_back(2);li.push_back(3);li.push_back(3);li.push_back(4);cout << "删除前:" << endl;for(list<int>::iterator it = li.begin();it!=li.end();++it)cout << *it << "  ";cout << endl;for(list<int>::iterator it = li.begin();it!=li.end();++it)if(*it == 3)li.erase(it);cout << "删除后:" << endl;for(list<int>::iterator it = li.begin();it!=li.end();++it)cout << *it << "  ";
        给list添加5个元素,然后遍历,当值为3的时候,就删除这个元素,这个程序看上什么问题都没有,编译也能通过,但是运行就会报错,报错就出现在删除这行代码,为什么会这样呢?继续看上图三删除节点的例子,当第二个节点被删除之后,迭代器还是指向这个位置,但是这里的元素已经被删除,内存已经被释放,所以该指针指向的不是一个有效的内存单元,而变成了野指针,当然就会报错。但是相同的代码出现在vector中为什么就不会报错呢?因为指针还是指向这个位置,但是后面的元素会集体往前移动,所以该指针实际上指向的是下一个元素。所以这行可以改成it = li.erase(it),因为erase()返回的是下一个元素的迭代器。这样是不是就可以了呢?其实不行,当这行执行完之后,又会执行++it,这样就又指向了后面一个元素,那么就漏掉了一个元素,而且如果后面没有元素了就又会报错。所以这里还得改进一下,将++it放在for循环里面条件执行。这样的问题我在java的容器中遇到过。下面为改进的代码:

list<int> li;li.push_back(1);li.push_back(2);li.push_back(3);li.push_back(3);li.push_back(4);cout << "删除前:" << endl;for(list<int>::iterator it = li.begin();it!=li.end();++it)cout << *it << "  ";cout << endl;for(list<int>::iterator it = li.begin();it!=li.end();)if(*it == 3)it = li.erase(it);else++it;cout << "删除后:" << endl;for(list<int>::iterator it = li.begin();it!=li.end();++it)cout << *it << "  ";

        vector在随机访问上是个好手,但是在添加和删除元素上是个手残,而list在添加和删除元素上是个好手,但是在访问上是个手残。俗话说鱼与熊掌不可兼得,二者只能得其一,但是如果来个杂交品种不是更好,集成二者的优点,就想我们吃的大米一样,杂交水稻。这里就介绍deque,deque占用的不是一块内存空间,而是几块连续的内存空间,它集成了vector的访问优点,虽然有所逊色,而且集成了list的增删优点,虽然有所逊色,但是这也给我们提供了多一种选择。看下图一,该deque是由4个连续的内存块组成的,所以在访问上是和vector差不多,也是根据下标快速的随机访问,每个连续块就是一个vector。在两头增加和删除也是和vector差不多,但是当空间不足的时候,要重新分配新的内存空间,不再像vector一样将原来所有的元素拷贝,而是像list一样,重新开辟一块新的内存空间来存放增加的元素。但是在中间增删就会像vector一样,会整体移动,而且剩下空出来的内存就会立即释放掉,但是也是会有预留空间的,在a和b之间增加元素x为图二,删除e节点为图3.所以deque集成的优点不算太少也不算太多,在实际应用开发中还得靠自己权衡选择。

            

        这里有一篇关于deque分析写的很好的文章:http://www.360doc.com/content/12/1031/22/11020030_244982641.shtml

        以上为顺序性容器总结,关联式容器和适配器容器后面总结。

0 0
原创粉丝点击