この記事では、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
には以下のような特徴があります:
- 動的サイズ変更: 必要に応じてサイズを自動的に変更できます。
要素を追加するときに容量が足りない場合、自動的にメモリを再割り当てします。
- 連続したメモリ配置: 内部的には連続したメモリ領域に要素が格納されるため、ランダムアクセスが高速です。
- 自動メモリ管理: メモリの確保と解放を自動的に行うため、メモリリークの心配が少なくなります。
- 豊富なメソッド: 要素の追加、削除、アクセス、検索、ソートなど、多くの便利なメソッドが提供されています。
std::vectorと配列の違い
std::vector
と配列(C++の標準配列やCスタイルの配列)にはいくつかの違いがあります:
- サイズの変更:
- 配列: サイズは固定されており、宣言時に決定されます。
後からサイズを変更することはできません。
std::vector
: サイズは動的に変更可能で、要素の追加や削除が容易です。
- メモリ管理:
- 配列: メモリ管理は手動で行う必要があります。
特に動的配列の場合、メモリの確保と解放を自分で行う必要があります。
std::vector
: メモリ管理は自動的に行われます。
メモリの確保や解放を意識する必要がありません。
- 機能の豊富さ:
- 配列: 基本的な要素のアクセスしかできません。
追加の機能を使いたい場合は、自分で実装する必要があります。
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_back
はpush_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
の容量管理において、size
とcapacity
は重要な概念です。
size
:現在の要素数を返します。capacity
:現在確保されているメモリの容量を返します。
以下のコード例で、size
とcapacity
の違いを確認してみましょう。
#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;
}
このコードを実行すると、size
とcapacity
の違いが明確にわかります。
size
は要素の数を示し、capacity
はメモリの再割り当てが発生する前に格納できる要素の数を示します。
reserveとshrink_to_fit
std::vector
の容量を効率的に管理するために、reserve
とshrink_to_fitメソッド
を使用します。
reserve
:指定した容量を確保します。
これにより、頻繁なメモリ再割り当てを防ぐことができます。
shrink_to_fit
:未使用のメモリを解放し、容量を現在の要素数に合わせます。
以下のコード例で、reserve
とshrink_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::vector
のend()
イテレータを返します。
#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_bound
とstd::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_bound
とstd::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::vector
とstd::map
を組み合わせて使うことで、より複雑なデータ構造を実現できます。
以下に、std::vector
とstd::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
の要素の追加や削除を行うと、イテレータが無効化されることがあります。
特に、メモリ再割り当てが発生した場合や、要素の削除によって他の要素がシフトされた場合に注意が必要です。
イテレータの無効化を避ける方法
イテレータの無効化を避けるためには、以下の点に注意しましょう。
- メモリ再割り当てを最小限に抑える(
reserveメソッド
の使用)。 - 末尾への追加と削除を行う。
- イテレータを再取得する。
#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
の基本的な使い方から応用までを理解し、実際のプログラミングに役立てていただければ幸いです。