개요
처음 C언어로 코드를 작성했을 때에는 자료형에 대해 주의 깊게 생각하지 않았다. 자료형의 크기를 고려해 overflow를 방지해야한다는 사실만 겨우 알고, 환경에 따라 자료형의 크기가 다를 수 있다는 것은 몰랐다. 이 기회에 자료형에 관해 정리해두자 싶어서 공부하던 중 책에서 읽게된 충격적인 내용. 실수형 변수의 값은 정확한 값이 아니라고??
목차
1.
c언어는 환경에 따라 자료형의 크기가 다르다.
2.
실수형 변수의 값은 근사값이다.
들어가기 전에
자료형이란?
모든 변수들은 반드시 자료형(type)을 가져야 한다. 즉, 어떤 특정 데이터를 저장할 것인지를 정해주어야 한다.
변수는 데이터가 저장된 위치를 말하는데 우리가 어떤 데이터를 입력하던 컴퓨터는 이것을 2진수로 바꾸어 저장한다. 컴퓨터가 변수에 저장된 데이터를 읽어오려면 얼마만큼의 메모리에 어떤 종류의 데이터를 저장한 것인지를 알아야 읽어올 방법을 정할 수 있다. 게다가 그 변수가 어떤 연산을 수행할 수 있는지 확실하게 정해야하기 때문에 정확한 형을 정하는 것이 중요하다.
c언어는 환경에 따라 자료형의 크기가 다르다?
C언어는 매우 오래 전에 개발된 언어이기 때문에 시간에 따라 달라진 점이 많다. 16비트, 32비트, 64비트 CPU가 나오고 운영체제가 발전하면서 정수 자료형의 크기도 그때그때 달라졌다.
C 표준에서는 자료형의 크기를 정확하게 정의하지 않는다. 자료형 별로 최소 크기와 다른 자료형과의 관계만 정의하고 있다. 예를 들면 이런 식이다.
•
long long의 최소 크기는 64비트이며 long보다 작지 않아야 한다.
•
long의 최소 크기는 32비트이며 int 보다 작지 않아야 한다.
•
short와 int의 최소크기는 16비트이며 int는 short보다 작지 않아야 한다.
•
char는 항상 자료형 중에 가장 작아야 하며 최소 크기는 8비트이다.
이러한 혼란을 줄이기 위해 C99 표준 부터는 stdint.h 헤더 파일이 추가되었다. 이 헤더 파일을 사용하면 크기가 표시된 정수 자료형으로 변수를 선언할 수 있고 최소값과 최대값도 확인이 가능하다.
예시
•
int8_t, int16_t, int32_t는 %d로, int64_t는 %lld로 printf 출력이 가능하다.
•
uint8_t, uint16_t, uint32_t는 %u로, uint64_t는 %llu로 printf 출력이 가능하다.
#include <stdint.h>
#include <stdio.h>
int main()
{
int8_t n1; // 8비트(1바이트) int
int16_t n2; // 16비트(2바이트) int
int32_t n3; // 32비트(4바이트) int
int64_t n4; // 64비트(8바이트) int
uint8_t n5; // 8비트(1바이트) unsigned int
uint16_t n6; // 16비트(2바이트) unsigned int
uint32_t n7; // 32비트(4바이트) unsigned int
uint64_t n8; // 64비트(8바이트) unsigned int
n1 = INT8_MAX;
n2 = INT16_MAX;
n3 = INT32_MAX;
n4 = INT64_MAX;
n5 = UINT8_MAX;
n6 = UINT16_MAX;
n7 = UINT32_MAX;
n8 = UINT64_MAX;
printf("%d %d %d %lld\n", n1, n2, n3, n4);
printf("%u %u %u %llu\n", n5, n6, n7, n8);
return 0;
}
C
복사
출력
127 32767 2147483647 9223372036854775807
255 65535 4294967295 18446744073709551615
C
복사
과제할 때 참고하기 위해 42서울 아이맥 기준 자료형의 크기와 범위를 표로 정리해보았다. 자료형별 최소값과 최대값은 limits.h 헤더파일에서 확인이 가능하다. 참고로 window 환경에서는 long이 4바이트이다.
자료형 | 크기(byte) | 범위 |
char | 1 | -128 ~ 127 |
unsigned char | 1 | 0 ~ 255 |
short | 2 | -32768 ~ 32767 |
unsigned short | 2 | 0 ~ 65535 |
int | 4 | -2147483648 ~ 2147483647 |
unsigned int | 4 | 0 ~ 4294967295 |
long | 8 | -9223372036854775808 ~ 9223372036854775807 |
long long | 8 | -9223372036854775808 ~ 9223372036854775807 |
float | 4 | 1.175494e-38 ~ 3.402823e+38 |
double | 8 | 2.22507e-308 ~ 1.79769e+308 |
long double | 16 | 3.362103e-4932L ~ 1.189731e+4932L |
실수형 변수의 값은 근사값이다?
실수형 변수는 정수형보다 큰 숫자나 소수점이 있는 실수를 저장할 때 사용한다. 실수형 변수는 숫자를 부동소수점 방식으로 저장하는데 부동소수점 방식은 숫자를 유효 숫자를 나타내는 ‘가수’와 ‘지수’로 나누어 표기하는 방법을 말한다. 예를 들어 10진수 12345 를 부동 소수점으로 표기하면 1.2345e4 (1.2345 * 10의 4승) 이 된다. 여기서 1.2345가 가수이며 4가 지수이다.
실수형 변수에 숫자가 저장될 때에는 2진수 부동소수점으로 저장된다. float 자료형으로 예를 들면 맨 앞 1비트는 부호를 저장하고, 그 후 8비트는 지수를 저장, 나머지 비트는 가수를 저장한다.
메모리에 실제로 어떻게 저장 되는지 궁금하다면 이 글을 참고하자.
그런데 부동 소수점으로 숫자를 저장하면 정확한 값을 저장하는 게 아니라 근사값을 저장하기 때문에 오차가 생길 수 있다. 단순히 값을 저장 하기만 하면 오차가 매우 적어 괜찮을 수 있지만 계산을 하게 되면 유효 숫자를 기준으로 반올림 하면서 오차가 쌓여 정확하지 않은 값이 나올 수 있다.
예를 들어 float 변수 f에 0.1을 할당하고 출력하면 0.1이 출력되지만 f에 0.1을 10번 더한 값을 할당 후 출력해 보면 1이 아니라 다른 값이 들어있는 것을 확인할 수 있다.
코드
•
f 에 0.1을 저장 후 f를 출력
#include <stdio.h>
int main(void)
{
float f;
f = 0.1f;
printf("%f\n", f);
}
C
복사
출력값
0.10000
C
복사
코드
•
f에 0.1을 10번 더한 값을 저장 후 예상한 값(1.0)과 맞는지 확인 후 f를 출력
int main(void)
{
float f;
f = 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f;
if (f == 1.0f)
printf("OK\n");
else
{
printf("KO\n");
}
printf("%.7f\n", f);
}
C
복사
출력값
•
출력값이 KO가 나와 f가 1.0이 아니라는 것을 알 수 있음
•
이를 확인하기위해 소수점 아래 7번째 자리까지 출력해 봄
KO
1.0000001
C
복사
이러한 문제 때문에 실수형 변수를 사용할 때에는 오차 범위를 고려해야 한다. 여기서는 float.h 헤더를 사용하여 예상한 결과 값과 실제 결과 값의 차이가 float의 오차범위내에 있는지 확인하는 방법을 쓸 수 있다.
코드
•
실제 f값과 예상한 결과 값인 1.0의 차이를 구하고 fabsf 함수를 이용해 절대값을 구함
•
구한 값을 FLT_EPSILON (float 오차 범위) 와 비교
#include <stdio.h>
#include <float.h> // FLT_EPSILON을 사용하기 위함
#include <math.h> // fabsf를 사용하기 위함
int main(void)
{
float f;
f = 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f;
if (fabsf(f - 1.0f) <= FLT_EPSILON)
printf("OK\n");
else
{
printf("KO\n");
}
printf("%.7f\n", f);
}
C
복사
출력값
•
오차 범위 내에 있으므로 계산 결과가 맞다는 것을 확인할 수 있음
OK
1.0000001
C
복사
하지만 이 경우에도 연산 결과가 오차범위 내에 있는 것을 확인한 것 뿐이지 실제로 변수 f가 가지고 있는 값은 1.0과는 차이가 있다. 그리고 연산을 10번이 아니라 100번, 1000번을 하게 되면 오차가 매우 커져 오차범위를 벗어나 버리는 현상이 발생한다. 그렇기 때문에 실수형 변수는 정확한 값을 가지고 있는 것이 아니라 근사값을 가지고 있다는 사실을 항상 염두에 두고 코드를 작성해야 한다. 이 때 참고하기 좋은 글이 있어 남긴다.
참고 자료
책
C Programming: A Modern Approach (C 프로그래밍: 현대적 접근)
C언어 코딩 도장
C언어 이야기
사이트
https://en.wikipedia.org/wiki/C_data_types 위키백과 - C data types
https://dojang.io/mod/page/view.php?id=35 코딩 도장 - 크기가 표시된 정수 자료형 사용하기
(틀린 부분이 있다면 슬랙 @subson 으로 DM부탁드립니다.)