std::vector | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、複数の同じ型の変数をひとまとめにして扱う機能を取り上げます。プログラムの内容によっては、大量のデータを取り扱わなければならないことがありますが、そのデータ1個ごとに1つの変数を宣言していたのでは、num1、num2、num3、・・・num100000 のように大量の変数ができてしまい、ソースコードが大変なことになります。変数を1つにまとめて整理できれば、記述量を大きく減らせるうえに、取り扱いも簡単になります。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



std::vector

蔵書リストのプログラムを作ることを考えると、登録する本の冊数分のデータを扱わなければならないことがわかると思います。1000冊登録したいのなら、変数は 1000個必要ということになりそうです。本を登録した日時を保存する変数が必要だとすると、次のような宣言が並ぶことになります。

// それぞれの本の登録日時を記憶する変数
int book1_registered_date {};
int book2_registered_date {};
int book3_registered_date {};
int book4_registered_date {};
// こうやって 1000個繰り返す?

例として 1000冊といっただけで、本当は 1万冊ぐらいは登録したいかもしれません。そうなったら 1万個の変数を宣言しなければならないことになります。いずれにしても、これでは大変すぎます。

もし、「登録日時を〇冊分記憶できる変数」というものが作れれば便利でしょう。たとえば次のような感じです(これは仮のコードであって、C++ にこのような構文はありません)。

// 1000冊分の本の登録日時を記憶する変数
int books_registered_date x 1000;

books_registered_date という1つの変数で、int型の値を 1000個記憶できるという理想を示したコードです。これはコンパイルできませんが、同じことを、このページのテーマである std::vector によって実現できます。std::vector を使うと、次のように変数宣言できます。

// 1000冊分の本の登録日時を記憶する変数
std::vector<int> books_registered_date(1000);

books_registered_date という1つの変数で、int型の値を 1000個記憶できます。変数books_registered_date は、int型の変数が 1000個連結されたものであると考えていいです。このような、複数の同じ型の変数が連結された構造を、配列 (array) と呼びます。また、配列を構成する1つ1つの変数のことを、要素 (element) と呼びます。

C++ には、配列を作る方法がいくつかあって、std::vector はその1つです。配列を実現するもっとも初歩的な方法では、要素の個数をプログラムの実行中に変化させられない制約があるのですが、std::vector が実現する配列は、要素を追加したり削除したりして、要素数を変化させられます。このような配列は、可変長配列 (variable-length array) とか動的配列 (dynamic array) などと呼ばれます。解説が膨大になってしまうので、要素を増減させる話はこのページでは取り上げません。

なお、std::string も、char型の変数が複数連結されて1つの文字列になっており、配列の一種とみなせます。

変数books_registered_date を宣言するコードから明らかなように、std::vector<int> が型の名前です。単に std::vector と書くのではなく、<int> を末尾に付けることで、この std::vector の要素が int型だと指定したことになります。std::vector<double> とすれば、要素は double型ですし、std::vector<std::string> とすれば、要素は std::string型ということになります。要素の型の違いだけであって、できることに違いはありません。そのため、要素の型のことは考えずに一般的な話をするときには、単に「std::vector」と読んだり表記したりします。

ただし例外的に、std::vector<bool> だけは特別な扱いを受けることになっており、要素が bool型以外のときとは異なる部分があります1。熟知したうえで使うことは問題ありませんが、そうでなければ避けておいたほうがいいかもしれません。

変数books_registered_date の宣言のとき、変数名の後ろに (1000) という記述がありました。これは 1000個の要素を持った状態で初期化しているということです。std::vector の初期化の方法は多種多様にあるので、後でまとめて取り上げることにします。

std::vector は、C++ の標準ライブラリ (standard library) の一部です。標準ライブラリとは、C++ の標準規格で仕様が定められている、機能の詰め合わせです。仕様が定められているため、どのコンパイラでも同じ方法で使えます。標準ライブラリに含まれている機能を使うためには、#include による準備が必要で、std::vector の場合は、#include <vector> という記述が必要です。std::string や std::ifstream など、これまでに登場した std:: が付くものたちは、標準ライブラリに含まれています。

std::vector は非常に多機能ですし、このページまでの知識では説明できないことも多いので、すべてを取り上げて説明することはしません。std::vector の全体像を知りたければ、リファレンスサイト(cpprefjpcpprefernce.com)などで確認してください。

初期化

