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 retourLa 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 où un nom est visible. La durée de vie dit combien de temps la donnée existe.
| Déclaration | Portée | Durée de vie | Initialisation par défaut |
|---|---|---|---|
| Variable locale | le bloc | l'exécution du bloc | aucune (contenu indéfini) |
| Paramètre | la fonction | l'appel | la valeur de l'argument |
| Variable globale | tout le fichier, voire le programme | tout le programme | zéro |
Locale static | le bloc | tout le programme | zé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); #endifLa 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 programmeDeux 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 retour — copie des arguments
- Parce qu'il manque le mot-clé static devant les paramètres — static 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 globale — identiques
- 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 partout — même durée, portée différente
- Une locale static est allouée sur la pile, une globale sur le tas — pile 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é
- 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.
- 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.
- 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.
- Découper le fil rouge — Réorganisez journal en trois modules : lecture, analyse, affichage. Chaque module a son en-tête. Aucune variable globale ne doit subsister.
- Le contrat d'une fonction — Pour 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.
- 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.
- Static au niveau fichier — Rendez 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
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.
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 3static à 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.
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.
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.
/* 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.
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.