Skip to content

Commit 7985d99

Browse files
committedMay 2, 2020
Work on data-structures
1 parent 759372c commit 7985d99

12 files changed

+1155
-122
lines changed
 

‎_templates/layout.html

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{% extends "!layout.html" %}
2+
{% block extrahead %}
3+
<link href="{{ pathto("_static/custom.css", True) }}" rel="stylesheet" type="text/css">
4+
{% endblock %}

‎assets/src/memoize.c

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#include <stdio.h>
2+
#include <stdbool.h>
3+
4+
#define SIZE 1000
5+
6+
bool cache_input[SIZE] = { false };
7+
int cache_output[SIZE];
8+
9+
int memoize(int input, int output) {
10+
cache_input[input % SIZE] = true;
11+
cache_output[input % SIZE] = output;
12+
return output;
13+
}
14+
15+
bool memoize_has(int input) {
16+
return cache_input[input % SIZE];
17+
}
18+
19+
int memoize_get(int input) {
20+
return cache_output[input % SIZE];
21+
}
22+
23+
int fib(int n)
24+
{
25+
if (memoize_has(n)) return memoize_get(n);
26+
if (n < 2) return 1;
27+
return memoize(n, fib(n - 1) + fib(n - 2));
28+
}
29+
30+
int main() {
31+
for (int i = 0; i < 40; i++) {
32+
printf("%d\n", fib(i));
33+
}
34+
}

‎content/20-datatype.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,15 @@ L'utilisation d'un type énuméré peut être la suivante :
733733
}
734734
}
735735
736+
Type incomplet
737+
==============
738+
739+
Un type incomplet est un qualificatif de type de donnée décrivant un objet dont sa taille en mémoire n'est pas connue.
740+
736741
Type vide (*void*)
737742
==================
738743

739-
Le type ``void`` est particulier car c'est un type qui ne vaut rien. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :
744+
Le type ``void`` est particulier. Il s'agit d'un type dit **incomplet** car la taille de l'objet qu'il représente en mémoire n'est pas connue. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :
740745

741746
.. code-block:: c
742747

‎content/40-functions.rst

+140
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,146 @@ En des termes plus corrects, mais nous verrons cela au chapitre sur les pointeur
279279

280280
Retenez simplement que lors d'un passage par référence, on cherche à rendre la valeur passée en paramètre modifiable par le *caller*.
281281

