brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Feb 12. 2016

배열과 포인터: X-files 배열

배열 탐험 신비의 세계, 진실은 저 너머에 III



이차원 배열도 없다. 다차원 배열도 없다?!  



머 이렇게 없는 게 많아? 라고 생각해도 어쩔 수 없습니다.

없는 건 없는 겁니다.

이미 우리가 알고 있는 모양의 2차원 바둑판 배열이 C언어엔 존재 하지 않습니다.

사실 이것은 메모리에서만 그런 게 아니라 실제로도 그렇습니다.

앞서 우리는 배열이 연산자 임을 알았습니다.

여러분은 2 * 3 * 4를 한번에 계산합니까? 와우~ 대단합니다. 완전 천재입니다.

그럼 557 * 3917 * 7829 는 어떻습니까?

그저 평범한 저, Sinclair는 하나씩, 하나씩 계산합니다.

그런데 왜? 어째서? int matrix[3][4] ; 를 한번에 생각합니까?

혹시 천재인가요? (역시 깜냥이 대단하십니다. 부디 하늘이 내린 천재인 여러분들이 한국 IT발전을 위해 평생 C 프로그램을 짜주면 안되나요? 이상은 Sinclair의 투정 같은 넋두리였습니다.)


그러면 앞에 그 배열 선언을 이어받은 matrix[2][1]의 위치는 어떻게 한번에 찾아 갑니까?

똑같습니다. 전산학은 수학의 일부라고 했습니다. 하지만 그냥 보기에도 C언어는 수학 근처까지 가지도 않습니다. 초등학교 산수의 수준입니다. 하나가 되면 나머지 열 두 개도 같은 방법으로 가능합니다. 적어도 integral이 나와주시고 미분했다가 다시 적분도 하고 편 미분이니 exponential이니 하는 정도가 등장해줘야 수학이라고 할만 하지 않겠습니까?


자세히 보면 이차원 배열의 선언은 연산자 두 개가 연달아 나오고 있습니다.

이차원 배열을 시작으로 여러 가지 복잡하고 다양한 타입의 선언들에는 그 모든 것을 아우르는 한가지 원칙이 있습니다.

그것은 바로 가장 우선 순위가 높은 연산자를 제외한 나머지는 타입으로 생각하면 된다는 것 입니다.

그 타입에 존재하는 나머지 연산자(들)에 대해서는 다시 우선순위를 생각할 수 있습니다.


int matrix[3][4]라고 선언되었다면 두 개의 배열연산자 중 배열 이름 matrix에 가까운 연산자가 우선순위가 높고 그것을 제외한 나머지는 타입이 됩니다.


다시 말하면 int[4]의 타입으로 3개를 저장할 공간 sizeof(int[4]) * 3 bytes를 저장할 메모리 공간은 할당하라는 의미입니다.
그렇다면 int ** variable[5] ; 라고 선언이 되었다면 어떨까요?

포인터가 아무리 많아도 배열 연산자의 우선순위에 밀리게 되어 있습니다.

그러니 우선순위가 가장 높은 배열을 제외한 나머지 int**가 타입이 되고 sizeof(int**) * 5 bytes 만큼 메모리 할당하게 됩니다.  


잘못 그린 이차원 배열 int matrix[3][4] 메모리 그림                                



제대로 그린 이차원 배열 int matrix[3][4] 메모리 그림



실제 배열에 대한 선언의 메모리가 앞에 그림대로라면 matrix[2][1]을 어떻게 찾아야 할까요? 물론 바둑판 식 행렬 배열이라면 한번에 찾아갈 수 있습니다. 연산이 따로 필요하지 않습니다.

하지만 다시 한 번 말하면 배열은 연산자입니다.

여기엔 배열 연산자가 두 번 나왔으니 두 번의 연산을 통해 찾아 가면 됩니다.

두 번 연산을 하는 동안 수식으로써의 기본 배열 연산자의 의미가 변하지 않습니다.

배열 연산자는 피 연산자의 타입에 민감한 연산자들 중 하나입니다.

