전문가칼럼

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

C++프로그래밍 : 부동소수점 구조와 원리

전문가칼럼
DBMS별 분류
Etc
작성자
dataonair
작성일
2014-11-17 00:00
조회
54539





C++프로그래밍

부동소수점 구조와 원리



C/C++, 자바(Java)와 같은 프로그래밍 언어에서는 수를 표현하기 위해 크게 두 가지 타입을 제공한다. 바로 정수 타입과, 부동소수점 타입이다.



수학 교과 과정에서 배운 것처럼 정수는 실수에 포함되므로, 일반적으로 실수를 표현하는 부동소수점 타입만 있어도 충분할 것 같다는 생각이 든다. 그런데 왜 정수 타입과 부동소수점 타입을 애써 나눠 놓은 것일까 이유는 무척 간단한데, 부동소수점이 실수를 완벽하게 표현할 수 없기 때문이다. 잘 알려져 있지는 않지만 사실 정수조차도 제대로 표현하지 못한다. 즉, 정수를 가지고 계산을 할 때, 부동소수점 타입을 사용할 경우 정확한 결과를 얻을 수 없게 된다. 또 한 가지! 보통 개발자들은 부동소수점이 1보다 작은 소수를 표현하기 위해 도입된 개념이라는 인식을 많이 가지고 있다. 아마도 이름에 ‘소수점’이란 말이 들어있기 때문일 것이다. 분명 그런 목적도 있긴 하지만 본래 목적의 일부분만을 나타낼 뿐이다. 부동소수점은 아주 작은 수와 아주 큰 수 양쪽을 표현하기 위해 도입됐다. 지금부터 부동소수점의 구조와 원리 그리고 본래 목적과 함께 그에 따른 한계를 살펴보겠다.



고정소수점과 부동소수점

부동소수점이란 실수를 표현할 때 소수점의 위치를 고정하지 않는 것을 말한다. 왜 소수점을 고정하지 않는 것일까 반대의 경우인 고정소수점을 살펴보면 알 수 있다. 가령 123.456의 경우 고정소수점에서는 정수 부분 123과 소수 부분 456을 나눠서 표현해야만 한다. 결국 한정된 비트에 정수와 소수 부분을 분할해 배치할 경우 고정소수점이 나타낼 수 있는 범위가 무척 한정된다. 그에 비해 부동소수점에서는 123.456을 123456이라는 유효숫자와 3이라는 소수점 위치를 통해서 고정소수점보다 훨씬 넓은 범위의 수를 표현할 수 있는 장점이 있다. 그래서 프로그래밍에서 실수를 표현할 때는 부동소수점을 주로 사용하게 된다. 사실 계산기만 봐도 왜 부동소수점을 사용하는지를 알 수 있다. 계산기가 표현할 수 있는 한계(칸)를 넘어설 경우 E(e)가 나오면서 지수 표기법이 나오는 것을 확인할 수 있다. 여기서 E(e)가 바로 부동소수점, 즉 소수점의 위치를 표시하겠다는 의미다.



2진법과 부동소수점

컴퓨터는 오직 0과 1로 계산을 수행한다. 따라서 정수도 0과 1로 표현하는 2진법으로 다뤄진다. 다들 알고 있듯이 정수의 경우 ‘2의 보수법’을 통해 표현한다. 그렇다면 1 이하의 소수는 어떻게 2진법으로 표현하는 것일까 사실 소수점 이하를 2진법으로 표현하는 것은 무척 간단하다. 부동소수점은 실수를 2진법으로 변환하는 것부터 시작한다.

10진법으로 표현되는 -9.6875를 2진법으로 나타내보자! 먼저 부호, 정수, 소수로 나눠서 생각하면 된다. 부호는 음수이고, 정수 9는 2진수로 1001이 되는 것을 쉽게 알 수 있다. 소수 부분이 문제인데, 초등학교에서 배운 방식의 반대로 생각하면 된다. 정수 부분을 2진수로 변환할 때는 2로 나눠서 나머지를 구했는데, 소수 부분의 경우 2를 곱해서 정수 부분을 취하면 된다. 0.6875를 2진수로 변환하는 절차는 다음과 같다.

column_img_1533.jpg

