이 코드는 sizeof ()를 사용하지 않고 어떻게 배열 크기를 결정합니까?


134

C 인터뷰 질문을 통해 다음과 같은 해결책으로 "sizeof 연산자를 사용하지 않고 C에서 배열의 크기를 찾는 방법은 무엇입니까?"라는 질문을 발견했습니다. 작동하지만 이유를 이해할 수 없습니다.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

예상대로 5를 반환합니다.

편집 : 사람들은 답변을 지적 했지만 구문은 약간 다릅니다. 즉 인덱싱 방법

size = (&arr)[1] - arr;

두 질문 모두 유효하며 문제에 대한 접근 방식이 약간 다릅니다. 엄청난 도움과 철저한 설명에 감사드립니다!


13
글쎄, 그것을 찾을 수는 없지만 엄격하게 말하는 것처럼 보입니다. 부속서 J.2 는 다음과 같이 명시하고있다. 단항 * 연산자의 피연산자는 유효하지 않은 값을 갖는다 . 여기 &a + 1그래서 유효하지 유효한 개체를 가리키는되지 않습니다.
유진 Sh.



@AlmaDo 구문은 인덱싱 부분과 약간 다르 므로이 질문은 여전히 ​​유효하지만 잘못되었을 수 있습니다. 지적 해 주셔서 감사합니다!
janojlic

1
@janojlicz와 동일하기 때문에 본질적으로 (ptr)[x]동일합니다 *((ptr) + x).
SS Anne

답변:


135

포인터에 1을 추가하면 결과는 지정된 유형 (즉, 배열)의 객체 시퀀스에서 다음 객체의 위치입니다. 객체를 p가리키는 경우 시퀀스에서 int다음 p + 1을 가리 킵니다 int. 경우 p의 5 요소의 배열에 점 int(이 경우,식이 &a), 다음 p + 1다음를 가리 5 요소의 배열int 순서이다.

두 포인터를 빼면 (두 포인터가 모두 같은 배열 객체를 가리 키거나 하나가 배열의 마지막 요소를 가리키고있는 경우) 두 포인터 사이의 객체 수 (배열 요소)가 생성됩니다.

이 표현식 &a은의 주소를 산출하고 a유형 int (*)[5](의 5- 요소 배열에 대한 포인터)을 갖습니다 int. 발현은 &a + 1차기 5 소자 어레이의 어드레스 수득 int다음 a과 같은 형식을 갖는다 int (*)[5]. 이 표현식 *(&a + 1)은의 결과를 역 참조하여 마지막 요소 다음에 오는 &a + 1첫 번째 주소를 생성하고 type을 가지며이 컨텍스트에서 type의 표현식으로 "부패"합니다 .intaint [5]int *

마찬가지로, 표현식 a은 배열의 첫 번째 요소에 대한 포인터로 "부패"하고 유형이 int *있습니다.

사진이 도움이 될 수 있습니다.

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

이것은 동일한 스토리지의 두 가지보기입니다. 왼쪽은 5 요소 배열의 int시퀀스로보고 있지만 오른쪽은 int. 또한 다양한 표현과 유형을 보여줍니다.

식,주의 *(&a + 1)의 결과 정의되지 않은 동작 :

...
결과가 배열 객체의 마지막 요소를 지난 지점을 가리키는 경우 평가되는 단항 * 연산자의 피연산자로 사용되지 않습니다.

C 2011 온라인 초안 , 6.5.6 / 9


13
텍스트가 공식이다 "사용할 수 없다"고 : 2018 C 6.5.6 (8)
에릭 Postpischil

@EricPostpischil : 2018 프리 펍 초안 (N1570.pdf와 유사)에 대한 링크가 있습니까?
John Bode

1
@JohnBode : 이 답변 에는 Wayback Machine 링크가 있습니다. 구입 한 사본에서 공식 표준을 확인했습니다.
Eric Postpischil

7
따라서이 size = (int*)(&a + 1) - a;코드를 작성한 경우 완전히 유효합니까? : o
기즈모

@Gizmo 그들은 아마도 원래 형식을 쓰지 않았을 것입니다. 원본은 아마도 다른 요소 유형에 대한 일반적인 유형의 사용을 위해 매크로로 정의되어 작성되었을 것입니다.
Leushenko

35

이 라인은 가장 중요합니다 :

size = *(&a + 1) - a;

보시다시피, 먼저 주소를 가져와 주소를 a추가합니다. 그런 다음 포인터를 역 참조하고 원래 값을 뺍니다 a.

C에서 포인터 산술로 인해 배열의 요소 수 또는가 반환 5됩니다. 하나를 추가 &a하면 다음 5 int초의 다음 배열에 대한 포인터 a입니다. 그 후,이 코드는 결과 포인터를 역 참조하고 그로부터 빼기 a(포인터로 쇠퇴 한 배열 유형)에서 배열의 요소 수를 제공합니다.

포인터 산술 작동 방식에 대한 세부 사항 :

유형 xyz을 가리키고 int값을 포함 하는 포인터가 있다고 가정하십시오 (int *)160. 에서 숫자를 빼면 xyzC에서 빼는 실제 양 xyz이 해당 숫자가 가리키는 형식 크기의 숫자에 해당한다고 지정합니다. 예를 들어, 당신이 차감 경우 5에서 xyz의 값 xyz이 될 것입니다 결과 xyz - (sizeof(*xyz) * 5)포인터 연산이 적용되지 않은 경우.

