본문으로 건너뛰기 [2026] C++ string fundamentals — std::string, C strings, string_view, and practical patterns

[2026] C++ string fundamentals — std::string, C strings, string_view, and practical patterns

[2026] C++ string fundamentals — std::string, C strings, string_view, and practical patterns

이 글의 핵심

When you use C-style strings (char* or const char*) with legacy APIs, comparing with == compares pointer addresses—not the text. This post walks through concepts and examples step by step for learning and production use.

Introduction: crashes from comparing strings with ==

“I compared two C strings with == and it always says they’re equal”

When you work with C-style strings (char* or const char*) alongside legacy APIs, comparing with == compares pointer addresses. You only check whether they point to the same memory, not whether the contents match.

Common problem scenarios

Scenario 1: Misinterpreting strcmp’s return value

strcmp returns 0 if equal, a negative value if the first is less, and positive if greater—but if (strcmp(a, b)) treats “equal” as false. In C++, if (x) is true when x is non-zero, so when strcmp returns 0 (equal), the condition is false—the opposite of what people often want.

Scenario 2: Slow string concatenation

If you repeat result += str inside a for loop, you may reallocate on every iteration. Without reserve, joining thousands of small strings can take seconds.

Scenario 3: Modifying the source after substr

When you use std::string_view to refer to part of a string, if the backing storage is modified or destroyed you get a dangling view. string_view does not copy; it only “views” data, so you must use it only while the memory stays valid.

Scenario 4: Passing std::string to C APIs

Calling something like printf("%s", str) with a std::string is a compile error. C APIs expect const char*, so use str.c_str().

In plain terms: std::string is the standard C++ string type: size grows as needed and memory is managed automatically. string_view (C++17) is a lightweight non-owning view—it observes text without copying. Analogy: std::string is “a notebook I own”; string_view is “a bookmark pointing at a page in a notebook.”

Problematic code:

const char* a = "hello";
const char* b = "hello";
if (a == b) {  // ❌ Pointer comparison! Contents may match but addresses may differ
    std::cout << "Same\n";
}

Explanation: a and b may point to different addresses. The compiler may merge identical string literals (string pooling), but that is not guaranteed for strings built at runtime or in other translation units. For content comparison, use strcmp (or std::string).

Correct comparison:

#include <cstring>
// Variable declarations and initialization
const char* a = "hello";
const char* b = "hello";
if (strcmp(a, b) == 0) {  // ✅ Content comparison
    std::cout << "Same\n";
}

After reading this article you will be able to:

  • Understand the main operations on std::string.
  • Compare and convert between C strings and std::string correctly.
  • Use string_view to avoid unnecessary copies.
  • Recognize frequent mistakes and their fixes.
  • Apply solid string patterns in production.

Choosing a string type—summary:

flowchart TB
  subgraph choice[String type choice]
    A[When you need a string] --> B{Use case}
    B -->|Own / need to mutate| C["std::string"]
    B -->|Read-only / parameter| D["std::string_view"]
    B -->|C API interop| E["const char*"]
  end
  subgraph caution[Cautions]
    C --> F["Minimize reallocations with reserve"]
    D --> G["Check backing lifetime"]
    E --> H["Compare with strcmp"]
  end

Production note: This article is based on real issues and fixes from large C++ codebases. It includes practical pitfalls and debugging tips that textbooks often skip.

Table of contents

  1. Problem scenarios: real-world string issues
  2. std::string guide
  3. C-string comparison and conversion
  4. std::string_view (C++17)
  5. Complete string examples
  6. Common errors and fixes
  7. Performance tips
  8. Production patterns
  9. Implementation checklist

1. Problem scenarios: real-world string issues

Scenario 1: Log parsing treats "user" and "User" as the same

When you need case-insensitive comparison, == is case-sensitive: "user" == "User" is false.

Fix: Lowercase both sides with std::transform and compare, or use strcasecmp (POSIX).

// After pasting: g++ -std=c++17 -o case_compare case_compare.cpp && ./case_compare
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
bool equalsIgnoreCase(const std::string& a, const std::string& b) {
    if (a.size() != b.size()) return false;
    return std::equal(a.begin(), a.end(), b.begin(),
        [](char ca, char cb) {
            return std::tolower(static_cast<unsigned char>(ca)) ==
                   std::tolower(static_cast<unsigned char>(cb));
        });
}
int main() {
    std::string a = "user";
    std::string b = "User";
    std::cout << (equalsIgnoreCase(a, b) ? "Same" : "Different") << "\n";
    return 0;
}

