C++ 문자열 기초 완벽 가이드 | std::string·C 문자열·string_view와 실전 패턴

C++ 문자열 기초 완벽 가이드 | std::string·C 문자열·string_view와 실전 패턴

이 글의 핵심

C++ 문자열 기초 완벽 가이드에 대해 정리한 개발 블로그 글입니다. C 스타일 문자열(char 또는 const char)을 사용하는 레거시 API와 연동할 때, ==로 비교하면 포인터 주소가 비교됩니다. 문자열 내용이 아니라 "같은 메모리 주소를 가리키는지"만 확인하게 됩니다. 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: C++, st…

들어가며: 문자열 비교에서 == 쓰다가 크래시

”C 문자열 두 개를 ==로 비교했는데 항상 같다고 나와요”

C 스타일 문자열(char* 또는 const char*)을 사용하는 레거시 API와 연동할 때, ==로 비교하면 포인터 주소가 비교됩니다. 문자열 내용이 아니라 “같은 메모리 주소를 가리키는지”만 확인하게 됩니다.

자주 겪는 문제 시나리오들

시나리오 1: strcmp 반환값 잘못 해석
strcmp는 “같으면 0, 작으면 음수, 크면 양수”를 반환하는데, if (strcmp(a, b))처럼 쓰면 “같을 때” false가 됩니다. C++에서 if (x)는 x가 0이 아니면 true이므로, strcmp가 0(같음)을 반환할 때 false가 되어 의도와 반대입니다.

시나리오 2: 문자열 연결 시 성능 저하
for 루프 안에서 result += str를 반복하면, 매번 재할당이 발생할 수 있습니다. reserve로 미리 공간을 잡아 두지 않으면 작은 문자열을 수천 번 연결할 때 수 초가 걸릴 수 있습니다.

시나리오 3: substr로 잘라낸 뒤 원본 수정
std::string_view로 받은 문자열의 일부를 가리킬 때, 원본이 수정되거나 삭제되면 댕글링 참조가 됩니다. string_view는 복사하지 않고 “보기만” 하므로, 참조하는 메모리가 유효한 동안만 사용해야 합니다.

시나리오 4: C API에 std::string 넘기기
printf("%s", str)처럼 C 함수에 std::string을 넘기면 컴파일 에러입니다. C API는 const char*를 기대하므로 str.c_str()를 사용해야 합니다.

정의를 풀어 쓰면 std::string은 C++ 표준 문자열 클래스로, 크기가 자동으로 늘어나고 메모리를 자동 관리합니다. string_view(C++17)는 문자열을 복사하지 않고 “보기만” 하는 경량 뷰입니다. 비유하면 std::string은 “내가 소유한 노트”이고, string_view는 “노트의 특정 페이지를 가리키는 북마크”입니다.

문제의 코드:

const char* a = "hello";
const char* b = "hello";

if (a == b) {  // ❌ 포인터 비교! 내용은 같아도 주소가 다를 수 있음
    std::cout << "Same\n";
}

위 코드 설명: a와 b는 서로 다른 메모리 주소를 가리킬 수 있습니다. 컴파일러가 문자열 리터럴을 “같은 주소”로 합치는 최적화(string pooling)를 할 수도 있지만, 런타임에 만든 문자열이나 다른 번역 단위에선 보장되지 않습니다. 내용 비교가 목적이면 strcmp를 써야 합니다.

올바른 비교:

#include <cstring>

const char* a = "hello";
const char* b = "hello";

if (strcmp(a, b) == 0) {  // ✅ 내용 비교
    std::cout << "Same\n";
}

이 글을 읽으면:

  • std::string의 주요 연산을 이해할 수 있습니다.
  • C 문자열과 std::string의 비교·변환을 올바르게 할 수 있습니다.
  • string_view로 불필요한 복사를 제거할 수 있습니다.
  • 자주 겪는 에러와 해결법을 알 수 있습니다.
  • 프로덕션에서의 문자열 패턴을 배울 수 있습니다.

