ys memos

Blog

C++のvectorの使い方


cpp

2020/04/23

C++リファレンスC++日本語リファレンスで使い方が理解できる方にとってはあまり意味をなさない情報かもしれません。

また、私の Qiita 上の vector 記事のブラッシュアップバージョンとなっております。


動的配列のシーケンスコンテナの1つ。 機能はarrayと似ているが、具体的な理由で選ぶ事ができない限りはvectorを使う方が無難と私は考えている。


new/deleteは、動的にメモリを確保/解放する C++の機能である。 きちんと使用すればvectorよりも高速(なはず!)だが、具体的な理由が無い限りvectorを使う方が無難と私は考えている(二度目)。 理由として、newで確保されたメモリは、変数のスコープが切れてもdeleteしない限りは解放されないことが挙げられる。


  1. v.size()で要素数を確認できるため、関数に渡す時に要素数を別で送らなくてよい
  2. v.size()で for 文を扱う事で配列外参照を起こしづらい
  3. algorithmライブラリを利用可能
  4. 範囲 for 文(range-based-for)を利用可能
  5. v.push_back(value)で要素を追加できる
  6. =演算子で普通の代入のようにコピーできる
  7. 値渡しのように関数の引数にできる(参照渡しも可)
  8. 関数の戻り値にできる
  9. eraseで指定した要素を削除できる

vectorは非常に便利な反面、使い方を誤ると利点を欠点にしてしまう事があるので、注意が必要である。 欠点というよりも注意点と行った方が語弊がないかもしれない。

  1. v.push_back(value)で要素を追加できてしまう
  2. =演算子でコピーできてしまう
  3. 値渡しのように関数の引数にできてしまう
  4. 関数の戻り値にできてしまう
  5. eraseで指定した要素を削除できてしまう

欠点に挙げた内容は、何故なのかはだんだんとわかってくるものである。 わからない間は思い思いに書くのも手かなと考えることもある(ただし、念頭に置くことは大切)。

また、これらは使い方次第で毒にも薬にもなるため、一概に利点とも欠点とも明言することは困難だと考えている。

実際に困った問題が発生するまでは深く気にしなくても良いかもしれませんが、気づいた時からは改めるべきである。



vector使用のために必要なライブラリは以下でインクルード可能。 また、簡単のためコードサンプルには以下の変数を用いるものとする。 本記事内では、配列の要素はint型で例をあげるが、実際に使う場合はintを好きな型・クラスを置き換える事ができる。

#include <vector>
using namespace std;
constexpr int n = 100; // 配列の要素数を指す変数
constexpr int m = 100; // 配列の要素数を指す変数2
constexpr int value = 0; // 配列の初期値を指す変数

vector<int> v; // 空の配列
vector<int> v(); // 空の配列
vector<int> v(n); // n要素配列を値の代入なしで宣言(intだと0, boolだとfalseのように初期化される)
vector<int> v(n, value); // n要素配列を値`value`で宣言

vector<double> vd; // doubleの場合
vector<string> vs; // doubleの場合
vector<myClass> vMyClass; // 自分で定義したクラスmyClassの場合

行列のような表現をする方法。言葉で表すとすると「vector を要素にもつ vector」と言える。

vector<vector<int>> vv; // 空の配列を0個持つ配列を宣言
vector<vector<int>> vv(); // 空の配列を0個持つ配列を宣言
vector<vector<int>> vv(n); // 空の配列をn個持つ配列を宣言
vector<vector<int>> vv(n, vector<int>(m)); // m個の要素を持つ配列をn個持つ配列
vector<vector<int>> vv(n, vector<int>(m, value)); // 値がvalueの要素をm個持つ配列をn個持つ配列

本来であればvectorによる多次元配列はお勧めではない。しかし、これに関しても気づいた時に修正していけば良いだろう。

ほとんど使わないであろうが、「配列を持つ配列を持つ配列」を表現する事で使用可能。 ここまでくると、工夫する事で次元を削減するなど、他の実装を考えた方が良いであろう。

