Week 3 (2024/10/17)¶
今日やること
- 文字列
- 型変換
- 入出力
- printf詳細
まずはじめに¶
文字について¶
それでは文字列を勉強していきましょう。これまで、文字を表示するときは
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
は正しい文字列になっていません。ここでprintfするとおかしなことになります。
例えば私の環境では以下のようになりました。
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"); // 文字列をコピー。 s2 = "hello" 的なもの。
printf("%s\n", s2);
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
をするときは、十分に領域を長く確保しておく必要がある点に注意しましょう。
コラム
上記の例のstrcmp
の値がなぜ-100なのか、気になるかもしれません。あくまで参照実装ですが、以下のようになっているかもしれません。King, p306
int strcmp(char s[], char t[]) {
int i;
for(i = 0; s[i] == t[i]; i++) {
if (s[i] == '\0') {
return 0;
}
}
return s[i] - t[i];
}
やってみよう(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 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
という新しいファイルが出来て、その中に上記の内容が書き込まれていることがわかります。
この>
を使う方法は、プログラムの出力をファイルにすることができるので何かと便利です。例えば、プログラムが出力する内容をログファイル(log.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| unsigned 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|
コラム
printf
において%d
や%f
に対し型が違うものを指定すると、その際の挙動は未定義になります。
ここで未定義というのは単に値がおかしくなるというだけでなく、極めて非自明な挙動をとることもありますので、注意してください。
例えば以下の例です。
printf("%d %f\n", 1.2, 3); // なんと「3 1.2」と表示される(こともある)!
やってみよう(15分)
- 上記を写経してみましょう。
宿題¶
それでは今週の宿題です。
- 締切は次の授業の前日の深夜23:59までです(今回の場合、2024/10/23, 23:59)
- 宿題リンクをslackで配付します。このリンクは公開しないでください。
- ちなみに、
<string.h>
に含まれる文字列関数は使わないようにしてください。また、<string.h>
に限らず、ライブラリは何もinclude
せずに全て自前で書いてみましょう - また、
scanf
など、便利入力関数は使わずに、getchar
でやってみてください。 - 以下の課題について、出来る分だけやってみてください。
【2024/10/17更新】配布コードにはargc
, argv
の記述がありますが、使わなくて大丈夫です
week3_1¶
文字列を受け取り、連続する重複文字列を削除して数値に置き換えるプログラムを書きましょう。main.c
にはほとんど何も書いてありません。それを編集して以下を実現してください。以下、タスクBを解けるプログラムはタスクAも解ける、タスクCを解けるプログラムはタスクAもBも解ける・・というようになっています。
- ヒント1:難しい場合は、
getchar
を使って文字を一文字ずつ読み込み、char buff[1000]
のようなバッファのchar配列にまず保存してみるといいかもしれません。どのような場合でも、プログラムに入力される文字数は1000個以下ぐらいを仮定して大丈夫です(異常に長い文章みたいなコーナーケースは考えなくていいです) - ヒント2:難しい場合は、やれるところまでやってみてください。自動採点以外のケースも自分で試してみて、上手くいくか挙動を確認しましょう。
タスクA¶
まず、入力された文字列をそのまま出力してみましょう。ここで入力とは標準入力からの入力を指します。全てのタスクについて、入力文字列はアルファベットのみで構成され、数字は含みません。自動採点では以下をチェックします。
入力:
$ echo abcdef | ./a.out
出力:
abcdef
タスクB¶
次に、「二文字連続する文字」があった場合、二つ目の文字を削除して、その代わりに2
を表示するようにしましょう。たとえば、aa
という記載があった場合、それをa2
に置き換えてください。自動採点は以下になります。
入力:
echo abbcdef | ./a.out
出力:
ab2cdef
タスクC¶
さらに、そのような「連続二文字」が複数回出現する場合でも、正しく処理できるようにしましょう。自動採点は以下です。
入力:
echo abbcddef | ./a.out
出力:
ab2cd2ef
タスクD¶
次に「連続三文字」の場合、2つ目と3つ目を削除して、代わりに3
を表示するようにしましょう。すなわち、aaa
ならa3
に書き換えます。これは文字列全体の長さを縮めるので注意しましょう。
自動採点は以下になります。
入力:
echo abcdeeef | ./a.out
出力:
abcde3f
タスクE¶
最後に、三文字の場合でも複数回出現する場合に対応しましょう。自動採点は以下です。ここでは3文字と2文字が混在しており、どちらも処理しています。
入力:
echo abbcdeeef | ./a.out
出力:
ab2cde3f
松井の手元の本採点では、「4文字」や「5文字」といったように出現数がさらに長いパターンや、3度以上出現するパターンなど、より複雑な例での採点も行うかもしれません。
答えの例¶
答え
week3_1
#include <stdio.h>
#include <stdlib.h>
// stdio.hとstdlib.h以外は何もincludeしないでください
// 関数は自由に追加していいです。
void RLE(char src[]) {
for (int i = 0; src[i] != '\0'; ++i) { // 文字列周回のイディオム
printf("%c", src[i]); // まず文字を出力
int k = 1; // # おなじ文字が続く個数
while (src[i + k] != '\0') { // はみ出さないように
if (src[i] == src[i + k]) { // for周回中でi番目の次を見るので、はみだしチェックが必須
k++; // おなじ文字が続く場合にカウント
} else {
break;
}
}
if (k > 1) { // おなじ文字が続いていた場合、jには個数の数字が入る
printf("%d", k);
i += k - 1; // おなじ文字が続いていたぶんだけiを進める
}
}
printf("\n");
}
int main(int argc, char *argv[]) {
// ==== ここから下に記入 =====
// 入力内容をbuffにためる
char buff[1000];
int c = getchar();
int i = 0;
while (c != '\n') {
buff[i] = c;
i++;
c = getchar();
}
buff[i] = '\0';
// Run Length Encodingを実行
RLE(buff);
return 0;
}