대입 연산자의 양쪽 두 피 연산자가 타입이 같아야 하는 것처럼 배열 연산자도 피 연산자들 중 반드시 하나는 타입이 있는 주소 값이고 나머지 하나는 정수형이어야만 합니다.

그것은 첫 번째 배열 연산을 할 때 만 적용되는 것이 아니라 두 번째 배열 연산도 동일하게 적용됩니다.

matrix와 가장 가까운 첫 번째 배열 연산자에서는 matrix(==&matrix[0])가 주소입니다.

그리고 두 번째 배열 연산자에서는 matrix[2](==&matrix[2][0])가 주소가 됩니다.


그렇다면 우리가 시작하는 기준을 바꾸게 되면 matrix[2][1]과 같은 결과를 갖는 matrix[1][5], matrix[0][9]도 가능해 집니다. 처음에 그린 바둑판 식 행렬 배열이라면 전혀 불가능한 일이지만 놀랍게도 다음 세가지 수식은 메모리의 같은 곳을 같은 사이즈만큼 참조하고 있습니다.



배열이 연산자이기 때문입니다. 일반 곱셈에서 3 * 4 와 4 * 3 도 같지만 2 * 6 과 1 * 12 도 같은 값을 갖습니다. 이것들은 이해되지만 배열은 이해가 안된다구요? 그것은 곱셈은 수없이 많은 반복을 거쳐 이해해왔지만 배열의 경우는 오늘 처음 해봤기 때문입니다. 안심하세요. 만약에 한 번에 이해가 된다면 그건 바로 정말 재수 없는 일 입니다. 저는 3년이나 걸린 것을 다른 누가 단 한 번 보고 이해한다면 제게 얼마나 큰 상처가 되겠습니까? 그래도 이제는 순수한 눈으로 마음을 새롭게 하고 자꾸 이렇게 생각하고 이해하려고 노력해야 합니다. 첫눈에 반해서 사랑에 빠지는 것이 가능하다고 믿습니다. 그렇지만 사랑하는 그 사람의 모든 것을 알려면 서로 이해하며 평생을 같이 보내야 합니다. 자꾸 코드로 사용하고 확인하다 보면 정도 들고 나중엔 저절로 이해가 되고 뭐, 다 그렇게 사는 겁니다. 혹시 나중에 딴소리 하는 거 아냐? 의심하는 사람들도 있겠지만 그런 걱정일랑 붙들어 메길 바랍니다. 10년이 넘도록 이것을 벗어나는 경우를 본적이 단 한번도 없습니다. 오히려 바둑판 식 행렬 배열이 매번 다른 설명으로 접근해야 합니다. 더욱이 안타깝게도 그 배열 방식은 이 모든 것을 완벽하게 설명해주지도 않습니다.



                                 

만약에 우리가 굳이 메모리에 선을 그려 넣어야 한다면 이렇게 그려야 합니다.




#include <stdio.h>  

/*

* copyleft ⓛ 2006 - 2017 programmed by Sinclair

*/   


