STL--vector

发布于:2024-04-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

【本节目标】

1.vector的介绍及使用

1.1 vector的介绍

1.2 vector的使用及底层模拟实现

vector类中成员变量

1.2.1 vector的定义

1.2.2 vector iterator 的使用

1.2.3 vector 空间增长问题

1.2.3 vector 增删查改

1.2.4 vector 迭代器失效问题

1.2.5 使用memcpy拷贝问题


【本节目标】

1.vector的介绍及使用
2.vector深度剖析及模拟实现

1.vector的介绍及使用

1.1 vector的介绍

  1. vector是一个可变大小数组的序列容器
  2. 就如同数组一样,vector也采用的是连续存储空间来存储元素;又不同于数组,他的大小会被容器自动处理,大小会自动改变
  3. 本质上,vector使用动态分配数组来存储元素,新元素插入时,vector会重新分配一个内存更大的新数组,而将之前内存的元素都转移到这个数组中,释放之前的数组空间
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大
  5. vector占用更多的存储空间,为了获得管理存储空间的能力,以一种有效方式动态增长
  6. vector在访问元素时会更加高效,尾删和尾插元素相对高效

1.2 vector的使用及底层模拟实现

vector类中成员变量

template<class T>
class vector
{
    public:
	...
    ...

    private:
    T* _start;    //指向数组内存的开始
    T* _finish;   //指向数组内存结束的下一个
    T* _endofstorage;    //数组内存当前的最大容量
}

1.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); 使用迭代器进行初始化构造

无参构造 vector()

vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
{}

 构造并且初始化n个val  vector(size_type n, const value_type& val = value_type())

//初始化一定数量的相同数据
vector(size_t n, const T& val=T())
{
	//扩容
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

//重载函数解决 vector<int> v1(10, 1);的调用错误
vector(int n, const T& val = T())
{
	//扩容
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

由于用上面第一个函数时,用如下的代码,可能会导致寻址错误,会与使用迭代器进行初始化构造相冲突,导致调用构造的时候会调用与使用迭代器进行初始化构造

vector<int> v1(10, 1);

由于类型转换,则会两次类型转换,与使用迭代器进行初始化构造,会只用依次匹配成功,则会选用与使用迭代器进行初始化构造

而下面代码则无影响

vector<int> v2(10, 1);
print_vector(v2);

vector<char> v3(10, 'a');
print_vector(v3);

因而要解决寻址错误,则会直接重载一个会冲突的那个类型的函数

拷贝构造 vector (const vector& x)

//拷贝构造
//v1(v3)
vector(const vector<T>& v)
{
	//扩容
	reserve(v.capacity());
	//对于不确定的类型T,加上&
	for (auto& e : v)
	{
		push_back(e);
	}
}

对于不确定的类型迭代器要加上&,将遍历v中数据插入到this中

使用迭代器进行初始化构造 vector (InputIterator first, InputIterator last)

//如果写iterator的话,会只允许vector的迭代器初始化
//类模板的成员函数是函数模板
//部分初始化
template<class Inputiterator>
vector(Inputiterator first, Inputiterator end)
{
	while (first != end)
	{
		push_back(*first);
		first++;
	}
}

类模板的成员函数时函数模板

 C++11中初始化数组

//C++11大括号初始化数组
 vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10 };
vector(initializer_list<T> il)
{
	//扩容
	reserve(il.size());

	//运用initializer_list 中的迭代器
	for (auto e : il)
	{
		push_back(e);
	}
}

ininializer_list<T>类中可以实现大括号初始化数组,数组中的元素可以任意添加

// 隐式类型转换+优化
//vector<int> v1 = { 1,2,3,4,5,6,7,8,9,10 };
vector<int> v1{ 1,2,3,4,5,6,7,8,9,10 };

在C++11中也有这样的初始化方式

int i = 1;
// C++11,都可以实现
int j = { 1 };
int k{ 1 };

认识既可 

1.2.2 vector iterator 的使用

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

 iterator迭代器实现begin,end

typedef T* iterator;
typedef const T* const_iterator;

//普通迭代器
iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

//const迭代器
const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

迭代器的作用类似于指针,我这里以指针为例

1.2.3 vector 空间增长问题

容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize(重点) 改变vector的size
reserve (重点) 改变vector的capacity

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

 获取数据个数 size()

//大小
size_t size() const
{
	return _finish - _start;
}

获取容量大小 capacity()

size_t capacity() const
{
    return _endofstorage - _start;
}

const函数,指明this指向的对象数据不会被修改,并且const对象和普通对象都可以使用

 判断是否为空 empty()

//判断是否为空
bool empty()
{
	return _start == _finish;
}

改变vector的size resize()

//这里的T()指的是对象的默认构造,T是谁就是神的默认构造的值,int()=0
void resize(size_t n,const T& val=T())
{
	if (n > capacity())
	{
		//扩容
		reserve(n);
		//插入
		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}

	}
	else
	{
		//删除
		_finish = _start + n;
	}

}

用if判断容量是否大于自身,从而判断是否需要扩容

resize函数的第二个参数的缺省值是用来初始化的,这个值由于不知道T的类型,用一个匿名对象T()来构造成功

改变vector的capacity reserve()

//扩容
void reserve(size_t n)
{
	if (n > capacity())
	{
		T *tmp = new T[n];	//开辟n个空间
		size_t old_size = size();//防止开辟内存时,_start更新以后,_finish与之不在同一段空间,提前拷贝
		//如果是string类型的vector扩容时,会导致string浅拷贝,两个内容会指向同一份空间
		//memcpy(tmp, _start, sizeof(T)*size());//将_start原有的数据内存空间拷贝到tmp中
		for (size_t i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;

		_start = tmp;
		_finish = _start + old_size;
		_endofstorage = _start + n;
	}
}

如果是一个类类型的vector,如string时,开辟空间时可能会产生浅拷贝,memcpy导致的,memcpy是将内存的数据拷贝一份到另一段内存,但即两个内存的对象都指向同一个字符空间

1.2.3 vector 增删查改

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

尾插 push_back(const T& val)

//尾插
void push_back(const T& val)
{
	//扩容
	if (_finish == _endofstorage)
	{
		//扩容2倍
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = val;
	_finish++;
}

尾删 pop_back()

//尾删
void pop_back()
{
	//判断是否为空
	assert(!empty());
	--_finish;
}

要注意判断是否为空,空时删除无效

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

//插入
//指针pos
void insert(iterator pos,const T& val)
{
	assert(pos >= _start);
	assert(pos < _finish);

	//判断扩容
	
	if (_finish == _endofstorage)
	{
		//扩容会导致_start会指向另一段新空间,而pos没有改变到新空间,还是在就空间里
		//计算pos的位置
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		//更新pos
		pos = _start + len;
	}
	
	//插入
	//由于这里是指针,就不用在意和string产生的无符号整数会无限循环
	iterator it = _finish - 1;
	while (it >= pos)
	{
		*(it + 1) = *it;
		it--;
	}

	*pos = val;
	++_finish;
}

这里的iterator指的是前面T*,typedef T* iterator

使用时

v2.insert(v2.begin(), 11.11);
print_vector(v2);

删除position位置的数据 erase(iterator pos)

//删除
void erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator it = pos + 1;
	while (it < _finish)
	{
		*it = *it + 1;
		it++;
	}
	
	_finish--;
}

注意要判断pos指向的位置是否在_start和_finish之间,删除时,是将pos之后的位置的数据向前移动覆盖之前的数据

交换两个vector的数据空间 swap(const vector<T>& v)

//交换
void swap(const vector<T>& v)
{
	std::swap(this->_start, v._start);
	std::swap(this->_finish, v._finish);
	std::swap(this->_endofstorage, v._endofstorage);
}

赋值重载 vector<T> operator=(vector<T> v)

//赋值重载
//v1=v3
//v3的拷贝赋值给v1,保证不改变v3的值
vector<T> operator=(vector<T> v)
{
	//内存不够,扩容
	reserve(v.capacity());
	swap(v);
}

赋值重载时,要v3的值给v1,由于v3的改变不能导致v1的改变,所以参数是值传递的拷贝

 像数组一样访问 T operator[](size_t pos)

//运算符重载
T& operator[](size_t pos)
{
	assert(pos >= 0);
	
	return _start[pos];
}

const T& operator[](size_t pos) const
{
	assert(pos >= 0);

	return _start[pos];
}

将const类型对象和普通对象分开来定义使用

1.2.4 vector 迭代器失效问题

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

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

#include <iostream>
using namespace std;
#include <vector>
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.end())
{
cout<< *it << " " ;
++it;
}
cout<<endl;
return 0;
}

