Skip to content

第09回 ポインター入門 / メモリアドレスとオブジェクトの寿命

前回: 第08回 映像以外の構成要素 / BGM、効果音、エフェクト / 次回: 第10回 C++実践 独自ゲームの制作 1 / 企画とクラス設計

前回の振り返り

音とエフェクトを追加しました

  • BGMと効果音の音源ファイルを読み込み、再生しました
  • Effect クラスと配列で、簡単ながらエフェクトを追加しました
  • 様々な機能が実装された結果、扱う情報が増えてきました
  • 今回は、C++でオブジェクトを扱うときに避けて通れないポインターに触れ、 複雑な情報をより効率的に管理する方法を学びます

今回の目的

メモリアドレスとオブジェクトの寿命を認識する

  • ポインターが何を表すものか扱う
  • nullptr と有効なポインターを区別する
  • オブジェクトの寿命という考え方に触れる
  • newdelete による動的な管理があることを知る

今回の授業内容

ポインター入門 / メモリアドレスとオブジェクトの寿命

  • 変数の値と、変数が置かれている場所を分けて考える
  • &* の基本に触れる
  • ポインターを使う場面と、使わなくてよい場面を分ける
  • オブジェクトの静的な管理と動的な管理に触れる
  • 必要以上にポインターを増やさない判断も大切にする

ポインター入門

オブジェクトを値ではなく メモリ上の場所で扱う方法

時間をかけて じっくり覚えよう

突然ですが、コンピューターの基本構成について

画像
CPUとメモリ、入出力装置の関係を表す図 出典: Wikimedia Commons Von Neumann Architecture.png / CC0 1.0

  • コンピューターは、CPU、メモリ、 入出力装置などで構成されます
  • CPU は必要なデータをメモリから取り出し、 プログラムに従って命令を実行し、 計算結果をメモリに書き戻します
  • キー入力の受け付けや画面表示などの 入出力も、CPUがメモリを介して行います
  • この授業では メモリ上のデータ を CPUがどのように扱うかに触れ、 C++でオブジェクトを扱う方法を学びます

メモリとは

プログラムが作業中のデータを置く場所

  • メモリはコンピューターが作業中に使う記憶領域です
  • プログラムは必要なときにメモリからデータを読み書きして処理を行います
  • ゲーム実行中の変数、画像や音のハンドル、オブジェクトの情報などは すべてメモリ上に置かれます
  • これらのメモリ上のデータの管理はプログラムの責任です つまり、プログラマーはプログラムの動作のみならず メモリも管理する必要があります

メモリの上にデータを置く

変数はメモリ上に配置されます

cpp
int score = 100;
  • プログラムで使うデータはメモリ上に置かれます
  • メモリのどこに置かれるかはプログラムの動作や環境によって変わりますが 変数は必ずメモリ上のどこかに置かれます
  • 変数 score は人間が扱いやすいように名前を付けられていますが 実際には変数の中身である 100 という値がメモリ上に保存されています
  • プログラムはその場所を参照して値を読み書きします

変数の中身と置かれている場所

同じ値を持っていても、同じ変数とは限らない

cpp
int score = 100;
int hp = 100;
  • 変数 scorehp は、どちらも値としては 100 ですが 別々の変数ですので、メモリ上でも別々の場所に置かれています
  • 同じ値が入っていても同じ変数という意味ではありません 異なる変数であれば、それぞれがメモリ上の別の場所に置かれます
  • 変数には、それぞれ個別にメモリ上の場所が割り当てられています

アドレスとは

メモリ上の場所を表す情報

cpp
int score = 100;

printfDx(L"score = %d\n", score);
printfDx(L"&score = %p\n", &score);
  • この例で、変数 score は、100 という値を持つ変数です
  • ですが &score と書くと score の中身ではなく score が置かれている場所の情報が得られます
  • この場所のことを アドレス と呼びます アドレスは住所の意味で、メモリ上の場所を特定するための情報です

アドレスをポイントする

メモリ上のデータの住所を指定する

  • 敵の配列から、当たった敵だけを覚えておきたい
  • 見つかったアイテムだけを消したい
  • 選ばれているメニュー項目だけを変更したい
  • こうした場面では「見つけたもの」をあとから操作したいことがあります
  • オブジェクトをコピーすると、元の配列にあるものとは別のデータになります コピーした側を変更しても、元の敵やアイテムは変わりません
  • そのため、値をコピーするのではなく アドレスを指定して操作する方法が必要になります

ポインターとは

変数やオブジェクトの場所を持つ変数

cpp
// 100 という値を持つ変数 score
int score = 100;
// score の場所を持つポインター変数 scorePtr
int* scorePtr = &score;

// score の値を表示する
printfDx(L"score = %d\n", score);

// score の場所を表示する
// 二つは同じ場所が表示される
printfDx(L"&score = %p\n", &score);
printfDx(L"scorePtr = %p\n", scorePtr);
  • int score100 という値を持つ変数です
  • int* scorePtr は、score が置かれている 場所を持つ ポインター変数 です
  • &score と書くと score の場所を取得できます
  • score の値は 100 ですが、 score が置かれている場所は不定です

ポインターから変数を操作する

* を使ってポイント先の値にアクセスする

cpp
int hp = 3;
int* hpPtr = &hp;

// hpPtr を使って hp の値を変更する
*hpPtr = 2;
  • ポインター hpPtrhp の場所をポイントしています ここで *hpPtr と書くと、ポインターが指し示している場所の値に アクセスできます
  • この例では hp の値を hpPtr 経由で 2 に変更しています

アロー演算子 ->

ポインター先のメンバーにアクセスする

cpp
GameObject enemy = { 320, 80, 48, 48, true };
GameObject* target = &enemy;

