Différence entre le passage d'un tableau et d'un pointeur de tableau en fonction en C

110

Quelle est la différence entre les deux fonctions en C?

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

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

Si je devais appeler les fonctions sur un tableau sensiblement long, ces deux fonctions se comporteraient-elles différemment, prendraient-elles plus d'espace sur la pile?

Kaushik Shankar
la source

Réponses:

114

Tout d'abord, quelques standards :

6.7.5.3 Déclarateurs de fonction (y compris les prototypes)
...
7 Une déclaration d'un paramètre comme `` tableau de type '' doit être ajustée en `` pointeur qualifié vers le type '', où les qualificatifs de type (le cas échéant) sont ceux spécifiés dans [et ]de la dérivation de type tableau. Si le mot-clé staticapparaît également dans la dérivation de type tableau [et ]de, alors pour chaque appel à la fonction, la valeur de l'argument réel correspondant doit fournir l'accès au premier élément d'un tableau avec au moins autant d'éléments que spécifié par la taille expression.

Donc, en bref, tout paramètre de fonction déclaré comme T a[]ou T a[N]est traité comme s'il avait été déclaré T *a.

Alors, pourquoi les paramètres de tableau sont-ils traités comme s'ils étaient déclarés comme des pointeurs? Voici pourquoi:

6.3.2.1 Lvalues, tableaux et désignateurs de fonction
...
3 Sauf s'il s'agit de l'opérande de l' sizeofopérateur ou de l' &opérateur unaire , ou s'il s'agit d' une chaîne littérale utilisée pour initialiser un tableau, une expression de type '' tableau de type ' 'est converti en une expression avec le type' 'pointeur sur le type ' 'qui pointe vers l'élément initial de l'objet tableau et n'est pas une lvalue. Si l'objet tableau a une classe de stockage de registre, le comportement n'est pas défini.

Compte tenu du code suivant:

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

Dans l'appel à foo, l'expression de tableau arrn'est pas un opérande de l'un sizeofou l' autre &, donc son type est implicitement converti de "tableau à 10 éléments de int" en "pointeur vers int" selon 6.2.3.1/3. Ainsi, foorecevra une valeur de pointeur, plutôt qu'une valeur de tableau.

En raison de 6.7.5.3/7, vous pouvez écrire foocomme

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

mais il sera interprété comme

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

Ainsi, les deux formes sont identiques.

La dernière phrase du 6.7.5.3/7 a été introduite avec C99, et signifie essentiellement que si vous avez une déclaration de paramètre comme

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

le paramètre réel correspondant à adoit être un tableau d' au moins 10 éléments.

John Bode
la source
1
Il existe une différence lors de l'utilisation de compilateurs MSVC C ++ (au moins certains plus anciens), en raison du fait que le compilateur ne modifie pas correctement le nom de la fonction différemment dans les deux cas (tout en reconnaissant qu'ils sont les mêmes dans le cas contraire), ce qui entraîne des problèmes de liaison. Voir le rapport de bogue " Ne
résoudra
29

La différence est purement syntaxique. En C, lorsque la notation tableau est utilisée pour un paramètre de fonction, elle est automatiquement transformée en déclaration de pointeur.

Thomas Pornin
la source
1
@Kaushik: Bien qu'ils soient les mêmes dans ce cas, gardez à l'esprit qu'ils ne sont pas les mêmes dans le cas général
BlueRaja - Danny Pflughoeft
@BlueRaja: oui, c'est l'un des pièges de C. La déclaration des paramètres de fonction est très similaire à la déclaration de variables locales, mais il existe quelques différences subtiles (comme cette transformation automatique tableau en pointeur) qui sont enclin à mordre le programmeur imprudent.
Thomas Pornin
0

Non, il n'y a aucune différence entre eux. Pour tester, j'ai écrit ce code C dans le compilateur Dev C ++ (mingw):

#include <stdio.h>

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

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

Lorsque je désassemble la fonction principale dans .exe des deux versions appelantes du fichier binaire dans IDA, j'obtiens exactement le même code d'assemblage que ci-dessous:

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

Il n'y a donc aucune différence entre les deux versions de cet appel, du moins le compilateur les menace également.

caltuntas
la source
18
Désolé, mais cela prouve seulement que certaines versions de gcc génèrent le même assembly sur x86 pour les deux. Bonne réponse, mauvaise explication.
lambdapower