Week 3 (2023/10/19)¶
今日やること
- 文字列
- 型変換
- 入出力
- printf詳細
まずはじめに¶
- 宿題の注意
- 全般的にコメントや質問は歓迎なので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
を使うと、文字列そのものを表示できます。 これこで変数として与えられるs
は必ず適切な文字列(charの配列であり、最後の一個は\0
)でなければならないので注意してください。 - そして、文字列は単なる配列なので、その要素を逐一表示できます。
%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 s1[3] = "abcdefg"; // {'a', 'b', 'c'}になる
char s2[3] = "hijklm"; // {'h', 'i', 'j'}になる
printf("%s\n", s1);
printf("%s\n", s2);
'\0'
が入りません。なのでs1
, s2
は正しい文字列になっていません。ここでprintするとおかしなことになります。
例えば私の環境では以下のようになりました。
abchij
hij
s1
の直後にs2
があり、s2
の直後は0になっているため、上記のようになってしまうのだと考えらえます。すなわち、printfは0がくるまでは文字列だとおもってabchij
と表示しています。
配列と文字列¶
次に、配列と文字列の性質について勉強していきましょう。 まず初期化についてです。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};
printf("%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: 8
// 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
にします。これにより、右辺の演算が小数の割り算となり
正しい答えになります。
ところで、キャストは単項の演算子なので、上の式は次と同じということに注意してください(すなわち、式全体にかかっているわけではない)
【10/21更新】単項というよりも、キャストは演算の優先度が高いので、割り算よりも先に実行される、というのが正しい説明でした。
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 new line\n");
} else {
printf("You entered %c\n", c);
}
c = getchar();
}
$ ./a.out
abcd <- エンターを押す
You entered a
You entered b
You entered c
You entered d
You entered new line
すなわち、
getchar
に到達すると、「入力待機モード」になる- 「入力待機モード」中にキーボードから入力すると、それらはバッファに貯められる。
- エンターを押した瞬間、(1) エンターそのものもバッファに貯められる (2) そのうえで、バッファの中身が
getchar
に流れ込む。上記の場合、a
,b
,c
,d
,\n
が流れ込むことになる。まず最初のgetchar
にはa
が流れ込む。 getchar
が連続して呼ばれている上記のようなコードの場合、バッファが空になるまでどんどん流れ込む。すなわち、上の場合\n
に至るまで流れこむ。- その後にさらに
getchar
が呼ばれている場合、再び「入力待機モード」になる
次に、次のような「入力される文字をカウント」する関数を見てみましょう(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 <- a, b, c, d, \n, の合計5
\n
に対する処理が記述されているわけではないので)。
ここではCtrl+D を押すとEOFとなり、カウント数が表示されます。
コラム
マックの場合は、最後の表示が5D
となってしまうことがあるようです。これはCtrl+Dを押す際に表示される^D
が重なって表示されれてしまうらしいです。これについては気にしないでください。
コラム
正確には、Ctrl+Dは状況に応じて二つの異なる挙動をします。
- バッファに何か溜まっている場合:Ctrl+Dを押すと「バッファを流し込むだけ」という動作になります。「改行をふくまないエンター」のようなものです。上記の単語カウントプログラムで「a, b, c, Ctrl+D」とした場合に、プログラムが止まらないのは、そのためです。
- バッファに何も溜まっていない場合:Ctrl+Dを押すと、EOFのみを送り込みます。「EOF+改行」ではないです。そして、End-of-Fileの名の通りそこでファイル読み込みが終わりなので、以降
getchar
で待ち受けてもすべてにEOFが自動で送り込まれるように見えます。
一つ目の「バッファに何か溜まっている場合にCtrl+Dを押す」は想定外の挙動になりえますので注意してください。
リダイレクト:ファイルからの入出力¶
上記はキーボードからの入力、および画面への出力でした。 これは簡単に「ファイルからの入力」および「ファイルへの出力」に変更することができます。
以下のプログラムを考えましょう。これは入力に対して字の間にスラッシュを入れて返すものです。 これを複写スラッシュプログラムとしましょう。
#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
は、%u
を用います。実は、unsigned char
およびunsigned short
はint
の範囲を越えないので%d
でも表現できます。
unsigned int
およびunsigned long
については、それぞれint
, long
の範囲を超える場合は、%u
および%lu
を使う必要があります。
printf("|%p|\n", &n); // |0x7ffe08a2e6b8|
printf("|%s|%c|\n", "abc", 'x'); // |abc|x|
やってみよう(15分)
- 上記を写経してみましょう。
宿題¶
それでは今週の宿題です。
- 締切は次の授業の前日の深夜23:59までです(今回の場合、2023/10/25, 23:59)
- 宿題リンクをslackで配付します。このリンクは公開しないでください。
- ちなみに、
<string.h>
に含まれる文字列関数は使わないようにしてください。また、<string.h>
に限らず、ライブラリは何もinclude
せずに全て自前で書いてみましょう - また、
scanf
など、便利入力関数は使わずに、getchar
でやってみてください。 - 以下の課題について、出来る分だけやってみてください。
week3_1¶
文字と数字を与えると、それを数直線上に表示するプログラムを書きましょう。main.c
にはほとんど何も書いてありません。それを編集して以下を実現してください。
ヒント:難しい場合は、getchar
を使って文字を一文字ずつ読み込み、char buff[1000]
のようなバッファのchar配列にまず保存してみるといいかもしれません。どのような場合でも、プログラムに入力される文字数は1000個以下ぐらいを仮定して大丈夫です(異常に長い文章みたいなコーナーケースは考えなくていいです)
タスクA¶
まずは準備です。./a.out
を実行すると、入力待ちになります。ここで、Ctrl+Dを押してEOFを与えると、以下の数直線を表示してプログラムが終了するようにしてください。
0 5 10 15
--+----+----+----+-->
【10/23更新】上記では数字が赤く表示されていますが、数字の色を変更する必要はありません。
この操作は、リダイレクトを用いて以下のように書けます。自動採点では以下が実行されます。
- 入力
$ echo -n "" | ./a.out
- 出力
0 5 10 15 --+----+----+----+-->
ここでは「echo ""
」により、「何もない」というものが表示されます。それをパイプを使って./a.out
に入力します。echo
の-n
オプションは、改行を出力しないというものです(単純にEOFのみが送られます)
タスクB¶
さて、それでは本題に入りましょう。./a.out
を実行すると入力待ちになります。ここでa,3
と入力しEnterを押します。するとまた入力待ちになります。ここでCtrl+Dを押してEOFを送ります。すると以下が表示され、プログラムが終了します。
0 5 10 15
--+--a-+----+----+-->
すなわち、数直線上の「3」の位置に「a」が表示されます。このように、「aという文字」「その位置」を入力すると数直線上に表示するプログラムを書いてください。 数字は1以上9以下を仮定して大丈夫です。
より正確に書いておくと、実行手順は以下になります
./a.out
、Enterを入力してプログラム実行開始。入力待ち状態になる。- 「
a
」を入力 - 「
,
」を入力 - 「
3
」を入力 - 「Enter」を入力
- 最初に戻り、まだ待機状態が続いている
- 「Ctrl+D」を入力
- 数直線が表示され、プログラムが終了
これに関する自動採点としては、上記の手順がcase2.txt
というファイルに保存されているので、それを読み込んでリダイレクトで入力します。
- 入力
$ ./a.out < case2.txt
- 出力
0 5 10 15 --+--a-+----+----+-->
ちなみに、タスクBが解ける場合、そのプログラムはタスクAも解ける、という感じになっています。なので、最終的に作ったプログラムは、全てのタスクを解けるようになっています。
タスクC¶
次に、複数の点をプロットできるようにしましょう。./a.out
を実行し、a,3
を押してEnterを行ったあと、次はa,5
を入力しEnterを押し、そしてCtrl+DでEOFを与えるとします。すると結果が次のようになるようにしてください。
0 5 10 15
--+--a-a----+----+-->
より正確な実行手順は以下になります
./a.out
、Enterを入力してプログラム実行開始。入力待ち状態になる。- 「
a
」を入力 - 「
,
」を入力 - 「
3
」を入力 - 「Enter」を入力
- 最初に戻り、まだ待機状態が続いている
- 「
a
」を入力 - 「
,
」を入力 - 「
5
」を入力 - 「Enter」を入力
- 最初に戻り、まだ待機状態が続いている
- 「Ctrl+D」を入力
- 数直線が表示され、プログラムが終了
ここで、入力される2つの数字は別のものだと仮定してOKです。自動採点は以下です。
- 入力
$ ./a.out < case3.txt
- 出力
0 5 10 15 --+--a-a----+----+-->
タスクD¶
次に、「b
」も入力できるようにしましょう。すなわち、b,7
のように入力すると、数直線上にb
を表示するようにしましょう。すなわち、「a,4
」「b,7
」「a,3
」と入力した場合は
0 5 10 15
--+--aa+-b--+----+-->
となるようにしてください。
- 入力
$ ./a.out < case4.txt
- 出力
0 5 10 15 --+--aa+-b--+----+-->
全ての数字は異なると仮定して大丈夫です。
タスクE¶
最後に、15以下の二桁の数字も対応できるようにしましょう。すなわち、入力可能な数字は1以上15以下で、かつ全ての数字が違うものです。「a,2
」,「a,10
」,「b,13
」,「a,8
」,「b,5
」とした場合、以下のようになるようにしてください。
0 5 10 15
--+-a--b--a-a--b-+-->
- 入力
$ ./a.out < case5.txt
- 出力
0 5 10 15 --+-a--b--a-a--b-+-->
week3_2¶
自由課題です。week3_1のプログラムをベースに、改造を施し、何か面白いものを作ってみましょう。 自動採点は無いです。面白かったものは次回か次々回に紹介するかもしれません。
プログラムを作ったら、README.md
を編集して、説明を追記してください。README.md
を編集すると、最初のページが更新されることがわかると思います。マークダウンファイル(拡張子が.md
のファイル)の編集方法についてはマークダウンファイルの編集方法を参考にしてください。
改造の例は例えば以下ですが、自由な発想で作ってみてください。
- 数直線の長さを変更する
- ゼロや負の数など、より幅広い数字を受け付ける
- 数直線や記号に色をつける
- 同じ数字を複数回入力すると、何かが起こる
- x, yの二次元にする
- 入力時間に制限を設ける
何を作ってもらってもいいのですが、あくまでc言語でお願いします。好きなライブラリをincludeしてもらって大丈夫です。ただ、松井の環境で実行できるように注意してください。つまり、自分のPCでしか実行できないライブラリなどは使わないでください。基本的に、何も設定していないGoogle Cloud Shell Editorで実行できるようにしてください。
ヒント¶
いくつかのヒントを紹介します。
- 文字に色を付けることができます。
- 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>
#include <stdlib.h>
// 上記以外は何もincludeしないでください
// 関数は自由に追加していいです。
int char2num(char c) {
// 文字である「'3'」とかを数字の「3」にする
return c - '0';
}
int isnum(char c) {
// 入力のcが、数字を表すかどうか調べる
int n = char2num(c);
if (0 <= n && n < 10) {
return 1;
} else {
return 0;
}
}
int main() {
// ==== ここから下に記入 =====
char buff[100] = {0};
int i = 0;
int c = getchar();
// EOFが出るまで全てbuffに詰める
while (c != EOF) { // 最後でなければ
buff[i++] = c;
c = getchar();
}
// a, bの出現位置の記録用
int a[100] = {0};
int b[100] = {0};
int j = 0; // aの配列の記入位置の記録用
int k = 0; // bの配列の記入位置の記録用
for (int n = 0; n < 100; ++n) { // nでbuffを周回
if (buff[n] == 'a') { // 'a' が出た場合は、buffの先を見て、数字を取得。
if (isnum(buff[n + 3])) { // 2桁の場合
a[j++] = char2num(buff[n + 2]) * 10 + char2num(buff[n + 3]);
} else { // 1桁の場合
a[j++] = char2num(buff[n + 2]);
}
} else if (buff[n] == 'b') { // 'b'も同様
if (isnum(buff[n + 3])) {
b[k++] = char2num(buff[n + 2]) * 10 + char2num(buff[n + 3]);
} else {
b[k++] = char2num(buff[n + 2]);
}
} else {
continue;
}
}
char bar[] = "--+----+----+----+-->";
// 数直線上に'a', 'b'を記載
for (int n = 0; a[n] != 0; ++n) {
bar[2 + a[n]] = 'a';
}
for (int n = 0; b[n] != 0; ++n) {
bar[2 + b[n]] = 'b';
}
printf(" 0 5 10 15\n");
printf("%s\n", bar);
return 0;
}