brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Aug 19. 2016

Advanced Pointer II

이제, 저 높은 곳을 향하여 I



배열과 포인터, 문자열, 동적 할당을 거쳐 이제 우리는 포인터의 정상에 오를 준비가 끝났습니다. 사실 포인터에 대한 기본적인 원리와 Sinclair's 정리를 모두 다 이해했다면 무지막지하게 어렵다고 알려진 공유 메모리semaphore 따위(?)도 결코 우리에겐 두려움이 될 수 없습니다.

모든 것이 메모리일 뿐이고 그곳에 접근 가능하게 해주는 녀석이 포인터이며 어떤 포인터건 배열처럼 사용할 수 있는 내공이 쌓여 있는 사람이라면 가히 강호의 고수라 칭함을 받을 수 있는 깜냥을 지닌 경지에 오르고 있는데 그것들이 무에 두렵겠습니까?

하지만 우리는 여전히 마음 한구석에 허전함을 느끼고 있어야 합니다. 바로 그 모든 허전함을 단번에 채워줄 내용들이 이곳에 있습니다.

아직 내공이 부족하거나 마음의 준비가 되어있지 않다면 그냥 바로 다음 글타래인 구조체로 넘어 가더라도 내용 전개상 아무런 문제도 없고 괜찮습니다.


여기에서는 제가 강의 중에, 개발 중에 보고 듣고 그리고 질문 받았던 수 많은 다양한 종류의 포인터와 포인터의 꽃이라 할 수 있는 void*와 함수의 포인터를 활용하는 법을 공부하도록 하겠습니다.



어느 날 열심히 프로그램을 작성하고 있는데 갑자기 논리적으로 아래와 같은 정수형 배열이 필요하다고 합니다. 이럴 때 우리는 어떻게 해야 할까요?




그냥 int matrix[4][5] 라고 선언하면 될까요? 그럼 필요 없는 부분들은 어쩔 겁니까? 안 쓰면 된다구요? 회사가 그렇게 잘 나가나요? 게다가 메모리는 막 남아돌죠? 그런데 지금은 이런 요철 묶음이 4개뿐이지만 실제로는 저런 요철 배열이 1000개 이상 필요하다면 어쩌겠습니까?


정말 안타까운 현실 중 하나는 대부분의 프로그래머들에게 문제를 해결하라고 하면 매번 하드웨어나 운영체제, 또는 프로그래밍 툴을 탓한다는 것입니다.


이제 막 입사한 신입 사원들에게 프로그램을 작성하라고 하면 가장 먼저 비주얼스튜D5가 있는 지부터 확인합니다. 그리고 없으면 대뜸 하는 말이 "어? 비주얼스튜D5가 없는데요?"입니다. 우주선을 날릴 때 인텔 펜T엄 모바일 센트리X 듀얼 코어의 CPU를 사용할까요? 천만에 말씀 만만에 콩떡입니다. 우주선은 날려 보내려고 만드는 게 절대 아닙니다. 반드시 다시 돌아 와야 하는데 계산상의 아주 작은 오차도 지구 밖에선 어마어마한 결과를 초래하게 되어 결국 우주의 미아로 남게 될 수도 있습니다. 그런데 펜티엄프로세서에는 아직도 알려지지 않은 버그가 많답니다. 그래서 거의 모든 버그가 다 알려진 낮은 사양의 프로세서를 사용하는 겁니다.

우리가 알다시피 16bit인 286프로세서의 최대 메모리는 2의16승byte 즉 64Kbyte 입니다. 온갖 편법을 쓰더라도 1Mbyte를 넘어가는 부분은 결국 중첩 확장이니 하는 따위의 기술을 사용해야 합니다. 자, 만약에 이런 열악한 상황이라면 어떻게 하겠습니까? 진정한 고수는 결코 도구를 탓하는 일이 없습니다.  



// 가장 간단한 방법으로 free() 함수도 필요 없음

int a[2] , b[1] , c[5] , d[3] ;

int * first[] = { a , b , c , d } ;

// 표준 C99부터 배열초기화에 수식을 사용할 수 있습니다.

// 신이여 정녕 이게 가능하단말입니까?

// 우리의 상상력은 늘 이렇게 부족했습니다.  



// 가장 일반적인 방법

int * second[4] ;

getMemory2(second , sizeof(int)*2) ;

getMemory2(second+1 , sizeof(int)*1) ;

getMemory2(second+2 , sizeof(int)*5) ;

getMemory2(second+3 , sizeof(int)*3) ;  



// for루프를 사용하여 간단하게

int * third[4] ;

int i ; // for index

int sizes[] = { 2 , 1 , 5 , 3 } ;

for( i = sizeof third / sizoe * third ; i > 0 ; )

    getMemory2(third+i , sizeof(int)*sizes[--i]) ;



// 이중 포인터를 사용하려면

int ** forth ;

int i ; // for index

char size ;

char sz[] = { 2 , 1 , 5 , 3 } ;

getMemroy2(&forth , sizeof(int*))*(size=sizeof(sz)/sizeof(*sz))) ;

for( i = size ; i > 0 ; )

    getMemory2(forth+i , sizeof(int) * sz[--i]) ;   