그림과 같이 소수 부분에 2를 곱해나간다. 만일 2를 곱한 결과가 1을 넘을 경우 1을 빼준다. 각 단계에서 정수 부분(1 or 0)을 2진수로 얻어낸다. 위의 경우 최종 결과가 0.0이 나와서 멈췄지만, 보통 대부분의 실수는 무한히 순환하며 반복된다. 즉, 적당한 개수의 유효숫자만을 취하게 된다. 이 과정에서 먼저 오차가 발생하게 된다. 결국 -9.6875를 2진법으로 표현하면 -1001.1011(2) 이 됨을 알 수 있다.



부동소수점 구조

-9.6875는 이제 -1001.1011(2)이 됐다. 그러나 아직 끝난 것이 아니다. -1001.1011(2)을 그대로 저장하면 바로 고정소수점이 된다. 부호부 음수, 정수부 1001, 소수부 1011을 각각 저장해야 되기 때문이다. 따라서 부동소수점 방식으로 저장하기 위해 정규화 과정을 거치는데, 바로 지수를 이용해 실수를 표현하는 것이다.

-1001.1011(2)은 정규화할 경우 -1.0011011×2³이 된다. 정규화란 정수부를 1로 맞춰 놓고, 적절하게 소수점 위치를 조정하는 것을 말한다. 여기서 지수부는 2³으로, 보통 승수인 3을 얘기한다. 이제 간단하게 실수를 부동소수점으로 표현할 수 있게 됐다. 정규화를 거친 후에 부호와 소수부의 유효숫자들 그리고 지수부만 있으면 되기 때문이다. 정규화를 거친 후 소수부의 유효숫자들을 특별히 ‘가수부’라고 부른다.

-9.6875 → -1001.1011(2) → -1.0011011×2³ → [부호부 음수, 지수부 3, 가수부 0011011]

부동소수점 형식은 정해진 비트(32Bit or 64Bit)를 적절히 분배해 부호부, 지수부, 가수부를 할당하는 일종의 규칙이자 약속이다. 일반적으로 가장 많이 사용되는 표준은 IEEE754이며, 이 글도 IEEE754 기준으로 설명할 것이다. 이 글의 목적이 IEEE754를 설명하는 것은 아니다. IEEE754를 비롯해 부동소수점을 표현하는 방식의 목적과 그에 따른 문제점을 살피는 것이 주목적이다. 단지 대표적으로 IEEE754를 기준으로 설명하는 것뿐이다. 일반적으로 C/C++, 자바(Java)에서는 부동소수점으로 플로트(float, 32Bit), 더블(double, 64Bit) 타입을 제공한다. 당연히 double이 크기가 커서 더욱 정밀도도 좋으며 표현 범위도 넓다. 따라서 가능하다면 double을 사용해야 한다. 그러나 여기서는 구조 및 원리를 설명하는 것이 목적이므로 편의상 float을 기준으로 설명할 것이다.

float : [ 부호부 1Bit | 지수부 8Bit | 가수부 23Bit ] = 총 32Bit
double : [ 부호부 1Bit | 지수부 11Bit | 가수부 52Bit ] = 총 64Bit

지금부터는 float을 기준으로 설명한다. 부호부는 정말 간단하다. 양수면 0, 음수면 1이다. 지수부를 설명하기 전에 가수부부터 설명하자! 이미 변환된 가수부를 왼쪽부터 그대로 채우면 된다. 물론 나머지 부분은 모두 0이다. 가장 이해하기 어려운 부분이 바로 지수부이다. 보통 float에서 사용되는 지수는 2-127~2-128이다. -127부터 128까지 사용해 아주 작은 수부터 아주 큰 수까지 다양하게 표현할 수 있다. 부동소수점의 목적은 1보다 작은 소수를 표현하는 것도 있지만, 아주 큰 수를 표현하는 것도 있다는 사실을 명심해야 한다.

즉, 지수부 8Bit로 -127~128까지 256단계를 나타내야 한다. 가장 간단하게 정수를 표시하듯이 2의 보수법을 이용해 표시할 것 같지만 그렇지 않다. 모두 0으로 채워진 [0000,0000]이 -127을 나타내고, 1 증가한 [0000,0001]은 -126을 나타내며, 이렇게 반복해 모두 1로 채워진 [1111,1111]은 128을 나타내게 된다. 이런 방식을 바이어스 표현법이라고 하는데, 왜 이런 구조를 택했는지 궁금할 것이다. 바로 순수한 0을 표시하기 위해서다. 일반적으로 모든 변수를 기본값으로 설정하는 방법은 변수가 차지하는 메모리 비트를 모두 0으로 설정하는 것이다. 만일 float의 모든 비트를 0으로 채운다고 생각해보자. 지수부를 일반적인 정수 표현 방식인 2의 보수법으로 표시할 경우 1.00000...00000×20 = 1이 된다. 즉, 부동소수점에서만 모든 비트를 0으로 채울 경우 기본값이 1이 되는 문제가 발생하는 것이다. 그러나 바이어스 표현법을 사용 표시하게 된다. 이 부분은 의문이 들 수도 있는데, 이후에 자세히 설명할 것이다.

