8-1-라.정수화 함수

정수화 함수는 실수형 데이터에서 정수부만을 취하는, 즉 소수점 이하의 소수부를 잘라 버리는 함수이다. 소수부를 잘라 버린다고 해서 계산 결과가 정수가 되는 것은 아니며 리턴값은 여전히 실수이다. 실수값의 소수부만을 0으로 만든다고 생각하면 된다. 정수화 함수에는 다음 두 가지가 있다.

 

double floor( double x );

double ceil( double x );

 

두 함수는 소수점 이하를 자르는 방식이 다른데 floor는 소수점 이하를 버리고 정수부만을 취하고 ceil은 소수점 이하를 올림해서 정수부를 1증가시킨다. 다음 호출 예를 보면 쉽게 이해가 될 것이다.

 

floor(3.14);             // 결과는 3.0

ceil(3.14)           // 결과는 4.0

 

floor는 내림을 하는 함수이고 ceil은 올림을 하는 함수라고 일단 정리할 수 있다. 그러나 단순히 내림, 올림으로 이 두 함수의 동작을 정의하는 것은 정확하지 않다. 다음 예를 보자.

 

floor(-3.14);       // 결과는 -4.0

ceil(-3.14)              // 결과는 -3.0

 

결과가 조금 이상해 보이는데 인수가 음수일 때 floor는 정수부가 1 감소하며 ceil은 소수부를 버린다. 왜 그런가 하면 음수에서도 버림에 의해 수가 더 작아져야 하고 올림에 의해 수가 더 커져야 하기 때문이다. 그래서 floor, ceil 함수의 동작을 좀 더 일반적으로 표현하면 다음과 같다.

 

floor : 주어진 인수보다 크지 않은 최대 정수

ceil : 주어진 인수보다 작지 않은 최소 정수

 

수직선상에서 이 함수들의 동작을 설명해 보면 floor는 인수의 바로 왼쪽 정수값을 구하고 ceil은 바로 오른쪽 정수값을 구한다.

수 체계를 시각화해서 보면 좀 더 쉽게 이해가 갈 것이다. floor는 "마루, 바닥"이라는 뜻이고 ceil은 "천장"이라는 뜻인데 단어뜻과 연관지어 보면 자연스럽게 이해가 될 것이다. 예제를 통해 이 함수들의 동작을 테스트해 보자.

 

: floor

#include <Turboc.h>

#include <math.h>

 

void main(void)

{

     printf("floor(3.14)=%f\n",floor(3.14));

     printf("ceil(3.14)=%f\n",ceil(3.14));

     printf("floor(-3.14)=%f\n",floor(-3.14));

     printf("ceil(-3.14)=%f\n",ceil(-3.14));

}

 

실행 결과는 앞에서 테스트해 본 바와 같다.

 

floor(3.14)=3.000000

ceil(3.14)=4.000000

floor(-3.14)=-4.000000

ceil(-3.14)=-3.000000

 

실수를 단순히 정수로 바꾸기만 한다면 정수형 변수에 단순히 대입하기만 해도 된다. 정수와 실수는 호환되는 타입이기 때문에 정수형 변수에 실수를 대입할 경우 하강 변환이 발생하고 이 과정에서 소수부는 자연스럽게 버려진다.

 

double d=3.14;

int i=d;

 

이렇게 대입하면 i에는 소수부가 버려진 3이 대입될 것이다. 컴파일러에 따라 실수값을 정수에 대입할 경우 값의 일부를 잃을 수 있다는 경고가 발생할 수 있는데 i가 정수형 변수이므로 0.14가 버려진다는 뜻이다. 이런 경고를 받기 싫으면  int i=(int)d; 처럼 캐스트 연산자를 사용하여 명확한 형 변환을 하면 된다.

음수의 실수값을 정수형 변수에 대입할 경우, 그러니까 위 코드에서 d가 -3.14일 경우는 i에 -3이 대입된다. 단순한 대입문이나 캐스트 연산자는 별다른 처리없이 소수부를 기계적으로 잘라 버린다는 것을 알 수 있다. 이에 비해 floor, ceil 함수는 부호까지 고려하여 값을 조금 더 논리적으로 다룬다는 차이점이 있다.

실수의 소수점 이하는 굉장히 길 수 있기 때문에 가끔은 지저분해 보이기도 하므로 정확한 값보다는 대충 비슷한 값으로 정리를 좀 하는 것이 좋다. 우리반 월말 고사 평균은 87.24198666점이다 라고 하는 것 보다는 87점이다 또는 87.24점이다라고 하는 것이 더 보기에 좋지 않은가? 이럴 때 정수화 함수가 필요하다.

그런데 실생활에서는 소수점을 잘라 버리거나 또는 인접한 정수값을 취하는 것보다 반올림하는 것이 오차도 적고 가장 일반적으로 애용되는 방법이다. 그러나 C 언어는 반올림 함수를 따로 제공하지 않는다. 왜 반올림 함수가 없는가 하면 굳이 이런 함수를 제공하지 않아도 올림, 내림 함수를 응용하면 반올림을 할 수 있을 뿐만 아니라 응용 방식에 따라 반올림 방식을 자유롭게 선택할 수도 있기 때문이다. 어떻게 응용하는지 보자.

 

실수 x 반올림한 = floor(x+0.5)

 

척 보면 이해가 갈 것이다. 0.5를 더한 후 바로 왼쪽의 정수를 찾으면 이 값이 바로 반올림값이 된다. 어째서 이게 반올림이 되는가 선뜻 이해가 가지 않는다면 x에 1.4와 1.5를 대입해 보도록 하자.

 

floor(1.4+0.5) = 1.0

floor(1.5+0.5) = 2.0

 

