- Apprendre
- Informatique et numérique
- Programmation
- Langage C : pointeurs, mémoire et structures
- Fonctions et structure des programmes en C
Fonctions et structure des programmes en C
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
- 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
returnetvoid, et appelle des fonctions dans des expressions. - Déclarer des fonctions à l’aide de prototypesDé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ésultatExplique 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.
- Définir des fonctions qui reçoivent des données et renvoient un résultat
- 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 stockageDistingue 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 globalesDé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é.
- Déterminer où un nom peut être utilisé (portée)
- 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 macrosExplique 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 conditionUtilise #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.
- Inclure des fichiers et définir des macros
- 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 liensDé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êtePlace 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 externUtilise 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.
- Compiler plusieurs fichiers source et effectuer l’édition de liens
- É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 motifUtilise des variables comme
CCetCFLAGS, 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.
- Écrire les règles et les dépendances d’un fichier makefile
Fonctions et structure des programmes en C : cours
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.
returntermine l’appel ; l’affichage ne fournit pas son résultat.- Utilise
voidpour 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éclaration | Sens en C17 | Sens en C23 |
|---|---|---|
int read_sensor(void); | Aucun paramètre | Aucun paramètre |
int read_sensor(); | Informations sur les paramètres non spécifiées | Aucun 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 appelante | Effet 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 actif | Calcul 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.
2Maî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écificateur | Sens dans ce code C17 |
|---|---|
auto | Stockage automatique explicite pour une variable de bloc ; généralement omis |
register | Indication pour un accès rapide ; il est interdit de prendre son adresse |
static dans une fonction | Stockage conservé avec un nom local |
static à la portée de fichier | Liaison interne, qui garde le nom dans son unité de traduction |
extern | Dé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
staticdé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.
3Utiliser 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
| Nom | Information |
|---|---|
__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
#ifdefteste l’existence ;#ifteste une valeur.- Les identificateurs non définis dans
#ifsont évalués à zéro. -DNAME=valuefournit 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.
4Ré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.
| Étape | Résultat | Option GCC qui arrête le processus à cette étape |
|---|---|---|
| Prétraitement | Texte C développé | -E |
| Compilation | Texte assembleur | -S |
| Assemblage | Fichier objet | -c |
| Édition de liens | Exécutable | Compilation 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.
-cproduit 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 fonctions | Corps de fonctions à liaison externe |
Déclarations de variables extern | Définitions d’objets partagés |
| Macros et types nécessaires | Fonctions 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
| Liaison | Sens |
|---|---|
| Externe | Des déclarations peuvent désigner une même entité dans plusieurs unités de traduction |
| Interne | Les déclarations désignent une entité dans une seule unité de traduction |
| Absente | Un 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
externet une seule définition. - À la portée de fichier,
staticrend 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
externde portée de fichier en définition. - La conservation d’une variable locale par
staticet la liaison donnée parstaticà la portée de fichier ont des rôles différents.
5Compiler 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.cuniquement : le recompiler et refaire l’édition de liens : deux commandes ; réutilisermain.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
| Variable | Valeur 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
.dgé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
1Construire et suivre un appel de fonction
19 étapes
- 1IntroductionCommencer 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.
- 2ExempleDonner à 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; }intavant le nom décrit le résultat.int readingdé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. - 3ExempleUtiliser un appel comme expression
Pour
int result = corrected(9);, l’argument9initialise le paramètrereading. L’expression de retour produit13et l’affectation dans la fonction appelante stocke cette valeur dansresult.L’appel peut aussi se trouver dans une autre expression :
2 * corrected(9)donne26. Écrire seulementcorrected(9);exécute tout de même l’appel, mais ignore sa valeur de retour. - 4ConseilChoisir 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
voidpeut 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 unreturnapproprié ; afficher un nombre ne le fournit pas à la fonction appelante. - 5Dé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 donne13, la division entière donne6et la conversion du retour donne6.0. Utilise2.0pour effectuer la division en virgule flottante si tu as besoin de6.5. Le type de retour ne peut pas rétablir une partie fractionnaire déjà supprimée. - 6ExplicationInformer 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. - 7Point 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. - 8DéfiSuivre une conversion autorisée
Le compilateur a rencontré
int corrected(int reading);. Que renvoiecorrected(6.8)et l’appel doit-il être rejeté ?---reveal---
L’appel peut être compilé. Le prototype entraîne la conversion de
6.8en entier6, puis la fonction ajoute4et renvoie10. 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. - 9ExempleRepré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,sampleet le paramètre contiennent chacun9dans des objets distincts. Le corps donne au paramètre la valeur13. Au retour de l’appel,samplecontient toujours9: aucune affectation ne l’a modifié. - 10LienLaisser 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 metsampleà jour. - 11Dé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
sampleensuite ?---reveal---
Le premier appel calcule
11et l’ignore, laissantsampleà7. Le second reçoit lui aussi7, renvoie11et, cette fois, la fonction appelante le stocke. La valeur finale est11, pas15; un appel ne met pas à jour sa variable argument simplement en renvoyant une valeur modifiée. - 12ExplicationAjouter 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.
- 13ExempleÉcrire les appels à la descente
Suis
total_to(3). Le premier appel contientn == 3et attend de pouvoir ajouter3àtotal_to(2). Cet appel contient son propren == 2et attendtotal_to(1).L’appel avec
n == 1attendtotal_to(0), qui renvoie immédiatement0. À ce point le plus profond, quatre appels sont actifs. Ne remplace pas les valeurs précédentes denpar zéro ; elles appartiennent à des appels différents. - 14ExempleRenseigner les résultats à la remontée
Une fois que
total_to(0)renvoie0, les calculs en attente peuvent s’achever :- L’appel avec
n == 1renvoie1 + 0, soit1. - L’appel avec
n == 2renvoie2 + 1, soit3. - L’appel avec
n == 3renvoie3 + 3, soit6.
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é.
- L’appel avec
- 15DéfiVérifier si un cas de base est atteignable
Supposons que le test de base de
total_tosoit remplacé parn == 0et que l’étape récursive utilise toujoursn - 1. Quel problème survient lorsque l’argument initial est-2?---reveal---
Les valeurs d’entrée deviennent
-2,-3,-4et 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 avecn <= 0arrête immédiatement cet appel. - 16ConseilVé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
intde 32 bits,factorial(12)est représentable, tandis que la valeur mathématique defactorial(13)dépasse le maximum2147483647.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.
- 17ConseilSé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 modifieisans le séquencement nécessaire. Le passage par valeur ne rend pas sûre une expression d’argument qui ne l’est pas. - 18DéfiCombiner déclaration, conversion et appels
Avec
total_todéclarée avant son utilisation, une fonction appelante écritint x = 2; total_to(x); x = total_to(3.9);. Quelle est la valeur finale dex?---reveal---
Le premier résultat est ignoré, donc
xreste à2. Le second argument est converti en3; le suivi récursif renvoie6, que l’affectation stocke dansx. La déclaration contrôle la conversion, la récursivité calcule la valeur et la fonction appelante décide de la conserver ou non. - 19Ré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.
2Suivre les noms et les valeurs conservées
13 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionCommencer par une petite tâche
- 2ExempleLire la déclaration visible la plus proche
- 3ExplicationConserver la portée à l’endroit où la fonction est écrite
- 4DéfiCorriger une valeur nécessaire après un bloc
- 5ExempleObserver le redémarrage d’un compteur automatique
- 6ExempleConserver le compteur avec static
- 7DéfiDistinguer initialisation et affectation
- 8ConseilInterpréter le mot-clé selon son emplacement
- 9ExempleSuivre la mise à jour d’une variable globale
- 10DéfiRepérer la variable locale qui masque la variable globale
- 11LienRendre un calcul plus facile à tester
- 12DéfiCombiner portée et état mémorisé
- 13RécapitulatifAnnoter séparément les noms et les durées de vie
3Examiner le code avant la compilation
13 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionCommencer par une petite tâche
- 2ExplicationVoir ce que fournit réellement l’inclusion
- 3ExempleRemplacer les lexèmes avant d’appliquer les règles des opérateurs
- 4ExempleProtéger les regroupements intérieur et extérieur
- 5DéfiRepérer ce que les parenthèses ne peuvent pas corriger
- 6ConseilDistinguer les règles des macros de celles de la portée
- 7ExempleTester l’existence et la valeur d’un réglage
- 8DéfiSélectionner une branche avec une option du compilateur
- 9ExempleSuivre la première inclusion d’un fichier d’en-tête
- 10Point cléRepartir d’un état de garde vierge dans une autre unité
- 11LienSituer un diagnostic dans le code source
- 12DéfiCombiner développement et sélection de branche
- 13RécapitulatifExaminer la transformation dans l’ordre
4Construire un programme à partir de deux sources
14 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionCommencer par une petite tâche
- 2ExplicationSuivre une unité de traduction jusqu’au fichier objet
- 3ExempleÉcrire la déclaration partagée
- 4ExemplePlacer l’implémentation dans son propre fichier source
- 5DéfiPlacer correctement une variable partagée
- 6ExempleCompiler séparément, puis effectuer l’édition de liens
- 7DéfiCorriger une référence indéfinie
- 8ConseilNe pas copier l’implémentation dans le fichier appelant
- 9ExempleGarder un compteur dans le module
- 10ExplicationDistinguer trois propriétés du compteur
- 11DéfiExpliquer deux compteurs indépendants
- 12ConseilRendre un fichier d’en-tête autonome
- 13DéfiCorriger ensemble la confidentialité et l’interface
- 14RécapitulatifAssocier chaque besoin à son étape de compilation
5Ne reconstruire que ce qui a changé
15 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionCommencer par une petite tâche
- 2ExempleEnregistrer les entrées de l’exécutable et des fichiers objets
- 3ExempleDonner une recette à un fichier objet
- 4ExempleEffectuer une première compilation puis une reconstruction après modification d’un fichier source
- 5DéfiSuivre la modification d’un fichier d’en-tête partagé
- 6ConseilExaminer les opérations prévues avant de les exécuter
- 7ExplicationRemplacer les paramètres répétés par des variables
- 8ExempleDévelopper la recette d’édition de liens
- 9ExempleUtiliser un même motif de compilation pour les deux fichiers objets
- 10DéfiChoisir une source sans compiler le fichier d’en-tête
- 11ConseilFaire du nettoyage une action
- 12LienLaisser le compilateur enregistrer les dépendances aux fichiers d’en-tête
- 13DéfiGarantir une reconstruction parallèle correcte
- 14DéfiAppliquer les options modifiées à tout le programme
- 15RécapitulatifVérifier le graphe avant de se fier au résultat
Fonctions et structure des programmes en C : QCM
Série 1 : Suivre les noms locaux et l’état mémorisé10 Questions Pratiques
Question 1 sur 10
i de for (int i = 0; i < 6; i++) { } peut-il être utilisé ?Afficher la réponseMasquer la réponse
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);
}
Afficher la réponseMasquer la réponse
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);
Afficher la réponseMasquer la réponse
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
int amount = amount + 2; dans un bloc ne s’initialise-t-il pas à partir d’un amount extérieur ?Afficher la réponseMasquer la réponse
Question 5 sur 10
static int calls = 0; est-il initialisé pendant une exécution du programme ?Afficher la réponseMasquer la réponse
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;
}
Afficher la réponseMasquer la réponse
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;
}
Afficher la réponseMasquer la réponse
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
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 ?Afficher la réponseMasquer la réponse
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);
}
Afficher la réponseMasquer la réponse
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
read_sensor(). Pourquoi static int first = read_sensor(); est-il invalide en C17 ?Afficher la réponseMasquer la réponse
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
int triple(int value) { return value * 3; } ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
void report(int code) à une fonction appelante ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
double portion(int total) { return total / 4; } renvoie-t-elle pour portion(11) ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
int fifth(int n) { return n / 5; } déclarée avant son utilisation, que vaut fifth(-14.8) ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
double stretch(int n);. Quelle définition est compatible avec cette déclaration ?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);
}
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);
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);
}
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);
}
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
#define LIMIT 18 remplace-t-il dans la suite du code source ?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
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
#define LIMIT 12; rend-il int count = LIMIT * 2; invalide ?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);
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))
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
#define TRACE 0, que fait #ifdef TRACE ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
#if VERBOSE peut-il ignorer silencieusement une branche lorsque la compilation ne définit que VERB0SE ?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
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));
}
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
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 ?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
gcc -std=c17 -c driver.c sur un système de type Unix ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
reading, mais l’édition de liens de son seul fichier objet signale une référence indéfinie à reading. Que faut-il ajouter ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
static int cached = 3; lorsqu’il est déclaré en dehors de toute fonction ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
int failures défini dans un seul fichier source ?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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
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é ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
size_t packet_count(void);. Qu’est-ce qui rend ce fichier d’en-tête autonome ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
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 ?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
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
tool: main.o engine.o, en quoi $@ et $^ se développent-ils ?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 $@
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
clean peut-il empêcher une règle clean: sans dépendances et non marquée comme factice de supprimer les fichiers objets ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
.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 ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
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 ?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 ?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
A
Absence de liaison
Argument
cube(5 + 1), l’argument est évalué à 6 avant le début du corps de cube.auto
auto indique explicitement un stockage automatique en C17 ; C23 l’utilise aussi pour déduire un type à partir d’un initialiseur.auto int level; a le même comportement de stockage qu’une variable locale ordinaire int level;.C
Cadre de pile
factorial(4), quatre appels de la fonction factorielle possèdent des cadres actifs distincts.Cas de base
1, ce qui permet aux multiplications en attente des appels précédents de s’achever.Cas récursif
Cible
Cible factice
.PHONY, afin qu’un fichier existant de ce nom ne puisse pas empêcher l’exécution de sa recette.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
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
Concaténation de lexèmes
## dans une liste de remplacement de macro pour combiner des lexèmes voisins en un seul lexème de prétraitement.temp et 2 peut former l’identificateur temp2 dans le code source développé.Conversion d’argument
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
# 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.3 + 4 en chaîne pour afficher l’expression à côté de sa valeur calculée.D
Déclaration
main permet de vérifier un appel même si le corps de la fonction vient plus loin.Définition de fonction
double scale(double factor) doit concorder avec le prototype inclus dans le fichier appelant et dans l’implémentation.Définition multiple
main provoquent une erreur de définition multiple ou de symbole dupliqué.Définition provisoire
extern, qui peut fournir une définition.int count; dans un fichier d’en-tête partagé n’est pas un remplacement portable pour une déclaration extern.Dépendance
Dépendance cachée
Directive de prétraitement
# et contrôle la transformation du texte source.#include "sensor.h" insère le texte du fichier d’en-tête dans le prétraitement en cours.Durée de stockage
Durée de stockage automatique
Durée de stockage statique
E
Effet de bord
En-tête autonome
size_t inclut <stddef.h> avant cette déclaration.extern
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.extern int errors;, tandis qu’un fichier source fournit la définition initialisée de l’objet.É
Éditeur de liens
État partagé
F
Fichier d’en-tête
Fichier de dépendances généré
-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
sensor.c, la compilation peut remplacer sensor.o tout en réutilisant un main.o inchangé.Fonction
Fonction appelante
main appelle read_sensor, main est la fonction appelante qui attend de recevoir la mesure renvoyée.G
Garde d’inclusion
L
Liaison
static limite sa liaison à une seule unité de traduction.Liaison externe
Liaison interne
check sans conflit lors de l’édition de liens.M
Macro fonction
Macro objet
#define LIMIT 16 remplace le lexème LIMIT, mais laisse "LIMIT" inchangé.Macro prédéfinie
__FILE__ et __LINE__ peut indiquer l’origine de son message.Masquage
total dans une boucle peut laisser inchangé l’accumulateur extérieur visé.Module
O
Opérateur defined
defined vaut un si son opérande, un nom de macro, est défini et zéro sinon.#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
P
Paramètre
int cube(int n), le paramètre n reçoit une valeur d’entrée distincte pour chaque appel.Passage par valeur
n dans une fonction laisse inchangée la variable entière ordinaire de la fonction appelante.Pile d’appels
main.Portée
Portée de bloc
if ne peut pas être nommée par un affichage après la fermeture de ce bloc.Portée de fichier
Portée de prototype
value tandis que la définition nomme le même paramètre n.Préprocesseur
gcc -E sample.c pour examiner le code développé avant que le compilateur vérifie les expressions C.Prototype de fonction
double scale(double factor); indique au compilateur de convertir son argument en double.R
Recette
Récursivité
Référence indéfinie
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
register associe à une variable locale une indication d’optimisation que les compilateurs peuvent ignorer et interdit de prendre son adresse.register à un compteur de boucle ne force pas un compilateur moderne à utiliser un registre du processeur.Règle
main.o indique à la fois main.c et les fichiers d’en-tête du projet inclus pendant sa compilation.Règle de motif
% 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.%.o: %.c associe sensor.o à sensor.c en utilisant sensor comme radical commun.S
Simulation d’exécution
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.U
Unité de traduction
V
Valeur de retour
double result sans être affichée.Variable automatique
$@ désigne sensor.o pendant l’exécution de sa recette de fichier objet.Variable de make
$(CFLAGS).CC change le texte du compilateur utilisé par les recettes qui font référence à $(CC).Variable globale
main.Variable locale statique
static, qui conserve une même valeur stockée entre des appels répétés.void
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.void accomplit sa tâche de sortie, mais ne peut pas fournir le côté droit d’une affectation.#
#error
#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.#error lorsque la norme C choisie est plus ancienne que celle requise par le programme.- 1Types et opérateurs en C
- 2Fonctions et structure des programmes en C
- 3Tableaux et chaînes de caractères en C
- 4Pointeurs en C
- 5Gestion dynamique de la mémoire en C
- 6Structures et listes chaînées en C
- 7Entrées et sorties de fichiers en C
- 8Débogage en C
À propos de cette unité
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.