네 가지 방법 모두 정확하게 원하는 양만큼만 적당히 메모리 할당을 하고 사용할 수 있습니다. 각각 장단점이 있습니다. 경우에 따라서 적절히 잘 골라서 사용해야 합니다. 저는 여기에서 따로 메모리 할당에 실패했을 때에 대한 것을 처리하지 않았습니다. 그리고 여러분이 직접 테스트를 할 목적이라면 반드시 free() 함수도 잊지 말고 사용해야 합니다. 포인터의 산을 겨우 하나 넘었는데 그것보다 더 어렵고 복잡한 것들이 등장합니다. 아싸~ 이쯤 되면 슬슬 다음 글타래로 넘어가야지 마음을 먹은 사람들이 생겨나고 있을 겁니다. 하지만 포인터도 원리가 있었던 것처럼 이런 복잡한 것들도 동일한 원리가 존재합니다.


C프로그래밍을 하면서 제가 늘 고마운 것은 K&R께서 C언어를 자유롭고 심플하게 설계하셨다는 것입니다. 작은 한 가지가 되면 아무리 복잡하고 어렵게 꼬여있더라도 동일하게 동작합니다.  




int * (* something[4])[5] ; // 이놈은 배열입니다.

int * (** anything)[4][5] ; // 요놈은 포인터입니다.


이것들이 의미하는 바가 무엇일까요? 질문을 하면 항상 되돌아오는 질문이 정말 그런걸 써요? 라는 것입니다. 물론 저도 이것은 매우 중뿔나는 코드라고 생각되어 거의 안 쓰지만 간혹 사용하는 경우를 본적이 있습니다. 예전에는 이런 코드가 나올 때 마다 혀 깨물고 죽고 싶었습니다. 우리가 포인터와 메모리의 도(?)를 깨달았다면 이렇게 사용하지 않더라도 동일한 작업을 하는 더 쉬운 코드를 찾아 사용할 수 있어야 합니다.  



type * array[4] vs. type (* pointer)[4]


포인터와 배열이 섞여있는 다양한 종류의 타입의 기본 형이 되는 것이 바로 type * array[4] 와 type (* pointer)[4] 입니다. 두 가지를 기본으로 설명하겠습니다.

배열이 등장하면 선언 후엔 이미 메모리 할당이 끝난 상태이며 배열이름 앞 뒤에 &와 [0]가 있음을 기억해야 합니다.

포인터는 선언할 때는 타입이지만 수식에서는 배열 연산자와 바꿔 사용할 수 있으며 반드시 등장한 포인터의 개수만큼 초기화를 해야 한다는 것을 잊으면 절대 안됩니다. 이것이 바로 아무리 복잡한 선언이 나와도 변하지 않는 원칙입니다.  



type * array[4] ;

// 배열 연산자의 우선 순위가 가장 높으니 배열입니다.


초기화 : 포인터가 네 개니까 네 번 초기화

type a[3], b[50], c[7], b[100] ;

type * array[4] = { a, b, c, d } ; // 배열로 초기화


type * array[4] = { NULL } ; // 동적할당

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

    array[i] = (type*)malloc(sizeof(type)*m) ;

    //type array[4][m]; 와 같은 효과


사이즈 : sizeof array == sizeof(type*) * 4

// on 32bit OS: 16


함수의 인자로 넘길 때

function(array, 다른 인자들) ;

// array == &array[0] ∴ 타입은 type**가 된다


인자로 넘길 함수의 prototype 

type function(type ** , size_t , size_t) ;


array[0]?

type*, type*형으로 type형 데이터의 주소 값


할당된 메모리 공간의 데이터참조

*(array[i]+j) == array[i][j]


&array의 타입?

type * (* p)[4] ;     p = &array ;

// on 32bit OS: sizeof(p) == 4


특징

실제 많이 사용, 묶음은 일정하고 묶음 속 데이터는 가변일 때

즉, 행렬개념에서 행은 고정되어 있고 열이 가변일 때 사용



type (* pointer)[4] ;

// 포인터 연산자의 우선 순위가 가장 높으니 포인터입니다.                               


초기화 : 포인터가 한 개니까 한 번 초기화

type (* pointer)[4] = NULL ;

type (* pointer)[4] ;

pointer = (type(*)[4])malloc(sizeof(type[4])*n);

// type pointer[n][4] ; 와 같은 효과


사이즈 : sizeof pointer == sizeof(type*)

// on 32bit OS: 4


함수의 인자로 넘길 때

inputFunc(pointer, 다른 인자들) ;    // 참조

outputFunc(&pointer, 다른 인자들) ;    // 변경


인자로 넘길 함수의 prototypes

type inputFunc(type (*)[4] , size_t) ;

type outputFunc(type (**)[4] , size_t , size_t);


*pointer의 정체?

type[4], type형 데이터 4개 있는 묶음


할당된 메모리 공간의 데이터참조

(*(pointer+i))[j] == pointer[i][j]


&pointer의 타입?

type (** p)[4] ;     p = &pointer ;

// on 32bit OS: sizeof(p) == 4


특징

주로 이차원 배열을 함수의 인자로 넘길 때 인자 타입

행렬개념에서 행은 가변이고 열이 고정되어 있을 때 사용




물론 더 어렵고 복잡한 경우도 많이 있습니다. 질문을 받아보면 별 이상한 포인터도 있습니다. 하지만 그 어렵고 복잡하게 꼬아놓은 포인터도 결국 연산자에 불과하고 연산자는 우선순위에 따라 차례대로 처리하면 됩니다. 결코 어려운 것이 아닙니다. 저의 경우 포인터가 어려웠던 가장 큰 이유는 포인터가 특별하다고 생각했기 때문이었습니다. 덧셈 뺄셈처럼 익숙해지니 결코 어려운 것이 아니었습니다.








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

매거진의 이전글 Advanced Pointer II
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari