자료구조 처음 배우기 시작하면 가장 먼저 순차 자료구조와 선형 리스트(배열 및 리스트)에 대해 배운다. 이 주제는 프로그래밍 언어의 문법을 학습할 때 배우는 내용과 거의 차이점이 없어 학생들에게도 부담이 적은 부분이라 생각한다.
하지만 이다음으로 순차 자료구조와 비교되는 '연결 자료구조'에 대해 배우게 되는데, 아마 이 부분이 자료구조를 처음 학습하는 학생의 입장에서 마주하는 첫 번째 고비가 아닐까 싶다. 비록 개념 자체는 단순하지만, '포인터'를 기반으로 이루어진 자료구조라, 포인터와 메모리 개념을 잘 이해하고 있지 않는다면 연결 리스트의 개념은 물론 삽입, 삭제, 탐색 연산을 이해하고 구현하는데 상당한 어려움을 겪을 가능성이 높기 때문이다.
그래서 연결 자료구조에 대해 알아보기 전에, 우선 이 포스트에선 포인터에 대한 개념을 간단하게 짚어보고자 한다.
메모리 주소의 이해
포인터를 잘 이해하려면, 모든 변수는 메모리 공간의 어딘가에 저장된다는 것과, 이 메모리의 위치를 나타내는 값인 '메모리 주소'에 대해 우선 이해하고 있아야 한다.
아래의 예시를 살펴보자.
int a = 1;
이 코드는 변수 a를 선언함과 동시에 1이라는 정수값을 할당하는 C언어 코드이다. 이때 a라는 이름을 가진 변수는 (4byte 크기의 공간을 할당받고) 컴퓨터의 메모리 어딘가에 위치하게 된다. 그리고 여기에 1이라는 값을 (2진수로) 저장한다.
이제 아래 출력문 코드를 통해 a의 값을 출력하고자 한다.
printf("a의 값 : %d\n", a);
이 코드의 결과가 1이라는 것은 굳이 프로그램을 실행해보지 않고도 바로 알 수 있다. 그렇지만 우리의 목표인 포인터를 잘 이해하기 위해 컴퓨터에서 1이 출력되는 과정을 짧게 살펴보자.
컴퓨터의 뇌라고 불리는 CPU는 변수 a의 값을 출력하기 위해 a가 있는 메모리 공간으로 찾아간다. 이때 a가 어디에 있는지를 정확히 알아야만 CPU가 올바르게 해당 위치로 갈 수 있는데, 이 위치를 알려주는 이정표 역할을 하는 것이 바로 '메모리 주소'이다. 즉 CPU는 a의 메모리 주소를 확인하고 a이 있는 곳으로 올바르게 찾아가 내부에 저장되어 있는 1의 값을 읽어 화면에 출력해 주는 것이다.
모든 메모리는 자신의 위치를 나타내주는 메모리 주소값이 존재한다. 메모리 주소는 보통 16진수로 표현한다. (ex. 0x23)
포인터의 개념 및 연산
그렇다면 무엇을 포인터라 부르는 것일까? 우선 포인터는 포인터 변수의 줄임말이다. 즉 포인터는 변수이다.
포인터는 '어떠한 값'이 아닌, 어떠한 값의 '메모리 주소'를 저장하고 있는 변수를 의미한다.
일반적으로 변수에 숫자나 문자와 같은 값이 담기는 것과 달리, 포인터 변수엔 메모리 주소값이 담긴다. 이 말은 즉슨, 포인터 변수는 특정 변수의 위치를 가리카고 있는 것이다. 그래서 이름이 포인터(pointer)이다.
다음 예시를 살펴보자.
int a;
int *b = &a;
위 코드에서 변수 b가 바로 변수 a를 가리키고 있는 포인터 변수이다. 변수 b에는 변수 a의 주소값이 담긴다. 변수 b의 자료형은 (int *)인데, 이는 'int 자료형 변수를 가리키는 포인터 자료형'이란 의미이다.
그리고 두 번째 줄의 & 연산자에 대해 잠시 주목해보자. 이 & 연산자는 변수의 주소를 가져오는 주소 연산자라 불린다.
이어서 변수 a의 주소와 값을 각각 출력하는 다음 코드를 살펴보자.
printf("변수 a의 주소 : %d", b);
printf("변수 a의 값 : %d", *b);
*은 변수의 주소가 저장된 포인터에 사용하여 해당 주소 영역의 값을 가져오는 참조 연산자이다. 즉, 포인터 변수에 참조 연산자를 사용하면, 저장된 주소값에 해당하는 위치로 액세스 할 수 있는 것이다.
이때 주의할 점은, 포인터 변수를 선언할 때 쓰이는 *와 참조 연산자 *은 서로 다른 의미를 지닌다는 것이다. 포인터 변수 선언에 쓰이는 *은 그저 자료형이 포인터 자료형임을 나타내는 기호에 불과하다. 변수 이름 앞에 *이 붙는다고 해서 해당 변수의 주소값으로 액세스 한다는 의미가 아니다.
내가 포인터를 처음 배울 때 학교에서 이 부분을 제대로 설명해주지 않아서 포인터를 이해하는 데 많은 어려움을 겪었다. 그래서 꼭 이렇게 설명을 해주고 싶었다.
// 포인터 변수 선언 시 쓰이는 *
int *b = &a;
// 참조 연산자 *
printf("변수 a의 값 : %d", *b);
// 이 둘은 서로 의미하는 바가 다르다.
이로서 포인터의 핵심 개념만을 한 번 짧고 간결하게 정리해보았다. (이중 포인터, 포인터 배열 등등의 응용 개념은 추후에 다룰 자료구조들을 이해하는 데 많이 중요한 개념은 아니라고 생각하여 굳이 다루진 않았다.)
포인터 개념은 정말 응용도 많이 되고, 프로그래밍을 할 때 안쓰이는 곳이 없을 만큼 매우 매우 중요한 개념이다. 그러니 한 번 확실히 공부해서 꼭 본인의 지식으로 만들어두길 바란다. 그렇지만 조급할 필요는 없다고 생각한다. 나도 처음 포인터를 배울 때 잘 이해하지 못했다. 그러니 어렵다고 포기하지 말고 정 이해가 잘 안 되면 시간 텀을 두고 다시 공부해 보길 바란다. 그렇게 하면 '아하 이게 이런 개념이구나'하고 깨닫는 모먼트가 반드시 올 것이다.
다음 포스트에서는 포인터의 기본 개념을 바탕으로 연결 자료구조와 연결 리스트에 대해 정리해 볼 예정이다.