5-2-나.논리 연산자

논리 연산자는 주로 관계 연산자와 함께 사용되며 두 개 이상의 조건식을 결합하여 하나의 진리값을 만들어 낸다. 다음 세 가지 종류가 있다.

 

연산자

설명

!

논리 부정(Not)

논리식의 진위를 반대로 만든다.

&&

논리곱(And)

논리식이 모두 참이어야 참이다.

||

논리합(Or)

논리식 하나만 참이면 참이다.

 

먼저 !연산자에 대해 알아보자. ! 연산자는 조건식 하나를 피연산자로 취하는 단항 연산자로서 조건식을 반대로 바꾼다. 즉 조건문의 평가 결과가 참이면 거짓으로 바꾸고 거짓이면 참으로 바꾼다.

 

if (a == 1) 명령;

 

이 문장은 변수 a의 값이 1이면 명령을 실행하라는 뜻이다. 앞에서 설명했듯이 조건식은 진위에 따라 0또는 1의 값을 가지는데 ! 연산자는 이 결과를 반대로 만든다. 조건식이 0이면 1로 만들고 0이외의 값(일반적으로 1)이면 0으로 바꾼다. 그래서 조건식앞에 ! 연산자를 붙이면 반대의 문장이 된다.

 

if (!(a == 1)) 명령;

 

이렇게 하면 a가 1이 아닐 때만 명령을 실행하며 a가 1일 때는 명령을 실행하지 않는다. 조건문의 부정은 역조건문과 동일하므로 다음과 같이 바꿀 수도 있다.

 

if (a != 1) 명령;

 

"a가 1이 아니다"라는 조건문은 "a가 1이다가 아니다"와 같은 표현이므로 어떤 식으로 표현하든지 결과는 동일하다. 하지만 논리식이 여러 개 연결되어 있거나 수식이 무척 복잡하면 논리식의 역을 구하기가 무척 어렵기 때문에 이럴 때는 원래 조건문을 모두 괄호로 싸고 앞에 ! 연산자를 쓰는 것이 훨씬 더 쉽다. "a가 1이다"의 역조건 정도야 쉽게 암산할 수 있지만 다음과 같은 논리식의 역조건을 구하는 것은 좀 어렵다.

 

a가 5보다 크고 10보다 작으며 b의 절대값이 c의 제곱보다 크다

 

어렵다기보다는 사실 귀찮다고 표현하는 것이 옳을 것이다. 이런 논리식의 역을 구하려면 역, 이, 대우, 드 모르강의 법칙같은 것들이 동원되어야 하는데 이런 것을 대신해 주는 것이 바로 ! 연산자이며 다음과 같이 쓰면 된다. 얼마나 간단한가?

 

!(a가 5보다 크고 10보다 작으며 b의 절대값이 c의 제곱보다 크다)

 

&& 연산자와 || 연산자는 두 개의 논리식을 피연산자로 가지는 이항 연산자이며 두 논리식의 값을 정해진 규칙에 따라 결합하여 하나의 진리값을 만든다. 이 연산자들이 논리식을 어떻게 결합시키는지는 진리표를 만들어 보면 쉽게 알 수 있다.

 

좌변 논리식

우변 논리식

&& 연산자

|| 연산자

1

1

1

1

1

0

0

1

0

1

0

1

0

0

0

0

 

&& 연산자는 말 그대로 And, 즉 양쪽이 모두 참일 때만 참이 되며 둘 중 하나라도 거짓이면 전체 논리식은 거짓이 된다. 두가지 조건이 모두 만족할 때만 어떤 동작을 하고 싶다면 이 연산자를 사용하면 된다. 예를 들어 어떤 변수가 일정 범위에 있는지 조사할 때 이 연산자가 사용되는데 다음 조건문은 a가 5보다 크고 10보다 작을 때만 명령을 실행한다.

 

if (a > 5 && a < 10) 명령;

 

