閉じる

C言語のfgets()でファイルを1行ずつ読む基本と実例

ファイルを1行ずつ安全に読み込むには、C言語のfgetsが基本になります。

行の区切りや改行の扱い、バッファサイズ、エラー処理などを理解しておけば、ログ解析や設定ファイルの読み取りなど多くの実務で役立ちます。

本記事ではfgetsを使った堅牢な行入力の基礎から実用パターンまで、初心者の方にも分かりやすく丁寧に解説します。

fgets()の基本

fgets()はファイルから1行を読み込む関数

fgetsはストリーム(ファイルなど)から文字列を1行単位で読み込む関数です。

最大size - 1文字までを読み込み、末尾に必ず'\0'(ヌル文字)を付与します。

改行文字'\n'バッファに残る仕様で、行の途中でバッファが一杯になった場合やファイルの最後の行に改行が付いていない場合は、改行が含まれないことがあります。

必要なヘッダ

fgets本体は#include <stdio.h>で宣言されています。

文字列操作でstrlenstrcspnを使うなら#include <string.h>、空白判定でisspaceを使うなら#include <ctype.h>、エラー表示にperrorerrnoを使う場合は#include <errno.h>も併せてインクルードします。

FILEポインタの基本

FILE *は標準ライブラリが提供するストリームのハンドルです。

ファイルを読むにはfopenで開き、終わったらfcloseで必ず閉じます。

読み取り専用は"r"、追記や書き込みは別モードになりますが、本記事では読み取りに集中します。

戻り値(NULL/ポインタ)の意味

fgetsは成功時にバッファの先頭アドレスを返し、失敗時にNULLを返します。

失敗にはEOFに達した場合と読み取りエラーの2種類があります。

区別にはfeofferrorを使います。

以下に代表的な挙動をまとめます。

状況戻り値バッファ内容例備考
通常の行バッファ先頭“Hello\n\0”改行が含まれる
最後の行で改行ありバッファ先頭“Last line\n\0”改行が含まれる
最後の行で改行なしバッファ先頭“Last line\0”改行は含まれない
文字数がバッファ上限超えバッファ先頭“Very lo…<省略>\0”改行は含まれないことが多い
EOF直後NULL変更不定feof(fp)が真
読み取りエラーNULL変更不定ferror(fp)が真

改行が残る仕様

fgetsは原則として改行'\n'をバッファに残します

そのため、そのままprintf("%s", buf)と出力すると二重改行にならず自然に表示できます。

一方、改行を除去してから処理したい場合は後述の「改行を削除する方法」を使います。

Windowsのテキストは"\r\n"で終わるため、末尾の'\r'にも注意が必要です。

バッファとサイズの指定

fgets(buf, size, fp)sizeバッファ全体のサイズを指定します。

関数は最大size - 1文字を読み、最後に'\0'を付けます。

典型的にはchar buf[1024];とし、呼び出し時にsizeof(buf)を渡します。

旧来のgetsは危険ですので絶対に使用しないでください

fgets()でテキストファイルを1行ずつ読むサンプル

手順

手順はシンプルです。

まずfopenで読み取りモード"r"としてファイルを開き、ループでfgetsを呼び続けます。

戻り値がNULLになったらループを抜け、feofferrorで終了理由を確認し、最後にfcloseで閉じます。

サンプルファイルの内容(example.txt)

以下のテキストを例にします。

Hello, world!
This is the second line.

# A comment line
The last line has no newline at end

上の最終行は便宜上改行無しとします。

最小サンプルコード

