コンテンツにスキップ

第8章 いろいろなプログラムを作ってみよう

本章では、いくつかのプログラムを題材にして、主として、次のようなことがらを学習する。

  • 関数形式マクロ
  • コンマ演算子
  • ソート
  • 列挙体
  • 再帰
  • 入出力
  • 文字
  • 数字文字と数値
  • 拡張表記

8-1 関数形式マクロ

関数と同じような感じで使えて、かつ、関数よりも融通が利くのが、本節で学習する関数形式マクロである。

関数形式マクロ

読み込んだ数値の2乗値を求めて表示するプログラムを作る。数値といっても、表現する型は、バリエーションが豊富である。まずは、int型用とdouble型用の二つを作成しよう。

List 8-1に示すのが、そのプログラムである。

List 8-1

// 整数の2乗と浮動小数点数の2乗(関数)

#include <stdio.h>

//--- int型整数の2乗値を求める ---//
int sqr_int(int x)
{
    return x * x;
}

//--- double型浮動小数点数の2乗値を求める ---//
double sqr_double(double x)
{
    return x * x;
}

int main(void)
{
    int    n;
    double x;

    printf("整数を入力せよ:");
    scanf("%d", &n);
    printf("その数の2乗は%dです。\n", sqr_int(n));

    printf("実数を入力せよ:");
    scanf("%lf", &x);
    printf("その数の2乗は%fです。\n", sqr_double(x));

    return 0;
}

実行例

整数を入力せよ:3
その数の2乗は9です。
実数を入力せよ:4.25
その数の2乗は18.062500です。

二つの型に対して、関数を作り分けていることともに使い分けていることが分かるであろう。

それでは、long型の値の2乗値を求めることを考える。そうすると、たとえば、sqr_longといった名前の関数を作り、再び使い分けることになる。

このように、関数をどんどん作っていき、それぞれに別の名前を与えていくと、プログラムは、よく似た名前の、似て非なる関数であふれかえってしまう。

このような問題を簡単に解決する関数形式マクロ(function-like macro)を使って作り直したのが、List 8-2のプログラムである。

// 整数の2乗と浮動小数点数の2乗(関数形式マクロ)

#include <stdio.h>

#define sqr(x)  ((x) * (x))    // xの2乗値を求める関数形式マクロ

int main(void)
{
    int    n;
    double x;

    printf("整数を入力せよ:");
    scanf("%d", &n);
    printf("その数の2乗は%dです。\n", sqr(n));

    printf("実数を入力せよ:");
    scanf("%lf", &x);
    printf("その数の2乗は%fです。\n", sqr(x));

    return 0;
}

実行例

整数を入力せよ:3
その数の2乗は9です。
実数を入力せよ:4.25
その数の2乗は18.062500です。

第5章で学習したオブジェクト形式マクロは《置換》のイメージであったが、今回の関数形式マクロは《展開》のイメージである。

本プログラムの#define指令は、コンパイラに対して次の指示を行っている。

これ以降に、sqr(☆)という形の式があれば、次のように展開せよ。 ((☆) * (☆))

その結果、Fig.8-1のようにプログラムが展開された上で翻訳・実行される。

重要

関数形式マクロを使えば、各型用に関数を作り分けて使い分ける必要性から解放される。

もちろん、関数形式マクロsqrは、long型やfloat型などにも対応している。

Fig.8-1 関数形式マクロの展開 Fig.8-1 関数形式マクロの展開

関数と関数形式マクロ

見かけ上は、関数と同じように使える関数形式マクロですが、どのように違うのか、ポイントをおさえよう。

関数は、仮引数の型や返却値の型を一意に決めた上で定義する。型ごとに別々の名前で定義する必要があるとともに、呼出し側でも使い分ける必要がある。 関数形式マクロsqrは、2項*演算子で乗算できる型でさえあれば、あらゆる型に適用できる。

関数では、次のような処理が、内部的に(私たちの意識しないところで)行われる。 - 引数の受渡し(実引数の値が仮引数にコピーされる)。 - 関数の呼出しや関数からもどる作業(プログラムの流れが行ったり来たりする)。 - 返却値の受渡し。 展開された式が埋め込まれる関数形式マクロでは、このような処理は行われない。

上記の特徴によって、関数形式マクロのほうが、プログラムの実行がわずかに速くなることが期待できる反面、コンパイルによって作られた実行プログラムが大きくなる可能性がある。展開後が複雑で大きな式であれば、関数形式マクロを利用するすべての箇所に、その複雑な式が展開されて埋め込まれるからである。

関数形式マクロのデメリットの一つが、コードの見た目では気付きにくい動作が起こる可能性があることである。たとえば、sqr(a++)は、((a++) * (a++))と展開されるため、aのインクリメントが2回行われる。

