Cherno C++系列笔记15——P47~P48 动态数组std::vector、动态数组vector的使用优化

本文详细介绍了C++中std::vector动态数组的原理、存储方式及其优缺点,强调了在存储对象与指针间的权衡。同时,通过实例分析了push_back操作中的对象复制现象,并提出了两种优化策略:一是通过reserve预分配数组大小避免扩容拷贝,二是使用emplace_back直接在内存中构造对象,减少不必要的复制。文章内容涵盖了vector的基本操作、访问方式以及参数传递的最佳实践。
摘要由CSDN通过智能技术生成

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次:
在这里插入图片描述

原因

  1. 传入push_back函数的形参Vertex(1, 2, 3)是在main函数的堆栈上被创建的,它自己有一个内存。而std::vecotr<Vertex> vertices这个数组也有自己的一个内存。所以当把形参Vertex(1, 2, 3)push_back进数组的时候,就需要把它从main的栈上放到vector分配的内存中,从而发生了一次拷贝。因此运行完第23行会输出一个Copied!
  2. 由于数组定义了之后并没有用reverse声明它预先要保留大小,因此默认他是没有内存的。当运行完第23行之后,数组大小是变成1。当运行到第24行的时候,此时需要在加入一个对象,原数组大小是1不够,因此需要重新分配内存大小为2,这里就会把23行存储的数组里面的对象复制出来,打印一个Copied。然后再加入新的对象,同23行的原理一样,又会打印一个Copied。因此第24行打印两个Copied
  3. 运行第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是直接在容器尾部创建这个元素从而省去了拷贝或移动元素的过程。

运行代码发现没有复制一次。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值