do while文 ループ構文3

do while文

do while文while文とよく似たループ文です。

以下のコードは、while文を用いてキー入力をチェックし、「y」キーが押下されるまでループするプログラムのサンプルです。

#include <stdio.h>

int main()
{
    int moji = 0;

    while (moji != 'y')
    {
        printf("プログラムを終了しますか?\n");
        printf("y=終了 n=キャンセル\n");

        if((moji = getchar()) != '\n')
            while (getchar() != '\n');
    }
    printf("キー入力で終了。\n");
    getchar();
}

while文は、一番最初に変数mojiのチェックを行います。
このコードでは、一回目のチェックでは変数mojiの値は0であることはわかりきっているため、このチェックは無駄な処理となります。

また、条件判定を最初に行うため、何らかの処理により変数mojiの値が書き換えられていると、while文のブロックが一度も実行されずにスルーされてしまうこともあります。

#include <stdio.h>

int main()
{
    int moji = 0;

    //何らかの処理
    {
        //...
        moji = 'y';
        //...
    }
    
    while (moji != 'y') //このwhileブロックは実行されない!
    {
        printf("プログラムを終了しますか?\n");
        printf("y=終了 n=キャンセル\n");

        if((moji = getchar()) != '\n')
            while (getchar() != '\n');
    }
    printf("キー入力で終了。\n");
    getchar();
}

もちろん条件判定用の変数の中身を適切に管理すればこのような事態は防げますが、do while文を用いれば確実にループブロックを実行できます。
これをdo while文で書き直すと以下のようになります。

#include <stdio.h>

int main()
{
    int moji = 'y';

    do
    {
        printf("プログラムを終了しますか?\n");
        printf("y=終了 n=キャンセル\n");

        if((moji = getchar()) != '\n')
            while (getchar() != '\n');
    } while (moji != 'y');
    printf("キー入力で終了。\n");
    getchar();
}
do{ 文 }while(条件式)
文を実行し、条件式が真の間、文を繰り返し実行する

do while文は、ループ終了の条件判定が最後に行われます。
条件式に関係なく、ループブロック内の処理は必ず一度は実行される点がwhile文とは異なります。

サンプルコードでは、あえて変数mojiの中身を「y」で初期化しています。
しかし、変数mojiの値をチェックするのはdoブロックの後ですから、変数mojiの値に関わらずdoブロックは確実に一度は実行されます。
doブロック内では変数mojiの値はgetchar関数によりキーボードからの入力値に置き換えられていますので、このサンプルコードは問題なく動作します。

改行のみの入力時の処理の改善

while文のループ文とgetchar関数の項と同じく、今回のサンプルコードも標準入力に保存されている文字のクリア処理を行っています。
基本的にwhile文の際に紹介した方法と同じですが、少しだけ変更しています。

while文の時に使用したクリア処理は以下のようなものです。
(関係のない部分は省略しています)

int moji = getchar();
while (getchar() != '\n');

これでも大きな問題はないのですが、何も文字入力をせずに改行だけ入力した場合に、少しだけ意図しない動作となります。

キー入力で改行のみが入力されると、変数mojiに改行文字が代入され、標準入力には何もデータが残っていない状態となります。
その状態で次のクリア処理が実行されると、標準入力は空なので本来は意図しないキーボードからの入力待ちが発生してしまいます。

この入力待ちでは何を入力しても、その入力データは結局すべて読み捨てられるのでプログラム自体に支障はありません。
しかし意図しない動作はやはり修正するのが望ましいです。

この問題を修正したのが以下のコードです。

if((moji = getchar()) != '\n')
    while (getchar() != '\n');

最初のif文の条件式では、標準入力から読み取った値を変数mojiに格納しています。
代入式を評価すると代入された値が得られますから、その値と「\n」とを比較して、一致しない場合にif文を実行します。

逆に言えば、最初のgetchar関数の実行の段階で「\n」が入力された場合は次のwhile文は実行されません。
これにより、不要なキー入力待ちが発生しないようになります。

最初の入力が「\n」でなかった場合はwhile文によって「\n」が登場するまで標準入力のデータを読み捨てます。

