LearnAI ToolsCareerPractice BuildsPlayContact
C ProgrammingIntermediate~2 hours

Banking System

Develop a simple banking application with account creation, deposit, withdrawal, and balance inquiry.

PointersFunctionsFile Handling

Overview

A banking system is an excellent project for practicing pointers because it has a natural reason to use them: every operation on an account (deposit, withdrawal, balance check) needs to change or read the actual account data, not a disposable copy of it. Passing a `struct Account *` into a function and modifying `acc->balance` directly is the idiomatic C way to do this, and it mirrors exactly how larger real-world systems pass records around by reference instead of copying them everywhere.

This tutorial builds a console banking application that stores multiple accounts in an array, lets the user create accounts, deposit and withdraw money with validation, check balances, and persist every account to a file so the bank's data survives between runs.

What You'll Build
  • An `Account` struct holding an account number, holder name, and balance.
  • Account creation that assigns a new, unique account number automatically.
  • A deposit function that modifies the balance through a pointer to the account.
  • A withdrawal function that validates the amount and rejects overdrafts.
  • A balance inquiry feature that looks up an account by number.
  • File handling that saves all accounts to disk and reloads them on startup.

Prerequisites

  • Structures — defining and using a `struct` to group related fields.
  • Pointers — the `&` address-of operator, the `->` member-access operator on struct pointers, and passing pointers to functions.
  • Functions — writing functions that take parameters by pointer so they can modify the caller's data.
  • File handling — reading and writing structured data with `fopen`, `fwrite`, `fread`, and `fclose`.
  • Basic validation logic — using `if` statements to reject invalid input before it changes program state.

Project Structure

All accounts live in a single array, `Account accounts[MAX_ACCOUNTS]`, tracked alongside an `accountCount` variable that records how many slots are currently in use. Every function that needs to change an account's data — `deposit`, `withdraw` — takes an `Account *acc` parameter, so it operates directly on the account stored in the array rather than a copy. Lookup functions like `findAccount` return a pointer into the array (or `NULL` if nothing matches), which the calling code then passes straight into `deposit` or `withdraw`.

Persistence works the same way as the Contact Book project: the entire `accounts` array is written to `accounts.dat` in one `fwrite` call whenever the data changes, and read back with one `fread` call at startup. Because `Account` has fixed-size fields, the whole array can be treated as one contiguous block of bytes for binary I/O.

Step 1: Define the Account Structure

Each account needs an account number to identify it, a holder name, and a balance. We also declare the global-ish array and count that every other function will operate on.

#include <stdio.h> // printf/scanf and FILE for console and file I/O
#include <string.h> // String helpers used elsewhere in the program
#include <stdlib.h> // Standard utility library, included by convention
#define FILENAME "accounts.dat" // Single named constant for the data file, used by save/load
#define MAX_ACCOUNTS 100 // Upper bound on how many accounts the bank can hold
#define NAME_LEN 50 // Max stored length for an account holder's name
typedef struct {
int accountNumber; // Unique identifier assigned automatically at creation time
char holderName[NAME_LEN]; // Fixed-size buffer so the whole struct has a predictable byte size
double balance; // Current balance, floating-point to support cents
} Account;
Account accounts[MAX_ACCOUNTS]; // All accounts live in this one array for the program's lifetime
int accountCount = 0; // How many of accounts[]'s slots are currently in use

Step 2: Create a New Account

Creating an account means reading the holder's name and an initial deposit, assigning the next available account number (starting at 1001), storing the new `Account` in the array, and incrementing `accountCount`. Choosing the account number ourselves — rather than asking the user for one — guarantees every account number is unique.

void createAccount(void) {
if (accountCount >= MAX_ACCOUNTS) { // Guard against writing past the end of accounts[]
printf("Cannot create more accounts, bank is full.\n");
return;
}
Account *acc = &accounts[accountCount]; // Point at the next free slot so we can build it up via acc->
acc->accountNumber = 1001 + accountCount; // Deriving the number from the count guarantees uniqueness
printf("Enter account holder name: ");
scanf(" %49[^\n]", acc->holderName);
printf("Enter initial deposit amount: ");
scanf("%lf", &acc->balance); // %lf reads a double directly into the balance field
if (acc->balance < 0) { // Defend against a negative starting balance from bad input
printf("Initial deposit cannot be negative. Setting balance to 0.\n");
acc->balance = 0;
}
printf("Account created! Your account number is %d.\n", acc->accountNumber);
accountCount++; // Only now, after the account is fully populated, mark the slot as used
}

