コンテンツにスキップ

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

8-4 再帰的な関数

関数の中では、自分自身と同じ関数を呼び出せるようになっている。そのような呼び出しは再帰関数呼出しと呼ばれる。本節では、再帰の基本を学習する。

関数と型

ある事象は、自分自身を含んでいたり、自分自身を用いて定義されていたりすれば、再帰的(recursive)であるといわれる。

Fig.8-9 再帰の例 Fig.8-9 再帰の例

Fig.8-9に示すのが、再帰的な図の一例である。ディスプレイ画面の中に、ディスプレイ画面が映っている。そのディスプレイ画面の中にも…。

再帰の考えを利用すると、1から始まって、2、3、…と無限に続く自然数は、次のように定義できる。

自然数の定義

  • [a] 1は自然数である。
  • [b] ある自然数の直後の整数も自然数である。

再帰的定義(recursive definition)によって、無限に存在する自然数を、わずか二つの文で表した。

再帰を効果的に利用すれば、定義だけではなく、プログラムも簡潔かつ効率のよいものとなる。

階乗値

再帰の例として取り上げるのは、非負の整数値の階乗値を求める問題である。非負の整数nの階乗を、再帰的に定義すると、次のようになる。

階乗n!の定義(nは非負の整数とする)

  • [a] 0! = 1
  • [b] n > 0ならば n! = n × (n - 1)!

たとえば、5の階乗である5!は、5 × 4!で求められる。また、その計算式で使われている式4!の値は、4 × 3!によって求められる。

Note

もちろん、3!は3 × 2!によって求められ、2!は2 × 1!によって求められ、1!は1 × 0!によって求められる。0!は、定義により1である。

ここに示した定義をプログラムとして実現したのが、List 8-7の関数factorialである。

List 8-7

//                                            chap08/list0807.c
// 階乗を再帰的に求める

#include <stdio.h>

//--- 整数値nの階乗値を返却 ---//
int factorial(int n)
{
    if (n > 0)
        return n * factorial(n - 1);
    else
        return 1;
}

int main(void)
{
    int num;

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

    printf("%dの階乗は%dです。\n", num, factorial(num));

    return 0;
}

実行例

整数を入力せよ:3
3の階乗は6です。

関数factorialが返す値は、次のようになっている。

  • 仮引数nに受け取った値が0より大きければ:n * factorial(n - 1)
  • そうでなければ            :1

見かけは単純ですが、実行時の挙動は複雑である。詳しく理解していきましょう。

Note

関数factorialの本体は、条件演算子を使うと1行で実現できます("chap08/list0807a.c")。

return n > 0 ? n * factorial(n - 1) : 1;

再帰関数呼出し

関数factorialがどのように階乗値を求めていくのかを、Fig.8-10に示す「3の階乗値を求める」例で理解しましょう。

Fig.8-10 3の階乗値を再帰的に求める手順 Fig.8-10 3の階乗値を再帰的に求める手順

[a] 関数呼出し式factorial(3)の評価・実行によって関数factorialが起動される。この関数は、仮引数nに3を受け取っており、次の値を返します。 3 * factorial(2)

もっとも、この乗算を行うには、factorial(2)の値が必要である。そこで、実引数として整数値2を渡して関数factorialを呼び出します。

[b] 呼び出された関数factorialは、仮引数nに2を受け取っています。 2 * factorial(1)

の乗算を行うために、関数factorial(1)を呼び出します。

[c] 呼び出された関数factorialは、仮引数nに1を受け取っています。 1 * factorial(0)

の乗算を行うために、関数factorial(0)を呼び出します。

[d] 呼び出された関数factorialは、仮引数nに受け取った値が0ですから、1を返します。

Note

この時点で、初めてreturn文が実行されます。

[c] 返却された値1を受け取った関数factorialは、1 * 1すなわち1 * 1を返します。

[b] 返却された値1を受け取った関数factorialは、2 * 1すなわち2 * 1を返します。

[a] 返却された値2を受け取った関数factorialは、3 * 2すなわち3 * 2を返します。

これで3の階乗値6が得られます。

関数factorialは、n - 1の階乗値を求めるために、関数factorialを呼び出します。このような関数呼出しが、再帰関数呼出し(recursive function call)である。

Note

再帰関数呼出しは『"自分自身の関数"の呼出し』というよりも、『"自分と同じ関数"の呼出し』と理解したほうが自然である。もし本当に自分自身を呼び出すのであれば、延々と自分を呼び出し続けることになってしまうからである。

再帰的アルゴリズムが適しているのは、解くべき問題や計算すべき関数、あるいは処理すべきデータ構造が再帰的に定義されている場合である。

したがって、再帰的手続きによって階乗値を求めるのは、再帰の原理を理解するための作為的な例であって、現実的には適切ではありません。

Note

再帰的アルゴリズムは、木構造、グラフ、分割統治法を利用するプログラムなどで幅広く応用されます。姉妹書『新・明解C言語で学ぶアルゴリズムとデータ構造』で詳しく学習できます。

演習8-6

再帰呼出しを用いずに、関数factorialを実現せよ。

演習8-7

異なるn個の整数からr個の整数を取り出す組合せの数nCrを求める関数を作成せよ。

