왜 배열은 인덱스 0에서 시작하는가: 메모리 수준 설명
Source: Dev.to
📌 Table of Contents
- 연속 메모리 블록으로서의 배열
arr[i]작동 방식: 포인터 연산 설명- 왜 이것이 인덱스를 0부터 시작하도록 강제하는가
- 배열이 인덱스 1부터 시작한다면?
arr[i]와i[arr]가 같은 의미인 이유- 결론
연속 메모리 블록으로서의 배열
본질적으로 C/C++에서 배열은 동일한 타입의 요소들을 고정된 크기로 모아 놓은 컬렉션이며, 연속된 메모리 위치에 저장됩니다. 다음과 같이 선언하면
int arr[100];
컴파일러는 100개의 연속된 정수를 위한 공간을 할당합니다.
대부분의 최신 시스템에서:
int는 일반적으로 4 바이트를 차지합니다(32‑ 또는 64‑비트 아키텍처에서).- 따라서 전체 배열은 400 바이트를 차지하며, 메모리 상에 연속적으로 배치됩니다.
arr[i] 작동 방식: 포인터 연산 설명
배열이 인덱스 0부터 시작하는 진짜 이유는 계산이나 관습 때문이 아니라, 언어가 배열 인덱싱을 정의하는 방식 때문이다.
arr[i]를 쓰면 직접 *(arr + i) 로 번역된다.
이것은 구현 세부 사항이 아니라; C/C++ 언어 정의의 일부이다.
*(arr + i) 분석
| Part | Meaning |
|---|---|
arr | 배열의 기본 주소 (즉, &arr[0]). |
+ i | 포인터 연산 – i × sizeof(element_type) 바이트를 더하고, i 바이트를 더하는 것이 아니다. |
* | 계산된 주소를 역참조하여 값을 읽거나 쓴다. |
따라서 arr[i]는 문자 그대로 배열 시작점에서 i 요소만큼 떨어진 위치로 이동한 뒤, 그곳에 저장된 값을 접근한다는 의미이다.
Example
#include <stdio.h>
int main(void) {
int arr[] = {10, 20, 30, 40};
// Direct array access
printf("arr[1]: %d\n", arr[1]); // Output: 20
// Equivalent pointer version
printf("*(arr + 1): %d\n", *(arr + 1)); // Same output: 20
return 0;
}
두 문장은 정확히 같은 연산이므로 20을 출력한다.
왜 이것이 인덱싱을 0부터 시작하게 하는가
첫 번째 요소는 “한 단계 떨어져” 있지 않으며, 베이스 주소 자체에 위치한다.
- 베이스 주소로부터 거리 = 0
- 오프셋 = 0
- 인덱스 = 0
따라서 첫 번째 요소는 다음과 같이 접근한다:
arr[0] == *(arr + 0) // 조정이 필요 없음
각각의 다음 요소는 메모리에서 앞으로 이동함으로써 도달한다:
arr[1] == *(arr + 1) // 1 요소 건너뛰기 (int는 4바이트)
arr[2] == *(arr + 2) // 2 요소 건너뛰기 (8바이트)
인덱스는 요소 단위로 측정된 오프셋에 불과하다. 오프셋은 원점으로부터의 거리가 0보다 가까울 수 없기 때문에 0부터 시작한다. 이는 메모리 주소 지정 및 포인터 연산이 작동하는 방식에서 직접적으로 비롯된다.
배열이 인덱스 1부터 시작한다면?
배열이 MATLAB처럼 1‑베이스 인덱싱이라 가정해 보겠습니다. 이 경우 첫 번째 요소는 arr[1] 로 접근합니다.
포인터 연산 자체는 변하지 않으며, arr[i]는 여전히 *(arr + i) 로 변환됩니다. 이 규칙을 그대로 적용하면 다음과 같습니다:
arr[1] → *(arr + 1) // points to the *second* element, not the first
1‑베이스 인덱싱을 동작하게 하려면, 컴파일러는 모든 접근을 다음과 같이 다시 작성해야 합니다:
arr[i] → *(arr + (i - 1))
추가된 뺄셈은 하드웨어의 자연스러운 “base + offset” 주소 지정 모델과 의미적 불일치를 일으키며, 경계 판단을 복잡하게 만들고 단순한 “베이스로부터의 오프셋”이라는 사고 모델을 흐리게 합니다. 현대 컴파일러가 이 뺄셈을 최적화해서 없앨 수는 있지만, 불필요한 개념적 단계가 추가되는 것입니다.
arr[i]와 i[arr]가 같은 의미인 이유
C 표준은 첨자 연산자를 다음과 같이 정의합니다:
a[b] == *(a + b)
덧셈은 교환법칙이므로 (a + b == b + a), 다음도 성립합니다:
*(a + b) == *(b + a)
따라서:
a[b] == b[a]
이는 속임수가 아니며 정의되지 않은 동작도 아닙니다; 언어 정의의 직접적인 결과입니다.
시연
#include <stdio.h>
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
// 일반적인 배열 인덱싱
printf("arr[3] = %d\n", arr[3]); // Output: 4
// 동등하지만 특이한 인덱싱
printf("3[arr] = %d\n", 3[arr]); // Output: 4
return 0;
}
두 문장 모두 4를 출력합니다. 이는 arr[3]와 3[arr]가 같은 주소를 계산하기 때문입니다.
결론
- 제로 기반 인덱싱은 C/C++가 배열 서브스크립트를 정의하는 방식(
*(base + offset))과 완벽히 일치합니다. - 첫 번째 요소는 베이스 주소에 위치하므로 오프셋은 0입니다.
- 1 기반 인덱싱은 모든 접근마다 추가적인
-1조정이 필요해 불필요한 복잡성을 초래합니다. a[b] == *(a + b)라는 정의는arr[i]와i[arr]가 서로 교환 가능함을 설명합니다.
이러한 저수준 세부 사항을 이해하면 배열 인덱스를 0부터 시작하는 것이 겉보기에 이상해 보일 수 있지만, 실제로는 언어와 하드웨어에 가장 자연스럽고 효율적인 선택임을 명확히 알 수 있습니다.
#include <stdio.h>
int main(void) {
int arr[] = {1, 2, 3, 4};
int i = 3;
printf("%d\n", i[arr]); // Output: 4
return 0;
}
NOTE: i[arr]는 유효한 C 문법이지만 가독성을 해치기 때문에 실제 코드에서는 거의 사용되지 않습니다. 이는 배열 인덱싱이 포인터 연산으로 정의되어 있기 때문에 존재합니다.
Conclusion
C/C++에서 배열 인덱싱은 위치를 세는 것이 아니라 기본 주소로부터의 오프셋을 측정하는 것입니다.