이제 실제로 -9.6875를 float으로 나타내보자! [부호부 음수, 지수부 3, 가수부 0011011]로 정규화를 하고, 지수부는 3이므로 -127을 1번째 기준으로 삼을 경우 130번째가 될 것이다. 즉, 130을 2진수로 표현하면 [1000,0010]이 된다. 이제 이것을 그대로 비트 배열로 옮겨보자!

column_img_1534.jpg

이것을 실제로 확인해보자! C/C++은 2진수를 직접 표현하지 못하며, 16진수 표현방식으로 비트 배열을 표현할 수 있다. 4Bit씩 16진수 하나로 변경하면 [0xC11B,0000]으로 표현된다.



<리스트 1> float 확인
unsigned int ui = 0xC11B0000;
float f;
memcpy(&f, &ui, 4);
cout < < f < < endl;

소스 코드를 실행할 경우 정확하게 -9.6875가 출력되는 것을 확인할 수 있을 것이다. 여기까지는 실수를 부동소수점으로 표현하는 방법을 살펴보았다. 그러나 중요한 부분은 지금부터 시작된다. 위에서 제시했던 모든 비트가 0으로 채워진 경우에 대해 살펴보자! 지금까지 살펴본 float 구조를 적용하면 모든 비트가 0으로 채워진 경우는 부호부가 양수이며, 지수는 -127, 유효숫자는 1.0000 ...0000으로 표현될 수 있을 것이다. 즉, 2-127가 될 것이며, 이것은 (1 / 2127)이라는 아주 작은 수를 표현하게 된다. 분명 아주 작은 수이긴 하지만 0은 아닌 것이다. 만일 이 값을 0으로 지정해버리게 된다면 (1 / 2-127)보다 작은 양수를 표현할 수 없게 된다. 그리하여 IEEE754에서는 2-127보다 더 작은 수를 표현할 수 있도록 특별한 규정을 두었는데, 바로 지수부가 모두 0으로 채워진 경우에는 유효숫자의 정수부가 1이 되는 것이 아니라 0이 되도록 했으며, 그에 맞춰 지수를 -127이 아닌 -126으로 고정한 것이다. 즉, 지수부가 모두 0인 경우는 -127이 아닌 -126을 나타내게 된다(만일 지수부가 모두 0인 경우를 -127로 하고, 유효숫자의 정수부를 0으로 할 경우 2-127을 표현할 방법이 없어진다. 따라서 지수부가 실제로 표현할 수 있는 지수의 범위는 -126 ~ 128이 된다).

이런 규정을 적용하게 되면 float은 더욱 작은 수를 표현할 수 있게 된다. 가령 지수부는 모두 0으로 채워지고, 가수부의 마지막 비트만 1인 경우를 살펴보자!

column_img_1535.jpg

