ベクトルにおける反復子の無効化の問題と解決策

 

目次

ベクトル共通インターフェース 

ベクトル反復子の無効化の問題 

ベクトルの深いコピーと浅いコピーの問題 


ベクトルのデータ配置と演算方法は配列と非常に似ています。2 つの唯一の違いは、スペースの使用の柔軟性にあります。配列は静的スペースであり、一度構成すると変更できません。より大きな (または小さな) 家に変更したい場合は変更できます。すべての詳細はクライアント次第です。まず新しいスペースを構成し、次に要素を古い場所から新しい場所に 1 つずつ移動し、元の空間をシステムに戻します。Vector は動的な空間であり、要素が追加されると、その内部メカニズムが新しい要素を収容するために空間を拡張します。したがって、ベクトルの使用は、メモリの合理的な使用と使用の柔軟性に非常に役立ち、スペース不足を心配して最初に大きな配列を要求する必要がなくなり、安心してベクトルを使用できます。そして私たちが食べるのと同じくらい使います。

ベクトル定義

template<class T>
 クラス ベクトル 

{

public:
        typedef T* イテレータ;
        typedef const T* const_iterator;

プライベート:

        iterator _start; //現在使用されているスペースの先頭を表します
        iterator _finish; //現在使用されているスペースの終わりを表します
        iterator _end_of_storage; //使用可能なスペースの終わりを表します

}

ベクトル共通インターフェース 

  • Push_back() メンバー関数は、ベクトルの末尾に値を挿入し、必要に応じてベクトルのサイズを拡張します。
  • Pop_back( ) メンバー関数は、ベクトルの末尾の値を削除します。
  • size( ) 関数はベクトルのサイズを表示します。
  • begin( ) 関数は、ベクトルの先頭を指す反復子を返します。
  • end( ) 関数は、ベクトルの末尾を指す反復子を返します。
  • empty() ベクトルが空かどうかを判断します。
  • find() 検索します。(これはアルゴリズム モジュールの実装であり、vector のメンバー インターフェイスではないことに注意してください)
  • insert() は位置の前に val を挿入します
  • Erase() はその位置のデータを削除します
  • swap() 2 つのベクトルのデータ空間を交換します。
  • Operator[] は配列と同様に添字アクセスを使用します

size は現在のベクトル コンテナの実際のサイズ、つまりコンテナに現在コンテナがいくつあるかです。

容量とは、 再割り当てが発生する前に許可される要素の最大数、つまり事前に割り当てられたメモリ空間を指します。

もちろん、これら 2 つの属性は、resize() と reserve()という 2 つのメソッドに対応します。

コンテナ内のオブジェクト メモリ空間は実際に存在します。

reserve() を使用すると、容量の値が変更されるだけであり、コンテナ内のオブジェクトには実メモリ領域がありません (領域は「ワイルド」です)。

容量コードを vs および g++ でそれぞれ実行すると、vs での容量は 1.5 倍、g++ では 2 倍に増加することがわかります。具体的な増加額は、特定のニーズに応じて定義されます。vs は PJ バージョン STL、g++ は SGI バージョン STL です。リザーブはスペースを空けることのみを担当します。必要なスペースがどのくらいかわかっている場合、リザーブはベクトル容量拡張のコスト上の欠点を軽減できます。サイズ変更もスペースを開くときに初期化されるため、サイズに影響します。

このとき、コンテナ内のオブジェクトにアクセスするには [] 演算子を使用することを忘れないでください。配列の範囲外の問題が発生する可能性があります。

ベクトル反復子の無効化の問題 

イテレータの主な機能は、アルゴリズムが基礎となるデータ構造を考慮しないようにすることです。基礎となるデータ構造は実際にはポインタであるか、ポインタがカプセル化されています。たとえば、vector のイテレータは元のエコロジー ポインタ T* です。イテレータの無効化は、イテレータの下部にある対応するポインタが指す空間が破壊され、解放された空間の一部が使用されることを意味します。

イテレータの無効化は、次の 2 つの大きなカテゴリに分類されます。

