ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C Language] 37. 2차원 배열과 포인터 - C 언어
    CSE/C Language 2015. 8. 4. 15:00

    1. 대상체의 개념

     다음은 2행 3열짜리 tmp 배열을 정의한 것이다.


      

    1
    2
    int tmp[2][3];
     
    cs



     int 형을 저장할 수 있는 방이 6(2X3)개가 생성되었다. 2 차원 배열을 정의하고 사용하는 것은 쉬워 보이나 대상체의 개념이 적용되면서 2 차원 배열은 생각보다 쉽게 정복되지 않는다.



     아래 예제를 통해 2 차원 배열의 다양한 형태를 확인해 보도록 하자.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    /*
     * pointer.c
     *
     *  Created on: 2015. 7. 21.
     *      Author: Yeonsu
     */
     
    #include <stdio.h>
     
    #define PRINT_D(x) printf(#x" = %d\n", x)
    #define PRINT_P(x) printf(#x" = %#X\n", x)
     
    int main(void) {
     
        int tmp[2][3= { { 7915 }, { 217911 } };
     
        puts("<== (tmp) ==>");
        PRINT_P(tmp);
        PRINT_P(tmp + 1);
        PRINT_D(sizeof(tmp));
        PRINT_D(sizeof(tmp + 1));
        PRINT_P(*tmp);
        PRINT_P(&tmp);
        PRINT_D(sizeof(*tmp));
        PRINT_D(sizeof(&tmp));
     
        puts("");
        puts("<== (tmp[0]) ==>");
        PRINT_P(tmp[0]);
        PRINT_P(&tmp[0]);
        PRINT_P(tmp[0+ 1);
        PRINT_D(sizeof(tmp[0]));
        PRINT_D(sizeof(tmp[0+ 1));
        PRINT_D(*tmp[0]);
        PRINT_P(&tmp[0]);
        PRINT_D(sizeof(*tmp[0]));
        PRINT_D(sizeof(&tmp[0]));
     
        puts("");
        puts("<== (tmp[0][0]) ==>");
        PRINT_D(tmp[0][0]);
        PRINT_D(tmp[0][0+ 1);
        PRINT_P(&tmp[0][0]);
        PRINT_P(&tmp[0][0+ 1);
        PRINT_D(sizeof(tmp[0][0]));
        PRINT_D(sizeof(tmp[0][0+ 1));
        PRINT_D(sizeof(&tmp[0][0]));
     
     
        return 0;
    }
     
     
    cs









     Line 18: tmp가 저장된 곳의 주소를 출력한다. 그런데 tmp는 메모리에 저장된 값이 아니다. tmp는 배열명이므로 주소 값 자체이고 메모리 할당을 받지 않는다.


     Line 19: tmp + 0은 첫 번째 자식의 주소 값을 의미하므로 0x28FF18가 출력되고, tmp + 1은 두 번째 자식의 주소 값이므로 0x28FF24가 출력된다.


     tmp 에게는 tmp[0]와 tmp[1]이라는 자식이 있다. 이를 다른 말로 표현하면 'tmp의 관리대상은 tmp[0]와 tmp[1]이다'라고 말할 수 있다. 첫 번째 관리대상인 tmp[0]의 주소는 tmp + 0 이며 두 번째 자식의 주소는 tmp + 1로 표현된다.


     Line 20: tmp가 관리하는 대상은 tmp[0]과 tmp[1]이라는 자식이다. 그런데  그 자식 하나는 12바이트로 구성되어 있으므로 24가 출력된다.


     Line 21: tmp + 1은 대상체의 개념이 전혀 없는 주소 값이므로 크기는 4가 출력된다. 대상체의 개념이 있는 모든 변수에 정수를 더하면 그것은 대상체의 개념이 사라진 단순한 주소 값이라는 것을 기억해 두자.


     Line 22: 어떠한 포인터 변수에 *를 사용하면 포인터 변수가 가리키는 대상체가 된다. 그것이 모(母) 배열이라면 대상첸느 하위 배열인 부분배열이 되고 가장 하위의 부분배열일 경우 특정한 값을 뜻한다. 여기서 tmp는 모배열이므로 *tmp를 사용하면 tmp의 부분배열 중에서 첫 번째 부분배열을 지칭하므로 위와 같은 주소 값이 출력된다.


     Line 23: 조금 전에 확인해 본 것처럼 tmp는 메모리에 할당된 변수가 아니기 때문에 tmp와 &tmp의 값은 같다.


     Line 24: *tmp는 tmp 가 관리하는 두 자식 중에서 첫 번째 자식을 말한다. 여기서는 tmp[0]이므로 tmp[0]의 크기를 출력할 것이다. tmp[0]은 tmp[0][0], tmp[0][1], tmp[0][2]를 자식으로 가지기 때문에 12가 출력된다. 


     Line 29: tmp[0]이 0x28ff18이라는 주소 값을 가질 때 &tmp[0]의 주소 값은 무엇일까? tmp[0][0], tmp[0][1],... 등은 메모리를 할당받지만 tmp, tmp[0], tmp[1] 등은 포인터 자체를 나타낼 뿐 메모리를 할당 받는 것이 아니다. tmp[0]과 &tmp[0]의 주소 값은 같다.


     Line 31: 일단 tmp[0]이라는 주소 값에 정수를 더했으므로 결과는 주소 값이란 것을 알 수 있다. 0x28ff1c라는 주소는 무엇을 가리키고 있을까? tmp[0]은 tmp[0][0], tmp[0][1], tmp[0][2] 이라는 자식들을 가지고 있다. 이 중에서 '+1' 만큼 떨어진 자식을 뜻하므로 여기서는 tmp[0][1]이 된다. 


     Line 34: tmp[0]은 세 명의 자식을 거느린 부모이다. *tmp[0]은 이 중에서 첫 번째 자식을 지칭한다. 두 번째 자식은 *(tmp[0] + 1)로 표현할 수 있다. 결국 첫 번째 자식은 *(tmp[0] + 0)과 같다는 것을 알 수 있다. 문법적으로 말하면 *(tmp[0] + 0), *(tmp[0] + 1), *(tmp[0] + 2)는 tmp[0]이 가리키고 있는 곳에서 0번째, 1번째, 2번째 떨어진 요소이다라고 말할 수 있다. tmp 라는 조상은 tmp[0], tmp[1]이라는 부모들을 관리대상으로 삼았기 때문에 tmp에 정수를 더하면 부모 사이를 움직이고, tmp[0]이나 tmp[1]은 각각 세 명의 자식들을 관리하기 때문에 tmp[0]이나 tmp[1]에 정수를 더하면 자식 사이를 움직이는 것이다.


     Line 36: '*는 []와 완전히 일치한다'라는 개념을 떠올려보자. 그러면 앞의 연산은 tmp[0][0]이 된다. *tmp[0]은 정수 값을 뜻하며 int 형이기 때문에 4가 출력된다.



     위를 정리하면 아래와 같다.



     

     int tmp[2][3]; 일때


      

      1. tmp는 tmp[0], tmp[1]을 대상으로 하며 그 하위의 존재를 알지 못한다.

      2. tmp[0]은 tmp[0][0], tmp[0][1], tmp[0][2]를 대상으로 한다.

      3. tmp[1]은 tmp[1][0], tmp[1][1], tmp[1][2]를 대상으로 한다. 

      4. tmp, tmp[0], tmp[1]은 메모리 할당을 받지 않기 때문에 

        tmp == &tmp

        tmp[0] == &tmp[0]

        tmp[1] == &tmp[1]

         의 관계가 성립한다.

      5. *tmp == *(tmp + 0) == tmp[0]이 성립한다.

      6. *(tmp + 1) == tmp[1]이 성립한다. 







    2. 2차원 배열 포인터 정의와 초기화

     1차원 배열이 있고 이를 포인터에 접목하기 위해서 1차원 포인터 변수를 사용했다. 포인터 변수는 1차원 배열을 가리키기 때문에 포인터 변수를 사용하여 1차원 배열의 모든 것을 다룰 수가 있었다. 마찬가지로 2차원 배열을 다루기 위해서는 2차원 배열 포인터가 마련되어 있다. 2차원 배열 포인터를 정의하는 방법을 1차원과 비교해서 살펴보자.



     1차원 배열 포인터 정의와 할당


     

    1
    2
    3
    4
    int  tmp[10= {12345678910};
    int *tmp_p;
     
    tmp_p = tmp;
    cs


     

      tmp라는 배열명을 가볍게 tmp_p에 할당해 주면 tmp_p에 tmp의 주소 값이 할당되고 tmp_p를 이용하여 tmp를 다루게 된다. 

      tmp_p는 주소 값을 저장할 수 있는 포인터 변수이며 tmp[0]이 저장된 주소 값을 가리키게 된다.




     2차원 배열 포인터 정의와 할당


    1
    2
    3
    4
    5
    int tmp[2][3= {{123}, {456}};
    int (*tmp_p)[3];
     
    tmp_p = tmp;
     
    cs



      tmp 는 2차원 배열이다. 2차원 배열을 2차원 포인터 변수에 할당하는 것은 1차원과 차이가 없다. 단지 변수를 정의할 때만 차이가 나난다. 초보자는 (*tmp_p)[3]이라는 정의에 심한 거부감을 느낄 것이다. 일반적인 프로그램에서 거의 사용되지 않으므로 초보자들에게는 다행이라 하겠다.


     일단 정의된 표현을 하나씩 살펴보자. 가장 눈에 거슬리는 것은 괄호인데, 이것을 제거하면 *tmp_p[3]이 된다. (*tmp_p)[3]과 *tmp_p[3]은 의미에서 하늘과 땅만큼의 차이가 나기 때문에 다음 단원에서 다룰 것이다. 


     (*tmp_p)[3]이라고 정의하면 이것은 한행이 3열로 구성된 2차원 배열을 다루겠다는 의미이다. 즉, 가르키는 대상이 2행이든 5행이든 100행이든 한 행이 3열로 구성되어 잇다면 할당에는 문제가 발생하지 않는다는 말이다. 그러므로 (*tmp_p)[3]에서의 3은 행이 아니라 열에 대한 첨자가 된다.



     아래 예제를 통해 2차원 포인터 변수를 사용한 2차원 배열요소 다루기 예제를 살펴보자.






    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    /*
     * poit.c
     *
     *  Created on: 2015. 8. 4.
     *      Author: Yeonsu
     */
     
    #include <stdio.h>
     
    int main(void) {
     
        int i, j;
        int tmp[3][2= { { 12 }, { 34 }, { 56 } };
        int (*tmp_p)[2= tmp;
     
        puts("use dimension");
        for (i = 0; i < 3; i++)
            for (j = 0; j < 2; j++)
                printf("tmp[%d][%d] = %d\n", i, j, tmp[i][j]);
     
        puts("");
        puts("use pointer");
        for (i = 0; i < 3; i++)
            for (j = 0; j < 2; j++)
                printf("tmp[%d][%d] = %d\n", i, j, *(*(tmp_p + i) + j));
     
        return 0;
    }
     
    cs









    3. 왜 대상체가 중요한가?

     대상체는 우리가 값을 꺼내 오려는 대상을 일컫는 말이다. 그 대상은 특정한 값일 수도 있지만 주소 값일 수도 있다. 꺼내 오려는 대상을 정확히 알지 못하면 원하는 결과 값이 나오지 않았을 때 그 이유를 정확히 파악하지 못하고, 포인터 변수에 *, 괄호 등을 수 없이 이리저리 이동하면서 결과 값을 얻어 내려고 진땀을 뺀다. 우연히 재수가 좋아서 몇 번 *를 여기저기 붙여 보다가 원하는 결과 값을 얻었다고 하더라도 그 건 우연일 뿐 실력이 아니기 때문에 나중에 똑같은 경우를 만나면 마찬가지로 많은 시간을 소비해야 결과 값을 얻을 수 있다는 안타까운 결론이 나온다.


     대상체를 모르는 순간 원하는 값을 얻을 수 없다.


     

     대상체를 모르면 정말 원하는 값을 얻을 수 없는지 아래 예제를 통해 살펴보자.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
     
    int main(void) {
     
        int tmp[2][3= {{654}, {321}};
        int (*tmp_p)[3];
     
        tmp_p = tmp;
     
        ...
    }
     
    cs

     



     1. 포인터 변수(tmp_p)를 사용하여 tmp 배열에 있는 모든 값을 출력하라.

     2. 포인터 변수(tmp_p)를 사용하여 각 행의 합을 구하라.

     3. 포인터 변수(tmp_p)를 사용하여 4와 2를 출력하라.



    단, 포인터 변수를 사용할 때 배열 방식의 표현이 아닌 별표(*) 단항 연산자를 사용해야 한다.



     tmp_p는 2차원 배열을 가리키고 있다. tmp_p로 tmp의 모든 요소를 다룰 수 있게 된 것이다. 하지만 tmp_p, &tmp_p, tmp_p[0], *tmp_p, ... 등을 모르면 tmp의 모든 요소를 자유자재로 다루기란 사실상 어렵다. 특히 대상체를 모르면 그때 그때의 상황에 따라서 주먹구구식으로 프로그램을 진행하는데 그야말로 고역이 아닐 수 없다.


     예를 들어 보자.


     tmp_p = tmp를 사용해서 2차원 배열인 tmp를 tmp_p가 가리키도록 했다. 첫 번째 배열 요소인 6을 얻기 위해서는 tmp_p는 어떻게 표현해야 할까?


     1. tmp_p

     2. *tmp_p

     3. **tmp_p







     

    1번 tmp_p는 배열전체를 대상체로 하기 때문에 답이 아니며 *tmp_p는 하나의 행을 대상체로 하기 때문에 역시 답이 될 수 없다.

    **tmp_p는 tmp[0][0], tmp_p[0][0]과 같은 표현이므로 하나의 배열요소를 가리키고 있고 6을 뜻한다. 정확히 표현하면 tmp_p가 가르키고 있는 곳에서 0번째 행 0번째 열의 위치로 이동한 후 그곳의 값을 가리키는 것이다. 0번째 행 0번째 열의 요소는 6이므로 3번이 정답이다.



    그렇다면 두 번째 배열요소는 **tmp_p + 1 이라고 표현해야 할까? **tmp_p는 6이라는 값이며 6 + 1은 7이 되므로 두 번째 배열의 개념과는 거리가 멀다. tmp_p + 1 은 행 단위로 동작하기 때문에 답이 될수 없으며, 행에서 배열요소 단위로 움직이는 것이 있어야 두 번째 배열요소를 구할 수 있다. *tmp_p + 1 가 이러한 표현에 해당된다. 하지만 *tmp_p는 첫 번째 행을 뜻하며 주소 값이므로 '+1'을 하면 역시 주소 값이 된다. 그러므로 *가 하나 더 있어야 배열의 요소를 구할 수 있다. *tmp_p + 1이 주소 값이므로 *(*tmp_p + 1)은 그 주소 값을 가리키는 영역의 값이므로 5가 된다. '주소 값 + 주소 값' 역시 주소 값이라는 것을 기억하고 있다면 어렵지 않게 이해할 수 있을 것이다. 


    대상체의 개념이 중요하게 쓰이는 것 중의 하나가 함수의 인자로 넘겨질 때인데, 나중에 이에 대해 자세히 배울 것이다. 여기서는 맛보기로만 살펴보자.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
     
    /*
     * poit.c
     *
     *  Created on: 2015. 8. 4.
     *      Author: Yeonsu
     */
     
    #include <stdio.h>
     
    int add(int *int);
     
    int main(void) {
     
        int result;
     
        int tmp[2][3= {{123}, {456}};
     
        result = add(tmp[0], 3);
        printf("첫 번째 행의 합계: %d\n", result);
     
        result = add(tmp[1], 3);
        printf("두 번째 행의 합계: %d\n", result);
     
     
        return 0;
    }
     
    int add(int *data, int length) {
        int i;
        int sum = 0;
     
        for (i = 0; i < length; i++)
            sum += *data++;
     
        return sum;
    }
     
    cs











    댓글

Designed by Tistory.