コンテンツにスキップ

Week 7 (2020/11/12)

今日やること

  • バージョン管理(Git/GitHub)
  • 入出力のまとめ
  • ライブラリツアー

まずはじめに

  • Googleツアーについて

バージョン管理(Git/GitHub)

バージョン管理について勉強しましょう。独立したページにまとめました。

入出力のまとめ

Cには様々な入出力方法があります。これまでの講義ではprintfによる書式付き表示、putchar/getcharによる一単語の入出力、argvによる引数受付、を学びました。 Cには他にも様々な入出力関数があります。ここでは有名どころをいくつか紹介し、簡単にまとめておきましょう。

書式付き入出力:printf, scanf

printfは書式付きの表示関数です。すなわち、何を表示するかを文字列で指定して、その中に変数を埋め込むことができます(これを書式文字列といいます)。 その内容を、標準出力に表示します。 これは既にみなさんよく知っていますね。

printf("ID:%d\n", 13);          // 表示する書式文字列を渡し、その中に変数を埋め込む
リダイレクトを思い出すと、この内容を標準出力ではなくファイルに書き出すことも簡単にできます。上記をコンパイルして以下を実行してみましょう。
$ ./a.out
ID:13                  <- 標準出力に表示。すなわちターミナルに表示。
$ ./a.out > hoge.txt   <- リダイレクトでファイルに書き出す。hoge.txtというファイルが出来てその中にID:13が書かれる。
                       <- ターミナルには表示されない。
これが最も簡単な「書き出し」です。たとえば実験を行って結果を保存したいときに、一番簡単なのはprintfしてそれをリダイレクトで吐き出すことです。

さて、printfはすなわち変数書式文字列に埋め込み標準出力に表示するものでした。 scanfはこの逆の操作をします。 すなわち、標準入力からのストリームを、書式文字列で受け取り、変数に与えます。

int n;
scanf("ID:%d", &n);
printf("n is %d\n", n);  // 確認
ここでは書式文字列に対応する変数として&nすなわちintのアドレスを与えています。 上記を実行すると、端末が入力を受け付ける待機状態になります。そこでID:13のように書式文字列に沿って打ち込むと、 nには13がセットされます。

コラム

なぜここではnのアドレスを渡す必要があるのでしょうか? Cの基本ルールとして、関数が呼び出し元の変数を編集するためにはポインタを経由しないといけなかったことを思い出しましょう。 ここではアドレスをポインタ経由で渡すので、scanf関数は呼び出し元のnを変更できます。

ここで注意として、入力でミスがあると正しく値がセットされません。例えばID :13と入力する(空白を打ってしまうミス) と、その入力は書式文字列と合わないので、nが正しく読み込まれません。 また、書式のルールはprintfに似ているのですが色々違います。例えば、printfでは%fでdoubleもfloatも表示できましたが、 scanfでは%fはfloat用で、doubleには%lfを使います。 以上により、scanfは簡単で便利なのですが、非常にミスが起きやすいので注意が必要です。

標準出力、リダイレクト、パイプ

さて、scanfは標準入力からストリームを読み込みます。すなわち、キーボードからの入力を受け付けます。 標準入力から受け付けるものは、以下の二つに変更出来たことを思い出しましょう:(1) リダイレクトを使ってファイルからの読み込み、(2) パイプを使って実行時に渡す

リダイレクトでファイルから読み込む場合を考えましょう。上のprintfのときに作った、ID:13とだけ書いてあるファイルhoge.txtを準備しましょう。 そのうえで、次を実行します

$ ./a.out < hoge.txt
これにより、読み込みは標準入力(キーボード)からではなく、hoge.txtからに変更されます。こうすることで、キーボード入力ではなく、ファイルから入力を受け付けることができます。

次にパイプについて思い出しましょう。

$ echo ID:13 | ./a.out
パイプは、直前のコマンドの標準出力を、自分の標準入力にします。上の例ではechoにより「ID:13」という単語列がechoの標準出力に吐き出されます。これがパイプを通してa.outの標準入力に 入ります。結論として、echoと組み合わせることで、コマンドの実行時にキーボード入力と同じものを渡すことができます。

これはscanfに限らず、標準入力(キーボード)から入力を受けつけるものは、上記のようにリダイレクトとパイプを使うことができます。

一単語の入出力:putchar, getchar

一単語の入出力は week3ですでにやりましたね。以下のようになります。

