【C++】STL--Vector容器--拆析解剖Vector的实现以及Vector的底层详解(1)

一.vector的介绍及使用

文档参考:

https://cplusplus.com/reference/vector/vector/

  • vector 是一个可以动态增长、支持下标访问的连续数组容器。
  • 它和数组一样,元素在内存中连续存放,所以支持 [] 下标访问,效率 O(1)。但它不像数组那样大小固定,可以自动增长,不需要手动管理容量。
  • vector 内部用三个指针管理内存:_start 指向数组开头,_finish 指向最后一个元素的下一个位置,_end_of_storage 指向容量末尾的下一个位置。元素个数 size 就是 _finish - _start,总容量 capacity 就是 _end_of_storage - _start
  • size 等于 capacity 时,空间已满,vector 会触发扩容:新开一块更大的内存(通常是原容量的 1.5 或 2 倍),把旧元素拷贝或移动到新空间,释放旧内存,最后调整三个指针指向新位置。扩容有开销,但不是每次插入都触发,所以尾部插入的均摊时间仍然是 O(1)。vector 用额外占用的空间换取了尾部操作的效率。
  • 优点是下标访问快(O(1)),尾部增删快(均摊 O(1)),内存连续对缓存友好。
  • 缺点是中间插入删除需要移动元素(O(n)),效率低;扩容时有拷贝开销;可能占用比实际需要更多的内存。
  • 和其他容器相比:vectorlist 访问更快,但中间插入删除更慢;和 deque 比,vector 的尾部操作更稳定,但头部操作不如 deque
  • 常见操作复杂度:下标访问 [] 是 O(1),push_backpop_back 均摊 O(1),中间插入 insert 和删除 erase 是 O(n),查找 find 是 O(n)。

使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我也是按照这个方法去学

二.vector的使用

2.1 vector的定义

default (1) ------ 空间配置器 alloc

cpp 复制代码
vector(const allocator_type& alloc = allocator_type());

alloc 是缺省参数 ,绝大多数情况下不需要手动传入,它是 STL 六大组件中的空间配置器,负责底层内存的分配与释放简单点说就是:alloc**=** new/ delete的封装版本,允许用户自定义内存管理策略日常使用完全可以忽略它,直接用默认值即可

cpp 复制代码
vector<int> v;  // 等价于 vector<int, allocator<int>> v;

fill (2) ------ value_type'\0' 的问题

cpp 复制代码
vector(size_type n, const value_type& val = value_type());

value_type 就是 vector 的模板参数类型,好比 vector<int>value_type 就是 int

这里的 '\0' 是 char 类型的缺省值;

cpp 复制代码
vector<char> v1(5);           // 5 个 '\0'(缺省值)
vector<char> v2(5, 'a');      // 5 个 'a'

vector<int> v3(5);            // 5 个 0(int 的缺省值)
vector<string> v4(3);         // 3 个空字符串

更正 :缺省值不是固定的 '\0',而是该类型的默认构造值value_type())。对于 char 来说正好是 '\0'


range (3) ------ 迭代器是函数模板

cpp 复制代码
template <class InputIterator>
vector(InputIterator first, InputIterator last);
  • 这里的迭代器不一定是 vector::iterator,它可以是任意满足输入迭代器要求的类型

  • 比如可以用数组指针其他容器的迭代器istream_iterator

cpp 复制代码
// 用数组指针初始化
int arr[] = {1, 2, 3, 4, 5};
vector<int> v1(arr, arr + 5);

// 用 list 的迭代器初始化
list<int> lst = {10, 20, 30};
vector<int> v2(lst.begin(), lst.end());

// 用另一个 vector 的迭代器(部分区间)
vector<int> v3(v1.begin() + 1, v1.end() - 1);

// 用 istream_iterator 从输入流读取
vector<int> v4((istream_iterator<int>(cin)), istream_iterator<int>());

copy (4)------ 拷贝构造函数

cpp 复制代码
vector (const vector& x);

只有一个参数:另一个同类型的 vector 对象。

作用:用已有的 vector 对象 x 构造一个新的 vector,新对象是 x 的独立副本(深拷贝)

特点 :参数类型 : const vector&(常量引用,只读); 深拷贝(新对象有独立的内存);修改 v2 不会影响 v1

1. 无参构造 vector()

cpp 复制代码
vector<int> v1;  // 创建一个空的 vector

2. 构造并初始化 n 个 val

cpp 复制代码
vector<int> v2(5, 10);  // 创建包含 5 个 10 的 vector
// v2: [10, 10, 10, 10, 10]

