brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Feb 13. 2016

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

늬들이 포인터를 알아? 진실은 저 넘어에 VI




우리가 포인터를 사용할 때 가장 어려운 것은 바로 포인터 연산의 의미를 파악하지 못해 그 수식의 결과를 알기 어렵다는 것입니다.


어찌하다 보니 초기화를 끝내고 어찌하다 보니 포인터를 사용하게 되었는데, 갑자기 *pointer도 나오고 **pointer도 나오고 그러다 막 ***pointer도 나오고 때론 ***********pointer 등 정말로 별의별 포인터가 다 나오게 되는데 대체 쟤들의 정체가 무엇이냐는 겁니다. 자 ,이제 들어갑니다. 팔 육에?  


대체 뭔지를 알아야 거기에 맞는 데이터를 넣든가 아니면 넘겨주든가 해야 할 텐데 보면 볼수록 매일 다르게 보이고 헛갈려서 도무지 알 수가 없습니다.


그나마 별이 한 두 개쯤 붙을 땐 좀 나은데 사용하다 보면 꼭 별이 하나씩 자꾸 늘어납니다. 하지만 앞으로 이제는 걱정 안 해도 됩니다. 포인터가 100만개(이론상 불가능합니다만) 나오더라도 두렵지 않게 만들어 드립니다.


변수는 데이터를 담는 그릇이라고 했습니다. 기억하죠?

그럼 이 그릇이 어떤 그릇인지 알아야 뭘 담을지 결정할게 아닙니까? 안그래요?

하지만 그전에 우리는 어떤 타입의 포인터이건 모두 OS(운영체제)마다 같은 사이즈를 가지고 있다는 것을 알고 있어야 합니다. 16비트 OS(운영체제)는 사이즈가 2바이트 이지만 32비트 OS(운영체제)에서는 4바이트가 됩니다.


  

int * ip ;             

int ** ipp ;

int *** ippp ;         

int **** ipppp ;

int ***** ippppp ;     

int ****** ipppppp ;

int ******* p ;

double * dp ;     

char * cp ;

// 라고 선언되었을 때, 아래 등식이 항상 성립합니다.


/*

sizeof ip == sizeof ipp == sizeof ippp == sizeof ipppp

                == sizeof ippppp == sizeof ipppppp == sizeof p

sizeof ip == sizeof dp == sizeof cp == sizeof & dp

*/  



만약에 여기서 선언된 p를 수식으로 사용할 때 ****p의 정체가 무엇인지 알 수 있다면 여러분은 이미 고수입니다. 이제 그만 하산하셔도 됩니다. 하지만 아직 감을 못 잡겠지요?   



다음과 같이 포인터 타입을 선언했다면 아래 세가지 형태로 사용할 수 있습니다.  


type * pointer ;


// type은 void를 포함 이 세상 모든 타입이 올 수 있습니다.

// 포인터 타입이나 사용자 지정 타입도 가능합니다.  



pointer:     

type형 데이터의 주소 값을 담을 수 있는 변수  


*pointer:    

pointer변수의 값의 주소 번지로부터 sizeof(type) 바이트만큼 참조한 값으로 type형 데이터입니다. pointer가 주소 값이기 때문에 *을 붙일 수 있습니다. 배열 연산자로 바꾸면 pointer[0]로 표현할 수 있습니다.  


&pointer:    

pointer변수도 레지스터변수가 아니라면 반드시 주소 값을 갖게 되며 그 타입은 Sinclair의 정리 #3에 의해 type ** 형이 됩니다. 이렇게 포인터 변수를 사용하다 보면 포인터 연산자가 하나씩 늘어나게 됩니다. 앞에서 포인터 변수도 일반 변수와 같다고 했습니다. 그리고 C언어는 call by value를 지원하기 때문에 현재 선언한 포인터 변수를 다른 함수의 인자로 넘겨서 그 값을 변경하고 싶다면 주소 값을 넘기는 것이 당연합니다. 포인터 변수의 주소 값이니 포인터의 포인터가 되지 않겠습니까?   



