본문 바로가기

관계 연산자와 논리 연산자

두 값을 비교해 bool을 만드는 관계 연산자와 등호의 함정, 실수를 비교할 때의 허용 오차를 다루고, 여러 조건을 잇는 논리 연산자와 오른쪽을 계산하지 않고 끝내는 단락 평가, 수학식처럼 이어 쓴 비교가 C++에서 다르게 묶이는 이유를 확인합니다.

프로그램은 계산만 하지 않고, 점수가 60점 이상이면 합격으로 처리하는 것처럼 조건에 따라 다른 일을 합니다. 그 조건을 만드는 것이 두 값을 비교하는 관계 연산자(relational operator)와, 여러 조건을 잇는 논리 연산자(logical operator)입니다.

두 연산자는 모두 참과 거짓을 나타내는 bool 값을 만들며, 다음 장의 if와 반복문의 조건으로 쓰입니다. 이 글은 비교할 때의 함정과, 논리 연산자가 필요한 만큼만 계산하는 단락 평가, 여러 연산자가 섞인 조건식이 묶이는 방식을 봅니다.

관계 연산자

관계 연산자는 두 피연산자를 비교해 true나 false를 돌려줍니다.

연산자뜻예결과
==같다5 == 5true
!=다르다5 != 3true
>크다5 > 3true
<작다5 < 3false
>=크거나 같다5 >= 5true
<=작거나 같다5 <= 3false
relational.cpp
#include <iostream>
#include <string>

int main() {
    int score = 75;
    int passingScore = 60;
    std::string name1 = "Alice";
    std::string name2 = "Bob";
    std::string name3 = "Alice";

    std::cout << std::boolalpha;
    std::cout << "passed: " << (score >= passingScore) << "\n";
    std::cout << "perfect: " << (score == 100) << "\n";
    std::cout << "name1 == name2: " << (name1 == name2) << "\n";
    std::cout << "name1 != name3: " << (name1 != name3) << "\n";
    std::cout << "name1 < name2: " << (name1 < name2) << "\n";
}
passed: true
perfect: false
name1 == name2: false
name1 != name3: false
name1 < name2: true

std::string끼리의 ==는 내용이 같은지를, <는 사전에서 앞에 오는지를 비교하므로 "Alice" < "Bob"은 참입니다. 이것은 std::string이 제공하는 비교이며, 뒤에서 배울 C 문자열인 char 배열을 ==로 비교하면 내용이 아니라 주소를 비교하게 됩니다. 비교 식을 <<와 함께 쓸 때는 우선순위 때문에 괄호로 감쌉니다.

비교할 때 가장 흔한 실수는 == 대신 =를 쓰는 것입니다. if (a = 10)은 오류가 아니라, a에 10을 대입한 뒤 그 값 10을 bool로 바꾸어 조건으로 쓰는 올바른 식이어서 조건은 언제나 참이 됩니다. GCC는 -Wall에서 조건 안의 대입에 경고를 내지만, 경고에만 기대지 말고 비교하려는 곳에는 ==를 썼는지 확인합니다.

실수를 비교할 때는 기본 데이터 타입에서 본 반올림 오차를 생각해야 합니다. 0.1 + 0.2와 0.3은 서로 다른 double로 저장되므로 ==로 비교하면 거짓이 되며, 오차를 허용해야 한다면 두 값의 차이가 정한 범위 안에 드는지를 봅니다.

float_compare.cpp
#include <cmath>
#include <iostream>

int main() {
    double a = 0.1 + 0.2;
    double b = 0.3;
    const double tolerance = 1e-8;

    std::cout << std::boolalpha << (a == b) << "\n";
    std::cout << (std::abs(a - b) <= tolerance) << "\n";
}
false
true

두 값의 차이는 약 5.55 × 10⁻¹⁷이어서 허용 오차 10⁻⁸ 안에 듭니다. 다만 이렇게 고정된 절대 오차는 값의 크기를 알 때만 알맞습니다. 값이 아주 크거나 작을 수 있다면 값의 크기에 비례하는 상대 오차를 쓰고, 계산 결과가 NaN이나 무한대가 될 수 있다면 std::isfinite로 먼저 확인합니다.

