포인터
Pointer.
프로그래밍 언어에서 다른 변수(클래스를 비롯한 객체, 함수 등등도 포함한다)의 메모리 공간 주소를 가리키는 변수를 뜻한다.[1] C/C++ 언어의 강력한 기능이자[2] 만악의 근원이며 프로그래머의 뒷목을 잡게 만드는 원흉이기도 하다.
포인터는 주로 C에서 많이 쓰이는 기능이기 때문에 여기서는 C를 기준으로 설명한다.
포인터는 데이터의 메모리 주소값만을 가지고 있기 때문에 포인터의 크기는 주소값의 크기로 고정된다. 예를 들어 32비트 프로그래밍을 한다고 가정하면, 그 '32비트'가 포인터의 크기가 된다. 즉 char든 double이든 float든 뭐든, 1만 바이트짜리 긴 문자열이든 그 포인터의 크기는 32비트다. 또한 포인터는 데이터의 시작 부분 주소값만을 담고 있다. 100 바이트짜리 char* 문자열이라고 해도 포인터는 무조건 첫 바이트의 주소값만 가지고 있다. 따라서 포인터만 가지고는 그 데이터가 어디에서 끝나는지 알 수 없다. C는 문자열의 끝이 NULL로 끝나야 한다고 정하고 있다.
포인터의 연산을 허용하는 언어의 경우, 포인터의 값을 변경시키면 포인터가 가리키는 주소가 바뀌는 결과가 된다. 예를 들어 포인터 변수에 1을 더하면 포인터가 가리키는 주소가 1 바이트 다음으로 옮겨 간다.[3] 이를 배열 변수에 위치해 있는 원소를 가리키기 위해 사용할 수 있다. 예를 들어 포인터가 배열의 선두를 가리키고 있을 때 여기에 2를 더하면 배열의 두 번째 원소를 얻을 수 있다.
요즈음의 운영체제는 각 프로세스가 격리된 메모리 공간을 가지기 때문에 포인터 연산에 문제가 있더라도 그 프로세스가 죽어버리거나 하는 식으로 끝나지만 MS-DOS 혹은 그 위에서 돌아갔던 윈도우 3처럼 프로세스별 메모리 격리가 철저하지 않았던 운영체제는 포인터 연산을 잘못하면 다른 프로세스의 메모리를 침범하거나 심지어 운영체제가 사용하는 메모리 공간을 건드리는 사태가 벌어질 수도 있었다. 프로세스가 격리된 메모리 공간을 가지도록 설계한 운영체제라고 해도, 그 운영체제의 핵심인 커널이라든가, 운영체제가 컴퓨터에 연결된 각종 하드웨어를 제어할 수 있도록 하는 드라이버와 같은 것들은 여전히 위험이 있다. 게다가 이런 종류의 소프트웨어들은 대부분 C 또는 C++ 언어를 사용해야 한다.[4]
겉보기에는 포인터를 사용하지 않는 것처럼 보이는 프로그래밍 언어도 알고 보면 내부에는 포인터 개념을 사용하고 있는 모습을 볼 수 있다. 예를 들어 포인터와는 전혀 관련이 없을 것 같은 파이썬의 경우,
x = [1, 2, 3]
y = x
print(id(x), id(y))
이 코드는 y에 [1, 2, 3] 리스트를 대입한 것이 아니다. 이 코드를 실행시켜 보면 '4413603008'와 같은 결과가 나온다. 파이썬애서는 이를 객체의 ID라고 부르는데, 숫자 자체는 그때 그때 달라지지만 x와 y는 같은 숫자값이 들어 있다. 즉 x, y 모두 같은 메모리 공간 주소를 가리킨다는 뜻이다. 즉 y에는 x의 메모리 공간 주소를 대입시킨 것이다. 따라서 y.append(4)
명령으로 리스트 y에 1을 추가시키면 x 역시 [1, 2, 3, 4]로 리스트의 내용이 바뀐다. 파이썬에서 클래스의 인스턴스 메서드를 정의할 때 반드시 첫 번째 매개변수로 self를 써 줘야 하는데, 실제 메서드를 부를 때에는 self는 쓰지 않는다.[5][6] 파이썬이 자동으로 클래스 인스턴스의 포인터를 self에 넣어주기 때문이다. 단, 포인터 연산 같은 것은 막혀 있다. 포인터에 관련된 심각한 버그를 자주 일으키는 원인이 포인터 연산이다 보니 아예 이를 막는 언어들도 있지만 자바처럼 가상머신 위에서 돌아가는 언어는 물리적인 메모리 주소를 가지고 포인터 연산을 하는 게 의미가 없는 문제도 있기 때문.
포인터는 어려운 개념이고 C처럼 포인터 연산도 허용하면 여러 가지 버그를 일으키는 원인이 되지만 그래도 프로그래밍에서는 중요한 개념으로 쓰인다. 함수에 매개변수로 1 메가바이트짜리 데이터를 넘겨준다고 가정해 보자. 포인터가 없다면 데이터를 통째로 넘겨줘야 하지만 포인터를 쓰면 32 비트 프로그래밍이라면 32 비트짜리 데이터, 즉 메모리 주소만 넘겨주면 그만이다.[7] 현대 프로그램이 언어는 사용자들이 함부로 포인트를 건드리지 못하도록 감추어 놓았거나 엄격하게 제한해 놓았을 뿐이다.
각주
- ↑ 'Point'에는 '가리키다'라는 뜻이 있다. 즉 포인터는 point-er, '가리키는 것'이라는 뜻이 된다.
- ↑ 고급 프로그래밍 언어 가운데 운영체제의 커널, 하드웨어 드라이버, 펌웨어와 같은 것들이 거의 대부분 C 아니면 C++인 주요한 이유 중 하나가 포인터 사용의 자유도 때문이다. 여러 언어들이 C/C++를 대체하면서도 포인터 때문이 일어나는 문제를 줄이겠다고 나섰지만 목적 달성에 실패했다. 그러다가 Rust가 등장하면서 C/C++의 아성에 조금씩 다가가는 중이다. 아직 갈 길이 멀긴 하지만.
- ↑ 정확히는 자료형의 단위만큼 움직인다. 예를 들어 포인트 변수의 자료형이 2바이트라면 여기에 1을 더했을 때에는 2바이트만큼 움직인다.
- ↑ 최근에는 C 수준으로 시스템 프로그래밍을 할 수 있으면서 포인터로 인한 버그 위험을 대폭 줄여주는 Rust 언어가 점점 인기를 얻어가고 있다.
- ↑ 예를 들어 클래스 메서드는
def func(self, a)
같은 식으로 정의하지만 부를 때에는c.func('Hello')
처럼 매개변수 a만 넘겨준다. - ↑ 만약 파이썬 클래스의 메서드를 정의할 때 첫 매개변수에 self를 쓰지 않으면 정적 메서드로 간주한다. 즉 이 메서드 안에서 self를 쓰면 오류가 난다.
- ↑ 물론 이 경우 함수 안에서 데이터를 건드리면 당연히 함수를 호출한 쪽의 데이터도 똑같이 바뀐 상태가 된다. 이를 피하려면 함수 안에서는 읽기 전용으로만 쓰거나, 데이터를 복사해서 넘겨줘야 한다.