Essayer de coder ← Retour aux sujets

Définir et appeler

type nom(params)

Une fonction a un type de retour, un nom et des paramètres typés ; le compilateur doit la connaître avant son premier appel.

int carre(int x) { return x * x; }
Définition : renvoie un int
int carre(int x);
Prototype : annonce la fonction avant sa définition
void saluer(void) { … }
Ne renvoie rien et ne prend rien
y = carre(5);
Appel : y reçoit 25
return valeur;
Quitte la fonction en renvoyant la valeur
carre.c
#include <stdio.h>

int carre(int x);              /* prototype */

int main(void) {
    printf("%d\n", carre(7));
    return 0;
}

int carre(int x) {             /* définition */
    return x * x;
}
Affichage
49

Valeur ou adresse ?

passage

Les arguments sont copiés : la fonction travaille sur ses propres copies. Pour modifier une variable de l'appelant, on lui passe son adresse.

void f(int n)
Passage par valeur : n est une copie
void f(int *p)
Passage par adresse : la fonction peut modifier *p
f(&x);
On passe l'adresse de x
static int c = 0;
Variable locale qui garde sa valeur d'un appel à l'autre
void f(const int *t)
Promesse de ne pas modifier les données pointées
echanger.c
#include <stdio.h>

void echanger(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int x = 3, y = 7;
    echanger(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}
Affichage
x = 7, y = 3

📘 Le cours : structurer un programme en fonctions

Une fonction regroupe un morceau de code nommé et réutilisable. Découper un programme en petites fonctions (une idée par fonction) le rend plus lisible, plus facile à tester et à corriger. Tu en utilises déjà : printf, scanf et main sont des fonctions.

Anatomie d'une fonction

type_de_retour nom(type param1, type param2)
{
    /* corps de la fonction */
    return valeur;      /* du type annoncé */
}
  • Le type de retour dit ce que la fonction renvoie ; void signifie « rien ».
  • Les paramètres sont des variables locales initialisées avec les valeurs de l'appel (les arguments).
  • return termine la fonction immédiatement, même au milieu d'une boucle.
  • Écris void f(void) et non void f() : en C, des parenthèses vides veulent dire « paramètres non précisés ».

Prototype et définition

Le compilateur lit le fichier de haut en bas : une fonction doit être connue avant d'être appelée. Deux solutions : la définir avant main, ou écrire un prototype (la première ligne, terminée par ;) en haut du fichier et donner la définition plus loin, comme dans l'exemple de la première carte.

Fonction sans résultat

Une fonction void agit (elle affiche, modifie, enregistre…) sans renvoyer de valeur.

barre.c
#include <stdio.h>

void barre(int longueur) {
    for (int i = 0; i < longueur; i++) {
        printf("=");
    }
    printf("\n");
}

int main(void) {
    barre(10);
    printf("NSI\n");
    barre(10);
    return 0;
}
Affichage
==========
NSI
==========

Passage par valeur

En C, tout est passé par valeur : la fonction reçoit une copie de chaque argument. Modifier le paramètre ne change donc pas la variable de l'appelant. Pour la modifier, on passe son adresse avec & et la fonction écrit à cette adresse avec *.

copie.c
#include <stdio.h>

void ajoute_un(int n) {
    n = n + 1;              /* modifie la copie locale */
}

void ajoute_un_adr(int *n) {
    *n = *n + 1;            /* modifie la variable de l'appelant */
}

int main(void) {
    int a = 5;
    ajoute_un(a);
    printf("%d\n", a);      /* inchangé */
    ajoute_un_adr(&a);
    printf("%d\n", a);      /* augmenté de 1 */
    return 0;
}
Affichage
5
6

Renvoyer plusieurs résultats

Une fonction ne renvoie qu'une seule valeur. Pour en produire plusieurs, on passe des adresses dans lesquelles elle écrit ses résultats.

extremes.c
#include <stdio.h>

void extremes(const int t[], int n, int *min, int *max) {
    *min = t[0];
    *max = t[0];
    for (int i = 1; i < n; i++) {
        if (t[i] < *min) *min = t[i];
        if (t[i] > *max) *max = t[i];
    }
}

int main(void) {
    int notes[] = {12, 7, 15, 9};
    int petit, grand;

    extremes(notes, 4, &petit, &grand);
    printf("min = %d, max = %d\n", petit, grand);
    return 0;
}
Affichage
min = 7, max = 15

Portée des variables

  • Une variable locale (dans une fonction ou un bloc { }) n'existe que dans ce bloc : elle disparaît à la sortie.
  • Une variable globale (hors de toute fonction) est visible partout. À éviter : n'importe quelle fonction peut la modifier, ce qui rend les bugs difficiles à trouver.
  • Une variable locale static est créée une seule fois et garde sa valeur entre deux appels.
static.c
#include <stdio.h>

int compteur(void) {
    static int appels = 0;   /* initialisée une seule fois */
    appels++;
    return appels;
}

int main(void) {
    printf("%d\n", compteur());
    printf("%d\n", compteur());
    printf("%d\n", compteur());
    return 0;
}
Affichage
1
2
3

Récursivité

Une fonction récursive s'appelle elle-même. Elle a besoin d'un cas de base qui arrête la récursion et d'un appel sur un cas plus petit. Sans cas de base, les appels s'empilent jusqu'au dépassement de la pile (*stack overflow*) et le programme plante.

factorielle.c
#include <stdio.h>

unsigned long long fact(unsigned int n) {
    if (n <= 1) {
        return 1;                /* cas de base */
    }
    return n * fact(n - 1);      /* appel sur un cas plus petit */
}

int main(void) {
    printf("5! = %llu\n", fact(5));
    printf("20! = %llu\n", fact(20));
    return 0;
}
Affichage
5! = 120
20! = 2432902008176640000

Au-delà de 20!, le résultat dépasse ce qu'un unsigned long long peut contenir et devient faux : c'est le dépassement de capacité vu dans Entiers signés.

Découper en plusieurs fichiers

Dans un vrai projet, les prototypes sont regroupés dans un fichier d'en-tête .h et les définitions dans un .c. Le garde d'inclusion (#ifndef / #define / #endif) évite d'inclure deux fois le même en-tête.

utils.h
#ifndef UTILS_H
#define UTILS_H

int carre(int x);
int max2(int a, int b);

#endif

On l'inclut avec des guillemets : #include "utils.h" (fichier du projet) alors que #include <stdio.h> désigne une bibliothèque du système. Compilation : gcc main.c utils.c -o main.

⚠️ Erreurs fréquentes

Appeler une fonction avant de la déclarer

Si la définition vient après main et qu'il n'y a pas de prototype, gcc signale une *implicit declaration* : la fonction est devinée de travers. Ajoute le prototype en haut du fichier.

Oublier le return

À éviter
faux.c
int max2(int a, int b) {
    if (a > b) {
        return a;
    }
    /* si a <= b : rien n'est renvoyé */
}
Correct
correct.c
int max2(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

Croire qu'une fonction modifie son argument

Après ajoute_un(a), a n'a pas bougé : la fonction a travaillé sur une copie. Passe &a et reçois un int * si tu veux modifier la variable.

Renvoyer l'adresse d'une variable locale

À éviter
faux.c
int *cree(void) {
    int x = 42;
    return &x;     /* x disparaît au return ! */
}
Correct
correct.c
int cree(void) {
    int x = 42;
    return x;      /* on renvoie la valeur */
}

Une variable locale n'existe plus après le return : son adresse devient invalide (*dangling pointer*). Pour garder des données, renvoie la valeur ou alloue de la mémoire dynamique (voir Mémoire dynamique).

Une récursion sans fin

Une fonction récursive sans cas de base, ou dont l'appel ne se rapproche pas du cas de base, plante par dépassement de pile. Vérifie toujours : « quel est le cas le plus simple ? » et « l'argument diminue-t-il à chaque appel ? ».

📋 Aide-mémoire

  • type nom(type param, …) { … return valeur; } ; void si rien n'est renvoyé ; (void) si rien n'est reçu.
  • Prototype (int f(int x);) avant l'appel, ou définition avant main.
  • Arguments copiés : pour modifier la variable de l'appelant, passe &x et reçois un int *p (écris *p).
  • Variable locale : propre à son bloc ; static : garde sa valeur ; globale : à éviter.
  • Récursion : un cas de base + un appel sur un cas plus petit.
  • .h : prototypes et garde d'inclusion ; .c : définitions ; #include "fichier.h" pour ses propres en-têtes.