Fonctions et structure des programmes en C

Apprends à organiser les programmes C comme dans de vrais projets. Écris des fonctions avec des paramètres, des valeurs de retour et des prototypes, découvre pourquoi C passe les arguments par valeur et suis les appels et la récursivité. Maîtrise la visibilité et la durée de vie des variables grâce aux portées et aux classes de stockage, notamment static et extern. Utilise le préprocesseur pour les fichiers d’en-tête, les macros et la compilation conditionnelle, répartis un programme entre fichiers source et fichiers d’en-tête, puis compile-le avec make pour ne recompiler que les fichiers modifiés.

DébutantUnité 2 sur 8 de la formation Formation : Langage C : pointeurs, mémoire et structures

Ce que tu vas apprendre

  1. Décompose un programme en fonctions qui reçoivent des paramètres et renvoient des résultats, déclare-les à l’aide de prototypes, comprends que C passe les arguments par valeur et suis l’exécution et le retour des appels, y compris récursifs.
    • Définir des fonctions qui reçoivent des données et renvoient un résultat
      Écris des définitions de fonctions avec un type de retour, des paramètres et un corps, utilise correctement return et void, et appelle des fonctions dans des expressions.
    • Déclarer des fonctions à l’aide de prototypes
      Déclare une fonction avant son premier appel à l’aide d’un prototype, sache ce que le compilateur vérifie et convertit, et évite les parenthèses vides et les déclarations implicites.
    • Passer les arguments par valeur et renvoyer le résultat
      Explique que chaque argument C est copié dans son paramètre, prévois ce qu’une fonction peut ou ne peut pas modifier et utilise les valeurs de retour pour renvoyer des résultats.
    • Suivre les appels de fonctions et la récursivité
      Suis chaque appel, qui possède ses propres paramètres et variables locales et revient à la fonction appelante, puis écris et suis des fonctions récursives comportant un cas de base.
  2. Distingue la portée d’un nom de la durée de vie d’une variable, utilise des variables locales statiques pour conserver un état entre les appels et évalue les coûts des variables globales.
    • Déterminer où un nom peut être utilisé (portée)
      Applique les portées de bloc, de fichier et de prototype, prévois quelle variable un nom désigne lorsqu’il y a masquage et évite les erreurs de portée.
    • Choisir la durée de vie d’une variable à l’aide des classes de stockage
      Distingue les durées de stockage automatique et statique, utilise des variables locales statiques pour mémoriser des valeurs entre les appels et connais le sens de auto, register, static et extern.
    • Partager des données avec précaution à l’aide de variables globales
      Définis et utilise des variables de portée de fichier (globales), explique leur visibilité et leur initialisation, et privilégie les paramètres et les valeurs de retour lorsqu’une variable globale introduirait un couplage caché.
  3. Utilise #include, #define et les macros en toute sécurité, et sélectionne le code à la compilation à l’aide de directives conditionnelles, de macros prédéfinies et d’options du compilateur.
    • Inclure des fichiers et définir des macros
      Explique ce que fait le préprocesseur avant la compilation, inclus des fichiers d’en-tête, définis des macros constantes et des macros fonctions, et évite les pièges classiques des macros.
    • Compiler du code sous condition
      Utilise #if, #ifdef, #ifndef, #elif, #else et #endif avec les options -D et les macros prédéfinies pour inclure ou exclure du code à la compilation, notamment à l’aide de gardes d’inclusion.
  4. Compile séparément plusieurs fichiers source et effectue leur édition de liens, écris des fichiers d’en-tête qui partagent des déclarations et contrôle les noms visibles par les autres fichiers avec static et extern.
    • Compiler plusieurs fichiers source et effectuer l’édition de liens
      Décris les étapes qui mènent du fichier source à l’exécutable, compile séparément les fichiers en fichiers objets, effectue leur édition de liens et distingue les erreurs de compilation des erreurs d’édition de liens.
    • Écrire des fichiers d’en-tête
      Place les déclarations partagées dans un fichier d’en-tête muni d’une garde, inclus-le là où il est nécessaire, notamment dans son propre fichier source, et n’y place pas de définitions.
    • Contrôler la liaison avec static et extern
      Utilise extern pour partager une variable entre plusieurs fichiers et static pour garder des fonctions et des variables privées à leur fichier, puis corrige les erreurs d’édition de liens dues à un mauvais usage.
  5. Écris des makefiles dont les règles et les dépendances ne recompilent que ce qui a changé, et garde-les concis grâce aux variables, aux variables automatiques, aux règles de motif et aux cibles factices.
    • Écrire les règles et les dépendances d’un fichier makefile
      Écris des règles avec des cibles, des dépendances et des recettes indentées par une tabulation, prévois les commandes que make exécute après une modification et indique correctement les dépendances aux fichiers d’en-tête.
    • Simplifier les fichiers makefile avec des variables et des règles de motif
      Utilise des variables comme CC et CFLAGS, les variables automatiques $@, $< et $^, les règles de motif, les règles prédéfinies et les cibles factices pour garder un makefile court et correct.

Fonctions et structure des programmes en C : cours

5 chapitres

1

Écrire, déclarer et appeler des fonctions

Une fonction donne à une tâche un nom, un ensemble de données d’entrée et une façon claire de produire un résultat. Tu vas construire des définitions et des prototypes, puis suivre les valeurs dans les appels et lors de leur retour aux fonctions appelantes. De courts suivis rendent les règles visibles : un paramètre reçoit une copie, une valeur de retour doit être utilisée quelque part et un appel récursif doit finir par s’arrêter. Les exemples utilisent C17 ; des listes de paramètres explicites rendent aussi le code clair lors d’une compilation en C23.

1.1 Définir des fonctions qui reçoivent des données et renvoient un résultat

Une fonction regroupe une tâche pour qu’une autre partie du programme puisse l’appeler. Donne-lui un nom qui décrit cette tâche et choisis ses données d’entrée et son résultat avant d’écrire son corps. Dans un programme C ordinaire exécuté dans un environnement hôte, l’exécution commence dans main.

Construire une définition
int square(int n)
{
    return n * n;
}

La définition comporte un type de retour (int), un nom (square), une liste de paramètres (int n) et un corps entre accolades. Le paramètre est la variable locale qui reçoit une donnée d’entrée. Un argument est la valeur fournie par un appel : dans square(6), cette valeur est 6. L’appel produit 36. Définis chaque fonction au niveau du fichier, en dehors des autres fonctions ; le C standard n’autorise pas les définitions de fonctions imbriquées.

Chaque paramètre doit avoir son propre type. Écris int max(int a, int b), avec une virgule entre les déclarations. Dans un appel, fournis les valeurs sans leurs types : max(8, 3).

Renvoyer une valeur à la fonction appelante
int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

Un return termine immédiatement l’appel en cours. Pour max(8, 3), le premier retour est exécuté, donc le second n’est jamais atteint. Une expression renvoyée est convertie dans le type de retour de la fonction. Choisis ce type de façon à préserver le type de résultat dont tu as besoin.

Pour un résultat fractionnaire, utilise des calculs à virgule flottante du début à la fin :

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

L’appel c_to_f(37.0) renvoie 98.6. Une version entière utilisant c * 9 / 5 + 32 renvoie 98, car la division supprime la partie fractionnaire. Réorganiser le calcul en 9 / 5 * c + 32 fait d’abord évaluer 9 / 5 à 1, ce qui donne 69 pour 37. Un type de retour à virgule flottante ne peut pas, à lui seul, annuler un calcul entier déjà effectué dans le corps.

Utiliser le résultat

Un appel qui renvoie une valeur est une expression. Avec la définition ci-dessus, square(3) + square(4) donne 25. Dans square(square(2)), l’appel intérieur renvoie 4, qui devient l’argument de l’appel extérieur, ce qui donne 16. Une instruction telle que square(5); est autorisée, mais ignore la valeur renvoyée.

Afficher et renvoyer sont deux opérations différentes. printf affiche du texte ; return fournit un résultat que la fonction appelante peut stocker ou utiliser dans une autre expression. Si une fonction ordinaire dont le type de retour n’est pas void atteint son accolade fermante sans rien renvoyer et que la fonction appelante utilise son résultat, le comportement est indéfini. Compile avec -Wall -Wextra pour faciliter la détection des retours manquants.

Fonctions qui ne produisent pas de valeur

Une fonction void accomplit une tâche sans renvoyer de valeur. Elle peut quitter avant la fin avec return;. Par exemple, void print_line(int width) peut afficher une ligne de tirets, mais int x = print_line(5); est invalide car l’appel ne fournit aucune valeur. Pour une fonction sans donnée d’entrée, écris void dans la liste des paramètres, comme dans int read_sensor(void).

int main(void) renvoie un code de retour à l’environnement hôte. Utilise return 0; pour indiquer la réussite ; <stdlib.h> fournit aussi EXIT_SUCCESS et EXIT_FAILURE. Atteindre la fin de main renvoie automatiquement zéro. Les autres fonctions qui renvoient une valeur ne bénéficient pas de cette exception.

Points clés

  • Une définition indique un type de retour, un nom, des paramètres typés et un corps.
  • Un paramètre reçoit un argument fourni par un appel.
  • return termine l’appel ; l’affichage ne fournit pas son résultat.
  • Utilise void pour l’absence de valeur de retour et (void) pour l’absence de paramètres.
  • Choisis des types pour les calculs et le retour qui préservent le résultat nécessaire.

1.2 Déclarer des fonctions à l’aide de prototypes

Un appel de fonction nécessite une déclaration que le compilateur a déjà rencontrée. Si la définition vient plus loin, place un prototype avant le premier appel. Le prototype indique au compilateur comment vérifier l’appel sans fournir le corps de la fonction.

Séparer la promesse du corps
#include <stdio.h>

double c_to_f(double c);

int main(void)
{
    printf("%.1f\n", c_to_f(25.0));
    return 0;
}

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

Le prototype se termine par un point-virgule. La définition possède un corps et aucun point-virgule entre sa liste de paramètres et son accolade ouvrante. Une organisation pratique est la suivante : fichiers d’en-tête, prototypes, main, puis les autres définitions. Le résultat affiché ici est 77.0.

Les noms des paramètres sont facultatifs dans les prototypes : double c_to_f(double); déclare la même fonction. Des noms comme c peuvent documenter une donnée d’entrée, mais ils ne doivent pas nécessairement correspondre aux noms de la définition. Toutes les déclarations doivent concorder sur le type de retour et les types des paramètres. Une fonction utilisée doit avoir une définition ; répéter des prototypes compatibles ne crée pas de fonctions supplémentaires.

Ce que vérifie le compilateur

Avec un prototype, le compilateur vérifie le nombre d’arguments et convertit chaque valeur fournie dans le type déclaré du paramètre. Avec double c_to_f(double c);, c_to_f() et c_to_f(20.0, 5.0) ont tous deux un nombre incorrect d’arguments et nécessitent un diagnostic.

Une différence de type numérique peut en revanche donner lieu à une conversion autorisée :

int half(int n)
{
    return n / 2;
}

Pour half(7.9), la conversion donne d’abord au paramètre la valeur 7. La division entière produit ensuite 3. Pour half(-7.9), la conversion donne -7, et la division produit -3. Dans les deux cas, les parties fractionnaires sont supprimées par troncature vers zéro. L’appel ne conserve pas la valeur double d’origine simplement parce que l’argument a été écrit avec un point décimal. Ajoute -Wconversion si tu souhaites recevoir des avertissements sur ces conversions.

Indiquer explicitement l’absence de paramètres
DéclarationSens en C17Sens en C23
int read_sensor(void);Aucun paramètreAucun paramètre
int read_sensor();Informations sur les paramètres non spécifiéesAucun paramètre

En C17, une liste de paramètres vide dans une déclaration empêche la vérification complète des arguments ; elle n’autorise pas pour autant des arguments arbitraires pour la fonction finalement définie. Utilise systématiquement (void). GCC 15 a changé son dialecte C par défaut pour gnu23 ; -std=c17 rend donc explicite la norme visée lorsque tu suis ces exemples.

Corriger un problème de déclaration

Appeler une fonction non déclarée enfreint les règles du C moderne. Inclus le fichier d’en-tête standard qui la déclare, comme <stdio.h> pour printf, ou ajoute ton propre prototype correspondant. Les versions actuelles de GCC et de Clang rejettent normalement les déclarations implicites.

Un prototype tel que double area(double r); ne peut pas correspondre à une définition qui renvoie un float et reçoit un float. Inclus le fichier d’en-tête d’un module dans le fichier qui définit ses fonctions pour que le compilateur puisse les comparer. N’essaie pas de corriger une incompatibilité de types en ajoutant une conversion explicite à l’appel : corrige la déclaration et la définition pour qu’elles décrivent la même fonction.

Points clés

  • Place une déclaration avant le premier appel.
  • Un prototype se termine par un point-virgule ; une définition fournit le corps.
  • Les prototypes permettent de vérifier le nombre d’arguments et précisent leurs conversions.
  • Utilise (void) pour indiquer l’absence de paramètres en C17 comme en C23.
  • Inclus le fichier d’en-tête qui fournit la déclaration et veille à ce que toutes les déclarations soient compatibles.
  • Une conversion numérique autorisée peut tout de même faire perdre une partie fractionnaire.

1.3 Passer les arguments par valeur et renvoyer le résultat

C passe chaque argument par valeur : chaque paramètre reçoit initialement une copie de la valeur de son argument. Affecter une valeur à un paramètre entier ordinaire modifie cette copie, même s’il porte le même nom qu’une variable de la fonction appelante.

Suivre la copie

Après avoir inclus <stdio.h> et placé cette définition avant son appel :

void double_it(int n)
{
    n *= 2;
    printf("inside: %d\n", n);
}

La fonction appelante exécute :

