포인터

 

2. 개의 품종
3. 프로그래밍 용어

Pointer

1. 레이저 포인터


레이저를 이용해서 무언가를 가리키는 용도로 쓰는 물건. 물론 원래의 용도만으로 쓰이지는 않는다. 항목 참고.

2. 개의 품종


[image]
품종 중 하나.
스페인이 원산지인 사냥개이며 이후 잉글리시 포인터, 저먼 포인터, 스패니쉬 포인터 등으로 개량되었다.
총을 든 사냥꾼과 팀을 이뤄 새를 사냥하는 조렵견이다.
새를 포착하면 가만히 멈춰서서 앞다리 한쪽을 구부려 올리고 포인팅(Pointing)을 해서 위치를 알려준다. 앞다리 한쪽을 들고 남은 세 다리로 버티며 무슨 전진무의탁 자세마냥 앞으로 쓰러진듯 버티고 서는 자세다.
[image]
[image]
[image]
[image]
사냥꾼에게 사냥감의 위치를 알려주기 위해 가리키기(pointing) 때문에 포인터(Pointer)라는 이름이 붙었다. 총을 맞은 사냥감을 물어오는 것도 역할 중 하나이다.
수컷은 성체가 60~70cm에 25~34kg, 암컷은 58~66cm, 20~30kg 정도의 대형견이다.
본래 가지고 있는(목, 다리가 길고 우아한 곡선의) 몸 형태 뿐만 아니라 서있는 모습, 움직이는 행동, 앉아있는 모습에서도 적당히 힘이 있으나 꼿꼿하고 우아한 특징이 있다.
옆에서 서있는 모습을 보면 몸형태가 h 모양이다. (국내 의류 브랜드 헤지스의 마스코트가 포인터인데 헤지스의 h모양을 본뜬 듯하다.)
모색은 잉글리시(영국) 포인터는 블랙&화이트, 오렌지&화이트가 보이고, 저먼(독일) 포인터는 블랙, 회색과 화이트 색이 섞여있다.
단모종으로 추위를 쉽게 타서 보온에 신경을 써줘야한다. 이해가 부족한 주인들은 대형견이 크다(크다=튼튼하다?)는 이유로 야외에서 무심히 묶어두고 키우는데, 특히 포인터 중에서 얼어죽는 개들이 많다고 한다.
공격성이 낮고 짖음도 거의 없는 순한 성격이라 아이들과도 잘 어울려 주고 다른 개와 같이 키우는 것도 쉽다. 그리고 덩치에 안맞게 겁도 많다.작은 강아지가 짖으면 무서워한다거나... 아래는 포인터 성격을 보여주는 컷.
모든 유년기의 개들과 같이 어릴 때는 힘을 소모시켜 주기위해 적절히 운동을 시켜주는 것이 필수이다. 실내에서도 유순하여 키우기 무난하다. 평균 수명은 12~15년 정도. 처음 본 사람이나 개에게도 공격성을 보이지 않으며 짖는 일이 적어 반려견으로도 많이 키우지만 반대로 경비견으로는 부적절하다.

3. 프로그래밍 용어




''' 포인터는 마녀다. 하지만 포인터를 이해하면 마녀의 힘을 쓸 수 있다.'''

한 C언어 프로그래밍 서적에서 포인터를 표현한 말[1]

