C 언어 메모리 구조 분석: 정수, 바이트 오더, 부동소수점 저장 방식

1. 정수의 메모리 저장 방식

정수는 이진수로 표현될 때 원코드(부호-크기), 1의 보수, 2의 보수 세 가지 형태를 가집니다. 이 세 방식 모두 최상위 비트(MSB)는 부호 비트로 사용되며, 0은 양수, 1은 음수를 나타냅니다. 나머지 비트는 실제 수치 값을 표현합니다.

  • 양수: 원코드, 1의 보수, 2의 보수가 모두 동일합니다.
  • 음수: 세 가지 표현 방식이 서로 다릅니다.
  • 원코드: 수치를 그대로 이진수로 변환한 형태입니다. 사람이 읽기 쉬운 형태입니다.
  • 1의 보수: 원코드의 부호 비트는 유지한 채 나머지 비트를 반전시킨 형태입니다.
  • 2의 보수: 1의 보수에 1을 더한 값입니다. 실제 메모리에는 정수 데이터가 2의 보수 형태로 저장되며, 모든 산술 연산은 2의 보수를 기반으로 수행됩니다.

2. 바이트 오더(Endianness)와 시스템 판단

메모리에 데이터를 저장할 때, 특히 1바이트를 초과하는 데이터의 경우 바이트 단위의 저장 순서가 중요합니다. 이에 따라 빅 엔디안과 리틀 엔디안으로 나뉩니다.

#include <stdio.h>

int main() {
    unsigned int hex_val = 0xAABBCCDD;
    return 0;
}

디버거를 통해 메모리를 확인해 보면, 0xAABBCCDD가 바이트 단위로 역순으로 저장된 것을 볼 수 있습니다. 이를 이해하려면 엔디안 개념을 알아야 합니다.

2.1 빅 엔디안과 리틀 엔디안

  • 빅 엔디안(Big-Endian): 데이터의 상위 바이트(가장 큰 자릿값)를 메모리의 낮은 주소에 저장합니다. 사람이 숫자를 읽는 순서와 동일합니다.
  • 리틀 엔디안(Little-Endian): 데이터의 하위 바이트(가장 작은 자릿값)를 메모리의 낮은 주소에 저장합니다. 인텔 x86 아키텍처 등 대부분의 현대 프로세서가 이 방식을 사용합니다.

예를 들어 unsigned int hex_val = 0xAABBCCDD;일 때, 리틀 엔디안에서는 DD CC BB AA 순으로, 빅 엔디안에서는 AA BB CC DD 순으로 저장됩니다.

2.2 바이트 오더 판단 실습

실습 1: 현재 시스템의 엔디안 확인

현재 실행 중인 머신의 바이트 오더를 판별하는 함수를 작성해 봅시다.

#include <stdio.h>

int detect_endianness() {
    unsigned int test_val = 1;
    unsigned char *byte_ptr = (unsigned char *)&test_val;
    return *byte_ptr;
}

int main() {
    if (detect_endianness() == 1) {
        printf("Little-Endian\n");
    } else {
        printf("Big-Endian\n");
    }
    return 0;
}

해설: detect_endianness 함수는 정수 1을 할당한 후, 해당 주소를 unsigned char 포인터로 캐스팅하여 첫 번째 바이트만 읽어옵니다. 리틀 엔디안이라면 낮은 주소에 하위 바이트인 1이 저장되어 있으므로 1을 반환하고, 빅 엔디안이라면 0을 반환합니다.

실습 2: 부호 있는/없는 문자형의 오버플로우

#include <stdio.h>

int main() {
    char var1 = -1;
    signed char var2 = -1;
    unsigned char var3 = -1;
    printf("var1=%d, var2=%d, var3=%d\n", var1, var2, var3);
    return 0;
}

해설: charsigned char는 -128~127 범위를 가지므로 -1이 정상 저장됩니다. 하지만 unsigned char는 0~255 범위이므로 -1을 할당하면 2의 보수 표현에 따라 8비트 모두 1이 채워져 255로 출력됩니다. 정수 승격(Integer Promotion) 시 상위 비트는 0으로 채워집니다.

실습 3: 경계값 출력과 정수 승격

#include <stdio.h>

int main() {
    signed char limit_neg = -128;
    signed char limit_pos = 128; // 실제로는 -128로 오버플로우됨
    printf("neg=%u, pos=%u\n", limit_neg, limit_pos);
    return 0;
}

해설: 두 변수 모두 8비트 메모리에는 1000 0000으로 저장됩니다. %u(부호 없는 정수)로 출력할 때 정수 승격이 발생하며, 부호 비트가 1이므로 상위 비트가 모두 1로 확장됩니다. 결과적으로 두 값은 동일한 매우 큰 양수로 출력됩니다.