ちなみに、この処理を以下のように書くと上手く動作しないので注意してください。

if(moji = getchar() != '\n')
    while (getchar() != '\n');

1行目、「moji = getchar()」を囲っていた丸括弧がなくなっています。
そうすると比較演算子「!=」による比較が先に行われてしまいます。
その比較の結果が変数mojiに代入されてしまい、正しい値がmojiに代入されなくなるのです。

演算子の優先順位については演算子と予約語の一覧を参考にしてください。

EOFとfeof関数

以下は余談的な内容です。

getchar関数は入力ストリーム上のデータがなくなるとEOFを返す仕様になっています。
EOFは「End Of File」の略で、ファイルの終端を意味する定数です。

普通に使用する範疇ではgetchar関数がEOFを返すことはありません。
getchar関数が入力の終端の改行文字を読み取り、次にgetchar関数を実行するとEOFを取得せずにキーの入力待ちになるからです。

実はWindowsのコマンドプロンプトは「Ctrl + z」キーでEOFを入力することができます。
これを入力した場合、改行文字は入力されず終端はEOFになります。
上記の入力読み捨てはこのことを考慮していないため、EOFが入力されると少し期待に反する動作となります。

別に致命的な問題とはならないのでEOFのことは考慮しなくても良いのですが、完璧に対応する場合はfoef関数を使用します。
feof関数は引数に指定したストリームがEOFを返す状態ならば0以外を返します。
EOF以外ならば0を返します。
feof関数の引数にstdinを指定することで標準入力の状態をチェックできます。

#include <stdio.h>

int main()
{
	int moji, eof;

	while (1)
	{
		printf("プログラムを終了しますか?\n");
		printf("y=終了 n=キャンセル\n");

		//eofとmojiには同じ内容が入力される
		eof = moji = getchar();
		while (eof != '\n')
		{
			if (feof(stdin) != 0)
			{
				clearerr(stdin);
				break;
			}
			eof = getchar();
		}
		if (moji == 'y')
			break;
	}

	printf("キー入力で終了。\n");
	getchar();
}
プログラムを終了しますか?
y=終了 n=キャンセル
^Z
プログラムを終了しますか?
y=終了 n=キャンセル
y
キー入力で終了。

「^Z」が「Ctrl + z」の入力を意味します。

feof関数がEOFを検出した場合にループを抜けるようにしています。
feof関数は一度EOFを検出すると、自動では元の状態に戻りません。
そのためclearerr関数で状態をクリアしておきます。

これでも動作しますが、当方の環境では通常の文字入力に続いて「Ctrl+z」を入力するとfeof関数がEOFを検出しませんでした。
そのため、不要なキー入力待ちが発生してしまいます。

プログラムを終了しますか?
y=終了 n=キャンセル
a^Z

プログラムを終了しますか?
y=終了 n=キャンセル
y
キー入力で終了。

「a」と「Ctrl + z(^Z)」を入力した次の空行が不要なキー入力待ちです。

これに対応したのが以下のコードです。

#include <stdio.h>

int main()
{
	int moji, eof;

	while (1)
	{
		printf("プログラムを終了しますか?\n");
		printf("y=終了 n=キャンセル\n");

		eof = moji = getchar();
		while (eof != '\n')
		{
			if (feof(stdin) != 0)
			{
				clearerr(stdin);
				break;
			}
			if (eof == 26)
				break;
			eof = getchar();
		}
		if (moji == 'y')
			break;
	}

	printf("キー入力で終了。\n");
	getchar();
}
プログラムを終了しますか?
y=終了 n=キャンセル
^Z
プログラムを終了しますか?
y=終了 n=キャンセル
a^Z
プログラムを終了しますか?
y=終了 n=キャンセル
y
キー入力で終了。

feof関数によるチェックに続いて変数eofと「26」とを比較し、真ならばループを終了しています。
26はEOFを表す制御文字です。
これで期待通りの動作となります。

ここまで厳密にチェックする意味はほとんどありませんし、そもそもgetchar関数だけでなんとかしようとするのが間違っているかもしれません。

なお、Linux(CentOS)環境でのテストでは上記のような問題はなく、改行文字との比較だけで問題なく動作しました。