C (5) - 배열과 포인터 ②

배열과 포인터

 

배열은 자료형이 같은변수를 메모리에 연속으로 할당합니다. 따라서 각 배열 요소는 일정한 간격으로 주소를 갖게 됩니다. 

예를 들어 int arr[5]; 의 배열이 메모리 100번지부터 할당되고 int형 변수의 크기가 4바이트라면 각 배열 요소의 주소는 100, 104, 108, 112, 116번지가 됩니다.

결국 첫 번째 요소의 주소를 알면 나머지 요소의 주소도 쉽게 알 수 있고 각 주소에 간접 참조 연산을 수행하면 모든 배열 요소를 사용할 수 있습니다. 따라서 컴파일러는 첫 번째 배열 요소의 주소를 쉽게 사용하도록 배열명을 컴파일 과정에서 첫 번째 배열 요소의 주소로 변경합니다.

 

배열명으로 배열 요소의 주소 구하기

 

주소는 정수처럼 보이지만 자료형에 관한 정보를 갖고있는 특별한 값입니다. 따라서 연산을 자유롭게 할 수 없고 정해진 연산만 가능합니다. 정수 덧셈이 대표적인데, 다음과 같이 독특한 방식으로 수행됩니다.

위 그림처럼 4바이트인 int형 변수 a의 주소 100번지에 1을 더한 결과는 101이 아닌 104가 됩니다. 물론 연산 결과 또한 주소가 됩니다.

 

이런 연산 규칙은 배열을 사용할 때 유용합니다. 배열명도 주소이므로 정수를 차례로 더하면 연속된 배열 요소의 주소를 구할 수 있고 여기에 간접 참조 연산을 수행하면 모든 배열 요소를 사용할 수 있습니다.

 

배열명을 주소로 활용하는 예를 살펴보겠습니다.

#include <stdio.h>

int main(void)
{
    int arr[2];
    int i;

    *(arr + 0) = 10;
    *(arr + 1) = *(arr + 0) + 10;

    printf("%3d", arr[0]);
    printf("%3d", arr[1]);

    return 0;
}

위 예제를 보면, 배열의 대괄호([ ])는 포인터 연산의 '간접 참조, 괄호, 더하기' 연산 기능을 갖는다는 것을 알 수 있습니다.

배열 요소를 사용할 때는 상황에 따라 대괄호나 포인터 연산식 중 적당한 것을 골라 쓰면 됩니다. 특별한 경우가 아니라면 대괄호를 사용하는 것이 편하며, &arr[1] 와 같은 경우 arr + 1로 쓰면 연산 과정을 줄일 수 있습니다.

 

물론 배열의 할당 영역을 벗어나는 포인터 연산식도 문법적으로는 문제가 없으므로 컴파일은 되나 실행했을 때의 결과를 예상할 수 없기 때문에 사용에 주의해야 합니다.

 

배열명 역할을 하는 포인터

 

배열명은 주소이므로 포인터에 저장할 수 있습니다. 이 경우 포인터로도 연산식이나 대괄호를 써서 배열 요소를 쉽게 사용할 수 있습니다.

int main(void)
{
    int arr[3];
    int *pa = arr;
    int i;

    *pa = 10;
    *(pa + 1) = 20;
    pa[2] = pa[0] + pa[1];

    return 0;
}

만약 배열이 메모리 100번지부터 할당되었다면 배열명 arr의 주소 값은 100번지가 되고, 포인터 pa는 100을 저장해 첫 번째 배열 요소를 가리키는 상태가 됩니다.

또한 9행은 다음과 같이 다양하게 표현할 수 있습니다.

 

배열명과 포인터의 차이

 

포인터가 배열명처럼 쓰이므로 배열명과 포인터가 서로 비슷해 보이지만, 다른점이 더 많습니다.

 

1. sizeof 연산의 결과

배열명에 사용하면 배열 전체의 크기를 구하고 포인터에 사용하면 포인터 하나의 크기를 구합니다.

int arr[3];
int *pa = arr;

sizeof(arr)	// 12바이트
sizeof(pa)	// 4바이트

 

2. 변수와 상수

포인터는 그 값을 바꿀 수 있지만, 배열명은 상수이므로 값을 바꿀 수 없습니다. 즉, 포인터 pa에 1을 더해 다시 pa에 저장할 수 있으나, 배열명 arr에 1을 더한 후 그 값을 다시 저장하는 것은 불가능합니다. 

pa = pa + 1	// (o)
pa++		// (o)

arr = arr + 1	// (x)
arr++		// (x)

