CodeGym /행동 /C++ SELF /전형적인 실수: = vs ==, 입력 검사

전형적인 실수: = vs ==, 입력 검사

C++ SELF
레벨 3, 레슨 5
사용 가능

1. 치명적 실수: = 대신 ==

사소해 보이지만 의미는 완전히 다릅니다: 등호 하나인지 두 개인지. ==는 "같은가?"라는 질문이고, =는 "이 값을 변수에 넣어라"라는 명령입니다.

더 교묘한 점은 할당 연산자(=)도 표현식이라는 것입니다: 할당된 값을 반환합니다. 그래서 컴파일러가 항상 경고를 띄우지 않을 수 있습니다. 조건식은 숫자로 계산되고, 그 숫자는 조건에서 "0은 거짓, 0이 아닌 값은 참"으로 해석됩니다.

시험장에서 "이게 사실인가?"라고 묻는 대신 "이것을 사실로 만들어라!"고 말한다고 상상해보세요. 교수는 놀랄 수 있지만 형식적으로는 문장을 말한 셈입니다. =가 if 안에 있으면 비슷한 속임수입니다.

미니 데모: 실수가 데이터와 로직을 어떻게 망치는지

아래 예는 컴파일은 되지만 의도한 동작을 하지 않습니다:

#include <iostream>

int main() {
    int x = 5;

    if (x = 0) {                 // 오류: 비교 대신 할당
        std::cout << "x is zero\n";
    }

    std::cout << x << '\n';      // 0
}

무슨 일이 일어나는지 요약하면: x = 0은 0을 할당하고, 표현식의 값은 0이므로 조건은 거짓입니다. if 블록은 실행되지 않지만, x는 이미 변경되어 버립니다.

올바른 버전

#include <iostream>

int main() {
    int x = 5;

    if (x == 0) {                // 비교
        std::cout << "x is zero\n";
    } else {
        std::cout << "x is not zero\n"; // x is not zero
    }
}

컴파일러가 항상 도와주지 않는 이유

언어 입장에서는 합법적이기 때문입니다: if (...)의 조건은 bool로 변환 가능한 무언가여야 합니다. 숫자는 bool로 변환됩니다. 따라서 형식적으로는 올바릅니다.

물론 많은 컴파일러가 경고(warning)를 띄우지만, 경고는 보장되지 않습니다. 특히 설정이 느슨한 웹 IDE에서는 경고가 표시되지 않을 수 있습니다. 따라서 당신의 안전장치는 조건의 중앙 연산자를 주의 깊게 확인하는 습관입니다.

습관: 암시적 표현보다 명시적 비교

초보자에게 "영리하게" 쓰는 것은 좋지 않습니다:

if (x) { /* ... */ } // 작동하지만 초반에는 읽기 어려움

좋은 습관은 명확히 비교하는 것입니다:

if (x != 0) { /* ... */ }

학습용 코드에서는 이렇게 쓰는 것이 가독성 측면에서 거의 항상 이점이 있습니다. 가독성은 버그의 절반을 줄여줍니다.

2. 입력 검사: if (std::cin >> x)

std::cin >> x로 숫자를 읽을 때, 사용자가 정확히 숫자를 입력할 것이라고 조용히 가정하게 됩니다. 하지만 사용자는 창의적일 수 있습니다. "qwe"를 입력할 수도 있고, Enter를 잘못 누를 수도 있으며, 정수 대신 "10.5"를 붙여넣을 수도 있습니다. 그러면 std::cin은 실패 상태가 되고, 변수는 이전 값을 유지할 수 있으며 이후 읽기들이 이상하게 동작할 수 있습니다.

스트림의 자세한 상태(fail, bad, clear, ignore)는 다른 주제에서 다룹니다. 오늘은 최소한의 패턴, 즉 읽기 자체를 확인하는 것으로 충분합니다.

가장 간단하고 유용한 패턴

#include <iostream>

int main() {
    int n = 0;

    if (std::cin >> n) {
        std::cout << "read: " << n << '\n';   // 예: read: 42
    } else {
        std::cout << "input error\n";         // 숫자가 아닌 경우
    }
}

조건 if (std::cin >> n)에서는 먼저 숫자 읽기를 시도하고, 그다음 성공했는지 확인합니다. 간결하고 현재 단계에서는 이상적입니다.

미니 예제: 명령과 숫자, '운에 맡기지 않는' 방식

간단한 콘솔 도우미 아이디어를 계속합니다. 아직 루프는 쓰지 않습니다: 한 번 실행에 한 명령 처리입니다.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "square") {
        int x = 0;
        if (std::cin >> x) {
            std::cout << x * x << '\n';       // 예: 25
        } else {
            std::cout << "input error\n";
        }
    }
}

입력 검사는 숫자를 사용하는 지점 근처에 있어야 합니다. 우리는 숫자가 읽혔다고 '기대'하지 않습니다.

3. 경계 사례: 나이, 범위, 문자열

경계값은 프로그램이 가장 자주 깨지는 곳입니다: 0, 1, -1, "정확히 18", 빈 문자열, 문자열의 마지막 문자, 0으로 나누기. 초보자는 보통 '중간'만 테스트합니다(예: 20세). 오류는 종종 '끝'에 숨어 있습니다(예: 18세).