このように、展開後の式が複数回評価されることなどに起因して、意図しない結果となることは、マクロの副作用(side effect)と呼ばれる。

Important

関数形式マクロの作成時や利用時は、副作用の可能性に注意が必要である。

Note

関数版のsqr_intをsqr_int(a++)と呼び出した場合に、aのインクリメントが2回行われることはない。マクロ版であれば、sqr(a)とa++を分離して記述する必要がある。

Column 8-1 関数形式マクロとオブジェクト形式マクロ

マクロ名sqrと、続く(とのあいだに空白を入れて、

#define sqr (x)  ((x)*(x))
と定義すると、sqrは関数形式マクロではなく、オブジェクト形式マクロとみなされる。そのため、『sqrを(x) ((x)*(x))に置換せよ』という指示となってしまうのである。 関数形式マクロを定義するときは、マクロ名と(とのあいだに空白を入れないようにする。

引数のない関数形式マクロ

関数形式マクロは、引数を受け取らない形式のものも定義できる。次に示すalertが、その一例である。

#define alert()  (putchar('\a'))    // 警報を発するマクロ

これは、警報を発するマクロである。

Note

関数形式マクロalertを呼び出すプログラム例は、本章の「まとめ」にある。

演習 8-1

二つの値xとyの差を求める関数形式マクロを定義せよ。

diff(x, y)

演習 8-2

二つの値xとyの大きい方の値を求める関数形式マクロは次のように定義できる。

#define max(x, y)  (((x) > (y)) ? (x) : (y))
このマクロを利用して、四つの値a、b、c、dの最大値を求める、次に示す各式がどのように展開されるかを示し、考察を加えよ。
max(max(a, b), max(c, d))
max(max(max(a, b), c), d)

演習 8-3

type型の二つの値を交換する、関数形式マクロを次の形式で定義せよ。

swap(type, a, b)
たとえば、int型の変数xとyの値が5と10であるときに、swap(int, x, y)を呼び出した後は、xとyには10と5が格納されていなければならない。

Column 8-2 関数形式マクロの定義内の式は()で囲む

次に示すのは、二つの値の和を求める関数形式マクロである。

#define sum_of(x, y)  x + y
これを、次のように呼び出すことを考えよう。
z = sum_of(a, b) * sum_of(c, d);
残念ながら、マクロ展開後の式は、期待とは異なるものになってしまう。
z = a + b * c + d;
たとえ必要なくても、個々の引数と、全体を()で囲んでおけば安心である。
#define sum_of(x, y)  ((x) + (y))
こうしておけば、先ほどの式は、次のように展開される。
z = ((a) + (b)) * ((c) + (d));

関数形式マクロとコンマ演算子

次に考えるのは、警報を発した上で、文字列を表示する関数形式マクロである。List 8-3に示すのが、そのプログラムである。

List 8-3

// 警報を発した上で表示を行うマクロ(誤り)

#include <stdio.h>

#define puts_alert(str)  { putchar('\a'); puts(str); }

int main(void)
{
    int n;

    printf("整数を入力せよ:");
    scanf("%d", &n);

    if (n)
        puts_alert("その数はゼロではありません。");
    else
        puts_alert("その数はゼロです。");

    return 0;
}

実行例

翻訳時にエラーとなるため、実行することはできません。

本プログラムは、翻訳時にエラーとなるため、実行できない。main関数のif文を展開したFig.8-2を見ながら、エラーが発生する理由を探っていこう。

関数形式マクロ呼出しの展開結果は、次のようになっている。

{ putchar関数の呼出し; puts関数の呼出し; }    // { 式文 式文 } → 複合文

これは、2個の式文が{}で囲まれている複合文である。そのため、展開後のif文は、図の水色部分であって、最初の複合文の終端}で完結する。続く赤い部分の;は、単一の空文とみなされる。そうすると、コンパイラは、『ifがないのに、どうしてelseが出てくるのだろうか?』となってしまうのである。

Note

だからといって、マクロ定義の{}を取り去ることもできない。というのも、別のエラーが発生するからである(ご自身で確認してみよう)。

Fig.8-2 誤った関数形式マクロの展開 Fig.8-2 誤った関数形式マクロの展開

この問題の解決に有効なのが、Table 8-1に示すコンマ演算子(comma operator)である。

Table 8-1 コンマ演算子 Table 8-1 コンマ演算子

このコンマ演算子を使ってマクロputs_alertを書き直したのが、List 8-4のプログラムである。

List 8-4

// 警報を発した上で表示を行うマクロ

#include <stdio.h>

#define puts_alert(str)  ( putchar('\a') , puts(str) )

int main(void)
{
    int n;

    printf("整数を入力せよ:");
    scanf("%d", &n);

    if (n)
        puts_alert("その数はゼロではありません。");
    else
        puts_alert("その数はゼロです。");

    return 0;
}

実行例

整数を入力せよ:0
♪その数はゼロです。

本プログラムのif文を展開したFig.8-3を見ながら理解していこう。各呼出しの展開結果は、次のように、文ではなく式となる。

( putchar関数の呼出し , puts関数の呼出し )    // (式 , 式) → (式)

Note

二つの式aとbをコンマ演算子,で結んだ式a, bが、一つの式となるのは、式を+演算子で結んだa + bが、式となるのと同じ理屈である。

さて、式の後ろにセミコロン;を置いたものは、式文である。そのため、図の水色の部分が、式文という単一の文とみなされる。

これで、if文全体が正しく展開されることが分かりました。

Important

複数の式に置換するマクロは、コンマ演算子,で結び付けることで、展開結果のコードが単一の式となるように定義する。

Fig.8-3 関数形式マクロの展開 Fig.8-3 関数形式マクロの展開

Note

コンマ演算子を利用したコンマ式a, bでは、式aとbが順に評価される。左側の式aは評価だけが行われて、その値は切り捨てられる。そして、右側の式bの評価によって得られる型と値が、コンマ式a, b全体の型と値になる。

たとえば、iの値が3でjの値が5のときに、

x = (++i, ++j);
を実行すると、iとjの両方がインクリメントされ、インクリメント後のjの値である6がxに代入される(変数iはインクリメントされますが、式++iの値は無視される)。

8-2 ソート

何らかの基準を設けた上で、データの集まりを昇順(小さい順)や降順(大きい順)に並べかえることをソート(sort)という。

バブルソート

List 8-5は、5人の学生の身長を昇順にソートするプログラムである。仮引数に受け取った配列をソートするbsort関数の中身を理解していこう。

List 8-5

// 学生の身長を読み込んでソート

#include <stdio.h>

#define NUMBER  5    // 人数

//--- バブルソート ---//
void bsort(int a[], int n)
{
    for (int i = 0; i < n - 1; i++) {  <---- 全部でn-1パス
        for (int j = n - 1; j > i; j--) {  <---- 末尾から先頭側へ走査
            if (a[j - 1] > a[j]) {   <---- 着目した2要素の左側が大きければ交換
                int temp = a[j];      2値の交換はp.123で学習済み
                a[j] = a[j - 1];
                a[j - 1] = temp;
            }
        }
    }
}

int main(void)
{
    int height[NUMBER];    // NUMBER人の学生の身長

    printf("%d人の身長を入力せよ。\n", NUMBER);
    for (int i = 0; i < NUMBER; i++) {
        printf("%2d番:", i + 1);
        scanf("%d", &height[i]);
    }

    bsort(height, NUMBER);    // ソート

    puts("昇順にソートしました。");
    for (int i = 0; i < NUMBER; i++)
        printf("%2d番:%d\n", i + 1, height[i]);

    return 0;
}

実行例

5人の身長を入力せよ。
1番:179
2番:163
3番:175
4番:178
5番:173
昇順にソートしました。
1番:163
2番:173
3番:175
4番:178
5番:179

実行例の場合、bsort関数が受け取る要素数nの配列aには、次の値が入っている。

179  163  175  178  173

最初に、末尾二つの数値[178, 173]に着目しよう。先頭側の値の方が大きいのであれば、昇順に並んでいないということですから、これらの値を交換する。

179  163  175  173  178

次に、後ろから2番目と3番目[175, 173]に着目し、同様に交換する。

179  163  173  175  178

後ろから3番目と4番目[163,173]は妥当な順序ですので、交換の必要はない。

179  163  173  175  178

続いて、後ろから4番目と5番目[179, 163]に着目して、交換する。

163  179  173  175  178

ここまでの手順をまとめると、次のようになる。この一連の作業をパスと呼ぶ。

179  163  175  178  173
179  163  175  173  178
179  163  173  175  178
179  163  173  175  178   1パス目
163  179  173  175  178

最小の数値163が先頭に引っ張り出された結果、先頭の1要素がソート済みとなる。

同じ作業を、先頭から2番目の要素173まで行います。その2パス目の過程は、次のようになる(点線より右側が比較・交換の対象です)。

163  179  173  175  178
163  179  173  175  178
163  179  173  175  178   2パス目
163  173  179  175  178

2番目に小さい数値173が2番目に引っ張り出され、先頭の2要素がソート済みとなる。

引き続き、先頭から3番目の要素175までの3パス目を行う。

163  173  179  175  178
163  173  179  175  178   3パス目
163  173  175  179  178

これで、先頭の3要素がソート済みとなる。続いて4パス目です。

163  173  175  179  178
163  173  175  178  179   4パス目

これで、先頭の4要素がソート済みとなる。このとき、末尾の要素は最大値ですので、ソートが完了する(要素数nの配列に対して必要なパスはn - 1回です)。

ソートを行うアルゴリズム(一連の手順)には、数多くの手法が考案されている。本プログラムで用いたのは、バブルソート(bubble sort)と呼ばれるアルゴリズムである。

Note

パスをn - 1回行うための繰返しが、外側のfor文である。
そのループ本体である内側のfor文がパスを担当し、2要素a[j - 1]とa[j]を比較する。パスにおける走査は配列の末尾から先頭へと行いますので、jの開始値は、末尾要素の添字n - 1である。jの値をデクリメントしていき、先頭側へと走査する。各パスにおいて、先頭i個の要素はソート済み、未ソート部はa[i]~a[n-1]ですから、jのデクリメントは、値がi + 1になるまで行います。

8-3 列挙体

第7章では、有限範囲の連続した整数を表す整数型を学習した。本節では、少数の限られた整数値の集合を表す列挙体について学習する。

列挙体

List 8-6は、犬、猫、猿の選択肢を提示して、選ばれた動物の鳴き声を表示するプログラムである。このプログラムを理解していこう。

List 8-6

// 選ばれた動物の鳴き声を表示

#include <stdio.h>

enum animal { Dog, Cat, Monkey, Invalid };

//--- 犬が鳴く ---//
void dog(void)
{
    puts("ワンワン!!");
}

//--- 猫が鳴く ---//
void cat(void)
{
    puts("ニャ~オ!!");
}

//--- 猿が鳴く ---//
void monkey(void)
{
    puts("キッキッ!!");
}

//--- 動物を選択 ---//
enum animal select(void)
{
    int tmp;

    do {
        printf("0…犬 1…猫 2…猿 3…終了:");
        scanf("%d", &tmp);
    } while (tmp < Dog || tmp > Invalid);
    return tmp;
}

int main(void)
{
    enum animal selected;

    do {
        switch (selected = select()) {
        case Dog    : dog();    break;
        case Cat    : cat();    break;
        case Monkey : monkey(); break;
        }
    } while (selected != Invalid);

    return 0;
}

実行例

0…犬 1…猫 2…猿 3…終了:0
ワンワン!!
0…犬 1…猫 2…猿 3…終了:2
キッキッ!!
0…犬 1…猫 2…猿 3…終了:3

プログラム冒頭の水色の部分は、値の集合を表す列挙体(enumeration)の宣言である。

Fig.8-4 列挙体の宣言 ig.8-4 列挙体の宣言

Fig.8-4に示すように、与えられた識別子=名前animalが列挙体タグ(enumeration tag)で、{}で囲まれたDog、Cat、Monkey、Invalidが列挙定数(enumeration constant)である。

各列挙定数には、先頭から順に、0、1、2、3という整数値が与えられるため、列挙体animalのイメージはFig.8-6のようになる。ちょうど、複数個の選択肢から1個だけが選択可能なラジオボタンのような感じである。

Fig.8-6 列挙のイメージ Fig.8-6 列挙のイメージ

整数型が幅広い範囲の整数値を自由に表すのとは異なり、列挙体は、限られた値のみを表す。しかも、各値に対しては名前が与えられる。

この宣言で作られる型は列挙型(enumerated type)と呼ばれる型であり、その名前は、『enum animal型』である(列挙体タグ名animalだけでは、型名となりません)。

さて、main関数内の赤い部分が、『enum animal型』の変数selectedの宣言である。

Note

Fig.8-5のように対比させると、はっきりするように、int型でも列挙型でも、変数の宣言の形式は、『型名 識別子;』である。

Fig.8-5 宣言の形式 Fig.8-5 宣言の形式

この宣言によって、selectedは、0、1、2、3のいずれか1個の値をとり得る変数となる。

次は、関数selectに着目する。動物の選択肢を表示して、選択された動物に対応するenum animal型の値を返す関数である。

個々の列挙定数Dog、Cat、Monkey、Invalidの型は、int型である。そのため、返却値型がenum animal型である関数selectが、int型のtmpの値を返しているわけである。

曖昧さをなくしたいのであれば、次のようにキャストを行えばよいであろう。

return (enum animal)tmp;

Note

読み込む値を0~2に制限するためのdo文の制御式に着目しよう。

tmp < Dog || tmp > Invalid

動物とは無関係な列挙定数Invalidが使われています(Invalidは、『無効な』という意味です)。

もし仮に、列挙定数Invalidが存在せず、4番目の動物として『アザラシ』が追加されたとすると、列挙体の宣言と制御式は、次のように変更することになる。

enum animal { Dog, Cat, Monkey, Seal };
tmp < Dog || tmp > Seal

これだと、動物を追加や削除するたびに、条件判定のための制御式を変更しなければならくなってしまいます。

最後の列挙定数をInvalidにするという方針をとっておけば、列挙体の宣言と制御式は、次のようになります(制御式1を変更することなく、そのまま使えます)。

enum animal { Dog, Cat, Monkey, Seal, Invalid };
tmp < Dog || tmp > Invalid

列挙定数

先ほどのプログラムでは、0から始まる連番が各列挙定数に与えられていました。列挙定数の識別子の後ろに、=と値を置くことで、各列挙定数に与える値を自由に設定できます。たとえば、

enum kyushu { Fukuoka, Saga = 5, Nagasaki };
では、Fukuokaは0、Sagaは5、Nagasakiは6となる。

Fig.8-7 列挙体kyushu Fig.8-7 列挙体kyushu

このように、=によって値が与えられた列挙定数は、その値となり、与えられていない列挙定数は、一つ前の列挙定数の値に1を加えた値となる。

複数の列挙定数を同じ値とすることも可能である。たとえば、

enum namae { Asuka, Nara = 0 };

と宣言すると、AsukaとNaraの両方が0になる。

Fig.8-8 列挙体namae Fig.8-3 列挙体namae

なお、列挙体タグ名を省略して宣言することも可能である。例を示す。

enum { JANUARY = 1, FEBRUARY, /*...中略...*/ , DECEMBER };

この列挙型の変数は宣言できません(宣言したくても、名前がないからです)。

ただし、このような列挙定数も、右に示すように、switch文内のラベルなどで有効活用できます。

int month;
// ...
switch (month) {
case JANUARY  : // 1月の処理
case FEBRUARY : // 2月の処理
//--- 中略 ---//
}

演習 8-4

バブルソートの走査を末尾側からではなく、先頭側から行うように、List 8-5(p.234)を書きかえたプログラムを作成せよ(本問は、前節の内容に関する問題である)。

演習 8-5

性別や季節などを表す列挙体を自由に定義し、それを用いたプログラムを作成せよ。

Column 8-3 enumの読み方

enumのもとの単語であるenumerationの発音は、カタカナでの『イニューメレーション』に近い感じである。ところが、英語を母国語とする人でも、enumを『イニューム』とか『イーナム』などと適当に発音しているようである。コンピュータ用語に限らず、短縮された言葉の発音には絶対的な規則があるわけではありません。

列挙体にはいろいろな特徴がある。

  • 前ページに示した、月を表す列挙体を、もしオブジェクト形式マクロで定義するのであれば、次のようになる。
#define JANUARY   1      // 1月
#define FEBRUARY  2      // 2月
//--- 中略 ---//
#define DECEMBER  12     // 12月

宣言が12行にもなる上に、一つ一つに値を与える必要がある。

列挙体を使えば、手短に宣言できますし、先頭のJANUARYの値さえ正しく指定しておけば、それ以降の値をミスすることもありません。

  • 動物を表すenum animalは、0、1、2、3の値を表す型である。たとえば、変数anが、この型の変数であるときに、次の代入が行われたとする。
an = 5;      // 不正な値の代入

親切なコンパイラであれば、このような不正な値を使うコードに対して警告メッセージを発するため、プログラムのミスが発見しやすくなる。

もちろん、変数anがint型であれば、このようなチェックは不可能である。

  • プログラムの動作確認や誤り修正(デバッグ)などを支援する、デバッガなどのツールには、列挙型の変数の値を、整数値ではなく列挙定数の名前で表示するものがある。

その場合、enum animal型の変数selectedの値は、0や1ではなく、DogやCatと表示されますので、作業が容易になる。

Important

列挙体で表せそうな整数値の集合は、列挙体として定義しよう。

名前空間

列挙体タグ名と変数名は、異なる名前空間(name space)に属しているため、同じ綴りの識別子があっても区別できます。これは、岩手の福岡と、地名の福岡は、性格が異なるため区別できるのと同じです。たとえば、『福岡君と福岡市に行く。』では、前者が人名で後者が地名であることが明白です。

そのため、enum animal型の変数にanimalという識別子を与えることもできます。次のように宣言します。

enum animal animal;    // enum animal型の変数animalの宣言

もちろん、前者のanimalは列挙体タグ名で、後者のanimalは変数名です。

Note

名前空間については、第12章で詳しく学習します(p.341)。

演習問題

  1. 絶対値を計算する関数形式マクロ
  2. 降順バブルソート
  3. 四季を表す列挙体の使用
  4. 選択ソートの実装

演習問題9-1:絶対値を計算する関数形式マクロ

問題の説明

数値の絶対値を計算する関数形式マクロabsolute(x)を定義してください。この関数形式マクロは整数型と浮動小数点型の両方で使用できるようにしてください。

期待される結果

出力例:

整数値のテスト:
  |-5| = 5
  |10| = 10

浮動小数点値のテスト:
  |-3.14| = 3.14
  |2.7| = 2.7

ヒント

  • 絶対値の定義を考えてみましょう:数値が負の場合は符号を反転し、正の場合はそのままです
  • 条件演算子(?:)を使用すると簡潔に表現できます
  • マクロ内では必ず引数に括弧()をつけて、予期しない演算順序を防ぎましょう
  • マクロ全体も括弧で囲みましょう
  • 入力値が負の場合と正の場合の両方でテストしましょう
  • 整数型と浮動小数点型の両方に対応するか確認しましょう

演習問題9-2:降順バブルソート

問題の説明

List 8-5のバブルソートを修正して、配列を降順(大きい順)にソートするプログラムを作成してください。学生の身長データを読み込み、それを降順にソートして表示するプログラムにしてください。

期待される結果

出力例:

5人の身長を入力せよ。
 1番:179
 2番:180
 3番:165
 4番:171
 5番:168

降順にソートしました。
 1番:180
 2番:179
 3番:171
 4番:168
 5番:165

ヒント

  • バブルソートのアルゴリズムを理解しましょう:隣接する要素を比較し、必要に応じて交換します
  • 昇順ソート(小さい順)と降順ソート(大きい順)の違いは何でしょうか?
  • バブルソートの比較条件(if文の中の条件式)を変更することで、ソート順序を変更できます
  • ループ構造(for文)はそのままで、比較条件だけを変更します
  • 配列の要素がn個あるとき、必要なパスの回数はn-1回です
  • 各パスでは、未ソート部分の要素を隣接ペアごとに比較して交換します
  • 降順ソートでは、小さい要素が後ろ(大きいインデックス)へ移動します

演習問題9-3:四季を表す列挙体の使用

問題の説明

四季(春、夏、秋、冬)を表す列挙体を定義し、入力された季節に応じてその特徴を表示するプログラムを作成してください。List 8-6のスタイルを踏襲し、選ばれた季節の特徴を表示してください。

期待される結果

出力例:

0…春 1…夏 2…秋 3…冬 4…終了:0
桜が咲き、新しい始まりの季節です!
0…春 1…夏 2…秋 3…冬 4…終了:1
太陽が輝き、暑い季節です!
0…春 1…夏 2…秋 3…冬 4…終了:2
紅葉が美しく、収穫の季節です!
0…春 1…夏 2…秋 3…冬 4…終了:3
雪が降り、静かな季節です!
0…春 1…夏 2…秋 3…冬 4…終了:4

ヒント

  • 列挙体(enum)とは何かを理解しましょう:特定の値の集合を名前付きで定義する方法です
  • 四季を表す列挙体を定義し、各季節に対応する数値(0から始まる連番)を持たせます
  • 「終了」のための無効値(Invalid)も列挙体に含めると便利です
  • 各季節の特徴を表示する個別の関数を作りましょう
  • 季節を選択するための関数を作りましょう(入力値のチェックも忘れずに)
  • メインループでは、選択された季節に応じて適切な関数を呼び出します
  • Invalid(終了)が選択されるまでループを続けましょう
  • switch文を使って季節ごとに異なる処理を行いましょう

演習問題9-4:選択ソートの実装

問題の説明

List 8-5のバブルソートを選択ソートに書き換えなさい。選択ソートでは、未ソート部分の最小値を探し、ソート済み部分の末尾と交換します。学生の身長データをソートして表示するプログラムにしてください。

期待される結果

出力例:

5人の身長を入力せよ。
 1番:179
 2番:163
 3番:175
 4番:178
 5番:173

昇順にソートしました。
 1番:163
 2番:173
 3番:175
 4番:178
 5番:179

ヒント

以下の選択ソートの関数を参考にして、List 8-5のプログラムを選択ソートを使用するように書き換えなさい。

/*
 * selection_sort: 配列を昇順(小さい順)に選択ソートする関数
 * 引数:
 *   a[] - ソートする配列
 *   n   - 配列の要素数
 */
void selection_sort(int a[], int n)
{
    int i, j;

    for (i = 0; i < n - 1; i++) {      /* 固定位置を先頭から順に移動(n-1回) */
        int min_idx = i;               /* 現在の最小値の位置を記録 */

        /* 未ソート部分(i+1以降)から最小値を探す */
        for (j = i + 1; j < n; j++) {
            if (a[j] < a[min_idx]) {   /* より小さい値を見つけたら */
                min_idx = j;           /* 最小値の位置を更新 */
            }
        }

        /* 最小値が見つかったら交換(最適化:必要な場合のみ交換) */
        if (min_idx != i) {            /* 最小値が現在位置と異なる場合のみ交換 */
            int temp = a[i];           /* 交換のための一時変数 */
            a[i] = a[min_idx];         /* 交換処理 */
            a[min_idx] = temp;         /* 交換処理 */
        }
    }
}

C言語の歴史と発展

概要

C言語は1972年にデニス・リッチー(Dennis Ritchie)がAT&Tベル研究所で開発した汎用的な手続き型プログラミング言語です。Unixオペレーティングシステムの実装言語として誕生し、ハードウェアに近い低レベル操作を可能にしながらも高水準言語のデータ構造機能も備えています。このバランスにより、高い実行効率と優れた移植性・可読性を実現しています。

Fig.C-1 デニス・リッチー(Dennis Ritchie) Fig.C-1 デニス・リッチー(Dennis Ritchie)

https://www.japanprize.jp/prize_prof_2011_ritchie.html

C言語は長い歴史の中でANSI(アンシ) C(C89/C90)、C99、C11、C17、C23など複数の標準化とバージョンアップを経てきました。現在では、オペレーティングシステム、コンパイラ、組み込みシステムから大規模ソフトウェア開発まで幅広い分野で用いられています。また、C++、Java、Pythonなど後続のモダン言語にも大きな影響を与えています。

C言語の起源

BCPLとB言語からの発展

1960年代、Martin Richardsがケンブリッジ大学でBCPL(Basic Combined Programming Language)を開発しました。その後1969年に、ケン・トンプソン(Ken Thompson)がBCPLを簡略化してB言語を作成し、Unixの初期実装に使用しました。しかし、B言語は型システムや表現力に乏しかったため、リッチーは1971年から1973年にかけてB言語を拡張し、データ型、構造体、豊富な演算子などを追加しました。そして1972年に、これらの改良を加えた新言語を「C」と名付けて完成させました。

Fig.C-2 ケン・トンプソン(Ken Thompson) Fig.C-2 ケン・トンプソン(Ken Thompson)

https://www.japanprize.jp/prize_prof_2011_thompson.html

B言語の限界

B言語は単純な構造を持っていましたが、型システムがなく、ハードウェア制御の柔軟性に欠けていました。リッチーはこの課題を解決するために新しい言語設計に着手したのです。

『K&R』の登場

1978年、ブライアン・カーニハン(Brian Kernighan)とデニス・リッチーによる共著書『The C Programming Language』が発行されました。この書籍はC言語の文法と機能を体系的にまとめたもので、業界では「K&R C」と呼ばれ、初期C言語の事実上の標準リファレンスとなりました。『K&R』の出版は学術界や産業界でのC言語普及を大きく促進しました。

Fig.C-3 ブライアン・カーニハン(Brian Kernighan) Fig.C-3 ブライアン・カーニハン(Brian Kernighan)

https://engineering.princeton.edu/news/2019/04/18/computer-scientist-kernighan-elected-american-academy-arts-and-sciences

Fig.C-4 『The C Programming Language』「K&R C」 Fig.C-4 『The C Programming Language』「K&R C」

K&Rの影響力

『The C Programming Language』はプログラミング言語の解説書としては珍しく、実用的なサンプルコードと簡潔な説明で構成されていました。この書籍はプログラミング書籍の金字塔として今日でも参照され続けています。

初期の発展とUnix

Unixの移植性向上

Unixの第6版(1975年)までは、カーネルの大部分がアセンブリ言語で実装されていたため、新しいハードウェアへの移植には多大な工数を要していました。リッチーはC言語を使ってUnixカーネルの主要部分を書き直す作業を行い、1973年にこれを完了させました。この取り組みによりUnixは真の移植可能なオペレーティングシステムとなり、システムソフトウェア開発の過程が大幅に簡素化されました。

移植性の実証例

PDP-11向けに作られたUnixのコードがC言語で書き直されたことで、Intercom社のInterdata 8/32のような全く異なるアーキテクチャにも比較的短期間で移植できるようになりました。この成功はC言語の「一度書けばどこでも動く」という哲学の実証例となりました。

コミュニティとツールチェーンの形成

UnixとC言語の成功に伴い、多様なCコンパイラや開発ツールが登場するようになりました。AT&Tや大学からは実験的なソフトウェアパッケージが配布され、オープンソースコミュニティでもテキスト処理ツールやネットワークプロトコルスタックなど、C言語で作られた実用的なツールが次々に公開されました。これらの動きがシステムソフトウェア分野におけるC言語の地位を確立することにつながりました。

初期のCツール

この時期に登場した重要なツールには、テキストフォーマッタのnroff/troff、シェルスクリプト、make、lexやyaccといった構文解析ツールがあります。これらはすべてC言語で書かれ、今日のソフトウェア開発の基礎となりました。

標準化の歩み

ANSI C(C89/C90)の確立

1983年、米国標準協会(ANSI)はC言語の標準化を目的としてX3J11委員会を設置しました。そして1989年にANSI X3.159-1989「Programming Language C」を制定しました。これは「ANSI C」または「C89」と呼ばれ、翌年にはISO/IEC 9899:1990「C90」として国際標準にも採択されました。ANSI CとC90は技術的には同一のもので、書式上にのみわずかな差異があります。

実装の多様性

標準化以前のC言語は実装ごとに微妙な違いがあり、移植性の問題が発生していました。ANSI Cは「関数プロトタイプ」や「const」などの新機能を導入しつつ、既存のコードの互換性も確保する重要な役割を果たしました。

C95による初の修正版

1995年、ISO/IECによるAmendment 1が発行され、ワイドキャラクタ対応、ダイグラフ(二義的記号)、__STDC_VERSION__マクロなどが追加されました。また、技術的な誤りの修正も行われました。この改訂版は一般的に「C95」または「C94」と呼ばれています。

国際化対応

C95の主な目的は、非英語圏でのC言語の利用をサポートすることでした。特に、ワイドキャラクタのサポートは多言語処理において重要な進歩となりました。

後続バージョンとその影響

C99以降の拡張

C99の主な新機能

  • 可変長配列(VLA)
  • インライン関数(inline)
  • ブール型(_Bool)
  • 複素数型(complex)
  • 単行コメント(//)
  • 可変引数マクロ
  • 制限付きポインタ(restrict)

1999年のISO/IEC 9899:1999(C99)では、上記の機能が導入され、C言語の表現力が大幅に向上しました。2011年のC11(ISO/IEC 9899:2011)ではマルチスレッド対応()や静的アサート(_Static_assert)などの機能が追加されました。

その後も標準化は継続し、2018年のC17(ISO/IEC 9899:2018)はC11の技術的な修正版として位置づけられています。最新の2024年C23(ISO/IEC 9899:2024)では、ライブラリ関数の拡充、Unicodeサポートの強化、安全関数インターフェースの追加など、さらなる機能向上が図られています。

C11のマルチスレッド対応

#include <threads.h>
#include <stdio.h>

int thread_func(void *arg) {
    printf("スレッド実行中: %s\n", (char*)arg);
    return 0;
}

int main(void) {
    thrd_t thread;
    thrd_create(&thread, thread_func, "Hello from thread!");
    thrd_join(thread, NULL);
    return 0;
}

モダン言語への示唆

C言語の「簡潔・高効率・移植性」という設計哲学は、後続の多くのプログラミング言語に大きな影響を与えました。C++はCにオブジェクト指向プログラミングの概念を導入し、JavaやC#はCの文法構造を踏襲しています。さらに、PythonやGoなど多くの言語のランタイムやインタープリタはC言語で実装されています。

言語間の関係

現代のプログラミング言語の多くはC言語の影響を受けています:

  • C++ → Cの拡張(オブジェクト指向、テンプレート)
  • Java、C# → C/C++に似た文法、ガベージコレクション追加
  • Objective-C → CにSmalltalkのメッセージング機能を追加
  • Rust → Cの安全性問題を解決する近代的な代替言語
  • Go → Cの単純さを目指しつつ、並行処理を強化

現在でも、組み込みシステムやオペレーティングシステムのカーネル(Linuxなど)、IoTデバイス、自動運転システム、機械学習ライブラリの低位層など、多くの重要な分野でC言語は主要な開発言語として活躍しています。

C言語の課題

C言語の強力さは同時に危険性も伴います。メモリ管理の誤りは深刻なセキュリティ脆弱性(バッファオーバーフロー、使用後解放など)を引き起こす可能性があります。モダン言語の多くはこれらの問題への対策を取り入れています。

結論

BCPLからB言語、そしてC言語へと至る発展過程の中で、デニス・リッチーはシステムプログラミング言語における新たな基準を打ち立てました。Unixとの密接な結びつき、『K&R』書籍による普及活動、そしてANSI/ISO標準化の取り組みを経て、C言語は成熟した言語として確固たる地位を築きました。

C言語の現代的意義

C言語は性能、移植性、表現力のバランスに優れており、ソフトウェア開発とシステムプログラミングの中核技術として現代のコンピューティング環境を支え続けています。今日においても、様々な高級言語が登場する中で、C言語はその低レベル制御能力とクロスプラットフォーム対応により、オペレーティングシステム、コンパイラ、組み込みシステム、科学計算など多岐にわたる重要領域で不可欠な存在であり続けています。