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
Accountstruct 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
structto 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, andfclose. - Basic validation logic — using
ifstatements 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
Accountand 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.datfile 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.