[C++ Grocery Store] ベクターの基礎となる実装を調べる

ここに画像の説明を挿入

1.STL

1.1 STLとは何ですか?

STL (標準テンプレート ライブラリ - 標準テンプレート ライブラリ): C++ 標準ライブラリの一部であり、再利用可能なコンポーネント ライブラリであるだけでなく、データ構造とアルゴリズムを含むソフトウェア フレームワークでもあります。

ここに画像の説明を挿入

1.2 STLのバージョン

  • オリジナル バージョン: Hewlett-Packard Labs の Alexander Stepanov と Meng Lee によって完成されたバージョン オープン ソースの精神に基づき、これらのコードは誰でも無償で使用、コピー、変更、配布、商業利用できると宣言されています。唯一の条件は、オリジナルバージョンと同様にオープンソースとして使用する必要があることです。HP バージョンはすべての STL の祖先です。

  • PJ バージョン: PJ Plauger によって開発され、HP バージョンから継承され、Microsoft によって採用されました (Windows Visual C++)。公開または変更できません。欠点: 可読性が比較的低く、シンボルの命名が奇妙です。

  • RW版:ルージュ・ウェイジ社が開発し、HP版を継承。C++Builder で採用されているため、公開または変更することはできず、可読性は平均的です。

  • SGI バージョン: Silicon Graphics Computer Systems, Inc によって開発され、HP バージョンから継承されています。GCC (Linux) で採用されており、移植性が高く、公開、改変、販売も可能であり、命名スタイルやプログラミングスタイルの観点からも非常に読みやすいです。STL を学習する過程で、このバージョンのソース コードを参照することをお勧めします。

1.3 STL の 6 つの主要コンポーネント

ここに画像の説明を挿入

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

2.1 ベクトルの概要

  • ベクトルは、可変サイズの配列を表すシーケンス コンテナーです。

  • 配列と同様に、ベクトルは要素を格納するために連続したストレージを使用します。これは、添字を使用してベクトルの要素にアクセスできることを意味し、配列処理と同じくらい効率的です。ただし、配列とは異なり、そのサイズは動的に変更でき、そのサイズはコンテナーによって自動的に処理されます。

  • 基本的に、vector は動的に割り当てられた配列を使用して要素を格納します。新しい要素が挿入されると、記憶領域を増やすために配列のサイズを変更する必要があります。これは、新しい配列を割り当て、すべての要素をこの配列に移動することによって行われます。新しい要素がコンテナに追加されるたびにベクトルがサイズを再割り当てするわけではないため、これは時間の点で比較的高価なタスクです。

  • ベクター スペース割り当て戦略: ベクターは、増加の可能性に対応するために追加のスペースを割り当てるため、ストレージ スペース (容量) は実際に必要なストレージ スペースよりも大きくなります。ライブラリーが異なれば、スペースの使用量と再割り当てを比較検討するために異なる戦略が採用されます。ただし、いずれの場合も、再割り当ては間隔サイズが対数的に増加する必要があるため、最後に要素を挿入するときに一定の時間計算量で実行されます。

  • したがって、Vector は、ストレージ スペースを管理し、効率的な方法で動的に拡張する機能を得るために、より多くのストレージ スペースを占有します。

  • 他の動的シーケンス コンテナー (deque、list、forward_list など) と比較して、vector は要素にアクセスする際の効率が高く、最後の要素の追加と削除も比較的効率的です。最後ではない他の削除および挿入操作の場合、効率は低くなります。

2.2 ベクターの使用

Vector を学習するときは、ドキュメントの表示方法を学習する必要があります: Vector 、 Vector のドキュメントの紹介は、実際には非常に重要です。実際には、一般的に使用されるインターフェイスに精通するだけで十分です。マスターする必要があるインターフェイスは次のとおりです。 。

2.2.1 ベクトルの定義

コンストラクター宣言 インターフェースの説明
ベクター() パラメータ構築なし
ベクトル(サイズタイプ n, const 値タイプ& val = 値タイプ()) n 個の値を構築して初期化する
ベクトル(定数ベクトル& x) コピー構築
ベクトル(最初に入力反復子、最後に入力反復子) イテレータ範囲を使用して構築を初期化する

ちょっとしたヒント: size_type は符号なし整数型を表し、value_type は最初のテンプレート パラメーターであり、保存されるデータ型です。イテレータ範囲を使用するコンストラクタは関数テンプレートであり、Input 型を満たすイテレータであればこのコンストラクタを使用できます。

