C++ Strings
A string is a sequence of characters used to represent text — names, sentences, file paths, anything made of letters, digits, and symbols. In C++ you almost always work with strings through the std::string class from the <string> header, which manages a resizable buffer of characters for you. This lesson covers how std::string works internally, how to create and manipulate strings, and the classic mistakes that trip up beginners moving from other languages or from raw C-style character arrays.
Overview / How Strings Work
C++ actually has two ways to represent text, and understanding the difference matters. A C-style string is a plain array of char values terminated by a special null byte ('\0') that marks the end. You manipulate it with pointer arithmetic and functions like strlen and strcpy from <cstring>. It is fast but dangerous: there is no bounds checking, no automatic memory management, and it is easy to write past the end of the buffer.
std::string, part of the C++ Standard Library, wraps that raw buffer in a safe, resizable object. Internally, a std::string stores a pointer to a heap-allocated character buffer, a length (how many characters are actually stored), and a capacity (how much space is currently allocated). When you append characters and the buffer runs out of room, the string allocates a larger buffer — typically by doubling capacity — copies the old characters over, and frees the old buffer. This happens automatically; you never call malloc or delete yourself.
Most modern implementations (including the one shipped with g++, libstdc++) also apply the Small String Optimization (SSO): short strings (commonly up to 15 characters on a 64-bit system) are stored directly inside the std::string object itself, with no heap allocation at all. This makes short strings extremely fast to create and copy. Once a string grows past that internal buffer, the implementation transparently switches to heap allocation. You don’t need to do anything special to benefit from this — it’s an implementation detail, but it explains why short strings are so cheap in C++.
Because std::string manages its own memory, it supports value semantics: copying a string copies its contents (or, since C++11, a move can transfer ownership without copying at all when the source is a temporary). This makes strings safe to pass around, store in containers like std::vector<std::string>, and return from functions, without the memory-management headaches of raw C-strings.
Syntax
Here is how you declare, initialize, and read strings:
#include <string>
using namespace std;
string s1; // empty string
string s2 = "Hello"; // initialize from a literal
string s3("World"); // constructor syntax
string s4(5, 'x'); // "xxxxx" - 5 copies of 'x'
string s5 = s2 + " " + s3; // concatenation
cin >> s1; // reads one whitespace-delimited word
getline(cin, s1); // reads an entire line, including spaces
Key members you will use constantly:
| Expression | Meaning |
|---|---|
s.length() / s.size() |
Number of characters currently stored (identical, both provided for convenience). |
s.empty() |
True if the string has zero characters. |
s[i] |
Access character at index i. No bounds checking (undefined behavior if out of range). |
s.at(i) |
Same as [] but throws std::out_of_range if i is invalid. |
s.substr(pos, len) |
Returns a new string copying len characters starting at pos. |
s.find(text) |
Returns the index of the first match, or string::npos if not found. |
s.replace(pos, len, text) |
Replaces len characters starting at pos with text. |
s.insert(pos, text) |
Inserts text before index pos. |
s.erase(pos, len) |
Removes len characters starting at pos. |
s.c_str() |
Returns a read-only, null-terminated C-string view of the data (for APIs that need const char*). |
string::npos |
A special constant meaning “no position” / “until the end” — used to detect failed searches. |
Examples
Example 1: Building and inspecting a string
#include <iostream>
#include <string>
using namespace std;
int main() {
string first = "Ada";
string last = "Lovelace";
string full = first + " " + last;
cout << "Full name: " << full << endl;
cout << "Length: " << full.length() << endl;
cout << "First character: " << full[0] << endl;
cout << "Last character: " << full.back() << endl;
full += "!";
cout << "With exclamation: " << full << endl;
return 0;
}
Output:
Full name: Ada Lovelace
Length: 12
First character: A
Last character: e
With exclamation: Ada Lovelace!
The + operator concatenates strings (and can mix in literals), length() counts characters, [0] grabs the first character, and back() returns the last one. += appends in place, growing the internal buffer if needed.
Example 2: Searching, extracting, and editing
#include <iostream>
#include <string>
using namespace std;
int main() {
string sentence = "The quick brown fox jumps";
size_t pos = sentence.find("brown");
if (pos != string::npos) {
cout << "'brown' found at index " << pos << endl;
}
string word = sentence.substr(pos, 5);
cout << "Extracted word: " << word << endl;
sentence.replace(pos, 5, "red");
cout << "After replace: " << sentence << endl;
sentence.insert(0, "Look! ");
cout << "After insert: " << sentence << endl;
sentence.erase(0, 6);
cout << "After erase: " << sentence << endl;
return 0;
}
Output:
'brown' found at index 10
Extracted word: brown
After replace: The quick red fox jumps
After insert: Look! The quick red fox jumps
After erase: The quick red fox jumps
find locates the substring’s starting index; always compare the result against string::npos before using it, since a failed search returns that special value rather than -1. substr, replace, insert, and erase all operate on positions and lengths, and each returns or mutates the string accordingly.
Example 3: Parsing structured text
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
using namespace std;
int main() {
string csvLine = "Alice,29,Engineer";
stringstream ss(csvLine);
string token;
vector<string> fields;
while (getline(ss, token, ',')) {
fields.push_back(token);
}
cout << "Name: " << fields[0] << endl;
cout << "Age: " << fields[1] << endl;
cout << "Job: " << fields[2] << endl;
string reversed(fields[0].rbegin(), fields[0].rend());
cout << "Reversed name: " << reversed << endl;
return 0;
}
Output:
Name: Alice
Age: 29
Job: Engineer
Reversed name: ecilA
A stringstream lets you treat a string as an input stream. The three-argument getline(stream, token, delimiter) overload splits on a custom character (here a comma) instead of a newline — a very common way to parse CSV-style data. The reversed-name line builds a new string from reverse iterators (rbegin()/rend()), a compact idiom for reversing without writing a loop.
Under the Hood
When you write s1 = s2, the string class allocates its own buffer and copies characters from s2 — the two strings are completely independent afterward. When you write s1 + s2, a brand-new string object is created large enough to hold both, and both operands are copied into it; chaining many + operations in a loop therefore creates and destroys many temporary strings, which is why repeated += (appending in place) is more efficient for building up long strings than repeated +.
Capacity growth is amortized: each time a string outgrows its buffer, the implementation typically allocates roughly double the previous capacity rather than exactly what’s needed right now. That means appending one character at a time is still efficient on average, because reallocations become rarer as the string grows. You can see (and control) this with s.capacity(), and pre-allocate space with s.reserve(n) if you know roughly how large a string will get, avoiding repeated reallocations.
Comparison operators (==, <, >, etc.) compare strings lexicographically — character by character, using each character’s underlying integer (ASCII/Unicode code point) value, the same way a dictionary orders words. This is different from comparing the strings as numbers, which is a frequent source of bugs (see Common Mistakes below).
Common Mistakes
Mistake 1: Assuming string comparison is numeric. Beginners often compare numeric-looking strings expecting numeric ordering, but std::string compares character by character.
#include <iostream>
#include <string>
using namespace std;
int main() {
string a = "10";
string b = "9";
if (a < b) {
cout << a << " is less than " << b << endl;
} else {
cout << a << " is not less than " << b << endl;
}
return 0;
}
Output:
10 is less than 9
This looks wrong at first glance, but it’s correct: comparing "10" and "9" compares '1' to '9' first, and '1' (ASCII 49) is less than '9' (ASCII 57), so "10" < "9" is true. If you actually want numeric comparison, convert first with stoi:
#include <iostream>
#include <string>
using namespace std;
int main() {
string a = "10";
string b = "9";
if (stoi(a) < stoi(b)) {
cout << a << " is less than " << b << endl;
} else {
cout << a << " is not less than " << b << endl;
}
return 0;
}
Output:
10 is not less than 9
Mistake 2: Using operator[] for out-of-range access. s[i] performs no bounds checking, so reading or writing past the end is undefined behavior — it might silently return garbage, corrupt memory, or crash unpredictably, and you can’t rely on any particular symptom to catch it. Use at() when the index isn’t guaranteed to be valid; it throws a catchable exception instead:
#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;
int main() {
string word = "Cat";
try {
cout << word.at(10) << endl;
} catch (const out_of_range& e) {
cout << "Caught exception: " << e.what() << endl;
}
return 0;
}
Output:
Caught exception: basic_string::at: __n (which is 10) >= this->size() (which is 3)
Reserve [] for cases where you have already verified the index is valid (for example, a loop bounded by s.length()), and prefer at() whenever the index comes from outside input or user-controlled data.
Best Practices
- Prefer
std::stringover raw C-style char arrays for almost all text handling — it manages memory safely and provides far richer operations. - Always compare the result of
find()againststring::nposbefore using it as an index; ignoring this is one of the most common bugs in C++ string code. - Use
getline(cin, s)instead ofcin >> swhenever the input may contain spaces (full names, sentences, file paths). - Use
reserve()before building up a very large string in a loop if you have a rough size estimate, to avoid repeated reallocations. - Use
at()instead of[]when the index isn’t provably in range (e.g., derived from user input). - Pass strings by
const string&in function parameters unless you need a modifiable copy, to avoid unnecessary copying. - Use
c_str()only when interfacing with an API that specifically requires aconst char*; otherwise stay instd::string.
Practice Exercises
Exercise 1: Write a program that reads a full sentence with getline and prints how many times the letter 'e' appears in it (case-insensitive).
Exercise 2: Given the string "2026-07-18", use substr and find (searching for '-') to print the year, month, and day separately, without hardcoding the positions of the dashes.
Exercise 3: Write a function-style snippet that takes a string and returns a new string with every word’s first letter capitalized (e.g., "hello world" becomes "Hello World"). Hint: walk the string and capitalize the character right after each space and at index 0.
Summary
std::stringfrom<string>is the safe, resizable way to work with text in C++, in contrast to raw null-terminated C-style char arrays.- Internally, strings use heap allocation with amortized doubling for growth, plus a Small String Optimization that avoids heap allocation entirely for short strings.
- Core operations include
length(),substr(),find(),replace(),insert(), anderase(); always checkfind()results againststring::npos. - Use
getline()for whitespace-containing input andstringstreamfor parsing delimited data. - String comparisons are lexicographic, not numeric — convert with
stoi/stodwhen you need numeric ordering. - Prefer
at()over[]when index validity isn’t guaranteed, since[]gives undefined behavior on out-of-range access.