読み込んだ行をそのまま表示するだけの最小例です。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = "example.txt";
    FILE *fp = fopen(path, "r");                // 読み取りで開く
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
        // ここで終了する場合はfcloseは不要
    }

    char buf[256];                               // 1行バッファ
    // fgetsは最大でsizeof(buf) - 1文字読み、末尾に'
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *path = "example.txt";
FILE *fp = fopen(path, "r");                // 読み取りで開く
if (!fp) {
perror("fopen");
return EXIT_FAILURE;
// ここで終了する場合はfcloseは不要
}
char buf[256];                               // 1行バッファ
// fgetsは最大でsizeof(buf) - 1文字読み、末尾に'\0'を付ける
while (fgets(buf, sizeof buf, fp)) {
// fgetsは原則改行を含むため、printf("%s", buf)で二重改行にならない
printf("%s", buf);
}
if (ferror(fp)) {
// 読み取り途中にエラーがあればここで検出
fprintf(stderr, "read error occurred\n");
fclose(fp);
return EXIT_FAILURE;
}
fclose(fp);
return EXIT_SUCCESS;
}
'を付ける while (fgets(buf, sizeof buf, fp)) { // fgetsは原則改行を含むため、printf("%s", buf)で二重改行にならない printf("%s", buf); } if (ferror(fp)) { // 読み取り途中にエラーがあればここで検出 fprintf(stderr, "read error occurred\n"); fclose(fp); return EXIT_FAILURE; } fclose(fp); return EXIT_SUCCESS; }
実行結果
Hello, world!
This is the second line.

# A comment line
The last line has no newline at end

while(fgets())で最後まで読む

while (fgets(...))という書き方は典型パターンです。

条件式でNULLかどうかを判定しつつ、読み取った文字列はバッファに残ります。

なお、ループ終了時にfeofferrorでEOFかエラーかを判定することが推奨です。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("example.txt", "r");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    char line[1024];
    size_t line_count = 0;

    while (fgets(line, sizeof line, fp)) {
        ++line_count;
        // 必要なら処理をここに書く
    }

    if (ferror(fp)) {
        fprintf(stderr, "Error while reading. read lines=%zu\n", line_count);
    } else if (feof(fp)) {
        // EOFで正常終了
        fprintf(stderr, "EOF reached. total lines=%zu\n", line_count);
    }

    fclose(fp);
    return 0;
}

出力例(標準エラー出力):

text
EOF reached. total lines=5

1行ごとに処理して表示

ここでは行番号を付けて表示し、末尾改行を取り除いてから出力します。

C言語
#include <stdio.h>
#include <string.h>

// 改行('\n')とCR('\r')を末尾から取り除くヘルパ
static void chomp(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
        s[--n] = '
#include <stdio.h>
#include <string.h>
// 改行('\n')とCR('\r')を末尾から取り除くヘルパ
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
s[--n] = '\0';
}
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) {
perror("fopen");
return 1;
}
char line[256];
size_t lineno = 0;
while (fgets(line, sizeof line, fp)) {
++lineno;
chomp(line);                    // 改行を削除
printf("%4zu: %s\n", lineno, line);
}
if (ferror(fp)) {
fprintf(stderr, "read error\n");
}
fclose(fp);
return 0;
}
'; } } int main(void) { FILE *fp = fopen("example.txt", "r"); if (!fp) { perror("fopen"); return 1; } char line[256]; size_t lineno = 0; while (fgets(line, sizeof line, fp)) { ++lineno; chomp(line); // 改行を削除 printf("%4zu: %s\n", lineno, line); } if (ferror(fp)) { fprintf(stderr, "read error\n"); } fclose(fp); return 0; }
実行結果
   1: Hello, world!
   2: This is the second line!
   3: 
   4: # A comment line
   5: The last line has no newline at end

バッファサイズの決め方

バッファサイズは想定される最大行長に応じて決めます。

ログや設定ファイルは512〜4096程度で十分なことが多いですが、最悪ケースで行が非常に長いこともあります。

2つの方針があります。

  • 固定長で部分読み取り: 例えばchar buf[1024]で読み、改行がない間は追記していく。
  • 可変長で動的拡張: ヒープ領域をreallocで伸ばしながら1行をまるごと取り込む。

後者の例は「長い行がバッファに収まらない場合」で示します。

fgets()のエラー処理と注意点

NULLチェックとEOFの違い

fgetsがNULLを返すのは、EOFかエラーのいずれかです。

違いはfeof(fp)ferror(fp)で判定します。

