1. 소개
초보자가 std::cin >> n; 을 작성할 때, 이것을 종종 마법 주문처럼 받아들입니다. “숫자 좀 입력해 줘.” 하지만 std::cin은 텔레파시도 아니고 심리학자도 아닙니다. 이 객체가 하는 일은 딱 하나입니다. 입력 스트림에서 필요한 형식의 데이터를 시도해서 추출합니다. 실패하면 그 문제를 내부 상태에 기록합니다. 그리고 여기서 중요한 점은, 이 상태가 이후의 모든 읽기 연산에 영향을 준다는 것입니다.
std::cin을 계산원이라고 생각해 보면, 연산자 >>는 “바코드를 스캔하려는 시도”입니다. 바코드가 없고 숫자 대신 글자가 들어오면, 계산원은 기분에 따라 가격을 추측하지 않습니다. “처리할 수 없습니다”라고 말하고, 오류 표시등이 켜지며, 여러분이 문제를 해결하기 전까지 계산대는 정상적으로 계속 동작하지 않습니다.
기술적인 세부 사항도 하나 있습니다. C++의 입출력 스트림은 템플릿 클래스로 구성되어 있습니다 (basic_istream, basic_ostream 등). 그리고 익숙한 std::cin은 char 스트림을 위한 구체적인 객체입니다. 오늘 실습에 꼭 필요한 내용은 아니지만, 알아 두면 좋습니다. “istream”은 하나의 “마법 클래스”가 아니라 타입 계열입니다.
스트림 상태 플래그: good/eof/fail/bad
우리가 “std::cin의 상태”라고 말할 때는 내부 플래그들의 집합을 뜻합니다. 이들은 good(), eof(), fail(), bad() 메서드로 확인할 수 있습니다. 이 메서드들은 아무것도 “고치지” 않습니다. 단지 지금 스트림이 어떤 상태에 있는지를 정직하게 알려 줄 뿐입니다.
쉽게 기억하자면, 상태란 지금 우리가 읽기를 계속할 수 있는지 없는지를 설명하는 이유입니다.
| 검사 | 쉽게 말하면 | 전형적인 원인 |
|---|---|---|
|
“모든 것이 정상, 계속 읽을 수 있음” | 오류가 없음 |
|
“더 이상 데이터가 없음” | 입력 끝(파일이 끝났거나 Web‑IDE에서 더 이상 입력이 주어지지 않음) |
|
“형식이 맞지 않아 파싱하지 못함” | int를 기대했는데 abc를 받음 |
|
“스트림/장치 수준에서 심각한 문제가 발생함” | 심각한 입력 오류(초보자에게는 드문 경우) |
헷갈리지 말아야 할 점이 있습니다. good()은 별도의 “최상위 진실”이 아니라, 오히려 “오류 플래그가 없다”에 가깝습니다. 실전에서는 good()을 덜 쓰는 편입니다. “스트림이 좋으면 읽어 보자”보다 “읽기에 성공하면 계속 작업하자”라고 생각하는 편이 더 편리하기 때문입니다.
2. 검사와 함께 읽기: if (std::cin >> x)
C++에서 가장 중요한 패턴 중 하나는, 특히 문제 풀이에서, 다음과 같은 형태입니다.
#include <iostream>
int main() {
int n = 0;
if (std::cin >> n) {
std::cout << "n = " << n << '\n'; // 예: n = 42
} else {
std::cout << "Input failed\n"; // Input failed
}
}
여기에는 메커니즘을 한 번만 이해하면 “이상한 꼼수” 같은 것은 없습니다.
표현식 std::cin >> n 은 스트림을 반환합니다(참조로). 그리고 스트림은 bool로 변환될 수 있습니다. 즉, “내가 지금 정상 상태인가 아닌가?”를 뜻합니다. 그래서 if (std::cin >> n) 은 이렇게 읽으면 됩니다. “n을 읽어 보자. 성공하면 then 분기를 실행하라.”
이 형태가 좋은 이유는, 실수로 쓰레기 값을 사용하는 일을 막아 주기 때문입니다. 입력에 실패하면 변수는 이전 값을 그대로 유지할 수 있고, 그 상태에서 검사 없이 사용하면 논리 오류가 됩니다.
왜 “변수에 어쨌든 뭔가 들어 있다”는 것이 나쁜 소식인가
심리적으로는 “그래도 x가 출력됐으니 읽힌 거 아닌가?”라는 함정에 빠지기 쉽습니다. 하지만 C++에서 “변수가 출력됐다”는 말은 그 변수 안에 어떤 값이 들어 있다는 뜻일 뿐입니다. 그 값이 어디서 왔는지는 이제 여러분의 책임입니다.
위험한 예를 보겠습니다.
#include <iostream>
int main() {
int n = 100; // 기본값이 이미 있다고 가정
std::cin >> n; // 입력이 잘못되면 n은 100으로 남을 수 있음
std::cout << "n=" << n << '\n'; // n=100 ("oops"를 입력해도)
}
사용자가 oops를 입력하면, 출력은 n=100이 될 수 있습니다. 그러면 초보자는 “오, 잘 되네!”라고 생각합니다. 그런데 그 “100”이 갑자기 할인 계산, 배열 길이, 벡터 크기, 반복 횟수 계산 등에 들어가면 그때부터 난장판이 시작됩니다.
핵심은 단순합니다. 읽기 성공을 확인하기 전까지는 변수의 값을 신뢰하지 마세요.
3. fail()과 형식 오류 뒤의 “고착” 현상
초보자가 “내 프로그램이 멈췄어요”라고 말할 때 가장 흔한 원인은 failbit, 즉 fail() 상태입니다.
숫자를 기다리고 있다고 가정해 봅시다.
- 입력: 10 — 아주 좋음
- 입력: abc — 숫자가 아님
두 번째 경우 스트림은 int를 추출할 수 없습니다. 그럼 무엇을 할까요?
스트림은 fail 플래그를 세우고, “잘못된 문자들”(a, b, c)은 입력 버퍼에 그대로 남겨 둡니다. 그 이후의 모든 std::cin >> int 시도는 다시 같은 abc를 보게 되고, 다시 실패합니다. 그래서 “끝없이 같은 날이 반복되는” 효과가 생깁니다. 숫자를 요구하는데, 스트림은 계속 같은 글자에 다시 부딪히는 것입니다.
최소 예제:
#include <iostream>
int main() {
int a = 0;
int b = 0;
std::cin >> a;
std::cin >> b;
std::cout << "fail=" << std::cin.fail() << '\n';
std::cout << "a=" << a << " b=" << b << '\n';
}
사용자가 123 abc를 입력하면, 대략 다음과 같은 상황을 보게 됩니다.
- a는 읽힘: 123
- b는 읽히지 않음. abc가 int로 변환되지 않기 때문
- fail=1
- b는 0 그대로 남음(또는 이전 값이 있었다면 그 값)
핵심 결론은 다음과 같습니다. fail()이 발생한 뒤에는 스트림이 스스로 “회복”되지 않습니다. 이렇게 설계된 이유는 명확합니다. 그렇지 않으면 프로그램은 아무 일도 없었던 것처럼 계속 진행되고, 여러분은 조용하지만 매우 불쾌한 오류를 얻게 될 것입니다.
다음 강의에서는 스트림을 올바르게 “수리”하는 법을 배우겠지만, 오늘은 메커니즘 자체를 분명히 이해하는 것이 중요합니다. fail()은 “한 번 실패했다”가 아니라 “고쳐 주기 전까지는 오류 상태에 있다”는 뜻입니다.
4. eof()와 bad(): 멈추는 이유는 서로 다르다
왜 eof()를 미리 검사하면 안 되는가
초보자는 EOF(end-of-file, 입력 끝)를 두고 종종 철학적인 비극을 겪습니다. 스트림에게 “아직 데이터가 남아 있나요?”라고 먼저 묻고, 그다음에 읽고 싶어집니다. 하지만 실제로는 eof()가 읽기 시도가 실패한 뒤에야 참이 되는 경우가 많습니다. 즉, 스트림이 “이제 더 이상 데이터가 없네”라고 깨달은 뒤입니다.
그래서 올바른 접근은 대개 이렇습니다. “읽히는 동안 읽어라.”
#include <iostream>
int main() {
int x = 0;
while (std::cin >> x) {
std::cout << "read: " << x << '\n';
}
if (std::cin.eof()) {
std::cout << "Stopped by EOF\n";
} else if (std::cin.fail()) {
std::cout << "Stopped by format error\n";
} else if (std::cin.bad()) {
std::cout << "Stopped by bad stream\n";
}
}
이 루프는 서로 다른 두 시나리오를 깔끔하게 처리합니다.
- 사용자/플랫폼이 숫자 집합을 주었고 입력이 끝남 → 루프가 종료되고 eof()는 true가 됨
- 사용자가 중간에 “형식을 깨뜨림” → 루프가 종료되고 fail()는 true가 됨
멈춘 이유가 다르기 때문에, 우리는 이 둘을 구분해야 합니다.
bad() — 드물지만 중요한 “빨간 버튼”
bad()에 대해서는 솔직하게 말할 수 있습니다. Web‑IDE의 과제에서는 이것을 자주 보지 못할 것입니다. 하지만 이것이 존재한다는 사실은 알아야 합니다. 적어도 오류의 성격을 올바르게 이해하려면 필요합니다.
fail()이 “형식을 파싱할 수 없음”이라면, bad()는 “입력 스트림 자체가 망가졌다”에 가깝습니다. 예를 들어 장치 문제, 심각한 버퍼 오류 등이 있습니다. 일반적인 학습용 문제에서는 거의 나타나지 않지만, 파일이나 네트워크를 읽는 실제 프로그램에서는 충분히 발생할 수 있습니다.
초보자에게 실용적인 태도는 다음과 같습니다. bad()가 발생하면 보통 작업을 중단합니다. 여러분 수준에서 “수리”할 수 있는 일이 아닌 경우가 많기 때문입니다. 이 수업에서는 bad()를 “사고”, fail()을 “사용자 오류”로 생각하겠습니다.
5. 상태 미니 자동자로서의 읽기
입력이 있는 프로그램을 작성할 때는 간단한 다이어그램을 머릿속에 두는 것이 좋습니다. 읽기 시도는 스트림을 몇 가지 상태 중 하나로 옮기며, 여러분의 코드는 그에 반응해야 합니다.
flowchart TD
A["읽기 시도: std::cin >> x"] --> B{성공?}
B -- "예" --> C["x를 사용하고 계속 진행"]
B -- "아니오" --> D{원인은?}
D -- "EOF" --> E["종료: 더 이상 데이터가 없음"]
D -- "fail" --> F["형식 오류: 사용자가 잘못 입력함"]
D -- "bad" --> G["심각한 스트림 오류: 비정상 종료"]
이것은 “이론을 위한 이론”이 아닙니다. 오히려 여러분의 반(反)디버거입니다. 프로그램이 이상하게 동작할 때 달의 위상을 보고 추측하는 대신, “우리가 fail에 들어갔나? eof인가?”라고 스스로 묻게 해 줍니다.
6. 실습: 명령을 읽고 올바르게 종료하기
이 주제를 공중에 띄워 두지 않기 위해, 학습용 애플리케이션 쪽으로 작은 한 걸음을 옮겨 보겠습니다.
예를 들어, 앞서 struct를 다루던 날에 우리는 간단한 작업 모델을 만들었다고 해 봅시다.
- id — 정수
- title — 공백 없는 짧은 단어(주제를 섞지 않기 위해 아직 getline은 사용하지 않음)
오늘은 잘못된 입력을 “치료”하지는 않을 것입니다(그건 다음 강의 주제입니다). 대신 왜 멈췄는지 그 이유를 올바르게 판별하는 법을 배우겠습니다.
#include <iostream>
#include <string>
#include <vector>
struct Task {
int id = 0;
std::string title;
};
int main() {
std::vector<Task> tasks;
Task t;
while (std::cin >> t.id >> t.title) {
tasks.push_back(t);
}
std::cout << "Loaded tasks: " << tasks.size() << '\n';
if (std::cin.eof()) {
std::cout << "Reason: EOF\n";
} else if (std::cin.fail()) {
std::cout << "Reason: format error\n";
} else if (std::cin.bad()) {
std::cout << "Reason: bad stream\n";
}
}
이 코드는 다음과 같이 동작합니다.
입력이 다음과 같다면:
1 wash
2 code
3 sleep
그 뒤 입력이 끝나면, Reason: EOF를 얻게 됩니다.
입력이 다음과 같다면:
1 wash
two code
3 sleep
루프는 two code 줄에서 멈추고, Reason: format error를 출력합니다. 이것이 정직한 동작입니다. 모든 것이 괜찮은 척하지 않습니다.
여기서 주목할 아이디어는 이것입니다. 아직 스트림을 복구하는 방법을 모른다면, 가장 안전한 선택은 멈추고 이유를 알려 주는 것입니다. 그것만으로도 프로그램은 훨씬 예측 가능해집니다.
7. std::cin 상태를 다룰 때 흔한 실수
실수 1: std::cin >> x를 확인하지 않고 데이터를 사용하는 것.
가장 흔한 시나리오는 이렇습니다. int를 읽고, 확인하지 않고, 그다음 x를 계산에 사용합니다. 입력이 잘못되면 여러분은 “오류”가 아니라 “조용한 쓰레기 값”을 얻게 됩니다(변수의 이전 값). 해결 방법은 규율입니다. if (std::cin >> x)를 쓰거나, while (std::cin >> x) 루프를 쓰세요.
실수 2: 읽기 전에 eof()를 검사하고 그 위에 로직을 세우는 것.
eof()는 보통 읽으려 했지만 데이터 끝 때문에 실패한 뒤에야 참이 됩니다. 그래서 “eof가 아니면 읽어라” 같은 로직은 불필요한 한 번의 반복과 이상한 동작을 만들기 쉽습니다. 더 안전한 방법은 “읽히는 동안 읽기”입니다. 즉, while (std::cin >> x) 입니다.
실수 3: fail()와 eof()를 혼동하고, 둘 다에 대해 “실패했습니다”라는 한 문장만 쓰는 것.
입력 끝과 형식 오류는 서로 다른 상황입니다. 하나는 더 이상 데이터가 없는 것이고(정상), 다른 하나는 사용자가 잘못 입력한 것입니다(시나리오 오류). 이 둘을 섞어 버리면 무엇이 망가졌는지 이해할 수 없고, 프로그램의 올바른 동작 규칙도 만들 수 없습니다.
실수 4: fail() 뒤에도 계속 읽으면서 저절로 고쳐지기를 기대하는 것.
fail() 이후에는 스트림이 앞으로 나아가지 않고, 잘못된 문자들은 보통 버퍼에 그대로 남아 있습니다. 이후의 읽기 연산은 그 쓰레기를 “건너뛰지” 않습니다. 그래서 프로그램이 “멈춘 것 같고” 또는 “입력을 무시하는 것처럼” 보이는 느낌이 생깁니다. 다음 강의에서는 스트림 복구를 통해 이것을 어떻게 고치는지 다루겠지만, 오늘은 사실 자체를 기억하는 것이 중요합니다. fail()은 끈적한 상태입니다.
실수 5: bad()를 fail()과 같은 것으로 보고 무시하는 것.
학습용 문제에서는 드물더라도, 의미상 bad()는 더 심각한 상황입니다. fail()은 흔히 “사용자가 잘못 입력함”을 의미하고, bad()는 “입력 메커니즘 자체가 망가짐”에 가깝습니다. 기초 과정 수준에서 bad()에 대한 합리적인 반응은 작업을 종료하고, “아무 일도 없었던 것처럼 계속 진행”하려 하지 않는 것입니다.
GO TO FULL VERSION