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