int TestVector1()
{
    
    
    vector<int> first;                                
    vector<int> second(4, 100);                       
    vector<int> third(second.begin(), second.end());  
    vector<int> fourth(third);                       

    int myints[] = {
    
     16,2,77,29 };
    vector<int> fifth(myints, myints + sizeof(myints) / sizeof(int));

    cout << "The contents of fifth are:";
    for (vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
        cout << ' ' << *it;
    cout << '\n';

    return 0;
}

2.2.2 ベクトル反復子

イテレータの使用 インターフェースの説明
開始 + 終了 最初のデータ位置の iterator/const_iterator を取得、最後のデータの次の位置の iterator/const_iterator を取得
r開始+終了 最後のデータ位置の reverse_iterator を取得、最初のデータの前の位置の reverse_iterator を取得

ここに画像の説明を挿入

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

2.2.3 ベクトル空間の成長問題

容量スペース インターフェースの説明
サイズ() データ数を取得する
容量() 容量のサイズを取得する
空の() 空かどうかを判断する
サイズ変更(size_type n); サイズ変更 (size_type n、const value_type& val) ベクトルのサイズを変更する
リザーブ(サイズタイプn) ベクターの容量を変更する
  • vs と g++ では拡張機構が異なり、vs では容量が 1.5 倍、g++ では 2 倍になります。vs は PJ バージョン STL、g++ は SGI バージョン STL です。

  • リザーブはスペースを空けることのみを担当します。必要なスペースがどのくらいかわかっている場合、リザーブはベクトル容量拡張のコスト上の欠点を軽減できます。

  • サイズ変更もスペースを開くときに初期化され、メンバー変数 _size に影響します。

void TestVectorExpand()
{
    
    
    size_t sz;
    vector<int> v;
    sz = v.capacity();
    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';
        }
    }
}

VS での結果:
ここに画像の説明を挿入
Linux での結果:
ここに画像の説明を挿入
ヒント: ベクトルに格納するおおよその要素数が決まっている場合は、挿入時の拡張による非効率の問題を回避するために、事前に十分なスペースを設定できます。

void TestVectorExpandOP()
{
    
    
    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';
        }
    }
}

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

ベクトルの追加、削除、確認、変更 インターフェースの説明
プッシュバック テールプラグ
ポップバック 末尾削除
探す Find (これはアルゴリズム モジュールの実装であり、vector のメンバー インターフェイスではありません)
入れる 位置の前に val を挿入します
消す 位置のデータを削除します
スワップ 2 つのベクトルのデータ空間を交換する
演算子[ ] 配列のようにアクセスし、アサーションでチェックし、例外をスローして実行します
//经典的错误
void Testerro()
{
    
    
    vector<int> v1;
    v1.reserve(10);
    for (size_t i = 0; i < 10; i++)
    {
    
    
        v1[i] = i;
    }
}

: 上記のコードは v1 に対してあらかじめ 10 個のスペースを空けていますが、v1 の有効な要素の数はまだ 0、つまり v1.size() の戻り値が 0 であるため、添字を使用して直接アクセスすることはできません。ベクトル オブジェクト内の要素。operator[ ] の実装の最初のステップは、境界外アクセスを防ぐために添え字の合理性をチェックし、assert(pos < _size) を実行することであるため、この時点では _size は 0 です。エラーが発生します。サイズ変更により _size のサイズが変更されるため、上記のコードは通常に実行するためにreserveをsizeに変更するだけで済みます。事前にスペースを空けるためにreserveを使用したい場合は、次にpush_backを使用してデータを挿入します。

2.3 Vector<char> は文字列を置き換えることができますか?

答えは「いいえ」です。ただし、どちらの最下層も基本的には動的に増加する配列ですが、文字列 string の末尾にはデフォルトで \0 があり、これにより C インターフェイスとの互換性が向上します。一方、vector<char の末尾は \0 になります。 > デフォルトでは \0 がありません。はい、自分で挿入する必要があります。

3、ベクトルシミュレーションの実装

ここに画像の説明を挿入

3.1 メンバー変数

public:
	typedef T* iterator;
	typedef const T* const_iterator;
private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;

3.2 メンバー関数

3.2.1 コンストラクター

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

vector(size_t n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
    
    
	resize(n, val);
}

vector(int n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
    
    
	resize(n, val);
}

//迭代器区间初始化
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
    
    
	while (first != last)
	{
    
    
		push_back(*first);
		first++;
	}
}