vector<vector<vector<int>>> vvv;
vector<vector<vector<vector<int>>>> vvv;

vectorでは、通常の配列と同様の書き方で要素へアクセスする事ができる。 初期化については省略する。 また、vectorには、at()関数という要素アクセサも準備されており、これは配列外参照を検知して例外を投げてくれる。安全性は増すが、「確実に配列内に存在するインデックスが配列内であるか確認する」というのはなんとも無駄な事であるため、使用するか否かは開発者が責任を持って判断する必要があるといえよう。 at()関数によるプログラム実行時の挙動は、後述する。

配列のインデックス番号ivalueを代入してそれを出力する例。

シンプル
v[i] = value; // 代入
cout << v[i] << endl; // 出力

v.at(i) = value; // 配列外参照を検知して代入
cout << v.at(i) << endl; // 配列外参照を検知して出力

2 次元配列のインデックス番号i, jvalueを代入してそれを出力する例。

v[i][j] = value; // 代入
cout << v[i][j] << endl; // 出力

v.at(i).at(j) = value; // 配列外参照を検知して代入
cout << v.at(i).at(j) << endl; // 配列外参照を検知して出力

配列の要素数(配列長)を確認する事ができる。

cout << v.size() << endl; // 要素数nを出力
2 次元配列の一部分
cout << vv.size() << endl; // 「配列をn個持つ配列」のnを出力

cout << vv[i].size() << endl; // 「2次元配列のインデックス番号iのn個の配列」のnを出力

動的配列のシーケンスコンテナという名の通り、配列長を動的に(プログラム実行時に)変更する事ができる。

v.resize(n); // 要素数nの配列
v.resize(n, value); // 要素数n、値valueの配列
2 次元配列の一部分
vv.resize(n); // 2次元、「配列をn個持つ配列」

vv[i].resize(n); // 2次元、「2次元配列のインデックス番号iはn個の配列」
2 次元配列の全体
vv.resize(n);
for(size_t i=0; i<vv.size(); ++i){
	vv[i].resize(n);
} // このforループによって、「n個の配列をn個持つ配列」

配列vの末尾に「要素数を 1 つ増やしつつ値を追加」するためには、push_back(value)を用いる。

v.push_back(value);

int, bool, double等の基礎的なデータ型を用いる場合は、5, false, 0.5のようにプログラム内に値を書き込む事ができるのだが、自身で作成したクラスを要素とする配列をvector<myClass>のように扱っている場合は、emplace_back(value1, value2)を用いる。

この時、myClassはコンストラクタmyClass(int value1, int value2)を持っている必要がある。

vector<myClass> vMyClass; // ここでは、myClassはint型を2つ持つクラスとする。
vMyClass.push_back(0, 1); // このようにする事で、そのまま末尾に値を追加できる。

体感的に一番わかりやすいのが=演算子による代入

vector<int> v1(n, value);
vector<int> v2;
v2 = v1;

宣言直後にコピーする場合は、コピーコンストラクタを用いるのが可読性が高い。

vector<int> v1(n, value);
vector<int> v2(v1);

利点として、(簡単に)関数の引数にできる点がある。この時、関数側からも要素数を参照する事ができるので、配列の要素数等を別途引数にしなくても良い。

ここが、欠点として挙げた、関数の引数にできてしまう点に関係する。 値渡しは変数を丸ごとコピーしてしまうらしく、理由なしに値渡しを用いると、徒らに処理時間がかかってしまう。「関数で vector を使ってるんだけど処理がめちゃくちゃ遅い!!」という方は見直してみて欲しい。

配列の要素の総和を求める関数
値渡し(非推奨)
int sumVector(vector<int> v){
	int sumValue = 0;
	for(size_t i=0; i<v.size(); i++){
		sumValue += v[i];
	}
	return sumValue;
}
参照渡し

具体的な理由がない限りは、const&を用いるの無難であると考える。

