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

💡 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?  0

2. 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:  3

3. 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 fox

4. 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.98

5. 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
//    abc

Your 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

Common Errors (and the fix)

📋 Quick Reference

OperationDescriptionExample
s.size() / s.length()number of characters"abc".size() → 3
s.empty()true if length is 0"".empty() → true
a + b, s += bconcatenate / append"Hi" + " there"
s[i]char at index (no check)fast; UB if out of range
s.at(i)bounds-checked accessthrows 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 substringreturns string::npos if absent
s.replace(pos, len, str)swap part of the stringin 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 < bcompare lexicographically"abc" < "abd" → true
to_string(n)number → stringto_string(42) → "42"
stoi(s) / stod(s)string → int / doublethrows on non-numeric text
s.c_str()C-style null-terminated textfor 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

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

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.

Related lessons