std::vector の変数を宣言し、初期化する方法は複数あります。すべてを紹介できませんが、std::vector<int> を例にとって、いくつか取り上げます。

// 要素がない状態で初期化
std::vector<int> v1;

// 要素の値を1つ1つ指定して初期化(この例では要素は 3個になる)
std::vector<int> v2 {10, 100, 1000};

// 要素がない状態で初期化
std::vector<int> v3 {};

// 要素が 1000個ある状態で初期化。各要素はデフォルト値を持つ
std::vector<int> v4 (1000);

// 要素が 1000個ある状態で初期化。各要素は 7 という値を持つ
std::vector<int> v5 (1000, 7);

// ほかの std::vector の内容で初期化(つまり v5 のコピーができる)
std::vector<int> v6 = v5;

【C++17】std::vector<int> v {10, 20, 30}; のように、初期値によって型が判断できる場合(この場合、102030 から int型だと判る)、<int> の部分を省略して、std::vector v {10, 20, 30}; と書けるようになりました2

v1 と v3 の結果は同じです。v1 のように明示的に初期値を指示しなくても問題ありませんが、これまでのページでそうしてきたように、初期値の指示を明確に書きたければ v3 のようにすればいいでしょう。

v4 の場合、各要素の初期値は、要素の型に応じた値になります。int型なら 0、double型なら 0.0、bool型なら false、char型なら '\0'、std::string型なら "" です。

後で登場しますが、このような初期値のルールは値初期化と呼ばれます。

なお、std::vector を constexpr変数として宣言することはできません。

【C++20】C++20 から可能になりました。

初期化に使う括弧について

さきほどのコード例の中には、初期値の指定に {} を使うものと () を使うもの、そしてどちらも使わないものがあります。これらには違いがあるので、ここで少し補足しておきます。この話題は、std::vector 特有のものではなく、ほかの型にも共通する一般的なルールです。

初期化の構文としてこれまで、型名 変数名 {初期値};型名 変数名 = 初期値; といったものが登場しました。ここで、初期値を指定する部分の文法({初期値}= 初期値 のところ)を、初期化子 (initializer) と呼びます。初期化子がない 型名 変数名; というパターンもあります。

一般的に書けば、変数を宣言する構文はこうなります。

型名 変数名 初期化子;
型名 変数名;

初期化子に () を使う構文では、コンストラクタ (constructor) と呼ばれる機能を呼び出します。コンストラクタは、型の特性に合わせて用意されている初期化機能で、型によってその使い方(() の内側に何を書くか)は異なります。なお、この構文のときには = を使うことはできません。

std::vector<int> v1(1000);       // 1000個の要素。各要素は値初期化
std::vector<int> v2(1000, 7);    // 1000個の要素。各要素は 7 で初期化
std::string s1("xyz");           // "xyz" で初期化
std::string s2(1000, 'x');       // 1000文字。各文字は 'x'

コンストラクタの仕様を知りたければ、適宜リファレンスなどで調べる必要があります。ここまでの知識だけで理解するのは難しいですが、たとえば std::vector ならここ(cpprefjpcpprefernce.com)、std::string ならここ(cpprefjpcpprefernce.com)にコンストラクタの仕様があります。

【上級】() の内側を空にできるコンストラクタがあるとしても、変数宣言時に std::vector<int> v(); のように、() の内側を空にすることはできません。この記述が、C++ の文法ルールでは、戻り値型が std::vector<int> で、仮引数がない関数 v を宣言する文法のようにみえてしまうからです。そのため、初期化子を書かず、std::vector<int> v; とするか、{} を使って std::vector<int> v {}; と書きます。


次に {} を用いる構文です。この方法はリスト初期化 (list initialization) と呼ばれ、{} 全体のことを初期化子リスト (initializer list) と呼びます。リスト初期化は基本的に、要素1つ1つに与える初期化子を書き並べるための構文です。{} の内側は , で区切って初期化子を書き並べるか、空にします。

これまでのページでは、単独の int型の変数などに対しても {} による初期化を行っており、一様初期化と表現してきました。一様初期化という用語は「以前の C++ ではばらばらだった初期化の記法を {} を使ったものに統一できるようになった」ことを表現したものであり、{} を使う初期化構文の名称としてはリスト初期化と呼べばいいです。

{} の内側に初期化子がある場合、要素1つ1つをその値で初期化します。

std::vector<int> v {10, 100, 1000};
std::string s {'a', ' ', 'b'};

