C++ 조건문: if/else/switch 사용법과 흔한 실수
이 글의 핵심
if/else와 비교·논리 연산자, switch 문과 fall-through, 삼항 연산자, 중첩 조건문까지 C++ 조건문의 기본을 예제로 정리합니다. = 와 == 혼동, break 누락, 부동소수점 비교처럼 초보자가 자주 하는 실수도 함께 짚습니다.
if 문 기본
// 변수 선언 및 초기화
int age = 20;
if (age >= 20) {
cout << "성인입니다" << endl;
}
if의 괄호 안 조건은 bool로 변환되어 평가됩니다. age >= 20 같은 비교식은 true/false를 만들고, 정수나 포인터를 그대로 넣으면 0이나 nullptr은 거짓, 나머지는 참으로 취급됩니다. 그래서 if (count)는 “count가 0이 아니면”이라는 뜻이 되는데, 짧아서 편하지만 초보 단계에서는 if (count != 0)처럼 의도를 적어 두는 편이 읽기 쉽습니다. 본문이 한 줄뿐이면 중괄호를 생략할 수도 있지만, 나중에 한 줄을 추가하면서 중괄호를 빠뜨리면 두 번째 줄은 조건과 상관없이 실행됩니다. 들여쓰기만 보면 조건 안에 있는 것처럼 보여서 찾기 어려운 버그가 되므로, 한 줄짜리라도 항상 중괄호를 쓰는 습관을 권합니다.
if-else
// 변수 선언 및 초기화
int score = 85;
if (score >= 90) {
cout << "A" << endl;
} else if (score >= 80) {
cout << "B" << endl;
} else if (score >= 70) {
cout << "C" << endl;
} else {
cout << "F" << endl;
}
else if 사슬은 위에서부터 차례로 검사하다가 처음으로 참인 분기 하나만 실행하고 나머지는 건너뜁니다. 그래서 두 번째 조건을 score >= 80 && score < 90처럼 쓰지 않아도 됩니다. 여기까지 왔다면 이미 score >= 90이 거짓이라는 뜻이기 때문입니다. 반대로 순서를 score >= 70부터 쓰면 85점도 첫 조건에 걸려 “C”가 되므로, 범위 조건은 큰 값부터(또는 작은 값부터 일관되게) 배치해야 합니다. 마지막 else는 위의 어떤 조건에도 맞지 않는 모든 경우를 받는 안전망입니다.
비교 연산자
int a = 10, b = 20;
if (a == b) // 같음
if (a != b) // 다름
if (a < b) // 작음
if (a > b) // 큼
if (a <= b) // 작거나 같음
if (a >= b) // 크거나 같음
위 코드는 연산자 목록을 보여 주려고 본문 없이 나열한 것이라 그대로는 컴파일되지 않습니다. 비교 연산자에서 알아 두면 좋은 함정은 두 가지입니다. 하나는 수학식처럼 if (0 < x < 10)이라고 쓰는 것인데, C++에서는 (0 < x) < 10으로 해석되어 앞의 결과(0 또는 1)를 10과 비교하므로 항상 참입니다. 범위 검사는 if (0 < x && x < 10)처럼 두 비교를 &&로 이어야 합니다. 다른 하나는 부호 있는 정수와 부호 없는 정수의 비교입니다. int i = -1;과 v.size()(부호 없는 size_t)를 비교하면 -1이 아주 큰 양수로 변환되어 i < v.size()가 거짓이 됩니다. GCC/Clang은 -Wall -Wextra에서 comparison of integer expressions of different signedness 경고로 알려 줍니다.
논리 연산자
int age = 25;
bool hasLicense = true;
// AND (&&): 둘 다 참
if (age >= 18 && hasLicense) {
cout << "운전 가능" << endl;
}
// OR (||): 하나라도 참
if (age < 18 || !hasLicense) {
cout << "운전 불가" << endl;
}
// NOT (!): 반대
if (!hasLicense) {
cout << "면허 없음" << endl;
}
&&와 ||는 단락 평가(short-circuit evaluation)를 합니다. A && B에서 A가 거짓이면 B는 아예 계산하지 않고, A || B에서 A가 참이면 B를 건너뜁니다. 이 성질 덕분에 if (ptr != nullptr && ptr->isValid())처럼 앞 조건이 뒤 조건의 안전을 보장하는 코드를 쓸 수 있습니다. 순서를 바꿔 ptr->isValid() && ptr != nullptr로 쓰면 널 포인터를 먼저 역참조해 크래시가 납니다. 같은 이유로 &&/|| 오른쪽에 i++나 함수 호출처럼 부수 효과가 있는 식을 두면, 왼쪽 결과에 따라 실행되지 않을 수도 있다는 점을 기억해야 합니다. 또 &와 |는 비트 연산자라서 단락 평가를 하지 않으니 &&와 혼동하지 않도록 주의하세요.
switch 문
int day = 3;
switch (day) {
case 1:
cout << "월요일" << endl;
break;
case 2:
cout << "화요일" << endl;
break;
case 3:
cout << "수요일" << endl;
break;
default:
cout << "기타" << endl;
}
switch는 괄호 안의 값을 계산한 뒤 같은 값을 가진 case 레이블로 바로 점프합니다. case 값은 컴파일 시간에 정해지는 정수(또는 열거형) 상수여야 하므로 int, char, enum 등은 쓸 수 있지만 double이나 std::string은 쓸 수 없고, case x:처럼 변수를 쓰면 the value of 'x' is not usable in a constant expression 에러가 납니다. 같은 값의 case가 두 개 있으면 duplicate case value 에러입니다. default는 일치하는 case가 없을 때 실행되며, 위치는 어디든 상관없지만 관례상 맨 끝에 둡니다.
enum class로 상태를 표현하고 switch로 분기할 때는 default를 일부러 빼는 선택도 있습니다. 그러면 나중에 열거형에 값을 추가했을 때 GCC/Clang이 -Wswitch 경고(enumeration value 'Paused' not handled in switch)로 처리하지 않은 곳을 모두 알려 주기 때문입니다. default가 있으면 새 값이 조용히 default로 흘러가서 이 경고를 받을 수 없습니다.
switch vs if-else
// switch: 값이 정확히 일치할 때
switch (grade) {
case 'A': cout << "우수" << endl; break;
case 'B': cout << "보통" << endl; break;
}
// if-else: 범위 비교
if (score >= 90) {
cout << "A" << endl;
} else if (score >= 80) {
cout << "B" << endl;
}
선택 기준은 단순합니다. 하나의 값이 여러 상수 중 무엇과 같은지를 가르면 switch가, 범위나 여러 변수를 섞은 조건이면 if-else가 자연스럽습니다. 성능 차이를 걱정할 필요는 거의 없습니다. 컴파일러는 case 값이 촘촘하면 switch를 점프 테이블(값을 인덱스로 바로 분기)로 만들고, 반대로 단순한 if-else 사슬을 switch와 같은 코드로 최적화하기도 합니다. 읽는 사람에게 의도가 더 잘 드러나는 쪽을 고르면 됩니다.
switch와 fall-through
switch는 일치하는 case로 점프한 뒤, break가 없으면 아래 case 코드까지 이어서 실행합니다. 이 동작을 fall-through라고 부릅니다.
의도적인 fall-through
여러 값이 같은 처리를 할 때는 case를 쌓아 두는 패턴이 흔합니다. 뒤에 나오는 윤년 예제의 case 1: case 3: ...도 같은 방식입니다.
switch (cmd) {
case 'q':
case 'Q':
case 27: // ESC 등 (환경에 따라)
quit(); // 예: 프로그램 종료를 담당하는 함수
break;
default:
break;
}
C++17부터는 의도적인 fall-through임을 명시하려면 [[fallthrough]] 속성을 쓸 수 있습니다. 코드 리뷰에서 “실수가 아니다”라는 신호가 됩니다. GCC와 Clang은 -Wimplicit-fallthrough(GCC는 -Wextra에 포함) 옵션을 켜면 break 없이 다음 case로 넘어가는 곳마다 this statement may fall through 경고를 내는데, [[fallthrough]];가 있는 곳은 의도된 것으로 보고 경고하지 않습니다. 위의 case 'q': case 'Q':처럼 사이에 문장이 없는 레이블 나열은 경고 대상이 아닙니다.
switch (n) {
case 1:
doFirst();
[[fallthrough]]; // 다음 case로 일부러 넘어감
case 2:
doSecond();
break;
}
실수로 인한 fall-through
break를 빼먹으면 다음 case의 코드까지 실행되어 의도하지 않은 출력이나 중복 처리가 생깁니다(아래 “자주 하는 실수” 절의 break 예제 참고). 각 case가 독립된 처리라면 break를 습관적으로 넣는 것이 안전합니다. 마지막 case나 default에는 break가 없어도 동작은 같지만, 나중에 그 아래에 새 case를 추가하면서 fall-through가 생기는 일이 많으므로 마지막에도 넣어 두는 편이 좋습니다. 함수 안에서 case마다 바로 return하는 경우(윤년 예제의 getDaysInMonth)는 return이 함수를 빠져나가므로 break가 필요 없습니다.
삼항 연산자
// 조건 ? 참일때 : 거짓일때
int age = 20;
string result = (age >= 20) ? "성인" : "미성년자";
// if-else와 동일
string result;
if (age >= 20) {
result = "성인";
} else {
result = "미성년자";
}
위 코드는 두 방식을 비교하려고 result를 두 번 선언했기 때문에 한 함수 안에 그대로 두면 redeclaration 에러가 납니다. 삼항 연산자의 장점은 값을 만드는 식이라는 점입니다. if-else는 문장이라 먼저 빈 변수를 만들고 나중에 대입해야 하지만, 삼항 연산자는 const string result = ...;처럼 선언과 동시에 초기화할 수 있어서 const와 잘 어울립니다. 반면 a ? (b ? x : y) : z처럼 중첩하면 금방 읽기 어려워지므로, 분기가 두 개를 넘으면 if-else나 별도 함수로 빼는 것이 좋습니다. 두 결과의 타입이 달라도 문제가 됩니다. cond ? "성인" : string("미성년자")처럼 한쪽이 const char*, 다른 쪽이 string이면 공통 타입으로 변환되지만, 서로 변환할 수 없는 타입이면 operands to ?: have different types 에러가 납니다. 또 ?:와 >=는 <<보다 우선순위가 낮아서 cout << age >= 20 ? "성인" : "미성년자";는 (cout << age) >= 20 ...으로 해석되어 컴파일 에러가 납니다. 출력할 때는 cout << (age >= 20 ? "성인" : "미성년자");처럼 전체를 괄호로 감싸야 합니다.
중첩 조건문
int age = 25;
bool hasTicket = true;
if (age >= 18) {
if (hasTicket) {
cout << "입장 가능" << endl;
} else {
cout << "티켓 필요" << endl;
}
} else {
cout << "미성년자 입장 불가" << endl;
}
중첩이 두세 단계를 넘어가면 오른쪽으로 계속 밀려나는 “화살표 코드”가 되어 어떤 else가 어떤 if의 짝인지 헷갈리기 시작합니다. 함수 안이라면 예외 상황을 먼저 처리하고 빠져나가는 조기 반환(early return)으로 중첩을 줄일 수 있습니다. if (age < 18) { cout << "미성년자 입장 불가"; return; }를 먼저 두면 그 아래 코드는 “성인”이라는 전제 위에서 한 단계 얕게 쓸 수 있습니다. 그리고 중괄호 없이 중첩하면 else는 가장 가까운 짝 없는 if에 붙는다는 규칙(dangling else) 때문에 들여쓰기와 실제 동작이 달라질 수 있으므로, 중첩 조건에서는 특히 중괄호를 생략하지 말아야 합니다.
C++17부터는 if 안에서 변수를 선언하고 바로 검사할 수 있습니다. if (auto it = scores.find(name); it != scores.end()) { ... }처럼 쓰면 it의 범위가 if/else 블록 안으로 제한되어, 검사용 임시 변수가 함수 전체에 남지 않습니다. switch (auto c = getCommand(); c) { ... }처럼 switch에도 같은 문법을 쓸 수 있습니다.
자주 하는 실수
실수 1: = vs ==
int x = 10;
// ❌ 잘못된 코드 (대입)
if (x = 5) { // x에 5를 대입하고 true
cout << "항상 실행됨" << endl;
}
// ✅ 올바른 코드 (비교)
if (x == 5) {
cout << "x가 5일 때만 실행" << endl;
}
x = 5는 대입식이고, 대입식의 결과는 대입된 값(5)이라 if (5), 즉 항상 참이 됩니다. 게다가 x의 값까지 5로 바뀌어 버려서 그 뒤의 코드도 모두 영향을 받습니다. 문법상 올바른 코드라 컴파일 에러가 나지 않는다는 점이 이 실수를 위험하게 만듭니다. GCC/Clang에서 -Wall을 켜면 suggest parentheses around assignment used as truth value 경고가 나오므로, 경고를 켜고 무시하지 않는 것이 가장 확실한 방지책입니다. if (5 == x)처럼 상수를 왼쪽에 쓰는 “요다 조건식”은 실수로 =를 쓰면 5 = x가 되어 컴파일 에러가 나게 만드는 오래된 요령인데, 요즘은 컴파일러 경고가 같은 역할을 하므로 가독성을 해치면서까지 쓸 필요는 적습니다.
실수 2: break 누락
// ❌ break 없음
switch (day) {
case 1:
cout << "월" << endl; // break 없음!
case 2:
cout << "화" << endl; // 계속 실행됨
}
// ✅ break 추가
switch (day) {
case 1:
cout << "월" << endl;
break;
case 2:
cout << "화" << endl;
break;
}
실수 3: 세미콜론
// ❌ 세미콜론 있음
if (age >= 20); // 빈 문장!
{
cout << "항상 실행됨" << endl;
}
// ✅ 세미콜론 제거
if (age >= 20) {
cout << "성인일 때만 실행" << endl;
}
if (age >= 20);의 세미콜론은 “아무것도 하지 않는 빈 문장”을 if의 본문으로 만듭니다. 그 아래 중괄호 블록은 if와 무관한 별개의 블록이 되어 무조건 실행됩니다. 조건이 맞을 때와 틀릴 때 결과가 같아서 “조건문이 동작하지 않는다”고 느끼게 되는데, 코드를 아무리 봐도 줄 끝의 작은 세미콜론은 눈에 잘 띄지 않습니다. GCC는 -Wempty-body(-Wextra에 포함)를 켜면 suggest braces around empty body in an 'if' statement, Clang은 기본 설정에서도 if statement has empty body 경고로 이 실수를 잡아 줍니다. for와 while 뒤에 세미콜론을 붙여도 같은 문제가 생깁니다.
실전 예시
예시 1: 성적 등급 계산기
#include <iostream>
using namespace std;
char getGrade(int score) {
if (score >= 90) return 'A';
else if (score >= 80) return 'B';
else if (score >= 70) return 'C';
else if (score >= 60) return 'D';
else return 'F';
}
int main() {
int score;
cout << "점수 입력: ";
cin >> score;
if (score < 0 || score > 100) {
cout << "잘못된 점수입니다" << endl;
return 1;
}
char grade = getGrade(score);
cout << "등급: " << grade << endl;
if (grade == 'A') {
cout << "우수합니다!" << endl;
} else if (grade == 'F') {
cout << "재시험이 필요합니다" << endl;
}
return 0;
}
main이 입력값의 범위를 먼저 검사하고 getGrade는 “0~100 사이의 점수”라는 전제 위에서 등급만 계산하도록 역할을 나눴습니다. 검증과 계산을 분리하면 getGrade를 다른 곳에서 재사용할 때도 규칙이 한 곳에 모여 있습니다. 여기서 잘 드러나지 않는 함정은 숫자가 아닌 입력입니다. 사용자가 abc를 입력하면 cin >> score가 실패하면서 score에 0이 들어가고(C++11 이후) 스트림이 실패 상태가 되므로, 프로그램은 “F” 등급을 출력합니다. if (!(cin >> score)) { cout << "숫자를 입력하세요"; return 1; }처럼 입력 자체의 성공 여부를 먼저 확인해야 이 경우를 구분할 수 있습니다. getGrade의 각 return 뒤에는 함수가 끝나므로 else를 빼고 if만 나열해도 결과가 같다는 점도 알아 두면 좋습니다.
예시 2: 간단한 ATM 메뉴
#include <iostream>
using namespace std;
int main() {
int balance = 10000;
int choice;
cout << "=== ATM ===" << endl;
cout << "1. 잔액 조회" << endl;
cout << "2. 입금" << endl;
cout << "3. 출금" << endl;
cout << "4. 종료" << endl;
cout << "선택: ";
cin >> choice;
switch (choice) {
case 1:
cout << "잔액: " << balance << "원" << endl;
break;
case 2: {
int amount;
cout << "입금액: ";
cin >> amount;
if (amount > 0) {
balance += amount;
cout << "입금 완료. 잔액: " << balance << "원" << endl;
} else {
cout << "잘못된 금액입니다" << endl;
}
break;
}
case 3: {
int amount;
cout << "출금액: ";
cin >> amount;
if (amount > 0 && amount <= balance) {
balance -= amount;
cout << "출금 완료. 잔액: " << balance << "원" << endl;
} else {
cout << "출금 불가" << endl;
}
break;
}
case 4:
cout << "종료합니다" << endl;
break;
default:
cout << "잘못된 선택입니다" << endl;
}
return 0;
}
case 2와 case 3이 모두 amount라는 같은 이름의 변수를 쓰는데, 각각 { } 블록 안에 선언했기 때문에 서로 충돌하지 않습니다. 중괄호가 없으면 두 선언이 같은 switch 범위에 놓여 redeclaration of 'int amount' 에러가 납니다. 또 int amount = 0;처럼 초기화식을 붙인 선언이 중괄호 밖에 있으면, 그 아래 case로 점프할 때 아래 “문제 1”의 초기화 건너뛰기 에러가 납니다(초기화식이 없는 int amount;는 건너뛰어도 에러가 아니지만, 값이 정해지지 않은 변수를 남기므로 권하지 않습니다). 출금 조건 amount > 0 && amount <= balance는 음수 입력으로 잔액이 늘어나는 것과 잔액보다 많이 출금하는 것을 한 번에 막습니다. 다만 두 경우 모두 “출금 불가”라는 같은 메시지를 보여 주므로, 사용자가 무엇을 잘못했는지 알 수 있게 조건을 나눠 다른 메시지를 주는 편이 더 친절합니다. 이 프로그램은 한 번 선택하면 끝나는데, 실제 메뉴처럼 계속 선택하게 하려면 전체를 while (true)로 감싸고 case 4에서 루프를 빠져나가야 합니다. 이때 case 4 안의 break는 switch만 빠져나올 뿐 while은 계속되므로, 플래그 변수를 두거나 return을 써야 한다는 점이 초보자가 자주 막히는 부분입니다.
예시 3: 윤년 판별
#include <iostream>
using namespace std;
bool isLeapYear(int year) {
// 400으로 나누어떨어지면 윤년
if (year % 400 == 0) {
return true;
}
// 100으로 나누어떨어지면 평년
if (year % 100 == 0) {
return false;
}
// 4로 나누어떨어지면 윤년
if (year % 4 == 0) {
return true;
}
// 그 외는 평년
return false;
}
int getDaysInMonth(int year, int month) {
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2:
return isLeapYear(year) ? 29 : 28;
default:
return 0;
}
}
int main() {
int year, month;
cout << "연도와 월 입력: ";
cin >> year >> month;
if (month < 1 || month > 12) {
cout << "잘못된 월입니다" << endl;
return 1;
}
int days = getDaysInMonth(year, month);
cout << year << "년 " << month << "월은 " << days << "일입니다" << endl;
if (month == 2 && isLeapYear(year)) {
cout << "(윤년)" << endl;
}
return 0;
}
윤년 규칙은 “4로 나누어떨어지면 윤년, 단 100으로 나누어떨어지면 평년, 단 400으로 나누어떨어지면 다시 윤년”입니다. isLeapYear가 가장 구체적인 조건(400)부터 검사하는 이유는 조건이 서로 포함 관계이기 때문입니다. 4로 나누어떨어지는지부터 검사하면 1900년이 윤년이 되어 버립니다. 같은 규칙을 한 줄로 쓰면 return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;이며, 이 식을 직접 만들어 보는 것이 논리 연산자 우선순위를 연습하기에 좋습니다. 확인용으로 2000년(윤년), 1900년(평년), 2024년(윤년), 2023년(평년)을 넣어 보면 네 갈래를 모두 검증할 수 있습니다. getDaysInMonth의 default: return 0;은 잘못된 월에 대한 안전장치인데, main에서 이미 범위를 검사하므로 실제로는 도달하지 않습니다. 그래도 함수가 다른 곳에서 호출될 수 있으므로 남겨 두는 편이 안전합니다.
자주 발생하는 문제
문제 1: switch에서 변수 선언
case 아래에서 초기화식이 있는 변수를 선언하면 컴파일 에러가 납니다. case 레이블은 점프 대상이라서, 뒤쪽 case로 점프하면 그 변수의 초기화를 건너뛰게 되기 때문입니다.
// ❌ 잘못된 코드
switch (choice) {
case 1:
int x = 10; // 에러!
cout << x << endl;
break;
case 2:
cout << x << endl; // x가 초기화 안될 수 있음
break;
}
GCC에서는 이 코드가 error: jump to case label과 note: crosses initialization of 'int x'로, MSVC에서는 initialization of 'x' is skipped by 'case' label 에러로 나타납니다. case는 switch 블록 안의 점프 목적지일 뿐 새 범위를 만들지 않으므로, case 2로 바로 점프하면 x의 선언은 보이는데 초기화는 건너뛴 상태가 됩니다. C++은 이렇게 초기화를 건너뛰는 점프를 금지합니다.
// ✅ 올바른 코드 (방법 1: 중괄호 사용)
switch (choice) {
case 1: {
int x = 10;
cout << x << endl;
break;
}
case 2:
cout << "case 2" << endl;
break;
}
// ✅ 올바른 코드 (방법 2: switch 밖에서 선언)
int x;
switch (choice) {
case 1:
x = 10;
cout << x << endl;
break;
case 2:
x = 20;
cout << x << endl;
break;
}
문제 2: 부동소수점 비교
계산 결과를 ==로 소수와 비교하는 조건은 기대와 다르게 거짓이 되기 쉽습니다. 원인은 부동소수점 표현 오차입니다.
// ❌ 잘못된 코드
double x = 0.1 + 0.1 + 0.1;
if (x == 0.3) { // false일 수 있음!
cout << "같음" << endl;
}
// ✅ 올바른 코드 (epsilon 사용)
#include <cmath>
double x = 0.1 + 0.1 + 0.1;
const double EPSILON = 1e-9;
if (fabs(x - 0.3) < EPSILON) { // 차이가 매우 작으면 같다고 판단
cout << "같음" << endl;
}
0.1은 2진수로 정확히 표현할 수 없는 수라서, 컴퓨터에는 0.1000000000000000055...처럼 가장 가까운 근삿값이 저장됩니다. 이 오차가 덧셈마다 쌓여 0.1 + 0.1 + 0.1은 0.30000000000000004가 되고, 역시 근삿값으로 저장된 0.3과 비트 단위로 다르기 때문에 ==가 거짓이 됩니다. cout은 기본적으로 6자리만 출력해서 둘 다 0.3으로 보이므로 더 헷갈리는데, cout << setprecision(17) << x로 출력해 보면 차이가 보입니다.
고정된 EPSILON은 비교하는 값의 크기가 1 근처일 때만 잘 맞는다는 한계가 있습니다. 값이 수백만 단위라면 오차 자체가 1e-9보다 훨씬 커서 같은 값도 다르다고 판단하고, 아주 작은 값끼리는 반대로 다른 값도 같다고 판단합니다. 크기가 다양한 값을 비교한다면 fabs(a - b) <= 1e-9 * max(fabs(a), fabs(b))처럼 값의 크기에 비례한 상대 오차를 쓰는 편이 안전합니다. 금액처럼 정확해야 하는 값이라면 애초에 원 단위 정수(long long)로 저장해 부동소수점 비교를 피하는 것이 가장 좋은 해결책입니다.
문제 3: 논리 연산자 우선순위
&&와 ||를 괄호 없이 섞으면 조건식이 의도와 다르게 묶일 수 있습니다.
// ❌ 의도와 다른 코드
int age = 25;
bool hasLicense = false;
// age >= 18 || (hasLicense && age < 65) 로 평가됨
if (age >= 18 || hasLicense && age < 65) {
cout << "운전 가능" << endl; // 18세 이상이면 나이 상한과 상관없이 실행됨!
}
// ✅ 올바른 코드 (괄호로 명확하게)
if ((age >= 18 || hasLicense) && age < 65) {
cout << "운전 가능" << endl;
}
&&는 ||보다 우선순위가 높아서, 곱셈이 덧셈보다 먼저 계산되듯 &&로 묶인 부분이 먼저 결합됩니다. 첫 코드는 “65세 미만”이라는 조건이 hasLicense에만 붙어 버려서 70세도 age >= 18 하나만으로 통과합니다. 이런 식은 컴파일러가 틀렸다고 판단할 수 없으므로 -Wall에서 suggest parentheses around '&&' within '||' 경고를 주는 것이 전부입니다. 규칙을 외워서 괄호를 생략하기보다, &&와 ||가 섞이면 항상 괄호를 쓴다는 규칙이 읽는 사람에게도 친절합니다. 조건이 더 길어지면 bool isWorkingAge = age >= 18 && age < 65;처럼 이름 있는 변수로 쪼개는 것이 가장 읽기 쉬운 방법입니다.
컴파일러 경고로 잡히는 조건문 실수
조건문 실수는 컴파일이 되기 때문에 경고를 켜 두지 않으면 발견하기 어렵습니다. 환경은 Ubuntu 24.04, g++ 13.3, -std=c++20 -Wall입니다(표시한 곳은 -Wextra 추가).
그림: 실제 실행 결과(g++ 13.3, Linux) — 세 실수 모두 컴파일은 되고 -Wall 경고로만 드러난다. 경고를 켜 두지 않으면 그대로 실행 파일이 만들어진다. (환경: Linux 6.18.44 · g++ (13.3.0-6ubuntu2~24.04.1))
int x = 3;
if (x = 5) { ... } // (1) 대입
if (a < b < c) { ... } // (2) 연쇄 비교
switch (c) { case Color::Red: ...; case Color::Green: ...; } // (3) enum 일부만 처리
(1) warning: suggest parentheses around assignment used as truth value [-Wparentheses]
(2) warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
(3) warning: enumeration value 'Blue' not handled in switch [-Wswitch]
(3)은 enum class Color { Red, Green, Blue }에서 Blue 케이스를 빼고 default도 두지 않았을 때입니다. default를 두면 이 경고가 나오지 않으므로, enum 값이 늘어났을 때 컴파일러가 알려 주길 원하면 일부러 default를 빼는 편이 낫습니다.
else는 가장 가까운 if에 붙는다
int x = 1, y = 0;
if (x > 0)
if (y > 0) std::cout << "A\n";
else std::cout << "B\n"; // 들여쓰기는 바깥 if처럼 보인다
std::cout << "끝\n";
warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]
실행 결과는 B, 끝이었습니다. x > 0이 참이라 바깥 if의 else였다면 B가 출력되지 않아야 하는데, else가 안쪽 if (y > 0)에 붙어서 y > 0이 거짓일 때 실행되었습니다. 중괄호를 항상 쓰면 이 경고 자체가 필요 없어집니다.
부동소수점 == 비교
std::cout << (0.1 + 0.2 == 0.3) << " " << (0.1f + 0.2f == 0.3f);
false true
double에서는 0.1 + 0.2가 0.3과 다르지만 float에서는 같게 나왔습니다. 정밀도에 따라 결과가 갈리므로 == 비교가 어떤 때는 통과하고 어떤 때는 실패하는 코드가 됩니다. 오차 허용 범위(std::abs(a - b) < eps)로 비교해야 합니다.
조건문과 성능
조건문의 성능은 초급 단계에서 신경 쓸 부분이 아니지만, 알아 두면 코드를 고를 때 도움이 되는 사실이 두 가지 있습니다. 첫째, 앞에서 말했듯 switch와 if-else 중 무엇을 쓰든 최적화 빌드(-O2)에서 컴파일러가 알맞은 형태로 바꿔 주므로, 속도를 이유로 읽기 어려운 쪽을 고를 필요는 없습니다. 둘째, CPU는 분기의 결과를 미리 예측해서 실행하는데, 결과가 규칙적이면(대부분 참, 또는 번갈아 가며) 예측이 잘 맞고 무작위면 자주 틀려 느려집니다. 수백만 번 도는 반복문 안의 조건이라면 이 차이가 측정될 수 있고, C++20의 [[likely]]/[[unlikely]] 속성으로 어느 쪽이 자주 실행되는지 컴파일러에 알려 줄 수도 있습니다. 하지만 이런 조정은 프로파일러로 병목을 확인한 뒤의 일이고, 그 전에는 경계 조건을 정확히 쓰고 경고를 모두 없애는 것이 훨씬 중요합니다.