このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

教養としてのC言語プログラミング入門は成立するのか

比較的に短い期間のなかで、プログラミングの基本的なことを勉強する(させる)とき、どんな話題や課題を選ぶべきでしょうか?

ここでの「基本的」とは、「将来、もっと発展的なことを学ぶための土台を作る」という意味ではありません。「入門者でも手が届く典型的な事項」という程度の意味です。教養としてプログラミングをかじるときに、どこをかじるべきか? という問〈とい〉と言ってもいいでしょう。

使うプログラミング言語はC言語です -- と、ここでズッコケる人がいそう。そもそも、入門になんでC言語使うんだよ!? って話は当然あるんですが、まー、そこは「なんらかの事情でC言語しかない」前提にします。実は、この前提のテキストと問題集があったのですが、それを見て僕が感じたことを雑駁に書きます。

内容:

パラメータの与え方:scanf()を使う

まず、どんな種類の処理を扱うか? C言語による文字列処理/テキスト処理はだいぶ難しいですよね。となると、やれることは数の計算になってしまいます。ユークリッドの互除法とか、数値データの平均値を求めるとか、ですね。

ここでは、引き算と大小比較で割り算の余りを求めるコードフラグメントを例とします。

// nとkは整数変数
int r = n;
while (r >= k) {
    r = r - k;
}

この例題を、n = 249, k = 83 に関して実行したいなら、次のコードになるでしょう。

// exam-1.c
#include <stdio.h>

int main()
{
    int n = 249, k = 83;

    int r = n;
    while (r >= k) {
        r = r - k;
    }
    printf("結果: %d\n", r);

    return 0;
}

これを、n = 175, k = 47 に対して計算するために、ソースファイルをコピーして一部書き換えはさすがにやりたくない(やらせたくない)ですよね。そこで登場するのがscanf()です。

// exam-2.c
#include <stdio.h>

int main()
{
    int n, k;
    printf("整数を入力してください > ");
    scanf("%d", &n);
    printf("もうひとつ整数を入力してください > ");
    scanf("%d", &k);

    int r = n;
    while (r >= k) {
        r = r - k;
    }
    printf("結果: %d\n", r);

    return 0;
}
$ gcc ./exam-2.c

$ ./a.exe
整数を入力してください > 175
もうひとつ整数を入力してください > 47
結果: 34

$ 

scanf()でいいのか?

現実的なプログラミングでscanf()を使うことはあまりないでしょう。むしろ、使用を(暗黙的にでも)禁止されていることがありそうです。そんな問題含みの関数を使う(使わせる)のはいかがなものか? とは思います。

ですが、次のような理由でscanf()が消極的に支持されているのでしょう。

  1. scanf()の使用を禁止すると、代替手段のための知識やスキルが必要になってしまう。
  2. scanf()を安全に使うことは注意すれば出来るので、禁止するほどのことはない。
  3. 教養としてのプログラミングなら、そのへんのこと(不注意によるバッファ・オーバーランとか)に神経質になる必要はない。

これらの意見に反論するのは難しいので、僕もscanf()を排除する気はないのですが、それにしてもscanf()は使い勝手が悪い。

上の例で、不適切な値や数値と解釈できない入力をしてしまうと、予測できない奇妙な動作をします。(k = -47 に対する結果はscanf()のせいじゃないです。整数が循環してるせいですが、「循環を巡る話:螺旋、時計、2の補数表現、角度算、リング」を参照。)

$ ./a.exe
整数を入力してください > 175
もうひとつ整数を入力してください > -47
結果: -2147483635

$ ./a
整数を入力してください > x
もうひとつ整数を入力してください > 結果: 32

$

不適切な入力をキチンとハンドルするには、値の検査だけでなく、scanf()の戻り値をチェックしたり、標準入力のフラッシュをしたりする必要があるので、けっこう面倒なことになります。面倒を避けるなら、「入力が正しくないときのことは考えないことにしよう」とかでお茶を濁すしかありません。

