変数のさまざまな性質について理解し, それらをうまく使い分けられるようになろう.
また,これまでにほとんど説明もなく使ってきた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 のアドレスは不変.