vectorクラス
配列に代わる機能2
C++には配列のようなデータの集合をより便利に扱うために、様々な機能が用意されています。
(コンテナ型)
arrayクラスもその一つですが、より便利に扱えるのがvectorクラスです。
#include <iostream>
#include <vector>
int main()
{
//1~5の要素を持つvectorを作成
std::vector<int> vec{ 1, 2, 3, 4, 5 };
for (size_t i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << " ";
}
//1 2 3 4 5
std::cin.get();
}
vectorクラスを使用するには#include <vector>
を記述します。
vectorクラスの特徴は、要素数が可変という点です。
(要素数が可変の配列は動的配列や可変長配列と呼ばれます)
通常の配列やarrayクラスなどは、要素数を変更する場合には少し複雑な処理を書く必要がありました。
vectorクラスは要素数を自由に増減できます。
それでいて配列並みに高速です。
(厳密には配列のほうが速い場合が多い。でも普通は気にならない)
宣言と初期化
vectorクラスの使用の宣言は以下のように行います。
std::vector<int> vec;
これはint型、要素数0のvectorクラスの使用の宣言です。
要素数0でも後から要素数の追加ができますから、配列の場合とは違い意味のある宣言方法です。
要素の追加方法は後に説明しますので、まずは他の宣言方法や初期化について。
std::vector<int> vec1{ 1, 2, 3, 4, 5 }; //C++11
std::vector<int> vec2 = { 1, 2, 3, 4, 5 }; //C++11
//要素数100、すべての値に0をセット
std::vector<int> vec3(100);
//要素数100、すべての値に1をセット
std::vector<int> vec4(100, 1);
//vec1の要素をすべてコピー
std::vector<int> vec5(vec1);
//vec1[1]からvec[3]手前までをコピー
std::vector<int> vec6(vec1.begin() + 1, vec1.begin() + 3);
配列のような初期化子リストを使用しての初期化はC++11から可能です。
配列の場合は要素数のみを指定した場合の中身は不定ですが、vectorクラスでは自動的にすべて0
で埋められます。
bool型の場合はfalse
で埋められます。
要素がクラス型の場合はデフォルトコンストラクタが呼び出されます。
デフォルトコンストラクタが存在しない場合はコンパイルエラーになります。
最後の方法はやや特殊な書き方で、イテレータというものを使用しています。
これについては次ページで説明しますが、begin()
は最初の要素を、end()
は最後の要素の次を指していると考えてください。
代入操作
vector同士はそのまま代入によるコピーが可能です。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec1{ 1, 2, 3, 4, 5 };
std::vector<int> vec2{ 9, 8, 7 };
//vec2にvec1の要素をすべてコピー
vec2 = vec1;
//コピー元を書き換えてもコピー先に影響しない
vec1[0] = 10;
std::cout << vec2[0] << std::endl;
std::cin.get();
}
代入ができるのはデータ型が同一であるvector同士に限られます。
要素数は自動的に変更されるので、違っていても、一方が空であっても問題ありません。
各要素へのアクセス
各要素の値の取り出し、値の代入は配列と同じです。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
vec[1] = 10;
//10
std::cout << vec[1] << std::endl;
std::cin.get();
}
at関数
arrayクラスやstringクラスと同じくat
関数を使用して要素にアクセスする方法もあります。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
vec.at(1) = 10;
std::cout << vec.at(1) << std::endl;
//10
//例外発生
std::cout << vec.at(10) << std::endl;
std::cin.get();
}
vectorは要素数が可変なため、要素の範囲外へのアクセスをしてしまう可能性が配列やarrayクラスよりも多少高くなります。
そのため、at関数はやや有用です。
とはいえ、例外処理はそこそこ重たい処理なため、特別な理由がなければ範囲外アクセスをしないように気を付ける方が良いです。
(例外については例外処理参照)
二次元配列
vectorクラスで二次元配列を使用するには以下のようにします。
#include <iostream>
#include <vector>
int main()
{
std::vector<std::vector<int>> vec{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
for (size_t i = 0; i < vec.size(); i++)
{
for (size_t j = 0; j < vec[i].size(); j++)
{
std::cout << vec[i][j] << " ";
}
std::cout << "\n";
}
std::cin.get();
}
arrayクラスの時と比べて、配列とほぼ同等の記述ができます。
vectorクラスは、呼び出し側からデータ型を指定して使用します。
上記のコードでは、このデータ型にさらにvectorクラスを指定しています。
(vectorの入れ子)
std::vector<std::vector<int>> vec;
入れ子になったデータ型の閉じ側に山括弧(>)がふたつ連続していますが、C++03という古いバージョンではこれがデータ型の指定の終了ではなく右シフト演算子(>>
)として解釈され、コンパイルエラーになってしまいます。
これを回避するには、ふたつの山括弧の間に半角スペースを挿入します。
//C++03
std::vector<std::vector<int> > vec;
関数
arrayクラスですでに解説した機能は簡単な説明に留めます。
(→arrayクラス)
size関数
要素数の取得にはsize
関数を使用します。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
for (size_t i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << "\n";
}
std::cin.get();
}
empty関数
vectorが空か否かを判定するにはempty
関数を使用します。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec1;
std::vector<int> vec2{ 1, 2, 3, 4, 5 };
std::cout << vec1.empty() << std::endl;
std::cout << vec2.empty() << std::endl;
std::cin.get();
}
front、back関数
vectorの先頭、末尾要素にアクセスするにはfront
関数、back
関数を使用する方法があります。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
std::cout << vec.front() << std::endl;
std::cout << vec.back() << std::endl;
//値の書き換えもできる
vec.back() = 50;
std::cout << vec.back() << std::endl;
std::cin.get();
}
これらの関数は参照を返すので、要素の読み取りと書き込みの両方が可能です。
push_back、emplace_back関数
vectorに要素を追加するにはpush_back
関数、emplace_back
関数を使用します。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
for (size_t i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << std::endl;
}
//1 2 3 4
std::cin.get();
}
push_back
/emplace_back
関数はvectorの末尾に新しい要素を追加します。
両者はほぼ同じものとして動作しますが、内部的な動作が異なります。
ごく簡単に言えば、emplace_back
関数の方が高速に動作する可能性があります。
戻り値はありません。
C++17からemplace_back
関数は挿入した要素への参照を返します。
push_back
関数は「既に存在するオブジェクトを追加する」という動作を行いますが、emplace_back
などの「emplace」系の関数は「オブジェクトをvectorクラス側で生成して追加する」という動作が可能です。
以下のサンプルコードはクラスの機能の理解が必要なので、まだ学習していない場合は読まなくてもかまいません。
class TestClass
{
public:
TestClass(int a, int b) {}
};
int main()
{
std::vector<TestClass>vec;
//push_back関数は
//すでに存在するインスタンスを追加する
vec.push_back(TestClass(1, 2));
//emplace_back関数でも同じことはできる
vec.emplace_back(TestClass(1, 2));
//emplace_back関数は
//目的のクラスのコンストラクタに指定する値を
//引数に直接指定することができる
vec.emplace_back(1, 2);
//これはコンパイルエラー
//vec.push_back(1, 2);
}
vec.push_back(TestClass(1, 2));
は、まずTestClass
のコンストラクタを実行してインスタンスを生成した上でpush_back
関数を実行してvec
に追加する、という手順になります。
それに対してvec.emplace_back(1, 2);
は、引数の「1, 2」をvectorクラスに渡し、vectorクラスの内部でTestClass
のインスタンスを生成してvec
に追加する、という動作をします。
これにより、コピーやムーブにかかるコストを省くことができます。
#include <iostream>
#include <vector>
class TestClass
{
public:
//explicitで変換コンストラクタは禁止している
explicit TestClass(int a = 0) {
std::cout << "default constructor" << std::endl;
}
TestClass(const TestClass &tc) {
std::cout << "copy cconstructor" << std::endl;
}
TestClass(TestClass&& tc) noexcept {
std::cout << "move constructor" << std::endl;
}
};
int main()
{
std::vector<TestClass>vec;
vec.reserve(3); //あらかじめcapacityを拡張しておく
//A:
//事前にインスタンスを生成して追加
TestClass tc(1);
vec.emplace_back(tc);
std::cout << std::endl;
//B:
//やってることは最初と同じだが
//こちらはムーブ可能
vec.emplace_back(TestClass(2));
std::cout << std::endl;
//C:
//TestClassのコンストラクタ引数を直接記述する
vec.emplace_back(3);
std::cin.get();
}
default constructor copy constructor default constructor move constructor default constructor
AとBはTestClass
のデフォルトコンストラクタ(インスタンスの生成)以外にもコピーコンストラクタやムーブコンストラクタが呼び出されているのが分かります。
Cはデフォルトコンストラクタの呼び出しのみで、コピー/ムーブコンストラクタの呼び出しが省かれるので高速になります。
insert関数
vectorの末尾ではなく途中にデータを挿入するにはinsert
関数を使用します。
std::vector<int> vec{ 1, 2, 3 };
//先頭+1の位置に7を挿入
vec.insert(vec.begin() + 1, 7);
//1 7 2 3
//末尾に3個の9を挿入
vec.insert(vec.end(), 3, 9);
//1 7 2 3 9 9 9
挿入位置の指定(第一引数)にはイテレータを使用します。
begin() + 1
で1番目(先頭要素は0番目)の手前に新しい要素を挿入できます。
つまり、挿入された要素の番号は1番になります。
戻り値は挿入した要素へのイテレータです。
ただしC++03では「insert(位置, 要素)」以外のオーバーロードでは戻り値はありません。
insert
関数は任意の場所に要素を追加できますが、vectorクラスは末尾への要素追加は高速だが途中への要素追加は低速という特徴があります。
コンテナ型には多数の種類があり、任意位置への挿入が高速なクラスもあります。
挿入処理を何度も行うのであれば、別のコンテナ型を利用した方が良いでしょう。
(とはいえ、要素数が少なければ体感できるほどの差はありません)
insert
関数は別のvectorの追加も可能です。
std::vector<int> vec1{ 1, 2, 3 };
std::vector<int> vec2{ 4, 5, 6 };
//1 4 5 6 2 3
vec1.insert(vec1.begin() + 1, vec2.begin(), vec2.end());
以下のようにすればvector同士を連結することもできます。
std::vector<int> vec1{ 1, 2, 3 };
std::vector<int> vec2{ 4, 5, 6 };
//vec1の後ろにvec2を追加
vec1.insert(vec1.end(), vec2.begin(), vec2.end());
//C++11以降は初期化子リストを連結することも可能
vec1.insert(vec1.end(), { 7, 8, 9 });
emplace関数
emplace
関数はinsert
関数と同じく途中に要素を挿入します。
これはpush_back
関数とemplace_back
の関係と同じで、動作結果は同じですが内部動作が異なり、こちらの方が高速に動作する可能性があります。
戻り値は挿入した要素へのイテレータです。
pop_back関数
末尾要素を削除するにはpop_back
関数を使用します。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
vec.pop_back();
//1 2 3 4
戻り値はありません。
push_back
関数と同じく、末尾要素の削除は高速に動作します。
vectorクラスは要素を削除した場合、要素数はもちろん減少しますが、メモリ上に占有しているサイズは減少しません。
メモリを解放する方法は後述します。
erase関数
任意の位置の要素の削除はerase
関数を使用します。
std::vector vec{ 1, 2, 3, 4, 5 };
vec.erase(vec.begin() + 1);
//1 3 4 5
erase
関数は途中の要素を削除できます。
戻り値は削除された次の要素を指すイテレータです。
次の要素がない場合はvectorの終端を指すイテレータです。
insert
関数と同じく、途中の要素の削除は低速となります。
以下のようにすれば範囲を指定して要素を削除できます。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
vec.erase(vec.begin() + 1, vec.end() - 1);
//1 5
clear関数
要素をすべて削除するにはclear
関数を使用します。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
//vecを空にする
vec.clear();
戻り値はありません。
他の削除系関数と同じく、メモリは解放されません。
swap関数
vector同士の内容を入れ替えるにはswap
関数を使用します。
std::vector<int> vec1{ 1, 2, 3 };
std::vector<int> vec2;
//vec1とvec2を入れ替え
vec1.swap(vec2);
戻り値はありません。
resize関数
要素数を変更するにはresize
関数を使用します。
std::vector<int> vec{ 1, 2, 3 };
//要素数を5に拡張
//1 2 3 0 0
vec.resize(5);
//要素数を2に縮小
//1 2
vec.resize(2);
//要素数を5に拡張し、指定した値で埋める
//1 2 99 99 99
vec.resize(5, 99);
戻り値はありません。
resize
関数で要素数を拡張した場合、自動的に0で埋められます。
(要素がクラス型の場合はデフォルトコンストラクタが呼ばれます)
現在の要素数よりも少ない値を指定した場合、はみ出した分は削除されます。
要素の拡張は、拡張した要素を埋める値を第二引数に指定することができます。
他の削除系関数と同じく、要素を削除してもメモリは解放されません。
assign関数
特定の値で要素を埋めるにはassign
関数を使用します。
std::vector<int> vec1{ 1, 2 };
std::vector<int> vec2{ 1, 2 };
std::vector<int> vec3{ 1, 2, 3, 4, 5 };
//5個の9で埋める
vec1.assign(5, 9);
//9 9 9 9 9
//リストで埋める
vec2.assign({ 9, 8, 7, 6, 5 });
//9 8 7 6 5
//他のvectorの範囲で埋める
vec3.assign(vec2.begin() + 1, vec2.end() - 1);
//8 7 6
戻り値はありません。
resize関数は元のvectorの要素を保持したまま要素数を増減させますが、assign
関数は指定した値で要素を埋めてしまいます。
元よりも少ない要素数で埋めなおした場合、要素数は少なくなりますが、他の削除系関数と同じくメモリは解放されません。
reserve関数
vectorはあらかじめ内部に確保しているメモリ領域があり、要素はこのメモリ上に配置されます。
要素の追加により確保メモリの容量が足りなくなると、新しいメモリ領域が自動的に確保されます。
メモリ確保はそれなりに重たい処理なので、ループ文などで要素の追加を繰り返すとメモリ確保処理が頻発する可能性があり、パフォーマンスに影響します。
追加されるデータの総容量があらかじめわかっている場合、前もってメモリ容量を確保しておくことでパフォーマンスを改善できます。
そのための関数がreserve
関数です。
std::vector<int> vec1;
std::vector<int> vec2;
//vec2はあらかじめメモリ容量を確保しておく
vec2.reserve(100000);
for (size_t i = 0; i < 100000; i++)
{
vec1.push_back(i);
}
//こちらの方が高速に動作する可能性がある
for (size_t i = 0; i < 100000; i++)
{
vec2.push_back(i);
}
引数は確保したい要素数の指定です。
戻り値はありません。
サンプルコードの後半は、あらかじめint型100000(十万)個分のサイズをメモリに確保した上でデータを追加しています。
数十、数百程度の追加ではほとんど差はありませんが、大量に追加する場合には差が出てきます。
なお、reserve
関数が行うのは「メモリの確保」だけで、メモリ解放には使用できません。
現在確保されているサイズ以下の値を指定しても何も起こりません。
十万回push_back
するからといって十万回メモリ確保処理が発生するわけではありません。
確保したメモリが足りなくなった場合、足りなくなった分だけを確保するのではなく、ある程度余裕を持った大きさのメモリを確保します。
(Visual C++2015では、現在確保しているサイズの半分を新たに確保するようです)
そのため、実際に行われるメモリ確保処理の回数はずっと少なくなります。
capacity関数
vectorクラスは現在必要な分だけメモリを確保しているわけではありません。
現在確保しているメモリサイズが足りなくなったら、ある程度の大きさのメモリを改めて確保します。
(実際に確保されるサイズは処理系依存です)
現在確保しているサイズを確認するにはcapacity
関数を使用します。
//Visual C++ 2015の場合
//要素数100を確保
std::vector<int> vec(100, 0);
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//100 100
vec.push_back(0);
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//101 150
size
関数は単純に「現在の要素数」を返します。
capacity
関数は「現在確保しているメモリサイズ(要素数)」を返します。
capacity
関数で返される値よりも要素数を増やそうとするとメモリの確保処理が発生します。
max_size関数
確保可能な要素の最大値はmax_size
関数で取得できます。
これはシステムまたはライブラリの実装による最大値で、実際にそのサイズのメモリが確保できることを保証するものではありません。
std::vector<int> vec;
std::cout << vec.max_size() << std::endl;
//実行環境により出力は異なる
shrink_to_fit関数
要素の追加や削除などを行うと、要素数とメモリサイズが一致しなくなります。
別にそのままにしておいても実害はありませんが、大量の要素を削除した後などに無駄なメモリを解放したい場合はshrink_to_fit
関数を使用します。
この関数はC++11から使用可能です。
//Visual C++ 2015の場合
//要素数100を確保
std::vector<int> vec(100, 1);
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//100 100
vec.push_back(1);
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//101 150
//不要なメモリを解放
vec.shrink_to_fit();
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//101 101
戻り値はありません。
shrink_to_fit
関数によって、size
関数とcapacity
関数の値が一致するようになります。
なお、関数内で宣言したvector変数のインスタンスは関数を抜けた時に自動的に破棄されます。
(ローカル変数なので)
この時にメモリもすべて解放されるので、普段はそこまでメモリに対して神経質にならなくても問題ありません。
むしろ、ちまちまメモリ解放処理をするとパフォーマンスが落ちるかもしれません。
厳密にはshrink_to_fit
関数はメモリ解放の要求をするだけで、実際に解放されるかは実装依存です。
swap関数によるメモリ解放
shrink_to_fit
関数は比較的新しく追加された関数です。
それ以前ではswap
関数を利用したメモリ解放が行われていました。
//Visual C++ 2015の場合
//要素数100を確保
std::vector<int> vec(100, 1);
//100 100
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
vec.push_back(0);
//101 150
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//不要なメモリを解放
//存在する要素は残す
std::vector<int>(vec).swap(vec);
//101 101
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//不要なメモリを解放
//全ての要素を消去する
std::vector<int>().swap(vec);
//0 0
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
処理としては新しいvectorインスタンスに既存のvectorインスタンスをコピーし、さらにswap
関数で中身を入れ替えています。
こうすることでcapacity
が新しいインスタンスのものになります。
これは俗にswap技法と呼ばれます。
shrink_to_fit
関数が実装されてから使用されなくなった方法ですが、やや古めのコードには登場することがあります。
data関数
data
関数は、vectorが管理するメモリの先頭要素へのポインタを返します。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
size_t size = vec.size();
int *p = vec.data();
//int *p = &vec[0]; //同じ
for (size_t i = 0; i < size; i++) {
std::cout << p[i] << " ";
}
//1 2 3 4 5
vectorの各要素は配列のようにメモリ上に連続して配置されるため、配列と同じようにアクセスが可能です。
ただしこの関数の実行後に元のvectorを操作することでメモリが再確保されると、ポインタは無効になるので注意して下さい。
なお空のvectorに対してこの関数を実行した場合、関数自体は成功しますがその戻り値は不定であるため使用できません。