コマンドライン引数

「基本部分だけを教える」であっても、コマンドライン引数は使ったほうがいいと僕は思います。

先の例でプロンプトを出して入力してもらうパラメータを、コマンドライン上で指定するように変更すると例えば次のようになります。

// exam-3.c
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc < 3) {
        fprintf(stderr, "引数が少なすぎます.\n");
        return 1;
    }
    int n = atoi(argv[1]);
    int k = atoi(argv[2]);

    int r = n;
    while (r >= k) {
        r = r - k;
    }
    printf("結果: %d\n", r);

    return 0;
}

*argv[] の先頭のアスタリスクがオマジナイになってしまいますが、それを言うなら、scanf()の第2引数に入っているアンパサンド(&nとか)だってオマジナイです。( char *argv[] や &n を理解するにはアドレスやポインターを知る必要がありますが、これはけっこうハードルが高い。)

実際に使われているコマンドラインコマンドでは、プロンプトを出してパラメータ入力を求めるプログラムはあんまりなくて、コマンドライン上にパラメータを指定します。この事実を根拠に、対話的プロンプトはやめてコマンドラインだけでもいいような気がします。

標準入力の利用

例えば、次のような課題を考えてみます。

  • ファイルに整数値の列が入っているとして、それらの整数値の平均値を求めよ。

データが入っているファイルを名前で扱うなら、ファイルのオープン/リード/クローズという一連の手順が必要です。この手順を「基本」のなかに含めていいかも知れませんが、ファイルハンドルやファイルポインタの概念は割と難しいものです。

データ入力の口は、とりあえずは標準入力でいいと思います。

// exam-4.c
#include <stdio.h>

int main()
{

    int x; // 個々のデータ(整数値)
    int n = 0; // データの個数
    float sum = 0.0; // データの総和、後でnで割り算する

    int rscan; // scanf() の戻り値
    while (1) {
        rscan = scanf("%d", &x);
        if (rscan != 1) break;
        n++;
        sum += x;
    }
    if (n == 0) {
        fprintf(stderr, "データがありません.\n");
        return 1;
    } else {
        printf("結果: %f\n", sum/n); // 注意: %d だとダメ
        return 0;
    }
}

標準入力のいいところは、ファイルからでもキーボードからでもデータを入力できることです。

$ gcc exam-4.c

$ ./a.exe
1 2 3 4
5 6
^D
結果: 3.500000

$ cat nums.txt
1 2 3 4
5 6

$ ./a.exe < nums.txt
結果: 3.500000

$

標準入力は、プログラム自身が対話的プロンプトやコマンドライン引数からファイル名を取る方法よりむしろ優れた方法と言えるでしょう。

バッファ

C言語で頭が痛いのがバッファの扱いです。前の問題と同じくファイルやキーボードからデータを入力するとして、平均からの差(偏差)を求める課題を考えてみます。出力も数値(整数とは限らない)の列になります。

平均の場合は、数値の列を頭からお尻に向かって一回なめる過程で計算が出来ました。しかし今度の問題では、読み取った数値達を憶えておかなくてはなりません。憶えておく場所がバッファですが、これは難しい。

数値の個数は前もって予測できないので、動的にバッファを取る必要がありますが、「教養としての」「基本」で、動的バッファ確保をやるんかい? 「大きめのバッファを取る」が妥協点でしょうね。

とりあえず、バッファに保存するだけのコード。

// exam-5.c
#include <stdio.h>

int main()
{
    int numbers[100];
    int x; // 個々のデータ(整数値)
    int n = 0; // データの個数

    int i;
    int rscan; // scanf() の戻り値
    for (i = 0; i < 100; i++) {
        rscan = scanf("%d", &x);
        if (rscan != 1) break;
        numbers[i] = x;
    }
    n = i;
    // バッファ内容をそのまま出力
    printf("%d個の数値を読み込みました.\n", n);
    for (i = 0; i < n; i++) {
        printf("[%d] %d\n", i, numbers[i]);
    }

    return 0;
}

これは100個までは保存して、その後は捨てます(「裸の100はやめろ」とか言われそうですが、まぁまぁ)。現実的には、元データが100個を越える数値を含むときはエラーにしたほうが好ましいでしょうが、問題を「最初の100個まで」とすればこれでも許されるでしょう。

main()以外の関数

プログラムが多少長くなったら関数に切り出したくなりますし、切り出すべきです。でも、関数呼び出し/引数渡しはけっこう難しい。main()内にすべて書く、で済ませるのもひとつの判断だと思います。

前節の問題でも、平均値と偏差を求める部分をmain()内に混ぜ込んでしまうことはできます。

// exam-6.c
#include <stdio.h>

int main()
{
    int numbers[100];
    int x; // 個々のデータ(整数値)
    int n = 0; // データの個数
    float sum = 0.0; // データの総和

    int i;
    int rscan; // scanf() の戻り値
    for (i = 0; i < 100; i++) {
        rscan = scanf("%d", &x);
        if (rscan != 1) break;
        numbers[i] = x;
        sum += x;
    }
    n = i;
    float mean = sum/n; // 平均値
    for (i = 0; i < n; i++) {
        printf("%f\n", numbers[i] - mean);
    }

    return 0;
}

これくらいなら、そんなにゴチャゴチャではありませんが、配列の平均値を求める関数は独立させたい感じはします。

でもね、C言語だと配列は長さを保持してないので、float mean(int cout, float numbers[]) のような関数が必要なんですよね。int main(int argc, char *argv[]) をちゃんと説明しておけば大丈夫かな? って、char *argv[] を“ちゃんと説明する”って、すごく大変なんですけど、、、

言わないお約束ではあったんですが、冒頭で出した「入門になんでC言語使うんだよ!?」という疑問はどうしても頭をもたげますね。

考慮すべきこと

適切な妥協点を見出すのは、ほんとに難しいなー、と感じます。気付いた点や考慮すべきと思える点を順不同でズラズラと書き並べます。

  1. リテラル文字列はいいが、処理対象としての文字列(ヌル文字で終わるキャラクタ配列)は難しい。
  2. したがって、処理対象は数値に限られるだろう。
  3. つまり、データ型としてはintとfloatだけ。(longやdoubleも使うと混乱しそう。)
  4. scanf()の使用は避けられないと思う。
  5. printf(), scanf() のフォーマット指定は %d, %f, %s の3つだけでいいだろう。
  6. scanf()の引数に現れる &n のアンパサンドはオマジナイでも致し方ない。
  7. どうせオマジナイが避けられないなら、scanf()のフォーマット指定を %10s とかのオマジナイ(長さ付き)にしたほうがいいかも。
  8. 固定長のバッファは必要だが、動的バッファは難しいから諦める。
  9. 固定長のバッファを使い、バッファに入らない残余は捨てるかエラーとする。
  10. エラーハンドリングは難しい問題だから、深入りはしない。不正な入力に対しては変なことが起きてもしょうがない、とする。
  11. エラーメッセージはstderrに書く、くらいは注意してもいいと思う(これもオマジナイになるけど)。
  12. コマンドライン引数の取り方は学んで使ったほうがいい。char *argv[](または char **argv)はオマジナイ。
  13. コマンドライン引数が使えれば、主要なデータを標準入力から取り、オプショナルなパラメータをコマンドライン引数から取るようなことが出来る。対話的プロンプトによるパラメータ入力では、標準入力からのデータ入力と両立しない。
  14. (main()以外の)関数を導入したほうがいいと思うが、配列やポインターが引数に渡るメカニズムは難しい。引数を、数値と配列の名前に限定するといいかも知れない。

最後に身も蓋もないことを言ってしまうと:

  • 可能ならば、C言語は避けるべきである。