[C] 우리 무슨 사이야?

장세민·2022년 9월 9일
1

📝 TIL

목록 보기
20/40

배열의 데이터를 자유롭게 다루기 위해서는 배열의 구현 원리를 이해해야 하는데, 이를 위해서는 포인터와의 관계를 이해해야한다.

지금부터 배열과 포인터가 무슨 사이인지를 밝혀보자.

📌 배열과 포인터

배열은 자료형이 같은 변수를 메모리에 연속으로 할당한다.

예를 들어,

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

결국 첫 번째 요소의 주소를 알면 나머지 요소의 주소도 쉽게 알 수 있으므로
컴파일러는 배열명 'ary'를 컴파일 과정에서 첫 번째 배열 요소의 주소로 변경한다.


배열명으로 배열 요소 사용하기

주소는 정수처럼 보여도 자료형에 대한 정보를 갖고 있는 특별한 값이므로
정해진 연산만 가능하다.

  • 정수연산
    주소 + 정수 → 주소 + (정수 * 주소를 구한 변수의 크기)

예를 들어 크기 4바이트 int형 변수 a의 주소 100번지에 1을 더한 결과는

&a + 1100 + (1 * sizeof(int))

이런 방식으로 계산하여 주소 104번지가 되는 것이다.

배열명에 정수 연산을 수행하여 배열 요소를 사용하는 예를 보자.

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int ary[3];
  6. int i;
  7.  
  8. *(ary + 0) = 10;
  9. *(ary + 1) = *(ary + 0) + 10;
  10.  
  11. printf("세 번째 배열 요소에 키보드 입력: ");
  12. scanf("%d", ary + 2);
  13.  
  14. for (i=0; i<3; i++)
  15. {
  16. printf("%5d", *(ary + i));
  17. }
  18.  
  19. return 0;
  20. }

5행에 선언된 배열이 메모리 100번지부터 할당되었다고 가정해보자.

일단 배열명은 첫 번째 배열 요소의 주소이므로 값 자체는 100이다.
그리고 8행에서 'ary'에 0을 더한 결과도 그대로 100이므로 첫 번째 배열 요소가 주소가 된다.
여기에 간접 참조 연산을 수행하면 첫 번째 배열 요소 자체가 되는 것!

즉,

*(ary + 0) == ary[0]

그럼 결국 9행은 첫 번째 배열 요소의 값에 10을 더해 두 번째 배열 요소에 저장하는 식.
이해가죠?

배열의 대괄호([])는 포인터 연산의 '간접 참조, 괄호, 더하기' 연산 기능

배열 요소 표현식      포인터 연산식
      ary[1]*(ary + 1)

🔔 배열의 할당 영역을 벗어나는 포인터 연산식은 사용할 수 있을까?

문법적으로 문제가 없으므로 컴파일은 되나 실행할 때 결과를 예상할 수 없다.

예를 들어 ary[2], 즉 배열 요소의 개수가 3개인 배열에서
ary + 3은 네 번째 배열 요소의 주소가 되고
*(ary + 3)은 네 번째 배열 요소가 된다.


📌 배열명 역할을 하는 포인터

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

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int ary[3];
  6. int *pa = ary;
  7. int i;
  8.  
  9. *pa = 10;
  10. *(pa + 1) = 20;
  11. pa[2] = pa[0] + pa[1];
  12.  
  13. for (i=0; i<3; i++)
  14. {
  15. printf("%5d", pa[i]);
  16. }
  17.  
  18. return 0;
  19. }

5행의 배열이 int형 배열이고, 배열명은 첫 번째 배열 요소의 주소이므로 'ary'는 int형 변수의 주소가 된다.
따라서 6행처럼 int형을 가리키는 포인터에 저장 가능!

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


따라서 9행의 *pa는 pa가 가리키는 것이므로,
*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]


📌 배열명과 포인터의 차이

포인터가 배열명처럼 쓰이긴 하지만 서로 다른 점이 더 많다.

