매개 변수 선언에서 배열의 크기를 지정하는 이유는 무엇입니까?

Nov 19 2020
#include <stdio.h>

int a[] = {1,2};

void test(int in[3]){
  //
}
 
int main() {
 test(a); 
 return 0;
}

위의 코드 int in[3]에서 int *in. 숫자 3는 실제로 아무 일도하지 않고 정확한 크기도 아니지만 컴파일러가 불평하지 않습니다. 이 구문이 C에서 허용되는 이유가 있습니까? 아니면 기능이 누락 되었습니까?

답변

5 EricPostpischil Nov 19 2020 at 19:40

배열 매개 변수 선언에 일정한 크기가 포함 된 경우 함수가 예상하는 배열 크기를 표시하여 독자를위한 문서 역할 만 할 수 있습니다. 상수 표현식 n의 경우 컴파일러는 int in[n]to 와 같은 배열 선언을 변환합니다. int *in그 후에는 컴파일러에 차이가 없으므로 값의 영향을받지 않습니다 n.

원래 C에서 함수 매개 변수는 다음과 같이 초기 함수 선언 이후 선언 목록에 의해 지정되었습니다.

int f(a, b, c)
int a;
float b;
int c[3];
{
    … function body
}

나는 다른 선언과 동일한 문법을 ​​사용했기 때문에 이러한 선언에서 배열 크기가 허용되었다고 추측합니다. 단순히 크기가 발생하도록 허용하지만 무시하는 것보다 크기를 제외한 컴파일러 코드와 문서를 작성하는 것이 더 어려웠을 것입니다. 함수 프로토 타입 ( int f(int a, float b, int c[3])) 내에서 매개 변수 유형을 선언 할 때 동일한 추론이 적용된 것으로 추측합니다.

하나:

  • static에서와 같이 선언 에이 포함 된 경우 int in[static n]함수가 호출 될 때 해당 인수는 nC 2018 6.7.6.3 7에 따라 최소한 요소를 가리켜 야합니다. 컴파일러는이를 최적화에 사용할 수 있습니다.
  • 배열 크기가 상수가 아닌 경우 함수가 호출 될 때 컴파일러에서 평가할 수 있습니다. 예를 들어 함수 선언이 void test(int in[printf("Hi")])이면 함수가 호출 될 때 GCC 10.2와 Apple Clang 11.0 모두 "Hi"를 인쇄합니다. (그러나 C 표준이이 평가를 요구하는 것은 분명하지 않습니다.)
  • 이 조정은 실제 배열 매개 변수에 대해서만 발생하며 그 안에있는 배열에는 적용되지 않습니다. 예를 들어, 매개 변수 선언 int x[3][4]에서의 유형은 x로 조정됩니다 int (*)[4]. 4는 크기의 일부로 남아 있으며를 사용하는 포인터 산술에 영향을 x줍니다.
  • 매개 변수가 배열로 선언되면 요소 유형이 완전해야합니다. 반대로 포인터로 선언 된 매개 변수는 완전한 유형을 가리킬 필요가 없습니다. 예를 들어 는 완전히 정의 되지 않았지만 그렇지 않은 struct foo x[3]경우 진단 메시지를 생성합니다 .struct foostruct foo *x
2 KrishnaKanthYenumula Nov 19 2020 at 12:45

함수 정의에서 배열의 크기를 지정하면 정적 분석 도구를 사용하여 오류를 확인하는 데 사용할 수 있습니다. cppcheck다음 코드에 도구를 사용했습니다 .

#include <stdio.h>
void test(int in[3])
{
  in[3] = 4;
}

출력은 다음과 같습니다.

Cppcheck 2.2
[test.cpp:4]: (error) Array 'in[3]' accessed at index 3, which is out of bounds.
Done!

그러나 크기를 지정하지 않으면에서 오류가 발생하지 않습니다 cppcheck.

#include <stdio.h>
void test(int in[])
{
   in[3] = 4;
}

