入門書でのおさらい18・多重ループ

今日は102ページ多重ループから始めたいと思います。

102ページ・多重ループ

演習は一回終わって次は多重ループの勉強しましょう。2重ループからです

いいですね、ステップアップしましょう!多重ループ(入れ子構造)は、表形式のデータ処理や図形描画など、プログラミングの表現力が一気に広がる大事なステップです。

2重ループのイメージ

外側のループが「行(縦の動き)」、内側のループが「列(横の動き)」を担当します。

まずは2重ループの感覚を掴むために、一番シンプルな「長方形」を表示する例を見てみましょう。

#include <stdio.h>

int main(void) {
    int height = 3; // たて
    int width = 5;  // よこ

    for (int i = 1; i <= height; i++) {     /* 外側:たての繰り返し */
        for (int j = 1; j <= width; j++) { /* 内側:よこの繰り返し */
            putchar('*');
        }
        putchar('\n'); /* 1行(横一列)終わったら改行 */
    }

    return 0;
}

ポイント

  • 内側のループが先に回るiが1のとき、jが1〜5まで一気に動きます。
  • 役割分担: 「何行作るか」を外側が決め、「1行の中に何を並べるか」を内側が決めるイメージです。

なるほど内側のループが横(列)、外側が縦(行)にあてはまるんだね

その通りです!「外側のループが1回回る間に、内側のループが最後まで回り切る」という動きが、ちょうど「1行分を端まで描いてから、次の行へ移る」という縦横の動きにピッタリ重なるんです。

この感覚が掴めると、九九の表や複雑な図形もスイスイ書けるようになりますよ。

104ページのList4-19の「表示」ではまった話

104ページのList4-19のプログラムを解説書通りに入力してもエラーがでるんだ。;とか抜けてないか探したけど大丈夫で何が原因かわからない。みてくれない?これがエラーの表示。kadai14というファイル名だよ。

kadai14.c: In function 'main':
kadai14.c:4:33: warning: unknown escape sequence: '\216'
    4 |     printf("<92><b7><95><fb><8c>`<82><f0><95>\<8e><a6><82><b5><82><U+0702><b7><81>B");
      |

そのエラーメッセージは、コードそのものの間違いというより、日本語(全角文字)が原因でコンパイラが混乱してしまっている状態です。

エラーの原因

warning: unknown escape sequence: '\216' という警告は、GCCなどのコンパイラが「Shift_JIS」という文字コードで書かれた日本語をうまく処理できず、日本語の一部を「特殊な命令(エスケープシーケンス)」と勘違いしたときに出ます。
特に「長方形」の「方」や「表示」の「表」といった漢字は、コンピューター内部のデータ構造上、C言語の \(バックスラッシュ/円マーク)と同じ値を含んでいるため、このエラーが起きやすいことで有名です。

解決する方法

今の環境でこのエラーを回避するには、以下の2つの方法がおすすめです。

  1. 日本語部分を英語にする(一番確実です)
    printf("長方形を表示します。"); を printf("Rectangle:"); などに変えてみてください。
  2. 日本語の直後に \ を入れる(強引な解決策)
    printf("表\\示"); のように、問題の起きている漢字のすぐ後ろに \ をもう一つ足すと、コンパイラの勘違いを防げることがあります。 

どこが原因かな

一番の大きな原因は、「表」という漢字です。

実は、C言語の世界では昔から有名な「2バイト文字の罠」にはまっています。

なぜエラーが出るのか

「表示」の「表」という漢字は、コンピュータ内部のデータ(Shift_JISという形式)で見ると、終わりの部分に偶然 \ (バックスラッシュ) と同じコードが含まれています。

そのため、コンパイラは printf("表示") というコードを読んだときに:
「おっと、\ があるぞ!これは改行の \n みたいに、次の文字と組み合わせて何か特別な命令をする合図だな!」
と勘違いしてしまいます。しかし、その後に続くデータが有効な命令ではないため、unknown escape sequence(不明な命令です) と怒られてしまうのです。

どこを直せばいいか

一番手っ取り早い解決策は、日本語のメッセージをシンプルにすることです。

  • 修正前: printf("長方形を表示します。");
  • 修正後: printf("長方形を表示します。 "); (最後に半角スペースを1つ入れるだけで解決することが多いです)
  • 確実な方法: printf("Rectangle\n"); のように英語にする。

この「表」という字でエラーが出る現象、C言語初学者が必ずと言っていいほど遭遇する「儀式」のようなものなので、あまり気にしないでくださいね!

