std::string for Text Handling
Store and manipulate text using the std::string class with built-in memory management.
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.
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.
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)
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.
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.
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.
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.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
std::string for Text Handling - Quiz
Test your understanding of the lesson.
Practice Exercises
Introduction to std::string
Practice using std::string for text manipulation.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!