Files and String Streams

Every program so far took its input from the keyboard and sent its output to the output panel. When the program ended, all of it was gone. A file is text that is kept under a name, such as scores.txt, and is still there after the program has ended. This lesson reads files and writes them.

Very little of it is new. std::cin is a stream: a source that hands over one value after another. A file is opened as a stream too, and >> and std::getline work on it as they do on std::cin. The last two sections make a stream out of a line of text, and convert between text and numbers.

Reading from a file

To read a file, a program declares a variable of type std::ifstream, an "input file stream", and gives it the file's name. The type comes from the header <fstream>.

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream file("shelf.txt");
    std::string title;
    int copies = 0;
    int total = 0;
    while (file >> title >> copies) {
        std::cout << title << ": " << copies << "\n";
        total += copies;
    }
    std::cout << "Books: " << total << "\n";
}
Atlas 3
Novel 12 Poems
5
Atlas: 3
Novel: 12
Poems: 5
Books: 20

std::ifstream file("shelf.txt"); reads: the type std::ifstream, the name file, and in parentheses the name of the file to open. The variable could have any name. From this line on, file is a stream that hands over what shelf.txt holds, starting with its first character.

Everything else is module 3 and module 5 with file in the place of std::cin.

Three things are different from the keyboard.

<fstream> is a header like the others. A program that uses std::ifstream must include it.

The file that is not there

The program above trusts that shelf.txt exists. Now suppose the name is misspelt.

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream file("shelv.txt");
    if (!file) {
        std::cout << "Cannot open shelv.txt\n";
        return 0;
    }
    std::string title;
    int copies = 0;
    int total = 0;
    while (file >> title >> copies) {
        total += copies;
    }
    std::cout << "Books: " << total << "\n";
}
Atlas 3
Novel 12 Poems
5
Cannot open shelv.txt

There is no file named shelv.txt, so it cannot be opened. That is not a compile error: the compiler knows nothing about the files a program will find when it runs. It does not stop the program either. The stream file is created all the same, in a failed state, and from then on every read from it fails at once and stores nothing.

Without the if, this program would run to the end and print Books: 0. The loop's test would be false the first time, and total would still hold the 0 it started with. A reader of that output cannot tell a missing file from a shelf with no books on it. So the rule is: test the stream straight after opening it, before reading anything.

Module 4 used a read as a condition: if (std::cin >> x). A stream on its own can be a condition too. It counts as true while the stream is usable, and as false once something has failed. ! is the "not" of module 4, so if (!file) reads "if the file is not usable".

Inside the if, the program says what went wrong and leaves with return 0;. A return in main ends the program, as a return ends any function. Outside the lab, many programs return 1 at such a place, to tell whatever started them that something went wrong. This course returns 0 everywhere.

A file's name is a string, so it does not have to be written into the program. It can be read from the input.

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::string name;
    std::cin >> name;
    std::ifstream file(name);
    if (!file) {
        std::cout << "Cannot open " << name << "\n";
        return 0;
    }
    int value = 0;
    int largest = 0;
    while (file >> value) {
        if (value > largest) {
            largest = value;
        }
    }
    std::cout << "Largest in " << name << ": " << largest << "\n";
}
14 62 37
depths.txt
Largest in depths.txt: 62

This program works for any file of numbers that are not negative, and it reports a name it cannot open in place of an answer that looks real.

A file that opens may still be empty. Then the stream is usable, the first read fails because there is nothing to read, and the loop body never runs. Opening and reading are tested separately.

A file, line by line

>> reads word by word and forgets where the lines ended. When the lines matter, read them with std::getline. Its first value is the stream to read from, which until now was always std::cin.

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream file("settings.txt");
    if (!file) {
        std::cout << "Cannot open settings.txt\n";
        return 0;
    }
    std::string line;
    int kept = 0;
    int skipped = 0;
    while (std::getline(file, line)) {
        if (line.size() == 0 || line[0] == '#') {
            ++skipped;
        } else {
            std::cout << line << "\n";
            ++kept;
        }
    }
    std::cout << kept << " settings, " << skipped << " other lines\n";
}
# colours
ink dark blue
paper cream

# sizes
margin 24
ink dark blue
paper cream
margin 24
3 settings, 3 other lines

while (std::getline(file, line)) is the loop of module 9. Each turn reads one whole line into line, spaces included, and removes the line end from the stream without storing it. The test is false when there are no more lines.

This program treats a line that starts with # as a note and does not print it. Notice the order of the two tests joined by ||. An empty line has no character at position 0, so line.size() == 0 is asked first. When it is true the answer is already known, and line[0] is never looked at.

Three facts about lines.

A std::string variable holds one line at a time. To keep them all, push_back each line onto a std::vector<std::string>, as module 9 did with lines from the keyboard.

Writing a file

