이차원 포인터, 포인터 배열, 배열 포인터 비교

이차원 포인터와 포인터 배열, 배열 포인터의 관계에 대한 포스팅을 진행하겠습니다.
이차원 포인터는 포인터의 포인터로 주소의 주소 값을 저장하는 역할을 합니다.

포인터 배열은 int *ptr[3]의 형식으로 연산자 우선 순위에 따라 배열이 먼저 선언되고 해당 배열 안의 값들이 int*인 포인터형인 배열입니다.
ptr+1은 4byte가 증가된 다음 배열 항목의 주소 값을 가리키게 됩니다.

배열 포인터는 int (*ptr)[3]의 형식으로 괄호에 의해 포인터가 먼저 선언되고 해당 포인터는 arr[][3]의 크기를 가지는 배열을 가리킬 수 있는 포인터입니다. ptr+1은 12byte가 증가된 다음 배열의 항목의 주소 값을 가리키게 됩니다.

포인터는 변수의 주소를 저장함으로써 저장된 주소 값을 기준으로 메모리에 할당된 배열에 대한 핸들링이 가능한 것 입니다.
주소 값을 통해 핸들링이 가능하다는 것이 이차원 포인터를 통해 이차원 배열을 핸들링 할 수 없는 이유입니다.

포인터 배열의 경우 배열에 포인터 자료형이 들어있는 형태로,

이차원 포인터를 통한 ptr, ptr+1, ptr+2의 주소는 포인터 배열의 다음 시작 주소와 정확하게 일치합니다.

하지만, 배열 포인터는 배열마다 int arr[2][3], int arr[3][4]의 크기로 선언되어 있을 경우  int (*ptr)[3], int (*ptr)[4]의 형태로 배열의 주소값을 대입해야 합니다.
전자의 경우, arr+1은 12byte가 증가하는 형태이고,
후자의 경우, arr+1은 16byte가 증가하는 형태이기 때문입니다.
배열 포인터도 다음 배열 요소의 시작 주소를 알아야 핸들링이 가능하다는 이야기입니다.

이차원 포인터의 경우도 마찬가지로 배열 포인트를 가리킬 때 강제형변환 시 시작 주소는 정확하게 일치하나,
배열의 열에 대한 크기를 알지 못하므로 다음 요소의 시작주소 값을 핸들링하지 못하고, 

단순히 size(int*)의 크기만 증가시킨 부분을 포인팅하여 원하는 값이 나오지 않는 것을 볼 수 있습니다.
결국 이차원 포인터로 배열 포인터를 핸들링 할 수 없기 때문에 int **ptr = (*arr)[3]이라는 선언에 오류가 발생하는 것입니다.

 

예제 및 테스트 결과

아래에 테스트를 진행한 코드와 설명 첨부하도록 하겠습니다.

// Variables and Array declaration
    int a=1, b=2, c=3;
    int arr[2][3] = {{1,2,3}, {4,5,6}};

 

// Assigning a variable to an array of pointer
// 포인터 배열에 미리 선언한 변수의 주소 값을 넣는 과정입니다.
// pointerArr1는 포인터 배열로 3개의 배열 안에 각각 1차원 포인터가 들어있는 형태입니다.
    int *pointerArr1[3] = {&a, &b, &c};
    pointerArr1[0] = &a;
    pointerArr1[1] = &b;
    pointerArr1[2] = &c;
    printf("%p:%d\n", pointerArr1[0], *pointerArr1[0]);
    printf("%p:%d\n", pointerArr1[1], *pointerArr1[1]);
    printf("%p:%d\n\n", pointerArr1[2], *pointerArr1[2]);

// Assigning an array of pointer to a two-dimensional pointer
// 포인터 배열의 주소를 이차원 포인터에 넣는 과정입니다.
// 대입 후 이차원 포인터는 포인터 배열의 시작 주소를 가리키게 됩니다. 
    int **doulePtr1 = NULL;
    doulePtr1 = pointerArr1;
    printf("%p:%d\n", doulePtr1[0], *doulePtr1[0]);
    printf("%p:%d\n", doulePtr1[1], *doulePtr1[1]);
    printf("%p:%d\n\n", doulePtr1[2], *doulePtr1[2]);

// Assigning an array to a one-dimensional pointer
// 선언한 2차원 배열을 일차원 포인터에 강제형변환을 하여 대입하였습니다.

// 배열은 2차원으로 선언되어 있지만 메모리에는 일차원의 형태로 존재하기 때문에 일차원 포인터를 통해 배열의 원소값에 접근이 가능합니다.
    int *singlePtr = NULL;
    singlePtr = (int*)arr;
    printf("%p:%d\n", singlePtr, *singlePtr);
    printf("%p:%d\n", singlePtr+1, *(singlePtr+1));
    printf("%p:%d\n", singlePtr+2, *(singlePtr+2));
    printf("%p:%d\n", singlePtr+3, *(singlePtr+3));
    printf("%p:%d\n", singlePtr+4, *(singlePtr+4));
    printf("%p:%d\n\n", singlePtr+5, *(singlePtr+5));

// Assigning an array to an array of pointer
// 선언한 2차원 배열을 포인터 배열이 가리키도록 하였습니다.
// 메모리에 배열은 1차원의 형태로 int형 6개가 존재합니다.

// 생성된 포인터 배열은 pointerArr2는 새로운 메모리 공간에 할당되고 각각 pointerArr2[0]은 arr[0]의 주소값, pointerArr2[1]은 arr[1]의 주소값을 가집니다.
    int *pointerArr2[2];
    pointerArr2[0] = arr[0];
    pointerArr2[1] = arr[1];
    printf("%p:%d\n", pointerArr2[0], *pointerArr2[0]);
    printf("%p:%d\n\n", pointerArr2[1], *pointerArr2[1]);

// Assigning an array to an pointer of array
// 선언한 2차원 배열을 배열 포인터가 가리키도록 하였습니다.
// 배열 포인터는 int [][3]의 형태의 배열을 정확하게 가리킬 수 있습니다.
    int (*arrPointer)[3];
    arrPointer = arr;
    printf("%p:%d\n", &arrPointer[0][0], arrPointer[0][0]);
    printf("%p:%d\n", &arrPointer[0][1], arrPointer[0][1]);
    printf("%p:%d\n", &arrPointer[0][2], arrPointer[0][2]);
    printf("%p:%d\n", &arrPointer[1][0], arrPointer[1][0]);
    printf("%p:%d\n", &arrPointer[1][1], arrPointer[1][1]);
    printf("%p:%d\n\n", &arrPointer[1][2], arrPointer[1][2]);

// 컴파일 오류 발생

// 반대로, 포인터 배열은 원소 하나의 사이즈가 포인터의 사이즈 값을 가져 이차원 포인터로 가리킬 경우 이차원 포인터의 사이즈형과 같기 때문에 포인팅이 가능하나, 배열 포인터의 경우 배열의 행의 크기를 알 수 없으므로 이차원 포인터로 포인팅을 할 수 없습니다.
     int **doulePtr2 = NULL;
    doulePtr2 = arrPointer;

 

// 강제형변환 후 출력
// 배열 포인터가 제대로 핸들링되지 않습니다.
    int **doulePtr2 = NULL;
    doulePtr2 = (int**)arrPointer;
    printf("%p:%d\n", &doulePtr2[0], doulePtr2[0]);
    printf("%p:%d\n\n", &doulePtr2[1], doulePtr2[1]);