고려사항1. 변수의 크기

변수의 크기를 고려해야 하는 이유

  • 변수는 반드시 어떠한 자료형에 해당되어야 한다.
  • 어떤 자료형은 지정된 크기와 표현 범위를 가지고 있다.
  • 따라서, 변수는 자료형에 따라 담을 수 있는 값의 범위가 달라진다.

발생할 수 있는 문제

문제 영문 설명
오버플로 overflow - 계산 결과가 해당 데이터 타입이 표현할 수 있는 최대값보다 커서 올바른 값을 저장하지 못하는 현상.
- 정수형 오버플로는 wrap-around 를 발생시키며
- 실수형 오버플로가 발생하면 변수에 담기는 값이 무한대(Inf)로 처리된다.
언더플로 underflow - 계산 결과의 절대값이 해당 데이터 타입이 표현할 수 있는 0에 가장 가까운 양수값보다 더 작아져서 값을 제대로 표현하지 못하는 현상.
- 실수형 연산에서 사용되는 용어
- 언더플로가 발생하면 담기는 값이 0.0 으로 처리될 수 있다.
정밀도 손실 loss of precision - 자료형이 표현할 수 있는 유효숫자(가수부)의 정밀도보다 더 정밀한 값을 입력받거나 연산했을 때, 초과하는 값이 버려지거나 반올림되어 원래의 정밀도를 잃는 현상 (반올림 오차).

(0) 정상인 경우

  • 먼저, 문제가 일어나지 않는 정상의 경우를 살펴보자
1
2
3
4
5
6
short int a = 100 + 1;
short int b = 0 - 1;
short int c = a + b;
printf("%d, %d, %d\n", a, b, c);
------------------------------
>> 101, -1, 100

(1-1) 오버플로 overflow - 정수형

  • 계산 결과가 해당 데이터 타입이 표현할 수 있는 최대값보다 커서 올바른 값을 저장하지 못하는 현상.
  • 정수형 오버플로는 wrap-around 를 발생시키며
  • 실수형 오버플로가 발생하면 변수에 담기는 값이 무한대(Inf)로 처리된다.
1
2
3
4
5
short int a = 32767 + 1;
short int b = -32768 -1;
printf("%d, %d\n", a, b);
------------------------------
>> -32768, 32767
  • wrap around란 자료형이 표현할 수 있는 최대값보다 커지면 최소값이 되고
  • 표현할 수 있는 최소값보다 작아지면 최대값이 되는, 순환적 현상을 일컫는다.
  • 이 현상이 일어나는 원리는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// short int 형은 -32768 ~ 32767 까지를 저장할 수 있다.
// 최대값인 32767을 이진수로 표현하면 0111111111111111
SHRT_MAX =(이진수)=> 0111111111111111

// 그런데 여기에 1을 더하면 1000000000000000 이 되면서
// 10진수로 -32768 가 되어버린다.
  SHRT_MAX 0111111111111111
+ 숫자 1    0000000000000001
------------------------------
           1000000000000000 (10진수로 -32768)

// 비슷하게 최소값 -32768 은 1000000000000000 이며,
// 여기서 1을 빼면 되면서 0111111111111111 즉, 10진수로 32767이 된다.  
  SHRT_MIN 1000000000000000
- 숫자 1    0000000000000001
------------------------------
           0111111111111111 (10진수로 32767)

(1-2) 오버플로 overflow - 실수형

  • 실수형 오버플로가 발생하면 변수에 담기는 값이 무한대(Inf)로 처리된다.
1
2
3
4
5
6
7
8
9
float a = FLT_MAX;
printf("float의 최댓값: %f\n", a);
------------------------------
>> float 최댓값: 340282346638528859811704183484516925440.000000

float b = FLT_MAX * 2.0f;
printf("오버플로 : %f\n", b);
------------------------------
>> 오버플로 : inf
  • 그런데 위 예시에서는 “곱셈”을 한 것을 볼 수 있다.
  • 그 이유는, 작은 크기의 숫자를 더해선 실수형의 오버플로를 일으킬 수 없기 때문이다.
  • 이는 아래 “정밀도 손실” 부분에서 자세히 설명한다.

