C++基础入门:vector深度解析(七千字深度剖析)

◆博主名称:少司府

欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆

数据结构系列个人专栏:

初阶数据结构_少司府的博客-CSDN博客

C++基础个人专栏:

C++初阶_少司府的博客-CSDN博客

⭐琢玉成器终有时,笔底生花夺锦归

目录

[一、vector 相关介绍](#一、vector 相关介绍)

[1.1 vector 的介绍](#1.1 vector 的介绍)

[1.2 vector 的基本使用](#1.2 vector 的基本使用)

[二、capacity 相关接口](#二、capacity 相关接口)

[2.1 reserve()](#2.1 reserve())

[2.2 resize()](#2.2 resize())

[2.3 vs中的vector扩容](#2.3 vs中的vector扩容)

[三、modifiers 相关接口](#三、modifiers 相关接口)

[3.1 push_back 和 insert](#3.1 push_back 和 insert)

[3.2 自定义类型作为顺序表中元素](#3.2 自定义类型作为顺序表中元素)

[四、vector 模拟实现](#四、vector 模拟实现)

[4.1 基本架构](#4.1 基本架构)

[4.2 构造、析构、赋值运算符](#4.2 构造、析构、赋值运算符)

[4.3 begin、end、reserve、resize](#4.3 begin、end、reserve、resize)

[4.4 size、capacity、empty、push_back](#4.4 size、capacity、empty、push_back)

[4.5 print_vector 和 print_container](#4.5 print_vector 和 print_container)

[4.6 insert、erase、[]](#4.6 insert、erase、[])

[4.7 迭代器失效](#4.7 迭代器失效)

[4.8 内置类型的显示实例化](#4.8 内置类型的显示实例化)


一、vector 相关介绍

1.1 vector 的介绍

vector的英文是"向量"的意思,在C++中,我们可以利用vector创建类似于顺序表的数据结构。其主要是通过模板来实现的。

我们可以看看官方的相关文档如何介绍vector的:

vector 的文档介绍

无论是在学习vector、string还是其他相关容器的时候,我们都要学会看文档来帮助我们学习。

1.2 vector 的基本使用

vector本质属于类模板,因此,我们需要使用类模板的相关的实例化方式。

cpp 复制代码
void test_vector1()
{
	vector<int> v1;
	vector<int> v2(10, 1); // 10个数据,全为1
	vector<int> v3(++v2.begin(), --v2.end());

	for (int i = 0; i < v3.size(); i++) cout << v3[i] << " ";
	cout << endl;
	vector<int>::iterator it = v3.begin();
	while (it != v3.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	for (auto v : v3) cout << v << " ";
}

如图,和string一样,vector也有迭代器,也有范围for,以及其他相关的 begin、end、push_back、insert等相关接口。

我们实例化v1,v1没有数据;实例化v2,第一个参数是数据个数,第二个是具体数据;实例化v3,传入v2的迭代器,左闭右开,有8个1。

打印出来看看:

如图,打印v3中的8个1。

二、capacity 相关接口

2.1 reserve()
cpp 复制代码
	vector<int> v(10, 1);
	v.reserve(20);
	cout << v.size() << endl;
	cout << v.capacity() << endl;

	v.reserve(15);
	cout << v.size() << endl;
	cout << v.capacity() << endl;

	v.reserve(5);  
	cout << v.size() << endl;
	cout << v.capacity() << endl; 
	cout << endl;

如图,reserve的用法和string类中基本保持一致,需要注意的是:与string不一样的是,reserve 不能缩容改变capacity!

如图,v.reserve(5) 既不会删除数据,也不会缩容。

2.2 resize()
cpp 复制代码
	v.resize(5);
	cout << v.size() << endl;
	cout << v.capacity() << endl; // <size,不缩容,但删除数据,默认保留前5个
	for (auto V : v) cout << V << ' ';
	cout << endl;

	v.resize(10, 2);
	cout << v.size() << endl;
	cout << v.capacity() << endl; // >size && <capacity ,尾插数据
	for (auto V : v) cout << V << ' ';
	cout << endl;

	v.resize(25, 3);
	cout << v.size() << endl;
	cout << v.capacity() << endl; // >size && >capacity,扩容并插入数据
	for (auto V : v) cout << V << ' ';
	cout << endl;

	v.resize(30);
	cout << v.size() << endl;
	cout << v.capacity() << endl; // 不给数据的话会直接初始化为0
	for (auto V : v) cout << V << ' ';
	cout << endl;

如图,第一个resize(5)就是删除数据,只保留前5个。

第二个resize(10,2) 是保留10个数据空间,>size 的话就尾插5个2。

第三个 > capacity 的话会扩容。

没有给数据默认插入0。

结果如图。

2.3 vs中的vector扩容
cpp 复制代码
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	//v.reserve(100);
	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';
		}
	}
}

如图,这是在调试状态下单个TestVectorExpand() 函数缓慢扩容的状态,大约是1.5倍扩容的样子。

把它封装在测试函数中,编译器就会优化它的扩容。

三、modifiers 相关接口

3.1 push_back 和 insert
cpp 复制代码
void test_vector3()
{
	vector<int> v(10, 1);
	v.push_back(2);
	v.insert(v.begin(), 0);
	for (auto e : v) cout << e << ' ';
	cout << endl;

	v.insert(v.begin() + 3, 3); // 第一个参数只能是迭代器,不支持下标
	for (auto e : v) cout << e << ' ';
	cout << endl;
	// [] erase clear的用法和string中的类似
}

如图,这两者的用法和string中基本一样。

需要注意的是,insert 只能支持传迭代器,不支持下标。

3.2 自定义类型作为顺序表中元素
cpp 复制代码
void test_vector4()
{
	vector<string> v1;
	string s1("xxxx");
	v1.push_back(s1);

	v1.push_back("yyyy"); // 隐式类型转换

	// 本质是二维数组
	// 10*5
	vector<int> v(5, 1);
	vector<vector<int>> vv(10, v);
	vv[2][1] = 2;
	vv.operator[](2).operator[](1) = 3; // 两个[] 调用的不是同一个函数,不一样
	for (int i = 0; i < vv.size(); i++)
	{
		for (int j = 0; j < vv[i].size(); j++)
		{
			cout << vv[i][j] << ' ';
		}
		cout << endl;
	}
}

如图,vector中存放的是string类的对象。

1)、除了内置类型外,vector顺序表中还能存放内置类型,例如string

2)、vector中存放vector的话,相当于是二维数组的实现。

四、vector 模拟实现

4.1 基本架构

如图,与string不一样的一点是,vector的模拟实现需要用到类模板。

如图,成员变量中,_start 指向起始位置,_finish 指向最后一个数据的下一个位置,_end_of_storage 指向容量的下一个位置。

4.2 构造、析构、赋值运算符
cpp 复制代码
// 默认构造
vector()
{ }

如图是默认构造的写法,中间不需要处理任何数据。

C++11 中提供了强制生成默认构造的写法:

cpp 复制代码
	// C++11 强制生成默认构造写法
	vector()=default;
cpp 复制代码
	vector(const vector<T>& v)
	{
		reserve(v.size());
		for (auto& e : v)
		{
			push_back(e);
		}
	}

如图是拷贝构造,直接范围for赋值插入就行。

新 vector 有独立的内存空间,和原 vector 完全分离,修改新对象不会影响原对象,因此是深拷贝

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

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

如图,赋值运算符重载方式和string一模一样。

cpp 复制代码
	// 类模板的成员函数,还可以是函数模板
	template<class InputIterator>
	vector(InputIterator first, InputIterator last) // 可以用任意类型的迭代器初始化
	{
		while (first != last)
		{
			push_back(*first);
			++first;
		}
	}

如图,可以在类模板中通过模板函数来实现迭代器区间构造。

cpp 复制代码
	~vector()
	{
		// 销毁所有元素
		for (auto& e : *this)
		{
			e.~T();
		}
		// 释放内存
		delete[] _start;
	    // 指针清空
		_start = _finish = _end_of_storage = nullptr;
	}

如图,析构函数释放内存时,必须显示调用每个元素的析构。

最后,再delete释放内存并让指针置空。

4.3 begin、end、reserve、resize
cpp 复制代码
	iterator begin()
	{
		return _start;
	}

	const_iterator begin() const
	{
		return _start;
	}

	iterator end()
	{
		return _finish;
	}

	const_iterator end() const
	{
		return _finish;
	}

如图,分别是普通版本和const版本的begin和end。

且,我们发现,实现迭代器的时候大多数左闭右开的情况。

cpp 复制代码
	void reserve(size_t n)
	{
		if (n > capacity())
		{
			size_t old_size = size();
			T* tmp = new T[n];
			// memcpy(tmp,_start,size()*sizeof(T)) 浅拷贝
			for (size_t i = 0; i < old_size; i++) // 深拷贝
			{
				tmp[i] = _start[i]; // 库里的string的=会自己在末尾加上\0,string自己深拷贝
			}

			delete[] _start;
			_start = tmp;
			_finish = _start + old_size;
			_end_of_storage = _start + n;
		}
	}

如图是reserve实现深拷贝的逻辑。和拷贝构造一样,for循环直接实现深拷贝就好。需要注意的是:拷贝完成之后更新指针,_start、_finish 和 _end_of_storage 指向新空间。

cpp 复制代码
void resize(size_t n, T val = T())
{
    if (n < size())
    {
        for (T* it = _start + n; it != _finish; ++it)
        {
            it->~T();  // 显式调用顺序表内单个元素的析构
        }
        _finish = _start + n;
    }
    else
    {
        reserve(n);
        while (_finish < _start + n)
        {
            // ✅ 正确:定位new,在指定内存构造对象
            new(_finish) T(val);  
            _finish++;
        }
    }
}

如图是resize的实现,缺省参数的传入实现了resize(n) 版本和n个val版本。T()是匿名对象。

利用定位new来实现对已经分配好的内存空间的初始化

4.4 size、capacity、empty、push_back
cpp 复制代码
	size_t size() const
	{
		return _finish - _start;
	}

	size_t capacity()
	{
		return _end_of_storage - _start;
	}

如图,左闭右开返回个数。

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

判空逻辑简单,判断是否是**_start==_finish**就行。

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

push_back 实现逻辑和string中的push_back几乎一模一样。

如图是下标+[] 的遍历和范围for的遍历。

cpp 复制代码
	template<class T>
	void print_vector(const vector<T>& v)
	{
		// 不能从没有实例化的类模板里面取东西,编译器无法确定const_iterator是类型还是静态成员变量
		// typename 指明类型
		typename vector<T>::const_iterator it = v.begin();
		// 可以直接写 auto it = v.begin();
		while (it != v.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
		for (auto e : v) cout << e << " ";
		cout << endl;
	}

如图,我们不能从没有实例化的类模板里面取东西,编译器无法确定const_iterator是类型还是静态成员变量,需要用到 typename 指明类型。

当然,也可以直接用 auto 推导类型。

cpp 复制代码
	template<class Container>
	void print_container(const Container& v)
	{
		auto it = v.begin();
		while (it != v.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}

如图代码可以打印任意类型的容器。用模板来推导对应容器类型生成相应代码。

4.6 insert、erase、[]
cpp 复制代码
	T& operator[](size_t n)
	{
		assert(n < size());
		return *(_start + n);
	}

如图,_start 是原生指针,不需要operator *

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

		// 扩容导致迭代器失效,本质就是野指针
		if (_finish == _end_of_storage)
		{
			size_t len = pos - _start; // 迭代器失效,pos指向旧空间
			reserve(capacity() == 0 ? 4 : capacity() * 2);
			pos = _start + len;
		}

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

		*(pos) = x;
		_finish++;

		return pos;
	}

如图,扩容之后会导致指向原空间的迭代器pos失效,因此扩容之后需要更新pos。

插入完之后返回更新之后的迭代器pos。

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

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

		return pos + 1;
	}

如图,erase删除之后返回原空间原pos位置后一个位置

4.7 迭代器失效
cpp 复制代码
	void test_vector2()
	{
		vector<int> v;
		//v.push_back(1);
		v.insert(v.begin(), 2);
		v.insert(v.begin() + 1, 3);
		v.insert(v.begin() + 2, 4);
		print_vector(v);

		int x;
		cin >> x;
		auto pos = find(v.begin(), v.end(), x); // C++规定传迭代器范围要求左闭右开
		if (pos != v.end())
		{
			pos = v.insert(pos, 30); 
			// v.insert(pos,30);
			// (*pos) *= 10; 
			(*(pos + 1)) *= 10; 
		}
		print_vector(v);
	}

注意:

1)、C++规定传迭代器范围要求左闭右开。

2)、对于 v.insert(pos,30); 和 (*pos) *= 10; 这两句:

如果扩容,迭代器失效,insert之后形参修正pos,实参pos失效,不要访问,错误写法!!

如果不扩容,也算一种迭代器失效,pos位置意义变了,不再指向原来的数据,不要访问!

不扩容的话,这句在vs下会挂,Linux下不会。

3)、库里面insert实现了返回新pos的功能。

结果如图。

4.8 内置类型的显示实例化
cpp 复制代码
	int i = int(); // 内置类型也可以构造函数实例化的写法
	int j = int(1);
	int k(2);
	printf("%d %d %d\n", i, j, k);

如图,内置类型也可以显示实例化。

int() 没传参就是默认构造0。

本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~

您的三连是我持续更新的动力~

相关推荐
yqcoder1 小时前
突破性能瓶颈:深入理解 JavaScript TypedArray
java·开发语言·javascript
yqcoder1 小时前
JS 中的“空”之双雄:null vs undefined
开发语言·前端·javascript
ch.ju1 小时前
Java Programming Chapter 3——Traversal of array
java·开发语言
he___H1 小时前
子串----
java·数据结构·算法·leetcode
计算机安禾1 小时前
【c++面向对象编程】第8篇:const成员与mutable:常对象与常函数
开发语言·javascript·c++
七月稻草人1 小时前
DailyTxT 私人日记服务:NAS + Docker 部署,数据完全本地存储
运维·docker·容器
9命怪猫2 小时前
[K8S小白问题集] - runtime是不是类似Ghost装机后OS的运行
云原生·容器·kubernetes
Cat_Rocky2 小时前
k8s-特殊容器
云原生·容器·kubernetes
Dlrb12112 小时前
C语言-函数传参
c语言·数据结构·算法