----------------
title : 씹어먹는 C 언어 - <12 - 3. 포인터는 영희이다! (포인터)>
cat_title : 12 - 3. 포인터는 영희이다! (포인터)
next_page : 26
publish_date : 2009-11-26
tex_title : 포인터의 포인터
path : /C
--------------


이번 강좌에서는

* 1 차원 배열을 가리키는 포인터

* 2 차원 배열을 가리키는 포인터 (배열 포인터)

* 포인터 배열

* 더블 포인터 (`**`)

![씹어먹는 C 언어](/img/ChewingClogo.png)

안녕하세요 여러분~! 이전 강좌는 잘 보시고 계시는지요? 아마도 이번 강좌가 최대의 난관일 듯 하네요. 이번 강좌를 잘 이해하냐, 이해 못하냐에 따라서 C 언어가 쉽다/어렵다가 완전히 좌우됩니다. 그러니 지금 졸린 사람들은 잠을 자고 쌩쌩할 때 오시길 바랍니다. (아마도 이 부분이 C 언어에서 가장 어려울 부분이 될 듯 하네요.. 저도 최대한 쉽게 설명하기 위해 노력하겠습니다^^)

잠깐 지난 시간에 배웠던 것을 머리속으로 상기시켜봅시다. 일단,

**배열을 배열이고 포인터는 포인터**이다. 다만;

* `sizeof` 와 주소값 연산자와 함께 사용할 때를 제외하면, 배열의 이름은 첫 번째 원소를 가리킨다. 
* `arr[i]` 와 같은 문장은 사실 컴파일러에 의해 `*(arr + i)` 로 변환된다.


이 두 가지 사실을 머리속에 잘 들어 있겠지요. 만일 위 두 문장을 읽으면서 조금이라도 의구심이 드는 사람은 바로 뒤로가기를 눌러서 이전 강좌를 보시기 바랍니다.



###  1 차원 배열 가리키기


일단, 강의의 시작은 간단한 것으로 해보겠습니다. 이전해도 말했듯이 `int arr[10];` 이라는 배열을 만든다면 앞서 이야기한 두 가지 경우를 제외한다면 `arr` 이 `arr[0]` 을 가리키는 포인터로 타입 변환 된다고 하였습니다. 

그렇다면 다른 `int*` 포인터가 이 배열을 가리킬 수 있지 않을까요? 한 번 프로그램을 짜봅시다.

```cpp-formatted
#include <stdio.h>
int main() {
  int arr[3] = {1, 2, 3};
  int *parr;

  parr = arr;
  /* parr = &arr[0]; 도 동일하다! */

  printf("arr[1] : %d \n", arr[1]);
  printf("parr[1] : %d \n", parr[1]);
  return 0;
}
```

성공적으로 컴파일 한다면

```exec
arr[1] : 2 
parr[1] : 2 
```

일단, 중점적으로 볼 부분은 아래와 같습니다.

```cpp-formatted
parr = arr;
```

바로 `arr` 를 `parr` 에 대입하는 부분이지요. 앞에서 말했듯이 `arr` 은 배열의 첫 번째 원소를 가리키는 포인터로 변환되고, 그 원소의 타입이 `int` 이므로, 포인터의 타입은 `int*` 가 되겠지요. 위 문장은 아래와 정확히 동일한 문장이 됩니다. 

```cpp-formatted
parr = &arr[0]
```

따라서, `parr` 을 통해서 `arr` 을 이용했을 때와 동일하게 배열의 원소에 마음껏 접근할 수 있게 되는 것이 됩니다. 위 모습을 한 번 그림으로 나타내보면 (아마도 여러분들은 지금 수준이라면 머리속으로 다 그릴 수 있어야 할 것입니다)

![caption=참고적으로 한 방의 크기는 그림의 단순화를 위해 4 바이트로 하였습니다.](/img/12.3.1.png)


```cpp-formatted
/* 포인터 이용하기 */
#include <stdio.h>
int main() {
  int arr[10] = {100, 98, 97, 95, 89, 76, 92, 96, 100, 99};

  int* parr = arr;
  int sum = 0;

  while (parr - arr <= 9) {
    sum += (*parr);
    parr++;
  }

  printf("내 시험 점수 평균 : %d \n", sum / 10);
  return 0;
}
```

성공적으로 컴파일 하면

```exec
내 시험 점수 평균 : 94 
```

일단, 포인터를 이용한 간단한 예제를 다루어보겠습니다.

```cpp-formatted
int* parr = arr;
```

