1-4. Pointer Arithmetic : 주소 연산의 비밀

hyunahn·2025년 12월 4일

Pointer Arithmetic: 주소 연산의 비밀

1. The Core Concept (본질 정의)

"포인터의 덧셈은 수학의 덧셈이 아닙니다. '보폭(Stride)'의 이동입니다."

수학에서 1000 + 11001입니다.
하지만 C언어의 포인터 세계에서 0x1000 + 10x1001일 수도, 0x1004일 수도, 심지어 0x2000일 수도 있습니다.

포인터 연산의 결과는 "그 포인터가 가리키는 자료형(Type)의 크기"에 따라 결정됩니다. 이것을 Scaling Factor(배율 계수)라고 합니다.


2. Under the Hood (하드웨어 투시)

CPU가 포인터 덧셈 명령(ptr + 1)을 만나면 내부적으로 어떤 계산을 수행하는지 뜯어보겠습니다.

공식은 다음과 같습니다:
TargetAddress=BaseAddress+(Step×sizeof(Type))Target Address = Base Address + (Step \times sizeof(Type))

시각적으로 확인해 봅시다. 주소 0x1000에서 시작한다고 가정합니다.

A. char *ptr (1 Byte stride)

char는 1바이트입니다. 보폭이 작습니다.

ptr     (0x1000) -> [ char ]
ptr + 1 (0x1001) -> [ char ]
ptr + 2 (0x1002) -> [ char ]

실제 CPU 계산: 0x1000 + (1 * 1) = 0x1001

B. int *ptr (4 Byte stride)

int는 4바이트입니다. 보폭이 큽니다.

ptr     (0x1000) -> [  int (4 bytes)  ]
ptr + 1 (0x1004) -> [  int (4 bytes)  ]  <-- 1을 더했는데 주소는 4가 증가!
ptr + 2 (0x1008) -> [  int (4 bytes)  ]

실제 CPU 계산: 0x1000 + (1 * 4) = 0x1004

C. struct Big *ptr (1024 Byte stride)

만약 1KB짜리 거대한 구조체라면?

ptr     (0x1000) -> [ Struct Big ... ]
ptr + 1 (0x1400) -> [ Struct Big ... ]  <-- 1을 더했는데 주소가 1024(0x400) 점프!

실제 CPU 계산: 0x1000 + (1 * 1024) = 0x1400


3. The "Why" & Real-world Analogy (이유와 비유)

Q: 왜 그냥 +1 하면 1바이트만 이동하게 안 만들었나요? 헷갈리게...
A: "배열(Array)을 쉽게 다루기 위해서입니다."

🏢 아파트 층간 이동 비유

여러분이 아파트 엘리베이터를 타고 있습니다.

  • 1층에서 2층으로 가고 싶습니다. 버튼 +1을 누릅니다.
  • 실제로는 높이가 3미터(m) 올라갑니다.

만약 엘리베이터가 "3미터 올라가주세요"라고 입력해야 한다면 얼마나 불편할까요?

  • "5층 가주세요" -> "15미터 위로 이동해" (계산 필요함)

C언어 설계자는 프로그래머가 "몇 바이트 뒤로 갈지" 계산하느라 머리 쓰는 것을 원치 않았습니다. 대신 "몇 번째 요소(Element)로 갈지"만 생각하게 만든 것입니다.

이것이 바로 arr[i]가 존재하는 이유입니다.
사실 컴파일러에게 arr[i]*(arr + i)를 예쁘게 쓴 것(Syntactic Sugar)에 불과합니다. CPU는 i번째 방을 찾기 위해 자동으로 i * 크기를 곱해서 주소를 찾아갑니다.


4. Code & Best Practice

1) void* 포인터의 함정 (주의!)

void는 "크기가 없는(알 수 없는) 타입"입니다. 표준 C언어(Standard C)에서 void*에 덧셈을 하는 것은 불법(Undefined Behavior)입니다.

void *v_ptr = malloc(100);
v_ptr++; // Error! 크기를 모르는데 몇 칸을 갈지 어떻게 알아?

Tip: GCC 컴파일러는 편의상 void* 연산을 char* 처럼 1바이트 이동으로 처리해주지만, 이식성을 위해 절대 의존하지 마십시오. 반드시 캐스팅 후 연산하세요.

2) 바이트 단위 정밀 제어 (강력 추천)

하드웨어 제어, 통신 패킷 처리 등을 할 때는 자료형과 상관없이 무조건 1바이트 단위로 주소를 움직여야 할 때가 있습니다. 이때는 uint8_t* (unsigned char) 캐스팅을 사용하십시오.

#include <stdio.h>
#include <stdint.h> // uint8_t

struct Packet {
    int id;
    int data;
};

int main() {
    struct Packet pkt[2]; // 구조체 배열
    
    // 1. 일반적인 포인터 연산 (구조체 크기만큼 점프)
    struct Packet *p_struct = pkt;
    p_struct++; // +8 bytes 이동 (sizeof(struct Packet))
    
    // 2. 바이트 단위 정밀 제어 (하드웨어 해킹 스타일)
    // "구조체 시작점에서 정확히 5바이트 뒤의 값을 보고 싶다!"
    uint8_t *p_byte = (uint8_t*)pkt;
    uint8_t val = *(p_byte + 5); 
    
    // 이 방식은 패딩 바이트나 특정 레지스터에 접근할 때 필수적입니다.
    return 0;
}

3) 두 포인터의 뺄셈

ptr2 - ptr1의 결과는 무엇일까요? 바이트 차이일까요?
아닙니다. "두 포인터 사이에 몇 개의 요소(Element)가 들어가는가"입니다.
즉, (Address2 - Address1) / sizeof(Type) 값이 반환됩니다.


생각해볼만한 점:
"포인터를 이해했다는 건, 이제 여러분이 메모리라는 건물의 '마스터키'를 쥐었다는 뜻입니다. 자료형이라는 껍데기를 벗겨내고(casting), char*를 이용해 1바이트 단위로 메모리를 수술할 수 있는 능력이 생겼으니까요."

0개의 댓글