C言語でユーザ定義関数にargvやFILEを渡したかった（関数へのポインタ渡し）

# はじめに

前回の記事「[C言語でユーザ定義関数にargvやFILEを渡したい（関数へのポインタ渡し）](https://qiita.com/Reed_X1319RAY/items/2577cb1e2eb670f4bd3f)」の続きです．
正解例についてはそちらを参考にしてください．

ここでは間違いだった例を振り返ることでポインタやポインタ渡しについて考察します．

自分の勉強のためと誰かの参考になればという記事です．

# 正解だった例

正解のコードはこうでした．詳細は上のリンクから前回の記事を見てみてください．

```fgetc.c
#include <stdio.h>
#include <ctype.h> // isdigit()に必要

// ファイルを開く操作
int fileopen(FILE** fpp, char *filename);

// fgetc()で1文字ずつ読み込む出力とsumの計算をする
// 読み込んだものの出力とsumの計算をする
int get_sum(FILE** fpp);

int main(int argc, char* argv[]){
	
	// コマンドライン引数のチェック
	if(argc != 2){
		fprintf(stderr, "引数の数が間違っています．\n");
		fprintf(stderr, "./fgetc input.txt\n");
		return 1;
	}
	
	// ファイルを開く処理
	// fileopen()は失敗したら1を返す
	// それを受け取ったらreturn 1;で異常終了
	FILE* fp;
	if(fileopen(&fp, argv[1]) == 1)
		return 1;
	
	// ファイルを読み込んで出力しながらsumを計算して代入
	int sum = get_sum(&fp);
	
	printf("\nsum: %d\n", sum);
	
	// ファイルを閉じる
	fclose(fp);
	return 0;
}

int fileopen(FILE** fpp, char *filename){
	// 読み込みモードでファイルを開く
	printf("open %s\n", filename);
	*fpp = fopen(filename, "r"); // 失敗するとNULLを返す
	// ファイルを開くのに失敗したときの処理
	if(fpp == NULL){
		fprintf(stderr, "Error: file not opened.\n");
		return 1;
	}
	else{
		printf("success file open\n");
		return 0;
	}
}

int get_sum(FILE** fpp){
	int tmp; // fgetc()はint型の文字コードを返す
	int num = 0;
	int sum = 0;
	while((tmp = fgetc(*fpp)) != 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;
		}
	}
	return sum;
}
```

# 間違いだった例

上のfgetc.cに辿り着くまでの「コンパイルは通るけどコアダンプした例」を見ていきます．

コンパイラ: gcc 7.3.0

## 間違い その1

仮引数（関数の宣言や定義に書かれている引数）のfppの`*`が1つ少ない例．

```mistake1.c
#include <stdio.h>
#include <ctype.h> // isdigit()に必要

// ファイルを開く操作
int fileopen(FILE* fpp, char *filename);

// fgetc()で1文字ずつ読み込む出力とsumの計算をする
// 読み込んだものの出力とsumの計算をする
int get_sum(FILE* fpp);

int main(int argc, char* argv[]){
	
	// コマンドライン引数のチェック
	if(argc != 2){
		fprintf(stderr, "引数の数が間違っています．\n");
		fprintf(stderr, "./fgetc input.txt\n");
		return 1;
	}
	
	// ファイルを開く処理
	// fileopen()は失敗したら1を返す
	// それを受け取ったらreturn 1;で異常終了
	FILE* fp;
	if(fileopen(fp, argv[1]) == 1)
		return 1;
	
	// ファイルを読み込んで出力しながらsumを計算して代入
	int sum = get_sum(fp);
	
	printf("\nsum: %d\n", sum);
	
	// ファイルを閉じる
	fclose(fp);
	return 0;
}

int fileopen(FILE* fpp, char *filename){
	// 読み込みモードでファイルを開く
	printf("open %s\n", filename);
	fpp = fopen(filename, "r"); // 失敗するとNULLを返す
	// ファイルを開くのに失敗したときの処理
	if(fpp == NULL){
		fprintf(stderr, "Error: file not opened.\n");
		return 1;
	}
	else{
		printf("success file open\n");
		return 0;
	}
}

int get_sum(FILE* fpp){
	int tmp; // fgetc()はint型の文字コードを返す
	int num = 0;
	int sum = 0;
	while((tmp = fgetc(fpp)) != 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;
		}
	}
	return sum;
}
```

「`fp`がポインタだしそのままポインタの仮引数に受けさせればいいのでは」と思ってダメだった例．コンパイル通ったしと思って実行すると

```:output1
$ ./mistake1 input.txt
open input.txt
success file open
Segmentation fault (コアダンプ)
```

となります．

試しに下のように`fp`を確認するコードを書き足すと

```C
	FILE* fp;
	if(fileopen(fp, argv[1]) == 1)
		return 1;
	
	if(fp == NULL){
		printf("NULL file pointer\n");
		return 1;
	}
```

```:output1-1
$ ./mistake1 input.txt
open input.txt
success file open
NULL file pointer
```

となります．

開いたはずのFILE構造体を`main()`内の`fp`がポイントできていません．

勉強不足で自信はないのですが，これはおそらく「ポインタ変数の値渡し」になってしまっているのだと思います．
`main()`内で`FILE *fp`が宣言されているので，プログラムを実行すると`NULL`を格納したポインタ`fp`がメモリに確保されます．
`fpp`はこの`fp`が指す`NULL`を受け取り，その後`fopen()`で展開されたinput.txtのFILE構造体をポイントします．が，この説明が正しければ`main()`内の`fp`には何も起こりません．なので`NULL`を格納したままなのだと思います．
そして，値渡しを書くこと自体は問題無いのでコンパイルは通ってしまいます．


## 間違い その2

`main()`内で`FILE* fp`ではなく`FILE** fp`と宣言しています．
行き当たりばったりに適当にコーディングしてるのが伺えます．

```mistake2.c
#include <stdio.h>
#include <ctype.h> // isdigit()に必要

// ファイルを開く操作
int fileopen(FILE** fpp, char *filename);

// fgetc()で1文字ずつ読み込む出力とsumの計算をする
// 読み込んだものの出力とsumの計算をする
int get_sum(FILE** fpp);

int main(int argc, char* argv[]){
	
	// コマンドライン引数のチェック
	if(argc != 2){
		fprintf(stderr, "引数の数が間違っています．\n");
		fprintf(stderr, "./fgetc input.txt\n");
		return 1;
	}
	
	// ファイルを開く処理
	// fileopen()は失敗したら1を返す
	// それを受け取ったらreturn 1;で異常終了
	FILE** fp;
	if(fileopen(fp, argv[1]) == 1)
		return 1;
	
	// ファイルを読み込んで出力しながらsumを計算して代入
	int sum = get_sum(fp);
	
	printf("\nsum: %d\n", sum);
	
	// ファイルを閉じる
	fclose(*fp);
	return 0;
}

int fileopen(FILE** fpp, char *filename){
	// 読み込みモードでファイルを開く
	printf("open %s\n", filename);
	*fpp = fopen(filename, "r"); // 失敗するとNULLを返す
	// ファイルを開くのに失敗したときの処理
	if(fpp == NULL){
		fprintf(stderr, "Error: file not opened.\n");
		return 1;
	}
	else{
		printf("success file open\n");
		return 0;
	}
}

int get_sum(FILE** fpp){
	int tmp; // fgetc()はint型の文字コードを返す
	int num = 0;
	int sum = 0;
	while((tmp = fgetc(*fpp)) != 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;
		}
	}
	return sum;
}
```

コンパイルは通ります．
実行すると

```:output2
$ ./mistake2 input.txt
open input.txt
Segmentation fault (コアダンプ)
```

こうなります．

今度は`fopen()`がsuccessしませんでした．

`main()`内でFILE型のポインタのポインタとして`fp`を宣言しています．
もし仮に`FILE* tmp`があれば`fp = &tmp`とできて，`*fp`と書けばFILE型のポインタと同様に扱えるはずです．`fp = &tmp`で代入してから`fileopen()`を呼び出せば，`fpp`はポインタのポインタである`fp`が格納してるポインタのアドレス`&tmp`を受けてポインタ渡しが成立し，`fileopen()`内で`tmp = fopen()`と等価の処理が行われ，プログラムは正常に動きます．
確認しました．

```mistake2-1.c
	// ファイルを開く処理
	// fileopen()は失敗したら1を返す
	// それを受け取ったらreturn 1;で異常終了
	FILE** fp;
	FILE* tmp;
	fp = &tmp;
	if(fileopen(fp, argv[1]) == 1)
		return 1;
	
	// ファイルを読み込んで出力しながらsumを計算して代入
	int sum = get_sum(fp);
```

```:output2-1
$ ./mistake2-1 input.txt
open input.txt
success file open
ant: 88
buffalo: 40
cat: 84
dog: 34

sum: 246
```

（もはやmistakeじゃない）

さて，しかしmistake2.cには`FILE* tmp`はありません．FILE型の**ポインタのポインタはある**けど，**肝心のポインタは無い**のです．
なので`fp`や`fpp`はFILE型のポインタのアドレスを格納する代わりに`NULL`を格納しているでしょうし，`*fpp = fopen()`もできるはずがありません．
コードに手を加えて確認したところ，`if(fpp == NULL)`がtrueなのは確認できましたが，`if(*fpp == NULL)`のところでコアダンプしました（コンパイルは通ります）．


# まとめ

以上，ポインタ渡しを書こうとしてコンパイルの通ってしまうバグを書いた話でした．
こうして考察し自分なりに説明してみると「そらあかんやろ」という感想になりますが，コーディング中は認識も甘く適当なことを書いてしまいがちなので反省です．
前回の記事と合わせて執筆や執筆のためのコードのいじくりで結構勉強になったはずなので，今後はポインタをより正確に取り扱える気がします．

普段はC++書いてるので参照渡しを使うことが多いですが．


ポインタ渡しやポインタのさらなる勉強のために小さなサンプルコードを書いていろいろ試したことがあるので，次の記事「[C言語でポインタ渡し・ポインタ演算をいろいろ試した](https://qiita.com/Reed_X1319RAY/items/65847e6914392bdd160d)」ではその話をしたいと思います．


[サンプルコード](https://github.com/X1319RAY/function.git)