容器——vector

本文深入介绍了C++标准库中的vector容器,包括其构造函数、赋值运算符、迭代器、容量操作、访问操作、内容操作等核心功能。特别强调了vector在内存管理和效率上的特点,如动态增长策略和尾插尾删的高效性。同时,文章探讨了迭代器失效的原因和解决措施,并展示了如何模拟实现vector的部分功能。最后,提到了在使用memcpy拷贝自定义类型时可能出现的问题及其解决方案。
摘要由CSDN通过智能技术生成

介绍

  1. vector是表示可变大小数组的序列容器,在使用时需要包含#include<vector>头文件;
  2. vectorstring一样也采用了连续存储空间来存储元素,只不过它是一种泛型编程,也就是说该容器可以存储不同类型的元素,并不局限于字符型元素;
  3. 本质讲,vector使用动态分配数组来存储它的元素,当空间不够但需要插入元素时,其做法是分配一个新的数组,然后将全部元素移到这个数组,就时间而言,这是一个代价相对高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小;
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因此存储空间比实际需要的存储空间更大,因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,它以一种有效的方式动态增长;
  5. 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效;

接口介绍

构造函数
  • 内置类型名():这相当于是该类型的类 0 值,不同类型的类 0 值是不相同的,int——0,float / double——0.0,char——\0,指针——nullptr;
  • 自定义类型名():构造一个匿名对象;
  • vector<val_type> name;:创建一个空的数组,容量大小为 0,在创建时需指明类型;
  • vector<val_type> name(size_type n, const val_type& val = val_type());
    • 内置类型:创建长度为 n 的指定内置类型的数组,并将其全部初始化为给定的 val,如果没有给定 val,那么则使用该类型的类 0 值来初始化;
    • 自定义类型:创建长度为 n 的指定自定义类型的数组,并使用构造函数(参数)作为函数的第二个参数对创建的每个对象进行初始化,如果没有给定第二个参数,那么则调用其默认的构造函数为每一个元素初始化;
  • vector<val_type> name(const vector& x);:使用创建好的vector对象来进行拷贝构造,注意前后类型要一致,此处拷贝为深拷贝,内部资源需要重新开辟空间进行存放;
  • 函数模板:使用某一数组的迭代区间来进行构造,元素个数为迭代区间的元素个数,元素内容为迭代区间的每一个元素;
template <class InputIterator>
vector<val_type> name(InputIterator first, InputIterator last);
struct A{
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
//内置类型
vector<int> first;                          // 空的int型对象
vector<int> second (4,100);                 // 四个int型对象,初始为100
vector<int> third (second);                       // 拷贝一个int型对象
vector<int> fourth (second.begin(),second.end());  // 使用second的迭代区间
// 数组的地址区间也可以作为迭代区间,因为迭代器本质就是指针
int myints[] = {16,2,77,29};
vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );
//自定义类型
vector<A> first;                          // 空的A对象
vector<A> second (4,A(2,3));                 // 四个A类对象,初始化使用构造函数
vector<A> third (second);                       // 拷贝一个A类对象
vector<A> fourth (second.begin(),second.end());  // 使用second的迭代区间
  • 初始化列表:创建vector容器的另一种方式是使用初始化列表来指定初始值以及元素个数,可以用等号后面加花括号来输入参数,也可以直接在变量名后加花括号来输入参数,如果是自定义类型,那么输入的参数则是构造函数(参数)的形式,注意,使用了初始化列表那么就不能指明数组的大小了;
struct A{
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
//内置类型
vector<int> v1 = {1, 2, 3};
vector<int> v2{1, 2, 3};
//自定义类型
vector<A> v3 = {A(1, 2), A(3, 4)};
vector<A> v4{A(1, 2), A(3, 4)};
赋值运算符
  • vector& operator= (const vector& x);:将一个vector对象的值赋给另一个vector对象,此处赋值为深拷贝,内部资源需要重新开辟空间进行存放;
vector<int> foo (3,0);
vector<int> bar (5,0);
bar = foo;
//将一个空的匿名对象赋值给foo
foo = vector<int>();
迭代器

  vector容器的迭代器在使用上和string容器的迭代器是一模一样的,只需将string中的每个字符替换为vector中的每个元素即可,因此就不做过多介绍,这里附上string容器的迭代器的介绍以及vector容器的迭代器的网上详细定义;

容量操作

  vector容器的容量操作在使用上和string容器的容量操作是一模一样的,只需将string中的每个字符替换为vector中的每个元素即可,因此就不做过多介绍,这里附上string容器的容量操作函数的介绍以及vector容器的容量操作函数的网上详细定义;