int score = 21;
double_it(score);
printf("after: %d\n", score);

Le résultat affiché est inside: 42, puis after: 21. Le paramètre passe de 21 à 42 ; aucune instruction n’affecte de valeur au score de la fonction appelante.

Conserver un résultat renvoyé
int doubled(int n)
{
    return n * 2;
}
Instruction de la fonction appelanteEffet sur un score initialement égal à 21
doubled(score);Ignore 42 ; score reste à 21
score = doubled(score);Stocke 42 dans score

La fonction calcule une valeur ; l’affectation dans la fonction appelante met à jour sa variable. Renvoyer la valeur d’une variable locale est sûr : int total = a + b; return total; copie la valeur avant que la variable locale automatique cesse d’exister. Les calculs qui utilisent uniquement des paramètres et des résultats renvoyés peuvent être testés sans préparer d’état partagé caché.

Comprendre pourquoi un échange de valeurs échoue
void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
}

Après int p = 3, q = 7; swap(p, q);, la fonction appelante a toujours p == 3 et q == 7. Le corps a échangé ses copies. Modifier les objets de la fonction appelante par des paramètres nécessite leurs adresses, transmises sous forme de valeurs de pointeurs.

Séquencer les arguments lorsque l’ordre compte

Les arguments peuvent être des expressions comme a + 1 ou square(b). Leur évaluation s’achève avant le début du corps de la fonction appelée, mais leur ordre d’évaluation n’est pas spécifié. Deux appels à next_ticket(), qui modifie un état, dans un même affichage peuvent fournir les numéros dans un ordre ou dans l’autre. Utilise des instructions distinctes :

int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);

Évite f(i, i++) : la lecture et la modification non séquencées de i produisent un comportement indéfini.

Points clés

  • Les paramètres reçoivent des copies des valeurs des arguments.
  • Stocke un résultat renvoyé par une affectation lorsque la fonction appelante doit mettre à jour une valeur.
  • Un échange par valeur modifie ses paramètres, pas les variables de la fonction appelante.
  • Utilise des instructions distinctes lorsque l’ordre d’évaluation des arguments compte.

1.4 Suivre les appels de fonctions et la récursivité

Un appel suspend la fonction appelante jusqu’à ce qu’un résultat soit prêt. Suivre les appels actifs explique comment un programme revient au bon endroit et pourquoi la récursivité nécessite une règle d’arrêt.

Conserver un enregistrement par appel actif

La plupart des implémentations utilisent une pile d’appels. Chaque appel actif possède un cadre qui enregistre les valeurs de ses paramètres, ses variables locales automatiques et l’endroit où reprendre dans la fonction appelante. La norme spécifie le comportement sans imposer cette organisation particulière de la mémoire.

Si main appelle print_table et que print_table appelle c_to_f, la fonction de conversion revient en premier. L’exécution reprend dans print_table, qui revient ensuite à main. Les variables locales automatiques ordinaires appartiennent à un seul appel. Une variable locale statique est différente : tous les appels partagent son unique valeur stockée.

Suivre la descente d’un calcul récursif

Une fonction récursive s’appelle elle-même sur un problème plus petit. Un cas de base renvoie directement un résultat ; un cas récursif progresse vers ce cas de base.

int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

Pour factorial(4), le premier appel ne peut pas terminer sa multiplication avant le retour de factorial(3). Cet appel attend factorial(2), qui attend factorial(1).

Appel actifCalcul en attente d’un résultat
factorial(4)4 * factorial(3)
factorial(3)3 * factorial(2)
factorial(2)2 * factorial(1)
factorial(1)Cas de base : renvoyer 1

Il y a quatre appels actifs de la fonction factorielle au point le plus profond. Chacun possède son propre n ; atteindre n == 1 ne modifie pas les valeurs des appels précédents.

Suivre la remontée des résultats

L’appel du cas de base renvoie 1. L’appel avec n == 2 calcule alors 2 * 1 et renvoie 2. Le suivant renvoie 3 * 2, soit 6 ; le premier renvoie 4 * 6, soit 24. Inscris les valeurs de retour dans le suivi à la remontée, une fois le résultat de la fonction appelée connu.

La condition d’arrêt doit être atteignable pour les valeurs d’entrée prises en charge. Si le test était n == 1, commencer à zéro continuerait à lancer des appels avec -1, -2 et des valeurs inférieures au lieu d’atteindre un. La poursuite de la récursivité épuise les ressources et peut provoquer un plantage. Le test plus large n <= 1 arrête ces appels, même si la factorielle mathématique est utilisée pour les entiers positifs ou nuls.

Vérifier la profondeur et le type du résultat

S’arrêter correctement ne garantit pas que le résultat est représentable. Sur une plateforme où int est sur 32 bits, la plus grande valeur est 2147483647. factorial(12) donne 479001600, qui est représentable ; factorial(13) nécessiterait 6227020800, qui ne l’est pas. Un débordement d’entier signé constitue un comportement indéfini.

Une boucle peut calculer la factorielle en conservant un produit courant dans un seul appel de fonction. La récursivité est utile lorsque le problème suivant a la même structure mais une taille plus petite. Pour les entiers positifs, la somme des chiffres est un autre exemple : sum_digits(472) nécessite 2 + sum_digits(47), puis 2 + 7 + sum_digits(4), ce qui donne 13. Un suivi doit montrer à la fois la réduction de la valeur d’entrée et la combinaison ultérieure des valeurs renvoyées.

Points clés

  • Les appels se terminent dans l’ordre inverse de leur imbrication.
  • Chaque appel possède ses propres paramètres et variables locales automatiques.
  • La récursivité nécessite un cas de base atteignable et une progression vers celui-ci.
  • Suis les appels à la descente et les valeurs renvoyées à la remontée.
  • Vérifie que les résultats récursifs sont représentables dans leur type numérique.
2

Maîtriser la portée et la durée de vie des variables

Un nom peut être visible dans une petite partie du code source alors que sa variable existe pendant toute l’exécution du programme. Distinguer ces deux propriétés facilite grandement la compréhension des variables locales, des variables locales statiques et des variables globales. Tu vas suivre les noms dans des blocs imbriqués, comparer les nouvelles valeurs aux valeurs mémorisées et choisir comment une fonction reçoit les données dont elle a besoin. Ce choix influe sur la facilité avec laquelle tu peux retrouver l’origine d’un résultat incorrect et tester une fonction seule.

2.1 Déterminer où un nom peut être utilisé (portée)

La portée est la partie du code source où un nom peut être utilisé. Lis les accolades et les déclarations pour déterminer quelle variable une expression désigne ; l’ordre des appels ne modifie pas ces règles.

Trouver la limite de la portée

Une variable déclarée dans un bloc a une portée de bloc, qui commence juste après son déclarateur et se termine à l’accolade fermante de ce bloc. Les paramètres sont visibles dans le corps de leur fonction. Une déclaration dans l’en-tête d’un for, telle que for (int i = 0; i < 10; i++), rend i disponible dans cette boucle, mais pas après celle-ci.

Un nom déclaré en dehors de toutes les fonctions a une portée de fichier, de sa déclaration jusqu’à la fin de l’unité de traduction. Les fonctions situées au-dessus de cette déclaration ont besoin d’une déclaration antérieure pour l’utiliser. Un nom de paramètre dans un prototype a une portée de prototype, qui se termine avec le prototype. Ainsi, double c_to_f(double c); peut être suivi d’une définition qui nomme le paramètre degrees.

Suivre le masquage de l’intérieur vers l’extérieur
#include <stdio.h>

int x = 10;

void show(void)
{
    printf("%d\n", x);
}

int main(void)
{
    int x = 5;
    {
        int x = 1;
        printf("%d\n", x);
    }
    printf("%d\n", x);
    show();
    return 0;
}

Le premier affichage, dans le bloc intérieur, utilise 1, le suivant utilise 5 et show affiche 10. La déclaration visible de x la plus proche l’emporte. Quitter le bloc intérieur rend à nouveau visible la variable locale extérieure. Appeler show ne rend pas la variable locale de main visible dans cette fonction : show a été définie là où le x de portée de fichier est visible.

Différentes fonctions peuvent chacune déclarer un i, un count ou un result local. Ce sont des variables distinctes car leurs déclarations appartiennent à des blocs différents.

Déclarer une valeur là où ses utilisations ultérieures peuvent y accéder

Ce code ne compile pas :

if (temp > 30.0) {
    int warning = 1;
}
printf("%d", warning);

L’affichage se trouve en dehors de la portée de warning. Si la valeur doit être utilisée ensuite, déclare-la et initialise-la avant la condition :

int warning = 0;
if (temp > 30.0) {
    warning = 1;
}
printf("%d", warning);

L’initialisation fournit aussi la valeur pour le chemin où la condition est fausse.

Éviter de créer accidentellement une variable

Déclarer int total = 0; dans une boucle crée une nouvelle variable locale à chaque entrée dans son bloc. Si tu voulais accumuler des valeurs dans un total extérieur, utilise une affectation comme total += value;. Active -Wshadow pour faciliter la détection des noms masqués.

La portée commence avant l’évaluation de l’initialiseur. Dans un bloc, int x = x + 1; lit le nouveau x, non initialisé, même s’il existe un x extérieur. Utilise un autre nom si l’initialiseur a besoin de la valeur extérieure.

La portée décrit où un nom est visible dans le code source. La durée de stockage décrit combien de temps son objet existe pendant l’exécution du programme. Une variable locale statique a une portée de bloc, tandis que sa valeur stockée existe pendant toute l’exécution.

Points clés

  • La portée de bloc se termine à l’accolade fermante correspondante.
  • Les noms de portée de fichier sont visibles sous leur déclaration.
  • Une déclaration intérieure masque une déclaration extérieure visible.
  • Une fonction appelée ne peut pas voir les variables locales automatiques de la fonction appelante.
  • La portée commence avant l’évaluation d’un initialiseur.
  • La portée et la durée de stockage répondent à des questions différentes.

2.2 Choisir la durée de vie d’une variable à l’aide des classes de stockage

La durée de stockage décrit combien de temps l’espace de stockage d’une variable existe. Utilise des variables automatiques pour le travail propre à un appel et le stockage statique lorsqu’une valeur doit subsister entre les appels.

Comparer une nouvelle valeur à une valeur mémorisée

Les paramètres et les variables ordinaires de portée de bloc ont une durée de stockage automatique. Chaque appel de fonction reçoit ses propres instances. Une déclaration sans initialiseur, telle que int sum;, ne fournit aucune valeur initiale utilisable. Initialise un accumulateur avant de le lire : int sum = 0;.

Les variables de portée de fichier et les variables de portée de bloc déclarées static ont une durée de stockage statique. Elles existent pendant toute l’exécution du programme. L’initialisation a lieu une fois avant le démarrage du programme ; sans initialiseur explicite, les objets numériques valent initialement zéro.

int next_ticket(void)
{
    static int n = 0;
    n++;
    return n;
}

Trois appels dans des instructions distinctes renvoient 1, 2 et 3. L’initialiseur ne s’exécute pas à nouveau à l’entrée dans la fonction. Supprimer static crée un nouveau n = 0 automatique à chaque appel : les trois appels renvoient donc tous 1.

Le nom n conserve une portée de bloc. Les autres fonctions ne peuvent pas le nommer directement. Les appels récursifs partagent eux aussi la même variable locale statique ; ils ne reçoivent pas de copies statiques distinctes comme c’est le cas pour les variables locales automatiques.

Initialiser une fois, affecter une valeur si nécessaire

En C, un initialiseur numérique statique doit être une expression constante. static int limit = 12; est valide ; static int limit = read_sensor(); est invalide. Effectue une affectation à l’exécution dans le code exécutable lorsqu’une valeur doit provenir d’un appel. Une instruction telle que n = 0; s’exécute chaque fois que l’exécution l’atteint : la placer au début de next_ticket remettrait donc le compteur à zéro à chaque appel.

Lire les spécificateurs de classe de stockage
SpécificateurSens dans ce code C17
autoStockage automatique explicite pour une variable de bloc ; généralement omis
registerIndication pour un accès rapide ; il est interdit de prendre son adresse
static dans une fonctionStockage conservé avec un nom local
static à la portée de fichierLiaison interne, qui garde le nom dans son unité de traduction
externDéclaration faisant référence à un objet ou une fonction existante

Les compilateurs modernes choisissent eux-mêmes l’attribution des registres : écrire register améliore donc rarement le code. En C17, auto int x; signifie la même chose qu’une variable locale ordinaire int x;. C23 autorise aussi l’inférence de type avec auto x = 3.5;, qui déclare un double.

C17 possède aussi _Thread_local, écrit thread_local en C23, pour le stockage propre à un thread. La grammaire regroupe typedef avec les spécificateurs de classe de stockage, bien qu’il nomme un type au lieu de choisir un stockage. En C17, un seul spécificateur de classe de stockage apparaît normalement dans une déclaration ; _Thread_local peut aussi accompagner static ou extern.

Un extern int count; de portée de bloc ne crée pas de copie locale automatique. Il déclare un objet doté d’une liaison et d’une durée de stockage statique. Tiens toujours compte de l’emplacement de la déclaration avant de déterminer l’effet d’un mot-clé de classe de stockage.

Points clés

  • Initialise les variables automatiques avant de les lire.
  • Le stockage statique dure toute l’exécution du programme et vaut zéro par défaut.
  • Une variable locale statique conserve sa valeur tandis que son nom reste local.
  • Un initialiseur statique s’exécute une fois ; une affectation s’exécute lorsqu’elle est atteinte.
  • L’emplacement de static détermine si son rôle principal concerne la durée de vie ou la liaison.

2.3 Partager des données avec précaution à l’aide de variables globales