먼저, `int` 형 1 차원 배열을 가리킬 수 있는 `int*` 포인터를 정의하였습니다. 그리고, 이 `parr` 은 배열 `arr` 을 가리키게 됩니다.

```cpp-formatted
while (parr - arr <= 9) {
  sum += (*parr);
  parr++;
}
```

그 다음 `while` 문을 살펴봅시다. `while` 문을 오래전에 배워서 기억이 안난다면 [여기로 돌아가세요!](/12)

이 `while` 문은 `parr - arr` 이 9 이하 일 동안 돌아가게 됩니다. `sum` 에 `parr` 이 가리키는 원소의 값을 더했습니다. `+=` 연산자의 의미는 아시죠? `sum += (*parr);` 문장은 `sum = sum + *parr` 와 같다는 것 알고 계시지요?

```cpp-formatted
parr++;
```

`parr` 을 1 증가시켰습니다. 이전 강좌에서도 이야기 하였지만 포인터 연산에서 1 증가시킨다면, `parr` 에 저장된 주소값에 1 이 더해지는 것이 아니라 `1 *` (포인터가 가리키는 타입의 크기) 가 더해진다는 것이지요.

즉, `int` 형 포인터 이므로 4 가 더해지게되서, 배열의 그 다음 원소를 가리킬 수 있게 됩니다. 아무튼, 위 작업을 반복하면 `arr` 배열의 모든 원소들의 합을 구하게 됩니다. `while` 문에서 9 이하일 동안만 반복하는 이유는, `parr - arr >= 10` 이 된다면 `parr[10` 이상의 값] 을 접근하게 되므로 오류를 뿜게 됩니다.

여기서 궁금한 것이 없나요? 우리가 왜 굳이 `parr` 을 따로 선언하였을까요? 우리는 `arr` 이 `arr[0]` 을 가리킨다는 사실을 알고 있으므로 `arr` 을 증가시켜서 `*(arr)` 으로 접근해도 되지 않을까요? 한 번, `arr` 의 값을 변경할 수 있는지 없는지 살펴봅시다.

```cpp-formatted
/* 배열명 */
#include <stdio.h>
int main() {
  int arr[10] = {100, 98, 97, 95, 89, 76, 92, 96, 100, 99};

  arr++;  // 오류
  return 0;
}
```

컴파일 해보면

```compiler-warning
error C2105: '++'에 l-value가 필요합니다.
```

와 같은 오류를 만나게 됩니다. 

배열의 이름이 첫 번째 원소를 가리키는 포인터로 타입 변경 된다고 했을 때, 이는 단순히 배열의 첫 번째 원소를 가리키는 주소값 자체가 될 뿐입니다. 따라서 `arr++` 문장은 C 컴파일러 입장에서 다음을 수행한 것과 같습니다.

```cpp-formatted
(0x7fff1234) ++; 
```

이는 애초에 말이 안되는 문장 이지요. 

###  포인터의 포인터

똑똑한 분들이라면 이러한 것들에 대해서도 생각해 보신 적이 있을 것입니다. 물론, 안하셔도 상관 없고요. 저의 경우 포인터 처음 배울 때 그것 마저 이해하기도 힘들어서 한참 버벅거렸습니다 :) 아무튼. 지금 머리속으로 예상하시는 대로 포인터의 포인터는 다음과 같이 정의합니다.

```cpp-formatted
int **p;
```

위는 **`int` 를 가리키는 포인터를 가리키는 포인터** 라고 할 수 있습니다. 쉽게 머리에 와닿지 않죠? 당연합니다. 이전 강좌의 내용도 어려워 죽겠는데 위 내용까지 머리속에 쑤셔 넣으려면 얼마나 힘들겠어요? 그래서, 한 번 예제를 봅시다.

```cpp-formatted
/* 포인터의 포인터 */
#include <stdio.h>
int main() {
  int a;
  int *pa;
  int **ppa;

  pa = &a;
  ppa = &pa;

  a = 3;

  printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);
  printf("&a : %p // pa : %p // *ppa : %p \n", &a, pa, *ppa);
  printf("&pa : %p // ppa : %p \n", &pa, ppa);

  return 0;
}
```

성공적으로 컴파일 했다면

```exec
a : 3 // *pa : 3 // **ppa : 3 
&a : 0x7ffd26a79dd4 // pa : 0x7ffd26a79dd4 // *ppa : 0x7ffd26a79dd4 
&pa : 0x7ffd26a79dd8 // ppa : 0x7ffd26a79dd8 
```

