Fonctions et modularitéDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Programmation en C · C2 Structures de contrôle et fonctions · Chapitre 2 · 7 h

Fonctions et modularité

Prototype, définition, valeur de retour ; portée des variables ; passage par valeur ; fonctions récursives ; compilation séparée, en-têtes et Makefile.

Écrivons la fonction la plus banale qui soit : échanger deux variables.

void echanger(int a, int b) {    int t = a;    a = b;    b = t;} int main(void) {    int x = 3, y = 7;    echanger(x, y);    printf("%d %d\n", x, y);      /* affiche 3 7 */}

Elle ne fonctionne pas. Le code est correct, il compile sans le moindre avertissement, et il ne fait rien. La raison tient en une règle que ce chapitre doit installer : en C, une fonction ne reçoit jamais vos variables, elle en reçoit des copies.

C'est la difficulté du bloc II, et sa résolution demandera le bloc IV — d'où la place de ce chapitre, juste avant les tableaux et juste après le contrôle du flux.

Déclarer et définir

Le C distingue deux choses que les langages récents confondent.

La définition est le code complet de la fonction. La déclaration — ou prototype — n'est que sa signature, terminée par un point-virgule.

int maximum(int a, int b);              /* déclaration : la signature */ int maximum(int a, int b) {             /* définition : le code */    return (a > b) ? a : b;}

Le prototype existe parce que le compilateur travaille de haut en bas et un fichier à la fois — c'est le chapitre 1. Pour vérifier un appel, il doit connaître la signature avant de rencontrer l'appel. Deux solutions : définir la fonction plus haut, ou la déclarer en tête de fichier. La seconde est la bonne, parce qu'elle permet de ranger les fonctions dans un ordre logique et qu'elle est indispensable dès qu'il y a plusieurs fichiers.

Sans prototype, les compilateurs anciens supposaient un retour int et ne vérifiaient rien. La norme C99 a interdit cette « déclaration implicite », mais gcc se contente parfois d'un avertissement : c'en est un qu'il ne faut jamais ignorer, car il signale que le compilateur invente une signature.

Un mot sur void, qui a deux emplois distincts. En type de retour, il signifie « ne rend rien ». En liste de paramètres, int f(void) signifie « ne prend aucun paramètre » — tandis que int f() signifie historiquement « prend un nombre non spécifié de paramètres », ce qui désactive toute vérification. Écrire (void) n'est donc pas une coquetterie.

Le passage par valeur

Voici la règle, et il faut la formuler exactement. Tout argument est copié dans un nouvel emplacement mémoire — un paramètre est une variable locale à la fonction, initialisée avec la valeur de l'argument.

main                          echanger┌──────────┐                  ┌──────────┐│ x  │  3  │  ──copie──►      │ a  │  3  ││ y  │  7  │  ──copie──►      │ b  │  7  │└──────────┘                  └──────────┘                              après l'échange : a = 7, b = 3                              …et les copies disparaissent au retour

La fonction a parfaitement échangé ses copies. Celles-ci vivent dans son cadre d'appel — la pile du chapitre 1 d'Algorithmique 2 — et ce cadre est détruit au retour. Les variables de main n'ont jamais été touchées.

Le passage par valeur a un mérite considérable, qu'il faut souligner avant d'en montrer la limite : une fonction ne peut pas modifier ce qu'on ne lui a pas explicitement donné le droit de modifier. C'est une garantie de raisonnement locale, que peu de langages offrent aussi franchement.

La limite est évidente : il faut bien pouvoir modifier. La solution du C est de passer non pas la variable, mais son adresse — le passage par adresse du chapitre 7. Vous l'avez déjà rencontré sans le nommer : c'est l'esperluette de scanf("%d", &age). Cette fonction doit modifier votre variable, donc elle en reçoit l'adresse.

Retenez la formulation exacte : le C ne connaît que le passage par valeur. Le passage par adresse n'est pas une seconde façon de passer les arguments — c'est un passage par valeur d'une adresse.