> 차이점1. sizeof 연산의 결과가 다르다.

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

따라서, 배열명을 포인터에 저장하면 포인터로 배열 전체의 크기를 확인하는 것은 불가능하다.

> 차이점2. 변수와 상수의 차이

포인터 pa에 1을 더하여 다시 pa에 저장할 수 있으나, 변수
배열명 ary는 1을 더하는 것은 가능하고 그 값을 다시 저장하는 것은 불가능 상수


포인터가 지닌 변수로서의 특징을 활용하는 예를 한 번 보자.

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int ary[3] = {10, 20, 30};
  6. int *pa = ary;
  7. int i;
  8.  
  9. printf("배열의 값: ");
  10. for (i=0; i<3; i++)
  11. {
  12. printf("%d ", *pa);
  13. pa++;
  14. }
  15.  
  16. return 0;
  17. }
  18.  

5행의 배열이 메모리 100번지부터 할당되고 6행의 포인터가 초기화되면
포인터로 pa로 첫 번째 배열 요소를 출력하는 방법은

방법1. pa를 배열명처럼 사용하여 첫 번째 배열 요소를 출력하는 방법

printf("%d", pa[0]);

방법2. pa[0]를 그대로 포인터 연산식으로 바꾸는 방법

printf("%d", *(pa + 0));

방법3. *(pa + 0)에서 의미 없는 0과 괄호를 제거한 표현방법

printf("%d", *pa);

표현 방법은 다르지만 모두 첫 번째 배열 요소를 출력한다.
이중 마지막 방법의 경우 pa는 첫 번째 배열 요소를 가리키므로 *pa의 연산식으로 첫 번째 배열 요소를 출력한다.

pa에 1을 더하면 두 번째 배열 요소의 주소가 되므로 이 값을 다시 pa에 저장하는 식으로
n 번째 배열 요소의 값도 출력할 수 있다.


🚨 포인터로 배열을 처리할 때 주의할 점

1. 배열이 할당된 영역의 주소가 아니면 간접 참조 연산을 통해 그 공간이나 저장된 값을 사용해서는 안된다.

> 만약 pa로 다시 배열의 처음부터 데이터를 처리해야한다면 배열명으로 다시 초기화
+ 만약 같은 방식으로 입력을 받을 때는 **간접 참조 연산 없이** 포인터만 사용!

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

> 전위 표현을 사용하면 전혀 다른 결과가 출력

*(++pa)는 pa의 값이 먼저 증가된 후에 증가된 pa가 가리키는 배열 요소를 간접 참조하므로
두 번째 배열 요소부터 출력.

++(*pa)와 같은 경우 pa의 값 자체는 바뀌지 않으며 첫 번째 배열 요소를 가리키는 상태로 고정.
그리고 pa가 가리키는 배열 요소의 값이 증가하면서 차례로 출력.


📌 포인터의 뺄셈과 관계 연산

가리키는 자료형이 같으면 포인터끼리 뺄셈이 가능하고, 관계 연산자로 대소관계도 확인 가능하다.

포인터 - 포인터 → 값의 차 / 가리키는 자료형의 크기
  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int ary[5] = {10, 20, 30, 40, 50};
  6. int *pa = ary;
  7. int *pb = pa + 3;
  8.  
  9. printf("pa: %u\n", pa);
  10. printf("pb: %u\n", pb);
  11. pa++;
  12. printf("pb - pa: %u\n", pb - pa);
  13.  
  14. printf("앞에 있는 배열 요소의 값 출력: ");
  15. if (pa < pb) printf("%d\n", *pa);
  16. else printf("%d\n", *pb);
  17.  
  18. return 0;
  19. }

최초 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개 떨어져 있음을 알 수 있다.


잊지말자.

배열명은 첫 번째 요소의 주소이다!

profile
분석하는 남자 💻

1개의 댓글

comment-user-thumbnail
2022년 10월 5일

너랑 나랑드 사이다

답글 달기