출력은 다음과 같습니다.

Cppcheck 2.2
Done!

그러나 일반적으로 함수 정의에서 배열의 크기를 지정할 필요가 없습니다. sizeof포인터의 값만 복사되기 때문에 연산자를 사용하여 다른 함수 내에서 배열의 크기를 찾을 수 없습니다 . 따라서 sizeof연산자의 입력은 유형이 int*아니라 유형이됩니다 int[](함수 내부 test()). 따라서 배열 크기 값은 코드에 영향을주지 않습니다. 아래 코드를 참조하십시오.

#include <stdio.h>

int a[] = {1, 2, 3, 4, 5, 6, 7, 8};

void test(int in[8]) // Same as void test(int *arr)
{
    unsigned int n = sizeof(in) / sizeof(in[0]); // sizeof(int*)/sizeof(int)
    printf("Array size inside test() is %d\n", n);
}

int main()
{
    unsigned int n = sizeof(a) / sizeof(a[0]);  //sizeof(int[])/sizeof(int)
    printf("Array size inside main() is %d\n", n);
    test(a);
    return 0;
}

출력은 다음과 같습니다.

Array size inside main() is 8
Array size inside test() is 2

따라서 다른 변수로 배열의 크기를 전달해야합니다.

1 Simson Nov 19 2020 at 11:56

C에서는 한 구조에 대한 포인터와 동일한 데이터 구조의 배열에 대한 포인터 사이에 차이가 없습니다. 다음의 시작 주소를 얻으려면 데이터의 크기로 포인터를 증가시키기 만하면됩니다. 포인터 자체에서만 크기를 결정할 수 없기 때문에이를 프로그래머로 제공해야합니다.

프로그램을 수정 해 보겠습니다.

#include <stdio.h>

void test(int in[3]){
  printf("%d %d,%d,%d\n",in[0],in[1],in[2],in[3]); // !Sic bug intentional 
}
int main() {
 int a[] = {1,2};
 int b[] = {3,4};
 test(a); 
 test(b); 
 return 0;
}

그리고 그것을 실행하십시오.

$ gcc pointer_size.c  -o a.out  && ./a.out 

1 2,3,4
3 4,-1420617472,-1719256057

이 경우 배열은 서로 연달아 배치되므로 a에서 인덱스 2와 3을 읽으면 b에서 데이터가 생성되고 b에서 너무 많이 읽으면 해당 주소에있는 모든 것을 읽게됩니다.

이것은 현재까지도 보안 취약성의 매우 일반적인 소스입니다.

1 Lundin Nov 19 2020 at 20:03

C 언어와 컴파일러가 신경 쓰는 한, 배열이 어쨌든 첫 번째 요소에 대한 포인터로 조정되기 때문에 크기를 지정하더라도 중요하지 않습니다.

그러나 크기를 지정하면 컴파일러가 아닌 외부 도구에 의한 정적 분석 기능이 향상 될 수 있습니다. 예를 들어 정적 분석기는 이것이 배열 범위를 벗어난 버그임을 쉽게 알 수 있습니다.

void test(int in[3]){
  in[3] = 0;
}

그러나 이것이 버그인지는 알 수 없습니다.

void test(int* in){
  in[3] = 0;
}

이와 관련하여 서로 다른 배열 크기간에 존재하지 않는 유형 안전성은 실제로 트릭을 사용하여 해결할 수 있습니다. 대신 포인터로 배열을 전달합니다. 배열에 대한 포인터는 쇠퇴하지 않고 올바른 크기를 가져 오는 것에 대해 까다롭기 때문입니다. 예:

void test(int (*in)[3]){
  int* ptr = *in;
  ptr[3] = 0;
}

int foo[10];
test(&foo);  // compiler error

int bar[3];
test(&bar);  // ok

그러나이 트릭은 코드를 읽고 이해하기 조금 더 어렵게 만듭니다.