入門書でのおさらい7・複合文、論理演算子

今日は60ページの複合文から勉強していきたいと思います。以前に先走って{}の使い方を先生に聞いたことありますが今日改めて勉強して納得がいきました。

60ページ・複合文

今日初めてif else分の複合文(ブロック)という考えを解説書で勉強しました

「ブロック(複合文)」の理解、おめでとうございます!これはC言語の構造を理解する上でめちゃくちゃ重要なステップです。

今までは if の後に1行だけ書いていたかもしれませんが、{ }(中カッコ)で囲むことで、複数の命令を「ひとまとめのグループ」として扱えるようになった、ということですね。

「if」も「else」も制御できる分は一つだけという決まりなんですね?

その通りです!まさにそこが、プログラミングにおける最重要ルールの一つです。

if や else が直接コントロールできるのは、直後の「たった一つの文」だけです。

もともとの決まりがそれで、じゃあ2つの文や宣言(;の数ともいえる)をifやelseで制御したいときに{}をでくくると{}内は一つの文とみなす。だからifやelseが読めるようになるといった説明のほうが正しいですか??

その解釈、100点満点の正解です!

まさにその通りで、「if文は一つの文しか支配できない」という頑固なルールを、「{} を使って、複数の文を一つの巨大な文(複合文)にパッケージングする」ことで突破しているんです。

  1. 「;」の数に注目したこと:
    C言語では「;」が文の終わりを示すので、;が2つあればそれは2つの文です。それを {} で囲むことで、外側からは一つの塊に見えるようになります。
  2. 宣言も制御できる:
    おっしゃる通り、ブロック {} の中であれば、その中だけで使う変数の「宣言」も書けます。これも {} が一つの独立した「世界(文の塊)」を作っている証拠です。

この「{} は複数の文を一つの文として扱う魔法の箱」という考え方は、この先学習する while や for(繰り返し処理)でも全く同じように使います。

解説書の著者はわざと最初のほうは{}の説明をしていなかったのですね。そこにちょっとうん十年前にかじった知識が邪魔をしてなんで{}がついてないんだ!?ってこんがらがってしまったというわけです。すみません。

いえいえ、謝ることなんて全くありません!むしろ、その「違和感」こそが深い理解への入り口でしたね。
入門書が最初に {} を省くのは、おそらく「まずは最小限の見た目で、条件分岐の考え方に慣れてほしい」という著者なりの配慮(あるいは罠?)だったのでしょう。

しかし、以前少しでも触れたことがある方からすれば、「if といえば {} がセットのはずでは?」と混乱するのは当然の反応です。むしろ、そこで「なんとなく」流さずに、「なぜ {} がなくても動くのか」「{} の本当の役割は何なのか」という理屈を突き止めたのは、プログラミング学習において理想的な姿勢ですよ。

今日の学びで、以下のことがスッキリつながったはずです:

  1. C言語の厳格なルールif が支配できるのは直後の「1文」だけ。
  2. {} の魔法: 複数の文を「1つの複合文」にパッキングする道具。
  3. 初心者の混乱: 1行だけの時は {} が省略可能(でも現場では「読みやすさ」と「ミス防止」のために1行でも {} を書くのが一般的です)。

「うん十年前」の知識が、現代の学習とガッチリ結びついた瞬間ですね。これでもう、{} があってもなくても、迷うことはありません!

はい、謎が解けたようですっきりしました!

62ページ・論理演算子

論理演算子を解説本で勉強しました!

論理演算子への突入、おめでとうございます!条件演算子の「2択」から、さらに複雑な「条件の組み合わせ」ができるようになりましたね。

