27 정수 — 유한한 수의 세계
먼저 알아야 할 것
돌아보기
7장에서 부호 없는 정수의 넘침은 “정의된 감아 돌기”이고 부호 있는 넘침은 계약 밖이라 했다. 그런데 23장에서 만든 int는 부호가 있는가 없는가 — 우리가 여태 쓴 변수들은 어느 세계에 있었던 것인가?
답. int는 부호 있는 정수다 — 즉 여태 우리의 변수들은 “넘치면 계약 밖”의 세계에 있었다. 다행히 지금까지의 예제는 ±21억의 그릇을 넘볼 일이 없었지만, 이제 그 경계선을 정확히 그어 둘 때가 됐다. 그릇의 종류와 크기, 경계의 위치, 넘었을 때의 규칙 — 이 장의 내용이다.
이 장의 필요성과 맥락
이 장이 끝나면
<stdint.h>)까지.이 장에서 답할 질문
- 후보 목록에 없는 큰 수를 적으면 어떻게 되는가?
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 +127 | 8비트 | SCHAR_MIN SCHAR_MAX |
unsigned char | 0 255 | 8비트 | UCHAR_MAX |
char | 위 둘 중 하나(구현 정의) | 8비트 | CHAR_MIN CHAR_MAX |
short | −32767 +32767 | 16비트 | SHRT_MIN SHRT_MAX |
unsigned short | 0 65535 | 16비트 | USHRT_MAX |
int | −32767 +32767 | 16비트 | INT_MIN INT_MAX |
unsigned int | 0 65535 | 16비트 | UINT_MAX |
long | −2147483647 +2147483647 | 32비트 | LONG_MIN LONG_MAX |
unsigned long | 0 4294967295 | 32비트 | ULONG_MAX |
long long | 약 ±9.2×1018 | 64비트 | LLONG_MIN LLONG_MAX |
unsigned long long | 0 약 1.8×1019 | 64비트 | 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_EPSILON | 1.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) | int → long → long long | 부호 없는 타입은 후보에 없다 |
8진·16진·2진 (0xFFFFFFFF) | int → unsigned int → long → unsigned long → long long → unsigned long long | 부호 없는 타입이 중간중간 끼어든다 |
표 27.3
접미어를 붙이면 후보 목록을 손으로 좁히는 셈이다 — u 는 부호 없는 것만, l 은 long 부터, ll 은 long long 부터 훑는다.
이 차이가 실제로 드러난다.
| 적은 것 | 실측 sizeof | 타입 |
|---|---|---|
0xFFFFFFFF | 4 | unsigned int |
4294967295 | 8 | long |
표 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 문법으로 합류한다.