こんにちは、ナナです。
ここまで学んだファイルハンドルの技術をもとに、オリジナルのハンドル機構を自分で作ってみましょう。
私たちの生活の中でよく利用される「カード」や「チケット」といったものは、プログラムの世界の「ハンドル」に近いものです。
今回は、銀行の「キャッシュカード」をハンドルに見立ててプログラム化してみましょう。
銀行の普通預金口座で受けられるサービス
師匠!何やら銀行のキャッシュカードを発行いただけると聞きつけ参りました。使いたい放題ということで、是非1枚お願いしたく。
今回は仮想の銀行カードだからね。買い物はできないけど、預金額はどこまでも増やすことができるから、自由に使っていいよ。
まずは、銀行というものが、どのようなサービスを私たちに提供してくれているかを考えてみましょう。
その内容を元にハンドルの構成を検討します。
キャッシュカードの便利さを考える
キャッシュカードというものを当たり前に使っていると、便利さというものを改めて感じることは少ないかもしれません。
しかし、キャッシュカードがない世界を想像してみてください。それはとても不便な生活になることでしょう。
キャッシュカードの便利さとはいったい何なのかを考えてみましょう。
このようなキャッシュカードの仕組みをプログラムの世界で実現してみましょう。
オリジナルのハンドルを作る場合は、そのハンドルがどのようなサービスを提供できるかを考えましょう。
「利用者にとっての便利さとは何か?」それがハンドルで提供すべきサービスなのです。
ハンドル型の定義:銀行口座が管理する情報
師匠!ハンドルを作るためにはいったい最初に何をすべきなのでしょうか?スタート地点がわかりません。
そうですね、まずはハンドルが管理すべき情報を検討しましょう。洗い出した情報を構造体として定義します。
ハンドルの正体は、ハンドルが管理するデータになります。まずは、どのようなデータをハンドルとして管理すべきかを検討します。
銀行口座で管理するデータ
今回は簡易的な銀行口座なので、最小限の管理すべきデータを検討してみます。
管理項目 | データ型 | メンバ名 | 説明 |
---|---|---|---|
登録氏名 | char[64] | name | キャッシュカードを利用する人の名前を管理 |
暗証番号 | unsigned short | pincode | 不正利用防止のため、16進数4桁の暗証番号を管理 |
預金残高 | unsigned long | balance | 預け入れや引き出しによって変わる預金の残高を管理 |
このように、利用するシーンを想定して、管理すべき情報を検討します。
この段階で全ての項目を出すことが難しいこともあります。
プログラムを作る中でさらに必要な項目が出てきたら、適宜項目を足していくことになります。気にせずに思い浮かぶだけの項目を出していけばよいです。
銀行口座のハンドル型の定義
それでは、先ほど検討した管理データを「S_BANK_ACCOUNT」構造体として定義してみましょう。
銀行側がキャッシュカードを発行することになりますので、この定義は「bank.c」ファイルを作成し、そこに型定義を行ってみてください。
#define D_ACCOUNT_NAMESIZE (64) // 氏名サイズ
// 銀行口座情報
typedef struct
{
char name[D_ACCOUNT_NAMESIZE]; // 登録氏名
unsigned short pincode; // 暗証番号
unsigned long balance; // 預金残高
} S_BANK_ACCOUNT;
配列サイズといった後から変更されそうな定数値は、マクロ定義にしておくと便利です。
また、外部へ提供するヘッダーファイル「bank.h」に、次のキャッシュカードハンドルを定義しておきましょう。
#ifndef BANK_H
#define BANK_H
//------------------------------------------------
//------------------------------------------------
// マクロ定義(Macro definition)
//------------------------------------------------
//------------------------------------------------
// 型定義(Type definition)
//------------------------------------------------
// キャッシュカードハンドル
typedef void * H_CASHCARD;
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
//------------------------------------------------
#endif
構造体定義は一般的にはヘッダファイルに記述します。しかし、ここではあえてソースファイルに記述しています。
これは、「bank.h」に定義した「H_CASHCARD」と強く関連づいた構成となっています。理由は後ほど解説しましょう。
ハンドル生成:キャッシュカードの作り方
師匠!カードと言えば私にお任せください。私はいつでもカードを投げられるように、ポケットにたくさん持っているんですよ。
なくなったら銀行に行って作ればいいのです!何枚でも作りますよっ!シュパッ!
その手に持っているのはカードだったんだね。いろいろな意味で危ないからキャッシュカードは大事に管理しておこうね。
それじゃあ、次はキャッシュカードを作ることを想定して、ハンドル生成を学ぶよ!
データ構造を検討した次は、ハンドル生成の工程になります。
キャッシュカードの申請時に必要なもの
銀行でキャッシュカードを作りたい時には、申請用紙に必要事項を記入して銀行員に渡しますよね。
このイメージから、ハンドル生成の関数仕様を決定します。今回申請に必要な情報を次のものと定義しましょう。
- 登録氏名
- 暗証番号
これらの情報をハンドル生成関数の引数として定義するとします。
キャッシュカードハンドル生成の関数定義
それでは、キャッシュカードのハンドル生成関数を定義してみましょう。
先ほどの情報を引数とした「creatBankAccount関数」を作りましょう。仕様は次のものとします。
項目 | データ型・変数名 | 説明 |
---|---|---|
ファイル | – | bank.c |
関数名 | – | creatBankAccount |
概要 | – | 銀行口座を作成し、キャッシュカードハンドルを戻り値で発行する。 ハンドルの実体はS_BANK_ACCOUNT構造体のメモリとする。 |
引数1 | const char * name | 最大64文字までの登録氏名 |
引数2 | unsigned short pincode | カードの暗証番号 |
戻り値 | H_CASHCARD | 作成したキャッシュカードハンドル。 作成に失敗した場合はNULLポインタを返却する。 |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "bank.h"
#define D_ACCOUNT_NAMESIZE (64)
// 銀行口座情報
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;
}
ハンドル領域の基本は動的メモリになります。そのため、型定義した「S_BANK_ACCOUNT」の構造体をmalloc関数でメモリを確保しています。
確保したメモリには、銀行口座情報を初期化した状態とします。預金残高である「balance」は0円で初期化することになります。
ハンドル生成でやることは、ハンドル領域のメモリ領域の確保と、ハンドル情報の初期化となります。
関数の戻り値では利用者となる関数呼び出し元に、「ハンドルへのポインタ」を返却します。この時の戻り値の型が「S_BANK_ACCOUNT*」ではなく「H_CASHCARD」としているのがハンドル特有のギミックとなります。
ハンドル制御:キャッシュカードを利用したサービスの作成
師匠!これで銀行口座を申請すれば、キャッシュカードが出来上がるってことですね。
でも、キャッシュカードが手に入っても、お金を引き出したりできないと作った意味がないですよ。
そうだね。次はキャッシュカードを利用してできるサービスを作っていくよ。普段使っているATMをイメージしてサービスを検討しようね。
それでは、ATMで行える次のサービスをハンドル制御の関数として用意してみましょう。
- 預金の残高照会
- 預け入れ
- 引き出し
ハンドル制御:銀行口座の預金残高照会のサービスの関数化
では、最も基本的な残高照会のサービスをまずは作ってみましょう。
項目 | データ型・変数名 | 説明 |
---|---|---|
ファイル | – | bank.c |
関数名 | – | printBalance |
概要 | – | 銀行口座に登録されている氏名と預金残高を表示する。 表示フォーマットは次の通りとする。 ————————– 氏名:山田太郎様 残高:0円 |
引数1 | H_CASHCARD hCard | 表示対象のキャッシュカードハンドル |
戻り値 | int | 正常:0 異常:-1 |
「bank.c」には下記関数を追加するものとします。
//------------------------------------------------
// 概 要:残高表示
// 引 数: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;
}
第1引数の「H_CASHCARD型」はvoid型ポインタのため、銀行口座の情報にアクセスする際には、白抜き部分のように「S_BANK_ACCOUNT型」のポインタにキャストを行うことで可能とします。
ハンドル制御:銀行口座への預け入れサービスの関数化
続いて、銀行口座へのお金を預け入れできるようにしてみましょう。
項目 | データ型・変数名 | 説明 |
---|---|---|
ファイル | – | bank.c |
関数名 | – | depositMoney |
概要 | – | 銀行口座へのお金の預け入れを行う。 |
引数1 | H_CASHCARD hCard | 預け入れ対象のキャッシュカードハンドル |
引数2 | unsigned long money | 預け入れする金額(円) |
戻り値 | int | 正常:0 異常:-1 |
//------------------------------------------------
// 概 要:お金の預入
// 引 数: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;
}
預金残高には「+=」を使って加算しましょう。「=」にしてしまうと、元の残高が消滅してしまうので注意してください。
ハンドル制御:銀行口座からの引き出しサービスの関数化
続いて、銀行口座からお金を引き出すことができるようにしてみましょう。
引き出しは他の人が不正に行えないように暗証番号のチェックをするようにします。また、残高不足の可能性があることを考慮しましょう。
項目 | データ型・変数名 | 説明 |
---|---|---|
ファイル | – | bank.c |
関数名 | – | withdrawalMoney |
概要 | – | 銀行口座からお金の引き出しを行う。 |
引数1 | H_CASHCARD hCard | 預け入れ対象のキャッシュカードハンドル |
引数2 | unsigned short pincode | 暗証番号 |
引数3 | unsigned long reqmoney | 引出し希望金額(円) |
戻り値 | int | 引出した金額:0以上 その他の異常:-1 暗証番号不一致:-2 残高不足:-3 |
//------------------------------------------------
// 概 要:お金の引出し
// 引 数: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;
}
カード不正 ⇒ 暗証番号 ⇒ 残高不足 を順に確認し、問題なければ引き出しを行いましょう。
ハンドル破棄:キャッシュカードの破棄
師匠!私はカードをビュンビュン飛ばしてたら、よくカードなくしちゃうんですよ。キャッシュカードって使わなくなったら投げちゃってもいいですよね?
投げてなくしちゃうのはダメだよ。キャッシュカードを捨てても口座情報は銀行に残ってるからね。
ちゃんと銀行で手続きをして、キャッシュカードを正式に破棄しようね。
キャッシュカードを発行してもらうことができるのであれば、不要になったキャッシュカードを破棄することもできなければなりません。
ハンドル破棄:銀行口座の解約手続きを関数化
破棄の手続きでは、他の人が不正に行えないように暗証番号のチェックをするようにします。
項目 | データ型・変数名 | 説明 |
---|---|---|
ファイル | – | bank.c |
関数名 | – | destroyBankAccount |
概要 | – | 銀行口座の破棄を行う。 |
引数1 | H_CASHCARD hCard | 破棄対象のキャッシュカードハンドル |
引数2 | unsigned short pincode | 暗証番号 |
戻り値 | int | 正常:0 その他の異常:-1 暗証番号不一致:-2 |
//------------------------------------------------
// 概 要:銀行口座の破棄
// 引 数: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);
return 0;
}
ハンドル破棄で行う代表的なことは、ハンドル領域のメモリ解放です。
生成によって動的メモリにハンドルを割り付けたので破棄にて解放する、これが基本的なハンドル破棄の構成です。
キャッシュカードハンドルを使って預金の出し入れをしてみる
師匠!銀行口座のサービスは作りましたよね。とっとと私のキャッシュカードでお金をガボガボと出し入れしたいんですよ。
ここまで作ってきたシステムは、「bank.c」という銀行サービスを提供する側でしたね。
それでは、今度は利用者側となる「main.c」から銀行サービスを活用するプログラムを作ってみましょう!
銀行サービスの外部公開ヘッダファイルの整備
利用者側のプログラムを作る前に、ヘッダファイルの整備を行っておきましょう。
ここまで、キャッシュカードの作成・破棄、銀行口座の照合・預け入れ・引き出しの関数サービスを作成してきました。
利用者側がこのサービスを受けるためには、ヘッダファイルでサービス内容を取り込む必要があります。
#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 hCard);
int depositMoney(H_CASHCARD hCard, unsigned long money);
int withdrawalMoney(H_CASHCARD hCard, unsigned short pincode, unsigned long reqmoney);
int destroyBankAccount(H_CASHCARD hCard, unsigned short pincode);
//------------------------------------------------
#endif
このように作成した関数を外部から呼び出せるように、関数プロトタイプ宣言を追加してあげましょう。
銀行口座を利用する側のプログラム
それでは、銀行口座を利用するサンプルプログラムを示しましょう。ハンドルの使い方はこのようなプログラムになります。
2人の登場人物「野口英世」と「福沢諭吉」が、次の作業をそれぞれ行うシーンを想定します。
- 銀行口座の作成
- 預金残高の照会
- 預け入れ
- 預金残高の照会
- 引き出し
- 預金残高の照会
- 銀行口座の破棄
#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;
}
このようにハンドルを利用する側は、ハンドル毎に独立した情報を管理することができます。
実行した結果、正しく預け入れや引き出しができているか、皆さんの目で確かめてみてください。
デバッガのステップ実行で追いかけると、よりリアルにハンドルの動きがわかるようになるでしょう。
手続きは同じことができるのに、情報は別で管理することができる!これがハンドルの便利なところです。
Q&A:ハンドルに関するよくある質問
Q:ヘッダーファイルに定義した「H_CASHCARD」は何だったの?
師匠!結局、「bank.h」に定義した「H_CASHCARD」とは何だったのですか?なんでこんなものを用意したのですか?
ちゃんと説明責任を果たしてくださいっ!
そうだったね。これは銀行口座のデータ構造を、外部となる利用者側へ公開しないためのギミックなんだよ。
これをしておくことで、君がキャッシュカードの残高を不正に多くしないようにしてるんだね。
銀行側となる「bank.c」において、顧客の口座情報は厳重に守らなければならない情報ですね。
その口座情報のデータ型となる「S_BANK_ACCOUNT型」をヘッダファイルで外部へ公開してしまうと、利用者側が自由にハンドルの口座情報を参照できるようになってしまいます。
では、実際に定義すると何ができるようになってしまうかを示しましょう。
#ifndef BANK_H
#define BANK_H
//------------------------------------------------
//------------------------------------------------
// マクロ定義(Macro definition)
//------------------------------------------------
#define D_ACCOUNT_NAMESIZE (64)
//------------------------------------------------
// 型定義(Type definition)
//------------------------------------------------
// 銀行口座情報
typedef struct
{
char name[D_ACCOUNT_NAMESIZE]; // 登録氏名
unsigned short pincode; // 暗証番号
unsigned long balance; // 預金残高
} S_BANK_ACCOUNT;
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
S_BANK_ACCOUNT* creatBankAccount(const char * name, unsigned short pincode);
int printBalance(S_BANK_ACCOUNT* hCard);
int depositMoney(S_BANK_ACCOUNT* hCard, unsigned long money);
int withdrawalMoney(S_BANK_ACCOUNT* hCard, unsigned short pincode, unsigned long reqmoney);
int destroyBankAccount(S_BANK_ACCOUNT* hCard, unsigned short pincode);
//------------------------------------------------
#endif
このヘッダファイルを取り込んで、利用する側のプログラムは次のようなことができます。
#include <stdio.h>
#include "bank.h"
int main(void)
{
S_BANK_ACCOUNT* hCard1 = NULL;
hCard1 = creatBankAccount("悪井金蔵", 0x1234);
// 銀行口座の中身が丸見えのため1億円を不正入金!
hCard1->balance = 100000000;
// 口座情報の表示
printBalance(hCard1);
// 1億円を口座から引き出し
withdrawalMoney(hCard1, 0x1234, 100000000);
// 口座情報の表示
printBalance(hCard1);
destroyBankAccount(hCard1, 0x1234);
return 0;
}
このように銀行サービスを利用する側の「main.c」では、ハンドルの中身である「預金残高:balance」の構造体メンバが直接見えていますね。
このプログラムを動かした結果は、次のようになります。
-----------------------------
氏名:悪井金蔵様
残高:100000000円
-----------------------------
氏名:悪井金蔵様
残高:0円
このようにハンドルの中身である銀行口座が利用者から見えてしまえば、いくらでも預金残高を増やすことが可能になってしまいます。
そのため、次のように「void型ポインタ」を使って外部への型情報を隠蔽するのです。
// キャッシュカードハンドル
typedef void * H_CASHCARD;
「void型ポインタ」に関しては『C言語 void型の意味と使い方【void型ポインタの扱い方も解説】』を見ておくとよいでしょう。
ハンドルの中身は利用者側に見せないようにする!これがハンドルの基本です。性善説に立ってはいけないんです。
意図せずともシステムが悪用されることがあるのですから。