C 프로그래밍 입문/부동소수형 데이터

위키책, 위키책

부동소수형 데이터[+/-]

부동소수형(floating point number type)을 일반적으로 수학에서 사용되는 용어로 바꾼다면 '실수'-소수점이 있는 숫자를 말한다. 가장 익숙한 실수의 예는 '3.141592' 일 것이다. 컴퓨터에서 소수점을 갖는 실수를 다루는 방법은 두 가지가 있다. '고정소수형'과 '부동소수형'인데, 고정 소수형은 소수점의 위치가 정해져 있어서 소수점 이상의 자릿수와 소수점 이하 자릿수가 제한 되어있는 방법이고, 부동소수형은 소수점 위치가 정해져있지 않고, 소수점이 없는 값과 소수점의 위치를 표기하는 방법이다.[1]사실 '부동소수형'이라는 단어는 사람을 헷갈리게 만드는 단어중 하나 인데, '부동(浮動)'이라는 거의 사용되지 않는 단어를 사용하기 때문 일 것이다.[2] '부동소수형'이라는 단어를 들으면 마치 소수점이 꼼짝 않고 제자리에 있다는 의미로 이해 될 텐데, 실제는 '소수점이 떠다니는'이라는 의미이다.

C에서 사용할 수 있는 부동 소수형은 다음과 같다:

타입 바이트수 유효자리수(정수부)[3] 최소허용값/최대허용값[4] 설명
float 4 24 (6) 1.175494351 E - 38
3.402823466 E + 38
단일 정밀도 부동소수 (single precision)
double 8 53 (15) 2.2250738585072014 E - 308
1.7976931348623158 E + 308
두배 정밀도 부동소수 (double precision)
long double 16[5] 113 (33) 3.36210314311209350626267781732175260 E - 4932
1.18973149535723176508575932662800702 E + 4932
사배 정밀도 부동소수 (quadruple precision)
float _Complex[6] 8 단일 정밀도 복소수
double _Complex 16 두배 정밀도 복소수
long double _Complex 32 사배 정밀도 복소수
float _Imaginary 단일 정밀도 허수
double _Imaginary 두배 정밀도 허수
long double _Imaginary 사배 정밀도 허수

부동소수형 데이터와 관련된 정의는 <float.h>파일에 있으며, 관련된 구체적인 값을 알고 싶다면 이 파일을 확인해 보면 된다. 실제로 부동소수형은 지수부와 가수부로 나뉘어 저장되고 처리 되기 때문에 자리수나 최소/최대값은 의미가 없다. 중요한 것은 소수점 이하 몇자리 까지 정확하게 연산이 되는가 하는 점 인데, 실상은 C 컴파일러/라이브러리에서 제공하는 연산능력은 그다지 정밀하지 않기 때문에 보통은 별도의 연산 라이브러리를 사용하는 경우가 다반사이다.

수학연산을 위한 함수들의 정의는 <math.h>에 정의되어 있으며, 자릿수가 많은 연산을 해야 하는 경우에는 별도의 라이브러리를 사용한다. 대표적인 큰 수 연산을 위한 라이브러리로는 GMP(GNU MP Bignum Library)가 있다.

복소수 표기는 수학에서 표기하는 것과 동일하게 '1.0 + 2.0i'라는 형식으로 표기한다. 추가적으로 일관성 있는 표기를 위해 _Complex_I와 I 라는 매크로가 제공된다. 이 매크로는 <complex.h>에 정의되어 있기 때문에 매크로를 사용 하려면 이 헤더 파일을 소스에 포함 시켜야 한다. <complex.h>가 제대로 포함되어 있다면 '1.0 + 2.0i'를 '1.0 + 2.0 * _Complex_I' 혹은 '1.0 + 2.0 * I' 라는 형태로 사용할 수 있다. 복소수나 허수를 위한 수학연산 함수는 별도로 존재한다. 복소수를 위한 연산 함수들은 <complex.h>에 정의되어 있다.

