C 언어 | 포인트 | 배열과 포인터


배열을 가리키는 포인터를 계산하고, 어떤 요소에 간접 참조하는 방법을 설명한다.

배열에 간접 참조

지금까지 배열의 첫번째 요소의 주소를 얻기 위해 &iArray[0]과 같이 쓰는 방법으로 사용해 왔다. 그러나 사실은 배열 변수라는 것은 특별한 변수 이름만 지정하면 배열의 위로의 주소를 나타내는 것으로 되어 있다. 예를 들어, 지금까지는 배열 변수 iArray의 선두 요소의 메모리 주소를 얻으려면 다음과 같이 기술하였다.

int *iPo = &iArray[0];

이는 틀린 것은 아니지만, 이보다 단순히 배열 이름에서 선두의 주소를 가져올 수 있다. 이는 다음과 같이 쓸 수도 있다.

int *iPo = iArray;

자세히 다루지 않았지만, 이전 printf() 함수에 %s으로 문자열을 표시하는 경우, 배열 이름만 전달했던 이유가 여기에 있다. printf() 함수의 서식 지정 문자 %s에 대응하는 인수는 문자 배열에 대한 포인터해야만 한다. 일반적으로 주소 연산자를 사용하여 주소를 가져오지만, 문자 배열의 경우는 붙어있지 않는다. 왜냐하면 배열 변수의 경우, 인덱스 생략의 변수 이름은 배열의 시작 주소를 나타내기 때문이다.

그러나 배열 변수의 변수 이름은 포인터가 아니므로 착각하지 않도록 주의하자. 인덱스를 생략한 배열 변수의 이름은 선두 요소의 주소를 나타내는 상수와 같은 것이며, 이에 어떤 값을 대입할 수는 없다.

코드1

#include <stdio.h>

int main() {
 char chStr[] = "Kitty on your lap";
 printf("&chStr[0] = %p : chStr = %p\n" , &chStr[0] , chStr);

 return 0;
}

코드1은 &chStr[0]chStr가 동일한 주소를 나타내고 있음을 증명하고 있다. printf() 함수는 &chStr[0]chStr을 인수 받아, 이를 16진수로 화면에 표시하면, 프로그램은 동일한 수치를 화면에 표시한다.

이 특성을 이용하면 인덱스 대신에 간접 연산자 *를 사용하여 배열을 제어할 수 있다. 배열이 메모리 상에 어떻게 배치되어 있는가하는 본질에 관련되어 있기 때문에, 이 방법에 의한 요소에 대한 액세스를 이해하는 것은 매우 중요하다. 다음 문은 배열 변수 iArray 중 iArray[2]에 간접 연산자를 사용하여 참조한다.

*(iArray + 2)

이 때 주의해야 하는 것이, 간접 연산자 *는 덧셈 연산자 +보다 우선 순위가 높기 때문에 괄호를 빼버리면 먼저 *iArray이 연산된다. 따라서 괄호로 iArray + 2를 먼저 계산하도록 작성해야 한다. 이와 같이, 연산과 간접 참조가 동일한 식에 있으면, * 연산자 우선 순위에 주의해야 한다. 특히 증가 연산자와 간접 참조 연산자를 동시에 사용하는 경우 *iPo++로 작성하면 iPo 포인터의 주소를 증가하는 것을 나타내며, (*iPo)++로 작성하면 iPo가 가리키는 내용을 증가하는 것을 나타내는 것이다. 이러한 미묘한 차이에 당황하는 경우가 많기 때문에, 조심해야 한다.

코드2

#include <stdio.h>

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

 printf("간접참조=%d,%d,%d\n", *iArray , *(iArray + 1) , *(iArray + 2));
  printf("첨자지정=%d,%d,%d\n" , iArray[0] , iArray[1] , iArray[2]);
 return 0;
}

코드2를 실행하면 *(iArray + 1)iArray[1]이나 *(iArray + 2)iArray[2]가 같은 값을 참조하는 것을 확인할 수 있다.

실질적으로 iArray[index]라는 인덱스 연산은 *(iArray + index)와 동일한 조작이다. 따라서 iArray[index]*(iArray + index)는 동일하다고 생각할 수 있다. 마찬가지로, &iArray[index] 문장은 iArray + index와 동일하다. 이것은 포인터와 배열의 관계에서 가장 중요한 관계이기 때문에 충분히 이해해야 한다.

이 특성을 반대로 생각하면, 인덱스를 지정하고 배열 변수의 요소에 액세스하는 것은 간접 참조이라는 것이다. 결국은 배열의 주소를 저장하는 포인터 iPo에서도 iPo[index]와 같이 인덱스 지정에 간접 참조를 할 수 있다.

다차원 배열과 포인터

저장 공간 및 주소의 관계는 1차원 배열과 같은 것이다. 다차원 배열은 프로그래밍 언어가 개념적 계층을 위한 것이며, 물리적으로 (기계어 수준) 다원화되는 것은 아니다. 2개의 요소를 가진 2차원 배열 Array [2] [2]는 4개의 요소를 가진 배열 Array[4]와 요소의 수를 동일하기 때문에 할당된 메모리 크기는 같다.

코드3

#include <stdio.h>

int main() {
 char chArray[2][2] = { 2 , 4 , 8 , 16 };

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

  return 0;
}

코드3은 다차원 배열의 주소 구조를 이해하기 위한 것이다. 먼저 첨자를 생략한 배열 이름이 항상 배열의 시작 주소를 나타내는 것은 동일하다. 2차원 배열에서 배열의 선두는 [0] [0] 요소이다. chArray 돌려주는 값이 &chArray[0] [0]가 반환된 값과 동일하다는 것을 확인할 수 있다.

주목해야 하는 것은 각 요소가 반환 메모리 주소이다. 2차원 배열이라고 해도, 할당된 메모리는 일렬로 되어 있다는 것을 이 결과에서 확인할 수 있다. 다차원 배열도 보통의 1차원 배열처럼 연속된 메모리 영역에 할당된다.

이 특성을 알면, 다차원 배열에서도 목적의 차원과 요소에 포인터에서 액세스할 수 있다. 예를 들어 2차원 배열에 있어서 다음과 같은 계산식을 이용하는 방법이 있다.

*(iPointer + (일차원 첨자 * 이차원 요소 수) + 이차원 첨자)

반복 문장의 카운터를 이용하여 다차원 배열을 처리하는 일이 요구된 경우는 이러한 방법으로 간접 참조할 수 있을 것이다. 그러나 다차원 배열의 첨자를 생략한 변수 이름에서 간접 참조를 시도하면 뜻밖의 결과를 얻을 수 있다. 다음 문장은 아마 예상한 것과 다른 결과가 될 것이다.

int iArray[2][2] = { 1 , 2 , 3 , 4 };
printf("%d" , *iArray);

이 경우 iArray는 배열의 선두 iArray[0] [0]의 주소를 보여주기 위해, *iArray는 1이라는 결과를 간접 참조해 줄 것을 기대하고 있지만, 전혀 다른 값을 표시한다. 첨자를 생략한 변수 이름 iArray을 printf()함수에서 예시로 표시시켜 보면, 확실히 &iArray[0][0]에 동일한 비록 간접으로 얻을 수 있는 값은 iArray[0][0]에 저장되는 값이 아니다. 이것은 매우 이상한 현상이다.

이전, iArray[index] 문은 *(iArray + index)와 동일하다고 설명했지만 ,이를 역으로 생각하면 다차원 배열의 *iArrayiArray[0]에 동일한 것이다. 이것은 차원을 지정하고 있지만 요소를 지정하지 않는다. 2차원 배열 변수에서 iArray[0]을 지정한 경우는 iArray[0][0]의 주소를 돌려준다. 따라서, 위에 예문은 기대한 결과 1 대신, &iArray[0][0]을 표시하게 되는 것이다.

이것을 해결하는 하나의 방법으로는 iArray 2차원 배열 변수를 (int *)로 캐스팅해 버리는 것이다. 그러면 차원을 신경 쓸 필요없이 직접 int 변수에 대한 간접 참조로 각 요소에 액세스할 수 있다.

코드4

#include <stdio.h>

int main() {
  int iCount1 , iCount2;
  int iArray[][9] = {
   { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ,
   { 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 } ,
    { 0 } ,
 };
  int *iPo = (int *)iArray;

 for(iCount1 = 0 ; *(iPo + (iCount1 * 9)) ; iCount1++) {
   for(iCount2 = 0 ; iCount2 < 9 ; iCount2++) {
      printf("%d " , *(iPo + (iCount1 * 9) + iCount2));
   }
   printf("\n");
  }

 return 0;
}

코드4는 for 문을 사용하여 2차원 배열을 처리하지만, 배열 액세스는 인덱스가 아닌, 포인터의 연산에 의한 간접 참조로 실현하고 있다. iArray을 직접 연산한 경우는 간접 참조를 하여도 요소 주소가 반환되기 때문에, 한번 int 형 포인터 iPo에 캐스팅하고 이를 조작하고 있다. 형식 변환은 명시적으로 할 필요는 없지만 컴파일러에서 잘못된 포인터 조작임을 경고될 가능성이 있기 때문에 (int *)iArray와 같이 명시적으로 변환한다.

이 프로그램은 iArray의 각 요소를 화면에 표시하고 첫 번째 요소가 0 인 경우에 처리를 종료한다.

함수와 포인터

표준적인 설계는 배열이나 구조체 등의 대용량 데이터를 함수에 전달하는 방법으로 포인터를 이용한다. 포인터를 이용하지 않는 이유는 값을 반환하지 입력 목적의 단순 형식을 사용하는 경우에만, 예를 들면 char, int, double 등 형태의 입력할 경우이다.

문자열과 데이터 배열을 받거나 호출자에게 돌려주는 경우, 함수는 포인터를 받아야한다 생각되고 있다. 포인터가 아닌 일반 배열로 전달할 수도 있는데, 이 경우 크기가 명확하게 결정되어 있어야 해서 불편하다. 포인터 이외의 방법으로 합성체를 전달하려면 함수를 호출 할 때마다 배열 등을 통째로 복사해야 되고, 이것은 실행 속도와 메모리에 심각한 부담을 주게 된다. 이 방법에 장점이 없기 때문에, 상급자는 망설임없이 포인터 정보를 교환해야 한다고 판단하는 것이다.

코드5

#include <stdio.h>

void stringToLower(char *chpStr) { 
 int iCount;
 for(iCount = 0 ; *(chpStr + iCount) ; iCount++) {
   if (*(chpStr + iCount) > 0x40 && *(chpStr + iCount) < 0x5B)
   *(chpStr + iCount) += 0x20;
 }
}

int main() {
  char chStr[] = "~KITTY on YOUR lap~";
 stringToLower(chStr);

 printf("%s\n" , chStr);
  return 0;
}

이 프로그램의 함수 tolower()에는 인수 문자열에 대한 포인터를 지정한다. tolower() 함수는 주어진 포인터로부터 문자에 간접 참조하고, 그 문자가 대문자인지 여부를 ASCII 코드의 성질을 이용하여 조사하고 있다. ASCII 코드의 대문자에 16진수 0x20를 추가하면 그대로 소문자가 되기 때문에, 이를 이용하여 문자열 내에 대문자가 존재하는 경우에 이를 소문자로 변환한다. 즉,이 함수의 역할은 주어진 문자 배열의 문자로 변환할 수 있는 모든 문자를 소문자로 하는 것이다.

덧붙여서, tolower 함수 정의의 매개 변수에

char *chpStr

라는 선언은 다음과 같이 설명할 수 있다.

char chpStr[]

이 두개는 함수의 매개 변수에만 동일하다고 해석되지만, 전자 char *chpStr이라는 기법이 인수가 포인터임을 명확하게 보여주고 있기 때문에 권장된다.