๐พ Why File I/O Matters
Every program in this entire roadmap has lost all its data the instant it closed. This phase fixes that โ genuinely persisting data between runs.
Phase 5's BankAccount, Phase 7's inventory system, Phase 9's generic Stack โ every single one loses all its data the moment the program exits. A real, useful program almost always needs to SAVE data somewhere and LOAD it back later. File I/O ("input/output") is how.
Phase 1 Module 6's std::cin and std::cout read from and write to the console. File I/O uses genuinely similar tools โ << and >> still work the same way โ just pointed at a FILE instead of the screen/keyboard.
| Tool | Purpose |
|---|---|
std::ofstream | "output file stream" โ writing TO a file (Module 1) |
std::ifstream | "input file stream" โ reading FROM a file (Module 2) |
โ๏ธ Writing to a File โ ofstream
Your first genuinely persistent program โ data that survives after the program closes.
#include <fstream> int main() { std::ofstream outFile("scores.txt"); outFile << "Alex: 85" << std::endl; outFile << "Jordan: 92" << std::endl; outFile.close(); return 0; }
Creating an std::ofstream with a filename opens (creating it if it doesn't exist) that file for writing. << works EXACTLY like Phase 1's std::cout << โ except the text goes into the file instead of the screen. .close() explicitly closes the file, ensuring everything is properly saved.
.close() explicitly, the file is still safely closed when outFile goes out of scope, since ofstream's own destructor handles it. Calling .close() explicitly is still good practice for clarity, but you're not risking a leak either way.std::ofstream outFile("scores.txt", std::ios::app); // append mode โ adds to the end, doesn't erase existing content
By default, opening a file for writing ERASES any existing content first. Passing std::ios::app as a second argument switches to append mode โ new writes are added to the end, preserving what was already there.
๐ Reading from a File โ ifstream
Getting Module 1's saved data back โ the read side of persistence.
#include <fstream> std::ifstream inFile("scores.txt"); std::string word; while (inFile >> word) { // reads one word at a time, exactly like cin >> std::cout << word << std::endl; } inFile.close();
inFile >> word reads one whitespace-separated word at a time โ genuinely the exact same behavior as Phase 1 Module 6's std::cin >> word, including the "stops at spaces" limitation from that lesson. Using it as a while loop's condition works because the stream itself becomes "false" once there's nothing left to read.
A stream, when used as a condition, evaluates to true if the last read succeeded and false once it fails (typically, reaching the end of the file). This directly connects to Phase 2's loop patterns โ genuinely the sentinel pattern from Phase 2 Module 6, except the "sentinel" here is the file simply running out of content, rather than a specific value the user types.
โ Checking If a File Opened Successfully
Files fail to open constantly in the real world โ wrong path, missing permissions, the file genuinely doesn't exist. A real program has to handle this.
std::ifstream inFile("does_not_exist.txt"); std::string word; inFile >> word; // doesn't crash โ word just stays unchanged, silently
Opening a nonexistent file for reading doesn't throw an exception (Phase 10) or crash by default โ it just quietly fails, and subsequent reads simply don't do anything. This is a genuinely common source of confusing bugs for beginners: the program runs, produces no error, and just... does nothing useful.
std::ifstream inFile("scores.txt"); if (!inFile.is_open()) { std::cout << "Error: could not open scores.txt" << std::endl; return 1; } // safe to proceed โ the file genuinely opened
.is_open() returns true if the file genuinely opened successfully. This directly connects to Phase 4 Module 9's null-checking discipline โ verify success BEFORE trusting the resource is usable, exactly the same defensive habit applied to a different kind of resource.
๐ Reading Line by Line
Module 2's word-by-word reading breaks apart at every space โ genuinely wrong for data like "Jordan Smith: 92" where you want the whole line intact.
std::ifstream inFile("scores.txt"); std::string line; while (std::getline(inFile, line)) { std::cout << "Line: " << line << std::endl; }
This is genuinely the exact same std::getline from Phase 1 Module 6, just given a file stream instead of std::cin โ it reads one entire line at a time, spaces and all, stopping only at the newline character. The while (std::getline(...)) pattern works identically to Module 3's while (inFile >> word) โ the function itself returns something that evaluates to false once there's nothing left to read.
| Situation | Better Fit |
|---|---|
| Data is naturally space-separated tokens (like individual numbers) | inFile >> value (Module 2) |
| Each line represents one complete, meaningful record (a name, a sentence) | std::getline(inFile, line) |
๐ Working With Structured Data
Real files often store multiple pieces of information per line, in a consistent format. Here's the standard approach for parsing them.
Alex,25,alex@example.com Jordan,30,jordan@example.com
Each line has three pieces of data (name, age, email), separated by commas โ a genuinely common, simple real-world format. To parse it, you need to split each line at the commas.
#include <sstream> std::string line = "Alex,25,alex@example.com"; std::stringstream ss(line); std::string name, ageStr, email; std::getline(ss, name, ','); // reads up to the first comma: "Alex" std::getline(ss, ageStr, ','); // reads up to the next comma: "25" std::getline(ss, email, ','); // reads to the end: "alex@example.com" int age = std::stoi(ageStr); // convert the text "25" into a real int
std::stringstream treats a std::string AS IF it were a stream โ letting you reuse Module 4's std::getline, but with a custom delimiter (',' instead of the default newline) to split the line at each comma. std::stoi ("string to int") converts the resulting text into a real number, exactly like Phase 10's capstone used it.
๐ Capstone โ A Contact Book
Every idea from this phase, combined: writing structured data to a file, reading it back, and genuinely persisting a Phase 5-style class between separate program runs.
Build a Contact class (Phase 5), a function to save a vector<Contact> (Phase 7) to a CSV file, and a function to load them back โ genuinely surviving between separate runs of the program, not just within one execution.
#include <iostream> #include <fstream> #include <sstream> #include <vector> class Contact { public: std::string name; int age; std::string email; Contact(std::string n, int a, std::string e) : name(n), age(a), email(e) {} }; void saveContacts(const std::vector<Contact>& contacts, std::string filename) { std::ofstream outFile(filename); for (const auto& c : contacts) { outFile << c.name << "," << c.age << "," << c.email << std::endl; } } std::vector<Contact> loadContacts(std::string filename) { std::vector<Contact> contacts; std::ifstream inFile(filename); if (!inFile.is_open()) { std::cout << "No existing contact file found. Starting fresh." << std::endl; return contacts; } std::string line; while (std::getline(inFile, line)) { std::stringstream ss(line); std::string name, ageStr, email; std::getline(ss, name, ','); std::getline(ss, ageStr, ','); std::getline(ss, email, ','); contacts.push_back(Contact(name, std::stoi(ageStr), email)); } return contacts; } int main() { std::vector<Contact> contacts = loadContacts("contacts.csv"); contacts.push_back(Contact("Jude", 30, "jude@example.com")); saveContacts(contacts, "contacts.csv"); std::cout << "Contacts saved. Total: " << contacts.size() << std::endl; return 0; }
loadContactschecks.is_open()(Module 4) and handles a missing file gracefully โ the FIRST time this program ever runs, there's genuinely no file yet, and that's a normal, expected case, not an error.- Each line is parsed with Module 6's
stringstream/comma-delimiter pattern into a realContactobject. - Run this program, close it, and run it again โ the contact you added persists, genuinely proving the data survived between separate executions, not just within one run.
Add a Search Function
Write a function that takes the loaded vector<Contact> and a name, using Phase 7 Module 6's std::find (with a lambda, since you're searching by a specific FIELD rather than the whole object) to locate a matching contact and print their email. This combines file loading with STL searching โ genuinely two different phases' tools working together on the same data.