C는 처음 27년 동안 불리언 타입 없이 지냈습니다. 실수로 빠뜨린 게 아니라 의도된 설계 결정이고, 그 결과는 C를 배우는 첫날부터 마주치게 되며, 여기서 곧바로 따라 나오는 유명한 버그가 하나 있습니다.
0은 거짓, 나머지는 모두 참
C의 조건 규칙에는 특별한 참/거짓 값이 없습니다. if, while, for, &&, ||, !은 모두 딱 하나만 묻습니다. 이 값이 0인가?
음수도 참입니다. 소수도 참입니다. 널이 아닌 포인터도 참입니다. 타입이 무엇이든 오직 0만 거짓입니다.
그래서 C 코드에는 규칙을 알기 전까지는 지나치게 짧아 보이는 관용구가 가득합니다.
if (count) /* count가 0이 아니면 */
if (!count) /* count가 0이면 */
if (p) /* p가 NULL이 아니면 */
if (!strcmp(a, b)) /* 두 문자열이 같으면 - strcmp는 일치할 때 0을 반환 */
마지막 줄에서 많은 사람이 걸립니다. strcmp는 문자열이 일치할 때 0을 반환하므로, if 안에서 "같다"가 "아니다"로 읽히는 것이죠. if (strcmp(a, b) == 0)이라고 쓰면 더 명확하면서 하는 일은 똑같습니다.
비교 연산은 int를 내놓습니다
C에서 비교는 특별한 종류의 표현식이 아닙니다. 값이 1 또는 0인 평범한 int로 평가되므로, 출력하거나 저장하거나 산술에 쓸 수 있습니다.
! 연산자도 같은 방식입니다. 피연산자가 0이면 1을, 아니면 0을 내놓습니다. 그래서 !!x는 어떤 값이든 정확히 0이나 1로 정규화하는 고전적인 관용구입니다.
bool, true, false (C99)
C99는 _Bool이라는 진짜 불리언 타입과, 거기에 읽기 좋은 이름을 붙여 주는 헤더를 추가했습니다.
<stdbool.h>는 아주 작습니다. bool을 _Bool로, true를 1로, false를 0으로 정의하는 게 헤더의 전부입니다. 하지만 이것을 쓰면 int flag = 1;이 결코 해내지 못하는 방식으로 의도가 눈에 보입니다.
_Bool에는 진짜로 구별되는 동작이 하나 있습니다. 0 또는 1만 저장한다는 것입니다. 0이 아닌 값을 대입하면 정확히 1로 변환됩니다.
printf에는 여전히 불리언용 %b가 없습니다. %d로 출력하거나, 직접 텍스트로 변환하세요.
C23에서는 이것이 한층 더 간단해졌습니다. bool, true, false가 진짜 키워드가 되어 헤더 포함이 더 이상 필요 없습니다. 다만 오늘날 대부분의 코드는 여전히 C17을 대상으로 하니 #include <stdbool.h>는 남겨 두세요.
함수에서 불리언 반환하기
bool이 자기 자리를 가장 분명하게 증명하는 곳입니다.
반환 타입이 bool이면 그 값이 무슨 뜻인지 호출자에게 알려 줍니다. check_file이라는 함수의 반환 타입이 int라면 불리언일 수도, 개수일 수도, 오류 코드일 수도 있습니다. 그리고 C 라이브러리 관례에서는 세 번째인 경우가 많은데, 그때는 0이 성공을 뜻합니다. 타입을 명시하면 추측할 일이 사라집니다.
= 대 == 버그
대입은 대입된 값을 결과로 내놓는 표현식이고 0이 아닌 값은 모두 참이므로, 다음 코드는 그대로 컴파일됩니다.
두 가지가 한꺼번에 잘못됐습니다. 조건이 x에 5를 대입해 원래 값을 없앴고, 그다음 0이 아닌 5를 검사했으므로 분기가 실행됐습니다. 이 프로그램에는 기본 설정에서 오류도 경고도 없습니다.
더 나쁜 건 0을 쓴 버전은 분기가 결코 실행되지 않는다는 점입니다.
if (found = 0) { /* 절대 실행되지 않음 - 게다가 found는 이제 0 */ }
유용한 순서대로 세 가지 방어책이 있습니다.
경고를 켜고 컴파일하세요. -Wall이 이것을 잡아냅니다.
warning: suggest parentheses around assignment used as truth value
경고를 0으로 만드는 것이 여기서의 진짜 해결책이고, 비용은 플래그 하나입니다.
상수를 앞에 쓰세요. "요다 조건문"은 오타를 컴파일 오류로 바꿔 줍니다.
if (5 == x) /* 올바름 */
if (5 = x) /* 오류: lvalue가 아님 - 컴파일러가 막아 줍니다 */
이 방식을 좋아하는 팀도 있고 읽기 어렵다고 여기는 팀도 있습니다. 어느 쪽이든 나름의 근거가 있지만, 효과가 있다는 것만은 분명합니다.
정말 그럴 의도라면 분명하게 쓰세요. 조건 안의 대입은 입력을 읽을 때 진짜로 유용합니다.
int c;
while ((c = getchar()) != EOF) { ... }
c = getchar()를 감싼 추가 괄호는 연산자 우선순위 때문에 반드시 필요하고, 동시에 이 대입이 의도된 것임을 알려 주는 신호이기도 합니다. 이것이 바로 그 관용구입니다. 조건 안의 대입이 이런 모양이 아니라면, 십중팔구 오타입니다.
흔한 불리언 실수
true와 비교하기. if (flag == true)는 stdbool.h와 함께라면 동작하지만, flag가 42를 담은 int가 되는 순간 깨집니다. 42 == 1은 거짓이기 때문이죠. if (flag)라고 쓰세요.
연쇄 비교. if (0 < x < 10)은 항상 참입니다. 0 < x가 0이나 1을 내놓는데, 둘 다 10보다 작으니까요. if (x > 0 && x < 10)이라고 쓰세요.
논리 연산 대신 비트 연산. &와 |는 &&, ||가 아닙니다. 단락 평가를 하지 않고 비트끼리 결합합니다.
두 피연산자 모두 "참"인데도 a & b는 0입니다. &&를 써야 할 자리에 &를 쓰면 특정 값에서만 틀리는 조건이 만들어지는데, 이것이 가장 찾기 어려운 종류의 버그입니다.
부동소수점을 같은지 비교하기. if (0.1 + 0.2 == 0.3)은 거짓입니다. 대신 허용 오차와 비교하세요. 이유는 자료형에서 다룹니다.
자주 묻는 질문
C에도 불리언 타입이 있나요?
C99부터는 있습니다. _Bool이 내장 타입이고, <stdbool.h>를 포함하면 더 읽기 좋은 이름인 bool, true, false를 쓸 수 있습니다. C99 이전에는 불리언이 아예 없어서 프로그램들이 int에 거짓은 0, 참은 1을 담아 썼는데, 이 방식도 여전히 동작하고 여전히 흔합니다.
C에서 무엇이 참으로 취급되나요?
0이 아닌 모든 값입니다. 조건에서 1, -5, 0.01, 널이 아닌 포인터는 모두 참이고, 0, 0.0, 널 포인터만 거짓입니다. 별도의 불리언 검사는 없습니다. if (x)는 그저 x가 0이 아닌지를 확인할 뿐입니다.
C에서 bool은 어떻게 쓰나요?
#include <stdbool.h>를 추가하면 bool ready = true;가 기대한 대로 동작합니다. 내부적으로 bool은 _Bool이고, 이 타입은 0 또는 1만 저장하므로 0이 아닌 값을 대입하면 정확히 1이 저장됩니다.
C에서 =와 ==의 차이는 무엇인가요?
=는 대입, ==는 비교입니다. if (x = 5)는 x에 5를 대입한 뒤 5를 검사하는데, 5는 0이 아니므로 분기가 항상 실행됩니다. if (x == 5)는 같은지를 검사합니다. 앞의 코드가 오류 없이 컴파일된다는 사실이 바로 -Wall과 if (5 == x)라고 쓰는 습관이 존재하는 이유입니다.