  • 简单介绍
  • 详细定义
  • 注意事项:
    • 这里需要了解的一点就是,对于不同类型的max_size都是不一样的,每种类型都有对应的最大空间大小;
    • capacity 的代码在 vs 和 g++ 下分别运行会发现,vs 下 capacity 是按 1.5 倍增长的,g++ 是按 2 倍增长的。这个问题经常会考察,不要固化的认为,顺序表增容都是 2 倍,具体增长多少是根据具体的需求定义的。导致不同的具体原因是使用的 STL 的版本不同,vs 是 PJ 版本 STL,g++ 是 SGI 版本 STL;
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以直接开辟足够的空间,以缓解vector增容的代价缺陷问题;
  • resize在开空间的同时还会进行初始化,影响 size;
访问操作

  vector容器的访问操作在使用上和string容器的访问操作是基本一样的,只需将string中的每个字符替换为vector中的每个元素即可,因此相同的部分就不做过多介绍,这里附上string容器的访问操作函数的介绍以及vector容器的访问操作函数的网上详细定义;

  • 简单介绍
  • 详细定义
  • value_type* data() noexcept;:该函数返回对象的数组空间的首地址,拿到该地址就可以对数组元素进行访问修改操作;
  • const value_type* data() const noexcept;:如果调用该函数的对象是一个const修饰的,那么返回的空间地址只能进行读操作,而不能进行写操作;
内容操作
整体替换操作
  • 函数模板:将对象的内容替换为指定的迭代区间(也就是地址区间)的内容,该迭代区间是左闭右开的,右边的元素是拿不到的;
template <class InputIterator>
  void assign (InputIterator first, InputIterator last);
  • void assign (size_type n, const value_type& val);:将对象的内容替换为 n 个 val 值,如果是自定义类型,那么第二个参数就是构造函数(参数)
vector<int> first;
vector<int> second;
vector<int> third;
first.assign (7,100);             // 替换为7个int型内容,值为100
vector<int>::iterator it;
it=first.begin()+1;
second.assign (it,first.end()-1); // 替换为迭代区间的五个元素
int myints[] = {1776,7,4};
third.assign (myints,myints+3);   // 替换为地址区间的内容,迭代器的本质就是地址
尾插尾删
  • void push_back (const value_type& val);:尾插一个元素,赋初值为 val,如果是自定义类型,那么该参数就是构造函数(参数)
  • void pop_back();:尾删一个元素;
vector<int> myvector;
int sum = 0;
myvector.push_back (100);
myvector.push_back (200);
myvector.push_back (300);
while (!myvector.empty()){
	sum += myvector.back();
    myvector.pop_back();
}
//sum = 600
插入操作
  • iterator insert (iterator position, const value_type& val);:在迭代器的位置插入 一个元素 val,如果是自定义类型,那么第二个参数就为构造函数(参数),该函数将返回插入位置的迭代器;
  • void insert (iterator position, size_type n, const value_type& val);:在迭代器的位置插入 n 个元素 val,如果是自定义类型,那么第三个参数就为构造函数(参数),该函数将返回插入位置的迭代器;
  • 函数模板:将在对象的迭代器位置插入某一个迭代区间的元素;
template <class InputIterator>
    void insert (iterator position, InputIterator first, InputIterator last);
vector<int> myvector (3,100);
vector<int>::iterator it;
it = myvector.begin();
it = myvector.insert ( it , 200 );
myvector.insert (it,2,300);
it = myvector.begin();
vector<int> anothervector (2,400);
myvector.insert (it+2,anothervector.begin(),anothervector.end());
int myarray [] = { 501,502,503 };
myvector.insert (myvector.begin(), myarray, myarray+3);
删除操作
  • iterator erase (iterator position);:删除迭代器指向位置的元素;
  • iterator erase (iterator first, iterator last);:删除迭代区间范围内的元素;
vector<int> myvector;
// 放入1~10这10个元素
for (int i=1; i<=10; i++) myvector.push_back(i);
// 删除第六个元素
myvector.erase (myvector.begin()+5);
// 删除前三个元素
myvector.erase (myvector.begin(),myvector.begin()+3);
//4 5 7 8 9 10
成员交换函数
  • void swap (vector& x);:交换两个对象的内容;
vector<int> foo (3,100);   // 100,100,100
vector<int> bar (5,200);   // 200,200,200,200,200
foo.swap(bar);
//foo:200,200,200,200,200
//bar:100,100,100
清空函数
  • void clear();:清空有效元素个数,意味着 size 值赋为 0;
vector<int> myvector;
myvector.push_back (100);    //100
myvector.push_back (200);    //100,200
myvector.push_back (300);    //100,200,300
myvector.clear();
myvector.push_back (1101);    //1101
myvector.push_back (2202);    //2202
带构造的插入操作
  • 函数模板:在调用者迭代器指向位置插入一个元素,如果是内置类型就直接写入数据,如果是自定义类型就写入构造函数所需的参数;
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);
  • 函数模板:在调用者的尾部插入一个元素,如果是内置类型就直接写入数据,如果是自定义类型就写入构造函数所需的参数;