컴퓨터 프로그래밍에서 이용되는 용어로, 어떤 값을 가리키기 위한 형식을 뜻한다.
시초는 기계어(와 어셈블리어)에서 이용되는 간접 참조인데, 이는 명령어에서 이용할 값을 명령어 코드에 직접 쓰는 것이 아니라 특정 메모리 번지에 있는 값을 읽어서 이용하라고 하는 것이다. 예를 들어 더하기를 할 때 '3'을 더해라"고 할 수도 있지만 '메모리 1000에 저장된 값'을 더해라"고 해야 할 수도 있다. 이 때 메모리의 값을 먼저 읽는 명령을 쓰고, 더하는 명령을 따로 쓰면 아무래도 효율이 떨어지기 때문에 같이 처리하는 명령이 생기게 되었다. 이후 이러한 아이디어가 CC++ 등 다른 프로그래밍 언어에도 적용되면서 현재의 포인터가 되었다.[2]
처음 배울때는 그냥 값을 사용하면 되지 왜 포인터처럼 복잡하게 주소를 써서 머리를 아프게 하냐고 생각할 수 있는데, 포인터가 실제로 사용되는 예를 들어 간단히 설명하자면 서로 다른 함수에서 선언된 변수들은 유효범위(Scope)가 서로 안 겹치기 때문에 해당 변수를 직접 사용할 수 없다. 이때 포인터를 이용하면 서로 다른 함수 내의 변수값을 주소를 통해서 제어할 수 있게 만드는 도구로 쓸 수 있다. static 변수에 값이 덮어씌워지는 것을 방지하기 위해 해당 변수를 포인터로 참조하고 다른 주소에 전용 공간을 할당하여 해당 변수값을 컨트롤하도록 할 수도 있다. 즉 접근이 까다로운 변수를 참조하여 활용하고 데이터 손실의 위험도 방지할 수 있는 것이다.
미적분을 배울 때 이걸 함수를 이해하는 도구로 써먹느냐 단순히 머리 아픈 수학공식으로 이해하느냐의 차이라고 볼 수 있다.
포인터를 사용해 얻는 대표적 이득으로는 C에서의 연결 리스트, 동적 메모리 할당, 멀티스레드 프로그램 등이 있다. 하드웨어 컨트롤이나 그래픽관련 퍼포먼스에 관해서도 이득이 크다.
프로그래밍 언어에 쓰이는 다른 기능들이 2차원 평면적이라면, 포인터는 거기에 수직적인 축을 하나 더 만들어 수직으로 움직이는 셈이 되기때문에 포인터가 사용되면 말그대로 '입체적으로' 복잡해진다. 아무래도 한 단계를 더 거치는 특성상 생각해야 할 것이 많아지기 때문에 이용하기도 쉽지 않다. 특히 포인터가 이중, 삼중으로 쓰이거나 하면 난이도는 수직상승하기 때문에 프로그래머의 골머리를 썩히는 일등공신으로 꼽힌다. 게다가, 포인터 관련 에러는 디버깅으로 잡기 힘든 버그들도 많고, 컴파일러에서 행하는 여러가지 최적화들이 문제가 생길 여지도 많아진다. 여러모로 단점이 상당히 많아보이지만 그런걸 전부 감안해도 포인터를 사용해서 얻어지는 이득이 워낙 크기 때문에 일종의 양날의 검이라 할 수 있다.
수많은 전자공학, 컴퓨터공학을 지망하여 들어온 대학생 1학년들이 여기서 좌절하고, 전공을 바꾸려한다. 특히 C 포인터에서 가장 악마같은건 포인터와 배열을 왔다갔다하는 장난질. 이쯤에 오면 그들에게는 언어가 아니고, 외계어가 된다. 여기를 넘어가느냐 안가느냐에 따라 남은 대학생활의 학점이 결정된다고 해도 과언이 아니다.
크고 아름다운 데이터(특히 객체)를 다루는 현대의 프로그래밍 언어에서 포인터가 아주 없기란 사실상 불가능하지만, JavaC\#과 같이 최근의 고생산성 언어에서는 포인터를 가능한 한 직접적으로 건드릴 필요가 없는 방향으로 가고 있다.
포인터는 메모리 주소를 가리키는 숫자 변수다. 메모리 주소는 정수이므로 정수형을 사용하고 있으며, 32비트 아키텍처 상에서는 32비트 정수, 64비트 아키텍처 상에선 64비트 정수를 사용한다. 포인터의 포인터는 숫자 보고 찾아간 주소 안에 다른 주소를 가리키는 숫자가 들어있는 것이다. 3차원 이상 포인터도 똑같다. 함수 포인터도 마찬가지다. 다만 마지막 주소가 함수의 시작 주소를 가리킨다의 차이. 그래서 모든 포인터의 크기는 같은 것이다[3]. char든 int든 구조체든 뭐든. C언어로 코딩이 가능한 사람은 당장 sizeof(char)와 sizeof(char *)를 비교해보라. 전자는 1(8bits)이고 후자는 8(64bits)이다. sizeof(int)는 4이고 sizeof(int *)는 8이다. 구조체의 경우에는 구조체를 어떻게 정의했느냐에 따라 sizeof(STRUCT)의 값이 달라지지만 sizeof(STRUCT*) 의 값은 무조건 8이다. 참고로 32비트 C컴파일러로 컴파일하면 모두 4가 나온다. sizeof(char**)도 8이며 sizeof(int***)도 8이다. 포인터가 몇 단계가 중첩되든 이 값은 항상 8이다.[4]
포인터의 진짜 어려움은 스택 공간과 힙 공간의 차이를 이해하느냐 마느냐에서 결정된다.[5] malloc 함수는 어째서 반복 수행할 때마다 서로 다른 포인터를 리턴하는가? 그리고 함수 안에서 선언된 변수는 배열 포함해서 신경 안 써도 되는데 어째서 malloc 계열 함수는 꼭 free를 해 줘야 하는가? 이에 대한 답을 찾아야 포인터를 이해할 수 있게 되는 것.[6] 그리고 포인터 변수가 단지 숫자값이라는 점을 이해하고 있어야 포인터의 포인터(2차원 포인터)나 함수 포인터의 개념을 이해할 수 있다. 그리고 크기가 10만을 넘어가는 배열을 선언하면 안 된다고 하면서 malloc으로는 기가바이트 단위의 공간을 할당해도 잘 돌아가는가? 그리고 그렇게 할당한 공간에 배열처럼 접근해서 숫자 천만이 넘어가는 인덱스를 넣어도 왜 멀쩡한가에 대해서도 이해할 수 있다.
C의 malloc 함수, C++의 new 연산자는 OS의 서비스를 사용한다. 즉, 이것들은 프로그램 내부를 떠나 외부 세계인 OS와 통신한다.[7][8] 스택 공간이라고 함은 함수나 메소드 안에서 사용할 수 있는 격리된 공간인데 이 공간은 20kb나 64kb 정도로 작으며 컴파일러 옵션으로만 조정이 가능하다. 그러니까 스택 공간은 따로 얘기 안해도 주는 기본 용량이라 할 수 있다. 일반적인 함수는 이 기본 용량 가지고 충분하지만 데이터를 다루려고 하면 택도 없는 용량이 된다. 그래서 데이터용 공간은 메모리에 별도로 할당해달라고 OS에 요청할 필요가 있는데 이 때 OS는 힙 공간에서 그 별도의 메모리를 예약해주고 그 시작 번지를 포인터 주소로 리턴한다. 그러면 프로그램은 그 포인터 주소로 가서 할당된 공간만큼 자기 맘대로 사용하는 것이다. 참고로 시작 번지만 알려주고 끝 번지를 알려주지 않는데 끝 번지는 시작 번지에서 요청했던 용량을 더하면 얻을 수 있다. 즉 OS는 할당해달라는 주소 공간에 단편화를 일으키지 않고 연속된 주소 공간을 돌려준다.[9] 그리고 사용이 끝난 공간은 OS에게 반납하겠다고 선언한다. 가비지 콜렉션은 이 반납 절차를 생략해도 프로그램이 알아서 반납 처리를 진행하는 것을 말하고(OS가 아니다!), 프로그램이 종료하면 OS가 그 프로그램이 임대한 힙 공간 전체를 자동 반납하게 설계돼있다. 그래서 프로그램 강제 종료 이벤트 이후에는 프로그램이 아무리 막장으로 짜여 있더라도 메모리를 영구적으로 갉아먹지 않는 것이다.[10] 디바이스 드라이버 등 커널 공간에서 수행되는 프로그램은 커널 자체가 종료해야 반납이 이루어지는데, 커널의 종료는 곧 시스템 셧다운을 의미하므로 디바이스 드라이버에서 메모리 누수가 일어나면 치명적이다.[11]
스택은 말 그대로 접시 쌓듯이 데이터를 포개 쌓아서 보관하므로 스택이고, 힙 공간은 어원은 알 수 없으나 그냥 아무렇게나 할당할 수 있는 자유 공간이다. 자유에는 책임이 따르는 법. 스택은 함수가 리턴할 때 pop을 적당히 해 주면 메모리가 알아서 정리되지만 힙 공간은 그런 관리 메커니즘이 없으므로(가비지 콜렉션이 있다면 얘기가 다르다) 할당을 요청한 쪽에서 책임지고 해제해줘야 하는 것이다.
OS는 힙 공간 내부에서 데이터가 어떻게 다루어지는지 모르기 때문에 할당해 준 힙공간을 해제해도 되는지 판단할 수 있는 근거는 둘 뿐이다.
  1. 프로그램 스스로가 다 썼다면서 반납한다(명시적 메모리 해제, 가비지 콜렉터 작동)
  2. 프로그램이 종료되었다.
