CodeGym /コース /C++ SELF /配列はポインタになる

配列はポインタになる

C++ SELF
レベル 11, レッスン 2
使用可能

1. array-to-pointer conversion(decay)との出会い

C++ の配列には歴史的な一面があります。言語は「単にデータのアドレスを渡す」スタイルをサポートしており、そのために 式中で 配列は「N 個のコンテナ」というより「先頭要素のアドレス」として振る舞うことが多くなっています。この変換は array-to-pointer conversion または会話的に decay(「配列がポインタに“劣化”する」)と呼ばれます。

なぜそうなっているのか。主に C 言語との互換性と利便性のためです:関数や古いインタフェースは「データの先頭」を扱う方が簡単でした。しかし利便性には代償があります:アドレスはサイズを保持しないため、配列のサイズという重要な情報が失われます。

この講義の一貫したキーメッセージ:「オブジェクトとしての配列」と「式中の配列名」は同じではなく、それらを混同するとプログラムの論理が壊れます。

オブジェクトとしての配列:メモリ、要素、サイズ

整理しておきましょう:配列はオブジェクトです。全要素分のメモリを連続して占有し、コンパイル時に既知の固定長を持ちます。

例としてプロジェクトを進めます:スコア表(例えば 5 ラウンド分の得点)。

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};

    std::cout << scores[0] << ' ' << scores[N - 1] << '\n'; // 10 9
}

scores[i] にアクセスし N を使っている間は問題ありません:長さがあり、要素があり、境界は明確です 0..N-1。

配列を「配列として」保持している間は要素数を安全に数えられ、sizeof を使ったり、std::size(scores) を使ったりして安心できます。

ポインタの最小モデル:「ポインタ = アドレス」

「ポインタ」という言葉はメモリリークや new/delete、デバッガの悪夢を想起させますが、ここでは違います:今日のポインタは単にアドレスを保持する変数、つまり「あるオブジェクトが置かれているメモリ上の位置」です。

簡単な比喩:配列は「本が並んだ棚」、ポインタは「棚の先頭の本の位置を示すラベル」です。ラベルは便利ですが、棚に何冊あるかが書かれていなければ、6 冊目を取ろうとして棚が終わっていることに気づかないかもしれません。

必要な最小限の考え方:もし p が int へのポインタなら、式 p[0] は「そのアドレスにある最初の int」を意味します。なぜそうなっているかの詳細やポインタ算術はここでは扱わず、C++ ではそのようにデータにアクセスできると受け入れてください。

2. 実例で見る decay:配列が先頭要素のアドレスになる

重要なポイントです。配列 int scores[N] には次の性質があります:

  • scores は オブジェクトとして N 個の要素からなる配列である;
  • しかし scores は ほとんどの式で 自動的に int* に変換され、先頭要素のアドレスになる。

実際には次のようになります:

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};

    int* p = scores; // decay: p "points to" scores[0]

    std::cout << scores[0] << ' ' << p[0] << '\n'; // 10 10
}

ここでは「配列 → 先頭要素のアドレス」への変換が起きています。ポインタ p は scores[0] と同じ場所を指します。

もっと明示的に見せるには、先頭要素のアドレスを直接取ればよいです:

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};

    int* p1 = scores;      // 暗黙的(decay)
    int* p2 = &scores[0];  // 明示的

    std::cout << p1 << '\n'; // 例えば: 0x7ff...(実行環境で異なる)
    std::cout << p2 << '\n'; // 同じアドレス
}

std::cout は int* をアドレスとして出力します(通常は16進表記)。形式は実装依存ですが、本質は「メモリ上の座標」であることです。

5. decay の代償:サイズが失われる

なぜ T* からサイズを取り出せないのか

配列 scores[N] を持っているときは N を知っていますが、int* p しか残っていない場合は「先頭要素がどこにあるか」しか分かりません。そこから先が自分の要素でいくつあるか、他人のメモリかは分からないのです。

ポインタはコンテナでもスマートな構造でもなく、単なるアドレスです。したがって「p が指す配列の長さを教えて」と問うこと自体が概念的に不可能です:言語は int* の中に配列サイズを保持しません。

問題を体験するために「悪い考え」のデモをします:ポインタに対して sizeof を使って要素数を計算してみる例です。

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};

    int* p = scores;

    std::size_t wrongCount = sizeof(p) / sizeof(p[0]);
    std::cout << wrongCount << '\n'; // 64-bit ではしばしば 2(8 / 4)、だがこれは配列の長さではない
}

この結果は要素数とは無関係です。アドレスのサイズ(例えば 8 バイト)を int のサイズ(例えば 4 バイト)で割っただけの偶然の数値であり、プログラムの論理ではありません。

sizeof(配列) と sizeof(ポインタ) は別物

sizeof は 型/オブジェクトのバイト数 を測ります。配列とポインタでは次のように大きく異なります:

  • sizeof(scores) — 配列全体のサイズ(全要素分);
  • sizeof(p) — アドレスを保持する変数のサイズ(通常 4 または 8 バイト)、データそのもののサイズではない。
#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};
    int* p = scores;

    std::cout << "sizeof(scores) = " << sizeof(scores) << '\n';
    std::cout << "sizeof(p)      = " << sizeof(p) << '\n';
}

典型的な 64 ビット環境では sizeof(scores) は 20(5 * 4)、sizeof(p) は 8 になります。具体的な値にこだわらないでください:要点は配列は「重く」、ポインタは「軽い」ということです。

配列の長さを正しく取得する方法

初心者がよく覚える式があります:

sizeof(a) / sizeof(a[0])

これは確かに機能します…ただし a が配列である場合に限ります。a がポインタに「劣化」すると、アドレスのサイズを要素のサイズで割っただけの意味のない値になります。

