AI 요약
C 언어에서 배열 타입 T[n]은 메모리상에 n개의 T 타입 요소가 연속적으로 배치된 구조를 정의하지만, 실제 사용 시에는 직관적이지 않은 복잡한 규칙을 따릅니다. 배열은 대다수의 식에서 첫 번째 요소를 가리키는 포인터(T *)로 즉각 변환(decay)되어 처리되기 때문에, 인덱싱 연산인 arr[ix] 역시 포인터 연산인 *(arr + ix)로 치환되어 작동합니다. 이러한 변환 규칙의 거의 유일한 예외는 sizeof arr 연산으로, 이 경우에는 배열의 실제 전체 크기(sizeof(T) * n)를 올바르게 반환합니다. 하지만 함수 매개변수로 배열을 전달할 때는 배열 크기 지정(n)이 완전히 무시되고 포인터로 해석되어 내부에서의 sizeof 결과가 포인터 크기로 축소되는 왜곡이 발생합니다. 본문은 이와 같은 C 언어 배열 타입의 독특한 동작 방식과 한계점을 지적하며, 함수 포인터와의 유사점 및 차이점을 비교해 설명하고 있습니다.
핵심 인사이트
- 자동 포인터 변환(Decay): C 언어의 배열 타입
T[n]은 거의 모든 식에서 첫 번째 요소를 가리키는 포인터인T *타입으로 자동 변환됩니다. - 함수 매개변수의 크기 유실: 함수 시그니처에서
char buf[6]과 같이 배열 크기를 명시하더라도 컴파일러는 이를 포인터로만 인식하며 크기 정보6은 폐기됩니다. - 배열 포인터를 통한 크기 보존: 함수 매개변수에서 원래 배열 크기를 유지하려면
char (*buf)[6]형태의 배열 포인터를 사용하고 호출 시 주소 연산자&를 붙여야 합니다. - 함수 타입과의 유사성: C 언어의 함수 타입 또한 배열처럼 참조 시 함수 포인터로 자동 변환되지만, 주소 연산자(
&) 적용 시의 동작 방식에는 미세한 차이가 존재합니다.
주요 디테일
- 인덱싱의 실체: 배열 인덱싱 연산자
arr[ix]는 본질적으로*(arr + ix)포인터 연산과 완전히 동일하게 컴파일됩니다. - sizeof 연산자의 예외성: 일반 식과 달리
sizeof arr는 배열 전체 크기인sizeof(T) * n을 정확히 반환하여 포인터가 아님을 증명합니다. - static 키워드 사용의 한계:
char buf[static 8]을 사용하면 컴파일러 최적화를 도울 수 있으나, 더 짧은 크기의 배열이 전달될 경우 정의되지 않은 동작(Undefined Behaviour)을 유발하는 부작용이 있습니다. - 함수 포인터와의 비교: 배열 명
arr은 첫 번째 요소의 주소인&arr[0]으로 변환되지만, 함수 명fn은 그 자체로 함수 주소인&fn과 동일하게 자동 변환됩니다. - & 연산자의 특성: 배열과 함수 모두
&(address-of) 연산자의 피연산자로 사용될 때는 포인터 변환(decay)이 일어나지 않아 정상적인 주소 타입을 보존합니다.
향후 전망
- 현대 언어로의 전환 가속화: C 언어의 복잡하고 직관적이지 않은 배열/포인터 규칙은 개발자의 실수를 유발하기 쉬워, 향후 Rust나 Zig와 같이 안전한 슬라이스(Slice) 타입을 제공하는 언어로의 전환 흐름을 가속화할 것입니다.
- 정적 분석 도구의 중요성 증대: C 기반의 레거시 및 임베디드 프로젝트에서는 배열 크기 유실로 인한 메모리 오버플로우 취약점을 방지하기 위해 정적 분석 도구를 통한 매개변수 검증이 필수가 될 것입니다.
출처:hackernews