Output:

Same

Scenario 2: Failing to distinguish JSON "null" string vs JSON null

When parsing JSON, you may need to separate the string "null" from a JSON null value—mixing up string comparison logic causes confusion.

std::string value = getJsonString(key);
if (value == "null") {  // JSON null as a string token
    // ...
}
// Also distinguish empty values and parse failures

Scenario 3: HTTP headers "Content-Type" vs "content-type"

Many HTTP header comparisons are case-insensitive; exact == makes "Content-Type" and "content-type" look different.

Scenario 4: Mixed slashes in file paths

To treat Windows paths like "C:\\Users\\file.txt" and Unix-style "C:/Users/file.txt" as equivalent, normalize first.


2. std::string guide

Construction and initialization

// After pasting: g++ -std=c++17 -o string_basic string_basic.cpp && ./string_basic
#include <iostream>
#include <string>
int main() {
    std::string s1;                    // empty
    std::string s2("hello");           // from C string
    std::string s3 = "world";          // copy initialization
    std::string s4(5, 'a');            // five 'a' characters: "aaaaa"
    std::string s5(s2, 1, 3);         // three chars from index 1: "ell"
    std::string s6(s2.begin(), s2.end());  // iterator range
    std::cout << s1 << "|" << s2 << "|" << s4 << "\n";
    return 0;
}

Output:

|hello|aaaaa

Explanation: s1 is empty; s2 is initialized from a literal; s4 uses (count, char); s5 uses (string, start, length) for a substring.

Main operations: concatenation, append, insert

#include <string>
#include <iostream>
int main() {
    std::string a = "Hello";
    std::string b = "World";
    // Concatenation: + returns a new string
    std::string c = a + " " + b;       // "Hello World"
    std::string d = a + std::string("!");  // "Hello!"
    // Append: += mutates the existing string
    a += " ";      // a = "Hello "
    a += b;        // a = "Hello World"
    // append
    std::string e = "Hi";
    e.append(" there");     // "Hi there"
    e.append(3, '!');       // "Hi there!!!"
    // push_back: one character
    e.push_back('?');       // "Hi there!!!"
    std::cout << c << "\n" << a << "\n" << e << "\n";
    return 0;
}

Output:

Hello World
Hello World
Hi there!!!

Note: + creates a new std::string, so a loop that does result = result + piece builds many temporaries. Prefer += or append to grow the existing buffer.

Search and replace

#include <string>
#include <iostream>
int main() {
    std::string s = "Hello World, Hello C++";
    // find: first match (or npos)
    size_t pos = s.find("Hello");
    std::cout << "First 'Hello' at: " << pos << "\n";  // 0
    pos = s.find("Hello", 1);  // search from index 1
    std::cout << "Second 'Hello' at: " << pos << "\n";  // 12
    // rfind: search backward from the end
    pos = s.rfind("Hello");
    std::cout << "Last 'Hello' at: " << pos << "\n";  // 12
    // find_first_of: first char from a set
    pos = s.find_first_of("aeiou");
    std::cout << "First vowel at: " << pos << "\n";  // 1 (e)
    // replace
    s.replace(0, 5, "Hi");  // replace 5 chars from 0 with "Hi"
    std::cout << s << "\n";  // "Hi World, Hello C++"
    return 0;
}

Output:

First 'Hello' at: 0
Second 'Hello' at: 12
Last 'Hello' at: 12
First vowel at: 1
Hi World, Hello C++

substr: extract a substring

#include <string>
#include <iostream>
int main() {
    std::string s = "Hello World";
    // substr(start): from start to end
    std::string sub1 = s.substr(6);   // "World"
    // substr(start, length)
    std::string sub2 = s.substr(0, 5);  // "Hello"
    // substr returns a new string (copy)
    std::cout << sub1 << " " << sub2 << "\n";
    return 0;
}

Output:

World Hello

Comparison