The type for writing is std::ofstream, an "output file stream", from the same header. Everything << can send to std::cout it can send to an std::ofstream.

#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>

int main() {
    std::ofstream out("label.txt");
    if (!out) {
        std::cout << "Cannot write label.txt\n";
        return 0;
    }
    out << "To: Oslo\n";
    out << std::setw(6) << 2300 << " grams\n";
    out.close();

    std::ifstream back("label.txt");
    std::string line;
    int lines = 0;
    while (std::getline(back, line)) {
        ++lines;
    }
    std::cout << "label.txt has " << lines << " lines\n";
}
To: Turin
450 grams
fragile
keep dry
label.txt has 2 lines
To: Oslo
  2300 grams

Before the program ran, label.txt held four lines about a parcel for Turin. Afterwards it holds two lines about Oslo, and nothing else.

Opening a file for writing empties it. The four old lines are gone at the line std::ofstream out("label.txt");, before anything new is written. What the program then sends with << is the whole of the new file. If no file of that name exists, opening creates one.

Nothing appears in the output panel for out << .... The text goes to the file and only there. "\n" ends a line in a file as it does on the screen, and std::setw, std::fixed and std::setprecision work as they do with std::cout.

Opening for writing can fail as well, for example when the name is not one a file may have. The test is the same if (!out).

out.close(); finishes the file. An output stream does not hand every character to the file the moment << runs. It collects text and passes it on in larger pieces, and the last piece is passed on when the stream is closed. A program that writes a file and then reads it back must close it first, or it may read a file that is not complete yet.

A stream is also closed when its variable ends, at the closing brace of the block or function it was declared in. A function that opens a stream, writes, and returns has therefore closed the file, and at the end of main every file is closed. close() is needed only when the same function goes on to use the file again, as here.

In the lab, every file a program writes is shown with its output. Every Run starts with the task's own files again. A file written by one Run is not there for the next, and a file your program emptied is back as it was.

One function for the keyboard and for a file

A function that reads numbers and adds them up should not care where the numbers come from. Streams make that possible, because a stream can be a parameter.

#include <fstream>
#include <iostream>

double total_of(std::istream& in) {
    double total = 0;
    double amount = 0;
    while (in >> amount) {
        total += amount;
    }
    return total;
}

void print_receipt(std::ostream& out, double total) {
    out << "Total due: " << total << "\n";
}

int main() {
    double typed = total_of(std::cin);

    std::ifstream till("till.txt");
    if (!till) {
        std::cout << "Cannot open till.txt\n";
        return 0;
    }
    double from_file = total_of(till);

    print_receipt(std::cout, typed + from_file);
    std::ofstream receipt("receipt.txt");
    print_receipt(receipt, typed + from_file);
}
10
3.25
2.5 4
Total due: 19.75
Total due: 19.75

std::istream is the type of "any stream that can be read from". std::cin is one. An std::ifstream is one as well: it is an std::istream that happens to get its characters from a file. So total_of accepts both, and inside it in >> amount does not know which it has been given.

std::ostream is the same idea for writing. std::cout is an std::ostream, and so is an std::ofstream. print_receipt is called twice with the same total. The first call writes the line on the screen, the second writes it into receipt.txt. Both types come with <iostream>.

The & is not a choice. A stream parameter is always a reference, for two reasons.

It is a plain reference and not a const one, for the same reason: reading from a stream changes it, and so does writing to one.

By custom the stream is the first parameter. Notice also what total_of does not do. It does not open a file and it does not test whether one opened. The function that has the name of the file does that, here main, and a function that is handed a stream trusts that it is usable.

Taking a line apart

>> cannot say where a line ended, and getline hands over a whole line in one piece. Often a program needs both: read one line, and then read the values inside it. An std::istringstream, an "input string stream", is a stream that gets its characters from a string. Its header is <sstream>.

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::ifstream file("routes.txt");
    if (!file) {
        std::cout << "Cannot open routes.txt\n";
        return 0;
    }
    std::string line;
    while (std::getline(file, line)) {
        std::istringstream parts(line);
        int route = 0;
        parts >> route;
        std::string stop;
        std::string last = "nowhere";
        int stops = 0;
        while (parts >> stop) {
            last = stop;
            ++stops;
        }
        std::cout << "Route " << route << ": " << stops << " stops, ends at " << last << "\n";
    }
}
12 Harbour Market Station
7 Station Hilltop
40 Market Museum Park Harbour Depot
Route 12: 3 stops, ends at Station
Route 7: 2 stops, ends at Hilltop
Route 40: 5 stops, ends at Depot

std::istringstream parts(line); makes a stream named parts that holds a copy of the text of line. Reading from parts with >> works like reading from std::cin or from a file. The difference is where it ends: parts runs out at the end of the one line. The inner while therefore reads the stops of this route and no further, however many there are.