더 깊이: NaN과 음의 0은 어떻게 비교될까

IEEE 754 부동 소수점에는 비교 규칙이 특별한 값이 둘 있습니다. 하나는 숫자가 아님을 나타내는 NaN(not a number)으로, std::numeric_limits<double>::is_iec559가 참인 구현에서 0.0 / 0.0의 결과가 NaN입니다. NaN은 자기 자신을 포함한 어떤 값과도 같지 않아서, x가 NaN이면 x == x는 거짓이고 x != x는 참이며 <, >, <=, >=도 모두 거짓입니다. 그래서 if (value < 0.0)로 잘못된 값을 거르면 NaN은 그대로 통과하므로, NaN이 들어올 수 있다면 <cmath>의 std::isnan으로 따로 확인하거나 조건을 !(value >= 0.0)처럼 씁니다.

다른 하나는 부호 비트만 다른 음의 0인 -0.0입니다. -0.0 == 0.0은 참이지만 std::signbit(-0.0)은 참이고, 1.0 / -0.0은 음의 무한대가 되어 양의 0과 다르게 동작합니다. ==가 참이라고 두 값의 비트까지 같은 것은 아닙니다.

C++20의 <compare>에 있는 std::strong_order는 IEEE 754가 정한 전순서(totalOrder)로 부동 소수점을 비교합니다. 이 순서에서는 음의 0이 양의 0보다 앞서고, NaN에도 부호와 비트에 따라 정해진 자리가 있습니다. 그래서 NaN이 섞일 수 있는 값을 정렬하거나 순서가 필요한 컨테이너의 키로 써야 한다면, 기본 < 대신 이런 전순서 비교를 고려합니다.

논리 연산자

논리 연산자는 bool 값들을 이어 더 복잡한 조건을 만듭니다. 피연산자가 bool이 아니면 먼저 bool로 바뀌며, 0은 거짓이고 0이 아닌 값은 참입니다.

연산자이름결과
&&논리 AND두 피연산자가 모두 참일 때만 참
두 세로선논리 OR둘 중 하나라도 참이면 참, 둘 다 거짓일 때만 거짓
!논리 NOT참과 거짓을 뒤집음

논리 OR는 세로선 두 개로 적으며, 코드에서는 a || b의 모양입니다. 같은 뜻으로 and, or, not이라는 낱말을 쓸 수도 있지만, 대부분의 코드는 기호를 씁니다.

logical.cpp
#include <iostream>

int main() {
    int age = 20;
    bool hasLicense = true;
    bool hasCar = false;
    int points = 85;

    std::cout << std::boolalpha;
    std::cout << "canDrive: " << ((age >= 18) && hasLicense) << "\n";
    std::cout << "licenseOrCar: " << (hasLicense || hasCar) << "\n";
    std::cout << "failed: " << !(points >= 70) << "\n";
    std::cout << "canRent: " << ((age >= 21) && hasLicense && (points >= 80)) << "\n";
}
canDrive: true
licenseOrCar: true
failed: false
canRent: false

canRent는 셋째 조건 points >= 80은 참이지만 첫 조건 age >= 21이 거짓이어서 전체가 거짓입니다. 이 나이와 점수의 기준은 논리식을 연습하기 위한 가정이며, 실제 운전이나 대여 자격의 기준은 아닙니다.

단락 평가

&&와 ||는 왼쪽 피연산자만으로 결과가 정해지면 오른쪽 피연산자를 아예 계산하지 않습니다. 이것을 단락 평가(short-circuit evaluation)라고 합니다.

기본 제공 논리 연산자의 조건부 오른쪽 평가

내장 &&/||가 왼쪽 값에 따라 오른쪽 평가를 생략하는 실제 실행 규칙이다. 본문 nullptr 검사 사례의 조건 하나를 명시해 선택된 경로만 진행하고 전체 분기는 위 관계도와 표에 남긴다.

기본 제공 논리 AND와 OR가 왼쪽 피연산자의 bool 값에 따라 오른쪽 피연산자를 평가하거나 건너뛰는 두 갈래 비교

BUILT-IN SHORT-CIRCUIT CONTRACT