#include <string>
#include <iostream>
int main() {
    std::string a = "apple";
    std::string b = "banana";
    // ==, !=, <, <=, >, >=
    std::cout << (a == b) << "\n";   // 0 (false)
    std::cout << (a < b) << "\n";    // 1 (true, lexicographic)
    // compare: 0 equal, <0 a<b, >0 a>b
    int cmp = a.compare(b);
    std::cout << "compare: " << cmp << "\n";
    // Compare to C string literals
    std::cout << (a == "apple") << "\n";  // 1 (true)
    return 0;
}

Output:

1
compare: -1
1

Numeric conversion (C++11)

#include <string>
#include <iostream>
int main() {
    // String to number
    std::string s1 = "42";
    int i = std::stoi(s1);
    std::string s2 = "3.14";
    double d = std::stod(s2);
    // Number to string (C++11)
    std::string s3 = std::to_string(42);
    std::string s4 = std::to_string(3.14);
    std::cout << i << " " << d << " " << s3 << " " << s4 << "\n";
    return 0;
}

Output:

42 3.14 42 3.140000

Note: stoi throws std::invalid_argument or std::out_of_range on failure. For untrusted input, prefer std::from_chars (C++17) or strtol with explicit error checks.


3. C-string comparison and conversion

Using strcmp

// After pasting: g++ -std=c++17 -o strcmp_demo strcmp_demo.cpp && ./strcmp_demo
#include <iostream>
#include <cstring>
int main() {
    const char* a = "apple";
    const char* b = "banana";
    const char* c = "apple";
    // strcmp(a,b): negative if a<b, 0 if equal, positive if a>b
    std::cout << "strcmp(a,b): " << strcmp(a, b) << "\n";   // negative
    std::cout << "strcmp(a,c): " << strcmp(a, c) << "\n";   // 0
    // Correct: equal when == 0
    if (strcmp(a, c) == 0) {
        std::cout << "a and c are equal\n";
    }
    // ❌ Wrong: if strcmp returns 0, condition is false
    // if (strcmp(a, c)) { ....}  // runs when strings *differ*!
    return 0;
}

Output:

strcmp(a,b): -1
strcmp(a,c): 0
a and c are equal

strncmp: length-limited comparison

#include <cstring>
#include <iostream>
int main() {
    const char* a = "apple";
    const char* b = "application";
    // Compare only first 3 characters
    if (strncmp(a, b, 3) == 0) {
        std::cout << "First 3 chars match\n";
    }
    // strncmp stops at null terminator within the limit
    return 0;
}

strcasecmp: case-insensitive comparison (POSIX)

#include <cstring>
#include <iostream>
int main() {
    const char* a = "Hello";
    const char* b = "hello";
#ifdef _POSIX_C_SOURCE
    if (strcasecmp(a, b) == 0) {
        std::cout << "Equal (case-insensitive)\n";
    }
#else
    // Windows: _stricmp
    std::cout << "Use platform-specific or manual tolower\n";
#endif
    return 0;
}

Converting between std::string and C strings

#include <string>
#include <iostream>
#include <cstring>
int main() {
    // std::string → const char*
    std::string s = "hello";
    const char* cstr = s.c_str();   // null-terminated
    // Passing to C APIs
    printf("%s\n", s.c_str());
    // Warning: c_str() may be invalidated if s is modified
    s += " world";  // may reallocate
    // printf("%s", cstr);  // ❌ may dangle
    // C string → std::string
    const char* input = "from C";
    std::string s2(input);
    // std::string may contain embedded nulls (C++11)
    std::string s3 = "hello";
    s3 += '\0';
    s3 += "world";
    std::cout << s3.size() << "\n";  // 11 (includes null byte)
    return 0;
}

4. std::string_view (C++17)

What is string_view?

std::string_view is a lightweight non-owning type that does not own characters—it only views them. Without copying, it can refer to std::string, const char*, or literals, which makes it a good parameter type for read-only text.

// After pasting: g++ -std=c++17 -o string_view_basic string_view_basic.cpp && ./string_view_basic
#include <iostream>
#include <string>
#include <string_view>
void print(std::string_view sv) {
    std::cout << sv << " (size=" << sv.size() << ")\n";
}
int main() {
    std::string s = "Hello World";
    const char* cstr = "C string";
    const char* literal = "Literal";
    print(s);        // no copy of std::string
    print(cstr);
    print(literal);
    print("Inline");
    // substring: no copy of char data
    print(std::string_view(s).substr(0, 5));  // "Hello"
    return 0;
}

