目次
1. 準備
準備作業では、これまでに学習したこと、名前空間と クラス テンプレートの知識が必要です。また、STL ソース コードを実装する前に、その実装方法を学習する必要があります。
実装を開始する前に、vector のフレームワークについて理解しましょう。
// 头文件
#include <iostream>
#include <vector>
using namespace std;
namespace my_vector // 里面我们使用的类也叫vector,命名空间隔离,避免与STL中的vector命名重复
{
template <class T> // 这里缺了内存池的模板,后面再学习
class vector
{
typedef T* iterator; // vector在物理空间上是一段连续的空间,所有这里的迭代器就是指针
public:
private:
iterator _start; // 迭代器开始位置
iterator _finish; // 当前迭代器指向位置,相当于size
iterator end_of_storage; // 该段迭代器的最终位置
};
}
// 测试文件///
#include "my_vector.h"
int main()
{
my_vector::vector<int> p1; // 使用自己的vector需要进行标识命名空间,否则用的就是STL中vector
return 0;
}
二、プッシュバック
void push_back(const T& data) // 这里有关 两个问题的探讨,1. const 修饰; 2. 引用
{
}
ここで議論する必要がある2 つの点: 1. パラメータ const の変更 、2.参照について
引用について一言
1. 参考文献について
データは int で、char は問題ありません。参照は使用しません。値のコピーもそれほど悪くはありませんが、パラメータは文字列クラス、vector<T>などです。現時点では、文字列はディープ コピーであるため、無駄が生じます。パフォーマンスが良すぎる。したがって、ここではリファレンスを選択します。
2. パラメータconstの変更
次のシーンを見てみましょう
int main()
{
my_vector::vector<int> p1; // 使用自己的vector需要进行标识命名空间,否则用的就是STL中的vector
// 场景一:参数是 变量对象
int a = 10;
p1.push_back(a); // ok的
// 场景二:参数是临时变量,或者匿名对象
p1.push_back(10); // 挪,没有const 修饰,无法接收临时数据
my_vector::vector<string> p2;
p2.push_back(string("xxxxx"));
return 0;
}
結論: 1. ベクトルがさまざまなデータ型に直面しても比較的高いパフォーマンスを発揮できるようにするには、参照が必要です; 2. データの const 変更により、データの互換性を向上させることができます。
Push_back + Operator[]の実装コードは 次のとおりです。
size_t size() const // 针对被const修饰的对象,使其兼容
{
return _finish - _start;
}
size_t capacity() const
{
return end_of_storage - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
T* new_start = new T[n];
if (_start) // 如果旧空间不是空,则需要拷贝备份
{
// memcpy(new_start, _start, sizeof(T) * size()); 为啥不直接选择memcopy
// 这里需要重点讲解
for (size_t i = 0; i < n; i++)
{
new_start[i] = _start[i]; // 如果是自定义类型,则会调用其operator
}
delete[] _start;
}
_finish = new_start + size();
_start = new_start;
end_of_storage = new_start + n;
}
}
T& operator[](size_t pos)
{
assert(pos < size()); //进行越界判断
return _start[pos];
}
void push_back(const T& data) // 这里有关 两个问题的探讨,1. const 修饰; 2. 引用
{
if (_finish == end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = data;
_finish++;
}
補充する
memcpy コードを使用しないのはなぜですか?
T にint のような組み込み型を使用している場合、それを見つけるのは難しいでしょう。データがカスタム タイプの場合、外側の層は深いコピーですが、内側の層は依然として浅いコピーです。
同様に、従来の書き方のコピー構成も改善できます(コピー構成は以下の通りです)
3番目、イテレータの実装
STL では、反復子には 2 つのバージョンがあることがわかります。違いは、オブジェクトが const オブジェクトであるかどうかです。
分析: const メンバー関数では、this の表示パフォーマンスは次のとおりです: const T& const this。これは内容の変更を許可しない絞り込み権限であるため、いくつかの関数を実装する必要がある場合は 2 つのバージョンが必要です。
約束:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
void func(const vector<T>& v)
{
const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << endl;
it++;
}
}
イテレータのconst バージョンは存在しないため、 const オブジェクトを受信したときにこの型エラーが発生する可能性があります。
変更は簡単で、完全な実装は次のとおりです。
iterator begin()
{
return _start;
}
iterator begin() const
{
return _start;
}
iterator end()
{
return _finish;
}
iterator end() const
{
return _finish;
}
begin() 関数だけでなく、他のメンバー関数も、特定の状況下では const 修正バージョンが必要です。
イテレータの begin と end の実装が完了したので、 const オブジェクトに直面したときに範囲を解決できます。(基礎となる範囲は反復子アクセスに置き換えられるだけです)
約束:
my_vector::vector<int> p1;
const my_vector::vector<int> p2;
for (cosnt auto& i : p1) // 调用普通迭代器 (这里的const与&,兼顾效率与兼容性)
{
cout << p1[i] << " ";
}
for (cosnt auto& i : p2) // 调用const迭代器
{
cout << p1[i] << " ";
}
4、ポップバック
void Pop_back()
{
assert(_finish > _start);
_finish--;
}
5、挿入
挿入に関しては、C言語で挿入するという考え方は悪くありません。
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
if (size() + 1 >= capacity())
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
if (pos < _finish)
{
iterator end = _finish;
while ( end != pos)
{
*end = *(end - 1);
end--;
}
*pos = val;
_finish++;
}
else
{
push_back(val);
}
}
ここに地雷があります、みんながそれを取り出せるかどうか見てみましょう? イテレータが無効であるため、上記のコードが改善されました。
1. 補足 - イテレータの無効化
先ほど書いたコードをもとに、次のコードを試してみましょう。
void func1()
{
my_vector::vector<int> p2;
p2.push_back(1);
p2.push_back(2);
p2.push_back(3);
p2.push_back(4);
p2.push_back(5);
auto pos = find(p2.begin(), p2.end(), 3);
p2.insert(pos, 100); // insert一般搭配find算法,一般情况下是不知道数据的迭代器
for (const auto& n : p2)
{
cout << n << " ";
}
}
現時点では実行に問題はありませんが、p2.push_back(5)をコメントアウトしてから実行します。消す:
???!!! バグがあります??!!!
これは秘密ではありませんが、エラーの理由は次のとおりです。
このシナリオでは、挿入と拡張の両方が必要です。イテレータ pos は本質的にはポインタですが、展開後も pos は古い空間の位置を保持し、pos データは更新されず、ワイルド ポインタは間違っています。
変更方法も非常に簡単で、pos 更新メカニズムを改善します。
さて、イテレータの無効化は何を伝えようとしているのでしょうか?
イテレータの無効化 —データを挿入した後の pos は無効である可能性があるため、データへのアクセスに pos を使用しないようにしてください。
ここで質問があるかもしれません? もうposを更新していないでしょうか?
回答: これは挿入関数で変更され、値が渡され、行パラメータは実際のパラメータには影響しません。(こんな質問してしまい申し訳ありませんが(笑えない))
また、パス内の pos のリファレンスを参照するという人もいるかもしれませんが、この環境では、この問題は確かに解決できますが、私の答えは、STL との整合性を保つように努め、設計を変更することです。フレームワークが発動することが多く、体全体を動かしてみても、今はまだ掴みきれていません。
六, erase
データを前方に置き換えるのは比較的簡単です。例:vector<int> p1(10, 2); 初期値は 10 個のデータ、初期値は 2 です。
// STL中要求erase返回被删除位置的下一个位置的迭代器
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
while (pos + 1 != _finish)
{
*pos = *(pos + 1);
pos++;
}
_finish--;
return pos;
}
では、ここでイテレータの無効化に問題はあるのでしょうか? 結果: 実装した消去ではイテレータは無効になりません。STL ライブラリ内のイテレータはどうなるでしょうか? 答えはコンパイラを調べることです。(STL が標準ライブラリであり、C++ ライブラリ関数の機能標準を実装するよう誰にでも伝えることを目的としていることがわかっています。具体的な実装はコンパイラの実装方法によって異なります。たとえば、一部のコンパイラは消去を実装すると縮小する可能性があるため、反復デバイスを使用します)失敗するかもしれない)
イテレータの無効化を要約すると、挿入/消去の位置を更新する必要があります。直接アクセスしないでください。予期しない結果が発生する可能性があります。
セブン、コンストラクター
最初に単純なコンストラクターを作成しました。ここにイテレーター構築、コピー構築を追加します。
1. イテレータの構築
template <class inputIterator> // 提供一个接收参数迭代器的新模板
25 vector (inputIterator first, inputIterator last)
26 :_start(nullptr)
27 ,_finish(nullptr)
28 ,end_of_storage(nullptr)
29 {
30 while (first != last)
31 {
32 push_back(*first);
33 first++;
34 }
35 }
ここで、これはデータを初期化する必要があるコンストラクター関数であることに注意してください。忘れないでください。
2. その他の構造
それに気づいて、複数のデータを一度に初期化します。
vector(size_t n , const T& val = T())
{
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
しかし、この方法で書くとバグがあります。つまり、両方のパラメータが int の場合、コンピュータは最も一致する関数を探すため、イテレータ コンストラクタを呼び出し、後続のアクセスではイテレータとして int が使用され、エラーが発生します。
調整方法は??
実際、解決するのは非常に簡単です。
方法 1: size_t を int に変更する
ベクトル(int n , const T& val = T())
方法 2: size_t を保持し、int コンストラクターをオーバーロードします。
vector(size_t n , const T& val = T())
{
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n , const T& val = T())
{
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
3. コピー構築
1) 伝統的な書き方
// 传统写法
E> 56 vector (const my_vector::vector<T>& v)
57 :_start(nullptr)
58 ,_finish(nullptr)
59 ,end_of_storage(nullptr)
60 {
61 reserve(v.size());
62 // memcpy(_start, v._start, sizeof(T) * v.size() );
63 for (size_t i = 0; i < v.size(); i++ )
64 {
65 _start[i] = v[i];
66 }
67 _finish += v.size();
68 }
2) 現代文(機能の再利用性向上)
void swap(my_vector::vector<T>& order)
38 {
39 std::swap(_start, order._start);
40 std::swap(_finish, order._finish);
41 std::swap(end_of_storage, order.end_of_storage);
42 }
43
44 // 拷贝构造
45 vector (const my_vector::vector<T>& v )
46 : _start(nullptr)
47 , _finish(nullptr)
48 , end_of_storage(nullptr)
49 {
50 my_vector::vector<T> tmp(v.begin(), v.end());
51 // 借用迭代器构造,然后交换数据。
52 swap(tmp);
53 }
8、代入シンボルのオーバーロード
これは間違いが起こりやすい点の分析です 。一部の生徒は p2 = p1 & p2(Data) を区別できないでしょう。
void t() 81 { 82 vector<int> p1 = 10; // 这是一个p1的初始化,等价于p1(10) 83 vector<int> p2; 84 p2 = p1; // p2已经存在,这才是调用赋值重载函数 86 }
この代入のオーバーロードに関しては、次のように書く方が簡単です。
vector<T>& operator= (vector<T> v) // 不添加&是故意的
195 {
196 swap(v);
197 return *this;
198 }
内部のアイデアは次のとおりです。
9、サイズ変更
データのサイズを調整する機能は、既存のオブジェクトのサイズを変更する 問題を解決します。
// 调整数据大小
132 void resize(size_t n , const T& val = T())
133 {
134 if (n > capacity())
135 {
136 reserve(n);
137 }
138
139 if (n > size())
140 {
141 // 开始填充数据
142 while ( _finish < _start + n )
143 {
144 *_finish = val;
145 _finish++;
146 }
147 }else
148 {
149 _finish = _start + n;
150 }
151 }
エピローグ
このセクションはここで終了です。閲覧してくれた友人に感謝します。何か提案があれば、コメント欄へのコメントを歓迎します。友人に利益をもたらした場合は、いいねを残してください。あなたの好きや関心がブロガーになる原動力です。マスターの作品。