【C++】ポインタとは?|初心者がつまずく原因と図解で完全理解

サムネイル画像 C++

今回は、C++でかなり多くの初心者がつまずく「ポインタ」について解説していきます。

「ポインタって結局何を指しているの?」
*& がいつもごちゃごちゃになる…」
「ポインタを使うと何が便利なの?」

こんな疑問はありませんか?

C++を学び始めると、かなり高い確率で出てくるのがポインタです。ここで苦手意識を持ってしまう方も多いのですが、ポインタは「変数そのもの」ではなく「変数が置かれている場所」を扱う仕組みだと考えると、かなり理解しやすくなります。

この記事を読み終えると、あなたはポインタの意味・基本の使い方・初心者がつまずく原因・よくあるミスをしっかり理解できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

ポインタとは?

ポインタとは、変数の値ではなく、変数が保存されているメモリアドレスを持つ変数のことです。

たとえば、int value = 10; という変数があるとします。この時、value には 10 という値が入っていますが、メモリ上のどこかの場所にも置かれています。ポインタは、その「どこにあるか」を持つわけです。

つまりポインタは、「値そのもの」ではなく「値がある住所」を扱う変数というイメージです。

図解で見るとポインタはこういう仕組み

まずは、図で見るとかなりわかりやすいです。

pointer の中には 10 が入っているのではなく、value の住所である 0x1000 のような値が入っています。

そして、*pointer と書くと、その住所の先にある実際の値、つまり 10 を取り出せます。

なぜ初心者がポインタでつまずくのか?

ポインタでつまずきやすい理由は、見えている変数が2段階になっているからです。

  • 変数の値を見る考え方
  • 変数の住所を見る考え方

この2つが同時に出てくるので、最初はかなり混乱しやすいです。

特に、& は「アドレスを取る」、* は「そのアドレスの先の値を見る」という役割があります。この2つが頭の中で整理できると、ポインタはかなり理解しやすくなります。

ポインタの基本の書き方

まずは一番シンプルな例を見てみましょう。

▼main.cpp


#include <iostream>

int main() {
    int value = 10;
    int* ptr = &value;

    std::cout << "value: " << value << "\n";
    std::cout << "valueのアドレス: " << &value << "\n";
    std::cout << "ptrが持つ値: " << ptr << "\n";
    std::cout << "*ptr: " << *ptr << "\n";

    return 0;
}

実行結果

value: 10
valueのアドレス: 0x...
ptrが持つ値: 0x...
*ptr: 10

ここで大事なのは、ptr の中身は 10 ではなく、value のアドレスだということです。

*ptr と書いた時だけ、そのアドレス先の実際の値を取り出せます。

ポインタを使うと何ができる?

ポインタを使うと、関数の外にある変数を直接変更できるようになります。

これはゲーム制作でもかなり重要で、複数の処理から同じデータを触りたい時に考え方が役立ちます。メモリやポインタの基礎を理解しておくと、DirectXとUnityの違いの記事でも触れているような、C++らしい低レイヤーの理解にもつながります。

たとえば、関数にポインタを渡して値を書き換えると、元の変数も変更されます。

▼main.cpp


#include <iostream>

void AddScore(int* score) {
    *score += 100;
}

int main() {
    int score = 0;

    AddScore(&score);

    std::cout << "score: " << score << "\n";

    return 0;
}

実行結果

score: 100

このように、関数にアドレスを渡して、その先の値を変更しているわけです。

【重要】私が実際にポインタで困った体験談

私も自主制作でC++のコードを書いていた時、ポインタが原因でかなり混乱したことがあります。

最初の頃、関数の中で使うためにローカル変数のアドレスをどこかに保存して、あとで使い回そうとしたことがありました。書いている時は動いているように見えたのですが、しばらくすると値が急におかしくなったり、クラッシュしたりしました。

原因は、関数の中で作ったローカル変数は、その関数が終わると寿命が切れるのに、そのアドレスだけを後から使おうとしていたことでした。つまり、もう有効ではない場所を見に行っていたわけです。

これで、「ポインタは便利だけど、どの変数の寿命を指しているのかを考えないとかなり危ない」と実感しました。

こういう不具合は見た目だけでは原因がかなりわかりづらいので、ブレークポイントや変数ウォッチを使うのが有効です。デバッグの流れは、C++でのデバッグのやり方の記事もかなり相性がいいです。

ポインタ使用時のよくある失敗例と対処法

  • *& を混同する
    &value はアドレス、*ptr はその先の値です。まずはこの役割を分けて覚えるのが大切です。
  • nullptrチェックをしない
    ポインタが何も指していない状態で *ptr を使うとクラッシュの原因になります。nullptrとNULLの違いの記事もあわせて読むと整理しやすいです。
  • 配列とポインタを同じだと思ってしまう
    似て見える場面はありますが、完全に同じではありません。特に sizeof を使う時は結果が変わることがあるので、sizeofの記事も参考になります。

実践例:ポインタ経由でHPを減らす

ゲームっぽい例として、ポインタを使ってプレイヤーHPを直接減らす例を見てみましょう。

▼main.cpp


#include <iostream>

void DamagePlayer(int* hp, int damage) {
    if (hp == nullptr) return;

    *hp -= damage;

    if (*hp < 0) {
        *hp = 0;
    }
}

int main() {
    int playerHp = 100;

    DamagePlayer(&playerHp, 30);
    std::cout << "HP: " << playerHp << "\n";

    DamagePlayer(&playerHp, 80);
    std::cout << "HP: " << playerHp << "\n";

    return 0;
}

実行結果

HP: 70
HP: 0

このように、関数にHPのアドレスを渡すことで、呼び出し元の変数を直接書き換えられます。

クラス設計と組み合わせて考えると、ポインタがどこで使われるかも見えやすくなります。設計面が気になる方は、クラスの記事もあわせて読むと理解しやすいです。

注意点

  • ポインタは便利ですが、何を指しているか分からなくなると一気に読みづらくなります
  • 有効なアドレスか、寿命が切れていないかを意識することがかなり大切です
  • 初心者のうちは、まず「アドレスを持つ」「*で中身を見る」という2つに集中すると理解しやすいです

最初は難しく感じても、実際に &* を何度か手で追っていくと、かなり慣れてきます。

まとめ

  • ポインタは、変数の値ではなくアドレスを持つ変数
  • & はアドレスを取得し、* はその先の値を取り出す
  • 関数から元の変数を直接変更したい時などに便利
  • nullptrチェックと変数の寿命の意識がかなり重要
  • 最初は「住所」と「その先の値」で分けて考えると理解しやすい

ポインタは、C++を学ぶうえでかなり大きな山場のひとつです。

ですが、ここを超えるとメモリや関数呼び出しの仕組みが見えやすくなって、C++全体の理解もかなり深まります。

ここまで読んでくださり、ありがとうございました。

関連記事