vector<string> v3(3, "hello");  // 创建包含 3 个 "hello" 的 vector
// v3: ["hello", "hello", "hello"]

3. 拷贝构造

cpp 复制代码
vector<int> v4(5, 10);   // [10, 10, 10, 10, 10]
vector<int> v5(v4);      // 用 v4 拷贝构造 v5
                        // v5: [10, 10, 10, 10, 10](独立)

4. 使用迭代器区间初始化

cpp 复制代码
vector<int> v6(5, 10);                    // [10, 10, 10, 10, 10]
vector<int> v7(v6.begin(), v6.end());     // 用迭代器拷贝整个 v6

int arr[] = {1, 2, 3, 4, 5};
vector<int> v8(arr, arr + 5);             // 用数组区间初始化
                                          // v8: [1, 2, 3, 4, 5]

vector<int> v9(v6.begin() + 1, v6.end() - 1);  // 部分区间
                                               // v9: [10, 10, 10](去掉首尾)

2.2 vector iterator 的使用

cpp 复制代码
//正向迭代器

iterator begin();                 // 返回第一个元素的迭代器
const_iterator begin() const;     // const 对象调用,只能读

iterator end();                   // 返回最后一个元素后面的位置(不能解引用)
const_iterator end() const;       // const 对象调用


//反向迭代器
reverse_iterator rbegin();        // 返回最后一个元素的迭代器(反向遍历起点)
const_reverse_iterator rbegin() const;

reverse_iterator rend();          // 返回第一个元素**前面**的位置(反向遍历终点)
const_reverse_iterator rend() const;

1. begin()end()

begin() 返回指向第一个元素的迭代器,end() 返回指向最后一个元素后面一个位置的迭代器。

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 正向遍历
for (auto it = v.begin(); it != v.end(); ++it)
{
    cout << *it << " ";   // 输出:1 2 3 4 5
}
cout << endl;

// 修改元素
for (auto it = v.begin(); it != v.end(); ++it)
{
    *it += 10;            // 每个元素加 10
}
                            输出:11, 12, 13, 14, 15

// const 版本(只读)
const vector<int> v2 = {1, 2, 3};
auto cit = v2.begin();    // 返回 const_iterator,不能修改
   *cit = 100;   报错

2. rbegin()rend()


rbegin() 返回指向最后一个元素的反向迭代器,rend() 返回指向第一个元素前面一个位置的反向迭代器。

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 反向遍历(从后往前)
for (auto it = v.rbegin(); it != v.rend(); ++it)
{
    cout << *it << " ";   // 输出:5 4 3 2 1
}
cout << endl;

// 修改元素
for (auto it = v.rbegin(); it != v.rend(); ++it)
{
    *it += 10;            // 每个元素加 10
}
                        输出:11, 12, 13, 14, 15

2.3 vector 空间增长问题

cpp 复制代码
// 获取数据个数
size_t size() const;

// 获取容量大小
size_t capacity() const;

// 判断是否为空
bool empty() const;

// 改变 vector 的 size
void resize(size_t n, const T& val = T());

// 改变 vector 的 capacity
void reserve(size_t n);

size() ------ 获取元素个数

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
cout << v.size() << endl;  // 输出:5

v.push_back(6);
cout << v.size() << endl;  // 输出:6

v.pop_back();
cout << v.size() << endl;  // 输出:5

empty() ------ 判断是否为空

cpp 复制代码
vector<int> v1;
cout << v1.empty() << endl;   // 输出:1

vector<int> v2 = {1, 2, 3};
cout << v2.empty() << endl;   // 输出:0

if (!v2.empty())
{
    cout << "vector 不为空" << endl;
}

resize() ------ 改变 size(重点)

new_size < size:发生截断,会删除多余元素;new_size > size:发生扩容 size,会新增元素用默认值或指定值填充

reserve() ------ 改变 capacity(重点)

只改变容量,不改变元素个数;n <= capacity:无效果;n > capacity:扩容到至少 n

cpp 复制代码
vector<int> v(10, 1);     // size=10, capacity=10(10个1)
v.reserve(20);            // 扩容到 20
cout << v.size() << endl;       // 10(不变)
cout << v.capacity() << endl;   // 20

v.reserve(15);            // 15 ≤ 20,不缩容
cout << v.size() << endl;       // 10(不变)
cout << v.capacity() << endl;   // 20(不变)

v.reserve(5);             // 5 ≤ 20,不缩容
cout << v.size() << endl;       // 10(不变)
cout << v.capacity() << endl;   // 20(不变)

结论reserve 只能扩大 容量,不能缩小;且不改变 size


