본문 바로가기

awesome-c Beginner 번역/A tutorial on pointers

<비공식 번역>awesome-c Beginner 번역 : CHAPTER 9 : Pointers and Dynamic Allocation of Memory

현재위치



<주의!!>

'A TUTORIAL ON POINTERS AND ARRAYS IN C'의 공식적인 번역이 아니며 수를 받은 것 역시 아닙니다!!




CHAPTER 9: Pointers and Dynamic Allocation of Memory

(챕터 9: 포인터와 동적 메모리 할당)


여러분은 malloc(), calloc() 또는 다른 할당 함수를 사용하여 런타임에서 메모리 할당을 편리하게 한 경우가 있을 것입니다. 예를들어, 이 접근의 사용은 저장할 배열에 필요한 메모리 블록의 크기를 결정할 시기의 연기를 허용합니다. 또한 어떤 시간의 한 점에서 정수 배열의 저장을 위해 메모리 섹션 사용을 허가합니다. 그리고 그 메모리가 더 이상 필요하지 않게 되면 다른 유저들을 위해 할당이 해제될 수 있습니다.(번역자 : 미리 크기를 정하지 않아도 필요한 공간을 확보할 수 있다. 란 뜻입니다.)


메모리가 할당되면, 할당 함수(malloc(), calloc(), 등등) 포인터를 리턴합니다. 이 포인터의 타입은 여러분이 사용하는 예전 K&R 컴파일러식이나 요즘 ANSI 컴파일러식에 따라 결정됩니다. K&R 컴파일러를 따를 경우 리턴되는 포인터 타입은 char, ANSI 컴파일러를 따를 경우는 void 입니다.


만약 여러분이 예전 컴파일러(K&R)를 쓰고 있는데 정수 배열의 메모리 할당을 원한다면 여러분은 char 포인터를 integer 포인터로 캐스팅해야 할겁니다. 예를 들어 정수 10개의 공간할당을 봅시다. 이렇게 쓰일 것입니다:


int *iptr;

iptr = (int *)malloc(10 * sizeof(int));

if (iptr == NULL)

{ .. ERROR ROUTINE INE GOES HERE ..}


만약 여러분이 ANSI 컴파일러를 쓰고있다면 malloc()함수는 void 포인터를 리턴합니다. void 포인터는 다양한 객체 타입의 변수로 할당될 수 있습니다. 위의 예제처럼 (int *)로 캐스팅이 필요하지 않습니다. 배열 크기는 런타임에 결정될 수 있으며, 컴파일시에 필요하지 않습니다. 실행 시에 위의 10개 공간은 데이터 파일에서 읽어오거나 키보드로부터 입력될 수 있으며 필요에 따라 계산될 수 있습니다.


배열과 포인터 표기법의 동일성 때문에, 위에서 할당된 iptr은 배열 표기법을 사용할 수 있습니다. 예를 들어서 이렇게 써보겠습니다:


int k;

for (k=0; k<10; k++)

iptr[k] = 2;


모든 항목(배열의 칸)에 2를 넣었습니다.


심지어 포인터와 배열의 합리적이고 좋은 이해하고 있더라도, C 초보자가 처음으로 장애물을 만나는 곳은 다차원 배열의 동적할당입니다. 일반적으로, 그러한 배열요소는 가능하면 포인터 표기법이 아닌 배열 표기법을 사용하여 접근할 수 있었을 것입니다. 응용프로그램(application)에 따라 컴파일시에 크기를 알 수도 있고 모를 수도 있습니다. 이것은 우리의 작업을 다양한 방법으로 할 수 있도록 이끕니다.


우리가 봐 왔듯이, 동적 할당에서 1차원 배열의 크기는 실행할 때 결정될 수 있습니다. 이제, 더 높은 순서배열의 동적할당을 사용하는 경우, 우리는 컴파일시에 첫 번째 차원을 알 필요가 없습니다. 더 높은 차원을 알 필요가 있는지는 우리가 어떻게 코드를 작성하는지에 따라 다릅니다. 이제부터 정수 2차원 배열의 동적할당에 대한 다양한 방법을 이야기해봅시다.


먼저 우리는 컴파일할 때 2차원 배열이 알려진 경우를 고려할 것입니다.


방법 1:


