매개 변수 선언에서 배열의 크기를 지정하는 이유는 무엇입니까?
#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에서 허용되는 이유가 있습니까? 아니면 기능이 누락 되었습니까?
답변
배열 매개 변수 선언에 일정한 크기가 포함 된 경우 함수가 예상하는 배열 크기를 표시하여 독자를위한 문서 역할 만 할 수 있습니다. 상수 표현식 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]
함수가 호출 될 때 해당 인수는n
C 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 foo
struct foo *x
함수 정의에서 배열의 크기를 지정하면 정적 분석 도구를 사용하여 오류를 확인하는 데 사용할 수 있습니다. 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
따라서 다른 변수로 배열의 크기를 전달해야합니다.
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에서 너무 많이 읽으면 해당 주소에있는 모든 것을 읽게됩니다.
이것은 현재까지도 보안 취약성의 매우 일반적인 소스입니다.
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
그러나이 트릭은 코드를 읽고 이해하기 조금 더 어렵게 만듭니다.