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) 를 합니다. 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를 빼먹으면 의도하지 않은 출력·로직 중복이 생깁니다. 아래 “자주 하는 실수” 절의 break 예제와 함께 익혀 두세요. 범위가 겹치지 않는 분기면 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 ? "성인" : "미성년자";는 의도대로 동작하지 않으므로 출력할 때는 전체를 괄호로 감싸야 합니다.

중첩 조건문

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에 포함)와 Clang은 suggest braces around empty body in an 'if' statement 같은 경고로 이 실수를 잡아 줍니다. for와 while 뒤에 세미콜론을 붙여도 같은 문제가 생깁니다.

실전 패턴: 메뉴 시스템과 등급 판정

아래 두 가지는 초급 과제·실습에서 자주 나옵니다. 공통점은 “입력 → 검증 → 분기 → 출력” 흐름을 명확히 만드는 것입니다.

메뉴 시스템 (switch 중심)

  • 정수·문자 코드처럼 이산적인 선택지면 switch가 읽기 좋습니다.
  • 각 case에서 같은 이름의 지역 변수가 필요하면 { }로 블록을 씁니다(아래 ATM 예시 참고).
  • 잘못된 입력은 default에서 처리하며, 반복 메뉴가 필요하면 이 글의 반복문 글과 같이 while로 감쌉니다.

등급 판정 (if-else if 또는 함수)

  • 점수 구간처럼 범위 비교가 많으면 if-else if 체인이 자연스럽습니다.
  • 등급 문자만 구하는 함수(getGrade 등)를 분리하면 같은 규칙을 출력·저장·비교 여러 곳에서 재사용하기 쉽습니다.
  • 경계값(89점은 B인지 A인지)을 표나 주석으로 정해 두면 실수가 줄어듭니다.

흔한 실수와 연결

  • 메뉴에서 =와 == 혼동 → 의도치 않은 대입.
  • switch에서 break 누락 → 다음 메뉴까지 실행되는 fall-through.

이어지는 실전 예시에서 등급 계산기·ATM·윤년 예제로 위 패턴이 그대로 드러납니다.

실전 예시

예시 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;
}

설명: 점수에 따라 등급을 부여하는 프로그램입니다. if-else if 체인을 사용하여 범위 비교를 수행합니다.

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;
}

설명: switch 문을 사용한 메뉴 선택 프로그램입니다. 각 case에서 중괄호를 사용하여 지역 변수를 선언할 수 있습니다.

case 2와 case 3이 모두 amount라는 같은 이름의 변수를 쓰는데, 각각 { } 블록 안에 선언했기 때문에 서로 충돌하지 않습니다. 중괄호가 없으면 두 선언이 같은 switch 범위에 놓여 redeclaration of 'int amount' 에러가 나고, 한쪽만 있어도 아래 “문제 1”의 초기화 건너뛰기 에러가 납니다. 출금 조건 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 레이블은 점프 대상이므로 초기화를 건너뛸 수 있음

해결법:

// ❌ 잘못된 코드
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: 부동소수점 비교

증상: if (x == 0.1) 조건이 예상과 다르게 동작

원인: 부동소수점 오차로 정확한 비교 불가

해결법:

// ❌ 잘못된 코드
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;
}

// 또는
if (age >= 18 && age < 65 || hasLicense && age < 65) {
    cout << "운전 가능" << endl;
}

&&는 ||보다 우선순위가 높아서, 곱셈이 덧셈보다 먼저 계산되듯 &&로 묶인 부분이 먼저 결합됩니다. 첫 코드는 “65세 미만”이라는 조건이 hasLicense에만 붙어 버려서 70세도 age >= 18 하나만으로 통과합니다. 이런 식은 컴파일러가 틀렸다고 판단할 수 없으므로 -Wall에서 suggest parentheses around '&&' within '||' 경고를 주는 것이 전부입니다. 규칙을 외워서 괄호를 생략하기보다, &&와 ||가 섞이면 항상 괄호를 쓴다는 규칙이 읽는 사람에게도 친절합니다. 조건이 더 길어지면 bool isWorkingAge = age >= 18 && age < 65;처럼 이름 있는 변수로 쪼개는 것이 가장 읽기 쉬운 방법입니다.

조건문과 성능

조건문의 성능은 초급 단계에서 신경 쓸 부분이 아니지만, 알아 두면 코드를 고를 때 도움이 되는 사실이 두 가지 있습니다. 첫째, 앞에서 말했듯 switch와 if-else 중 무엇을 쓰든 최적화 빌드(-O2)에서 컴파일러가 알맞은 형태로 바꿔 주므로, 속도를 이유로 읽기 어려운 쪽을 고를 필요는 없습니다. 둘째, CPU는 분기의 결과를 미리 예측해서 실행하는데, 결과가 규칙적이면(대부분 참, 또는 번갈아 가며) 예측이 잘 맞고 무작위면 자주 틀려 느려집니다. 수백만 번 도는 반복문 안의 조건이라면 이 차이가 측정될 수 있고, C++20의 [[likely]]/[[unlikely]] 속성으로 어느 쪽이 자주 실행되는지 컴파일러에 알려 줄 수도 있습니다. 하지만 이런 조정은 프로파일러로 병목을 확인한 뒤의 일이고, 그 전에는 경계 조건을 정확히 쓰고 경고를 모두 없애는 것이 훨씬 중요합니다.

같이 보면 좋은 글