C++ string vs string_view: 복사 없는 문자열 처리와 선택 기준

이 글의 핵심

string_view는 가볍지만 원본 수명에 기대는 참조라서 댕글링이 가장 큰 함정입니다. 임시 string에서 만든 string_view, null 종료가 보장되지 않아 printf나 C API에 넘길 때의 위험, 멤버 변수로 저장할 때의 문제를 HTTP 요청·CSV 파싱 사례와 함께 짚습니다.

들어가며

C++17의 string_view는 문자열을 복사하지 않고 참조만 하는 경량 타입입니다. 함수 매개변수로 사용하면 복사 비용을 제거할 수 있습니다. 비유로 말씀드리면, string은 책을 사서 책장에 꽂는 것(소유), string_view는 책 제목만 적힌 메모를 들고 원본 책을 가리키는 것(비소유)에 가깝습니다. 메모만 남기고 원본을 반납하면 내용을 읽을 수 없습니다(댕글링).

string_view가 등장하기 전에는 읽기 전용 문자열 매개변수를 const std::string&로 받는 것이 관례였습니다. 그런데 이 방식은 호출하는 쪽이 std::string을 이미 갖고 있을 때만 복사가 없습니다. 문자열 리터럴이나 const char*, 다른 라이브러리의 문자열 타입(Qt의 QString을 변환한 버퍼 등)을 넘기면 함수에 들어가기 전에 임시 std::string이 만들어지고, 길면 힙 할당이 일어납니다. 반대로 const char*로 받으면 std::string을 넘길 때 .c_str()을 붙여야 하고 길이 정보를 잃어 strlen을 다시 계산해야 합니다. string_view는 “어떤 연속된 문자 배열이든 포인터와 길이로 받는다”는 공통 인터페이스로 이 두 문제를 동시에 해결합니다. 그 대가가 이 글 뒷부분에서 반복해서 다루는 수명 문제입니다.


string vs string_view 비교

비교표

항목std::stringstd::string_view
소유권소유 (힙 할당)비소유 (참조만)
복사 비용높음 (문자 복사)낮음 (포인터 복사)
수정가능불가능 (읽기 전용)
크기24~32바이트 (64비트, 구현 의존)16바이트 (포인터 + 길이, 64비트)
null 종료보장보장 안 됨
수명자동 관리수동 관리 (주의)

내부 구조

// std::string (간략화)
class string {
    char* data_;     // 힙 메모리 포인터
    size_t size_;    // 길이
    size_t capacity_; // 용량
};
// std::string_view
class string_view {
    const char* data_;  // 원본 포인터 (비소유)
    size_t size_;       // 길이
};

위의 std::string 구조는 개념을 보여 주기 위한 단순화입니다. 실제 구현은 대부분 짧은 문자열 최적화(SSO)를 써서, 짧은 문자열은 힙 대신 객체 안의 버퍼에 직접 저장합니다. libstdc++는 15자, MSVC는 15자, libc++는 22자까지를 객체 안에 담으므로, 이보다 짧은 문자열의 복사는 힙 할당 없이 바이트 복사로 끝납니다. 뒤의 벤치마크에서 짧은 부분 문자열의 string::substr이 생각보다 빠르게 나오는 이유가 이것입니다. sizeof(std::string)이 구현마다 다른 것도 이 내부 버퍼 크기 때문입니다.

string_view는 const char*와 길이만 가지므로 복사해도 원본과 같은 메모리를 가리킵니다. 값으로 전달하는 것이 참조로 전달하는 것보다 오히려 권장되는데, 16바이트짜리 구조체는 x86-64 System V 호출 규약에서 레지스터 두 개로 전달되어 참조의 간접 접근보다 싸기 때문입니다. 그래서 const std::string_view&로 받을 이유는 거의 없습니다.


실전 구현

함수 매개변수

#include <iostream>
#include <string>
#include <string_view>
// ❌ 복사 발생
void printString(std::string s) {  // 값 전달 → 복사
    std::cout << s << std::endl;
}
// ✅ 복사 없음
void printStringView(std::string_view s) {  // 뷰 → 복사 없음
    std::cout << s << std::endl;
}
int main() {
    std::string str = "Hello, World!";
    
    printString(str);      // 복사 발생
    printStringView(str);  // 복사 없음
    
    return 0;
}

