コラム
2024/10/31
第85回 プログラミングについて『いまさらですがC言語の門を叩こう その8』 ~ フロー制御文 ~
どんなプログラミング言語にもフローを制御する文があります。要するに、ある条件のときに実行したり、ループを行なうなどの文のことです。
まずIF文を考えてみましょう。IF文と聞けば多分どなたでも想像が付くと思います。『もし~だったらこうして、そうでないときにはもし~だったらああして、どうしようもないときにはそうする』という文のことです。C言語ではこのIF文を次の形式で表現します。
if(式)文;
else if(式)文;
:
:
else 文;
文は1つであっても複数であっても構いません。複数の文を記述するときには、{ }の中に記述します。else if に相当するものや else に相当するものがないときには記述する必要はありません。式は以前にもお話ししたように、最終的に真か偽の値となるものであればなんでも構いません。
if(i)文; ー 変数 i の値が 0 以外のときに文を実行。
if(100)文; ー 定数 100 は真なので常に実行。
if(i==j)文; ー 変数 i と 変数 j が等しいときに実行。
ですので上の例はすべて正しいIF文になります。
if(sub())文; ー 関数 sub の戻り値が 0 以外のときに実行。
しかしながら上の例は関数 sub が void 型のときにはコンパイラに叱られてしまいますので注意して下さい。void 型とはなんにもないという意味で、この場合関数 sub は値を返さないものになります。
IF文自体はあまり注意することはありませんが、
if(i)
if(j)printf("i and j\n");
else
printf("!i\n"); ー(1)
というIF文のときに(1)の else がどのIF文に対応しているかということです。ちょっと意地悪に段下げをして記述していますので if(i) の else のいうな感じもしますが、これは if(j) に対応しているのです。このような記述のときにはC言語の約束に最も近いIF文に対応すると判断するとなっているのです。ですので、
if(i)
if(j)printf("i and j\n");
else printf("!i\n");
と段下げをするのが文の意味に合うことになります。
私の場合、1つの文であっても次の行に記述するときには必ず { } で文を囲むようにしています。この方が表現に曖昧なところがなくなるからです。上記の例を書き直すと、
if(i){
if(j)printf("i and j\n");
else printf("!i\n");
}
と書くことができ、これならば誰でも直感的に意味が理解できると思います。だからと言って同じ行に書くのならば { } を使わなくてもいいのかと言うとそうでもありません。
if(i)if(j)printf("i and j\n"); else printf("!i\n");
こんな風に書いても同じ意味になるのですが、これではコンパイラは理解できても、人間にとっては迷惑そのものです。ポインタと同様、C言語はプログラムの記述方法がかなり自由ですので、本人にも分からないプログラムになってしまいますので、プログラムのスタイルには注意をしなければなりません。
当たり前の様なことなのですが、C言語のIF文内で使用する式でORやAND演算子を使って複数の値の論理和あるいは論理積とする場合、それらの式は記述した順序で評価され、式全体の値として結論付けできるときにはそれ以上の式の評価は行ないません。例えば次のようなプログラムの場合、
int i,j;
i=1;
j=0;
if(i || j++)printf("True.\n");
printf("%d %d\n",i,j);
結果は、
True.
1 0
となります。
if(i || j++)printf("True.\n");
のIF文で、変数 i の値を評価したときに真となったので、それ以上の評価を行なわなくても式全体は真になると判断できるので j++ は実行されないのです。しばしばこの規則を忘れていて、発見しずらいバグになることもあります。逆にこの規則を利用することもあります。
もう1つ大切な規則は、式を評価する順序はコンパイラによって変更されることがないことです。当然といえば当然のことの様ですが、FORTRANのコンパイラは最適化を行なうときにこれをやってしまうことがあるのです。FORTRANは科学計算をできるだけ高速に行なうことを使命にしているので、単純に評価できる部分式を最初に評価するようになっているのです。ですので、
IF(SUB(I,J,K).OR.L.EQ.100)THEN
というFORTRANの文の場合、コンパイラが関数 SUB(I,J,K) の値の評価よりも、L.LE.100 の評価の方が簡単に行なえると判断したときには、
IF(L.EQ.100.OR.SUB(I,J,K))THEN
という内容になるように評価の順番を変える可能性があるのです。
C言語ではこういうことは行ないませんので、
int *i;
i=NULL;
if(i && *i==100){
という文のとき、i はヌル(偽)ですので *i==100 の評価が行われないことになります。もしFORTRANのコンパイラの様に評価の順番が変更されるとすれば、
int *i;
i=NULL;
if(*i==100 && i){
ということになってしまって、最初に *i==100 を評価しようとしたときに全然関係のないアドレスの値を参照しようとしたことになり、プログラムが異常停止してしまうことになってしまいます。この様なことはC言語としては当たり前すぎるのか意外にどの本にも書いていないようです。
ループを形成する文にFOR文、WHILE文、DO文があります。
FOR文は、
for(式1;式2;式3)文;
という形式でそれぞれの式は、
式1 FOR文を開始するときに実行する式。
式2 文を実行する前に実行する式で、真のときには文を実行し、偽のときにはループ(FOR文)を終了します。
式3 文の終わりに実行する式。
というようになっています。WHILE文は、
while(式)文;
という形式で式が真の間、文の実行を繰返します。DO文は、
do 文 while(式);
という形式で式が真の間、文の実行を繰返します。WHILE文は必ず文の実行に先立って式を評価しますが、DO文は最低一回は無条件に文を実行します。
FOR文はWHILE文に置き換えることができます。
for(式1;式2;式3)文;
は、
式1;
while(式2){
文;
式3;
}
となります。どちらの形式を選ぶかは、そのときの状況にもよりますが、単純に繰り返し実行するのであれば、FOR文の方がループの内容を明快に表現できるのでWHILE文よりはいいと思います。またFOR文は必要に応じて式を記述すればいいので、必要がなければ省略することができます。極端な場合には、
for(;;)文;
とすることもできます。この場合は無限に繰り返すという意味になります。無限に繰り返す(無限ループ)はWHILE文でも、DO文でも式に真の定数を与えれば簡単に作ることができます。
while(1)文;
も
do 文 while(1);
も無限ループになります。無限ループを作成したときのループからの脱出方法は、BREAK文か GOTO 文あるいは RETURN文のいずれかを使用するしか方法はありません。BREAK文は最も内側のループからの脱出のみを行ないます。
while(1){
while(2){
while(3){
break;
}
}
}
このようなループの場合、BREAK文は while(3){} のループからの脱出のみを行ないますので、もし while(1){} ループまでも脱出したいときには、IF文と組み合わせて各ループにBREAK文を記述します。このようなときにはGOTO文またはRETURN文を使用すると簡単に外側のループまでも脱出することができますが、色々と問題があります。
GOTO文は、
goto ラベル名;
ラベル名:
と行き先のラベル名に実際のラベル名が対応している必要があります。以前にGOTO文のお話しをしたことがありますので、そちらの方も参考にして下さい。GOTO文の最大の問題点は、1つの関数内であればどこへでもジャンプすることができるので、プログラムが読みにくくなる可能性があることです。ですので、
main()
{
goto a;
for(;;){
b:
printf("...\n");
}
a:
goto b;
}
こんな変なプログラムも書けてしまうのです。これでは後になったらプログラムの流れを理解するのに非常に時間がかかってしまいますので、使い方には注意が必要です。私の場合はループからの脱出や、共通のエラー処理等をして関数を終了するときぐらいにしか使用しないようにしています。
RETURN文は関数を終了して呼び出し側に処理を戻す文ですので、
sub()
{
while(1){
:
:
return 1;
}
}
というような関数では大変役に立ちますが、ループを終了して次の処理を行なうということができません。
GOTO文を使ってループを作ることもできますが、あまりお勧めできません。このようなやり方は、昔のFORTRANやBASICなどでやっていましたが、最近の言語はちゃんとしたループを形成する文がありますので無理をしてGOTO文を使う必要はないと思います。
a: ....
goto a;
このように記述すれば簡単にループになってしまいますが、プログラムを読んだときにどこからどこまでがループなのかが直感的に分かりづらく、先ほどのGOTO文を使った変なプログラムのようになりがちなので参考程度にして下さい。
ループを形成する文のなかでのみ使用できる文に CONTINUE文があります。この文はそれ以降の文を実行せずループを再び繰返すものです。
for(i=0;i<10;i++){
printf("a=%d\n",i);
if(i<5)continue;
printf("b=%d\n",i);
}
このようなループのときには、i が 5 未満のときには printf("b=%d\n",i); は実行しないで再びループを先頭から実行します。
最後にSWITCH文を説明しましょう。
SWITCH文は次の形式です。
switch(整数値){
case 整数定数 :
文;
case 整数定数 :
文;
:
:
default :
文;
}
SWITCH文は case 文、default 文と組になっています。使われる整数値は整数の定数でも変数でも構いませんが、実数やポインタは使用することはできません。SWITCH文は switch の整数値が整数定数と一致する case 文までジャンプしそれ以降の文をすべて実行しようとします。ですので、
i=1;
switch(i){
case 0 :
printf("0\n");
case 1 :
printf("1\n");
case 2 :
printf("2\n");
}
というときには、i が 1 ですので case 1 : の文までジャンプし、それ以降の文をすべて実行するので、
printf("1\n");
printf("2\n");
の2つの文を実行します。このようにしたいときもあるとは思いますが、殆どの場合は次の case 文以降の実行を行なわないので、BREAK文を使用してSWITCH文を終了させます。
i=1;
switch(i){
case 0 :
printf("0\n");
break;
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
}
このようにすれば、printf("1\n"); を実行した後、SWITCH文を終了します。
DEFAULT文はIF文で言えばELSE文の相当するもので、case で設定した値以外のときにジャンプし実行させるものです。ですので、
switch(i){
case 0 :
printf("0\n");
break;
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
default :
printf("else\n");
}
のときに i が 0、1、2 以外のときに default にジャンプして printf("else\n"); を実行します。
2つ以上の整数定数に対して同じ処理を行ないたいときには、
switch(i){
case 0 :
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
default :
printf("else\n");
}
のようにして記述すれば、i が 0 か 1 のときに printf("1\n"); を実行するようになります。
コンピュータのアーキテクチャやコンパイラにもよりますがSWITCH文はIF文よりも速く実行できる可能性があります。というのも(あくまでもアーキテクチャやコンパイラによりますが)、SWITCH文は整数値しか使用できませんのでその値によってどこにジャンプするかというテーブルを作成することが可能になるからです。
i=4;
if(i==0)printf("0\n");
else if(i==1)printf("1\n");
else if(i==2)printf("2\n");
else if(i==3)printf("3\n");
else printf("else\n");
というIF文があったとき、コンピュータは
i と 0 を比較
i と 1 を比較
i と 2 を比較
i と 3 を比較
という計算を必ず行いますが、
i=4;
switch(i){
case 0 : printf("0\n"); break;
case 1 : printf("1\n"); break;
case 2 : printf("2\n"); break;
case 3 : printf("3\n"); break;
default : printf("else\n");
}
のときには、i の値の評価はSWITCH文の最初に行なうだけで、後はジャンプするだけなのです。ですので、比較する値が整数で else if が延々と続くIF文のときにはSWITCH文にするとゴキゲンになれるかも知れません。
それではまた次回。
まずIF文を考えてみましょう。IF文と聞けば多分どなたでも想像が付くと思います。『もし~だったらこうして、そうでないときにはもし~だったらああして、どうしようもないときにはそうする』という文のことです。C言語ではこのIF文を次の形式で表現します。
if(式)文;
else if(式)文;
:
:
else 文;
文は1つであっても複数であっても構いません。複数の文を記述するときには、{ }の中に記述します。else if に相当するものや else に相当するものがないときには記述する必要はありません。式は以前にもお話ししたように、最終的に真か偽の値となるものであればなんでも構いません。
if(i)文; ー 変数 i の値が 0 以外のときに文を実行。
if(100)文; ー 定数 100 は真なので常に実行。
if(i==j)文; ー 変数 i と 変数 j が等しいときに実行。
ですので上の例はすべて正しいIF文になります。
if(sub())文; ー 関数 sub の戻り値が 0 以外のときに実行。
しかしながら上の例は関数 sub が void 型のときにはコンパイラに叱られてしまいますので注意して下さい。void 型とはなんにもないという意味で、この場合関数 sub は値を返さないものになります。
IF文自体はあまり注意することはありませんが、
if(i)
if(j)printf("i and j\n");
else
printf("!i\n"); ー(1)
というIF文のときに(1)の else がどのIF文に対応しているかということです。ちょっと意地悪に段下げをして記述していますので if(i) の else のいうな感じもしますが、これは if(j) に対応しているのです。このような記述のときにはC言語の約束に最も近いIF文に対応すると判断するとなっているのです。ですので、
if(i)
if(j)printf("i and j\n");
else printf("!i\n");
と段下げをするのが文の意味に合うことになります。
私の場合、1つの文であっても次の行に記述するときには必ず { } で文を囲むようにしています。この方が表現に曖昧なところがなくなるからです。上記の例を書き直すと、
if(i){
if(j)printf("i and j\n");
else printf("!i\n");
}
と書くことができ、これならば誰でも直感的に意味が理解できると思います。だからと言って同じ行に書くのならば { } を使わなくてもいいのかと言うとそうでもありません。
if(i)if(j)printf("i and j\n"); else printf("!i\n");
こんな風に書いても同じ意味になるのですが、これではコンパイラは理解できても、人間にとっては迷惑そのものです。ポインタと同様、C言語はプログラムの記述方法がかなり自由ですので、本人にも分からないプログラムになってしまいますので、プログラムのスタイルには注意をしなければなりません。
当たり前の様なことなのですが、C言語のIF文内で使用する式でORやAND演算子を使って複数の値の論理和あるいは論理積とする場合、それらの式は記述した順序で評価され、式全体の値として結論付けできるときにはそれ以上の式の評価は行ないません。例えば次のようなプログラムの場合、
int i,j;
i=1;
j=0;
if(i || j++)printf("True.\n");
printf("%d %d\n",i,j);
結果は、
True.
1 0
となります。
if(i || j++)printf("True.\n");
のIF文で、変数 i の値を評価したときに真となったので、それ以上の評価を行なわなくても式全体は真になると判断できるので j++ は実行されないのです。しばしばこの規則を忘れていて、発見しずらいバグになることもあります。逆にこの規則を利用することもあります。
もう1つ大切な規則は、式を評価する順序はコンパイラによって変更されることがないことです。当然といえば当然のことの様ですが、FORTRANのコンパイラは最適化を行なうときにこれをやってしまうことがあるのです。FORTRANは科学計算をできるだけ高速に行なうことを使命にしているので、単純に評価できる部分式を最初に評価するようになっているのです。ですので、
IF(SUB(I,J,K).OR.L.EQ.100)THEN
というFORTRANの文の場合、コンパイラが関数 SUB(I,J,K) の値の評価よりも、L.LE.100 の評価の方が簡単に行なえると判断したときには、
IF(L.EQ.100.OR.SUB(I,J,K))THEN
という内容になるように評価の順番を変える可能性があるのです。
C言語ではこういうことは行ないませんので、
int *i;
i=NULL;
if(i && *i==100){
という文のとき、i はヌル(偽)ですので *i==100 の評価が行われないことになります。もしFORTRANのコンパイラの様に評価の順番が変更されるとすれば、
int *i;
i=NULL;
if(*i==100 && i){
ということになってしまって、最初に *i==100 を評価しようとしたときに全然関係のないアドレスの値を参照しようとしたことになり、プログラムが異常停止してしまうことになってしまいます。この様なことはC言語としては当たり前すぎるのか意外にどの本にも書いていないようです。
ループを形成する文にFOR文、WHILE文、DO文があります。
FOR文は、
for(式1;式2;式3)文;
という形式でそれぞれの式は、
式1 FOR文を開始するときに実行する式。
式2 文を実行する前に実行する式で、真のときには文を実行し、偽のときにはループ(FOR文)を終了します。
式3 文の終わりに実行する式。
というようになっています。WHILE文は、
while(式)文;
という形式で式が真の間、文の実行を繰返します。DO文は、
do 文 while(式);
という形式で式が真の間、文の実行を繰返します。WHILE文は必ず文の実行に先立って式を評価しますが、DO文は最低一回は無条件に文を実行します。
FOR文はWHILE文に置き換えることができます。
for(式1;式2;式3)文;
は、
式1;
while(式2){
文;
式3;
}
となります。どちらの形式を選ぶかは、そのときの状況にもよりますが、単純に繰り返し実行するのであれば、FOR文の方がループの内容を明快に表現できるのでWHILE文よりはいいと思います。またFOR文は必要に応じて式を記述すればいいので、必要がなければ省略することができます。極端な場合には、
for(;;)文;
とすることもできます。この場合は無限に繰り返すという意味になります。無限に繰り返す(無限ループ)はWHILE文でも、DO文でも式に真の定数を与えれば簡単に作ることができます。
while(1)文;
も
do 文 while(1);
も無限ループになります。無限ループを作成したときのループからの脱出方法は、BREAK文か GOTO 文あるいは RETURN文のいずれかを使用するしか方法はありません。BREAK文は最も内側のループからの脱出のみを行ないます。
while(1){
while(2){
while(3){
break;
}
}
}
このようなループの場合、BREAK文は while(3){} のループからの脱出のみを行ないますので、もし while(1){} ループまでも脱出したいときには、IF文と組み合わせて各ループにBREAK文を記述します。このようなときにはGOTO文またはRETURN文を使用すると簡単に外側のループまでも脱出することができますが、色々と問題があります。
GOTO文は、
goto ラベル名;
ラベル名:
と行き先のラベル名に実際のラベル名が対応している必要があります。以前にGOTO文のお話しをしたことがありますので、そちらの方も参考にして下さい。GOTO文の最大の問題点は、1つの関数内であればどこへでもジャンプすることができるので、プログラムが読みにくくなる可能性があることです。ですので、
main()
{
goto a;
for(;;){
b:
printf("...\n");
}
a:
goto b;
}
こんな変なプログラムも書けてしまうのです。これでは後になったらプログラムの流れを理解するのに非常に時間がかかってしまいますので、使い方には注意が必要です。私の場合はループからの脱出や、共通のエラー処理等をして関数を終了するときぐらいにしか使用しないようにしています。
RETURN文は関数を終了して呼び出し側に処理を戻す文ですので、
sub()
{
while(1){
:
:
return 1;
}
}
というような関数では大変役に立ちますが、ループを終了して次の処理を行なうということができません。
GOTO文を使ってループを作ることもできますが、あまりお勧めできません。このようなやり方は、昔のFORTRANやBASICなどでやっていましたが、最近の言語はちゃんとしたループを形成する文がありますので無理をしてGOTO文を使う必要はないと思います。
a: ....
goto a;
このように記述すれば簡単にループになってしまいますが、プログラムを読んだときにどこからどこまでがループなのかが直感的に分かりづらく、先ほどのGOTO文を使った変なプログラムのようになりがちなので参考程度にして下さい。
ループを形成する文のなかでのみ使用できる文に CONTINUE文があります。この文はそれ以降の文を実行せずループを再び繰返すものです。
for(i=0;i<10;i++){
printf("a=%d\n",i);
if(i<5)continue;
printf("b=%d\n",i);
}
このようなループのときには、i が 5 未満のときには printf("b=%d\n",i); は実行しないで再びループを先頭から実行します。
最後にSWITCH文を説明しましょう。
SWITCH文は次の形式です。
switch(整数値){
case 整数定数 :
文;
case 整数定数 :
文;
:
:
default :
文;
}
SWITCH文は case 文、default 文と組になっています。使われる整数値は整数の定数でも変数でも構いませんが、実数やポインタは使用することはできません。SWITCH文は switch の整数値が整数定数と一致する case 文までジャンプしそれ以降の文をすべて実行しようとします。ですので、
i=1;
switch(i){
case 0 :
printf("0\n");
case 1 :
printf("1\n");
case 2 :
printf("2\n");
}
というときには、i が 1 ですので case 1 : の文までジャンプし、それ以降の文をすべて実行するので、
printf("1\n");
printf("2\n");
の2つの文を実行します。このようにしたいときもあるとは思いますが、殆どの場合は次の case 文以降の実行を行なわないので、BREAK文を使用してSWITCH文を終了させます。
i=1;
switch(i){
case 0 :
printf("0\n");
break;
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
}
このようにすれば、printf("1\n"); を実行した後、SWITCH文を終了します。
DEFAULT文はIF文で言えばELSE文の相当するもので、case で設定した値以外のときにジャンプし実行させるものです。ですので、
switch(i){
case 0 :
printf("0\n");
break;
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
default :
printf("else\n");
}
のときに i が 0、1、2 以外のときに default にジャンプして printf("else\n"); を実行します。
2つ以上の整数定数に対して同じ処理を行ないたいときには、
switch(i){
case 0 :
case 1 :
printf("1\n");
break;
case 2 :
printf("2\n");
break;
default :
printf("else\n");
}
のようにして記述すれば、i が 0 か 1 のときに printf("1\n"); を実行するようになります。
コンピュータのアーキテクチャやコンパイラにもよりますがSWITCH文はIF文よりも速く実行できる可能性があります。というのも(あくまでもアーキテクチャやコンパイラによりますが)、SWITCH文は整数値しか使用できませんのでその値によってどこにジャンプするかというテーブルを作成することが可能になるからです。
i=4;
if(i==0)printf("0\n");
else if(i==1)printf("1\n");
else if(i==2)printf("2\n");
else if(i==3)printf("3\n");
else printf("else\n");
というIF文があったとき、コンピュータは
i と 0 を比較
i と 1 を比較
i と 2 を比較
i と 3 を比較
という計算を必ず行いますが、
i=4;
switch(i){
case 0 : printf("0\n"); break;
case 1 : printf("1\n"); break;
case 2 : printf("2\n"); break;
case 3 : printf("3\n"); break;
default : printf("else\n");
}
のときには、i の値の評価はSWITCH文の最初に行なうだけで、後はジャンプするだけなのです。ですので、比較する値が整数で else if が延々と続くIF文のときにはSWITCH文にするとゴキゲンになれるかも知れません。
それではまた次回。