Pointers Aren't Arrows. How C and C++ Actually Talk to Hardware.
포인터는 화살표가 아닙니다 — C와 C++가 메모리를 다루는 방식
C와 C++의 포인터를 메모리 주소와 바이트 단위의 관점에서 설명합니다. 포인터 연산, 배열 인덱싱, 동적 할당, 가상 메모리와 세그멘테이션 오류가 어떻게 연결되는지 예제로 살펴봅니다. 다만 포인터를 단순한 정수로 보는 설명에는 언어 표준과 구현에 따른 주의점이 있습니다.
- 주제
AI 요약
이 글은 포인터를 상자 사이를 잇는 화살표로 그리는 익숙한 비유 대신, 메모리의 주소를 저장하는 값으로 설명합니다. 메모리를 주소가 붙은 1바이트 칸의 연속으로 보면 포인터 연산과 배열 인덱싱, 동적 메모리 관리가 어떤 원리로 동작하는지 이해하기 쉽다는 접근입니다.
주소와 포인터
예를 들어 int score = 42를 선언하면 컴파일러는 score라는 이름을 실제 메모리 주소와 연결합니다. &score는 변수의 주소를 구하고, int* ptr = &score는 그 주소를 포인터 변수에 저장합니다. *ptr은 포인터에 담긴 주소의 메모리를 읽거나 쓰는 연산입니다. 글은 64비트 환경에서 여러 종류의 포인터가 각각 8바이트를 차지하는 예도 보여줍니다.
다만 포인터를 “그냥 정수”라고 단정하면 언어 차원의 의미를 놓칩니다. 포인터는 C와 C++에서 주소를 나타내는 별도 타입이며, 크기와 표현 방식은 구현에 따라 달라질 수 있습니다. 흔한 64비트 환경에서는 포인터 크기가 8바이트인 경우가 많지만 모든 플랫폼에서 보장되는 규칙은 아닙니다.
포인터 연산과 배열
int*에 1을 더하면 주소가 1바이트가 아니라 sizeof(int)만큼 이동합니다. 포인터 연산은 가리키는 타입의 크기를 기준으로 이동량을 계산하기 때문입니다. 따라서 char*는 보통 1바이트, int*는 해당 플랫폼의 int 크기만큼 이동합니다. 글은 이 규칙을 바탕으로 배열의 arr[i]가 *(arr + i)와 같은 표현이라고 설명합니다. 그래서 2[arr]도 문법상 가능하지만, 읽기 어려우므로 실제 코드에서는 쓰지 말라고 덧붙입니다.
void에는 크기가 정해져 있지 않으므로 표준 C에서는 void*에 대한 포인터 산술을 허용하지 않습니다. 일부 컴파일러는 확장 기능으로 이를 지원할 수 있지만, 이식 가능한 코드라면 먼저 구체적인 객체 포인터 타입으로 변환해야 합니다.
스택과 힙, 메모리 해제
함수의 지역 변수는 보통 스택 프레임에 놓이며, 함수가 끝나면 해당 프레임을 더는 사용하지 않습니다. 글은 x86-64의 RSP(Stack Pointer)가 이동하는 예로 스택 공간 확보와 반환을 설명합니다. 단, 실제 생성 코드와 레지스터 사용은 컴파일러와 최적화 설정에 따라 달라집니다. 스택 영역이 자동으로 초기화되는 것은 아니므로 초기화하지 않은 지역 변수의 값을 읽는 코드는 피해야 합니다.
실행 중 크기를 정하거나 함수 호출 뒤에도 유지할 데이터는 malloc으로 힙에서 할당할 수 있습니다. 할당이 끝나면 free를 호출하지만, 이 호출은 포인터 변수 자체를 지우거나 메모리 내용을 초기화하지 않습니다. 따라서 해제 뒤에도 기존 포인터를 역참조하면 Use-After-Free 문제가 생깁니다. 글은 해제 직후 buffer = NULL을 대안으로 제시합니다. 이 대입은 해당 변수의 재사용을 막는 데 도움을 주지만, 다른 포인터가 같은 메모리를 가리키고 있다면 그 포인터까지 무효화하지는 않습니다.
가상 메모리와 세그멘테이션 오류
프로그램은 보통 물리 메모리 주소를 직접 다루지 않습니다. CPU의 MMU(Memory Management Unit)는 가상 주소를 페이지 단위로 물리 메모리와 연결하고, 접근 권한을 확인합니다. 글은 널 포인터를 역참조했을 때 유효한 메모리 접근이 되지 않아 하드웨어 예외가 발생하고, 운영체제가 이를 SIGSEGV로 처리해 프로세스를 종료하는 과정을 설명합니다. 세그멘테이션 오류는 RAM 자체의 고장이라기보다 허용되지 않은 메모리 접근을 운영체제가 감지한 결과라는 설명입니다.
여기에도 구현별 차이가 있습니다. 널 포인터가 반드시 숫자 주소 0x0으로 표현된다거나, 주소 0이 모든 환경에서 항상 매핑되지 않는다고 일반화할 수는 없습니다. C와 C++가 보장하는 널 포인터의 의미와 특정 운영체제·CPU에서의 주소 매핑은 구분해야 합니다.
이중 포인터
int**는 포인터 변수를 가리키는 포인터입니다. C 함수 인자는 값으로 전달되므로 호출한 쪽의 포인터 변수 자체를 함수 안에서 바꾸려면 그 변수의 주소를 넘겨야 합니다. 글은 int**를 이용해 함수가 메모리를 할당한 뒤 호출자 쪽 포인터에 주소를 기록하는 예를 듭니다. 이때 이중 포인터를 화살표를 겹쳐 그린 그림보다 “포인터 변수의 주소를 받아 그 값을 바꾼다”고 읽으면 역할이 분명해집니다.
글의 주소 중심 설명은 포인터 연산과 배열 표기, Use-After-Free, 메모리 접근 오류를 하나의 모델로 연결합니다. 다만 비유를 실제 언어 규칙과 동일시하지 않도록 포인터 타입, 구현별 크기, 널 포인터 표현을 구분해서 이해해야 합니다.
원문: dev.to / 번역·요약: Trawling