string_view 매개변수를 받은 함수 안에서 그 문자열을 std::string을 요구하는 다른 함수에 넘겨야 하는 경우가 자주 있습니다. 이때 std::string_view에서 std::string으로의 변환은 명시적(explicit)이라 takesString(sv)는 컴파일 에러가 나고, takesString(std::string(sv))처럼 직접 변환해야 합니다. 복사가 일어난다는 것을 코드에 드러내려는 설계입니다. 호출 경로 깊은 곳에서 결국 std::string이 필요하다면, 중간 계층을 string_view로 바꿔도 복사가 사라지지 않고 위치만 옮겨진다는 점을 기억해야 합니다. 처음 string_view를 도입할 때 인터페이스 한 곳만 바꾸고 “빨라졌겠지” 하고 넘어가기 쉬운데, 호출 체인 전체를 따라가 봐야 실제로 복사가 줄었는지 알 수 있습니다.


부분 문자열

#include <iostream>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello, World!";
    
    // string: 복사 발생
    std::string sub1 = str.substr(0, 5);  // "Hello" 복사
    std::cout << sub1 << std::endl;
    
    // string_view: 복사 없음
    std::string_view sv = str;
    std::string_view sub2 = sv.substr(0, 5);  // 포인터 + 길이만
    std::cout << sub2 << std::endl;
    
    return 0;
}

문자열 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view s, char delim) {
    std::vector<std::string_view> tokens;
    size_t start = 0;
    
    while (start < s.size()) {
        size_t end = s.find(delim, start);
        if (end == std::string_view::npos) {
            tokens.push_back(s.substr(start));
            break;
        }
        
        tokens.push_back(s.substr(start, end - start));
        start = end + 1;
    }
    
    return tokens;
}
int main() {
    std::string data = "apple,banana,cherry";
    auto tokens = split(data, ',');  // 복사 없음
    
    for (auto token : tokens) {
        std::cout << token << std::endl;
    }
    
    return 0;
}

출력:

apple
banana
cherry

split이 돌려준 토큰들은 모두 data 문자열의 일부를 가리킵니다. 그래서 data가 살아 있는 동안만 유효하고, data에 +=로 문자를 덧붙이는 것처럼 재할당을 일으킬 수 있는 수정을 하면 모든 토큰이 한꺼번에 댕글링이 됩니다. 이 함수에 split(std::string("a,b,c"), ',')처럼 임시 문자열을 넘기면 반환 직후 임시 객체가 사라지므로 벡터 전체가 쓸 수 없는 상태로 돌아옵니다. 컴파일러는 이를 막지 않으며, GCC 13 이후의 -Wdangling-reference 같은 경고도 모든 경우를 잡지는 못합니다.

이 구현은 "a,b,"처럼 구분자로 끝나는 입력에서 마지막 빈 필드를 돌려주지 않습니다. start가 문자열 끝에 도달하면 루프가 끝나기 때문입니다. CSV처럼 빈 필드도 의미가 있는 형식이라면 루프 조건을 start <= s.size()로 바꾸고 npos일 때 나머지를 넣은 뒤 빠져나오게 해야 합니다.


로깅

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class Logger {
private:
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {  // 매개변수는 string_view
        logs_.emplace_back(msg);  // string으로 변환해 저장
    }
    
    const std::string& getLog(size_t idx) const {
        return logs_[idx];
    }
    
    size_t size() const {
        return logs_.size();
    }
};
int main() {
    Logger logger;
    
    logger.log("시작");
    logger.log("처리 중");
    logger.log("완료");
    
    for (size_t i = 0; i < logger.size(); ++i) {
        std::cout << logger.getLog(i) << std::endl;
    }
    
    return 0;
}

고급 활용

접두사/접미사 체크 (C++20)

#include <iostream>
#include <string_view>
int main() {
    std::string_view sv = "Hello, World!";
    
    // C++20: starts_with/ends_with
    bool b1 = sv.starts_with("Hello");  // true
    bool b2 = sv.ends_with("World!");   // true
    bool b3 = sv.starts_with("Hi");     // false
    
    std::cout << std::boolalpha;
    std::cout << "starts_with(\"Hello\"): " << b1 << std::endl;
    std::cout << "ends_with(\"World!\"): " << b2 << std::endl;
    std::cout << "starts_with(\"Hi\"): " << b3 << std::endl;
    
    return 0;
}

