C++ stringstream | 문자열 파싱·변환·포맷팅
이 글의 핵심
C++ stringstream 완벽 가이드. istringstream으로 문자열 파싱, ostringstream으로 포맷팅, stringstream으로 타입 변환, CSV 파싱, 숫자↔문자열 변환, setw·setprecision 포맷 조정, 실전 문자열 처리 패턴을 상세히 설명합니다.
들어가며: 문자열을 숫자로 바꾸다가 크래시
“123abc”를 int로 변환하려다 실패했다
사용자 입력을 받아서 숫자로 변환하는 코드를 작성했습니다. 하지만 잘못된 입력이 들어오면 프로그램이 죽었습니다.
문제의 코드:
std::string input = "123abc";
int value = std::stoi(input); // ❌ 예외 발생!
위 코드 설명: stoi는 숫자가 아닌 문자가 나오면 예외를 던지고, “123abc”처럼 앞부분만 숫자인 경우 123만 추출하는 것도 지원하지 않습니다. 사용자 입력처럼 잘못된 값이 자주 들어오는 경로에서는 stringstream으로 >> 후 성공 여부를 확인하는 편이 안전합니다.
원인:
stoi는 변환 실패 시 예외 던짐- 부분 변환 불가 (123만 추출 못 함)
- 에러 처리 복잡
stringstream(문자열을 메모리 상의 스트림처럼 읽고 쓰게 해 주는 클래스)은 “문자열을 스트림처럼 읽고 쓰는” 도구라서, >>로 타입별로 끊어 읽거나 부분만 파싱할 수 있습니다. 예외 대신 if (ss >> value)로 성공 여부를 확인할 수 있어서, 사용자 입력·로그 파싱처럼 실패가 자주 나는 경로에서 안전하게 쓸 수 있습니다.
정리: “한 줄에 숫자랑 문자가 섞여 있을 때” 앞부분만 숫자로 읽고 나머지는 그대로 두는 식의 파싱에는 std::stringstream이 편하고, 단순히 정수 하나만 필요하면 std::stoi+try-catch나 C++17 std::from_chars(문자열→숫자 변환을 예외 없이, 오류는 반환값으로 알리는 함수)도 선택지입니다.
해결 후 (아래는 복사해 붙여넣은 뒤 g++ -std=c++17 -o sstream_parse sstream_parse.cpp && ./sstream_parse 로 실행 가능):
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o sstream_parse sstream_parse.cpp && ./sstream_parse
#include <sstream>
#include <iostream>
#include <string>
int main() {
std::string input = "123abc";
std::stringstream ss(input);
int value;
if (ss >> value) {
std::cout << "Parsed: " << value << "\n"; // 123
std::string rest;
ss >> rest;
std::cout << "Remaining: " << rest << "\n"; // abc
} else {
std::cout << "Parse failed\n";
}
return 0;
}
위 코드 설명: stringstream에 문자열을 넣고 ss >> value로 읽으면, 공백·숫자가 아닌 문자 전까지만 파싱해 123을 얻습니다. 실패 시 ss는 fail 상태가 되므로 if (ss >> value)로 성공 여부를 확인할 수 있고, 나머지 “abc”는 ss >> rest로 따로 읽을 수 있습니다.
실행 결과: Parsed: 123 과 Remaining: abc 가 각각 한 줄씩 출력됩니다.
이 글을 읽으면:
- stringstream으로 문자열을 파싱할 수 있습니다.
- 타입 변환을 안전하게 할 수 있습니다.
- 문자열을 조립하고 포맷팅할 수 있습니다.
- 실전에서 유용한 문자열 처리 패턴을 익힐 수 있습니다.
목차
- stringstream 기초
- 문자열 파싱
- 문자열 조립
- 포맷팅
- 매니퓰레이터 완전 가이드
- 사용자 정의 타입 포맷팅
- C++20 std::format
- 자주 발생하는 문제와 해결법
- 성능 최적화 팁
- 프로덕션 패턴
- 실전 패턴
문제 시나리오: 실제로 겪는 stringstream 문제들
시나리오 1: 로그 파일 파싱에서 숫자가 0으로 나온다
로그 형식: "2024-03-10 14:23:45 [INFO] User 12345 connected"
문제: ss >> timestamp >> level >> userId로 파싱했는데 userId가 항상 0이다.
원인: [INFO]는 공백으로 구분된 토큰이 아니라 [로 시작하는 하나의 토큰이다. >>는 [INFO] 전체를 읽어서 int 변환에 실패하고, 스트림이 fail 상태가 된다.
// ❌ 잘못된 파싱
std::istringstream iss("2024-03-10 14:23:45 [INFO] User 12345 connected");
std::string timestamp, level;
int userId;
iss >> timestamp >> level >> userId; // level이 "[INFO]"가 되고, userId는 0
시나리오 2: 포맷 플래그가 다음 출력에 영향
// ❌ 의도치 않은 결과
std::ostringstream oss;
oss << std::hex << 255; // "ff"
oss << std::showbase << 256; // "0x100" - hex가 아직 적용됨!
원인: hex, dec, oct, fixed, scientific 등은 한 번 설정하면 계속 유지된다. 다음 출력 전에 std::dec나 std::defaultfloat로 초기화해야 한다.
시나리오 3: stringstream 재사용 시 이전 데이터가 남는다
std::stringstream ss;
ss << "Hello";
std::string s1;
ss >> s1; // s1 = "Hello"
ss << "World"; // "Hello" 뒤에 "World"가 붙음!
std::string s2;
ss >> s2; // s2 = "World" (읽기 위치가 끝에 있어서)
원인: str()로 내용을 바꾸지 않고 <<만 하면 기존 버퍼에 이어 붙는다. 재사용 시 ss.clear(); ss.str("");로 초기화해야 한다.
1. stringstream 기초
stringstream이란?
**std::stringstream**은 메모리 안의 문자열을 스트림처럼 다룹니다. <<로 숫자·문자열을 이어 붙이면 내부 버퍼에 쌓이고, **str()**로 최종 std::string을 꺼낼 수 있습니다. std::cout처럼 쓰되 출력 대상이 화면이 아니라 문자열이라서, “문자열을 조립할 때” sprintf 대신 타입 안전하게 쓸 수 있습니다.
#include <sstream>
// 문자열을 스트림처럼 다룸
std::stringstream ss;
ss << "Hello " << 123 << " " << 3.14;
std::string result = ss.str();
std::cout << result << "\n"; // "Hello 123 3.14"
위 코드 설명: stringstream에 <<로 문자열·숫자를 이어 붙이면 메모리 버퍼에 쌓이고, str()로 최종 std::string을 꺼냅니다. cout처럼 쓰되 출력 대상이 문자열이라 sprintf 없이 타입 안전하게 문자열을 조립할 수 있습니다.
특징:
- 메모리 내 문자열 스트림
<<연산자로 쓰기>>연산자로 읽기str()메서드로 문자열 추출
stringstream 클래스 관계도
flowchart TB
subgraph "스트림 계층"
ios[basic_ios]
istream[basic_istream]
ostream[basic_ostream]
iostream[basic_iostream]
end
subgraph "stringstream 계층"
istringstream[istringstream]
ostringstream[ostringstream]
stringstream[stringstream]
end
ios --> istream
ios --> ostream
istream --> iostream
ostream --> iostream
istream --> istringstream
ostream --> ostringstream
iostream --> stringstream
style istringstream fill:#e1f5fe
style ostringstream fill:#fff3e0
style stringstream fill:#e8f5e9
위 다이어그램 설명: istringstream은 읽기 전용, ostringstream은 쓰기 전용, stringstream은 읽기·쓰기 모두 가능하다. 모두 basic_ios를 상속해 fail(), eof(), clear() 등의 상태 메서드를 공유한다.
istringstream vs ostringstream
**istringstream**은 이미 있는 문자열에서만 읽기(>>)가 가능하고, **ostringstream**은 쓰기(<<)만 해서 문자열을 만든 뒤 str()로 꺼냅니다. **stringstream**은 읽기·쓰기 둘 다 가능해서, 같은 스트림에 쓰고 나서 다시 읽는 패턴(예: 포맷 후 파싱)에 씁니다. 용도에 맞춰 골라 쓰면 의도가 드러나고 실수도 줄어듭니다.
// 읽기 전용
std::istringstream iss("123 456");
int a, b;
iss >> a >> b;
// 쓰기 전용
std::ostringstream oss;
oss << "Result: " << 42;
std::string result = oss.str();
// 읽기/쓰기
std::stringstream ss;
ss << "Data";
std::string data;
ss >> data;
위 코드 설명: istringstream은 생성자에 넘긴 문자열에서만 >>로 읽고, ostringstream은 <<로만 쓰다가 str()로 결과 문자열을 얻습니다. stringstream은 읽기·쓰기 모두 가능해 같은 버퍼에 썼다가 다시 읽는 패턴에 씁니다.
2. 문자열 파싱
기본 타입 추출
std::string input = "42 3.14 hello";
std::stringstream ss(input);
int i;
double d;
std::string s;
ss >> i >> d >> s;
std::cout << "int: " << i << "\n"; // 42
std::cout << "double: " << d << "\n"; // 3.14
std::cout << "string: " << s << "\n"; // hello
위 코드 설명: >>는 공백으로 구분된 토큰을 타입에 맞게 추출합니다. int·double·string 순서로 선언했으므로 “42”, “3.14”, “hello”가 각각 i, d, s에 들어갑니다. 같은 스트림을 여러 번 >>로 읽으면 순서대로 소비됩니다.
안전한 변환
template <typename T>
bool tryParse(const std::string& str, T& result) {
std::stringstream ss(str);
ss >> result;
// 변환 성공 && 전체 문자열 소비
return !ss.fail() && ss.eof();
}
int main() {
int value;
if (tryParse("123", value)) {
std::cout << "Parsed: " << value << "\n";
}
if (!tryParse("123abc", value)) {
std::cout << "Parse failed\n";
}
}
위 코드 설명: stringstream에 str을 넣고 >> result로 읽은 뒤, fail()이 아니고 eof()이면 “전체 문자열이 해당 타입 하나로 정확히 변환된” 경우입니다. “123abc”는 숫자 뒤에 문자가 남으므로 eof()가 아니어서 false가 됩니다.
공백으로 구분된 데이터
std::string line = "10 20 30 40 50";
std::stringstream ss(line);
std::vector<int> numbers;
int num;
while (ss >> num) {
numbers.push_back(num);
}
// numbers: {10, 20, 30, 40, 50}
위 코드 설명: while (ss >> num)은 읽기가 성공하는 동안만 루프를 돌아, 공백으로 구분된 정수를 끝까지 벡터에 넣습니다. 스트림이 끝나거나 타입이 맞지 않으면 루프가 끝납니다.
CSV 파싱
std::vector<std::string> parseCSV(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 csv = "Alice,25,Engineer";
auto fields = parseCSV(csv);
// fields: {"Alice", "25", "Engineer"}
}
위 코드 설명: getline(ss, cell, ’,‘)는 구분자 ’,’ 전까지를 cell에 넣고, 루프로 반복하면 한 줄을 쉼표 기준으로 나눈 벡터를 얻습니다. CSV 한 줄을 필드 배열로 바꿀 때 쓰는 기본 패턴입니다.
복잡한 파싱
struct Person {
std::string name;
int age;
double salary;
};
Person parsePerson(const std::string& str) {
// 형식: "Name:Alice Age:25 Salary:50000.5"
std::stringstream ss(str);
Person p;
std::string token;
while (ss >> token) {
size_t pos = token.find(':');
if (pos == std::string::npos) continue;
std::string key = token.substr(0, pos);
std::string value = token.substr(pos + 1);
if (key == "Name") {
p.name = value;
} else if (key == "Age") {
p.age = std::stoi(value);
} else if (key == "Salary") {
p.salary = std::stod(value);
}
}
return p;
}
위 코드 설명: ss >> token으로 “Name:Alice” 같은 토큰을 하나씩 읽고, find(’:‘)로 키와 값을 나눈 뒤 key가 “Name”/“Age”/“Salary”일 때마다 value를 해당 타입으로 변환해 구조체에 넣습니다. 키=값 형태가 반복되는 문자열 파싱 패턴입니다.
3. 문자열 조립
기본 조립
std::ostringstream oss;
oss << "User: " << "Alice" << "\n";
oss << "Age: " << 25 << "\n";
oss << "Score: " << 95.5 << "\n";
std::string result = oss.str();
위 코드 설명: ostringstream에 <<로 여러 줄을 이어 붙이고, str()로 한 번에 문자열을 꺼냅니다. cout에 쓰던 것처럼 포맷만 바꿔 쓰면 되고, 재할당 없이 버퍼가 커지므로 반복 연결보다 효율적일 수 있습니다.
반복문에서 조립
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::ostringstream oss;
oss << "[";
for (size_t i = 0; i < numbers.size(); ++i) {
if (i > 0) oss << ", ";
oss << numbers[i];
}
oss << "]";
std::string result = oss.str(); // "[1, 2, 3, 4, 5]"
위 코드 설명: [ 로 시작해 숫자들을 ”, “로 구분해 붙이고 ] 로 닫습니다. 첫 항목만 쉼표 없이 넣기 위해 first 플래그로 구분하는 흔한 패턴입니다.
SQL 쿼리 생성
std::string buildQuery(const std::string& table,
const std::map<std::string, std::string>& conditions) {
std::ostringstream oss;
oss << "SELECT * FROM " << table << " WHERE ";
bool first = true;
for (const auto& [key, value] : conditions) {
if (!first) oss << " AND ";
oss << key << " = '" << value << "'";
first = false;
}
return oss.str();
}
int main() {
std::map<std::string, std::string> cond = {
{"name", "Alice"},
{"age", "25"}
};
std::string query = buildQuery("users", cond);
// "SELECT * FROM users WHERE name = 'Alice' AND age = '25'"
}
위 코드 설명: “SELECT * FROM ” + table + ” WHERE ” 에 이어서, map의 각 key=value를 ” AND “로 연결해 조건 절을 만듭니다. first로 첫 조건만 구분해 앞에 ” AND “가 붙지 않게 합니다. 동적 쿼리 조립에 ostringstream이 자주 쓰입니다.
4. 포맷팅
정수 포맷
#include <iomanip>
std::ostringstream oss;
// 너비 지정
oss << std::setw(5) << 42; // " 42"
// 0으로 채우기
oss << std::setfill('0') << std::setw(5) << 42; // "00042"
// 16진수
oss << std::hex << 255; // "ff"
oss << std::showbase << std::hex << 255; // "0xff"
// 8진수
oss << std::oct << 64; // "100"
위 코드 설명: setw(n)은 최소 너비, setfill(c)는 빈 자리 채울 문자, hex/oct는 진법을 바꿉니다. showbase면 0x, 0 같은 접두사가 붙습니다. iomanip과 플래그로 정수 포맷을 제어할 수 있습니다.
실수 포맷
std::ostringstream oss;
double pi = 3.14159265359;
// 고정 소수점
oss << std::fixed << std::setprecision(2) << pi; // "3.14"
// 과학적 표기법
oss << std::scientific << pi; // "3.14e+00"
// 기본 (자동 선택)
oss << std::defaultfloat << pi; // "3.14159"
위 코드 설명: fixed는 고정 소수점(setprecision이 소수 자리 수), scientific은 지수 표기, defaultfloat는 원래대로 자동 선택입니다. 포맷 플래그는 한 번 설정하면 다음 출력까지 유지됩니다.
정렬
std::ostringstream oss;
// 왼쪽 정렬
oss << std::left << std::setw(10) << "Name" << "Age\n";
oss << std::left << std::setw(10) << "Alice" << 25 << "\n";
// 오른쪽 정렬
oss << std::right << std::setw(10) << "Total" << 100 << "\n";
// 결과:
// Name Age
// Alice 25
// Total100
위 코드 설명: left/right로 정렬 방향을 정하고 setw로 열 너비를 맞춥니다. left면 왼쪽 정렬, right면 오른쪽 정렬이라 숫자나 제목을 열 맞춰 출력할 때 씁니다.
테이블 포맷
void printTable(const std::vector<std::tuple<std::string, int, double>>& data) {
std::ostringstream oss;
// 헤더
oss << std::left << std::setw(15) << "Name"
<< std::right << std::setw(10) << "Age"
<< std::right << std::setw(15) << "Salary" << "\n";
oss << std::string(40, '-') << "\n";
// 데이터
for (const auto& [name, age, salary] : data) {
oss << std::left << std::setw(15) << name
<< std::right << std::setw(10) << age
<< std::right << std::setw(15) << std::fixed
<< std::setprecision(2) << salary << "\n";
}
std::cout << oss.str();
}
int main() {
std::vector<std::tuple<std::string, int, double>> data = {
{"Alice", 25, 50000.50},
{"Bob", 30, 60000.75},
{"Charlie", 28, 55000.00}
};
printTable(data);
}
위 코드 설명: 헤더 행에 setw로 열 너비를 맞추고, 구분선을 string(40,’-‘)으로 넣은 뒤, 각 행을 left/right와 setw로 같은 너비로 출력합니다. fixed와 setprecision으로 소수 자리도 맞추면 표 형태 문자열을 만들 수 있습니다.
5. 매니퓰레이터 완전 가이드
<iomanip>과 <ios>의 모든 주요 매니퓰레이터를 한눈에 정리합니다.
정수 매니퓰레이터
#include <iomanip>
#include <sstream>
#include <iostream>
int main() {
std::ostringstream oss;
int n = 42;
// 진법: dec(10), hex(16), oct(8)
oss << std::dec << n; // "42"
oss.str(""); oss << std::hex << n; // "2a"
oss.str(""); oss << std::oct << n; // "52"
// 접두사: showbase (0x, 0)
oss.str(""); oss << std::showbase << std::hex << 255; // "0xff"
// 대문자: uppercase
oss.str(""); oss << std::uppercase << std::hex << 255; // "0XFF"
// 너비·채우기: setw, setfill (setw는 다음 출력에만 적용!)
oss.str(""); oss << std::setw(6) << std::setfill('0') << 42; // "000042"
// noshowbase, nouppercase로 초기화
oss.str(""); oss << std::noshowbase << std::nouppercase << std::dec;
std::cout << oss.str() << "\n";
return 0;
}
실수 매니퓰레이터
#include <iomanip>
#include <sstream>
void demonstrateFloatManipulators() {
std::ostringstream oss;
double pi = 3.14159265359;
// fixed: 고정 소수점, setprecision = 소수 자릿수
oss << std::fixed << std::setprecision(2) << pi; // "3.14"
// scientific: 지수 표기
oss.str(""); oss << std::scientific << std::setprecision(2) << pi; // "3.14e+00"
// defaultfloat: 자동 선택 (원래대로)
oss.str(""); oss << std::defaultfloat << std::setprecision(6) << pi; // "3.14159"
// showpoint: 소수점 항상 표시
oss.str(""); oss << std::showpoint << 42.0; // "42.000000"
}
정렬·부호 매니퓰레이터
// left, right, internal
oss << std::left << std::setw(10) << "Name"; // "Name "
oss << std::right << std::setw(10) << 42; // " 42"
oss << std::internal << std::setw(10) << -42; // "- 42"
// showpos: 양수에 + 표시
oss << std::showpos << 42; // "+42"
oss << std::noshowpos; // 초기화
매니퓰레이터 적용 순서 주의
// setfill은 setw보다 먼저 써도 됨 (상태로 유지)
// setw는 "다음 한 번" 출력에만 적용
oss << std::setfill('-') << std::setw(8) << 42; // "------42"
oss << 100; // "100" (setw 적용 안 됨! setfill은 적용됨)
6. 사용자 정의 타입 포맷팅
operator<<와 operator>>를 오버로드하면 stringstream으로 사용자 정의 타입을 직렬화·역직렬화할 수 있다.
기본 operator<< 오버로드
#include <sstream>
#include <string>
#include <iostream>
struct Point {
double x, y;
// ostream에 출력 (cout, ostringstream 모두 사용 가능)
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "(" << p.x << ", " << p.y << ")";
}
// istream에서 입력
friend std::istream& operator>>(std::istream& is, Point& p) {
char open, comma, close;
if (is >> open >> p.x >> comma >> p.y >> close &&
open == '(' && comma == ',' && close == ')') {
return is;
}
is.setstate(std::ios::failbit);
return is;
}
};
int main() {
Point p{3.14, 2.71};
std::ostringstream oss;
oss << "Point: " << p; // "Point: (3.14, 2.71)"
std::cout << oss.str() << "\n";
std::istringstream iss("(1.0, 2.0)");
Point p2;
if (iss >> p2) {
std::cout << "Parsed: " << p2.x << ", " << p2.y << "\n";
}
return 0;
}
enum과 stringstream
enum class LogLevel { Debug, Info, Warning, Error };
std::ostream& operator<<(std::ostream& os, LogLevel level) {
switch (level) {
case LogLevel::Debug: return os << "DEBUG";
case LogLevel::Info: return os << "INFO";
case LogLevel::Warning: return os << "WARNING";
case LogLevel::Error: return os << "ERROR";
default: return os << "UNKNOWN";
}
}
std::istream& operator>>(std::istream& is, LogLevel& level) {
std::string s;
if (is >> s) {
if (s == "DEBUG") level = LogLevel::Debug;
else if (s == "INFO") level = LogLevel::Info;
else if (s == "WARNING") level = LogLevel::Warning;
else if (s == "ERROR") level = LogLevel::Error;
else is.setstate(std::ios::failbit);
}
return is;
}
포맷 옵션을 가진 커스텀 출력
struct Money {
long long cents;
friend std::ostream& operator<<(std::ostream& os, const Money& m) {
// fixed, setprecision과 함께 사용
return os << std::fixed << std::setprecision(2)
<< (m.cents / 100.0) << " USD";
}
};
7. C++20 std::format
C++20부터 std::format이 도입되어, Python 스타일의 포맷 문자열로 타입 안전하게 문자열을 만들 수 있다. stringstream보다 가독성과 성능 면에서 유리한 경우가 많다.
기본 사용법
#include <format>
#include <string>
#include <iostream>
int main() {
// g++ -std=c++20 필요
std::string s = std::format("Hello, {}!", "World"); // "Hello, World!"
std::cout << s << "\n";
int a = 42;
double b = 3.14;
std::string s2 = std::format("a={}, b={:.2f}", a, b); // "a=42, b=3.14"
std::cout << s2 << "\n";
return 0;
}
format vs stringstream 비교
// stringstream
std::ostringstream oss;
oss << "User " << name << " (ID: " << std::setw(6) << std::setfill('0')
<< id << ") logged in";
std::string msg = oss.str();
// C++20 format (동일 결과)
std::string msg = std::format("User {} (ID: {:06d}) logged in", name, id);
format 지정자
// 정수: d(10진수), x(16진수), o(8진수), b(2진수)
std::format("{:d}", 255); // "255"
std::format("{:x}", 255); // "ff"
std::format("{:06x}", 255); // "0000ff"
// 실수: f(고정), e(지수), g(자동)
std::format("{:.2f}", 3.14159); // "3.14"
std::format("{:.2e}", 3.14159); // "3.14e+00"
// 정렬·채우기
std::format("{:<10}", "left"); // "left "
std::format("{:>10}", "right"); // " right"
std::format("{:*^10}", "mid"); // "*mid**"
format이 stringstream보다 나은 경우
| 상황 | stringstream | std::format |
|---|---|---|
| 로케일 독립 | 복잡 | 기본 로케일 독립 |
| 포맷 가독성 | setw, setfill 등 여러 줄 | 한 줄 "{:06d}" |
| 성능 | 동적 할당·스트림 오버헤드 | 컴파일 타임 포맷 검사, 더 적은 할당 |
| 타입 안전성 | 런타임 오류 가능 | 컴파일 타임 검사 |
주의: std::format은 C++20 이상 필요. MSVC 2019 16.10+, GCC 13+, Clang 14+에서 지원.
8. 자주 발생하는 문제와 해결법
문제 1: eof() 체크 없이 파싱 후 추가 읽기
// ❌ 잘못된 코드
std::istringstream iss("123");
int a, b;
iss >> a >> b; // b는 0, 읽기 실패
if (!iss.fail()) { // b 읽기 실패했지만 fail()은 아직 false일 수 있음
// ...
}
해결: eof()로 전체 소비 여부 확인, 또는 fail() 체크
// ✅ 올바른 코드
std::istringstream iss("123");
int a, b;
iss >> a >> b;
if (iss && iss.eof()) {
// a, b 모두 정상 파싱되고 전체 문자열 소비됨
}
문제 2: getline과 >> 혼용 시 줄바꿈 처리
// ❌ 의도치 않은 빈 줄
std::istringstream iss("42\nhello");
int n;
std::string s;
iss >> n; // "42" 읽음, '\n'은 버퍼에 남음
std::getline(iss, s); // s = "" (줄바꿈만 읽음!)
해결: >> 후 iss.ignore() 또는 getline으로 빈 줄 스킵
// ✅ 올바른 코드
iss >> n;
iss.ignore(1, '\n'); // 또는 std::getline(iss, dummy);
std::getline(iss, s); // s = "hello"
문제 3: CSV 셀에 쉼표가 포함된 경우
입력: "Alice,\"Engineer, Senior\",25"
단순 getline(ss, cell, ',')는 "Engineer, Senior"를 제대로 못 나눔
해결: 따옴표 감싼 필드는 getline으로 한 번에 읽은 뒤, 수동으로 따옴표를 제거하는 로직을 추가한다. 또는 전문 CSV 라이브러리(libcsv 등) 사용을 고려한다.
문제 4: clear() 없이 str()만 호출
// ❌ fail 상태가 남아 있음
std::stringstream ss("abc");
int n;
ss >> n; // 실패, ss.fail() == true
ss.str("123"); // 내용만 바꿈
ss >> n; // 여전히 실패! (fail 비트가 남아 있음)
해결: clear()로 상태 초기화
// ✅ 올바른 코드
ss.clear();
ss.str("123");
ss >> n; // 성공
문제 5: 포맷 플래그 누수
// ❌ 다음 코드에 hex가 영향
void logHex(int n) {
std::ostringstream oss;
oss << std::hex << n;
return oss.str();
}
// 이후 다른 ostringstream에서 dec 기대했는데 hex로 출력됨 (같은 스트림 재사용 시)
해결: 함수 내 로컬 스트림 사용(이미 그렇게 함), 또는 사용 후 std::dec로 복원
// ✅ 스트림을 로컬로 쓰면 다른 코드에 영향 없음
// 공유 스트림을 쓴다면: oss << std::dec;
9. 성능 최적화 팁
1. reserve로 사전 할당
// ostringstream은 내부적으로 버퍼를 확장함
// 반복 조립 시 str() 호출 횟수 줄이기
std::ostringstream oss;
oss << "prefix";
for (int i = 0; i < 1000; ++i) {
oss << "," << i; // 내부 버퍼가 필요 시 자동 확장
}
std::string result = oss.str();
팁: 매우 큰 문자열을 만들 때는 std::string::reserve로 미리 공간을 잡고 +=로 붙이는 것도 고려. 단, stringstream이 타입 변환·포맷팅을 한 번에 처리해 주므로 편의성과 성능을 trade-off해서 선택.
2. from_chars로 순수 숫자 변환
// 숫자→문자열, 문자열→숫자만 필요할 때
#include <charconv>
std::string intToString(int n) {
char buf[32];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), n);
return std::string(buf, ptr);
}
bool stringToInt(const std::string& s, int& out) {
int value;
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
return ec == std::errc{} && ptr == s.data() + s.size();
}
장점: 예외 없음, stringstream보다 빠름. 단점: 포맷팅(소수점, 정렬 등)은 직접 구현해야 함.
3. stringstream 재사용 시 clear + str
// ❌ 매번 새로 생성 (할당 비용)
for (const auto& line : lines) {
std::istringstream iss(line);
// 파싱...
}
// ✅ 하나 재사용
std::istringstream iss;
for (const auto& line : lines) {
iss.clear();
iss.str(line);
// 파싱...
}
4. C++20 format 사용
// 성능·가독성 모두 유리
auto msg = std::format("{}: {} ({} bytes)", timestamp, level, size);
10. 프로덕션 패턴
패턴 1: 스레드 안전 로그 버퍼
#include <sstream>
#include <mutex>
#include <iostream>
class ThreadSafeLogger {
std::ostringstream buffer;
std::mutex mtx;
public:
template <typename T>
ThreadSafeLogger& operator<<(const T& value) {
std::lock_guard lock(mtx);
buffer << value;
return *this;
}
~ThreadSafeLogger() {
std::lock_guard lock(mtx);
std::cout << "[LOG] " << buffer.str() << "\n";
}
};
패턴 2: 에러 메시지 빌더
class ErrorBuilder {
std::ostringstream oss;
public:
ErrorBuilder& context(const std::string& ctx) {
oss << "[" << ctx << "] ";
return *this;
}
ErrorBuilder& code(int c) {
oss << "code=" << c << " ";
return *this;
}
ErrorBuilder& detail(const std::string& d) {
oss << d;
return *this;
}
std::string build() { return oss.str(); }
};
// 사용: throw std::runtime_error(ErrorBuilder().context("DB").code(1001).detail("connection failed").build());
패턴 3: 설정 파일 파싱 (키=값)
std::map<std::string, std::string> parseConfig(const std::string& content) {
std::map<std::string, std::string> config;
std::istringstream iss(content);
std::string line;
while (std::getline(iss, line)) {
if (line.empty() || line[0] == '#') continue;
size_t pos = line.find('=');
if (pos != std::string::npos)
config[line.substr(0, pos)] = line.substr(pos + 1);
}
return config;
}
패턴 4: 버전 문자열 파싱
struct Version {
int major, minor, patch;
friend std::istream& operator>>(std::istream& is, Version& v) {
char dot;
return is >> v.major >> dot >> v.minor >> dot >> v.patch;
}
};
// "1.2.3" -> Version{1,2,3}
11. 실전 패턴
패턴 1: 문자열 분할
std::vector<std::string> split(const std::string& str, char delimiter) {
std::vector<std::string> result;
std::stringstream ss(str);
std::string token;
while (std::getline(ss, token, delimiter)) {
result.push_back(token);
}
return result;
}
int main() {
std::string path = "/home/user/documents/file.txt";
auto parts = split(path, '/');
// {"", "home", "user", "documents", "file.txt"}
}
위 코드 설명: getline(ss, token, delimiter)로 구분자 전까지를 token에 넣고, 루프로 반복해 벡터에 넣습니다. ’/‘로 split하면 경로가 “home”, “user”, “documents”, “file.txt”처럼 나뉘고, 맨 앞 빈 문자열은 첫 문자가 구분자일 때 나옵니다.
패턴 2: 문자열 조인
template <typename Container>
std::string join(const Container& items, const std::string& delimiter) {
std::ostringstream oss;
bool first = true;
for (const auto& item : items) {
if (!first) oss << delimiter;
oss << item;
first = false;
}
return oss.str();
}
int main() {
std::vector<std::string> words = {"Hello", "World", "C++"};
std::string result = join(words, ", ");
// "Hello, World, C++"
}
위 코드 설명: 첫 항목만 구분자 없이 넣고, 그 다음부터는 delimiter를 앞에 붙여서 이어 붙입니다. ostringstream으로 조립한 뒤 str()로 반환하면 vector를 “Hello, World, C++” 같은 하나의 문자열로 만들 수 있습니다.
패턴 3: 타입 변환 헬퍼
template <typename T>
std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}
template <typename T>
T fromString(const std::string& str) {
std::istringstream iss(str);
T value;
iss >> value;
return value;
}
int main() {
int num = 42;
std::string str = toString(num); // "42"
int parsed = fromString<int>("123"); // 123
double d = fromString<double>("3.14"); // 3.14
}
위 코드 설명: toString은 ostringstream에 <<로 넣어 str()로 반환하고, fromString은 istringstream에서 >>로 읽어 반환합니다. operator<<를 지원하는 타입이면 어떤 타입이든 문자열과 상호 변환할 수 있는 헬퍼입니다.
패턴 4: 로그 메시지 생성
class Logger {
std::ostringstream buffer;
public:
template <typename T>
Logger& operator<<(const T& value) {
buffer << value;
return *this;
}
~Logger() {
std::cout << "[LOG] " << buffer.str() << "\n";
}
};
int main() {
Logger() << "User " << "Alice" << " logged in at " << 1234567890;
// [LOG] User Alice logged in at 1234567890
}
위 코드 설명: Logger 임시 객체에 <<로 메시지를 이어 붙이면 buffer에 쌓이고, 소멸자에서 “[LOG] ” + buffer.str()을 cout에 출력합니다. 스트림처럼 썼다가 한 번에 로그로 남기는 RAII 패턴입니다.
패턴 5: 명령줄 파싱
std::map<std::string, std::string> parseArgs(const std::string& cmdline) {
std::map<std::string, std::string> args;
std::stringstream ss(cmdline);
std::string token;
while (ss >> token) {
if (token.starts_with("--")) {
size_t pos = token.find('=');
if (pos != std::string::npos) {
std::string key = token.substr(2, pos - 2);
std::string value = token.substr(pos + 1);
args[key] = value;
} else {
args[token.substr(2)] = "true";
}
}
}
return args;
}
int main() {
std::string cmd = "--host=localhost --port=8080 --debug";
auto args = parseArgs(cmd);
// args: {{"host", "localhost"}, {"port", "8080"}, {"debug", "true"}}
}
위 코드 설명: 공백으로 토큰을 나누고, — 로 시작하면 key=value 형태인지 확인해 ’=’ 위치로 자르고, = 가 없으면 값은 “true”로 둡니다. —host=localhost —debug 같은 명령줄을 map으로 파싱하는 패턴입니다.
패턴 6: JSON 간단 생성
class JsonBuilder {
std::ostringstream oss;
bool first = true;
public:
JsonBuilder() { oss << "{"; }
JsonBuilder& add(const std::string& key, const std::string& value) {
if (!first) oss << ",";
oss << "\"" << key << "\":\"" << value << "\"";
first = false;
return *this;
}
JsonBuilder& add(const std::string& key, int value) {
if (!first) oss << ",";
oss << "\"" << key << "\":" << value;
first = false;
return *this;
}
std::string build() {
oss << "}";
return oss.str();
}
};
int main() {
std::string json = JsonBuilder()
.add("name", "Alice")
.add("age", 25)
.add("city", "Seoul")
.build();
// {"name":"Alice","age":25,"city":"Seoul"}
}
위 코드 설명: add(key, value)나 add(key, int)로 키-값을 이어 붙이고, 첫 항목만 쉼표 없이 넣기 위해 first 플래그를 씁니다. build()에서 } 로 닫고 str()로 반환해 간단한 JSON 객체 문자열을 만듭니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ optional·variant·any | “nullptr 체크 지겹다” C++17 타입 안전 처리
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
- C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
이 글에서 다루는 키워드 (관련 검색어)
C++ stringstream, istringstream ostringstream, 문자열 파싱, 포맷팅, setw setprecision, CSV 파싱 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 용도 |
|---|---|
| stringstream | 읽기/쓰기 |
| istringstream | 읽기 전용 |
| ostringstream | 쓰기 전용 |
| >> | 파싱 |
| << | 조립 |
| str() | 문자열 추출 |
| setw() | 너비 지정 |
| setprecision() | 소수점 자릿수 |
핵심 원칙:
- 파싱은
istringstream과>> - 조립은
ostringstream과<< - 안전한 변환은 실패 체크
- 포맷팅은
<iomanip>활용 - 재사용 시
clear()+str("")
구현 체크리스트
- 파싱 시
fail()또는eof()체크 -
getline과>>혼용 시ignore()처리 - stringstream 재사용 시
clear()+str("") - 포맷 플래그 사용 후
dec/defaultfloat등으로 복원 - C++20 이상이면
std::format검토 - 성능 중요 시
std::from_chars/std::to_chars고려
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 문자열 파싱(로그, CSV, 설정 파일), 타입 변환(숫자↔문자열), 포맷팅(테이블, 로그 메시지), SQL/JSON 조립 등에 활용합니다. C++20 이상이면 std::format도 함께 검토하세요.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: stringstream으로 문자열 파싱·포맷팅을 타입 안전하게 할 수 있습니다. 다음으로 auto와 decltype(#12-1)를 읽어보면 좋습니다.
이전 글: C++ 실전 가이드 #11-2: 바이너리 파일과 직렬화
다음 글: C++ 실전 가이드 #12-1: auto와 decltype
관련 글
- C++ 파일 입출력 | ifstream·ofstream으로
- C++ 문자열 기초 완벽 가이드 | std::string·C 문자열·string_view와 실전 패턴
- C++ 바이너리 직렬화 |
- C++ 문자열 알고리즘 완벽 가이드 | split·join·trim·replace·정규식 [실전]
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