Portée et durée de vie

Ce sont deux notions distinctes, et les confondre produit des erreurs curieuses.

La portée dit un nom est visible. La durée de vie dit combien de temps la donnée existe.

DéclarationPortéeDurée de vieInitialisation par défaut
Variable localele blocl'exécution du blocaucune (contenu indéfini)
Paramètrela fonctionl'appella valeur de l'argument
Variable globaletout le fichier, voire le programmetout le programmezéro
Locale staticle bloctout le programmezéro

Les deux dernières lignes appellent des commentaires.

Une variable globale est visible partout et vit du début à la fin. C'est commode, et c'est généralement une mauvaise idée : n'importe quelle fonction peut la modifier, donc plus aucun raisonnement local n'est possible, et les bogues qui en découlent sont difficiles à localiser. On les réserve aux constantes et aux cas où l'on a une raison précise.

Une locale static conserve sa valeur d'un appel à l'autre : elle est allouée une fois pour toutes, comme une globale, mais son nom reste confiné au bloc. C'est le moyen propre de donner une mémoire à une fonction — un compteur d'appels, un cache — sans polluer l'espace de noms.

Le mot-clé static a d'ailleurs un second sens, qui n'a rien à voir : appliqué à une fonction ou à une variable globale, il en restreint la visibilité au fichier. C'est l'équivalent C du « privé », et c'est la bonne pratique pour toute fonction auxiliaire qu'on ne veut pas exposer — l'éditeur de liens du chapitre 1 ne verra même pas le symbole.

Récursivité en C

Rien de nouveau par rapport au bloc I d'Algorithmique 2 : mêmes cas de base, même variant, même pile.

unsigned long factorielle(unsigned int n) {    if (n <= 1) return 1;               /* cas de base */    return n * factorielle(n - 1);      /* cas récursif */}

Deux précisions propres au C.

La pile est finie et petite : 8 Mio par défaut sous Linux, soit quelques centaines de milliers de cadres. Un dépassement ne produit pas d'exception : le programme est tué par le système avec une erreur de segmentation, message trompeur qui suggère un problème de pointeur alors qu'il s'agit d'une récursion mal terminée.

L'optimisation d'appel terminal n'est pas garantie par la norme. gcc -O2 l'applique souvent, gcc -O0 presque jamais — donc un code qui n'y survit qu'en mode optimisé est un code fragile. En C, une récursion profonde s'écrit itérativement.

Compilation séparée

Dès que le projet dépasse quelques centaines de lignes, on le découpe — et l'on retrouve les étages du chapitre 1.

Chaque module a deux fichiers. Le .h contient l'interface : prototypes, types, constantes. Le .c contient l'implémentation. Les autres modules incluent le .h et ignorent tout du .c.

/* pile.h */#ifndef PILE_H                    /* garde d'inclusion */#define PILE_H typedef struct Pile Pile;         /* type opaque : le contenu reste caché */void empiler(Pile *p, int v);int  depiler(Pile *p); #endif

La garde d'inclusion est indispensable : le #include étant un copier-coller, un en-tête inclus par deux chemins différents serait recopié deux fois, et le compilateur signalerait des redéfinitions. Le trio #ifndef / #define / #endif fait que la seconde copie est vide.

La compilation se fait alors en deux temps — chaque .c vers son .o, puis l'édition de liens — ce qui a un avantage décisif sur les gros projets : modifier un fichier ne demande de recompiler que celui-là. C'est le travail du Makefile, qui décrit les dépendances et ne reconstruit que ce dont la source est plus récente que la cible.

programme: main.o pile.o	gcc -o programme main.o pile.o main.o: main.c pile.h	gcc -Wall -g -c main.c clean:	rm -f *.o programme

