入門書でのおさらい10・do文、4-1変数の宣言の場所、演習4-1,4-2

今日は74ページのdo文から勉強したいと思います。

74ページ・do文

了解しました!今日は「do-while文」からですね。

forwhileとの最大の違いは、「条件に関わらず、必ず最初に1回は実行される」という点です。

do-while文の基本形
do {
    // 繰り返したい処理
} while (条件式);

※ 最後の ;(セミコロン) を忘れやすいので注意してください。

特徴:後判定(あとはんてい)
  • while文: 最初に条件をチェック。条件が偽なら一度も実行しない。
  • do-while文: 処理をしてから条件をチェック。最低1回は必ず動く。

whileの()の中はifと同じで0か1のみを判定するものなの?

厳密には「0か、それ以外(0以外すべて)か」で判定しています!

C言語には「真(True)」と「偽(False)」を専用に表す型が標準ではないため、数値で代用しているんです。

あ、そうだった。0かそれ以外かだね。よく間違えちゃう!ifと同じ?

はい、まったく同じです!

C言語において、if も while も do-while も、条件式の判断基準はすべて共通しています。

共通のルール
  • 「0」 であれば 偽(False):処理を行わない / ループを抜ける
  • 「0 以外」 であれば 真(True):処理を行う / ループを継続する

ですから、例えば if (1) と書けば必ず中の処理が実行されますし、while (1) と書けば(意図的な)無限ループになります。

豆知識

最近のC言語(C99規格以降)では、bool型(true / false)を使える仕組みもありますが、その裏側でも結局は 1(true) と 0(false) として処理されています。

だけど等価演算子「==」や論理演算子「&&」「||」が返す値は0か1なんだよね?

その通り、大正解です!

「判定」と「返り値」を整理すると、以下のようになります。

  1. 判定(受け取り)0 か 0以外 かで判断する。
  2. 演算結果(出力)== や && などの演算子が「真」として出す値は、必ず 1 になる。演算子が「偽」のときに返す値は、必ず 0 です。

だからifやwhileの中も0か1で判別してるのかなって勘違いしちゃうんだよ。初心者あるあるかな

まさに「初心者あるある」の筆頭ですね!教科書には「正しいときは1、間違いなら0」と書かれていることが多いので、そう思うのは当然です。

「0か1か」ではなく、「0(OFF)か、それ以外(ON)か」というイメージにアップデートしておくと、C言語のマスターに一歩近づきます。

例題4-1の変数の宣言の場所について

複合文の中だけで使う変数は複合文の中で宣言しないといけないのはどうして?

「複合文(ブロック)の中でしか使わないなら、その中で宣言するのが現代のC言語では推奨」ですが、「必ずしも外で宣言してはいけないわけではない」というルールです。

このコードの int no; について、ポイントを3つに整理しますね。

1. ブロック内で宣言するメリット(noの場合)

no を do { ... } の中で宣言すると、その変数の寿命(有効範囲)はブロックの中だけになります。

  • 間違いが減る: ブロックの外で間違えて no を使ってしまうミスを防げます。
  • メモリの節約: ブロックを抜けると消えるため、効率が良いです。
2. 外で宣言しなければならないケース(retryの場合)

逆に、retry は外で宣言されていますよね。これは外に出さないと動かないからです。

  • while(retry == 0) の判定はブロックので行われます。
  • もし retry を do の中で宣言してしまうと、最後の while の時には「その変数はもう消えていて見つからないよ!」とエラーになってしまいます。
3. C言語の歴史的な話

実は、昔のC言語(古い規格)では「変数は必ず関数の最初(int main のすぐ下など)で全部宣言しなさい」というルールがありました。
しかし、今の規格では「使う場所の近くで宣言する」のが一般的で、読みやすいコードとされています。

たしかにint no; をdoの{}の外で宣言してみたけどエラーにならずに動いたよ。

実験ありがとうございます!その通り、外で宣言してもエラーにはなりません。

C言語のルールとしては、「使う場所よりも上(前)で宣言されていればOK」だからです。

逆に{}の中で宣言した変数は{}の外では使えないの?

