意識低い系会社員

意識低い系会社員の日常

【スポンサーリンク】

ふつうのLinuxプログラミング 第2版の内容メモ(6章)

【スポンサーリンク】

 

こんにちは。

 

最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。

このエントリは完全な個人のメモです。

 

お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。

 

5章の内容はこちら。

amistad06-a.hatenablog.com

 

第6章 ストリームにかかわるライブラリ関数

この章で説明すること

5章で説明したストリームにかかわるシステムコールを使いやすくした標準入出力ライブラリ(stdio : standard I/O library)と呼ばれるライブラリの内容を説明する。

 

バッファリング

stdio(標準入出力ライブラリ)では、システムコールをより使いやすくしたAPIを提供している。

システムコールを使いやすくするための工夫のひとつがバッファリング。

例えば、システムコールのread()は欲しいデータサイズのバッファをその都度自分で用意しなければならないが、stdioではライブラリの中でバッファを持っている。

stdioは内部でシステムコールを使用してストリームから内部バッファに読み込み、ユーザ(プログラム)から1byte欲しいと言われたら内部バッファから1byte返すようになっている。

また、システムコールの呼び出しはかなり遅いので、ライブラリを間に挟んでライブラリの中のバッファに対してデータを読み書きすることで実行速度も上がる。

簡単にいうと、キャッシュのようなもの。

 

バッファリングモード

書き込みの場合も読み込むときと同様、ライブラリの中にバッファを設ける。

プログラムはそのバッファに対して書き込みを行い、バッファがいっぱいになったタイミングでwrite()を実行する。

ただし、描き出すタイミングにはいくつか例外がある。

  • ストリームの先が端末の場合は"\n"が書き込まれたタイミングでwrite()を実行する。
  • stdioストリームがアンバッファモード(unbuffered mode)のときは一切バッファリングを行わずwrite()を実行する。
  • 標準エラー出力に対応するstderrストリームを使用する場合、このストリームはデフォルトでアンバッファモードになっている

詳細は以下を参照。

linuxjm.osdn.jp

 

FILE型

システムコールではストリームを指定するためにファイルディスクリプタを使用していたが、stdioではFILE型のポインタを使用する。

FILE型はファイルディスクリプタとstdioバッファの情報を持った構造体。

ユーザ(プログラマ)は基本的にFILE型の中を気にする必要はない。

標準入出力を指すFILE型の定数は以下。

標準入力:stdin

標準出力:srdout

標準エラー出力:stderr

 

fopen(3)

引数で指定されたファイルに繋がるストリームを作成する。

読み込み専用、書き込み専用、追加書き込み、読み書き両用など様々なモードを指定できる。

 

fclose(3)

引数で指定されたストリームを閉じる。

 

fgetc(3)

引数で指定されたストリームから1byte読み込んで返す。

戻り値はchar型ではなくint型なので注意。

これはストリームが終了した場合やエラー時にEOFを返すことがあるため。

 

fputc(3)

引数で指定されたストリームに1byte書き込む。

書き込む値は引数で指定するが、int型なので注意。

書き込む値をint型にすることでfgetc()で取得した値をそのままfputc()に渡すことができる。

 

getc(3), putc(3)

fgetc()とfputc()は関数だが、こちらはマクロ。

動作はfgetc(), fputc()と全く同じ。

 

getchar(3), putchar(3)

入出力先が標準入出力固定のもの。

getchar()はgetc(stdin)と同じ。

putchar()はputc(c, stdout)と同じ。

 

ungetc(3)

1byteバッファに戻す関数。

2byte以上戻すことはできない。

使い道やメリットは書籍内の絵がとても分かりやすかったので画像載せておきます。

f:id:amistad06-k:20181121163718p:plain

※書籍内の絵を拝借

 

stdio版catを作る

5章でシステムコールを使ってcatコマンドを作りましたが、それと同じものをstdioを使って作ります。

ソースコードは以下。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){

    for(int i = 1; i < argc; i++){
        FILE *f;
        int c;

        f = fopen(argv[i], "r");
        if(!f){
            perror(argv[i]);
            exit(1);
        }
        while((c = fgetc(f)) != EOF){
            if(putchar(c) < 0){
                exit(1);
            }
        }
        fclose(f);
    }
    exit(0);
}

 

詳細な処理の流れは割愛します。

簡単な流れは以下の通り。

  1. コマンドライン引数で与えられたファイルに対して以下の処理を繰り返す
  2. fopen()でファイルに繋がるストリームを作成する。
  3. fopen()が失敗したらプログラムを終了(fopenが失敗した場合は返り値はNULL(整数の0))
  4. ストリームからfgetc()で1byte読み込む
  5. 読み込んだ結果がEOFなら読み込みを終了
  6. EOF以外ならputchar()で読み込んだ内容を標準出力に出力する
  7. 出力結果がエラーならプログラムを終了する