문자열 타입 선택을 요약하면 아래와 같습니다.

flowchart TB
  subgraph choice["문자열 타입 선택"]
    A[문자열이 필요할 때] --> B{용도}
    B -->|소유·수정 필요| C["std string"]
    B -->|읽기만·함수 인자| D["std string_view"]
    B -->|C API 연동| E["const char*"]
  end
  subgraph caution["주의"]
    C --> F["reserve로 재할당 최소화"]
    D --> G["원본 수명 확인"]
    E --> H["strcmp로 비교"]
  end

목차

  1. 문제 시나리오: 실제로 겪는 문자열 문제들
  2. std::string 완전 가이드
  3. C 문자열 비교와 변환
  4. std::string_view (C++17)
  5. 완전한 문자열 예제 모음
  6. 자주 발생하는 에러와 해결법
  7. 성능 최적화 팁
  8. 프로덕션 패턴
  9. 구현 체크리스트

1. 문제 시나리오: 실제로 겪는 문자열 문제들

시나리오 1: 로그 파싱에서 “user”와 “User”가 같은 것으로 처리됨

대소문자를 구분하지 않는 비교가 필요할 때, ==는 대소문자를 구분합니다. "user" == "User"는 false입니다.

해결: std::transform으로 소문자 변환 후 비교하거나, strcasecmp(POSIX)를 사용합니다.

// 복사해 붙여넣은 뒤: 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(),
         {
            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;
}

실행 결과:

Same

시나리오 2: JSON 키에서 “null” 문자열과 실제 null 구분 실패

파싱된 JSON에서 "null"(문자열)과 null(JSON null 값)을 구분해야 할 때, 문자열 비교를 잘못하면 혼동됩니다.

std::string value = getJsonString(key);
if (value == "null") {  // JSON의 null 문자열
    // ...
}
// value가 비어 있거나 파싱 실패와 구분 필요

시나리오 3: HTTP 헤더에서 “Content-Type” vs “content-type”

HTTP 헤더는 대소문자를 구분하지 않는 경우가 많습니다. ==로 비교하면 “Content-Type”과 “content-type”이 다르게 처리됩니다.

시나리오 4: 파일 경로에서 슬래시/백슬래시 혼용

Windows 경로 "C:\\Users\\file.txt"와 Linux 스타일 "C:/Users/file.txt"를 같은 것으로 처리하려면 정규화가 필요합니다.


2. std::string 완전 가이드

기본 생성과 초기화

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o string_basic string_basic.cpp && ./string_basic
#include <iostream>
#include <string>

int main() {
    std::string s1;                    // 빈 문자열
    std::string s2("hello");           // C 문자열로 초기화
    std::string s3 = "world";          // 복사 초기화
    std::string s4(5, 'a');            // 'a' 5개: "aaaaa"
    std::string s5(s2, 1, 3);         // s2의 1번 인덱스부터 3글자: "ell"
    std::string s6(s2.begin(), s2.end());  // 반복자 범위

    std::cout << s1 << "|" << s2 << "|" << s4 << "\n";
    return 0;
}

실행 결과:

|hello|aaaaa

위 코드 설명: s1은 빈 문자열, s2는 C 문자열 리터럴로 초기화, s4는 (개수, 문자)로 반복 문자, s5는 (문자열, 시작위치, 길이)로 부분 문자열을 만듭니다.

주요 연산: 연결, 추가, 삽입

#include <string>
#include <iostream>

int main() {
    std::string a = "Hello";
    std::string b = "World";

    // 연결: + 연산자 (새 문자열 반환)
    std::string c = a + " " + b;       // "Hello World"
    std::string d = a + std::string("!");  // "Hello!"

    // 추가: += (기존 문자열 수정)
    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: 문자 하나 추가
    e.push_back('?');       // "Hi there!!!"

    std::cout << c << "\n" << a << "\n" << e << "\n";
    return 0;
}

실행 결과:

Hello World
Hello World
Hi there!!!

