Week 3 (2021/10/21)¶
今日やること
- 文字列
- 型変換
- 入出力
- printf詳細
まずはじめに¶
- 宿題は締切(前回の宿題の場合は、昨日の深夜23:59)を過ぎてもコミットできますが、得点に反映されないので注意してください。
- 全般的にコメントや質問は歓迎なのでslackにどんどん書いてください。まわりの友人とも是非議論してください。
文字について¶
それでは文字列を勉強していきましょう。これまで、文字を表示するときは
printf("aaa\n");
"aaa\n"
は一体何なのでしょうか?
文字¶
それではまず文字について学びましょう。 c言語では、文字(英数字)は単なる小さな整数です。以下の例を見てみましょう。
char ch1 = 'a';
printf("ascii: %c, integer: %d\n", ch1, ch1); // 'a', 97
文字一つは、シングルクオーテーション('
)で囲まれた英数字一つです。
ダブルクオーテーション("
)ではないので注意しましょう。
これを%c
で表示すると、対応する文字が表示されます。
一方で、char
は単なる整数型なので、これを%d
で表示させると、'a'
に割り振られた整数である97が表示されます。
このように、英数字を0から127までの整数に対応させた仕組みをASCIIコードと言います。
これをピッタリ表現するために、char
型が用いられます。文字を意味する「char」という単語が8ビット整数の型名になっているのは、ここに由来します。
ここで、'a'
と97は全く同じなので、先ほどのch1
と以下のch2
は全く同じです。
char ch2 = 97;
printf("ascii: %c, integer: %d\n", ch2, ch2); // 'a', 97
'A'
は65
です。
また、英単語はアルファベット順に連続した値が割り振られています。すなわち、'B'
は66
です。
文字列¶
次に文字列を勉強しましょう。文字列とは、ダブルクオーテーション("
)で任意個の文字を囲ったものです。
例えば"hello"
といったものです。皆さんはこれまでprintf
の引数に文字列のリテラルを渡すことで文字を表示してきましたね。
文字列とは、実際はchar
の配列です。
char s[6] = "hello";
"hello"
で初期化しています。
文字列は、終わりの記号としてNULL文字である'\0'
を準備する必要があります。
そのため、hello
は5文字でありそれぞれchar
一つで表現されますが、最後にもう一つ'\0'
のためのchar
を用意する必要があり、
結局6つの要素が必要になります。
さて、先ほど'a'
は実は97
という数字と同じだと言いました。同様にNULL文字である'\0'
も実体はただの数字であり、それは0
です。
ややこしいですが、覚えておきましょう。
ちなみに、右辺のリテラルを見れば要素数は確定できるため、s[6]
の6
の数字は実は省略できます。
こちらの方が安全で簡単です。この表記で、必要十分な要素数(この場合6)が確保されます。
char s[] = "hello";
文字列がメモリ上でどのように表されているかを図示してみると次のようになります。
注意として、バックスラッシュ(\
)はwindows環境では上図のように円記号(¥
)で表示されてしまうことがあるので注意してください。
さて、これを様々な形式で表示してみましょう。
printf("%s\n", s); // hello
for(int i = 0; i < 6; ++i) {
printf("%c, ", s[i]);
}
printf("\n"); // h, e, l, l, o, ,
for(int i = 0; i < 6; ++i) {
printf("%d, ", s[i]);
}
printf("\n"); // 104, 101, 108, 108, 111, 0,
for(int i = 0; i < 6; ++i) {
printf("%p, ", &s[i]);
}
printf("\n"); // 0x7ffc84551842, 0x7ffc84551843, 0x7ffc84551844, 0x7ffc84551845, 0x7ffc84551846, 0x7ffc84551847,
printf
中では%s
を使うことで文字列を表示することもできます。
そして、文字列は単なる配列なので、その要素を逐一表示できます。%c
で表示すれば、文字を一つずつ表示できます。
整数だと思って表示すれば、これは単なる整数であることがわかります。
そのアドレスを表示してみれば、char
は1バイトなので、1ずつずれたアドレスが表示されます。
ここで、NULL文字'\0'
は文字として表示すると何も表示さず、その実際の値は0
であることに注意しましょう。
ところで、上図を見ていると、文字列は単なる配列なので、次のように「配列の初期化の方式」 で文字を並べても同じだということがわかります。
char s[] = {'h', 'e', 'l', 'l', 'o', '\0'}; // char s[] = "hello"; と同じ
さて、文字列を宣言するときにもし要素数が多すぎるとどうなるでしょうか?
char s2[8] = "hello"; // {'h', 'e', 'l', 'l', 'o', '\0', '\0', '\0'};
'\0'
で埋められます。これは、配列の初期化のときに学んだ「要素を指定しないと0詰めされる」のそのままの処理です。
このように、char
配列の長さが表現したい文字列より長いことは全く問題ありません。
一方、要素数が少なすぎるとどうなるでしょうか?
char s3[3] = "hello"; // {'h', 'e', 'l'};
'\0'
が無いため、正しい文字列になっていません。
よって、このようなことをしてはいけません。
配列と文字列¶
次に、配列と文字列の性質について勉強していきましょう。 まず初期化についてです。char型配列にイコールで文字列リテラルを割り着ける作業は、変数の初期化でのみ有効です。 変数にあらためて代入するときは、イコールは使えません。
char s1[10];
s1 = "abc"; // 出来ない
int a[3];
a = {1, 2, 3}; // 出来ない
int a[3] = {1, 2, 3};
printf("%p %p\n", a, &a[0]); // 同じになる。例えば 0x7fffd6711a90 0x7fffd6711a90
&
演算子をつけていないことに注意)と、
配列の最初の要素のアドレスである&a[0]
は同じ値になります。
これはちょっと謎だと思いますよね。
これについて、ポインタを勉強するときに再度確認します。
以上を考えると、文字列に対して==
を用いた一致判定もできないことがわかります。
char s1[] = "abc";
char s2[] = "abc";
if (s1 == s2) { ... } // 想定した挙動にならない。常に偽になる
int a[3] = {1, 2, 3};
int b[3] = {1, 2, 3};
if (a == b) { ... } // 想定した挙動にならない。常に偽になる
次に、「型、あるいは配列について、使用しているバイト数」を取得する演算子sizeof
を見てみましょう。
printf("%lu\n", sizeof(char)); // 1
printf("%lu\n", sizeof(int)); // 4
sizeof
は型を引数にとり、その型の変数一つが使用するバイト数を返します。
ちなみにsizeof
の返り値の型はunsigned long
です。
ここでは%d
で表示しても表示できますが、型が違うよと警告を受けます。
ここでは、long
を意味する%l
、さらにunsigned
を意味する%u
を
組み合わせて%lu
を用いると警告が出ません。
ここでsizeof
のもう一つの機能として、配列名を引数にとった場合、その配列が使用するメモリ領域の合計バイト数を返します。
int a[] = {1, 2, 3};
printf("%lu\n", sizeof(a)); // 12
int
型が3つの配列なので、合計で12バイト使うため、12が返されます。
よって、sizeof(配列名)
をsizeof(配列の要素の型)
で割ることで、配列の長さを得ることができます。
printf("%lu\n", sizeof(a) / sizeof(int)); // 12 / 4 = 3
sizeof(char)
は1なので、char
型の配列すなわち文字列の場合はsizeof(char)
で割らなくても
配列長を求めることが出来ます。文字列の場合、最後の'\0'
まで含んだ長さになっていることに注意しましょう。
char s[] = "abc";
printf("%lu\n", sizeof(s)); // 4になる。3ではない
さて、次に、関数の引数に配列をとる方法を紹介します。
int sum_array(int a[], int n) {
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += a[i];
}
return sum;
}
int main() {
int arr[3] = {1, 2, 3};
pritnf("%d\n", sum_array(arr, 3)); // 6
}
int a[]
という書き方で、関数の引数に配列をとることができます。ここでは、配列の長さを示すn
も渡し、
配列の要素の合計値を計算しています。
さて、ここでなぜ配列の要素数であるn
を渡しているのでしょうか?先ほどのsizeof
を使えば配列の長さは決定できるのでは?
と思うかもしれません。しかし、関数の引数として与えられた配列に対してsizeof
を適用することはできません。
なぜなら、
- 関数の引数に配列を渡すと、配列全てがコピーされるのではなく、その先頭アドレスのみが渡される
- それを
a[]
という表記で受け取る - 実は関数引数の
a[]
は来週ならうポインタそのものになっている - ポインタに
sizeof
を適用しても、配列のメモリ使用料は得られない
というロジックになっています。要点としては、関数に配列を渡すときのa[]
は特殊なもの(ポインタ)になっていて、それにsizeof
を適用して要素数を取り出すことはできない。
と覚えておきましょう。このあたりは次週に詳細に勉強します。
コラム
a[]
という表記は、配列を宣言するとき:
int a[] = {1, 2, 3}; // 配列を作るとき
int f(int a[], int n) { ... } // 関数の引数
int f(int *a, int n) { ... } // 関数の引数
さて、文字列を引数に渡す場合はどうでしょうか?
int count_spaces(char s[]) {
int ct = 0;
for (int i = 0; s[i] != '\0'; ++i) {
if (s[i] == ' ') {
++ct;
}
}
return ct;
}
int main() {
char s[] = "abc def gh";
printf("%d\n", count_spaces(s)); // 2
printf("%d\n", count_spaces("ijk lmn opq rst")); // 文字列リテラルを与えることも出来る。 3
}
'\0'
で終わるという取り決めになっているからです。
次のようなイディオムで文字列を周回することができます。
for (int i = 0; s[i] != '\0'; ++i) { ... }
文字列に関する関数¶
これまでに、文字列は単なるchar
の配列で、比較や代入が簡単ではないことを見てきました。
そのため、c言語では文字列を扱う便利関数がいくつも準備されています。
#include <stdio.h>
#include <string.h> // string.hをinclude
int main() {
// strlen: 文字列の長さ
char s1[] = "abc";
unsigned long len = strlen(s1); // 長さを取得
printf("sizeof(s1): %lu, strlen(s1): %lu\n", sizeof(s1), len); // sizeof: 4, strlen: 3
// strcpy: 文字列をコピー
char s2[10]; // 十分に長く確保
strcpy(s2, "hello");
printf("%s\n", s2); // 文字列をコピー。 s2 = "hello" 的なもの。
printf("sizeof(s2): %lu, strlen(s2): %lu\n", sizeof(s2), strlen(s2)); // sizeof: 10, strlen: 5
// strcat: 文字列を結合
char s3[10] = "hoge"; // 十分に長く確保
printf("sizeof(s3): %lu, strlen(s3): %lu\n", sizeof(s3), strlen(s3)); // sizeof: 10, strlen: 4
strcat(s3, "fuga"); // 文字列を結合する。stringのconcatenate
printf("%s\n", s3); // "hogefuga"
printf("sizeof(s3): %lu, strlen(s3): %lu\n", sizeof(s3), strlen(s3)); // sizeof: 10, strlen: 5
// strcmp: 文字列を比較
char s4[] = "abc";
char s5[] = "abcd";
int cmp = strcmp(s4, s5); // 文字列の比較。s4 == s5的なもの。二つの文字列の辞書順に応じて、正か0か負になる
printf("%d\n", cmp); // ここでは-100
}
include <string.h>
を忘れないようにしましょう。
ここでは、文字列を扱う関数が入っているstring.h
というファイルを最初に読み込んでいます。
このように、c言語では別のライブラリを使いたいときには、一番最初にinclude
します。
ここで、strcpy
やstrcat
をするときは、十分に領域を長く確保しておく必要がある点に注意しましょう。
やってみよう(40分)
- 上記を写経してみましょう。
int sum_array(int a[], int n) { ... }
のような配列を引数にとる関数について、その内部でsizeof
を使っても配列長を得ることが出来ないことを確認しましょう。0
から127
までの整数を、%c
を用いて単語として表示していましょう。ASCIIコードにはどういうものがあるか確認できます。特に、最初のほうの数字は制御コードになっています。strcpy
やstrcat
で十分な領域を確保しない場合にどのような問題が発生するでしょうか?例をあげてみましょう。例えば、十分な領域を確保せずにstrcpy
を行ったあとにそれをprintf
で%s
で表示するとどうなるでしょうか?- 実は
printf
の最初の引数にこれまで文字列リテラルを与えてきましたが、実は文字列の変数でもOKです。次のコードが想定通り動くことを確認しましょう。char fmt[] = "%d\n"; int n = 2; printf(fmt, n);
- Cの文字列のルールは、「先頭から
\0
が出現するところまでを文字列とする」というだけです。そこより後ろは気にしません。また、char
の配列長がいくつであっても構いません。例えば以下を考えてみましょう。上の例では、char s[] = {'a', 'b', 'c', '\0', 'd', 'e'}; printf("%s\n", s); // abc になる
s
は全く問題なく「"abc"
という文字列」として扱えます。上記を実際に表示してみましょう。
クイズ
char c = '3'
のように、数字の「文字」のc
が与えられたとき、数字そのものを得るにはどうすればいいでしょうか?- 任意のアスキーである
char c
に対して、もしアルファベットの大文字であれば小文字に変換する以下の関数を作りましょう。char lower (char c) { /* ここを穴埋め **/ }
0
と、'0'
と、'\0'
と、"0"
と、"\0"
の違いを説明してください。- 文字列リテラルはcharの配列なので、配列に対する操作が実行できます。次のコードは何を出力するでしょうか?
printf("%c\n", "abcd"[3]);
- 以下を穴埋めして、文字列の長さを計算する
strlen
関数を作ってみましょうint strlen(char s[]) { /* ここを穴埋め */ }
答え
- 例えば
int n = c - '0'
とすると、数字そのものが得られます。 - 例えば以下
K&R 53p
を元に改造。'A'
との差で得られる「アルファベットオフセット」を'a'
に足すことで対応小文字を取り出せる。char lower (char c) { if (c >= 'A' && c <= 'Z') { return c - 'A' + 'a'; } else { return c; } }
-
0
: 整数の0。その実体は当然0'0'
: 文字。アルファベットの0を表すASCIIコード。その実体は何らかの整数(48)'\0'
: 文字。NULL文字を表すASCIIコード。その実体は整数の0。すなわち0 == '\0'
"0"
: 文字列。char型が2つの配列。一つ目は'0'
の文字であり、二つ目はNULL文字。すなわち、char s[2] = {'0', '\0'};
。言い換えるとchar s[2] = {48, 0};
"\0"
: 文字列。「NULL文字が二つ」。すなわち、char s[2] = {'\0', '\0'};
。言い換えるとchar s[2] = {0, 0};
。ちなみにNULL文字そのものを文字列としてprintfしても表示されないので、printf("\0")
とすると何も表示しない(エディタに注意されるかも)
'd'
が出力されます。"abcd"[3]
という表記は、char型の配列である{'a', 'b', 'c', 'd', '\0'}
の3番目の要素を取得することを意味するからです。- 例えば以下になります(
K&R, 48p
を参考)int strlen(char s[]) { int i = 0; while (s[i] != '\0') ++i; return i; }
型変換¶
それでは次に型の変換について学んでいきましょう。 Cでは型が違う変数同士の演算に対し、暗黙の変換を行います。これにより、プログラマは型の細かい違いを気にせずにコーディングを行うことができます。 一方で、どのような変換が行われているかを把握していないと、おかしな挙動をするコードを書いてしまうかもしれません。 そこで、型変換について勉強していきましょう。
まず原則について見ていきましょう。Cにおいて型の変換が行われるのは次の4つの場合です。
- 演算において型が違う場合。例:
int i; float j;
のときにi + j
(通常の算術型変換) - 値の代入時の両辺の型が違う場合。例:
int i;
のときにfloat j = i
(代入時の変換) - 関数の引数の型が呼び出し元と一致しない場合。例:
int f(int a) { ... }
のときにf(0.2)
- returnの型が一致しな場合。例:
int f() { return 0.2; }
ここでは上の二つを学びましょう。
通常の算術型変換¶
Cの変換の原則は、情報を失うことなく、「より狭い」演算から「より広い」演算にするというものです。
例えば、int
とchar
の足し算であれば、char
はint
に変換されます。これは情報の損失が生じません。
また、int
とfloat
の足し算であれば、int
がfloat
に変換されます。これは情報は損失されるのですが、float
をint
にするよりも損失しません。
以下が例になります。
char c = 12; // 8 bit
short s = 34; // 16 bit
int i = 56; // 32 bit
float f = 1.0; // 32 bit
double d = 2.0; // 64 bit
i = i + c; // i + c の計算部分で、cがintに変換される
i = i + s; // i + s の計算部分で、sがintに変換される
f = f + i; // f + i の計算部分で、iがfloatに変換される
d = d + f; // d + f の計算部分で、fがdoubleに計算される
また、上の話とちょっと被りがあるのですが、
小さな整数同士の演算では整数拡張が行われます。これは、元の型がint
で表現できる場合、int
に変換して計算するというものです。以下の計算を考えましょう
char c1 = 10;
char c2 = 100;
char c3 = 20;
char c4 = c1 * c2 / c3; // 整数拡張。c1もc2もc3もintに変換される
printf("%d\n", c4); // 50. OK
c1 * c2
の部分の計算はchar
同士の計算なのでchar
で行われるように思いますが、それだとchar
の数値表現の限界(127)を超えてしまうことがわかります。
それだと困ってしまうので、Cはここで整数拡張を行います。すなわち、char
での計算はまず全てint
に変換されます。
なので、ここでは右辺は全てint
だとして計算されます。そのため、計算結果は全て想定したものになります。
ちなみに、int
同士の演算はlong
にはならないので注意してください。
代入時の変換¶
さて、計算結果を左辺に代入するときに、右辺と型が違う場合は、左辺の方に変換されます。左辺のほうが「広い」場合は、問題はおきません:
char c;
int i;
float f;
double d;
i = c; // cはintに変換される
f = i; // iはfloatに変換される
d = f; // fはdoubleに変換される
int i;
i = 123.45; // iは123になる
i = -123.45; // iは-123になる
下は「広い」整数型から「狭い」整数型に値を代入しておかしくなっている例です。
char c = 1000; // charの限界の127を超えている
printf("%d\n", c); // ダメ。-24
1000
というリテラル(型はint
)はchar
の限界を超えているので、代入時に値が捨てられて、おかしくなります。
ところで、ここではなぜ-24なのでしょうか?ここでは、10進数の1000の二進数表現(32 bit)は00000000|00000000|00000011|11101000
です。
見やすさのため8ビットごとに区切りをいれました。
ここで、char c
は8ビットなので、無理やり代入することで11101000
となります。この値はchar
では10進数で-24です。
そのため、%d
で表示すると-24になります。
この挙動(符号付き整数のオーバーフロー)は未定義です。私の環境(gcc, 64-bitマシン)ではそうなりましたが、他の環境では違うかもしれません。
一方で、符号なし整数のオーバーフローの場合は定義されているそうです。
int i = 5000000000; // intの限界の2147483647を超えている
printf("%d\n", i); // ダメ。705032704
printf("%ld\n", i); // ダメ。705032704
printf("%d\n", 5000000000); // ダメ。705032704
printf("%ld\n", 5000000000); // これはOK。5000000000
int
の限界を超えている例です。5000000000
はint
の限界を超えているので、代入時に情報の損失が起きます。
ここで、10進数の整数リテラルはint
で表現できるときはint
ですが、それを超えるときはlong
になることを覚えておきましょう。
なので、左辺の5000000000
はlong
として適切ですが、int
に代入するときに壊れてしまうというわけです。
そのため、結果として得られたi
を%d
で表示してみると、別の値になっています。
なので、これをlong
(64 bit 整数)の表示モードである%ld
で表示してもやはりダメです。
ちなみに、10進数の5000000000
は64bitで
00000000|00000000|00000000|00000001|00101010|00000101|11110010|00000000
int
にすると
00101010|00000101|11110010|00000000
705032704
です(繰り返しますが、ここは処理系依存です)
面白い点として、一番最後の、「long
リテラルを直接表示する」場合はうまくいっていることに注意しましょう。
float
に小数を入れる場合も注意が必要です。暗黙的にdouble
リテラルがfloat
に変換されています。
float v = 3.223; // この場合はOKだが、doubleからfloatに変換されている3.223f
float v2 = 3.223f; // 明示的にfloatのリテラルだと示すときはこちらのほうがいい
キャスト¶
さて、ここまでは暗黙的な型変換を見てきましたが、型変換は明示的に行うこともできます。それがキャストです。
キャストは、(型名) 変数名
という書き方で、変数を別の型に強制的に変換します。
float quotient;
int dividend = 3, divisor = 2;
// キャスト無し
quotient = dividend / divisor; // 1.000
// キャストあり
quotient = (float) dividend / divisor; // 1.5000
上の式では、分母も分子も整数で表現したいという要請があるとします。ここで、dividend / divisor
は整数の割り算なので
小数部分を切り捨ててしまいます。それがfloat
に代入されるため、1.000
になります。
ここで、(float) dividend
とすることで、dividend
を無理やりfloat
にします。これにより、右辺の演算が小数の割り算となり
正しい答えになります。
ところで、キャストは単項の演算子なので、上の式は次と同じということに注意してください(すなわち、式全体にかかっているわけではない)
quotient = ((float) dividend) / divisor;
unsignedの場合¶
上記は全て負の数も含むsigned
の話でした。unsigned
を含む型変換は、話が複雑になります。
たとえばint
とunsigned int
を含む演算では、両方をunsigned int
にそろえるように変換されます。
本講義では詳しく述べませんが、第一原則として符号あり変数と符号なし変数を混ぜた演算は絶対にしないと覚えておいてください。
コンパイラが警告を出すと思います。
以下に非常にわかりにくいバグを生む例をお見せします。
int i = -10;
unsigned int u = 10;
printf("%d\n", i < u); // なんと偽になる
i
をunsigned int
に型変換しようとしますが、unsigned
が示す通り負の値をunsigned int
に変換すると
おかしなことがおこります(ここでは4294967286
になります)。そのため4294967286 < 10
は偽になってしまうのです。
このようなミスを防ぐために、符号なしと符号ありは必ずそろえるようにしましょう。ここでは値が十分に小さいとすると
printf("%d\n", i < (int) u); // 真になる
型変換は複雑なので、より完全なルールは例えばここを参照してください。
やってみよう(30分)
- 上記を写経してみましょう。
int i;
として、long j = i * i;
を考えてみましょう。char
同士の演算はオーバーフローにならないようにそれぞれ自動的にint
になるという話 (整数拡張)がありましたが、int
同士の計算はそれぞれがlong
になるわけではありません。そのため、i
を大きくしていくと、i * i
が破綻します。 ここで、破綻したあとにlong
に代入されるので、j
には破綻した値しか残らないことに注意しましょう。 それを実際に書いて確かめてみてください。- また、上の状況で
(long) i * i
とした場合は、((long) i) * i
となり、まずi
がlong
に変換されるため、「通常の算術型変換」により後ろのi
もlong
になり、 結果ちゃんとした値になることを確認しましょう。
クイズ
int i;
char c = /* charにおさまる何らかの値 */;
i = c;
c = i;
答え
損失しません。c
をi
に入れる段階で損失はありません(int
のほうがより多くの情報を保持できるので)。そして、その値はc
に収まるものです(-128から127))。その値を再度char
に入れても、値は保持されます。ちなみに、ひっくり返すと保持されないことがあります。
int i = 300;
char c;
c = i; // charに収まらないので値が損失
i = c; // 損失した値をintに戻すので、やはり損失している
printf("%d\n", i); // 例えば、44になる
入出力¶
さて、次はどのようにプログラムに情報を入力し、どのように出力するかを見ていきましょう。 C言語の標準ライブラリによってサポートされる入出力のモデルは非常にシンプルです。テキストの入力あるいは出力は、 その発生元や発生先などにかかわらず、文字のストリーム(流れ)として扱われます。 ストリームとは、「文字列 + 改行」を一行とすると、それが複数集まったものだと思えばOKです。
ストリームの種類¶
3種類の標準的なストリームが用意されています。
stdin
: Standard input. 標準入力。キーボードからの入力だと思ってくださいstdout
: Standard output. 標準出力。端末の画面への出力だと思ってくださいstderr
: Standard error: 標準エラー出力。エラーの出力用。stdoutと同じく端末画面への出力。
我々が今回使うのはstdin
とstdout
です。つまり、キーボードから入力を受け取り、処理を行い、
画面に表示する、というものです。
一文字の読み書き(getchar, putchar)¶
ここで、まず最も基本となる一字の読み込みと書き込みを勉強しましょう。
int getchar(void)
は、標準入力(キーボード)のストリームから一文字を取り出し、int
として返す関数です。ストリームが終わるときは、特殊記号のEOF
を返します。これはstdio.h
で定義されている整数で、負の値になっています。皆さんの環境では-1
だと思います。int putchar(int c)
は、一文字c
を受け取り、それを標準出力(端末の画面)に表示する関数です。もしエラーが起きれば、EOF
を返します。そうでなければ、入力であるc
と同じ値を返します。
それでは、読み込んだ内容を出力するだけの次の複写プログラムを考えてみましょう(K&R, p20
を調整)
これを複写プログラムとします。
#include <stdio.h>
int main() {
int c = getchar(); // 一文字読み込む
while (c != EOF) { // 最後でなければ
putchar(c); // 一文字書き込む
c = getchar();
}
}
これをコンパイルして実行してみましょう。すると、端末が「待機中」になるとおもいます。その画面で適当に文字を入力しエンターを押すと、その内容(標準入力からの入力) が読み取られ、コピーされることがわかります。
$ gcc main.c
$ ./a.out
abcd <- エンターを押す
abcd
xxx <- エンターを押す
xxx <- CTRL + C あるいは CTRL + Dを押す
$
- 強制終了を意味する Ctrl+C を押す
- EOFの入力を意味する Ctrl+D を押す
のどちらかを実行してください。
ここで、エンターを押したあたりの挙動を詳細に確認しておきましょう。
getchar
があると、まずユーザが打ち込んだ文字はwhileにいきなり入るのではなくバッファされて貯め込まれます。
そこにエンターが押されると、バッファの最後に「改行」も足したうえで、全体がgetchar
にどどどっと流れ込みます。
次を実行して挙動を詳細に確認してみましょう。
int c = getchar();
while (c != EOF) {
if (c == '\n') {
printf("You entered a new line\n");
} else {
printf("You enterd %c\n", c);
}
c = getchar();
}
$ ./a.out
a <- エンターを押す
You entered a
You entered a new line
abcd <- エンターを押す
You entered a
You entered b
You entered c
You entered d
You entered a new line
次に、次のような「入力される文字をカウント」する関数を見てみましょう(K&R, p22
)
#include <stdio.h>
int main() {
long nc;
nc = 0;
while (getchar() != EOF) {
++nc;
}
printf("%ld\n", nc);
}
$ gcc main.c
$ ./a.out
abcd <- エンターを押す
<- Ctrl + D を押してEOFを送る
5
$
\n
に対する処理が記述されているわけではないので)。
ここではCtrl+D を押すとEOFとなり、カウント数が表示されます。
リダイレクト:ファイルからの入出力¶
上記はキーボードからの入力、および画面への出力でした。 これは簡単に「ファイルからの入力」および「ファイルへの出力」に変更することができます。
以下のプログラムを考えましょう。これは入力に対して字の間にスラッシュを入れて返すものです。 これを複写スラッシュプログラムとしましょう。
#include <stdio.h>
int main() {
int c = getchar();
while (c != EOF) {
putchar('/'); // 複写プログラムに加え、文字の間にスラッシュをいれる
putchar(c);
c = getchar();
}
}
$ gcc main.c
$ ./a.out
abcd <- エンターを押す
/a/b/c/d/
xxx <- エンターを押す
/x/x/x/ <- CTRL + C あるいは CTRL + Dを押す
$
ここで、次のようなファイルを作ってみましょう
aaa bbb ccc
dd ee ff
sample.dat
として保存しましょう。
そして、このファイルを複写スラッシュプログラムに入力してみましょう。
$ ./a.out <sample.dat
$ ./a.out < sample.dat
sample.dat
の中身がa.out
によって処理され、その内容が表示されます。
/a/a/a/ /b/b/b/ /c/c/c/
/d/d/ /e/e/ /f/f/
このように、<
を用いることで、標準入力のソース元を変更することができます。
これをリダイレクトと言います。
同様に、>
を用いることで、出力先を変えることができます。
$ ./a.out >out.dat
hoge <- ENTER
<- CTRL + D で終了
$
out.dat
という新しいファイルが出来て、その中に上記の内容が書き込まれていることがわかります。
この>
を使う方法は、プログラムの出力をファイルにすることができるので何かと便利です。$ ./a.out > log.dat
のように。
そして、これらは同時に使うことができます。
$ ./a.out <sample.dat >out.dat
out.dat
に出力されることがわかります。
また、上記の表記だと常にout.dat
は新しいファイルとして生成されるのですが、>>
を用いると「追記モード」になります。
$ ./a.out <sample.dat >>out.dat
out.dat
の中に処理結果が何度も書き込まれていることを確認しましょう。
パイプ¶
さらに、標準出力と標準入力を組み合わせるパイプ(|
)を紹介します。
まず準備として、標準入力を標準出力に1回だけ返すのコマンドecho
を紹介します。
$ echo hogehoge
hogehoge
コマンド1 | コマンド2
./a.out
のような実行可能バイナリの実行も含みます。
これを用いると、コマンドライン上でコマンドを組み合わせることができます。
次のように、echo
の出力を複写スラッシュプログラムの入力にしてみましょう。
$ echo abcd | ./a.out
/a/b/c/d/
echo
の出力にとって代わられました。
別の例もみてみましょう。
$ ls
a.out main.c
ls
をするカレントディレクトリのファイルが表示されますね。これをパイプで繋いでみましょう。
$ ls | ./a.out
/a/./o/u/t/
/m/a/i/n/./c/
ls
の特殊仕様ですので気にしないでください)
最後に、パイプとリダイレクトを組み合わせることもできます。次をやってみましょう
$ cat main.c | ./a.out >out2.dat
out2.dat
というファイルが生成され、その内容は以下のようになります
/
/#/i/n/c/l/u/d/e/ /</s/t/d/i/o/./h/>/
/
/i/n/t/ /m/a/i/n/(/)/ /{/
/ / / / /i/n/t/ /c/ /=/ /g/e/t/c/h/a/r/(/)/;/
/ / / / /w/h/i/l/e/ /(/c/ /!/=/ /E/O/F/)/ /{/
/ / / / / / / / /p/u/t/c/h/a/r/(/'///'/)/;/
/ / / / / / / / /p/u/t/c/h/a/r/(/c/)/;/
/ / / / / / / / /c/ /=/ /g/e/t/c/h/a/r/(/)/;/
/ / / / /}/
/}/
やってみよう(30分)
- 上記を写経してみましょう。
EOF
は負の整数になっています。実際に整数として表示して確認してみましょう。- 「文字列」のクイズで出来てきた、文字を小文字に変換する関数
lower
を思い出しましょう。 上記の複写のプログラムとlower
を組み合わせることで、「入力文字に対して、大文字であれば小文字にして出力」とするプログラムを書いてみましょう。 - リダイレクトを用いて、そのプログラムに何らかのテキストファイルを入力し、それをファイルとして出力してみましょう。
- パイプと
echo
を用いて、そのプログラムにテキストを入力し、端末上に出力してみましょう
クイズ
- 上で悦明した複写プログラムについて、
getchar
を一回しか使わないように書き換えてみましょう。
答え
- 例えば以下
K&R, p21
このwhileの中身は少し複雑です。どういう意味なのか考えてみましょう。int c; while ((c = getchar()) != EOF) { putchar(c); }
printf詳細¶
今日の最後に、printf
の詳細を見ていきましょう。これまでに色々な使い方を見てきましたが、それらをまとめて、新しい使い方も見ていきましょう。
int i = 3;
double f = 154.423;
printf("|%d|%5d|\n", i, i); // |3| 3|
printf("|%f|%.2f|%8.2f|\n", f, f, f); // |154.423000|154.42| 154.42|
%5d
と指定すると、「少なくとも5桁」を表示するように空白が挿入されます。
少数に対し%.2f
とすると、小数部分の桁数が2になります。%8.2f
は、「全体が少なくとも8桁」で、かつ小数点以下が2桁になります。
double v = 1.3e10;
printf("|%f|%e|\n", v, v); // |13000000000.000000|1.300000e+10| 通常出力と、e+10の形式の出力
printf("|%g|%g|\n", f, v); // |154.423|1.3e+10| 自動で選ぶ
%e
にすると、\(1.3\times 10^{10}\)を意味する1.300000e+10
という表記になります。
%g
は、数字の大きさに応じて普通に表示するかe
を使うか自動で選んでくれます。
ちなみに、double v = 1.3e10
というように、小数リテラルはe
を含んで表記することが出来ます。
int n = 500;
printf("|%d|%o|%x|\n", n, n, n); // |500|764|1f4|
int n_octal = 0764; // "0"は8進数。10進数への変換: 7 * 8^2 + 6 * 8^1 + 4 * 8^0 = 500
int n_hexadecimal = 0x1f4; // "0x"は16進数。10進数への変換: 1 * 16^2 + 15 * 16^1 + 4 * 16^0 = 500
printf("|%d|%d|%d|\n", n, n_octal, n_hexadecimal); // |500|500|500
%o
で8進数表記、%x
で16進数表記で出力できます。
ちなみに、8進数の整数リテラルは先頭に0
をつけることで表現でき、16進数のリテラルは先頭に0x
をつけることで表現できます。
これらは整数の表現が違うだけで、同じものを指します。
long ln1 = 123;
printf("|%d|%ld|\n", ln1, ln1); // |123|123| OK
long ln2 = 10e10;
printf("|%d|%ld|\n", ln2, ln2); // |1215752192|100000000000| %dではダメ
long
型は、int
に収まる小さい値のときは%d
でも表現できますが、それを超える場合は%ld
としていなければなりません。
unsigned char c = 200; // charの範囲を超えている
unsigned short s = 32800; // shortの範囲を超えている
unsigned int j = 2147483649; // intの範囲を超えている
unsigned long l = 18446744073709551615; // longの範囲を超えている
printf("|%u|%u|%u|%lu|\n", c, s, j, l); // |200|32800|2147483649|18446744073709551615| OK
printf("|%d|%d|%d|%ld|\n", c, s, j, l); // |200|32800|-2147483647|-1| unsinged intとunsigned longはおかしくなる
unsigned
について、int
の範囲を越えなければ%d
で表現できます(char
, unsigned char
, short
, unsigned short
)
unsigned int
およびunsigned long
については、それぞれint
, long
の範囲を超える場合は、%u
および%lu
を使う必要があります。
printf("|%p|\n", &n); // |0x7ffe08a2e6b8|
printf("|%s|%c|\n", "abc", 'x'); // |abc|x|
やってみよう(15分)
- 上記を写経してみましょう。
宿題¶
それでは今週の宿題です。
- 締切は次の授業の前日の深夜23:59までです(今回の場合、2021/10/27, 23:59)
- 宿題リンクをslackで配付します。このリンクは公開しないでください。
week3_1¶
タイピングゲームを作りましょう。
This is a pen
と間違えずにタイピングできたらOKというゲームです。main.c
を編集してください。$ gcc main.c
としてa.out
をコンパイルしたうえで、以下を実現しましょう。
各課題がマイルストーンになっています。できるところまでやってみてください。「課題2を実行可能なmain.c
は課題1も実行可能」という風にしてください。
ちなみに、Cには入力を扱う関数はたくさんありますが、この宿題では今日ならったgetchar
を使ってみましょう。
課題1¶
まずは以下を実装しましょう。何も押さない場合は0ポイントです。
$ ./a.out
Type: "This is a pen" <- ここで入力受付(待機)になる
<- なにも押さずにエンターだけ押す
0 <- スコアが0点なので0を表示
次に、一か所も間違えずにタイピングできた場合はタイプ数ぶんのポイントが表示されます。
$ ./a.out
Type: "This is a pen" <- ここで入力受付(待機)になる
This is a pen <- This is a penと入力してエンターを押す
13 <- スコアが13点なので13を表示
これらは自動採点では次のように実現されています。自動採点のほうではキーボード入力の部分は表示されないので注意してください。
- 入力:
$ echo '' | ./a.out
-
出力:
Type: "This is a pen" 0
-
入力:
$ echo 'This is a pen' | ./a.out
- 出力:
Type: "This is a pen" 13
以下の課題も全て同様に自動採点が定義されます。
課題2¶
次に、途中まで合っていた場合に、合っていた単語数分の得点も表示するようにしましょう。
$ ./a.out
Type: "This is a pen" <- ここで入力受付(待機)になる
This <- Thisと入力してを押す
4 <- スコアが4点なので4を表示
課題3¶
さらに、ところどころ合っていた場合に、合っていたぶんだけ得点をゲットできるようにしましょう。
$ ./a.out
Type: "This is a pen" <- ここで入力受付(待機)になる
Tis is a ppp <- Tis is a pppと入力してを押す
2 <- スコアが2点なので2を表示
課題4¶
最後に、正解したあとも追加でタイピングしてしまうと減点されるようにしましょう。
$ ./a.out
Type: "This is a pen" <- ここで入力受付(待機)になる
This is a penxx <- This is a penxxと入力してを押す
11 <- スコアが11点なので11を表示
【10/23更新】課題4の解釈について指摘があったので、以下のようにします。
- This is a pexxxxのように、「正解じゃない文字列」かつ「超過」の場合は、課題3までの回答だと12点だが、課題4までの回答だと9点だと解釈される
- よって、このような場合はどちらでもよい
- 松井側で課題3について本採点するときは、「正解じゃない文字列」かつ「超過しない」場合だけを考える
week3_2¶
自由課題です。week3_1のプログラムをベースに、改造を施し、イケてるタイピングゲームを作ってみましょう。 自動採点は無いです。好きにゲームを作ってみてください。面白かったものは次回か次々回に紹介するかもしれません。
ゲームを作ったら、README.md
を編集して、説明を追記してください。マークダウンファイル(拡張子が.md
のファイル)の編集方法についてはマークダウンファイルの編集方法を参考にしてください。
改造の例は例えば以下です。
- 複数の問題が出題されるようにする
- ランダムに問題が出題されるようにする
- スコアに応じて出題する問題のレベルを変える
- イケてる見かけにする
- かかった時間に応じてスコアを増減する
ヒント¶
いくつかのヒントを紹介します。
- Week 5で詳細に紹介しますが、文字列の配列はcharの二重配列を用いて以下のように表現できます
char msgs[3][100] = {"hoge", "fugafuga", "piyopiyopiyo"}; // 100の部分は十分に大きな値にする printf("%s\n", msgs[1]); // fugafugaを表示
stdlib.h
をインクルードすることで、ランダムな数字を作ることが出来ます。去年の資料を参考にしてください。- 同じく
stdlib.h
をインクルードしたうえで、以下を実行すると、画面の表示をクリア(上にスクロール)して綺麗にしてくれます。system("clear");
time.h
をインクルードしたうえで、以下のようにすると処理の時間を計測できます(以下はlinux限定の表記なので、windowsネイティブ環境でやっている人は代替の表記を使ってください)// timespecという型の変数であるstartとendを準備。これを開始と終了の時刻計測に使う struct timespec start, end; clock_gettime( CLOCK_REALTIME, &start); // startに現在時刻を書き込む。 sleep(3); // ここでなんらかの処理。たとえば3秒待機するsleep(3) clock_gettime( CLOCK_REALTIME, &end); // endに現在時刻を書き込む // startとendを元に、秒単位の差分を計算する。ここでは、秒レベルの情報と // nano秒レベルの情報が別々に保存されているので、それらを適切に処理している。 double diff = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) * 1e-9; printf("%f sec", diff); // 例えば 3.000092 sec となる
- 文字に色を付けることもできます。
答えの例¶
答え
week3_1
#include <stdio.h>
int main() {
char template[] = "This is a pen";
int template_len = 13;
printf("Type: \"%s\"\n", template);
int score = 0;
int i = 0;
while (1) {
int c = getchar();
if (c == '\n' || c == EOF) {
break;
}
if (i < template_len && c == template[i]) {
++score;
}
if (template_len <= i) {
--score;
}
++i;
}
printf("%d\n", score);
return 0;
}