Une variable globale est déclarée à la portée de fichier, en dehors de toute fonction. Elle conserve une valeur pendant toute l’exécution et peut donner à plusieurs fonctions accès à un état partagé.

Suivre l’objet partagé
#include <stdio.h>

double total_km = 0.0;

void add_trip(double km)
{
    total_km += km;
}

int main(void)
{
    add_trip(12.5);
    add_trip(7.25);
    printf("%.2f\n", total_km);
    return 0;
}

Les deux appels mettent à jour le même objet et produisent 19.75. Le paramètre km reste une copie ; la fonction effectue séparément une affectation à une variable globale qu’elle peut désigner. Le passage par valeur n’empêche pas cette affectation.

Les variables globales ont une durée de stockage statique. Les variables globales numériques valent zéro par défaut et leurs initialiseurs doivent être des expressions constantes : int error_count; vaut initialement 0, et double limit = 2.5 * 4; vaut initialement 10.0.

Vérifier la visibilité et le masquage

Un nom global n’est visible que sous sa déclaration. Une fonction située au-dessus de la définition a besoin d’une déclaration antérieure, telle que extern double total_km;.

Si add_trip contient plutôt :

double total_km = 0.0;
total_km += km;

elle crée et met à jour un objet local. La variable globale reste inchangée. Supprime la déclaration supplémentaire pour mettre à jour le total partagé ; -Wshadow aide à repérer les masquages accidentels.

Rendre les entrées explicites lorsque le partage est inutile

Un compteur d’erreurs partagé peut être utile lorsque plusieurs fonctions signalent des échecs. Mais un calcul qui lit une variable globale reçoit une donnée d’entrée absente de sa liste de paramètres. Les tests doivent préparer cet état et, pour retrouver l’origine d’une valeur incorrecte, il faut repérer les endroits susceptibles de la modifier.

Une autre solution pour le calcul des trajets est :

double with_trip(double total, double km)
{
    return total + km;
}

La fonction appelante utilise total = with_trip(total, 12.5);. Un test fournit directement les deux valeurs d’entrée, sans préparer de variable globale.

Limiter les noms partagés

Les variables ordinaires de portée de fichier ont une liaison externe. Partage une variable au moyen de déclarations et d’une seule définition ; des définitions répétées dans plusieurs fichiers rendent cette organisation invalide. À la portée de fichier, static donne au contraire une liaison interne, ce qui permet à un autre fichier de posséder son propre objet indépendant portant le même nom. Les deux objets existent toujours pendant toute l’exécution : ici, le mot-clé limite la liaison et non la durée de vie.

Points clés

  • Une variable globale conserve une même valeur entre les appels et possède une durée de stockage statique.
  • Le passage par valeur n’empêche pas une fonction de modifier une variable globale visible.
  • Un état partagé ajoute des entrées et des effets que la liste des paramètres ne montre pas.
  • Privilégie des paramètres explicites et des résultats renvoyés lorsque le partage d’état est inutile.
3

Utiliser le préprocesseur C

Avant la compilation du code C, le préprocesseur inclut les fichiers d’en-tête, développe les macros et sélectionne les branches conditionnelles. Ces opérations transforment le code reçu par le compilateur : une courte macro peut donc modifier une expression de manière surprenante. Tu vas examiner les développements, regrouper les expressions en toute sécurité et utiliser des options et des gardes d’inclusion pour contrôler le texte conservé. La distinction essentielle est celle entre choisir du code à la compilation et choisir un chemin pendant l’exécution du programme.

3.1 Inclure des fichiers et définir des macros

Le préprocesseur transforme le texte source avant que le compilateur vérifie les expressions et les types C. Ses directives commencent par #. Examine sa sortie avec gcc -E file.c lorsqu’une inclusion ou le développement d’une macro est difficile à expliquer.

Inclure des déclarations et nommer des constantes

#include <stdio.h> insère un fichier d’en-tête standard. Utilise #include "convert.h" pour un fichier d’en-tête du projet : GCC cherche d’abord dans le répertoire du fichier qui l’inclut, puis dans les chemins de recherche configurés pour les fichiers d’en-tête. L’option -Iinclude ajoute un répertoire de recherche. L’inclusion copie le texte du fichier d’en-tête ; elle n’effectue pas l’édition de liens des fonctions compilées.

Une macro objet substitue des lexèmes :

#define MAX_STUDENTS 30

Les utilisations ultérieures du lexème MAX_STUDENTS deviennent 30. Le préprocesseur ne modifie ni la chaîne "MAX_STUDENTS" ni le nom distinct MAX_STUDENTS_2. N’écris ni = ni point-virgule final. Avec #define PI 3.14159;, le développement de return PI * r * r; donne le code incorrect return 3.14159; * r * r;. Une directive se termine normalement au changement de ligne ; une barre oblique inverse finale la prolonge.

Développer avant de calculer

Une macro fonction ne comporte pas d’espace entre son nom et la parenthèse ouvrante :

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))

SQUARE_BAD(2 + 3) devient 2 + 3 * 2 + 3, ce qui donne 11. 100 / SQUARE_BAD(5) devient 100 / 5 * 5, ce qui donne 100. La macro avec parenthèses donne respectivement 25 et 4. Place des parenthèses autour de chaque paramètre et de toute l’expression de remplacement afin que les opérateurs environnants ne puissent pas modifier son regroupement.

Écrire à la place #define SQUARE (x) ((x) * (x)) crée une macro objet dont le remplacement commence par (x). L’espace change le sens de la définition.

Les parenthèses n’empêchent pas les évaluations répétées

SQUARE(i++) se développe en ((i++) * (i++)). Cette expression modifie i deux fois sans séquencement : le comportement est donc indéfini. Une véritable fonction telle que square(i++) évalue l’argument une seule fois avant d’entrer dans le corps. Privilégie les fonctions pour les calculs, en particulier lorsqu’une expression d’entrée modifie un état.

Une macro fonction n’a pas de types déclarés pour ses paramètres ou son retour. Les types du code C issu de son développement sont tout de même vérifiés, mais elle n’a pas de prototype précisant les conversions d’arguments. Les macros restent utiles pour les gardes d’inclusion, la compilation conditionnelle et les expressions constantes entières. En C17, une variable const int n’est pas une expression constante entière ; une macro ou une constante enum peut fournir une valeur pour case. C23 introduit aussi constexpr.

Transformer des arguments en lexèmes ou en texte
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b

SHOW(3 + 4); utilise # pour transformer l’argument en texte et affiche 3 + 4 = 7. CAT(temp, 1) utilise ## pour réunir les lexèmes dans le nom temp1. La conversion en chaîne de caractères utilise les lexèmes de l’argument fourni sans développer les macros dans cette occurrence ; une occurrence ordinaire de l’argument est développée.

Les macros ne suivent pas la portée de bloc. Une définition reste active jusqu’à #undef NAME ou jusqu’à la fin de l’unité de traduction, même si la directive a été écrite entre accolades. Supprime une ancienne définition avec #undef avant de lui donner un texte de remplacement différent.

Points clés

  • Le prétraitement inclut du texte et développe les lexèmes avant la compilation C.
  • Une définition de macro ne comporte ni = ni point-virgule final.
  • Place des parenthèses autour des paramètres d’une macro et de toute l’expression de remplacement.
  • Les arguments de macro répétés peuvent répéter les effets de bord ; utilise une fonction pour les calculs.
  • # convertit un argument en chaîne de caractères et ## réunit des lexèmes.
  • Une macro reste définie au-delà des limites d’un bloc jusqu’à ce que sa définition soit supprimée.

3.2 Compiler du code sous condition

La compilation conditionnelle sélectionne les lignes source qui parviennent au compilateur. Elle peut activer un affichage de débogage, choisir du code propre à une plateforme ou protéger un fichier d’en-tête contre les inclusions répétées.

Tester l’existence ou tester une valeur
#define DEBUG 0

#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif

#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif

#ifdef DEBUG vérifie si la macro est définie. #if DEBUG évalue sa valeur de remplacement comme une expression constante entière. Dans #if, un identificateur non défini compte pour zéro ; une macro mal orthographiée peut supprimer silencieusement une branche. Les expressions defined DEBUG et defined(DEBUG) testent toutes deux l’existence.

Utilise #ifndef NAME pour le test d’existence inverse. Ferme chaque condition avec #endif. #elif ajoute une autre condition et #else fournit le cas restant. C23 ajoute #elifdef et #elifndef ; C17 peut exprimer ces tests avec #elif defined(NAME) ou #elif !defined(NAME).

Choisir le réglage à la compilation
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif

gcc -std=c17 -DDEBUG main.c définit DEBUG comme 1 et conserve l’affichage. Sans cette définition, la ligne est supprimée avant la compilation. -DLEVEL=2 fournit une valeur comme si #define LEVEL 2 apparaissait avant le texte du fichier.

Cela diffère d’un if ordinaire, dont la condition est évaluée pendant l’exécution du programme. Les conditions du préprocesseur ne peuvent pas lire une variable ordinaire à l’exécution. #if 0 supprime un bloc, y compris du code contenant déjà des commentaires /* */ qui rendraient risqué son encadrement par un autre commentaire de ce type.

Protéger un fichier d’en-tête
#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);

#endif

La première inclusion définit CONVERT_H et conserve le prototype. Les inclusions ultérieures dans cette même unité de traduction ignorent le contenu. Utilise un nom de garde distinct fondé sur le nom du fichier d’en-tête ; évite les traits de soulignement initiaux réservés à l’implémentation. Une autre unité de traduction commence son propre prétraitement et inclut les déclarations pour elle-même. Une garde n’empêche pas différents fichiers objets de définir le même symbole externe.

De nombreux compilateurs prennent en charge #pragma once, mais ce n’est pas une directive ISO C. Les gardes fonctionnent avec les directives conditionnelles standard.

Lire les informations prédéfinies
NomInformation
__FILE__Nom du fichier source courant sous forme de chaîne
__LINE__Numéro de la ligne source courante
__DATE__, __TIME__Date et heure de traduction
__STDC_VERSION__Version de la norme sélectionnée

Les valeurs de version comprennent 199901L pour C99, 201112L pour C11, 201710L pour C17 et 202311L pour C23. __func__ contient le nom de la fonction courante, mais c’est un identificateur prédéfini et non une macro.

Une branche propre à une plateforme peut tester defined(_WIN32) ou defined(__linux__), puis utiliser #else pour le cas restant. Pour rejeter explicitement une compilation non prise en charge, écris une condition suivie de #error "This program needs C11 or later" et de #endif. L’omission d’un #endif produit une erreur de directive telle que « unterminated conditional directive ».

Points clés

  • #ifdef teste l’existence ; #if teste une valeur.
  • Les identificateurs non définis dans #if sont évalués à zéro.
  • -DNAME=value fournit une définition de macro à la compilation.
  • Les gardes d’inclusion empêchent les traitements répétés dans une même unité de traduction.
  • Les macros prédéfinies fournissent des informations sur le fichier, la ligne, la date et l’heure de compilation et la norme.
4

Répartir un programme entre des fichiers source et des fichiers d’en-tête

Un programme plus volumineux peut conserver les fonctions liées dans des fichiers source distincts tout en partageant leurs déclarations dans des fichiers d’en-tête. Chaque fichier source est compilé séparément, puis l’éditeur de liens relie les fichiers objets obtenus. Tu vas construire un petit module, déterminer les noms dont les autres fichiers ont besoin et diagnostiquer les définitions manquantes ou multiples. Inclure le même fichier d’en-tête dans les fichiers appelants et dans celui qui contient la définition aide le compilateur à détecter les incompatibilités avant l’exécution du programme.

4.1 Compiler plusieurs fichiers source et effectuer l’édition de liens

La compilation séparée transforme chaque fichier source en fichier objet avant l’édition de liens du programme. Elle permet de réutiliser les fichiers objets inchangés et donne aux fonctions liées un emplacement clairement défini.

Suivre les étapes de compilation

Une unité de traduction est un fichier .c après prétraitement, comprenant le contenu des fichiers d’en-tête qu’il inclut. Chaque unité est compilée indépendamment.

ÉtapeRésultatOption GCC qui arrête le processus à cette étape
PrétraitementTexte C développé-E
CompilationTexte assembleur-S
AssemblageFichier objet-c
Édition de liensExécutableCompilation finale normale

Un fichier objet contient du code machine et des informations sur les symboles correspondant aux noms qu’il définit ou dont il a besoin. Sur les systèmes de type Unix, il porte généralement le suffixe .o. L’éditeur de liens relie les utilisations de noms externes aux définitions fournies par les fichiers objets et les bibliothèques.

Compiler deux fichiers et effectuer leur édition de liens

Supposons que convert.h déclare double c_to_f(double c);, que convert.c inclue ce fichier d’en-tête et définisse la fonction, et que main.c inclue le fichier d’en-tête et appelle la fonction.

gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp

La première commande produit main.o sans effectuer l’édition de liens. La deuxième produit convert.o. La dernière crée l’exécutable temp à partir des deux fichiers objets. Tu peux aussi compiler et effectuer l’édition de liens en une seule commande :

gcc -Wall -Wextra -std=c17 main.c convert.c -o temp

Fournis les fichiers source pour la compilation et les fichiers objets pour l’édition de liens. Les fichiers d’en-tête sont ajoutés par #include ; ne les indique pas comme des sources de programme à compiler séparément. Si seul convert.c change, réutilise main.o, reconstruis convert.o et refais l’édition de liens. Si un fichier d’en-tête partagé change, reconstruis chaque fichier objet dont le fichier source l’inclut.

Diagnostiquer l’étape qui a échoué

Une erreur de compilation concerne une unité de traduction : syntaxe invalide, nombre d’arguments incorrect ou types visibles incompatibles. L’éditeur de liens ne peut pas les corriger.