`Account *acc = &accounts[accountCount];` takes the address of the next free slot in the array and stores it in a pointer, so every following line uses `acc->field` instead of the longer `accounts[accountCount].field`. This is a common C idiom: point at the thing you are about to build up, then work through the pointer.

Sample Run of createAccount()

Click Run to see what this code prints.

Step 3: Deposit Funds Through a Pointer

The `deposit` function is where pointers earn their keep: it takes an `Account *acc` and an amount, validates the amount, and updates `acc->balance` directly. Because `acc` is a pointer to the actual struct sitting inside the `accounts` array, this change is visible everywhere else in the program immediately — there is no copy to keep in sync.

void deposit(Account *acc, double amount) { // Pointer means changes are visible on the original account
if (amount <= 0) { // Reject zero or negative deposits before touching the balance
printf("Deposit amount must be positive.\n");
return;
}
acc->balance += amount; // Modifies the real account in the accounts[] array, not a copy
printf("Deposited %.2f. New balance: %.2f\n", amount, acc->balance);
}

Step 4: Withdraw Funds With Validation

Withdrawal follows the same pointer pattern as deposit, but adds a second validation rule: the account must have enough balance to cover the withdrawal. Rejecting an overdraft before touching `acc->balance` keeps the account in a consistent state even when the user requests an invalid amount.

void withdraw(Account *acc, double amount) {
if (amount <= 0) { // Reject zero or negative withdrawal amounts
printf("Withdrawal amount must be positive.\n");
return;
}
if (amount > acc->balance) { // Reject overdrafts before the balance is ever changed
printf("Insufficient funds. Current balance: %.2f\n", acc->balance);
return;
}
acc->balance -= amount; // Both validations passed, safe to subtract
printf("Withdrew %.2f. New balance: %.2f\n", amount, acc->balance);
}

Step 5: Check Balance and Find an Account

`findAccount` searches the array for a matching account number and returns a pointer to it, or `NULL` if no account matches. Returning a pointer (instead of copying the whole struct out) means the caller can use that same pointer to deposit or withdraw immediately, without searching the array a second time.

Account* findAccount(int accNum) { // Returns a pointer into the array so callers can modify the account directly
int i;
for (i = 0; i < accountCount; i++) {
if (accounts[i].accountNumber == accNum) {
return &accounts[i]; // Found it: hand back its address, not a copy
}
}
return NULL; // No match; callers must check for this before dereferencing
}
void checkBalance(Account *acc) {
printf("Account #%d (%s) - Balance: %.2f\n",
acc->accountNumber, acc->holderName, acc->balance);
}

Step 6: Persist Accounts to File

Saving writes the entire `accounts` array to disk in one `fwrite` call; loading reads it back in one `fread` call at program startup, restoring both the account data and `accountCount`. Because `fread` returns the number of elements it successfully read, we use that value directly as the new `accountCount`.

void saveAccounts(void) {
FILE *fp = fopen(FILENAME, "wb"); // "wb" truncates, so this always writes the current full state
if (fp == NULL) {
printf("Error: could not save accounts.\n");
return;
}
fwrite(accounts, sizeof(Account), accountCount, fp); // Whole array written as one contiguous block
fclose(fp);
}
void loadAccounts(void) {
FILE *fp = fopen(FILENAME, "rb");
if (fp == NULL) {
accountCount = 0; // No file yet, so start with an empty bank rather than crashing
return;
}
accountCount = (int)fread(accounts, sizeof(Account), MAX_ACCOUNTS, fp); // fread's return value is exactly how many accounts existed
fclose(fp);
}

Step 7: Build the Menu Loop

