STL コンテナ - ベクターのシミュレーション実装 (詳細なメモ付き)

1. ベクターコンテナとは何ですか?

C++ の STL (標準テンプレート ライブラリ) では、vector は動的配列コンテナーです。これは、固定サイズの配列と同様に、要素を保存してアクセスする方法を提供しますが、動的にサイズを変更することができます。

ベクター コンテナは実行時に必要に応じて自動的にサイズ変更でき、最後に要素をすばやく追加または削除できます。要素の挿入、削除、検索など、コンテナ内の要素にアクセスして操作するための一連のメンバー関数とイテレータを提供します。

以下は、いくつかのベクター コンテナーの特性と使用法です。

ベクトル内の要素はメモリに連続して保存され、添字を使用して要素にアクセスしたり変更したりできます。
コンテナのサイズは必要に応じて自動的に拡大または縮小でき、push_back や Pop_back などの関数を使用して最後に要素を簡単に追加または削除できます。
イテレータを使用すると、コンテナ内の要素を走査してアクセスできます。
Vector は、コンテナー内の要素の数を取得する size() や、コンテナーが空かどうかを確認する empty() など、いくつかのメンバー関数も提供します。
ベクター コンテナーは、可変サイズのデータ​​ コレクションを保存および操作するために C++ で非常に一般的に使用されており、さまざまなシナリオや要件に適用できます。

2番目に、ベクトルのシミュレーション実装

2.1 ベクトルのメンバ変数

Vector のメンバー変数は、_start、_finish、_endofstorage の 3 つのポインター変数です。_start はシーケンス テーブルの最初の要素の位置を指し、_finish は最後の有効な要素の次の位置を指し、_endofstorage は容量サイズの次の位置を指します。
ここに画像の説明を挿入

2.2 コンストラクター

コンストラクターはメンバー変数を初期化します。

2.2.1 引数のないコンストラクタ

引数なしのコンストラクターは 3 つのポインターを空に設定するだけでよく、関数を作成するときに特定のスペースを空けたり、要素を挿入するときにスペースを空けたりすることができます。

		//构造函数
		//这里的初始化列表的值可以在这里给,也可以在成员变量直接给默认值nullptr
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
    
    }

2.2.2 パラメータ付きコンストラクタ



		//有参构造函数
		// 
		//T()是T类型的匿名对象做缺省值,因为vector能存放任何类型的值,这里不能给0作为
		// 缺省值,因为整形int的缺省值是0,但是假如这里的T是string,那么给0做缺省值就
		// 不对了,所以这里是需要注意的,同样,如果成员变量处已经给了默认值,那么这里的
		//初始化列表初始化就可以不写了
		vector(size_t n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			//这里复用resize就能完成需求
			resize(n, value);
		}

		//有参构造
		//跟上面的构造函数构成函数重载,避免在调用时和下面的迭代器区间
		//构造函数产生歧义
		vector(int n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			resize(n, value);
		}

		//利用迭代器区间初始化的构造函数
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			//把这段迭代器区间的值一个一个地尾插即可
			while (first != last)
			{
    
    
				push_back(*first);
				++first;
			}
		}


ここに画像の説明を挿入

2.3 コピーコンストラクター

ここではメンバー変数の中にポインタ変数があるため、深いコピーと浅いコピーの問題に注意する必要があります。

		//拷贝构造(传统写法),注意一定要深拷贝,不然是会出现同一块空间被析构两次的情况的
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
    
    
			//开辟跟v一样大小的空间
			T* tmp = new T[v.capacity()];
			if (tmp)
			{
    
    
				//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
				// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
				// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
				// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
				// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
				// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
				// 解决了string的浅拷贝问题
				// 
				//不能用memcpy(tmp, v._start, sizeof(T) * v.size());
				for (size_t i = 0; i < v.size(); i++)
				{
    
    
					tmp[i] = v._start[i];
				}
				_start = tmp;
				_finish = _start + v.size();
				_endofstorage = _start + v.capacity();
			}
		}

2.4 代入オーバーロード関数

メンバ変数にポインタを持つ代入オーバーロード関数も、深いコピーと浅いコピーの問題に注意する必要があります。

		//赋值重载函数(现代写法)
		//利用传参调用拷贝构造得到想要的临时vector,再利用Swap函数
		//把这个拷贝构造好了的临时对象的成员变量交换给自己的成员变量即可完成赋值
		vector<T>& operator=(vector<T> v)
		{
    
    
			Swap(v);
			return *this;
		}

2.5 デストラクター

デストラクターの役割は、動的に割り当てられたリソースを解放してクリーンアップすることです。

		//析构函数
		~vector()
		{
    
    
			if (_start)
			{
    
    
				delete[] _start;
				_start = nullptr;
				_finish = nullptr;
				_endofstorage = nullptr;
			}
		}