Une erreur d’édition de liens concerne le programme assemblé. Si tu compiles seulement main.c, le prototype peut permettre à la compilation de réussir alors que l’édition de liens signale « undefined reference to c_to_f » ou « Undefined symbols ». La déclaration promet une fonction ; elle ne fournit pas son code machine. Ajoute convert.c à une compilation combinée, ou convert.o à l’édition de liens.

Deux définitions externes liées d’une même fonction provoquent une erreur de symbole multiple. Un programme ordinaire possède une seule fonction main ; l’édition de liens de deux sources qui la définissent toutes deux échoue. Des fonctions static privées à leur fichier peuvent porter le même nom dans différentes unités de traduction car elles ont une liaison interne.

Les bibliothèques fournissent aussi des définitions. Sur de nombreux systèmes Linux, le code utilisant des fonctions déclarées par <math.h> nécessite une option d’édition de liens pour la bibliothèque mathématique : gcc main.c -o app -lm. Place cette option de bibliothèque après le code qui en a besoin.

Inclure l’interface, compiler l’implémentation

Évite #include "convert.c". Cela copie les corps des fonctions dans une autre unité de traduction. Si convert.c est aussi compilé et lié, ses définitions externes apparaissent deux fois. Le fichier .h d’un module partage ses déclarations ; son fichier .c est compilé une seule fois comme fichier source.

Points clés

  • Une unité de traduction est un fichier source prétraité avec le texte qu’il inclut.
  • -c produit un fichier objet sans effectuer l’édition de liens.
  • Inclus chaque fichier objet nécessaire lors de l’édition de liens de l’exécutable.
  • Les définitions manquantes et multiples sont des problèmes d’édition de liens.
  • Inclus les fichiers d’en-tête ; compile séparément les fichiers source.

4.2 Écrire des fichiers d’en-tête

Le fichier d’en-tête d’un module partage son interface ; son fichier source fournit l’implémentation. Les fichiers appelants et le fichier d’implémentation incluent tous le fichier d’en-tête pour que leurs déclarations concordent.

Déclarer l’interface une seule fois

Un fichier convert.h muni d’une garde contient :

#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);
double f_to_c(double f);

#endif

L’implémentation dans convert.c l’inclut avant les corps des fonctions :

#include "convert.h"

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

double f_to_c(double f)
{
    return (f - 32.0) * 5.0 / 9.0;
}

Un fichier appelant inclut le même fichier d’en-tête et <stdio.h>. L’appel printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); affiche 77.0 100.0.

Si l’implémentation définit float c_to_f(float c), l’inclusion de son fichier d’en-tête révèle les types incompatibles au compilateur. Une compilation indépendante sans cette déclaration peut ne pas détecter la différence.

Choisir le bon contenu
À partager dans un fichier d’en-têteÀ garder dans un fichier source
Prototypes de fonctionsCorps de fonctions à liaison externe
Déclarations de variables externDéfinitions d’objets partagés
Macros et types nécessairesFonctions auxiliaires et état privés

Un fichier d’en-tête contenant int count = 0; définit un objet dans chaque unité de traduction qui l’inclut, ce qui provoque des définitions externes multiples. Même int count; est une définition provisoire. Depuis la version 10, le comportement par défaut de GCC signale les copies répétées lors de l’édition de liens, tandis que certaines chaînes de compilation fusionnent les symboles communs et masquent le bogue. Utilise extern int count; dans le fichier d’en-tête et une seule définition dans un fichier source.

Rendre l’inclusion fiable

Une garde empêche le traitement répété dans une unité de traduction, y compris lors d’une inclusion indirecte par d’autres fichiers d’en-tête. Les prototypes compatibles peuvent être répétés, mais des définitions de types tels qu’un enum peuvent provoquer des erreurs de redéfinition.

Un fichier d’en-tête autonome inclut ce dont ses déclarations ont besoin. Si un prototype utilise size_t, inclus-y <stddef.h>. Les fichiers appelants ne devraient pas avoir besoin d’un ordre d’inclusion particulier.

Pour les fichiers d’en-tête du projet, les guillemets font chercher GCC d’abord dans le répertoire du fichier qui inclut. Ajoute un répertoire distinct de fichiers d’en-tête avec -Iinclude, comme dans gcc -Iinclude -std=c17 -c src/main.c. Le fichier objet de l’implémentation doit toujours être lié.

Points clés

  • Les fichiers d’en-tête partagent les prototypes, les déclarations extern, les macros et les types.
  • Garde les définitions d’objets partagés et les corps de fonctions à liaison externe dans les fichiers source.
  • Inclus le fichier d’en-tête d’un module dans son propre fichier d’implémentation.
  • Utilise des gardes et inclus les fichiers d’en-tête dont tes déclarations ont besoin.

4.3 Contrôler la liaison avec static et extern

La liaison détermine si des déclarations situées à différents endroits désignent la même entité. Elle est distincte de la portée, qui décrit où un nom peut être utilisé, et de la durée de stockage, qui décrit combien de temps un objet existe.

Relier un nom à un objet
LiaisonSens
ExterneDes déclarations peuvent désigner une même entité dans plusieurs unités de traduction
InterneLes déclarations désignent une entité dans une seule unité de traduction
AbsenteUn nom local, tel qu’un paramètre ordinaire, identifie sa propre entité

Les fonctions et les variables ordinaires de portée de fichier ont une liaison externe par défaut. À la portée de fichier, static donne une liaison interne. Les variables locales automatiques ordinaires et les paramètres n’ont pas de liaison.

Pour partager un compteur d’erreurs, place ces déclarations dans un fichier d’en-tête muni d’une garde :

extern int error_count;
void log_error(int code);

Un fichier source inclut ce fichier d’en-tête et fournit :

int error_count = 0;

void log_error(int code)
{
    printf("error: %d\n", code);
    error_count++;
}

Ce fichier source inclut aussi <stdio.h> pour printf. Les autres fichiers source incluent le fichier d’en-tête partagé, sans ajouter de seconde définition. Après trois appels, chaque fonction appelante qui lit error_count désigne le même objet et voit 3.

Distinguer déclaration et définition

extern int error_count; déclare un objet existant et n’alloue aucun nouvel espace de stockage. int error_count = 0; le définit. Un initialiseur change la situation : à la portée de fichier, extern int error_count = 0; est une définition malgré le mot-clé. Garde l’initialiseur dans l’unique fichier qui définit la variable.

Dans les déclarations ordinaires de fonctions publiques, extern est facultatif : double c_to_f(double c); déclare déjà une fonction à liaison externe. Inclus la déclaration publique dans le fichier qui contient sa définition pour que le compilateur puisse vérifier leur compatibilité.

Masquer l’état derrière des fonctions
static int count = 0;

void counter_increment(void)
{
    count++;
}

int counter_value(void)
{
    return count;
}

La valeur stockée existe pendant toute l’exécution, mais seule cette unité de traduction peut y faire référence par le nom count. Place les prototypes des fonctions publiques dans un fichier d’en-tête. Un autre fichier peut incrémenter et lire le compteur par ces fonctions sans modifier directement la variable privée. Une fonction auxiliaire peut aussi être static lorsque les autres fichiers n’en ont pas besoin.

Diagnostiquer un mauvais choix de liaison

Une définition static int count; dans un fichier d’en-tête crée un compteur privé indépendant dans chaque unité de traduction qui l’inclut. Mettre l’un à jour ne met pas les autres à jour. Les gardes d’inclusion ne fusionnent pas ces objets : chaque unité de traduction prétraite sa propre copie du fichier d’en-tête.

Une fonction définie static dans une unité de traduction ne peut pas répondre à un appel externe provenant d’une autre ; l’édition de liens échoue pour ce nom externe. Ne donne une liaison externe qu’aux fonctions dont les autres fichiers ont besoin et déclare-les dans le fichier d’en-tête.

Dans une fonction, static donne à une variable locale un stockage conservé sans élargir sa portée. En dehors des fonctions, il donne à un objet ou une fonction une liaison interne. Les variables de portée de fichier ont une durée de stockage statique avec ou sans ce mot-clé.

Points clés

  • La liaison externe relie des déclarations entre unités de traduction.
  • Partage une variable avec des déclarations extern et une seule définition.
  • À la portée de fichier, static rend un objet ou une fonction privé à son unité de traduction.
  • Un fichier d’en-tête contenant une variable statique donne des objets distincts aux unités qui l’incluent.
  • Un initialiseur transforme une déclaration d’objet extern de portée de fichier en définition.
  • La conservation d’une variable locale par static et la liaison donnée par static à la portée de fichier ont des rôles différents.
5

Compiler des programmes composés de plusieurs fichiers avec make

Une compilation correcte demande plus qu’une liste de commandes de compilateur : il faut savoir quels fichiers dépendent de quels autres fichiers. Un makefile enregistre ces relations afin qu’une modification du code source recompile les fichiers objets concernés, puis déclenche à nouveau l’édition de liens du programme. Tu vas suivre ces décisions de reconstruction, inclure les dépendances aux fichiers d’en-tête et raccourcir les commandes répétées à l’aide de variables et de règles de motif. Les simulations d’exécution montrent les opérations prévues avant l’exécution d’une recette.

5.1 Écrire les règles et les dépendances d’un fichier makefile

make lit Makefile ou makefile dans le répertoire du projet et met à jour la cible demandée. Sans argument indiquant une cible, il construit normalement la première cible ordinaire.

Enregistrer les sorties et les entrées

Une règle commence par target: prerequisites. Sa recette se trouve soit sur les lignes suivantes indentées par une tabulation, soit après un point-virgule sur la même ligne :

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g

temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o

main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c

convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c

.PHONY: clean
clean:; rm -f temp main.o convert.o

Ici, temp est la cible par défaut. make clean demande l’action de nettoyage. Si tu écris une recette sur la ligne suivante, utilise une véritable tabulation : des espaces peuvent provoquer « missing separator ». L’option d’un éditeur qui rend les caractères blancs visibles aide à repérer la différence.

Suivre les décisions de reconstruction

Make met d’abord à jour les dépendances d’une cible. Il reconstruit ensuite la cible si elle est absente ou si une dépendance a une date de modification plus récente.

  • Première compilation : compiler les deux sources, puis effectuer l’édition de liens de temp : trois commandes.
  • Aucun changement : aucune recette du programme n’a besoin d’être exécutée.
  • Modification de convert.c uniquement : le recompiler et refaire l’édition de liens : deux commandes ; réutiliser main.o.
  • Modification de convert.h : recompiler les deux sources et refaire l’édition de liens : trois commandes.

Le fichier d’en-tête concerne les deux unités de traduction, même si aucune des deux sources n’a été modifiée. Représente-toi les fichiers d’entrée alimentant les fichiers objets, puis les fichiers objets alimentant l’exécutable. Les modifications se propagent selon ces relations enregistrées.

Inclure les dépendances aux fichiers d’en-tête

La règle incomplète main.o: main.c ne tient pas compte des modifications de convert.h. Make ne lit pas le code source C pour découvrir les inclusions ; il suit les dépendances déclarées. Liste les fichiers d’en-tête inclus ou charge les fichiers de dépendances générés par le compilateur pour éviter de lier des fichiers objets périmés.

Regrouper les opérations dépendantes de l’interpréteur

Des lignes de recette distinctes utilisent normalement des interpréteurs distincts. Une ligne contenant uniquement cd build ne change pas le répertoire de la ligne suivante. Utilise une seule commande comme cd build && gcc -c ../main.c lorsque la compilation dépend de ce changement de répertoire. Prévisualise les recettes prévues avec make -n.

Points clés

  • Les règles indiquent des cibles, des dépendances et des recettes indentées par une tabulation.
  • Make met les dépendances à jour avant de décider si leur cible doit être reconstruite.
  • Une cible absente ou une dépendance plus récente déclenche une reconstruction.
  • Les modifications d’un fichier d’en-tête doivent déclencher la reconstruction de chaque fichier objet concerné.
  • Chaque ligne de recette s’exécute normalement dans son propre interpréteur de commandes.

5.2 Simplifier les fichiers makefile avec des variables et des règles de motif

Les variables et les règles de motif éliminent les répétitions de commandes du compilateur tout en préservant le graphe de dépendances. Dans les exemples, chaque recette est placée après un point-virgule sur la ligne de sa règle. Ne raccourcis un makefile qu’une fois ses relations de compilation correctes.

Nommer les paramètres partagés
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o

temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^

%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:; rm -f temp $(OBJS)

$(CC) développe la variable du compilateur ; $(CFLAGS) développe ses options. CPPFLAGS contient par convention les options du préprocesseur comme -DDEBUG ou -Iinclude. Les options -Wall -Wextra activent les avertissements, -g ajoute des informations de débogage et -O2 active l’optimisation. Les variables make non définies se développent en texte vide. Écris $(CC) et non $CC : cette dernière écriture désigne la variable à une lettre C, suivie d’un C littéral.

Lire les variables automatiques dans une règle
VariableValeur dans la règle courante
$@Nom de la cible
$<Première dépendance
$^Toutes les dépendances, sans répétition des noms
$?Dépendances plus récentes que la cible

Pour main.o, le radical du motif est main : %.c devient donc main.c. La recette de compilation utilise $< pour transmettre cette source au compilateur et $@ pour nommer main.o. Le fichier d’en-tête est une dépendance, mais il n’est pas transmis comme autre entrée de compilation.

Dans la règle d’édition de liens, toutes les dépendances sont des fichiers objets. $^ se développe en main.o convert.o et $@ devient temp. Utiliser $< à cet endroit ne transmettrait que main.o, en perdant les définitions de convert.o.

GNU make possède des règles prédéfinies qui peuvent compiler x.c en x.o à l’aide de CC, CPPFLAGS et CFLAGS. Une règle de dépendance explicite sans recette peut utiliser cette recette prédéfinie. GNU make peut même construire un programme simple avec make hello à partir de hello.c sans makefile.