Deux règles à connaître : les lignes de commande commencent par une tabulation, pas des espaces — l'erreur la plus fréquente des débutants — et un .o doit dépendre des .h qu'il inclut, sans quoi une modification d'interface ne déclenche aucune recompilation.

Quiz · 1 question

Pourquoi la fonction void echanger(int a, int b) ne modifie-t-elle pas les variables de l'appelant ?

  • Parce qu'elle est déclarée void : une fonction sans valeur de retour ne peut rien modifierà cause de void
  • Parce que les arguments sont COPIÉS : a et b sont des variables locales à la fonction, initialisées avec les valeurs de x et y. Elle échange ses copies, qui disparaissent au retourcopie des arguments
  • Parce qu'il manque le mot-clé static devant les paramètresstatic manquant

Réponse : Le C ne connaît QUE le passage par valeur. Un paramètre est une variable locale, allouée dans le cadre d'appel et initialisée avec la valeur de l'argument — les variables de l'appelant ne sont jamais atteignables. La fonction échange donc parfaitement ses deux copies, puis le cadre est détruit au retour et le travail est perdu. Le type void n'y est pour rien : une fonction qui rend une valeur aurait exactement le même comportement sur ses paramètres. Cette règle a un mérite qu'il faut voir : une fonction ne peut pas modifier ce qu'on ne lui a pas donné le droit de modifier, ce qui rend le raisonnement local. Pour permettre la modification, on passe l'ADRESSE de la variable — c'est le & de scanf, et c'est le chapitre 7.

Quiz · 1 question

Quelle est la différence entre une variable globale et une variable locale déclarée static ?

  • Aucune : static sur une locale la transforme exactement en globaleidentiques
  • Leur DURÉE DE VIE est la même — tout le programme, initialisée à zéro — mais leur PORTÉE diffère : le nom d'une locale static reste confiné à son bloc, alors qu'une globale est visible partoutmême durée, portée différente
  • Une locale static est allouée sur la pile, une globale sur le taspile contre tas

Réponse : Portée et durée de vie sont deux notions indépendantes, et static sur une locale les dissocie. La DURÉE DE VIE devient celle du programme : la variable est allouée une fois, dans la zone des données du chapitre 3 du cours de systèmes — pas sur la pile —, initialisée à zéro, et conserve sa valeur d'un appel à l'autre. La PORTÉE, elle, reste le bloc : aucune autre fonction ne peut la nommer. C'est le moyen propre de donner une mémoire à une fonction — compteur d'appels, cache — sans polluer l'espace de noms ni exposer la variable à des modifications extérieures. Attention au second sens du mot-clé, qui n'a aucun rapport : static devant une fonction ou une variable GLOBALE en restreint la visibilité au fichier, ce qui est l'équivalent C du « privé ».

À vous

L'exercice simule le passage par valeur et la portée, en modélisant explicitement les cadres d'appel — le seul moyen de rendre visible ce que le C fait silencieusement.

Trois temps. L'échange qui ne fonctionne pas, avec l'affichage des deux cadres avant et après : on voit les copies changer et les originaux rester. Puis le compteur d'appels, écrit avec une variable locale ordinaire — qui repart de zéro — puis avec une locale static — qui se souvient. Enfin un mini-Makefile : vous décrivez les dépendances et le programme dit ce qu'il faut recompiler après modification d'un fichier.

Exercice de code

Rendez visibles les cadres d'appel, opposez locale et static, puis écrivez un mini-make.

Point de départ

// ── 1. Les cadres d'appel, rendus visibles ────────────────────────────────
function cadre(nom, variables) {
  return { nom, variables };
}
function afficher(...cadres) {
  for (const c of cadres) {
    console.log("   " + c.nom.padEnd(10) +
      Object.entries(c.variables).map(([k, v]) => k + " = " + v).join("  |  "));
  }
}