ほんとうだ!「表」をけしたらちゃんと動いたよ!「方」は大丈夫だった。

残念ながら半角スペースと\\はだめだった。だからもう「表」消しちゃった!

「表」という字を消して解決しましたか!お見事です。

実は「表」の他にも「予・能・申」などの漢字が、C言語(というか昔ながらの文字コード)では同じようにエラーを引き起こす「ダメ文字」として知られています。
「\」でもダメだったということは、環境の文字コード設定がかなりシビアなのかもしれませんね。今後も日本語でエラーが出たら、潔く文字を変えるか、英語にするのが一番の近道です!

演習4-20

うう、これはむずかしいね。だいぶ考えたけどわからないなあ。少しヒント教えてほしい。下の九九のコード使うでしょ?

#include<stdio.h>
int main(void){
    for(int i = 1; i <= 9; i++){
        for(int j = 1; j <= 9; j++){
            printf("%3d",i * j);
        }
        putchar('\n');
    }
    return 0;
}

はい、そのコードが九九の表の「核」になる部分です!大正解です。
printf("%3d", i * j); を使って、3桁分の幅を確保して数字を並べるというのも完璧ですね。

演習4-20を完成させるためのヒントを少しだけお伝えしますね。

ヒント

  1. 見出し(横)を作る: 表の1行目に、1 2 3 ... 9 という数字の「見出し」を出す必要があります。これは2重ループのに、単発の for 文で書きます。
  2. 仕切り線を作る: 見出しのすぐ下に、横棒 --- のような仕切り線を引くと表らしくなります。
  3. 見出し(縦)を作る: 各行の最初(内側のループが始まる直前)に、1 |2 | のように現在の段の数字を表示させる必要があります。

今のコードをベースに、「数字だけの塊」の外側に、「見出し」をどう付け足すかを考えてみてください。

ヒントの1、見出し横は単独でforのループ使ったらいいのか。0番目から9番目とカウントして0番目に見出しを入れて1から9番を九九に使えばよさそう

ヒント2、最初は半角4つ(—+)あとは半角3つ分(—)を出力したらいいね
横の見出しと仕切り線は単独のforでかけばいいんだ。

いずれも0番目は半角4つ分、1から9は半角3つ分でそろえることにしました

ヒント3で立ての見出しを作るには、こちらも横と同じで0番目から9番目とカウントして0番目に見出しを入れて1から9番を九九に使う。

#include<stdio.h>
int main(void){
    for(int i = 0;i <= 9;i++){
        if(i == 0)  printf("   |");
        else    printf("%3d",i);
    }
    putchar('\n');

    for(int i = 0;i <= 9;i++){
        if(i == 0)    printf("---+");
        else    printf("---");
    }
    putchar('\n');
  
    for(int i = 1; i <= 9; i++){
        for(int j = 0; j <= 9; j++){
            if(j == 0) printf(" %d |",i);
            else    printf("%3d",i * j);
        }
        putchar('\n');
    }
    return 0;
}

すごい!見出しと仕切り線まで含めた「完全な表」の形にたどり着きましたね。
実行すると、九九の表がしっかりレイアウトされて表示されるはずです。

特に工夫されているのが、「列の始まり(j == 0)」を判定して、縦の仕切り棒 | を入れているところです。これによって表の構造がグッと分かりやすくなっています。

仕切り線のループ回数と printf の修正もバッチリですね。これで実行すると、縦横の数字と枠線が綺麗に揃った、見栄えのいい九九の表が表示されます。

自分で書いたロジックで複雑な表が描けると、達成感がありますよね!

演習4-21

これは簡単。

#include<stdio.h>
int main(void){
    int n;
    puts("正方形を作ります。");
    printf("何段ですか?:");   scanf("%d",&n);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            putchar('*');
        }
        putchar('\n');
    }
    return 0;
}

正解です!完璧な正方形プログラムですね。

先ほどの長方形の経験を活かして、縦(i)と横(j)の両方を入力された値 n まで回すことで、きれいに正方形が描けるようになっています。

さらに puts を使って最初のメッセージを出力しているのも、コードがスッキリしていて良いですね!

演習4-22

入力された値をどっちが大きいか比べるプログラムにしようと思います