pa에 1을 더하면 두 번째 배열 요소의 주소 104번지가 되므로 이 값을 다시 pa에 저장하면 pa가 두 번째 배열 요소를 가리키게 됩니다.

포인터로 배열의 데이터를 처리할 때는 포인터의 값이 변할 수 있으므로 유효한 값인지 확인하는 습관이 필요합니다.

 

증가 연산자와 간접 참조 연산자

 

다음처럼 포인터로 배열 요소를 차례로 출력할 때 증가 연산자와 간접 참조 연산자를 함께 사용하는 방법도 있습니다.

for (i = 0; i < 3; i++)
{
    printf("%d ", *(pa++));
}

이 경우 pa++는 후위 증가 연산자이므로, 연산자 우선순위에 따라 pa의 값은 먼저 증가합니다.
하지만 후위형이기 때문에 다음 연산인 간접 참조(*)를 수행할 때는 증가 이전의 주소 값이 사용됩니다.

 

다음은 포인터 연산과 간접 참조의 조합에 따른 차이를 비교한 네 가지 예시입니다.

  • *(pa++)
  • *(++pa)
  • ++(*pa)
  • (*pa)++

각 표현식이 어떻게 동작하는지 자세히 살펴보겠습니다.

1. *(pa++)

 

2. *(++pa)

포인터에 증가 연산자와 간접 참조 연산자를 함께 사용할 때 전위 표현을 사용하면 안됩니다.

3. ++(*pa)

4. (*pa)++

 

포인터의 뺄셈과 관계 연산

 

포인터에는 정수 덧셈이나 증가 연산 외에도 다양한 연산을 수행할 수 있습니다. 예를 들어 가리키는 자료형이 같으면 포인터끼리의 뺄셈이 가능합니다. 물론 일반 뺄셈과는 다른 방식으로 연산됩니다.

포인터 - 포인터  =  값의 차 / 자료형의 크기
#include <stdio.h>

int main(void)
{
    int arr[5] = { 10, 20, 30, 40, 50};
    int *pa = arr;
    int *pb = pa + 3;

    printf("pa : %u\n", pa);	// 3799428
    printf("pb : %u\n", pb);	// 3799440

    printf("pb - pa : %u\n", pb - pa);	// 3

    return 0;
}

위 예제는 포인터 pa와 pb로 배열 arr의 각각 다른 배열 요소를 가리키도록 한 후 포인터의 뺄셈을 수행합니다.

 

위 그림에서 설명의 편의성을 위해 포인터 출력 결과에서 끝의 2자리만을 사용하였습니다. 

뺄셈 결과는 배열 요소 간의 간격 차이를 의미합니다. 따라서 포인터 pa와 pb가 가리키는 배열 요소의 위치가 3개 떨어져 있음을 알 수 있습니다.

 

배열을 처리하는 함수

 

배열명을 꼭 포인터에 넣는 방식으로 배열을 처리할 필요는 없습니다. 하지만 함수로 배열을 처리하려면 포인터가 필요합니다. arr 배열에서 배열명 arr은 첫 번째 배열 요소의 주소입니다. 이 주소 값을 함수의 인수로 주면, 함수는 이 값을 받아 주소 계산을 통해 모든 배열 요소를 사용할 수 있습니다. 이때 배열명을 받을 함수의 매개변수 자리에 포인터가 필요합니다.

#include <stdio.h>

void print_any(int *pa);

int main(void)
{
    int arr[5] = { 10, 20, 30, 40, 50 };
    print_any(arr, sizeof(arr)/sizeof(arr[0]);

    return 0;
}

void print_any(int *pa, int size)
{
    int i;

    for (i = 0; i < size; i++)
    {
    	printf("%d ", pa[i]);
    }
}

이때 함수의 매개변수 자리에 포인터 대신에 배열을 선언해도 배열의 저장 공간이 할당되지 않으며, 배열명은 컴파일 과정에서 첫 번째 배열 요소를 가리키는 포인터로 바뀝니다.

일반적으로 초기화 없이 빈 대괄호를 갖는 배열을 선언할 때 컴파일 과정에서 에러가 발생하지만, 함수의 매개변수에 빈 대괄호를 가지는 배열을 선언할 경우에는 컴파일 과정에서 포인터로 변경되므로 에러가 발생하지 않습니다.

'Lang > C' 카테고리의 다른 글

C (7) - 변수 영역과 데이터 공유  (0) 2025.08.21
C (6) - 문자와 문자열  (2) 2025.08.18
C (4) - 배열과 포인터 ①  (3) 2025.08.11
C (3) - 제어문과 함수  (4) 2025.08.07
C (2) - 연산자  (3) 2025.08.03