プロジェクト演習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は今後必要に応じて掘り下げます
© Copyright 2024 ExpyDoc