#include<stdio.h>
int main(void){
    int w1, w2;
    puts("横長の方形を作ります。");
    printf("一辺(その1):");   scanf("%d",&w1);
    printf("一辺(その2):");   scanf("%d",&w2);
    int height, width;
    if(w1 >= w2){
        height = w2;
        width = w1;
    }else{
        height = w1;
        width = w2;
    }
    for(int i = 1; i <= height; i++){
        for(int j = 1; j <= width; j++){
            putchar('*');
        }
        putchar('\n');
    }
    return 0;
}

正解です。

if 文を使って、どちらの入力が大きくても自動的に「小さい方を height」「大きい方を width」に振り分けるロジック、非常に明快で素晴らしいです。これで必ず「横長」の長方形が表示されますね。論理的にも、これで全てのケース(一辺が同じ場合も含めて)を正しくカバーできています。

演習4-23

左上に直角がくるタイプから作ってみます。

#include<stdio.h>
int main(void){
    int len;
    puts("左上直角三角形をひょうじします");
    printf("短辺:");   scanf("%d", &len);

    for(int i = 1; i <= len; i++){
        for(int j = 1; j <= len - i + 1; j++){
            putchar('*');
        }
        putchar('\n');
    }
    return 0;
}

正解です!お見事!

j <= len - i + 1 という条件式を自力で導き出せたのは素晴らしいですね。
iが増える(下の段に行く)につれて、表示する星の数が1つずつ減っていくロジックが完璧に機能しています。

次は右上に直角がくるタイプを作ってみます。

#include<stdio.h>
int main(void){
    int len;
    puts("右上直角三角形をひょうじします");
    printf("短辺:");   scanf("%d", &len);

    for(int i = 1; i <= len; i++){
        for(int j = 1; j <= i - 1; j++){
            putchar(' ');
        }
        for(int j = 1; j <= len - i + 1; j++){
            putchar('*');
        }
        putchar('\n');
    }
    return 0;
}

完璧です!素晴らしいロジックですね。

内側のループを2つ並べて、「まず空白を i - 1 個書く」→「次に星を len - i + 1 個書く」 という組み立て方が非常に明快です。
特に i - 1 の部分、1段目(i=1)のときは空白を0個にするという処理が正確に反映されていますね。

これで「左上直角」と「右上直角」の両方をマスターしました!

演習4-24

これはむずかしそうだね。ヒントがあるから*の個数は楽勝。あとは空白の個数を図に描いて調べたらいいね

#include<stdio.h>
int main(void){
    int dan;
    puts("ピラミッドを作ります。");
    printf("何段ですか:");   scanf("%d", &dan);

    for(int i = 1; i <= dan; i++){
        for(int j = 1; j <= dan-i; j++){
            putchar(' ');
        }
        for(int j = 1; j <= (i - 1) * 2 + 1; j++){
            putchar('*');
        }
        for(int j = 1; j <= dan - i; j++){
            putchar(' ');
        }
        putchar('\n');
    }
    return 0;
}

正解です。

特に、星の数を決める (i - 1) * 2 + 1 という式、素晴らしいですね。1, 3, 5… と増えていく奇数の法則をしっかりコードに落とし込めています。

また、星の後に空白を入れるループもしっかり書いてくれているので、論理的に左右対称なピラミッドが完成していますね(実は星の後の空白はなくても見た目は同じになりますが、構造として完璧を目指す姿勢が素晴らしいです!)。

あそっか*の後の空白はいらないんだね。つい左右対称で考えちゃったから。処理の速さとメモリのこと考えてなしにしておくよ。

#include<stdio.h>
int main(void){
    int dan;
    puts("ピラミッドを作ります。");
    printf("何段ですか:");   scanf("%d", &dan);

    for(int i = 1; i <= dan; i++){
        for(int j = 1; j <= dan-i; j++){
            putchar(' ');
        }
        for(int j = 1; j <= (i - 1) * 2 + 1; j++){
            putchar('*');
        }
        putchar('\n');
    }
    return 0;
}

その考え方、プログラミングにおいて非常に重要です!

「見た目が同じなら、無駄なループ(処理)は省く」という判断は、プログラムの効率化(最適化)の第一歩ですね。左右対称の美学も素晴らしいですが、コンピュータに余計な仕事をさせないスマートな書き方に修正するのはナイスな判断です。

ちなみに星の個数はヒントで教えてくれてたけどどうしてこうなるか説明してくれる?

もちろんです!「1, 3, 5, 7…」と増えていく奇数の並びをどう式にするか、という話ですね。

ピラミッドの段数(i)と星の数の関係を整理すると分かりやすくなります。

  • 1段目():1個
  • 2段目():3個
  • 3段目():5個
  • 4段目():7個