허수(imagenary)는 실제로 복소수에서 실수부가 없는 수를 말하기 때문에 _Imaginary 타입이 정의되어 있지 않은 컴파일러도 있다[7]. 다음은 복소수를 사용한 연산의 예이다:

#include <stdio.h>
#include <float.h>

int main(int argc, char *argv[])
{
	double _Complex a = 3.4 + 2.7i;
	double _Complex b = 0.0 + 2.0i;

	printf("The complex number: (%6.4f, %6.4f)\n", a);
	printf("The imaginary number: (%6.4f, %6.4f)\n", b * b);
	return 0;
}
참고:

C++ 에서는 복소수를 표현하기 위해 complex 라는 타입을 별도로 가지고 있습니다.

부동소수형 상수를 표기할 때 표기 방법은 두 가지 방법이 있다. 하나는 일반적으로 쓰는 부동소수점 표기 방법과 과학에서 표기할 때 쓰는 표기 방법이다. 다음의 두 수는 같은 값을 다른 방법으로 표기한 것이다.

0.0012
1.2e-3

기본적으로 부동소수형 상수는 double 타입으로 간주되며, float 타입의 상수가 필요한 경우에 f나 F접미사를 사용하여 해당 상수가 float 타임임을 분명히 할 수 있다. 마찬가지로 long double 타입의 상수가 필요한 경우에는 l혹은 L접미사를 지정함으로서 해당 상수가 long double 타입으로 다루어 지도록 할 수 있다. 다음은 각각의 타입에 해당되는 접미사를 사용하여 상수의 타입을 변환하는 것을 확인할 수 있는 프로그램이다.

#include <stdio.h>
int main(int argc, char * argv[])
{
    printf("The size of float constant: %d\n", sizeof(1.2f));
    printf("The size of double constant: %d\n", sizeof(1.2));
    printf("The size of long double constant: %d\n", sizeof(1.2l));
    printf("The size of float complex constant: %d\n", sizeof(1.2f + 1.2if));
    printf("The size of double complex constant: %d\n", sizeof(1.2 + 1.2i));
    printf("The size of long complex constant: %d\n", sizeof(1.2l + 1.2il));
    return 0;
}
참고: 부동소수의 오버플로우와 언더플로우

float형의 최대값이 3.402823466E+38 라고 위에서 언급 했는데 만약 그 값에 10을 곱하면 어떻게 될 것인가? 라는 의문을 가진 독자가 혹시 있을지 모르겠다. 이런 경우를 overflow라고 하며, 반대의 경우 underflow가 발생한다. 컴파일러에 따라 반응 방식이 다르긴 하겠지만 underflow인 경우에는 일반적으로 0으로 대체한다. overflow의 경우에는 최근의 컴파일러들은 무한대 값을 나타내는 inf(infinite)상수로 값이 지정되며, 이전의 컴파일러 들은 실행시간 에러를 발생시키며 프로그램을 정지시킨다.

무한대를 나타내는 inf상수는 inf와 -inf 두 가지가 있다.

참고: 반올림 오차(round-off error)

먼저 다음 프로그램을 보자, 입력해서 실행하기 전에 출력결과가 무엇이 나올지 예측해 본 후 실행해서 결과를 보자:

#include <stdio.h>
int main (int argc, char *argv[]) {
        volatile float a, b;
        a = 3402823466.0 + 1.0;
        b = a - 3402823466.0;
        printf ("%f\n", b);
        return 0;
}
  • 지정자 volatile은 궂이 입력하지 않아도 정상적으로 동작하겠지만, 혹시라도 출력결과가 1.0이 나온 독자는 volatile을 넣고 다시 실행시켜 보기를 바랍니다.[8]

