【C++】STLのstd::vectorの使い方について詳しく解説

この記事では、C++の標準ライブラリに含まれるstd::vectorについて詳しく解説します。

std::vectorは、動的にサイズを変更できる便利な配列です。

この記事を読むことで、以下のことがわかります:

  • std::vectorの基本的な使い方
  • 要素の追加や削除の方法
  • 要素へのアクセス方法
  • メモリ管理とパフォーマンスの最適化
  • std::vectorを使った応用例

初心者の方でも理解しやすいように、サンプルコードとその実行結果を交えて説明します。

これを読めば、std::vectorを使ったプログラミングがスムーズにできるようになります。

目次から探す

std::vectorとは

std::vectorの概要

std::vectorは、C++標準ライブラリ(STL: Standard Template Library)に含まれる動的配列を提供するコンテナクラスです。

動的配列とは、必要に応じてサイズを変更できる配列のことです。

std::vectorは、要素の追加や削除が容易であり、メモリ管理を自動的に行ってくれるため、プログラマにとって非常に便利なツールです。

std::vectorの特徴

std::vectorには以下のような特徴があります:

  1. 動的サイズ変更: 必要に応じてサイズを自動的に変更できます。

要素を追加するときに容量が足りない場合、自動的にメモリを再割り当てします。

  1. 連続したメモリ配置: 内部的には連続したメモリ領域に要素が格納されるため、ランダムアクセスが高速です。
  2. 自動メモリ管理: メモリの確保と解放を自動的に行うため、メモリリークの心配が少なくなります。
  3. 豊富なメソッド: 要素の追加、削除、アクセス、検索、ソートなど、多くの便利なメソッドが提供されています。

std::vectorと配列の違い

std::vectorと配列(C++の標準配列やCスタイルの配列)にはいくつかの違いがあります:

  1. サイズの変更:
  • 配列: サイズは固定されており、宣言時に決定されます。

後からサイズを変更することはできません。

  • std::vector: サイズは動的に変更可能で、要素の追加や削除が容易です。
  1. メモリ管理:
  • 配列: メモリ管理は手動で行う必要があります。

特に動的配列の場合、メモリの確保と解放を自分で行う必要があります。

  • std::vector: メモリ管理は自動的に行われます。

メモリの確保や解放を意識する必要がありません。

  1. 機能の豊富さ:
  • 配列: 基本的な要素のアクセスしかできません。

追加の機能を使いたい場合は、自分で実装する必要があります。

  • std::vector: 要素の追加、削除、検索、ソートなど、多くの便利なメソッドが提供されています。

以下に、std::vectorと配列の基本的な使い方の違いを示すサンプルコードを示します。

#include <iostream>
#include <vector>
int main() {
    // 配列の宣言と初期化
    int arr[5] = {1, 2, 3, 4, 5};
    // std::vectorの宣言と初期化
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // 配列の要素にアクセス
    std::cout << "配列の要素: ";
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    // std::vectorの要素にアクセス
    std::cout << "std::vectorの要素: ";
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
    // std::vectorに要素を追加
    vec.push_back(6);
    std::cout << "std::vectorに要素を追加後: ";
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードを実行すると、以下のような出力が得られます:

配列の要素: 1 2 3 4 5 
std::vectorの要素: 1 2 3 4 5 
std::vectorに要素を追加後: 1 2 3 4 5 6

このように、std::vectorは動的にサイズを変更できるため、要素の追加が非常に簡単です。

一方、配列は固定サイズであり、要素の追加や削除が難しいです。

std::vectorを使うことで、より柔軟で効率的なプログラムを作成することができます。

std::vectorの基本操作

std::vectorの宣言と初期化

デフォルトコンストラクタ

std::vectorの宣言は非常に簡単です。

デフォルトコンストラクタを使用すると、空のベクターが作成されます。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec; // 空のベクターを宣言
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 0
    return 0;
}

サイズを指定したコンストラクタ

サイズを指定してベクターを初期化することもできます。