#include <stdio.h>  

/*

* copyleft ⓛ 2006-2017 programmed by Sinclair

*/   


main() {  

    int i = 23 ;  

    int * p ;

    int ** pp ;

    int *** ppp ;

    int **** pppp ;

    int ***** ppppp ;  


    p = &i ;

    // p는 타입이 int* 입니다. int형의 주소 값을 담습니다.

    // p == &i 등식이 성립합니다. 타입은 두 녀석이 모두 다 int* 입니다.

    // *p == *&i == i == 23  -▶ 타입이 int 형입니다.  


    pp = &p ;

    // pp는 타입이 int** 입니다. int* 형의 주소 값을 담습니다.

    // pp == &p 등식이 성립합니다. 타입은 두 녀석이 모두 다 int** 입니다.

    // *pp == *&p == p == &i  -▶ 타입이 int* 형입니다.

    // **pp == *p == *&i == i == 23  -▶ 타입이 int 형입니다.  


    ppp = &pp ;

    // ppp는 타입이 int*** 입니다. int** 형의 주소 값을 담습니다.

    // ppp == &pp 등식이 성립합니다. 타입은 두놈이 모두 다 int*** 입니다.

    // *ppp == *&pp == pp == &p  -▶ 타입이 int** 형입니다.

    // **ppp == *pp == *&p == p == &i  -▶ 타입이 int* 형입니다.

    // ***ppp == **pp == *p == *&i == i == 23  -▶ 타입이 int 형입니다.  

    pppp = &ppp ;

    // pppp는 타입이 int**** 입니다. int*** 형의 주소 값을 담습니다.

    // pppp == &ppp 등식이 성립합니다. 모두 다 int**** 입니다.

    // *pppp == *&ppp == ppp == &pp  -▶ int*** 형입니다.

    // **pppp == *ppp == *&pp == pp == &p  -▶ int** 형입니다.

    // ***pppp == **ppp == *pp == *&p == p == &i  -▶ int* 형입니다.

    // ****pppp == ***ppp == **pp == *p == *&i == i == 23  -▶ int  

    ppppp = &pppp ;

    // ppppp는 타입이 int***** 입니다. int**** 형의 주소 값을 담습니다.

    // ppppp == &pppp 등식이 성립합니다. 모두 다 int***** 입니다.

    // *ppppp == *&pppp == pppp == &ppp  -▶ int****

    // **ppppp == *pppp == *&ppp == ppp == &pp  -▶ int***

    // ***ppppp == **pppp == *ppp == *&pp == pp == &p  -▶ int**

    // ****ppppp == ***pppp == **ppp == *pp == p == &i  -▶ int*

    // *****ppppp == ****pppp == ***ppp == **pp == *p == i == 23  -▶ int
 

    // ***ppppp 는 어떤 값이 출력될까요? 아래 세 값은 같은 값입니다.

    printf("***ppppp = %u, pp = %u, &p = %u\n" , ***ppppp , pp , &p) ;       

    printf("%d %d %d %d %d %d\n" , *****ppppp , ****pppp , ***ppp , **pp , *p , i) ;

    return 0 ;  

} // end main()    




반드시 선언할 때 등장한 *의 개수만큼 초기화 해야 한다고 했습니다. 세가지 초기화 방법 중 어떤 것을 사용해도 괜찮습니다. 이렇게 초기화가 끝난 변수는 선언 할 때 사용한 *의 개수만큼 *를 붙일 수 있습니다.

선언한 것 보다 적게 붙였다면 아직은 주소 값입니다.

하지만 더 많이 붙이면 에러가 발생합니다. 컴파일이 안됩니다.


주소 값이 아니면 *을 붙일 수 없습니다. 그렇게 사용하면 잘못된 수식입니다.


int i ;

printf("%d\n", *i) ; // 이게 뭡니까? 이게..