template <class... Args>
void emplace_back (Args&&... args);
struct A{
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
//内置类型
vector<int> myvector = {10,20,30};
auto it = myvector.emplace ( myvector.begin()+1, 100 );//10,100,20,30
myvector.emplace_back (100);                      //10,100,20,30,100
myvector.emplace_back (200);                      //10,100,20,30,100,200
//自定义类型
vector<A> a(4);
a.emplace_back (1, 2);      
a.emplace (a.begin()+1, 5, 6);//10,100,20,30
外部交换函数
  • 函数模板:交换两个vector对象;
template <class T, class Alloc>
void swap (vector<T,Alloc>& x, vector<T,Alloc>& y);
各种比较函数

迭代器失效

  关于vector容器的接口基本介绍完了,相比之string容器少了许多,主要是因为string中存放的是字符,而vector中存放的都是许多数据,因此用不到拼接、查找、匹配等接口,所以上述的接口基本上就能对vector对象灵活操作了,而下面我们就来谈谈迭代器失效的问题;

原因

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

那些操作会导致
  1. 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back 等;
#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v{1,2,3,4,5,6};
	auto it = v.begin();
	// 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容
	// v.resize(100, 8);
	// reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变
	// v.reserve(100);
	// 插入元素期间,可能会引起扩容,而导致原空间被释放
	// v.insert(v.begin(), 0);
	// v.push_back(8);
	// 给vector重新赋值,可能会引起底层容量改变
	// v.assign(100, 8);
	/*
	出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
	解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。
	*/
	while(it != v.begin()){
		cout << *it << " " ;
		++it;
	}
	cout << endl;
	return 0;
}
  1. 指定位置元素的删除操作——eraseerase删除 pos 位置元素后,pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么 pos 就失效了。因此删除vector中任意位置上元素时,vs 就认为该位置迭代器失效了,这是一种严格的判定,不过在 Linux 中使用erase操作是不会导致迭代器失效的,但是如果访问无权限空间仍会报错;
#include <iostream>
#include <vector>
using namespace std;
int main(){
	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));
	// 使用find模板函数查找3所在位置的iterator
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	// 删除pos位置的数据,导致pos迭代器失效。
	v.erase(pos);
	cout << *pos << endl; // 此处会导致非法访问
	return 0;
}
解决措施

  迭代器失效解决办法就是在使用前,对迭代器重新赋值即可,下面两个代码中使用了erase操作,然后在 Linux 下运行,虽然检测没问题,但是在运行时一个正确一个错误,大家可以来看看:

#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v{ 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0)
			//错误的,删除后仍直接使用迭代器,因此导致迭代器最终越界
			v.erase(it);
		++it;
	}
	return 0;
}

int main(){
	vector<int> v{ 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0)
			//正确,删除后给迭代器重新赋值
			it = v.erase(it);
		else
			++it;
	}
	return 0;
}

模拟实现

代码
使用memcpy拷贝问题

在这里插入图片描述

  • 原因:当我们vector容器中存放的是自定义类型时,如果进行插入操作需要增容时,是使用memcpy来实现的,那么在进行多次增容时,就会出现多次释放同一片空间的问题,具体如上图所示和下文讲解:
    • 第一次插入一个元素时,由于容量为零,需要开辟一个元素大小的空间来存放string对象,并且string产生了一号指向;
    • 第二次插入一个元素时,此时容量不够,需要再次增容,此时开辟两个元素大小的空间,首先将原来空间的string对象memcpy到新空间,再将原来的空间进行释放,此时会深度释放①号指向,然后将第二个元素插入;
    • 第三年瓷插入一个元素时,此时容量再次不够,再开辟四个元素大小的空间,首先将原来空间的string对象memcpy到新空间,再将原来的空间进行释放,此时会深度释放②号指向和③号指向,而②号指向和之前的①号指向都指向同一片空间,此处就会发生多次释放,程序崩溃;
  • 解决:因此我们在进行增容将原空间内容拷贝到新空间时,不能简单地使用memcpy来实现,而是使用循环赋值的操作将原空间内容赋值到新空间,因为赋值操作进行的是资源的深拷贝,因此不会出现上述问题;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值