// En C, appeler echanger(x, y) COPIE les valeurs dans de nouvelles cases.
function echangerParValeur(appelant) {
  const local = cadre("echanger", { a: appelant.variables.x, b: appelant.variables.y });
  console.log("   à l'entrée :");
  afficher(appelant, local);

  const t = local.variables.a;
  local.variables.a = local.variables.b;
  local.variables.b = t;

  console.log("   après l'échange des COPIES :");
  afficher(appelant, local);
  // le cadre local disparaît ici : rien n'est rendu à l'appelant
}

// ── 2. Portée et durée de vie ─────────────────────────────────────────────
// Une locale ordinaire est recréée à chaque appel.
function compteurLocal() {
  let n = 0;      // recréée, réinitialisée
  n++;
  return n;
}

// Une locale « static » survit d'un appel à l'autre. En JS on l'imite par
// une variable de portée englobante — ce qui montre bien la dissociation
// entre PORTÉE (le bloc) et DURÉE DE VIE (le programme).
const compteurStatic = (function () {
  let n = 0;      // ← allouée UNE fois, invisible de l'extérieur
  return function () { return 0; };   // ← à écrire
})();

// ── 3. Un mini-Makefile ───────────────────────────────────────────────────
const REGLES = {
  "programme": ["main.o", "pile.o"],
  "main.o": ["main.c", "pile.h"],
  "pile.o": ["pile.c", "pile.h"],
};