insert导致的迭代器失效

由于insert扩容时,迭代器it还在指向着原本的空间,所以迭代器失效,而需要更新迭代器

2. 指定位置元素的删除操作--erase

(1)这是正常的

(2)结果出错

由于erase是不断的向前覆盖由于每次it++则会跳过数据删除,导致迭代器错误

3.程序崩溃

这个是由于数据覆盖导致了访问越界了

更新迭代器 

vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(4);
v1.push_back(5);
//v1.push_back(4);

// 删除偶数 -- 迭代器失效以后,不要直接使用,如果要使用按规则重新更新后使用
//std::vector<int>::iterator it = v1.begin();
vector<int>::iterator it = v1.begin();

// 21:15
//cout << typeid(it).name() << endl;
while (it != v1.end())
{
	if (*it % 2 == 0)
	{
		it = v1.erase(it);
	}
	else
	{
		++it;
	}
}

 3. 注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端

// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
vector<int> v{1,2,3,4,5};
for(size_t i = 0; i < v.size(); ++i)
cout << v[i] << " ";
cout << endl;
auto it = v.begin();
cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
// 通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效
v.reserve(100);
cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
// 经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux下不会
// 虽然可能运行,但是输出的结果是不对的
while(it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
程序输出:
1 2 3 4 5
扩容之前,vector的容量为: 5
扩容之后,vector的容量为: 100
0 2 3 4 5 409 1 2 3 4 5
// 2. erase删除任意位置代码后,linux下迭代器并没有失效
// 因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
#include <vector>
#include <algorithm>
int main()
{
vector<int> v{1,2,3,4,5};
vector<int>::iterator it = find(v.begin(), v.end(), 3);
v.erase(it);
cout << *it << endl;
while(it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
程序可以正常运行,并打印:
4 4
5
// 3: erase删除的迭代器如果是最后一个元素,删除之后it已经超过end
// 此时迭代器是无效的,++it导致程序崩溃
int main()
{
vector<int> v{1,2,3,4,5};
// vector<int> v{1,2,3,4,5,6};
auto it = v.begin();
while(it != v.end())
{
if(*it % 2 == 0)
v.erase(it);
++it;
}
for(auto e : v)
cout << e << " ";
cout << endl;
return 0;
}
========================================================
// 使用第一组数据时,程序可以运行
[sly@VM-0-3-centos 20220114]$ g++ testVector.cpp -std=c++11
[sly@VM-0-3-centos 20220114]$ ./a.out
1 3 5
=========================================================
// 使用第二组数据时,程序最终会崩溃
[sly@VM-0-3-centos 20220114]$ vim testVector.cpp
[sly@VM-0-3-centos 20220114]$ g++ testVector.cpp -std=c++11
[sly@VM-0-3-centos 20220114]$ ./a.out
Segmentation fault

从上述三个例子中可以看到:SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的
 

 4. 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效

#include <string>
void TestString()
{
string s("hello");
auto it = s.begin();
// 放开之后代码会崩溃,因为resize到20会string会进行扩容
// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
// 后序打印时,再访问it指向的空间程序就会崩溃
//s.resize(20, '!');
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
it = s.begin();
while (it != s.end())
{
it = s.erase(it);
// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
// it位置的迭代器就失效了
// s.erase(it);
++it;
}
}

迭代器失效解决办法:在使用前,对迭代器重新赋值即可

1.2.5 使用memcpy拷贝问题

reserve中memcpy拷贝问题

//扩容
void reserve(size_t n)
{
	if (n > capacity())
	{
		T *tmp = new T[n];	//开辟n个空间
		size_t old_size = size();//防止开辟内存时,_start更新以后,_finish与之不在同一段空间,提前拷贝
		//如果是string类型的vector扩容时,会导致string浅拷贝,两个内容会指向同一份空间
		memcpy(tmp, _start, sizeof(T)*size());//将_start原有的数据内存空间拷贝到tmp中
		

		_start = tmp;
		_finish = _start + old_size;
		_endofstorage = _start + n;
	}
}
int main()
{
bite::vector<bite::string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}

问题分析:
1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