배열의 데이터를 자유롭게 다루기 위해서는 배열의 구현 원리를 이해해야 하는데, 이를 위해서는 포인터와의 관계를 이해해야한다.
지금부터 배열과 포인터가 무슨 사이인지를 밝혀보자.
배열은 자료형이 같은
변수를 메모리에 연속
으로 할당한다.
예를 들어,
int ary[5];의 배열이 메모리 100번지부터 할당되고 int형 변수의 크기가 4바이트라면
결국 첫 번째 요소의 주소를 알면 나머지 요소의 주소도 쉽게 알 수 있으므로
컴파일러는 배열명 'ary'를 컴파일 과정에서 첫 번째 배열 요소의 주소
로 변경한다.
주소는 정수처럼 보여도 자료형에 대한 정보를 갖고 있는 특별한 값이므로
정해진 연산만 가능하다.
주소 + 정수 → 주소 + (정수 * 주소를 구한 변수의 크기)
예를 들어 크기 4바이트 int형 변수 a의 주소 100번지에 1을 더한 결과는
&a + 1 → 100 + (1 * sizeof(int))
이런 방식으로 계산하여 주소 104번지가 되는 것이다.
# include <stdio.h> int main(void) { int ary[3]; int i; *(ary + 0) = 10; *(ary + 1) = *(ary + 0) + 10; printf("세 번째 배열 요소에 키보드 입력: "); scanf("%d", ary + 2); for (i=0; i<3; i++) { printf("%5d", *(ary + i)); } return 0; }
5행에 선언된 배열이 메모리 100번지부터 할당되었다고 가정해보자.
일단 배열명은 첫 번째 배열 요소의 주소이므로 값 자체는 100이다.
그리고 8행에서 'ary'에 0을 더한 결과도 그대로 100이므로 첫 번째 배열 요소가 주소가 된다.
여기에 간접 참조 연산을 수행하면 첫 번째 배열 요소
자체가 되는 것!
즉,
*(ary + 0) == ary[0]
그럼 결국 9행은 첫 번째 배열 요소의 값에 10을 더해 두 번째 배열 요소에 저장하는 식.
이해가죠?
배열의 대괄호([])는 포인터 연산의 '간접 참조, 괄호, 더하기' 연산 기능
배열 요소 표현식 포인터 연산식 ary[1] ↔ *(ary + 1)
문법적으로 문제가 없으므로 컴파일은 되나 실행할 때 결과를 예상할 수 없다.
예를 들어 ary[2], 즉 배열 요소의 개수가 3개인 배열에서
ary + 3은 네 번째 배열 요소의 주소가 되고
*(ary + 3)은 네 번째 배열 요소가 된다.
배열명은 주소이므로 포인터에 저장할 수 있다.
포인터로도 연산식이나 대괄호를 써서 배열 요소를 쉽게 사용할 수 있다.
# 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 = 10;ary[0]에 10을 저장.
👍
포인터 연산식 pa[2] = pa[0] + pa[1] pa에 저장된 값은 ary ↳ *(pa + 2) = *(pa + 0) + *(pa + 1) 배열 요소 표현식 ↳ *(ary + 2) = *(ary + 0) + *(ary + 1) ↳ ary[2] = ary[0] + ary[1]
포인터가 배열명처럼 쓰이긴 하지만 서로 다른 점이 더 많다.
배열명에 사용하면 배열 전체
의 크기를 구하고
포인터에 사용하면 포인터 하나
의 크기를 구한다.
따라서, 배열명을 포인터에 저장하면 포인터로 배열 전체의 크기를 확인하는 것은 불가능하다.
포인터 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; }
5행의 배열이 메모리 100번지부터 할당되고 6행의 포인터가 초기화되면
포인터로 pa로 첫 번째 배열 요소를 출력하는 방법은
printf("%d", pa[0]);
printf("%d", *(pa + 0));
printf("%d", *pa);
표현 방법은 다르지만 모두 첫 번째 배열 요소를 출력한다.
이중 마지막 방법의 경우 pa는 첫 번째 배열 요소를 가리키므로 *pa의 연산식으로 첫 번째 배열 요소를 출력한다.
pa에 1을 더하면 두 번째 배열 요소의 주소가 되므로 이 값을 다시 pa에 저장하는 식으로
n 번째 배열 요소의 값도 출력할 수 있다.
> 만약 pa로 다시 배열의 처음부터 데이터를 처리해야한다면 배열명으로 다시 초기화
+ 만약 같은 방식으로 입력을 받을 때는 **간접 참조 연산 없이** 포인터만 사용!
> 전위 표현을 사용하면 전혀 다른 결과가 출력
*(++pa)는 pa의 값이 먼저 증가된 후에 증가된 pa가 가리키는 배열 요소를 간접 참조
하므로
두 번째 배열 요소부터 출력.
++(*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("pb: %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는 6행에서 배열명으로 초기화 하므로 첫 번째 배열 요소를 가리키고, 28번지를 갖는다.
반면 pb는 pa에 3을 더해 초기화하므로 네 번째 배열 요소를 가리키고, 40번지를 갖는다.
이 상태에서 11행이 수행되면 pa는 32로 증가하면서 두 번째 배열 요소를 가리킨다.
그리고 12행에서 pb - pa의 연산은
pb - pa = (40-32) / sizeof(int) = 8/4 = 2
즉, 뺄셈 결과는 배열 요소 간의 간격 차이를 의미한다.
따라서 결과값으로 포인터 pa와 pb가 가리키는 배열 요소의 위치가 2개 떨어져 있음을 알 수 있다.
너랑 나랑드 사이다