문자열 트림

#include <cctype>
#include <iostream>
#include <string>
#include <string_view>
std::string_view trim(std::string_view s) {
    // isspace에 음수 char를 넘기면 미정의 동작이므로 unsigned char로 변환
    auto is_space = [](char c) { return std::isspace(static_cast<unsigned char>(c)) != 0; };
    size_t start = 0;
    while (start < s.size() && is_space(s[start])) {
        ++start;
    }
    
    size_t end = s.size();
    while (end > start && is_space(s[end - 1])) {
        --end;
    }
    
    return s.substr(start, end - start);
}
int main() {
    std::string str = "  Hello, World!  ";
    std::string_view trimmed = trim(str);
    
    std::cout << "[" << trimmed << "]" << std::endl;  // [Hello, World!]
    
    return 0;
}

std::isspace에 char를 그대로 넘기지 않고 unsigned char로 바꾼 데는 이유가 있습니다. <cctype> 함수들은 EOF나 unsigned char 범위의 값만 받도록 정의돼 있는데, char가 부호 있는 타입인 플랫폼(x86의 GCC, MSVC 등)에서는 한글 UTF-8 바이트처럼 0x80 이상인 문자가 음수로 전달되어 미정의 동작이 됩니다. MSVC 디버그 빌드에서는 이때 “Expression: c >= -1 && c <= 255” 어설션 창이 뜨고, 릴리스 빌드에서는 조용히 잘못된 결과를 내기도 합니다. 영어 테스트 문자열로는 절대 드러나지 않다가 한국어 입력을 처리하는 순간 터지는 대표적인 버그입니다.


문자열 비교 최적화

#include <iostream>
#include <string>
#include <string_view>
bool isCommand(std::string_view input, std::string_view command) {
    return input == command;
}
int main() {
    std::string input = "quit";
    
    // string_view로 비교 (복사 없음)
    if (isCommand(input, "quit")) {
        std::cout << "종료" << std::endl;
    } else if (isCommand(input, "help")) {
        std::cout << "도움말" << std::endl;
    }
    
    return 0;
}

성능 비교

벤치마크 1: 함수 매개변수 전달

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
void benchString(std::string s) {
    // 읽기만
}
void benchStringRef(const std::string& s) {
    // 읽기만
}
void benchStringView(std::string_view s) {
    // 읽기만
}
int main() {
    std::string str = "Hello, World! This is a test string.";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchString(str);
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringRef(str);
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    auto start3 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringView(str);
    }
    auto end3 = std::chrono::high_resolution_clock::now();
    auto time3 = std::chrono::duration_cast<std::chrono::milliseconds>(end3 - start3).count();
    
    std::cout << "string (값): " << time1 << "ms" << std::endl;
    std::cout << "const string&: " << time2 << "ms" << std::endl;
    std::cout << "string_view: " << time3 << "ms" << std::endl;
    
    return 0;
}

결과를 읽는 법: 이 코드를 -O2와 -O0로 각각 빌드해 돌려 보면 절대값은 컴파일러와 CPU마다 다르지만 경향은 비슷하게 나옵니다. 값 전달만 눈에 띄게 오래 걸리고, const std::string&와 string_view는 -O2에서 0ms 근처로 떨어집니다.

값 전달이 느린 이유는 36자 문자열이 SSO(짧은 문자열 최적화) 한도를 넘기 때문입니다. libstdc++와 MSVC는 15자, libc++는 64비트에서 22자까지만 객체 내부 버퍼에 담으므로, 이보다 긴 문자열을 값으로 넘기면 호출마다 힙 할당과 복사가 일어납니다. 반면 const std::string&와 string_view가 0ms에 가깝게 나오는 것은 함수 본문이 비어 있어 컴파일러가 호출 자체를 지웠기 때문입니다. 즉 이 벤치마크는 “둘이 같다”를 보여 주는 게 아니라 아무것도 재지 않고 있습니다. 제대로 비교하려면 함수 안에서 문자열을 실제로 읽고, 결과를 Google Benchmark의 benchmark::DoNotOptimize 같은 장치로 묶어 최적화기가 지우지 못하게 해야 합니다.