주의: + 연산자는 새 문자열을 반환하므로 임시 객체가 생성됩니다. 루프 안에서 result = result + piece를 반복하면 매번 새 문자열이 만들어져 비효율적입니다. += 또는 append를 사용하면 기존 버퍼에 추가합니다.

검색과 치환

#include <string>
#include <iostream>

int main() {
    std::string s = "Hello World, Hello C++";

    // find: 처음 발견 위치 (없으면 npos)
    size_t pos = s.find("Hello");
    std::cout << "First 'Hello' at: " << pos << "\n";  // 0

    pos = s.find("Hello", 1);  // 1번 인덱스부터 검색
    std::cout << "Second 'Hello' at: " << pos << "\n";  // 12

    // rfind: 처음부터 역순 검색 (마지막 발견 위치)
    pos = s.rfind("Hello");
    std::cout << "Last 'Hello' at: " << pos << "\n";  // 12

    // find_first_of: 문자 집합 중 하나라도 처음
    pos = s.find_first_of("aeiou");
    std::cout << "First vowel at: " << pos << "\n";  // 1 (e)

    // replace: 치환
    s.replace(0, 5, "Hi");  // 0번부터 5글자를 "Hi"로
    std::cout << s << "\n";  // "Hi World, Hello C++"

    return 0;
}

실행 결과:

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

substr: 부분 문자열 추출

#include <string>
#include <iostream>

int main() {
    std::string s = "Hello World";

    // substr(시작): 시작부터 끝까지
    std::string sub1 = s.substr(6);   // "World"

    // substr(시작, 길이): 시작부터 지정 길이
    std::string sub2 = s.substr(0, 5);  // "Hello"

    // substr은 새 문자열을 반환 (복사)
    std::cout << sub1 << " " << sub2 << "\n";

    return 0;
}

실행 결과:

World Hello

비교 연산

#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, 사전순)

    // compare: 0=같음, <0=a가 작음, >0=a가 큼
    int cmp = a.compare(b);
    std::cout << "compare: " << cmp << "\n";

    // C 문자열과도 비교 가능
    std::cout << (a == "apple") << "\n";  // 1 (true)

    return 0;
}

실행 결과:

0
1
compare: -1
1

숫자 변환 (C++11)

#include <string>
#include <iostream>

int main() {
    // 문자열 → 숫자
    std::string s1 = "42";
    int i = std::stoi(s1);

    std::string s2 = "3.14";
    double d = std::stod(s2);

    // 숫자 → 문자열 (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;
}

실행 결과:

42 3.14 42 3.140000

주의: stoi는 변환 실패 시 std::invalid_argument 또는 std::out_of_range 예외를 던집니다. 사용자 입력처럼 잘못된 값이 자주 들어오는 경로에서는 std::from_chars(C++17) 또는 strtol+에러 검사가 안전합니다.


3. C 문자열 비교와 변환

strcmp 사용법

// 복사해 붙여넣은 뒤: 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): a < b 이면 음수, a == b 이면 0, a > b 이면 양수
    std::cout << "strcmp(a,b): " << strcmp(a, b) << "\n";   // 음수
    std::cout << "strcmp(a,c): " << strcmp(a, c) << "\n";   // 0

    // 올바른 비교: == 0일 때 "같음"
    if (strcmp(a, c) == 0) {
        std::cout << "a and c are equal\n";
    }

    // ❌ 잘못된 사용: strcmp(a,c)가 0이면 false
    // if (strcmp(a, c)) { ... }  // "다를 때" 실행됨!

    return 0;
}

실행 결과:

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

strncmp: 길이 제한 비교

#include <cstring>
#include <iostream>

int main() {
    const char* a = "apple";
    const char* b = "application";

    // 앞 3글자만 비교
    if (strncmp(a, b, 3) == 0) {
        std::cout << "First 3 chars match\n";
    }

    // strncmp는 null 종료 전까지만 비교
    // strncmp("app", "apple", 5) → "app"은 3글자만 있으므로 3글자까지만 비교

    return 0;
}

