5장 연산자 - 응용

덧셈, 뺄셈 처럼 숫자 계산처럼 보이지는 않지만 명백히 연산자인것이 있다.

sizeof 처럼 컴파일러가 수행하고 CPU가 실행하지 않는 특수한 연산자도 있다.

그러므로 프로그래밍을 위해 반드시 주의하고 미리 대응해야한다.

sizeof 연산자

sizeof 연산자는 피연산자의 자료형에 대한 연산이다. sizeof(7) 라는 연산은 정수 7를 말하는것이 아니라 sizeof(int) 라는 연산을 의미한다. int 형의 크기는 4바이트 이므로 결과는 4가 된다. sizeof 연산자의 피연산자는 자료형 이다.

#include <stdio.h>

int main(void){
    int nData = 10;
    printf("%d %d %d\n", sizeof(10), sizeof(nData),sizeof(int));
    printf("%d %d\n", sizeof('A'), sizeof(char));
    printf("%d %d\n", sizeof(2.34F), sizeof(2.34));
    return 1;

}

실행결과
❯ ./welcome
4 4 4
4 1
4 8

sizeof 연산자의 피연산자가 자료형임을 생각하면 이해하기 쉽고 피연산자로 기술한 연산식을 실제 실행하지 않는다. sizeof 연산자는 프로그램을 빌드후 CPU 가 실행하는 런타임 연산자가 아니라 컴파일러가 컴파일 할때 수행되는 연산자라는것이다. 따라서 아무리 많이 사용하여도 프로그램의 수행능력이 떨어지지 않는다.

관계 연산자

두 피연산자의 값을 비교해 참(True,1), 거짓(False, 0)의 결과를 내는 연산자이다.

연산자 설명 예제 결과
== 같음을 비교 a == b ab와 같으면 참
!= 다름을 비교 a != b ab와 다르면 참
> 크기를 비교 (크다) a > b ab보다 크면 참
< 크기를 비교 (작다) a < b ab보다 작으면 참
>= 크거나 같음을 비교 a >= b ab보다 크거나 같으면 참
<= 작거나 같음을 비교 a <= b ab보다 작거나 같으면 참

'참'의 정확한 의미는 "0이 아닌 모든 값" 이다.

#include <stdio.h>

int main(void){
    int x = 5, y = 7;
    printf("%d\n", x == y);
    printf("%d\n", x != y);
    printf("%d\n", x > y);
    printf("%d\n", x < y);
    return 1;
}
실행결과
❯ ./welcome
0
1
0
1

논리 연산자

논리 연산자는 참/거짓으로 결론 내릴 수 있는 두 대상을 피연산자로 사용한다. 관계 연산식이 있고 다시 관계 연산식의 피연산자로 산술 연산식이 올 수 도 있는데 이렇게 완성된 논리식을 조건식이라고 부른다.

조건식은 프로그래머에게 중요하다. 소프트웨어는 다양한 경우를 고려하고 각 경우에 적합한 연산을 수행해야한다. 조건에 맞는 경우와 그렇지 않는 경우를 나눠 처리하고 여러번 중첩하여 다양하게 대응할 수 있다.

논리합(OR)과 논리곱(AND)

논리합(OR)를 의미하는 연산자는 || 이다.

#include <stdio.h>

int main(void){
    printf("%d\n", 4 > 3 || 1 > 5);
    return 1;
}

실행결과
❯ ./welcome
1

논리합으로 여러 관계식을 결합하면 그중 어떤 것이라도 하나만 만족하면 전체 논리식의 결과는 참이다. 예제를 보면 4 > 3 의 연산결과는 참이다 그럼 논리합 연산에서는 뒤는 실행도 안하고 참이라는 결과를 출력한다. 첫 번째로 확인한 결과가 참이므로 두 번째 조건의 결과와 상관없이 전체 조건식의 결과를 확정할수 있기 때문이다.

이렇게 하지 않아도 될 연산을 생략하여 논리식의 효율을 놓이는 것을 쇼트서킷(short circuit) 이라고 한다.

