Strings (std::string)
Reviewed & published by Brayan K
std::string is the safe, flexible way to handle text in C++. It grows on demand, manages its own memory, and comes with a rich toolkit for joining, searching, slicing, and transforming text. This lesson takes you from + concatenation to bounds-checked access, searching with find, and converting between strings and numbers.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Create, concatenate (+, +=), and measure strings with .size()
- Access characters with [] and the safe .at()
- Search with .find() and handle string::npos
- Slice with .substr() and edit with .replace()/.erase()
- Convert with to_string, stoi, and stod
- Transform case with <algorithm> and <cctype>
💡 Real-World Analogy
A std::string is like a magnetic letter board that resizes itself. You can snap letters on the end (+=), read the letter at any slot (s[3]), search for a word (find), or peel off a strip of letters (substr) — and the board automatically makes room when you add more. A raw char[] array, by contrast, is a fixed-size board: run out of slots and letters spill off the edge, corrupting whatever is next to it in memory. That spill is the infamous buffer overflow, and std::string exists to prevent it.
1. Creating & Joining Strings
Build text with + to concatenate and += to append in place. .size() (or its synonym .length()) gives the character count, .front()/.back() read the ends, and .empty() checks for an empty string.
#include <iostream>
#include <string>
using namespace std;
int main() {
string greeting = "Hello";
string name = "World";
// + concatenates; += appends in place.
string message = greeting + ", " + name + "!";
message += " Welcome.";
cout << message << '\n'; // Hello, World! Welcome.
cout << "length: " << message.size() << '\n'; // length: 23
cout << "first: " << message.front() << '\n'; // first: H
cout << "last: " << message.back() << '\n'; // last: .
cout << "empty? " << greeting.empty() << '\n';// empty? 0
return 0;
}
// ✅ Expected output:
// Hello, World! Welcome.
// length: 22
// first: H
// last: .
// empty? 02. Accessing Characters
Index individual characters with s[i] starting from 0. For untrusted indexes prefer s.at(i), which throws std::out_of_range instead of silently reading garbage. A range-based for visits every character in turn.
#include <iostream>
#include <string>
using namespace std;
int main() {
string word = "programming";
// Index characters from 0. Each element is a char.
cout << "char[0]: " << word[0] << '\n'; // p
cout << "char[3]: " << word[3] << '\n'; // g
// .at(i) is bounds-checked: it throws on a bad index.
cout << "at(2): " << word.at(2) << '\n'; // o
// Loop with a range-based for (each c is a copy of one char).
int vowels = 0;
for (char c : word)
if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') ++vowels;
cout << "vowels: " << vowels << '\n'; // vowels: 3
return 0;
}
// ✅ Expected output:
// char[0]: p
// char[3]: g
// at(2): o
// vowels: 33. Searching & Slicing
find returns the index of a substring, or the special value string::npos when it isn't there — always test against npos, never -1. substr(start, length) extracts a slice, and replace swaps out a section.
#include <iostream>
#include <string>
using namespace std;
int main() {
string text = "the quick brown fox";
// find returns the index, or string::npos if absent.
size_t pos = text.find("brown");
if (pos != string::npos)
cout << "found at " << pos << '\n'; // found at 10
if (text.find("cat") == string::npos)
cout << "'cat' not found\n"; // prints
// substr(start, length) extracts a piece.
cout << "word: " << text.substr(4, 5) << '\n'; // word: quick
// replace(start, len, new) swaps part of the string.
text.replace(0, 3, "THE");
cout << text << '\n'; // THE quick brown fox
return 0;
}
// ✅ Expected output:
// found at 10
// 'cat' not found
// word: quick
// THE quick brown fox4. Converting Between Strings & Numbers
to_string turns a number into text (handy for building messages), while stoi and stod parse text back into an int or double. Note that to_string(4.5) prints with trailing zeros — use <iomanip> formatting if you need control.
#include <iostream>
#include <string>
using namespace std;
int main() {
// Number -> string
int score = 95;
double rate = 4.5;
string report = "Score: " + to_string(score)
+ ", Rate: " + to_string(rate);
cout << report << '\n'; // Score: 95, Rate: 4.500000
// String -> number
string ageText = "30";
string priceText = "19.99";
int age = stoi(ageText); // 30
double price = stod(priceText);// 19.99
cout << "age+1=" << age + 1 << ", price*2=" << price * 2 << '\n';
// age+1=31, price*2=39.98
return 0;
}
// ✅ Expected output:
// Score: 95, Rate: 4.500000
// age+1=31, price*2=39.985. Transforming Text
Combine std::transform with toupper/tolower to change case, and the erase-remove idiom to strip characters. These standard-library tools are faster and clearer than hand-written loops once you recognize the pattern.
#include <iostream>
#include <string>
#include <algorithm> // transform
#include <cctype> // toupper, tolower
using namespace std;
int main() {
string s = "Hello World";
// Uppercase every character in place.
transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return toupper(c); });
cout << s << '\n'; // HELLO WORLD
// Count and erase spaces.
string t = "a b c";
t.erase(remove(t.begin(), t.end(), ' '), t.end());
cout << t << '\n'; // abc
return 0;
}
// ✅ Expected output:
// HELLO WORLD
// abcYour turn. Build a username and report its length:
#include <iostream>
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — build a username from a name.
string first = "Ada";
string last = "Lovelace";
// 1) Concatenate: first + "." + last
string username = ___; // 👉 "Ada.Lovelace"
// 2) Get its length
size_t len = ___; // 👉 username.size()
cout << username << " (" << len << " chars)\n";
// ✅ Expected: Ada.Lovelace (12 chars)
return 0;
}🧩 Reorder Challenge
These lines should find a word and print the text after it. Reorder them so the program prints fox.
A. string s = "brown fox";
B. size_t p = s.find("fox");
C. if (p != string::npos)
D. cout << s.substr(p) << '\n';
E. return 0;Why: first you need the string (A), then locate "fox" with find (B). You must check the result against string::npos (C) before using it, because substr with an out-of-range index would throw. Only then is it safe to slice from p to the end (D), giving fox. Return last (E).
🧠 Quick Recall
Predict the output before revealing each answer.
string s = "abcdef"; cout << s.substr(2, 3);
cde — substr(2, 3) starts at index 2 and takes 3 characters.
1 — "z" isn't in "cat", so find returns npos and the comparison is true (prints as 1).
46 — both strings are parsed to ints first, so it's numeric addition (12 + 34), not string concatenation.
Pro Tips
- 💡 Pass big strings by const&: void f(const string& s) avoids copying.
- 💡 Compare with npos, not -1: find returns an unsigned type.
- 💡 Use .at() for untrusted indexes: it throws instead of corrupting memory.
- 💡 "text"s with using namespace std::string_literals makes a std::string literal directly.
Common Errors (and the fix)
- Forgetting #include <string>: using string without it fails to compile.
- Out-of-range []: s[s.size()] is undefined behavior. Use .at() to catch it.
- Adding two string literals: "a" + "b" doesn't compile — at least one side must be a std::string. Use string("a") + "b".
- stoi on bad text: stoi("abc") throws std::invalid_argument. Validate or wrap in try/catch.
📋 Quick Reference
| Operation | Description | Example |
|---|---|---|
| s.size() / s.length() | number of characters | "abc".size() → 3 |
| s.empty() | true if length is 0 | "".empty() → true |
| a + b, s += b | concatenate / append | "Hi" + " there" |
| s[i] | char at index (no check) | fast; UB if out of range |
| s.at(i) | bounds-checked access | throws std::out_of_range |
| s.front() / s.back() | first / last character | "abc".back() → 'c' |
| s.substr(pos, len) | extract a slice | "abcdef".substr(2,3) → "cde" |
| s.find(sub) | index of substring | returns string::npos if absent |
| s.replace(pos, len, str) | swap part of the string | in place |
| s.insert(pos, str) | insert text at pos | "hello".insert(2,"XY") → "heXYllo" |
| s.erase(pos, len) | remove characters | "hello".erase(1,2) → "hlo" |
| a == b, a < b | compare lexicographically | "abc" < "abd" → true |
| to_string(n) | number → string | to_string(42) → "42" |
| stoi(s) / stod(s) | string → int / double | throws on non-numeric text |
| s.c_str() | C-style null-terminated text | for old C APIs |
Mini-Challenge: Initials Generator
Read a full name and print the initials, each followed by a period. Match the expected output in the comments.
#include <iostream>
#include <string>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: initials generator
// Given a full name read with getline (e.g. "Grace Brewster Hopper"),
// 1. Find the spaces and print the first letter of each word,
// each followed by a '.' -> "G.B.H."
// Hint: loop over the chars; print s[0], then any char right
// after a space, each followed by '.'.
//
// Input: Grace Brewster Hopper
// Expected output: G.B.H.
// your code here
return 0;
}🎉 Lesson Complete
- ✅ +/+= join strings; .size() measures them
- ✅ s[i] is fast, s.at(i) is bounds-checked
- ✅ find returns an index or string::npos
- ✅ substr slices; replace/erase edit
- ✅ to_string, stoi, stod convert between text and numbers
Practice quiz
Which operators join (concatenate) strings in C++?
- & and &=
- * and *=
- + and +=
- , and ;
Answer: + and +=. + concatenates two strings and += appends in place.
Which member function returns the number of characters in a string?
- s.size()
- s.count()
- s.chars()
- s.length() only
Answer: s.size(). s.size() (a synonym of s.length()) returns the character count.
What does s.substr(2, 3) return for the string 'abcdef'?
- 'abc'
- 'bcd'
- 'def'
- 'cde'
Answer: 'cde'. substr(2, 3) starts at index 2 and takes 3 characters: cde.
What does std::string::find return when the substring is NOT present?
- -1
- string::npos
- 0
- an empty string
Answer: string::npos. find returns string::npos when the substring is absent; compare against npos, not -1.
Which access is bounds-checked and throws std::out_of_range on a bad index?
- s.at(i)
- s[i]
- s.front()
- s.get(i)
Answer: s.at(i). s.at(i) checks bounds and throws; s[i] does not check for speed.
Which function converts an int to a std::string?
- stoi
- stod
- to_string
- atoi
Answer: to_string. to_string(42) turns a number into a string.
What does stoi('12') + stoi('34') evaluate to?
- '1234'
- 46
- '46'
- 1234
Answer: 46. Both strings are parsed to ints first, so it is numeric addition: 12 + 34 = 46.
Why must you compare find's result against string::npos rather than -1?
- npos is faster
- -1 is reserved
- find never returns -1 by accident
- find returns an unsigned type
Answer: find returns an unsigned type. find returns an unsigned size_t, so comparing to -1 is unreliable; use string::npos.
Why does 'a' + 'b' (two string literals with +) fail to compile?
- Literals are too short
- At least one side must be a std::string
- + is not defined for text
- You need three operands
Answer: At least one side must be a std::string. You cannot add two C-string literals; wrap one in std::string, e.g. string("a") + "b".
What does stoi('abc') do at runtime?
- Returns 0
- Returns -1
- Throws std::invalid_argument
- Returns the ASCII code
Answer: Throws std::invalid_argument. stoi on non-numeric text throws std::invalid_argument; validate or wrap in try/catch.
Continue this course
- Previous: Loops
- Next: Type Casting — static_cast and the C++ cast operators
- Quick reference: C++ cheat sheet › Strings & I/O
Frequently asked questions
What is the difference between std::string and a C-style char array?
A std::string manages its own memory, grows automatically, and offers dozens of safe member functions. A C-style array (char[]) is a fixed block you must size and null-terminate yourself, and it is a common source of buffer overruns. In modern C++ you should reach for std::string almost every time.
Why does s.find("xyz") sometimes return a huge number?
find returns std::string::npos when the substring is not found. npos is the largest possible size_t value, which prints as a very large number. Always compare against std::string::npos rather than checking for -1, because the return type is unsigned.
How do I convert a number to a string and back?
Use std::to_string(42) to turn a number into a string, and std::stoi("42"), std::stod("3.14") to parse a string into an int or double. The sto* functions throw std::invalid_argument if the text is not a valid number, so wrap them in a try/catch for user input.
Is it expensive to pass a string to a function?
Passing by value copies the whole string, which can be costly for long text. Pass by const reference (const std::string& s) when the function only reads the string, or take a std::string_view for read-only access without any copy at all.
Why does s[100] not crash even when the string is short?
operator[] does not check bounds for speed, so reading out of range is undefined behavior — it may print garbage or crash unpredictably. Use s.at(100) instead, which throws std::out_of_range so the error is caught immediately and visibly.