하지만 스택 공간을 해제해도 되는지 판단할 근거는 아주 직관적이다.
  1. 함수가 return문을 실행했다.
  2. 함수가 끝났다.
  3. 프로그램이 종료되었다.
물론 세마포어 같은 걸로 들어가면 더 복잡해지지만 일단 공유메모리는 빼고 얘기하자면 저렇다.
OS를 사용하지 않는 펌웨어 등은 기기의 전체 메모리를 자기가 직접 관리해야 한다. 임베디드 시스템에서는 소형이고 저전력이다 보니 처리속도가 떨어져서 OS를 못 올리는 경우가 많다. 이런 곳에서는 메모리 관리 기능이 없으므로 소스 코드가 포인터와 define 매크로로 떡칠되기도 한다.
스택이고 힙이고 없다. 자기 자신이 OS니까 자기가 메모리 관리까지 책임져야 한다. malloc 같은 것은 없고 그냥 포인터 주소를 기준으로 한정된 메모리 공간을 적절히 나눠서 알아서 관리해야 한다. 이 작업이 너무너무 빡세기 때문에 사람들이 OS를 사용하는 것이다. 일반사용자 입장에서 OS는 부팅 시간이나 잡아먹고 가끔 블루스크린이나 띄워주는 욕만 들어먹는 프로그램이지만 프로그래머 입장에서는 이게 없으면 대단히 불편하다.
[12]