여러분의 결과는 약간 다를 수 있습니다. 다만, 같은 행에 있는 값들이 모두 같음을 주목하세요

일단 위에 보시다 싶이 같은 행에 있는 값들은 모두 같습니다. 사실, 위 예제는 그리 어려운 것이 아닙니다. 포인터에 제대로 이해만 했다면 말이죠. 일단 `ppa` 는 `int*` 를 가리키는 포인터 이기 때문에

```cpp-formatted
ppa = &pa;
```

와 같이 이전의 포인터에서 했던 것 처럼 똑같이 해주면 됩니다. `ppa` 에는 `pa` 의 주소값이 들어가게 되죠.

```cpp-formatted
printf("&pa : %p // ppa : %p \n", &pa, ppa);
```

따라서 우리는 위의 문장이 같은 값을 출력함을 알 수 있습니다. 위의 실행한 결과를 보아도 둘다 1636564 를 출력했잖아요.

```cpp-formatted
printf("&a : %p // pa : %p // *ppa : %p \n", &a, pa, *ppa);
```

그리고 이제 아래에서 두 번째 문장을 봐 봅시다. `pa` 가 `a` 를 가리키고 있으므로 `pa` 에는 `a` 의 주소값이 들어갑니다. 따라서, `&a` 와 `pa` 는 같은 값이 되겠지요. 그러면 `*ppa` 는 어떨까요? `ppa` 가 `pa` 를 가리키고 있으므로 `*ppa` 를 하면 `pa` 를 지칭하는 것이 됩니다. 따라서 역시 `pa` 의 값, 즉 `&a` 의 값이 출력되게 됩니다.

```cpp-formatted
printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);
```

마지막으로 위의 문장을 살펴 봅시다. `pa` 가 `a` 를 가리키고 있으므로 `*pa` 를 하면 `a` 를 지칭하는 것이 되어 `a` 의 값이 출력됩니다. 그렇다면 `**ppa` 는 어떨까요? 이를 다시 써 보면 `*(*ppa)` 가 되는데, `*ppa` 는 `pa` 를 지칭하는 것이기 때문에 `*pa` 가 되서, 결국 `a` 를 지칭하는 것이 됩니다. 따라서, 역시 `a` 의 값이 출력되겠지요. 어때요? 간단하죠?

위 관계를 그림으로 그리면 다음과 같습니다.


