학습 목표 : 배열의 데이터를 자유롭게 다루기 위해서는 배열의 구현 원리를 이해할 필요가 있습니다. 이 절에서는 배열과 포인터의 관계를 밝히고 포인터로 배열을 사용하는 방법을 살펴보겠습니다.
배열은 자료형이 같은 변수를 메모리에 연속으로 할당합니다. 따라서 각 배열 요소는 일정한 간격으로 주소를 갖게 됩니다. 예를 들어 int ary[5]; 의 배열이 메모리 100번지부터 할당되고 int형 변수의 크기가 4바이트라면 각 배열 요소의 주소는 100, 104, 108, 112, 116번지가 됩니다. 결국 첫 번째 요소의 주소를 알면 나머지 요소의 주소도 쉽게 알 수 있고, 각 주소에 간접 참조 연산을 수행하면 모든 배열 요소를 사용할 수 있습니다. 따라서 컴파일러는 첫 번째 배열 요소의 주소를 쉽게 사용하도록 배열명을 컴파일 과정에서 첫 번재 배열 요소의 주소로 변경합니다.
배열명으로 배열 요소 사용하기
주소는 정수처럼 보이지만 자료형에 대한 정보를 갖고 있는 특별한 값입니다. 따라서 연산을 자유롭게 할 수 없고 정해진 연산만 가능합니다. 정수 덧셈이 대표적인데, 다음과 같이 독특한 방식으로 수행됩니다.
주소 + 정수 → 주소 + (정수 * 주소를 구한 변수의 크기)
예를 들어 크기가 4바이트인 int형 변수 a의 주소 100번지에 1을 더한 결과는 101이 아닌 104가 됩니다. 물론 연산 결과 또한 주소가 됩니다. 이런 연산 규칙은 배열을 사용할 때 유용합니다. 배열명도 주소이므로 정수를 차례로 더하면 연속된 배열 요소의 주소를 구할 수 있고 여기에 간접 참조 연산을 수행하면 모든 배열 요소를 사용할 수 있습니다. 배열명을 주소로 활용하는 예를 살펴보겠습니다.
#include <stdio.h>
int main (void)
{
int ary[3];
int i;
*(ary + 0) = 10; //ary[0] = 0
*(ary + 1) = *(ary + 0) + 10; //ary[1] = ary[0] + 10
printf("세 번째 배열 요소에 키보드 입력 : ");
scanf("%d", ary + 2); //ary + 2 == &ary[2]
for (i = 0; i < 3; i++){
printf("%d ", *(ary + i));
}
return 0;
}
*
배열 요소 표현식 : ary[1]
포인터 연산식 : *(ary + 1)
5행에 선언된 배열이 메모리 100번지 부터 할당되었다고 가정하고 이야기를 풀어보겠습니다. 일단 배열명은 첫 번째 배열 요소의 주소이므로 값 ㄱ자채는 100입니다. 그리고 8행에서 ary에 0을 더한 결과도 그대로 100이므로 첫 번째 배열 요소가 주소가 됩니다. 여기에 간접 참조 연산을 수행하면 첫 번째 배열 요소 자체가 되겠지요. 물론 ary에 0을 더하지 안호 바로 간접 참조 연산을 수행한 것과 결과는 같습니다. 반면에 ary에 1을 더하면 104번지로 두 번째 배열 요소의 주소가 됩니다. 이 값에 간접 참조 연산을 수행하면 두 번째 배열 요소를 사용할 수 있습니다. 결국 9행은 첫 번째 배열 요소의 값에 10을 더해 두 번째 배열 요소에 저장하는 식이 됩니다. 마지막으로 세 번째 배열 요소는 키보드로 값을 입력합니다. 보통은 배열 요소의 표현 방법을 써서 쉽게 작성할 수 있습니다. 12행을 scanf("%d", &ary[2]); 으로 수정해서 컴파일해도 결과는 같습니다. 결국 배열 요소 ary[2]의 주소를 scanf 함수에 주는 것이므로 본래 코드의 12행처럼 배열명 ary에 2를 더한 값으로 간단히 표현할 수 있습니다. 16행은 반복 과정에서 배열명에 i를 더해 각 배열 요소의 주소를 구하고 간접 참조 연산으로 모든 배열 요소의 값을 출력합니다. 즉, 배열의 대괄호는 포인터 연산의 '간접 참조, 괄호, 더하기' 연산 기능을 같습니다. 배열 요소를 사용할 때는 상황에 따라 대괄호나 포인터 연산식 중 적당한 것을 골라 쓰면 됩니다. 특별한 경우가 아니면 대괄호를 사용하는 것이 쉬우며, &art[2]와 같은 경우는 ary + 2로 쓰면 연산 과정을 줄일 수 있습니다.
배열명 역할을 하는 포인터
배열명은 주소이므로 포인터에 저장할 수 있습니다. 이 경우 포인터로도 연산식이나 대괄호를 써서 배열 요소를 쉽게 사용할 수 있습니다.
#include <stdio.h>
int main (void)
{
int ary[3];
int *pa = ary;
int i;
*pa = 10;
*(pa + 1) = 20;
pa[2] = pa[0] + pa[1];
for (i = 0; i < 3; i++)
{
printf("%5d", pa[i]);
}
return 0;
}
5행의 배열이 int형 배열이고 배열명은 첫 번째 배열 요소의 주소이므로 ary는 int형 변수의 주소가 됩니다. 따라서 6행처럼 int형을 가리키는 포인터에 저장할 수 있습니다. 만약 배열이 메모리 100번지부터 할당되었다면 배열명 ary의 주소 값은 100번지가 되고, 포인터 pa는 100을 저장하여 첫 번째 배열 요소를 가리키는 상태가 됩니다. 이제 포인터 pa로 배열 요소를 사용할 수 있습니다. 9행의 *pa는 pa가 가리키는 것이므로, ary[0]에 10을 저장합니다. 물론 pa의 값은 변함이 없습니다. 10행은 우선 pa + 1을 계산하여 두 번째 배열 요소의 주소 104번지를 구합니다. 그 결과에 간접 참조 연산을 수행하면 두 번째 배열 요소가 되므로 ary[1]에 20을 대입합니다. 11행처럼 포인터 pa에 대괄호를 사용하는 것도 가능합니다. 대괄호를 사용하면 포인터 연산식으로 바뀌므로 결국 각 배열 요소를 사용할 수 있습니다. 13 ~ 16행도 포인터에 대괄호를 사용하여 모든 배열 요소의 값을 출력합니다. 결국 포인터에 배열명을 저장하면 포인터 연산을 통해 모든 배열 요소를 사용할 수 있으므로 포인터를 마치 배열명처럼 사용할 수 있습니다.
배열명과 포인터의 차이
포인터가 배열명처럼 쓰이기는 하지만 서로 다른 점이 더 많습니다.
1. sizeof 연산의 결과가 다릅니다.
배열명에 사용하면 배열 전체의 크기를 구하고, 포인터에 사용하면 포인터 하나의 크기를 구합니다. 따라서 배열명을 포인터에 저장하면 포인터 배열 전체의 크기를 확인하는 것은 불가능합니다.
int ary[3];
int *pa = ary;
sizeof(ary) //12바이트, 배열 전체 크기
sizeof(pa) //8바이트, 포인터 하나의 크기
2.변수와 상수의 차이가 있습니다.
포인터는 그 값을 바꿀 수 있지만 배열명은 상수이므로 값을 바꿀 수 없습니다. 즉, 포인터 pa에 1을 더하여 다시 pa에 저장할 수는 있으나, 배열명 ary는 1을 더하는 것은 가능하고 그 값을 다시 저장하는 것은 불가능 합니다. 포인터가 지닌 변수로서의 특징을 활용하는 예를 살펴봅시다.
#include <stdio.h>
int main (void)
{
int ary[3] = { 10, 20, 30 };
int *pa = ary;
int i;
printf("배열의 값 : ");
for (i = 0; i < 3; i++) {
printf("%d ", *pa);
pa++;
}
return 0;
}
이 상태에서 포인터 pa로 첫 번째 요소를 출력하는 방법은 여러가지가 있습니다.
1. printf("%d", pa[0]);
2. printf("%d", *(pa + 0));
3. printf("%d", *pa);
표현방법은 다르지만 모두 첫 번째 배열 요소를 출력합니다. 이중 마지막 방법의 경우 pa는 첫 번째 배열 요소를 가리키므로 *pa의 연산식으로 첫 번째 배열 요소의 값을 출력합니다. 만약 pa가 두 번째 배열 요소를 가리키도록 하면 같은 연산식으로 두 번째 배열 요소의 값도 출력할 수 있습니다. 방법은 간단합니다. pa에 1을 더하면 두 번째 요소의 주소 104번지가 되므로 이 값을 다시 pa에 저장하면 됩니다. 간단히 증가 연산자를 사용할 수도 있습니다.
pa = pa + 1; 또는 pa++;
세 번째 배열 요소도 같은 방식으로 pa의 값을 증가시키면 됩니다. 12행에서 pa가 가리키는 배열 요소를 출력하고 13행에서 다음 배열 요소를 가리키도록 pa의 값을 바꾸므로 이 과정을 반복하면 결국 모든 배열 요소를 출력하게 됩니다. 이런 방식이 가능한 이유는 포인터 pa가 변수이므로 그 값을 바꿀 수 있기 때문입니다.
다음처럼 포인터로 배열 요소를 차례로 출력할 때 증가 연산자와 간접 참조 연산자를 함께 사용하는 방법도 있습니다.
for (i = 0; i < 3; i++)
{
printf("%d ", *(pa++));
}
연산자 우선 순위에 따라 pa++가 먼저 수행되므로 pa의 값은 증가합니다. 그러나 후위형이므로 다음 연산인 간접 참조 연산을 수행할 때는 증가되기 이전의 값이 사용됩니다. 컴파일러는 pa가 증가되기 이전 값을 임시공간에 저장해두었다가 간접 참조 연산에 사용하거나 또는 연산자 우선순위를 바꾸어 pa++ 보다 *pa의 연산을 먼저 수행하도록 컴파일합니다. 컴파일러가 어떤 방식을 사용하든 결국 pa가 가리키던 배열 요소의 값이 먼저 출력되고 pa가 다음 배열 요소를 가리키는 것과 결과는 같습니다.
포인터로 배열의 데이터를 처리할 때 주의할 점이 있습니다.
1. 포인터의 값이 변할 수 있으므로 유효한 값인지 확인하는 습관이 필요합니다.
예를 들어 반복문을 모두 수행한 후에 15행에서 pa의 값은 112번지가 되며, 이 값은 배열이 할당된 영역의 주소가 아니므로 간접 참조 연산을 통해 그 공간이나 저장된 값을 사용해서는 안 됩니다. 만약 pa로 다시 배열의 처음부터 데이터를 처리해야 한다면, 배열명으로 다시 초기화 합니다. 배열명은 주소 상수로 그 값이 바뀌지 않으므로 언제든지 배열의 시작 위치를 찾아갈 때 사용할 수 있습니다. 예제에는 생략되었지만, 같은 방식으로 입력을 받을 때는 간접 참조 연산 없이 포인터만 사용합니다. scanf함수는 입력할 배열 요소의 주소가 필요하므로 그 값을 갖고 있는 포인터를 그대로 사용하면 됩니다. 만약 배열을 초기화하지 않고 키보드로 입력한다면 7행과 9행 사이에 다음 코드를 추가합니다.
for (i = 0; i < 3; i++)
{
scanf("%d", pa); // pa가 가리키는 배열 요소에 입력, 간접 참조 연산 없음.
pa++; // 다음 배열 요소를 가리키도록 pa증가
}
2. 포인터에 증가 연산자와 간접 참조 연산자를 함께 사용할 때 전위 표현을 사용하면 안 됩니다.
전위 표현을 사용하면 전혀 다른 결과가 출력됩니다. *(++pa)는 pa의 값이 먼저 증가된 후에 증가된 pa가 가리키는 배열 요소를 간접 참조라므로 두 번째 배열 요소부터 출력됩니다. 따라서 마지막에 출력되는 값은 배열의 값이 아닌 쓰레기 값이 출력됩니다. 또한 전위형이나 후위형 모두 괄호를 생략해도 결과는 같습니다. 간접 참조 연산자와 증가 연산자는 모두 단항 연산자로 우선순위가 같고 이 경우 연산 방향은 오른쪽에서 왼쪽이므로 항상 증가 연산자가 먼저 수행됩니다.
포인터의 뺄셈과 관계 연산
포인터에는 정수 덧셈이나 증가 연산 외에도 다양한 연산을 수행할 수 있습니다. 예를 들어 가리키는 자료형이 같으면 포인터끼리의 뺄셈이 가능합니다. 물론 일반 뺄셈과는 다른 방식으로 연산됩니다. 또한 관계 연산자로 대소관계도 확인할 수 있습니다. 예제를 통해 포인터에 수행할 수 있는 연산들을 살펴보겠습니다.
#include <stdio.h>
int main (void) {
int ary[5] = { 10, 20, 30, 40, 50 };
int *pa = ary;
int *pb = pa + 3;
printf("pa : %u\n", pa);
printf("pa : %u\n", pb);
pa++;
printf("pb - pa : %u\n", pb - pa);
printf("앞에 있는 배열 요소의 값 출력 : ");
if (pa < pb)
printf("%d\n", *pa);
else
printf("%d\n", *pb);
return 0;
}
이 예제는 포인터 pa와 pb로 배열 ary의 각각 다른 배열 요소를 가리키도록 한 후에 포인터의 뺄셈과 대소관계 연산을 수행합니다. 설명의 편의를 위해 출력 결과에서 끝의 2자리만 사용합니다. 실행결과는 시스템에 따라 다를 수 있습니다.
최초 pa는 6행에서 배열명으로 초기화 하므로 첫 번째 배열 요소를 가리킵니다. 반면에 pb는 pa에 3을 더해 초기화하므로 48번지를 갖고 네 번째 배열 요소를 가리킵니다. 이 상태에서 11행이 수행되면 pa는 40으로 증가면서 두 번째 배열 요소를 가리키게 됩니다. 그리고 12행에서 pb - pa의 연산은 다음과 같이 수행됩니다.
pb - pa → (48 - 40) / sizeof(int) → 8 / 4 → 2
뺄셈 결과는 배열 요소 간의 간격 차이를 의미합니다. 따라서 결괏값으로 포인터 pa와 pb가 가리키는 배열 요소의 위티가 2개 떨어져 있음을 알 수 있습니다. 15행은 포인터에 관계 연산을 수행합니다. 배열은 배열 요소가 메모리에 순서대로 할당되므로 앞의 배열 요소가 뒤의 배열 요소보다 주소 값이 작스니다. 따라서 관계 연산의 결과로 두 포인터가 가리키는 배열 요소의 순서를 확인할 수 있습니다. 물론 필요에 따라적당한 관꼐 연산자를 골라 사용하면 됩니다.
마무리
- 배열명은 첫 번째 요소의 주소이다.
- 포인터에 배열명을 저장하면 포인터를 배열명처럼 사용할 수 있다.
- 배열명의 정수 덧셈은 가리키는 자료형의 크기를 곱해서 더한다.
- 포인터의 뺄셈 결과는 배열 요소 간의 간격 차이를 의미한다.
'C언어' 카테고리의 다른 글
혼공C 도전실전 예제 내 풀이 (0) | 2023.08.17 |
---|---|
[C언어] 배열을 처리하는 함수 (0) | 2023.08.15 |
[C언어] 포인터 완전 정복하기 (0) | 2023.08.12 |
[C언어] 포인터 (0) | 2023.08.03 |
[C언어] 포인터 (0) | 2023.08.02 |