Output:

Hello World (size=11)
C string (size=8)
Literal (size=7)
Inline (size=6)
Hello (size=5)

Lifetime pitfalls with string_view

#include <string>
#include <string_view>
#include <iostream>
std::string_view getBadView() {
    std::string s = "temporary";
    return s;  // ❌ Dangerous: s is destroyed; view dangles
}
std::string_view getGoodView() {
    static std::string s = "static";
    return s;  // ✅ Valid until program exit
}
int main() {
    // std::string_view bad = getBadView();  // ❌ dangling
    std::string_view good = getGoodView();
    std::cout << good << "\n";
    return 0;
}

Rule: The storage referenced by a string_view must not be destroyed before the string_view is done. Returning a string_view to a local std::string from a function is a classic dangling bug.

Common string_view operations

#include <string_view>
#include <iostream>
int main() {
    std::string_view sv = "Hello World";
    // substr: no copy of underlying chars; new view only
    std::string_view sub = sv.substr(0, 5);  // "Hello"
    // find, rfind, find_first_of similar to std::string
    size_t pos = sv.find(' ');
    std::cout << "Space at: " << pos << "\n";
    // remove_prefix, remove_suffix: adjust view (C++20 availability may vary)
#if __cplusplus >= 202002L
    sv.remove_prefix(6);  // "World"
    std::cout << sv << "\n";
#endif
    return 0;
}

5. Complete string examples

Example 1: Parse one CSV line

// After pasting: g++ -std=c++17 -o csv_parse csv_parse.cpp && ./csv_parse
#include <sstream>
#include <string>
#include <vector>
#include <iostream>
std::vector<std::string> splitCSV(const std::string& line) {
    std::vector<std::string> result;
    std::stringstream ss(line);
    std::string cell;
    while (std::getline(ss, cell, ',')) {
        result.push_back(cell);
    }
    return result;
}
int main() {
    std::string line = "apple,banana,cherry";
    auto cells = splitCSV(line);
    for (const auto& c : cells) {
        std::cout << "[" << c << "] ";
    }
    std::cout << "\n";
    return 0;
}

Output:

[apple] [banana] [cherry]

Example 2: trim (strip leading/trailing whitespace)

#include <string>
#include <iostream>
std::string trim(const std::string& s) {
    size_t start = s.find_first_not_of(" \t\n\r");
    if (start == std::string::npos) return "";
    size_t end = s.find_last_not_of(" \t\n\r");
    return s.substr(start, end - start + 1);
}
int main() {
    std::string s = "  hello world  ";
    std::cout << "[" << trim(s) << "]\n";
    return 0;
}

Output:

[hello world]

Example 3: Join strings with reserve

#include <string>
#include <vector>
#include <iostream>
std::string join(const std::vector<std::string>& parts, const std::string& sep) {
    if (parts.empty()) return "";
    size_t total = 0;
    for (const auto& p : parts) total += p.size();
    total += sep.size() * (parts.size() - 1);
    std::string result;
    result.reserve(total);
    result = parts[0];
    for (size_t i = 1; i < parts.size(); ++i) {
        result += sep;
        result += parts[i];
    }
    return result;
}
int main() {
    std::vector<std::string> v = {"a", "bb", "ccc"};
    std::cout << join(v, ", ") << "\n";
    return 0;
}

Output:

a, bb, ccc

Example 4: Tokenize with string_view (no per-token copies)

#include <string>
#include <string_view>
#include <vector>
#include <iostream>
std::vector<std::string_view> splitView(std::string_view sv, char delim) {
    std::vector<std::string_view> result;
    while (!sv.empty()) {
        size_t pos = sv.find(delim);
        if (pos == std::string_view::npos) {
            result.push_back(sv);
            break;
        }
        result.push_back(sv.substr(0, pos));
        sv.remove_prefix(pos + 1);
    }
    return result;
}
int main() {
    std::string s = "one,two,three";
    auto tokens = splitView(s, ',');
    for (auto t : tokens) {
        std::cout << "[" << t << "] ";
    }
    std::cout << "\n";
    return 0;
}