この場合、指定したサイズ分の要素がデフォルト値(int型なら0)で初期化されます。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec(5); // サイズ5のベクターを宣言
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 5
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 0 0 0 0 0
    }
    return 0;
}

初期値を指定したコンストラクタ

サイズと初期値を指定してベクターを初期化することも可能です。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec(5, 10); // サイズ5で全ての要素が10のベクターを宣言
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 5
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 10 10 10 10 10
    }
    return 0;
}

イテレータを使ったコンストラクタ

他のコンテナや配列からイテレータを使ってベクターを初期化することもできます。

#include <vector>
#include <iostream>
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec(std::begin(arr), std::end(arr)); // 配列からベクターを初期化
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 5
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 1 2 3 4 5
    }
    return 0;
}

要素の追加

push_back

ベクターの末尾に要素を追加するには、push_backメソッドを使用します。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 3
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 1 2 3
    }
    return 0;
}

emplace_back

emplace_backpush_backと似ていますが、オブジェクトをその場で構築するため、効率が良い場合があります。

#include <vector>
#include <iostream>
int main() {
    std::vector<std::pair<int, int>> vec;
    vec.emplace_back(1, 2);
    vec.emplace_back(3, 4);
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 2
    for (const auto& p : vec) {
        std::cout << "(" << p.first << ", " << p.second << ") "; // (1, 2) (3, 4)
    }
    return 0;
}

要素の削除

pop_back

ベクターの末尾の要素を削除するには、pop_backメソッドを使用します。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3};
    vec.pop_back();
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 2
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 1 2
    }
    return 0;
}

erase

特定の位置にある要素を削除するには、eraseメソッドを使用します。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.erase(vec.begin() + 2); // 3番目の要素を削除
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 4
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " "; // 1 2 4 5
    }
    return 0;
}

clear

ベクターの全ての要素を削除するには、clearメソッドを使用します。

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.clear();
    std::cout << "サイズ: " << vec.size() << std::endl; // サイズ: 0
    return 0;
}

std::vectorのアクセス方法

std::vectorは、配列と同様に要素へのアクセスが非常に簡単です。

ここでは、いくつかの代表的なアクセス方法について解説します。

インデックスによるアクセス

std::vectorの要素には、配列と同じようにインデックスを使ってアクセスできます。

インデックスは0から始まります。

#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 << "] = " << vec[i] << std::endl;
    }
    return 0;
}

このコードを実行すると、以下のような出力が得られます。

vec[0] = 1
vec[1] = 2
vec[2] = 3
vec[3] = 4
vec[4] = 5

atメソッドによるアクセス

インデックスによるアクセスは便利ですが、範囲外のインデックスを指定すると未定義動作になります。

これを防ぐために、std::vectorにはatメソッドが用意されています。

atメソッドは範囲外のインデックスを指定すると例外を投げます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    try {
        // atメソッドを使って要素にアクセス
        for (size_t i = 0; i < vec.size(); ++i) {
            std::cout << "vec.at(" << i << ") = " << vec.at(i) << std::endl;
        }
        // 範囲外のインデックスにアクセス
        std::cout << "vec.at(10) = " << vec.at(10) << std::endl;
    } catch (const std::out_of_range& e) {
        std::cerr << "例外発生: " << e.what() << std::endl;
    }
    return 0;
}

このコードを実行すると、以下のような出力が得られます。

vec.at(0) = 1
vec.at(1) = 2
vec.at(2) = 3
vec.at(3) = 4
vec.at(4) = 5
例外発生: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)

frontとbackメソッド

frontメソッドbackメソッドを使うと、std::vectorの最初の要素と最後の要素に簡単にアクセスできます。

#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;
    return 0;
}

このコードを実行すると、以下のような出力が得られます。

最初の要素: 1
最後の要素: 5

イテレータによるアクセス

イテレータを使うと、std::vectorの要素を順番にアクセスすることができます。

イテレータはポインタのように扱うことができ、beginメソッドendメソッドを使って範囲を指定します。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // イテレータを使って要素にアクセス
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << "要素: " << *it << std::endl;
    }
    return 0;
}

このコードを実行すると、以下のような出力が得られます。

要素: 1
要素: 2
要素: 3
要素: 4
要素: 5

イテレータを使うことで、std::vectorの要素を柔軟に操作することができます。

例えば、逆順にアクセスしたり、特定の条件に基づいて要素を処理したりすることが可能です。

std::vectorの容量管理

sizeとcapacity

std::vectorの容量管理において、sizecapacityは重要な概念です。

  • size:現在の要素数を返します。
  • capacity:現在確保されているメモリの容量を返します。

以下のコード例で、sizecapacityの違いを確認してみましょう。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec;
    std::cout << "Initial size: " << vec.size() << std::endl;
    std::cout << "Initial capacity: " << vec.capacity() << std::endl;
    vec.push_back(1);
    std::cout << "Size after one push_back: " << vec.size() << std::endl;
    std::cout << "Capacity after one push_back: " << vec.capacity() << std::endl;
    vec.push_back(2);
    vec.push_back(3);
    std::cout << "Size after three push_back: " << vec.size() << std::endl;
    std::cout << "Capacity after three push_back: " << vec.capacity() << std::endl;
    return 0;
}

このコードを実行すると、sizecapacityの違いが明確にわかります。

sizeは要素の数を示し、capacityはメモリの再割り当てが発生する前に格納できる要素の数を示します。

reserveとshrink_to_fit

std::vectorの容量を効率的に管理するために、reserveshrink_to_fitメソッドを使用します。

  • reserve:指定した容量を確保します。

これにより、頻繁なメモリ再割り当てを防ぐことができます。

  • shrink_to_fit:未使用のメモリを解放し、容量を現在の要素数に合わせます。

以下のコード例で、reserveshrink_to_fitの使い方を確認してみましょう。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec;
    vec.reserve(10);
    std::cout << "Capacity after reserve(10): " << vec.capacity() << std::endl;
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
    }
    std::cout << "Size after adding 10 elements: " << vec.size() << std::endl;
    std::cout << "Capacity after adding 10 elements: " << vec.capacity() << std::endl;
    vec.shrink_to_fit();
    std::cout << "Capacity after shrink_to_fit: " << vec.capacity() << std::endl;
    return 0;
}

このコードを実行すると、reserveによって容量が確保され、shrink_to_fitによって未使用のメモリが解放される様子が確認できます。

resizeメソッド

resizeメソッドは、std::vectorのサイズを変更するために使用します。

新しいサイズが現在のサイズより大きい場合、追加された要素はデフォルト値で初期化されます。

新しいサイズが現在のサイズより小さい場合、余分な要素は削除されます。

以下のコード例で、resizeメソッドの使い方を確認してみましょう。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::cout << "Initial size: " << vec.size() << std::endl;
    vec.resize(3);
    std::cout << "Size after resize(3): " << vec.size() << std::endl;
    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    vec.resize(7, 10);
    std::cout << "Size after resize(7, 10): " << vec.size() << std::endl;
    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードを実行すると、resizeメソッドによってサイズが変更される様子が確認できます。

最初のresize(3)では要素が削除され、次のresize(7, 10)では新しい要素が追加され、デフォルト値(ここでは10)で初期化されます。

以上が、std::vectorの容量管理に関する基本的な操作方法です。

これらのメソッドを適切に使用することで、効率的なメモリ管理が可能になります。

std::vectorのソートと検索

std::vectorは、標準ライブラリのアルゴリズムと組み合わせて使用することで、ソートや検索といった操作を簡単に行うことができます。

ここでは、std::vectorのソートと検索の方法について詳しく解説します。

ソート

std::sort

std::sortは、std::vectorの要素を昇順または降順に並べ替えるための関数です。

デフォルトでは昇順にソートされますが、カスタムの比較関数を指定することで降順にソートすることも可能です。

#include <iostream>
#include <vector>
#include <algorithm> // std::sort
int main() {
    std::vector<int> vec = {5, 3, 8, 1, 2};
    // 昇順にソート
    std::sort(vec.begin(), vec.end());
    // 結果を表示
    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    // 降順にソート
    std::sort(vec.begin(), vec.end(), std::greater<int>());
    // 結果を表示
    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードでは、まずstd::sortを使って昇順にソートし、その後std::greater<int>()を使って降順にソートしています。

std::stable_sort

std::stable_sortは、std::sortと同様にソートを行いますが、同じ値の要素の相対的な順序を保持します。

これは、安定なソートが必要な場合に便利です。

#include <iostream>
#include <vector>
#include <algorithm> // std::stable_sort
int main() {
    std::vector<int> vec = {5, 3, 8, 1, 2, 3};
    // 安定な昇順ソート
    std::stable_sort(vec.begin(), vec.end());
    // 結果を表示
    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードでは、std::stable_sortを使って昇順にソートしています。

同じ値(3)の要素の相対的な順序が保持されていることが確認できます。

検索

std::find

std::findは、指定した値をstd::vector内で検索し、最初に見つかった要素のイテレータを返します。

見つからなかった場合は、std::vectorend()イテレータを返します。

#include <iostream>
#include <vector>
#include <algorithm> // std::find
int main() {
    std::vector<int> vec = {5, 3, 8, 1, 2};
    // 値3を検索
    auto it = std::find(vec.begin(), vec.end(), 3);
    if (it != vec.end()) {
        std::cout << "Found: " << *it << std::endl;
    } else {
        std::cout << "Not Found" << std::endl;
    }
    return 0;
}

このコードでは、std::findを使って値3を検索し、見つかった場合はその値を表示しています。

std::binary_search

std::binary_searchは、std::vectorがソートされている場合に使用できる高速な検索アルゴリズムです。

指定した値が存在するかどうかをブール値で返します。

#include <iostream>
#include <vector>
#include <algorithm> // std::sort, std::binary_search
int main() {
    std::vector<int> vec = {5, 3, 8, 1, 2};
    // 昇順にソート
    std::sort(vec.begin(), vec.end());
    // 値3を二分探索
    bool found = std::binary_search(vec.begin(), vec.end(), 3);
    if (found) {
        std::cout << "Found" << std::endl;
    } else {
        std::cout << "Not Found" << std::endl;
    }
    return 0;
}

このコードでは、まずstd::sortを使ってstd::vectorを昇順にソートし、その後std::binary_searchを使って値3を検索しています。

std::lower_boundとstd::upper_bound

std::lower_boundstd::upper_boundは、ソートされたstd::vector内で特定の値の範囲を検索するために使用されます。

std::lower_boundは指定した値以上の最初の要素のイテレータを返し、std::upper_boundは指定した値より大きい最初の要素のイテレータを返します。

#include <iostream>
#include <vector>
#include <algorithm> // std::sort, std::lower_bound, std::upper_bound
int main() {
    std::vector<int> vec = {5, 3, 8, 1, 2, 3};
    // 昇順にソート
    std::sort(vec.begin(), vec.end());
    // 値3の範囲を検索
    auto lower = std::lower_bound(vec.begin(), vec.end(), 3);
    auto upper = std::upper_bound(vec.begin(), vec.end(), 3);
    std::cout << "Lower bound: " << (lower - vec.begin()) << std::endl;
    std::cout << "Upper bound: " << (upper - vec.begin()) << std::endl;
    return 0;
}

このコードでは、まずstd::sortを使ってstd::vectorを昇順にソートし、その後std::lower_boundstd::upper_boundを使って値3の範囲を検索しています。

結果として、値3が存在する範囲のインデックスを表示しています。

以上が、std::vectorのソートと検索に関する基本的な使い方です。

これらの機能を活用することで、効率的なデータ操作が可能になります。

std::vectorのコピーとムーブ

C++のstd::vectorは、コピーとムーブの両方に対応しています。

これにより、効率的なメモリ管理とパフォーマンスの向上が可能です。

ここでは、コピーコンストラクタと代入演算子、ムーブコンストラクタとムーブ代入演算子について詳しく解説します。

コピーコンストラクタと代入演算子

コピーコンストラクタと代入演算子は、std::vectorの内容を別のstd::vectorにコピーするために使用されます。

コピー操作は、元のstd::vectorの要素を新しいstd::vectorに複製します。

コピーコンストラクタ

コピーコンストラクタは、既存のstd::vectorを使って新しいstd::vectorを初期化するために使用されます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2(vec1); // コピーコンストラクタを使用
    // vec2の内容を表示
    for (int i : vec2) {
        std::cout << i << " ";
    }
    return 0;
}

このコードでは、vec1の内容がvec2にコピーされます。

出力は以下のようになります。

1 2 3 4 5

代入演算子

代入演算子は、既存のstd::vectorに別のstd::vectorの内容を代入するために使用されます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2;
    vec2 = vec1; // 代入演算子を使用
    // vec2の内容を表示
    for (int i : vec2) {
        std::cout << i << " ";
    }
    return 0;
}

