콘텐츠로 건너뛰기

배열과 포인터는 지구인과 화성 아바타!

[C언어] 배열 포인터 완벽 정리: 화성 아바타 이론 (int* vs int (*)[3])

C언어에서 포인터를 배우다 보면 첫 번째 난관인 배열과 포인터의 관계를 마주하게 됩니다. 오늘은 이 관계를 조금 색다르게 “화성 식민지 개척”에 비유하여 정리해 보려 합니다.

특히 많은 분들이 “멘붕”을 겪는 int (*ptr)[3] (배열 포인터)의 주소 연산 비밀까지 파헤쳐 보겠습니다.

1. 배경 스토리: 화성 아바타 이론

가까운 미래, 인류는 화성에 식민지를 건설했습니다. 하지만 밖은 위험하기에 인간은 안전한 돔 안에 머물고, 자신의 인격을 동기화한 **’아바타’**를 밖으로 보내 작업을 수행합니다.

  • 지구인 (배열, Array): 안전한 돔(메모리 스택)에 터를 잡고 움직이지 않는 본체. 상수(Constant)처럼 취급됩니다.
  • 아바타 (포인터, Pointer): 밖(다른 메모리 주소)을 자유롭게 돌아다니며 본체를 가리키거나 다른 곳을 가리킬 수 있는 변수.

2. 같은 점: 동기화율 100%

아바타(포인터)가 지구인(배열)과 연결되면, 겉으로 보기엔 똑같이 행동합니다.

#include <stdio.h>

int main() {
    int arr[3] = { 1, 2, 3 }; // 지구인 (배열)
    int* parr = arr;          // 아바타 (포인터)

    // 1. 접근 방법이 동일하다
    printf("arr[0]=%d, *arr=%d\n", arr[0], *arr);
    printf("parr[0]=%d, *parr=%d\n", parr[0], *parr);
    
    return 0;
}

결과: 둘 다 1을 출력합니다. arr[i]parr[i]나 문법적으로 완벽하게 호환됩니다.

3. 다른 점: 서식지와 덩치

하지만 본질적으로 이 둘은 다릅니다.

  1. 서식지의 차이:
    • arr: 태어난 곳(메모리 주소)에서 절대 이사 갈 수 없습니다. (주소 상수)
    • parr: 언제든 연결을 끊고 다른 배열을 가리킬 수 있습니다. (주소 변수)
  2. 덩치(sizeof)의 차이:
    • sizeof(arr): 12바이트. (int 4byte * 3개). 지구인 3인.
    • sizeof(parr): 4바이트 (또는 8바이트). 아바타를 조종하는 인터페이스의(주소)의 크기일 뿐입니다.

4. 심화: 배열을 ‘통째로’ 가리키기 (&arr의 비밀)

여기서부터가 진짜입니다.

#include <stdio.h>

int main() {
    // [지구인] 3명으로 구성된 분대 (12바이트)
    int arr[3] = { 0, 1, 2 }; 

    // [일반 아바타] int형 하나(4바이트)를 가리킴
    int* parr = arr; 

    // [거대 아바타] int 3개짜리 덩어리(12바이트)를 가리킴
    // 괄호(*pbarr)는 "나는 포인터다"라는 선언이고, [3]은 "3칸씩 건너뛴다"는 뜻입니다.
    int(*pbarr)[3] = &arr; 

    printf("=== 1. 주소값 확인 (시작점은 같다) ===\n");
    printf("arr (첫 번째 원소 주소) : %p\n", arr);
    printf("&arr (배열 전체의 주소) : %p\n", &arr);
    // pbarr은 &arr을 담고 있으므로 똑같습니다.
    printf("pbarr (거대 아바타 값)  : %p\n", pbarr); 
    
    printf("\n");

    printf("=== 2. 일반 아바타의 이동 (보폭: 4바이트) ===\n");
    printf("arr     : %p \n", arr);
    printf("arr + 1 : %p (4바이트 증가)\n", arr + 1);
    printf("arr + 2 : %p (8바이트 증가)\n", arr + 2);

    printf("\n");

    printf("=== 3. 거대 아바타의 이동 (보폭: 12바이트) ===\n");
    // &arr은 배열 전체를 하나의 유닛으로 봅니다.
    printf("&arr     : %p\n", &arr);
    
    // 여기서 +1은 '다음 분대'로 이동하라는 뜻입니다.
    // int(4byte) * 3개 = 12바이트(0xC) 점프!
    printf("&arr + 1 : %p (12바이트 증가)\n", &arr + 1);
    
    // 12바이트 * 2 = 24바이트(0x18) 점프!
    printf("&arr + 2 : %p (24바이트 증가)\n", &arr + 2);

    return 0;
}
=== 1. 주소값 확인 (시작점은 같다) ===
arr (첫 번째 원소 주소) : 0x7fffffffd89c
&arr (배열 전체의 주소) : 0x7fffffffd89c
pbarr (거대 아바타 값)  : 0x7fffffffd89c