두 방식의 실제 차이는 전달 비용이 아니라 무엇을 받을 수 있느냐에 있습니다. const std::string& 매개변수에 "literal"이나 char*를 넘기면 임시 std::string이 만들어지고(길면 힙 할당), string_view는 그런 변환 없이 받습니다.


벤치마크 2: 부분 문자열

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello, World!";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string sub = str.substr(0, 5);  // 복사
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string_view sv = str;
        std::string_view sub = sv.substr(0, 5);  // 복사 없음
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    std::cout << "string::substr: " << time1 << "ms" << std::endl;
    std::cout << "string_view::substr: " << time2 << "ms" << std::endl;
    
    return 0;
}

결과를 읽는 법: 이 예제는 두 방식의 차이가 생각보다 작게 나오기 쉽고, -O2에서는 string_view 쪽 루프가 통째로 지워져 0ms가 찍히기도 합니다. 결과 값을 어디에도 쓰지 않으니 최적화기 입장에서는 없애도 되는 코드이기 때문입니다.

분석: substr(0, 5)의 결과 5글자는 SSO 한도 안이라 힙 할당이 없고 스택 버퍼에 5바이트를 복사할 뿐이어서 std::string 쪽도 빠릅니다. 부분 문자열이 길어져 SSO를 넘으면 string::substr은 호출마다 할당이 생기고, 그때 string_view::substr(포인터와 길이만 바꿈)과의 차이가 커집니다. 파서에서 토큰을 수천·수만 개 잘라 내는 코드처럼 긴 부분 문자열을 반복해서 만들 때 string_view의 이점이 가장 분명하고, 짧은 조각 몇 개라면 체감하기 어렵습니다.


실무 사례

사례 1: HTTP 요청 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
class HttpRequest {
private:
    std::string method_;
    std::string path_;
    std::unordered_map<std::string, std::string> headers_;
    
public:
    void parse(std::string_view request) {
        size_t methodEnd = request.find(' ');
        method_ = request.substr(0, methodEnd);
        
        size_t pathStart = methodEnd + 1;
        size_t pathEnd = request.find(' ', pathStart);
        path_ = request.substr(pathStart, pathEnd - pathStart);
    }
    
    std::string_view getMethod() const {
        return method_;
    }
    
    std::string_view getPath() const {
        return path_;
    }
};
int main() {
    std::string request = "GET /api/users HTTP/1.1";
    
    HttpRequest req;
    req.parse(request);
    
    std::cout << "Method: " << req.getMethod() << std::endl;
    std::cout << "Path: " << req.getPath() << std::endl;
    
    return 0;
}

이 설계는 입력은 string_view로 받아 복사 없이 훑고, 보관할 값만 std::string 멤버로 복사하는 전형적인 패턴입니다. 요청 버퍼는 다음 요청을 읽으면서 재사용되거나 해제되므로, 파싱 결과를 버퍼를 가리키는 string_view로 들고 있으면 곧 댕글링이 됩니다. 반면 요청을 처리하는 동안만 쓰고 버리는 값이라면 버퍼를 가리키는 string_view로 두는 것이 더 효율적이며, 고성능 HTTP 파서들이 이 방식으로 할당을 없앱니다. 어느 쪽이든 “이 뷰가 가리키는 버퍼는 언제까지 살아 있는가”를 코드 구조로 보장해야 합니다.

getMethod()가 멤버 std::string의 뷰를 반환하는 것은 HttpRequest 객체가 살아 있는 동안에는 안전하지만, HttpRequest{}.getMethod()처럼 임시 객체에서 호출하면 반환 즉시 댕글링이 됩니다. 또 find가 npos를 돌려주는 경우(공백이 없는 잘못된 요청)를 확인하지 않아 methodEnd + 1이 0이 되는 등 엉뚱한 결과가 나오므로, 외부 입력을 파싱할 때는 각 find 결과를 검사해야 합니다.