논리곱(AND)를 의미하는 연산자는 && 이다. 모든 조건이 다 참이여야 최종 결과도 참인 연산이다. 중간에 하나라도 거짓인 경우가 확인되면 그 뒤 연산은 생략한채 거짓으로 확정한다.

#include <stdio.h>

int main(void){
    printf("%d\n", 1 > 3 && 5 > 4 && 3 < 5);
    return 1;
}

실행결과
❯ ./welcome
0

위 예제도 마찬가지로 AND 연산중 하나라도 거짓이면 최종 거짓이 된다. 처음 1 > 3을 비교하는데 거짓임이 확정되어 그 뒤에 따라오는 연산은 생략해버린다.

부정

부정(NOT)을 의미하는 연산자는 ! 이다.

#include <stdio.h>

int main(void){
    int nInput = 0;
    scanf("%d", &nInput);

    printf("nInput 부정 %d\n", !nInput);
    return 1;
}

실행결과
❯ ./welcome
2
nInput 부정 0
❯ ./welcome
0
nInput 부정 1

쇼트서킷

논리곱 연산을 중첩하는 방법으로 세 가지 조건을 묶은 예이다.

#include <stdio.h>

int main(void){
     int nAge = 0, nHeight = 0;

     printf("나이 입력 : ");
     scanf("%d", &nAge);
     printf("키 입력 : ");
     scanf("%d", &nHeight);
     
     printf("결과 : %d (1:합격, 2:불합)\n", nAge >= 20 && nAge <= 30 && nHeight >= 150);

     return 1;
}

실행결과
❯ ./welcome
나이 입력 : 26
키 입력 : 178
결과 : 1 (1:합격, 2:불합)

만약 나이가 20 미만이면 첫 번째 결과가 거짓이다. 그렇다면 나머지 두 관계식은 실행조차 하지 않는다. 하나마나 결과는 거짓이기 때문이다.

이처럼 하지 않아도 될 연산을 생략해 논리 연산식의 효율을 높이는 것이 쇼트 서킷이라고 한다.

  1. 논리 연산식은 무조건 왼쪽에서 오른쪽으로 수행한다.
  2. OR 논리식은 조건에 만족하면 이후 연산을 생략한다.
  3. AND 논리식은 조건에 만족하지 않으면 이후 연산은 생략한다.
  4. 혼합 논리식에서 AND는 한 덩어리로 묶고 OR의 연속으로 살핀다.

조건 연산자(삼항 연산자)

조건식이 필요하고 그 조건의 결과로 두개의 항중 하나를 선택한다.

조건식 ? A : B

조건식이 참이면 A를 선택하고 그렇지 않으면 B 를 선택한다. 두 항이 동시에 선택될 수 없다.

사용자가 입력한 정수가 20 이하면 20으로 조정하고 20 초과면 30으로 조정하는 예제이다.

#include <stdio.h>

int main(void){
     int nInput = 0;

     printf("정수 입력 : ");
     scanf("%d", &nInput);

    nInput = nInput <= 20 ? 20 : 30;
    printf("nInput : %d\n", nInput);
    return 1;
}

실행결과
❯ ./welcome
정수 입력 : 26
nInput : 30

최댓값 구하기

임의 세 변수 A, B, C 가 있을때 각 변수에는 어떤 값이 들어있는지 전혀 알 수 없는 상태에서 어느 변수의 값이 가장 큰 값인지 알아내야 한다. 어떤 연산을 어떤 순서로 해야할지 생각해봐야 한다. 이전에는 "관계 연산자가 내부적으로는 뺄셈을 수행하여 결과가 0이면 같은 수로 판단" 했다. 이번에도 배운 연산자들을 활용해서 문제를 해결할수 있다.

토너먼트 방식

첫 번째 해결방법은 축구나 야구 같은 경기에서 볼수 있는 토너먼트(tournament) 방식을 이용하는것이다. 예를 들어 중국, 일본, 한국의 축구 경기를 생각해본다.

대진표A 를 보면 중국 과 일본이 대결하여 승자가 한국과 겨뤄서 최종 승자를 가려낸다. 중국과 일본의 경기에서 누가 이길지는 정해진것이 없다. 그러나 중국과 일본의 경기 승자가 중국이고 다시 한국과 경기하여 승자가 한국이라면 한국은 일본과 경기를 하지 않더라도 "한국이 일본보다 강한 팀이다." 라는 가정이 성립 한다.

