[C언어 50강] 05강. 기본 자료형: char/int/float/double, sizeof, 자료형 범위

[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;
}

설명:

  • %zusize_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 코드를 만들 수 있다.

연습문제

  1. char, int, long, long long, float, doublesizeof를 출력하고, 결과를 표로 정리해보세요.
  2. INT_MAX에 1을 더하기 전, 안전하게 검사해 overflow를 피하는 함수를 작성해보세요.
  3. 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 옵션으로 경고/오류를 확인