Jaki jest sens określania rozmiaru tablicy w deklaracji parametrów?

Nov 19 2020
#include <stdio.h>

int a[] = {1,2};

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

W powyższym kodzie int in[3]jest to samo co int *in. Liczba 3tak naprawdę nic nie robi i nie ma nawet prawidłowego rozmiaru, ale mimo to kompilator nie narzeka. Czy jest więc powód, dla którego ta składnia jest akceptowana w C lub brakuje mi funkcji?

Odpowiedzi

5 EricPostpischil Nov 19 2020 at 19:40

Gdy deklaracja parametru tablicy zawiera stały rozmiar, jedynym celem, jaki może służyć, jest dokumentacja dla czytelników, wskazując im, jakiego rozmiaru tablica oczekuje funkcja. W przypadku wyrażenia stałego nkompilator konwertuje deklarację tablicy, taką jak int in[n]to int *in, po czym nie ma różnicy w stosunku do kompilatora, a więc wartość n.

Pierwotnie w C parametry funkcji były określane przez listę deklaracji po początkowej deklaracji funkcji, na przykład:

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

Przypuszczam, że rozmiary tablic były dozwolone w tych deklaracjach po prostu dlatego, że używały tej samej gramatyki, co inne deklaracje. Trudniej byłoby napisać kod kompilatora i dokumentację wykluczającą rozmiary, niż po prostu pozwolić im występować, ale je zignorować. Kiedy wprowadzono deklarowanie typów parametrów wewnątrz prototypów funkcji ( int f(int a, float b, int c[3])), przypuszczam, że zastosowano to samo rozumowanie.

Jednak:

  • Jeśli deklaracja zawiera static, jak w int in[static n], to przy wywołaniu funkcji odpowiedni argument musi wskazywać co najmniej na nelementy, zgodnie z C 2018 6.7.6.3 7. Kompilatory mogą to wykorzystać do optymalizacji.
  • Jeśli rozmiar tablicy nie jest stałą, może zostać oszacowany przez kompilator podczas wywołania funkcji. Na przykład, jeśli deklaracja funkcji to void test(int in[printf("Hi")]), to zarówno GCC 10.2, jak i Apple Clang 11.0 wypisują „Hi”, gdy funkcja jest wywoływana. (Jednak nie jest dla mnie jasne, że standard C wymaga takiej oceny).
  • To dopasowanie dotyczy tylko rzeczywistego parametru tablicy, a nie tablic w nim zawartych. Na przykład w deklaracji parametru dostosowywany jest int x[3][4]typ . 4 pozostaje częścią rozmiaru i ma wpływ na arytmetykę wskaźnika z .xint (*)[4]x
  • Gdy parametr jest zadeklarowany jako tablica, typ elementu musi być kompletny. W przeciwieństwie do tego parametr zadeklarowany jako wskaźnik nie musi wskazywać na pełny typ. Na przykład struct foo x[3]zwraca komunikat diagnostyczny, jeśli struct foonie został w pełni zdefiniowany, ale struct foo *xtak się nie dzieje.
2 KrishnaKanthYenumula Nov 19 2020 at 12:45

Jeśli określimy rozmiar tablicy w definicji funkcji, można ją wykorzystać do sprawdzania błędów za pomocą narzędzia do analizy statycznej. Użyłem cppchecknarzędzia do następującego kodu.

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

Wynik to:

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

Ale jeśli nie podasz żadnego rozmiaru, nie otrzymasz żadnego błędu cppcheck.

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

Wynik to:

Cppcheck 2.2
Done!

Ale generalnie nie ma potrzeby określania rozmiaru tablicy w definicji funkcji. Nie możemy znaleźć rozmiaru tablicy wewnątrz innej funkcji za pomocą sizeofoperatora, ponieważ kopiowana jest tylko wartość wskaźnika. Stąd dane wejściowe sizeofoperatora będą typu, int*a nie typu int[](wewnątrz funkcji test()). Zatem wartość rozmiaru tablicy nie wpływa na kod. Zobacz poniższy kod:

#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;
}

Wynik to:

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

Tak więc musimy przekazać rozmiar tablicy z inną zmienną.

1 Simson Nov 19 2020 at 11:56

W języku C nie ma różnicy między wskaźnikiem do jednej struktury a wskaźnikiem do tablicy o tej samej strukturze danych. Aby uzyskać adres początkowy następnego, po prostu zwiększaj wskaźnik o rozmiar danych, a ponieważ nie jest możliwe określenie rozmiaru na podstawie samego wskaźnika, musisz go podać jako programista.

Spróbujmy zmodyfikować program

#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;
}

I uruchom to:

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

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

W tym przypadku tablice są umieszczane tyłem do siebie, więc odczyt z indeksów 2 i 3 z a da dane z b, a gdy przeczytamy za dużo z b, odczytane zostanie wszystko, co jest pod tymi adresami.

Jest to bardzo powszechne źródło luk w zabezpieczeniach nawet do dziś.

1 Lundin Nov 19 2020 at 20:03

Jeśli chodzi o język C i kompilator, nie ma znaczenia, czy określisz rozmiar, ponieważ tablica i tak zostanie dostosowana do wskaźnika do pierwszego elementu.

Jednak określenie rozmiaru może poprawić możliwość analizy statycznej przez narzędzia zewnętrzne inne niż kompilator. Na przykład analizator statyczny może łatwo stwierdzić, że jest to błąd spoza zakresu tablicy:

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

Ale nie ma pojęcia, czy to błąd:

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

W związku z tym nieistniejące bezpieczeństwo typów między różnymi rozmiarami tablic można w rzeczywistości rozwiązać za pomocą sztuczki - przepuszczaj tablice przez wskaźnik. Ponieważ wskaźnik do tablicy nie gnije i jest wybredny, jeśli chodzi o uzyskanie odpowiedniego rozmiaru. Przykład:

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

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

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

Jednak ta sztuczka sprawia, że ​​kod jest nieco trudniejszy do odczytania i zrozumienia.