20 topics
/
← Back to Quick Reference
Topic 12

Strings

SSO · Searching · Modifying · Conversions · string_view · std::format

C++17 · Advanced Reference

std::string Internals

01

How std::string works

std::string owns a heap-allocated buffer of characters. Most implementations use Small String Optimization (SSO) — strings of 15 characters or fewer are stored directly inside the string object (on the stack), avoiding any heap allocation. Longer strings spill to the heap.

Key properties

  1. 1.Always null-terminated internally — c_str() is always safe to call for C APIs.
  2. 2.SSO threshold is typically 15 chars (GCC/Clang/MSVC) — short strings are essentially free.
  3. 3.Mutable — unlike Java/Python strings. Modify in-place with +=, replace, erase.
  4. 4.Copying is O(n) — prefer passing as const string& or string_view.

Choosing the right type

  1. 1.std::string — own and modify a string. Pass by const-ref to avoid copies.
  2. 2.std::string_view — read-only, non-owning view. Zero-cost parameter type for read-only access.
  3. 3.const char* — C API interop only. Use s.c_str() to get one from a string.
  4. 4.char[] — avoid in new code. Use string or string_view instead.

Prefer string_view over const string& for function parameters — it accepts literals and substrings without allocating.

Construction, Concatenation & Access

02
#include <string>
using std::string;

// Construction
string a = "hello";           // from string literal
string b("hello", 3);         // "hel" — first 3 chars
string c(5, 'x');             // "xxxxx" — 5 copies of 'x'
string d = a;                 // copy
string e = std::move(a);      // move — a is now empty

// Concatenation
string s = "Hello" + string(", ") + "World";  // + needs at least one string
string t = s + '!';           // char appended
s += " suffix";               // in-place append (more efficient)
s.append(" more");            // same as +=
s.push_back('.');             // append single char

// Comparison — lexicographic, case-sensitive
s == t    s != t    s < t    s > t    s <= t    s >= t
s.compare(t)   // <0, 0, >0 — like strcmp

// Accessing characters
s[0]            // 'H' — unchecked
s.at(0)         // 'H' — throws std::out_of_range
s.front()       // first char
s.back()        // last char
s.data()        // const char* (C++17: also writable)
s.c_str()       // null-terminated const char* for C APIs
string(n, c)Repeat character c exactly n times. Useful for padding and separators.
s += tAppends in-place — more efficient than s = s + t which creates a temporary.
s.c_str()Returns a null-terminated const char*. Valid until the string is modified or destroyed.
s.data()C++17: returns a writable char*. Pre-C++17: same as c_str() (const).

Searching

03
string s = "Hello, World!";

// ── Finding ──────────────────────────────────────────────────
s.find("World")         // 7  — index of first match
s.find("xyz")           // string::npos — not found
s.find('o')             // 4  — first 'o'
s.rfind('o')            // 8  — last 'o'
s.find("o", 5)          // 8  — search starting at index 5

// Always check for npos
auto pos = s.find("World");
if (pos != string::npos) {
  std::cout << "found at " << pos << "\n";
}

// ── Contains / starts_with / ends_with (C++20) ───────────────
s.contains("World")     // true
s.starts_with("Hello")  // true
s.ends_with("!")         // true

// ── Pre-C++20 equivalents ────────────────────────────────────
s.find("World") != string::npos          // contains
s.rfind("Hello", 0) == 0                 // starts_with
s.size() >= 1 && s.back() == '!'        // ends_with
find(str, pos)Returns index of first match at or after pos. Returns string::npos if not found — always check!
rfind(str)Searches from the end — returns index of last match.
string::nposA large sentinel value (size_t max). Comparing an index to npos is the standard not-found check.
contains (C++20)Returns bool — cleaner than find() != npos for simple existence checks.
Always check for string::npos. Passing find()'s return directly as an index without checking will cause undefined behavior when the substring isn't found.

Modifying Strings

04
string s = "Hello, World!";

// ── Substrings ───────────────────────────────────────────────
s.substr(7)        // "World!" — from index 7 to end
s.substr(7, 5)     // "World" — 5 chars starting at index 7

// ── Replace ──────────────────────────────────────────────────
s.replace(7, 5, "C++");   // "Hello, C++!" — replace 5 chars at index 7
s.replace(s.find("C++"), 3, "everyone");

// ── Insert / erase ───────────────────────────────────────────
s.insert(5, "!!!");       // insert "!!!" at index 5
s.erase(5, 3);            // erase 3 chars starting at index 5
s.erase(s.begin() + 5);   // erase single char via iterator

// ── Case conversion (no stdlib — use cctype) ─────────────────
#include <cctype>
#include <algorithm>
string lower = s;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
std::transform(lower.begin(), lower.end(), lower.begin(), ::toupper);

