vectorクラス
配列に代わる機能2
C++には配列のようなデータの集合をより便利に扱うために、様々な機能が用意されています。
(コンテナ型)
arrayクラスもその一つですが、より便利に扱えるのがvectorクラスです。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
for (int i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << "\n";
}
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 };
std::vector<int> vec2 = { 1, 2, 3, 4, 5 };
//要素数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);
要素数だけ指定して宣言しても、自動的にすべて0で埋められるため安全にアクセスできます。
最後の方法はやや特殊な書き方で、イテレータというものを使用しています。
これについては後ほと説明します。
代入操作
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();
}
arrayクラス、stringクラスと同じくat関数を使用する方法もあります。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
vec.at(1) = 10;
//10
std::cout << vec.at(1) << std::endl;
//例外発生
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 (int i = 0; i < vec.size(); i++)
{
for (int j = 0; j < vec[i].size(); j++)
{
std::cout << vec[i][j] << " ";
}
std::cout << "\n";
}
std::cin.get();
}
arrayクラスの時と比べて、配列とほぼ同等の記述ができます。
関数
arrayクラスですでに解説した機能は簡単な説明に留めます。
(→arrayクラス)
size関数
要素数の取得にはsize関数を使用します。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
for (int 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);
//1 2 3 4
for (int i = 0; i < vec.size(); i++)
{
std::cout << vec[i] << std::endl;
}
std::cin.get();
}
push_back/emplace_back関数はvectorの末尾に新しい要素を追加します。
両者はほぼ同じものとして動作しますが、内部的な動作が異なります。
やや難解な話となりますので詳細は省きますが、emplace_back関数の方が高速に動作する場合が多いです。
insert関数
vectorの末尾ではなく途中にデータを追加するにはinsert関数を使用します。
std::vector<int> vec{ 1, 2, 3 };
//1 99 2 3
vec.insert(vec.begin() + 1, 99);
先頭要素を0番とし、1番目の要素の手前に新しい要素を追加するには上記のように書きます。
第一引数の書き方はイテレータを利用しています。
(イテレータについては後述)
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());
emplace関数
emplace関数はinsert関数と同じく途中に要素を追加します。
これはpush_back関数とemplace_backの関係と同じで、動作結果は同じですが、内部動作が異なり処理速度に差があります。
pop_back関数
要素を削除するにはpop_back関数を使用します。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
//1 2 3 4
vec.pop_back();
pop_back関数は末尾要素をひとつ削除します。
push_back関数と同じく、末尾の削除は高速に動作します。
vectorクラスは要素を削除した場合、要素数はもちろん減少しますが、メモリ上に占有しているサイズは減少しません。
メモリを開放する方法は後述します。
erase関数
要素の削除はerase関数を使用する方法もあります。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
//1 3 4 5
vec.erase(vec.begin() + 1);
erase関数は途中の要素を削除できます。
insert関数と同じく、途中の要素の削除はやや低速となります。
erase関数もイテレータを利用します。
イテレータについては後述します。
以下のようにすれば範囲を指定して要素を削除できます。
削除したい要素が連続している場合はこちらの方が高速です。
std::vector<int> vec{ 1, 2, 3, 4, 5 };
//1 5
vec.erase(vec.begin() + 1, vec.end() - 1);
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);
std::cin.get();
resize関数
要素数を変更するにはresize関数を使用します。
std::vector<int> vec{ 1, 2, 3 };
//要素を拡張
//1 2 3 0 0
vec.resize(5);
//要素を縮小
//1 2
vec.resize(2);
//要素を拡張し、指定した値で埋める
//1 2 99 99 99
vec.resize(5, 99);
resize関数で要素数を拡張した場合、自動的に0で埋められます。
現在の要素数よりも少ない値を指定した場合、はみ出した分は削除されます。
第二引数に値を指定して要素を拡張した場合、第二引数の値で要素が埋められます。
他の削除系関数と同じく、要素を削除してもメモリは解放されません。
assign関数
特定の値や範囲で要素を埋めるにはassing関数を使用します。
std::vector<int> vec1{ 1, 2 };
std::vector<int> vec2{ 1, 2 };
std::vector<int> vec3{ 1, 2, 3, 4, 5 };
//5個の9で埋める
//9 9 9 9 9
vec1.assign(9, 5);
//リストで埋める
//9 8 7 6 5
vec2.assign({ 9, 8, 7, 6, 5 });
//他のvectorの範囲で埋める
//8 7 6
vec3.assign(vec2.begin() + 1, vec2.end() - 1);
resize関数は元のvectorの要素を保持したまま要素数を増減させますが、assign関数は指定した値でvectorを埋めてしまいます。
元よりも少ない要素数で埋めなおした場合、要素数は少なくなりますが、他の削除系関数と同じくメモリは解放されません。
reserve関数
vectorに要素を追加する場合、それに必要な容量が自動的にメモリ上に確保されます。
メモリ容量確保はそれなりに重たい処理なので、for文などで大量に要素を追加すると何度もメモリ確保処理を行うことになり、パフォーマンスに影響します。
追加したいデータの大きさがあらかじめわかっている場合、前もってメモリ容量を確保しておくことでパフォーマンスを改善できます。
そのための関数がreserve関数です。
//特に何もしない場合
std::vector<int> vec1;
for (int i = 0; i < 100000; i++)
{
vec1.push_back(i);
}
//あらかじめメモリ容量を確保しておく
std::vector<int> vec2;
vec2.reserve(100000);
for (int i = 0; i < 100000; i++)
{
vec2.push_back(i);
}
サンプルコードは、あらかじめ100000(十万)分のサイズをメモリに確保した上でデータを追加しています。
数十、数百程度の追加ではほとんど差はありませんが、大量に追加する場合には差が出てきます。
十万回push_backするからといって十万回メモリ確保処理が発生するわけではありません。
確保したメモリが足りなくなった場合、足りなくなった分だけを確保するのではなく、ある程度余裕を持った大きさのメモリを確保します。
(Visual C++ 2015では、現在確保しているサイズの半分を新たに確保するようです)
そのため、実際に行われるメモリ確保処理の回数はずっと少なくなります。
なお、reserve関数が行うのは「メモリの確保」だけで、メモリ解放には使用できません。
現在確保されているサイズ以下の値を指定しても何もおこりません。
capacity関数
vectorクラスは現在必要な分だけメモリを確保しているわけではありません。
要素の追加や削除、reserve関数などによって「現在の要素数」と「メモリ上に確保されているサイズ」が一致しなくなります。
(これは高速化のための動作です)
これを確認するにはcapacity関数を使用します。
//Visual C++ 2015の場合
//要素数100を確保
std::vector<int> vec(100, 0);
//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;
size関数は単純に「現在の要素数」を返します。
capacity関数は「メモリ上に確保しているサイズ(要素数)」を返します。
つまり、capacity関数で返される値以上に要素数を増やそうとするとメモリの確保処理が発生します。
shrink_to_fit関数
要素の追加や削除などを行うと、要素数とメモリサイズが一致しなくなります。
別にそのままにしておいても実害はありませんが、巨大なvectorを空にした場合など、メモリを解放したい場合はshrink_to_fit関数を使用します。
//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(1);
//101 150
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
//不要なメモリを解放
vec.shrink_to_fit();
//101 101
std::cout << vec.size() << " ";
std::cout << vec.capacity() << std::endl;
shrink_to_fit関数によって、size関数とcapacity関数の値が一致するようになります。
なお、関数内で宣言したvectorは関数が抜けた時に自動的に破棄されます。
(ローカル変数なので)
この時にメモリもすべて解放されますので、普段はそこまでメモリに対して神経質にならなくても問題ありません。
むしろ、ちまちまメモリ解放処理をするとパフォーマンスが落ちるかもしれません。
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;
これは俗にswap技法と呼ばれます。
shrink_to_fit関数が実装されてから使用されなくなった方法ですが、やや古めのコードには登場することがあります。