ベクトルの使用とシミュレーション実装

目次

1. ベクターの導入と利用

1.ベクトルの紹介

2.ベクターの利用

1. ベクトルの定義

 2.ベクトル反復子の使用

3. ベクトル空間の成長問題

4.ベクターの追加、削除、確認、修正

3.ベクトル反復子の失敗問題(ポイント)

1. 基礎となる空間に変化をもたらす操作

2. 指定位置要素の削除操作 --erase

3. Linux で g++ コンパイラがイテレータを処理する方法。

2. ベクトルの詳細な分析とシミュレーションの実装

1. std::vector のコア フレームワーク インターフェイスのシミュレートされた実装

2. memcpyを使用して問題をコピーする

3. 動的二次元配列の理解



1. ベクターの導入と利用

1.ベクトルの紹介

1. Vector は、可変サイズの配列を表すシーケンス コンテナーです。
2. 配列と同様に、ベクトルも要素を格納するために連続的な記憶領域を使用します。これは、添字を使用して、配列と同じくらい効率的にVector の要素にアクセスできることを意味します。ただし、配列とは異なり、そのサイズは動的に変更でき、そのサイズはコンテナーによって自動的に処理されます。
3. 基本的に、vector は動的に割り当てられた配列を使用して要素を格納します新しい要素が挿入されると、記憶領域を増やすために配列のサイズを変更する必要があります。これは、新しい配列を割り当て、すべての要素をこの配列に移動することによって行われます。新しい要素がコンテナに追加されるたびにベクトルのサイズが変更されるわけではないため、これは時間の点で比較的高価なタスクです。
4. ベクトル割り当てスペース戦略:ストレージ スペースが実際に必要なストレージ スペースよりも大きいため、ベクトルは増加の可能性を考慮して追加のスペースを割り当てます。ライブラリーが異なれば、スペースの使用量と再割り当てをトレードオフするために異なる戦略が使用されます。ただし、いずれの場合も、最後の要素の挿入が一定時間内に完了するように、再割り当ての間隔サイズは対数的に増加する必要があります。
5. したがって、Vector は、ストレージ スペースを管理し、効率的な方法で動的に拡張する機能を得るために、より多くのストレージ スペースを占有します。
6. 他の動的シーケンス コンテナ (deque、list、forward_list) と比較して、vector は要素にアクセスする際の効率が高く、最後の要素の追加と削除も比較的効率的です最後ではない他の削除および挿入操作の場合、効率はさらに低くなります。統合されたイテレータと参照は、list や forward_list よりも優れています。

2.ベクターの利用

ベクトルを学習するときは、ドキュメントを確認することを学ぶ必要があります:ベクトル ドキュメントの紹介、ベクトルは実際には非常に重要です。実際には、一般的なインターフェイスに精通しているだけで済みます。

1. ベクトルの定義

 2.ベクトル反復子の使用

 

 注: すべての反復子の範囲は左閉および右開であり、型が一致する限り、ベクトル反復子だけでなく、他の型の反復子も渡すことができます。

以下はコードのデモです

void Print(const vector<int>& v)
{
	// const对象使用const迭代器进行遍历打印
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

3. ベクトル空間の成長問題

 

1. 容量コードを vs および g++ で実行すると、 vs では容量が 1.5 倍、g++ では 2 倍増加することがわかります。具体的な成長量は、特定のニーズに基づいて定義されます。vs は PJ バージョン STL、g++ は SGI バージョン STL です。
2.サイズを変更すると、スペースを開くときに初期化も行われるため、サイズに影響します
3.リザーブはスペースを開くことのみを担当します。どれくらいのスペースが必要かが確実にわかっている場合、リザーブはベクトル展開のコストの問題を軽減できます。
// 如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够
// 就可以避免边插入边扩容导致效率低下的问题了
void TestVector()
{
     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';
     }
     }
}

 テストの結果、事前にスペースを空けており、定員が100人になっていることが判明した。

4.ベクターの追加、削除、確認、変更

 重要な関数インターフェイスパラメータ

void push_back (const value_type& val);

void pop_back();

template <class InputIterator, class T>   
InputIterator find (InputIterator first, InputIterator last, const T& val);

iterator insert (iterator position, const value_type& val);
void insert (iterator position, size_type n, const value_type& val);

iterator erase (iterator position);iterator erase (iterator first, iterator last);

3.ベクトル反復子の失敗問題(ポイント)

イテレータの使用は特に広く普及しています。イテレータの主な機能は、アルゴリズムが基礎となるデータ構造を考慮しないようにすることです。基礎となる層は実際には ポインタ であるか、ポインタカプセル化されています。たとえば、ベクトルのイテレータはオリジナルです。ポインタ T*。したがって、イテレータが失敗すると、実際にはイテレータの下部にある対応するポインタが指すスペースが破壊されることを意味し、解放されたスペースを使用するとプログラムがクラッシュすることになります(つまり、期限切れのイテレータを使用すると、プログラムがクラッシュする可能性があります)。

