Proven C BookEnglish GitHub

27 정수 — 유한한 수의 세계

먼저 알아야 할 것

26장 타입의 갈래 · 정수 타입과 기본 타입의 자리
7장 정수의 표현 · 부호와 넘침
23장 변수 선언 · 타입은 그릇의 모양이다

돌아보기

7장에서 부호 없는 정수의 넘침은 “정의된 감아 돌기”이고 부호 있는 넘침은 계약 밖이라 했다. 그런데 23장에서 만든 int는 부호가 있는가 없는가 — 우리가 여태 쓴 변수들은 어느 세계에 있었던 것인가?

답. int는 부호 있는 정수다 — 즉 여태 우리의 변수들은 “넘치면 계약 밖”의 세계에 있었다. 다행히 지금까지의 예제는 ±21억의 그릇을 넘볼 일이 없었지만, 이제 그 경계선을 정확히 그어 둘 때가 됐다. 그릇의 종류와 크기, 경계의 위치, 넘었을 때의 규칙 — 이 장의 내용이다.

이 장의 필요성과 맥락

7장에서 배운 정수의 표현이 여기서 C 의 타입으로 바뀐다. 두 장을 스무 장이나 떼어 놓은 것은 그 사이에 언어의 뼈대를 세워야 했기 때문이고, 이제 비트 이야기를 문법 위에 얹을 수 있다. 6부가 「값을 벼린다」로 시작하는 것도 그래서다 — 흐름을 다루기 전에 흐를 값을 먼저 정확히 안다.

이 장이 끝나면

제6부는 값을 벼리고 흐름을 다스리는 부다. 첫 장은 C의 정수를 정면으로 다룬다 — 7장에서 배운 표현의 세계가 C의 타입 가족으로 어떻게 나타나는지, 오버플로가 실무에서 어떤 얼굴인지, 그리고 현대 C의 관행 (<stdint.h>)까지.

이 장에서 답할 질문

  1. 후보 목록에 없는 큰 수를 적으면 어떻게 되는가?
  2. char가 정수 타입 가족이라는 것이 이상하다 — 문자 아닌가?

26장에서 편 갈래의 한 칸을 파고든다 — 「부호 있는 정수 타입과 부호 없는 정수 타입」이라 한 마디로 지나간 그 가족이다.

27.1 정수 타입 가족

C의 정수 타입은 int 하나가 아니라 가족이다. 부호 있는 쪽의 기본 구성원은 크기 순으로 char(1바이트 — 4장의 그 “바이트=문자” 사연이 이름에 남았다), short, int, long, long long이고, 각각의 앞에 unsigned를 붙이면 부호 없는 짝이 된다(unsigned int 등). 표준은 각 타입의 최소 크기만 정한다 — int는 최소 16비트, long은 최소 32비트라는 식이라, 정확한 크기는 플랫폼의 몫이다(주류 환경에서 int는 32비트).

“기계마다 다르다”가 불안하게 들린다면 정확한 직감이다 — 그래서 현대 C의 관행은 크기가 이름에 박힌 타입을 쓰는 것이다. <stdint.h> 도구 상자의 int32_t(정확히 32비트, 부호 있음), uint8_t, int64_t 같은 타입들이다. 크기가 계약의 일부인 자리 — 파일 형식, 통신, 5장의 엔디안이 문제 되는 모든 곳 — 에서는 이쪽이 표준 관행이다. C23은 여기에 _BitInt(N) — 원하는 비트 폭을 직접 적는 정수 — 까지 보탰다. 이 책의 예제는 지면의 단순함을 위해 int를 기본으로 쓰되, 크기가 중요한 장면에서는 <stdint.h> 가족으로 갈아탄다.

27.2 기본 자료형 한눈에 보기 — 최소 범위와 실제 범위

C의 기본 자료형이 정확히 무엇무엇인지, 그리고 각자 얼마나 큰지를 한자리에 정리한다. 이 절은 찾아보는 자리다.

정수형은 다섯 층이고 각 층에 부호 있는 판과 없는 판이 있다 — char, short, int, long, long long. 여기에 bool(C99)과 문자 전용 타입들(char16_t·char32_t·wchar_t)이 딸린다. 실수형은 셋이다 — float, double, long double.

표준이 못박는 것은 크기가 아니라 최소 범위다. “int는 4바이트”라는 흔한 말은 표준의 문장이 아니라 오늘날 대부분의 플랫폼이 그렇다는 관찰일 뿐이다.

