포인터
고친 사람 github-actions[bot]
포인터는 데이터가 놓인 곳을 가리킵니다. 데이터를 복사해 넘기는 대신 메모리 주소만 넘기고, 받은 쪽은 그 주소를 따라가 값을 꺼냅니다. 같은 이름을 다른 뜻으로 쓰는 분야도 있습니다. 이 항목은 메모리 주소를 담는 쪽을 다룹니다.
쉽고 빠른 이해
포인터는 데이터가 놓인 메모리 주소를 담아 두는 값입니다. 사용자가 올린 파일을 읽어 담은 큰 덩어리를 함수마다 복사해 넘기는 대신, 그 덩어리의 주소 하나만 넘길 때 씁니다.
큰 데이터를 넘길 때마다 복사하면 복사에 드는 시간이 계속 붙습니다. 여러 곳이 같은 데이터를 함께 고쳐야 할 때도 복사본이 아니라 한 벌을 봐야 합니다.
도는 모양은 셋입니다.
- 데이터를 메모리에 두고 그 주소를 얻습니다
- 그 주소를 포인터에 담아 필요한 곳으로 넘깁니다
- 받은 쪽은 주소를 따라가 데이터를 읽거나 고칩니다
대가가 있습니다. 주소를 따라가는 걸음이 한 번 더 붙습니다. 가리키던 데이터가 이미 치워졌는데 주소만 남으면 엉뚱한 곳을 읽게 됩니다.
상세
포인터는 집 주소를 적은 쪽지와 닮았습니다. 친구네 집을 들고 다닐 수는 없으니 주소를 적어 두었다가 필요할 때 그 주소로 찾아갑니다.
메모리는 바이트마다 번호가 하나씩 매겨져 있습니다. 이 번호를 주소라고 부릅니다. 포인터는 이 주소를 값으로 담는 변수입니다. 정수 하나가 메모리 어딘가에 놓여 있으면, 그 정수를 가리키는 포인터는 정수가 시작하는 바이트의 번호를 담습니다.
포인터가 담은 주소로 찾아가 그곳의 값을 읽거나 쓰는 일을 역참조라고 부릅니다. 포인터 자신을 읽으면 주소가 나오고, 역참조하면 가리키는 값이 나옵니다. 둘을 헷갈리면 주소를 값으로 알고 쓰게 됩니다.
C 로 쓰면 이렇습니다.
int n = 10; // 값 10
int *p = &n; // n 의 주소
*p = 20; // n 이 20 이 됨
셋째 줄이 역참조입니다. p 를 고친 것이 아니라 p 가 가리키던 n 을 고쳤습니다. 이 한 줄이
포인터로 할 수 있는 일의 뿌리입니다. 주소를 아는 쪽은 그 주소의 데이터를 읽을 수도, 고칠 수도
있습니다.
주소를 넘기면 달라지는 것
이 소절은 데이터를 복사해 넘기는 것과 주소만 넘기는 것이 어떻게 갈리는지 봅니다.
함수에 데이터를 넘기는 방법은 둘입니다. 데이터를 복사해서 넘기거나, 데이터가 있는 곳의 주소를 넘기거나입니다. 어느 쪽을 고르느냐에 따라 넘어가는 양도, 고쳤을 때 일어나는 일도 달라집니다.
| 복사해 넘길 때 | 주소를 넘길 때 | |
|---|---|---|
| 넘어가는 양 | 데이터 크기만큼 | 주소 하나 |
| 받은 쪽이 고치면 | 복사본만 바뀝니다 | 원본이 바뀝니다 |
| 원본이 치워지면 | 복사본은 멀쩡합니다 | 남은 주소가 엉뚱한 곳을 가리킵니다 |
가운데 줄이 포인터를 쓰는 가장 큰 이유입니다. 여러 곳이 한 데이터를 함께 보고 고쳐야 할 때, 복사본을 나눠 주면 각자 다른 값을 들게 됩니다.
첫 줄은 크기가 큰 데이터에서 드러납니다. 주소는 크기가 정해져 있어서 가리키는 데이터가 크든 작든 넘어가는 양이 같습니다. 주소를 몇 바이트로 적는지는 기계가 정합니다. 예순네 비트 주소 폭을 쓰는 기계에서는 포인터 하나가 여덟 바이트입니다.
마지막 줄이 대가입니다. 뒤의 「자주 나는 오류」가 이 줄에서 나오는 것들입니다.
흩어진 데이터를 잇는다
포인터는 데이터를 넘기는 수단이면서, 데이터끼리 이어 붙이는 수단이기도 합니다. 이 소절은 그 쓰임을 봅니다.
배열은 값을 붙여 놓습니다. 몇 번째 값이 어디 있는지 계산으로 알 수 있는 대신, 가운데에 값을 끼워 넣으려면 뒤쪽을 모두 밀어야 합니다. 값을 메모리 이곳저곳에 흩어 두고 「다음은 저기」만 적어 두면 밀 것이 없습니다. 그 「저기」를 적는 값이 포인터입니다.
값 하나와 다음을 가리키는 포인터를 묶은 덩어리를 노드라고 부릅니다. 노드를 사슬처럼 이어 만든 것이 연결 리스트입니다.
flowchart TD
subgraph 스택
H["포인터 head"]
end
subgraph 힙
A["노드 · 사과"]
B["노드 · 포도"]
C["노드 · 귤"]
end
H --> A
A --> B
B --> C
그림에서 스택에 있는 것은 포인터 하나뿐입니다. 데이터는 힙에 흩어져 있고, 화살표가 전부 포인터입니다. 트리와 그래프도 같은 방법으로 섭니다. 자식이 여럿이면 노드가 포인터를 여럿 갖습니다.
아무것도 안 가리킬 때
포인터에는 「지금은 아무것도 안 가리킨다」를 나타내는 값이 따로 있습니다. 이 값을 담은 포인터를 널 포인터라고 부릅니다. 찾는 것이 없을 때, 목록이 여기서 끝날 때 이 값을 씁니다.
널 포인터를 역참조하면 프로그램이 멈춥니다. 가리키는 데가 없는 주소를 따라간 셈이라 읽을 값이 없습니다. 그래서 포인터를 받은 쪽은 쓰기 전에 널인지 보는 검사를 넣습니다. 이 검사를 빠뜨리는 것이 포인터를 쓰는 코드에서 가장 흔한 실수입니다.
언어마다 얼마나 드러내나
포인터를 다루는 방법은 언어마다 다릅니다. 이 소절은 그 갈림을 봅니다.
C 는 포인터를 그대로 내놓습니다. 주소를 더하고 빼는 셈도 되고, 어떤 타입으로 읽을지도 프로그래머가 고릅니다. 하드웨어를 직접 다루는 코드를 쓸 수 있는 대신 실수도 사람 몫입니다.
Java · Go · Python 은 주소를 감춥니다. 객체를 가리키는 값을 쓰기는 하지만 그 값을 참조라고 부르고, 주소를 더하고 빼는 셈을 막습니다. 가리키던 데이터를 언제 치울지는 가비지 컬렉션이 맡습니다.
Rust 는 포인터를 내놓되 컴파일할 때 검사합니다. 어느 포인터가 데이터를 소유하는지, 빌려 쓰는 포인터가 원본보다 오래 남지는 않는지를 빌드 단계에서 따집니다.
세 갈래 모두 하는 일은 같습니다. 데이터의 위치를 값으로 들고 다닙니다. 다른 것은 그 값을 얼마나 만지게 해 주느냐, 치우는 일을 누구에게 맡기느냐입니다.
자주 나는 오류
포인터에서 나는 오류는 대개 「주소는 남았는데 데이터가 없다」는 한 가지 모양입니다.
| 오류 | 무슨 일이 나나 |
|---|---|
| 널 포인터 역참조 | 아무것도 안 가리키는 포인터를 따라갑니다. 프로그램이 그 자리에서 멈춥니다 |
| 댕글링 포인터 | 치운 데이터의 주소를 계속 들고 씁니다. 거기에 이미 다른 데이터가 들어와 있을 수 있습니다 |
| 메모리 누수 | 데이터를 가리키던 포인터를 잃어버립니다. 치울 방법이 없어져 메모리가 쌓입니다 |
앞의 둘은 프로그램을 멈추거나 엉뚱한 값을 읽게 만듭니다. 운영체제가 허락하지 않은 주소를 읽으면 세그멘테이션 폴트로 끊깁니다. 허락된 주소면 안 끊기고 잘못된 값을 그대로 씁니다. 뒤쪽이 더 고약합니다. 증상이 한참 뒤 엉뚱한 곳에서 나옵니다.
같은 이름으로 부르는 다른 것들
주소를 담는 값 말고도 포인터라고 부르는 것이 있습니다. 헷갈리기 쉬워 여기서 갈라 둡니다.
파일 포인터는 열어 둔 파일에서 다음에 읽거나 쓸 위치를 가리킵니다. 메모리 주소가 아니라 파일 시작점에서 몇 바이트 떨어졌는지를 가리키는 값입니다.
명령어 포인터는 다음에 실행할 기계어 명령의 주소를 담는 레지스터입니다. 담는 것이 메모리 주소라는 점은 이 항목의 포인터와 같습니다. 다만 프로그래머가 값으로 다루는 변수가 아니라 CPU(Central Processing Unit, 중앙 처리 장치)가 실행을 이어 가려고 쓰는 것입니다.
마우스 포인터는 화면에서 마우스가 가리키는 지점을 보여 주는 표시입니다. 메모리와 아무 관계가 없습니다.
관련 항목
포인터가 가리키는 메모리 영역
힙 · 스택 · 주소 공간 · 가상 메모리 · 메모리 · 데이터 세그먼트
포인터가 담는 값과 그 단위
메모리 주소 · 가상 주소 · 물리 주소 · 주소 폭 · 워드 · 엔디언 · 정렬
포인터를 다루는 연산
역참조 · 포인터 산술 · 주소 연산자 · 형 변환 · 함수 포인터
포인터로 이어 만드는 자료구조
연결 리스트 · 트리 · 그래프 · 노드 · 해시테이블 · 배열
포인터에서 자주 나는 오류·장애
널 포인터 · 댕글링 포인터 · 메모리 누수 · 이중 해제 · 해제 후 사용 · 버퍼 오버플로 · 세그멘테이션 폴트
포인터를 감싸거나 대신하는 수단
참조 · 스마트 포인터 · 약한 참조 · 핸들 · 옵셔널 · 소유권
포인터가 가리키는 메모리를 치우는 방식
가비지 컬렉션 · 참조 카운팅 · 메모리 할당 · 메모리 해제 · RAII
포인터를 드러내거나 감추는 언어
C · C++ · Go · Rust · Java · Python
포인터와 이름이 겹치는 이웃
다른 이름: pointer · 포인터 변수