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.
- 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 lifetimeint accountCount = 0; // How many of accounts[]'s slots are currently in useStep 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.
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 lifetimeint 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
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.