![a 의 주소값을 pa 가, pa 의 주소값을 ppa 가 가지고 있습니다. ](http://img1.daumcdn.net/thumb/R1920x0/?fname=http%3A%2F%2Fcfile23.uf.tistory.com%2Fimage%2F161550244AFEB8B93D7FD2)

### 배열 이름의 주소값?

지난 강좌에서 배열 이름에 `sizeof` 연산자와 주소값 연산자를 사용할 때 빼고는 전부다 포인터로 암묵적 변환이 이루어진다고 하였습니다. 그렇다면 주소값 연산자를 사용하면 어떻게 되길래 그러는 것일까요? 한 번 코드로 살펴봅시다.

```cpp-formatted
#include <stdio.h>

int main() {
  int arr[3] = {1, 2, 3};
  int (*parr)[3] = &arr;

  printf("arr[1] : %d \n", arr[1]);
  printf("parr[1] : %d \n", (*parr)[1]);
}
```

성공적으로 컴파일 하였다면

```exec
arr[1] : 2 
parr[1] : 2 
```

와 같이 잘 나옵니다. 

```cpp
  int (*parr)[3] = &arr;
```

`&arr` 은 도대체 무슨 의미를 가질까요? 이전에 `arr` 은 `int *` 로 암묵적 변환된다고 하였으니까 `&arr` 은 `int **` 가 되는 것일까요? **아닙니다!!** 암묵적 변환은 주소값 연산자가 왔을 때에는 이루어지지 않습니다.

`arr` 이 **크기가 3 인 배열** 이기 때문에, `&arr` 을 보관할 포인터는 **크기가 3 인 배열을 가리키는 포인터**가 되어야 할 것입니다. 그리고 C 언어 문법상 이를 정의하는 방식은 위와 같습니다. 

참고로 `parr` 을 정의할 때 `*parr` 을 꼭 `()` 로 감싸야만 하는데, 만일 괄호를 빼버린다면

```cpp
  int *parr[3]
```

와 같이 되어서 C 컴파일러가 `int *` 원소 3 개를 가지는 배열을 정의한 것으로 오해하게 됩니다 (아래 **포인터의 배열** 에서 좀 더 자세히 다룹니다). 

```cpp
  printf("parr[1] : %d \n", (*parr)[1]);
```

`parr` 은 *크기가 3 인 배열을 가리키는 포인터* 이기 때문에 배열을 직접 나타내기 위해서는 `*` 연산자를 통해 원래의 `arr` 을 참조해야 합니다. 따라서 `(*parr)[1]` 과 `arr[1]` 은 같은 문장이 되겠지요.

한 가지 재미있는 점은 `parr` 과 `arr` 은 같은 값을 가진다는 점입니다.

```cpp
#include <stdio.h>

int main() {
  int arr[3] = {1, 2, 3};
  int(*parr)[3] = &arr;

  printf("arr : %p \n", arr);
  printf("parr : %p \n", parr);
}
```

성공적으로 컴파일 하였다면

```exec
arr : 0x7ffda08cd25c 
parr : 0x7ffda08cd25c 
```

와 같이 나옵니다. `arr` 과 `parr` 모두 배열의 첫 번째 원소의 주소값을 출력합니다. 물론 두 개의 타입은 다르지만요. 이는 당연한데, `arr` 자체가 어떤 메모리 공간에 존재하는 것이 아니기 때문입니다. 

이와 같이 C 언어가 변태적으로 동작하는 이유는 사실 그 역사에 숨어있습니다. C 언어는 B 언어에서 파생된 언어인데, B 언어에서는 실제 배열이 있고, 배열을 가리키는 포인터가 따로 있었습니다. B 언어에서 `arr` 과 `arr[0]`, `arr[1]` 은 각기 다른 메모리를 차지하는 녀석들이고, `arr` 이 실제로 `arr[0]` 를 가리키는 포인터 였습니다. 따라서 `arr` 의 값을 출력하면 실제로 `arr[0]` 의 주소값이 나왔고, `&arr` 은 `arr` 의 주소값이 나왔겠지요. 따라서 B 언어에서 `arr` 과 `&arr` 은 다른 값을 출력했을 것입니다.

하지만 C 언어를 만든 데니스 리치 아저씨는 B 언의 문법을 계승하되, 이와 같이 비효율적으로 배열을 정의할 때 배열의 시작점을 가리키는 포인터로 공간을 낭비하고 싶지 않았습니다. 따라서 위와 같이 조금 이상하지만 그래도 메모리 공간을 효율적으로 쓰게 되는 배열 - 포인터 관계가 탄생하게된 것입니다.\sidenote{C 언어가 C 언어인 이유는 Computer 의 C 를 딴 것이 아니라 그냥 B 언어 다음에 나와서 그렇습니다. 그렇다면 B 언어는 A 언어 다음 언어라서 그런거냐 라고 물을 수 있는데, 사실은 BCPL 이라는 언어를 바탕으로 만들어졌기에 B 언어로 붙여졌습니다. 그럼 마지막으로 BCPL 은 어디서 온거냐 라고 물을 수 있는데 BCPL 은 Basic Computer Programming Language 의 약자로 1966년에 등장한 컴퓨터 언어 입니다. BCPL 의 C 는 진짜 Computer 의 C 네요!}

###  2 차원 배열의 [] 연산자

2 차원 배열의 `[]` 연산자에 대해선 제가 지난번 강좌에서 생각 해보기 문제로 내었던 것 같은데, 생각해보셨는지요? 일단 이전의 기억을 더듬에서 다음과 같은 배열이 컴퓨터 메모리 상에 어떻게 표현되는지 생각 해보도록 합시다.

```cpp-formatted
int a[2][3];
```

이전에도 이야기 하였듯이 이차원 배열은 쉽게 생각해서 1 차원 배열이 여러 개 있다고 생각하시면 됩니다. 위 경우 `int a[3]` 짜리 배열 2 개가 메모리에 연속적으로 존재한다고 생각하시면 됩니다. 

*2 차원* 이라는 단어에 혹해서 *메모리에 2 차원으로 존재하는 것 아니야?* 라고 생각할 수 도 있겠지만, 컴퓨터 메모리 구조는 1 차원 이기 때문에 항상 선형으로 퍼져서 있음을 알 수 있습니다. 

![caption=2 차원 배열은 메모리에 선형으로 존재합니다. 그냥 우리가 2차원 이라 생각만 하는 것일 뿐이죠.](/img/c/12.3.1.png)

실제로 프로그램을 짜서 각 원소들의 주소값을 찍어보면 메모리 상에 위와 같이 나타남을 알 수 있습니다. 한 번 해보세요.

그렇다면 위 2 차원 배열에서 `arr[0]` 와 같은 애들은 무엇을 의미할까요?

```cpp-formatted
/* 정말로? */
#include <stdio.h>
int main() {
  int arr[2][3];

  printf("arr[0] : %p \n", arr[0]);
  printf("&arr[0][0] : %p \n", &arr[0][0]);

  printf("arr[1] : %p \n", arr[1]);
  printf("&arr[1][0] : %p \n", &arr[1][0]);

  return 0;
}
```

성공적으로 컴파일 했다면

```exec
arr[0] : 0x7ffda354e530 
&arr[0][0] : 0x7ffda354e530 
arr[1] : 0x7ffda354e53c 
&arr[1][0] : 0x7ffda354e53c 
```

표현된 주소값은 여러분과 다를 수 있습니다.

`arr[0]` 의 값이 `arr[0][0]` 의 주소값과 같고, `arr[1]` 의 값이 `arr[1][0]` 의 주소값과 같습니다. 이것을 통해 알 수 있는 사실은 기존의 1 차원 배열과 마찬가지로 `sizeof` 나 주소값 연산자와 사용되지 않을 경우, **`arr[0]` 은 `arr[0][0]` 을 가리키는 포인터로 암묵적으로 타입 변환되고, `arr[1]` 은 `arr[1][0]` 을 가리키는 포인터로 타입 변환된다**라는 뜻이 됩니다. 

```lec-warning
1 차원 배열 `int arr[]` 에서 `arr` 과 `&arr[0]` 는 그 자체로는 완전히 다른 것이였던 것처럼 2 차원 배열 `int arr[][]` 에서 `arr[0]` 과 `&arr[0][0]` 와 다릅니다. 다만 암묵적으로 타입 변환 시에 같은 것으로 변할 뿐입니다.
```

따라서 `sizeof` 를 사용하였을 경우 2 차원 배열의 열의 개수를 계산할 수 있습니다. 

```cpp-formatted
int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  printf("전체 크기 : %d \n", sizeof(arr));
  printf("총 열의 개수 : %d \n", sizeof(arr[0]) / sizeof(arr[0][0]));
  printf("총 행의 개수 : %d \n", sizeof(arr) / sizeof(arr[0]));
}
```

성공적으로 컴파일 하였다면

```exec
전체 크기 : 24 
총 열의 개수 : 3 
총 행의 개수 : 2 
```

와 같이 나옵니다. 먼저 전체 배열에 `sizeof` 를 할 경우 당연하게도 배열의 전체 크기가 나오게 됩니다. 그렇다면 

```cpp-formatted
  printf("총 열의 개수 : %d \n", sizeof(arr[0]) / sizeof(arr[0][0]));
```

위 문장에서 `sizeof(arr[0])` 를 하면 무엇이 나올까요? 바로 0 번째 행의 길이 (총 열의 개수) 가 나오겠지요. 앞에서도 강조해왔듯이 `sizeof` 연산자의 경우 포인터로 타입 변환을 시키지 않기 때문에 `sizeof(arr[0])` 는 마치 `sizeof` 에 1 차원 배열을 전달한 것과 같습니다. 그리고 그 크기 (3) 을 알려주겠지요.

그리고 `sizeof(arr[0][0])` 을 하게 된다면 `int` 의 크기인 4 를 리턴하게 되어서 총 열의 개수를 알 수 있게 됩니다. 

```cpp-formatted
  printf("총 행의 개수 : %d \n", sizeof(arr) / sizeof(arr[0]));
```

그리고 총 행의 개수는 당연히도 전체 크기를 열의 크기로 나눈 것이 됩니다.

이 때, `arr[0][0]` 의 형이 `int` 이므로 `arr[0]` 은 `int*` 형이 되겠고, 마찬가지로 `arr[1]` 도 `int*` 형이 되겠습니다.

자 그렇다면 한 가지 질문을 해보겠습니다. 만일 2 차원 배열의 이름을 포인터에 전달하기 위해서는 해당 포인터의 타입이 뭐가 될까요? `arr[0]` 는 `int *` 가 보관할 수 있으니까, `arr` 은 `int **` 이 보관할 수 있을까요?

> 당연하지. 너가 위에서 설명했잖아. `int*` 를 가리키는 포인터는 `int**` 이라고

그런데 답은 **아니오** 입니다.

###  포인터의 형(type) 을 결정짓는 두 가지 요소

먼저 포인터의 형을 결정하는 두 가지 요소에 대해 이야기 하기 전에, 위에서 배열의 이름이 왜 `int**` 형이 될 수 없는지에 대해 먼저 이야기 해봅시다. 만일 `int**` 형이 될 수 있다면 맨 위에서 했던 것 처럼 `int**` 포인터가 배열의 이름을 가리킨다면 배열의 원소에 자유롭게 접근할 수 있어야만 할 것입니다.

```cpp-formatted
/* 과연 될까? */
#include <stdio.h>
int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int **parr;

  parr = arr;

  printf("arr[1][1] : %d \n", arr[1][1]);
  printf("parr[1][1] : %d \n", parr[1][1]);

  return 0;
}
```

그런데 컴파일 시에 아래와 같은 경고가 기분을 나쁘게 하네요.

```compiler-warning
test.c: In function ‘main’:
test.c:6:8: warning: assignment to ‘int **’ from incompatible pointer type ‘int (*)[3]’ [-Wincompatible-pointer-types]
    6 |   parr = arr;
      |        ^
```

아무튼, 무시하고 실행해봅시다.

```exec
arr[1][1] : 5 
[1]    8834 segmentation fault (core dumped)  ./test
```

헉! 예전에 보았던 친근한 오류가 뜹니다. 무슨 뜻일까요? 예전에 배열에 대해 공부하였을 때 ([11 - 1 강](18)) **초기화 되지 않은 값** 에 대해서 이야기한 적이 있었을 것입니다. 이 때, `int arr[3];` 이라 했는데 `arr[10] = 2;` 와 같이 허가되지 않은 공간에 접근하기만 해도 위와 같은 오류가 발생한다고 했습니다.

위 예제의 경우도 마찬가지 입니다. `parr[1][1]` 에서 이상한 메모리 공간의 값에 접근하였기에 발생한 일이지요. 그렇다면 왜? 왜? 이상한 공간에 접근하였을까요?

먼저, `int arr[10]` 이라는 배열에서 `x` 번째 원소의 주소값을 알아내는 방법을 생각해봅시다. 만일 이 배열의 시작주소를 그냥 `arr` 이라 한다면 `arr[x]` 의 주소값은

$$ arr + 4x $$

와 같이 나타낼 수 있습니다. 

이번에는 `int arr[a][b]` 라고 정의된 2 차원 배열을 생각해봅시다. 여기서 `arr[x][y]` 라는 원소를 참조할 때 이 원소의 주소값은 어떻게 알 수 있을까요?

앞서 말했듯이 `int arr[a][b]` 는 `int arr[b]` 짜리 배열이 메모리에 `a` 개 존재하는 것이라 생각하면 됩니다. 따라서 `arr[x][0]` 의 주소값은 `x` 번째 `int arr[b]` 짜리 배열이 되겠죠. 그렇다면 `arr[x][0]` 의 주소값은

$$ arr + 4bx$$
 
가 됩니다. 왜냐하면 `arr[b]` 배열의 크기는 `4b` 이니까 `x` 번째 배열의 시작 주소는 `4bx` 가 되기 때문이죠. 따라서 `arr[x][y]` 의 시작 주소값은

$$ arr + 4bx + 4y$$

가 됩니다. 여기서 중요한 점은 `arr[x][y]` 의 주소값을 정확히 계산하기 위해서는 `x`, `y` 뿐만이 아니라 `b` 가 뭔지 알아야 한다는 점입니다.

따라서 2 차원 배열을 가리키는 포인터를 통해서 원소들을 정확히 접근하기 위해서는;

1. 가리키는 원소의 크기 (여기서 4)
2. b 의 값

위 두 정보가 포인터의 타입에 명시되어 있어야지 컴파일러가 원소를 올바르게 접근할 수 있습니다. 그렇다면 실제로 2 차원 배열을 가리키는 포인터는 어떻게 생겼는지 살펴보겠습니다.

```cpp-formatted
/* 드디어! 배우는 배열의 포인터 */
#include <stdio.h>
int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int(*parr)[3];  // 괄호를 꼭 붙이세요

  parr = arr;  // parr 이 arr 을 가리키게 한다.

  printf("parr[1][2] : %d , arr[1][2] : %d \n", parr[1][2], arr[1][2]);

  return 0;
}
```

성공적으로 컴파일 한다면

```exec
parr[1][2] : 6 , arr[1][2] : 6 
```

2 차원 배열을 가리키는 포인터는 배열의 크기에 관한 정보가 있어야 한다고 했습니다. 2 차원 배열을 가리키는 포인터는 아래와 같이 써주면 됩니다.

```cpp
/* (배열의 형) */ (* /* (포인터 이름) */ )[ /* 2 차원 배열의 열 개수 */ ] ;
// 예를 들어서
int (*parr)[3];
```

이렇게 포인터를 정의하였을 때 앞서 이야기한 포인터의 조건을 잘 만족하는지 보도록 합시다. 일단, (배열의 형) 을 통해서 원소의 크기에 대한 정보를 알 수 있습니다. 즉, 가리키는 것에 대한 정보를 알 수 있게 됩니다. (조건 1 만족).

또한, `[2 차원 배열의 열 개수]` 를 통해서 `b` 의 값을 전달할 수 있습니다. (즉 배열의 한 행의 크기)

```cpp-formatted
int (*parr)[3];
```

위와 같이 정의한 포인터 `parr` 을 해석해 보면, `int` 형 이차원 배열을 가리키는데, 그 배열 한 행의 길이가 3 이군요! 라는 사실을 알 수 있습니다.

그런데 말이죠. 어디서 위와 같은 형태의 포인터 정의를 보지 않으셨나요? 맞습니다. 저 `parr` 은 사실 **크기가 3 인 배열을 가리키는 포인터** 를 의미합니다. 그런데 이게 말이 되는게, 1 차원 배열에서 배열의 이름이 첫 번째 원소를 가리키는 포인터로 타입 변환이 된 것 처럼, 2 차원 배열에서 배열의 이름이 첫 번째 **행** 을 가리키는 포인터로 타입 변환이 되어야 합니다. 그리고 그 첫 번째 행은 사실 크기가 3 인 1 차원 배열이지요!

```cpp-formatted
/* 배열 포인터 */
#include <stdio.h>
int main() {
  int arr[2][3];
  int brr[10][3];
  int crr[2][5];

  int(*parr)[3];

  parr = arr;  // O.K
  parr = brr;  // O.K
  parr = crr;  // 오류!!!!

  return 0;
}
```

위 코드에서 `parr` 이 `arr` 과 `brr` 은 받을 수 있어도 `crr` 은 왜 못받는지 아실 수 있겠죠? 

#### 그렇다면 이 코드는 무슨 일을 했었을 까요?

```cpp
#include <stdio.h>
int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int **parr;

  parr = arr;

  printf("parr[1][1] : %d \n", parr[1][1]); // 버그!

  return 0;
}
```

그렇다면 위 코드는 무슨 일을 했던 것일까요? 일단 parr 에는 arr 배열의 주소값이 들어가 있기는 합니다. 하지만 `parr[1][1]` 이 어떻게 해석되는지 생각해봅시다.

먼저 `parr[1][1]` 은 `*(*(parr + 1) + 1)` 과 동일한 문장이죠? `parr + 1` 을 하면 뭐가 될까요? 지금 `parr` 은 `int*` 를 가리키는 포인터 이고, `int*` 의 크기는 8 바이트 이기 때문에 `parr + 1` 을 하면 실제 주소값이 8 증가하게 됩니다.

따라서 `parr + 1` 은 `arr` 배열의 **세 번째** 원소의 주소값을 가지게 됩니다 (왜냐면 `int` 는 4 바이트 니까). 따라서 `*(parr + 1)` 은 3 이 될 것입니다.

그 다음에 `*(parr + 1) + 1` 을 하면 몇이 증가할까요? 현재 *(parr + 1) 의 타입은 `int *` 입니다. 따라서 `int` 의 크기 만큼인 4 가 늘어나게 됩니다. 결국 `*(parr + 1) + 1` 은 7 이 되겠죠.

그래서 결국 `*(*(parr + 1) + 1)` 은 마치 주소값 7 에 있는 값을 읽어라! 하는 말과 동일합니다. 그리고 해당 위치는 프로그램이 읽을 수 없기에 오류가 발생하게 되는 것이지요. 이해가 되시나요?

###  포인터 배열

자 이제 주제를 바꾸어서 마지마긍로 포인터 배열에 대해 이야기 해보고자 합니다. 포인터 배열은 말 그대로 **포인터들의 배열** 입니다. 위에서 설명한 배열 포인터는 배열을 가리키는 포인터이고, 애네는 반대로 포인터들을 모아놓은 배열 입니다. 두 용어가 상당히 헷갈리는데, 그냥 **언제나 진짜는 뒷부분** 이라고 생각하시면 됩니다. 즉, **포인터 배열은 정말로 배열이고, 배열 포인터는 정말로 포인터 였죠**.

```cpp-formatted
/* 포인터배열*/
#include <stdio.h>
int main() {
  int *arr[3];
  int a = 1, b = 2, c = 3;
  arr[0] = &a;
  arr[1] = &b;
  arr[2] = &c;

  printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
  printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
  printf("b : %d, *arr[2] : %d \n", c, *arr[2]);

  printf("&a : %p, arr[0] : %p \n", &a, arr[0]);
  return 0;
}
```

성공적으로 컴파일 한다면

```exec
a : 1, *arr[0] : 1 
b : 2, *arr[1] : 2 
b : 3, *arr[2] : 3 
&a : 0x7ffe8a2fa4e4, arr[0] : 0x7ffe8a2fa4e4 
```

마지막 출력결과는 여러분과 상이할 수 있으나 두 값이 같음을 주목하세요.

일단, `arr` 배열의 정의 부분을 봐봅시다.

```cpp-formatted
int *arr[3];
```

위 정의가 마음에 와닿나요? 사실, 저는 처음에 배울 때 별로 와닿지 않았습니다. 사실, 이전에도 말했듯이 위 정의는 아래의 정의와 동일합니다.

```cpp-formatted
int* arr[3];
```

이제, 이해가 되시는지요? 우리가 배열의 형을 `int, char` 등등으로 하듯이, 배열의 형을 역시 `int*` 으로도 할 수 있습니다. 다시말해, 배열의 각각의 원소는 *`int` 를 가리키는 포인터* 형으로 선언된 것입니다. 따라서, `int` 배열에서 각각의 원소를 `int` 형 변수로 취급했던 것처럼 `int*` 배열에서 각각의 원소를 포인터로 취급할 수 있습니다. 마치, 아래처럼 말이지요.

```cpp-formatted
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
```

각각의 원소는 각각 `int` 형 변수 `a,b,c` 를 가리키게 됩니다.이를 그림으로 표현하면 아래와 같습니다.


![caption=`arr[0]` 에는 `a` 의 주소값, `arr[1]` 에는 `b` 의 주소값, `arr[2]` 에는 `c` 의 주소값](http://img1.daumcdn.net/thumb/R1920x0/?fname=http%3A%2F%2Fcfile10.uf.tistory.com%2Fimage%2F121495024B0D491240D174)

`arr[0]` 에는 변수 `a` 의 주소가, `arr[1]` 에는 변수 `b` 의 주소, `arr[2]` 에는 변수 `c` 의 주소가 각각 들어가게 됩니다. 이는 마지막 `printf` 문장에서도 출력된 결과로 확인 할 수 있습니다.

사실, 포인터 배열에 관한 내용은 짧게 끝냈습니다. 하지만, C 언어에서 상당히 중요하게 다루어지는 개념입니다. 아직 여러분이 그 부분에 대해 이야기할 단계가 되지 않았다고 보아, 기본적인 개념만 알려 드린 것입니다. 꼭 잊지 마시길 바랍니다.

자. 이제 배열을 향한 대장정이 끝이 났습니다. 여기까지 부담없이 이해하셨다면 여러분은 C 언어의 성지를 넘게 된 것입니다! 사실, 여러분은 이 포인터를 무려 3 강의를 연달아 들으면서 '도대체 이걸 왜하냐?' 라는 생각이 머리속에 끝없이 멤돌았을 것입니다. 물론, 앞에서도 이야기 했지만 포인터는 다음 단계에서 배울 내용에 필수적인 존재입니다. 사실, 지금은 아무짝에도 쓸모 없는 것 같지만...

여기까지 스크롤을 내리면서도 마음 한 구석에 응어리가 있는 분들은 과감하게 포인터 강좌를 처음부터 읽어 보세요. 저의 경우 포인터만 책 수십권을 찾아보고 인터넷에서 수십개의 자료를 찾아가며 익혔습니다. 그래도 궁금한 내용들은 꼬오옥 댓글을 달아주세요. 저는 정말 아무리 이상하고 괴상한 질문도 환영하니.. 꼭 궁금한 내용을 물어봐주세요 :)

### 생각 해 볼 문제

#### 문제 1

3 차원 배열의, 배열이름과 동일한 포인터는 어떻게 정의될 것인가? (난이도 : 中)
(참고 :  2 차원 배열에선 `int (*arr)[4];` 와 같은 꼴이었다)

#### 문제 2

포인터 간의 형변환은 무엇을 의미하는가? 그리고, C 언어에서 포인터 간의 형변환이 위험한 것인가? (난이도 : 中)
(참고적으로, 포인터간의 형 변환은 아직 이야기 한 적이 없으나 한 번 시도는 해보세요)

##@ chewing-c-end