C言語の論理演算子といえば、この3つが主役です:

  1. &&(論理積 / AND): 「かつ」
    • a > 0 && a < 10 (aが0より大きく、かつ10より小さい)
  2. ||(論理和 / OR): 「または」
    • a == 1 || a == 10 (aが1、または10である)
  3. !(論理否定 / NOT): 「〜ではない」
    • !(a == 0) (aが0ではない
ここが「面白い&注意」ポイント!

勉強された中で、特にこのあたりが気になりませんでしたか?

  • 「0かそれ以外か」: C言語には厳密な「Boolean型(True/False)」が本来なく、0を「偽」0以外(主に1)を「真」として扱うという点。
  • 「短絡評価(ショートサーキット)」: 例えば A && B で、もしAが「偽」だったら、Bの計算はスルーされるという合理的な(時にトリッキーな)仕組み。

先ほどの「条件演算子」や「if文のブロック」と組み合わせると、一気にプログラムに「知能」を持たせられるようになります。

論理演算子は何個つなげてもいいの?または、または、または・・・10こぐらい。みたいに

結論から言うと、何個つなげても文法上はOKです!「または(||)」を10個つなげても、100個つなげてもコンピューターは文句を言わずに処理してくれます。

ただし、組み込みの世界やプロの現場では、以下のポイントを意識します。

読みやすさの限界(可読性)

条件が5つも6つも並ぶと、人間は一瞬で理解できなくなります。「10個つなげたい」と思ったときは、「もっと楽な判定方法(先ほどの計算トリックや配列など)があるかも?」というサインです。

「短絡評価」で賢く動く

C言語の論理演算子はとても効率的です。

  • A || B || C ... と並んでいるとき、もし Aが「真」だったら、残りのBやCのチェックは一切せずに「はい、真!」と即決します。
  • これを「短絡評価(ショートサーキット)」と呼びます。重い計算を伴う条件は左側に置かない、といった工夫で速度を稼げます。

例題3-18

早速例題3-18をやってみましたが、大間違いをしてしまいました。プログラムができたので条件分岐の部分を先生にみてもらいました

    if(month >= 3 && month <= 5){
        printf("%d月は春です\n",month);
    }else if(month >= 6 && month <= 8){
        printf("%d月は夏です\n",month);
    }else if(month >= 9 && month <= 11){
        printf("%d月は秋です\n",month);
    }else if(month >= 12 && month <= 2){
        printf("%d月は冬です\n",month);
    }else{
        puts("1~12を入力してください");
    }

季節判定プログラム、完成しましたね!論理演算子 && を使って、範囲をバッチリ指定できています。

ただ、実際に実行してみると、「冬」の判定(1月・2月・12月)だけが、意図通りに動かないかもしれません。ここが論理演算子の面白い(そして少しややこしい)ところです!

うわ、ほんとだ。挙動がおかしい。自信満々だったのに

冬の条件をチェックしてみましょう

C言語の &&(かつ)は、「両方の条件を同時に満たす」必要があります。

  • month >= 12 && month <= 2
    「12以上」かつ「2以下」という数字は存在しないため、この条件は常に「偽」になってしまいます。

やられたーーー!12月の次は1月で数字が小さくなっちゃうんだ。すっかり抜けてました

カレンダーでは当たり前に12月の次は1月ですが、コンピュータにとっては「12」の次は「13」であって、「1」に戻るという概念がないんですよね。

この「数値の連続性が途切れる」パターンは、プログラミングでは非常によくある落とし穴です。

どう直すと良い?

冬を正しく判定するには、||(または) を使うのがコツです。

if (month == 12 || month == 1 || month == 2) {
    printf("%d月は冬です\n", month);
}

あるいは、他の月(3〜11月)を全部はじいた後の「最後の砦」として、else でまとめてしまうのも一つの手です。

よっしゃ、ちょっとその2パターンでなおしてみます

    if(month >= 3 && month <= 5){
        printf("%d月は春です\n",month);
    }else if(month >= 6 && month <= 8){
        printf("%d月は夏です\n",month);
    }else if(month >= 9 && month <= 11){
        printf("%d月は秋です\n",month);
    }else if(month == 12 || month == 1 || month == 2){
        printf("%d月は冬です\n",month);
    }else{
        puts("1~12を入力してください");
    }

素晴らしいです!|| (OR)演算子を使いこなして、「冬」の不規則な並びを真っ向から攻略しましたね。

このパターンの良いところ
  • 直感的でわかりやすい: 「12または1または2」という条件がそのままコードになっているので、誰が読んでも一瞬で「冬の判定だ」とわかります。
  • 構造がフラット: 先ほどの「入れ子(ネスト)」構造に比べて、else if が縦に並んでいるので、上から順に条件をチェックしていく流れが非常にシンプルです
if(month>=1 && month <= 12){
    if(month >= 3 && month <= 5){
        printf("%d月は春です\n",month);
    }else if(month >= 6 && month <= 8){
        printf("%d月は夏です\n",month);
    }else if(month >= 9 && month <= 11){
        printf("%d月は秋です\n",month);
    }else{
        printf("%d月は冬です\n",month);
    }
}else{
    puts("1~12を入力してください");
}

お見事!これは非常に「賢い」修正方法ですね。

入れ子(ネスト)」の構造をうまく使って、二段構えで問題を解決しています。

このコードの素晴らしい点
  1. 「冬」の問題を鮮やかに解決:
    一番外側の if で「1〜12月であること」を保証しているので、春・夏・秋(3〜11月)以外の残りは、自動的に「12, 1, 2」だけになります。そこを else で受けることで、複雑な条件を書かずに100%確実に冬を判定できています。
  2. エラー処理が完璧:
    最初に範囲外(13月や0月など)を弾いているので、プログラムとしての信頼性がぐっと上がりました。
  3. 可読性が高い:
    「まず正しい月かどうかをチェックし、その中で季節を分ける」という人間の思考プロセスに近いので、後から見ても非常に分かりやすいです。

ちなみに先生が言った「最後の砦」として、else でまとめてしまうプログラムはどんなものですか?

「最後の砦」というのは、「それまでの条件(春・夏・秋)に当てはまらなかったものは、消去法で全部これだ!」と処理する書き方です。

あなたの「入れ子(ネスト)」案の後半部分が、まさにその形になっています。

if (month >= 3 && month <= 5) {
    printf("%d月は春です\n", month);
} else if (month >= 6 && month <= 8) {
    printf("%d月は夏です\n", month);
} else if (month >= 9 && month <= 11) {
    printf("%d月は秋です\n", month);
} else {
    // 春でも夏でも秋でもないなら、冬に違いない!
    printf("%d月は冬です\n", month);
}

おまけ:論理演算子は使わない組み方。メモリを少なく

さっきちらっと言ってた「もっと楽に冬を判定するズルい方法」に興味があります

お、その「ズルさ」への興味、エンジニアとして最高ですね!
楽をするための工夫こそが、プログラミングを進化させてきました。

「冬」を判定するときのズルい(でも数学的にスマートな)方法は、「12月を0月とみなして、3で割る」という計算トリックです。

3で割った商を使う

C言語の割り算(整数同士)は、あまりを切り捨てて「商」だけを出す性質があります。これを利用します。

monthを3で割ってみると…
3, 4, 5月 → 3で割ると商は 1 (春)
6, 7, 8月 → 3で割ると商は 2 (夏)
9, 10, 11月 → 3で割ると商は 3 (秋)
12, 1, 2月 → !?(ここが不規則)

ここでズルをします。12月を 0 に変換して考えてみるんです。
(month % 12) を使うと、12月は 0 になります。
0, 1, 2月 → 3で割ると商は 0 !!(冬)

(month % 12)の答えは1月のとき1,2月の時2、3月の時3、…12月の時0のように11月までは入力した月と一致しますが、12月だけ0になる。それを使うんですね。

その通りです!「12だけを0に弾き飛ばして、他の数字はそのまま通す」という、% 12(12で割った余り)ならではの魔法ですね。

これを使うと、バラバラだった「12, 1, 2」というグループが 「0, 1, 2」という0から始まる綺麗な連番に生まれ変わります。

これを使うと、if-else を繰り返さなくても一撃で判定できます。

char *seasons[] = {"冬", "春", "夏", "秋"};
// 3で割った答え(0〜3)を、そのまま配列の番号に使っちゃう!
printf("%d月は%sです\n", month, seasons[(month % 12) / 3]);

さすがにここまでのズルは、初心者向けの解説書には載っていません。でも、「規則性を見つけて計算で解く」という考え方は、後々アルゴリズムを学ぶ時にとても役立ちます。

これは季節のわり方が2,3,4、5,6,7、8,9,10、11,12,1とかなった場合も使えますか

はい、使えます! 数字がズレても、「月にある数字を足したり引いたりして、グループの先頭を 0 に合わせる」という調整(オフセット)をすれば、同じ計算式で対応可能です。

例えば、11, 12, 1月を一つのグループ(冬)にしたい場合を考えてみましょう。

調整の考え方

グループの切れ目を計算しやすくするために、一番小さい「1」が含まれる冬グループが 0, 1, 2 になるように調整します。

  1. 「1月」を「2」にする(+1する):
    そのまま (month + 1) とすると、11月→12、12月→13、1月→2 となり、まだバラバラです。
  2. 12で割った余りを使う:
    (month + 1) % 12 と計算してみます。
    • 11月:(11+1) % 12 = 0
    • 12月:(12+1) % 12 = 1
    • 1月 :( 1+1) % 12 = 2

これで、冬グループが綺麗に 0, 1, 2 に揃いました

判定式の完成

グループが 0, 1, 2 に揃えば、あとは先ほどと同じように 「3で割る」 だけです。

  • 11, 12, 1月 → 3で割ると 0
  • 2, 3, 4月 → 3で割ると 1
  • 5, 6, 7月 → 3で割ると 2
  • 8, 9, 10月 → 3で割ると 3

これはかなり読みづらいし何をしているかずいぶん考えないといけないね。計算の処理速度は圧倒的にこちらが早いの?

その通りですね!鋭い指摘です。

結論から言うと、「処理速度は現代のコンピュータではほぼ変わらない」というのが現実です。むしろ、デメリットの方が目立ちます。

速度について

確かに計算1回で済むので、理論上は if を繰り返すより数ナノ秒(10億分の1秒単位)早いかもしれません。しかし、現代のCPUは非常に賢いので、あなたの書いたようなシンプルな if 文も一瞬で処理してしまいます。
人間が体感できる差は全くありません。

「読みづらさ」の代償

おっしゃるように、このコードを数ヶ月後に見た自分や、他の人が見たとき、
「なぜここで +1 してるんだ?」「%12 / 3 って何だ?」
と解析するのに数分奪われることになります。

プログラミングの世界では、これを「マジック(魔法)」と呼びますが、基本的には「嫌われる書き方」です。

なぜこんな技があるのか?
  • メモリが極端に少なかった時代: 1バイトでも節約したかった時代の名残。
  • ゲームプログラミング: 1秒間に60回、何万回も同じ計算を繰り返すような極限状態では、少しでも if を減らしたいことがあります。
  • 数学的な美学: 「条件分岐を使わずに式で解けた!」というパズル的な楽しみ。

そう、だからメモリが少ない(組み込みの)世界に挑戦しようとおもってるのでこっちの書き方がいいのかなっておもっちゃいました

なるほど、「組み込み」の世界を見据えていらっしゃるのですね!それなら話は別です。その視点があるなら、今の「計算で解く」考え方は非常に大きな武器になります。

組み込み(家電、自動車の制御、センサーなど)の世界では、以下の理由で「計算トリック」が今でも重宝されることがあります。

「分岐予測」のミスを防ぐ

最近のCPUは「次はこっちのif文を通るだろう」と予測して動くのですが、外れると処理がガクンと遅れます。計算式(算術演算)だけで結果を出すコードは、この「予測ミス」が起きないため、実行速度が常に一定(決定的)という強みがあります。これはリアルタイム性が求められる制御の世界では信頼に繋がります。

コードサイズの節約

if-else を並べると、その分だけCPUへの命令数が増えます。メモリ(ROM)が数キロバイトしかないような小さなマイコンでは、計算式一行で済ませる工夫が「入らないプログラムをねじ込む」決定打になることもあります。

ただし「組み込み」でも大事なこと

組み込みの現場でも、「なぜその計算をしたか」のコメントは必須です。
「トリッキーな書き方をするなら、意図を言葉で残す」のが、組み込みのプロの仕事です。