#include <cstddef>
#include <iostream>

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    int* p = a;

    std::size_t ok = sizeof(a) / sizeof(a[0]);
    std::size_t bad = sizeof(p) / sizeof(p[0]);

    std::cout << ok << ' ' << bad << '\n'; // 5 2 (bad — 無意味な値)
}

可読性が高く安全な方法は std::size(a) を使うことです。これは引数が配列であることを認識し、正しい要素数を返します。

#include <cstddef>
#include <iostream>
#include <iterator> // std::size

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    int* p = a;

    std::cout << std::size(a) << '\n'; // 5

    // std::cout << std::size(p) << '\n'; // これは不可: p は配列ではない
    (void)p;
}

6. 実践:ポインタ経由の出力と走査

なぜ std::cout << a; は要素を出力しないのか

よくあるコミカルな誤解:std::cout << a; を書いて 1 2 3 4 5 が出ると思ったら、代わりに 0x7ff... のような出力が出る、というものです。

原因は同じで、式中の a が int* に変換され、ポインタに対する operator<< はアドレスを出力するからです。

#include <iostream>

int main() {
    int a[3] = {10, 20, 30};

    std::cout << a << '\n';    // 0x7ff...(アドレス、"10 20 30" ではない)
    std::cout << a[0] << '\n'; // 10
}

要素を出力したければループで明示的に出力してください(ここではアルゴリズムや range-for は扱わず基本だけ示します):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 3;
    int a[N] = {10, 20, 30};

    for (std::size_t i = 0; i < N; ++i) {
        std::cout << a[i] << ' ';
    }
    std::cout << '\n'; // 10 20 30
}

もし T* しかないなら、長さを別で持つ

実用上のルール:一度ポインタ側に移ったら、長さ情報を必ず別に管理してください。

この講義では単純に、N と p を保持し、ポインタ p[i] を走査する際はループの境界に N を使う、という方針を取ります(推測はしない)。

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int scores[N] = {10, 12, 8, 15, 9};

    int* p = scores;

    int sum = 0;
    for (std::size_t i = 0; i < N; ++i) {
        sum += p[i];
    }

    std::cout << "sum=" << sum << '\n'; // sum=54
}

より厳密にしたければ(N を誤って変更してしまうリスクを減らすため)配列から直接 std::size(scores) で長さを取得し、それを保存してからポインタへ移る、という手順を取るとよいです:

#include <cstddef>
#include <iostream>
#include <iterator>

int main() {
    int scores[5] = {10, 12, 8, 15, 9};

    const std::size_t n = std::size(scores);
    int* p = scores;

    for (std::size_t i = 0; i < n; ++i) {
        std::cout << p[i] << ' ';
    }
    std::cout << '\n'; // 10 12 8 15 9
}

図解:decay のときに何が起きるか

これを「コンパイラの魔法」として扱わないように、頭の中にシンプルな図を持っておくと良いです:オブジェクトとしての配列があり、式として現れるときはしばしばアドレスに変わります。

flowchart TD
    A["int scores[N] (配列オブジェクト)
は N 要素を持つ"] --> B["scores (式中)"] B --> C["int* p = scores;
p は scores[0] のアドレスを保持"] C --> D["p[i] (アドレス経由のデータアクセス)
ただし i は別途制限する必要がある"]

この図の意味:配列とポインタは関連しているが、同一ではないということです。

7. 典型的な間違い

誤りその 1:T* が「配列の長さを知っている」と考えること。
int* p = a; の後で、p がアドレスだけでなくサイズも保持していると直感的に期待する人は多いですが、サイズはポインタの一部ではありません。p をループで走査する場合は境界情報を別途持つ(定数 N、decay 前の std::size(a) など)必要があります。そうしないと容易に境界を越えて未定義動作になります。

誤りその 2:sizeof(p) / sizeof(p[0]) で要素数を計算すること。
一見論理的に見えますが、sizeof(p) はデータのサイズではなくアドレスのサイズです。得られる数は時にもっともらしく見える(例えば 2)ため誤りが長く残りがちです。正しい式 sizeof(a) / sizeof(a[0]) は a が本当の配列オブジェクトである場合にのみ有効です。

誤りその 3:std::cout << a; を書いて要素が出ると期待すること。
ストリーム出力は式中の配列がポインタに変換されるためアドレスを出力します。要素を出したければループで a[i] を明示的に出力してください。出力が 0x... の場合、それは cout の故障ではなく、あなたがアドレスを出力するように指示した結果です。

誤りその 4:「最後のインデックス」と「長さ」を混同すること。
ポインタ側に移ると境界を取り違えやすくなります:長さは N、最後のインデックスは N-1 です。ループ条件で i <= N とすると、5 要素の配列で p[N](6 番目)を読もうとし、たとえクラッシュしなくても他人のメモリを読み取ってしまう危険があります。

誤りその 5:ポインタ算術で自分を納得させた直後に実験を始めること。
確かにポインタには算術があり、それを使ってアクセスできますが、それは別のトピックで注意深く扱う必要があります。この講義の枠では「ポインタ = アドレス」「p[i] は i 番目の要素にアクセスする」「境界は別途与える」を守ることで堅実な基礎を築けます。そうすれば未定義動作の森に迷い込む確率が減ります。

1
タスク
C++ SELF, レベル 11, レッスン 2
ロック未解除
失われたサイズ
失われたサイズ
1
タスク
C++ SELF, レベル 11, レッスン 2
ロック未解除
計測値の合計
計測値の合計
1
タスク
C++ SELF, レベル 11, レッスン 2
ロック未解除
ウィンドウの参照
ウィンドウの参照
1
タスク
C++ SELF, レベル 11, レッスン 2
ロック未解除
ランキングのリーダー
ランキングのリーダー
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION