「C++ のからくり」の小遣い帳プログラム ( 1 ) ～ 生粋の C 版

下表の [IT 入門チャンネル](https://www.youtube.com/@it_introduction/playlists) の「丁寧に学ぶ C++」は、各項目が簡潔に解説 ( 10～15分程度の動画 ) されていますので、C++ を学ぶ必要が出たときには参照してみてください。 
|丁寧に学ぶ C++|内容|
|:--|:--|
|入門|① 特徴・環境構築 ② コードをじっくり見る ③ 変数・型・演算子 ④ 代入演算子・インクリメント・const ⑤ bool 型・if 文 ⑥ 名前空間・入力 ⑦ for 文・配列 ⑧ 関数 ⑨ オーバーロード・テンプレート ⑩ ポインタ ⑪ 総集編|
|初級|① オブジェクト指向 ② 構造体・クラス ③ メソッド・継承 ④ コンストラクタ・デストラクタ ⑤ オーバーライド・仮想関数 ⑥ 純粋仮想関数・抽象クラス|
|中級|① explicit ② this ポインタ ③ スマートポインタ ④ クラステンプレート ⑤ 関数テンプレート・参照 ⑥ コンテナクラス|
|関数編|① 関数の基本 ② オーバーロード・テンプレート ③ 再帰関数 ④ ラムダ式 ⑤ 続ラムダ式|
|構造体|① 構造体の基本 ② ポインタと構造体 ③ 関数とポインタ、そして構造体 ④ クラスは構造体の進化系 ⑤ ポインタとクラス ⑥ 静的メンバ変数とオブジェクトのカウント|
|vector|① 配列 ( vector の前に基本の振り返り ) ② ベクトルの基本 ③ 初期化とコピー ④ vector の基本操作 ⑤ イテレータ ⑥ アルゴリズム ( コピー、一括指定、交換、ソート、反転 ) ⑦ 二次元ベクトル ⑧ リスト ( List )|
|プリプロセッサ|① define マクロ ② ifdef / ifndef 条件分岐 ③ include インクルード ④ インクルードガイド pragma once|
|Eigen ( 行列ライブラリ )|① インストール ② 最初のコード ③ 宣言 ④ 初期化 ⑤ 値の代入 ⑥ 要素数 ( row, col, size ) ⑦ 便利機能 ⑨ 四則演算 ⑨ 逆行列の解法 ( 直接法、反復法 ) ⑩ Array ( 配列 )|
|ファイル操作|① 書き込み ( C 言語 ) ② 読み込み ( C 言語 ) ③ 書き込み ( C++ ofstream ) ④ 読み込み ( C++ ifstream ) ⑤ 読み書き ( C++ fstream )|

しかし、文法の説明のための短いソースコードの他に、プログラム実例として少しまとまったプログラムも試してみたくなりました。「『**改訂C++のからくり**』スティーブン・R・デイビス著 瀬谷啓介訳 ソフトバンク出版 1999年」に、「**C版 小遣い帳プログラム** ( 224 行 )」を四段階に分けて C++ プログラムに改変していく様子が載っていましたので、それを教材として見ていこうと思います。

## 小遣い帳の機能
小遣い帳プログラムは、シンプルな当座預金口座 ( 小切手によってお金を引き出す口座 ) と普通預金口座の記録プログラムです。この機能は以下のとおりです。( 「C++ のからくり」から引用 )
1. 当座預金口座の処理、普通預金口座の処理、またはプログラムの終了。
2. 口座番号の登録。
3. 入金と引き出しの処理を受け付ける際に、0 が入力された場合はその処理を終了する。
4. ユーザが終了を選択後、最終的な一覧表として、すべての当座預金口座の残高集計と、すべての普通預金口座の残高集計、およびすべての口座の残高集計を表示する。

## 処理を行う際の金融ルール
このプログラムでは、処理を行う際に下記のような金融ルールを適用します。(「C++ のからくり」から引用 )
- 出金時に、口座に入っている残高がマイナスになってはならない。
- 入金する際は手数料はかからない。
- 当座預金口座の残高が 500.00 ドル未満であった場合、小切手 1 枚につき 0.20 ドル ( 20 セント ) の手数料がかかる。それ以外の場合は、手数料がかからない。
- 最初の引き出し以外は、普通預金口座から引き出すのに 5.00 ドルの手数料がかかる。
- 開くことのできる当座預金と普通預金の最大口座数 ( MAXACCOUNTS ) は、それぞれ 10。

## Visual Studio の環境構築とプロジェクトの設定
- Visual Studio の環境構築は、[初心者でも安心！Visual Studio 2022を使ってC言語を始める方法](https://jet-blog.com/start-c-language-with-visual-studio-2022/) をご参照ください。私の場合は、プロジェクト名は「小遣い帳(C版)」(ソリューション名も同じ)、場所は「```C\Users\～\source\C++のからくり```」としました。( '\\' は Windows では '￥' と表示される )
- プロジェクト名.cpp というソースファイルが自動で作成されますが、ファイル名を Budget1.c に変更しました。
- ファイル名の拡張子はデフォルトでは .cpp で、そのままでもプログラムは動きますが、C 言語の場合は明示的に .c に変更したほうがよいでしょう。
- Budget1.c のサンプルコードを削除し、一番下のソースコードをコピー & ペーストしてください。

## ソースコード ( Budget1.c ) のマクロ、構造体、関数、配列
Visual Studio 2022 のソリューション・エクスプローラでは、以下のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3549839/658cb11e-7b4c-21d1-313a-0ad93ad1b4b3.png)

|マクロ、構造体、関数|説明|
|:--|:--|
|**\#define MAXACCOUNTS 10**|開ける口座の最大数を 10 に設定する。|
|**struct Checking{ }**|当座預金の構造体。メンバーは口座番号<br>(**accountNumber**) と残高 (**balance**)。|
|**chkAcnts[ ]**|**Checking** 型の配列。|
|**struct Savings{ }**|普通預金の構造体。メンバーは口座番号<br>(**accountNumber**) 、残高 (**balance**)、<br>引き出し回数 (**noWithdrawals**)。|
|**svgAcnts[ ]**|**Savings** 型の配列。|
|**void initChecking(Checking \*)**|当座預金口座を初期化する関数。|
|**void initSavings(Savings \*)**|普通預金口座を初期化する関数。|
|**void processChecking(Checking \*)**|当座預金口座のデータ入力関数。|
|**void processSavings(Savings \*)**|普通預金口座のデータ入力関数。|

## プログラムの動作
Budget1.c は以下のように動作します。(「C++ のからくり」から引用 )

【プログラムの構造】
1. **Checking** と **Savings** の 2 つの構造体が、当座預金口座と普通預金口座に必要な情報を持つ。
2. これらの口座のための記憶領域は、**Checking** 型の配列 **chkAcnts[ ]** と **Savings** 型の配列 **svgAcnts[ ]** に割り当てられている。
3. メイン・プログラム ( **int main( )** ) は 2 つの部分に分かれている。1 つはデータを蓄積する部分 ( **注 1** )、もう 1 つは最終的な結果を表示する部分 ( **注 5** ) である。
4. データを蓄積する部分では、**S**、**C** または **X** の入力を促すので、文字を選択入力する。

【当座預金処理】
1. ユーザが **C** を入力すると、当座預金処理に移る ( **注 2** )。
2. この時、当座預金口座の数 ( **noChkAccounts** )が最大口座数 ( **MAXACCOUNTS** ) に達していなければ、新たに 1 つの当座預金口座が追加され、初期化され ( **initChecking( )** ) 、処理される ( **processChecking( )** )。
3. 最大口座数に達してしまった場合は、口座の追加は拒否され、エラーメッセージが出力される。
4. 関数 **initChecking( )** の処理は、口座の登録番号( **accoutNumber** ) の登録と口座の残高 ( **balance** ) を 0 にする。
5. 関数 **processChecking( )** の処理は、入金には正の数、引き出しには負の数、そして 0 を入力すると終了する。
6. 入金はそのまま受け付けるが、引き出すには、口座に十分な残高が残っているかの確認と当座預金引き出し手数料の計算 ( **金融ルール**参照 ) をおこなう。

【普通預金処理】
1. ユーザが **S** を入力すると、当座預金処理に移る ( **注 3** )。
2. この時、普通預金口座の数 ( **noSvgAccounts** )が最大口座数 ( **MAXACCOUNTS** ) に達していなければ、新たに 1 つの当座預金口座が追加され、初期化され ( **initSavings( )** ) 、処理される ( **processSavings( )** )。
3. 最大口座数に達してしまった場合は、口座の追加は拒否され、エラーメッセージが出力される。
4. 関数 **initSavings( )** の処理は、口座の登録番号( **accoutNumber** ) の登録と口座の残高 ( **balance** ) 及び引き出し回数 ( **noWithdrawals** ) を 0 にする。
5. 関数 **processSavings( )** の処理は、入金には正の数、引き出しには負の数、そして 0 を入力すると終了する。
6. 入金はそのまま受け付けるが、引き出すには、口座に十分な残高が残っているかの確認と普通預金引き出し手数料の計算 ( **金融ルール**参照 ) をおこなう。

【最終結果処理】
1. ユーザが **X** を入力すると ( **注 4** )、ループ処理を脱して最終的な結果を示す処理に移る ( **注 5** )。
2. 当座預金口座と普通預金口座のそれぞれの口座に登録されているすべての記録を吐き出し、当座預金の残高トータル ( **chkTotal** )、普通預金の残高トータル ( **svgTotal** )、全預金の残高トータル ( **total** ) を表示する。

※**このプログラムでは、引き出す前に必ず入金がなされていることを前提としている。**

## 実行例
下記に実行例を示しますが、数値を入力するところに文字を入力すると暴走するので注意してください。
```
入力　S（普通預金口座）, C（当座預金口座）, X（終了)  
S
口座番号の入力：123
入力：入金の場合（正の数）
出金の場合（負の数）入力を終了する場合（0）:200
:-50
:-50
:0
入力　S（普通預金口座）, C（当座預金口座）, X（終了)
C
口座番号の入力：234
入力：入金の場合（正の数
出金の場合（負の数）入力を終了する場合（0）：200
：-25
：-20
：0
入力　S（普通預金口座）, C（当座預金口座）, X（終了)
X
当座預金口座：
口座    234 =   154.60
普通預金口座：
口座    123 =    95.00 (引き出し回数 = 2)
当座預金口座の残高トータル =   154.60
普通預金口座の残高トータル =    95.00
全預金口座の残高トータル   =   249.60
```
## ソースコード
コーディング・スタイルは、以下のような Borland の命令慣例に従っています。(「C++ のからくり」から引用 )
- 型の名前は、大文字で始める。
- 変数名は小文字で始める。
- ポインタ変数名は、文字 p から始める。
- 複数の単語から構成される変数名は、各々の単語の先頭文字を大文字で始める。ただし、一番先頭の単語の頭文字は小文字。
- マクロや #define 文で定義される定数名はすべて大文字にする。

【注意点】
- '\\' は Windows では '￥' と表示されます。
- ```#pragma warning(disable : 4996)```
Visual C++ では非推奨の ( エラーが発生する ) ```scanf()``` が使えるように、ソースコードの先頭に追加します。
- ```scanf("\n%c", &accountType)```
 ストリームに改行文字 ( \\n )があれば、読み飛ばします。( [scanf関数](http://rainbow.pc.uec.ac.jp/edu/program/b1/Ex2-1b.htm) 参照 )
- -> ( アロー ) 演算子は、& 演算子や ++ 演算子よりも優先順位が高いので、下記の左側の式は、右側の式のように評価されます。
```&pChecking->accountNumber``` → ```&(pChecking->accountNumber)```
```&pSavings->accountNumber``` → ```&(pSavings->accountNumber)```
```++pSavings->noWithdrawals``` → ```++(pSavings->noWithdrawals)```
```c++
/* BUDGET1.C  -  銀行口座の残高計算
                 下記の2種類の口座を扱う：
                 当座預金口座 - 口座の残高 < 500ドルの場合、
                                1回の引き出しにつき0.20ドルの手数料
                 普通預金口座 - 引き出しごとに5.00ドルの手数料
                                （最初の引き出しは、手数料なし）
*/
#pragma warning(disable : 4996) /* 非推奨の scanf() 関数が使えるように追加 */
#include <stdio.h>

/* 開くことのできる最高口座数 */
#define MAXACCOUNTS 10

/* Checking - 当座預金口座の記述 */
struct Checking
{
   unsigned accountNumber;  /* 口座番号 */
   float    balance;        /* 残高 */
} chkAcnts[MAXACCOUNTS];

/* Savings - 普通預金口座の記述（何をしているかわかるね）*/
struct Savings
{
   unsigned accountNumber;  /* 口座番号 */
   float    balance;        /* 残高 */
   int      noWithdrawals;
} svgAcnts[MAXACCOUNTS];

/* プロトタイプ宣言：きちんと宣言しておこう */
void initChecking(struct Checking *pCO);    /* 当座預金口座の初期化 */
void processChecking(struct Checking *pCO);  /* 当座預金口座のデータ入力 */

void initSavings (struct Savings  *pSO);    /* 普通預金口座の初期化 */
void processSavings (struct Savings *pSO);   /* 普通預金口座のデータ入力 */
int main();

/* main - 処理の選択と残高トータル一覧の表示 */
int main()
{
   char     accountType;     /* SまたはC */
   unsigned keepLooping;     /* 0 -> ループを終了 */
   float    chkTotal;        /* 当座預金口座のトータル金額 */
   float    svgTotal;        /* 普通預金口座のトータル金額 */
   float    total;           /* 全口座のトータル金額 */
   int      i;
   int      noChkAccounts;   /* 当座預金口座の口座数 */
   int      noSvgAccounts;   /* 普通預金口座の口座数 */

   /* Xまたはxが入力されるまでループ */
   noChkAccounts = 0;
   noSvgAccounts = 0;
   keepLooping = 1;
   while (keepLooping)       /* 注 1 */
   {
      printf("入力　S（普通預金口座）,"
             " C（当座預金口座）,"
             " X（終了)\n");
      scanf("\n%c", &accountType);

      switch (accountType)
      {
         case 'c':
         case 'C':           /* 注 2 */
            if (noChkAccounts < MAXACCOUNTS)
            {
               initChecking(&chkAcnts[noChkAccounts]);
               processChecking(&chkAcnts[noChkAccounts]);
               noChkAccounts++;
            }
            else
            {
               printf("これ以上口座を開くことはできません。\n");
            }
            break;

         case 's':
         case 'S':          /* 注 3 */
            if (noSvgAccounts < MAXACCOUNTS)
            {
               initSavings(&svgAcnts[noSvgAccounts]);
               processSavings(&svgAcnts[noSvgAccounts]);
               noSvgAccounts++;
            }
            else
            {
               printf("これ以上口座を開くことはできません。\n");
            }
            break;

         case 'x':
         case 'X':          /* 注 4 */
            keepLooping = 0;
            break;
            
         default:
            printf("理解できません。XかCかSを選択してください。\n");
      }
   }

   /* ここで、残高トータルと最終的な結果を表示する */
   chkTotal = 0.0F;     /* 注 5 */
   printf("当座預金口座：\n");
   for (i = 0; i < noChkAccounts; i++)
   {
      printf("口座 %6u = %8.2f\n",
             chkAcnts[i].accountNumber,
             chkAcnts[i].balance);
      chkTotal += chkAcnts[i].balance;
   }
   svgTotal = 0.0F;
   printf("普通預金口座：\n");
   for (i = 0; i < noSvgAccounts; i++)
   {
      printf("口座 %6u = %8.2f (引き出し回数 = %d)\n",
             svgAcnts[i].accountNumber,
             svgAcnts[i].balance,
             svgAcnts[i].noWithdrawals);
      svgTotal += svgAcnts[i].balance;
   }
   total = chkTotal + svgTotal;
   printf("当座預金口座の残高トータル = %8.2f\n", chkTotal);
   printf("普通預金口座の残高トータル = %8.2f\n", svgTotal);
   printf("全預金口座の残高トータル   = %8.2f\n", total);
   return 0;
}

/* initChecking - 当座預金口座を初期化する */
void initChecking(struct Checking *pChecking)
{
   printf("口座番号の入力：");
   scanf("%u", &pChecking->accountNumber);
   pChecking->balance = 0.0F;
}

/* processChecking - 当座預金口座のデータ入力 */
void processChecking(struct Checking *pChecking)
{
   float transaction;

   printf("入力：入金の場合（正の数）\n"
          "出金の場合（負の数）入力を終了する場合（0）");
   do
   {
      printf("：");
      scanf("%f", &transaction);

      /* これは入金か？ */
      if (transaction > 0.0F)
      {
         pChecking->balance += transaction;
      }

      /* それとも出金か？ */
      if (transaction < 0.0F)
      {
         transaction = -transaction;
         if (pChecking->balance < transaction)
         {
            printf("残高が足りません： "
                   "残高 %f, 出金 %f\n",
                   pChecking->balance, transaction);
         }
         else
         {
            pChecking->balance -= transaction;

            /* 残高が少ない場合は、手数料がかかる */
            if (pChecking->balance < 500.00F)
            {
               pChecking->balance -= 0.20F;
            }
         }
      }
   } while (transaction != 0.0F);
}

/* initSavings - 普通預金口座を初期化する */
void initSavings (struct Savings  *pSavings)
{
   printf("口座番号の入力：");
   scanf("%u", &pSavings->accountNumber);
   pSavings->balance = 0.0F;
   pSavings->noWithdrawals = 0;
}

/* processSavings - 普通預金口座のデータ入力 */
void processSavings(struct Savings *pSavings)
{
   float transaction;

   printf("入力：入金の場合（正の数）\n"
          "出金の場合（負の数）入力を終了する場合（0）");
   do
   {
      printf(":");
      scanf("%f", &transaction);

      /* これは入金か？ */
      if (transaction > 0.0F)
      {
         pSavings->balance += transaction;
      }

      /* それとも出金か？ */
      if (transaction < 0.0F)
      {
         transaction = -transaction;
         if (pSavings->balance < transaction)
         {
            printf("残高が足りません： "
                   "残高 %f, 引き出し額 %f\n",
                   pSavings->balance, transaction);
         }
         else
         {
            if (++pSavings->noWithdrawals > 1)
            {
               pSavings->balance -= 5.00F;
            }
            pSavings->balance -= transaction;
         }
      }
   } while (transaction != 0.0F);
}

```