はじめに
もうすぐ卒業する情報工学科の大学院生です.
学部の授業から今の研究までC,C++にお世話になってきたけど,ずっとファイル入力(ファイル読み込み)に苦手意識があり,毎回ググって誰かの記事のお世話になるのを繰り返してきました.
いい加減,「(自分は)こういうときはこうする」を確立したいのと,これからCやC++でプログラミングの授業受ける後輩やファイル入力必要になったって人のためにもなればとも思い,自分だったらこれ使うかな(+これ使えたら課題に対応できるかな)ってのをまとめてみることにしました.自分と初心者向けの記事です。
#ファイル入力で使うもの
- C言語
もの | 書式 | 処理 |
---|---|---|
fgetc() |
int fgetc(FILE *stream); |
1文字ずつ |
fgets() |
char *fgets(char *s, int n, FILE *stream); |
1行ずつ 最大n-1文字+\0 |
fscanf() |
int fscanf(FILE *fp, const char *format, ...); |
1行ずつ 書式指定 |
- C++
もの | 書式 | 処理 |
---|---|---|
>> |
ifstream ifs >> 変数やstring; |
デリミタ(「,」やスペース)で 区切られた単語や値を格納 |
getline() |
getline(ifstream ifs, string str); |
1行読み込んで stringに格納 |
- こういうときはこれ
1文字ずつ | 1単語ずつ | 1行ずつ | 書式指定 | |
---|---|---|---|---|
C | fgetc() |
fscanf() |
fgets() |
fscanf() |
C++ | >> char型変数 |
>> stringインスタンス or 変数 |
getline() |
fscanf() |
自分は基本的にC++の方を使うのが楽だと思います.
C言語で1単語ずつ: できなくはないけど手間なのでわざわざCでやらないかなと思います.C言語の範囲でやりたければisalnum()
を使って単語の区切れを探すのがよさそうです.
追記(2018-12-29): fscanf()
を使うことでスペースなどをデリミタとする単語の読み込みができそうです.
参考: IBM Knowledge Center - fscanf()、scanf()、sscanf() - データの読み取りとフォーマット設定
書式が特定されていれば書式指定が簡単そうですが,そうでない場合はとりあえずchar型配列に格納し,そのあと処理するという流れになるかと思います.char配列から数値への変換はstrtol()
などを使うのが良さそうです.
参考: C言語関数辞典 - strtol
C++の書式指定: Cのfscanf()
でいいと思います.自分は書式指定で読み込むの面倒くさいと感じるのでC++であえてやろうとは思いません.
追記(2018-12-29): C++用のCスタイル入出力ライブラリが<cstdio>で,この中にfscanf()
が含まれているようです.C++ではこれをincludeしてfscanf()
を使うのが自然かと思われます.
参考: std::scanf, std::fscanf, std::sscanf - cppreference.com
それでは以下でfgetc()
,fgets()
,fscanf()
,>>
,getline()
の使い方を紹介します.
#その前に入力ファイルの紹介
読み込ませる入力ファイルの内容です.
生き物:
の後に半角スペースがあり,その後に整数値が書いてあります.
見える人には整数値の後ろに「改行コード」が見えるかも.
このファイルの意味は分かりません.
ant: 5
buffalo: 2
cat: 20
dog: 23
サンプルプログラムでは読み込んだものをそのまま出力した後,なんとなく4つの整数の合計を計算し出力します.
ant: 5
buffalo: 2
cat: 20
dog: 23
sum: 50
いったい何の意味があるんだ… ぼっちで過ごすクリスマス並みに虚無感の漂う処理だ…
一応,「文字列を読み込んで何かする」と「数字を読み込んで値として扱い何かする」という処理を含めたいという意図はあります.
こんな処理をしたいとき,こう書いたらできるよっていう例を紹介していきます.
それではC言語から.
#fgetc()
C言語で1文字ずつ読み込むときに使います.
書式はint fgetc(FILE *stream);
返り値はint型で,読み込んだ文字の文字コード,もしくはファイル終端か読み取り失敗でEOF
1文字ずつ読み込むので値も1桁ずつ読み込んで処理しています.
#include <stdio.h>
#include <ctype.h> // isdigit()に必要
int main(int argc, char* argv[]){
// コマンドライン引数のチェック
if(argc != 2){
fprintf(stderr, "引数の数が間違っています.\n");
fprintf(stderr, "./fgetc input.txt\n");
return 1;
}
// C言語のファイル入力のための準備
FILE* fp; // FILE型構造体
// 読み込みモードでファイルを開く
fp = fopen(argv[1], "r"); // 失敗するとNULLを返す
// ファイルを開くのに失敗したときの処理
if(fp == NULL){
fprintf(stderr, "Error: file not opened.\n");
return 1;
}
// fgetc()で1文字ずつ読み込む
int tmp; // fgetc()はint型の文字コードを返すので格納先もint型
int num = 0;
int sum = 0;
while((tmp = fgetc(fp)) != EOF){
// ここでtmpを煮るなり焼くなりする
printf("%c", (char)tmp); // そのまま出力
// 数字ならnumに数として格納
if(isdigit(tmp)){ // tmpが数字なら
num = num * 10; // 位を1つ大きくする
num += tmp - '0'; // 一の位に値を入れる
}
else{
// 数字が終わった直後ならnumがsumに加算される
// その後numを0にしているので直後以外はsumに0が加算される
sum += num;
num = 0;
}
}
printf("\nsum: %d\n", sum);
// ファイルを閉じる
fclose(fp);
return 0;
}
#fgets()
C言語で1行ずつ読み込むときに使います.
書式はchar *fgets(char *s, int n, FILE *stream);
最大n-1
文字の行を読み込んでs
に格納します.1文字分は終端記号\0
の格納に使用されます.
返り値は,読み取り成功時はs
,ファイルの終わりで1文字も読み取らなかった場合はNULL
ここでは1行をchar型配列に読み込んだ後,上と同じように1文字ずつ処理しています.
追記でstrtol()
による{文字列$\rightarrow$long int
}変換を使った例を紹介しています.そっちの方がシンプルになっていると思います.
#include <stdio.h>
#include <ctype.h> // isdigit()に必要
#define BUFSIZE 1024 // 1行に最大1024(終端を除けば1023)文字しかない前提
int main(int argc, char* argv[]){
// コマンドライン引数のチェック
if(argc != 2){
fprintf(stderr, "引数の数が間違っています.\n");
fprintf(stderr, "./fgets input.txt\n");
return 1;
}
// C言語のファイル入力のための準備
FILE* fp; // FILE型構造体
// 読み込みモードでファイルを開く
fp = fopen(argv[1], "r"); // 失敗するとNULLを返す
// ファイルを開くのに失敗したときの処理
if(fp == NULL){
fprintf(stderr, "Error: file not opened.\n");
return 1;
}
// fgets()で1行ずつ読み込む
char tmp[BUFSIZE]; // fgets()はchar型配列に格納するのでchar型配列を用意
int num = 0;
int sum = 0;
while(fgets(tmp, BUFSIZE, fp) != NULL){
// ここでtmpを煮るなり焼くなりする
printf("%s", tmp); // そのまま出力
// tmpの中身を確認
for(int i = 0; i < BUFSIZE; i++){
// 数字ならnumに数として格納
if(isdigit(tmp[i])){
num = num * 10; // 位を1つ大きくする
num += tmp[i] - '0'; // 一の位に値を入れる
}
else{
// 数字が終わった直後ならnumがsumに加算される
// その後numを0にしているので直後以外はsumに0を加算
sum += num;
num = 0;
}
if(tmp[i] == '\n') break;
}
}
printf("\nsum: %d\n", sum);
// ファイルを閉じる
fclose(fp);
return 0;
}
#fscanf()
C言語で書式指定して1行ずつ読み込むときに使います.
書式はint fscanf(FILE *fp, const char *format, ...);
これだけ見ても分かりにくいかもしれませんがサンプルコードを見てもらえばscanf()
と似ているのに気づくかと思います.怖くありませんよ.
返り値は,読み取り成功時は代入された項目の数,失敗時はEOF
1文字ずつ処理せずに済み,値も変数に簡単に格納できます.
#include <stdio.h>
#define BUFSIZE 1024 // 1行に最大1024(終端を除けば1023)文字しかない前提
int main(int argc, char* argv[]){
// コマンドライン引数のチェック
if(argc != 2){
fprintf(stderr, "引数の数が間違っています.\n");
fprintf(stderr, "./fscanf input.txt\n");
return 1;
}
// C言語のファイル入力のための準備
FILE* fp; // FILE型構造体
// 読み込みモードでファイルを開く
fp = fopen(argv[1], "r"); // 失敗するとNULLを返す
// ファイルを開くのに失敗したときの処理
if(fp == NULL){
fprintf(stderr, "Error: file not opened.\n");
return 1;
}
// fscanf()で書式指定で1行ずつ読み込む
char tmp[BUFSIZE];
int num;
int sum = 0;
while(fscanf(fp, "%s %d", tmp, &num) != EOF){
// ここでtmpを煮るなり焼くなりする
printf("%s %d\n", tmp, num); // そのまま出力
// 値をsumに加算
sum += num;
}
printf("\nsum: %d\n", sum);
// ファイルを閉じる
fclose(fp);
return 0;
}
#ここまでC言語
一見fscanf()
が楽そうですが(素朴に使うと)バッファオーバーフローの危険性があります.
つまり想定している読み込みサイズを超えてしまうかもということです.
まあ,手元で自分が使うプログラムに関してはそんなに気にしなくてもいいかなとは思います.
追記: 下のコメントでより詳しい解説を紹介いただいてます.気になる方はチェックしてください.
#ここからC++
僕はC言語からC++使うようになって入力すごく楽になったなーと感じています.
「今は授業がC言語だから…」という人は上の3つから適当なものを選んで使えばいいと思いますが,C++が選択肢にあるなら下をお勧めしたいです.
stringクラスがあるのも便利さの一端を担っているかも.
それでは.
#>>
(抽出演算子)
正式には「ストリーム抽出演算子」でしょうか.
C++で1単語ずつ読み込むときに使います.
書式はifstream ifs >> 変数やstringインスタンス;
スペースがあると区切り(デリミタ)とみなして1単語ずつ読み込んで代入してくれるので実用的だと思います.
サンプルコードを見てみましょう.
区切りを飛ばさずに読み取ってほしい時はnoskipws
が使えるそうです.
#include <iostream>
#include <fstream>
int main(int argc, char* argv[]){
using namespace std;
// コマンドライン引数のチェック
if(argc != 2){
cerr << "引数の数が間違っています." << endl;
cerr << "./extraction input.txt" << endl;
return 1;
}
// C++のファイル入力のための準備
ifstream ifs(argv[1], ios::in);
// ファイルを開くのに失敗したときの処理
if(!ifs){
cerr << "Error: file not opened." << endl;
return 1;
}
string str;
int num;
int sum = 0;
// 抽出演算子>>を使ってデリミタで区切られた単語,値を読み込む
while(ifs >> str >> num){ // str == "name:"; num == number;
// ここで格納した文字列や変数を煮るなり焼くなりする
cout << str << " " << num << endl; // そのまま出力
sum += num; // 値をsumに加算
}
cout << "\nsum: " << sum << endl;
// ファイルを閉じる
ifs.close();
return 0;
}
#getline()
C++で1行ずつ読み込むときに使います.
書式はgetline(ifstream ifs, string str);
getline(ifstream ifs, string str, ',');
なんて書き方をすると「,」で区切って読み取ってくれるみたいです.
getline()で読み込んだあと結局>>
で処理してしまってます.
#include <iostream>
#include <fstream>
#include <sstream>
int main(int argc, char* argv[]){
using namespace std;
// コマンドライン引数のチェック
if(argc != 2){
cerr << "引数の数が間違っています." << endl;
cerr << "./getline input.txt" << endl;
return 1;
}
// C++のファイル入力のための準備
ifstream ifs(argv[1], ios::in);
// ファイルを開くのに失敗したときの処理
if(!ifs){
cerr << "Error: file not opened." << endl;
return 1;
}
string tmp;
string str;
int num;
int sum = 0;
// getline()で1行ずつ読み込む
while(getline(ifs, tmp)){
// ここでtmpを煮るなり焼くなりする
cout << tmp << endl; // そのまま出力
stringstream ss;
ss << tmp;
ss >> str >> num; // str == "name:"; num == number;
sum += num; // 値をsumに加算
}
cout << "\nsum: " << sum << endl;
// ファイルを閉じる
ifs.close();
return 0;
}
#おわりに
このn番煎じな内容を見てくださりありがとうございます.
C,C++でファイル入力どうしたらいいんだろうって人向けに,かなり簡単めに(大雑把に)まとめてみました.
説明してないこともたくさんあるし,説明できないからスルーしてるところもあるし,もしかしたらおかしな説明になってるところもあるかもしれません.僕も勉強中の身です.卒業したらC++使わない気もしてる
「詳しくは分からないけどお手軽で参考になり分かった気になれる」そんな内容かも知れません.とりあえずはそれでもいいんじゃないでしょうか.
(「結局分からん」ってなった人はごめんなさい)
「ifstreamにiosがinしてるってどういうことやねん」とかは気になった人がまたググればいいんです.
「情報系の学科じゃないけどC言語だけ必修で…」みたいなイヤイヤやってる人や,「やる気はあるけどまだ初心者で訳わからん」みたいな人がささやかな「自分にもできるじゃん」感を得られたらいいなと思っています.
メリークリスマス
#おまけ
- input.txtを生成するプログラム
#include <iostream>
#include <fstream>
#include <random>
int main(int argc, char* argv[]){
using namespace std;
random_device seed_gen; // 乱数のseedを生成
mt19937 mt(seed_gen()); // メルセンヌ・ツイスターと呼ばれる乱数生成法
ofstream out("input.txt", ios::out);
uniform_int_distribution<> dista(1, 'a');
out << "ant: " << dista(mt) << endl;
uniform_int_distribution<> distb(1, 'b');
out << "buffalo: " << distb(mt) << endl;
uniform_int_distribution<> distc(1, 'c');
out << "cat: " << distc(mt) << endl;
uniform_int_distribution<> distd(1, 'd');
out << "dog: " << distd(mt) << endl;
out.close();
return 0;
}
- サンプルコードをDLしていじったりとかしたい人は下からどうぞ
サンプルコードのDL: github.com/X1319RAY/samplecode
記事の内容でおかしいなと思ったら自信を持って僕の方を疑ってください.
その辺にいるへぼい情報工学徒なので.
コメントを残してくださると勉強になります.
#追記
2018-12-24
指摘を頂いて乱数の生成法をrand()から推奨されている(であろう)ものに変えました.
GitHubのコードも差し替えました.
Makefileに間違いがあったので修正しました.
2018-12-29
fgets.cを,strtol()
を使って{文字列$\rightarrow$long int
}変換するように書き変えたfgets2.cを作ってみました.
#include <stdlib.h>
long int strtol(const char *nptr, char **endptr, int base);
nptr
には変換するchar型配列のアドレス(変換する箇所の先頭のアドレス)を,endptr
にはchar型ポインタのアドレス(渡すポインタは何も指してなくていい,変換が終了した位置(アドレス)を返すためのもの)を,base
には基数(10進数なら10)を,それぞれ渡します.
fgets2.cではfgets()
で1行ずつchar tmp[]
に格納しているので,tmp[]
を前から辿って,数字を見つけたらそこから後ろをlong int
に変換しています.
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h> // isdigit()に必要
#define BUFSIZE 1024 // 1行に最大1024(終端を除けば1023)文字しかない前提
int main(int argc, char* argv[]){
/* 省略 */
// fgets()で1行ずつ読み込む
char tmp[BUFSIZE]; // fgets()はchar型配列に格納するのでchar型配列を用意
char* endptr;
int sum = 0;
while(fgets(tmp, BUFSIZE, fp) != NULL){
// ここでtmpを煮るなり焼くなりする
printf("%s", tmp); // そのまま出力
// tmpの中身を確認
for(int i = 0; i < BUFSIZE; i++){
// 数値ならnumに数として格納
if(isdigit(tmp[i])){
// strtol(): tmpがlongに変換可能なら数値が,そうでなければ0が返ってくる
sum += (int) strtol(&tmp[i], &endptr, 10);
break;
}
}
}
/* 省略 */
return 0;
}