cpp 复制代码
vector<int> v(10, 1);     // size=10, capacity=10
v.reserve(20);            // 先扩容到 20
cout << v.size() << endl;       // 10
cout << v.capacity() << endl;   // 20

v.resize(15, 2);          // 扩大到 15 个元素,多出的填 2
cout << v.size() << endl;       // 15
cout << v.capacity() << endl;   // 20(容量够,不变)

v.resize(25, 3);          // 扩大到 25 个元素,容量不够
cout << v.size() << endl;       // 25
cout << v.capacity() << endl;   // 可能 30 或 40(自动扩容)

v.resize(5);              // 缩小到 5 个元素
cout << v.size() << endl;       // 5
cout << v.capacity() << endl;   // 不变(还是之前的容量)

resize 改变 size,可能改变 capacity

小结一下二者的区别:

reserve 只改容量(只能扩大),不改元素个数;resize 改元素个数(可扩大可缩小),容量不够时会自动扩容,但缩小 size 不会缩容量。

capacity() ------ 获取当前容量大小

cpp 复制代码
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	//v.reserve(100);

	sz = v.capacity();
	cout << "capacity changed: " << sz << '\n';

	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

在Linux下运行结果:

  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题
  • resize在开空间的同时还会进行初始化,影响size

2.4vector 增删查改

cpp 复制代码
// 尾插
void push_back(const T& val);

// 尾删
void pop_back();

// 查找(算法,不是 vector 成员)
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& val);

// 在 position 之前插入 val
iterator insert(iterator position, const T& val);

// 在 position 之前插入 n 个 val
void insert(iterator position, size_type n, const T& val);

// 在 position 之前插入迭代器区间 [first, last)
template <class InputIterator>
void insert(iterator position, InputIterator first, InputIterator last);

// 删除 position 位置的元素
iterator erase(iterator position);

// 删除 [first, last) 区间内的元素
iterator erase(iterator first, iterator last);

// 交换两个 vector
void swap(vector& x);

// 下标访问(不检查边界)
reference operator[](size_type n);
const_reference operator[](size_type n) const;

// 下标访问(检查边界,越界抛异常)
reference at(size_type n);
const_reference at(size_type n) const;

// 第一个元素
reference front();
const_reference front() const;

// 最后一个元素
reference back();
const_reference back() const;

// 返回底层数组指针
T* data();
const T* data() const;

T 是模板参数,表示这个 vector 存储的元素类型

value_type 是 STL 标准中对"容器元素类型"的统一定义名称

所以**T 就是 value_type**

push_back ------ 尾插

作用:在 vector 末尾添加一个元素

cpp 复制代码
vector<int> v;
v.push_back(10);   // v: [10]
v.push_back(20);   // v: [10, 20]
v.push_back(30);   // v: [10, 20, 30]

底层原理

检查当前 size 是否等于 capacity;

如果容量够:直接把元素放到 _finish 位置,然后 _finish++;

如果容量不够:先扩容(新容量 ≈ 1.5 倍或 2 倍),再添加元素.