int combination(int n, int r);
なお、nCrは次のように定義される。 nCr = n-1Cr-1 + n-1Cr(ただし、nC0 = nCn = 1、nC1 = n)

演習8-8

二つの整数値xとyの最大公約数をユークリッドの互除法を用いて求める関数を作成せよ。

int gcd(int x, int y);

ユークリッドの互除法とは

二つの整数を長方形の辺の長さとする。短いほうの辺を一辺とする正方形で埋めつくす。残った長方形に対して同じ操作を繰り返す。正方形のみで埋めつくされたとき、その正方形の辺の長さが最大公約数である。

ユークリッドの互除法の図示 22×8の長方形 ユークリッドの互除法の図示 22×8の長方形)

8-5 入出力と文字

多くのプログラムは、何らかの形で文字の入出力を行います。本節では、文字と入出力について学習します。

getchar関数とEOF

第4章では、1個の文字を出力するputchar関数を学習しました。これと対照的に、1個の文字の入力を行うのがgetchar関数である。これら二つの関数をうまく利用すると、入力から読み込んだ文字を、そのまま出力できます。List 8-8に示すのが、そのプログラムである。

List 8-8

//                                              chap08/list0808.c
// 標準入力からの入力を標準出力にコピーする

#include <stdio.h>

int main(void)
{
    int ch;

    while ((ch = getchar()) != EOF)
        putchar(ch);

    return 0;
}

実行例

Hello!
Hello!
This is a pen.
This is a pen.
Ctrl + Z

Ctrlキーを押しながらZキーを押下する。
一部の環境での最後の□が必要。
なお、UNIXやLinuxなどの環境では、
Ctrlキーを押しながらDキーを押下する。

Note

プログラム実行時の挙動については、Column 8-4(p.247)をご覧ください。

getchar関数が行うのは、1個の文字を読み込んで、その文字を返すことである。

Fig8-getchar

たとえば、'H'を読み込んだら、その'H'をそのまま返却します。

ただし、入力からの文字がつきるか、あるいは、読込み時にエラーが発生した際は、読込みに失敗したことを表すEOFを返却します。そのEOFは、ヘッダ内でオブジェクト形式マクロとして、負の値となるように定義されています。次に示すのが、定義の一例である。

Fig8-EOF

Note

EOFの名前は、End Of Fileに由来します。なお、ヘッダをインクルードしなければ、その定義が得られなくなって、プログラムの翻訳・実行が行えなくなります。

入力から出力へのコピー

本プログラムは、実質的にwhile文だけで構成されています。

継続条件を表す制御式(ch = getchar()) != EOFは、構造が複雑である。Fig.8-11を見ながら理解していきましょう。

Fig.8-11 while文の制御式の解釈 Fig.8-11 while文の制御式の解釈

① 代入演算子=による代入

代入式ch = getchar()では、読み込んだ文字がchに代入されます(たとえば、読み込んだのが'H'であれば、getchar関数が返却した'H'がchに代入されます)。

ただし、入力からの文字がつきるか、何らかのエラーが発生した場合は、chに代入されるのはEOFである。

② 等価演算子!=による判定

左オペランドの代入式ch = getchar()の評価で得られるのは、『代入後のch』である。その値が、右オペランドのEOFと等しくないかどうかが、等価演算子!=で判定されます。

Note

代入式の評価によって、代入後の左辺の型と値が得られることは、p.126で学習しました。

この判定が真となるのは、文字が正しく読み込まれているときである。そのため、文字が正しく読み込まれているあいだは、while文が繰り返し実行されます。このようにして、読み込んだ文字chをputchar関数で表示する、という処理を繰り返します。

入力からの文字がつきてしまうか、何らかのエラーが発生すると、判定が偽となるため、while文は終了します。

Note

代入=と等価!=の判定をつめ込まずに実現するのであれば、プログラムは、次のように実現することになります("chap08/list0808a.c")。

while (1) {                 // 無限ループ
    ch = getchar();         // 読み込んだ文字をchに代入
    if (ch == EOF)          // エラーが発生したら
        break;              // while文を強制的に抜け出す
    putchar(ch);            // 文字chを表示
}

数字文字のカウント

次の問題を考えましょう。

次々と文字を読み込んで、各数字文字'0'~'9'の出現回数をカウントする。

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

List 8-9

//                                              chap08/list0809.c
// 標準入力から読み込まれた数字文字をカウントする(第1版)

#include <stdio.h>

int main(void)
{
    int ch;
    int cnt[10] = {0};    // 数字文字の出現回数
                          // 全要素を0で初期化する

    while ((ch = getchar()) != EOF) {
        switch (ch) {
        case '0' : cnt[0]++; break;
        case '1' : cnt[1]++; break;
        case '2' : cnt[2]++; break;
        case '3' : cnt[3]++; break;
        case '4' : cnt[4]++; break;
        case '5' : cnt[5]++; break;
        case '6' : cnt[6]++; break;
        case '7' : cnt[7]++; break;
        case '8' : cnt[8]++; break;
        case '9' : cnt[9]++; break;
        }
    }

    puts("数字文字の出現回数");
    for (int i = 0; i < 10; i++)
        printf("'%d':%d\n", i, cnt[i]);

    return 0;
}

