プログラミング

C++超入門⑦ ポインタと参照を理解しよう

普通の変数・ポインタ・参照を、値のコピーと元データの変更で比較。&と*、nullptr、値渡しと参照渡しの違いを短い実行例で整理します。

ポインタと参照は、似ているのに書き方が違うので混乱しやすいですよね。どちらも「元のデータを扱う」場面で使いますが、同じ仕組みではありません。

まず値・アドレス・別名を分けます。自分でメモリを確保するnewやdeleteは、この回では使いません。すでにある変数を安全に指すところから始めましょう。

#includeから始まるコード欄は、そのまま1本として実行できます。短い変数操作はmain内、関数の定義はmainより前へ書く抜粋です。例ごとに分けて試してください。

1. 値と、値の置き場所を分ける

#include <iostream>

int main() {
    int score = 80;

    std::cout << score << '\n';
    std::cout << &score << '\n';
    return 0;
}

1行目は80、2行目はアドレスの表示です。アドレスは環境や実行ごとに変わるため、決まった数字を期待しません。

実行中のデータを置くメモリをロッカーにたとえると、scoreの80が荷物、その位置を表すのがメモリアドレスです。&scoreでそのアドレスを得ます。

「値が80である」と「どこに80があるか」は別の情報です。この違いが、ポインタを読む出発点になります。

2. ポインタは、指す場所を保持する

int score = 80;
int* p = &score;

int*はint型のオブジェクトを指すポインタ型です。pにはscoreのアドレスを保存しています。ポインタも変数の一種で、普通のint変数との違いは保存する値の種類です。点数80ではなく、その点数を持つ変数の場所を覚えます。

式 今の意味
score 値80
&score scoreのアドレス
p 保存したアドレス
*p そのアドレスにあるintの値80

p自体にも置き場所はあります。&pはポインタ変数自身のアドレス、pに保存しているのは&scoreです。「住所を書いたメモの置き場所」と「メモに書いた住所」を区別するイメージですね。

int* pの*はポインタを宣言する記号、式としての*pは指す先を使う間接参照です。同じ記号が違う場面で出てくるので、前後を見て読み分けます。

コピーを書き換える場合と、*pを書き換える場合

表示は左からscore、copy、*pです。どの値が変わるか、1行ずつ追ってみましょう。

#include <iostream>

int main() {
    int score = 80;
    int copy = score;
    int* p = &score;

    std::cout << score << ' ' << copy << ' ' << *p << '\n';
    copy = 90;
    std::cout << score << ' ' << copy << ' ' << *p << '\n';
    *p = 95;
    std::cout << score << ' ' << copy << ' ' << *p << '\n';
    return 0;
}
80 80 80
80 90 80
95 90 95
操作した直後 score copy pが指す相手 *p
3つを用意 80 80 score 80
copy = 90 80 90 score 80
*p = 95 95 90 score 95

copyはその時点の80を受け取った、独立した変数です。copy = 90で元のscoreは変わりません。一方、*p = 95はscoreそのものへ書くため、scoreが95になります。pに入っているアドレスは変わっていません。

p = &another;なら指す相手の変更、*p = 95;なら今指す相手の値の変更です。

確認問題:途中で指す相手を変えると?

最後の*p = 95;の直前へp = &copy;を追加した場合、最後の表示を予想してください。

答えは80 95 95です。今度は*pがcopyを表すため、copyだけが95になり、scoreは80のままです。アドレス取得と間接参照の仕様でも、&と*の役割を確認できます。

3. nullptrは「指す相手がない」

int* p = nullptr;

if (p != nullptr) {
    std::cout << *p << '\n';
}

nullptrはヌルポインタを表すために使います。どのオブジェクトも指さない状態なので、*pで読み書きしてはいけません。

この例は条件がfalseになり、何も表示しません。nullptrの間接参照は未定義動作です。「必ず分かりやすいエラーが出る」とは限らない点が大切です。

nullptrでなければ安全、とは言えない

次のようなポインタも無効です。

  • 初期化していない
  • 元の変数の寿命が終わった
  • vectorの再確保後、以前の要素の場所を覚えている
  • 配列の範囲外を指し、その先を読もうとしている

nullptrの確認は、このすべてを検出する万能な検査ではありません。相手が今も生きていて、読み書きしてよい場所かを設計側で守ります。

4. 参照は、同じ変数の別名