扩容时 O(n)(需要拷贝所有元素


pop_back ------ 尾删

作用:删除 vector 末尾的一个元素

cpp 复制代码
vector<int> v = {10, 20, 30, 40};
v.pop_back();   // v: [10, 20, 30]
v.pop_back();   // v: [10, 20]

底层原理

检查是否非空(不能对空 vector 执行 pop_back

_finish--(只是指针前移,不释放内存

元素个数减 1,容量不变

O(1)(只是指针移动,不需要拷贝)


find 是 算法(在 <algorithm> 中),不是 vector 的成员函数。它用于在容器中查找值

cpp 复制代码
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& val);

first :查找范围的起始迭代器

last :查找范围的结束迭代器(不包含)

val :要查找的值

返回值:找到返回指向该元素的迭代器,找不到返回 last。

cpp 复制代码
vector<int> v = {10, 20, 30, 40};
auto it = find(v.begin(), v.end(), 30);
if (it != v.end())
    cout << *it << endl;

底层实现:

cpp 复制代码
template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& val)
{
    while (first != last)
    {
        if (*first == val)   // 用 == 比较
            return first;
        ++first;
    }
    return last;
}

std::find 是算法,用迭代器范围 [first, last) 查找值,返回迭代器;找到返回位置,找不到返回 last。vector 本身没有 find 成员函数。


insert ------ 在 pos 前插入

容量不足则扩容,将 [pos, _finish) 往后挪一位,然后构造新元素。

cpp 复制代码
iterator insert(iterator position, const T& val);
cpp 复制代码
vector<int> v = {10, 20, 40};
auto it = v.begin() + 2;
v.insert(it, 30);
                    // 输出v: [10, 20, 30, 40]

erase ------ 删除 pos 位置的数据

[pos+1, _finish) 往前挪一位,覆盖被删除元素,然后 _finish--

cpp 复制代码
iterator erase(iterator position);
cpp 复制代码
vector<int> v = {10, 20, 30, 40};
auto it = v.begin() + 2;
v.erase(it);
                // v: [10, 20, 40]

swap ------ 交换两个 vector 的数据空间

只交换三个指针(_start_finish_end_of_storage),不拷贝元素。

cpp 复制代码
void swap(vector& x);
cpp 复制代码
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {4, 5, 6};
v1.swap(v2);
//输出: v1: [4, 5, 6], v2: [1, 2, 3]

operator[] ------ 下标访问

return _start[n],直接指针偏移,不检查越界。

cpp 复制代码
reference operator[](size_type n);
const_reference operator[](size_type n) const;
cpp 复制代码
vector<int> v = {10, 20, 30};
cout << v[0] << endl;   // 10
v[1] = 100;             // 进行修改
// v: [10, 100, 30]

2.4 vector 迭代器失效问题(重点)

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

对于vector可能会导致其迭代器失效的操作有:

会引起其底层空间改变的操作**,都有可能是迭代器失效,比如:resize、reserve、insert、
assign、push_back等。**

  • 1.insert

第一种:插入操作导致的内存重新分配(扩容)

cpp 复制代码
void TestInsert1()
{
// 迭代器失效问题 -- 类似野指针问题
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    vector<int>::iterator pos = find(v.begin(), v.end(), 2);
    if (pos != v.end())
    {
        // 第一种:扩容导致失效
        // 当 v.capacity() < v.size() + 1 时,vector 重新分配内存
        // 所有迭代器(包括 pos)指向已释放的旧内存
        v.insert(pos, 20);
        
        //  pos 已失效,不能使用
        // cout << *pos << endl;
        
        // 解决方法:重新赋值
        // pos = v.insert(pos, 20);
    }
}

size() + 1 > capacity() 时,vector 会重新分配更大内存;所有迭代器(包括 pos)都失效(指向已释放的旧内存)

图解

第二种**:插入操作导致的位置移动(即使不扩容)**

cpp 复制代码
void TestInsert2()
{
// 迭代器失效问题 -- 类似野指针问题
    vector<int> v;
    v.reserve(10);  // 预留足够空间,避免扩容
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    vector<int>::iterator pos = find(v.begin(), v.end(), 2);
    if (pos != v.end())
    {
        // 虽然没扩容,但 insert 导致 pos 之后的所有元素后移
        // pos 仍然指向原位置,但该位置现在放的是新插入的 20
        // 原本的 2 已经被后移,pos 的语义改变了
        v.insert(pos, 20);
        
        //  pos 现在指向 20,而不是原来的 2
        // cout << *pos << endl;  // 输出 20,不是 2
        
        // 解决方法:重新赋值
        // pos = v.insert(pos, 20);  // pos 指向新插入的 20
    }
}

pos 仍指向原位置,但该位置现在是新插入的元素;pos 原本指向的元素已经被后移,所以 pos 语义改变

图解:

insert 操作导致迭代器失效的两种场景:

  1. 扩容导致的失效:当插入元素时发生扩容,vector 会重新分配内存空间,将旧数据迁移到新空间后释放旧空间。此时迭代器 pos 仍然指向已被释放的旧空间,变成了野指针,无法再使用。

  2. 不扩容但语义改变导致的失效:当插入元素时没有发生扩容,vector 会将 pos 位置及其之后的元素向后移动,为插入腾出空间。此时 pos 仍然指向原来的内存位置,但该位置已经被新插入的元素占据,pos 不再指向原来的值,其语义发生了改变。

补充:

  • 发生扩容时,当前迭代器和之后所有迭代器都会失效

  • 无论哪种场景,迭代器失效后都不能再进行访问(读取、写入、比较等操作)

  • 解决方法:使用 insert 函数返回的新迭代器(指向新插入元素)重新赋值

不同平台的表现:

  • VS2019/VS2022(Debug 模式):程序崩溃,触发迭代器失效断言

  • VS2019/VS2022(Release 模式):可能不会立即崩溃,但属于未定义行为

  • Linux(GCC/Clang):通常不会崩溃,但迭代器确实已失效


  • 2.erase
cpp 复制代码
// erase 操作导致迭代器失效演示
void TestEraseDemo()
{
    vector<int> v{ 10, 20, 30, 40, 50 };
    
    // 查找值为30的元素
    vector<int>::iterator it = find(v.begin(), v.end(), 30);
    if (it != v.end())
    {
        // 删除it指向的元素(值为30)
        v.erase(it);
        // 此时it已失效,因为it后面的元素(40,50)都向前移动了
        // it指向的位置现在存放的是40,不再是原来的30
        
        // 错误:访问已失效的迭代器
        // cout << *it << endl;  // 虽然可能输出40,但这是未定义行为
        // *it = 100;           // 危险操作
        
        // 正确:使用erase返回值更新迭代器
        // it = v.erase(it);    // it指向被删除元素的下一个位置(也就是40)
    }
    
    // 删除后,it及其之后的所有迭代器都失效了
    // 不能再用it去访问vector中的元素
}

erase 导致迭代器失效的原因:

erase 删除元素时,pos 位置之后的元素会向前移动填补空缺。pos 仍然指向原来的内存位置,但该位置的数据已经被覆盖或删除,因此 pos 不再指向原来的值,迭代器失效。

不同平台的表现:

VS2019/VS2022(Debug 模式):程序崩溃,触发迭代器失效断言

VS2019/VS2022(Release 模式):可能不会立即崩溃,但属于未定义行为

Linux(GCC/Clang):通常不会崩溃,但迭代器确实已失效

图解:

erase 导致迭代器失效的两种场景:

  1. 缩容引发失效:比如 vector 当前有 100 个容量且存满 100 个数据,删除操作后有效数据只剩 30 个,这时编译器可能会触发缩容,重新开辟一块 50 个容量的新空间,把数据搬过去后释放原来的 100 个容量空间。之前保存的迭代器 pos 还指着被释放的旧空间,就成了野指针。

  2. 元素搬移导致意义改变:删除 pos 位置的数据后,pos 后面的所有元素会依次前移填补空缺。pos 还是指向原来那个内存地址,但里面的数据已经不是原来的值了,迭代器的含义发生了变化。如果 pos 正好指向最后一个元素,删除后 pos 就变成了 end 位置,end 位置没有有效数据,去访问它也是非法的。

补充:

  • 缩容并不是 C++ 标准规定的行为,不同的 STL 实现可能不同,有的编译器会缩容,有的不会
  • 但无论缩容与否,erase 之后的迭代器都应该认为是失效的,因为即使没有缩容,迭代器指向的位置意义也已经发生了改变
  • VS 系列(Debug 模式):做了非常严格的检查,即使迭代器仅仅是意义改变(并非野指针),也会被检测出来并触发断言,导致程序崩溃
  • G++(Linux):检查相对宽松,即使迭代器已经失效,访问时可能也不会报错,能正常输出结果,但这不代表迭代器是有效的

迭代器失效小结:

vector 插入或删除操作对迭代器的影响:

插入或删除元素会导致当前迭代器及其之后所有元素的迭代器失效。这是因为:

  • 插入时:元素后移或扩容搬移数据,迭代器指向的位置或内存发生变化

  • 删除时:元素前移填补空缺,迭代器指向的数据或语义发生改变

解决办法:

在使用迭代器之前重新赋值即可。

  • 对于 insertpos = v.insert(pos, value); 获取指向新插入元素的迭代器

  • 对于 erasepos = v.erase(pos); 获取指向被删除元素下一个位置的迭代器

重点就是:

进行插入或删除操作后,原有的迭代器不再可靠。如果后续还需要通过迭代器操作 vector 中的元素,必须用函数返回值对迭代器重新赋值,获取新的有效迭代器。

相关推荐
森G1 小时前
76、仿ASIO实现的Linux c++服务器------服务器源码解析----云视频服务项目
c++·qt
xxwl5851 小时前
Python语言初步认识(1)
开发语言·python·学习
TCW11211 小时前
AI底层系列:用C++实现线性代数的公式推导与算法设计-6.线性方程组的解集
c++·人工智能·算法
z落落1 小时前
C# FileStream文件流读取文件
开发语言·c#
砍材农夫1 小时前
python环境|conda安装和使用(1)
开发语言·后端·python·conda
拳里剑气2 小时前
C++算法:链表
c++·算法·链表
superkcl20222 小时前
【QT Thread】
c++·qt
星环科技2 小时前
数据标准Agent ,让企业数据说同一种语言
java·开发语言·前端
旖-旎2 小时前
《LeetCode 417 太平洋大西洋水流问题 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill