C의 모든 변수에는 타입이 있습니다. 선언할 때 정해지고 그 수명 동안 바뀌지 않습니다. 타입은 세 가지를 결정합니다. 변수가 몇 바이트를 차지하는지, 그 바이트들을 어떻게 해석하는지, 그리고 그 위에서 어떤 연산이 말이 되는지입니다.
C의 타입 목록은 짧습니다. 복잡한 쪽은 수식어입니다.
네 가지 기본 타입
- **
char**는 1바이트를 담습니다. 문자 하나('A'), 문자열의 바이트, 그리고 가끔은 아주 작은 정수로 쓰입니다. - **
int**는 정수를 다루는 일꾼 타입입니다. 반복 카운터, 크기, ID 같은 것들이죠. - **
float**과 **double**은 실수를 담습니다.double의 정밀도가 대략 두 배입니다. - **
void**도 어떤 의미로는 네 번째 타입이지만 "값이 없음"을 뜻합니다.void변수는 선언할 수 없고, 함수의 반환 타입이나 빈 매개변수 목록, 타입 없는 포인터로만 쓸 수 있습니다.
3.14f의 f 접미사에 주목하세요. 이게 없으면 리터럴은 double이 되고, 그걸 float에 대입하면 조용히 정밀도를 잃습니다. 일부 컴파일러는 이에 대해 경고합니다.
수식어: short, long, unsigned
기본 정수 타입에는 크기나 부호를 바꾸는 수식어가 딸려 옵니다.
정수 타입마다 고유한 서식 지정자가 필요합니다. short에는 %hd, int에는 %d, long에는 %ld, long long에는 %lld, unsigned에는 %u입니다. 잘못된 것을 쓰는 건 반올림 오차 정도가 아니라 미정의 동작입니다.
**unsigned**는 부호 비트를 없애고 양수 범위를 두 배로 늘립니다. unsigned int는 -21억에서 +21억 대신 0에서 약 43억까지를 담습니다. 크기, 바이트 수, 비트 패턴처럼 정말로 음수가 될 수 없는 것에는 알맞은 선택이지만, 0 아래로 뺄셈이 일어날 수 있는 것에는 함정입니다.
int, short, long, long long은 signed가 기본이라 거의 쓸 일이 없습니다. 중요한 곳은 char 하나입니다. 그냥 char가 signed인지 unsigned인지는 구현 정의이므로, 부호가 중요하다면 signed char나 unsigned char라고 쓰세요.
크기: 실제로 얻게 되는 값
C 표준은 정확한 크기가 아니라 최솟값을 정합니다. 요즘의 64비트 리눅스, macOS, 윈도우 머신이라면 보통 이렇습니다.
| 타입 | 일반적인 크기 | 일반적인 범위 |
|---|---|---|
char | 1바이트 | -128 ~ 127 (또는 0 ~ 255) |
short | 2바이트 | -32,768 ~ 32,767 |
int | 4바이트 | -2,147,483,648 ~ 2,147,483,647 |
long | 8바이트 (윈도우에서는 4) | 대략 ±9.2×10^18 |
long long | 8바이트 | 대략 ±9.2×10^18 |
float | 4바이트 | 유효 숫자 약 7자리 |
double | 8바이트 | 유효 숫자 약 15자리 |
long double | 16바이트 (환경마다 다름) | double보다 큼 |
사람들이 걸려 넘어지는 건 long 행입니다. 리눅스와 macOS에서는 8바이트이지만 64비트 윈도우에서는 4바이트입니다. long이 64비트 값을 담을 수 있다고 가정한 코드는 이식성이 없습니다. 크기가 요구사항의 일부라면 long long을 쓰거나 stdint.h의 정확한 폭 타입(int32_t, uint64_t)을 쓰세요.
sizeof: 컴파일러에게 물어보기
크기는 절대 추측하지 말고 측정하세요.
sizeof는 함수가 아니라 연산자이며, 컴파일 시점에 평가됩니다. 결과는 size_t 타입의 값이고 %zu로 출력합니다.
sizeof(char)는 정확히 1임이 보장됩니다. 그것이 C에서 바이트의 정의이기 때문입니다. 다른 모든 것은 그에 상대적으로 측정됩니다.
limits.h와 float.h
여러분의 컴파일러에서 정확한 범위는 이름 붙은 상수로 제공됩니다.
연산이 오버플로할지 알아야 할 때 비교 대상으로 삼을 값들이 이것입니다. a + b를 계산하기 전에 if (a > INT_MAX - b)를 검사하는 것이 오버플로를 일어나기 전에 잡아내는 방법입니다. 부호 있는 오버플로는 일어난 뒤에는 잡아낼 수 없기 때문에 중요합니다.
정수 오버플로
값이 타입의 범위를 넘어설 때 벌어지는 일은 전적으로 부호에 달려 있습니다.
부호 없는 오버플로는 정의되어 있습니다. 값이 2^N을 법으로 순환합니다.
두 번째 경우는 진짜 버그의 원천입니다. for (unsigned i = n - 1; i >= 0; i--) 같은 반복문은 결코 끝나지 않습니다. 부호 없는 값은 항상 >= 0이기 때문입니다.
부호 있는 오버플로는 미정의 동작입니다. "값이 감긴다"가 아니라 미정의입니다. 컴파일러는 그런 일이 절대 일어나지 않는다고 가정하고 최적화할 수 있으므로, 일이 벌어진 뒤에 쓴 오버플로 검사는 삭제될 수 있습니다.
int sum = a + b;
if (sum < a) { /* 컴파일러가 이 검사를 통째로 제거할 수 있습니다 */ }
한계값을 써서 미리 검사하세요.
if (b > 0 && a > INT_MAX - b) {
/* a + b가 오버플로할 상황 - 여기서 처리 */
}
부동소수점 정밀도
float과 double은 숫자를 이진수로 저장하는데, 대부분의 십진 소수는 정확한 이진 표현이 없습니다. 1/3에 정확한 십진 표현이 없는 것과 같은 이치입니다.
여기서 따라 나오는 규칙은 이렇습니다. 부동소수점 값을 ==로 비교하지 마세요. 대신 차이의 절댓값을 작은 허용 오차와 비교하세요.
그리고 돈에는 절대 부동소수점을 쓰지 마세요. 센트 단위를 정수로 저장하세요. 달러 단위의 double은 부정확하지만 센트 단위의 long long은 정확합니다.
타입 고르기
대부분의 코드를 커버하는 짧은 결정 목록입니다.
- 정수: 특별한 이유가 없다면
int. CPU가 가장 효율적으로 다루는 타입이고, 모든 산술 규칙이 그 위에 세워져 있습니다. - 20억보다 큰 값:
long long, 또는stdint.h의int64_t. sizeof나strlen에서 오는 크기, 길이, 배열 인덱스:size_t. 부호가 없고 어떤 객체든 담을 만큼 크다는 게 보장됩니다.- 소수:
double.float은 큰 배열에서 메모리를 절반으로 줄이거나 배정밀도 연산 장치가 없는 임베디드 하드웨어에서만 쓰세요. - 문자 하나와 원시 바이트: 텍스트에는
char, 이진 데이터에는unsigned char. - 참/거짓:
stdbool.h의bool- C의 불리언을 참고하세요. - 정확한 비트 폭(파일 포맷, 네트워크 프로토콜, 하드웨어 레지스터):
stdint.h의uint8_t,int16_t,uint32_t등.
타입 섞어 쓰기
한 식 안에 서로 다른 두 타입을 넣으면, C는 산술을 하기 전에 뒤에서 몰래 변환합니다. 보통은 도움이 되지만 가끔은 참사가 됩니다.
첫 번째는 정수 나눗셈입니다. 두 피연산자가 모두 int이므로 결과도 int이고 소수 부분은 버려집니다. 세 번째는 더 고약합니다. 부호 있는 값과 부호 없는 값을 비교하면 부호 있는 쪽이 부호 없는 값으로 변환되어 -1이 거대한 양수가 됩니다.
이런 변환 규칙과 명시적 캐스트로 그것을 제어하는 방법은 C의 타입 캐스팅에서 다룹니다.
자주 묻는 질문
C의 기본 자료형에는 어떤 것들이 있나요?
네 가지 기본 타입이 있습니다. 문자 하나와 바이트를 담는 char, 정수를 담는 int, 소수를 담는 float과 double입니다. 여기에 크기와 부호를 바꾸는 수식어 short, long, long long, signed, unsigned가 붙어 전체 목록이 만들어집니다.
C에서 int는 몇 바이트인가요?
요즘 데스크톱과 서버 시스템에서는 거의 항상 4바이트(32비트)이며, 대략 -21억에서 21억까지의 범위를 가집니다. 표준이 보장하는 것은 최소 2바이트뿐이고, 16비트 마이크로컨트롤러는 실제로 2바이트를 씁니다. 확실히 알아야 한다면 sizeof(int)를 쓰세요.
C에서 float과 double의 차이는 무엇인가요?
float은 4바이트로 유효 숫자가 약 7자리이고, double은 8바이트로 약 15자리입니다. 부동소수점 리터럴과 수학 함수의 기본은 double이며, 값을 수백만 개 저장하거나 임베디드 칩을 대상으로 하는 게 아니라면 double이 맞는 선택입니다.
C에서 int가 오버플로되면 어떻게 되나요?
부호 있는 int의 오버플로는 미정의 동작입니다. 컴파일러가 값을 감쌀 수도, 포화시킬 수도, 검사 자체를 통째로 최적화해 없앨 수도 있습니다. 부호 없는 int에서는 완전히 정의되어 있어서 2^N을 법으로 값이 순환합니다. 그래서 UINT_MAX + 1은 0입니다. 부호 있는 오버플로에는 절대 의존하지 마세요.