お知らせ:10月末まで応募可能な期間限定の無料メンター企画実施中

C言語入門 ファイルハンドルから学ぶハンドルの概念と作り方

C言語
この記事は約23分で読めます。

ハンドルはC言語特有の機能というわけではなく、ソフトウェアで情報を管理するための仕組みです。ハンドルの概念や使い方を学びましょう。

スポンサーリンク

ハンドルの概念をイメージしよう

プログラムの世界には、皆さんの生活の中の仕組みをプログラムとして具現化したものがたくさんあります。プログラムにおけるハンドルとは、生活の中での「カード」や「チケット」のようなものです。

ハンドルの概念

キャッシュカードというものを当たり前に使っていると、便利さというものを改めて感じることは少ないかもしれません。しかし、キャッシュカードがない世界を想像してみてください。それはとても不便な生活になることでしょう。

これと同じでハンドルという概念をプログラミングの世界で一度知ると、様々なものをハンドルとして捉えられないか、と考えだすほど強力な機能です。この考え方こそがオブジェクト指向というものなのですが、本サイトはC言語の解説なのでオブジェクト指向の解説は別の機会としましょう。

さて、キャッシュカードの便利さとはいったい何なのかを考えてみましょう。

  • キャッシュカードの枠組みは共通
  • キャッシュカードで管理される情報はカード毎に異なる
  • 専用のATM機からキャッシュカードで管理情報にアクセスできる

このようなキャッシュカードの仕組みをプログラムの世界から考えてみましょう。

スポンサーリンク

ファイルハンドルを使ってみよう

キャッシュカードとファイルハンドルを比較しながらハンドルの扱い方を学びます。

ファイルハンドル:ファイルを操作するためのハンドル

ソフトウェアの世界で最も有名なハンドルといえばファイルハンドルです。

皆さんは普段パソコンで作業する際に数多くのファイルを扱いますね。キーボードから手作業でファイルの中身を編集することもあるでしょう。

このようなファイル操作はプログラムから行うことも可能です。ファイルの作成、編集、削除、コピーなど様々な作業をプログラムからも行えます。

C言語のプログラムからファイルを操作するためにはファイルハンドルが必要となります。ファイルハンドルの作成には標準ライブラリ関数を利用します。

ファイルハンドルの使用例

まずは、ファイルハンドルを使用したプログラムを見ていただきましょう。

#include <stdio.h>
#include <string.h>

int main(void)
{
    // ファイルハンドルへのポインタ
    FILE * pfile = NULL;

    // ファイルへの書き出し文字
    char * pmoji = "Hello World";
    size_t i;

    // ファイルハンドルの生成とオープン
    fopen_s(&pfile, "learnC.txt", "w");

    // ファイルへの書き出し
    for (i=0 ; i < strlen(pmoji) ; i++)
    {
        fputc(pmoji[i], pfile);
    }

    // ファイルのクローズ
    fclose(pfile);
    pfile = NULL;

    return 0;
}

皆さんの環境でこのプログラムを実施するとプロジェクトフォルダにlearnC.txtが作成され、中身に「Hello World」と書かれているのがわかることでしょう。

ファイルハンドルの管理情報

プログラムの中に登場するpfileがファイルハンドルと呼ばれる情報(正確にはファイルハンドルへのポインタ)です。fopen_s関数を呼び出すことでファイルハンドルが取得できます。

取得したpfileをfputc関数に渡すことで対象ファイルに文字を書き込んでいます。このようにファイルハンドルを利用すると特定のファイルへ読み書きを行うことができます。

ファイルハンドルは対象のファイル情報を構造体のFILE型として管理しています。FILE型ではファイルのどこを読み書きしているかなどの情報が定義されています。

ファイルハンドルの便利さとは

ファイルハンドルの便利さを考えてみましょう。

プログラミングをする中でファイルに情報を記録したい、ファイルから情報を読み込みたい、といったニーズはよくあります。その際に複数のファイルを同時に編集したいといったケースもありそうですね。

