프로그램이 스스로 무언가를 판단하려면 먼저 참과 거짓을 다룰 재료가 있어야 합니다. 나이가 열아홉을 넘었는지, 비밀번호가 맞았는지 같은 물음의 답을 만들어 내는 것이 이번에 볼 연산자들입니다. 크게 세 갈래로, 두 값을 견주는 비교 연산자, 여러 조건을 엮는 논리 연산자, 그리고 값을 이진수 비트 단위로 주무르는 비트 연산자입니다. 앞의 둘은 다음에 배울 조건문 if의 밑재료이고, 마지막은 메모리와 비트를 직접 만지는 저수준 도구입니다.


06.1 참과 거짓의 정체

C에는 원래 참, 거짓만을 위한 전용 타입이 따로 없었습니다. 대신 정수로 다룹니다. 0은 거짓이고, 0이 아닌 모든 값은 참입니다. 그래서 1도 참, -5도 참, 100도 참입니다. 반대로 비교나 논리 연산이 만들어 내는 결과값은 언제나 딱 두 가지, 참이면 1이고 거짓이면 0입니다.


#include <stdio.h>

int main(void) {

int a = 5, b = 3;

printf("a > b : %d\n", a > b); // 참이라 1

printf("a == b : %d\n", a == b); // 거짓이라 0

return 0;

}


실행 결과입니다.


a > b : 1

a == b : 0


비교의 답이 숫자라는 사실이 처음엔 낯설지만, 이 1과 0이 뒤에 배울 조건문의 판단 재료가 됩니다.


06.2 두 값을 견주는 비교 연산자

비교 연산자는 여섯 개입니다. 큼 >, 작음 <, 크거나 같음 >=, 작거나 같음 <=, 같음 ==, 다름 !=입니다. 크거나 같음은 반드시 >= 순서로 적으며 =>는 문법 오류입니다.


int x = 7;

printf("%d\n", x >= 7); // 7은 7 이상이라 1

printf("%d\n", x != 10); // 7과 10은 다르니 1

printf("%d\n", x < 5); // 7은 5 미만이 아니라 0


출력입니다.


1

1

0


06.3 대입 =와 비교 ==의 함정

초보자가 가장 많이 걸려 넘어지는 곳입니다. 등호 하나 =는 대입, 곧 오른쪽 값을 왼쪽 변수에 넣으라는 명령입니다. 등호 둘 ==가 같은지 견주는 비교입니다. 뜻이 완전히 다른데 생김새가 비슷해 자꾸 헷갈립니다. 조건문에서 특히 조심해야 합니다.


함정. 조건 자리에 == 대신 =를 쓰면 컴파일은 대개 통과하지만 뜻이 뒤바뀝니다.


int score = 0;

if (score = 100) { // 비교가 아니라 대입!

printf("만점\n");

}


여기서 score = 100은 score에 100을 넣고, 그 대입식 자체의 값인 100을 조건으로 씁니다. 100은 0이 아니므로 참, 그래서 원래 점수가 몇이든 언제나 만점이 찍히고 score까지 100으로 바뀝니다. 같은지 물으려면 반드시 if (score == 100)이어야 합니다. 이런 실수를 컴파일러가 잡아 주도록 -Wall 경고 옵션을 켜 두시길 권합니다.


06.4 조건을 엮는 논리 연산자

여러 조건을 한데 묶을 때 논리 연산자를 씁니다. 그리고 &&는 양쪽이 모두 참일 때만 참, 또는 ||는 한쪽만 참이어도 참, 부정 !는 참과 거짓을 뒤집습니다. 놀이기구가 키 130 이상이면서 나이 10 이상이어야 탄다고 해 보겠습니다.


int height = 140, age = 8;

int canRide = (height >= 130) && (age >= 10);

printf("탑승 가능? %d\n", canRide); // 나이 미달이라 0


탑승 가능? 0


키 조건은 참이지만 나이 조건이 거짓이라, 그리고 연산은 전체를 거짓 0으로 만듭니다. 만약 ||였다면 한쪽만 참이어도 되니 1이 나왔을 것입니다. 이번엔 부정 연산자입니다.


int loggedIn = 0; // 0은 로그인 안 된 상태

printf("%d\n", !loggedIn); // 뒤집어서 1


!는 0을 1로, 0이 아닌 값을 0으로 바꿉니다. 그래서 로그인이 안 됐을 때만 안내하고 싶으면 if (!loggedIn)처럼 자연스럽게 읽힙니다.


06.5 끝까지 보지 않는 단축 평가

그리고, 또는 연산에는 게으른 성질이 하나 있습니다. &&는 왼쪽이 거짓이면 결과가 이미 거짓으로 확정되므로 오른쪽을 아예 계산하지 않습니다. ||는 왼쪽이 참이면 오른쪽을 건너뜁니다. 이것을 단축 평가(short-circuit evaluation, 왼쪽만으로 답이 정해지면 오른쪽을 생략)라 합니다.


