C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
228: Building a Simple Banking System with File Persistence
Up until now, our programs have been "forgetful." Every time you hit Ctrl+C or the program finishes, all your data vanishes into the ether. In a real-world scenario—like a banking system—that's a disaster. We need a way to make our data persist across sessions. Today, we're going to build a rudimentary banking system that saves account records to a binary file.
Defining our account structure
First, we need a way to represent an account. I'm going to use a struct because it keeps the account ID, the owner's name, and the balance bundled together. This makes it much easier to write a single block of memory to a file rather than managing three separate files or a messy CSV.
typedef struct {
int id;
char name[50];
double balance;
} Account;
I'm using a fixed-size array for the name. I'll explain why in a moment, but for now, just know that in C, fixed sizes are your best friend when you're dealing with binary file I/O.
Dumping data to the disk
To save our data, we'll use fwrite. Unlike fprintf, which converts numbers to text, fwrite just grabs a chunk of memory and dumps it exactly as it is onto the disk. It's faster and more efficient for structured data.
void save_accounts(Account accounts[], int count) {
FILE *file = fopen("bank.dat", "wb");
if (file == NULL) {
perror("Error opening file for writing");
return;
}
fwrite(accounts, sizeof(Account), count, file);
fclose(file);
}
Notice the "wb" mode. The 'b' stands for binary. If you're on Windows and forget that 'b', the runtime might try to "help" you by converting newline characters, which will absolutely corrupt your binary data.
The pointer persistence trap
Now, here is where I almost messed up this build. In an earlier draft of this code, I tried to be "efficient" and defined the name as a char *name instead of char name[50]. I thought, "Why waste 50 bytes if the name is only 5 letters?"
The problem is that a pointer is just a memory address. When I used fwrite, I wasn't saving the name "Alice"; I was saving the hexadecimal address 0x7ffc1234. When I restarted the program and read that address back, it pointed to a piece of memory that no longer contained the name—or worse, it pointed to memory the program didn't own, leading to a segmentation fault.
The fix is simple: always use fixed-size arrays inside structs that you intend to write to disk. This ensures the actual data is stored inside the struct, not somewhere else in the heap.
Restoring the bank on startup
To get our data back, we use fread. We need to make sure we have enough space allocated in our array to hold whatever is in the file. For this simple example, we'll assume a maximum of 100 accounts.
int load_accounts(Account accounts[]) {
FILE *file = fopen("bank.dat", "rb");
if (file == NULL) {
return 0; // No file exists yet, which is fine for the first run
}
int count = fread(accounts, sizeof(Account), 100, file);
fclose(file);
return count;
}
In a production system, you'd probably store the number of records at the very beginning of the file so you don't have to guess the count or hardcode a limit, but for our purposes, this gets the job done.
Wiring it all together
Let's put this into a small loop. We'll load the accounts, allow the user to add one, and then save it back. This creates a persistent loop where the data survives the program's death.
int main() {
Account accounts[100];
int count = load_accounts(accounts);
printf("Loaded %d accounts.\n", count);
if (count < 100) {
accounts[count].id = count + 1;
printf("Enter name for new account: ");
scanf("%s", accounts[count].name);
accounts[count].balance = 0.0;
count++;
}
save_accounts(accounts, count);
printf("Bank state saved. Total accounts: %d\n", count);
return 0;
}
It's a basic flow, but it demonstrates the core cycle of persistence: Load → Modify → Save.
📋 Practical Task
Exercise: Implementing a Balance Update Feature
Your task is to extend the banking system we just built. Currently, the program only adds a new account every time it runs. You need to modify the program to allow the user to update the balance of an existing account and ensure that change is persisted to the file.
Requirements:
- Modify the
mainfunction to ask the user for an Account ID. - Search the
accountsarray for that ID. - If found, prompt the user for a deposit amount and add it to the balance.
- Call
save_accountsafter the update to ensure the new balance is written tobank.dat. - If the ID is not found, print a friendly error message.
There are no comments for now.