[C언어 50강] 07강. 연산자 2: 비교/논리 연산자, 단락 평가(short-circuit)
조건문을 잘 쓰는 사람과 못 쓰는 사람의 차이는 문법 지식이 아니라 평가 흐름을 머릿속에서 추적하는 능력에서 갈립니다. 오늘은 비교/논리 연산자와 단락 평가(short-circuit)를 묶어서, C가 조건식을 어떻게 읽고 멈추는지까지 개념 중심으로 정리해보겠습니다.
핵심 개념
- 비교 연산자는 참/거짓을 만든다. C에서는 참은 0이 아닌 값, 거짓은 0이다.
- 논리 연산자
&&,||,!는 단순 계산이 아니라 평가 순서와 중단 조건을 가진 제어 도구다. - 단락 평가는 오른쪽 식을 아예 실행하지 않을 수 있으므로, 함수 호출/증감 연산/포인터 접근과 결합할 때 특히 중요하다.
개념 먼저 이해하기
초보자가 조건문에서 자주 막히는 이유는 “조건식도 결국 식(expression)”이라는 사실을 놓치기 때문입니다. if (x > 10)은 단순히 영어 문장처럼 보이지만, C 컴파일러 입장에서는 x > 10을 먼저 계산해서 정수 0 또는 1(정확히는 0 또는 비0 값)을 만드는 연산입니다. 즉 비교 연산자는 문장 장식이 아니라, 값을 만드는 기계입니다. 이 인식이 생기면 조건식 디버깅이 훨씬 쉬워집니다.
비교 연산자의 핵심은 6개입니다. ==, !=, >, <, >=, <=. 여기까지는 익숙해 보이지만, C에서는 결과가 bool 타입 하나로 고정되는 언어와 달리 역사적으로 정수 개념과 깊게 연결되어 있습니다. C99 이후 _Bool이 도입되고 stdbool.h도 있지만, 실제 현장 코드에서는 여전히 “0이면 거짓, 0이 아니면 참”이라는 규칙으로 읽어야 하는 코드가 많습니다. 예를 들어 if (ptr)는 if (ptr != NULL)의 축약 표현입니다. 코드가 짧아지지만, 의미를 이해하지 못하면 왜 동작하는지 놓치기 쉽습니다.
논리 연산자로 가면 진짜 중요한 지점이 시작됩니다. &&는 양쪽이 모두 참이어야 참, ||는 한쪽만 참이어도 참, !는 참/거짓을 뒤집습니다. 여기까지만 외우면 절반짜리입니다. 나머지 절반은 “왼쪽부터 평가하고 필요 없으면 오른쪽을 평가하지 않는다”는 단락 평가 규칙입니다. A && B에서 A가 거짓이면 B를 볼 필요가 없습니다. 결과는 어차피 거짓이기 때문입니다. 반대로 A || B에서 A가 참이면 B는 필요 없습니다. 결과는 이미 참입니다.
이 규칙은 성능보다 안전성에서 더 빛납니다. 예를 들어 if (ptr != NULL && ptr->count > 0)는 널 포인터 방어의 전형입니다. 왼쪽 ptr != NULL이 거짓이면 오른쪽 ptr->count 접근은 아예 실행되지 않으니, 잘못된 메모리 접근을 막을 수 있습니다. 만약 순서를 바꿔 ptr->count > 0 && ptr != NULL로 쓰면 첫 평가에서 바로 터질 수 있습니다. 즉 같은 의미처럼 보여도 실행 안전성은 순서에 따라 완전히 달라집니다.
또 하나 중요한 부분은 부작용(side effect)입니다. if (a > 0 && ++b > 3)에서 a > 0이 거짓이면 ++b는 실행되지 않습니다. 따라서 어떤 경우에는 b가 증가하고, 어떤 경우에는 증가하지 않습니다. 이게 의도라면 괜찮지만, 모르면 “왜 값이 들쑥날쑥하지?”라는 디버깅 지옥에 들어갑니다. 실무에서는 조건식 내부에서 상태 변경(++, --, 대입, 함수 호출로 내부 상태 변경`)을 최소화해 예측 가능성을 높입니다.
정리하면, 비교/논리 연산자는 if 문을 위한 문법이 아니라 프로그램의 분기 전략을 결정하는 핵심 장치입니다. 특히 단락 평가를 이해하면 널 체크, 입력 검증, 경계 검사, 비용 큰 함수 호출 제어까지 훨씬 안전하고 효율적으로 구현할 수 있습니다. “참/거짓 계산”보다 한 단계 더 나아가 “어떤 식이 실제로 실행되는가”까지 보는 습관을 들이세요. C 실력은 여기서 급격히 올라갑니다.
기본 사용
예제 1) 비교 연산자와 조건 결과 확인
#include <stdio.h>
int main(void) {
int age = 19;
int min_age = 20;
printf("age == min_age : %d\n", age == min_age);
printf("age != min_age : %d\n", age != min_age);
printf("age >= min_age : %d\n", age >= min_age);
printf("age < min_age : %d\n", age < min_age);
if (age >= min_age) {
printf("입장 가능\n");
} else {
printf("입장 불가\n");
}
return 0;
}
설명:
- 비교식은 결국 0 또는 1 형태의 정수 결과로 관찰할 수 있습니다.
if는 “문장”처럼 보이지만 실제로는 식의 결과값을 소비하는 구조입니다.- 디버깅할 때 조건식을
printf로 직접 찍어보면 오해를 빠르게 줄일 수 있습니다.
예제 2) 단락 평가로 널 포인터 방어
#include <stdio.h>
typedef struct {
int count;
} Item;
int main(void) {
Item *ptr = NULL;
if (ptr != NULL && ptr->count > 0) {
printf("count is positive\n");
} else {
printf("안전하게 분기됨 (오른쪽 접근 생략)\n");
}
Item box = {3};
ptr = &box;
if (ptr != NULL && ptr->count > 0) {
printf("count=%d\n", ptr->count);
}
return 0;
}
설명:
- 첫 번째 if에서는 왼쪽이 거짓이므로 오른쪽
ptr->count가 평가되지 않습니다. - 단락 평가는 “최적화”가 아니라 “실행 안전성 보장” 도구로 이해하는 게 맞습니다.
- 팀 코드 리뷰에서 널 체크 순서는 필수 점검 항목입니다.
예제 3) 부작용이 있는 조건식의 위험
#include <stdio.h>
int main(void) {
int a = 0;
int b = 1;
if (a > 0 && ++b > 1) {
printf("case1 true\n");
}
printf("case1 b=%d\n", b); // a>0 거짓이라 ++b 미실행 -> b는 1
a = 2;
if (a > 0 && ++b > 1) {
printf("case2 true\n");
}
printf("case2 b=%d\n", b); // a>0 참 -> ++b 실행 -> b는 2
return 0;
}
설명:
- 같은 코드라도 왼쪽 조건 결과에 따라 오른쪽 증감 실행 여부가 달라집니다.
- 조건식에 상태 변경을 섞으면 “읽기 난이도 + 버그 가능성”이 동시에 올라갑니다.
- 실무에서는
++b; if (a > 0 && b > 1)처럼 분리해 의도를 명확히 쓰는 편이 안전합니다.
예제 4) 값 범위 검증 패턴
#include <stdio.h>
int is_valid_score(int score) {
return (score >= 0 && score <= 100);
}
int main(void) {
int s1 = 95;
int s2 = -8;
if (is_valid_score(s1)) {
printf("s1 valid\n");
}
if (!is_valid_score(s2)) {
printf("s2 invalid\n");
}
return 0;
}
설명:
- 논리 연산을 함수로 추출하면 조건의 의미가 이름으로 드러납니다.
!연산자는 “부정” 자체보다 “조건 의미 반전”으로 읽는 습관이 중요합니다.- 검증 함수를 분리하면 테스트 작성과 재사용성이 함께 좋아집니다.
자주 하는 실수
실수 1) =와 ==를 혼동
- 원인: 조건문에서도 등호를 습관적으로 하나만 써서 대입이 발생함.
- 해결: 비교는
==를 강제하고, 컴파일 옵션-Wall -Wextra -Werror로 경고를 에러 처리합니다.
실수 2) 널 체크 순서를 반대로 작성
- 원인:
ptr->value > 0 && ptr != NULL처럼 의미만 보고 순서를 대충 씀. - 해결: 포인터 검증을 항상 왼쪽에 둡니다.
ptr != NULL && ...패턴을 팀 규칙으로 고정하세요.
실수 3) 조건식 안에서 변수 변경 남발
- 원인: 코드를 짧게 쓰려다
if (x > 0 && y++ < 3)같은 표현을 반복함. - 해결: 상태 변경과 조건 검사를 분리하세요. 한 줄 절약보다 디버깅 시간 절약이 훨씬 큽니다.
실수 4) 참/거짓을 1/0으로만 고정 해석
- 원인: “참은 무조건 1”이라고 착각해 기존 C 코드의 관용 표현을 오해함.
- 해결: C의 기본 규칙은 “0은 거짓, 0이 아닌 값은 참”입니다. 출력 결과를 통해 직접 검증해보세요.
실무 패턴
- 포인터 접근 전 검증:
p != NULL && p->field ...를 기본 템플릿으로 사용합니다. - 값 검증은 함수로 추출:
is_valid_xxx()형태로 분리해 읽기성과 테스트 용이성을 확보합니다. - 조건식은 최대 2~3개 논리 항목을 넘기지 말고, 넘기면 중간 bool 변수를 도입합니다.
- 비용 큰 함수 호출은 왼쪽 조건으로 선별한 뒤 오른쪽에 배치해 불필요한 실행을 줄입니다.
- 코드 리뷰 체크리스트:
- 대입/비교 오타 여부
- 단락 평가 의존 코드의 순서 적절성
- 조건식 내 부작용 존재 여부
오늘의 결론
한 줄 요약: 비교/논리 연산자의 본질은 참거짓 계산이 아니라 평가 순서 제어이며, 단락 평가를 이해하면 안전하고 읽기 쉬운 조건문을 만들 수 있습니다.
연습문제
- 정수
x가 10 이상 99 이하인지 판별하는 조건식을 작성하고, 왜 그 식이 안전한지 설명해보세요. char *name = NULL;일 때 런타임 오류 없이 name이 비어 있지 않은 문자열인지 검사하는 조건식을 작성해보세요.- 다음 코드를 부작용 없는 형태로 리팩터링하세요.
- 기존:
if (ready || ++retry_count < 3) { ... }
- 기존:
이전 강의 정답
지난 6강(연산자 1: 산술/대입/증감) 연습문제 해설:
int a=10, b=3;일 때
a / b결과:3(정수 나눗셈이라 소수점 버림)a % b결과:1(10을 3으로 나눈 나머지)(double)a / b결과:3.333333...(실수 연산으로 승격)
result = base + bonus * rate - penalty++;리팩터링 예시
int adjusted_bonus = bonus * rate;int old_penalty = penalty;penalty++;result = base + adjusted_bonus - old_penalty;
- 시작 50점, 보너스 3회(+7), 패널티 2회(-4) 예시
score = 50;score += 7; score += 7; score += 7;score -= 4; score -= 4;- 최종 점수:
63
실습 환경/재현 정보
- 컴파일러: Apple clang version 17.x
- 컴파일 옵션:
-std=c11 -Wall -Wextra -O0 - 실행 환경: macOS arm64, zsh 터미널
- 재현 체크:
clang -std=c11 -Wall -Wextra -O0 lesson07.c -o lesson07./lesson07- 단락 평가 동작 확인: 널 체크/증감 실행 여부 출력값으로 검증