What Is std::string?

std::string is the standard library type for holding text whose length is decided while the program runs. It lives in the <string> header, it owns the characters it holds, and it resizes itself whenever you hand it something longer or shorter than what it had before.

You have already met C-style string literals in the Literals lesson: a double-quoted run of characters such as "Clay Seals". Those literals are fine to write. C-style string variables are the part to avoid, and the comparison below is why.

Question C-style string variable std::string
Who fixes the capacity? You, while writing the code The object, while the program runs
Can you give it a new value with =? No Yes
What if the new text is longer? Undefined behaviour, it writes past the end It obtains more memory and carries on
How do you find out its length? Walk the characters until the terminator Ask the object for it
What does holding it cost? Almost nothing A memory allocation for all but very short text

Only the last row favours the C-style variable. Everything before it favours std::string, which is why the rest of this lesson is about using std::string well, and why the final section is about paying that last cost as rarely as you can.

std::string is not a fundamental type like int or double. It is a class type, and classes are covered much later. None of that machinery is needed here: the type behaves like a variable that holds text, and that is enough to start using it.

Creating One and Changing What It Holds

A std::string is defined and initialized like any other variable. Empty braces give you a string with no characters in it, and a string literal in the braces gives you a copy of that text.

#include <iostream>
#include <string>

int main()
{
    std::string exhibitTitle{ "Clay Seals" };
    std::cout << exhibitTitle << " (" << exhibitTitle.length() << " chars)\n";

    exhibitTitle = "Songbirds of the Estuary";
    std::cout << exhibitTitle << " (" << exhibitTitle.length() << " chars)\n";

    exhibitTitle = "Ferns";
    std::cout << exhibitTitle << " (" << exhibitTitle.length() << " chars)\n";

    return 0;
}

Output:

Clay Seals (10 chars)
Songbirds of the Estuary (24 chars)
Ferns (5 chars)

One variable held ten characters, then twenty-four, then five, and the program never mentions a capacity anywhere. Printing works the way you would expect too: sending a std::string to std::cout writes exactly the characters it holds, and an empty string writes nothing at all.

Key Concept
When the text will not fit in the space a `std::string` already has, the object requests more memory while the program is running. That mechanism is called dynamic memory allocation, and it is covered in a later chapter. It is what lets one variable hold text of any length, and it is also the reason `std::string` is comparatively slow: a request for memory is far more work than copying a few bytes.

Digits Held as Text

A string made of digit characters is still text.

#include <iostream>
#include <string>

int main()
{
    std::string accessionCode{ "0084" }; // text that happens to be digits
    int visitorCount{ 84 };              // an actual number

    std::cout << "accession " << accessionCode << '\n';
    std::cout << "visitors " << visitorCount << '\n';
    std::cout << "double the visitors " << visitorCount * 2 << '\n';

    return 0;
}

Output:

accession 0084
visitors 84
double the visitors 168

Notice that the leading zero survives in accessionCode. An int would have thrown it away, because 0084 and 84 are the same number, while "0084" and "84" are different text. The trade is that you cannot do arithmetic with accessionCode: multiplying it by two is not a valid operation, and C++ will not quietly convert between text and numbers in either direction. Functions that perform that conversion deliberately are covered in a later lesson.

Asking the String About Itself

To find out how many characters a std::string holds, you ask the object itself rather than handing it to a standalone function:

#include <iostream>
#include <string>

int main()
{
    constexpr int placardWidth{ 40 };
    std::string exhibitTitle{ "Songbirds of the Estuary" };

    int titleWidth{ static_cast<int>(exhibitTitle.length()) };

    std::cout << "spare room on the placard: " << placardWidth - titleWidth << '\n';

    return 0;
}

Output:

spare room on the placard: 16

Two details in that program are worth slowing down for.

The first is the call syntax. It is exhibitTitle.length(), not length(exhibitTitle). length() is not a standalone function, it is a function declared inside std::string itself, which makes it a member function. Documentation writes it as std::string::length() for that reason. size() is a second name for the same member function and returns exactly the same value, so use whichever reads better to you.

Key Concept
A normal function is called as `function(object)`. A member function is called as `object.function()`. Writing your own member functions comes later; for now, the dot is the signal that the function belongs to the object on its left.

The second detail is the static_cast. length() reports an unsigned integral value, most likely a size_t, because a count of characters can never be negative. placardWidth is a signed int. Subtracting one from the other without a cast mixes signed and unsigned values, which is exactly the kind of quiet conversion the compiler warns about, so the length is converted to int first and the arithmetic stays signed throughout.

One more thing the count does not include: a std::string keeps a null terminator after its characters so that it can hand its text to older interfaces, but that terminator is never counted in the length. "Ferns" reports 5, not 6.