このコードでは、vec1の内容がvec2に代入されます。

出力は以下のようになります。

1 2 3 4 5

ムーブコンストラクタとムーブ代入演算子

ムーブコンストラクタとムーブ代入演算子は、リソースの所有権を移動するために使用されます。

これにより、コピー操作に比べて効率的にメモリを管理できます。

ムーブコンストラクタ

ムーブコンストラクタは、既存のstd::vectorから新しいstd::vectorにリソースを移動するために使用されます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2(std::move(vec1)); // ムーブコンストラクタを使用
    // vec2の内容を表示
    for (int i : vec2) {
        std::cout << i << " ";
    }
    // vec1の内容を表示(空になっているはず)
    std::cout << "\nvec1 size: " << vec1.size() << std::endl;
    return 0;
}

このコードでは、vec1の内容がvec2に移動され、vec1は空になります。

出力は以下のようになります。

1 2 3 4 5
vec1 size: 0

ムーブ代入演算子

ムーブ代入演算子は、既存のstd::vectorに別のstd::vectorのリソースを移動するために使用されます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2;
    vec2 = std::move(vec1); // ムーブ代入演算子を使用
    // vec2の内容を表示
    for (int i : vec2) {
        std::cout << i << " ";
    }
    // vec1の内容を表示(空になっているはず)
    std::cout << "\nvec1 size: " << vec1.size() << std::endl;
    return 0;
}

このコードでは、vec1の内容がvec2に移動され、vec1は空になります。

出力は以下のようになります。

1 2 3 4 5
vec1 size: 0

ムーブ操作は、特に大きなデータ構造を扱う場合にパフォーマンスの向上に寄与します。

コピー操作とムーブ操作を適切に使い分けることで、効率的なプログラムを作成することができます。

std::vectorのメモリ管理

アロケータのカスタマイズ

std::vectorはデフォルトで標準のアロケータを使用してメモリを管理しますが、カスタムアロケータを使用することも可能です。

カスタムアロケータを使用することで、メモリ管理の挙動を細かく制御することができます。

以下はカスタムアロケータを使用した例です。