Marquer les cibles d’action comme factices

clean désigne une action, non un fichier à créer. .PHONY: clean garantit l’exécution de l’action même si un fichier nommé clean existe. rm -f accepte que des fichiers soient déjà absents. Une cible all qui regroupe plusieurs programmes est généralement factice elle aussi.

Générer les listes de fichiers d’en-tête et examiner les reconstructions

Ajoute -MMD -MP aux options de compilation pour écrire un fichier .d à côté de chaque fichier objet. -MMD enregistre les dépendances aux fichiers d’en-tête utilisateur ; -MP ajoute des règles vides qui facilitent la gestion de la suppression d’un fichier d’en-tête enregistré. Après la cible par défaut ordinaire, charge les listes avec :

-include $(OBJS:.o=.d)

La substitution transforme les noms des fichiers objets en noms de fichiers de dépendances. Le signe moins initial autorise l’absence de fichiers .d lors de la première compilation. Supprime ces fichiers générés dans clean avec les fichiers objets.

make -n prévisualise les commandes, make -B force la reconstruction et make -j4 permet d’exécuter jusqu’à quatre recettes à la fois. Des dépendances correctes sont aussi importantes pour les compilations parallèles. Modifier uniquement le texte de CFLAGS ne rend pas les fichiers objets existants plus anciens qu’une dépendance. Après avoir modifié les options, utilise make -B ou effectue une reconstruction après nettoyage pour que chaque fichier objet utilise les nouveaux paramètres.

Points clés

  • Utilise $(NAME) pour développer une variable make nommée.
  • $@, $< et $^ désignent la cible, la première dépendance et toutes les dépendances.
  • Les règles de motif réutilisent une recette pour des fichiers aux radicaux correspondants.
  • Les cibles factices exécutent des actions même si des fichiers de même nom existent.
  • Les fichiers .d générés enregistrent les dépendances aux fichiers d’en-tête utilisateur.
  • Force une reconstruction ou nettoie avant de reconstruire après avoir modifié les options du compilateur.

Fonctions et structure des programmes en C : exemples

5 Leçons Interactives

1

Construire et suivre un appel de fonction

19 étapes
Écris une fonction, déclare-la, conserve son résultat et suis un calcul récursif.
  1. 1
    IntroductionCommencer par une petite tâche

    Suis une valeur dans une fonction, puis repère où va son résultat. Nous allons construire une définition et un prototype, distinguer un paramètre copié de la variable de la fonction appelante, puis suivre des appels récursifs jusqu’à un cas de base et à la remontée de leurs valeurs de retour.

  2. 2
    ExempleDonner à une tâche une entrée typée

    Nous voulons une fonction qui ajoute une correction fixe à une mesure entière.

    int corrected(int reading)
    {
        return reading + 4;
    }
    

    int avant le nom décrit le résultat. int reading déclare un paramètre et le corps calcule la valeur renvoyée. Définis cette fonction en dehors de tous les autres corps de fonctions ; le C standard ne prend pas en charge les définitions imbriquées.

  3. 3
    ExempleUtiliser un appel comme expression

    Pour int result = corrected(9);, l’argument 9 initialise le paramètre reading. L’expression de retour produit 13 et l’affectation dans la fonction appelante stocke cette valeur dans result.

    L’appel peut aussi se trouver dans une autre expression : 2 * corrected(9) donne 26. Écrire seulement corrected(9); exécute tout de même l’appel, mais ignore sa valeur de retour.

  4. 4
    ConseilChoisir entre renvoyer et afficher

    Un calcul peut renvoyer un résultat sans l’afficher. La fonction appelante peut ainsi le stocker, le comparer ou l’afficher plus tard.

    Une fonction déclarée void peut effectuer une tâche d’affichage, mais ne fournit aucune valeur pour une affectation. Dans son corps, return; permet de quitter sans valeur. Dans une fonction qui renvoie une valeur, assure-toi que chaque chemin produisant un résultat atteint un return approprié ; afficher un nombre ne le fournit pas à la fonction appelante.

  5. 5
    DéfiVérifier le calcul avant le retour

    Que renvoie cette fonction pour average_pair(5, 8) ?

    double average_pair(int a, int b)
    {
        return (a + b) / 2;
    }
    

    ---reveal---

    Elle renvoie 6.0 : l’addition entière donne 13, la division entière donne 6 et la conversion du retour donne 6.0. Utilise 2.0 pour effectuer la division en virgule flottante si tu as besoin de 6.5. Le type de retour ne peut pas rétablir une partie fractionnaire déjà supprimée.

  6. 6
    ExplicationInformer le compilateur avant l’appel

    Lorsque le corps vient plus loin, place int corrected(int reading); avant l’appel. Ce prototype permet au compilateur de vérifier le nombre d’arguments et les conversions. Son point-virgule marque une déclaration ; la définition fournit le corps.

  7. 7
    Point cléIndiquer une liste d’entrées vide

    Utilise int read_sensor(void); pour indiquer l’absence de paramètres en C17 comme en C23. En C17, int read_sensor(); laisse les informations sur les paramètres non spécifiées ; en C23, cette écriture signifie qu’il n’y a aucun paramètre. Sélectionne explicitement C17 avec -std=c17.

  8. 8
    DéfiSuivre une conversion autorisée

    Le compilateur a rencontré int corrected(int reading);. Que renvoie corrected(6.8) et l’appel doit-il être rejeté ?

    ---reveal---

    L’appel peut être compilé. Le prototype entraîne la conversion de 6.8 en entier 6, puis la fonction ajoute 4 et renvoie 10. Un avertissement de conversion peut aider à révéler les informations perdues ; utilise -Wconversion. Un prototype permet de vérifier un appel à l’aide de ses types déclarés, mais un argument numérique de type différent ne constitue pas toujours une erreur.

  9. 9
    ExempleReprésenter deux valeurs distinctes

    Considère une autre fonction :

    void change(int reading)
    {
        reading += 4;
    }
    

    La fonction appelante exécute int sample = 9; change(sample);. À l’entrée, sample et le paramètre contiennent chacun 9 dans des objets distincts. Le corps donne au paramètre la valeur 13. Au retour de l’appel, sample contient toujours 9 : aucune affectation ne l’a modifié.

  10. 10
    LienLaisser la fonction appelante conserver un résultat

    Utilise sample = corrected(sample); pour conserver le résultat. La fonction calcule à partir d’une copie ; la fonction appelante stocke ensuite la valeur renvoyée. Des noms identiques ne relieraient pas leurs objets : c’est l’affectation qui met sample à jour.

  11. 11
    DéfiRepérer le résultat d’appel ignoré

    En partant de int sample = 7;, la fonction appelante exécute :

    corrected(sample);
    sample = corrected(sample);
    

    Que vaut sample ensuite ?

    ---reveal---

    Le premier appel calcule 11 et l’ignore, laissant sample à 7. Le second reçoit lui aussi 7, renvoie 11 et, cette fois, la fonction appelante le stocke. La valeur finale est 11, pas 15 ; un appel ne met pas à jour sa variable argument simplement en renvoyant une valeur modifiée.

  12. 12
    ExplicationAjouter une règle d’arrêt à la récursivité

    Une fonction récursive s’appelle elle-même sur un problème plus petit :

    int total_to(int n)
    {
        if (n <= 0) return 0;
        return n + total_to(n - 1);
    }
    

    Pour une valeur d’entrée positive, soustraire un rapproche du cas de base. Le cas de base répond directement. Le cas récursif ne peut pas terminer son addition avant le retour de l’appel portant sur le problème plus petit.

  13. 13
    ExempleÉcrire les appels à la descente

    Suis total_to(3). Le premier appel contient n == 3 et attend de pouvoir ajouter 3 à total_to(2). Cet appel contient son propre n == 2 et attend total_to(1).

    L’appel avec n == 1 attend total_to(0), qui renvoie immédiatement 0. À ce point le plus profond, quatre appels sont actifs. Ne remplace pas les valeurs précédentes de n par zéro ; elles appartiennent à des appels différents.

  14. 14
    ExempleRenseigner les résultats à la remontée

    Une fois que total_to(0) renvoie 0, les calculs en attente peuvent s’achever :

    • L’appel avec n == 1 renvoie 1 + 0, soit 1.
    • L’appel avec n == 2 renvoie 2 + 1, soit 3.
    • L’appel avec n == 3 renvoie 3 + 3, soit 6.

    La plupart des implémentations conservent ces appels actifs dans des cadres de pile. Chaque retour fait reprendre la fonction appelante après son appel, puis libère l’état automatique de l’appel terminé.

  15. 15
    DéfiVérifier si un cas de base est atteignable

    Supposons que le test de base de total_to soit remplacé par n == 0 et que l’étape récursive utilise toujours n - 1. Quel problème survient lorsque l’argument initial est -2 ?

    ---reveal---

    Les valeurs d’entrée deviennent -2, -3, -4 et continuent à s’éloigner de zéro. Le test d’arrêt existe, mais il est impossible à atteindre sur ce chemin. La poursuite de la récursivité peut épuiser l’espace de la pile. La version avec n <= 0 arrête immédiatement cet appel.

  16. 16
    ConseilVérifier aussi la taille du résultat

    Un calcul récursif qui se termine peut tout de même dépasser la plage de son type de retour. Avec un int de 32 bits, factorial(12) est représentable, tandis que la valeur mathématique de factorial(13) dépasse le maximum 2147483647.

    Un débordement signé est un comportement indéfini. Avant de choisir les valeurs d’entrée d’une fonction récursive, vérifie à la fois que les appels s’arrêtent et que les résultats intermédiaires et finaux des calculs sont représentables dans les types utilisés. Une boucle nécessite elle aussi cette vérification des calculs.

  17. 17
    ConseilSéparer les appels lorsque l’ordre compte

    L’évaluation de toutes les expressions des arguments s’achève avant le début du corps de la fonction appelée, mais C ne précise pas leur ordre d’évaluation. Si deux appels modifient un état partagé, calcule leurs résultats dans des instructions distinctes avant de les transmettre.

    Évite f(i, i++) : cet appel lit et modifie i sans le séquencement nécessaire. Le passage par valeur ne rend pas sûre une expression d’argument qui ne l’est pas.

  18. 18
    DéfiCombiner déclaration, conversion et appels

    Avec total_to déclarée avant son utilisation, une fonction appelante écrit int x = 2; total_to(x); x = total_to(3.9);. Quelle est la valeur finale de x ?

    ---reveal---

    Le premier résultat est ignoré, donc x reste à 2. Le second argument est converti en 3 ; le suivi récursif renvoie 6, que l’affectation stocke dans x. La déclaration contrôle la conversion, la récursivité calcule la valeur et la fonction appelante décide de la conserver ou non.

  19. 19
    RécapitulatifUtiliser trois points de contrôle pour un appel

    Pour un appel, vérifie sa déclaration, suis ses entrées copiées et repère l’utilisation de sa valeur de retour. Pour la récursivité, suis séparément chaque appel actif et combine les résultats en remontant depuis le cas de base.

2

Suivre les noms et les valeurs conservées

13 étapes
Suis un bloc, un compteur statique et un total partagé sans confondre leurs objets.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionCommencer par une petite tâche
  2. 2
    ExempleLire la déclaration visible la plus proche
  3. 3
    ExplicationConserver la portée à l’endroit où la fonction est écrite
  4. 4
    DéfiCorriger une valeur nécessaire après un bloc
  5. 5
    ExempleObserver le redémarrage d’un compteur automatique
  6. 6
    ExempleConserver le compteur avec static
  7. 7
    DéfiDistinguer initialisation et affectation
  8. 8
    ConseilInterpréter le mot-clé selon son emplacement
  9. 9
    ExempleSuivre la mise à jour d’une variable globale
  10. 10
    DéfiRepérer la variable locale qui masque la variable globale
  11. 11
    LienRendre un calcul plus facile à tester
  12. 12
    DéfiCombiner portée et état mémorisé
  13. 13
    RécapitulatifAnnoter séparément les noms et les durées de vie
3

Examiner le code avant la compilation

13 étapes
Développe des expressions, sélectionne une branche de débogage et suis une garde d’inclusion.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionCommencer par une petite tâche
  2. 2
    ExplicationVoir ce que fournit réellement l’inclusion
  3. 3
    ExempleRemplacer les lexèmes avant d’appliquer les règles des opérateurs
  4. 4
    ExempleProtéger les regroupements intérieur et extérieur
  5. 5
    DéfiRepérer ce que les parenthèses ne peuvent pas corriger
  6. 6
    ConseilDistinguer les règles des macros de celles de la portée
  7. 7
    ExempleTester l’existence et la valeur d’un réglage
  8. 8
    DéfiSélectionner une branche avec une option du compilateur
  9. 9
    ExempleSuivre la première inclusion d’un fichier d’en-tête
  10. 10
    Point cléRepartir d’un état de garde vierge dans une autre unité
  11. 11
    LienSituer un diagnostic dans le code source
  12. 12
    DéfiCombiner développement et sélection de branche
  13. 13
    RécapitulatifExaminer la transformation dans l’ordre
4

Construire un programme à partir de deux sources

14 étapes
Crée un fichier d’en-tête et une implémentation, effectue l’édition de liens de leurs fichiers objets et protège l’état privé.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionCommencer par une petite tâche
  2. 2
    ExplicationSuivre une unité de traduction jusqu’au fichier objet
  3. 3
    ExempleÉcrire la déclaration partagée
  4. 4
    ExemplePlacer l’implémentation dans son propre fichier source
  5. 5
    DéfiPlacer correctement une variable partagée
  6. 6
    ExempleCompiler séparément, puis effectuer l’édition de liens
  7. 7
    DéfiCorriger une référence indéfinie
  8. 8
    ConseilNe pas copier l’implémentation dans le fichier appelant
  9. 9
    ExempleGarder un compteur dans le module
  10. 10
    ExplicationDistinguer trois propriétés du compteur
  11. 11
    DéfiExpliquer deux compteurs indépendants
  12. 12
    ConseilRendre un fichier d’en-tête autonome
  13. 13
    DéfiCorriger ensemble la confidentialité et l’interface
  14. 14
    RécapitulatifAssocier chaque besoin à son étape de compilation
