엔지니어링 101: 포인터 이해하기

엔지니어링 101: 포인터 이해하기

Python이나 JavaScript로 시작했다면 포인터는 흰 수염의 시스템 엔지니어만 쓰는 신비한 마법처럼 들린다. 대학에서 C++를 배웠다면 세그멘테이션 폴트와 메모리 누수에 고생한 기억이 있다.

비밀은 이렇다. 포인터는 그냥 변수다. 그 값은 숫자다. 그 숫자는 다른 무언가의 주소를 담고 있다.

포인터를 이해하려면 먼저 데이터가 어디에 사는지 알아야 한다.

두 영역: 스택과 힙

코드가 실행되면 데이터는 어딘가에 놓여야 한다. 그 자리는 스택 아니면 힙이다.

스택: 빠르고 정돈돼 있다

스택은 책상 위의 포스트잇 더미를 떠올리면 된다.

int a = 10; 같은 변수가 있다면 보통 여기에 놓인다. 빠르고 안전하다.

힙: 무법지대

힙은 거대한 창고다.

그럼 포인터는 뭘까

포인터는 단순히 이정표다.

score = 99 라는 변수가 힙(주소 0x1234)에 있다고 하자. 포인터는 스택 위의 아주 작은 변수로, "데이터는 0x1234에 있다"고 알려준다.

int score = 99;  // 실제 값
int *p = &score; // 포인터 'p'는 'score'의 주소를 담고 있음

p는 주소다. 데이터가 어디 있는지를 말한다. *p는 값, 곧 그 주소에 담긴 것이다.

왜 이런 고생을 하는 걸까

그냥 값을 전달하고 언어가 뒤처리를 하게 두면 안 되나?

1. 속도(복사 안 함)

10MB 이미지를 떠올려 보자. 그것을 process(image) 함수에 넘기고 언어가 이미지를 복사하면, 픽셀을 복사하는 데 RAM 10MB와 CPU 시간을 낭비한다. 포인터라면 주소(8바이트)만 전달하면 된다. 함수는 이미지가 어디 있는지 알고 그곳을 보러 간다.

2. 상태 공유

변수를 값으로 넘기면 함수는 복제본을 받는다. 복제본을 바꿔도 원본은 그대로다. 포인터를 넘기면 함수는 원본이 어디에 사는지 안다. 실제 데이터를 바꿀 수 있다.

3. 동적 구조

연결 리스트, 트리, 그래프는 다른 데이터 덩어리를 가리키는 포인터를 담은 데이터 덩어리다. 참조 없이 이것들을 만들기는 어렵다.

치르는 대가

포인터는 공짜가 아니다.

  1. 메모리 오버헤드: 포인터 자체가 공간을 차지한다(64비트 시스템에서는 보통 8바이트). 작은 정수를 가리키는 포인터 배열이라면 데이터보다 포인터가 더 많은 메모리를 쓸 수도 있다.
  2. 성능 손실: "역참조"(포인터를 따라가 데이터에 닿는 일)는 CPU 사이클을 쓴다. "캐시 미스"를 일으키기도 한다. 데이터가 RAM의 먼 곳에 있으면 CPU는 그것을 가져올 때까지 기다려야 한다. 스택은 캐시에 친화적이다. 연결 리스트처럼 포인터가 많은 구조는 대개 그렇지 않다.
  3. 머릿속 부담: 널 포인터 예외. 댕글링 포인터. 메모리 누수. 포인터를 손수 관리하려면 규율이 필요하다. 그래서 Rust, Go 같은 현대 언어나 Java, V8 같은 런타임은 포인터를 숨기거나 대신 관리하려 한다.

결론

포인터는 코드와 하드웨어를 잇는 다리다. 고수준 JavaScript나 Python을 쓸 때도 만지는 객체는 속으로는 참조다. 그래서 a = b는 a를 b와 같은 데이터를 가리키게 한다.

© Melvin Laplanche - All rights reserved.