プロジェクト演習 インタラクティブゲーム制作

プロジェクト演習III,V
<インタラクティブ・ゲーム制作>
プログラミングコース
第5回
グローバル変数とファイル分割と
コンパイルの割と根本的な話
今日のメニュー
• グローバル変数の安全な使い方
– リフスローの発表のフォローアップ的内容
– あまり使うことをおすすめはしませんが、
必要悪な場合もあるので覚えておこう
• ファイル分割とコンパイルの関係
– 根本から理解していないと誤解を引きずるの
で丁寧に話します
嫌われがちだけど、必要な時もあるんだよ?
グローバル変数の安全な使い方
グローバル変数とは
• どの関数からでもアクセスできる変数
• あまり利用は推奨されないが、
「定数」を定義する際にはよく使われる
• 先週のコードレビューで2つのヘッダに
分割して定義と宣言を行っていたが、
あの利用方法には若干問題がある
– どちらもヘッダになっているので
分離している意味が無い
グローバル変数もお品書き(宣言)
と実体(定義)に分けよう
Global.h
Global.cpp
#pragma once
#include “Global.h”
extern const int WIDTH;
extern const int HEIGHT;
const int WIDTH = 800;
const int HEIGHT = 600;
void procGlobalFunc(void);
void procGlobalFunc(void)
{
// 処理実体を記述
return;
}
ポイント
• ヘッダでは型名の前にexternを付けて、
変数の型と名前だけを「宣言」する
– externを付けると「実体は他にある変数の
宣言だけするよ」という意味になる
• 実体を書くためのcppを用意し、extern宣言
を書いたヘッダをインクルードしてから、
変数の実体を「定義」する
– 定数なら定数値の代入も行っておく
• 同様の手順で関数も宣言、定義が可能
– 関数の場合は宣言側のexternは不要
使用上の注意点
• 基本的に「定数」しか使わないのが無難
– 変数をあちこちからいじるのは超危険
• 間違っても「クラスオブジェクトの実
体」をグローバルに置いてはいけない!
– どうしても必要な場合はポインタをグローバ
ルに置いて初期値をNULLにしておき、初回
使用時にnewするようにして使う
– デザインパターンのSingletonに近い考え方
グローバルオブジェクト利用例
Global.h
Global.cpp
#pragma once
#include “GlobalHoge.h”
#include “Global.h”
/*
ポインタを返す関数を用意して、
ダイレクトに触らせない方が良い
*/
//extern GlobalHoge *g_hoge;
GlobalHoge* getHoge(void);
GlobalHoge *g_hoge = NULL;
GlobalHoge* getHoge(void)
{
if(g_hoge == NULL) {
g_hoge =
new GlobalHoge();
}
return g_hoge;
}
ダメな理由
• 以下の状況を前提とする
– a.cpp、b.cppにそれぞれ「HogeA g_a」と
「HogeB g_b」が定義されている
– HogeAはHogeBの情報を利用する
• つまりHogeAより先にHogeBのオブジェクトが
作られていなければならない
• このプログラムを実行した時、g_aとg_b
のうち、どっちが先に生成されるか?
– それは誰にも分からない
C++(C)のコンパイルの仕組みについて述べます
定義と宣言の分離が必要な理由
cppからexeまでの流れ
1. 各cppごとにインクルードを処理します
– 実質的にやっているのは「コピペ」です
– ヘッダファイルの内容をその場所に取り込んで、結
果的に「宣言」を取り込むことになります
2. ヘッダを取り込んだcppを翻訳(コンパイル)して、
中間ファイル(obj)にします
–
文法間違い、未宣言のクラスや変数の使用はここで
「コンパイルエラー」として弾かれる
3. 中間ファイルとライブラリを結合(リンク)して、
実行ファイル(exe)にします
–
利用するライブラリの指定ミスや、同名の関数や変
数がかち合った場合は「リンカエラー」になる
模式図
Hoge.cpp
Hoge.h
Fuga.h
#include
“Hoge.h”
Fuga.h
Fuga.cpp
#include
“Hoge.h”
#include
“Fuga.h”
Hoge.h
Hoge.h
Hoge.cpp
Fuga.cpp
コンパイル
main.cpp
Fuga.h
#include
“Hoge.h”
#include
“Fuga.h”
Hoge.h
main.cpp
コンパイル単位はcpp
• ヘッダの内容はcppにインクルードされない
限り、プログラムに何も影響も与えない
• インクルードが済んだ時点で、利用するクラ
ス、変数、関数の「定義か宣言」が含まれて
いないとエラーになる
– 見えないものは使えない
• 複数のcppそれぞれに「宣言」が含まれるの
は問題ないが、「定義」がそれぞれで行われ
るとリンカエラーになる
インクルードガードの意義
• 「#pragma once」と
書くか、右のようにする
ことで「1つのcppにお
いてはそのヘッダが1回
しかインクルードされな
い」ようにできる
• 2つのヘッダが同じもの
をインクルードしようと
している際にガードする
ことができる
– 模式図の例など
• 異なるcpp間での多重定
義は防げない
#ifndef __HOGE.H__
#ifdef __HOGE.H__
// ここにヘッダの宣言を書く
#endif
「#pragma once」が使えないコンパイラ
ではこちらの書き方を使う。
原理はプリプロセッサの仕組みを利用した
テクニックで、「__HOGE.H__」が定義
されていなければその文字列を定義した上で
宣言を展開する。一度でも宣言されていたら
それ以降はスキップする、というもの。
もしヘッダに「定義」が
含まれていたら?
• グローバル変数の定義
をヘッダでしてしまい、
それを複数のcppで取
り込むと、それぞれの
cppごとに別々の変数
が作られてしまう
– そのcppの中でしか見え
ない変数になる
• 定数値(const)なら大し
て問題にならないが、
変数やオブジェクトの
場合は致命傷になる
Global.h
(実体付き)
Global.h
(実体付き)
Fuga.h
Hoge.h
Hoge.h
Hoge.cpp
Fuga.cpp
Global.h
(実体付き)
Fuga.h
Hoge.h
main.cpp
宣言と定義の分離
• cppに定義を記述する
ことで、コンパイル
された際に実体が生
成される
• 他のcppからはヘッダ
の宣言を通じて、他
のcppに記述された変
数や関数、クラスを
利用できる
Global.h
(宣言)
Global.cpp
(実体)
Global.h
(宣言)
Global.h
(宣言)
Fuga.h
Hoge.h
Hoge.h
Hoge.cpp
Fuga.cpp
Global.h
(宣言)
Fuga.h
Hoge.h
main.cpp
意味は単一でも用途が広くて説明に困る…
謎のキーワードSTATICの正体
static、この説明の難しきもの
• 一言で説明するのがとても難しい
– 「スコープ内に静的な変数および関数を定義
し、その唯一性を保証する」じゃ分からんで
しょ?
• なので、代表的な目的と用途を述べるに
留めます
staticの2大用途(+1)
• クラスのメンバに付けて「インスタンスとは
関係なく、クラスのスコープ内で共通の値を
持った変数、関数を作る」時
• ローカル変数に付けて「初期化は1回だけ、
スコープを抜けても値が保持される変数にし
たい」時
• 1つのcppの中だけで扱いたいグローバル変
数、関数を作りたい時
– externなどで外部から宣言しても扱えなくする
まとめ
• とりあえずC++では「宣言」と「定義」
に分けるのがいいらしい
• VisualStudioでは「ビルド」の一言で済
ましているけど、実は中では様々なドラ
マがあるらしい
• extern、const、staticの用途はとりあえ
ずバッチリ!
– staticは今後必要に応じて掘り下げます