포인터 배열과 배열 포인터 차이
들어가는 말
한국어로는 단어의 순서만 달라 헷갈리기 쉽지만, 두 개념은 분명히 다르다.
두 선언은 메모리 구조와 포인터 산술, 함수에 전달할 수 있는 인자 타입에서 차이를 보인다.
포인터 배열 (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열 배열로 해석할 수도 있지만, matrix는 int[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;를 살펴보자.
이 대입문에서 matrix는 array-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로 이동 |