ランダムアクセス

fread() はストリームの「現在位置」からデータを読み込む。任意の位置のデータを読み込むためにはこの「現在位置」を動かしてやる必要がある。
fseek() はそのための関数だ。

int fseek(FILE *ストリーム, long オフセット, int 開始位置);

fseek() は「開始位置」から「オフセット」バイトだけ「現在位置」を移動する。「開始位置」は次のマクロのどれかでなければいけない。

SEEK_SETファイルの先頭から
SEEK_CUR現在位置から
SEEK_ENDファイルの終わりから

これらのマクロは stdio.h で定義されている。

fseek() は成功すると 0 を返し、失敗すると 0 以外の値を返す。

ところで、ファイルの現在位置を知りたいときは ftell() を使う。

long ftell(FILE *ストリーム);

ftell() は成功すると現在位置を返し、失敗すると -1 を返す。

次のプログラムは、10 個のデータ(double型)をファイルに書き込み、ファイルを開き直す。そして何番目のデータを表示したいかをユーザーに尋ねて、それを表示する。

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

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    long loc;
    double value;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    if (fwrite(d, sizeof d, 1, fp) != 1) {
        printf("Error: at writing.\n");
        exit(1);
    }
    fclose(fp);

    /* re-open file */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    printf("What number of element? [0-9] ");
    scanf("%ld", &loc);
    if (fseek(fp, loc * sizeof(double), SEEK_SET)) {
        printf("Seek error.\n");
        exit(1);
    }

    fread(&value, sizeof(double), 1, fp);
    printf("Element %ld is %f\n", loc, value);

    fclose(fp);

    return 0;
}

47 行目で fseek() を使っている。loc に double 型のサイズをかけて、移動すべきバイト数を求めている。

実行例:

takatoh@nightschool $ ./sample_9_6
What number of element? [0-9] 4
Element 4 is 0.897000

バイナリデータの入出力

fread()とfwrite()

バイナリデータを読み書きするには、fread() と fwrite() 関数を使う。これらはどんなデータでもバイナリ表現を使って読み書きができる。一般的な形式は次のとおり。

size_t fread(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);
size_t fwrite(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);

fread() は「ストリーム」から「サイズ」バイトの大きさのオブジェクトを「数値」個読み込んで、「バッファ」の指す領域に格納する。戻り値は実際に読み込んだオブジェクトの数。これが「数値」よりも小さければ、エラーが発生したか、ファイルの終わりに達したことになる。どちらかは feof() と ferror() で調べられる。

fwrite() は逆のことを行う。「バッファ」から「サイズ」バイトの大きさのオブジェクトを「数値」個、「ストリーム」に書き込む。戻り値は書き込んだオブジェクト数で、「数値」よりも小さければエラーが発生したということだ。

void型ポインタ

上の、「void *バッファ」というのは、void 型のポインタだ。void 型のポインタは、型変換を行わずに任意の型のデータを指すことのできるポインタで、汎用ポインタ(generic pointer)と呼ばれる。
fread() や fwrite() ではどのような型のデータを扱うかわからないので(違う言い方をすると、どんな型のデータでも扱えるように)、void 型のポインタを使っている。

size_t

size_t 型は stdio.h で定義されている型で、この型の変数はコンパイラがサポートする最大オブジェクトの大きさを持つ値を保持できる。基本の型ではなく size_t 型を使うのは、コンパイラがそれぞれの環境の違いを吸収できるようにするため。

sizof

sizeof 演算子は型、あるいは変数の大きさをバイト数で返す。

sizeof(型);
sizeof 変数;

sizeof を型に対して使うときにはカッコで囲む必要があるけど、変数に対して使うときにはカッコが囲んでも囲まなくてもいい。

例1

次のプログラムは、10 この要素(double 型)をバイナリでファイルに書き込み、開きなおして読み込んで、画面に表示する。当然だけど、ファイルを開くときはバイナリモードで開く必要がある。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    for (i = 0; i < 10; i++) {
        if (fwrite(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at writing.\n");
            exit(1);
        }
     }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    for (i = 0; i < 10; i++) {
        if (fread(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at reading.\n");
            exit(1);
        }
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}
takatoh@nightschool $ ./sample_9_5a
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

ちなみに myfile ファイルを cat すると、

takatoh@nightschool $ cat myfile
�(\u$@��Q��3@�p=
�Q������)@NbX9��?fffff�&@�(\�R@)\�(�?_�@takatoh@nightschool $

となっている。バイナリだから人間には読めない。

例2

上の例では、配列の要素を1つずつ書き込み・読み込みしていたけど、配列はメモリ上に連続して確保されているので、1つの塊として1度で済ませることもできる。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    if (fwrite(d, sizeof d, 1, fp) != 1) {
        printf("Error: at writing.\n");
        exit(1);
    }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    if (fread(d, sizeof d, 1, fp) != 1) {
        printf("Error: at reading.\n"); exit(1);
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}

30 行目で配列を一気に書き込み、49 行目では一気に読み込んでいる。結果は同じになる。

takatoh@nightschool $ ./sample_9_5b
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

高レベルテキスト関数

fputs()

fputs() はストリームに文字列を書き込む。

int fputs(char *文字列, FILE *ストリーム);

fputs() は書き込みが成功すると負でない値を返し、エラーが発生すると EOF を返す。文字列の終端にあるヌル文字は書き込まれず、改行文字が追加されることもない。

fgets()

char *fgets(char *文字列, int 数値, FILE *ストリーム);

fgets() はストリームから文字を読み込み、文字列に書き込んでいく。この動作は、

  • 数値 – 1 個分の文字を読む
  • 改行文字に出会う
  • ファイルの終わりに達する

まで続けられる。どの場合にも、文字列の終端にはヌル文字が付け加えられる。
fgets() は成功すると文字列へのポインタを返し、エラーが発生するとヌルポインタを返す。

例1

次のプログラムは、キーボードから入力された文字列を読み込み、ファイルへ保存する。そしてファイルを開きなおして、内容を表示する。

#include
#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    char str[80];

    /* check parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

    /* open file to write */
    if ((fp = fopen(argv[1], "w")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

    printf("Input string. Empty to quit.\n");
    do {
        printf(": ");
        gets(str);
        strcat(str, "\n");
        if (*str != '\n') {
            fputs(str, fp);
        }
    } while (*str != '\n');
    fclose(fp);

    /* open file to read */
    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("Cannot open file %s\n", argv[1]);
        exit(1);
    }

    /* read from file */
    do {
        fgets(str, 80, fp);
        if (!feof(fp)) {
            printf("%s", str);
        }
    } while (!feof(fp));
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4a sample_9_4a.txt
Input string. Empty to quit.
: Hello
: I'm takatoh
: How are you?
: 
Hello
I'm takatoh
How are you?
takatoh@nightschool $ cat sample_9_4a.txt
Hello
I'm takatoh
How are you?

fprintf()とfscanf()

これらは printf() と scnf() のストリーム版だ。コンソールから入出力する代わりに、ストリームから入出力する。

int fprintf(FILE *ストリーム, char *制御文字列, ...);
int fscanf(FILE *ストリーム, char *制御文字列, ...);

例2

次のプログラムは、double 値と int 値と文字列をコマンドラインで指定されたファイルに書き込み、それから、ファイルから読み込みなおして表示する。

#include
#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    double ld;
    int d;
    char str[80];

    /* chech parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

    /* open file to write */
    if ((fp = fopen(argv[1], "w")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

    fprintf(fp, "%f %d %s", 12345.342, 1908, "hello");
    fclose(fp);

    /* open file to read */
    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

    fscanf(fp, "%lf%d%s", &ld, &d, str);
    printf("%f %d %s\n", ld, d, str);
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4b
Specify file name
takatoh@nightschool $ ./sample_9_4b sample_9_4b.txt
12345.342000 1908 hello
takatoh@nightschool $ cat sample_9_4b.txt
12345.342000 1908 hellotakatoh@nightschool $

書き込んだファイルの最後に改行文字がついてないせいで次のプロンプトが表示されちゃてるけど、画面に表示されるのと同じように、ファイルにも書き込まれているのがわかる。

練習問題9.3

1. コマンドラインで指定されたファイル(テキストまたはバイナリ)のバイト数を数え、その結果を表示するプログラムを作成してください。

#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    long count = 0;
    char ch;

    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    if ((fp = fopen(argv[1], "rb")) == NULL) {
        printf("Channot open file: %s\n", argv[1]);
        exit(1);
    }

    while (!feof(fp)) {
        ch = fgetc(fp);
        if (ferror(fp)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        count++;
    }

    if (fclose(fp) == EOF) {
        printf("Error: at closing file.\n");
        exit(1);
    }

    printf("%ld bytes\n", count - 1);

    return 0;
}

出力部分で – 1 しているのは、EOF も数えてしまっているから、それを差し引いている。

実行例:

takatoh@nightschool $ ./practice_9_3_1 sample_9_3
8925 bytes
takatoh@nightschool $ ls -l sample_9_3
-rwxrwxr-x 1 takatoh takatoh 8925  5月 10 19:11 sample_9_3

feof()とferror()

fgetc() が EOF を返した場合、エラーが起こったのかファイルの終わりに達したのか判定できない。バイナリファイルを読み込んでいる途中で EOF と同じ値が返ってきた時もファイルの終わりなのか有効なデータなのか判定できない。
これらを判定するためには、feof() と ferror() を使う。一般的な形式は次のとおり。

int feof(FILE *ストリーム);
int ferror(FILE *ストリーム);

feof() はストリームの終わりに達していると 0 以外の値(真)を返し、そうではければ 0 (偽)を返す。
ferror() はエラーが起これば 0 以外の値を返し、そうでなければ 0 を返す。

FILE *fp;
.
.
.
while (!feof(fp)) {
    ch = fgetc(fp);
    if (ferror(fp)) {
        printf("File error!\n");
        break;
    }
}

上のコードでは、feof(fp) が偽のあいだ while でループし、ストリームからデータを読み込むたびに ferror(fp) でエラーチェックをしている。

次のプログラムはファイルのコピーを行う。その際、完全なエラー検査を行う。

#include
#include

int main(int argc, char *argv[])
{
    FILE *from, *to;
    char ch;

    if (argc != 3) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    /* open src file */
    if ((from = fopen(argv[1], "rb")) == NULL) {
        printf("Cannot open src file.\n");
        exit(1);
    }

    /* open dest file */
    if ((to = fopen(argv[2], "wb")) == NULL) {
        printf("Cannot open dest file.\n");
        exit(1);
    }

    /* copy file */
    while (!feof(from)) {
        ch = fgetc(from);
        if (ferror(from)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        if (!feof(from)) {
            fputc(ch, to);
        }
        if (ferror(to)) {
            printf("Error: at writing to file.\n");
            exit(1);
        }
    }

    if (fclose(from) == EOF) {
        printf("Error: at closing src file.\n");
        exit(1);
    }

    if (fclose(to) == EOF) {
        printf("Error: at closing dest file.\n");
        exit(1);
    }

    return 0;
}

実行例:

takatoh@nightschool $ ./sample_9_3
Usage: ./sample_9_3  
takatoh@nightschool $ ./sample_9_3 myfile myfile2
takatoh@nightschool $ cat myfile2
This is a test for file system.

ファイルシステムの基礎

ファイルシステムというか、ファイル入出力の基礎。

fopen()

ファイルを開くとき、つまりファイルをストリームと結びつけるときには fopen() を使う。fopen() の一般的な形式は次のとおり。

FILE *fopen(char *ファイル名, char *モード);

本(「独習 C」)に習ってプロトタイプ風に書いてみた。FILE * は戻り値の型を示していて、FILE という構造体へのポインタだ。
fopen() をはじめ、ファイルシステム関連の関数は stdio.h を使う。FILE も stdio.h で定義されている。
モードは、書き込みか読み込みか、あるいはテキストかバイナリかなどを示す文字列(へのポインタ)だ。次のようなモードがある。

r読み込み用にテキストファイルを開く
w書き込み用にテキストファイルを開く
aテキストファイルに追加する
rb読み込み用にバイナリファイルを開く
wb書き込み用にバイナリファイルを開く
abバイナリファイルに追加する
r+読み込み/書き込み用にテキストファイルを開く
w+読み込み/書き込み用にテキストファイルを作成する
a+読み込み/書き込み用にテキストファイルに追加する。またはテキストファイルを作成する。
r+b読み込み/書き込み用にバイナリファイルを開く
w+b読み込み/書き込み用にバイナリファイルを作成する
a+b読み込み/書き込み用にバイナリファイルに追加する。またはバイナリファイルを作成する。

ファイルを開く操作が失敗すると、fopen() はヌルポインタを返す。stdio.h では NULL というマクロがヌルポインタとして定義されている。ファイルを開いた時には、それが成功したかどうかをチェックしなければいけない。

FILE *fp;

if ((fp = fopen("myfile", "r")) == NULL) {
    printf("Channot open the file.");
    exit(1);
}

fclose()

ストリームが不要になった時にはこれを閉じる必要がある。ストリーム(ファイル)を閉じるには fclose() を使う。

int fclose(FILE *ストリーム);

文字の読み込みと書き込み

文字の読み込みには fgetc()、書き込みには fputc() をつかう。

int fgetc(FILE *ストリーム);
int fputc(int 文字, FILE *ストリーム);

fgetc() はストリームの次のバイトを unsigned char として読み込み、int として返す。エラーが起こったり、ファイルの終わりに達すると EOF を返す。fgetc() は int を返すけど、読み込んだ文字は下位バイトに含まれるので char に代入することもできる。
fputc() はストリームに「文字」の下位バイトを unsigned char の値として書き込む。「文字」は int と定義されているけど、char を渡しても問題ない。書き込みが成功すると書き込んだ文字を返し、失敗すると EOF を返す。

次のプログラムは、文字列をテキストファイル myfile に書き込み、いったん閉じたあと読み込み用に開きなおして、内容を表示する。

#include
#include

int main(void)
{
    char str[80] = "This is a test for file system.\n";
    FILE *fp;
    char ch, *p;

    /* open myfile to write */
    if ((fp = fopen("myfile", "w")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* write str. */
    p = str;
    while (*p) {
        if (fputc(*p, fp) == EOF) {
            printf("Error.\n");
            exit(1);
        }
        p++;
    }
    fclose(fp);

    /* open myfile to read */
    if ((fp = fopen("myfile", "r")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* read the file */
    for ( ; ; ) {
        ch = fgetc(fp);
        if (ch == EOF) {
            break;
        }
        putchar(ch);
    }
    fclose(fp);

    return 0;
}

実行結果:

takatoh@nightschool $ ./sample_9_2
This is a test for file system.

練習問題9.2

1. コマンドライン引数で指定したテキストファイルの内容を表示するプログラムを作成してください。

#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    char ch;

    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./practice_9_2_1
Usage: ./practice_9_2_1 <textfile>
takatoh@nightschool $ ./practice_9_2_1 myfile
This is a test for file system.

ストリームとファイル

C の入出力システムでは、プログラムと実際のデバイスとのあいだに抽象的なレベルを設けている。これをストリームという。ストリームのおかげで、プログラムからは実際のデバイスにかかわらずほとんど同じように扱える。
これに対して、実際のデバイスをファイルと呼ぶ。この文脈ではいわゆる普通のファイルだけでなく、画面、キーボード、ポートなどを含んでいる。

ストリームには、テキストストリームとバイナリストリームがある。
テキストストリームは ASCII 文字を扱う。テキストストリームでは、何らかの文字変換が行われることがある。例えば、改行文字を出力する場合には大抵復帰改行に変換される。なので、ストリームに送られるデータとファイルに書き込まれるデータが一致するとは限らない。
一方、バイナリストリームはどんな形式のデータでも扱うことが出来、文字変換も行わない。ストリームに送られれデータとファイルに書き込まれるデータは一致する。

もうひとつ、現在位置(current possition)という概念がある。現在位置は次のファイルアクセスが起こる位置を示している。例えば、100バイトのファイルがあり、すでに半分読み込んでいるとすると、次のアクセスでは 50 バイトめ(51バイトめ?)であり、これが現在位置ということになる。

ふむ、まあ、何とかわかった。次はいよいよファイルアクセスだ。

scanf()

scanf() の一般的な形式は次のとおり。

scanf(制御文字列, ...);

制御文字列は printf() とよく似たフォーマット指定子とその他の文字からなる。「…」の部分は可変長の引数で、制御文字列内のフォーマット指定子と対応する。これらの引数はフォーマット指定子の指定する型のアドレスでなければならない。scanf() 自体は入力されたフィールドの数を返す。

フォーマット指定子

%c文字
%d10進数
%i10進数
%e浮動小数点数
%f浮動小数点数
%g浮動小数点数
%o8進数
%s文字列
%x16進数
%pポインタ
%nこれまで読み込まれた文字数に等しい整数値を受け取る
%u符号なし整数
%[]文字集合

%c と %n 以外については最大フィールド幅を指定できる。
注意しなくちゃいけないのは %s は文字列を読み込むけど、空白文字が現れるとそこで読み込みをやめてしまうこと。例えば、

This is a test.

という文字列を %s で読み込もうとしても、「This」しか読み込まれない。

#include

int main(void)
{
    char str[80];

    printf("Input string: ");
    scanf("%s", str);
    printf("%s\n", str);

    return 0;
}
takatoh@nightschool $ ./sample_8_6
Input string: This is a test.
This

こういう場合は代わりに gets() を使う。

文字集合(スキャン集合)

%[ ] は大カッコで囲まれた文字を入力する。これをスキャン集合と呼ぶ。例えば

%[ABC]

という指定は A、B、C の続く限り読み込まれる。ほかの文字が現れた時点で次のフォーマット指定子に移る。[ ] 内の最初に ^ をつけることで否定を意味する。

#include

int main(void)
{
    char str[80];

    printf("Input string: ");
    scanf("%[a-z]", str);
    printf("%s\n", str);

    return 0;
}
takatoh@nightschool $ gcc sample_8_6b.c -o sample_8_6
takatoh@nightschool $ ./sample_8_6
Input string: ABCdefGHI
�l,

ありゃ、文字化けする。なんかおかしいんだろうか。

フォーマット指定子内の*

フォーマット指定子に * をつけると、そのフィールドは読み込まれずに捨てられる。入力に不要な文字が含まれている場合に便利。

int first, second;
scanf("%d%*c%d", &first, &second);

に対して

123-456

という入力があったとすると、first には 123、second には 456 が入力される。捨てられるフィールドに対応する引数は不要。

#include

int main(void)
{
    int first, second;

    printf("Input: ");
    scanf("%d%*c%d", &first, &second);
    printf("%d %d\n", first, second);

    return 0;
}
takatoh@nightschool $ ./sample_8_6c
Input: 123-456
123 456

制御文字列内のその他の文字の働き

制御文字列内に空白文字がある場合、scanf() は空白文字でない文字が現れるまで空白文字を捨てていく。その他の文字がある場合、一致しない文字が現れるまでその文字すべてを捨てていく。

include

int main(void)
{
    int first, second;

    printf("Input: ");
    scanf("A%d% %d", &first, &second);
    printf("%d %d\n", first, second);

    return 0;
}
takatoh@nightschool $ gcc sample_8_6d.c -o sample_8_6d
sample_8_6d.c: In function ‘main’:
sample_8_6d.c:9:5: warning: unknown conversion type character 0x20 in format [-Wformat=]
     scanf("A%d% %d", &first, &second);
     ^
takatoh@nightschool $ ./sample_8_6d
Input: AAA123    456
0 0

あれ、これもうまくいかない。

printf()

printf() はこれまでにも使ってきたけど改めて。一般的な形式は次のとおり。

printf(制御文字列, ...);

「…」の部分は可変長の引数で、制御文字列内に現れるフォーマット指定子に(数、順番ともに)対応する。printf() 自体は出力した文字数を返す。
フォーマット指定子は次のものがある。

%c文字
%d符号付き10進整数
%i符号付き10進整数
%e指数部付き表記(小文字)
%E指数部付き表記(大文字)
%f10進の浮動小数点数
%g%eと%fのいずれか短いほう
%G%Eと%fのいずれか短いほう
%o符号なし8進数
%s文字列
%u符号なし10進数
%x符号なし16進数(小文字)
%X符号なし16進数(大文字)
%pポインタを表示
%n対応する引数は整数へのポインタでなければならず、その領域にこれまで出力された文字数を書き込む
%%%記号

%%、%c、%p、%n以外は最小フィールド幅指定子と精度指定子を指定できる。また、デフォルトでは数値は右詰めで表示されるけど、-記号をつけることで左詰めにできる。ま、だいたいわかるな。

ちょっとわかりづらいのは %n だ。これは出力するためのものではなくて、出力した文字数を保存するためのものだ。次のプログラムはその例。

#include

int main(void)
{
    int i;

    printf("%d %f\n%n", 100, 123.45, &i);
    printf("Output %d characters\n", i);

    return 0;
}
takatoh@nightschool $ ./sample_8_5
100 123.450000
Output 15 characters

%n が現れるまでに 15 文字出力したと(15 文字目は改行文字)。

gets()とputs()

gets() はキーボードから文字列を読み込む。puts() はコンソールに文字列を出力する。どちらも stdio.h が必要。

gets()

キーボードからの入力というのは、ユーザーが文字列に続いて Enter キーを押すまで。Enter キーが押されると、文字列を読み込んで最後にある改行をヌル文字に置き換えたうえで引数の配列に格納する。関数自体の戻り値は、入力が成功すると配列の先頭へのポインタを、失敗するとヌルポインタを返す。
次のプログラムでは、gets() の戻り値を p に格納して、p がヌルでないことを確認してから出力している。p も str も配列(文字列)の先頭のアドレスを保持しているので、同じ文字列が2回出力されるはず。

#include

int main(void)
{
    char *p, str[80];

    printf("Input string: ");
    p = gets(str);
    if (p) { /* if p is not null */
        printf("%s %s\n", p, str);
    }

    return 0;
}

前にも書いたと思うけど、コンパイルすると gets は危険だから使うべきじゃないと警告される。

実行例:

takatoh@nightschool $ ./sample_8_4a
Input string: hello
hello hello

puts()

puts() はポインタの指している文字列を出力する。その際、自動的に改行を追加する。

#include

int main(void)
{
    puts("A");
    puts("B");
    puts("C");

    return 0;
}
takatoh@nightschool $ ./sample_8_4b
A
B
C

ちゃんと1つずつ改行されてる。
puts() は printf() よりもサイズが小さくて速い。なので、複雑な文字列を出力するのでなければ puts() を使ったほうが有利。