はじめに
元々はこの前の記事のプログラムをユーザ定義関数使ってmain()
内をすっきりさせるとどうなるかっていう話をするつもりでした.
そして見事「あ,関数への渡し方が分からん」に陥りました.
なんとか正解には辿り着き,それが正しいことも何となくわかったつもりではいるのですが,間違いのコードのどこがどう間違っていたのかを確認すると良い勉強になるという感覚があるので,正解についての自分の理解と間違いへのツッコみをノートとして残しておこうと思います.
(↑の強調は文字列を「**」で囲うことで表現できるのですが「ポインタのポインタ」とやらに散々悩まされたので「コイツ…!」という気持ちで強調しています(とばっちり))
この記事では正解例を紹介し,後日,別の記事で間違いだった例を挙げ,なぜそれが間違いなのか説明できればと考えています.
自分の勉強のためと,困った人の参考になればという記事です.
まずは正解
「ユーザ定義関数にargv
やFILE*
を渡したい」という気持ちでこの記事に辿り着いた方はとりあえずこちらを参考にしてもらったらいいかと思います.
main()
の手前で「関数のプロトタイプ宣言」をして,後ろで定義を書くスタイルです.入力ファイルを開く処理とファイルの内容を読み込んで処理する部分をそれぞれ関数にしました.main()
内がすっきりして見えるでしょうか.
#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;
}
そもそも「関数へのポインタ渡し」はどんなだったか
受ける側の関数ではポインタを仮引数(sanmple1のint* pt
)として用意しておき,渡す側では変数のアドレスを渡すように記述します.
これにより関数側で用意されたポインタが実在する変数を指す(ポイントする)ようになり,*
を付ければその中身を触れるようになります.
(関数の宣言や定義のときにこんな引数を取るよと書いておくのを仮引数,main()
内などで実際に関数を呼び出すときに取らせる引数を実引数と言います)
#include <stdio.h>
void func1(int* pt){
*pt = 5; // ポインタが指す先の変数の中身を5に
}
int main(void){
int a;
func1(&a);
printf("a=%d\n",a);
return 0;
}
func1()
にa
のアドレス&a
を渡すことでpt
がa
を指すようになる様子がイメージできるでしょうか.
a=5
argvを渡す
char a[5];
と宣言したとき,a
はchar型の配列の先頭アドレス($\subset$ char型の変数のアドレス)になります.
そのことを思い出しながら見ていくと,
-
char* argv[]
はchar型のポインタの配列. -
argv[1]
はchar型のポインタの配列の2番目の要素,つまりchar型のポインタで,$ ./fgetc input.txt
と実行した場合,char型配列"input.txt"
の先頭アドレスを格納している. -
filename
はchar型のポインタ -
fileopen(FILE** fp, char *filename)
が,char型のポインタであるfilename
でargv[1]
が格納しているアドレスargv[1]
を受ける.
こうしてfilename
が"input.txt"
の先頭アドレスを格納することで関数内で"input.txt"
を扱えるようになります.
FILEを渡す
FILE型,main()
内でFILE* fp;
と宣言して使いますね.
-
fp
はFILE型のポインタ. -
fpp
はFILE型のポインタのポインタ -
fileopen(FILE** fpp, char *filename)
が,FILE型のポインタのポインタであるfpp
でポインタのアドレス&fp
を受ける.
まとめ
「ポインタの」が付いてややこしく見えますが,**「仮引数として用意したポインタで,実引数としてアドレスを受ける」**というのは一貫しています.
- char型配列をポインタ渡しするなら,char型のポインタでchar型配列の先頭アドレスを受ける.
- FILE型のポインタをポインタ渡しするなら,FILE型のポインタのポインタでFILE型のポインタのアドレスを受ける.
この関係と,渡そうとしている実引数が何なのか(特に配列は注意)が把握できていれば,ポインタ渡しでの悩みはぐっと減るんじゃないでしょうか.
今回は元々の目的だったfgetc.cのコードの改変に加えて,勉強のためにサンプルコードをいろいろ書いてみてセルフレビューする(+ネットでいろいろな解説を漁る)ことで自分としてはかなり勉強になったと思います.「とりあえず課題のプログラムが完成すればいいや」を超えて理解を深めたい人はそういう時間を取ってみるのもいいかもしれません.
追記
2018-12-29
修正しました.
当初,
fileopen(FILE** fpp, char **filename);
int main(void){
if(fileopen(&fp, argv) == 1)
return 1;
}
としてポインタのポインタであるfilename
でポインタの配列の先頭アドレスであるargv
を受けるコードになっていましたが,
fileopen(FILE** fpp, char *filename);
int main(void){
if(fileopen(&fp, argv[1]) == 1)
return 1;
}
でも正しいことに気付き,argv
の何番目の要素を渡したいのかが明確な方が良いコードだと判断して,コードと説明を修正しました.