int score = 80;
int& alias = score;

alias = 90;
std::cout << score << '\n';

結果は90です。aliasはscoreのコピーではなく、同じオブジェクトの参照です。値を使うときに*を付ける必要はありません。

int& aliasの&は参照の宣言です。&scoreのように式の前で使うアドレス取得とは役割が違います。

同じscoreから用意しても、int copy = score;は独立したコピー、int* p = &score;は場所を保持する変数、int& alias = score;はscoreの別名です。元のscoreを変える書き方は、ポインタなら*p = 90;、参照ならalias = 90;になります。

参照は、後から別の相手へ付け替えない

int first = 10;
int second = 20;
int& alias = first;

alias = second;
std::cout << first << ", " << second << '\n';

結果は20, 20です。aliasがsecondの別名へ変わったのではなく、firstへsecondの値20が代入されました。

これが、p = &secondで指す先を変えられるポインタとの大きな違いです。C++公開標準草案の参照の説明でも、参照の初期化とその後の扱いを確認できます。

5. 関数の値渡しと参照渡しを比べる

#include <iostream>

void addCopy(int number) {
    number++;
}

void addOriginal(int& number) {
    number++;
}

int main() {
    int count = 5;

    addCopy(count);
    std::cout << count << '\n';

    addOriginal(count);
    std::cout << count << '\n';
    return 0;
}
5
6

最初の関数はコピーを1増やすだけなので、countは5のままです。2つ目はcountの別名を受け取るため、元の値が6になります。

この例は、値がintの最大値ではないことを前提にしています。参照で渡したから整数の範囲制限がなくなるわけではありません。

読むだけならconst参照

大きな構造体や文字列を、コピーせず読むときはconstを付けた参照を使えます。

void showName(const std::string& name) {
    std::cout << name << '\n';
}

この関数には<string>と<iostream>が必要です。const std::string&は「元の文字列を参照するが、この参照を通して変更しない」という意味です。

6. いちばん大切なのは、相手の寿命

寿命は、そのオブジェクトが有効に存在する期間です。

関数内で作った普通のローカル変数は、その関数が終わると寿命が終わります。その変数のアドレスや参照を返して後で使うことはできません。

「アドレスの数字が残っているから、まだ使えるのでは?」と思うかもしれませんが、そうではありません。部屋番号を覚えていても、借りる契約が終われば使ってよい部屋ではなくなる、という区別です。

有効な相手を失ったポインタや参照をダングリングと呼びます。参照にもこの問題はあり、常に安全とは限りません。オブジェクトの寿命の仕様が基礎になります。

また、vectorに追加してメモリが再確保されると、以前の要素への参照やポインタは無効になる場合があります。追加後に必要な要素を取り直すなど、寿命と変更を意識します。

7. ポインタと参照、どちらを使う?

比較 ポインタ 今回の参照
相手なしの状態 nullptrを表せる 有効な相手に結び付ける
相手の付け替え できる できない
元の値の操作 *p 普通の変数と同じ形
寿命への注意 必要 必要

必ず相手がある値を扱うなら参照、相手がない可能性を表したいならポインタ、という判断ができます。

newとdeleteで手動管理を始める前に、複数要素ならvector、所有が必要ならスマートポインタなどを学ぶ方が、管理を整理しやすくなります。C++ Core Guidelinesでも、所有と非所有の区別を重視しています。

8. 練習:ポインタからHPを変える

int型のhpを100で作り、その場所をポインタへ保存してください。ポインタ経由で80へ変え、元のhpを表示します。

模範解答

#include <iostream>

int main() {
    int hp = 100;
    int* hpPointer = &hp;

    *hpPointer = 80;
    std::cout << hp << '\n';
    return 0;
}

解説

&hpで有効な変数のアドレスを得て、hpPointerへ保存します。*hpPointerがhpそのものを表すので、そこへ80を代入すると元のhpも80になります。

この間、hpは同じmain内で生き続けています。別の関数から寿命切れのアドレスを受け取る例ではありません。

参照で書く別解はint& hpAlias = hp; hpAlias = 80;です。結果は同じですが、参照には間接参照の*が不要です。

次の一歩

記号を暗記するより、「これは値? 場所? 別名?」と1つずつ分類しましょう。次は構造体を関数へ渡すときにも、この参照を使います。

前の記事 / 次の記事:構造体とenum

← 記事一覧へ戻る