#include <iostream>
#include <vector>
#include <memory>
// カスタムアロケータの定義
template <typename T>
class CustomAllocator {
public:
    using value_type = T;
    CustomAllocator() = default;
    template <typename U>
    CustomAllocator(const CustomAllocator<U>&) {}
    T* allocate(std::size_t n) {
        std::cout << "Allocating " << n << " elements." << std::endl;
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    void deallocate(T* p, std::size_t n) {
        std::cout << "Deallocating " << n << " elements." << std::endl;
        ::operator delete(p);
    }
};
template <typename T, typename U>
bool operator==(const CustomAllocator<T>&, const CustomAllocator<U>&) { return true; }
template <typename T, typename U>
bool operator!=(const CustomAllocator<T>&, const CustomAllocator<U>&) { return false; }
int main() {
    // カスタムアロケータを使用したstd::vectorの宣言
    std::vector<int, CustomAllocator<int>> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    for (const auto& elem : vec) {
        std::cout << elem << std::endl;
    }
    return 0;
}

この例では、CustomAllocatorというカスタムアロケータを定義し、それを使用してstd::vectorを作成しています。

allocateメソッドdeallocateメソッドでメモリの割り当てと解放を行い、その際にメッセージを表示しています。

メモリリークの防止

メモリリークは、動的に割り当てたメモリが解放されずに残ってしまう問題です。

std::vectorを使用することで、メモリリークのリスクを大幅に減らすことができます。

std::vectorは自動的にメモリを管理し、不要になったメモリを適切に解放します。

以下は、std::vectorを使用してメモリリークを防ぐ例です。

#include <iostream>
#include <vector>
class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called" << std::endl;
    }
    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
};
int main() {
    {
        std::vector<MyClass> vec;
        vec.push_back(MyClass());
        vec.push_back(MyClass());
    } // このスコープを抜けると、vecのデストラクタが呼ばれ、メモリが解放される
    std::cout << "End of main" << std::endl;
    return 0;
}

この例では、MyClassのインスタンスをstd::vectorに追加しています。

std::vectorのスコープを抜けると、自動的にデストラクタが呼ばれ、メモリが解放されます。

これにより、メモリリークを防ぐことができます。

std::vectorを使用することで、手動でメモリを管理する必要がなくなり、メモリリークのリスクを大幅に減らすことができます。

これにより、より安全で効率的なプログラムを作成することができます。

std::vectorの応用例

2次元配列としてのstd::vector

std::vectorは1次元の動的配列として使うことが一般的ですが、2次元配列としても利用することができます。

2次元配列として使う場合、std::vectorの中にstd::vectorを格納する形になります。

以下に2次元配列としてのstd::vectorの例を示します。

#include <iostream>
#include <vector>
int main() {
    // 3x3の2次元配列を作成
    std::vector<std::vector<int>> matrix(3, std::vector<int>(3, 0));
    // 値を設定
    matrix[0][0] = 1;
    matrix[1][1] = 2;
    matrix[2][2] = 3;
    // 値を出力
    for (const auto& row : matrix) {
        for (const auto& elem : row) {
            std::cout << elem << " ";
        }
        std::cout << std::endl;
    }
    return 0;
}

このコードでは、3×3の2次元配列を作成し、各要素に値を設定しています。

出力結果は以下のようになります。

1 0 0 
0 2 0 
0 0 3

std::vectorを使った動的配列の実装

std::vectorは動的配列として非常に便利です。

動的にサイズを変更できるため、要素の追加や削除が容易に行えます。

以下に、std::vectorを使った動的配列の例を示します。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> dynamicArray;
    // 要素の追加
    dynamicArray.push_back(10);
    dynamicArray.push_back(20);
    dynamicArray.push_back(30);
    // 要素の出力
    for (const auto& elem : dynamicArray) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    // 要素の削除
    dynamicArray.pop_back();
    // 要素の出力
    for (const auto& elem : dynamicArray) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードでは、動的配列に要素を追加し、削除しています。

出力結果は以下のようになります。

10 20 30 
10 20

std::vectorと他のSTLコンテナの連携

std::vectorは他のSTLコンテナと連携して使うこともできます。

例えば、std::vectorstd::mapを組み合わせて使うことで、より複雑なデータ構造を実現できます。

以下に、std::vectorstd::mapを組み合わせた例を示します。