この「2つずつ増える」という関係を式にする考え方は2通りあります。

「2倍して調整する」考え方(解説書のヒントの式)

まずは2倍してみます(i * 2)。
すると、2, 4, 6, 8… となります。
欲しいのは 1, 3, 5, 7… なので、全部から1を引けばいいことになります。

つまり、i * 2 - 1 です。
解説書のヒントの式 (i - 1) * 2 + 1 も、展開すると 2i - 2 + 1 = 2i - 1 なので、全く同じ正解です!

「植木算」的な考え方

「1個からスタートして、1段増えるごとに2個ずつ追加する」と考えます。

  • i = 1のとき:1 + (2 × 0)
  • i = 2のとき:1 + (2 × 1)
  • i = 3のとき:1 + (2 × 2)

追加する回数はいつも 「段数マイナス1」 なので、
1 + (i - 1) * 2
となります。数学の「等差数列」の公式そのものです

へーむずかしい。数学の知識が必要なんだね

演習4-25

いろいろいじってできました。最初の空白の個数は図に描いてみたら「i – 1」個だったので簡単でした。

#include<stdio.h>
int main(void){
    int dan;
    puts("下向きピラミッドを作ります。");
    printf("何段ですか:");   scanf("%d", &dan);

    for(int i = 1; i <= dan; i++){
        for(int j = 1; j <= i - 1; j++){
            putchar(' ');
        }
        for(int j = 1; j <= (dan - i) * 2 + 1; j++){
            printf("%d",i % 10);
        }
        putchar('\n');
    }
    return 0;
}

問題は数字の数です。図から「(dan – i) * 2 + 1」とだしたけれどもよくわかってないから教えて

図を描いて法則を見つけたのは素晴らしいですね!それが一番確実な方法です。

「なぜ (dan - i) * 2 + 1 になるのか」を、先ほどのピラミッドと比較して考えるとスッキリしますよ。

「残り何段あるか」を考える

上向きピラミッドの時は、段数 i が増えるほど星が増えましたが、今回は「下の段に行くほど減る」ので、「あと何段分残っているか」に注目します。

  • 全体の段数を dan とします。
  • 現在の段 i に対して、「自分より下の段がいくつあるか」は、dan - i で計算できます。
具体的な数値で当てはめる(例えば dan = 4 のとき)
  • 1段目( i = 1)
    自分より下は4 ‐ 1 = 3段
    式に入れると( 4 ‐ 1 ) × 2 + 1 = 7個
  • 2段目( i = 2)
    自分より下は4 ‐ 2 = 2段
    式に入れると( 4 ‐ 2 ) × 2 + 1 = 5個
  • 3段目( i = 3)
    自分より下は4 ‐ 3 = 1段
    式に入れると( 4 ‐ 3 ) × 2 + 1 = 3個
  • 4段目( i = 4)
    自分より下は4 ‐ 4 = 0段
    式に入れると( 4 ‐ 4 ) × 2 + 1 = 1個

このように、「逆さまのときは、『全体の高さ – 今の段』を一つの塊として、それを2倍して調整する」と考えれば、必ず奇数の減少列が作れるんです。

「図から出した」というのは、まさにプログラミングの現場で「仕様からアルゴリズムを導き出した」のと同じことですよ!

演習はこれで終わりです!

おまけ:残りのとこで疑問に思ったとこを聞いた

識別子

108ページ、識別子のとこで、国際文字名と呼ばれる文字も使えますとあるけど具体的にどんなやつ?

「国際文字名」というのは、簡単に言うと「日本語(ひらがな、漢字)や、英語以外のアルファベット」をプログラムの変数名(識別子)として使うための仕組みのことです。
昔のC言語はアルファベットと数字しか使えませんでしたが、今の規格(C99以降)では、以下のような名前も「識別子」として認められています。

具体的な例
  • 日本語の変数名
    int りんご = 150;
    int 合計 = 0;
  • 特殊なアルファベット
    double café = 450.0;(フランス語などのアクセント付き文字)
内部的な仕組み

コンピュータの内部では、これらは「ユニコード(Unicode)」という番号で管理されています。
例えば「あ」という文字を使いたいとき、コード上では \u3042 という「ユニコード形式の名前(これが本来の国際文字名)」として扱われます。

現場でのリアルな話

