- 动态数组,特别是讨论标准向量类
- 要习惯CPP的标准库(标准模版库)
- 标准模版库包含了各种容器、迭代器、算法、仿函数
- 容器的数据类型由程序员自己决定
- cpp提供了一个Vector的类,在std命名空间中,可以调整大小,也可以不指定初始大小
- 本系列会重写C++中的数据结构,优化后比标准模版库中的快很多
std::vector 的实际工作方式:
当超出实际的分配大小时。当创建一个向量,可能会先分配10个元素大小的空间,当超出这个大小时,会在内存中创建一个比原来更大的数组,并把所有内容复制过去,然后删除旧数组。这样就有了一个更大存储空间的新数组
46动态数组#
struct Test
{
};
struct Vertex
{
float x, y, z;
/*virtual*/ void test()
{
}
};
//main()中
Vertex v = { 1,2,3 };聚合初始化(Aggregate Initialization),Vertex 结构体是一个聚合类型(Aggregate Type)。在 C++ 中,如果一个类或结构体满足以下条件:
- 没有显式定义的构造函数。
- 成员变量是公有的(public)。
- 没有基类(没有继承)。
- 没有虚函数。
那么你可以使用大括号 {} 直接按顺序为成员变量赋值。
vector的基本使用#
#include <iostream>
#include <string>
#include <vector>
struct Test
{
};
struct Vertex
{
float x, y, z;
//有了其他的构造函数,如果需要用到无参构造函数,
//就必须手动写一个
Vertex()
{
}
//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
// 有构造函数),所以要使用显示构造函数
// 使 Vertex v = { 1,2,3 }; 编译通过
Vertex(float x, float y, float z)
{
this->x = x;
this->y = y;
this->z = z;
}
//参数必须是&,原因如下:
//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
//必须是const
Vertex(const Vertex& vertex)
{
std::cout << "copied constructor handled" << std::endl;
this->x = vertex.x;
this->y = vertex.y;
this->z = vertex.z;
}
/*virtual*/ void test()
{
}
~Vertex()
{
std::cout << "destructor handled" << std::endl;
}
};
std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
stream << vertex.x << "," << vertex.y << "," << vertex.z;
return stream;
}
int main()
{
Vertex vertices1[5];
Vertex* vertices2 = new Vertex[5];
std::vector<int> myarray;
//存储Vertex对象,则动态数组的内存是连续的,
//其中的Vertext对象会按行排列
//问题是:实际要调整向量大小时得复制所有(类)数据,但内存连续性
// 带来的读取速度提升通常远超扩容时的开销
std::vector<Vertex> vertices;
//存储Vertext指针,实际要调整向量大小是只是复制指针地址(一个数字)
//,即实际数据的内存地址
std::vector<Vertex*> vertices4;
std::cout << "---1---" << std::endl;
Vertex v = { 1,2,3 };
//这里演示vertices的使用
std::cout << "---2---" << std::endl;
vertices.push_back({ 1,2,3 });
std::cout << "---3---" << std::endl;
vertices.push_back({ 4,5,6 });
std::cout << "---4---" << std::endl;
for (int i = 0; i < vertices.size(); i++)
{
//c++对[]进行了重载
std::cout << vertices[i] << std::endl;
}
std::cout << "---5---" << std::endl;
//这里会把每个Vertex复制到v
for (Vertex v : vertices)
std::cout << v << std::endl;
std::cout << "---6---" << std::endl;
//避免复制
for (Vertex& v : vertices)
std::cout << v << std::endl;
//clear() 会移除容器中的所有元素,但通常不一定会立即释放底层内存(容量不变),它只是销毁现有的对象。
vertices.clear();
std::cin.get();
}解释一下vertices.push_back({1, 2, 3});#
当你执行 vertices.push_back({1, 2, 3}); 时,逻辑确实是:在 vector 内部的内存中,通过调用复制构造函数来构造一个属于 vector 自己的对象。
具体的“接力”过程如下:
- 第一棒:创建临时对象
编译器先在栈上创建一个临时对象(假设叫 temp)。此时会调用你的普通构造函数
Vertex(float x, float y, float z)。 - 第二棒:传参给 push_back
push_back 函数需要接收这个 temp。由于 temp 是个临时对象,它只能传递给带有 const 的引用
(const Vertex&)。 - 第三棒:在 vector 内部“克隆” 这是最关键的一步。vector 已经在堆(Heap)上为你找好了空位。它会拿着刚才传进来的 temp 作为参考,在那个空位上调用你的复制构造函数。
复制构造函数做的事:它把 temp.x 拷到新位子的 x,temp.y 拷到 y……
结果:此时 vector 内部就有了一个完整的 Vertex 对象。 4. 第四棒:临时对象自毁 这行代码结束,栈上的 temp 任务完成,被自动销毁。
对输出解释#
//输出为:
/*
---1---
---2---
copied constructor handled vertices.push_back({ 1,2,3 });
destructor handled push{1,2,3}时的那个临时对象被销毁了
---3---
copied constructor handled vertices.push_back({ 4,5,6 });导致vector内部数组扩容,1: 复制{1,2,3}到新扩容空间 2. 复制{4,5,6}到新扩容空间 所以包括下面是两次copied
copied constructor handled
destructor handled 旧的 {1,2,3} 被销毁了
destructor handled 栈上的临时对象 {4,5,6} 被销毁了。
---4---
1,2,3
4,5,6
---5---
copied constructor handled vertices有两个元素,每个元素都要复制到临时的v一次(第1次)
1,2,3
destructor handled for运行结束后v被销毁了
copied constructor handled vertices有两个元素,每个元素都要复制到临时的v一次(第2次)
4,5,6
destructor handled for运行结束后v被销毁了
---6---
1,2,3
4,5,6
destructor handled
destructor handled
*/—3— 为什么输出了 2 次?
Vector 扩容(Reallocation) 机制:
- 创建新空间:vector 在堆上分配一块足以容纳 2 个对象的新内存。
- 搬迁老居民(拷贝 1):将旧内存位置的
{1, 2, 3}拷贝到新内存的第一个格子里。输出: copied constructor handled - 迎接新成员(拷贝 2):将栈上的临时对象
{4, 5, 6}拷贝到新内存的第二个格子里。输出: copied constructor handled - 善后工作:销毁旧内存里的对象并释放旧空间,以及销毁栈上的临时对象
{4,5,6}
结果:一共 2 次。不过拷贝新旧的顺序不固定,能确定的是一定会拷贝两次
避免复制整个vector#
主要看Function函数
#include <iostream>
#include <string>
#include <vector>
struct Test
{
};
struct Vertex
{
float x, y, z;
//有了其他的构造函数,如果需要用到无参构造函数,
//就必须手动写一个
Vertex()
{
}
//因为有构造函数了,所以Vertex 结构体不是一个聚合类型(
// 有构造函数),所以要使用显示构造函数
// 使 Vertex v = { 1,2,3 }; 编译通过
Vertex(float x, float y, float z)
{
this->x = x;
this->y = y;
this->z = z;
}
//参数必须是&,原因如下:
//1. 当你尝试通过 Vertex a = b; 调用复制构造函数时,你需要将 b 传递给参数 other。
//2. 在 C++ 中,按值传递(Pass by Value)参数本身就会触发一次复制。
//3. 为了复制 b 到 other,编译器又需要调用复制构造函数。[死循环了]
//因为main中使用 vector.push({1,2,3}) 需要将临时对象拷贝到vector中,所以这里的参数
//必须是const
Vertex(const Vertex& vertex)
{
std::cout << "copied constructor handled" << std::endl;
this->x = vertex.x;
this->y = vertex.y;
this->z = vertex.z;
}
/*virtual*/ void test()
{
}
~Vertex()
{
std::cout << "destructor handled" << std::endl;
}
};
std::ostream& operator<<(std::ostream& stream, const Vertex& vertex)
{
stream << vertex.x << "," << vertex.y << "," << vertex.z;
return stream;
}
//使用引用避免复制整个数组
void Function(const std::vector<Vertex>& vertices)
{
}
int main()
{
std::vector<Vertex> vertices;
vertices.push_back({ 1,2,3 });
std::cout << "==0===" << std::endl;
vertices.push_back({ 4,5,6 });
std::cout << "==1===" << std::endl;
Function(vertices);
std::cout << "==2===" << std::endl;
vertices.erase(vertices.begin() + 1);
std::cout << "==3===" << std::endl;
for (Vertex& v : vertices)
std::cout << v << std::endl;
std::cin.get();
}
/*
copied constructor handled //第一个临时对象复制到vector中之后,马上又销毁了
destructor handled
==0===
copied constructor handled //空间不够了,需要把第一个元素复制到新数组中。第二个元素创建临时对象,并复制到新数组中
copied constructor handled
destructor handled //旧的数组中的第一个元素被销毁了
destructor handled //第二个元素的临时对象被销毁了
==1===
==2===
destructor handled
==3===
1,2,3
*/这里简单解释一下迭代器,后面视频会详解:
迭代器是一个对象(类),它的设计目的是为了让你能够像使用“指针”一样去访问容器里的元素。
- 它内部保存了容器中某个元素的内存地址。
我的理解是,这样就决定了从哪里进行迭代容器(的所有元素) - 它重载了各种运算符(比如 ++、+、*、->),让你可以通过简单的数学运算来移动它。