第12回 プログラミングについて 『範囲と寿命の話』

『範囲と寿命の話』
プログラムで使用する変数、関数にはそれが使用できる範囲と寿命があります。
昔のインタプリタのBASICは、変数も関数もプログラム内のすべての範囲が勢力圏でプログラムの起動時から終了時まで生き続けるものでした。変数の勢力圏がプログラム全域でしたので1000行程度のプログラムを書くには、よっぽど注意しないと変数の値を間違って変えてしまって正常に動作させるには苦労をさせられました。
FORTRANはFORTRAN77までは、COMMONブロックの変数はプログラム全域、それ以外は各ルーチン内のみで、プログラムの起動時から終了時まで生き続けます。大きなテンポラリの配列を作って計算した後は必要もないのにメモリーを消費したままになってしまうので、色々な工夫をしたものです。
初めのころは最初から運動場用に大きな配列を作っておいて各ルーチンが必要に応じてそれを使用する様にしましたが、間違って複数のルーチンが同時に使用しないように神経をすり減らしたものです。以前もお話しましたが、文字関数を使ってスタック上に自動変数的なメモリーを確保する手法を発見してからは、この方式に切り替えましたがこの方式もFORTRANにポインタがなかったのでスマートには表現できませんでした。
C言語では様々な勢力範囲のものと寿命のものが使用できます(C++ではもっと強力ですが ...)。C言語の変数の勢力範囲と寿命には、
勢力範囲 寿命
-------------- ----------------------------------------------------------
プログラム全域 プログラムの起動時から終了時までの寿命
ファイル内全域 プログラムの起動時から終了時までの寿命
ブロック内全域 プログラムの起動時から終了時までの寿命とブロックの開始時からブロックの終了時までの寿命
の種類があります。関数については、
勢力範囲 寿命
-------------- ----------------------------------------------------------
プログラム全域 プログラムの起動時から終了時までの寿命
ファイル内全域 プログラムの起動時から終了時までの寿命
となります。関数の寿命はすべてプログラムの起動時から終了時までです。
関数の外側で定義された変数は、プログラム全域にその勢力範囲が及びます。そしてプログラムの起動時から終了時までが寿命となります。
ファイル a.c ファイル b.c
------------ ------------
int data; int data; /* 正式には extern int data; と */
/* 宣言する */
main() sub2()
{ {
: :
/* ここで data を使える */ /* ここで a.c と同じ data が使える */
: :
} }
sub()
{
:
/* ここで data を使える */
:
}
この様に関数の外側の変数 data は他のソースファイルの外側で int data; が定義されても同じものと解釈されますが、コンパイル時にデータが初期化されているものが定義されていないものは宣言と解釈されます。もし定義がなければコンパイラまたはリンカが自動的に1つだけを定義とみなします。定義が2つ以上あると叱られます。正式には2つ目以降は extern 文を使って明示的に宣言することになっています。
関数の外側で定義する変数を static 属性をつけると、変数の勢力範囲はそのソースファイル内のみとなります。寿命はプログラムの起動時から終了時までです。
ファイル a.c ファイル b.c
------------ ------------
int data; static int data;
main() sub2()
{ {
: :
/* ここで data を使える */ /* ここでの data は a.c とは違う */
: :
} }
sub()
{
:
/* ここで data を使える */
:
}
C言語でブロックとは、{ } で囲まれた部分を言います。ブロックにはブロックが入れ子になることができます。このブロックで定義された変数はそのブロック全域が勢力範囲となります。入れ子になったブロックにも勢力範囲が及びますが、入れ子のブロックで同一名の変数が定義されるとそのブロック内では外側のものとは別となります。
int data=10;
main()
{
printf("%d\n",data); /* 10 が表示される */
{
int data=20;
printf("%d\n",data); /* 20 が表示される */
{
int data=30;
printf("%d\n",data); /* 30 が表示される */
{
printf("%d\n",data); /* 30 が表示される */
}
}
printf("%d\n",data); /* 20 が表示される */
}
printf("%d\n",data); /* 10 が表示される */
}
おおよそこのようになります。
関数は、
ファイル a.c ファイル b.c
------------ ------------
s1() s3()
{ {
: :
/* ここで s1、s2、s3 が使える */ /* ここで s1、s3、s4 が使える */
: :
} }
static s2() static s4()
{ {
: :
/* ここで s1、s2、s3 が使える */ /* ここで s1、s3、s4 が使える */
: :
} }
となり、static属性の関数はそのソースファイル内では使用できますが、他のファイルでは使用できません。
このように様々な勢力範囲と寿命があるのはプログラミングに柔軟性を持たせるためです。
プログラムの全域が勢力範囲の変数は、当然ですがプログラム全域で共通に使用する目的に使用します。これは便利なものなのですが、なんでもかんでもこれにしてしまうと、おおきなプログラムを作ったときなどにはいつどのルーチンが使用しているかが分りずらくなりますので注意して下さい。またプログラムの変更作業が煩わしくなる可能性があります。
#define PROC_MODE_ADD 1
#define PROC_MODE_DEL 2
int process_mode=PROC_MODE_ADD;
main()
{
:
if(process_mode==PROC_MODE_ADD){
process_mode=PROC_MODE_DEL;
:
}
:
if(process_mode==PROC_MODE_DEL){
process_mode=PROC_MODE_ADD;
:
}
:
}
sub1()
{
:
if(process_mode==PROC_MODE_ADD){
process_mode=PROC_MODE_DEL;
:
}
:
if(process_mode==PROC_MODE_DEL){
process_mode=PROC_MODE_ADD;
:
}
:
}
:
上の例はプログラム全域が勢力範囲である process_mode という変数が現在の処理モード(追加と削除の2種類)を示しているとします。この変数をプログラム全域で使用しており、モードの変更を各ルーチンが任意に行なっているものです。1千行程度の小さいプログラムのときにはこれでも問題はありませんが、1万行ぐらいに大きくなって、更に複数のソースファイルに分割してプログラムを組み立てていくと事情が変わってきます。
いまモードの変更に際して、モードの変更が可能か否かを示す変数 enable_change_mode を追加しなければならなくなったとします。この例の場合ではモード変更をしているルーチンすべてを下の例のように変更しなければなりません。
#define PROC_MODE_ADD 1
#define PROC_MODE_DEL 2
int process_mode=PROC_MODE_ADD;
int enable_change_mode=1;
main()
{
:
if(enable_change_mode){
if(process_mode==PROC_MODE_ADD){
process_mode=PROC_MODE_DEL;
:
}
}
:
if(...)enable_change_mode=0;
:
if(enable_change_mode){
if(process_mode==PROC_MODE_DEL){
process_mode=PROC_MODE_ADD;
:
}
}
:
}
sub1()
{
:
if(enable_change_mode){
if(process_mode==PROC_MODE_ADD){
process_mode=PROC_MODE_DEL;
:
}
}
:
if(...)enable_change_mode=1;
:
if(enable_change_mode){
if(process_mode==PROC_MODE_DEL){
process_mode=PROC_MODE_ADD;
:
}
}
:
}
:
こういう変更を随所でやらなければなりません。もし WindowsやXwindowのプログラムであれば、モードが切り替わったことも表示しなければなりません。
これでは小さい変更内容がおおきな変更作業になってしまいます。
そこでモードの切り替えに関する管理を1つのかたまり(1つのソースファイル内の複数のルーチン)に行なわせるようにします。モード切り替えの可能/不可能の変数がない例では、
ファイル procmode.h
-------------------
#define PROC_MODE_ADD 1
#define PROC_MODE_DEL 2
ファイル main.c
-------------------
#include <procmode.h>
main()
{
:
if(proc_mode__change()==PROC_MODE_DEL){
:
}
:
if(proc_mode__change()==PROC_MODE_ADD){
:
}
:
}
sub1()
{
:
if(proc_mode__change()==PROC_MODE_DEL){
:
}
:
if(proc_mode__change()==PROC_MODE_ADD){
:
}
:
}
:
ファイル procmode.c
-------------------
#include <procmode.h>
static int process_mode=PROC_MODE_ADD;
proc_mode__change()
{
if(process_mode==PROC_MODE_ADD)process_mode=PROC_MODE_DEL;
else process_mode=PROC_MODE_ADD;
return process_mode;
}
とします。 procmode.c では変数 process_mode を他人には直接触らせないようにこのソースファイル内の全域でのみ使用できるように static としています。他のルーチンは procmode.c が一体何をやっているかの詳細は知りませんが、 procmode.c が何をしてくれるかを知っているのみです。
次にモード切り替えの可能/不可能の変数を追加してみましょう。
ファイル procmode.h
-------------------
#define PROC_MODE_ADD 1
#define PROC_MODE_DEL 2
ファイル main.c
-------------------
#include <procmode.h>
main()
{
:
if(proc_mode__change()==PROC_MODE_DEL){
:
}
:
proc_mode__disable_change();
:
if(proc_mode__change()==PROC_MODE_ADD){
:
}
:
}
sub1()
{
:
if(proc_mode__change()==PROC_MODE_DEL){
:
}
:
proc_mode__enable_change();
:
if(proc_mode__change()==PROC_MODE_ADD){
:
}
:
}
:
ファイル procmode.c
-------------------
#include <procmode.h>
static int process_mode=PROC_MODE_ADD;
syatic int enable_change_mode=1;
proc_mode__change()
{
if(enable_change_mode){
if(process_mode==PROC_MODE_ADD)process_mode=PROC_MODE_DEL;
else process_mode=PROC_MODE_ADD;
}
return process_mode;
}
proc_mode__enable_change()
{
enable_change_mode=1;
return enable_change_mode;
}
proc_mode__disable_change()
{
enable_change_mode=0;
return enable_change_mode;
}
となります。だいぶ変更作業の労力が減ります。またモード切り替えの可能/不可能が他の状態にも影響されるとなると、 proc_mode__enable_change と proc_mode__disable_change の2つのルーチンの変更で済む可能性もあり、モードの種類が追加されたときでも少ない変更作業で済むことが予想されます。
このようにプログラム全域が勢力範囲となる変数を使用するときには、参照される頻度、変更の可能性、プログラムのスピードなどを考慮しなければなりません。
ブロック内のみで有効な変数でブロックの終了とともに消滅する変数は自動変数と呼ばれますが、メモリーの節約には有効な変数です。またこの変数があるおかげで自分自身を呼び出す関数(再帰呼びだし)が可能となっているのです。この変数はメモリーの節約には大きく貢献する便利なものなのですが、1つだけ注意しなければならないことがあります。変数のポインタを返す関数で、自動変数のポインタを返してはいけないことです。
int *int_point()
{
int data;
data=100;
return &data;
}
という関数は正常に data のポインタを返しますが、data は int_point が呼びだし側に戻るとき消滅してしまうので、呼びだし側は運が良ければ正常に動作し、運が普通だと予測のつかない結果となってしまいます。
変数の初期化にも注意が必要です。
プログラムの起動時から終了時まで生存期間のある変数は、宣言とともに行なう初期化は一度しか行なわれません。それに対してブロックの終了時に消滅してしまう変数は(コンパイラによってはできないものがありますが)、生成されるたびに初期化が行なわれます。
#include <stdio.h>
main()
{
int i;
for(i=0;i<10;i++){
static int j=10;
int k=10;
printf("j=%d k=%d\n",j++,k++);
}
}
上の例の結果は、
j=10 k=10
j=11 k=10
j=12 k=10
j=13 k=10
j=14 k=10
j=15 k=10
j=16 k=10
j=17 k=10
j=18 k=10
j=19 k=10
となります。この例で注意することは、for 文が繰返し実行する {...} のブロックは一回の実行で終了し、for 文が再びブロックの実行を行なうので、自動変数である kはそのつど生成されることになります。
プログラミング言語の違いで変数や関数の勢力範囲と寿命の違いがあります。どの言語にしてもそれらの性格をうまく使い分けることが大切です。
それではまた次回。