見出し画像

C言語教室 第39回 宣言文と実行文と初期化

唐突かもしれませんが、プログラムのコードを見て、それぞれの文がCPUにとって何を意味するのか意識したことはありますか。例えば

int i;

という文は、プログラムの中で、その後 i という変数が登場したら、それを「整数型の変数であると解釈せよ」という指示を「コンパイラ」に対して出しているだけで、その行で何らかの処理をCPUが実行するわけではありません。他にも typedef のような文も1行もコードを出すことはなく、このような文を「宣言文」と呼ぶことがあります。

これに対して

printf(”%d\n”, i);

という文は、整数型である変数 i の値と文字列リテラルの先頭のアドレスを順にスタックに積んで printf という名前の関数のアドレスを呼び出すという処理を行い、そのためのマシン語のコードを作ります。こちらは「実行文」と呼ばれることもあります。

こんな区別をしてもどんな意味があるんだと思うかもしれませんが、いざプログラムを書いてデバッガで走らせると、宣言文の部分にはコードが無く、そこでブレークポイント(デバッグのために一時的にプログラムを止める場所)を設定することも出来ませんし、コード自身には宣言に関する情報は含まれていません。とはいえ宣言文は何もしていないわけではなく、プログラムの他の場所に形を変えて痕跡を残しているのです。

例えば先の i という変数の宣言ですが、これが関数内で宣言されたローカル変数であった場合、スタックポインタまたはフレームポインタと呼ばれる関数内での変数領域の起点から何バイト目から例えば4バイトが、この変数の値が入るアドレスであるというのをコンパイラが決めて、このアドレスが使われるようにします。これがグローバル変数であれば、初期化されない変数なので、BSS領域と呼ばれるプログラム開始時に動的に割り当てられるメモリ領域の何バイト目からが、この変数のアドレスであるということをコンパイラが決めます。ですから宣言文自身のコードはありませんが、実行文である printf を実行するときには、もう変数 i のアドレスは決まっていて、このアドレスの値が関数を呼び出す時に使われるのです。

さて、ここまではそんなに悩むこともなく何となくはわかるかもしれません。初期のC言語は、この宣言文と実行文の区別がうるさくて、関数においては、まず宣言文を書いてから実行文を書くという順序が決まっていました。つまり何らかの実行文の後に宣言文を追加するとエラーとなりコンパイルすることが出来なかったのですね。当然、for文の制御変数を、その場で宣言するなんて言う芸当は禁止されていたわけです。これはコンパイラの処理を軽くすることが目的だったのだろうと思います。実行文でコードを生成する前にすべての変数のアドレスを決定してしまえば少しは楽ですし、レジスタのレイアウトなどの最適化もやりやすいです。

その後のC言語の仕様改定で、この制限はなくなり、実行文の後に宣言文を書いたりすることも出来るようになりましたが、大部分の実装を見る限り、変数の有効範囲をコンパイラがチェックするだけで、宣言のたびにスタックの調整などをすることはなく、その関数の中でのアドレスは先頭で宣言した変数と同じように確定させているようです。但し重ならないブロック内で有効な変数に関しては同じアドレスが再利用されることもあるようです。このあたりの話はC言語では、あまり気にする必要は無いのですが、C++ではどこでデストラクタが実行されるかという話にもなるので、覚えておいても損はありません。

ここまで来て「もうわかった」と思われる方も居るかも知れませんが、まだ罠が残っています。それが初期化です。通常のローカル変数はスタック上にアドレスが割り当てられるので、関数が実行されるまで、そのアドレスは決まりません。その為、宣言文の位置で関数が実行される都度、初期化の処理が行われます。つまり実行文なわけです。それに対してグローバル変数やローカルな静的変数は、プログラム開始時に初期化される値の入ったアドレスを確定させてしまうので、関数を実行するときには何も行いません。つまり宣言文のままなわけです。

void func() {
  int i = 1;
  static j = 2;

  i = 3;
  j = 4;
}

つまり func が呼ばれた時に、毎回 i に対しては、そのアドレスに 1 という整数を格納するのですが、j に関してはプログラム開始時に 2 という値が入ったアドレスを決めるだけで、関数を実行するときには何もしません。これで次に func が呼ばれたときには、i には再び 1 が入りますが、j は前回 4 を格納したので、4 のままになるのです。C99以前は初期化はリテラルでしか行えなかったのですが、今は関数を呼び出すことも出来るので、事態はより複雑になります。static 変数の初期化で関数を呼び出すと、その関数は main関数の実行に「先立って」実行されます。この時に、どの関数の初期化がどの順序で実行されるかは決まっていません。ですから初期化で呼び出される関数で使われる変数の初期化が、やはり関数呼び出しで行われる場合、必ずしも正しく初期化されるとは限りません。さらにスレッドを起こした場合、その時に静的変数の初期化が再び呼び出されることもあり、初期化が必ずしも1度しか行われないと決めてかかることもできないのですね。ただでさえ static な変数はスレッドとの相性に問題が出やすいのですが、こういうところまで考えていられないという方が「マトモ」なので、いくら可能だと言っても複雑な初期化は宣言で行わずに、明示的な関数を用意したほうが飼い慣らしやすいです。

いろいろとややこしいことを説明してきましたが、C言語は何だかんだ、そのコードからCPUが何をしているのかの想像がし易いです。デバッガでコードを追えば、ほぼ対応したコードを見つけることができますし、コードの無い部分で謎の処理が行われることはまず無いです。そういう意味ではCPUの気持ちに寄り添った処理系なんだと思います。大体においてCPUにとって複雑な処理はC言語のコードも複雑なものになります。つまりC言語に足りないものは、そもそもCPUにとっても足りないもので、この「近さ」が特にシステム周りの記述にC言語が使い続けられている大きな理由なんでしょう。もっとも最近のCPUの進化にC言語が追いついているのかといえば、そうとも限らなくて、特にマルチコアな時代にスレッドが使いにくいのは大きな制約になっていますし、ある意味CPUの情報が隠蔽することもなく丸裸で見えてしまうのは、セキュリティリスクが減らない原因にもなっています。

とはいえすべてのシステムはC言語から始まっていると言っても過言ではない状態はまだ続いているので、この言語と仲良くすることこそ「使えるプログラミング」能力が身につくと言って間違いないでしょう。

ヘッダ画像は、いらすとや より
https://www.irasutoya.com/2017/07/blog-post_1.html

#C言語 #プログラミング講座 #宣言文 #実行文 #初期化 #static  

この記事が気に入ったらサポートをしてみませんか?