=== 2. 일반 아바타의 이동 (보폭: 4바이트) ===
arr     : 0x7fffffffd89c 
arr + 1 : 0x7fffffffd8a0 (4바이트 증가)
arr + 2 : 0x7fffffffd8a4 (8바이트 증가)

=== 3. 아바타 군단의 이동 (보폭: 12바이트) ===
&arr     : 0x7fffffffd89c
&arr + 1 : 0x7fffffffd8a8 (12바이트 증가)
&arr + 2 : 0x7fffffffd8b4 (24바이트 증가)

arr은 첫 번째 원소의 주소(&arr[0])를 의미하지만, &arr배열 전체의 시작 주소를 의미합니다.

  • arr: 101동 101호(첫 번째 집)의 주소
  • &arr: 101동 전체(건물)의 주소

주소판에 적힌 숫자는 똑같지만, “다음 칸으로 가라(+1)”고 명령했을 때 이동하는 거리가 다릅니다. 이걸 이용하면, 아바타 군단에게 명령을 내릴 수 있습니다.

배열 포인터의 등장 int (*pbarr)[3]

배열 전체(&arr)를 담으려면, 리모콘이 아니라 “작전 상황판(배열 포인터)”이 필요합니다. 아래 코드를 보면 이해할 수 있습니다.

// int *pbarr = &arr;  <-- 에러! (단순 포인터는 거대 배열을 못 담음)
int (*pbarr)[3] = &arr; // 성공! (3개짜리 덩어리를 인식하는 포인터)
  • (*pbarr): 괄호가 필수입니다. “나는 포인터다!”라고 먼저 선언하는 것입니다.
  • [3]: “나는 int 3개짜리 덩어리를 가리킨다”는 뜻입니다. (아바타 3개가 한 소대)

5. 심화실험: 주소값은 같은데 왜 결과가 다를까?

실험을 하다 보면 가장 혼란스러운 순간이 옵니다.

printf("pbarr : %p\n", pbarr);   // 0x100 (가정)
printf("*pbarr: %p\n", *pbarr);  // 0x100 (가정) -> 왜 같지??

pbarr은 배열 전체를 가리키고, *pbarr은 껍질을 하나 벗겨서 배열의 첫 번째 원소를 가리키기 때문입니다. 위치는 같지만 신분(Type)이 다릅니다.

이 차이는 +1을 해보면 명확해집니다.

  • *pbarr + 1 (일반 아바타의 이동):
    • int 하나 크기인 4바이트만큼 이동합니다.
    • 결과: arr[1]의 주소.
  • pbarr + 1 (거대 아바타의 이동):
    • int[3] 전체 크기인 12바이트만큼 이동합니다.
    • 결과: arr 배열 바로 뒤의 메모리 주소 (마치 2차원 배열의 다음 행으로 점프하는 것과 같음).

6. 결론: 값을 꺼내려면 별이 두 개 (**)

배열 포인터로 실제 값(1)을 꺼내려면 껍질을 두 번 벗겨야 합니다.

  1. pbarr: 배열 포인터 (Level 2)
  2. *pbarr: 배열 이름 arr로 변신 (Level 1)
  3. **pbarr: *arr과 같으므로 정수 값 1 (Level 0)
printf("%d", **pbarr); // 출력: 1
printf("%d", *(*pbarr + 1)); // 출력: 2 (옆집으로 이동 후 값 꺼내기)

[요약]

  • arr: 배열의 첫 번째 원소 주소. (보폭 4바이트)
  • &arr: 배열 전체의 주소. (보폭 12바이트)
  • int (*p)[3]: &arr을 저장하기 위한 전용 그릇. 2차원 배열을 다룰 때 필수적으로 사용됩니다.

화성에서 아바타를 조종할 때, 한 명씩 조종할지(int *), 분대 단위로 통째로 조종할지(int (*)[3])를 구분한다고 생각하면 이해가 쉽습니다!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다