ファイルハンドルは1つのファイルに対して1つ作成します。もし複数のファイルをプログラムから制御したいときは、複数のハンドルを作成すればよいということです。

ハンドルの便利さ

生成されるハンドルはもちろん別のものであり、ハンドルAに対して書き込み処理を行えばlearnA.txtへ、ハンドルBに対して書き込みを行えばlearnB.txtへ反映されることになります。

このようにハンドルさえ作ってしまえば、各ハンドルに対する操作から対象ファイルへアクセスすることができるようになります。これがハンドルの便利さです。

スポンサーリンク

ファイルハンドルから考察するハンドル構成

ファイルハンドルはC言語を学ぶ項目としてよく登場しますが、本サイトで皆さんに学んで頂きたいことはファイルハンドルの扱い方ではありません。ファイルハンドルを通じてハンドルという概念と開発スキルを手に入れることです。

ハンドルはC言語に限らずプログラミングの幅を確実に広くしてくれる概念です。本サイトを見ていただけている方は少し広い目線を意識してください。

ハンドルの構成要素

組み込み開発の世界ではファイルハンドルを利用できる環境はそれほど多くありません。しかし、ハンドルという仕組みを使って独自の情報を管理するケースは結構多いのです。

ファイルハンドルに限らずハンドルを扱うための関数は大きく分けて3つになります。

  • ハンドル生成
  • ハンドル制御
  • ハンドル解放

順番に学んでいきましょう。

ハンドル生成

最初に知るべきはハンドル生成についてです。これがないと何もできません。

皆さん銀行でキャッシュカードを作る時には窓口で氏名、住所、暗証番号などを用意して申請しますね。ハンドルの生成も同様に申請が必要となります。

ファイルハンドルを生成したい時には次の標準ライブラリ関数で申請します。

fopen_s

ファイルハンドルを生成するためにfopen_s関数を呼び出し、申請に必要な情報を引数で渡すことになります。ファイルハンドルではファイル名や読み書きの属性を申請情報として引数に指定する必要があります。

ハンドル作成

ファイルハンドルはfopen_sの第1引数により取得することができます。多くの場合、ハンドルの正体はとあるデータ型で確保されたヒープメモリ領域へのポインタです。

ファイル制御ライブラリはハンドル利用者から生成要求を受けたら、ヒープメモリを確保してハンドル領域を作成します。ヒープメモリを忘れてしまった人はメモリの章を復習しましょう。

ヒープを使ったハンドル作成

確保したヒープメモリにファイルに関する情報を設定し、利用者にポインタとして提供します。ファイルハンドルに限らず、これがハンドル生成の基本的な流れです。

ハンドル制御

ハンドルに対して様々な要望に応えるのがハンドル制御関数です。

制御用の関数はハンドルへの操作ニーズによって複数用意されます。ファイルハンドルについてはサンプルプログラムでfputc関数を使用しましたが、この関数以外にもたくさんあります。

ハンドル制御

これらの制御関数に共通することは、必ず引数にファイルハンドルが存在することです。ハンドルこそが対象の情報を示すのですから、それを指定しないことは絶対にありません。ATMの前でキャッシュカードを持っていなかったらお金はおろせませんよね。これと同じことなのです。

ハンドル制御イメージ

制御関数毎に提供するサービスが異なるため、入力すべき引数情報は関数毎に異なるのが普通です。ファイルハンドルを切り替えるだけで、制御対象のファイルが切り替わることもハンドルの特徴です。

ハンドルの違いは結果の違い

このようにATM機やハンドル制御関数は同じですが、ハンドルを切り替えることで結果の反映先を変化させることができるのです。

ハンドル破棄

キャッシュカードも時には使わなくなって破棄することがありますね。ハンドルも同じように使わなくなったら破棄する必要があります。

ハンドルの正体は確保されたヒープメモリでした。そのため、ハンドルが不要になった場合はメモリの解放処理が必要になります。ファイルハンドルの場合はfclose関数であり、引数にはもちろんファイルハンドルを指定することになります。

fclose

やってはならないのはハンドル破棄を忘れることです。ヒープメモリで解説した通り、ハンドルの破棄忘れはメモリの解放漏れと同じです。使い終わったら確実に破棄処理を行うようにしましょう。

借りたメモリは返す
スポンサーリンク

Q&A:ハンドルに関するよくある質問

fopen_s関数で取得できるファイルハンドルですが第1引数の型がFILE**型になっています。これはダブルポインタですか?

その通りです。第1引数は下記の通りダブルポインタ型になっていますね。

errno_t fopen_s(FILE** pFile, const char *filename, const char *mode);

ダブルポインタが登場する代表的なケースがこのハンドル生成の関数です。ポインタ上級編にて解説したダブルポインタですが、次のケースで登場するとだけ紹介しました。

このように呼び出し先の関数に対して番地の書き込みを依頼するときにダブルポインタが引数で登場するということでしたね。このハンドル生成こそがまさにこのシーンなのです。

ハンドルの正体は動的メモリへの番地ですから、第1引数のFILEポインタ変数の中にハンドルの番地を書き込んでくださいと依頼する先がfopen_s関数なのです。

オブジェクト指向とはなんでしょうか?

昨今のプログラミング言語の多くが、オブジェクト指向言語と呼ばれるものです。オブジェクト指向とはプログラムの世界をモノという単位で捉え、システムを構築する手法です。

C言語はオブジェクト指向言語ではありませんが、このハンドルという仕組みが疑似的なオブジェクト指向に近い存在となっています。ですので、ハンドルをしっかりと学んだあとにオブジェクト指向言語を学ぶと「これってハンドルと一緒の考え方だよね」となるのです。

C言語をしっかり学んだ方は、言語的スキルのステップアップとしてオブジェクト指向を学ぶのもよいでしょう。

スポンサーリンク

課題:ハンドルの使い方を学べたかを確認しよう

課題1

課題内容

プログラムからHello.txtとWorld.txtの2つのファイルハンドルを新規書き込み属性で作成し、それぞれに「Hello」と「World」の文字を書き込み保存せよ。文字列の書き込みはfputs関数を使用せよ。

main.c

#include <stdio.h>

int main(void)
{
    //  ファイルハンドルの定義


    // ファイルハンドルの生成とオープン


    // ファイルへの書き出し


    // ファイルのクローズ


    return 0;
}

実行後にプロジェクトフォルダ内に作成されたHello.txtとWorld.txtに出力期待結果の内容が書き込まれていることを確認せよ。


出力期待結果

Hello.txt

Hello

World.txt

World

main.c

#include <stdio.h>

int main(void)
{
    //  ファイルハンドルの定義
    FILE * pfile1 = NULL;
    FILE * pfile2 = NULL;

    // ファイルハンドルの生成とオープン
    fopen_s(&pfile1, "Hello.txt", "w");
    fopen_s(&pfile2, "World.txt", "w");

    // ファイルへの書き出し
    fputs("Hello", pfile1);
    fputs("World", pfile2);

    // ファイルのクローズ
    fclose(pfile1);
    fclose(pfile2);
    pfile1 = NULL;
    pfile2 = NULL;

    return 0;
}

ファイルハンドルを別で生成し、それぞれに対象の文字列を書き出している。

課題2

課題内容

※本課題はファイルハンドルではなくシステム独自のハンドルを作成する課題である。

銀行におけるキャッシュカードをハンドルとして管理するシステムを構築するものとする。システムはmain.c、bank.c、bank.hで構成するものとする。

次のキャッシュカードハンドルの定義をbank.hへtypedefを使用し定義せよ。

bank.h

#ifndef BANK_H
#define BANK_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------

//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------

//------------------------------------------------
#endif

次の構造体と関数をbanc.cへ定義せよ。


bank.c

#include <stdio.h>
#include "bank.h"

//  各関数を定義

作成したbank.hとbank.cを次のプログラムと結合・実行し、出力期待結果が表示されることを確認せよ。

main.c

#include <stdio.h>
#include "bank.h"

int main(void)
{
    H_CASHCARD hCard = NULL;

    //  銀行口座の作成
    hCard = creatBankAccount("山田太郎", 0x1234);

    //  口座情報の表示
    printBalance(hCard);

    //  口座を破棄
    destroyBankAccount(hCard, 0x1234);

    return 0;
}

出力期待結果

-----------------------------
氏名:山田太郎様
残高:0円
-----------------------------

bank.h

#ifndef BANK_H
#define BANK_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------

//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------
//  キャッシュカードハンドル
typedef void * H_CASHCARD;

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
H_CASHCARD creatBankAccount(const char * name, unsigned short pincode);
int printBalance(H_CASHCARD card);
int destroyBankAccount(H_CASHCARD card, unsigned short pincode);

//------------------------------------------------
#endif

キャッシュカードハンドルをvoid型ポインタとして外部へ公開することで詳細な口座情報を意図的に隠ぺいする効果がある。


bank.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "bank.h"

#define D_ACCOUNT_NAMESIZE      (32)

//  銀行口座情報
typedef struct
{
    char            name[D_ACCOUNT_NAMESIZE];   //  登録氏名
    unsigned short  pincode;                    //  暗証番号
    unsigned long   balance;                    //  預金残高
} S_BANK_ACCOUNT;

//------------------------------------------------
//  概  要:銀行口座の作成
//  引  数:name    登録氏名
//          pincode 暗証番号
//  戻り値:キャッシュカードハンドル
//------------------------------------------------
H_CASHCARD creatBankAccount(const char * name, unsigned short pincode)
{
    S_BANK_ACCOUNT * pAccount = NULL;

    if (name == NULL)
    {
        return NULL;
    }

    //  口座情報のメモリ領域をヒープメモリに確保
    pAccount = (S_BANK_ACCOUNT *)malloc(sizeof(S_BANK_ACCOUNT));
    if (pAccount == NULL)
    {
        return NULL;
    }

    //  口座情報を初期化
    strcpy_s(pAccount->name, D_ACCOUNT_NAMESIZE, name);
    pAccount->pincode = pincode;
    pAccount->balance = 0;

    return pAccount;
}

//------------------------------------------------
//  概  要:残高表示
//  引  数:hCard   キャッシュカードハンドル
//  戻り値:0       正常終了
//          -1      ハンドル異常
//------------------------------------------------
int printBalance(H_CASHCARD hCard)
{
    S_BANK_ACCOUNT * pAccount = (S_BANK_ACCOUNT *)hCard;

    //  カード情報の正当性チェック
    if (pAccount == NULL)
    {
        return -1;
    }

    printf("-----------------------------\n");
    printf("氏名:%s様\n", pAccount->name);
    printf("残高:%d円\n", pAccount->balance);

    return 0;
}

//------------------------------------------------
//  概  要:銀行口座の破棄
//  引  数:hCard   キャッシュカードハンドル
//          pincode 暗証番号
//  戻り値:0       正常終了
//          -1      ハンドル異常
//          -2      暗証番号不一致
//------------------------------------------------
int destroyBankAccount(H_CASHCARD hCard, unsigned short pincode)
{
    S_BANK_ACCOUNT * pAccount = (S_BANK_ACCOUNT *)hCard;

    //  カード情報の正当性チェック
    if (pAccount == NULL)
    {
        return -1;
    }

    //  暗証番号の確認
    if (pAccount->pincode != pincode)
    {
        return -2;
    }

    //  口座情報を破棄
    free(pAccount);
    pAccount = NULL;

    return 0;
}

口座情報はmalloc関数を利用し、S_BANK_ACCOUNT構造体をヒープメモリ上に確保して実現していることに着目すること。S_BANK_ACCOUNT構造体をスタックメモリに確保してはならないことに注意。

課題3

課題内容

課題2に対して次の関数をbank.cへさらに追加せよ。