a > 5 조건과 a < 10 조건 둘 다 모두 참일 때만 전체식은 참이 된다. 만약 a가 7이라면 이 값은 5보다는 크고 10보다는 작으므로 참이다. 그러나 a가 12라면 5보다 크기는 하지만 10보다 작지 않으므로 전체 조건은 거짓이다. a가 6,7,8,9중 하나일 때만 전체 논리식이 참이 된다. 만약 5와 10도 포함시키고 싶다면 <, > 대신 <=, >=관계 연산자를 사용하면 될 것이다.

두 개의 조건을 하나로 묶기 위해 && 연산자를 사용했는데 수학에서와 같은 (5 < a < 10) 이런 형식은 지원하지 않는다. 에러는 아니지만 5 < a 식이 먼저 평가되고 그 결과가 10보다 작은지를 비교하는데 a의 값에 상관없이 0, 1은 항상 10보다 작으므로 이 조건식은 항상 참이다. 수학에서는 이런 식으로 범위를 표현할 수 있지만 C에서는 두 조건을 따로 평가한 후 논리 연산자로 연결해야 한다. 이 연산자를 계속 사용하여 세 가지 이상의 조건을 연결할 수도 있다.

 

if (a > 5 && a < 10 && a != 7) 명령;

 

5~10사이에 있되 단, 7인 경우는 제외하고 싶다면 이렇게 세 조건문을 모두 And로 연결한다. 이렇게 되면 a가 6,8,9일 때만 전체 조건이 참이 될 것이다.

|| 연산자는 Or, 즉 양쪽 조건중 어느 하나라도 참이면 전체식은 참이 되며 둘 다 거짓일 때만 전체식도 거짓이 된다. 두가지 조건중 하나라도 만족할 때 어떤 동작을 하고 싶다면 두 조건문을 이 연산자로 묶어 준다.

 

if (a == 3 || a == 5) 명령;

 

이 문장은 a가 3이거나 5일 때 명령을 실행한다. a가 3이라고 할 때 a == 5라는 조건은 거짓이지만 앞쪽의 a == 3이 참이므로 전체 조건문은 참이 된다. || 연산자도 세 개 이상의 조건을 연결할 수 있으며 && 와 ||을 같이 사용할 수도 있다. 다음과 같이 말이다. 단, 이렇게 조건이 복잡할 때는 괄호로 조건의 그룹을 적당히 묶어 주는 것이 좋다. 그렇지 않으면 연산 순위에 의해 엉뚱한 결과가 나올 수도 있다.

 

if ((a > 5 && a < 10) || (b >= 20 && b <= 100) && c != 7) 명령;

 

C의 논리 연산자는 속도 향상과 안전을 위해 불필요한 연산은 하지 않는다. a가 5~10의 범위에 있다는 (a > 5 && a < 10)조건식의 경우를 보자. 우선 좌변을 먼저 평가하여 a가 5보다 큰지 점검한다. 만약 a가 5보다 크다면 전체식의 진리 판단을 위해 우변의 조건식을 점검해 보겠지만 그렇지 않다면 우변은 아예 평가하지 않는다. 예를 들어 a가 2라고 한다면 이 값은 5보다 크지 않으므로 a > 5 조건식은 거짓이 된다. 좌변의 조건식이 거짓이면 우변의 진위와는 상관없이 전체식은 이미 거짓으로 결정되었으므로 우변을 평가해 볼 필요가 없다.

|| 연산자도 마찬가지다. (a == 3 || a == 5) 조건문을 평가할 때 a가 3이라면 좌변이 참이므로 우변이 참이든 거짓이든 전체 조건식의 값은 이미 결정난 것이므로 더 이상 평가하지 않고 전체식의 결과를 리턴한다. 요약하자면 && 연산자는 좌변이 거짓이면 나머지 조건식을 무시하고 전체를 거짓으로 평가하며 || 연산자는 좌변이 참이면 나머지 조건식을 무시하고 전체를 참으로 평가한다. 이미 결정난 값에 대해 불필요한 연산을 하지 않으므로써 실행 속도를 높이는데 컴파일러의 이런 기능을 쇼트 서키트(Short Circuit)라고 한다. 지원하는 컴파일러도 있고 그렇지 않은 컴파일러도 있는데 최신 컴파일러는 모두 이 기능을 지원한다.