実行例

3.14159265358979328460
Ctrl + Z
数字文字の出現回数
'0':0
'1':2
'2':2
'3':3
'4':2
'5':3
'6':2
'7':1
'8':2
'9':3

このプログラムは、入力がつきるまで文字を読み込みます。そのため、赤色のwhile文の制御式は、前のプログラムと同じである。

Note

getchar関数で文字が正しく読み込まれているあいだは、while文が繰り返し実行され、次々と文字が読み込まれていきます。終了するのは、文字がつきるか、あるいはエラーが発生したときである。

int[10]型の配列cntが、文字の出現回数の格納先である。具体的には、文字'0'~'9'の出現回数を、cnt[0]~cnt[9]に格納します(全要素が0で初期化されています)。

while文のループ本体である、水色のswitch文を理解していきましょう。

getchar関数が返した値がEOFでないときに実行される、このswitch文では、10個の数字文字'0'~'9'に対するcaseを用意して、それぞれの文字に対応する配列cntの要素をインクリメントしています。

Note

読み込んだ文字chが'0'であればcnt[0]の値をインクリメントしますし、chが'1'であればcnt[1]の値をインクリメントします。

そのため、たとえば、文字'1'を初めて読み込んだときは、cnt[1]の値を0⇒1と更新することになり、文字'1'を2回目に読み込んだときは、1⇒2と更新することになります。

while文が繰り返されることによって、数字文字'0'~'9'のカウンタcnt[0]~cnt[9]をカウントアップしていくことが分かりました。

みなさんは、まどろっこしく感じたのではないでしょうか。数字文字'0'~'9'の出現回数を格納するための配列cntの添字が0~9なのですから、数字文字と添字をうまく対応させれば、もっと短いコードで実現できるはずです。

演習8-9

標準入力に現れた行数をカウントするプログラムを作成せよ。

Column 8-4 バッファリングとリダイレクト

本節のプログラムで行っている入出力に関して、補足学習しましょう。

  • バッファリング

List 8-8(p.244)の実行例では、1文字を読み込むたびに、すぐにその文字が出力されるのではなく、□が押された後に、1行分がまとめて出力が行われています。

C言語の入出力では、読み込んだ文字や、書き出すべき文字を、いったんバッファにためておいて、次の条件を契機(きっかけ)にして、実際の入力動作や出力動作が行われるのが一般的です。

[A] バッファが満杯になった
[B] 改行文字を読み込んだ

もちろん、バッファに文字をためずに、

[C] 即座に読み書きが行われる

という環境もあります。

これらの方式は、それぞれ、[A]完全バッファリング、[B]行バッファリング、[C]無バッファリングと呼ばれます。

□が押された後に出力が行われる環境では、[B]方式が採用されているということになります。

  • リダイレクト

次に示すように、入力と出力のファイル名を与えて実行してみましょう(実行ファイルの名前がlist0808であるとします)。

> list0808 < 入力ファイル名 > 出力ファイル名

そうすると、「入力ファイル」が、「出力ファイル」にコピーされます。これは、C言語ではなく、UNIXやMS-Windowsなどのosがもつリダイレクトという機能によるものです。

これまで、printf関数、puts関数、putchar関数は「画面」に表示し、scanf関数は「キーボード」から読み込むと学習しましたが、その入出力先は、プログラム起動時に変更できるのです。

文字コードと数字文字

前章でも簡単に学習した文字について、少し詳しいところまで踏み込んでいきましょう。 C言語では、文字は非負の整数値として扱われることになっており、個々の文字には、非負の整数値の文字コードが与えられます。

重要

文字は、その文字に与えられた、非負の整数値の文字コードで表される。

日本で普及しているパソコンの多くは、Table 8-2に示すJISコードに準じた文字コード体系が採用されていますので、これを例に理解していきましょう。

Table 8-2 JIS コード表 Table 8-2 JIS コード表

まずは、この表の読み方です。この表は16進数で表記されています。 たとえば、文字'h'は、6列目の8行目であり、16進数の68となります。 同様に、文字'A'は16進数での41です。

さて、数字文字'0'~'9'の文字コードを、16進数と10進数で表すと、次のようになります。

Fig8-moji

これで、数字文字'0'~'9'のコードが48~57であって、0~9ではないことが分かりました。文字の'0'と数値の0は、見かけは似ていても、まったく異なるものです。

EOFの値と、各数字文字の値を確認しましょう。List 8-10に示すのが、そのプログラムです。

List 8-10

//                                             chap08/list0810.c
// EOFの値と数字文字の値を表示

#include <stdio.h>

int main(void)
{
    printf("EOF = %d\n", EOF);
    printf("'0' = %d\n", '0');
    printf("'1' = %d\n", '1');
    /*… 中略 …*/
    printf("'9' = %d\n", '9');

    return 0;
}

実行結果一例

EOF = -1
'0' = 48
'1' = 49
'2' = 50
'3' = 51
'4' = 52
'5' = 53
'6' = 54
'7' = 55
'8' = 56
'9' = 57

Note

実行によって表示される値は、実行環境などによって異なります。

