入門書でのおさらい13・文字定数、putchar関数、演習4-8

今日は86ページの文字定数とputchar関数から始めたいと思います

86ページ・文字定数とputchar関数

今日は文字定数とputchar関数からだね

文字定数(’A’)

C言語では、1文字を表すときにシングルクォート ' ' で囲みます。

  • 'A' や '7''\n'(改行)などがこれにあたります。
  • ポイント: ダブルクォート "A" は「文字列」になり、全く別物として扱われるので注意してください。

putchar関数

カッコの中に書いた文字を画面に1文字だけ表示する関数です。

putchar('A'); // 画面に A と表示される

文字列ではないんだね。でもさ見た目ですごく混同します。

分かります、その感覚。見た目はほぼ同じですもんね。
C言語では、この「チョン(’)」か「ダブル(”)」かが、コンピュータにとっては天と地ほどの差になります。

一番分かりやすい見分け方はこれです:

  • 'A'(文字定数)
    • 中身は「数字」です。
    • コンピュータ内部ではASCIIコードという番号(’A’なら65)として扱われます。
    • だから putchar('A' + 1) と書くと ‘B’ が表示されるなど、計算ができます
  • "A"(文字列リテラル)
    • 中身は「住所(アドレス)」です。
    • メモリ上のどこかに置かれた「A」と「終端(\0)」のセットがどこにあるか、という場所を指します。
    • だから "A" + 1 なんて計算をしようとすると、変な場所を指してしまいます。
混同を防ぐイメージ
  • シングル ' ' は 「1つのカゴ」。1文字(1つの数字)しか入りません。
  • ダブル " " は 「列車の車両」。たとえ1文字でも、最後尾に「終わり」という印を連結した連なりとして扱います。

まずはputchar はカゴ(’ ‘)専用の関数」と覚えておくと間違いが減りますよ!

char型じゃないの?

ああそうだったんだ!文字定数の型がintだと入門書にあったので、ん?なぜintなのって思ったけど「ASCIIコードという番号」なんだね。数字だからint型だったんだ。一方で、文字列はポインタと一緒でアドレスなの?char型じゃないの?

その気づき、素晴らしいです!まさにC言語の「正体」に触れましたね。

1. 文字定数が int 型なのはなぜ?

おっしゃる通り、'A' は内部で 65 という数値(整数)として扱われます。
歴史的な経緯もありますが、C言語では文字を「計算できる最小の数値」として扱うため、文字定数そのものは int 型として定義されています。だから 'A' + 1 が 66 になり、'B' が表示できるんです。

2. 文字列はポインタ(アドレス)なの?

ここが混乱の元ですが、「文字列そのものは char 型の配列」ですが、プログラムの中で "A" と書くと、それは「その配列が置かれている先頭のアドレス」を指します。

  • 'A': 65 という 「値」 そのもの。
  • "A": メモリのどこかにある「A」というデータの 「住所」
型の違いを整理すると
  • 文字char(変数に格納した時)や int(文字定数)。とにかく「1つの数字」。
  • 文字列char*(ポインタ)。「ここから文字が並んでるよ」という目印。

なぜ char 型じゃないの?

char 型は「1バイト分の箱」なので、住所(ポインタ)のような大きな情報は入り切らないんです。なので、文字列を扱うときは必ず char型の配列」 か charを指すポインタ」 を使うことになります。

「文字列は住所」という感覚が掴めると、この先のポインタ学習が劇的に楽になりますよ!

まだ習ってないけどchar型の配列の一つ一つに文字が入ってて。それをつなげると文字列になる。その最初のchar型の住所を渡して文字列を表す(printfの場合)っていう風になってるのかな

その通り!まさに大正解です。まだ習っていないのに、その構造を言い当てるのはセンスが凄すぎます。

おっしゃった通り、C言語の文字列は以下のような仕組みになっています。

  1. 一文字ずつ箱に入るchar型の変数が横に並んだ「配列」に、1文字ずつ格納されます。
  2. 最後に目印: 文字列の終わりには、必ず「ここでおしまい」という意味の \0(ヌル文字)という数値の 0 が入ります。
  3. 先頭の住所を渡すprintf などの関数には、その「1文字目が入っている箱の住所」だけを教えます。
なぜ住所だけでいいのか?

printf くんは住所を教えてもらうと、そこから順番に文字を読み取っていき、\0(おしまい)を見つけるまで表示し続ける」という動きをします。

  • putchar('A')中身(65)を直接渡して、それを文字として出す。
  • printf("A"):「A」が入っている住所を渡して、中身をのぞきに行かせる。
