インタラクティブ・ゲーム制作 <プログラミングコース> 第4回 スコープ・ポインタ・参照 今日の内容 • スコープの話 • ポインタと参照 – 変数の寿命について – ポインタとは何ぞや – コンストラクタと デストラクタの確認 – 参照とは何ぞや – クラスのスコープ – うまいお付き合いの 仕方 変数やオブジェクトの寿命の話 スコープ スコープとは • 変数やオブジェクトの 有効範囲のこと • {}で囲われた範囲を 「ブロック」と呼ぶ – 右の例だとwhileループの ブロック内に、条件分岐で 更に3つブロックがある – もちろんwhileループの 外側にも関数のブロックが ある • スコープはブロック で決まる while(window.update() == true) { if(キーが押されてたら?) { うごけー } else { 押されてなかったら これやっとけー(省略可) } if(違うキーが押されてたら?) { 違う感じにうごけー } } 変数が生まれる時・死ぬ時 • 処理がスコープに 入って宣言された時、 変数が生成される • 処理がスコープから 抜ける時、 変数は破壊される – 関数呼び出しで一時的に ジャンプする時は大丈夫 • 今処理している スコープから見えない 変数は使えない – コンパイルエラーになる void func(void) { // ここで変数hogeが作られる int hoge = 0; if(適当な条件式) { // ここで変数hogehogeが作られる int hogehoge = 0; } // ここで変数hogehogeが破壊される return; // ここで変数hogeが破壊される } じゃあ こう書くしかないじゃない!! int main(int argc, char *argv[]) { /* 凄まじい分量の変数宣言 */ /* 殺人的な分量の初期化処理 */ // メインループ while(true) { /* 目を覆いたくなるようなゲーム処理 */ } // ここに至るまでに数万行 return 0; } なんでああなるのか? • 関数やクラスを覚えても、 うまく使えずこう書く人は多い – スコープの問題を解決できないから • 今日取り扱う、ポインタや参照を 理解すれば、こんなことはしなくて済む – 絶対にやめよう スコープイン・アウトに 際する重要イベント • 変数の場合は、 単純に箱が確保され、 消滅するだけ • クラスオブジェクト は、イン・アウトで 次の関数が呼ばれる – スコープインで コンストラクタ – スコープアウトで デストラクタ #ifndef __SCOPR_CHECKER_H__ #define __SCOPR_CHECKER_H__ #include <iostream> // スコープの出入り時にメッセージを出すクラス class ScopeChecker { public: ScopeChecker(void) { std::cout << "こうして俺はこの世界に生 まれた。" << std::endl; }; ~ScopeChecker() { std::cout << "そして俺は世界から抹殺さ れた。" << std::endl; }; }; #endif 初期化と後片付け以外にも 色々使い道がある • 自動で呼んでくれる という構造を利用し て、闇の魔術を行使 するC++erも多い – ScopeCheckerは その一例 • コンストラクタに 引数を取ることで、 実体生成時に名前を 付けるようにした→ // さっきのクラスのコンストラクタを改造 // ※一部省略 class ScopeChecker { private: std::string name; public: ScopeChecker(std::string argName) : name(argName) { std::cout << “俺の名は” << name << “……たった今生まれたところさ。” << std::endl; }; // デストラクタも同じように改造しよう }; トピック:初期化子リスト • コンストラクタで、 メンバ変数に値をセット する最速で確実なやり方 – 引数リストの後ろに 「: メンバ名(値)」 – 複数ある時はカンマ区切り で並べる • : hoge(1), fuge(2) – [上級] 継承している場合は、 親クラスのコンストラクタ に引数を渡す時にも使う ScopeChecker(std::string argName) : name(argName) { } // 次のように書いても同じ ScopeChecker(std::string argName) { name = argName; } // できるだけ初期化子リストを使おう クラス内のスコープって どうなってるの? • お品書きと中身を 分けずに、まとめて 書いた状態で考える と分かりやすい • クラスという枠で 括られているので、 メンバ関数からは メンバ変数が見える class HogeHoge { private: // メンバ変数はクラス内スコープ std::string name; public: HogeHoge() { // スコープ内だからメンバ変数が見える name = “ほげ~”; }; void func() { // メンバだから、見えま~す std::cout << name << std::endl; }; }; でもまとめて書くとしんどいので • お品書きと本体に 分離して書く • 本体を書く時は 「クラス名::関数名」 と書くことで、 「ここだけ一時的に クラスの中だよ!」 ということを示す class HogeHoge { // 省略 // 関数の名前・返値の型・引数だけ書く void func(); // 省略 }; // CPP側では #include “HogeHoge.h” // ここだけHogeHogeの中ってことにして! void HogeHoge::func() { // 飛び地だけど、メンバだから見えま~す std::cout << name << std::endl; } クラスが見えてるか? オブジェクトが見えているか? • クラスを利用するに は、クラスの宣言が スコープ内に見えて ないとダメ – なのでcppの冒頭で includeする • 作ったオブジェクト を操作出来るのは、 オブジェクトを作っ たスコープ内だけ // HogeHogeクラスを使いたい #include “HogeHoge.h” void func() { // これはOK HogeHoge hoge; hoge.func(); } void funcOther() { // hogeは飽くまでfunc()で作ったから // これはNG hoge.func(); } ここまでのまとめ • ブロック{}によってスコープが決まる • スコープに出入りする時、 クラスオブジェクトならコンストラクタと デストラクタが走る • クラスのメンバは、クラススコープという 括りで括られていると考える 怖くない、ほら、怖くない ポインタと参照 画像を友達に送りたい • どんなやり方が考えられるだろうか? 1. 「画像を直接転送する」 2. 「画像をアップロードしてURLを教える」 – 他にもやり方があるが、それぞれメリットと デメリットがある – ポインタは後者の考え方に基づく 現物渡しとURL渡しの違い • 現物の場合 – 転送に時間がかかる – お互いそれぞれ手元に ファイルが残る • コピーしたことになる – 送り先の相手が その画像をいじっても、 送り主には影響なし • その逆もまた同じ • URLの場合 – 転送は一瞬 – 送り先の相手は手元に コピーしなくても、 URLから画像を見れる • コピーすることも可 – 送り主が画像を 消したり、内容を 変更したら送り先も 影響を受ける • 重要 全ての変数にはアドレスがある • 変数名に&を付けると、 アドレスになる • アドレスは 「ポインタ変数」に しまうことができる – ポインタは 「型名 *ポインタ名;」 で宣言する • ポインタ変数に*を 付けると、 その中身を取り出せる // main()の中だとして // 適当に作った変数のアドレスを見るコード int hoge = 0; int *pHoge = NULL; pHoge = &hoge; std::cout << pHoge << std::endl; std::cout << *pHoge << std::endl; 関数の引数で利用してみる 関数の定義 呼び出し側 // 渡されてきた値を2倍しようとする関数 void cantChangeValue(int arg_num) { arg_num *= 2; return; } // main()の中だとして // 渡されてきた値を2倍する関数 void changeValue(int *arg_pNum) { *arg_pNum *= 2; return; } int num = 10; cout << “before:” << num << endl; cantChangeValue(num); //changeValue(&num); cout << “after:” << num << endl; // 呼び出す側を切り替えて試してみよう 何が違うのか? • コピー先をいじっても意味が無い // 渡されてきた値を2倍しようとする関数 void cantChangeValue(int arg_num) { arg_num *= 2; return; } 10 // main()の中だとして int num = 10; cout << “before:” << num << endl; cantChangeValue(num); cout << “after:” << num << endl; 10 arg_num 20 num 何が違うのか? • ポインタを通じて元の変数がいじれる // 渡されてきた値を2倍する関数 void changeValue(int *arg_pNum) { *arg_pNum *= 2; return; } 10 // main()の中だとして int num = 10; cout << “before:” << num << endl; changeValue(&num); cout << “after:” << num << endl; 10 arg_pNum 20 num 20 参照はポインタの簡易版 • 関数の引数リスト側で &を付けて宣言 • 関数の呼び出し側は、 実体を渡せばよい • 後から指し示す対象を 変えることはできない • 実体と同様に扱える – メリットでもあり、 デメリットでもある // 渡されてきた値を2倍する関数(参照版) void changeRefValue(int &arg_pNum) { arg_pNum *= 2; return; } // main()の中だとして int num = 10; cout << “before:” << num << endl; changeRefValue(num); cout << “after:” << num << endl; 実体・ポインタ・参照の違い 宣言の仕方 Hoge hoge; Hoge *a; Hoge &a = other; 種類 実体 ポインタ 参照 代入できるもの 実体・定数 アドレス 実体を宣言時に 必ず代入 メンバアクセス .(ピリオド) ->(アロー演算子) .(ピリオド) &を付けると アドレスになる ポインタの アドレスになる アドレスになる *を付けると エラーになる 実体が取れる エラーになる • 関数の引数として使う場合、オブジェクトは 実体渡しだと不都合が生じる場合が多い – 読み出し専用は参照、いじくる場合はポインタ、 という使い分けをすることが多い ポインタだからできること • newとdeleteの使用 – スコープに左右されずに確保したいメモリや オブジェクトを宣言できる • 大きなサイズの配列など – ポリモフィズムを利用したオブジェクト生成 • 1つのポインタ変数が状況によって指す 対象を切り替えることができる – 最初はとりあえずNULLにしておくことも可能 newとdelete • new 型名(); で 実体を作成する – 配列なら new 型名[個数]; とする – 実体ができた場所の アドレスが得られるので、 ポインタ型の変数で 捕まえて利用する • newしたものは 基本的に自分で 片付けねばならない • 片付けるには delete アドレス;とする – 配列を作った場合には delete [] アドレス;とする • うかつなdeleteは死を 招く – まだ利用しているものを delete したりとか • かといってdelete しないでいると、 プログラムが、OSが、 死ぬ まとめ • ポインタはエロ画像のURLである – 参照はその簡易版である • ポインタ(参照)を使えば、スコープの枠を 飛び越えて変数やオブジェクトを扱える 今日の課題 • 2変数の中身を入れ替える関数を作ろう – ポインタ・参照どちらでもよい – 入れ替えるのはint型かdouble型とする • それぞれのバージョンを作るとなおよし • 期限は来週の授業開始時まで – 超絶楽勝だから今回は期限厳守で! TO BE CONTINUED…
© Copyright 2024 ExpyDoc