사례 2: CSV 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
std::vector<std::string_view> parseCSVLine(std::string_view line) {
    std::vector<std::string_view> fields;
    size_t start = 0;
    
    while (start < line.size()) {
        size_t end = line.find(',', start);
        if (end == std::string_view::npos) {
            fields.push_back(line.substr(start));
            break;
        }
        
        fields.push_back(line.substr(start, end - start));
        start = end + 1;
    }
    
    return fields;
}
int main() {
    std::string line = "홍길동,30,서울";
    auto fields = parseCSVLine(line);
    
    std::cout << "이름: " << fields[0] << std::endl;
    std::cout << "나이: " << fields[1] << std::endl;
    std::cout << "주소: " << fields[2] << std::endl;
    
    return 0;
}

사례 3: 로그 필터링

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class LogFilter {
public:
    bool shouldLog(std::string_view level, std::string_view message) {
        if (level == "DEBUG") {
            return false;  // DEBUG 레벨 필터링
        }
        
        if (message.find("password") != std::string_view::npos) {
            return false;  // 민감 정보 필터링
        }
        
        return true;
    }
};
int main() {
    LogFilter filter;
    
    std::string log1 = "DEBUG: 디버그 메시지";
    std::string log2 = "INFO: 사용자 로그인";
    std::string log3 = "ERROR: password 오류";
    
    if (filter.shouldLog("DEBUG", log1)) {
        std::cout << log1 << std::endl;
    }
    
    if (filter.shouldLog("INFO", log2)) {
        std::cout << log2 << std::endl;
    }
    
    if (filter.shouldLog("ERROR", log3)) {
        std::cout << log3 << std::endl;
    }
    
    return 0;
}

사례 4: 명령어 처리

#include <iostream>
#include <string>
#include <string_view>
class CommandHandler {
public:
    void handleCommand(std::string_view cmd) {
        if (cmd == "quit" || cmd == "exit") {
            std::cout << "종료" << std::endl;
        } else if (cmd.starts_with("echo ")) {
            std::string_view msg = cmd.substr(5);
            std::cout << msg << std::endl;
        } else if (cmd == "help") {
            std::cout << "도움말" << std::endl;
        } else {
            std::cout << "알 수 없는 명령어" << std::endl;
        }
    }
};
int main() {
    CommandHandler handler;
    
    handler.handleCommand("echo Hello");
    handler.handleCommand("help");
    handler.handleCommand("quit");
    
    return 0;
}

트러블슈팅

문제 1: 댕글링 포인터

증상: 소멸된 문자열 참조

// ❌ 댕글링
std::string_view getSuffix() {
    std::string str = "Hello, World!";
    return std::string_view(str).substr(7);  // "World!"
}  // str 소멸
int main() {
    auto sv = getSuffix();
    std::cout << sv << std::endl;  // ❌ 소멸된 문자열 참조
    
    return 0;
}
// 짧은 문자열이면 SSO 버퍼가 스택에 있어 우연히 맞는 값이 찍히기도 함
// → "테스트에선 됐는데" 가 가장 위험한 형태의 UB
// ✅ string 반환
std::string getSuffix() {
    std::string str = "Hello, World!";
    return str.substr(7);  // string 반환
}
int main() {
    auto s = getSuffix();
    std::cout << s << std::endl;  // ✅ OK
    
    return 0;
}

문제 2: 임시 string에서 string_view

증상: 임시 객체 소멸

// ❌ 임시 객체
std::string_view sv = std::string("Hello");  // 임시 객체
std::cout << sv << std::endl;  // ❌ 임시 객체 이미 소멸
// ✅ string 저장
std::string s = std::string("Hello");
std::string_view sv = s;
std::cout << sv << std::endl;  // ✅ OK
// 또는 string_view 리터럴
using namespace std::string_view_literals;
std::string_view sv2 = "Hello"sv;
std::cout << sv2 << std::endl;  // ✅ OK

문자열 리터럴에서 만든 string_view가 안전한 이유는 리터럴이 프로그램 전체 수명 동안 존재하는 정적 저장소에 있기 때문입니다. 위험한 것은 함수가 반환한 std::string에서 만드는 경우입니다. std::string_view name = user.getName();에서 getName()이 const std::string&을 반환하면 안전하지만, std::string을 값으로 반환하도록 나중에 바뀌면 같은 코드가 조용히 댕글링으로 바뀝니다. 리팩터링 한 줄로 멀리 떨어진 코드가 깨지는 전형적인 경우입니다. 이런 버그는 AddressSanitizer(-fsanitize=address)로 실행하면 heap-use-after-free나 stack-use-after-scope 리포트와 함께 문제가 된 줄을 알려 주므로, string_view를 많이 쓰는 코드는 ASan을 켠 테스트를 꼭 돌려 보는 것이 좋습니다.


