Quick Syntax
배열 길이는 같은 스코프에서만 sizeof(arr) / sizeof(arr[0])로 안전하게 구하고, 함수로 넘길 때는 길이를 같이 전달합니다.
#include <stddef.h>
#include <stdio.h>
int scores[5] = {90, 85, 100, 70, 88};
size_t len = sizeof(scores) / sizeof(scores[0]);
for (size_t i = 0; i < len; i++) {
printf("%d\n", scores[i]);
}
void print_scores(const int *arr, size_t n) {
for (size_t i = 0; i < n; i++) {
printf("%d\n", arr[i]);
}
}고정 배열은 선언 위치에서 길이를 구하고, 함수에는 원소 포인터와 길이를 함께 전달합니다. 런타임에 길이가 정해지면 동적 저장 공간의 소유자와 길이를 같이 관리합니다.
문법
배열은 같은 타입 원소가 연속해서 놓인 고정 길이 메모리 블록입니다. arr[i]는 시작 주소에서 i * sizeof(T)만큼 이동한 위치를 읽는 표현이고, 그래서 배열은 문법과 메모리 경계를 같이 봐야 이해가 쉽습니다. 길이를 어디서 안전하게 구할 수 있는지, 함수 경계를 넘기면 무엇이 사라지는지도 같이 보는 편이 좋습니다.
기본 문법은 이렇게 읽습니다.
int scores[5];
scores[0] = 90;
scores[4] = 88;scores는 원소 5개를 가진int배열입니다.- 유효한 인덱스는
0부터4까지입니다. - 배열 이름은 첫 원소를 가리키는 식처럼 쓰일 수 있지만, 선언 시점의 배열 전체 정보가 언제나 같이 따라가지는 않습니다.
고정 길이 배열은 선언 시 크기가 정해집니다. 런타임 크기가 필요하면 VLA 지원 여부를 별도로 확인하거나 동적 할당을 사용하고, 둘 다 길이·소유권 계약을 함수 경계에서 명시합니다.
같은 스코프에서 길이를 계산하는 기본 패턴
int scores[5] = {90, 85, 100, 70, 88};
int len = (int)(sizeof(scores) / sizeof(scores[0]));
for (int i = 0; i < len; i++) {
printf("%d\n", scores[i]);
}이 패턴이 안전한 이유는 아직 컴파일러가 scores를 "진짜 배열"로 보고 있기 때문입니다. sizeof(scores)는 배열 전체 바이트 수를, sizeof(scores[0])는 원소 하나의 바이트 수를 뜻합니다.
함수로 넘길 때 길이를 같이 전달하는 패턴
void print_scores(const int *arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d\n", arr[i]);
}
}함수 인자로 넘어가면 int arr[]나 int *arr는 사실상 같은 의미가 됩니다. 이 시점부터는 호출자가 넘긴 배열의 전체 길이를 잃어버리므로, 길이를 따로 같이 넘기는 구조가 기본입니다.
print_scores(scores, len); /* 맞는 호출 */반대로 이런 코드는 틀립니다.
print_scores(scores, sizeof(scores)); /* 바이트 수를 길이처럼 넘김 */함수는 원소 개수를 기대하는데 호출자가 배열 전체 바이트 수를 넘기면 경계가 바로 틀어집니다.
선언 시점에 길이를 추론하는 패턴
int primes[] = {2, 3, 5, 7, 11};
int count = (int)(sizeof(primes) / sizeof(primes[0]));배열 길이 추론은 선언 시점에서 끝납니다. 이 정보가 함수 호출 경계까지 자동으로 따라가지는 않습니다.
경계 이해
- 같은 스코프에서는
sizeof(arr) / sizeof(arr[0])로 길이를 구할 수 있습니다. - 함수 인자로 넘어가면 배열은 포인터처럼 decay되어 길이 정보가 사라집니다.
- 함수에서는 길이를 별도 인자로 같이 넘기는 구조가 기본입니다.
- 배열 문제의 핵심은 경계 초과와 길이 전달 누락입니다.
arr[i] == *(arr + i);arr[i]는 결국 시작 주소에서 i * sizeof(T)만큼 이동한 위치를 읽는 것과 같습니다. 그래서 배열 순회와 포인터 산술이 연결됩니다.
길이 계산이 안전한 위치와 위험한 위치를 같이 보면 차이가 더 분명합니다.
int scores[5] = {90, 85, 100, 70, 88};
int len = (int)(sizeof(scores) / sizeof(scores[0])); /* 안전 */
void print_scores(const int *arr) {
size_t wrong = sizeof(arr) / sizeof(arr[0]); /* 잘못된 길이 */
}- 위
len은 배열 선언이 보이는 같은 스코프에서 계산하므로 안전합니다. - 아래
wrong은 함수 안에서arr가 포인터가 된 뒤에 계산하므로, 배열 길이가 아니라 포인터 크기를 기준으로 계산됩니다. sizeof(scores)는 바이트 수이고,len은 원소 개수라는 점도 같이 구분해야 합니다.- 인덱스와 길이는 보통
size_t로 맞춰 signed/unsigned 비교를 피합니다. 역방향 반복처럼 음수가 필요한 경우에는 빈 배열을 먼저 처리합니다.
주의할 점
C는 배열 경계를 자동으로 검사하지 않고, 함수 안에서 배열 길이도 자동으로 보존하지 않습니다.
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 99;- 이런 코드는 즉시 크래시가 안 나더라도 정의되지 않은 동작입니다.
- 함수 안에서
sizeof(arr)를 다시 구하면 배열 전체 크기가 아니라 포인터 크기만 나옵니다. - 배열 길이는 호출자 쪽에서 계산하고, 함수에는 원소 포인터와 길이를 같이 넘기는 편이 맞습니다.
void fill(int arr[]) {
size_t count = sizeof(arr) / sizeof(arr[0]); /* 포인터 크기 기준 */
}arr[]로 적어도 함수 매개변수에서는 배열이 아니라 포인터처럼 동작합니다.- 그래서 배열을 함수로 넘길 때는
pointer + length를 같이 설계하는 습관이 중요합니다.
참고 링크
2 sources