その通りです!{ } の中で生まれた変数は、その { } を抜けると消滅する」というのがC言語の鉄の掟です。

これを専門用語で 「変数のスコープ(生存範囲)」 と呼びます。

このルールのメリット

もし「中で作った変数が外でもずっと生き残る」というルールだったら、プログラムが巨大になったときに、「今どの変数が生きていて、どこで値が変わったのか」を管理するのが地獄になってしまいます。

「この中だけで使うから、外のことは気にしないでね」という合図になるので、大人数で開発するときも安心なんです。

例外的な存在「retry」

前のアドバイスで「retry は外で宣言しないとダメ」と言ったのは、まさにこれがあるからです。

  • while(retry == 0); の判定部分は、実は do のブロック { } の外側 という扱いになります。
  • だから、retry だけは外に置いておかないと、最後の判定で使えないんですね。

へーそうなんだね。ちなみにif文やなんかも{}で囲まれてるものなら全部共通のルール?

はい、すべて共通のルールです!

if 文でも for 文でも while 文でも、{ }(ブロック) が出てきたら、その中は「独立した小さな世界」だと思って間違いありません。

共通ルールのポイント
  • 中で宣言したものは外で使えない:
    if の中で作った変数は、else に行った瞬間に消滅します。
  • 外で宣言したものは中で使える:
    main 関数の最初の方で宣言した変数は、その後の if や while の中でも自由に使えます。
例えばこんな時も「おこられる」
if (score > 80) {
    int bonus = 10; // ここで誕生
} else {
    int bonus = 0;  // ここで誕生(上のbonusとは別物!)
}

printf("%d", bonus); // エラー! ifやelseを抜けた瞬間にbonusは消滅している
なぜこのルールが徹底されているの?

もし { } を抜けても変数が生き残っていたら、あとで似たような名前の変数を使いたいときに「もうその名前は使われてるよ!」と衝突して、プログラムが書きにくくなってしまうからです。

「使い終わったら片付ける」という、プログラミング界の整理整頓ルールなんですね。

ド・モルガンの法則

説明を読んでも全く分からなかったよ。イメージできない。

「各条件の否定を取って、論理積・論理和を入れかえた式」の否定が元の条件と同じになる
x && y と !(!x || !y)は等しい
x || y と !(!x && !y)は等しい

出典:柴田望洋『新・明解C言語 入門編』77ページ

図を使って上の「x && y と !(!x || !y)は等しい」が本当か見てみたよ。

x && y は「xかつy」

だからxとyの重なったところ(以下の範囲)が「x && y」である。

!(!x || !y)

!(!x || !y)の括弧の中からひとつづつ見ていくとまず「x!」はxでない以下の部分

「y!」はyでない以下の部分

(!x || !y)は、xでない部分またはyでない部分だから重ねてみると

↓↓↓↓↓つまり下の画像の着色された範囲。

最後に全体を否定するのでこの部分以外の部分というと、白の部分になります。
つまり「x && y」と同じ範囲になります。

このように図で視覚化するとかなり分かりやすくなりました!

演習4-1

これは簡単にできました!4-3のとほぼ一緒だったので。先生にみてもらってOKも出ました!

#include<stdio.h>
int main(void){

    int retry;

    do{
        int no;
        printf("整数値を入力せよ:"); scanf("%d",&no);

        if(no == 0)
                puts("その数は0です");
            else if(no > 0)
                puts("その数は正です");
            else
                puts("その数は負です");
    

        printf("まだ??<はい・・0 / いいえ・・9>:");
        scanf("%d",&retry);

    }while(retry == 0);

    return 0;
    
}

バッチリですね!完璧な正解です。

演習4-2

お、これは少し難しそうです。まずはノーヒントでプログラムを組んでみたいと思います。

な、なんとか絞り出した・・・しかしなんかおかしい。問題はdo whileの間の式ってわかってるんだけど・・・。実はさわからんすぎて等差数列の和の公式ググってしまいました

1/2×(個数)×(最初 +最後)

