C 언어 | 포인트 | 포인터 연산

포인터에 저장되는 주소는 실질적으로 정수이므로 포인터를 연산하여 참조를 변경할 수 있다. 이를 응용하여 배열과 같은 구조적인 데이터에 대한 포인터에서 모든 요소를 가르키 있습니다.

주소를 계산하기

포인터는 메모리 주소를 저장하는 특수한 변수이다. 포인터도 다른 변수와 동일하게 데이터 형이 존재하고, 변수처럼 취급할 수 있다. 중요한 것은 포인터의 실체는 간단한 정수 값이며,이를 산술 계산을 할 수 있다는 것이다.

하지만 엉터리 숫자를 메모리 주소로 참조해서는 그 정보에 의미는 없다. 변수 등에서 취득한 올바른 주소 이외를 간접 참조하는 것은 프로그램의 충돌로 이어진다.

포인터 연산은 연속된 메모리 영역을 확보하고 있는 주소에 유효한 것이다. 연속된 메모리 영역은 예를 들어 배열이다. 배열과 같은 연속적인 공간을 가진 합성체는 메모리 주소도 연속하고 있기 때문에 그 범위를 예상할 수 있다. 따라서 인덱스를 사용하는 대신 포인터를 연산하여 특정 요소를 볼 수 있게 되는 것이다.

코드1

#include <stdio.h>

int main() {
  char chStr[] = "Kitty on your lap";
 int iCount;

 for(iCount = 0 ; chStr[iCount] ; ++iCount)
    printf("&chStr[%d] = %p\n" , iCount , &chStr[iCount]);
 return 0;
}

코드1은 chStr 배열의 각 요소의 메모리 주소를 순서대로 표시한다. 이 결과에서 흥미로운 것은 배열과 주소의 관계이다. 재미있는 것은 주소가 배열의 처음부터 순서대로 단순 증가하고 있다.

이것은 배열이 연속된 메모리 영역을 확보하고 있음을 나타낸다. 이 특징을 잘 이용하면 포인터를 산술 연산자로 계산하여 간접 참조를 할 수 있다. 위의 예제에서 생각해 보면, chStr[0]의 주소에 다섯을 가산하면 chStr[5]의 주소와 동일하다.

코드2

#include <stdio.h>

int main() {
 int iArray[] = { 10 , 100 , 1000 };
 int *iPo = &iArray[0] + 1;

  printf("*iPo = %d\n" , *iPo);
  return 0;
}

코드2는 배열이 연속된 주소를 가지는 것을 이용하여, iPo 포인터에 iArray 배열 변수의 시작 주소를 할당 할 때, 덧셈 연산자 +를 사용하여 메모리 주소에 1을 가산하고 있다. 그 결과, 본래는 iArray[0]을 나타내는 것이었던 주소가 그 다음에 iArray[1]로 변경된다. printf()에서 iPo 포인터에서 간접 참조 값을 출력하고 있는데, 결과에서 포인터가 iArray [1]을 나타내고 있음을 확인할 수 있다.

포인터 연산에서는 모든 주소가 1바이트 단위로 할당한다는 점에 유의해야 한다. 32비트 컴퓨터에서 int는 4바이트의 메모리 공간을 확보한다. 이 경우 int 형 변수 다음의 주소는 이 변수의 주소에 4를 가산한다. 코드1의 실행 결과가 단순히 1씩 증가되어 있던 것은 chStr 배열 변수가 1바이트의 char 형 이었기 때문이다.

코드3

#include <stdio.h>

int main() {
  int iArray[] = { 2 , 4 };

 printf("&iArray[0] = %p\n" , &iArray[0]);
  printf("&iArray[1] = %p\n" , &iArray[1]);

  return 0;
}

코드3와 코드1의 결과와는 달리, 주소가 4단위로 증가하고 있는 것을 알 수 있다. 이 결과는 int 형이 4바이트의 컴퓨터에서 실행한 것이다. iArray 배열 변수는 int 형이기 때문에 각 요소가 4바이트 단위로 다루어지고 있는 것이다.

다행히도, 개발자는 이 사실을 거의 신경 쓸 필요는 없다. 예를 들어 iArray[0] 포인터 iPointer이 존재하는 것처럼, 이 포인터를 사용하여 iArray[1]의 주소를 계산하려면, iPointer += 4 로 할 필요는 없다. 직관적으로 iPointer += 1으로 한다.

“포인터"절에서도 설명했지만, 포인터 형은 컴파일러가 참조하는 주소를 몇 바이트 단위로 취급해야 하는지 판단하기 위해 이용된다. 컴파일러는 주소의 계산을 수행하면, 포인터의 형태에 따라 올바른 것라고 생각할 수 있는 연산 결과를 산출한다. 32비트 컴퓨터에서 int 형 변수에 대한 포인터에 1을 가산 한 경우 실제 메모리 주소는 4가 가산되는 구조로 되어 있다.

즉, 포인터에 대한 연산이 행해진 경우 암시적으로 정수로 포인터의 크기를 곱한 값이 계산된다. 컴파일러가 이 계산을 해주기 때문에, 개발자는 포인터 형에 따른 번거로운 메모리 주소 계산을 할 필요가 없다.

코드4

#include <stdio.h>

int main() {
 int iArray[] = { 2 , 4 };
 int *iPo = &iArray[0];

  printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);
 iPo += 1;
 printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);

 return 0;
}

주소는 실행했을 때 상황에 따라 다르지만, 그것은 중요하지 않다. 결과를 보면, 프로그램을 실행했을 때 iArray 배열 변수의 선두 요소에 할당된 메모리 주소는 0033FDA4라는 주소 이었다. int 형는 32비트 컴퓨터에서 실행되고 있다고 가정하고 0033FDA4, 0033FDA5,0033FDA6,0033FDA7 4 바이트을 iArray[0]가 점유하고 있다고 생각된다.

프로그램은 iArray 배열 변수의 첫번째 요소에 대한 포인터 iPo가 정의되어 있다. 먼저 iPo의 포인터가 가리키는 값으로 저장하는 메모리 주소를 표시하지만, 이것은 당연히 iArray[0]의 값 2와 주소 0033FDA4가 표시된다. 다음 iPo += 1을 의해 iPo에 정수1을 가산하고 있지만만, 여기에서는 iPo 값이 0033FDA5 대신 0033FDA8이 되는 것이 포인트이다.

이것은 두번째 printf() 함수의 결과에서 확인할 수 있다. iPo의 주소에 1을 더하면 int 형의 크기만큼 메모리 주소가 나아가서, 정확하게 두번째 요소 iArray[1]을 가리킨다.