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
}
int
やdouble
であればatoi
やatof
で変換できます。
このプログラムは以下のように実行できます。
$ ./a.out runge_kutta 123 0.5
method: runge_kutta
seed: 123
v0: 0.500000
argv
で引数を受け付けてプログラムの挙動を制御することは極めて一般的です。
ファイルの読み書き:fopen, fclose, fprintf, fscanf, etc¶
さて、上ではリダイレクトを使ってファイルに対する読み書きを行ってきましたが、
それは標準出力を無理やり変更したものでした。
よりキチンとファイルに出力するためには、ファイルを操作する関数を使います。fopen
やfscanf
のように、先頭に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
です。
もしファイルのオープンに失敗していると、fp
はNULL
になります。
ここで、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);
}
$ ./a.out fuga.txt
のようにして、fuga.txt
に情報を書き込むことができます。
ファイルへの入出力には他にもいろいろ方式があります。とくに、上記はテキストの読み書きでしたが、数値を実際に保存するときはバイナリとして
保存します。これはwrite binary: wb
のようなモードを使います。
そして、fread
やfwrite
といった関数を使います。
個人的なオススメ¶
さて、色々紹介して疲れたと思いますので、実際にこれから講義や研究でコードを書く上でどれを使えばいいかについて、以下に松井のオススメ例を示します。
- 短い入力を受け付けるときは、
argv
で受け取って文字列を解析する。文字列を読みたい場合はそのままだし、数値であればatoi
やatof
だけで読み込めると楽(というか、argv
で受け付けるべきなのはそういう短いものだけ) - 長い入力(宿題でやった時系列データとか)を受け付けるときは、
fread
やfopen
で開く。より正確には、argv
でファイル名を受け取り、それをfread
などで開いて読み込む。 - とりあえず簡単に出力したいときは、リダイレクトを使う
$ ./a.out > out.txt
- ちゃんと出力するときは、
fwrite
やfopen
で保存する
やってみよう(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=4
でout.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.h
をinclude
すると、様々な数学関数が使えます。
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.h
をinclude
すると文字列に対する様々な関数が使えることは既に述べました。
それ以外にも、ローレベルで直接配列をコピーするものがあります。
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年度になったら消える可能性があります。 なので、手元においておきたいコードはバックアップをとっておいてください。