const: 関数内で配列の要素の変更を許さない => (開発者が)望まない値の変更を抑制する => 関数内における変更を反映させたい場合はつけない

&: 参照渡し関数へ渡す時にコピーしない => 引数をコピーせずに扱うのでボトルネック(速度低下の原因)になりづらい => 関数内で配列を変更する事が可能

int sumVector(vector<int> const& v){
	int sumValue = 0;
	for(size_t i=0; i<v.size(); i++){
		sumValue += v[i];
	}
	return sumValue;
}

値渡しに関しては省略する。

void view(vector<int> cosnt& v){
	for(size_t i=0; i<v.size(); i++){
		cout << v[i] << " ";
	}
	cout << endl;
}

void view(vector<vector<int>> cosnt& vv){
	for(size_t i=0; i<vv.size(); i++){
		for(size_t j=0; j<vv[i].size(); j++){
			cout << vv[i][j] << " ";
		}
		cout << endl;
	}
}
ベクトルとスカラーの積
vector<int> scalarMultiplication(vector<int> const& v, int s){
	vector<int> mul(v.size());
	for(size_t i=0; i<mul.size(); i++){
		mul[i] = v[i]*s;
	}
	return mul;
}

利点として挙げたalgorithmライブラリの活用について最もわかりやすい恩恵が、1行でソートができる事だと考えられる。

sort(v.begin(), v.end()); //昇順ソート
sort(v.begin(), v.end(), greater<int>()); //降順ソート

erase()によって任意の要素を削除する事ができる。

v.erase(v.begin()); //先頭を削除
v.erase(v.end()); //末尾を削除
v.erase(v.begin()+i); // インデックス番号iを削除

これは[]による配列外参照。プログラミングを学ぶ人が一度は引き起こすであろうエラー。

#include <iostream>
#include <vector>
using namespace std;

int main(){
	vector<int> v(10, 5); // 10は要素数(インデックス番号は0~9)、5は値
	cout << v[10] << endl;
	return 0;
}

これを実行するとエラーは起こさない(配列外参照は未定義動作)。

0

at()関数による配列外参照。

#include <iostream>
#include <vector>
using namespace std;

int main(){
	vector<int> v(10, 5);
	cout << v.at(10) << endl;
	return 0;
}

出力結果が以下で、vector が配列外参照を起こしているという事が実行時にコードを見る事なく判明する。

libc++abi.dylib: terminating with uncaught exception of type std::out_of_range: vector

実際に、プログラムが通常終了しているにもかかわらず挙動がおかしい状況や何故かわからないがプログラムが想定通りの挙動をするというのは、開発中に多くの思考時間を奪われる原因となる。 そのため、不安があればat()関数を用いて開発してみるのも良いかなと感じる。

char型は使い慣れていないため、stringにしたい。ここで以下の1行が非常に便利。

これにより,コマンドライン引数がvector<string>になってくれる.

#include <iostream>
#include <vector>
#include <string>

main(int argc, char **argv){
	vector<string> args(argv, argv+argc);
	// args[0]は実行ファイル名
	// args[1]は第一コマンドライン引数
	// args[2]は第一コマンドライン引数
}

この方法は、コマンドライン引数を活用するための応急処置としての印象である。 C++慣れている人は Boost とか使った方が良い。

訳がわからない場合は使う必要がないのではと感じるが、慣れてくると重宝する。 先ほどの値渡しの例としてあげたview()関数の for ループを範囲 for 文に書き換えてみる。

void view(const vector<int> cosnt& v){
	for(const auto& e : v){
		cout << e << " ";
	}
	cout << endl;
}

void view(const vector<vector<int>> cosnt& vv){
	for(const auto& v : vv){
		for(const auto& e : v){
			cout << e << " ";
		}
		cout << endl;
	}
}

range-based-for は、イテレータを元にループする。v.begin()からイテレータitrを動かしていき、「itrv.end()ではない間」ループする。 イテレータについては割愛する。また機会があれば実装を織り交ぜながら説明していけたらなと考えている。

関連タグを探す