1.基礎となる空間に変化をもたらす操作

たとえば、サイズ変更、予約、挿入、割り当て、プッシュバックなどにより、イテレータが失敗する可能性があります。

#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);
  while(it != v.end())
  {
  cout<< *it << " " ;
  ++it;
  }
  cout<<endl;
  return 0;
}
エラーの理由: 上記の操作によりベクトルが拡張される可能性があります。これは、ベクトルの基本的な原則として、古いスペースが解放され、印刷時にリリース間の古いスペースが引き続き使用されることを意味します。itイテレーターを操作するとき、実際の動作は公開されている作品です
スペースが含まれるため、実行時にコードがクラッシュする原因になります。
解決策: 上記の操作が完了した後、反復子を介してベクトル内の要素を操作し続ける場合は、反復子を再割り当てするだけで済みます。

2.指定位置の要素の削除操作 - -erase

#include <iostream>
using namespace std;
#include <vector>
int main()
{
 int a[] = { 1, 2, 3, 4 };
 vector<int> v(a, a + sizeof(a) / sizeof(int));
 // 使用find查找3所在位置的iterator
 vector<int>::iterator pos = find(v.begin(), v.end(), 3);
 // 删除pos位置的数据,导致pos迭代器失效。
 v.erase(pos);
 cout << *pos << endl; // 此处会导致非法访问
 return 0;
}
Erase が pos 位置の要素を削除した後、pos 位置の後の要素は、基になる空間に変更を引き起こすことなく前方に移動されます。理論的には、反復子は失敗しませんが、pos が削除後、たまたま最後の要素だった場合、pos は次のようになります。たまたま終了位置であり、終了位置に要素がない場合、pos は無効です。したがって、ベクトル内の任意の位置にある要素を削除すると、 vs はその位置にある反復子を無効であると見なします。

次のコードの機能は、ベクトル内のすべての偶数を削除することです。どのコードが正しいのでしょうか?またその理由は何ですか?
#include <iostream>
using namespace std;
#include <vector>
int main()
{
 vector<int> v{ 1, 2, 3, 4 };
 auto it = v.begin();
 while (it != v.end())
 {
 if (*it % 2 == 0)
 v.erase(it);
 ++it;
 } 
 return 0;
}


int main()
{
 vector<int> v{ 1, 2, 3, 4 };
 auto it = v.begin();
 while (it != v.end())
 {
 if (*it % 2 == 0)
 it = v.erase(it);    //返回一个迭代器,指向删除数据的下一个位置
 else
 ++it;
 }
 return 0;
}

最初のコードは間違っており、反復子が無効になり、その削除ロジックも間違っています。上記のコードを例にとると、プログラムが「2」を削除すると、pos の位置が「3」になり、次に it++ となり、イテレータは 4 を指し、3 の判定は外れ、最後の 1 つは偶数の 4 の場合は削除します。後でイテレータが _finish を超えると、== v.end() にはなりません。

3. Linux で g++ コンパイラがイテレータを処理する方法。

// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
 vector<int> v{1,2,3,4,5};
 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;
}
输出:
扩容之前,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;
}

上記の 3 つの例からわかるように、Linux では、g++ コンパイラは反復子の失敗の検出においてそれほど厳密ではなく、処理は SGI STL の場合ほど極端ではありません。反復子が失敗した後、コードは必ずしもしかし、実行結果は明らかに間違っており、begin と end の範囲内にない場合は確実にクラッシュします。

イテレータの失敗に対する解決策: 使用する前にイテレータを再割り当てしてください。

2.ベクトルの詳細な分析とシミュレーションの実装

 

1. std::vector のコア フレームワーク インターフェイスのシミュレートされた実装

#pragma once

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


namespace Kevin
{
	template<class T>
	class vector
	{
	public:
		// Vector的迭代器是一个原生指针
		typedef T* iterator;
		typedef const T* const_iterator;

		///
		// 构造和销毁
		vector()
			: _start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{}