main() {  

    int array[4] ;

    int matrix[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 } ;  

    typedef int intArr[4] ;  

    intArr ma ;         // 새로운 타입으로 만든 일차원 배열

    intArr mat[3] ;     // 새로운 타입으로 만든 이차원 배열  


    int * iPointer ;

    int ** iPPointer ;

    intArr * mPointer ;  


    int (* ptr)[4] = NULL ; // 우선 순위 상 *가 가장 높으니까 pointer

    int * arr[4] = {NULL} ; // 우선 순위 상 []가 가장 높으니까 배열  


    int i , j ; // for index  


    printf("%d %d %d %d %d %d %d\n", sizeof(int), sizeof(int[4]),

                          sizeof(array), sizeof(matrix),

                          sizeof(intArr), sizeof(ma), sizeof(mat)) ;

    // 4 16 16 48 16 16 48 : the result on 32bit OS  

    printf("%d %d %d %d %d\n", sizeof iPointer, sizeof iPPointer,
                            sizeof mPointer, sizeof ptr, sizeof arr) ;

    // 4 4 4 4 16 : the result on 32bit OS  

    iPointer = array ; // == iPointer = &array[0] ;  


    // array = iPointer ; // == &array[0] = iPointer

    // left operand must be l-value : the error message on VC 6.0

    // 대입 연산자는 피 연산자의 타입에 아주 민감하다고 했습니다.

    // 그리고 대입 연산자의 l-value가 될 수 없는 것이 있습니다.

    // 상수 값, 상수 변수,그리고 단항 연산자 &의 결과 값들 입니다.  


    iPointer = &array ; // 타입이 안 맞아서 warning 발생합니다.

    ptr = &array ;      // &가 붙으면 전체 배열이라고 했습니다.

                        // int[4]배열에 대한 주소 값입니다.  

    iPPointer = matrix ; // 타입이 안 맞아서 warning 발생합니다.  

    ptr = matrix ; // ptr = &matrix[0]

                   // matrix[0]방에 int[4]가 들어 있습니다.  

    mPointer = &ma ; // 당연한 거죠?

    mPointer = mat ; // mPointer = &mat[0]

    mPointer = matrix ;  

    ptr = mat ;  

    puts("***************************************************");

    printf("%u %u %u\n" ,     sizeof matrix , sizeof matrix[0] ,

                                            sizeof matrix[0][0]) ;  

    printf("%u %u : %u %u\n" ,     matrix , matrix + 1 ,

                                   &matrix , &matrix + 1) ;

    printf("%u %u : %u %u\n" ,     matrix[0] , matrix[0] + 1 ,

                                   &matrix[0] , &matrix[0] + 1) ;  

    puts("***************************************************");

    printf("%u %u %u\n"
                     , sizeof mat , sizeof mat[0], sizeof mat[0][0]) ;  

    printf("%u %u : %u %u\n" , mat , mat + 1 , &mat , &mat + 1) ;

    printf("%u %u : %u %u\n" ,
                      mat[0] , mat[0] + 1 , &mat[0] , &mat[0] + 1) ;  

   

 for( i = 0 ; i < 3 ; i++ )

        for( j = 0 ; j < 4 ; j++ )

            matrix[i][j] = 100 * ( i + j) ;  

    puts("***************************************************");

    for( i = 0 ; i < 12 ; i++ )

        printf("%u :%5d" , &matrix[0][i] , matrix[0][i]) ;

    putchar('\n') ;   

    for( i = 0 ; i < 12 ; i++ )  {  mat[0][i] = i ; }  

    puts("***************************************************");


    for( i = 0 ; i < 3 ; i++ ) {

        for( j = 0 ; j < 4 ; j++ ) {

            printf("%u :%5d" , &mat[i][j] , mat[i][j]) ;

        } // end inner for

        putchar('\n') ;

    } // end outer for  

    return 0 ;  

} //end main()    




결과를 보다시피 더 이상 차원이 의미 없습니다.

우리에겐 그저 모두 메모리일 뿐입니다.

3차원을 1차원처럼 또는 2차원처럼 아니면 1차원을 다차원 배열처럼 사용하는 것도 가능해집니다.

수식을 바꿨다고 결과 값이 달라지지는 않습니다.

오일러 방정식에 따라 e^{{i\pi }}+1결과 값도 0이지만 전화기에 있는 모든 숫자를 다 곱해도 0이고 알다시피 cos 90˚ 값도 0입니다. 이렇게 같은 거라면 왜 굳이 작정을 하고 이 어렵고 힘든 것을 설명하냐구요? 젊어서 고생은 사서도 한다지 않습니까?







하하하~ 때로 우리는 우리가 알고 있는 것으로부터 좀 더 자유로워질 필요가 있습니다. 적어도 생각만이라도 말입니다. 프로그래머에게 가장 무서운 적은 어쩌면 우리 안에 존재하고 있는 고정관념일지도 모르겠습니다. 우리가 생각의 폭을 조금만 넓히면 다른 세상을 발견할 수도 있습니다. 좀 더 많은 것을 보여드리고 싶은 마음이 굴뚝같지만 혹 여러분들의 상상력을 제한할까 두려워(?) 고이 접어 두도록 하겠습니다. 모두 함께 상상의 나래를 한 번 펼쳐보길 바랍니다.  

 




#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍  #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language

매거진의 이전글 배열과 포인터: X-files 배열
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari