記事ディレクトリ
序文
この記事では、ベクトルのシミュレーション実装について説明します。ベクトルについては、STL を詳しく学ぶために必要な方法です。
1. ベクトルのメンバー関数
ライブラリの実装方法に応じて、メンバー関数は次のとおりです。
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
C++11からはメンバー変数にデフォルト値が使用できるようになり、ここでもデフォルト値を使用することができます。
2. 追加・削除・検査・変更作業
size() と capacity() について説明します
サイズのサイズは _finish - _start です。
容量のサイズは _end_of_storage - _start です。
2.1予約()
この関数の機能は次のとおりです: 拡張。
void reserve (size_type n);
- アイデア:
- 1. 拡張するサイズ n が容量より小さい場合、縮小しません。
- 2. それ以外の場合は、容量拡張を実行します。
-
- まずはnサイズのスペースを申請してください
-
- 元のスペースのデータを新しいスペースにコピーし、元のスペースを解放します
-
- 新しいスペースのデータを _start に渡して管理します。
//扩容
void reserve(size_t n)
{
//防止有些情况是直接调用
if (n > capacity())
{
size_t oldsize = size();//必须要记录旧的size,旧的size是用旧的_finish - _start获得的
iterator tmp = new T[n];
//memcpy(tmp, _start, sizeof(T) * size());
//这里如果使用memcpy函数的话,如果vector对象是自定义类型,比如string,会造成浅拷贝问题。
for (size_t i = 0; i < oldsize;i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
//_finish = _start + size();
//如果这样用的话,size()是用旧的_finish - 新的_start,不是连续的空间,没有意义
_finish = _start + oldsize;//_finish必须更新,旧的_finish迭代器已经失效了
_end_of_storage = _start + n;
}
//tmp不用处理,出了作用域自动调用析构
}
注意:在扩容时,如果是自定义类型,比如string,使用memcpy函数进行拷贝数据,会出现浅拷贝问题。
v1 は v2 にコピーされますが、v2 のポインタは引き続き v1 の文字列オブジェクトが指すスペースを指します。これにより、同じスペースを解放するためにデストラクターを 2 回呼び出すという問題が発生します。
正しい解決策は、
コピー時に文字列の代入演算子のオーバーロードを呼び出して、ディープ コピーを完了することです。
もう一つのポイントは次のとおりです。
注: ここではイテレータの無効化の問題が発生します。
この時点でスペースがいっぱいになっているため、スペースを再申請する必要があります。
スペースを再申請した後、_start と _end_of_stortage は両方とも新しいスペースを指しますが、_finish は解放された古いスペースを指し続けます。_finish の位置を次のように更新すると、次のようになります。
_finish = _start + size();
ここでの size() は、古い _finish - 新しい _start から取得されますが、意味がありません。
解決策:
古いサイズを保存し、_start に古いサイズを加えた値が _finish の位置になります。
2.2 リサイズ()
この関数の機能は、ベクトル コンテナのサイズを再変更し、それが n になるようにすることです。新しいスペースはデフォルトで val に初期化されます
。
- 1. n が size より小さい場合、_finish が _start + n の位置を指すようにします。
- 2. n が size 以上の場合、最初にreserve関数を呼び出して容量を拡張し、次に新しいスペースをvalで埋めます。
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
//扩容到n,如果n<capacity(),则啥事不做,否则,扩容到n
reserve(n);
//将多的空间全部设置成缺省值
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
ここでのデフォルト値は T() であり、T 型の匿名オブジェクトであることに注意してください。
T が何であっても、テンプレートを使用して特定の匿名オブジェクトをインスタンス化できます。たとえそれが int などの組み込み型であっても、値が 0 である int() をインスタンス化することもできます。
2.3 挿入()
ここでは、最も一般的に使用されるインターフェイスのみを実装します。
この関数が行うことは次のとおりです。
テンプレート オブジェクト val を pos の位置に挿入します。
アイデア:
- エレメントを挿入する前に容量を確認してください
- 容量がいっぱいの場合は、リザーブ関数を呼び出して容量拡張を実行します。
- 次に、pos 位置から開始して、その後のすべての要素を元に戻します。
- 次に、pos の位置に val を挿入します。
void insert(iterator pos, const T& val)
{
assert(pos <= _finish);
//插入前先检查扩容
if (_finish == _end_of_storage)
{
size_t oldpos = pos - _start;//记录相对位置
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
pos = _start + oldpos;
}
//记录旧的size,防止出现迭代器失效问题。
//将pos之后的位置往后挪,再插入
iterator end = _finish - 1; // reserve之后,_finish也跟着更新了。
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
_finish += 1;
}
注: 挿入には、容量拡張中にイテレータが無効になるという問題が依然として残ります。
解決策: 事前に pos の相対位置 (oldpos = pos - _start) を記録し、
展開が完了したら、pos の位置を更新します (pos = _start + oldpos)。
そうしないと、古い pos イテレータは展開後に無効になります。
2.4 消去()
この関数の機能は、pos 位置の値を削除することです。
//在删除最后一个位置或者只剩下一个待删除元素时,会出现迭代器失效问题。
iterator erase(iterator pos)
{
assert(pos < _finish);
iterator end = pos;
while (end != _finish)
{
*end = *(end + 1);
++end;
}
--_finish;
return pos;
}
注意すべき点が 1 つあります。要素を削除すると、スペースが解放されているため、pos イテレータは無効になり、現時点では pos イテレータは使用できません。
解決策: 削除された位置の次の要素の位置、つまり pos 位置を返します。
ただし、削除する要素が 1 つだけ残っている場合、または最後の位置の要素を削除する場合は、pos イテレータは使用できません。
2.5 プッシュバック()とポップバック()
名前が示すように、これら 2 つの関数の機能は、最後の位置に要素を挿入し、最後の位置を削除することです。
挿入関数と消去関数を直接再利用するだけなので、ここでは繰り返しません。
3. [] オーバーロードとイテレータ
typedef T* iterator;
typedef const T* const_iterator;
3.1イテレータの開始
_start アドレスを返すだけです。
iterator begin()
{
return _start;
}
const_iterator begin() const
{
return _start;
}
3.2 終了反復子
_finish アドレスを返すだけです。
iterator end()
{
return _finish;
}
const_iterator end() const
{
return _finish;
}
3.3 [] 演算子のオーバーロード
アイデア: pos の添え字を指定するだけです。
その前に、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);
}
4 番目、デフォルトのメンバー関数
4.1 コンストラクター
コンストラクターには次の 3 種類があります。
- パラメータ構築なし
- n 個の val オブジェクトを構築する
- 反復間隔から構築
引数のない構築では、メンバー関数を初期化するだけで済みます。
- 1. パラメータ構築なし
//无参构造函数
vector()
//:_start(nullptr)
//,_finish(nullptr)
//,_end_of_storage(nullptr)
{
}
- 2. n val オブジェクトを構築する
//构造n个对象
vector(size_t n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//1.开空间
reserve(n);
//2.将空间填充为val对象
for (size_t i = 0; i < capacity(); i++)
{
_start[i] = val;
}
//也可以复用resize
//resize(n, val);
}
注: 構築時に初期化する必要がありますが、初期化されていない場合、領域を開くために予約関数を呼び出すときにワイルド ポインタにアクセスするという問題が発生します。
サイズ変更関数を呼び出して、n 個のオブジェクトをコピーして構築することもできます。
- 3. 構築には反復間隔を使用します
同様に、初期化されていない場合、容量拡張後にワイルドポインタにアクセスすることになります。
template<class Inputiterator>
vector(Inputiterator first, Inputiterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
4.2 コピーコンストラクター
- 1. コピー構築の伝統的な書き方
//拷贝构造,必须实现深拷贝
//传统写法
//v1(v)
vector(const vector<T>& v)
//这里必须初始化,否则扩容时会访问不属于自己的空间
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
//如果私有成员给了缺省值,就不用再再次初始化了
{
reserve(v.capacity());
//memcpy(_start, v._start, sizeof(T) * v.size());
//这里使用memcpy的错误跟扩容的错误一样,不再赘述
for (size_t i = 0; i < v.size(); i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
}
コピーを構築する際に注意する必要があるいくつかの問題:
- 1. コピー構造はコンストラクタと同じですが、初期化しないとスペースオープン時にワイルドポインタにアクセスします。
- 2. コピー構築ではディープ コピーを実行する必要があります。そうしないと、ベクトルのオブジェクトが文字列などのカスタム タイプである場合、同じスペースを 2 回解放するという問題が発生します。
4.3 デストラクター
_start が指すスペースを解放し、すべて空に設定します。
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage;
}
}
要約する
これで、vector のシミュレーション実装は終了です。参考になれば、ぜひ注目してください。