【C++】vector类

vector

1、vector的介绍

  • vector是数组大小可变化的序列容器
  • 就像数组一样,vector使用连续的存储空间来存储元素 ,可以采用下标对vector的元素进行访问,他的大小是可以动态改变
  • vector可能会分配一些额外的存储空间以适应可能的生长,因此实际容量可能大于严格容纳其元素所需的容量
  • 与数组相比,vector消耗更多内存,以换取高效管理和动态增长的能力
  • 与其他动态序列容器(deques, lists and forward_lists )相比,向量访问其元素非常高效,对于涉及在非末尾位置插入或移除元素的操作,它们的性能不如其他操作,且迭代器和引用的一致性不如list和forward_lists。

2、vector的使用

2.1 vector的定义

(constructor) 构造函数声明 接口说明
vector() (重点) 无参构造
vector (size_type n, const value_type& val = value_type()) 构造并初始化n个val
vector (const vector& x); (重点) 拷贝构造
vector (InputIterator first, InputIterator last); 使用迭代器进行初始化构造

size_type表示无符号整形,value是第一个模板参数,使用迭代器区间的构造函数是函数模板

c 复制代码
vector<int> first;
vector<int> second (4,100); 
vector<int> third (second.begin(),second.end()); 
vector<int> fourth (third); 

int myints[] = {16,2,77,29};
vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int));

cout << "The contents of fifth are:";
  for (vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
    cout << ' ' << *it;
  cout << '\n';

2.2 vector iterator

iterator 的使用 接口说明
begin + end(重点) 获取第一个数据位置的 iterator/const_iterator,获取最后一个数据的下一个位置的 iterator/const_iterator
rbegin + rend 获取最后一个数据位置的 reverse_iterator,获取第一个数据前一个位置的 reverse_iterator

迭代器遍历:

c 复制代码
void Print(vector<int>& v)
{
	vector<int>::iterator it = v.begin();
	while (it != v.end()) 
	{
		cout << *it << endl;
		++it;
	}
	cout << endl;
}

2.3 vector空间增长问题

容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize(重点) 改变 vector 的 size
reserve(重点) 改变 vector 的 capacity
  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。vs是PJ版本STL,g++是SGI版本STL。
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
  • resize在开空间的同时还会进行初始化,影响size。
c 复制代码
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	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';
		}
	}
}

在VS下的输出结果:

making foo grow:

capacity changed: 1

capacity changed: 2

capacity changed: 3

capacity changed: 4

capacity changed: 6

capacity changed: 9

capacity changed: 13

capacity changed: 19

capacity changed: 28

capacity changed: 42

capacity changed: 63

capacity changed: 94

capacity changed: 141

在Linux下的结果:

making v grow:

capacity changed: 1

capacity changed: 2

capacity changed: 4

capacity changed: 8

capacity changed: 16

capacity changed: 32

capacity changed: 64

capacity changed: 128

由于VS在扩容时按大约1.5倍扩容,所以如果知道vector中存储元素的大概个数,可以提前将空间设置足够,避免边插入边扩容导致效率低下

c 复制代码
void TestVectorExpandOP()
{
	vector<int> v;
	size_t sz = v.capacity();
	v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
	cout << "making bar 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';
		}
	}
}

2.4 vector增删查改

vector 增删查改 接口说明
push_back(重点) 尾插
pop_back(重点) 尾删
find 查找。(注意这个是算法模块实现,不是 vector 的成员接口)
insert 在 position 之前插入 val
erase 删除 position 位置的数据
swap 交换两个 vector 的数据空间
operator[](重点) 像数组一样访问

注意:reserve只改变vector的capacity,不改变size

eg:

c 复制代码
int main() {
    vector<int> v;

    // 一开始:size=0,capacity=0
    cout << "初始:size=" << v.size() << " capacity=" << v.capacity() << endl;

    // 调用 reserve(100) ------ 只改容量
    v.reserve(100);

    // 重点:size 还是 0!capacity 变成 100
    cout << "reserve(100)后:size=" << v.size() << " capacity=" << v.capacity() << endl;

    return 0;
}

正确做法是把reserve改成resize,resize会改变size的大小,如果使用reserve,插入数据时需要使用push_back

2.5 vector和string的区别

  1. vector插入时结尾没有\0,string插入后结尾有\0
  2. string可以实现+=一个字符串,vector一次只能插入一个字符串

3、vector的模拟实现

3.1 成员变量

c 复制代码
public:
		typedef T* iterator;
		typedef const T* const_iterator;
private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;

3.2 成员函数

3.2.1 构造函数和析构函数

c 复制代码
vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{}

vector(size_t n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	resize(n, val);
}

vector(int n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	resize(n, val);
}

vector(initializer_list<T> il)
	: _start(nullptr)
  , _finish(nullptr)
  , _endofstorage(nullptr)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

template <class InputIterator>
vector (InputIterator first, InputIterator last)
{
	while (first != last)
	{
			push_back(*first);
			++first;
	}
}

//强制编译器生成默认构造
vector() = default;

//析构函数
~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _endofstorage = nullptr;
	}
}
  • 迭代器区间初始化采用的是函数模板,可能会有不同类型的迭代器
  • 必须要提供vector(int n, const T& val = T()),如果没有这个构造函数,vector v(10, 20)就会选择最匹配的**vector(size_t n, const T& val = T())**构造函数,但10被认为是int型,和size_t匹配不上,因此会和迭代器区间初始化函数进行匹配,不符合逻辑,所以要对int型单独提供一个构造函数
  • vector(initializer_list il)构造函数作用是可以用vector v = {1,2,3,4};这种花括号方式初始化对象,il就是接受这串数据的变量名,最后用尾插把元素x添加到vector的末尾
  • vector() = default;是强制编译器生成默认构造