#include<stdio.h>
int main(void){
    
    int a, b;
    int min, max;
    int sum = 0;
    int cnt = 0;

    puts("整数値を2つ入力せよ:");
    printf("整数1:"); scanf("%d",&a);
    printf("整数2:"); scanf("%d",&b);

    if(a > b){
        min = b;
        max = a;
    }else{
        min = a;
        max = b;
    }

    do{
        sum = 0.5 * (max - min + 1) * (min + max);//等差数列の公式
        cnt = cnt + 1;
    }while(cnt == max - min);
        
    printf("%d以上%d以下の整数の和は%dです\n",min, max, sum);

    return 0;
    

コードを見せていただきありがとうございます!

「等差数列の和の公式」をプログラムに落とし込みましたね。
minmax をしっかり判定して入れ替えているので、どちらの数字を先に入力しても正しく動くようになっています。

実は、このプログラムには「公式を使う場合」「繰り返しを使う場合」の考え方が混ざっている面白い状態になっています。

そこで1点だけ、ご自身のコードで実験してみてほしいことがあります。

今のコードの do-while の部分、実はdo { ... } while(0);(1回だけ実行して終わる)」と同じ動きになっているのですが、気づきましたか?

このプログラムをさらに良くするために、どちらの方向に進めてみたいですか?

  1. 公式を極める: 繰り返し(do-while)を使わずに、1行の計算式でバシッと答えを出す。
  2. 繰り返しの練習にする: 公式を使わずに、min から max まで1つずつ足していく処理に書き換えてみる。

どちらに挑戦してみますか?公式の中身についても、実はC言語特有の「型」の落とし穴が隠れているので、そこも語り合いたいポイントです!

ああなるほど公式を使うとループを使わなくても答えが出てしまうんだ・・じゃあダメだ。いまはdo whileのループの練習だからこれはだめ!もう一度考え直すね。あとwhileは0以外のときループだったね。また間違えてたよ

minとmax出すまではよい?そこも出さない?

min と max を出すところまでは、完璧です!

あそこで大きい方と小さい方をしっかり仕分けたおかげで、その後の計算や処理がすごく楽になります。プログラミングにおいて、最初に「データを整理整頓する」のは最高の習慣です。

ただ、今のコードで公式を使わずに「繰り返しの練習」として解くなら、その下の do-while の中身を少し工夫する必要があります。

例として3から7で考えると、

3 (= min)4567(=max)
min+0min+1min+2min+3min+4
足す回数:01234

この表から考えると足す回数は4回。「7-3」つまり「max-min」になる。
足す回数をcntにいれてループ一回ごとに1づつ増やして、「max-min」になったらループを終了させればいい。

だからwhileの()の中に入れるのは回数が「max-min」回以下の場合はループ、それを超えたら偽になって終了のように書けばよい。
while(cnt <= max – min);

つぎに、3,4,5,6,7はminをつかって書き直すと
min+0,min+1,min+2,min+3,min+4と書ける。

ここでminにプラスしてる数字はcntと一致することにきづいたので
min+cntとかける。sumという合計を入れる変数を作って、ループさせる。
sum = sum +min +cnt;
この式は1回目はプログラムの上の方で
sum = 0; cnt = 0;で初期化してあるので
1回目:sum = 0 +min + 0 = min
2回目:sum = min + (min +1)
3回目:sum = min + (min +1) + (min+2)
4回目:sum = min + (min +1) + (min+2) + (min + 3)となる

以上の考えをプログラムにして修正してみました。公式は使ってません

#include<stdio.h>
int main(void){
    
    int a, b;
    int min, max;
    int sum = 0;
    int cnt = 0;

    puts("整数値を2つ入力せよ:");
    printf("整数1:"); scanf("%d",&a);
    printf("整数2:"); scanf("%d",&b);

    if(a > b){
        min = b;
        max = a;
    }else{
        min = a;
        max = b;
    }

    do{
    
        sum = sum + min + cnt;
        cnt = cnt + 1;
    }while(cnt <= max- min);
        
    printf("%d以上%d以下の整数の和は%dです\n",min, max, sum);

    return 0;
    
}

おめでとうございます!大正解です!!

実際に「1と3」や「5と10」などでシミュレーションしてみても、完璧に計算が合うようになっていますね。

今回のポイント
  1. sum = sum + min + cnt; の発想
    「ベースとなる最小値(min)」に、「増えていく分(cnt)」を足して、それを合計していく……非常に論理的で、プログラミングらしい解き方です。
  2. while(cnt <= max - min) の境界判定
    ここが一番の難所ですが、<= を使って「個数」の分だけきっちりループさせる条件を見事に導き出しましたね。

自分で考えて、試行錯誤して辿り着いた答えは、公式をググって書いた時よりも何倍も身につきますよ。

この演習のジェミニ先生のよりスマートなプログラム

この「cnt」を使わずにもっとシンプルに書く方法(例えば min 自体を増やしていく方法など)に興味がありますか?

はいお願いします

「min 自体を増やしていく」という発想を使うと、用意する変数が少なくなって、コードがさらに直感的になります。

ヒントなしで頑張った後のご褒美として、その「もっとシンプルな書き方」を紹介しますね。

min 自体を増やして足していく方法
#include <stdio.h>

int main(void) {
    int a, b, min, max;
    int sum = 0;

    puts("整数値を2つ入力せよ:");
    printf("整数1:"); scanf("%d", &a);
    printf("整数2:"); scanf("%d", &b);

    if (a > b) { min = b; max = a; }
    else { min = a; max = b; }

    do {
        sum += min;  // 今の min を合計に足す
        min++;       // min を 1 増やす(3→4→5...と育てる)
    } while (min <= max); // min が max を超えるまで続ける

    printf("整数の和は %d です\n", sum);

    return 0;
}
どこがシンプルになったのか?
  1. 変数が減った!
    cnt という「おまけの変数」を使わず、min そのものをカウントアップさせているので、頭の中で追いかける数字が減ります。
  2. 式が自然!
    sum = sum + min + cnt という複雑な計算ではなく、ただの sum += min(今の数字を足すだけ)になりました。
  3. 条件式が直感的!
    while (min <= max) = 「小さい方の数が、大きい方の数に追いつくまで」という、日本語として自然な条件になります。

あなた天才ね。そうかminを増やせばいいだけだったんだ。

豆知識:使い分け
  • あなたの書いた方法:元の min の値をとっておけるので、最後に「3から7までの和は〜」と表示したい時に便利です。
  • この新しい方法:計算が終わったときには min の中身が書き換わってしまっていますが、計算ロジック自体はスッキリします。

「変数を育てる」という感覚、これからのループ学習でよく使うので、ぜひ引き出しに入れておいてください!

今回は表示を「28以上37以下の全整数の和は325です。」のようになるようにしないといけなかったんだ。それを踏まえてどう描けますか?

なるほど、表示に元の「min」と「max」の値が必要ということですね!

minを直接増やしていく方法」だと、計算が終わった時にminの中身がmaxを超えた数値に書き換わってしまうので、そのままでは最初の数字を表示できなくなります。

これを解決するには、「表示用の変数」を別に残しておくのが一番スマートです。

表示用の値をキープする書き方
#include <stdio.h>

int main(void) {
    int a, b, min, max, start, end; // start, end を追加
    int sum = 0;

    puts("整数値を2つ入力せよ:");
    printf("整数1:"); scanf("%d", &a);
    printf("整数2:"); scanf("%d", &b);

    if (a > b) { min = b; max = a; }
    else { min = a; max = b; }

    // ★ 最初の値をコピーしてとっておく
    start = min;
    end = max;

    do {
        sum += min;
        min++;
    } while (min <= max);

    // ★ とっておいた start と end を使って表示
    printf("%d以上%d以下の全整数の和は%dです。\n", start, end, sum);

    return 0;
}
この書き方のポイント
  1. 値を「避難」させるmin は計算(ループ)のためにどんどん変化させますが、最初に start にコピーしておくことで、最後の printf で「37」や「28」を正しく表示できます。
  2. 役割分担: 「計算で使い潰す変数(min)」と「記録しておく変数(start)」を分けるのは、プログラミングでよく使うテクニックです。

今日は疲れたので続きはまた明日。明日はwhile文です