C++vector

概念:

vector是表示可变大小数组的序列容器。它跟数组一样,只不过它可以动态的改变数组的大小。

这里简单的看看vector的使用:

vector是个类模板,所以它的实例化跟类模板的实例化一样,要指定类型:

cpp 复制代码
vector<int> vt;
vt.push_back(1);
vt.push_back(2);
vt.push_back(3);
vt.push_back(4);

for (int i = 0; i < vt.size(); i++)//for循环
{
	cout << vt[i] << " ";
}
cout << endl;
vector<int>::iterator it = vt.begin();//迭代器
while (it != vt.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;
for (auto e : vt)//范围for
{
	cout << e << " ";
}
cout << endl;
return 0;

其实在学习了string类之后,有很多方法我们都会很熟练。

来看看它的构造函数:

这里介绍一下里面我们没见过的概念:

value-type:他其实就是那个模板类型的T

alloc:它是一个空间配置器,也就是内存池,这里我们使用它给的缺省值就行。

这里来看看都是怎么使用的:

cpp 复制代码
vector<int> v1;//默认构造
vector<int> v2(10, 1);//用10个1初始化
for (auto e : v2)
{
	cout << e << " ";
}
cout << endl;
vector<char> v3(10, 'x');//用十个字符x初始化
for (auto e : v3)
{
	cout << e << " ";
}
cout << endl;
vector<string> v4(10, "xxx");//用十个字符串xxx初始化
for (auto e : v4)
{
	cout << e << " ";
}
cout << endl;

string s("hello world");
vector<char>v5(s.begin(), s.end());//迭代器区间初始化
for (auto e : v5)
{
	cout << e << " ";
}
cout << endl;

如果我们指定的类型和用来初始化的类型不同的话,会有一个提升的:

cpp 复制代码
vector<int>v6(3, 'x');

这个时候会将char提升为整型:

然后我们也可以这样进行初始化:

cpp 复制代码
int a[] = { 1,3,16,20 };
vector<int> v1(a,a+4);

因为迭代器它是像指针一样的类型,可能是指针,也可能不是,在这里,a是首元素的地址,它的底层实现是指针,所以在这种情况下也是可以使用的。

那么通过上面的一些用法,我们来看看string和vector的区别:

1.string规定了字符串后面有一个隐藏的\0,而vector是没有的如果自己不插入的话

2.string的接口比vector的接口丰富

然后在一些算法当中,找到数据的方法其实就是迭代器:

这里介绍一下快排算法:
sort

可以看到快排的底层找数据就是用的迭代器,这里的comp参数,它的作用其实是让我们可以自己控制升降序。

我们来测试一下:

cpp 复制代码
int a[] = { 24,18,31,20 };
vector<int> v1(a, a + 4);
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;
sort(v1.begin(), v1.end());
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

然后呢,我们也可以通过greater类控制降序,less类控制升序:

具体使用:

cpp 复制代码
int a[] = { 24,18,31,20 };
vector<int> v1(a, a + 4);
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;
greater<int> gt;//控制降序
sort(v1.begin(), v1.end(),gt);
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

less<int> ls;//控制升序
sort(v1.begin(), v1.end(), ls);
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

然后我们再来看看扩容:

reserve:

我们来看这段代码:

cpp 复制代码
vector<int> v1;
v1.reserve(5);
for (size_t i = 0; i < 5; i++)
{
	v1[i] = i;
}
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

这段代码运行之后会中止,原因是什么:

首先,reserve它是只扩容,也就是改变capacity,而不会让size改变,也就是说这个时候的size是0,而我们的operator[]它有个强制检查:

assert(n<size)

而上面代码i=0和size相等,所以会强制检查,导致程序终止

解决方法是,我们可以使用resize,resize是开空间加初始化,所以size是不会等于零的,另一种是我们可以尾插给入数据

cpp 复制代码
vector<int> v1;
//v1.reserve(5);
v1.resize(5);
for (size_t i = 0; i < 5; i++)
{
	v1[i] = i;
}
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

vector<int> v1;
v1.reserve(5);
for (size_t i = 0; i < 5; i++)
{
	v1.push_back(i);
}
for (auto e : v1)
{
	cout << e << " ";
}
cout << endl;

由于之前详细的学习过string类和string类的模拟实现,所以接下来我将用模拟实现的方式来介绍一下vector的底层实现:

首先我们先来看看stl库里面的vector的成员变量:

cpp 复制代码
iterator start;
iterator finish;
iterator end_of_storage;

可以看到,vector不同于我们的string类,它是用迭代器作为它的一个成员变量的,因为vector他是管理任意类型的数组,所以他的迭代器肯定跟数组是有关的

从上面这张图可以看出,这些成员变量是可以用来计算我们的元素个数和容量大小的,具体方法就是以前学习过的指针的相减。

从这里我们也就知道了这些成员变量的含义:

start:指向数组的首元素

finish:指向最后一个元素的下一个位置

end_of_storage:容量

接下来我们来实现一下它的构造和析构:

首先要声明和定义一下模板:

cpp 复制代码
template<class T>
class vector
{
public:
	typedef T* iterator;
private:
	iterator _start;
	iterator _finish;
	iterator _end_of_storage;
};

构造和析构

cpp 复制代码
vector()//构造函数初始化
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_finish(nullptr)
{

}

~vector()
{
	if (_start)
	{
		delete[] _start;//因为这是数组,开辟了多个空间,所以要用delete[]
		_start=_finish=_end_of_storage=nullptr;
	}
}

迭代器失效问题:

迭代器其底层实际就是一个指针,或者是对指针进行了封装(这个情况在list下有体现)

迭代器失效情况在VS的检查下有两种,一种是野指针问题,一种是位置挪动问题

1.野指针问题,如改变空间,导致迭代器指向的空间被销毁,那么迭代器就会变成野指针,导致迭代器失效

2.位置改变,比如我们执行erase函数,如果迭代器指向的是最后一个位置,那么删除最后一个位置,迭代器就处于end位置,这个时候是没有元素的,那么迭代器就失效了,因此在vs的检查中,迭代器位置改变,那么编译器就会强制报错。
解决方法就是使用前对迭代器重新赋值

用insert和erase来看看迭代器失效的解决:
insert:

cpp 复制代码
iterator insert(iterator it, const T& val)
{
	if (it >= _start && it <= _finish)
	{
		if (_finish == _end_of_storage)
		{
			size_t len = it - _start;//记录迭代器和新空间_start的相对位置
			reserve(capacity() == 0 ? 4 : capacity() * 2);


			it = len + _start;//得到新空间迭代器的位置
		}
		iterator end = _finish;
		while (end != it)
		{
			*end = *(end-1);
			end--;
		}
		*it = val;
		++_finish;
	}
	return it;
}

如果我们不对迭代器的位置进行重新赋值,那么程序就会出错。insert就是野指针问题,而下面说到的erase则是位置问题
erase:

cpp 复制代码
iterator erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);
	
	iterator it = pos;
		while (it!= _finish - 1)
		{
			*it = *(it + 1);
			it++;
		}
		_finish--;
	
	return pos;
}

先看它的返回值:

这个意思是返回被删除元素的下一个元素,它在被删除元素删除之后所在的新位置。

比如

我们要删除3,所以返回的是3的下一个元素在3被删除之后的新位置,4在3被删除之后的新位置其实就是原来3的位置;所以上面的代码我们直接返回pos。

深拷贝中的浅拷贝:

memcpy的一些问题:

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

这是什么意思呢:

cpp 复制代码
space::vector<string> v;
v.push_back("111111");
v.push_back("222222");
v.push_back("333333");
v.push_back("444444");
v.push_back("555555");
for (auto e : v)
{
	cout << e << " ";
}
cout << endl; 

这段代码运行会出错,原因是,我们扩容时,memcpy是浅拷贝,会将string对象进行值拷贝,这样拷贝出来的string对象中的_str指针和原来的string对象中的str指针指向同一空间,这样析构就会对同一片空间析构两次,导致程序崩溃

解决方法:

cpp 复制代码
void reserve(size_t n)
{
	assert(n > capacity());
	size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * sz);
			for (size_t i = 0; i < sz; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		
		_start = tmp;
		_finish = sz + _start;
		_end_of_storage =_start + n;
}

_start[i]和tmp[i]解引用之后是string,然后这里就是string对象的赋值,string的赋值是深拷贝,就解决了这个问题。

补充:c++在有了模板之后,对内置类型进行了升级,使得内置类型也有了构造:

cpp 复制代码
vector(size_t n, const T& val = T())
{
	resize(n, val);

}

所以在这里我们用n个数据来初始化,这里T可能是string,vector,也可能是内置类型,所以我们采用匿名对象来作为缺省值。

迭代器区间初始化。

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

为什么加模板,是因为这个迭代器不一定只是vector类型的迭代器,也可以是string的等等。

相关推荐
花千树-0102 小时前
MCP + Function Calling:让模型自主驱动工具链完成多步推理
java·agent·react·mcp·toolcall·harness·j-langchain
Benszen2 小时前
Linux容器:轻量级虚拟化革命
java·linux·运维
凸头2 小时前
Lombok 包底层浅析
java
不懂的浪漫2 小时前
mqtt-plus 架构解析(三):Payload 序列化与反序列化,为什么要拆成两条链
java·spring boot·物联网·mqtt·架构
2401_892070982 小时前
顺序栈(动态数组实现) 超详细解析(C++ 语言 + 可直接运行)
数据结构·c++·顺序栈
卷福同学2 小时前
去掉手机APP开屏广告,李跳跳2.2下载使用
java·后端·算法
漫霂2 小时前
二叉树的翻转
java·数据结构·算法
语戚2 小时前
力扣 51. N 皇后:基础回溯、布尔数组优化、位运算全解(Java 实现)
java·算法·leetcode·力扣·剪枝·回溯·位运算
程序猿阿越2 小时前
Kafka4源码(三)Share Group共享组
java·后端·源码阅读