C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

항상 간단히 해당 개념을 사용만 하실 분들은 각 번호의 마지막 부분이나 게시물의 제일 하단을 봐주시면 됩니다!

1. 포인터의 표현과 주소 관계

저번 게시물을 보신 분들은 배열 포인터를 사용해서 2차원 배열 값을 참조할 수 있다는 것을 확인하셨을 겁니다.

일단 저번 게시물 복습도 할 겸 좀 더 심화해 봅시다. (잘 기억 안 나시는 분들은 이전 게시물 3번을 확인하고 와주세요.)

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

위와 같이 출력 됐던 걸 기억하실 겁니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

얘네들의 주소는 위와 같이 차례로 int 형의 크기인 4 byte 씩 차이나는 것도 확인해봤습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

배열 포인터 뒤에 붙이는 괄호를 하나만 하고 주소를 확인했을 때는 각 행의 첫번째 원소의 주소값이 반환되는 것을 확인했습니다.

각 행에 4개의 열, 즉, 4개의 원소가 있기 때문에 parr[n]과 parr[n+1]의 주소는 4*4 = 16 만큼 차이가 발생합니다.

우리는 이전 게시물들을 통해 한가지 분명한 사실을 배웠습니다.

    int a[2] = {11, 12};

    int *pa = &a;

이렇게 선언되었을 때 a의 주소에 할당된 메모리 내부의 값 '12'을 포인터 pa를 통해 참조하는 방법은

*(pf+1)과 pf[1]가 동일한 값을 보입니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

그래서 이번 게시물의 시작은 다음과 같은 질문으로 시작해보겠습니다.

그럼 위와 같은 배열 포인터도 *(parr + n) 과 같은 꼴로 표현할 수 있지 않을까?

일단 위의 결과로 보아 배열 포인터를 사용해 2차원 배열을 참조했을 때 *(pa+i)의 꼴로 출력할 수 없습니다.

i가 1 증가할 때 마다 열의 갯수 * int 자료형 크기만큼 주소가 증가하기 때문입니다.

이것의 이유는 parr의 크기가 [4]로 선언되었기 때문입니다. (int *parr[4];)

그럼 좀 다른 방법으로 포인터를 찔러보겠습니다.

제가 실험해볼 것은 *(parr+i)가 parr[i]과 동일한 결과를 보이기 때문에 저희가 처음에 배열 포인터를 통해 2차원 배열을 출력했던 방식인 parr[i][j] 꼴을 다음과 같이 변경해 볼 것입니다.

parr[i]로 먼저 다가가고 그 다음에 뒤에 붙은 [j]가 따라올 것이기 때문입니다.

우선 parr[i]는 이전과 동일하게 *(parr+i)와 같습니다.

P = *(parr+i)로 치환하면 P[j]가 됩니다.

이건 *(P+j)와 동일합니다.

따라서 *(P+j)는 *(*(parr+i)+j)와 같습니다.

*(*(parr+i)+j) 이게 잘 동작하는 지 확인해볼까요?

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

완전 잘 나오네요.

근데 또 하나 궁금한게 생겼습니다.

위에서 봤듯이 편의상 2차원 배열과 같이 parr[i][j]로 참조했지만 사실 주소값은 4씩 차이가 나면서 연결되어 있는 것을 확인했습니다.

그럼 굳이 위에서 i자리를 바꾸지 않아도 되지 않을까요?

또 확인해봅시다.

*(*(parr+i)+j)는 j가 1 증가할 때마다 바깐 *( )의 내부 주소가 4씩 커지니까 저희가 선언한 배열 순서 대로 출력이 됐을겁니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

역시 맞습니다.

어차피 j 하나 당 주소가 4씩 증가하는데 i 자리는 0으로 두고 j를 0에서 11까지 변화시켜도 2차원 배열의 모든 값을 출력할 수 있을 것 같습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

다행히 예상대로 출력됐습니다.

이제 추측말고 정확한 개념을 알아보겠습니다.

parr의 주소가 500이라고 하겠습니다.

parr + i = parr + i*(sizeof(parr[0])) = parr + i*(16) = 500 + i * 16가 됩니다.

따라서 i가 1 증가할 때 parr+i의 주소값이 16 증가하게 됩니다.

*(parr+i)+j는 다음과 같습니다.

parr+i = 500 + i * 16 이라는 주소를 *가 가리키면 그 주소에 할당된 메모리 내의 데이터가 반환이 됩니다.