어느 날 강의를 하는 도중에 갑자기 질문을 하나 받았습니다. 저보고 왜 포인터 선언을 할 때 *를 중간에 찍느냐는 것이었습니다. 보통 일반적으로 다른 책에서는 변수 쪽으로 몰아서 붙입니다. 어느 회사의 표준 코딩스타일에서는 타입 쪽으로 몰아서 붙이라고 권고합니다. 하지만 저는 꿋꿋이 가운데 놓고 있습니다. 우리가 위의 예제를 자세히 살펴보면 변수 쪽에 *가 하나씩 붙을 때 마다 타입과 남은 *가 바로 타입이 된다는 것을 알 수 있습니다. 놀라운 사실은 바로 타입에 남아있는 *와 변수에 붙은 *의 합이 처음에 선언했던 *의 개수와 같다는 것입니다. 놀랍지 않습니까? 이제 *가 붙은 포인터 변수의 타입이 보입니까? 타입을 알았으니 어떤 그릇인지 알게 되고 그러면 당연히 어떤 요리를 담아야 할지 결정 됩니다. 그러면 끝입니다. 이렇게 *가 타입 쪽으로 붙으면 주소 값을 담는 포인터 타입이 되고 변수 쪽으로 붙으면 메모리를 참조하는 포인터 수식이 됩니다. 혹시 *가 100만개가 나오더라도 이것은 변하지 않는 법칙입니다. 그러니 중간에 둬야 하지 않겠습니까? 어때유? 쉽쥬?~


변수 상자를 그리고 포인터 마다 메모리 참조의 화살표를 그어 가면서 포인터를 설명 할 수는 있습니다. 하지만 저는 절대로 그렇게 설명하지 않습니다. 포인터 두 개까지는 따라가며 이해할 수 있었지만 세 개, 네 개가 되면 더 이상 나의 것이 아니었습니다. 화살표를 따라가다 보면 내가 지금 어디에 있는가? 나는 누구인가? 의문이 들기 시작합니다. 우리가 그러다 좌절한 적이 어디 한 두 번입니까? 눈물이 앞을 가립니다. 흑흑흑~  




반짝 반짝 작은 별~ 아름답게 비치네~

이쪽 소스 에서도 저쪽 소스에서도~~
반짝 반짝 작은 별~ 아름답게 비치네~

 

아무리 많은 별들이 한꺼번에 반짝이더라도 이제 우리는 그 의미를 알 수 있습니다.


아직 잘 모르겠다구요? 아직 마음 속 깊이 존재하는 바둑판과 화살표를 포기하지 못했기 때문입니다. 많은 사람들이 포인터에서 바둑판과 화살표를 버리고 메모리를 핸들링 하는 연산자로서의 새로운 패러다임을 만나게 되었을 때 프로그래밍의 모든 것이 변하더라는 말을 하고 있습니다. 우리가 오해하고 잘못 알고 있었던 그것을 포기하면 좀 더 멋진 세상을 만날 수 있습니다.    



오늘은 아무리 시간이 없고 눈코 뜰새 없이 바쁘더라도 밤하늘을 한번 올려다 보길 바랍니다. 그리고 별을 노래하는 마음으로 모든 죽어 가는 것을 사랑해야지 하셨던 윤동주 시인의 마음을 한 번 헤아려 보는 것도 좋을 듯합니다. 부디 기억하길 바랍니다. 지난 20세기를 마감하면서 IT의 거장들이 모인 자리에서 21세기 최고의 화두가 과연 무엇이냐 질문을 했더니 모두가 입을 모아 그것은 바로 사람이라고 대답을 했다고 합니다. 이 세상 어떠한 기술도 사람을 앞서지 못합니다. 또 절대 그래서도 안됩니다.


어둠이 짙게 깔릴수록 더욱 빛을 발하는 밤하늘 별들이 제 눈에는 꼭 *를 닮아 보이는 까닭은 왜일까요? 오늘 밤에도 별이 바람에 스치울 겁니다.

 





C언어 및 기타 프로그래밍 관련 질문은 오픈 카톡으로

group talk - https://is.gd/yourc

1:1 talk - https://is.gd/aboutc

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

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