논리 연산은 왼쪽 결과에 따라 오른쪽 평가를 생략한다

왼쪽 A를 먼저 평가해 bool로 문맥 변환합니다. 그 값만으로 결과가 정해지지 않을 때에만 오른쪽 B를 평가합니다.

AND와 OR의 오른쪽 평가 분기기본제공AND는Afalse면B를평가하지않고false로끝내며,Atrue일때B를평가한다. 기본제공OR는Atrue면B를평가하지않고true로끝내며,Afalse일때B를평가한다. 두 연산은 독립 비교이며 사용자정의함수호출에적용하지않는다.A && B왼쪽 A 값?falsetrueB 평가 생략전체 결과 falseB 평가전체 결과 bool(B)A || B왼쪽 A 값?truefalseB 평가 생략전체 결과 trueB 평가전체 결과 bool(B)
AND와 OR의 오른쪽 평가 분기기본제공AND는Afalse면B를평가하지않고false로끝내며,Atrue일때B를평가한다. 기본제공OR는Atrue면B를평가하지않고true로끝내며,Afalse일때B를평가한다. 두 연산은 독립 비교이며 사용자정의함수호출에적용하지않는다.A && B왼쪽 A 값?falsetrueB 평가 생략전체 결과 falseB 평가전체 결과 bool(B)A || B왼쪽 A 값?truefalseB 평가 생략전체 결과 trueB 평가전체 결과 bool(B)

A && B

전체가 참이려면 두 피연산자가 모두 참이어야 합니다.

bool(A) == false
B 미평가 · 결과는 즉시 false
bool(A) == true
B 평가 · 결과는 bool(B)

A || B

전체가 거짓이려면 두 피연산자가 모두 거짓이어야 합니다.

bool(A) == true
B 미평가 · 결과는 즉시 true
bool(A) == false
B 평가 · 결과는 bool(B)

이 계약은 기본 제공 &&와 ||에 적용됩니다. 논리 연산자를 오버로드한 함수 호출은 오른쪽 피연산자를 생략하지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명기본 제공 AND는 A가 false면 B를 평가하지 않고 false로 끝내며, A가 true일 때 B를 평가한다. 기본 제공 OR는 A가 true면 B를 평가하지 않고 true로 끝내며, A가 false일 때 B를 평가한다. 두 연산은 독립 비교이며 사용자 정의 함수 호출에 적용하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
왼쪽 검사

nullptr 사례의 내장 &&에서 먼저 포인터를 검사한다.

  • 포인터: nullptr
  • 왼쪽 조건: false
  • 오른쪽 접근에 앞서 유효성 조건을 확인한다.
오른쪽 생략

선택한 false 경로에서는 역참조식을 평가하지 않는다.

  • 오른쪽: 평가되지 않음
  • && 결과: false
  • 내장 &&는 왼쪽이 거짓이면 단락한다; overload 동작과 구분한다.

기본 제공 논리 연산자의 조건부 오른쪽 평가

내장 &&/||가 왼쪽 값에 따라 오른쪽 평가를 생략하는 실제 실행 규칙이다. 본문 nullptr 검사 사례의 조건 하나를 명시해 선택된 경로만 진행하고 전체 분기는 위 관계도와 표에 남긴다.

기본 제공 논리 AND와 OR가 왼쪽 피연산자의 bool 값에 따라 오른쪽 피연산자를 평가하거나 건너뛰는 두 갈래 비교

BUILT-IN SHORT-CIRCUIT CONTRACT

논리 연산은 왼쪽 결과에 따라 오른쪽 평가를 생략한다

왼쪽 A를 먼저 평가해 bool로 문맥 변환합니다. 그 값만으로 결과가 정해지지 않을 때에만 오른쪽 B를 평가합니다.

