Initiation au C
Nous allons écrire de petits programmes qui écrivent dans le terminal. Le but est d'apprendre :
- à compiler un seul fichier C (compiler = transformer le programme C texte en fichier exécutable que la machine peut exécuter)
- écrire un programme C basique (sans pointeur)
⌛ Plus tard, on fera des programmes avec plusieurs fichiers, et on verra les pointeurs.
Mon premier programme C
Voici mon premier programme en C :
#include <stdio.h>
int main() {
printf("Bienvenue à l'ENS Lyon/n");
return 0;
}
Vous pouvez mettre ce contenu dans un fichier :
🖹 programme.c
#include <stdio.h>
permet d'utiliser des fonctions pour écrire et lire dans la console.
int main()
est le début de la définition de la fonction principalemain
. Sa définition commence par le type que renvoie la fonction, ici un entierint
. La fonction renvoie0
quand tout s'est bien passé. Elle renvoie une valeur non nulle pour signaler une erreur.printf
permet d'écrire dans la console.return 0;
quitte la fonction et renvoie0
.
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Bienvenue à l'ENS Lyon/n");
return EXIT_SUCCESS;
}
Compilation
Le fichier 🖹 programme.c n'est pas exécutable : il s'agit d'un fichier texte que le processeur ne comprend pas. Pour construire un fichier exécutable par la machine, on utilise un compilateur. Ce processus s'appelle la compilation.
Pour compiler, taper :
gcc programme.c
On obtient un nouveau fichier qui est exécutable :
🖹 programme.c
⚙ a.out
et que l'on peut lancer :
./a.out
Bibliothèque standard
Le langage C est fourni avec la bibliothèque standard. Elle offre des fichiers appelés fichiers d'en-tête ou fichier header. Par exemple, le fichier stdio.h
offre les fonctionnalités d'entrées/sortie sur la console. Les fichiers d'en-tête sont normalement disponibles dans le dossier usr/include
. Ils contiennent des signatures de fonctions.
⌛ On peut aussi créer des fichiers header nous-même, ou alors utiliser ceux d'une bibliothèque non-standard (comme la bibliothèque raylib
pour dessiner à l'écran !).
Voici les fichiers d'en-tête de la bibliothèque standard :
Bibliothèque standard C (C23)
│
├── Entrées / Sorties & Utilitaires
│ ├── <stdio.h> (E/S standard : printf, scanf, fopen…)
│ ├── <stdlib.h> (malloc, exit, atoi, rand…)
│ ├── <string.h> (strlen, strcpy, memcpy…)
│ ├── <time.h> (time, clock, struct tm…)
│ ├── <locale.h> (setlocale, conventions régionales)
│ └── <uchar.h> (Unicode UTF-16/UTF-32)
│
├── Chaînes & Caractères
│ ├── <ctype.h> (isalpha, tolower…)
│ ├── <wchar.h> (wchar_t, fonctions chaînes larges)
│ └── <wctype.h> (classification/conversion wchar_t)
│
├── Mathématiques
│ ├── <math.h> (sin, cos, sqrt…)
│ ├── <complex.h> (nombres complexes)
│ ├── <tgmath.h> (maths génériques type-safe)
│ ├── <float.h> (caractéristiques flottants)
│ └── <fenv.h> (contrôle environnement flottant)
│
├── Types & Limites
│ ├── <stddef.h> (size_t, NULL, ptrdiff_t…)
│ ├── <stdbool.h> (bool, true, false)
│ ├── <stdint.h> (entiers fixes : int32_t…)
│ ├── <inttypes.h> (formatage et macros pour stdint)
│ ├── <limits.h> (INT_MAX, etc.)
│ ├── <stdalign.h> (alignement mémoire, alignof…)
│ ├── <stdnoreturn.h>(_Noreturn)
│ └── <iso646.h> (alias syntaxiques : and, or, not…)
│
├── Gestion d’erreurs & Signaux
│ ├── <assert.h> (assert)
│ ├── <errno.h> (errno, codes d’erreur)
│ ├── <signal.h> (SIGINT, SIGSEGV…)
│ └── <setjmp.h> (setjmp/longjmp)
│
├── Fonctions variadiques
│ └── <stdarg.h> (va_list, va_start, va_arg…)
│
└── Concurrence (C11+)
├── <threads.h> (threads, mutex, cond vars)
└── <stdatomic.h> (atomiques)
Entrées-sorties
stdio.h
est le fichier pour de la bibliothèque standard (std
) pour les entrées-sorties (io
).
Pour utiliser ces fonctions, on écrit :
#include <stdio.h>
Ecrire sur stdout
(sortie standard)
La sortie standard est typiquement la console, mais cela peut être un fichier, ou alors une entrée à un autre programme.
putchar('c')
écrit le caractère c
dans le terminal.
printf
est la fonction pour écrire d'un coup des informations plus compliquées ✎ :
printf("Bonjour");
printf("Bonjour\n");
printf("Voici votre identifiant : %d", id);
printf("Voici votre nouveau prénom : %s", prenom);
printf("Voici votre nouveau prénom et votre identifiant : %s %d", prenom, id);
⚠ Ne jamais écrire printf(dataFromTheUser)
car l'utilisateur pourrait écrire des %s
, %d
, voire des %n
qui écrivent dans la mémoire. Le comportement peut être inattendu, et c'est faille de sécurité garantie :).
Lire l'entrée stdin
(entrée standard)
L'entrée standard est typiquement ce qu'écrit l'utilisateur dans la console, mais cela peut être un fichier, ou alors une sortie d'un autre programme.
La fonction getchar()
renvoie le prochain caractère sur l'entrée puis le consomme, c'est-à-dire que la prochaine fois que l'on exécute getchar()
on lira le prochain caractère. getchar()
renvoie EOF
(end of file) si c'est fini.
scanf
est la fonction pour lire des informations plus compliquées que l'utilisateur écrit dans la console ou de manière générale dans stdin
⌨ :
int n;
scanf("%d", &n);
On peut aussi lire plusieurs variables d'un coup.
int n, m;
scanf("%d", &n, &m);
La fonction scanf
renvoie le nombre de variables qui ont été affectées correctement.
int n, m;
int nbLus = scanf("%d %d", &n, &m);
Spécification de formats
Spécificateur | Type attendu | Exemple | Résultat affiché |
---|---|---|---|
%d ou %i | int (entier signé) | printf("%d", -42); | -42 |
%u | unsigned int | printf("%u", 42); | 42 |
%o | entier non signé (octal) | printf("%o", 42); | 52 |
%x | entier non signé (hexadécimal min.) | printf("%x", 42); | 2a |
%X | entier non signé (hexadécimal maj.) | printf("%X", 42); | 2A |
%c | char (caractère) | printf("%c", 'A'); | A |
%s | chaîne (char* ) | printf("%s", "Hello"); | Hello |
%f | float ou double (décimal) | printf("%f", 3.14); | 3.140000 |
%.2f | float ou double (précision) | printf("%.2f", 3.14); | 3.14 |
%e / %E | float ou double (scientifique) | printf("%e", 1234.0); | 1.234000e+03 |
%g / %G | float ou double (auto %f ou %e ) | printf("%g", 1234.0); | 1234 |
%p | pointeur (void* ) | printf("%p", ptr); | 0x7ffee2bff6c0 |
%% | affiche % | printf("50%%"); | 50% |
Quelques éléments du langage C
Une déclaration de variable est un morceau de code qui renseigne le type d'une variable. Par exemple :
int x;
Il y a pleins de types primitifs : int
, long
, unsigned int
, char
, bool
(avec #include <stdbool.h>
).
Une instruction est un morceau du programme qui ne renvoie rien. Par exemple, printf("aïe");
Une expression est un morceau du programme qui renvoie une valeur. Par exemple 1+2
. Mais aussi x = 42
qui renvoie... 42
.
Incrémentation
Considérons l'instruction (qui est aussi une expression)
x = 42;
- Both expressions
x++
and++x
incrementx
. - The value of the expression
x++
is 42. - The value of expression
++x
is 43.
L'instruction (qui est aussi une expression) suivante
x += 3;
ajoute 3 à x.
Boucle for
for(int i = 0; i < n; i++)
...
for(int i = 0; i < n; i++) {
...
}
En fait, la boucle for
en C est puissante. Elle a l'expressivité des boucles while
:
for(expr1; expr2; expr3) {
...
}
est équivalent à
expr1;
while(expr2) {
...
expr3;
}
La fonction main
Un programme C contient toujours une fonction main
qui est la fonction appelée quand on exécute le programme :
#include <stdlib.h>
int main() {
...
return EXIT_SUCCESS;
}
On verra plus tard une signature plus compliquée pour main
car on peut gérer des arguments donnés au programme.
Utilisation du compilateur
gcc -Wall -pedantic -ainsi -std=c99 programme.c -o programme
Options | Meaning |
---|---|
-Wall | enables all compiler's warning messages |
-Wextra | still a bit more warnings |
-pedantic | enables all compiler's warning messages |
-ansi | turns off gcc features that are not in the ANSI C |
-std=c99 | ANSI C99 |
-std=c11 | ANSI C99 |
-o programme | tells that the output is the file programme |
Exercices
Triangle d'étoiles
Ecrire un programme qui prend demande à l'utilisateur/rice un nombre entier n
puis affiche un triangle d'étoiles sur n
lignes. Par exemple si n
vaut 5, on affiche :
*
**
***
****
*****
Pyramide
Même chose avec une pyramide :
*
***
*****
*******
Nombre de caractères dans le texte sur stdin
En utilisant getchar()
, écrire un programme nombreligne.c
qui lit stdin
et sur donne le nombre de lignes dans l'entrée.
Par exemple sur
./nombreligne < textExemple.txt
il donne le nombre de lignes dans le fichier texte textExemple.txt
.
- Pareil avec le nombre de lignes
- Pareil avec le nombre de mots
Occurrences de caractères
Dans cet exercice, vous aurez besoin d'un tableau d'entiers de taille 26 déclaré avec int A[26];
. On accède au i
-ème élément du tableau A
avec A[i]
. On rappelle que chaque caractère est un nombre entre 0 et 255, codé sur 8 bits. Si c
est un char
, on peut écrire c - 'a'
pour faire la différence entre le code de c
et celui de la lettre 'a'
.
Ecrire un programme qui lit stdin
et qui affiche un histogramme du nombre des occurrences des lettres (on ignore les autres caractères, les chiffres, etc.) comme :
a ***
b ******
c **
d *
e ************
z ***
Variables
La portée (scope) d'une variable est la portion de code dans laquelle la variable peut être utilisée.
La durée de vie (life-time) d'une variable est la durée d'existence d'une variable.
Scope | Life-time | |
---|---|---|
Global variables | All | Always |
Variables in functions | Function | Function |
Static variables in functions | Function | Always |
Les variables statiques peuvent servir pour :
- faire un compteur et cacher le contenu du compteur en dehors
- faire une fonction pour le drag drop avec la souris (on stocke/cache la position précédente de la souris)
Mémoire
La mémoire allouée pour un programme est divisée en trois parties :
- Segment de données. On y trouve les variables globales et les variables statiques.
- Pile d'appels. On y trouve les variables locales à une fonction. Les tableaux locaux sont stockées sur la pile aussi.
- Le tas mémoire. C'est pour y mettre des données dont on ne connait pas à l'avance la taille, ou dont la taille peut modifiée au cours du programme (structure de données comme des tableaux redimensionnables, des listes chaînées, etc.).
Typage en C
Le type en C sert à dire combien d'octets prend une variable.
La fonction sizeof
renvoie combien d'octets sont utilisé pour stocker un certain type / une certaine variable.
sizeof(truc) | utilise ... octets |
---|---|
sizeof(char) | 1 |
sizeof(int) | 4 |
int x;
printf("%d\n", sizeof(x));
int A[5];
printf("%d\n", sizeof(A));
Conversion implicite
1 + 2.3f
Casting
int i = 2;
int j = 3;
float quotient = 2/3;
Oh non, quotient
vaut 0. Mais on peut caster :
quotient = ((float) 2) / 3
Le C est de bas niveau...
Voici un site où on peut compiler du code C et voir le résultat en assembleur :
https://godbolt.org/
Exercices
Génération de labyrinthes
Proposer un algorithme aléatoire pour générer un labyrinthe comme
XXXXXXXXXXXXXXX
X X X X
X XXX X X X XXX
X X X X X X
X X XXXXX XXX X
X X X X X
X X X X XXXXX X
X X X X X X X
X XXX X X X X X
X X X X X X
X X XXX X XXX X
X X X X X X
X XXXXXXX X X X
X X X
XXXXXXXXXXXXXXX
Il s'agit de produire un tableau A 2D de taille n x m où n et m sont des entiers impairs avec :
- A[i, j] =
'X'
ou' '
- A[0, j], A[i, 0], A[n-1, j], A[i, m-1] contiennent
'X'
; - A[i, j] =
'X'
lorsque i et j sont pairs - A[i, j] =
' '
lorsque i et j sont impairs - Le graphe G est un arbre, où G est le graphe non orienté (V, E) où V sont les couples (i, j) dans {1, 3, .., n-2} x {1, 3, ..., m-2} et E contient les arêtes {(i, j), (i', j')} avec :
- i' == i+2 et j'== j et A[i+1, j] ==
' '
- ou i' == i et j' == j+1 et A[i, j+1] ==
' '
Prochaine séance
Nous allons utiliser la bibliothèque raylib
pour pouvoir afficher des dessins (rectangles, points, lignes, etc.) à l'écran :).
- Jeu vidéo comme
agar.io
- Afficher une fractale comme celle de mandelbrot
- Promenade aléatoire
- Appartenance d'un point à un polygone ?
- parcours de Graham
Pour aller plus loin
- https://emscripten.org/ Pour développer des programmes qui tournent sur le Web
- https://cs50.readthedocs.io/style/c/
- https://www.gnu.org/prep/standards/html_node/Writing-C.html#Writing-C
- Fonctions de base en langage C sur wikiversité
- Du matériel sur la génération de labyrinthes