// ── Trim whitespace (no stdlib function — manual) ────────────
auto start = s.find_first_not_of(" \t\n\r");
auto end   = s.find_last_not_of(" \t\n\r");
string trimmed = (start == string::npos) ? "" : s.substr(start, end-start+1);
substr(pos, n)Returns a NEW string — always allocates. Use string_view::substr for a zero-copy slice.
replace(pos, n, s)Replaces n chars at pos with s. s can be longer or shorter.
tolower/toupperFrom <cctype> — operates on single chars. Use std::transform to apply to the whole string.
find_first_not_ofFind first character NOT in the given set — use for trimming whitespace.

String ↔ Number Conversions

05
// ── String → number ──────────────────────────────────────────
int    i  = std::stoi("42");          // string to int
long   l  = std::stol("1234567890");
double d  = std::stod("3.14");
float  f  = std::stof("2.71f");

// stoi throws std::invalid_argument on bad input
// stoi throws std::out_of_range if value doesn't fit
try {
  int n = std::stoi("abc");   // throws invalid_argument
} catch (const std::exception& e) {
  std::cerr << e.what() << "\n";
}

// stoi with position — stops at first non-digit
std::size_t pos;
int n = std::stoi("42px", &pos);   // n=42, pos=2 (chars consumed)

// ── Number → string ──────────────────────────────────────────
string s1 = std::to_string(42);        // "42"
string s2 = std::to_string(3.14159);   // "3.141590" (6 decimal places)

// Better formatting via ostringstream or std::format (C++20)
#include <sstream>
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << 3.14159;
string s3 = oss.str();   // "3.14"
stoi / stol / stollString to int/long/long long. Throws on invalid input or overflow.
stof / stod / stoldString to float/double/long double.
to_string(n)Number to string. For floats: always 6 decimal places — use ostringstream for control.
stoi pos argumentOptional pointer to size_t — set to number of characters consumed. Use to detect trailing non-numeric chars.

std::string_view (C++17)

06
#include <string_view>

// string_view — a non-owning read-only view into a string
// No heap allocation, no copy — just a pointer + length

void print(std::string_view sv) {   // accepts string, char*, literal
  std::cout << sv.size() << ": " << sv << "\n";
}

print("hello");              // ✅ string literal — no copy
print(myString);             // ✅ std::string — no copy
print(myString.substr(2,3)); // ⚠ substr returns std::string (copy!)
print(std::string_view(myString).substr(2,3)); // ✅ no copy

// string_view supports most read-only string operations
std::string_view sv = "Hello, World!";
sv.substr(7, 5)    // "World" — still a view, no allocation
sv.find("World")   // 7
sv.starts_with("Hello")  // true (C++20)
sv[0]              // 'H'

// ⚠ Lifetime trap — string_view must not outlive its source
std::string_view bad() {
  std::string s = "hello";
  return s;   // ❌ s destroyed on return — dangling view!
}
non-owningJust a pointer + length. No allocation, no copy. Read-only.
accepts allA string_view parameter accepts std::string, const char*, and string literals without conversion.
lifetimeThe view is only valid while the underlying string exists. Never store a view to a temporary.
no c_str()string_view is NOT null-terminated. Cannot pass directly to C APIs — convert to string first.
Use string_view for all read-only string parameters. It's strictly better than const string& — it accepts literals without heap allocation and substrings without copying.

Split · Join · Format · Raw Literals

07
// ── Split by delimiter (no stdlib function pre-C++23) ────────
#include <sstream>
std::vector<std::string> split(const std::string& s, char delim) {
  std::vector<std::string> parts;
  std::istringstream ss(s);
  std::string token;
  while (std::getline(ss, token, delim)) {
    parts.push_back(token);
  }
  return parts;
}
auto words = split("one,two,three", ',');  // {"one","two","three"}

// ── Join (no stdlib) ─────────────────────────────────────────
std::string join(const std::vector<std::string>& v, std::string_view sep) {
  std::string result;
  for (std::size_t i = 0; i < v.size(); i++) {
    if (i) result += sep;
    result += v[i];
  }
  return result;
}

// ── C++20: std::format ───────────────────────────────────────
#include <format>
std::string s = std::format("{:>10} | {:.2f}", "pi", 3.14159);
// "        pi | 3.14"

// ── Raw string literals ──────────────────────────────────────
std::string path = R"(C:\Users\Name\file.txt)";   // no escaping
std::string json = R"({"key": "value\n"})";
istringstreamUse getline with a delimiter to split a string into tokens.
std::format C++20Python-style {} placeholders with format specs. Type-safe. Prefer over printf and ostringstream.
R"(...)"Raw string literal — backslashes are literal. Use for regex patterns, Windows paths, JSON.