Skip to content

06 · Strings (std::string)

🎥 Video walkthrough

std::string is C++'s standard, safe, resizable string type — prefer it over raw C-style char* strings for almost everything in application code.

Creating and printing strings

#include <iostream>
#include <string>

int main() {
    std::string name = "Ada Lovelace";
    std::cout << name << std::endl;          // Ada Lovelace
    std::cout << name.length() << std::endl; // 12 -- length() and size() are equivalent
}

#include <string> is required to use std::string<iostream> alone does not pull it in.

Concatenation

std::string first = "Grace";
std::string last = "Hopper";

std::string full = first + " " + last;        // + concatenates strings
std::cout << full << std::endl;                // Grace Hopper

full += "!";                                    // += appends in place
std::cout << full << std::endl;                 // Grace Hopper!

Converting between strings and numbers

int age = 30;
std::string ageStr = std::to_string(age);       // int -> std::string
std::cout << "Age: " + ageStr << std::endl;      // Age: 30

std::string input = "42";
int parsed = std::stoi(input);                   // std::string -> int
double parsedD = std::stod("3.14");              // std::string -> double

std::cout << parsed + 8 << std::endl;            // 50

std::stoi throws std::invalid_argument if the string doesn't start with a valid number — see Module 9 for handling that safely.

Comparing strings

std::string a = "apple";
std::string b = "banana";

std::cout << (a == b) << std::endl;   // 0 (false)
std::cout << (a < b) << std::endl;    // 1 (true) -- lexicographic ("dictionary") order
std::cout << (a != b) << std::endl;   // 1 (true)

std::string supports ==, !=, <, >, etc. directly — no special method needed, unlike some languages.

Accessing and iterating characters

std::string word = "hello";

std::cout << word[0] << std::endl;        // h
std::cout << word.at(1) << std::endl;     // e -- bounds-checked, like vector

for (char c : word) {
    std::cout << c << "-";
}
std::cout << std::endl;
// h-e-l-l-o-

A std::string behaves a lot like a std::vector<char> — the same operator[] vs .at() tradeoff from Module 5 applies here.

Substrings and searching

std::string sentence = "The quick brown fox";

std::string sub = sentence.substr(4, 5);       // starts at index 4, length 5
std::cout << sub << std::endl;                  // quick

size_t pos = sentence.find("brown");
if (pos != std::string::npos) {                // npos means "not found"
    std::cout << "Found at index " << pos << std::endl;   // Found at index 10
}

size_t missing = sentence.find("zebra");
std::cout << (missing == std::string::npos) << std::endl;  // 1 (true) -- not found

std::string::npos is a special constant meaning "no position" — always compare against it rather than assuming -1, since find returns an unsigned size_t.

Useful transformations

#include <algorithm>

std::string text = "Hello World";

std::string upper = text;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << upper << std::endl;   // HELLO WORLD

std::string padded = "  trim me  ";
size_t start = padded.find_first_not_of(' ');
size_t end = padded.find_last_not_of(' ');
std::string trimmed = padded.substr(start, end - start + 1);
std::cout << "[" << trimmed << "]" << std::endl;   // [trim me]

<algorithm> provides generic operations like std::transform, which are covered more thoroughly with the STL in Level 2 — the pattern above (apply a function to every character) is a common one worth recognizing early.

Building strings piece by piece with stringstream

#include <sstream>

std::ostringstream oss;
oss << "Total: " << 42 << " items, $" << 19.99;
std::string result = oss.str();
std::cout << result << std::endl;   // Total: 42 items, $19.99

std::ostringstream is handy when you need to build up a formatted string from mixed types (numbers, strings) without a lot of manual std::to_string and + calls.

How It Actually Works

std::string is not a primitive — it's a class that manages a heap-allocated buffer of characters, much like std::vector<char> internally, plus a null terminator it maintains automatically so .c_str() can hand raw C APIs a valid C-string. Most implementations also apply Small String Optimization (SSO): strings shorter than roughly 15-22 characters (implementation-dependent) are stored directly inside the std::string object's own stack/member memory, with no heap allocation at all. Only once a string grows past that threshold does it allocate on the heap — which is why short strings are essentially free to copy and construct, while long ones incur a real new[] call.

Concatenating with + on std::string allocates a brand-new buffer sized to hold both operands and copies both into it — repeated concatenation in a loop (result += s; many times) can trigger the same reallocate-and-copy growth pattern as std::vector, which is why +=/append on the same string object is cheaper than chaining + to build new temporaries repeatedly.

Raw C-style char* strings, by contrast, are just a pointer to the first byte of a sequence that keeps going until a '\0' byte is found — there's no length stored anywhere, so strlen has to scan byte-by-byte until it hits that terminator. This is the root cause of classic C string bugs: read or write past the terminator (or forget it entirely) and every string function either walks off into unrelated memory or corrupts it. std::string sidesteps this by tracking its length explicitly as a member field, so .size() is an O(1) lookup, not a scan.

🔀 See this in another language

Exercise

Write a function std::string reverseWords(const std::string& sentence) that takes a sentence and returns it with the order of words reversed (e.g. "The quick fox" becomes "fox quick The"). You can split on spaces manually using find and substr in a loop, storing each word in a std::vector<std::string> before reassembling it in reverse.