float 아마 예측한 출력값은 1.0이었겠지만 실제 출력값은 전혀 예측하지 못한 값이 출력되었을 것 이다. 그 이유는 부동소수를 컴퓨터 내부에 표기하기 위해 부동소수의 값을 지수부와 가수부로 나누어 저장하도록 되어있는데, 일반적으로 float 타입은 가수부를 위해 10진수를 기준으로 6자리에서 7자리 정도를 저장할 공간만을 할애하고 나머지는 지수부와 부호를 표시하기 위해 사용된다. 그렇기 때문에 그 한계를 넘어가는 연산을 수행하려고 한 경우 연산결과가 가수부에서 표현 불가능한 값이 되어버리고 만다. 부동소수형의 연산결과가 전혀 예상치 못한 결과를 낳게되는 다른 원인도 있는데, 지수부에 사용될 메모리 공간이 부족한 경우 가수부의 공간 일부를 가져다 쓰는 경우도 있기 때문에 부동소수 연산을 어떻게 구현했느냐에 따라 같은 자리올림 에러에 의한 값이라 해도 다르게 나올 수 있다.

실제로 C라는 언어는 정밀한 연산을 위해 만들어진 언어가 아니라 기계 -- 컴퓨터 하드웨어를 가능하면 효율적으로 동작하는 프로그램을 만들기 위해 만들어진 언어이기 때문에 정확한 실수연산을 하고자 한다면 다른 언어 - 예를 들자면 과학기술 계산용으로 만들어진 포트란(Fortran)을 이용해야 정확한 연산 결과를 얻을 수 있다. C에서 포트란이 제공하는 정밀한 실수연산 기능을 사용하고 싶다면 포트란으로 만들어진 함수를 C에서 호출해서 사용하는 방법도 있다. 포트란 함수를 C에서 호출하는 방법을 알고자 하는 사람은 포트란 함수를 C에서 호출하는 방법을 참조하기 바란다.

주석 및 참고 자료[+/-]

  1. 엄밀하게 말하면 정확한 설명은 아닙니다만 이해를 돕기 위해 대략적인 이야기만 썼습니다. 또한 부동소수형 표기법에 관한 자세한 내용을 알기 원한다면 표준 문서 IEC 60559:1989, Binary floating-point arithmetic for microprocessor systems (previously designated IEC 559:1989)를 찾아 보거나, IEEE 754와 관련된 내용을 인터넷에서 찾아 보시기 바랍니다.
  2. 아마도 이는 일본에서 건너온 잔재가 남아있는 것 이거나 오래전에 수학 교재들이 번역되어야 했던 시절에 선택된 단어가 그대로 사용되기 때문이 아닐까 합니다.
  3. 정수부와 소수부 모두를 합한 자리수를 의미하며, 유효자리수는 시스템마다, 컴파일러마다 다릅니다.
  4. 최소허용값이나 최대 허용값은 실제로 큰 의미는 없습니다.
  5. long double 타입은 시스템마다 정의된 자리수가 약간씩 다릅니다. 경우에 따라 double과 동일하게 8 바이트로 처리하는 경우도 있고, 10 바이트로 처리 하는 경우도 있고, 3배 정밀도 부동소수라 하여 12 바이트로 처리하는 경우도 있습니다.
  6. _Complex와 _Imaginary 타입은 C99에서 도입 되었습니다.
  7. joshuajh: 제 경우에는 아직 _Imagenary를 구현한 컴파일러를 확인한 적은 없습니다. 수학연산에 관한 관심은 대부분 정수쪽에 집중되어 있는 관계로 지금 당장 테스트 해 볼 수 없는, 이전에 사용했던 컴파일러들이 _Imaginary 타입을 구현 했는지 아닌지에 대해 아는바가 없습니다.
  8. volatile은 필요없는 코드 이지만 혹시라도 컴파일러가 최적화를 수행하면서 의미 의미 없다고 보여지는 코드를 제거하는 경우를 배제하기 위해, 노파심에서 코드를 끼워 넣었습니다. 개인적으로 알고있는 컴파일러 중 volatile을 넣어야 하는 컴파일러는 없지만 그래도 혹시나 하는 마음에 끼워 넣었습니다.