各関数がエラーを吐いていないかチェックするのを忘れずに。

 

Linuxにおける行

今まではファイルの中身を単なるバイト列として扱っていたが、行で扱うこともできる。

Linuxにおいては、"\n"までが1行という扱いになる。

例外として、ファイルの最後やストリームからの入力の末尾に限っては"\n"がなくても1行とみなす。

 

fgets(3)

第3引数で指定されたストリームからから1行読み込んで、第1引数で指定されたバッファに格納する。

この関数は1行読み込んで終了したのか、第2引数で与えられたバッファサイズ-1だけ読み込んで終了したのかが判別できない。

区別するためにはgetc()を使って読み込むしかない。

 

gets(3)

fgets()に似た関数でgets()関数というものがあるが、この関数はバッファオーバーフローを起こす可能性があるので絶対に使ってはいけない。

 

バッファオーバーフロー

バッファをはみ出して使ってしまうこと。

たとえば、バッファがbuf[256]のときにbuf[5555]などに値を書き込んでしまうこと。

重大な問題やバグにつながる。

システムに用意されているAPIだからといって安全というわけではない。

 

fputs(3)

第1引数の文字列を第2引数のストリームに書き込む。

途中に"\n"が含まれていても全て出力する。

fgets()の対になるかと思いきやそうでもないので注意が必要。

エラーが起きた場合、errnoに値が格納されるがストリームが終了した場合と区別するためにはあらかじめerrnoに0を設定しておく。

マルチスレッド環境でグローバル変数のerrnoを使うのはどうなの?っていう問いに関しては以下を参照。

docs.oracle.com

 

puts(3)

1行、出力先が標準出力固定。

最後に改行コードを追加して出力する。

与えられた文字列が改行コードで終わっていても最後に改行コード追加する。

 

printf(3), fprintf(3)

指定されたフォーマットに従って文字列を出力する。

printf()は出力先が標準出力、fpirntf()は引数でしてされたストリーム。

フォーマット指定子などの説明は割愛。

 

fscanf(3)

フォーマットを指定して入力ができる。

この関数は潜在的にバッファオーバーフローを起こす危険があり、推奨されない。

 

fread(3)

ストリームからから固定長のデータを読み込み、バッファに書き込む。

ストリーム、バッファ、サイズは引数で指定する。

 

fwrite(3)

バッファから固定長のデータを読み込み、ストリームに書き込む。

ストリーム、バッファ、サイズは引数で指定する。

 

read(2), write(2)でいいのでは? 

fread(3)やfwrite(3)は他のstdio関数と一緒に使用することが可能。

stdioでは内部バッファを持っているので、バッファを経由しないread(2), write(2)とstdio関数を使用すると入出力の順序が入れ替わってしまうことがある。

また、システムコールをそのまま使うと指定したサイズだけ読み書きできなかった場合を想定し、while文などでループさせる処理が必要(それもread/writeをするたびに!)だが、標準ライブラリはそこらへんも内部で行ってくれるので簡潔なコードを書ける。

また、read(2), write(2)はUNIXのシステムコールなのでC言語の標準ライブラリAPIであるfread(3)やfwrite(3)を使うことでプログラムの可搬性が高くなる。

 

fseek(3), fseeko(3)

ストリームのファイルオフセットを任意の場所に移動する。

ストリームと移動先は引数で指定する。

fseek()とfseeko()の違いは引数の型のみ。

fseeko()の方が大きなファイルに対応できるので、基本的にこちらを使用する。

 

ftell(3), ftello(3)

引数で指定されたストリームのファイルオフセットを取得する。

ftell()とftello()の違いもfseek()とfseeko()と同様。

 

rewind(3)

引数で指定されたストリームのファイルオフセットをファイルの先頭に戻す。

 

fileno(3)

引数で指定されたストリームに対応するファイルディスクリプタを返す。

 

fdopen(3)

引数で指定されたファイルディスクリプタをラップするFILE型の値を新しく作成する。

 

ファイルディスクリプタとFILE型の混在

やろうと思えばファイルディスクリプタで直接ストリームを操作したり、FILE型を使ってストリームを操作したりできる。

しかし、直接ストリームを操作するとバッファを経由しないので避けるべき。

ファイルディスクリプタの使用はstdioではできない操作(ファイルを開くときのパーミッション指定)などに限定するのが良い。

 

fflush(3)

バッファの内容を即時write()する。

