배열 탐험 신비의 세계, 진실은 저 너머에 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