5

Ne reconstruire que ce qui a changé

15 étapes
Écris les dépendances, suis les reconstructions et raccourcis un makefile sans en perdre le sens.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionCommencer par une petite tâche
  2. 2
    ExempleEnregistrer les entrées de l’exécutable et des fichiers objets
  3. 3
    ExempleDonner une recette à un fichier objet
  4. 4
    ExempleEffectuer une première compilation puis une reconstruction après modification d’un fichier source
  5. 5
    DéfiSuivre la modification d’un fichier d’en-tête partagé
  6. 6
    ConseilExaminer les opérations prévues avant de les exécuter
  7. 7
    ExplicationRemplacer les paramètres répétés par des variables
  8. 8
    ExempleDévelopper la recette d’édition de liens
  9. 9
    ExempleUtiliser un même motif de compilation pour les deux fichiers objets
  10. 10
    DéfiChoisir une source sans compiler le fichier d’en-tête
  11. 11
    ConseilFaire du nettoyage une action
  12. 12
    LienLaisser le compilateur enregistrer les dépendances aux fichiers d’en-tête
  13. 13
    DéfiGarantir une reconstruction parallèle correcte
  14. 14
    DéfiAppliquer les options modifiées à tout le programme
  15. 15
    RécapitulatifVérifier le graphe avant de se fier au résultat

Fonctions et structure des programmes en C : QCM

50 Questions Pratiques en 5 séries

Série 1 : Suivre les noms locaux et l’état mémorisé10 Questions Pratiques

Question 1 sur 10

Où le nom i de for (int i = 0; i < 6; i++) { } peut-il être utilisé ?
  1. Dans toute la fonction qui contient la boucle.
  2. Dans tout le reste du fichier source.
  3. Dans chaque fonction appelée par la boucle.
  4. Dans cette instruction for.
Afficher la réponseMasquer la réponse
Réponse : D
La déclaration dans l’en-tête limite la portée de i à cette instruction for, y compris son corps. Elle ne se comporte pas comme une déclaration placée avant la boucle, qui resterait visible après celle-ci.

Question 2 sur 10

Quelles valeurs ce programme affiche-t-il ?

#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
    int size = 4;
    show();
    printf("%d", size);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
Afficher la réponseMasquer la réponse
Réponse : A
Dans show, la déclaration visible est celle de size à la portée de fichier, dont la valeur est 9. L’affichage dans main utilise sa valeur locale 4. Appeler show depuis main ne lui donne pas accès à la portée locale de la fonction appelante : le premier affichage ne peut donc pas utiliser cette variable locale simplement parce qu’elle porte le même nom.

Question 3 sur 10

Qu’est-ce qui est affiché ?

int total = 0;
for (int i = 1; i <= 3; i++) {
    int total = i;
    total += 2;
}
printf("%d", total);
  1. 6
  2. 0
  3. 12
  4. 5
Afficher la réponseMasquer la réponse
Réponse : B
Chaque itération de la boucle déclare un nouveau total intérieur et modifie cet objet. Le total extérieur reste à zéro et c’est le nom visible après la boucle. Ajouter les valeurs des itérations au total extérieur nécessiterait une affectation à celui-ci dans la boucle, sans la déclaration qui le masque.

Question 4 sur 10

Pourquoi int amount = amount + 2; dans un bloc ne s’initialise-t-il pas à partir d’un amount extérieur ?
  1. Le nom extérieur perd sa valeur stockée.
  2. Tous les initialiseurs s’exécutent avant le début de la portée.
  3. Le nouveau nom est dans sa portée dès son initialiseur.
  4. Les deux déclarations désignent le même objet local.
Afficher la réponseMasquer la réponse
Réponse : C
Le nouveau nom est dans sa portée avant l’exécution de son initialiseur : le côté droit lit donc la nouvelle variable locale non initialisée. Le masquage ne copie pas la valeur de l’objet extérieur dans cette variable.

Question 5 sur 10

Combien de fois static int calls = 0; est-il initialisé pendant une exécution du programme ?
  1. Une fois, avant le démarrage du programme.
  2. Une fois par appel de sa fonction.
  3. Une fois à chaque niveau de récursivité.
  4. Une fois à chaque lecture.
Afficher la réponseMasquer la réponse
Réponse : A
La variable locale statique est initialisée une seule fois avant le démarrage du programme. Sa déclaration ne la remet pas à zéro à l’entrée ; une affectation distincte comme calls = 0; la remettrait à zéro chaque fois qu’elle serait atteinte.

Question 6 sur 10

Quelles valeurs trois appels distincts à tick renvoient-ils ?

int tick(void) {
    static int n = 5;
    n += 2;
    return n;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
Afficher la réponseMasquer la réponse
Réponse : C
L’incrémentation précède chaque retour et la valeur conservée passe de 5 à 7, 9, puis 11. Renvoyer 7 à chaque fois décrirait une nouvelle variable locale automatique, et non cet objet statique.

Question 7 sur 10

Que vaut result après int result = visit(2); ?

int visit(int n) {
    static int calls = 0;
    calls++;
    if (n == 0) return calls;
    int inner = visit(n - 1);
    return inner + calls;
}
  1. 6
  2. 9
  3. 7
  4. 3
Afficher la réponseMasquer la réponse
Réponse : B
Les trois entrées dans la fonction mettent toutes à jour le même calls statique, qui atteint 3 lors de l’appel du cas de base. Cet appel renvoie 3 ; les deux appels en attente ajoutent chacun la valeur partagée actuelle 3, ce qui produit 6 et 9. Obtenir 6 revient à traiter calls comme des valeurs conservées distinctes de 1, 2 et 3 pour les trois cadres, alors qu’une variable locale statique est partagée.

Question 8 sur 10

Une fonction de calcul int permitted(int n) lit une variable globale limit. Pourquoi le même argument pourrait-il produire un résultat différent lors d’un appel ultérieur ?
  1. Le passage d’un argument int modifie sa variable d’origine.
  2. Chaque appel remet à zéro tous les objets de portée de fichier.
  3. Un nom local de la fonction appelante remplace la variable globale.
  4. Une autre opération peut avoir modifié limit.
Afficher la réponseMasquer la réponse
Réponse : D
La fonction dépend de la valeur globale en plus de n : une mise à jour de limit peut donc modifier un résultat ultérieur. Passer le même argument ne remet pas cette entrée partagée à zéro. L’initialisation à la portée de fichier a lieu une seule fois et le passage par valeur ne transforme pas les variables locales de la fonction appelante en entrées cachées pour cette fonction.

Question 9 sur 10

Quels sont les deux entiers affichés ?

#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
    int count = 2;
    count = advance(count);
    int next = advance(1);
    printf("%d %d", count, next);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Afficher la réponseMasquer la réponse
Réponse : D
Le premier appel reçoit la valeur locale 2, mais fait passer la variable globale de 6 à 8, puis renvoie 8 dans la variable locale. L’appel suivant ne modifie que la variable globale, qui passe à 9, et renvoie cette valeur dans next. Afficher 9 9 reviendrait à traiter à tort le count local auquel une valeur a été affectée comme un alias de la variable globale après le premier appel.

Question 10 sur 10

Une fonction doit initialiser à l’exécution une variable numérique conservée avec read_sensor(). Pourquoi static int first = read_sensor(); est-il invalide en C17 ?
  1. Une variable locale statique ne peut pas être lue après initialisation.
  2. Un initialiseur numérique statique doit être constant.
  3. Une fonction ne peut pas renvoyer de valeur à une variable locale.
  4. Une variable locale statique est initialisée à chaque appel.
Afficher la réponseMasquer la réponse
Réponse : B
L’appel de fonction est un calcul effectué à l’exécution, et non une expression constante adaptée à cet initialiseur statique. Le stockage statique lui-même est autorisé dans la fonction ; le problème vient de l’initialiseur. Une variable locale automatique ordinaire peut être initialisée à partir d’un appel, tandis qu’un état conservé nécessite une affectation à l’exécution dans le code exécutable.
Série 2 : Suivre les valeurs dans les appels de fonctions10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Quel est le paramètre dans int triple(int value) { return value * 3; } ?
  1. triple
  2. int
  3. value
  4. value * 3
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Que permet de faire void report(int code) à une fonction appelante ?
  1. Stocker report(4) dans un int.
  2. Appeler report(4); comme instruction.
  3. Ajouter report(4) à un entier.
  4. Renvoyer report(4) depuis une fonction int.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Quelle valeur double portion(int total) { return total / 4; } renvoie-t-elle pour portion(11) ?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Une définition de int capacity(void) apparaît sous main. Quelle déclaration placée au-dessus de main décrit entièrement un appel sans entrée en C17 ?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Avec int fifth(int n) { return n / 5; } déclarée avant son utilisation, que vaut fifth(-14.8) ?
  1. -3
  2. -2
  3. -2.96
  4. -14
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Un fichier d’en-tête déclare double stretch(int n);. Quelle définition est compatible avec cette déclaration ?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Qu’affiche le programme complet suivant ?

#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
    int level = 17;
    lower(level);
    printf("%d\n", level);
}
  1. 17
  2. 13
  3. 4
  4. 0
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Quelle est la valeur finale de x ?

int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
  1. 22
  2. 28
  3. 16
  4. 14
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Que renvoie depth(3) ?

int depth(int n) {
    if (n == 0) return 2;
    return 1 + depth(n - 1);
}
  1. 3
  2. 4
  3. 5
  4. 6
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Quelle valeur int answer = collect(4.9); stocke-t-il ?

int collect(int n) {
    if (n <= 1) return 1;
    return n + collect(n - 2);
}
  1. 7
  2. 8
  3. 6
  4. 5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Série 3 : Développer des macros et choisir des branches à la compilation10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Quel texte #define LIMIT 18 remplace-t-il dans la suite du code source ?
  1. Les caractères LIMIT dans "LIMIT".
  2. Le lexème autonome LIMIT.
  3. Le suffixe du nom LOCAL_LIMIT.
  4. L’instruction entière contenant LIMIT.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Quelle est la valeur de 5 * INC(3) ?

#define INC(x) x + 1
  1. 16
  2. 20
  3. 19
  4. 15
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Pourquoi #define LIMIT 12; rend-il int count = LIMIT * 2; invalide ?
  1. La macro ne peut pas remplacer un lexème après =.
  2. Une macro numérique exige un type de retour déclaré.
  3. Le point-virgule devient une partie de l’expression.
  4. La macro transforme count en nom de constante.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Après cet extrait, quelles sont les valeurs de a et de b ?

#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Quel est le comportement de CUBE(i++) avec cette définition ?

#define CUBE(x) ((x) * (x) * (x))
  1. L’argument est évalué une fois et cette valeur est élevée au cube.
  2. La variable est incrémentée trois fois dans un ordre gauche-droite spécifié.
  3. L’appel est rejeté car une macro ne peut pas recevoir d’expressions.
  4. Le comportement est indéfini à cause de modifications répétées non séquencées.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Avec #define TRACE 0, que fait #ifdef TRACE ?
  1. Ignore sa branche car TRACE se développe en zéro.
  2. Conserve sa branche car TRACE est défini.
  3. Rejette la définition car zéro n’est pas un nom.
  4. Lit une variable d’exécution nommée TRACE.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Pourquoi #if VERBOSE peut-il ignorer silencieusement une branche lorsque la compilation ne définit que VERB0SE ?
  1. L’identificateur non défini VERBOSE compte pour zéro.
  2. Les deux noms partagent une définition car ils se ressemblent.
  3. Tout nom non défini du préprocesseur provoque une erreur fatale de compilation.
  4. La directive attend une valeur de VERBOSE à l’exécution.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Avec -DLEVEL=2, quelle valeur parvient au compilateur pour result ?

#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
  1. 6
  2. 7
  3. 8
  4. 2
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Quel texte ce programme complet affiche-t-il ?