バッファの内容をwrite()することをフラッシュ(flush)という。

文字列を改行せずに端末に出力したいときなどに使う。

 

setvbuf(3)

バッファリングモードの変更と、自分が用意したバッファをstdioのバッファとして使用させることが可能。

 

feof()

引数で指定されたストリームのEOFフラグを取得する。

他のstdio APIでEOFに到達した後に初めて真(0以外の値)を返すので、入出力APIを使用する前ではなく使用した後にこの関数を使用するべき。

基本的にこの関数を使うより、次のferror()を使った方がよい。

 

ferror(3)

引数で指定されたストリームのエラーフラグを取得する。

fread()のように戻り値でエラーとEOFが区別できないAPIで、エラーを区別したいときに使用する。

 

clearerr(3)

引数で指定されたストリームのエラーフラグとEOFフラグをクリアする。

 

stdioの動作の確認

straceコマンドを使う。

動作中のプログラムが呼んだシステムコールを表示するツール。

 

練習問題 

1. タブ文字("\t")を「\t」という2文字、改行を「"$" + 改行」の2文字にに置き換えながら出力するcatコマンドを作る

 

サポートサイトの解説を見る前に書いたソースコードを載せます。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){

    for(int i = 1; i < argc; i++){
        FILE *f;
        int c;

        f = fopen(argv[i], "r");
        if(!f){
            perror(argv[i]);
            exit(1);
        }
        while((c = fgetc(f)) != EOF){
            switch(c){
                case '\t':
                    if(putchar('\\') < 0){
                        exit(1);
                    }
                    if(putchar('t') < 0){
                        exit(1);
                    }
                    break;
                case '\n':
                    if(putchar('&') < 0){
                        exit(1);
                    }
                default:
                    if(putchar(c) < 0){
                        exit(1);
                    }
            }
        }
        fclose(f);
    }
    exit(0);
}

 

詳しい説明が割愛しますが、fgetc()で読み込んだバイトの種類によってswitch-case文でputchar()する文字を変えています。

正しい実装方法と解説が見たい方は書籍のサポートサイトを参照してください。

 

2.stdio APIを使ってファイルを読み込み、その行数を表示するコマンド(wc -lと同じ)を書く。 ファイル末尾に"\n"がない場合にも対応する。

 

サポートサイトの解説を見る前に書いたソースコードを載せます。

 

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){

    FILE *f;
    int pre;
    int c;
    int line = 0;

    if(argc != 2){
        printf("arg err!\n");
        exit(0);
    }

    f = fopen(argv[1], "r");
 
     if(!f){
        perror(argv[1]);
        exit(1);
    }

    while((c = fgetc(f)) != EOF){
        if(c == '\n'){
            line++;
        }
        pre = c;
    }
    
    if(pre != '\n'){
        line++;
    }
    
    fclose(f);

    printf("line = %d\n", line);

    exit(0);
}

 

細かい説明は割愛しますが、fgetc()で読み込んだデータが"\n"ならば行カウンタを増やす処理をしています。

また、ファイルの最後に"\n"がない場合もカウンタを増やしています。

EOFに当たったら前の文字を取得できないので、fgetc()を行う前にひとつ前に読み込んだ文字を保持しています。

 

3.fread()とfwrite()を使ってcatコマンドを作る。

サポートサイトの解説を見る前に書いたソースコードを載せます。

 

#include <stdio.h>
#include <stdlib.h>

#define BUFSIZE 50

int main(int argc, char *argv[]){
    size_t size = 10;
    size_t no = 5;
    char buf[BUFSIZE];
    FILE *f;
    int ret;

    if(argc < 2){
        printf("arg err.");
    }

    for(int i = 1; i < argc; i++){
        f = fopen(argv[i], "r");
        if(!f){
            perror(argv[i]);
            exit(1);
        }
    
        while(1){
            ret = fread(buf, sizeof(buf[0]), sizeof(buf), f);
            if(ret < sizeof(buf)){
                if(ferror(f)){
                    perror(argv[i]);
                    exit(1);
                }
            }

            fwrite(buf, sizeof(buf[0]), ret, stdout);

            if(ret < 5){
                break;
            }
        }
        fclose(f);
    }
    exit(0);
}

 

細かい説明は割愛しますが、コマンドライン引数の数だけループを回し、与えられたファイルをfopen()で開き、fread()で内容を読み込みfwrite()で標準出力に出しています。

ファイルの最後まで読み込んで内容を出力したらfclose()でストリームを閉じます。 

 

6章の内容はこんな感じです。

7章の内容はこちら。

amistad06-a.hatenablog.com

 

おわり。

 

【スポンサーリンク】