作成したbank.hとbank.cを次のプログラムと結合・実行し、出力期待結果が表示されることを確認せよ。

main.c

#include <stdio.h>
#include "bank.h"

int main(void)
{
    H_CASHCARD hCard1 = NULL;
    H_CASHCARD hCard2 = NULL;

    //  銀行口座の作成
    hCard1 = creatBankAccount("野口英世", 0x1876);
    hCard2 = creatBankAccount("福沢諭吉", 0x1835);

    //  口座情報の表示
    printBalance(hCard1);
    printBalance(hCard2);

    //  預け入れ
    depositMoney(hCard1, 5000);
    depositMoney(hCard2, 500000);

    //  口座情報の表示
    printBalance(hCard1);
    printBalance(hCard2);

    //  引き出し
    withdrawalMoney(hCard1, 0x1876, 1000);
    withdrawalMoney(hCard2, 0x1835, 50000);

    //  口座情報の表示
    printBalance(hCard1);
    printBalance(hCard2);

    //  口座を破棄
    destroyBankAccount(hCard1, 0x1876);
    destroyBankAccount(hCard2, 0x1835);

    return 0;
}

出力期待結果

-----------------------------
氏名:野口英世様
残高:0円
-----------------------------
氏名:福沢諭吉様
残高:0円
-----------------------------
氏名:野口英世様
残高:5000円
-----------------------------
氏名:福沢諭吉様
残高:500000円
-----------------------------
氏名:野口英世様
残高:4000円
-----------------------------
氏名:福沢諭吉様
残高:450000円

bank.h

#ifndef BANK_H
#define BANK_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------

//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------
//  キャッシュカードハンドル
typedef void * H_CASHCARD;

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
H_CASHCARD creatBankAccount(const char * name, unsigned short pincode);
int printBalance(H_CASHCARD card);
int depositMoney(H_CASHCARD hCard, unsigned long money);
int withdrawalMoney(H_CASHCARD hCard, unsigned short pincode, unsigned long reqmoney);
int destroyBankAccount(H_CASHCARD card, unsigned short pincode);

//------------------------------------------------
#endif

新しく追加するdepositMoney関数とwithdrawalMoney関数のプロトタイプ宣言を追加する。


bank.c

//------------------------------------------------
//  概  要:お金の預入
//  引  数:hCard   キャッシュカードハンドル
//          money   預入のお金
//  戻り値:0       正常終了
//          -1      ハンドル異常
//------------------------------------------------
int depositMoney(H_CASHCARD hCard, unsigned long money)
{
    S_BANK_ACCOUNT * pAccount = (S_BANK_ACCOUNT *)hCard;

    //  カード情報の正当性チェック
    if (pAccount == NULL)
    {
        return -1;
    }

    //  預入のお金を残高に加算
    pAccount->balance += money;

    return 0;
}

//------------------------------------------------
//  概  要:お金の引出し
//  引  数:hCard       キャッシュカードハンドル
//          pincode     暗証番号
//          reqmoney    引出し希望金額
//  戻り値:0以上   引出し金額
//          -1      ハンドル異常
//          -2      暗証番号不一致
//          -3      残高不足
//------------------------------------------------
int withdrawalMoney(H_CASHCARD hCard, unsigned short pincode, unsigned long reqmoney)
{
    S_BANK_ACCOUNT * pAccount = (S_BANK_ACCOUNT *)hCard;

    //  カード情報の正当性チェック
    if (pAccount == NULL)
    {
        return -1;
    }

    //  暗証番号確認
    if (pAccount->pincode != pincode)
    {
        return -2;
    }

    //  残高確認
    if (pAccount->balance < reqmoney)
    {
        return -3;
    }

    //  残高から減算
    pAccount->balance -= reqmoney;

    return reqmoney;
}

ハンドル制御用の関数であるdepositMoney関数とwithdrawalMoney関数を追加する。キャッシュカードハンドルに紐づく口座情報を変更することで複数の口座アカウントを異なるものとして制御している。