왜 배열은 인덱스 0에서 시작하는가: 메모리 수준 설명

발행: (2026년 1월 4일 오후 11:52 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

📌 Table of Contents

연속 메모리 블록으로서의 배열

본질적으로 C/C++에서 배열은 동일한 타입의 요소들을 고정된 크기로 모아 놓은 컬렉션이며, 연속된 메모리 위치에 저장됩니다. 다음과 같이 선언하면

int arr[100];

컴파일러는 100개의 연속된 정수를 위한 공간을 할당합니다.

대부분의 최신 시스템에서:

  • int는 일반적으로 4 바이트를 차지합니다(32‑ 또는 64‑비트 아키텍처에서).
  • 따라서 전체 배열은 400 바이트를 차지하며, 메모리 상에 연속적으로 배치됩니다.

arr[i] 작동 방식: 포인터 연산 설명

배열이 인덱스 0부터 시작하는 진짜 이유는 계산이나 관습 때문이 아니라, 언어가 배열 인덱싱을 정의하는 방식 때문이다.

arr[i]를 쓰면 직접 *(arr + i)번역된다.

이것은 구현 세부 사항이 아니라; C/C++ 언어 정의의 일부이다.

*(arr + i) 분석

PartMeaning
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++에서 배열 인덱싱은 위치를 세는 것이 아니라 기본 주소로부터의 오프셋을 측정하는 것입니다.

Back to Blog

관련 글

더 보기 »