The menu loop loads existing accounts on startup, offers the usual numbered options, looks up the target account by number for deposit/withdraw/balance operations, and saves the account list back to disk after every change so no data is lost even if the program is closed unexpectedly.

int main(void) {
int choice, accNum; // Menu selection and the account number typed for lookups
double amount; // Deposit or withdrawal amount typed by the user
Account *acc; // Result of findAccount(), reused across the deposit/withdraw/balance cases
loadAccounts(); // Restore whatever accounts were saved from a previous run
do {
printf("\n===== BANKING SYSTEM =====\n");
printf("1. Create Account\n");
printf("2. Deposit\n");
printf("3. Withdraw\n");
printf("4. Check Balance\n");
printf("5. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
createAccount();
saveAccounts(); // Persist immediately so a new account survives an unexpected exit
break;
case 2:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum); // Look up once, reuse the pointer below
if (acc == NULL) {
printf("Account not found.\n");
} else {
printf("Enter deposit amount: ");
scanf("%lf", &amount);
deposit(acc, amount); // Modifies the account in place through the pointer
saveAccounts(); // Save right after the change so nothing is lost
}
break;
case 3:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum);
if (acc == NULL) {
printf("Account not found.\n");
} else {
printf("Enter withdrawal amount: ");
scanf("%lf", &amount);
withdraw(acc, amount);
saveAccounts();
}
break;
case 4:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum);
if (acc == NULL) {
printf("Account not found.\n");
} else {
checkBalance(acc); // Read-only, so no save is needed here
}
break;
case 5:
printf("Goodbye!\n");
break;
default:
printf("Invalid choice, try again.\n"); // Catches anything outside 1-5
}
} while (choice != 5); // Loop until the user explicitly chooses Exit
return 0;
}

Complete Code

Here is the full program, ready to compile with `gcc banking.c -o banking`.