*(500 + i * 16)은 arr[i]에 해당하는 원소의 주소값을 가지고 있습니다.

*(parr + i) = parr[i]이고 이것은 arr[i]와 동일합니다.

arr[i][j]에서 arr[i]는 i 행의 첫번째 열의 값과 동일한 값을 반환합니다.

즉, arr[i] = arr[i][0]과 같고, 주소값도 같습니다.

따라서 *(parr+i)+j는 아래의 식으로 계산 가능합니다.

이때 arr[0]의 주소를 300이라고 하겠습니다.

*(parr+i) + j = 300 + j * sizeof(parr[0][0]) = 300 + j * 4

따라서, *(300 + j * 4)를 해줌으로써 300 + j * 4의 주소에 할당된 메모리에 저장된 데이터를 가져올 수 있습니다.

sizeof 를 연산의 결과는 아래를 보시면 확인하실 수 있습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

여기서 하나 알 수 있는 것은 parr[0][0] 꼴로 2차원 배열을 가져올 순 있지만, parr[0] 꼴로 arr[0]의 데이터를 가져올 수는 없습니다.

자 정리합시다!

2차원 배열을 배열 포인터로 받는 것에 대해 자세히 알아봤습니다.

막상 쓸 때는 간단하게 다음과 같이 사용하시면 됩니다.

int arr[3][4] = {

    {11, 12, 13, 14},

    {21, 22, 23, 24},

    {31, 32, 33, 34}

};

arr을 다음과 같이 포인터가 가리킬 수 있습니다.

int (*parr)[4] = arr; // 괄호와 열의 크기를 꼭 맞춰줘야 합니다.

or

int (*parr)[4];

parr = arr;

배열의 값은 parr을 사용해서 다음과 같이 참조할 수 있습니다,

기준 : arr[i][j]

1. parr[i][j]

2. *(*(parr+i)+j)

이렇게 길게 설명을 한 것은 이중 포인터와 연관이 크기 때문입니다.

사실 지금까지 설명한 것과 이중 포인터는 거의 동일합니다.

2. 이중 포인터 (포인터의 포인터)

포인터와 배열의 크기와 주소의 관계를 알아보고 이중 포인터로 넘어가겠습니다.

바쁘신 분들은 그냥 바로 이중 포인터 (2번의 하단 = 2) 이중 포인터 )로 넘어가셔도 돼요!

1) 포인터와 배열의 크기와 주소 관계

포인터 변수는 어떤 변수의 주소를 저장하고 *을 통해 주소에 할당된 메모리를 참조할 수 있습니다.

  int a   = 10;

  int *p = &a;

여기서 p가 포인터 변수, a가 어떤 변수, &a가 어떤 변수의 주소입니다.

p도 변수이기 때문에 자기 자신의 주소와 크기를 갖고 있습니다.

p는 주소를 저장하고 있기 때문에 크기는 8(= sizeof(p))입니다.

*p는 int 형 변수 a의 데이터를 참조하기 때문에 크기는 4(= sizeof(*p))입니다.

    printf("*p          = %d\n", *p);

    printf("sizeof(p)   = %d\n", sizeof(p));

    printf("sizeof(*p) = %d\n", sizeof(*p));

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

한 줄이라도 놓지지 말고 따라오셔야 나중에 안 헷갈립니다!

이제 배열로 넘어가겠습니다.

    int b[4][3];

위와 같은 배열은 2차원 배열이라고 배웠습니다.

    int b[4][3] = {

        {11, 12, 13},

        {21, 22, 23},

        {31, 32, 33},

        {41, 42, 43}

    };

이렇게 선언할 수 있었습니다.

이제 중요합니다!!

사실 b는 4칸 짜리 배열입니다.

각 칸에 다시 3칸짜리 배열이 들어있는거죠.

이해하셨죠?

크기도 한번 확인해보겠습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

b는 4 (행크기) * 3 (열 크기) * 4 (int 자료형 크기) = 48

b[0]는 내부에 3개의 열을 가지고 있으므로 3 (열 크기) * 4 (int 자료형 크기) = 12

b[0][0]은 내부에 int 자료형의 데이터를 저장할 수 있으므로 4 (int 자료형 크기) = 4

전에 주소를 확인할 때 배열로 선언된 변수 명 (= b)과 배열의 첫번째 요소의 주소 (= &b[0])는 모두 같다는 것을 확인했습니다.

2차원 배열에서는 b[0] 내부에 있는 첫 번째 배열 요소인 b[0][0]의 주소도 동일합니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