Note: remove_prefix on std::string_view advances the start of the view without copying characters—useful for large inputs.


6. Common errors and fixes

Error 1: Comparing C strings with ==

Symptom: str1 == str2 is always false or behaves unexpectedly.
Cause: For const char*, == compares pointers.
Fix:

// ❌ Wrong
const char* a = "hello";
const char* b = "hello";
if (a == b) { /* ....*/ }
// ✅ Correct
if (strcmp(a, b) == 0) { /* ....*/ }
// Or use std::string
std::string sa(a); std::string sb(b);
if (sa == sb) { /* ....*/ }

Error 2: Misusing strcmp’s return value

Symptom: “Same” strings evaluate as false.
Cause: if (strcmp(a, b)) is true when strings differ.
Fix:

// ❌ Wrong
if (strcmp(a, b)) {
    std::cout << "Same\n";  // Opposite: runs when different
}
// ✅ Correct
if (strcmp(a, b) == 0) {
    std::cout << "Same\n";
}

Error 3: Holding c_str() too long

Symptom: Crashes or garbage.
Cause: After std::string mutates, reallocation may invalidate earlier c_str() pointers.
Fix:

// ❌ Wrong
const char* p = s.c_str();
s += " more";  // may reallocate
use(p);        // ❌ may dangle
// ✅ Correct
s += " more";
use(s.c_str());  // call immediately after s is stable

Error 4: Dangling string_view

Symptom: Crashes or garbage.
Cause: The viewed storage was destroyed first.
Fix:

// ❌ Wrong
std::string_view getView() {
    std::string s = "temp";
    return s;  // dangling after s is destroyed
}
// ✅ Correct: return std::string or guarantee lifetime
std::string getString() {
    return "temp";
}

Error 5: Uncaught stoi exceptions

Symptom: Program exits on input like "abc".
Cause: stoi throws on failure.
Fix:

// ❌ Risky
int n = std::stoi(user_input);  // "abc" → exception
// ✅ Better
try {
    int n = std::stoi(user_input);
} catch (const std::invalid_argument&) {
    // handle bad input
} catch (const std::out_of_range&) {
    // handle overflow
}
// Or std::from_chars (C++17, no exceptions)
int n;
auto [p, ec] = std::from_chars(user_input.data(),
                               user_input.data() + user_input.size(), n);
if (ec != std::errc{}) {
    // handle error
}

Error 6: Out-of-range indexing

Symptom: Crash or undefined behavior.
Cause: s[i] or s.substr(start, len) out of range.
Fix:

// ❌ Wrong
std::string s = "hi";
char c = s[10];  // undefined behavior
// ✅ Correct
if (i < s.size()) {
    char c = s[i];
}
// Or at() (bounds-checked, throws)
char c = s.at(i);  // std::out_of_range if invalid

7. Performance tips

Tip 1: Use reserve to limit reallocations

// ❌ May reallocate often
std::string result;
for (const auto& piece : parts) {
    result += piece;
}
// ✅ Reserve first
std::string result;
size_t total = 0;
for (const auto& piece : parts) total += piece.size();
result.reserve(total);
for (const auto& piece : parts) {
    result += piece;
}

Tip 2: Read-only parameters: string_view

// ❌ May copy into std::string at call site
void process(const std::string& s);
// ✅ No copy for string literal / string_view pipeline
void process(std::string_view s);

Tip 3: Short strings and SSO

Most implementations use SSO (small string optimization): strings up to roughly 15–23 bytes often live inside the std::string object with no heap allocation.

Tip 4: Many appends: reserve + append

std::string result;
result.reserve(estimated_size);
for (const auto& s : parts) {
    result.append(s);
}

Tip 5: from_chars for parsing (C++17)

std::from_chars does not throw and is often faster than stoi/stod.

#include <charconv>
#include <iostream>
int main() {
    std::string s = "12345";
    int value;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec == std::errc{}) {
        std::cout << "Parsed: " << value << "\n";
    } else {
        std::cout << "Parse failed\n";
    }
    return 0;
}

8. Production patterns

Pattern 1: Use string_view for read-only string parameters