ちょっとしたヒント: さまざまな種類の反復子を使用する可能性があるため、反復子の範囲の初期化では関数テンプレートを使用します。次に、イテレータ範囲の初期化には関数テンプレートが使用されるため、コンストラクタを個別に提供する必要がありますvector(int n, const T& val = T())。このコンストラクタが個別に提供されていない場合、vector<int> v1(10, 1)この場合、最も一致するものが使用されます。つまり、イテレータの初期化関数と一致します。 range であり、それがvector(size_t n, const T& val = T())コンストラクターに進むことを願っていますが、10 は int 型とみなされ、size_t と一致しないため、イテレータ範囲初期化関数と一致し、InputIterator が int 型にインスタンス化され、int type は関数内で逆参照されるため、エラーが報告され、ロジックが一致しません。したがって、int には別のコンストラクターを提供する必要があります。

3.2.2 コピー構築

//方案一
vector(const vector<T>& V)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
    
    
	iterator tmp = new T[V.capacity()];
	//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();
	_end_of_storage = _start + V.capacity();
}

//方案二
vector(const vector<T>& V)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
    
    
	reserve(V.capacity());
	for (auto e : V)
	{
    
    
		push_back(e);
	}
}

ちょっとしたヒント: ディープ コピーの問題はここで設計されており、これについては以下の予備で説明します。

3.2.3 演算子=

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

vector<T>& operator=(vector<T> v)//调用拷贝构造函数
{
    
    
	swap(v);
	return *this;
}

3.2.4 サイズ

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

3.2.5 容量

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

3.3.6 イテレータ関連

iterator begin()
{
    
    
	return _start;
}

iterator end()
{
    
    
	return _finish;
}

const_iterator begin() const
{
    
    
	return _start;
}

const_iterator end() const
{
    
    
	return _finish;
}

3.2.7 リザーブ(ディープコピー問題)

void reserve(size_t new_capacity)
{
    
    
	if (new_capacity > capacity())
	{
    
    
		iterator tmp = new T[new_capacity];
		if (_start)//如果原来的_start申请过空间,要先将源空间中的内容拷贝过来
		{
    
    
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}

		size_t vsize = size();

		_start = tmp;
		_finish = tmp + vsize;//记得更新_finish
		_end_of_storage = _start + new_capacity;
	}
}

: _finish と _end_ofstorage は位置を表すため、ここで更新する必要があります。_finish を更新するには、まず size() を保存します。これは、_start を更新した後、_start は新しい領域の先頭を指し、_finish は古い領域の末尾を指すためです。このとき、size() を呼び出し、計算された数値は次のようになります。問題があるので、_startを更新する前に元の要素数、つまりsize()を保存する必要があります。

ちょっとしたヒント: T が組み込みクラスまたはディープ コピーを必要としないカスタム型である場合、上記の展開ロジックは完全に満たされます。しかし、T がディープコピーを必要とする組み込み型の場合、上記の拡張方法では大きな問題が発生します。例として、vector<string> を取り上げます。つまり、T が string の場合です。

ここに画像の説明を挿入
上の図に示すように、memcpy を使用して古いスペースのデータを新しいスペースにコピーするだけの場合、古いスペースと新しいスペースに格納されている文字列オブジェクトは、同じヒープ領域上の文字列を指します。 _start が string* ポインターであるため、古いスペースは破棄されますdelete[] _start;。そのため、最初に string のデストラクターが呼び出され、オブジェクト内で要求されたスペースが解放されます。つまり、_str が指すスペースが解放され、その後、関数が呼び出され、文字列オブジェクトのスペースが解放されますoperator deleteこのように、新しい空間に格納されている文字列オブジェクトに問題があり、そのメンバー変数 _str が指す空間が解放されています。ここでの問題は、memcpy が浅いコピーを実行することです。上記のコードを少し変更できます。