1. 拡張によりワイルド ポインタが生成される

 

Push_back で 4 つの尾を挿入してから insert を呼び出すと、ランダムな値が表示されることがわかりました。問題は、pos が更新されないため、展開によって pos イテレータが失敗し、ワイルド ポインタへの不正なアクセスが発生することです。

最後に 4 つの数値を挿入した後、先頭に別の数値を挿入して拡張が発生します。予約拡張メカニズムに従って、拡張アドレスが変更され、イテレータが無効になります。挿入中に拡張が発生すると、その数値が指すスペースが失われます。イテレータが解放される イテレータは本質的にはワイルド ポインタです。_ start と _finish は両方とも更新されますが、挿入位置 pos は更新されません。このとき、pos は依然として古い空間を実行し、古い空間は予約後に解放されます。このとき、pos はワイルド ポインタです。 *pos = x が不正になる原因 ワイルド ポインターへのアクセス。pos iterator が更新されないため、その後のデータの移動は実装されず、データの挿入は解放された領域で操作することになりますが、これも無意味です。つまり、どこに挿入しても効果はありません。

解決:

拡張後に pos を更新して、pos の失敗の問題を解決します。

iterator insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			//扩容地址改变,迭代器会失效
			//insert中发生扩容,it指向的空间被释放,it本质上就是一个野指针
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);

				//扩容后更新pos,解决pos失效的问题
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (pos >= end)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = val;
			++_finish;
			return pos;
		}

2. イテレータの指す位置の意味が変わる

たとえば、ベクトル内のすべての偶数を削除する必要があります。

 Erase が pos の要素を削除した後、pos の後の要素は、基になる空間を変更せずに前方に移動します。理論的には、反復子は失敗しませんが、pos がたまたま最後の要素である場合、pos は削除位置の後に終了し、 end の位置に有効な要素がない場合、pos は無効になります。したがって、ベクトル内の任意の位置にある要素が削除されると、その位置にある反復子は無効とみなされます。これを使用するときは、イテレータが有効な位置を指すように注意する必要があります。

イテレータが失敗した後、コードが必ずしもクラッシュするわけではありませんが、実行結果は間違いなく間違っており、begin と end の範囲内にない場合は確実にクラッシュします。

ベクトルの深いコピーと浅いコピーの問題 

コピーコンストラクター

 memcpy は浅いコピーであり、T が組み込み型の場合はこのコピー機能で問題ありませんが、T が文字列型などカスタム型の場合には問題が発生します。

 

 このとき、コピー構築に memcpy 関数を使用すると、コピーによって構築されたベクトル内の各文字列のメンバー変数の値は、コピーされたベクトル内の各文字列のメンバー変数の値と同じになります。 、2 ベクトル内の対応する各文字列メンバーは、同じ文字列空間を指します。

解決: 

_start[i] = _v[i] 本質は、ディープコピー用の文字列クラスの代入演算子のオーバーロード関数を呼び出すことです。 

拡張ではシャローコピーの問題にも注意する必要があります。

容量拡張時に呼び出されるmemcpyは浅いコピーのため、以前に保存されていたデータはmemcpyの後に削除され、その後削除されてランダムな値になります。Vector はデストラクターを呼び出して元のオブジェクトを破棄し、各オブジェクトは独自のデストラクターを呼び出してポイントされたスペースを解放します。その後、ランダムな値が表示されます。

 古い空間を解体するときは、オブジェクトの配列を解体し、各配列は独自のデストラクターを呼び出します。これにより、配列の空間が解体されます。memcpy の浅いコピーを使用すると、コピーされた一時オブジェクトと元のオブジェクトは同じ空間を指すため、古い空間が破棄された後、拡張した新しい空間内のオブジェクトはワイルド ポインターになり、アクセスされるデータはすべてランダムな値になります。for ループを使用して、vector の代入演算子のオーバーロードを呼び出し、古い空間のデータを新しい空間にコピーします。これにより、古い空間の破壊が新しい空間に影響を与えなくなります。

おすすめ

転載: blog.csdn.net/m0_55752775/article/details/129406979