// When the function only reads text
std::string findAndReplace(std::string_view text,
                           std::string_view find,
                           std::string_view replace) {
    std::string result;
    result.reserve(text.size());
    size_t pos = 0;
    while (true) {
        size_t found = text.find(find, pos);
        if (found == std::string_view::npos) {
            result.append(text.substr(pos));
            break;
        }
        result.append(text.substr(pos, found - pos));
        result.append(replace);
        pos = found + find.size();
    }
    return result;
}

Pattern 2: Building log/error messages

#include <sstream>
#include <string>
std::string formatError(const std::string& filename, int line, const std::string& msg) {
    std::ostringstream oss;
    oss << filename << ":" << line << ": " << msg;
    return oss.str();
}
// C++20: std::format
// return std::format("{}:{}: {}", filename, line, msg);

Pattern 3: String interning pool

#include <string>
#include <unordered_set>
class StringPool {
    std::unordered_set<std::string> pool;
public:
    std::string_view intern(const std::string& s) {
        auto it = pool.find(s);
        if (it != pool.end()) {
            return *it;
        }
        auto [inserted, _] = pool.insert(s);
        return *inserted;
    }
};

Pattern 4: Safe wrappers for C APIs

void legacyApi(const char* str);
void safeCall(const std::string& s) {
    legacyApi(s.c_str());  // use c_str() only at the call
}
void safeCall(std::string_view sv) {
    std::string temp(sv);  // copy if you need a null-terminated buffer
    legacyApi(temp.c_str());
}

Pattern 5: Environment / config strings

#include <string>
#include <iostream>
#include <cstdlib>
std::string getEnvOrDefault(const std::string& key, const std::string& def) {
    const char* val = std::getenv(key.c_str());
    return val ? std::string(val) : def;
}

9. Implementation checklist

Use this when working with strings:

  • Comparison: C strings → strcmp; == on pointers compares addresses
  • strcmp: equal strings when result == 0
  • Concatenation: in loops prefer +=/append and reserve to cut reallocations
  • c_str(): do not store the pointer across mutating operations on the string
  • string_view: backing storage must outlive the view
  • stoi/stod: handle exceptions or use from_chars
  • Indexing: bounds checks or at()
  • Parameters: read-only text → std::string_view

  • C++ vector performance | “1M pushes in 10 seconds” and reserve
  • C++ stringstream | parsing, conversion, formatting
  • C++ string parsing guide | stringstream, getline, zero-copy, benchmarks

C++ std::string, string comparison, strcmp, string_view, concatenation, reserve, SSO, C string conversion, string parsing.

Summary

ItemSyntax / roleWatch out
std::stringOwns and mutatesUse reserve to reduce reallocations
string_viewRead-only viewVerify backing lifetime
C stringsconst char*Compare with strcmp
c_str()C API interopUse the pointer immediately
stoi/stodNumeric parsingExceptions or from_chars

Principles:

  1. Compare C strings with strcmp (or lift to std::string).
  2. Use string_view for read-only parameters when lifetimes are clear.
  3. reserve when concatenating many times.
  4. Treat c_str() pointers as immediately invalidated after further changes to the string.
  5. Never let a string_view outlive the data it views.

References

FAQ

Q. Should I use std::string or string_view?

A. Use std::string when you own or mutate the text; use std::string_view for read-only use. For many function parameters, string_view is a good default.

Q. Performance: C string vs std::string comparison?

A. std::string’s == compares contents—typically O(n). strcmp is also O(n). string_view avoids copies when passing large strings repeatedly.

Q. What is SSO?

A. Small String Optimization: short strings (often ≤15–23 bytes, implementation-defined) are stored inside the std::string object without a separate heap allocation.

Q. How do I handle Korean or UTF-8 text?

A. std::string holds bytes. UTF-8 is multi-byte: size() is the byte length, not necessarily the number of Unicode characters. Use ICU, iconv, or similar for proper grapheme/codepoint handling.

One-line recap: std::string for ownership and mutation, string_view for non-owning reads, C strings compared with strcmp. Next, see stringstream (#11-3) and string parsing (#32-2).

Next post: C++ in practice #11-2: binary serialization
Previous post: C++ in practice #10-3: STL algorithms


More in this series

  • C++ file I/O | ifstream and ofstream basics
  • C++ binary serialization
  • C++ string algorithms | split, join, trim, replace, regex
  • C++ stringstream | parsing, conversion, formatting
  • C++ vector performance