数字文字'0'~'9'の値を使うと、数字文字カウントプログラムのswitch文は、[A]のようになります。

[A] switch (ch) {
        case 48: cnt[0]++; break;
        case 49: cnt[1]++; break;
        case 50: cnt[2]++; break;
        case 51: cnt[3]++; break;
        //--- 中略 ---//
        case 57: cnt[9]++; break;
    }

このコードをよく読むと、規則性が見えてきます。 数字文字chの値から48を引いたch - 48が、添字にピッタリの0~9となります。

それを利用すると、このswitch文は、[B]に示す簡潔なif文に書きかえられます。10行以上におよぶプログラムが、わずか2行に収まりました!

[B] if (ch >= 48 && ch <= 57)
        cnt[ch - 48]++;

注意

[A]と[B]には、可搬性が欠けるという致命的な欠陥があります。文字のコードが、プログラムの実行環境で採用されている文字コード体系に依存するからです。

すなわち、文字'0'の値が48ではない環境で、[A]や[B]を実行すると、48~57という値をもつ別の文字の出現回数がカウントされてしまいます。

ところが、幸いなことに、C言語のプログラムが実行される環境では、

数字文字'0'、'1'、…、'9'の値は一つずつ増えていく。

という規則が満たされることが、保証されているのです。すなわち、'0'の値は文字コード体系によって異なるものの、たとえば、'5'は'0'より5だけ大きくなり、'5' - '0'の値が5になることが、すべての環境で成立します。

どの数字文字から'0'を引いても、ここで必要とする添字の値となりますので、[B]のif文は、[C]のように書きかえられます。

[C] if (ch >= '0' && ch <= '9')
        cnt[ch - '0']++;

このif文を使って書きかえたのが、List 8-11のプログラムです。随分と簡潔になりました。

List 8-11

//                                             chap08/list0811.c
// 標準入力から読み込まれた数字文字をカウントする(第2版)

#include <stdio.h>

int main(void)
{
    int ch;
    int cnt[10] = {0};    // 数字文字の出現回数

    while ((ch = getchar()) != EOF)
        if (ch >= '0' && ch <= '9')
            cnt[ch - '0']++;

    puts("数字文字の出現回数");
    for (int i = 0; i < 10; i++)
        printf("'%d':%d\n", i, cnt[i]);

    return 0;
}

実行例

3.14159265358979328460
Ctrl + Z
数字文字の出現回数
'0':0
'1':2
'2':2
'3':3
'4':2
'5':3
'6':2
'7':1
'8':2
'9':3

Note

数字文字'0'~'9'が、'0'、'0' + 1、'0' + 2'、…、'0' + 9で求められることを利用すると、左ページのList 8-10も、for文を使って簡潔に記述できます("chap08/list0810a.c")。

拡張表記

p.248のTable 8-2に示したJISコード表では、0x07から0x0Dの箇所が、\a、\b、\t、\n、\v、\f、\rとなっています。

このうち、'\n'が改行文字を、'\a'が警報を表す拡張表記であることは、第1章で学習しました。Table 8-3に示すのが、拡張表記の一覧です。

ここでは、引用符と、8進拡張表記と16進拡張表記について学習します。

Table 8-3 拡張表記 able 8-3 拡張表記

\'と\"…単一引用符と二重引用符

引用符記号'と"を表す拡張表記が\'と\"です。文字列リテラル中で使う場合と、文字定数中で使う際に注意すべきことがあります。

・文字列リテラル"…"の中での表記 * 二重引用符 必ず拡張表記\"で表します。たとえば、4文字の文字列XY"Zを表す文字列リテラルは、"XY\"Z"です。そのままの"XY"Z"はNGです。

Fig.8-12 文字列リテラルの例 Fig.8-12 文字列リテラルの例

Fig.8-12の例も考えてみます。3文字のABCを表す文字列リテラルは"ABC"ですが、前後を"で囲んだ5文字の"ABC"を表す文字列リテラルは"\"ABC\""です。

  • 単一引用符 そのままの表記'と拡張表記\'のいずれもOKです。 たとえば、4文字の文字列It'sを表す文字列リテラルは、"It's"と"It\'s"の両方の表記が可能です。