int x = 0;

if (x != 0 && 10 / x > 1) {

printf("통과\n");

}


x가 0이라 왼쪽 x != 0이 거짓이 되고, 그 순간 오른쪽 10 / x는 실행조차 되지 않습니다. 덕분에 0으로 나누는 사고를 막습니다. 함정. 이 순서를 뒤집어 10 / x > 1 && x != 0으로 쓰면 왼쪽이 먼저 계산되어 0으로 나누기가 터집니다. 안전을 검사하는 조건을 왼쪽에 두는 것은 그래서 관용이자 안전장치입니다.


06.6 비트를 직접 만지는 비트 연산자


[C 06] 비교, 논리, 비트 연산자


지금부터는 결이 다릅니다. 앞의 연산자들이 값을 하나의 덩어리로 봤다면, 비트 연산자는 정수를 이진수로 펼쳐 각 자리(비트)를 하나하나 다룹니다. 예를 들어 십진수 6은 이진수로 110, 3은 011입니다. 종류는 네 가지로, 비트 그리고 &, 비트 또는 |, 배타적 또는 ^, 비트 반전 ~입니다.


int a = 6, b = 3; // 110, 011

printf("%d\n", a & b); // 둘 다 1인 자리만: 010 = 2

printf("%d\n", a | b); // 한쪽만 1이어도: 111 = 7

printf("%d\n", a ^ b); // 서로 다를 때만 1: 101 = 5


2

7

5


주의. 논리 연산자 &&와 비트 연산자 &는 기호 하나 차이인데 하는 일이 전혀 다릅니다. 하나짜리 &는 비트별 계산, 둘짜리 &&는 참거짓 판단입니다. 반전 ~는 모든 자리를 0은 1로, 1은 0으로 뒤집습니다.


unsigned char m = 0; // 00000000

printf("%u\n", (unsigned char)~m); // 11111111 = 255


주의. 반전은 부호 있는 정수에 쓰면 결과가 음수가 되어 직관과 어긋나니, 비트를 뒤집을 때는 위처럼 부호 없는 unsigned 타입에 쓰는 편이 안전합니다.


06.7 짝수 판별에 쓰는 비트 그리고

비트 그리고의 대표적 쓰임이 홀짝 가리기입니다. 어떤 수든 맨 끝 비트가 0이면 짝수, 1이면 홀수입니다. & 1은 맨 끝 비트만 남기므로 나눗셈 없이 홀짝을 알 수 있습니다.


int n = 10;

printf("%d\n", n & 1); // 끝 비트가 0, 짝수

printf("%d\n", 7 & 1); // 끝 비트가 1, 홀수


0

1


06.8 비트를 밀어내는 시프트 연산자

시프트 연산자는 비트를 통째로 왼쪽이나 오른쪽으로 밀어냅니다. 왼쪽 시프트 <<는 비트를 왼쪽으로 밀고 빈 자리를 0으로 채웁니다. 한 칸 밀 때마다 값이 두 배가 되는데, 이진수에서 자리를 하나 올리는 것이 곱하기 2이기 때문입니다.


int n = 3; // 011

printf("%d\n", n << 1); // 110 = 6, 두 배

printf("%d\n", n << 2); // 1100 = 12, 네 배


6

12


오른쪽 시프트 >>는 반대로 밀어 한 칸마다 2로 나눈 몫이 됩니다. 20 >> 1은 10, 20 >> 2는 5입니다. 함정. 시프트 폭이 그 타입의 비트 수 이상이거나 음수이면 결과가 정의되지 않은 동작(undefined behavior, 표준이 결과를 보장하지 않는 상태)입니다. 32비트 intn << 32처럼 미는 것이 그렇고, 부호 있는 값을 왼쪽으로 밀어 부호 자리를 침범하는 것도 위험하니 시프트 역시 부호 없는 타입에 쓰는 편이 안전합니다.


06.9 정리하며


[C 06] 비교, 논리, 비트 연산자 (2)[C 06] 비교, 논리, 비트 연산자 (3)


비교 연산자는 두 값을 견줘 1이나 0을 내놓고, 논리 연산자는 그 참거짓을 엮되 왼쪽만으로 답이 정해지면 오른쪽을 건너뛰는 단축 평가로 안전장치가 되기도 합니다. 무엇보다 대입 =와 비교 ==, 논리 &&와 비트 &는 기호 하나 차이로 뜻이 갈리니 늘 눈여겨봐야 합니다. 비트 연산은 짝수 판별이나 두 배 만들기처럼 작은 쓰임부터 익히면 감이 잡힙니다. 코드의 값을 직접 바꿔 가며 출력이 어떻게 달라지는지 확인해 보시길 권합니다.