N Log

포인터 배열과 배열 포인터 차이

들어가는 말

한국어로는 단어의 순서만 달라 헷갈리기 쉽지만, 두 개념은 분명히 다르다.
두 선언은 메모리 구조와 포인터 산술, 함수에 전달할 수 있는 인자 타입에서 차이를 보인다.

포인터 배열 (Array of Pointers)

#include <stdio.h>

int main(void)
{
    int row_0[] = { 1, 2 };
    int row_1[] = { 3, 4, 5 };
    int* rows[] = { row_0, row_1 };

    printf("%d\n", rows[0][1]);  // 2
    printf("%d\n", rows[1][2]);  // 5

    return 0;
}

포인터 배열은 배열인데 원소로 포인터를 가지고 있다.
int* rows[]은 포인터를 원소로 갖는 포인터 배열(Array of Pointers)이다.
이 예제에서는 각 포인터가 행 역할을 하는 배열을 가리키므로 rows[0][1]처럼 인덱싱해 2차원 배열처럼 접근할 수 있다.

rows는 배열이므로 포인터 원소들은 메모리에 연속해서 배치된다.
반면 각 포인터가 가리키는 row_0, row_1은 독립된 배열이다.

       rows

    +----------+          +------------+
 0  | &row_0   | -------> | row_0      |
    +----------+          | [ 1 ][ 2 ] |
 1  | &row_1   | --+      +------------+
    +----------+   |          0x1000
                   |
                   |      +-----------------+
                   +----> | row_1           |
                          | [ 3 ][ 4 ][ 5 ] |
                          +-----------------+
                                 0x8000

따라서 이 배열들이 메모리에 연속해서 배치된다고 보장할 수 없다.

배열 포인터 (Pointer to an Array)

#include <stdio.h>

int main(void)
{
    int matrix[2][3] =
    {
        { 1, 2, 3 },
        { 4, 5, 6 }
    };

    int (*array_pointer)[3] = matrix;

    printf("%d\n", array_pointer[1][2]);  // 6

    printf("%p\n", (void *)array_pointer);     // 00AFF740
    printf("%p\n", (void *)matrix);            // 00AFF740
    printf("%p\n", (void *)matrix[0]);         // 00AFF740
    printf("%p\n", (void *)&matrix[0][0]);     // 00AFF740

    printf("%p\n", (void *)matrix[1]);         // 00AFF74C
    printf("%p\n", (void *)&matrix[1][0]);     // 00AFF74C

    return 0;
}

matrix의 원소는 몇 개일까?
1부터 6까지의 값이 들어 있으므로 6개라고 생각하기 쉽다.
int 단위로 세면 맞는 말이지만, 타입 관점에서 matrix의 직접적인 원소는 2개다.

+---------+---------+---------+---------+---------+---------+
|    1    |    2    |    3    |    4    |    5    |    6    |
+---------+---------+---------+---------+---------+---------+
   [0][0]    [0][1]    [0][2]    [1][0]    [1][1]    [1][2]

matrix

├── matrix[0]
│     ├── matrix[0][0] = 1
│     ├── matrix[0][1] = 2
│     └── matrix[0][2] = 3

└── matrix[1]
      ├── matrix[1][0] = 4
      ├── matrix[1][1] = 5
      └── matrix[1][2] = 6

matrix의 원소는 matrix[0]matrix[1]이며, 각각 int 3개를 가지고 있고 타입은 int[3]이다.
matrix를 2행 3열 배열로 해석할 수도 있지만, matrixint[3] 타입의 원소 두 개를 갖는 배열이라고 생각할 수도 있다.

이 구조는 sizeof를 이용해 크기로도 확인할 수 있다.
여기서는 int의 크기가 4바이트라고 가정한다.
matrix에는 int가 6개 들어 있으므로 전체 크기는 24바이트이고, matrix[0] 하나의 크기는 12바이트다.

sizeof(matrix)    = 2 * 3 * 4 = 24 byte
sizeof(matrix[0]) = 3 * 4 = 12 byte

전체 크기를 첫 번째 원소인 matrix[0]의 크기로 나누면 matrix의 직접적인 원소 개수를 구할 수 있다.

sizeof(matrix) / sizeof(matrix[0]) = 24 / 12 = 2

반면 전체 int 원소의 개수는 다음과 같이 구할 수 있다.

sizeof(matrix) / sizeof(int) = 24 / 4 = 6

이 개념은 배열의 첫 번째 원소가 무엇인지 이해하는 데 중요하다.
matrix의 첫 번째 원소를 matrix[0][0], 즉 값 1이라고 생각할 수 있다.
하지만 matrix의 첫 번째 원소는 matrix[0]이며, 그 타입은 int[3]인 배열이다.
첫 번째 원소와 그 타입을 알아야 어떤 포인터로 가리킬 수 있는지도 알 수 있다.

C 언어에서는 배열 타입의 표현식이 대부분의 표현식 문맥에서 배열의 첫 번째 원소를 가리키는 포인터 식으로 암묵적으로 변환된다.
이때 배열 자체가 포인터로 바뀌는 것은 아니며, 변환된 포인터 식의 값은 첫 번째 원소의 주소인 &arr[0]에 해당한다.
이를 array-to-pointer conversion이라 하며, 흔히 array-to-pointer decay 또는 array decay라고도 부른다.

int (*array_pointer)[3] = matrix;를 살펴보자.
이 대입문에서 matrixarray-to-pointer conversion을 거쳐 첫 번째 원소인 matrix[0]의 주소, 즉 &matrix[0]으로 변환된다.
따라서 int (*array_pointer)[3] = &matrix[0];와 같은 의미가 된다.
matrix[0]의 타입은 int[3]이므로, &matrix[0]int[3] 배열을 가리키는 포인터이며 그 타입은 int (*)[3]이다.
따라서 &matrix[0]을 대입받으려면 왼쪽에도 int[3] 배열을 가리키는 int (*)[3] 타입의 포인터가 필요하다.
즉 배열 포인터는 포인터인데 배열을 가리킨다.

이제 코드에서 printf 결과를 살펴보자.
출력값을 보면 서로 다른 식이 같은 주소를 나타낸다는 것을 알 수 있다.
하지만 각 식의 타입은 다르므로 포인터 연산에서 한 칸 이동하는 크기가 다르다.

식의 타입 + 1 연산 시 의미
array_pointer int (*)[3] 다음 행으로 이동 (int 3개 크기)
matrix int[2][3] 다음 행으로 이동 (int 3개 크기)
matrix[0] int[3] 다음 int로 이동
matrix[1] int[3] 다음 int로 이동
&matrix[0][0] int * 다음 int로 이동
&matrix[1][0] int * 다음 int로 이동