int c = 'a';  // "a"ではないことに注意
putchar(c);
上記の表記により、標準出力(ターミナル)に一語を書き出すことができます。これは単純な一語の出力なので、改行(\n)もしません。

コラム

'a'のような文字にはcharを使ってきましたが、charは単なる小さな整数なので、intで表現しても問題ありません。putcharの引数、およびgetcharの返り値の型はintです。

また、標準入力(キーボード)から一語を読み込むときは次のようにすることを既に習いました。

int ret;
ret = getchar();
このgetcharも、リダイレクトやパイプを使うことができることも既に習った通りです。 getcharは一語ずつ読み込むので動作が簡便ですが、たとえば三桁の数字を入力しようと「112 Enter」のようにしても、retには'1'だけが入る点に注意してください。

引数からの入力:argv

次に、引数からの入力について復習しましょう。コマンドを実行するとき、その後ろに空白を伴って引数を入力することができます。

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {
    if (argc != 4) {
        printf("Error. #arg must be 3: method, seed, and v0\n");
        return -1;
    }
    char *method = argv[1];
    int seed = atoi(argv[2]);
    double v0 = atof(argv[3]);

    printf("method: %s\nseed: %d\nv0: %f\n", method, seed, v0);

    // do something here
}
例えば上のプログラムは、引数の一つ目に手法名、二つ目に乱数の種、三つ目に計算の初期値を入れるようなプログラムです。 それぞれの値は、文字列であればそのまま使えますし、intdoubleであればatoiatofで変換できます。 このプログラムは以下のように実行できます。
$ ./a.out runge_kutta 123 0.5
method: runge_kutta
seed: 123
v0: 0.500000
このように、argvで引数を受け付けてプログラムの挙動を制御することは極めて一般的です。

ファイルの読み書き:fopen, fclose, fprintf, fscanf, etc

さて、上ではリダイレクトを使ってファイルに対する読み書きを行ってきましたが、 それは標準出力を無理やり変更したものでした。 よりキチンとファイルに出力するためには、ファイルを操作する関数を使います。fopenfscanfのように、先頭にf: fileがついているものです。 ファイルの読み書きについてはソフトウェア2で詳しくやりますので深入りしませんが、例えば次のようにします。

char filename[] = "hoge.txt";

// ここからがファイル読み込みの定型文
FILE *fp;
fp = fopen(filename, "r");  // "read"
if (fp == NULL) {
    printf("Cannot open %s\n", filename);
    return -1;
}

// ここでfpを経由してファイルからデータを読み込む
int n;
fscanf(fp, "ID:%d", &n);
printf("n is %d\n", n);  // 確認

// ここでfpの後始末
fclose(fp);

ここで、FILE *fpとは、ファイルの操作をつかさどるファイルポインタというものです。 fopenとう関数でファイルを開きます。その「開いているファイルを指し示す印」がfpです。 もしファイルのオープンに失敗していると、fpNULLになります。 ここで、fopenの二つ目の引数は、どういうモードでファイルを開くかを決定します。 読み込むだけのときはread: "r"に、書き込むときはwrite: "w"に、追記モードにするときはappend: "a"にします。

ファイルのオープンに成功した場合、fpを経由して、先頭にfがつく関数で読み書きができます。 たとえばfscanf(fp, ...)では、scanfと似たような挙動をしますが、その読み込み元が標準入力(キーボード)ではなくfp、すなわち 今開いたファイルになります。

全ての処理が終ったあとは、ファイルポインタを開放しておくとよいです。それがfcloseです。 上の例では開放しなくてもプログラムが終了しますが、例えば別の関数の中でfopenなどを行った場合は、ちゃんとfcloseしておかないと 問題が発生するかもしれません。

上はファイルを読み込む例でしたが、下は書き込む例です。

#include <stdio.h>

int main (int argc, char *argv[]) {   
    // argvを使い、引数からファイル名をうけとる
    if (argc != 2) {
        printf("Error. #arg must be 1: filename\n");
        return -1;
    }
    char *filename = argv[1];

    // ここからがファイル書き込みの定型文
    FILE *fp;
    fp = fopen(filename, "w");   // "write"
    if (fp == NULL) {
        printf("Cannot open %s\n", filename);
        return -1;
    }

    // ここでfpを経由してファイルにデータを書き込む
    fprintf(fp, "ID:%d", 13);

    // ここでfpの後始末
    fclose(fp);
}
ここでは完全なプログラムの形式にしました。そして、filenameは引数から受け付ける形式にしました。 これにより、$ ./a.out fuga.txt のようにして、fuga.txtに情報を書き込むことができます。

