[C언어 50강] 05강. 기본 자료형: char/int/float/double, sizeof, 자료형 범위
C언어에서 자료형은 단순히 "변수 종류"가 아니라, 메모리를 몇 바이트 할당하고 그 비트를 어떤 규칙으로 해석할지를 정하는 약속입니다. 오늘은 char/int/float/double의 차이와 sizeof, 그리고 자료형 범위를 연결해서 이해해보겠습니다. 핵심은 "값"보다 먼저 "표현 방식"을 이해하는 것입니다.
핵심 개념
- 자료형은 메모리 크기 + 해석 규칙이다.
- 정수형과 실수형은 같은 비트 수여도 저장 방식이 완전히 다르다.
sizeof와 한계값(<limits.h>, <float.h>)을 이용해 코드가 환경에 의존하지 않게 작성해야 한다.
개념 먼저 이해하기
초보자가 C언어에서 가장 먼저 겪는 혼란은 "왜 같은 숫자인데 결과가 다르지?"입니다. 예를 들어 100000을 int에 넣을 때는 정확하지만 float로 옮긴 뒤 연산하면 미묘한 오차가 생깁니다. 이건 컴파일러 버그가 아니라 자료형의 설계 목적이 다르기 때문입니다. 정수형은 정수 계산의 정확도를 목표로 하고, 실수형은 매우 넓은 범위를 근사값으로 표현하는 대신 정밀도를 일부 포기합니다. 즉 int는 "정확한 개수"에 강하고, float/double은 "측정값"이나 "연속값"에 강합니다.
char, int, float, double을 외울 때도 "몇 바이트인지"만 외우면 실전에서 금방 깨집니다. C 표준은 int가 정확히 4바이트라고 보장하지 않습니다(현대 시스템에선 대부분 4바이트지만 표준적으론 구현 정의). 그래서 우리가 믿어야 하는 건 감(感)이 아니라 sizeof(type) 결과와 한계 헤더의 상수입니다. 예를 들어 INT_MAX, INT_MIN, FLT_MAX, DBL_MAX를 출력해보면 "내가 지금 쓰는 시스템"의 경계를 알 수 있습니다. 이 습관이 중요한 이유는, 로컬에서는 잘 되던 코드가 다른 아키텍처나 임베디드 환경으로 가면 갑자기 오버플로, 형 변환 오류, 데이터 손실을 내기 때문입니다.
또 하나 중요한 포인트는 "자료형을 선택할 때 값의 의미를 먼저 본다"는 원칙입니다. 나이는 음수가 될 수 없으니 unsigned를 쓰고 싶어질 수 있지만, 실제로는 signed 정수가 더 안전한 경우도 많습니다(부호 혼합 연산에서 예기치 않은 형 변환이 발생). 반대로 파일 크기처럼 절대 음수가 아닌 크기 값은 size_t를 쓰는 것이 표준 라이브러리와 궁합이 맞습니다. 즉 자료형 선택은 메모리 절약 게임이 아니라, 버그를 줄이는 설계 행위입니다.
마지막으로 sizeof는 함수가 아니라 연산자이며, 컴파일 시점에 계산 가능한 경우가 많습니다. 그래서 배열 길이를 구할 때 sizeof(arr) / sizeof(arr[0]) 패턴을 자주 씁니다. 하지만 배열이 함수 매개변수로 전달되면 포인터로 decay되므로 같은 코드가 깨질 수 있습니다. 이 지점이 1차원 배열/포인터 강의로 이어지는 다리입니다. 지금 단계에서는 "sizeof는 실제 객체 타입을 본다"는 감각만 확실히 가져가면 충분합니다.
기본 사용
예제 1) 최소 동작 예제
#include <stdio.h>
int main(void) {
char ch = 'A';
int count = 100000;
float ratio = 0.1f;
double precise = 0.1;
printf("ch=%c, count=%d\n", ch, count);
printf("ratio=%.20f\n", ratio);
printf("precise=%.20lf\n", precise);
return 0;
}
설명:
float는 리터럴 뒤에f를 붙여 float 상수임을 명확히 합니다.- 같은 0.1이라도 float와 double 출력 자릿수에서 차이가 보입니다.
- 이 차이는 "틀림"이 아니라 "표현 정밀도 차이"입니다.
예제 2) 실무에서 자주 맞닥뜨리는 패턴
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void) {
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(float) = %zu\n", sizeof(float));
printf("sizeof(double) = %zu\n", sizeof(double));
printf("INT_MIN=%d, INT_MAX=%d\n", INT_MIN, INT_MAX);
printf("FLT_MIN=%e, FLT_MAX=%e\n", FLT_MIN, FLT_MAX);
printf("DBL_MIN=%e, DBL_MAX=%e\n", DBL_MIN, DBL_MAX);
return 0;
}
설명:
%zu는size_t출력용 포맷입니다.%d를 쓰면 경고 또는 UB 가능성이 생깁니다.<limits.h>,<float.h>를 활용하면 하드코딩 없이 경계를 확인할 수 있습니다.- 환경 의존 이슈를 조기에 탐지하는 기본 진단 코드로 유용합니다.
예제 3) 디버깅 포인트 포함 예제
#include <stdio.h>
#include <limits.h>
int main(void) {
int a = INT_MAX;
int b = a + 1; // signed overflow: C 표준에서 정의되지 않음
unsigned int ua = (unsigned int)INT_MAX;
unsigned int ub = ua + 1; // unsigned overflow: 모듈러 연산으로 정의됨
printf("a=%d, b=%d\n", a, b);
printf("ua=%u, ub=%u\n", ua, ub);
return 0;
}
설명:
- signed overflow는 "대충 한 바퀴 돌아감"으로 가정하면 안 됩니다(UB).
- unsigned는 비트폭 기준 모듈러로 정의되지만, 의도 없는 wraparound는 버그 신호입니다.
- 디버깅 시
-Wall -Wextra -Wconversion -fsanitize=undefined옵션으로 조기 탐지하는 습관이 좋습니다.
자주 하는 실수
실수 1) sizeof 결과를 %d로 출력
- 원인:
sizeof가 int를 반환한다고 오해함. - 해결:
sizeof의 결과 타입은size_t이므로%zu를 사용한다.
실수 2) float 비교를 ==로 직접 수행
- 원인: 실수도 정수처럼 정확히 같아야 한다고 생각함.
- 해결: 허용 오차(epsilon)를 두고 비교한다. 예:
fabs(a-b) < 1e-9.
실수 3) 정수 오버플로를 정상 동작으로 가정
- 원인: 특정 컴파일러/플랫폼에서 우연히 보인 결과를 표준 동작으로 착각.
- 해결: 경계값 검사를 먼저 하고, 필요하면 더 넓은 타입(
long long,int64_t)을 선택한다.
실무 패턴
- 수치 데이터의 의미를 먼저 정의한다: 개수(정수), 측정값(실수), 바이트 크기(
size_t). - API 경계에서는 타입을 명시적으로 맞춘다. (예: 라이브러리가
size_t를 요구하면 그대로 사용) - 매직 넘버 대신
<limits.h>,<stdint.h>상수를 사용한다. - 경고를 끄지 말고 경고가 나지 않는 타입 설계를 한다.
- 합계/누적값은 입력 단일값보다 더 큰 타입을 검토한다(누적 오버플로 예방).
오늘의 결론
한 줄 요약: 자료형은 "저장 크기"보다 "값의 의미와 표현 규칙"으로 선택해야 안정적인 C 코드를 만들 수 있다.
연습문제
char,int,long,long long,float,double의sizeof를 출력하고, 결과를 표로 정리해보세요.INT_MAX에 1을 더하기 전, 안전하게 검사해 overflow를 피하는 함수를 작성해보세요.float두 값을 비교할 때 허용 오차를 받는is_close(float a, float b, float eps)함수를 구현해보세요.
이전 강의 정답
4강(변수와 상수) 기준 복습 정답:
- 핵심: 변수는 "이름 있는 메모리", 상수는 "변하지 않는 값에 대한 계약"입니다.
const int DAYS = 7;처럼 의미가 있는 상수명을 주면 가독성과 유지보수성이 좋아집니다.- 스코프 관점에서 블록 내부 변수는 블록 밖에서 접근할 수 없으며, 같은 이름 shadowing은 혼란을 키우므로 피하는 것이 좋습니다.
예시 정답 코드:
#include <stdio.h>
int main(void) {
const int DAYS = 7;
int total = 0;
for (int day = 1; day <= DAYS; ++day) {
total += day;
}
printf("1~%d 합계 = %d\n", DAYS, total);
return 0;
}
실습 환경/재현 정보
- 컴파일러: Apple clang 17.x 또는 GCC 13+
- 컴파일 옵션:
-std=c11 -Wall -Wextra -Wconversion -pedantic - 실행 환경: macOS (arm64), 터미널 zsh
- 재현 체크:
sizeof출력이 환경마다 달라도 코드가 깨지지 않는지 확인- 실수 출력 자릿수에서 float/double 차이가 보이는지 확인
- 오버플로 예제는 sanitizer 옵션으로 경고/오류를 확인