282+
Récursion
283+
=========
284+
285+
La récursion, caractère d'un processus, d'un mécanisme récursif, c'est à dire qui peut être répété un nombre indéfini de fois par l'application de la même règle, est une méthode d'écriture dans laquelle une fonction s'appelle elle même.
286+
287+
Au chapitre sur les fonctions, nous avions donné l'exemple du calcul de la somme de la suite de fibonacci jusqu'à ``n`` :
288+
289+
.. code-block:: c
290+
291+
int fib(int n)
292+
{
293+
int sum = 0;
294+
int t1 = 0, t2 = 1;
295+
int next_term;
296+
for (int i = 1; i <= n; i++)
297+
{
298+
sum += t1;
299+
next_term = t1 + t2;
300+
t1 = t2;
301+
t2 = next_term;
302+
}
303+
return sum;
304+
}
305+
306+
Il peut sembler plus logique de raisonner de façon récursive. Quelque soit l'itération à laquelle l'on soit, l'assertion suivante est valable :
307+
308+
fib(n) == fib(n - 1) + fib(n - 2)
309+
310+
Donc pourquoi ne pas réécrire cette fonction en employant ce caractère récursif ?
311+
312+
.. code-block:: c
313+
314+
int fib(int n)
315+
{
316+
if (n < 2) return 1;
317+
return fib(n - 1) + fib(n - 2);
318+
}
319+
320+
Le code est beaucoup plus simple a écrire, et même à lire. Néanmoins cet algorithme est notoirement connu pour être mauvais en terme de performance. Calculer ``fib(5)`` revient à la chaîne d'appel suivant.
321+
322+
Cette chaîne d'appel représente le nombre de fois que ``fib`` est appelé et à quel niveau elle est appelée. Par exemple ``fib(4)`` est appelé dans ``fib(5)`` :
323+
324+
.. code-block:: text
325+
326+
fib(5)
327+
fib(4)
328+
fib(3)
329+
fib(2)
330+
fib(1)
331+
fib(2)
332+
fib(1)
333+
fib(0)
334+
fib(3)
335+
fib(2)
336+
fib(1)
337+
fib(0)
338+
fib(1)
339+
340+
Si l'on somme le nombre de fois que chacune de ces fonctions est appelée :
341+
342+
.. code-block:: text
343+
344+
fib(5) 1x
345+
fib(4) 1x
346+
fib(3) 2x
347+
fib(2) 3x
348+
fib(1) 4x
349+
fib(0) 2x
350+
-----------
351+
fib(x) 13x
352+
353+
Pour calculer la somme de fibonacci, il faut appeler 13 fois la fonction. On le verra plus tard mais la complexité algoritmique de cette fonction est dite :math:`O(2^n)`. C'est à dire que le nombre d'appels suit une relation exponentielle. La réelle complexité est donnée par la relation :
354+
355+
.. math::
356+
357+
T(n) = O\left(\frac{1+\sqrt(5)}{2}^n\right) = O\left(1.6180^n\right)
358+
359+
Ce terme 1.6180 est appelé `le nombre d'or <https://fr.wikipedia.org/wiki/Nombre_d%27or>`__.
360+
361+
Ainsi pour calculer fib(100) il faudra sept cent quatre-vingt-douze trillions soixante-dix mille huit cent trente-neuf billions huit cent quarante-huit milliards trois cent soixante-quatorze millions neuf cent douze mille deux cent quatre-vingt-douze appels à la fonction `fib` (792'070'839'848'374'912'292). Pour un processeur Core i7 (2020) capable de calculer environ 100 GFLOPS (milliards d'opérations par seconde), il lui faudra tout de même 251 ans.
362+
363+
En revanche, dans l'approche itérative, on constate qu'une seule boucle ``for``. C'est à dire qu'il faudra seulement 100 itérations pour calculer la somme.
364+
365+
Généralement les algorithmes récursifs (s'appelant eux même) sont moins performants que les algorithmes itératifs (utilisant des boucles). Néanmoins il est parfois plus facile d'écrire un algorithme récursif.
366+
367+
Notons que tout algorithme récursif peut être écrit en un algorithme itératif, mais ce n'est pas toujours facile.
368+
369+
Mémoïsation
370+
===========
371+
372+
En informatique la `mémoïsation <https://fr.wikipedia.org/wiki/M%C3%A9mo%C3%AFsation>`__ est une technique d'optimisation du code souvent utilisée conjointement avec des algorithmes récursifs. Cette technique est largement utilisée en `programmation dynamique <https://fr.wikipedia.org/wiki/Programmation_dynamique>`__.
373+
374+
Nous l'avons vu précédemment, l'algorithme récursif du calcul de la somme de la suite de Fibonacci n'est pas efficace du fait que les mêmes appels sont répétés un nombre inutiles de fois. La parade est de mémoriser pour chaque appel de ``fib``, la sortie correspondante à l'entrée.
375+
376+
Dans cet exemple nous utiliserons un mécanisme composé de trois fonctions :
377+
378+
- ``int memoize(Cache *cache, int input, int output)``
379+
- ``bool memoize_has(Cache *cache, int input)``
380+
- ``int memoize_get(Cache *cache, int input)``
381+
382+
La première fonction mémorise la valeur de sortie ``output`` liée à la valeur d'entrée ``input``. Pour des raisons de simplicité d'utilsation, la fonction retourne la valeur de sortie ``output``.
383+
384+
La seconde fonction ``memoize_has`` vérifie si une valeur de correspondance existe pour l'entrée ``input``. Elle retourne ``true`` en cas de correspondance et ``false`` sinon.
385+
386+
La troisième fonction ``memoize_get`` retourne la valeur de sortie correspondante à la valeur d'entrée ``input``.
387+
388+
Notre fonction récursive sera ainsi modifiée comme suit :
389+
390+
.. code-block:: c
391+
392+
int fib(int n)
393+
{
394+
if (memoize_has(n)) return memoize_get(n);
395+
if (n < 2) return 1;
396+
return memoize(n, fib(n - 1) + fib(n - 2));
397+
}
398+
399+
Quant aux trois fonctions utilitaires, voici une proposition d'implémentation. Notons que cette implémentation est très élémentaire et n'est valable que pour des entrées inférieures à 1000. Il sera possible ultérieurement de perfectionner ces fonctions, mais nous aurons pour cela besoin de concepts qui n'ont pas encore été abordés, tels que les structures de données complexes.
400+
401+
.. code-block:: c
402+
403+
#define SIZE 1000
404+
405+
bool cache_input[SIZE] = { false };
406+
int cache_output[SIZE];
407+
408+
int memoize(int input, int output) {
409+
cache_input[input % SIZE] = true;
410+
cache_output[input % SIZE] = output;
411+
return output;
412+
}
413+
414+
bool memoize_has(int input) {
415+
return cache_input[input % SIZE];
416+
}
417+
418+
int memoize_get(int input) {
419+
return cache_output[input % SIZE];
420+
}
421+
282422
------
283423

284424
.. exercise:: Dans la moyenne

‎content/55-memory-management.rst

+51
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Lorsqu'un programme à besoin de mémoire, il peut générer un appel système p
5959

6060
L'allocation se fait sur le `tas` (*heap*) qui est de taille variable. À chaque fois qu'un espace mémoire est demandé, ``malloc`` recherche dans le segment un espace vide de taille suffisante, s'il ne parvient pas, il exécute l'appel système `sbrk <https://en.wikipedia.org/wiki/Sbrk>`__ qui permet de déplacer la frontière du segment mémoire et donc d'agrandir le segment.
6161

62+
.. _fig_allocation:
6263
.. figure:: ../assets/figures/dist/memory/malloc.*
6364

6465
Mémoire de programme
@@ -265,3 +266,53 @@ allez piétiner les zones mémoires voisines sans en avoir la permission.
265266
Le compilateur (en réalité l'éditeur de liens - le *linker*) vous permet
266267
de spécifier la taille de la pile ; c'est une de ses nombreuses options.
267268

269+
Variables automatiques
270+
======================
271+
272+
Une variable est dite *automatique* lorsque sa déclaration est faite au sein d'une fonction. La variable d'itération ``int i`` dans une boucle ``for`` est dite automatique. C'est à dire que le compilateur à le choix de placer cette variable :
273+
274+
- sur la pile ;
275+
- dans un registre mémoire processeur.
276+
277+
Jadis, le mot clé ``register`` était utiliser pour forcer le compilateur à placer une variable locale dans un registre processeur pour obtenir de meilleures performances. Aujourd'hui, les compilateurs sont assez malins pour déterminer automatiquement les variables souvent utilisées.
278+
279+
Fragmentation mémoire
280+
=====================
281+
282+
On peut observer à la figure :numref:`fig_allocation` qu'après un appel successif de ``malloc`` et de ``free`` des espaces mémoires non utilisés peuvent apparaître entre des régions utilisées. Ces *trous* sont appelés fragmentation mémoire.
283+
284+
Dans la figure suivante, on suit l'évolution de l'utilisation du *heap* au cours de la vie d'un programme. Au début ➀, la mémoire est libre. Tant que de la mémoire est allouée sans libération (``free``), aucun problème de fragmentation ➁. Néanmoins, après un certain temps la mémoire devient fragmentée ➂ ; il reste dans cet exemple 2 emplacements de taille 2, un emplacement de taille 5 et un emplacement de taille 8. Il est donc impossible de réserver un espace de taille 9 malgré que l'espace cumulé libre est suffisant.
285+
286+
.. figure:: ../assets/figures/dist/memory/fragmentation.*
287+
288+
Dans une petite architecture, l'allocation et la libération fréquente d'espaces mémoire de taille arbitraire est malvenue. Une fois que la fragmentation mémoire est installée, il n'existe aucun moyen de soigner le mal si ce n'est au travers de l'ultime solution de l'informatique : `éteindre puis redémarrer <https://www.youtube.com/watch?v=nn2FB1P_Mn8>`__.
289+
290+
MMU
291+
---
292+
293+
Les systèmes d'exploitations modernes (Windows, Linux, macOS...) utilisent tous un dispositif matériel nommé `MMU <https://en.wikipedia.org/wiki/Memory_management_unit>`__ pour *Memory Management Unit*. La MMU est en charge de créer un espace mémoire **virtuel** entre l'espace physique. Cela crée une indirection supplémentaire mais permet de réorganiser la mémoire physique sans compromettre le système.
294+
295+
En pratique l'espace de mémoire virtuelle est toujours beaucoup plus grand que l'espace physique. Cela permet de s'affranchir dans une large mesure de problèmes de fragmentation car si l'espace virtuel est suffisament grand, il y aura statistiquement plus de chance d'y trouver un emplacement non utilisé.
296+
297+
La programmation sur de petites architectures matérielles (microcontrôleurs, DSP) ne possèdent pas de MMU et dès lors l'allocation dynamique est généralement à proscrire à moins qu'elle soit faite en connaissance de cause et en utilisant des mécanisme comme les *memory pool*.
298+
299+
Dans la figure ci-dessous. La mémoire physique est représentée à droite en termes de pages mémoires physiques (*Physical Pages* ou **PP**). Il s'agit de blocs mémoires contigus d'une taille fixe, par exemple 64 kB. Chaque page physique est mappée dans une table propre à chaque processus (programme exécutable). On y retrouve quelques proriétés utiles à savoir est-ce que la page mémoire est accessible en écriture, est-ce qu'elle peut contenir du code exécutable ? Une propriété peut indiquer par exemple si la page mémoire est valide. Chacune de ces entrées est considérée comme une page mémoire virtuelle (*virtual page* **VP**).
300+
301+
.. figure:: ../assets/figures/dist/memory/mmu.*
302+
303+
Erreurs de segmentation (*segmentation fault*)
304+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
305+
306+
Lorsqu'un programme tente d'accéder à un espace mémoire qui n'est pas mappé dans la MMU, ou que cet espace mémoire ne permet pas le type d'accès souhaité : par exemple une écriture dans une page en lecture seule. Le système d'exploitation tue le processus avec une erreur *Segmentation Fault*. C'est la raison pour laquelle, il n'est pas systématique d'avoir une erreur de segmentation en cas de jarinage mémoire. Tant que les valeurs modifées sont localisées au sein d'un bloc mémoire autorisé, il n'y aura pas d'erreur.
307+
308+
L'erreur de segmentation est donc générée par le système d'exploitation en lèvant le signal **SIGSEGV** (Violation d'accès à un segment mémoire, où erreur de segmentation).
309+
310+
Memory Pool
311+
-----------
312+
313+
Un *memory pool* est une méthode faisant appel à de l'allocation dynamique de blocs de taille fixe. Lorsqu'un programme doit très régulièrement allouer et désalouer de la mémoire, il est préférable que les blocs mémoire ait une taille fixe. De cette façon, après un ``free``, la mémoire libérée est assez grande pour une allocation ultérieure.
314+
315+
Lorsqu'un programme est exécuté sous Windows, macOS ou Linux, l'allocation dynamique standard ``malloc``, ``calloc``, ``realloc`` et ``free`` sont performants et le risque de crash dû à une fragmentation mémoire est rare.
316+
317+
En revanche lors de l'utilisation sur de petites architectures (microcontrôleurs) qui n'ont pas de système sophistiqués pour gérer la mémoire, il est parfois nécessaire d'écrire son propre système de gestion de mémoire.
318+

‎content/77-algorithms.rst

+21
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ Programmation dynamique
177177

178178
.. todo:: Compléter
179179

180+
Algorithmes de tris
181+
===================
182+
183+
Heap Sort
184+
---------
185+
186+
187+
188+
.. code-block:: text
189+
190+
8
191+
|
192+
----+----
193+
/ \
194+
4 12
195+
-- --
196+
/ \ / \
197+
20 6 42 14
198+
/ \ / \ / \ / \
199+
11 3 35 7 9 11 50 16
200+
180201
-----
181202

182203
.. exercise:: Intégrateur de Kahan

‎content/78-translation-units.rst

+33-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
.. _TranslationUnits:
3+
24
===================
35
Compilation séparée
46
===================
@@ -31,7 +33,7 @@ Ainsi, lorsque le programme commence à être volumineux, sa lecture, sa compré
3133
├── complex.h
3234
└── main.c
3335
34-
Le programme principal et la fonction ``main`` est contenu dans ``main.c`` quant au module *complex* il est composé de deux fichiers : ``complex.h`` l'en-tête et ``complex.c``, l'implémentation du module.
36+
Le programme principal et la fonction ``main`` est contenu dans ``main.c`` quant au module *complex* il est composé de deux fichiers : ``complex.h`` l'en-tête et ``complex.c``, l'implémentation du module.
3537

3638
Le fichier ``main.c`` devra inclure le fichier ``complex.h`` afin de
3739
pourvoir utiliser correctement les fonctions du module de gestion des
@@ -44,7 +46,7 @@ nombres complexes. Exemple :
4446
4547
int main() {
4648
Complex c1 = { .real = 1., .imag = -3. };
47-
complex_fprint(stdout, c1);
49+
complex_fprint(stdout, c1);
4850
}
4951
5052
.. code-block:: c
@@ -86,43 +88,43 @@ Cet exemple sera compilé dans un environnement POSIX de la facon suivante :
8688
gcc -c main.c -o main.o
8789
gcc complex.o main.o -oprogram -lm
8890
89-
Nous verrons plus bas les éléments théoriques vous permettant de mieux comprendre ces lignes.
91+
Nous verrons plus bas les éléments théoriques vous permettant de mieux comprendre ces lignes.
9092

9193
Module logiciel
9294
===============
9395

94-
Les applications modernes dépendent souvent de nombreux modules logiciels externes aussi utilisés dans d'autres projets. C'est avantageux à plus d'un titre :
96+
Les applications modernes dépendent souvent de nombreux modules logiciels externes aussi utilisés dans d'autres projets. C'est avantageux à plus d'un titre :
9597

9698
- les modules externes sont sous la responsabilité d'autres développeurs et le programme a développer comporte moins de code ;
9799
- les modules externes sont souvent bien documentés et testés et il est facile de les utiliser ;
98100
- la lisibilité du programme est accrue car il est bien découpé en des ensembles fonctionnels ;
99101
- les modules externes sont réutilisables et indépendants, ils peuvent donc être réutilisés sur plusieurs projets.
100102

101-
Lorsque vous utiliser la fonction ``printf``, vous dépendez d'un module externe nommé ``stdio``. En réalité l'ensemble des modules ``stdio``, ``stdlib``, ``stdint``, ``ctype``... sont tous groupé dans une seule bibliothèque logicielle nommée ``libc`` disponible sur tous les systèmes compatibles POSIX. Sous Linux, le pendant libre ``glibc`` est utilisée. Il s'agit de la biblothèque `GNU C Library <https://fr.wikipedia.org/wiki/GNU_C_Library>`__.
103+
Lorsque vous utiliser la fonction ``printf``, vous dépendez d'un module externe nommé ``stdio``. En réalité l'ensemble des modules ``stdio``, ``stdlib``, ``stdint``, ``ctype``... sont tous groupé dans une seule bibliothèque logicielle nommée ``libc`` disponible sur tous les systèmes compatibles POSIX. Sous Linux, le pendant libre ``glibc`` est utilisée. Il s'agit de la biblothèque `GNU C Library <https://fr.wikipedia.org/wiki/GNU_C_Library>`__.
102104

103-
Un module logiciel peut se composer de fichiers sources, c'est à dire un ensemble de fichiers ``.c`` et ``.h`` ainsi qu'une documentation et un script de compilation (``Makefile``). Alternativement, un module logiciel peut se composer de bibliothèques déjà compilées sous la forme de fichiers ``.h``, ``.a`` et ``.so``. Sous Windows on rencontre fréquemment l'extension ``.dll``. Ces fichiers compilés ne donnent pas accès au code source mais permettent d'utiliser les fonctionnalités quelles offrent dans des programmes C en mettant à disposition un ensemble de fonctions documentées.
105+
Un module logiciel peut se composer de fichiers sources, c'est à dire un ensemble de fichiers ``.c`` et ``.h`` ainsi qu'une documentation et un script de compilation (``Makefile``). Alternativement, un module logiciel peut se composer de bibliothèques déjà compilées sous la forme de fichiers ``.h``, ``.a`` et ``.so``. Sous Windows on rencontre fréquemment l'extension ``.dll``. Ces fichiers compilés ne donnent pas accès au code source mais permettent d'utiliser les fonctionnalités quelles offrent dans des programmes C en mettant à disposition un ensemble de fonctions documentées.
104106

105107
Compilation avec assemblage différé
106108
===================================
107109

108-
Lorsque nous avions compilé notre premier exemple `Hello World <hello>`__ nous avions simplement appelé ``gcc`` avec le fichier source ``hello.c`` qui nous avait créé un exécutable ``a.out``. En réalité, GCC est passé par plusieurs sous étapes de compilation :
110+
Lorsque nous avions compilé notre premier exemple `Hello World <hello>`__ nous avions simplement appelé ``gcc`` avec le fichier source ``hello.c`` qui nous avait créé un exécutable ``a.out``. En réalité, GCC est passé par plusieurs sous étapes de compilation :
109111

110112
1. **Préprocessing** : les commentaires sont retirés, les directives pré-processeur sont remplacées par leur équivalent C.
111113
2. **Compilation** : le code C d'une seule *translation unit* est converti en langage machine en un fichier objet ``.o``.
112114
3. **Édition des liens** : aussi nommé *link*, les différents fichiers objets sont réunis en un seul exécutable.
113115

114-
Lorsqu'un seul fichier est fourni à GCC, les trois opérations sont effectuées en même temps mais ce n'est plus possible aussitôt que le programme est composé de plusieurs unités de translation (plusieurs fichiers C). Il est alors nécessaire de compiler manuellement chaque fichier source et d'en créer.
116+
Lorsqu'un seul fichier est fourni à GCC, les trois opérations sont effectuées en même temps mais ce n'est plus possible aussitôt que le programme est composé de plusieurs unités de translation (plusieurs fichiers C). Il est alors nécessaire de compiler manuellement chaque fichier source et d'en créer.
115117

116-
La figure suivante résume les différentes étapes de GCC. Les pointillés indiquent à quel niveau les opérations peuvent s'arrêter. Il est dès lors possible de passer par des fichiers intermédiaires assembleur (``.s``) ou objets (``.o``) en utilisant la bonne commande.
118+
La figure suivante résume les différentes étapes de GCC. Les pointillés indiquent à quel niveau les opérations peuvent s'arrêter. Il est dès lors possible de passer par des fichiers intermédiaires assembleur (``.s``) ou objets (``.o``) en utilisant la bonne commande.
117119

118120
.. figure:: ../assets/figures/dist/toolchain/gcc.*
119121

120-
Notons que ces étapes existent quelque soit le compilateur ou le système d'exploitation. Nous retrouverons ces exactes mêmes étapes avec Microsoft Visual Studio mais le nom des commandes et les extensions des fichiers peuvent varier s'ils ne respectent pas la norme POSIX (et GNU).
122+
Notons que ces étapes existent quelque soit le compilateur ou le système d'exploitation. Nous retrouverons ces exactes mêmes étapes avec Microsoft Visual Studio mais le nom des commandes et les extensions des fichiers peuvent varier s'ils ne respectent pas la norme POSIX (et GNU).
121123

122-
Notons que généralement, seul deux étapes de GCC sont utilisées :
124+
Notons que généralement, seul deux étapes de GCC sont utilisées :
123125

124126
1. Compilation avec ``gcc -c <fichier.c>``, ceci génère automatiquement un fichier ``.o`` du même nom que le fichier d'entrée.
125-
2. Édition des liens avec ``gcc <fichier1.o> <fichier2.o> ...``, ceci génère automatiquement un fichier exécutable ``a.out``.
127+
2. Édition des liens avec ``gcc <fichier1.o> <fichier2.o> ...``, ceci génère automatiquement un fichier exécutable ``a.out``.
126128

127129
Fichiers d'en-tête (*header*)
128130
=============================
@@ -133,7 +135,7 @@ Les fichiers d'en-tête (``.h``) sont des fichiers écrits en langage C mais qui
133135
- Des déclaration de types (``typedef``, ``struct``).
134136
- Des définitions pré-processeur (``#include``, ``#define``).
135137

136-
Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include`` ne fait qu'inclure le contenu du fichier cible à l'emplacement de la directive. Il est donc possible (mais fort déconseillé), d'avoir la situation suivante :
138+
Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include`` ne fait qu'inclure le contenu du fichier cible à l'emplacement de la directive. Il est donc possible (mais fort déconseillé), d'avoir la situation suivante :
137139

138140
.. code-block:: c
139141
@@ -142,32 +144,32 @@ Nous l'avons vu dans le chapitre sur le pré-processeur, la directive ``#include
142144
#include "foobar.def"
143145
}
144146
145-
Et le fichier ``foobar.def`` pourrait cotenir :
147+
Et le fichier ``foobar.def`` pourrait cotenir :
146148

147149
.. code-block:: c
148150
149-
// foobar.def
150-
#ifdef FOO
151+
// foobar.def
152+
#ifdef FOO
151153
printf("hello foo!\n");
152154
#else
153155
printf("hello bar!\n");
154156
#endif
155157
156-
Vous noterez que l'extension de ``foobar`` n'est pas ``.h`` puisque le contenu n'est pas un fichier d'en-tête. ``.def`` ou n'importe quelle autre extension pourrait donc faire l'affaire ici.
158+
Vous noterez que l'extension de ``foobar`` n'est pas ``.h`` puisque le contenu n'est pas un fichier d'en-tête. ``.def`` ou n'importe quelle autre extension pourrait donc faire l'affaire ici.
157159

158-
Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``foobar.def`` à l'emplacement de la définition ``#include "foobar.def"``. Voyons le en détail :
160+
Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``foobar.def`` à l'emplacement de la définition ``#include "foobar.def"``. Voyons le en détail :
159161

160-
.. code-block:: c
162+
.. code-block:: console
161163
162164
$ cat << EOF > main.c
163165
→ int main() {
164166
→ #include "foobar.def"
165-
→ #include "foobar.def"
167+
→ #include "foobar.def"
166168
→ }
167169
→ EOF
168170
169171
$ cat << EOF > foobar.def
170-
→ #ifdef FOO
172+
→ #ifdef FOO
171173
→ printf("hello foo!\n");
172174
→ #else
173175
→ printf("hello bar!\n");
@@ -177,12 +179,12 @@ Dans cet exemple, le pré-processeur ne fait qu'inclure le contenu du fichier ``
177179
$ gcc -E main.c | sed '/^#/ d'
178180
int main() {
179181
printf("hello bar\n");
180-
printf("hello bar\n");
182+
printf("hello bar\n");
181183
}
182184
183185
Lorsque l'on observe le résultat du pré-processeur, on s'aperçois que toutes les directives préprocesseur ont disparues et que la directive ``#include`` a été remplacée par de contenu de ``foobar.def``. Remarquons que le fichier est inclus deux fois, nous verrons plus loin comme éviter cela.
184186

185-
Nous avons vu au chapitre sur les `prototypes de fonctions <function_prototype>`__ qu'il est possible de ne déclarer que la première ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est composé une fonction sans nécessairement disposer de l'implémentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-tête des déclaration en amont (*forward declaration*). Dans le fichier d'en-tête ``stdio.h`` on trouvera la ligne : ``int printf( const char *restrict format, ... );``.
187+
Nous avons vu au chapitre sur les `prototypes de fonctions <function_prototype>`__ qu'il est possible de ne déclarer que la première ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est composé une fonction sans nécessairement disposer de l'implémentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-tête des déclaration en amont (*forward declaration*). Dans le fichier d'en-tête ``stdio.h`` on trouvera la ligne : ``int printf( const char *restrict format, ... );``.
186188

187189
.. code-block::c
188190
@@ -201,7 +203,7 @@ Un fichier d'en-tête contiendra donc tout le nécessaire utile à pouvoir utili
201203
Protection de réentrance
202204
------------------------
203205

204-
La protection de réentrence aussi nommée *header guards* est une solution au problème d'inclusion multiple. Si par exemple on défini dans un fichier d'en-tête un nouveau type et que l'on inclus ce fichier, mais que ce dernier est déjà inclu par une autre bibliothèque une erreur de compilation apparaîtera :
206+
La protection de réentrence aussi nommée *header guards* est une solution au problème d'inclusion multiple. Si par exemple on défini dans un fichier d'en-tête un nouveau type et que l'on inclus ce fichier, mais que ce dernier est déjà inclu par une autre bibliothèque une erreur de compilation apparaîtera :
205207

206208
.. code-block:: console
207209
@@ -242,7 +244,7 @@ La protection de réentrence aussi nommée *header guards* est une solution au p
242244
^~~
243245
...
244246
245-
Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà inclus avec ``foo.h`` et le résultat après pré-processing est le suivant :
247+
Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà inclus avec ``foo.h`` et le résultat après pré-processing est le suivant :
246248

247249
.. code-block:: console
248250
@@ -260,11 +262,11 @@ Dans cet exemple l'utilisateur ne sait pas forcément que ``bar.h`` est déjà i
260262
foo(bar);
261263
}
262264
263-
On y retrouve la définition de ``Bar`` deux fois et donc, le compilateur génère une erreur.
265+
On y retrouve la définition de ``Bar`` deux fois et donc, le compilateur génère une erreur.
264266

265-
Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par exemple avec ceci:
267+
Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par exemple avec ceci:
266268

267-
.. code-block:: c
269+
.. code-block:: c
268270
269271
#ifndef BAR_H
270272
#define BAR_H
@@ -275,16 +277,16 @@ Une solution à ce problème est d'ajouter des gardes d'inclusion multiple par e
275277
276278
#endif // BAR_H
277279
278-
Si aucune définition du type ``#define BAR_H`` n'existe, alors le fichier ``bar.h`` n'a jamais été inclus auparavant et le contenu de la directive ``#ifndef BAR_H`` dans lequel on commence par définir ``BAR_H`` est exécuté. Lors d'une future inclusion de ``bar.h``, la valeur de ``BAR_H`` aura déjà été définie et le contenu de la directive ``#ifndef BAR_H`` ne sera jamais exécuté.
280+
Si aucune définition du type ``#define BAR_H`` n'existe, alors le fichier ``bar.h`` n'a jamais été inclus auparavant et le contenu de la directive ``#ifndef BAR_H`` dans lequel on commence par définir ``BAR_H`` est exécuté. Lors d'une future inclusion de ``bar.h``, la valeur de ``BAR_H`` aura déjà été définie et le contenu de la directive ``#ifndef BAR_H`` ne sera jamais exécuté.
279281

280282
Alternativement, il existe une solution **non standard** mais supportée par la plupart des compilateurs. Elle fait intervenir un pragma :
281283

282-
.. code-block:: c
284+
.. code-block:: c
283285
284286
#pragma once
285287
286288
typedef struct Bar {
287289
int b, a, r;
288290
} Bar;
289291
290-
Cette solution est équivalente à la méthode traditionnelle et présente plusieurs avantages. C'est tout d'abord une solution atomique qui ne nécessite pas un ``#endif`` à la fin du fichier. Il n'y a ensuite pas de conflit avec la règle SSOT car le nom du fichier ``bar.h`` n'apparaît pas dans le fichier ``BAR_H``.
292+
Cette solution est équivalente à la méthode traditionnelle et présente plusieurs avantages. C'est tout d'abord une solution atomique qui ne nécessite pas un ``#endif`` à la fin du fichier. Il n'y a ensuite pas de conflit avec la règle SSOT car le nom du fichier ``bar.h`` n'apparaît pas dans le fichier ``BAR_H``.

‎content/82-testing.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Considérons le programme suivant :
6161
6262
A priori, c'est un programme tout à fait correct. Si l'utilisateur entre le bon mot de passe, il se voit octroyé des privilèges administrateurs. Testons ce programme :
6363

64-
.. code-block:: c
64+
.. code-block:: console
6565
6666
$ gcc u.c -fno-stack-protector
6767
$ ./a.out
@@ -71,7 +71,7 @@ A priori, c'est un programme tout à fait correct. Si l'utilisateur entre le bon
7171
7272
Très bien, maintenant testons avec un mauvais mot de passe :
7373

74-
.. code-block:: c
74+
.. code-block:: console
7575
7676
$ ./a.out
7777
Password: startrek

‎content/85-data-structures.rst

+684-85
Large diffs are not rendered by default.

‎content/90-advanced-topics.rst

+175-1
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,178 @@ On voit immédiatement que la partie entière vaut 2, donc 90% de 3.14 donnera u
228228
inline int16_t q12_mul(int16_t a, int16_t b)
229229
{
230230
return q_mul(a, b, 12);
231-
}
231+
}
232+
233+
Mémoire partagée
234+
================
235+
236+
Nous le verrons plus loin au chapitre sur la MMU, mais la mémoire d'un processus mémoire (programme) ne peut pas être accedée par un autre programme. Le système d'exploitation l'en empêche.
237+
238+
Lorsque l'on souhaite communiquer entre plusieurs programmes, il est possible d'utiliser différentes méthodes :
239+
240+
- les flux (fichiers, stdin, stdout...)
241+
- la mémoire partagée
242+
- les sockets
243+
244+
Vous avez déjà vu les flux au chapitre précédant, et les sockets ne font pas partie de ce cours d'introduction.
245+
246+
Notons que la mémoire partagée est un mécanisme propre à chaque système d'exploitation. Sous POSIX elle est normalisée et donc un programme compatible POSIX et utilisant la mémoire partagée pourra fonctionner sous Linux, WSL ou macOS, mais pas sous Windows.
247+
248+
C'est principalement l'appel système ``mmap`` qui est utilisé. Il permet de mapper ou démapper des fichiers ou des périphériques dans la mémoire.
249+
250+
.. code-block:: c
251+
252+
void *mmap(
253+
void *addr,
254+
size_t length, // Taille en byte de l'espace mémoire
255+
int prot, // Protection d'accès (lecture, écriture, exécution)
256+
int flags, // Attributs (partagé, privé, anonyme...)
257+
int fd,
258+
off_t offset
259+
);
260+
261+
262+
Voici un exemple permettant de réserver un espace partagé en écriture et en lecture entre deux processus :
263+
264+
.. code-block:: c
265+
266+
#include <stdio.h>
267+
#include <stdlib.h>
268+
#include <sys/mman.h>
269+
270+
void* create_shared_memory(size_t size) {
271+
// Accessible en lecture et écriture
272+
int protection = PROT_READ | PROT_WRITE;
273+
274+
// D'autres processus peuvent accéder à cet espace
275+
// lequel est anonyme
276+
// so only this process and its children will be able to use it:
277+
int visibility = MAP_SHARED | MAP_ANONYMOUS;
278+
279+
// The remaining parameters to `mmap()` are not important for this use case,
280+
// but the manpage for `mmap` explains their purpose.
281+
return mmap(NULL, size, protection, visibility, -1, 0);
282+
}
283+
284+
File memory mapping
285+
-------------------
286+
287+
Traditionnellement lorsque l'on souhaite travailler sur un fichier, il convient de l'ouvrir avec ``fopen`` et de lire son contenu. Lorsque cela est nécessaire, ce fichier est copié en mémoire :
288+
289+
.. code-block:: c
290+
291+
FILE *fp = fopen("foo", "r");
292+
fseek(fp, 0, SEEK_END);
293+
int filesize = ftell(fp);
294+
fseek(fp, 0, SEEK_SET);
295+
char *file = malloc(filesize);
296+
fread(file, filesize, sizeof(char), fp);
297+
fclose(fp);
298+
299+
Cette copie n'est pas nécessairement nécessaire. Une approche **POSIX**, qui n'est donc pas couverte par le standard **C99** consiste à lier le fichier dans un espace mémoire partagé.
300+
301+
Ceci nécessite l'utilisation de fonctions bas niveau.
302+
303+
.. code-block:: c
304+
305+
#include <stdio.h>
306+
#include <stdlib.h>
307+
#include <sys/mman.h>
308+
309+
int main() {
310+
int fd = open("foo.txt", O_RDWR, 0600);
311+
char *addr = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
312+
printf("Espace mappé à %p\n", addr);
313+
printf("Premiers caractères du fichiers : %.*s...\n", 20, addr);
314+
}
315+
316+
Les avantages de cette méthode sont :
317+
318+
- pas nécessaire de copier l'intégralité du fichier en mémoire ;
319+
- possibilité de partager le même fichier ouvert entre plusieurs processus ;
320+
- possibilité laissée au système d'exploitation d'utiliser la RAM ou non si les ressources mémoires deviennent tendues.
321+
322+
Collecteur de déchets (*garbage collector*)
323+
===========================================
324+
325+
Le C est un langage primitif qui ne gère pas automatiquement la libération des ressources allouées dynamiquement. L'exemple suivant est évocateur :
326+
327+
.. code-block:: c
328+
329+
int* get_number() {
330+
int *num = malloc(sizeof(int));
331+
*num = rand();
332+
}
333+
334+
int main() {
335+
for (int i = 0; i < 100; i++) {
336+
printf("%d\n", *get_number());
337+
}
338+
}
339+
340+
La fonction ``get_number`` alloue dynamiquement un espace de la taille d'un entier et lui assigne une valeur aléatoire. Dans le programme principal, l'adresse retournée est déréférencée pour être affichée sur la sortie standard.
341+
342+
A la fin de l'exécution de la boucle for, une centaine d'espaces mémoire sont maintenant dans les `limbes <https://fr.wikipedia.org/wiki/Limbes>`__. Comme le pointeur retourné n'a jamais été mémorisé, il n'est plus possible de libérer cet espace mémoire avec ``free``.
343+
344+
On dit que le programme à une `fuite mémoire <https://fr.wikipedia.org/wiki/Fuite_de_m%C3%A9moire>`__. En admettant que ce programme reste résidant en mémoire, il peut arriver un moment où le programme peut aller jusqu'à utiliser toute la RAM disponible. Dans ce cas, il est probable que ``malloc`` retourne ``NULL`` et qu'une erreur de segmentaiton apparaisse lors du ``printf``.
345+
346+
Allons plus loin dans notre exemple et considérons le code suivant :
347+
348+
.. code-block:: c
349+
350+
#include <stdio.h>
351+
#include <stdlib.h>
352+
353+
int foo(int *new_value) {
354+
static int *values[10] = { NULL };
355+
static int count = 0;
356+
357+
if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {
358+
values[count++] = new_value;
359+
}
360+
361+
if (count > 0)
362+
printf("Foo aime %d\n", *values[rand() % count]);
363+
}
364+
365+
int bar(int *new_value) {
366+
static int *values[10] = { NULL };
367+
static int count = 0;
368+
369+
if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {
370+
values[count++] = new_value;
371+
}
372+
373+
if (count > 0)
374+
printf("Bar aime %d\n", *values[rand() % count]);
375+
}
376+
377+
int* get_number() {
378+
int *number = malloc(sizeof(int));
379+
*number = rand() % 1000;
380+
return number;
381+
}
382+
383+
int main() {
384+
int experiment_iterations = 10;
385+
for (int i = 0; i < experiment_iterations; i++) {
386+
int *num = get_number();
387+
foo(num);
388+
bar(num);
389+
#if 0 // ...
390+
free(num) ??
391+
#endif
392+
};
393+
}
394+
395+
La fonction ``get_number`` alloue dynamiquement un espace mémoire et assigne un nombre aléatoire. Les fonctions ``foo`` et ``bar`` reçoivent en paramètre un pointeur sur un entier. Chacune à le choix de mémoriser ce pointeur et de clamer sur ``stdout`` qu'elle aime un des nombre mémorisés.
396+
397+
Au niveau du ``#if 0`` dans la fonction ``main``, il est impossible de savoir si l'adresse pointée par ``num`` est encore utilisée ou non. Il se peut que ``foo`` et ``bar`` utilisent cet espace mémoire, comme il se peut qu'aucun des deux ne l'utilise.
398+
399+
Comment peut-on savoir si il est possible de libérer ou non ``num`` ?
400+
401+
Une solution courament utilsée en C++ s'appelle un *smart pointer*. Il s'agit d'un pointeur qui contient en plus de l'adresse de la valeur, le nombre de références utilisées. De cette manière il est possible en tout temps de savoir si le pointeur est référencé quelque part. Dans le cas ou le nombre de référence tombe à zéro, il est possible de libérer la ressource.
402+
403+
Dans un certain nombre de langage de programmation comme Python ou Java, il existe un mécanisme automatique nommé *Garbage Collector* et qui, périodiquement, fait un tour de toutes les allocations dynamique pour savoir si elle sont encore référencées ou non. Le cas échéant, le *gc* décide libérer la ressource mémoire. De cette manière il n'est plus nécessaire de faire la chasse aux ressources allouées.
404+
405+
En revanche en C, il n'existe aucun mécanisme aussi sophistiqués alors prenez garde à bien libérer les ressources utilisée et à éviter d'écrire des fonctions qui allouent du contenu mémoire dynamiquement.

‎index.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
.. raw:: latex
3+
24
\frontmatter
35

46
=====================
@@ -17,7 +19,8 @@ Le contenu de ce cours est calqué sur les fiches d'unités de cours et de modul
1719
- Unité **Informatique 1** (:ref:`Info1 <info1>`)
1820
- Unité **Informatique 2** (:ref:`Info2 <info2>`)
1921

20-
La version actuelle est |release| générée le |today|.
22+
La version actuelle : |release|
23+
Date : |today|
2124

2225
.. raw:: latex
2326

0 commit comments

Comments
 (0)
Please sign in to comment.