7.変数のスコープと寿命


7−1.暗黙のグローバル変数

スクリプトの一番最初の解説を思い出してもらいたいのですが、スクリプトには「SpE4.exeの起動時に直接起動されるスクリプト」と「アンカーポイントに記述するスクリプト」がありました。 そしてSpE4.exeの起動時に直接起動されるスクリプトの説明にはこう記述していました。

===============
#include "SpeSystem/script/system.inc"
function main(){
    message_box("HelloWorld", "", 0);
}
===============

ここまで読んだ方ならば、mainという関数が定義され、その中でmessage_box関数が呼ばれている、というのが理解できると思います。一方で、アンカーポイント内のスクリプトではmain関数などはありませんでした。 本稿の内容はこの点の違いが大きく影響するので、最初はこちらで説明させてもらいます。

さて、解説に戻りますが、これを少し変えてみます。

===============
#include "SpeSystem/script/system.inc"
function main(){
    msg_str="HelloWorld";
    test();
}
function test(){
    message_box(msg_str, "", 0);    //  ここでランタイムエラーが発生する。
}
===============

main関数内で作った変数を別の関数で使おうとしました。ローカル変数はその関数の中だけしか使えません。どこでも使えるのはグローバル変数ですので、こう書けばOKです。

===============
#include "SpeSystem/script/system.inc"
msg_str="HelloWorld";
function main(){
    test();
}
function test(){
    message_box(msg_str, "", 0);    //  正しく動く
}
===============

変数を書く場所によって、ローカル変数がグローバル変数に変わりました。これを『暗黙のグローバル変数』と呼びます。

ところで、グローバル変数と異なりローカル変数は他の関数では使えない、このような「変数が使える範囲」のことを変数の『スコープ』と呼びます。 ローカル変数のスコープはその関数の内部のみ、暗黙のグローバル変数のスコープはそのソースコード単体です。ソースコード「単体」とあえて書いたことには意味がありますが、詳しくは後述します。


7−2.明示的グローバル変数と静的ローカル変数

さて、「暗黙の〜」と書いたからには「明示的なグローバル変数」があります。いきなりですが以下のスクリプトは正しく動作します。

===============
#include "SpeSystem/script/system.inc"
function main(){
    extern msg_str="HelloWorld";
    test();
}
function test(){
    message_box(msg_str, "", 0);     //  変数msg_strは関数の外でも使える
}
===============

変数宣言としてexternを付与すると、強制的にグローバル変数となります。グローバル変数なので、以下のスクリプトは「1」「2」「3」と表示されます。

===============
#include "SpeSystem/script/system.inc"
function main(){
    test();
    test();
    test();
}
function test(){
    extern num=0;     //  グローバル変数なので0が代入されるのは最初の一回のみ
    num+=1;
    message_box(num, "", 0);
}
===============

関数の外に書いたことと同じ振る舞いになります。 厳密にはexternを付与すると『グローバル変数』よりもさらに大きな『外部参照変数』になりスコープが「ソースコード全て」に変わりますが、これについても後述します。 なお、externは変数の宣言と同じなので、intやfloatなども使えますし、配列やオブジェクトの初期値設定も可能です。

===============
extern int num=0;             //  int型の変数を宣言
extern list=[0,1,2,3,4,5,6];  //  配列もOK
extern string std_name;       //  初期値を設定しなくてもOK
extern num=0;                 //  型を何も書かないと可変型になる
===============

ところで上の例では、externを付与することで変数numの内容が関数を抜けても保持されていました。グローバル変数ですから当たり前です。 ここで、ローカル変数だけど変数の内容はずっと保持して欲しい、という要求もあったりします。その場合はこのように記述します。

===============
#include "SpeSystem/script/system.inc"
function main(){
    test();
    test();
    test();
}
function test(){
    static num=0;     //  静的ローカル変数なので0が代入されるのは最初の一回のみ
    num+=1;
    message_box(num, "", 0);
}
===============

