第 4 回:スコープ・ポインタ・参照

Download Report

Transcript 第 4 回:スコープ・ポインタ・参照

インタラクティブ・ゲーム制作
<プログラミングコース>
第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…