The _std::string_ lesson covered how to build a std::string, concatenate it, compare it, index into it, and ask how long it is. That is enough to hold text, but real programs do more: pull a product code out of a longer label, find where a colon sits in a parsed line, replace a placeholder in a greeting, or turn "9.99" into the number 9.99. This lesson is a working tour of the std::string methods that handle those jobs, grouped by purpose rather than by alphabetical order.
std::string ships with dozens of member functions. Most fall into a small number of jobs:
| Job | Methods | Returns |
|---|---|---|
| Find something | find, rfind, find_first_of, find_last_of | Index, or std::string::npos if not found |
| Pull out a piece | substr | A new std::string |
| Change the contents | replace, insert, erase, append, push_back, pop_back | Reference to the modified string |
| Compare | compare | Negative, zero, or positive int |
| Convert numbers | std::to_string, std::stoi, std::stod (free functions) | Either a string or a number |
| Read each character | range-based for loop | n/a (just iteration) |
The table is a reference, not required reading. Skim the sections below, and the pieces will fall into place by the worked example at the end.
For every example in this lesson, assume the following two headers are at the top of the file:
That is all std::string needs. The compiler line is g++ -std=c++17 file.cpp -o file.
find, rfind, and FriendsThe most common question about a string is "where does this substring start?". find answers it. Pass the substring to search for, and find returns the index where it begins, counting from zero.
The return type is std::size_t, which is an unsigned integer type sized to hold any index into a string. It also appears as size_t; both names refer to the same thing.
What if the substring is not there? find returns a special sentinel value called std::string::npos. It is a std::size_t constant defined as "the largest possible value of std::size_t", which in practice cannot be a real index. Compare against it to check whether the search hit anything.
A common bug: treating the return value as a signed int and checking for -1. That is how some other languages report "not found", but in C++ the sentinel is std::string::npos, and storing it in a signed int truncates the bits in a way that does not guarantee -1. Compare against std::string::npos directly.
find runs in O(n*m) in the worst case, where n is the haystack length and m is the needle length. For short needles this is effectively linear in the haystack. It is fine for typical product labels; it is not the right tool for scanning a megabyte log for a fixed pattern many times.
The search can start at a specific index by passing a second argument. That is useful for finding the second, third, or later occurrence.
Passing first + 1 as the starting position skips past the colon we already found, so the next call locates the next one.
rfind is find's mirror image. It searches from the right and returns the index of the last match.
rfind still returns the index from the start of the string, not from the end. The "r" only changes which match is returned, not how the index is counted.
For finding any one of a set of characters (rather than a specific substring), use find_first_of and find_last_of. Pass a string of characters and the function returns the index of the first (or last) position that matches any of them.
The string "-/" here is a set, not a literal substring. find_first_of returns the index of whichever character from the set appears earliest in the haystack. It is a useful method when the parser does not know which separator it will hit first.
substrOnce a piece of the string is located, the next step is usually to pull it out. substr does that. It returns a new std::string containing a range of characters from the original.
There are two forms:
s.substr(pos) returns everything from index pos to the end.s.substr(pos, len) returns at most len characters starting at pos.The "at most" part of the second form matters. If len runs past the end of the string, substr stops at the end instead of crashing or reading out of bounds. That makes it forgiving for an approximate length.
The first call grabs everything from index 8 onward. The second grabs five characters starting at 8, which is the word Apple. The third pulls the first seven characters. The fourth asks for a thousand characters, but substr stops at the end of the string instead of erroring.
One detail: passing a pos larger than s.size() throws std::out_of_range. Equal to s.size() is fine and returns an empty string. Anything past that is an error.
substr always returns a new string. The original is untouched. This matters when extracting several pieces without modifying the source.
Two slices, both safe, original string unchanged. This is the common pattern for parsing simple formatted text.
substr copies the chosen range into a new string, so it is O(len). For read-only inspection without storage, std::string_view (covered in the _std::string_view_ lesson) avoids the copy entirely.
replace, insert, erase, and Character-Level OpsThe methods so far have either read the string or built a new one. The next group changes the original. Each one modifies the string in place and returns a reference to the modified string, which allows chaining.
replace(pos, n, s)replace overwrites a range of characters with a new substring. Pass it the starting position, how many characters to replace, and the replacement string. The replacement does not have to be the same length as the range it overwrites; the string resizes as needed.
replace(7, 4, "Alice") says: starting at index 7, drop the next 4 characters (which spell NAME), and put in Alice instead. The replacement is one character longer than what it replaced, so the string grew by one character.
A visual of a smaller example. Replacing 3 characters starting at index 2 with the string "XYZ":
The original 7-character string becomes another 7-character string in this case because "XYZ" is the same length as the slice it replaces. With a longer or shorter replacement, the final length changes accordingly.
insert(pos, s)insert slips a substring into the middle of the string, pushing later characters to the right. Pass the position where the new text should start.
insert(0, "Wireless ") puts the prefix at the very start. Everything that was already there shifts right by 9 characters.
insert at the front, like the example above, is O(n) because every existing character has to move. Appending to the end with append or push_back is amortised O(1). Code that repeatedly inserts at the start is usually better off building in reverse and reversing once at the end, or appending in the natural order.
erase(pos, n)erase is the opposite of insert: it removes a range of characters and pulls the rest of the string left to fill the gap.
erase(2, 1) removes one character at index 2, which was the hyphen. The rest of the string slides left to close the gap.
With no second argument, erase(pos) removes everything from pos to the end:
This is a quick way to truncate a string at a known position.
append(s) vs +=append adds characters to the end of the string. It does the same job as += and is almost always interchangeable with it.
There is no meaningful performance difference between the two. Use whichever reads more naturally at the call site. The one minor edge append has is that it offers overloads taking a count and a position into another string, which += does not.
push_back(ch) and pop_back()These two are character-level helpers. push_back adds a single character to the end. pop_back removes the last character.
push_back takes a single char (note the single quotes), not a string. pop_back takes no arguments because there is only ever one "last" character to remove. Calling pop_back on an empty string is undefined behavior; check empty() first when the string might be empty.
clear() and empty()The _std::string_ lesson already covered these. A quick recap: clear() resets the string to zero length. empty() returns true when the size is zero. Both are O(1).
std::boolalpha is the formatting flag that prints true/false instead of 1/0. A small detail that makes boolean output more readable.
compareThe _std::string_ lesson covered == and != for full-string equality. compare is a more powerful tool. It returns an int that indicates not just "equal or not" but also "smaller or larger", based on lexicographic order.
The return value follows this rule:
The exact magnitude is not guaranteed; only the sign is. Some implementations return -1 or 1, others return the difference between the first mismatching characters. Treat the result as "negative, zero, or positive", not as "always -1, 0, or 1".
The reason to use compare over == is when a three-way result is needed in one call, typically inside a custom sort comparator or a tree-style lookup. For an everyday "are these the same?" check, == is shorter and more obvious.
compare also has overloads that take a position and length, so a slice of the string can be compared without calling substr:
Here compare(0, 3, "PRD") compares the first three characters of code with the literal "PRD". It returns zero when they match. This is cheaper than slicing with substr first, because no temporary string is created.
On C++20 or later, the language also provides starts_with and ends_with, which are usually clearer when prefix or suffix matching is all that is needed. This chapter sticks to C++17, where compare(0, 3, "PRD") == 0 is the idiomatic way to ask "does it start with PRD?".
to_string, stoi, stodPrograms often have to move between numbers and the text that represents them. Printing a price as "$9.99", parsing a quantity from user input, building a summary line that mixes labels and totals: all of those need conversion in one direction or the other.
C++ provides free functions (not member functions of std::string, but in the same <string> header) for both directions.
std::to_string takes any built-in numeric type and returns a std::string with the value written out as text:
Note the trailing zeros on 9.990000. std::to_string for double uses a default precision that is not always what is wanted for display. For tighter formatting, use std::ostringstream with std::fixed and std::setprecision, covered in the stream I/O section later. For now, std::to_string is the simplest tool, with bulky default formatting for floats.
Going the other direction, std::stoi (string to int) parses a string into an int:
std::stod does the same for double:
Sibling functions exist for other integer widths: std::stol for long, std::stoul for unsigned long, std::stoll for long long, and std::stof for float. They all work the same way, with different return types.
When the string cannot be parsed as a number, these functions throw std::invalid_argument. When the parsed value would overflow the target type, they throw std::out_of_range. So std::stoi("hello") throws, not returns zero. Wrap calls in a try/catch block when the input might be untrusted.
A quick reference table:
| Function | Input | Output |
|---|---|---|
std::to_string(n) | int, long, double, etc. | std::string |
std::stoi(s) | std::string | int |
std::stol(s) | std::string | long |
std::stoul(s) | std::string | unsigned long |
std::stoll(s) | std::string | long long |
std::stof(s) | std::string | float |
std::stod(s) | std::string | double |
A tiny program pulling both directions together. It takes a raw price string, increases it by a tax rate, and builds a formatted display line:
Again the formatting is bulky because of the default to_string precision. The larger point is that strings and numbers can flow into and out of each other with these two function families.
A std::string is a sequence of characters, and they can be walked with a range-based for loop, the same construct seen for vectors and arrays. Each element is a single char.
The loop variable c is a char (a single character), not a std::string. The check c >= '0' && c <= '9' works because characters compare by their numeric codes, and the digit characters '0' through '9' are contiguous in those codes.
To modify each character in place, use char& (a reference) instead of char:
The reference lets the loop write back into the string. With a plain char c, the modification happens on a copy, and the original stays lowercase. (<cctype> has std::toupper and std::tolower for this job in real code; the manual arithmetic above is a teaching example, not the idiomatic version.)
Range-based for is the most direct way to read or transform every character. When the index is also needed, use a plain for loop with i < s.size().
Time to combine several methods. The label format is fixed: Product:<name>:Price:<price>. The job is to pull out the name and the price separately, parse the price as a double, and print a clean summary line.
The plan: find the colons, then use substr to grab the pieces between them.
Walking the logic:
Product and Apple.Apple and Price.Price and 9.99.colon2 + 1 (right after the second colon), and the length is colon3 - colon2 - 1 (the gap between the two colons, minus one for the colon itself).substr(colon3 + 1) with no length argument gives us the rest of the string.std::stod turns the text "9.99" into the number 9.99.std::to_string turns the number back into text for the summary line.This pattern, find-the-separator then substr-between-separators, is the common shape for simple parsing. For more complex formats (CSV with quoted fields, JSON, free-form user input), use purpose-built libraries or std::stringstream, both later in the course. For fixed-format strings like this one, the methods in this chapter are enough.
The same parse with a small twist: count how many colons are in the string before parsing, as a sanity check.
The range-based for loop counts colons. If the count is anything other than three, the function bails out. That kind of guard is cheap insurance against bad input, and it shows the range-based for pattern from the previous section being useful for real work.
10 quizzes