#include <stdio.h> // printf/scanf and FILE for console and file I/O
#include <string.h> // String helpers used elsewhere in the program
#include <stdlib.h> // Standard utility library, included by convention
#define FILENAME "accounts.dat" // Single named constant for the data file, used by save/load
#define MAX_ACCOUNTS 100 // Upper bound on how many accounts the bank can hold
#define NAME_LEN 50 // Max stored length for an account holder's name
typedef struct {
int accountNumber; // Unique identifier assigned automatically at creation time
char holderName[NAME_LEN]; // Fixed-size buffer so the whole struct has a predictable byte size
double balance; // Current balance, floating-point to support cents
} Account;
Account accounts[MAX_ACCOUNTS]; // All accounts live in this one array for the program's lifetime
int accountCount = 0; // How many of accounts[]'s slots are currently in use
void createAccount(void) {
if (accountCount >= MAX_ACCOUNTS) { // Guard against writing past the end of accounts[]
printf("Cannot create more accounts, bank is full.\n");
return;
}
Account *acc = &accounts[accountCount]; // Point at the next free slot so we can build it up via acc->
acc->accountNumber = 1001 + accountCount; // Deriving the number from the count guarantees uniqueness
printf("Enter account holder name: ");
scanf(" %49[^\n]", acc->holderName);
printf("Enter initial deposit amount: ");
scanf("%lf", &acc->balance); // %lf reads a double directly into the balance field
if (acc->balance < 0) { // Defend against a negative starting balance from bad input
printf("Initial deposit cannot be negative. Setting balance to 0.\n");
acc->balance = 0;
}
printf("Account created! Your account number is %d.\n", acc->accountNumber);
accountCount++; // Only now, after the account is fully populated, mark the slot as used
}
void deposit(Account *acc, double amount) { // Pointer means changes are visible on the original account
if (amount <= 0) { // Reject zero or negative deposits before touching the balance
printf("Deposit amount must be positive.\n");
return;
}
acc->balance += amount; // Modifies the real account in the accounts[] array, not a copy
printf("Deposited %.2f. New balance: %.2f\n", amount, acc->balance);
}
void withdraw(Account *acc, double amount) {
if (amount <= 0) { // Reject zero or negative withdrawal amounts
printf("Withdrawal amount must be positive.\n");
return;
}
if (amount > acc->balance) { // Reject overdrafts before the balance is ever changed
printf("Insufficient funds. Current balance: %.2f\n", acc->balance);
return;
}
acc->balance -= amount; // Both validations passed, safe to subtract
printf("Withdrew %.2f. New balance: %.2f\n", amount, acc->balance);
}
Account* findAccount(int accNum) { // Returns a pointer into the array so callers can modify the account directly
int i;
for (i = 0; i < accountCount; i++) {
if (accounts[i].accountNumber == accNum) {
return &accounts[i]; // Found it: hand back its address, not a copy
}
}
return NULL; // No match; callers must check for this before dereferencing
}
void checkBalance(Account *acc) {
printf("Account #%d (%s) - Balance: %.2f\n",
acc->accountNumber, acc->holderName, acc->balance);
}
void saveAccounts(void) {
FILE *fp = fopen(FILENAME, "wb"); // "wb" truncates, so this always writes the current full state
if (fp == NULL) {
printf("Error: could not save accounts.\n");
return;
}
fwrite(accounts, sizeof(Account), accountCount, fp); // Whole array written as one contiguous block
fclose(fp);
}
void loadAccounts(void) {
FILE *fp = fopen(FILENAME, "rb");
if (fp == NULL) {
accountCount = 0; // No file yet, so start with an empty bank rather than crashing
return;
}
accountCount = (int)fread(accounts, sizeof(Account), MAX_ACCOUNTS, fp); // fread's return value is exactly how many accounts existed
fclose(fp);
}
int main(void) {
int choice, accNum; // Menu selection and the account number typed for lookups
double amount; // Deposit or withdrawal amount typed by the user
Account *acc; // Result of findAccount(), reused across the deposit/withdraw/balance cases
loadAccounts(); // Restore whatever accounts were saved from a previous run
do {
printf("\n===== BANKING SYSTEM =====\n");
printf("1. Create Account\n");
printf("2. Deposit\n");
printf("3. Withdraw\n");
printf("4. Check Balance\n");
printf("5. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice) {
case 1:
createAccount();
saveAccounts(); // Persist immediately so a new account survives an unexpected exit
break;
case 2:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum); // Look up once, reuse the pointer below
if (acc == NULL) {
printf("Account not found.\n");
} else {
printf("Enter deposit amount: ");
scanf("%lf", &amount);
deposit(acc, amount); // Modifies the account in place through the pointer
saveAccounts(); // Save right after the change so nothing is lost
}
break;
case 3:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum);
if (acc == NULL) {
printf("Account not found.\n");
} else {
printf("Enter withdrawal amount: ");
scanf("%lf", &amount);
withdraw(acc, amount);
saveAccounts();
}
break;
case 4:
printf("Enter account number: ");
scanf("%d", &accNum);
acc = findAccount(accNum);
if (acc == NULL) {
printf("Account not found.\n");
} else {
checkBalance(acc); // Read-only, so no save is needed here
}
break;
case 5:
printf("Goodbye!\n");
break;
default:
printf("Invalid choice, try again.\n"); // Catches anything outside 1-5
}
} while (choice != 5); // Loop until the user explicitly chooses Exit
return 0;
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a PIN field to `Account` and require it to match before any deposit, withdrawal, or balance check succeeds.
  • Write a `transferFunds(Account *from, Account *to, double amount)` function that withdraws from one account and deposits into another atomically.
  • Log every transaction (type, amount, resulting balance, timestamp) to a separate `transactions.dat` file for an auditable history.
  • Add an interest calculation feature that applies a fixed annual interest rate to every account's balance when the user selects a "Run Interest Cycle" menu option.
  • Introduce account types (Savings vs. Checking) with different withdrawal rules, such as a minimum balance requirement for Savings accounts.

Summary

You built a working banking system that uses pointers the way real C programs do: passing `Account *` into functions so they can read and modify the actual account data in place, backed by binary file persistence so no transaction is lost between runs. The pattern of "find a pointer to the record, then operate on it through that pointer" is one of the most common and useful idioms in C, and you now have hands-on practice applying it.