문제 3: null 종료 보장 안 됨

증상: C API 연동 오류

#include <cstdio>
#include <string>
#include <string_view>
int main() {
    std::string str = "Hello";
    std::string_view sv = str;
    
    // ❌ null 종료 보장 안 됨
    const char* cstr = sv.data();
    printf("%s\n", cstr);  // 위험 (null 종료 보장 안 됨)
    
    // ✅ string으로 변환
    std::string s(sv);
    const char* cstr2 = s.c_str();  // null 종료 보장
    printf("%s\n", cstr2);
    
    return 0;
}

문제 3-1: remove_prefix는 원본을 바꾸지 않는다

std::string s = "  hello";
std::string_view v = s;
v.remove_prefix(2);        // v == "hello", s는 여전히 "  hello"
// v[0] = 'H';             // ❌ 에러: string_view는 읽기 전용

remove_prefix·remove_suffix·substr은 뷰의 시작 위치와 길이만 바꿉니다. 트림 함수를 string_view로 만들면 복사 없이 빠르지만, 결과를 저장해 두려면 원본이 살아 있어야 하고, 원본을 고치려면 std::string 쪽을 직접 수정해야 합니다.

문제 4: string_view를 멤버 변수로 저장

증상: 댕글링 포인터

// ❌ string_view를 멤버로 저장
class Logger {
private:
    std::vector<std::string_view> logs_;  // ❌ 위험
    
public:
    void log(std::string_view msg) {
        logs_.push_back(msg);  // 원본 수명 주의
    }
};
// ✅ string으로 저장
class Logger {
private:
    std::vector<std::string> logs_;  // ✅ 안전
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};

마무리

string_view는 문자열 복사를 제거해 성능을 높이는 강력한 도구입니다.

핵심 요약

  1. string vs string_view
    • string: 소유, 수정 가능
    • string_view: 참조, 읽기 전용
  2. 선택 기준
    • 읽기만 하는 함수 매개변수: string_view
    • 받아서 저장하는 함수 매개변수: string을 값으로 받고 std::move
    • 저장: string
    • 수정: string&
    • C API: string::c_str()
  3. 성능
    • 매개변수 전달: string_view ≈ const string& >>> string (값)
    • 부분 문자열: string_view >>> string
    • 저장: string (string_view는 위험)
  4. 주의사항
    • 댕글링 포인터 주의
    • null 종료 보장 안 됨
    • 임시 객체 주의
    • 멤버 변수로 저장 금지

선택 가이드

상황권장이유
함수 매개변수 (읽기)string_view복사 없음
멤버 변수 (저장)string소유권
함수 매개변수 (수정)string&수정 가능
C API 연동string::c_str()null 종료
임시 객체 생성string소유권 필요

코드 예제 치트시트

// 함수 매개변수: string_view
void print(std::string_view s) {
    std::cout << s << std::endl;
}
// 저장: string
class Logger {
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};
// 부분 문자열: string_view
std::string str = "Hello, World!";
std::string_view sv = str;
std::string_view sub = sv.substr(0, 5);  // 복사 없음
// C API: c_str()
std::string s = "Hello";
printf("%s\n", s.c_str());

다음 단계

참고 자료


자주 묻는 질문 (FAQ)

Q. string_view의 data()를 printf나 C API에 바로 넘기면 왜 위험한가요?

A. string_view는 문자열의 시작 포인터와 길이만 들고 있어서, 가리키는 범위 끝에 널 문자가 있다는 보장이 없습니다. 특히 부분 문자열로 만든 string_view라면 data()가 가리키는 곳 뒤에 원래 문자열이 이어져 있어, %s로 출력하면 의도보다 긴 내용이 찍히거나 버퍼 끝을 넘어 읽을 수 있습니다. 널 종료 문자열이 필요한 C API에는 std::string으로 변환해서 c_str()을 넘기거나, printf라면 %.*s에 길이를 함께 넘겨야 합니다.


같이 보면 좋은 글