「使えるか使えないか」で言えば「使えます」
ただし、実際の現場や学習の場では、以下の理由であまり使われません。

  1. 環境依存: コンパイルするソフトによってはエラーになることがある。
  2. 入力が面倒: 日本語入力と半角入力を切り替えるのが手間。
  3. 世界基準: プログラムは世界中の人が読む可能性があるため、基本は英語(アルファベット)で書くのがマナーとされています。

今の段階では、「知識として知っておけばOK。でも、自分ではアルファベットと数字(とアンダースコア)で名前をつける」という方針が一番安全です!

まって絵文字も使えるの?

結論から言うと、最新の規格(C11やC23など)と最近のコンパイラであれば、絵文字を変数名に使うことも可能です!

ただし、さきほどの「国際文字名」のルール(ユニコード)に基づいているため、いくつかの「面白いけれど厄介な特徴」があります。

実際に動く例(環境によります)

お使いのコンパイラ(GCCなど)が新しければ、こんなコードも通ります。

int 🍎 = 150;
int 💰 = 5000;
int 合計 = 🍎 + 💰;
printf("結果は %d です\n", 合計);
絵文字が使える理由

C言語のルールで「ユニコードの特定の範囲にある文字は識別子に使っていいよ」と決められています。絵文字もその範囲に含まれているため、理屈の上では「漢字」や「ひらがな」と同じ扱いになります。

でも、おすすめしない理由(現場のホンネ)
  • 見た目がバグる: エディタによっては絵文字の幅がズレて、コードがガタガタになります。
  • 検索できない: 🍎(赤いリンゴ)と 🍏(青いリンゴ)を見分けるのが大変だったり、入力ミスに気づきにくかったりします。
  • OSによる違い: Macでは動くけどWindowsではエラーになる、といったことがよく起きます。
結論

「技術的には可能だけど、プログラミング界の禁じ手」のようなものです(笑)。

友達を驚かせるための「ネタ」として知っておくのは面白いですが、演習問題などではアルファベットと数字で進めるのが一番平和ですよ!

区切り子

区切り子だけどさifなら区切り子の順番が「if(式){文}」って決まってるじゃん?「 if(式){文}」じゃなくて「if{式}(文)」でも使えちゃうの?

結論から言うと、絶対に「if{式}(文)」とは書けません。

C言語の文法では、記号の順番が「法律」のように厳格に決まっているからです。

なぜ入れ替えられないの?

C言語のコンパイラ(翻訳機)は、コードを頭から順番に読んで解釈します。

  • if という文字を見つける
    「おっ、次は条件が来るな。( を探そう」と身構えます。
  • もし if { と書いてあったら
    「条件の ( が来るはずなのに、いきなり中身の { が来たぞ!ルール違反だ!」と判断して、エラー(Syntax Error)を出して止まってしまいます。
「区切り子」の役割

「区切り子」というのは、いわばプログラミングにおける「句読点」や「記号のルール」です。

  • ():条件式や、関数の引数を包むためのもの。
  • {}:複数の命令を一つの「ブロック(塊)」としてまとめるためのもの。

この順番や組み合わせが変わると、コンパイラはプログラムの意味を理解できなくなってしまいます。日本語で「私は・リンゴを・食べる」を「食べる・リンゴを・私は」と言い換えても意味は通じますが、C言語はもっと頭が固くて、決まった型どおりに並んでいないと受け付けてくれないのです。

文字列リテラル

文字列リテラルって何だった?わすれちゃった・・・。

「文字列リテラル」とは、プログラムの中に直接書き込まれたダブルクォーテーション ” で囲まれた文字の連なりのことです。
これまでの演習で何度も使っていますよ!

具体的な例

  • printf("こんにちは"); の "こんにちは"
  • printf("整数値:"); の "整数値:"
  • printf("%3d", i * j); の "%3d"

これらがすべて「文字列リテラル」です。

覚えておくと役立つポイント
  1. 「リテラル」の意味: プログラムに「直接書かれた値」のことです。
    • 10 は整数のリテラル
    • "10" は文字列のリテラル
  2. 中身は「配列」: 実は、C言語の内部では文字列リテラルは「文字の配列」として扱われています。だから、次から勉強する「第5章 配列」を学ぶと、文字列の正体ももっと深く理解できるようになります。
  3. 最後に隠れキャラがいる: 文字列リテラルの最後には、目に見えないですが '\0'(ヌル文字) という「ここで終わりですよ」という印が自動的にくっついています。

お疲れさまでした今日は演習が多く長くなってしまいました。明日は配列です。