strcasecmp: 대소문자 무시 비교 (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;
}

std::string ↔ C 문자열 변환

#include <string>
#include <iostream>
#include <cstring>

int main() {
    // std::string → const char*
    std::string s = "hello";
    const char* cstr = s.c_str();   // null 종료 보장

    // C API에 넘길 때
    printf("%s\n", s.c_str());

    // 주의: c_str() 반환값은 s가 수정되면 무효화됨
    s += " world";  // 재할당 가능
    // printf("%s", cstr);  // ❌ 댕글링 가능

    // C 문자열 → std::string
    const char* input = "from C";
    std::string s2(input);

    // std::string이 null 포함할 수 있음 (C++11)
    std::string s3 = "hello";
    s3 += '\0';
    s3 += "world";
    std::cout << s3.size() << "\n";  // 11 (null 포함)

    return 0;
}

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

string_view란?

std::string_view는 문자열을 소유하지 않고 “보기만” 하는 경량 타입입니다. 복사 없이 std::string, const char*, 리터럴을 받을 수 있어, 함수 인자로 전달할 때 효율적입니다.

// 복사해 붙여넣은 뒤: 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);        // std::string 복사 없음
    print(cstr);     // C 문자열
    print(literal);  // 리터럴
    print("Inline"); // inline 리터럴

    // substring: 복사 없음
    print(std::string_view(s).substr(0, 5));  // "Hello"

    return 0;
}

실행 결과:

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

string_view 주의사항: 수명

#include <string>
#include <string_view>
#include <iostream>

std::string_view getBadView() {
    std::string s = "temporary";
    return s;  // ❌ 위험! s가 파괴되면 댕글링
}

std::string_view getGoodView() {
    static std::string s = "static";
    return s;  // ✅ s가 프로그램 종료까지 유효
}

int main() {
    // std::string_view bad = getBadView();  // ❌ 댕글링
    std::string_view good = getGoodView();
    std::cout << good << "\n";
    return 0;
}

핵심: string_view가 가리키는 원본이 string_view보다 먼저 파괴되면 안 됩니다. 함수 반환값으로 string_view를 반환할 때, 그 안에서 만든 지역 std::string을 반환하면 댕글링입니다.

string_view 주요 연산

#include <string_view>
#include <iostream>

int main() {
    std::string_view sv = "Hello World";

    // substr: 복사 없음, 뷰만 생성
    std::string_view sub = sv.substr(0, 5);  // "Hello"

    // find, rfind, find_first_of 등 std::string과 유사
    size_t pos = sv.find(' ');
    std::cout << "Space at: " << pos << "\n";

    // remove_prefix, remove_suffix: 뷰 범위 조정 (C++20)
#if __cplusplus >= 202002L
    sv.remove_prefix(6);  // "World"
    std::cout << sv << "\n";
#endif

    return 0;
}

5. 완전한 문자열 예제 모음

예제 1: CSV 한 줄 파싱

// 복사해 붙여넣은 뒤: 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;
}

실행 결과:

[apple] [banana] [cherry]

예제 2: trim (앞뒤 공백 제거)

#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;
}

실행 결과:

[hello world]

예제 3: 문자열 연결 (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;
}

실행 결과:

a, bb, ccc

예제 4: string_view로 토큰 파싱 (복사 없음)

#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;
}

위 코드 설명: remove_prefix는 C++17 std::string_view 멤버로, 뷰의 시작 위치를 앞으로 당깁니다. 복사 없이 토큰을 나눌 수 있어 대용량 파싱에 유리합니다.


6. 자주 발생하는 에러와 해결법

에러 1: C 문자열을 ==로 비교

증상: str1 == str2가 항상 false 또는 예상과 다르게 동작

원인: const char*==는 포인터 주소 비교

해결:

// ❌ 잘못된 코드
const char* a = "hello";
const char* b = "hello";
if (a == b) { /* ... */ }

// ✅ 올바른 코드
if (strcmp(a, b) == 0) { /* ... */ }