target->x = 300;
target->isActive = false;
  • targetenemy のアドレスをポイントしています target->x と書くことで、ポインター経由で enemy.x にアクセスできます
  • -> はアロー演算子と呼びます target->x(*target).x と同じ意味です

nullptr

ポインターがどこも指していない状態を表す

cpp
GameObject* target = nullptr;

if (target != nullptr)
    target->isActive = false;
  • nullptr は、ポインターが何も指していない状態です
  • 何も指していないポインターの中身は 参照できません 強引に参照すると通常 プログラムが停止してしまいます
  • ポインターの中身が有効な場所を指しているかは状況によります 必要であれば、参照する前に nullptr でないかを確認します

サンプルコード

当たった敵をポインターで覚える

  • 命中した相手を target に入れます
  • 見つからなかった場合は nullptr のままです
  • 使う前に nullptr でないかを確認することで 対象となる敵を見つけた時だけ 処理を実行できます
cpp
GameObject* target = nullptr;

for (int i = 0; i < enemyMax; i++)
{
    // 当たった敵をtarget に入れてループ終了する
    if (IsHit(bullet, enemies[i]))
    {
        target = &enemies[i];
        break;
    }
}

// target が nullptr でないときだけ処理する
if (target != nullptr)
{
    // target が指している敵を非アクティブにする
    target->isActive = false;
}

変数の寿命

変数は消滅することがある

cpp
// GameObject のポインターを用意する
GameObject* enemyPtr = nullptr;

{
    // ブロックの中で作った enemy の
    // アドレスを enemyPtr に入れる
    GameObject enemy;
    enemyPtr = &enemy;
}

// ブロックを抜けると enemy は消える
// この時点で enemyPtr は謎の場所を指している
// エラーにはならないが、何が起きるかわからない
enemyPtr->x = 100;
  • ブロックや関数の中で作ったローカル変数は、 その範囲を抜けた時点で寿命が終わります
  • そのメモリの場所は別のオブジェクトが使う 可能性があります 古い場所を指すポインターを参照すると 何が起きるかわかりません
  • ポインターは便利ですが、参照先が まだ存在しているかを意識して扱わないと 非常に危険です

言語が動作を定義していない プログラムを実行すると 「未定義動作」となります

その先は何が起きるかわかりません 十分に注意してください

「鼻から悪魔が飛び出しても 仕様に反しない」という ジョークが有名

オブジェクトの寿命の種類

静的な管理と動的な管理

  • ここでは、通常の変数や配列として持つ管理を静的な管理と呼びます
  • 通常の変数や配列は、作られる場所や範囲によって寿命が決まります
  • ブロックや関数の中で作ったローカル変数は、 その範囲を抜けた時点で寿命が終わります
  • その他に、プログラムの実行中 動的にオブジェクトを作ることもできます 事前に定義しなくても必要なときに必要な数だけ作ることができますが 用が済んだら自己責任で削除しなければならず、慎重に扱う必要があります

動的に作られるオブジェクト

new と delete という仕組み

cpp
// 動的に GameObject を作る
GameObject* enemy = new GameObject;
enemy->x = 320;
enemy->y = 80;

// 中略

// 用が済んだら削除する
delete enemy;
// 使わないポインターは nullptr にしておく
enemy = nullptr;
  • new は動的にメモリを確保して プログラムの実行中にオブジェクトを作ります
  • delete は、new で作ったオブジェクトを 削除しメモリを解放します
  • delete 後のポインターは元のアドレスを 指したままとなっており、放置すると危険です
  • 解放済みの場所を指すポインターには nullptr を代入しておくと誤操作を防げます

最初から new を使わなくても大丈夫です

今回は顔見せだけ

  • newdelete は重要な機能ですが 使い方を間違えると非常に危険です
  • 小さなゲームであれば、配列や普通の変数だけでも十分に作りきることができます
  • 動的なオブジェクト生成には、所有関係や寿命の管理などの複雑な問題が伴います 時間をかけて理解し、徐々に使いこなせるようになっていきましょう

実習1: アドレスを表示する

値と場所の違いに触れる

  1. int x = 10; を用意する
  2. x の値を表示する
  3. &x のアドレスを表示する
  4. int* p = &x; を用意する
  5. *p を変更して、x の値が変わることを確認する

実習2: nullptr を使う

参照先がある場合だけ処理する

  1. GameObject* target = nullptr; を用意する
  2. 条件に合う敵が見つかったら target = &enemy; にする
  3. target != nullptr のときだけ処理する
  4. nullptr の確認を外すと何が危ないか説明できるようにする

実習3: 制作での管理方法を考える

ポインターが必要かを判断する

  1. 自分のゲームに登場する要素を書き出す
  2. 1つだけ使うもの、複数使うものを分ける
  3. 配列で管理できるものを選ぶ
  4. ポインターが必要になりそうな場面をメモする
  • 判断に迷う場合は、単純な変数や配列から始めます
  • newdelete は、今回の実習では使いません

よくあるつまずき

ポインターでエラーが出るときの確認ポイント

  • nullptr のまま使っていないか
  • ローカル変数のアドレスを関数の外で使っていないか
  • ポインターを使わなくても書ける場面で複雑にしていないか
  • 動的に管理する場合、delete し忘れや解放後の使用をしていないか

今回のまとめ

今日のポイント

  • ポインターは、値そのものではなく場所を持つ変数です
  • nullptr は、何も指していない状態を表します
  • オブジェクトには寿命があり、使えなくなった場所を参照すると危険です
  • オブジェクトには、静的な管理と動的な管理があります
  • 次回からは独自ゲーム制作に入り、企画とクラス設計を行います

おつかれさまでした!

次回予告 第10回 独自ゲーム制作 1

いよいよ自分のゲームを 仕上げていきます