はじめに
前回の記事「C言語でユーザ定義関数にargvやFILEを渡したい(関数へのポインタ渡し)」の続きです.
正解例についてはそちらを参考にしてください.
ここでは間違いだった例を振り返ることでポインタやポインタ渡しについて考察します.
自分の勉強のためと誰かの参考になればという記事です.
正解だった例
正解のコードはこうでした.詳細は上のリンクから前回の記事を見てみてください.
#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つ少ない例.
#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
がポインタだしそのままポインタの仮引数に受けさせればいいのでは」と思ってダメだった例.コンパイル通ったしと思って実行すると
$ ./mistake1 input.txt
open input.txt
success file open
Segmentation fault (コアダンプ)
となります.
試しに下のようにfp
を確認するコードを書き足すと
FILE* fp;
if(fileopen(fp, argv[1]) == 1)
return 1;
if(fp == NULL){
printf("NULL file pointer\n");
return 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
と宣言しています.
行き当たりばったりに適当にコーディングしてるのが伺えます.
#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;
}
コンパイルは通ります.
実行すると
$ ./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()
と等価の処理が行われ,プログラムは正常に動きます.
確認しました.
// ファイルを開く処理
// fileopen()は失敗したら1を返す
// それを受け取ったらreturn 1;で異常終了
FILE** fp;
FILE* tmp;
fp = &tmp;
if(fileopen(fp, argv[1]) == 1)
return 1;
// ファイルを読み込んで出力しながらsumを計算して代入
int sum = get_sum(fp);
$ ./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言語でポインタ渡し・ポインタ演算をいろいろ試した」ではその話をしたいと思います.