여기서 진짜 같은 것은 b와 &b[0]입니다.

b는 원래 3칸 짜리 배열이기 때문에 그에 해당하는 괄호에 0을 넣은게 정확히 동일하다고 할 수 있습니다.

확인해봅시다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

잘 보이시나요?

    int (*p2)[3] = b;

이것만 따로 경고나 에러가 뜨지 않고 아직 사용되지 않았다는 경고만 나옵니다.

하... 어렵네요 진짜

에러나 경고가 안 뜨는 이유는 등호의 좌측과 우측의 형이 동일하다는 겁니다.

즉, int (*p2)[3]에서 변수 명인 p2를 제외한 int (*)[3]라는 '형'과 b의 형이 같습니다.

b는 &b[0]과 동일하고 b[0]에는 int [3] 짜리 배열이 들어있다고 했습니다.

그래서 b[0]의 주소 (b = &b[0])를 참조하기 위한 포인터는 int [3]짜리 배열을 가리켰던 배열 포인터와 동일한 자료형인 int (*)[3]으로 참조해야합니다.

으악.. 복잡쓰....

int (*)[3]은 또 어떤 의미를 가지고 있었을까요?

정답은 int 타입의 인덱스 (요소)를 3개 가지고 있는 배열을 가리키는 포인터입니다.

더 자세히 말하면 (*)는 * 옆에 붙은 문자를 포인터 변수로 정해주고, [3]은 포인터 연산에 대한 증감폭을 나타냅니다.

즉,

int (*p2)[3];은 p+i을 했을 때 [3]으로 선언됐기 때문에 주소값이 한번에 i * 3 (= [3])*4 (=int 자료형의 크기) = i * 12만큼 씩 변화합니다.

아래의 그림에서 확인해보세요.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

반면, 위에서 경고가 나왔던 int *p1 = b; 는 다음과 같이 (int *)로 강제 형변환 (casting)을 해주면 마찬가지로 아직 사용하지 않은 변수라는 경고만 뜨게됩니다.

//    int *p1 = b;

    int *p1 = (int *)b;

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

당연히 강제로 형을 맞춰줬기 때문에 동일한 형이 아니라도 문제가 발행하진 않는거죠.

이번에는 int 형 포인터기 때문에  포인터 연산 시 주소값이 4씩 변화하게 됩니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

따라서, 위의 두가지 포인터를 사용하는 것은 상당히 다릅니다.

잘 봐주세요.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

우선 강제 형변환을 통해 2차원 배열 b를 참조했던 포인터 p는 위와 같이 데이터가 출력됩니다.

즉, b[i][j] = *(p1+3*i+j)와 같습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

형변환을 하지않고 자료형을 맞춰줬던 포인터 p2는 위와 같이 데이터가 출력됩니다.

즉, b[i][j] = *(*(p2+i)+j)와 같습니다.

*(p2+i)가 b[i] 내부에 있는 int [3] 짜리 배열을 가리키고, 다시 *(p2+i) = P일 때 *(P+j)를 통해 int [3] 배열의 각 원소 [j]를 가리키는 것입니다.

그럼 굳이 왜 2가지를 비교했냐!!!

다시 복습해보면 *(p+n) = p[n]과 동일한 역할을 하는 것을 배웠습니다.

따라서 2차원 배열을 포인터를 통해 참조하는 것은 각각 다음과 같이 치환될 수 있습니다.

b[i][j] = *(p1+3*i+j) = p1[p1+3*i+j]

b[i][j] = *(*(p2+i)+j) = p2[i][j]

뭐가 더 직관적이신가요?

답은 아실거라 생각합니다.

2) 이중 포인터 (포인터의 포인터)

드디어 이중 포인터를 배울 차례네요.

1)번에서 2차 배열을 참조하기위한 포인터 형 중에 3번째 형을 기억하시나요?

    int *p1 = (int *)b;

    int (*p2)[3] = b;

    int **p3 = b;

위에 붉게 표시된 박스가 바로 이중 포인터를 선언한겁니다.

그냥 *를 두번 붙이면 됩니다.

이중 포인터는 **p와 같고 *p에 저장되어 있는 주소를 다시 바깥에 있는 *로 참조하는 포인터 입니다.

예시를 보시죠.

    int c = 10;

    int *pc = &c;

    int **ppc = &pc;

이렇게 선언하고 각각을 출력한 결과는 다음과 같습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

