Lesson 11: Input and Output

Lesson 0011 — A Tour of C++, Chapter 11 (§11.1–§11.10)

This chapter covers the two standard I/O facilities. The I/O stream library (<iostream> and friends) provides formatted and unformatted buffered I/O of text and numeric values; it is extensible to support user-defined types exactly like built-in types, and it is type-safe. The file system library (<filesystem>) provides basic facilities for manipulating files and directories, built around the path type. An ostream converts typed objects to a stream of characters; an istream converts a stream of characters to typed objects. The operations are type-safe, type-sensitive, and extensible to user-defined types (§11.5).

Thread running through this chapter: the stream is a stateful converter between typed objects and characters. Every sharp corner is a state question: >> skips leading whitespace (but is.get(c) doesn't); the stream's error state (fail, eof) is how you detect ill-formed input; formatting manipulators are "sticky" (while format() specifiers are not); streams can't be copied (move-only — pass by reference); and I/O from multiple threads is a data race unless synchronized with osyncstream. The interview story is: use format() (C++20, type-safe) over printf, check your file streams opened, and treat input as untrusted data.

11.1 Introduction

The I/O stream library provides formatted and unformatted buffered I/O of text and numeric values. It is extensible to support user-defined types exactly like built-in types and is type-safe. The operations on istreams and ostreams are type-safe, type-sensitive, and extensible to handle user-defined types (§11.5). Other forms of user interaction (graphical I/O) are handled through libraries that are not part of the ISO standard and are not described here. The streams can be used for binary I/O, for a variety of character types, be locale-specific, and use advanced buffering strategies — topics beyond the scope of this book. The I/O stream classes all have destructors that free all resources owned (buffers, file handles): they are examples of "Resource Acquisition Is Initialization" (RAII, §6.3).

11.2 Output

In <ostream>, the I/O stream library defines output for every built-in type, and it's easy to define output for a user-defined type (§11.5). The operator << ("put to") is the output operator on ostream objects; cout is the standard output stream and cerr the standard stream for reporting errors. By default, values written to cout are converted to a sequence of characters:

cout << 10;    // places the characters '1' and '0' on the standard output stream
int x {10};
cout << x;     // same output

// Chaining: the result of an output expression can be used for further output:
void h2(int i) { cout << "the value of i is " << i << '\n'; }   // "the value of i is 10"

A character is output as a character, not as a numerical value:

int b = 'b';   // char implicitly converted to int: 98 (ASCII)
char c = 'c';
cout << 'a' << b << c;   // outputs: a98c

11.3 Input

In <istream>, the standard library offers istreams for input. The operator >> ("get from") is the input operator; cin is the standard input stream. The type of the right-hand operand of >> determines what input is accepted and where it goes:

int i;
cin >> i;   // read an integer into i
double d;
cin >> d;   // read a double-precision floating-point number into d

Input operations chain like output operations: cin >> i >> d;. The read of an integer is terminated by any character that is not a digit. By default, >> skips initial whitespace, so 1234 12.34e5 is a suitable input sequence.

Reading a sequence of characters into a string stops at whitespace by default — entering Eric Bloodaxe still yields Hello, Eric!. To read a whole line, use getline():

string str;
getline(cin, str);   // reads the whole line; the terminating newline is discarded

Using the formatted I/O operations is usually less error-prone, more efficient, and less code than manipulating characters one by one — istreams take care of memory management and range checking. The standard strings expand to hold what you put in them: you don't have to pre-calculate a maximum size, so string input never overflows.

11.4 I/O State

An iostream has a state we can examine to determine whether an operation succeeded. The most common use is reading a sequence of values:

vector<int> read_ints(istream& is) {
    vector<int> res;
    for (int i; is>>i; )   // read until something that is not an integer (typically end of input)
        res.push_back(i);
    return res;
}

The operation is>>i returns a reference to is, and testing an iostream yields true if the stream is ready for another operation. The I/O state holds everything needed to read or write: formatting information, error state (e.g., has end-of-input been reached?), and buffering kind. We can set the state to reflect that an error occurred (§11.5) and clear it if the error wasn't serious:

vector<int> read_ints(istream& is, const string& terminator) {
    vector<int> res;
    for (int i; is >> i; ) res.push_back(i);
    if (is.eof())                    // fine: end of file
        return res;
    if (is.fail()) {                 // we failed to read an int; was it the terminator?
        is.clear();                  // reset the state to good()
        string s;
        if (is>>s && s==terminator) return res;
        is.setstate(ios_base::failbit);   // add fail() to is's state
    }
    return res;
}
auto v = read_ints(cin, "stop");

11.5 I/O of User-Defined Types

The iostream library lets us define I/O for our own types. Consider an Entry for a telephone book:

struct Entry { string name; int number; };

ostream& operator<<(ostream& os, const Entry& e) {
    return os << "{\"" << e.name << "\", " << e.number << "}";
}

A user-defined output operator takes its output stream (by reference) as its first argument and returns it as its result — so chaining keeps working. The corresponding input operator is more complicated because it has to check for correct formatting and deal with errors:

istream& operator>>(istream& is, Entry& e)   // read { "name" , number } pair
{
    char c, c2;
    if (is>>c && c=='{' && is>>c2 && c2=='"') {   // start with a { followed by a "
        string name;
        while (is.get(c) && c!='"')   // anything before a " is part of the name
            name += c;
        if (is>>c && c==',') {
            int number = 0;
            if (is>>number>>c && c=='}') {   // read the number and a }
                e = {name, number};           // assign to the entry
                return is;
            }
        }
    }
    is.setstate(ios_base::failbit);   // register the failure in the stream
    return is;
}

An input operation returns a reference to its istream that can be used to test success — used as a condition, is>>c means "did we succeed in reading a char from is into c?" Note the whitespace subtlety: is>>c skips whitespace by default, but is.get(c) does not — so this input operator ignores whitespace outside the name string, but not within it. Reading { "John Marwood Cleese", 123456 } via for (Entry ee; cin>>ee; ) cout << ee << '\n'; round-trips the entry. For a more systematic pattern-recognition technique, see regular expressions (§10.4).

11.6 Output Formatting

The iostream and format libraries provide operations for controlling the format of input and output. The iostream facilities are about as old as C++ and focus on formatting streams of numbers; the format facilities (§11.6.2) are recent (C++20) and focus on printf()-style (§11.8) specification of combinations of values.

11.6.1 Stream Formatting

The simplest formatting controls are called manipulators and are found in <ios>, <istream>, <ostream>, and <iomanip> (for manipulators that take arguments):

cout << 1234 << ' ' << hex << 1234 << ' ' << oct << 1234 << dec << 1234 << '\n';
// 1234 4d2 2322 1234

constexpr double d = 123.456;
cout << d << "; "                 // default format
     << scientific << d << "; "  // 1.123e2 style
     << hexfloat << d << "; "    // hexadecimal notation
     << fixed << d << "; "       // 123.456 style
     << defaultfloat << d << '\n';  // back to default
// 123.456; 1.234560e+002; 0x1.edd2f2p+6; 123.456000; 123.456

Precision is an integer determining the number of digits used to display a floating-point number. The general format (defaultfloat) lets the implementation choose a style that best preserves the value in the space available, with precision as the maximum number of digits. The scientific format presents one digit before the decimal point plus an exponent, with precision as the maximum digits after the point. The fixed format presents an integer part, decimal point, and fractional part, precision again the digits after the point. Values are rounded, not truncated, and precision() doesn't affect integer output:

cout.precision(8);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n';   // 1234.5679 1234.5679 123456
cout.precision(4);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n';   // 1235 1235 123456

These floating-point manipulators are "sticky": their effects persist for subsequent floating-point operations — they're designed for formatting streams of values. We can also specify the field size and alignment of a number. In addition, << handles time and dates (duration, time_point, year_month_day, weekday, month, zoned_time; §16.2), complex numbers, bitsets, error codes, and pointers.

11.6.2 printf()-style Formatting

It has been credibly argued that printf() is the most popular function in C and a significant factor in its success — but it suffers from a lack of type safety and a lack of extensibility to user-defined types. In <format>, the standard library provides a type-safe, though not extensible, printf()-style mechanism. The basic function format() produces a string:

string s = format("Hello, {}\n", val);   // if val is "World": "Hello, World\n"

Ordinary characters in the format string go straight into the output; {} takes the next argument and prints it with its << default. A formatting directive is preceded by a colon: {:x} hexadecimal, {:o} octal, {:d} decimal, {:b} binary (not directly supported by ostream):

cout << format("{} {:x} {:o} {:d} {:b}\n", 1234,1234,1234,1234,1234);
// 1234 4d2 2322 1234 10011010010

By default format() takes arguments in order, but we can specify an arbitrary order — and format an argument more than once:

cout << format("{3:} {1:x} {2:o} {0:b}\n", 000, 111, 222, 333);   // 333 6f 336 0
cout << format("{0:} {0:x} {0:o} {0:d} {0:b}\n", 1234);            // default, hex, octal, decimal, binary

The number before the colon is the argument index; numbering starts at zero. The ability to place arguments out of order is highly praised by people composing messages in different natural languages. The floating-point formats are the same as for ostream: e scientific, a hexfloat, f fixed, g default. A dot precedes a precision specifier:

cout << format("precision(8): {:.8} {} {}\n", 1234.56789, 1234.56789, 123456);
// precision(8): 1234.5679 1234.56789 123456   — unlike streams, specifiers are NOT sticky

format() offers a mini-language of about 60 format specifiers for very detailed control over numbers and dates; all time and date format strings start with %. If a formatting error is caught at run time, a format_error exception is thrown:

string ss = format("{:%F}", 2);   // error: mismatched argument (potentially caught at compile time)
string sss = format("{%F}", 2);   // error: bad format (potentially caught at compile time)

The constant formats above can be checked at compile time. The complementary vformat() takes a variable as its format — more flexibility, more run-time errors:

string fmt = "{}";
cout << vformat(fmt, make_format_args(2));   // OK
fmt = "{:%F}";
cout << vformat(fmt, make_format_args(2));   // error: format and argument mismatch, caught at run time

Finally, format_to(back_inserter(buf), "iterator: {} {}\n", "Hi! ", 2022) writes directly into a buffer defined by an iterator — interesting for performance when using a stream's buffer directly or some other output device.

11.7 Streams

The standard library directly supports:

We can also define our own streams, e.g., attached to communication channels. Streams cannot be copied — they are move-only; always pass them by reference. All standard-library streams are templates parameterized on character type: ostream is basic_ostream<char>, with a wide-character version wostream (basic_ostream<wchar_t>) for Unicode.

11.7.1 Standard Streams

cout for "ordinary output"; cerr for unbuffered "error output"; clog for buffered "logging output"; cin for standard input.

11.7.2 File Streams

In <fstream>: ifstream for reading from a file, ofstream for writing, fstream for both. Testing that a file stream was properly opened is usually done by checking its state:

ofstream ofs {"target"};   // "o" for "output"
if (!ofs) error("couldn't open 'target' for writing");

ifstream ifs {"source"};   // "i" for "input"
if (!ifs) error("couldn't open 'source' for reading");

Once opened, ofs behaves like an ordinary ostream (just like cout) and ifs like an ordinary istream. File positioning and detailed open control are beyond this book's scope.

11.7.3 String Streams

In <sstream>: istringstream (read from a string), ostringstream (write to a string), stringstream (both). The contents of an ostringstream can be read with str() (a string copy) or view() (a string_view). One common use is formatting before giving the result to a GUI; a string received from a GUI can be parsed by putting it into an istringstream. A stringstream supports general string-based conversion:

template<typename Target = string, typename Source = string>
Target to(Source arg)   // convert Source to Target
{
    stringstream buf;
    Target result;
    if (!(buf << arg)                    // write arg into stream
        || !(buf >> result)              // read result from stream
        || !(buf >> std::ws).eof())     // is anything left in stream?
        throw runtime_error{"to<>() failed"};
    return result;
}

auto x1 = to<string,double>(1.2);   // very explicit (and verbose)
auto x2 = to<string>(1.2);          // Source deduced to double
auto x3 = to<>(1.2);                // Target defaulted to string; Source deduced
auto x4 = to(1.2);                  // the <> is redundant: all args defaulted/deduced

Stroustrup calls this "a good example of the generality and ease of use that can be achieved by a combination of language features and standard-library facilities."

11.7.4 Memory Streams

Streams attached to user-designated memory have existed since the earliest days of C++: the old strstream has been deprecated for decades, but its replacement — spanstream, ispanstream, and ospanstream — won't be official before C++23 (already widely available in implementations):

void user(int arg) {
    array<char,128> buf;
    ospanstream ss(buf);            // takes a span rather than a string
    ss << "write " << arg << " to memory\n";
    // ...
}

Attempts to overflow the target buffer set the stream state to failure (§11.4).

11.7.5 Synchronized Streams

In a multi-threaded system, I/O becomes an unreliable mess unless only one thread uses the stream, or access is synchronized so that only one thread at a time gains access. An osyncstream guarantees that a sequence of output operations completes and its results appear in the output buffer as expected, even if another thread tries to write:

void unsafe(int x, string& s) {
    cout << x;   // a different thread may introduce a data race (§18.2) between these
    cout << s;
}
void safer(int x, string& s) {
    osyncstream oss(cout);   // the two writes complete as a group
    oss << x;
    oss << s;
}

Other threads that also use osyncstreams won't interfere — but a thread using cout directly could. Either use osyncstream consistently or make sure only a single thread produces output to a specific stream. Concurrency is tricky (Chapter 18): avoid data sharing between threads whenever feasible.

11.8 C-style I/O

The C++ standard library also supports the C standard-library I/O, including printf() and scanf(). Many uses of this library are unsafe from a type and security point of view; Stroustrup doesn't recommend its use. It's difficult to use for safe and convenient input, and it does not support user-defined types. If you don't use C-style I/O but care about performance, call

ios_base::sync_with_stdio(false);   // avoid significant overhead

Without that call, the standard iostreams (e.g., cin and cout) can be significantly slowed down to stay compatible with C-style I/O. If you like printf()-style formatted output, use format (§11.6.2): it's type-safe, easier to use, as flexible, and as fast.

11.9 File System

Most systems have a notion of a file system providing access to permanent information stored as files — but their properties and manipulation methods vary greatly. The file system library in <filesystem> offers a uniform interface to most facilities of most file systems. Using it, we can portably express file system paths and navigate through a file system, and examine file types and permissions.

11.9.1 Paths

path f = "dir/hypothetical.cpp";   // naming a file
assert(exists(f));                 // f must exist
if (is_regular_file(f))            // is f an ordinary file?
    cout << f << " is a file; its size is " << file_size(f) << '\n';

A program manipulating a file system usually runs alongside other programs, so the contents can change between two commands: even though we asserted f existed, that may no longer be true on the very next line. A path is a quite complicated class, handling the varied character sets and conventions of many operating systems — including command-line file names from main(). A path is not checked for validity until it is used; even then, validity depends on the conventions of the system the program runs on. A path can be used to open a file: ofstream f {p}; — and tested: if (!f) error("bad file name: ", p);. A string is implicitly converted to a path.

Types for traversing directories: path (a directory path), filesystem_error (a file system exception), directory_entry, directory_iterator, recursive_directory_iterator. Listing a directory:

void print_directory(path p)   // print the names of all files in p
try {
    if (is_directory(p)) {
        cout << p << ":\n";
        for (const directory_entry& x : directory_iterator{p})
            cout << " " << x.path() << '\n';
    }
}
catch (const filesystem_error& ex) { cerr << ex.what() << '\n'; }

Use recursive_directory_iterator{p} to also list subdirectories; copy the paths into a vector and sort to print in lexicographical order. Among the useful path operations (with p, p2 paths):

Operation Meaning
p=p2 Assign p2 to p
p/=p2 p and p2 concatenated using the file-name separator (by default /)
p+=p2 p and p2 concatenated (no separator)
s=p.string() / p.generic_string() p in the native / generic format as a string
p2=p.filename() / p.stem() / p.extension() The filename / stem / extension part of p
i=p.begin() / i=p.end() Iterate over p's element sequence
p==p2, p<p2 Equality and lexicographical comparisons
is>>p, os<<p Stream I/O to/from p
u8path(s) A path from a UTF-8 encoded source

A typical use — examining files in a directory:

void test(path p) {
    if (is_directory(p)) {
        for (const directory_entry& x : directory_iterator(p)) {
            const path& f = x;   // refer to the path part of a directory entry
            if (f.extension() == ".exe")
                cout << f.stem() << " is a Windows executable\n";
            else {
                string n = f.extension().string();
                if (n == ".cpp" || n == ".C" || n == ".cxx")
                    cout << f.stem() << " is a C++ source file\n";
            }
        }
    }
}

Naming conventions, natural languages, and string encodings are rich in complexity — the standard-library filesystem abstractions offer portability and great simplification.

11.9.2 Files and Directories

The standard library offers a small set of file operations implementable on a wide variety of systems: exists(p), copy(p1,p2), copy_file(p1,p2), create_directory(p) (intermediate directories must exist), create_directories(p) (creates all intermediates), current_path() (get or set), file_size(p), remove(p) (file or empty directory). Many operations have overloads taking extra arguments such as permissions.

Like copy(), all operations come in two versions:

Use the error-code versions when operations are expected to fail frequently in normal use; the throwing versions when an error is considered exceptional. The library knows a few common kinds of files and classifies the rest as "other": is_block_file, is_character_file, is_directory, is_empty, is_fifo, is_other, is_regular_file, is_socket, is_symlink, status_known.

11.10 Advice

Here is a summary of the guidance from this chapter. All 30 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.

# Guideline §
1 iostreams are type-safe, type-sensitive, and extensible. 11.1
2 Use character-level input only when you have to. 11.3
3 When reading, always consider ill-formed input. 11.3
4 Avoid endl (if you don't know what endl is, you haven't missed anything).
5 Define << and >> for user-defined types with values that have meaningful textual representations. 11.1
6 Use cout for normal output and cerr for errors. 11.1
7 There are iostreams for ordinary and wide characters, and you can define an iostream for any kind of character. 11.1
8 Binary I/O is supported. 11.1
9 There are standard iostreams for standard I/O streams, files, and strings. 11.2
10 Chain << operations for a terser notation. 11.2
11 Chain >> operations for a terser notation. 11.3
12 Input into strings does not overflow. 11.3
13 By default >> skips initial whitespace. 11.3
14 Use the stream state fail to handle potentially recoverable I/O errors. 11.4
15 We can define << and >> operators for our own types. 11.5
16 We don't need to modify istream or ostream to add new << and >> operators. 11.5
17 Use manipulators or format() to control formatting. 11.6.1
18 precision() specifications apply to all following floating-point output operations. 11.6.1
19 Floating-point format specifications (e.g., scientific) apply to all following floating-point output operations. 11.6.1
20 #include <ios> or <iostream> when using standard manipulators. 11.6
21 Stream formatting manipulators are "sticky" for use for many values in a stream. 11.6.1
22 #include <iomanip> when using standard manipulators taking arguments. 11.6
23 We can output time, dates, etc. in standard formats. 11.6.1
24 Don't try to copy a stream: streams are move only. 11.7
25 Remember to check that a file stream is attached to a file before using it. 11.7.2
26 Use stringstreams or memory streams for in-memory formatting. 11.7.3
27 We can define conversions between any two types that both have string representation. 11.7.3
28 C-style I/O is not type-safe. 11.8
29 Unless you use printf-family functions, call ios_base::sync_with_stdio(false). 11.8
30 Prefer <filesystem> to direct use of platform-specific interfaces. 11.9

Retrieval Quiz

Stream direction

What do an ostream and an istream each convert, and what makes the operations type-safe?

Whitespace rules

What is the difference between cin >> str, getline(cin, str), and is.get(c) regarding whitespace?

I/O state

In read_ints(is, "stop"), why must the function call is.clear() before reading the terminator string, and what does setstate(ios_base::failbit) do?

User-defined I/O operators

What must a user-defined operator<< and operator>> each do, and why is is.get(c) (not is>>c) used to read the Entry's name?

Sticky manipulators

What does it mean that floating-point manipulators like scientific and precision() are "sticky", and what is the contrast with format()?

format() mini-language

What does format("{3:} {1:x} {2:o} {0:b}\n", 000, 111, 222, 333) output, and what does the number before the colon mean?

format_error and vformat

When is a format_error thrown, and what does vformat(fmt, make_format_args(...)) add that format() lacks?

Streams are move-only

Why can't you copy a stream, and what does that mean for function parameters?

File and string streams

After ofstream ofs {"target"}; why test if (!ofs), and what do str() and view() each return on an ostringstream?

osyncstream

Why is void unsafe(int x, string& s) { cout << x; cout << s; } unsafe in a multi-threaded program, and how does osyncstream fix it?

C-style I/O

What is ios_base::sync_with_stdio(false) for, and when should you NOT call it?

path semantics

Why can a file system change "between two commands", and when is a path's validity actually checked?


Notes

Streams :-

ostream typed objects → chars ; istream chars → typed objects

type-safe + type-sensitive + extensible user types like built-ins (§11.5)

RAII destructors free buffers + file handles

move-only pass by reference operator<</>> take ostream&/istream&

templates basic_ostream<char> | wostream wide/unicode

Output/input :-

cout normal | cerr unbuffered errors | clog buffered logging | cin input

<</>> chain return stream ref

char output as char, not number (eg 'a' << 98a98)

>> skips initial whitespace ; stops at whitespace use getline for lines

is.get(c) does not skip whitespace

string input grows no overflow ; no pre-calc size

I/O state :-

is>>i returns is ; testing stream true if ready

eof() end reached | fail() failed read

clear() reset to good continue after recoverable error

setstate(ios_base::failbit) deliberately mark failure

User-defined I/O :-

operator<<(ostream&, const T&) return stream ref

operator>>(istream&, T&) check format char-by-char ; setstate(failbit) on failure

whitespace >> skips v/s get(c) doesn't names keep spaces

Formatting :-

manipulators hex/oct/dec ; scientific/hexfloat/fixed/defaultfloat ; <iomanip> for arg-taking

sticky persist for following floats ; designed for value streams

precision general = max digits ; sci/fixed = digits after point ; rounds, not truncates ; × integers

field size + alignment specifiable

format() :-

C++20 type-safe printf <format> ; × extensible to user types

{} next arg ; {:x} hex {:o} oct {:d} dec {:b} binary

{3:} index (zero-based) reorder + repeat {0:} {0:x}

floats e sci a hexfloat f fixed g default ; {:.8} precision not sticky

time/date specifiers start with % ; ~60 specifiers

format_error mismatch at run time (compile-time if constant)

vformat(fmt, make_format_args(...)) variable format, run-time only

format_to(back_inserter(buf), ...) write into iterator buffer

Stream kinds :-

standard cout/cerr/clog/cin

file ifstream/ofstream/fstream check state after open if (!ofs)

string istringstream/ostringstream/stringstream ; str() copy | view() string_view

memory spanstream C++23 (strstream deprecated) ; overflow → failure state

sync osyncstream groups output ops no interleave ; × direct cout user

C-style I/O :-

printf/scanf not type-safe ; × user-defined types

ios_base::sync_with_stdio(false) remove stdio-compat overhead (× if mixing)

prefer format() type-safe, easy, flexible, fast

Filesystem :-

<filesystem> uniform interface, portable

path not validated until used ; fs changes between commands re-check

/= concat with separator | += without ; filename()/stem()/extension() ; u8path()

iteration directory_iterator | recursive_directory_iterator ; directory_entry

ops exists/copy/copy_file/create_directories/current_path/file_size/remove

two versions throwing filesystem_error | error_code& for expected failures

types is_regular_file, is_directory, is_symlink … rest = is_other


Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed., Chapter 11: "Input and Output." Addison-Wesley.
Reference: Chapter 1–11 Quick Reference & Glossary — keep it beside you while you study.
Recommended supplement: cppreference on I/O library, std::format, std::filesystem, and sync_with_stdio.

Questions? Ask your agent — your teacher — about anything unclear: why a stream is move-only, when to use error codes instead of filesystem exceptions, or how osyncstream differs from locking cout. Follow-ups are expected, not optional.

Chapters

  1. Lesson 1: The Basics (Ch. 1)
  2. Lesson 2: User-Defined Types (Ch. 2)
  3. Lesson 3: Separate Compilation (Ch. 3)
  4. Lesson 4: Error Handling (Ch. 4)
  5. Lesson 5: Classes (Ch. 5)
  6. Lesson 6: Essential Operations (Ch. 6)
  7. Lesson 7: Templates (Ch. 7)
  8. Lesson 8: Concepts and Generic Programming (Ch. 8)
  9. Lesson 9: Library Overview (Ch. 9)
  10. Lesson 10: Strings and Regular Expressions (Ch. 10)
  11. Lesson 11: Input and Output (Ch. 11 — this lesson)
  12. Lesson 12: Containers (Ch. 12)
  13. Lesson 13: Algorithms (Ch. 13)
  14. Lesson 14: Ranges (Ch. 14)
  15. Lesson 15: Pointers and Containers (Ch. 15)
  16. Lesson 16: Utilities (Ch. 16)
  17. Lesson 17: Numerics (Ch. 17)
  18. Lesson 18: Concurrency (Ch. 18)
  19. Lesson 19: History and Compatibility (Ch. 19)
← Course Dashboard ← Ch. 10: Strings Ch. 12: Containers → Quick Reference →