(2) 언더플로 underflow

  • 계산 결과의 절대값이 해당 데이터 타입이 표현할 수 있는 0에 가장 가까운 양수값보다 더 작아져서 값을 제대로 표현하지 못하는 현상.
  • 실수형 연산에서 사용되는 용어
  • 언더플로가 발생하면 담기는 값이 0.0 으로 처리될 수 있다.
  • 데이터 타입의 표현 범위를 벗어났지만, 0.0이 되지 않는 “비정규 값 영역”도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 양의 float형이 표현할 수 있는 가장 작은 수 FLT_MIN
float flt_min = FLT_MIN;
printf("\n%e\n", flt_min);
------------------------------
>> 1.175494e-38

// float이 표현할 수 있는 가장 작은 양수값보다 작은 양수값을 저장하면 0.0으로 처리됨
float flush_to_zero = FLT_MIN * 1.0e-8;
printf("%e\n", flush_to_zero);
------------------------------
>> 0.000000e+00

// 하지만 정밀도보다 더 작아도 0.0이 되지 않는 실수 범위도 있다.
// 이를 "비정규 값 영역"이라고 하며,
// 정밀도를 조금씩 희생하면서 0에 더 가까운 값을 표현하려고 시도하는 영역을 뜻한다.  
float just_before_flush_to_zero = FLT_MIN * 1.0e-7;
printf("%e\n", just_before_flush_to_zero);
------------------------------
>> 1.401298e-45

(3) 정밀도 손실 loss of precision

  • 자료형이 표현할 수 있는 유효숫자(가수부)의 정밀도보다 더 정밀한 값을 입력받거나 연산했을 때, 초과하는 값이 버려지거나 반올림되어 원래의 정밀도를 잃는 현상 (반올림 오차).
  • 정밀도 손실이 일어나는 이유는 아래와 같다.
1
2
3
4
5
6
/*
	float형은 숫자를 c * 2^q 형태로 저장한다.
	숫자를 전부 저장하지 않고, 앞부분의몇 자리(유효숫자)와 크기(지수)만 저장하는 것이다.
	바꿔 말하면, float 형은 10진수 기준 7자리정도만 정확하게 기억할 수 있다.
	그걸 넘어선 자리의 숫자는 반올림되거나, 아예 무시될 수도 있다.
*/
  • 실제 정밀도 손실이 일어나는 예시를 들어보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
	float a = 1.0f;
	float b = 0.0000000001f; // 10^-10
	float c = a + b;
	
	printf("a         = %.10f\n", a);
	printf("b         = %.10f\n", b);
	printf("a + b     = %.10f\n", c);
	
	// 두 값의 차이 확인
	printf("a + b - a = %.10f\n", c - a);
	
	return 0;
}

------------------------------
>> a         = 1.0000000000
>> b         = 0.0000000001
>> a + b     = 1.0000000000
>> a + b - a = 0.0000000000
  • b 는 분명히 0.0000000001 이지만, a+b 의 계산 결과는 1.0000000000, 즉 a 와 같다.
  • 즉, float 의 정밀도 한계 때문에 b 가 반영되지 못한 것이다.
  • float 은 유효숫자 기준 약 7자리 정도의 정밀도만 가지므로, 1.00000000011.000000의 차이는 float 입장에서는 “같은 값”처럼 보이는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
	float a = 10000000.0f;
	float b = 0.1f;
	float c = a + b;
	
	printf("a = %.10f\n", a);
	printf("b = %.10f\n", b);
	printf("a + b = %.10f\n", c);
	printf("a + b - a = %.10f\n", c - a);
	return 0;
}

------------------------------
>> a         = 10000000.0000000000
>> b         = 0.1000000015
>> a + b     = 10000000.0000000000
>> a + b - a = 0.0000000000
  • 위도 똑같은 예시다.
  • 원리 자체는 좀 더 복잡해서, 다음에 기회가 되면 살펴보도록 한다.

Reference

C 프로그래밍 (김형근, 곽덕훈, 정재화 공저)
C 프로그래밍 강의 (방송통신대 - 이병래)

Comments