3.2.2 拷贝构造

c 复制代码
vector(initializer_list<T> il)
	: _start(nullptr)
  , _finish(nullptr)
  , _endofstorage(nullptr)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

vector(const vector& v)作用是用一个已有的vector对象v来创建一个新的vector对象,这里涉及深拷贝

  • 浅拷贝是只复制指针地址,不复制实际数据,修改时会互相影响
  • 深拷贝不仅复制指针,还复制指针指向的全部数据,给新对象开辟一块独立的内存空间,修改时互不影响

3.2.3 operator=

c 复制代码
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

// v1 = v7
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

3.2.4 size

c 复制代码
size_t size() const
{
	return _finish - _start;
}

3.2.5 capacity

c 复制代码
size_t capacity() const
{
	return _endofstorage - _start;
}

3.2.6 迭代器

c 复制代码
iterator begin()
{
	return _start; 
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

加const后,迭代器只读,不会意外修改const对象

3.2.7 reserve

c 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		// 拷贝旧空间数据到新空间  迭代器失效
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * old_size);
			delete[] _start;
		}

		_start = tmp;   // 更新_start(更新新内存的起始位置)
		_finish = _start + old_size;
		_endofstorage = _start + n;
	}
}

注意

  • 关于old_size:因为拷贝旧空间后被释放,所以需要重新设置_start的位置,同时需要更新_finish和_endofstorage,更新_start后,_start指向新空间的开头,而_finish指向旧空间的结尾,此时调用size()计算出的结果有问题,所以需要先将旧空间的size()保存一份
  • 关于拷贝数据的方式:memcpy是逐字节的拷贝数据,是浅拷贝,当执行delete[] _start;销毁空间时,会影响到新空间,所以用for循环实现深拷贝

修改后:

c 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		// 拷贝旧空间数据到新空间  迭代器失效
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * old_size);
			for (size_t i = 0; i < old_size; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		_start = tmp;   // 更新_start(更新新内存的起始位置)
		_finish = _start + old_size;
		_endofstorage = _start + n;
	}
}

修改后执行tmp[i] = _start[i];会调用赋值运算符重载进行深拷贝

3.2.8 resize

c 复制代码
void resize(size_t n, T val = T())
{
	if (n > size())
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	else
	{
		_finish = _start + n;
	}
}

3.2.9 operator[]

c 复制代码
T& operator[](size_t i)
{
	assert(i < size());

	return _start[i];
}

const T& operator[](size_t i) const  //只读
{
	assert(i < size());

	return _start[i];
}

3.2.10 insert(迭代器失效问题)

c 复制代码
iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	//检查容量
	if (_finish == _endofstorage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		// 更新pos
		pos = _start + len;
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = x;
	++_finish;

	return pos;
}

再进行insert操作时,会引发迭代器失效,需要将pos重新移动位置,在pos位置插入一个数据时,pos是一个迭代器,首先检查容量,进行扩容,在reserve后,_start、_finish、_endofstorage都指向了新空间,旧空间被释放,而pos指向的还是原来空间的某个位置,此时pos成为野指针,造成非法访问,为了解决这个问题,可以先保存pos的相对位置,扩容后再更新pos

3.2.11 erase(迭代器失效问题)

c 复制代码
iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	// 挪动数据
	iterator it = pos + 1;
	while (it != _finish)
	{
		*(it - 1) = *it;
		++it;
	}

	--_finish;

	return pos;
}

erase删除pos位置的元素后,导致缩容,pos位置之后的元素会往前移动,使pos指向已释放的空间,使迭代器失效,因此采用返回值的方式返回pos位置下一个位置的元素

3.2.12 push_back

c 复制代码
void push_back(const T& x)
{
	if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = x;
	++_finish;
}

先检查扩容,再插入元素,最后移动指针

3.2.13 pop_back

c 复制代码
void pop_back()
{
	//assert(_finish > _start);
	assert(!empty());

	--_finish;
}

3.2.14 clear

c 复制代码
void clear()
{
	_finish = _start;
}

3.2.15 empty

c 复制代码
bool empty() const
{
	return _start == _finish;
}

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
jf加菲猫2 小时前
第15章 文件和目录
开发语言·c++·qt·ui
思麟呀2 小时前
Select多路转接
linux·网络·c++·网络协议·http
aq55356002 小时前
开源吐槽大会:让技术痛点变笑点
c++·mfc
t***5442 小时前
如何在 Dev-C++ 中切换编译器至 Clang
开发语言·c++
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:线段覆盖
c++·算法·贪心·csp·信奥赛·区间贪心·线段覆盖
CoderCodingNo3 小时前
【信奥业余科普】C++ 的奇妙之旅 | 14:程序的分叉路口——逻辑判断与 if-else 语句
开发语言·c++
The Chosen One9853 小时前
a进制转b进制的转换总结
开发语言·c++
tankeven3 小时前
C++ 学习杂记05:std::map
c++
Magic@3 小时前
Redis学习[1] ——基本概念和数据类型
linux·开发语言·数据库·c++·redis·学习