따라서 조건을 말로 표현하고 경계가 어디에 있는지, 포함되는지 아닌지를 확인하는 것이 유용합니다.

간단한 규칙 하나를 기억하세요: 인덱스로 접근하거나 나누는 모든 경우에는 보호 코드가 필요합니다. 그리고 '항상 괜찮을 것 같다'고 생각하더라도 실제로는 그렇지 않을 때가 많습니다.

> vs >=: "경계를 포함"할지 여부는 별도의 결정입니다

두 검사를 비교해 보세요:

#include <iostream>

int main() {
    int age = 18;

    std::cout << (age > 18)  << '\n';  // 0
    std::cout << (age >= 18) << '\n';  // 1
}

"성인 기준이 18세"라면 >=가 필요합니다. 이것은 스타일 문제가 아니라 의미론적 결정입니다.

실제 예:

#include <iostream>

int main() {
    int age = 0;
    std::cin >> age;

    if (age >= 18) {
        std::cout << "adult\n";
    } else {
        std::cout << "minor\n";
    }
}

부호를 잘못 쓰면 프로그램은 딱 하나의 값에서만 잘못 작동합니다. 그래서 이런 버그는 오래 사는 편입니다: 보통 20, 30, 40을 테스트하니까요. 방지 습관은 먼저 조건을 말로 표현하는 것입니다("18을 포함한다").

범위 "L에서 R까지 포함": &&로 작성

"x가 1에서 10 사이"라는 조건은 거의 항상 "1 이상이며 10 이하"를 의미합니다.

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

    if (x >= 1 && x <= 10) {
        std::cout << "in range\n";
    } else {
        std::cout << "out of range\n";
    }
}

경계는 문제의 일부입니다. 단순히 if를 쓰는 것이 아니라, 1과 10에서 무엇을 할지 결정해야 합니다.

빈 문자열: s[0]를 사용하기 전에 비어 있지 않은지 확인

문자열이 항상 하나 이상의 문자를 갖는 것은 아닙니다. std::getline으로 읽은 경우 사용자가 단순히 Enter만 눌렀을 수 있습니다.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() == 0) {
        std::cout << "empty\n";
    } else {
        std::cout << s[0] << '\n';      // 첫 문자 출력
    }
}

여기서 빈/비어있지 않음 검사는 번거로운 규칙이 아니라, 경계 초과를 막는 핵심입니다.

마지막 문자: s[s.size() - 1]은 더 주의가 필요

s.size()가 0이면 s.size() - 1은 -1이 되어 인덱스로는 부적절합니다.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() > 0) {
        char last = s[s.size() - 1];
        std::cout << last << '\n';
    } else {
        std::cout << "empty\n";
    }
}

여기서는 > 0로 검사하는 것이 맞습니다. size()는 빈 문자열에서 0입니다.

4. 보호 검사: 나누기와 단락 평가

0으로 나누기는 고전적인 문제입니다. 특히 사용자가 0을 입력할 수 있다는 사실을 미처 생각하지 않았기 때문에 발생합니다. 사용자는 "나눗셈의 분모를 입력하라"는 요청을 받아도 0을 입력할 수 있습니다.

&&의 단락 평가는 왼쪽 피연산자가 거짓이면 오른쪽을 평가하지 않는 성질이 있습니다. 이 성질을 안전 검사에 이용하면 왼쪽에서 안전을 확인하고 오른쪽에 위험한 연산을 놓아도 안전합니다.

&&에서 올바른 순서

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if (b != 0 && (a / b > 2)) {
        std::cout << "a/b > 2\n";
    } else {
        std::cout << "condition is false\n";
    }
}

만약 b가 0이면 왼쪽 조건 b != 0이 거짓이 되어, 나눗셈은 발생하지 않습니다. 이것이 바로 보호 검사입니다.

잘못된 순서(위험)

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if ((a / b > 2) && b != 0) {        // 위험: 이미 나눗셈이 일어남
        std::cout << "a/b > 2\n";
    }
}

검사를 하더라도 타이밍이 늦으면 소용이 없습니다. 자전거에서 떨어진 뒤에 헬멧을 쓰는 것과 같습니다: 헬멧은 좋지만 타이밍이 문제입니다.

5. 실전 미니 예제: 한 명령에 한 동작

지금까지를 모아 작고 안전한 조건 스타일의 애플리케이션을 만듭니다. 아직 루프나 함수를 배우지 않았으므로(main 제외) 사용하지 않습니다. 그래도 잘못된 입력으로 프로그램이 죽지 않고, 검사 없이 위험한 연산을 하지 않는 프로그램을 만들 수 있습니다.

아이디어: 사용자는 명령(divide, first, last)을 입력하고, 그에 필요한 데이터를 입력합니다.

명령 분기 골격

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        std::cout << "mode: divide\n";
    } else if (cmd == "first") {
        std::cout << "mode: first\n";
    } else {
        std::cout << "unknown command\n";
    }
}