staticを付与することで、その変数は『静的ローカル変数』となり、関数を抜けても内容が保持されるようになります。 このようなものをいちいち使わずにグローバル変数にすればよいと思うかもしれませんが、そうすると、他の関数ではその同じ変数名が使えなくなってしまいます。 staticを使うことで、例えば「count」というような単純な変数名も問題なく使えるようになります。もちろん、その関数内部で名前を重複させないように管理しておく必要はありますが。 ちなみに「静的」の対義語は「動的」です。通常のローカル変数は関数を呼ぶたびに新規に作成及び破棄されますが、当たり前なので『動的ローカル変数』とはあまり言いません。

ここで変数のスコープと寿命についてまとめます。
名称記述スコープ寿命
ローカル変数関数内で記述その関数内関数を抜けたら削除
グローバル変数関数外で記述ソースコード単体スクリプト終了まで
外部参照変数extern ...;ソースコード全て(複数)スクリプト終了まで
静的ローカル変数static ...;その関数内スクリプト終了まで


7−3.アンカーポイントにおけるグローバル変数

上記の説明は「SpE4.exeの起動時に直接起動されるスクリプト」での話ですが、6.関数の使い方(応用編)でのグローバル変数の説明のとおり「アンカーポイントに記述するスクリプト」では下の例のように、明示的にexternを付与する必要がありました。

===============
extern int b=0;     //グローバル変数bを宣言しておく
function my_function(int a)
{
    b=a*2;          //引数aを2倍にしてbへ代入(bはグローバル変数なのでどこでも使用可能)
}
my_function(5);     //引数を2倍にする関数を呼ぶ
print_string(b);    //関数my_functionで2倍になった変数bを表示する。
===============

この違いが少し紛らわしいので「違う」ということを改めて説明しておきます。違う理由やその仕組みについては、次章で詳しく解説します。


7−4.インクルードとリンクと外部参照

「5.関数の使い方(入門編)」において、インクルードの説明を少ししました。 本稿ではこのインクルードおよび後述のリンクについてさらに詳しく説明します。これより先はかなり上級者向けの機能になりますので、よく分からない場合は一旦ここまでの説明の内容でゲームを作ってみて、慣れた頃に戻ってくると良いでしょう。

さて、インクルードとはつまりこれですね。

===============
#include "SpeSystem/script/system.inc"
===============

UnpackPackage.rspを使ってSpeSystem.pacを解凍するとsystem.incというファイルがありますが、これはソースコードがそのまま間に挿入されます。 ここでは大量のシステム関数の定義をまとめて行っているのですが、この機能を自分で使うと、スクリプトをいくつかのファイルに分割することが出来ます。

===============
(↓アンカーポイント「main」内に記述↓)
#include "test.inc"
a=5;                //ローカル変数a
a=my_function(a);   //引数を2倍にする関数を呼ぶ
print_string(a);    //関数my_functionで2倍になった変数bを表示する。
(↑アンカーポイント「main」内に記述↑)

(↓「test.inc」というファイルを作ってそこに記述↓)
function my_function(int b)
{
    return b*2;     //引数bを2倍にして返す関数
}
(↑「test.inc」というファイルを作ってそこに記述↑)
===============

上記のスクリプトをコンパイルすると、「#include」の行に「test.inc」が単純に挿入されるため、以下のようになります。

===============
function my_function(int b)
{
    return b*2;     //引数bを2倍にして返す関数
}
a=5;                //ローカル変数a
a=my_function(a);   //引数を2倍にする関数を呼ぶ
print_string(a);    //関数my_functionで2倍になった変数bを表示する。
===============

普通に関数を作って呼び出すスクリプトです。この仕組みのメリットは共通処理や定義を自分で作っておいたときに、アンカーポイントごとにコピペする必要がなくなります。 後からその自作の共通部分を修正したくなったときもファイルを一つ修正するだけで完結します。この仕組みを『インクルード』と呼びます。

このインクルードは非常に便利な仕組みですが、単純に挿入されてしまうことが一つのデメリットとなってしまいます。 複数のソースコードを作って全てインクルードするときに、以下のような同じ変数名を使うことが出来ません。

