Compilation
Fichiers source, header et main¶
Un fichier dont l'extension est .c
est un fichier source contenant du code C. Une bonne pratique est de séparer son code en plusieurs fichiers, plutôt que de tout mettre dans un seul énorme fichier.
Une autre bonne pratique et de séparer l'implémentation/définition des fonctions (dans un fichier .c) de leurs déclarations/prototypes (dans un fichier header .h).
Pour pouvoir utiliser une fonction définie dans un autre fichier, il faut importer le header correspondant avec #include ...
.
Par exemple, on pourrait implémenter une structure de pile avec un tableau en utilisant un header pile.h (cat
servant à afficher le contenu d'un fichier) :
! cat pile/pile.h
#include <stdbool.h> struct pile { int* t; // tableau contenant les éléments int size; // nombre d'éléments (= dessus de la pile) }; typedef struct pile pile; pile create(int); bool is_empty(pile); void push(pile*, int); int pop(pile*);
pile.h utilise bool
qui est défini dans stdbool
, d'où le #include <stdbool.h>
.
La définition des fonctions de pile.h se trouvent dans un fichier source pile.c :
! cat pile/pile.c
#include "pile.h" #include <stdlib.h> pile create(int capacity) { pile p; int* t = malloc(capacity*sizeof(int)); p.t = t; p.size = 0; return p; } bool is_empty(const pile p) { return p.size == 0; } void push(pile* p, int e) { p->t[p->size] = e; p->size++; } int pop(pile* p) { p->size--; return p->t[p->size]; }
pile.c utilise la définition de struct pile
de pile.h
, donc a besoin de l'importer avec #include "pile.h"
, les guillemets signifiants que pile.h
est dans le même dossier (alors que #include <stdlib.h>
demande de chercher dans la librairie standard).
Enfin, nous allons utiliser ce fichier main.c pour tester notre implémentation de pile :
! cat pile/main.c
#include <stdio.h> #include "pile.h" int main() { pile p = create(10); printf("is_empty(p): %d\n", is_empty(p)); push(&p, 42); push(&p, -5); printf("pop(p): %d\n", pop(&p)); printf("pop(p): %d", pop(&p)); return 0; }
Compilateur¶
Le langage C est normalement compilé (nous avons pour l'instant utilisé xeus-cling, qui permet d'utiliser C de façon interactive avec Jupyter... mais ce n'est pas habituel), c'est-à-dire que le code source (dans un fichier .c) est transformé en un fichier exécutable (.exe sous Windows) par un compilateur.
Il existe de nombreux compilateurs pour le C, dont voici les plus connus :
- GCC (GNU Compiler Collection), historiquement utilisé par Linux
- Clang, alternative à GCC
- Compilateur Visual C/C++ (Microsoft)
Étapes¶
La compilation d'un fichier source en un exécutable suit quatre grandes étapes :
- Traitement par le préprocesseur
- Compilation
- Assemblage
- Édition de liens
! cat hello.c
#include <stdio.h> int main() { printf("Hello World!"); }
main
est une fonction spéciale : c'est celle qui va être exécutée lorsque nous lancerons notre programme. Elle est censée renvoyer un entier représentant un code d'erreur. Par défaut, 0 est renvoyé (aucune erreur).
Préprocesseur¶
Le préprocesseur remplace les instructions commençant par #
. Par exemple, le #include <stdio.h>
est remplacé par le contenu du header correspondant. On peut voir le résultat du préprocesseur avec l'option -E
de gcc (en spécifiant le fichier de sortie avec -o hello.i
) :
! gcc -E -o hello.i hello.c
hello.i
contient alors beaucoup de code dont notamment la ligne suivante, donnant le prototype de printf
:
extern int printf (const char *__restrict __format, ...);
Compilation¶
La compilation traduit le code C en de l'assembleur, qui est un langage très proche du processeur (et qui dépend du processeur : x86 pour Intel, AMD64 pour AMD...). On peut obtenir le code assembleur dans un fichier hello.s
avec l'option -S de gcc :
! gcc -S hello.c
! head -20 hello.s
.file "hello.c" .text .section .rodata .LC0: .string "Hello World!" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi movl $0, %eax call printf@PLT
Par exemple, movl
permet de déplacer de la mémoire dans les registres.
Assemblage¶
L'étape d'assemblage produit un fichier objet binaire (hello.o
) :
! gcc -c hello.c
Édition de liens (linker)¶
Le fichier hello.o
ne contient que les implémentations des fonctions définies dans hello.c
, mais pas celles importées par des #include
. Le linker réunis plusieurs fichiers objets pour en faire un seul exécutable contenant l'implémentation de toutes les fonctions utilisées :
! gcc hello.o -o hello
gcc ajoute automatiquement la librairie standard lors de l'étape de linkage, il n'y a donc pas besoin de renseigner le fichier objet correspondant à stdio.h
.
Exécution¶
On peut enfin exécuter notre programme :
! ./hello
Hello World!
Nous avons décomposé toutes les étapes ici, mais on aurait pu tout faire avec une seule commande :
! gcc hello.c -o hello
! ./hello
Hello World!
Exemple : pile¶
Revenons sur notre exemple d'implémentation de pile avec un tableau. Si on essaie de compiler directement main.c
, on obtient une erreur :
! gcc pile/main.c
/usr/bin/ld: /tmp/ccQ10bZg.o: in function `main': main.c:(.text+0x21): undefined reference to `create' /usr/bin/ld: main.c:(.text+0x3c): undefined reference to `is_empty' /usr/bin/ld: main.c:(.text+0x63): undefined reference to `push' /usr/bin/ld: main.c:(.text+0x74): undefined reference to `push' /usr/bin/ld: main.c:(.text+0x80): undefined reference to `pop' /usr/bin/ld: main.c:(.text+0x9f): undefined reference to `pop' collect2: error: ld returned 1 exit status
ld
, qui est le linker de gcc, nous indique que les implémentations des fonctions create
, is_empty
, push
et pop
n'ont pas été trouvées. En effet, nous n'avons pas demandé de produire les fichiers objets correspondant à pile.c
. Essayons :
! gcc pile/pile.c
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start': (.text+0x24): undefined reference to `main' collect2: error: ld returned 1 exit status
Cette fois, une erreur nous indique que main
n'a pas été trouvé : cette fonction est essentielle pour produire un exécutable.
On peut produire les deux objets pile.o
et main.o
puis les linker :
! gcc -c pile/pile.c -o pile/pile.o
! gcc -c pile/main.c -o pile/main.o
! gcc pile/pile.o pile/main.o -o pile/pile
On peut maintenant exécuter notre programme pile
:
! ./pile/pile
is_empty(p): 1 pop(p): -5 pop(p): 42
En fait, les 3 appels précédents à gcc auraient pu s'écrire sur une ligne :
! gcc pile/pile.c pile/main.c -o pile/pile
pile/main.c: In function ‘main’: pile/main.c:5:5: error: variable ‘p’ has initializer but incomplete type 5 | pile p = create(10); | ^~~~ pile/main.c:5:14: error: invalid use of incomplete typedef ‘pile’ {aka ‘struct pile’} 5 | pile p = create(10); | ^~~~~~ pile/main.c:5:10: error: storage size of ‘p’ isn’t known 5 | pile p = create(10); | ^ pile/main.c:6:42: error: type of formal parameter 1 is incomplete 6 | printf("is_empty(p): %d\n", is_empty(p)); | ^
! ./pile/pile
is_empty(p): 1 pop(p): -5 pop(p): 42
Exercice
Implémenter une liste chaînée en utilisant un fichier .c
et un fichier .h
. Compiler et tester.
Exercice
De même, implémenter une file (par tableau ou liste chaînée, selon votre préférence). Compiler et tester.
Inclusions multiples¶
Si on inclut plusieurs fois le même fichier, on obtient une erreur de redéfinition :
#include "pile/pile.h"
#include "pile/pile.h"
In file included from input_line_9:1: ./pile/pile.h:3:8: error: redefinition of 'pile' struct pile { ^ input_line_7:1:10: note: './pile/pile.h' included multiple times, additional include site here #include "pile/pile.h" ^ input_line_9:1:10: note: './pile/pile.h' included multiple times, additional include site here #include "pile/pile.h" ^ ./pile/pile.h:3:8: note: unguarded header; consider using #ifdef guards or #pragma once struct pile { ^ In file included from input_line_9:2: ./pile/pile.h:3:8: error: redefinition of 'pile' struct pile { ^ input_line_7:1:10: note: './pile/pile.h' included multiple times, additional include site here #include "pile/pile.h" ^ input_line_9:2:10: note: './pile/pile.h' included multiple times, additional include site here #include "pile/pile.h" ^ ./pile/pile.h:3:8: note: unguarded header; consider using #ifdef guards or #pragma once struct pile { ^
Interpreter Error:
Cette erreur peut arriver par exemple si un fichier file1.c
inclut deux fichiers file2.h
, file3.h
et que file3.h
inclut file2.h
. file1.c
se retrouve alors avec deux inclusions du fichier file2.h
. On peut éviter ce genre de problème en rajoutant les lignes suivantes dans chaque header (par exemple file2.h
) :
#ifndef FILE2_H
#define FILE2_H
// ce code ne sera inclut qu'une fois
#endif