이 문제를 다루는 한 방법은 typedef의 사용입니다. 2차원 정수 배열 할당을 위해 다음 두 표기법으로 재호출합니다. 같은 객체 코드의 결과를 만들어 냅니다.


multi[row][col] = 1;        *(*multi + row) + col) = 1;


다음에 나올 두가지 표기법도 동일한 코드를 생성합니다.


multi[row]                *(multi + row)


오른쪽의 포인터로 평가하기 때문에, 왼쪽의 배열 표기법도 포인터로 평가해야 합니다. multi[0]은 첫 번째 행의 첫 항목의 포인터를 리턴할 것입니다. multi[1]은 두 번째 행의 첫 항목의 포인터입니다. 실제로, multi[n]은 2차원 배열의 n 번째 행의 첫 항목의 포인터입니다. multi는 배열의 배열로 multi[n]은 배열의 배열인 multi의 n 번째 행의 포인터로 간주될 수 있습니다. 여기서 포인터란 단어는 주소값을 나타내는 단어로 쓰입니다. 이러한 쓰임은 문헌에서 공통이지만, 그러한 구문을 읽을 때는 배열의 상수 주소와 데이터 객체의 변수 포인터를 구분하는데에 주의를 기울여야 합니다.


이것을 생각해봅시다:

--------------- Program 9.1 --------------------------------


/* Program 9.1 from PTRTUT10.HTM  6/13/97 */


#include <stdio.h>

#include <stdlib.h>


#define COLS 5


typedef int RowArray[COLS];

RowArray *rptr;


int main(void)

{

    int nrows = 10;

    int row, col;

    rptr = malloc(nrows * COLS * sizeof(int));

    for (row = 0; row < nrows; row++)

    {

        for (col = 0; col < COLS; col++)

        {

            rptr[row][col] = 17;

        }

    }


    return 0;

}

------------- End of Prog. 9.1 -------------------------------


여기에선 ANSI 컴파일러를 썼다고 가정하겠습니다. 그래서 malloc()이 리턴한 void 포인터에 캐스팅이 필요하지 않습니다. 만약 여러분이 K&R 컴파일러를 사용한다면 여러분은 캐스팅이 필요할 것입니다.