// 또는 std::string 사용
std::string sa(a); std::string sb(b);
if (sa == sb) { /* ... */ }

에러 2: strcmp 반환값 잘못 해석

증상: “같은 문자열”인데 조건이 false로 처리됨

원인: if (strcmp(a, b))는 “다를 때” true

해결:

// ❌ 잘못된 코드
if (strcmp(a, b)) {
    std::cout << "Same\n";  // 반대! 다를 때 실행됨
}

// ✅ 올바른 코드
if (strcmp(a, b) == 0) {
    std::cout << "Same\n";
}

에러 3: c_str() 반환값을 오래 보관

증상: 크래시 또는 쓰레기 값

원인: std::string이 수정되면 내부 버퍼가 재할당되어 c_str() 이전 반환값이 무효화됨

해결:

// ❌ 잘못된 코드
const char* p = s.c_str();
s += " more";  // 재할당 가능
use(p);        // ❌ 댕글링

// ✅ 올바른 코드
s += " more";
use(s.c_str());  // 수정 직후 호출

에러 4: string_view가 댕글링

증상: 크래시 또는 쓰레기 값

원인: string_view가 가리키는 원본이 먼저 파괴됨

해결:

// ❌ 잘못된 코드
std::string_view getView() {
    std::string s = "temp";
    return s;  // s 파괴 후 댕글링
}

// ✅ 올바른 코드: std::string 반환 또는 원본 수명 보장
std::string getString() {
    return "temp";
}

에러 5: stoi 예외 미처리

증상: "abc" 같은 입력 시 프로그램 종료

원인: stoi는 변환 실패 시 예외 던짐

해결:

// ❌ 잘못된 코드
int n = std::stoi(user_input);  // "abc" → 예외

// ✅ 올바른 코드
try {
    int n = std::stoi(user_input);
} catch (const std::invalid_argument&) {
    // 잘못된 입력 처리
} catch (const std::out_of_range&) {
    // 오버플로우 처리
}

// 또는 std::from_chars (C++17, 예외 없음)
int n;
auto [p, ec] = std::from_chars(user_input.data(),
                               user_input.data() + user_input.size(), n);
if (ec != std::errc{}) {
    // 에러 처리
}

에러 6: 인덱스 범위 초과

증상: 크래시 또는 undefined behavior

원인: s[i] 또는 s.substr(start, len)에서 범위 초과

해결:

// ❌ 잘못된 코드
std::string s = "hi";
char c = s[10];  // undefined behavior

// ✅ 올바른 코드
if (i < s.size()) {
    char c = s[i];
}
// 또는 at() 사용 (범위 검사, 예외)
char c = s.at(i);  // 범위 초과 시 std::out_of_range

7. 성능 최적화 팁

tip 1: reserve로 재할당 최소화

// ❌ 비효율: 매번 재할당 가능
std::string result;
for (const auto& piece : parts) {
    result += piece;
}

// ✅ 효율: 한 번에 공간 확보
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: 읽기 전용 인자는 string_view

// ❌ std::string 복사 발생
void process(const std::string& s);

// ✅ 복사 없음 (std::string, const char*, 리터럴 모두 수용)
void process(std::string_view s);

tip 3: 작은 문자열은 SSO 활용

대부분의 구현에서 SSO(Small String Optimization)를 사용합니다. 보통 15~23바이트 이하 문자열은 힙 할당 없이 객체 내부에 저장됩니다. 따라서 짧은 문자열은 std::string을 그대로 사용해도 됩니다.

tip 4: 반복 연결 시 += 대신 reserve + append

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

tip 5: from_chars로 숫자 변환 (C++17)

std::from_chars는 예외를 던지지 않고, 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. 프로덕션 패턴

패턴 1: 문자열 인자로 string_view 사용

// 함수가 문자열을 읽기만 할 때
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;
}

패턴 2: 로그/에러 메시지 조립

#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);

패턴 3: 문자열 풀 (공통 문자열 캐싱)

#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;
    }
};

패턴 4: 안전한 C API 래퍼

