Różnica między przekazywaniem tablicy i wskaźnika tablicy do funkcji w C

110

Jaka jest różnica między tymi dwiema funkcjami w C?

void f1(double a[]) {
   //...
}

void f2(double *a) {
   //...
}

Gdybym miał wywołać funkcje na zasadniczo długiej tablicy, czy te dwie funkcje zachowywałyby się inaczej, czy zajmowałyby więcej miejsca na stosie?

Kaushik Shankar
źródło

Odpowiedzi:

114

Najpierw kilka standardowych :

6.7.5.3 Deklaratory funkcji (w tym prototypy)
...
7 Deklaracja parametru jako `` tablica typu '' powinna być dostosowana do `` kwalifikowanego wskaźnika do typu '', gdzie kwalifikatorami typu (jeśli istnieją) są te określone w obrębie [i ]wyprowadzenia typu tablicy. Jeśli słowo kluczowe staticwystępuje również w [i ]w wyprowadzeniu typu tablicy, to dla każdego wywołania funkcji wartość odpowiedniego rzeczywistego argumentu zapewnia dostęp do pierwszego elementu tablicy zawierającego co najmniej tyle elementów, ile określono w rozmiarze wyrażenie.

Krótko mówiąc, każdy parametr funkcji zadeklarowany jako T a[]lub T a[N]jest traktowany tak, jakby był zadeklarowany T *a.

Dlaczego więc parametry tablicowe są traktowane tak, jakby były zadeklarowane jako wskaźniki? Dlatego:

6.3.2.1 Wartość lvalues, tablice i desygnatory funkcji
...
3 Z wyjątkiem sytuacji, gdy jest to operand sizeofoperatora lub operator jednoargumentowy &lub literał łańcuchowy używany do inicjalizacji tablicy, wyrażenie o typie `` tablica typu '' 'jest konwertowane na wyrażenie z typem' 'wskaźnik do typu ' ', które wskazuje na początkowy element obiektu tablicy i nie jest lwartością. Jeśli obiekt tablicy ma klasę pamięci rejestru, zachowanie jest niezdefiniowane.

Biorąc pod uwagę następujący kod:

int main(void)
{
  int arr[10];
  foo(arr);
  ...
}

W wywołaniu do foo, wyrażenie tablicowe arrnie jest operandem ani sizeoflub &, więc jego typ jest niejawnie konwertowany z „10-elementowej tablicy int” na „wskaźnik do int” zgodnie z 6.2.3.1/3. W ten sposób foootrzyma wartość wskaźnika, a nie wartość tablicy.

Z powodu 6.7.5.3/7 możesz pisać foojako

void foo(int a[]) // or int a[10]
{
  ...
}

ale będzie to interpretowane jako

void foo(int *a)
{
  ...
}

Zatem te dwie formy są identyczne.

Ostatnie zdanie w 6.7.5.3/7 zostało wprowadzone za pomocą C99 i zasadniczo oznacza, że ​​jeśli masz deklarację parametru, taką jak

void foo(int a[static 10])
{
  ...
}

rzeczywisty parametr odpowiadający amusi być tablicą zawierającą co najmniej 10 elementów.

John Bode
źródło
1
Istnieje różnica w przypadku używania (przynajmniej niektórych starszych) kompilatorów MSVC C ++, ponieważ kompilator nieprawidłowo zmienia nazwę funkcji w obu przypadkach (rozpoznając, że są one takie same w innym przypadku), co powoduje problemy z łączem. Zobacz raport o błędzie „Nie naprawię” tutaj connect.microsoft.com/VisualStudio/feedback/details/326874/ ...
greggo
29

Różnica jest czysto składniowa. W języku C, gdy notacja tablicowa jest używana jako parametr funkcji, jest ona automatycznie przekształcana w deklarację wskaźnika.

Thomas Pornin
źródło
1
@Kaushik: Chociaż w tym przypadku są takie same, pamiętaj, że w ogólnym przypadku
BlueRaja - Danny Pflughoeft
@BlueRaja: tak, jest to jedna z pułapek C. Deklaracja parametrów funkcji jest bardzo podobna do deklaracji zmiennych lokalnych, ale istnieje kilka subtelnych różnic (takich jak automatyczna transformacja tablicy do wskaźnika), które są skłonny do gryzienia nieostrożnego programisty.
Thomas Pornin,
0

Nie, nie ma między nimi różnicy. Aby przetestować napisałem ten kod w C w kompilatorze Dev C ++ (mingw):

#include <stdio.h>

void function(int* array) {
     int a =5;
}

void main() {  
     int array[]={2,4};
     function(array);
     getch();
}

Kiedy demonstruję główną funkcję w .exe obu wywołujących wersje pliku binarnego w IDA, otrzymuję dokładnie taki sam kod asemblera jak poniżej:

push    ebp
mov     ebp, esp
sub     esp, 18h
and     esp, 0FFFFFFF0h
mov     eax, 0
add     eax, 0Fh
add     eax, 0Fh
shr     eax, 4
shl     eax, 4
mov     [ebp+var_C], eax
mov     eax, [ebp+var_C]
call    sub_401730
call    sub_4013D0
mov     [ebp+var_8], 2
mov     [ebp+var_4], 4
lea     eax, [ebp+var_8]
mov     [esp+18h+var_18], eax
call    sub_401290
call    _getch
leave
retn

Więc nie ma różnicy między dwiema wersjami tego wywołania, przynajmniej kompilator grozi im jednakowo.

caltuntas
źródło
18
Przykro nam, ale to tylko dowodzi, że niektóre wersje gcc generują ten sam zestaw na x86 dla obu. Prawidłowa odpowiedź, złe wyjaśnienie.
lambdapower