// dates[fichier] = horodatage de dernière modification
function aReconstruire(cible, dates, sortie = []) {
  const deps = REGLES[cible];
  if (!deps) return sortie;                    // un fichier source : rien à faire
  // ← à écrire : reconstruire d'abord les dépendances, puis la cible si
  //   l'une d'elles est plus récente qu'elle.
  return sortie;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Lancez l'échange et constatez que x et y n'ont pas bougé.
// 2. Écrivez compteurStatic, et comparez les deux compteurs sur trois appels.
// 3. Écrivez aReconstruire : après modification de pile.h, que faut-il
//    recompiler ? Et après modification de main.c seul ?

const principal = cadre("main", { x: 3, y: 7 });
echangerParValeur(principal);
console.log("   de retour dans main :");
afficher(principal);

Solution

function cadre(nom, variables) { return { nom, variables }; }
function afficher(...cadres) {
  for (const c of cadres) {
    console.log("   " + c.nom.padEnd(10) +
      Object.entries(c.variables).map(([k, v]) => k + " = " + v).join("  |  "));
  }
}

function echangerParValeur(appelant) {
  const local = cadre("echanger", { a: appelant.variables.x, b: appelant.variables.y });
  console.log("   à l'entrée :");
  afficher(appelant, local);
  const t = local.variables.a;
  local.variables.a = local.variables.b;
  local.variables.b = t;
  console.log("   après l'échange des COPIES :");
  afficher(appelant, local);
}

// La version qui marchera au chapitre 7 : on ne passe plus les valeurs mais
// de quoi ATTEINDRE les cases — ici l'objet du cadre et les noms.
function echangerParAdresse(appelant, nomA, nomB) {
  const t = appelant.variables[nomA];
  appelant.variables[nomA] = appelant.variables[nomB];
  appelant.variables[nomB] = t;
}

function compteurLocal() { let n = 0; n++; return n; }

const compteurStatic = (function () {
  // Allouée UNE fois, au chargement, et jamais réinitialisée. Son nom n'est
  // visible que d'ici : durée de vie du programme, portée du bloc.
  let n = 0;
  return function () { n++; return n; };
})();

const REGLES = {
  "programme": ["main.o", "pile.o"],
  "main.o": ["main.c", "pile.h"],
  "pile.o": ["pile.c", "pile.h"],
};

function aReconstruire(cible, dates, sortie = []) {
  const deps = REGLES[cible];
  if (!deps) return sortie;
  // D'abord les dépendances, en profondeur : une cible ne peut être jugée
  // à jour que si tout ce dont elle dépend l'est déjà.
  for (const d of deps) aReconstruire(d, dates, sortie);
  const dateCible = dates[cible] ?? -Infinity;
  const plusRecente = Math.max(...deps.map((d) => dates[d] ?? 0));
  if (plusRecente > dateCible) {
    sortie.push(cible);
    dates[cible] = plusRecente;   // la reconstruction la met à jour
  }
  return sortie;
}

console.log("— 1. passage par valeur —");
const principal = cadre("main", { x: 3, y: 7 });
echangerParValeur(principal);
console.log("   de retour dans main :");
afficher(principal);
console.log("   x et y n'ont pas bougé : les copies ont disparu avec le cadre.");
console.log("");
echangerParAdresse(principal, "x", "y");
console.log("   avec l'adresse (chapitre 7) :");
afficher(principal);

console.log("");
console.log("— 2. portée et durée de vie —");
for (let i = 1; i <= 3; i++) {
  console.log("   appel " + i + " : locale ordinaire = " + compteurLocal() +
              " | locale static = " + compteurStatic());
}
console.log("   La locale ordinaire est recréée à chaque appel ; la static est");
console.log("   allouée une fois, initialisée à zéro, et se souvient.");

console.log("");
console.log("— 3. mini-Makefile —");
for (const [quoi, dates] of [
  ["on modifie pile.h", { "main.c": 1, "pile.c": 1, "pile.h": 9, "main.o": 5, "pile.o": 5, "programme": 6 }],
  ["on modifie main.c seul", { "main.c": 9, "pile.c": 1, "pile.h": 1, "main.o": 5, "pile.o": 5, "programme": 6 }],
  ["rien n'a changé", { "main.c": 1, "pile.c": 1, "pile.h": 1, "main.o": 5, "pile.o": 5, "programme": 6 }],
]) {
  const r = aReconstruire("programme", { ...dates });
  console.log("   " + quoi.padEnd(24) + "-> " + (r.length ? r.join(", ") : "rien à faire"));
}
// Modifier un en-tête force la recompilation de tous les .o qui l'incluent :
// c'est pourquoi un .o doit DÉPENDRE des .h, faute de quoi make ne verrait
// rien et l'on déboguerait un binaire construit sur une interface périmée.

En travaux pratiques

Travaux pratiques 4 · 3 h

Découper, et buter sur le passage par valeur

Modulariser le fil rouge, puis rencontrer la limite qui rend les pointeurs inévitables : une fonction ne peut pas modifier son argument.

Avant de commencer

  • Les TP 1 à 3
  • Le fil rouge journal, en deux fichiers

Énoncé

  1. La fonction qui ne marche pasÉcrivez une fonction echanger qui prend deux entiers et échange leurs valeurs. Appelez-la et affichez le résultat. Constatez l'échec, puis expliquez-le avec un schéma de la pile.
  2. Portée et durée de vieÉcrivez une fonction avec une variable locale, la même en static, et une globale. Appelez la fonction trois fois et comparez les trois comportements.
  3. Le pointeur qui pendÉcrivez une fonction qui renvoie l'adresse d'une variable locale. Compilez avec -Wall, lisez l'avertissement, exécutez quand même et expliquez ce qui se passe. Indice : L'espace de la variable est repris par l'appel suivant.
  4. Découper le fil rougeRéorganisez journal en trois modules : lecture, analyse, affichage. Chaque module a son en-tête. Aucune variable globale ne doit subsister.
  5. Le contrat d'une fonctionPour chaque fonction publique, écrivez au-dessus ce qu'elle attend, ce qu'elle rend, et ce qu'elle fait en cas d'erreur. Puis vérifiez que le code respecte ce que vous avez écrit.
  6. Récursivité, en avant-goûtÉcrivez la factorielle en récursif et en itératif. Faites-les tourner sur 20, puis sur 100000. Notez ce qui se passe dans le second cas.
  7. Static au niveau fichierRendez privée une fonction utilitaire de votre module d'analyse. Tentez de l'appeler depuis main et lisez l'erreur.

C'est réussi quand

  • Vous expliquez l'échec d'echanger sans employer le mot « bogue »
  • Vos trois modules compilent séparément et aucune variable globale ne subsiste
  • La factorielle récursive de 100000 échoue, et vous savez sur quelle ressource

Correction

Pourquoi echanger ne marche pas
void echanger(int a, int b) { int t = a; a = b; b = t; }

int x = 1, y = 2;
echanger(x, y);
printf("%d %d\n", x, y);   → 1 2  (inchangés)

pile :
main      : x=1     y=2
echanger  : a=1     b=2      ← des COPIES
après     : a=2     b=1      ← les copies sont échangées
retour    : les copies sont détruites, x et y n'ont jamais bougé

En C, un argument est TOUJOURS copié — il n'existe pas de passage par référence. La fonction ne reçoit pas la variable, elle reçoit sa valeur. Pour modifier une variable de l'appelant, il n'y a qu'une voie : recevoir son ADRESSE. C'est ce constat, et non une envie d'abstraction, qui rend le chapitre 7 nécessaire.

Trois durées de vie
void f(void) {
  int         a = 0;   /* pile   : recréée à chaque appel */
  static int  b = 0;   /* statique : créée UNE fois, survit */
  a++; b++; g++;       /* g globale */
  printf("%d %d %d\n", a, b, g);
}
f(); f(); f();
→ 1 1 1
1 2 2
1 3 3

static à l'intérieur d'une fonction change la DURÉE DE VIE sans changer la portée : la variable persiste mais reste invisible ailleurs. C'est utile pour un compteur d'appels ou un cache — et c'est un piège en présence de fils d'exécution, parce que cet état est partagé. Une fonction avec un static n'est pas réentrante.

Le pointeur pendant
int *mauvais(void) {
  int local = 42;
  return &local;        /* warning: address of local variable returned */
}

int *p = mauvais();
autre_fonction();          /* réutilise le même espace de pile */
printf("%d\n", *p);       /* affiche n'importe quoi */

L'espace n'est pas effacé, il est RÉUTILISÉ : c'est pourquoi le programme affiche parfois 42 et donne l'illusion de fonctionner. Un bogue qui marche par hasard est plus dangereux qu'un bogue qui plante. Un objet doit survivre à la fonction qui le crée : d'où l'allocation dynamique du TP 8, ou le fait de recevoir de l'appelant l'espace où écrire.

Le fil rouge modularisé
lecture.h    int   lire_lignes(const char *chemin, char ***lignes);
analyse.h    Stats analyser(char **lignes, int n);
affichage.h  void  afficher(const Stats *s);

main.c :
  char **lignes; 
  int n = lire_lignes(argv[1], &lignes);
  if (n < 0) return 1;
  Stats s = analyser(lignes, n);
  afficher(&s);

Chaque module a une responsabilité et une seule, et main ne fait plus qu'orchestrer. Notez le triple pointeur de lire_lignes : la fonction doit modifier un tableau appartenant à l'appelant, donc en recevoir l'adresse — application directe de l'étape 1. Le retour négatif signale l'erreur, convention qu'il faut écrire et tenir partout.

Le contrat, et pourquoi l'écrire
/* Lit toutes les lignes de « chemin ».
* Attend  : chemin non NULL, lignes non NULL.
* Rend    : le nombre de lignes, ou -1 en cas d'erreur.
* Alloue  : *lignes, à libérer par l'appelant avec liberer_lignes().
* En cas d'échec : *lignes n'est pas modifié. */
int lire_lignes(const char *chemin, char ***lignes);

La ligne qui compte le plus est celle de l'allocation : en C, il faut dire QUI libère, sinon personne ne le fait ou deux le font. Ce commentaire n'est pas de la documentation d'agrément, c'est la seule spécification que le compilateur ne peut pas vérifier à votre place — const en dit une partie, jamais toute.

Le débordement de pile
factorielle(20)     → 2432902008176640000, correct
factorielle(100000) → Segmentation fault

100 000 appels imbriqués × ~48 octets de trame ≈ 4,8 Mo
pile par défaut : 8 Mo, mais chaque appel coûte aussi
l'adresse de retour et les registres sauvegardés

ulimit -s   → 8192 (Ko)

Ce n'est pas la mémoire qui manque, c'est la PILE, dont la taille est fixée. La version itérative n'utilise qu'une trame, quelle que soit l'entrée. C'est exactement le mécanisme du TP 6 d'Architecture, vu depuis le C, et c'est le premier chapitre d'Algorithmique 2 qui en fera la théorie.

Ce que la suite en fait

Le bloc III applique tout cela aux tableaux, et y ajoute une exception majeure : un tableau passé en paramètre n'est pas copié. C'est la première fissure dans la règle du passage par valeur, et l'explication — un tableau se convertit en pointeur sur son premier élément — demandera le chapitre 7.

Le bloc IV résoudra enfin l'échange, en trois lignes. Vous les lirez alors comme la conclusion de ce chapitre, et non comme une syntaxe nouvelle.

À retenir

Flashcards · 5 cartes

Pourquoi le C exige-t-il un prototype, et que signifie int f(void) ?
Parce que le compilateur travaille DE HAUT EN BAS et un fichier à la fois : pour vérifier un appel, il doit connaître la signature avant de le rencontrer. Le prototype permet de ranger les fonctions dans un ordre logique et devient indispensable dès qu'il y a plusieurs fichiers. int f(void) signifie « aucun paramètre » ; int f() signifie historiquement « nombre non spécifié », ce qui désactive toute vérification — écrire (void) n'est donc pas une coquetterie.
Énoncez exactement la règle du passage d'arguments en C.
Le C ne connaît QUE le passage par valeur : tout argument est COPIÉ dans un nouvel emplacement, et un paramètre est une variable locale initialisée avec cette valeur. Une fonction ne peut donc pas modifier les variables de son appelant — c'est ce qui fait échouer echanger(x, y). Mérite de cette règle : une fonction ne modifie que ce qu'on lui a explicitement donné le droit de modifier. Pour permettre la modification, on passe l'ADRESSE — le & de scanf — ce qui reste un passage par valeur, d'une adresse.
Distinguez portée et durée de vie, et les deux sens de static.
La PORTÉE dit OÙ un nom est visible ; la DURÉE DE VIE, COMBIEN DE TEMPS la donnée existe. Une locale static a la durée de vie d'une globale — allouée une fois, initialisée à zéro, conserve sa valeur entre les appels — mais la portée d'une locale : le moyen propre de donner une mémoire à une fonction. Second sens, sans rapport : static devant une fonction ou une globale en restreint la visibilité AU FICHIER — l'équivalent C du privé.
Que faut-il savoir de la récursivité, propre au C ?
La PILE est finie et petite — 8 Mio par défaut sous Linux, quelques centaines de milliers de cadres — et son dépassement ne lève pas d'exception : le processus est tué avec une ERREUR DE SEGMENTATION, message trompeur qui suggère un problème de pointeur. Et l'optimisation d'appel terminal n'est PAS garantie par la norme : gcc -O2 l'applique souvent, -O0 presque jamais. Une récursion profonde s'écrit itérativement.
Quel est le rôle d'une garde d'inclusion, et les deux règles d'un Makefile ?
GARDE D'INCLUSION (#ifndef / #define / #endif) : comme #include est un copier-coller, un en-tête atteint par deux chemins serait recopié deux fois et provoquerait des redéfinitions ; la garde rend la seconde copie vide. MAKEFILE : les lignes de commande commencent par une TABULATION, pas des espaces — l'erreur classique — et un .o doit dépendre des .h qu'il inclut, sans quoi une modification d'interface ne déclenche aucune recompilation.