LoginSignup
5
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-28

はじめに

前回の記事「C言語でユーザ定義関数にargvやFILEを渡したい(関数へのポインタ渡し)」の続きです.
正解例についてはそちらを参考にしてください.

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

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

正解だった例

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

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を確認するコードを書き足すと

    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型のポインタのポインタはあるけど,肝心のポインタは無いのです.
なのでfpfppはFILE型のポインタのアドレスを格納する代わりにNULLを格納しているでしょうし,*fpp = fopen()もできるはずがありません.
コードに手を加えて確認したところ,if(fpp == NULL)がtrueなのは確認できましたが,if(*fpp == NULL)のところでコアダンプしました(コンパイルは通ります).

まとめ

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

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

ポインタ渡しやポインタのさらなる勉強のために小さなサンプルコードを書いていろいろ試したことがあるので,次の記事「C言語でポインタ渡し・ポインタ演算をいろいろ試した」ではその話をしたいと思います.

サンプルコード

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1