그깟 조건문 평가 하나 생략하는게 뭐 대단한 차이가 있을까 싶겠지만 우변이 함수 호출문일 경우 굉장한 차이가 있을 수 있다. 또한 이 기능은 코드의 안전성을 높이는데도 기여한다. 다음 조건문을 보자.

 

if (a != 0 && b/a == 3) 명령;

 

a가 0이 아니고 b를 a로 나눈 값이 3일 때 명령을 실행하라는 뜻이다. a가 0이라면 첫 번째 조건이 벌써 거짓이 되므로 쇼트 서키트에 의해 b/a == 3이라는 조건식은 아예 평가되지도 않는다. 만약 쇼트 서키트 기능이 없다면 a가 0인 상태에서 b/a 연산을 하게 될 것이고 이는 치명적인 예외를 발생시킨다. 왜냐하면 임의의 수를 0으로 나누는 연산은 할 수 없기 때문이다. 그래서 b를 a 로 나누기 전에 먼저 a가 0이 아니라는 조건을 앞에 두어 이런 예외를 피해 가도록 했다. 만약 쇼트 서키트 기능이 없다면 위 조건문은 다음과 같이 고쳐 쓰는 수밖에 없다.

 

if (a != 0) {

     if (b/a == 3) 명령;

}

 

얼마나 불편한가? 쇼트 서키트 기능은 컴파일러가 제공하는 일종의 서비스이므로 개발자가 이 기능의 존재에 대해 신경쓸 필요는 없다. 그러나 이 기능의 존재를 알고 있으면 몇 가지 유리한 경우가 있으며 더 빠른 코드를 작성하는데 이용할 수 있다. 쇼트 서키트의 존재를 아는가 모르는가에 따라 작성된 프로그램의 품질차가 발생하며 때로는 안전도가 달라지기도 한다.

&&, || 이항 연산자는 교환 법칙이 성립하므로 좌우 조건식을 바꾸어도 결과는 동일하다. (a == 3 || a == 5) 조건문과 (a == 5 || a == 3) 조건문이 아무 차이가 없다는 것을 직관적으로 이해할 수 있을 것이다. 그러나 쇼트 서키트 기능을 고려하면 함부로 교환 법칙을 적용해서는 안된다. 다음 문장은 아주 위험하다.

 

if (b/a == 3 && a != 0) 명령;

 

쇼트 서키트의 지원을 받으려면 a가 0이 아닌지 점검하는 조건식이 왼쪽에 와야 한다. 또 이런 경우를 가정해 보자. 변수값을 평가하는 단순 조건A와 조건 판단에 시간이 많이 걸리는 조건B가 있다고 하자. B는 지구 반대편에 있는 웹 서버가 동작중인지 점검한다거나 데이터 베이스의 총 용량이 초과되었는지를 점검하는 조건식이며 평가하는데 시간이 아주 많이 걸린다. 이 두 조건을 논리 연산자로 연결할 때는 (A && B)나 (A || B)로 써야 한다.

순서를 바꾸어 (B && A)나 (B || A)로 조건문을 작성하면 결과는 똑같더라도 굉장히 손해본다. (B && A) 조건식의 진위 판단을 위해 오랜 시간동안 B를 먼저 평가하여 웹 서버가 동작중임을 알아 냈는데 뒤쪽의 A가 거짓이라면 B를 평가하는 시간만 버린 셈이 되는 것이다. 가급적이면 쉽게 조사할 수 있는 조건을 앞쪽에 두는 것이 유리하다. 또 반드시 실행해야 할 조건도 가급적 앞쪽에 배치해야 한다.

 

if (a == 8 || (ch=getch()) != ' ') {

     switch (ch) {

          ....

     }

}

 

원래 의도는 a가 8이고 입력받은 문자 ch가 공백이 아니면 ch에 따른 분기를 하고자 하는 것인데 a가 8이면 getch가 아예 호출되지도 않으므로 ch가 쓰레기값을 가지게 되며 이후 동작은 예측할 수 없게 된다. getch는 반드시 호출되어야 하므로 조건문의 순서를 바꾸어야 하며 그보다 더 좋은 코드는 ch=getch() 호출문을 if문 이전으로 옮기는 것이다.