2.6 リザーブ機能

リザーブ機能は、スペースのサイズを調整する機能であり、一般に容量拡張に使用されます。

		void reserve(size_t n)
		{
    
    
			if (n > capacity())
			{
    
    
				//需要先把size()记录下来,不然后面更新_finish的时候会出错
				//因为存在异地扩容,而size()=_finish-_start,更新_finish
				//的逻辑是_finish=_start+size(),size()=_finish-_start;
				//即_finish=_start+_finish-_start,所以_finish永远都是
				//nullptr的,所以要想后续正确地更新_finish,需要把原来的size()
				//用sz记录下来,然后_finish=_start+sz才是正确的
				size_t sz = size();

				T* tmp = new T[n];
				if (tmp && _start)
				{
    
    
					//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
					// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
					// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
					// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
					// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
					// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
					// 解决了string的浅拷贝问题
					// 
					//不能用memcpy(tmp, _start, sizeof(T) * size());
					for (size_t i = 0; i < sz; i++)
					{
    
    
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}

2.7 リサイズ機能

サイズ変更もスペースのサイズを調整する機能ですが、サイズ変更にはさらに多くの機能があり、デフォルト値をもたらすこともできます。

		void resize(size_t n, const T& value = T())
		{
    
    
			if (n <= size())
			{
    
    
				//如果n<=size(),直接更新_finish指针为最后一个元素的下一个位置即可
				_finish = _start + n;
			}
			else
			{
    
    
				//n>capacity()时,需要先扩容,reserve函数里面会判断是否需要
				//扩容,所以这里可以判断,也可以不判断都行
				reserve(n);
				//根据resize的性质,给_finish到_start+n这段区间填充value即可
				while (_finish != _start + n)
				{
    
    
					*_finish = value;
					++_finish;
				}
			}
		}

2.8 挿入関数

挿入関数は、特定の位置にデータを挿入するために使用される関数です。

		iterator insert(iterator pos, const T& x)
		{
    
    
			assert(pos >= _start && pos <= _finish);

			//满了就扩容
			if (_finish == _endofstorage)
			{
    
    
				//存在迭代器失效问题,需要记录pos相对于原来空间的位置,由于扩容后指向了
				//新的空间,造成原来的迭代器失效,需要更新pos找到pos在新的空间的相对位置
				size_t len = pos - _start;
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);

				//更新pos在新的空间的相对位置
				pos = _start + len;
			}
			//这里的insert就是顺序表的插入,就是把从pos位置开始往后的所有元素
			// 都往后移动一位,给pos腾出一个位置即可插入目标元素
			iterator end = _finish - 1;
			while (end >= pos)
			{
    
    
				*(end + 1) = *end;
				end--;
			}
			*pos = x;
			++_finish;

			//根据源码的要求需要返回插入的元素的迭代器
			return pos;
		}

2.9 消去機能

消去関数は、特定の位置の要素を削除する関数です。

		iterator erase(iterator pos)
		{
    
    
			//assert(pos >= begin() && pos < end());
			//iterator ret = pos;
			//iterator start = pos + 1;
			//while (start < end())
			//{
    
    
			//	*pos = *start;
			//	++pos;
			//	++start;
			//}
			//--_finish;
			//return ret;

			assert(pos >= begin() && pos < end());

			//顺序表的删除也非常简单,把pos后面的所有元素都往前挪动一位即可
			iterator start = pos + 1;
			while (start < end())
			{
    
    
				*(start - 1) = *start;
				++start;
			}
			--_finish;
			return pos;
		}

2.10 Push_back 関数と Pop_back 関数

Push_back関数とpop_back関数は、末尾の挿入と末尾の削除を行う関数です。挿入および消去機能は直接再利用できます。

		void push_back(const T& x)
		{
    
    
			//复用insert尾部插入x即可
			//end()是最后一个元素的下一个位置,刚好就是尾插的位置
			insert(end(), x);
		}

		void pop_back()
		{
    
    
			//end()是最后一个元素的下一个位置,所以尾删需要删除
			//end()的前一个位置,前置--是先--,再使用
			erase(--end());
		}

2.11 容量機能

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

2.12サイズ機能

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

2.13 演算子によるoperator[]関数のオーバーロード

		//operator[]返回的是pos位置的引用
		T& operator[](size_t pos)
		{
    
    
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
    
    
			assert(pos < size());
			return _start[pos];
		}

2.14 イテレータ

		//对于vector,vector里面存放的数据类型的指针就是iterator
		typedef T* iterator;
		typedef const T* const_iterator;
		
		iterator begin()
		{
    
    
			return _start;
		}

		iterator end()
		{
    
    
			return _finish;
		}

		const_iterator begin() const
		{
    
    
			return _start;
		}

		const_iterator end() const
		{
    
    
			return _finish;
		}

三、STLコンテナベクトルシミュレーション実装コードまとめ

#pragma once

#include <iostream>
using namespace std;
#include <assert.h>
#include <vector>

namespace kb
{
    
    
	template<class T>
	class vector
	{
    
    
		//对于vector,vector里面存放的数据类型的指针就是iterator
		typedef T* iterator;
		typedef const T* const_iterator;

	public:
		iterator begin()
		{
    
    
			return _start;
		}

		iterator end()
		{
    
    
			return _finish;
		}

		const_iterator begin() const
		{
    
    
			return _start;
		}

		const_iterator end() const
		{
    
    
			return _finish;
		}

		//构造函数
		//这里的初始化列表的值可以在这里给,也可以在成员变量直接给默认值nullptr
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
    
    }

		//有参构造函数
		// 
		//T()是T类型的匿名对象做缺省值,因为vector能存放任何类型的值,这里不能给0作为
		// 缺省值,因为整形int的缺省值是0,但是假如这里的T是string,那么给0做缺省值就
		// 不对了,所以这里是需要注意的,同样,如果成员变量处已经给了默认值,那么这里的
		//初始化列表初始化就可以不写了
		vector(size_t n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			//这里复用resize就能完成需求
			resize(n, value);
		}

		//有参构造
		//跟上面的构造函数构成函数重载,避免在调用时和下面的迭代器区间
		//构造函数产生歧义
		vector(int n, const T& value = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			resize(n, value);
		}

		//利用迭代器区间初始化的构造函数
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
    
    
			//把这段迭代器区间的值一个一个地尾插即可
			while (first != last)
			{
    
    
				push_back(*first);
				++first;
			}
		}
		
		//拷贝构造(传统写法),注意一定要深拷贝,不然是会出现同一块空间被析构两次的情况的
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
    
    
			//开辟跟v一样大小的空间
			T* tmp = new T[v.capacity()];
			if (tmp)
			{
    
    
				//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
				// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
				// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
				// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
				// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
				// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
				// 解决了string的浅拷贝问题
				// 
				//不能用memcpy(tmp, v._start, sizeof(T) * v.size());
				for (size_t i = 0; i < v.size(); i++)
				{
    
    
					tmp[i] = v._start[i];
				}
				_start = tmp;
				_finish = _start + v.size();
				_endofstorage = _start + v.capacity();
			}
		}

		void Swap(vector<T>& tmp)
		{
    
    
			std::swap(_start, tmp._start);
			std::swap(_finish, tmp._finish);
			std::swap(_endofstorage, tmp._endofstorage);
		}

		//赋值重载函数(现代写法)
		//利用传参调用拷贝构造得到想要的临时vector,再利用Swap函数
		//把这个拷贝构造好了的临时对象的成员变量交换给自己的成员变量即可完成赋值
		vector<T>& operator=(vector<T> v)
		{
    
    
			Swap(v);
			return *this;
		}

		//析构函数
		~vector()
		{
    
    
			if (_start)
			{
    
    
				delete[] _start;
				_start = nullptr;
				_finish = nullptr;
				_endofstorage = nullptr;
			}
		}

		void reserve(size_t n)
		{
    
    
			if (n > capacity())
			{
    
    
				//需要先把size()记录下来,不然后面更新_finish的时候会出错
				//因为存在异地扩容,而size()=_finish-_start,更新_finish
				//的逻辑是_finish=_start+size(),size()=_finish-_start;
				//即_finish=_start+_finish-_start,所以_finish永远都是
				//nullptr的,所以要想后续正确地更新_finish,需要把原来的size()
				//用sz记录下来,然后_finish=_start+sz才是正确的
				size_t sz = size();

				T* tmp = new T[n];
				if (tmp && _start)
				{
    
    
					//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
					// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
					// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
					// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
					// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
					// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
					// 解决了string的浅拷贝问题
					// 
					//不能用memcpy(tmp, _start, sizeof(T) * size());
					for (size_t i = 0; i < sz; i++)
					{
    
    
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}

		void resize(size_t n, const T& value = T())
		{
    
    
			if (n <= size())
			{
    
    
				//如果n<=size(),直接更新_finish指针为最后一个元素的下一个位置即可
				_finish = _start + n;
			}
			else
			{
    
    
				//n>capacity()时,需要先扩容,reserve函数里面会判断是否需要
				//扩容,所以这里可以判断,也可以不判断都行
				reserve(n);
				//根据resize的性质,给_finish到_start+n这段区间填充value即可
				while (_finish != _start + n)
				{
    
    
					*_finish = value;
					++_finish;
				}
			}
		}

		iterator insert(iterator pos, const T& x)
		{
    
    
			assert(pos >= _start && pos <= _finish);

			//满了就扩容
			if (_finish == _endofstorage)
			{
    
    
				//存在迭代器失效问题,需要记录pos相对于原来空间的位置,由于扩容后指向了
				//新的空间,造成原来的迭代器失效,需要更新pos找到pos在新的空间的相对位置
				size_t len = pos - _start;
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);

				//更新pos在新的空间的相对位置
				pos = _start + len;
			}
			//这里的insert就是顺序表的插入,就是把从pos位置开始往后的所有元素
			// 都往后移动一位,给pos腾出一个位置即可插入目标元素
			iterator end = _finish - 1;
			while (end >= pos)
			{
    
    
				*(end + 1) = *end;
				end--;
			}
			*pos = x;
			++_finish;

			//根据源码的要求需要返回插入的元素的迭代器
			return pos;
		}

		iterator erase(iterator pos)
		{
    
    
			//assert(pos >= begin() && pos < end());
			//iterator ret = pos;
			//iterator start = pos + 1;
			//while (start < end())
			//{
    
    
			//	*pos = *start;
			//	++pos;
			//	++start;
			//}
			//--_finish;
			//return ret;

			assert(pos >= begin() && pos < end());

			//顺序表的删除也非常简单,把pos后面的所有元素都往前挪动一位即可
			iterator start = pos + 1;
			while (start < end())
			{
    
    
				*(start - 1) = *start;
				++start;
			}
			--_finish;
			return pos;
		}

		void push_back(const T& x)
		{
    
    
			//复用insert尾部插入x即可
			//end()是最后一个元素的下一个位置,刚好就是尾插的位置
			insert(end(), x);
		}

		void pop_back()
		{
    
    
			//end()是最后一个元素的下一个位置,所以尾删需要删除
			//end()的前一个位置,前置--是先--,再使用
			erase(--end());
		}

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

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

		//operator[]返回的是pos位置的引用
		T& operator[](size_t pos)
		{
    
    
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
    
    
			assert(pos < size());
			return _start[pos];
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};

	void test1(void)
	{
    
    
		vector<int> v;
		//v.push_back(1);
		//v.push_back(2);
		//v.push_back(3);
		//v.push_back(4);
		//v.push_back(5);
		v.insert(v.begin(), 1);
		v.insert(v.begin(), 2);
		v.insert(v.begin(), 3);
		v.insert(v.begin(), 4);
		v.insert(v.begin(), 5);

		//v.insert(v.begin() + 3, 100);

		//for (const auto& e : v)
		//{
    
    
		//	cout << e << " ";
		//}
		//cout << endl;

		//kb::vector<int>::iterator it = v.erase(v.begin());
		//cout << *it << endl;

		size_t sz = v.size();
		for (size_t i = 0; i < sz; i++)
		{
    
    
			v.erase(v.begin());
		}


		for (size_t i = 0; i < v.size(); i++)
		{
    
    
			cout << v[i] << " ";
		}
		cout << endl;

	}

	void test2(void)
	{
    
    
		std::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		std::vector<int>::iterator ret = v.erase(v.begin());
		cout << *ret << endl;
		//for (const auto& e : v)
		//{
    
    
		//	cout << e << " ";
		//}
		//cout << endl;

	}

	void test3(void)
	{
    
    
		vector<int> v(5, 1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(2);
		v.insert(v.begin(), 9);

		vector<int> v1(v);

		for (const auto& e : v1)
		{
    
    
			cout << e << " ";
		}
		cout << endl;

		for (const auto& e : v1)
		{
    
    
			cout << e << " ";
		}
		cout << endl;
	}

	void test4(void)
	{
    
    
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		vector<int> v2(v1.begin(), v1.end());
		//v1.resize(6, 1);
		for (auto& e : v2)
		{
    
    
			cout << e << " ";
		}
		cout << endl;


	}
}

以上がSTLコンテナvectorのよく使われるインターフェースのシミュレーション実装の全内容ですが、実際vectorには多くのインターフェースがありますが、これらはよく使われるインターフェースであり、その他のあまり使われないインターフェースは実装されません。以上が今日私が皆さんにお伝えしたいことですが、学習できましたか?この記事が役に立った場合は、十分に注意して読んでください。今後も C++ の関連知識を更新していきます。また次号でお会いしましょう。

おすすめ

転載: blog.csdn.net/weixin_70056514/article/details/131740707