타입표준이 보장하는 최소 범위최소 폭한계 매크로
signed char−127   +1278비트SCHAR_MIN SCHAR_MAX
unsigned char0   2558비트UCHAR_MAX
char위 둘 중 하나(구현 정의)8비트CHAR_MIN CHAR_MAX
short−32767   +3276716비트SHRT_MIN SHRT_MAX
unsigned short0   6553516비트USHRT_MAX
int−32767   +3276716비트INT_MIN INT_MAX
unsigned int0   6553516비트UINT_MAX
long−2147483647   +214748364732비트LONG_MIN LONG_MAX
unsigned long0   429496729532비트ULONG_MAX
long long약 ±9.2×101864비트LLONG_MIN LLONG_MAX
unsigned long long0   약 1.8×101964비트ULLONG_MAX

표 27.1

최소 범위가 −127(−128이 아니라)인 것에 눈이 갈 것이다. 옛 표준이 세 가지 부호 표현을 모두 허용했기 때문이고(7장), C23이 2의 보수를 못박은 지금은 실질적으로 −128이다.

크기의 순서도 표준이 정한다. 폭은 이 순서를 거스를 수 없다.

char  ≤  short  ≤  int  ≤  long  ≤  long long

그리고 sizeof(char)는 언제나 1이다 — 바이트가 곧 char의 크기라는 정의(4장) 때문이다. 다만 한 바이트가 몇 비트인지CHAR_BIT이 말해 주고, 표준은 8 이상만 보장한다.

실수형의 한계는 <float.h>가 담당한다. 자주 쓰는 것만 추리면 이렇다.

매크로IEEE 754 double 기준
FLT_DIG DBL_DIG믿을 수 있는 십진 자릿수6 / 15
FLT_MAX DBL_MAX표현 가능한 최댓값약 1.8×10308
FLT_MIN DBL_MIN정규화된 최솟값약 2.2×10−308
FLT_EPSILON DBL_EPSILON1.0과 구분되는 최소 차이약 2.2×10−16
FLT_RADIX지수의 밑2

표 27.2

DBL_EPSILON은 50장에서 실수를 비교할 때 다시 만난다 — “같다”를 판단하는 기준을 세울 때 쓰는 값이다.

examples/ch27/limits.c

#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <float.h>
#include <stddef.h>

int main(void)
{
    printf("--- sizes of the basic integer types (on this machine) ---\n");
    printf("char=%zu short=%zu int=%zu long=%zu long long=%zu\n",
           sizeof(char), sizeof(short), sizeof(int), sizeof(long), sizeof(long long));
    printf("size_t=%zu ptrdiff_t=%zu void*=%zu\n",
           sizeof(size_t), sizeof(ptrdiff_t), sizeof(void *));

    printf("\n--- actual ranges (<limits.h> macros) ---\n");
    printf("CHAR_BIT  = %d\n", CHAR_BIT);
    printf("SCHAR_MIN = %d, SCHAR_MAX = %d, UCHAR_MAX = %u\n",
           SCHAR_MIN, SCHAR_MAX, (unsigned)UCHAR_MAX);
    printf("SHRT_MIN  = %d, SHRT_MAX  = %d, USHRT_MAX = %u\n",
           SHRT_MIN, SHRT_MAX, (unsigned)USHRT_MAX);
    printf("INT_MIN   = %d, INT_MAX   = %d, UINT_MAX  = %u\n",
           INT_MIN, INT_MAX, UINT_MAX);
    printf("LONG_MIN  = %ld, LONG_MAX = %ld\n", LONG_MIN, LONG_MAX);
    printf("LLONG_MAX = %lld, ULLONG_MAX = %llu\n", LLONG_MAX, ULLONG_MAX);

    printf("\n--- fixed-width types (<stdint.h>) ---\n");
    printf("INT8_MAX=%d INT16_MAX=%d INT32_MAX=%d\n",
           (int)INT8_MAX, (int)INT16_MAX, (int)INT32_MAX);
    printf("INT64_MAX=%lld UINT64_MAX=%llu\n",
           (long long)INT64_MAX, (unsigned long long)UINT64_MAX);
    printf("SIZE_MAX=%zu\n", SIZE_MAX);

    printf("\n--- floating types (<float.h>) ---\n");
    printf("float : %zu bytes, %d digits, max %g\n", sizeof(float), FLT_DIG, (double)FLT_MAX);
    printf("double: %zu bytes, %d digits, max %g\n", sizeof(double), DBL_DIG, DBL_MAX);
    printf("DBL_EPSILON = %g (smallest difference distinguishable from 1.0)\n", DBL_EPSILON);
    return 0;
}

실행 결과

--- sizes of the basic integer types (on this machine) ---
char=1 short=2 int=4 long=8 long long=8
size_t=8 ptrdiff_t=8 void*=8

--- actual ranges (<limits.h> macros) ---
CHAR_BIT  = 8
SCHAR_MIN = -128, SCHAR_MAX = 127, UCHAR_MAX = 255
SHRT_MIN  = -32768, SHRT_MAX  = 32767, USHRT_MAX = 65535
INT_MIN   = -2147483648, INT_MAX   = 2147483647, UINT_MAX  = 4294967295
LONG_MIN  = -9223372036854775808, LONG_MAX = 9223372036854775807
LLONG_MAX = 9223372036854775807, ULLONG_MAX = 18446744073709551615

--- fixed-width types (<stdint.h>) ---
INT8_MAX=127 INT16_MAX=32767 INT32_MAX=2147483647
INT64_MAX=9223372036854775807 UINT64_MAX=18446744073709551615
SIZE_MAX=18446744073709551615

--- floating types (<float.h>) ---
float : 4 bytes, 6 digits, max 3.40282e+38
double: 8 bytes, 15 digits, max 1.79769e+308
DBL_EPSILON = 2.22045e-16 (smallest difference distinguishable from 1.0)

이 출력은 이 책을 만든 기계의 것이라는 점이 중요하다. 다른 기계에서 돌리면 다른 수가 나올 수 있고, 그것이 바로 이 절의 요점이다 — 크기를 가정하지 말고 물어보라.

27.3 정수 상수의 타입 — 같은 값도 표기에 따라 다르다

20장에서 정수 상수를 적는 네 진법과 접미어를 보았다. 그때 미뤄 둔 것 — 컴파일러가 그 상수에 어떤 타입을 주는가 — 을 이제 정수를 배웠으니 정면으로 본다.

규칙은 「목록을 훑어 처음으로 담기는 타입」이다. 그런데 그 목록이 진법에 따라 다르다.

접미어 없는 상수후보 목록(앞에서부터)요점
10진 (4294967295)intlonglong long부호 없는 타입은 후보에 없다
8진·16진·2진 (0xFFFFFFFF)intunsigned intlongunsigned longlong longunsigned long long부호 없는 타입이 중간중간 끼어든다

표 27.3

접미어를 붙이면 후보 목록을 손으로 좁히는 셈이다 — u 는 부호 없는 것만, llong 부터, lllong long 부터 훑는다.

이 차이가 실제로 드러난다.

적은 것실측 sizeof타입
0xFFFFFFFF4unsigned int
42949672958long

표 27.4

같은 수인데 타입이 다르다. 그리고 타입이 다르면 그 뒤의 모든 것이 달라진다 — 승격과 통상 산술 변환(29장)이 다르게 걸리고, 비교의 결과가 뒤집히기도 한다.

반례. 비트 마스크를 십진으로 적기

x & 4294967295      /* long 과의 연산이 된다 */
x & 0xFFFFFFFFU     /* unsigned int 32비트 마스크임이 눈에 보인다 */

실무가 마스크를 16진으로 적는 이유는 읽기 좋아서만이 아니다. 타입이 달라지기 때문이고, 무엇보다 비트 수가 눈에 보이기 때문이다 — 0xFFFF 는 16비트, 0xFFFFFFFF 는 32비트라는 것이 자릿수로 드러난다. 여기에 U 를 붙여 부호까지 못박는 것이 관행이다(부호 있는 정수의 시프트는 28장의 회색지대다).

문. 후보 목록에 없는 큰 수를 적으면 어떻게 되는가?

답. 가장 큰 후보에도 담기지 않으면 구현이 정한 확장 정수 타입에 담기거나, 그런 타입이 없으면 제약 위반이라 진단을 받는다. 실무에서 이 자리를 만나면 답은 하나다 — 고정 폭 타입과 그 매크로를 쓴다(UINT64_C(…), <stdint.h>). 「어떤 타입이 될지 컴파일러에게 맡기지 않는다」가 이 장 전체의 규율과 같은 말이다.

27.4 크기가 계약인 자리 — 고정 폭 타입

폭이 정확히 정해져야 하는 곳이 있다. 파일 형식, 네트워크 프로토콜, 하드웨어 레지스터가 그렇다. 이런 자리에는 <stdint.h>의 고정 폭 타입을 쓴다.

갈래
정확한 폭int8_t uint16_t int32_t uint64_t정확히 그 비트 수. 없으면 제공되지 않는다
최소 폭int_least16_t적어도 그만큼인 것 중 가장 작은 타입
가장 빠른int_fast32_t적어도 그만큼이면서 기계가 가장 빨리 다루는 타입
포인터 크기intptr_t uintptr_t포인터를 담을 수 있는 정수(14장의 출처 주의)
최대intmax_t uintmax_t가장 넓은 정수
크기·차이size_t ptrdiff_t크기와 포인터 차이의 타입

표 27.5

각각의 한계도 매크로로 있다 — INT32_MAX, UINT64_MAX, SIZE_MAX, PTRDIFF_MAX처럼. 서식 지정자는 <inttypes.h>PRId32 계열을 쓴다(부록 B).

수칙은 단순하다. 의미가 “이 기계의 자연스러운 정수”라면 int, “크기·인덱스”라면 size_t, “형식이 정한 폭”이라면 고정 폭 타입.

27.5 경계를 눈으로 본다

그릇의 가장자리는 <limits.h> 도구 상자가 알려 준다. 그리고 부호 없는 세계의 감아 돌기는 — 7장에서 지면으로 배운 그것 — 이제 직접 실행해 보일 수 있다.

examples/ch27/wrap.c

#include <limits.h>
#include <stdio.h>

int main(void)
{
    printf("INT_MAX  = %d\n", INT_MAX);
    printf("UINT_MAX = %u\n", UINT_MAX);
    printf("UINT_MAX + 1 = %u\n", UINT_MAX + 1u);  /* 정의된 감아 돌기 */
    return 0;
}

실행 결과

INT_MAX  = 2147483647
UINT_MAX = 4294967295
UINT_MAX + 1 = 0

서식 하나가 새로 합류했다 — 부호 없는 정수는 %d가 아니라 %u로 찍는다(22장의 서식 계약이 타입 가족을 따라 늘어나는 것이다). 그리고 셋째 줄이 7장의 약속 그대로다: 최댓값에 1을 더하니 0 — 시계가 한 바퀴 돈 것이고, 이것은 정의된 동작이다.

부호 있는 쪽은 사정이 다르다. INT_MAX + 1을 계산하는 코드는 계약 밖(정의되지 않은 동작)이라서, 이 책의 검증 파이프라인에 넣을 수조차 없다 — 17장에서 장만한 UBSan이 정확히 이런 코드를 실행 중에 잡아 낸다. 같은 “+1”인데 한쪽은 시연 가능하고 한쪽은 불가능하다는 것 자체가, 두 세계의 차이를 웅변한다.

흔한 오해. “오버플로는 큰 수를 다루는 특수한 프로그램의 문제다”

그럴듯하지만, 실제 사고 목록은 반대를 말한다. 가장 흔한 오버플로는 평범한 곳에서 난다 — 두 인덱스의 평균을 (a + b) / 2로 구하다가 더하는 중간 결과가 넘치고(유명한 이진 탐색 버그: 표준 라이브러리 들에 20년 가까이 숨어 있었다), 밀리초 타이머가 49일 만에 감아 돌고 (Windows 95의 49.7일 멈춤 사고), 크기 계산 개수 * 크기가 넘쳐 터무니없이 작은 메모리를 잡는다(보안 사고의 고전). 공통점은 최종 값이 아니라 중간 계산이 넘친다는 것이다 — 그릇 걱정은 결과가 아니라 수식의 모든 중간 단계에 대해 하는 것이고, 이것이 proven류의 검사 산술이 존재하는 이유다(43장).

문. char가 정수 타입 가족이라는 것이 이상하다 — 문자 아닌가?

답. C에서 문자와 작은 정수는 같은 것이다 — 8장에서 배운 대로 문자는 번호이고, char는 그 번호를 담는 1바이트 정수 그릇일 뿐이다. 'A'라는 문자 리터럴의 값은 그냥 65다. 한 가지 함정만 미리 — char가 부호 있는지 없는지는 플랫폼마다 다르다(둘 다 아닌 제3의 타입이다). 그래서 수치 계산에는 char를 쓰지 않고, 바이트가 필요하면 uint8_t를 쓰는 것이 현대 관행이다. 문자열과 char의 본격적인 이야기는 42장에 있다.

정수의 그릇들을 정리했다. 다음 장은 그 그릇 위의 연산 — 20장에서 미뤄 둔 나눗셈의 진실과, 7장에서 배운 비트 세계의 연산자들이 C 문법으로 합류한다.