C언어 포인터, 택배기사 비유로 단숨에 이해하기
C언어를 공부하다 보면 어김없이 등장하는 난관, 바로 포인터입니다. 하지만 포인터의 기본 원리만 제대로 이해하면 결코 어렵지 않습니다. ‘가리킨다’는 의미를 기억하며 택배기사의 배송 과정을 떠올려 보세요.
포인터(pointer), 이름에 담긴 비밀
먼저 ‘point’라는 영어 단어의 뜻을 생각해볼까요? ‘가리키다’, ‘지시하다’라는 의미를 가지고 있습니다. 여기에 사람이나 도구를 의미하는 접미사 ‘-er’가 붙어 ‘pointer’, 즉 ‘가리키는 것’이라는 뜻이 됩니다. 이름 그대로 무언가를 가리키는 역할을 하는 것이죠.
포인터의 원리: 주소와 택배기사
포인터의 원리는 아주 간단합니다. 우리가 사는 집의 주소와 그 주소로 물건을 배송하는 택배기사를 생각하면 쉽습니다.
- 주소: 메모리 공간의 고유한 위치 값 (예:
0x12345678
) - 택배기사 (포인터): 주소를 알고 있는 존재
- 배송 (write): 택배기사가 주소로 찾아가 물건을 내려놓는 것
- 반품 수거 (read): 택배기사가 주소로 찾아가 물건을 가져오는 것
코드를 통해 더 자세히 알아볼까요?
int* addr = 0x12345678;
int*
는 “int, 즉 정수형 데이터의 주소를 가리킬 거야!”라고 알려주는 포인터 타입입니다.
int* addr;
코드는 ‘주소’를 담을 수 있는 addr
이라는 변수(메모리 공간)를 만드는 과정입니다. addr
은 이제 주소를 저장하는 택배기사의 ‘수첩’과 같습니다.
addr = 0x12345678;
는 addr
이라는 수첩에 0x12345678
이라는 주소를 적어 넣는(write) 행위입니다. 이제 addr
은 해당 주소를 정확히 기억하게 됩니다.
잠깐! 실제 프로그래밍에서는?
여기서 한 가지 중요한 점을 짚고 넘어가겠습니다. 개념을 설명하기 위해 addr = 0x12345678;
처럼 임의의 숫자를 주소로 사용했습니다.
C언어의 동작을 자세히 들여다보면, 사실 addr = 0x12345678;
이 코드 한 줄만으로는 컴파일이나 실행 시 오류가 발생하지 않습니다. 이 코드는 그저 포인터 변수 addr
(택배기사의 수첩)에 0x12345678
이라는 숫자(주소)를 적는 행위일 뿐이기 때문입니다.
진짜 문제는 *addr
을 사용해서 그 주소로 실제로 찾아가려고 ‘시도’할 때 발생합니다.
// 이 코드 자체는 문제가 없습니다.
// 그냥 '알 수 없는 주소'를 적어두는 것 뿐입니다.
addr = 0x12345678;
// 바로 이 줄에서 에러가 발생합니다!
// addr에 적힌 주소로 찾아가 값을 쓰려고 시도하기 때문입니다.
*addr = 1234;
현대의 운영체제(Windows, macOS, Linux 등)는 프로그램을 보호하기 위해 각 프로그램이 사용할 수 있는 메모리 영역을 엄격하게 통제합니다. 따라서 허가받지 않은 주소(0x12345678
같은)에 접근하려고 시도하면 운영체제가 이를 감지하고 프로그램을 강제로 종료시킵니다. 이것이 바로 우리가 흔히 만나는 ‘Segmentation fault(메모리 침범)’ 오류입니다.
마치 택배기사가 수첩에 존재하지 않는 주소를 적는 것은 가능하지만, 그 주소로 실제로 배송을 하려고 하면 경비 시스템에 막히는 것과 같습니다.
따라서 ‘주소 할당’ 자체는 가능할지라도, 결국 사용할 수 없는 주소를 가리키는 포인터는 위험하고 의미가 없기 때문에 실제 프로그래밍에서는 이런 방식을 사용하지 않습니다.
그럼 일반적인 환경에서는 어떻게 주소를 얻을까요? 바로 &
연산자를 사용해 이미 존재하는 변수의 주소를 가져옵니다.
int a = 100; // 100이라는 값이 담긴 'a'라는 변수(주택)를 만듭니다.
int* addr; // 주소를 담을 포인터 변수(택배기사)를 준비합니다.
addr = &a; // &a는 변수 a의 실제 메모리 주소(주택 주소)를 의미합니다.
// 이제 addr은 a의 주소를 가리키게 됩니다.
이렇게 &
연산자로 실제 운영체제가 허용한 변수의 주소를 얻어와 사용하는 것이 훨씬 안전하고 일반적인 방법입니다.
별표(*)의 두 가지 의미: 읽기(read)와 쓰기(write)
이제 포인터의 핵심, 별표(*
, 애스터리스크) 연산자를 살펴보겠습니다. 이 연산자는 addr
이 가리키는 실제 주소로 ‘찾아가는’ 역할을 합니다.
1. 주소 찾아가서 데이터 읽기 (read)
*addr;
*addr
은 addr
에 저장된 주소(바로 위 예제의 a
변수 주소)로 찾아가서 그곳에 있는 데이터를 읽어옵니다. addr
이 a
의 주소를 가지고 있으므로, *addr
은 변수 a
의 값인 100
을 읽어오게 됩니다.
이때 중요한 점은 ‘얼마나’ 읽어올 것인가 입니다. addr
은 int*
타입이므로, 컴퓨터는 해당 주소부터 int
자료형의 크기(보통 4바이트)만큼 데이터를 읽어옵니다.
2. 주소 찾아가서 데이터 쓰기 (write)
*addr = 1234;
이번에는 대입 연산자 =
와 함께 쓰였습니다. 이 코드는 addr
이 가리키는 주소(바로 a
의 주소)로 찾아가서 그 공간에 1234
라는 값을 써넣습니다(write). 이 코드가 실행되면 변수 a
의 값이 100
에서 1234
로 변경됩니다.
마찬가지로 addr
이 int*
타입이기 때문에 sizeof (int)
크기만큼의 공간에 1234
라는 값을 기록합니다.
어떤가요, 간단하지 않나요? 포인터는 결국 ‘주소를 저장하는 변수’이며, 별표(*
)를 통해 그 주소에 찾아가 값을 읽거나 쓰는 도구일 뿐입니다. 택배기사가 주소를 보고 물건을 배송하거나 수거하는 것처럼 말이죠. 이제 포인터에 대한 막연한 두려움을 버리고 C언어 정복에 한 걸음 더 다가가시길 바랍니다!