ファイルを1行ずつ安全に読み込むには、C言語のfgets
が基本になります。
行の区切りや改行の扱い、バッファサイズ、エラー処理などを理解しておけば、ログ解析や設定ファイルの読み取りなど多くの実務で役立ちます。
本記事ではfgetsを使った堅牢な行入力の基礎から実用パターンまで、初心者の方にも分かりやすく丁寧に解説します。
fgets()の基本
fgets()はファイルから1行を読み込む関数
fgets
はストリーム(ファイルなど)から文字列を1行単位で読み込む関数です。
最大size - 1
文字までを読み込み、末尾に必ず'\0'
(ヌル文字)を付与します。
改行文字'\n'
はバッファに残る仕様で、行の途中でバッファが一杯になった場合やファイルの最後の行に改行が付いていない場合は、改行が含まれないことがあります。
必要なヘッダ
fgets
本体は#include <stdio.h>
で宣言されています。
文字列操作でstrlen
やstrcspn
を使うなら#include <string.h>
、空白判定でisspace
を使うなら#include <ctype.h>
、エラー表示にperror
やerrno
を使う場合は#include <errno.h>
も併せてインクルードします。
FILEポインタの基本
FILE *
は標準ライブラリが提供するストリームのハンドルです。
ファイルを読むにはfopen
で開き、終わったらfclose
で必ず閉じます。
読み取り専用は"r"
、追記や書き込みは別モードになりますが、本記事では読み取りに集中します。
戻り値(NULL/ポインタ)の意味
fgets
は成功時にバッファの先頭アドレス
を返し、失敗時にNULL
を返します。
失敗にはEOFに達した場合と読み取りエラーの2種類があります。
区別にはfeof
やferror
を使います。
以下に代表的な挙動をまとめます。
状況 | 戻り値 | バッファ内容例 | 備考 |
---|---|---|---|
通常の行 | バッファ先頭 | “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
になったらループを抜け、feof
とferror
で終了理由を確認し、最後にfclose
で閉じます。
サンプルファイルの内容(example.txt)
以下のテキストを例にします。
Hello, world!
This is the second line.
# A comment line
The last line has no newline at end
上の最終行は便宜上改行無しとします。
最小サンプルコード
読み込んだ行をそのまま表示するだけの最小例です。
#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
かどうかを判定しつつ、読み取った文字列はバッファに残ります。
なお、ループ終了時にfeof
かferror
でEOFかエラーかを判定することが推奨です。
#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;
}
出力例(標準エラー出力):
EOF reached. total lines=5
1行ごとに処理して表示
ここでは行番号を付けて表示し、末尾改行を取り除いてから出力します。
#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)
で判定します。
両方が偽のケースはほぼありませんが、ストリームの状態が未定義のケースを避けるため、基本は両方を確認します。
#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'
を連続で削る
汎用的な実装例です。
#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)
のように自前で改行を入れるロジックにしておくと、最終行だけ行がつながって見える問題を避けられます。
#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行を完全に取り込む関数例です。
#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かを調べます。
#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の失敗時の対処
必ず戻り値を確認し、失敗時はperror
かfprintf(stderr,...)
で状況を通知します。
開けなかった場合は直ちに処理を中断し、開けた後はfclose
を忘れないようにします。
#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()の実用パターン
行番号を付けて表示
ログや検証で役に立つ行番号付き出力です。
#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
コメント行(先頭#)をスキップ
設定ファイル等で、#から始まる行を無視する例です。
前方空白を無視して判定します。
#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系の空白に限定した実装を示します。
#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とキー=値形式の設定ファイルの例を示します。
#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
などのパターンが有効です。
#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
する、という基本を押さえれば、多くのテキスト処理が安全かつ堅牢に実装できます。
設定ファイルの読み取りやログ解析などに発展させる際も、ここで紹介したパターンが土台として役立つはずです。