void legacyApi(const char* str);

void safeCall(const std::string& s) {
    legacyApi(s.c_str());  // 호출 시점에만 c_str() 사용
}

void safeCall(std::string_view sv) {
    std::string temp(sv);  // null 종료 필요하면 임시 복사
    legacyApi(temp.c_str());
}

패턴 5: 환경 변수/설정 파싱

#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. 구현 체크리스트

문자열을 다룰 때 확인할 항목입니다.

  • 비교: C 문자열은 strcmp 사용, ==는 포인터 비교
  • strcmp: == 0일 때 “같음”
  • 연결: 루프 안에서는 += 또는 append, reserve로 재할당 최소화
  • c_str(): 반환값을 오래 보관하지 말 것
  • string_view: 원본 수명이 string_view보다 길어야 함
  • stoi/stod: 예외 처리 또는 from_chars 사용
  • 인덱스: 범위 검사 또는 at() 사용
  • 함수 인자: 읽기 전용이면 std::string_view

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ vector 성능 | “100만 개 넣는데 10초” 문제와 reserve
  • C++ stringstream | 문자열 파싱·변환·포맷팅
  • C++ 문자열 파싱 완벽 가이드 | stringstream·getline·제로카피·성능 벤치마크

이 글에서 다루는 키워드 (관련 검색어)

C++ std::string, 문자열 비교, strcmp, string_view, 문자열 연결, reserve, SSO, C 문자열 변환, 문자열 파싱 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목문법/용도주의
std::string소유·수정reserve로 재할당 최소화
string_view읽기 전용원본 수명 확인
C 문자열const char*strcmp로 비교
c_str()C API 연동반환값 단기 사용만
stoi/stod숫자 변환예외 처리

핵심 원칙:

  1. C 문자열은 strcmp로 비교
  2. 읽기 전용 인자는 string_view
  3. 연결 시 reserve로 재할당 최소화
  4. c_str() 반환값은 즉시 사용 후 버림
  5. string_view는 원본보다 오래 살지 않도록

참고 자료

자주 묻는 질문 (FAQ)

Q. std::string과 string_view 중 어떤 것을 써야 하나요?

A. 문자열을 소유하고 수정할 때는 std::string, 읽기만 할 때는 std::string_view를 사용합니다. 함수 인자로 문자열을 받을 때 대부분 string_view가 적합합니다.

Q. C 문자열과 std::string 비교 시 성능 차이는?

A. std::string==는 내용 비교를 하므로 O(n)입니다. C 문자열 strcmp도 O(n)입니다. string_view는 복사 없이 비교할 수 있어, 큰 문자열을 여러 번 전달할 때 유리합니다.

Q. SSO란?

A. Small String Optimization. 짧은 문자열(보통 15~23바이트 이하)은 힙에 할당하지 않고 std::string 객체 내부에 저장하는 최적화입니다. 구현에 따라 다릅니다.

Q. 한글(UTF-8) 문자열은 어떻게 처리하나요?

A. std::string은 바이트 시퀀스를 저장합니다. UTF-8은 멀티바이트이므로 size()는 바이트 수가 아니라 문자 수가 아닙니다. 문자 단위로 다루려면 ICU, iconv 등 외부 라이브러리를 사용합니다.

한 줄 요약: std::string은 소유·수정, string_view는 읽기 전용, C 문자열은 strcmp로 비교합니다. 다음으로 stringstream(#11-3)문자열 파싱(#32-2)을 읽어보면 좋습니다.

다음 글: C++ 실전 가이드 #11-2: 바이너리 직렬화

이전 글: C++ 실전 가이드 #10-3: STL 알고리즘


관련 글

  • C++ 파일 입출력 | ifstream·ofstream으로
  • C++ 바이너리 직렬화 |
  • C++ 문자열 알고리즘 완벽 가이드 | split·join·trim·replace·정규식 [실전]
  • C++ stringstream | 문자열 파싱·변환·포맷팅
  • C++ vector 성능 |