그림에서 확인할 수 있듯이 2-149, 즉 (1 / 2149)라는 훨씬 작은 수를 표현할 수 있게 된다. 실제로 이 값은 float이 표현할 수 있는 가장 작은 양의 실수가 되는 것이다. 만일 가수부가 모두 0으로 채워진 경우에는 순수하게 0을 나타낼 수도 있다. float의 구조는 그대로 double에도 적용될 수 있다. double은 지수부가 11비트이고, 가수부가 52비트이므로 이론적으로 2-1074, 즉 (1 / 2-1074)라는 상상도 할 수 없이 작은 값을 표현할 수 있는 것이다. 즉, 오차를 충분히 줄일 수 있도록 정밀도가 급격히 상승하게 된다. 그러나 여기서 착각하기 쉬운 점이 있는데, 모든 실수를 최소 양자 값(float: 2-149 혹은 double: 2-1074 정도의 오차를 가지고 표현할 수 있는 것은 아니라는 것이다.



부동소수점 표현 한계

부동소수점의 구조에서 알 수 있듯이, float이나 double의 경우 가수부의 크기는 일정하다. 따라서 지수가 충분히 클 경우에는 소수점 이하를 표현할 수 없게 된다. 무슨 의미인지 살펴보자!

column_img_1536.jpg

<그림 4>를 살펴보자! 만일 지수부의 지수가 23을 나타낸다고 가정해보자! 가수부가 어떤 비트로 채워져 있건 상관없이 소수점이 오른쪽으로 23자리 이동하게 되므로 더 이상 소수점 이하 부분이 남아있지 않게 된다. 이것이 무슨 의미인가 하면 지수부의 지수가 23 이상일 경우에는 더 이상 float은 소수점 이하를 표현할 수 없다는 의미이다. 즉, 정수만을 표현하게 된다. 실제로 그림에서 가수부가 모두 0일 경우를 계산할 경우 float이 나타내는 값은 10진수로 8,388,608이 된다. 즉, float으로 8,388,608이상을 나타낼 때는 더 이상 소수점 이하를 표현할 수 없다는 것이다.



<리스트 2> float의 소수 표현 한계
void main()
{
float f[10];
f[0] = 8388608.0;
f[1] = 8388608.1;
f[2] = 8388608.2;
f[3] = 8388608.3;
f[4] = 8388608.4;
f[5] = 8388608.5;
f[6] = 8388608.6;
f[7] = 8388608.7;
f[8] = 8388608.8;
f[9] = 8388608.9;
cout.precision(32); // ①
for(int i = 0; i < 10; i++)
{
cout < < f[i] < < endl;
}
}

실제로 확인해보자! float 배열 f를 마련하여 8388608.0 ~ 8388608.9까지 10개의 수를 대입하고 출력해보자! 주석①은 전체 숫자를 표현하도록 하기 위한 옵션이다. 옵션을 주지 않을 경우 지수 표기법(E) 형식으로 출력해 확인이 어렵다. 출력 결과는 다음과 같다.

8388608 // ① 8388608 8388608 8388608 8388608 8388608 8388609 // ② 8388609 8388609 8388609 float은 8388608 이상에서 소수 부분을 표현할 수 없기 때문에, 소수가 입력될 경우 가장 근사한 정수 값을 선택하게 된다. 그래서 주석①, ②에서 확인할 수 있듯이 8388608.0 ~ 8388608.5까지 여섯 개는 8388608로 근사 추정되고, 8388608.6 ~ 8388608.9까지 네 개는 8388609로 근사 추정되는 것이다. 이런 과정에서 또다시 오차가 발생하게 된다.

이런 방식을 계속해서 적용해본다면 재밌는 사실을 알게 된다. 바로 숫자가 커지면 커질 수록 표현할 수 있는 수의 오차도 급격히 증가한다는 사실이다. 지수가 23 이상일 때는 더 이상 소수점 이하를 표현할 수 없고, 오직 정수만을 표현할 수 있었다. 마찬가지로 지수가 24 이상일 때는 짝수 정수만을 표현하게 된다. 즉, 홀수 정수는 표현할 수 없게 된다. 지수가 25 이상일 때는 오직 4의 배수만을 표현할 수 있다. 혹시라도 잘 이해가 안 갈 경우를 위해 다시 한번 테스트를 해보자!

column_img_1537.jpg

지수가 25인 경우를 따져보자! 가수부가 23Bit이므로 지수가 25 이상일 경우 소수점을 오른쪽으로 이동할 경우 마지막에 무조건 0이 두 개 이상 붙게 된다. 2진수에서 마지막이 00으로 끝날 경우 4의 배수가 된다. 따라서 33,554,432 이상의 수를 float으로 표현할 경우 오직 4의 배수만을 나타낼 수 있다. 즉, 33,554,432 다음으로 나타낼 수 있는 수는 33,554,436이 된다. 아래는 확인용 소스 코드와 출력 결과이다.


<리스트 3> float - 4의 배수만 표현
void main()
{
float f[10];
f[0] = 33554432;
f[1] = 33554433;
f[2] = 33554434;
f[3] = 33554435;
f[4] = 33554436;
f[5] = 33554437;
f[6] = 33554438;
f[7] = 33554439;
f[8] = 33554440;
f[9] = 33554441;
cout.precision(32); // ①
for(int i = 0; i < 10; i++)
{
cout < < f[i] < < endl;
}
}
33554432 // ①
33554432
33554432
33554436 // ②
33554436
33554436
33554440 // ③
33554440
33554440
33554440

지수가 커지면 커질수록 오차 또한 급격하게 커진다. float의 경우 지수를 128까지 표현할 수 있기 때문에, 표현 가능한 이웃된 숫자 사이의 격차는 2의 제곱씩 커지게 된다.

즉, 2-149, 2-148, ... , 1, 2, 4, 8, ... , 2105

왜 부동소수점은 이런 형식을 사용하는 것일까 바로 인간이 그와 유사한 방식으로 수를 세기 때문이다. 나이를 따져보자! 갓 태어난 아기의 나이를 따질 때는 태어난 지 며칠 지났다고 표현한다. 서너 살까지는 몇 개월 지났다고 얘기한다. 그러나 어느 정도 나이가 차면 년 단위로 얘기를 하지, 며칠지났다고 얘기하지는 않는다. 사람이 아닐 경우는 더욱 극명하게 드러난다. 암석의 경우 최소 십만 년 ~ 백만 년 단위로 얘기하지 정확히 몇 년 지났다고 얘기하지 않는다. 우주의 나이로 가면 기본 단위가 억 년이다. 필자가 알기로는 150억 년에서 200억 년 사이로 거의 기본 단위는 10억 년이다.



부동소수점 사용시 주의점

간단하게 정리하자! 우주 나이를 계산할 때 정확하게 빅뱅 이후 몇 년 따지는 것이 무의미하다는 것을 알기 때문에, 부동소수점 타입을 이용해 큰 수를 대상으로 정밀 계산을 하는 것은 어리석다는 것이다. 만일 정수 단위로 큰 수를 다뤄야 한다면 64Bit 정수 타입인 long long(= __int64)을 사용해야 한다.


<리스트 4> float, long long 비교
void main()
{
float f = 33554432 + 3;
cout < < f < < endl; // ① 33554436 출력
long long ll = 33554432 + 3;
cout < < ll < < endl; // ② 33554435 출력
}

<리스트 4>에서 알 수 있듯이 정수로 정확한 계산을 수행하고자 할 경우 반드시 정수 타입을 사용해야만 한다.


<리스트 5> float 반올림
void main()
{
printf("%.3f\r\n", 0.3255); // ① 0.326
printf("%.3f\r\n", 0.4255); // ② 0.425
printf("%.3f\r\n", 0.42550001); // ③ 0.426
}

부동소수점은 반올림에서도 문제가 발생할 수 있다. [%.3f]는 소수점 아래 네 자리에서 반올림을 해 소수점 아래 세 자리까지 출력하는 포맷(format)이다. 주석②를 보면 알 수 있듯이 제대로 반올림이 되지 않을 수도 있다. 왜냐하면 0.4255가 실제로 부동소수점으로 표현될 경우 0.425499999……이기 때문이다. 따라서 반올림을 정확하게 수행하고자 한다면 주석③과 같이 반올림 경계를 넘을 만큼 작은 값을 더해줄 필요도 있다.

이외에도 부동소수점의 구조적 한계로 인해 계산상 많은 문제가 발생할 수 있다. 계산상 오차를 줄이기 위해서는 float보다는 정밀도가 훨씬 큰 double을 사용해야만 한다. 물론 double을 사용한다고 해서 문제가 완전히 사라지는 것은 아니겠으나 오차를 무시할 수 있는 수준으로 줄일 수 있을 것이다. 만일 더욱 정밀한 계산을 해야만 한다면 계산 전용 라이브러리를 사용해야만 한다. 더 많은 연산 능력과 시간을 요구하겠지만, 단순히 부동소수점 타입만을 사용할 때보다 훨씬 안전하게 계산을 수행할 수 있다.



무한 & NaN

부동소수점의 구조를 살펴보면서 충분히 느낄 수 있겠지만, 부동소수점은 아주 작은 수와 아주 큰 수를 표현하기 위해 설계했다는 것이다. 그런데 이것이 전부인 것은 아니다. IEEE754 부동소수점은 좀 더 특별한 것을 표현할 수 있도록 추가적인 규정을 마련했다. 바로 무한(Infinity)과 NaN(Not a Number)이다. 무한이야 개념적으로 알고 있을 것이고, NaN은 바로 DB 작업을 할 때 많이 보게 되는 [미정의 결과 | 값이 없는 상태] 정도를 나타낸다.

무한과 NaN을 어떻게 표현할까 지수부의 모든 비트가 1로 채워져 있는 경우가 그렇다. 위에서 float의 지수부는 8Bit이므로, 표현 가능한 지수의 범위는 -126~128이라고 하였으나, 모든 비트가 1로 채워진 상태인 128이 무한과 NaN에 대해 예약돼 있으므로 실제 지수의 범위는 -126~127이 되는 것이다.

무한과 NaN을 나누는 것은 가수부이다. 가수부의 모든 비트가 0으로 채워져 있는 경우가 바로 무한을 나타내며, 비트 하나라도 1이 있을 경우는 NaN을 나타내게 된다. 무한은 양의 무한과 음의 무한으로 나눠진다. 무한의 부호를 결정하는 것은 역시 부호비트이다. 그러나 NaN의 경우 수가 아니어서 양의 NaN, 음의 NaN이라고 특별히 구분하지는 않는다. NaN의 경우 가수부의 가장 왼쪽 비트 값에 따라서 QNaN(Quiet NaN)과 SNaN(Signalling NaN)으로 나누게 된다. SNaN은 계산 결과가 NaN이 나올 경우 예외를 발생시키는 목적으로 디자인됐다. 반대로 QNaN은 예외가 발생하지 않는다. CPU에 따라서 가수부 가장 왼쪽 비트의 설정 여부에 따라서 QNaN과 SNaN을 구분하는 방식이 달라질 수도 있다.

실제로 float을 이용해 양의 무한을 표현해보자. 부호 비트는 0으로 하고, 지수부를 나타내는 8비트는 모두 1로 채운다. 무한의 핵심은 가수부 23비트가 모두 0이라는 것이다. 양의 무한은 따라서 다음과 같이 표현될 수 있다.

[0111,1111,1000,0000,0000,0000,0000,0000] => 0x7F80,0000


<리스트 6> float의 양의 무한 표현
void main()
{
unsigned int ui = 0x7F800000;
float f;
memcpy(&f, &ui, 4); // ① f is infinity.
cout < < f < < endl;
float f2 = f + f; // ② f2 is infinity.
cout < < f2 < < endl;
}

위 소스의 출력 결과는 다음과 같다.

1.#INF // ① f
1.#INF // ② f2

INF는 Infinity를 나타낸다. 즉 무한을 나타낸다. 출력 형식은 컴파일러에 따라 달라질 것이며, GCC의 경우 inf로 출력된다. 음의 무한일 경우는 1앞에 마이너스 부호- 가 붙게 된다. 주의 깊게 보아야 할 점은 주석 ②와 같다. 무한끼리 더해도 역시 무한일 뿐이다. 따라서 f2의 경우도 무한으로 표현될 뿐이다.

이번에는 float으로 NaN을 표현해보자! NaN을 가장 간단히 표현할 수 있는 방법은 모든 비트를 모두 1로 채우는 것이다. 따라서 0xFFFF,FFFF은 NaN을 나타내기에 충분하다.



<리스트 7> float의 NaN 표현
void main()
{
unsigned int ui = 0xFFFFFFFF;
float f;
memcpy(&f, &ui, 4); // ① f is NaN
cout < < f < < endl;
float f2 = f + f; // ② f2 is NaN
cout < < f2 < < endl;
}

위 소스의 출력 결과는 다음과 같다.

-1.#QNAN // ① f
-1.#QNAN // ② f2

결과에서 알 수 있듯이 QNAN은 NaN을 의미한다(필자가 x86 CPU에서 VC++로 테스트했을 경우 SNaN(Signaling NaN)의 경우도 출력은 QNAN으로 나오는 것을 확인했다. 출력 버그인지 의도적으로 다른 이유에 의해서인지는 정확히 알 수 없었다). 앞에 마이너스가 붙은 것은 부호 비트가 1로 설정돼 있기 때문이다. NaN끼리 더해도 그 결과는 NaN이 됨을 확인할 수 있다. 역시 출력형식은 컴파일러에 따라 달라질 수 있는데, GCC의 경우 nan으로 나타난다.



정리

부동소수점은 아주 작은 수와 아주 큰 수, 그리고 무한과 NaN을 표현하기 위해 도입된 타입이다. 표현의 한계로 인해 오차는 값이 커질수록 기하급수적으로 증가한다. 이 사실을 기억하면서 정수 계산은 정수 타입에 맡기고, 정밀 계산을 위해서라면 계산 전용 라이브러리를 이용해야만 한다. 부동소수점을 사용해야만 한다면 정밀도가 높을수록 좋기 때문에 double을 써야 한다.