===============
(↓アンカーポイント「main」内に記述↓)
#include "test1.inc"
#include "test2.inc"
str=get_xxx();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
str=get_yyy();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
(↑アンカーポイント「main」内に記述↑)

(↓「test1.inc」というファイルを作ってそこに記述↓)
str="xxx";            //変数に文字列を入れておく
function get_xxx(int b)
{
    return str;       //文字列"xxx"返す関数
}
(↑「test1.inc」というファイルを作ってそこに記述↑)

(↓「test2.inc」というファイルを作ってそこに記述↓)
str="yyy";            //変数に文字列を入れておく
function get_yyy(int b)
{
    return str;       //文字列"yyy"返す関数
}
(↑「test2.inc」というファイルを作ってそこに記述↑)
===============

test1.incとtest2.incが単純にコピーされるのでこうなります。

===============
str="xxx";            //変数に文字列を入れておく
function get_xxx(int b)
{
    return str;       //文字列"xxx"返す関数
}
str="yyy";            //変数に文字列を入れておく
function get_yyy(int b)
{
    return str;       //文字列"yyy"返す関数
}
str=get_xxx();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
str=get_yyy();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
===============

3箇所全てで同じ変数strを使っていますので、コンパイルは出来て実行されますが、間違った実行結果となります。 インクルードの仕組みを使って汎用的なライブラリを作ろうとしたときに、使ってはいけない変数名がどんどん増えていってしまったのでは困ります。 そういう時はこうします。

===============
(↓アンカーポイント「main」内に記述↓)
#link "test1.inc"
#link "test2.inc"
str=get_xxx();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
str=get_yyy();        //関数を呼び文字列を得る
print_string(str);    //関数から得た文字列を表示する
(↑アンカーポイント「main」内に記述↑)

(↓「test1.inc」というファイルを作ってそこに記述↓)
str="xxx";            //変数に文字列を入れておく
extern function get_xxx(int b)
{
    return str;       //文字列"xxx"返す関数
}
(↑「test1.inc」というファイルを作ってそこに記述↑)

(↓「test2.inc」というファイルを作ってそこに記述↓)
str="yyy";            //変数に文字列を入れておく
extern function get_yyy(int b)
{
    return str;       //文字列"yyy"返す関数
}
(↑「test2.inc」というファイルを作ってそこに記述↑)
===============

「#include」という記述が「#link」という記述に変わり、また関数の最初に「extern」が追加されました。これは上で『外部参照変数』として説明したものでした。 今回の場合は関数なので『外部参照関数』となります。たったこれだけの違いなのですが、このように書くことでインクルードと異なり、コピーされなくなります。 しかしexternを付与した変数または関数のみ、別のファイルからその変数書き換えたり、その関数を呼んだりできるようになるのです。 言い換えると、externをつけない変数や関数は、そのソースコード単体のみアクセスできます。これらの仕組みを『リンク』と呼びます。

これが上で表にまとめた変数のスコープの「グローバル変数」と「外部参照変数」の違いです。 繰り返しになりますが、『インクルード』はどれだけやっても「一つのソースコード」とみなされるのでグローバル変数は別のファイルでも共有です。 一方で『リンク』は別のソースコードとなるので、単なるグローバル変数は別のファイルのスクリプトからはアクセスできません。「extern」をつけて外部参照にする必要があります。 言い換えれば、リンクする場合、「extern」を付与しない普通の変数や関数は、そのほかのソースコードから保護されていると言えます。

補足説明になりますが、「#include」は書くたびに単純にその場所にファイルが挿入されますが、「#link」は「そのファイルを外部参照するよ」という宣言に過ぎないので、どこに書いても結果は同じですし、複数回書いても2回目以降は無視されます。 外部参照したスクリプトはそれ単体で一つのスクリプトとしてコンパイルされます。必要なインクルードは各ファイルごとに行う必要があります。この部分の詳しい仕様についてはアンカーポイントの仕様にも関わっているので、次章で解説します。


最初のページへ戻る