AND와 OR의 오른쪽 평가 분기기본제공AND는Afalse면B를평가하지않고false로끝내며,Atrue일때B를평가한다. 기본제공OR는Atrue면B를평가하지않고true로끝내며,Afalse일때B를평가한다. 두 연산은 독립 비교이며 사용자정의함수호출에적용하지않는다.A && B왼쪽 A 값?falsetrueB 평가 생략전체 결과 falseB 평가전체 결과 bool(B)A || B왼쪽 A 값?truefalseB 평가 생략전체 결과 trueB 평가전체 결과 bool(B)
AND와 OR의 오른쪽 평가 분기기본제공AND는Afalse면B를평가하지않고false로끝내며,Atrue일때B를평가한다. 기본제공OR는Atrue면B를평가하지않고true로끝내며,Afalse일때B를평가한다. 두 연산은 독립 비교이며 사용자정의함수호출에적용하지않는다.A && B왼쪽 A 값?falsetrueB 평가 생략전체 결과 falseB 평가전체 결과 bool(B)A || B왼쪽 A 값?truefalseB 평가 생략전체 결과 trueB 평가전체 결과 bool(B)

A && B

전체가 참이려면 두 피연산자가 모두 참이어야 합니다.

bool(A) == false
B 미평가 · 결과는 즉시 false
bool(A) == true
B 평가 · 결과는 bool(B)

A || B

전체가 거짓이려면 두 피연산자가 모두 거짓이어야 합니다.

bool(A) == true
B 미평가 · 결과는 즉시 true
bool(A) == false
B 평가 · 결과는 bool(B)

이 계약은 기본 제공 &&와 ||에 적용됩니다. 논리 연산자를 오버로드한 함수 호출은 오른쪽 피연산자를 생략하지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명기본 제공 AND는 A가 false면 B를 평가하지 않고 false로 끝내며, A가 true일 때 B를 평가한다. 기본 제공 OR는 A가 true면 B를 평가하지 않고 true로 끝내며, A가 false일 때 B를 평가한다. 두 연산은 독립 비교이며 사용자 정의 함수 호출에 적용하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
왼쪽 검사

nullptr 사례의 내장 &&에서 먼저 포인터를 검사한다.

  • 포인터: nullptr
  • 왼쪽 조건: false
  • 오른쪽 접근에 앞서 유효성 조건을 확인한다.
오른쪽 생략

선택한 false 경로에서는 역참조식을 평가하지 않는다.

  • 오른쪽: 평가되지 않음
  • && 결과: false
  • 내장 &&는 왼쪽이 거짓이면 단락한다; overload 동작과 구분한다.

그림처럼 A && B는 A가 거짓이면 B와 상관없이 거짓이므로 B를 계산하지 않고, A가 참일 때만 B를 계산합니다. A || B는 A가 참이면 B를 계산하지 않고 참으로 끝나며, A가 거짓일 때만 B를 계산합니다. 그래서 왼쪽에 안전 검사를 두면, 그 검사를 통과할 때만 오른쪽의 위험한 계산이 실행됩니다.

short_circuit.cpp
#include <iostream>

int main() {
    int boxes{0};
    bool enough = (boxes > 0) && (10 / boxes >= 2);
    std::cout << std::boolalpha << enough << "\n";
}
false

boxes > 0이 거짓이므로 오른쪽의 10 / boxes는 계산되지 않고, 0으로 나누는 일 없이 false가 됩니다. 두 조건의 순서를 바꾸면 검사하기 전에 0으로 나누게 되고, && 대신 비트 연산자 &를 쓰면 단락 평가를 하지 않아 같은 보호가 되지 않습니다. 뒤에서 배울 포인터에서도 ptr != nullptr && ptr->value > 0처럼, 포인터가 널이 아닌지 먼저 확인한 뒤에만 그 포인터로 접근하는 데 이 성질을 씁니다.

다만 ptr != nullptr은 널 포인터만 걸러 낼 뿐, 이미 사라진 객체를 가리키는 포인터까지 걸러 내지는 못합니다. 또 클래스가 &&와 ||를 직접 정의한 경우에는 함수 호출이 되어 단락 평가를 하지 않으므로, 이 성질은 기본 타입의 논리 연산자에만 기대합니다.

조건식의 우선순위

산술과 관계, 논리 연산자가 한 식에 섞이면 다음 순서로 묶입니다.

순서연산자
1후위 x++, x--
2전위와 단항 !, ++x, --x, +, -
3곱셈과 나눗셈 *, /, %
4덧셈과 뺄셈 +, -
5크기 비교 <, >, <=, >=
6같음 비교 ==, !=
7논리 AND &&
8논리 OR
9대입 =, +=, -= 등