混同しやすいポイントの復習
  • 文字定数 ('A')
    • 単なる数値(int型)。
    • putchar にそのまま放り込める。
  • 文字列リテラル ("A")
    • メモリ上に「A」と「\0」の2つが並んだ配列。
    • "A" と書いた瞬間、それはその配列の「先頭の住所」になる。

putcharはアスキーコードを文字にして表示するものであってchar型の変数に入った中の文字を読むわけではないってことかな?

いいえ、実は「両方とも同じこと」として扱われます!ここがC言語の面白くて、少しややこしいところです。

結論から言うと、putchar「渡された数値を、ASCIIコード表に照らし合わせて文字として表示する」という仕事だけをしています。

その数値が「直接書かれた数字」なのか、「変数の箱の中身」なのかは気にしません。

具体例で見てみましょう

以下の3つは、putcharにとってはすべて「65という数値を表示せよ」という同じ命令になります。

char c = 'A'; // 変数cの中身は数値の65

putchar(65);   // ① 直接数値を渡す
putchar('A');  // ② 文字定数(正体は65)を渡す
putchar(c);    // ③ char型変数の中身(65)を渡す

このプログラムを実際に実行したら結果は「AAA」だったよ!

動作のイメージ
  1. putchar(c) を実行しようとする。
  2. コンピュータが変数 c の中身をのぞく → 「お、65が入ってるな」
  3. putchar に 65 という数値を放り込む。
  4. putchar がASCIIコード表を見て 「65は ‘A’ だな」 と判断して画面に出す。
文字を読む」の正体

「変数に入った文字を読む」というのは、コンピュータにとっては「変数という箱に入っている『数値』を取り出す」ことと全く同じなんです。

  • char型の変数:1バイトの小さな数値をいれる箱。
  • putchar:もらった数値を文字として画面に映すプロジェクター。

ですので、「char型の変数に入っている数値(ASCIIコード)を読み取って、文字として表示している」というのが正確な解釈になります!

char型は文字を入れるものって勘違いしてたんだけど、char型に入っているものはアスキーコードなんだ

その気づき、C言語の核心を突いています!

まさにその通りで、C言語に「文字」というデータは存在しません。あるのは「小さな整数」だけです。

「文字を入れる」の正体

char 型の正体は、1バイト(8ビット)の小さな整数を入れる箱です。

  • 私たちが char c = 'A'; と書くとき、コンピュータは「c という箱に 65 という数字を入れよう」と処理しています。
  • char 型は、たまたま「ASCIIコード(0〜127)」を保存するのにちょうどいいサイズ(-128〜127)なので、「文字用」として使われているだけなんです。
なぜこれが重要なのか?

「中身はただの数字(ASCIIコード)」だと分かると、こんな面白いことができるようになります。

  1. 計算ができる: 'A' + 3 は 68 なので、putchar すると 'D' が出ます。
  2. 大小比較ができる: if ('a' < 'z') のような判定ができます(コードの番号が a の方が小さいから)。
  3. 小文字を大文字にする: ASCIIコードでは a と A の差は常に 32 です。なので、'a' - 32 と計算するだけで 'A' に変換できます。
結論
  • char型の中身 = 整数(ASCIIコード)
  • putcharの役割 = その数字を「文字の形」で画面に映し出すだけ

「char型は文字専用の特別な型」という思い込みが解けると、この先のアルゴリズム(並べ替えや暗号化など)の理解がめちゃくちゃ早くなりますよ。

なるほどー!新しい発見だった。よくわかりました!

おまけ:putcharにひらがなは入れられないの?

putcharで1文字のひらがなを入れたらエラーになりました。ひらがなは入れられないの?

結論から言うと、残念ながら putchar にひらがな(マルチバイト文字)を直接入れることはできません。

試しに putchar('あ'); と書くと、コンパイルエラーになるか、実行しても文字化けしてしまいます。

理由は主に2つあります。

  1. サイズが足りない: putchar が受け取れるのは「1バイト(0〜255の数値)」だけです。ひらがなは、今の主流(UTF-8)だと 3バイト も使ってしまうので、1バイト専用の putchar には入りきらないんです。
  2. 型が違う: 先ほど学んだ通り、'A' は整数(int)ですが、ひらがなはC言語では「文字の集まり(文字列)」として扱われます。
もしひらがなを表示したいなら

1文字であっても、ダブルクォートを使って printf で出すのが一般的です。

printf("あ"); // これならOK!

どうしても「1文字ずつ出す関数」でひらがなを扱いたい場合は、ワイド文字用の putwchar という別の関数を使う必要がありますが、設定が少し面倒なので、今は「ひらがな=文字列(printf)で扱うもの」と覚えておくのが一番安全です。

じゃあさ結局あ使えるのは何?アルファベットくらいなの?ギリシャ文字やキリル文字は?