#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
    int item2 = 7;
    PRINT(JOIN(item, 2));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Un fichier d’en-tête muni d’une garde contient int mode = 2;. Deux fichiers source distincts l’incluent, sont compilés, puis liés ensemble. Pourquoi la garde n’empêche-t-elle pas une erreur de définition multiple ?
  1. La seconde source réutilise l’état de la garde de la première.
  2. La garde transforme les deux définitions en déclarations partagées.
  3. Chaque unité de traduction inclut sa propre définition externe.
  4. L’éditeur de liens supprime les noms de garde avant de vérifier les doublons.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Série 4 : Diagnostiquer les fichiers d’en-tête, les fichiers objets et la liaison10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Que produit normalement gcc -std=c17 -c driver.c sur un système de type Unix ?
  1. Un fichier objet nommé driver.o.
  2. Un exécutable nommé driver.
  3. Un texte assembleur nommé driver.s.
  4. Un fichier d’en-tête nommé driver.h.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Un fichier appelant inclut un prototype correct pour reading, mais l’édition de liens de son seul fichier objet signale une référence indéfinie à reading. Que faut-il ajouter ?
  1. Un autre prototype compatible, dans le fichier appelant.
  2. Une garde autour de la déclaration de fonction existante.
  3. Le fichier objet définissant reading, parmi les entrées de l’édition de liens.
  4. Le même fichier d’en-tête, comme entrée compilée séparément.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Un fichier d’en-tête déclare int classify(double x);, mais son fichier source définit double classify(double x). Qu’est-ce qui rend l’incompatibilité visible pendant la compilation du fichier source contenant la définition ?
  1. Ajouter le fichier source contenant la définition à une commande d’édition de liens ultérieure.
  2. Placer un second prototype uniquement dans le fichier source appelant.
  3. Ajouter une garde d’inclusion uniquement autour du code appelant.
  4. Inclure ce même fichier d’en-tête dans le fichier source contenant la définition.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Quelle liaison possède static int cached = 3; lorsqu’il est déclaré en dehors de toute fonction ?
  1. Une liaison externe.
  2. Une liaison interne.
  3. Aucune liaison.
  4. Une liaison automatique.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un fichier d’en-tête muni d’une garde contient static int errors; et void record(void);. Un fichier source l’inclut et définit record pour incrémenter errors ; un autre l’inclut, appelle record deux fois, puis affiche son propre errors. Qu’est-ce qui est affiché et pourquoi ?
  1. 2, car les deux inclusions désignent le même compteur partagé.
  2. 1, car la garde ne permet qu’un seul appel réussi.
  3. 0, car les deux fichiers source ont des compteurs indépendants.
  4. Une erreur d’édition de liens, car les deux compteurs statiques sont externes.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Que faut-il placer dans un fichier d’en-tête pour permettre à plusieurs fichiers source d’utiliser un même int failures défini dans un seul fichier source ?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

main.c inclut "calc.c" ; calc.c inclut un fichier calc.h muni d’une garde et définit la fonction publique int calc(int n). La compilation avec gcc main.c calc.c -o run signale deux définitions de calc. Quelle modification préserve la compilation séparée et vérifie correctement les appels ?
  1. Inclure calc.h dans main.c, en gardant les deux sources dans la compilation.
  2. Retirer calc.c de la compilation et conserver son inclusion dans main.c.
  3. Garder les deux sources et ajouter une autre garde d’inclusion à calc.h.
  4. Inclure main.c dans calc.c pour que les deux définitions concordent.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un fichier source définit static int quota = 9; et int quota_value(void) { return quota; }. Un autre inclut un fichier d’en-tête qui déclare à la fois extern int quota; et int quota_value(void);, puis utilise les deux noms. Quelle correction de l’interface convient tout en gardant le quota stocké privé ?
  1. Supprimer static de la variable et conserver toutes les utilisations de quota.
  2. Ajouter static int quota; au fichier d’en-tête et conserver les lectures directes.
  3. Répéter extern int quota; dans le fichier appelant avant chaque utilisation.
  4. Supprimer la déclaration de variable externe et utiliser quota_value() pour lire sa valeur.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un fichier d’en-tête déclare size_t packet_count(void);. Qu’est-ce qui rend ce fichier d’en-tête autonome ?
  1. Demander à chaque fichier appelant d’inclure <stddef.h> après le fichier d’en-tête.
  2. Déplacer la déclaration dans chaque fichier source avec une copie du nom du type.
  3. Inclure le fichier source de l’implémentation avant le fichier d’en-tête dans les fichiers appelants.
  4. Inclure <stddef.h> avant cette déclaration dans le fichier d’en-tête.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Deux fichiers source incluent chacun un fichier d’en-tête contenant extern int jobs = 1;. L’un incrémente jobs et l’autre l’affiche. Quelle correction crée un seul objet partagé tout en permettant de vérifier les types dans les deux sources ?
  1. Utiliser extern int jobs; dans le fichier d’en-tête et une définition dans un fichier source.
  2. Utiliser static int jobs = 1; dans le fichier d’en-tête sans définition dans un fichier source.
  3. Garder le fichier d’en-tête inchangé et le protéger avec un nouveau nom de macro.
  4. Supprimer le fichier d’en-tête et initialiser jobs dans les deux fichiers source.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Série 5 : Planifier des compilations incrémentales fiables10 Questions Pratiques

Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.

Question 1 sur 10

Qu’est-ce qui rend nécessaire la reconstruction d’une cible make ordinaire existante après la mise à jour de ses dépendances ?
  1. Son fichier source a plus de lignes qu’avant.
  2. Une dépendance a une date de modification plus récente.
  3. Sa recette contient plus d’une commande.
  4. La même commande a été exécutée un jour précédent.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

L’exécutable viewer dépend de main.o et de screen.o ; chaque fichier objet dépend de sa source correspondante. Seul screen.c devient plus récent que son fichier objet et toutes les autres entrées restent inchangées. Quelles recettes sont nécessaires ?
  1. Compiler main.c, puis effectuer l’édition de liens de viewer.
  2. Compiler les deux sources, puis effectuer l’édition de liens de viewer.
  3. Effectuer l’édition de liens de viewer sans compiler les sources.
  4. Compiler screen.c, puis effectuer l’édition de liens de viewer.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Un fichier source inclut settings.h, mais la règle de son fichier objet ne liste que le fichier source. Pourquoi une modification du fichier d’en-tête peut-elle laisser le programme inchangé après make ?
  1. Make ne dispose d’aucun chemin déclaré entre le fichier d’en-tête et le fichier objet.
  2. Les fichiers d’en-tête ne sont toujours traités qu’à l’exécution du programme.
  3. L’éditeur de liens restaure automatiquement l’ancien contenu du fichier d’en-tête.
  4. Une inclusion dans la source empêche son fichier objet de dépendre des fichiers d’en-tête.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Pour la règle d’édition de liens tool: main.o engine.o, en quoi $@ et $^ se développent-ils ?
  1. main.o et engine.o
  2. tool et main.o
  3. tool et main.o engine.o
  4. main.o engine.o et tool
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Pour la règle ci-dessous, quelles entrées et quelle sortie du compilateur sont utilisées lorsque make construit sensor.o ? Suppose que CPPFLAGS = -DTRACE=1 et CFLAGS = -std=c17.

%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
  1. Source sensor.c, sortie sensor.o, avec -DTRACE=1 -std=c17.
  2. Sources sensor.c shared.h, sortie sensor.o, avec -DTRACE=1 -std=c17.
  3. Source sensor.o, sortie sensor.c, avec -DTRACE=1 -std=c17.
  4. Source sensor.c, sortie shared.h, avec -DTRACE=1 -std=c17.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Après une compilation récente, les dates de modification sont dans l’ordre shared.h < main.o < helper.o < app. Les règles sont app: main.o helper.o, main.o: main.c shared.h et helper.o: helper.c. Seul shared.h est ensuite modifié. Les deux sources l’incluent. Que se passe-t-il et quel est le problème ?
  1. Les deux fichiers objets sont reconstruits avant l’édition de liens ; le graphe de dépendances est complet.
  2. Seul helper.o est reconstruit avant l’édition de liens ; il manque une dépendance à main.o.
  3. Aucun fichier objet n’est reconstruit ; les fichiers d’en-tête ne peuvent pas déclencher leurs recettes.
  4. Seul main.o est reconstruit avant l’édition de liens ; il manque une dépendance à helper.o.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Pourquoi un fichier nommé clean peut-il empêcher une règle clean: sans dépendances et non marquée comme factice de supprimer les fichiers objets ?
  1. Make réserve toute cible nommée clean à la compilation prédéfinie.
  2. L’interpréteur refuse les commandes des cibles qui ont un fichier correspondant.
  3. Make peut considérer que cette cible existante est déjà à jour.
  4. Le compilateur supprime les recettes de nettoyage lorsque l’optimisation est activée.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un projet charge des fichiers .d générés après sa règle d’exécutable. main.d indique main.o: main.c a.h ; worker.d indique worker.o: worker.c b.h a.h. Toutes les sorties sont à jour, puis seul a.h est modifié. Que doit montrer make -n ?
  1. Compiler seulement main.c, puis effectuer l’édition de liens ; n’exécuter aucune de ces recettes.
  2. Compiler les deux sources, puis effectuer l’édition de liens ; n’exécuter aucune de ces recettes.
  3. Compiler les deux sources, puis effectuer l’édition de liens ; exécuter les trois recettes.
  4. Effectuer seulement l’édition de liens ; n’exécuter aucune recette car les fichiers de dépendances existent déjà.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un projet contient app: main.o work.o, main.o: main.c shared.h et work.o: work.c. Toutes les sorties sont à jour. CFLAGS passe de -O0 à -O2, puis seul shared.h est modifié. Les deux recettes utilisent CFLAGS. Que fait make en exécution ordinaire et qu’est-ce qui garantit l’application cohérente des nouvelles options ?
  1. Reconstruire les deux fichiers objets et effectuer l’édition de liens ; aucune autre reconstruction n’est nécessaire pour les nouvelles options.
  2. Reconstruire main.o et effectuer l’édition de liens ; forcer une reconstruction complète pour mettre aussi work.o à jour.
  3. Refaire seulement l’édition de liens et garder les deux fichiers objets ; modifier à nouveau le fichier d’en-tête pour appliquer les options.
  4. Reconstruire work.o et effectuer l’édition de liens ; forcer une reconstruction complète pour mettre aussi main.o à jour.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

app dépend de main.o et de codec.o. Chaque fichier objet possède une règle complète de dépendances aux fichiers source et d’en-tête ; seul codec.h est partagé par les deux sources. Après modification de ce fichier d’en-tête, pourquoi make -j4 app peut-il compiler simultanément tout en effectuant correctement l’édition de liens ?
  1. L’édition de liens peut commencer dès qu’un fichier objet est terminé et réutiliser l’autre ancien fichier objet.
  2. Chaque fichier objet attend l’autre, puis l’édition de liens commence avec les deux anciens fichiers.
  3. Les deux recettes des fichiers objets peuvent s’exécuter ensemble ; l’édition de liens attend leur fin.
  4. L’édition de liens s’exécute d’abord pour réserver app, puis les deux recettes de fichiers objets le remplacent.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Tu bloques sur ton propre exercice ? Aide aux Devoirs

Mots-Clés

68 Mots-Clés, chacun avec sa définition

A

Absence de liaison
L’absence de liaison signifie qu’un nom identifie sa propre entité au lieu de relier des déclarations par une liaison interne ou externe.
Les variables locales automatiques ordinaires et les paramètres n’ont pas de liaison, même lorsqu’une autre fonction utilise les mêmes noms.
Argument
Un argument est une expression fournie par un appel pour donner sa valeur initiale à un paramètre.
Dans cube(5 + 1), l’argument est évalué à 6 avant le début du corps de cube.
auto
Le mot-clé auto indique explicitement un stockage automatique en C17 ; C23 l’utilise aussi pour déduire un type à partir d’un initialiseur.
En C17, auto int level; a le même comportement de stockage qu’une variable locale ordinaire int level;.

C

Cadre de pile
Un cadre de pile est un enregistrement pour un appel actif, qui contient généralement ses paramètres, ses variables locales automatiques et son point de retour.
Au point le plus profond de factorial(4), quatre appels de la fonction factorielle possèdent des cadres actifs distincts.
Cas de base
Un cas de base répond directement à une valeur d’entrée sans effectuer de nouvel appel récursif.
Le cas de base de la factorielle renvoie 1, ce qui permet aux multiplications en attente des appels précédents de s’achever.
Cas récursif
Un cas récursif est une branche qui résout une partie d’un problème en appelant la même fonction sur une valeur d’entrée plus proche d’un cas de base.
Soustraire un à une valeur d’entrée positive de la factorielle rapproche le cas récursif de son test d’arrêt.
Cible
Une cible est le fichier de sortie ou l’action que make doit mettre à jour.
La cible de l’exécutable est généralement placée en premier afin qu’une exécution de make sans argument construise tout le programme.
Cible factice
Une cible factice désigne une action déclarée avec .PHONY, afin qu’un fichier existant de ce nom ne puisse pas empêcher l’exécution de sa recette.
Déclarer clean comme cible factice garantit que le nettoyage s’exécute même si un fichier nommé clean apparaît dans le répertoire.
Cible par défaut
La cible par défaut est la cible que make choisit sans argument indiquant explicitement une cible, normalement la première cible ordinaire du fichier.
Placer all ou l’exécutable final en premier empêche une exécution ordinaire de make de s’arrêter à un seul fichier objet.
Compilation conditionnelle
La compilation conditionnelle conserve ou ignore des branches du code source selon des tests de prétraitement, avant la compilation du code obtenu.
Un affichage de débogage supprimé par compilation conditionnelle est absent de l’exécutable obtenu.
Concaténation de lexèmes
La concaténation de lexèmes utilise ## dans une liste de remplacement de macro pour combiner des lexèmes voisins en un seul lexème de prétraitement.
Un appel de macro qui réunit temp et 2 peut former l’identificateur temp2 dans le code source développé.
Conversion d’argument
Une conversion d’argument transforme la valeur d’un argument fourni dans le type du paramètre précisé par un prototype de fonction visible.
Passer 5.8 à un paramètre int le convertit en 5 ; la valeur n’est pas arrondie à l’entier le plus proche.
Conversion en chaîne de caractères
La conversion en chaîne de caractères utilise # devant un paramètre de macro dans le texte de remplacement pour transformer les lexèmes de l’argument fourni en littéral de chaîne de caractères.
Une macro de diagnostic peut convertir 3 + 4 en chaîne pour afficher l’expression à côté de sa valeur calculée.

D