*pc : int 형 변수 c의 주소는 pc에 저장되어 있고, *을 통해 (= *pc) pc에 저장된 c의 주소에 할당된 메모리 내부의 데이터를 참조할 수 있습니다.

**ppc : int 형 포인터 변수 pc의 주소는 ppc에 저장되어 있고, *을 통해 (= *ppc) ppc에 저장된 pc의 주소에 할당된 메모리 내부의 데이터 (= c의 주소 = &c)를 참조할 수 있습니다. => 다시, *을 통해 (= **ppc) 앞에서 가져온 pc 내부에 저장된 c의 주소를 가리킬 수 있고, 그 주소에 할당된 메모리 내부의 데이터 (= c의 값)을 참조할 수 있습니다.

왜 이걸 2차원 배열과 묶어서 설명하는가?

바로 많은 분들이 2차원 배열과 이중 포인터가 연관이 많다고 생각하시기 때문입니다.

결론적으로 말하자면 서로 관련되게 잘 사용하지 않습니다.

일단 위의 코드는 경고가 발생한다고 했습니다.

당연히 오른쪽의 b는 &b[0]와 같고, 이 주소를 참조하려면 int (*) [4]와 같은 자료형이 필요해서 그렇습니다.

마찬가지로 b를 (int **)로 강제 형변환해주면 경고는 사라집니다.

확인해보시죠.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

사용되지 않은 변수라는 것만 알려줄 뿐 문제가 있다곤 하지 않습니다.

그럼 이중 포인터 변수 p3로 어떻게 2차원 배열 b를 표현할 수 있을까요?

일단 p3에는 b = &b[0] 가 저장되어 있을 것 입니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

그리고 p3+i는 따로 뒤에 [] 표시 없이 선언된 포인터이기 때문에 int 자료형의 크기인 4씩 포인터 연산이 이뤄질 것입니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

아니네요? 8 씩 변화했습니다.

8은 주소값의 크기와 동일합니다.

왜 주소값의 크기인 8 byte 씩 이중 포인터 변수 p3의 포인터 연산이 이뤄졌을까요?

사실 int **p3 = (int **) b = (int **) &b[0]과 같이 선언했기 때문에 p3의 자료형은 int **입니다.

int *p3 = (int *)b 였다면 int 자료형을 가리키는 포인터라서 4씩 포인터 연산이 됐겠지만,

int **p3 = (int **)b 이기 때문에 **이 되어 4씩 포인터 연산이 되는 포인터 자료형 (포인터 변수는 주소를 저장하기 때문에 크기가 8byte)의 크기인 8만큼 주소가 변화하게 됩니다.

이렇듯 본래 배열 b에서 행이 넘어갈 때 12, 열이 넘어갈 때 4 씩 포인터 연산이 이뤄지지 않고, 이중 포인터와 강제 형변환을 통해 2차원 배열을 받았을 때 포인터 연산이 8씩 이뤄져 적절히 2차원 배열 b를 참조할 수 없습니다....

사용하는 방법은 분명히 있습니다.

다른 게시물에서 혹시 여유가 된다면 포인터에 대한 내용을 추가로 설명해보겠습니다.

아마 거기서 malloc이나 calloc에 대해서도 자세히 알아볼 것 같아요

결론!!

1. 이중 포인터와 2차원 배열은 함께 사용하기 어렵다.

2. 이중 포인터의 사용은 다음과 같다.

이중 포인터는 **p와 같고 *p에 저장되어 있는 주소를 다시 바깥에 있는 *로 참조하는 포인터 입니다.

예시를 보시죠.

    int c = 10;

    int *pc = &c;

    int **ppc = &pc;

이렇게 선언하고 각각을 출력한 결과는 다음과 같습니다.

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

3. 2차원 배열을 포인터로 참조하기 위해선 강제 형변환을 사용한 것과 배열 포인터를 사용한 것 두가지가 있다.

1) 선언

  int b[4][3] = {

        {11, 12, 13},

        {21, 22, 23},

        {31, 32, 33},

        {41, 42, 43}

    };

    int *p1 = (int *)b; // 강제 형변환

    int (*p2)[3] = b;  // 배열 포인터

2) 사용

b[i][j] = *(p1+3*i+j) = p1[p1+3*i+j] // 강제 형변환

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

b[i][j] = *(*(p2+i)+j) = p2[i][j]        // 배열 포인터

C언어 2차원 배열 최단경로 - ceon-eo 2chawon baeyeol choedangyeonglo

==> 보다 직관적인 배열 포인터로 사용하는게 권장된다.