유형 a의 배열과 마찬가지로 5 int결과 값은 5입니다. 그러나 이것은 포인터에서만 작동하지 않으며 배열에서만 작동합니다. 포인터로 이것을 시도하면 결과는 항상입니다 1.

다음은 주소와 이것이 정의되지 않은 방법을 보여주는 작은 예입니다. 왼쪽은 주소를 보여줍니다 :

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

이것은 코드가 (또는 ) a에서 빼고 있음을 의미합니다 .&a[5]a+55

이것은 정의되지 않은 동작이므로 어떤 상황에서도 사용해서는 안됩니다. 이 동작이 모든 플랫폼에서 일관성이있을 것으로 기대하지 말고 프로덕션 프로그램에서 사용하지 마십시오.


27

흠, 나는 이것이 C의 초기에는 다시 작동하지 않았을 것이라고 생각합니다. 그러나 그것은 영리합니다.

한 번에 하나씩 단계 수행 :

  • &a int [5] 유형의 객체에 대한 포인터를 가져옵니다.
  • +1 해당 객체의 배열이 있다고 가정하고 다음 객체를 가져옵니다.
  • * 해당 주소를 int에 대한 유형 포인터로 효과적으로 변환
  • -a 두 개의 int 포인터를 빼고 그 사이의 int 인스턴스 수를 반환합니다.

진행중인 일부 유형 작업을 고려할 때 그것이 완전히 합법적이라고 확신하지 못합니다 (이것은 언어 변호사가 합법적이라는 것을 의미합니다. 예를 들어 두 개의 포인터가 같은 배열의 요소를 가리킬 때 빼는 것이 "허용"됩니다. *(&a+1)부모 배열이지만 다른 배열에 액세스하여 합성되었으므로 실제로 같은 배열에 대한 포인터는 아닙니다 a. 또한 배열의 마지막 요소를 지나서 포인터를 합성 할 수 있으며 모든 개체를 1 요소의 배열로 취급 할 수 있지만 역 참조 ( *) 작업은 이 합성 포인터에서 "허용되지"않습니다. 이 경우에는 동작이 없습니다!

C의 초기 (K & R 구문, 누군가?)에서 배열이 포인터로 훨씬 빨리 붕괴되어 *(&a+1)int ** 유형의 다음 포인터의 주소 만 반환 할 수 있다고 생각합니다. 현대 C ++에 대한보다 엄격한 정의는 확실히 배열 유형에 대한 포인터가 존재하고 배열 크기를 알 수있게하며 아마도 C 표준이 적합했을 것입니다. 모든 C 함수 코드는 포인터를 인수로만 사용하므로 기술적으로 눈에 띄는 차이는 최소화됩니다. 그러나 나는 여기서 추측하고 있습니다.

이러한 종류의 자세한 합법성 질문은 일반적으로 컴파일 된 코드가 아닌 C 인터프리터 또는 보푸라기 도구에 적용됩니다. 인터프리터는 구현할 런타임 기능이 하나 더 적기 때문에 2D 배열을 배열에 대한 포인터 배열로 구현할 수 있습니다.

또 다른 가능한 약점은 C 컴파일러가 외부 배열을 정렬 할 수 있다는 것입니다. 이것이 5 자 ( char arr[5]) 의 배열 인 경우 프로그램이 수행 &a+1할 때 "배열의 배열"동작을 호출 한다고 가정하십시오 . 컴파일러는 5 개의 문자 배열 ( char arr[][5]) 배열 이 실제로는 8 개의 문자 배열 ( char arr[][8])로 배열되어 외부 배열이 잘 정렬되도록 결정할 수 있습니다 . 우리가 논의하는 코드는 이제 배열 크기를 5가 아닌 8 로보 고합니다. 특정 컴파일러가 확실히 그렇게 할 것이라고 말하지는 않지만 그렇게 할 수 있습니다.


그럴 수 있지. 그러나 설명하기 어려운 이유로 모든 사람이 sizeof () / sizeof ()?
보석 테일러

5
대부분의 사람들이합니다. 예를 들어 sizeof(array)/sizeof(array[0])배열의 요소 수를 제공합니다.
SS Anne

C 컴파일러는 배열을 정렬 할 수는 있지만 그렇게 한 후에 배열 유형을 변경할 수 있는지 확신 할 수 없습니다. 패딩 바이트를 삽입하여 정렬을보다 현실적으로 구현할 수 있습니다.
Kevin

1
포인터를 빼는 것은 동일한 배열에 대한 두 개의 포인터로 제한되지 않으며 포인터는 배열의 끝을지나 하나가 될 수도 있습니다. &a+1정의됩니다. 존 볼링 거 (John Bollinger) *(&a+1)는 존재하지 않는 객체를 역 참조하려고하기 때문에 그렇지 않다.
Eric Postpischil

5
컴파일러는 char [][5]as를 구현할 수 없습니다 char arr[][8]. 배열은 반복되는 객체 일뿐입니다. 패딩이 없습니다. 또한 C 2018 6.5.3.4 7의 (비 규범 적) 예 2를 위반하여 배열의 요소 수를로 계산할 수 있습니다 sizeof array / sizeof array[0].
Eric Postpischil
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.