parts is declared inside the outer loop's braces, so every line gets a new stream that starts at its own first character. So do stops and last, which must start afresh for each route.

A string stream also answers the question "does this line hold what I expect?", because a read from it can be tested like any read.

#include <iostream>
#include <sstream>
#include <string>

void show(const std::string& line) {
    std::istringstream parts(line);
    std::string item;
    int amount = 0;
    if (parts >> item >> amount) {
        std::cout << amount << " of " << item << "\n";
    } else {
        std::cout << "skipped [" << line << "]\n";
    }
}

int count_words(std::istream& in) {
    std::string word;
    int words = 0;
    while (in >> word) {
        ++words;
    }
    return words;
}

int main() {
    show("kettle 4");
    show("kettle four");
    show("kettle");
    show("");

    std::istringstream text("one short sentence");
    std::cout << count_words(text) << " words\n";

    std::ostringstream made;
    made << "Total: " << 7 << "\n";
    std::cout << made.str();
}
4 of kettle
skipped [kettle four]
skipped [kettle]
skipped []
3 words
Total: 7

In show, the test parts >> item >> amount is true only when a word and a number were read. four is not a number, the third line has no second value, and the fourth has nothing at all. None of these stops the program. The read fails quietly and the else runs.

An std::istringstream is an std::istream too. count_words takes any input stream, and here it is given text made on the spot. That makes such a function easy to try out: no file is needed, and nothing has to be typed.

The last three lines show the opposite type. An std::ostringstream collects what is sent to it with <<, and made.str() gives everything collected so far as one string. A function that writes to an std::ostream& can be handed one, and what it wrote can then be looked at.

Numbers and text

"12" and 12 are different values of different types. The first is text, two characters long. The second is a number. + joins two strings and adds two numbers, so with std::string a = "12"; and std::string b = "3"; the value of a + b is the text "123".

Two functions from <string> convert between the two.

#include <iostream>
#include <string>

int main() {
    std::string clock = "07:45";
    std::size_t colon = clock.find(':');
    int hours = std::stoi(clock.substr(0, colon));
    int minutes = std::stoi(clock.substr(colon + 1));
    int since_midnight = hours * 60 + minutes;
    std::cout << since_midnight << "\n";

    std::string label = "Minute " + std::to_string(since_midnight) + " of the day";
    std::cout << label << "\n";
    std::cout << label.size() << "\n";

    std::cout << std::stoi("  42 apples") + 1 << "\n";
}
465
Minute 465 of the day
21
43

find and substr from module 8 cut the text at the colon into "07" and "45". Those are still strings. std::stoi turns them into the numbers 7 and 45, and only then can the program multiply and add.

std::to_string(since_midnight) goes the other way. Its result is the string "465", which + can join to other text. Without it, "Minute " + since_midnight would not mean "put the number into the text". Printing with << never needed this, because << converts numbers itself. std::to_string is for building a string that is kept, returned or measured.

The last line shows how std::stoi reads. It skips spaces at the front, takes digits for as long as it finds them, with a minus sign before them if there is one, and stops at the first character that cannot belong to the number. " apples" is ignored.

What if there is no number at the front at all?

#include <iostream>
#include <string>

int main() {
    std::string word = "apples";
    std::cout << "before\n";
    int n = std::stoi(word);
    std::cout << "after " << n << "\n";
}

This program prints before and then stops. std::stoi has no int to give back for "apples", and no way to say so in its result, so the program is ended at that line. The same happens for an empty string, and for digits that spell a number too large for an int. Part IV of the course shows how a program can catch such a failure and carry on. Until then, call std::stoi only on text that is known to hold a number: because the task promises it, or because the program has looked first.

Looking first is a loop over the characters.

#include <cctype>
#include <iostream>
#include <string>

bool is_whole_number(const std::string& text) {
    int n = text.size();
    if (n == 0 || n > 9) {
        return false;
    }
    for (int i = 0; i < n; ++i) {
        if (!std::isdigit(text[i])) {
            return false;
        }
    }
    return true;
}

int main() {
    std::cout << is_whole_number("465") << "\n";
    std::cout << is_whole_number("4x5") << "\n";
    std::cout << is_whole_number("") << "\n";
    std::cout << is_whole_number("12345678901234") << "\n";
}
1
0
0
0

Text of one to nine digits always fits in an int, so std::stoi is safe on any string this function accepts.

A string stream is the other safe way. parts >> amount on text that is not a number fails quietly, as show in the last section relied on. Use >> when the values are separated by spaces. Use find, substr and std::stoi when they are separated by something else, such as the colon here or the commas of the lab's last task.

std::to_string also accepts a double, and then always gives six digits after the point: std::to_string(2.5) is "2.500000". For text with a chosen number of digits, send the value to an std::ostringstream with std::fixed and std::setprecision.