文章目录
1.P47 动态数组 std::vector
1.1.STL标准模板库
标准模板库本质上是一个库,里面装满了容器,这些容器包含特定的数据。之所以被称为标准模板库,因为它可以模板化任何东西。这意味着容器的底层数据类型(容器包含的数据类型)由我们自己决定,所有东西由模板组成,模板可以处理我们提供的底层数据类型,意味着不需要编写自己的数据结构或类似的东西。
1.2.std::vector
C++提供给我们一个叫做Vector
的类,这个Vector
在std命名空间中,它应该被称为ArrayList
,本质上是一个动态数组(不是向量)。在创建动态数组时(Vector),它没有固定大小(可以给一个特定大小来初始化)。创建Vector后每次往里面添加一个元素,Vector的数组大小会增长。当添加的元素超过Vector数组的大小时,它会在内存中创建一个比第一个大的新数组,把所有东西都复制到这里,然后删除旧的那个。
1.2.1.数组的存储
如下面的代码所示,在std::vector
中存储了自定义的Vertex
对象,注意这里直接存储的对象,而不是指向对象的指针。二者之间的区别如下:
-
存储对象:
优点:存储对象则它的内存分配将是一条线上的,而动态数组是内存连续的数组,这意味着它在内存中不是碎片,内容都在一条高速缓存线上。这样读取、修改这些对象速度都会非常快。
缺点:当数组大小发生变化的时候需要对数组内的所有数组进行复制,从而创建一个新的数组。此时速度会比较慢。 -
存储指针:
优点:存储的仅仅是一个指针,也就是整型数据。当数组发生拷贝的时候运算量小。
缺点:并非直接存储数据,而是存储的指针,访问真正的数据的时候需要通过指针间接访问。因此对真正的数据的批量访问性能不高,因为这些数据可能是在内存中杂乱的存储的,而不是在内存的一条线上存储。
总结何时存储对象、何时存储指针:一般情况下尽量存储对象,因为访问速度快。如果数组经常被复制,那么就存储指针。
#include<iostream>
#include<string>
#include<vector>
struct Vertex
{
float x, y, z;
};
//输出运算符的重载
std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
stream << vertex.x << "," << vertex.y << "," << vertex.z;
return stream;
}
int main()
{
std::vector<Vertex> vertices;
vertices.push_back({ 1,2,3 });
vertices.push_back({ 4,5,6 });
std::cin.get();
}
1.2.2.数组的访问
- 基于C语言格式访问数组:因为vector是一个类,里面有size方法,知道其大小。而原生的数组比如int [5]这种,访问其大小可能就不准确(若数组存储的是指针,那么计算数组大小的时候可能出错)
- C++格式的基于范围的for循环:注意下面的代码可以写成
for(Vertex v : vertices)
,但是这样写会将每个vertex复制到这个for范围循环中,从而造成性能浪费。所以写成for(Vertex& v : vertices)
引用的格式避免拷贝。
// 1.C语言格式访问整个数组
for (int i = 0; i < vertices.size(); i++)
{
std::cout << vertices[i] << std::endl;
}
// 1.2.基于范围的for循环
for (Vertex& v: vertices)
{
std::cout << v << std::endl;
}
1.2.3.数组作为参数要用引用进行传递
当我们将这些vector传递给函数或类或其他东西时间,要确保是通过引用传递它们的。若不会修改它们则使用const引用,这样可以确保没有把整个数组复制到这个函数中。
void Function(const std::vector<Vertex> & vertices)
{
}
2.P48 动态数组 std::vector 的使用优化
2.1.push_back中的复制现象
std::vector是这样工作的:创建一个vector然后开始push_back元素,也就是向数组中添加元素。如果vector的容量不够大,不能容纳我们想要的新元素,vector需要分配新的内存,当前vector的内容从内存中的旧位置复制到内存中的新位置,然后删除旧位置的内存。
这就是将代码拖慢的原因,因为我们需要不断地重新分配并复制所有现有的元素。所以我们的优化思路就是如何避免复制对象,如果我们处理的是基于vector的对象而没有存储vector指针。
2.1.1.代码分析
使用以下代码查看push_back的时候进行了几次复制。注意代码中的几个语法细节:
- 为了查看对对象的复制次数,需要在类中声明一个拷贝构造函数打印相关内容。
- 主函数中
vertices.push_back({1, 2, 3})
这句话等价于vertices.push_back(Vertex(1,2,3))
,也就是{1, 2, 3}
是类的形参列表,这种写法相当于进行了一次隐式转换,实际效果和传入Vertex(1,2,3)
是一样的。实际上为了让代码的可读性更强,更推荐写成vertices.push_back(Vertex(1,2,3))
的形式。
2.1.2.结果分析
根据上图可以发现,两句vectices.push_back({1, 2, 3})
复制了3次Vectex
对象。
再push_back
一次看结果,同时为了增强代码的可读性将代码改成如下形式,也就是显式地构造对象。
int main()
{
std::vector<Vertex> vertices;
vertices.push_back(Vertex(1,2,3)); // 显式地构造对象
vertices.push_back(Vertex(4,5,6));
vertices.push_back(Vertex(7,8,9));
std::cin.get();
}
运行代码会发现复制了6次:
原因:
- 传入
push_back
函数的形参Vertex(1, 2, 3)
是在main
函数的堆栈上被创建的,它自己有一个内存。而std::vecotr<Vertex> vertices
这个数组也有自己的一个内存。所以当把形参Vertex(1, 2, 3)
给push_back
进数组的时候,就需要把它从main的栈上放到vector分配的内存中,从而发生了一次拷贝。因此运行完第23行会输出一个Copied!
- 由于数组定义了之后并没有用
reverse
声明它预先要保留大小,因此默认他是没有内存的。当运行完第23行之后,数组大小是变成1。当运行到第24行的时候,此时需要在加入一个对象,原数组大小是1不够,因此需要重新分配内存大小为2,这里就会把23行存储的数组里面的对象复制出来,打印一个Copied
。然后再加入新的对象,同23行的原理一样,又会打印一个Copied
。因此第24行打印两个Copied
。 - 运行第25行的时候原理同上,要从原来的数组中拷贝两个,输出两个
Copied
;然后新加入一个对象,输出一个Copied
。最后第25行一共输出三个Copied
。
2.2.优化一:reserve声明数组大小避免数组扩容产生拷贝
我们可直接告诉vector需要三个对象的内存,设置vertices.reserve(3);,这里的3就是要设置的容量,运行代码发现复制了3次,这三次都是因为把对象从main的栈上放到vector分配的内存中,从而发生的拷贝。
2.3.优化二:用emplace_back直接在数组内存上构造对象
因为vertex
实际上是在main
函数中构造的,然后复制到实际的Vector
中。若想在实际的vector
中构造,可以使用emplace_back
代替push_back
。
注意:emplace_back
不是传递我们已经构造的vertex
对象,只是传递了构造函数的参数列表。emplace_back
接收一个参数列表,它告诉c++在我们实际的vector内存中,使用以下参数构造一个vertex对象,因此写法是emplace_back(1, 2, 3)
,而不是emplace_back({1, 2, 3)}
或者emplace_back(Vertex(1, 2, 3))
。emplace_back
是直接在容器尾部创建这个元素从而省去了拷贝或移动元素的过程。
运行代码发现没有复制一次。