사람들이 C언어를 공부할 때 가장 어려워하고 넘을 수 없는 장벽 같이 느끼는 부분이 바로 "포인터"입니다. 사실 포인터도 개념을 잘 이해하면 큰 무리 없이 배울 수 있습니다. 지금부터 포인터에 대해서 자세히 알아보도록 하겠습니다.
포인터(Pointer)
포인터도 마찬가지로 "특정한 값을 가지는 변수입니다." 그 특정한 값은 바로 "주소값" 입니다.
포인터는 특정한 데이터의 주소값을 저장하는 변수입니다.
포인터를 정확히 이해하기 위해서는 컴퓨터가 메모리 주소를 어떻게 지정하는지 알아야 합니다. 여기에는 "직접 주소 지정 방식"과 "간접 주소 지정 방식" 이렇게 2가지 방식이 존재합니다.
- 직접 주소 지정 방식: 말 그대로 메모리를 사용할 때 프로그래머가 사용할 메모리 주소를 직접 적는 방식입니다.
- 간접 주소 지정 방식: 메모리를 사용할 때 중간에 매개체를 사용해서 매개체를 통해 주소를 전달하는 방식을 말합니다. 예를 들어, 누군가에게 쪽지를 전달할 때 직접 주는 것이 아니라 사물함에 몰래 넣어두어 그 사람에게 전달하는 방식이 간접 주소 지정 방식과 같습니다.
포인터는 특정한 데이터의 주소값을 저장하는 변수라고 했는데, 일반 변수들도 사실 주소를 저장할 수 있습니다. 그런데 일반 변수들은 단지 그 값을 숫자로만 생각할 뿐이며 실제 해당하는 주소의 메모리에 가거나 값을 읽을 수는 없습니다. 즉, 일반 변수는 직접 주소 지정 방식으로 동작하는 것입니다.
그런데 포인터 변수는 조금 다릅니다. 포인터 변수는 "간접 주소 지정 방식"으로 동작하는 특별한 변수를 선언하기 위하여 만들어진 문법입니다. 즉, 포인터는 메모리 주소만을 저장하기 위해 탄생한 특별한 변수이며 자신이 사용하고자 하는 메모리의 '주소'를 저장하고 있는 메모리가 포인터인 것입니다.
포인터는 어떻게 선언할 수 있는 지에 대해서 알아보겠습니다. "포인터는 일반 변수와는 다르게 * 기호를 추가로 사용하여 다음과 같이 선언합니다"
// 포인터 선언하기
// (자료형) (포인터기호) (포인터 변수 이름)
int *ptr;
포인터를 선언한 것을 보면 궁금증이 생길 수 있습니다. '포인터의 자료형을 적는 부분에 int를 사용했으면 4바이트의 크기를 가지겠구나. 그렇다면 char형이나 short형을 쓰면 포인터의 크기는 각각 1바이트, 2바이트를 가지겠다.'라고 생각할 수 있으나 그렇지 않습니다. 포인터 변수의 크기는 자료형과 상관없이 항상 "4바이트"로 고정됩니다.
그렇다면 포인터 변수 앞에 자료형은 왜 붙일까요? 그 이유는 "ptr 변수에 저장된 주소에 저장될 값의 자료형을 말하는 것이기 때문입니다." 다시 말해, 포인터가 저장한 "주소"에 들어가는 실제 값의 자료형을 표현하는 것입니다. (정수형, 실수형, 문자형 등 무엇이든지 될 수 있겠지요.)
& 연산자와 * 연산자
포인터와 관련된 연산자들에 대해서 알아보도록 하겠습니다.
& 연산자
포인터 변수에는 & 연산자를 사용합니다. & 연산자는 "변수의 주소를 받아오기 위하여 사용합니다." 프로그램은 실행할 때마다 사용하는 메모리 공간이 달라지기에 직접 주소를 입력해서 사용할 수 없습니다. 만약 그렇게 한다면 컴파일러는 에러를 내뿜을 것이죠. 그렇기에 프로그램에서 선언한 다른 변수의 주소를 & 연산자를 통해 받아오는 게 안전하고 정확합니다.
int birthday; // int형 변수 birthday를 선언합니다.
int *ptr; // int형 포인터 변수를 선언합니다.
ptr = &birthday; // birthday 변수의 주소를 ptr 변수에 선언합니다.
* 연산자
* 연산자는 사실 C언어에서는 곱하기 연산을 쓸 때 사용합니다. 이때 사용하는 * 연산자는 "이항 연산자"입니다. 두 개의 항을 연산할 때 사용하는 연산자라는 뜻입니다. 포인터에서 사용되는 * 연산자는 "단항 연산자"로서의 * 연산자입니다. 이 * 단항연산자는 포인터와 밀접한 관련이 있습니다.
처음에 우리가 포인터를 선언할 때 포인터의 자료형 뒤에 * 연산자를 붙임으로서 선언했지요. * 연산자는 또 한 가지의 역할이 더 있습니다. 바로 "포인터가 가리키는 주소의 변수에 가서 값을 대입하는 것"입니다. 아래의 예시 코드를 보며 이해해보도록 합시다.
int birthday; // int형 변수 birthday를 선언합니다.
int *ptr; // int형 포인터 변수를 선언합니다.
ptr = &birthday; // birthday 변수의 주소를 ptr 변수에 선언합니다.
*ptr = 1042; // ptr에 저장된 주소에 가서 값 1042를 대입합니다. 즉 birthday는 1042라는 뜻입니다.
*ptr = 1042 이 코드를 보면 알 수 있듯이 ptr 포인터가 가리키는 주소(&birthday)의 변수인 birthday의 값에 1042라는 숫자 값 (int 값)을 집어넣은 것입니다. 이렇게 &연산자와 *연산자에 대해서 알아보았습니다. 마지막으로 총정리를 하겠습니다.
포인터는 특정한 데이터의 자료형의 "주소값"을 저장한다.
포인터는 주소값을 보관하는 데이터의 자료형에 * 를 붙임으로 선언되며,
& 연산자를 통해 특정한 데이터의 주소값을 알 수 있으며,
* 연산자를 통해 주소값에 위치한 대상 값을 지정할 수 있습니다.
포인터의 덧셈과 뺄셈
포인터의 중요한 특성으로는 "포인터는 자신이 가리킬 대상 메모리의 시작 주소만 기억하면 된다" 가 있습니다. 이런 특성 때문에 포인터의 덧셈과 뺄셈에서는 재미난 결과가 나오는데요. 이에 대해서 알아보겠습니다.
int data = 0;
int *p = &data;
p = p + 1;
이 식의 결과는 어떻게 될까요? data의 주소값에 1이 더해진 값이 나오게 될까요? 그렇지 않습니다. 포인터 변수에 + 1을 하게 될 경우 자신이 가리키는 대상의 자료형의 크기만큼 증가하게 됩니다. 즉, 위의 코드 같은 경우는 포인터 p가 가리키는 주소의 자료형은 int형입니다. int형은 4바이트의 크기를 가지기에 결과값은 data의 주소값에 4를 더해진 값이 나오게 될 것입니다.
이것이 포인터의 덧셈의 원리입니다. 뺄셈은 사실 덧셈과 원리가 같기 때문에 생략하도록 하겠습니다. 위 코드에서 덧셈이 아니라 뺄셈으로 바뀌었다면 data의 주소값에 4를 뺀 값이 나오게 될 것입니다.