1.4에 0.5를 더하면 1.9가 되는데 이 값을 내림하면 1.0이 된다. 1.5에 0.5를 더하면 2.0이 되므로 내림이 발생하지 않으며 2.0의 값을 그대로 유지할 것이다. 그러므로 floor(x+0.5)라는 공식은 0.5이상의 값은 다음 오른쪽 정수로 올림하고 0.5미만의 값은 직전의 왼쪽 정수로 내림함으로써 반올림 계산을 멋지게 하고 있는 것이다.

그렇다면 floor(x+0.5) 대신 ceil(x-0.5) 공식을 사용하는 것은 어떤 효과가 있을까? 두 공식은 비슷하지만 0.5의 경계에 걸렸을 때의 처리가 조금 다르다. floor(x+0.5)는 0.5 이상의 값을 반올림하는데 비해 ceil(x-0.5)는 0.5초과 값을 반올림한다. x가 1.5일 때 floor는 2.0으로 반올림하지만 ceil은 1.0으로 내림해 버릴 것이다. 물론 x가 1.500001이라면 ceil도 2.0으로 반올림을 한다. 두 공식의 차이를 그림으로 그려 보면 반올림되는 범위가 조금 다르다.

일반적으로 반올림이라 하면 0.5 이상을 올림하는 것이므로 floor 함수를 사용하는 것이 논리적으로 합당하다. 이 반올림 공식이 음수의 경우에도 제대로 동작하는지 확인해 보기 위해 x에 -1.4, -1.5, -1.6을 대입해 보자.

 

floor(-1.4+0.5) = -1.0

floor(-1.5+0.5) = -1.0

floor(-1.6+0.5) = -2.0

 

-1.4나 -1.6의 경우는 쉽게 이해가 되지만 -1.5를 반올림했을 때 -2.0이 아닌 -1.0이 된다는 점이 순간적으로 조금 헷갈릴 것이다. 그러나 수직선을 그려 놓고 생각해 보면 제대로 반올림되었다는 것을 확인할 수 있다. int(x+0.5)도 floor(x+0.5)와는 다르다. 캐스트 연산자는 무조건 소수점 이하를 기계적으로 잘라 버리기 때문에 x가 음수일 경우 터무니없는 결과가 나온다.

C는 자주 사용하는 기능에 대해 표준 함수들을 제공하지만 응용이 가능한 기능에 대해서는 별도의 함수를 제공하지 않는다. floor라는 간단한 함수로 반올림을 하는 것처럼 표준 함수를 조금만 응용하면 얼마든지 좀 더 복잡한 연산을 만들어 쓸 수 있기 때문이다. 표준 함수들을 많이 아는 것도 중요하지만 이미 알고 있는 함수들을 응용하는 능력도 중요하다. 이번에는 floor 함수를 한단계 더 응용하여 소수점 둘 째자리에서 반올림되도록 해 보자. 이 공식은 다음과 같다.

 

floor(x*10+0.5)/10

 

x를 10배한 후 반올림하고 다시 10으로 나누면 소수점 둘 째자리에서 반올림된다. x가 3.14라고 했을 때 31.4로 만든 후 0.5를 더해 31.9로 만들고 이 값에 대해 floor 함수를 호출하면 31이 된다. 결과값을 다시 10으로 나누면 3.1이 될 것이다. 원래 x값에 10을 곱해 소수점을 잠시 왼쪽으로 한칸 옮긴 후 반올림 처리하고 다시 오른쪽으로 한칸 옮기는 것이다.

같은 원리로 소수점 셋 째자리에서 반올림하려면 floor(x*100+0.5)/100 공식을 사용하면 되고 소수점 넷 째자리에서 반올림하려면 floor(x*1000+0.5)/1000 공식을 쓴다. 곱하고 나누는 수만 조정하면 반올림되는 자리수를 원하는대로 지정할 수 있고 더하는 0.5를 조정하면 반올림 경계도 입맞대로 설정할 수 있다. 이 공식을 좀 더 일반화하면 소수점 n번째 자리에서 반올림되는 함수를 만들 수 있는데 아주 간단하므로 매크로 함수로 정의해 보았다.

 

: round

#include <Turboc.h>

#include <math.h>

 

#define banollim(x,dig) (floor((x)*pow(10,dig)+0.5)/pow(10,dig))

 

void main(void)

{

     double x=123.456789;

     int i;

 

     for (i=-2;i<5;i++) {

          printf("%f의 %d자리 반올림 = %f \n",x,i,banollim(x,i));

     }

}

 

banollim 함수는 실수 x와 반올림할 자리수 dig를 인수로 전달받는데 dig는 0을 기준(Zero Base)으로 하므로 우리가 생각하는 소수점 자리수보다는 하나 더 작다. 즉 dig가 0일 때 소수점 첫 번째 자리를 지정하며 n일 때 n+1자리에서 반올림하여 결국 반올림 후 dig자리까지가 유효한 수로 남게 된다. pow 함수는 음수승도 잘 계산하므로 dig에 음수를 주면 정수부에서 반올림을 할 수도 있다. 실행 결과는 다음과 같다.

 

123.456789의 -2자리 반올림 = 100.000000

123.456789의 -1자리 반올림 = 120.000000

123.456789의 0자리 반올림 = 123.000000

123.456789의 1자리 반올림 = 123.500000

123.456789의 2자리 반올림 = 123.460000

123.456789의 3자리 반올림 = 123.457000

123.456789의 4자리 반올림 = 123.456800

 

지정한 자리수에서 정확하게 반올림되었다. 이렇게 한 번 매크로로 만들어 놓으면 임의의 실수를 임의의 자리에서 반올림할 때 편리하게 활용할 수 있을 것이다. 이 예제에 대한 분석은 여러분들이 직접 해 보기 바라며 매크로가 마음에 들면 종종 애용하기 바란다.