・文字定数'…'の中での表記 * 二重引用符 そのままの表記"と拡張表記\"のいずれもOKです。 すなわち、文字"を表す文字定数は'"'と'\"'の両方の表記が可能です。 * 単一引用符 拡張表記\'で表します。そのため、単一引用符を表す文字定数は'\''となります。 そのままの'''はNGです。

8進拡張表記と16進拡張表記

8進数または16進数のコードで文字を表すのが、\で始まる8進拡張表記(octal escape sequence)と、\xで始まる16進拡張表記(hexadecimal escape sequence)です。前者は文字コードを1~3桁の8進数で、後者は任意の桁数の16進数で表します。

たとえばJISコード体系では、数字文字'1'の文字コードは10進数の49であるため、8進拡張表記で'\61'、16進拡張表記で'\x31'と表せます。

ただし、このような表記は、プログラムの可搬性を低下させますから、なるべく使わないようにします。

Note

JISコード体系では、文字は8ビットで表せますが、9ビットの実行環境なども存在します(過去に実在しました)。文字が8ビットであることを前提とするプログラムは、可搬性が失われます。

また、C言語では、日本語文字などのようにchar型では表せない文字セットをもつ環境を考慮して、ワイド文字などの概念が定められています。

演習8-10

List 8-11(p.249)のプログラムをもとにして、数字文字の出現回数を、*を並べたグラフで表示するプログラムを作成せよ。List 5-12(p.128)や演習5-8(p.129)と同じ表示を行うこと。

Column 8-5 アルファベットの文字コード

本文で学習したように、数字文字'0'~'9'の文字コードは一つずつ増えていきます。その一方で、

* 英大文字'A'~'Z'の値は一つずつ増えていく。
あるいは
* 英小文字'a'~'z'の値は一つずつ増えていく。

という規則が成り立つという保証はありません。

事実、大型計算機で広く使われているEBCDICコードでは、上の規則は成立しません。

まとめ

  • 単純な置換が行われるオブジェクト形式マクロとは異なり、関数形式マクロは、引数を含めた展開が行われる(引数のない関数形式マクロも定義可能である)。 #define max2(a, b) (((a) > (b)) ? (a) : (b))

  • 関数が、特定の型ごとに作り分けて使い分ける必要があるのに対し、関数形式マクロは一つの定義で複数の型に対応できる。また、引数や返却値のやりとりなどが不要であるため、実行効率が高くなる傾向がある。

  • 展開後の式が2回以上評価されることなどに起因して、意図しない結果となることを、マクロの副作用という。関数形式マクロの作成時や利用時は、副作用の可能性に十分に注意する必要がある。

  • コンマ演算子,を利用したコンマ式a, bでは、aとbが順に評価される。その評価によって得られるのは、右オペランドbを評価した値である(左オペランドの値は切り捨てられる)。

  • 構文上1個の式が要求される箇所に複数の式を置く必要がある場合、それらの式をコンマ演算子で結ぶとよい。

  • 一定の基準に基づいて、データの集まりを昇順や降順に並べ替えることをソートという。ソートのアルゴリズムには、バブルソートなどがある。

  • 列挙体は、限られた整数値の集合を表す。列挙体に与える識別子が列挙体タグであり、個々の値に対する識別子が列挙定数である。

  • 列挙型の型名は、『enum 列挙体タグ名』であり、列挙体タグ名のみでは型名とならない。

  • 列挙体タグ名と変数名は、異なる名前空間に所属する。

Fig.8-13

  • ある事象は、それが自分自身を含んでいたり、自分自身を用いて定義されていたりするときに、再帰的であるといわれる。

  • 再帰関数呼出しは、自分自身と同じ関数を呼び出すことである。

  • getchar関数は、キーボード(標準入力ストリーム)から単一の文字を読み取って、その文字を返却するライブラリ関数である。

  • ファイルの終了を示すオブジェクト形式マクロEOFが、ヘッダ内で、負の値として定義されている。

  • C言語での文字は、その文字に与えられた、非負の整数値の文字コードである。

  • 数字文字'0'~'9'の値は一つずつ増えていく。そのため、数字文字'n'から'0'を引くと、整数値nが得られるし、数字文字'0'にnを加えると'n'が得られる。

  • 単一引用符を表す拡張表記は\'で、二重引用符を表す拡張表記は\"である。 文字列リテラルの中での二重引用符は、"ではなく\"を使って表記する。 文字定数   の中での単一引用符は、'ではなく\'を使って表記する。

  • 8進拡張表記あるいは16進拡張表記を使うと、文字コードで特定の文字を表せる。

  • Fig.8-14

演習問題

  1. 階乗計算の非再帰的実装
  2. 数字文字の合計値
  3. フィボナッチ数列
  4. アルファベット文字のカウント

演習問題10-1:階乗計算の非再帰的実装

問題の説明

非負の整数値nの階乗を計算する関数factorialを再帰を使わずに実装してください。階乗n!は次のように定義されます。 - 0! = 1 - n > 0のとき、n! = n × (n-1) × (n-2) × ... × 2 × 1

期待される結果

入力例1:

非負整数を入力してください: 5

出力例1:

5の階乗は120です。

入力例2:

非負整数を入力してください: 0

出力例2:

0の階乗は1です。

入力例3:

非負整数を入力してください: -3

出力例3:

エラー: 負の数の階乗は定義されていません。

ヒント

  • 階乗の計算は1から始めて、各数をかけ合わせていくことで計算できます
  • 初期値の設定に注意しましょう(0!は1なので、結果の初期値は1にします)
  • forループを使って1からnまでの整数を順に掛けていきます
  • 負の数の階乗は定義されていないため、入力チェックを忘れないようにしましょう
  • int型では大きな階乗値(13!以上)を正確に表現できないことに注意してください
  • main関数の基本的な構造は以下のようになります:
    int main(void)
    {
        int num;
    
        // 整数値の入力
        printf("非負整数を入力してください: ");
        scanf("%d", &num);
    
        // 入力値が負の場合はエラーメッセージを表示
        if (num < 0) {
            printf("エラー: 負の数の階乗は定義されていません。\n");
            return 1;
        }
    
        // 階乗を計算して結果を表示
        printf("%dの階乗は%dです。\n", num, factorial(num));
    
        return 0;
    }
    

演習問題10-2:数字文字の合計値

問題の説明

標準入力から文字を読み込み、入力された数字文字('0'〜'9')の数値としての合計を計算するプログラムを作成してください。例えば、入力が「123」の場合、1+2+3=6が出力されます。入力終了(EOF)まで読み込みを続けてください。

期待される結果

入力例1:

テキストを入力してください(Ctrl+Dで終了): 
123
Ctrl+D

出力例1:

数字文字の合計: 6

入力例2:

テキストを入力してください(Ctrl+Dで終了): 
abc123xyz
45.67
end890
Ctrl+D

出力例2:

数字文字の合計: 39

ヒント

  • 文字'0'〜'9'は、それぞれ0〜9の数値に変換する必要があります
  • 文字から数値への変換は「文字 - '0'」で行えます(例:'3' - '0' = 3)
  • getchar関数は標準入力から1文字読み込み、EOFに達したら特殊な値EOFを返します
  • Windows環境ではCtrl+Z、Unix/Linux環境ではCtrl+Dを入力するとEOFとして認識されます
  • 入力された文字が数字文字かどうかは、「ch >= '0' && ch <= '9'」で判定できます

演習問題10-3:フィボナッチ数列

問題の説明

非負の整数nに対するフィボナッチ数を返す関数fibonacci(n)を再帰的に実装してください。フィボナッチ数列は次のように定義されます。 - F(0) = 0 - F(1) = 1 - F(n) = F(n-1) + F(n-2) (n ≥ 2)

期待される結果

入力例1:

非負整数nを入力してください: 7

出力例1:

F(7) = 13

フィボナッチ数列(最初の5項):
F(0) = 0
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3

入力例2:

非負整数nを入力してください: 10

出力例2:

F(10) = 55

フィボナッチ数列(最初の5項):
F(0) = 0
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3

ヒント

  • 再帰関数では「基底ケース」(再帰が終了する条件)を明確に定義することが重要です
  • フィボナッチ数列の定義をそのままコードに変換しましょう
  • 再帰関数は直感的に記述できますが、大きな値のnに対しては計算時間が長くなることに注意してください
  • 負の入力に対してはエラーメッセージを表示するようにしましょう
  • フィボナッチ数列の最初の10項は: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 です
  • F(5)の計算過程を図示すると以下のようになります:
    F(5) = F(4) + F(3)
         = [F(3) + F(2)] + [F(2) + F(1)]
         = [F(2) + F(1) + F(2)] + [F(1) + F(0)]
         = [F(1) + F(0) + F(1) + F(1) + F(0)] + [1 + 0]
         = [1 + 0 + 1 + 1 + 0] + 1
         = 3 + 2
         = 5
    
  • 再帰関数では「コールスタック」と呼ばれるメモリ領域が使われます。関数が呼び出されるたびに、戻り先の情報などがスタックに積まれていきます
  • デバッグ時は再帰呼び出しの深さを追跡するために、各呼び出し時に値を表示させるとよいでしょう
  • 再帰関数の代わりにループを使った実装も可能です(効率は良くなりますが、コードの直感性は低下します)
  • main関数の基本的な構造は以下のようになります:
    int main(void)
    {
        int n;
    
        // 整数値の入力
        printf("非負整数nを入力してください: ");
        scanf("%d", &n);
    
        // 入力値のチェック
        if (n < 0) {
            printf("エラー: 負の数は入力できません。\n");
            return 1;
        }
    
        // フィボナッチ数を計算して結果を表示
        printf("F(%d) = %d\n", n, fibonacci(n));
    
        // フィボナッチ数列の最初の5項を表示
        printf("\nフィボナッチ数列(最初の5項):\n");
        for (int i = 0; i < 5; i++) {
            printf("F(%d) = %d\n", i, fibonacci(i));
        }
    
        return 0;
    }
    

演習問題10-4:アルファベット文字のカウント

問題の説明

標準入力から文字を読み込み、小文字アルファベット('a'〜'z')と大文字アルファベット('A'〜'Z')の出現回数をそれぞれカウントして表示するプログラムを作成してください。EOFまで読み込みを続けてください。

期待される結果

入力例1:

テキストを入力してください(Ctrl+Dで終了): 
Hello, World!
This is a test.
Ctrl+D

出力例1:

小文字アルファベットの出現回数:
'a': 1
'd': 1
'e': 2
'h': 1
'i': 1
'l': 3
'o': 2
'r': 1
's': 2
't': 2

大文字アルファベットの出現回数:
'H': 1
'T': 1
'W': 1

ヒント

  • 文字'a'〜'z'と'A'〜'Z'は、それぞれ連続した文字コードを持っています
  • 文字から配列のインデックスへの変換は「文字 - 基準文字」で行えます(例:'e' - 'a' = 4)
  • 2つの配列(小文字用と大文字用)を用意して、各文字の出現回数をカウントします
  • 結果表示の際は、出現回数が0の文字は表示しないようにするとすっきりします
  • Windows環境ではCtrl+Z、Unix/Linux環境ではCtrl+Dを入力するとEOFとして認識されます

プログラミング言語の歴史

このドキュメントでは、1804年から2022年までに登場した主要なプログラミング言語とコンピュータ技術の発展について時系列で紹介します。各言語について、英語名、日本語の読み(カタカナ)、および簡単な概要を記載しています。

目次

初期のコンピューティング(1800年代~1940年代)

年代 言語/出来事 概要
1804年 Jacquard Loom
(ジャカード織機)
ジョゼフ・マリー・ジャカール(Joseph Marie Jacquard)によって発明された、パンチカードで織物のパターンを制御する装置で、最初のプログラム可能な機械とされる。
1842–1843年 Ada Lovelace's Algorithm
(エイダ・ラブレスのアルゴリズム)
エイダ・ラブレス(Ada Lovelace)がチャールズ・バベッジ(Charles Babbage)の解析機関のためにベルヌーイ数を計算するプログラムを記述し、世界初のコンピュータプログラムとされる。
1940年代 Machine Language
(機械語)
コンピュータのハードウェアを直接制御するための2進数コードで、実行速度は速いが、プログラミングは困難。
1944年 Harvard Mark I
(ハーバード・マークI)
ハワード・エイケン(Howard Aiken)が設計し、IBMが製造した、パンチテープでプログラムを入力する初期の自動計算機。
1945年 Plankalkül
(プランカルキュール)
コンラート・ツーゼ(Konrad Zuse)が設計した、配列や条件文などの構造をサポートする、コンピュータ用に設計された最初の高級プログラミング言語。

初期のプログラミング言語(1950年代~1960年代)

年代 言語/出来事 概要
1957年 FORTRAN
(フォートラン)
IBMによって開発された、科学技術計算向けの最初の広く使用された高級プログラミング言語。
1958年 LISP
(リスプ)
ジョン・マッカーシー(John McCarthy)によって開発された、人工知能研究向けの関数型プログラミング言語。
1959年 COBOL
(コボル)
グレース・ホッパー(Grace Hopper)らによって開発された、商業データ処理向けの言語で、可読性と移植性を重視。
1960年 ALGOL 60
(アルゴル・シックスティ)
ブロック構造や再帰の概念を導入し、後の多くのプログラミング言語に影響を与えた。
1962年 Simula
(シミュラ)
オーレ=ヨハン・ダール(Ole-Johan Dahl)とクリステン・ニガード(Kristen Nygaard)によって開発された、最初のオブジェクト指向プログラミング言語。
1964年 BASIC
(ベーシック)
ジョン・ケメニー(John Kemeny)とトーマス・カーツ(Thomas Kurtz)によって開発された、教育目的で簡単に学べるプログラミング言語。
1964年 PL/I
(ピーエルワン)
IBMによって開発された、FORTRANとCOBOLの特性を組み合わせた、科学計算と商業データ処理向けの言語。
1969年 B
(ビー)
ケン・トンプソン(Ken Thompson)によって開発された、C言語の前身となる言語で、主にシステムプログラミングに使用された。

構造化プログラミングの時代(1970年代~1980年代前半)

年代 言語/出来事 概要
1970年 Pascal
(パスカル)
ニクラウス・ヴィルト(Niklaus Wirth)によって設計された、構造化プログラミングを強調した教育向けの言語。
1972年 C
(シー)
デニス・リッチー(Dennis Ritchie)によって開発された、高級言語の柔軟性と低級言語の効率性を兼ね備えた言語で、システムプログラミングに広く使用された。
1972年 Prolog
(プロログ)
アラン・コルメラウアー(Alain Colmerauer)らによって開発された、人工知能分野で広く使用される論理プログラミング言語。
1973年 ML
(エムエル)
ロビン・ミルナー(Robin Milner)によって開発された、多相型システムを導入した関数型プログラミング言語。
1977年 Ada
(エイダ)
アメリカ国防総省の委託で開発された、信頼性と保守性を重視した組み込みおよびリアルタイムシステム向けの言語。
1979年 Modula-2
(モジュラ・ツー)
ニクラウス・ヴィルト(Niklaus Wirth)によって開発された、モジュール化プログラミングを強調したPascalの後継言語。

オブジェクト指向と広域利用の時代(1980年代後半~1990年代)

年代 言語/出来事 概要
1980年代 オブジェクト指向プログラミングの興隆 SmalltalkやC++などの言語がクラスとオブジェクトの概念を導入し、ソフトウェアのモジュール化と再利用性を促進した。
1983年 C++
(シープラスプラス)
ビャーネ・ストロヴストルップ(Bjarne Stroustrup)によって開発された、C言語にオブジェクト指向の特性を追加した言語。
1984年 MATLAB
(マトラボ)
クリーブ・モル(Cleve Moler)によって開発された、行列演算と数値計算を重視した言語で、工学や科学の分野で広く使用されている。
1987年 Perl
(パール)
ラリー・ウォール(Larry Wall)によって開発された、テキスト処理とシステム管理に優れた多目的スクリプト言語。
1990年 Haskell
(ハスケル)
複数の研究機関によって共同開発された、純粋関数型プログラミング言語で、型安全性と遅延評価を強調。
1991年 Python
(パイソン)
グイド・ヴァンロッサム(Guido van Rossum)によって開発された、コードの可読性と簡潔さを重視し、データサイエンスや人工知能など多くの分野で使用されている。
1991年 Visual Basic
(ビジュアル・ベーシック)
マイクロソフトによって開発された、Windowsアプリケーションの開発を簡素化するグラフィカルプログラミング言語。
1993年 R
(アール)
統計分析とデータ可視化に特化した言語で、学術界やデータサイエンティストに広く支持されている。
1993年 Lua
(ルア)
ブラジルのカトリカ大学によって開発された、軽量なスクリプト言語で、ゲーム開発や組み込みシステムで広く使用されている。
1995年 Ruby
(ルビー)
松本行弘(Yukihiro Matsumoto)によって設計された、Perlの実用性とSmalltalkのオブジェクト指向特性を組み合わせた、迅速な開発に適した言語。
1995年 Java
(ジャバ)
サン・マイクロシステムズ(Sun Microsystems)によって開発された、クロスプラットフォーム特性を持ち、企業向けアプリケーションやAndroid開発で広く使用されている。
1995年 JavaScript
(ジャバスクリプト)
ブレンダン・アイク(Brendan Eich)によって開発された、ウェブページのインタラクティブ性を高めるための主要な言語で、動的なウェブの発展を促進した。
1995年 PHP
(ピーエイチピー)
元々ウェブ開発用に作られたスクリプト言語で、学習が容易で、サーバーサイド開発に広く使用されている。
1995年 Delphi
(デルファイ)
ボーランド(Borland)によって開発された、Object Pascalに基づく迅速なアプリケーション開発に適した言語。

現代のプログラミング言語(2000年代~2010年代)

年代 言語/出来事 概要
2000年 C#
(シーシャープ)
マイクロソフトによって開発された、C++の性能とJavaの簡潔さを組み合わせた、.NETプラットフォーム向けのオブジェクト指向言語。
2003年 Groovy
(グルーヴィー)
Javaプラットフォーム上で動作する動的言語で、Javaの構文を簡素化し、スクリプトや迅速な開発に適している。
2004年 Scala
(スカラ)
オブジェクト指向と関数型プログラミングの特性を融合し、JVM上で動作する、並行処理やビッグデータ処理に適した言語。
2005年 F#
(エフシャープ)
マイクロソフトによって開発された、.NETプラットフォーム上で動作する関数型プログラミング言語で、数学的計算やデータ分析に適している。
2007年 Clojure
(クロージャ)
リッチ・ヒッキー(Rich Hickey)によって開発された、Lispの現代的な実装で、JVM上で動作し、不変データ構造と並行プログラミングを重視している。
2009年 Go
(ゴー)
Googleによって開発された、シンプルな構文と高い並行処理能力を持つ言語で、システムプログラミングやクラウドサービスに適している。
2010年 Rust
(ラスト)
Mozillaによって開発された、メモリ安全性と高性能を兼ね備えたシステムプログラミング言語で、C/C++の代替として注目されている。
2010年 Whiley
(ワイリー)
デイヴィッド・パーカー(David Parker)によって開発された、形式的検証をサポートする言語で、ソフトウェアの信頼性と安全性を向上させることを目的としている。
2011年 Kotlin
(コトリン)
JetBrainsによって開発された、Javaと互換性のある現代的な言語で、Android公式サポート言語として採用されている。
2012年 Julia
(ジュリア)
高性能な数値計算を目的として開発された言語で、動的言語の使いやすさと静的言語の性能を兼ね備えている。
2012年 TypeScript
(タイプスクリプト)
マイクロソフトによって開発された、JavaScriptのスーパーセットで、静的型付けを導入し、大規模開発での保守性を向上させている。
2014年 Swift
(スウィフト)
Appleによって開発された、iOSおよびmacOSアプリケーション開発向けのモダンな言語で、Objective-Cの後継として位置付けられている。
2019年 Nim
(ニム)
Pythonの簡潔さとCの性能を融合した言語で、システムプログラミングやクロスプラットフォーム開発に適している。
2019年 V
(ブイ)
高速なコンパイルと簡潔な構文を特徴とする言語で、高性能なアプリケーションの構築に適している。
2019年 Bosque
(ボスケ)
マーク・マロン(Mark Marron)によって開発された、コードの複雑性を排除し、人間と機械の両方にとって理解しやすい構造を提供することを目指した言語。
2019年 Ballerina
(バレリーナ)
サンジーヴァ・ウィーラワラナ(Sanjiva Weerawarana)らによって開発された、クラウドネイティブアプリケーションと統合サービス向けに設計されたオープンソース言語。

最新のプログラミング言語(2020年代~)

年代 言語/出来事 概要
2021年 Carbon
(カーボン)
チャンドラー・カルース(Chandler Carruth)によって開発された、C++の後継を目指す実験的な言語で、既存のC++コードベースとの統合や移行を容易にすることを目的としている。
2022年 Vale
(ヴェイル)
エヴァン・オヴァディア(Evan Ovadia)によって開発された、メモリ安全性と性能を重視したシステムプログラミング言語で、ガベージコレクションなしでの安全なメモリ管理を実現している。