Building a Longer String

Strings join with +, and += appends to a string you already have. Individual characters come out with [], counting from zero.

#include <iostream>
#include <string>

int main()
{
    std::string exhibitTitle{ "Clay Seals" };
    std::string galleryLabel{ "East Wing" };
    std::string curatorNote{};

    std::string placard{ exhibitTitle + ", " + galleryLabel };
    placard += " (free entry)";

    std::cout << placard << '\n';
    std::cout << "starts with " << placard[0] << '\n';
    std::cout << "placard width " << placard.size() << '\n';
    std::cout << "note follows this colon:" << curatorNote << '\n';

    return 0;
}

Output:

Clay Seals, East Wing (free entry)
starts with C
placard width 34
note follows this colon:

placard grew from two shorter strings and then grew again, and at no point did the program state how long the result would be. The last line shows the empty string in action: curatorNote was never given any text, so it contributed nothing to the output and the line simply ends after the colon.

Reading Text the User Types

There are three ways to read text into a std::string, and picking the wrong one is the single most common beginner bug in this area.

What you want to read Use Where it stops
One word std::cin >> word at the first whitespace after the word
A whole line, with nothing read beforehand std::getline(std::cin, line) at the next newline
A whole line, after anything else was read std::getline(std::cin >> std::ws, line) at the next newline, after discarding leading whitespace

Start with the extraction operator, >>. It reads one whitespace-separated word and leaves everything after that word sitting in std::cin for the next read.

#include <iostream>
#include <string>

int main()
{
    std::string exhibitTitle{};
    std::string galleryLabel{};

    std::cin >> exhibitTitle;
    std::cin >> galleryLabel;

    std::cout << "exhibitTitle holds " << exhibitTitle << '\n';
    std::cout << "galleryLabel holds " << galleryLabel << '\n';

    return 0;
}

Input:

Clay Seals
East Wing

Output:

exhibitTitle holds Clay
galleryLabel holds Seals

Both variables were filled from the first typed line, and the second line was never looked at. The first extraction took Clay and stopped at the space, leaving Seals behind; the second extraction then took Seals from that leftover text instead of waiting for anything new.

std::getline() reads to the end of the line instead of to the first space. It takes two arguments: the stream to read from, and the string to fill. Read the first argument below as "std::cin, with any whitespace sitting at the front thrown away first"; the next section is entirely about why that part is there.

#include <iostream>
#include <string>

int main()
{
    std::string exhibitTitle{};
    std::string galleryLabel{};

    std::getline(std::cin >> std::ws, exhibitTitle);
    std::getline(std::cin >> std::ws, galleryLabel);

    std::cout << "exhibitTitle holds " << exhibitTitle << '\n';
    std::cout << "galleryLabel holds " << galleryLabel << '\n';

    return 0;
}

Input:

Clay Seals
East Wing

Output:

exhibitTitle holds Clay Seals
galleryLabel holds East Wing

Why std::ws Is In There

std::ws is an input manipulator: a value you feed to a stream to change how the next read behaves, in the same way that output manipulators change how values are printed. This one tells the stream to throw away any whitespace waiting at the front before extraction begins.

The reason that matters becomes visible the moment a number is read before a line. The next program is broken on purpose, and the missing std::ws is the whole of the bug:

#include <iostream>
#include <string>

int main()
{
    int roomCode{};
    std::cin >> roomCode;

    std::string exhibitTitle{};
    std::getline(std::cin, exhibitTitle); // no std::ws, and that is the bug

    std::cout << "room " << roomCode << " shows (" << exhibitTitle << ")\n";

    return 0;
}

Input:

12
Clay Seals

Output:

room 12 shows ()

The title came out empty, and the parentheses in the output are there to make that visible. Pressing Enter after 12 put a newline character into std::cin along with the digits. >> took the 12 and stopped, because a newline is whitespace, so the newline stayed in the stream. std::getline() then read from the very next character, found the newline immediately, and concluded that it had just read an empty line. It never waited for the user at all.

Adding std::ws discards that stranded newline before reading starts:

#include <iostream>
#include <string>

int main()
{
    int roomCode{};
    std::cin >> roomCode;

    std::string exhibitTitle{};
    std::getline(std::cin >> std::ws, exhibitTitle); // std::ws discards the waiting newline

    std::cout << "room " << roomCode << " shows (" << exhibitTitle << ")\n";

    return 0;
}

Input:

12
Clay Seals

Output:

