Paramètres et arguments
Deux mots employés indifféremment mais qu'il vaut la peine de séparer :
- Un paramètre est la variable dans la définition de la fonction -
int ndansint square(int n). - Un argument est la valeur que vous fournissez au point d'appel - le
4danssquare(4).
Appeler une fonction crée ses paramètres comme des variables locales neuves et y copie les arguments. Cette copie est tout le sujet de cette page.
Le C passe par valeur - toujours
C'est la règle, et le C n'y fait aucune exception : une fonction reçoit des copies de ses arguments. Affecter un paramètre ne change que la copie.
value vaut toujours 42. La fonction a reçu une copie nommée x, a mis la copie à zéro, et la copie a disparu au retour de la fonction. Renommer le paramètre en value ne change rien - le nom ne les relie pas ; ce sont deux variables différentes.
Il en va de même pour tous les types : int, double, char, un pointeur, et même une structure entière (copiée champ par champ). Si vous venez de Python, Java ou JavaScript, notez que ces langages passent des références d'objets par valeur, donc muter un objet dans une fonction est visible à l'extérieur. Le C n'a pas cela - il copie l'objet lui-même.
Le passage par valeur est une fonctionnalité, pas une limitation. Une fonction ne peut pas corrompre les données de l'appelant par accident, ce qui rend le raisonnement bien plus simple.
L'échec classique : swap
La démonstration canonique est une fonction qui essaie d'échanger deux valeurs :
La logique est correcte - elle échange bien a et b. Mais a et b sont des copies, donc le travail est jeté à l'instant où la fonction retourne.
Simuler le passage par référence avec des pointeurs
Pour qu'une fonction puisse changer une variable de l'appelant, donnez-lui l'adresse de la variable au lieu de sa valeur. Cette adresse est elle-même passée par valeur - le pointeur est copié - mais la copie pointe toujours sur l'objet d'origine, donc écrire à travers elle atteint la mémoire de l'appelant.
Trois morceaux de syntaxe portent toute l'idée :
int *adans la liste de paramètres déclareacomme « pointeur sur unint».&xau point d'appel produit l'adresse dex.*adans la fonction signifie « l'intqueapointe » - lisible et affectable.
C'est ce que les programmeurs C appellent « passage par référence », et il vaut la peine d'être précis : c'est toujours un passage par valeur. C'est le pointeur qui est copié. Affecter a lui-même (a = NULL;) ne changerait que la copie ; affecter *a change la variable de l'appelant. Les pointeurs couvrent le modèle en entier.
Le coût est qu'un point d'appel ne vous dit plus si une variable peut changer - c'est exactement pourquoi le & est obligatoire. swap(x, y) ne compilera pas ; vous devez écrire swap(&x, &y), et ce & est le marqueur visible que cet appel peut modifier x.
Renvoyer plusieurs résultats
return produit une valeur, donc les sorties supplémentaires voyagent par des paramètres pointeurs. La forme courante est un code d'état comme valeur de retour et les vrais résultats écrits à travers des pointeurs :
Notez que q et r gardent leurs valeurs précédentes quand l'appel échoue - la fonction a retourné avant d'écrire quoi que ce soit. C'est le contrat que l'appelant doit respecter, et c'est pourquoi la vérification de l'état précède l'usage des sorties.
L'alternative est de renvoyer une structure contenant les deux champs, ce qui évite entièrement les pointeurs et constitue souvent une meilleure conception quand les valeurs vont réellement ensemble.
Les tableaux sont différents
Les tableaux ne suivent pas la règle de copie, et cela piège presque tout le monde. Quand vous passez un tableau, il se dégrade en pointeur sur son premier élément - donc la fonction reçoit une adresse, pas une copie des données.
Le tableau de l'appelant a réellement changé, sans aucun & - parce que data est déjà une adresse dans ce contexte. Trois conséquences :
int a[] et int *a signifient exactement la même chose dans une liste de paramètres. Même int a[100] ; la taille est ignorée. Écrire int a[] documente l'intention, mais ne le lisez pas comme « un tableau est copié ».
sizeof dans la fonction est faux. Voici le bug :
Dans main, sizeof(data) vaut 20 octets et la longueur sort à 5. Dans la fonction, sizeof(a) est la taille d'un pointeur - 8 sur un système 64 bits - donc la « longueur » vaut 2. La taille du tableau n'est tout simplement pas disponible là. Passez toujours la longueur en paramètre séparé. Les compilateurs modernes avertissent sur ce point avec -Wall.
Utilisez const quand vous n'allez pas écrire. const int a[] dit que la fonction ne fait que lire, ce que le compilateur fait respecter et sur quoi un lecteur peut compter :
int sum(const int a[], int n); /* promet de ne pas modifier le tableau */
Les chaînes sont aussi des tableaux, donc la même chose s'applique : void greet(const char *name) est la signature standard d'une fonction qui lit une chaîne sans la modifier.
Et le coût ?
Passer une grosse structure par valeur en copie chaque octet à chaque appel. Pour une structure de quelques champs, c'est très bien et souvent plus rapide que l'indirection qu'ajoute un pointeur. Pour une grosse, passez un pointeur - et marquez-le const si la fonction ne fait que lire :
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* pas de copie de 512 octets par appel */
const struct Config *cfg signifie « un pointeur vers un Config que je ne modifierai pas », ce qui offre l'efficacité d'un pointeur avec la sûreté d'une copie.
Erreurs courantes
- Attendre d'un paramètre simple qu'il change la variable de l'appelant. Il ne le peut pas. Passez un pointeur.
- Oublier le
&au point d'appel.swap(x, y)là oùswapveut des pointeurs est une erreur de type - le compilateur l'attrape, mais seulement si un prototype est visible. - Oublier le
*dans la fonction.a = b;sur deux paramètres pointeurs échange les copies locales, pas les valeurs. Vous voulez*a = *b;. - Utiliser
sizeofsur un paramètre tableau. Toujours faux. Passez la longueur. - Renvoyer un pointeur vers une variable locale. La locale meurt au retour de la fonction, donc l'appelant détient un pointeur pendouillant, ce qui est un comportement indéfini. Renvoyez par valeur, écrivez dans un tampon fourni par l'appelant, ou allouez avec
malloc. - Ne pas vérifier un paramètre pointeur contre
NULL. Une fonction qui déréférence ce qu'on lui donne plantera sur un argument nul.
Questions fréquentes
Le C a-t-il le passage par référence ?
Non. Le C n'a qu'un seul mode : le passage par valeur. Une fonction reçoit toujours une copie de chaque argument, donc affecter un paramètre ne touche jamais la variable de l'appelant. Ce qui ressemble à un passage par référence est le passage d'un pointeur par valeur - le pointeur est copié, mais il pointe toujours sur l'objet de l'appelant, donc la fonction peut le modifier à travers le pointeur.
Comment passe-t-on un tableau à une fonction en C ?
Écrivez le paramètre int a[] ou int *a - c'est la même chose - et passez la longueur en paramètre séparé : int sum(const int a[], int n). Le tableau n'est pas copié ; la fonction reçoit un pointeur sur son premier élément, donc les modifications de a[i] sont visibles par l'appelant.
Pourquoi sizeof ne fonctionne-t-il pas sur un paramètre tableau en C ?
Parce que le paramètre est en réalité un pointeur. sizeof(a) dans la fonction donne la taille d'un pointeur (typiquement 8 octets), pas celle du tableau - donc sizeof(a)/sizeof(a[0]) donne quelque chose comme 2 au lieu du nombre d'éléments. Passez toujours la longueur explicitement.
Comment une fonction C peut-elle renvoyer plus d'une valeur ?
Passez des pointeurs pour les résultats supplémentaires et écrivez à travers eux : int divide(int a, int b, int *quotient, int *remainder) renvoie un indicateur de succès et remplit les deux sorties. L'alternative est de renvoyer une structure qui regroupe les valeurs.