기본 분기입니다: 명령은 문자열이고, 문자열 비교는 bool을 반환합니다. if/else if/else는 하나의 분기만 선택합니다.

명령 divide: 입력 검사와 나눗셈 보호

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        int a = 0, b = 0;
        if (std::cin >> a >> b) {
            if (b != 0) std::cout << (a / b) << '\n';
            else std::cout << "division by zero\n";
        } else {
            std::cout << "input error\n";
        }
    }
}

여기서는 두 단계의 보호가 있습니다: 먼저 숫자가 제대로 읽혔는지, 그다음 분모가 0이 아닌지 확인합니다.

명령 first: 문자열을 읽고 빈 문자열 검사

first의 경우 한 가지 주의점이 있습니다: std::cin >> cmd 후 버퍼에 개행 문자가 남아 있을 수 있어, getline이 빈 문자열을 읽을 수 있습니다. 이 문제에 대해 이미 다뤘으므로 여기서는 실용적인 방법을 사용합니다: std::getline(std::cin >> std::ws, s) — std::ws는 실제 텍스트 앞의 공백과 개행을 제거합니다.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "first") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() == 0) std::cout << "empty\n";
        else std::cout << s[0] << '\n';
    }
}

std::ws로 인해 약간 마법처럼 보일 수 있지만, >> 후의 빈 줄 문제를 해결하는 표준적인 방법입니다.

명령 last: 경계 초과 없이 마지막 문자

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "last") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() > 0) std::cout << s[s.size() - 1] << '\n';
        else std::cout << "empty\n";
    }
}

논리는 동일합니다: 먼저 검사하고, 그다음 인덱싱합니다.

위험 지점을 생각하는 방식

if 분기를 작성할 때 유용한 사고 흐름은 다음과 같습니다:

flowchart TD
    A[작업을 수행해야 함] --> B{데이터가 필요한가?}
    B -->|예| C{입력이 성공했나?}
    C -->|아니오| X[오류 메시지]
    C -->|예| D{위험한 연산이 있는가?}
    D -->|나누기/인덱스| E{보호가 되었나?}
    E -->|아니오| Y[메시지 + 수행하지 않음]
    E -->|예| F[작업 수행]
    B -->|아니오| F

이 흐름도는 형식이 아니라, 특히 코드가 분기할 때 검사를 잊지 않도록 도와주는 방법입니다.

6. 전형적인 실수 모음

실수 #1: 조건에서 =를 == 대신 사용함.
컴파일을 방해하지 않기 때문에 매우 성가신 실수입니다. =는 변수를 변경하고 그 값을 반환하므로, 조건은 비교가 아니라 방금 덮어쓴 값에 의해 결정됩니다. 해결책은 조건의 중앙 연산자를 주의 깊게 보고 명시적으로 비교하는 습관을 들이는 것입니다: x == 0, cmd == "exit".

실수 #2: 경계 기호 혼동(> 대신 >=, < 대신 <=).
이런 실수는 거의 항상 "정확히 한 값"에서만 나타납니다: 18, 0, "마지막 인덱스" 등. 그래서 오래 남습니다. 방지 습관은 먼저 조건을 말로 표현하는 것입니다("18을 포함한다").

실수 #3: 길이 검사 없이 문자열 인덱싱.
std::string이 비어있을 수 있다는 사실을 잊지 마세요. s[0]나 s[s.size() - 1]를 사용하기 전에 반드시 s.size() > 0인지 확인해야 합니다. 특히 마지막 문자는 s.size() - 1가 빈 문자열에서 잘못된 인덱스가 되므로 교묘합니다. 보호는 별도의 if 브랜치나 &&의 왼쪽 조건으로 구현하세요.

실수 #4: 0 검사 없이 나눗셈(또는 검사가 '오른쪽'에 있음).
b != 0 검사는 나눗셈보다 먼저 와야 합니다. 가장 편한 패턴은 b != 0 && (a / b > ...)처럼 왼쪽에서 보호하는 것입니다. 조건 순서를 바꾸면 검사해도 위험한 동작이 발생할 수 있습니다.

실수 #5: 최소한의 입력 검사 누락.
만약 std::cin >> x; 하고 바로 x를 사용하면, 입력이 올바르리라 가정하는 것입니다. 실제로는 사용자가 숫자가 아닌 것을 입력하면 스트림이 멈추고 이상한 동작이 발생합니다. 스트림 내부 구조를 깊게 이해하지 않아도, 간단히 if (std::cin >> x) { ... } else { ... }와 같이 검사를 추가하면 수시간의 디버깅을 절약할 수 있습니다.

1
과제
C++ SELF, 레벨 3, 레슨 5
잠금
제로 인디케이터
제로 인디케이터
1
과제
C++ SELF, 레벨 3, 레슨 5
잠금
값 레이블
값 레이블
1
과제
C++ SELF, 레벨 3, 레슨 5
잠금
합계 확인
합계 확인
1
과제
C++ SELF, 레벨 3, 레슨 5
잠금
세 가지 명령
세 가지 명령
1
설문조사/퀴즈
조건 연산자와 논리, 레벨 3, 레슨 5
사용 불가능
조건 연산자와 논리
조건 연산자와 논리
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION