C++11 固定長配列クラス std::array とは
std::array は C++11 で追加された固定長配列のコンテナクラスだ。
通常配列では、std::vector の size() や back() などの便利な機能が使えない。 それらを使いたいけど、サイズは固定でいいので vector を使うほどでもない。ってときは std::array の出番だ。
データ構造
array は vector や list のようなデータ構造を持たず、通常配列と同じように要素のためにだけメモリを消費する。
その分、vector に比べるとメモリ効率がいいのだが、要素数が多い場合はその効果は無視できるほどだ。
array の交換においては、中身のデータを交換するしかなく O(N) の処理時間を要する。 vector 等であればデータ領域へのポインタを交換するだけなので O(1) の処理時間で済む。
どちらかが一方的に優れているというわけではないので、状況によって賢く使い分けて欲しい。
準備:インクルード
まずは、array を使うための準備だ。
array は C++標準のライブラリであり、「#include <array>」を記述することで利用可能になる。
名前空間は「std」なので、使用の度に「std::」を前置するか、または「using namespace std;」を記述しておく。
#include <array> // ヘッダファイルインクルード int main() { std::array<int, 10> ar; // ローカル変数として、ar を生成 ..... }
#include <array> // ヘッダファイルインクルード using namespace std; // 名前空間指定 int main() { array<int, 10> ar; // ローカル変数として、ar を生成 ..... }
宣言・初期化
宣言
array を宣言するときは std::array<型, 要素数> オブジェクト名; で宣言する。 int 型で、要素数10個の array ar を宣言する場合は、以下のように記述する。
std::array<int, 10> ar; // int型、要素数10
これは、通常配列の int ar[10]; と同じ意味だ。
要素数は 0 以上の整数を指定します。要素数0は特に使い道はないが、コンパイルエラーにはならない。
このようにして宣言した array は通常の配列と同じように使用することが出来る。 単に宣言した場合、要素はデフォルト値(整数であれば0)に初期化される。
const int SZ = 10; std::array<int, SZ> ar; // int型、要素数10 for(int i = 0; i < SZ; ++i) ar[i] = i;
初期化リストによる初期化
初期化リストを使って、各要素を初期化することが出来る。
std::array<int, 4> ar{1, 2, 3, 4}; // int型、要素 = {1, 2, 3, 4}
初期化リストの要素数が配列要素数より少ない場合、デフォルト値で初期化される。
std::array<int, 4> ar{1, 2}; // ar[2], ar[3] は 0 で初期化される。
初期化リストの要素数が配列要素数より多いと、コンパイルエラーとなる。
std::array<int, 2> ar{1, 2, 3, 4}; // コンパイルエラーとなる
通常配列であれば、int ar[] = {1, 2, 3, 4}; のように、要素数を省略できるが、 array の場合は、要素数を省略できないようだ(VSC2013)。
コピーコンストラクタ
コピーコンストラクタとは、同じ型のオブジェクトを渡され、それと同じ内容のオブジェクトを生成するコンストラクタのことである。
「std::array<型, サイズ> オブジェクト名(コピー元オブジェクト名);」と記述する。
当然ながら、コピー元オブジェクトと生成するオブジェクトは通常同じ型である。また、サイズも同一でないといけない。
std::array<int, 3> org{1, 2, 3}; std::array<int, 3> x(org); // コピーコンストラクタ
上記のコードは org をコピーするので {1, 2, 3} という値をもつ固定長配列 x を生成する。
演習問題
- int型、要素数5 の固定長配列 ar を宣言しなさい。
- string型、要素数10 の固定長配列 strs を宣言しなさい。
- int型、要素数5x5 の2次元固定長配列 ar2 を宣言しなさい。
- int型、要素数5 の固定長配列 ar を {1, 2, 3, 4, 5} で初期化しなさい。
std::array<int, 5> ar;
std::array<std::string, 10> strs;
std::array<std::array<int, 5>, 5> ar2;
std::array<int, 5> ar{1, 2, 3, 4, 5};
値の参照、代入
[] 演算子(operator[](int)) を使って、普通の配列と同じように、配列要素値の参照・代入が可能。
operator[](int) と聞くと身構える人がいるかもしれないが、「ar[10]」の様に、普通の配列の要素にアクセスする時と同じ記述だ。
恐れることは何も無い。
下記は、配列要素値の参照と代入例。普通の配列要素の参照・代入とまったく同じでしょ。
const int SZ = 10; std::array<int, SZ> ar{3, 1, 4, 1, 5, 9, 2, 6, 5, 3}; for (int i = 0; i < SZ; ++i) std::cout << ar[i]; // ar の i 番目の要素を表示
const int SZ = 10; // 要素数 std::array<int, SZ>v; // 指定要素数で、array を生成 for(int i = 0; i < SZ; ++i) v[i] = i; // 要素を 0, 1, 2, 3, ... 9 に設定
ix 番目の要素へのポインタ
ix 番目の要素へのポインタを取得したい場合は &ar[ix] とする。これも通常配列とまったく同じだ。
演習問題
- int型、要素数10000 の固定長配列 ar を {1, 2, 3, ... 10000} で初期化しなさい。
const int N = 10000; std::array<int, N> ar; for(int i = 0; i < N; ++i) { ar[i] = i + 1; }
メンバ関数
array の状態を取得
- empty() : bool
- size() : size_t
empty() : bool
「bool empty()」は配列が空かどうかを判定する関数。空ならば true を、空でなければ false を返す。
次に出てくる size() を使って、size() == 0 と判定するのと同等だ。
が、コンテナクラスによっては size() 計算よりも empty() の方が高速な場合がある。
なので、array などのコンテナクラスに対しては empty() を使うことが推奨されている。
if( !ar.empty() ) { // ar が空でなければ、なんらかの処理を行う }
size() : size_t
「size_t size()」は、要素数を返す関数。
通常配列だと「sizeof(data)/sizeof(data[0])」のように記述しないと、要素数を取得できないが、
array であれば「ar.size()」と簡潔かつ分かりやすく書ける。
※ size_t はサイズを表す型で、符号なし整数の組み込み型である。ちなみに、sizeof() も size_t 型を返す。
for(int i = 0; i != ar.size(); ++i) { // 全要素に対するループ ..... }
array の要素を取得
- front() : 要素の型への参照
- back() : 要素の型への参照
- data() : 要素の型へのポインタ
front() : 要素の型への参照、back() : 要素の型への参照
「front()」は先頭要素への参照を返す関数。「オブジェクト名[0]」と記述するのと同等。
「back()」は末尾要素への参照を返す関数。「オブジェクト名[オブジェクト名.size() - 1]」と記述するのと同等。
front() はあまりありがたみが無いが、back() はタイプ数が大幅に減るし、分かりやすいので存在価値が高いぞ。
if( !ar.empty() ) { auto f = ar.front(); // 最初の要素 auto b = ar.back(); // 最後の要素 ... }
front(), back() ともに、値を返すのではなく、最初・最後の要素への参照を返すので、最初・最後の要素へ代入することも出来るぞ。
std::array<int, 4> ar{1, 2, 3, 4}; ar.front() = 11; // 最初の要素を 11 に変更 ar.back() = 44; // 最後の要素を 44 に変更
data() : 要素の型へのポインタ
「data()」は配列データ先頭アドレスを返す関数。「&オブジェクト名[0]」と記述するのと同等。
array は通常配列と同じようにデータ領域が連続したアドレスだということが保証されている。
なので、データアドレスを他の関数に渡して処理することも可能だ。
std::array<int, 4> ar{1, 2, 3, 4}; int *ptr = ar.data(); // ptr は ar の先頭要素へのポインタ cout << *ptr << "\n"; // ptr が指す先の値:1 が表示される
array の状態を変更
- fill(値) : void
- swap() : void
fill(値) : void
fill(値) は、array の全ての要素を指定した値にする関数。
std::array<int, 10> ar; ar.fill(123); // 全ての要素を 123 に
swap() : void
「swap(オブジェクト名)」は引数で指定されたオブジェクトと内容を入れ替える関数。 入れ替える array は型、要素数ともに同じでなくてはいけません。
std::array<int, 10> v, z; v, z にデータを追加 v.swap(z); // v と z の内容を入れ替える
vector の場合オブジェクトが持つデータ領域へのポインタ等を交換するだけなので、処理時間は O(1) だが、 array の場合は要素の全てを交換するので、O(N) となる。
イテレータ
- begin() : iterator
- end() : iterator
- cbegin() : const_iterator
- cend() : const_iterator
begin() : iterator、end() : iterator
begin() は最初の要素へのイテレータ、end() は最後の要素の次へのイテレータを返します。
int i = 0; for(auto itr = ar.begin(); itr != ar.end(); ++itr, ++i) *itr = i;
cbegin() : const_iterator、cend() : const_iterator
cbegin() は最初の要素への const イテレータ、cend() は最後の要素の次への const イテレータを返します。
通常イテレータであれば *itr = 123; の様に、指している先の値を変更可能ですが、const イテレータの場合は、
*itr = 123; の様に書くとコンパイルエラーとなります。
for(auto itr = ar.cbegin(); itr != ar.cend(); ++itr) { std::cout << *itr << "\n"; // *itr = 123; // const イテレータの指す先への代入はコンパイルエラーとなる }
演習問題
- std::array<int, 0> ar0; に対して、empty(), size() をコールし、それぞれの値を表示するコードを書きなさい。
- std::array<int, 5> ar5; に対して、empty(), size() をコールし、それぞれの値を表示するコードを書きなさい。
- std::array<int, 3> v{3, 1, 4}; に対して、front(), back() をコールし、それぞれの値を表示するコードを書きなさい。
- std::array<int, 3> v{3, 1, 4}; に対して、data() をコールし、返されたアドレスの内容を確認しなさい。
- std::array<int, 10> ar; を宣言し、fill() を使って、全ての要素の値を 7 にするコードを書きなさい。
- std::array<int, 3> v{3, 1, 4}; std::array<int, 3> z{7, 8, 9}; に対して、v.swap(z); を実行し、それぞれの内容を表示するコードを書きなさい。
- vector はサイズが異なっているものと swap で交換できるのに、array は交換できないのは何故か?理由を考えなさい。
std::array<int, 0> ar0; cout << ar0.empty() << "\n"; cout << ar0.size() << "\n";
std::array<int, 5> ar; cout << ar5.empty() << "\n"; cout << ar5.size() << "\n";
std::array<int, 3> v{3, 1, 4}; cout << v.front() << "\n"; cout << v.back() << "\n";
std::array<int, 3> v{3, 1, 4}; int *ptr = v.data();
std::array<int, 10> ar; ar.fill(7);
std::array<int, 3> v{3, 1, 4}; std::array<int, 3> z{7, 8, 9}; v.swap(z); for(int i = 0; i < (int)v.size(); ++i) std::cout << v[i] << "\n"; for(int i = 0; i < (int)z.size(); ++i) std::cout << z[i] << "\n";
アルゴリズム
実は C++ には結構便利なアルゴリズムがいくつも用意されている。
参照:http://www.cplusplus.com/reference/algorithm/
ここでは、array に対して特によく使うであろうアルゴリズムを3つだけ簡単に説明する
- accumulate(first, last, 初期値) : 要素型
- sort(first, last) : void
- reverse(first, last) : void
accumulate(first, last, 初期値) : 要素型
「accumulate(オブジェクト名.begin(), オブジェクト名.end(), init)」は、コンテナの最初から最後まで、要素を積算(全加算)するものだ。
最後の引数は初期値で、通常は0を指定する。
なお、accumulate を利用するには「#include <numeric>」が必要。
#include <numeric> ..... std::array<int, 10> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::cout << std::accumulate(v.begin(), v.end(), 0) << "\n";
一部を積算したい場合は、「accumulate(v.begin() + i, v.begin() + k, 0)」の様にイテレータで範囲を指定する。
「accumulate(&v[i], &v[k], 0)」のように、要素へのポインタで指定することも可能。
sort(first, last) : void
「sort(オブジェクト名.begin(), オブジェクト名.end())」は、コンテナの要素を昇順にソートしてくれる関数だ。
なお、sort を利用するには「#include <algorithm>」が必要。
#include <algorithm> ..... std::array<int, 11> v{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}; std::sort(v.begin(), v.end());
一部をソートしたい場合は、「sort(v.begin() + i, v.begin() + k)」の様にイテレータで範囲を指定する。
「sort(&v[i], &v[k])」のように、要素へのポインタで指定することも可能。
reverse(first, last) : void
「reverse(オブジェクト名.begin(), オブジェクト名.end())」は、コンテナの要素を逆順にする関数だ。
std::array<int, 11> v{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}; std::reverse(v.begin(), v.end()); // 順序反転 for(auto x : v ) std::cout << x << " "; std::cout << "\n";
演習問題:
- int 型、要素数100個の固定長配列を作成し、各要素を [0, 99] の範囲でランダムに設定し、accumulate() を使い、それらの合計を求め表示しなさい。
- int 型、要素数100個の固定長配列を作成し、各要素を [0, 99] の範囲でランダムに設定し、sort() を使い、それを昇順にソートしなさい。
- int 型、要素数100個の固定長配列を作成し、各要素を [0, 99] の範囲でランダムに設定し、sort(), reverse() を使い、それを降順に並べ替えなさい。
#include <numeric> ..... std::array<int, 100> v; for(int i = 0; i < (int)v.size(); ++i) v[i] = rand() % 100; std::cout << std::accumulate(v.begin(), v.end(), 0) << "\n";
#include <numeric> ..... std::array<int, 100> v; for(int i = 0; i < (int)v.size(); ++i) v[i] = rand() % 100; std::cout << std::accumulate(v.begin(), v.end(), 0) << "\n";
#include <algorithm> ..... std::array<int, 100> v; for(int i = 0; i < (int)v.size(); ++i) v[i] = rand() % 100; std::sort(v.begin(), v.end()); std::reverse(v.begin(), v.end());
まとめ・参考
std::array は、通常配列とまったく同じものに、std::vector にある便利な各種メンバ関数を追加したようなものだ。 それらを使用したい場合は、使うといいだろう。
個人的には、array 型は要素数を省略できないので、void print(const array<int, > &ar) のような、関数を作ることが出来ないのはちょっと不満だ。
固定長配列であっても vector を使ったとしてもたいした差はないので、現状では、常に vector を使う方がいいのではないかと考えている。