C++ ——vector数组笔记

时间:2024-01-21 20:10:52

 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 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  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