관계 연산자와 논리 연산자
두 값을 비교해 bool을 만드는 관계 연산자와 등호의 함정, 실수를 비교할 때의 허용 오차를 다루고, 여러 조건을 잇는 논리 연산자와 오른쪽을 계산하지 않고 끝내는 단락 평가, 수학식처럼 이어 쓴 비교가 C++에서 다르게 묶이는 이유를 확인합니다.
프로그램은 계산만 하지 않고, 점수가 60점 이상이면 합격으로 처리하는 것처럼 조건에 따라 다른 일을 합니다. 그 조건을 만드는 것이 두 값을 비교하는 관계 연산자(relational operator)와, 여러 조건을 잇는 논리 연산자(logical operator)입니다.
두 연산자는 모두 참과 거짓을 나타내는 bool 값을 만들며, 다음 장의 if와 반복문의 조건으로 쓰입니다. 이 글은 비교할 때의 함정과, 논리 연산자가 필요한 만큼만 계산하는 단락 평가, 여러 연산자가 섞인 조건식이 묶이는 방식을 봅니다.
관계 연산자
관계 연산자는 두 피연산자를 비교해 true나 false를 돌려줍니다.
| 연산자 | 뜻 | 예 | 결과 |
|---|---|---|---|
== | 같다 | 5 == 5 | true |
!= | 다르다 | 5 != 3 | true |
> | 크다 | 5 > 3 | true |
< | 작다 | 5 < 3 | false |
>= | 크거나 같다 | 5 >= 5 | true |
<= | 작거나 같다 | 5 <= 3 | false |
#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: truestd::string끼리의 ==는 내용이 같은지를, <는 사전에서 앞에 오는지를 비교하므로 "Alice" < "Bob"은 참입니다. 이것은 std::string이 제공하는 비교이며, 뒤에서 배울 C 문자열인 char 배열을 ==로 비교하면 내용이 아니라 주소를 비교하게 됩니다. 비교 식을 <<와 함께 쓸 때는 우선순위 때문에 괄호로 감쌉니다.
비교할 때 가장 흔한 실수는 == 대신 =를 쓰는 것입니다. if (a = 10)은 오류가 아니라, a에 10을 대입한 뒤 그 값 10을 bool로 바꾸어 조건으로 쓰는 올바른 식이어서 조건은 언제나 참이 됩니다. GCC는 -Wall에서 조건 안의 대입에 경고를 내지만, 경고에만 기대지 말고 비교하려는 곳에는 ==를 썼는지 확인합니다.
실수를 비교할 때는 기본 데이터 타입에서 본 반올림 오차를 생각해야 합니다. 0.1 + 0.2와 0.3은 서로 다른 double로 저장되므로 ==로 비교하면 거짓이 되며, 오차를 허용해야 한다면 두 값의 차이가 정한 범위 안에 드는지를 봅니다.
#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이라는 낱말을 쓸 수도 있지만, 대부분의 코드는 기호를 씁니다.
#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: falsecanRent는 셋째 조건 points >= 80은 참이지만 첫 조건 age >= 21이 거짓이어서 전체가 거짓입니다. 이 나이와 점수의 기준은 논리식을 연습하기 위한 가정이며, 실제 운전이나 대여 자격의 기준은 아닙니다.
단락 평가
&&와 ||는 왼쪽 피연산자만으로 결과가 정해지면 오른쪽 피연산자를 아예 계산하지 않습니다. 이것을 단락 평가(short-circuit evaluation)라고 합니다.
기본 제공 논리 연산자의 조건부 오른쪽 평가
내장 &&/||가 왼쪽 값에 따라 오른쪽 평가를 생략하는 실제 실행 규칙이다. 본문 nullptr 검사 사례의 조건 하나를 명시해 선택된 경로만 진행하고 전체 분기는 위 관계도와 표에 남긴다.
BUILT-IN SHORT-CIRCUIT CONTRACT
논리 연산은 왼쪽 결과에 따라 오른쪽 평가를 생략한다
왼쪽 A를 먼저 평가해 bool로 문맥 변환합니다. 그 값만으로 결과가 정해지지 않을 때에만 오른쪽 B를 평가합니다.
A && B
전체가 참이려면 두 피연산자가 모두 참이어야 합니다.
bool(A) == falseB미평가 · 결과는 즉시falsebool(A) == trueB평가 · 결과는bool(B)
A || B
전체가 거짓이려면 두 피연산자가 모두 거짓이어야 합니다.
bool(A) == trueB미평가 · 결과는 즉시truebool(A) == falseB평가 · 결과는bool(B)
이 계약은 기본 제공 &&와 ||에 적용됩니다. 논리 연산자를 오버로드한 함수 호출은 오른쪽 피연산자를 생략하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 기본 제공 AND는 A가 false면 B를 평가하지 않고 false로 끝내며, A가 true일 때 B를 평가한다. 기본 제공 OR는 A가 true면 B를 평가하지 않고 true로 끝내며, A가 false일 때 B를 평가한다. 두 연산은 독립 비교이며 사용자 정의 함수 호출에 적용하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 왼쪽 검사 nullptr 사례의 내장 &&에서 먼저 포인터를 검사한다. |
|
|
| 오른쪽 생략 선택한 false 경로에서는 역참조식을 평가하지 않는다. |
|
|
기본 제공 논리 연산자의 조건부 오른쪽 평가
내장 &&/||가 왼쪽 값에 따라 오른쪽 평가를 생략하는 실제 실행 규칙이다. 본문 nullptr 검사 사례의 조건 하나를 명시해 선택된 경로만 진행하고 전체 분기는 위 관계도와 표에 남긴다.
BUILT-IN SHORT-CIRCUIT CONTRACT
논리 연산은 왼쪽 결과에 따라 오른쪽 평가를 생략한다
왼쪽 A를 먼저 평가해 bool로 문맥 변환합니다. 그 값만으로 결과가 정해지지 않을 때에만 오른쪽 B를 평가합니다.
A && B
전체가 참이려면 두 피연산자가 모두 참이어야 합니다.
bool(A) == falseB미평가 · 결과는 즉시falsebool(A) == trueB평가 · 결과는bool(B)
A || B
전체가 거짓이려면 두 피연산자가 모두 거짓이어야 합니다.
bool(A) == trueB미평가 · 결과는 즉시truebool(A) == falseB평가 · 결과는bool(B)
이 계약은 기본 제공 &&와 ||에 적용됩니다. 논리 연산자를 오버로드한 함수 호출은 오른쪽 피연산자를 생략하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 기본 제공 AND는 A가 false면 B를 평가하지 않고 false로 끝내며, A가 true일 때 B를 평가한다. 기본 제공 OR는 A가 true면 B를 평가하지 않고 true로 끝내며, A가 false일 때 B를 평가한다. 두 연산은 독립 비교이며 사용자 정의 함수 호출에 적용하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 왼쪽 검사 nullptr 사례의 내장 &&에서 먼저 포인터를 검사한다. |
|
|
| 오른쪽 생략 선택한 false 경로에서는 역참조식을 평가하지 않는다. |
|
|
그림처럼 A && B는 A가 거짓이면 B와 상관없이 거짓이므로 B를 계산하지 않고, A가 참일 때만 B를 계산합니다. A || B는 A가 참이면 B를 계산하지 않고 참으로 끝나며, A가 거짓일 때만 B를 계산합니다. 그래서 왼쪽에 안전 검사를 두면, 그 검사를 통과할 때만 오른쪽의 위험한 계산이 실행됩니다.
#include <iostream>
int main() {
int boxes{0};
bool enough = (boxes > 0) && (10 / boxes >= 2);
std::cout << std::boolalpha << enough << "\n";
}falseboxes > 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, 시프트 연산자와, 이것으로 여러 개의 켜짐과 꺼짐을 한 정수에 담는 방법을 다룹니다.