【C++】静态数组array、动态数组vector创建及优化

十二、C++中的array静态数组、vector动态数组的创建和优化

从现在开始我们终于开始讲标准库的类型了。这些也是C++的基础。本篇讲数组,重点讲C++中的静态数组array和动态数组vector。

从C++的数据类型角度来看,常量、变量是最底层的内置(built in)数据类型;指针和数组已经是复合类型(compound type)了。本篇这里讲的array数组和vector数组就已经是C++标准库的类型了。

C++标准库中有很多不同功能的库文件,比如支持输入输出相关的库文件iostream、支持字符串处理的string文件等。本篇我们讲的是数组,C++标准库中和数组相关的库,我们一般也叫标准模板库,因为是和容器相关的文件,就是里面装的都是容器,容器类型(container type)。这些容器包含特定的数据,所以被称为标准模板库,因为它可以模板化任何东西。整个数组库模板化意味着容器是它的底层数据类型。也就是说,容器包含的数据类型,实际上是由你决定,所有东西由模板(template)组成。如果你还不了解模板,那也不妨碍你先学习如何使用标准模板库,你只要知道模板可以处理你提供的底层数据类型。也就是你不需要编写自己的数据结构。

此后我们会重写很多C++中存在的数据结构,会对它们进行优化,使其比标准模板库中的快很多,因为标准模板库优先考虑的不是速度。也所以我们在实际工作中,一般是要创建自己的容器库,一般都比标准模板库快得多。

(一)单纯从数组的角度看,C++中的数组有4种实现方式
第一种是兼容C数组的 类型 数组名[元素个数] 。这种数组最原始、最接近底层,所以效率最高,但不友好。在 【C++】深度理解C++数据类型:常量、变量、数组、字符串、指针、函数_c++ 字符串常量-CSDN博客 里面有对这种数组及其各种变异的详细讲解,这种数组非常灵活多变,还没有越界检查,极其考验程序员的功底。
第二种是静态数组array数组。array数组是对第一种数组再次包装的产物,所以虽然会增加一点点开销,但要友好很多。是本篇(三)中要讲的内容。
第三种也是静态数组,C++标准库提供的string支持的字符串数组,当然这种数组只支持字符串。
第四种是动态数组vector数组。vector数组就是最高层的解决方案了。也是本篇(四)中讲的内容。

(二)什么是静态数组、什么是动态数组
静态数组和动态数组都是数组。之所以有静态和动态之分的根本区别就是,静态的数组是一旦创建,其长度就同时确定下来了,数组的长度就不能改变了;而动态数组的长度则是随着元素的输入是可以动态增加的。
上图的两种数组,都是字符串数组,同时也都是静态数组。下面再举个非字符串数组的例子:

上图例子中的两个数组,一个是b数组,是创建在栈内存上的静态数组;一个是bs数组,是创建在堆内存上的静态数组。
上面例子中的这些创建数组的做法都是,你需要提前明确告诉程序你要创建多长的数组,数组存放的内存大小一旦定下就不能改变了。
而现实情况是,我们也不知道用户要输入多少数据,只能边输入边看了。此时我们就需要更高层的解决方案————动态数组了。关于动态数组的创建和优化(四)中再详解。
可见静态数组和动态数组的使用场景是不同的。你需要根据你的需求来选择使用什么样数组。

(三)array静态数组
本部分讨论C++中的标准数组类array,也就是C++标准模板库中的array类(std::array)。array是一个类,是由C++标准库提供的,用来处理静态数组。
静态数组是指不增长的数组。或者说创建数组时就需要确定下来数组的长度、以及数组中元素的类型。所以当你创建这个数组时,你要定义它有多大,也就是有多少个元素、以及里面有什么类型的元素。你不能改变它的大小,不能将它变大变小。一旦创建,它的大小就是一个常数。

1、创建array数组

array数组的创建就非常简单,使用array模板,一般需要2个参数,一个是数组中的元素的类型,一个是数组的长度。

2、安全检查

在使用下标操作的时候,array数组会进行下标越界的检查,而传统的数组不会。其实更加确切的说,应该是在debug模式下,会进行安全检查。在release模式下,会追求更高的效率,会忽略。所以在调试模式下,越界就会造成你的程序崩溃。

这是array标准库中重载的[]函数的函数体,当对应的宏满足条件的时候,或执行一个宏函数,进行越界检查。否则,不进行越界检查,直接返回数组。

3、使用效率
相对于原来C风格数组,效率是差不多的。因为array底层的数据是存储在栈上面的(vector底层是在堆上面存储的),所以其效率是很快的。
其次,array数组的越界检查也只是在debug模式下才会检查,如何在优化模式下,这些额外的代码也不会运行,这就和C风格数组一样了。
当然,c++11的array是一种选择,你可以继续使用原来的数组,这样你需要自己计算长度或者确保安全性,array的安全性高,因为它要进行相应的检测,所以其性能要比普通的数组性能低。(但是并没有低太多) -- 所以,应该使用array,因为它会更加的安全。

4、比普通数组传参更方便一些

传参时传入一个数组的引用即可,不需要再传长度,因为内部可以调用.size()获得长度。
这里想说的是,数组的长度其实是没有存储的,size是我们给模板的一个参数,也就是size函数就直接是返回这个参数值,size这个对象本身是没有存储的,所以是没有性能成本的,还帮我们记住了数组的长度。

5、成员函数

(四)vector动态数组
本部分讨论C++中的动态数组,就是标准库的vector类(std::vector)。

1、如何实现数组的动态化
C++给我们提供一个叫做Vector的类,这个Vector在std命名空间中。Vector不是向量的意思,更为准确的说是ArrayList,因为它的本质是一个动态数组。但与我们之前讲的数组类型不同的是,它可以调整数组的大小。意思就是当你创建一个vector时,它是没有固定大小的(当然你也可以初始化一个特定的大小,但一般我们不这样做),我们也不需要care它的大小,只要把元素放进去就行。每次你放入一个元素,数组的大小就会增长。就是当你写入的内容超过了分配的大小,它会在内存中创建一个比第一个大的新数组,然后把所有东西都复制到新数组,并删除旧数组,这样就实现了动态。其实就是不断地分配一个个静态数组,以达到动态的效果。

2、动态数组的基本用法

(1)手写要包含vector头文件。

(2)创建动态数组
A处:<>里面写的是数组中的元素的类型,这里我们都放Book类类型。后面跟的bs就是你要创建的动态数组的名字,这里我取名bs。
在C++模板中,<>里面也可以传递原始类型,比如vector v1;或者vector v2;等等。

普通数组的底层都是存储的是一堆指针,比如上上图可以说就是一个二维数组了,那底层也是一个二维指针,有一步跨一个元素的,有一步走一个float的。
但是动态数组底层没有bs指针。实际上只是把bs存储在一条直线(一段内存)上, in line,这是普通数组和动态数组之间最大的区别。

有人会问,是否应该把指向堆分配的类对象的指针,存储在bs中?或者是否应该存储栈分配、一条线上分配的Book类或者结构体?答案是视情况而定。因为很难决定你是否应该使用Book指针还是Book对象。但是你要知道,存储Book对象比存储指针在技术上更优。因为如果是Book对象,你的内存分配将是一条线上的。
动态数组是内存连续的数组,也就是它在内存中不是碎片,而是在一条线上。如果你像这样将Book对象存储在一条直线上,xyzxyzxyz....,这种一个接一个的,这就是最优的。因为如果你要遍历、修改等操作,它们都在同一条高速缓存线上。所以如果可以的话,我们尽量要像这样(一条线上)分配。唯一的问题是,如果要调整bs的大小,就要复制所有的数据。如果你碰巧是一个字符串的Book,那你要调整bs,就要重新分配和复制所有的东西,这可能是一个非常缓慢的操作。
这里作者的原话是:moving instead of copying largerly solves this particular issue, but there is still some copying which is not ideal.
而指针就不同了,因为你的实际内存是保持不变的,你只要能正确的保存了指向内存的指针就可以了。到了调整大小的时候,只是调整下实际数据的内存地址,而实际数据仍然被存储在内存中的不同位置。
所以说这两种操作很难说哪种好,但是这里我想强调的是,尽量使用对象而不是指针,除非不得不选指针。

B处:给数组添加元素。使用.push_back(),也就是初始化每个Book类实例。

(3)遍历数组
遍历数组,上图的CDE三种方法都可以。
C处:因为vector是一个完整的类,所以我们是知道bs的大小的,用bs.size()即可。也所以我们也是可以使用索引[]操作符的。
D处:实际上是将bs中的每个Book实例都逐个复制到for循环中,然后打印出来。
E处:前面我们刚刚讲过,尽量不要copy,所以E处我们就是对bs中的每个Book类实例进行了引用。当然E处不要const也是可以的。