포인터 참조 오류는 멀티스레드 동기화 이슈에 버금갈 정도로 굉장한 골칫거리이다. 프로그램이 오류를 일으키는 주범이자 압도적인 비율을 차지한다. C 언어 오류의 90%는 포인터에서 나온다. 구조적으로 포인터는 기계 제어에는 꼭 필요한 요소이기에, 이쪽 분야에서는 자유롭게 포인터를 사용할 수 있는 C가 절대적인 지위를 차지하고 있다.
반대로, 기계 제어가 '''아닌''' 분야에서는 굳이 포인터에 목 매일 필요가 없다. 포인터로 온갖 문제를 야기하느니, 포인터를 아예 없애는 게 더 효율이 높다고 판단하기 때문이다. 그래서 최신 언어인 경우 프로그래머는 포인터를 직접 다룰 수 '''없다'''. 정확히는 내부적으로는 포인터를 사용하겠지만, 외부적으로는 프로그래머가 직접 포인터를 다룰 필요가 없도록 숨겨 놓은 것이다. 또한, 레퍼런스(참조자, 참조변수)라는 대안이 있기 때문에, 포인터와 사실상 같은 동작을 수행할 수 있다. C를 고집하지 말고 바로 JAVA부터 시작하라는말은 여기서 비롯된것. 실제로도 그렇고 국비지원으로 양산하는 코더들도 이 단계를 생략하기 때문에 프로그래밍의 구조적인 이해도면에서 정규커리큘럼을 거쳐온 학부생들에게 밀리게 되는것이다.


