Pointer arithmetic을 가능하게 하려면 배열의 자료형이 고정되어 있어야 한다
배열의 경우 정적(static) / 동적(dynamic) 상관없이 둘 다 메모리 상 하나의 연속된 블럭으로 메모리가 할당된다. 이로인해 offset 계산이 가능한 배열은 indexing을 사용하여 빠른 조회 연산의 시간 복잡도를 가진다 (250511090805).
offset 계산이 가능한 이유는 배열의 각 원소가 고정된 크기를 가지기 때문이다. 배열의 경우 연속된 하나의 블럭으로 메모리에 할당이 된다고 했다. 각 블럭의 크기는 동일하기 때문에 첫 번째 원소의 메모리 주소에서 +1
을 하면, 숫자 1을 더하는 것이 아니라 해당 자료형의 바이트 수 만큼 더하게 된다. 즉, 다음 원소가 위치해 있는 블럭으로 이동이 가능하다.
예를들어 32bits 정수형 배열이 있다고 가정했을 때, 첫 번째 원소의 메모리 주소에서 +1
을 하면, 32bits인 4 bytes만큼 이동하게 된다.
int arr[3] = {17, 34, 51};
&arr[0] // 0x..bb9c
&arr[0] + 1 // 0x..bba0 (0번에서 4 bytes 이동)
&arr[0] + 2 // 0x..bba4 (0번에서 8 bytes 이동)
참고로 할당된 메모리의 시작점을 가리키는 것이 배열의 첫 번째 원소이기도 하지만, 배열의 이름 그 자체이기도 하다. 아래 예제를 보면 배열의 이름인 arr
와 첫 번째 원소인 arr[0]
의 주소가 같은 것을 확인할 수 있다.
int arr[3] = {17, 34, 51};
printf("%p\n", arr); // 0x..eb9c
printf("%p\n", &arr[0]); // 0x..eb9c
// 위 예제를 아래처럼 바꿔도 결과는 동일하다
printf("%p\n", arr); // 0x..bb9c
printf("%p\n", arr+1); // 0x..bba0
printf("%p\n", arr+2); // 0x..bba4
인뎅싱과 pointer arithmetic이 어떻게 연관되는 것인지 궁금할 수 있는데, 사실 배열의 인덱싱은 offset을 []
으로 표현한 것 뿐이다. arr[1]
은 *(arr + 1)
을 의미한다.
int arr[3] = {17, 34, 51};
printf("%d\n", *(arr+0)); // 17
printf("%d\n", *(arr+1)); // 34
printf("%d\n", *(arr+2)); // 51
그래서 이런 재밌는 것도 가능하다.
int arr[3] = {17, 34, 51};
printf("%d\n", 0[arr]); // 17
printf("%d\n", 1[arr]); // 34
printf("%d\n", 2[arr]); // 51
이게 동작하는 이유는 1[arr]
은 결국 *(1 + arr)
이 되기 때문이다.
배열의 큰 장점 중 하나인 빠른 조회는 자료형이 있기에 가능하다. 만약 배열에 타입이 없다면, 각각의 블럭에 대한 정보가 전혀 없어서 offset 계산이 불가능할 것이다. 즉, 배열이 자료형 없이 동작하는 것처럼 보이는 스크립트 언어도, 내부적으로는 참조 타입 혹은 공통 조상 클래스(Object 등)를 통해 일관된 방식으로 메모리를 다루고 있는 것이다 (250518152200).
확장 🌱
- n/a
관련 노트 📘
- {0a} 250511090805 - 배열이 조회 연산에 강점을 보이는 이유
- {0a0} 250518152200 - 객체의 참조를 사용하면 배열에 여러 자료형을 담을 수 있다