실습 4: 문자열 길이 함수와 char 배열의 순환

#include <stdio.h>
#include <string.h>

int main() {
    char buffer[500];
    for (int idx = 0; idx < 500; idx++) {
        buffer[idx] = -10 - idx;
    }
    printf("Length: %zu\n", strlen(buffer));
    return 0;
}

해설: bufferchar 배열이므로 값은 -128~127 사이를 순환합니다. strlen은 널 문자(\0, 즉 0)를 만날 때까지 길이를 셉니다. -10에서 시작하여 계속 감소하다가 0이 되는 시점까지의 바이트 수를 계산하면 246이 됩니다.

실습 5: 포인터 산술과 메모리 주소

#include <stdio.h>

int main() {
    int arr[4] = { 10, 20, 30, 40 };
    int *p_end = (int *)(&arr + 1);
    int *p_byte_shift = (int *)((char *)arr + 1);
    printf("%d, %x\n", p_end[-1], *p_byte_shift);
    return 0;
}

해설: &arr + 1은 배열 전체 크기(16바이트)만큼 주소를 이동시키므로, p_end[-1]은 배열의 마지막 요소인 40을 가리킵니다. (char *)arr + 1은 주소를 1바이트만 이동시킵니다. 리틀 엔디안 시스템에서 10(0x0000000A)의 두 번째 바이트부터 4바이트를 읽게 되므로, 출력 결과는 시스템의 바이트 오더와 메모리 레이아웃에 따라 결정됩니다.

3. 부동소수점의 메모리 저장 방식

다음 코드를 통해 정수와 부동소수점의 메모리 해석 차이를 확인해 봅시다.

#include <stdio.h>

int main() {
    int int_val = 9;
    float *float_ptr = (float *)&int_val;
    
    printf("int_val: %d\n", int_val);
    printf("*float_ptr: %f\n", *float_ptr);
    
    *float_ptr = 9.0f;
    printf("int_val after: %d\n", int_val);
    printf("*float_ptr after: %f\n", *float_ptr);
    
    return 0;
}

동일한 메모리 공간임에도 정수와 부동소수점으로 해석할 때 결과가 완전히 다릅니다. 이는 IEEE 754 표준에 따른 부동소수점 표현 방식 때문입니다.

3.1 IEEE 754 부동소수점 표현

부동소수점은 V = (-1)^S × M × 2^E 형태로 표현됩니다.

  • S (부호 비트): 0이면 양수, 1이면 음수.
  • M (가수부): 유효 숫자를 나타내며, 보통 1 ≤ M < 2 범위를 가집니다.
  • E (지수부): 2의 거듭제곱을 나타내는 지수.

32비트 단정밀도(Single Precision)의 경우, 1비트는 S, 8비트는 E, 나머지 23비트는 M에 할당됩니다.

3.1.1 저장 과정

가수부(M) 최적화: M은 항상 1.xxx 형태이므로, 앞의 1은 생략하고 소수점 아래 부분만 저장하여 1비트를 절약합니다.

지수부(E) 바이어스: E는 음수도 될 수 있으므로, 8비트 지수부의 경우 실제 값에 127을 더한 바이어스 값을 저장합니다. 예를 들어 지수가 3이라면 3 + 127 = 130이 저장됩니다.

3.1.2 읽어오는 과정

  • E가 0이 아니고 255도 아닌 경우: 일반적인 부동소수점입니다. 지수에서 127을 빼고, 가수부 앞에 1을 복원합니다.
  • E가 모두 0인 경우: 비정규화 숫자(Denormalized number)로, 매우 작은 값을 표현합니다. 지수는 1 - 127 = -126이 되며, 가수부 앞에는 1 대신 0이 복원됩니다.
  • E가 모두 1인 경우: 가수부가 0이면 무한대(Infinity), 0이 아니면 NaN(Not a Number)을 나타냅니다.

3.2 코드 결과 분석

초기 int_val = 9의 이진수는 0000 0000 0000 0000 0000 0000 0000 1001입니다. 이를 부동소수점으로 해석하면 지수부(E)가 모두 0이므로 비정규화 숫자로 처리되어 0에 매우 가까운 값(약 1.001 × 2^-146)이 되어 0.000000으로 출력됩니다.

반대로 *float_ptr = 9.0f를 대입하면, 9.0은 이진수로 1.001 × 2^3입니다. 부호는 0, 지수는 3 + 127 = 130 (10000010), 가수부는 001000...이 됩니다. 이를 32비트 정수로 해석하면 01000001 00010000 00000000 00000000이 되며, 10진수로 변환 시 1091567616이 출력됩니다.

태그: C언어 메모리구조 바이트오더 부동소수점 IEEE754

7월 21일 21:39에 게시됨