7.4 최솟값과 최댓값 표현하기

지금까지 오버플로우, 언더플로우와 자료형의 크기에 대해서 알아보았습니다. 이번에는 소스 코드에서 정수의 최솟값과 최댓값을 표현하는 방법을 알아보겠습니다.

유닛 맨 앞의 표 7‑1에서 부호 있는 int의 최솟값은 -2,147,483,648이라고 했지만 Visual Studio에서 이 값을 직접 넣어보면 컴파일 에러가 발생합니다.따라서 소스 코드에서 정수의 최솟값을 표현하려면 limits.h 헤더 파일을 사용해야 합니다. 다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요(실행 결과는 Visual Studio, Windows 기준).

integer_min.c

#include <stdio.h>
#include <limits.h>    // 자료형의 최댓값과 최솟값이 정의된 헤더 파일

int main()
{
    char num1 = CHAR_MIN;          // char의 최솟값
    short num2 = SHRT_MIN;         // short의 최솟값
    int num3 = INT_MIN;            // int의 최솟값
    long num4 = LONG_MIN;          // long의 최솟값
    long long num5 = LLONG_MIN;    // long long의 최솟값

    // char, short, int는 %d로 출력하고 long은 %ld로 출력, long long은 %lld로 출력
    printf("%d %d %d %ld %lld\n", num1, num2, num3, num4, num5);
    // -128 -32768 -2147483648 -2147483648 -9223372036854775808

    return 0;
}

실행 결과

-128 -32768 -2147483648 -2147483648 -9223372036854775808

CHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN은 부호 있는 정수의 최솟값입니다. limits.h 헤더 파일에는 다음과 같이 부호 있는 정수와 부호 없는 정수의 최솟값과 최댓값이 정의되어 있습니다.

표 7‑3 정수 자료형의 최솟값과 최댓값
자료형 최솟값 최댓값
char CHAR_MIN CHAR_MAX
short SHRT_MIN SHRT_MAX
int INT_MIN INT_MAX
long LONG_MIN LONG_MAX
long long LLONG_MIN LLONG_MAX
unsigned char 0 UCHAR_MAX
unsigned short 0 USHRT_MAX
unsigned int 0 UINT_MAX
unsigned long 0 ULONG_MAX
unsigned long long 0 ULLONG_MAX

다음과 같이 limits.h에 정의된 최댓값을 넘어서도 오버플로우가 발생합니다.

integer_max_overflow.c

#include <stdio.h>
#include <limits.h>    // 자료형의 최댓값과 최솟값이 정의된 헤더 파일

int main()
{
    char num1 = CHAR_MAX + 1;          // char의 최댓값보다 큰 수를 할당. 오버플로우 발생
    short num2 = SHRT_MAX + 1;         // short의 최댓값보다 큰 수를 할당. 오버플로우 발생
    int num3 = INT_MAX + 1;            // int의 최댓값보다 큰 수를 할당. 오버플로우 발생
    long long num4 = LLONG_MAX + 1;    // long long의 최댓값보다 큰 수를 할당. 오버플로우 발생

    // char, short, int는 %d로 출력하고 long long은 %lld로 출력
    // 부호 있는 정수에서 저장할 수 있는 범위를 넘어서면 최솟값부터 다시 시작
    printf("%d %d %d %lld\n", num1, num2, num3, num4);
    // -128 -32768 -2147483648 -9223372036854775808

    unsigned char num5 = UCHAR_MAX + 1;          // unsigned char의 최댓값보다 큰 수를 할당
                                                 // 오버플로우 발생
  
    unsigned short num6 = USHRT_MAX + 1;         // unsigned short의 최댓값보다 큰 수를 할당
                                                 // 오버플로우 발생

    unsigned int num7 = UINT_MAX + 1;            // unsigned int의 최댓값보다 큰 수를 할당
                                                 // 오버플로우 발생

    unsigned long long num8 = ULLONG_MAX + 1;    // unsigned long long의 최댓값보다 큰 수를 할당
                                                 // 오버플로우 발생

    // unsigned char, unsigned short, unsigned int는 %u로 출력하고 
    // unsigned long long은 %llu로 출력
    // 부호 없는 정수에서 저장할 수 있는 범위를 넘어서면 최솟값 0부터 다시 시작
    printf("%u %u %u %llu\n", num5, num6, num7, num8); // 0 0 0 0

    return 0;
}

