今回はまず,これまでにほとんど説明もなく使ってきたCの基本的な事項 ({ } や ; など)について,正しく理解しよう.
また,変数のさまざま性質について理解しよう.
Cのソースコードは次のような単語・記号を要素として構成されている:
データや演算子を組み合わせたものは式, 予約語や式を組み合わせたものは文と呼ばれている. 文の末尾には必ずセミコロン「;」が置かれる.
複数の文を中括弧「{ }」でまとめたものは, ブロックと呼ばれ, ひとつの文と同じように取り扱われる. これまでに何度も,for 文や関数とともに利用してきた.
ブロックの中に,さらにブロックを作ることも可能だ. たとえば,for ブロックの中に, さらに for ブロックとか if ブロックを入れてよい. ただし,一番外側のブロックは関数(名前付きのブロック)でなければならない. また,関数の中に関数を定義することはできない.
なお,ブロックの記号 { } は, 文だけでなく複数のデータをひとまとめにするためにも利用される. たとえば,配列の初期化 にも使っていた.
違いに注意:
Cでは,異なる場所(ブロック)で定義された変数は, たとえ名前が同じであっても異なる変数として区別される. (名前は同じでも,異なるメモリ領域を使い,実体は異なる. 要するに,同姓同名の他人だ.)
たとえば,次のようなソースコードでは, 関数 main( ) 内の変数 a, x と 関数 sub( ) 内の変数 a, x とは, 名前は同じだが,メモリ上の実体はまったくの別物になる:
sub()
{
int a;
int x;
...
}
main()
{
int a; // 上の a と同型同名だが,別物
double x; // 上の x と異型だが,別物なので無問題
...
}
変数 x の型の不一致については, そもそも別物なので,気にする必要はない.
名前と実体の区別については,変数だけでなく関数でも同様. たとえば再帰呼び出しの場合, 前回の説明の通り, 呼び出し側と呼び出され側は同じ名前の関数だが, 実体は異なる物(分身,コピー)だった.
一方,複数の関数の間で同一の変数を共有したいという場合もある. そこでCでは,変数の スコープ(scope;通用範囲)を 使い分けられるようになっている:
なお,ローカルとグローバルに同じ変数名が使われている場合, そのブロック内ではローカル変数だけが有効になり, グローバル変数は一時的に無効になる.
しかし,この能能を利用してはいけない. ローカルとグローバルに同じ名前をつけてしまうと, どちらの変数を使っているのか?混乱の元だ.
オススメの命名方法:
List 1 のプログラムを実行し, グローバル変数とローカル変数の違いを考えてみよう. 同じ名前の変数がいくつも定義されているが, それぞれ異なる変数であることを確認しよう.
// グローバル変数
char *s = "global";
int x = 100;
/* 関数 sub */
sub()
{
printf("sub : s = %s , x = %d\n",s, x);
}
/* 関数 main */
main()
{
// main( ) のローカル変数
char *s = "local";
int x = 10;
sub();
printf("main1 : s = %s , x = %d\n",s, x);
// 名無しブロック
{
// 名無しブロックのローカル変数
char *s = "block";
int x = 20;
printf("block : s = %s , x = %d\n", s, x);
}
printf("main2 : s = %s , x = %d\n", s, x);
}
変数の宣言(int x 等)については, 必ず,ブロック内の先頭部分に記述すること. 他の部分に記述しても許してくれるコンパイラもあるが, これはあくまでもC言語の方言であり,標準語ではない:
...
main()
{
int a; // ブロック内の先頭での変数宣言...正しい
a = 1 + 2; // 何らかの処理
printf(...); // ...
int b; // ブロック内の途中での変数宣言...誤り(動くが...正しくない)
...
}
なお,グローバル変数を使えば関数間でのデータの受け渡しが楽になるので, 「何でもグローバルにすればよい」と考えてしまう人がいる. しかし,よく理解しないまま利用してしまうと,思わぬトラブルの原因にもなる. うまく使い分けること.
グローバル変数の罠:
これまで使ってきたローカル変数は, その関数(ブロック)を実行する時に生成され, その関数(ブロック)を終了する時に消滅する.
ただし,関数終了時に,メモリがクリアされるわけではないので, ローカル変数の値は,メモリ上にゴミとして残ることになる. しかし,それを当てにしてはならない. その領域は,他の関数によっても再使用されるので, その残されたゴミの値は書き換えられてしまう可能性が高い. つまり,次にその関数を呼び出した時には, 以前の変数の値を忘れてしまっていることに注意しよう.
一方,以前の値を憶えておいて欲しい場合もある. そこでCでは,変数の記憶クラス(storage class)を使い分けられる:
List 2 のプログラムを実行し, 記憶クラスによる違いを確認してみよう.
sub()
{
auto int a = 0; // 自動変数
static int s = 0; // 静的変数
printf ("a = %d , s = %d\n",a,s); // テスト出力
a = 10;
s = 10;
}
main()
{
printf("1st call :\n");
sub();
printf("2nd call :\n");
sub();
}
静的変数の濫用もトラブルの元だ. 自動変数で済む場合は,自動変数を使うこと.
変数の種類の選択順序:
引数の整数値を累積し合計値を返す関数を次の2通りの方法で定義せよ:
基本ソースコード:
// グローバル変数の宣言
...
// グローバル変数版の累積関数
int accum1(int x)
{
...
}
// 静的変数版の累積関数
int accum2(int x)
{
...
}
// テスト用メイン関数(書き換え禁止)
main()
{
int i;
// 累積関数による総和 Sn = 1 + 2 + ... + n の計算
for (i = 1; i <= 10; i++) {
printf("S%02d = %2d , %2d\n", i, accum1(i), accum2(i));
}
}
また,静的変数を自動変数に変えるとうまく行かないことも確かめよ.
余裕のある人は,メモリマップについても実験してみるとよい. 関数内で変数(グローバル変数,静的変数,自動変数)のアドレスを表示し, その変化を観察. (実は変化しないが,変数の種類によるアドレスの違いを観察.)
List 3 も試してみよう. 自動変数がどのようにメモリを使っているか? メモリマップの変化を想像しながら, 実行結果を確認しよう. 注目すべきは,sub1( ) のゴミの値の変化.
sub1()
{
int a; // 初期化していない(メモリを書き換えていない)
printf("sub1: %d\n", a); // ゴミを表示
}
sub2()
{
int b = 2; // 初期化している(メモリを書き換えている)
printf("sub2: %d\n", b); // 初期値を表示
}
main()
{
sub1(); // a=ゴミ
sub1(); // a=ゴミ
sub2(); // b=2
sub1(); // a=何? なぜ?
printf("%f\n", 1.234); // この関数でも実はメモリを...
sub1(); // a=何? なぜ?
}
sub1() 内と sub2() 内で, 変数 a,b のアドレスも表示してみれば, わかりやすくなるかもしれない.
このように,自動変数であっても, static のように記憶できる場合もある. しかし,それを当てにしたプログラムを作ってはいけない.
List 4 では,静的変数と自動変数のメモリマップの違いについて確認しよう.
int sub1()
{
auto int a;
static int s;
printf("&a = 0x%08X &s = 0x%08X\n", &a, &s);
}
int sub2()
{
int x[100];
sub1();
}
int main()
{
printf("sub1:\n");
sub1();
printf("sub2 -> sub1:\n");
sub2();
}
sub1() だけ呼び出した場合と, sub2 経由で sub1() を呼び出した場合とで, 自動変数 a のアドレスは変化する. 一方,静的変数 s のアドレスは不変.