【上級】std::vector や std::string に対するリスト初期化が、各要素に対する初期化子を順番に記述したものと扱われるのは、仮引数の型が std::initializer_list<T> のコンストラクタがあるからです。リスト初期化では、このようなコンストラクタがほかのコンストラクタよりも優先されます。このようなコンストラクタがない場合は、ほかのコンストラクタから一致するものを探します。4

宣言する型が int や double などのように、1つの値しか持たないものである場合は、{} の内側には要素が1つだけでなければなりません。

int n {10};        // OK
int n2 {10, 20};   // エラー

{} の内側が空の場合には、値初期化 (value initialize) というルールに沿って初期化されます。値初期化のルールは複雑3なので完全な説明は避けますが、要は、もっとも自然と思われる値で初期化するということです。int型なら 0、double型なら 0.0、bool型なら false、char型なら '\0'、std::string型なら空文字列、std::vector なら要素なしの状態になります。

std::vector<int> v {};  // 要素無し
std::string s {};       // 空文字列
int n {};               // 0

{} を使う場合は = を挟めますが、挙動が異なる場面があります。現時点の知識では解説できないので、とりあえず当面は {}= の併用は避けておくことにします。

【上級】int n {10}; が可能である一方で、auto a {10}; は std::initializer_list<int> に推論されるので注意が必要です(ただし、C++17 で仕様変更)。auto a = {10}; でも同様です。

【上級】【C++17】auto a {10}; のように、{} の内側の要素が1つの場合で、= を使わない場合、その要素の型に推論されるようにルールが変更されました。したがって、変数a は int型です。= を使った場合は以前のままで、auto a = {10}; は std::initializer_list<int> に推論されます。

リスト初期化では、暗黙的に縮小変換されず、コンパイルエラーになるという特徴があります。

std::vector<int> v {10.0, 100, 1000};  // 不可。10.0 は double (int へは縮小変換が必要)
std::string s {5000, ' ', 'b'};        // 不可。5000 は int (char へは縮小変換が必要)
int n {100.0};                         // 不可。100.0 は double (int へは縮小変換が必要)


最後に、{}() も使わない場合に触れておきます。このパターンでは、= の右側に書いた式の結果によって初期化する方法と、初期化子を書かない方法があります。

int n = 100;    // 100 で初期化
int n2;         // 要注意。初期値が不明
std::vector v;  // OK. 要素なし
std::string s;  // OK. 空文字列

初期化子を書かない場合、int、double、char、bool といったシンプルな型では、初期値が不明なまま宣言されてしまうことに注意が必要です。原則として、これは避けたほうがいいです。std::vector や std::string のような型の場合は、空の {} を使った場合と同じ結果になります(したがって、std::vector なら要素なし、std::string なら空文字列)。

要素へのアクセス

std::vector の要素へアクセスするには [] を使用します。

v[0] = 10;
int n = v[0];

std::string の [] と同様、[] で範囲外アクセスをしてしまった場合は未定義動作です。この危険を避ける方法を後で取り上げます

すべての要素に1つ1つアクセスしたければ、for文が便利です。

#include <iostream>
#include <vector>

int main()
{
    constexpr auto element_count = 5;
    std::vector<int> v(element_count);

    // 各要素の値を書き換える
    for (int i = 0; i < element_count; ++i) {
        v[i] = i;
    }

    // 各要素の値を出力
    for (int i = 0; i < element_count; ++i) {
        std::cout << v[i] << "\n";
    }
}

実行結果:

0
1
2
3
4

前述したとおり、範囲外アクセスしないように注意が必要です。ここでは要素数を constexpr変数で宣言して、要素数が登場する箇所の記述を共通化しました。あとから要素数を変えたいと思ったら、constexpr変数の値だけを書き換えれば、すべての箇所が自然に対応され、問題なく動作します。これも1つの方法ですが、そもそも添字を使わないようにすればいいという考え方もあります。これについては後で取り上げます

at関数

[] で誤った位置を指定して未定義動作になることを避けるために、at関数を使う方法があります。この方法は std::vector だけでなく、std::string でも可能です。

v[i] のように書くところを、v.at(i) のように変形します。

#include <iostream>
#include <vector>

int main()
{
    constexpr auto element_count = 5;
    std::vector<int> v(element_count);

    for (int i = 0; i < element_count; ++i) {
        v.at(i) = i;
    }

    for (int i = 0; i < element_count; ++i) {
        std::cout << v.at(i) << "\n";
    }
}

実行結果:

0
1
2
3
4

at関数で範囲外アクセスを起こした場合、プログラムの実行が強制終了させられます(そうしないことも可能ですが、現時点では解説を省きます。以下の上級者向けコラムを参照してください)。何が起こるかまったく想定できない未定義動作と違って、at関数の動作は想定できるものなので、そういう意味で安全性が高いといえます。もちろん、プログラムの実行が終了してしまうのは不具合以外の何者でもありませんから、プログラムを修正しなければなりません。

【上級】範囲外アクセスをした場合、at関数は std::out_of_range という例外を送出します5。例外が捕捉されなかった場合、例外機構のルールとして、プログラムの強制終了に至ります。例外を捕捉して適切に処置を行えば、プログラムを続行させることも可能です。

#include <iostream>
#include <vector>

int main()
{
    constexpr auto element_count = 5;
    std::vector<int> v(element_count);

    // わざと範囲外アクセスしてみる
    for (int i = 0; i < element_count + 10; ++i) {
        std::cout << v.at(i) << "\n";
    }
}

実行結果:

0
0
0
0
0
(ここで強制終了)

at関数は、[] による方法よりも、実行速度の面ではわずかに劣ります。速度低下を嫌って [] を使うことも選択肢としてありえることですが、未定義動作による不具合は深刻なものになりえますから、まずは安全な方法を選ぶべきでしょう。今後は at関数のほうを使って書くことにします。

範囲for文

範囲外アクセスによる未定義動作を防ぐためには、そもそも添字を登場させないという発想もあります。使える場面は限定されますが、その方法として、範囲for文 (range-based for) があります。

範囲for文は、配列のような「複数の要素が集まったもの」を、先頭から末尾まで1つ1つアクセスするという目的に特化した、特殊な for文です。したがって、std::vector や std::string に対して使用できます。

範囲for文が使える対象は厳密なルールがあり6、「複数の要素が集まったもの」であれば必ず使えるというわけではありません。

範囲for文の文法は次のようになっています。

for (変数宣言 : 複数の要素が集まったもの)
    繰り返したい文

使用するキーワードは、通常の for文と同じく「for」です。( ) の中の構文の違いによって、通常の for文なのか、範囲for文なのかが判断されます。

範囲for文は1周ごとに、「複数の要素が集まったもの」から要素を1つ、「変数宣言」で宣言した変数へコピーします。1周目は先頭の要素がコピーされます。この変数は「繰り返したい文」の中で使うことができます。次の周回では、さきほどコピーした要素の次の要素がコピーされます。末尾の要素まで終わっていたら、ループを抜け出します。要素が1つもない場合は、範囲for文の内側に入ることはありません。

「変数宣言」のところに記述するのは、int e のような型名と変数名だけで、ここには初期化子は書きません。

「繰り返したい文」に書ける文は1つだけですが、{ } で囲んでブロック文にすることで、複数の文を書けます。また、break文や continue文を使うことも可能です。

【C++20】「変数宣言」の手前に、for文と同じ「初期化文」を置けるようになりました。これによりたとえば、添字として使う変数を併用できます(for (int i = 0; int e : v) { std::cout << i << ": " << e << "\n"; ++i; })。

実際のプログラムにすると、次のようになります。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {3, 4, 5, 6};

    for (int e : v) {
        std::cout << e << "\n";
    }
}

実行結果:

3
4
5
6

はじめて範囲for文のところが実行されるとき、v[0] の値が e にコピーされ、範囲for文の内側で e を使えます。2周目では、v[1] が e にコピーされ、範囲for文の内側で e を使えます。これが末尾まで繰り返されます。

間違えてはならないのは、e は v に含まれている要素の “コピー” であって、v に含まれている要素そのものではないということです。その意味は次のサンプルからわかります。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {3, 4, 5, 6};

    // 各要素を書き換える?
    for (int e : v) {
        e = 10;
    }

    // 各要素の値を出力
    for (int e : v) {
        std::cout << e << "\n";
    }
}

実行結果:

3
4
5
6

範囲for文の内側で e = 10; とすることで、v に含まれている各要素の値を 10 に書き換えたつもりですが、出力してみるとそうはなっていません。書き換えられたのは、コピーである e の方であって、コピー元になった v の各要素ではないからです。

範囲for文を使いつつ、要素の値を書き換えたい場合は、次のように変更します。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {3, 4, 5, 6};

    // 各要素を書き換える
    for (int& e : v) {
        e = 10;
    }

    // 各要素の値を出力
    for (int e : v) {
        std::cout << e << "\n";
    }
}

実行結果:

10
10
10
10

1つ目の範囲for文の変数宣言で、型名を int から int& に変更しただけです。型名に & が付くことで、参照型 (reference type) という型になります。

参照型の変数は、ほかの変数の別名として機能します。int& であれば、int型の変数に対する別名になれます。たとえば、int& r {n}; と宣言したあとに r = 10; とした場合、変数n に 10 を代入したことになります。つまり r が n の別名として機能したということです。

for (int& e : v) の場合、1周目の e は v[0] の別名です。したがって、e = 10;v[0] = 10; をしていることになります。同様に2周目のときには、e は v[1] の別名です。そのため、e = 10;v[1] = 10; をしていることになります。変更前のサンプルプログラムとちがって、v の要素のコピーを作っているわけではなくなり、v の要素を e という別名を通して変更していることになります。

【C言語プログラマー】実質的にはポインタと同じ考え方ですが、参照はポインタの機能を限定したような仕様になっており、ポインタにまつわる色々な危険な面を回避できます。たとえば、ヌルポインタのような状態がなく、ヌルアクセスが防がれます。

代入

std::vector の変数を、代入によってコピーすることは可能です。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1(5, 7);
    std::vector<int> v2 {};

    v2 = v1;

    for (int& e : v2) {
        std::cout << e << "\n";
    }
}

実行結果:

7
7
7
7
7

代入によって、= の右辺側の std::vector と同じ内容になります。このサンプルでいえば、v2 は要素なしの状態で初期化されていますが、v1 を代入したことによって、v1 と同様に、要素数が 5個、それぞれの値が 7 という状態になります。

なお、たとえば、std::vector<int> と std::vector<double> は別の型なので、代入できません。< > の中まで含めて、同じでなければなりません。

【C言語プログラマー】std::vector はシンプルな配列とは違い、memcpy関数や memmove関数を使ってコピーしてはいけません。

比較

std::vector 同士での比較も可能ですが、これもやはり < > の中まで含めて、同じ型でなければなりません。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1 {3, 4, 5};
    std::vector<int> v2 {3, 4, 6};

    std::cout << std::boolalpha
              << (v1 == v2) << "\n"
              << (v1 != v2) << "\n"
              << (v1 < v2) << "\n"
              << (v1 <= v2) << "\n"
              << (v1 > v2) << "\n"
              << (v1 >= v2) << "\n";
}

実行結果:

false
true
true
true
false
false

std::vector 同士での比較は、互いの要素を先頭から1つずつ突き合わせて比較します。このサンプルでは、3 と 3、4 と 4 の比較では同等ですが、5 と 6 の比較のところで 5 の方が小さいため、v1 の方が小さいということになります。どちらかの方が要素が少なくて、比較の最中に末尾に到達してしまった場合、要素が少ない方が小さいということになります。

【C言語プログラマー】std::vector はシンプルな配列とは違い、memcmp関数を使って比較してはいけません。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の変数宣言のうち、エラーになるものをすべて選んでください。

  1. std::vector<int> v;
  2. std::vector<int> v {10};
  3. std::vector<int> v {};
  4. std::vector<int> v();
  5. std::vector<int> v {2.0, 2.5, 3.0};
  6. std::vector<int> v = (10, 20, 30);
  7. std::vector<std::string> v(10, "Hello");

解答・解説

問題2 (基本★)

標準入力から整数を5つ入力させ、std::vector に格納するプログラムを作成してください。

解答・解説

問題3 (基本★)

問題2のプログラムを改造して、各要素が奇数か偶数かに応じて、“odd”、“even” のいずれかを標準出力へ出力するようにしてください。

たとえば、{7, -2, 5, 2, 3} という要素が入っているなら、oddevenoddevenodd を出力します。

解答・解説

問題4 (応用★★)

標準入力から文字列を5つ入力させ、std::vector に格納したあと、入力された順番とは逆の順番で出力するプログラムを作成してください。

解答・解説

問題5 (応用★★★)

標準入力から整数を5つ入力させ、std::vector<int> に格納したあと、各要素を以下のルールで変換して、std::vector<std::string> に格納するプログラムを作成してください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る