プログラミングでは、多くの場面で、値の集まりを作ってその操作を行います。
値の集まりを扱うクラスたちをC++ではコンテナ(container)と呼んでいます。
vector, list, dequeなど多数のコンテナがありますが、最もよく使われるであろうvectorを紹介します。
詳しくはストラウストラップの本で。
std::vector
C++に限らず、プログラミングをやっていく中、多くの場面で、値の集まりを作ってその操作を行います。
値の集まりを扱うクラスたちをC++ではコンテナ(container)と呼んでいます。
最もよく使うコンテナ
C++のコンテナの中で最もよく使うのがvectorです。
何かのデータを保存したり、計算したり、参照したり、と大活躍します。
使い方を知っておきましょう。他のコンテナも、微妙な差はあるにしても、使い方はほぼ同じです。
まずは基本のvectorを押さえておきましょう。
インクルード
vectorを使用するには以下のように<vector>をインクルードする必要があります。
#include <vector>
vectorという名前が好きではありません
私は数学科で数学を勉強してきたので、vectorという言葉から連想するものが、C++が指すvectorと全く一致しません。
数学ではベクトル空間の要素をvectorと呼んだ気がします。けれども、C++では値の集まりというような意味で使います。
日本の高校でもベクトルというのを習うので違和感がありますね。
とりあえず、学校の数学で習ったベクトルとは別のものと思いましょう。
C言語から来た人へ
C言語の配列は、C++ではコンテナに進化していて、vector, listなどいろいろあります。
なかでも、C言語の配列に最も近いのはvectorです。
C言語の配列はひどかったですね。控えめに言って、言語仕様が奇妙奇天烈でした。[]をつけないとポインタ型になるし、a[2]とa+2が同じって言われても何のことかを理解するのにどれほどの時間がかかったか。特に、メモリ不正アクセスの温床でした。配列を使わないプログラムなどありえないですから、精通するしかなかったですが、心の中では配列を呪っていたでしょう。
せっかく習得したC言語の配列の知識は、C++のvectorでとても役に立ちます。時折見せるC言語由来のわけわからない仕様が理解できるのもC言語を知っているからです。
vectorの実装のイメージは以下の感じです。
class vector { int* p;//配列のポインタ size_t sz;//配列のサイズ }
配列のポインタと配列の大きさを管理しています。本当に配列の拡張と思って大丈夫です。※実際はメモリ確保をあらかじめ大きくやったりとしています。
宣言と初期化
単に宣言
宣言はstd::vector<int>のように、<>の中に型を入れてください。
std::vector<int> v;//int型のvector
上記では、長さ0の配列が確保された感じです。
初期化(値を指定する)
初期化(値を入れて宣言する)はいろいろあります。
まずは、値を指定するときは{}で囲います。
std::vector<int> v{1, 2, 3};
※実はいろいろ書き方がありますが、C++11から導入された上記の書き方が最も汎用的です。
上記では、型がintで値が1, 2, 3でサイズ3となります。
初期化(サイズを指定する)
サイズを指定する初期化は、以下のように()で囲います。
std::vector<int> v(10);
上記では、型intでサイズ10です。値はC言語では不定ですが、C++では0です。
同じ値でしょきかするのであれば、次のように書きます。
std::vector<int> v(10, 3);
上記では、型intでサイズ10です。値はすべて3となります。
値がすべて異なる場合は、次の章の代入の方法を使ってください。
値の代入と参照
vectorの値へのアクセスは、配列の様に[]またはイテレータでアクセスします。
配列風アクセス
以下のようなアクセスします。
std::vector v(10); for (int i=0; i<10; ++i){ v[i] = i; //v[i]にiを代入 }
[]演算子はC言語の配列みたいだから悪夢を思い出す、という人はat演算子を使います。
std::vector v(10); for (int i=0; i<10; ++i){ v.at(i) = i; //v[i]にiを代入 }
イテレータによるアクセス
イテレータでアクセスするときも多いと思います。イテレータはポインタ扱いなのでアクセスするときは*をつけてください。
std::vector v(10); for (auto itr=v.begin(); itr != v.end(); ++itr){ *itr = 100; //100を代入 }
追加と削除
vectorに値を末尾に追加するときpush_backを使います。vectorは配列ですのでランダムな位置への追加は性能が悪いです。ランダムな位置へのアクセスをする場合はlistを使ってください。
以下のようにpush_backで末尾に値を追加します。
std::vector<int> v(10, 3); v.push_back(8);// 末尾に8を追加
このとき、最初の10個は3、11個目は8となるvectorになります。
末尾の値を削除するときpop_backを使います。こちらもランダムな位置の値の削除はやらない方がよいです。
以下のようにして使います。
std::vector<int> v{1,2,3,4,5}; v.pop_back();
このとき、vは{1,2,3,4}になります。
リサイズ
よく使うメソッドにresize()があります。
クラスのメンバ変数としてサイズ0で宣言して、初期化時にresize()するというのはよくあることです。
大きさがあらかじめ分かっているなら1個ずつpush_backするよりも最初にresizeして代入していく方が性能がよいです。
std::vector<int> v; v.resize(10); // 値の大きさは10です
使い方注意 関数の引数では参照で、範囲チェックなし
いくつか注意事項があります。
関数の引数では参照
関数の引数にvectorを使うときは必ず参照にしてください。
int myFunction(std::vector<int> v){ // 参照にしていない。やめた方がよい。 // いろいろ }
参照は以下のように&をつけてください。
int myFunction(std::vector<int> &v){ // 参照。 // いろいろ }
参照にしないと値のコピーが行われます。例えばvが4Kバイトあれば、関数コールするだけで4Kバイトのコピーが走ります。プログラム全体が値のコピーになっていると地味に効いてきますので、特段の理由がない限りは、vectorの引数は参照にしておきます。
範囲チェックなし
なぜ範囲チェックを入れなかったか分かりませんが、チェックされません。
例えば、以下は不正メモリアクセスです。
std::vector<int> v(10); v[10] = 1; // v[0], v[1], .. , v[9]までアクセス可能。v[10]はアクセス不可
領域を確保していないところのアクセスについて、コンパイラも警告を出しませんし、実行時も普通に実行されます。運がいいと(悪いと?)、プログラムがクラッシュします。
ループ
forループに関しては以下の記事を参考にしてください。
>>> C++のループ for, while, do-while
範囲指定のfor文がとても便利です。覚えておきましょう。
std::vector<int> v(10); for (auto x : v){ std::cout << x << " "; }
まとめ
C++のvectorの使い方について説明しました。
コメント