rptr = (RowArray *)malloc(.... etc.


이러한 접근을 통해 rptr은 배열의 모든 특성을 가지고(rptr의 수정은 제외), 배열 표기법으로 프로그램 전반에 걸쳐 사용할 수 있을 것입니다. 만일 배열의 내용을 수정하는 기능을 작성한다면 예전에 토의했었던 함수에 2차원 배열 전달하기 처럼 공식 인자의 일부인 COLS를 사용해야함을 의미합니다.


방법 2:


위의 방법1에서 rptr은 "정수 열(COLS)의 1차원 배열"의 포인터로 밝혀졌습니다. 만약 이렇게 쓴다면 typedef 없이도 이 타입을 사용할 수 있음도 알려졌습니다:


int (*xptr)[COLS];


변수 xptr은 방법1의 변수 rptr과 동일한 특성을 가질 것이고 typedef 사용은 필요하지 않습니다. xptr은 정수 배열의 포인터이고 #define COLS에 의해 주어진 배열의 크기입니다. 괄호 배치는 배열 표기법이 상위 포인터 표기법에서 우선순위를 만듭니다,즉, 이렇게 쓴다는거죠:


int *xptr [COLS];


우리는 xptr을 #define COLS에 의해 동일한 숫자의 포인터를 가진 포인터의 배열로서 정의할 수 있습니다. 즉, 전혀 같은 것이 아닙니다. 그러나 포인터 배열은 다음 두 방법에서 알 수 있듯이 2차원 배열의 동적 할당에서 쓰임이 있습니다.

(번역자 : typedef을 사용하지 않고 직접 int**와 같은 형식으로 사용한 방법을 말하는 것 같습니다. 방법 4쪽의 예제코드를 보시면 그러한 방식으로 char** 를 사용했습니다.)


방법 3:


컴파일할 때 각 행의 항목의 개수를 모른다고 가정해봅시다. 즉, 행과 열의 개수가 실행할 때 결정되는 겁니다. 이를 수행하기 위한 방법은 int 타입의 포인터 배열을 만들고 각 행의 공간을 할당하고 각 행을 가리키는 포인터를 둡니다. 아래를 봅시다:


-------------- Program 9.2 ------------------------------------


/* Program 9.2 from PTRTUT10.HTM   6/13/97 */


#include <stdio.h>

#include <stdlib.h>


int main(void)

{

    int nrows = 5;     /* Both nrows and ncols could be evaluated */

    int ncols = 10;    /* or read in at run time */

    int row;

    int **rowptr;

    rowptr = malloc(nrows * sizeof(int *));

    if (rowptr == NULL)

    {

        puts("\nFailure to allocate room for row pointers.\n");

        exit(0);

    }


    printf("\n\n\nIndex   Pointer(hex)   Pointer(dec)   Diff.(dec)");


    for (row = 0; row < nrows; row++)

    {

        rowptr[row] = malloc(ncols * sizeof(int));

        if (rowptr[row] == NULL)

        {

            printf("\nFailure to allocate for row[%d]\n",row);

            exit(0);

        }

        printf("\n%d         %p         %d", row, rowptr[row], rowptr[row]);

        if (row > 0)

        printf("              %d",(int)(rowptr[row] - rowptr[row-1]));

    }


    return 0;

}


--------------- End 9.2 ------------------------------------


위 코드의 rowptrint 타입의 포인터의 포인터입니다. 이 경우에 int 타입 포인터 배열의 첫 번째 항목을 가리킵니다. malloc()을 호출한 수를 고려해봅시다:


To get the array of pointers           1    call

To get space for the rows              5    calls

------

Total                      6    calls


만약 여러분이 이러한 접근방식을 선택한다면 배열 표기법을 통해서 배열의 개별요소에 접근할 수 있지만(예를 들어 rowptr[row][col] = 17;) 그것이 "2차원 배열"의 데이터가 메모리에 연속적으로 존재함을 의미하진 않습니다.


그러나 여러분은 배열 표기법을 사용하여 메모리 블럭이 연속적인 것같이 사용할 수 있습니다. 예를 들어 이렇게 쓸 수 있겠죠:


rowptr[row][col] = 176;


만약 rowptr이 컴파일할 때 만들어진 2차원 배열의 이름이었다면, 당연히 컴파일할 때 만들어진 배열로서 rowcol은 여러분이 만든 배열의 경계 안에 있어야 합니다.


만약 여러분이 배열저장 전용으로 연속적인 메모리 블록을 원한다면 다음과 같이 따라하면 됩니다:


방법 4:


이 방법을 통해 우리는 먼저 배열 전체를 보유하는 메모리 블록을 할당합니다. 그리고 각 행을 가리키는 포인터 배열을 만듭니다. 따라서 비록 포인터 배열을 사용할지라도 메모리상은 연속적으로 존재합니다. 코드는 다음과 같습니다:


----------------- Program 9.3 -----------------------------------


/* Program 9.3 from PTRTUT10.HTM   6/13/97 */


#include <stdio.h>

#include <stdlib.h>


int main(void)

{

    int **rptr;

    int *aptr;

    int *testptr;

    int k;

    int nrows = 5;     /* Both nrows and ncols could be evaluated */

    int ncols = 8;    /* or read in at run time */

    int row, col;


    /* we now allocate the memory for the array */


    aptr = malloc(nrows * ncols * sizeof(int));

    if (aptr == NULL)

    {

        puts("\nFailure to allocate room for the array");

        exit(0);

    }


    /* next we allocate room for the pointers to the rows */


    rptr = malloc(nrows * sizeof(int *));

    if (rptr == NULL)

    {

        puts("\nFailure to allocate room for pointers");

        exit(0);

    }


    /* and now we 'point' the pointers */


    for (k = 0; k < nrows; k++)

    {

        rptr[k] = aptr + (k * ncols);

    }


    /* Now we illustrate how the row pointers are incremented */

    printf("\n\nIllustrating how row pointers are incremented");

    printf("\n\nIndex   Pointer(hex)  Diff.(dec)");


    for (row = 0; row < nrows; row++)

    {

        printf("\n%d         %p", row, rptr[row]);

        if (row > 0)

        printf("              %d",(rptr[row] - rptr[row-1]));

    }

    printf("\n\nAnd now we print out the array\n");

    for (row = 0; row < nrows; row++)

    {

        for (col = 0; col < ncols; col++)

        {

            rptr[row][col] = row + col;

            printf("%d ", rptr[row][col]);

        }

        putchar('\n');

    }


    puts("\n");


    /* and here we illustrate that we are, in fact, dealing with

       a 2 dimensional array in a contiguous block of memory. */

    printf("And now we demonstrate that they are contiguous in memory\n");


    testptr = aptr;

    for (row = 0; row < nrows; row++)

    {

        for (col = 0; col < ncols; col++)

        {

            printf("%d ", *(testptr++));

        }

        putchar('\n');

    }


    return 0;

}


------------- End Program 9.3 -----------------


malloc()을 호출한 부분을 다시 봅시다.


각 malloc() 호출마다 추가적인 공간 오버 헤드가 생깁니다. malloc()은 보통 OS가 형성한, 블록의 사이즈에 관한 데이터를 포함하는, linked list에의해 구현되었기 때문입니다. 그러나 더 중요한 것은 큰 배열을(가끔 100개의 행) 해제할 필요가 있을 경우 시간이 더 복잡할 수 있습니다. 이것은 데이터 블록 인접과 memset()함수를 사용하여 0으로 초기화를 허용한 것과 결합하여 선호했던 두 번째 대안으로 만든것 같이 보입니다.


다차원 배열인 마지막 예제에서는 우리는 3차원 배열의 동적 할당을 설명할 것입니다. 이 예제는 할당 종류의 동작이 일어날 때 볼 수 있는 것들을 설명합니다. 위에서 언급한 것 중 두 번째 대체 접근방법을 사용할 것입니다. 다음 코드를 봅시다:


------------------- Program 9.4 -------------------------------------


/* Program 9.4 from PTRTUT10.HTM   6/13/97 */


#include <stdio.h>

#include <stdlib.h>

#include <stddef.h>


int X_DIM=16;

int Y_DIM=5;

int Z_DIM=3;


int main(void)

{

    char *space;

    char ***Arr3D;

    int y, z;

    ptrdiff_t diff;


    /* first we set aside space for the array itself */


    space = malloc(X_DIM * Y_DIM * Z_DIM * sizeof(char));


    /* next we allocate space of an array of pointers, each

       to eventually point to the first element of a

       2 dimensional array of pointers to pointers */


    Arr3D = malloc(Z_DIM * sizeof(char **));


    /* and for each of these we assign a pointer to a newly

       allocated array of pointers to a row */


    for (z = 0; z < Z_DIM; z++)

    {

        Arr3D[z] = malloc(Y_DIM * sizeof(char *));


        /* and for each space in this array we put a pointer to

           the first element of each row in the array space

           originally allocated */


        for (y = 0; y < Y_DIM; y++)

        {

            Arr3D[z][y] = space + (z*(X_DIM * Y_DIM) + y*X_DIM);

        }

    }


    /* And, now we check each address in our 3D array to see if

       the indexing of the Arr3d pointer leads through in a

       continuous manner */


    for (z = 0; z < Z_DIM; z++)

    {

        printf("Location of array %d is %p\n", z, *Arr3D[z]);

        for ( y = 0; y < Y_DIM; y++)

        {

            printf("  Array %d and Row %d starts at %p", z, y, Arr3D[z][y]);

            diff = Arr3D[z][y] - space;

            printf("    diff = %d  ",diff);

            printf(" z = %d  y = %d\n", z, y);

        }

    }

    return 0;

}


------------------- End of Prog. 9.4 ----------------------------


만일 여러분이 지금까지의 튜토리얼을 따라왔다면 위의 프로그램을 혼자 이해하는데 무리가 없을 것입니다. 그러나 만들어야만 하는 점이 몇가지 있습니다. 이 라인을 읽어봅시다:


Arr3D[z][y] = space + (z*X_DIM * Y_DIM) + y*X_DIM);


여기는 문자 포인터와 동일한 타입 Arr3D[z][y]의 공간인 것을 기억합시다. (z*(X_DIM * Y_DIM) + y*X_DIM)의 연산으로 얻어진 포인터인 정수를 추가할 때, 그 결과는 새로운 포인터 값임이 중요합니다. 그리고 포인터 변수에 할당한 포인터 값은 값과 변수의 데이터 타입이 일치해야 합니다.


출처1 : https://github.com/aleksandar-todorovic/awesome-c

출처2 : http://home.netcom.com/~tjensen/ptr/pointers.htm