C++ ——vector数组笔记
vector 是 C++ 标准库中的一个动态数组容器(Sequence Container),它可以自动管理内存大小,可以在运行时根据需要动态增长或缩小。它是一个非常常用且强大的容器,用于存储一系列元素。可以简单的认为,vector是一个能够放任意类型的动态数组。
下面详细介绍 vector
的使用方法,并提供相应的代码案例。
1,基本函数实现
1.1 构造函数
构造函数是初始化一个数组。vector本质是类模板,可以存储任何类型的数据。vector在声明前需要加上数据类型,而vector则通过模板参量设定类型。
vector():创建一个空vector vector(int nSize):创建一个vector,元素个数为nSize vector(int nSize,const t& t):创建一个vector,元素个数为nSize,且值均为t vector(const vector&):复制构造函数 vector(begin,end):复制[begin,end)区间内另一个数组的元素到vector中
示例:
vector<int> vec; // 创建一个空数组vec vector<int> vec(1, 2, 3, 4, 5, 6);//vec中的内容为1, 2, 3, 4, 5, 6 vector<int> vec(4); // 开辟四个空间,值默认为0 vector<int> vec(5, 4); //创建5个值为4的数组 vector<int> vec(a);//声明并用a向量初始化vec向量 vector<int> vec(a.begin(), a.end()); // 将a的值从头开始到尾复制 vector<int> vec(a.rbegin(), a.rend()); // 将a的值从尾到头复制 int a[5]={1,2,3,4,5}; vector<int> vec(a,a+5);//将a数组的元素用来初始化vector向量 vector<int> vec(&a[1],&a[4]);//将a[1]-a[4]范围内的元素作为vec的初始值
1.2 增加函数
即向vector中插入元素。其中emplace_back的效果和push_back一样,都是尾部插入元素。二者的差别在于底部实现的机制不同;push_back是将这个元素拷贝或移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而emplace_back在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程,所以emplace_back的速度更快。
void push_back(const T& x):向量尾部增加一个元素X iterator insert(iterator it,const T& x):向量中迭代器指向元素前增加一个元素x iterator insert(iterator it,int n,const T& x):向量中迭代器指向元素前增加n个相同的元素x iterator insert(iterator it,const_iterator first,const_iterator last):向量中迭代器指向元素前插入另一个相同类型向量的[first,last)间的数据
示例:
//在vector的末尾插入新元素 vec.push_back(1); //在迭代器的前面插入新元素 vector<int>::iterator it; it=vec.begin(); vec.insert(it,5);//在第一个元素前面插入5 //在vector中加入3个1元素,同时清除掉以前的元素 vec.assign(3,1);//现在vector中只有3个1
assgin修改vector,和insert操作类似,不过insert是从尾部插入,而assign则将整个vector改变。
1.3 删除函数
erase是通过迭代器删除某个或某个范围的元素,并返回下一个元素的迭代器。
iterator erase(iterator it):删除向量中迭代器指向元素 iterator erase(iterator first,iterator last):删除向量中[first,last)中元素 void pop_back():删除向量中最后一个元素 void clear():清空向量中所有元素
示例:
//删除最后一个元素 vec.pop_back(); //删除指定位置的元素 vec.erase(vec.begin());//删除第一个位置的元素值 //清除所有元素 vec.clear(); //判断该数组是否为空 vec.empty();
1.4 遍历函数
reference at(int pos):返回pos位置元素的引用 reference front():返回首元素的引用 reference back():返回尾元素的引用 iterator begin():返回向量头指针,指向第一个元素 iterator end():返回向量尾指针,指向向量最后一个元素的下一个位置 reverse_iterator rbegin():反向迭代器,指向最后一个元素 reverse_iterator rend():反向迭代器,指向第一个元素之前的位置
遍历数组示例:
//向数组一样利用下标进行访问 vector<int> a; for(int i=0;i<a.size();i++){ cout<<a[i]; } //利用迭代器进行访问 vector<int>::iterator it; for(it=a.begin();it!=a.end();it++){ cout<<*it; }
1.5 vector容量和大小
顾名思义,size表示当前有多少个元素,capacity是可容纳的大小,因为vector是顺序存储的,那么和数组一样,有一个初始容量,在vector里就是capacity。capacity必然大于等于size,每次扩容时会改变,具体大小和vector底层实现机制有关。
max_size 是可存储的最大容量,和实际的编译器,系统有关,使用的比较少。
empty就是判断vector是否为空,其实就是判断size是否等于0。定义vector时设定了大小,resize修改大小等操作,vector都不为空,clear后,size=0,那么empty判断就为空。
bool empty() const:判断向量是否为空,若为空,则向量中无元素 int size() const:返回向量中元素的个数 int capacity() const:返回当前向量所能容纳的最大元素值 int max_size() const:返回最大可允许的vector元素数量值
2,容器特性和属性
2.1 容器的特性
2.1.1,顺序序列(Sequential Container)
顺序容器中的元素按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。
std::vector
是C++标准库中的一种顺序序列容器,这意味着它按照元素的顺序进行存储和访问。- 每个元素都有一个唯一的位置(索引),可以通过索引直接访问,同时支持迭代器用于循环访问。
2.1.2,动态数组(Dynamic Array)
支持对序列中的任意元素进行快速直接访问,甚至可以通过指针算述进行该操作。提供了在序列末尾相对快速地添加/删除元素的操作。
std::vector
是一个动态数组容器,它在内部使用动态分配的数组来存储元素。- 它能够动态调整数组的大小,可以在运行时根据需要增加或减少元素的数量,而无需预先指定数组的大小。
2.1.3,能够感知内存分配器的(Allocator-aware)
容器使用一个内存分配器对象来动态地处理它的存储需求。
std::vector
是 allocator-aware 的,这意味着它能够感知并与特定的内存分配器协同工作。- 使用者可以通过传递自定义的内存分配器(通常是一个模板参数),以实现对内存分配和释放过程的控制。
以下是一个简单的示例,演示了 std::vector
的这三个特性:
#include <iostream> #include <vector> int main() { // 创建一个空的vector std::vector<int> myVector; // 添加元素 myVector.push_back(10); myVector.push_back(20); myVector.push_back(30); // 遍历并输出元素 for (const auto& num : myVector) { std::cout << num << " "; } return 0; }
在这个例子中,std::vector
被用作顺序序列,存储了动态数组,并且它的内存管理是由C++标准库的分配器机制处理的。
2.2 容器的属性
下面介绍一下 std::vector
的基本属性,以及一些示例。
2.2.1 动态调整大小:
std::vector
是一个动态大小的容器,可以在运行时根据需要自动调整大小。这意味着你可以随时向vector
中添加或删除元素,而无需担心数组大小的固定性。它会自动处理内存管理。
示例:
#include <iostream> #include <vector> int main() { std::vector<int> myVector; // 创建一个空的vector myVector.push_back(1); // 添加元素 myVector.push_back(2); myVector.push_back(3); std::cout << "Vector的大小: " << myVector.size() << std::endl; // 输出vector的大小 return 0; }
2.2.2 快速随机访问
std::vector
支持随机访问,即通过索引直接访问元素。这使得在访问和修改元素时效率非常高。 由于元素的连续存储(底层实现是数组),std::vector 支持快速的随机访问。你可以通过索引直接访问向量中的元素。
示例:
#include <iostream> #include <vector> int main() { std::vector<std::string> names = {"Alice", "Bob", "Charlie"}; // 随机访问元素 std::cout << "第二个元素是: " << names[1] << std::endl; return 0; }
2.2.3 动态内存管理
std::vector
负责管理其内部的动态内存,因此你无需手动分配或释放内存。当元素被添加或删除时,vector
会自动处理内存的分配和释放。当向量的大小超过其当前容量时,会动态分配更多的内存以容纳更多的元素。这可以确保在添加元素时不会频繁重新分配内存。
#include <iostream> #include <vector> int main() { std::vector<double> prices; // 添加元素 prices.push_back(10.5); prices.push_back(20.0); prices.push_back(15.75); // 遍历元素并输出 for (const auto& price : prices) { std::cout << "价格: " << price << std::endl; } return 0; }
总结: std::vector
是C++中一个强大且灵活的容器,它提供了动态大小、随机访问、动态内存管理和丰富的功能。通过合理利用std::vector
,你可以更轻松地处理动态数据集合,而无需手动管理内存或担心固定数组大小的限制。除此之外,还有下面属性:
- 1,连续内存存储:std::vector中的元素在内存中是连续存储的,这意味着可以通过指针算术直接访问元素,并且对于许多算法来说,这种存储方式是高效的,有助于提高访问效率。
- 2,尾部插入和删除: 向量支持在尾部进行快速的元素插入和删除操作。
- 3,快速尾部操作: 由于元素的尾部插入和删除是快速的,std::vector 适用于需要在序列的末尾执行大量操作的场景。
- 4,STL 算法和迭代器支持: std::vector 与 C++ 标准模板库(STL)的其他部分无缝集成,可以使用算法和迭代器对向量进行操作。你可以使用迭代器来遍历
std::vector
中的元素。
2.3 如何获取vector的内存
在C++中,std::vector
是一个封装了动态数组的容器,它自动处理内存的分配和释放。要获取 std::vector
的底层内存地址,你可以使用 data()
成员函数,它返回指向容器第一个元素的指针。
以下是一个示例:
#include <iostream> #include <vector> int main() { std::vector<int> myVector = {1, 2, 3, 4, 5}; // 获取底层内存地址 int* ptr = myVector.data(); // 输出每个元素及其内存地址 for (size_t i = 0; i < myVector.size(); ++i) { std::cout << "元素 " << myVector[i] << " 的内存地址: " << &myVector[i] << std::endl; } // 输出整个vector的底层内存地址 std::cout << "Vector的底层内存地址: " << ptr << std::endl; return 0; }
请注意,通过 data()
获取的指针指向的是 vector 中第一个元素的地址。如果 vector 是空的,data()
返回的是一个合法但未定义的指针,因此在使用之前应确保 vector 不为空。
注意:一般情况下,直接使用 data()
获取 vector 的底层内存并进行操作可能不是一个良好的实践,因为 std::vector
提供了许多高级的函数和方法,可以更安全和方便地访问元素。直接访问内存可能会引发未定义的行为,特别是在修改元素时。
2.3.1 疑问:获取内存使用 myVector.data() 和 myVector[0]的区别是什么?
myVector.data()
和 myVector[0]
返回的是不同类型的指针,并且有不同的用途。
myVector.data()
:
myVector.data()
返回指向 vector 第一个元素的指针,是一个原始指针(T*
类型,其中T
是 vector 中元素的类型)。- 这个指针允许你直接访问 vector 的底层内存,但是需要小心使用,因为它没有边界检查和安全保障。
- 在对
data()
返回的指针进行操作时,务必确保 vector 不为空。
int* ptr = myVector.data(); // 对 ptr 进行直接内存操作
myVector[0]
:
myVector[0]
是 vector 的运算符重载,它返回 vector 中第一个元素的引用。- 这个引用允许你直接访问 vector 的第一个元素,并且通过引用操作进行修改。它提供了边界检查,确保你访问的是有效的元素。
int& ref = myVector[0]; // 对 ref 进行直接操作,修改会影响 vector 中的元素
选择使用的场景:
- 如果你需要直接访问 vector 中第一个元素的底层内存,并且要进行原始指针的操作,可以使用
myVector.data()
。 - 如果你只是想获取第一个元素的值,并且可能会进行修改,使用
myVector[0]
更直观且更安全,因为它提供了引用而不是裸指针,并且有边界检查。
3,C++ vector 底层实现机制
这里参考的是:https://c.biancheng.net/view/6901.html
STL 众多容器中,vector是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。
3.1 vector源码分析
通过分析vector容器的源码就可以看到,他就是使用三个迭代器来表示:
//_Alloc 表示内存分配器,此参数几乎不需要我们关心 template <class _Ty, class _Alloc = allocator<_Ty>> class vector{ ... protected: pointer _Myfirst; pointer _Mylast; pointer _Myend; };
其中,_Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。
下图演示了以上这 3 个迭代器分别指向的位置。
如上图所示,通过这 3 个迭代器,就可以表示出一个已容纳 2 个元素,容量为 5 的 vector 容器。
在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:
- _Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
- _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
- _Myfirst 和 _Myend 可以用表示 vector 容器的容量。
- 对于空的 vector 容器,由于没有任何元素的空间分配,因此 _Myfirst、_Mylast 和 _Myend 均为 null。
通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能,比如:
template <class _Ty, class _Alloc = allocator<_Ty>> class vector{ public: iterator begin() {return _Myfirst;} iterator end() {return _Mylast;} size_type size() const {return size_type(end() - begin());} size_type capacity() const {return size_type(_Myend - begin());} bool empty() const {return begin() == end();} reference operator[] (size_type n) {return *(begin() + n);} reference front() { return *begin();} reference back() {return *(end()-1);} ... };
3.2 vector 扩大容量的本质
上面有说到果,vector作为容器有着动态数组的功能,当加入的数据大于vector容量(capacity)时会自动扩容,系统会自动申请一片更大的空间,把原来的数据拷贝过去,释放原来的内存空间。
另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放。
这也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
由此可见,vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。vector 容器扩容时,不同的编译器申请更多内存空间的量是不同的。以 VS 为例,它会扩容现有容器容量的 50%。
4,使用示例
std::vector
是一个非常有用的标准库容器,它提供了动态数组的功能,可以根据需要自动调整大小。以下是一些关于 std::vector
的基本示例:
4.1:引入头文件
首先,使用的话需要引入头文件 <vector>
#include <vector>
4.2:创建 vector 对象
直接使用vector
模板类来创建一个 vector 对象。可以创建存储特定类型元素的 vector,格式为: vector<数据类型> 名字
。例如:
vector<int> myVector; // 创建一个存储整数的 vector,名字为myVector vector<char> myVector; // 创建一个存储字符的 vector,名字为myVector vector<string> myVector; // 创建一个存储字符串的 vector,名字为myVector ……
4.3:创建 vector并添加元素
使用push_back()函数将元素添加到vector的末尾,默认且只能添加到末尾。
代码如下:
#include <iostream> #include <vector> int main() { // 创建一个空的 vector std::vector<int> numbers; // 向 vector 中添加元素 numbers.push_back(1); numbers.push_back(2); numbers.push_back(3); // 访问 vector 中的元素 std::cout << "Vector elements: "; for (int i = 0; i < numbers.size(); ++i) { std::cout << numbers[i] << " "; } std::cout << std::endl; return 0; }
打印结果如下:
Vector elements: 1 2 3
4.4:使用范围循环遍历 vector
代码如下:
#include <iostream> #include <vector> int main() { // 创建一个 vector,并初始化 std::vector<std::string> fruits = {"Apple", "Banana", "Orange"}; // 也可以向 vector 中添加元素 fruits.push_back("pear"); // 使用范围循环遍历 vector std::cout << "Fruits: "; for (const auto& fruit : fruits) { std::cout << fruit << " "; } std::cout << std::endl; return 0; }
结果如下:
Fruits: Apple Banana Orange pear
4.5:插入和删除元素
#include <iostream> #include <vector> int main() { // 创建一个 vector,并初始化 std::vector<double> prices = {10.5, 20.3, 15.0}; // 直接在尾部插入元素 prices.push_back(88); // 在指定位置插入元素 prices.insert(prices.begin() + 1, 25.8); // 删除指定位置的元素 prices.erase(prices.begin() + 2); // 打印 vector 元素 std::cout << "Prices: "; for (const auto& price : prices) { std::cout << price << " "; } std::cout << std::endl; return 0; }
结果:
Prices: 10.5 25.8 15 88
4.6:使用迭代器访问数组
除了直接使用for循环遍历访问数组之外,我们也可以利用迭代器将容器中数组输出。
首先声明一个迭代器,vector<int>::iterator it; 来访问vector容器,目的是遍历或者指向vector容器的元素。
#include <iostream> #include <vector> int main() { // 创建一个 vector,并初始化 std::vector<int> numbers = {1, 2, 3, 4, 5}; // 使用迭代器访问 vector 中的元素 std::cout << "Vector elements: "; for (auto it = numbers.begin(); it != numbers.end(); ++it) { std::cout << *it << " "; } std::cout << std::endl; return 0; }
结果如下:
Vector elements: 1 2 3 4 5