putchar で確実に扱えるのは 「ASCII(アスキー)コード表」に載っている128種類だけ と思っておくのが一番安全です!

具体的には以下の通りです:

  • 確実にOK:
    • アルファベット(A-Z, a-z)
    • 数字(0-9)
    • 記号(!@#$%+-*/ など)
    • 制御文字(改行 \n、タブ \t など)
  • ギリシャ文字・キリル文字・ひらがな:
    • これらは 「マルチバイト文字」 と呼ばれ、1文字で2〜4バイト使います。
    • putchar は 「1バイト(0〜255)」 しか運べない小さなカゴなので、これらの文字を入れると「カゴからはみ出して壊れた状態」になり、文字化けしてしまいます。
なぜアルファベットだけ特別なの?

C言語が生まれたアメリカでは、アルファベットと記号さえあれば事足りたので、「1文字=1バイト(256種類あれば十分)」という設計にされました。後から「世界中の文字(ギリシャ文字や日本語)も使いたい!」となったときに、1バイトでは足りなくなってしまい、今の複雑な仕組み(UTF-8など)が作られたという歴史があります。

どうしてもギリシャ文字を出したい時は?

printf を使って、文字列として出力します。

今の段階では、putchar は英語と記号、あと数字専用の特急便」とイメージしておくと混乱しなくて済みますよ!

演習4-8

これはいつもの改行を表示しない演習だね。改行はわかりにくいので「a」をそのあとに入れています。

ループは常に通る(0以下のときは回らないだけ)。最後に hantei を見て改行するか決める。↓

#include<stdio.h>
int main(void){
    int no;
    printf("正の整数を入力せよ:"); scanf("%d",&no);

    int hantei = no;
     while (no-- > 0)
        { 
            putchar('*');
        }

    if(hantei >=1){ 
        putchar('\n'); 
        putchar('a'); 
    }
    
    return 0;
}

1 未満ならループにも入らせない。↓

#include <stdio.h>

int main(void) {
    int no;
    printf("正の整数:");
    scanf("%d", &no);

    if (no > 0) {
        while (no-- > 0) {
            putchar('*');
        }
        putchar('\n');
    }

    return 0;
}

おまけ:ジェミニ先生が珍しく間違ったこと・・・

実はもう一つ、よく使われる「第3の答え」があります。

変数を増やさず(hantei を使わず)、while ループの直後に「ある条件」で if を書くパターンです。

どんな条件だと思いますか?
ヒントは、「ループが終わった直後、no の値はどうなっているか」です。

(※答えは言いませんよ!笑)

もし興味があれば、この「変数を増やさないバージョン」を予想して書いてみますか?

じゃあ一回考えてみるよ。

「ループが終わった直後、no の値はどうなっているか」というヒントのもと

while (no-- > 0)
        { 
            putchar('*');
        }
    printf("%d",no);

printf関数でループが終わった後のnoの中身を見てみました。0以下は入力した数字から1を引いた数字(例えば-5なら-6),0より大きい整数は一律で「-1」です。

ん?読み込んだ値が1未満やから0の時も表示してはダメだよね。?でも今回はnoの値が0でも0より大きい整数も全部-1になるから区別がつけられなくない?

0の時は?

入力が 0 のときも、-5 のときと同じ動きになります!

シミュレーションしてみましょう。

  1. 判定while (no-- > 0) に 0 が入る。
  2. 比較0 > 0 かどうかチェック → 「偽(いいえ)」
  3. デクリメント: 判定が終わった直後に no が 1 減って、-1 になる。
  4. 終了: ループの中身は実行されずに終了。

つまり、入力が 0 以下の場合、判定を1回だけ通って no は必ず -1 になる ということです。

では、逆に入力が 1 以上(例えば 1)だった場合は、ループを最後まで終えたあとの no はいくつになっているでしょうか?

ここが分かれば、if (no == ???) で改行するかどうかの条件が書けますね!

「1以上のとき」と「0以下のとき」で、終わった後の no の値に違いは出そうですか?

あなたの方法で0か0以外正の整数か区別付きますか?

鋭いところを突いてきましたね!

結論から言うと、今のままの while (no-- > 0) を使った方法だと、ループ終了後の no の値だけで「最初が 0 だったのか、それとも 1 以上だったのか」を 区別することはできません。

なぜなら、以下のようになるからです。

  • 最初が 1 のとき
    1. 1-- > 0 で「真」。no は 0 になる。
    2. 0-- > 0 で「偽」。no は -1 になる。
      → 終了時の no は -1
  • 最初が 0 のとき
    1. 0-- > 0 で「偽」。no は -1 になる。
      → 終了時の no は -1

どちらも最終的に -1 になってしまうので、これだけでは「改行するかどうか」の判断がつきません。

じゃあ、どうすればいい?

「変数を増やさない」かつ「最初に if で丸ごと囲まない」でやるなら、デクリメントを判定(カッコ)の中でやらない というのが一つの手です。

例えば、こんな風に考えてみるのはどうでしょう?
no が 0 より大きい間だけループを回し、ループの中で no を減らす」

これなら、ループが終わった後の no の値で、最初が 0 だったのかどうかが判定できるかもしれません。

この「判定とデクリメントを分ける」作戦、一度コードで試してみますか?それとも「やっぱり最初に if で囲むのが一番スマートだな」と結論づけますか?

デクリメントを分けたけど0か1以上の正の整数か区別がつけられないよ。もうわからないから答えを教えて。

承知いたしました。ここまでご自身でしっかりと考え、while(no-- > 0) の限界(どちらも -1 になってしまう点)まで突き止められたのは素晴らしいです!

「変数を増やさない」かつ「最初に if で囲まない」場合の、一つの解答例(答え)をお見せします。

答え:デクリメントを「中」で行う方法

判定と更新(デクリメント)を分けることで、ループ終了後の値をコントロールします。

#include <stdio.h>

int main(void) {
    int no;
    printf("正の整数を入力せよ:");
    scanf("%d", &no);

    // 1. まず「最初から0以下」ではないか、ここでnoの値を保ったまま判定する
    while (no > 0) {
        putchar('*');
        no--; // 2. ループの最後で減らす
    }

    // 3. ループが終わった時の no の値を見る
    // 最初が0以下なら no は元のまま(0以下)
    // 最初が1以上なら no は 0 になっている
    if (no == 0) {
        putchar('\n');
    }

    return 0;
}

解説

  1. while (no > 0): ここでデクリメントをしないことで、入力が 0 だった場合は no は 0 のままループを通り過ぎます。
  2. no--: ループの中({ })で減らします。これにより、1回でもループを回れば、最後は必ず no が 0 になった時点で止まります。
  3. 判定:
    • 最初が 3 なら、3→2→1→0 と減って止まるので no == 0 が 
    • 最初が 0 なら、一度も減らずに no == 0 なので……あれ?これだと 0 の時も改行されてしまいますね。

さらなる答え:実は while (no-- > 0) のままでも……

実は、あなたが最初に書いた 「別の変数に取っておく」 方法や、私が最初に出してしまった 「全体を if で囲む」 方法が、この問題においては最もシンプルで間違いのない「王道の正解」です。

実行した結果、きみの方法だと0でも改行しちゃうよ?

おっしゃる通りです!私の出した「デクリメントを中に入れる案」でも、最初が 0 のときに結局 if (no == 0) を通って改行されてしまいますね。お恥ずかしい限りです。

「変数を増やさず、if で丸ごと囲わず、no だけで何とかする」という難題に挑むなら、唯一の解決策はこれかもしれません。

究極の「no」だけバージョン

while (no > 0) {
    putchar('*');
    if (--no == 0) { // 減らした結果が0になった瞬間だけ改行する
        putchar('\n');
    }
}

これなら、

  • 最初が 0 のとき:ループに入らないので、改行もされない。
  • 最初が 1 以上のとき:最後の一回で no が 0 になった瞬間に改行される。

という動きになります。

–no;の形はまだ習ってないよ

あなたがずっとおっしゃっていた通り、no-- > 0 という判定を通ってしまう以上、「最初から0だった場合」と「1から減って0になった場合」の区別を、ループが終わった後の no の値だけで判断するのは不可能でした。

こうして色々とこねくり回してみると、あなたが最初に書いたhantei 変数に取っておく」というコードが、いかに素直で、誰が見てもミスが起きにくい優れた書き方だったかがよく分かりますね。

この問題の条件が「1未満」でなく「負の整数なら出力しない」なら使えましたね

おっしゃる通りです!もし問題の条件が「負の数のときだけ改行しない」であれば、そのロジックで完璧に機能しましたね。

  • 入力が 0 のとき:判定 0-- > 0 を1回通り、no は -1 になる。
  • 入力が 1 以上のとき:最終的に no は -1 になる。
  • 入力が -1 以下のとき:判定 (-1)-- > 0 を1回通り、no は -2 以下 になる。

これなら、if (no == -1) という条件で「0以上のときだけ改行する」という処理が書けました。今回の「1未満(つまり0も含む)」という条件が、絶妙にその「終了時の重なり」を突いてくる意地悪な設定だったわけですね。

演習4-3の時は3種類の答えができたけど今回はそれはできなかったようです