[1] 마녀처럼 이해하기 어렵지만 포인터를 활용하면 마녀의 힘. 그러니까 마법을 쓰는것처럼 프로그래밍의 효율성이 비약적으로 높아진다는 소리.[2] 프로그래밍을 처음 접하는 사람이라면 이해가 어려울 수 있겠지만 게이머라면 치트 엔진을 사용하는 것으로 직관적으로 이해할 수 있을 것이다. 치트 엔진으로 게임 내 변수의 값을 조작하는 게 바로 포인터 접근 방식이다.[3] C/C++적으로는 잘못된 설명이다. C언어에서는 모든 포인터의 크기가 같을 필요가 없다. C++에서는 가상 맴버 함수 때문에 더더욱 그러하다. POSIX 2008적으로는 옳은 설명. [4] 포인터가 항상 똑같은 64비트 정수인데 어째서 포인터에 타입이 다양하게 있는지 궁금한 사람을 위해 첨언하자면 포인터 연산 때문에 타입 정보를 제공하는 것일 뿐이다. 따라서 모든 포인터는 강제 형변환을 통해 어떠한 타입으로도 변환이 되며 그것의 궁극은 void포인터. void 값 타입은 없어도 void 포인터 타입은 존재할 수 있는 이유가 이것이다.[5] 꼭 그렇지는 않다. 정말 놀랍게도, C/C++ 자체에는 stack이나 heap이라는 개념이 없다. 다만 대다수의 C/C++ 구현체들이 stack/heap의 구조를 이용하여 구현하고 있을 뿐이다. 이 문단의 내용은 C/C++에서는 stack/heap보다는 기억 수명(storage duration)과 관련된 내용이다. 그러나 그렇다고 하더라도 stack/heap에 대한 이해는 OS의 동작을 이해하기 위해 여전히 중요하다.[6] malloc 함수의 경우 메모리를 직접적으로 건드리고, C의 쓰레기 수집은 매우 비효율적이기 때문에 사용하고 난 후 반드시 해당 메모리 부분을 청소해 주어야 한다. 그렇게 하지 않으면 메모리 누수가 발생하여 프로그램 속도가 처참해지는 등 골치를 썩인다.[7] 그래서 이 둘은 어셈블리어 이외의 방법으로는 작성이 불가능하였지만 최근의 라이브러리에서는 ''syscall'' 래퍼를 제공해주기에 C로도 작성할 수 있다.[8] 정확히 이야기하면 malloc이나 new는 운영체제로부터 얻어온 메모리 페이지를 쪼개서 요청한 만큼만 메모리 블럭을 리턴해주고 나머지 메모리는 차후에 사용할 수 있도록 보관해두는 일을 한다. 보관해둔 메모리 페이지보다 할당 요청 크기가 클 때에만 (유닉스/리눅스를 예로 들자면) ''sbrk'', 혹은 ''mmap'' 시스템 콜을 이용하여 메모리 페이지를 얻어온다. 따라서 malloc을 호출했을 때나 new로 객체를 생성할 때 무조건 OS의 기능을 활용하는 것은 아니다.[9] 실제 물리 메모리에는 단편화돼서 저장된다. OS가 페이지 맵핑 테이블을 가지고 연속적인 메모리 공간을 에뮬레이션해주는 것이다. 심지어 물리 메모리에 저장된 데이터는 상황에 따라 이리저리 옮겨다니며 스와핑에 의해 메모리에서 아예 퇴출돼 나가기도 한다. 하지만 애플리케이션에서는 그런 거 신경쓰지 않아도 된다.[10] 프로그램 종료 후에도 메모리 누수를 일으키게 의도적으로 만들어진 프로그램은 있을 수 있다.[11] 이 때문에 디바이스 드라이버를 제작할 때 하드웨어와 직접적으로 통신할 때에만 커널의 도움을 받고 나머지 기능은 사용자 공간에서 작동하도록 만들 수도 있다.[12] 능엄겸에서 석가모니아난다에게 한 말.