#include <iostream>
#include <vector>
#include <map>
int main() {
    // std::mapの中にstd::vectorを格納
    std::map<int, std::vector<int>> mapOfVectors;
    // std::vectorを作成
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};
    // std::mapにstd::vectorを追加
    mapOfVectors[1] = vec1;
    mapOfVectors[2] = vec2;
    // std::mapの内容を出力
    for (const auto& pair : mapOfVectors) {
        std::cout << "Key: " << pair.first << " Values: ";
        for (const auto& elem : pair.second) {
            std::cout << elem << " ";
        }
        std::cout << std::endl;
    }
    return 0;
}

このコードでは、std::mapの中にstd::vectorを格納し、各キーに対応するベクターの内容を出力しています。

出力結果は以下のようになります。

Key: 1 Values: 1 2 3 
Key: 2 Values: 4 5 6

このように、std::vectorは他のSTLコンテナと組み合わせることで、柔軟で強力なデータ構造を実現できます。

std::vectorのパフォーマンス最適化

std::vectorは非常に便利なコンテナですが、パフォーマンスを最大限に引き出すためにはいくつかのポイントに注意する必要があります。

ここでは、メモリ再割り当ての最小化、効率的な要素の追加と削除、そしてイテレータの無効化に関する注意点について解説します。

メモリ再割り当ての最小化

std::vectorは動的にサイズを変更できるため、要素を追加するたびにメモリの再割り当てが発生することがあります。

これにより、パフォーマンスが低下する可能性があります。

メモリ再割り当てを最小限に抑えるためには、以下の方法を使用します。

reserveメソッドの使用

reserveメソッドを使用すると、事前に必要なメモリを確保することができます。

これにより、頻繁なメモリ再割り当てを防ぐことができます。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec;
    vec.reserve(100); // 事前に100個分のメモリを確保
    for (int i = 0; i < 100; ++i) {
        vec.push_back(i);
    }
    std::cout << "Vector size: " << vec.size() << std::endl;
    std::cout << "Vector capacity: " << vec.capacity() << std::endl;
    return 0;
}

このコードでは、最初に100個分のメモリを確保しているため、100回のpush_back操作でもメモリ再割り当てが発生しません。

効率的な要素の追加と削除

std::vectorの要素の追加と削除は、特定の位置に対して行うと効率が悪くなることがあります。

特に、先頭や中間の位置に対する操作は、他の要素をシフトする必要があるため、時間がかかります。

末尾への追加と削除

末尾への追加と削除は、他の要素をシフトする必要がないため、非常に効率的です。

可能な限り、末尾への操作を行うようにしましょう。

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // 末尾に要素を追加
    vec.push_back(6);
    // 末尾の要素を削除
    vec.pop_back();
    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードでは、末尾に要素を追加し、削除する操作を行っています。

これにより、他の要素をシフトする必要がないため、効率的です。

イテレータの無効化に注意

std::vectorの要素の追加や削除を行うと、イテレータが無効化されることがあります。

特に、メモリ再割り当てが発生した場合や、要素の削除によって他の要素がシフトされた場合に注意が必要です。

イテレータの無効化を避ける方法

イテレータの無効化を避けるためには、以下の点に注意しましょう。

  1. メモリ再割り当てを最小限に抑える(reserveメソッドの使用)。
  2. 末尾への追加と削除を行う。
  3. イテレータを再取得する。
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.reserve(10); // メモリ再割り当てを防ぐために事前に確保
    // イテレータを取得
    auto it = vec.begin();
    // 要素を追加
    vec.push_back(6);
    // イテレータを再取得
    it = vec.begin();
    for (; it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

このコードでは、要素を追加した後にイテレータを再取得しています。

これにより、イテレータの無効化を防ぐことができます。

まとめ

この記事では、C++の標準ライブラリであるSTLの一部であるstd::vectorについて詳しく解説しました。

std::vectorは動的配列として非常に便利であり、C++プログラミングにおいて頻繁に使用されるコンテナです。

この記事を通じて、std::vectorの基本的な使い方から応用までを理解し、実際のプログラミングに役立てていただければ幸いです。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

目次から探す