room 12 shows (Clay Seals)
Best Practice
Write `std::getline(std::cin >> std::ws, text)` rather than `std::getline(std::cin, text)`. `std::ws` applies to one extraction only and is not remembered afterwards, so it belongs in every `std::getline()` call, not just the first one.
Key Concept
The two readers treat whitespace in opposite ways. `>>` skips whitespace at the front and stops at the first whitespace it meets afterwards. `std::getline()` keeps whitespace at the front and stops only at a newline, which is why leading whitespace has to be removed with `std::ws` when something else was read first.

What a Copy Costs

Initializing a std::string copies the text it is initialized from, and that copy usually means a memory allocation. Every extra copy repeats the expense, so it is worth knowing where copies come from. Function parameters are the usual source:

#include <iostream>
#include <string>

void printPlacard(std::string caption) // caption is a fresh copy of the argument
{
    std::cout << "placard: " << caption << '\n';
}

int main()
{
    std::string exhibitTitle{ "Clay Seals" }; // copy one, made from the literal
    printPlacard(exhibitTitle);               // copy two, made for the parameter

    return 0;
}

Output:

placard: Clay Seals

Ten characters were copied twice to print them once. A parameter passed by value has to be created and initialized from the argument, and for a std::string that means duplicating the whole text.

Important
Do not give a function a `std::string` parameter passed by value. The copy is expensive, and it is entirely avoidable.
Parameter form What the call does When to reach for it
std::string caption Copies the whole text Almost never
std::string_view caption Copies nothing, and also accepts string literals directly Read-only text, and the subject of the next lesson
const std::string& caption Copies nothing, but the caller must already hold a std::string When the function genuinely needs the object itself, covered once references are introduced

Returning is the reverse situation and is far less alarming than it looks. Returning a local std::string by value is fine: the compiler either builds the string directly in the caller's storage or hands over the existing characters instead of duplicating them. What to avoid is returning a copy of text the caller already holds, which is work for nothing.

Literals That Are Already std::string

A double-quoted literal is a C-style string by default. Adding a lowercase s suffix makes it a std::string instead. That suffix is declared inside std::literals::string_literals, so the namespace has to be brought into scope before the suffix can be used:

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals; // brings in the s suffix

    std::cout << "Clay Seals\n";  // no suffix, so a C-style string literal
    std::cout << "East Wing\n"s;  // the s suffix makes this a std::string

    return 0;
}

Output:

Clay Seals
East Wing

using namespace std::literals would work as well, but it drags in every literal suffix the standard library defines. using namespace std::string_literals brings in only the string ones. Importing a whole namespace is normally a habit to avoid, and this is one of the rare exceptions, because these suffixes are very unlikely to collide with anything you write. Keep the using-directive inside a function rather than at the top of a header.

You will not need the suffix often, since initializing a std::string from a plain literal works perfectly well. It earns its keep in situations where the type has to be deduced rather than written out, which is a topic for a later chapter.

Text That Exists at Compile Time

constexpr and std::string almost never combine. This program does not compile:

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals;

    constexpr std::string exhibitTitle{ "Clay Seals"s }; // will not compile

    std::cout << exhibitTitle << '\n';

    return 0;
}

The compiler rejects the initializer with a message ending in is not a constant expression. The underlying problem is that a std::string normally manages memory, and memory obtained during constant evaluation cannot survive into the running program. constexpr std::string was not supported at all before C++20 and works only in narrow cases now.

Important
When you need text that exists at compile time, reach for `std::string_view` instead. The next lesson introduces it.

Summary

What it is: std::string, from the <string> header, holds text and manages its own storage. Assign a longer or shorter value at any time and the object adjusts, because it can request more memory at runtime through dynamic memory allocation. That flexibility is also why it is slower than a fixed buffer.

Numbers versus text: "0084" is four characters, not the number 84. C++ converts between text and numeric types only when you ask it to.

Inspecting: exhibitTitle.length() and exhibitTitle.size() both report the character count, excluding the null terminator. They are member functions, so they are called as object.function() rather than function(object). The result is unsigned, so static_cast<int> it before mixing it with signed arithmetic.

Combining: + joins strings, += appends to one, and [] reads a single character by position starting at zero.

Reading input: std::cin >> text stops at the first whitespace, so it reads one word. std::getline(std::cin, text) reads to the end of the line but takes any newline left behind by an earlier >> as an immediately empty line. std::getline(std::cin >> std::ws, text) discards leading whitespace first and is the form to write by default, once per call.

Copies: initializing a std::string copies its text, and a by-value parameter copies it again. Do not pass std::string by value; prefer std::string_view for read-only parameters. Returning a local std::string by value is fine.

Literals: the lowercase s suffix, enabled by using namespace std::string_literals, makes a literal a std::string rather than a C-style string.

Compile-time text: constexpr std::string is rejected in all but narrow cases. Use std::string_view, which the next lesson covers, for text that must exist at compile time.