실행 결과

-128 -32768 -2147483648 -9223372036854775808 0 0 0 0

부호 있는 정수는 저장할 수 있는 범위를 넘어서면 최솟값(음수)부터 다시 시작하고, 부호 없는 정수는 범위를 넘어서면 최솟값인 0부터 다시 시작합니다.

마찬가지로 최솟값보다 작아지면 언더플로우가 발생합니다

integer_min_underflow.c

#include <stdio.h>
#include <limits.h>    // 자료형의 최댓값과 최솟값이 정의된 헤더 파일

int main()
{
    char num1 = CHAR_MIN - 1;          // char의 최솟값보다 작은 수를 할당. 언더플로우 발생
    short num2 = SHRT_MIN - 1;         // short의 최솟값보다 작은 수를 할당. 언더플로우 발생
    int num3 = INT_MIN - 1;            // int의 최솟값보다 작은 수를 할당. 언더플로우 발생
    long long num4 = LLONG_MIN - 1;    // long long의 최솟값보다 작은 수를 할당. 언더플로우 발생

    // char, short, int는 %d로 출력하고 long long은 %lld로 출력
    // 부호 있는 정수에서 최솟값보다 작아지면 최댓값부터 다시 시작
    printf("%d %d %d %lld\n", num1, num2, num3, num4);
    // 127 32767 2147483647 9223372036854775807

    unsigned char num5 = 0 - 1;         // unsigned char의 최솟값보다 작은 수를 할당
                                        // 언더플로우 발생

    unsigned short num6 = 0 - 1;        // unsigned short의 최솟값보다 작은 수를 할당
                                        // 언더플로우 발생

    unsigned int num7 = 0 - 1;          // unsigned int의 최솟값보다 작은 수를 할당
                                        // 언더플로우 발생

    unsigned long long num8 = 0 - 1;    // unsigned long long의 최솟값보다 작은 수를 할당
                                        // 언더플로우 발생

    // unsigned char, unsigned short, unsigned int는 %u로 출력하고
    // unsigned long long은 %llu로 출력
    // 부호 없는 정수에서 최솟값보다 작아지면 최댓값부터 다시 시작
    printf("%u %u %u %llu\n", num5, num6, num7, num8);
    // 255 65535 4294967295 18446744073709551615

    return 0;
}

실행 결과

127 32767 2147483647 9223372036854775807 255 65535 4294967295 18446744073709551615

최솟값에서 1을 빼서 값이 더 작아지면 언더플로우가 발생하여 다시 한 바퀴 돌게 되므로 최댓값이 출력됩니다.

지금까지 정수 자료형의 오버플로우와 언더플로우를 설명했습니다. 값을 계산하다가 오버플로우나 언더플로우 현상이 발생하면 의도치 않은 결과가 나올 수 있습니다. 따라서 프로그래밍할 때는 정수 자료형의 크기를 항상 생각하고, 값이 범위를 넘어서지는 않는지 반드시 확인합니다.

읽을거리

게임을 개발하면서 몬스터를 스폰하는데 각 몬스터마다 겹치지 않는 ID가 필요하다고 치죠. 그래서 unsigned int 변수가 1씩 증가하면서 고유의 ID(유니크 ID)를 발급하는 함수를 만들었습니다. 언뜻 보기에는 잘 동작할 것 같지만 실제로는 문제가 있습니다. 시간이 오래 흘러 발급된 ID가 4,294,967,295을 넘어서면 오버플로우가 발생하여 0부터 다시 시작하게 됩니다. 이때부터는 ID가 중복되면서 여러 가지 문제가 발생할 것입니다.

오버플로우 문제는 당장 버그를 일으키지 않지만 언제 터질지 모르는 시한 폭탄과 같습니다. 즉, 오버플로우 상황이 발생하면 예상치 못한 버그가 발생하며 원인을 찾기가 매우 힘듭니다. 따라서 기능을 구현하기 전에 사용한 자료형이 적합한지 세심히 살펴봐야 합니다.

유튜브에서 싸이의 강남스타일 뮤직 비디오 조회수가 21억을 넘어서면서 음수로 표시된 적이 있었는데 이 사례도 정수 오버플로우 문제입니다.