C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교
이 글의 핵심
C++ string vs string_view 차이점. string_view는 복사 없이 문자열을 참조하는 경량 타입으로, 함수 매개변수에 유리합니다. 저장은 string, 읽기는 string_view를 사용하세요.
들어가며
C++17의 string_view는 문자열을 복사하지 않고 참조만 하는 경량 타입입니다. 함수 매개변수로 사용하면 복사 비용을 제거할 수 있습니다.
비유로 말씀드리면, string은 책을 사서 책장에 꽂는 것(소유), string_view는 책 제목만 적힌 메모를 들고 원본 책을 가리키는 것(비소유)에 가깝습니다. 메모만 남기고 원본을 반납하면 내용을 읽을 수 없습니다(댕글링).
이 글을 읽으면
- string과 string_view의 차이를 이해합니다
- 성능 비교와 복사 비용을 파악합니다
- 함수 매개변수 선택 기준을 익힙니다
- 댕글링 포인터 주의사항을 확인합니다
목차
string vs string_view 비교
비교표
| 항목 | std::string | std::string_view |
|---|---|---|
| 소유권 | 소유 (힙 할당) | 비소유 (참조만) |
| 복사 비용 | 높음 (문자 복사) | 낮음 (포인터 복사) |
| 수정 | 가능 | 불가능 (읽기 전용) |
| 크기 | 32바이트 (구현 의존) | 16바이트 (포인터 + 길이) |
| 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_; // 길이
};
실전 구현
1) 함수 매개변수
#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;
}
2) 부분 문자열
#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;
}
3) 문자열 파싱
#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
4) 로깅
#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;
}
고급 활용
1) 접두사/접미사 체크 (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;
}
2) 문자열 트림
#include <iostream>
#include <string_view>
std::string_view trim(std::string_view s) {
size_t start = 0;
while (start < s.size() && std::isspace(s[start])) {
++start;
}
size_t end = s.size();
while (end > start && std::isspace(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;
}
3) 문자열 비교 최적화
#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;
}
결과 (GCC 13, -O3):
| 매개변수 타입 | 시간 | 상대 속도 |
|---|---|---|
| std::string (값) | 850ms | 17x |
| const std::string& | 50ms | 1.0x |
| std::string_view | 50ms | 1.0x |
분석: string_view는 const string& 와 동일한 성능
벤치마크 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;
}
결과 (GCC 13, -O3):
| 방법 | 시간 | 상대 속도 |
|---|---|---|
| string::substr | 120ms | 24x |
| string_view::substr | 5ms | 1.0x |
분석: string_view가 24배 빠름 (복사 없음)
실무 사례
사례 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;
}
사례 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;
}
// ✅ 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
문제 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;
}
문제 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는 문자열 복사를 제거해 성능을 높이는 강력한 도구입니다.
핵심 요약
-
string vs string_view
- string: 소유, 수정 가능
- string_view: 참조, 읽기 전용
-
선택 기준
- 함수 매개변수: string_view
- 저장: string
- 수정: string&
- C API: string::c_str()
-
성능
- 매개변수 전달: string_view ≈ const string& >>> string (값)
- 부분 문자열: string_view >>> string
- 저장: string (string_view는 위험)
-
주의사항
- 댕글링 포인터 주의
- 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());
다음 단계
- string 기초: C++ string 기초
- string_view: C++ string_view
- 문자열 최적화: C++ 문자열 최적화
참고 자료
- “Effective Modern C++” - Scott Meyers
- “C++ Primer” - Stanley Lippman
- cppreference: https://en.cppreference.com/w/cpp/string/basic_string_view
한 줄 정리: 함수 매개변수는 string_view로 복사를 제거하고, 저장은 string으로 소유권을 확보하며, 원본 수명을 항상 주의한다.