void reserve(size_t new_capacity)
{
    
    
	if (new_capacity > capacity())
	{
    
    
		iterator tmp = new T[new_capacity];
		if (_start)//如果原来的_start申请过空间,要先将源空间中的内容拷贝过来
		{
    
    
			//memcpy(tmp, _start, sizeof(T)*size());
			for (size_t i = 0; i < size(); i++)
			{
    
    
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		size_t vsize = size();

		_start = tmp;
		_finish = tmp + vsize;//记得更新_finish
		_end_of_storage = _start + new_capacity;
	}
}

変更後に実行すると、tmp[i] = _start[i];文字列オブジェクトの代入操作オーバーロードが呼び出され、ディープ コピーが実行されます。

3.2.8 サイズ変更

void resize(size_t n, const T& val = T())//缺省参数给的是一个匿名对象
{
    
    
	if (n > size())
	{
    
    
		//检查容量,扩容
		if (n > capacity())
		{
    
    
			reserve(n);
		}

		//开始填数
		iterator it = end();
		while (it < _start + n)
		{
    
    
			*it = val;
			it++;
		}

	}

	_finish = _start + n;
}

3.2.9 演算子[ ]

T& operator[](size_t pos)//读写版本
{
    
    
	assert(pos < size());
	return _start[pos];
}

const T& operator[](size_t pos) const//只读版本
{
    
    
	assert(pos < size());
	return _start[pos];
}

3.2.10 挿入 (反復子の失敗問題)

iterator insert(iterator pos, const T& val)
{
    
    
	assert(pos >= _start && pos <= _finish);
	size_t rpos = pos - _start;//保存一下pos的相对位置
	//检查容量
	if (_finish + 1 >= _end_of_storage)
	{
    
    
		size_t old_capacity = capacity();
		reserve(old_capacity == 0 ? 4 : old_capacity * 2);
	}
	pos = _start + rpos;//更新pos
	//插入数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
    
    
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}

: 挿入すると、有名な問題、イテレータの無効化が発生します。pos の位置にデータを挿入したいのですが、pos はイテレータです。データを挿入する前に、容量を確認して容量を拡張してください。拡張ロジックが実行されると、_start、_finish、および _end_of_storage はすべて新しい領域を指し、古い領域は解放され、pos は元の領域の特定の位置を指し続けます。このとき、pos はワイルドポインタとなり、pos が指す位置にデータを埋めると不正アクセスとなります。この問題を回避するには、まず pos の相対位置を保存し、展開が完了した後に pos を更新します。

ここに画像の説明を挿入
ちょっとしたヒント: 相対位置を保存し、insert 関数の内部解である pos を更新します。パラメータは値渡しであるため、仮パラメータの pos 更新は実パラメータの pos を変更しません。外部イテレータの失敗の問題を解決するには、ここで更新された位置を戻り値として返します。仮パラメータのposを直接参照に変更するのは良くないと考える友人もいるかもしれません。このように、仮パラメータの更新は実パラメータの更新と同じです。このアイデアは非常に優れていますが、実際のパラメーターは定数である可能性が高いため、非現実的です。たとえば、実際のパラメーターで begin() と end() が使用され、どちらも値によって返される場合、一時変数は次のようになります。生成され、一時変数が定数プロパティを持つ場合、仮パラメータ pos が参照を使用する場合は、const で装飾する必要があります。しかし!const で装飾されている場合、関数内で pos を更新することはできません。したがって、仮パラメータ pos を参照によって使用することはできません。

概要
: サイズ変更、予約、挿入、割り当て、プッシュバックなど、基になる空間に変更を引き起こす操作によりイテレータが失敗する可能性があります。

3.2.11 消去(イテレータ無効化問題)

iterator erase(iterator pos)
{
    
    
	assert(pos >= _start && pos <= _finish);
	iterator cur = pos + 1;
	while (cur != _finish)
	{
    
    
		*(cur - 1) = *cur;
		cur++;
	}
	_finish--;
	return pos;
}

: 消去により pos の要素が削除された後、pos の後の要素は、基になるスペースを変更せずに前方に移動されます。理論的に言えば、イテレータは無効ではありませんが、pos がたまたま最後の要素である場合、削除後の pos が発生します。が _finish の位置になり、_finish の位置に要素がない場合、pos は無効になります。したがって、ベクトル内の任意の位置にある要素が削除されると、VS は反復子が無効であるとみなします (VS は書き換えられた反復子を使用して必須のチェックを実行します)。Linux では、g++ コンパイラは反復子の失敗の検出においてそれほど厳密ではなく、その処理は vs の処理ほど極端ではありません。外部反復子の無効化の問題を解決するために、ここでも値を返すメソッドが使用され、pos の次の位置にある要素の反復子を返します。

3.2.12 ポップバック

//直接复用即可
void pop_back()
{
    
    
	erase(--end());
}

4. 結論

今日のシェアはここまでです!記事が悪くないと思ったら、3回サポートしてください .春連のホームページには興味深い記事がたくさんあります. お友達のコメントも大歓迎です. あなたのサポートが春連を前進させる原動力です!

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_63115236/article/details/132475686