Déclaration
Une déclaration introduit un nom et les informations de type nécessaires pour vérifier ses utilisations.
Une déclaration au-dessus de main permet de vérifier un appel même si le corps de la fonction vient plus loin.
Définition de fonction
Une définition de fonction fournit le corps exécutable d’une fonction ainsi que son nom, son type de retour et sa liste de paramètres.
La définition de double scale(double factor) doit concorder avec le prototype inclus dans le fichier appelant et dans l’implémentation.
Définition multiple
Une erreur de définition multiple se produit lorsque l’édition de liens reçoit des définitions incompatibles d’un même nom à liaison externe.
Deux fichiers source liés qui définissent tous deux main provoquent une erreur de définition multiple ou de symbole dupliqué.
Définition provisoire
Une définition provisoire est une déclaration d’objet à la portée de fichier, sans initialiseur ni extern, qui peut fournir une définition.
Placer int count; dans un fichier d’en-tête partagé n’est pas un remplacement portable pour une déclaration extern.
Dépendance
Une dépendance est une entrée ou une dépendance déclarée que make vérifie et met à jour avant de décider si une cible doit être reconstruite.
Un fichier d’en-tête partagé est une dépendance de chaque fichier objet dont l’unité de traduction l’inclut.
Dépendance cachée
Une dépendance cachée est une entrée ou un état nécessaire qui influe sur une fonction sans apparaître dans sa liste de paramètres.
Pour tester un calcul qui lit une limite globale, il faut préparer cette limite au préalable.
Directive de prétraitement
Une directive de prétraitement commence par # et contrôle la transformation du texte source.
La directive #include "sensor.h" insère le texte du fichier d’en-tête dans le prétraitement en cours.
Durée de stockage
La durée de stockage décrit combien de temps le stockage d’un objet existe pendant l’exécution, indépendamment de l’endroit où son nom est visible.
Une variable locale statique combine une portée de bloc et une durée de stockage qui couvre toute l’exécution du programme.
Durée de stockage automatique
La durée de stockage automatique donne aux variables locales ordinaires et aux paramètres un stockage lié à l’exécution de leur bloc ou à leur appel de fonction.
Deux appels récursifs actifs possèdent des paramètres automatiques distincts même si leurs noms sont identiques.
Durée de stockage statique
La durée de stockage statique signifie qu’un objet existe pendant toute l’exécution du programme et est initialisé une seule fois avant son démarrage.
Un objet numérique de portée de fichier sans initialiseur explicite vaut initialement zéro et conserve les valeurs qui lui sont affectées ensuite.

E

Effet de bord
Un effet de bord est une modification de l’état du programme ou une interaction telle qu’un affichage, qui va au-delà du simple calcul de la valeur d’une expression.
Incrémenter un compteur partagé est un effet de bord : l’ordre d’appels distincts au compteur peut donc compter.
En-tête autonome
Un en-tête autonome inclut les fichiers d’en-tête nécessaires à ses propres déclarations, ce qui rend inutile un ordre d’inclusion particulier.
Un fichier d’en-tête qui déclare une fonction utilisant size_t inclut <stddef.h> avant cette déclaration.
extern
Le mot-clé extern peut déclarer une entité dotée d’une liaison sans définir d’objet, à condition qu’aucun initialiseur n’en fasse une définition.
Un fichier d’en-tête partagé déclare extern int errors;, tandis qu’un fichier source fournit la définition initialisée de l’objet.

É

Éditeur de liens
Un éditeur de liens combine des fichiers objets et des bibliothèques et résout les noms externes en les reliant à leurs définitions.
L’éditeur de liens a besoin du fichier objet qui définit une fonction de conversion même si le fichier appelant a inclus son prototype.
État partagé
Un état partagé est un ensemble de données conservées que plusieurs opérations peuvent lire ou modifier.
Un nombre d’erreurs partagé fait dépendre un rapport ultérieur des fonctions qui l’ont mis à jour auparavant.

F

Fichier d’en-tête
Un fichier d’en-tête partage des déclarations, des macros ou des types par inclusion de texte.
Un fichier d’en-tête de conversion déclare les deux sens de conversion sans fournir leurs corps compilés.
Fichier de dépendances généré
Un fichier de dépendances généré enregistre les dépendances aux fichiers source et d’en-tête découvertes par le compilateur, afin que make les charge lors des compilations ultérieures.
Compiler avec -MMD peut écrire sensor.d, qui provoque la reconstruction de sensor.o lors d’une modification ultérieure d’un fichier d’en-tête.
Fichier objet
Un fichier objet contient du code machine compilé et des informations sur les symboles que l’éditeur de liens peut combiner avec d’autres fichiers objets.
Après avoir modifié uniquement sensor.c, la compilation peut remplacer sensor.o tout en réutilisant un main.o inchangé.
Fonction
Une fonction est un bloc nommé de code exécutable qui accomplit une tâche lorsqu’il est appelé.
Une fonction de conversion de température permet à plusieurs parties d’un programme de réutiliser un calcul testé.
Fonction appelante
Une fonction appelante est la fonction qui en appelle une autre et reprend son exécution après son retour.
Lorsque main appelle read_sensor, main est la fonction appelante qui attend de recevoir la mesure renvoyée.

G

Garde d’inclusion
Une garde d’inclusion empêche le traitement répété d’un fichier d’en-tête dans une même unité de traduction.
Deux fichiers d’en-tête peuvent inclure un fichier d’en-tête commun muni d’une garde sans répéter son contenu dans la même unité.

L

Liaison
La liaison détermine si les déclarations d’un nom à différents endroits désignent le même objet ou la même fonction.
Une variable globale peut avoir une durée de stockage statique tandis que static limite sa liaison à une seule unité de traduction.
Liaison externe
La liaison externe permet à des déclarations dans différentes unités de traduction d’identifier le même objet ou la même fonction.
Une fonction publique de compteur a une liaison externe pour que les fichiers appelants d’une autre source puissent être liés à sa définition.
Liaison interne
La liaison interne limite l’identité partagée d’un nom à une seule unité de traduction, ce qui rend son entité privée aux autres unités.
Deux fichiers peuvent chacun définir une fonction auxiliaire statique nommée check sans conflit lors de l’édition de liens.

M

Macro fonction
Une macro fonction reçoit des arguments constitués de lexèmes et les substitue dans un texte de remplacement avant la compilation C ordinaire.
Les parenthèses d’une macro fonction préservent le regroupement, mais n’empêchent pas l’évaluation répétée des arguments.
Macro objet
Une macro objet associe un nom à des lexèmes de remplacement qui lui sont substitués lorsque ce nom apparaît ensuite.
La définition #define LIMIT 16 remplace le lexème LIMIT, mais laisse "LIMIT" inchangé.
Macro prédéfinie
Une macro prédéfinie est fournie par l’implémentation et donne des informations telles que la ligne source courante ou la version du langage.
Un diagnostic utilisant __FILE__ et __LINE__ peut indiquer l’origine de son message.
Masquage
Le masquage se produit lorsqu’une déclaration intérieure cache un nom extérieur visible, si bien que les utilisations dans la portée intérieure désignent la nouvelle entité.
Déclarer un second total dans une boucle peut laisser inchangé l’accumulateur extérieur visé.
Module
Un module regroupe du code d’implémentation C lié dans un fichier source accompagné d’un fichier d’en-tête correspondant.
Un module de compteur peut exposer deux fonctions publiques tout en gardant son compteur stocké privé.

O

Opérateur defined
L’opérateur de prétraitement defined vaut un si son opérande, un nom de macro, est défini et zéro sinon.
Utilise #if defined(DEBUG) pour tester l’existence de la macro même lorsque DEBUG a pour texte de remplacement 0.
Ordre non spécifié d’évaluation des arguments
L’ordre non spécifié d’évaluation des arguments signifie que C n’impose pas d’ordre d’évaluation des expressions des arguments d’un même appel.
Deux appels générant des tickets dans un même affichage peuvent fournir leurs numéros dans un ordre ou dans l’autre.

P

Paramètre
Un paramètre est une variable de fonction initialisée à l’entrée avec une copie de la valeur d’un argument.
Dans int cube(int n), le paramètre n reçoit une valeur d’entrée distincte pour chaque appel.
Passage par valeur
Le passage par valeur est la règle de transmission des arguments en C : chaque paramètre reçoit une copie de la valeur calculée par l’expression de son argument.
Une affectation au paramètre n dans une fonction laisse inchangée la variable entière ordinaire de la fonction appelante.
Pile d’appels
Une pile d’appels est la structure habituellement utilisée par les implémentations pour suivre les appels de fonctions actifs et leur permettre de revenir dans l’ordre inverse de leur imbrication.
Une fonction de conversion appelée par une fonction qui affiche un tableau revient avant que cette dernière revienne à main.
Portée
La portée d’un nom est la partie du code source où sa déclaration est visible.
Une variable peut encore exister alors que son nom est masqué par une autre déclaration dans un bloc imbriqué.
Portée de bloc
La portée de bloc limite un nom local à la partie comprise entre le point situé juste après son déclarateur et la fin du bloc qui le contient.
Une variable déclarée dans un bloc if ne peut pas être nommée par un affichage après la fermeture de ce bloc.
Portée de fichier
La portée de fichier s’étend d’une déclaration extérieure aux fonctions jusqu’à la fin de l’unité de traduction.
Une fonction auxiliaire définie au-dessus d’une variable globale a besoin d’une déclaration antérieure avant de pouvoir l’utiliser.
Portée de prototype
La portée de prototype limite un nom de paramètre au prototype de fonction qui le déclare.
Un prototype peut nommer son paramètre value tandis que la définition nomme le même paramètre n.
Préprocesseur
Le préprocesseur transforme le texte source à l’aide de directives telles que l’inclusion, le remplacement de macros et la sélection conditionnelle.
Exécute gcc -E sample.c pour examiner le code développé avant que le compilateur vérifie les expressions C.
Prototype de fonction
Un prototype de fonction est une déclaration qui précise le type de retour et les types des paramètres d’une fonction pour vérifier les appels.
Le prototype double scale(double factor); indique au compilateur de convertir son argument en double.

R

Recette
Une recette est la suite de commandes de l’interpréteur attachée à une règle make, dont les lignes ordinaires commencent par de véritables tabulations.
La recette d’un fichier objet compile sa source, tandis que celle de l’exécutable effectue l’édition de liens des fichiers objets nécessaires.
Récursivité
La récursivité se produit lorsqu’une fonction s’appelle elle-même pour résoudre une autre instance d’un problème.
Une fonction de somme des chiffres peut traiter le dernier chiffre et additionner récursivement les chiffres restants.
Référence indéfinie
Une référence indéfinie est un échec de l’édition de liens dans lequel un nom externe nécessaire n’a pas de définition disponible parmi les entrées liées.
Omettre sensor.o de l’édition de liens peut laisser une référence indéfinie à une fonction publique de lecture que seul sensor.o définit.
register
Le mot-clé register associe à une variable locale une indication d’optimisation que les compilateurs peuvent ignorer et interdit de prendre son adresse.
Ajouter register à un compteur de boucle ne force pas un compilateur moderne à utiliser un registre du processeur.
Règle
Une règle make indique une cible, ses dépendances et les éventuelles commandes de recette nécessaires pour mettre cette cible à jour à partir de ces entrées.
La règle de main.o indique à la fois main.c et les fichiers d’en-tête du projet inclus pendant sa compilation.
Règle de motif
Une règle de motif utilise % pour trouver un radical commun dans les noms de fichiers, afin qu’un même motif de dépendances et une même recette puissent construire plusieurs cibles.
La règle %.o: %.c associe sensor.o à sensor.c en utilisant sensor comme radical commun.

S

Simulation d’exécution
Une simulation d’exécution avec make -n affiche les recettes ordinaires de compilation sans les exécuter, ce qui montre les commandes prévues du compilateur et de l’éditeur de liens.
Examine une simulation d’exécution après avoir modifié un fichier d’en-tête pour voir quels fichiers objets make prévoit de reconstruire.

U

Unité de traduction
Une unité de traduction est un fichier source après prétraitement, avec le texte qu’il inclut, traduit indépendamment.
Inclure le même fichier d’en-tête dans deux fichiers source apporte des déclarations à deux unités de traduction distinctes.

V

Valeur de retour
Une valeur de retour est le résultat qu’une fonction fournit à la fonction appelante pour qu’elle puisse l’utiliser dans une expression.
La valeur de retour d’une conversion peut être affectée à double result sans être affichée.
Variable automatique
Une variable automatique est définie par make pour la règle courante et fournit des valeurs telles que les noms de sa cible ou de ses dépendances.
La variable automatique $@ désigne sensor.o pendant l’exécution de sa recette de fichier objet.
Variable de make
Une variable de make nomme un texte réutilisable, tel que des paramètres du compilateur ou des listes de fichiers objets, développé par une référence comme $(CFLAGS).
Modifier une seule affectation de CC change le texte du compilateur utilisé par les recettes qui font référence à $(CC).
Variable globale
Une variable globale est un objet déclaré à la portée de fichier, doté d’une durée de stockage statique et visible sous sa déclaration.
Deux fonctions qui ajoutent des trajets peuvent mettre à jour un même total global de distance qui reste disponible pour main.
Variable locale statique
Une variable locale statique est un objet de portée de bloc déclaré static, qui conserve une même valeur stockée entre des appels répétés.
Un compteur local statique de tickets progresse à chaque appel au lieu de repartir de sa valeur initiale.
void
Le mot-clé void indique l’absence de valeur renvoyée comme type de retour, ou l’absence de paramètres lorsqu’il est utilisé seul dans une liste de paramètres.
Une fonction d’affichage void accomplit sa tâche de sortie, mais ne peut pas fournir le côté droit d’une affectation.

#

#error
La directive #error impose l’émission d’un diagnostic lorsqu’elle est atteinte pendant le prétraitement, ce qui permet de rejeter une compilation non prise en charge.
Un test de version peut sélectionner #error lorsque la norme C choisie est plus ancienne que celle requise par le programme.

À propos de cette unité

Par StudeliSignaler une erreur

Crée ton propre Matériel d'Étude

Transforme tes notes, diapos et documents en Guides d'Étude, Questions Pratiques, Flashcards et 6 autres outils.

Commencer