C言語によるプログラミングに関する世界観5(入出力)です。
ツール関係はC/C++ツールに移動しました。
システムに依存する部分はLinux(システムコール・API)やWindowsプログラミングも参照のこと。
文字列(単語など)を変数にしたい時は、文字型の配列か、それへのポインタを使う。
文字列は文字型の配列へのポインタとして扱われるため、そのままでは別の変数にコピーできない(参照的アドレスだけがコピーされる)。
文字列をコピーしたい時はstrcpy()関数を使う。
ただしstrcpy()にはバッファーオーバーフローの問題があるため、セキュリティ機能を追加したstrcpy_s()を使うべきである。
Visual C++ではstrcpy()関数を使うとコンパイルエラーが発生し、strcpy_s()関数を使うように促されることがある。
文字はシングルクォーテーションの'~'で、文字列はダブルクォーテーションの"~"で表す。
char h, o, g, e; h = 'H'; o = 'O'; g = 'G'; e = 'E';
文字列リテラルは、文字のポインタを使う場合以下のように表せる。
char *str = "Hoge";
ある変数に文字列を格納したい場合はバッファとして文字の配列を使う。
char str[50]; str = "Hoge";
printf()やscanf()などで文字列を指定する時は文字列の中に%sを埋め込んで表す。
char *str = "Hoge"; printf("strの内容は%s。\n", str);
2023.06.30編集
文字列は普通ヌル文字(\0)で終わる。
よって、文字列を最後まで繰り返し処理するためには、char型の変数に一文字ずつ読みながら、今の文字を\0と比較して\0であればループを終了する。
char *p; char *hoge = "Hoge"; p = hoge; while (*p != '\0'){ printf("%c\n", *p); p++; }
たとえば、
char *p; char *hoge = "hogehogehoge"; p = hoge; while (*p != '\0'){ switch (*p) { case 'h': printf("HHHHHHHHH\n"); break; case 'o': printf("OOOOOOOOO\n"); break; case 'g': printf("GGGGGGGGG\n"); break; case 'e': printf("EEEEEEEEE\n"); break; } p++; }
実行結果:
HHHHHHHHH OOOOOOOOO GGGGGGGGG EEEEEEEEE HHHHHHHHH OOOOOOOOO GGGGGGGGG EEEEEEEEE HHHHHHHHH OOOOOOOOO GGGGGGGGG EEEEEEEEE
後日注記:このコードを改良して、空白や=や:のような記号が登場するごとに、過去に出てきたキーワードを別々の変数に分割したり、右辺の値を左辺の名前をつけて保管するようにすると、原始的なトークン解析のパーサーが作れるかもしれません。単語の始点を表すポインタと、単語の終点を表すポインタの、二つあるいはそれ以上の複数のポインタを使うと、実装が楽になるかもしれません。単なる文字列の分割なら、JavaやPerlなら標準のAPIでできますが、C言語で行う方法を知っておくとためになるでしょう。もちろん正規表現を使うこともできますが、正規表現を使わずに独自のパーサーを作るほうが高速になる場合もあるでしょう。
後日注記:本当に作るなら、プロパティの設定名と値の始点と終点を表す4つのポインタと、カーソルとして使うための1つのポインタを合わせた5つのポインタを作ります。そして、行の先頭、行の末尾、条件式で比較して一致した=の文字の左と右の場所にそれぞれのポインタを置きます。そして、カーソルとして使うポインタでそれらのポインタ位置を移動しながら、str[]のようなバッファにカーソルをインクリメントしながら文字列をコピーします。解析したデータを保存するためにハッシュテーブルを表す構造体を使い、「設定名」と「値」をこの構造体が持つようにします。これがもっとも単純だと思います。ぜひ、挑戦して作ってみてください。
後日注記:本当は、ポインタをそんなに4つも5つも作る必要はありません。文字数をカウントすればいいからです。行の先頭から=が現れる場所まで、カウント変数をインクリメントして文字数を数え、=が現れてから行の末尾までの文字数も数えます。そうすれば、どこからどこまでが設定の名前で、どこからどこまでが値か、ということが分かります。
後日注記:あるいは、文字が出現するたびにひとつひとつの文字をバッファにコピーするというのも、単純で悪くない方法だと思います。下手に工夫して作るよりも、そのほうが分かりやすくていいかもしれません。
2023.06.30
2023.07.01編集
トークン(字句)を解析する方法として、バッファに入れて空白やその他の記号で区切って配列にする、という方法があります。最初から空白で区切られることが分かっているデータは、データをバッファに入れた上で、空白文字で区切って配列にします。そうすることで、いちいち空白文字が登場する度に一文字一文字解析しなくてよくなります。
空白文字以外でも、{~}や[~]や"~"や:や,などで区切るようにすれば、JSONのパーサなどは作れます。ただし、JSONの場合はネスト構造のツリー構造になるので、簡単に配列にすることはできません。C言語では標準でツリー構造のデータ構造は用意されていないので、自分でツリー構造のJSONのデータエレメントを表す構造体を作らなければなりません。
また、区切り文字によって分割するからといって、一文字一文字解析する必要が絶対にないわけではありません。[の次に]が来るまでは特殊なモードで文字列解析を行うといったように、区切り文字で区切る場合においても、一文字一文字解析する必要は依然としてあります。
また、構文解析の時に使える手法が再帰です。再帰的にループさせることで、「0回以上の繰り返し」や「1回以上の繰り返し」について、それ以上繰り返される部分がなくなるまで繰り返して処理することができます。また、最初に閉じた括弧は最後に開いた括弧に対応します。そのような時にはスタックのデータ構造を活用することができます。
後日注記:ここで書いたのと同じことはパーサーだけではなくコンパイラの開発にも役に立ちます。コンパイラ開発やJSONも参照のこと。
2024.04.15
2024.04.17編集
また、char型は単に一番大きさの小さい整数型として使うこともできる(たとえばカレンダーの日付などはchar型で格納すればいい)。
逆に、任意の文字と少しの追加的な数が入る型であれば、charではなくint型を使うこともある(char型では任意の文字に加えて追加的な数が表せない)。
C言語には、文字列処理用の関数として、
関数 | 説明 |
---|---|
strlen() | 文字列の長さを得る |
strcpy() | 文字列をコピーする |
strcat() | 文字列を連結する |
strcmp() | 文字列を比較する |
strchr() | 文字列から文字を検索する |
strstr() | 文字列から文字列を検索する |
atoi() | 文字列を数値に変換する |
などがある。
gets()と同様、strcpy()にもセキュリティ上の問題がある。なのでstrcpy_s()を使うこと。
(「情報基礎概論 第4版 喜久川政吉・殿塚勲 共著(学術図書出版社)」を参考に執筆しました。)
文字列操作を行うためには、一文字ずつ操作する場合、char型の変数を用意し、標準入力からgetchar()で取得した文字を比較する。
文字列は\0で終わりになる。標準入力・ファイルの終わりはEOFとなる。
このような時、ポインタを使うことも多い。たとえば、ひとつひとつchar型へのポインタでポイントし、ポインタをインクリメントすることで最後までスキャンする。
たとえば、コマンドラインオプションを解析する場合、自前で作るなら、'-'の後に続く'a'とか'b'とかの文字を比較して、条件分岐を行えばよい。
C言語では、基本的に、ポインタなどを使って、一文字一文字解析を行います。
ですが、複数の文字(文字列)を解析したい場合、どのようにすればいいでしょうか。
これはたとえば、文字列が「the」と一致するかどうかを知りたければ、一文字目がtで、二文字目がhで、三文字目がeであることを比較すればいいでしょう。
なので、これを一般化した関数にして、一文字目が与えられた一文字目と一致し、二文字目が与えられた二文字目と一致し、三文字目が与えられた三文字目と一致し、…ということを、最後の文字まで繰り返して行い、最後まですべて一致すれば真を返すような関数にします。
そう、文字列を一致するのであっても、抽象的かつ一般化された関数にしてしまえば、簡単にトークン(字句)の解析器は作れるのです。
実際は、このように、自分でトークンを解析する場面は多くありません。既に同じようなAPIがあります。C言語に用意されたAPIは少ないですが、文字列から文字列を検索するAPIなどもあります。
実際にこの関数を使う際には、if文の条件式などにこの関数を入れて、さまざまな文字列と文字列を比較するようにします。それで、コンパイラやインタプリタやWebブラウザも作れるでしょう。
2024.08.04
2024.08.08編集
printf()は文字列を出力(表示)する関数で、gets()は文字列を入力する関数。コマンドラインのプログラムでは、入力と出力が主な操作になる。
printf()では、フォーマットされた書式で変数の内容を表示できる。数値型の変数には%dを使用する。改行は\nで表す。
int x; x = 22; printf("変数xの中身は%d。\n", x);
フォーマットの書式は、
書式 | 意味 |
---|---|
%d | 数値 |
%c | 一文字の文字 |
%s | 文字列 |
%o | 8進表記 |
%x | 16進表記 |
%f | 小数点以下のある数 |
%e | 指数表記 |
%6dとすると、半角6文字の桁数の領域を表示のために確保することができる。
%04dとすると、幅を4桁にした上で0詰めにできる。
小数点以下の数を表示する際に、13.195のように全部の桁数を6桁、小数点以下を3桁にするには%6.3fとする。
gets()は入力用の関数で、1行分の文字列を入力するが、本当は使ってはならない。バッファオーバーラン(バッファオーバーフロー)を防ぐことができないからである。
gets()は文字列の代入にポインタを指定するが、データのサイズを指定する引数がないため、ポインタが指す配列のサイズ以上の文字を代入されてしまうとセキュリティホールが発生する。
よって、サンプルとしてのみ用いるだけで、実際には使うべきではない。
scanf()についても同様にバッファオーバーランの問題があるが、scanf()の場合は文字幅指定で回避できる。
こうした場合、fgets()をgets()やscanf()の代替として用いることができる。
後日注記:文字列をコピーするstrcpy()にも同様の問題がある。バッファのサイズを指定できるstrcpy_s()を使うようにしよう。
セキュリティも参照のこと。
標準入力から文字列を得るにはfgets()を、標準出力に文字列を表示するにはfputs()を使う。
fgets()とfputs()は以下のように使う。
char str[50]; printf("文字列を入力しなさい。\n"); fgets(str, sizeof str, stdin); fputs(str, stdout);
文字列を入力する際の注意点として、fgets()で得た文字列をそのままprintf()した場合、%が含まれているとそれが変数名であると解釈されることがある。なので要注意。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆しました。)
2023.06.30編集
printf()とよく似た関数として、出力は行わずフォーマットだけを行うsprintf()という関数もある。
また、基本的にstdinから一文字入力するにはgetchar()、stdoutに一文字出力するにはputchar()を使い、ファイル全体を表示するならEOFまでgetchar()とputchar()を繰り返せばいい。
また、入力解析の必然的な性質として、「一文字余計に読まなければ最後まで読んだか分からない」ということがあるが、そのために1バイト戻す関数としてungetc()がある。
入出力関数について詳細はLinux(システムコール・API)を参照のこと。
C言語の入出力関数は、入力系の関数でバッファオーバーフローを起こす可能性があるので気を付けよう。
後日注記:FreeBSD Developers' Handbookに、バッファオーバーフローを起こす危険性のある主な関数の一覧があります。これによると、strcpy(), strcat(), getwd(), gets(), scanf(), realpath(), sprintf()は使うべきでありません。
そのほか、FreeBSDの各種ハンドブックはUNIXのプログラマにとっていい文書だと思うので、参考になさってください。
ポインタで特に良く使うのは、FILE型のポインタである。ファイルを操作する入出力の処理ができる。
たとえば、
FILE *fp; int c; if ((fp = fopen("hoge.txt", "r")) == NULL) { fprintf(stderr, "ファイルのオープンができません\n"); exit(1); } while ((c = getc(fp)) != EOF) { putchar(c); } fclose(fp);
cがcharではなくintである理由は、char型の値に加えてファイル終端のEOFという値を含まなければならないため、char型よりも大きな型でなければならないから。
また、このように文字を格納するためにint型を使うことはたまにある。文字として表現できる値に加えて、エラーが返ってきた時の値などを含む必要があるからである。
以下のページを参考にしました。
UNIXでシステムコールを用いてファイルを操作する場合、ストリームを識別するためにファイルディスクリプタと呼ばれる整数値(どのファイルを読み書きするかというOSの識別子)を用いて、それをopen()やclose()などのシステムコールに渡すことで、そのファイルをopen()あるいはclose()し、それに対してread()あるいはwrite()することでデータを読み書きする。
ファイルディスクリプタは、プロセスが生成されると、標準のファイルディスクリプタとして標準入力(コマンドプロンプトからの入力)、標準出力(端末への出力)、標準エラー出力(エラーメッセージの出力)が与えられる。このほか、open()を呼び出してファイルを開いた時にも、ファイルディスクリプタが与えられ、このファイルディスクリプタに読み書きすることで、ファイルを読み書きすることができる。
パイプでやり取りするのも、この標準入力・標準出力である。標準入力を読んだり標準出力に書いたりする場合、リダイレクトなどを使うことで、ユーザーがそのプログラムをさまざまな場合や状況に応じて使い分けられるというメリットがある。標準入力を読んで標準出力に書くプログラムは、端末の入出力にもファイルの入出力にも同時に対応できる。
Linux API(システムコール)を参照のこと。
しかしながら、システムコールを用いた処理では、固定長の読み書きしかできない。このため、一文字入力・一文字出力したり、一行読み書きしたりする場合、stdio(標準入出力ライブラリ)と呼ばれるC言語の関数を使うことができる。
stdioでは、バッファリング機構を用いて、メモリ上に「バッファ」を保持することができる。すなわち、何度も頻繁に読み書きされるデータに関しては、その都度ストレージから読み書きするのではなく、メモリ上に確保しておいて、実際の読み書きが必要にならない限りバッファから読み書きをする。このため、システムコールに比べてstdioの読み書きはとても効率的で高速である。
また、stdioには、フォーマット入出力と呼ばれる機能がある。これは、たとえば数値を出力する際、変数をどこに展開するか、どれくらいの桁で出力するか、小数点や進数表記をどのように表示するか、のようなもの。ここでよく使われるのはprintf()関数。ファイルに出力する際はfprintf()、出力せず文字フォーマットだけを行う場合はsprintf()が使える。
stdioを使う場合、ファイルを識別するにはFILE型のポインタを使う。これは先に記述したファイルディスクリプタのラッパーであり、内部にファイルディスクリプタやバッファ情報などが格納されている。stdioにおいては、open()やclose()を使わず、fopen()あるいはfclose()を使う。
また、ファイルの今の位置がどこにあるのか(ファイルオフセット)は保存される。lseek(), fseek(), fseeko()などで現在位置を動かして読むことができる。
注意点として、gets()を標準入力を得るためのサンプルコードに使う書籍が多くあるが、これは使ってはならない。gets()ではポインタのみを読み込みバッファに指定するため、バッファを超えたデータが与えられた時にデータが溢れてしまい、悪意を持った人間によってバッファオーバーランが起きるかもしれない脆弱性を持っているからである。これはscanf()などでも同様だが、scanf()では文字幅を指定することで回避できる。これらへの対処策としてfgets()などが利用できる。また文字列をコピーするstrcpy()も使うべきではなく、代わりにstrcpy_s()を使うべきである。VC++ではstrcpy()が使われていると代わりにstrcpy_s()を使うようにエラーメッセージが表示される。C11ではgets()は廃止された。
Linux API(stdio)を参照のこと。
fopen()などを実行する時、モードは
モード | 説明 |
---|---|
"r" | 読み込み |
"w" | 新規書き込み |
"a" | 追加書き込み |
"r+" | 読み書き |
"w+" | 新規読み書き |
"a+" | 追加読み書き |
ができる。また、これにbが付いている場合("rb"など)はバイナリファイルを表す。
後日注記:"r+"と"w+"と"a+"の違いは、"r+"は既存のファイルに対して読み書きを行うが、"w+"はファイルの内容を消去して新しく読み書きを行う。"a+"は追記読み書きを行う。
(「C言語プログラミングレッスン 入門編―ANSI対応 (SOFTBANK BOOKS)」と「情報基礎概論 第4版 喜久川政吉・殿塚勲 共著(学術図書出版社)」を参考に執筆しました。)
2023.01.18編集
また、ファイルオープンの際のエラーチェックも忘れないようにしよう。
C言語でファイルを読み書きするテクニックとして、とてもたくさんのサイズの文字列があった時、順番にそれを処理していくのであれば、一気に巨大サイズのバッファを確保するのではなく、4096などにサイズを決め打ちしたバッファを確保しておいて、その中に4096の固定サイズずつファイルを読み込んでいき、処理を行って、繰り返し読み込みを続け、ファイルの最後まで少しずつ処理していくという方法がある。この方法なら、確保されるメモリ領域の削減に繋がる。
システムコールとstdioについて、詳しくは以下の書籍が参考になります。
ストリームを識別する指定子。
プロセスには標準入力、標準出力、標準エラー出力の3つのファイルディスクリプタが自動的に設定される。
これら以外に、ファイルをopen()した場合やsocketを作った場合にもファイルディスクリプタが設定され、これらを用いてread()やwrite()を行う。
システムコールのread()やwrite()は、固定長入出力であり、「行単位」とか「文字・バイト単位」という入出力ができない。stdioではこうした便利な入出力関数が用意されている。
テキスト処理でよく使うやり方として、4096など固定長のバッファを作っておいて、一時的にバッファにコピーしながら処理をしていくというやり方がある。少しずつ処理をするので可変長の文字バッファを得る必要がなく、一度に確保するよりもメモリ使用量が抑えられる。Pythonなどでは同様の目的にジェネレータを使うこともできる。
gets()は使ってはならない。確保したメモリ領域よりも大きな入力が行われた際に、バッファオーバーランを防ぐ手段がないからである。scanf()も同様の理由で使うのはおすすめしない(scanf()の場合は文字幅指定で回避できる)。同様に文字列をコピーするstrcpy()も使用すべきでなく、strcpy_s()を使うことが推奨される。
stdioではバッファ処理を行うためシステムコール呼び出しよりも速度が速い。
ファイル型(FILE)のポインタはstdioでファイルを識別するのに使う。
FILEの中にはファイルディスクリプタやバッファの情報が格納されている。要するにラッパー。
fork()すると、fork()した親プロセスと子プロセスが2つに分かれ、どちらも同じ場所からその後の処理に進む。
exec()すると、その時点でそのプロセスが上書きされ、新しいプロセスが実行される。
基本的にfork()してexec()することで新しいプロセスを実行する。
以下の書籍・ページが参考になります。
FreeBSD man intro(3) / stdio(3)は参考になります。