		vector(size_t n, const T& value = T())
			: _start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(value);
			}
		}

		/*
		* 理论上将,提供了vector(size_t n, const T& value = T())之后
		* vector(int n, const T& value = T())就不需要提供了,但是对于:
		* vector<int> v(10, 5);
		* 编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型
		* 就不会走vector(size_t n, const T& value = T())这个构造方法,
		* 最终选择的是:vector(InputIterator first, InputIterator last)
		* 因为编译器觉得区间构造两个参数类型一致,因此编译器就会将InputIterator实例化为int
		* 但是10和5根本不是一个区间,编译时就报错了
		* 故需要增加该构造方法
		*/
		vector(int n, const T& value = T())
			: _start(new T[n])
			, _finish(_start+n)
			, _endOfStorage(_finish)
		{
			for (int i = 0; i < n; ++i)
			{
				_start[i] = value;
			}
		}

		// 若使用iterator做迭代器,会导致初始化的迭代器区间[first,last)只能是vector的迭代器
		// 重新声明迭代器,迭代器区间[first,last)可以是任意容器的迭代器
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		vector(const vector<T>& v)
			: _start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)
		{
			reserve(v.capacity());
			iterator it = begin();
			const_iterator vit = v.cbegin();
			while (vit != v.cend())
			{
				*it++ = *vit++;
			}
			_finish = it;
		}

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

		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _endOfStorage = nullptr;
			}
		}

		/
		// 迭代器相关
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator cbegin() const
		{
			return _start;
		}

		const_iterator cend() const
		{
			return _finish;
		}

		//
		// 容量相关
		size_t size() const 
		{ 
			return _finish - _start; 
		}

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

		bool empty() const 
		{ 
			return _start == _finish; 
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldSize = size();
				// 1. 开辟新空间
				T* tmp = new T[n];

				// 2. 拷贝元素
		        // 这里直接使用memcpy会有问题吗?同学们思考下
		        //if (_start)
		        //	memcpy(tmp, _start, sizeof(T)*size);

				if (_start)
				{
					for (size_t i = 0; i < oldSize; ++i)
						tmp[i] = _start[i];

					// 3. 释放旧空间
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + oldSize;
				_endOfStorage = _start + n;
			}
		}

		void resize(size_t n, const T& value = T())
		{
			// 1.如果n小于当前的size,则数据个数缩小到n
			if (n <= size())
			{
				_finish = _start + n;
				return;
			}

			// 2.空间不够则增容
			if (n > capacity())
				reserve(n);

			// 3.将size扩大到n
			iterator it = _finish;
			_finish = _start + n;
			while (it != _finish)
			{
				*it = value;
				++it;
			}
		}

		///
		// 元素访问
		T& operator[](size_t pos) 
		{ 
			assert(pos < size());
			return _start[pos]; 
		}

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

		T& front()
		{
			return *_start;
		}

		const T& front()const
		{
			return *_start;
		}

		T& back()
		{
			return *(_finish - 1);
		}

		const T& back()const
		{
			return *(_finish - 1);
		}
		/
		// vector的修改操作
		void push_back(const T& x) 
		{ 
			insert(end(), x); 
		}

		void pop_back() 
		{ 
			erase(end() - 1); 
		}

		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endOfStorage, v._endOfStorage);
		}

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

			// 空间不够先进行增容
			if (_finish == _endOfStorage)
			{
				//size_t size = size();
				size_t newCapacity = (0 == capacity()) ? 1 : capacity() * 2;
				reserve(newCapacity);

				// 如果发生了增容,需要重置pos
				pos = _start + size();
			}

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

			*pos = x;
			++_finish;
			return pos;
		}

		// 返回删除数据的下一个数据
		// 方便解决:一边遍历一边删除的迭代器失效问题
		iterator erase(iterator pos)
		{
			// 挪动数据进行删除
			iterator begin = pos + 1;
			while (begin != _finish) {
				*(begin - 1) = *begin;
				++begin;
			}

			--_finish;
			return pos;
		}
	private:
		iterator _start;		// 指向数据块的开始
		iterator _finish;		// 指向有效数据的尾
		iterator _endOfStorage;  // 指向存储容量的尾
	};
}

2. memcpyを使用して問題をコピーする

int main()
{
 bite::vector<bite::string> v;
 v.push_back("1111");
 v.push_back("2222");
 v.push_back("3333");
 return 0;
}

シミュレーションで実装したベクター内のリザーブインターフェイスをmemcpyでコピーしたと仮定した場合、上記のコードで問題はありますか?

 

「2222」を挿入するには、新しいスペースを開く必要があります。

 

memcpy はメモリのバイナリ形式のコピーで、あるメモリ空間の内容を別のメモリ空間にそのままコピーします。

カスタム タイプ要素をコピーする場合、memcpy は効率的でエラーが発生しません。ただし、カスタム タイプ要素をコピーする場合、カスタム タイプ要素にリソース管理が含まれる場合、memcpy のコピーは実際には浅いコピーであるため、エラーが発生します。

 結論: オブジェクトにリソース管理が関係している場合、memcpy は浅いコピーであるため、オブジェクト間のコピーに memcpy を使用してはなりません。そうしないと、メモリ リークやプログラムのクラッシュが発生する可能性があります。

3.動的二次元配列の理解

vector<vector<int>> vv(n);
vv 動的 2 次元配列 を構築します。 vvには 合計 n 個の 要素があります。各要素は ベクトル 型です。各行には要素は含まれません。 n 5の場合 、次のようになります。

要素の充填が完了すると、次のようになります。

標準ライブラリの ベクトルを使用して 動的 2 次元配列を構築すると、実際には上の図と一致します。

おすすめ

転載: blog.csdn.net/weixin_65592314/article/details/129446007