(4)动态数组被当作参数传递给一个函数
对复制 【C++】理解C++中的复制、复制构造函数-CSDN博客 这篇博文有感触的同学就一定会记得:一定一定要确保你是通过引用传递的。千万别把整个数组复制到函数中去。

(5)删除数组列表
F处:将数组大小设为0。
G处:erase会返回vector_iterator,返回的类型是Book。G处是erase的函数,可以看到参数是const iterator。所以erase的参数应该是个迭代器。
H处:如果我只想擦除第二个元素,也就是索引是1的元素,我就可以通过H代码实现。其中bs.begin()返回的是0。

3、vector使用优化
vector通常情况下是很慢的,所以我们需要想尽办法尽力榨出所有的性能。本部分讲如何优化,如何避免重新分配,如何避免复制等技巧。总之就是怎么以一种更优化的方式使用vector类,就是优化vector的使用。C++语言能很好的发挥优化的作用,但是前提是你要先清楚vector是如何工作的,你才能想办法改变它,使之更好的工作。

当我们创建一个vector,然后开始push_back元素,也就是向数组逐个添加元素。也就是我们现在存储的不是vector指针,存储的是vector对象。我们先看下面代码的运行结果:

上面代码都是非常普通的、写入数组元素的代码,但是不可思议的是,就仅仅写入了4个元素,就copy了1+2+3+4=10次之多!也销毁了同样多次!不断地复制再销毁这多浪费时间和性能啊!而且这个复制和销毁的次数是随着输入的元素增加而增加的,就是越往后存储越慢!下面我们分析一下为什么会出现这么多复制和销毁。

首先,我们从代码运行结果来看:
当代码运行到B处之前,也就是我们push_back第一个元素时,在push_back()之前,其实是先运行Book(1,2,3),就是先实例化一个x=1,y=2,z=3的Book类实例,所以就先调用了初始化构造函数a。而这个工作是在主函数main的当前栈帧中构造的,就是说是在main的栈上创建了Book(1,2,3)。然后push_back要做的就是把main函数中的Book(1,2,3)放到实际的vector分配的内存中,所以实例对象Book(1,2,3)被复制了一次,从main函数复制到vector类中。所以调用了一次复制构造函数b。当复制完毕,还得销毁main中的Book(1,2,3),所以又调用了一次析构函数e。
可见,如果我们直接构建(创建)Book实例对象,在vector分配的内存中,是不是就省去了一次复制和销毁?这是优化的一个点。下面我们看看优化结果:

我们只要把push_back替换成emplace_back即可,此时emplace_back方法传递给vector的就不是已经实例化好的Book(1,2,3)了,而是传递的只是Book构造函数的参数列表。那创建Book(1,2,3)的工作就可以在vector内存中构造了。也就是直接在vector内存中初始化三个float:1,2,3,分别表示x,y,z。就算是在vector内存区域存储了第一个元素了。
也所以上图我们每个元素都减少了一次复制和销毁。

但是,代码运行中依然还有复制和销毁,那我们再打断点看看问题出在哪里:

发现,我们的动态数组bs创建时,长度竟然是0!我们前面说过,如果当vector的容量不够时,vector就开始分配新的、更大的内存,将当前的vector的内容,从内存中的旧位置复制到内存中的新位置,然后删除旧位置的内存。这也正好和上上图的运行结果对上了!输入第一个元素的时候就是直接创建,但输入第二个元素时,除了创建第二个元素外,还得复制第一个元素。同理输入第三个元素时,又复制了第一、二个元素!就是我们每输入一个元素,就要重新找一块连续的内存把之前的所有元素都copy到新位置,还得再删除旧位置上的元素。就是出现了不断分配、复制的操作了。如何避免?下面是我们的优化代码:

上面代码仅仅是增加了一行bs.reserve();表示一开始就给我留多少个元素的内存空间,因为我大概还是知道我要输入多少东西的。这里就是设置大致的容量,以防止没有意义的拷贝。

此时我们的代码要比之前快很多,这就是优化的过程,也就是先找到代码拖慢的原因,了解解决的工具,就可以逐个解决或避免了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值