배열과 포인터
배열은 자료형이 같은변수를 메모리에 연속으로 할당합니다. 따라서 각 배열 요소는 일정한 간격으로 주소를 갖게 됩니다.
예를 들어 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 |