C言語の学習をしており、C言語でファイル操作について分からないことがある。
また、ポインタも登場したので、実装例とともに考え方を解説して欲しい。
こんなお悩みを解決します。
今回は、CSVファイルの読み込みと書き込みを題材に、ファイル操作やポインタについて学べるように解説していきたいと思います。
対象とするCSVファイルの形式・使われ方
今回は、以下のようなCSVファイルの読み込みと書き込みができるように実装をします。
また、データ型はint型とします。
001,002,003,004,005,006,007,008,009,010
011,012,013,014,015,016,017,018,019,020
021,022,023,024,025,026,027,028,029,030
031,032,033,034,035,036,037,038,039,040
041,042,043,044,045,046,047,048,049,050
051,052,053,054,055,056,057,058,059,060
061,062,063,064,065,066,067,068,069,070
071,072,073,074,075,076,077,078,079,080
081,082,083,084,085,086,087,088,089,090
091,092,093,094,095,096,097,098,099,100
101,102,103,104,105,106,107,108,109,110
111,112,113,114,115,116,117,118,119,120
さらに、読み込んだデータは四則演算を行い、同じ行数と列数で出力することを想定します。
実装結果
説明を聞く前に、「動かしてみたい!」という方は、今回の実装をGitHubに格納したので、以下のリポジトリのReadme.mdを参考に動かしてみてください。
https://github.com/yuruto-free/csv-reader-writer
考え方
CSVファイルの構成を考慮すると、CSVファイルを読み込む際のポイントは、カンマ(,)と改行コード(\n)を識別することになります。
イメージを以下に示します。1文字ずつデータを読み込み、カンマと改行コードを識別します。
これにより以下の情報が得られます。
- カンマのカウント:行方向のデータ数
- 改行コードのカウント:列方向のデータ数
行方向と列方向のデータ数が分かれば、行方向×列方向分の配列を用意し、対応する順に格納することでCSVデータの読み込みが完了します。
また、今回の実装では、以下の3点を読み込んだ結果として保持します。
対象 | 変数名 | 説明 |
---|---|---|
CSVファイル名 | filename | 読み込むCSVファイル名 |
行数 | row | 読み込んだCSVファイルの行数 |
列数 | col | 読み込んだCSVファイルの列数 |
実装
今回は、CSVの読み込みと書き込みをAPIとして提供できるように実装します。
機能一覧を以下に示します。
- 初期化機能
読み込むCSVファイル名、CSVファイルの行数、CSVファイルの列数を初期化します。 - CSV読み込み機能
上記の考え方に沿って、データを読み込みます。また、読み込んだCSVファイルの行数と列数をアップデートします。 - CSV書き込み機能
読み込んだCSVファイルの行数と列数と同じサイズで、CSVファイルに書き出します。 - 終了機能
初期化処理に対応する処理です。 - 行数取得機能
読み込んだCSVファイルの行数を返却します。 - 列数取得機能
読み込んだCSVファイルの列数を返却します。
上記の機能をヘッダーファイルとして以下のように定義し、csv_api.h
として保存します。
また、領域節約のため、行列形式のデータを読み込み、1次元で保持するため、該当するindex
が参照できるように以下のマクロを定義しておきます。
#define CSVAPI_INDEX(row, col, dim) ((row) * (dim) + (col))
#ifndef CSV_API_H_
#define CSV_API_H_
#define CSVAPI_INDEX(row, col, dim) ((row) * (dim) + (col))
#define CSVAPI_RETURN_OK (0)
#define CSVAPI_INTERNAL_ERR (1)
#define CSVAPI_STATUS_ERR (2)
#define CSVAPI_ARGUMENT_ERR (3)
#define CSVAPI_IO_ERR (4)
#define CSVAPI_MALLOC_ERR (5)
/**
* @brief 初期化処理
* @param csv_filename: CSVファイル名
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_ARGUMENT_ERR: 引数エラー
CSVAPI_STATUS_ERR: 内部状態エラー
CSVAPI_MALLOC_ERR: mallocエラー
*/
int CSVAPI_initialize(const char *csv_filename);
/**
* @brief データ読み込み処理
* @param data: 読み込んだデータの格納先
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_STATUS_ERR: 内部状態エラー
CSVAPI_ARGUMENT_ERR: 引数エラー
CSVAPI_IO_ERR: 入出力エラー
CSVAPI_MALLOC_ERR: mallocエラー
CSVAPI_INTERNAL_ERR: 内部エラー
*/
int CSVAPI_read_data(int **data);
/**
* @brief データの書き込み
* @param filename 書き込み先
* @param data 書き込むデータ
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_STATUS_ERR: 失敗
CSVAPI_ARGUMENT_ERR: 引数エラー
CSVAPI_IO_ERR: 入出力エラー
CSVAPI_INTERNAL_ERR: 内部エラー
*/
int CSVAPI_write_data(const char *filename, int *data);
/**
* @brief rowの取得
* @param row: rowの値
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_STATUS_ERR: 内部状態エラー
CSVAPI_IO_ERR: 入出力エラー
*/
int CSVAPI_get_row(int *row);
/**
* @brief colの取得
* @param col: colの値
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_STATUS_ERR: 内部状態エラー
CSVAPI_IO_ERR: 入出力エラー
*/
int CSVAPI_get_col(int *col);
/**
* @brief 終了処理
* @return CSVAPI_RETURN_OK: 成功
CSVAPI_STATUS_ERR: 内部状態エラー
*/
int CSVAPI_finalize();
#endif
以降に示す各機能は、csv_api.c
の内容を抜粋して記載します。
完全な実装は以下に格納しています。
https://github.com/yuruto-free/csv-reader-writer/blob/master/libs/csv_api.c
初期化機能
初期化機能では、以下の2点を管理します。
- 初期化機能が呼び出されたことの状態管理
- 読み込むCSVファイルのファイル名の保持
#include "csv_api.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define IS_NOT_INIT (0)
#define IS_INIT (1)
#define RETVAL_OK (0)
#define RETVAL_NG (1)
#define MAX_DIGIT (11) // int型の最大桁数
struct param_t {
char *filename;
int row;
int col;
};
static int is_init = (int)IS_NOT_INIT;
static struct param_t args;
// 中略
int CSVAPI_initialize(const char *csv_filename) {
int ret;
int length;
// 内部状態チェック
if ((int)IS_INIT == is_init) {
ret = (int)CSVAPI_STATUS_ERR;
goto EXIT_INITIALIZE;
}
// 引数チェック
if (NULL == csv_filename) {
ret = (int)CSVAPI_ARGUMENT_ERR;
goto EXIT_INITIALIZE;
}
// 内部変数を初期化
memset(&args, 0, sizeof(struct param_t));
// 読み込むファイル名を取得
length = strlen(csv_filename);
args.filename = (char *)malloc(sizeof(char) * length);
if (NULL == args.filename) {
ret = CSVAPI_MALLOC_ERR;
goto EXIT_INITIALIZE;
}
// ファイル名をコピー
memcpy(args.filename, csv_filename, length);
// 内部状態を更新
is_init = (int)IS_INIT;
ret = (int)CSVAPI_RETURN_OK;
EXIT_INITIALIZE:
return ret;
}
24/11/28追記:ヘッダーの読み込み部分が正しく表示されない不具合を修正いたしました。
まず、「考え方」のところで示した「保持するデータの内容」を構造体struct param_t
として定義します。
そして、構造体の情報と初期化状態を管理する変数を以下のように定義します。
static int is_init = (int)IS_NOT_INIT;
static struct param_t args;
staticという修飾子を付与することで、同一ファイル内で利用できるグローバル変数を作成でき、変数のスコープを狭めることができます。
次に、CSVAPI_initialize
関数の実装内容を説明します。
内部状態と引数をチェックした後、読み込むファイル名の情報を保持します。
ポインタの説明も兼ねて、以下のように実装しています。
// ①読み込むファイル名を取得
length = strlen(csv_filename);
args.filename = (char *)malloc(sizeof(char) * length);
if (NULL == args.filename) {
ret = CSVAPI_MALLOC_ERR;
goto EXIT_INITIALIZE;
}
// ②ファイル名をコピー
memcpy(args.filename, csv_filename, length);
ここでは、csv_filename
が"sample.csv"である場合を仮定して、それぞれの処理を図解しました。
ファイル名などは、連続した領域に1文字ずつ格納されています。
このため、これを複製する際は、以下の順で処理する必要があります。
- 同じ長さ以上の領域を確保する。
- 対応する部分にデータをコピーする。
文字列の長さのカウント処理やデータのコピー処理は、C言語の標準ライブラリとして実装されているため、ここでは、これらの関数を利用しています。
CSV読み込み機能
今回のメイン機能となります。
この機能では、大きく分けて以下の2つをこの順で実行しています。
- 行数(row)と列数(column)のカウント
- CSVファイルからデータを読み込み、該当する位置にデータを格納
実装は以下のようになります。
また、ローカル関数も使用しているので併せて掲載します。
int CSVAPI_read_data(int **data) {
int ret;
int func_val;
FILE *fd = NULL;
// 内部状態チェック
if ((int)IS_NOT_INIT == is_init) {
ret = (int)CSVAPI_STATUS_ERR;
goto EXIT_READ_DATA;
}
// 引数チェック
if ((NULL == data) || (NULL != (*data))) {
ret = (int)CSVAPI_ARGUMENT_ERR;
goto EXIT_READ_DATA;
}
// ファイル読み込み準備
fd = fopen(args.filename, "r");
if (NULL == fd) {
ret = (int)CSVAPI_IO_ERR;
goto EXIT_READ_DATA;
}
// データカウント
func_val = row_col_counter(fd);
if ((int)RETVAL_OK != func_val) {
ret = (int)CSVAPI_INTERNAL_ERR;
goto EXIT_READ_DATA;
}
// malloc
(*data) = (int *)malloc(sizeof(int) * args.row * args.col);
if (NULL == (*data)) {
ret = (int)CSVAPI_MALLOC_ERR;
goto EXIT_READ_DATA;
}
memset(*data, 0, sizeof(int) * args.row * args.col);
// ファイル読み込み
fseek(fd, 0, SEEK_SET);
func_val = read_data(fd, *data);
if ((int)RETVAL_OK != func_val) {
ret = (int)CSVAPI_INTERNAL_ERR;
goto EXIT_READ_DATA;
}
ret = (int)CSVAPI_RETURN_OK;
EXIT_READ_DATA:
if (NULL != fd) {
fclose(fd);
}
return ret;
}
static int row_col_counter(FILE *fd) {
int ret = (int)RETVAL_NG;
int row, col;
char val;
if (NULL == fd) {
goto EXIT_ROW_COL_COUNTER;
}
row = 0;
col = 0;
// 順に読み込む
while (EOF != fscanf(fd, "%c", &val)) {
// カンマの場合
if (',' == val) {
col++;
}
// 改行コードの場合
if ('\n' == val) {
col++;
row++;
}
}
args.row = row;
args.col = col / row;
ret = (int)RETVAL_OK;
EXIT_ROW_COL_COUNTER:
return ret;
}
static int read_data(FILE *fd, int *data) {
int ret = (int)RETVAL_NG;
int max_row, max_col;
int row, col;
int count;
char val;
char input[MAX_DIGIT];
if ((NULL == fd) || (NULL == data)) {
goto EXIT_READ_DATA;
}
max_row = args.row;
max_col = args.col;
for (row = 0; row < max_row; row++) {
for (col = 0; col < max_col; col++) {
count = 0;
input[0] = '\0';
while (EOF != fscanf(fd, "%c", &val)) {
// カンマもしくは改行コードの場合ループを抜ける
if ((',' == val) || ('\n' == val)) {
break;
}
if (((int)'0' <= (int)val) && ((int)val <= (int)'9')) {
input[count++] = val;
}
}
input[count] = '\0';
data[CSVAPI_INDEX(row, col, max_col)] = atoi(input);
}
}
ret = (int)RETVAL_OK;
EXIT_READ_DATA:
return ret;
}
引数をダブルポインタ(ポインタのポインタ)にする理由
引数がダブルポインタ(ポインタのポインタ)になっている点から説明していきます。
まず、ポインタは、アドレスを扱うもので、アスタリスク(*)を付けることで、アドレスの中のデータが読み出せる、というものでした。
以下のような実装を例に挙げると、ptr
とval
は、それぞれ下図のような対応関係になります。
int *ptr;
int val = 3;
ptr = &val;
printf("%d\n", *ptr); // 3
では、ptr
のアドレスを扱う変数dptr
を用意したい場合は、どうすれば良いでしょうか?ここで、ダブルポインタ(ポインタのポインタ)が登場します。
実装例と対応する図解を以下に示します。
int **dptr;
int *ptr;
int val = 3;
dptr = &ptr;
ptr = &val;
printf("%d\n", **dptr); // 3
ここで、CSVAPI_read_data
関数の実装に話を戻します。
CSVAPI_read_data
関数では、内部でmalloc
を用いて領域確保を行っています。
C言語では、領域確保(malloc)と領域解放(free)はセットで行う必要があります。
領域解放する際に開放対象のアドレスが必要になるため、呼び出し先で領域を確保した場合、そのアドレスを呼び出し元に返却する必要があります。
また、C言語では、引数がスタックに積まれるので、関数の引数を変更しても呼び出し元に影響を与えません。
上記を踏まえると、malloc
で確保した領域の先頭アドレスを格納しておく変数を引数として指定することとなります。
引数をダブルポインタ(ポインタのポインタ)にしているので、呼び出し元は以下のようにします。
int *data = NULL;
// 中略
// CSVファイルの読み込み
CSVAPI_read_data(&data);
ファイル読み込み処理
C言語でファイル読み込みやファイル書き込みを行う場合、FILE構造体を利用します。
これは、stdio.h
内で該当する関数が定義されているため、includeしておけば利用できます。
ファイル読み込み・書き込みをする際は、fopen
関数を利用します。
今回は、ファイル読み込みなので、モードに"r"
を指定します。
FILE *fd;
// 中略
// ファイル読み込み準備
fd = fopen(args.filename, "r");
行数と列数のカウント
この内容は、考え方で示したことをそのまま実装していきます。
fscanf
関数を用いて1文字ずつ文字を読み込み、カンマもしくは改行の時に加算処理を行っていきます。
int row, col;
char val;
// 中略
// 順に読み込む
while (EOF != fscanf(fd, "%c", &val)) {
// カンマの場合
if (',' == val) {
col++;
}
// 改行コードの場合
if ('\n' == val) {
col++;
row++;
}
}
args.row = row;
args.col = col / row;
データ読み込み
データを読み込む部分の解説の前に、fseek(fd, 0, SEEK_SET)
について解説しておきます。
fscanf
関数でファイルからデータを読み込んだ場合、どこまで読み込んでいるかをFILE構造体が保持しています。
このため、ファイルの最初から読み込み直すには、読み込み位置(seek位置)を修正する必要があります。
この読み込み位置を修正する際に利用するのが、fseek
関数となります。
引数の意味を以下に示します。
引数 | 説明 |
---|---|
fd | 更新するFILE構造体の変数 |
offset | 移動バイト数 |
origin | 基準 ・ SEEK_SET :先頭・ SEEK_CUR :現在位置・ SEEK_END :終端 |
今回の場合、先頭位置から移動量0を指定しているため、ファイルの先頭から読み出すことになります。
本題に戻ります。行数と列数のカウントでは、実際のデータを読み込んでいませんでした。
ここでは、文字列のデータを数値に変換するところまで実装していきます。
具体的なステップは以下のようになります。
- ファイルから文字列を1文字読み込む。
- 読み込んだ文字が0~9の数字だった場合、予め用意しておいた配列の先頭から順に詰めていく。
- 読み込んだ文字がカンマもしくは改行コードだった場合、予め用意しておいた配列にNULL文字を詰める。そして、
atoi
関数を用いて文字列を数字に変換する。
上記を実装すると以下のようになります。
int max_row, max_col;
int row, col;
int count;
char val;
char input[MAX_DIGIT];
max_row = args.row;
max_col = args.col;
for (row = 0; row < max_row; row++) {
for (col = 0; col < max_col; col++) {
count = 0;
input[0] = '\0';
// 1.ファイルから文字列を1文字読み込む
while (EOF != fscanf(fd, "%c", &val)) {
// カンマもしくは改行コードの場合ループを抜ける
if ((',' == val) || ('\n' == val)) {
break;
}
// 2.読み込んだ文字が0~9の数字だった場合、予め用意しておいた配列の先頭から順に詰めていく。
if (((int)'0' <= (int)val) && ((int)val <= (int)'9')) {
input[count++] = val;
}
}
// 3.読み込んだ文字がカンマもしくは改行コードだった場合、予め用意しておいた配列にNULL文字を詰める。そして、atoi関数を用いて文字列を数字に変換する。
input[count] = '\0';
data[CSVAPI_INDEX(row, col, max_col)] = atoi(input);
}
}
以上の操作により、ファイルから読み込んだデータをint型で配列に格納できます。
CSV書き込み機能
今回のもう一つのメイン機能となります。
ただ、こちらはさほど難しくなく、読み込んだデータを決まったフォーマットで出力するだけの処理となります。
int CSVAPI_write_data(const char *filename, int *data) {
int ret;
int func_val;
FILE *fd;
// 内部状態チェック
if ((int)IS_NOT_INIT == is_init) {
ret = (int)CSVAPI_STATUS_ERR;
goto EXIT_WRITE_DATA;
}
// 引数チェック
if (NULL == data) {
ret = (int)CSVAPI_ARGUMENT_ERR;
goto EXIT_WRITE_DATA;
}
// ファイル書き込み準備
fd = fopen(filename, "w");
if (NULL == fd) {
ret = (int)CSVAPI_IO_ERR;
goto EXIT_WRITE_DATA;
}
// 書き込み処理
func_val = write_data(fd, data);
fclose(fd);
if ((int)RETVAL_OK != func_val) {
ret = (int)CSVAPI_INTERNAL_ERR;
goto EXIT_WRITE_DATA;
}
ret = (int)CSVAPI_RETURN_OK;
EXIT_WRITE_DATA:
return ret;
}
static int write_data(FILE *fd, int *data) {
int ret = (int)RETVAL_NG;
int max_row, max_col;
int row, col;
if ((NULL == fd) || (NULL == data)) {
goto EXIT_READ_DATA;
}
max_row = args.row;
max_col = args.col;
for (row = 0; row < max_row; row++) {
for (col = 0; col < max_col - 1; col++) {
fprintf(fd, "%d,", data[CSVAPI_INDEX(row, col, max_col)]);
}
fprintf(fd, "%d\n", data[CSVAPI_INDEX(row, max_col - 1, max_col)]);
}
ret = (int)RETVAL_OK;
EXIT_READ_DATA:
return ret;
}
ファイルに書き込む際は、fopen関数のモードに"w"
を指定します。
終了機能
一通り処理が終わった後は、終了処理を行います。
注意点として、finalize時は、malloc
関数で確保した領域を解放する必要があります。
int CSVAPI_finalize() {
int ret;
// 内部状態チェック
if ((int)IS_INIT == is_init) {
// 領域解放
if (NULL != args.filename) {
free(args.filename);
}
// 内部変数を初期化
memset(&args, 0, sizeof(struct param_t));
// 内部状態を更新
is_init = (int)IS_NOT_INIT;
ret = (int)CSVAPI_RETURN_OK;
} else {
ret = (int)CSVAPI_STATUS_ERR;
}
return ret;
}
行数取得機能・列数取得機能
行数と列数の取得は、グローバル変数で保持している値を返却するだけの処理となります。
int CSVAPI_get_row(int *row) {
int ret;
if (NULL == row) {
ret = (int)CSVAPI_ARGUMENT_ERR;
goto EXIT_GET_ROW;
}
*row = args.row;
ret = (int)CSVAPI_RETURN_OK;
EXIT_GET_ROW:
return ret;
}
int CSVAPI_get_col(int *col) {
int ret;
if (NULL == col) {
ret = (int)CSVAPI_ARGUMENT_ERR;
goto EXIT_GET_COL;
}
*col = args.col;
ret = (int)CSVAPI_RETURN_OK;
EXIT_GET_COL:
return ret;
}
再掲となりますが、すべての実装は、以下にあるので、必要に応じて参照してください。
https://github.com/yuruto-free/csv-reader-writer
実行結果
実行結果は、GitHubのReadme.mdに記載しています。
sample.csvを入力に与えた場合、以下のような結果が得られます。
# =================
# コンソールの出力
# =================
# 1 2 3 4 5 6 7 8 9 10
# 11 12 13 14 15 16 17 18 19 20
# 21 22 23 24 25 26 27 28 29 30
# 31 32 33 34 35 36 37 38 39 40
# 41 42 43 44 45 46 47 48 49 50
# 51 52 53 54 55 56 57 58 59 60
# 61 62 63 64 65 66 67 68 69 70
# 71 72 73 74 75 76 77 78 79 80
# 81 82 83 84 85 86 87 88 89 90
# 91 92 93 94 95 96 97 98 99 100
# 101 102 103 104 105 106 107 108 109 110
# 111 112 113 114 115 116 117 118 119 120
# =====================
# CSVファイルの出力結果
# =====================
cat output.csv
# 1,2,3,4,5,6,7,8,9,10
# 11,12,13,14,15,16,17,18,19,20
# 21,22,23,24,25,26,27,28,29,30
# 31,32,33,34,35,36,37,38,39,40
# 41,42,43,44,45,46,47,48,49,50
# 51,52,53,54,55,56,57,58,59,60
# 61,62,63,64,65,66,67,68,69,70
# 71,72,73,74,75,76,77,78,79,80
# 81,82,83,84,85,86,87,88,89,90
# 91,92,93,94,95,96,97,98,99,100
# 101,102,103,104,105,106,107,108,109,110
# 111,112,113,114,115,116,117,118,119,120
演習を通してC言語に慣れよう!
今回は、CSVの読み込み・書き込みを行う処理について紹介しました。
ファイル操作やポインタを用いるので、理解するのに時間がかかるかもしれませんが、実際に手を動かして理解を深めると良いと思います。
今後も、便利な機能やよく使う機能を紹介していきたいと思います。