C++のポインタによる配列走査:伝統的手法と現代の安全な代替策

C++において、配列の各要素に順番にアクセスする「走査(トラバーサル)」を行う際、arr[i] のようにインデックス(添字)を使う方法が一般的です。

しかし、C言語から受け継がれたもう一つの方法として、配列の先頭要素を指すポインタを取得し、そのポインタ自体をインクリメント(++)させて配列上を「歩かせる」手法があります。

目次

ポインタ演算による配列走査

配列の要素はメモリ上で連続して配置されているため、ある要素を指すポインタに 1 を加算(インクリメント)すると、そのポインタは「次の要素」を指すようになります。

  • p++ (インクリメント): ポインタ pp + 1 とします。これは、p が指すアドレスに sizeof(*p)p が指す型、例えば int なら4バイト)を加算することを意味し、結果として配列の次の要素を指します。
  • p-- (デクリメント): ポインタ pp - 1 とし、前の要素を指します。
  • *p (間接参照): ポインタ p が現在指している要素の値にアクセスします。

サンプルコード:ポインタで配列要素を変更する

このポインタ演算を使うと、インデックス変数 i を使わずにループ処理を記述できます。以下の例は、配列の全要素に特定の定数を加算する関数です。

#include <iostream>
#include <cstddef> // size_t のために必要

/**
 * @brief 配列の全要素に指定された値を加算する (ポインタ演算版)
 * @param arr (in/out) 対象の配列の先頭要素へのポインタ
 * @param size (in) 配列の要素数
 * @param value (in) 加算する値
 */
void add_value_to_array(int* p, size_t size, int value) {
    // size 回だけループ
    while (size > 0) {
        // 1. ポインタ p が指す先の値 (*p) を変更
        *p = *p + value;
        
        // 2. ポインタ p を次の要素へ進める
        p++;
        
        // 3. カウンタを減らす
        size--;
    }
}

int main() {
    int scores[] = {100, 150, 130, 170};
    
    // Cスタイル配列のサイズを安全に取得
    size_t scores_size = sizeof(scores) / sizeof(scores[0]);
    
    std::cout << "--- 変更前 ---\n";
    for (size_t i = 0; i < scores_size; ++i) {
        std::cout << "scores[" << i << "] = " << scores[i] << "\n";
    }

    // scores は配列名だが、関数に渡されると先頭要素へのポインタ (int*) に縮退する
    add_value_to_array(scores, scores_size, 10); // 全要素に10を加算

    std::cout << "\n--- 変更後 ---\n";
    for (size_t i = 0; i < scores_size; ++i) {
        std::cout << "scores[" << i << "] = " << scores[i] << "\n";
    }

    return 0;
}

実行結果:

--- 変更前 ---
scores[0] = 100
scores[1] = 150
scores[2] = 130
scores[3] = 170

--- 変更後 ---
scores[0] = 110
scores[1] = 160
scores[2] = 140
scores[3] = 180

ポインタを使った線形探索

*p++ という記述は、C言語スタイルのコードで頻繁に見られるイディオム(慣用句)です。これは「現在のポインタが指す値 *p を評価(使用)したに、ポインタ p をインクリメントする」という意味を持ちます。

以下の例は、このイディオムを使って配列から特定の値を探索する(線形探索)関数です。

#include <iostream>
#include <cstddef>

/**
 * @brief 配列から特定の値を線形探索し、インデックスを返す
 * @param arr (in) 探索対象の配列
 * @param size (in) 配列の要素数
 * @param key (in) 探す値
 * @return 見つかった要素のインデックス。見つからなければ -1。
 */
int find_index_by_pointer(const int* arr, size_t size, int key) {
    // 比較用のポインタを別途用意
    const int* p = arr;
    
    for (size_t i = 0; i < size; ++i) {
        // *p++ : まず *p を評価 (key と比較) し、その後 p をインクリメント
        if (*p++ == key) {
            return i; // 見つかった時点のインデックス i を返す
        }
    }
    return -1; // ループを抜けても見つからなかった
}

int main() {
    int data[] = {10, 25, 30, 45, 50};
    size_t data_size = sizeof(data) / sizeof(data[0]);
    int key = 30;

    int index = find_index_by_pointer(data, data_size, key);

    if (index != -1) {
        std::cout << "値 " << key << " は、インデックス " << index << " で見つかりました。\n";
    } else {
        std::cout << "値 " << key << " は見つかりませんでした。\n";
    }

    return 0;
}

ポインタ同士の減算と ptrdiff_t

同じ配列の内部を指す2つのポインタは、減算(-)することができます。

ptr_end - ptr_start という演算は、2つのポインタ間のバイト数の差ではなく、間に含まれる要素の個数を返します。

この減算結果の型は ptrdiff_t と呼ばれる符号付き整数型(<cstddef> ヘッダで定義)になります。

int data[] = {10, 20, 30, 40, 50};
int* p_start = &data[1]; // 20 を指す
int* p_end = &data[4];   // 50 を指す

// p_end と p_start の間にいくつの要素があるか
ptrdiff_t diff = p_end - p_start; // 結果は 3

現代C++におけるこの手法の是非

この記事で解説したポインタ演算による配列走査は、C++の低レベルな動作を理解する上で重要です。

しかし、この手法は非常に危険を伴います。 size 引数を渡し間違えたり、ループの制御を誤ったりすると、ポインタは配列の境界を簡単に超えてしまいます(バッファオーバーラン)。範囲外のメモリを読み書きすることは、プログラムのクラッシュや重大なセキュリティ脆弱性に直結します。

現代C++での推奨される代替策

現代のC++では、生のポインタ演算を避け、より安全で可読性の高い方法が強く推奨されます。

1. 範囲ベース for ループ (C++11以降)

配列やコンテナの全要素を走査する際の、最も安全で、最も推奨される方法です。ポインタ、インデックス、size 変数を一切管理する必要がありません。

#include <iostream>
#include <vector>

int main() {
    // Cスタイル配列でも、std::vectorでも機能する
    int scores[] = {100, 150, 130, 170};
    int value_to_add = 10;

    // 'score' は配列の各要素の「参照」(&) になる
    // 参照 (score) を変更すれば、元の配列 (scores) が変更される
    for (int& score : scores) {
        score += value_to_add;
    }

    // 読み取り専用の場合
    for (const int& score : scores) {
        std::cout << score << "\n";
    }
    
    return 0;
}

add_value_to_array 関数のロジックが、for ループの2行(for (...), score += ...;)だけで安全に実現できています。

2. イテレータ

std::vectorstd::array などのコンテナには、イテレータ(iterator)という機能が備わっています。イテレータは「コンテナの要素を指す、安全で汎用的なポインタ」のように振る舞い、it++*it といったポインタに似た操作が可能です。

まとめ(総括)

  • Cスタイル配列とポインタ演算(*p++ など)を用いた走査は、C++のC言語としての側面を理解する上で重要です。
  • この手法は、ポインタが配列の先頭を指し、++ で次の要素に進み、* で値にアクセスすることが基本です。
  • しかし、この手法はバッファオーバーランを引き起こす危険性が高く、現代のC++プログラミングでは非推奨とされています。
  • 可能な限り、範囲ベース for ループ (for (auto& item : container)) を使用してください。これが最も安全で、読みやすく、効率的な方法です。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次