両方が偽のケースはほぼありませんが、ストリームの状態が未定義のケースを避けるため、基本は両方を確認します。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("example.txt", "r");
    if (!fp) { perror("fopen"); return 1; }

    char buf[8];
    while (fgets(buf, sizeof buf, fp)) {
        // 何もしない
    }

    if (ferror(fp)) {
        fprintf(stderr, "I/O error detected\n");
    } else if (feof(fp)) {
        fprintf(stderr, "Reached EOF\n");
    } else {
        fprintf(stderr, "Stopped for unknown reason\n");
    }

    fclose(fp);
    return 0;
}
実行結果
Reached EOF

改行を削除する方法

改行を削除するには、以下のよくある手法があります。

  • strcspn"\r\n"のいずれかに当たった位置に'\0'を入れる
  • 末尾から'\n''\r'を連続で削る

汎用的な実装例です。

C言語
#include <string.h>

// 末尾のCRLFを含む改行を除去する
void chomp_all(char *s) {
    size_t n = strlen(s);
    while (n > 0) {
        char c = s[n - 1];
        if (c == '\n' || c == '\r') {
            s[--n] = '
#include <string.h>
// 末尾のCRLFを含む改行を除去する
void chomp_all(char *s) {
size_t n = strlen(s);
while (n > 0) {
char c = s[n - 1];
if (c == '\n' || c == '\r') {
s[--n] = '\0';
} else {
break;
}
}
}
'; } else { break; } } }

この方法なら、UNIXの"\n"でもWindowsの"\r\n"でも問題なく処理できます。

最後の行に改行がないケース

最後の行に改行が無い場合、fgets改行無しの文字列を返します。

従って、printf("%s\n", buf)のように自前で改行を入れるロジックにしておくと、最終行だけ行がつながって見える問題を避けられます。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("example.txt", "r");
    if (!fp) { perror("fopen"); return 1; }

    char line[64];
    while (fgets(line, sizeof line, fp)) {
        // 常に自分で改行を付けて出力
        // (lineに改行が含まれていても二重改行にはならないようchompしてから出す)
        for (char *p = line; *p; ++p) {
            if (*p == '\n' || *p == '\r') { *p = '
#include <stdio.h>
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char line[64];
while (fgets(line, sizeof line, fp)) {
// 常に自分で改行を付けて出力
// (lineに改行が含まれていても二重改行にはならないようchompしてから出す)
for (char *p = line; *p; ++p) {
if (*p == '\n' || *p == '\r') { *p = '\0'; break; }
}
printf("%s\n", line);
}
fclose(fp);
return 0;
}
'; break; } } printf("%s\n", line); } fclose(fp); return 0; }
実行結果
Hello, world!
This is the second line!

# A comment line
The last line has no newline at end

長い行がバッファに収まらない場合

固定長バッファでは、行が長すぎると分割されて読み込まれます

改行が見つかるまで読み足して1行を復元するか、動的にバッファを拡張します。

以下は動的拡張で1行を完全に取り込む関数例です。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 1行を動的に読み込む。成功: mallocした文字列(要free)、失敗またはEOF: NULL
// 改行は除去して返す。エラー判定はferror(fp)で行う。
char *readline_dyn(FILE *fp) {
    if (!fp) return NULL;

    size_t cap = 256;
    size_t len = 0;
    char *buf = (char *)malloc(cap);
    if (!buf) return NULL;

    for (;;) {
        // 一時バッファで読み取り。cap - len は残り容量。
        size_t chunk_cap = (cap - len);
        if (chunk_cap < 2) { // '
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1行を動的に読み込む。成功: mallocした文字列(要free)、失敗またはEOF: NULL
// 改行は除去して返す。エラー判定はferror(fp)で行う。
char *readline_dyn(FILE *fp) {
if (!fp) return NULL;
size_t cap = 256;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) return NULL;
for (;;) {
// 一時バッファで読み取り。cap - len は残り容量。
size_t chunk_cap = (cap - len);
if (chunk_cap < 2) { // '\0'と最低1文字分を確保
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
continue;
}
if (!fgets(buf + len, (int)chunk_cap, fp)) {
if (len == 0) { // 何も読めていない
free(buf);
return NULL; // EOFまたはエラー
}
break; // 既に部分的に読んだものがある
}
// 直近の読み取りで改行が入ったか判定
len += strlen(buf + len);
if (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
// 改行とCRを削除
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
break;
}
// 改行がなくバッファが埋まったので拡張を続ける
if (len + 1 >= cap) {
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
}
}
return buf; // 改行なし、終端'\0'付き
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char *line;
while ((line = readline_dyn(fp)) != NULL) {
printf("len=%zu | %s\n", strlen(line), line);
free(line);
}
if (ferror(fp)) {
fprintf(stderr, "read error\n");
}
fclose(fp);
return 0;
}
'と最低1文字分を確保 cap *= 2; char *nbuf = (char *)realloc(buf, cap); if (!nbuf) { free(buf); return NULL; } buf = nbuf; continue; } if (!fgets(buf + len, (int)chunk_cap, fp)) { if (len == 0) { // 何も読めていない free(buf); return NULL; // EOFまたはエラー } break; // 既に部分的に読んだものがある } // 直近の読み取りで改行が入ったか判定 len += strlen(buf + len); if (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { // 改行とCRを削除 while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { buf[--len] = '
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1行を動的に読み込む。成功: mallocした文字列(要free)、失敗またはEOF: NULL
// 改行は除去して返す。エラー判定はferror(fp)で行う。
char *readline_dyn(FILE *fp) {
if (!fp) return NULL;
size_t cap = 256;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) return NULL;
for (;;) {
// 一時バッファで読み取り。cap - len は残り容量。
size_t chunk_cap = (cap - len);
if (chunk_cap < 2) { // '\0'と最低1文字分を確保
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
continue;
}
if (!fgets(buf + len, (int)chunk_cap, fp)) {
if (len == 0) { // 何も読めていない
free(buf);
return NULL; // EOFまたはエラー
}
break; // 既に部分的に読んだものがある
}
// 直近の読み取りで改行が入ったか判定
len += strlen(buf + len);
if (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
// 改行とCRを削除
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
break;
}
// 改行がなくバッファが埋まったので拡張を続ける
if (len + 1 >= cap) {
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
}
}
return buf; // 改行なし、終端'\0'付き
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char *line;
while ((line = readline_dyn(fp)) != NULL) {
printf("len=%zu | %s\n", strlen(line), line);
free(line);
}
if (ferror(fp)) {
fprintf(stderr, "read error\n");
}
fclose(fp);
return 0;
}
'; } break; } // 改行がなくバッファが埋まったので拡張を続ける if (len + 1 >= cap) { cap *= 2; char *nbuf = (char *)realloc(buf, cap); if (!nbuf) { free(buf); return NULL; } buf = nbuf; } } return buf; // 改行なし、終端'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1行を動的に読み込む。成功: mallocした文字列(要free)、失敗またはEOF: NULL
// 改行は除去して返す。エラー判定はferror(fp)で行う。
char *readline_dyn(FILE *fp) {
if (!fp) return NULL;
size_t cap = 256;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) return NULL;
for (;;) {
// 一時バッファで読み取り。cap - len は残り容量。
size_t chunk_cap = (cap - len);
if (chunk_cap < 2) { // '\0'と最低1文字分を確保
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
continue;
}
if (!fgets(buf + len, (int)chunk_cap, fp)) {
if (len == 0) { // 何も読めていない
free(buf);
return NULL; // EOFまたはエラー
}
break; // 既に部分的に読んだものがある
}
// 直近の読み取りで改行が入ったか判定
len += strlen(buf + len);
if (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
// 改行とCRを削除
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
break;
}
// 改行がなくバッファが埋まったので拡張を続ける
if (len + 1 >= cap) {
cap *= 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); return NULL; }
buf = nbuf;
}
}
return buf; // 改行なし、終端'\0'付き
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char *line;
while ((line = readline_dyn(fp)) != NULL) {
printf("len=%zu | %s\n", strlen(line), line);
free(line);
}
if (ferror(fp)) {
fprintf(stderr, "read error\n");
}
fclose(fp);
return 0;
}
'付き } int main(void) { FILE *fp = fopen("example.txt", "r"); if (!fp) { perror("fopen"); return 1; } char *line; while ((line = readline_dyn(fp)) != NULL) { printf("len=%zu | %s\n", strlen(line), line); free(line); } if (ferror(fp)) { fprintf(stderr, "read error\n"); } fclose(fp); return 0; }
実行結果
len=13 | Hello, world!
len=24 | This is the second line!
len=0 | 
len=16 | # A comment line
len=36 | The last line has no newline at end

空行の判定

空行は"\n"のみの行や"\r\n"のみの行です。

fgets直後ならline[0] == '\n'で簡易判定できます。

改行を削除する運用なら、長さが0なら空行と扱えます。

空白類のみの行も空行として扱いたい場合は、後述のトリムを行い、長さが0かを調べます。

C言語
#include <stdio.h>
#include <string.h>
#include <ctype.h>

static void rstrip_crlf(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void rstrip_crlf(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static void trim_spaces(char *s) {
// 前後の空白を削る
char *p = s;
while (*p && isspace((unsigned char)*p)) ++p;
memmove(s, p, strlen(p) + 1);
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0';
}
int main(void) {
char line[64] = "   \t  \n";
rstrip_crlf(line);
trim_spaces(line);
printf("is_empty=%s\n", line[0] == '\0' ? "true" : "false");
return 0;
}
'; } static void trim_spaces(char *s) { // 前後の空白を削る char *p = s; while (*p && isspace((unsigned char)*p)) ++p; memmove(s, p, strlen(p) + 1); size_t n = strlen(s); while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void rstrip_crlf(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static void trim_spaces(char *s) {
// 前後の空白を削る
char *p = s;
while (*p && isspace((unsigned char)*p)) ++p;
memmove(s, p, strlen(p) + 1);
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0';
}
int main(void) {
char line[64] = "   \t  \n";
rstrip_crlf(line);
trim_spaces(line);
printf("is_empty=%s\n", line[0] == '\0' ? "true" : "false");
return 0;
}
'; } int main(void) { char line[64] = " \t \n"; rstrip_crlf(line); trim_spaces(line); printf("is_empty=%s\n", line[0] == '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void rstrip_crlf(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static void trim_spaces(char *s) {
// 前後の空白を削る
char *p = s;
while (*p && isspace((unsigned char)*p)) ++p;
memmove(s, p, strlen(p) + 1);
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0';
}
int main(void) {
char line[64] = "   \t  \n";
rstrip_crlf(line);
trim_spaces(line);
printf("is_empty=%s\n", line[0] == '\0' ? "true" : "false");
return 0;
}
' ? "true" : "false"); return 0; }
実行結果
is_empty=true

fopenやfcloseの失敗時の対処

必ず戻り値を確認し、失敗時はperrorfprintf(stderr,...)で状況を通知します。

開けなかった場合は直ちに処理を中断し、開けた後はfcloseを忘れないようにします。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = "missing.txt";
    FILE *fp = fopen(path, "r");
    if (!fp) {
        perror("fopen failed"); // OS依存の理由が出る(例: No such file or directory)
        return EXIT_FAILURE;
    }

    // ... 読み取り処理 ...

    if (fclose(fp) == EOF) {
        perror("fclose failed");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
実行結果
fopen failed: No such file or directory

fgets()の実用パターン

行番号を付けて表示

ログや検証で役に立つ行番号付き出力です。

C言語
#include <stdio.h>
#include <string.h>

static void chomp(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '
#include <stdio.h>
#include <string.h>
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char line[512];
size_t n = 0;
while (fgets(line, sizeof line, fp)) {
chomp(line);
printf("%05zu | %s\n", ++n, line);
}
fclose(fp);
return 0;
}
'; } int main(void) { FILE *fp = fopen("example.txt", "r"); if (!fp) { perror("fopen"); return 1; } char line[512]; size_t n = 0; while (fgets(line, sizeof line, fp)) { chomp(line); printf("%05zu | %s\n", ++n, line); } fclose(fp); return 0; }
実行結果
00001 | Hello, world!
00002 | This is the second line!
00003 | 
00004 | # A comment line
00005 | The last line has no newline at end

コメント行(先頭#)をスキップ

設定ファイル等で、#から始まる行を無視する例です。

前方空白を無視して判定します。

C言語
#include <stdio.h>
#include <string.h>
#include <ctype.h>

static void chomp(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static char *lskip_spaces(char *s) {
while (*s && isspace((unsigned char)*s)) ++s;
return s;
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char line[256];
while (fgets(line, sizeof line, fp)) {
chomp(line);
char *p = lskip_spaces(line);
if (*p == '#' || *p == '\0') {
// コメント行または空行をスキップ
continue;
}
printf("DATA: %s\n", p);
}
fclose(fp);
return 0;
}
'; } static char *lskip_spaces(char *s) { while (*s && isspace((unsigned char)*s)) ++s; return s; } int main(void) { FILE *fp = fopen("example.txt", "r"); if (!fp) { perror("fopen"); return 1; } char line[256]; while (fgets(line, sizeof line, fp)) { chomp(line); char *p = lskip_spaces(line); if (*p == '#' || *p == '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static char *lskip_spaces(char *s) {
while (*s && isspace((unsigned char)*s)) ++s;
return s;
}
int main(void) {
FILE *fp = fopen("example.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char line[256];
while (fgets(line, sizeof line, fp)) {
chomp(line);
char *p = lskip_spaces(line);
if (*p == '#' || *p == '\0') {
// コメント行または空行をスキップ
continue;
}
printf("DATA: %s\n", p);
}
fclose(fp);
return 0;
}
') { // コメント行または空行をスキップ continue; } printf("DATA: %s\n", p); } fclose(fp); return 0; }
実行結果
DATA: Hello, world!
DATA: This is the second line!
DATA: The last line has no newline at end

前後の空白をトリム

トリムはキーや値の正規化に有用です。

マルチバイトの全角空白はisspaceでは判定できない点に注意してください。

ここではASCII系の空白に限定した実装を示します。

C言語
#include <stdio.h>
#include <string.h>
#include <ctype.h>

static void chomp(char *s) {
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static void trim(char *s) {
char *p = s;
while (*p && isspace((unsigned char)*p)) ++p;      // 左トリム
memmove(s, p, strlen(p) + 1);
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0'; // 右トリム
}
int main(void) {
char line[] = "   key = value with spaces   \r\n";
chomp(line);
trim(line);
printf("\"%s\"\n", line);
return 0;
}
'; } static void trim(char *s) { char *p = s; while (*p && isspace((unsigned char)*p)) ++p; // 左トリム memmove(s, p, strlen(p) + 1); size_t n = strlen(s); while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
static void chomp(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) s[--n] = '\0';
}
static void trim(char *s) {
char *p = s;
while (*p && isspace((unsigned char)*p)) ++p;      // 左トリム
memmove(s, p, strlen(p) + 1);
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0'; // 右トリム
}
int main(void) {
char line[] = "   key = value with spaces   \r\n";
chomp(line);
trim(line);
printf("\"%s\"\n", line);
return 0;
}
'; // 右トリム } int main(void) { char line[] = " key = value with spaces \r\n"; chomp(line); trim(line); printf("\"%s\"\n", line); return 0; }
実行結果
"key = value with spaces"

CSVや設定ファイルを行単位で処理

単純なCSVならstrtokで分割できますが、引用符やエスケープを含む本格的なCSVは専用ライブラリを推奨します。

ここでは簡易CSVとキー=値形式の設定ファイルの例を示します。

C言語
#include <stdio.h>
#include <string.h>
#include <ctype.h>

// 簡易: カンマで分割(引用符対応なし)
static void split_csv_simple(char *line) {
    for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ",")) {
        // 前後空白を軽く削る
        while (*tok && isspace((unsigned char)*tok)) ++tok;
        char *end = tok + strlen(tok);
        while (end > tok && isspace((unsigned char)end[-1])) *--end = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// 簡易: カンマで分割(引用符対応なし)
static void split_csv_simple(char *line) {
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ",")) {
// 前後空白を軽く削る
while (*tok && isspace((unsigned char)*tok)) ++tok;
char *end = tok + strlen(tok);
while (end > tok && isspace((unsigned char)end[-1])) *--end = '\0';
printf("field: \"%s\"\n", tok);
}
}
// キー=値形式の行をパース(前後空白をトリム)
static int parse_kv(char *line, char **out_key, char **out_val) {
char *eq = strchr(line, '=');
if (!eq) return 0;
*eq = '\0';
char *key = line;
char *val = eq + 1;
// 左右トリム
while (*key && isspace((unsigned char)*key)) ++key;
char *kend = key + strlen(key);
while (kend > key && isspace((unsigned char)kend[-1])) *--kend = '\0';
while (*val && isspace((unsigned char)*val)) ++val;
char *vend = val + strlen(val);
while (vend > val && isspace((unsigned char)vend[-1])) *--vend = '\0';
*out_key = key;
*out_val = val;
return 1;
}
int main(void) {
// CSV例
{
char csv[] = "apple, banana , cherry";
printf("[CSV]\n");
split_csv_simple(csv);
}
// 設定ファイル例
{
char kvline[] = "  host = localhost  ";
char *k, *v;
printf("[KV]\n");
if (parse_kv(kvline, &k, &v)) {
printf("key=\"%s\", value=\"%s\"\n", k, v);
} else {
printf("not a kv line\n");
}
}
return 0;
}
'; printf("field: \"%s\"\n", tok); } } // キー=値形式の行をパース(前後空白をトリム) static int parse_kv(char *line, char **out_key, char **out_val) { char *eq = strchr(line, '='); if (!eq) return 0; *eq = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// 簡易: カンマで分割(引用符対応なし)
static void split_csv_simple(char *line) {
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ",")) {
// 前後空白を軽く削る
while (*tok && isspace((unsigned char)*tok)) ++tok;
char *end = tok + strlen(tok);
while (end > tok && isspace((unsigned char)end[-1])) *--end = '\0';
printf("field: \"%s\"\n", tok);
}
}
// キー=値形式の行をパース(前後空白をトリム)
static int parse_kv(char *line, char **out_key, char **out_val) {
char *eq = strchr(line, '=');
if (!eq) return 0;
*eq = '\0';
char *key = line;
char *val = eq + 1;
// 左右トリム
while (*key && isspace((unsigned char)*key)) ++key;
char *kend = key + strlen(key);
while (kend > key && isspace((unsigned char)kend[-1])) *--kend = '\0';
while (*val && isspace((unsigned char)*val)) ++val;
char *vend = val + strlen(val);
while (vend > val && isspace((unsigned char)vend[-1])) *--vend = '\0';
*out_key = key;
*out_val = val;
return 1;
}
int main(void) {
// CSV例
{
char csv[] = "apple, banana , cherry";
printf("[CSV]\n");
split_csv_simple(csv);
}
// 設定ファイル例
{
char kvline[] = "  host = localhost  ";
char *k, *v;
printf("[KV]\n");
if (parse_kv(kvline, &k, &v)) {
printf("key=\"%s\", value=\"%s\"\n", k, v);
} else {
printf("not a kv line\n");
}
}
return 0;
}
'; char *key = line; char *val = eq + 1; // 左右トリム while (*key && isspace((unsigned char)*key)) ++key; char *kend = key + strlen(key); while (kend > key && isspace((unsigned char)kend[-1])) *--kend = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// 簡易: カンマで分割(引用符対応なし)
static void split_csv_simple(char *line) {
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ",")) {
// 前後空白を軽く削る
while (*tok && isspace((unsigned char)*tok)) ++tok;
char *end = tok + strlen(tok);
while (end > tok && isspace((unsigned char)end[-1])) *--end = '\0';
printf("field: \"%s\"\n", tok);
}
}
// キー=値形式の行をパース(前後空白をトリム)
static int parse_kv(char *line, char **out_key, char **out_val) {
char *eq = strchr(line, '=');
if (!eq) return 0;
*eq = '\0';
char *key = line;
char *val = eq + 1;
// 左右トリム
while (*key && isspace((unsigned char)*key)) ++key;
char *kend = key + strlen(key);
while (kend > key && isspace((unsigned char)kend[-1])) *--kend = '\0';
while (*val && isspace((unsigned char)*val)) ++val;
char *vend = val + strlen(val);
while (vend > val && isspace((unsigned char)vend[-1])) *--vend = '\0';
*out_key = key;
*out_val = val;
return 1;
}
int main(void) {
// CSV例
{
char csv[] = "apple, banana , cherry";
printf("[CSV]\n");
split_csv_simple(csv);
}
// 設定ファイル例
{
char kvline[] = "  host = localhost  ";
char *k, *v;
printf("[KV]\n");
if (parse_kv(kvline, &k, &v)) {
printf("key=\"%s\", value=\"%s\"\n", k, v);
} else {
printf("not a kv line\n");
}
}
return 0;
}
'; while (*val && isspace((unsigned char)*val)) ++val; char *vend = val + strlen(val); while (vend > val && isspace((unsigned char)vend[-1])) *--vend = '
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// 簡易: カンマで分割(引用符対応なし)
static void split_csv_simple(char *line) {
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ",")) {
// 前後空白を軽く削る
while (*tok && isspace((unsigned char)*tok)) ++tok;
char *end = tok + strlen(tok);
while (end > tok && isspace((unsigned char)end[-1])) *--end = '\0';
printf("field: \"%s\"\n", tok);
}
}
// キー=値形式の行をパース(前後空白をトリム)
static int parse_kv(char *line, char **out_key, char **out_val) {
char *eq = strchr(line, '=');
if (!eq) return 0;
*eq = '\0';
char *key = line;
char *val = eq + 1;
// 左右トリム
while (*key && isspace((unsigned char)*key)) ++key;
char *kend = key + strlen(key);
while (kend > key && isspace((unsigned char)kend[-1])) *--kend = '\0';
while (*val && isspace((unsigned char)*val)) ++val;
char *vend = val + strlen(val);
while (vend > val && isspace((unsigned char)vend[-1])) *--vend = '\0';
*out_key = key;
*out_val = val;
return 1;
}
int main(void) {
// CSV例
{
char csv[] = "apple, banana , cherry";
printf("[CSV]\n");
split_csv_simple(csv);
}
// 設定ファイル例
{
char kvline[] = "  host = localhost  ";
char *k, *v;
printf("[KV]\n");
if (parse_kv(kvline, &k, &v)) {
printf("key=\"%s\", value=\"%s\"\n", k, v);
} else {
printf("not a kv line\n");
}
}
return 0;
}
'; *out_key = key; *out_val = val; return 1; } int main(void) { // CSV例 { char csv[] = "apple, banana , cherry"; printf("[CSV]\n"); split_csv_simple(csv); } // 設定ファイル例 { char kvline[] = " host = localhost "; char *k, *v; printf("[KV]\n"); if (parse_kv(kvline, &k, &v)) { printf("key=\"%s\", value=\"%s\"\n", k, v); } else { printf("not a kv line\n"); } } return 0; }
実行結果
[CSV]
field: "apple"
field: "banana"
field: "cherry"
[KV]
key="host", value="localhost"

読み終わったら必ずfclose

ファイルディスクリプタの枯渇やデータの破損を防ぐため、読み取りが終わったらfcloseで必ず閉じます。

エラー時でもfpが有効に開けているなら、後始末の中でfcloseするのが良い習慣です。

関数の出口を1箇所にまとめる場合はgoto cleanupなどのパターンが有効です。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    FILE *fp = fopen("example.txt", "r");
    if (!fp) { perror("fopen"); return EXIT_FAILURE; }

    char buf[128];
    while (fgets(buf, sizeof buf, fp)) {
        // 何らかの処理...
    }

cleanup:
    if (fp && fclose(fp) == EOF) {
        perror("fclose");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

出力はありません。

まとめ

本記事では、C言語でファイルを1行ずつ読み込むfgetsの基本から実践的な使い方まで解説しました。

要点は次のとおりです。

改行は原則残ることを理解し、必要に応じて削除する、戻り値NULLのときはEOFかエラーかを判定する長い行に備えてバッファ戦略を考える、そして読み終えたら必ずfcloseする、という基本を押さえれば、多くのテキスト処理が安全かつ堅牢に実装できます。

設定ファイルの読み取りやログ解析などに発展させる際も、ここで紹介したパターンが土台として役立つはずです。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!