같은 원리로 대진표B 를 보면 한국 과 일본 경기의 승자가 중국과 결승전을 치르게 된다. 만약 한국이 우승했다면 한국은 두번의 경기에서 승리했음을 의미한다. 이런 의미로 변수 A, B, C로 놓고 생각하면 다음과 같은 순서로 최댓값을 구할수 있다.

  1. A와 B를 비교하여 둘 줄 더 큰 수 하나를 선택한다.
  2. 1번 과정에서 선택한 값과 C를 비교하여 더 큰 수 하나를 선택한다.
  3. 2번 과정에서 선택한 값을 최댓값으로 확정한다.

세 정수 중에서 가장 큰 수 구하기(토너먼트 방식)

#include <stdio.h>

int main(void){
    int nMax = 0;
    int a, b, c;

    printf("세 정수를 입력하세요 : ");
    scanf("%d%d%d",&a,&b,&c);
    
    nMax = a; //a 변수가 가장 크다고 먼저 확정한다.
    nMax = b > nMax ? b : nMax; //b와 a를 비교하여 더 큰수를 nMax에 저장.
    nMax = c > nMax ? c : nMax; //c와 nMax를 비교하여 더 큰수를 nMax에 저장.
    printf("MAX : %d\n", nMax);
    return 1;    
}

실행결과
❯ ./welcome
세 정수를 입력하세요 : 10 20 30
MAX : 30

서바이벌 방식

운동경기를 진행하는 방식에는 서바이벌(survival) 방식도 있다. 가수를 뽑는 공개 오디션 같은 방식이다.

사용자로부터 정수 세개를 입력받아 서바이벌 방식으로 최댓값을 구하는 방법이다. 핵심은 사용자 입력 직후 최댓값을 다시 계산한다는 것이다.

#include <stdio.h>

int main(void){
    int nMax = 0, nInput = 0;
    
    scanf("%d", &nInput);
    nMax = nInput;

    scanf("%d", &nInput);
    nMax = nInput > nMax ? nInput : nMax;

    scanf("%d", &nInput);
    nMax = nInput > nMax ? nInput : nMax;
    
    printf("MAX : %d\n", nMax);
    return 1;    
}

실행결과
❯ ./welcome
10
20
30
MAX : 30

이 예제의 구성요소는 크게 두 가지 이다. 최초 입력한 숫자를 무조건 최댓값으로 간주하는 요소와 이후로 입력하는 나머지 숫자들은 현재까지 최댓값과 비교하여 최댓값을 갱신할지 말지 결정하는 요소이다.

최댓값 구하기 방식 비교

어떤 알고리즘이 더 좋은 것인지를 놓고 따져볼 수 있는 근거 중 하나는 비교 횟수이다. 크다, 작다 라는 것은 두 대상을 비교하는 것이 기본 이다. 100개 중 최댓값을 구하든 1000개든 결국 두 대상 항을 단순 비교하는 행위를 반복해 결론을 알수있다. 중요한 것은 같은 결과를 얻기까지 비교 횟수가 가장 적은 알고리즘이 더 성능이 우수하다는 것이다.

비교 횟수

토너먼트 방식 과 서바이벌 방식의 알고리즘을 비교 회수로 효율이 뭐가 더 좋다라곤 할수 없다. 비교 횟수를 가지고 보면 두 알고리즘은 효율이 비슷하다. 두 방법 모두 동일한 비교 연산으로 최댓값을 확정한다.

유지보수 및 확장성

코드 유지보수 및 확장성을 근거로 생각해보면, 서바이벌 방식이 좋다. 논리구조가 간단하고 비교할 항의 개수가 얼마나 더 늘거나 줄거나 복붙 코딩으로 문제가 해결되기 때문이다.

메모리 사용량

메모리 사용문제를 놓고 생각하여도 서바이벌 방식이 압도적으로 우세한다. 이유는 대상 항의 개수와 상관없이 입력을 받는 변수하나와 최댓값이 저장된 변수 하나면 끝나기 때문이다.