C 언어의 배열 타입이 유독 이상한 이유

C 언어의 배열 타입 T[n]은 본질적으로 포인터 T 와 다르지만, 대부분의 식에서 첫 번째 요소를 가리키는 포인터로 자동 변환(decay)되는 독특한 특성을 가집니다. 특히 함수 매개변수 선언 시 char buf[6]처럼 크기를 명시하더라도 실제로는 크기 정보가 유실된 포인터로 해석되며, 이를 온전히 보존하려면 char (buf)[6]와 같은 배열 포인터 형식을 사용해야 합니다.

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 기반의 레거시 및 임베디드 프로젝트에서는 배열 크기 유실로 인한 메모리 오버플로우 취약점을 방지하기 위해 정적 분석 도구를 통한 매개변수 검증이 필수가 될 것입니다.
Share

이것도 읽어보세요

댓글

이 소식에 대한 의견을 자유롭게 남겨주세요.

댓글 (0)

불러오는 중...