산술 연산자가 관계 연산자보다, 관계 연산자가 논리 연산자보다 먼저 묶이므로, x + y > z && z - y < x는 ((x + y) > z) && ((z - y) < x)로 묶입니다. x, y, z가 10, 5, 15라면 왼쪽 15 > 15가 거짓이므로, 단락 평가로 오른쪽은 계산하지 않고 전체가 false가 됩니다. &&가 OR보다 먼저 묶인다는 점도 기억해 두고, 둘을 섞을 때는 괄호로 의도를 밝힙니다.

수학식처럼 비교를 이어 쓰면 C++에서는 다른 뜻이 됩니다. 0 <= x <= 10은 (0 <= x) <= 10으로 묶여, 앞의 비교 결과인 bool을 0이나 1로 바꾸어 10과 비교합니다. 그래서 x가 -3이어도 false <= 10, 즉 0 <= 10이 되어 참이 됩니다. 범위를 검사할 때는 0 <= x && x <= 10처럼 두 비교를 &&로 잇습니다.

정리

  • 관계 연산자는 두 값을 비교해 bool을 만들며, std::string은 내용과 사전 순서로 비교합니다.
  • == 자리에 =를 쓰면 대입이 되어 조건이 대입한 값으로 판단되므로, 비교하려는 곳에 ==를 썼는지 확인합니다.
  • 실수는 반올림 오차 때문에 ==로 비교하기 어려우며, 값의 크기에 맞는 허용 오차를 정해 그 안에 드는지를 봅니다.
  • 논리 연산자 &&, OR, !는 조건을 잇고 뒤집으며, 0이 아닌 값은 참으로 바뀝니다.
  • &&와 OR는 왼쪽만으로 결과가 정해지면 오른쪽을 계산하지 않으므로, 안전 검사를 왼쪽에 둡니다.
  • 산술, 관계, 논리 연산자의 순서로 묶이며, 0 <= x <= 10 같은 이어 쓴 비교는 범위 검사가 아니므로 &&로 잇습니다.

연습 문제

1. 상자의 수를 바꾸면

short_circuit.cpp에서 boxes가 2일 때와 6일 때 무엇이 출력되나요? 두 경우에 오른쪽 조건은 계산되나요?

해설 보기

두 경우 모두 boxes > 0이 참이므로 오른쪽 조건이 계산됩니다. 2이면 10 / 2가 5여서 true이고, 6이면 정수 나눗셈 10 / 6이 1이어서 false가 출력됩니다.

2. 이어 쓴 비교

x가 -3일 때 0 <= x <= 10과 0 <= x && x <= 10은 각각 무엇인가요? x가 20이면 어떻게 되나요?

해설 보기

x가 -3이면 0 <= x <= 10은 false <= 10, 즉 0 <= 10이 되어 참이고, 0 <= x && x <= 10은 첫 조건이 거짓이어서 거짓입니다. x가 20이면 앞의 식은 true <= 10, 즉 1 <= 10이 되어 역시 참이고, 뒤의 식은 둘째 조건이 거짓이어서 거짓입니다. 이어 쓴 비교는 x와 상관없이 언제나 참이 되므로 범위 검사로 쓸 수 없습니다.

3. 대입이 된 조건

int a = 3; 뒤에 if (a = 0) { ... }을 실행하면 중괄호 안이 실행되나요? 실행한 뒤 a의 값은 얼마인가요?

해설 보기

a = 0은 a에 0을 대입하고 그 값 0을 조건으로 쓰므로, 0이 거짓으로 바뀌어 중괄호 안은 실행되지 않습니다. 비교만 하려던 의도와 달리 a의 값은 0으로 바뀌어 있습니다. 비교하려면 a == 0이라고 써야 합니다.

다음 글 비트 연산자에서는 정수를 이루는 각 비트를 직접 다루는 AND, OR, XOR, NOT, 시프트 연산자와, 이것으로 여러 개의 켜짐과 꺼짐을 한 정수에 담는 방법을 다룹니다.