ファイルへの入出力には他にもいろいろ方式があります。とくに、上記はテキストの読み書きでしたが、数値を実際に保存するときはバイナリとして 保存します。これはwrite binary: wbのようなモードを使います。 そして、freadfwriteといった関数を使います。

個人的なオススメ

さて、色々紹介して疲れたと思いますので、実際にこれから講義や研究でコードを書く上でどれを使えばいいかについて、以下に松井のオススメ例を示します。

  • 短い入力を受け付けるときは、argvで受け取って文字列を解析する。文字列を読みたい場合はそのままだし、数値であればatoiatofだけで読み込めると楽(というか、argvで受け付けるべきなのはそういう短いものだけ)
  • 長い入力(宿題でやった時系列データとか)を受け付けるときは、freadfopenで開く。より正確には、argvでファイル名を受け取り、それをfreadなどで開いて読み込む。
  • とりあえず簡単に出力したいときは、リダイレクトを使う $ ./a.out > out.txt
  • ちゃんと出力するときは、fwritefopenで保存する

やってみよう(40分)

上記を写経しましょう

クイズ

  • バージョン管理のクイズで作ったリポジトリに、以下のcompute_squares.cを追加しましょう。
    • 引数から配列長Nとファイル名filenameを受け取る
    • filenameの最初の行には、Nの値を書き込む。
    • 0, 1, 2, .., N-1について、それぞれ二乗する
    • 二乗したそれぞれについて、filenameに対し、一行に一つの数字が入るようにして書き出す
    • 出力例:./compute_squares 4 out.txt
    • こうするとout.txtは以下のようになる
      4
      0
      1
      4
      9
      
  • 上で出力されたファイルを読み込み、さらに整数を一つ受け取り、その整数がout.txtに書かれたものがチェックする関数check_val.cを追加しましょう。これもレポジトリに追加しましょう。
    • 引数からファイル名filenameと整数valを受け取る
    • filenameからNと数列を読み込み、valが含まれているか調べる。
    • 例えば上の出力例の通りN=4out.txtが作られているとします。
    • 出力例;./check_val out.txt 12 <- 含まれない、というなんらかのメッセージを吐く
    • 出力例;./check_val out.txt 9 <- 含まれる、というなんらかのメッセージを吐く
答え
  • 例えば以下
    #include <stdio.h>
    #include <stdlib.h>
    
    int main (int argc, char *argv[]) {   
        // argvを使い、引数からファイル名をうけとる
        if (argc != 3) {
            printf("Error. #arg must be 2: N and filename\n");
            return -1;
        }
        int N = atoi(argv[1]);
        char *filename = argv[2];
    
        // ここからがファイル書き込みの定型文
        FILE *fp;
        fp = fopen(filename, "w");   // "write"
        if (fp == NULL) {
            printf("Cannot open %s\n", filename);
            return -1;
        }
    
        // ここでfpを経由してファイルにデータを書き込む
        fprintf(fp, "%d\n", N);
        for (int n = 0; n < N; ++n) {
            fprintf(fp, "%d\n", n * n);
        }
    
        // ここでfpの後始末
        fclose(fp);
    }
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main (int argc, char *argv[]) {   
        // argvを使い、引数からファイル名をうけとる
        if (argc != 3) {
            printf("Error. #arg must be 2: N and filename\n");
            return -1;
        }
    
        char *filename = argv[1];
        int val = atoi(argv[2]);
    
        // ここからがファイル読み込みの定型文
        FILE *fp;
        fp = fopen(filename, "r");   // "read"
        if (fp == NULL) {
            printf("Cannot open %s\n", filename);
            return -1;
        }
    
        // ここでfpを経由してファイルにデータを読み込む
        int N;
        fscanf(fp, "%d", &N);
        int vals[N];
        for (int n = 0; n < N; ++n) {
            fscanf(fp, "%d", vals + n);  // vals[n]に読み込む
        }
    
        // ここでfpの後始末
        fclose(fp);
    
        // 値のチェック
        printf("N: %d\n", N);
        for (int n = 0; n < N; ++n) {
            printf("%d, ", vals[n]);
        }
        printf("\n");
    
        for (int n = 0; n < N; ++n) {
            if (vals[n] == val) {
                printf("Found the value!: %d\n", val);
                return 0;
            }
        }
        printf("Cannot found the value: %d\n", val);
        return 0;
    }
    

ライブラリツアー

Cでは講義で扱わなかった様々な便利関数があります。それらは適切なファイルをincludeすると使えるようになります。以下に見てみましょう。

数学関数:math.h

math.hincludeすると、様々な数学関数が使えます。 math.hを使う場合、コンパイル時に、gcc -lm main.cのように-lmオプションが必要かもしれません。

printf("PI: %f\n", M_PI);            // 3.141593  円周率が定義されています
printf("cos(PI): %f\n", cos(M_PI));  // -1  コサイン
printf("exp(3): %f\n", exp(3.0));    // 20.085537  exp
printf("pow(3.0, 2.0: %f\n", pow(3.0, 2.0)); // 9.000000  累乗
printf("sqrt(9.0): %f\n", sqrt(9.0));     // 3.000000   ルート
printf("fabs(-2.5): %f\n", fabs(-2.5));   // 2.500000   絶対値

文字判定:ctype.h

ctype.hをインクルードすると、文字に関する様々な関数 が使えるようになります。

// アルファベットかどうかの判定
printf("isalpha('a'): %d\n", isalpha('a'));  // 1024  非0の値になる
printf("isalpha('3'): %d\n", isalpha('3'));  // 0

// 数字かどうかの判定
printf("isdigit('a'): %d\n", isdigit('a'));  // 0
printf("isdigit('3'): %d\n", isdigit('3'));  // 2048  非0の値になる

ローレベルなメモリコピー:string.h

string.hincludeすると文字列に対する様々な関数が使えることは既に述べました。 それ以外にも、ローレベルで直接配列をコピーするものがあります。

char src[] = {'a', 'b', '\0', 'c', 'd'};
char dst1[10];
char dst2[10];

// 文字列のコピー。'\0'が出るところまでをコピー
strcpy(dst1, src);    // 'a', 'b', '\0'

// 文字列に限らない、任意のメモリ分量のコピー
// 三番目の引数はコピーする`char`の個数
memcpy(dst2, src, 5); // 'a', 'b', '\0', 'c', 'd'

警告:assert.h

assert.hには、警告に関する関数が含まれます。 これは、次のように、「必ずある条件を満たしてほしい」ということを示すために使います。

int N = 10;
int a[N];

int n = getchar();
n -= '0';

// assertの中身が真であれば何もしない。偽であればエラーになる。
// この場合、キーボードから入力された文字がN未満の数字であればOK
assert(0 <= n && n < N);   

a[n] = 123;
printf("a[%d]: %d\n", n, a[n]);

乱数、system:stdlib.h

stdlib.hには様々な機能があることを既に勉強してきましたが、 乱数も扱うことができます。 乱数とは、その名の通りランダムな数です。 ここではrand()関数によりランダムなintが返ります。 また、取りうる値の最大値としてRAND_MAXも準備されているため、 それで割ることで疑似的に0 - 1のレンジの乱数も作れます。 ここでsrand(seed)というのは、乱数の種(seed)を与えるものです。 疑似乱数生成器は同じseedに対しては同じ乱数列を与えます。 このseedを同じにすれば、乱数であっても結果が再現できます。

srand(123);
for (int n = 0; n < 4; ++n) {
    printf("%d\n", rand());
}
// 128959393
// 1692901013
// 436085873
// 748533630

for (int n = 0; n < 4; ++n) {
    double r = (double) rand() / RAND_MAX;
    printf("%f\n", r);
}
// 0.361609
// 0.134639
// 0.375968
// 0.259322

また、システムに関する関数も準備されています。 例えばsystem(cmd)関数はcmdをターミナルから実行したものと同じことをする関数です。 これは危険なのであまり使わないほうがいいです。

system("ls");   // ターミナルでlsと打った時とおなじように、現在のディレクトリのファイルを表示

やってみよう(15分)

上記を写経してみましょう

最終課題

最終課題を近いうちに出します。slackでリンクを共有します。期限もその際に提示します。 期限までに解いてください。

また、repl.itのプライベート機能はこの学期限定なので、2021年度になったら消える可能性があります。 なので、手元においておきたいコードはバックアップをとっておいてください。