HOWTO · C++
C++ で配列から要素を削除する
この記事では、C++ で配列から要素を削除する方法を示します
このページの内容
値の削除方法はコンテナーによって異なります。Cスタイル配列は宣言時の容量が変わらないため、値をずらして論理上の長さを短く管理するか、残す値を別のコンテナーにコピーします。std::vector はサイズを直接変更できます。以下の例では、説明に応じて最初に一致した値を1つ削除する方法と、一致する値をすべて削除する方法を示します。
要素を削除すると配列の論理サイズが変わる理由
int values[5] のような配列には、常に5個分の整数を格納する領域があります。要素を後続の値で上書きすれば有効な並びを短くできますが、配列の容量は変わりません。有効な長さは別の変数で管理し、並びを読み取ったり表示したりするときはその長さを使います。一方、std::vector<int> は、格納領域と現在の要素数の両方を管理します。
Cスタイル配列の要素をずらす
配列内の最初の一致要素をその場で削除するには、まずその要素を探し、後続の各値を1つ左へずらしてから、論理上の長さを1減らします。配列の容量は5のままで、短くなった並びに含まれるのは先頭からlength個の値だけです。次の例を実行します。
このセクションのコードと表示されるラベルや出力は英語のままです。
#include <iostream>
void remove_first(int values[], int& length, int target) {
int index = 0;
while (index < length && values[index] != target) {
++index;
}
if (index == length) {
return;
}
for (int i = index; i + 1 < length; ++i) {
values[i] = values[i + 1];
}
--length;
}
int main() {
int values[5] = {1, 2, 3, 4, 5};
int length = 5;
remove_first(values, length, 3);
for (int i = 0; i < length; ++i) {
if (i != 0) std::cout << ' ';
std::cout << values[i];
}
std::cout << '\n';
}
1 2 4 5
この関数が削除するのは最初の一致要素だけです。一致する値をすべて削除するには、読み取り位置と書き込み位置を使って配列を詰めます。一致しない値を次の書き込み位置へコピーし、最後に書き込んだ要素数を論理上の長さに設定します。この方法では各要素を1回ずつ調べます。一致するたびに要素をずらす方法は単純ですが、同じ値を何度も移動することがあります。
残りの要素を新しい配列にコピーする
別の格納先を使うと、元の配列は変更されずに残ります。std::vector はサイズを動的に変更でき、格納領域の所有権も管理するため、new[] と delete[] を手動で扱わずに済む便利な格納先です。この例では3以外の値をすべてコピーします。一致する値がなければ、格納先には元の配列のコピーが入ります。
このセクションのコードと表示されるラベルや出力は英語のままです。
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
const int source[] = {1, 2, 3, 3, 4, 5};
const int length = sizeof(source) / sizeof(source[0]);
std::vector<int> remaining;
for (int i = 0; i < length; ++i) {
if (source[i] != 3) {
remaining.push_back(source[i]);
}
}
for (std::size_t i = 0; i < remaining.size(); ++i) {
if (i != 0) std::cout << ' ';
std::cout << remaining[i];
}
std::cout << '\n';
}
1 2 4 5
格納先のサイズは、残す要素の数とちょうど同じになります。インターフェースの都合で動的確保した生配列が必要な場合は、まず残す要素数を数え、その数(0の場合もあります)だけ要素を確保します。所有者が使い終えたらdelete[]で解放してください。生配列の所有権を求められない場合は、標準コンテナーを使うのが適切です。
std::vectorから一致する値をerase-removeイディオムで削除する
サイズを変更できるコンテナーでは、std::remove とvectorのメンバー関数eraseを組み合わせます。アルゴリズムには<algorithm>、コンテナーには<vector>をインクルードします。std::removeは、一致しない値を先頭側へ移動し、新しい論理上の末尾を指すイテレーターを返しますが、vectorのサイズは変更しません。その後、eraseが末尾側の範囲を取り除き、サイズを更新します。この方法では、対象と一致する値をすべて削除できます。
このセクションのコードと表示されるラベルや出力は英語のままです。
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3, 3, 4, 5};
values.erase(std::remove(values.begin(), values.end(), 3), values.end());
for (std::size_t i = 0; i < values.size(); ++i) {
if (i != 0) std::cout << ' ';
std::cout << values[i];
}
std::cout << '\n';
}
1 2 4 5
固定長配列でstd::removeを使う
std::removeは、Cスタイル配列のイテレーター範囲にも使えます。値を並べ替えて新しい論理上の末尾を返しますが、配列の格納領域を短くすることはできません。結果を処理するときは、返されたイテレーターを残った範囲の末尾として使ってください。元の配列全体の範囲を、短くなった並びとして扱ってはいけません。
このセクションのコードと表示されるラベルや出力は英語のままです。
#include <algorithm>
#include <iostream>
#include <iterator>
int main() {
int values[] = {1, 2, 2, 3, 4};
int* logical_end = std::remove(std::begin(values), std::end(values), 2);
for (int* current = std::begin(values); current != logical_end; ++current) {
if (current != std::begin(values)) std::cout << ' ';
std::cout << *current;
}
std::cout << '\n';
}
1 3 4
logical_endから配列の実際の末尾までの要素は、有効なオブジェクトのままですが、その値はアルゴリズムによって保証されません。この末尾部分を表示したり、値を前提にしたりしないでください。物理的に短い並びが必要なら、logical_endまでの先頭部分を適切なサイズの格納先にコピーします。
方法を選び、端のケースにも対応する
固定長配列をその場で更新し、論理上の長さを別に管理できるなら、要素をずらす方法を使います。元の配列を維持したい場合や、結果を独立した格納領域に置きたい場合は、新しい格納先を使います。コレクションのサイズを自然に増減させたい場合はstd::vectorを選びます。一致する値をすべて削除するには、通常、そのerase-removeイディオムが最も明快です。
最初の一致要素だけを削除するのか、一致する要素をすべて削除するのかを明確にしてください。一致する値が見つからなければ、並びは変わりません。要素が1つの配列では、削除後の論理上の長さを0にできますが、宣言時の容量は1のままです。空のvectorではstd::removeがbegin()を返し、その空範囲を消去しても空のままです。長さとイテレーター範囲を一貫して扱えば、こうしたケースに特別なメモリ確保の工夫は必要ありません。