diff --git a/404.html b/404.html index 9df8bdae..0e2f9eff 100644 --- a/404.html +++ b/404.html @@ -1562,6 +1562,8 @@ + + @@ -1741,6 +1743,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/appendix/bibliography/index.html b/appendix/bibliography/index.html index 7ce42fb1..9d90a923 100644 --- a/appendix/bibliography/index.html +++ b/appendix/bibliography/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/appendix/grammar/index.html b/appendix/grammar/index.html index 497b4873..e1fbaab9 100644 --- a/appendix/grammar/index.html +++ b/appendix/grammar/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/appendix/laboratories/index.html b/appendix/laboratories/index.html index 448fd2ed..fad51148 100644 --- a/appendix/laboratories/index.html +++ b/appendix/laboratories/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/appendix/refcards/index.html b/appendix/refcards/index.html index 6ab32a10..adb1f703 100644 --- a/appendix/refcards/index.html +++ b/appendix/refcards/index.html @@ -1582,6 +1582,8 @@ + + @@ -1761,6 +1763,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/appendix/unit/index.html b/appendix/unit/index.html index c11cd0b4..dbd7213e 100644 --- a/appendix/unit/index.html +++ b/appendix/unit/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/assets/images/arctic-world-archive.png b/assets/images/arctic-world-archive.png new file mode 100644 index 00000000..b9adfef7 Binary files /dev/null and b/assets/images/arctic-world-archive.png differ diff --git a/assets/images/bipartite.drawio b/assets/images/bipartite.drawio new file mode 100644 index 00000000..90804a5b --- /dev/null +++ b/assets/images/bipartite.drawio @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/forest.drawio b/assets/images/forest.drawio new file mode 100644 index 00000000..8e8d7626 --- /dev/null +++ b/assets/images/forest.drawio @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/oriented.drawio b/assets/images/oriented.drawio new file mode 100644 index 00000000..82b5710b --- /dev/null +++ b/assets/images/oriented.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wav.drawio b/assets/images/wav.drawio index f41d5184..c37e9d97 100644 --- a/assets/images/wav.drawio +++ b/assets/images/wav.drawio @@ -1,253 +1,253 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/assets/images/weighted.drawio b/assets/images/weighted.drawio new file mode 100644 index 00000000..a4aaa8dd --- /dev/null +++ b/assets/images/weighted.drawio @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/course-c/00-preface/index.html b/course-c/00-preface/index.html index f86ecb47..349ffb4f 100644 --- a/course-c/00-preface/index.html +++ b/course-c/00-preface/index.html @@ -1593,6 +1593,8 @@ + + @@ -1772,6 +1774,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/05-introduction/c-lang/index.html b/course-c/05-introduction/c-lang/index.html index 9cc6f168..6df687fb 100644 --- a/course-c/05-introduction/c-lang/index.html +++ b/course-c/05-introduction/c-lang/index.html @@ -1788,6 +1788,8 @@ + + @@ -1967,6 +1969,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/05-introduction/code-of-conduct/index.html b/course-c/05-introduction/code-of-conduct/index.html index d78f5de6..f99103d1 100644 --- a/course-c/05-introduction/code-of-conduct/index.html +++ b/course-c/05-introduction/code-of-conduct/index.html @@ -1727,6 +1727,8 @@ + + @@ -1906,6 +1908,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/05-introduction/me-and-my-computer/index.html b/course-c/05-introduction/me-and-my-computer/index.html index 99d28a75..54fb9c2a 100644 --- a/course-c/05-introduction/me-and-my-computer/index.html +++ b/course-c/05-introduction/me-and-my-computer/index.html @@ -1711,6 +1711,8 @@ + + @@ -1890,6 +1892,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/05-introduction/programming/index.html b/course-c/05-introduction/programming/index.html index d7b516ff..5191bc0b 100644 --- a/course-c/05-introduction/programming/index.html +++ b/course-c/05-introduction/programming/index.html @@ -1810,6 +1810,8 @@ + + @@ -1989,6 +1991,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/10-numeration/bases/index.html b/course-c/10-numeration/bases/index.html index 46b4b59e..fb45045d 100644 --- a/course-c/10-numeration/bases/index.html +++ b/course-c/10-numeration/bases/index.html @@ -1705,6 +1705,8 @@ + + @@ -1884,6 +1886,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/10-numeration/data/index.html b/course-c/10-numeration/data/index.html index 3047bef7..53898b80 100644 --- a/course-c/10-numeration/data/index.html +++ b/course-c/10-numeration/data/index.html @@ -1705,6 +1705,8 @@ + + @@ -1884,6 +1886,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6187,6 +6216,7 @@

    Transmission de l'informationPérennité de l'information¤

    La pérennité de l'information représente un défi tout aussi crucial que sa transmission. En effet, les supports physiques sur lesquels nous conservons nos données sont souvent fragiles et périssables. Le papier, qui a servi de base à la transmission du savoir pendant des siècles, s'use et se dégrade au fil du temps, même lorsqu'il est conservé dans des conditions optimales. Les bandes magnétiques, autrefois utilisées pour stocker de grandes quantités de données numériques, ont une durée de vie limitée, avec une détérioration progressive de l'information qu'elles contiennent. De même, les CD et les DVD, longtemps perçus comme des solutions de stockage robustes, ont montré leurs limites : leur surface peut se corroder, entraînant une perte irrémédiable des données.

    +

    Arctic World Archive
    Arctic World Archive

    Consciente de ces limitations, l'humanité a entrepris de consolider ses connaissances en les stockant dans des endroits spécialement conçus pour résister à l'épreuve du temps. Un des projets les plus ambitieux en la matière est l'Arctic World Archive (AWA), situé dans l'archipel de Svalbard, en Norvège, à proximité du Global Seed Vault. L'Arctic World Archive, ouvert en 2017, est une installation sécurisée dans une ancienne mine de charbon, enfouie sous des centaines de mètres de pergélisol. Ce projet vise à préserver les données numériques pour des siècles, voire des millénaires, en utilisant une technologie de stockage sur PiqlFilm, un film photosensible spécialement conçu pour garantir la durabilité des informations sans nécessiter d'électricité ou de maintenance continue.

    Des institutions du monde entier, telles que les Archives nationales du Brésil, l'Agence spatiale européenne (ESA) et même GitHub, y ont déjà déposé des données importantes. Par exemple, GitHub a archivé 21 téraoctets de données provenant de l'ensemble des dépôts publics actifs de la plateforme, garantissant ainsi la pérennité de précieuses ressources en code source pour les générations futures (voir GitHub Archive Program). Ainsi, Svalbard, avec l'Arctic World Archive, s'affirme comme un bastion de la conservation des connaissances numériques, soulignant l'importance de la préservation des données dans un monde où la technologie évolue rapidement, mais où la fiabilité des supports de stockage demeure un enjeu majeur.

    @@ -6211,7 +6241,7 @@

    Pérennité de l'information - 24 septembre 2024 + 27 septembre 2024 diff --git a/course-c/10-numeration/index.html b/course-c/10-numeration/index.html index 3ae5a785..76330724 100644 --- a/course-c/10-numeration/index.html +++ b/course-c/10-numeration/index.html @@ -1593,6 +1593,8 @@ + + @@ -1772,6 +1774,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/10-numeration/numbers/index.html b/course-c/10-numeration/numbers/index.html index 20cd5523..31220177 100644 --- a/course-c/10-numeration/numbers/index.html +++ b/course-c/10-numeration/numbers/index.html @@ -1822,6 +1822,8 @@ + + @@ -2001,6 +2003,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/control-structures/index.html b/course-c/15-fundations/control-structures/index.html index bae576ac..bc9ba098 100644 --- a/course-c/15-fundations/control-structures/index.html +++ b/course-c/15-fundations/control-structures/index.html @@ -1872,6 +1872,8 @@ + + @@ -2051,6 +2053,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/darkside/index.html b/course-c/15-fundations/darkside/index.html index 164c8462..61953a47 100644 --- a/course-c/15-fundations/darkside/index.html +++ b/course-c/15-fundations/darkside/index.html @@ -1593,6 +1593,8 @@ + + @@ -1772,6 +1774,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/datatype/index.html b/course-c/15-fundations/datatype/index.html index d0d9b3b0..240b7d9e 100644 --- a/course-c/15-fundations/datatype/index.html +++ b/course-c/15-fundations/datatype/index.html @@ -1905,6 +1905,8 @@ + + @@ -2084,6 +2086,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/functions/index.html b/course-c/15-fundations/functions/index.html index 36d9f0cd..0acdc613 100644 --- a/course-c/15-fundations/functions/index.html +++ b/course-c/15-fundations/functions/index.html @@ -1778,6 +1778,8 @@ + + @@ -1957,6 +1959,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/grammar/index.html b/course-c/15-fundations/grammar/index.html index ee0cc7e5..b3ee0e11 100644 --- a/course-c/15-fundations/grammar/index.html +++ b/course-c/15-fundations/grammar/index.html @@ -1650,6 +1650,8 @@ + + @@ -1829,6 +1831,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -5938,37 +5967,38 @@

    La grammaire | <enum_specifier> | <typedef_name> -<struct_or_union_specifier> ::= <struct_or_union> <identifier> { {<struct_declaration>}+ } - | <struct_or_union> { {<struct_declaration>}+ } - | <struct_or_union> <identifier> - -<struct_or_union> ::= "struct" - | "union" - -<struct_declaration> ::= {<specifier_qualifier>}* <struct_declarator_list> - -<specifier_qualifier> ::= <type_specifier> - | <type_qualifier> - -<struct_declarator_list> ::= <struct_declarator> - | <struct_declarator_list> "," <struct_declarator> - -<struct_declarator> ::= <declarator> - | <declarator> ":" <constant_expression> - | ":" <constant_expression> - -<declarator> ::= {<pointer>}? <direct_declarator> - -<pointer> ::= * {<type_qualifier>}* {<pointer>}? - -<type_qualifier> ::= "const" - | "volatile" - -<direct_declarator> ::= <identifier> - | ( <declarator> ) - | <direct_declarator> "[" {<constant_expression>}? "]" - | <direct_declarator> "(" <parameter_type_list> ")" - | <direct_declarator> "(" {<identifier>}* ")" +<struct_or_union_specifier> ::= <struct_or_union> <identifier> { + {<struct_declaration>}+ } + | <struct_or_union> { {<struct_declaration>}+ } + | <struct_or_union> <identifier> + +<struct_or_union> ::= "struct" + | "union" + +<struct_declaration> ::= {<specifier_qualifier>}* <struct_declarator_list> + +<specifier_qualifier> ::= <type_specifier> + | <type_qualifier> + +<struct_declarator_list> ::= <struct_declarator> + | <struct_declarator_list> "," <struct_declarator> + +<struct_declarator> ::= <declarator> + | <declarator> ":" <constant_expression> + | ":" <constant_expression> + +<declarator> ::= {<pointer>}? <direct_declarator> + +<pointer> ::= * {<type_qualifier>}* {<pointer>}? + +<type_qualifier> ::= "const" + | "volatile" + +<direct_declarator> ::= <identifier> + | ( <declarator> ) + | <direct_declarator> "[" {<constant_expression>}? "]" + | <direct_declarator> "(" <parameter_type_list> ")" + | <direct_declarator> "(" {<identifier>}* ")"

    Ce que l'on observe par exemple c'est que la grammaire du C est récursive. Cela signifie que l'on peut définir un élément en fonction de lui-même. Par exemple, un declarator peut contenir un pointer qui peut lui-même contenir un pointer.

    Comment est-ce que cela fonctionne derrière les coulisses ?

    @@ -6012,7 +6042,7 @@

    Définir mon propre langage - 20 septembre 2024 + 27 septembre 2024 diff --git a/course-c/15-fundations/operators/index.html b/course-c/15-fundations/operators/index.html index 23b5f449..f14db401 100644 --- a/course-c/15-fundations/operators/index.html +++ b/course-c/15-fundations/operators/index.html @@ -1871,6 +1871,8 @@ + + @@ -2050,6 +2052,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/preprocessor/index.html b/course-c/15-fundations/preprocessor/index.html index e5a9d986..606f35e7 100644 --- a/course-c/15-fundations/preprocessor/index.html +++ b/course-c/15-fundations/preprocessor/index.html @@ -1915,6 +1915,8 @@ + + @@ -2094,6 +2096,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/scope/index.html b/course-c/15-fundations/scope/index.html index 480164f9..b1106036 100644 --- a/course-c/15-fundations/scope/index.html +++ b/course-c/15-fundations/scope/index.html @@ -1783,6 +1783,8 @@ + + @@ -1962,6 +1964,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/stdio/index.html b/course-c/15-fundations/stdio/index.html index 22857a44..65967677 100644 --- a/course-c/15-fundations/stdio/index.html +++ b/course-c/15-fundations/stdio/index.html @@ -1806,6 +1806,8 @@ + + @@ -1985,6 +1987,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/15-fundations/syntax/index.html b/course-c/15-fundations/syntax/index.html index 7cec94dd..1976d4cb 100644 --- a/course-c/15-fundations/syntax/index.html +++ b/course-c/15-fundations/syntax/index.html @@ -1827,6 +1827,8 @@ + + @@ -2006,6 +2008,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6375,23 +6404,22 @@

    Fin de lignes (EOL: End Of Line) il faut utiliser, faite preuve de bon sens et surtout, soyez cohérent.

    Mots clés¤

    -

    Le langage de programmation C tel que défini par C11 comporte environ 37 mots clés :

    -
    _Bool       do        int       switch
    -_Complex    double    long      typedef
    -_Imaginary  else      register  union
    -auto        enum      restrict  unsigned
    -break       extern    return    void
    -case        float     short     volatile
    -char        for       signed    while
    -const       goto      sizeof
    -continue    if        static
    -default     inline    struct
    +

    Le langage de programmation C tel que défini par C17 comporte 44 mots clés :

    +
    auto       break      case       char       const
    +continue   default    do         double     else
    +enum       extern     float      for        goto
    +if         inline     int        long       register
    +restrict   return     short      signed     sizeof
    +static     struct     switch     typedef    union
    +unsigned   void       volatile   while      _Alignas
    +_Alignof   _Atomic    _Bool      _Complex   _Generic
    +_Imaginary _Noreturn  _Static_assert        _Thread_local
     

    Dans ce cours, l'usage des mots clés suivants est découragé, car leur utilisation pourrait prêter à confusion ou mener à des inélégances d'écriture.

    _Bool, _imaginary, auto, goto, inline, long, register, restrict, short
     
    -

    Il n'y a donc plus que 28 mots clés à connaître pour être un bon développeur C.

    +

    Il n'y a donc plus que 35 mots clés à connaître pour être un bon développeur C.

    Notons que les mots clés true et false ne sont pas standardisés en C, mais ils le sont en C++.

    Ces mots clés font partie intégrante de la grammaire du langage et ne peuvent être utilisés pour identifier des variables, des fonctions ou des étiquettes.

    diff --git a/course-c/20-composite-types/arrays/index.html b/course-c/20-composite-types/arrays/index.html index 36e83de4..9e5f1d75 100644 --- a/course-c/20-composite-types/arrays/index.html +++ b/course-c/20-composite-types/arrays/index.html @@ -1749,6 +1749,8 @@ + + @@ -1928,6 +1930,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/20-composite-types/generic-programming/index.html b/course-c/20-composite-types/generic-programming/index.html index 648a2e12..800fdfb5 100644 --- a/course-c/20-composite-types/generic-programming/index.html +++ b/course-c/20-composite-types/generic-programming/index.html @@ -1683,6 +1683,8 @@ + + @@ -1862,6 +1864,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/20-composite-types/index.html b/course-c/20-composite-types/index.html index 545eeea3..d0fa51d1 100644 --- a/course-c/20-composite-types/index.html +++ b/course-c/20-composite-types/index.html @@ -1593,6 +1593,8 @@ + + @@ -1772,6 +1774,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -5815,26 +5844,25 @@

    Types Composites¤

    -

    Un type composite est un type de données qui est construit à partir d'autres types de données plus simples ou primitifs (comme int, char, etc.). Ces types permettent de regrouper plusieurs éléments de données sous une seule entité, ce qui est essentiel pour organiser des structures de données plus complexes dans les programmes.

    -

    En C on retrouve les types composites suivants :

    +

    Un type composite est un type de données qui est construit à partir d'autres types de données plus simples ou primitifs (comme int, char, etc.). Ces types permettent de regrouper plusieurs éléments de données sous une seule entité, ce qui est essentiel pour organiser des structures de données plus complexes dans les programmes. En C on retrouve les types composites suivants :

    -
    Les tableaux
    +
    Les tableaux

    Un tableau est une collection d'éléments de même type organisée de manière continuë en mémoire.

    -
    Les structures (struct)
    +
    Les structures (struct)

    Une structure est un type composite qui regroupe des éléments de données, appelés membres qui peuvent être de types différents.

    -
    Les unions
    +
    Les unions

    Une union est similaire à une structure, mais tous les membres partagent la même zone mémoire. Cela signifie qu'une union ne peut stocker qu'une seule valeur à la fois parmi ses membres.

    -
    Les énumérations (enum)
    +
    Les énumérations (enum)

    Une énumération est un type composite qui associe des noms symboliques à des valeurs intégrales. Bien que techniquement les énumérations soient des types scalaires, elles sont souvent considérées dans le cadre des types composites en raison de leur capacité à représenter des ensembles de valeurs possibles.

    -
    Les chaînes de caractères
    +
    Les chaînes de caractères

    Les chaînes de caractères sont techniquement des tableaux de caractères (char), mais elles peuvent être considérées comme un type composite en raison de la manière dont elles sont manipulées et utilisées pour représenter du texte.

    @@ -5862,7 +5890,7 @@

    Types Composites - 2 septembre 2024 + 27 septembre 2024 diff --git a/course-c/20-composite-types/pointers/index.html b/course-c/20-composite-types/pointers/index.html index 5dcf9ba1..d0b19ecb 100644 --- a/course-c/20-composite-types/pointers/index.html +++ b/course-c/20-composite-types/pointers/index.html @@ -1928,6 +1928,8 @@ + + @@ -2107,6 +2109,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -7626,7 +7655,7 @@

    Exercices de révision - 2 septembre 2024 + 27 septembre 2024 diff --git a/course-c/20-composite-types/strings/index.html b/course-c/20-composite-types/strings/index.html index d241e18c..aa9c393c 100644 --- a/course-c/20-composite-types/strings/index.html +++ b/course-c/20-composite-types/strings/index.html @@ -1694,6 +1694,8 @@ + + @@ -1873,6 +1875,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/20-composite-types/structures/index.html b/course-c/20-composite-types/structures/index.html index 5a0a5c3d..6da9113a 100644 --- a/course-c/20-composite-types/structures/index.html +++ b/course-c/20-composite-types/structures/index.html @@ -1727,6 +1727,8 @@ + + @@ -1906,6 +1908,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/20-composite-types/unions/index.html b/course-c/20-composite-types/unions/index.html index 5d44dbb3..5ad96b30 100644 --- a/course-c/20-composite-types/unions/index.html +++ b/course-c/20-composite-types/unions/index.html @@ -1650,6 +1650,8 @@ + + @@ -1829,6 +1831,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/25-architecture-and-systems/abi/index.html b/course-c/25-architecture-and-systems/abi/index.html new file mode 100644 index 00000000..3a99a5ce --- /dev/null +++ b/course-c/25-architecture-and-systems/abi/index.html @@ -0,0 +1,6320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ABI - L'informatique pour ingénieurs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + +
    + + +
    + +
    + + + + + + + + + +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +

    ABI¤

    +

    Une Application Binary Interface (ABI) est une interface entre deux modules de code binaire, souvent un programme et une bibliothèque, au niveau de l'assembleur. L'ABI définit comment les fonctions, les données et les structures sont organisées et accessibles dans le code binaire. Elle spécifie des conventions pour les appels de fonctions, la gestion de la mémoire, le format des données, etc.

    +

    Nous avons vu par exemple que l'appel d'une fonction en C peut être réalisé en utilisant la pile. Mais comment est-ce que cela fonctionne derrière les coulisses ? L'ABI définit comment les arguments sont passés à une fonction, comment les valeurs de retour sont retournées, comment les variables locales sont stockées, etc. Elle définit quels sont les registres qui doivent être sauvegardés avant un appel de fonction et ceux qui sont utilisés pour passer des arguments. En somme, c'est une convention qui permet à des modules de code binaire de communiquer entre eux.

    +

    En outre, sur un système d'exploitation, un programme communique avec le noyau du système d'exploitation en utilisant des appels système. L'ABI définit comment ces appels système sont réalisés. Par exemple, sur Linux, les appels système sont réalisés en utilisant le registre eax pour passer le numéro de l'appel système et les arguments sont passés dans les registres ebx, ecx, edx, esi, edi, ebp. Le résultat de l'appel système est retourné dans le registre eax.

    +

    L'ABI est donc spécifique à une architecture matérielle et à un système d'exploitation. Par exemple, l'ABI pour les programmes Linux sur une architecture x86-64 est différente de celle pour les programmes Windows sur la même architecture. Cela signifie que les programmes compilés pour une architecture et un système d'exploitation ne peuvent pas être exécutés sur une autre architecture ou un autre système d'exploitation car ces conventions ne sont pas respectées. C'est pourquoi il est important de connaître l'ABI de la plateforme cible lors de la compilation d'un programme.

    +

    ELF¤

    +

    Le format de fichier exécutable et de liaison (Executable and Linkable Format, ELF) est un format de fichier binaire standard pour les fichiers exécutables, les fichiers objet, les bibliothèques partagées et les fichiers de base de données de débogage. Il est utilisé sur la plupart des systèmes d'exploitation Unix et Unix-like, y compris Linux, Solaris, FreeBSD, etc.

    +

    Lorsque vous compilez un programme avec GCC, le compilateur génère un fichier binaire au format ELF. Vous pouvez en avoir la preuve avec la commande file sur un programme compilé avec gcc sous Linux :

    +
    $ file a.out
    +a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
    +dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
    +BuildID[sha1]=2066ecb2b4fed0b91adbcc63d0e7c13a8bea14a8,
    +for GNU/Linux 3.2.0, not stripped
    +
    +

    On peut également consulter le contenu d'un fichier ELF avec la commande readelf:

    +
    $ readelf -h a.out
    +ELF Header:
    +  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
    +  Class:                             ELF64
    +  Data:                              2's complement, little endian
    +  Version:                           1 (current)
    +  OS/ABI:                            UNIX - System V
    +  ABI Version:                       0
    +  Type:                              DYN (Position-Independent Executable file)
    +  Machine:                           Advanced Micro Devices X86-64
    +  Version:                           0x1
    +  Entry point address:               0x1060
    +  Start of program headers:          64 (bytes into file)
    +  Start of section headers:          13968 (bytes into file)
    +  Flags:                             0x0
    +  Size of this header:               64 (bytes)
    +  Size of program headers:           56 (bytes)
    +  Number of program headers:         13
    +  Size of section headers:           64 (bytes)
    +  Number of section headers:         31
    +  Section header string table index: 30
    +
    +

    Ce que l'on constate c'est que le format ELF est composé de plusieurs parties, notamment un en-tête ELF, des en-têtes de programme, des en-têtes de section, des tables de symboles, des réimplantations dynamiques, etc. Chaque partie a un rôle spécifique dans la gestion des fichiers binaires et des bibliothèques partagées. Dans cet exemple on observe que cet elf est pour l'ABI UNIX - System V.

    +

    Le System V est une version d'Unix développée par AT&T et commercialisée par Sun Microsystems. Elle a été l'une des premières versions d'Unix à être largement utilisée dans l'industrie. Le format ELF est donc un standard pour les systèmes Unix et Unix-like qui suit les conventions de l'ABI System V.

    +

    Sous Windows, le format de fichier exécutable est le format Portable Executable (PE) qui est différent du format ELF. Cela signifie que les programmes compilés pour Windows ne peuvent pas être exécutés sur un système Unix et vice versa sans une couche de compatibilité spécifique.

    +

    Sous macOS, le format de fichier exécutable est le format Mach-O (Mach Object) qui est également différent du format ELF. Cela signifie que les programmes compilés pour macOS ne peuvent pas être exécutés sur un système Unix ou Windows sans une couche de compatibilité spécifique.

    +

    EABI¤

    +

    L'ABI embarqué (Embedded ABI, EABI) est une version de l'ABI conçue pour les systèmes embarqués, c'est-à-dire des systèmes informatiques spécialisés qui sont intégrés dans des appareils électroniques. Les systèmes embarqués sont souvent limités en termes de ressources matérielles et logicielles, ce qui nécessite des conventions spécifiques pour les appels de fonctions, la gestion de la mémoire, le format des données, etc.

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + + + +
    + + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/course-c/25-architecture-and-systems/computer/index.html b/course-c/25-architecture-and-systems/computer/index.html index f0504d57..ddd8739a 100644 --- a/course-c/25-architecture-and-systems/computer/index.html +++ b/course-c/25-architecture-and-systems/computer/index.html @@ -1593,6 +1593,8 @@ + + @@ -1918,6 +1920,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/25-architecture-and-systems/files/index.html b/course-c/25-architecture-and-systems/files/index.html index d4f0c04e..ef08dc37 100644 --- a/course-c/25-architecture-and-systems/files/index.html +++ b/course-c/25-architecture-and-systems/files/index.html @@ -21,7 +21,7 @@ - + @@ -1593,6 +1593,8 @@ + + @@ -2012,6 +2014,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -7030,13 +7059,13 @@

    Exercices de révision +

    Interruptions¤

    -

    Ports¤

    +

    Dans un système embarqué ou un microcontrôleur, une interruption est un mécanisme qui permet à un processeur de suspendre temporairement l’exécution du programme en cours afin de répondre à un événement particulier, souvent externe ou d'une urgence particulière. En d'autres termes, l'interruption interrompt le flux normal d'instructions pour traiter un événement prioritaire, comme si quelqu’un frappait à la porte pendant que vous lisez un livre captivant. Il faut alors poser le livre pour ouvrir la porte.

    +

    Voici comment cela se passe, étape par étape, de manière plus technique :

    +
      +
    1. +

      Détection de l'événement : Une interruption est déclenchée par un événement spécifique. Cela peut être, par exemple, l'appui sur un bouton, l'arrivée d'une donnée par un port série, ou encore une alarme temporelle. Ces événements sont surveillés en arrière-plan, pendant que le microcontrôleur poursuit normalement l'exécution de son programme, c'est à dire exécuter les instructions les unes après les autres.

      +
    2. +
    3. +

      Suspension du programme principal : Lorsqu'une interruption est détectée, le microcontrôleur interrompt son programme principal. Il mémorise l'endroit précis où il s’est arrêté (l’adresse de l’instruction suivante) dans une pile (ou stack). Cela permet de reprendre exactement là où il s'était arrêté une fois l’interruption gérée.

      +
    4. +
    5. +

      Exécution de la routine d’interruption : Après avoir suspendu l’exécution du programme principal, le microcontrôleur saute vers une fonction spéciale appelée Routine de Service d’Interruption ou ISR (Interrupt Service Routine). Cette routine est un petit programme dédié qui est conçu pour traiter l’événement en question. Par exemple, si un bouton a été pressé, l’ISR peut augmenter un compteur ou effectuer une autre action spécifique.

      +
    6. +
    7. +

      Reprise du programme principal : Une fois l’interruption traitée, le microcontrôleur restaure son état précédent grâce aux informations stockées dans la pile. Il reprend alors l’exécution du programme principal exactement là où il s'était arrêté, comme si de rien n'était.

      +
    8. +
    +

    Pourquoi est-ce si utile ?¤

    +

    Les interruptions permettent au microcontrôleur de réagir immédiatement à des événements extérieurs sans avoir à constamment vérifier leur occurrence (ce que l’on appelle le polling, un processus inefficace et gourmand en ressources). Grâce aux interruptions, un microcontrôleur peut effectuer plusieurs tâches de manière efficace sans négliger aucun événement important.

    +

    Imaginons que vous devez surveiller en permanence si la pizza est cuite. Si l'algorithme est de faire pause après avoir dégommé 5 zombies à cet addictif jeu pour allez prendre connaissance de la cuisson, ce n'est pas efficace. Au contraire, en réglant un compte à rebours (timer) sur 10 minutes, il suffit de se laisser interrompre après le décompte...

    +

    Les types d'interruptions¤

    +

    Il existe généralement deux types d'interruptions :

    +
    +
    Interruptions matérielles
    +
    +

    Elles sont générées par des événements extérieurs au processeur, tels qu'un signal provenant d'un capteur, un bouton poussoir, ou une transmission de données par un périphérique. Dans un ordinateur personnel, les interruptions matérielles peuvent être générées par des périphériques tels que le clavier, la souris, le disque dur, etc.

    +
    +
    Interruptions logicielles
    +
    +

    Ces interruptions sont générées par le programme lui-même, souvent pour organiser des tâches complexes ou pour indiquer qu’une certaine tâche est terminée. En C ABI on peut générer une interruption logicielle en utilisant l’instruction raise(SIGINT) par exemple.

    +
    +
    +

    Un coût¤

    +

    Les interruptions ne sont pas gratuites. Elles ont un coût en termes de temps de traitement et de ressources. Lorsqu'une interruption se produit, le microcontrôleur doit :

    +
      +
    1. sauver le contexte du processeur (tous les registres utilisés par C et l'état du programme) ;
    2. +
    3. sauver l'adresse de l'instruction suivante à exécuter ;
    4. +
    5. vider les pipelines d'instructions ;
    6. +
    7. exécuter la routine d'interruption ;
    8. +
    9. vider les pipelines d'instructions à nouveau ;
    10. +
    11. restaurer le contexte du processeur ;
    12. +
    13. reprendre l'exécution du programme principal.
    14. +
    +

    Il est de coutume de dire qu'une interruption doit être la plus courte possible, mais une interruption d'une seule ligne de code en coûte déjà plusieurs dizaines.

    +

    Dans un contexte de microncontrôleur il n'est pas rare de devoir désactiver les interruptions pendant certaines opérations critiques, comme la modification d'une structure de données partagée entre l'ISR et le programme principal. Cela évite les problèmes de concurrence et de corruption de données. Il n'est pas rare non plus d'exécuter l'intégralité du programme dans une ISR.

    +

    Voici un exemple de code pour une montre à pile :

    +
    struct clock {
    +    int hours;
    +    int minutes;
    +    int seconds;
    +} clock;
    +
    +interrupt void timer_isr() {
    +    update_display(clock);
    +    if (++clock.seconds == 60) {
    +        clock.seconds = 0;
    +        if (++clock.minutes == 60) {
    +            clock.minutes = 0;
    +            if (++clock.hours == 24)
    +                clock.hours = 0;
    +        }
    +    }
    +    sleep();
    +}
    +
    +int main() {
    +    init_device();
    +    init_timer(timer_isr, 1'000'000 /* us */);
    +    enable_low_power_mode();
    +    sleep(); // Suspend l'exécution du programme jusqu'à une interruption
    +    for(;;) {} // Boucle infinie
    +}
    +
    +

    On observe d'une part l'utilisation d'un contexte global (variable globale) pour stocker l'heure, et d'autre part l'exécution de l'intégralité du programme dans l'ISR. Cela est possible car le programme est très simple et ne nécessite pas de traitement en dehors de l'ISR.

    +

    La variable globale est ici inévitable car il n'est généralement pas possible de passer des arguments à une ISR.

    +

    Conclusion¤

    +

    Les interruptions sont une clé de voûte des systèmes embarqués modernes, leur permettant de traiter des événements en temps réel sans sacrifier les performances. Elles constituent une forme de gestion multitâche simplifiée, car elles donnent au microcontrôleur la capacité de suspendre son travail pour traiter un événement imprévu, puis de reprendre là où il s'était arrêté, un peu comme un pianiste capable d’interrompre son morceau pour répondre à une note inattendue, avant de reprendre son jeu avec la même fluidité.

    +

    Qu'il s'agisse de microcontrôleur, de système embarqué ou d'ordinateur personnel, les interruptions sont centrales pour garantir une réactivité et une efficacité maximales.

    +

    Si je devais conclure sur une note littéraire : les interruptions sont comme ces moments où la réalité frappe à la porte de la contemplation, exigeant une réponse immédiate et pressante, avant que la marche des choses ne reprenne son cours, inexorablement.

    Timers¤

    +

    Un timer est un compteur interne au microcontrôleur qui s’incrémente de manière régulière, en fonction d’un signal d’horloge. Ce signal d’horloge provient souvent d’un oscillateur externe, comme un quartz. Les timers sont utilisés pour mesurer le temps qui s’écoule, générer des intervalles précis ou déclencher des événements à des moments définis. En ce sens, ils sont un peu comme des horloges invisibles, comptant patiemment les cycles de l’oscillateur pour déclencher une action lorsque le moment est venu.

    +

    Dans un microcontrôleur, les oscillateurs ou quartz fournissent la cadence à laquelle toutes les opérations internes se déroulent. Ce sont eux qui dictent la fréquence à laquelle le microcontrôleur exécute ses instructions. Par exemple, un quartz de 16 MHz signifie que le microcontrôleur exécute des cycles d'horloge à la fréquence de 16 millions de fois par seconde.

    +

    En pratique cet oscillateur passe par une PLL (Phase-Locked Loop) qui permet de multiplier la fréquence de l'oscillateur pour obtenir une fréquence plus élevée. Un microcontrôleur avec un oscillateur de 20 MHz pourrait très bien être cadencé à 400 MHz.

    +

    Ceci étant, pour mesurer des intervalles de temps plus longs (comme une seconde, une milliseconde, etc.), un microcontrôleur a besoin de compter un certain nombre de ces cycles d'horloge. Le timer est donc ce mécanisme interne qui s'incrémente à chaque cycle d'horloge, et il peut être configuré pour déclencher une action après un certain nombre de cycles.

    +

    Imaginons que vous vouliez mesurer une seconde avec un microcontrôleur cadencé à 100 MHz. Une seconde représente 100 millions de cycles. Un timer configuré pour s’incrémenter à chaque cycle d’horloge pourra compter jusqu’à 100 millions, et une fois arrivé à ce chiffre, il déclenchera une action, par exemple une interruption, pour signaler que l’intervalle de temps est écoulé.

    +

    Cependant, atteindre un tel compte d’un seul coup n’est pas toujours pratique. C'est pourquoi les microcontrôleurs utilisent souvent des prescalers, qui divisent la fréquence de l’horloge pour permettre au timer de fonctionner à des fréquences plus basses. Par exemple, avec un prescaler de 8, l'horloge d'entrée pour le timer sera réduite à 12.5 MHz (100 MHz / 8), ce qui permet de compter plus lentement et donc plus précisément pour des intervalles de temps plus longs. Néanmoins, pour obtenir une seconde cela reste compliqué.

    +

    Il est un cas particulier ou on utilise des quartz à la fréquence singulière de 32.768 kHz. Les modules RTC (Real-Time Clock, ou horloge temps réel) parfois intégrés à un microcontrôleur permettent de calculer automatiquement l'heure, la date, l'exquinoxe... sans passer par le CPU, et donc sans le réveiller s'il dort. Cette fréquence de 32.768 kHz est une fréquence qui, une fois divisée par \(2^15\) (32'768 = \(2^15\)), donne précisément 1 Hz, soit un cycle par seconde. Cela signifie qu’un timer qui fonctionne avec un quartz de 32.768 kHz et qui divise son signal d’horloge par 32 768 peut fournir un tic par seconde, ce qui est idéal pour maintenir le temps avec une grande précision.

    +

    Fonctionnement¤

    +

    Le timer est un périphérique souvent indépendant du processeur principal, qui possède des registres de configuration, et des lignes d'interruptions.

    +

    Le timer est alimenté par le signal d'horloge du microcontrôleur (souvent après un passage par un prescaler, qui divise la fréquence). À chaque cycle de l'horloge, le compteur interne du timer s’incrémente. Un Seuil de comparaison peut être configuré pour déclencher une action lorsqu’il atteint une valeur spécifique (ce qu’on appelle un match). Par exemple, on peut configurer un timer pour qu’il déclenche une interruption toutes les 1000 millisecondes (1 seconde), ou après un certain nombre de cycles d'horloge.Lorsque le timer atteint cette valeur prédéfinie, il déclenche soit une interruption, soit une autre action définie (mise à jour d'une variable, allumage d'un LED, etc.).

    +

    Cas d'utilisation¤

    +

    Les timers dans un microcontrôleur sont utilisés pour une variété d’applications :

    +
    +
    Générer des signaux périodiques
    +
    +

    Les timers peuvent produire des signaux réguliers, utiles pour des tâches comme la génération de PWM (Pulse Width Modulation) pour contrôler la vitesse d’un moteur ou la luminosité d'une LED.

    +
    +
    Mesurer des intervalles de temps
    +
    +

    Ils peuvent mesurer des délais précis, essentiels pour des protocoles de communication comme UART ou I2C.

    +
    +
    Créer des horloges temps réel
    +
    +

    Comme évoqué avec les quartz de 32.768 kHz, les timers peuvent être utilisés pour maintenir le temps, même lorsque le reste du microcontrôleur est inactif.

    +
    +
    Déclencher des événements asynchrones
    +
    +

    Par exemple, tu peux demander à un timer de générer une interruption toutes les millisecondes pour effectuer une mise à jour d’affichage ou une action récurrente dans ton programme.

    +
    +
    +

    Ports¤

    +

    Un port sur un microcontrôleur représente une interface de communication physique entre le monde extérieur (les périphériques, les capteurs, les actionneurs, etc.) et le microcontrôleur lui-même. En termes simples, il s’agit d’un ensemble de broches ou de pins sur le microcontrôleur, que l’on peut configurer pour envoyer ou recevoir des signaux électriques. Ces signaux, souvent des tensions logiques (0 ou 1, basse ou haute tension), permettent au microcontrôleur d’interagir avec l’extérieur.

    +

    Un port est souvent groupé en plusieurs broches, généralement 8 (on parle alors d’un port sur 8 bits, mais il peut aussi y en avoir 16 ou d'autres configurations). Chaque broche d’un port peut être contrôlée indépendamment pour être configurée en entrée ou en sortie.

    +

    Chaque broche d’un port peut généralement être configurée en deux modes principaux : entrée ou sortie. On parle alors d'un GPIO (General Purpose Input/Output) pour une broche qui peut être configurée en entrée ou en sortie.

    +

    Dans le microcontrôleur, les ports sont contrôlés par des registres (variables internes à l’architecture). On trouve généralement trois types de registres associés à chaque port :

    +
    +
    Registre de direction
    +
    +

    Ce registre permet de configurer chaque broche d’un port en mode entrée ou sortie.

    +
    +
    Registre de données
    +
    +

    Ce registre contient les données que tu envoies ou lis sur les broches. Si la broche est en mode sortie, ce registre détermine si tu envoies un signal haut ou bas. Si la broche est en mode entrée, il lit la valeur du signal reçu.

    +
    +
    Registre d'état
    +
    +

    Ce registre permet de lire l'état actuel des broches configurées en mode entrée. Si tu as un bouton ou un capteur connecté à une broche d’entrée, tu pourras vérifier son état via ce registre.

    +
    +
    +

    Souvent, il n'est pas possible de modifier l'état d'un bit d'un port de manière indépendante. On aura alors recours à l'algèbre de Boole pour modifier un bit sans modifier les autres.

    +
    // Mettre à 1 le bit 2 du port B
    +PORTB |= (1 << 2);
    +
    +// Mettre à 0 le bit 2 du port B
    +PORTB &= ~(1 << 2);
    +
    +// Inverser le bit 2 du port B
    +PORTB ^= (1 << 2);
    +
    @@ -6046,7 +6367,7 @@

    Timers - 24 septembre 2024 + 27 septembre 2024 diff --git a/course-c/25-architecture-and-systems/memory-management/index.html b/course-c/25-architecture-and-systems/memory-management/index.html index 3bf82946..1778d0cf 100644 --- a/course-c/25-architecture-and-systems/memory-management/index.html +++ b/course-c/25-architecture-and-systems/memory-management/index.html @@ -1593,6 +1593,8 @@ + + @@ -2123,6 +2125,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/25-architecture-and-systems/programs-and-processes/index.html b/course-c/25-architecture-and-systems/programs-and-processes/index.html index 65e736d2..59644b4e 100644 --- a/course-c/25-architecture-and-systems/programs-and-processes/index.html +++ b/course-c/25-architecture-and-systems/programs-and-processes/index.html @@ -1593,6 +1593,8 @@ + + @@ -1990,6 +1992,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/27-data-structures/dynamic-array/index.html b/course-c/27-data-structures/dynamic-array/index.html index 2af56567..bc345f04 100644 --- a/course-c/27-data-structures/dynamic-array/index.html +++ b/course-c/27-data-structures/dynamic-array/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/27-data-structures/graphs/index.html b/course-c/27-data-structures/graphs/index.html index db8505d3..6aa410c5 100644 --- a/course-c/27-data-structures/graphs/index.html +++ b/course-c/27-data-structures/graphs/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6173,13 +6202,17 @@

    GraphesTypes de graphes¤

    Forêts¤

    -

    Un graphe sans cycle est appelé une forêt. Une forêt est un ensemble d'arbres. Un arbre est un graphe connexe sans cycle.

    +

    Un graphe sans cycle est appelé une forêt. Une forêt est un ensemble d'arbres. Un arbre est un graphe connexe sans cycle.

    +

    Exemple de forêt
    Exemple de forêt

    Graphes orientés¤

    Un graphe orienté est un graphe dont les arêtes ont une direction. Les graphes orientés sont utilisés pour modéliser des relations asymétriques. Par exemple, un graphe orienté peut être utilisé pour représenter un réseau de transport où les arêtes représentent des routes à sens unique.

    +

    Exemple de graph orienté
    Exemple de graph orienté

    Graphes pondérés¤

    Un graphe pondéré est un graphe dont les arêtes ont un poids. Les graphes pondérés sont utilisés pour modéliser des relations quantitatives. Par exemple, un graphe pondéré peut être utilisé pour représenter un réseau de transport où les arêtes représentent des routes avec une longueur ou un coût associé.

    +

    Exemple de graph pondéré
    Exemple de graph pondéré

    Graphes bipartis¤

    Un graphe biparti est un graphe dont les sommets peuvent être divisés en deux ensembles disjoints. Les arêtes d'un graphe biparti relient les sommets des deux ensembles. Les graphes bipartis sont utilisés pour modéliser des relations binaires. Par exemple, un graphe biparti peut être utilisé pour représenter des relations d'adjacence entre deux ensembles d'objets.

    +

    Exemple de graphe biparti
    Exemple de graphe biparti

    Représentation des graphes¤

    Il existe plusieurs façons de représenter un graphe en mémoire. Les deux représentations les plus courantes sont les listes d'adjacence et les matrices d'adjacence.

    Liste d'adjacence¤

    @@ -6190,10 +6223,53 @@

    Parcours de graphesDFS¤

    Le parcours en profondeur (Depth-First Search) est un algorithme récursif qui explore le graphe en profondeur. Il commence par un sommet de départ et explore tous les sommets accessibles depuis ce sommet avant de passer au suivant. L'algorithme DFS est utilisé pour trouver des cycles dans un graphe, pour vérifier la connexité d'un graphe, pour trouver des composantes fortement connexes, etc.

    +
    void dfs(int u) {
    +    visited[u] = true;
    +    for (int v : adj[u]) {
    +        if (!visited[v]) {
    +            dfs(v);
    +        }
    +    }
    +}
    +

    BFS¤

    Le parcours en largeur (Breadth-First Search) est un algorithme itératif qui explore le graphe en largeur. Il commence par un sommet de départ et explore tous les sommets à une distance k avant de passer à la distance k+1. L'algorithme BFS est utilisé pour trouver le plus court chemin entre deux sommets, pour trouver le nombre de composantes connexes, pour trouver le nombre de sommets à une distance donnée, etc.

    +
    void bfs(int u) {
    +    Queue q = INIT_QUEUE;
    +    q.push(u);
    +    visited[u] = true;
    +    while (!q.empty()) {
    +        int v = q.front();
    +        q.pop();
    +        for (int w : adj[v]) {
    +            if (!visited[w]) {
    +                q.push(w);
    +                visited[w] = true;
    +            }
    +        }
    +    }
    +}
    +

    Dijkstra¤

    L'algorithme de Dijkstra est un algorithme qui permet de trouver le plus court chemin entre un sommet de départ et tous les autres sommets d'un graphe pondéré. L'algorithme de Dijkstra est basé sur le parcours en largeur et utilise une file de priorité pour explorer les sommets dans l'ordre croissant de leur distance par rapport au sommet de départ.

    +
    void dijkstra(int u) {
    +    PriorityQueue pq = INIT_PRIORITY_QUEUE;
    +    pq.push({0, u});
    +    dist[u] = 0;
    +    while (!pq.empty()) {
    +        int d = -pq.top().first;
    +        int v = pq.top().second;
    +        pq.pop();
    +        if (d > dist[v]) continue;
    +        for (auto [w, c] : adj[v]) {
    +            if (dist[v] + c < dist[w]) {
    +                dist[w] = dist[v] + c;
    +                pq.push({-dist[w], w});
    +            }
    +        }
    +    }
    +}
    +
    @@ -6216,7 +6292,7 @@

    Dijkstra - 19 août 2024 + 27 septembre 2024 diff --git a/course-c/27-data-structures/index.html b/course-c/27-data-structures/index.html index 6accbdf1..17e79033 100644 --- a/course-c/27-data-structures/index.html +++ b/course-c/27-data-structures/index.html @@ -18,7 +18,7 @@ - + @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -5966,7 +5995,7 @@

    Conteneurs de données - + diff --git a/course-c/27-data-structures/lists/index.html b/course-c/27-data-structures/lists/index.html index 570de464..8b0e196e 100644 --- a/course-c/27-data-structures/lists/index.html +++ b/course-c/27-data-structures/lists/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/27-data-structures/maps/index.html b/course-c/27-data-structures/maps/index.html index b4d1669e..93b0a87f 100644 --- a/course-c/27-data-structures/maps/index.html +++ b/course-c/27-data-structures/maps/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/27-data-structures/performances/index.html b/course-c/27-data-structures/performances/index.html index 773067be..acdc4b07 100644 --- a/course-c/27-data-structures/performances/index.html +++ b/course-c/27-data-structures/performances/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/27-data-structures/trees/index.html b/course-c/27-data-structures/trees/index.html index 7257f297..f7d259a5 100644 --- a/course-c/27-data-structures/trees/index.html +++ b/course-c/27-data-structures/trees/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/30-modular-programming/index.html b/course-c/30-modular-programming/index.html index c1518e0d..6287a231 100644 --- a/course-c/30-modular-programming/index.html +++ b/course-c/30-modular-programming/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/30-modular-programming/libraries/index.html b/course-c/30-modular-programming/libraries/index.html index d5c21efa..97c0b93f 100644 --- a/course-c/30-modular-programming/libraries/index.html +++ b/course-c/30-modular-programming/libraries/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/30-modular-programming/translation-units/index.html b/course-c/30-modular-programming/translation-units/index.html index b11a7be5..df79741c 100644 --- a/course-c/30-modular-programming/translation-units/index.html +++ b/course-c/30-modular-programming/translation-units/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/35-libraries/introduction/index.html b/course-c/35-libraries/introduction/index.html index e9046a32..ee03baf4 100644 --- a/course-c/35-libraries/introduction/index.html +++ b/course-c/35-libraries/introduction/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/35-libraries/standard-library/index.html b/course-c/35-libraries/standard-library/index.html index a0f677e9..d03d83ef 100644 --- a/course-c/35-libraries/standard-library/index.html +++ b/course-c/35-libraries/standard-library/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/35-libraries/third-party-libraries/index.html b/course-c/35-libraries/third-party-libraries/index.html index 9f1e8514..d1dbfbf7 100644 --- a/course-c/35-libraries/third-party-libraries/index.html +++ b/course-c/35-libraries/third-party-libraries/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/automata/index.html b/course-c/40-algorithms/automata/index.html index d702d38b..253d7313 100644 --- a/course-c/40-algorithms/automata/index.html +++ b/course-c/40-algorithms/automata/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/index.html b/course-c/40-algorithms/index.html index c3a67b3e..ec1c5a2b 100644 --- a/course-c/40-algorithms/index.html +++ b/course-c/40-algorithms/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/introduction/index.html b/course-c/40-algorithms/introduction/index.html index 15e3ae74..7e58d673 100644 --- a/course-c/40-algorithms/introduction/index.html +++ b/course-c/40-algorithms/introduction/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/popular-algorithms/a-star/index.html b/course-c/40-algorithms/popular-algorithms/a-star/index.html index 2ac7dd50..36df97ca 100644 --- a/course-c/40-algorithms/popular-algorithms/a-star/index.html +++ b/course-c/40-algorithms/popular-algorithms/a-star/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/popular-algorithms/conway/index.html b/course-c/40-algorithms/popular-algorithms/conway/index.html index d94631f3..1f9d581b 100644 --- a/course-c/40-algorithms/popular-algorithms/conway/index.html +++ b/course-c/40-algorithms/popular-algorithms/conway/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/popular-algorithms/fast-exp/index.html b/course-c/40-algorithms/popular-algorithms/fast-exp/index.html index 8bf6889e..bea27bce 100644 --- a/course-c/40-algorithms/popular-algorithms/fast-exp/index.html +++ b/course-c/40-algorithms/popular-algorithms/fast-exp/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/index.html b/course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/index.html index 492acb93..49a0fa20 100644 --- a/course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/index.html +++ b/course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -5919,29 +5948,6 @@

    Racine carrée inverse rapide - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/fast-sin/index.html b/course-c/40-algorithms/popular-algorithms/fast-sin/index.html index bd90c671..d00a1225 100644 --- a/course-c/40-algorithms/popular-algorithms/fast-sin/index.html +++ b/course-c/40-algorithms/popular-algorithms/fast-sin/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6288,29 +6317,6 @@

    Démonstration graphique avec - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/huffman/index.html b/course-c/40-algorithms/popular-algorithms/huffman/index.html index 771275f0..793f5578 100644 --- a/course-c/40-algorithms/popular-algorithms/huffman/index.html +++ b/course-c/40-algorithms/popular-algorithms/huffman/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6184,29 +6213,6 @@

    Encodage du texte - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/infography/index.html b/course-c/40-algorithms/popular-algorithms/infography/index.html index c208ff42..ff4caf8d 100644 --- a/course-c/40-algorithms/popular-algorithms/infography/index.html +++ b/course-c/40-algorithms/popular-algorithms/infography/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6180,29 +6209,6 @@

    Floyd-Stenberg - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/lindenmayer/index.html b/course-c/40-algorithms/popular-algorithms/lindenmayer/index.html index 5391d5e5..9cf93f36 100644 --- a/course-c/40-algorithms/popular-algorithms/lindenmayer/index.html +++ b/course-c/40-algorithms/popular-algorithms/lindenmayer/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -6112,29 +6141,6 @@

    Évolution - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/rabin-karp/index.html b/course-c/40-algorithms/popular-algorithms/rabin-karp/index.html index 2d771da4..18af604d 100644 --- a/course-c/40-algorithms/popular-algorithms/rabin-karp/index.html +++ b/course-c/40-algorithms/popular-algorithms/rabin-karp/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + @@ -5986,29 +6015,6 @@

    Algorithme de Rabin-Karp - - - - - - GitHub - - - - - - diff --git a/course-c/40-algorithms/popular-algorithms/random/index.html b/course-c/40-algorithms/popular-algorithms/random/index.html index d611caf0..e99a0b4d 100644 --- a/course-c/40-algorithms/popular-algorithms/random/index.html +++ b/course-c/40-algorithms/popular-algorithms/random/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/popular-algorithms/shunting-yard/index.html b/course-c/40-algorithms/popular-algorithms/shunting-yard/index.html index fe05c653..6994c814 100644 --- a/course-c/40-algorithms/popular-algorithms/shunting-yard/index.html +++ b/course-c/40-algorithms/popular-algorithms/shunting-yard/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/recursion/index.html b/course-c/40-algorithms/recursion/index.html index 7275691d..98c3f90f 100644 --- a/course-c/40-algorithms/recursion/index.html +++ b/course-c/40-algorithms/recursion/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/searching/index.html b/course-c/40-algorithms/searching/index.html index d53171df..b965fe51 100644 --- a/course-c/40-algorithms/searching/index.html +++ b/course-c/40-algorithms/searching/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/sorting/count-sort/index.html b/course-c/40-algorithms/sorting/count-sort/index.html index 325a470f..7a19adfb 100644 --- a/course-c/40-algorithms/sorting/count-sort/index.html +++ b/course-c/40-algorithms/sorting/count-sort/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/sorting/heap-sort/index.html b/course-c/40-algorithms/sorting/heap-sort/index.html index 13016540..77f077a5 100644 --- a/course-c/40-algorithms/sorting/heap-sort/index.html +++ b/course-c/40-algorithms/sorting/heap-sort/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/sorting/quick-sort/index.html b/course-c/40-algorithms/sorting/quick-sort/index.html index 78053bbe..27e4aae6 100644 --- a/course-c/40-algorithms/sorting/quick-sort/index.html +++ b/course-c/40-algorithms/sorting/quick-sort/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/40-algorithms/utilities/index.html b/course-c/40-algorithms/utilities/index.html index fa93c68d..b258fedd 100644 --- a/course-c/40-algorithms/utilities/index.html +++ b/course-c/40-algorithms/utilities/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/contribute/index.html b/course-c/45-software-design/contribute/index.html index e8c93300..7a2f28dc 100644 --- a/course-c/45-software-design/contribute/index.html +++ b/course-c/45-software-design/contribute/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/dependencies/index.html b/course-c/45-software-design/dependencies/index.html index df4d7705..49a5495a 100644 --- a/course-c/45-software-design/dependencies/index.html +++ b/course-c/45-software-design/dependencies/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/index.html b/course-c/45-software-design/index.html index 46965e0e..072d4620 100644 --- a/course-c/45-software-design/index.html +++ b/course-c/45-software-design/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/licenses/index.html b/course-c/45-software-design/licenses/index.html index 70c4f3d7..42023e2b 100644 --- a/course-c/45-software-design/licenses/index.html +++ b/course-c/45-software-design/licenses/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/software-project/index.html b/course-c/45-software-design/software-project/index.html index e807c074..c7ef97bc 100644 --- a/course-c/45-software-design/software-project/index.html +++ b/course-c/45-software-design/software-project/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/teamwork/index.html b/course-c/45-software-design/teamwork/index.html index 77455db8..ad2c6277 100644 --- a/course-c/45-software-design/teamwork/index.html +++ b/course-c/45-software-design/teamwork/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/45-software-design/testing/index.html b/course-c/45-software-design/testing/index.html index 91e4c8cb..78167d73 100644 --- a/course-c/45-software-design/testing/index.html +++ b/course-c/45-software-design/testing/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/gtk/index.html b/course-c/47-gui/gtk/index.html index 4e82fe3e..8bf73539 100644 --- a/course-c/47-gui/gtk/index.html +++ b/course-c/47-gui/gtk/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/index.html b/course-c/47-gui/index.html index dbfdc607..e686e3a3 100644 --- a/course-c/47-gui/index.html +++ b/course-c/47-gui/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/introduction/index.html b/course-c/47-gui/introduction/index.html index 5fe8bc84..6959b233 100644 --- a/course-c/47-gui/introduction/index.html +++ b/course-c/47-gui/introduction/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/opengl/index.html b/course-c/47-gui/opengl/index.html index c68947e8..5fc30ee6 100644 --- a/course-c/47-gui/opengl/index.html +++ b/course-c/47-gui/opengl/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/sdl/index.html b/course-c/47-gui/sdl/index.html index c7bbfbae..cedc27d9 100644 --- a/course-c/47-gui/sdl/index.html +++ b/course-c/47-gui/sdl/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/47-gui/window/index.html b/course-c/47-gui/window/index.html index f55decec..15e7b6fb 100644 --- a/course-c/47-gui/window/index.html +++ b/course-c/47-gui/window/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/48-network/applications/index.html b/course-c/48-network/applications/index.html index 762e62d5..0a296f3f 100644 --- a/course-c/48-network/applications/index.html +++ b/course-c/48-network/applications/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/48-network/index.html b/course-c/48-network/index.html index a8c724e1..5603a675 100644 --- a/course-c/48-network/index.html +++ b/course-c/48-network/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/48-network/osi/index.html b/course-c/48-network/osi/index.html index f2005439..8fd56dd3 100644 --- a/course-c/48-network/osi/index.html +++ b/course-c/48-network/osi/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/48-network/protocoles/index.html b/course-c/48-network/protocoles/index.html index 31a8cd4c..4ccbd0ed 100644 --- a/course-c/48-network/protocoles/index.html +++ b/course-c/48-network/protocoles/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/48-network/sockets/index.html b/course-c/48-network/sockets/index.html index 5f824a0b..3375f611 100644 --- a/course-c/48-network/sockets/index.html +++ b/course-c/48-network/sockets/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/60-safety/introduction/index.html b/course-c/60-safety/introduction/index.html index fbcf0a90..8a9ec406 100644 --- a/course-c/60-safety/introduction/index.html +++ b/course-c/60-safety/introduction/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/70-philosophy/funny/index.html b/course-c/70-philosophy/funny/index.html index 1c53f296..ed5371fb 100644 --- a/course-c/70-philosophy/funny/index.html +++ b/course-c/70-philosophy/funny/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/70-philosophy/philosophy/index.html b/course-c/70-philosophy/philosophy/index.html index 545507f6..4f34dd59 100644 --- a/course-c/70-philosophy/philosophy/index.html +++ b/course-c/70-philosophy/philosophy/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-c/90-exercises/index.html b/course-c/90-exercises/index.html index 780b82b1..1466556c 100644 --- a/course-c/90-exercises/index.html +++ b/course-c/90-exercises/index.html @@ -1591,6 +1591,8 @@ + + @@ -1770,6 +1772,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/arch/index.html b/course-concurrent/arch/index.html index 951f19c4..a6871a96 100644 --- a/course-concurrent/arch/index.html +++ b/course-concurrent/arch/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/async/index.html b/course-concurrent/async/index.html index 13403753..b6bc92b6 100644 --- a/course-concurrent/async/index.html +++ b/course-concurrent/async/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/memory/index.html b/course-concurrent/memory/index.html index b0b239bd..d4fcc790 100644 --- a/course-concurrent/memory/index.html +++ b/course-concurrent/memory/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/mutex/index.html b/course-concurrent/mutex/index.html index abfda55d..8692888c 100644 --- a/course-concurrent/mutex/index.html +++ b/course-concurrent/mutex/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/os/index.html b/course-concurrent/os/index.html index 6e339c15..1e4a7217 100644 --- a/course-concurrent/os/index.html +++ b/course-concurrent/os/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/semaphores/index.html b/course-concurrent/semaphores/index.html index 6fea9d09..e813f42e 100644 --- a/course-concurrent/semaphores/index.html +++ b/course-concurrent/semaphores/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/summary/index.html b/course-concurrent/summary/index.html index b842103c..af201297 100644 --- a/course-concurrent/summary/index.html +++ b/course-concurrent/summary/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/thread-pool/index.html b/course-concurrent/thread-pool/index.html index ddf5fb64..a1ba9db9 100644 --- a/course-concurrent/thread-pool/index.html +++ b/course-concurrent/thread-pool/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-concurrent/threads/index.html b/course-concurrent/threads/index.html index bcbd2ca1..e0a7a86d 100644 --- a/course-concurrent/threads/index.html +++ b/course-concurrent/threads/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-cpp/cpp/index.html b/course-cpp/cpp/index.html index 7841a267..e3ac1209 100644 --- a/course-cpp/cpp/index.html +++ b/course-cpp/cpp/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-cpp/garbage-collection/index.html b/course-cpp/garbage-collection/index.html index 107cdbfa..714ef399 100644 --- a/course-cpp/garbage-collection/index.html +++ b/course-cpp/garbage-collection/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-cpp/object-oriented/index.html b/course-cpp/object-oriented/index.html index a5878271..0e12b79b 100644 --- a/course-cpp/object-oriented/index.html +++ b/course-cpp/object-oriented/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/course-cpp/shared-memory/index.html b/course-cpp/shared-memory/index.html index c42584e3..77ab6e12 100644 --- a/course-cpp/shared-memory/index.html +++ b/course-cpp/shared-memory/index.html @@ -1584,6 +1584,8 @@ + + @@ -1763,6 +1765,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/index.html b/index.html index ba7c4c11..f0a9e05e 100644 --- a/index.html +++ b/index.html @@ -1750,6 +1750,8 @@

    Un cours d'informatique pour ingénieurs

    + + @@ -1929,6 +1931,33 @@

    Un cours d'informatique pour ingénieurs

    + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/search/search_index.json b/search/search_index.json index 87877ad1..c690103e 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config": {"lang": ["fr"], "separator": "[\\s\\-]+", "pipeline": ["stopWordFilter"], "fields": {"title": {"boost": 1000.0}, "text": {"boost": 1.0}, "tags": {"boost": 1000000.0}}}, "docs": [{"location": "", "title": "Home", "text": "

    Bienvenue sur le cours de programmation

    ", "tags": ["InfoMicro"]}, {"location": "#preface", "title": "Pr\u00e9face", "text": "

    Cet ouvrage est destin\u00e9 aux \u00e9tudiants de premi\u00e8re ann\u00e9e Bachelor HEIG-VD, d\u00e9partement TIN et fili\u00e8res G\u00e9nie \u00e9lectrique. Il est une introduction \u00e0 la programmation en C. Il couvre la mati\u00e8re vue durant le cycle des cours Info1 et Info2 .

    Le contenu de ce cours est calqu\u00e9 sur les fiches d'unit\u00e9s de cours et de modules suivantes\u2009:

    • Module InfoMicro (InfoMicro)
    • Unit\u00e9 Informatique 1 (Info1 )
    • Unit\u00e9 Informatique 2 (Info2 )
    "}, {"location": "appendix/bibliography/", "title": "Bibliographie", "text": "

    Les r\u00e9f\u00e9rences utilis\u00e9es dans cet ouvrage sont les suivantes.

    "}, {"location": "appendix/bibliography/#normes", "title": "Normes", "text": "
    • ISO norme C 1999 - ISO/IEC 9899:1999 (9899:1999 pdf)
    • ISO norme C 2011 - ISO/IEC 9899:2011 (9899:201x pdf)
    • ISO norme C 2018 - ISO/IEC 9899:2018 (9899:202x pdf)
    • POSIX - IEEE Std 1003.1-2017
    • Unicode - Unicode 13.0
    • IEEE 754 - IEEE 754-2008
    • UML - OMG Unified Modeling Language (OMG UML), Superstructure, Version 2.5.1
    • BPMN - OMG Business Process Model and Notation (BPMN), Version 2.0
    "}, {"location": "appendix/bibliography/#livres", "title": "Livres", "text": "
    • Le guide complet du langage C - Claude Delanoy, 844 pages (ISBN-13 978-2212140125)
    • Le Langage C 2e \u00e9dition - K&R, 304 pages (ISBN-13 978-2100715770)
    • Cracking the coding interview - Gayle Laakmann, 687 pages (ISBN-13 978-0984782857)
    • C\u2009: The Complete Reference, 4th Ed. - Osborne, 2.5 pounds (ISBN-13 978-0070411838)
    • C Programming Absolute Beginner's Guide - Perry, 432 pages (ISBN-13 978-0789751980)
    • Clean Code\u2009: A Handbook of Agile Software Craftsmanship - Robert C. Martin, 464 pages (ISBN-13 978-0132350884)
    • Code Complete\u2009: A Practical Handbook of Software Construction - Steve McConnell, 960 pages (ISBN-13 978-0735619678)
    • The Mythical Man-Month\u2009: Essays on Software Engineering - Frederick P. Brooks Jr., 336 pages (ISBN-13 978-0201835953)
    • G\u00f6del, Escher, Bach\u2009: An Eternal Golden Braid - Douglas R. Hofstadter, 824 pages (ISBN-13 978-0465026562)
    • The Art of Computer Programming - Donald E. Knuth, 3168 pages (ISBN-13 978-0201896831)
    • Numerical Recipes in C\u2009: The Art of Scientific Computing - William H. Press, 1232 pages (ISBN-13 978-0521750332)
    • Introduction \u00e0 la programmation - Karl Tombre, 178 pages - v1.3 - \u00c9cole des mines de Nancy
    • Concepts fondamentaux de l'informatique - Alfred Vaino Aho et Jeffrey David Ullman, 872 pages (ISBN-13 978-2100031276)
    "}, {"location": "appendix/bibliography/#sites-web", "title": "Sites Web", "text": "
    • Un r\u00e9sum\u00e9 du langage C - Learn X in Y minutes
    • R\u00e9f\u00e9rence C compl\u00e8te - C Reference
    • Encyclop\u00e9die libre - Wikipedia
    • Questions R\u00e9ponses - StackOverflow
    • Bac \u00e0 sable pour expressions r\u00e9guli\u00e8res - Regex101
    • Documentation officielle - GNU C Library
    • Documentation officielle - GNU Compiler Collection
    • Documentation officielle - Clang
    "}, {"location": "appendix/bibliography/#problemes-en-lignes", "title": "Probl\u00e8mes en lignes", "text": "
    • CSES, 300 probl\u00e8mes d'algorithmique
    • Exercism
    • LeetCode
    • CodeWars
    • Project Euler
    • Advent of Code
    • CodeAbbey
    • Codingame
    "}, {"location": "appendix/grammar/", "title": "Grammaire C", "text": "

    Yacc (Yet Another COmpiler-Compiler) est un logiciel utilis\u00e9 pour \u00e9crire des analyseurs syntaxiques de code. Il prend en entr\u00e9e une grammaire.

    Parce que les informaticiens ont de l'humour, Yacc \u00e0 son pendant GNU Bison plus r\u00e9cent (1985) mais toujours activement d\u00e9velopp\u00e9.

    Voici \u00e0 titre d'information la d\u00e9finition formelle du langage C99\u2009:

    %{\n#include \"ast.h\"\n#include <stdio.h>\nvoid yyerror(const char *s);\nint yylex(void);\nextern int yylineno;\nASTNode *root;\n%}\n\n%token IDENTIFIER I_CONSTANT F_CONSTANT STRING_LITERAL FUNC_NAME SIZEOF\n%token PTR_OP INC_OP DEC_OP LEFT_OP RIGHT_OP LE_OP GE_OP EQ_OP NE_OP\n%token AND_OP OR_OP MUL_ASSIGN DIV_ASSIGN MOD_ASSIGN ADD_ASSIGN\n%token SUB_ASSIGN LEFT_ASSIGN RIGHT_ASSIGN AND_ASSIGN\n%token XOR_ASSIGN OR_ASSIGN\n%token TYPEDEF_NAME ENUMERATION_CONSTANT\n\n%token TYPEDEF EXTERN STATIC AUTO REGISTER INLINE\n%token CONST RESTRICT VOLATILE\n%token BOOL CHAR SHORT INT LONG SIGNED UNSIGNED FLOAT DOUBLE VOID\n%token COMPLEX IMAGINARY\n%token STRUCT UNION ENUM ELLIPSIS\n\n%token CASE DEFAULT IF ELSE SWITCH WHILE DO FOR GOTO CONTINUE BREAK RETURN\n\n%token ALIGNAS ALIGNOF ATOMIC GENERIC NORETURN STATIC_ASSERT THREAD_LOCAL\n\n%start translation_unit\n%%\n\nprimary_expression\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | constant {\n        $$ = $1;\n    }\n    | string {\n        $$ = $1;\n    }\n    | '(' expression ')' {\n        $$ = $2;\n    }\n    | generic_selection {\n        $$ = $1;\n    }\n    ;\n\nconstant\n    : I_CONSTANT {\n        $$ = create_constant_node($1);\n    }\n    | F_CONSTANT {\n        $$ = create_constant_node($1);\n    }\n    | ENUMERATION_CONSTANT {\n        $$ = create_identifier_node($1);\n    }\n    ;\n\nstring\n    : STRING_LITERAL {\n        $$ = create_string_node($1);\n    }\n    | FUNC_NAME {\n        $$ = create_string_node($1);\n    }\n    ;\n\ngeneric_selection\n    : GENERIC '(' assignment_expression ',' generic_assoc_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ngeneric_assoc_list\n    : generic_association\n    | generic_assoc_list ',' generic_association\n    ;\n\ngeneric_association\n    : type_name ':' assignment_expression\n    | DEFAULT ':' assignment_expression\n    ;\n\npostfix_expression\n    : primary_expression {\n        $$ = $1;\n    }\n    | postfix_expression '[' expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '(' argument_expression_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '.' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression PTR_OP IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression INC_OP {\n        $$ = create_unary_op_node(INC_OP, $1);\n    }\n    | postfix_expression DEC_OP {\n        $$ = create_unary_op_node(DEC_OP, $1);\n    }\n    | '(' type_name ')' '{' initializer_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' type_name ')' '{' initializer_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nargument_expression_list\n    : assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | argument_expression_list ',' assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nunary_expression\n    : postfix_expression {\n        $$ = $1;\n    }\n    | INC_OP unary_expression {\n        $$ = create_unary_op_node(INC_OP, $2);\n    }\n    | DEC_OP unary_expression {\n        $$ = create_unary_op_node(DEC_OP, $2);\n    }\n    | unary_operator cast_expression {\n        $$ = create_unary_op_node($1, $2);\n    }\n    | SIZEOF unary_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SIZEOF '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ALIGNOF '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nunary_operator\n    : '&' {\n        $$ = '&';\n    }\n    | '*' {\n        $$ = '*';\n    }\n    | '+' {\n        $$ = '+';\n    }\n    | '-' {\n        $$ = '-';\n    }\n    | '~' {\n        $$ = '~';\n    }\n    | '!' {\n        $$ = '!';\n    }\n    ;\n\ncast_expression\n    : unary_expression {\n        $$ = $1;\n    }\n    | '(' type_name ')' cast_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nmultiplicative_expression\n    : cast_expression {\n        $$ = $1;\n    }\n    | multiplicative_expression '*' cast_expression {\n        $$ = create_binary_op_node('*', $1, $3);\n    }\n    | multiplicative_expression '/' cast_expression {\n        $$ = create_binary_op_node('/', $1, $3);\n    }\n    | multiplicative_expression '%' cast_expression {\n        $$ = create_binary_op_node('%', $1, $3);\n    }\n    ;\n\nadditive_expression\n    : multiplicative_expression {\n        $$ = $1;\n    }\n    | additive_expression '+' multiplicative_expression {\n        $$ = create_binary_op_node('+', $1, $3);\n    }\n    | additive_expression '-' multiplicative_expression {\n        $$ = create_binary_op_node('-', $1, $3);\n    }\n    ;\n\nshift_expression\n    : additive_expression {\n        $$ = $1;\n    }\n    | shift_expression LEFT_OP additive_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | shift_expression RIGHT_OP additive_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nrelational_expression\n    : shift_expression {\n        $$ = $1;\n    }\n    | relational_expression '<' shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression '>' shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression LE_OP shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression GE_OP shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nequality_expression\n    : relational_expression {\n        $$ = $1;\n    }\n    | equality_expression EQ_OP relational_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | equality_expression NE_OP relational_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nand_expression\n    : equality_expression {\n        $$ = $1;\n    }\n    | and_expression '&' equality_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexclusive_or_expression\n    : and_expression {\n        $$ = $1;\n    }\n    | exclusive_or_expression '^' and_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninclusive_or_expression\n    : exclusive_or_expression {\n        $$ = $1;\n    }\n    | inclusive_or_expression '|' exclusive_or_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlogical_and_expression\n    : inclusive_or_expression {\n        $$ = $1;\n    }\n    | logical_and_expression AND_OP inclusive_or_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlogical_or_expression\n    : logical_and_expression {\n        $$ = $1;\n    }\n    | logical_or_expression OR_OP logical_and_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nconditional_expression\n    : logical_or_expression {\n        $$ = $1;\n    }\n    | logical_or_expression '?' expression ':' conditional_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nassignment_expression\n    : conditional_expression {\n        $$ = $1;\n    }\n    | unary_expression assignment_operator assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nassignment_operator\n    : '=' {\n        $$ = '=';\n    }\n    | MUL_ASSIGN {\n        $$ = MUL_ASSIGN;\n    }\n    | DIV_ASSIGN {\n        $$ = DIV_ASSIGN;\n    }\n    | MOD_ASSIGN {\n        $$ = MOD_ASSIGN;\n    }\n    | ADD_ASSIGN {\n        $$ = ADD_ASSIGN;\n    }\n    | SUB_ASSIGN {\n        $$ = SUB_ASSIGN;\n    }\n    | LEFT_ASSIGN {\n        $$ = LEFT_ASSIGN;\n    }\n    | RIGHT_ASSIGN {\n        $$ = RIGHT_ASSIGN;\n    }\n    | AND_ASSIGN {\n        $$ = AND_ASSIGN;\n    }\n    | XOR_ASSIGN {\n        $$ = XOR_ASSIGN;\n    }\n    | OR_ASSIGN {\n        $$ = OR_ASSIGN;\n    }\n    ;\n\nexpression\n    : assignment_expression {\n        $$ = $1;\n    }\n    | expression ',' assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nconstant_expression\n    : conditional_expression {\n        $$ = $1;\n    }\n    ;\n\ndeclaration\n    : declaration_specifiers ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers init_declarator_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | static_assert_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclaration_specifiers\n    : storage_class_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | storage_class_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | function_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | function_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | alignment_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | alignment_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninit_declarator_list\n    : init_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | init_declarator_list ',' init_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninit_declarator\n    : declarator '=' initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstorage_class_specifier\n    : TYPEDEF {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | EXTERN {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | STATIC {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | THREAD_LOCAL {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | AUTO {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | REGISTER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_specifier\n    : VOID {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CHAR {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SHORT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | INT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | LONG {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FLOAT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DOUBLE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SIGNED {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | UNSIGNED {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | BOOL {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | COMPLEX {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | IMAGINARY {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | atomic_type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enum_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | TYPEDEF_NAME {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_or_union_specifier\n    : struct_or_union '{' struct_declaration_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union IDENTIFIER '{' struct_declaration_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_or_union\n    : STRUCT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | UNION {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declaration_list\n    : struct_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_declaration_list struct_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declaration\n    : specifier_qualifier_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | specifier_qualifier_list struct_declarator_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | static_assert_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nspecifier_qualifier_list\n    : type_specifier specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declarator_list\n    : struct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_declarator_list ',' struct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declarator\n    : ':' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator ':' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenum_specifier\n    : ENUM '{' enumerator_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM '{' enumerator_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER '{' enumerator_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER '{' enumerator_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenumerator_list\n    : enumerator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enumerator_list ',' enumerator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenumerator\n    : enumeration_constant '=' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enumeration_constant {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\natomic_type_specifier\n    : ATOMIC '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_qualifier\n    : CONST {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RESTRICT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | VOLATILE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ATOMIC {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nfunction_specifier\n    : INLINE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | NORETURN {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nalignment_specifier\n    : ALIGNAS '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ALIGNAS '(' constant_expression ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclarator\n    : pointer direct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndirect_declarator\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | '(' declarator ')' {\n        $$ = $2;\n    }\n    | direct_declarator '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' identifier_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\npointer\n    : '*' type_qualifier_list pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' type_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_qualifier_list\n    : type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier_list type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_type_list\n    : parameter_list ',' ELLIPSIS {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | parameter_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_list\n    : parameter_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | parameter_list ',' parameter_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_declaration\n    : declaration_specifiers declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nidentifier_list\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | identifier_list ',' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_name\n    : specifier_qualifier_list abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nabstract_declarator\n    : pointer direct_abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndirect_abstract_declarator\n    : '(' abstract_declarator ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninitializer\n    : '{' initializer_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '{' initializer_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninitializer_list\n    : designation initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer_list ',' designation initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer_list ',' initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignation\n    : designator_list '=' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignator_list\n    : designator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | designator_list designator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignator\n    : '[' constant_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '.' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstatic_assert_declaration\n    : STATIC_ASSERT '(' constant_expression ',' STRING_LITERAL ')' ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstatement\n    : labeled_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | expression_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | selection_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | iteration_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | jump_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlabeled_statement\n    : IDENTIFIER ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CASE constant_expression ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DEFAULT ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ncompound_statement\n    : '{' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '{' block_item_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nblock_item_list\n    : block_item {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | block_item_list block_item {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nblock_item\n    : declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexpression_statement\n    : ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | expression ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nselection_statement\n    : IF '(' expression ')' statement ELSE statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | IF '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SWITCH '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\niteration_statement\n    : WHILE '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DO statement WHILE '(' expression ')' ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' expression_statement expression_statement ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' expression_statement expression_statement expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' declaration expression_statement ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' declaration expression_statement expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\njump_statement\n    : GOTO IDENTIFIER ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CONTINUE ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | BREAK ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RETURN ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RETURN expression ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntranslation_unit\n    : external_declaration {\n        root = $1;\n    }\n    | translation_unit external_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexternal_declaration\n    : function_definition {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nfunction_definition\n    : declaration_specifiers declarator declaration_list compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers declarator compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclaration_list\n    : declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_list declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\n%%\n#include <stdio.h>\n\nvoid yyerror(const char *s) {\n    fprintf(stderr, \"Error: %s at line %d\\n\", s, yylineno);\n}\n\nint yylex(void);\n\nint main(int argc, char **argv) {\n    if (argc > 1) {\n        FILE *file = fopen(argv[1], \"r\");\n        if (!file) {\n            fprintf(stderr, \"Could not open file %s\\n\", argv[1]);\n            return 1;\n        }\n        yyin = file;\n    }\n    yyparse();\n    if (root) {\n        print_ast(root);\n    }\n    return 0;\n}\n

    A partir de cette grammaire, Bison g\u00e9n\u00e8re un fichier c99.tab.c qui contient le code C de l'analyseur syntaxique.

    Pour la cr\u00e9er vous-m\u00eame, vous pouvez utiliser la commande suivante\u2009:

    bison -d -o c99.tab.c c99.y\n
    ", "tags": ["c99.tab.c"]}, {"location": "appendix/laboratories/", "title": "Laboratoires", "text": "

    Les laboratoires sont des travaux pratiques permettant \u00e0 l'\u00e9tudiant d'attaquer des probl\u00e8mes de programmation plus difficiles que les exercices faits en classe.

    "}, {"location": "appendix/laboratories/#protocole", "title": "Protocole", "text": "
    1. R\u00e9cup\u00e9rer le r\u00e9f\u00e9rentiel du laboratoire en utilisant GitHub Classroom.
    2. Prendre connaissance du cahier des charges.
    3. R\u00e9diger le code.
    4. Le tester.
    5. R\u00e9diger votre rapport de test si demand\u00e9.
    6. Le soumettre avant la date butoir.
    "}, {"location": "appendix/laboratories/#evaluation", "title": "\u00c9valuation", "text": "

    Une grille d'\u00e9valuation est int\u00e9gr\u00e9e \u00e0 tous les laboratoires. Elle prend la forme d'un fichier criteria.yml que l'\u00e9tudiant peut consulter en tout temps.

    ", "tags": ["criteria.yml"]}, {"location": "appendix/laboratories/#directives", "title": "Directives", "text": "
    • La recherche sur internet est autoris\u00e9e et conseill\u00e9e.
    • Le plagiat n'est pas autoris\u00e9, et sanctionn\u00e9 si d\u00e9couvert par la note de 1.0.
    • Le rendu pass\u00e9 la date butoir est sanctionn\u00e9 \u00e0 raison de 1 point puis 1/24 de point par heure de retard.
    "}, {"location": "appendix/laboratories/#format-de-rendu", "title": "Format de rendu", "text": "
    • Fin de lignes\u2009: LF ('\\n').
    • Encodage\u2009: UTF-8 sans BOM.
    • Code source respectueux de ISO/IEC 9899:1999.
    • Le code doit comporter un exemple d'utilisation et une documentation mise \u00e0 jour dans README.md.
    • Lorsqu'un rapport est demand\u00e9, vous le placerez dans REPORT.md.
    ", "tags": ["README.md", "REPORT.md"]}, {"location": "appendix/laboratories/#anatomie-dun-travail-pratique", "title": "Anatomie d'un travail pratique", "text": "

    Un certain nombre de fichiers vous sont donn\u00e9s, il est utile de les conna\u00eetre. Un r\u00e9f\u00e9rentiel sera g\u00e9n\u00e9ralement compos\u00e9 des \u00e9l\u00e9ments suivants\u2009:

    $ tree\n.\n\u251c\u2500\u2500 .clang-format\n\u251c\u2500\u2500 .devcontainer\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 Dockerfile\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 devcontainer.json\n\u251c\u2500\u2500 .editorconfig\n\u251c\u2500\u2500 .gitattributes\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 .vscode\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 launch.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 tasks.json\n\u251c\u2500\u2500 Makefile\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 assets\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 test.txt\n\u251c\u2500\u2500 foo.c\n\u251c\u2500\u2500 foo.h\n\u251c\u2500\u2500 main.c\n\u251c\u2500\u2500 criteria.yml\n\u2514\u2500\u2500 tests\n    \u251c\u2500\u2500 Makefile\n \u00a0\u00a0 \u2514\u2500\u2500 test_foo.c\n
    "}, {"location": "appendix/laboratories/#readmemd", "title": "README.md", "text": "

    Il s'agit de la documentation principale de votre r\u00e9f\u00e9rentiel. Elle contient la donn\u00e9e du travail pratique en format Markdown. Ce fichier est \u00e9galement utilis\u00e9 par d\u00e9faut dans GitHub. Il contient notamment le titre du laboratoire, la dur\u00e9e, le d\u00e9lai de rendu et le format individuel ou de groupe\u2009:

    # Laboratoire <!-- omit in toc -->\n- **Dur\u00e9e**: 2 p\u00e9riodes + environ 3h \u00e0 la maison\n- **Date de rendu**: dimanche avant minuit\n- **Format**: travail individuel\n...\n
    "}, {"location": "appendix/laboratories/#criteriayml", "title": "criteria.yml", "text": "

    Ce fichier contient les directives d'\u00e9valuation du travail pratique. Il est au format YAML. Pour chaque point \u00e9valu\u00e9 une description est donn\u00e9e avec la cl\u00e9 description et un nombre de points est sp\u00e9cifi\u00e9. Une exigence peut avoir soit un nombre de points positifs soit n\u00e9gatifs. Les points n\u00e9gatifs agissent comme une p\u00e9nalit\u00e9. Ce choix d'avoir des points et des p\u00e9nalit\u00e9s permet de ne pas diluer les exigences au travers d'autres crit\u00e8res importants, mais normalement respect\u00e9s des \u00e9tudiants.

    Des points bonus sont donn\u00e9s si le programme dispose d'une aide et d'une version et si la fonctionnalit\u00e9 du programme est \u00e9tendue.

    # Crit\u00e8res d'\u00e9valuation du travail pratique\n%YAML 1.2\n---\ntests:\n    build:\n        description: Le programme compile sans erreurs ni warning\n        points: 0/-4\n        test: test_build\n    unit-testing:\n        function_foo:\n        points: 0/10\n        test: test_foo\n        function_bar:\n        points: 0/10\n        test: test_bar\n    functional-testing:\n        arguments:\n        description: La lecture des arguments fonctionne comme demand\u00e9\n        points: 0/7\n        test: test_arguments\n        output-display:\n        description: Affichage sur stdout/stderr comme sp\u00e9cifi\u00e9\n        points: 0/3\n        test: test_output\n        errors:\n        description: Le programme affiche des erreurs si rencontr\u00e9es\n        points: 0/2\n        test: test_errors\nreport:\n    introduction:\n        description: Le rapport de test contient une introduction\n        points: 0/2\n    conclusion:\n        description: Le rapport de test contient une conclusion\n        points: 0/2\n    analysis:\n        description: Le rapport de test contient une analyse du comportement\n        points: 0/3\ncode:\n    specifications:\n        prototypes:\n            description: Les prototypes des fonctions demand\u00e9es sont respect\u00e9s\n            points: 0/3\n        main:\n            description: Le programme principal est minimaliste\n            points: 0/3\n        algorithm:\n            description: L'algorithme de encode/decode est bien pens\u00e9\n            points: 0/5\n    comments:\n        header:\n        description: Un en-t\u00eate programme est clairement d\u00e9fini\n        points: 0/2\n        purpose:\n        description: Les commentaires sont pertinents\n        points: 0/-2\n        commented-code:\n        description: Du code est comment\u00e9\n        points: 0/-2\n    variables:\n        naming:\n        description: Le noms des variables est minimaliste et explicite\n        points: 0/2\n        scope:\n        description: La port\u00e9e des variables est r\u00e9duite au minimum\n        points: 0/2\n        type:\n        description: Le type des variables est appropri\u00e9\n        points: 0/2\n    functions:\n        length:\n        description: La longueur des fonctions est raisonnable\n        points: 0/-4\n    control-flow:\n        description: Les structures de contr\u00f4le sont appropri\u00e9es\n        points: 0/4\n    overall:\n        dry:\n        description: Pas de r\u00e9p\u00e9tition dans le code\n        points: 0/-5\n        kiss:\n        description: Le code est minimaliste et simple\n        points: 0/-5\n        ssot:\n        description: Pas de r\u00e9p\u00e9tition d'information\n        points: 0/-5\n        indentation:\n        description: L'indentation du code est coh\u00e9rente\n        points: 0/-5\nbonus:\n    help:\n        description: Le programme dispose d'une aide\n        bonus: 0/1\n        test: test_help\n    version:\n        description: La version du programme peut \u00eatre affich\u00e9e\n        bonus: 0/1\n        test: test_version\n    extension:\n        description: La fonctionnalit\u00e9 du programme est \u00e9tendue\n        bonus: 0/3\n    english:\n        description: Usage de l'anglais\n        bonus: 0/1\n

    Ce fichier est utilis\u00e9 par des tests automatique pour faciliter la correction du travail pratique.

    ", "tags": ["description"]}, {"location": "appendix/refcards/", "title": "Cartes de r\u00e9f\u00e9rence", "text": "

    Des cartes de r\u00e9f\u00e9rences A4 recto-verso ont \u00e9t\u00e9 r\u00e9alis\u00e9es pour diff\u00e9rents sujets d'informatique. Elles sont disponibles en version num\u00e9rique et en version papier \u00e0 la HEIG-VD.

    Ces cartes sont normalement autoris\u00e9es durant les examens et travaux \u00e9crits. Pri\u00e8re de v\u00e9rifier avec les enseignants concern\u00e9s avant de les utiliser.

    "}, {"location": "appendix/refcards/#carte-de-reference-c", "title": "Carte de r\u00e9f\u00e9rence C", "text": "

    Carte de r\u00e9f\u00e9rence C

    "}, {"location": "appendix/refcards/#carte-de-reference-c_1", "title": "Carte de r\u00e9f\u00e9rence C++", "text": "

    Carte de r\u00e9f\u00e9rence C

    "}, {"location": "appendix/refcards/#carte-de-reference-de-programmation-concurrente", "title": "Carte de r\u00e9f\u00e9rence de programmation concurrente", "text": "

    Carte de r\u00e9f\u00e9rence Programmation concurrente

    "}, {"location": "appendix/refcards/#carte-de-reference-python", "title": "Carte de r\u00e9f\u00e9rence Python", "text": "

    Carte de r\u00e9f\u00e9rence Python

    "}, {"location": "appendix/refcards/#carte-de-reference-latex", "title": "Carte de r\u00e9f\u00e9rence LaTeX", "text": "

    Carte de r\u00e9f\u00e9rence LaTeX

    "}, {"location": "appendix/unit/", "title": "Fiches d'unit\u00e9s de cours", "text": "

    Les fiches d'unit\u00e9s sont les documents de r\u00e9f\u00e9rence pour les cours d'info1 et d'info2, ici pr\u00e9sent\u00e9es sous forme de donn\u00e9es brutes au format YAML.

    "}, {"location": "appendix/unit/#informatique-1", "title": "Informatique 1", "text": "
    # Version formelle et \u00e9tendue de la fiche d'unit\u00e9 de cours disponible sur GAPS\n# http://gaps.heig-vd.ch/public/fiches/uv/uv.php?id=5637\n---\ntitle:\n  name: Informatique 1\n  tag: info1\n  id: 6488\ndomain: Ing\u00e9nierie et Architecture\nfili\u00e8re: G\u00e9nie \u00e9lectrique\norientations:\n  EAI:\n  EEM:\n  MI:\nformation: Plein temps\nvalidityDate:\n  - 2021-2022\nauthor: Pierre Bressy\ncharge:\n  academicHours: 150\n  inClassAcademicHours: 96\nplanning:\n  s1:\n    class: # 48\n      - hours: 4\n        chapters:\n          - Introduction.\n          - Aper\u00e7u du fonctionnement de l'ordinateur.\n          - Codage de l'information\n      - hours: 2\n        chapters:\n          - Pr\u00e9sentation du langage C\n      - hours: 12\n        chapters:\n          - Types de donn\u00e9es de base\n          - Variables\n          - Constantes\n          - Op\u00e9rateurs\n          - Entr\u00e9es et sorties console\n      - hours: 8\n        chapters:\n          - Structures de contr\u00f4le\n          - Branchements\n          - Boucles\n      - hours: 6\n        chapters:\n          - Fonctions\n          - Passage par valeur et par adresse\n      - hours: 8\n        chapters:\n          - Tableaux\n          - Cha\u00eenes de caract\u00e8res\n      - hours: 4\n        chapters:\n          - Introduction \u00e0 l'analyse et \u00e0 la conception (d\u00e9coupage du probl\u00e8me)\n      - hours: 4\n        chapters:\n          - Contr\u00f4le continu et corrections\n    laboratory:\n      - hours: 2\n        chapters:\n          - Mise en place de l'environnement de travail.\n      - hours: 2\n        chapters:\n          - Environnement de d\u00e9veloppement int\u00e9gr\u00e9\n            - Installation\n            - Configuration\n            - \u00c9dition\n            - Compilation\n      - hours: 8\n        chapters:\n          - Dialogues utilisateurs\n      - hours: 10\n        chapters:\n          - Th\u00e9orie sur les instructions if, for, while, do..while, switch.\n          - Utilisation des structures de contr\u00f4le\n      - hours: 8\n        chapters:\n          - Type de donn\u00e9es compos\u00e9s\n      - hours: 8\n        chapters:\n          - Mini projet.\nprerequisites: |\n  L'\u00e9tudiant-e doit conna\u00eetre et savoir utiliser les notions suivantes\n    - utilisation g\u00e9n\u00e9rale d'un syst\u00e8me d'exploitation graphique notamment\n      la gestion de fichiers et les bases des outils de bureautique,\n    - notation binaire, octale et hexad\u00e9cimale et de l'alg\u00e8bre bool\u00e9enne\n    \u00e9l\u00e9mentaire.\ngoals:\n  classroom:\n    - |\n      Expliquer les principes g\u00e9n\u00e9raux de repr\u00e9sentation de l'information\n      dans les ordinateurs.\n    - |\n      D\u00e9crire la marche \u00e0 suivre et les outils n\u00e9cessaires pour cr\u00e9er\n      un programme ex\u00e9cutable.\n    - |\n      Assurer la tra\u00e7abilit\u00e9 du code source de la conception \u00e0 la\n      livraison du programme.\n    - |\n      Citer les \u00e9l\u00e9ments syntaxiques du langage C utilis\u00e9 couramment pour\n      \u00e9crire des programmes.\n    - |\n      Choisir le type de donn\u00e9es le plus adapt\u00e9 pour repr\u00e9senter une\n      information physique.\n    - |\n      Concevoir et programmer un dialogue op\u00e9rateur en mode console.\n    - |\n      Formater un affichage sur la sortie standard pour le rendre lisible.\n    - |\n      Calculer la valeur d'une expression construite avec diff\u00e9rents\n      op\u00e9rateurs du langage C et en d\u00e9terminer le type de stockage r\u00e9sultant.\n    - |\n      Choisir la structure de contr\u00f4le appropri\u00e9e pour r\u00e9soudre un probl\u00e8me\n      algorithmique simple.\n    - |\n      Concevoir et impl\u00e9menter un algorithme imbriquant jusqu'\u00e0 trois niveaux\n      de structure de contr\u00f4le.\n    - |\n      Cr\u00e9er une fonction impliquant un passage de param\u00e8tre par valeur et\n      par adresse.\n    - |\n      Utiliser le \u00ab type tableau \u00bb multidimensionnel et manipuler ses\n      \u00e9l\u00e9ments constituants.\n    - |\n      Manipulation simple de cha\u00eenes de caract\u00e8res en utilisant la\n      biblioth\u00e8que standard.\n    - |\n      Mettre en \u0153uvre des algorithmes utilisant des fonctions math\u00e9matiques\n      de la biblioth\u00e8que standard.\n    - D\u00e9boguer un programme informatique en utilisant des points d'arr\u00eat.\n    - |\n      Interagir avec un programme ex\u00e9cutable via les arguments et les flux\n      d'entr\u00e9es sorties.\n    - Conna\u00eetre les idiomes (patron d'impl\u00e9mentation) de base (SSOT, DRY, KISS).\n  laboratory:\n    - |\n      Installer et configurer un environnement de d\u00e9veloppement int\u00e9gr\u00e9 (IDE)\n      pour le langage C.\n    - |\n      Cr\u00e9er des programmes avec un IDE et compiler un programme en ligne de\n      commande.\n    - Construire une liste d'arguments.\n    - |\n      Cr\u00e9er un programme g\u00e9rant un menu en mode console et affichant des\n      r\u00e9sultats sous forme structur\u00e9e.\n    - |\n      Mettre au point it\u00e9rativement un programme pour atteindre un\n      fonctionnement fiable et ergonomique.\n    - |\n      Comprendre un cahier des charges, identifier et clarifier les exigences\n      importantes, et s'y conformer.\n    - |\n      Analyser de mani\u00e8re autonome les probl\u00e8mes rencontr\u00e9s et proposer\n      une solution impl\u00e9mentable.\n    - Livrer un logiciel en assurant sa tra\u00e7abilit\u00e9 en respectant un d\u00e9lai.\n    - |\n      Citer des applications pratiques de la programmation en relation avec\n      ses futurs d\u00e9bouch\u00e9s professionnels.\n    - Chercher des solutions par soi-m\u00eame en utilisant internet.\nplan:\n  - Num\u00e9ration\n    - Bases (syst\u00e8me d\u00e9cimal, hexad\u00e9cimal, octal, binaire)\n    - Conversion de bases\n    - Compl\u00e9ment \u00e0 un\n    - Compl\u00e9ment \u00e0 deux\n    - Arithm\u00e9tique binaire (et, ou, ou exclusif, n\u00e9gation)\n  - Processus de d\u00e9veloppement\n    - Outils\n    - Environnement int\u00e9gr\u00e9 (IDE)\n    - Compilateur (*compiler*)\n    - Cha\u00eene de d\u00e9veloppement (*toolchain*)\n    - Cycle de d\u00e9veloppement\n    - Cycle de compilation\n    - Installation d'un environnement de d\u00e9veloppement\n    - Programmes et processus\n  - G\u00e9n\u00e9ralit\u00e9s du langage C\n    - S\u00e9quences\n    - Embranchements (if, switch)\n    - Boucles (while, do..while, for)\n    - Sauts (break, continue, return, goto)\n  - Types de donn\u00e9es\n    - Typage\n    - Stockage des donn\u00e9es en m\u00e9moire\n    - Entiers naturels\n    - Entiers relatifs\n    - Nombres r\u00e9els (virgule flottante)\n    - Caract\u00e8res\n    - Table ASCII\n    - Cha\u00eenes de caract\u00e8res\n    - Bool\u00e9ens\n  - Interaction utilisateur en mode console\n    - Entr\u00e9e standard\n    - Sortie standard\n    - Sortie d'erreur standard\n    - Questions/R\u00e9ponses avec `printf` et `scanf`\n    - Formater un r\u00e9sultat sous forme tabul\u00e9e et lisible\n    - Menu (choix multiples)\n  - Op\u00e9rateurs\n    - Op\u00e9rateurs du langage C\n    - Priorit\u00e9 des op\u00e9rateurs\n    - Expressions\n    - Promotion et promotion implicite\n  - Conception\n    - Choix des structures de contr\u00f4les adapt\u00e9es \u00e0 des probl\u00e8mes\n    - Algorithmes simple (min, max, moyenne, ...)\n    - Manipulation de cha\u00eenes\n    - Manipulation de tableaux\n    - Manipulation de bits\n  - Algorithmie\n    - Complexit\u00e9 d'un algorithme\n    - Exemples d'algorithmes\n    - Algorithmes de tri (tri \u00e0 bulle)\n  - Fonctions\n    - Passage par valeur et par adresse\n    - Utilisation de la valeur de retour\n    - Prototypes de fonctions\n  - Types de donn\u00e9es compos\u00e9es\n    - Structures\n    - Unions\n    - Tableaux\n    - \u00c9num\u00e9rations\n  - Biblioth\u00e8ques standard\n    - <math.h>\n    - Fonctions trigonom\u00e9triques\n    - Exponentielle\n    - Logarithme\n    - <string.h>\n    - Comparaison de cha\u00eenes de caract\u00e8res\n    - Concat\u00e9nation de cha\u00eenes de caract\u00e8res\n    - Copie de cha\u00eenes de caract\u00e8res\n    - Longueur d'une cha\u00eene de caract\u00e8res\n    - Recherche d'une sous-cha\u00eene dans une cha\u00eene de caract\u00e8res\n    - <stdio.h>\n    - printf\n    - scanf\n    - putchar\n    - getchar\n    - puts\n    - gets\n  - Structure du code\n    - Corriger les erreurs de syntaxes\n    - Corriger les erreurs s\u00e9mantiques\n    - Indentation du code\n    - Commentaires\n
    "}, {"location": "appendix/unit/#panification-du-semestre-dhiver", "title": "Panification du semestre d'hiver", "text": "Semaine Acad\u00e9mique Cours Labo 38 1 Introduction 00 Familiarisation 39 2 Num\u00e9ration 01 Premier pas en C 40 3 Fondements du C 02 \u00c9quation quadratique 41 4 Variables, op\u00e9rateurs 03 Fl\u00e9chettes 42 5 Types, entr\u00e9es sorties 04 Pneus 43 Vacances d'automne 44 6 Entr\u00e9es sorties 05 Monte-Carlo 45 7 TE1 06 Tables Multiplications 46 8 Structure de contr\u00f4les 07 Cha\u00eenes (par \u00e9quipe) 47 9 Fonctions 08 Nombre d'Armstrong 48 10 Tableaux et structures 09 Sudoku 49 11 Programmes et processus 50 12 Algorithmique Labo Test 51 13 Pointeurs 10 Galton 52 Vacances de No\u00ebl 1 2 14 Ergonomie et dialogues 12 Tableau des scores 3 15 TE2 4 16 Exercices de r\u00e9vision 5 Pr\u00e9paration Examens 6 Examens 7 Rel\u00e2ches"}, {"location": "appendix/unit/#informatique-2", "title": "Informatique 2", "text": "
    # Version formelle et \u00e9tendue de la fiche d'unit\u00e9 de cours disponible sur GAPS\n# https://gaps.heig-vd.ch/consultation/fiches/uv/uv.php?id=6491\n---\ntitle:\n  name: Informatique 2\n  tag: info2\n  id: 6491\ndomain: Ing\u00e9nierie et Architecture\nfili\u00e8re: G\u00e9nie \u00e9lectrique\norientations:\n  EAI:\n  EEM:\n  MI:\nformation: Plein temps\nvalidityDate:\n  - 2021-2022\nauthor: Pierre Bressy\ncharge:\n  academicHours: 120\n  inClassAcademicHours: 80\nplanning:\n  s1:\n    class: # 48\n      - hours: 4\n        chapters:\n          - \"Pr\u00e9processeur (#include, #define, #if, #pragma)\"\n      - hours: 4\n        chapters:\n          - Classes de stockage (static, volatile, extern)\n      - hours: 8\n        chapters:\n          - Conception de type de donn\u00e9es abstraits simples\n          - Cr\u00e9ation de biblioth\u00e8ques\n      - hours: 10\n        chapters:\n          - Pointeurs, arithm\u00e9tique de pointeurs\n          - Allocation dynamique\n          - Segments m\u00e9moire (stack, heap)\n      - hours: 6\n        chapters:\n          - Impl\u00e9mentation des listes\n          - Queues et files d'attente bas\u00e9e sur les tableaux\n      - hours: 8\n        chapters:\n          - Type de donn\u00e9es r\u00e9cursifs, queues et files d'attente\n      - hours: 4\n        chapters:\n          - Gestion des flux (stdin, stdout, stderr)\n          - Fichiers binaires et textes\n      - hours: 4\n        chapters:\n          - Contr\u00f4les continus\n    laboratory:\n      - hours: 6\n        chapters:\n          - >\n            Mise en \u0153uvre de type de donn\u00e9es compos\u00e9es\n            (structures, tableaux multidimensionnels)\n      - hours: 4\n        chapters:\n          - Lecture et \u00e9criture de fichiers texte et binaire en mode s\u00e9quentiel\n      - hours: 4\n        chapters:\n          - Mise en \u0153uvre de l'allocation dynamique de m\u00e9moire\n      - hours: 2\n        chapters:\n          - Compilation s\u00e9par\u00e9e et impl\u00e9mentation de biblioth\u00e8ques\n      - hours: 4\n        chapters:\n          - Impl\u00e9mentation de types de donn\u00e9es abstraits, type simple, liste tableau\n      - hours: 6\n        chapters:\n          - Impl\u00e9mentation de types de donn\u00e9es abstraits, file, pile\n      - hours: 6\n        chapters:\n          - Mini-projet\nprerequisites: |\n  L'\u00e9tudiant-e doit conna\u00eetre et savoir utiliser les notions suivantes\n      - bases de la programmation en C : types de base, structures\n        de contr\u00f4le et sous-programmes,\n      - utilisation d'un environnement de d\u00e9veloppement,\n        compilation et ex\u00e9cution de programmes.\n  L'unit\u00e9 d'enseignement Informatique 1 permet d'acqu\u00e9rir ces connaissances.\n\ngoals:\n  class:\n    - >\n      D\u00e9composer un algorithme selon l'approche descendante (raffinage successif)\n      et ascendante.\n    - D\u00e9composer une application de complexit\u00e9 moyenne en algorithmes \u00e9l\u00e9mentaires.\n    - Concevoir un type de donn\u00e9es abstrait simple et les fonctions pour le manipuler.\n    - Concevoir une biblioth\u00e8que de fonctions en utilisant la compilation s\u00e9par\u00e9e.\n    - \u00c9crire un programme qui manipule (lecture/\u00e9criture) des fichiers binaires\n    - Lire et g\u00e9n\u00e9rer un fichier de donn\u00e9es tabul\u00e9es (p.ex. csv),\n    - Mettre en \u0153uvre un tableau dynamique avec facteur de croissance,\n    - D\u00e9finir et manipuler un type de donn\u00e9es r\u00e9cursif e.g. liste cha\u00een\u00e9e,\n    - Comprendre le fonctionnement d'un algorithme de tri en O(n log n),\n    - Savoir impl\u00e9menter une recherche dichotomique\n    - Comprendre le fonctionnement du pr\u00e9processeur C\n    - Conna\u00eetre et savoir quand utiliser les diff\u00e9rentes classes de stockage\n    - Conna\u00eetre en d\u00e9tail la notion de pointeur et savoir les utiliser\n    - >\n      Utiliser les fonctions standard de recherche et de manipulation\n      de cha\u00eene de caract\u00e8res (p.ex. strstr, strchr, qsort).\n  laboratory:\n    - R\u00e9unir un ensemble de fonctions dans un module logiciel et l'utiliser\n    - Programmer et mettre au point des algorithmes de complexit\u00e9 moyenne\n    - >\n      R\u00e9aliser une application de taille et de complexit\u00e9 moyennes,\n      m\u00ealants diff\u00e9rents aspects de la programmation\n    - D\u00e9velopper un programme en utilisant un outil de gestion de version\n    - Utiliser un syst\u00e8me de test automatique pour valider le fonctionnement d'un programme.\n\nplan:\n  - Algorithmie\n    - Raffinage successif\n    - D\u00e9composition en \u00e9l\u00e9ments fonctionnels simples\n    - Conception d'algorithmes de complexit\u00e9 moyenne\n  - Types compos\u00e9s\n    - Manipulation d'une structure (struct)\n    - Passage par copie et adresse\n    - Cr\u00e9ation de types (typedef)\n  - Biblioth\u00e8que\n    - Concevoir une biblioth\u00e8que statique\n    - Utilisation d'une biblioth\u00e8que statique dans un programme\n  - Fichiers\n    - Types de fichiers\n    - Binaire\n    - Textes\n    - Donn\u00e9es tabul\u00e9es\n    - Donn\u00e9es index\u00e9es\n    - Syst\u00e8me de fichier\n    - Arborescence\n    - Dossiers\n    - Chemins relatifs et absolus\n    - Manipulation de fichiers\n    - Pointeur de fichier (ftell, fseek)\n    - Lecture (fread, fscanf)\n    - \u00c9criture (fwrite, fprintf)\n  - Gestion de la m\u00e9moire\n    - Pointeurs\n    - R\u00e8gle gauche droite\n    - Arithm\u00e9tique de pointeurs\n    - Types de pointeurs imbriqu\u00e9s (p.ex. int**[])\n    - Allocation dynamique\n    - malloc\n    - calloc\n    - free\n    - Cr\u00e9ation de tableaux dynamiques\n    - Comprendre la diff\u00e9rence entre le stack et le heap\n  - Types de donn\u00e9es r\u00e9cursifs\n    - Liste simplement cha\u00een\u00e9e\n    - Liste doublement cha\u00een\u00e9e\n  - Alignement m\u00e9moire\n    - Unions\n    - Champs de bits\n  - Livraison\n    - Pr\u00e9parer le code \u00e0 la livraison\n    - Construire une biblioth\u00e8que document\u00e9e\n    - Utiliser GitHub pour tracer le d\u00e9veloppement\n    - Utiliser une biblioth\u00e8que de test unitaire\nbibliographie:\n  - author: Jean-Michel L\u00e9ry\n    title: Algorithmique, Applications en C, C++ et Java\n    editor: Pearson\n    year: 2013\n  - standard: ISO/IEC 9899:2011\n    title: Langage de programmation C, ISO/IEC\n    year: 2011\n  - author:\n    Brian Kernighan:\n    Dennis Ritchie:\n    title: Le langage C\n    edition: 2nd\n    editor: Dunod\n    year: 2014\n    isbn: 978-2100715770\n  - author: Claude Delannoy\n    title: Programmer en langage C, Cours et exercices corrig\u00e9s\n    editor: Eyrolles\n    year: 2016\n  - author: Stephen Kochan\n    title: Programming in C\n    edition: 4\n    editor: Pearson\n    year: 2014\n    isbn: 978-0321776419\n
    "}, {"location": "appendix/unit/#panification-du-semestre-de-printemps", "title": "Panification du semestre de printemps", "text": "Semaine Acad\u00e9mique Cours Labo 8 1 Introduction GitHub - WSL 9 2 Fichiers Proust (partie 1) 10 3 Allocation dynamique Proust (partie 2) 11 4 Allocation dynamique M\u00e9t\u00e9o (partie 1) 12 5 Compilation s\u00e9par\u00e9e M\u00e9t\u00e9o (partie 2) 13 6 Pr\u00e9processeur Tableau dynamique (\u00bd) 14 7 Unions, champs de bits Tableau dynamique (2/2) 15 8 Usage biblioth\u00e8ques St\u00e9ganographie 16 Vacances de P\u00e2ques 17 9 TE1 Wave (partie 1) 18 10 Algorithmique Big-O Wave (partie 2) 19 11 Tris Quick-Sort / Merge-Sort 20 12 Queues et piles Tries 21 13 Sockets Labo Test 22 14 TE2 Shunting-yard 23 15 Arbres binaires Tries (partie 1) 24 16 Exercices de r\u00e9vision Tries (partie 2) 25 Pr\u00e9paration Examens 26 Examens"}, {"location": "appendix/unit/#modalites-devaluation-et-de-validation", "title": "Modalit\u00e9s d'\u00e9valuation et de validation", "text": "

    Le cours se compose de\u2009:

    • Travaux \u00e9crits not\u00e9s (coefficient 100%)
    • Quiz not\u00e9s ou non (coefficient 10% ou 0%)
    • S\u00e9ries d'exercices
    • Travaux pratiques, 2 \u00e0 3 labos not\u00e9s (laboratoires)
    • Labo test not\u00e9 comptabilis\u00e9 comme un labo

    La note finale est donn\u00e9e par l'expression\u2009:

    FormuleProgramme C \\[ \\text{final} = \\frac{ \\sum\\limits_{t=1}^\\text{T}{\\text{TE}_t} + 10\\% \\cdot \\sum\\limits_{q=1}^\\text{Q}{\\text{Quiz}_q} }{ 4 \\cdot (\\text{T} + 10\\% \\cdot \\text{Q}) } + \\frac{1}{4 L} \\sum\\limits_{l=1}^L \\text{Labo}_l + \\frac{1}{2} \\text{Exam} \\]
    #define QUIZ_WEIGHT (.1) // Percent\n#define EXAM_WEIGHT (.5) // Percent\n\ntypedef struct notes {\n    size_t size;\n    float values[];\n} Notes;\n\nfloat sum(Notes *notes) {\n    float s = 0;\n    for (int i = 0; i < notes->size; i++)\n        s += notes->values[i];\n    return s;\n}\n\nfloat mark(Notes tes, Notes quizzes, Notes labs, float exam) {\n    return (\n    sum(tes) + QUIZ_WEIGHT * sum(quizzes)\n    ) / (\n        (EXAM_WEIGHT / 2.) * (tes.size + QUIZ_WEIGHT * quizzes.size)\n    ) +\n            (EXAM_WEIGHT / 2.) * sum(labs) / labs.size +\n        EXAM_WEIGHT * exam;\n}\n
    "}, {"location": "appendix/unit/#directives", "title": "Directives", "text": "
    • En cas d'absence \u00e0 un quiz, la note de 1.0 est donn\u00e9e.
    • En cas de plagiat, le dilemme du prisonnier s'applique.
    "}, {"location": "course-c/00-preface/", "title": "Avant-Propos", "text": ""}, {"location": "course-c/00-preface/#a-qui-sadresse-cet-ouvrage", "title": "\u00c0 qui s'adresse cet ouvrage\u2009?", "text": "

    Con\u00e7u comme un r\u00e9sum\u00e9 du savoir n\u00e9cessaire \u00e0 l'ing\u00e9nieur pour s'initier \u00e0 la programmation et prendre en main le langage C, cet ouvrage n'est pas un manuel de r\u00e9f\u00e9rence. Il se r\u00e9f\u00e8re \u00e0 de nombreuses ressources internet et livres que le lecteur pourra consulter au besoin pour approfondir certains concepts.

    Chaque chapitre est compos\u00e9 d'exercices, mais \u00e0 des fins p\u00e9dagogiques, l'int\u00e9gralit\u00e9 des solutions ne sont pas fournies\u2009; certains exercices sont destin\u00e9s \u00e0 \u00eatre faits en \u00e9tudes.

    Cet ouvrage est destin\u00e9 aux \u00e9tudiants futurs ing\u00e9nieurs de premi\u00e8re ann\u00e9e n'ayant aucune exp\u00e9rience en programmation.

    "}, {"location": "course-c/00-preface/#cours-dinformatique-cursus-bachelor", "title": "Cours d'informatique cursus bachelor", "text": "

    Ce cours d'informatique \u00e0 la HEIG-VD est donn\u00e9 par le d\u00e9partement TIN dans les cours du cursus Bachelor en G\u00e9nie \u00c9lectrique. Il concerne tout particuli\u00e8rement les \u00e9tudiants des cours suivants\u2009:

    • Informatique 1 (INFO1) - 101 Premi\u00e8re ann\u00e9e
    • Informatique 2 (INFO2) - 102 Premi\u00e8re ann\u00e9e
    • Microinformatique (MICROINFO) - 101 Premi\u00e8re ann\u00e9e
    "}, {"location": "course-c/00-preface/#quel-programmeur-etes-vous", "title": "Quel programmeur \u00eates-vous\u2009?", "text": "

    Les \u00e9tudes en \u00e9coles d'ing\u00e9nieurs sont souvent cloisonn\u00e9es. On observe, entre les diff\u00e9rentes facult\u00e9s (\u00e9lectronique, informatique, etc.), que l'enseignement de l'informatique s'inscrit dans une culture distincte avec un langage sp\u00e9cifique. Les informaticiens, dot\u00e9s d'un esprit d'abstraction remarquable, acqui\u00e8rent des connaissances approfondies du fonctionnement interne des syst\u00e8mes d'exploitation et poss\u00e8dent une expertise \u00e9tendue en programmation. N\u00e9anmoins, ils manquent parfois d'une exp\u00e9rience pratique avec le mat\u00e9riel \u00e9lectronique et les contraintes impos\u00e9es par des architectures mat\u00e9rielles l\u00e9g\u00e8res (syst\u00e8mes embarqu\u00e9s, microcontr\u00f4leurs, etc.). Les \u00e9lectroniciens, quant \u00e0 eux, disposent d'une compr\u00e9hension approfondie des syst\u00e8mes \u00e0 bas niveau. Ils ont une vision pragmatique des syst\u00e8mes et des contraintes mat\u00e9rielles. Cependant, ils manquent souvent de connaissances pouss\u00e9es en programmation et en algorithmique.

    Ces deux profils, bien que compl\u00e9mentaires, ont souvent du mal \u00e0 se comprendre. Les informaticiens per\u00e7oivent les \u00e9lectroniciens comme trop terre-\u00e0-terre, tandis que les \u00e9lectroniciens jugent les informaticiens trop abstraits. Des divergences d'opinions peuvent \u00e9merger de ces diff\u00e9rences culturelles, notamment dans des notions communes dont les d\u00e9finitions varient. Par exemple, la notion de temps r\u00e9el diff\u00e8re pour un informaticien et un \u00e9lectronicien. Pour un informaticien, le temps r\u00e9el d\u00e9signe un syst\u00e8me qui r\u00e9pond dans un d\u00e9lai d\u00e9termin\u00e9 pour un utilisateur (environ 100 ms). Pour un \u00e9lectronicien, le temps r\u00e9el d\u00e9signe un syst\u00e8me qui r\u00e9pond dans un d\u00e9lai d\u00e9termin\u00e9 et qui est d\u00e9terministe (environ 100 \u00b5s).

    Un autre exemple est la complexit\u00e9 algorithmique. Pour un informaticien, la complexit\u00e9 algorithmique mesure la performance d'un algorithme en termes g\u00e9n\u00e9raux. Un acc\u00e8s \u00e0 un dictionnaire est en \\(O(1)\\), m\u00eame s'il implique le calcul d'un sha256, une op\u00e9ration triviale sur un ordinateur. Pour un \u00e9lectronicien, il est impossible de r\u00e9aliser un sha256 sur un microcontr\u00f4leur 8 bits, ce qui l'incite \u00e0 rechercher des optimisations profondes de l'algorithme, quitte \u00e0 le rendre moins g\u00e9n\u00e9rique et modulaire.

    Cet ouvrage a pour objectif de rapprocher ces deux cultures en fournissant aux \u00e9lectroniciens les bases de la v\u00e9ritable informatique, de la programmation et de sa culture, en rendant accessibles des concepts complexes tels que les arbres et les graphes.

    ", "tags": ["sha256"]}, {"location": "course-c/00-preface/#organisation-de-louvrage", "title": "Organisation de l'ouvrage", "text": ""}, {"location": "course-c/00-preface/#recherche", "title": "Recherche", "text": "

    Pour faciliter la recherche d'informations, vous pouvez utiliser la barre de recherche en haut \u00e0 droite de la page. Vous pouvez \u00e9galement utiliser les raccourcis clavier pour naviguer plus rapidement (voir ci-dessous). Notez que recherche est instantan\u00e9e et vous permet de trouver des informations dans le texte, les titres et les liens.

    "}, {"location": "course-c/00-preface/#theme", "title": "Theme", "text": "

    Selon votre pr\u00e9f\u00e9rence, vous pouvez choisir entre deux th\u00e8mes pour la lecture de ce livre (clair ou sombre ). Pour changer de th\u00e8me, cliquez sur l'ic\u00f4ne \u00e0 gauche de la barre de recherche.

    "}, {"location": "course-c/00-preface/#raccourcis-clavier", "title": "Raccourcis clavier", "text": "

    Pour am\u00e9liorer votre navigation sur ce site, voici quelques raccourcis clavier que vous pouvez utiliser\u2009:

    F, S, /

    Ouvre la barre de recherche

    P, ,

    Va \u00e0 la page pr\u00e9c\u00e9dente

    N, .

    Va \u00e0 la page suivante

    B

    Afficher/cacher les tables des mati\u00e8res

    M

    Afficher/cacher le menu

    H

    Afficher/cacher la table des mati\u00e8res

    "}, {"location": "course-c/00-preface/#cookies", "title": "Cookies", "text": "

    Ce site utilise des cookies pour sauvegarder vos pr\u00e9f\u00e9rences de th\u00e8me ainsi que votre progression dans les exercices.

    Des information d'analyse de fr\u00e9quentation sont \u00e9galement collect\u00e9es pour am\u00e9liorer le contenu de ce livre.

    "}, {"location": "course-c/00-preface/#conventions-decriture", "title": "Conventions d'\u00e9criture", "text": ""}, {"location": "course-c/00-preface/#encodage-de-caractere", "title": "Encodage de caract\u00e8re", "text": "

    Il sera souvent fait mention dans cet ouvrage la notation du type 1F4A9, il s'agit d'une notation Unicode qui ne d\u00e9pend pas d'un quelconque encodage. Parler du caract\u00e8re ASCII 234 est incorrect, car cela d\u00e9pend de la table d'encodage utilis\u00e9e\u2009; en revanche, la notation Unicode est plus pr\u00e9cise.

    La notation est cliquable et vous redirigera vers le site symbl.cc.

    "}, {"location": "course-c/00-preface/#expressions-regulieres", "title": "Expressions r\u00e9guli\u00e8res", "text": "

    Les expressions r\u00e9guli\u00e8res sont utilis\u00e9es pour d\u00e9crire des motifs de texte. Elles sont utilis\u00e9es pour rechercher, remplacer ou valider des cha\u00eenes de caract\u00e8res. Les expressions r\u00e9guli\u00e8res sont utilis\u00e9es dans de nombreux langages de programmation, d'outils de recherche et de traitement de texte.

    Aussi dans cet ouvrage, les expressions r\u00e9guli\u00e8res sont mises en \u00e9vidence avec /regex/. Le lien m\u00e8ne au site regex101.com. Pour tester les expressions r\u00e9guli\u00e8res, il vous suffit alors d'ajouter votre propre texte pour tester l'exemple donn\u00e9.

    "}, {"location": "course-c/00-preface/#symbole-degalite", "title": "Symbole d'\u00e9galit\u00e9", "text": "

    Nous verrons que le signe d'\u00e9galit\u00e9 = peut ais\u00e9ment \u00eatre confondu avec l'op\u00e9rateur d'affectation du langage C qui s'\u00e9crit de la m\u00eame mani\u00e8re. Dans certains exemples o\u00f9 l'on montre une \u00e9galit\u00e9 entre diff\u00e9rentes \u00e9critures, le signe d'\u00e9galit\u00e9 triple 2261 sera utilis\u00e9 pour dissiper toute ambigu\u00eft\u00e9 \u00e9ventuelle\u2009:

    'a' \u2261 0b1100001 \u2261 97 \u2261 0x61 \u2261 00141\n
    "}, {"location": "course-c/00-preface/#symbole-de-remplissage", "title": "Symbole de remplissage", "text": "

    Dans les exemples qui seront donn\u00e9s, on pourra voir while (condition) { \u301c } ou le caract\u00e8re \u301c 3030 indique une continuit\u00e9 logique d'op\u00e9ration. Le symbole exprime ainsi ... (points de suspension ou ellipsis). Or, pour ne pas confondre avec le symbole C ... utilis\u00e9 dans les fonctions \u00e0 arguments variables tels que printf.

    ", "tags": ["printf"]}, {"location": "course-c/00-preface/#types-de-donnees", "title": "Types de donn\u00e9es", "text": "

    Les conventions C s'appliquent \u00e0 la mani\u00e8re d'exprimer les grandeurs suivantes\u2009:

    • 0xABCD pour les valeurs hexad\u00e9cimales /0x[0-9a-f]+/i
    • 00217 pour les valeurs octales /0[0-7]+/
    • 'c' pour les caract\u00e8res /'([^']|\\\\[nrftvba'])'/
    • 123 pour les grandeurs enti\u00e8res /-?[1-9][0-9]*/
    • 12. pour les grandeurs r\u00e9elles en virgule flottante
    "}, {"location": "course-c/00-preface/#encadres", "title": "Encadr\u00e9s", "text": "

    Des encadr\u00e9s sont utilis\u00e9s pour mettre en avant des informations compl\u00e9mentaires ou des astuces. Ils sont \u00e9galement utilis\u00e9s pour donner des informations sur des concepts avanc\u00e9s ou des d\u00e9tails techniques.

    Info

    Fait historique o\u00f9 information compl\u00e9mentaire pour ceux qui voudraient en savoir plus.

    Avertissement

    Point important \u00e0 faire attention qui source d'erreur fr\u00e9quente.

    Danger

    Note importante qui comporte des risques \u00e0 consid\u00e9rer.

    Exemple

    Exemple pratique pour illustrer un concept.

    Note

    Corollaire \u00e0 retenir.

    Astuce

    Truc ou Astuce pour faciliter la compr\u00e9hension.

    Bogue

    Limitations ou bugs possibles d'une m\u00e9thode propos\u00e9e.

    Exercice 1\u2009: Quelle ic\u00f4ne\u2009?

    Quelle ic\u00f4ne est utilis\u00e9e pour les exercices\u2009?

    "}, {"location": "course-c/00-preface/#anglicismes", "title": "Anglicismes", "text": "

    Parler l'informatique ou de technologies sans utiliser d'anglicismes est un exercice difficile. Il est parfois moins lourd de parler de hardware que de mat\u00e9riel informatique. Certains termes n'ont pas de traduction en fran\u00e7ais. Par exemple, le terme set appliqu\u00e9 \u00e0 un ensemble de donn\u00e9es n'a pas de traduction cr\u00e9dible en fran\u00e7ais. La table ci-dessous montre quelques termes qui seront utilis\u00e9s dans cet ouvrage\u2009:

    Anglicismes Anglais Fran\u00e7ais Pr\u00e9f\u00e9rence byte octet byte debug d\u00e9verminer debug hardware mat\u00e9riel informatique hardware listing extrait de code listing pipe tube pipe process processus processus seekable positionnable seekable set ensemble set software logiciel informatique software stream flux de donn\u00e9es stream

    Notons que byte et octet ne sont pas exactement synonymes. Un byte est un ensemble g\u00e9n\u00e9ralement admis de 8 bits mais dont la taille a pu varier selon les ann\u00e9es, alors qu'un octet est un ensemble de 8 bits sans exception. En pratique, les deux termes sont souvent utilis\u00e9s de mani\u00e8re interchangeable. En anglais il n'existe pas de mot pour octet.

    Les termes anglais sont g\u00e9n\u00e9ralement indiqu\u00e9s en italique.

    "}, {"location": "course-c/00-preface/#copyright-et-references", "title": "Copyright et r\u00e9f\u00e9rences", "text": "

    Le contenu de ce livre est sous licence Creative Commons. Vous \u00eates libre de partager et d'adapter ce contenu pour toute utilisation, m\u00eame commerciale, \u00e0 condition de citer l'auteur et de partager vos travaux d\u00e9riv\u00e9s sous la m\u00eame licence.

    De nombreuses r\u00e9f\u00e9rences et sources de ce livre sont issues de Wikipedia, de la documentation officielle de la norme C, de StackOverflow, de forums de discussion et de blogs.

    "}, {"location": "course-c/00-preface/#comment-contribuer", "title": "Comment contribuer\u2009?", "text": "

    Vous avez remarqu\u00e9 une erreur, une faute de frappe ou une information manquante\u2009? Vous auriez d\u00e9sir\u00e9 une explication plus d\u00e9taill\u00e9e sur un sujet\u2009? Vous pouvez contribuer \u00e0 l'am\u00e9lioration de ce livre en soumettant une issue. Alternativement, vous pouvez faire un fork du projet et proposer une pull request. Ce travail est donc une \u0153uvre vivante qui \u00e9volue avec le temps et les contributions de chacun.

    La version imprim\u00e9e sera n\u00e9anmoins fig\u00e9e \u00e0 un moment donn\u00e9, mais la version en ligne sera toujours mise \u00e0 jour.

    "}, {"location": "course-c/00-preface/#colophon", "title": "Colophon", "text": "

    Ce livre est \u00e9crit en Markdown et g\u00e9n\u00e9r\u00e9 en HTML par MkDocs. Le th\u00e8me utilis\u00e9 est Material for MkDocs. Les sources sont disponibles sur GitHub et l'h\u00e9bergement est assur\u00e9 par GitHub Pages.

    La plupart des illustrations sont r\u00e9alis\u00e9es avec Draw.io, un outil de dessin vectoriel en ligne. Les sch\u00e9mas sont rendus dans le navigateur avec GraphViewer. Les diagrammes utilisent la technologie Mermaid. Les autres sources d'images sont issues en grande partie de Wikimedia Commons et Wikipedia.

    La g\u00e9n\u00e9ration de l'ouvrage en PDF utilise son propre convertisseur vers LaTeX. Les extraits de code sources sont color\u00e9s avec Pygments en utilisant le paquet minted.

    L'orthographe et la grammaire ont \u00e9t\u00e9 revues avec Antidote.

    ", "tags": ["GraphViewer"]}, {"location": "course-c/05-introduction/c-lang/", "title": "Le langage C", "text": "C is quirky, flawed, and an enormous success.Dennis Ritchie

    Le langage C est l'un des premiers langages de programmation. Il se situe tr\u00e8s pr\u00e8s de l'assembleur, ce langage de bas niveau utilis\u00e9 par les processeurs. Le C permet ainsi de concevoir des applications extr\u00eamement performantes, et il est exploit\u00e9 dans une multitude de domaines informatiques, que ce soit pour une montre connect\u00e9e, un stimulateur cardiaque (pacemaker) ou encore une machine \u00e0 caf\u00e9.

    Bien qu'il soit ancien (il date de 1972), le langage C reste largement employ\u00e9 et enseign\u00e9, gr\u00e2ce \u00e0 son efficacit\u00e9 et sa capacit\u00e9 \u00e0 inculquer les bases fondamentales de la programmation.

    En v\u00e9rit\u00e9, en 2024, il n'existe gu\u00e8re d'alternative aussi m\u00fbre et \u00e9prouv\u00e9e que le C pour d\u00e9velopper des applications embarqu\u00e9es \u00e0 haute performance ou pour le noyau des syst\u00e8mes d'exploitation. Des langages plus r\u00e9cents tels que Rust ou Zig commencent certes \u00e0 \u00e9merger, mais ils peinent encore \u00e0 s'imposer dans l'industrie.

    ", "tags": ["rust", "zig", "1972"]}, {"location": "course-c/05-introduction/c-lang/#historique", "title": "Historique", "text": "

    En 1964 na\u00eet, d'une collaboration entre les laboratoires Bell (Bell Telephone Laboratories), General Electric et le MIT, le projet Multics (Multiplexed Information and Computing Service), qui vise \u00e0 d\u00e9velopper un nouveau syst\u00e8me d'exploitation.

    Cependant, la fin de la d\u00e9cennie est marqu\u00e9e par des remous. Les laboratoires Bell, d\u00e9sillusionn\u00e9s par les promesses de Multics, d\u00e9cident de se retirer du projet pour \u00e9laborer leur propre syst\u00e8me d'exploitation. Un groupe informel, dirig\u00e9 notamment par Ken Thompson et Dennis Ritchie, entreprend de revoir certains concepts de Multics qui leur d\u00e9plaisaient, notamment le langage de programmation PL/I (Programming Language number 1), alors pr\u00e9dominant pour l\u2019\u00e9criture de syst\u00e8mes d\u2019exploitation. Thompson d\u00e9veloppe un langage baptis\u00e9 B, inspir\u00e9 du BCPL, dans lequel il ne conserve que les \u00e9l\u00e9ments qu'il juge essentiels pour fonctionner sur de petites machines. \u00c0 ce stade, B ne comporte qu\u2019un seul type de donn\u00e9e, le \u00ab\u2009mot\u2009\u00bb (word).

    BCPL, con\u00e7u par Martin Richards au MIT dans les ann\u00e9es 1960, est l'anc\u00eatre de B, et par extension, l'arri\u00e8re-grand-p\u00e8re du C. Dennis Ritchie, alors coll\u00e8gue de Thompson, retravaille le langage B pour y ajouter la gestion des types de donn\u00e9es.

    Le syst\u00e8me d'exploitation que Thompson et Ritchie d\u00e9veloppent aux laboratoires Bell s\u2019appelle d'abord UNICS, par opposition \u00e0 Multics, o\u00f9 Multiplexed est remplac\u00e9 par Uniplexed. Le nom \u00e9volue ensuite pour devenir UNIX, un pilier dans l'histoire de l'informatique.

    Plus tard, Brian Kernighan contribue \u00e0 la popularisation de ce nouveau langage. Il est l'auteur principal du livre The C Programming Language, tandis que Dennis Ritchie s\u2019est concentr\u00e9 sur les annexes.

    Les \u00e9volutions du C continuent, notamment avec Bjarne Stroustrup qui, dans les ann\u00e9es 1980, \u00e9tend le langage en y apportant la programmation orient\u00e9e objet (OOP). Ce concept sera \u00e9tudi\u00e9 dans un autre cours. La figure suivante pr\u00e9sente le trio fondateur du langage C.

    Les p\u00e8res fondateurs du C

    Il faut attendre 1989 pour que le langage C soit normalis\u00e9 par l\u2019ANSI (American National Standards Institute). L\u2019ann\u00e9e suivante, l\u2019ISO (International Organization for Standardization) ratifie le standard ISO/IEC 9899:1990, commun\u00e9ment appel\u00e9 C90. D\u00e8s lors, le C devient un standard international et s\u2019impose comme le langage dominant dans le domaine de l\u2019informatique.

    Les langages de programmation se nourrissent souvent les uns des autres, et le C ne fait pas exception. La figure suivante illustre quelques-unes des influences entre langages\u2009:

    %% Influences des langages de programmation\nflowchart LR\n    COBOL --> PLI[\"PL/I\"]\n    FORTRAN --> ALGOL\n    ALGOL --> CPL\n    CPL --> BCPL\n    ALGOL --> SIMULA\n    ALGOL --> PASCAL\n    FORTRAN --> PASCAL\n    FORTRAN --> PLI\n    BCPL --> B\n    ALGOL --> PLI\n    PLI --> C(\"C\")\n    B --> C
    Influences des langages de programmation

    Cinquante ans plus tard, le C reste l'un des langages les plus utilis\u00e9s par les ing\u00e9nieurs. Alliant une vision de haut niveau avec la possibilit\u00e9 de manipulations de bas niveau, il s\u2019av\u00e8re \u00eatre un choix privil\u00e9gi\u00e9 pour les applications embarqu\u00e9es sur microcontr\u00f4leurs, ou pour l\u2019optimisation de code afin d\u2019obtenir des performances maximales, comme dans les noyaux de syst\u00e8mes d'exploitation tels que le noyau Linux (Kernel) ou Windows.

    Retenons simplement que C est un langage \u00e0 la fois simple et puissant. Votre machine \u00e0 caf\u00e9, votre voiture, vos \u00e9couteurs Bluetooth ont probablement \u00e9t\u00e9, au moins partiellement, programm\u00e9s en C.

    ", "tags": ["bcpl", "linux", "noyau", "martin-richards", "unix", "1989", "1964", "bell", "mit", "multics", "1960", "kernel", "general-electric"]}, {"location": "course-c/05-introduction/c-lang/#standardisation", "title": "Standardisation", "text": "

    Comme nous l'avons vu, le langage C a un long historique. Il a fallu attendre pr\u00e8s de vingt ans apr\u00e8s sa cr\u00e9ation pour qu\u2019il fasse l\u2019objet d\u2019une normalisation internationale.

    Le standard le plus couramment utilis\u00e9 en 2024 reste sans doute C99. C11 commence \u00e0 le remplacer dans l'industrie, mais l'\u00e9volution se poursuit avec C17, C18 et C23. La figure suivante r\u00e9sume les diff\u00e9rents standards internationaux du C\u2009:

    Normes internationales du langage C Notation courte Standard international Date C n/a 1972 K&R C n/a 1978 C89 (ANSI C) ANSI X3.159-1989 1989 C90 ISO/IEC 9899:1990 1990 C99 ISO/IEC 9899:1999 1999 C11 ISO/IEC 9899:2011 2011 C17/C18 ISO/IEC 9899:2018 2018 C23 ISO/IEC 9899:2023 2023

    En substance, C18 n'apporte pas de nouvelles fonctionnalit\u00e9s majeures au langage, mais se concentre sur la clarification des ambigu\u00eft\u00e9s laiss\u00e9es par C11, qui, lui-m\u00eame, n\u2019introduisait que peu de changements fondamentaux pour le d\u00e9veloppement sur microcontr\u00f4leurs.

    Info

    Vous entendrez ou lirez souvent des r\u00e9f\u00e9rences \u00e0 ANSI C ou K&R. Privil\u00e9giez toutefois une compatibilit\u00e9 avec C99 au minimum.

    Le standard C est, il faut bien l\u2019admettre, dense et ardu \u00e0 lire. Avec ses quelque 552 pages pour C99, il est peu probable que vous y trouviez un grand plaisir. Et pourtant, il est parfois n\u00e9cessaire de s\u2019y plonger pour d\u00e9m\u00ealer certaines subtilit\u00e9s du langage, rarement explicit\u00e9es dans les manuels. Vous vous retrouverez un jour ou l\u2019autre confront\u00e9 \u00e0 des probl\u00e8mes non document\u00e9s, et c\u2019est souvent dans le standard que se trouve la solution.

    Comme mentionn\u00e9 plus haut, bien que C99 soit le standard le plus utilis\u00e9 en 2024, il a d\u00e9j\u00e0 plus de 25 ans. Vous vous demandez peut-\u00eatre pourquoi l'industrie semble si en retard face aux derni\u00e8res versions. Contrairement au domaine des logiciels grand public, o\u00f9 chaque mise \u00e0 jour est adopt\u00e9e avec enthousiasme, le secteur industriel est r\u00e9gi par des processus de validation stricts. Migrer vers un nouveau standard est une op\u00e9ration co\u00fbteuse, tant en termes de tests que de conformit\u00e9, et les entreprises pr\u00e9f\u00e8rent souvent s\u2019en tenir \u00e0 des versions \u00e9prouv\u00e9es plut\u00f4t que de risquer de co\u00fbteuses erreurs, notamment dans des domaines critiques comme l\u2019a\u00e9ronautique ou la m\u00e9decine.

    ", "tags": ["c17", "c18", "c23", "c11", "normalisation"]}, {"location": "course-c/05-introduction/c-lang/#le-c-et-les-autres", "title": "Le C et les autres...", "text": "

    Si ce cours se concentre principalement sur le langage C, il est loin d'\u00eatre le seul langage de programmation, et ce n'est certainement pas le seul que vous apprendrez au cours de votre carri\u00e8re. La table suivante pr\u00e9sente une liste non exhaustive de langages de programmation ainsi que leur ann\u00e9e de cr\u00e9ation. Cette liste permet de mieux comprendre l'\u00e9volution des langages de programmation et leurs usages typiques\u2009:

    Langages de programmation et leur ann\u00e9e de cr\u00e9ation Langage de programmation Ann\u00e9e Utilisation Fortran 1957 Calcul scientifique Lisp 1958 Intelligence artificielle Cobol 1959 Finance, banque Basic 1964 Enseignement Pascal 1970 Enseignement C 1972 Syst\u00e8mes embarqu\u00e9s C++ 1985 Applications lourdes Perl 1987 Scripts Python 1991 Ing\u00e9nierie, sciences Ruby 1995 Scripts, Web Java 1995 Applications lourdes PHP 1995 Web C# 2000 Applications graphiques Go 2009 Syst\u00e8mes distribu\u00e9s Rust 2010 Syst\u00e8mes embarqu\u00e9s Swift 2014 Applications mobiles Zig 2016 Syst\u00e8mes embarqu\u00e9s

    L'index TIOBE constitue un excellent indicateur de la popularit\u00e9 des langages de programmation. Il est mis \u00e0 jour mensuellement et permet de suivre l'\u00e9volution de la popularit\u00e9 des diff\u00e9rents langages. En 2024, le classement des 10 langages de programmation les plus populaires est pr\u00e9sent\u00e9 dans la table suivante\u2009:

    Top 10 des langages de programmation Top 10 Langage de programmation 1 Python 2 C++ 3 C 4 Java 5 C# 6 JavaScript 7 Go 8 SQL 9 Visual Basic 10 Fortran

    Sur le podium, Python est un langage de tr\u00e8s haut niveau simple \u00e0 apprendre, mais \u00e9loign\u00e9 du mat\u00e9riel. C++ est un langage de programmation orient\u00e9e objet, tr\u00e8s puissant, mais complexe \u00e0 dompter. Avec la m\u00e9daille d'argent, C est un excellent compromis entre les deux, il est simple, mais permet de comprendre les bases de la programmation et de la manipulation du mat\u00e9riel. C'est pour cela que ce cours est bas\u00e9 sur le langage C. Ai-je r\u00e9ussi \u00e0 vous convaincre\u2009?

    ", "tags": ["python", "c"]}, {"location": "course-c/05-introduction/c-lang/#programmation-texte-structuree", "title": "Programmation texte structur\u00e9e", "text": "

    Le C comme la plupart des langages de programmation utilise du texte structur\u00e9, c'est-\u00e0-dire que le langage peut \u00eatre d\u00e9fini par un vocabulaire, une grammaire et se compose d'un alphabet. \u00c0 l'inverse des langages naturels comme le Fran\u00e7ais, un langage de programmation est un langage formel et se veut exact dans sa grammaire et son vocabulaire, il n'y a pas de cas particuliers ni d'ambigu\u00eft\u00e9s possibles dans l'\u00e9criture. Les compilateurs sont ainsi construits autour d'une grammaire du langage qui est r\u00e9duite au minimum par souci d'\u00e9conomie de m\u00e9moire, pour taire les ambigu\u00eft\u00e9s et accro\u00eetre la productivit\u00e9 du d\u00e9veloppeur.

    Pour mieux comprendre, voici un exemple sous forme de pseudo-code utilisant une grammaire simple\u2009:

    POUR CHAQUE \u0153uf DANS le panier :\n    jaune, blanc \u2190 CASSER(\u0153uf)\n    omelette \u2190 MELANGER(jaune, blanc)\n    omelette_cuite \u2190 CUIRE(omelette)\n\nSERVIR(omelette_cuite)\n

    La structure de la phrase permettant de traiter tous les \u00e9l\u00e9ments d'un ensemble d'\u00e9l\u00e9ments (les \u0153ufs d'un panier) peut alors s'\u00e9crire de fa\u00e7on g\u00e9n\u00e9rique comme suit\u2009:

    POUR CHAQUE \u301c DANS \u301c:\n    \u301c\n

    o\u00f9 les \u301c sont des marques substitutives (placeholder) qui seront remplac\u00e9es par le d\u00e9veloppeur par ce qui convient.

    Les grammaires des langages de programmation sont souvent formalis\u00e9es \u00e0 l'aide d'un m\u00e9talangage, c'est-\u00e0-dire un langage qui permet de d\u00e9crire un langage. On l'appelle la grammaire du langage C. C'est un peu le Bescherelle du C. On observe dans ce formalisme une syntaxe rigoureuse, l'utilisation de termes en majuscules, la s\u00e9paration de mots par des virgules, la pr\u00e9sence de parenth\u00e8ses et de fl\u00e8ches (\u2190). Cette syntaxe diff\u00e8re d'un langage \u00e0 l'autre, mais selon le paradigme du langage de grandes similarit\u00e9s peuvent exister.

    "}, {"location": "course-c/05-introduction/c-lang/#les-paradigmes-de-programmation", "title": "Les paradigmes de programmation", "text": "

    Chaque langage de programmation que ce soit C, C++, Python, ADA, COBOL et Lisp sont d'une mani\u00e8re g\u00e9n\u00e9rale assez proche les uns des autres. Nous citions par exemple le langage B, pr\u00e9curseur du C (c. f. [thompson]{c-history}). Ces deux langages, et bien que leurs syntaxes soient diff\u00e9rentes, ils demeurent assez proches, comme l'espagnol et l'italien qui partagent des racines latines. En programmation on dit que ces langages partagent le m\u00eame paradigme de programmation.

    Certains paradigmes sont plus adapt\u00e9s que d'autres \u00e0 la r\u00e9solution de certains probl\u00e8mes et de nombreux langages de programmation sont dit multi-paradigmes, c'est-\u00e0-dire qu'ils supportent diff\u00e9rents paradigmes.

    Nous citions plus haut le C++ qui permet la programmation orient\u00e9e objet, laquelle est un paradigme de programmation qui n'existe pas en C. Ce qu'il est essentiel de retenir c'est qu'un langage de programmation peut ais\u00e9ment \u00eatre substitu\u00e9 par un autre pour autant qu'ils s'appuient sur les m\u00eames paradigmes.

    Le langage C r\u00e9pond aux paradigmes suivants\u2009:

    Imp\u00e9ratif

    Programmation en s\u00e9quences de commandes, qui se lisent dans un ordre donn\u00e9 (de haut en bas).

    Structur\u00e9

    Programmation imp\u00e9rative poss\u00e9dant des structures de d\u00e9cision imbriqu\u00e9es comme les boucles et les conditions.

    Proc\u00e9dural

    Programmation imp\u00e9rative poss\u00e9dant des appels de proc\u00e9dures isol\u00e9es qui regroupent une s\u00e9quence d'instructions.

    D'autres langages comme le C++ apportent les paradigmes suppl\u00e9mentaires \u00e0 C\u2009:

    Fonctionnel

    Programmation bas\u00e9e sur l'appel de fonction. Utilis\u00e9 dans les langages Lisp, Haskell, Erlang.

    Orient\u00e9 objet

    Programmation bas\u00e9e sur la d\u00e9finition de classes et d'objets. Utilis\u00e9 dans les langages C++, Java, Python. Une classe associe des donn\u00e9es \u00e0 des actions qui manipulent ces donn\u00e9es.

    Des langages de plus haut niveau comme Python ou C# apportent davantage de paradigmes comme la programmation r\u00e9flective ou la programmation \u00e9v\u00e9nementielle.

    Ce que nous devons retenir c'est que le langage C est imp\u00e9ratif et proc\u00e9dural, c'est-\u00e0-dire qu'il est bas\u00e9 sur des s\u00e9quences d'instructions s\u00e9par\u00e9es les unes des autres qui s'ex\u00e9cutent dans un ordre donn\u00e9 et lesquelles peuvent \u00eatre regroup\u00e9es en proc\u00e9dures. En reprenant notre exemple d'omelette, si nous souhaitons cette fois-ci r\u00e9aliser une bonne p\u00e2te \u00e0 cr\u00eapes, nous pourrions \u00e9crire\u2009:

    POUR REALISER un \u0153uf:\n    CHERCHER poule\n    \u0153uf \u2190 PONDRE(poule)\n\nPOUR REALISER du lait:\n    CHERCHER vache\n    lait \u2190 TRAITRE(vache)\n\nPOUR REALISER de la farine:\n    PLANTER bl\u00e9\n    ATTENDRE 6 mois\n    moisson \u2190 MOISSONNER(bl\u00e9)\n    farine \u2190 MOUDRE(moisson)\n\nPOUR REALISER une p\u00e2te \u00e0 cr\u00e8pes:\n    \u0153uf \u2190 REALISER(\u0153uf)\n    jaune, blanc \u2190 CASSER(\u0153uf)\n    \u0153uf-liquide \u2190 MELANGER(jaune, blanc)\n    farine \u2190 REALISER(farine)\n    lait \u2190 REALISER(lait)\n    p\u00e2te \u2190 MELANGER(\u0153uf-liquide, farine, lait)\n

    Dans cet exemple, les s\u00e9quences d'instructions ont \u00e9t\u00e9 regroup\u00e9es en proc\u00e9dures, c'est de la programmation proc\u00e9durale. Les proc\u00e9dures permettent de d\u00e9couper un programme en morceaux plus petits, plus faciles \u00e0 comprendre et \u00e0 maintenir.

    "}, {"location": "course-c/05-introduction/c-lang/#cycle-de-developpement", "title": "Cycle de d\u00e9veloppement", "text": "

    Savoir \u00e9crire un programme en C n'est qu'une facette de la programmation. Il est important de comprendre que la programmation est un processus it\u00e9ratif qui n\u00e9cessite de suivre un cycle de d\u00e9veloppement logiciel. Ce cycle de d\u00e9veloppement comprend des \u00e9tapes menant de l'\u00e9tude \u00e0 l'analyse d'un probl\u00e8me jusqu'\u00e0 la r\u00e9alisation d'un programme informatique ex\u00e9cutable. Dans l'industrie, il existe de nombreux mod\u00e8les comme le Cycle en V ou le mod\u00e8le en cascade que nous verrons plus en d\u00e9tail plus tard (Mod\u00e8les de d\u00e9veloppement). Quel que soit le mod\u00e8le utilis\u00e9, il comprendra les \u00e9tapes suivantes\u2009:

    1. \u00c9tude et analyse du probl\u00e8me
    2. \u00c9criture d'un cahier des charges (sp\u00e9cifications)
    3. \u00c9criture de tests \u00e0 r\u00e9aliser pour tester le fonctionnement du programme
    4. Conception d'un algorithme
    5. Transcription de cet algorithme en utilisant le langage C
    6. Compilation du code et g\u00e9n\u00e9ration d'un ex\u00e9cutable
    7. Test de fonctionnement
    8. V\u00e9rification que le cahier des charges est respect\u00e9
    9. Livraison du programme

    Mis \u00e0 part la derni\u00e8re \u00e9tape o\u00f9 il n'y a pas de retour en arri\u00e8re possible, les autres \u00e9tapes sont it\u00e9ratives. Il est tr\u00e8s rare d'\u00e9crire un programme juste du premier coup. Durant tout le cycle de d\u00e9veloppement logiciel, des it\u00e9rations successives sont faites pour permettre d'optimiser le programme, de r\u00e9soudre des bogues, d'affiner les sp\u00e9cifications, d'\u00e9crire davantage de tests pour renforcer l'assurance d'un bon fonctionnement du programme et d\u2019\u00e9viter une coul\u00e9e de lave.

    "}, {"location": "course-c/05-introduction/c-lang/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    Le langage C \u00e0 une particularit\u00e9 que d'autres langages n'ont pas, il comporte une double grammaire. Le processus de compilation s'effectue donc en deux \u00e9tapes.

    1. Le pr\u00e9processeur qui enl\u00e8ve les commentaires du d\u00e9veloppeur et regroupe en un fichier les diff\u00e9rentes parties du programme.
    2. La compilation \u00e0 proprement parler du code source en un fichier binaire.

    Vient ensuite la phase d'\u00e9dition des liens ou linkage lors de laquelle le programme ex\u00e9cutable est cr\u00e9\u00e9 \u00e0 partir des fichiers binaires g\u00e9n\u00e9r\u00e9s lors de la compilation. La figure suivante illustre le cycle de compilation d'un programme C.

    Cycle de compilation illustr\u00e9

    "}, {"location": "course-c/05-introduction/c-lang/#preprocesseur-pre-processing", "title": "Pr\u00e9processeur (pre-processing)", "text": "

    La phase de preprocessing permet de g\u00e9n\u00e9rer un fichier interm\u00e9diaire en langage C dans lequel toutes les instructions n\u00e9cessaires \u00e0 la phase suivante sont pr\u00e9sentes. Le preprocessing r\u00e9alise le remplacement des directives du pr\u00e9processeur de d\u00e9finitions par leurs valeurs r\u00e9sultantes. Ce pr\u00e9processeur permet d'inclure des fichiers externes, de d\u00e9finir des valeurs constantes ou de conditionner l'ex\u00e9cution de certaines parties du code par exemple avec des options de configuration. Avec le compilateur gcc il est possible de demander uniquement cette \u00e9tape avec l'option -E. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    Processus de pr\u00e9processing

    Lorsque vous \u00e9crivez votre programme, vous le faites en utilisant des fichiers sources avec l'extension .c. N\u00e9anmoins, dans votre programme, vous vous basez sur de nombreuses biblioth\u00e8ques logicielles qui donnent acc\u00e8s \u00e0 des fonctions pr\u00e9d\u00e9finies. Ces biblioth\u00e8ques sont incluses dans votre programme \u00e0 l'aide de la directive #include. Lors de la compilation, le pr\u00e9processeur va remplacer ces directives par le contenu des fichiers d'en-t\u00eate correspondants. Par exemple, la directive #include <stdio.h> sera remplac\u00e9e par le contenu du fichier stdio.h qui contient les d\u00e9clarations des fonctions de la biblioth\u00e8que standard d'entr\u00e9es sorties. Cette proc\u00e9dure prend donc en entr\u00e9e un fichier source et un ou plusieurs fichiers d'en-t\u00eate et le transforme en un fichier source unique.

    ", "tags": ["stdio.h", "gcc"]}, {"location": "course-c/05-introduction/c-lang/#compilation-build", "title": "Compilation (build)", "text": "

    La phase de compilation consiste en une analyse syntaxique du fichier \u00e0 compiler selon la grammaire du langage puis en sa traduction en langage assembleur pour le processeur cible. Le fichier g\u00e9n\u00e9r\u00e9 est un fichier binaire (extension .o ou .obj) qui sera utilis\u00e9 pour la phase suivante. Lors de la compilation, des erreurs peuvent survenir et emp\u00eacher le d\u00e9roulement complet de la g\u00e9n\u00e9ration de l'ex\u00e9cutable final. L\u00e0 encore, la correction des erreurs passe toujours par un examen minutieux des messages d'erreur.

    \u00c0 l'instar de l'option -E vue plus haut, il est aussi possible de ne demander que l'assemblage d'un code avec l'option -S. \u00c0 partir d'un fichier pr\u00e9-process\u00e9, le compilateur g\u00e9n\u00e8re un fichier assembleur qui est un fichier texte lisible par un humain (qui conna\u00eet l'assembleur) et qui contient les instructions du processeur cible. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    Assemblage d'un programme C pr\u00e9-process\u00e9 en assembleur

    Une fois g\u00e9n\u00e9r\u00e9 le fichier assembleur, il doit encore est transform\u00e9 en langage machine, c'est-\u00e0-dire en un fichier binaire. Cette \u00e9tape est r\u00e9alis\u00e9e par un programme appel\u00e9 as qui prend en entr\u00e9e le fichier assembleur et g\u00e9n\u00e8re un fichier binaire comme le montre la figure suivante.

    Traduction d'un programme C pr\u00e9-process\u00e9 en objet binaire

    "}, {"location": "course-c/05-introduction/c-lang/#edition-de-liens-link", "title": "\u00c9dition de liens (link)", "text": "

    L'\u00e9dition de liens permet d'assembler ensemble les diff\u00e9rents fichiers binaires (.o) issus de la compilation et d'autres fichiers binaires n\u00e9cessaires au programme pour former un ex\u00e9cutable complet. Ces autres fichiers binaires sont appel\u00e9s des biblioth\u00e8ques ou plus commun\u00e9ment librairies. Elles peuvent appartenir au syst\u00e8me d'exploitation, ou avoir \u00e9t\u00e9 install\u00e9es manuellement avec l'environnement de d\u00e9veloppement. L'\u00e9dition de liens a pour r\u00f4le de r\u00e9soudre les r\u00e9f\u00e9rences entre les diff\u00e9rents fichiers binaires et de g\u00e9n\u00e9rer un ex\u00e9cutable complet.

    Imaginez un livre dont vous \u00eates le h\u00e9ros. Plusieurs auteurs diff\u00e9rents peuvent prendre en charge des chapitres diff\u00e9rents et lors des choix laissez des marques substitutives pour le num\u00e9ro de page o\u00f9 le lecteur doit se rendre\u2009:

    Face \u00e0 cette horde de cr\u00e9atures, vous avez le choix entre\u2009: attaquer le Gol\u00e8me qui semble \u00eatre le chef, rendez-vous \u00e0 la page XXX, ou fuir par la porte d\u00e9rob\u00e9e, rendez-vous \u00e0 la page XXX.

    Naturellement vous ne conna\u00eetrez le num\u00e9ro de page exact qu'une fois que tous les chapitres seront r\u00e9unis. L'\u00e9dition de liens est un peu comme l'assemblage de tous les chapitres pour former un livre complet, elle s'occupe de remplacer les marques substitutives par les bons num\u00e9ros de pages. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    \u00c9dition des liens de plusieurs objets

    "}, {"location": "course-c/05-introduction/c-lang/#hello-world_1", "title": "Hello World\u2009!", "text": "

    Il est traditionnellement coutume depuis la publication en 1978 du livre The C Programming Language de reprendre l'exemple de Brian Kernighan comme premier programme.

    hello.c
    #include <stdio.h>\n\nint main(void)\n{\n    printf(\"hello, world\\n\");\n    return 0;\n}\n

    Ce programme est compos\u00e9 de deux parties. L'inclusion de la biblioth\u00e8que standard d'entr\u00e9es sorties (STandarD Inputs Outputs) \u00e0 l'aide d'une directive pr\u00e9processeur qui d\u00e9finit l'existence de la fonction printf qui vous permet d'\u00e9crire sur le terminal. Le programme principal est nomm\u00e9 main et tout ce qui se situe \u00e0 l'int\u00e9rieur des accolades { } appartient \u00e0 ce dernier. L'ensemble que d\u00e9finit main et ses accolades est appel\u00e9 une fonction, et la t\u00e2che de cette fonction est ici d'appeler une autre fonction printf. On prend soin de terminer chaque instruction par un point-virgule ;.

    L'appel d'une fonction comme printf peut prendre des param\u00e8tres comme ici le texte Hello world!\\n dont le \\n repr\u00e9sente un retour \u00e0 la ligne.

    Une fois ce code \u00e9crit, il faut le compiler. Pour bien comprendre ce que l'on fait, utilisons la ligne de commande\u2009; plus tard vous utiliserez votre \u00e9diteur de texte favori pour \u00e9crire vos programmes.

    Pour obtenir un invit\u00e9 de commande, vous devez ouvrir un terminal. Comme nous avons choisi de travailler sur un syst\u00e8me compatible POSIX, sur n'importe quel syst\u00e8me d'exploitation vous lancez un terminal et sous Windows vous devez installer WSL2. Une fois lanc\u00e9e la console ressemble \u00e0 ceci\u2009:

    $\n

    C'est intimidant si l'on n\u2019en a pas l'habitude, mais vraiment puissant, croyez-moi\u2009! La premi\u00e8re \u00e9tape est de s'assurer que le fichier test.c contient bien notre programme. Pour ce faire on utilise un autre programme cat qui ne fait rien d'autre que lire le fichier pass\u00e9 en argument et de l'afficher sur la console\u2009:

    $ cat hello.c\n#include <stdio.h>\n\nint main()\n{\n    printf(\"hello, world\");\n}\n

    \u00c9videmment, vous devez avoir \u00e9crit le programme hello.c au pr\u00e9alable. Alternativement vous pouvez utiliser la commande suivante pour cr\u00e9er le fichier hello.c :

    echo '#include <stdio.h>\\n\\nint main()\\n{\\n  printf(\"hello, world\");\\n}' > hello.c\n

    \u00c0 pr\u00e9sent on peut utiliser notre compilateur par d\u00e9faut\u2009: cc pour C Compiler. Ce compilateur prend en argument un fichier C et sans autre option, il g\u00e9n\u00e8rera un fichier a.out pour assembler output. C'est un fichier ex\u00e9cutable que l'on peut donc ex\u00e9cuter.

    Utilisez donc la commande suivante pour compiler votre programme\u2009:

    $ gcc hello.c\n

    Rien ne s'est affich\u00e9\u2009? C'est une excellente nouvelle\u2009! La philosophie POSIX veut qu'un programme soit aussi discret que possible\u2009: si tout s'est bien d\u00e9roul\u00e9, il n'est pas n\u00e9cessaire d'en informer l'utilisateur. Toutefois, cela ne signifie pas que la commande n'a eu aucun effet. En r\u00e9alit\u00e9, vous devriez maintenant trouver dans le r\u00e9pertoire courant votre fichier source ainsi que le r\u00e9sultat de la compilation, \u00e0 savoir le fichier a.out. Pour v\u00e9rifier cela, nous allons utiliser le programme ls, qui liste les fichiers pr\u00e9sents dans un r\u00e9pertoire\u2009:

    $ ls\nhello.c       a.out\n

    Parfait, nous avons bien les deux fichiers. Maintenant, ex\u00e9cutons le programme en prenant soin de pr\u00e9fixer le nom par ./, car les programmes g\u00e9n\u00e9r\u00e9s localement, comme a.out, ne peuvent pas \u00eatre lanc\u00e9s directement par leur nom pour des raisons de s\u00e9curit\u00e9. En effet, imaginez qu'un pirate malintentionn\u00e9 cr\u00e9e un programme nomm\u00e9 ls dans ce r\u00e9pertoire, qui effacerait toutes vos donn\u00e9es. Si vous ex\u00e9cutez la commande ls pour voir le contenu du r\u00e9pertoire, vous lanceriez involontairement ce programme malveillant avec des cons\u00e9quences d\u00e9sastreuses. Pour \u00e9viter ce type de probl\u00e8me, tout programme local doit \u00eatre explicitement pr\u00e9fix\u00e9 par ./ pour pouvoir \u00eatre ex\u00e9cut\u00e9. \u00c0 vous de jouer\u2009:

    $ ./a.out\nhello, world\n

    F\u00e9licitations, le programme s'est ex\u00e9cut\u00e9 correctement\u2009! Mais \u00e0 pr\u00e9sent, nous pouvons en apprendre davantage sur ce fichier. Par exemple, nous pourrions examiner la date de cr\u00e9ation du programme ainsi que l'espace qu'il occupe sur votre disque. Encore une fois, ls nous sera utile, cette fois-ci avec l'option l :

    $ ls -l a.out\n-rwxr-xr-- 1 ycr iai 8.2K Jul 24 09:50 a.out*\n

    Voyons ensemble le d\u00e9tail de ce charabia lu de gauche \u00e0 droite\u2009:

    -             Il s'agit d'un fichier\nrwx           Lisible (r), \u00c9ditable (w) et Ex\u00e9cutable (x) par le propri\u00e9taire\nr-x           Lisible (r) et Ex\u00e9cutable (x) par le groupe\nr--           Lisible (r) par les autres utilisateurs\n1             Nombre de liens mat\u00e9riels pour ce fichier\nycr           Nom du propri\u00e9taire\niai           Nom du groupe\n8.2K          Taille du fichier, soit 8200 bytes soit 65'600 bits\nJul 24 09:50  Date de cr\u00e9ation du fichier\na.out         Nom du fichier\n

    hello, world

    Les puristes peuvent se demander s'il est pr\u00e9f\u00e9rable d'\u00e9crire hello, world, hello, world! ou Hello, world!\\n. Dans son livre, Brian Kernighan a opt\u00e9 pour hello, world\\n, et c'est cette version que nous avons reprise ici.

    Au-del\u00e0 de ce souci du d\u00e9tail, il est important de souligner que la casse des caract\u00e8res a une grande importance en informatique. Hello n'est pas \u00e9quivalent \u00e0 hello, car le stockage en m\u00e9moire diff\u00e8re, et par cons\u00e9quent, le r\u00e9sultat de l'ex\u00e9cution d'un programme peut varier.

    Il est donc primordial de pr\u00eater attention \u00e0 ces subtilit\u00e9s. Vous le constaterez au fil du temps\u2009: vous d\u00e9velopperez une aisance naturelle pour rep\u00e9rer les ; manquants, les {} mal plac\u00e9es ou encore les == qui auraient d\u00fb \u00eatre =.

    Cependant, ce qui prime avant tout, c'est la coh\u00e9rence de l'ensemble. Si vous choisissez d'\u00e9crire Hello, World!, veillez \u00e0 le faire de mani\u00e8re uniforme, que ce soit dans vos exemples, vos commentaires ou l'ensemble de votre documentation.

    ", "tags": ["posix", "test.c", "1978", "bibliotheque-standard", "main", "hello", "brian-kernighan", "printf", "compiler", "Hello", "hello.c", "a.out"]}, {"location": "course-c/05-introduction/c-lang/#conclusion", "title": "Conclusion", "text": "

    Le langage C, invent\u00e9 dans les ann\u00e9es 70 par des pionniers de l'informatique, reste aujourd'hui un pilier fondamental dans le monde de la programmation, notamment pour le d\u00e9veloppement d'applications embarqu\u00e9es et de syst\u00e8mes d'exploitation. Son efficacit\u00e9, sa proximit\u00e9 avec le mat\u00e9riel, et sa capacit\u00e9 \u00e0 offrir un contr\u00f4le pr\u00e9cis des ressources en font un langage toujours pertinent, malgr\u00e9 l'\u00e9mergence de concurrents modernes comme Rust ou Zig.

    Son histoire, riche et marqu\u00e9e par des figures embl\u00e9matiques telles que Dennis Ritchie et Ken Thompson, ainsi que son influence sur de nombreux autres langages, t\u00e9moigne de sa long\u00e9vit\u00e9 et de son importance. Apprendre le C, c'est non seulement saisir les bases de la programmation, mais aussi acqu\u00e9rir des comp\u00e9tences indispensables pour tout d\u00e9veloppeur d\u00e9sireux de ma\u00eetriser les rouages du mat\u00e9riel et des syst\u00e8mes informatiques.

    Le d\u00e9veloppement en C suit un cycle rigoureux, comportant plusieurs \u00e9tapes que chaque d\u00e9veloppeur doit comprendre. Maintenant que vous avez r\u00e9ussi \u00e0 compiler votre premier programme, vous \u00eates pr\u00eat pour la suite...

    ", "tags": ["zig", "rust"]}, {"location": "course-c/05-introduction/c-lang/#exercices-de-revision", "title": "Exercices de R\u00e9vision", "text": "

    Exercice 1\u2009: Exercice

    Ouvrez le standard C99 et cherchez la valeur maximale possible de la constante ULLONG_MAX. Que vaut-elle\u2009?

    Solution

    Au paragraphe \u00a75.2.4.2.1-1 on peut lire que ULLONG_MAX est encod\u00e9 sur 64-bits et donc que sa valeur est \\(2^{64}-1\\) donc 18'446'744'073'709'551'615.

    Exercice 2\u2009: Hello World

    Pouvez-vous \u00e9crire, puis compiler votre premier programme en C\u2009? R\u00e9diger le programme hello.c qui affiche Hello, World! \u00e0 l'\u00e9cran.

    Ex\u00e9cutez le programme et v\u00e9rifiez que le message s'affiche bien.

    Exercice 3\u2009: Auteurs

    Qui a invent\u00e9 le C\u2009?

    • Ken Thompson
    • Brian Kernighan
    • Bjarne Stroustrup
    • Linus Torvalds
    • Dennis Ritchie
    • Guido van Rossum

    Exercice 4\u2009: Standard International

    Quel est le standard C \u00e0 utiliser dans l'industrie en 2024 et pourquoi\u2009?

    • C89
    • C99
    • C11
    • C17
    • C23
    Solution

    Le standard industriel, malgr\u00e9 que nous soyons en 2024 est toujours ISO/IEC 9899:2017, car peu de changements majeurs ont \u00e9t\u00e9 apport\u00e9s au langage depuis et les entreprises pr\u00e9f\u00e8rent migrer sur C++ plut\u00f4t que d'adopter un standard plus r\u00e9cent qui n'apporte que peu de changements.

    Exercice 5\u2009: Paradigmes

    Quels est le paradigme de programmation support\u00e9s par C\u2009?

    • Fonctionnel
    • Orient\u00e9 objet
    • R\u00e9flectif
    • Imp\u00e9ratif
    • D\u00e9claratif
    Solution

    C supporte les paradigmes imp\u00e9ratifs, structur\u00e9s et proc\u00e9dural.

    Exercice 6\u2009: Langage imp\u00e9ratif

    Pourriez-vous d\u00e9finir ce qu'est la programmation imp\u00e9rative\u2009?

    Solution

    La programmation imp\u00e9rative consiste en des s\u00e9quences de commandes ordonn\u00e9es. C'est-\u00e0-dire que les s\u00e9quences sont ex\u00e9cut\u00e9es dans un ordre d\u00e9finis les unes \u00e0 la suite d\u2019autres.

    Exercice 7\u2009: Coul\u00e9e de lave

    Qu'est-ce qu'une coul\u00e9e de lave en informatique\u2009?

    Solution

    Lorsqu'un code immature est mis en production, l'industriel qui le publie risque un retour de flamme d\u00fb aux bogues et m\u00e9contentement des clients. Afin d'\u00e9viter une coul\u00e9e de lave il est important qu'un programme soit test\u00e9 et soumis \u00e0 une \u00e9quipe de beta-testing qui s'assure qu'outre le respect des sp\u00e9cifications initiales, le programme soit utilisable facilement par le public cible. Il s'agit aussi d'\u00e9tudier l'ergonomie du programme.

    Un programme peut respecter le cahier des charges, \u00eatre convenablement test\u00e9, fonctionner parfaitement, mais \u00eatre difficile \u00e0 l'utilisation, car certaines fonctionnalit\u00e9s sont peu ou pas document\u00e9es. La surcharge du service de support par des clients perdus peut \u00e9galement \u00eatre assimil\u00e9e \u00e0 une coul\u00e9e de lave.

    Exercice 8\u2009: Cat

    Qu'est-ce que cat?

    • Un programme de chat
    • Un programme de compilation
    • Un programme d'affichage de fichiers
    • Un programme de copie de fichiers
    • Un programme de recherche de fichiers
    Solution

    cat est un programme normalis\u00e9 POSIX prenant en entr\u00e9e un fichier et l'affichant \u00e0 l'\u00e9cran. Il est utilis\u00e9 notamment dans cet ouvrage pour montrer que le contenu du fichier hello.c est bel et bien celui attendu.

    ", "tags": ["cat", "hello.c", "ULLONG_MAX"]}, {"location": "course-c/05-introduction/code-of-conduct/", "title": "D\u00e9veloppement logiciel", "text": "Les programmes doivent \u00eatre \u00e9crits pour \u00eatre lus par des humains, et seulement accessoirement pour \u00eatre ex\u00e9cut\u00e9s par des machines.Harold Abelson

    Objectifs

    • Comprendre les valeurs et les bonnes pratiques du d\u00e9veloppement logiciel.
    • Saisir l'importance d'apprendre par soi-m\u00eame.
    • Reconna\u00eetre l'importance de l'anglais en informatique.
    • Comprendre le r\u00f4le fondamental de la communaut\u00e9 des d\u00e9veloppeurs.

    Devenir d\u00e9veloppeur logiciel, que ce soit professionnellement ou par passion, ne se r\u00e9sume pas simplement \u00e0 \u00e9crire du code. C\u2019est une discipline qui exige une certaine finesse dans l\u2019ex\u00e9cution, le respect de r\u00e8gles, de consensus partag\u00e9s, et l\u2019adoption de bonnes pratiques.

    J\u2019ai souvent observ\u00e9, tant dans les milieux acad\u00e9miques que professionnels, des individus se revendiquant experts ou professeurs, inculquant \u00e0 leurs \u00e9l\u00e8ves ou coll\u00e8gues des pratiques dogmatiques issues de croyances personnelles ou d'habitudes d\u00e9su\u00e8tes. L\u2019informatique est une discipline vivante, fond\u00e9e sur la collaboration, l\u2019\u00e9coute et l\u2019introspection. Ainsi, il est primordial d\u2019avoir l\u2019esprit ouvert et de faire preuve d\u2019humilit\u00e9.

    On ne d\u00e9veloppe pas sur la base de certitudes fig\u00e9es, mais en s\u2019appuyant sur des principes et des valeurs qui \u00e9voluent avec le temps et varient en fonction du contexte. Un d\u00e9veloppeur web n\u2019adoptera pas les m\u00eames approches qu\u2019un scientifique utilisant Python, ou qu\u2019un programmeur embarqu\u00e9.

    Dans le cadre de projets personnels, vous pouvez coder de mani\u00e8re isol\u00e9e. Cependant, dans une entreprise, vous faites partie d\u2019une \u00e9quipe. Le code que vous \u00e9crivez doit pouvoir perdurer apr\u00e8s votre d\u00e9part. Il doit \u00eatre lisible, maintenable, testable, \u00e9volutif. Il doit \u00eatre conforme aux standards de l\u2019entreprise, respecter les conventions de codage, les bonnes pratiques, les r\u00e8gles de s\u00e9curit\u00e9 et les normes de qualit\u00e9. Il doit \u00eatre document\u00e9, comment\u00e9, versionn\u00e9 et archiv\u00e9. En somme, il doit pouvoir \u00eatre partag\u00e9, diffus\u00e9, \u00e9chang\u00e9. Pour cela, il existe des m\u00e9thodes de travail bien \u00e9tablies que nous aborderons dans ce cours.

    Cependant, les valeurs humaines fondamentales du d\u00e9veloppement logiciel transcendent les consid\u00e9rations purement techniques et m\u00e9thodologiques. Elles sont immuables, les m\u00eames qui r\u00e9gissent la soci\u00e9t\u00e9 humaine depuis des mill\u00e9naires. Parmi ces valeurs, on trouve l\u2019ouverture d\u2019esprit, l\u2019humilit\u00e9, la curiosit\u00e9, la rigueur, la patience, la pers\u00e9v\u00e9rance, l\u2019\u00e9coute, l\u2019entraide et le partage.

    ", "tags": ["curiosite", "discipline", "patience", "developpeur"]}, {"location": "course-c/05-introduction/code-of-conduct/#les-regles-evoluent", "title": "Les r\u00e8gles \u00e9voluent", "text": "

    En 1750 av. J.-C., le roi Hammurabi de Babylone a grav\u00e9 sur une st\u00e8le de basalte le premier code de lois connu de l\u2019histoire. Ce code, qui comprend 282 lois, r\u00e9gissait la vie quotidienne en M\u00e9sopotamie. Bien que ces lois soient consid\u00e9r\u00e9es comme un jalon important vers une justice \u00e9quitable, elles imposaient des sanctions souvent s\u00e9v\u00e8res\u2009: ch\u00e2timents corporels, mutilations, esclavage, voire ex\u00e9cutions. La c\u00e9l\u00e8bre loi du talion, \u00ab\u2009\u0153il pour \u0153il, dent pour dent\u2009\u00bb, en est un exemple embl\u00e9matique.

    Code d'Hammurabi (1750 av. J.-C.)

    Ce que l'on doit retenir de cette analogie avec le d\u00e9veloppement logiciel, c\u2019est que, tout comme les conventions sociales, les r\u00e8gles et les consensus en informatique \u00e9voluent avec le temps. Les bonnes pratiques d'aujourd'hui seront probablement diff\u00e9rentes demain.

    H\u00e9las, l\u2019inertie des institutions, des entreprises et des individus conduit \u00e0 la perp\u00e9tuation des habitudes et \u00e0 l\u2019\u00e9tablissement de dogmes sans que l'on s'en aper\u00e7oive. Il est donc indispensable de faire preuve d'ouverture d\u2019esprit, de remise en question et de curiosit\u00e9 pour s\u2019adapter \u00e0 un monde en perp\u00e9tuelle \u00e9volution.

    En d\u2019autres termes, ce que je vous transmets aujourd'hui n'est pas une v\u00e9rit\u00e9 absolue. Elle est teint\u00e9e par mon v\u00e9cu, mes exp\u00e9riences et mes valeurs. Il vous appartient donc de la remettre en question, d\u2019y r\u00e9fl\u00e9chir avec un esprit critique.

    ", "tags": ["1750", "mesopotamie", "hammurabi-de-babylone", "loi-du-talion"]}, {"location": "course-c/05-introduction/code-of-conduct/#langlais", "title": "L'Anglais", "text": "

    La langue, une barri\u00e8re

    En programmation, quel que soit le langage utilis\u00e9, l\u2019anglais est omnipr\u00e9sent. Les mots-cl\u00e9s des langages de programmation sont majoritairement issus de l'anglais, et bon nombre d'outils de d\u00e9veloppement sont exclusivement disponibles dans cette langue. Pourquoi cela\u2009? Tout comme un article de journal local n\u2019int\u00e9ressera que peu des lecteurs \u00e0 l\u2019autre bout du globe, un code informatique doit pouvoir \u00eatre r\u00e9utilis\u00e9 pour \u00e9conomiser les co\u00fbts de d\u00e9veloppement, et donc s\u2019affranchir des barri\u00e8res linguistiques.

    On r\u00e9utilise ainsi volontiers un algorithme \u00e9crit par un illustre programmeur japonais, ou une biblioth\u00e8que de calcul matriciel d\u00e9velopp\u00e9e en Am\u00e9rique du Sud. Pour que chacun puisse corriger, maintenir ou am\u00e9liorer le code des autres, une langue commune est n\u00e9cessaire, et l'anglais est naturellement devenu cette langue.

    Dans ce cours, bien que r\u00e9dig\u00e9 en fran\u00e7ais, l\u2019anglais sera privil\u00e9gi\u00e9 pour les exemples de code et les noms des symboles (variables, constantes, etc.). Les termes techniques seront traduits lorsqu'un consensus existe, mais sinon l'anglicisme sera pr\u00e9f\u00e9r\u00e9. En effet, bien que l\u2019on pourrait parler de \u00ab\u2009feu d\u2019alerte\u2009\u00bb \u00e0 la place de warning, le terme perdrait sa pertinence technique. J'opte donc, m\u00eame au risque de froisser l\u2019Acad\u00e9mie, pour pr\u00e9server les usages \u00e9tablis parmi les d\u00e9veloppeurs.

    Un autre point m\u00e9rite d'\u00eatre mentionn\u00e9\u2009: l\u2019interaction constante d\u2019un d\u00e9veloppeur avec Internet pour y puiser exemples, conseils ou assistance dans l\u2019utilisation d'outils d\u00e9velopp\u00e9s par d'autres. La majorit\u00e9 de ces ressources sont en anglais.

    Apprenez les langues

    Ne n\u00e9gligez pas les cours de langues. Partez \u00e0 l\u2019\u00e9tranger, lisez des livres en anglais, regardez des films en version originale, \u00e9coutez des podcasts, des conf\u00e9rences et des tutoriels en anglais\u2009: cela vous ouvrira les portes de la connaissance.

    De surcro\u00eet, sans cette comp\u00e9tence, trouver un emploi pourra s\u2019av\u00e9rer plus difficile, car les entreprises sont souvent internationales et les \u00e9quipes de d\u00e9veloppement multiculturelles.

    ", "tags": ["anglais"]}, {"location": "course-c/05-introduction/code-of-conduct/#apprendre-a-pecher", "title": "Apprendre \u00e0 p\u00eacher", "text": "

    Un p\u00e8re et son fils p\u00eachant

    Un jeune homme s'en va \u00e0 la mer avec son p\u00e8re et lui demande\u2009: \u00ab\u2009papa, j'ai faim, comment ram\u00e8nes-tu du poisson\u2009?\u2009\u00bb Le p\u00e8re, fier, lance sa ligne \u00e0 la mer et lui ram\u00e8ne un beau poisson. Plus tard, alors que le jeune homme revient d'une balade sur les estrans, il demande \u00e0 son p\u00e8re\u2009: \u00ab\u2009papa, j'ai faim, me ram\u00e8nerais-tu du poisson\u2009?\u2009\u00bb Le p\u00e8re, sort de son \u00e9tui sa plus belle canne et l'\u00e9quipant d'un bel hame\u00e7on, lance sa ligne \u00e0 la mer et ram\u00e8ne un gros poisson. Durant longtemps, le jeune homme mange ainsi \u00e0 sa faim cependant que le p\u00e8re ram\u00e8ne du poisson pour son fils.

    Un jour, alors que le fils invective son p\u00e8re l'estomac vide, le p\u00e8re annonce. \u00ab\u2009Fils, il est temps pour toi d'apprendre \u00e0 p\u00eacher, je peux te montrer encore longtemps comment je ram\u00e8ne du poisson, mais ce ne serait pas t'aider, voici donc cette canne et cet hame\u00e7on.\u2009\u00bb

    Le jeune homme tente de r\u00e9p\u00e9ter les gestes de son p\u00e8re, mais il ne parvient pas \u00e0 ramener le poisson qui le rassasierait. Il demande \u00e0 son p\u00e8re de l'aide que ce dernier refuse. \u00ab\u2009Fils, c'est par la pratique et avec la faim au ventre que tu parviendras \u00e0 prendre du poisson, pers\u00e9v\u00e8re et tu deviendras meilleur p\u00eacheur que moi, la lign\u00e9e sera ainsi assur\u00e9e de toujours manger \u00e0 sa faim\u2009\u00bb.

    La morale de cette histoire est plus que jamais applicable en programmation, confier aux exp\u00e9riment\u00e9s l'\u00e9criture d'algorithmes compliqu\u00e9s, ou se contenter d'observer les r\u00e9ponses des exercices pour se dire\u2009: j'ai compris ce n'est pas si compliqu\u00e9, est une erreur, car p\u00eacher ou expliquer comment p\u00eacher n'est pas la m\u00eame chose.

    Aussi, cet ouvrage se veut \u00eatre un guide pour apprendre \u00e0 apprendre le d\u00e9veloppement logiciel et non un guide exhaustif du langage, car le standard C99/C11 est disponible sur internet ainsi que le K&R qui reste l'ouvrage de r\u00e9f\u00e9rence pour apprendre le C. Il est donc inutile de paraphraser les exemples donn\u00e9s quand internet apporte toutes les r\u00e9ponses, pour tous les publics du profane r\u00e9serv\u00e9 au hacker passionn\u00e9.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#une-affaire-de-consensus", "title": "Une affaire de consensus", "text": "

    En informatique, tout comme dans la soci\u00e9t\u00e9, il existe des religieux, des pros\u00e9lytes, des dogmatiques, des fanatiques, des contestataires et des maximalistes. Leurs querelles portent souvent sur les outils qu'ils utilisent ou sur des pratiques dont on ne doit pas d\u00e9vier. Ils d\u00e9battent parfois \u00e2prement des conventions de codage, de l'encodage des fichiers, du choix de la fin de ligne, de l'interdiction du goto, ou encore du strict respect des r\u00e8gles MISRA.

    Ces \u00ab\u2009guerres de croyances\u2009\u00bb, qui perdurent parfois depuis des g\u00e9n\u00e9rations, se nourrissent d\u2019un manque d\u2019ouverture d\u2019esprit et d\u2019un attachement dogmatique \u00e0 des habitudes. Cela s\u2019explique souvent par le biais d\u2019ancrage mental qui s\u2019installe d\u00e8s l\u2019\u00e9cole, l\u00e0 o\u00f9 l'on inculque parfois des certitudes fig\u00e9es.

    L'enseignant se doit d'\u00eatre sensible \u00e0 ces questions et doit encourager l'impartialit\u00e9, ainsi qu'un \u00e9tat d'esprit ouvert, guid\u00e9 par le bon sens de l'ing\u00e9nieur.

    Un exemple c\u00e9l\u00e8bre est celui des guerres d'\u00e9diteurs qui opposent, depuis les ann\u00e9es 1970, les adeptes de vi aux fervents d\u00e9fenseurs d'emacs. Ces deux \u00e9diteurs de texte, extr\u00eamement puissants et complexes \u00e0 ma\u00eetriser, polarisent les opinions de mani\u00e8re radicale. Ces guerres, entretenues \u00e0 l'origine par un esprit de plaisanterie, ont progressivement pris une tournure \u00e9motionnelle qui d\u00e9passe parfois le simple cadre de l'outil.

    S\u2019enfermer dans une zone de confort renforce le biais du Marteau de Maslow, car lorsqu'on est un marteau, on finit par voir tous les probl\u00e8mes comme des clous. Ce confort devient alors un ennemi qui freine le regard critique et le pragmatisme, pourtant essentiels. Il faut accepter l\u2019existence de diverses approches pour r\u00e9soudre un probl\u00e8me donn\u00e9, car le d\u00e9veloppement logiciel, plus que tout autre domaine technique, est une aventure collaborative qui ne devrait jamais \u00eatre soumise \u00e0 des emprises \u00e9motionnelles.

    Un programme se doit d'\u00eatre neutre, impartial, et minimaliste. L\u2019essentiel n\u2019est pas de s'attarder sur des questions esth\u00e9tiques comme la position des accolades, l\u2019utilisation d'espaces ou de tabulations, ou le choix d\u2019un \u00e9diteur sur un autre.

    La cl\u00e9 de la bonne attitude c'est d'\u00eatre \u00e0 l'\u00e9coute du consensus et de ne pas sombrer au biais d'attention. Il faut non seulement \u00eatre sensible au consensus local direct\u2009: son entreprise, son \u00e9cole, son \u00e9quipe de travail, mais surtout au consensus plan\u00e9taire dont l'acc\u00e8s ne peut se faire que par l'interaction directe avec la communaut\u00e9 de d\u00e9veloppeurs, soit par les forums de discussions (Reddit, stackoverflow), soit par le code lui-m\u00eame. Vous avez un doute sur la bonne m\u00e9thode pour \u00e9crire tel algorithme ou sur la fa\u00e7on dont votre programme devrait \u00eatre structur\u00e9\u2009? Plongez-vous dans le code des autres, multipliez vos exp\u00e9riences, observez les disparit\u00e9s et les oppositions, et apprenez \u00e0 ne pas y \u00eatre sensible.

    Vous verrez qu'au d\u00e9but, un programme ne vous semble lisible que s'il respecte vos habitudes, la taille de vos indentations pr\u00e9f\u00e9r\u00e9es, la police de caract\u00e8re qui vous sied le mieux, l'\u00e9diteur qui supporte les ligatures...

    Par la suite, et \u00e0 la relecture de cette section, vous apprendrez \u00e0 faire fi de cette zone de confort qui vous \u00e9tait si cher et que l'important n'est plus la forme, mais le fond. Vous aurez comme N\u00e9o, lib\u00e9r\u00e9 votre esprit et serez capable de voir la matrice sans filtre ni biais.

    En somme, restez ouvert aux autres points de vue, cherchez \u00e0 adopter le consensus majoritaire qui dynamise au mieux votre \u00e9quipe de d\u00e9veloppement, qui s'encadre le mieux dans votre strat\u00e9gie de croissance et de collaboration et surtout, abreuvez-vous de code, faites-en des indigestions, r\u00eavez-en la nuit. Vous tradez du Bitcoin, allez lire le code source, vous aimez Linux, plongez-vous dans le code source du kernel, certains coll\u00e8gues ou amis vous ont parl\u00e9 de Git, allez voir ses entrailles... Oui, tous ces projets sont \u00e9crits en C, n'est-ce pas merveilleux\u2009?

    ", "tags": ["emacs", "goto"]}, {"location": "course-c/05-introduction/code-of-conduct/#lopen-source", "title": "L'open source", "text": "

    Au d\u00e9but de l'informatique, les programmes \u00e9taient distribu\u00e9s avec leur code source, car les ordinateurs \u00e9taient rares et co\u00fbteux et que les utilisateurs \u00e9taient souvent des d\u00e9veloppeurs. Avec l'arriv\u00e9e des ordinateurs personnels, les \u00e9diteurs de logiciels ont commenc\u00e9 \u00e0 distribuer des programmes compil\u00e9s, car les utilisateurs n'\u00e9taient plus des d\u00e9veloppeurs et que le code source \u00e9tait devenu un secret industriel mon\u00e9tisable. C'est ainsi que le logiciel propri\u00e9taire est n\u00e9. Les \u00e9diteurs de logiciels ont tir\u00e9 parti de cette situation pour verrouiller leurs clients dans un \u00e9cosyst\u00e8me propri\u00e9taire, les emp\u00eachant de modifier le logiciel, de le partager ou de le vendre.

    Dans les ann\u00e9es 1980, Richard Stallman, un informaticien am\u00e9ricain, a lanc\u00e9 le projet GNU pour cr\u00e9er un syst\u00e8me d'exploitation libre, c'est-\u00e0-dire un syst\u00e8me d'exploitation dont le code source est librement accessible, modifiable et redistribuable. N\u00e9anmoins la licence GPL (GNU Public License) qui prot\u00e8ge le code source de GNU est tr\u00e8s contraignante et ne permet pas de cr\u00e9er des logiciels propri\u00e9taires bas\u00e9s sur du code source GPL. C'est un frein pour les entreprises qui souhaitent prot\u00e9ger leur propri\u00e9t\u00e9 intellectuelle.

    En 1991, Linus Torvalds, un \u00e9tudiant finlandais, a cr\u00e9\u00e9 le noyau Linux, qui est devenu le noyau du syst\u00e8me d'exploitation GNU/Linux. Depuis lors, de nombreux logiciels libres ont \u00e9t\u00e9 d\u00e9velopp\u00e9s, notamment le navigateur web Firefox, le serveur web Apache, le syst\u00e8me de gestion de base de donn\u00e9es MySQL, le langage de programmation Python, le syst\u00e8me de gestion de versions Git, etc.

    Cette philosophie du logiciel libre a \u00e9t\u00e9 popularis\u00e9e par le hacker am\u00e9ricain Eric Raymond dans son essai \u00ab\u2009La cath\u00e9drale et le bazar\u2009\u00bb qui d\u00e9crit deux mod\u00e8les de d\u00e9veloppement logiciel\u2009: le mod\u00e8le de la cath\u00e9drale, o\u00f9 le code source est d\u00e9velopp\u00e9 en interne par une \u00e9quipe restreinte, et le mod\u00e8le du bazar, o\u00f9 le code source est d\u00e9velopp\u00e9 de mani\u00e8re collaborative par une communaut\u00e9 de d\u00e9veloppeurs.

    L'expression open source s'est largement impos\u00e9e dans le monde de l'informatique pour d\u00e9signer les logiciels libres, car elle est plus neutre et moins id\u00e9ologique que l'expression logiciel libre. De grandes soci\u00e9t\u00e9s comme Google, Facebook, Microsoft, IBM, Oracle, etc., ont adopt\u00e9 la philosophie du logiciel libre et contribuent activement \u00e0 de nombreux projets open source. Par exemple, Google a d\u00e9velopp\u00e9 le syst\u00e8me d'exploitation Android, qui est bas\u00e9 sur le noyau Linux, et qui est utilis\u00e9 par la plupart des smartphones dans le monde. Facebook a d\u00e9velopp\u00e9 le framework React, qui est utilis\u00e9 par de nombreux sites web pour cr\u00e9er des interfaces utilisateur interactives. Microsoft a rachet\u00e9 GitHub, la plateforme de d\u00e9veloppement collaboratif la plus populaire au monde, et a ouvert le code source de nombreux projets, notamment le framework .NET. IBM a rachet\u00e9 Red Hat, l'\u00e9diteur de la distribution Linux Red Hat Enterprise Linux, et contribue activement \u00e0 de nombreux projets open source.

    Mettre un logiciel ou une partie de logiciel en open source c'est permettre \u00e0 d'autres d\u00e9veloppeurs de contribuer au projet, de corriger des bogues, d'ajouter des fonctionnalit\u00e9s, de traduire le logiciel dans d'autres langues, etc. C'est aussi un moyen de faire conna\u00eetre son travail, de se faire un nom dans la communaut\u00e9 des d\u00e9veloppeurs, de trouver un emploi, de cr\u00e9er une entreprise, etc. Mais c'est aussi un moyen de partager ses connaissances, de contribuer \u00e0 l'\u00e9ducation, \u00e0 la recherche, \u00e0 la culture, \u00e0 l'humanit\u00e9.

    L'open source est devenu un mod\u00e8le \u00e9conomique viable pour de nombreuses entreprises, qui vendent des services autour de logiciels open source, comme le support, la formation, la personnalisation, l'h\u00e9bergement, etc. C'est aussi un moyen de r\u00e9duire les co\u00fbts de d\u00e9veloppement, de mutualiser les efforts, de partager les risques, de favoriser l'innovation et de promouvoir la transparence.

    Pourquoi ne pas faire d'open source\u2009? C'est une question que vous vous poserez t\u00f4t ou tard dans votre carri\u00e8re de d\u00e9veloppeur. Vous avez peut-\u00eatre peur de la concurrence, de la critique, du piratage, de la perte de contr\u00f4le, de la complexit\u00e9, de l'engagement, de la responsabilit\u00e9, de la r\u00e9putation, de la l\u00e9galit\u00e9, de la s\u00e9curit\u00e9, de la confidentialit\u00e9, de la propri\u00e9t\u00e9 intellectuelle, etc. De nombreuses soci\u00e9t\u00e9s ont fait le choix de prot\u00e9ger leur propri\u00e9t\u00e9 intellectuelle en gardant leur code source secret, mais cela \u00e0 un co\u00fbt. Les logiciels doivent \u00eatre prot\u00e9g\u00e9s par des licences, des brevets, le code doit \u00eatre crypt\u00e9, les serveurs doivent \u00eatre s\u00e9curis\u00e9s, les employ\u00e9s doivent \u00eatre surveill\u00e9s, les clients doivent \u00eatre contr\u00f4l\u00e9s, etc. C'est un cercle vicieux qui peut conduire \u00e0 la parano\u00efa, \u00e0 la m\u00e9fiance.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#la-communaute", "title": "La communaut\u00e9", "text": "

    Se passionner pour le d\u00e9veloppement logiciel c'est aussi se passionner pour la communaut\u00e9 des d\u00e9veloppeurs. Avant internet, les d\u00e9veloppeurs se rencontraient dans des clubs d'informatique, des associations d'utilisateurs, des conf\u00e9rences, des salons, des formations, des hackathons, des meetups, etc. Moi-m\u00eame, \u00e0 douze ans, je suis rentr\u00e9 au Mac Club de Gen\u00e8ve. Un club d'informatique pour les passionn\u00e9s de Macintosh. J'ai fait mes premiers pas sur internet avec des modem rudimentaires.

    Avec internet, les d\u00e9veloppeurs se rencontrent maintenant sur des forums (Stack Overflow, Reddit...), des listes de diffusion, des chats, des blogs, des r\u00e9seaux sociaux, des plateformes de d\u00e9veloppement collaboratif, etc.

    GitHub a \u00e9t\u00e9 cr\u00e9\u00e9 en 2008 par Tom Preston-Werner, Chris Wanstrath, PJ Hyett et Scott Chacon pour faciliter le d\u00e9veloppement collaboratif de logiciels open source. GitHub est devenu la plateforme de d\u00e9veloppement collaboratif la plus populaire au monde, avec plus de 100 millions de d\u00e9p\u00f4ts de code source, plus de 40 millions de d\u00e9veloppeurs. On y trouve de tout, il suffit de chercher.

    Pour les questions techniques, il y a Stack Overflow, un site de questions-r\u00e9ponses cr\u00e9\u00e9 en 2008 par Jeff Atwood et Jo\u00ebl Spolsky. Stack Overflow est devenu le site de questions-r\u00e9ponses le plus populaire au monde, avec plus de 10 millions de questions, plus de 20 millions de r\u00e9ponses, plus de 10 millions de membres. Je vous encourage personnellement \u00e0 y contribuer, cela commence par cr\u00e9er un compte, poser des questions, r\u00e9pondre \u00e0 des questions, voter pour des questions, voter pour des r\u00e9ponses.

    Voici quelques liens utiles\u2009:

    Stack Overflow

    Aujourd'hui le plus grand portail de questions/r\u00e9ponses d\u00e9di\u00e9 \u00e0 la programmation logicielle

    GitHub

    Un portail de partage de code

    Google Scholar

    Un point d'entr\u00e9e essentiel pour la recherche d'articles scientifiques

    Man Pages

    La documentation (man pages) des commandes et outils les plus utilis\u00e9s dans les environnements macOS/Linux/Unix et POSIX compatible.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#la-revue-de-code", "title": "La revue de code", "text": "

    Enfin, je voudrais terminer cette introduction par un point essentiel du d\u00e9veloppement logiciel\u2009: la revue de code, qui est trop souvent n\u00e9glig\u00e9e.

    La revue de code est une pratique essentielle pour am\u00e9liorer la qualit\u00e9 du code source, la productivit\u00e9 des d\u00e9veloppeurs, la s\u00e9curit\u00e9 des logiciels, la satisfaction des clients. La revue de code consiste \u00e0 examiner le code source d'un d\u00e9veloppeur par un autre d\u00e9veloppeur pour d\u00e9tecter des erreurs, des anomalies, des incoh\u00e9rences, des inefficacit\u00e9s, des non-conformit\u00e9s, des risques, des opportunit\u00e9s. La revue de code peut \u00eatre formelle ou informelle, manuelle ou automatique, individuelle ou collective, interne ou externe, syst\u00e9matique ou al\u00e9atoire, planifi\u00e9e ou impromptue, etc.

    Dans les entreprises c'est un des plus gros probl\u00e8mes. Les d\u00e9veloppeurs n'aiment pas qu'on critique leur code, les chefs de projet n'aiment pas perdre du temps \u00e0 examiner le code, les clients n'aiment pas payer pour la revue de code, les managers n'aiment pas les conflits entre d\u00e9veloppeurs, les commerciaux n'aiment pas les retards de livraison, les juristes n'aiment pas les risques de litige. Les gens manquent d'humilit\u00e9 et d'ouverture d'esprit. Pourtant, s'ouvrir \u00e0 la critique, cela permet de s'am\u00e9liorer et d'apprendre.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#conclusion", "title": "Conclusion", "text": "

    En r\u00e9sum\u00e9, devenir d\u00e9veloppeur logiciel, que ce soit en tant que professionnel ou par passion, est bien plus qu\u2019une simple question d'\u00e9criture de code. C'est un art qui demande une compr\u00e9hension profonde des principes, des bonnes pratiques et des valeurs fondamentales qui r\u00e9gissent cette discipline en constante \u00e9volution. Trop souvent, les dogmes acad\u00e9miques et les habitudes obsol\u00e8tes polluent l'apprentissage et la pratique, au d\u00e9triment de l'esprit collaboratif et critique indispensable \u00e0 la r\u00e9ussite dans ce domaine.

    Le d\u00e9veloppement logiciel ne repose pas sur des acquis immuables, mais sur des principes adaptatifs et des valeurs humaines intemporelles telles que l'ouverture d'esprit, l'humilit\u00e9, la curiosit\u00e9, la rigueur, la patience, la pers\u00e9v\u00e9rance, l'\u00e9coute, l'entraide et le partage. Ces valeurs, qui ont permis \u00e0 l'humanit\u00e9 de prosp\u00e9rer, sont tout aussi essentielles dans le monde du d\u00e9veloppement logiciel.

    Il est crucial de comprendre que le code que vous \u00e9crivez aujourd'hui doit pouvoir \u00eatre compris, maintenu et \u00e9volu\u00e9 par d'autres demain. Ainsi, il doit respecter les standards de l'entreprise, les conventions de codage, les bonnes pratiques et les r\u00e8gles de s\u00e9curit\u00e9. Cela n\u00e9cessite un engagement envers des m\u00e9thodes de travail \u00e9prouv\u00e9es, mais aussi une attitude de remise en question constante et d'ouverture \u00e0 l'innovation.

    L'anglais, langue universelle de la programmation, est un outil indispensable pour naviguer dans cet univers globalis\u00e9. Il permet de partager des connaissances, d'acc\u00e9der \u00e0 des ressources et de collaborer avec des d\u00e9veloppeurs du monde entier. Ne sous-estimez pas l'importance de ma\u00eetriser cette langue pour votre carri\u00e8re.

    Apprendre \u00e0 p\u00eacher plut\u00f4t qu'\u00e0 se faire donner du poisson est une le\u00e7on cl\u00e9 dans l'apprentissage du d\u00e9veloppement logiciel. La pratique, la pers\u00e9v\u00e9rance et l'exp\u00e9rimentation personnelle sont indispensables pour acqu\u00e9rir les comp\u00e9tences n\u00e9cessaires et devenir autonomes.

    L'informatique est aussi une affaire de consensus. Les guerres de croyances et les dogmes ne font que limiter la croissance et l'innovation. Adopter une attitude pragmatique et ouverte, en se basant sur le consensus de la communaut\u00e9 mondiale des d\u00e9veloppeurs, est essentiel pour progresser et s'\u00e9panouir dans ce m\u00e9tier.

    L'open source incarne parfaitement l'esprit de partage et de collaboration qui est au c\u0153ur du d\u00e9veloppement logiciel. Il permet non seulement de contribuer \u00e0 des projets d'envergure mondiale, mais aussi de se faire un nom, d'apprendre des autres et de donner en retour.

    Enfin, la communaut\u00e9 des d\u00e9veloppeurs est une ressource inestimable. Participer activement \u00e0 des forums, des plateformes collaboratives, des conf\u00e9rences et des meetups enrichit non seulement vos comp\u00e9tences, mais aussi votre r\u00e9seau professionnel.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/", "title": "Mon ordinateur et moi", "text": "

    Vous \u00eates devant votre ordinateur, vous avez certainement devant vous un clavier, une souris \u00e0 droite de votre clavier, et un ou plusieurs \u00e9crans. Votre ordinateur d\u00e9marre et vous voyez appara\u00eetre soit\u2009:

    • une pomme croqu\u00e9e ( Apple);
    • une fen\u00eatre \u00e0 carreaux ( Windows);
    • un manchot Ad\u00e9lie ( Linux).

    Le responsable de cet \u00e9cran de d\u00e9marrage, c'est votre syst\u00e8me d'exploitation et peu importe lequel vous utilisez, la bonne nouvelle c'est que vous pourrez \u00e9crire vos premiers programmes.

    Familiarisons-nous un peu avec l'ordinateur, le voulez-vous bien\u2009?

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#systeme-dexploitation", "title": "Syst\u00e8me d'exploitation", "text": "

    Dans cet ouvrage, la plupart des exemples seront pr\u00e9sent\u00e9s sous Linux. C'est un choix dogmatique parce que je pr\u00e9f\u00e8re Linux \u00e0 Windows, mais c'est aussi pour des raisons objectives et respectables. D'une part, Linux a l'avantage d'\u00eatre normalis\u00e9. Il respecte en grande partie le standard POSIX, comme d'ailleurs Apple macOS. D'autre part, m\u00eame sous Windows, il est possible \u00e0 tout utilisateur d'installer d'un sous-syst\u00e8me Linux nomm\u00e9 WSL2 (Windows Subsystem for Linux), facilitant ainsi l'ex\u00e9cution de programmes Linux sur un environnement Windows. Aussi quelque soit votre ob\u00e9dience geeko-spirituelle, vous aurez toujours la possibilit\u00e9 de suivre les exemples de ce cours.

    Notons qu'un syst\u00e8me d'exploitation en lui-m\u00eame n'est rien d'autre qu'un programme sophistiqu\u00e9 qui sert d'interm\u00e9diaire entre le mat\u00e9riel et les autres logiciels. On peut le comparer \u00e0 un chef d'orchestre, coordonnant les ressources de votre ordinateur, lan\u00e7ant les programmes, g\u00e9rant les fichiers, et supervisant les utilisateurs. C'est une couche d'abstraction qui permet \u00e0 votre machine de fonctionner harmonieusement, en masquant la complexit\u00e9 du mat\u00e9riel. Vous n'avez pas \u00e0 vous soucier des milliards de changements d'\u00e9tats \u00e9lectroniques par seconde survenant dans le processeur ou des quelque deux millions de pixels de votre \u00e9cran qui peuvent \u00eatre configur\u00e9s selon 16 millions de couleurs diff\u00e9rentes environ soixante fois par seconde.

    Votre syst\u00e8me d'exploitation c'est votre interface coh\u00e9rente et conviviale humain-machine.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#editeur-de-code-source", "title": "\u00c9diteur de code source", "text": "

    Pour \u00e9crire un programme, vous aurez besoin d'un \u00e9diteur de code, c'est un programme (oui, lui aussi) qui vous permet d'\u00e9crire du texte et de le sauvegarder dans un fichier\u2009; il en existe des centaines, certains plus aboutis que d'autres.

    Si vous trouvez une Dolor\u00e9ane munie d'un convecteur temporel, et que vous d\u00e9passez les 88 miles \u00e0 l'heure, avec une \u00e9nergie de 2.21 Gigot-Watt vous pouvez vous rendre en 1973 et utiliser un \u00e9diteur de code qui s'appelle ed (prononc\u00e9 \u00ab\u2009idi\u2009\u00bb) \u00e9crit par Ken Thompson (un des cr\u00e9ateurs d'Unix et du langage C, mais nous reviendrons sur lui plus tard).

    C'est un \u00e9diteur de texte qui a \u00e9t\u00e9 \u00e9crit \u00e0 l'\u00e9poque des t\u00e9l\u00e9types et qui curieusement a travers\u00e9 les \u00e2ges, car il est encore int\u00e9gr\u00e9 au standard POSIX. Il est par cons\u00e9quent toujours disponible sur nos syst\u00e8mes d'exploitation modernes. Toutefois \u00e0 cette \u00e9poque, il n'y avait pas d'\u00e9cran, et nos homologues de cette \u00e9poque utilisaient des imprimantes pour afficher un r\u00e9sultat. Cet \u00e9diteur primitif n'\u00e9tait donc pas tr\u00e8s interactif.

    Autre fait notable c'est que ed est l'un des premiers \u00e9diteurs dit modale. En effet, son utilisation \u00e9tant assez d\u00e9routante puisqu'il n'y a pas de retour visuel imm\u00e9diat (n'imaginez tout de m\u00eame pas qu'\u00e0 cette \u00e9poque nous imprimions chaque lettre frapp\u00e9e au clavier sur du papier), donc pour saisir du texte il fallait entrer taper des commandes, certaines pour sauvegarder, pour quitter, pour rechercher et remplacer, etc. Un exemple vaut mieux qu'un long discours. Imaginons que nous voulions saisir le po\u00e8me \u00ab\u2009L'albatros\u2009\u00bb de Charles Baudelaire dans un fichier nomm\u00e9 albatros.txt. Voici comment nous pourrions proc\u00e9der\u2009:

    $ ed\na\nSouvent, pour s'amuser, les homes d'\u00e9quipage\nPrennent des albatros,\nvastes oiseaux des mers,\nLe navire glissant sur les gouffres amers.\n.\ni\nQui suivent, indolents compagnons de voyage,\n.\n2,3j\n,p\nSouvent, pour s'amuser, les homes d'\u00e9quipage\n%s/homes/hommes/g\nw albatros.txt\n164\nq\n

    Dans les \u00e9tapes ci-dessus, nous avons lanc\u00e9 l'\u00e9diteur ed puis bascul\u00e9 en mode d'insertion avec la commande a pour ajouter du texte. Nous avons ensuite ajout\u00e9 un texte, mais comme il n'y a pas de retour visuel, nous ne sommes pas certains d'avoir orthographi\u00e9 juste tous les mots. \u00c0 la fin de la saisie, on revient en mode commande avec . puis nous d\u00e9cidons de poursuivre en ins\u00e9rant une nouvelle ligne i. Passons maintenant \u00e0 la correction. On sait d\u00e9j\u00e0 que l'on a ajout\u00e9 un retour \u00e0 la ligne en trop entre la ligne 2 et 3. Nous pouvons les joindre avec 2,3j (joindre lignes 2 et 3). Enfin, nous imprimons (physiquement sur une imprimante) la premi\u00e8re ligne avec 1p (print ligne 1). Constatant l'erreur, nous rempla\u00e7ons homes par hommes avec %s/homes/hommes/g. Enfin, on sauvegarde le fichier avec w albatros.txt, la commande retourne (sur l'imprimante) le nombre de caract\u00e8res sauvegard\u00e9s, soit 164. \u00c0 la fin de ce laborieux exercice, nous quittons ed avec la commande q.

    Tr\u00eave de plaisanteries, je vous rassure, vous n'allez probablement pas utiliser ed au quotidien, ni probablement m\u00eame jamais. Cependant conna\u00eetre son existence permet de mieux comprendre le contexte g\u00e9n\u00e9ral. Aussi, je vous propose de continuer un peu notre voyage spatio-temporel...

    En 1991 na\u00eet un \u00e9diteur de code qui va r\u00e9volutionner le monde de la programmation, il s'appelle vim (Vi Improved). C'est un \u00e9diteur de code qui est ultra puissant, mais dont la courbe d'apprentissage assez velue. Il est toujours tr\u00e8s utilis\u00e9 de nos jours, et il est disponible sur tous les syst\u00e8mes d'exploitation. En outre, la plupart des \u00e9diteurs modernes disposent d'une extension pour \u00e9muler, du moins en partie vim. Comme ed, c'est un \u00e9diteur modal\u2009: un mode pour \u00e9crire du texte, un mode pour \u00e9diter du texte, un mode pour naviguer dans le texte, un mode pour saisir des commandes, etc.

    Puisque nous nommons Vim, je dois aussi nommer son plus f\u00e9roce concurrent\u2009: Emacs. Emacs est un \u00e9diteur de code invent\u00e9 par Richard Stallman, le p\u00e8re fondateur de l'open source. Rival de Vim depuis des d\u00e9cennies, Emacs est un \u00e9diteur qui est aussi incroyablement puissant, mais il semble un peu moins utilis\u00e9 de nos jours. Si je prends le soin de mentionner les deux \u00e9diteurs, c'est que leurs utilisateurs sont souvent tr\u00e8s passionn\u00e9s et tr\u00e8s engag\u00e9s dans leur choix d'\u00e9diteurs. Il y a m\u00eame des blagues d'informaticiens sur le sujet\u2009:

    Guerre d'\u00e9diteurs

    Je fais volontiers l'impasse sur d'autres \u00e9diteurs qui ont aussi \u00e9t\u00e9 populaires en leurs temps, mais qui me semblent technologiquement d\u00e9pass\u00e9s\u2009: TextPad, UltraEdit, Sublime Text, Atom, NotePad++... L'Usain Bolt, le Michael Phelps des \u00e9diteurs c'est Visual Studio Code, l'\u00e9diteur phare de Microsoft qui a conquis les doigts agiles des d\u00e9veloppeurs du monde entier. Il est gratuit, open source, et il est disponible sur tous les syst\u00e8mes d'exploitation. Il dispose de nombreuses extensions (notamment l'extension Vim utilis\u00e9e par 6'700'000 personnes et l'extension Emacs utilis\u00e9e par 55'000 personnes). Il est tr\u00e8s rapide, tr\u00e8s puissant, et il est tr\u00e8s bien int\u00e9gr\u00e9 la plupart des outils tiers que nous utiliserons dans ce cours. Il est donc l'\u00e9diteur que je vous recommande jusqu'\u00e0 la prochaine r\u00e9volution.

    De mani\u00e8re plus factuelle, le r\u00e9sultat de l'\u00e9tude annuelle 2023 de Stackoverflow donne une id\u00e9e g\u00e9n\u00e9ralement assez bonne de la popularit\u00e9 des \u00e9diteurs et environnements de d\u00e9veloppement int\u00e9gr\u00e9s les plus utilis\u00e9s par les d\u00e9veloppeurs de logiciels\u2009:

    %% Utilisation des \u00e9diteurs de code\npie\n    \"Visual Studio Code\" : 73.3\n    \"Visual Studio\" : 28.4\n    \"IntelliJ IDEA\" : 26.8\n    \"Notepad++\": 24.5\n    \"Vim\": 22.3\n    \"Emacs\": 4.69\n    \"Eclipse\": 9.9\n    \"Nano\": 8.98
    Utilisation des \u00e9diteurs de code", "tags": ["hommes", "vim", "albatros.txt", "homes"]}, {"location": "course-c/05-introduction/me-and-my-computer/#fonctionnalites-attendues", "title": "Fonctionnalit\u00e9s attendues", "text": "

    Les \u00e9diteurs de code modernes contrairement \u00e0 des outils comme notepad sous Windows, disposent de nombreuses fonctionnalit\u00e9s qui facilitent la vie des d\u00e9veloppeurs. Voici quelques-unes des fonctionnalit\u00e9s que vous pouvez attendre d'un \u00e9diteur de code moderne\u2009:

    Coloration Synatxique (syntax highlighting)

    L'\u00e9diteur de code colorise les mots-cl\u00e9s du langage de programmation que vous utilisez, les parenth\u00e8ses, les erreurs. Cela permet de mieux visualiser la structure du code.

    Correspondance des parenth\u00e8ses (brace matching)

    L'\u00e9diteur de code vous permet de voir la correspondance des parenth\u00e8ses, accolades, crochets, etc. Cela permet de voir en un tournemain si vous avez oubli\u00e9 une parenth\u00e8se fermante.

    Indentation automatique (auto-indent)

    L'\u00e9diteur de code vous permet d'indenter automatiquement votre code. Cela permet de voir la structure du code. Il est consensuellement admis qu'une r\u00e9gion de code s\u00e9lectionn\u00e9e peut \u00eatre indent\u00e9e avec Tab et d\u00e9sindent\u00e9e avec Shift+Tab.

    Repli de code (code folding)

    L'\u00e9diteur de code vous permet de replier le code. En cliquant sur une petite fl\u00e8che \u00e0 gauche du code, vous pouvez regrouper les \u00e9l\u00e9ments hi\u00e9rarchiques de votre code pour mieux visualiser la structure.

    Structure du code (outline)

    L'\u00e9diteur de code vous permet de voir dans une fen\u00eatre s\u00e9par\u00e9e les \u00e9l\u00e9ments cl\u00e9 de votre programme. Cela permet de naviguer plus rapidement.

    Navigation hi\u00e9rarchique (go to definition)

    L'\u00e9diteur de code vous permet de naviguer rapidement entre diff\u00e9rents fichiers. En cliquant sur un mot-cl\u00e9, vous pouvez vous rendre \u00e0 la d\u00e9finition de ce mot-cl\u00e9 situ\u00e9e ailleurs dans un projet. Habituellement Alt+Left vous permet de revenir en arri\u00e8re l\u00e0 o\u00f9 vous \u00e9tiez.

    Expressions r\u00e9guli\u00e8res (regular expressions)

    L'\u00e9diteur de code vous permet de rechercher ou remplacer des \u00e9l\u00e9ments en utilisant des expressions r\u00e9guli\u00e8res. Par exemple, si vous voulez inverser l'ordre des mots \u00e9crits, vous activez le mode regex (\u25aa\u20f0 dans vscode). Vous pouvez alors utiliser l'expression r\u00e9guli\u00e8re suivante\u2009:

    /(M.|Mme.)\\s+([^ ]+)\\s+([^ ]+)/\\1 \\3 \\2/\n\nQui permet d'inverser le pr\u00e9nom et le nom.\n\nM. Yves Chevallier` --> M. Chevallier Yves\n
    Multicurseurs (multi-cursor)

    L'\u00e9diteur de code vous permet de placer plusieurs curseurs dans votre code. Cela permet de modifier plusieurs lignes ou mots en m\u00eame temps. Dans vscode vous pouvez ajouter un curseur avec la touche Alt. Vous pouvez aussi s\u00e9lectionner le prochain mot identique avec Ctrl+D.

    Compl\u00e9tion automatique (auto-completion)

    L'\u00e9diteur de code vous permet de compl\u00e9ter automatiquement le code en utilisant la touche Tab. Il utilise une technologie nomm\u00e9e IntelliSense qui, ayant connaissance des mots-cl\u00e9s du langage de programmation et de ce que vous avez d\u00e9j\u00e0 \u00e9crit, vous propose les possibilit\u00e9s de compl\u00e9tion.

    Intelligence artificielle (AI)

    L'\u00e9diteur de code vous permet de compl\u00e9ter automatiquement le code en utilisant une intelligence artificielle comme GitHub Copilot. Cette technologie propose des suggestions de code en fonction de ce que vous avez d\u00e9j\u00e0 \u00e9crit bas\u00e9 sur des millions programmes open-source disponibles sur internet.

    Gestion d'extensions (extensions)

    L'\u00e9diteur de code vous permet d'ajouter des extensions permettant d'ajouter des fonctionnalit\u00e9s \u00e0 votre \u00e9diteur de code tel que l'extension Vim ou Emacs, celle de GitHub Copilot, ou encore celle pour d\u00e9velopper en langage C.

    Int\u00e9gration du terminal (terminal integration)

    L'\u00e9diteur de code vous permet d'int\u00e9grer un terminal (TTY) dans votre \u00e9diteur de code pour lancer directement des commandes. Cela permet d'ex\u00e9cuter votre programme dans la m\u00eame interface et r\u00e9cup\u00e9rer les r\u00e9sultats produits.

    Gestion de version (git integration)

    L'\u00e9diteur de code vous permet d'int\u00e9grer Git, l'outil dominant pour g\u00e9rer les diff\u00e9rentes versions de votre programme.

    ", "tags": ["Emacs", "Vim"]}, {"location": "course-c/05-introduction/me-and-my-computer/#compilateur", "title": "Compilateur", "text": "

    Un compilateur est un programme qui permet de transformer un programme \u00e9crit dans un langage de programmation en un programme ex\u00e9cutable. Il existe de nombreux compilateurs, et chaque langage de programmation a son propre compilateur pour autant qu'il ne soit pas interpr\u00e9t\u00e9 (comme Python, Ruby, JavaScript, etc.).

    Parmi quelques compilateurs populaires, on peut citer\u2009:

    GCC

    Un compilateur open-source utilis\u00e9 sous Linux et macOS. Il est sous licence GPL.

    CLANG

    Un compilateur open-source gagnant en popularit\u00e9, une alternative \u00e0 GCC. Il est sous licence Apache et utilise la biblioth\u00e8que LLVM.

    IAR

    Un compilateur propri\u00e9taire assez on\u00e9reux utilis\u00e9 pour les syst\u00e8mes m\u00e9dicaux, ou les syst\u00e8mes embarqu\u00e9s critiques.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#ide", "title": "IDE", "text": "

    Un IDE est un Integrated Development Environment, c'est un environnement de d\u00e9veloppement int\u00e9gr\u00e9. C'est un programme qui vous permet d'\u00e9crire du code, de le compiler, de le d\u00e9boguer, de le tester, de le d\u00e9ployer, etc.

    Tous les \u00e9diteurs ne sont pas des IDE, mais tous les IDE sont des \u00e9diteurs. En fin de compte, un IDE est un \u00e9diteur qui poss\u00e8de des fonctionnalit\u00e9s suppl\u00e9mentaires telles que\u2009:

    • un compilateur pour g\u00e9n\u00e9rer un programme ex\u00e9cutable\u2009;
    • un d\u00e9bogueur avec des points d'arr\u00eat pour ex\u00e9cuter le programme ligne par ligne\u2009;
    • une gestion de param\u00e8tres par projet pour compiler le programme avec des options sp\u00e9cifiques\u2009;
    • une gestion de d\u00e9pendances logicielles pour inclure des ressources externes d\u00e9velopp\u00e9es par d'autres d\u00e9veloppeurs\u2009;
    • une gestion de versions pour suivre l'\u00e9volution du code et collaborer avec d'autres d\u00e9veloppeurs.

    La figure suivante illustre les relations entre les diff\u00e9rents outils que nous avons \u00e9voqu\u00e9s jusqu'\u00e0 pr\u00e9sent.

    Repr\u00e9sentation graphique des notions de compilateur, IDE, toolchain...

    Parmi les plus connus on peut citer IntelliJ IDEA, Eclipse, Visual Studio, Visual Studio Code, Xcode, etc.

    On notera que l'ensemble des outils n\u00e9cessaires \u00e0 cr\u00e9er un logiciel ex\u00e9cutable est appel\u00e9 cha\u00eene de compilation, plus commun\u00e9ment appel\u00e9e toolchain. Cette derni\u00e8re est commun\u00e9ment associ\u00e9e \u00e0 une SDK (Software Development Kit), un ensemble d'outils logiciels permettant de d\u00e9velopper des logiciels pour une cible donn\u00e9e (microcontr\u00f4leur, Raspberry Pi, smartphones, etc.).

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Norme

    est la norme respect\u00e9e par la plupart syst\u00e8mes d'exploitation modernes sauf Windows\u2009? Elle unifie les syst\u00e8mes d'exploitation en d\u00e9finissant une interface standardis\u00e9e pour les programmes.

    Solution

    La norme POSIX (Portable Operating System Interface) est une norme qui d\u00e9finit une interface standardis\u00e9e pour les syst\u00e8mes d'exploitation. Elle est respect\u00e9e en grande partie par Unix, Linux, Solaris, BSD, macOS, Android, QNX, Cygwin, Haiku, VxWorks, RTEMS, etc.

    H\u00e9las, Windows ne respecte pas cette norme ce qui le positionne en marge des autres syst\u00e8mes d'exploitation.

    Submit

    Exercice 2\u2009: Eclipse

    Un ami vous parle d'un outil utilis\u00e9 pour le d\u00e9veloppement logiciel nomm\u00e9 Eclipse. De quel type d'outil s'agit-il\u2009?

    Solution

    Eclipse est un IDE. Il n'int\u00e8gre donc pas de cha\u00eene de compilation et donc aucun compilateur.

    Exercice 3\u2009: Stack Overflow

    Combien y a-t-il eu de questions pos\u00e9es en C sur le site Stack Overflow\u2009?

    Solution

    Il suffit pour cela de se rendre sur le site de Stackoverflow et d'acc\u00e9der \u00e0 la liste des tags. En 2019/07 il y eut 307'669 questions pos\u00e9es.

    Seriez-vous capable de r\u00e9pondre \u00e0 une question pos\u00e9e\u2009?

    Exercice 4\u2009: Quel syst\u00e8me d'exploitation\u2009?

    Quel syst\u00e8me d'exploitation doit-on utiliser pour ex\u00e9cuter un programme \u00e9crit en C\u2009?

    • Windows
    • Linux
    • macOS
    • N'imorte lequel

    Exercice 5\u2009: Copilot

    Qu'est-ce que Copilot\u2009?

    • Une intelligence artificielle
    • Un \u00e9diteur de code
    • Un compilateur
    • Un IDE

    Exercice 6\u2009: POSIX sous Windows\u2009?

    Si je souhaite pouvoir d\u00e9velopper des programmes en C sous Windows compatibles avec la norme POSIX, que dois-je faire\u2009?

    • Rien, Windows est compatible POSIX
    • Installer un sous-syst\u00e8me Linux comme WSL2
    • Installer un compilateur GCC
    • Installer un IDE
    • Changer de syst\u00e8me d'exploitation
    "}, {"location": "course-c/05-introduction/programming/", "title": "Programmation", "text": "La programmation, c'est l'art d'organiser la complexit\u00e9.E. Dijkstra

    Objectifs

    • D\u00e9finir les concepts d'algorithmique et de programmation.
    • Exemples d'algorithmes.
    • Origine de la programmation.
    • Bref historique de l'informatique.
    • Introduction \u00e0 la machine de Turing.

    La programmation, \u00e9galement appel\u00e9e codage, est l'art subtil et rigoureux de transformer des concepts abstraits en instructions ex\u00e9cutables par une machine. \u00c0 travers cette discipline, le programmeur devient l'architecte d'un univers logique, o\u00f9 chaque ligne de code est une brique ajout\u00e9e \u00e0 une structure plus vaste, guid\u00e9e par un plan pr\u00e9cis\u2009: l'algorithme. Ce dernier, semblable \u00e0 une partition musicale, dicte la succession des op\u00e9rations que la machine, fid\u00e8le ex\u00e9cutante, doit suivre sans faillir.

    L'essence m\u00eame de la programmation r\u00e9side donc dans la traduction de ces algorithmes en un langage formel, une sorte de langage commun, \u00e9pur\u00e9 et sans ambigu\u00eft\u00e9, o\u00f9 l'esprit humain et le processeur se rencontrent. Cette activit\u00e9, \u00e0 la crois\u00e9e des chemins entre la science, l'ing\u00e9nierie et l'art, est avant tout une qu\u00eate de pr\u00e9cision, d'efficacit\u00e9, et d'\u00e9l\u00e9gance.

    Dans le cadre d'un enseignement acad\u00e9mique, on parle souvent de cours d'Algorithmique et de Programmation, soulignant ainsi la dualit\u00e9 indissociable entre la conception d'une solution (l'algorithme) et sa mise en \u0153uvre concr\u00e8te (la programmation). Ces deux notions, bien que distinctes, s'entrelacent pour former le c\u0153ur battant de l'informatique, o\u00f9 l'abstraction des id\u00e9es prend forme dans la rigueur du code. C'est \u00e0 ce croisement que nous allons maintenant nous attarder, pour \u00e9claircir ces concepts et en d\u00e9voiler toute la richesse.

    L'un des premiers ordinateurs\u2009: l'Eniac

    "}, {"location": "course-c/05-introduction/programming/#algorithmique", "title": "Algorithmique", "text": "

    L'algorithmique, et non l'algorithmie (qui n'a pas sa place dans la langue fran\u00e7aise), est la science qui se consacre \u00e0 l'\u00e9laboration des r\u00e8gles et techniques r\u00e9gissant la cr\u00e9ation et la conception des algorithmes. Ce domaine, que nous explorerons plus en d\u00e9tail dans le chapitre d\u00e9di\u00e9 aux algorithmes et \u00e0 leur conception, d\u00e9passe largement le cadre de l'informatique. L'algorithmique ne se cantonne pas aux ordinateurs\u2009; elle est omnipr\u00e9sente dans notre quotidien, se manifestant dans des contextes aussi vari\u00e9s que\u2009:

    • l'art de concocter une recette de cuisine,
    • la ma\u00eetrise du tissage des tapis persans,
    • la r\u00e9solution de casse-t\u00eate comme le Rubik's Cube,
    • l'\u00e9laboration de tactiques sportives,
    • ou encore dans les m\u00e9andres des proc\u00e9dures administratives.

    Ainsi, l'algorithmique n'est rien de moins que l'essence m\u00eame de la pens\u00e9e organis\u00e9e, une discipline qui transcende les fronti\u00e8res du num\u00e9rique pour s'infiltrer dans les moindres recoins de la vie courante, l\u00e0 o\u00f9 la logique et la m\u00e9thode s'imposent comme les guides naturels de toute action efficace.

    "}, {"location": "course-c/05-introduction/programming/#algorithme-deuclide", "title": "Algorithme d'Euclide", "text": "

    Dans le cadre math\u00e9matique et scientifique qui nous occupe, l'algorithme d'Euclide, datant probablement de 300 av. J.-C., constitue un exemple embl\u00e9matique. Cet algorithme, d'une \u00e9l\u00e9gance intemporelle, permet de d\u00e9terminer le plus grand commun diviseur (PGCD) de deux nombres. Sa logique, simple, mais puissante, se pr\u00eate parfaitement \u00e0 une repr\u00e9sentation sous forme de diagramme de flux comme repr\u00e9sent\u00e9 sur cette figure\u2009:

    Algorithme de calcul du PGCD d'Euclide

    Les informaticiens et ing\u00e9nieurs appr\u00e9cient particuli\u00e8rement l'usage des diagrammes pour synth\u00e9tiser et clarifier leurs id\u00e9es complexes. Le diagramme de flux, en tant qu'outil de communication visuelle, permet de repr\u00e9senter un processus de mani\u00e8re structur\u00e9e et accessible. Dans ce type de diagramme, les formes g\u00e9om\u00e9triques symbolisent des \u00e9tapes du processus, tandis que les fl\u00e8ches en indiquent le d\u00e9roulement. Par convention, les formes ovales marquent le d\u00e9but et la fin du processus, les rectangles d\u00e9signent les op\u00e9rations de traitement, et les losanges repr\u00e9sentent les d\u00e9cisions \u00e0 prendre. Une forme de d\u00e9cision pose une question et offre deux chemins possibles, chacun correspondant \u00e0 une r\u00e9ponse sp\u00e9cifique. Comme nous le verrons plus tard, tout processus de traitement d'information comporte n\u00e9cessairement une entr\u00e9e et une sortie, illustrant ainsi la dynamique intrins\u00e8que de l'algorithme.

    Ainsi, l'algorithme d'Euclide, par sa simplicit\u00e9 de conception et sa pertinence universelle, demeure un exemple parfait de la mani\u00e8re dont les id\u00e9es abstraites peuvent \u00eatre traduites en instructions claires, tant pour l'esprit que pour la machine.

    Si l'on d\u00e9sire d\u00e9terminer le plus grand commun diviseur de 42 et 30, il suffit de suivre pas \u00e0 pas l'algorithme d'Euclide, depuis le d\u00e9but jusqu'\u00e0 la conclusion comme le montre le tableau ci-dessous\u2009:

    Exemple de calcul du PGCD entre 42 et 30 \u00c9tape \\(a\\) \\(b\\) \\(r\\) Prendre deux entiers naturels \\(a\\) et \\(b\\) 42 30 non d\u00e9fini Est-ce que \\(b\\) est nul\u2009? non\u2009! 42 30 non d\u00e9fini Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 42 30 12 Remplacer \\(a\\) par \\(b\\) 30 30 12 Remplacer \\(b\\) par \\(r\\) 30 12 12 Est-ce que \\(b\\) est nul\u2009? non\u2009! 30 12 12 Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 30 12 6 Remplacer \\(a\\) par \\(b\\) 12 12 6 Remplacer \\(b\\) par \\(r\\) 12 6 6 Est-ce que \\(b\\) est nul\u2009? non\u2009! 12 6 6 Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 12 6 0 Remplacer \\(a\\) par \\(b\\) 6 6 0 Remplacer \\(b\\) par \\(r\\) 6 0 0 Est-ce que \\(b\\) est nul\u2009? oui\u2009! 6 0 0 Le PGCD de 42 et 30 est 6 6 0 0

    Exercice 1\u2009: Algorithme d'Euclide

    Appliquer l'algorithme d'Euclide aux entr\u00e9es \\(a\\) et \\(b\\) suivantes.

    Que vaut \\(a, b\\) et \\(r\\) \u00e0 la fin de l'algorithme, et quel est le plus grand diviseur commun\u2009?

    \\[a = 1260, b = 630\\]"}, {"location": "course-c/05-introduction/programming/#tri-a-bulles", "title": "Tri \u00e0 bulles", "text": "

    Un autre algorithme c\u00e9l\u00e8bre est celui du tri \u00e0 bulles, un proc\u00e9d\u00e9 de tri simple qui consiste \u00e0 comparer les \u00e9l\u00e9ments adjacents et \u00e0 les \u00e9changer si n\u00e9cessaire afin de les organiser dans l'ordre souhait\u00e9.

    Pour mieux l'illustrer, imaginez que vous avez un jeu de 54 cartes m\u00e9lang\u00e9 et que vous souhaitez le trier par ordre croissant (As, 2, 3, ..., 10, Valet, Dame, Roi). Vous disposez les cartes en ligne et proc\u00e9dez par \u00e9changes successifs de deux cartes adjacentes mal plac\u00e9es, r\u00e9p\u00e9tant l'op\u00e9ration jusqu'\u00e0 ce que l'ensemble du jeu soit correctement ordonn\u00e9.

    Voici un diagramme de flux repr\u00e9sentant l'algorithme du tri \u00e0 bulles\u2009:

    Algorithme de tri \u00e0 bulles.

    Soit un tableau de \\(N = 5\\) valeurs \u00e0 trier donn\u00e9 ci-dessous, le cycle se r\u00e9p\u00e8te jusqu'\u00e0 ce que le tableau soit compl\u00e8tement tri\u00e9. Si \\(s\\) est \u00e9gal \u00e0 0, il n'y a pas eu d'\u00e9change lors du parcours du tableau et le tableau est donc tri\u00e9.

    \\[T = {5, 3, 8, 4, 2}\\]

    Les diff\u00e9rentes \u00e9tapes du tri \u00e0 bulles sont illustr\u00e9es ci-dessous\u2009:

    \u00c9tape par \u00e9tape du tri \u00e0 bulles.

    Pour les cycles \\(3\\) et \\(4\\), nous ne montrons pas les \u00e9tapes ou il n'y a pas eu d'\u00e9change. Au cinqui\u00e8me cycle, aucun \u00e9change n'est n\u00e9cessaire, l'algorithme se termine.

    On peut compter le nombre de cycles assez facilement. Pour ce tableau de \\(N = 5\\) valeurs, il y a \\(5\\) cycles. Durant un cycle, il faut regarder \\(N - 1\\) paires d'\u00e9l\u00e9ments. Donc pour un tableau de \\(N\\) valeurs, il y a \\(N^2 - N\\) comparaisons. Ce type d'algorithme est dit de complexit\u00e9 \\(O(N^2)\\). Cela signifie que le nombre d'op\u00e9rations \u00e0 effectuer est proportionnel au carr\u00e9 du nombre d'\u00e9l\u00e9ments \u00e0 trier. Nous verrons plus tard que la complexit\u00e9 d'un algorithme est un crit\u00e8re important. Nous verrons comment le calculer.

    "}, {"location": "course-c/05-introduction/programming/#conclusion", "title": "Conclusion", "text": "

    Les algorithmes se d\u00e9clinent en une multitude de formes, des plus simples aux plus complexes, et trouvent leur utilit\u00e9 dans des domaines aussi vari\u00e9s que la cryptographie, la bio-informatique, la finance ou encore la robotique.

    En tant que d\u00e9veloppeur, vous serez souvent amen\u00e9 \u00e0 concevoir des algorithmes pour r\u00e9soudre divers probl\u00e8mes. Avant de plonger t\u00eate baiss\u00e9e dans l'\u00e9criture du code, il est essentiel de prendre un moment pour r\u00e9fl\u00e9chir pos\u00e9ment. Sortez une feuille de papier, un crayon, et laissez vos neurones travailler. Comprendre profond\u00e9ment le probl\u00e8me est une \u00e9tape cruciale, souvent n\u00e9glig\u00e9e par les jeunes d\u00e9veloppeurs qui, press\u00e9s de passer \u00e0 l'action, se jettent dans le code sans plan pr\u00e9cis, \u00ab\u2009touillant\u2009\u00bb leur syntaxe \u00e0 la vaudoise \u00e0 la recherche d'une solution miraculeuse qui na\u00eetrait du hasard. Prenez le temps de m\u00fbrir votre r\u00e9flexion, imaginez des exemples concrets, testez vos hypoth\u00e8ses, et vous d\u00e9couvrirez que la programmation, loin d'\u00eatre un combat, peut devenir un jeu d'enfant, empreint de logique et de clart\u00e9.

    "}, {"location": "course-c/05-introduction/programming/#programmation_1", "title": "Programmation", "text": "

    Parlons couture\u2009! La machine Jacquard est un m\u00e9tier \u00e0 tisser mis au point par Joseph Marie Jacquard en 1801. Il constitue le premier syst\u00e8me m\u00e9canique programmable avec cartes perfor\u00e9es.

    M\u00e9canisme Jacquard au Mus\u00e9e des arts et m\u00e9tiers de Paris.

    Les cartes perfor\u00e9es, ici des rouleaux de papier, contiennent donc la suite des actions guidant les crochets permettant de tisser des motifs complexes. Elles sont donc le programme de la machine et dont le format (largeur, dimension des trous, etc.) est sp\u00e9cifique \u00e0 la machine. En termes informatiques, on dirait que les cartes perfor\u00e9es sont \u00e9crites en langage machine.

    La r\u00e9volte des canuts

    L'av\u00e8nement de la machine Jacquard a r\u00e9volutionn\u00e9 l'industrie textile mais a aussi eu des cons\u00e9quences sociales. L'automatisation d'un travail qui jadis \u00e9tait effectu\u00e9 manuellement causa une vague de ch\u00f4mage menant \u00e0 la R\u00e9volte des canuts en 1831.

    La programmation d\u00e9finit toute activit\u00e9 menant \u00e0 l'\u00e9criture de programmes. En informatique, un programme est d\u00e9fini comme un ensemble ordonn\u00e9 d'instructions cod\u00e9es avec un langage donn\u00e9 et d\u00e9crivant les \u00e9tapes menant \u00e0 la r\u00e9solution d'un probl\u00e8me. Comme nous l'avons vu, il s'agit le plus souvent d'une \u00e9criture formelle d'un algorithme par l'interm\u00e9diaire d'un langage de programmation.

    Les informaticiens-tisserands responsables de la cr\u00e9ation des cartes perfor\u00e9es auraient pu se poser la question de comment simplifier leur travail en cr\u00e9ant un langage formel pour cr\u00e9er des motifs complexes et dont les composants de base se r\u00e9p\u00e8tent d'un travail \u00e0 l'autre. Prenons par exemple un ouvrier sp\u00e9cialis\u00e9 en h\u00e9raldique et devant cr\u00e9er des motifs complexes de blasons.

    Armoiries des ducs de Mayenne

    Nul n'est sans savoir que l'h\u00e9raldique a son langage parfois obscur et celui qui le ma\u00eetrise voudrait par exemple l'utiliser au lieu de manuellement percer les cartes pour chaque point de couture. Ainsi l'anachronique informaticien-tisserand souhaitant tisser le motif des armoiries duc de Mayenne aurait sans doute r\u00e9dig\u00e9 un programme informatique en utilisant sa langue. Le programme aurait pu ressembler \u00e0 ceci\u2009:

    \u00c9cartel\u00e9, en 1 et 4 :\n    coup\u00e9 et parti en 3,\n        au premier fasc\u00e9 de gueules et d'argent,\n        au deuxi\u00e8me d'azur sem\u00e9 de lys d'or\n            et au lambel de gueules,\n        au troisi\u00e8me d'argent \u00e0 la croix potenc\u00e9e d'or,\n            cantonn\u00e9e de quatre croisettes du m\u00eame,\n        au quatri\u00e8me d'or aux quatre pals de gueules,\n        au cinqui\u00e8me d'azur sem\u00e9 de lys d'or\n            et \u00e0 la bordure de gueules,\n        au sixi\u00e8me d'azur au lion contourn\u00e9 d'or,\n            arm\u00e9,\n            lampass\u00e9 et couronn\u00e9 de gueules,\n        au septi\u00e8me d'or au lion de sable,\n            arm\u00e9,\n            lampass\u00e9 de gueules,\n        au huiti\u00e8me d'azur sem\u00e9 de croisettes d'or\n            et aux deux bar d'or.\n    Sur le tout d'or \u00e0 la bande de gueules\n        charg\u00e9 de trois al\u00e9rions d'argent\n    Le tout bris\u00e9 d'un lambel de gueules ;\nEn 2 et 3 contre-\u00e9cartel\u00e9 :\n    en 1 et 4 d'azur,\n        \u00e0 l'aigle d'argent, becqu\u00e9e,\n        langu\u00e9e et couronn\u00e9e d'or et en 2 et 3 d'azur,\n        \u00e0 trois fleurs de lys d'or,\n        \u00e0 la bordure endent\u00e9e de gueules et d'or.\n

    Tout l'art est de pouvoir traduire ce texte compr\u00e9hensible par tout h\u00e9raldiste en un programme en langage machine compr\u00e9hensible par un m\u00e9tier \u00e0 tisser. Cette traduction est le r\u00f4le du compilateur que nous verrons plus tard. Quant au texte, et bien qu'il nous vient tout droit du moyen-\u00e2ge, il partage avec les langages de programmation modernes des caract\u00e9ristiques communes\u2009:

    Lexique

    le texte est compos\u00e9 de mots et de symboles qui ont un sens pr\u00e9cis, les couleurs (\u00e9maux) ont des termes sp\u00e9cifiques (gueules pour le rouge, azur pour le bleu, sable pour le noir, etc.), les figures (meubles) aussi (lys, croix, lion, aigle, etc.).

    Syntaxe

    le texte suit une structure grammaticale pr\u00e9cise, le fond (champ) est toujours mentionn\u00e9 en premier, les figures en second suivi de leurs attributs.

    S\u00e9mantique

    les termes peuvent adopter une certaine morphologie, par exemple le lion peut \u00eatre lampass\u00e9 (langue de couleur diff\u00e9rente), couronn\u00e9 (avec une couronne), arm\u00e9 (avec des griffes et des dents de couleur diff\u00e9rente). Cette s\u00e9mantique implique l'adjonction de pr\u00e9fixes ou de suffixes.

    Grammaire

    le texte est organis\u00e9 en phrases, les phrases sont organis\u00e9es en paragraphes, les paragraphes en sections, les symboles vont \u00eatre interpr\u00e9t\u00e9s en fonction de leur position dans le texte, de leur contexte.

    De gueules

    Notons que gueules signifie rouge. Le drapeau suisse est donc de gueules, \u00e0 la croix al\u00e9s\u00e9e d'argent.

    ", "tags": ["lexique", "mayenne-duc-de", "heraldique", "morphologie", "syntaxe", "langage-machine", "semantique", "grammaire", "compilateur"]}, {"location": "course-c/05-introduction/programming/#langage-de-programmation", "title": "Langage de programmation", "text": "

    Traduire un algorithme en une suite d'ordres compr\u00e9hensibles par une machine est donc le travail du programmeur. Il existe de nombreux langages de programmation, mais la plupart se regroupent en deux cat\u00e9gories\u2009:

    1. Les langages textuels qui utilisent du texte pour d\u00e9crire les instructions.
    2. Les langages visuels qui utilisent des \u00e9l\u00e9ments graphiques pour d\u00e9crire les instructions.

    L'\u00eatre humain a appris depuis des mill\u00e9naires \u00e0 communiquer avec des symboles, il stocke son savoir dans des livres ou \u00e0 une certaine \u00e9poque, sur des tablettes de cire. Au d\u00e9but de l'\u00e8re de l'informatique, l'ordinateur ne pouvait communiquer que par du texte. Les premiers langages de programmation \u00e9taient donc textuels. Avec l'av\u00e8nement des interfaces graphiques, les langages visuels ont vu le jour, mais ils sont davantage r\u00e9serv\u00e9s pour enseigner la programmation aux enfants ou pour faciliter la programmation de robots ou de jeux vid\u00e9os.

    Scratch

    Scratch est un langage de programmation visuel d\u00e9velopp\u00e9 par le MIT. Il est utilis\u00e9 pour enseigner les bases de la programmation aux enfants. Il permet de cr\u00e9er des animations, des jeux et des histoires interactives.

    Interface de scratch

    LabView

    LabView est un langage de programmation visuel d\u00e9velopp\u00e9 par National Instruments. Il est utilis\u00e9 pour la programmation de syst\u00e8mes de mesure et de contr\u00f4le. Il est tr\u00e8s utilis\u00e9 dans l'industrie et la recherche.

    Interface de LabView

    Son interface est compos\u00e9e de blocs graphiques que l'on relie entre eux pour cr\u00e9er un programme.

    Common Lisp

    Common Lisp est un langage de programmation invent\u00e9 en 1984. C'est un langage de programmation textuel de type fonctionnel. Voici un exemple de programme en Common Lisp pour r\u00e9soudre le probl\u00e8me des tours de Hano\u00ef :

    (defun hanoi (n source target auxiliary)\n  (when (> n 0)\n    (hanoi (- n 1) source auxiliary target)\n    (format t \"~%Move disk from ~A to ~A\" source target)\n    (hanoi (- n 1) auxiliary target source)))\n\n(defun solve-hanoi (n)\n  (hanoi n 'A 'C 'B))\n\n(solve-hanoi 3)\n

    Pour ce cours, et pour l'enseignement de la programmation en g\u00e9n\u00e9ral, nous utiliserons des langages textuels.

    ", "tags": ["lisp", "labview", "scratch"]}, {"location": "course-c/05-introduction/programming/#calculateur", "title": "Calculateur", "text": "

    Un calculateur du latin calculare: calculer avec des cailloux, originellement appel\u00e9s abaque, \u00e9tait un dispositif permettant de faciliter les calculs math\u00e9matiques.

    Les os d'Ishango dat\u00e9s de 20'000 ans sont des art\u00e9facts arch\u00e9ologiques attestant la pratique de l'arithm\u00e9tique dans l'histoire de l'humanit\u00e9.

    Os d'Ishango

    Si les anglophones ont d\u00e9tourn\u00e9 le verbe compute (calculer) en un nom computer, un ordinateur est g\u00e9n\u00e9ralement plus qu'un simple calculateur, car m\u00eame une calculatrice de poche doit g\u00e9rer en plus des calculs un certain nombre de p\u00e9riph\u00e9riques comme\u2009:

    • l'interface de saisie (pav\u00e9 num\u00e9rique);
    • l'affichage du r\u00e9sultat (\u00e9cran \u00e0 cristaux liquides).

    Notons qu'\u00e0 l'instar de notre diagramme de flux, un calculateur dispose aussi d'une entr\u00e9e, d'une sortie et d'\u00e9tats internes.

    ", "tags": ["calculateur"]}, {"location": "course-c/05-introduction/programming/#ordinateur", "title": "Ordinateur", "text": "

    Le terme ordinateur est tr\u00e8s r\u00e9cent, il daterait de 1955, cr\u00e9\u00e9 par Jacques Perret \u00e0 la demande d'IBM France (c.f. 2014\u2009: 100 ans d'IBM en France). Voici la lettre de Jacques Perret \u00e0 IBM France\u2009:

    \u00ab Le 16 IV 1955, Cher Monsieur,

    Que diriez-vous d\u2019ordinateur? C\u2019est un mot correctement form\u00e9, qui se trouve m\u00eame dans le Littr\u00e9 comme adjectif d\u00e9signant Dieu qui met de l\u2019ordre dans le monde. Un mot de ce genre a l\u2019avantage de donner ais\u00e9ment un verbe ordiner, un nom d\u2019action ordination. L\u2019inconv\u00e9nient est que ordination d\u00e9signe une c\u00e9r\u00e9monie religieuse\u2009; mais les deux champs de signification (religion et comptabilit\u00e9) sont si \u00e9loign\u00e9s et la c\u00e9r\u00e9monie d\u2019ordination connue, je crois, de si peu de personnes que l\u2019inconv\u00e9nient est peut-\u00eatre mineur. D\u2019ailleurs votre machine serait ordinateur (et non-ordination) et ce mot est tout \u00e0 fait sorti de l\u2019usage th\u00e9ologique. Syst\u00e9mateur serait un n\u00e9ologisme, mais qui ne me para\u00eet pas offensant\u2009; il permet syst\u00e9matis\u00e9\u2009; \u2014 mais syst\u00e8me ne me semble gu\u00e8re utilisable \u2014 Combinateur a l\u2019inconv\u00e9nient du sens p\u00e9joratif de combine\u2009; combiner est usuel donc peu capable de devenir technique\u2009; combination ne me para\u00eet gu\u00e8re viable \u00e0 cause de la proximit\u00e9 de combinaison. Mais les Allemands ont bien leurs combinats (sorte de trusts, je crois), si bien que le mot aurait peut-\u00eatre des possibilit\u00e9s autres que celles qu\u2019\u00e9voque combine.

    Congesteur, digesteur \u00e9voquent trop congestion et digestion. Synth\u00e9tiseur ne me para\u00eet pas un mot assez neuf pour d\u00e9signer un objet sp\u00e9cifique, d\u00e9termin\u00e9 comme votre machine.

    En relisant les brochures que vous m\u2019avez donn\u00e9es, je vois que plusieurs de vos appareils sont d\u00e9sign\u00e9s par des noms d\u2019agent f\u00e9minins (trieuse, tabulatrice). Ordinatrice serait parfaitement possible et aurait m\u00eame l\u2019avantage de s\u00e9parer plus encore votre machine du vocabulaire de la th\u00e9ologie. Il y a possibilit\u00e9 aussi d\u2019ajouter \u00e0 un nom d\u2019agent un compl\u00e9ment\u2009: ordinatrice d\u2019\u00e9l\u00e9ments complexes ou un \u00e9l\u00e9ment de composition, par exemple\u2009: s\u00e9lecto-syst\u00e9mateur. S\u00e9lecto-ordinateur a l\u2019inconv\u00e9nient de deux o en hiatus, comme \u00e9lectro-ordonnatrice.

    Il me semble que je pencherais pour ordonnatrice \u00e9lectronique. Je souhaite que ces suggestions stimulent, orientent vos propres facult\u00e9s d\u2019invention. N\u2019h\u00e9sitez pas \u00e0 me donner un coup de t\u00e9l\u00e9phone si vous avez une id\u00e9e qui vous paraisse requ\u00e9rir l\u2019avis d\u2019un philologue.

    V\u00f4tre, Jacques Perret \u00bb

    ", "tags": ["jacques-perret", "ordinateur"]}, {"location": "course-c/05-introduction/programming/#la-machine-de-turing", "title": "La machine de Turing", "text": "

    Il est impossible d'introduire les notions d'ordinateur, de programmes et d'algorithmes sans \u00e9voquer la figure embl\u00e9matique d'Alan Turing. Ce math\u00e9maticien britannique, v\u00e9ritable pionnier de l'informatique, a jou\u00e9 un r\u00f4le crucial dans l'histoire, notamment en d\u00e9chiffrant le code de la machine Enigma utilis\u00e9e par les forces allemandes pendant la Seconde Guerre mondiale.

    La machine de Turing est un mod\u00e8le th\u00e9orique fondamental qui repr\u00e9sente la conception d'un ordinateur. Imagin\u00e9e comme une bande infinie divis\u00e9e en cases, elle est dot\u00e9e d'une t\u00eate de lecture/\u00e9criture et d'un ensemble fini d'\u00e9tats. Cette machine peut lire et \u00e9crire des symboles sur la bande, se d\u00e9placer \u00e0 gauche ou \u00e0 droite, et changer d'\u00e9tat en fonction des instructions re\u00e7ues. Capable de simuler n'importe quel algorithme, la machine de Turing est un mod\u00e8le abstrait qui a permis de d\u00e9finir la notion de calculabilit\u00e9 et de poser les bases de l'informatique th\u00e9orique.

    Lorsqu'on parle d'un ordinateur Turing-complet, on fait r\u00e9f\u00e9rence \u00e0 un dispositif capable de simuler n'importe quel algorithme, condition sine qua non pour les ordinateurs modernes. Ces machines se composent d'un programme et d'une m\u00e9moire\u2009: le programme, une suite d'instructions pr\u00e9cises, est ex\u00e9cut\u00e9 par le processeur, tandis que la m\u00e9moire sert d'espace de stockage pour les donn\u00e9es et les instructions.

    Prenons l'exemple d'un programme visant \u00e0 ajouter 1 \u00e0 un nombre n en binaire. L'algorithme correspondant pourrait \u00eatre d\u00e9crit ainsi\u2009:

    Algorithme d'addition binaire

    On commence par l'\u00e9tat de gauche, on lit un symbole sur la bande. Tant que ce symbole est 0 ou 1 on avance \u00e0 droite. Lorsque l'on rencontre une case vide, on se d\u00e9place \u00e0 gauche et on entre dans le second \u00e9tat. Tant qu\u2019on lit un 1, on le remplace par un 0 et on avance \u00e0 gauche. Lorsqu\u2019on lit un 0 ou une case vide, on le remplace par un 1 et on se d\u00e9place \u00e0 gauche. On revient \u00e0 l'\u00e9tat initial et on continue jusqu'\u00e0 ce que l'on rencontre une case vide.

    Sur la figure ci-dessous, on peut voir l'ex\u00e9cution de l'algorithme sur une bande apr\u00e8s chaque \u00e9tape. La case centrale est celle sous la t\u00eate de lecture/\u00e9criture. On voit bien qu'au d\u00e9but on a le nombre 101 (5) et \u00e0 la fin on obtient le nombre 110 (6). L'algorithme a bien fonctionn\u00e9.

    Ex\u00e9cution de l'algorithme sur une bande

    On peut essayer de traduire cet algorithme dans un langage formel\u2009:

    Pseudo codeLangage formel de TuringC
    d\u00e9but:\n    lire symbole\n    si symbole = 0 ou 1 alors\n        avancer \u00e0 droite\n        aller \u00e0 d\u00e9but\n    sinon si symbole = vide alors\n        se d\u00e9placer \u00e0 gauche\n        aller retenue\nretenue:\n    lire symbole\n    si symbole = 1 alors\n        \u00e9crire 0\n        se d\u00e9placer \u00e0 gauche\n        aller \u00e0 retenue\n    sinon si symbole = 0 ou vide alors\n        \u00e9crire 1\n        se d\u00e9placer \u00e0 gauche\n
    input: '101'\ntable:\n  right:\n    [1,0]: R\n    ' '  : {L: carry}\n  carry:\n    1      : {write: 0, L}\n    [0,' ']: {write: 1, L: done}\ndone:\n
    #include <stdio.h>\n#include <string.h>\n\n#define BAND_SIZE 1000\n\nint main() {\n    char tape[BAND_SIZE] = {0};\n    int head = BAND_SIZE / 2; // Position au milieu de la bande\n\n    scanf(\"%s\", tape + head); // Saisie du nombre d'entr\u00e9e\n\n    // Algorithme d'addition\n    char c = tape[head];\n    while (c == '0' || c == '1')\n        c = tape[++head];\n    c =  tape[--head];\n    while (c == '1') {\n        tape[head--] = '0';\n        c = tape[head];\n    }\n    tape[head] = '1';\n\n    // Recherche de la position du premier symbole non nul\n    while (tape[head]) head--;\n    head++;\n    printf(\"%s\\n\", tape + head);\n}\n

    "}, {"location": "course-c/05-introduction/programming/#lordinateur-dantan", "title": "L'ordinateur d'antan", "text": "

    T\u00e9l\u00e9scripteur Siemens T100

    Le t\u00e9l\u00e9scripteur Siemens T100 est un exemple d'ordinateur des ann\u00e9es 1960. Il \u00e9tait utilis\u00e9 pour la transmission de messages t\u00e9l\u00e9graphiques. Il \u00e9tait compos\u00e9 d'un clavier et d'une imprimante. Il \u00e9tait capable de lire et d'\u00e9crire des messages sur une bande de papier. Il \u00e9tait programm\u00e9 en utilisant des cartes perfor\u00e9es.

    On les appelait aussi t\u00e9l\u00e9type ou abr\u00e9g\u00e9 TTY. Ce terme est rest\u00e9 aujourd'hui pour d\u00e9signer une console de terminal.

    "}, {"location": "course-c/05-introduction/programming/#lordinateur-moderne", "title": "L'ordinateur moderne", "text": "

    Les ordinateurs modernes sont des machines complexes qui contiennent plusieurs composants. Les composants principaux d'un ordinateur sont\u2009:

    Le processeur (CPU)

    c'est le cerveau de l'ordinateur. Il ex\u00e9cute les ordres du programme.

    La m\u00e9moire (RAM)

    c'est l'espace de stockage temporaire des donn\u00e9es et des instructions du programme.

    Le disque dur (HDD/SSD)

    c'est l'espace de stockage permanent des donn\u00e9es.

    Les p\u00e9riph\u00e9riques d'entr\u00e9e/sortie

    ce sont les interfaces qui permettent \u00e0 l'ordinateur de communiquer avec l'utilisateur (clavier, souris, \u00e9cran, imprimante, etc.).

    Contrairement \u00e0 la machine de Turing, les ordinateurs sont \u00e9quip\u00e9s d'une m\u00e9moire \u00e0 acc\u00e8s al\u00e9atoire qui permet d'acc\u00e9der n'importe quel \u00e9l\u00e9ment de la m\u00e9moire sans avoir \u00e0 parcourir toute la bande. \u00c9galement, ces ordinateurs disposent d'un processeur capable de calculer des op\u00e9rations arithm\u00e9tiques et logiques en un temps tr\u00e8s court. Ces processeurs peuvent m\u00eame calculer des fonctions trigonom\u00e9triques, exponentielles et logarithmiques facilement. En reprenant notre programme d'addition binaire, il est beaucoup plus facile de l'\u00e9crire en C\u2009:

    #include <stdio.h>\nint main() {\n    int n;\n    scanf(\"%d\", &n);\n    printf(\"%d\", n + 1);\n}\n

    N\u00e9anmoins, il est important de comprendre que ce programme est traduit en langage machine par un programme appel\u00e9 compilateur. Une \u00e9tape interm\u00e9diaire est la traduction du programme en langage assembleur. Le langage assembleur est un langage de plus bas niveau qui permet de contr\u00f4ler directement le processeur. Ce sont les instructions primitives du processeur. Le programme ci-dessus sera converti en assembleur X86 comme suit\u2009:

    .LC0:\n  .string \"%d\"\nmain:\n  sub     rsp, 24\n  mov     edi, OFFSET FLAT:.LC0\n  xor     eax, eax\n  lea     rsi, [rsp+12]\n  call    scanf\n  mov     eax, DWORD PTR [rsp+12]\n  mov     edi, OFFSET FLAT:.LC0\n  lea     esi, [rax+1]\n  xor     eax, eax\n  call    printf\n  xor     eax, eax\n  add     rsp, 24\n  ret\n

    Ce programme assembleur peut ensuite \u00eatre converti en langage machine binaire qui est le langage compris par le processeur.

    48 83 ec 18\nbf 00 00 00 00\n31 c0\n48 8d 74 24 0c\ne8 00 00 00 00\n8b 44 24 0c\nbf 00 00 00 00\n48 8d 70 01\n31 c0\ne8 00 00 00 00\n31 c0\n48 83 c4 18\nc3\n

    In fine, ce programme sera \u00e9crit en m\u00e9moire avec des 1 et des 0\u2009:

    01001000100000111110110000011000101111110000000000000000000000000000000000110001\n11000000010010001000110101110100001001000000110011101000000000000000000000000000\n00000000100010110100010000100100000011001011111100000000000000000000000000000000\n01001000100011010111000000000001001100011100000011101000000000000000000000000000\n0000000000110001110000000100100010000011110001000001100011000011\n

    "}, {"location": "course-c/05-introduction/programming/#les-systemes-a-microcontroleurs", "title": "Les syst\u00e8mes \u00e0 microcontr\u00f4leurs", "text": "

    Les microcontr\u00f4leurs sont des ordinateurs complets int\u00e9gr\u00e9s dans un seul circuit int\u00e9gr\u00e9. Ils sont omnipr\u00e9sents dans notre vie quotidienne. Que ce soit la t\u00e9l\u00e9vision, le t\u00e9l\u00e9phone portable, les machines \u00e0 caf\u00e9, les voitures, les jouets, les montres ou les appareils \u00e9lectrom\u00e9nagers, ils contiennent tous un ou plusieurs microcontr\u00f4leurs.

    Ces derniers sont aussi programm\u00e9s en impl\u00e9mentant des algorithmes. Le plus souvent ces algorithmes sont \u00e9crits en langage C car c'est un langage de programmation tr\u00e8s proche du langage machine. Les microcontr\u00f4leurs sont souvent utilis\u00e9s pour contr\u00f4ler des syst\u00e8mes en temps r\u00e9el. Ils sont capables de lire des capteurs, de contr\u00f4ler des actionneurs et de communiquer avec d'autres syst\u00e8mes.

    Machine \u00e0 caf\u00e9 Citiz de Nespresso

    Prenons l'exemple de cette machine \u00e0 caf\u00e9. C'est une machine qui co\u00fbte environ 100 CHF. Elle est \u00e9quip\u00e9e d'un microcontr\u00f4leur \u00e0 30 centimes qui contr\u00f4le le chauffage, la pompe \u00e0 eau et les leds. Le microcontr\u00f4leur est programm\u00e9 pour lire les boutons de commande, contr\u00f4ler les actionneurs et afficher des messages \u00e0 l'utilisateur.

    Sch\u00e9ma bloc de la machine \u00e0 caf\u00e9 Citiz

    Derri\u00e8re se cache un programme, bien complexe. Si vous avez une de ces machines mettez l\u00e0 en service, vous verrez que s'il manque de l'eau vous aurez un message d'erreur. Au d\u00e9marrage, les LEDs clignotent le temps que la machine chauffe. Une fois en temp\u00e9rature, vous pouvez l'utiliser. Ce sont des algorithmes qui sont derri\u00e8re tout cela.

    "}, {"location": "course-c/05-introduction/programming/#historique", "title": "Historique", "text": "

    Historique

    Pour mieux se situer dans l'histoire de l'informatique, voici quelques dates cl\u00e9s\u2009:

    87 av. J.-C.

    La machine d'Anticyth\u00e8re consid\u00e9r\u00e9 comme le premier calculateur analogique pour positions astronomiques permettant de pr\u00e9dire des \u00e9clipses. Cette machine encore si myst\u00e9rieuse a inspir\u00e9 de nombreux sc\u00e9narios comme le film Indiana Jones et le Cadran de la destin\u00e9e. Elle a \u00e9t\u00e9 d\u00e9couverte en 1901 dans une \u00e9pave au large de l'\u00eele d'Anticyth\u00e8re. Gr\u00e2ce aux techniques modernes de radiographie, on a pu reconstruire une partie de son m\u00e9canisme.

    1642

    La Pascaline: machine d'arithm\u00e9tique de Blaise Pascal, premi\u00e8re machine \u00e0 calculer. Elle permettait d'effectuer des additions et des soustractions en utilisant des roues dent\u00e9es.

    1801

    M\u00e9tier \u00e0 tisser Jacquard programmable avec des cartes perfor\u00e9es.

    1837

    Machine \u00e0 calculer programmable de Charles Babbage. Charles Babbage est consid\u00e9r\u00e9 comme le p\u00e8re de l'informatique. Il a con\u00e7u la machine analytique qui est consid\u00e9r\u00e9e comme le premier ordinateur programmable. Ada Lovelace, fille de Lord Byron, est consid\u00e9r\u00e9e comme la premi\u00e8re programmeuse de l'histoire.

    1936

    La machine de Turing est un mod\u00e8le th\u00e9orique d'un ordinateur capable de simuler n'importe quel algorithme. Elle a \u00e9t\u00e9 invent\u00e9e par Alan Turing.

    1937

    l'ASCC (Automatic Sequence Controlled Calculator Mark I) d'IBM, le premier grand calculateur num\u00e9rique. Il \u00e9tait constitu\u00e9 de 765'000 pi\u00e8ces, dont des interrupteurs, des relais, des arbres m\u00e9caniques et des embrayages. Les ordres \u00e9taient lus \u00e0 partir d'une bande perfor\u00e9e. Une seconde bande perfor\u00e9e contenait les donn\u00e9es d'entr\u00e9e. Les instructions \u00e9tant simples, pour r\u00e9p\u00e9ter un algorithme en boucle comme l'algorithme d'Euclide, on pouvait typiquement cr\u00e9er une boucle dans la bande perfor\u00e9e.

    • 4500 kg
    • 6 secondes par multiplication \u00e0 23 chiffres d\u00e9cimaux
    • Cartes perfor\u00e9es
    1945

    L'ENIAC, de Presper Eckert et John William Mauchly. C'est le premier ordinateur Turing-complet enti\u00e8rement \u00e9lectronique et fonctionnant avec des diodes et des tubes \u00e0 vide. Il \u00e9tait programm\u00e9 en branchant des c\u00e2bles et en changeant des interrupteurs. Il \u00e9tait utilis\u00e9 pour des calculs balistiques.

    • 160 kW
    • 100 kHz
    • Tubes \u00e0 vide
    • 100'000 additions/seconde
    • 357 multiplications/seconde
    1965

    Premier ordinateur \u00e0 circuits int\u00e9gr\u00e9s, le PDP-8

    • 12 bits
    • m\u00e9moire de 4096 mots
    • Temps de cycle de 1.5 \u00b5s
    • Fortran et BASIC
    2018

    Le Behold Summit est un superordinateur construit par IBM.

    • 200'000'000'000'000'000 multiplications par seconde
    • simple ou double pr\u00e9cision
    • 14.668 GFlops/watt
    • 600 GiB de m\u00e9moire RAM
    2022

    Le Frontier ou OLCF-5 est le premier ordinateur exaflopique du monde.

    • 1,714,810,000,000,000,000 multiplications par seconde (1.1 exaflops)
    • 9472 processeurs Trento \u00e0 64 c\u0153urs de 2 GHz (606 208 c\u0153urs)
    • 37888 processeurs graphiques MI250x (8 335 360 coeurs)
    • 22.7 MW (5 locomotives \u00e9lectriques ou 56'750 foyers europ\u00e9ens)
    • 62.68 GFlops/watt
    "}, {"location": "course-c/05-introduction/programming/#conclusion_1", "title": "Conclusion", "text": "

    Les algorithmes existent depuis fort longtemps et sont utilis\u00e9s dans de nombreux domaines. Ils sont la base de la programmation et de l'informatique.

    Les hommes ont cherch\u00e9 \u00e0 pouvoir automatiser leurs t\u00e2ches, d'abord avec des machines m\u00e9caniques comme le m\u00e9tier \u00e0 tisser Jacquard. Puis, apr\u00e8s l'invention de la micro\u00e9lectronique, il a \u00e9t\u00e9 possible de complexifier ces machines pour en faire des ordinateurs.

    Pour les contr\u00f4ler, les informaticiens \u00e9crivent des programmes qui impl\u00e9mentent des algorithmes. Ces programmes sont ensuite traduits en langage machine par un compilateur.

    Aujourd'hui, les superordinateurs sont capables de r\u00e9aliser des milliards de milliards d'op\u00e9rations par seconde, mais ils sont toujours programm\u00e9s de la m\u00eame mani\u00e8re\u2009: avec du texte.

    "}, {"location": "course-c/05-introduction/programming/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Ordinateur

    Quelle est l'\u00e9tymologie du mot ordinateur ?

    • calculateur
    • ordonnateur
    • syst\u00e9mateur
    • ordiner

    Exercice 3\u2009: Machine de Turing

    Qu'est-ce que la machine de Turing\u2009?

    • Une bombe r\u00e9alis\u00e9e pour casser le code de la machine Enigma.
    • Un mod\u00e8le th\u00e9orique d'un ordinateur capable de simuler n'importe quel algorithme.
    • Le premier ordinateur \u00e9lectronique.
    • Un mod\u00e8le th\u00e9orique d'un ordinateur ne pouvant pas simuler n'importe quel algorithme.

    Exercice 4\u2009: Machine \u00e0 caf\u00e9

    Une machine \u00e0 caf\u00e9 est \u00e9quip\u00e9e d'un , qui est l'organe de contr\u00f4le de la machine. Ce dernier comporte des comme les boutons de commande ou les capteurs ainsi que des comme les LEDs et les actionneurs. Une permet de stocker les param\u00e8tres de configuration de la machine ainsi que son programme.

    Submit

    "}, {"location": "course-c/10-numeration/", "title": "Num\u00e9ration", "text": "

    La num\u00e9ration d\u00e9signe le mode de repr\u00e9sentation des nombres (p. ex. cardinaux, ordinaux), leur base (syst\u00e8me binaire, ternaire, quinaire, d\u00e9cimal ou vic\u00e9simal), ainsi que leur codification comme IEEE 754, compl\u00e9ment \u00e0 un, compl\u00e9ment \u00e0 deux. Bien comprendre les bases de la num\u00e9ration est important pour l'ing\u00e9nieur d\u00e9veloppeur, car il est souvent amen\u00e9 \u00e0 effectuer des op\u00e9rations de bas niveau sur les nombres.

    Ce chapitre n'est strictement essentiel qu'au programmeur de bas niveau, l'\u00e9lectronicien ou l'informaticien technique. Bien comprendre la mani\u00e8re dont les nombres sont repr\u00e9sent\u00e9s dans un ordinateur et de mani\u00e8re plus g\u00e9n\u00e9rale l'information trait\u00e9e par ces machines est tr\u00e8s utile pour \u00e9crire des programmes performants et \u00e9laborer des algorithmes. En somme, la num\u00e9ration permet de mieux se repr\u00e9senter la mani\u00e8re dont l'ordinateur traite les donn\u00e9es au niveau le plus fondamental\u2009: le bit.

    ", "tags": ["numeration", "complement-a-deux", "bit", "complement-a-un"]}, {"location": "course-c/10-numeration/bases/", "title": "Bases", "text": "Il y a 10 types de personnes dans le monde, celles qui comprennent le binaire, et celles qui ne le comprennent pas.M\u00e8me internet

    Une base d\u00e9signe la valeur dont les puissances successives interviennent dans l'\u00e9criture des nombres dans la num\u00e9ration positionnelle, laquelle est un proc\u00e9d\u00e9 par lequel l'\u00e9criture des nombres est compos\u00e9e de chiffres ou symboles reli\u00e9s \u00e0 leur position voisine par un multiplicateur, appel\u00e9 base du syst\u00e8me de num\u00e9ration.

    Sans cette connaissance \u00e0 priori du syst\u00e8me de num\u00e9ration utilis\u00e9, il vous est impossible d'interpr\u00e9ter les nombres suivants\u2009:

    69128\n11027\nj4b12\n>>!!0\n\u4e5d\u5343\u5341\u516b\n\u4e5d\u5343 \u96f6\u5341\u516b\n

    En effet, au-del\u00e0 de l'ordre des symboles (de gauche \u00e0 droite), la base du syst\u00e8me utilis\u00e9 est cruciale pour interpr\u00e9ter ces nombres. Cette base d\u00e9termine le nombre de symboles distincts qui peuvent \u00eatre employ\u00e9s pour chaque position, et par cons\u00e9quent, elle r\u00e9git la structure m\u00eame du nombre. Par exemple, une base dix (d\u00e9cimale) utilise dix symboles (0-9), tandis qu'une base deux (binaire) n'en utilise que deux (0 et 1). Sans cette compr\u00e9hension, les nombres demeurent incompr\u00e9hensibles et d\u00e9pourvus de signification.

    Exercice 1\u2009: Symboles binaires

    Dans la notation binaire, compos\u00e9s de 1 et de 0, combien de symboles existent et combien de positions y-a-t-il dans le nombre 11001 ?

    Solution

    Le nombre 11001 est compos\u00e9 de 5 positions et de deux symboles possibles par position\u2009: 1 et 0. La quantit\u00e9 d'information est donc e 5 bits.

    ", "tags": ["base"]}, {"location": "course-c/10-numeration/bases/#systeme-decimal", "title": "Syst\u00e8me d\u00e9cimal", "text": "

    Le syst\u00e8me d\u00e9cimal est le syst\u00e8me de num\u00e9ration utilisant la base dix et le plus utilis\u00e9 par l'humanit\u00e9 au vingt et uni\u00e8me si\u00e8cle, ce qui n'a pas toujours \u00e9t\u00e9 le cas. Par exemple, les anciennes civilisations de M\u00e9sopotamie (Sumer ou Babylone) utilisaient un syst\u00e8me positionnel de base sexag\u00e9simale (60) toujours utilis\u00e9 pour la repr\u00e9sentation des heures ou des angles, la civilisation maya utilisait un syst\u00e8me de base 20 encore ancr\u00e9e dans la culture fran\u00e7aise de m\u00eame que certaines langues celtiques dont il reste aujourd'hui quelques traces en fran\u00e7ais avec la d\u00e9nomination quatre-vingts.

    L'exemple suivant montre l'\u00e9criture de 1506 en \u00e9criture hi\u00e9roglyphique de\u2009:

    \\[ 1000+100+100+100+100+100+1+1+1+1+1+1\\]

    Il s'agit d'une num\u00e9ration additive.

    1506 en \u00e9criture hi\u00e9roglyphique

    Notre syst\u00e8me de repr\u00e9sentation des nombres d\u00e9cimaux est le syst\u00e8me de num\u00e9ration indo-arabe qui emploie une notation positionnelle et dix chiffres (ou symboles) allant de z\u00e9ro \u00e0 neuf et un nombre peut se d\u00e9composer en puissance successive\u2009:

    \\[ 1506_{10} = 1 \\cdot 10^{3} + 5 \\cdot 10^{2} + 0 \\cdot 10^{1} + 6 \\cdot 10^{0} \\]

    Nous l'avons vu au chapitre pr\u00e9c\u00e9dent, la base dix n'est pas utilis\u00e9e dans les ordinateurs, car elle n\u00e9cessite la manipulation de dix \u00e9tats, ce qui est difficile avec les syst\u00e8mes logiques \u00e0 deux \u00e9tats\u2009; le stockage d'un bit en m\u00e9moire \u00e9tant g\u00e9n\u00e9ralement assur\u00e9 par des transistors.

    Exercice 2\u2009: Deux mains

    Un dessin repr\u00e9sentant deux mains humaines (compos\u00e9es chacune de cinq doigts) est utilis\u00e9 pour repr\u00e9senter un chiffre. Les doigts peuvent \u00eatre soit lev\u00e9s, soit baiss\u00e9s mais un seul doigt peut \u00eatre lev\u00e9. Quelle est la base utilis\u00e9e\u2009?

    Solution

    Deux mains de cinq doigts forment une paire compos\u00e9e de 10 doigts. Il existe donc dix possibilit\u00e9s, la base est donc d\u00e9cimale\u2009: 10.

    Si plusieurs doigts peuvent \u00eatre lev\u00e9s \u00e0 la fois, il faut r\u00e9duire le syst\u00e8me \u00e0 l'unit\u00e9 de base \u00ab\u2009le doigt\u2009\u00bb pouvant prendre deux \u00e9tats\u2009: lev\u00e9 ou baiss\u00e9. Avec dix doigts (dix positions) et 2 symboles par doigts, un ombre binaire est ainsi repr\u00e9sent\u00e9.

    ", "tags": ["systeme-decimal", "indo-arabe", "sexagesimale"]}, {"location": "course-c/10-numeration/bases/#systeme-binaire", "title": "Syst\u00e8me binaire", "text": "

    Le syst\u00e8me binaire est similaire au syst\u00e8me d\u00e9cimal, mais utilise la base deux. Les symboles utilis\u00e9s pour exprimer ces deux \u00e9tats possibles sont d'ailleurs emprunt\u00e9s au syst\u00e8me indo-arabe\u2009:

    \\[ \\begin{bmatrix} 0\\\\ 1 \\end{bmatrix} = \\begin{bmatrix} \\text{true}\\\\ \\text{false} \\end{bmatrix} = \\begin{bmatrix} T\\\\ F \\end{bmatrix} \\]

    En termes techniques ces \u00e9tats sont le plus souvent repr\u00e9sent\u00e9s par des signaux \u00e9lectriques dont souvent l'un des deux \u00e9tats est dit r\u00e9cessif tandis que l'autre est dit dominant. Par exemple si l'\u00e9tat 0 est symbolis\u00e9 par un verre vide et l'\u00e9tat 1 par un verre contenant du liquide. L'\u00e9tat dominant est l'\u00e9tat 1. En effet, si le verre contient d\u00e9j\u00e0 du liquide, en rajouter ne changera pas l'\u00e9tat actuel, il y aura juste plus de liquide dans le verre.

    Un nombre binaire peut \u00eatre \u00e9galement d\u00e9compos\u00e9 en puissance successive\u2009:

    \\[ 1101_{2} = 1 \\cdot 2^{3} + 1 \\cdot 2^{2} + 0 \\cdot 2^{1} + 1 \\cdot 2^{0} \\]

    Le nombre de possibilit\u00e9s pour un nombre de positions \\(E\\) et une quantit\u00e9 de symboles (ou base) \\(b\\) de 2 est simplement exprim\u00e9 par\u2009:

    \\[ N = b^E \\]

    Avec un seul bit il est donc possible d'exprimer 2 valeurs distinctes.

    Exercice 3\u2009: Base 2

    Combien de valeurs d\u00e9cimales peuvent \u00eatre repr\u00e9sent\u00e9es avec 10-bits\u2009?

    Solution

    Avec une base binaire 2 et 10 bits, le total repr\u00e9sentable est\u2009:

    \\[2^10 = 1024\\]

    Soit les nombres de 0 \u00e0 1023.

    ", "tags": ["systeme-binaire", "bit"]}, {"location": "course-c/10-numeration/bases/#systeme-octal", "title": "Syst\u00e8me octal", "text": "

    Invent\u00e9 par Charles XII de Su\u00e8de , le syst\u00e8me de num\u00e9ration octal utilise 8 symboles emprunt\u00e9s au syst\u00e8me indo-arabe. Ce syst\u00e8me pourrait avoir \u00e9t\u00e9 utilis\u00e9 par l'homme en comptant soit les jointures des phalanges proximales (trous entre les doigts), ou les doigts diff\u00e9rents des pouces.

    0 1 2 3 4 5 6 7\n

    Notons que l'utilisation des 8 premiers symboles du syst\u00e8me indo-arabe est une convention d'usage bien pratique, car tout humain occidental est familier de ces symboles. L'inconv\u00e9nient est qu'un nombre \u00e9crit en octal pourrait \u00eatre confondu avec un nombre \u00e9crit en d\u00e9cimal. Comme pour le syst\u00e8me d\u00e9cimal, un nombre octal peut \u00e9galement \u00eatre d\u00e9compos\u00e9 en puissance successive\u2009:

    \\[ 1607_{8} = 1 \\cdot 8^{3} + 6 \\cdot 8^{2} + 0 \\cdot 8^{1} + 7 \\cdot 8^{0} \\]

    Au d\u00e9but de l'informatique, la base octale fut tr\u00e8s utilis\u00e9e, car il est tr\u00e8s facile de la construire \u00e0 partir de la num\u00e9ration binaire, en regroupant les chiffres par triplets\u2009:

    010'111'100'001\u2082 = 2741\u2088\n

    En C, un nombre octal est \u00e9crit en pr\u00e9fixant la valeur \u00e0 repr\u00e9senter d'un z\u00e9ro. Attention donc \u00e0 ne pas confondre\u2009:

    int octal = 042; // (1)!\nint decimal = 42;\n\nassert(octal != decimal);\n
    1. La valeur 042 est un nombre octal, soit \\(4 \\cdot 8^1 + 2 \\cdot 8^0 = 34\\) en d\u00e9cimal. En C un nombre octal est pr\u00e9fix\u00e9 par un z\u00e9ro.

    Il est \u00e9galement possible de faire r\u00e9f\u00e9rence \u00e0 un caract\u00e8re en utilisant l'\u00e9chappement octal dans une cha\u00eene de caract\u00e8re\u2009:

    char cr = '\\015';\nchar msg = \"Hell\\0157\\040World!\";\n

    Important

    N'essayez pas de pr\u00e9fixer vos nombres avec des z\u00e9ros lorsque vous programmer car ces nombres seraient alors interpr\u00e9t\u00e9s en octal et non en d\u00e9cimal.

    "}, {"location": "course-c/10-numeration/bases/#systeme-hexadecimal", "title": "Syst\u00e8me hexad\u00e9cimal", "text": "

    Ce syst\u00e8me de num\u00e9ration positionnel en base 16 est le plus utilis\u00e9 en informatique pour exprimer des grandeurs binaires. Il utilise les dix symboles du syst\u00e8me indo-arabe, plus les lettres de A \u00e0 F. Il n'y a pas de r\u00e9el consensus quant \u00e0 la casse des lettres qui peuvent \u00eatre soit majuscules ou minuscules. Veillez n\u00e9anmoins \u00e0 respecter une certaine coh\u00e9rence, ne m\u00e9langez pas les casses (majuscules/minuscules) dans un m\u00eame projet.

    0 1 2 3 4 5 6 7 8 9 A B C D E F\n

    Comme pour les autres bases, l'\u00e9criture peut \u00e9galement \u00eatre d\u00e9compos\u00e9e en puissance successive\u2009:

    \\[ 1AC7_{16} = (1 \\cdot 16^{3} + 10 \\cdot 16^{2} + 12 \\cdot 16^{1} + 7 \\cdot 16^{0})_{10} = 41415_{10} \\]

    La notation hexad\u00e9cimale est tr\u00e8s pratique en \u00e9lectronique et en informatique, car chaque chiffre hexad\u00e9cimal repr\u00e9sente un quadruplet de bits, soit deux caract\u00e8res hexad\u00e9cimaux par octet\u2009:

    0101'1110'0001\u2082 = 5E1\u2081\u2086\n

    Tout ing\u00e9nieur devrait conna\u00eetre par c\u0153ur la correspondance hexad\u00e9cimale de tous les quadruplets aussi bien que ses tables de multiplication (qu'il conna\u00eet d'ailleurs parfaitement, n'est-ce pas\u2009?). La table suivante vous aidera \u00e0 convertir rapidement un nombre hexad\u00e9cimal en d\u00e9cimal\u2009:

    Correspondance binaire, octale, hexad\u00e9cimale Binaire Hexad\u00e9cimal Octal D\u00e9cimal 0b0000 0x0 00 0 0b0001 0x1 01 1 0b0010 0x2 02 2 0b0011 0x3 03 3 0b0100 0x4 04 4 0b0101 0x5 05 5 0b0110 0x6 06 6 0b0111 0x7 07 7 0b1000 0x8 10 8 0b1001 0x9 11 0 0b1010 0xA 12 10 0b1011 0xB 13 11 0b1100 0xC 14 12 0b1101 0xD 15 13 0b1110 0xE 16 14 0b1111 0xF 17 15

    Le fichier albatros.txt (r\u00e9dig\u00e9 avec ed, rappelez-vous) contient un extrait du po\u00e8me de Baudelaire. Un ing\u00e9nieur en proie \u00e0 un bogue li\u00e9 \u00e0 de l'encodage de caract\u00e8re cherche \u00e0 le r\u00e9soudre et utilise le programme hexdump pour lister le contenu hexad\u00e9cimal de son fichier. Il obtient la sortie suivante sur son terminal\u2009:

    $ hexdump -C albatros.txt\n0000  53 6f 75 76 65 6e 74 2c  20 70 6f 75 72 20 73 27  |Souvent, pour s'|\n0010  61 6d 75 73 65 72 2c 20  6c 65 73 20 68 6f 6d 6d  |amuser, les homm|\n0020  65 73 20 64 27 c3 a9 71  75 69 70 61 67 65 0d 0a  |es d'..quipage..|\n0030  50 72 65 6e 6e 65 6e 74  20 64 65 73 20 61 6c 62  |Prennent des alb|\n0040  61 74 72 6f 73 2c 20 76  61 73 74 65 73 20 6f 69  |atros, vastes oi|\n0050  73 65 61 75 78 20 64 65  73 20 6d 65 72 73 2c 0d  |seaux des mers,.|\n0060  0a 51 75 69 20 73 75 69  76 65 6e 74 2c 20 69 6e  |.Qui suivent, in|\n0070  64 6f 6c 65 6e 74 73 20  63 6f 6d 70 61 67 6e 6f  |dolents compagno|\n0080  6e 73 20 64 65 20 76 6f  79 61 67 65 2c 0d 0a 4c  |ns de voyage,..L|\n0090  65 20 6e 61 76 69 72 65  20 67 6c 69 73 73 61 6e  |e navire glissan|\n00a0  74 20 73 75 72 20 6c 65  73 20 67 6f 75 66 66 72  |t sur les gouffr|\n00b0  65 73 20 61 6d 65 72 73  2e 0d 0a 0d 0a 2e 2e 2e  |es amers........|\n00c0  0d 0a 0d 0a 43 65 20 76  6f 79 61 67 65 75 72 20  |....Ce voyageur |\n00d0  61 69 6c 65 cc 81 2c 20  63 6f 6d 6d 65 20 69 6c  |aile.., comme il|\n00e0  20 65 73 74 20 67 61 75  63 68 65 20 65 74 20 76  | est gauche et v|\n00f0  65 75 6c 65 e2 80 af 21  0d 0a 4c 75 69 2c 20 6e  |eule...!..Lui, n|\n0100  61 67 75 c3 a8 72 65 20  73 69 20 62 65 61 75 2c  |agu..re si beau,|\n0110  20 71 75 27 69 6c 20 65  73 74 20 63 6f 6d 69 71  | qu'il est comiq|\n0120  75 65 20 65 74 20 6c 61  69 64 e2 80 af 21 0d 0a  |ue et laid...!..|\n0130  4c 27 75 6e 20 61 67 61  63 65 20 73 6f 6e 20 62  |L'un agace son b|\n0140  65 63 20 61 76 65 63 20  75 6e 20 62 72 c3 bb 6c  |ec avec un br..l|\n0150  65 2d 67 75 65 75 6c 65  2c 0d 0a 4c 27 61 75 74  |e-gueule,..L'aut|\n0160  72 65 20 6d 69 6d 65 2c  20 65 6e 20 62 6f 69 74  |re mime, en boit|\n0170  61 6e 74 2c 20 6c 27 69  6e 66 69 72 6d 65 20 71  |ant, l'infirme q|\n0180  75 69 20 76 6f 6c 61 69  74 e2 80 af 21           |ui volait...!|\n018d\n

    Il lit \u00e0 gauche sur la premi\u00e8re colonne l'offset m\u00e9moire de chaque ligne, au milieu le contenu hexad\u00e9cimal, chaque caract\u00e8re encod\u00e9 sur 8 bits \u00e9tant symbolis\u00e9s par deux caract\u00e8res hexad\u00e9cimaux, et \u00e0 droite le texte o\u00f9 chaque caract\u00e8re non imprimable est remplac\u00e9 par un point. On observe notamment ici que\u2009:

    • \u00e9 de \u00e9quipage est encod\u00e9 avec \\xc3\\xa9 ce qui est le caract\u00e8re Unicode 0065
    • \u00e9 de ail\u00e9 est encod\u00e9 avec e\\xcc\\x81, soit le caract\u00e8re e suivi du diacritique \u00b4 0301
    • Une espace fine ins\u00e9cable \\xe2\\x80\\xaf est utilis\u00e9e avant les !, ce qui est le caract\u00e8re Unicode 202F, conform\u00e9ment \u00e0 la recommandation de l'Acad\u00e9mie fran\u00e7aise.

    Ce fichier est donc encod\u00e9 en UTF-8 quant au bogue de notre ami ing\u00e9nieur, il concerne tr\u00e8s probablement les deux mani\u00e8res distinctes utilis\u00e9es pour encoder le \u00e9. La saisie du po\u00e8me manque donc de coh\u00e9rence et le diable est dans les d\u00e9tails.

    Par cet exercice, on observe n\u00e9anmoins l'\u00e9l\u00e9gance de l'encodage hexad\u00e9cimal qui permet de visualiser facilement, par groupe de 8 bits, le contenu du fichier, ce qui aurait \u00e9t\u00e9 beaucoup moins \u00e9vident en binaire.

    Exercice 4\u2009: Les chiffres hexad\u00e9cimaux

    Calculez la valeur d\u00e9cimale des nombres suivants et donnez le d\u00e9tail du calcul\u2009:

    0xaaaa\n0b1100101\n0x1010\n129\n0216\n
    Solution
    0xaaaa    \u2261 43690\n0b1100101 \u2261   101\n0x1010    \u2261  4112\n129       \u2261   129 (n'est-ce pas ?)\n0216      \u2261   142\n

    Exercice 5\u2009: Albatros

    Tentez de r\u00e9cup\u00e9rer vous m\u00eame le po\u00e8me l'Albatros de Baudelaire et d'afficher le m\u00eame r\u00e9sultat que ci-dessus depuis un terminal de commande Linux.

    $ wget https://.../albatros.txt\n$ hexdump -C albatros.txt\n

    Si vous n'avez pas les outils wget ou hexdump, tentez de les installer ia la commande apt-get install wget hexdump sous Ubuntu.

    ", "tags": ["albatros.txt", "wget", "hexdump"]}, {"location": "course-c/10-numeration/bases/#conversions-de-bases", "title": "Conversions de bases", "text": "

    La conversion d'une base quelconque en syst\u00e8me d\u00e9cimal utilise la relation suivante\u2009:

    \\[ \\sum_{i=0}^{n-1} h_i\\cdot b^i \\]

    o\u00f9\u2009:

    \\(n\\)

    Le nombre de chiffres (ou positions)

    \\(b\\)

    La base du syst\u00e8me d'entr\u00e9e (ou nombre de symboles)

    \\(h_i\\)

    La valeur du chiffre \u00e0 la position \\(i\\)

    Ainsi, la valeur AP7 exprim\u00e9e en base tritrigesimale (base 33) et utilis\u00e9e pour repr\u00e9senter les plaques des v\u00e9hicules \u00e0 Hong Kong peut se convertir en d\u00e9cimales apr\u00e8s avoir pris connaissance de la correspondance d'un symbole tritrigesimal vers le syst\u00e8me d\u00e9cimal\u2009:

    Tritrigesimal -> D\u00e9cimal :\n\n 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F\n 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15\n\n G  H  I  K  L  M  N  P  R  S  T  U  V  W  X  Y  Z\n16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32\n\nConversion :\n\nAP7 -> 10 * pow(33, 2) + 23 * pow(33, 1) + 7 * pow(33, 0) -> 11'656\n

    La conversion d'une grandeur d\u00e9cimale vers une base quelconque est malheureusement plus compliqu\u00e9e et n\u00e9cessite d'appliquer un algorithme.

    La conversion d'un nombre du syst\u00e8me d\u00e9cimal au syst\u00e8me binaire s'effectue simplement par une suite de divisions pour lesquelles on notera le reste.

    Pour chaque division par 2, on note le reste et tant que le quotient n'est pas nul, on it\u00e8re l'op\u00e9ration. Le r\u00e9sultat en binaire est la suite des restes lus dans le sens inverse\u2009:

    n = 209\n\n209 / 2 == 104, 209 % 2 == 1  ^ sens de lecture des restes\n104 / 2 ==  52, 104 % 2 == 0  |\n 52 / 2 ==  26,  52 % 2 == 0  |\n 26 / 2 ==  13,  26 % 2 == 0  |\n 13 / 2 ==   6,  13 % 2 == 1  |\n  6 / 2 ==   3,   6 % 2 == 0  |\n  3 / 2 ==   1,   3 % 2 == 1  |\n  1 / 2 ==   0,   1 % 2 == 1  |\n\n209 == 0b11010001\n

    Exercice 6\u2009: La num\u00e9ration Shadock

    Les Shadocks

    Les Shadocks ne connaissent que quatre mots\u2009: GA, BU, ZO, MEU. La vid\u00e9o \u00e9ducative comment compter comme les Shadocks en explique le principe. Ils utilisent par cons\u00e9quent une base quaternaire.

    Convertir \u2212\u2a3c\u25cb\u25ff\u25cb (BU ZO GA MEU GA) en d\u00e9cimal.

    Solution

    Le syst\u00e8me Shadock est un syst\u00e8me quaternaire similaire au syst\u00e8me du g\u00e9nome humain bas\u00e9 sur quatre bases nucl\u00e9iques. Assignons donc aux symboles Shadocks les symboles du syst\u00e8me indo-arabe que nous connaissons mieux\u2009:

    0 \u25cb (GA)\n1 \u2212 (BU)\n2 \u2a3c (ZO)\n3 \u25ff (MEU)\n

    Le nombre d'entr\u00e9e \u2212\u2a3cO\u25ffO peut ainsi s'exprimer\u2009:

    \u2212\u2a3c\u25cb\u25ff\u25cb \u2261 12030\u2084\n

    En appliquant la m\u00e9thode du cours, on obtient\u2009:

    \\[ 1 \\cdot 4^4 + 2 \\cdot 4^3 + 0 \\cdot 4^2 + 3 \\cdot 4^1 + 0 \\cdot 4^0 = 396_{10} \\]

    Notons que depuis un terminal Python vous pouvez simplement utiliser\u2009:

    int(\"12030\", 4)\n
    ", "tags": ["base-tritrigesimale", "MEU", "AP7"]}, {"location": "course-c/10-numeration/bases/#autres-bases", "title": "Autres bases", "text": "

    Une autre base couramment utilis\u00e9e est la base64, qui utilise les 26 lettres de l'alphabet latin (majuscules et minuscules), les 10 chiffres et deux symboles additionnels. Cette base est souvent utilis\u00e9e pour encoder des donn\u00e9es binaires en ASCII, par exemple pour les pi\u00e8ces jointes des courriels.

    Elle n'est pas \u00e0 proprement parler une base fondamentale, mais plut\u00f4t une m\u00e9thode de codage qui utilise 64 caract\u00e8res imprimables.

    On peut transmettre de l'information en binaire, mais cela implique de pouvoir g\u00e9rer un contenu arbitraire qui n'est pas toujours \u00e9vident dans des environnements pr\u00e9vus pour des caract\u00e8res imprimables. On pourrait se dire qu'on utilise la repr\u00e9sentation ASCII des caract\u00e8res, mais de nombreux caract\u00e8res ne sont pas imprimables. La base64 est une solution \u00e9l\u00e9gante pour encoder des donn\u00e9es binaires en ASCII.

    Prenons l'exemple de la phrase suivante\u2009:

    La fleur en bouquet f\u00e2ne... et jamais ne renait !\n

    Si l'on affiche le contenu hexad\u00e9cimal de cette phrase, on obtient\u2009:

    $ echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait !'  | hexdump -C\n0000  4c 61 20 66 6c 65 75 72  20 65 6e 20 62 6f 75 71  |La fleur en bouq|\n0010  75 65 74 20 66 c3 a2 6e  65 2e 2e 2e 20 65 74 20  |uet f..ne... et |\n0020  6a 61 6d 61 69 73 20 6e  65 20 72 65 6e 61 69 74  |jamais ne renait|\n0030  20 21                                             | !|\n0032\n

    En base64, le message est d\u00e9coup\u00e9 en mot de 6 bits, soit 64 valeurs possibles. Chaque mot de 6 bits est ensuite converti en un caract\u00e8re ASCII avec la table de codage suivante\u2009:

    0  000000 A    17 010001 R    34 100010 i    51 110011 z\n1  000001 B    18 010010 S    35 100011 j    52 110100 0\n2  000010 C    19 010011 T    36 100100 k    53 110101 1\n3  000011 D    20 010100 U    37 100101 l    54 110110 2\n4  000100 E    21 010101 V    38 100110 m    55 110111 3\n5  000101 F    22 010110 W    39 100111 n    56 111000 4\n6  000110 G    23 010111 X    40 101000 o    57 111001 5\n7  000111 H    24 011000 Y    41 101001 p    58 111010 6\n8  001000 I    25 011001 Z    42 101010 q    59 111011 7\n9  001001 J    26 011010 a    43 101011 r    60 111100 8\n10 001010 K    27 011011 b    44 101100 s    61 111101 9\n11 001011 L    28 011100 c    45 101101 t    62 111110 +\n12 001100 M    29 011101 d    46 101110 u    63 111111 /\n13 001101 N    30 011110 e    47 101111 v\n14 001110 O    31 011111 f    48 110000 w    (compl\u00e9ment) =\n15 001111 P    32 100000 g    49 110001 x\n16 010000 Q    33 100001 h    50 110010 y\n

    Ainsi le message commence par 4c612 ou en binaire 01001100 01100001 0010. D\u00e9coup\u00e9 en paquet de 6 bits 010011 000110 000100 10, on utilise selon la table de codage TGE. Comme il s'agit d'un encodage courant, il existe des outils pour le faire automatiquement\u2009:

    echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait !' | base64\nTGEgZmxldXIgZW4gYm91cXVldCBmw6JuZS4uLiBldCBqYW1haXMgbmUgcmVuYWl0ICE=\n

    Si le message n'est pas un multiple de \\(4\\times 6\\) bits, il est compl\u00e9t\u00e9 avec des z\u00e9ros et le caract\u00e8re = est ajout\u00e9 \u00e0 la fin du message pour indiquer le nombre de z\u00e9ros ajout\u00e9s. Notre message \u00e0 une longueur de 50 caract\u00e8res, ou bien 400 bits, qui n'est pas divisible par 24. On compl\u00e8te donc avec =.

    echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait'  | wc -c\n50\n
    ", "tags": ["base64", "TGE", "ascii"]}, {"location": "course-c/10-numeration/data/", "title": "L'information", "text": ""}, {"location": "course-c/10-numeration/data/#linformation", "title": "L'information", "text": "L'informatique ne concerne pas plus les ordinateurs que l'astronomie ne concerne les t\u00e9lescopes... La science ne concerne pas les outils. Elle concerne la mani\u00e8re dont nous les utilisons et ce que nous d\u00e9couvrons en les utilisant.Edsger W. Dijkstra

    L'information est au c\u0153ur de l'informatique. Elle est stock\u00e9e, transmise, trait\u00e9e, et transform\u00e9e par les ordinateurs. Comprendre comment l'information est repr\u00e9sent\u00e9e et manipul\u00e9e est essentiel pour tout d\u00e9veloppeur logiciel. Nous verrons que l'information d\u00e9signe non seulement un message, mais \u00e9galement les symboles utilis\u00e9s pour l'\u00e9crire.

    "}, {"location": "course-c/10-numeration/data/#quantite-dinformation-bit", "title": "Quantit\u00e9 d'information (bit)", "text": "

    Un bit est l'unit\u00e9 d'information fondamentale qui ne peut prendre que deux \u00e9tats\u2009: 1 ou 0. En \u00e9lectronique, cette information peut \u00eatre stock\u00e9e dans un \u00e9l\u00e9ment m\u00e9moire par une charge \u00e9lectrique. Dans le monde r\u00e9el, on peut stocker un bit avec une pi\u00e8ce de monnaie d\u00e9pos\u00e9e sur le c\u00f4t\u00e9 pile ou face. La combinaison de plusieurs bits permet de former des messages plus complexes.

    Le bit est l'abr\u00e9viation de binary digit (chiffre binaire) et il est central \u00e0 la th\u00e9orie de l'information. C'est un concept a \u00e9t\u00e9 popularis\u00e9 par Claude Shannon dans son article fondateur de la th\u00e9orie de l'information en 1948\u2009: A Mathematical Theory of Communication. Shannon y introduit le bit comme unit\u00e9 fondamentale de mesure de l'information.

    S'il existe un meuble avec huit casiers assez grands pour une pomme, et que l'on souhaite conna\u00eetre le nombre de possibilit\u00e9s de rangement, on sait que chaque casier peut contenir soit une pomme, soit aucune. Le nombre de possibilit\u00e9s d'agencer des pommes dans ce meuble est de \\(2^8 = 256\\). \\(2\\) repr\u00e9sente le nombre d'\u00e9tat que peut prendre un casier (pomme ou pas pomme) et \\(8\\) est le nombre de casiers. On d\u00e9finit que la quantit\u00e9 d'information n\u00e9cessaire \u00e0 conna\u00eetre l'\u00e9tat du meuble est de 8 bits.

    On pourrait tr\u00e8s bien utiliser ce meuble et ces pommes pour repr\u00e9senter son \u00e2ge. Un individu de 42 ans n'aurait pas besoin de 42 pommes, mais seulement de 3. En effet, si on repr\u00e9sente l'absence de pomme par 0 et la pr\u00e9sence d'une pomme par 1, on peut repr\u00e9senter l'\u00e2ge de 42 ans par\u2009:

    0 0 1 0 1 0 1 0\n

    De fa\u00e7on analogue, si l'on souhaite repr\u00e9senter l'\u00e9tat d'un meuble beaucoup plus grand, par exemple un meuble de 64 casiers, la quantit\u00e9 d'information repr\u00e9sentable serait de\u2009:

    \\[2^{64} = 18'446'744'073'709'551'616\\]

    ou 64 bits. Cela permet de repr\u00e9senter le nombre de grains de sable sur Terre, le nombre de secondes dans 584'942 ann\u00e9es, ou le nombre de combinaisons possibles pour un mot de passe de 8 caract\u00e8res. Alternativement, si on admet qu'un individu peut vivre jusqu'\u00e0 127 ans maximum, 7 bits suffisent \u00e0 repr\u00e9senter l'\u00e2ge d'une personne. Avec 64 pommes maximum, on peut dont donc repr\u00e9senter l'\u00e2ge d'environ 9 personnes.

    Ce nombre peu importe la mani\u00e8re dont il est interpr\u00e9t\u00e9 repr\u00e9sente une certaine quantit\u00e9 d'information qui peut s'exprimer par la formule g\u00e9n\u00e9rale suivante\u2009:

    \\[I = \\log_2(N)\\]

    o\u00f9 \\(I\\) est la quantit\u00e9 d'information en bits, et \\(N\\) est le nombre de possibilit\u00e9s.

    Les informaticiens ont l'habitude d'agencer les bits par groupe de 8 pour former ce que l'on appelle un octet. Un octet peut donc repr\u00e9senter \\(256\\) valeurs diff\u00e9rentes. Un octet est souvent appel\u00e9 un byte en anglais, mais ce terme reste ambigu, car il peut \u00e9galement d\u00e9signer un groupe de bits de taille variable. Historiquement les ordinateurs ont utilis\u00e9 des bytes de 6, 7, ou 8 bits, mais aujourd'hui l'octet est \u00e9quivalent au byte.

    Lorsque vous achetez un disque de stockage pour votre ordinateur, vous pouvez par exemple lire sur l'emballage que l'unit\u00e9 de stockage dispose d'une capacit\u00e9 de 1 Tio (T\u00e9bi-octet). Un T\u00e9bi-octet est \u00e9gal \u00e0 \\(2^{40}\\) octets, soit \\(1'099'511'627'776\\) octets. Un octet \u00e9tant \u00e9gal \u00e0 8 bits, donc un t\u00e9bi (millier de milliards) d'octet est \u00e9gal \u00e0 \\(8'796'093'022'208\\) bits. \u00c0 titre d'information l'enti\u00e8ret\u00e9 d'encyclop\u00e9die libre Wikip\u00e9dia en p\u00e8se environ 22 Go (Giga-octet). On peut affirmer que notre disque de 1 Tio, achet\u00e9 environ 50 dollars, permettrait de stocker 45 copies de Wikip\u00e9dia.

    Pour repr\u00e9senter l'\u00e9tat de Wikip\u00e9dia, il suffirait donc d'avoir \\(10'225'593'776'312\\) pommes et bien entendu de l'armoire idoine.

    Exercice 1\u2009: Pile ou face

    Lors d'un tir \u00e0 pile ou face de l'engagement d'un match de football, l'arbitre lance une pi\u00e8ce de monnaie qu'il rattrape et d\u00e9pose sur l'envers de sa main. Lorsqu'il annonce le r\u00e9sultat de ce tir, quelle quantit\u00e9 d'information transmet-il\u2009?

    Solution

    Il transmet un seul 1 bit d'information\u2009: \u00e9quipe A ou pile ou 1, \u00e9quipe B ou face ou 0. Il faut n\u00e9anmoins encore d\u00e9finir \u00e0 quoi correspond cette information.

    Entropie

    On entends souvent que l'entropie est la mesure du d\u00e9sordre d'un syst\u00e8me. En thermodynamique, l'entropie est une mesure de l'\u00e9nergie non disponible. En informatique, l'entropie est une mesure de l'incertitude d'une information. Plus une information est incertaine, plus elle contient d'entropie. L'entropie est souvent mesur\u00e9e en bits, et est utilis\u00e9e en cryptographie pour mesurer la qualit\u00e9 d'un g\u00e9n\u00e9rateur de nombres al\u00e9atoires.

    N\u00e9anmoins l'entropie peut \u00e9galement \u00eatre utilis\u00e9e pour mesurer la quantit\u00e9 d'information transmise par un message. Plus un message est incertain, plus il contient d'entropie. Par exemple, si un message est compos\u00e9 de 8 bits, il contient 8 bits d'entropie. Si le message est compos\u00e9 de 16 bits, il contient 16 bits d'entropie.

    ", "tags": ["claude-shannon", "octet", "entropie", "chiffre"]}, {"location": "course-c/10-numeration/data/#les-prefixes", "title": "Les pr\u00e9fixes", "text": "

    Comme \u00e9voqu\u00e9, le nombre de bits peut devenir rapidement colossal, m\u00eame divis\u00e9 par 8 pour obtenir un nombre d'octets. Il est difficile avec des nombres simples de repr\u00e9senter ces quantit\u00e9s et surtout de se les repr\u00e9senter. C'est pourquoi on utilise des pr\u00e9fixes. Un agriculteur parle de tonnes de bl\u00e9s, d'hectares de terres, un informaticien parle de giga de m\u00e9moire, de mega de donn\u00e9es, ou de kilo de bits par seconde dans un d\u00e9bit de connexion.

    Avec le syst\u00e8me international d'unit\u00e9s, nous utilisons tous des pr\u00e9fixes pour exprimer des multiples de dix. Par exemple, un kilogramme est \u00e9gal \u00e0 1000 grammes. De la m\u00eame mani\u00e8re, une tonne est \u00e9gale \u00e0 1000 kilogrammes et un hectare est \u00e9gal \u00e0 10'000 m\u00e8tres carr\u00e9s.

    L'informatique qui s'appuie sur l'unit\u00e9 fondamentale d'information en utilisant le syst\u00e8me binaire en puissance de deux, l'op\u00e9ration de rajouter un bit \u00e0 une quantit\u00e9 d'information double cette derni\u00e8re. On pr\u00e9f\u00e8rera donc des pr\u00e9fixes qui sont des multiples de 2. Or, par d\u00e9finition, un kilo-octet est \u00e9gal \u00e0 1000 octets \\(10^3\\). Le kibi-octet en revanche est \u00e9gal \u00e0 1024 octets \\(2^10\\). Les pr\u00e9fixes binaires sont normalis\u00e9s et d\u00e9finis par l'IEC (International Electrotechnical Commission) Voici un tableau des pr\u00e9fixes les plus courants\u2009:

    Pr\u00e9fixes standardsPr\u00e9fixes binaires Pr\u00e9fixes standards Pr\u00e9fixe Symbole \\(10^n\\) Kilo K \\(10^3\\) M\u00e9ga M \\(10^6\\) Giga G \\(10^9\\) T\u00e9ra T \\(10^{12}\\) Peta P \\(10^{15}\\) Exa E \\(10^{18}\\) Zetta Z \\(10^{21}\\) Yotta Y \\(10^{24}\\) Pr\u00e9fixes binaires Pr\u00e9fixe Symbole \\(2^{10n}\\) Kibi Ki \\(2^{10}\\) M\u00e9bi Mi \\(2^{20}\\) Gibi Gi \\(2^{30}\\) T\u00e9bi Ti \\(2^{40}\\) P\u00e9bi Pi \\(2^{50}\\) Exbi Ei \\(2^{60}\\) Zebi Zi \\(2^{70}\\) Yobi Yi \\(2^{80}\\)

    Info

    Les pr\u00e9fixes binaires restent m\u00e9connus et peu utilis\u00e9s. Les disques durs sont souvent vendus en Go (Giga-octets) alors que les syst\u00e8mes d'exploitation les affichent en Gio (Gibi-octets). Il est donc important de bien comprendre la diff\u00e9rence entre ces deux unit\u00e9s et de les utiliser correctement pour \u00e9viter toute confusion.

    ", "tags": ["prefixes-binaires"]}, {"location": "course-c/10-numeration/data/#notation-positionnelle", "title": "Notation positionnelle", "text": "

    La num\u00e9ration est la science de la repr\u00e9sentation des nombres. La num\u00e9ration d\u00e9cimale est un syst\u00e8me de base 10, c'est-\u00e0-dire que chaque chiffre peut prendre 10 valeurs diff\u00e9rentes\u2009: \\(0, 1, 2, 3, 4, 5, 6, 7, 8, 9\\). La position des chiffres dans un nombre d\u00e9cimal indique la puissance de 10 \u00e0 laquelle il est multipli\u00e9. Par exemple, le nombre 123 est \u00e9gal \u00e0\u2009:

    \\[1 \\times 10^2 + 2 \\times 10^1 + 3 \\times 10^0\\]

    On parle ici de notation positionnelle, car la position des chiffres est importante, comme nos pommes dans nos casiers. Le chiffre le plus \u00e0 droite est le chiffre des unit\u00e9s, le chiffre \u00e0 sa gauche est le chiffre des dizaines, puis des centaines, etc. Cela peut vous sembler d'une grande trivialit\u00e9, car notre civilisation moderne y est familiaris\u00e9e depuis des si\u00e8cles. N\u00e9anmoins, les syst\u00e8mes de num\u00e9ration les plus anciens, comme ceux bas\u00e9s sur les os d'Ishango (datant d'environ 20'000 ans avant notre \u00e8re), n'utilisaient pas ce concept. Ces syst\u00e8mes se contentaient souvent de repr\u00e9senter des quantit\u00e9s par des marques ou des symboles sans utiliser la position pour indiquer des valeurs diff\u00e9rentes.

    La v\u00e9ritable apparition de la notation positionnelle est attribu\u00e9e aux math\u00e9maticiens indiens, autour du 5^e si\u00e8cle de notre \u00e8re. Le syst\u00e8me de num\u00e9ration indien utilisait dix symboles (\\(0\\) \u00e0 \\(9\\)), et la position de chaque chiffre dans un nombre indiquait sa valeur multiplicative par une puissance de dix. Ce syst\u00e8me a \u00e9t\u00e9 r\u00e9volutionnaire, car il simplifiait grandement les calculs, rendant les op\u00e9rations arithm\u00e9tiques plus efficaces.

    Ce syst\u00e8me indien a ensuite \u00e9t\u00e9 transmis aux Arabes, qui l'ont adopt\u00e9 et perfectionn\u00e9 avant de le diffuser en Europe au cours du Moyen \u00c2ge. C'est ce syst\u00e8me, connu aujourd'hui sous le nom de \u00ab\u2009syst\u00e8me d\u00e9cimal\u2009\u00bb ou \u00ab\u2009syst\u00e8me indo-arabe\u2009\u00bb, qui est \u00e0 la base de la notation positionnelle utilis\u00e9e universellement de nos jours.

    Le choix du nombre de symboles est bien entendu arbitraire. On pourrait utiliser deux, trois ou cinquante symboles diff\u00e9rents pour autant que la position de ces symboles indique la valeur multiplicative par une puissance \u00e9quivalente au nombre de symboles. Ce concept c'est ce que nous appellerons la base du syst\u00e8me de num\u00e9ration.

    En informatique, nous utilisons deux symboles et donc une base de deux nomm\u00e9e base binaire. En binaire on nomme LSB (Least Significant Bit) le bit de poids faible et MSB (Most Significant Bit) le bit de poids fort. Le bit de poids faible est le bit le plus \u00e0 droite, et le bit de poids fort est le bit le plus \u00e0 gauche. Il est remarquable de noter que le LSB permet de savoir si le nombre est pair ou impair, si le LSB est \u00e0 0, le nombre est pair, et s'il est \u00e0 1, le nombre est impair\u2009:

    bool is_even(int n) {\n    return n & 1 == 0;\n}\n

    Le MSB quant \u00e0 lui permet de savoir si le nombre est positif ou n\u00e9gatif dans un nombre sign\u00e9 utilisant le compl\u00e9ment \u00e0 deux. Si le MSB est \u00e0 0, le nombre est positif, et s'il est \u00e0 1, le nombre est n\u00e9gatif (on pr\u00e9f\u00e8rera plut\u00f4t utiliser n < 0 pour v\u00e9rifier si un nombre est n\u00e9gatif).

    bool is_negative(int32_t n) {\n    return n & 0x80000000 == 0x80000000;\n}\n

    Exercice 2\u2009: Nature de ces nombres\u2009?

    Pour les nombres suivants stock\u00e9s sur 8-bit, pouvez-vous dire s'ils sont pairs ou impairs, positifs ou n\u00e9gatifs\u2009?

    1. 0b01100000 est et de signe
    2. 0b00001001 est et de signe
    3. 0b10000000 est et de signe
    4. 0b11011011 est et de signe

    Submit

    ", "tags": ["least-significant-bit", "most-significant-bit", "base-10"]}, {"location": "course-c/10-numeration/data/#codification-de-linformation", "title": "Codification de l'information", "text": "

    On a vu plus haut que les nombres en informatiques sont stock\u00e9s sous forme de bits agenc\u00e9s en octets et dont l'ordre est important. Cette m\u00e9thode permet de repr\u00e9senter des nombres entiers positifs, mais pour repr\u00e9senter les nombres n\u00e9gatifs ou les nombres \u00e0 virgule, il faut utiliser des m\u00e9thodes de codification sp\u00e9cifiques.

    La norme IEEE 754 est utilis\u00e9e pour repr\u00e9senter les nombres \u00e0 virgule et le compl\u00e9ment \u00e0 deux pour repr\u00e9senter les nombres n\u00e9gatifs. Ces deux m\u00e9thodes sur lesquels nous reviendrons plus tard sont essentielles pour comprendre comment les nombres sont stock\u00e9s en m\u00e9moire et comment les op\u00e9rations arithm\u00e9tiques sont effectu\u00e9es. En outre, une succession de bits peut repr\u00e9senter bien plus que des nombres. Ils peuvent repr\u00e9senter du texte, des images ou des programmes, mais in fine tout est stock\u00e9 sous forme de bits, et c'est \u00e0 l'interpr\u00e9tation de ces bits que l'on peut en extraire un contenu utile.

    "}, {"location": "course-c/10-numeration/data/#transmission-de-linformation", "title": "Transmission de l'information", "text": "

    Il est fondamental de comprendre que le stockage de l'information n'acquiert toute sa valeur que lorsque cette information peut \u00eatre efficacement transmise et re\u00e7ue. Ce processus ne se limite pas \u00e0 la simple conservation des donn\u00e9es\u2009: le protocole d'encodage joue un r\u00f4le tout aussi crucial. L'histoire regorge d'exemples o\u00f9 les vestiges des civilisations pass\u00e9es nous ont transmis des messages \u00e9nigmatiques, dont le d\u00e9chiffrement reste incertain. Parmi ces exemples, on peut citer les hi\u00e9roglyphes \u00e9gyptiens, les tablettes cun\u00e9iformes sum\u00e9riennes ou encore les manuscrits de la mer Morte, qui ont \u00e9t\u00e9 partiellement r\u00e9v\u00e9l\u00e9s gr\u00e2ce aux travaux \u00e9rudits de Jean-Fran\u00e7ois Champollion sur la pierre de Rosette, d'Henry Rawlinson sur l'inscription de Behistun, ou encore de William F. Albright sur les manuscrits de Qumr\u00e2n. Toutefois, il subsiste des \u00e9critures anciennes qui demeurent herm\u00e9tiques, comme l'\u00e9criture rongorongo de l'\u00eele de P\u00e2ques. Les khipus, ces cordes nou\u00e9es par les Incas, constituent un autre exemple fascinant d'un syst\u00e8me d'encodage dont le secret nous \u00e9chappe encore.

    L'\u00e9volution des moyens de communication nous permet aujourd'hui de transmettre des informations sur des distances inimaginables \u00e0 des vitesses vertigineuses. Par exemple, la transmission d'un signal entre la Terre et Mars, \u00e0 une distance moyenne d'environ 225 millions de kilom\u00e8tres, prend environ 12,5 minutes. Cette dur\u00e9e, bien que rapide \u00e0 l'\u00e9chelle cosmique, impose des contraintes significatives pour les missions spatiales, obligeant \u00e0 une planification m\u00e9ticuleuse et \u00e0 une anticipation des \u00e9changes. Un autre exemple marquant est la communication avec la sonde Voyager 1, situ\u00e9e actuellement \u00e0 plus de 23 milliards de kilom\u00e8tres de la Terre. Les signaux radio, voyageant \u00e0 la vitesse de la lumi\u00e8re, mettent plus de 21 heures pour atteindre notre plan\u00e8te, illustrant les d\u00e9fis de la transmission \u00e0 travers les vastes \u00e9tendues de l'espace.

    En ce qui concerne la quantit\u00e9 d'informations, l'exp\u00e9rience internationale de mesure utilisant des radiot\u00e9lescopes, telle que l'observation des trous noirs via l'Event Horizon Telescope (EHT), a g\u00e9n\u00e9r\u00e9 des volumes de donn\u00e9es si immenses qu'il a \u00e9t\u00e9 plus rapide de transporter les disques durs contenant des p\u00e9taoctets d'informations par avion que de les transmettre via les r\u00e9seaux de communication. Cette r\u00e9alit\u00e9 t\u00e9moigne des limites actuelles des infrastructures de t\u00e9l\u00e9communication face \u00e0 l'immensit\u00e9 des donn\u00e9es g\u00e9n\u00e9r\u00e9es par les sciences modernes. Aussi, la transmission de l'information est un enjeu majeur, non seulement dans sa capacit\u00e9 \u00e0 franchir les distances, mais aussi dans son aptitude \u00e0 pr\u00e9server et \u00e0 interpr\u00e9ter les donn\u00e9es cod\u00e9es, que ce soit \u00e0 travers le temps ou l'espace.

    ", "tags": ["dechiffrement", "rongorongo", "khipus"]}, {"location": "course-c/10-numeration/data/#perennite-de-linformation", "title": "P\u00e9rennit\u00e9 de l'information", "text": "

    La p\u00e9rennit\u00e9 de l'information repr\u00e9sente un d\u00e9fi tout aussi crucial que sa transmission. En effet, les supports physiques sur lesquels nous conservons nos donn\u00e9es sont souvent fragiles et p\u00e9rissables. Le papier, qui a servi de base \u00e0 la transmission du savoir pendant des si\u00e8cles, s'use et se d\u00e9grade au fil du temps, m\u00eame lorsqu'il est conserv\u00e9 dans des conditions optimales. Les bandes magn\u00e9tiques, autrefois utilis\u00e9es pour stocker de grandes quantit\u00e9s de donn\u00e9es num\u00e9riques, ont une dur\u00e9e de vie limit\u00e9e, avec une d\u00e9t\u00e9rioration progressive de l'information qu'elles contiennent. De m\u00eame, les CD et les DVD, longtemps per\u00e7us comme des solutions de stockage robustes, ont montr\u00e9 leurs limites\u2009: leur surface peut se corroder, entra\u00eenant une perte irr\u00e9m\u00e9diable des donn\u00e9es.

    Consciente de ces limitations, l'humanit\u00e9 a entrepris de consolider ses connaissances en les stockant dans des endroits sp\u00e9cialement con\u00e7us pour r\u00e9sister \u00e0 l'\u00e9preuve du temps. Un des projets les plus ambitieux en la mati\u00e8re est l'Arctic World Archive (AWA), situ\u00e9 dans l'archipel de Svalbard, en Norv\u00e8ge, \u00e0 proximit\u00e9 du Global Seed Vault. L'Arctic World Archive, ouvert en 2017, est une installation s\u00e9curis\u00e9e dans une ancienne mine de charbon, enfouie sous des centaines de m\u00e8tres de perg\u00e9lisol. Ce projet vise \u00e0 pr\u00e9server les donn\u00e9es num\u00e9riques pour des si\u00e8cles, voire des mill\u00e9naires, en utilisant une technologie de stockage sur PiqlFilm, un film photosensible sp\u00e9cialement con\u00e7u pour garantir la durabilit\u00e9 des informations sans n\u00e9cessiter d'\u00e9lectricit\u00e9 ou de maintenance continue.

    Des institutions du monde entier, telles que les Archives nationales du Br\u00e9sil, l'Agence spatiale europ\u00e9enne (ESA) et m\u00eame GitHub, y ont d\u00e9j\u00e0 d\u00e9pos\u00e9 des donn\u00e9es importantes. Par exemple, GitHub a archiv\u00e9 21 t\u00e9raoctets de donn\u00e9es provenant de l'ensemble des d\u00e9p\u00f4ts publics actifs de la plateforme, garantissant ainsi la p\u00e9rennit\u00e9 de pr\u00e9cieuses ressources en code source pour les g\u00e9n\u00e9rations futures (voir GitHub Archive Program). Ainsi, Svalbard, avec l'Arctic World Archive, s'affirme comme un bastion de la conservation des connaissances num\u00e9riques, soulignant l'importance de la pr\u00e9servation des donn\u00e9es dans un monde o\u00f9 la technologie \u00e9volue rapidement, mais o\u00f9 la fiabilit\u00e9 des supports de stockage demeure un enjeu majeur.

    "}, {"location": "course-c/10-numeration/numbers/", "title": "Nombres", "text": "Les nombres gouvernent le monde.Pythagore

    Vous avez tous appris dans votre enfance \u00e0 compter, puis vous avez appris que les nombres se classifient dans des ensembles. Les math\u00e9maticiens ont d\u00e9fini des ensembles de nombres pour lesquels des propri\u00e9t\u00e9s particuli\u00e8res sont v\u00e9rifi\u00e9es\u2009; ces ensembles sont imbriqu\u00e9s les uns dans les autres, et chaque ensemble est un sous-ensemble de l'ensemble suivant. La figure suivante illustre cette hi\u00e9rarchie.

    \\[ \\mathbb{N} \\in \\mathbb{Z} \\in \\mathbb{Q} \\in \\mathbb{R} \\in \\mathbb{C} \\in \\mathbb{H} \\in \\mathbb{O} \\in \\mathbb{S} \\]

    Ensemble des nombres

    Les ensembles de nombres sont\u2009:

    • \\(\\mathbb{N}\\) : ensemble des entiers naturels (0, 1, 2, 3, ...)
    • \\(\\mathbb{Z}\\) : ensemble des entiers relatifs (..., -3, -2, -1, 0, 1, 2, 3, ...)
    • \\(\\mathbb{D}\\) : ensemble des d\u00e9cimaux (-0.1, 0, 0.1, 0.2, 0.3, ...)
    • \\(\\mathbb{Q}\\) : ensemble des rationnels (0, 1, \u00bd, \u2153, \u00bc, ...)
    • \\(\\mathbb{R}\\) : ensemble des r\u00e9els (\\(\\pi\\), \\(\\sqrt{2}\\), ...)
    • \\(\\mathbb{C}\\) : ensemble des complexes (\\(i\\), \\(1 + i\\), ...)
    • \\(\\mathbb{H}\\) : ensemble des quaternions (\\(1 + i + j + k\\), ...)
    • \\(\\mathbb{O}\\) : ensemble des octonions
    • \\(\\mathbb{S}\\) : ensemble des s\u00e9d\u00e9nions

    Quaternions, octonions et s\u00e9d\u00e9nions

    Les quaternions, octonions et s\u00e9d\u00e9nions sont des nombres hypercomplexes qui g\u00e9n\u00e9ralisent les nombres complexes. Ils sont utilis\u00e9s en physique pour d\u00e9crire les rotations dans l'espace.

    Les quaternions sont utilis\u00e9s en informatique pour repr\u00e9senter les rotations en 3D. Les octonions et s\u00e9d\u00e9nions sont des g\u00e9n\u00e9ralisations des quaternions, mais ils sont moins utilis\u00e9s en pratique.

    \u00c0 chaque fois que s'\u00e9loigne du r\u00e9el (et c'est une mani\u00e8re amusante de le dire), on perd des propri\u00e9t\u00e9s int\u00e9ressantes. Les nombres complexes ne sont pas ordonn\u00e9s, les quaternions ne sont pas commutatifs, les octonions ne sont pas associatifs, et les s\u00e9d\u00e9nions ne sont m\u00eame pas alternatifs. Un nombre alternatif est un nombre pour lequel la formule suivante est v\u00e9rifi\u00e9e\u2009:

    \\[ (a \\cdot a) \\cdot b = a \\cdot (a \\cdot b) \\]

    En pratique dans une carri\u00e8re d'ing\u00e9nieur, vous n'aurez jamais \u00e0 manipuler ni des quaternions, ni des octonions ou des s\u00e9d\u00e9nions. Les nombres complexes sont n\u00e9anmoins une extension des nombres r\u00e9els qui sont utilis\u00e9s en physique et en math\u00e9matiques et qui peuvent \u00eatre utilis\u00e9s en C sous certaines conditions.

    Archim\u00e8de disait\u2009: \u0394\u03cc\u03c2 \u03bc\u03bf\u03b9 \u03c0\u1fb6 \u03c3\u03c4\u1ff6 \u03ba\u03b1\u1f76 \u03c4\u1f70\u03bd \u03b3\u1fb6\u03bd \u03ba\u03b9\u03bd\u03ac\u03c3\u03c9 (donnez-moi un point d'appui et je soul\u00e8verai le monde). Le Cr\u00e9ateur, s'il existe, aurait pu dire\u2009: donnez-moi un nombre et je vous construirai un univers\u2009! Bien entendu la quantit\u00e9 d'information dans l'univers est gargantuesque, elle cro\u00eet avec l'entropie et donc avec le temps qui passe, mais \u00e0 sa gen\u00e8se \u00e0 l'origine du temps et de l'espace, il n'est pas impensable que l'univers ait pu \u00eatre cr\u00e9\u00e9 \u00e0 partir d'un nombre. C'est une id\u00e9e qui a \u00e9t\u00e9 explor\u00e9e par Stephen Wolfram dans son livre A New Kind of Science. Cette vision repose sur l'id\u00e9e que l'univers pourrait \u00eatre vu comme une sorte de syst\u00e8me informatique ou algorithmique, o\u00f9 des lois fondamentales simples \u00e9voluent pour produire la diversit\u00e9 des ph\u00e9nom\u00e8nes que nous observons.

    Dans le jeu Minecraft, lorsque vous cr\u00e9ez un monde, vous pouvez utiliser une graine pour g\u00e9n\u00e9rer un monde al\u00e9atoire. Cette graine est un nombre fini qui sert de base \u00e0 l'algorithme de g\u00e9n\u00e9ration de monde. Si vous utilisez la m\u00eame graine, vous obtiendrez le m\u00eame monde. La graine -5584399987456711267 permet par exemple d'obtenir de merveilleux cerisiers en fleurs qui rappelle la saison de Sakura \u00e0 Kyoto. Mais pour que cela fonctionne il vous faut le code source de Minecraft, lui aussi c'est une succession de 0 et de 1, et donc c'est un nombre, lui aussi fini.

    Monde correspondant \u00e0 la graine -5584399987456711267

    Lorsque vous jouez, vos actions g\u00e9n\u00e8rent de l'information qui influence le monde, et donc la quantit\u00e9 d'information dans le monde cro\u00eet avec l'entropie que vous injectez dans le syst\u00e8me. C'est pour cela que plus vous jouez, plus la sauvegarde de votre monde devient grande, mais vous pouvez toujours la repr\u00e9senter aussi avec un nombre fini\u2009: une succession de 0 et de 1.

    \u00c0 noter que les m\u00e9moires des ordinateurs ne sont pas infinies, elles sont limit\u00e9es par la quantit\u00e9 de transistors qui les composent. Il n'est donc pas possible d'y stocker n'importe quel nombre. \\(\\pi\\) ne peut pas \u00eatre stock\u00e9 en m\u00e9moire, mais une approximation de \\(\\pi\\) peut l'\u00eatre. Aussi, l'informatique impose certaines limitations sur les nombres que l'on peut manipuler. Les nombres entiers sont les plus simples \u00e0 manipuler, mais ils sont limit\u00e9s par la taille de la m\u00e9moire et la mani\u00e8re dont on les enregistre en m\u00e9moire. Il est donc utile de se fixer des limites, de d\u00e9finir des bornes en fonction de l'usage que l'on veut en faire. La graine de Minecraft est par exemple un nombre de 64 bits et c'est un nombre entier.

    ", "tags": ["quaternions", "stephen-wolfram", "octonions", "monde", "entiers-relatifs", "sedenions", "archimede", "a-new-kind-of-science", "graine", "minecraft", "sakura", "kyoto", "entiers-naturels", "nombres-hypercomplexes", "nombres-complexes"]}, {"location": "course-c/10-numeration/numbers/#entiers-naturels", "title": "Entiers naturels", "text": "

    En math\u00e9matiques, un entier naturel est un nombre positif ou nul. Chaque nombre \u00e0 un successeur unique et peut s'\u00e9crire avec une suite finie de chiffres en notation d\u00e9cimale positionnelle, et donc sans signe et sans virgule. L'ensemble des entiers naturels est d\u00e9fini de la fa\u00e7on suivante\u2009:

    \\[ \\mathbb{N} = {0, 1, 2, 3, ...} \\]

    Les entiers sont les premiers types de donn\u00e9es manipul\u00e9s par les ordinateurs. Ils sont stock\u00e9s en m\u00e9moire sous forme de bits. En choisissant la taille de stockage des entiers, on d\u00e9termine la plage de valeurs que l'on peut repr\u00e9senter. Un entier de 8 bits peut par exemple repr\u00e9senter \\(2^8 = 256\\) valeurs diff\u00e9rentes, de 0 \u00e0 255. Un entier de 16 bits peut quant \u00e0 lui repr\u00e9senter \\(2^{16} = 65536\\) valeurs diff\u00e9rentes, de 0 \u00e0 65535. \u00c0 chaque bit suppl\u00e9mentaire, on double la plage de valeurs repr\u00e9sentables.

    Exemple

    Le nombre 142 peut s'\u00e9crire sur 8 bits en binaire, avec une notation positionnelle (o\u00f9 les bits sont align\u00e9s par poids d\u00e9croissants) on peut \u00e9crire\u2009:

    \\[ \\begin{array}{cccccccc} 2^7 & 2^6 & 2^5 & 2^4 & 2^3 & 2^2 & 2^1 & 2^0 \\\\ 1 & 0 & 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{array} \\]

    La taille de stockage d'un entier d\u00e9termine donc ses limites. Si cette mani\u00e8re est \u00e9l\u00e9gante, elle ne permet h\u00e9las pas de repr\u00e9senter des valeurs n\u00e9gatives. Pour cela, on aura recours aux entiers relatifs.

    "}, {"location": "course-c/10-numeration/numbers/#entiers-relatifs", "title": "Entiers relatifs", "text": "

    Math\u00e9matiquement un entier relatif appartient \u00e0 l'ensemble \\(\\mathbb{Z}\\):

    \\[ \\mathbb{Z} = {..., -3, -2, -1, 0, 1, 2, 3, ...} \\]

    Vous le savez maintenant, l'interpr\u00e9tation d'une valeur binaire n'est possible qu'en ayant connaissance de son encodage et s'agissant d'entiers, on peut se demander comment stocker des valeurs n\u00e9gatives, car manque une information permettant d'encoder le symbole pour le signe - (ni m\u00eame d'ailleurs +).

    Une approche na\u00efve serait de r\u00e9server une partie de la m\u00e9moire pour des entiers positifs et une autre pour des entiers n\u00e9gatifs et stocker la correspondance binaire/d\u00e9cimale simplement. Un peu comme si vous aviez deux bo\u00eetes chez vous, l'une pour les choses qui se mangent (le frigo) et une pour les choses qui ne se mangent plus (la poubelle).

    L'ennui pour les variables c'est que le contenu peut changer et qu'un nombre n\u00e9gatif pourrait tr\u00e8s bien devenir positif apr\u00e8s un calcul. Il faudrait alors le d\u00e9placer d'une r\u00e9gion m\u00e9moire \u00e0 une autre. Ce n'est donc pas la meilleure m\u00e9thode.

    On pourrait alors renseigner la nature du nombre, c'est-\u00e0-dire son signe avec sa valeur.

    ", "tags": ["nombre-negatif", "encodage"]}, {"location": "course-c/10-numeration/numbers/#bit-de-signe", "title": "Bit de signe", "text": "

    Pourquoi ne pas se r\u00e9server un bit de signe, par exemple le 8e bit de notre nombre de 8 bits, pour indiquer si le nombre est positif ou n\u00e9gatif\u2009? C'est cet exemple qui est montr\u00e9 ici\u2009:

    \u250c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u2502\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = (0 * (-1)) * 0b1010011 = 83\n\u2514\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25021\u2502\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = (1 * (-1)) * 0b1010011 = -83\n\u2514\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Cette m\u00e9thode impose le sacrifice d'un bit et donc l'intervalle repr\u00e9sentable est n'est plus que de [-127..127]. N\u00e9anmoins, elle pr\u00e9sente un autre inconv\u00e9nient majeur\u2009: la repr\u00e9sentation du z\u00e9ro.

    Dans cette repr\u00e9sentation, il existe deux z\u00e9ros\u2009: le z\u00e9ro n\u00e9gatif 0b00000000, et le z\u00e9ro positif 0b10000000 ce qui peut poser des probl\u00e8mes pour les comparaisons. Est-ce que \\(0\\) est \u00e9gal \\(-0\\) ? En un sens oui, mais en termes de l'information stock\u00e9e, ce n'est pas le m\u00eame nombre.

    En termes de calculs, l'addition ne fonctionne plus si on raisonne sur les bits. Car si on additionne au z\u00e9ro positif (0b10000000) la valeur 1 on aura 1, mais si on additionne au z\u00e9ro n\u00e9gatif (0b00000000) la valeur 1 on obtiendra -1 et c'est un peu d\u00e9routant\u2009:

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n\n000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500>  \u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500> M\u00e9thode du bit de signe\n 0     1     2     3     0    -1    -2    -3\n

    Il faudrait donc trouver une m\u00e9thode qui permettrait de conserver la possibilit\u00e9 de faire les op\u00e9rations directement en binaire. En d'autres termes, on souhaiterait pouvoir calculer en base deux sans se soucier du signe\u2009:

      00000010 (2)\n- 00000101 (5)\n----------\n  11111101 (-125)    2 - 5 != -125\n

    Si on r\u00e9sume, la solution propos\u00e9e qui utilise un bit de signe pose deux probl\u00e8mes\u2009:

    1. Les op\u00e9rations ne sont plus triviales, et un algorithme particulier doit \u00eatre mis en place pour les g\u00e9rer.
    2. Le double z\u00e9ro (positif et n\u00e9gatif) est g\u00eanant.
    ", "tags": ["zero", "bit-de-signe", "addition"]}, {"location": "course-c/10-numeration/numbers/#complement-a-un", "title": "Compl\u00e9ment \u00e0 un", "text": "

    Le compl\u00e9ment \u00e0 un est une m\u00e9thode plus maline utilis\u00e9e dans les premiers ordinateurs comme le CDC 6600 (1964) ou le UNIVAC 1107 (1962). Il existe \u00e9galement un bit de signe, mais il est implicite.

    Le compl\u00e9ment \u00e0 un tire son nom de sa d\u00e9finition g\u00e9n\u00e9rique nomm\u00e9e radix-complement ou compl\u00e9ment de base et s'exprime par\u2009:

    \\[ b^n - y \\]

    o\u00f9

    \\(b\\)

    La base du syst\u00e8me positionnel utilis\u00e9

    \\(n\\)

    Le nombre de chiffres maximal du nombre consid\u00e9r\u00e9

    \\(y\\)

    La valeur \u00e0 compl\u00e9menter.

    Ainsi, il est facile d'\u00e9crire le compl\u00e9ment \u00e0 neuf d'un nombre en base dix, car on s'arrange pour que chaque chiffre composant le nombre on trouve un autre chiffre dont la somme est \u00e9gale \u00e0 neuf.

      0 1 2 3 4 5 6 7 8 9\n          |\n          | Compl\u00e9ment \u00e0 9\n          v\n+ 9 8 7 6 5 4 3 2 1 0\n  -------------------\n  9 9 9 9 9 9 9 9 9 9\n

    On notera avec beaucoup d'int\u00e9r\u00eat qu'un calcul est possible avec cette m\u00e9thode. Sur l'exemple suivant, \u00e0 gauche, on montre une soustraction classique, \u00e0 droite on remplace la soustraction par une addition ainsi que les valeurs n\u00e9gatives par leur compl\u00e9ment \u00e0 9. Le r\u00e9sultat 939 correspond apr\u00e8s compl\u00e9ment \u00e0 un \u00e0 60.

      150      150\n- 210    + 789\n-----    -----\n  -60      939\n

    Notons que le cas pr\u00e9cis de l'inversion des chiffres correspond au compl\u00e9ment de la base, moins un. L'inversion des bits binaire est donc le compl\u00e9ment \u00e0 \\((2-1) = 1\\).

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n\n000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500> <\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500 compl\u00e9ment \u00e0 un\n 0     1     2     3    -3    -2    -1     0\n

    Reprenons l'exemple pr\u00e9c\u00e9dent de soustraction, on notera que l'op\u00e9ration fonctionne en soustrayant 1 au r\u00e9sultat du calcul.

      00000010 (2)\n+ 11111010 (-5)\n----------\n  11111101 (-3)\n-        1\n----------\n  11111011 (-4)\n

    Pour r\u00e9sumer les avantages et inconv\u00e9nients du compl\u00e9ment \u00e0 un\u2009:

    1. Les op\u00e9rations redeviennent presque triviales, mais il est n\u00e9cessaire de soustraire 1 au r\u00e9sultat (c'est dommage).
    2. Le double z\u00e9ro (positif et n\u00e9gatif) est g\u00eanant.

    ", "tags": ["1964", "1962", "complement-a-neuf", "complement-a-un"]}, {"location": "course-c/10-numeration/numbers/#complement-a-deux", "title": "Compl\u00e9ment \u00e0 deux", "text": "

    Le compl\u00e9ment \u00e0 deux n'est rien d'autre que le compl\u00e9ment \u00e0 un plus un. C'est donc une amusante plaisanterie des informaticiens. Car dans un syst\u00e8me binaire, le nombre de symboles et de 2 (0 et 1). On ne peut pas trouver un chiffre tel que la somme donne 2. C'est la m\u00eame id\u00e9e que de demander le compl\u00e9ment \u00e0 10 en base 10. Vous ne pouvez pas sur la base d'un chiffre unique obtenir un autre chiffre dont la somme est \u00e9gale \u00e0 10 sans avoir recours \u00e0 un autre chiffre.

    Pour r\u00e9aliser ce compl\u00e9ment \u00e0 deux (compl\u00e9ment \u00e0 un plus un), il y a deux \u00e9tapes\u2009:

    1. Calculer le compl\u00e9ment \u00e0 un du nombre d'entr\u00e9es.
    2. Ajouter 1 au r\u00e9sultat.

    Oui, et alors, en quoi cela change le Schmilblick ? Surprenamment, on r\u00e9sout tous les probl\u00e8mes amen\u00e9s par le compl\u00e9ment \u00e0 un. Prenons les diff\u00e9rentes repr\u00e9sentations que nous avons vues jusqu'\u00e0 pr\u00e9sent\u2009:

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n 0     1     2     3     4     5     6     7     sans compl\u00e9ment\n 0     1     2     3     0    -1    -2    -3     avec bit de signe\n 0     1     2     3    -3    -2    -1     0     compl\u00e9ment \u00e0 un\n 0     1     2     3    -4    -3    -2    -1     compl\u00e9ment \u00e0 deux\n

    On peut \u00e9galement les repr\u00e9senter sous forme d'un cercle comme illustr\u00e9 sur la figure suivante\u2009:

    Cercle des nombres

    Avec le bit de signe, on observe deux ruptures dans la continuit\u00e9 de la repr\u00e9sentation. Un saut de 3,0 et un autre -3,0. Avec le compl\u00e9ment \u00e0 un, on n'observe toujours deux sauts 0,0 et -3,-3. Avec le compl\u00e9ment \u00e0 deux, on n'observe plus qu'un seul saut 3, -4, et la continuit\u00e9 est assur\u00e9e de -1 \u00e0 0. Par ailleurs, le z\u00e9ro n'a plus de double repr\u00e9sentation.

    Au niveau du calcul, l'addition et la soustraction fonctionnent de mani\u00e8re identique. Prenons l'exemple de la soustraction suivante\u2009:

      2        00000010\n- 5      + 11111011   (~0b101 + 1 == 0b11111011)\n---     -----------\n -3        11111101   (~0b11111101 + 1 == 0b11 == 3)\n

    Cette notation est donc tr\u00e8s \u00e9l\u00e9gante, car\u2009:

    1. Les op\u00e9rations sont triviales.
    2. Le probl\u00e8me du double z\u00e9ro est r\u00e9solu.
    3. On gagne une valeur n\u00e9gative [-128..+127] contre [-127..+127] avec les m\u00e9thodes pr\u00e9c\u00e9demment \u00e9tudi\u00e9es.

    Vous l'aurez compris, le compl\u00e9ment \u00e0 deux est bien le m\u00e9canisme de repr\u00e9sentation des nombres n\u00e9gatifs qui a \u00e9t\u00e9 retenu par les informaticiens, et le plus utilis\u00e9 de nos jours dans les ordinateurs. Gardez cependant \u00e0 l'esprit que ces m\u00e9canismes ne sont qu'une interpr\u00e9tation d'un contenu binaire stock\u00e9 en m\u00e9moire.

    ", "tags": ["complement-a-deux"]}, {"location": "course-c/10-numeration/numbers/#les-nombres-reels", "title": "Les nombres r\u00e9els", "text": "

    Math\u00e9matiquement, les nombres r\u00e9els \\(\\mathbb{R}\\), sont des nombres qui peuvent \u00eatre repr\u00e9sent\u00e9s par une partie enti\u00e8re, et une liste finie ou infinie de d\u00e9cimales. En informatique, stocker une liste infinie de d\u00e9cimale demanderait une quantit\u00e9 infinie de m\u00e9moire et donc, la pr\u00e9cision arithm\u00e9tique est contrainte.

    Au d\u00e9but de l'\u00e8re des ordinateurs, il n'\u00e9tait possible de stocker que des nombres entiers, mais le besoin de pouvoir stocker des nombres r\u00e9els s'est rapidement fait sentir et la transition s'est faite progressivement. D'abord par l'apparition de la virgule fixe, puis par la virgule flottante.

    Le premier ordinateur avec une capacit\u00e9 de calcul en virgule flottante date de 1942 (ni vous ni moi n'\u00e9tions probablement n\u00e9s) avec le Zuse's Z4, du nom de son inventeur Konrad Zuse.

    Attardons-nous un peu sur ces concepts de virgule fixe et de virgule flottante.

    ", "tags": ["virgule-flottante", "virgule-fixe"]}, {"location": "course-c/10-numeration/numbers/#virgule-fixe", "title": "Virgule fixe", "text": "

    Pour illustrer notre propos, prenons l'exemple d'un nombre entier exprim\u00e9 sur 8-bits, on peut admettre facilement que bien qu'il s'agisse d'un nombre entier, une virgule pourrait \u00eatre ajout\u00e9e au bit z\u00e9ro sans en modifier sa signification. Dans cet exemple, ajoutons une virgule \u00e0 la position 0\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2^6 + 2^4 + 2^1 + 2^0 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n                , / 2^0     ----> 83 / 1 = 83\n

    Imaginons \u00e0 pr\u00e9sent que nous d\u00e9placions cette virgule virtuelle de trois \u00e9l\u00e9ments sur la gauche. En admettant que deux ing\u00e9nieurs se mettent d'accord pour consid\u00e9rer ce nombre 0b01010011 avec une virgule fixe positionn\u00e9e \u00e0 droite du quatri\u00e8me bit, l'interpr\u00e9tation de cette grandeur serait alors la valeur enti\u00e8re divis\u00e9e par 8 (\\(2^3\\)). On parvient alors \u00e0 exprimer une grandeur r\u00e9elle comportant une partie d\u00e9cimale\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2076 + 2\u2074 + 2\u00b9 + 2\u2070 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n          ,       / 2\u00b3     ----> 83 / 8 = 10.375\n

    Cependant, il manque une information. Un ordinateur, sans yeux et sans bon sens, est incapable sans information additionnelle d'interpr\u00e9ter correctement la position de la virgule puisque sa position n'est encod\u00e9e nulle part dans le nombre. En outre, puisque la position de cette virgule est dans l'intervalle [0..7], il serait possible d'utiliser trois bits suppl\u00e9mentaires \u00e0 cette fin\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2076 + 2\u2074 + 2\u00b9 + 2\u2070 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n          \u250c\u2500\u252c\u2500\u252c\u2500\u2510\n          \u25020\u25021\u25021\u2502 / 2\u00b3     ----> 83 / 8 = 10.375\n          \u2514\u2500\u2534\u2500\u2534\u2500\u2518\n

    Cette solution est \u00e9l\u00e9gante, mais demande \u00e0 pr\u00e9sent 11-bits contre 8-bits initialement. Un ordinateur n'\u00e9tant dou\u00e9 que pour manipuler des paquets de bits souvent sup\u00e9rieurs \u00e0 8, il faudrait soit \u00e9tendre inutilement le nombre de bits utilis\u00e9s pour la position de la virgule \u00e0 8, soit tenter d'int\u00e9grer cette information, dans les 8-bits initiaux.

    "}, {"location": "course-c/10-numeration/numbers/#virgule-flottante", "title": "Virgule flottante", "text": "

    Depuis l'exemple pr\u00e9c\u00e9dent, imaginons que l'on sacrifie 3 bits sur les 8 pour encoder l'information de la position de la virgule. Appelons l'espace r\u00e9serv\u00e9 pour positionner la virgule l' exposant et le reste de l'information la mantisse, qui en math\u00e9matique repr\u00e9sente la partie d\u00e9cimale d'un logarithme (\u00e0 ne pas confondre avec la mantis shrimp, une quille ou crevette-mante boxeuse aux couleurs particuli\u00e8rement chatoyantes).

      exp.  mantisse\n\u251e\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2074 + 2\u00b9 + 2\u2070 = 16 + 2 + 1 = 19\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> / 2\u00b9 ----> 19 / 2 = 9.5\n

    Notre construction nous permet toujours d'exprimer des grandeurs r\u00e9elles, mais avec ce sacrifice, il n'est maintenant plus possible d'exprimer que les grandeurs comprises entre \\(1\\cdot2^{7}=0.0078125\\) et \\(63\\). Ce probl\u00e8me peut \u00eatre ais\u00e9ment r\u00e9solu en augmentant la profondeur m\u00e9moire \u00e0 16 ou 32-bits. Ajoutons par ailleurs que cette solution n'est pas \u00e0 m\u00eame d'exprimer des grandeurs n\u00e9gatives.

    Poursuivons notre raisonnement. Cette fois-ci nous choisissons d'\u00e9tendre notre espace de stockage \u00e0 4 octets. Un bit de signe est r\u00e9serv\u00e9 pour exprimer les grandeurs n\u00e9gatives, 8 bits pour l'exposant et 23 bits pour la mantisse :

     \u250c Signe 1 bit\n \u2502        \u250c Exposant 8 bits\n \u2502        \u2502                             \u250c Mantisse 23 bits\n \u2534 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25020\u25021\u25020\u25020\u25021\u25020\u25020\u25020\u2502\u25021\u25021\u25020\u25021\u25021\u25021\u25021\u25021\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25020\u25021\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Peu \u00e0 peu, nous nous rapprochons du Standard for Floating-Point Arithmetic (IEEE 754). La formule de base est la suivante\u2009:

    \\[ x = s\\cdot b^e\\sum_{k=1}^p f_k\\cdot b^{-k},\\; e_{\\text{min}} \\le e \\le e_{\\text{max}} \\]

    Avec\u2009:

    \\(s\\)

    Signe (\\(\\pm1\\))

    \\(b\\)

    Base de l'exposant, un entier \\(>1\\).

    \\(e\\)

    Exposant, un entier entre \\(e_\\text{min}\\) et \\(e_\\text{max}\\)

    \\(p\\)

    Pr\u00e9cision, nombre de digits en base \\(b\\) de la mantisse

    \\(f_k\\)

    Entier non n\u00e9gatif plus petit que la base \\(b\\).

    \u00c9tant donn\u00e9 que les ordinateurs sont plus \u00e0 l'aise \u00e0 la manipulation d'entr\u00e9es binaire, la base est 2 et la norme IEEE nomme ces nombres binary16, binary32 ou binary64, selon le nombre de bits utilis\u00e9 pour coder l'information. Les termes de Single precision ou Double precision sont aussi couramment utilis\u00e9s.

    Les formats support\u00e9 par un ordinateur ou qu'un microcontr\u00f4leur \u00e9quip\u00e9 d'une unit\u00e9 de calcul en virgule flottante (FPU pour Floating point unit) sont les suivants\u2009:

    Formats de nombres en virgule flottante IEEE-754 Exposant Mantisse Signe binary32 8 bits 23 bits 1 bit binary64 11 bits 52 bits 1 bit

    Il est temps de faire quelques observations\u2009:

    • une valeur encod\u00e9e en virgule flottante sera toujours une approximation d'une grandeur r\u00e9elle\u2009;
    • la pr\u00e9cision est d'autant plus grande que le nombre de bits de la mantisse est grand\u2009;
    • la base ayant \u00e9t\u00e9 fix\u00e9e \u00e0 2, il est possible d'exprimer \\(1/1024\\) sans erreur de pr\u00e9cision, mais pas \\(1/1000\\) ;
    • un ordinateur qui n'est pas \u00e9quip\u00e9 d'une FPU sera beaucoup plus lent (10 \u00e0 100x) pour faire des calculs en virgule flottante\u2009;
    • bien que le standard C99 d\u00e9finisse les types virgule flottante float, double et long double, ils ne d\u00e9finissent pas la pr\u00e9cision avec laquelle ces nombres sont exprim\u00e9s, car cela d\u00e9pend de l'architecture du processeur utilis\u00e9.
    ", "tags": ["binary32", "mantisse", "float", "binary64", "exposant", "double", "binary16"]}, {"location": "course-c/10-numeration/numbers/#simple-precision", "title": "Simple pr\u00e9cision", "text": "

    Le type float aussi dit \u00e0 pr\u00e9cision simple utilise un espace de stockage de 32-bits organis\u00e9 en 1 bit de signe, 8 bits pour l'exposant et 23 bits pour la mantisse. Les valeurs pouvant \u00eatre exprim\u00e9es sont de\u2009:

    • \\(\\pm\\inf\\) lorsque l'exposant vaut 0xff
    • \\((-1)^{\\text{sign}}\\cdot2^{\\text{exp} - 127}\\cdot1.\\text{mantisse}\\)
    • \\(0\\) lorsque la mantisse vaut 0x00000

    Voici quelques exemples. Tout d'abord, la valeur de 1.0 est encod\u00e9e de la mani\u00e8re suivante\u2009:

    \\[ \\begin{aligned} 0\\:01111111\\:00000000000000000000000_2 &= \\text{3f80}\\: \\text{0000}_{16} \\\\ &= (-1)^0 \\cdot 2^{127-127} \\cdot \\frac{(2^{23} + 0)}{2^{23}} \\\\ &= 2^{0} \\cdot 1.0 = 1.0 \\end{aligned} \\]

    La valeur positive maximale exprimable est lorsque l'exposant vaut 0xfe et la mantisse 0x7fffff :

    \\[ \\begin{aligned} 0\\:11111110\\:11111111111111111111111_2 &= \\text{7f7f}\\: \\text{ffff}_{16} \\\\ &= (-1)^0 \\cdot 2^{254-127} \\cdot \\frac{(2^{23} + 838'607)}{2^{23}} \\\\ &\u2248 2^{127} \\cdot 1.9999998807 \\\\ &\u2248 3.4028234664 \\cdot 10^{38} \\end{aligned} \\]

    La valeur de \\(-\\pi\\) (pi) est\u2009:

    \\[ \\begin{aligned} 1\\:10000000\\:10010010000111111011011_2 &= \\text{4049}\\: \\text{0fdb}_{16} \\\\ &= (-1)^1 \\cdot 2^{128-127} \\cdot \\frac{(2^{23} + 4'788'187)}{2^{23}} \\\\ &\u2248 -1 \\cdot 2^{1} \\cdot 1.5707963 \\\\ &\u2248 -3.14159274101 \\end{aligned} \\]

    On peut encore noter quelques valeurs particuli\u00e8res\u2009:

    0 00000000 00000000000000000000000\u2082 \u2261 0000 0000\u2081\u2086 \u2261 0\n0 11111111 00000000000000000000000\u2082 \u2261 7f80 0000\u2081\u2086 \u2261 inf\n1 11111111 00000000000000000000000\u2082 \u2261 ff80 0000\u2081\u2086 \u2261 \u2212inf\n

    D\u00e9passement de capacit\u00e9

    Il ne faut pas oublier que la repr\u00e9sentation des nombres en virgule flottante n'est pas exacte et il est possible de d\u00e9passer la capacit\u00e9 de stockage d'un nombre en virgule flottante. Quant \u00e0 la pr\u00e9cision maximale d'un nombre en virgule flottante, il d\u00e9pend de sa mantisse.

    Par exemple, si l'on souhaite r\u00e9aliser un int\u00e9grateur simple, nous disposons d'un compteur u initialis\u00e9 \u00e0 1.0. \u00c0 chaque it\u00e9ration, on incr\u00e9mente u de 1.0. Lorsque la valeur cesse de cro\u00eetre, on affiche la valeur de u.

    #include <stdio.h>\nint main() {\n    float u = 1.0, v;\n    do { v = u++; } while (u > v);\n    printf(\"%f\\n\", u);\n}\n

    Vous pourriez vous attendre \u00e0 ce que le programme tourne \u00e0 l'infini, o\u00f9 du moins jusqu'\u00e0 une limite tr\u00e8s grande, mais en r\u00e9alit\u00e9, il s'arr\u00eate \u00e0 16777216.0. C'est parce que la pr\u00e9cision de la mantisse est de 23 bits, et que le nombre 16777217.0 est le premier nombre entier qui ne peut pas \u00eatre repr\u00e9sent\u00e9 avec une pr\u00e9cision de 23 bits.

    Les nombres subnormaux

    On l'a vu un nombre en virgule flottante simple pr\u00e9cision s'\u00e9crit sous la forme\u2009:

    \\[ (-1)^s \\times (1.m) \\times 2^{(e - Bias)} \\]

    Les nombres subnormaux sont des nombres qui ne respectent pas la norme IEEE 754, mais qui sont tout de m\u00eame repr\u00e9sentables. Ils sont utilis\u00e9s pour repr\u00e9senter des nombres tr\u00e8s petits, proches de z\u00e9ro. En effet, la norme IEEE 754 impose que le premier bit de la mantisse soit toujours \u00e9gal \u00e0 1, ce qui implique que le nombre 0 ne peut pas \u00eatre repr\u00e9sent\u00e9. Les nombres subnormaux permettent de repr\u00e9senter des nombres tr\u00e8s proches de z\u00e9ro, en diminuant la pr\u00e9cision de la mantisse.

    ", "tags": ["float"]}, {"location": "course-c/10-numeration/numbers/#double-precision", "title": "Double pr\u00e9cision", "text": "

    La double pr\u00e9cision est similaire \u00e0 la simple pr\u00e9cision, mais avec une mantisse \u00e0 52 bits et 11 bits d'exposants. Le nombre est donc repr\u00e9sentable sur 64 bits. La valeur maximale est de \\(1.7976931348623157 \\times 10^{308}\\) et la valeur minimale de \\(2.2250738585072014 \\times 10^{-308}\\). La r\u00e9solution en nombre de chiffres significatifs est de 15 \u00e0 16 chiffres contre 6 \u00e0 7 pour la simple pr\u00e9cision. Cette notation est donc tr\u00e8s pertinente pour les calculs scientifiques, mais elle requiert aussi plus de m\u00e9moire.

    Exercice 1\u2009: Expressions arithm\u00e9tiques flottantes

    Donnez la valeur des expressions ci-dessous\u2009:

    25. + 10. + 7. \u2013 3.\n5. / 2.\n24. + 5. / 2.\n25. / 5. / 2.\n25. / (5. / 2.)\n2. * 13. % 7.\n1.3E30 + 1.\n
    "}, {"location": "course-c/10-numeration/numbers/#quadruple-precision", "title": "Quadruple pr\u00e9cision", "text": "

    Bien que ce soit marginal dans le monde de l'informatique, la quadruple pr\u00e9cision est une norme d\u00e9finie dans IEEE 754 qui utilise 128 bits pour stocker les nombres r\u00e9els. Elle est utilis\u00e9e pour des calculs scientifiques n\u00e9cessitant une tr\u00e8s grande pr\u00e9cision comme au CERN ou pour l'\u00e9tude de mod\u00e8les cosmologiques. La quadruple pr\u00e9cision offre une pr\u00e9cision de 34 chiffres significatifs, soit environ 112 bits de pr\u00e9cision.

    Seul un nombre r\u00e9duit de langages de programmation peut g\u00e9rer nativement cette notation, et la grande majorit\u00e9 des processeurs n'est pas pr\u00e9vue pour les traiter efficacement. Il est n\u00e9anmoins possible de l'utiliser avec certains compilateurs C comme GCC en utilisant le type __float128 de la biblioth\u00e8que <quadmath.h>.

    Lenteurs

    Utiliser la quadruple pr\u00e9cision ralenti consid\u00e9rablement les calculs, car les processeurs actuels ne sont pas optimis\u00e9s pour travailler sur 128 bits. Un processeur peut faire des calculs sur 64 bits en une seule op\u00e9ration, mais pour des calculs en quadruple pr\u00e9cision, l'effort est consid\u00e9rablement plus grand.

    ", "tags": ["ieee-754", "cern", "quadruple-precision", "__float128"]}, {"location": "course-c/10-numeration/numbers/#nombres-complexes", "title": "Nombres complexes", "text": "

    En C, il est possible de d\u00e9finir des nombres complexes en utilisant le type complex de la biblioth\u00e8que <complex.h>. Les nombres complexes sont compos\u00e9s de deux parties, la partie r\u00e9elle et la partie imaginaire. Ils sont souvent utilis\u00e9s en math\u00e9matiques pour repr\u00e9senter des nombres qui ne peuvent pas \u00eatre exprim\u00e9s avec des nombres r\u00e9els. Ils ont \u00e9t\u00e9 introduits avec la version C99 du standard C.

    N\u00e9anmoins les nombres complexes ne sont pas support\u00e9s par les op\u00e9rateurs du langage, il est donc n\u00e9cessaire d'utiliser des fonctions sp\u00e9cifiques pour effectuer des op\u00e9rations complexes.

    Note

    Dans des langages plus haut niveau comme le C++, le C# ou Python, les nombres complexes sont support\u00e9s nativement.

    Exemple en Python\u2009:

    from math import sqrt\na, b, c = 1, 2, 3\ndelta = b**2 - 4*a*c # Calcul du discriminant qui sera n\u00e9gatif\nx1, x1 = (-b + sqrt(delta)) / (2*a), (-b - sqrt(delta)) / (2*a)\n

    x1 et x2 sont des nombres complexes.

    Voici un exemple en C\u2009:

    #include <stdio.h>\n#include <complex.h>\n\nint main() {\n    double complex z1 = 1.0 + 2.0*I;\n    double complex z2 = 3.0 + 4.0*I;\n\n    printf(\"z1 = %.1f + %.1fi\\n\", creal(z1), cimag(z1));\n    printf(\"z2 = %.1f + %.1fi\\n\", creal(z2), cimag(z2));\n\n    double complex sum = z1 + z2;\n    double complex product = z1 * z2;\n\n    printf(\"sum = %.1f + %.1fi\\n\", creal(sum), cimag(sum));\n    printf(\"product = %.1f + %.1fi\\n\", creal(product), cimag(product));\n\n    return 0;\n}\n
    ", "tags": ["complex"]}, {"location": "course-c/10-numeration/numbers/#format-q-virgule-fixe", "title": "Format Q (virgule fixe)", "text": "

    Le format Q est une notation en virgule fixe dans laquelle le format d'un nombre est repr\u00e9sent\u00e9 par la lettre Q suivie de deux nombres\u2009:

    1. Le nombre de bits entiers.
    2. Le nombre de bits fractionnaires.

    Ainsi, un registre 16 bits contenant un nombre allant de +0.999 \u00e0 -1.0 s'exprimera Q1.15 soit 1 + 15 valant 16 bits.

    Pour exprimer la valeur pi (3.1415...) il faudra au minimum 3 bits pour repr\u00e9senter la partie enti\u00e8re, car le bit de signe doit rester \u00e0 z\u00e9ro. Le format sur 16 bits sera ainsi Q4.12.

    La construction de ce nombre est facile\u2009:

    1. Prendre le nombre r\u00e9el \u00e0 encoder (\\(3.1415926535\\))
    2. Le multiplier par 2 \u00e0 la puissance du nombre de bits (\\(2^{12} * 3.1415926535 = 12867.963508736\\))
    3. Prendre la partie enti\u00e8re (\\(12867\\))

    Pour convertir un nombre Q4.12 en sa valeur r\u00e9elle il faut\u2009:

    1. Prendre le nombre encod\u00e9 en Q4.12 (\\(12867\\))
    2. Diviser sa valeur 2 \u00e0 la puissance du nombre de bits (\\(12867 / 2^{12} = 3.141357421875\\))

    On peut noter une perte de pr\u00e9cision puisqu'il n'est pas possible d'encoder un tel nombre dans seulement 16 bits. L'incr\u00e9ment positif minimal serait\u2009: \\(1 / 2^12 = 0.00024\\). Il convient alors d'arrondir le nombre \u00e0 la troisi\u00e8me d\u00e9cimale, soit \\(3.141\\).

    Les op\u00e9rations arithm\u00e9tiques restent triviales entre des nombres de m\u00eames types. Le chapitre sur les algorithmes d\u00e9crit une impl\u00e9mentation de calcul de sinus en utilisant ce format.

    ", "tags": ["virgule-fixe"]}, {"location": "course-c/10-numeration/numbers/#addition", "title": "Addition", "text": "

    L'addition peut se faire avec ou sans saturation\u2009:

    typedef int16_t Q;\ntypedef Q Q12;\n\nQ q_add(Q a, Q b) {\n    return a + b;\n}\n\nQ q_add_sat(Q a, Q b) {\n    int32_t res = (int32_t)a + (int32_t)b;\n    res = res > 0x7FFF ? 0x7FFF : res\n    res = res < -1 * 0x8000 ? -1 * 0x8000 : res;\n    return (Q)res;\n}\n
    "}, {"location": "course-c/10-numeration/numbers/#multiplication", "title": "Multiplication", "text": "

    Soit deux nombres 0.9 et 3.141\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25021\u25021\u25021\u25020\u2502\u25020\u25021\u25021\u25020\u25020\u25021\u25021\u25020\u2502 Q4.12 (0.9) 3686\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25021\u25021\u25020\u25020\u25021\u25020\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25021\u25021\u2502 Q4.12 (3.141) 12867\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Multiplier ces deux valeurs revient \u00e0 une multiplication sur 2 fois la taille. Le r\u00e9sultat doit \u00eatre obtenu sur 32-bits sachant que les nombres Q s'additionnent comme Q4.12 x Q4.12 donnera Q8.24.

    On voit imm\u00e9diatement que la partie enti\u00e8re vaut 2, donc 90% de 3.14 donnera une valeur en dessous de 3. Pour reconstruire une valeur Q8.8 il convient de supprimer les 16-bits de poids faible.

    3686 * 12867 = 47227762\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25020\u25020\u25021\u25020\u2502\u25021\u25021\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25021\u25020\u25021\u25020\u25020\u25020\u25021\u25021\u2502\u25020\u25021\u25021\u25021\u25020\u25020\u25021\u25020\u2502 Q8.24\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25020\u25020\u25021\u25020\u2502\u25021\u25021\u25020\u25021\u25020\u25020\u25020\u25020\u2502 Q8.8\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n
    inline Q q_sat(int32_t x) {\n    x = x > 0x7FFF ? 0x7FFF : x\n    x = x < -1 * 0x8000 ? -1 * 0x8000 : x;\n    return (Q)x;\n}\n\ninline int16_t q_mul(int16_t a, int16_t b, char q)\n{\n    int32_t c = (int32_t)a * (int32_t)b;\n    c += 1 << (q - 1);\n    return sat(c >> q);\n}\n\ninline int16_t q12_mul(int16_t a, int16_t b)\n{\n    return q_mul(a, b, 12);\n}\n
    "}, {"location": "course-c/15-fundations/control-structures/", "title": "Structures de contr\u00f4le", "text": "Tout probl\u00e8me informatique peut \u00eatre r\u00e9solu en introduisant un niveau d'indirection suppl\u00e9mentaire.David J. Wheeler / Butler Lampson

    Les structures de contr\u00f4le appartiennent aux langages de programmation imp\u00e9ratifs et structur\u00e9. Elles permettent de modifier l'ordre des op\u00e9rations lors de l'ex\u00e9cution du code. On peut citer les cat\u00e9gories suivantes\u2009:

    Les s\u00e9quences

    On d\u00e9fini par s\u00e9quences les instructions qui s'ex\u00e9cutent les unes apr\u00e8s les autres. Celles-ci peuvent \u00eatre jalonn\u00e9es explicitement par une instruction de fin d'instruction, implicitement par un point de s\u00e9quence ou regroup\u00e9 dans un bloc. On peut distinguer trois types de s\u00e9quences\u2009:

    • les s\u00e9quences de code (;);
    • les blocs de code ({});
    • les points de s\u00e9quences.
    Les s\u00e9lections ou sauts

    Il existe des instructions qui permettent de modifier le flux d'ex\u00e9cution du programme, c'est-\u00e0-dire de sauter \u00e0 une autre partie du code. Les sauts conditionnels d\u00e9pendent d'une condition, tandis que les sauts inconditionnels sont toujours ex\u00e9cut\u00e9s. On peut distinguer les sauts d\u2019instructions suivantes\u2009:

    • sauts conditionnels (if, switch);
    • sauts inconditionnels (break, continue, goto, return).
    Les it\u00e9rations ou boucles

    Une boucle est une structure de contr\u00f4le qui permet de r\u00e9p\u00e9ter une instruction ou un bloc d'instructions tant qu'une condition est vraie. On peut distinguer les boucles suivantes\u2009:

    • boucles it\u00e9ratives sur une valeur connue for;
    • boucles sur condition while;
    • boucles sur condition avec test \u00e0 la fin do...while.

    Sans structure de contr\u00f4le, un programme se comportera toujours de la m\u00eame mani\u00e8re et ne pourra pas \u00eatre sensible \u00e0 des \u00e9v\u00e8nements ext\u00e9rieurs puisque le flux d'ex\u00e9cution ne pourra pas \u00eatre modifi\u00e9 conditionnellement. L'intelligence d'un programme r\u00e9side donc dans sa capacit\u00e9 \u00e0 prendre des d\u00e9cisions en fonction de l'\u00e9tat du syst\u00e8me et des donn\u00e9es qu'il manipule. Les structures de contr\u00f4le permettent de d\u00e9finir ces d\u00e9cisions, un peu comme un livre dont vous \u00eates le h\u00e9ros o\u00f9 chaque choix vous m\u00e8ne \u00e0 une page diff\u00e9rente par un saut.

    Historiquement, les premiers langages de programmation ne disposaient pas de structures de contr\u00f4le \u00e9volu\u00e9es. Au niveau assembleur on il est possible d'\u00eatre Turing complet avec deux types de sauts\u2009: inconditionnel (jmp) et conditionnel jz (jump if zero: saut si la valeur de la condition est nulle). Avec plus de 100 ans de recul, et des milliers de langages de programmation, la programmation imp\u00e9rative n'a pas beaucoup \u00e9volu\u00e9e. Les structures de contr\u00f4le sont rest\u00e9es les m\u00eames, seules les syntaxes ont \u00e9volu\u00e9. Certains langages comme le Python on m\u00eame d\u00e9cid\u00e9 de simplifier certaines structures de contr\u00f4le comme le do...while qui n'existe pas.

    On peut n\u00e9anmoins citer certaines fonctions d'ordre sup\u00e9rieur en programmation fonctionnelle (p. ex. map, filter, reduce) qui permettent de manipuler des s\u00e9quences de donn\u00e9es sans utiliser de boucles explicites. Ces fonctions sont souvent plus expressives et plus s\u00fbres que les boucles traditionnelles, mais elles ne remplacent pas les structures de contr\u00f4le classiques et elles n'existent pas en C. Les monades en Haskell sont un autre exemple de structures de contr\u00f4le avanc\u00e9es qui permettent de g\u00e9rer des effets de bord de mani\u00e8re s\u00fbre et contr\u00f4l\u00e9e. Des langages comme Kotlin ou JavaScript ont introduit des concepts similaires comme les coroutines ou les promesses pour g\u00e9rer de mani\u00e8re asynchrone des t\u00e2ches longues, mais une fois de plus ce sont des concepts qui n'existent pas en C.

    ", "tags": ["while", "goto", "reduce", "for", "break", "switch", "jmp", "map", "return", "do...while", "filter", "continue"]}, {"location": "course-c/15-fundations/control-structures/#sequences", "title": "S\u00e9quences", "text": "

    En informatique, une s\u00e9quence repr\u00e9sente la forme la plus fondamentale de structure de contr\u00f4le dans les langages de programmation imp\u00e9ratifs. Elle d\u00e9finit un ordre d'ex\u00e9cution lin\u00e9aire o\u00f9 les instructions sont ex\u00e9cut\u00e9es les unes apr\u00e8s les autres, suivant l'ordre dans lequel elles apparaissent dans le code source. Cette ex\u00e9cution s\u00e9quentielle est essentielle pour garantir la pr\u00e9visibilit\u00e9 et la coh\u00e9rence du comportement d'un programme.

    Formellement, une s\u00e9quence peut \u00eatre vue comme une composition de plusieurs instructions \u00e9l\u00e9mentaires \\(S_1\u2009; S_2\u2009; \\dots\u2009; S_n\\), o\u00f9 chaque instruction \\(S_i\\) est ex\u00e9cut\u00e9e apr\u00e8s la pr\u00e9c\u00e9dente. Dans ce mod\u00e8le, le flux de contr\u00f4le passe implicitement d'une instruction \u00e0 la suivante sans interruption, sauf si une structure de contr\u00f4le (comme une boucle ou une condition) modifie ce flux.

    La notion de s\u00e9quence est au c\u0153ur de la programmation structur\u00e9e, qui pr\u00e9conise l'utilisation de structures de contr\u00f4le bien d\u00e9finies (s\u00e9quences, s\u00e9lections, it\u00e9rations) pour am\u00e9liorer la lisibilit\u00e9, la maintenabilit\u00e9 et la fiabilit\u00e9 du code. En \u00e9vitant les sauts non structur\u00e9s comme le goto, les programmes deviennent plus faciles \u00e0 comprendre et \u00e0 v\u00e9rifier formellement.

    En pratique, les s\u00e9quences en code source sont souvent d\u00e9limit\u00e9es par des symboles sp\u00e9cifiques ou des conventions syntaxiques du langage utilis\u00e9. Par exemple\u2009: en C, les instructions sont termin\u00e9es par un point-virgule ;, et les blocs de code sont d\u00e9limit\u00e9s par des accolades {}, en Python, l'indentation d\u00e9finit les blocs de code, et chaque instruction est g\u00e9n\u00e9ralement \u00e9crite sur une nouvelle ligne.

    Il est important de noter que m\u00eame si les s\u00e9quences repr\u00e9sentent l'ex\u00e9cution lin\u00e9aire de code, elles peuvent contenir des appels \u00e0 des fonctions ou des proc\u00e9dures qui encapsulent elles-m\u00eames des structures de contr\u00f4le plus complexes. Ainsi, la s\u00e9quence constitue le fondement sur lequel sont b\u00e2ties les constructions plus \u00e9labor\u00e9es d'un programme.

    Au sein d'une m\u00eame fonction (ou d'un m\u00eame bloc de code) on retrouve l'ordre s\u00e9quentiel des instructions\u2009:

    int main() {\n    /* 1 */ int a = 5;\n    /* 2 */ int b = 10;\n    /* 3 */ int sum = a + b;\n    /* 4 */ printf(\"%d\\n\", sum);\n}\n

    ", "tags": ["goto"]}, {"location": "course-c/15-fundations/control-structures/#sequences-de-code", "title": "S\u00e9quences de code", "text": "

    En C, chaque instruction est s\u00e9par\u00e9e de la suivante par un point-virgule ; 003B. On appelle ce caract\u00e8re le d\u00e9limiteur d'instruction. Nous noterons que le retour \u00e0 la ligne n'est pas un d\u00e9limiteur d'instruction, mais un s\u00e9parateur visuel qui permet de rendre le code plus lisible. Il est donc possible d'\u00e9crire plusieurs instructions sur une seule ligne\u2009:

    #include <stdio.h> // doit \u00eatre sur une seule ligne\nint main() { char hello[] = \"hello\"; printf(\"%s, world\", hello); return 42; }\n

    Seuls les directives du pr\u00e9processeur (qui commencent par #) et les commentaires de lignes (//) d\u00e9pendent du retour \u00e0 la ligne.

    Le point-virgule grec

    N'allez pas confondre le point virgule ; (003B) avec le \u037e (037E), le point d'interrogation grec (\u03b5\u03c1\u03c9\u03c4\u03b7\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc). Certains farceurs aiment \u00e0 le remplacer dans le code de camarades ce qui g\u00e9n\u00e8re naturellement des erreurs de compilation.

    "}, {"location": "course-c/15-fundations/control-structures/#sequence-bloc", "title": "S\u00e9quence bloc", "text": "

    Une s\u00e9quence bloc ou instruction compos\u00e9e (compound statement) est une suite d'instructions regroup\u00e9es en un bloc mat\u00e9rialis\u00e9 par des accolades {}:

    {\n    double pi = 3.14;\n    area = pi * radius * radius;\n}\n

    Il est possible d'ajouter autant de blocs que vous voulez, mais il est recommand\u00e9 de ne pas imbriquer les blocs de mani\u00e8re excessive. Un bloc est une unit\u00e9 de code qui peut \u00eatre trait\u00e9 comme une seule instruction, mais qui n'est pas termin\u00e9 par un point-virgule.

    Il est possible de d\u00e9clarer des variables locales dans un bloc, ces variables n'\u00e9tant accessibles que dans le bloc o\u00f9 elles sont d\u00e9clar\u00e9es. L'exemple suivant montre plusieurs variables locales dont la visibilit\u00e9 est limit\u00e9e \u00e0 leur bloc respectif\u2009:

    {\n    int a = 1;\n    {\n        int b = 2;\n        {\n            int c = 3;\n        }\n        // c n'est pas accessible ici\n    }\n    // b et c ne sont pas accessibles ici\n}\n// a, b et c ne sont pas accessibles ici\n

    Limites de profondeur

    Le standard C99 \u00a75.2.4.1 impose qu'un compilateur C doit supporter au moins 127 niveaux d'imbrication de blocs, ce qui est emplement suffisant. Cette valeur n'a pas \u00e9t\u00e9 introduite par hasard, 127 est la valeur maximale d'un entier sign\u00e9 sur 8 bits (char) et les ordinateurs ne savent pas manipuler efficacement des types de donn\u00e9es plus petits.

    Ceci \u00e9tant, le nombre d'imbrication de structures conditionnelles est limit\u00e9 \u00e0 63, ce qui est d\u00e9j\u00e0 beaucoup trop. Si vous avez besoin de plus de 63 niveaux d'imbrication, il est temps de revoir votre conception\u2009!

    Notons n\u00e9anmoins que les compilateurs modernes ne limitent pas le nombre d'imbrication de blocs et de structures conditionnelles.

    ", "tags": ["char"]}, {"location": "course-c/15-fundations/control-structures/#point-de-sequence", "title": "Point de s\u00e9quence", "text": ""}, {"location": "course-c/15-fundations/control-structures/#points-de-sequence", "title": "Points de s\u00e9quence", "text": "

    Un point de s\u00e9quence, ou sequence point, est une notion d\u00e9finie dans l'annexe du standard C, qui garantit que certains ordres d'\u00e9valuation sont respect\u00e9s lors de l'ex\u00e9cution d'instructions. En d'autres termes, un point de s\u00e9quence marque un moment o\u00f9 tous les effets secondaires d'expressions pr\u00e9c\u00e9dentes doivent \u00eatre achev\u00e9s avant d'entamer l'\u00e9valuation d'expressions suivantes.

    Les r\u00e8gles relatives aux points de s\u00e9quence sont les suivantes\u2009:

    Appel de fonction

    L'\u00e9valuation des arguments d'une fonction est enti\u00e8rement termin\u00e9e avant l'ex\u00e9cution de cette fonction elle-m\u00eame. Autrement dit, l'ordre d'\u00e9valuation des arguments peut \u00eatre ind\u00e9termin\u00e9, mais tous les arguments doivent \u00eatre \u00e9valu\u00e9s avant l'appel de la fonction.

    Op\u00e9rateurs conditionnels et logiques

    Les op\u00e9rateurs &&, ||, ? :, et , introduisent des points de s\u00e9quence entre leurs op\u00e9randes. Par exemple, dans l'expression a() && b(), si a() retourne false, b() ne sera jamais \u00e9valu\u00e9e, car le r\u00e9sultat global de l'expression est d\u00e9termin\u00e9 sans avoir besoin de calculer b(). Ce m\u00e9canisme, appel\u00e9 \u00ab\u2009court-circuit\u2009\u00bb, optimise le traitement logique.

    Entr\u00e9es-sorties

    Un point de s\u00e9quence est pr\u00e9sent avant et apr\u00e8s toute op\u00e9ration d'entr\u00e9e/sortie (I/O). Cela garantit que les effets li\u00e9s \u00e0 ces op\u00e9rations, comme l'\u00e9criture sur un fichier ou l'affichage \u00e0 l'\u00e9cran, se produisent dans un ordre pr\u00e9visible.

    Il est essentiel de noter que l'op\u00e9rateur d'affectation = n'est pas un point de s\u00e9quence. Par cons\u00e9quent, l'\u00e9valuation d'une expression telle que (a = 2) + a + (a = 2) est ind\u00e9termin\u00e9e. Selon l'ordre d'\u00e9valuation des sous-expressions, le r\u00e9sultat peut varier, car les modifications de la variable a peuvent intervenir de mani\u00e8re impr\u00e9visible.

    Pour mieux saisir la notion de point de s\u00e9quence, il est essentiel de comprendre que, lorsqu'un programme en C est compil\u00e9, il est traduit en instructions assembleur qui seront ensuite ex\u00e9cut\u00e9es par le processeur. Le compilateur, afin d'optimiser les performances, peut r\u00e9ordonner certaines instructions ou les parall\u00e9liser dans l'unit\u00e9 arithm\u00e9tique et logique (ALU). Toutefois, ces optimisations peuvent entra\u00eener des comportements ind\u00e9termin\u00e9s si elles interf\u00e8rent avec l'ordre d'ex\u00e9cution attendu par le programmeur.

    Les points de s\u00e9quence jouent un r\u00f4le crucial en imposant des barri\u00e8res explicites dans le flux d'instructions, garantissant qu'\u00e0 ces points pr\u00e9cis, tous les effets des calculs pr\u00e9c\u00e9dents sont achev\u00e9s avant de passer \u00e0 l'\u00e9valuation des instructions suivantes. En d'autres termes, ils emp\u00eachent le r\u00e9ordonnancement des instructions au-del\u00e0 d'un point donn\u00e9, assurant ainsi un comportement pr\u00e9visible et conforme aux attentes du programmeur.

    ", "tags": ["false", "point-de-sequence"]}, {"location": "course-c/15-fundations/control-structures/#les-sauts-conditionnels", "title": "Les sauts conditionnels", "text": "

    Les embranchements sont des instructions de contr\u00f4le permettant au programme de prendre des d\u00e9cisions en fonction de conditions sp\u00e9cifiques. Une prise de d\u00e9cision est dite binaire lorsqu'elle repose sur un choix entre deux alternatives\u2009: vrai ou faux. Elle est dite multiple lorsque la condition \u00e9value une variable scalaire, conduisant \u00e0 plusieurs chemins possibles. En langage C, il existe deux types principaux d'instructions d'embranchement\u2009:

    1. if et if else pour les d\u00e9cisions binaires,
    2. switch pour la s\u00e9lection parmi plusieurs cas possibles.

    Ces types d'embranchements peuvent \u00eatre repr\u00e9sent\u00e9s visuellement \u00e0 l'aide de diagrammes de flux, comme les diagrammes BPMN (Business Process Model and Notation) ou les structogrammes NSD (Nassi-Shneiderman Diagrams). Ces repr\u00e9sentations graphiques permettent de mod\u00e9liser les choix conditionnels de mani\u00e8re intuitive.

    Voici un exemple de diagrammes BPMN et NSD illustrant un embranchement binaire\u2009:

    Diagrammes BPMN

    Les embranchements reposent sur des s\u00e9quences d'instructions, car chaque branche, qu'elle soit choisie ou non, est elle-m\u00eame une s\u00e9quence de commandes \u00e0 ex\u00e9cuter selon l'\u00e9valuation de la condition.

    ", "tags": ["switch"]}, {"location": "course-c/15-fundations/control-structures/#if_1", "title": "if", "text": "

    L'instruction if traduite par si est de loin la plus utilis\u00e9e. L'exemple suivant illustre un embranchement binaire. Il affiche odd si le nombre est impair et even s'il est pair\u2009:

    if (value % 2)\n{\n    printf(\"odd\\n\");\n}\nelse\n{\n    printf(\"even\\n\");\n}\n

    Notons que les blocs sont facultatifs. L'instruction if s'attend \u00e0 une instruction ou une instruction compos\u00e9e. Il est recommand\u00e9 de toujours utiliser les blocs pour \u00e9viter les erreurs de logique. N\u00e9anmoins le code suivant est parfaitement valide\u2009:

    if (value % 2)\n    printf(\"odd\\n\");\nelse\n    printf(\"even\\n\");\n

    De m\u00eame que comme des ; s\u00e9parent les instructions, on peut aussi \u00e9crire\u2009:

    if (value % 2) printf(\"odd\\n\"); else printf(\"even\\n\");\n

    Dans certaines normes pour le m\u00e9dical ou l'a\u00e9ronautique, comme MISRA, l'absence d'accollades est interdite pour \u00e9viter les erreurs de logique. C'est g\u00e9n\u00e9ralement une bonne pratique \u00e0 suivre sauf lorsque la lisibilit\u00e9 du code est am\u00e9lior\u00e9e par l'absence d'accollades.

    Info

    Dans l'exemple ci-dessus, l'instruction ternaire est plus \u00e9l\u00e9gante\u2009:

    printf(\"%s\\n\", value % 2 ? \"odd\" : \"even\");\n

    Le mot cl\u00e9 else est facultatif. Si l'on ne souhaite pas ex\u00e9cuter d'instruction lorsque la condition est fausse, il est possible de ne pas la sp\u00e9cifier.

    int a = 42;\nint b = 0;\n\nif (b == 0) {\n    printf(\"Division par z\u00e9ro impossible\\n\");\n    exit(EXIT_FAILURE);\n}\n\nprintf(\"a / b = %d\\n\", a / b);\n

    En C il n'y a pas d'instruction if..else if comme on peut le trouver dans d'autres langages de programmation (p. ex. Python avec elif). Faire suivre une sous condition \u00e0 else est n\u00e9anmoins possible puisque if est une instruction comme une autre la preuve est donn\u00e9e par la grammaire du langage qui d\u00e9termine qu'une instruction de s\u00e9lection (selection_statement), qui est une instruction (statement), peut \u00eatre suivie d'une autre instruction, et donc d'une autre instruction de s\u00e9lection.

    selection_statement\n    : IF '(' expression ')' statement\n    | IF '(' expression ')' statement ELSE statement\n    | SWITCH '(' expression ')' statement\n    ;\n

    Voici un exemple d'imbriquement de conditions\u2009:

    if (value < 0) {\n    printf(\"La valeur est n\u00e9gative\\n\");\n}\nelse {\n    if (value == 0) {\n        printf(\"La valeur est nulle\\n\");\n    }\n    else {\n        printf(\"La valeur est positive\\n\");\n    }\n}\n

    N\u00e9anmoins, comme il n'y a qu'une instruction if apr\u00e8s le premier else, le bloc peut \u00eatre omis. En outre, il est correct de faire figurer le if sur la m\u00eame ligne que le else :

    if (value < 0) {\n    printf(\"La valeur est n\u00e9gative\\n\");\n}\nelse if (value == 0) {\n    printf(\"La valeur est nulle\\n\");\n}\nelse {\n    printf(\"La valeur est positive\\n\");\n}\n

    Une condition n'est pas n\u00e9cessairement unique, elle peut-\u00eatre la concat\u00e9nation logique de plusieurs conditions s\u00e9par\u00e9es\u2009:

    if((0 < x && x < 10) || (100 < x && x < 110) || (200 < x && x < 210))\n{\n    printf(\"La valeur %d est valide\", x);\n    is_valid = true;\n}\nelse\n{\n    printf(\"La valeur %d n'est pas valide\", x);\n    is_valid = false;\n}\n

    Remarquons que cet exemple peut \u00eatre simplifi\u00e9 pour diminuer la complexit\u00e9 cyclomatique :

    bool is_valid = (0 < x && x < 10) ||\n                (100 < x && x < 110) ||\n                (200 < x && x < 210);\n\nif (is_valid) {\n    printf(\"La valeur %d est valide\", x);\n}\nelse {\n    printf(\"La valeur %d n'est pas valide\", x);\n}\n

    Allman, Stroustrup ou K&R ?

    Il existe plusieurs conventions de style pour \u00e9crire les blocs de code. Les plus connues sont les styles Allman, Stroustrup et K&R. Le style Allman place les accolades sur des lignes s\u00e9par\u00e9es, le style Stroustrup les place sur la m\u00eame ligne que l'instruction de contr\u00f4le, et le style K&R les place sur la m\u00eame ligne que l'instruction de contr\u00f4le mais avec un d\u00e9calage.

    Chacun de ces styles a ses partisans et ses d\u00e9tracteurs, et il est important de choisir un style coh\u00e9rent pour un projet donn\u00e9.

    Le style de codage est pris\u00e9 par les managers qui ne savent pas programmer, ils ont appris \u00e0 rep\u00e9rer les incoh\u00e9rences de style et pense qu'il s'agit d'un indicateur de qualit\u00e9 du code. C'est un peu comme si un chef cuisinier jugeait la qualit\u00e9 d'un plat \u00e0 la couleur de l'assiette. Peu importe la couleur de l'assiette, ce qui compte c'est le go\u00fbt du plat. N\u00e9anmoins un restaurant qui n'aurait pas de coh\u00e9rence dans la couleur de ses assiettes pourrait \u00eatre per\u00e7u comme n\u00e9glig\u00e9.

    Point virgule en trop

    Il peut arriver par reflexe d'ajouter un point virgule derri\u00e8re un if qui a pour effet de terminer pr\u00e9matur\u00e9ment le bloc. Par exemple\u2009:

    if (z == 0);\nprintf(\"z est nul\"); // ALWAYS executed\n

    C'est une erreur classique qui peut \u00eatre difficile \u00e0 rep\u00e9rer.

    Affectation dans un test

    Le test de la valeur d'une variable s'\u00e9crit avec l'op\u00e9rateur d'\u00e9galit\u00e9 == et non l'op\u00e9rateur d'affectation =. Ici, l'\u00e9valuation de la condition vaut la valeur affect\u00e9e \u00e0 la variable.

    if (z = 0)               // set z to zero !!\n    printf(\"z est nul\"); // NEVER executed\n

    L'oubli des accolades

    Dans le cas ou vous souhaitez ex\u00e9cuter plusieurs instructions, vous devez imp\u00e9rativement d\u00e9clarer un bloc d'instructions. Si vous omettez les accolades, seule la premi\u00e8re instruction sera ex\u00e9cut\u00e9e puisque la s\u00e9quence se termine par un point virgule ou un bloc.

    if (z == 0)\n    printf(\"z est nul\");\n    is_valid = false;  // Ne fait par partie du bloc et s'ex\u00e9cute toujours\n

    On peut utiliser des conditions multiples pour d\u00e9terminer le comportement d'un programme. Par exemple, le programme suivant affiche un message diff\u00e9rent en fonction de la valeur de value :

    if (value % 2)\n{\n    printf(\"La valeur est impaire.\");\n}\nelse if (value > 500)\n{\n    printf(\"La valeur est paire et sup\u00e9rieure \u00e0 500.\");\n}\nelse if (!(value % 5))\n{\n    printf(\"La valeur est paire, inf\u00e9rieur \u00e0 500 et divisible par 5.\");\n}\nelse\n{\n    printf(\"La valeur ne satisfait aucune condition \u00e9tablie.\");\n}\n

    Exercice 1\u2009: Et si\u2009?

    Comment se comporte l'exemple suivant\u2009:

    if (!(i < 8) && !(i > 8))\n    printf(\"i is %d\\n\", i);\n

    Exercice 2\u2009: D'autres si\u2009?

    Compte tenu de la d\u00e9claration int i = 8;, indiquer pour chaque expression si elles impriment ou non i vaut 8:

    1. if (!(i < 8) && !(i > 8)) then\n    printf(\"i vaut 8\\n\");\n
    2. if (!(i < 8) && !(i > 8))\n    printf(\"i vaut 8\");\n    printf(\"\\n\");\n
    3. if !(i < 8) && !(i > 8)\n    printf(\"i vaut 8\\n\");\n
    4. if (!(i < 8) && !(i > 8))\n    printf(\"i vaut 8\\n\");\n
    5. if (i = 8) printf(\"i vaut 8\\n\");\n
    6. if (i & (1 << 3)) printf(\"i vaut 8\\n\");\n
    7. if (i ^ 8) printf(\"i vaut 8\\n\");\n
    8. if (i - 8) printf(\"i vaut 8\\n\");\n
    9. if (i == 1 << 3) printf (\"i vaut 8\\n\");\n
    10. if (!((i < 8) || (i > 8)))\n    printf(\"i vaut 8\\n\");\n

    ", "tags": ["elif", "statement", "even", "odd", "value", "else"]}, {"location": "course-c/15-fundations/control-structures/#switch_1", "title": "switch", "text": "

    L'instruction switch n'est pas fondamentale et certains langages de programmation ne la d\u00e9finissent pas. Elle permet essentiellement de simplifier l'\u00e9criture pour minimiser les r\u00e9p\u00e9titions. On l'utilise lorsque les conditions multiples portent toujours sur la m\u00eame variable. Par exemple, le code suivant peut \u00eatre r\u00e9\u00e9crit plus simplement en utilisant un switch :

    Switch Case BPMN

    if (defcon == 1)\n    printf(\"Guerre nucl\u00e9aire imminente\");\nelse if (defcon == 2)\n    printf(\"Prochaine \u00e9tape, guerre nucl\u00e9aire\");\nelse if (defcon == 3)\n    printf(\"Accroissement de la pr\u00e9paration des forces\");\nelse if (defcon == 4)\n    printf(\"Mesures de s\u00e9curit\u00e9 renforc\u00e9es et renseignements accrus\");\nelse if (defcon == 5\n    printf(\"Rien \u00e0 signaler, temps de paix\");\nelse\n    printf(\"ERREUR: Niveau d'alerte DEFCON invalide\");\n

    Voici la m\u00eame s\u00e9quence utilisant switch. Notez que chaque condition est plus claire\u2009:

    switch (defcon)\n{\n    case 1 :\n        printf(\"Guerre nucl\u00e9aire imminente\");\n        break;\n    case 2 :\n        printf(\"Prochaine \u00e9tape, guerre nucl\u00e9aire\");\n        break;\n    case 3 :\n        printf(\"Accroissement de la pr\u00e9paration des forces\");\n        break;\n    case 4 :\n        printf(\"Mesures de s\u00e9curit\u00e9 renforc\u00e9es et renseignements accrus\");\n        break;\n    case 5 :\n        printf(\"Rien \u00e0 signaler, temps de paix\");\n        break;\n    default :\n        printf(\"ERREUR: Niveau d'alerte DEFCON invalide\");\n}\n

    La valeur par d\u00e9faut default est optionnelle, mais recommand\u00e9e pour traiter les cas d'erreurs possibles.

    La structure d'un switch est compos\u00e9e d'une condition switch (condition) suivie d'une s\u00e9quence {}. Les instructions de cas case 42: sont appel\u00e9es \u00e9tiquettes (labels). Notez la pr\u00e9sence de l'instruction break qui est n\u00e9cessaire pour terminer l'ex\u00e9cution de chaque condition. Par ailleurs, les labels peuvent \u00eatre cha\u00een\u00e9s sans instructions interm\u00e9diaires ni break:

    switch (coffee)\n{\n    case IRISH_COFFEE :\n        add_whisky();\n\n    case CAPPUCCINO :\n    case MACCHIATO :\n        add_milk();\n\n    case ESPRESSO :\n    case AMERICANO :\n        add_coffee();\n        break;\n\n    default :\n        printf(\"ERREUR 418: Type de caf\u00e9 inconnu\");\n}\n

    Notons que le compilateur est capable d'optimiser les switch en fonction des valeurs des \u00e9tiquettes. Il n'est pas n\u00e9cessaire que les \u00e9tiquettes soient tri\u00e9es, car le compilateur peut r\u00e9ordonner les cas pour optimiser l'ex\u00e9cution. N\u00e9anmoins des \u00e9tiquettes \u00e0 progression logique p. ex. {12, 13, 14, 15} sont plus efficaces que des \u00e9tiquettes al\u00e9atoires p. ex. {1, 109, 9, 42}.

    La construction d'une \u00e9tiquette case implique une constante litt\u00e9rale. Il n'est pas possible d'utiliser une expression ou une variable. En outre, il ne peut y avoir qu'une seule \u00e9tiquette par ligne, car cette derni\u00e8re doit \u00eatre situ\u00e9e apr\u00e8s un retour \u00e0 la ligne. Voici un exemple permettant de d\u00e9terminer le nombre de jours dans un mois\u2009:

    int ndays = -1;\nswitch (month) {\n    case 1:  // JAN\n    case 3:  // MAR\n    case 5:  // MAY\n    case 7:  // JUL\n    case 8:  // AUG\n    case 10: // OCT\n    case 12: // DEC\n        ndays = 31;\n        break;\n    case 4:  // APR\n    case 6:  // JUN\n    case 9:  // SEP\n    case 11: // NOV\n        ndays = 30;\n        break;\n    case 2:  // FEB\n        if (year % 400 == 0)\n            ndays = 29;\n        else if (year % 100 == 0)\n            ndays = 28;\n        else if (year % 4 == 0)\n            ndays = 29;\n        else\n            ndays = 28;\n        break;\n    default:\n        // Erreur : mois invalide\n}\n
    ", "tags": ["default", "case", "break", "switch"]}, {"location": "course-c/15-fundations/control-structures/#declaration-de-variables", "title": "D\u00e9claration de variables", "text": "

    Il faut comprendre que la structure switch est un peu particuli\u00e8re. Le switch agit comme un bloc, la d\u00e9claration de variable est possible \u00e0 n'importe quel endroit du bloc, mais toutes les lignes de ce bloc ne seront pas ex\u00e9cut\u00e9es puisque le switch utilisera les labels pour sauter \u00e0 la bonne instruction. Consid\u00e9rons l'exemple suivant\u2009:

    int main(int argc) {\n   switch (argc) {\n      int i = 23;\n      case 1:\n         int j = 42;\n         printf(\"0. %d\\n\", i + j);\n         break;\n      case 2:\n         int j = 23;\n         printf(\"1. %d\\n\", i + j);\n         break;\n   }\n}\n

    \u00c0 la compilation on notera l'erreur suivante\u2009:

    test.c: In function \u2018main\u2019:\ntest.c:5:11: warning: statement will never be executed [-Wswitch-unreachable]\n    5 |       int i = 23;\n      |           ^\n

    En effet, cette instruction se trouve avant le premier label case et ne sera donc jamais ex\u00e9cut\u00e9. La variable est belle et bien d\u00e9clar\u00e9e, mais elle ne sera pas initialis\u00e9e.

    En outre, la d\u00e9claration de j = 23 pose \u00e9galement probl\u00e8me, l'erreur de compilation suivante appara\u00eet\u2009:

    test.c: In function \u2018main\u2019:\ntest.c:11:14: error: redefinition of \u2018j\u2019\n   11 |          int j = 23;\n      |              ^\ntest.c:7:14: note: previous definition of \u2018j\u2019 with type \u2018int\u2019\n    7 |          int j = 42;\n      |            ^\n

    Vous savez qu'il n'est pas possible de red\u00e9clarer une variable d\u00e9j\u00e0 existante dans le m\u00eame bloc. La solution \u00e0 ce probl\u00e8me est de d\u00e9clarer les variables propres \u00e0 un cas dans un bloc s\u00e9par\u00e9. Notez que la variable k n'\u00e9tant utilis\u00e9e qu'une fois, elle peut \u00eatre dans le contexte global du switch mais situ\u00e9 apr\u00e8s le premier label case. En pratique, n'essayez pas de jouer avec les limites de la syntaxe, cela ne fera que rendre votre code plus difficile \u00e0 lire et \u00e0 maintenir.

    #include <stdio.h>\n\nint main(int argc) {\n   switch (argc) {\n      case 1:\n         int k = 10;\n         {\n            int i = 23;\n            int j = 42;\n            printf(\"0. %d\\n\", i + j + k);\n            break;\n         }\n      case 2: {\n         int i = 23;\n         int j = 23;\n         printf(\"1. %d\\n\", i + j);\n         break;\n      }\n   }\n}\n
    ", "tags": ["case", "switch"]}, {"location": "course-c/15-fundations/control-structures/#imbrication", "title": "Imbrication", "text": "

    Il est possible d'imbriquer diff\u00e9rents niveaux dans un switch\u2009:

    switch(a) {\n    case 100:\n        switch(b) {\n            case 200:\n                printf(\"a=100, b=200\\n\");\n                break;\n            case 300:\n                printf(\"a=100, b=300\\n\");\n                break;\n        }\n        break;\n}\n
    "}, {"location": "course-c/15-fundations/control-structures/#appareil-de-duff", "title": "Appareil de Duff", "text": "

    Le Duff's device est une technique d'optimisation assez originale en langage C, qui permet de d\u00e9rouler une boucle de mani\u00e8re partiellement manuelle, dans le but de gagner en performance, notamment sur des architectures mat\u00e9rielles plus anciennes. Il a \u00e9t\u00e9 invent\u00e9 par Tom Duff en 1983 lorsqu'il travaillait chez Lucasfilm.

    L'objectif du Duff's device est de d\u00e9rouler manuellement une boucle afin de r\u00e9duire le nombre d'it\u00e9rations et d'optimiser l'ex\u00e9cution, notamment dans les situations o\u00f9 le co\u00fbt d'un saut conditionnel dans une boucle pouvait \u00eatre \u00e9lev\u00e9. Cette optimisation est souvent appel\u00e9e unrolling, o\u00f9 plusieurs it\u00e9rations de la boucle sont \u00ab\u2009fusionn\u00e9es\u2009\u00bb en une seule.

    La particularit\u00e9 du Duff's device est qu'il combine \u00e0 la fois une structure de boucle while et un switch de mani\u00e8re surprenante et astucieuse. Voici \u00e0 quoi ressemble le code original\u2009:

    register int count = (n + 7) / 8;  // Nombre d'it\u00e9rations par paquet de 8\nregister short *to = dest;\nregister short *from = src;\n\nswitch (n % 8) {  // D\u00e9termine le point d'entr\u00e9e initial dans la boucle\n    case 0: do { *to = *from++;\n    case 7:      *to = *from++;\n    case 6:      *to = *from++;\n    case 5:      *to = *from++;\n    case 4:      *to = *from++;\n    case 3:      *to = *from++;\n    case 2:      *to = *from++;\n    case 1:      *to = *from++;\n            } while (--count > 0);\n}\n
    ", "tags": ["while", "switch"]}, {"location": "course-c/15-fundations/control-structures/#resume-des-points-cles", "title": "R\u00e9sum\u00e9 des points cl\u00e9s", "text": "
    • La structure switch bien qu'elle puisse toujours \u00eatre remplac\u00e9e par une structure if..else if est g\u00e9n\u00e9ralement plus \u00e9l\u00e9gante et plus lisible. Elle \u00e9vite par ailleurs de r\u00e9p\u00e9ter la condition plusieurs fois (c.f. DRY).
    • Le compilateur est mieux \u00e0 m\u00eame d'optimiser un choix multiple lorsque les valeurs scalaires de la condition tri\u00e9es se suivent directement p. ex. {12, 13, 14, 15}.
    • L'ordre des cas d'un switch n'a pas d'importance, le compilateur peut m\u00eame choisir de r\u00e9ordonner les cas pour optimiser l'ex\u00e9cution.
    • Les \u00e9tiquettes case ne peuvent \u00eatre que des constantes litt\u00e9rales, il n'est pas possible d'utiliser des expressions ou des variables.
    • Les \u00e9tiquettes case doivent \u00eatre s\u00e9par\u00e9es par un retour \u00e0 la ligne, il n'est pas possible d'avoir plusieurs \u00e9tiquettes sur une m\u00eame ligne.
    • Il est possible de cha\u00eener les \u00e9tiquettes sans break pour ex\u00e9cuter plusieurs instructions.
    ", "tags": ["break", "case", "switch"]}, {"location": "course-c/15-fundations/control-structures/#les-boucles", "title": "Les boucles", "text": "

    Bien choisir sa structure de contr\u00f4le

    Une boucle est une structure it\u00e9rative permettant de r\u00e9p\u00e9ter l'ex\u00e9cution d'une s\u00e9quence. En C il existe trois types de boucles\u2009:

    1. for
    2. while
    3. do .. while

    Elles peuvent \u00eatre repr\u00e9sent\u00e9es par les diagrammes de flux suivants\u2009:

    Aper\u00e7u des trois structures de boucles

    On observe que quelque soit la structure de boucle, une condition de maintien est n\u00e9cessaire. Cette condition est \u00e9valu\u00e9e avant ou apr\u00e8s l'ex\u00e9cution de la s\u00e9quence. Si la condition est fausse, la s\u00e9quence est interrompue et le programme continue son ex\u00e9cution.

    "}, {"location": "course-c/15-fundations/control-structures/#while", "title": "while", "text": "

    La structure while r\u00e9p\u00e8te une s\u00e9quence tant que la condition est vraie. Dans l'exemple suivant, tant que le poids d'un objet d\u00e9pos\u00e9 sur une balance est inf\u00e9rieur \u00e0 une valeur constante, une masse est ajout\u00e9e et le syst\u00e8me patiente avant stabilisation.

    while (get_weight() < 420 /* newtons */) {\n    add_one_kg();\n    wait(5 /* seconds */);\n}\n

    S\u00e9quentiellement une boucle while teste la condition, puis ex\u00e9cute la s\u00e9quence associ\u00e9e.

    Exercice 3\u2009: Tant que...

    Comment se comportent ces programmes\u2009?

    1. size_t i=0; while(i<11) { i+=2; printf(\"%i\\n\",i); }
    2. i = 11; while(i--){ printf(\"%i\\n\",i--); }
    3. i = 12; while(i--){ printf(\"%i\\n\",--i); }
    4. i = 1; while ( i <= 5 ){ printf ( \"%i\\n\", 2 * i++ );}
    5. i = 1; while ( i != 9 ) { printf ( \"%i\\n\", i = i + 2 ); }
    6. i = 1; while ( i < 9 ) { printf ( \"%i\\n\", i += 2 ); break; }
    7. i = 0; while ( i < 10 ) { continue; printf ( \"%i\\n\", i += 2 ); }

    On utilise une boucle while lorsque le nombre d'it\u00e9rations n'est pas connu \u00e0 l'avance. Si la s\u00e9quence doit \u00eatre ex\u00e9cut\u00e9e au moins une fois, on utilise une boucle do...while.

    ", "tags": ["while"]}, {"location": "course-c/15-fundations/control-structures/#dowhile", "title": "do..while", "text": "

    De temps en temps il est n\u00e9cessaire de tester la condition \u00e0 la sortie de la s\u00e9quence et non \u00e0 l'entr\u00e9e. La boucle do...while permet justement ceci\u2009:

    size_t i = 10;\n\ndo {\n    printf(\"Veuillez attendre encore %d seconde(s)\\r\\n\", i);\n    i -= 1;\n} while (i);\n

    Contrairement \u00e0 la boucle while, la s\u00e9quence est ici ex\u00e9cut\u00e9e au moins une fois.

    Notez ci-dessus la pr\u00e9sence d'un ; apr\u00e8s le while. La structure do...while est un peu particuli\u00e8re, car elle est la seule structure de contr\u00f4le \u00e0 se terminer par un point-virgule.

    ", "tags": ["while"]}, {"location": "course-c/15-fundations/control-structures/#for", "title": "for", "text": "

    La boucle for est un while am\u00e9lior\u00e9 qui permet en une ligne de r\u00e9sumer les conditions de la boucle\u2009:

    for (/* expression 1 */; /* expression 2 */; /* expression 3 */) {\n    /* s\u00e9quence */\n}\n
    Expression 1

    Ex\u00e9cut\u00e9e une seule fois \u00e0 l'entr\u00e9e dans la boucle, c'est l'expression d'initialisation permettant par exemple de d\u00e9clarer une variable et de l'initialiser \u00e0 une valeur particuli\u00e8re.

    Expression 2

    Condition de validit\u00e9 (ou de maintien de la boucle). Tant que la condition est vraie, la boucle est ex\u00e9cut\u00e9e.

    Expression 3

    Action de fin de tour. \u00c0 la fin de l'ex\u00e9cution de la s\u00e9quence, cette action est ex\u00e9cut\u00e9e avant le tour suivant. Cette action permet par exemple d'incr\u00e9menter une variable.

    Voici comment r\u00e9p\u00e9ter 10x un bloc de code\u2009:

    for (size_t i = 0; i < 10; i++) {\n    something();\n}\n

    Notons que les portions de for sont optionnels et que la structure suivante est strictement identique \u00e0 la boucle while:

    for (; get_weight() < 420 ;) {\n    /* ... */\n}\n

    Exercice 4\u2009: Pour quelques tours

    Comment est-ce que ces expressions se comportent-elles\u2009?

    int i, k;\n
    1. for (i = 'a'; i < 'd'; printf (\"%i\\n\", ++i));
    2. for (i = 'a'; i < 'd'; printf (\"%c\\n\", ++i));
    3. for (i = 'a'; i++ < 'd'; printf (\"%c\\n\", i ));
    4. for (i = 'a'; i <= 'a' + 25; printf (\"%c\\n\", i++ ));
    5. for (i = 1 / 3; i ; printf(\"%i\\n\", i++ ));
    6. for (i = 0; i != 1 ; printf(\"%i\\n\", i += 1 / 3 ));
    7. for (i = 12, k = 1; k++ < 5 ; printf(\"%i\\n\", i-- ));
    8. for (i = 12, k = 1; k++ < 5 ; k++, printf(\"%i\\n\", i-- ));

    Exercice 5\u2009: Erreur

    Identifier les deux erreurs dans ce code suivant\u2009:

    for (size_t = 100; i >= 0; --i)\n    printf(\"%d\\n\", i);\n

    Exercice 6\u2009: De un \u00e0 cent

    \u00c9crivez un programme affichant les entiers de 1 \u00e0 100 en employant\u2009:

    1. Une boucle for
    2. Une boucle while
    3. Une boucle do..while

    Quelle est la structure de contr\u00f4le la plus adapt\u00e9e \u00e0 cette situation\u2009?

    L'op\u00e9rateur , est un op\u00e9rateur de s\u00e9quence qui permet de s\u00e9parer des expressions. Il est souvent utilis\u00e9 dans les boucles for pour ex\u00e9cuter plusieurs instructions dans les diff\u00e9rentes parties de la boucle. Par exemple pour d\u00e9finir deux variables i et j dans la partie d'initialisation de la boucle. Voici par exemple comment afficher les lettres de l'alphabet en alternance z-a y-b x-c... :

    for (char i = 'z', j = 'a'; i > j; i--, j++) {\n    printf(\"%c-%c \", i, j);\n}\n

    Le programme affiche\u2009:

    z-a y-b x-c w-d v-e u-f t-g s-h r-i q-j p-k o-l n-m\n

    Variable d'induction non sign\u00e9e

    Il est recommand\u00e9 d'utiliser des variables d'induction sign\u00e9es pour les boucles for afin d'\u00e9viter des erreurs de logique. En effet, si vous utilisez une variable d'induction non sign\u00e9e et que vous la d\u00e9cr\u00e9mentez, vous risquez de cr\u00e9er une boucle infinie\u2009:

    for (size_t i = 10; i >= 0; i--) {\n    printf(\"%d\\n\", i);\n}\n

    Dans cet exemple, i est une variable non sign\u00e9e de type size_t. Lorsque i atteint 0, la condition i >= 0 est toujours vraie, car size_t est un type non sign\u00e9 et ne peut pas \u00eatre n\u00e9gatif. Par cons\u00e9quent, la boucle ne se termine jamais et entra\u00eene un d\u00e9bordement de la variable i.

    En pratique on utilisera simplement un int pour les variables d'induction n\u00e9anmoins pour une grande portabilit\u00e9 on utilisera int_fast32_t ou int_fast64_t pour garantir une taille de variable optimale.

    Exercice 7\u2009: Op\u00e9rateur virgule dans une boucle

    Expliquez quelle est la fonctionnalit\u00e9 globale du programme ci-dessous\u2009:

    int main(void) {\n    for(size_t i = 0, j = 0; i * i < 1000; i++, j++, j %= 26, printf(\"\\n\"))\n        printf(\"%c\", 'a' + (char)j);\n}\n

    Proposer une meilleure impl\u00e9mentation de ce programme.

    ", "tags": ["do..while", "size_t", "int", "while", "for", "int_fast32_t", "int_fast64_t"]}, {"location": "course-c/15-fundations/control-structures/#boucles-infinies", "title": "Boucles infinies", "text": "

    Une boucle infinie n'est jamais termin\u00e9e. On rencontre souvent ce type de boucle dans ce que l'on appelle \u00e0 tort La boucle principale aussi nomm\u00e9e run loop. Lorsqu'un programme est ex\u00e9cut\u00e9 bare-metal, c'est \u00e0 dire directement \u00e0 m\u00eame le microcontr\u00f4leur et sans syst\u00e8me d'exploitation, il est fr\u00e9quent d'y trouver une fonction main telle que\u2009:

    void main_loop() {\n    // Boucle principale\n}\n\nint main(void) {\n    for (;;)\n    {\n        main_loop();\n    }\n}\n

    Il y a diff\u00e9rentes variantes de boucles infinies\u2009:

    for (;;) { }\n\nwhile (true) { }\n\ndo { } while (true);\n

    Notions que l'expression while (1) que l'on rencontre fr\u00e9quemment dans des exemples n'est pas syntaxiquement exacte. Une condition de validit\u00e9 devrait \u00eatre un bool\u00e9en, soit vrai, soit faux. Or, la valeur scalaire 1 devrait pr\u00e9alablement \u00eatre transform\u00e9e en une valeur bool\u00e9enne. Il est donc plus juste d'\u00e9crire while (1 == 1) ou simplement while (true). D'ailleurs pourquoi utiliser 1 et non pas 42 ? Moi j'aime bien while (42), c'est plus fun...

    Certains d\u00e9veloppeurs pr\u00e9f\u00e8rent l'\u00e9criture for (;;) qui ne fait pas intervenir de conditions ext\u00e9rieures ou de valeurs bulgares, car, avant C99 d\u00e9finir la valeur true \u00e9tait \u00e0 la charge du d\u00e9veloppeur et on pourrait s'imaginer cette plaisanterie de mauvais go\u00fbt\u2009:

    _Bool true = 0;\n\nwhile (true) { /* ... */ }\n

    Lorsque l'on a besoin d'une boucle infinie, il est g\u00e9n\u00e9ralement pr\u00e9f\u00e9rable de permettre au programme de se terminer correctement lorsqu'il est interrompu par le signal SIGINT (c. f. signals). On rajoute alors une condition de sortie \u00e0 la boucle principale\u2009:

    #include <stdlib.h>\n#include <signal.h>\n#include <stdbool.h>\n\nstatic volatile bool is_running = true;\n\nvoid sigint_handler(int dummy)\n{\n    is_running = false;\n}\n\nint main(void)\n{\n    signal(SIGINT, sigint_handler);\n\n    while (is_running)\n    {\n       /* ... */\n    }\n\n    return EXIT_SUCCESS;\n}\n

    ", "tags": ["main", "true"]}, {"location": "course-c/15-fundations/control-structures/#les-sauts", "title": "Les sauts", "text": "

    Il existe 4 instructions en C permettant de contr\u00f4ler le d\u00e9roulement de l'ex\u00e9cution d'un programme. Elles d\u00e9clenchent un saut inconditionnel vers un autre endroit du programme.

    break

    Cette instruction nterrompt la structure de contr\u00f4le en cours. Elle est valide pour while, do...while, for et switch.

    continue

    Cette instruction interrompt le cycle en cours et passe directement au cycle suivant. Elle est valide pour while, do...while et for.

    goto

    La redout\u00e9e instruction goto interrompt l'ex\u00e9cution et saute \u00e0 un label situ\u00e9 ailleurs dans la m\u00eame fonction.

    return

    Cette instruction interrompt l'ex\u00e9cution de la fonction en cours et retourne \u00e0 son point d'appel.

    ", "tags": ["while", "goto", "for", "switch", "break", "return", "continue"]}, {"location": "course-c/15-fundations/control-structures/#goto", "title": "goto", "text": "

    Il s'agit de l'instruction la plus controvers\u00e9e en C. Cherchez sur internet et les d\u00e9tracteurs sont nombreux, et ils ont partiellement raison, car dans la tr\u00e8s vaste majorit\u00e9 des cas o\u00f9 vous pensez avoir besoin de goto, une autre solution plus \u00e9l\u00e9gante existe.

    N\u00e9anmoins, il est important de comprendre que goto \u00e9tait dans certains langages de programmation comme BASIC, la seule structure de contr\u00f4le disponible permettant de faire des sauts. Elle est par ailleurs le reflet du langage machine, car la plupart des processeurs ne connaissent que cette instruction souvent appel\u00e9e JUMP. Il est par cons\u00e9quent possible d'imiter le comportement de n'importe quelle structure de contr\u00f4le si l'on dispose de if et de goto.

    goto effectue un saut inconditionnel \u00e0 un label d\u00e9fini en C par un identificateur suivi d'un :.

    L'un des seuls cas de figure autoris\u00e9s est celui d'un traitement d'erreur centralis\u00e9 lorsque de multiples points de retours existent dans une fonction ceci \u00e9vitant de r\u00e9p\u00e9ter du code\u2009:

    #include <time.h>\n\nint parse_message(int message)\n{\n    struct tm *t = localtime(time(NULL));\n    if (t->tm_hour < 7) {\n        goto error;\n    }\n\n    if (message > 1000) {\n        goto error;\n    }\n\n    /* ... */\n\n    return 0;\n\n    error:\n        printf(\"ERROR: Une erreur a \u00e9t\u00e9 commise\\n\");\n        return -1;\n}\n
    ", "tags": ["JUMP", "goto"]}, {"location": "course-c/15-fundations/control-structures/#continue", "title": "continue", "text": "

    Le mot cl\u00e9 continue ne peut exister qu'\u00e0 l'int\u00e9rieur d'une boucle. Il permet d'interrompre le cycle en cours et directement passer au cycle suivant.

    uint8_t airplane_seat = 100;\n\nwhile (--airplane_seat)\n{\n    if (airplane_seat == 13) {\n        continue;\n    }\n\n    printf(\"Dans cet avion il y a un si\u00e8ge num\u00e9ro %d\\n\", airplane_seat);\n}\n

    Cette structure est \u00e9quivalente \u00e0 l'utilisation d'un goto avec un label plac\u00e9 \u00e0 la fin de la s\u00e9quence de boucle, mais promettez-moi que vous n'utiliserez jamais cet exemple\u2009:

    while (true)\n{\n    if (condition) {\n        goto next;\n    }\n\n    /* ... */\n\n    next:\n}\n
    ", "tags": ["continue"]}, {"location": "course-c/15-fundations/control-structures/#break", "title": "break", "text": "

    Le mot-cl\u00e9 break peut \u00eatre utilis\u00e9 dans une boucle ou dans un switch. Il permet d'interrompre l'ex\u00e9cution de la boucle ou de la structure switch la plus proche. Nous avions d\u00e9j\u00e0 \u00e9voqu\u00e9 l'utilisation dans un switch (c.f. switch).

    ", "tags": ["switch", "break"]}, {"location": "course-c/15-fundations/control-structures/#return", "title": "return", "text": "

    Le mot cl\u00e9 return suivi d'une valeur de retour ne peut appara\u00eetre que dans une fonction dont le type de retour n'est pas void. Ce mot-cl\u00e9 permet de stopper l'ex\u00e9cution d'une fonction et de retourner \u00e0 son point d'appel.

    void unlock(int password)\n{\n    static tries = 0;\n\n    if (password == 4710 /* MacGuyver: A Retrospective 1986 */) {\n        open_door();\n        tries = 0;\n        return;\n    }\n\n    if (tries++ == 3)\n    {\n        alert_security_guards();\n    }\n}\n
    ", "tags": ["void", "return"]}, {"location": "course-c/15-fundations/control-structures/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 8\u2009: Faute d'erreur

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    long i = 0;\ndouble x = 100.0;\n

    Indiquer la nature de l'erreur dans les expressions suivantes\u2009:

    1.

    do\n    x = x / 2.0;\n    i++;\nwhile (x > 1.0);\n
    2.
    if (x = 0)\n    printf(\"0 est interdit !\\n\");\n
    3.
    switch(x) {\n    case 100 :\n        printf(\"Bravo.\\n\");\n        break;\n    default :\n        printf(\"Pas encore.\\n\");\n\n}\n
    4.
    for (i = 0 ; i < 10 ; i++);\n    printf(\"%d\\n\", i);\n
    5.
    while i < 100 {\n    printf(\"%d\", ++i);\n}\n

    Exercice 9\u2009: Cas appropri\u00e9s

    Parmi les cas suivants, quelle structure de contr\u00f4le utiliser\u2009?

    1. Test qu'une variable est dans un intervalle donn\u00e9.
    2. Actions suivant un choix multiple de l'utilisateur
    3. Rechercher un caract\u00e8re particulier dans une cha\u00eene de caract\u00e8re
    4. It\u00e9rer toutes les valeurs paires sur un intervalle donn\u00e9
    5. Demander la ligne suivante du t\u00e9l\u00e9gramme \u00e0 l'utilisateur jusqu'\u00e0 STOP
    Solution
    1. Le cas est circonscrit \u00e0 un intervalle de valeur donn\u00e9e, le if est appropri\u00e9\u2009:

      if (i > min && i < max) { /* ... */ }\n
    2. Dans ce cas un switch semble le plus appropri\u00e9

      switch(choice) {\n    case 0 :\n        /* ... */\n        break;\n    case 1 :\n        /* ... */\n}\n
    3. \u00c0 reformuler tant que le caract\u00e8re n'est pas trouv\u00e9 ou que la fin de la cha\u00eene n'est pas atteinte. On se retrouve donc avec une boucle \u00e0 deux conditions de sorties.

      size_t pos;\nwhile (pos < strlen(str) && str[pos] != c) {\n    pos++;\n}\nif (pos == strlen(str)) {\n    // Not found\n} else {\n    // Found `c` in `str` at position `pos`\n}\n
    4. La boucle for semble ici la plus adapt\u00e9e

      for (size_t i = 100; i < 200; i += 2) {\n    /* ... */\n}\n
    5. Il est n\u00e9cessaire ici d'assurer au moins un tour de boucle\u2009:

      const size_t max_line_length = 64;\nchar format[32];\nsnprintf(format, sizeof(format), \"%%%zus\", max_line_length - 1);\nunsigned int line = 0;\nchar buffer[max_lines][max_line_length];\ndo {\n    printf(\"%d. \", line);\n} while (\n    scanf(format, buffer[line]) == 1 &&\n    strcmp(buffer[line], \"STOP\") &&\n    ++line < max_lines\n);\n

    Exercice 10\u2009: Comptons sur les caract\u00e8res

    Un texte est pass\u00e9 \u00e0 un programme par stdin. Comptez le nombre de caract\u00e8res transmis.

    $ echo \"hello world\" | count-this\n11\n

    Exercice 11\u2009: Esperluette conditionnelle

    Quel est le probl\u00e8me avec cette ligne de code\u2009?

    if (x&mask==bits)\n
    Solution

    La priorit\u00e9 de l'op\u00e9rateur unitaire & est plus \u00e9lev\u00e9e que == ce qui se traduit par\u2009:

    if (x & (mask == bits))\n

    Le d\u00e9veloppeur voulait probablement appliquer le masque \u00e0 x puis le comparer au motif bits. La bonne r\u00e9ponse devrait alors \u00eatre\u2009:

    if ((x & mask) == bits)\n
    ", "tags": ["stdin", "for", "switch", "STOP", "bits"]}, {"location": "course-c/15-fundations/darkside/", "title": "Le c\u00f4t\u00e9 obscure", "text": ""}, {"location": "course-c/15-fundations/darkside/#shadowy-escape", "title": "Shadowy escape", "text": "

    Exercice 1\u2009: Shadowy escape

    Que retourne la fonction f ?

    ```c

    Exercice 2\u2009: Curieux entier

    Que vaut x

    int32_t x = '0123';\n
    • Erreur de compilation
    • 123
    • 0x30313233
    • 4817243
    • Warning multi-character character constant

    Exercice 3\u2009: Vilains crochets

    Que vaut x

    char x = \"hello\"[4];\n
    • Erreur de compilation
    • 'o'
    • 'h'
    • 0

    Exercice 4\u2009: La r\u00e8gle gourmande

    Que vaut x

    int x = 0;\nx = x+++x++;\n
    • 0
    • 1
    • 2
    • 3
    Solution

    L'expression x+++x++ est \u00e9quivalente \u00e0 x++ + x++. L'op\u00e9rateur ++ est associatif \u00e0 droite, donc x++ est \u00e9valu\u00e9 en premier, puis x++ est \u00e9valu\u00e9. x est incr\u00e9ment\u00e9 deux fois, donc x vaut 2. On parle de greedy lexer rule. Le parseur est gourmand et prend le plus grand nombre de caract\u00e8res possible pour former un jeton.

    Cela ne fonctionne pas \u00e0 tous les coups. L'expression x+++++y est invalide car l'op\u00e9rateur ++ ne peut pas \u00eatre suivi d'un autre op\u00e9rateur ++. Il est n\u00e9cessaire dans ce cas de s\u00e9parer les op\u00e9rateurs par des espaces pour que le code soit valide\u2009: x+++ ++y.

    Exercice 5\u2009: Collision globale

    Qu'affiche le programme\u2009?

    a.c
    int x = 42;\n
    b.c
    int x;\n{\n    printf(\"%d\\n\", x);\n}\n
    $ gcc a.c b.c && ./a.out\n
    • 42
    • 0
    • Ind\u00e9fini
    • Erreur de compilation
    "}, {"location": "course-c/15-fundations/darkside/#include", "title": "include", "text": "

    int x = 42\u2009; int f() { int x = 23\u2009; { extern int x\u2009; return x\u2009; } }

    • 42
    • 23
    • 0
    • 1
    "}, {"location": "course-c/15-fundations/datatype/", "title": "Types de donn\u00e9es", "text": "Well-typed programs don\u2019t go wrong.Robin Milner

    Inh\u00e9rent au fonctionnement interne d\u2019un ordinateur, un langage de programmation op\u00e8re \u00e0 un certain degr\u00e9 d\u2019abstraction par rapport au mode de stockage des donn\u00e9es dans la m\u00e9moire. De la m\u00eame fa\u00e7on qu\u2019il est impossible, dans la vie quotidienne, de rendre la monnaie \u00e0 une fraction de centime pr\u00e8s, un ordinateur ne peut enregistrer des informations num\u00e9riques avec une pr\u00e9cision infinie. Ce principe est intrins\u00e8que aux limites mat\u00e9rielles et au mod\u00e8le math\u00e9matique des nombres.

    Les langages de programmation se divisent ainsi en deux grandes cat\u00e9gories\u2009: ceux que l\u2019on qualifie de typ\u00e9s, o\u00f9 le programmeur a la charge explicite de d\u00e9finir la mani\u00e8re dont les donn\u00e9es seront stock\u00e9es, et ceux dits non typ\u00e9s, o\u00f9 ce choix est g\u00e9r\u00e9 implicitement. Chaque approche pr\u00e9sente des avantages et des inconv\u00e9nients. Reprenons l\u2019exemple du rendu de monnaie\u2009: s\u2019il \u00e9tait possible d\u2019enregistrer des montants avec une pr\u00e9cision sup\u00e9rieure \u00e0 celle des pi\u00e8ces en circulation, disons \u00e0 la fraction de centime, cela poserait probl\u00e8me pour qu\u2019un caissier puisse rendre la monnaie correctement. Dans de telles situations, un langage typ\u00e9 s\u2019av\u00e8re plus adapt\u00e9, car il permet de fixer des bornes pertinentes \u00e0 la pr\u00e9cision des donn\u00e9es. C, en ce sens, est un langage fortement typ\u00e9, ce qui convient particuli\u00e8rement \u00e0 la manipulation rigoureuse des donn\u00e9es financi\u00e8res, entre autres.

    Il convient de noter que les types de donn\u00e9es ne se limitent pas aux seules informations num\u00e9riques. On trouve des types plus \u00e9labor\u00e9s, capables de repr\u00e9senter des caract\u00e8res individuels comme A ou B, ou m\u00eame des structures plus complexes. Ce chapitre a pour vocation de familiariser le lecteur avec les diff\u00e9rents types de donn\u00e9es disponibles en C et leur utilisation optimale.

    Standard ISO 80000-2

    Les ing\u00e9nieurs ont une pr\u00e9dilection marqu\u00e9e pour les standards, et cela d'autant plus lorsqu\u2019ils sont de port\u00e9e internationale. Pour pr\u00e9venir des erreurs aussi regrettables que le crash d'une fus\u00e9e d\u00fb \u00e0 une incompr\u00e9hension entre deux ing\u00e9nieurs de nations diff\u00e9rentes, il existe des normes telles que l'ISO 80000-2, qui d\u00e9finit avec rigueur ce que l'on entend par un entier (incluant ou non le z\u00e9ro), la nature des nombres r\u00e9els, et bien d'autres concepts math\u00e9matiques fondamentaux. Il va sans dire que les compilateurs, lorsqu\u2019ils sont correctement con\u00e7us, s'efforcent de respecter ces normes internationales au plus pr\u00e8s. Et vous, en tant que d\u00e9veloppeur, faites-vous de m\u00eame\u2009?

    ", "tags": ["iso-80000-2"]}, {"location": "course-c/15-fundations/datatype/#stockage-et-interpretation", "title": "Stockage et interpr\u00e9tation", "text": "

    Ancrez-vous \u00e7a bien dans la cabosse\u2009: un ordinateur ne peut stocker l'information que sous forme binaire et il ne peut manipuler ces informations que par paquets d'octets.

    Un ordinateur 64-bits manipulera avec aisance des paquets de 64-bits, mais plus difficilement des paquets de 32-bits. Un microcontr\u00f4leur 8-bit devra quant \u00e0 lui faire plusieurs manipulations pour lire une donn\u00e9e 32-bits. Il est donc important par souci d'efficacit\u00e9 d'utiliser la taille appropri\u00e9e \u00e0 la quantit\u00e9 d'information que l'on souhaite stocker.

    Quant \u00e0 la repr\u00e9sentation, consid\u00e9rons le paquet de 32-bit suivant, \u00eates-vous \u00e0 m\u00eame d'en donner une signification\u2009?

    01000000 01001001 00001111 11011011\n

    Il y a une infinit\u00e9 d'interpr\u00e9tations possibles, mais voici quelques pistes les plus probables\u2009:

    1. 4 caract\u00e8res de 8-bits\u2009: 01000000 @, 01001001 I, 00001111 \\x0f et 11011011 \u00db.
    2. 4 nombres de 8-bits\u2009: 64, 73, 15, 219.
    3. Deux nombres de 16-bits 18752 et 56079.
    4. Un seul nombre de 32-bit 3675212096.
    5. Peut-\u00eatre le nombre -40331460896358400.000000 lu en little endian.
    6. Ou encore 3.141592 lu en big endian.

    Qu'en pensez-vous\u2009?

    Lorsque l'on souhaite programmer \u00e0 bas niveau, vous voyez que la notion de type de donn\u00e9e est essentielle, car en dehors d'une interpr\u00e9tation subjective\u2009: \u00ab\u2009c'est forc\u00e9ment PI la bonne r\u00e9ponse\u2009\u00bb, rien ne permet \u00e0 l'ordinateur d'interpr\u00e9ter convenablement l'information enregistr\u00e9e en m\u00e9moire. Le typage permet de r\u00e9soudre toute ambigu\u00eft\u00e9.

    \u00c0 titre d'exemple, le programme suivant reprend notre question pr\u00e9c\u00e9dente et affiche les diff\u00e9rentes interpr\u00e9tations possibles selon diff\u00e9rents types de donn\u00e9es du langage C. Vous n'avez pas encore vu tous les \u00e9l\u00e9ments pour comprendre ce programme, mais vous pouvez d\u00e9j\u00e0 en deviner le sens et surtout vous pouvez d\u00e9j\u00e0 essayer de l'ex\u00e9cuter pour voir si vos hypoth\u00e8ses \u00e9taient correctes.

    int main() {\n    union {\n        uint8_t u8[4];\n        uint16_t u16[2];\n        uint32_t u32;\n        float f32;\n    } u = { 0b01000000, 0b01001001, 0b00001111, 0b11011011 };\n\n    printf(\"'%c', '%c', '%c', '%c'\\n\", u.u8[0], u.u8[1], u.u8[2], u.u8[3]);\n    printf(\"%hhu, %hhu, %hhu, %hhu\\n\", u.u8[0], u.u8[1], u.u8[2], u.u8[3]);\n    printf(\"%hu, %hu\\n\", u.u16[0], u.u16[1]);\n    printf(\"%u\\n\", u.u32);\n    printf(\"%f\\n\", u.f32);\n    u.u32 = (\n        ((u.u32 >> 24) & 0xff) | // move byte 3 to byte 0\n        ((u.u32 << 8) & 0xff0000) | // move byte 1 to byte 2\n        ((u.u32 >> 8) & 0xff00) | // move byte 2 to byte 1\n        ((u.u32 << 24) & 0xff000000) // byte 0 to byte 3\n    );\n    printf(\"%f\\n\", u.f32);\n}\n

    "}, {"location": "course-c/15-fundations/datatype/#boutisme", "title": "Boutisme", "text": "

    Boutisme par J. J. Grandville (1838)

    La hantise de l\u2019ing\u00e9nieur bas-niveau, c\u2019est le concept de boutisme, ou endianess en anglais. Ce terme, popularis\u00e9 par l\u2019informaticien Danny Cohen, fait r\u00e9f\u00e9rence au livre Les Voyages de Gulliver de Jonathan Swift. Dans cette satire, les habitants de Lilliput se divisent en deux factions\u2009: ceux qui mangent leurs \u0153ufs \u00e0 la coque en commen\u00e7ant par le petit bout (les Little Endians) et ceux qui pr\u00e9f\u00e8rent le gros bout (les Big Endians), engendrant un conflit absurde.

    En informatique, cette question, loin d\u2019\u00eatre triviale, persiste dans le monde des microprocesseurs. Certains fonctionnent en big endian, o\u00f9 les octets sont stock\u00e9s en m\u00e9moire du plus significatif au moins significatif, tandis que d'autres adoptent le format little endian, inversant cet ordre. Imaginons qu\u2019une donn\u00e9e soit enregistr\u00e9e en m\u00e9moire ainsi\u2009:

    [0x40, 0x49, 0x0F, 0xDB]\n

    Doit-on lire ces octets de gauche \u00e0 droite (comme en big endian) ou de droite \u00e0 gauche (comme en little endian) ? Ce probl\u00e8me, bien qu\u2019il semble anodin, devient crucial dans des contextes internationaux. Si ce texte \u00e9tait \u00e9crit en arabe, une langue lue de droite \u00e0 gauche, votre perception pourrait \u00eatre diff\u00e9rente.

    Prenons un exemple plus concret. Un microcontr\u00f4leur big endian 8 bits envoie via Bluetooth la valeur 1'111'704'645 \u2013 repr\u00e9sentant, par exemple, le nombre de photons d\u00e9tect\u00e9s par un capteur optique. Il transmet les octets suivants\u2009: 0x42, 0x43, 0x44, 0x45. Cependant, l\u2019ordinateur qui re\u00e7oit ces donn\u00e9es en mode little endian interpr\u00e8te cette s\u00e9quence comme 1'162'101'570. Ce d\u00e9calage dans la lecture est un probl\u00e8me courant auquel les ing\u00e9nieurs en \u00e9lectronique se heurtent fr\u00e9quemment dans leur carri\u00e8re.

    Le boutisme intervient donc dans la mani\u00e8re de stocker et transmettre des donn\u00e9es, chaque approche ayant ses avantages et inconv\u00e9nients. Personnellement, en mati\u00e8re d\u2019\u0153ufs, je pr\u00e9f\u00e8re le gros boutisme : je trouve qu\u2019il est plus pratique de manger un \u0153uf \u00e0 la coque en le commen\u00e7ant par le gros bout. En informatique, cependant, les arguments des deux camps se valent, et les choix d\u00e9pendent souvent des exigences du syst\u00e8me.

    Pour mieux illustrer ce concept, prenons un exemple en base 10, plus accessible. Imaginez que je doive transmettre un nombre, comme 532, par un tuyau dans lequel une seule boule peut passer \u00e0 la fois, chaque boule repr\u00e9sentant un chiffre. Dois-je envoyer la boule marqu\u00e9e 5, puis 3, puis 2 ? Ou devrais-je commencer par la boule marqu\u00e9e 2 et terminer par celle portant le 5 ? Dans notre culture, nous lisons de gauche \u00e0 droite, mais lorsque les donn\u00e9es sont stock\u00e9es dans un nombre fixe de bits, comme c\u2019est le cas en informatique, les deux m\u00e9thodes se justifient.

    Par exemple, si je vous transmets le nombre 7 et vous dis qu'il est inf\u00e9rieur \u00e0 10, en big endian, vous devrez attendre deux boules suppl\u00e9mentaires (0, 0, 7), tandis qu\u2019en little endian, vous saurez imm\u00e9diatement que c'est 7 (7, 0, 0). C\u2019est cette raison de simplicit\u00e9 dans la gestion des petites valeurs qui a largement contribu\u00e9 \u00e0 la popularit\u00e9 du little endian dans les syst\u00e8mes modernes.

    Techniquement parlant, voici la repr\u00e9sentation en m\u00e9moire de trois entiers 32 bits\u2009:

    16909060, 42, 10000\n01 02 03 04  00 00 00 2a  00 00 27 10 (big endian)\n04 03 02 01  2a 00 00 00  10 27 00 00 (little endian)\n

    Ainsi, le boutisme n'est pas qu'un d\u00e9tail technique\u2009: c'est une question cruciale pour l'interop\u00e9rabilit\u00e9 des syst\u00e8mes num\u00e9riques.

    Organisation par type

    Selon le boutisme, ce n'est pas toute l'information qui est invers\u00e9e, mais l'ordre des octets au sein de chaque nombre.

    R\u00e9seau informatique

    Le r\u00e9seau informatique comme les protocoles TCP/IP, UDP, Wi-Fi utilisent le network byte order qui impose le big endian pour l'envoi des donn\u00e9es. Cela remonte aux premi\u00e8res normes dont l'id\u00e9e \u00e9tait d'adopter une convention unique et standardis\u00e9e \u00e0 une \u00e9poque ou les ordinateurs big endian \u00e9taient majoritaires. Or aujourd'hui la tr\u00e8s vaste majorit\u00e9 des ordinateurs sont en little endian et donc les donn\u00e9es transmises et r\u00e9ceptionn\u00e9es doivent \u00eatre converties. C'est le r\u00f4le de la fonction htonl (host to network long) qui convertit un entier 32 bits en big endian ou ntohl (network to host long) qui fait l'op\u00e9ration inverse.

    Pour vous lecteurs, cela n'a pas de grande importance, car nous n'allons pas approfondir le fonctionnement du r\u00e9seau informatique dans cet ouvrage.

    ", "tags": ["htonl", "ntohl"]}, {"location": "course-c/15-fundations/datatype/#les-nombres-entiers", "title": "Les nombres entiers", "text": "

    Les nombres entiers que nous avons d\u00e9finis plus t\u00f4t peuvent \u00eatre n\u00e9gatifs, nuls ou positifs. En C, il existe plusieurs types de donn\u00e9es pour les repr\u00e9senter, chacun ayant ses propres caract\u00e9ristiques.

    "}, {"location": "course-c/15-fundations/datatype/#les-entiers-naturels", "title": "Les entiers naturels", "text": "

    En informatique, les entiers naturels de l'ensemble \\(\\mathbb{N}\\) sont non sign\u00e9s, et peuvent prendre des valeurs comprises entre \\(0\\) et \\(2^N-1\\) o\u00f9 \\(N\\) correspond au nombre de bits avec lesquels la valeur num\u00e9rique sera stock\u00e9e en m\u00e9moire. Il faut naturellement que l'ordinateur sur lequel s'ex\u00e9cute le programme soit capable de supporter le nombre de bits demand\u00e9 par le programmeur. En C, on nomme ce type de donn\u00e9e unsigned int, int \u00e9tant le d\u00e9nominatif du latin integer signifiant \u00ab\u2009entier\u2009\u00bb.

    Voici quelques exemples des valeurs minimales et maximales possibles selon le nombre de bits utilis\u00e9s pour coder l'information num\u00e9rique\u2009:

    Stockage d'un entier non sign\u00e9 sur diff\u00e9rentes profondeurs Profondeur Minimum Maximum 8 bits 0 255 (\\(2^8 - 1\\)) 16 bits 0 65'535 (\\(2^{16} - 1\\)) 32 bits 0 4'294'967'295 (\\(2^{32} - 1\\)) 64 bits 0 18'446'744'073'709'551'616 (\\(2^{64} - 1\\))

    Notez l'importance du \\(-1\\) dans la d\u00e9finition du maximum, car la valeur minimum \\(0\\) fait partie de l'information m\u00eame si elle repr\u00e9sente une quantit\u00e9 nulle. Il y a donc 256 valeurs possibles pour un nombre entier non sign\u00e9 8-bits, bien que la valeur maximale ne soit que de 255.

    ", "tags": ["int"]}, {"location": "course-c/15-fundations/datatype/#les-entiers-bornes-signes", "title": "Les entiers born\u00e9s sign\u00e9s", "text": "

    Les entiers sign\u00e9s peuvent \u00eatre n\u00e9gatifs, nuls ou positifs et peuvent prendre des valeurs comprises entre \\(-2^{N-1}\\) et \\(+2^{N-1}-1\\) o\u00f9 \\(N\\) correspond au nombre de bits avec lesquels la valeur num\u00e9rique sera stock\u00e9e en m\u00e9moire. Notez l'asym\u00e9trie entre la borne positive et n\u00e9gative.

    Comme il sont sign\u00e9s (signed en anglais), il est par cons\u00e9quent correct d'\u00e9crire signed int bien que le pr\u00e9fixe signed soit optionnel, car le standard d\u00e9finit qu'un entier est par d\u00e9faut sign\u00e9. La raison \u00e0 cela rel\u00e8ve plus du lourd historique de C qu'\u00e0 des pr\u00e9ceptes logiques et rationnels.

    Voici quelques exemples de valeurs minimales et maximales selon le nombre de bits utilis\u00e9s pour coder l'information\u2009:

    Stockage d'un entier sign\u00e9 sur diff\u00e9rentes profondeurs Profondeur Minimum Maximum 8 bits -128 +127 16 bits -32'768 +32'767 32 bits -2'147'483'648 +2'147'483'647 64 bits -9'223'372'036'854'775'808 +9'223'372'036'854'775'807

    En m\u00e9moire, ces nombres sont stock\u00e9s en utilisant le compl\u00e9ment \u00e0 deux que nous avons d\u00e9j\u00e0 \u00e9voqu\u00e9.

    ", "tags": ["signed"]}, {"location": "course-c/15-fundations/datatype/#les-entiers-bornes", "title": "Les entiers born\u00e9s", "text": "

    Comme nous l'avons vu, les degr\u00e9s de libert\u00e9 pour d\u00e9finir un entier sont\u2009:

    • Sign\u00e9 ou non sign\u00e9
    • Nombre de bits avec lesquels l'information est stock\u00e9e en m\u00e9moire

    \u00c0 l'origine le standard C restait flou quant au nombre de bits utilis\u00e9s pour chacun des types et aucune r\u00e9elle coh\u00e9rence n'existait pour la construction d'un type. Le modificateur signed \u00e9tait optionnel, le pr\u00e9fixe long ne pouvait s'appliquer qu'au type int et long et la confusion entre long (pr\u00e9fixe) et long (type) restait possible. En fait, la plupart des d\u00e9veloppeurs s'y perdaient et s'y perd toujours ce qui menait \u00e0 des probl\u00e8mes de compatibilit\u00e9s des programmes entre eux.

    ", "tags": ["signed", "int", "long"]}, {"location": "course-c/15-fundations/datatype/#types-standards", "title": "Types standards", "text": "

    La construction d'un type entier C peut \u00eatre r\u00e9sum\u00e9e par la figure suivante\u2009:

    Entiers standardis\u00e9s

    Le pr\u00e9fixe signed est implicite, mais il est possible de l'utiliser pour plus de clart\u00e9. En pratique il sera rarement utilis\u00e9. De m\u00eame, lorsque short, long ou long long est utils\u00e9, le suffixe int est implicite.

    Les types suivants sont donc des synonymes\u2009:

    // Entier sign\u00e9\nsigned int, int, signed\n\n// Entier non sign\u00e9\nunsigned int, unsigned\n\n// Entier court sign\u00e9\nsigned short int, short, signed short\n\n// Entier court non sign\u00e9\nunsigned short int, unsigned short\n\n// Entier long sign\u00e9\nsigned long int, long, long int, signed long\n\n// Entier tr\u00e8s long sign\u00e9\nsigned long long int, long long, signed long long\n

    En revanche char est un type \u00e0 part enti\u00e8re sign\u00e9 par d\u00e9faut qui n'aura pas de suffixe int, mais il est possible de le d\u00e9clarer unsigned char.

    Ci-dessous la table des entiers standards en C. Le format est celui utilis\u00e9 par la fonction printf de la biblioth\u00e8que standard C.

    Table des types entiers en C Type Signe Profondeur Format char, signed char signed au moins 8 bits %c unsigned char unsigned au moins 8 bits %uc short, short int, ... signed au moins 16 bits %hi unsigned short, ... unsigned au moins 16 bits %hu unsigned, unsigned int unsigned au moins 32 bits %u int, signed, signed int signed au moins 32 bits %d unsigned, unsigned int, ... unsigned au moins 32 bits %u long, long int, ... signed au moins 32 bits %li unsigned long, .. unsigned au moins 32 bits %lu long long, ... signed au moins 64 bits %lli unsigned long long, ... unsigned au moins 64 bits %llu

    Avec l'av\u00e8nement de C99, une meilleure coh\u00e9sion des types a \u00e9t\u00e9 propos\u00e9e dans le fichier d'en-t\u00eate stdint.h. Cette biblioth\u00e8que standard offre les types suivants\u2009:

    Flux de construction d'un entier standardis\u00e9

    ", "tags": ["int", "unsigned", "stdint.h", "short", "printf", "long", "signed", "char"]}, {"location": "course-c/15-fundations/datatype/#nouveaux-types-standard", "title": "Nouveaux types standard", "text": "

    Avec l'av\u00e8nement de C99, une meilleure coh\u00e9sion des types a \u00e9t\u00e9 propos\u00e9e dans le fichier d'en-t\u00eate stdint.h. Cette biblioth\u00e8que standard offre les types suivants. Comme nous l'avons vu, la taille des types historiques n'est pas pr\u00e9cis\u00e9ment d\u00e9finie par le standard. On sait qu'un int contient au moins 16-bits, mais il peut, selon l'architecture, et aussi le mod\u00e8le de donn\u00e9e, prendre n'importe quelle valeur sup\u00e9rieure. Ceci pose des probl\u00e8mes de portabilit\u00e9 possibles si le d\u00e9veloppeur n'est pas suffisamment consciencieux et qu'il ne s'appuie pas sur une batterie de tests automatis\u00e9s. En cons\u00e9quence, il est recommand\u00e9 d'utiliser les types de <stdint.h> lorsque la taille du type doit \u00eatre garantie.

    Attention cependant \u00e0 noter que garantir un type \u00e0 taille fixe n'est pas toujours la meilleure solution. En effet, si vous avez besoin d'un entier de 32-bits, il est pr\u00e9f\u00e9rable d'utiliser int qui sera adapt\u00e9 \u00e0 l'architecture mat\u00e9rielle. Si vous utilisez int32_t vous risquez de perdre en performance si l'architecture mat\u00e9rielle est capable de traiter des entiers 64-bits de mani\u00e8re plus efficace. Voici les types \u00e0 taille fixe de <stdint.h> :

    Entiers standard d\u00e9fini par stdint Type Signe Profondeur Format uint8_t unsigned 8 bits %c int8_t signed 8 bits %c uint16_t unsigned 16 bits %hu int16_t signed 16 bits %hi uint32_t unsigned 32 bits %u int32_t signed 32 bits %d uint64_t unsigned 64 bits %llu int64_t signed 64 bits %lli

    \u00c0 ces types s'ajoutent les types rapides (fast) et minimums (least). Un type nomm\u00e9 uint_least32_t garanti l'utilisation du type de donn\u00e9e utilisant le moins de m\u00e9moire et garantissant une profondeur d'au minimum 32 bits. Les types rapides, moins utilis\u00e9s, vont automatiquement choisir le type adapt\u00e9 le plus rapide \u00e0 l'ex\u00e9cution. Par exemple, si l'architecture mat\u00e9rielle permet un calcul natif sur 48-bits, elle sera privil\u00e9gi\u00e9e par rapport au type 32-bits.

    Exercice 1\u2009: D\u00e9bordement

    Quel sera le contenu de j apr\u00e8s l'ex\u00e9cution de l'instruction suivante\u2009:

    uint16_t j = 1024 * 64;\n
    • 0
    • 1
    • 64
    • 1024
    • 65536

    ", "tags": ["int", "uint16_t", "uint_least32_t", "int8_t", "int64_t", "uint8_t", "int16_t", "int32_t", "uint32_t", "uint64_t", "stdint.h"]}, {"location": "course-c/15-fundations/datatype/#modele-de-donnee", "title": "Mod\u00e8le de donn\u00e9e", "text": "

    Comme nous l'avons \u00e9voqu\u00e9 plus haut, la taille des entiers short, int, ... n'est pas pr\u00e9cis\u00e9ment d\u00e9finie par le standard. On sait qu'un int contient au moins 16-bits, mais il peut, selon l'architecture, et aussi le mod\u00e8le de donn\u00e9e, prendre n'importe quelle valeur sup\u00e9rieure.

    Admettons que ce d\u00e9veloppeur sans scrupule d\u00e9veloppe un programme complexe sur sa machine 64-bits en utilisant un int comme valeur de comptage allant au-del\u00e0 de dix milliards. Apr\u00e8s tests, son programme fonctionne sur sa machine, ainsi que celle de son coll\u00e8gue. Mais lorsqu'il livre le programme \u00e0 son client, le processus crash. En effet, la taille du int sur l'ordinateur du client est de 32-bits. Comment peut-on s'affranchir de ce type de probl\u00e8me\u2009?

    La premi\u00e8re solution est de toujours utiliser les types propos\u00e9s par <stdint.h> lorsque la taille du type n\u00e9cessaire est sup\u00e9rieure \u00e0 la valeur garantie. L'autre solution est de se fier au mod\u00e8le de donn\u00e9es. Le mod\u00e8le de donn\u00e9es est une convention qui d\u00e9finit la taille des types de donn\u00e9es de base. Il est d\u00e9termin\u00e9 par l'architecture mat\u00e9rielle et le syst\u00e8me d'exploitation. Voici un tableau r\u00e9sumant les mod\u00e8les de donn\u00e9es les plus courants\u2009:

    Mod\u00e8le de donn\u00e9es Mod\u00e8le short int long long long size_t Syst\u00e8me d'exploitation LP32 16 16 32 32 Windows 16-bits, Apple Macintosh ILP32 16 32 32 64 32 Windows x86, Linux 32-bits LLP64 16 32 32 64 64 Microsoft Windows x86-64 LP64 16 32 64 64 64 Unix, Linux, macOS ILP64 16 64 64 64 64 HAL (SPARC) SILP64 64 64 64 64 64 UNICOS (Super ordinateur)

    Taille usuelle des types de base Type Taille Windows Linux char habituellement 8 bits 1 1 short au moins 16 bits 2 2 int taille naturelle pour l'architecture 4 4 long au moins 32 bits 4 8 long long au moins 64 bits 8 8 float normalement 32 bits 4 4 double normalement 64 bits 8 8 long double au moins 63 bits 8 16

    Les troisi\u00e8me et quatri\u00e8me colonnes repr\u00e9sentent la taille des types de base sur des machines modernes 64-bits. On notera que la taille des types long et long double varie selon l'architecture mat\u00e9rielle et le syst\u00e8me d'exploitation. On voit donc que selon le mod\u00e8le les types n'ont pas la m\u00eame taille et donc que la portabilit\u00e9 des programmes est un enjeu majeur. Aussi, pour s'assurer qu'un type est de la taille souhait\u00e9e, il est recommand\u00e9 d'utiliser les nouveaux types standards de <stdint.h>. Ainsi pour s'assurer qu'un type soit au moins de 32-bits, on utilisera uint_least32_t.

    ", "tags": ["int", "uint_least32_t", "short", "long"]}, {"location": "course-c/15-fundations/datatype/#les-caracteres", "title": "Les caract\u00e8res", "text": "

    Les caract\u00e8res, ceux que vous voyez dans cet ouvrage, sont g\u00e9n\u00e9ralement repr\u00e9sent\u00e9s par des grandeurs exprim\u00e9es sur 1 octet (8-bits):

    97 \u2261 0b1100001 \u2261 'a'\n

    Un caract\u00e8re du clavier enregistr\u00e9 en m\u00e9moire c'est donc un nombre entier de 8-bits. En C, le type de donn\u00e9e char est utilis\u00e9 pour stocker un caract\u00e8re.

    Mais comment un ordinateur sait-il que 97 correspond \u00e0 a ? C'est l\u00e0 que la notion d'encodage entre en jeu.

    ", "tags": ["char"]}, {"location": "course-c/15-fundations/datatype/#la-table-ascii", "title": "La table ASCII", "text": "

    Historiquement, alors que les informations dans un ordinateur ne sont que des 1 et des 0, il a fallu \u00e9tablir une correspondance entre une grandeur binaire et le caract\u00e8re associ\u00e9. Un standard a \u00e9t\u00e9 propos\u00e9 en 1963 par l'ASA (American Standards Association) aujourd'hui ANSI qui ne d\u00e9finissait alors que 63 caract\u00e8res imprimables. Comme la m\u00e9moire \u00e0 cette \u00e9poque \u00e9tait tr\u00e8s cher, un caract\u00e8re n'\u00e9tait cod\u00e9 que sur 7 bits. La premi\u00e8re table ASCII d\u00e9finissait donc 128 caract\u00e8res et est donn\u00e9e par la figure suivante\u2009:

    Table ASCII ASA X3.4 \u00e9tablie en 1963

    En 1986, la table ASCII a \u00e9t\u00e9 \u00e9tendue pour couvrir les caract\u00e8res majuscules et minuscules. Cette r\u00e9forme est donn\u00e9e par la figure suivante. Il s'agit de la table ASCII standard actuelle.

    Table ANSI INCITS 4-1986 (standard actuel)

    Ainsi qu'\u00e9voqu\u00e9 plusieurs fois dans cet ouvrage, chaque pays et chaque langue utilise ses propres caract\u00e8res et il a fallu trouver un moyen de satisfaire tout le monde. Il a \u00e9t\u00e9 alors convenu d'encoder les caract\u00e8res sur 8-bits au lieu de 7 et de profiter des 128 nouvelles positions offertes pour ajouter les caract\u00e8res manquants telles que les caract\u00e8res accentu\u00e9s, le signe euro, la livre sterling et d'autres. Le standard ISO/IEC 8859 aussi appel\u00e9 standard Latin d\u00e9finit 16 tables d'extension selon les besoins des pays. Les plus courantes en Europe occidentale sont les tables ISO-8859-1 ou (latin1) et ISO-8859-15 (latin9). Voici la table d'extension de l'ISO-8859-1 et de l'ISO-8859-15 :

    Table d'extension ISO-8859-1 (haut) et ISO-8859-15 (bas)

    Ce standard a g\u00e9n\u00e9r\u00e9 durant des d\u00e9cennies de grandes frustrations et de profondes incompr\u00e9hensions chez les d\u00e9veloppeurs et utilisateurs d'ordinateur. Ne vous est-il jamais arriv\u00e9 d'ouvrir un fichier texte et de ne plus voir les accents convenablement\u2009? C'est un probl\u00e8me typique d'encodage.

    Pour tenter de rem\u00e9dier \u00e0 ce standard incompatible entre les pays, Microsoft a propos\u00e9 un standard nomm\u00e9 Windows-1252 s'inspirant de ISO-8859-1. En voulant rassembler en proposant un standard plus g\u00e9n\u00e9ral, Microsoft n'a contribu\u00e9 qu'\u00e0 proposer un standard suppl\u00e9mentaire venant s'inscrire dans une liste d\u00e9j\u00e0 trop longue. Et l'histoire n'est pas termin\u00e9e...

    C'est pourquoi, en 1991, l'ISO a propos\u00e9 un standard universel nomm\u00e9 Unicode qui est capable d'encoder tous les caract\u00e8res de toutes les langues du monde.

    ", "tags": ["iso-8859-1", "ascii", "iso-8859-15"]}, {"location": "course-c/15-fundations/datatype/#unicode", "title": "Unicode", "text": "

    Avec l'arriv\u00e9e d'internet et les \u00e9changes entre les Arabes (\u0639\u064e\u0631\u064e\u0628), les Cor\u00e9ens (\ud55c\uad6d\uc5b4), les Japonais qui poss\u00e8dent deux alphabets ainsi que des caract\u00e8res chinois (\u65e5\u672c\u8a9e), sans oublier l'ourdou (\u067e\u0627\u06a9\u0650\u0633\u062a\u0627\u0646) pakistanais et tous ceux que l'on ne mentionnera pas, il a fallu bien plus que 256 caract\u00e8res et quelques tables de correspondance. Ce pr\u00e9sent ouvrage, ne pourrait d'ailleurs par \u00eatre \u00e9crit sans avoir pu r\u00e9soudre, au pr\u00e9alable, ces probl\u00e8mes d'encodage\u2009; la preuve \u00e9tant, vous parvenez \u00e0 voir ces caract\u00e8res qui ne vous sont pas familiers.

    Un consensus plan\u00e9taire a \u00e9t\u00e9 atteint en 2008 avec l'adoption majoritaire du standard Unicode (Universal Coded Character Set) et son encodage UTF-8 (Unicode Transformation Format). Ce standard est capable d'encoder tous les caract\u00e8res de toutes les langues du monde. Il est utilis\u00e9 par la plupart des syst\u00e8mes d'exploitation, des navigateurs web et des applications informatiques. Il est capable d'encoder 1'112'064 caract\u00e8res en utilisant de 1 \u00e0 4 octets. La figure suivante montre la tendance de l'adoption de 2001 \u00e0 2012. Cette tendance est accessible ici.

    Tendances sur l'encodage des pages web en faveur de UTF-8 d\u00e8s 2001, donn\u00e9es collect\u00e9es par Google et Erik van der Poel

    Ken Thompson, dont nous avons d\u00e9j\u00e0 parl\u00e9 en introduction, est \u00e0 l'origine de ce standard. Par exemple le devanagari caract\u00e8re \u0939 utilis\u00e9 en Sanskrit poss\u00e8de la d\u00e9nomination Unicode 0939 et s'encode sur 3 octets\u2009: 0xE0 0xA4 0xB9

    En programmation C, un caract\u00e8re char ne peut exprimer sans ambig\u00fcit\u00e9 que les 128 caract\u00e8res de la table ASCII standard et selon les conventions locales, les 128 caract\u00e8res d'extension. C'est-\u00e0-dire que vous ne pouvez pas exprimer un caract\u00e8re Unicode en utilisant un char. Pour cela, il faudra utiliser un tableau de caract\u00e8res char ou un tableau de caract\u00e8res wchar_t qui est capable de stocker un caract\u00e8re Unicode, mais nous verrons cela plus tard.

    Voici par exemple comment d\u00e9clarer une variable contenant le caract\u00e8re dollar\u2009:

    char c = '$';\n

    3 ou '3'

    Attention \u00e0 la pr\u00e9sence des guillemets simples car le caract\u00e8re '3' n'est pas \u00e9gal au nombre 3. Le caract\u00e8re 3 correspond selon la table ASCII standard \u00e0 la valeur 0x33 et donc au nombre 51 en d\u00e9cimal.

    #include <stdio.h>\n\nint main(void) {\n    char c = '3';\n    printf(\"Le caract\u00e8re %c vaut 0x%x en hexad\u00e9cimal ou %d en d\u00e9cimal.\\n\",\n        c, c, c);\n    return 0;\n}\n
    ", "tags": ["wchar_t", "char"]}, {"location": "course-c/15-fundations/datatype/#les-emojis", "title": "Les emojis", "text": "

    Les emojis sont des caract\u00e8res sp\u00e9ciaux qui ont \u00e9t\u00e9 introduits en 2010 par le standard Unicode 6.0. Ils sont donc cod\u00e9s sur 4 octets et permettent de repr\u00e9senter des \u00e9motions, des objets, des animaux, des symboles ou des \u00e9trons (\ud83d\udca9).

    Les \u00e9motic\u00f4nes que vous pouvez envoyer \u00e0 votre grand-m\u00e8re via WhatsApp sont donc des caract\u00e8res Unicode et non des images. Si vous dites \u00e0 votre grand-maman que vous l'aimez en lui envoyant un c\u0153ur, elle recevra le caract\u00e8re 2764 qui est le caract\u00e8re \u2764. Mais les navigateurs web et les applications informatiques remplacent \u00e0 la vol\u00e9e ces caract\u00e8res par des images.

    Ceci est vrai, mais encore faut-il que la police d'\u00e9criture utilis\u00e9e par votre ch\u00e8re grand-maman soit capable d'afficher ce caract\u00e8re. Si ce n'est pas le cas, elle verra probablement le caract\u00e8re \ufffd qui est un caract\u00e8re de remplacement tr\u00e8s disgracieux et qui ne d\u00e9montre pas tout l'amour que vous lui portez.

    ", "tags": ["emojis"]}, {"location": "course-c/15-fundations/datatype/#chaine-de-caracteres", "title": "Cha\u00eene de caract\u00e8res", "text": "

    Une cha\u00eene de caract\u00e8res est simplement la suite contigu\u00eb de plusieurs caract\u00e8res dans une zone m\u00e9moire donn\u00e9e. Afin de savoir lorsque cette cha\u00eene se termine, le standard impose que le dernier caract\u00e8re d'une cha\u00eene soit NUL ou \\0. On appelle ce caract\u00e8re le caract\u00e8re de fin de cha\u00eene. Il s'agit d'une sentinelle.

    Les l\u00e9gumes et les choux

    Imaginez que l'on vous demande de vous placer dans un champ et de d\u00e9terrer n'importe quel l\u00e9gume sauf un chou. Votre algorithme est\u2009:

    %% Algorithme de d\u00e9terrage de l\u00e9gumes\nflowchart LR\n    start(D\u00e9but) --> pick[D\u00e9terrer]\n    pick --> if{Choux?}\n    if --Non--> step[Avancer de 1 pas]\n    step --> pick\n    if --Oui--> stop(Fin)
    Algorithme de d\u00e9terrage de l\u00e9gumes

    Si vous trouvez un chou, vous savez que vous \u00eates arriv\u00e9s au bout du champ. Le chou fait office de sentinelle.

    Sans sentinelle, vous \u00eates oblig\u00e9 de conna\u00eetre \u00e0 l'avance le nombre de pas \u00e0 faire pour arriver au bout du champ. Vous devez donc stocker en m\u00e9moire cette information additionnelle ce qui n'est pas pratique.

    La cha\u00eene de caract\u00e8re Hello sera en m\u00e9moire stock\u00e9e en utilisant les codes ASCII suivants.

    char string[] = \"Hello\";\n
      H   E   L   L   O  \\0\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 72\u2502101\u2502108\u2502108\u2502111\u2502 0 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n\n 0x00  0b01001000\n 0x01  0b01100101\n 0x02  0b01101100\n 0x03  0b01101100\n 0x04  0b01101111\n 0x05  0b00000000\n

    On utilise le caract\u00e8re nul \\0 pour plusieurs raisons\u2009:

    1. Il est facilement reconnaissable.
    2. Dans un test il vaut false.
    3. Il n'est pas imprimable.

    Avertissement

    Ne pas confondre le caract\u00e8re nul \\0 avec le caract\u00e8re 0. Le premier est un caract\u00e8re de fin de cha\u00eene, le second est un caract\u00e8re num\u00e9rique qui vaut 0x30. Le caract\u00e8re nul est la valeur 0 selon la table ASCII.

    ", "tags": ["sentinelle", "false", "NUL", "Hello"]}, {"location": "course-c/15-fundations/datatype/#booleens", "title": "Bool\u00e9ens", "text": "

    Un bool\u00e9en est un type de donn\u00e9e \u00e0 deux \u00e9tats consensuellement nomm\u00e9s vrai (true) et faux (false) et destin\u00e9s \u00e0 repr\u00e9senter les \u00e9tats en logique bool\u00e9enne (Nom venant de George Boole, fondateur de l'alg\u00e8bre \u00e9ponyme).

    La convention est d'utiliser 1 pour m\u00e9moriser un \u00e9tat vrai, et 0 pour un \u00e9tat faux, c'est d'ailleurs de cette mani\u00e8re que les bool\u00e9ens sont encod\u00e9s en C.

    Les bool\u00e9ens ont \u00e9t\u00e9 introduits formellement en C avec C99 et n\u00e9cessitent l'inclusion du fichier d'en-t\u00eate <stdbool.h>. Avant cela le type bool\u00e9en \u00e9tait _Bool et d\u00e9finir les \u00e9tats vrais et faux \u00e9tait \u00e0 la charge du d\u00e9veloppeur.

    #include <stdbool.h>\n\nbool is_enabled = false;\nbool has_tail = true;\n

    Afin de faciliter la lecture du code, il est courant de pr\u00e9fixer les variables bool\u00e9ennes avec les pr\u00e9fixes is_ ou has_. \u00c0 titre d'exemple, si l'on souhaite stocker le genre d'un individu (m\u00e2le, ou femelle), on pourrait utiliser la variable is_male.

    Bien qu'un bool\u00e9en puisse \u00eatre stock\u00e9 sur un seul bit, en pratique, il est stock\u00e9 sur un octet, voire m\u00eame sur un mot de 32 ou 64 bits. Cela est d\u00fb \u00e0 la mani\u00e8re dont les processeurs manipulent les donn\u00e9es en m\u00e9moire. Sur une architecture LP64, un bool\u00e9en sera stock\u00e9 sur 8 octets. Les valeurs true et false vaudront donc\u2009:

    00 00 00 00 00 00 00 00   false\n00 00 00 00 00 00 00 01   true\n

    N\u00e9anmoins, il est possible d'utiliser le type char pour stocker un bool\u00e9en. On peut \u00e9galement utiliser de l'arithm\u00e9tique binaire pour stocker 8 bool\u00e9en sur un uint8_t. Voici un exemple de stockage de 8 bool\u00e9ens sur un uint8_t :

    #include <stdint.h>\n\nuint8_t flags = 0b00000000;\n\nint main (void) {\n    flags |= 1 << 3; // Mettre le quatri\u00e8me bit \u00e0 1\n    flags &= ~(1 << 3); // Mettre le quatri\u00e8me bit \u00e0 0\n}\n
    ", "tags": ["true", "is_male", "uint8_t", "has_", "false", "_Bool", "char", "is_"]}, {"location": "course-c/15-fundations/datatype/#enumerations", "title": "\u00c9num\u00e9rations", "text": "

    Une \u00e9num\u00e9ration est un type de donn\u00e9e un peu particulier qui permet de d\u00e9finir un ensemble de valeurs possibles associ\u00e9es \u00e0 des noms symboliques. Ce style d'\u00e9criture permet de d\u00e9finir un type de donn\u00e9es contenant un nombre fini de valeurs. Ces valeurs sont nomm\u00e9es textuellement et d\u00e9finies num\u00e9riquement dans le type \u00e9num\u00e9r\u00e9.

    enum ColorCode {\n    COLOR_BLACK, // Vaut z\u00e9ro par d\u00e9faut\n    COLOR_BROWN,\n    COLOR_RED,\n    COLOR_ORANGE,\n    COLOR_YELLOW,\n    COLOR_GREEN,\n    COLOR_BLUE,\n    COLOR_PURPLE,\n    COLOR_GRAY,\n    COLOR_WHITE\n};\n

    Le type d'une \u00e9num\u00e9ration est apparent\u00e9 \u00e0 un entier int. Sans pr\u00e9cision, la premi\u00e8re valeur vaut 0, la suivante 1, etc. Il est n\u00e9anmoins possible de forcer les valeurs de la mani\u00e8re suivante\u2009:

    typedef enum country_codes {\n    CODE_SWITZERLAND=41,\n    CODE_BELGIUM=32,\n    CODE_FRANCE, // Sera 33...\n    CODE_SPAIN,  // Sera 34...\n    CODE_US=1\n} CountryCodes;\n

    Pour ne pas confondre un type \u00e9num\u00e9r\u00e9 avec une variable, on utilise souvent la convention d'une notation en capitales. Pour \u00e9viter d\u2019\u00e9ventuelles collisions avec d'autres types, un pr\u00e9fixe est souvent ajout\u00e9 ce qu'on appelle un espace de nommage.

    L'utilisation d'un type \u00e9num\u00e9r\u00e9 peut \u00eatre la suivante\u2009:

    void call(enum country_codes code) {\n    switch(code) {\n    case CODE_SWITZERLAND :\n        printf(\"Calling Switzerland, please wait...\\n\");\n        break;\n    case CODE_BELGIUM :\n        printf(\"Calling Belgium, please wait...\\n\");\n        break;\n    case CODE_FRANCE :\n        printf(\"Calling France, please wait...\\n\");\n        break;\n    default :\n        printf(\"No calls to this country are allowed yet!\\n\");\n    }\n}\n
    ", "tags": ["int"]}, {"location": "course-c/15-fundations/datatype/#type-incomplet", "title": "Type incomplet", "text": "

    En C, un type incomplet est un type de donn\u00e9es dont la taille n'est pas encore compl\u00e8tement d\u00e9finie au moment de sa d\u00e9claration. En d'autres termes, le compilateur sait qu'un type existe, mais ne conna\u00eet pas encore la totalit\u00e9 des d\u00e9tails n\u00e9cessaires pour allouer de la m\u00e9moire ou effectuer certaines op\u00e9rations sur ce type. Un type incomplet peut appara\u00eetre dans le cas des structures ou des tableaux, notamment pour l'abstraction de donn\u00e9es. Certains types comme void sont \u00e9galement incomplets.

    ", "tags": ["void", "type-incomplet"]}, {"location": "course-c/15-fundations/datatype/#vlq", "title": "VLQ", "text": "

    Dans certains syst\u00e8mes, on peut stocker des nombres entiers \u00e0 taille variable. C'est-\u00e0-dire que l'on s'arrange pour r\u00e9server un bit suppl\u00e9mentaire dans le nombre pour indiquer si le nombre se poursuit sur un autre octet. C'est le cas des nombres entiers VLQ utilis\u00e9s dans le protocole MIDI

    On peut stocker un nombre VLQ en m\u00e9moire, mais on ne sait pas de combien d'octets on aura besoin. On peut donc d\u00e9finir un type incomplet pour ce type de donn\u00e9e, mais nous aurons besoin de notions que nous n'avons pas encore vues pour le manipuler, les structures et les unions.

    ", "tags": ["midi", "vlq"]}, {"location": "course-c/15-fundations/datatype/#type-vide-void", "title": "Type vide (void)", "text": "

    Le type void est particulier. Il s'agit d'un type dit incomplet, car la taille de l'objet qu'il repr\u00e9sente en m\u00e9moire n'est pas connue. Il est utilis\u00e9 comme type de retour pour les fonctions qui ne retournent rien\u2009:

    void shout() {\n    printf(\"Hey!\\n\");\n}\n

    Il peut \u00eatre \u00e9galement utilis\u00e9 comme type g\u00e9n\u00e9rique comme la fonction de copie m\u00e9moire memcpy :

    void *memcpy(void * restrict dest, const void * restrict src, size_t n);\n

    Le mot cl\u00e9 void ne peut \u00eatre utilis\u00e9 que dans les contextes suivants\u2009:

    • Comme param\u00e8tre unique d'une fonction, indiquant que cette fonction n'a pas de param\u00e8tres int main(void)
    • Comme type de retour pour une fonction indiquant que cette fonction ne retourne rien void display(char c)
    • Comme pointeur dont le type de destination n'est pas sp\u00e9cifi\u00e9 void* ptr
    ", "tags": ["void", "memcpy"]}, {"location": "course-c/15-fundations/datatype/#transtypage", "title": "Transtypage", "text": ""}, {"location": "course-c/15-fundations/datatype/#promotion-implicite", "title": "Promotion implicite", "text": "

    G\u00e9n\u00e9ralement le type int est de la m\u00eame largeur que le bus m\u00e9moire de donn\u00e9e d'un ordinateur. C'est-\u00e0-dire que c'est souvent, le type le plus optimis\u00e9 pour v\u00e9hiculer de l'information au sein du processeur. Les registres du processeur, autrement dit ses casiers m\u00e9moires, sont au moins assez grand pour contenir un int.

    Aussi, la plupart des types de taille inf\u00e9rieure \u00e0 int sont automatiquement et implicitement promus en int. Le r\u00e9sultat de a + b lorsque a et b sont des char sera automatiquement un int.

    Promotion num\u00e9rique Type source Type cible char int short int int long long float float double

    Notez qu'il n'y a pas de promotion num\u00e9rique vers le type short. On passe directement \u00e0 un type int.

    Exercice 2\u2009: Promotion num\u00e9rique

    Repr\u00e9sentez les promotions num\u00e9riques qui surviennent lors de l'\u00e9valuation des expressions ci-dessous\u2009:

    char c;\nshort sh;\nint i;\nfloat f;\ndouble d;\n
    1. c * sh - f / i + d;
    2. c * (sh \u2013 f) / i + d;
    3. c * sh - f - i + d;
    4. c + sh * f / i + d;

    Exercice 3\u2009: Expressions mixtes

    Soit les instructions suivantes\u2009:

    int n = 10;\nint p = 7;\nfloat x = 2.5;\n

    Donnez le type et la valeur des expressions suivantes\u2009:

    1. x + n % p
    2. x + p / n
    3. (x + p) / n
    4. .5 * n
    5. .5 * (float)n
    6. (int).5 * n
    7. (n + 1) / n
    8. (n + 1.0) / n
    ", "tags": ["int", "char"]}, {"location": "course-c/15-fundations/datatype/#promotion-explicite", "title": "Promotion explicite", "text": "

    Il est possible de forcer la promotion d'un type vers un autre en utilisant un transtypage explicite. Par exemple, pour forcer la promotion d'un int vers un double :

    int n = 10;\ndouble x = (double)n;\n

    Le changement de type forc\u00e9 (transtypage) entre des variables de diff\u00e9rents types engendre des effets de bord qu'il faut conna\u00eetre. Lors d'un changement de type vers un type dont le pouvoir de repr\u00e9sentation est plus important, il n'y a pas de probl\u00e8me. \u00c0 l'inverse, on peut rencontrer des erreurs sur la pr\u00e9cision ou une modification radicale de la valeur repr\u00e9sent\u00e9e\u2009!

    ", "tags": ["transtypage", "int", "double"]}, {"location": "course-c/15-fundations/datatype/#transtypage-dun-entier-en-flottant", "title": "Transtypage d'un entier en flottant", "text": "

    Par exemple, la conversion d'un nombre flottant (double ou float) en entier (sign\u00e9) doit \u00eatre \u00e9tudi\u00e9e pour \u00e9viter tout probl\u00e8me. Le type entier doit \u00eatre capable de recevoir la valeur (attention aux valeurs maxi).

    double d=3.9;\nlong l=(long)d; // valeur : 3 => perte de pr\u00e9cision\n

    A l'ex\u00e9cution, la valeur de \\(l\\) sera la partie enti\u00e8re de \\(d\\). Il n'y a pas d'arrondi.

    double d=0x12345678;\nshort sh=(short)d; // valeur : 0x5678 => changement de valeur\n

    La variable sh (short sur 16 bit) ne peut contenir la valeur r\u00e9elle. Lors du transtypage, il y a modification de la valeur ce qui conduit \u00e0 des erreurs de calculs par la suite.

    double d=-123;\nunsigned short sh=(unsigned short)d; // valeur : 65413 => changement de valeur\n

    L'utilisation d'un type non sign\u00e9 pour convertir un nombre r\u00e9el conduit \u00e9galement \u00e0 une modification de la valeur num\u00e9rique.

    "}, {"location": "course-c/15-fundations/datatype/#transtypage-dun-double-en-float", "title": "Transtypage d'un double en float", "text": "

    La conversion d'un nombre r\u00e9el de type double en r\u00e9el de type float pose un probl\u00e8me de pr\u00e9cision de calcul.

    double d=0.1111111111111111;\nfloat f=(float)d; // valeur : 0.1111111119389533 => perte de pr\u00e9cision\n

    \u00c0 l'ex\u00e9cution, il y a une perte de pr\u00e9cision lors de la conversion, ce qui peut, lors d'un calcul it\u00e9ratif induire des erreurs de calcul.

    Exercice 4\u2009: Conversion de types

    On consid\u00e8re les d\u00e9clarations suivantes\u2009:

    float x;\nshort i;\nunsigned short j;\nlong k;\nunsigned long l;\n

    Identifiez les expressions ci-dessous dont le r\u00e9sultat n'est pas math\u00e9matiquement correct.

    x = 1e6;\ni = x;\nj = -20;\nk = x;\nl = k;\nk = -20;\nl = k;\n
    Solution
    x = 1e6;\ni = x;    // Incorrect, i peut-\u00eatre limit\u00e9 \u00e0 -32767..+32767 (C99 \u00a75.2.4.2.1)\nj = -20;  // Incorrect, valeur sign\u00e9e dans un conteneur non sign\u00e9\nk = x;\nl = k;\nk = -20;\nl = k;    // Incorrect, valeur sign\u00e9e dans un conteneur non sign\u00e9\n

    Exercice 5\u2009: Un casting explicite

    Que valent les valeurs de p, x et n:

    float x;\nint n, p;\n\np = 2;\nx = (float)15 / p;\nn = x + 1.1;\n
    Solution
    p \u2261 2\nx = 7.5\nn = 8\n

    Exercice 6\u2009: Op\u00e9rateurs de relation et op\u00e9rateurs logiques

    Soit les d\u00e9clarations suivantes\u2009:

    float x, y;\nbool condition;\n

    R\u00e9\u00e9crire l'expression ci-dessous en mettant des parenth\u00e8ses montrant l'ordre des op\u00e9rations\u2009:

    condition = x >= 0 && x <= 20 && y > x || y == 50 && x == 2 || y == 60;\n

    Donner la valeur de condition \u00e9valu\u00e9e avec les valeurs suivantes de x et y:

    1. x = -1.0; y = 60.;
    2. x = 0; y = 1.;
    3. x = 19.0; y = 1.0;
    4. x = 0.0; y = 50.0;
    5. x = 2.0; y = 50.0;
    6. x = -10.0; y = 60.0;
    Solution
    condition = (\n    (x >= 0) && (x <= 20) && (y > x))\n    ||\n    ((y == 50) && (x == 2))\n    ||\n    (y == 60)\n);\n
    1. true
    2. true
    3. false
    4. true
    5. true
    6. true

    Exercice 7\u2009: Casse-t\u00eate

    Vous participez \u00e0 une revue de code et tombez sur quelques perles laiss\u00e9es par quelques coll\u00e8gues. Comment proposeriez-vous de corriger ces \u00e9critures\u2009? Le code est \u00e9crit pour un mod\u00e8le de donn\u00e9e LLP64.

    Pour chaque exemple, donner la valeur des variables apr\u00e8s ex\u00e9cution du code.

    1. unsigned short i = 32767;\ni++;\n
    2. short i = 32767;\ni++;\n
    3. short i = 0;\ni = i--;\ni = --i;\ni = i--;\n
    ", "tags": ["false", "condition", "true"]}, {"location": "course-c/15-fundations/datatype/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 8\u2009: \u00c9valuation d'expressions

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    char c = 3;\nshort s = 7;\nint i = 3;\nlong l = 4;\nfloat f = 3.3;\ndouble d = 7.7;\n

    Que vaut le type et la valeur des expressions suivantes\u2009?

    1. c / 2
    2. sh + c / 10
    3. lg + i / 2.0
    4. d + f
    5. (int)d + f
    6. (int)d + lg
    7. c << 2
    8. sh & 0xF0
    9. sh && 0xF0
    10. sh == i + lg
    11. d + f == sh + lg

    Exercice 9\u2009: Pr\u00e9cision des flottants

    Que vaut x?

    float x = 10000000. + 0.1;\n
    Solution

    Le format float est stock\u00e9 sur 32-bits avec 23-bits de mantisse et 8-bits d'exposants. Sa pr\u00e9cision est donc limit\u00e9e \u00e0 environ 6 d\u00e9cimales. Pour repr\u00e9senter 10'000'000.1 il faut plus que 6 d\u00e9cimales et l'addition est donc caduc\u2009:

    #include <stdio.h>\n\nint main(void) {\n    float x = 10000000. + 0.1;\n    printf(\"%f\\n\", x);\n}\n
    $ ./a.out\n10000000.000000\n

    Exercice 10\u2009: Type de donn\u00e9e idoine

    Pour chaque entr\u00e9e suivante, indiquez le nom et le type des variables que vous utiliseriez pour repr\u00e9senter les donn\u00e9es dans ce programme\u2009:

    1. Gestion d'un parking\u2009: nombre de voitures pr\u00e9sente
    2. Station m\u00e9t\u00e9o a. Temp\u00e9rature moyenne de la journ\u00e9e b. Nombre de valeurs utilis\u00e9es pour la moyenne
    3. Montant disponible sur un compte en banque
    4. Programme de calcul de d'\u00e9nergie produite dans une centrale nucl\u00e9aire
    5. Programme de conversion d\u00e9cimal, hexad\u00e9cimal, binaire
    6. Produit scalaire de deux vecteurs plans
    7. Nombre d'impulsions re\u00e7ues par un capteur de position incr\u00e9mental

    Exercice 11\u2009: Construction d'expressions

    On consid\u00e8re un disque, divis\u00e9 en 12 secteurs angulaires \u00e9gaux, num\u00e9rot\u00e9s de 0 \u00e0 11. On mesure l\u2019angle de rotation du disque en degr\u00e9s, sous la forme d\u2019un nombre entier non sign\u00e9. Une fl\u00e8che fixe d\u00e9signe un secteur. Entre 0 et 29 \u00b0, le secteur d\u00e9sign\u00e9 est le n\u00b0 0, entre 30 \u00b0 et 59 \u00b0, c\u2019est le secteur 1, ...

    Donnez une expression arithm\u00e9tique permettant, en fonction d\u2019un angle donn\u00e9, d\u2019indiquer quel est le secteur du disque se trouve devant la fl\u00e8che. Note\u2009: l\u2019angle de rotation peut \u00eatre sup\u00e9rieur \u00e0 360 \u00b0. V\u00e9rifiez cette expression avec les angles de 0, 15, 29, 30, 59, 60, 360, 389, 390 degr\u00e9s.

    \u00c9crivez un programme demandant l\u2019angle et affichant le num\u00e9ro de secteur correspondant.

    Exercice 12\u2009: Somme des entiers

    Il est prouv\u00e9 math\u00e9matiquement que la somme des entiers strictement positifs pris dans l'ordre croissant peut \u00eatre exprim\u00e9e comme\u2009:

    \\[ \\sum_{k=1}^n k = \\frac{n(n+1)}{2} \\]

    \u0b9a\u0bc0\u0ba9\u0bbf\u0bb5\u0bbe\u0b9a \u0b87\u0bb0\u0bbe\u0bae\u0bbe\u0ba9\u0bc1\u0b9c\u0ba9\u0bcd, un grand math\u00e9maticien (Srinivasa Ramanujan) \u00e0 d\u00e9montr\u00e9 que ce la somme \u00e0 l'infini donne\u2009:

    \\[ \\sum_{k=1}^{\\inf} k = -\\frac{1}{12} \\]

    Vous ne le croyez pas et d\u00e9cider d'utiliser le superordinateur Pens\u00e9es Profondes pour faire ce calcul. Comme vous n'avez pas acc\u00e8s \u00e0 cet ordinateur pour l'instant (et probablement vos enfants n'auront pas acc\u00e8s \u00e0 cet ordinateur non plus), \u00e9crivez un programme simple pour tester votre algorithme et prenant en param\u00e8tre la valeur n \u00e0 laquelle s'arr\u00eater.

    Tester ensuite votre programme avec des valeurs de plus en plus grandes et analyser les performances avec le programme time:

    $ time ./a.out 1000000000\n500000000500000000\n\nreal    0m0.180s\nuser    0m0.172s\nsys     0m0.016s\n

    \u00c0 partir de quelle valeur, le temps de calcul devient significativement palpable\u2009?

    Solution
    #include <stdio.h>\n#include <stdlib.h>\n\nint main(int argc, char *argv[]) {\n    long long n = atoi(argv[1]);\n    long long sum = 0;\n    for(size_t i = 0; i < n; i++, sum += i);\n    printf(\"%lld\\n\", sum);\n}\n

    Exercice 13\u2009: Syst\u00e8me de vision industriel

    La soci\u00e9t\u00e9 japonaise Nakain\u0153il d\u00e9veloppe des syst\u00e8mes de vision industriels pour l'inspection de pi\u00e8ces dans une ligne d'assemblage. Le programme du syst\u00e8me de vision comporte les variables internes suivantes\u2009:

    uint32_t inspected_parts, bad_parts;\nfloat percentage_good_parts;\n

    \u00c0 un moment du programme, on peut lire\u2009:

    percentage_good_parts = (inspected_parts - bad_parts) / inspected_parts;\n

    Sachant que inspected_parts = 2000 et bad_parts = 200:

    1. Quel r\u00e9sultat le d\u00e9veloppeur s'attend-il \u00e0 obtenir\u2009?
    2. Qu'obtient-il en pratique\u2009?
    3. Pourquoi\u2009?
    4. Corrigez les \u00e9ventuelles erreurs.
    Solution
    1. Le d\u00e9veloppeur s'attend \u00e0 obtenir le pourcentage de bonnes pi\u00e8ces avec plusieurs d\u00e9cimales apr\u00e8s la virgule.
    2. En pratique, il obtient un entier, c'est \u00e0 dire toujours 0.
    3. La promotion implicite des entiers peut \u00eatre d\u00e9coup\u00e9e comme suit\u2009:

      (uint32_t)numerator = (uint32_t)inspected_parts - (uint32_t)bad_parts;\n(uint32_t)percentage = (uint32_t)numerator / (uint32_t)inspected_parts;\n(float)percentage_good_parts = (uint32_t)percentage;\n

    La division est donc appliqu\u00e9e \u00e0 des entiers et non des flottants.

    1. Une possible correction consiste \u00e0 forcer le type d'un des membres de la division\u2009:

      percentage_good_parts = (float)(inspected_parts - bad_parts) / inspected_parts;\n

    Exercice 14\u2009: Missile Patriot

    Durant la guerre du Golfe le 25 f\u00e9vrier 1991, une batterie de missile am\u00e9ricaine \u00e0 Dharan en Arabie saoudite \u00e0 \u00e9chou\u00e9 \u00e0 intercepter un missile irakien Scud. Cet \u00e9chec tua 28 soldats am\u00e9ricains et en blessa 100 autres. L'erreur sera imput\u00e9e \u00e0 un probl\u00e8me de type de donn\u00e9e sera longuement discut\u00e9e dans le rapport GAO/OMTEC-92-26 du commandement g\u00e9n\u00e9ral.

    Un registre 24-bit est utilis\u00e9 pour le stockage du temps \u00e9coul\u00e9 depuis le d\u00e9marrage du logiciel de contr\u00f4le indiquant le temps en dixi\u00e8me de secondes. D\u00e8s lors il a fallait multiplier ce temps par 1/10 pour obtenir le temps en seconde. La valeur 1/10 \u00e9tait tronqu\u00e9e \u00e0 la 24^e d\u00e9cimale apr\u00e8s la virgule. Des erreurs d'arrondi sont apparue menant \u00e0 un d\u00e9calage de pr\u00e8s de 1 seconde apr\u00e8s 100 heures de fonction. Or, cette erreur d'une seconde s'est traduite par 600 m\u00e8tres d'erreur lors de la tentative d'interception.

    Le stockage de la valeur 0.1 est donn\u00e9 par\u2009:

    \\[ 0.1_{10} \\approx \\lfloor 0.1_{10}\\cdot 2^{23} \\rfloor = 11001100110011001100_{2} \\approx 0.09999990463256836 \\]

    Un registre contient donc le nombre d'heures \u00e9coul\u00e9es exprim\u00e9es en dixi\u00e8me de seconde soit pour 100 heures\u2009:

    \\[ 100 \\cdot 60 \\cdot 60 \\cdot 10 = 3'600'000 \\]

    En termes de virgule fixe, la premi\u00e8re valeur est exprim\u00e9e en Q1.23 tandis que la seconde en Q0.24. Multiplier les deux valeurs entre elles donne Q1.23 x Q0.24 = Q1.47 le r\u00e9sultat est donc exprim\u00e9 sur 48 bits. Il faut donc diviser le r\u00e9sultat du calcul par :math\u2009:2^{47} pour obtenir le nombre de secondes \u00e9coul\u00e9es depuis le d\u00e9but la mise sous tension du syst\u00e8me.

    Quel est l'erreur en seconde cumul\u00e9e sur les 100 heures de fonctionnement\u2009?

    Exercice 15\u2009: Expressions arithm\u00e9tiques enti\u00e8res

    Donnez la valeur des expressions ci-dessous\u2009:

    25 + 10 + 7 - 3\n5 / 2\n24 + 5 / 2\n(24 + 5) / 2\n25 / 5 / 2\n25 / (5 / 2)\n72 % 5 - 5\n72 / 5 - 5\n8 % 3\n-8 % 3\n8 % -3\n-8 % -3\n
    ", "tags": ["time"]}, {"location": "course-c/15-fundations/functions/", "title": "Functions", "text": ""}, {"location": "course-c/15-fundations/functions/#fonctions", "title": "Fonctions", "text": "

    Margaret Hamilton, directrice projet AGC (1969), photo du MIT Museum

    Margaret Hamilton la directrice du projet Apollo Guidance Computer (AGC) \u00e0 c\u00f4t\u00e9 du code du projet.

    \u00c0 l'\u00e9poque d'Apollo 11, les fonctions n'existaient pas, le code n'\u00e9tait qu'une suite monolithique d'instruction \u00e9sot\u00e9rique dont les sources du Apollo Guidance Computer ont \u00e9t\u00e9 publi\u00e9es sur GitHub. Le langage est l'assembler yaYUL dispose de sous-routines, ou proc\u00e9dures qui sont des fonctions sans param\u00e8tres. Ce type de langage est proc\u00e9dural.

    N\u00e9anmoins, dans ce langage assembleur \u00e9trange, le code reste monolithique et toutes les variables sont globales.

    Un programme convenablement structur\u00e9 est d\u00e9coup\u00e9 en \u00e9l\u00e9ments fonctionnels qui disposent pour chacun d'entr\u00e9es et de sorties. De la m\u00eame mani\u00e8re qu'un t\u00e9lenc\u00e9phale hautement d\u00e9velopp\u00e9 et son pouce pr\u00e9henseur aime organiser sa maison en pi\u00e8ces d\u00e9di\u00e9es \u00e0 des occupations particuli\u00e8res et que chaque pi\u00e8ce dispose de rangements assign\u00e9s les uns \u00e0 des assiettes, les autres \u00e0 des couverts, le d\u00e9veloppeur organisera son code en blocs fonctionnels et cherchera \u00e0 minimiser les effets de bord.

    Agencement de fonctions

    Une fonction est donc un ensemble de code ex\u00e9cutable d\u00e9limit\u00e9 du programme principal et disposant\u2009:

    • D'un identifiant unique
    • D'une valeur de retour
    • De param\u00e8tres d'appel

    L'utilisation des fonctions permet\u2009:

    • De d\u00e9composer un programme complexe en t\u00e2ches plus simples
    • De r\u00e9duire la redondance de code
    • De maximiser la r\u00e9utilisation du code
    • De s'abstraire des d\u00e9tails d'impl\u00e9mentation
    • D'augmenter la lisibilit\u00e9 du code
    • D'accro\u00eetre la tra\u00e7abilit\u00e9 \u00e0 l'ex\u00e9cution

    En revanche, une fonction apporte quelques d\u00e9savantages qui \u00e0 l'\u00e9chelle des ordinateurs moderne sont parfaitement n\u00e9gligeables. L'appel \u00e0 une fonction ou sous-routine requiert du housekeeping, qui se compose d'un pr\u00e9lude et d'un aboutissant et dans lequel le contexte doit \u00eatre sauvegard\u00e9.

    "}, {"location": "course-c/15-fundations/functions/#conventions-dappel", "title": "Conventions d'appel", "text": "

    Dans le Voyage de Chihiro (\u5343\u3068\u5343\u5c0b\u306e\u795e\u96a0\u3057) de Hayao Miyazaki, le vieux Kamaji (\u91dc\u723a) travaille dans la chaudi\u00e8re des bains pour l'alimenter en charbon et pr\u00e9parer les d\u00e9coctions d'herbes pour parfumer les bains des clients.

    Le vieux Kamaji et ses bras extensibles.

    Je vous propose de b\u00e2tir une m\u00e9taphore du changement de contexte en s'inspirant de cette illustration. Les murs de la chaudi\u00e8re sont emplis de casiers contenant diff\u00e9rentes herbes, ces casiers peuvent \u00eatre apparent\u00e9s \u00e0 la m\u00e9moire de l'ordinateur, et les diff\u00e9rentes herbes, des types de donn\u00e9es diff\u00e9rents. De son pupitre Kamaji dispose de plusieurs mortiers dans lequel il m\u00e9lange les herbes\u2009; ils sont \u00e0 l'instar de l'ALU d'un ordinateur le si\u00e8ge d'op\u00e9rations transformant, \u00e0 l'aide du pilon, plusieurs entr\u00e9es en une seule sortie\u2009: le m\u00e9lange d'herbes servant \u00e0 la d\u00e9coction. Bien qu'il ait six bras et afin de s'\u00e9viter des manipulations inutiles, il garde de petites r\u00e9serves d'herbes \u00e0 c\u00f4t\u00e9 de son pupitre dans de petits casiers, similaires aux registres du processeur.

    Il profite de son temps libre, pendant que les bains sont ferm\u00e9s pour pr\u00e9parer certains m\u00e9langes d'herbes les plus populaires et il place ce stock dans un casier du mur. Pr\u00e9parer un m\u00e9lange est tr\u00e8s similaire \u00e0 un programme informatique dans lequel une suite d'op\u00e9ration repr\u00e9sente une recette donn\u00e9e. Le vieux Kamaji \u00e0 une tr\u00e8s grande m\u00e9moire, et il ne dispose pas de livre de recettes, mais vous, moi, n'importe qui, aurions besoin d'instructions claires du type\u2009:

    AUTUMN_TONIC_TEA :\n\n  MOVE  R1 @B4      # D\u00e9place de la grande ortie du casier B4 au registre R1\n  MOVE  R2 @A8      # D\u00e9place la menthe verte (Mentha spicata) du casier\n                    # A8 au registre R2\n  MOVE  R3 @C7      # D\u00e9place le gingembre du casier C7 au registre R3\n  ...\n  CHOP  R4 R3, FINE # Coupe tr\u00e8s finement le gingembre et le place dans R4\n  ...\n  LEAV  R2 R5       # D\u00e9tache les feuilles des tiges de la menthe\n                    # verte, place les feuilles en R5\n  ...\n  ADD   R8 R1 R5    # Pilonne le contenu de R1 et R2 et place dans R8\n  ADD   R8 R8 R4\n  ...\n  STO   R8 @F6      # Place le m\u00e9lange d'herbe automnale tonic dans le casier F6\n

    Souvent, le vieux Kamaji r\u00e9p\u00e8te les m\u00eames suites d'op\u00e9ration et ce, peu importe les herbes qu'il manipule, une fois plac\u00e9es dans les petits casiers (registres), il pourrait travailler les yeux ferm\u00e9s.

    On pourrait r\u00e9sumer ce travail par une fonction C, ici prenant un rhizome et deux herbes en entr\u00e9e et g\u00e9n\u00e9rant un m\u00e9lange en sortie.

    blend slice_and_blend(rootstock a, herb b, herb c);\n

    Pour des recettes complexes, il se pourrait que la fonction slice_and_blend soit appel\u00e9e plusieurs fois \u00e0 la suite, mais avec des ingr\u00e9dients diff\u00e9rents. De m\u00eame que cette fonction fait appel \u00e0 une autre fonction plus simple tel que slice (d\u00e9couper) ou blend_together (incorporer).

    Et le contexte dans tout cela\u2009? Il existe selon le langage de programmation et l'architecture processeur ce que l'on appelle les conventions d'appel. C'est-\u00e0-dire les r\u00e8gles qui r\u00e9gissent les interactions entre les appels de fonctions. Dans notre exemple, on adoptera peut-\u00eatre la convention que n'importe quelle fonction trouvera ses ingr\u00e9dients d'entr\u00e9es dans les casiers R1, R2 et R3 et que le r\u00e9sultat de la fonction, ici le blend, sera plac\u00e9 dans le casier R8. Ainsi peu importe les herbes en entr\u00e9e, le vieux Kamaji peut travailler les yeux ferm\u00e9s, piochant simplement dans R1, R2 et R3.

    On observe n\u00e9anmoins dans la recette \u00e9voqu\u00e9e plus haut qu'il utilise d'autres casiers, R4, et R5. Il faut donc faire tr\u00e8s attention \u00e0 ce qu'une autre fonction peut-\u00eatre la fonction slice, n'utilise pas dans sa propre recette le casier R5, car sinon, c'est la catastrophe.

    herb slice(herb a);\n

    Kamaji entrepose temporairement les feuilles de menthe verte dans R5 et lorsqu'il en a besoin, plus tard, apr\u00e8s avoir d\u00e9coup\u00e9 les fleurs de mol\u00e8ne que R5 contient des tiges d'une autre plante.

    Dans les conventions d'appel, il faut donc \u00e9galement donner la responsabilit\u00e9 \u00e0 quelqu'un de ne pas utiliser certains casiers, ou alors d'en sauvegarder ou de restaurer le contenu au d\u00e9but et \u00e0 la fin de la recette. Dans les conventions d'appel, il y a en r\u00e9alit\u00e9 plusieurs cat\u00e9gories de registres\u2009:

    • ceux utilis\u00e9s pour les param\u00e8tres de la fonction,
    • ceux utilis\u00e9s pour les valeurs de retour,
    • ceux qui peuvent \u00eatre utilis\u00e9s librement par une fonction (la sauvegarde est \u00e0 la charge du caller, la fonction qui appelle une autre fonction),
    • ceux qui doivent \u00eatre sauvegard\u00e9s par le callee (la fonction qui est appel\u00e9e).

    En C, ce m\u00e9canisme est parfaitement automatique, le programmeur n'a pas \u00e0 se soucier du processeur, du nom des registres, de la correspondance entre le nom des herbes et le casier ou elles sont entrepos\u00e9es. N\u00e9anmoins, l'\u00e9lectronicien d\u00e9veloppeur, proche du mat\u00e9riel, doit parfois bien comprendre ces m\u00e9canismes et ce qu'ils co\u00fbtent (en temps et en place m\u00e9moire) \u00e0 l'ex\u00e9cution d'un programme.

    ", "tags": ["blend_together", "slice", "slice_and_blend"]}, {"location": "course-c/15-fundations/functions/#overhead", "title": "Overhead", "text": "

    L'appel de fonction co\u00fbte \u00e0 l'ex\u00e9cution, car avant chaque fonction, le compilateur ajoute automatiquement des instructions de sauvegarde et de restauration des registres utilis\u00e9s\u2009:

    Sauvegarde des registres du processeur et convention d'appel de fonction.

    Ce co\u00fbt est faible, tr\u00e8s faible, un ordinateur fonctionnant \u00e0 3 GHz et une fonction complexe utilisant tous les registres disponibles, mettons 10 registres, consommera entre l'appel de la fonction et son retour 0.000'000'003 seconde, \u00e7a va, c'est raisonnable. Sauf que, si la fonction ne comporte qu'une seule op\u00e9ration comme ci-dessous, l'overhead sera aussi plus faible.

    int add(int a, int b) {\n    return a + b;\n}\n
    "}, {"location": "course-c/15-fundations/functions/#stack", "title": "Stack", "text": "

    En fran\u00e7ais la pile d'ex\u00e9cution, est un emplacement m\u00e9moire utilis\u00e9 pour sauvegarder les registres du processeur entre les appels de fonctions, sauvegarder les adresses de retour des fonctions qui sont analogue \u00e0 sauvegarder le num\u00e9ro de page du livre de recettes\u2009: p 443. Recette du Bras de V\u00e9nus\u2009: commencer par r\u00e9aliser une g\u00e9noise de 300g (p. 225). Une fois la g\u00e9noise termin\u00e9e, il faut se rappeler de retourner \u00e0 la page 443. Enfin le stack est utilis\u00e9 pour m\u00e9moriser les param\u00e8tres des fonctions suppl\u00e9mentaires qui ne tiendraient pas dans les registres d'entr\u00e9es. La convention d'appel de la plupart des architectures pr\u00e9voit g\u00e9n\u00e9ralement 3 registres pour les param\u00e8tres d'entr\u00e9es, si bien qu'une fonction \u00e0 4 param\u00e8tres pourrait bien aussi utiliser le stack:

    double quaternion_norm(double a1, double b1, double c1, double d1);\n

    La pile d'ex\u00e9cution est, comme son nom l'indique, une pile sur laquelle sont empil\u00e9s et d\u00e9pil\u00e9s les \u00e9l\u00e9ments au besoin. \u00c0 chaque appel d'une fonction, la valeur des registres \u00e0 sauvegarder est empil\u00e9e et au retour d'une fonction les registres sont d\u00e9pil\u00e9s si bien que la fonction d'appel retrouve le stack dans le m\u00eame \u00e9tat qu'il \u00e9tait avant l'appel d'une fonction enfant.

    "}, {"location": "course-c/15-fundations/functions/#prototype", "title": "Prototype", "text": "

    Le prototype d'une fonction est son interface avec le monde ext\u00e9rieur. Il d\u00e9clare la fonction, son type de retour et ses param\u00e8tres d'appel. Le prototype est souvent utilis\u00e9 dans un fichier d'en-t\u00eate pour construire des biblioth\u00e8ques logicielles. La fonction printf que nous ne cessons pas d'utiliser voit son prototype r\u00e9sider dans le fichier <stdio.h> et il est d\u00e9clar\u00e9 sous la forme\u2009:

    \u200bint printf(const char* format, ...);\n

    Notons qu'il n'y a pas d'accolades ici.

    Rappelons-le, C est un langage imp\u00e9ratif et d\u00e9claratif, c'est-\u00e0-dire que les instructions sont s\u00e9quentielles et que les d\u00e9clarations du code sont interpr\u00e9t\u00e9es dans l'ordre ou elles apparaissent. Si bien si je veux appeler la fonction make_coffee, il faut qu'elle ait \u00e9t\u00e9 d\u00e9clar\u00e9e avant, c'est \u00e0 dire plus haut.

    Le code suivant fonctionne\u2009:

    int make_coffee(void) {\n    printf(\"Please wait...\\n)\";\n}\n\nint main(void) {\n    make_coffee();\n}\n

    Mais celui-ci ne fonctionnera pas, car make_coffee n'est pas connu au moment de l'appel\u2009:

    int main(void) {\n    make_coffee();\n}\n\nint make_coffee(void) {\n    printf(\"Please wait...\\n)\";\n}\n

    Si pour une raison connue seule du d\u00e9veloppeur on souhaite d\u00e9clarer la fonction apr\u00e8s main, on peut ajouter le prototype de la fonction avant cette derni\u00e8re. C'est ce que l'on appelle la d\u00e9claration avanc\u00e9e ou forward declaration.

    int make_coffee(void);\n\nint main(void) {\n    make_coffee();\n}\n\nint make_coffee(void) {\n    printf(\"Please wait...\\n\");\n}\n

    Un prototype de fonction diff\u00e8re de son impl\u00e9mentation par le fait qu'il ne dispose pas du code, mais simplement sa d\u00e9finition, permettant au compilateur d'\u00e9tablir les conventions d'appel de la fonction.

    ", "tags": ["main", "printf", "make_coffee"]}, {"location": "course-c/15-fundations/functions/#syntaxe", "title": "Syntaxe", "text": "

    La syntaxe d'\u00e9criture d'une fonction peut \u00eatre assez compliqu\u00e9e et la source de v\u00e9rit\u00e9 est issue de la grammaire du langage, qui n'est pas n\u00e9cessairement accessible au profane. Or, depuis C99, une fonction prend la forme\u2009:

    <storage-class> <return-type> <function-name> (\n    <parameter-type> <parameter-name>, ... )\n
    <storage-class>

    Classe de stockage, elle n'est pas utile \u00e0 ce stade du cours, nous aborderons plus tard les mots cl\u00e9s extern, static et inline.

    <return-type>

    Le type de retour de la fonction, s'agit-il d'un int, d'un float ? Le type de retour est anonyme, il n'a pas de nom et ce n'est pas n\u00e9cessaire.

    <function-name>

    Il s'agit d'un identificateur qui repr\u00e9sente le nom de la fonction. G\u00e9n\u00e9ralement on pr\u00e9f\u00e8re choisir un verbe, quelquefois associ\u00e9 \u00e0 un nom\u2009: compute_norm, make_coffee, ... N\u00e9anmoins, lorsqu'il n'y a pas d'ambig\u00fcit\u00e9, on peut choisir des termes plus simples tels que main, display ou dot_product.

    <parameter-type> <parameter-name>

    La fonction peut prendre en param\u00e8tre z\u00e9ro \u00e0 plusieurs param\u00e8tres o\u00f9 chaque param\u00e8tre est d\u00e9fini par son type et son nom tel que\u2009: double real, double imag pour une fonction qui prendrait en param\u00e8tre un nombre complexe.

    Apr\u00e8s la fermeture de la parenth\u00e8se de la liste des param\u00e8tres, deux possibilit\u00e9s\u2009:

    Prototype

    On clos la d\u00e9claration avec un ;

    Impl\u00e9mentation

    On poursuit avec l'impl\u00e9mentation du code { ... }

    ", "tags": ["int", "inline", "float", "extern", "main", "compute_norm", "display", "static", "make_coffee", "dot_product"]}, {"location": "course-c/15-fundations/functions/#void", "title": "void", "text": "

    Le type void est \u00e0 une signification particuli\u00e8re dans la syntaxe d'une fonction. Il peut \u00eatre utilis\u00e9 de trois mani\u00e8res diff\u00e9rentes\u2009:

    • Pour indiquer l'absence de valeur de retour\u2009:

      void foo(int a, int b);\n
    • Pour indiquer l'absence de param\u00e8tres\u2009:

      int bar(void);\n
    • Pour indiquer que la valeur de retour n'est pas utilis\u00e9e par le parent\u2009:

      (void) foo(23, 11);\n

    La d\u00e9claration suivante est formellement fausse, car la fonction ne poss\u00e8de pas un prototype complet. En effet, le nombre de param\u00e8tres n'est pas contraint et le code suivant est valide au sens de C99.

    void dummy() {}\n\nint main(void) {\n    dummy(1, 2, 3);\n    dummy(120, 144);\n}\n

    Aussi, il est imp\u00e9ratif de toujours \u00e9crire des prototypes complets et d'explicitement utiliser void lorsque la fonction ne prend aucun param\u00e8tre en entr\u00e9e. Si vous utilisez un compilateur C++, une d\u00e9claration incompl\u00e8te g\u00e9n\u00e8rera une erreur.

    ", "tags": ["void"]}, {"location": "course-c/15-fundations/functions/#parametres", "title": "Param\u00e8tres", "text": "

    Comme nous l'avons vu plus haut, pour de meilleures performances \u00e0 l'ex\u00e9cution, il est pr\u00e9f\u00e9rable de s'en tenir \u00e0 un maximum de trois param\u00e8tres, c'est \u00e9galement plus lisible pour le d\u00e9veloppeur, mais rien n'emp\u00eache d'en avoir plus.

    En plus de cela, les param\u00e8tres peuvent \u00eatre pass\u00e9s de deux mani\u00e8res\u2009:

    • Par valeur
    • Par r\u00e9f\u00e9rence

    En C, fondamentalement, tous les param\u00e8tres sont pass\u00e9s par valeur, c'est-\u00e0-dire que la valeur d'une variable est copi\u00e9e \u00e0 l'appel de la fonction. Dans l'exemple suivant, la valeur affich\u00e9e sera bel et bien 33 et non. 42

    void alter(int a) {\n    a = a + 9;\n}\n\nvoid main(void) {\n    int a = 33;\n    alter(a);\n    printf(\"%d\\n\", a);\n}\n

    Dans certains cas, on souhaite utiliser plus d'une valeur de retour et l'on peut utiliser un tableau. Dans l'exemple suivant, la valeur affich\u00e9e sera cette fois-ci 42 et non 33.

    void alter(int array[]) {\n    array[0] += 9;\n}\n\nvoid main(void) {\n    int array[] = {33, 34, 35};\n    alter(array);\n    printf(\"%d\\n\", array[0]);\n}\n

    Par abus de langage et en comparaison avec d'autres langages de programmation, on appellera ceci un passage par r\u00e9f\u00e9rence, car ce n'est pas une copie du tableau qui est pass\u00e9e \u00e0 la fonction alter, mais seulement une r\u00e9f\u00e9rence sur ce tableau.

    En des termes plus corrects, mais nous verrons cela au chapitre sur les pointeurs, c'est bien un passage par valeur dans lequel la valeur d'un pointeur sur un tableau est pass\u00e9e \u00e0 la fonction alter.

    Retenez simplement que lors d'un passage par r\u00e9f\u00e9rence, on cherche \u00e0 rendre la valeur pass\u00e9e en param\u00e8tre modifiable par le caller.

    ", "tags": ["alter"]}, {"location": "course-c/15-fundations/functions/#exemples-de-fonctions", "title": "Exemples de fonctions", "text": ""}, {"location": "course-c/15-fundations/functions/#suite-de-fibonacci", "title": "Suite de Fibonacci", "text": "

    La suite de Fibonacci est une suite d'entiers dans laquelle chaque terme est la somme des deux termes pr\u00e9c\u00e9dents. La suite commence par 0 et 1. La suite commence donc par 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

    Voici une impl\u00e9mentation de la suite de Fibonacci en utilisant une approche it\u00e9rative\u2009:

    int fib(int n)\n{\n    int sum = 0;\n    int t1 = 0, t2 = 1;\n    int next_term;\n    for (int i = 1; i <= n; i++)\n    {\n        sum += t1;\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    return sum;\n}\n
    "}, {"location": "course-c/15-fundations/functions/#syntaxe-traditionnelle", "title": "Syntaxe traditionnelle", "text": "

    Historiquement, la syntaxe des fonctions en C \u00e9tait diff\u00e9rente de celle que nous avons vue jusqu'\u00e0 pr\u00e9sent. Consid\u00e9rons la fonction suivante\u2009:

    double func(double x, double y, int z) {\n    return x + y + z;\n}\n

    En C89, la syntaxe de cette fonction \u00e9tait la suivante\u2009:

    double\nfunc(x, y, z)\ndouble x, y;\nint z;\n{ return x + y + z; }\n

    Cette syntaxe est toujours valide dans les versions plus r\u00e9centes du langage, mais elle est d\u00e9conseill\u00e9e car elle est moins lisible que la syntaxe moderne.

    "}, {"location": "course-c/15-fundations/functions/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Dans la moyenne

    \u00c9crire une fonction mean qui re\u00e7oit 3 param\u00e8tres r\u00e9els et qui retourne la moyenne.

    Solution
    double mean(double a, double b, double c) {\n    return (a + b + c) / 3.;\n}\n

    Exercice 2\u2009: Le plus petit

    \u00c9crire une fonction min qui re\u00e7oit 3 param\u00e8tres r\u00e9els et qui retourne la plus petite valeur.

    Solution
    double min(double a, double b, double c) {\n    double min_value = a;\n    if (b < min_value)\n        min_value = b;\n    if (c < min_value)\n        min_value = c;\n    return min_value;\n}\n

    Une mani\u00e8re plus compacte, mais moins lisible serait\u2009:

    double min(double a, double b, double c) {\n    return (a = (a < b ? a : b)) < c ? a : c;\n}\n

    Exercice 3\u2009: Algorithme de retour de monnaie

    On consid\u00e8re le cas d'une caisse automatique de parking. Cette caisse d\u00e9livre des tickets au prix unique de CHF 0.50 et dispose d'un certain nombre de pi\u00e8ces de 10 et 20 centimes pour le rendu de monnaie.

    Dans le code du programme, les trois variables suivantes seront utilis\u00e9es\u2009:

    // Available coins in the parking ticket machine\nunsigned int ncoin_10, ncoin_20;\n\n// How much money the user inserted into the machine (in cents)\nunsigned int amount_payed;\n

    \u00c9crivez l'algorithme de rendu de la monnaie tenant compte du nombre de pi\u00e8ces de 10 et 20 centimes restants dans l'appareil. Voici un exemple du fonctionnement du programme\u2009:

    $ echo \"10 10 20 20 20\" | ./ptm 30 1\nticket\n20\n10\n

    Le programme re\u00e7oit sur stdin les pi\u00e8ces introduites dans la machine. Les deux arguments pass\u00e9s au programme ptm sont 1. le nombre de pi\u00e8ces de 10 centimes disponibles et 2. le nombre de pi\u00e8ces de 20 centimes disponibles. stdout contient les valeurs rendues \u00e0 l'utilisateur. La valeur ticket correspond au ticket distribu\u00e9.

    Le cas \u00e9ch\u00e9ant, s'il n'est possible de rendre la monnaie, aucun ticket n'est distribu\u00e9 et l'argent donn\u00e9 est rendu.

    Solution

    Voici une solution partielle\u2009:

    #define TICKET_PRICE 50\n\nvoid give_coin(unsigned int value) { printf(\"%d\\n\", value); }\nvoid give_ticket(void) { printf(\"ticket\\n\"); }\n\nbool no_ticket = amount_payed < TICKET_PRICE;\n\nint amount_to_return = amount_payed - TICKET_PRICE;\ndo {\n    while (amount_to_return > 0) {\n        if (amount_to_return >= 20 && ncoin_20 > 0) {\n            give_coin(20);\n            amount_to_return -= 20;\n            ncoin_20--;\n        } else if (amount_to_return >= 10 && ncoin_10 > 0) {\n            give_coin(10);\n            amount_to_return -= 10;\n            ncoin_10--;\n        } else {\n            no_ticket = true;\n            break;\n        }\n    }\n} while (amount_to_return > 0);\n\nif (!no_ticket) {\n    give_ticket();\n}\n

    Exercice 4\u2009: La fonction f

    Consid\u00e9rons le programme suivant\u2009:

    int f(float x) {\n    int i;\n    if (x > 0.0)\n        i = (int)(x + 0.5);\n    else\n        i = (int)(x - 0.5);\n    return i;\n}\n

    Quel sont les types et les valeurs retourn\u00e9es par les expressions ci-dessous\u2009?

    f(1.2)\nf(-1.2)\nf(1.6)\nf(-1.6)\n

    Quel est votre conclusion sur cette fonction\u2009?

    Exercice 5\u2009: Mauvaise somme

    Le programme suivant compile sans erreurs graves, mais ne fonctionne pas correctement.

    #include <stdio.h>\n#include <stdlib.h>\n#include <math.h>\n\nlong get_integer()\n{\n    bool ok;\n    long result;\n    do\n    {\n        printf(\"Enter a integer value: \");\n        fflush(stdin); // Empty input buffer\n        ok = (bool)scanf(\"%ld\", &result);\n        if (!ok)\n            printf(\"Incorrect value.\\n\");\n    }\n    while (!ok);\n    return result;\n}\n\nint main(void)\n{\n    long a = get_integer;\n    long b = get_integer;\n\n    printf(\"%d\\n\", a + b);\n}\n

    Quel est le probl\u00e8me\u2009? \u00c0 titre d'information voici ce que le programme donne, notez que l'invit\u00e9 de saisie n'est jamais apparu\u2009:

    $ ./sum\n8527952\n
    ", "tags": ["ticket", "stdin", "min", "ptm", "mean", "stdout"]}, {"location": "course-c/15-fundations/grammar/", "title": "Grammar", "text": "", "tags": ["pointer", "declarator"]}, {"location": "course-c/15-fundations/grammar/#la-grammaire", "title": "La grammaire", "text": "

    Dans un langage formel, la grammaire est l'ensemble des r\u00e8gles qui r\u00e9gissent la construction des phrases. Elle est essentielle pour comprendre et produire un texte correctement. En fran\u00e7ais, la grammaire est complexe et comporte de nombreuses r\u00e8gles et exceptions.

    En programmation informatique, la grammaire est \u00e9galement tr\u00e8s importante. Elle d\u00e9finit la syntaxe du langage de programmation, c'est-\u00e0-dire la mani\u00e8re dont les instructions doivent \u00eatre \u00e9crites pour \u00eatre comprises par l'ordinateur. Une erreur de syntaxe peut emp\u00eacher un programme de fonctionner correctement, voire de compiler.

    L'ordinateur ne lit pas une phrase comme vous, elle ne peut pas ignorer les fautes de frappe, les erreurs de syntaxe ou les erreurs d'accord, car le compilateur doit \u00eatre capable d'interpr\u00e9ter le code source sans aucune ambigu\u00eft\u00e9.

    La grammaire d'un langage de programmation est g\u00e9n\u00e9ralement d\u00e9finie par un document appel\u00e9 \u00ab\u2009grammaire formelle\u2009\u00bb. Elle va d\u00e9finir les composants des \u00e9l\u00e9ments de votre code. Prenons par exemple le programme suivant\u2009:

    int main() {\n    const int n = 23 + 42;\n    for (int j = 0; j < n; ++j) {\n        printf(\"%d \", j);\n    }\n    return 0;\n}\n

    La figure suivante montre comment il serait possible de hi\u00e9rarchiser les \u00e9l\u00e9ments de ce programme. On constate une imbriquation des \u00e9l\u00e9ments.

    Exemple d'arbre syntaxique (AST)

    La grammaire formelle du langage C est tr\u00e8s complexe et comporte de nombreuses r\u00e8gles. Elle est d\u00e9finie par le standard du langage C, qui est un document officiel publi\u00e9 par l'ANSI (American National Standards Institute) et l'ISO (International Organization for Standardization).

    Une grammaire formelle est souvent \u00e9crite en utilisant une notation appel\u00e9e \u00ab\u2009Backus-Naur Form\u2009\u00bb (BNF). Cette notation est tr\u00e8s pr\u00e9cise et permet de d\u00e9crire de mani\u00e8re formelle la syntaxe d'un langage de programmation. Pour le C voici un extrait de la grammaire utilis\u00e9e par le compilateur\u2009:

    <translation_unit> ::= {<external_declaration>}*\n\n<external_declaration> ::= <function_definition>\n                        | <declaration>\n\n<function_definition> ::= {<declaration_specifier>}* <declarator> {<declaration>}* <compound_statement>\n\n<declaration_specifier> ::= <storage_class_specifier>\n                            | <type_specifier>\n                            | <type_qualifier>\n\n<storage_class_specifier> ::= \"auto\"\n                            | \"register\"\n                            | \"static\"\n                            | \"extern\"\n                            | \"typedef\"\n\n<type_specifier> ::= \"void\"\n                    | \"char\"\n                    | \"short\"\n                    | \"int\"\n                    | \"long\"\n                    | \"float\"\n                    | \"double\"\n                    | \"signed\"\n                    | \"unsigned\"\n                    | <struct_or_union_specifier>\n                    | <enum_specifier>\n                    | <typedef_name>\n\n<struct_or_union_specifier> ::= <struct_or_union> <identifier> { {<struct_declaration>}+ }\n                                | <struct_or_union> { {<struct_declaration>}+ }\n                                | <struct_or_union> <identifier>\n\n<struct_or_union> ::= \"struct\"\n                    | \"union\"\n\n<struct_declaration> ::= {<specifier_qualifier>}* <struct_declarator_list>\n\n<specifier_qualifier> ::= <type_specifier>\n                        | <type_qualifier>\n\n<struct_declarator_list> ::= <struct_declarator>\n                            | <struct_declarator_list> \",\" <struct_declarator>\n\n<struct_declarator> ::= <declarator>\n                        | <declarator> \":\" <constant_expression>\n                        | \":\" <constant_expression>\n\n<declarator> ::= {<pointer>}? <direct_declarator>\n\n<pointer> ::= * {<type_qualifier>}* {<pointer>}?\n\n<type_qualifier> ::= \"const\"\n                    | \"volatile\"\n\n<direct_declarator> ::= <identifier>\n                        | ( <declarator> )\n                        | <direct_declarator> \"[\" {<constant_expression>}? \"]\"\n                        | <direct_declarator> \"(\" <parameter_type_list> \")\"\n                        | <direct_declarator> \"(\" {<identifier>}* \")\"\n

    Ce que l'on observe par exemple c'est que la grammaire du C est r\u00e9cursive. Cela signifie que l'on peut d\u00e9finir un \u00e9l\u00e9ment en fonction de lui-m\u00eame. Par exemple, un declarator peut contenir un pointer qui peut lui-m\u00eame contenir un pointer.

    Comment est-ce que cela fonctionne derri\u00e8re les coulisses\u2009?

    Le compilateur va tout d'abord convertir votre code source en un arbre de syntaxe abstraite (AST) qui va repr\u00e9senter la structure de votre programme (c'est grosso modo la figure montr\u00e9e plus haut). Ensuite, il va v\u00e9rifier que cet arbre respecte les r\u00e8gles du standard C. Si ce n'est pas le cas, il va vous renvoyer une erreur de compilation. Si cela passe \u00e0 cette \u00e9tape, il va ensuite pouvoir assembler votre programme en convertissant cette repr\u00e9sentation interne en du code assembleur. Ce dernier peut ensuite \u00eatre optimis\u00e9 pour \u00eatre plus rapide ou plus petit.

    La grammaire est donc un \u00e9l\u00e9ment important de la programmation et elle explique pourquoi un simple ; manquant peut vous co\u00fbter quelques cheveux arrach\u00e9s car l'analyseur syntaxique ne sait pas comment d\u00e9couper votre code en phrases.

    Pour les curieux, vous pouvez consulter la grammaire compl\u00e8te du C dans le standard du langage C (ISO/IEC 9899:2018) dans l'annexe A.

    "}, {"location": "course-c/15-fundations/grammar/#definir-mon-propre-langage", "title": "D\u00e9finir mon propre langage", "text": "

    Imaginons que l'on souaite r\u00e9aliser notre propre langage formel, par exemple pour analyser une expression math\u00e9matique de la forme suivante\u2009:

    3 + 4 * 5 + ( sin(3.14) + sqrt(2) / 8 )\n

    On pourrait d\u00e9finir une grammaire formelle pour ce langage en utilisant la notation BNF :

    <expression> ::= <term> { \"+\" <term> | \"-\" <term> }*\n\n<term> ::= <factor> { \"*\" <factor> | \"/\" <factor> }*\n\n<factor> ::= <number> | \"(\" <expression> \")\" | <function> \"(\" <expression> \")\"\n\n<number> ::= [0-9]+(\".\"[0-9]*)?\n<function> ::= \"sin\" | \"cos\" | \"sqrt\"\n

    Des outils comme lex et yacc populaires sur les syst\u00e8mes Unix permettent de g\u00e9n\u00e9rer un analyseur lexical et un analyseur syntaxique utilisables en C \u00e0 partir de cette grammaire. Ces outils sont tr\u00e8s puissants et sont utilis\u00e9s dans de nombreuses biblioth\u00e8ques et logiciels pour analyser des fichiers de configuration ou des syntaxes sp\u00e9cifiques.

    ", "tags": ["yacc", "lex"]}, {"location": "course-c/15-fundations/operators/", "title": "Op\u00e9rateurs", "text": "L'un de mes jours les plus productifs a \u00e9t\u00e9 lorsque j'ai supprim\u00e9 1 000 lignes de code. Nous n'avons pas besoin de plus d'op\u00e9rateurs, nous en avons besoin de moins.Ken Thompson

    En programmation, un op\u00e9rateur est une fonction qui effectue une op\u00e9ration sur des valeurs. Les op\u00e9rateurs utilisent des identificateurs sp\u00e9cifiques propres \u00e0 chaque langage de programmation, ce qui permet de simplifier l'\u00e9criture des expressions. Par exemple, l'op\u00e9rateur d'addition + permet d'additionner deux valeurs.

    L'unit\u00e9 de calcul arithm\u00e9tique du processeur (ALU) est responsable d'effectuer les op\u00e9rations fondamentales. Un ordinateur \u00e0 2 GHz pourrait par exemple effectuer plus de 2 milliards (2'000'000'000) d'op\u00e9rations par seconde.

    Un op\u00e9rateur prend habituellement deux op\u00e9randes et retourne un r\u00e9sultat. On dit alors que cette classe d'op\u00e9rateurs a une arit\u00e9 de 2. Il existe \u00e9galement des op\u00e9rateurs \u00e0 arit\u00e9 de 1, aussi appel\u00e9s op\u00e9rateurs unaires comme pour obtenir l'oppos\u00e9 d'un nombre (\\(-x\\)). Connaissant le compl\u00e9ment \u00e0 deux, on sait que pour obtenir l'oppos\u00e9 d'un nombre, il suffit d'inverser tous les bits et d'ajouter 1. C'est-\u00e0-dire de faire l'op\u00e9ration de n\u00e9gation ~ puis de faire une addition +1.

    Un op\u00e9rateur poss\u00e8de plusieurs propri\u00e9t\u00e9s\u2009:

    Une priorit\u00e9

    La multiplication * est plus prioritaire que l'addition +

    Une associativit\u00e9

    L'op\u00e9rateur d'affectation = poss\u00e8de une associativit\u00e9 \u00e0 droite, c'est-\u00e0-dire que l'op\u00e9rande \u00e0 droite de l'op\u00e9rateur sera \u00e9valu\u00e9 en premier

    Un point de s\u00e9quence

    Certains op\u00e9rateurs comme &&, ||, ? ou , poss\u00e8dent un point de s\u00e9quence garantissant que l'ex\u00e9cution s\u00e9quentielle du programme sera respect\u00e9e avant et apr\u00e8s ce point. Par exemple si dans l'expression i < 12 && j > 2 la valeur de i est plus grande que 12, le test j > 2 ne sera jamais effectu\u00e9. L'op\u00e9rateur && garantit l'ordre des choses, ce qui n'est pas le cas avec l'affectation =.

    "}, {"location": "course-c/15-fundations/operators/#alu-arithmetic-logic-unit", "title": "ALU (Arithmetic Logic Unit)", "text": "

    Dans un ordinateur, ou sur un microcontr\u00f4leur, c'est l'unit\u00e9 de calcul arithm\u00e9tique ALU qui est en charge d'effectuer les op\u00e9rations fondamentales. Cette unit\u00e9 de calcul est consensuellement repr\u00e9sent\u00e9e comme illustr\u00e9e \u00e0 la figure suivante\u2009:

    ALU

    L'unit\u00e9 de calcul arithm\u00e9tique (ALU) repr\u00e9sent\u00e9e est compos\u00e9e de deux entr\u00e9es A et B, d'une sortie C et d'un mode op\u00e9ratoire O. Sur de petites architectures mat\u00e9rielles, l'ALU peut \u00eatre limit\u00e9 aux op\u00e9rations d'addition +, d'inversion bit \u00e0 bit ~, de d\u00e9calage vers la gauche << et vers la droite >> et de l'op\u00e9ration bit \u00e0 bit logique & pour la conjonction ainsi que | pour la disjonction.

    Si l'on souhaite faire une addition, on peut \u00e9crire en C\u2009:

    c = a + b;\n

    "}, {"location": "course-c/15-fundations/operators/#types-doperateurs", "title": "Types d'op\u00e9rateurs", "text": "

    Le langage C d\u00e9finit un certain nombre d'op\u00e9rateurs qui peuvent \u00eatre class\u00e9s en plusieurs cat\u00e9gories\u2009:

    • Les op\u00e9rateurs arithm\u00e9tiques
    • Les op\u00e9rateurs relationnels
    • Les op\u00e9rateurs logiques
    • Les op\u00e9rateurs bit \u00e0 bit
    • Les op\u00e9rateurs d'affectation
    • Les op\u00e9rateurs de pointeurs
    • Les op\u00e9rateurs de taille
    • Les op\u00e9rateurs de s\u00e9quence
    • Les op\u00e9rateurs de pr\u00e9/post-incr\u00e9mentation
    • Les op\u00e9rateurs de condition

    Nous allons tous les voir un par un...

    "}, {"location": "course-c/15-fundations/operators/#operateurs-arithmetiques", "title": "Op\u00e9rateurs arithm\u00e9tiques", "text": "

    Aux 4 op\u00e9rations de base (+, -, \u00d7, \u00f7) le C ajoute l'op\u00e9ration modulo, qui est le reste d'une division enti\u00e8re.

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur Abr\u00e9viation Description Assertion vraie + add Addition 5 == 2 + 3 - sub Soustraction 8 == 12 - 4 * mul Multiplication 42 == 21 * 2 / div Division 2 == 5 / 2 % mod Modulo 13 % 4 == 1

    Lors d'op\u00e9rations, il faut faire attention aux types des variables impliqu\u00e9es. La division 5 / 2 donnera 2 et non, 2.5 car les deux valeurs fournies sont enti\u00e8res et le r\u00e9sultat est donc un entier. Pour obtenir un r\u00e9sultat flottant, il faut que l'une des valeurs soit un flottant, ici le 5 est exprim\u00e9 en double, la propagation de type fera que le r\u00e9sultat sera aussi un double :

    int a = 5 / 2;      // 2\ndouble b = 5.0 / 2; // 2.5\n

    Le modulo (mod, %) est le reste de la division enti\u00e8re. L'assertion suivante est donc vraie, car 13 divis\u00e9 par 4 \u00e9gal 3 et il reste 1\u2009:

    assert(13 % 4 == 1)\n
    \\[ \\begin{array}{rr|l} 1 & 3 & 4 \\\\ \\hline - & 8 & \\textbf{2} \\\\ & \\textbf{5} & \\\\ & & \\end{array} \\]

    Il est important de noter aussi que les op\u00e9rateurs arithm\u00e9tiques sont tributaires des types sur lesquels ils s'appliquent. Par exemple, l'addition de deux entiers 8 bits 120 + 120 ne fera pas, 240 car le type ne permet pas de stocker des valeurs plus grandes que 127 :

    int8_t too_small = 120 + 120;\nassert(too_small != 120 + 120);\n

    Nous l'avons tous appris dans les petites \u00e9coles, les op\u00e9rations arithm\u00e9tiques s'effectuent de droite \u00e0 gauche et chiffre \u00e0 chiffre. Lorsque le r\u00e9sultat de l'op\u00e9ration d\u00e9passe la capacit\u00e9 d'un chiffre, on retient une unit\u00e9 et on la reporte \u00e0 la colonne suivante. L'addition de \\(123\\) et \\(89\\) en base \\(10\\) donne \\(212\\).

    \\[ \\begin{array}{lrrr} \\phantom{1}& _1 & _1 & \\\\ & 1 & 2 & 3_{~10} \\\\ + & \\phantom{0} & 8 & 9_{~10} \\\\ \\hline & 2 & 1 & 2_{~10} \\\\ \\end{array} \\]

    L'exemple reste valable quelque soit la base, en binaire par exemple, on commence par additionner les bits de poids faible et on reporte les retenues. Ainsi en premier lieu on aura \\(1_2 + 1_2 = 10_2\\). Donc le r\u00e9sultat est \\(0\\) et la retenue (carry) est \\(1\\) :

    \\[ \\begin{array}{lrrrrrrrr} & _1 & _1 & _1 & _1 & & _1 & _1 & \\\\ & & 1 & 1 & 1 & 1 & 0 & 1 & 1_{~2} \\\\ + & & 1 & 0 & 1 & 1 & 0 & 0 & 1_{~2} \\\\ \\hline &1 & 1 & 0 & 1 & 0 & 1 & 0 & 0_{~2} \\\\ \\end{array} \\]

    En alg\u00e8bre de Boole, l'addition de deux chiffres n'a que \\(2^2 = 4\\) cas de figure (contre \\(10^2=100\\) en base \\(10\\)).

    L'addition de deux bits \\(A\\) et \\(B\\) est donn\u00e9e par la table suivante o\u00f9 C est la retenue engendr\u00e9e par l'addition\u2009:

    Addition binaire A B A + B C 0 0 0 0 0 1 1 0 1 0 1 0 1 1 0 1

    Le cas de la soustraction

    La soustraction reste une addition mais elle s'effectue sur les nombres repr\u00e9sent\u00e9s en compl\u00e9ment \u00e0 deux. On pourrait s'amuser \u00e0 soustraire deux nombres en base \\(10\\) en les repr\u00e9sentant en compl\u00e9ment \u00e0 neuf plus 1. Par exemple, pour soustraire \\(23\\) de \\(12\\) il faut repr\u00e9senter \\(12\\) en compl\u00e9ment \u00e0 neuf plus un.

    La m\u00e9thode est la m\u00eame, on effectue le compl\u00e9ment \u00e0 \\(9\\) de \\(12\\), soit \\(87\\) et on ajoute \\(1\\) pour obtenir \\(88\\). Ensuite on peut faire l'addition en \u00e9liminant le chiffre suppl\u00e9mentaire qui d\u00e9passe\u2009:

    \\[ \\begin{array}{lrrr} & _1 & _1 & \\\\ & 0 & 2 & 3_{~10} \\\\ + & 0 & 8 & 8_{~10} \\\\ \\hline & 1 & 1 & 1_{~10} \\\\ \\end{array} \\]

    L'int\u00e9r\u00eat de cette m\u00e9thode c'est qu'il s'agisse d'une addition ou d'une soustraction, c'est la m\u00eame op\u00e9ration calcul\u00e9e par l'unit\u00e9 arithm\u00e9tique et logique.

    Exercice 1\u2009: Additions binaires

    Une unit\u00e9 de calcul arithm\u00e9tique (ALU) est capable d'effectuer les 4 op\u00e9rations de bases comprenant additions et soustractions.

    Traduisez les op\u00e9randes ci-dessous en binaire, puis poser l'addition en binaire.

    1. \\(1 + 51\\)
    2. \\(51 - 7\\)
    3. \\(204 + 51\\)
    4. \\(204 + 204\\) (sur 8-bits)
    Solution

    Voici la solution du calcul en binaire\u2009:

    1. \\(1 + 51\\)

              \u00b9\u00b9\n         1\u2082\n+   110011\u2082  (2\u2075 + 2\u2074 + 2\u00b9+ 2\u2070 \u2261 51)\n----------\n    110100\u2082\n
    2. \\(51 - 7\\)

        \u2026\u00b9\u00b9\u00b9  \u00b9\u00b9\n  \u2026000110011\u2082  (2\u2075 + 2\u2074 + 2\u00b9 + 2\u2070 \u2261 51)\n+ \u2026111111001\u2082  (compl\u00e9ment \u00e0 deux, 2\u00b3 + 2\u00b9 + 2\u2070 \u2261 111\u2082 \u2192 !7 + 1 \u2261 \u2026111001\u2082)\n  -----------\n  \u2026000101100\u2082  (2\u2075 + 2\u00b3 + 2\u2082 \u2261 44)\n
    3. \\(204 + 51\\)

          11001100\u2082\n+     110011\u2082\n  -----------\n  \u2026011111111\u2082  (2\u2078 - 1 \u2261 255)\n
    4. \\(204 + 204\\) (sur 8-bits)

          \u00b9|\u00b9  \u00b9\u00b9\n     |11001100\u2082\n +   |11001100\u2082\n  ---+--------\n    1|10011000\u2082  (152, le r\u00e9sultat complet devrait \u00eatre 2\u2078 + 152 \u2261 408)\n
    ", "tags": ["double"]}, {"location": "course-c/15-fundations/operators/#operateurs-relationnels", "title": "Op\u00e9rateurs relationnels", "text": "

    Les op\u00e9rateurs relationnels permettent de comparer deux valeurs. Le r\u00e9sultat d'un op\u00e9rateur relationnel est toujours un bool\u00e9en c'est-\u00e0-dire que le r\u00e9sultat d'une comparaison est soit vrai, soit faux.

    Rappelons qu'en C et dans la plupart des langages de programmation, une valeur vraie est repr\u00e9sent\u00e9e par 1 et une valeur fausse par 0.

    Les op\u00e9rateurs relationnels sont les suivants\u2009:

    Op\u00e9rateurs relationnels Op\u00e9rateur Abr\u00e9viation Description Exemple vrai == eq \u00c9gal 42 == 0x101010 != ne Diff\u00e9rent 'a' != 'c' >= ge Sup\u00e9rieur ou \u00e9gal 9 >= 9 <= le Inf\u00e9rieur ou \u00e9gal -8 <= 8 > gt Strictement sup\u00e9rieur 0x31 > '0' < lg Strictement inf\u00e9rieur 8 < 12.33

    Un op\u00e9rateur relationnel est plus prioritaire qu'un op\u00e9rateur d'affectation et donc l'\u00e9criture suivante applique le test d'\u00e9galit\u00e9 entre a et b et le r\u00e9sultat de ce test 1 ou 0 sera affect\u00e9 \u00e0 la variable c :

    int a = 2, b = 3;\nint c = a == b;\n

    Les op\u00e9rateurs relationnels sont le plus souvent utilis\u00e9s dans des structures de contr\u00f4les\u2009:

    if (a == b) {\n    printf(\"Les op\u00e9randes sont \u00e9gaux.\\n\");\n} else {\n    printf(\"Les op\u00e9randes ne sont pas \u00e9gaux.\\n\");\n}\n

    Astuce

    Programmer c'est \u00eatre minimaliste, d\u00e8s lors il serait possible de simplifier l'\u00e9criture ci-dessus de la fa\u00e7on suivante\u2009:

    printf(\"Les op\u00e9randes %s \u00e9gaux.\\n\", a == b ? \"sont\" : \"ne sont pas\");\n

    Dans se cas on utilise l'op\u00e9rateur ternaire ? : qui permet de s'affranchir d'une structure de contr\u00f4le explicite.

    Attention lors de l'utilisation du test d'\u00e9galit\u00e9 avec des valeurs flottantes, ces derni\u00e8res sont des approximations et il est possible que deux valeurs qui devraient \u00eatre \u00e9gales ne le soient pas.

    Par exemple, cette assertion est fausse\u2009:

    assert(0.1 + 0.2 == 0.3) // false\n

    Pour comparer des valeurs flottantes, il est recommand\u00e9 d'utiliser une fonction de comparaison qui prend en compte une marge d'erreur. Par exemple, on pourrait \u00e9crire une fonction float_eq qui compare deux valeurs flottantes avec une marge d'erreur de 0.0001 :

    bool float_eq(float a, float b) {\n    return fabs(a - b) < 0.0001;\n}\n

    Alternativement on peut utiliser la d\u00e9finition FLT_EPSILON qui est la plus petite valeur positive telle que 1.0 + FLT_EPSILON != 1.0 :

    assert(fabs(0.1 + 0.2 - 0.3) < FLT_EPSILON);\n

    Voici une d\u00e9monstration\u2009:

    #include <stdio.h>\n#include <float.h>\n#include <math.h>\nint main() {\n    double u1 = 0.3, u2 = 0.1 + 0.2;\n    long long int i1 = *(long int*)&u1;\n    long long int i2 = *(long int*)&u2;\n    printf(\"Hex value of 0.3:\\t\\t0x%x\\n\", i1);\n    printf(\"Hex value of 0.3:\\t\\t0x%x\\n\", i2);\n\n    printf(\"Float value of 0.3:\\t\\t%.20f\\n\", u1);\n    printf(\"Float value of 0.1 + 0.2:\\t%.20f\\n\", u2);\n\n    printf(\"0.1 + 0.2 == 0.3: %d\\n\", u1 == u2);\n    printf(\"0.1 + 0.2 == 0.3: %d\\n\", fabs(u1 - u2) < DBL_EPSILON );\n}\n

    Le r\u00e9sultat de ce programme est le suivant\u2009:

    Hex value of 0.3:               0x33333333\nHex value of 0.3:               0x33333334\nFloat value of 0.3:             0.29999999999999998890\nFloat value of 0.1 + 0.2:       0.30000000000000004441\n0.1 + 0.2 == 0.3: 0\n0.1 + 0.2 == 0.3: 1\n

    Confusion = et ==

    L'erreur est si vite commise, mais souvent fatale\u2009:

    if (c = 'o') {\n\n}\n

    L'effet contre-intuitif est que le test retourne toujours VRAI, car 'o' > 0. Ajoutons que la valeur de c est modifi\u00e9 au passage.

    Observations\u2009:

    • Pour \u00e9viter toute ambigu\u00eft\u00e9, \u00e9viter les affectations dans les structures conditionnelles.

    Triple \u00e9galit\u00e9\u2009?

    Dans certains langages comme le JavaScript, il existe un op\u00e9rateur de comparaison === qui compare non seulement les valeurs mais aussi les types.

    En C, il n'existe pas d'op\u00e9rateur de comparaison de type, il faut donc faire attention \u00e0 ce que les types des op\u00e9randes soient compatibles.

    Voici une diff\u00e9rence entre C et JavaScript\u2009:

    CJavaScript
    assert('4' == 4); // false\nassert(4 == 4.0); // true\n
    assert('4' == 4); // true\nassert('4' === 4); // false\n
    ", "tags": ["float_eq", "FLT_EPSILON"]}, {"location": "course-c/15-fundations/operators/#operateurs-bit-a-bit", "title": "Op\u00e9rateurs bit \u00e0 bit", "text": "

    Les op\u00e9rations bit \u00e0 bit (bitwise) agissent sur chaque bit d'une valeur. Les disponibles en C sont les suivantes\u2009:

    Op\u00e9rateurs bit \u00e0 bit Op\u00e9rateur Description Exemple & Conjonction (ET) (0b1101 & 0b1010) == 0b1000 | Disjonction (OU) (0b1101 | 0b1010) == 0b1111 ^ XOR binaire (0b1101 ^ 0b1010) == 0b0111 ~ Compl\u00e9ment \u00e0 un ~0b11011010 == 0b00100101 << D\u00e9calage \u00e0 gauche (0b1101 << 3) == 0b1101000 >> D\u00e9calage \u00e0 droite (0b1101 >> 2) == 0b11

    Important

    Ne pas confondre l'op\u00e9rateur ! et l'op\u00e9rateur ~. Le premier est la n\u00e9gation d'un nombre tandis que l'autre est l'inversion bit \u00e0 bit. La n\u00e9gation d'un nombre diff\u00e9rent de z\u00e9ro donnera toujours 0 et la n\u00e9gation de z\u00e9ro donnera toujours 1.

    "}, {"location": "course-c/15-fundations/operators/#conjonction", "title": "Conjonction", "text": "

    La conjonction ou ET logique (\\(\\wedge\\)) est identique \u00e0 la multiplication appliqu\u00e9e bit \u00e0 bit et ne g\u00e9n\u00e8re pas de retenue.

    Conjonction bit \u00e0 bit \\(A \u2227 B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 0 \\(B=1\\) 0 1

    Avec cette op\u00e9ration l'\u00e9tat dominant est le 0 et l'\u00e9tat r\u00e9cessif est le 1. Il suffit qu'une seule valeur soit \u00e0 z\u00e9ro pour forcer le r\u00e9sultat \u00e0 z\u00e9ro\u2009:

    assert(0b1100 & 0b0011 == 0b0000)\n

    Cet op\u00e9rateur est d'ailleurs souvent utilis\u00e9 pour imposer une valeur nulle suivant une condition. Dans l'exemple suivant, le Balrog est r\u00e9duit \u00e0 n\u00e9ant par Gandalf le gris\u2009:

    balrog = 0b1100110101;\ngandalf = 0;\n\nbalrog = balrog & gandalf; // You shall not pass!\n

    "}, {"location": "course-c/15-fundations/operators/#disjonction", "title": "Disjonction", "text": "

    La disjonction ou OU logique (\\(\\lor\\)) s'apparente \u00e0 l'op\u00e9ration +.

    Disjonction bit \u00e0 bit \\(A \u2228 B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 1 \\(B=1\\) 1 1

    Ici l'\u00e9tat dominant est le 1 car il force n'importe quel 0 \u00e0 changer d'\u00e9tat\u2009:

    bool student = false; // Veut pas faire ses devoirs ?\nbool teacher = true;\n\nstudent = student | teacher; // Tes devoirs tu feras...\n

    "}, {"location": "course-c/15-fundations/operators/#disjonction-exclusive", "title": "Disjonction exclusive", "text": "

    Le OU exclusif (\\(\\oplus\\) ou \\(\\veebar\\)) est une op\u00e9ration curieuse, mais extr\u00eamement puissante et utilis\u00e9e massivement en cryptographie.

    En \u00e9lectronique sur les symboles CEI, l'op\u00e9ration logique est nomm\u00e9e, =1 car si le r\u00e9sultat de l'addition des deux op\u00e9randes est diff\u00e9rent de 1, la sortie sera nulle. Lorsque A et B valent 1 la somme vaut 2 et donc la sortie est nulle.

    Disjonction exclusive \\(A \\veebar B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 1 \\(B=1\\) 1 0

    L'op\u00e9ration pr\u00e9sente une propri\u00e9t\u00e9 tr\u00e8s int\u00e9ressante\u2009: elle est r\u00e9versible.

    assert(1542 ^ 42 ^ 42 == 1542)\n

    Par exemple il est possible d'inverser la valeur de deux variables simplement\u2009:

    int a = 123;\nint b = 651;\n\na ^= b;\nb ^= a;\na ^= b;\n\nassert(a == 651);\nassert(b == 123);\n

    Attention

    Attention avec cet exemple, il ne fonctionne que si les deux valeurs a et b ont des adresses m\u00e9moires diff\u00e9rentes. Si les deux variables pointent vers la m\u00eame adresse m\u00e9moire, le r\u00e9sultat sera 0.

    int a = 123;\nint *b = &a;\n\na ^= *b;\n*b ^= a;\na ^= *b;\n\nassert(a == 0);\nassert(*b == 0);\n

    "}, {"location": "course-c/15-fundations/operators/#complement-a-un", "title": "Compl\u00e9ment \u00e0 un", "text": "

    Le compl\u00e9ment \u00e0 un (\\(\\lnot\\)) est simplement la valeur qui permet d'inverser bit \u00e0 bit une valeur\u2009:

    Compl\u00e9ment \u00e0 un \\(A\\) \\(\\lnot~A\\) 0 1 1 0

    Info

    Pourquoi l'op\u00e9ration s'appelle le compl\u00e9ment \u00e0 un\u2009? Compl\u00e9menter \u00e0 \\(N\\) signifie trouver la valeur telle que \\(A + \\lnot A = N\\). Par exemple, en base 10, pour compl\u00e9menter \u00e0 \\(A = 9\\) il suffit de faire \\(9 - A\\).

    En base 10, le symbole le plus grand est 9. En binaire le symbole le plus grand est 1. Donc pour inverser un nombre on compl\u00e9mente chaque bit \u00e0 un.

    "}, {"location": "course-c/15-fundations/operators/#decalages", "title": "D\u00e9calages", "text": "

    Les op\u00e9rations de d\u00e9calage permettent de d\u00e9placer les bits d'une valeur vers la gauche ou vers la droite. Les bits d\u00e9cal\u00e9s sont perdus et remplac\u00e9s par des z\u00e9ros dans le cas d'une valeur non sign\u00e9e et par le bit de signe dans le cas d'une valeur sign\u00e9e.

    assert(0b0000'1101 << 2 == 0b0011'0100)\nassert(0b0001'0000 >> 4 == 0b0000'0001)\n\nchar a = 0b1000'0000;\nchar b = a << 1;\nassert(b == 0x0000'0000);\n\nassert(-8 >> 1 == -4) // 0b1111'1000 >> 1 == 0b1111'1100\n

    Avertissement

    Le standard ne d\u00e9finit pas le comportement des d\u00e9calages pour des valeurs de d\u00e9calage n\u00e9gatives (a >> -2). N\u00e9anmoins il n'y aura pas d'erreur de compilation, le comportement est simplement ind\u00e9fini et le r\u00e9sultat d\u00e9pend donc du compilateur utlis\u00e9.

    "}, {"location": "course-c/15-fundations/operators/#tester-un-bit", "title": "Tester un bit", "text": "

    En micro-informatique, il est fr\u00e9quent de tester l'\u00e9tat d'un bit. Pour cela on utilise l'op\u00e9ration ET logique & avec un masque. Par exemple, pour tester le bit de poids faible d'une valeur, a on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint mask = 0b0000'0001;\n\nif (a & mask) {\n    printf(\"Le bit de poids faible est \u00e0 1.\\n\");\n} else {\n    printf(\"Le bit de poids faible est \u00e0 0.\\n\");\n}\n

    En pratique il est pr\u00e9f\u00e9rable de num\u00e9roter le bit que l'on souhaite tester pour plus de clart\u00e9. On peut positionner un bit \u00e0 la position souhait\u00e9e avec l'op\u00e9ration de d\u00e9calage << :

    int a = 0b1101'1010;\nint bit = 1;\nprintf(\"Le bit %d \u00e0 %d.\\n\", bit, a & (1 << bit));\n
    "}, {"location": "course-c/15-fundations/operators/#inverser-un-bit", "title": "Inverser un bit", "text": "

    Pour inverser un bit, on utilise l'op\u00e9ration XOR ^ avec un masque. Par exemple, pour inverser le bit de poids faible d'une valeur a on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 3;\nint b = a ^ (1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#forcer-un-bit-a-un", "title": "Forcer un bit \u00e0 un", "text": "

    Pour forcer un bit \u00e0 un, on utilise l'op\u00e9ration OU | avec un masque. Par exemple, pour forcer le bit de poids faible d'une valeur a \u00e0 un on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 2;\nint b = a | (1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#forcer-un-bit-a-zero", "title": "Forcer un bit \u00e0 z\u00e9ro", "text": "

    Pour forcer un bit \u00e0 z\u00e9ro, on utilise l'op\u00e9ration ET & avec un masque invers\u00e9. Par exemple, pour forcer le bit de poids faible d'une valeur a \u00e0 z\u00e9ro on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 4;\nint b = a & ~(1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#operations-logiques", "title": "Op\u00e9rations logiques", "text": "

    Les op\u00e9rateurs logiques sont au nombre de deux et ne doivent pas \u00eatre confondus avec leur petits fr\u00e8res & et |.

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur ISO646 Description Assertion vraie && and ET logique true && false == false || or OU logique true || false == true

    Le r\u00e9sultat d'une op\u00e9ration logique est toujours un bool\u00e9en (valeur 0 ou 1). Ainsi l'expression suivante affecte 1 \u00e0 x : x = 12 && 3 + 2.

    La priorit\u00e9 des op\u00e9rateurs logiques est plus faible que celle des op\u00e9rateurs de comparaison et plus forte que celle des op\u00e9rateurs d'affectation. Ainsi l'expression a == b && c == d est \u00e9quivalente \u00e0 (a == b) && (c == d). Les parenth\u00e8ses sont facultatives, mais permettent de clarifier l'expression.

    Avertissement

    La priorit\u00e9 de l'op\u00e9rateur && est plus forte que celle de l'op\u00e9rateur ||. Ainsi l'expression a || b && c est \u00e9quivalente \u00e0 a || (b && c). C'est un pi\u00e8ge classique en programmation, pour l'\u00e9viter il est recommand\u00e9 d'utiliser des parenth\u00e8ses.

    Confusion & et &&

    Confondre le ET logique et le ET binaire est courant. Dans l'exemple suivant, le if n'est jamais ex\u00e9cut\u00e9\u2009:

    int a = 0xA;\nint b = 0x5;\n\nif(a & b) {\n\n}\n
    ", "tags": ["bool\u00e9en"]}, {"location": "course-c/15-fundations/operators/#operateurs-daffectation", "title": "Op\u00e9rateurs d'affectation", "text": "

    Les op\u00e9rateurs d'affectation permettent d'assigner de nouvelles valeurs \u00e0 une variable. En C, il existe des sucres syntaxiques permettant de simplifier l'\u00e9criture lorsqu'une affectation est coupl\u00e9e \u00e0 un autre op\u00e9rateur.

    Originellement, la syntaxe h\u00e9rit\u00e9e de l'Algol-68 \u00e9tait de positionner le symbole = \u00e0 gauche suivi de l'op\u00e9rateur arithm\u00e9tique. Cette forme \u00e9tait confuse, car elle pouvait mener \u00e0 des incoh\u00e9rences d'\u00e9criture. Par exemple, l'expression x =- 3 peut \u00eatre confondue avec x = -3. Pour \u00e9viter ces ambigu\u00eft\u00e9s, le C a invers\u00e9 la logique en pla\u00e7ant l'op\u00e9rateur arithm\u00e9tique \u00e0 gauche de l'op\u00e9rateur d'affectation. L'histoire apporte parfois des r\u00e9ponses l\u00e0 o\u00f9 la logique \u00e9choue...

    Voici la liste des diff\u00e9rents op\u00e9rateurs d'affectation\u2009:

    Op\u00e9rateurs d'affectation Op\u00e9rateur Description Exemple \u00c9quivalence = Affectation simple x = y x = y += Affectation par addition x += y x = x + y -= Affectation par soustraction x -= y x = x - y *= Affectation par multiplication x *= y x = x * y /= Affectation par division x /= y x = x / y %= Affectation par modulo x %= y x = x % y &= Affectation par conjonction x &= y x = x & y |= Affectation par disjonction x |= y x = x | y ^= Affectation par XOR x ^= y x = x ^ y <<= Affectation par d\u00e9calage gauche x <<= y x = x << y >>= Affectation par d\u00e9calage droite x >>= y x = x >> y

    Un op\u00e9rateur d'affectation implique que la valeur \u00e0 gauche de l'\u00e9galit\u00e9 soit modifiable (lvalue). Ainsi l'expression 3 += 2 est incorrecte, car 3 est une constante et ne peut \u00eatre modifi\u00e9e.

    Exercice 2\u2009: R-value

    Est-ce que l'expression suivante est valide\u2009?

    int a, b, c = 42;\na + b = c;\n
    • Oui car la destination est une lvalue
    • Non car la destination est une rvalue
    Solution

    L'op\u00e9ration + entre deux nombre retourne une rvalue et ne peut donc pas \u00eatre affect\u00e9. L'expression est donc invalide.

    "}, {"location": "course-c/15-fundations/operators/#operateurs-dincrementation", "title": "Op\u00e9rateurs d'incr\u00e9mentation", "text": "

    Les op\u00e9rateurs d'incr\u00e9mentation sont r\u00e9guli\u00e8rement un motif primaire d'arrachage de cheveux pour les \u00e9tudiants. En effet, ces op\u00e9rateurs sont tr\u00e8s particuliers en ce sens qu'il se d\u00e9composent en deux \u00e9tapes\u2009: l'affectation et l'obtention du r\u00e9sultat. Il existe 4 op\u00e9rateurs d'incr\u00e9mentation\u2009:

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur Description Assertion vraie ()++ Post-incr\u00e9mentation i++ ++() Pr\u00e9-incr\u00e9mentation ++i ()-- Post-d\u00e9cr\u00e9mentation i-- --() Pr\u00e9-d\u00e9cr\u00e9mentation --i

    Ces op\u00e9rateurs furent con\u00e7us initialement par Ken Thompson pour le langage B, le pr\u00e9d\u00e9cesseur du C. Ils ont \u00e9t\u00e9 repris par Dennis Ritchie pour le C. Une croyance est que ces op\u00e9rateurs furent rest\u00e9s dans le langage, car le PDP-11, la machine sur laquelle le C fut d\u00e9velopp\u00e9, poss\u00e9dait des instructions sp\u00e9cifiques pour ces op\u00e9rations.

    La pr\u00e9incr\u00e9mentation ou pr\u00e9d\u00e9cr\u00e9mentation effectue en premier la modification de la variable impliqu\u00e9e puis retourne le r\u00e9sultat de cette variable modifi\u00e9e. Dans le cas de la post-incr\u00e9mentation ou pr\u00e9d\u00e9cr\u00e9mentation, la valeur actuelle de la variable est d'abord retourn\u00e9e, puis dans un second temps cette variable est incr\u00e9ment\u00e9e.

    Notons qu'on peut toujours d\u00e9composer ces op\u00e9rateurs en deux instructions explicites. Le code\u2009:

    Forme r\u00e9duiteForme \u00e9tendue
    y = x++;\n\ny = ++x;\n
    y = x;\nx = x + 1;\n\nx = x + 1;\ny = x;\n

    Astuce

    Pour r\u00e9soudre les ambigu\u00eft\u00e9s, on proc\u00e8de par \u00e9tape. Par exemple l'expression suivante n'est pas tr\u00e8s claire\u2009:

    k = i++ * 4 + --j * 2\n
    1. On commence par r\u00e9soudre les pr\u00e9-incr\u00e9mentation et pr\u00e9-d\u00e9cr\u00e9mentation\u2009:

      j = j - 1;\nk = i++ * 4 + j * 2\n
    2. Ensuite on r\u00e9sout les post-incr\u00e9mentation\u2009:

      j = j - 1;\nk = i * 4 + j * 2\ni = i + 1;\n
    3. On peut utiliser les sucres syntaxiques pour simplifier l'\u00e9criture\u2009:

      j -= 1;\nk = i * 4 + j * 2\ni += 1;\n

    \u00c9criture d\u00e9routante

    Selon la table de pr\u00e9c\u00e9dences on aura i-- calcul\u00e9 en premier suivi de - -j:

    k = i----j;\n

    Observations\u2009:

    • \u00c9viter les formes ambig\u00fces d'\u00e9criture
    • Favoriser la pr\u00e9c\u00e9dence explicite en utilisant des parenth\u00e8ses
    • S\u00e9parez vos op\u00e9rations par des espaces pour plus de lisibilit\u00e9\u2009: k = i-- - -j

    Astuce

    Il est g\u00e9n\u00e9ralement pr\u00e9f\u00e9rable d'utiliser la pr\u00e9-incr\u00e9mentation ou la pr\u00e9-d\u00e9cr\u00e9mentation car elles sont plus efficaces. En effet, la post-incr\u00e9mentation ou la post-d\u00e9cr\u00e9mentation n\u00e9cessitent de stocker la valeur actuelle de la variable pour la retourner apr\u00e8s l'incr\u00e9mentation ou la d\u00e9cr\u00e9mentation.

    C'est particuli\u00e8rement le cas en C++ o\u00f9 la post-incr\u00e9mentation ou la post-d\u00e9cr\u00e9mentation n\u00e9cessitent de cr\u00e9er une copie de la variable avant de l'incr\u00e9menter ou de la d\u00e9cr\u00e9menter.

    En C++ on utilise la surcharge d'op\u00e9rateur pour d\u00e9finir le comportement de l'op\u00e9rateur ++ et -- pour les classes personnalis\u00e9es. Ajouter \u00e0 une classe ce type de surcharge se fait comme ceci\u2009:

    class MyClass {\npublic:\n    // Pr\u00e9-incr\u00e9mentation\n    auto operator++() {\n        return *this;\n    }\n\n    // Post-incr\u00e9mentation\n    auto operator++(int) {\n        MyClass tmp(*this);\n        operator++();\n        return tmp;\n    }\n};\n

    On voit que la post-incr\u00e9mentation cr\u00e9e une copie de l'objet avant de l'incr\u00e9menter, elle est donc moins efficace.

    Donc dans une boucle for on pr\u00e9f\u00e9rera\u2009:

    for (int i = 0; i < 10; ++i) { }\n

    Plut\u00f4t que

    for (int i = 0; i < 10; i++) { }\n

    ", "tags": ["for"]}, {"location": "course-c/15-fundations/operators/#operateur-ternaire", "title": "Op\u00e9rateur ternaire", "text": "

    L'op\u00e9rateur ternaire aussi appel\u00e9 op\u00e9rateur conditionnel permet de faire un test et de retourner soit le second op\u00e9rande, soit le troisi\u00e8me op\u00e9rande. C'est le seul op\u00e9rateur du C avec une arit\u00e9 de 3. Chacun des op\u00e9randes est symbolis\u00e9 avec une paire de parenth\u00e8ses\u2009:

    ()?():()\n

    Cet op\u00e9rateur permet sur une seule ligne d'\u00e9valuer une expression et de renvoyer une valeur ou une autre selon que l'expression est vraie ou fausse.

    valeur = (condition ? valeur si condition vraie : valeur si condition fausse);\n

    Note

    Seule la valeur utilis\u00e9e pour le r\u00e9sultat est \u00e9valu\u00e9e. Par exemple, dans le code x > y ? ++y : ++x, seulement x ou y sera incr\u00e9ment\u00e9.

    On utilise volontiers cet op\u00e9rateur lorsque dans les deux cas d'un embranchement, la m\u00eame valeur est modifi\u00e9e\u2009:

    if (a > b)\n    max = a;\nelse\n    min = b;\n

    On remarque dans cet exemple une r\u00e9p\u00e9tition max =. Une fa\u00e7on plus \u00e9l\u00e9gante et permettant de r\u00e9duire l'\u00e9criture est d'utiliser l'op\u00e9rateur ternaire\u2009:

    max = a > b ? a : b;\n

    Avertissement

    Ne pas utiliser l'op\u00e9rateur ternaire si vous ne modifiez pas une valeur. L'op\u00e9rateur ternaire est un op\u00e9rateur de s\u00e9lection et non de modification.

    Bon exempleMauvais exemple
    int max = a > b ? a : b;\n
    a > b ? max = a : min = b;\n

    Cela va de m\u00eame pour afficher une valeur\u2009:

    Bon exempleMauvais exemple
    printf(\"Le maximum est %d\\n\", a > b ? a : b);\n
    a > b ? printf(\"Le maximum est %d\\n\", a) : printf(\"Le maximum est %d\\n\", b);\n

    Enfin, on notera que le r\u00e9sultat de l'op\u00e9rateur ternaire est une rvalue et ne peut donc pas \u00eatre modifi\u00e9e.

    ", "tags": ["arit\u00e9"]}, {"location": "course-c/15-fundations/operators/#operateur-de-transtypage", "title": "Op\u00e9rateur de transtypage", "text": "

    Le transtypage ou cast permet de modifier explicitement le type apparent d'une variable. C'est un op\u00e9rateur particulier, car son premier op\u00e9rande doit \u00eatre un type et le second une valeur.

    (type)(valeur)\n

    Dans l'exemple suivant, le r\u00e9sultat de la division est un entier, car la promotion implicite de type reste un entier int. La valeur c vaudra donc le r\u00e9sultat de la division enti\u00e8re alors que dans le second cas, b est cast\u00e9 en un double ce qui force une division en virgule flottante.

    int a = 5, b = 2;\ndouble c = a / b;\ndouble d = a / (double)(b);\nassert(c == 2.0 && d == 2.5);\n

    ", "tags": ["transtypage", "int", "double"]}, {"location": "course-c/15-fundations/operators/#operateur-sequentiel", "title": "Op\u00e9rateur s\u00e9quentiel", "text": "

    L'op\u00e9rateur s\u00e9quentiel (comma operator) permet l'ex\u00e9cution ordonn\u00e9e d'op\u00e9rations, et retourne la derni\u00e8re valeur. Son utilisation est couramment limit\u00e9e, soit aux d\u00e9clarations de variables, soit au boucles for:

    for (size_t i = 0, j = 10; i != j; i++, j--) { /* ... */ }\n

    Dans le cas ci-dessus, il n'est pas possible de s\u00e9parer les instructions i++ et j-- par un point-virgule, l'op\u00e9rateur virgule permet alors de combiner plusieurs instructions en une seule.

    Une particularit\u00e9 de cet op\u00e9rateur est que seule la derni\u00e8re valeur est retourn\u00e9e\u2009:

    assert(3 == (1, 2, 3))\n

    L'op\u00e9rateur agit \u00e9galement comme un Point de s\u00e9quence , c'est-\u00e0-dire que l'ordre des \u00e9tapes est respect\u00e9.

    Exercice 3\u2009: Op\u00e9rateur s\u00e9quentiel

    Que sera-t-il affich\u00e9 \u00e0 l'\u00e9cran\u2009?

    int i = 0;\nprintf(\"%d\", (++i, i++, ++i));\n
    ", "tags": ["for"]}, {"location": "course-c/15-fundations/operators/#operateur-sizeof", "title": "Op\u00e9rateur sizeof", "text": "

    Cet op\u00e9rateur est unaire et retourne la taille en byte de la variable ou du type pass\u00e9 en argument. Il n'existe pas de symbole particulier et son usage est tr\u00e8s similaire \u00e0 l'appel d'une fonction\u2009:

    int32_t foo = 42;\nassert(sizeof(foo) == 4);\nassert(sizeof(int64_t) == 64 / 8);\n

    L'op\u00e9rateur sizeof est tr\u00e8s utile durant le d\u00e9bogage pour conna\u00eetre la taille en m\u00e9moire d'une variable ou celle d'un type. On l'utilise en pratique pour conna\u00eetre la taille d'un tableau lors d'une boucle it\u00e9rative\u2009:

    int32_t array[128];\nfor (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n   array[i] = i * 10;\n}\n

    Dans l'exemple ci-dessus, sizeof(array) retourne la taille de l'espace m\u00e9moire occup\u00e9 par le tableau array, soit \\(128 \\cdot 4\\) bytes. Pour obtenir le nombre d'\u00e9l\u00e9ments dans le tableau, il faut alors diviser ce r\u00e9sultat par la taille effective de chaque \u00e9l\u00e9ment du tableau. L'\u00e9l\u00e9ment array[0] est donc un int32_t et sa taille vaut donc 4 bytes.

    Note

    Dans l'exemple ci-dessus, il est possible de s'affranchir de la taille effective du tableau en utilisant une sentinelle. Si le dernier \u00e9l\u00e9ment du tableau \u00e0 une valeur particuli\u00e8re et que le reste est initialis\u00e9 \u00e0 z\u00e9ro, il suffit de parcourir le tableau jusqu'\u00e0 cette valeur\u2009:

    int32_t array[128] = { [127]=-1 };\nint i = 0;\nwhile (array[i] != -1) {\n    array[i++] = i * 10;\n}\n

    Cette \u00e9criture reste malgr\u00e9 tout tr\u00e8s mauvaise, car le tableau de 128 \u00e9l\u00e9ments doit \u00eatre initialis\u00e9 \u00e0 priori, ce qui m\u00e8ne aux m\u00eames performances. D'autre part l'histoire racont\u00e9e par le d\u00e9veloppeur est moins claire que la premi\u00e8re impl\u00e9mentation.

    ", "tags": ["int32_t", "sizeof", "array"]}, {"location": "course-c/15-fundations/operators/#priorite-des-operateurs", "title": "Priorit\u00e9 des op\u00e9rateurs", "text": "

    La pr\u00e9c\u00e9dence est un anglicisme de precedence (priorit\u00e9) qui concerne la priorit\u00e9 des op\u00e9rateurs, ou l'ordre dans lequel les op\u00e9rateurs sont ex\u00e9cut\u00e9s.

    Chacun conna\u00eet la priorit\u00e9 des quatre op\u00e9rateurs de base (+, -, *, /), mais le C et ses nombreux op\u00e9rateurs sont bien plus complexes.

    La table suivante indique les r\u00e8gles \u00e0 suivre pour les pr\u00e9c\u00e9dences des op\u00e9rateurs en C.

    Priorit\u00e9 des op\u00e9rateurs Priorit\u00e9 Op\u00e9rateur Description Associativit\u00e9 1 ++, -- Postfix incr\u00e9ments/d\u00e9cr\u00e9ments Gauche \u00e0 Droite () Appel de fonction [] Indexage des tableaux . \u00c9l\u00e9ment d'une structure -> \u00c9l\u00e9ment d'une structure 2 ++, -- Pr\u00e9fixe incr\u00e9ments/d\u00e9cr\u00e9ments Droite \u00e0 Gauche +, - Signe !, ~ NON logique et NON binaire (type) Cast (Transtypage) * Indirection, d\u00e9r\u00e9f\u00e9rencement & Adresse de... sizeof Taille de... 3 *, /, % Multiplication, Division, Mod Gauche \u00e0 Droite 4 +, - Addition, soustraction 5 <<, >> D\u00e9calages binaires 6 <, <= Comparaison plus petit que >, >= Comparaison plus grand que 7 ==, != \u00c9galit\u00e9, non \u00e9galit\u00e9 8 & ET binaire 9 ^ OU exclusif binaire 10 | OU inclusif binaire 11 && ET logique 12 || OU logique 13 ?: Op\u00e9rateur ternaire Droite \u00e0 Gauche 14 = Assignation simple +=, -= Assignation par somme/diff *=, /=, %= Assignation par produit/quotient/modulo <<=, >>= Assignation par d\u00e9calage binaire 15 , Virgule Gauche \u00e0 Droite

    Consid\u00e9rons l'exemple suivant\u2009:

    int i[2] = {10, 20};\nint y = 3;\n\nx = 5 + 23 + 34 / ++i[0] & 0xFF << y;\n

    Selon la pr\u00e9c\u00e9dence de chaque op\u00e9rateur ainsi que son associativit\u00e9 on a\u2009:

    []  1\n++  2\n/   3\n+   4\n+   4\n<<  5\n&   8\n=   14\n

    Notation polonaise invers\u00e9e

    La notation polonaise invers\u00e9e (Reverse Polish Notation) est une notation math\u00e9matique o\u00f9 les op\u00e9rateurs sont plac\u00e9s apr\u00e8s leurs op\u00e9randes.

    L'\u00e9criture en notation polonaise invers\u00e9e donnerait alors\u2009:

    34, i, 0, [], ++,  /, 5, 23, +, +, 0xFF, y, <<, &, x, =\n

    C'est une notation tr\u00e8s utilis\u00e9e en informatique pour les calculatrices et les compilateurs car elle permet de simplifier l'\u00e9criture des expressions math\u00e9matiques, et surtout s'affranchir du probl\u00e8me des priorit\u00e9s d'op\u00e9rateurs.

    L'algorithme de Shunting Yard permet de convertir une expression en notation infix\u00e9e en une expression en notation polonaise invers\u00e9e.

    ", "tags": ["sizeof"]}, {"location": "course-c/15-fundations/operators/#associativite", "title": "Associativit\u00e9", "text": "

    L'associativit\u00e9 des op\u00e9rateurs (operator associativity) d\u00e9crit la mani\u00e8re dont sont \u00e9valu\u00e9es les expressions.

    Une associativit\u00e9 \u00e0 gauche pour l'op\u00e9rateur ~ signifie que l'expression a ~ b ~ c sera \u00e9valu\u00e9e ((a) ~ b) ~ c alors qu'une associativit\u00e9 \u00e0 droite sera a ~ (b ~ (c)).

    Note qu'il ne faut pas confondre l'associativit\u00e9 \u00e9valu\u00e9e de gauche \u00e0 droite qui est une associativit\u00e9 \u00e0 gauche.

    "}, {"location": "course-c/15-fundations/operators/#promotion-de-type", "title": "Promotion de type", "text": "

    Nous avons vu au chapitre sur les types de donn\u00e9es que les types C d\u00e9finis par d\u00e9faut sont repr\u00e9sent\u00e9s en m\u00e9moire sur 1, 2, 4 ou 8 octets. On comprend ais\u00e9ment que plus cette taille est importante, plus on gagne en pr\u00e9cision ou en grandeur repr\u00e9sentable. La promotion num\u00e9rique r\u00e9git les conversions effectu\u00e9es implicitement par le langage C lorsqu'on convertit une donn\u00e9e d'un type vers un autre. Cette promotion tend \u00e0 conserver le maximum de pr\u00e9cision lorsqu'on effectue des calculs entre types diff\u00e9rents (p.ex\u2009: l'addition d'un int avec un double donne un type double). Voici les r\u00e8gles de base\u2009:

    • les op\u00e9rateurs ne peuvent agir que sur des types identiques\u2009;
    • quand les types sont diff\u00e9rents, il y a conversion automatique vers le type ayant le plus grand pouvoir de repr\u00e9sentation\u2009;
    • les conversions ne sont faites qu'au fur et \u00e0 mesure des besoins.

    La promotion est l'action de promouvoir un type de donn\u00e9e en un autre type de donn\u00e9e plus g\u00e9n\u00e9ral. On parle de promotion implicite des entiers lorsqu'un type est promu en un type plus grand automatiquement par le compilateur.

    ", "tags": ["int", "double"]}, {"location": "course-c/15-fundations/operators/#lois-de-de-morgan", "title": "Lois de De Morgan", "text": "

    Les lois de De Morgan sont des identit\u00e9s logiques formul\u00e9es il y a pr\u00e8s de deux si\u00e8cles par Augustus De Morgan (1806-1871). \u00c0 noter que l'on peut prononcer d\u0259 m\u0254\u0281.g\u0251\u0303 (de Mort Gant) ou d\u0259 m\u0254\u0281.\u0261an (de Morgane).

    En logique classique, la n\u00e9gation d'une conjonction implique la disjonction des n\u00e9gations et la conjonction de n\u00e9gations implique la n\u00e9gation d'une disjonction. On peut donc \u00e9crire les relations suivantes\u2009:

    \\[ \\begin{aligned} & \\overline{P \\land Q} &\\Rightarrow~& \\overline{P} \\lor \\overline{Q} \\\\ & \\overline{P} \\land \\overline{Q} &\\Rightarrow~& \\overline{P \\lor Q} \\end{aligned} \\]

    Ces op\u00e9rations logiques sont tr\u00e8s utiles en programmation o\u00f9 elles permettent de simplifier certains algorithmes.

    \u00c0 titre d'exemple, les op\u00e9rations suivantes sont \u00e9quivalentes\u2009:

    int a = 0b110010011;\nint b = 0b001110101;\n\nassert(a | b == ~a & ~b);\nassert(~a & ~b == ~(a | b));\n

    En logique bool\u00e9enne on exprime la n\u00e9gation par une barre p.ex. \\(\\overline{P}\\).

    Exercice 4\u2009: De Morgan

    Utiliser les relations de De Morgan pour simplifier l'expression suivante

    \\[ D \\cdot E + \\overline{D} + \\overline{E} \\] Solution

    Si l'on applique De Morgan (\\(\\overline{XY} = \\overline{X} + \\overline{Y}\\)):

    \\[ D \\cdot E + \\overline{D} + \\overline{E} \\]

    "}, {"location": "course-c/15-fundations/operators/#arrondis", "title": "Arrondis", "text": "

    En programmation, la notion d'arrondi (rounding) est beaucoup plus d\u00e9licate que l'on peut l'imaginer de prime abord.Un nombre r\u00e9el dans \\(\\mathbb{R}\\) peut \u00eatre converti en un nombre entier de plusieurs mani\u00e8res. Les m\u00e9thodes les plus courantes sont donn\u00e9es dans la table suivante.

    M\u00e9thodes d'arrondi M\u00e9thode Description truncate Suppression de la partie fractionnaire ceiling Arrondi \u00e0 l'entier sup\u00e9rieur floor Arrondi \u00e0 l'entier inf\u00e9rieur towards zero Arrondi en direction du z\u00e9ro away from zero Arrondi loin du z\u00e9ro to the nearest integer Arrondi au plus proche entier rounding half up Arrondi la moiti\u00e9 en direction de l'infini rounding half to even Arrondi la moiti\u00e9 vers l'entier pair le plus proche

    Selon le langage de programmation et la m\u00e9thode utilis\u00e9e, le m\u00e9canisme d'arrondi sera diff\u00e9rent. En C, la biblioth\u00e8que math\u00e9matique offre les fonctions ceil pour l'arrondi au plafond (entier sup\u00e9rieur), floor pour arrondi au plancher (entier inf\u00e9rieur) et round pour l'arrondi au plus proche (nearest). Il existe \u00e9galement une fonction trunc qui tronque la valeur en supprimant la partie fractionnaire.

    Le fonctionnement de la fonction round n'est pas unanime entre les math\u00e9maticiens et les programmeurs. C utilise l'arrondi au plus proche, c'est-\u00e0-dire que -23.5 donne -24 et 23.5 donnent 24.

    La m\u00e9thode rounding half to even est aussi nomm\u00e9e bankers' rounding. Elle est utilis\u00e9e dans de nombreux langages de programmation, car elle minimise les erreurs d'arrondi. Cette m\u00e9thode arrondit les nombres \u00e0 l'entier pair le plus proche. Par exemple, 0.5 est arrondi \u00e0 0 et 1.5 est arrondi \u00e0 2. C'est la m\u00e9thode d'arrondi conseill\u00e9e par l'IEEE 754 pour les calculs en virgule flottante. Cette m\u00e9thode est pr\u00e9f\u00e9r\u00e9e pour deux raisons principales\u2009:

    1. R\u00e9duction du biais cumulatif : Lorsque vous arrondissez toujours vers le haut ou vers le bas en cas de valeur \u00e0 mi-chemin (comme 0.5), cela introduit un biais syst\u00e9matique dans vos donn\u00e9es. Par exemple, si vous arrondissez toujours 0.5 vers le haut, la somme des valeurs arrondies sera syst\u00e9matiquement plus grande que la somme des valeurs originales.

    2. Statistiques plus pr\u00e9cises : En arrondissant les valeurs \u00e0 la paire la plus proche, vous distribuez les erreurs d'arrondissement de mani\u00e8re plus \u00e9quitable, ce qui donne des statistiques globales plus pr\u00e9cises.

    Supposons que nous avons les montants suivants\u2009:

    3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5\n

    En utilisant l'arrondi classique (toujours vers le haut \u00e0 0.5), nous obtenons\u2009:

    4 + 5 + 6 + 7 + 8 + 9 + 10 = 49\n

    En utilisant l'arrondi commercial, nous obtenons\u2009:

    4 + 4 + 6 + 6 + 8 + 8 + 10 = 46\n

    Si on compare \u00e0 la somme r\u00e9elle des valeurs, on obtient\u2009:

    3.5 + 4.5 + 5.5 + 6.5 + 7.5 + 8.5 + 9.5 = 45.5\n

    La m\u00e9thode round half to even donne une somme arrondie (46) qui est plus proche de la somme r\u00e9elle (45.5) que la m\u00e9thode classique (49).

    L'utilisation cette m\u00e9thode est particuli\u00e8rement utile dans les domaines o\u00f9 l'exactitude statistique est cruciale et o\u00f9 les erreurs d'arrondissement peuvent s'accumuler sur de grands ensembles de donn\u00e9es, comme en finance, en analyse de donn\u00e9es, et en statistiques.

    ", "tags": ["trunc", "floor", "round", "ceil"]}, {"location": "course-c/15-fundations/operators/#valeurs-gauches", "title": "Valeurs gauches", "text": "

    Une valeur gauche (lvalue) est une particularit\u00e9 de certains langages de programmation qui d\u00e9finissent ce qui peut se trouver \u00e0 gauche d'une affectation. Ainsi dans x = y, x est une valeur gauche. N\u00e9anmoins, l'expression x = y est aussi une valeur gauche\u2009:

    int x, y, z;\n\nx = y = z;    // 1\n(x = y) = z;  // 2\n
    1. L'associativit\u00e9 de = est \u00e0 droite donc cette expression est \u00e9quivalente \u00e0 x = (y = (z)) qui \u00e9vite toute ambigu\u00eft\u00e9.

    2. En for\u00e7ant l'associativit\u00e9 \u00e0 gauche, on essaie d'assigner z \u00e0 une lvalue et le compilateur s'en plaint\u2009:

    4:8: error: lvalue required as left operand of assignment\n  (x = y) = z;\n          ^\n

    Voici quelques exemples de valeurs gauches\u2009:

    • x /= y
    • ++x
    • (x ? y : z)

    Par analogie une rvalue est une valeur qui ne peut se trouver \u00e0 gauche d'une affectation. Ainsi x + y est une rvalue car elle ne peut \u00eatre affect\u00e9e. De m\u00eame que x++ est une rvalue car elle ne peut \u00eatre affect\u00e9e.

    ", "tags": ["lvalue"]}, {"location": "course-c/15-fundations/operators/#optimisation", "title": "Optimisation", "text": "

    Le compilateur est en r\u00e8gle g\u00e9n\u00e9ral plus malin que le d\u00e9veloppeur. L'optimiseur de code (lorsque compil\u00e9 avec -O2 sous gcc), va regrouper certaines instructions, modifier l'ordre de certaines d\u00e9clarations pour r\u00e9duire soit l'empreinte m\u00e9moire du code, soit acc\u00e9l\u00e9rer son ex\u00e9cution.

    Ainsi l'expression suivante, ne sera pas calcul\u00e9e \u00e0 l'ex\u00e9cution, mais \u00e0 la compilation\u2009:

    int num = (4 + 7 * 10) >> 2;\n

    De m\u00eame que ce test n'effectuera pas une division, mais testera simplement le dernier bit de a:

    if (a % 2) {\n    puts(\"Pair\");\n} else {\n    puts(\"Impair\");\n}\n
    ", "tags": ["gcc"]}, {"location": "course-c/15-fundations/operators/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 5\u2009: Parenth\u00e8ses superflues

    Dans les expressions suivantes, lesquelles contiennent des parenth\u00e8ses superflues qui peuvent \u00eatre retir\u00e9es sans changer le sens de l'expression\u2009?

    • a = (b + c) * d
    • a = b + (c * d)
    • (a < b) && (c > d)
    • a = (b + c) / (d + e)
    • (a && b) || (c && d)
    • (a || b) && (c || d)
    • a = (b + c) % (d + e)

    Exercice 6\u2009: Quelle priorit\u00e9

    Quel est l'op\u00e9rateur qui a la priorit\u00e9 la plus basse\u2009?

    • +
    • *
    • ||
    • &&

    Exercice 7\u2009: Masque binaire

    Soit les d\u00e9clarations suivantes\u2009:

    char m, n = 2, d = 0x55, e = 0xAA;\n

    Repr\u00e9senter en binaire et en hexad\u00e9cimal la valeur de tous les bits de la variable m apr\u00e8s ex\u00e9cution de chacune des instructions suivantes\u2009:

    1. m = 1 << n;
    2. m = ~1 << n;
    3. m = ~(1 << n);
    4. m = d | (1 << n);
    5. m = e | (1 << n);
    6. m = d ^ (1 << n);
    7. m = e ^ (1 << n);
    8. m = d & ~(1 << n);
    9. m = e & ~(1 << n);

    Exercice 8\u2009: Registre syst\u00e8me

    Pour programmer les registres 16-bits d'un composant \u00e9lectronique charg\u00e9 de g\u00e9rer des sorties tout ou rien, on doit \u00eatre capable d'effectuer les op\u00e9rations suivantes\u2009:

    • mettre \u00e0 1 le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;
    • mettre \u00e0 0 le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;
    • inverser le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;

    Pour des questions d'efficacit\u00e9, ces op\u00e9rations ne doivent utiliser que les op\u00e9rateurs bit \u00e0 bit ou d\u00e9calage. On appelle r0 la variable d\u00e9signant le registre en m\u00e9moire et n la variable contenant le num\u00e9ro du bit \u00e0 modifier. \u00c9crivez les expressions permettant d'effectuer les op\u00e9rations demand\u00e9es.

    Exercice 9\u2009: Recherche d'expressions

    Consid\u00e9rant les d\u00e9clarations suivantes\u2009:

    float a, b;\nint m, n;\n

    Traduire en C les expressions math\u00e9matiques ci-dessous\u2009; pour chacune, proposer plusieurs \u00e9critures diff\u00e9rentes lorsque c'est possible. Le symbole \\(\\leftarrow\\) signifie assignation

    1. \\(n \\leftarrow 8 \\cdot n\\)
    2. \\(a \\leftarrow a + 2\\)
    3. \\(n \\leftarrow \\left\\{\\begin{array}{lr}m & : m > 0\\\\ 0 & : \\text{sinon}\\end{array}\\right.\\)
    4. \\(a \\leftarrow n\\)
    5. \\(n \\leftarrow \\left\\{\\begin{array}{lr}0 & : m~\\text{pair}\\\\ 1 & : m~\\text{impair}\\end{array}\\right.\\)
    6. \\(n \\leftarrow \\left\\{\\begin{array}{lr}1 & : m~\\text{pair}\\\\ 0 & : m~\\text{impair}\\end{array}\\right.\\)
    7. \\(m \\leftarrow 2\\cdot m + 2\\cdot n\\)
    8. \\(n \\leftarrow n + 1\\)
    9. \\(a \\leftarrow \\left\\{\\begin{array}{lr}-a & : b < 0\\\\ a & : \\text{sinon}\\end{array}\\right.\\)
    10. \\(n \\leftarrow \\text{la valeur des 4 bits de poids faible de}~n\\)

    Exercice 10\u2009: Nombres narcissiques

    Un nombre narcissique ou nombre d'Amstrong est un entier naturel n non nul qui est \u00e9gal \u00e0 la somme des puissances p-i\u00e8mes de ses chiffres en base dix, o\u00f9 p d\u00e9signe le nombre de chiffres de n:

    \\[ n=\\sum_{k=0}^{p-1}x_k10^k=\\sum_{k=0}^{p-1}(x_k)^p\\quad\\text{avec}\\quad x_k\\in\\{0,\\ldots,9\\}\\quad\\text{et}\\quad x_{p-1}\\ne 0 \\]

    Par exemple\u2009:

    • 9 est un nombre narcissique, car \\(9 = 9^1 = 9\\)
    • 153 est un nombre narcissique, car \\(153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153\\)
    • 10 n'est pas un nombre narcissique, car \\(10 \\ne 1^2 + 0^2 = 1\\)

    Implanter un programme permettant de v\u00e9rifier si un nombre d'entr\u00e9es est narcissique ou non. L'ex\u00e9cution est la suivante\u2009:

    $ ./armstrong 153\n1\n\n$ ./armstrong 154\n0\n

    Exercice 11\u2009: Swap sans valeur interm\u00e9diaire

    Soit deux variables enti\u00e8res a et b, chacune contenant une valeur diff\u00e9rente. \u00c9crivez les instructions permettant d'\u00e9changer les valeurs de a et de b sans utiliser de valeurs interm\u00e9diaires. Indice\u2009: utilisez l'op\u00e9rateur XOR ^.

    Testez votre solution...

    Solution
    a ^= b;\nb ^= a;\na ^= b;\n
    "}, {"location": "course-c/15-fundations/preprocessor/", "title": "Pr\u00e9processeur", "text": "

    Illustration du m\u00e9canisme de pr\u00e9-processing avant la compilation

    Comme nous l'avons vu en introduction, le langage C est bas\u00e9 sur une double grammaire, c'est-\u00e0-dire qu'avant la compilation du code, un autre processus est appel\u00e9 visant \u00e0 pr\u00e9parer le code source avant la compilation. Le c\u0153ur de cette op\u00e9ration est appel\u00e9 pr\u00e9processeur. Les instructions du pr\u00e9processeur C sont faciles \u00e0 reconna\u00eetre, car elles d\u00e9butent toutes par le croisillon # 0023, hash (ou she) en anglais et utilis\u00e9es r\u00e9cemment comme hashtag sur les r\u00e9seaux sociaux. Notons au passage que ce caract\u00e8re \u00e9tait historiquement utilis\u00e9 par les Anglais sous le d\u00e9nominatif pound (livre). Lorsqu'il est apparu en Europe, il a \u00e9t\u00e9 confondu avec le caract\u00e8re di\u00e8se (sharp) \u266f 266F pr\u00e9sent sur les pav\u00e9s num\u00e9riques de t\u00e9l\u00e9phone.

    Le vocabulaire du pr\u00e9processeur se compose de directives d\u00e9marrant par un croisillon. Notons que ces directives (\u00e0 l'exception des op\u00e9rateurs de concat\u00e9nation de conversion en cha\u00eene de caract\u00e8re) sont des instructions de ligne (line-wise), c'est-\u00e0-dire qu'elles doivent se terminer par un caract\u00e8re de fin de ligne. Le point-virgule n'a pas d'effet sur le pr\u00e9processeur. En outre, il est possible d'ins\u00e9rer des espaces et des tabulations entre le croisillon et la directive. Il est commun\u00e9ment admis d'utiliser cette fonctionnalit\u00e9 pour g\u00e9rer l'indentation des directives pr\u00e9processeur, car certaines conventions imposent que le croisillon soit en premi\u00e8re colonne. La table suivante r\u00e9sume les directives du pr\u00e9processeur.

    Vocabulaire du pr\u00e9processeur Terme Description #include Inclus un fichier dans le fichier courant #define Cr\u00e9e une d\u00e9finition (Macro) #undef D\u00e9truit une d\u00e9finition existante #if defined Teste si une d\u00e9finition existe #if .. #endif Test conditionnel # Op\u00e9rateur de conversion en cha\u00eene de caract\u00e8res ## Op\u00e9rateur de concat\u00e9nation de cha\u00eenes #line Directive de ligne #error \"error message\" G\u00e9n\u00e8re une erreur #pragma Directive sp\u00e9cifique au compilateur

    Le pr\u00e9processeur C est ind\u00e9pendant du langage C, c'est-\u00e0-dire qu'il peut \u00eatre ex\u00e9cut\u00e9 sur n'importe quel type de fichier. Pour le prouver, prenons l'exemple d'une lettre g\u00e9n\u00e9rique d'un cabinet dentaire\u2009:

    #ifdef FEMALE\n#    define NAME Madame\n#else\n#    define NAME Monsieur\n#endif\nBonjour NAME,\n\nVeuillez noter votre prochain rendez-vous le DATE, \u00e0 HOUR heure.\n\nVeuillez agr\u00e9er, NAME, nos meilleures salutations,\n\n#ifdef IS_BOSS\nLe directeur\n#elif defined IS_ASSISTANT\nLa secr\u00e9taire du directeur\n#elif defined OWNER_NAME\nOWNER_NAME\n#else\n#    error \"Lettre sans signature\"\n#endif\n

    Il est possible d'appeler le pr\u00e9processeur directement avec l'option -E de gcc. Des directives define additionnelles peuvent \u00eatre renseign\u00e9es depuis la ligne de commande avec le drapeau -D. Voici un exemple d'utilisation\u2009:

    $ gcc -xc -E test.txt \\\n    -DDATE=22 -DHOUR=9:00 \\\n    -DFEMALE \\\n    -DOWNER_NAME=\"Adam\" -DPOSITION=employee\n# 1 \"test.txt\"\n# 1 \"<built-in>\"\n# 1 \"<command-line>\"\n# 1 \"/usr/include/stdc-predef.h\" 1 3 4\n# 1 \"<command-line>\" 2\n# 1 \"test.txt\"\nBonjour Madame,\n\nVeuillez noter votre prochain rendez-vous le 22, \u00e0 9:00 heure.\n\nVeuillez agr\u00e9er, Madame, nos meilleures salutations,\n\nAdam\n

    En sortie il reste des directives # nomm\u00e9es linemarkers ou marqueurs de lignes qui sont des commentaires utilis\u00e9s pour le d\u00e9verminage. Ces directives de lignes contiennent en premi\u00e8re position le num\u00e9ro de la ligne du fichier originel suivi d'informations sont utiles pour le d\u00e9bogage, car elles permettent de retrouver la source des erreurs. Le format est sp\u00e9cifique \u00e0 GCC. Un marqueur de ligne \u00e0 le format suivant\u2009:

    # linenum filename flags\n

    Les drapeaux peuvent \u00eatre\u2009: 1 pour indiquer le d\u00e9but d'un nouveau fichier inclus, 2 pour indiquer la fin d'un fichier inclus, 3 pour indiquer que le texte suivant provient d'un fichier syst\u00e8me et 4 pour indiquer que le texte doit \u00eatre trait\u00e9 comme un bloc implicite.

    Suppression des marqueurs de lignes

    Il est possible de supprimer les directives de lignes g\u00e9n\u00e9r\u00e9es par le pr\u00e9processeur avec l'option -P de gcc. Le -xc indique que le fichier est un fichier C ce qui \u00e9vite un message d'erreur su l'extension du fichier d'entr\u00e9e n'est pas .c.

    $ gcc -xc -E test.txt -P\n
    ", "tags": ["define", "gcc"]}, {"location": "course-c/15-fundations/preprocessor/#phases-de-traduction", "title": "Phases de traduction", "text": "

    Le standard C99 \u00a75.1.1.2 alin\u00e9a 1 d\u00e9finit 8 phases de traductions dont les 4 premi\u00e8res concernent le pr\u00e9processeur\u2009:

    1. Remplacement des multicaract\u00e8res\u2009: les caract\u00e8res Unicode et autres caract\u00e8res sp\u00e9ciaux sont remplac\u00e9s par des caract\u00e8res ASCII. Les trigraphes sont \u00e9galement interpr\u00e9t\u00e9s.

    2. Remplacement des backslash de fin de ligne. Lorsqu'un backslash est suivi d'un saut de ligne, le saut de ligne est supprim\u00e9. Cela permet d'\u00e9crire des lignes longues sur plusieurs lignes.

    3. Suppression des commentaires. Chaque commentaire est remplac\u00e9 par une espace. Les commentaires de type // et /* */ sont par cons\u00e9quent supprim\u00e9s.

    4. Les directives de pr\u00e9traitement sont ex\u00e9cut\u00e9es, les invocations de macros sont \u00e9tendues et les expressions avec l'op\u00e9rateur unaire _Pragma sont ex\u00e9cut\u00e9es ainsi que les directives #include appliqu\u00e9es r\u00e9cursivement.

    Voici un exemple de code C avec des directives de pr\u00e9traitement\u2009:

    int no\u00ebl[] = ??< 23, 42 ??> ; // Array of integers\nchar *\ud83d\udca9 = /* WTF */ \"Pile of Poo\";\n

    Apr\u00e8s pr\u00e9traitement avec gcc -std=c99 -E -P on obtient le code suivant\u2009:

    int no\\U000000ebl[] = { 23, 42 } ;\nchar *\\U0001f4a9 = \"Pile of Poo\";\n

    Notons que dans le standard C23, les trigraphes ont \u00e9t\u00e9 supprim\u00e9s c'est pourquoi l'exemple ci-dessus ne fonctionne que si le flag -std=c99 est utilis\u00e9.

    ", "tags": ["_Pragma"]}, {"location": "course-c/15-fundations/preprocessor/#extensions-des-fichiers", "title": "Extensions des fichiers", "text": "

    Pour s'y retrouver, une convention existe sur les extensions des fichiers. Rappelons que selon POSIX un fichier n'a pas n\u00e9cessairement d'extension, c'est une convention libre \u00e0 l'utilisateur n\u00e9anmoins GCC utilise les extensions pour d\u00e9terminer le type de fichier. Selon le standard GNU, les extensions suivantes sont en vigueur\u2009:

    .h

    Fichier d'en-t\u00eate ne comportant que des d\u00e9finitions du pr\u00e9processeur, des d\u00e9clarations (structures, unions ...) et des prototypes de fonction, mais aucun code ex\u00e9cutable. Ce fichier sera soumis au pr\u00e9processeur s'il est inclus dans un fichier source.

    .c

    Fichier source C comportant les impl\u00e9mentations de fonctions et les variables globales. Ce fichier sera soumis au pr\u00e9processeur.

    .i

    Fichier source C qui a d\u00e9j\u00e0 \u00e9t\u00e9 pr\u00e9trait\u00e9 qui ne sera pas soumis au pr\u00e9processeur\u2009: gcc -E -o foo.i foo.c

    .s

    Fichier assembleur non soumis au pr\u00e9processeur.

    .S

    Fichier assembleur soumis au pr\u00e9processeur. Notons toutefois que cette convention n'est pas applicable sous Windows, car le syst\u00e8me de fichier n'est pas sensible \u00e0 la casse et un programme ne peut pas savoir si le fichier est .s ou .S (merci Windows pour toutes ces frustrations que tu me cr\u00e9es).

    .o

    Fichier objet g\u00e9n\u00e9r\u00e9 par le compilateur apr\u00e8s compilation

    .a

    Biblioth\u00e8que statique. Similaire \u00e0 un fichier .o mais peut contenir plusieurs fichiers objets.

    .so

    Biblioth\u00e8que dynamique. Fichier objet partag\u00e9.

    "}, {"location": "course-c/15-fundations/preprocessor/#inclusion-include", "title": "Inclusion (#include)", "text": "

    La directive #include permet d'incorporer le contenu d\u2019un fichier dans un autre de mani\u00e8re r\u00e9cursive, facilitant ainsi la modularisation et la lisibilit\u00e9 du code. Cette approche permet de structurer un programme en plusieurs fichiers, favorisant une meilleure organisation et maintenabilit\u00e9.

    Deux formes d'inclusion existent\u2009: locale et globale. C'est d\u2019ailleurs une question r\u00e9currente sur StackOverflow.

    Inclusion globale\u2009: #include <filename>

    Dans ce cas, le pr\u00e9processeur recherche le fichier \u00e0 inclure dans les chemins syst\u00e8me pr\u00e9d\u00e9finis (/usr/include, etc.), ainsi que dans ceux sp\u00e9cifi\u00e9s par les options -I du compilateur ou la variable d'environnement C_INCLUDE_PATH.

    Inclusion locale\u2009: #include \"filename\"

    Ici, la recherche d\u00e9bute dans le r\u00e9pertoire courant, puis se poursuit dans les chemins d\u00e9finis par les options -I et la variable C_INCLUDE_PATH.

    L\u2019inclusion de fichiers est un processus simple de concat\u00e9nation\u2009: le contenu du fichier inclus est copi\u00e9 \u00e0 l\u2019emplacement de la directive #include. Toutefois, cela peut mener \u00e0 des d\u00e9pendances cycliques, provoquant des inclusions infinies. Par exemple, si un fichier foo.h s'inclut lui-m\u00eame, le pr\u00e9processeur entrera dans une boucle sans fin, aboutissant \u00e0 une erreur apr\u00e8s un certain seuil\u2009:

    $ echo '#include \"foo.h\"' > foo.h\n$ gcc -E foo.h\nhead.h:1:18: error: #include nested depth 200 exceeds maximum of 200 (use\n-fmax-include-depth=DEPTH to increase the maximum)\n    1 | #include \"foo.h\"\n
    ", "tags": ["foo.h", "C_INCLUDE_PATH"]}, {"location": "course-c/15-fundations/preprocessor/#prevenir-les-inclusions-multiples", "title": "Pr\u00e9venir les inclusions multiples", "text": "

    Pour \u00e9viter ce genre de probl\u00e8me, il est courant d\u2019utiliser des include guards (ou header guards). Ce m\u00e9canisme consiste \u00e0 encapsuler le contenu d\u2019un fichier d\u2019en-t\u00eate avec une macro unique, garantissant ainsi que le fichier ne sera inclus qu'une seule fois au cours de la compilation. Ce proc\u00e9d\u00e9 est d\u00e9taill\u00e9 plus loin.

    "}, {"location": "course-c/15-fundations/preprocessor/#chevrons-ou-guillemets", "title": "Chevrons ou guillemets\u2009?", "text": "

    Une question souvent pos\u00e9e est de savoir si l'inclusion doit se faire avec des guillemets ou des chevrons. La r\u00e9ponse d\u00e9pend du contexte du projet. Pour les projets de petite envergure, les biblioth\u00e8ques standard sont incluses avec des chevrons, tandis que les fichiers locaux, souvent situ\u00e9s dans le m\u00eame r\u00e9pertoire que les fichiers sources, sont inclus via des chemins relatifs avec des guillemets. Dans le cas de projets plus cons\u00e9quents, o\u00f9 les fichiers sources et les en-t\u00eates sont souvent s\u00e9par\u00e9s, il peut \u00eatre inappropri\u00e9 d\u2019utiliser des chemins relatifs tels que #include \"../include/foo.h\". Une meilleure pratique consiste \u00e0 utiliser l\u2019option GCC -Iinclude/, qui permet d\u2019indiquer au compilateur o\u00f9 trouver les fichiers d\u2019en-t\u00eate. Bien qu'il soit techniquement possible d\u2019utiliser des chevrons dans ce cas, cela peut pr\u00eater \u00e0 confusion pour les lecteurs du code, qui pourraient penser qu\u2019il s\u2019agit d\u2019une biblioth\u00e8que standard.

    "}, {"location": "course-c/15-fundations/preprocessor/#ordre-des-inclusions", "title": "Ordre des inclusions", "text": "

    Il est recommand\u00e9 de respecter un ordre pr\u00e9cis lors de l'inclusion des fichiers d'en-t\u00eate, pour \u00e9viter des d\u00e9pendances implicites et des conflits potentiels\u2009:

    1. Les fichiers d'en-t\u00eate propres au projet.
    2. Les en-t\u00eates des biblioth\u00e8ques externes.
    3. Les en-t\u00eates de la biblioth\u00e8que standard.

    Ainsi, on pourrait avoir le code suivant\u2009:

    #include \"foo.h\"\n\n#include <SDL2/SDL.h>\n\n#include <stdio.h>\n#include <stdlib.h>\n

    Cet ordre garantit que les d\u00e9pendances des biblioth\u00e8ques externes ou standard ne soient pas r\u00e9solues par inadvertance gr\u00e2ce \u00e0 une inclusion locale. Pour mieux illustrer, consid\u00e9rons un fichier stack.h qui d\u00e9finit une structure de pile\u2009:

    #pragma once\n\nbool stack_push(int_least32_t value);\nbool stack_pop(int_least32_t *value);\n

    Ici, les types bool et int_least32_t ne sont pas d\u00e9finis dans ce fichier, car les inclusions de <stdbool.h> et <stdint.h> sont absentes. Si le fichier main.c les inclut avant stack.h :

    #include <stdbool.h>\n#include <stdint.h>\n#include <stdio.h>\n\n#include \"stack.h\"\n

    Aucune erreur ne sera g\u00e9n\u00e9r\u00e9e. Cependant, stack.h d\u00e9pend d'inclusions externes, ce qui est g\u00e9n\u00e9ralement \u00e0 \u00e9viter. En incluant les fichiers locaux en premier, on s'assure de leur autonomie.

    ", "tags": ["bool", "int_least32_t", "main.c", "stack.h"]}, {"location": "course-c/15-fundations/preprocessor/#macros-define", "title": "Macros (#define)", "text": "

    Les macros sont des symboles g\u00e9n\u00e9ralement \u00e9crits en majuscule et qui sont remplac\u00e9s par le pr\u00e9processeur. Ces d\u00e9finitions peuvent \u00eatre utiles pour d\u00e9finir des constantes globales qui sont d\u00e9finies \u00e0 la compilation et qui peuvent \u00eatre utilis\u00e9es comme des options de compilation. Par exemple, pour d\u00e9finir la taille d'une fen\u00eatre de filtrage\u2009:

    #ifndef WINDOW_SIZE\n#    define WINDOW_SIZE 10\n#endif\n\nint tab[WINDOW_SIZE];\n\nvoid init(void) {\n    for(size_t i = 0; i < WINDOW_SIZE; i++)\n        tab[i] = i;\n}\n

    L'ajout de la directive conditionnelle #ifndef permet d'autoriser la d\u00e9finition de la taille du tableau \u00e0 la compilation directement depuis GCC\u2009:

    $ gcc main.c -DWINDOW_SIZE=42\n

    Notons que durant le pr\u00e9processing toute occurrence d'un symbole d\u00e9fini est remplac\u00e9e par le contenu de sa d\u00e9finition. C'est un remplacement de cha\u00eene b\u00eate, idiot et na\u00eff. Il est par cons\u00e9quent possible d'\u00e9crire\u2009:

    #define MAIN int main(\n#define BEGIN ) {\n#define END return 0; }\n#define EOF \"\\n\"\n\nMAIN\nBEGIN\n    printf(\"Hello\" EOF);\nEND\n

    Par ailleurs, on rel\u00e8vera qu'il est aussi possible de commettre certaines erreurs en utilisant les d\u00e9finitions. Prenons l'exemple d'une macro d\u00e9clar\u00e9e sans parenth\u00e8ses\u2009:

    #define ADD a + b\n\nint a = 12;\nint b = 23;\nint c = ADD * ADD\n

    Apr\u00e8s pr\u00e9traitement le comportement ne sera pas celui attendu, car la multiplication devrait \u00eatre plus prioritaire que l'addition\u2009:

    int a = 12;\nint b = 23;\nint c = a + b * a + b\n

    Pour se pr\u00e9munir contre ces \u00e9ventuelles coquilles, on prot\u00e8gera toujours les d\u00e9finitions avec des parenth\u00e8ses

    #define ADD (a + b)\n#define PI (3.14159265358979323846)\n

    Espace n\u00e9cessaire

    Il est important de noter que l'espace entre le nom de la macro et les parenth\u00e8ses est strictement n\u00e9cessaire. En effet, si on \u00e9crit #define ADD(a + b), le pr\u00e9processeur g\u00e9n\u00e9rera une erreur car il consid\u00e8re que ADD est une macro.

    ", "tags": ["ADD"]}, {"location": "course-c/15-fundations/preprocessor/#linearisation", "title": "Lin\u00e9arisation", "text": "

    Le processus d'expansion des macros est une \u00e9tape cl\u00e9 dans la fa\u00e7on dont le pr\u00e9processeur C interpr\u00e8te et remplace les macros d\u00e9finies avec #define. Ce processus est appel\u00e9 lin\u00e9arisation, car les macros sont substitu\u00e9es avant la compilation en suivant une logique lin\u00e9aire et r\u00e9cursive. Prenons l'exemple de macros imbriqu\u00e9es\u2009:

    #define DOUBLE(x) (2 * (x))\n#define SQUARE(x) ((x) * (x))\n\nint main() {\n    int result = SQUARE(DOUBLE(3));\n}\n
    "}, {"location": "course-c/15-fundations/preprocessor/#macros-avec-arguments", "title": "Macros avec arguments", "text": "

    Une macro peut prendre des param\u00e8tres et permettre de g\u00e9n\u00e9rer du code \u00e0 la compilation par simple substitution de texte. Les macros sont souvent employ\u00e9es pour d\u00e9finir des fonctions simples qui ne n\u00e9cessitent pas de typage explicite. Par exemple, pour impl\u00e9menter une macro MIN qui retourne la valeur minimale entre deux arguments\u2009:

    #define MIN(x, y) ((x) < (y) ? (x) : (y))\n

    Le m\u00e9canisme de la macro repose uniquement sur un remplacement textuel\u2009:

    $ cat test.c\n#define MIN(x, y) ((x) < (y) ? (x) : (y))\nint main(void) { return MIN(23, 12); }\n\n$ gcc -E -P test.c -o-\nint main(void) { return ((23) < (12) ? (23) : (12)); }\n

    Notez que l'absence d'espace entre le nom de la macro et la parenth\u00e8se qui suit est cruciale. Une macro incorrectement d\u00e9finie, comme dans l'exemple ci-dessous, entra\u00eene un comportement inattendu\u2009:

    $ cat test.c\n#define ADD (x, y) ((x) + (y))\nint main(void) { return ADD(23, 12); }\n\n$ gcc -E -P test.c -o-\nint main(void) { return (x, y) ((x) + (y))(23, 12); }\n

    Une macro peut contenir plusieurs instructions, comme dans l'exemple suivant\u2009:

    #define ERROR(str) printf(\"Erreur: %s\\r\\n\", str); log(str);\n\nif (y < 0)\n    ERROR(\"Zero division\");\nelse\n    x = x / y;\n

    Dans cet exemple, l'absence d'accolades dans l'instruction if fait que seule la premi\u00e8re instruction est ex\u00e9cut\u00e9e lorsque la condition y < 0 est vraie. Or, la macro ERROR contient deux instructions distinctes, ce qui conduit \u00e0 un comportement incorrect.

    Une solution triviale consisterait \u00e0 encapsuler les instructions de la macro dans des accolades\u2009:

    #define ERROR(str) { printf(\"Erreur: %s\\r\\n\", str); log(str); }\n

    Cependant, cette approche pr\u00e9sente des probl\u00e8mes lorsqu\u2019elle est utilis\u00e9e dans des structures conditionnelles, comme un if-else. En effet, l'appel \u00e0 la macro est suivi d'un point-virgule, et un bloc d\u00e9limit\u00e9 par des accolades constitue une instruction compos\u00e9e (compound statement). L'ajout du point-virgule apr\u00e8s la macro provoque alors une terminaison anticip\u00e9e de l'instruction if, laissant le else sans correspondance\u2009:

    if (y < 0)\n    ERROR(\"Zero division\");\nelse\n    x = x / y;\n

    Pour contourner ce probl\u00e8me, il est pr\u00e9f\u00e9rable d'envelopper la macro dans une structure do { ... } while (0) afin de la transformer en une seule instruction indivisible, quel que soit le contexte d'utilisation\u2009:

    #define ERROR(str) do { \\\n    printf(\"Erreur: %s\\r\\n\", str); \\\n    log(str); \\\n} while (0)\n

    Cette construction cr\u00e9e une boucle vide qui s'ex\u00e9cute une seule fois, garantissant que la macro se comporte comme une instruction unique, m\u00eame si elle contient plusieurs lignes. De plus, l'utilisation de \\ permet d'\u00e9chapper le retour \u00e0 la ligne pour conserver une lisibilit\u00e9 optimale tout en faisant comprendre au pr\u00e9processeur que tout le bloc est une seule ligne logique.

    M\u00eame avec une bonne encapsulation des macros, certains pi\u00e8ges subsistent, notamment l'utilisation des post/pr\u00e9-incr\u00e9ments dans les arguments. Par exemple, consid\u00e9rons la macro ABS qui calcule la valeur absolue d\u2019un nombre\u2009:

    #define ABS(x) ((x) >= 0 ? (x) : -(x))\n\nreturn ABS(x++);\n

    Dans cet exemple, la variable x est post-incr\u00e9ment\u00e9e plusieurs fois, car l'argument de la macro est r\u00e9\u00e9valu\u00e9 \u00e0 chaque usage\u2009:

    return ((x++) >= 0 ? (x++) : -(x++));\n

    Ici, x est incr\u00e9ment\u00e9 trois fois au lieu d'une seule, ce qui peut entra\u00eener des comportements ind\u00e9sirables.

    Rappel des r\u00e8gles de bonnes pratiques\u2009:

    1. Prot\u00e9ger les param\u00e8tres de macro avec des parenth\u00e8ses pour garantir une \u00e9valuation correcte de l'expression.
    2. Encapsuler les macros \u00e0 plusieurs instructions dans une boucle vide pour \u00e9viter des erreurs dans les structures de contr\u00f4le comme if-else.
    3. \u00c9viter les post/pr\u00e9-incr\u00e9ments dans les macros, car ils peuvent provoquer des r\u00e9\u00e9valuations impr\u00e9vues et des erreurs difficiles \u00e0 d\u00e9tecter.

    Exercice 1\u2009: Macro compromise\u2009?

    Que retourne la fonction foo lors de son ex\u00e9cution avec le code suivant\u2009?

    #define ABS(x) x >= 0 ? x: -x\nint foo(void) { return ABS(5 - 8); }\n
    • 3
    • -3
    • -13
    • 5 - 8
    • 0

    ", "tags": ["ABS", "MIN", "foo", "ERROR", "else"]}, {"location": "course-c/15-fundations/preprocessor/#_static_assert", "title": "_Static_assert", "text": "

    Le mot-cl\u00e9 _Static_assert est une directive de pr\u00e9processeur introduite dans le standard C11. Elle permet de v\u00e9rifier des conditions \u00e0 la compilation. Contrairement \u00e0 #if, _Static_assert g\u00e9n\u00e8re une erreur de compilation si la condition n'est pas v\u00e9rifi\u00e9e. Par exemple, pour v\u00e9rifier que la taille d'un tableau est sup\u00e9rieure \u00e0 10\u2009:

    _Static_assert(sizeof(tab) > 10, \"La taille du tableau est inf\u00e9rieure \u00e0 10\");\n

    ", "tags": ["_Static_assert"]}, {"location": "course-c/15-fundations/preprocessor/#varadiques", "title": "Varadiques", "text": "

    Les macros varadiques permettent de d\u00e9finir des macros qui prennent un nombre variable d'arguments. Par exemple, pour d\u00e9finir une macro qui affiche un message avec un nombre variable d'arguments. Il s'agit d'une extension du standard C99. Pour cela on utilise l'op\u00e9rateur ... suivi de __VA_ARGS__ qui repr\u00e9sente les arguments pass\u00e9s \u00e0 la macro.

    C'est utilis\u00e9 par exemple pour cr\u00e9er des fonctions de d\u00e9bogage\u2009:

    #define DEBUG_LOG(level, fmt, ...) \\\n    printf(\"[%s] \" fmt \"\\n\", level, __VA_ARGS__)\n\nint main() {\n    DEBUG_LOG(\"\\033[32mINFO\\033[0m\", \"User %s logged in\", \"Alice\");\n    DEBUG_LOG(\"\\033[31mERROR\\033[0m\", \"Failed to open file %s\", \"data.txt\");\n}\n
    ", "tags": ["__VA_ARGS__"]}, {"location": "course-c/15-fundations/preprocessor/#terminologie", "title": "Terminologie", "text": "

    Que ce soit en anglais ou en fran\u00e7ais il n'est pas tr\u00e8s clair de comment nommer\u2009:

    1. #define FOO 42
    2. #define FOO(x) (x * x)

    Formellement les deux sont des d\u00e9finitions de pr\u00e9processeur mais \u00e9galement des macros. En informatique une macro est un modif de substitution de texte pouvant prendre des arguments.

    Dans un sens plus large la premi\u00e8re d\u00e9finition est souvent qualifi\u00e9e de constante symbolique bien que ce ne soit pas une vraie constante (const) elle rempli un r\u00f4le similaire.

    ", "tags": ["const"]}, {"location": "course-c/15-fundations/preprocessor/#directive-conditionnelle-if", "title": "Directive conditionnelle (#if)", "text": "

    La directive conditionnelle #if permet de tester des expressions \u00e0 la compilation. Elle est souvent utilis\u00e9e pour d\u00e9finir des options de compilation en fonction des besoins.

    Prenons l'exemple d'un programme qui g\u00e8re une grosse masse de messages. Ces derniers sont tri\u00e9s selon un crit\u00e8re sp\u00e9cifique et pour ce faire une fonction de tri est utilis\u00e9e. Nous verrons plus tard que le tri est un sujet complexe et qu'un crit\u00e8re du choix d'un algorithme de tri est la stabilit\u00e9. Un tri stable pr\u00e9serve l'ordre relatif des \u00e9l\u00e9ments qui ne sont pas distingu\u00e9s par le crit\u00e8re de tri. Par exemple, si on trie des personnes par leur \u00e2ge, un tri stable pr\u00e9servera l'ordre des personnes de m\u00eame \u00e2ge. Si la stabilit\u00e9 n'est pas un crit\u00e8re, un tri instable peut \u00eatre plus rapide. Pour cette raison, selon la n\u00e9cessit\u00e9 d'utilisation du programme, il peut \u00eatre pertinent de d\u00e9finir lors de la compilation s'il faut \u00eatre tr\u00e8s performant mais instable ou plus lent mais stable. Cela pourrait se faire avec\u2009:

    #ifdef STABLE_SORT\n#    define SORT merge_sort\n#else\n#    define SORT quick_sort\n#endif\n

    La configuration peut se faire soit directement dans le code source avec une directive pr\u00e9processeur\u2009:

    #define STABLE_SORT\n

    soit depuis la ligne de commande\u2009:

    $ gcc main.c -DSTABLE_SORT\n

    L'instruction #ifdef est un sucre syntaxique pour #if defined, autrement dit\u2009: si la d\u00e9finition est d\u00e9clar\u00e9e et quelque soit sa valeur. En effet, une d\u00e9claration sans valeur est tout \u00e0 fait possible. N\u00e9anmoins cela peut cr\u00e9er de la confusion. Penons l'exemple suivant pour lequel le message Dynamic allocation is allowed sera affich\u00e9 bien que la valeur de ALLOW_DYNAMIC_ALLOCATION soit 0 :

    #define ALLOW_DYNAMIC_ALLOCATION 0\n\n#if defined ALLOW_DYNAMIC_ALLOCATION\nprintf(\"Dynamic allocation is allowed\");\n#else\nprintf(\"Dynamic allocation is not allowed\");\n#endif\n

    Pour se pr\u00e9munir de ce genre de probl\u00e8me, il est recommand\u00e9 de toujours d\u00e9finir une valeur \u00e0 une d\u00e9claration et de tester si cette valeur est vraie\u2009:

    #define ALLOW_DYNAMIC_ALLOCATION 0\n\n#if ALLOW_DYNAMIC_ALLOCATION\n\u301c\n

    Une bonne pratique est soit de lever une erreur si la valeur n'est pas d\u00e9finie, soit de d\u00e9finir une valeur par d\u00e9faut\u2009:

    #define YES 1\n#define NO 0\n\n#define STRICT YES\n#define ALLOW_DYNAMIC_ALLOCATION YES\n\n#ifndef ALLOW_DYNAMIC_ALLOCATION\n#    if defined STRICT && STRICT == YES\n#        error \"ALLOW_DYNAMIC_ALLOCATION is not defined\"\n#    else\n#        define ALLOW_DYNAMIC_ALLOCATION NO\n#    endif\n#endif\n

    Par analogie \u00e0 l'instruction if, il est possible d'utiliser #else (else)et #elif (else if) pour d\u00e9finir des alternatives\u2009:

    #define MODE_UPPERCASE 0\n#define MODE_LOWERCASE 1\n#define MODE_ARABIC 2\n\n#ifndef DISPLAY_MODE\n#   define DISPLAY_MODE MODE_UPPERCASE\n#endif\n\nvoid display(char value) {\n#if DISPLAY_MODE == MODE_UPPERCASE\n    printf(\"%c\", 'A' + (value % 26));\n#elif DISPLAY_MODE == MODE_LOWERCASE\n    printf(\"%c\", 'a' + (value % 26));\n#elif DISPLAY_MODE == MODE_ARABIC\n    printf(\"%d\", value);\n#else\n#   error \"DISPLAY_MODE is not defined\"\n#endif\n}\n

    ", "tags": ["else"]}, {"location": "course-c/15-fundations/preprocessor/#suppression-undef", "title": "Suppression (#undef)", "text": "

    Un symbole d\u00e9fini soit par la ligne de commande -DFOO=1, soit par la directive #define FOO 1 ne peut pas \u00eatre red\u00e9fini\u2009:

    $ cat test.c\n#define ANSWER 42\n#define ANSWER 23\n$ gcc -E test.c\ntest.c:2: warning: \"ANSWER\" redefined\n    2 | #define ANSWER 23\n      |\ntest.c:1: note: this is the location of the previous definition\n    1 | #define ANSWER 42\n      |\n

    C'est pourquoi il peut \u00eatre utile d'utiliser #undef pour supprimer une directive pr\u00e9processeur\u2009:

    #ifdef FOO\n#   undef FOO\n#endif\n#define FOO 1\n

    L'utilisation de #undef dans un programme est tout \u00e0 fait l\u00e9gitime dans certains cas, mais elle doit \u00eatre manipul\u00e9e avec pr\u00e9caution.

    On peut citer par exemple la fameuse constante M_PI qui n'est pas d\u00e9finie par le standard C mais que la plupart des compilateurs d\u00e9finissent. Si vous avez besoin d'utiliser la valeur \\(\\pi\\) dans votre programme, vous pouvez soit utiliser par d\u00e9faut la valeur d\u00e9finie par la biblioth\u00e8que si elle existe sinon la v\u00f4tre\u2009:

    #ifndef M_PI\n#    define M_PI 3.14159265358979323846\n#endif\n

    Ou alors, vous pourriez forcer la valeur de \\(\\pi\\) \u00e0 celle que vous souhaitez\u2009:

    #undef M_PI\n#define M_PI 3.14159265358979323846\n

    ", "tags": ["M_PI"]}, {"location": "course-c/15-fundations/preprocessor/#erreur-error", "title": "Erreur (#error)", "text": "

    La directive #error g\u00e9n\u00e8re une erreur avec le texte qui suit la directive et arr\u00eate la compilation. Elle est souvent utilis\u00e9e pour s'assurer que certaines conditions sont remplies avant de compiler le code. Par exemple, pour s'assurer que la taille du noyau d'un filtre est impair\u2009:

    #if !(KERNEL_SIZE % 2)\n#    error Le noyau du filtre est pair\n#endif\n

    On peut l'utiliser \u00e9galement pour s'assurer que le compilateur est compatible avec une version sp\u00e9cifique du standard C\u2009:

    #if __STDC_VERSION__ < 201112L\n    #error \"C11 support required\"\n#endif\n

    Warning

    Certains compilateurs comme GCC ou Clang permettent d'utiliser #warning pour g\u00e9n\u00e9rer un avertissement, bien que cette directive ne soit pas standard.

    #warning \"This is a warning\"\n
    "}, {"location": "course-c/15-fundations/preprocessor/#macros-predefinies", "title": "Macros pr\u00e9d\u00e9finies", "text": "

    Le standard d\u00e9finit certains symboles utiles pour le d\u00e9bogage\u2009:

    __LINE__

    Est remplac\u00e9 par le num\u00e9ro de la ligne sur laquelle est plac\u00e9 ce symbole

    __FILE__

    Est remplac\u00e9 par le nom du fichier sur lequel est plac\u00e9 ce symbole

    __func__

    Est remplac\u00e9 par le nom de la fonction du bloc dans lequel la directive se trouve

    __STDC__

    Est remplac\u00e9 par 1 pour indiquer que l'impl\u00e9mentation est compatible avec C90

    __STDC_VERSION__

    Est remplac\u00e9 par la version du standard C utilis\u00e9e par le compilateur, il s'agit d'un entier de la forme AAAAMM o\u00f9 AAAA est l'ann\u00e9e et MM le mois. Par exemple pour C11, la valeur est 201112L

    __DATE__

    Est remplac\u00e9 par la date sous la forme \"Mmm dd yyyy\"

    __TIME__

    Est remplac\u00e9 par l'heure au moment du pre-processing \"hh:mm:ss\"

    __COUNTER__

    Est remplac\u00e9 par un entier qui s'incr\u00e9mente \u00e0 chaque fois qu'il est utilis\u00e9. C'est une directive non standard mais disponible dans GCC et Clang.

    On peut par exemple cr\u00e9er une macro pour afficher des messages de d'erreur avec le nom du fichier le num\u00e9ro de ligne et la date de compilation\u2009:

    #define ERROR(msg) fprintf(stderr, \"%s:%d %s %s\\n\", \\\n    __FILE__, __LINE__, __DATE__, msg)\n
    ", "tags": ["AAAA", "__DATE__", "__STDC__", "__func__", "__FILE__", "__COUNTER__", "__LINE__", "__TIME__", "AAAAMM"]}, {"location": "course-c/15-fundations/preprocessor/#caractere-dechappement", "title": "Caract\u00e8re d'\u00e9chappement", "text": "

    La plupart des instructions pr\u00e9processeur sont des instructions de ligne, c'est-\u00e0-dire qu'elles se terminent par un saut de ligne or parfois (notamment pour les macros), on souhaiterait \u00e9crire une instruction sur plusieurs lignes.

    L'anti-slash (backslash) suivi directement d'un retour \u00e0 la ligne est interpr\u00e9t\u00e9 par le pr\u00e9processeur comme un saut de ligne virtuel. Il permet par exemple de casser les longues lignes\u2009:

    #define TRACE printf(\"Le programme est pass\u00e9 \" \\\n    \" dans le fichier %s\" \\\n    \" ligne %d\\n\", \\\n    __FILE__, __LINE__);\n

    ", "tags": ["backslash"]}, {"location": "course-c/15-fundations/preprocessor/#directive-de-ligne", "title": "Directive de ligne", "text": "

    La directive #line permet de modifier le num\u00e9ro de ligne et le nom du fichier pour les directives de d\u00e9bogage. En pratique elle est peu utilis\u00e9e, car les compilateurs modernes g\u00e8rent correctement les num\u00e9ros de ligne et les noms de fichiers.

    #line 42 \"foo.c\"\n

    "}, {"location": "course-c/15-fundations/preprocessor/#concatenation-de-chaines", "title": "Concat\u00e9nation de cha\u00eenes", "text": "

    Parfois il est utile de vouloir concat\u00e9ner deux symboles comme si ce n'\u00e9tait qu'un seul. L'op\u00e9rateur de concat\u00e9nation ## permet de concat\u00e9ner deux arguments dans une macro et de les combiner en un seul token. Cet op\u00e9rateur est particuli\u00e8rement utilis\u00e9 dans des macros avanc\u00e9es pour g\u00e9n\u00e9rer du code automatiquement \u00e0 partir d'une combinaison d'arguments.

    Un cas d'usage typique est la cr\u00e9ation de noms dynamiques.

    #define CONCAT(a, b) a##b\n\nint main() {\n    int var1 = 10, var2 = 20;\n\n    CONCAT(var, 1) = 30;  // devient var1 = 30\n    CONCAT(var, 2) = 40;  // devient var2 = 40\n\n    printf(\"%d, %d\\n\", var1, var2);  // 30, 40\n}\n

    Cela peut \u00eatre utile par exemple pour g\u00e9rer des traductions\u2009:

    #define LANGUAGE FR\n\n#define CONCAT(a, b) a##b\n\nvoid greet_FR(void) { printf(\"Bonjour\\n\"); }\nvoid greet_EN(void) { printf(\"Hello\\n\"); }\n\n#define GREET CONCAT(greet_, LANGUAGE)\n\nint main() {\n    GREET();\n}\n

    Un autre usage courant est de d\u00e9finir le mangling des noms de fonctions pour une biblioth\u00e8que. Par exemple, si vous avez une biblioth\u00e8que qui d\u00e9finit une fonction foo et que vous voulez \u00e9viter les conflits de noms, vous pourriez d\u00e9finir une macro pour ajouter un pr\u00e9fixe et un suffixe\u2009:

    #define MANGLE(name) prefix_ ## name ## _suffix\n\nvoid MANGLE(foo)(void) {\n    printf(\"Hello\");\n}\n\nint main() {\n    MANGLE(foo)();\n}\n

    Rappelez-vous que le langage C ne permet pas de d\u00e9finir des fonctions avec le m\u00eame nom m\u00eame si elles ont des signatures diff\u00e9rentes et m\u00eame si elles sont dans deux fichiers s\u00e9par\u00e9s. Un param\u00e8tre de configuration MANGLE permettrait de sp\u00e9cifier \u00e0 la compilation d'une biblioth\u00e8que le pr\u00e9fixe \u00e0 ajouter \u00e0 toutes les fonctions de la biblioth\u00e8que.

    ", "tags": ["foo", "MANGLE"]}, {"location": "course-c/15-fundations/preprocessor/#conversion-en-chaine", "title": "Conversion en cha\u00eene", "text": "

    Il est possible de convertir un symbole en cha\u00eene de caract\u00e8res avec l'op\u00e9rateur # :

    #define STRINGIFY(x) #x\nprintf(STRINGIFY(42));\n

    Le plus souvent cet op\u00e9rateur est utilis\u00e9 pour traiter des messages d'erreurs\u2009:

    #define WARN_IF_NEGATIVE(x) \\\n    if (x < 0) { \\\n        printf(\"Warning: the value of \" #x \" is negative (%d)\\n\", x); \\\n    }\n\nint main() {\n    int value = -5;\n    WARN_IF_NEGATIVE(value);\n}\n

    Ou par exemple pour afficher la valeur d'une variable\u2009:

    #define PRINT_VAR(var) printf(#var \" = %d\\n\", var)\n\nint main() {\n    int a = 10;\n    PRINT_VAR(a);\n}\n
    "}, {"location": "course-c/15-fundations/preprocessor/#desactivation-de-code", "title": "D\u00e9sactivation de code", "text": "

    Je vois trop souvent des d\u00e9veloppeurs commenter des sections de code pour le d\u00e9bogage. Cette pratique n'est pas recommand\u00e9e, car les outils de refactoring (r\u00e9usinage de code), ne parviendront pas \u00e0 interpr\u00e9ter le code en commentaire jugeant qu'il ne s'agit pas de code, mais de texte insignifiant. Une m\u00e9thode plus robuste et plus sure consiste \u00e0 utiliser une directive conditionnelle\u2009:

    #if 0 // TODO: Check if this code is still required.\nif (x < 0) {\n    x = 0;\n}\n#endif\n

    "}, {"location": "course-c/15-fundations/preprocessor/#include-guard", "title": "Include guard", "text": "

    Comme \u00e9voqu\u00e9 plus haut, les fichiers d'en-t\u00eate peuvent \u00eatre inclus plusieurs fois dans un programme ce qui peut poser des probl\u00e8mes de red\u00e9finition de symboles ou d'erreurs de d\u00e9pendences cycliques. En informatique on parle volontiers d'idempotence pour d\u00e9signer une op\u00e9ration qui peut \u00eatre appliqu\u00e9e plusieurs fois sans changer le r\u00e9sultat au-del\u00e0 de la premi\u00e8re application. Inclure une ou plusieurs fois un m\u00eame fichier d'en-t\u00eate ne doit pas changer le r\u00e9sultat de la compilation.

    Pour ce faire, on utilise des garde-fous (guards) pour prot\u00e9ger les fichiers d'en-t\u00eate. Imaginons que la constante M_PI soit d\u00e9finie dans le header <math.h>:

    #define M_PI  3.14159265358979323846\n

    Si ce fichier d'en-t\u00eate est inclus \u00e0 nouveau, le pr\u00e9processeur g\u00e9n\u00e9rera une erreur, car le symbole est d\u00e9j\u00e0 d\u00e9fini. Pour \u00e9viter ce genre d'erreur, les fichiers d'en-t\u00eate sont prot\u00e9g\u00e9s par un garde\u2009:

    #ifndef MATH_H\n#define MATH_H\n\n...\n\n#endif // MATH_H\n

    Si le fichier a d\u00e9j\u00e0 \u00e9t\u00e9 inclus, la d\u00e9finition MATH_H sera d\u00e9j\u00e0 d\u00e9clar\u00e9e et le fichier d'en-t\u00eate ne sera pas r\u00e9-inclus.

    Le consensus veut que le nom du garde soit le nom du fichier en majuscule avec des underscores \u00e0 la place des points et des tirets. Par exemple, pour le fichier foo/bar.h on utilisera FOO_BAR_H. On ajoutera \u00e9galement un commentaire pour indiquer la fin du garde. J'ai personnellement un avis assez d\u00e9favorable sur cette pratique, car elle engeandre un probl\u00e8me de source de v\u00e9rit\u00e9. En effet, si le nom du fichier change, il faudra \u00e9galement changer le nom du garde et je peux vous garantir que bien souvent l'un est r\u00e9alis\u00e9 sans l'autre. Cela pose \u00e9galement un probl\u00e8me de suivi de modifications sous Git.

    Alternativement une solution est d'avoir une cha\u00eene de caract\u00e8re unique pour chaque fichier d'en-t\u00eate. Cela peut \u00eatre r\u00e9alis\u00e9 en s'\u00e9nervant sur le clavier. Il y a peu de chance de retrouver une telle cha\u00eene dans un autre fichier. N\u00e9anmoins, il est aussi possible de g\u00e9n\u00e9rer une telle cha\u00eene de mani\u00e8re automatique avec un outil comme uuidgen ou openssl rand -hex 16.

    #ifndef FJHJFDKLHSKIOUZEZEUWEHDLKSH\n#define FJHJFDKLHSKIOUZEZEUWEHDLKSH\n\n#endif // FJHJFDKLHSKIOUZEZEUWEHDLKSH\n

    On pr\u00e9f\u00e8rera utiliser la directive #pragma once qui est plus simple \u00e0 l'usage et \u00e9vite une collision de nom. N\u00e9anmoins et bien que cette directive ne soit pas standardis\u00e9e par l'ISO, elle est compatible avec la tr\u00e8s grande majorit\u00e9 des compilateurs C.

    #pragma once\n\n...\n

    ", "tags": ["FOO_BAR_H", "MATH_H", "uuidgen", "M_PI"]}, {"location": "course-c/15-fundations/preprocessor/#pragmas-pragma", "title": "Pragmas (#pragma)", "text": "

    Le terme pragma est une abr\u00e9viation de pragmatic information (information pragmatique). Les pragmas sont des instructions sp\u00e9cifiques \u00e0 un compilateur qui permettent de contr\u00f4ler le comportement du compilateur. Ils sont souvent utilis\u00e9s pour d\u00e9sactiver des avertissements, sp\u00e9cifier l'alignement m\u00e9moire ou encore pour optimiser le code. La directive #pragma permet donc de passer des options sp\u00e9cifiques au compilateur, elle n'est par cons\u00e9quent pas standardis\u00e9e.

    Une utilisation possible avec GCC serait forcer la d\u00e9sactivation d'un avertissement\u2009:

    #pragma GCC diagnostic ignored \"-Wformat\"\n

    On utilise \u00e9galement un #pragma pour forcer l'alignement m\u00e9moire d'une structure\u2009:

    #pragma pack(push, 1)\ntypedef struct {\n    char a;\n    int b;\n} MyStruct;\n#pragma pack(pop)\n

    Comme les pragmas ne sont pas standardis\u00e9es, il est recommand\u00e9 de les utiliser avec parcimonie et de les documenter correctement.

    Notons que si l'on souhaite d\u00e9finir un pragma au sein d'une directive pr\u00e9processeur tel qu'une macro, il faudra utiliser l'op\u00e9rateur unaire _Pragma :

    #define PRAGMA(x) _Pragma(#x)\n

    ", "tags": ["_Pragma"]}, {"location": "course-c/15-fundations/preprocessor/#simulation-dexceptions", "title": "Simulation d'exceptions", "text": "

    Dans des langages de plus haut niveau comme le C++, le Python ou le Java, il existe un m\u00e9canisme nomm\u00e9 exception qui permet de g\u00e9rer des erreurs plus efficacement. Au lieu de retourner une valeur d'erreur, on l\u00e8ve une exception qui sera attrap\u00e9e plus haut dans la cha\u00eene d'appel.

    En C il n'existe pas de m\u00e9canisme d'exception, mais il est possible de simuler ce comportement avec des macros et l'instruction setjmp et longjmp de la librairie standard. setjmp permet de sauvegarder l'\u00e9tat du programme \u00e0 un endroit donn\u00e9 et longjmp permet de revenir \u00e0 cet endroit en sautant les appels de fonctions interm\u00e9diaires. Il faut voir longjmp comme un goto encore plus dangereux.

    Cette utilisation n'est pas recommend\u00e9e car elle peut rendre le code plus obscure et plus difficile \u00e0 maintenir. N\u00e9anmoins dans certains cas de figure, notament pour des programmes embarqu\u00e9s, cette technique peut se r\u00e9v\u00e9ler tr\u00e8s utile.

    On d\u00e9finit tout d'abord les macros suivantes dans un fichier exception.h :

    #pragma once\n#include <setjmp.h>\n\n#define TRY do { jmp_buf ex_buf__; if (setjmp(ex_buf__) == 0) {\n#define CATCH } else {\n#define ETRY } } while (0)\n#define THROW longjmp(ex_buf__, 1)\n

    On peut ensuite utiliser ces macros pour g\u00e9rer des erreurs\u2009:

    #include \"exception.h\"\n\nvoid qux(void) {\n    printf(\"qux\\n\");\n    THROW; // Simulate an error !\n}\n\nvoid bar(void) { printf(\"bar\\n\"); qux(); }\nvoid foo(void) { printf(\"foo\\n\"); bar(); }\n\nint main(void) {\n    TRY {\n        foo();\n    } CATCH {\n        printf(\"An error occured\\n\");\n    } ETRY;\n}\n

    Dans cet exemple, si la fonction qux l\u00e8ve une exception, le programme sautera \u00e0 la ligne CATCH et affichera An error occured en court-circuitant les appels de fonctions interm\u00e9diaires.

    Notez que cette technique est tr\u00e8s dangereuse dans le cas de programmes utilisant l'allocation dynamique de m\u00e9moire. En effet, si une exception est lev\u00e9e alors que de la m\u00e9moire a \u00e9t\u00e9 allou\u00e9e, il y aura des fuites m\u00e9moires.

    ", "tags": ["goto", "longjmp", "exception.h", "qux", "setjmp", "CATCH"]}, {"location": "course-c/15-fundations/scope/", "title": "Port\u00e9e et visibilit\u00e9", "text": "

    Ce chapitre se concentre sur quatre caract\u00e9ristiques d'une variable\u2009:

    • La port\u00e9e
    • La visibilit\u00e9
    • La dur\u00e9e de vie
    • Son qualificatif de type

    Dans les quatre cas, elles d\u00e9crivent l'accessibilit\u00e9, c'est \u00e0 dire jusqu'\u00e0 ou jusqu'\u00e0 quand une variable est accessible, et de quelle mani\u00e8re

    Brouillard matinal sur le Golden Gate Bridge, San Francisco

    "}, {"location": "course-c/15-fundations/scope/#espace-de-nommage", "title": "Espace de nommage", "text": "

    L'espace de nommage ou namespace est un concept diff\u00e9rent de celui existant dans d'autres langages tel que C++. Le standard C99 d\u00e9crit 4 types possibles pour un identifiant\u2009:

    • fonction et labels
    • noms de structures (struct), d'unions (union), d'\u00e9num\u00e9ration (enum),
    • identifiants
    ", "tags": ["namespace", "enum", "union", "struct"]}, {"location": "course-c/15-fundations/scope/#portee", "title": "Port\u00e9e", "text": "

    La port\u00e9e ou scope d\u00e9crit jusqu'\u00e0 o\u00f9 une variable est accessible.

    Une variable est globale, c'est-\u00e0-dire accessible partout, si elle est d\u00e9clar\u00e9e en dehors d'une fonction\u2009:

    int global_variable = 23;\n

    Une variable est locale si elle est d\u00e9clar\u00e9e \u00e0 l'int\u00e9rieur d'un bloc, ou \u00e0 l'int\u00e9rieur d'une fonction. Elle sera ainsi visible de sa d\u00e9claration jusqu'\u00e0 la fin du bloc courant\u2009:

    int main(int)\n{\n    {\n        int i = 12;\n        i += 2; // Valide\n    }\n    i++; // Invalide, `i` n'est plus visible.\n}\n
    "}, {"location": "course-c/15-fundations/scope/#variable-shadowing", "title": "Variable shadowing", "text": "

    On dit qu'une variable est shadowed ou masqu\u00e9e si sa d\u00e9claration masque une variable pr\u00e9alablement d\u00e9clar\u00e9e\u2009:

    int i = 23;\n\nfor(size_t i = 0; i < 10; i++) {\n    printf(\"%ld\", i); // Acc\u00e8s \u00e0 `i` courant et non \u00e0 `i = 23`\n}\n\nprintf(\"%d\", i); // Acc\u00e8s \u00e0 `i = 23`\n
    "}, {"location": "course-c/15-fundations/scope/#visibilite", "title": "Visibilit\u00e9", "text": "

    Selon l'endroit o\u00f9 est d\u00e9clar\u00e9e une variable, elle ne sera pas n\u00e9cessairement visible partout ailleurs. Une variable locale n'est accessible qu'\u00e0 partir de sa d\u00e9claration et jusqu'\u00e0 la fin du bloc dans laquelle elle est d\u00e9clar\u00e9e.

    L'exemple suivant montre la visibilit\u00e9 de plusieurs variables\u2009:

                      //   a\nvoid foo(int a) { //   \u252c b\n    int b;        //   \u2502 \u252c\n    ...           //   \u2502 \u2502\n    {             //   \u2502 \u2502 c\n       int c;     //   \u2502 \u2502 \u252c\n       ...        //   \u2502 \u2502 \u2502 d\n       int d;     //   \u2502 \u2502 \u2502 \u252c\n       ...        //   \u2502 \u2502 \u2502 \u2502\n    }             //   \u2502 \u2502 \u2534 \u2534\n    ...           //   \u2502 \u2502\n}                 //   \u2534 \u2534\n

    Une variable d\u00e9clar\u00e9e globalement, c'est \u00e0 dire en dehors d'une fonction \u00e0 une dur\u00e9e de vie sur l'entier du module (translation unit) quel que soit l'endroit o\u00f9 elle est d\u00e9clar\u00e9e, en revanche elle n'est visible que depuis l'endroit ou elle est d\u00e9clar\u00e9e. Les deux variables i et j sont globales au module, c'est-\u00e0-dire qu'elles peuvent \u00eatre acc\u00e9d\u00e9es depuis n'importe quelle fonction contenue dans ce module.

    En revanche la variable j, bien qu'elle ait ait une dur\u00e9e de vie sur toute l'ex\u00e9cution du programme et que sa port\u00e9e est globale, elle ne pourra \u00eatre acc\u00e9d\u00e9e depuis, main car elle n'est pas visible.

    #include <stdio.h>\n                             //  i\nint i;                       //  \u252c\n                             //  \u2502\nint main() {                 //  \u2502\n    printf(\"%d %d\\n\", i, j); //  \u2502\n}                            //  \u2502\n                             //  \u2502 j\nint j;                       //  \u2534 \u252c\n

    Le mot cl\u00e9 extern permet non pas de d\u00e9clarer la variable, j mais de renseigner le compilateur qu'il existe ailleurs une variable j. C'est ce que l'on appelle une d\u00e9claration avanc\u00e9e ou forward-declaration. Dans ce cas, bien que j soit d\u00e9clar\u00e9e apr\u00e8s la fonction principale, elle est maintenant visible.

    #include <stdio.h>\n\n                        // j\nextern int j;           // \u252c   D\u00e9claration en amont de `j`\n                        // \u2502\nint main() {            // \u2502\n    printf(\"%d\\n\", j);  // \u2502\n}                       // \u2502\n                        // \u2502\nint j;                  // \u2502\n                        // \u2502\n

    Une particularit\u00e9 en C est que tout symbole global (variable ou fonction) a une accessibilit\u00e9 transversale. C'est-\u00e0-dire que dans le cas de la compilation s\u00e9par\u00e9e, une variable d\u00e9clar\u00e9e dans un fichier, peut \u00eatre acc\u00e9d\u00e9e depuis un autre fichier, il en va de m\u00eame pour les fonctions.

    L'exemple suivant implique deux fichiers foo.c et main.c. Dans l'un deux symboles sont d\u00e9clar\u00e9s, une variable et une fonction.

    foo.c
    int foo;\n\nvoid do_foo() {\n    printf(\"Foo does...\");\n}\n

    Depuis le programme principal, il est possible d'acc\u00e9der \u00e0 des symboles \u00e0 condition de renseigner sur le prototype de la fonction et l'existence de la variable\u2009:

    main.c
    extern int foo;\nextern void do_foo(); // Non obligatoire\n\nint main() {\n    foo();\n}\n

    Dans le cas o\u00f9 l'on voudrait restreindre l'accessibilit\u00e9 d'une variable au module dans lequel elle est d\u00e9clar\u00e9e, l'usage du mot cl\u00e9 static s'impose.

    En \u00e9crivant static int foo; dans foo.c, la variable n'est plus accessible en dehors du module m\u00eame avec une d\u00e9claration en avance. On dit que sa port\u00e9e est r\u00e9duite au module.

    ", "tags": ["extern", "main", "foo.c", "static", "main.c"]}, {"location": "course-c/15-fundations/scope/#qualificatif-de-type", "title": "Qualificatif de type", "text": "

    Les variables en C peuvent \u00eatre cr\u00e9\u00e9es de diff\u00e9rentes mani\u00e8res. Selon la mani\u00e8re dont elles pourront \u00eatre utilis\u00e9es, il est courant de les classer en cat\u00e9gories.

    Une classe de stockage peut \u00eatre implicite \u00e0 une d\u00e9claration de variable ou explicite, en ajoutant un attribut devant la d\u00e9claration de celle-ci.

    "}, {"location": "course-c/15-fundations/scope/#auto", "title": "auto", "text": "

    Cette classe est utilis\u00e9e par d\u00e9faut lorsqu'aucune autre classe n'est pr\u00e9cis\u00e9e. Les variables automatiques sont visibles uniquement dans le bloc o\u00f9 elles sont d\u00e9clar\u00e9es. Ces variables sont habituellement cr\u00e9\u00e9es sur la pile (stack), mais peuvent \u00eatre aussi stock\u00e9es dans les registres du processeur. C'est un choix qui incombe au compilateur.

    auto type identificateur = valeur_initiale;\n

    Pour les variables automatiques, le mot-cl\u00e9 auto n'est pas obligatoire, et n'est pas recommand\u00e9 en C99, car son utilisation est implicite.

    ", "tags": ["auto"]}, {"location": "course-c/15-fundations/scope/#register", "title": "register", "text": "

    Ce mot cl\u00e9 incite le compilateur \u00e0 utiliser un registre processeur pour stocker la variable. Ceci permet de gagner en temps d'ex\u00e9cution, car la variable n'a pas besoin d'\u00eatre charg\u00e9e depuis et \u00e9crite vers la m\u00e9moire.

    Jadis, ce mot cl\u00e9 \u00e9tait utilis\u00e9 devant toutes les variables d'it\u00e9rations de boucles. La traditionnelle variable i utilis\u00e9e dans les boucles for \u00e9tait d\u00e9clar\u00e9e register int i = 0;. Les compilateurs modernes savent aujourd'hui identifier les variables les plus souvent utilis\u00e9es. L'usage de ce mot cl\u00e9 n'est donc plus recommand\u00e9 depuis C99.

    ", "tags": ["for", "register"]}, {"location": "course-c/15-fundations/scope/#const", "title": "const", "text": "

    Ce mot cl\u00e9 rend une d\u00e9claration non modifiable par le programme lui-m\u00eame. N\u00e9anmoins il ne s'agit pas de constantes au sens strict du terme, car une variable de type const pourrait tr\u00e8s bien \u00eatre modifi\u00e9e par erreur en jardinant la m\u00e9moire. Quand ce mot cl\u00e9 est appliqu\u00e9 \u00e0 une structure, aucun des champs de la structure n'est accessible en \u00e9criture. Bien qu'il puisse para\u00eetre \u00e9trange de vouloir rendre \u00ab constante \u00bb une \u00ab variable \u00bb, ce mot cl\u00e9 a une utilit\u00e9. En particulier, il permet de faire du code plus s\u00fbr.

    ", "tags": ["const"]}, {"location": "course-c/15-fundations/scope/#static", "title": "static", "text": "

    Elle permet de d\u00e9clarer des variables dont le contenu est pr\u00e9serv\u00e9 m\u00eame lorsque l'on sort du bloc o\u00f9 elles ont \u00e9t\u00e9 d\u00e9clar\u00e9es.

    Elles ne sont donc initialis\u00e9es qu'une seule fois. L'exemple suivant est une fonction qui retourne \u00e0 chaque fois une valeur diff\u00e9rente, incr\u00e9ment\u00e9e de 1. La variable i agit ici comme une variable globale, elle n'est initialis\u00e9e qu'une seule fois \u00e0 0 et donc s'incr\u00e9mente d'appel en appel. En revanche, elle n'est pas accessible en dehors de la fonction\u2009; c'est donc une variable locale.

    int iterate() {\n    static int i = 0;\n    return i++;\n}\n

    Il n'est pas rare de voir des variables globales, ou des fonctions pr\u00e9c\u00e9d\u00e9es du mot cl\u00e9 static. Ces variables sont dites statiques au module. Elles ne sont donc pas accessibles depuis un autre module (translation unit)

    La fonction suivante est statique au module dans lequel elle est d\u00e9clar\u00e9e. Il ne sera donc pas possible d'y acc\u00e9der depuis un autre fichier C.

    static int add(int a, int b) { return a + b; }\n

    On utilisera volontiers le mot cl\u00e9 static pour marquer les fonctions qui ne sont pas exportables, c'est-\u00e0-dire qui ne sont pas destin\u00e9es \u00e0 \u00eatre utilis\u00e9es par d'autres modules. Il s'agit des fonctions utlitaires, des fonctions internes \u00e0 un module. Il en va de m\u00eame pour les variables. On \u00e9vitera dans tous les cas d'utiliser des variables globales mais si cela est n\u00e9cessaire, on les marquera static pour \u00e9viter qu'elle ne soient accessibles depuis un autre fichier.

    ", "tags": ["static"]}, {"location": "course-c/15-fundations/scope/#volatile", "title": "volatile", "text": "

    Cette classe de stockage indique au compilateur qu'il ne peut faire aucune hypoth\u00e8se d'optimisation concernant cette variable. Elle indique que son contenu peut \u00eatre modifi\u00e9 en tout temps en arri\u00e8re-plan par le syst\u00e8me d'exploitation ou le mat\u00e9riel. Ce mot cl\u00e9 est davantage utilis\u00e9 en programmation syst\u00e8me, ou sur microcontr\u00f4leurs.

    L'usage de cette classe de stockage r\u00e9duit les performances d'un programme puisqu'elle emp\u00eache l'optimisation du code et le contenu de cette variable devra \u00eatre recharg\u00e9 \u00e0 chaque utilisation

    Consid\u00e9rons le cas du programme suivant\u2009:

    #include <stdio.h>\n\nint main() {\n    int i = 0;\n\n    i = 1;\n    i = 0;\n    i = 1;\n    i = 0;\n\n    printf(\"%d\", i);\n}\n

    On notera que les 4 lignes o\u00f9 i successivement assign\u00e9 \u00e0 1 et 0 sont inutiles, car dans tous les cas, la valeur 0 sera affich\u00e9e. Si le programme est compil\u00e9, on obtient le listing suivant\u2009:

    $ gcc main.c\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001149 <main>:\n    1149:       f3 0f 1e fa             endbr64\n    114d:       55                      push   %rbp\n    114e:       48 89 e5                mov    %rsp,%rbp\n    1151:       48 83 ec 10             sub    $0x10,%rsp\n    1155:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    115c:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)\n    1163:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    116a:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)\n    1171:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    1178:       8b 45 fc                mov    -0x4(%rbp),%eax\n    117b:       89 c6                   mov    %eax,%esi\n    117d:       48 8d 3d 80 0e 00 00    lea    0xe80(%rip),%rdi\n    1184:       b8 00 00 00 00          mov    $0x0,%eax\n    1189:       e8 c2 fe ff ff          callq  1050 <printf@plt>\n    118e:       b8 00 00 00 00          mov    $0x0,%eax\n    1193:       c9                      leaveq\n    1194:       c3                      retq\n    1195:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    119c:       00 00 00\n    119f:       90                      nop\n

    Les lignes 1155 \u00e0 1171 refl\u00e8tent bien le comportement attendu. En revanche, si le programme est compil\u00e9 avec l'optimisation, notez la diff\u00e9rence\u2009:

    $ gcc main.c -O2\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001060 <main>:\n    1060:       f3 0f 1e fa             endbr64\n    1064:       48 83 ec 08             sub    $0x8,%rsp\n    1068:       31 d2                   xor    %edx,%edx\n    106a:       48 8d 35 93 0f 00 00    lea    0xf93(%rip),%rsi\n    1071:       31 c0                   xor    %eax,%eax\n    1073:       bf 01 00 00 00          mov    $0x1,%edi\n    1078:       e8 d3 ff ff ff          callq  1050 <__printf_chk@plt>\n    107d:       31 c0                   xor    %eax,%eax\n    107f:       48 83 c4 08             add    $0x8,%rsp\n    1083:       c3                      retq\n    1084:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    108b:       00 00 00\n    108e:       66 90                   xchg   %ax,%ax\n

    Les lignes ont disparu\u2009!

    Afin d'\u00e9viter cette optimisation, il faut marquer la variable i comme volatile:

    #include <stdio.h>\n\nint main() {\n    volatile int i = 0;\n\n    i = 1;\n    i = 0;\n    i = 1;\n    i = 0;\n\n    printf(\"%d\", i);\n}\n
    ", "tags": ["volatile"]}, {"location": "course-c/15-fundations/scope/#extern", "title": "extern", "text": "

    Cette classe est utilis\u00e9e pour signaler que la variable ou la fonction associ\u00e9e est d\u00e9clar\u00e9e dans un autre module (autre fichier). Ainsi le code suivant ne d\u00e9clare pas une nouvelle variable, foo mais s'attend \u00e0 ce que cette variable ait \u00e9t\u00e9 d\u00e9clar\u00e9e dans un autre fichier.

    extern int foo;\n
    ", "tags": ["foo", "extern"]}, {"location": "course-c/15-fundations/scope/#restrict", "title": "restrict", "text": "

    En C, le mot cl\u00e9 restrict, apparu avec C99, est utilis\u00e9 uniquement pour des pointeurs. Ce qualificatif de type informe le compilateur que pour toute la dur\u00e9e de vie du pointeur, aucun autre pointeur ne pointera que sur la valeur qu'il pointe ou une valeur d\u00e9riv\u00e9e de lui-m\u00eame (p. ex\u2009: p + 1).

    En d'autres termes, le qualificatif indique au compilateur que deux pointeurs diff\u00e9rents ne peuvent pas pointer sur les m\u00eames r\u00e9gions m\u00e9moires.

    Prenons l'exemple simple d'une fonction qui met \u00e0 jour deux pointeurs avec une valeur pass\u00e9e en param\u00e8tre\u2009:

    void update_ptr(size_t *a, size_t *b, const size_t *value) {\n    *a += *value;\n    *b += *value;\n}\n

    Le compilateur, n'ayant aucune information sur les pointeurs fournis, ne peut faire aucune hypoth\u00e8se d'optimisation. En effet, ces deux pointeurs a et b ainsi que value pourraient tr\u00e8s bien pointer sur la m\u00eame r\u00e9gion m\u00e9moire, et dans ce cas *a += *value aurait pour effet d'incr\u00e9menter value. En revanche, dans le cas o\u00f9 la fonction est d\u00e9clar\u00e9e de la fa\u00e7on suivante\u2009:

    void update_ptr(size_t *restrict a, size_t * restrict b,\n                const size_t *restrict value) {\n    *a += *value;\n    *b += *value;\n}\n

    le compilateur est inform\u00e9 qu'il peut faire l'hypoth\u00e8se que les trois pointeurs fournis en param\u00e8tres sont ind\u00e9pendants les uns des autres. Dans ce cas il peut optimiser le code. Voir restrict sur Wikipedia pour plus de d\u00e9tails.

    ", "tags": ["restrict", "value"]}, {"location": "course-c/15-fundations/stdio/", "title": "Entr\u00e9es Sorties", "text": "Le probl\u00e8me fondamental de la communication est celui de reproduire en un point, soit exactement, soit approximativement, un message s\u00e9lectionn\u00e9 \u00e0 un autre point.Claude Shannon

    Un programme informatique se compose d'entr\u00e9es (stdin) et de sorties (stdout et stderr).

    Pour faciliter la vie du programmeur, les biblioth\u00e8ques standard offrent toute une panoplie de fonctions pour formater les sorties et interpr\u00e9ter les entr\u00e9es.

    Les fonctions phares sont printf pour le formatage de cha\u00eene de caract\u00e8res et scanf pour la lecture de cha\u00eenes de caract\u00e8res. Ces derni\u00e8res fonctions se d\u00e9clinent en plusieurs variantes que nous verrons plus tard. La liste cit\u00e9e est non exhaustive, mais largement document\u00e9e ici\u2009: <stdio.h>.

    Les fonctions que nous allons aborder dans ce chapitre sont donn\u00e9es par la table suivante. Pour davantage de fonctions, vous pouvez vous rendre au chapitre traitant de la biblioth\u00e8que standard stdio.

    Fonctions d'entr\u00e9es/sorties principales Fonction Type Description putchar Sortie \u00c9crit un caract\u00e8re sur la sortie standard puts Sortie \u00c9crit une cha\u00eene de caract\u00e8res sur la sortie standard printf Sortie \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e sur la sortie standard getchar Entr\u00e9e Lit un caract\u00e8re sur l'entr\u00e9e standard gets Entr\u00e9e Lit une cha\u00eene de caract\u00e8res sur l'entr\u00e9e standard scanf Entr\u00e9e Lit une cha\u00eene de caract\u00e8res format\u00e9e sur l'entr\u00e9e standard", "tags": ["scanf", "stdin", "stderr", "printf", "stdout"]}, {"location": "course-c/15-fundations/stdio/#sorties-non-formatees", "title": "Sorties non format\u00e9es", "text": "

    Ces fonctions sont tr\u00e8s basiques et permettent d'\u00e9crire des caract\u00e8res ou des cha\u00eenes de caract\u00e8res sur la sortie standard.

    "}, {"location": "course-c/15-fundations/stdio/#putchar_1", "title": "Putchar", "text": "

    Cette fonction prend en param\u00e8tre un seul caract\u00e8re et l'\u00e9crit sur la sortie standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h.

    #include <stdio.h>\n\nint main() {\n    putchar('H');\n    putchar('e');\n    putchar('l');\n    putchar('l');\n    putchar('o');\n    putchar('\\n');\n}\n

    Avertissement

    Attention \u00e0 utiliser des apostrophes simples ' pour les caract\u00e8res. Si vous utilisez des guillemets doubles \" vous obtiendrez une erreur de compilation.

    On sait que les caract\u00e8res sont des entiers, donc on peut \u00e9crire putchar(65) pour \u00e9crire le caract\u00e8re A. Donc le programme suivant \u00e9crit la m\u00eame chose que le pr\u00e9c\u00e9dent\u2009:

    #include <stdio.h>\n\nint main() {\n    putchar(72);\n    putchar(101);\n    putchar(108);\n    putchar(108);\n    putchar(111);\n    putchar('\\n');\n}\n

    ", "tags": ["stdio.h"]}, {"location": "course-c/15-fundations/stdio/#puts_1", "title": "Puts", "text": "

    La fonction puts est une fonction de la biblioth\u00e8que standard C qui permet d'\u00e9crire une cha\u00eene de caract\u00e8res sur la sortie standard. Notons qu'elle ajoute automatiquement un retour \u00e0 la ligne \u00e0 la fin de la cha\u00eene.

    #include <stdio.h>\n\nint main() {\n    puts(\"hello, world\"); // Un retour \u00e0 la ligne est ajout\u00e9 automatiquement\n}\n

    Notez ici qu'on utilise des guillemets doubles \" pour les cha\u00eenes de caract\u00e8res.

    ", "tags": ["puts"]}, {"location": "course-c/15-fundations/stdio/#sorties-formatees", "title": "Sorties format\u00e9es", "text": "

    Convertir un nombre en une cha\u00eene de caract\u00e8res n'est pas trivial. Prenons l'exemple de la valeur 123. Il faut pour cela diviser it\u00e9rativement le nombre par 10 et calculer le reste\u2009:

    Etape  Op\u00e9ration  Resultat  Reste\n-----  ---------  --------  -----\n1      123 / 10   12        3\n2      12 / 10    1         2\n3      1 / 10     0         1\n

    Comme on ne sait pas \u00e0 priori combien de caract\u00e8res on aura, et que ces caract\u00e8res sont fournis depuis le chiffre le moins significatif, il faudra inverser la cha\u00eene de caract\u00e8res produite.

    Voici un exemple possible d'impl\u00e9mentation\u2009:

    #include <stdlib.h>\n#include <stdbool.h>\n\nvoid swap(char* a, char* b)\n{\n    char old_a = a;\n    a = b;\n    b = old_a;\n}\n\nvoid reverse(char* str, size_t length)\n{\n    for (size_t start = 0, end = length - 1; start < end; start++, end--)\n    {\n        swap(str + start, str + end);\n    }\n}\n\nvoid my_itoa(int num, char* str)\n{\n    const unsigned int base = 10;\n    bool is_negative = false;\n    size_t i = 0;\n\n    if (num == 0) {\n        str[i++] = '0';\n        str[i] = '\\0';\n        return;\n    }\n\n    if (num < 0) {\n        is_negative = true;\n        num = -num;\n    }\n\n    while (num != 0) {\n        int rem = num % 10;\n        str[i++] = rem + '0';\n        num /= base;\n    }\n\n    if (is_negative)\n        str[i++] = '-';\n\n    str[i] = '\\0';\n\n    reverse(str, i);\n}\n

    Cette impl\u00e9mentation pourrait \u00eatre utilis\u00e9e de la fa\u00e7on suivante\u2009:

    #include <stdlib.h>\n\nint main(void)\n{\n    int num = 123;\n    char buffer[10];\n\n    itoa(num, buffer);\n}\n

    "}, {"location": "course-c/15-fundations/stdio/#printf", "title": "Printf", "text": "

    Vous conviendrez que devoir manuellement convertir chaque valeur n'est pas des plus pratique, c'est pourquoi printf rend l'op\u00e9ration bien plus ais\u00e9e en utilisant des marques substitutives (placeholder). Ces sp\u00e9cifi\u00e9 d\u00e9butent par le caract\u00e8re % suivi du formatage que l'on veut appliquer \u00e0 une variable pass\u00e9e en param\u00e8tres. L'exemple suivant utilise %d pour formater un entier non sign\u00e9.

    #include <stdio.h>\n\nint main()\n{\n    int32_t earth_perimeter = 40075;\n    printf(\"La circonf\u00e9rence de la terre vaut vaut %d km\", earth_perimeter);\n}\n

    Le standard C d\u00e9fini le prototype de printf comme \u00e9tant\u2009:

    int printf(const char *restrict format, ...);\n

    Il d\u00e9finit que la fonction printf prend en param\u00e8tre un format suivi de .... La fonction printf comme toutes celles de la m\u00eame cat\u00e9gorie sont dites variadiques, c'est-\u00e0-dire qu'elles peuvent prendre un nombre variable d'arguments. Il y aura autant d'arguments additionnels que de marqueurs utilis\u00e9s dans le format. Ainsi le format \"Mes nombres pr\u00e9f\u00e9r\u00e9s sont %d et %d, mais surtout %s\" demandera trois param\u00e8tres additionnels\u2009:

    La fonction retourne le nombre de caract\u00e8res format\u00e9s ou -1 en cas d'erreur.

    La construction d'un marqueur est loin d'\u00eatre simple, mais heureusement on n'a pas besoin de tout conna\u00eetre et la page Wikip\u00e9dia printf format string est d'une grande aide. Le format de construction est le suivant\u2009:

    %[parameter][flags][width][.precision][length]type\n
    parameter (optionnel)

    Num\u00e9ro de param\u00e8tre \u00e0 utiliser

    flags (optionnel)

    Modificateurs\u2009: pr\u00e9fixe, signe plus, alignement \u00e0 gauche ...

    width (optionnel)

    Nombre minimum de caract\u00e8res \u00e0 utiliser pour l'affichage de la sortie.

    .precision (optionnel)

    Nombre minimum de caract\u00e8res affich\u00e9s \u00e0 droite de la virgule. Essentiellement, valide pour les nombres \u00e0 virgule flottante.

    length (optionnel)

    Longueur en m\u00e9moire. Indique la longueur de la repr\u00e9sentation binaire.

    type

    Type de formatage souhait\u00e9

    Formatage d'un marqueur

    Voici quelques exemples\u2009:

    Exemple de formatage avec printf Exemple Sortie Taille printf(\"%c\", 'c') c 1 printf(\"%d\", 1242) 1242 4 printf(\"%10d\", 42) 42 10 printf(\"%07d\", 42) 0000042 7 printf(\"%+-5dfr\", 23) +23 fr 6 printf(\"%5.3f\", 314.15) 314.100 7 printf(\"%*.*f\", 4, 2, 102.1) 102.10 7 printf(\"%8x\", 57005) dead 6 printf(\"%s\", \"Hello\") Hello 5

    On peut s'int\u00e9resser \u00e0 comment printf fonctionne en interne. Le premier argument est une cha\u00eene de caract\u00e8re qui est le motif de formatage. Il peut contenir des caract\u00e8res sp\u00e9ciaux placeholder qui seront intercept\u00e9s par printf pour \u00eatre remplac\u00e9s par les arguments suivants apr\u00e8s avoir \u00e9t\u00e9 convertis.

    Pour bien comprendre, on peut imaginer une impl\u00e9mentation na\u00efve de printf que nous appellerons my_printf et qui se basera sur une fonction de sortie non format\u00e9e putchar.

    Cette fonction ne sera capable que de traiter les marqueurs %d et %c, c'est suffisant pour comprendre le principe. \u00c9galement, elle prendra toujours deux arguments, donc une valeur \u00e0 afficher, ceci pour ne pas s'encombrer de la gestion de la liste variable d'arguments qui est un sujet avanc\u00e9.

    void my_printf(char format[], int a) {\n    // On parcourt la cha\u00eene de caract\u00e8res tant que l'on ne rencontre\n    // pas le caract\u00e8re de fin de cha\u00eene\n    for (int i = 0; format[i] != '\\0'; i++) {\n        // Si on rencontre un caract\u00e8re %, on regarde le caract\u00e8re suivant\n        if (format[i] == '%') {\n            // Est-ce que ce caract\u00e8re est sp\u00e9cial ?\n            switch (format[++i]) {\n                case 'd': {\n                    char str[32] = {0};\n                    my_itoa(int a, str);\n                    for (int j = 0; str[j] != '\\0'; j++) {\n                        putchar(str[j]);\n                    }\n                    break;\n                }\n                case 'c':\n                    // Affiche le caract\u00e8re en ASCII\n                    putchar(a);\n                    break;\n                default:\n                    // On affiche le caract\u00e8re tel quel,\n                    // ce qui permet d'afficher le caract\u00e8re %\n                    putchar(format[i]);\n            }\n        } else {\n            putchar(format[i]);\n        }\n    }\n}\n

    Exercice 1\u2009: Exercice

    Indiquez les erreurs dans les instructions suivantes\u2009:

    printf(\"%d%d\\n\", 10, 20);\nprintf(\"%d, %d, %d\\n\", 10, 20);\nprintf(\"%d, %d, %d, %d\\n\", 10, 20, 30, 40.);\nprintf(\"%*d, %*d\\n\", 10, 20);\nprintf(\"%6.2f\\n\", 10);\nprintf(\"%10s\\n\", 0x9f);\n
    ", "tags": ["type", "parameter", "dead", "putchar", "my_printf", "printf", "Hello", "flags", "length", "width"]}, {"location": "course-c/15-fundations/stdio/#entrees-non-formatees", "title": "Entr\u00e9es non format\u00e9es", "text": ""}, {"location": "course-c/15-fundations/stdio/#getchar_1", "title": "Getchar", "text": "

    La fonction getchar est une fonction de la biblioth\u00e8que standard C qui permet de lire un caract\u00e8re sur l'entr\u00e9e standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h. Elle retourne un entier qui correspond \u00e0 la valeur ASCII du caract\u00e8re lu.

    #include <stdio.h>\n\nint main() {\n    int c;\n    while ((c = getchar()) != EOF) {\n        printf(\"Caract\u00e8re lu : %c\\n\", c);\n    }\n}\n

    Notez ici l'utilisation de EOF qui est une constante d\u00e9finie dans la biblioth\u00e8que stdio.h et qui signifie End Of File. Elle est utilis\u00e9e pour d\u00e9tecter la fin d'un fichier.

    Lorsque vous ex\u00e9cutez ce programme, vous pouvez saisir des caract\u00e8res au clavier. Pour terminer la saisie, vous pouvez utiliser la combinaison de touches ++Ctrl+D++ sur Linux ou ++Ctrl+Z++ sur Windows.

    ", "tags": ["EOF", "getchar", "stdio.h"]}, {"location": "course-c/15-fundations/stdio/#gets_1", "title": "Gets", "text": "

    La fonction gets est une fonction de la biblioth\u00e8que standard C qui permet de lire une cha\u00eene de caract\u00e8res sur l'entr\u00e9e standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h.

    Elle est d\u00e9conseill\u00e9e, car elle ne permet pas de sp\u00e9cifier la taille maximale de la cha\u00eene \u00e0 lire. Cela peut entra\u00eener des d\u00e9bordements de m\u00e9moire si un utilisateur saisit une cha\u00eene de caract\u00e8res trop longue que le programme ne peut pas stocker.

    #include <stdio.h>\n\nint main() {\n    char str[128];\n    gets(str);\n    printf(\"Cha\u00eene lue : %s\\n\", str);\n}\n

    Avertissement

    La fonction gets est d\u00e9conseill\u00e9e. Il est pr\u00e9f\u00e9rable d'utiliser la fonction fgets qui permet de sp\u00e9cifier la taille maximale de la cha\u00eene \u00e0 lire.

    ", "tags": ["gets", "fgets", "stdio.h"]}, {"location": "course-c/15-fundations/stdio/#entrees-formatees", "title": "Entr\u00e9es format\u00e9es", "text": "

    Les fonctions de lecture de cha\u00eenes de caract\u00e8res sont plus complexes que les fonctions d'\u00e9criture. En effet, il est n\u00e9cessaire de sp\u00e9cifier le format de la cha\u00eene \u00e0 lire.

    "}, {"location": "course-c/15-fundations/stdio/#scanf_1", "title": "Scanf", "text": "

    \u00c0 l'instar de la sortie format\u00e9e, il est possible de lire les saisies au clavier ou parser une cha\u00eene de caract\u00e8res, c'est-\u00e0-dire faire une analyse syntaxique de son contenu pour en extraire de l'information.

    La fonction scanf est par exemple utilis\u00e9e \u00e0 cette fin\u2009:

    #include <stdio.h>\n\nint main()\n{\n    int favorite;\n\n    printf(\"Quelle est votre nombre favori ? \");\n    scanf(\"%d\", &favorite);\n\n    printf(\"Saviez-vous que votre nombre favori, %d, est %s ?\\n\",\n        favorite,\n        favorite % 2 ? \"impair\" : \"pair\");\n}\n

    Cette fonction utilise l'entr\u00e9e standard stdin. Il est donc possible soit d'ex\u00e9cuter ce programme en mode interactif\u2009:

    $ ./a.out\nQuelle est votre nombre favori ? 2\nSaviez-vous que votre nombre favori, 2, est pair ?\n

    soit d'ex\u00e9cuter ce programme en fournissant le n\u00e9cessaire \u00e0 stdin\u2009:

    $ echo \"23\" | ./a.out\nQuel est votre nombre favori ? Saviez-vous que votre nombre favori, 23, est impair ?\n

    On observe ici un comportement diff\u00e9rent, car le retour clavier lorsque la touche enter est press\u00e9e n'est pas transmis au programme, mais c'est le shell qui l'intercepte.

    Le format de scanf se rapproche de printf mais en plus simple. Le man scanf ou m\u00eame la page Wikip\u00e9dia de scanf renseigne sur son format.

    Cette fonction tient son origine une nouvelle fois de ALGOL 68 (readf), elle est donc tr\u00e8s ancienne.

    La compr\u00e9hension de scanf n'est pas \u00e9vidente et il est utile de se familiariser sur son fonctionnement \u00e0 l'aide de quelques exemples.

    Le programme suivant lit un entier et le place dans la variable n. scanf retourne le nombre d'assignements r\u00e9ussis. Ici, il n'y a qu'un placeholder, on s'attend naturellement \u00e0 lire 1 si la fonction r\u00e9ussit. Le programme \u00e9crit ensuite les nombres dans l'ordre d'apparition.

    #include <stdio.h>\n\nint main(void)\n{\n    int i = 0, n;\n\n    while (scanf(\"%d\", &n) == 1)\n        printf(\"%i\\t%d\\n\", ++i, n);\n    return 0;\n}\n

    Si le code est ex\u00e9cut\u00e9 avec une suite arbitraire de nombres\u2009:

    456 123 789     456 12\n456 1\n    2378\n

    il affichera chacun des nombres dans l'ordre d'apparition\u2009:

    $ cat << EOF | ./a.out\n456 123 789     456 12\n456 1\n    2378\nEOF\n1       456\n2       123\n3       789\n4       456\n5       12\n6       456\n7       1\n8       2378\n

    Voyons un exemple plus complexe (c.f. C99 \u00a77.19.6.2-19).

    int count;\nfloat quantity;\nchar units[21], item[21];\n\ndo {\n    count = scanf(\"%f%20s de %20s\", &quant, units, item);\n    scanf(\"%*[^\\n]\");\n} while (!feof(stdin) && !ferror(stdin));\n

    Lorsqu'ex\u00e9cut\u00e9 avec ce contenu\u2009:

    2 litres de lait\n-12.8degr\u00e9s Celsius\nbeaucoup de chance\n10.0KG de\npoussi\u00e8re\n100ergs d\u2019\u00e9nergie\n

    Le programme se d\u00e9roule comme suit\u2009:

    quantity = 2; strcpy(units, \"litres\"); strcpy(item, \"lait\");\ncount = 3;\n\nquantity = -12.8; strcpy(units, \"degrees\");\ncount = 2; // \"C\" \u00e9choue lors du test de \"d\" (de)\n\ncount = 0; // \"b\" de \"beaucoup\" \u00e9choue contre \"%f\" s'attendant \u00e0 un float\n\nquantity = 10.0; strcpy(units, \"KG\"); strcpy(item, \"poussi\u00e8re\");\ncount = 3;\n\ncount = 0; // \"100e\" \u00e9choue contre \"%f\", car \"100e3\" serait un nombre valable\ncount = EOF; // Fin de fichier\n

    Dans cet exemple, la boucle do... while est utilis\u00e9e, car il n'est pas simplement possible de traiter le cas while(scanf(...) > 0 puisque l'exemple cherche \u00e0 montrer les cas particuliers o\u00f9 justement, la capture \u00e9choue. Il est n\u00e9cessaire alors de faire appel \u00e0 des fonctions de plus bas niveau feof pour d\u00e9tecter si la fin du fichier est atteinte, et ferror pour d\u00e9tecter une \u00e9ventuelle erreur sur le flux d'entr\u00e9e.

    La directive scanf(\"%*[^\\n]\"); \u00e9tant un peu particuli\u00e8re, il peut valoir la peine de s'y attarder un peu. Le flag *, diff\u00e9rent de printf indique d'ignorer la capture en cours. L'exemple suivant montre comment ignorer un mot.

    #include <assert.h>\n#include <stdio.h>\n\nint main(void) {\n    int a, b;\n    char str[] = \"24 kayaks 42\";\n\n    sscanf(str, \"%d%*s%d\", &a, &b);\n    assert(a == 24);\n    assert(b == 42);\n}\n

    Ensuite, [^\\n]. Le marqueur [, termin\u00e9 par ] cherche \u00e0 capturer une s\u00e9quence de caract\u00e8res parmi une liste de caract\u00e8res accept\u00e9s. Cette syntaxe est inspir\u00e9e des expressions r\u00e9guli\u00e8res tr\u00e8s utilis\u00e9es en informatique. Le caract\u00e8re ^ \u00e0 une signification particuli\u00e8re, il indique que l'on cherche \u00e0 capturer une s\u00e9quence de caract\u00e8res parmi une liste de caract\u00e8res qui ne sont pas accept\u00e9s. C'est une sorte de n\u00e9gation. Dans le cas pr\u00e9sent, cette directive scanf cherche \u00e0 consommer tous les caract\u00e8res jusqu'\u00e0 une fin de ligne, car, dans le cas ou la capture \u00e9choue \u00e0 C de Celsius, le pointeur de fichier est bloqu\u00e9 au caract\u00e8re C et au prochain tour de boucle, scanf \u00e9chouera au m\u00eame endroit. Cette instruction est donc utilis\u00e9e pour repartir sur des bases saines en sautant \u00e0 la prochaine ligne.

    Exercice 2\u2009: scanf sur des entiers et des r\u00e9els

    Consid\u00e9rant les d\u00e9clarations\u2009:

    int i, j, k;\nfloat f;\n

    Donnez les valeurs de chacune des variables apr\u00e8s ex\u00e9cution. Chaque ligne est ind\u00e9pendante des autres.

    i = sscanf(\"1 12.5\", \"%d %d, &j, &k);\nsscanf(\"12.5\", \"%d %f\", &j, %f);\ni = sscanf(\"123 123\", \"%d %f\", &j, &f);\ni = sscanf(\"123a 123\", \"%d %f\", &j, &f);\ni = sscanf(\"%2d%2d%f\", &j, &k, &f);\n

    Exercice 3\u2009: Saisie de valeurs

    Consid\u00e9rant les d\u00e9clarations suivantes, donner la valeur des variables apr\u00e8s l'ex\u00e9cution des instructions donn\u00e9es avec les captures associ\u00e9es\u2009:

    int i = 0, j = 0, n = 0;\nfloat x = 0;\n
    no Expression Entr\u00e9e 1 n = scanf(\"%1d%1d\", &i, &j); 12\\n 2 n = scanf(\"%d%d\", &i, &j); 1 , 2\\n 3 n = scanf(\"%d%d\", &i, &j); -1 -2\\n 4 n = scanf(\"%d%d\", &i, &j); - 1 - 2\\n 5 n = scanf(\"%d,%d\", &i, &j); 1 , 2\\n 6 n = scanf(\"%d ,%d\", &i, &j); 1 , 2\\n 7 n = scanf(\"%4d %2d\", &i, &j); 1 234\\n 8 n = scanf(\"%4d %2d\", &i, &j); 1234567\\n 9 n = scanf(\"%d%*d%d\", &i, &j); 123 456 789\\n 10 n = scanf(\"i=%d , j=%d\", &i, &j); 1 , 2\\n 11 n = scanf(\"i=%d , j=%d\", &i, &j); i=1, j=2\\n 12 n = scanf(\"%d%d\", &i, &j); 1.23 4.56\\n 13 n = scanf(\"%d.%d\", &i, &j); 1.23 4.56\\n 14 n = scanf(\"%x%x\", &i, &j); 12 2a\\n 15 n = scanf(\"%x%x\", &i, &j); 0x12 0X2a\\n 16 n = scanf(\"%o%o\", &i, &j); 12 018\\n 17 n = scanf(\"%f\", &x); 123\\n 18 n = scanf(\"%f\", &x); 1.23\\n 19 n = scanf(\"%f\", &x); 123E4\\n 20 n = scanf(\"%e\", &x); 12\\n Solution Q i j n Remarque 1 1 2 2 2 1 0 1. j n'est pas lue car arr\u00eat pr\u00e9matur\u00e9 sur , 3 -1 -2 2 4 0 0 0. i n'est pas lue car arr\u00eat pr\u00e9matur\u00e9 sur - 5 1 0 1. 6 1 2 2 7 1 23 2 8 1234 56 2 9 123 789 2 10 0 0 0 11 1 2 2 12 1 0 1 13 1 23 2 14 18 42 2 15 10 1 2. Le chiffre 8 interdit en octal provoque un arr\u00eat x n 16 123. 1 17 1.23 1 18 1.23E6 1 19 12 1

    Exercice 4\u2009: Cha\u00eenes de formats

    1. Saisir 3 caract\u00e8res cons\u00e9cutifs dans des variables i, j, k.
    2. Saisir 3 nombres de type float s\u00e9par\u00e9s par un point-virgule et un nombre quelconque d'espaces dans des variables x, y et z.
    3. Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe =, dans des variables t, u et v.
    Solution
    1. Saisir 3 caract\u00e8res cons\u00e9cutifs dans des variables i, j, k.

      scanf(\"%c%c%c\", &i, &j, &k);\n
    2. Saisir 3 nombres de type float s\u00e9par\u00e9s par un point-virgule et un nombre quelconque d'espaces dans des variables x, y et z.

      scanf(\"%f ;%f ;%f\", &x, &y, &z);\n
    3. Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe =, dans des variables t, u et v.

      printf(\"t=\"); scanf(\"%f\", &t);\nprintf(\"u=\"); scanf(\"%f\", &u);\nprintf(\"v=\"); scanf(\"%f\", &v);\n
    ", "tags": ["readf", "while", "scanf", "ferror", "stdin", "printf", "Celsius", "feof"]}, {"location": "course-c/15-fundations/stdio/#saisie-de-chaine-de-caracteres", "title": "Saisie de cha\u00eene de caract\u00e8res", "text": "

    Lors d'une saisie de cha\u00eene de caract\u00e8res, il est n\u00e9cessaire de toujours indiquer une taille maximum de cha\u00eene comme %20s qui limite la capture \u00e0 20 caract\u00e8res, soit une cha\u00eene de 21 caract\u00e8res avec son \\0. Sinon, il y a risque de fuite m\u00e9moire :

    int main(void) {\n    char a[6];\n    char b[10] = \"R\u00e2teau\";\n\n    char str[] = \"jardinage\";\n    sscanf(str, \"%s\", a);\n\n    printf(\"a. %s\\nb. %s\\n\", a, b);\n}\n
    $ ./a.out\na. jardinage\nb. age\n

    Ici la variable b contient age alors qu'elle devrait contenir r\u00e2teau. La raison est que le mot captur\u00e9 jardinage est trop long pour la variable a qui n'est dispos\u00e9e \u00e0 stocker que 5 caract\u00e8res imprimables. Il y a donc d\u00e9passement de m\u00e9moire et comme vous le constatez, le compilateur ne g\u00e9n\u00e8re aucune erreur. La bonne m\u00e9thode est donc de prot\u00e9ger la saisie ici avec %5s.

    En m\u00e9moire, ces deux variables sont adjacentes et naturellement a[7] est \u00e9quivalente \u00e0 dire la septi\u00e8me case m\u00e9moire \u00e0 partir du d\u00e9but de ``a``.

         a[6]              b[10]\n\u251e\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u251e\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502\u2502R\u2502\u00e2\u2502t\u2502e\u2502a\u2502u\u2502 \u2502 \u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n
    ", "tags": ["age", "jardinage", "r\u00e2teau"]}, {"location": "course-c/15-fundations/stdio/#saisie-arbitraire", "title": "Saisie arbitraire", "text": "

    Comme bri\u00e8vement \u00e9voqu\u00e9 plus haut, il est possible d'utiliser le marqueur [ pour capturer une s\u00e9quence de caract\u00e8res. Imaginons que je souhaite capturer un nombre en tetrasexagesimal (base 64). Je peux \u00e9crire\u2009:

    char input[] = \"Q2hvY29sYXQ\";\nchar output[128];\nsscanf(input, \"%127[0-9A-Za-z+/]\", &output);\n

    Dans cet exemple je capture les nombres de 0 \u00e0 9 0-9 (10), les caract\u00e8res majuscules et minuscules A-Za-z (52), ainsi que les caract\u00e8res +, / (2), soit 64 caract\u00e8res. Le buffer d'entr\u00e9e \u00e9tant fix\u00e9 \u00e0 128 positions, la saisie est contrainte \u00e0 127 caract\u00e8res imprimables.

    Exercice 5\u2009: Bugs

    Parmi les instructions ci-dessous, indiquez celles qui sont correctes et celle qui comporte des erreurs. Pour celles comportant des erreurs, d\u00e9taillez la nature des anomalies.

    short i;\nlong j;\nunsigned short u;\nfloat x;\ndouble y;\nprintf(i);\nscanf(&i);\nprintf(\"%d\", &i);\nscanf(\"%d\", &i);\nprintf(\"%d%ld\", i, j, u);\nscanf(\"%d%ld\", &i, j);\nprintf(\"%u\", &u);\nscanf(\"%d\", &u);\nprintf(\"%f\", x);\nscanf(\"%f\", &x);\nprintf(\"%f\", y);\nscanf(\"%f\", &y);\n
    Solution
    // Incorrect ! Le premier param\u00e8tre de printf doit \u00eatre la cha\u00eene de format.\nprintf(i);\n\n// Incorrect ! Le premier param\u00e8tre de scanf doit \u00eatre la cha\u00eene de format.\nscanf(&i);\n\n// Correct, mais surprenant.\n// Cette instruction affichera l\u2019adresse de I, et non pas sa valeur !\nprintf(\"%d\", &i);\n\n// Incorrect. Le param\u00e8tre i est de type short, alors que la cha\u00eene de\n// format sp\u00e9cifie un type int. Fonctionnera sur les machines dont le type\n// short et int sont identiques\nscanf(\"%d\", &i);\n\n// Incorrect, la troisi\u00e8me variable pass\u00e9e en param\u00e8tre ne sera pas affich\u00e9e.\nprintf(\"%d%ld\", i, j, u);\n\n// Incorrect ! Le premier param\u00e8tre est de type short alors que int\n// est sp\u00e9cifi\u00e9 dans la cha\u00eene de format.\n// Le deuxi\u00e8me param\u00e8tre n\u2019est pas pass\u00e9 par adresse, ce qui va\n// probablement causer une erreur fatale.\nscanf(\"%d%ld\", &i, j);\n\n// Correct, mais \u00e9tonnant. Affiche l\u2019adresse de la variable u.\nprintf(\"%u\", &u);\n\n// Incorrect ! Le param\u00e8tre est de type unsigned short, alors que\n// la cha\u00eene de format sp\u00e9cifie int. Fonctionnera pour les valeurs\n// positives sur les machines dont le type short et int sont identiques.\n// Pour les valeurs n\u00e9gatives, le r\u00e9sultat sera l\u2019interpr\u00e9tation non\n// sign\u00e9e de la valeur en compl\u00e9ment \u00e0 2.\nscanf(\"%d\", &u);\n\n// Correct, mais x est trait\u00e9 comme double.\nprintf(\"%f\", x);\n\n// Correct.\nscanf(\"%f\", &x);\n\n// Correct ! %f est trait\u00e9 comme double par printf !\nprintf(\"%f\", y);\n\n// Incorrect ! La cha\u00eene de format sp\u00e9cifie float,\n// le param\u00e8tre pass\u00e9 est l\u2019adresse d\u2019une variable de type double.\nscanf(\"%f\", &y);\n

    Exercice 6\u2009: Test de saisir correcte

    \u00c9crivez un programme d\u00e9clarant des variables r\u00e9elles x, y et z, permettant de saisir leur valeur en une seule instruction, et v\u00e9rifiant que les 3 valeurs ont bien \u00e9t\u00e9 assign\u00e9es. Dans le cas contraire, afficher un message du type \u00ab\u2009donn\u00e9es invalides\u2009\u00bb.

    Solution
    int n;\nfloat x, y, z;\nprintf(\"Donnez les valeurs de x, y et z :\");\nn = scanf(\"%f%f%f\", &x, &y, &z);\nif (n != 3)\nprintf(\"Erreur de saisie.\\n\");\n

    Exercice 7\u2009: Produit scalaire

    \u00c9crire un programme effectuant les op\u00e9rations suivantes\u2009:

    • Saisir les coordonn\u00e9es r\u00e9elles x1 et y1 d\u2019un vecteur v1.
    • Saisir les coordonn\u00e9es r\u00e9elles x2 et y2 d\u2019un vecteur v2.
    • Calculer le produit scalaire. Afficher un message indiquant si les vecteurs sont orthogonaux ou non.
    Solution
    #include <stdio.h>\n#include <stdlib.h>\n\nint main(void)\n{\n    float x1, y1\n    printf(\"Coordonn\u00e9es du vecteur v1 s\u00e9par\u00e9es par un \\\";\\\" :\\n\");\n    scanf(\"%f ;%f\", &x1, &y1);\n\n    float x2, y2;\n    printf(\"Coordonn\u00e9es du vecteur v2 s\u00e9par\u00e9es par un \\\";\\\" :\\n\");\n    scanf(\"%f ;%f\", &x2, &y2);\n\n    float dot_product = x1 * x2 + y1 * y2;\n    printf(\"Produit scalaire : %f\\n\", dot_product);\n    if (dot_product == 0.0)\n        printf(\"Les vecteurs sont orthogonaux.\\n\");\n}\n

    Ce programme risque de ne pas bien d\u00e9tecter l\u2019orthogonalit\u00e9 de certains vecteurs, car le test d\u2019\u00e9galit\u00e9 \u00e0 0 avec les virgules flottantes pourrait mal fonctionner. En effet, pour deux vecteurs orthogonaux, les erreurs de calcul en virgule flottante pourraient amener \u00e0 un produit scalaire calcul\u00e9 tr\u00e8s proche, mais cependant diff\u00e9rent de z\u00e9ro. On peut corriger ce probl\u00e8me en modifiant le test pour v\u00e9rifier si le produit scalaire est tr\u00e8s petit, par exemple compris entre -0.000001 et +0.000001:

    if (dot_product >= -1E-6 && dot_product <= 1E-6)\n

    Ce qui peut encore s\u2019\u00e9crire en utilisant la fonction valeur absolue\u2009:

    if (fabs(dot_product) <= 1E-6)\n

    Exercice 8\u2009: Crampes de doigts

    Votre coll\u00e8gue n'a pas cess\u00e9 de se plaindre de crampes... aux doigts... Il a \u00e9crit le programme suivant avant de prendre cong\u00e9 pour se rendre chez son m\u00e9decin.

    Gr\u00e2ce \u00e0 votre esprit affut\u00e9 et votre \u0153il per\u00e7ant, vous identifiez 13 erreurs. Lesquelles sont-elles\u2009?

    #include <std_io.h>\n#jnclude <stdlib.h>\nINT Main()\n{\nint a, sum;\nprintf(\"Addition de 2 entiers a et b.\\n\");\n\nprintf(\"a: \")\nscanf(\"%d\", a);\n\nprintf(\"b: \");\nscanf(\"%d\", &b);\n\n/* Affichage du r\u00e9sultat\nsomme = a - b;\nPrintf(\"%d + %d = %d\\n\", a, b, sum);\n\nretturn EXIT_FAILURE;\n}\n}\n
    Solution

    Une fois la correction effectu\u00e9e, vous utilisez l'outil de diff pour montrer les diff\u00e9rences\u2009:

    1,3c1,3\n<         #include <stdio.h>\n<         #include <stdlib.h>\n<         int main()\n---\n>         #include <std_io.h>\n>         #jnclude <stdlib.h>\n>         INT Main()\n5c5\n<         int a, b, sum;\n---\n>         int a, sum;\n9c9\n<         scanf(\"%d\", &a);\n---\n>         scanf(\"%d\", a);\n14,16c14,16\n<         /* Affichage du r\u00e9sultat */\n<         sum = a + b;\n<         printf(\"%d + %d = %d\\n\", a, b, sum);\n---\n>         /* Affichage du r\u00e9sultat\n>         somme = a - b;\n>         Printf(\"%d + %d = %d\\n\", a, b, sum);\n18c18,19\n<         return EXIT_SUCCESS;\n---\n>         return EXIT_FAILURE;\n>         }\n

    Exercice 9\u2009: G\u00e9om\u00e9trie affine

    Consid\u00e9rez le programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main(void)\n{\n    float a;\n    printf(\"a = \");\n    scanf(\"%f\", &a);\n\n    float b;\n    printf(\"b = \");\n    scanf(\"%f\", &b);\n\n    float x;\n    printf(\"x = \");\n    scanf(\"%f\", &x);\n\n    float y = a * x + b;\n\n    printf(\"y = %f\\n\", y);\n}\n
    1. \u00c0 quelle ligne commence l'ex\u00e9cution de ce programme\u2009?
    2. Dans quel ordre s'ex\u00e9cutent les instructions\u2009?
    3. D\u00e9crivez ce que fait ce programme \u00e9tape par \u00e9tape
    4. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?
    5. Quelle est l'utilit\u00e9 de ce programme\u2009?
    Solution
    1. Ligne 6
    2. C est un langage imp\u00e9ratif, l'ordre est s\u00e9quentiel du haut vers le bas
    3. Les \u00e9tapes sont les suivantes\u2009:

      1. Demande de la valeur de a \u00e0 l'utilisateur
      2. Demande de la valeur de b \u00e0 l'utilisateur
      3. Demande de la valeur de x \u00e0 l'utilisateur
      4. Calcul de l'image affine de x (\u00e9quation de droite)
      5. Affichage du r\u00e9sultat
    4. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?

      • Il verra y = 12 pour a = 2; x = 5; b = 2
    5. Quelle est l'utilit\u00e9 de ce programme\u2009?

      • Le calcul d'un point d'une droite

    Exercice 10\u2009: \u00c9quation de droite

    L'exercice pr\u00e9c\u00e9dent souffre de nombreux d\u00e9fauts. Sauriez-vous les identifier et perfectionner l'impl\u00e9mentation de ce programme\u2009?

    Solution

    Citons les d\u00e9fauts de ce programme\u2009:

    • Le programme ne peut pas \u00eatre utilis\u00e9 avec les arguments, uniquement en mode interactif
    • Les invit\u00e9s de dialogue a =, b = ne sont pas clair, a et b sont associ\u00e9s \u00e0 quoi\u2009?
    • La valeur de retour n'est pas exploitable directement.
    • Le nom des variables utilis\u00e9 n'est pas clair.
    • Aucune valeur par d\u00e9faut.

    Une solution possible serait\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main(int argc, char* argv[])\n{\n    float x;\n    float offset;\n    float slope;\n\n    if (argc > 2) {\n        offset = atof(argv[1]);\n        slope = atof(argv[2]);\n    } else {\n        float offset_default = 0.;\n        printf(\"Offset? [%f]: \", offset_default);\n        if (!scanf(\"%f\", &offset)) {\n            offset = offset_default;\n        }\n\n        float slope_default = 1.;\n        printf(\"Pente? [%f]: \", slope_default);\n        if (!scanf(\"%f\", &slope)) {\n            slope = slope_default;\n        }\n    }\n\n    if (argc == 2 || argc > 3) {\n        slope = atof(argv[argc == 2 ? 2: 3]);\n    } else {\n        float x_default = 0;\n        printf(\"x (abscisse) [%f]:\", x_default);\n        if (!scanf(\"%f\", &x)) {\n            x = x_default;\n        }\n    }\n\n    printf(\"%f\\n\", slope * x + offset);\n\n    return 0;\n}\n

    Exercice 11\u2009: Loi d'Ohm

    \u00c9crivez un programme demandant deux r\u00e9els tension et r\u00e9sistance, et affichez ensuite le courant. Pr\u00e9voir un test pour le cas o\u00f9 la r\u00e9sistance serait nulle.

    Exercice 12\u2009: Tour Eiffel

    Consid\u00e9rons le programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <math.h>\n\nint main()\n{\n    printf(\"Quel angle mesurez-vous en visant le sommet du b\u00e2timent (en degr\u00e9s): \");\n    float angle_degre;\n    scanf(\"%f\", &angle_degrees);\n    float angle_radian = angle_degrees * M_PI / 45.;\n\n    printf(\"\u00c0 quelle distance vous trouvez vous du b\u00e2timent (en m\u00e8tres): \");\n    float distance;\n    scanf(\"%f\", &distance);\n\n    float height = distance / tan(angle_radian);\n    printf(\"La hauteur du b\u00e2timent est : %g m\u00e8tres.\\n\", height);\n}\n
    1. Que fait le programme \u00e9tape par \u00e9tape\u2009?
    2. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?
    3. \u00c0 quoi sert ce programme\u2009?
    4. Euh, mais\u2009? Ce programme comporte des erreurs, lesquelles\u2009?
    5. Impl\u00e9mentez-le et testez-le.

    Exercice 13\u2009: Hyperloop

    Hyperloop (aussi orthographi\u00e9 Hyperl\u221ep) est un projet ambitieux d'Elon Musk visant \u00e0 construire un moyen de transport ultra rapide utilisant des capsules voyageant dans un tube sous vide. Ce projet est analogue \u00e0 celui \u00e9tudi\u00e9 en suisse et nomm\u00e9 Swissmetro, mais abandonn\u00e9 en 2009.

    N\u00e9anmoins, les ing\u00e9nieurs suisses avaient \u00e0 l'\u00e9poque \u00e9crit un programme pour calculer, compte tenu d'une vitesse donn\u00e9e, le temps de parcours entre deux villes de Suisse.

    \u00c9crire un programme pour calculer la distance entre deux villes de suisse parmi lesquelles propos\u00e9es sont\u2009:

    • Gen\u00e8ve
    • Z\u00fcrich
    • B\u00e2le
    • Bern
    • St-Galle

    Consid\u00e9rez une acc\u00e9l\u00e9ration de 0.5 g pour le calcul de mouvement, et une vitesse maximale de 1220 km/h.

    ", "tags": ["diff", "tension", "r\u00e9sistance", "courant"]}, {"location": "course-c/15-fundations/stdio/#portabilite-des-formats", "title": "Portabilit\u00e9 des formats", "text": "

    Les formats de scanf et printf sont d\u00e9pendants de la plateforme. Par exemple, %d est un entier sign\u00e9, %u un entier non sign\u00e9, %ld est un entier long sign\u00e9. N\u00e9anmoins ces formats ne sont pas portables, car selon le mod\u00e8le de donn\u00e9es de la machine, un entier long peut \u00eatre de 32 bits ou de 64 bits.

    Cela n'a pas une grande importance si vous utilisez les types standards (comme int, long, short, char), mais si vous utilisez des types sp\u00e9cifiques comme int32_t, int64_t, uint32_t, uint64_t, vous devez utiliser les formats sp\u00e9cifiques de la biblioth\u00e8que inttypes.h. Voici la table de correspondance des formats\u2009:

    Formats portables Type Format int8_t PRId8 int16_t PRId16 int32_t PRId32 int64_t PRId64 uint8_t PRIu8 uint16_t PRIu16 uint32_t PRIu32 uint64_t PRIu64

    On peut ajouter des options \u00e0 ces formats, par exemple pour afficher un entier en hexad\u00e9cimal, on utilise %PRIx32 pour un entier 32 bits. Pour la valeur en octal, on utilise %PRIo32.

    Options des formats portables Option Description x Hexad\u00e9cimal o Octal u Non sign\u00e9 d Sign\u00e9

    L'utilisation est particuli\u00e8re car il faut utiliser la macro PRI pour d\u00e9finir le format. Par exemple, pour afficher un entier 32 bits en hexad\u00e9cimal, on utilise\u2009:

    #include <inttypes.h>\n#include <stdio.h>\n\nint main(void)\n{\n    int32_t i = 0x12345678;\n    printf(\"i = %\" PRIx32 \"\\n\", i);\n}\n

    Exercice 14\u2009: Constantes litt\u00e9rales caract\u00e9rielles

    Indiquez si les constantes litt\u00e9rales suivantes sont valides ou invalides.

    1. 'a'
    2. 'A'
    3. 'ab'
    4. '\\x41'
    5. '\\041'
    6. '\\0x41'
    7. '\\n'
    8. '\\w'
    9. '\\t'
    10. '\\xp2'
    11. \"abcdef\"
    12. \"\\abc\\ndef\"
    13. \"\\'\\\"\\\\\"
    14. \"hello \\world!\\n\"

    Exercice 15\u2009: Cha\u00eenes de formatage

    Pour les instructions ci-dessous, indiquer quel est l'affichage obtenu.

    char a = 'a';\nshort sh1 = 5;\nfloat f1 = 7.0f;\nint i1 = 7, i2 = 'a';\n
    1. printf(\"Next char: %c.\\n\", a + 1);
    2. printf(\"Char: %3c.\\n\", a);
    3. printf(\"Char: %-3c.\\n\", a);
    4. printf(\"Chars: \\n-%c.\\n-%c.\\n\", a, 'z' - 1);
    5. printf(\"Sum: %i\\n\", i1 + i2 - a);
    6. printf(\"Taux d\u2019erreur\\t%i %%\\n\", i1);
    7. printf(\"Quel charabia horrible:\\\\\\a\\a\\a%g\\b\\a%%\\a\\\\\\n\", f1);
    8. printf(\"Inventaire: %i4 pieces\\n\", i1);
    9. printf(\"Inventory: %i %s\\n\", i1, \"pieces\");
    10. printf(\"Inventaire: %4i pieces\\n\", i1);
    11. printf(\"Inventaire: %-4i pieces\\n\", i1);
    12. printf(\"Mixed sum: %f\\n\", sh1 + i1 + f1);
    13. printf(\"Tension: %5.2f mV\\n\", f1);
    14. printf(\"Tension: %5.2e mV\\n\", f1);
    15. printf(\"Code: %X\\n\", 12);
    16. printf(\"Code: %x\\n\", 12);
    17. printf(\"Code: %o\\n\", 12);
    18. printf(\"Value: %i\\n\", -1);
    19. printf(\"Value: %hi\\n\", 65535u);
    20. printf(\"Value: %hu\\n\", -1);
    ", "tags": ["int", "inttypes.h", "scanf", "int64_t", "short", "PRI", "int32_t", "printf", "uint32_t", "long", "uint64_t", "char"]}, {"location": "course-c/15-fundations/syntax/", "title": "Syntaxe", "text": "Tout devrait \u00eatre rendu aussi simple que possible, mais pas plus simple.Albert Einstein

    Ce chapitre traite des \u00e9l\u00e9ments constitutifs et fondamentaux du langage C. Il traite des g\u00e9n\u00e9ralit\u00e9s propres au langage, mais aussi des notions \u00e9l\u00e9mentaires permettant d'interpr\u00e9ter du code source. Notons que ce chapitre est transversal, \u00e0 la sa premi\u00e8re lecture, le profane ne pourra tout comprendre sans savoir lu et ma\u00eetris\u00e9 les chapitres suivants, n\u00e9anmoins il retrouvera ici les aspects fondamentaux du langage.

    "}, {"location": "course-c/15-fundations/syntax/#lalphabet", "title": "L'alphabet", "text": "

    Heureusement pour nous occidentaux, l'alphabet de C est compos\u00e9 de 52 caract\u00e8res latins et de 10 chiffres indo-arabes :

    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z\na b c d e f g h i j k l m n o p q r s t u v w x y z\n0 1 2 3 4 5 6 7 8 9\n

    Pour comparaison, le syst\u00e8me d'\u00e9criture cor\u00e9en (Hangul) est alphasyllabique, c'est-\u00e0-dire que chaque caract\u00e8re repr\u00e9sente une syllabe. Les lettres de base sont compos\u00e9es de 14 consonnes de base et 10 voyelles. Quant aux chiffres, ils sont les m\u00eames qu'en occident.

    g n d r/l m b s ng j ch k t p h\n\u3131 \u3134 \u3137 \u3139 \u3141 \u3142 \u3145 \u3147 \u3148 \u314a \u314b \u314c \u314d \u314e\n\na ya eo yeo o yo u yu eu i\n\u314f \u3151 \u3153 \u3155 \u3157 \u315b \u315c \u3160 \u3161 \u3163\n

    Les Japonais quant \u00e0 eux utilisent trois syst\u00e8mes d'\u00e9criture, le Hiragana, le Katakana et le Kanji. Les deux premiers sont des syllabaires et le dernier est un syst\u00e8me d'\u00e9criture logographique. Le Hiragana et Katakana ont tous deux 46 caract\u00e8res de base. Voici l'exemple du Katakana\u2009:

    \u3042 (a),  \u3044 (i),   \u3046 (u),   \u3048 (e),  \u304a (o)\n\u304b (ka), \u304d (ki),  \u304f (ku),  \u3051 (ke), \u3053 (ko)\n\u3055 (sa), \u3057 (shi), \u3059 (su),  \u305b (se), \u305d (so)\n\u305f (ta), \u3061 (chi), \u3064 (tsu), \u3066 (te), \u3068 (to)\n\u306a (na), \u306b (ni),  \u306c (nu),  \u306d (ne), \u306e (no)\n\u306f (ha), \u3072 (hi),  \u3075 (fu),  \u3078 (he), \u307b (ho)\n\u307e (ma), \u307f (mi),  \u3080 (mu),  \u3081 (me), \u3082 (mo)\n\u3084 (ya), \u3086 (yu),  \u3088 (yo)\n\u3089 (ra), \u308a (ri),  \u308b (ru),  \u308c (re), \u308d (ro)\n\u308f (wa), \u3092 (wo)\n\u3093 (n)\n

    On notera ici que l'alphabet latin est tout particuli\u00e8rement adapt\u00e9 \u00e0 la programmation, car, d'une part ce fut le premier alphabet \u00e0 \u00eatre utilis\u00e9 pour l'\u00e9criture de programmes informatiques et d'autre part, il reste assez simple pour \u00eatre utilis\u00e9 par des machines. On peut noter en outre que les pays qui utilisent leur propre alphabet doivent imp\u00e9rativement apprendre et ma\u00eetriser l'alphabet latin pour pouvoir programmer. Ceci implique qu'ils doivent \u00e9galement disposer d'un clavier latin pour pouvoir saisir leur code. Ayez \u00e0 l'esprit que nous sommes des privil\u00e9gi\u00e9s de ne pas devoir jongler avec plusieurs alphabets pour \u00e9crire du code...

    Outre ces caract\u00e8res, la s\u00e9paration des symboles est assur\u00e9e par une espace, une tabulation horizontale, une tabulation verticale, et un caract\u00e8re de retour \u00e0 la ligne. Ces caract\u00e8res ne sont pas imprimables, c'est-\u00e0-dire qu'ils ne sont pas directement visibles ni \u00e0 l'\u00e9cran ni \u00e0 l'impression (ni sur le papier d'ailleurs). Microsoft Word et d'autres \u00e9diteurs utilisent g\u00e9n\u00e9ralement le pied-de-mouche \u00b6 00B6 pour indiquer les fins de paragraphes qui sont \u00e9galement des caract\u00e8res non imprimables.

    La convention est de nommer les caract\u00e8res non imprimables soit par leur acronyme LF pour Line Feed ou soit par leur convention C \u00e9chapp\u00e9e par un backslash \\n:

    Caract\u00e8res non imprimables Acronyme \u00c9chapp\u00e9 Description LF \\n Retour \u00e0 la ligne VT \\v Tabulation verticale FF \\f Nouvelle page TAB \\t Tabulation horizontale CR \\r Retour charriot SPACE \\040 Espace NUL \\0 Caract\u00e8re nul

    Voici en pratique comment ces caract\u00e8res peuvent \u00eatre utilis\u00e9s\u2009:

    Distinction de diff\u00e9rents caract\u00e8res non imprimables

    La ponctuation utilise les 29 symboles graphiques suivants\u2009:

    ! # % ^ & * ( _ ) - + = ~ [ ] ' | \\ ; : \" { } , . < > / ?\n
    ", "tags": ["katakana", "hangul", "ecriture-logographique", "kanji", "alphasyllabique", "hiragana", "tabulation"]}, {"location": "course-c/15-fundations/syntax/#la-machine-a-ecrire", "title": "La machine \u00e0 \u00e9crire", "text": "

    Peut-\u00eatre avez-vous d\u00e9j\u00e0 \u00e9t\u00e9 confront\u00e9 \u00e0 une machine \u00e0 \u00e9crire m\u00e9canique. Elles disposent d'un levier sur la gauche du chariot qui dispose de deux fonctionnalit\u00e9s. D'une part il permet de faire revenir le chariot au d\u00e9but de la ligne, mais \u00e9galement de faire avancer le papier d'une ligne par une rotation du cylindre. C'est ce levier de retour chariot qui a donn\u00e9 son nom au caract\u00e8re de retour \u00e0 la ligne CR pour Carriage Return. Quant au caract\u00e8re de nouvelle ligne LF pour Line Feed, il est associ\u00e9 \u00e0 la rotation du rouleau qui entra\u00eene la feuille de papier.

    Machine \u00e0 \u00e9crire Herm\u00e8s 3000 h\u00e9bra\u00efque

    Historiquement il y a donc bien une distinction entre ces deux caract\u00e8res, mais aujourd'hui, cela n'a plus vraiment de sens. Un autre point que l'on peut relever est que pour souligner un texte, on utilisait le caract\u00e8re de soulignement (tiret bas, ou underscore) _ pour mettre en emphase du texte d\u00e9j\u00e0 \u00e9crit. De m\u00eame pour barrer un texte, on utilisait le caract\u00e8re - pour faire reculer le chariot d'une demi-case et ensuite frapper le m\u00eame caract\u00e8re. Enfin, pour ajouter un accent circonflexe, il fallait utiliser la touche ^ pour faire reculer le chariot d'une demi-case et ensuite frapper la lettre \u00e0 accentuer.

    Ces subtilit\u00e9s de la machine \u00e0 \u00e9crire ont \u00e9t\u00e9 partiellement reprises dans le format Unicode. Aussi pour \u00e9crire un accent aigu, il y a aujourd'hui plusieurs fa\u00e7ons de le faire.

    1. Utiliser le caract\u00e8re e 0065 suivi du caract\u00e8re \u00b4 0301 aussi appel\u00e9 combining acute accent pour obtenir \u00e9.
    2. Utiliser le caract\u00e8re \u00e9 00E9 directement.

    Ces h\u00e9ritages historiques font qu'il est difficile aujourd'hui de traiter sans bogue les textes multilingues. Les cas particuliers sont nombreux et bien souvent, les informaticiens utilisent des biblioth\u00e8ques logicielles pour g\u00e9rer ces cas particuliers.

    Un fait historique int\u00e9ressant est que les premiers ordinateurs ne disposaient pas d'un clavier ayant tous ces symboles et la commission responsable de standardiser C a int\u00e9gr\u00e9 au standard les trigraphes et plus tard les digraphes qui sont des combinaisons de caract\u00e8res de base qui remplacent les caract\u00e8res impossibles \u00e0 saisir directement. Ainsi <: est le digraphe de [ et ??< est le trigraphe de {. N\u00e9anmoins vous conviendrez cher lecteur que ces alternatives ne devraient \u00eatre utilis\u00e9es que dans des cas extr\u00eames et justifiables. Par ailleurs, le standard C et C++ songent \u00e0 les retirer.

    Retenez que C peut \u00eatre un langage extr\u00eamement cryptique tant il est permissif sur sa syntaxe. Il existe d'ailleurs un concours international d'obfuscation, le The International Obfuscated C Code Contest qui prime des codes les plus subtils et illisibles comme le code suivant \u00e9crit par Chris Mills. Il s'agit d'ailleurs d'un exemple qui compile parfaitement sur la plupart des compilateurs.

        int I=256,l,c, o,O=3; void e(\nint L){ o=0; for( l=8; L>>++l&&\n16>l;                           o+=l\n<<l-                            1) ;\no+=l                     *L-(l<<l-1); { ; }\nif (                    pread(3,&L,3,O+o/8)<\n2)/*                    */exit(0);  L>>=7&o;\nL%=1                     <<l; L>>8?256-L?e(\nL-1)                            ,c||\n(e(c                            =L),\nc=0)                            :( O\n+=(-I&7)*l+o+l>>3,I=L):putchar(\n    L); }int main(int l,char**o){\n                for(\n            /*          ////      */\n            open(1[o],0); ; e(I++\n            ))                    ;}\n
    Exemple grivois

    Ce code \u00e9galement issu du IOCCC est un exemple pas tr\u00e8s gracieux de ce qu'il est possible de faire en C. Il est \u00e0 noter que ce code compile parfaitement et affiche un message pas tr\u00e8s Catholique.

    main(i){for(i=160;i--;putchar(i%32?\"\u0153\u2122c\u00e6RJ\"\"\\\\J\u2022\u00e4RJ\"\"\u0153]d\u00e4\"[i/8]&1<<i%8?42:32:10));}\n
    ", "tags": ["tiret-bas"]}, {"location": "course-c/15-fundations/syntax/#fin-de-lignes-eol", "title": "Fin de lignes (EOL)", "text": "

    \u00c0 l'instar des premi\u00e8res machines \u00e0 \u00e9crire, les t\u00e9l\u00e9scripteurs poss\u00e9daient de nombreux caract\u00e8res de d\u00e9placement qui sont depuis tomb\u00e9s en d\u00e9su\u00e9tude et pr\u00eatent aujourd'hui \u00e0 confusion m\u00eame pour le plus aguerri des programmeurs. Maintenant que les ordinateurs poss\u00e8dent des \u00e9crans, la notion originale du terme retour chariot est compromise et comme il y a autant d'avis que d'ing\u00e9nieurs, les premiers PC IBM compatibles ont choisi qu'une nouvelle ligne d\u00fbt toujours se composer de deux caract\u00e8res\u2009: un retour chariot (CR) et une nouvelle ligne (LF) ou en C \\r\\n. Les premiers Macintosh d'Apple jugeant inutile de gaspiller deux caract\u00e8res pour chaque nouvelle ligne dans un fichier et ont d\u00e9cid\u00e9 d'associer le retour chariot et la nouvelle ligne dans le caract\u00e8re \\r. Enfin, les ordinateurs UNIX ont eu le m\u00eame raisonnement, mais ils ont choisi de ne garder que \\n.

    Heureusement, depuis qu'Apple a migr\u00e9 son syst\u00e8me sur une base BSD (UNIX) en 2001, les syst\u00e8mes d'exploitation modernes ont adopt\u00e9 le standard UNIX et il n'y a plus de probl\u00e8me de compatibilit\u00e9 entre les syst\u00e8mes. En sommes, il existe aujourd'hui deux types de fin de ligne\u2009:

    • LF ou \\n sur tous les ordinateurs de la plan\u00e8te \u00e0 l'exception de Windows,
    • CRLF ou \\r\\n sur les ordinateurs Windows.

    Il n'y a pas de consensus \u00e9tabli sur lesquels des deux types de fin de ligne (EOL: End Of Line) il faut utiliser, faite preuve de bon sens et surtout, soyez coh\u00e9rent.

    ", "tags": ["CRLF", "EOL"]}, {"location": "course-c/15-fundations/syntax/#mots-cles", "title": "Mots cl\u00e9s", "text": "

    Le langage de programmation C tel que d\u00e9fini par C11 comporte environ 37 mots cl\u00e9s\u2009:

    _Bool       do        int       switch\n_Complex    double    long      typedef\n_Imaginary  else      register  union\nauto        enum      restrict  unsigned\nbreak       extern    return    void\ncase        float     short     volatile\nchar        for       signed    while\nconst       goto      sizeof\ncontinue    if        static\ndefault     inline    struct\n

    Dans ce cours, l'usage des mots cl\u00e9s suivants est d\u00e9courag\u00e9, car leur utilisation pourrait pr\u00eater \u00e0 confusion ou mener \u00e0 des in\u00e9l\u00e9gances d'\u00e9criture.

    _Bool, _imaginary, auto, goto, inline, long, register, restrict, short\n

    Il n'y a donc plus que 28 mots cl\u00e9s \u00e0 conna\u00eetre pour \u00eatre un bon d\u00e9veloppeur C.

    Notons que les mots cl\u00e9s true et false ne sont pas standardis\u00e9s en C, mais ils le sont en C++.

    Ces mots cl\u00e9s font partie int\u00e9grante de la grammaire du langage et ne peuvent \u00eatre utilis\u00e9s pour identifier des variables, des fonctions ou des \u00e9tiquettes.

    Nombre de mots cl\u00e9s

    On peut se demander s'il est pr\u00e9f\u00e9rable pour un langage d'avoir plus ou moins de mots cl\u00e9s. En effet, plus il y a de mots cl\u00e9s, plus il est difficile d'apprendre le langage, mais plus il y a de mots cl\u00e9s, plus il est facile de comprendre le code des autres.

    C'est le m\u00eame dilemme entre les architectures processeur RISC et CISC. Les architectures RISC ont moins d'instructions, mais elles sont plus complexes \u00e0 utiliser, tandis que les architectures CISC ont plus d'instructions, mais elles sont plus simples \u00e0 utiliser.

    Le Perl par exemple n'a environ que 20 mots cl\u00e9s, mais il est r\u00e9put\u00e9 pour \u00eatre un langage difficile \u00e0 apprendre. Le C++ dans sa version 2020 en a plus de 84.

    ", "tags": ["false", "true"]}, {"location": "course-c/15-fundations/syntax/#identificateurs", "title": "Identificateurs", "text": "

    Un identificateur est une s\u00e9quence de caract\u00e8res repr\u00e9sentant une entit\u00e9 du programme et \u00e0 laquelle il est possible de se r\u00e9f\u00e9rer. Un identificateur est d\u00e9fini par une grammaire r\u00e9guli\u00e8re qui peut \u00eatre exprim\u00e9e comme suit\u2009:

    Grammaire d'un identificateur C

    La notation /[a-z]/ signifie que l'on peut utiliser n'importe quelle lettre minuscule de l'alphabet latin, /[A-Z]/ pour les lettres majuscules, /[0-9]/ pour les chiffres et _ pour le caract\u00e8re soulign\u00e9.

    En addition de cette grammaire, voici quelques r\u00e8gles\u2009:

    1. Un identificateur ne peut pas \u00eatre l'un des mots cl\u00e9s du langage.
    2. Les identificateurs sont sensibles \u00e0 la casse (majuscule/minuscule).
    3. Le standard C99, se r\u00e9serve l'usage de tous les identificateurs d\u00e9butant par _ suivi d'une lettre majuscule ou un autre underscore _.
    4. Le standard POSIX, se r\u00e9serve l'usage de tous les identificateurs finissant par _t.

    Expression r\u00e9guli\u00e8re

    Il est possible d'exprimer la syntaxe d'un identificateur \u00e0 l'aide de l'expression r\u00e9guli\u00e8re suivante\u2009:

    /^[a-zA-Z_][a-zA-Z0-9_]*$/

    Exercice 1\u2009: Validit\u00e9 des identificateurs

    Pour chacune des suites de caract\u00e8res ci-dessous, indiquez s'il s'agit d'un identificateur valide et utilisable en C. Justifier votre r\u00e9ponse.

    • [ ] 2_pi
    • [x] x_2
    • [x] x___3
    • [ ] x 2
    • [x] positionRobot
    • [x] piece_presente
    • [x] _commande_vanne
    • [ ] -courant_sortie
    • [x] _alarme_
    • [ ] panne#2
    • [ ] int
    • [ ] d\u00e9faillance
    • [ ] f'
    • [x] INT
    Solution

    Une excellente approche serait d'utiliser directement l'expression r\u00e9guli\u00e8re fournie et d'utiliser l'outil en ligne regex101.com.

    1. 2_pi invalide, car commence par un chiffre
    2. x_2 valide
    3. x___3 valide
    4. x 2 invalide, car comporte un espace
    5. positionRobot valide, notation camelCase
    6. piece_presente valide, notation snake_case
    7. _commande_vanne valide
    8. -courant_sortie invalide, un identificateur ne peut pas commencer par le signe -
    9. _alarme_ valide
    10. panne#2 invalide, le caract\u00e8re # n'est pas autoris\u00e9
    11. int invalide, int est un mot r\u00e9serv\u00e9 du langage
    12. d\u00e9faillance invalide, uniquement les caract\u00e8res imprimable ASCII sont autoris\u00e9s
    13. f' invalide l'apostrophe n'est pas autoris\u00e9e
    14. INT valide
    ", "tags": ["positionRobot", "piece_presente", "_commande_vanne", "int", "x___3", "INT", "x_2", "_alarme_", "d\u00e9faillance"]}, {"location": "course-c/15-fundations/syntax/#variables", "title": "Variables", "text": "

    Une variable est un symbole qui associe un nom (identificateur) \u00e0 une valeur. Comme son nom l'indique, une variable peut voir son contenu varier au cours du temps.

    Une variable est d\u00e9finie par\u2009:

    • Son nom (name), c'est-\u00e0-dire l'identificateur associ\u00e9 au symbole.
    • Son type (type), qui est la convention d'interpr\u00e9tation du contenu binaire en m\u00e9moire.
    • Sa valeur (value), qui est le contenu interpr\u00e9t\u00e9 connaissant son type.
    • Son adresse (address) qui est l'emplacement m\u00e9moire ou la repr\u00e9sentation binaire sera enregistr\u00e9e.
    • Sa port\u00e9e (scope) qui est la portion de code ou le symbole est d\u00e9finie et accessible.
    • Sa visibilit\u00e9 (visibility) qui ne peut \u00eatre que public en C.

    Pour mieux comprendre ce concept fondamental, imaginons la plage de Donnant \u00e0 Belle-\u00cele-en-Mer. Quelqu'un a \u00e9crit sur le sable, bien visible depuis la colline adjacente, le mot COIN. L'identificateur c'est Donnant, la valeur c'est COIN, le type permet de savoir comment interpr\u00e9ter la valeur. Cela peut s'agir d'une pi\u00e8ce de monnaie en anglais, du coin d'une table en fran\u00e7ais ou du lapin en n\u00e9erlandais.

    L'adresse est l'emplacement exact de la plage o\u00f9 le mot a \u00e9t\u00e9 \u00e9crit (47.32638670571\u00b0 N, 3.2363350691522\u00b0 W), la port\u00e9e est la plage de Donnant et la visibilit\u00e9 est la colline adjacente.

    On voit que sans conna\u00eetre le type de la variable, il est impossible de savoir comment interpr\u00e9ter sa valeur.

    La plage de Donnant

    En pratique l'adresse sera plut\u00f4t de la forme 0x7fffbf7f1b4c, la valeur serait plut\u00f4t 0100001101001111010010010100111000000000 et le type serait une cha\u00eene de caract\u00e8res char[].

    Variables initialis\u00e9es

    Le fait de d\u00e9clarer des variables dans en langage C implique que le logiciel doit r\u00e9aliser l'initialisation de ces variables au tout d\u00e9but de son ex\u00e9cution. De fait, on peut remarquer deux choses. Il y a les variables initialis\u00e9es \u00e0 la valeur z\u00e9ro et les variables initialis\u00e9es \u00e0 des valeurs diff\u00e9rentes de z\u00e9ro. Le compilateur regroupe en m\u00e9moire ces variables en deux cat\u00e9gories et ajoute un bout de code au d\u00e9but de votre application (qui est ex\u00e9cut\u00e9 avant le main).

    Ce code (que l'on n'a pas \u00e0 \u00e9crire) effectue les op\u00e9rations suivantes\u2009:

    • mise \u00e0 z\u00e9ro du bloc m\u00e9moire contenant les variables ayant \u00e9t\u00e9 d\u00e9clar\u00e9es avec une valeur d'initialisation \u00e0 z\u00e9ro,
    • recopie d'une zone m\u00e9moire contenant les valeurs initiales des variables ayant \u00e9t\u00e9 d\u00e9clar\u00e9es avec une valeur d'initialisation diff\u00e9rente de z\u00e9ro vers la zone de ces m\u00eames variables.

    Par ce fait, d\u00e8s que l'ex\u00e9cution du logiciel est effectu\u00e9e, on a, lors de l'ex\u00e9cution du main, des variables correctement initialis\u00e9es.

    ", "tags": ["Donnant", "COIN", "symbole", "variable", "main"]}, {"location": "course-c/15-fundations/syntax/#declaration", "title": "D\u00e9claration", "text": "

    Avant de pouvoir \u00eatre utilis\u00e9e, une variable doit \u00eatre d\u00e9clar\u00e9e afin que le compilateur puisse r\u00e9server un emplacement en m\u00e9moire pour stocker sa valeur.

    Voici quelques d\u00e9clarations valides\u2009:

    char c = '\u20ac';\nint temperature = 37;\nfloat neptune_stone_height = 376.86;\nchar message[] = \"Jarvis, il faut parfois savoir \"\n                 \"courir avant de savoir marcher.\";\n

    Il n'est pas n\u00e9cessaire d'associer une valeur initiale \u00e0 une variable, une d\u00e9claration peut se faire sans initialisation comme montr\u00e9 dans l'exemple suivant dans lequel on r\u00e9serve trois variables i, j, k.

    int i, j, k;\n

    Exercice 2\u2009: Affectation de variables

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    int a, b, c;\nfloat x;\n

    Notez apr\u00e8s chaque affectation, le contenu des diff\u00e9rentes variables\u2009:

    Ligne Instruction a b c x 1 a = 5; 2 b = c; 3 c = a; 4 a = a + 1; 5 x = a - ++c; 6 b = c = x; 7 x + 2. = 7.; Solution Ligne Instruction a b c x 1 a = 5; 5 ? ? ? 2 b = c; 5 ? ? ? 3 c = a; 5 ? 5 ? 4 a = a + 1; 6 ? 5 ? 5 x = a - ++c; 6 ? 6 12 6 b = c = x; 6 12 12 12 7 x + 2. = 7.; - - - -", "tags": ["initialisation", "declaration"]}, {"location": "course-c/15-fundations/syntax/#convention-de-nommage", "title": "Convention de nommage", "text": "

    Diff\u00e9rentes casses illustr\u00e9es

    Il existe autant de conventions de nommage qu'il y a de d\u00e9veloppeurs, mais un consensus majoritaire, que l'on retrouve dans d'autres langages de programmation dit que\u2009:

    • la longueur du nom d'une variable est g\u00e9n\u00e9ralement proportionnelle \u00e0 sa port\u00e9e et donc il est d'autant plus court que l'utilisation d'une variable est localis\u00e9e\u2009;
    • le nom doit \u00eatre concis et pr\u00e9cis et ne pas laisser place \u00e0 une quelconque ambigu\u00eft\u00e9\u2009;
    • le nom doit participer \u00e0 l'autodocumentation du code et permettre \u00e0 un lecteur de comprendre facilement le programme qu'il lit.

    Selon les standards adopt\u00e9s, chaque soci\u00e9t\u00e9 on trouve ceux qui pr\u00e9f\u00e8rent nommer les variables en utilisant un underscore (_) comme s\u00e9parateur et ceux qui pr\u00e9f\u00e8rent nommer une variable en utilisant des majuscules comme s\u00e9parateurs de mots.

    Conventions de nommage Convention Nom fran\u00e7ais Exemple camelcase Casse de chameau userLoginCount snakecase Casse de serpent user_login_count pascalcase Casse de Pascal UserLoginCount kebabcase Casse de kebab user-login-count

    Note

    La casse de kebab n'est pas accept\u00e9e par le standard C car les noms form\u00e9s ne sont pas des identificateurs valides. N\u00e9anmoins cette notation est beaucoup utilis\u00e9e par exemple sur GitHub.

    ", "tags": ["userLoginCount", "UserLoginCount"]}, {"location": "course-c/15-fundations/syntax/#variable-metasyntaxique", "title": "Variable m\u00e9tasyntaxique", "text": "

    Souvent lors d'exemples donn\u00e9s en programmation, on utilise des variables g\u00e9n\u00e9riques dites m\u00e9tasyntaxiques. En fran\u00e7ais les valeurs toto, titi, tata et tutu sont r\u00e9guli\u00e8rement utilis\u00e9es tandis qu'en anglais foo, bar, baz et qux sont r\u00e9guli\u00e8rement utilis\u00e9s. Les valeurs spam, ham et eggs sont quant \u00e0 elles souvent utilis\u00e9e en Python, en r\u00e9f\u00e9rence au sketch Spam des Monthy Python.

    Leur usage est conseill\u00e9 pour appuyer le cadre g\u00e9n\u00e9rique d'un exemple sans lui donner la consonance d'un probl\u00e8me plus sp\u00e9cifique.

    On trouvera une table des diff\u00e9rents noms les plus courants utilis\u00e9s dans diff\u00e9rentes langues.

    Foo, Bar, Titi et Toto

    L'origine de foo et bar remonte \u00e0 la deuxi\u00e8me guerre mondiale o\u00f9 les militaires am\u00e9ricains utilisaient ces termes pour d\u00e9signer des objets non identifi\u00e9s.

    Titi et Toto sont des personnages de bande dessin\u00e9e cr\u00e9\u00e9s par Maurice Cuvillier en 1931.

    ", "tags": ["ham", "eggs", "tata", "toto", "foo", "bar", "spam", "baz", "tutu", "qux", "titi"]}, {"location": "course-c/15-fundations/syntax/#les-constantes", "title": "Les constantes", "text": "

    Une constante par opposition \u00e0 une variable voit son contenu fixe et immuable. Il s'agit d'un espace m\u00e9moire qui ne peut \u00eatre modifi\u00e9 apr\u00e8s son initialisation. Formellement, une constante se d\u00e9clare comme une variable, mais pr\u00e9fix\u00e9e du mot-cl\u00e9 const.

    const double scale_factor = 12.67;\n

    Une constante est principalement utilis\u00e9e pour indiquer au d\u00e9veloppeur que la valeur ne doit pas \u00eatre modifi\u00e9e. Le compilateur peut \u00e9galement s'en servir pour mieux optimiser le code et donc am\u00e9liorer les performances d'ex\u00e9cution.

    Avertissement

    Il ne faut pas confondre la constante qui est une variable immuable, stock\u00e9e en m\u00e9moire et une macro qui appartient au pr\u00e9processeur. Sur certaines plateformes, le fichier d'en-t\u00eate math.h d\u00e9finit par exemple la constante M_PI sous forme de macro.

    #define M_PI 3.14159265358979323846\n

    Cette m\u00eame constante peut \u00eatre d\u00e9finie comme une variable constante\u2009:

    const double pi = 3.14159265358979323846;\n

    En r\u00e9sum\u00e9, les constantes sont utilis\u00e9es pour\u2009:

    • \u00c9viter les erreurs de programmation en \u00e9vitant de modifier une valeur qui ne devrait pas l'\u00eatre.
    • Indiquer au compilateur que la valeur ne changera pas et qu'il peut optimiser le code en cons\u00e9quence.
    • Indiquer au d\u00e9veloppeur que la valeur ne sera pas modifi\u00e9e plus tard dans le programme.
    ", "tags": ["M_PI", "const", "constante", "math.h"]}, {"location": "course-c/15-fundations/syntax/#constantes-litterales", "title": "Constantes litt\u00e9rales", "text": "

    Les constantes litt\u00e9rales repr\u00e9sentent des grandeurs scalaires num\u00e9riques ou de caract\u00e8res et initialis\u00e9es lors de la phase de compilation.

    En effet, lorsque l'on veut saisir un nombre, on ne veut pas que le compilateur la comprenne comme un identificateur, mais bien comme une valeur num\u00e9rique. C'est d'ailleurs la raison pour laquelle un identificateur ne peut pas commencer par un chiffre.

    Les constantes litt\u00e9rales sont g\u00e9n\u00e9ralement identifi\u00e9es avec des pr\u00e9fixes et des suffixes pour indiquer leur nature. Voici quelques exemples\u2009:

    6      // Le nombre d'heures sur l'horloge du Palais du Quirinal \u00e0 Rome\n12u    // Grandeur non sign\u00e9e\n6l     // Grandeur enti\u00e8re sign\u00e9e cod\u00e9e sur un entier long\n42ul   // Grandeur enti\u00e8re non sign\u00e9e cod\u00e9e sur un entier long\n010    // Grandeur octale valant 8 en d\u00e9cimal\n0xa    // Grandeur hexad\u00e9cimale valant 10 en d\u00e9cimal\n0b111  // Grandeur binaire valant 7 en d\u00e9cimal\n1.     // Grandeur r\u00e9elle exprim\u00e9e en virgule flottante\n'0'    // Grandeur caract\u00e8re valant 48 en d\u00e9cimal\n2e3    // Grandeur r\u00e9elle exprim\u00e9e en notation scientifique\n

    Nous l'avons vu plus haut, le type d'une variable est important pour d\u00e9terminer comment une valeur est stock\u00e9e en m\u00e9moire.

    Comme vu dans le chapitre sur la num\u00e9ration, les valeurs num\u00e9riques peuvent \u00eatre stock\u00e9es en m\u00e9moire de diff\u00e9rentes mani\u00e8res. Ainsi, une valeur 48 peut \u00eatre stock\u00e9e sur un octet, un mot de 16 bits, un mot de 32 bits ou un mot de 64 bits. De plus, la valeur peut faire r\u00e9f\u00e9rence au caract\u00e8re 0 en ASCII, mais aussi au nombre 72 s'il est exprim\u00e9 en hexad\u00e9cimal.

    On utilisera un pr\u00e9fixe devant un nombre 0x pour indiquer qu'il est en hexad\u00e9cimal, 0b pour indiquer qu'il est en binaire et 0 pour indiquer qu'il est en octal. Sans pr\u00e9fixe il s'agit d'un nombre d\u00e9cimal (base 10).

    On utilisera un suffixe u pour indiquer que le nombre est non sign\u00e9 (n'admettant pas de valeurs n\u00e9gatives) et l pour indiquer qu'il est long ou ll pour indiquer qu'il est tr\u00e8s long.

    Quant aux guillemets simples ', ils sont utilis\u00e9s pour d\u00e9limiter un caract\u00e8re de la table ASCII.

    Expressions r\u00e9guli\u00e8res

    Il est plus facile pour un informaticien de comprendre la syntaxe des constantes litt\u00e9rales en utilisant des expressions r\u00e9guli\u00e8res. Voici les expressions r\u00e9guli\u00e8res qui d\u00e9finissent les diff\u00e9rentes constantes litt\u00e9rales\u2009:

    Type Expression r\u00e9guli\u00e8re Exemple Nombre sign\u00e9 /[1-9][0-9]*/ 42 Nombre non sign\u00e9 /[1-9][0-9]*u/ 42u Nombre hexad\u00e9cimal /0x[0-9a-fA-F]+/ 0x2a Nombre octal /0[0-7]+/ 052

    Vous pouvez essayer de les tester sur regex101.com.

    Exercice 3\u2009: Constances litt\u00e9rales

    Pour les entr\u00e9es suivantes, indiquez lesquelles sont correctes.

    • [x] 12.3
    • [x] 12E03
    • [x] 12u
    • [ ] 12.0u
    • [ ] 1L
    • [ ] 1.0L
    • [x] .9
    • [x] 9.
    • [ ] .
    • [x] 0x33
    • [ ] 0xefg
    • [x] 0xef
    • [x] 0xeF
    • [ ] 0x0.2
    • [x] 09
    • [x] 02

    La notation scientifique, aussi appel\u00e9e notation exponentielle, est une mani\u00e8re d'\u00e9crire des nombres tr\u00e8s grands ou tr\u00e8s petits de mani\u00e8re plus compacte. Par exemple, 1.23e3 est \u00e9quivalent \u00e0 1230. et 1.23e-3 est \u00e9quivalent \u00e0 0.00123. Le caract\u00e8re e est utilis\u00e9 pour indiquer la puissance de 10 par laquelle le nombre doit \u00eatre multipli\u00e9. Il tire probablement son origine du Fortran qui l'utilisait d\u00e9j\u00e0 en 1957.

    Pas Euler

    Il ne faut pas confondre l'exponentiation avec le nombre d'Euler (2.71828...) e avec la notation scientifique e qui est utilis\u00e9e pour indiquer une puissance de 10.

    La notation scientifique est un double

    En C, la notation scientifique est toujours un nombre en virgule flottante de type double. Ainsi, 1e3 est un double et non un int. Ne prenez donc pas l'habitude d'\u00e9crire int i = 1e3;, mais plut\u00f4t int i = 1000;.

    ", "tags": ["notation-exponentielle", "int", "double", "notation-scientifique"]}, {"location": "course-c/15-fundations/syntax/#operateur-daffectation", "title": "Op\u00e9rateur d'affectation", "text": "

    Dans les exemples ci-dessus, on utilise l'op\u00e9rateur d'affectation pour associer une valeur \u00e0 une variable. Historiquement, et malheureusement, le symbole choisi pour cet op\u00e9rateur est le signe \u00e9gal = or, l'\u00e9galit\u00e9 est une notion math\u00e9matique qui n'est en aucun cas reli\u00e9e \u00e0 l'affectation.

    Pour mieux saisir la nuance, consid\u00e9rons le programme suivant\u2009:

    a = 42;\na = b;\n

    Math\u00e9matiquement, la valeur de b devrait \u00eatre \u00e9gale \u00e0 42 ce qui n'est pas le cas en C o\u00f9 il faut lire, s\u00e9quentiellement l'ex\u00e9cution du code, car oui, C est un langage imp\u00e9ratif. Ainsi, dans l'ordre, on lit\u2009:

    1. J'assigne la valeur 42 \u00e0 la variable symbolis\u00e9e par a
    2. Puis, j'assigne la valeur de la variable b au contenu de a.

    Comme on ne conna\u00eet pas la valeur de b, avec cet exemple, on ne peut pas conna\u00eetre la valeur de a. Certains langages de programmation ont \u00e9t\u00e9 sensibilis\u00e9s \u00e0 l'importance de cette distinction et dans les langages F#, OCaml, R ou S, l'op\u00e9rateur d'affectation est <- et une affectation pourrait s'\u00e9crire par exemple\u2009: a <- 42 ou 42 -> a.

    En C, l'op\u00e9rateur d'\u00e9galit\u00e9 que nous verrons plus loin s'\u00e9crit == (deux = concat\u00e9n\u00e9s).

    Remarquez ici que l'op\u00e9rateur d'affectation de C agit toujours de droite \u00e0 gauche c'est-\u00e0-dire que la valeur \u00e0 droite de l'op\u00e9rateur est affect\u00e9e \u00e0 la variable situ\u00e9e \u00e0 gauche de l'op\u00e9rateur. S'agissant d'un op\u00e9rateur il est possible de cha\u00eener les op\u00e9rations, comme on le ferait avec l'op\u00e9rateur + et dans l'exemple suivant il faut lire que 42 est assign\u00e9 \u00e0 c, que la valeur de c est ensuite assign\u00e9 \u00e0 b et enfin la valeur de b est assign\u00e9e \u00e0 a. Nous verrons plus tard comment l'ordre des op\u00e9rations et l'associativit\u00e9 de chaque op\u00e9rateur.

    a = b = c = 42;\n

    Exercice 4\u2009: Affectations simples

    Donnez les valeurs de x, n, p apr\u00e8s l'ex\u00e9cution des instructions ci-dessous\u2009:

    float x;\nint n, p;\n\np = 2;\nx = 15 / p;\nn = x + 0.5;\n
    Solution
    p \u2261 2\nx \u2261 7\nn \u2261 7\n

    Exercice 5\u2009: Trop d'\u00e9galit\u00e9s

    On consid\u00e8re les d\u00e9clarations suivantes\u2009:

    int i, j, k;\n

    Donnez les valeurs des variables i, j et k apr\u00e8s l'ex\u00e9cution de chacune des expressions ci-dessous. Qu'en pensez-vous\u2009?

    /* 1 */ i = (k = 2) + (j = 3);\n/* 2 */ i = (k = 2) + (j = 2) + j * 3 + k * 4;\n/* 3 */ i = (i = 3) + (k = 2) + (j = i + 1) + (k = j + 2) + (j = k - 1);\n
    Solution

    Selon la table de priorit\u00e9 des op\u00e9rateurs, on note\u2009:

    • () priorit\u00e9 1 associativit\u00e9 \u00e0 droite
    • * priorit\u00e9 3 associativit\u00e9 \u00e0 gauche
    • + priorit\u00e9 4 associativit\u00e9 \u00e0 droite
    • = priorit\u00e9 14 associativit\u00e9 \u00e0 gauche

    En revanche rien n'est dit sur les point de s\u00e9quences <https://en.wikipedia.org/wiki/Sequence_point>__. L'op\u00e9rateur d'affectation n'est pas un point de s\u00e9quence, autrement dit le standard C99 (Annexe C) ne d\u00e9finit pas l'ordre dans lequel les assignations sont effectu\u00e9es.

    Ainsi, seul le premier point poss\u00e8de une solution, les deux autres sont ind\u00e9termin\u00e9s

    1. i = (k = 2) + (j = 3)

      • i = 5
      • j = 3
      • k = 2
    2. i = (k = 2) + (j = 2) + j * 3 + k * 4

      • R\u00e9sultat ind\u00e9termin\u00e9
    3. i = (i = 3) + (k = 2) + (j = i + 1) + (k = j + 2) + (j = k - 1)

      • R\u00e9sultat ind\u00e9termin\u00e9
    "}, {"location": "course-c/15-fundations/syntax/#espaces-de-noms", "title": "Espaces de noms", "text": "

    En C, il est possible d'utiliser le m\u00eame identificateur pour autant qu'il n'appartient pas au m\u00eame espace de nom. Il existe en C plusieurs espaces de noms\u2009:

    • \u00e9tiquettes utilis\u00e9es pour l'instruction goto;
    • tag de structures, d'\u00e9num\u00e9rations et d'union\u2009;
    • membres de structures, d'\u00e9num\u00e9rations et d'union\u2009;
    • identificateurs de variable ou fonctions.

    Ces espaces de noms sont ind\u00e9pendants les uns des autres, il est donc possible d'utiliser le m\u00eame nom sans conflit. Par exemple\u2009:

    typedef struct x {  // Espace de nom des structures\n    int x;  // Membre de la structure point\n} x;  // Espace de nom des types\n\nint main() {\n    x x = {.x = 42};  // x est une variable de type x\n}\n
    ", "tags": ["goto"]}, {"location": "course-c/15-fundations/syntax/#commentaires", "title": "Commentaires", "text": "

    Comme en fran\u00e7ais et ainsi qu'illustr\u00e9 par la figure suivante, il est possible d'annoter un programme avec des commentaires. Les commentaires n'ont pas d'incidence sur le fonctionnement d'un programme et ne peuvent \u00eatre lu que par le d\u00e9veloppeur qui poss\u00e8de le code source. Par ailleurs, comme nous l'avons vu en introduction, le pr\u00e9processeur C supprime les commentaires du code source avant la compilation.

    Les carafes dans la Vivonne

    Il existe deux mani\u00e8res d'\u00e9crire un commentaire en C, les commentaires de lignes apparus avec le C++ et C99, ainsi que les commentaires de blocs. Les commentaires de blocs sont plus anciens et sont compatibles avec les versions ant\u00e9rieures du langage.

    Commentaire de ligneCommentaire de bloc
    // This is a single line comment.\n
    /* This is a\n   Multi-line comment */\n

    Il est important de rappeler que les commentaires sont trait\u00e9s par le pr\u00e9processeur, aussi ils n'influencent pas le fonctionnement d'un programme, mais seulement sa lecture. Rappelons qu'un code est plus souvent lu qu'\u00e9crit, car on ne l'\u00e9crit qu'une seule fois, mais comme tout d\u00e9veloppement doit \u00eatre si possible r\u00e9utilisable, il est plus probable qu'il soit lu part d'autres d\u00e9veloppeurs.

    En cons\u00e9quence, il est important de clarifier toute zone d'ombre lorsque l'on s'\u00e9loigne des consensus \u00e9tablis, ou lorsque le code seul n'est pas suffisant pour bien comprendre son fonctionnement.

    D'une fa\u00e7on g\u00e9n\u00e9rale, les commentaires servent \u00e0 expliquer pourquoi et non comment. Un bon programme devrait pouvoir se passer de commentaires, mais un programme sans commentaires n'est pas n\u00e9cessairement un bon programme.

    Note

    Il est pr\u00e9f\u00e9rable d'utiliser le commentaire de ligne d\u00e8s que possible, car d'une part il y a moins de caract\u00e8res \u00e0 \u00e9crire, mais surtout les commentaires de blocs ne sont pas imbriquables (nestable).

    /*\n// Autoris\u00e9\n*/\n
    /*\n/* Interdit */\n*/\n

    Les commentaires de blocs peuvent \u00eatre utilis\u00e9s pour documenter une fonction ou un bloc de code, mais \u00e9galement pour ins\u00e9rer un commentaire \u00e0 l'int\u00e9rieur d'une ligne\u2009:

    int deep_throught /* Name of the computer */ = 42; // The answer\n
    "}, {"location": "course-c/15-fundations/syntax/#commenter-du-code", "title": "Commenter du code\u2009?", "text": "

    Lorsque vous d\u00e9veloppez, vous avez souvent besoin de d\u00e9sactiver des portions de code pour des raisons de d\u00e9bogage ou de test. Il est tentant de commenter ces portions de code plut\u00f4t que de les supprimer. N\u00e9anmoins une r\u00e8gle \u00e0 retenir est que l'on ne commente jamais des portions de code, et ce pour plusieurs raisons\u2009:

    1. Les outils de refactoring ne pourront pas acc\u00e9der du code comment\u00e9.
    2. La syntaxe ne pourra plus \u00eatre v\u00e9rifi\u00e9e par l'IDE.
    3. Les outils de gestion de configuration (e.g. Git) devraient \u00eatre utilis\u00e9s \u00e0 cette fin.

    Si d'aventure vous souhaitez quand m\u00eame exclure temporairement du code de la compilation, il est recommand\u00e9 d'utiliser la directive de pr\u00e9processeur suivante, et n'oubliez pas d'expliquer pourquoi vous avez souhait\u00e9 d\u00e9sactiver cette portion de code.

    #if 0 // TODO: Check if divisor could still be null at this point.\nif (divisor == 0) {\n    return -1; // Error\n}\n#endif\n
    "}, {"location": "course-c/15-fundations/syntax/#quelques-conseils", "title": "Quelques conseils", "text": "

    D'une mani\u00e8re g\u00e9n\u00e9rale l'utilisation des commentaires ne devrait pas \u00eatre utilis\u00e9e pour\u2009:

    • d\u00e9sactiver temporairement une portion de code sans l'effacer\u2009;
    • expliquer le comment du fonctionnement du code\u2009;
    • faire dans le dithyrambique pompeux et notarial, des phrases \u00e0 rallonge bien trop romanesques\u2009;
    • cr\u00e9er de jolies s\u00e9parations telles que /*************************/.

    Il n'est pas rare de voir au d\u00e9but d'un fichier un commentaire de la forme suivante\u2009:

    /**\n * @brief Short description of the translation unit.\n *\n * @author John Doe <john@doe.com>\n * @date 2021-09-01\n * @file main.c\n *\n * Long description of the translation unit.\n *\n * NOTE: Important notes about this code\n * TODO: Things to fix...\n */\n

    SSOT

    Vous verrez souvent, trop souvent le nom de l'auteur et du fichiers dans les en-t\u00eates. Ce n'est pas une bonne pratique si vous utilisez Git, car ces informations sont d\u00e9j\u00e0 pr\u00e9sentes dans les m\u00e9tadonn\u00e9es du fichier.

    Voici un exemple de ce qu'il ne faut pas faire\u2009:

    Pas bienBien
    /*****************************************************\n                                           _..._\n _   _    _    ____    _                .'     '.      _\n| \\ | |  / \\  / ___|  / \\              /    .-\"\"-\\   _/ \\\n|  \\| | / _ \\ \\___ \\ / _ \\          .-|   /:.   |  |   |\n| |\\  |/ ___ \\ ___) / ___ \\         |  \\  |:.   /.-'-./\n|_| \\_/_/   \\_\\____/_/   \\_\\        | .-'-;:__.'    =/\n                                  .'=  *=|NASA _.='\n                                 /   _.  |    ;\n                                ;-.-'|    \\   |\n                               /   | \\    _\\  _\\\n                               \\__/'._;.  ==' ==\\\n                                        \\    \\   |\n                                 jgs    /    /   /\n                                        /-._/-._/\nNational Aeronautics and Space          \\   `\\  \\\nAdministration.                          `-._/._/\n\n@file appolo11.c\n@brief Launch control module\n@author Margaret Hamilton\n@date 1969-07-16\n\nThis module is responsible for the launch of the Apollo\n11 mission. It is a critical part of the mission and\nshould not be modified.\n*******************************************************/\n
    /**\n * Launch control module.\n *\n * This module is responsible for the launch of the Apollo\n * 11 mission. It is a critical part of the mission and\n * should not be modified.\n */\n

    Le format des commentaires est par essence libre au d\u00e9veloppeur, mais il est g\u00e9n\u00e9ralement souhait\u00e9 que\u2009: Les commentaires soient concis et pr\u00e9cis et qu'ils soient \u00e9crits en anglais.

    Exercice 6\u2009: Verbosit\u00e9

    Comment r\u00e9cririez-vous ce programme\u2009?

    for (register unsigned int the_element_index = 0;\n    the_element_index < number_of_elements; the_element_index += 1)\n    array_of_elements[the_element_index] =  the_element_index;\n
    Solution

    Une r\u00e8gle de programmation\u2009: le nom identifieurs doit \u00eatre proportionnel \u00e0 leur contexte. Plus le contexte de la variable est r\u00e9duit, plus le nom peut \u00eatre court. Le m\u00eame programme pourrait \u00eatre \u00e9crit comme suit\u2009:

    for (size_t i; i < nelems; i++)\n    elem[i] = i;\n

    Un consensus assez bien \u00e9tabli est qu'une variable commen\u00e7ant par n peut signifier number of.

    "}, {"location": "course-c/20-composite-types/", "title": "Types Composites", "text": "

    Un type composite est un type de donn\u00e9es qui est construit \u00e0 partir d'autres types de donn\u00e9es plus simples ou primitifs (comme int, char, etc.). Ces types permettent de regrouper plusieurs \u00e9l\u00e9ments de donn\u00e9es sous une seule entit\u00e9, ce qui est essentiel pour organiser des structures de donn\u00e9es plus complexes dans les programmes.

    En C on retrouve les types composites suivants\u2009:

    Les tableaux

    Un tableau est une collection d'\u00e9l\u00e9ments de m\u00eame type organis\u00e9e de mani\u00e8re continu\u00eb en m\u00e9moire.

    Les structures (struct)

    Une structure est un type composite qui regroupe des \u00e9l\u00e9ments de donn\u00e9es, appel\u00e9s membres qui peuvent \u00eatre de types diff\u00e9rents.

    Les unions

    Une union est similaire \u00e0 une structure, mais tous les membres partagent la m\u00eame zone m\u00e9moire. Cela signifie qu'une union ne peut stocker qu'une seule valeur \u00e0 la fois parmi ses membres.

    Les \u00e9num\u00e9rations (enum)

    Une \u00e9num\u00e9ration est un type composite qui associe des noms symboliques \u00e0 des valeurs int\u00e9grales. Bien que techniquement les \u00e9num\u00e9rations soient des types scalaires, elles sont souvent consid\u00e9r\u00e9es dans le cadre des types composites en raison de leur capacit\u00e9 \u00e0 repr\u00e9senter des ensembles de valeurs possibles.

    Les cha\u00eenes de caract\u00e8res

    Les cha\u00eenes de caract\u00e8res sont techniquement des tableaux de caract\u00e8res (char), mais elles peuvent \u00eatre consid\u00e9r\u00e9es comme un type composite en raison de la mani\u00e8re dont elles sont manipul\u00e9es et utilis\u00e9es pour repr\u00e9senter du texte.

    En combinant des tableaux, des structures avec des pointeurs on peut cr\u00e9er des types composites encore plus complexes que l'on appellera des conteneurs de donn\u00e9es.

    ", "tags": ["int", "char"]}, {"location": "course-c/20-composite-types/arrays/", "title": "Tableaux", "text": "

    Les tableaux (arrays) repr\u00e9sentent une s\u00e9quence finie d'\u00e9l\u00e9ments d'un type donn\u00e9 que l'on peut acc\u00e9der par leur position (indice) dans la s\u00e9quence. Un tableau est par cons\u00e9quent une liste index\u00e9e de variables du m\u00eame type.

    Un exemple typique d'utilisation d'un tableau est le Crible d'\u00c9ratosth\u00e8ne qui permet de trouver tous les nombres premiers inf\u00e9rieurs \u00e0 un entier donn\u00e9. Dans cet algorithme, un tableau de bool\u00e9ens est utilis\u00e9 pour marquer les nombres qui ne sont pas premiers. Le code est en 4 parties. D'abord la capture d'une valeur donn\u00e9e par l'utilisateur stock\u00e9e dans n, puis l'initialisation des valeurs du tableau \u00e0 true avec une boucle, suivi de l'algorithme du crible qui contient deux boucles imbriqu\u00e9es et enfin l'affichage du r\u00e9sultat. Notons que la plus ancienne r\u00e9f\u00e9rence connue au crible (en grec ancien\u2009: \u03ba\u03cc\u03c3\u03ba\u03b9\u03bd\u03bf\u03bd \u1f18\u03c1\u03b1\u03c4\u03bf\u03c3\u03b8\u03ad\u03bd\u03bf\u03c5\u03c2, k\u00f3skinon Eratosth\u00e9nous) se trouve dans l'Introduction \u00e0 l'arithm\u00e9tique de Nicomachus de G\u00e9rasa, un ouvrage du d\u00e9but du II\u1d49 si\u00e8cle de notre \u00e8re, qui l'attribue \u00e0 \u00c9ratosth\u00e8ne de Cyr\u00e8ne, un math\u00e9maticien grec du III\u1d49 si\u00e8cle avant J.-C., bien qu'il d\u00e9crive le criblage par les nombres impairs plut\u00f4t que par les nombres premiers.

    #define MAX 1000\n\nint main(int argc, char *argv[]) {\n   if (argc != 2) return -1;\n   int n = atoi(argv[1]);\n   if (n > MAX) return -2;\n\n   // At start, all numbers are prime numbers\n   bool primes[MAX];\n   for (int i = 0; i <= n; i++) primes[i] = true;\n\n   // \u00c9ratosth\u00e8ne sieve algorithm\n   primes[0] = primes[1] = false;  // 0 et 1 are not prime numbers\n   for (int p = 2; p <= sqrt(n); p++)\n      if (primes[p])\n         for (int i = p * p; i <= n; i += p) primes[i] = false;\n\n   // Display prime numbers\n   for (int i = 2; i <= n; i++)\n      if (primes[i]) printf(\"%d \", i);\n   printf(\"\\n\");\n}\n

    L'op\u00e9rateur crochet [] est utilis\u00e9 \u00e0 la fois pour le d\u00e9r\u00e9f\u00e9rencement (acc\u00e8s \u00e0 un indice du tableau) et pour l'assignation d'une taille \u00e0 un tableau\u2009:

    La d\u00e9claration d'un tableau d'entiers de dix \u00e9l\u00e9ments s'\u00e9crit de la fa\u00e7on suivante\u2009:

    int array[10];\n

    Par la suite il est possible d'acc\u00e9der aux diff\u00e9rents \u00e9l\u00e9ments ici l'\u00e9l\u00e9ment 1 et 3 (deuxi\u00e8me et quatri\u00e8me position du tableau) :

    array[1];\narray[5 - 2];\n

    Imaginons un tableau de int16_t de 5 \u00e9l\u00e9ments. En m\u00e9moire ce tableau est une succession de 10 bytes (5 \u00e9l\u00e9ments de 2 bytes chacun).

    Tableau en m\u00e9moire

    int16_t array[5] = {0x0201, 0x0403, 0x0605, 0x0807, 0x0A09};\n

    Rappelez-vous que les entiers sont stock\u00e9s en m\u00e9moire en little-endian, c'est-\u00e0-dire que l'octet de poids faible est stock\u00e9 en premier. Ainsi, l'entier 0x0201 est stock\u00e9 en m\u00e9moire 0x01 puis 0x02. Lorsque vous acc\u00e9dez \u00e0 un \u00e9l\u00e9ment du tableau. Chaque \u00e9l\u00e9ment en m\u00e9moire poss\u00e8de une adresse qui lui est propre. N\u00e9anmoins lorsque l'on se r\u00e9f\u00e8re au tableau dans son ensemble (ici array), c'est l'adresse du premier \u00e9l\u00e9ment qui est retourn\u00e9e soit 0xffacb10.

    Comme le tableau est de type int16_t, chaque \u00e9l\u00e9ment est de taille 2 bytes, donc lorsque l'on acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 3, une arithm\u00e9tique sur les adresse est effectu\u00e9e\u2009:

    \\[ \\begin{aligned} \\text{array} & = 0xffacb10 \\\\ \\text{array[3]} & = 0xffacb10 + 3 \\times 2 = 0xffacb16 \\end{aligned} \\]

    L'op\u00e9rateur sizeof qui permet de retourner la taille d'une structure de donn\u00e9e en m\u00e9moire est tr\u00e8s utile pour les tableaux. Cependant, cet op\u00e9rateur retourne la taille du tableau en bytes, et non le nombre d'\u00e9l\u00e9ments qui le compose. Dans l'exemple suivant sizeof(array) retourne \\(5\\cdot2 = 40\\) tandis que sizeof(array[0]) retourne la taille d'un seul \u00e9l\u00e9ment \\(2\\); et donc, sizeof(array) / sizeof(array[0]) est le nombre d'\u00e9l\u00e9ments de ce tableau, soit 5.

    size_t length = sizeof(array) / sizeof(array[0]);\nassert (length == 5);\n

    L'indice z\u00e9ro

    L'index d'un tableau commence toujours \u00e0 z\u00e9ro et par cons\u00e9quent l'index maximum d'un tableau de 5 \u00e9l\u00e9ments sera 4. Il est donc fr\u00e9quent dans une boucle d'utiliser < et non <=:

    for(size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n/* ... */\n}\n

    Nous le verrons plus tard lorsque nous parlerons des pointeurs, mais un tableau est en r\u00e9alit\u00e9 un pointeur, c'est-\u00e0-dire la position m\u00e9moire \u00e0 laquelle se trouvent les \u00e9l\u00e9ments du tableau (ici l'adresse 0xffacb10). Ce qu'il est important de retenir c'est que lorsqu'un tableau est pass\u00e9 \u00e0 une fonction comme dans l'exemple suivant, ce n'est pas l'int\u00e9gralit\u00e9 des donn\u00e9es du tableau qui sont copi\u00e9es sur la pile, mais seulement l'adresse de ce dernier. On dit que le tableau est pass\u00e9 par r\u00e9f\u00e9rence.

    Une preuve est que le contenu du tableau peut \u00eatre modifi\u00e9 \u00e0 distance\u2009:

    void function(int i[5]) {\n   i[2] = 12\n}\n\nint main(void) {\n   int array[5] = {0};\n   function(array);\n   assert(array[2] == 12);\n}\n

    Un fait remarquable est que l'op\u00e9rateur [] est commutatif. En effet, l'op\u00e9rateur crochet est un sucre syntaxique d\u00e9finit comme\u2009:

    a[b] == *(a + b)\n

    Et cela fonctionne de la m\u00eame mani\u00e8re avec les tableaux \u00e0 plusieurs dimensions\u2009:

    a[1][2] == *(*(a + 1) + 2))\n

    Pour r\u00e9sumer, un tableau permet de regrouper dans un m\u00eame conteneur une liste d'\u00e9l\u00e9ments du m\u00eame type. Il est possible d'acc\u00e9der \u00e0 ces \u00e9l\u00e9ments par leur indice, et il est possible de passer un tableau \u00e0 une fonction par r\u00e9f\u00e9rence.

    La taille est une constante litt\u00e9rale

    La taille d'un tableau doit \u00eatre une constante litt\u00e9rale. Il n'est pas possible de d\u00e9clarer un tableau avec une taille variable. Par exemple, l'\u00e9criture suivante est incorrecte\u2009:

    size_t size = 10;\nint array[size];\n

    En pratique, cet exemple va compiler mais en utilisant une fonctionnalit\u00e9 nomm\u00e9e VLA (Variable Length Array) qui n'est pas recommand\u00e9e. Les tableaux de taille variable sont une source de bugs potentiels et ne sont pas support\u00e9s par tous les compilateurs.

    Limites

    En C il n'y a pas de v\u00e9rification de limites lors de l'acc\u00e8s \u00e0 un tableau. Il est donc possible d'acc\u00e9der \u00e0 un \u00e9l\u00e9ment qui n'existe pas. Par exemple, si un tableau de 5 \u00e9l\u00e9ments est d\u00e9clar\u00e9, il est possible d'acc\u00e9der \u00e0 l'\u00e9l\u00e9ment 6 sans g\u00e9n\u00e9rer d'erreur. Cela peut \u00eatre source de bugs tr\u00e8s difficiles \u00e0 d\u00e9tecter.

    int array[5] = {0};\narray[6] = 42; // Pas d'erreur !\n

    C'est au programmeur de s'assurer que les indices utilis\u00e9s sont valides. Le plus souvent, ce type d'erreur ne m\u00e8ne pas \u00e0 un crash imm\u00e9diat, mais \u00e0 un comportement ind\u00e9termin\u00e9 car l'acc\u00e8s m\u00e9moire est en dehors de la zone allou\u00e9e au tableau et correspond \u00e0 une zone m\u00e9moire qui peut \u00eatre utilis\u00e9e par une autre variable. C'est ce que l'on appelle un buffer overflow. Dans l'exemple suivant, les tableaux a et b sont d\u00e9clar\u00e9s l'un apr\u00e8s l'autre sur la pile, et l'\u00e9criture en dehors de a modifie la valeur de b :

    int main() {\n    int a[4] = {0};\n    int b[4] = {0};\n    a[4] = 42;\n    printf(\"%d\\n\", b[0]); // Affiche 42 !\n}\n
    ", "tags": ["array", "sizeof", "int16_t", "true"]}, {"location": "course-c/20-composite-types/arrays/#initialisation", "title": "Initialisation", "text": "

    Lors de la d\u00e9claration d'un tableau, le compilateur r\u00e9serve un espace m\u00e9moire de la taille suffisante pour contenir tous les \u00e9l\u00e9ments du tableau. La d\u00e9claration suivante r\u00e9serve un espace pour 6 entiers, chacun d'une taille de 32-bits (4 bytes). L'espace m\u00e9moire r\u00e9serv\u00e9 est donc de 24 bytes.

    int32_t even[6];\n

    Compte tenu de cette d\u00e9claration, il n'est pas possible de conna\u00eetre la valeur des diff\u00e9rents \u00e9l\u00e9ments car ce tableau n'a pas \u00e9t\u00e9 initialis\u00e9 et le contenu m\u00e9moire est non pr\u00e9dictible puisqu'il peut contenir les vestiges d'un ancien programme ayant tant\u00f4t r\u00e9sid\u00e9 dans cette r\u00e9gion m\u00e9moire.

    Pour d\u00e9finir un contenu, il est n\u00e9cessaire d'initialiser le tableau en affectant une valeur \u00e0 chaque \u00e9l\u00e9ment comme suit\u2009:

    int32_t sequence[6];\nsequence[0] = 4;\nsequence[1] = 8;\nsequence[2] = 15;\nsequence[3] = 16;\nsequence[4] = 23;\nsequence[5] = 42;\n

    Cette \u00e9criture n'est certainement pas la plus optimis\u00e9e, car l'initialisation du tableau n'est pas r\u00e9alis\u00e9e \u00e0 la compilation, mais \u00e0 l'ex\u00e9cution du programme\u2009; et ce sera pas moins de six op\u00e9rations qui seront n\u00e9cessaires \u00e0 l'initialiser. En pratique on utilise la notation par accolades {} pour initialiser un tableau\u2009:

    int32_t sequence[6] = {4, 8, 15, 16, 23, 42};\n

    Ici, les accolades ne forment pas un bloc de code, mais une liste d'\u00e9l\u00e9ments, chacun s\u00e9par\u00e9 par une virgule.

    Dans cette derni\u00e8re \u00e9criture, on notera une redondance d'information. La liste d'initialisation {4, 8, 15, 16, 23, 42} comporte six \u00e9l\u00e9ments et le tableau est d\u00e9clar\u00e9 avec six \u00e9l\u00e9ments [6]. Pour \u00e9viter une double source de v\u00e9rit\u00e9, il est ici possible d'omettre la taille du tableau\u2009:

    int32_t sequence[] = {4, 8, 15, 16, 23, 42};\n

    Le compilateur peut inf\u00e9rer la taille du tableau en fonction du nombre d'\u00e9l\u00e9ments de la liste d'initialisation. N\u00e9anmoins, une liste d'initialisation n'initialise pas n\u00e9cessairement tous les \u00e9l\u00e9ments du tableau. Il est possible par exemple de d\u00e9clarer un tableau de 100 \u00e9l\u00e9ments o\u00f9 seul les premiers sont initialis\u00e9s\u2009:

    int32_t sequence[100] = {4, 8, 15, 16, 23, 42 /* le reste vaudra z\u00e9ro */ };\n

    Dans ce cas, les \u00e9l\u00e9ments 6 \u00e0 99 seront initialis\u00e9s \u00e0 z\u00e9ro. Pour s'en donner la preuve, observons le code assembleur g\u00e9n\u00e9r\u00e9 par le compilateur\u2009:

    mov     rbp, rsp\nsub     rsp, 280\nlea     rdx, [rbp-400]    ; adresse du tableau\nmov     eax, 0            ; valeur \u00e0 initialiser\nmov     ecx, 50           ; nombre de mots de 8 bytes \u00e0 initialiser\nmov     rdi, rdx          ; destination\nrep stosq                 ; r\u00e9p\u00e8te l'op\u00e9ration de stockage 50 fois\n\n; Tous les \u00e9l\u00e9ments du tableau sont maintenant initialis\u00e9s \u00e0 z\u00e9ro.\n; Ensuite, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s explicitement.\n\nmov     DWORD PTR [rbp-400], 4\nmov     DWORD PTR [rbp-396], 8\nmov     DWORD PTR [rbp-392], 15\nmov     DWORD PTR [rbp-388], 16\nmov     DWORD PTR [rbp-384], 23\nmov     DWORD PTR [rbp-380], 42\n

    Donc, lors de l'initialisation d'un tableau de 100 \u00e9l\u00e9ments dans une fonction on peut noter\u2009:

    1. Le tableau est d\u00e9clar\u00e9 sur la pile.
    2. Tous les \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro.
    3. Puis, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s explicitement.

    Le langage C permet \u00e9galement d'initialiser un tableau de fa\u00e7on partielle. Dans l'exemple suivant, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s, les autres \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro\u2009:

    int32_t sequence[100] = {[0]=4, [2]=8, [4]=15, [6]=16, [8]=23, [10]=42};\n

    Notons que lorsque la notation []= est utilis\u00e9e, les valeurs qui suivent seront positionn\u00e9es aux indices suivants. On peut donc \u00e9crire\u2009:

    int32_t sequence[6] = {[0]=4, 8, [3]=16, 23, 42};\n

    Dans l'exemple ci-dessus sequence[2] vaudra z\u00e9ro.

    Initialisation tardive

    Il n'est pas possible d'initialiser un tableau apr\u00e8s sa d\u00e9claration. L'initialisation doit \u00eatre r\u00e9alis\u00e9e lors de la d\u00e9claration du tableau. L'\u00e9criture suivante est donc incorrecte\u2009:

    int array[10];\n\n// Erreur: l'initialisation tardive n'est pas autoris\u00e9e.\narray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n
    "}, {"location": "course-c/20-composite-types/arrays/#initialisation-a-zero", "title": "Initialisation \u00e0 z\u00e9ro", "text": "

    Pour initialiser un tableau \u00e0 z\u00e9ro, on devrait \u00eatre autoris\u00e9 \u00e0 \u00e9crire le code suivant puisque tous les \u00e9l\u00e9ments non explicitement initialis\u00e9s sont initialis\u00e9s \u00e0 z\u00e9ro\u2009:

    int32_t sequence[6] = {};\n

    Cependant cette \u00e9criture n'est pas autoris\u00e9e selon la norme car au moins un \u00e9l\u00e9ment doit \u00eatre initialis\u00e9. Il est donc n\u00e9cessaire d'initialiser au moins un \u00e9l\u00e9ment \u00e0 z\u00e9ro\u2009:

    int32_t sequence[6] = {0};\n

    Initialisation \u00e0 42

    L'\u00e9criture ci-dessus ne veut pas dire que tout le tableau est initialis\u00e9 \u00e0 z\u00e9ro de la m\u00eame mani\u00e8re l'\u00e9criture suivante ne veut pas dire que tout le tableau est initialis\u00e9 \u00e0 42\u2009:

    int32_t sequence[6] = {42};\n

    Seul le premier \u00e9l\u00e9ment est initialis\u00e9 \u00e0 42, les autres \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro.

    "}, {"location": "course-c/20-composite-types/arrays/#initialisation-a-une-valeur-particuliere", "title": "Initialisation \u00e0 une valeur particuli\u00e8re", "text": "

    Cette \u00e9criture n'est pas normalis\u00e9e C99, mais est g\u00e9n\u00e9ralement compatible avec la majorit\u00e9 des compilateurs.

    int array[1024] = { [ 0 ... 1023 ] = -1 };\n

    En C99, il n'est pas possible d'initialiser un type compos\u00e9 \u00e0 une valeur unique. La mani\u00e8re traditionnelle reste la boucle it\u00e9rative\u2009:

    for (size_t i = 0; i < sizeof(array)/sizeof(array[0]); ++i)\n    array[i] = -1;\n
    "}, {"location": "course-c/20-composite-types/arrays/#tableaux-non-modifiables", "title": "Tableaux non modifiables", "text": "

    Maintenant que nous savons initialiser un tableau, il peut \u00eatre utile de d\u00e9finir un tableau avec un contenu qui n'est pas modifiable. Le mot cl\u00e9 const est utilis\u00e9 \u00e0 cette fin.

    const int32_t sequence[6] = {4, 8, 15, 16, 23, 42};\nsequence[2] = 12; // Interdit !\n

    Dans l'exemple ci-dessus, la seconde ligne g\u00e9n\u00e8rera l'erreur suivante\u2009:

    error: assignment of read-only location \u2018sequence[2]\u2019\n

    Notons que lors de l'utilisation de pointeurs, il serait possible, de fa\u00e7on d\u00e9tourn\u00e9e, de modifier ce tableau malgr\u00e9 tout\u2009:

    int *p = sequence;\np[2] = 12;\n

    Dans ce cas, ce n'est pas une erreur, mais une alerte du compilateur qui survient\u2009:

    warning: initialization discards \u2018const\u2019 qualifier from pointer\ntarget type [-Wdiscarded-qualifiers]\n
    ", "tags": ["const"]}, {"location": "course-c/20-composite-types/arrays/#tableaux-multidimensionnels", "title": "Tableaux multidimensionnels", "text": "

    Il est possible de d\u00e9clarer un tableau \u00e0 plusieurs dimensions. Si par exemple on souhaite d\u00e9finir une grille de jeu du tic-tac-toe ou morpion, il faudra une grille de 3x3.

    Pour ce faire, il est possible de d\u00e9finir un tableau de 6 \u00e9l\u00e9ments comme vu auparavant, et utiliser un artifice pour adresser les lignes et les colonnes\u2009:

    char game[6] = {0};\nint row = 1;\nint col = 2;\ngame[row * 3 + col] = 'x';\n

    N\u00e9anmoins, cette \u00e9criture n'est pas pratique et le langage C dispose heureusement de la syntaxe idoine pour all\u00e9ger l'\u00e9criture. La grille de jeu sera simplement initialis\u00e9e comme suit\u2009:

    char game[3][3] = {0};\n

    Jouer x au centre \u00e9quivaut \u00e0 \u00e9crire\u2009:

    game[1][1] = 'x';\n

    De la m\u00eame fa\u00e7on, il est possible de d\u00e9finir une structure tridimensionnelle\u2009:

    int volume[10][4][8];\n

    L'initialisation des tableaux multidimensionnelle est tr\u00e8s similaire aux tableaux standards, mais il est possible d'utiliser plusieurs niveaux d'accolades.

    Ainsi le jeu de morpion suivant\u2009:

     o | x | x\n---+---+---\n x | o | o\n---+---+---\n x | o | x\n

    Peut s'initialiser comme suit\u2009:

    char game[][3] = {{'o', 'x', 'x'}, {'x', 'o', 'o'}, {'x', 'o', 'x'}};\n

    Notons que l'\u00e9criture suivante est aussi accept\u00e9e car un tableau multidimensionnel est toujours repr\u00e9sent\u00e9 en m\u00e9moire de fa\u00e7on lin\u00e9aire\u2009: comme un tableau \u00e0 une dimension\u2009:

    char game[][3] = {'o', 'x', 'x', 'x', 'o', 'o', 'x', 'o', 'x'};\n

    Nous l'avons vu plus haut, il n'est pas n\u00e9cessaire de fournir toutes les informations de taille du tableau lors de l'initialisation. Dans l'exemple du tic-tac-toe, la valeur de la premi\u00e8re dimension est omise car elle peut \u00eatre inf\u00e9r\u00e9e du nombre d'\u00e9l\u00e9ments de la liste d'initialisation.

    Prenons l'exemple du tableau suivant\u2009:

    int array[2][3][4];\n

    On peut le repr\u00e9senter graphiquement comme suit\u2009:

    Tableau multidimensionnel 2x3x4

    N\u00e9anmoins en m\u00e9moire, ce tableau est toujours repr\u00e9sent\u00e9 de fa\u00e7on lin\u00e9aire. L'association des coordonn\u00e9es x, y et z est subjective et d\u00e9pend de la mani\u00e8re dont le tableau est utilis\u00e9. N\u00e9anmoins, on pourrait s'accorder sur une repr\u00e9sentation en m\u00e9moire logique. Dans le cas de cette figure, l'axe horizontal est l'axe des x, l'axe vertical est l'axe des y et la profondeur est l'axe des z. L'acc\u00e8s se fera avec [z][y][x]:

    int array[][3][4] = {\n    {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}},\n    {{13,14,15,16}, {17,18,19,20}, {21,22,23,24}}\n};\n\nint x = 0, y = 0, z = 0;\nassert(array[z][y][x] == 1);\n

    Si on d\u00e9cide l'agencement [x][y][z] qui peut sembler plus naturel, il sera d\u00e9clar\u00e9 comme suit\u2009:

    int array[4][3][2] = {\n    {{1,13}, {5,17}, {9,21}},\n    {{2,14}, {6,18}, {10,22}},\n    {{3,15}, {7,19}, {11,23}},\n    {{4,16}, {8,20}, {12,24}}\n};\n

    Pour cette raison, il est plus courant d'inverser les dimensions pour les tableaux multidimensionnels. Un tableau x-y \u00e0 deux dimensions est souvent d\u00e9clar\u00e9 array[y][x] pour faciliter l'initialisation.

    "}, {"location": "course-c/20-composite-types/arrays/#tableaux-et-fonctions", "title": "Tableaux et fonctions", "text": "

    Le passage d'un tableau \u00e0 une fonction est un peu particulier. En effet, comme nous l'avons \u00e9voqu\u00e9, un tableau est en r\u00e9alit\u00e9 un pointeur, c'est-\u00e0-dire l'adresse m\u00e9moire du premier \u00e9l\u00e9ment du tableau.

    Pour le cas le plus simple, lorsqu'une fonction re\u00e7oit un tableau, il est utile de passer la taille du tableau en param\u00e8tre, ceci permet de ne pas d\u00e9border du tableau. La fonction suivante re\u00e7oit un tableau de 5 entiers, pass\u00e9 par r\u00e9f\u00e9rence\u2009:

    void function(int array[5]);\n

    N\u00e9anmoins, la taille du tableau n'est pas n\u00e9cessaire, car un tableau est un pointeur et seul l'adresse du premier \u00e9l\u00e9ment et le type des \u00e9l\u00e9ments sont n\u00e9cessaires pour calculer les adresses des \u00e9l\u00e9ments suivants. La fonction suivante est donc strictement identique \u00e0 la pr\u00e9c\u00e9dente\u2009:

    void function(int array[]);\n

    N\u00e9anmoins, pour un tableau multidimensionnel, il est n\u00e9cessaire de passer la taille des dimensions suivantes afin de pouvoir calculer les adresses. En effet pour un tableau de 3x4x5 d\u00e9clar\u00e9 int array[3][4][5], si on souhaite acc\u00e9der \u00e0 l'\u00e9l\u00e9ment array[2][3][4], le compilateur va effectuer le calcul suivant\u2009:

    int* p = array + 2 * sizeof(int[4][5]) + 3 * sizeof(int[5]) + 4 * sizeof(int);\n     p = array + 2 * (4 * 5 * 4)       + 3 * (5 * 4)        + 4 * (4);\n     p = array + 2 * (80)              + 3 * (20)           + 4 * 4;\n

    Le seul \u00e9l\u00e9ment qui n'est pas n\u00e9cessaire c'est la premi\u00e8re dimension.

    "}, {"location": "course-c/20-composite-types/arrays/#allocation-memoire", "title": "Allocation m\u00e9moire", "text": "

    Jusqu'ici lorsque l'on d\u00e9clarait un tableau, au sein d'une fonction, il est d\u00e9clar\u00e9 sur la pile. Cette derni\u00e8re est limit\u00e9e en taille et il est possible de d\u00e9border et d'obtenir l'erreur tant redout\u00e9e stack smashing detected aussi appel\u00e9e stack overflow.

    En pratique lorsque l'on a besoin de grands espaces m\u00e9moire, il est pr\u00e9f\u00e9rable de d\u00e9clarer le tableau sur le tas (allocation dynamique) ou en variable globale.

    char a[1024];           // D\u00e9claration globale dans le segment de donn\u00e9es\n\nint main() {\n    char b[1024];           // D\u00e9claration sur la pile\n    char* c = malloc(1024); // D\u00e9claration sur le tas\n    free(c);                // Lib\u00e9ration de la m\u00e9moire\n}\n

    Ces trois d\u00e9clarations sont \u00e9quivalentes en termes de taille et d'utilisation du tableau. N\u00e9anmoins, leur utilisation est diff\u00e9rente.

    D\u00e9claration globale

    La m\u00e9moire est allou\u00e9e lors de la compilation et est disponible tout au long de l'ex\u00e9cution du programme. Il est possible de modifier le contenu de la m\u00e9moire \u00e0 tout moment. La visibilit\u00e9 de la variable est globale, c'est \u00e0 dire que la variable est accessible depuis n'importe quelle fonction du programme ce qui peut \u00eatre source de bogues.

    D\u00e9claration sur la pile

    La m\u00e9moire est allou\u00e9e lors de l'appel de la fonction et est lib\u00e9r\u00e9e \u00e0 la fin de la fonction. C'est une excellente m\u00e9thode pour de petits tableaux mais comme la pile (stack) \u00e0 une taille limit\u00e9e, cette m\u00e9thode ne doit pas \u00eatre utilis\u00e9e pour de grands tableaux.

    D\u00e9claration sur le tas

    La m\u00e9moire est allou\u00e9e dynamiquement lors de l'ex\u00e9cution du programme. La m\u00e9moire est disponible jusqu'\u00e0 ce que le programme lib\u00e8re l'espace m\u00e9moire. C'est la m\u00e9thode la plus flexible mais elle n'est pas utilisable sur des architectures embarqu\u00e9es car l'allocation dynamique de m\u00e9moire peut \u00eatre source de fragmentation de la m\u00e9moire et de risque de fuite m\u00e9moire.

    "}, {"location": "course-c/20-composite-types/arrays/#tableau-de-longueur-variable-vla", "title": "Tableau de longueur variable (VLA)", "text": "

    Un VLA (Variable Length Array) est un tableau dont la taille est d\u00e9termin\u00e9e \u00e0 l'ex\u00e9cution du programme. Cette fonctionnalit\u00e9 est disponible depuis le standard C99.

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n

    C'est une fonctionnalit\u00e9 qui peut \u00eatre tr\u00e8s utile, mais elle n'est pas sans risque. En effet, la taille du tableau est d\u00e9termin\u00e9e \u00e0 l'ex\u00e9cution du programme et il est possible de d\u00e9border la pile si les pr\u00e9cautions n\u00e9cessaires ne sont pas prises.

    D\u00e9bordement de pile

    L'exemple suivant illustre un d\u00e9bordement de pile. La fonction foo alloue un tableau de n \u00e9l\u00e9ments. Si n est tr\u00e8s grand, il est possible de d\u00e9border la pile et de provoquer un crash du programme.

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n\nint main() {\n    foo(1000000);\n}\n

    Dans cet exemple, la fonction foo alloue un tableau de 4 millions d'octets (4 Mo) sur la pile. Si la taille de la pile est de 1 Mo, le programme crashera.

    \u00c0 priori l'utilisateur ne sait pas ce que fait la fonction foo et il ne sait donc pas qu'un VLA est allou\u00e9 sur la pile.

    Pire, on pourrait imaginer le programme suivant\u2009:

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n\nint main() {\n    unsigned int n;\n    scanf(\"%u\", &n);\n    foo(n);\n}\n

    Cette fois, l'utilisateur peut choisir la taille du tableau allou\u00e9 sur la pile. Vous voyez le probl\u00e8me\u2009?

    Pour les raisons \u00e9voqu\u00e9es, les VLA sont tr\u00e8s controvers\u00e9s et il est recommand\u00e9 de ne pas les utiliser. Il est pr\u00e9f\u00e9rable d'utiliser l'allocation dynamique de m\u00e9moire pour allouer des tableaux de taille variable. Du reste, avec C11, le support des VLA sont devenus optionnels, c'est \u00e0 dire qu'un compilateur peut \u00eatre compatible C11 sans supporter les VLA.

    ", "tags": ["foo"]}, {"location": "course-c/20-composite-types/arrays/#copie-dun-tableau", "title": "Copie d'un tableau", "text": "

    Imagions le code suivant\u2009:

    char a[5] = {1, 2, 3, 4, 5};\nchar b[5] = {0};\n

    Pour copier le contenu du tableau a dans le tableau b, il est n\u00e9cessaire de copier chaque \u00e9l\u00e9ment un par un. Il n'existe pas d'affectation directe entre deux tableaux.

    void copy(char dest[], char src[], size_t size) {\n    for (size_t i = 0; i < size; i++)\n        dest[i] = src[i];\n}\n

    Notez ici que la d\u00e9claration de la fonction utilise la notation [] pour les tableaux car la taille des donn\u00e9es \u00e0 copier n'est pas connue \u00e0 la compilation. C'est pour cette raison qu'on utilise un param\u00e8tre size pour indiquer la taille des tableaux. Ainsi pour copier a dans b, il suffit d'appeler la fonction copy :

    copy(b, a, 5);\n

    En pratique on utilisera la fonction memcpy de la biblioth\u00e8que standard qui est plus rapide et plus s\u00fbre que la fonction copy que nous avons \u00e9crite.

    #include <string.h>\n\nmemcpy(b, a, 5);\n
    ", "tags": ["copy", "memcpy", "size"]}, {"location": "course-c/20-composite-types/arrays/#exercices", "title": "Exercices", "text": "

    Exercice 1\u2009: Assignation

    \u00c9crire un programme qui lit la taille d'un tableau de cinquante entiers de 8 bytes et assigne \u00e0 chaque \u00e9l\u00e9ment la valeur de son indice.

    Solution
    int8_t a[50];\nfor (size_t i = 0; i < sizeof(a) / sizeof(a[0]; i++) {\n    a[i] = i;\n}\n

    Exercice 2\u2009: Premi\u00e8re position

    Soit un tableau d'entiers, \u00e9crire une fonction retournant la position de la premi\u00e8re occurrence d'une valeur dans le tableau.

    Traitez les cas particuliers.

    int index_of(int *array, size_t size, int search);\n
    Solution
    int index_of(int *array, size_t size, int search) {\n    int i = 0;\n    while (i < size && array[i++] != search);\n    return i == size ? -1 : i;\n}\n

    Exercice 3\u2009: D\u00e9clarations de tableaux

    Consid\u00e9rant les d\u00e9clarations suivantes\u2009:

    #define LIMIT 10\nconst int twelve = 12;\nint i = 3;\n

    Indiquez si les d\u00e9clarations suivantes (qui n'ont aucun lien entre elles), sont correctes ou non.

    int t(3);\nint k, t[3], l;\nint i[3], l = 2;\nint t[LIMITE];\nint t[i];\nint t[douze];\nint t[LIMITE + 3];\nfloat t[3, /* five */ 5];\nfloat t[3]        [5];\n

    Exercice 4\u2009: Comparaisons

    Soit deux tableaux char u[] et char v[], \u00e9crire une fonction comparant leur contenu et retournant\u2009:

    0 La somme des deux tableaux est \u00e9gale.

    -1 La somme du tableau de gauche est plus petite que le tableau de droite

    1 La somme du tableau de droite est plus grande que le tableau de gauche

    Le prototype de la fonction \u00e0 \u00e9crire est\u2009:

    int comp(char a[], char b[], size_t length);\n
    Solution
    int comp(char a[], char b[], size_t length) {\n    int sum_a = 0, sum_b = 0;\n\n    for (size_t i = 0; i < length; i++) {\n        sum_a += a[i];\n        sum_b += b[i];\n    }\n\n    return sum_b - sum_a;\n}\n

    Exercice 5\u2009: Le plus grand et le plus petit

    Dans le canton de Gen\u00e8ve, il existe une tradition ancestrale\u2009: l'Escalade. En comm\u00e9moration de la victoire de la r\u00e9publique protestante sur les troupes du duc de Savoie suite \u00e0 l'attaque lanc\u00e9e contre Gen\u00e8ve dans la nuit du 11 au 12 d\u00e9cembre 1602 (selon le calendrier julien), une traditionnelle marmite en chocolat est bris\u00e9e par l'ain\u00e9 et le cadet apr\u00e8s la r\u00e9citation de la phrase rituelle \u00ab\u2009Ainsi p\u00e9rirent les ennemis de la R\u00e9publique\u2009!\u2009\u00bb.

    Pour gagner du temps et puisque l'assembl\u00e9e est grande, il vous est demand\u00e9 d'\u00e9crire un programme pour identifier le doyen et le benjamin de l'assistance.

    Un fichier contenant les ann\u00e9es de naissance de chacun vous est donn\u00e9, il ressemble \u00e0 ceci\u2009:

    1931\n1986\n1996\n1981\n1979\n1999\n2004\n1978\n1964\n

    Votre programme sera ex\u00e9cut\u00e9 comme suit\u2009:

    $ cat years.txt | marmite\n2004\n1931\n

    Exercice 6\u2009: L'index magique

    Un indice magique d'un tableau A[0..n-1] est d\u00e9fini tel que la valeur A[i] == i. \u00c9tant donn\u00e9 que le tableau est tri\u00e9 avec des entiers distincts (sans r\u00e9p\u00e9tition), \u00e9crire une m\u00e9thode pour trouver un indice magique s'il existe.

    Exemple\u2009:

        0   1   2   3   4   5   6   7   8   9   10\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502-90\u2502-33\u2502 -5\u2502 1 \u2502 2 \u2502 4 \u2502 5 \u2502 7 \u2502 10\u2502 12\u2502 14\u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n                                ^\n
    Solution

    Une solution triviale consiste \u00e0 it\u00e9rer tous les \u00e9l\u00e9ments jusqu'\u00e0 trouver l'indice magique\u2009:

    int magic_index(int[] array) {\n    const size_t size = sizeof(array) / sizeof(array[0]);\n\n    size_t i = 0;\n\n    while (i < size && array[i] != i) i++;\n\n    return i == size ? -1 : i;\n}\n

    La complexit\u00e9 de cet algorithme est :math\u2009:O(n) or, la donn\u00e9e du probl\u00e8me indique que le tableau est tri\u00e9. Cela veut dire que probablement, cette information n'est pas donn\u00e9e par hasard.

    Pour mieux se repr\u00e9senter le probl\u00e8me, prenons l'exemple d'un tableau\u2009:

        0   1   2   3   4   5   6   7   8   9   10\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502-90\u2502-33\u2502 -5\u2502 1 \u2502 2 \u2502 4 \u2502 5 \u2502 7 \u2502 10\u2502 12\u2502 14\u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n                                ^\n

    La premi\u00e8re valeur magique est 7. Est-ce qu'une approche dichotomique est possible\u2009?

    Prenons le milieu du tableau A[5] = 4. Est-ce qu'une valeur magique peut se trouver \u00e0 gauche du tableau\u2009? Dans le cas le plus favorable qui serait\u2009:

        0   1   2   3   4\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 -1\u2502 0 \u2502 1 \u2502 2 \u2502 3 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n

    On voit qu'il est impossible que la valeur se trouve \u00e0 gauche, car les valeurs dans le tableau sont distinctes et il n'y a pas de r\u00e9p\u00e9titions. La r\u00e8gle que l'on peut poser est A[mid] < mid o\u00f9 mid est la valeur m\u00e9diane.

    Il est possible de r\u00e9p\u00e9ter cette approche de fa\u00e7on dichotomique\u2009:

    int magic_index(int[] array) {\n    return _magic_index(array, 0, sizeof(array) / sizeof(array[0]) - 1);\n}\n\nint _magic_index(int[] array, size_t start, size_t end) {\n    if (end < start) return -1;\n    int mid = (start + end) / 2;\n    if (array[mid] == mid) {\n        return mid;\n    } else if (array[mid] > mid) {\n        return _magic_index(array, start, mid - 1);\n    } else {\n        return _magic_index(array, mid + 1, end);\n    }\n}\n

    Exercice 7\u2009: D\u00e9tectives priv\u00e9s

    Voici les d\u00e9penses de service annuelles d'un c\u00e9l\u00e8bre bureau de d\u00e9tectives priv\u00e9s\u2009:

    Mois Bosley Sabrina Jill Kelly Janvier 414.38 222.72 99.17 153.81 F\u00e9vrier 403.41 390.61 174.39 18.11 Mars 227.55 73.86 291.08 416.55 Avril 220.20 342.25 139.45 86.98 Mai 13.46 172.66 252.33 265.32 Juin 259.37 378.72 173.02 208.43 Juillet 327.06 16.53 391.05 266.84 Ao\u00fbt 50.82 3.37 201.71 170.84 Septembre 450.78 9.33 111.63 337.07 Octobre 434.45 77.80 459.46 479.17 Novembre 420.13 474.69 343.64 273.28 D\u00e9cembre 147.76 250.73 201.47 9.75

    Afin de laisser plus de temps aux d\u00e9tectives \u00e0 r\u00e9soudre des affaires, vous \u00eates mandat\u00e9 pour \u00e9crire une fonction qui re\u00e7oit en param\u00e8tre le tableau de r\u00e9els ci-dessus format\u00e9 comme suit\u2009:

    double accounts[][] = {\n    {414.38, 222.72,  99.17, 153.81, 0},\n    {403.41, 390.61, 174.39, 18.11,  0},\n    {227.55,  73.86, 291.08, 416.55, 0},\n    {220.20, 342.25, 139.45, 86.98,  0},\n    {13.46 , 172.66, 252.33, 265.32, 0},\n    {259.37, 378.72, 173.02, 208.43, 0},\n    {327.06,  16.53, 391.05, 266.84, 0},\n    {50.82 ,   3.37, 201.71, 170.84, 0},\n    {450.78,   9.33, 111.63, 337.07, 0},\n    {434.45,  77.80, 459.46, 479.17, 0},\n    {420.13, 474.69, 343.64, 273.28, 0},\n    {147.76, 250.73, 201.47, 9.75,   0},\n    {  0,      0,      0,    0,      0}\n};\n

    Et laquelle compl\u00e8te les valeurs manquantes.

    Exercice 8\u2009: Pot de peinture

    \u00c0 l'instar de l'outil pot de peinture des \u00e9diteurs d'image, il vous est demand\u00e9 d'impl\u00e9menter une fonctionnalit\u00e9 similaire.

    L'image est repr\u00e9sent\u00e9e par un tableau bidimensionnel contenant des couleurs index\u00e9es\u2009:

    typedef enum { BLACK, RED, PURPLE, BLUE, GREEN YELLOW, WHITE } Color;\n\n#if 0 // Image declaration example\nColor image[100][100];\n#endif\n\nboolean paint(Color* image, size_t rows, size_t cols, Color fill_color);\n

    Hint

    Deux approches int\u00e9ressantes sont possibles\u2009: DFS (Depth-First-Search) ou BFS (Breadth-First-Search), toutes deux r\u00e9cursives.

    ", "tags": ["mid"]}, {"location": "course-c/20-composite-types/generic-programming/", "title": "Programmation g\u00e9n\u00e9rique", "text": "

    La programmation g\u00e9n\u00e9rique est une technique de programmation qui permet de d\u00e9finir des algorithmes et des structures de donn\u00e9es qui peuvent \u00eatre utilis\u00e9s avec diff\u00e9rents types de donn\u00e9es. En C, la programmation g\u00e9n\u00e9rique est r\u00e9alis\u00e9e \u00e0 l'aide de macros, de types et de fonctions inline.

    \u00c9crire un code g\u00e9n\u00e9rique est le Graal de tout programmeur. Cela permet de r\u00e9utiliser du code et de le rendre plus robuste. En effet, un code g\u00e9n\u00e9rique est un code qui peut \u00eatre utilis\u00e9 avec diff\u00e9rents types de donn\u00e9es et diff\u00e9rentes configuration sans modification. Cela permet de r\u00e9duire la duplication de code et d'am\u00e9liorer la maintenabilit\u00e9 du code.

    N\u00e9anmoins, la programmation g\u00e9n\u00e9rique en C est limit\u00e9e par le fait que le langage n'est pas orient\u00e9 objet et ne poss\u00e8de pas de couche de m\u00e9ta-programmation comme les templates en C++. Certains d\u00e9veloppeurs ont \u00e9t\u00e9 jusqu'\u00e0 inventer un nouveau langage comme le Vala pour palier \u00e0 ces limitations. Vala est un meta-langage qui g\u00e9n\u00e8re du code C \u00e0 partir de code Vala. Il apporte le paradigme de la programmation orient\u00e9e objet et de la programmation g\u00e9n\u00e9rique \u00e0 C. Cependant, Vala n'est pas un langage tr\u00e8s r\u00e9pandu en dehors de la communaut\u00e9 GNOME.

    "}, {"location": "course-c/20-composite-types/generic-programming/#fonctions-generiques", "title": "Fonctions g\u00e9n\u00e9riques", "text": "

    Prenons l'exemple de la fonction d'addition suivante. Elle est \u00e9crite pour des entiers et ne fonctionnera donc pas pour des flottants. Il faudrait la r\u00e9\u00e9crire pour les flottants mais cela implique une collision de nom de fonction. Il faudrait alors d\u00e9finir autant de fonctions que de types avec des suffixes diff\u00e9rents (add_int, add_float, etc.).

    int add(int a, int b) { return a + b; }\n

    Alternativement, on peut utiliser des macros pour d\u00e9finir des fonctions g\u00e9n\u00e9riques. Par exemple, la macro suivante permet de d\u00e9finir une fonction d'addition pour n'importe quel type de donn\u00e9es\u2009:

    #define add(a, b) ((a) + (b))\n

    N\u00e9anmoins cette macro ne fonctionne que pour des fonctions simples. Pour des fonctions plus complexes, il est n\u00e9cessaire de d\u00e9finir des fonctions s\u00e9par\u00e9es pour chaque type de donn\u00e9es comme discut\u00e9 pr\u00e9c\u00e9demment\u2009:

    int add_int(int a, int b) { return a + b; }\nfloat add_float(float a, float b) { return a + b; }\n

    Pour obtenir un comportement g\u00e9n\u00e9rique, on peut utiliser le mot-cl\u00e9 _Generic introduit dans C11. _Generic permet de d\u00e9finir une fonction en fonction du type de donn\u00e9es pass\u00e9 en param\u00e8tre. Par exemple, la fonction suivante permet d'additionner des entiers ou des flottants\u2009:

    #define add(a, b) _Generic((a), \\\n    int: add_int, \\\n    float: add_float \\\n)(a, b)\n

    Le fonctionnement de _Generic est le suivant\u2009: le premier argument est une expression qui est \u00e9valu\u00e9e pour d\u00e9terminer le type de donn\u00e9es. Ensuite, _Generic compare le type de donn\u00e9es avec les types d\u00e9finis dans la liste. Si le type de donn\u00e9es correspond \u00e0 un type d\u00e9fini, alors la fonction correspondante est appel\u00e9e avec les arguments a et b. L'exemple donn\u00e9 n'est pas tr\u00e8s pertinent car la solution simple avec une macro est plus simple et plus lisible. N\u00e9anmoins, _Generic est tr\u00e8s utile pour des fonctions plus complexes. Prenons l'exemple de la fonction print qui affiche un entier ou un flottant\u2009:

    #include <stdio.h>\n\nvoid print_int(int x) { printf(\"Entier : %d\\n\", x); }\nvoid print_float(float x) { printf(\"Flottant : %.2f\\n\", x); }\nvoid print_string(const char *x) { printf(\"Cha\u00eene : %s\\n\", x); }\n\n#define print(x) _Generic((x), \\\n    int: print_int, \\\n    float: print_float, \\\n    const char*: print_string \\\n)(x)\n\nint main() {\n    int a = 10;\n    float b = 5.5;\n    const char *c = \"Bonjour\";\n\n    print(a);\n    print(b);\n    print(c);\n}\n

    Dans le standard C, l'en-t\u00eate <tgmath.h> fournit par exemple des fonctions g\u00e9n\u00e9riques pour les fonctions math\u00e9matiques. Il n'y a donc plus \u00e0 ce soucier du type de donn\u00e9es pass\u00e9 en param\u00e8tre ce qui pourrait en th\u00e9orie cr\u00e9er moins d'erreurs de programmation.

    ", "tags": ["_Generic", "add_float", "add_int", "print"]}, {"location": "course-c/20-composite-types/generic-programming/#tableaux-a-taille-variable", "title": "Tableaux \u00e0 taille variable", "text": "

    Nous avons vu qu'il est parfaitement correct d'\u00e9crire\u2009:

    int sum(int array[42]);\n

    Cependant, cette fonction n'est valable que pour des tableaux de taille fixe \u00e0 42 \u00e9l\u00e9ments. Pour des tableaux de taille variable, il est n\u00e9cessaire de passer la taille du tableau en param\u00e8tre.

    int sum(int *array, size_t size);\n

    Notez que la notation est pass\u00e9e de int array[] \u00e0 int *array. Cela met l'accent sur le fait que array est un pointeur et non un type tableau. L'avantage du pointeur est que l'on peut le parcourir avec une boucle for en utilisant l'arithm\u00e9tique des pointeurs.

    int sum(int *array, size_t size) {\n   int sum = 0, *end = array + size;\n   while (array < end) sum += *(array++);\n   return sum;\n}\n
    ", "tags": ["array", "for"]}, {"location": "course-c/20-composite-types/generic-programming/#fonction-de-callback", "title": "Fonction de callback", "text": "

    Dans des algorithmes comme le tri d'un tableau. Il est possible de parcourir le tableau facilement si l'on connait sa taille et la taille du type de donn\u00e9e. Cependant, pour comparer deux valeurs, il est n\u00e9cessaire de conna\u00eetre la m\u00e9thode de comparaison car elle d\u00e9pend du type. On peut utiliser _Generic mais cela fonctionnera que pour des types simples et connus.

    Une fonction de tri g\u00e9n\u00e9rique ne peut pas conna\u00eetre tous les types de donn\u00e9es. Par exemple imaginons un type comme \u00e9tant une structure contenant une personne\u2009:

    typedef struct {\n    char *name;\n    int age;\n} Person;\n

    Trier un tableau de personnes demande de savoir comment comparer deux personnes. On peut imaginer une fonction de comparaison qui compare deux personnes en fonction de leur \u00e2ge ou de leur nom\u2009:

    int compare_age(Person a, Person b) { return a.age - b.age; }\nint compare_name(Person a, Person b) { return strcmp(a.name, b.name); }\n

    L'astuce consiste \u00e0 passer une fonction de comparaison en param\u00e8tre de la fonction de tri. Cela permet de trier n'importe quel type de donn\u00e9es. Par exemple, la fonction de tri suivante permet de trier un tableau de n'importe quel type de donn\u00e9es\u2009:

    void swap(void *a, void *b, size_t size) {\n    char tmp[size];\n    memcpy(tmp, a, size);\n    memcpy(a, b, size);\n    memcpy(b, tmp, size);\n}\n\nvoid sort(void *array, size_t size, size_t count,\n    int (*comp)(const void *, const void *))\n{\n    for (size_t i = 0; i < count; i++) {\n        for (size_t j = i + 1; j < count; j++) {\n            void *a = (char *)array + i * size;\n            void *b = (char *)array + j * size;\n            if (comp(a, b) > 0) swap(a, b, size);\n        }\n    }\n}\n

    L'utilisation de fonctions de callback est un concept fondamental en programmation g\u00e9n\u00e9rique. En C il implique le plus souvent de passer par un pointeur g\u00e9n\u00e9rique void * afin de pouvoir d\u00e9finir un prototype de fonction \u00e9galement g\u00e9n\u00e9rique.

    La biblioth\u00e8que standard C fournit plusieurs fonctions g\u00e9n\u00e9riques telles que\u2009:

    // Copie d'une r\u00e9gion m\u00e9moire\nvoid memcpy(void *dest, const void *src, size_t n);\n// Tri rapide d'un tableau\nvoid qsort(void *base, size_t nmemb, size_t size,\n    int (*compar)(const void *, const void *));\n// Recherche dichotomique\nvoid bsearch(const void *key, const void *base, size_t nmemb, size_t size,\n    int (*compar)(const void *, const void *));\n
    ", "tags": ["_Generic"]}, {"location": "course-c/20-composite-types/generic-programming/#types-de-donnees-abstraits", "title": "Types de donn\u00e9es abstraits", "text": "

    Un type de donn\u00e9e abstrait (ADT pour Abstract Data Type) cache g\u00e9n\u00e9ralement une structure dont le contenu n'est pas connu de l'utilisateur final. Ceci est rendu possible par le standard (C99 \u00a76.2.5) par l'usage de types incomplets.

    Pour m\u00e9moire, un type incomplet d\u00e9crit un objet dont on ne conna\u00eet pas sa taille en m\u00e9moire. L'exemple suivant d\u00e9clare un nouveau type structure qui n'est alors pas (encore) connu dans le fichier courant\u2009:

    typedef struct Unknown *Known;\n\nint main() {\n    Known foo; // Autoris\u00e9, le type est incomplet\n\n    foo + 1; // Impossible car la taille de foo est inconnue.\n    foo->key; // Impossible car le type est incomplet.\n}\n

    De fa\u00e7on g\u00e9n\u00e9rale, les types abstraits sont utilis\u00e9s dans l'\u00e9criture de biblioth\u00e8ques logicielles lorsqu'il est important que l'utilisateur final ne puisse pas compromettre le contenu du type et en for\u00e7ant cet utilisateur \u00e0 ne passer que par des fonctions d'acc\u00e8s.

    Prenons le cas du fichier foobar.c lequel d\u00e9crit une structure struct Foo et un type Foo. Notez que le type peut \u00eatre d\u00e9clar\u00e9 avant la structure. Foo restera abstrait jusqu'\u00e0 la d\u00e9claration compl\u00e8te de la structure struct Foo permettant de conna\u00eetre sa taille. Ce fichier contient \u00e9galement trois fonctions\u2009:

    • init permet d'initialiser la structure\u2009;
    • get permet de r\u00e9cup\u00e9rer la valeur contenue dans Foo ;
    • set permet d'assigner une valeur \u00e0 Foo.

    En plus, il existe un compteur d'acc\u00e8s count qui s'incr\u00e9mente lorsque l'on assigne une valeur et se d\u00e9cr\u00e9mente lorsque l'on r\u00e9cup\u00e8re une valeur.

    #include <stdlib.h>\n\ntypedef struct Foo Foo;\n\nstruct Foo {\n    int value;\n    int count;\n};\n\nvoid init(Foo** foo) {\n    *foo = malloc(sizeof(Foo)); // Allocation dynamique\n    (*foo)->count = (*foo)->value = 0;\n}\n\nint get(Foo* foo) {\n    foo->count--;\n    return foo->value;\n}\n\nvoid set(Foo* foo, int value) {\n    foo->count++;\n    foo->value = value;\n}\n

    \u00c9videmment, on ne souhaite pas qu'un petit malin compromette ce compteur en \u00e9crivant maladroitement\u2009:

    foo->count = 42; // Hacked this !\n

    Pour s'en prot\u00e9ger, on a recours \u00e0 la compilation s\u00e9par\u00e9e (voir chapitre sur la compilation s\u00e9par\u00e9e [TranslationUnits]) dans laquelle le programme est d\u00e9coup\u00e9 en plusieurs fichiers. Le fichier foobar.h contiendra tout ce qui doit \u00eatre connu du programme principal, \u00e0 savoir les prototypes des fonctions, et le type abstrait\u2009:

    #pragma once\n\ntypedef struct Foo Foo;\n\nvoid init(Foo** foo);\nint get(Foo* foo);\nvoid set(Foo* foo, int value);\n

    Ce fichier sera inclus dans le programme principal main.c :

    #include \"foobar.h\"\n#include <stdio.h>\n\nint main() {\n    Foo *foo;\n\n    init(&foo);\n    set(foo, 23);\n    printf(\"%d\\n\", get(foo));\n}\n

    Un type abstrait peut \u00eatre vu comme une bo\u00eete noire et est par cons\u00e9quent une technique de programmation g\u00e9n\u00e9rique. Il est possible de d\u00e9finir des types abstraits pour des structures plus complexes comme des listes cha\u00een\u00e9es, des arbres, des graphes, mais que l'utilisateur final ne doit pas n\u00e9cessairement conna\u00eetre.

    ", "tags": ["Foo", "init", "get", "abstract-data-type", "foobar.h", "set", "foobar.c", "count", "main.c"]}, {"location": "course-c/20-composite-types/pointers/", "title": "Pointers", "text": "", "tags": ["gps_position", "lettre"]}, {"location": "course-c/20-composite-types/pointers/#pointeurs", "title": "Pointeurs", "text": "

    Attention les v\u00e9los, nous nous aventurons aujourd'hui sur un terrain d\u00e9licat, subtil et parfois d\u00e9routant, mais \u00f4 combien fondamental pour quiconque aspire \u00e0 ma\u00eetriser l'art de la programmation. Un sujet d'une importance cruciale, presque incontournable\u2009: les pointeurs.

    Les pointeurs sont des variables d'une nature singuli\u00e8re\u2009: au lieu de contenir directement une valeur, ils stockent une adresse m\u00e9moire. \u00c0 quoi bon, me demanderez-vous\u2009? L'objectif est de permettre des indirections, d'optimiser la gestion des donn\u00e9es et de rendre l'ex\u00e9cution du code plus fluide et efficiente.

    Imaginons une sc\u00e8ne tir\u00e9e des intrigues galantes du XVIIIe si\u00e8cle. Le Vicomte de Valmont, s\u00e9ducteur inv\u00e9t\u00e9r\u00e9, s'appr\u00eate \u00e0 \u00e9crire une missive enflamm\u00e9e \u00e0 la Marquise de Merteuil. Apr\u00e8s avoir r\u00e9dig\u00e9 sa lettre, il la scelle soigneusement avant de la d\u00e9poser dans sa bo\u00eete aux lettres, en esp\u00e9rant que le facteur l'acheminera \u00e0 bon port. Ce simple geste pourrait se traduire dans le langage des machines par la d\u00e9claration suivante\u2009:

    char lettre[] = \"Ch\u00e8re Marquise, ...\";\n

    Cette variable lettre est alors stock\u00e9e en m\u00e9moire \u00e0 une adresse sp\u00e9cifique, disons 0x12345abc, qui correspondrait \u00e0 l'emplacement de la bo\u00eete aux lettres du Vicomte dans ce grand r\u00e9seau qu'est la m\u00e9moire.

    Le facteur, fid\u00e8le \u00e0 son devoir mais pas \u00e0 l'abri des al\u00e9as du quotidien, d\u00e9couvre avec horreur que la chaleur \u00e9touffante a fait fondre le sceau de cire, collant irr\u00e9m\u00e9diablement la lettre au fond de la bo\u00eete. En s'effor\u00e7ant de la d\u00e9tacher, il finit par la d\u00e9chirer, r\u00e9v\u00e9lant par inadvertance son contenu.

    Bien entendu, il faut admettre que cette pirouette est une m\u00e9taphore pour illustrer le fait qu'une valeur en m\u00e9moire ne peut \u00eatre transport\u00e9e simplement. Notre pauvre facteur, dont la m\u00e9moire n'est plus ce qu'elle \u00e9tait, d\u00e9cide de m\u00e9moriser laborieusement les premiers mots de la lettre\u2009: Ch\u00e8re Ma. Il enfourche alors son v\u00e9lo tout nickel\u00e9, fait un premier voyage et les retranscrit dans la bo\u00eete de la Marquise de Merteuil. Cette op\u00e9ration se r\u00e9p\u00e8te encore et encore, jusqu'\u00e0 ce que la lettre soit enti\u00e8rement copi\u00e9e.

    Nous obtenons alors une copie de la lettre dans la bo\u00eete de la Marquise\u2009:

    char valmont_mailbox[] = \"Ch\u00e8re Marquise, ...\";\nchar merteuil_mailbox[] = \"Ch\u00e8re Marquise, ...\";\n

    Mais la chaleur persistante et les imperfections de cette m\u00e9thode ne satisfont gu\u00e8re la Marquise, qui d\u00e9cide de r\u00e9soudre le probl\u00e8me en se rendant \u00e0 Tarente \u2013 choix discutable en pleine canicule. L\u00e0-bas, elle grave sa r\u00e9ponse sur le mur sud du Castello Aragonese, prenant soin de noter avec pr\u00e9cision les coordonn\u00e9es GPS du mur\u2009:0x8F313233 (en r\u00e9alit\u00e9 8FGMPXJ7+2V selon l'OLC).

    char castello_wall[] = \"Cher Vicomte ...\";\nchar (*gps_position)[] = &castello_wall;\n

    De retour chez elle, elle confie au facteur l'adresse en m\u00e9moire (0x30313233), un message que celui-ci, soulag\u00e9, peut enfin retenir sans effort.

    Ainsi, la variable gps_position ne contient pas directement le message, mais uniquement l'adresse o\u00f9 celui-ci est stock\u00e9\u2009: un pointeur sur un tableau de caract\u00e8res.

    Pendant ce temps, le Vicomte, moins dispos\u00e9 \u00e0 l'effort, s'est muni d'un t\u00e9l\u00e9scripteur capable d'interpr\u00e9ter le code C. En r\u00e9cup\u00e9rant l'adresse fournie, il parvient \u00e0 lire le message de la Marquise\u2009:

    printf(\"%s\", *gps_position);\n

    S'il avait omis l'ast\u00e9risque (*, 002A) dans cette ligne, il n'aurait vu que 0123, l'adresse elle-m\u00eame, au lieu du message qu'elle contient.

    L'ast\u00e9risque joue donc un r\u00f4le essentiel\u2009: celui du d\u00e9r\u00e9f\u00e9rencement, c'est-\u00e0-dire l'acte de demander au facteur d'aller chercher le contenu \u00e0 l'adresse sp\u00e9cifi\u00e9e.

    Mais pourquoi donc avons-nous utilis\u00e9 l'esperluette (&, 0026) pour obtenir cette adresse\u2009: &castello_wall ? L'esperluette, pr\u00e9c\u00e9dant une variable, se traduit par l'adresse de cette variable, tout comme la Marquise avait relev\u00e9 la position GPS du mur.

    Quant \u00e0 l'ast\u00e9risque dans (*gps_position)[], il ne signifie pas un d\u00e9r\u00e9f\u00e9rencement dans ce contexte, mais participe \u00e0 la d\u00e9claration du pointeur. C'est souvent ici que les novices perdent le fil. Revenons \u00e0 l'essentiel.

    En C, l'ast\u00e9risque peut signifier plusieurs choses\u2009:

    1. Multiplication : a * b,
    2. D\u00e9r\u00e9f\u00e9rencement d'un pointeur : *ptr,
    3. D\u00e9claration d'un pointeur : int *ptr.

    Dans notre cas, nous d\u00e9clarons un pointeur. En appliquant la r\u00e8gle de lecture gauche-droite\u2009:

    char (*gps_position)[]\n       ^^^^^^^^^^^^        1. gps_position est\n                   ^       2. ...\n      ^                    3. un pointeur sur\n                    ^^     4. un tableau de\n^^^^                       5. caract\u00e8res\n                           6. PROFIT...\n

    En r\u00e9sum\u00e9\u2009:

    • Un pointeur est une variable.
    • Il contient une adresse m\u00e9moire.
    • Il peut \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9 pour obtenir la valeur de l'\u00e9l\u00e9ment point\u00e9.
    • L'adresse d'une variable peut \u00eatre obtenue avec une esperluette (&).

    Ainsi, nous voyons que les pointeurs, loin d'\u00eatre une simple abstraction technique, peuvent se r\u00e9v\u00e9ler de pr\u00e9cieux alli\u00e9s dans l'\u00e9criture d'un code \u00e0 la fois efficace et \u00e9l\u00e9gant.

    "}, {"location": "course-c/20-composite-types/pointers/#pointeur-simple", "title": "Pointeur simple", "text": "

    Le format le plus simple d'un pointeur sur un entier s'\u00e9crit avec l'ast\u00e9risque *:

    int *ptr = NULL;\n

    La valeur NULL correspond \u00e0 l'adresse nulle 0x00000000. On utilise cette convention et non 0 pour bien indiquer qu'il s'agit d'une adresse et non d'une valeur scalaire.

    nul, null, nulll

    Attention \u00e0 l'\u00e9criture de NULL:

    • nul est le caract\u00e8re de fin de cha\u00eene de caract\u00e8res '\\0' ;
    • null est une adresse qui pointe nulle part\u2009;
    • nulll veut dire que vous avez fait une faute de frappe.

    \u00c0 tout moment, la valeur du pointeur peut \u00eatre assign\u00e9e \u00e0 l'adresse d'un entier puisque nous avons d\u00e9clar\u00e9 un pointeur sur un entier. Dans l'exemple suivant, deux variables boiling et freezing sont d\u00e9clar\u00e9es et un pointeur ptr est assign\u00e9 soit \u00e0 l'adresse l'une, soit \u00e0 l'autre selon que i est pair ou impair\u2009:

    int boiling = 100;\nint freezing = 0;\n\nfor (char i = 0; i < 10; i++) {\n    int *ptr = i % 2 ? &boiling : &freezing;\n    printf(\"%d\", *ptr);\n}\n

    Lorsque nous avions vu les tableaux, nous \u00e9crivions pour d\u00e9clarer un tableau d'entiers la notation ci-dessous\u2009:

    int array[10] = {0,1,2,3,4,5,6,7,8,9};\n

    Vous ne le saviez pas, mais \ud834\udd3d plot twist \ud834\udd3d la variable array est un pointeur, et la preuve est que array peut \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9\u2009:

    printf(\"%d\", *array); // Affiche 0\n

    La diff\u00e9rence entre un tableau et un pointeur est la suivante\u2009:

    • Il n'est pas possible d'assigner une adresse \u00e0 un tableau
    • Il n'est pas possible d'assigner des valeurs \u00e0 un pointeur

    D'ailleurs, l'op\u00e9rateur crochet [] n'est rien d'autre qu'un sucre syntaxique et les deux codes suivants sont \u00e9quivalents\u2009:

    a[b] \u2261 *(a + b);\n

    Par ailleurs, et bien que ce soit une tr\u00e8s mauvaise id\u00e9e, il est tout \u00e0 fait possible d'\u00e9crire le code suivant puisque l'addition est commutative\u2009:

    assert(4[a] == a[4]);\n

    Asterix de gauche ou de droite\u2009?

    Lors de la d\u00e9claration d'un pointeur, l'ast\u00e9risque peut \u00eatre plac\u00e9 \u00e0 gauche ou \u00e0 droite du type. Les d\u00e9clarations suivantes sont \u00e9quivalentes\u2009:

    int *ptr;\nint* ptr;\nint*ptr;\n

    N\u00e9anmoins il est recommand\u00e9 de placer l'ast\u00e9risque \u00e0 droite du type pour \u00e9viter toute confusion. En effet, lorsque vous utilisez l'op\u00e9rateur virgule , pour d\u00e9clarer plusieurs variables, il est facile de penser que int* a, b; d\u00e9clare deux pointeurs, alors qu'en r\u00e9alit\u00e9 seul a est un pointeur.

    int* a, b;  // a est un pointeur, b est un entier\nint *a, *b; // a et b sont des pointeurs\n
    ", "tags": ["freezing", "ptr", "boiling", "NULL", "null", "nulll", "array", "nul"]}, {"location": "course-c/20-composite-types/pointers/#dereferencement-et-adresse", "title": "D\u00e9r\u00e9f\u00e9rencement et adresse", "text": "

    Avec les pointeurs, certains op\u00e9rateurs sont recycl\u00e9s pour de nouvelles fonctions. C'est d\u00e9routant au d\u00e9but mais le nombre de caract\u00e8re sp\u00e9ciaux dans la table ASCII \u00e9tant limit\u00e9, il faut bien faire avec. Dans le cas pr\u00e9cis des pointeurs deux op\u00e9rateurs sont \u00e0 conna\u00eetre\u2009:

    Op\u00e9rateurs de pointeurs Op\u00e9rateur Description * D\u00e9r\u00e9f\u00e9rencement & Adresse d'une variable"}, {"location": "course-c/20-composite-types/pointers/#adresse-dune-variable", "title": "Adresse d'une variable", "text": "

    L'op\u00e9rateur & utilis\u00e9 comme op\u00e9rateur unaire permet d'obtenir l'adresse m\u00e9moire d'une variable. C'est \u00e0 dire que si a est une variable, &a est l'adresse m\u00e9moire de cette variable. Par exemple\u2009:

    int a = 42;\nprintf(\"%p\", &a);  // Affiche l'adresse m\u00e9moire de la variable a\n

    Ici le %p est un format de cha\u00eene de caract\u00e8res pour afficher une adresse m\u00e9moire. L'adresse m\u00e9moire est affich\u00e9e en hexad\u00e9cimal. Par exemple 0x12345678. On pourrait aussi \u00e9crire de mani\u00e8re \u00e9quivalente\u2009:

    printf(\"0x%08lx\", (uintptr_t)&a);\n

    Le type uintptr_t est un type entier non sign\u00e9 qui est assez grand pour contenir une adresse m\u00e9moire. Il est d\u00e9fini dans le fichier d'en-t\u00eate stdint.h et il est g\u00e9n\u00e9ralement d\u00e9fini comme un alias pour unsigned long. Il est utilis\u00e9 g\u00e9n\u00e9ralement pour faire un cast d'une adresse m\u00e9moire en un entier.

    ", "tags": ["stdint.h", "uintptr_t"]}, {"location": "course-c/20-composite-types/pointers/#dereferencement", "title": "D\u00e9r\u00e9f\u00e9rencement", "text": "

    Le d\u00e9r\u00e9f\u00e9rencement correspond \u00e0 l'op\u00e9ration de lecture de la valeur point\u00e9e par un pointeur. C'est \u00e0 dire que si ptr est un pointeur sur un entier, *ptr est la valeur de cet entier. Dit autrement, si vous avez un post-it avec l'adresse d'un magasin de chaussures, la simple pocession de ce post-it ne vous permet pas d'avoir de nouvelles chaussures. Il vous faut vous rendre \u00e0 l'adresse indiqu\u00e9e pour obtenir les chaussures. C'est exactement ce que fait l'op\u00e9rateur *.

    char* shoe = \"Nike Mag\";\n\nprintf(\"%p\", shoe);  // Affiche d'une chaussure (p.ex. 0x12345678)\nprintf(\"%s\", *shoe);  // Affiche la chaussure (c-\u00e0-d. Nike Mag)\n

    Amusons-nous. Prenons le post-it de l'exemple pr\u00e9c\u00e9dent et d\u00e9cidons de le d\u00e9chirer et allons le cacher quelque part. Prenons l'adresse de cet emplacement et \u00e9crivons-la sur un nouveau post-it. Nous avons maintenant un post-it avec l'adresse d'un post-it qui contient l'adresse d'un magasin de chaussures. Pour obtenir les chaussures, il nous faut d\u00e9r\u00e9f\u00e9rencer deux fois\u2009:

    char *shoe = \"Nike Mag\";\nchar **p = &shoe;\n\nprintf(\"%s\", **p);  // Affiche la chaussure (c-\u00e0-d. Nike Mag)\n

    Cette op\u00e9ration est appel\u00e9e d\u00e9r\u00e9f\u00e9rencement multiple. Elle peut \u00eatre r\u00e9p\u00e9t\u00e9e autant de fois que n\u00e9cessaire\u2009:

    int a = 42;\nint *b = &a;\nint **c = &b;\nint ***d = &c;\nint ****e = &d;\n\nprintf(\"%d\", ****e);  // Affiche 42\nprintf(\"%d\", ***d);   // Affiche 42\nprintf(\"%d\", **c);    // Affiche 42\nprintf(\"%d\", *b);     // Affiche 42\nprintf(\"%d\", a);      // Affiche 42\n

    Comme l'op\u00e9ration & et la r\u00e9ciproque de l'op\u00e9ration *, il est possible de les combiner\u2009:

    int a = 42;\nint *b = &a;\nint c = *&*&*&*b;\n\nassert(c == a);\n
    ", "tags": ["ptr"]}, {"location": "course-c/20-composite-types/pointers/#arithmetique-de-pointeurs", "title": "Arithm\u00e9tique de pointeurs", "text": "

    Fondamentalement un pointeur n'est rien d'autre qu'une variable qui contient une adresse m\u00e9moire. D\u00e8s lors on peut imaginer vouloir incr\u00e9menter cette adresse pour r\u00e9v\u00e9ler la valeur suivante en m\u00e9moire. On dit qu'un pointeur est un ordinal.

    Dans l'exemple suivant, la boucle for affiche les caract\u00e8res de la cha\u00eene de caract\u00e8res str jusqu'\u00e0 ce qu'il rencontre le caract\u00e8re nul '\\0' :

    char str[] = \"Le vif z\u00e9phyr jubile sur les kumquats du clown gracieux\";\n\nfor (char* ptr = str; *ptr; ptr++) { putchar(*ptr); }\n

    En pratique cette \u00e9criture serait plus \u00e9l\u00e9gante avec une boucle while car le nombre d'it\u00e9ration est inconnu\u2009: il d\u00e9pend de la longueur de la cha\u00eene de caract\u00e8res. On pr\u00e9f\u00e8re donc\u2009:

    char* ptr = str;\nwhile (*ptr) { putchar(*ptr++); }\n

    Si nous devions nous arr\u00eater \u00e0 cette \u00e9tape, il serait tentant de croire que l'arithm\u00e9tique des pointeurs se r\u00e9duit \u00e0 une simple incr\u00e9mentation de l'adresse m\u00e9moire de 1 octet \u00e0 chaque \u00e9tape. Cependant, la r\u00e9alit\u00e9 est autrement plus nuanc\u00e9e. Vous \u00eates-vous jamais interrog\u00e9 sur la raison pour laquelle un pointeur est toujours associ\u00e9 \u00e0 un type, tel qu\u2019un int ou un char ? Apr\u00e8s tout, stocker une adresse m\u00e9moire ne requiert pas, en soi, de conna\u00eetre la nature de l'information qu'elle d\u00e9signe. Cette association r\u00e9pond pourtant \u00e0 deux exigences fondamentales.

    Assistance au compilateur\u2009: Le compilateur, pour produire le code machine appropri\u00e9, doit imp\u00e9rativement conna\u00eetre le type de la variable point\u00e9e. En effet, il doit savoir combien d'octets lire en m\u00e9moire pour obtenir la valeur lors d'un d\u00e9r\u00e9f\u00e9rencement. Ainsi, lorsqu\u2019un pointeur d\u00e9signe un double, le compilateur sait qu\u2019il devra lire 8 octets en m\u00e9moire pour restituer la valeur.

    Arithm\u00e9tique des pointeurs\u2009: Pour garantir une arithm\u00e9tique coh\u00e9rente, celle-ci est d\u00e9finie en fonction du type du pointeur. Si ptr pointe vers un entier, l'op\u00e9ration ptr++ n'incr\u00e9mente pas simplement l'adresse d\u2019un octet, mais bien de sizeof(int) octets. De m\u00eame, l'expression ptr + 2 augmente l'adresse de 2 * sizeof(int) octets.

    Ainsi, selon le type du pointeur, l'arithm\u00e9tique des pointeurs s'ajuste\u2009:

    int32_t *p = (int32_t *)1;\nassert(p + 2 == (int32_t *)9);\n\nint64_t *q = (int64_t *)1;\nassert(q + 2 == (int64_t *)17);\n

    Il convient de souligner que l'arithm\u00e9tique des pointeurs se limite aux op\u00e9rations d'addition et de soustraction. Les autres op\u00e9rations arithm\u00e9tiques, telles que la multiplication ou la division, ne sont pas d\u00e9finies pour les pointeurs.

    ", "tags": ["int", "str", "while", "ptr", "for", "double", "char"]}, {"location": "course-c/20-composite-types/pointers/#carre-magique", "title": "Carr\u00e9 magique", "text": "

    Prenons un autre exemple. Imaginons que l'on souhaite repr\u00e9senter le carr\u00e9 magique suivant\u2009:

    \u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 4 \u2502 9 \u2502 2 \u2502\n\u251c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2524\n\u2502 3 \u2502 5 \u2502 7 \u2502\n\u251c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2524\n\u2502 8 \u2502 1 \u2502 6 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n

    On peut le repr\u00e9senter en m\u00e9moire lin\u00e9airement et utiliser de l'arithm\u00e9tique de pointeur pour le dessiner. C'est \u00e0 dire qu'il n'est pas n\u00e9cessaire de d\u00e9clarer explicitement un tableau \u00e0 deux dimensions. On peut le repr\u00e9senter de la mani\u00e8re suivante\u2009:

    char magic[] = \"492\" \"357\" \"816\"; // \u00c9quivalent \u00e0 \"492357816\"\n\nchar *ptr = magic;\n\nfor (size_t row = 0; row < 3; ++row) {\n    for (size_t col = 0; col < 3; ++col)\n        putchar(*(ptr + row * 3 + col));\n    putchar('\\n');\n}\n

    Vous l'arez probablement remarqu\u00e9 l'\u00e9criture *(ptr + row * 3 + col) est \u00e9quivalente \u00e0 ptr[row * 3 + col]. N\u00e9anmoins pour pouvoir utiliser la notation bidiensionnelle, le compilateur doit conna\u00eetre la taille de la deuxi\u00e8me dimension, et celle-ci n'est pas explicitement d\u00e9clar\u00e9e. Si vous pr\u00e9f\u00e9rez la notation traditionnelle, l'exemple suivant est \u00e9quivalent \u00e0 celui ci-dessus\u2009:

    char magic[][3] = {\"792\", \"357\", \"816\"};\n\nfor (size_t row = 0; row < 3; row++) {\n    for (size_t col = 0; col < 3; col++)\n        putchar(magic[row][col]);\n    putchar('\\n');\n}\n

    On peut pousser l'exemple plus loin. Imaginons que vous avez des donn\u00e9es en m\u00e9moires, agenc\u00e9es lin\u00e9airements, mais que vous ne connaissez pas \u00e0 priori la taille de la matrice, c'est le cas de la d\u00e9claration char magic[] = \"492357816\";. Dans ce cas, seul la premi\u00e8re solution est envisageable. Cependant, la seconde est bien plus \u00e9l\u00e9gante et plus lisible. Peut-on avoir le beurre et l'argent du beurre\u2009? Oui, avec un peu de ruse.

    On peut d\u00e9clarer un nouveau pointeur de type tableau \u00e0 deux dimensions et le caster sur le pointeur magic. C'est \u00e0 dire que l'on va dire au compilateur que le pointeur magic est en r\u00e9alit\u00e9 un tableau \u00e0 deux dimensions. C'est une pratique courante en C.

    #include <stdio.h>\n#include <stdlib.h>\n#include <stdint.h>\n#include <assert.h>\n#include <string.h>\n\nchar *magic = \"492357816\";\nconst int size = 3;\n\nint main() {\n\n    assert(strlen(magic) == size * size);\n\n    char(*p)[size] = (char(*)[size])magic;\n    for (size_t row = 0; row < 3; row++) {\n        for (size_t col = 0; col < 3; col++)\n            putchar(p[row][col]);\n        putchar('\\n');\n    }\n}\n

    Tentons de d\u00e9cortiquer la d\u00e9claration ambigu\u00eb\u2009:

    char               // Type du pointeur\n    (*p)           // D\u00e9claration du pointeur\n        [size]     // Tableau de taille size\n=\n(char\n     (*)\n        [size])    // Identique mais via un cast\nmagic;\n

    Aprp\u00e8s l'assignation =, on a simplement le m\u00eame type permettant de transtyper le pointeur magic vers le type souhait\u00e9. C'est n\u00e9cessaire pour \u00e9viter que le compilateur ne g\u00e9n\u00e8re une alterte indiquant qu'un pointeur d'un certain type soit assign\u00e9 \u00e0 un pointeur d'un autre type. Dans le cast, le nom de la variable p est simplement ignor\u00e9.

    Avant l'assignation, la d\u00e9claration compliqu\u00e9e \u00e0 comprendre est la parenth\u00e8se (*p)[size]. En effet, le compilateur ne sait pas si *p[size] est un tableau de pointeurs ou un pointeur sur un tableau. Il est donc n\u00e9cessaire de placer des parenthjsons pour indiquer que *p est un pointeur sur un tableau de taille size. Rappelez-vous de la priorit\u00e9 des op\u00e9rateurs en C. L'op\u00e9rateur crochet [] a une priorit\u00e9 plus \u00e9lev\u00e9e que l'op\u00e9rateur ast\u00e9risque *. Donc\u2009:

    char *p[size]; // Tableau de taille size sur des pointeurs de type char\nchar (*p)[size]; // Pointeur sur un tableau de char de taille size\n

    Pour terminer l'exemple, on peut mentionner que l'on peut simplifier l'\u00e9criture en s'affranchissant du pointeur interm\u00e9diaire. On peut directement caster le pointeur magic dans la boucle for :

    putchar(((char(*)[size])magic)[row][col]);\n
    ", "tags": ["for", "size", "magic"]}, {"location": "course-c/20-composite-types/pointers/#cas-dun-tableau", "title": "Cas d'un tableau", "text": "

    Cela n'aura plus de secret pour vous, un tableau est un pointeur. N\u00e9anmoins il existe quelques subtilit\u00e9s. D'une part un tableau est un pointeur constant, c'est \u00e0 dire que l'adresse m\u00e9moire ne peut \u00eatre modifi\u00e9e. L'\u00e9criture suivante est donc incorrecte\u2009:

    int array[10];\n\nint *ptr = array;\nptr += 1; // Correct\n\narray += 1; // Erreur de compilation\n

    Prenons le cas d'un tableau \u00e0 deux dimensions\u2009:

    int array[3][3] = {\n    {4, 9, 2},\n    {3, 5, 7},\n    {8, 1, 6}\n};\n

    On peut r\u00e9sumer les types de pointeurs associ\u00e9s \u00e0 chaque \u00e9l\u00e9ment de ce tableau\u2009:

    Types de pointeurs associ\u00e9s \u00e0 un tableau \u00e0 deux dimensions D\u00e9claration Type du pointeur Description array int (*)[3] Pointeur sur un tableau de 3 entiers &array int (*)[3][3] Pointeur sur un tableau de 3 tableaux de 3 entiers array[0] int * Pointeur sur un entier array[0][0] int Entier

    Vous avez vonstat\u00e9 lors de l'exemple du carr\u00e9 magique qu'il n'\u00e9tait pas possible d'\u00e9crire magic[row][col] \u00e0 partir de la d\u00e9claration de pointeur char *magic car le compilateur ne connaissait pas la taille de la deuxi\u00e8me dimension. Dans le cas d'une d\u00e9claration de type int (*)[3] le compilateur sait que la deuxi\u00e8me dimension est de taille 3. On peut donc \u00e9crire array[row][col] sans probl\u00e8me.

    L'\u00e9criture &array m\u00e9rite une explication. En effet, &array est un pointeur sur un tableau de 3 tableaux de 3 entiers. C'est \u00e0 dire que &array est \u00e9quivalent \u00e0 int (*)[3][3]. C'est une subtilit\u00e9 de la syntaxe C. En effet, &array est l'adresse du tableau array qui est un tableau de 3 tableaux de 3 entiers, alors que array est simplement un pointeur sur un tableau de 3 entiers. Pourquoi cette subtilit\u00e9\u2009?

    Lorsque l'on utilise des pointeurs, l'objectif est d'utiliser l'arithm\u00e9tique de pointeurs pour parcourir les donn\u00e9es en m\u00e9moire. Donc pour un tableau on s'attends \u00e0 qu'ajouter une unit\u00e9 au pointeur nous m\u00e8ne \u00e0 la valeur suivante. Pour le cas int array[3][3], ajouter une unit\u00e9 au pointeur array nous m\u00e8ne \u00e0 la premi\u00e8re valeur du tableau suivant. C'est \u00e0 dire que array + 1 pointe sur le tableau array[1] et array + 2 pointe sur le tableau array[2], ce qui confirme le sucre syntaxique de l'op\u00e9rateur crochet [] qui est \u00e9quivalent \u00e0 l'arithm\u00e9tique de pointeur *(array + i).

    N\u00e9anmoins si on consid\u00e8re l'enti\u00e8ret\u00e9 de array et que l'on souhaite avec +1 aller apr\u00e8s le dernier \u00e9l\u00e9ment du tableau, il est n\u00e9cessaire de conna\u00eetre la taille totale du tableau c'est \u00e0 dire 9 \u00e9l\u00e9ments. Demander explicitement l'adresse de array retourne un pointeur sur un tableau de 3 tableaux de 3 entiers. C'est \u00e0 dire que &array + 1 pointe sur le tableau suivant de 3 tableaux de 3 entiers, car on pourrait tr\u00e8s bien imaginer array comme un \u00e9l\u00e9ment d'un tableau plus grand\u2009:

    int matrices[2][3][3] = {\n    {{4, 9, 2},\n        {3, 5, 7},\n        {8, 1, 6}},\n    {{1, 2, 3},\n        {4, 5, 6},\n        {7, 8, 9}}};\n\nint(*array)[3][3] = &(matrices[0]); // Parenth\u00e8ses facultatives [] > &\n
    ", "tags": ["int", "array"]}, {"location": "course-c/20-composite-types/pointers/#resume", "title": "R\u00e9sum\u00e9", "text": "

    L'arithm\u00e9tique de pointeur est donc chose courante avec les tableaux. \u00c0 vrai dire, les deux concepts sont interchangeables\u2009:

    Arithm\u00e9tique sur tableau unidimensionnel \u00c9l\u00e9ment Premier Deuxi\u00e8me Troisi\u00e8me n i\u00e8me Acc\u00e8s tableau a[0] a[1] a[2] a[n - 1] Acc\u00e8s pointeur *a *(a + 1) *(a + 2) *(a + n - 1)

    De m\u00eame, l'exercice peut \u00eatre r\u00e9p\u00e9t\u00e9 avec des tableaux \u00e0 deux dimensions\u2009:

    Arithm\u00e9tique sur tableau bidimensionnel \u00c9l\u00e9ment Premier Deuxi\u00e8me n ligne m colonne Acc\u00e8s tableau a[0][0] a[1][1] a[n - 1][m - 1] Acc\u00e8s pointeur *(*(a+0)+0) *(*(a+1)+1) *(*(a+i-1)+j-1)"}, {"location": "course-c/20-composite-types/pointers/#chaines-de-caracteres", "title": "Cha\u00eenes de caract\u00e8res", "text": "

    Les cha\u00eenes de caract\u00e8res sont des tableaux de caract\u00e8res termin\u00e9s par un caract\u00e8re nul '\\0'. Il est donc possible de d\u00e9clarer une cha\u00eene de caract\u00e8res de la mani\u00e8re suivante\u2009:

    static const char* conjonctions[] = {\n    \"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"\n};\n

    Pointeur sur une cha\u00eene de caract\u00e8re

    Dans ce cas, les cha\u00eenes \"mais\", \"ou\"... sont des constantes litt\u00e9rales de type const char*. Elles sont stock\u00e9es dans un segment de m\u00e9moire en lecture seule. Le tableau conjonctions est donc un tableau de pointeurs sur des cha\u00eenes de caract\u00e8res.

    Cette structure est tr\u00e8s exactement la m\u00eame que pour les arguments transmis \u00e0 la fonction main: la d\u00e9finition char *argv[].

    On se retrouve donc avec une indirection sur un tableau de pointeurs. Pour acc\u00e9der \u00e0 un \u00e9l\u00e9ment de la cha\u00eene de caract\u00e8res, on utilisera l'op\u00e9rateur crochet []. Il faut noter que dans ce cas, rien ne garanti que les \u00e9l\u00e9ments soient contigus en m\u00e9moire. Consid\u00e9rons l'exemple suivant\u2009:

    char *a = \"mais\";\nchar *b = \"ou\";\nchar *c = \"est\";\nchar *d = \"donc\";\nchar *e = \"or\";\nchar *f = \"ni\";\nchar *g = \"car\";\n\nchar *conjonctions[] = {a, b, c, d, e, f, g};\n

    Chaque variable a \u00e0 g sont des pointeurs sur des cha\u00eenes de caract\u00e8res mais vous ne savez pas n\u00e9cessairement o\u00f9 elles sont stock\u00e9es en m\u00e9moire.

    On est en droit de se demander quel est l'avantage de passer par des pointeurs, on pourrait tr\u00e8s bien d\u00e9clarer le tableau de conjonctions comme un char conjonctions[][5] et avoir ceci\u2009:

    char conjonctions[][5] = {\n    \"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"\n};\n

    N\u00e9anmoins dans ce cas, de l'espace m\u00e9moire est gaspill\u00e9 car chaque cha\u00eene de caract\u00e8res doit avoir la m\u00eame taille, et pour d\u00e9clarer le tableau il faut conna\u00eetre la taille de la plus grande cha\u00eene de caract\u00e8res. En revanche cette m\u00e9thode est de r\u00e9duire le niveau d'imbrications. Il n'y a plus de tableau interm\u00e9diaire de pointeurs.

    ", "tags": ["main", "conjonctions"]}, {"location": "course-c/20-composite-types/pointers/#structures", "title": "Structures", "text": "

    Les pointeurs peuvent bien entendu \u00eatre utilis\u00e9s avec n'importe quel type de donn\u00e9es. Les structures ne font pas exception \u00e0 la r\u00e8gle. On peut d\u00e9finir une structure de donn\u00e9es et d\u00e9clarer un pointeur sur cette structure\u2009:

    typedef struct Date {\n    unsigned char day;\n    unsigned char month;\n    unsigned int year;\n} Date;\n\nDate date;\nDate *p;  // Pointeur sur un type Date\n\np = &date;  // Assignation de l'adresse de date \u00e0 p\n

    Le pointeur reste un pointeur, soit un espace m\u00e9moire qui contient une adresse vers la structure Date. En cons\u00e9quence, la taille de ce pointeur est de 8 bytes sur une machine LP64 (64 bits) :

    Date *p;\nassert(sizeof(p) == 8);\n

    Le langage C introduit un autre sucre syntaxique pour d\u00e9r\u00e9f\u00e9rencer un \u00e9l\u00e9ment d'une structure. En effet, il est possible d'\u00e9crire p->day pour acc\u00e9der au champ day de la structure point\u00e9e par p. C'est \u00e9quivalent \u00e0 (*p).day :

    a->b \u2261 (*a).b\n

    Ajoutons que concernant l'arithm\u00e9tique de pointeur, il est important de noter que l'op\u00e9ration p + 1 incr\u00e9mente le pointeur de sizeof(Date) bytes. C'est \u00e0 dire que p + 1 pointe sur la structure suivante de type Date. On peut donc avoir un tableau de structures de la mani\u00e8re suivante\u2009:

    #define MAX 10\nDate dates[MAX];\nDate *p = dates;\n\nfor (size_t i = 0; i < MAX; i++) {\n    p->day = i;\n    p->month = i;\n    p->year = 2021 + i;\n    p++;\n}\n

    Cette \u00e9criture est d\u00e9routante et peut \u00eatre une cause fr\u00e9quente de trichotillomanie (arraquage de cheveux). Prenons l'exemple de deux fonctions, l'une prenant un pointeur vers une date et l'autre une structure date\u2009:

    void print_date1(Date *date) {\n    printf(\"%d/%d/%d\\n\", date->day, date->month, date->year);\n}\n\nvoid print_date2(Date date) {\n    printf(\"%d/%d/%d\\n\", date.day, date.month, date.year);\n}\n

    Lorsque vous d\u00e9cidez de modifier le type de l'argument de la fonction vous devez ajuster les d\u00e9r\u00e9f\u00e9rencements. C'est \u00e0 dire que si vous d\u00e9cidez de passer de Date \u00e0 Date* vous devez modifier . en -> et vice versa. En pratique il est toujours pr\u00e9f\u00e9rable de passer par des pointeurs pour \u00e9viter de copier des structures sur la pile. Pour \u00e9viter de les modifier, il est possible de d\u00e9clarer les structures comme const:

    void print_date(const Date *date) {\n    printf(\"%d/%d/%d\\n\", date->day, date->month, date->year);\n}\n
    ", "tags": ["const", "day", "Date"]}, {"location": "course-c/20-composite-types/pointers/#structure-recursive", "title": "Structure r\u00e9cursive", "text": "

    Lorsqu'on utilise des structures de donn\u00e9es plus complexes comme les listes cha\u00een\u00e9es, on a besoin de cr\u00e9er une structure contenant des donn\u00e9es ainsi qu'un pointeur sur l'\u00e9l\u00e9mnent suivant. On peut d\u00e9finir une structure r\u00e9cursive de la mani\u00e8re suivante\u2009:

    typedef struct Element {\n  struct Element *next;  // Pointeur sur l'\u00e9l\u00e9ment suivant\n  unsigned long data;  // Donn\u00e9e\n} Element;\n

    Exemple d'utilisation\u2009:

    Element e[3];\n\n// Premier \u00e9l\u00e9ment de la liste\ne[0].prev = NULL;\ne[0].next = &e[1];\n\n// Second \u00e9l\u00e9ment de la liste\ne[1].prev = &e[0];\ne[1].next = &e[2];\n\n// troisi\u00e8me \u00e9l\u00e9ment de la liste\ne[2].prev = &e[1];\ne[2].next = NULL;\n

    On peut parcourir cette liste de la mani\u00e8re suivante\u2009:

    Element *walk = &e[0];\nwhile (walk) {\n    printf(\"%lu\\n\", walk->data);\n    walk = walk->next;\n}\n

    En effet, tant que le pointeur n'est pas NULL, on peut continuer \u00e0 parcourir la liste. Le dernier \u00e9l\u00e9ment de la liste pointe sur NULL qui fait office de valeur sentinelle pour indiquer la fin de la liste.

    Les structures r\u00e9cursives sont tr\u00e8s utilis\u00e9es en informatique pour repr\u00e9senter des donn\u00e9es hi\u00e9rarchiques. Nous verrons plus tard les notions d'arbres et de graphes qui reposent sur ce concept.

    ", "tags": ["NULL"]}, {"location": "course-c/20-composite-types/pointers/#arguments-de-fonctions", "title": "Arguments de fonctions", "text": "

    Lors de l'introduction aux fonctions nous avons vu que ces derni\u00e8res peuvent recevoir des arguments. Ces arguments peuvent \u00eatre de n'importe quel type, y compris des pointeurs et dans de nombreux cas de figure le passage par pointeur est pr\u00e9f\u00e9rable. Voici quelques cas de figure o\u00f9 le passage par pointeur est recommand\u00e9\u2009:

    Modification de la valeur d'une variable dans une fonction

    void increment(int *a) {\n    (*a)++;\n}\n\nint main() {\n    int a = 0;\n    increment(&a);\n    printf(\"%d\\n\", a);  // Affiche 1\n}\n

    Le cas le plus notable est celui de la fonction scanf qui modifie la valeur de la variable pass\u00e9e en argument. En effet, scanf attend un pointeur sur la variable \u00e0 modifier. Jusqu'ici nous vous avions expliqu\u00e9 qu'il faut toujours utiliser l'esperluette & lors de l'utilisation de scanf. Maintenant vous savez que & est n\u00e9cessaire car scanf attend un pointeur puisque cette fonction modifie la valeur de la variable pass\u00e9e en argument.

    Eviter la copie de donn\u00e9es

    Dans certains cas de figure, les donn\u00e9es pass\u00e9es en argument \u00e0 une fonction peuvent \u00eatre tr\u00e8s grandes et elles sont copi\u00e9es sur la pile \u00e0 chaque appel. C'est le cas pour les structures. Consid\u00e9rons la structure suivante\u2009:

    struct Data {\n    int a[1000];\n};\n\nvoid process(struct Data *data) {\n    // Traitement des donn\u00e9es\n}\n

    Si la structure Data est tr\u00e8s grande, lors de l'appel d'une fonction la structure sera copi\u00e9e int\u00e9gralement sur la pile \u00e0 chaque appel. En revanche, si on passe un pointeur seul l'adresse de la structure sera copi\u00e9e. En pratique on pr\u00e9f\u00e8rera toujours passer des pointeurs pour les structures\u2009:

    process(struct Data data)  // Copie sur la pile\nprocess(struct Data *data) // Passage par r\u00e9f\u00e9rence (adresse)\n
    ", "tags": ["Data", "scanf"]}, {"location": "course-c/20-composite-types/pointers/#plusieurs-valeurs-de-retour", "title": "Plusieurs valeurs de retour", "text": "

    Lorsqu'une fonction doit retourner plusieurs valeurs, il est possible de passer des pointeurs en argument pour stocker les valeurs de retour. Par exemple la fonction compute retourne un statut d'ex\u00e9cution et une valeur calcul\u00e9e\u2009:

    int compute(double x, double *result) {\n    if (x == 0) return -1;\n    *result = 42. / x;\n    return 0;\n}\n

    On observe qu'il faut d\u00e9r\u00e9f\u00e9rencer le pointeur result pour modifier la valeur de la variable point\u00e9e. On peut appeler cette fonction de la mani\u00e8re suivante\u2009:

    double result = 4823.;\nint status = compute(0, &result);\nif (status == 0) {\n    printf(\"Result: %f\\n\", result);\n} else {\n    printf(\"Error: Division by zero\\n\");\n}\n
    ", "tags": ["result", "compute"]}, {"location": "course-c/20-composite-types/pointers/#transtypage-cast", "title": "Transtypage (cast)", "text": "

    Nous avons expliqu\u00e9 plus haut qu'un pointeur est g\u00e9n\u00e9ralement associ\u00e9 \u00e0 un type permettant l'arithm\u00e9tique de pointeurs. N\u00e9anmoins il existe un cas particulier, celui du type void. Un pointeur sur void est un pointeur neutre, c'est \u00e0 dire qu'il peut pointer sur n'importe quel type de donn\u00e9es. Comme le type n'est pas connu, l'arithm\u00e9tique de pointeurs n'est pas possible. Il est n\u00e9cessaire alors de transtyper le pointeur pour pouvoir l'utiliser.

    int a = 42;\nvoid *ptr = &a;\nptr++;  // Erreur de compilation\n

    La fonction memcpy est un exemple typique de l'utilisation de pointeurs sur void. Cette fonction permet de copier une r\u00e9gion m\u00e9moire vers une autre. Elle est d\u00e9clar\u00e9e de la mani\u00e8re suivante\u2009:

    void *memcpy(void *dest, const void *src, size_t n);\n

    Elle peut \u00eatre appel\u00e9e avec n'importe quel type de donn\u00e9es. Par exemple pour copier un tableau d'entiers, une structure ou m\u00eame un tableau de structures. En sp\u00e9cifiant le type explicite du pointeur, il faudrait autant de fonctions memcpy que de type possible, ce qui n'est ni raisonnable, ni m\u00eame imaginable. Face \u00e0 ce dilemme, on utilise un pointeur neutre. Consid\u00e9rons ces diff\u00e9rents types\u2009:

    char message[] = \"Mind the gap, please!\";\nint array[128];\nstruct { int a; char b; float c[3] } elements[128];\n

    On peut assigner l'adresse de ces variables \u00e0 un pointeur void mais on perd au passage l'arithm\u00e9tique de pointeurs\u2009:

    void *ptr;\n\nptr = message;\nptr = array;\nptr = elements;\n

    Ce pointeur neutre peut ensuite \u00eatre transtyp\u00e9 pour \u00eatre utilis\u00e9. La cl\u00e9 est dans le standard ISO/IEC 9899:2011 section 6.3.2.3 page 55\u2009:

    A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again\u2009; the result shall compare equal to the original pointer.

    Autrement dit, il n'est pas n\u00e9cessaire ni recommand\u00e9 de faire un transtypage explicite pour convertir vers et en provenance d'un pointeur sur void. Et donc, l'astuce de memcpy est que la fonction accepte n'importe quel type de pointeur. Et quant \u00e0 l'impl\u00e9mentation de cette fonction me direz-vous\u2009? Une possibilit\u00e9 serait\u2009:

    void memcpy(void *dest, void *src, size_t n)\n{\n    char* csrc = src;\n    char* cdest = dest;\n\n    for (size_t i = 0; i < n; i++)\n        cdest[i] = csrc[i];\n}\n

    O\u00f9 plus concis\u2009:

    void memcpy(void *dest, void *src, size_t n)\n{\n    for (size_t i = 0; i < n; i++)\n        ((char*)dst)[i] = ((char*)src)[i];\n}\n

    En r\u00e9alit\u00e9 ce n'est pas la fa\u00e7on dont memcpy est impl\u00e9ment\u00e9e. En effet, cette fonction est tr\u00e8s utilis\u00e9e et doit \u00eatre extr\u00eamement performante. Il est donc n\u00e9cessaire de tenir compte de l'architecture du processeur et de la taille des donn\u00e9es \u00e0 copier. L'impl\u00e9mentation de memcpy est une affaire de cuisine interne du compilateur. L'impl\u00e9mentation d\u00e9pend donc de l'architecture cible et doit tenir compte des \u00e9ventuels effets de bords. Par exemple s'il faut copier un tableau de 9 x 32 bits. Une architecture 64-bits aura une grande facilit\u00e9 \u00e0 copier les 8 premiers octets, mais quant au dernier, il s'agit d'un cas particulier. Vous \u00eates comme Thomas l'ap\u00f4tre, et ne me croyez pas\u2009? Alors, digressons et essayons\u2009:

    #include <string.h>\n#include <stdio.h>\n\nint main(void)\n{\n    char a[] = \"La Broye c'est fantastique!\";\n    char b[sizeof(a)];\n\n    memcpy(a, b, sizeof(a));\n\n    printf(\"%s %s\", a, b);\n}\n

    En observant l'assembleur cr\u00e9\u00e9 \u00e0 la compilation (avec gcc -S -O3), on peut voir que la copie est effectu\u00e9e en 6 instructions. Par ailleurs, il n'y a aucun appel de fonction \u00e0 memcpy comme c'est le cas pour printf (bl printf). Voici le code assembleur g\u00e9n\u00e9r\u00e9\u2009:

    main :\n    // Entry\n    str     lr, [sp, #-4]!\n    sub     sp, sp, #60\n\n    // Inline memcpy\n    mov     ip, sp      // Destination address\n    add     lr, sp, #28 // Source address (char b located 28 octets after a)\n\n    ldmia   lr!, {r0, r1, r2, r3}   // Load 4 x 32-bits\n    stmia   ip!, {r0, r1, r2, r3}   // Store 4 x 32-bits\n\n    ldm     lr, {r0, r1, r2}        // Load 3 x 32-bits\n    stm     ip, {r0, r1, r2}        // Store 3 x 32-bits\n\n    // Display (printf)\n    add     r2, sp, #28\n    mov     r1, sp\n    ldr     r0, .L4\n    bl      printf\n\n    // Exit\n    mov     r0, #0\n    add     sp, sp, #60\n    ldr     pc, [sp], #4\n.L4 :\n    .word   .LC0\n.LC0 :\n    .ascii  \"La Broye c'est fantastique!\\000\"\n

    Vous pouvez jouer avec cet exemple sur le site godbolt.

    Arithm\u00e9tique et void*

    Prenons l'exemple suivant\u2009:

    int main() {\n    int a = 42;\n    void *p = &a;\n    p++; // <--\n}\n

    Formellement ceci devrait mener \u00e0 une erreur de compilation, car void n'a pas de substance, et donc aucune taille associ\u00e9e. N\u00e9anmoins gcc est tr\u00e8s permissif de base et (\u00e0 ma grande surprise), il ne g\u00e9n\u00e8re par d\u00e9faut ni warning, ni erreurs sans l'option -Wpointer-arith.

    En compilant ce code avec gcc -Wall -Wextra -pedantic on obtient\u2009:

    warning: pointer of type 'void *' used in arithmetic [-Wpointer-arith]\n

    N\u00e9anmoins sans l'option -Wpointer-arith aucune erreur n'est g\u00e9n\u00e9r\u00e9e. C'est pourquoi il est recommand\u00e9 de toujours compiler avec les options -Wall -Wextra -pedantic pour obtenir un code plus robuste.

    Exercice 1\u2009: Void*

    Que pensez-vous que sizeof(void*) devrait retourner sur une architecture 64-bits\u2009?

    • 1
    • 2
    • 4
    • 8
    • 0
    Solution

    Un pointeur reste un pointeur, c'est une variable qui contient une adresse m\u00e9moire. Sur une architecture 64-bits, un pointeur est cod\u00e9 sur 8 bytes. sizeof(void*) retourne donc 8.

    ", "tags": ["void", "printf", "memcpy", "gcc"]}, {"location": "course-c/20-composite-types/pointers/#pointeurs-de-fonctions", "title": "Pointeurs de fonctions", "text": "

    Un pointeur peut pointer n'importe o\u00f9 en m\u00e9moire, et donc il peut \u00e9galement pointer non pas sur une variable, mais sur une fonction. Les pointeurs de fonctions sont tr\u00e8s utiles pour des fonctions de rappel (callback).

    Par exemple si on veut appliquer une transformation sur tous les \u00e9l\u00e9ments d'un tableau, mais que la transformation n'est pas connue \u00e0 l'avance. On pourrait alors \u00e9crire\u2009:

    int is_odd(int n) {\n    return !(n % 2);\n}\n\nvoid map(int array[], int (*callback)(int), size_t length) {\n    for (size_t i = 0; i < length; i++)\n        array[i] = callback(array[i]);\n}\n\nvoid main(void) {\n    int array[] = {1,2,3,4,5};\n    map(array, is_odd);\n}\n

    Avec la r\u00e8gle gauche droite, on parvient \u00e0 d\u00e9cortiquer la d\u00e9claration\u2009:

    int (*callback)(int)\n      ^^^^^^^^        callback est\n              ^\n     ^                un pointeur sur\n               ^^^^^  une function prenant un int\n^^^                   et retournant un int\n

    Les pointeurs de fonctions sont beaucoup utilis\u00e9s en programmation fonctionnelle. Ils permettent de passer des actions en argument \u00e0 d'autres fonctions. Par exemple la fonction qsort de la biblioth\u00e8que standard C permet de trier un tableau. Elle prend en argument un pointeur sur le tableau \u00e0 trier, le nombre d'\u00e9l\u00e9ments, la taille d'un \u00e9l\u00e9ment et une fonction de comparaison.

    Cette fonction de comparaison est un pointeur sur une fonction qui prend deux \u00e9l\u00e9ments et retourne un entier n\u00e9gatif si le premier est inf\u00e9rieur au second, z\u00e9ro s'ils sont \u00e9gaux et un entier positif si le premier est sup\u00e9rieur au second.

    int compare(const void *a, const void *b) {\n    return (*(int*)a - *(int*)b);\n}\n\nint main(void) {\n    int array[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};\n    qsort(array, sizeof(array) / sizeof(array[0]), sizeof(array[0]), compare);\n}\n

    Les pointeurs de fonctions sont \u00e9galement utilis\u00e9s pour effectuer des op\u00e9rations diff\u00e9rentes selon des crit\u00e8res. Admettons que l'on souhaite r\u00e9aliser un parseur d'expressions math\u00e9matiques en format infix\u00e9. C'est \u00e0 dire que les op\u00e9rateurs sont plac\u00e9s apr\u00e8s les nombres. 2+3*8-2 s'\u00e9crirait 238*+2-.

    Les op\u00e9rateurs selon la table ASCII sont + (43), - (45), * (42) et / (47). Un tableau de correspondance peut \u00eatre cr\u00e9\u00e9 pour associer un op\u00e9rateur \u00e0 une fonction\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nvoid display(double a, double b, char op) {\n    printf(\"%f %c %f\\n\", a, op, b);\n}\n\ndouble add(double a, double b) { display(a, b, '+'); return a + b; }\ndouble sub(double a, double b) { display(a, b, '-'); return a - b; }\ndouble mul(double a, double b) { display(a, b, '*'); return a * b; }\ndouble divide(double a, double b) { display(a, b, '/'); return a / b; }\n\ndouble (*operations[])(double, double) = {\n    /* 42 */ mul,   // *\n    /* 43 */ add,   // +\n    /* 44 */ NULL,  // ,\n    /* 45 */ sub,   // -\n    /* 46 */ mul,   // .\n    /* 47 */ divide // /\n};\n\nint parse(char *expression) {\n    int stack[128];\n    int top = -1;\n\n    while (*expression) {\n        const char c = *expression;\n        if (c >= '0' && c <= '9')\n            stack[++top] = (double)(c - '0');\n        else if (top > 0 && c >= '*' && c <= '/') {\n            const int index = c - '*';\n            int a = stack[top--], b = stack[top--];\n            stack[++top] = operations[index](b, a);\n        }\n        else {\n            printf(\"Invalid expression (stack: %d)\\n\", top);\n            exit(EXIT_FAILURE);\n        }\n        expression++;\n    }\n    return stack[top];\n}\n\nint main(void) {\n    char expression[] = \"238*+4-\";\n    printf(\"%d\\n\", parse(expression));\n}\n

    Le programme ci-dessus affiche\u2009:

    ./a.out\n3.000000 * 8.000000\n2.000000 + 24.000000\n26.000000 - 4.000000\n22\n
    ", "tags": ["qsort"]}, {"location": "course-c/20-composite-types/pointers/#la-regle-gauche-droite", "title": "La r\u00e8gle gauche-droite", "text": "

    Cette r\u00e8gle est une recette magique permettant de correctement d\u00e9cortiquer une d\u00e9claration C contenant des pointeurs. Il faut tout d'abord lire\u2009:

    R\u00e8gles gauche droite Symbole Traduction Direction * pointeur sur Toujours \u00e0 gauche [] tableau de Toujours \u00e0 droite () fonction retournant Toujours \u00e0 droite Premi\u00e8re \u00e9tape

    Trouver l'identifiant et se dire L'identifiant est.

    Deuxi\u00e8me \u00e9tape

    Chercher le symbole \u00e0 droite de l'identifiant. Si vous trouvez un (), vous savez que cet identifiant est une fonction et vous avez L'identifiant est une fonction retournant. Si vous trouvez un [] vous dites alors L'identifiant est un tableau de. Continuez \u00e0 droite jusqu'\u00e0 ce que vous \u00eates \u00e0 court de symboles, OU que vous trouvez une parenth\u00e8se fermante ).

    Troisi\u00e8me \u00e9tape

    Regardez le symbole \u00e0 gauche de l'identifiant. S\u2019il n'est aucun des symboles pr\u00e9c\u00e9dents, dites quelque chose comme int. Sinon, convertissez le symbole en utilisant la table de correspondance. Continuez d'aller \u00e0 gauche jusqu'\u00e0 ce que vous \u00eates \u00e0 court de symboles OU que vous rencontrez une parenth\u00e8se ouvrante (.

    Ensuite...

    Continuez les \u00e9tapes 2 et 3 jusqu'\u00e0 ce que vous avez une d\u00e9claration compl\u00e8te.

    Cet algorithme peut \u00eatre repr\u00e9sent\u00e9 par le diagramme suivant\u2009:

    Diagramme de la r\u00e8gle gauche-droite

    Voici quelques exemples\u2009:

    int *p[];\n
    1. Trouver l'identifiant\u2009: p: p est

      int *p[];\n     ^\n
    2. Se d\u00e9placer \u00e0 droite: p est un tableau de

      int *p[];\n      ^^\n
    3. Se d\u00e9placer \u00e0 gauche: p est un tableau de pointeurs sur

      int *p[];\n    ^\n
    4. Continuer \u00e0 gauche: p est un tableau de pointeurs sur un int

      int *p[];\n^^^\n
    ", "tags": ["int"]}, {"location": "course-c/20-composite-types/pointers/#cdecl", "title": "cdecl", "text": "

    Il existe un programme nomm\u00e9 cdecl qui permet de d\u00e9coder de complexes d\u00e9claration c\u2009:

    $ cdecl 'char (*(*x[3])())[5]'\ndeclare x as array 3 of pointer to function returning pointer to array 5 of char\n

    Une version en ligne est \u00e9galement disponible.

    Manuellement cette d\u00e9claration serait\u2009:

    char (*(*foo[3])())[5]\n         ^^^            L'identifiant `foo` est\n            ^^^         un tableau de 3\n        ^      <        pointeurs sur\n       >        ^^      une fonction prenant les arguments `` et retournant un\n      *           <     pointeur sur\n     >             ^^^  un tableau de 5\n^^^^                  < char\n
    "}, {"location": "course-c/20-composite-types/pointers/#implementation", "title": "Impl\u00e9mentation", "text": "

    Pour l'exemple, voici une impl\u00e9mentation tr\u00e8s rudimentaire de cdecl. Elle n'est pas compl\u00e8te mais elle donne le principe de fonctionnement\u2009:

    #include <ctype.h>\n#include <stdbool.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nint is_type(char *str) {\n   const char *type[] = {\"int\",   \"char\",   \"short\", \"long\",\n                         \"float\", \"double\", \"void\"};\n   const int n = sizeof(type) / sizeof(type[0]);\n   for (int i = 0; i < n; i++) {\n      const int len = strlen(type[i]);\n      if (strncmp(str, type[i], len) == 0) return len;\n   }\n   return 0;\n}\n\nchar *find_identifier(char *decl, size_t *len) {\n   // Find first string that is not a type\n   while (*decl) {\n      if (isalpha(*decl)) {\n         int len = is_type(decl);\n         if (len) {\n            decl += len;\n            continue;\n         }\n         break;\n      }\n      decl++;\n   }\n\n   // Find end of identifier and its length\n   char *c = decl;\n   while (isalnum(*c) || *c == '_') c++;\n   *len = c - decl;\n   return decl;\n}\n\nchar *explore_right(char *right) {\n   while (*right && *right != ')') {\n      if (*right == '[') {\n         right++;\n         printf(\"un tableau de \");\n         while (*right != ']') {\n            putchar(*right++);\n         }\n         putchar(' ');\n      } else if (*right == '(') {\n         right++;\n         printf(\"une fonction aux arguments '\");\n         while (*right != ')') putchar(*right++);\n         printf(\"' et retournant un \");\n      }\n      right++;\n   }\n   return right;\n}\n\nchar *explore_left(char *left, char *start) {\n   while (left >= start && *left != '(') {\n      if (*left == '*') printf(\"pointeur sur \");\n      if (*left == ']') {\n         printf(\"un tableau de \");\n         while (left >= start && *left != '[') left--;\n      }\n      if (*left == ')') {\n         printf(\"une fonction retournant \");\n         while (left >= start && *left != '(') left--;\n      } else if (isalpha(*left)) {\n         int len = 0;\n         while (left >= start && isalpha(*left)) {\n            left--;\n            len++;\n         }\n         printf(\"%.*s \", len, left + 1);\n      }\n      left--;\n   }\n   return left;\n}\n\nvoid cdecl(char *decl) {\n   // Step 1 : Find identifier\n   size_t len;\n   char *left = find_identifier(decl, &len);\n   printf(\"L'identifiant '%.*s' est \", (int)len, left);\n\n   // Step 2 and 3 : Explore right and left\n   char *right = left + len;\n   left--;\n   do {\n      right = explore_right(right) + 1;\n      left = explore_left(left, decl) - 1;\n   } while (left > decl && *right);\n   putchar('\\n');\n}\n\nint main() { cdecl(\"char (*(*foo[3])(int a))[5]\"); }\n

    Les am\u00e9liorations sur ce code seraient\u2009:

    • G\u00e9rer les erreurs de syntaxe (parenth\u00e8ses manquantes, etc.)
    • Afficher les arguments et la taille des tableaux en explorant \u00e0 gauche
    • G\u00e9rer les pluriels (tableau, tableaux, etc.)
    ", "tags": ["cdecl"]}, {"location": "course-c/20-composite-types/pointers/#initialisation-par-transtypage", "title": "Initialisation par transtypage", "text": "

    L'utilisation de structure peut \u00eatre utile pour initialiser un type de donn\u00e9e en utilisant un autre type de donn\u00e9e. Nous citons ici deux exemples.

    int i = *(int*)(struct { char a; char b; char c; char d; }){'a', 'b', 'c', 'd'};\n
    union {\n    int matrix[10][10];\n    int vector[100];\n} data;\n
    "}, {"location": "course-c/20-composite-types/pointers/#enchevetrement-ou-aliasing", "title": "Enchev\u00eatrement ou Aliasing", "text": "

    Travailler avec les pointeurs demande une attention particuli\u00e8re \u00e0 tous les probl\u00e8mes d'alisasing dans lesquels diff\u00e9rents pointeurs pointent sur une m\u00eame r\u00e9gion m\u00e9moire.

    Mettons que l'on souhaite simplement d\u00e9placer une r\u00e9gion m\u00e9moire vers une nouvelle r\u00e9gion m\u00e9moire. On pourrait impl\u00e9menter le code suivant\u2009:

    void memory_move(char *dst, char*src, size_t size) {\n    for (int i = 0; i < size; i++)\n        *dst++ = *src++;\n}\n

    Ce code est tr\u00e8s simple mais il peut poser probl\u00e8me selon les cas. Imaginons que l'on dispose d'un tableau simple de dix \u00e9l\u00e9ments et de deux pointeurs *src et *dst. Pour d\u00e9placer la r\u00e9gion du tableau de 4 \u00e9l\u00e9ments vers la droite. On se dirait que le code suivant pourrait fonctionner\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u25027\u25028\u25029\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n ^*src ^*dst\n      \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n      \u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502\n      \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n       \u2193 \u2193 \u2193 \u2193 \u2193 \u2193 \u2193\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Na\u00efvement l'ex\u00e9cution suivante devrait fonctionner, mais les deux pointeurs source et destination s'enchev\u00eatrent et le r\u00e9sultat n'est pas celui escompt\u00e9.

    char array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\nchar *src = &array[0];\nchar *dst = &array[3];\n\nmemory_move(b, a, 7);\n
    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u25027\u25028\u25029\u2502 Tableau d'origine\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25020\u25021\u25022\u25020\u2502 Op\u00e9ration avec `memory_move`\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502 Op\u00e9ration avec `memmove` (fonction standard)\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Notre simple fonction de d\u00e9placement m\u00e9moire ne fonctionne pas avec des r\u00e9gions m\u00e9moires qui s'enchev\u00eatrent. En revanche, la fonction standard memmove de <stdlib.h> fonctionne, car elle autorise, au d\u00e9triment d'une plus grande complexit\u00e9, de g\u00e9rer ce type de situation.

    Notons que sa fonction voisine memcpy ne dois jamais \u00eatre utilis\u00e9e en cas d'aliasing. Cette fonction se veut performante, c'est-\u00e0-dire qu'elle peut \u00eatre impl\u00e9ment\u00e9e en suivant le m\u00eame principe que notre exemple memory_move. Le standard C99 ne d\u00e9finit pas le comportement de memcpy pour des pointeurs qui se chevauchent.

    ", "tags": ["memory_move", "memmove", "memcpy"]}, {"location": "course-c/20-composite-types/pointers/#double-pointeurs", "title": "Double Pointeurs", "text": "

    Nous avons vu qu'un pointeur peut r\u00e9f\u00e9rencer un autre pointeur. Dans le langage C, il est courant de rencontrer des fonctions acceptant des double pointeurs, comme illustr\u00e9 ci-dessous\u2009:

    void function(int **ptr);\n

    Un double pointeur est, par d\u00e9finition, un pointeur qui pointe vers un autre pointeur. Ce m\u00e9canisme est particuli\u00e8rement utile lorsqu'il s'agit de modifier la valeur d'un pointeur \u00e0 l'int\u00e9rieur d'une fonction. En effet, si une fonction re\u00e7oit un pointeur simple (int *ptr) en argument, la valeur de ce pointeur est copi\u00e9e sur la pile, ce qui signifie que toute modification faite \u00e0 ptr \u00e0 l'int\u00e9rieur de la fonction n'affectera pas le pointeur d'origine. \u00c0 l'inverse, si la fonction re\u00e7oit un double pointeur (int **ptr), elle re\u00e7oit une copie de l'adresse du pointeur, ce qui permet de modifier directement la valeur du pointeur d'origine.

    Pour illustrer cette notion, imaginons une analogie simple. Supposons que vous \u00eates un peintre, et que votre patron vous donne un post-it avec l'adresse d'une maison \u00e0 peindre. En suivant cette adresse (en d\u00e9r\u00e9f\u00e9ren\u00e7ant le pointeur), vous pouvez vous rendre \u00e0 la maison et la peindre (modifier la valeur point\u00e9e). Cependant, si vous constatez que l'adresse est incorrecte, vous pouvez rectifier l'information sur votre post-it, mais vous ne pouvez pas informer directement votre patron de cette correction, car vous ne modifiez pas le post-it qu'il poss\u00e8de.

    En revanche, imaginez que votre patron vous donne un post-it o\u00f9 est inscrite l'adresse d'un autre post-it contenant l'adresse de la maison \u00e0 peindre. Par exemple, il vous remet un post-it sur lequel est \u00e9crit\u2009: \u00ab\u2009L'adresse se trouve sur le post-it rose coll\u00e9 sur la vitre de la vitrine de gauche dans mon bureau.\u2009\u00bb Si vous constatez une erreur, vous pouvez vous rendre dans le bureau de votre patron, corriger l'adresse sur le post-it rose, et ainsi, \u00e0 son retour, il aura acc\u00e8s \u00e0 la bonne information. Cette situation refl\u00e8te exactement l'utilit\u00e9 d'un double pointeur en C.

    Dans le code, cela pourrait se traduire ainsi\u2009:

    void painter(House **address) {\n    if (!is_correct(*address))\n        *address = get_new_address();  // Modification de l'adresse point\u00e9e\n    paint(*address);  // Peindre la maison \u00e0 l'adresse correcte\n}\n\nint main(void) {\n    House *address = get_address();  // Obtenir l'adresse initiale\n    painter(&address); // Le patron transmet l'adresse du post-it\n}\n
    ", "tags": ["ptr"]}, {"location": "course-c/20-composite-types/pointers/#cas-dutilisation", "title": "Cas d'utilisation", "text": "

    Les double pointeurs sont employ\u00e9s dans plusieurs sc\u00e9narios en programmation C\u2009:

    1. Allocation dynamique de m\u00e9moire pour des tableaux 2D : Un double pointeur est souvent utilis\u00e9 pour g\u00e9rer des tableaux dynamiques \u00e0 deux dimensions. Par exemple, int **matrix permet de cr\u00e9er une matrice dont les dimensions peuvent \u00eatre modifi\u00e9es \u00e0 l'ex\u00e9cution.

    2. Manipulation de cha\u00eenes de caract\u00e8res : Les double pointeurs sont utilis\u00e9s pour manipuler des tableaux de cha\u00eenes de caract\u00e8res (tableaux de pointeurs vers des caract\u00e8res), souvent employ\u00e9s pour traiter des arguments de programmes (char **argv).

    3. Passage par r\u00e9f\u00e9rence pour modifier un pointeur : Comme dans l'exemple pr\u00e9c\u00e9dent, lorsqu'une fonction doit modifier un pointeur pass\u00e9 en argument, on utilise un double pointeur pour que le changement soit effectif en dehors de la fonction.

    4. Listes cha\u00een\u00e9es ou autres structures dynamiques : Les double pointeurs sont utilis\u00e9s pour ins\u00e9rer ou supprimer des \u00e9l\u00e9ments dans des structures de donn\u00e9es dynamiques telles que des listes cha\u00een\u00e9es, o\u00f9 la t\u00eate de liste peut \u00eatre modifi\u00e9e.

    Nous verrons certains de ces cas d'utilisation dans des sections ult\u00e9rieures.

    "}, {"location": "course-c/20-composite-types/pointers/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Esperluettes cascad\u00e9es

    Quel est le type de\u2009:

    *&*&*&*&*&*&(int)x;\n

    Exercice 3\u2009: Passage par adresse

    Donnez les valeurs affich\u00e9es par ce programme pour les variables a \u00e0 e.

    #include <stdio.h>\n#include <stdlib.h>\n\nint test(int a, int * b, int * c, int * d) {\n    a = *b;\n    *b = *b + 5;\n    *c = a + 2;\n    d = c;\n    return *d;\n}\n\nint main(void) {\n    int a = 0, b = 100, c = 200, d = 300, e = 400;\n    e = test(a, &b, &c, &d);\n    printf(\"a:%d, b:%d, c:%d, d:%d, e:%d\\n\", a, b, c, d, e);\n}\n
    Solution
    a:0, b:105, c:102, d:300, e:102\n
    "}, {"location": "course-c/20-composite-types/strings/", "title": "Cha\u00eenes de caract\u00e8res", "text": "

    Une cha\u00eene de caract\u00e8res est repr\u00e9sent\u00e9e en m\u00e9moire comme une succession de bytes, chacun repr\u00e9sentant un caract\u00e8re ASCII sp\u00e9cifique. La cha\u00eene de caract\u00e8re hello contient donc 5 caract\u00e8res et sera stock\u00e9e en m\u00e9moire sur 5 bytes. Une cha\u00eene de caract\u00e8re est donc \u00e9quivalente \u00e0 un tableau de char.

    En C, un artifice est utilis\u00e9 pour faciliter les op\u00e9rations sur les cha\u00eenes de caract\u00e8res. Tous les caract\u00e8res de 1 \u00e0 255 sont utilisables sauf le 0 qui est utilis\u00e9 comme sentinelle. Lors de la d\u00e9claration d'une cha\u00eene comme ceci\u2009:

    char str[] = \"hello, world!\";\n

    Le compilateur ajoutera automatiquement un caract\u00e8re de terminaison '\\0' \u00e0 la fin de la cha\u00eene. Pour en comprendre l'utilit\u00e9, imaginons une fonction qui permet de compter la longueur de la cha\u00eene. Elle aurait comme prototype ceci\u2009:

    size_t strlen(const char str[]);\n

    On peut donc lui passer un tableau dont la taille n'est pas d\u00e9finie et par cons\u00e9quent, il n'est pas possible de conna\u00eetre la taille de la cha\u00eene pass\u00e9e sans le b\u00e9n\u00e9fice d'une sentinelle.

    size_t strlen(const char str[]) {\n    size_t len = 0,\n    while (str[len++] != '\\0') {}\n    return len;\n}\n

    Une cha\u00eene de caract\u00e8re est donc strictement identique \u00e0 un tableau de char.

    Ainsi une cha\u00eene de caract\u00e8re est initialis\u00e9e comme suit\u2009:

    char str[] = \"Pulp Fiction\";\n

    La taille de ce tableau sera donc de 12 caract\u00e8res plus une sentinelle '\\0' ins\u00e9r\u00e9e automatiquement. Cette \u00e9criture est donc identique \u00e0\u2009:

    char str[] = {'P', 'u', 'l', 'p', ' ', 'F', 'i', 'c', 't', 'i', 'o', 'n', '\\0'};\n
    ", "tags": ["char", "hello"]}, {"location": "course-c/20-composite-types/strings/#tableaux-de-chaines-de-caracteres", "title": "Tableaux de cha\u00eenes de caract\u00e8res", "text": "

    Un tableau de cha\u00eene de caract\u00e8res est identique \u00e0 un tableau multidimensionnel\u2009:

    char conjunctions[][10] = {\"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"};\n

    Il est ici n\u00e9cessaire de d\u00e9finir la taille de la seconde dimension, comme pour les tableaux. C'est \u00e0 dire que la variable conjunctions aura une taille de 7x10 caract\u00e8res et le contenu m\u00e9moire de conjunctions[1] sera \u00e9quivalent \u00e0\u2009:

    {'o', 'u', 0, 0, 0, 0, 0, 0, 0, 0}\n

    D'ailleurs, ce tableau aurait pu \u00eatre initialis\u00e9 d'une tout autre fa\u00e7on\u2009:

    char conjunctions[][10] = {\n    'm', 'a', 'i', 's', 0, 0, 0, 0, 0, 0, 'o', 'u', 0, 0, 0,\n    0, 0, 0, 0, 0, 'e', 's', 't', 0, 0, 0, 0, 0, 0 , 0, 'd',\n    'o', 'n', 'c', 0, 0, 0, 0, 0 , 0, 'o', 'r', 0, 0, 0, 0,\n    0, 0, 0, 0, 'n', 'i', 0, 0, 0, 0, 0, 0, 0, 0, 'c', 'a',\n    'r', 0, 0, 0, 0, 0, 0, 0,\n};\n

    Notons que la valeur 0 est strictement identique au caract\u00e8re 0 de la table ASCII '\\0'. La cha\u00eene de caract\u00e8re \"mais\" aura une taille de 5 caract\u00e8res, ponctu\u00e9e de la sentinelle \\0.

    ", "tags": ["conjunctions"]}, {"location": "course-c/20-composite-types/strings/#wide-chars", "title": "Wide-chars", "text": "

    Les cha\u00eenes de caract\u00e8res larges sont des cha\u00eenes de caract\u00e8res qui utilisent plus d'un byte pour repr\u00e9senter un caract\u00e8re. En C, les cha\u00eenes de caract\u00e8res larges sont repr\u00e9sent\u00e9es par le type wchar_t. Pour d\u00e9clarer une cha\u00eene de caract\u00e8res larges, il est n\u00e9cessaire d'utiliser le pr\u00e9fixe L :

    wchar_t wstr[] = L\"Hello, \u4e16\u754c\";\n

    Ce type de cha\u00eene de caract\u00e8res est typiquement utilis\u00e9 pour repr\u00e9senter des caract\u00e8res Unicode. Comme nous l'avons vu au chapitre sur les types de donn\u00e9es, les caract\u00e8res qui ne sont pas dans la table ASCII sont repr\u00e9sent\u00e9s par plusieurs bytes. La fameuse \u00e9moji \ud83d\udc4b est repr\u00e9sent\u00e9e par 5 bytes 1F44B.

    L'en-t\u00eate wchar.h contient les fonctions pour manipuler les cha\u00eenes de caract\u00e8res larges. Par exemple, pour obtenir la longueur d'une cha\u00eene de caract\u00e8res larges, on utilise la fonction wcslen :

    #include <wchar.h>\n\nint main(void) {\n    wchar_t wstr[] = L\"Hello, \u4e16\u754c\";\n    size_t len = wcslen(wstr);\n    assert(len == 9);\n\n    char str[] = L\"Hello, \u4e16\u754c\";\n    size_t len = strlen(str);\n    assert(len == 13);\n}\n

    En pratique les cha\u00eenes de caract\u00e8res larges sont peu utilis\u00e9es en C, car elles sont moins efficaces que les cha\u00eenes de caract\u00e8res ASCII. En effet, les cha\u00eenes de caract\u00e8res larges n\u00e9cessitent plus de m\u00e9moire pour stocker les caract\u00e8res et les op\u00e9rations sur ces cha\u00eenes sont plus lentes.

    De plus, elles ne sont pas portables, car la taille d'un wchar_t d\u00e9pend de l'impl\u00e9mentation. Par exemple, sur Windows, un wchar_t est de 16 bits, alors que sur Linux, il est de 32 bits. Et comme nous l'avons les emojis peuvent \u00eatre cod\u00e9s sur plus de 16 bits.

    Pour ces raisons, on pr\u00e9f\u00e8re utiliser l'UTF-8 pour repr\u00e9senter les caract\u00e8res Unicode en C. L'UTF-8 est un encodage de caract\u00e8res Unicode qui utilise un \u00e0 quatre bytes pour repr\u00e9senter un caract\u00e8re. Il est compatible avec l'ASCII et est plus efficace que les cha\u00eenes de caract\u00e8res larges. Il fonctionne de la mani\u00e8re suivante\u2009:

    • Les caract\u00e8res 0000 \u00e0 007F sont cod\u00e9s sur un byte.
    • Les caract\u00e8res au del\u00e0 de 007F sont cod\u00e9s sur plusieurs bytes.
    0x00 - 0x7F         0b0xxxxxxx o\u00f9 x est le code ASCII sur 7-bits\n0x80 - 0x7FF        0b110xxxxx 0b10xxxxxx\n0x800 - 0xFFFF      0b1110xxxx 0b10xxxxxx 0b10xxxxxx\n0x10000 - 0x10FFFF  0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx\n

    Prenons l'exemple du caract\u00e8re \ud83d\udc4b. Sa repr\u00e9sentation binaire utilise 21 bits et n\u00e9cessite donc une s\u00e9quence de 4 octets.

    U+1F44B = 0b0001 1111 0100 0100 1011\n

    En UTF-8, il sera repr\u00e9sent\u00e9 par la s\u00e9quence de bytes suivante\u2009:

    0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx\n       000     011111     010001     001011\n\n0xF0       0x9F       0x91       0x8B\n

    Donc la longueur de la cha\u00eene de caract\u00e8re avec strlen sera de 4 et non de 1.

    char str[] = \"\ud83d\udc4b\";\nsize_t len = strlen(str);\nassert(len == 4);\n

    Pour conna\u00eetre la longueur d'une cha\u00eene de caract\u00e8res en UTF-8, il est n\u00e9cessaire de la faire \u00e0 la main\u2009:

    #include <stdio.h>\n\nsize_t utf8_strlen(const char *s) {\n    size_t length = 0;\n\n    while (*s) {\n        // Si on trouve 0b10xxxxxx, c'est un byte de continuation\n        // on l'ignore...\n        if ((*s & 0xC0) != 0x80) length++;\n        s++;\n    }\n\n    return length;\n}\n\nint main() {\n    const char *str = \"Hello, \ud83d\udc4b World!\";\n    size_t len = utf8_strlen(str);\n    printf(\"The string length is: %zu characters\\n\", len);\n}\n
    ", "tags": ["wchar_t", "wcslen", "strlen", "wchar.h"]}, {"location": "course-c/20-composite-types/strings/#buffer-tampon", "title": "Buffer (tampon)", "text": "

    Bien souvent, les cha\u00eenes de caract\u00e8res sont manipul\u00e9es dans des buffers. Un buffer est un tableau de caract\u00e8res d'une taille fixe utilis\u00e9 pour stocker des donn\u00e9es interm\u00e9diaires. Un cas typique est la lecture depuis l'entr\u00e9e standard.

    Admettons que l'on souhaite lire le nom d'un utilisateur depuis l'entr\u00e9e standard. On peut d\u00e9finir un buffer de 256 caract\u00e8res pour stocker le nom de l'utilisateur\u2009:

    #include <stdio.h>\n\nint main(void) {\n    char name[32];\n    printf(\"Enter your name: \");\n    scanf(\"%31s\", name);\n}\n

    Dans cet exemple, name est un buffer de 32 caract\u00e8res. La fonction scanf lit au maximum 31 caract\u00e8res depuis l'entr\u00e9e standard et les stocke dans name. La taille du buffer est de 32 caract\u00e8res pour laisser de la place pour la sentinelle \\0. Sans cette s\u00e9curit\u00e9, il serait possible de d\u00e9border le buffer et d'\u00e9crire dans des zones m\u00e9moires qui ne nous appartiennent pas.

    ", "tags": ["name", "scanf"]}, {"location": "course-c/20-composite-types/strings/#chaine-de-caracteres-multi-lignes", "title": "Cha\u00eene de caract\u00e8res multi-lignes", "text": "

    En C, il n'est pas possible de d\u00e9clarer une cha\u00eene de caract\u00e8res sur plusieurs lignes. Pour ce faire, il est n\u00e9cessaire de concat\u00e9ner plusieurs cha\u00eenes de caract\u00e8res\u2009:

    char *str = \"Hello, \"\n            \"World!\";\n

    Dans cet exemple, str contiendra la cha\u00eene de caract\u00e8res Hello, World!.

    ", "tags": ["str"]}, {"location": "course-c/20-composite-types/strings/#chaine-de-caracteres-constantes", "title": "Cha\u00eene de caract\u00e8res constantes", "text": "

    Les cha\u00eenes de caract\u00e8res constantes sont des cha\u00eenes de caract\u00e8res qui ne peuvent pas \u00eatre modifi\u00e9es. Elles sont stock\u00e9es dans la section .rodata de la m\u00e9moire. Pour d\u00e9clarer une cha\u00eene de caract\u00e8res constante, il est n\u00e9cessaire d'utiliser le mot-cl\u00e9 const :

    const char *str = \"Hello, World!\";\n

    Notez que la d\u00e9claration est un peu diff\u00e9rente de la d\u00e9claration d'une cha\u00eene de caract\u00e8res classique. Ici nous n'utilisons plus la notation [] mais un pointeur *.

    ", "tags": ["const"]}, {"location": "course-c/20-composite-types/structures/", "title": "Structures", "text": "

    Les structures sont des d\u00e9clarations sp\u00e9cifiques permettant de regrouper une liste de variables dans un m\u00eame bloc m\u00e9moire et permettant de s'y r\u00e9f\u00e9rer \u00e0 partir d'une r\u00e9f\u00e9rence commune. Historiquement le type struct a \u00e9t\u00e9 d\u00e9riv\u00e9 de ALGOL 68. Il est \u00e9galement utilis\u00e9 en C++ et est similaire \u00e0 une classe.

    Il faut voir une structure comme un conteneur \u00e0 variables qu'il est possible de v\u00e9hiculer comme un tout.

    La structure suivante d\u00e9crit un agr\u00e9gat de trois grandeurs scalaires formant un point tridimensionnel\u2009:

    struct {\n    double x;\n    double y;\n    double z;\n};\n

    Il ne faut pas confondre l'\u00e9criture ci-dessus avec ceci, dans lequel il y a un bloc de code avec trois variables locales d\u00e9clar\u00e9es\u2009:

    {\n    double x;\n    double y;\n    double z;\n};\n

    En utilisant le mot-cl\u00e9 struct devant un bloc, les variables d\u00e9clar\u00e9es au sein de ce bloc ne seront pas r\u00e9serv\u00e9es en m\u00e9moire. Autrement dit, il ne sera pas possible d'acc\u00e9der \u00e0 x puisqu'il n'existe pas de variable x. En revanche, un nouveau conteneur contenant trois variables est d\u00e9fini, mais pas encore d\u00e9clar\u00e9.

    La structure ainsi d\u00e9clar\u00e9e n'est pas tr\u00e8s utile telle quelle, en revanche elle peut-\u00eatre utilis\u00e9e pour d\u00e9clarer une variable de type struct :

    struct {\n    double x;\n    double y;\n    double z;\n} point;\n

    \u00c0 pr\u00e9sent on a d\u00e9clar\u00e9 une variable point de type struct contenant trois \u00e9l\u00e9ments de type double. L'affectation d'une valeur \u00e0 cette variable utilise l'op\u00e9rateur . :

    point.x = 3060426.957;\npoint.y = 3192003.220;\npoint.z = 4581359.381;\n

    Comme point n'est pas une primitive standard, mais un conteneur \u00e0 primitive, il n'est pas correct d'\u00e9crire point = 12. Il est essentiel d'indiquer quel \u00e9l\u00e9ment de ce conteneur on souhaite acc\u00e9der.

    Ces coordonn\u00e9es sont un clin d'\u0153il aux Pierres du Niton qui sont deux blocs de roche erratiques d\u00e9pos\u00e9s par le glacier du Rh\u00f4ne lors de son retrait apr\u00e8s la derni\u00e8re glaciation. Les coordonn\u00e9es sont exprim\u00e9es selon un rep\u00e8re g\u00e9ocentr\u00e9\u2009; l'origine \u00e9tant le centre de la Terre. Ces pierres sont donc situ\u00e9es \u00e0 4.5 km du centre de la Terre, une valeur qui aurait repr\u00e9sent\u00e9, pour \u00eatre convenablement d\u00e9termin\u00e9e, un sacr\u00e9 d\u00e9fi pour Axel Lidenbrock et son fulmicoton.

    G\u00e9n\u00e9ralement les structures sont utilis\u00e9es pour communiquer des donn\u00e9es complexes entre diff\u00e9rentes fonctions d'un m\u00eame fichier ou entre diff\u00e9rents fichiers. C'est pour cette raison que l'on retrouve g\u00e9n\u00e9ralement ces d\u00e9finitions en dehors de toute fonction\u2009:

    struct Point {\n    double x;\n    double y;\n};\n\nstruct Point point_add(struct Point a, struct Point b) {\n    return (struct Point){\n        .x = a.x + b.x,\n        .y = a.y + b.y\n    };\n}\n\nvoid point_print(struct Point p) {\n    printf(\"(%g,%g)\", p.x, p.y);\n}\n\nint main(void) {\n    struct Point p = {1., 2.};\n    struct Point q = {3., 4.};\n    struct Point r = point_add(p, q);\n    point_print(r);\n}\n
    ", "tags": ["double", "point", "struct"]}, {"location": "course-c/20-composite-types/structures/#structures-nommees", "title": "Structures nomm\u00e9es", "text": "

    L'\u00e9criture que l'on a vue initialement struct { ... }; est appel\u00e9e structure anonyme, c'est-\u00e0-dire qu'elle n'a pas de nom. Telle quelle elle ne peut pas \u00eatre utilis\u00e9e et elle ne sert donc pas \u00e0 grand chose. En revanche, il est possible de d\u00e9clarer une variable de ce type en ajoutant un identificateur \u00e0 la fin de la d\u00e9claration struct { ... } nom;. N\u00e9anmoins la structure est toujours anonyme.

    Le langage C pr\u00e9voit la possibilit\u00e9 de nommer une structure pour une utilisation ult\u00e9rieure en rajoutant un nom apr\u00e8s le mot cl\u00e9 struct :

    struct Point {\n    double x;\n    double y;\n    double z;\n};\n

    Pour ne pas confondre un nom de structure avec un nom de variable, on pr\u00e9f\u00e9rera un identificateur en capitales ou en \u00e9criture camel-case. Maintenant qu'elle est nomm\u00e9e, il est possible de d\u00e9clarer plusieurs variables de ce type ailleurs dans le code\u2009:

    struct Point foo;\nstruct Point bar;\n

    Dans cet exemple, on d\u00e9clare deux variables foo et bar de type struct Point. Il est donc possible d'acc\u00e9der \u00e0 foo.x ou bar.z.

    Rien n'emp\u00eache de d\u00e9clarer une structure nomm\u00e9e et d'\u00e9galement d\u00e9clarer une variable par la m\u00eame occasion\u2009:

    struct Point {\n    double x;\n    double y;\n    double z;\n} foo;\nstruct Point bar;\n

    Notons que les noms de structures sont stock\u00e9s dans un espace de noms diff\u00e9rent de celui des variables. C'est-\u00e0-dire qu'il n'y a pas de collision possible et qu'un identifiant de fonction ou de variable ne pourra jamais \u00eatre compar\u00e9 \u00e0 un identifiant de structure. Aussi, l'\u00e9criture suivante, bien que perturbante, est tout \u00e0 fait possible\u2009:

    struct point { double x; double y; double z; };\nstruct point point;\npoint.x = 42;\n
    ", "tags": ["foo.x", "struct", "foo", "bar", "bar.z"]}, {"location": "course-c/20-composite-types/structures/#initialisation", "title": "Initialisation", "text": "

    Une structure se comporte \u00e0 peu de chose pr\u00e8s comme un tableau sauf que les \u00e9l\u00e9ments de la structure ne s'acc\u00e8dent pas avec l'op\u00e9rateur crochet, [] mais avec l'op\u00e9rateur .. N\u00e9anmoins une structure est repr\u00e9sent\u00e9e en m\u00e9moire comme un contenu lin\u00e9aire. Notre structure struct Point serait identique \u00e0 un tableau de trois double et par cons\u00e9quent l'initialisation suivante est possible\u2009:

    struct Point point = { 3060426.957, 3192003.220, 4581359.381 };\n

    N\u00e9anmoins on pr\u00e9f\u00e8rera la notation suivante, \u00e9quivalente\u2009:

    struct Point point = { .x=3060426.957, .y=3192003.220, .z=4581359.381 };\n

    Comme pour un tableau, les valeurs omises sont initialis\u00e9es \u00e0 z\u00e9ro. Et de la m\u00eame mani\u00e8re qu'un tableau, il est possible d'initialiser une structure \u00e0 z\u00e9ro avec = {0};.

    Il faut savoir que C99 restreint l'ordre dans lequel les \u00e9l\u00e9ments peuvent \u00eatre initialis\u00e9s. Ce dernier doit \u00eatre l'ordre dans lequel les variables sont d\u00e9clar\u00e9es dans la structure.

    Notons que des structures comportant des types diff\u00e9rents peuvent aussi \u00eatre initialis\u00e9es de la m\u00eame mani\u00e8re\u2009:

    struct Product {\n    int weight; // Grams\n    double price; // Swiss francs\n    int category;\n    char name[64];\n}\n\nstruct Product apple = {321, 0.75, 24, \"Pomme Golden\"};\n
    ", "tags": ["double"]}, {"location": "course-c/20-composite-types/structures/#tableaux-de-structures", "title": "Tableaux de structures", "text": "

    Une structure est un type comme un autre. Tout ce qui peut \u00eatre fait avec char ou double peut donc \u00eatre fait avec struct. Et donc, il est aussi possible de d\u00e9clarer un tableau de structures. Ici, donnons l'exemple d'un tableau de points initialis\u00e9s\u2009:

    struct Point points[3] = {\n    {.x=1, .y=2, .z=3},\n    {.z=1, .x=2, .y=3},\n    {.y=1}\n};\n

    Assigner une nouvelle valeur \u00e0 un point est facile\u2009:

    point[2].x = 12;\n
    ", "tags": ["double", "char", "struct"]}, {"location": "course-c/20-composite-types/structures/#structures-en-parametres", "title": "Structures en param\u00e8tres", "text": "

    L'int\u00e9r\u00eat d'une structure est de pouvoir passer ou retourner un ensemble de donn\u00e9es \u00e0 une fonction. On a vu qu'une fonction ne permet de retourner qu'une seule primitive. Une structure est ici consid\u00e9r\u00e9e comme un seul conteneur et l'\u00e9criture suivante est possible\u2009:

    struct Point generate_point(void) {\n    struct Point p = {\n        .x = rand(),\n        .y = rand(),\n        .z = rand()\n    };\n\n    return p;\n}\n

    Il est \u00e9galement possible de passer une structure en param\u00e8tre d'une fonction\u2009:

    double norm(struct point p) {\n    return sqrt(p.x * p.x + p.y * p.y + p.z * p.z);\n}\n\nint main(void) {\n    struct Point p = { .x = 12.54, .y = -8.12, .z = 0.68 };\n\n    double n = norm(p);\n}\n

    Contrairement aux tableaux, les structures sont toujours pass\u00e9es par valeur, c'est-\u00e0-dire que l'entier du contenu de la structure sera copi\u00e9 sur la pile (stack) en cas d'appel \u00e0 une fonction. En revanche, en cas de passage par pointeur, seule l'adresse de la structure est pass\u00e9e \u00e0 la fonction appel\u00e9e qui peut d\u00e8s lors modifier le contenu\u2009:

    struct Point {\n    double x;\n    double y;\n};\n\nvoid foo(struct Point m, struct Point *n) {\n    m.x++;\n    n->x++;\n}\n\nint main(void) {\n    struct Point p = {0}, q = {0};\n    foo(p, &q);\n    printf(\"%g, %g\\n\", p.x, q.x);\n}\n

    Le r\u00e9sultat affich\u00e9 sera 0.0, 1.0. Seule la seconde valeur est modifi\u00e9e.

    Hint

    Lorsqu'un membre d'une structure est acc\u00e9d\u00e9, via son pointeur, on utilise la notation -> au lieu de . car il est n\u00e9cessaire de d\u00e9r\u00e9f\u00e9rencer le pointeur. Il s'agit d'un sucre syntaxique permettant d'\u00e9crire p->x au lieu de (*p).x

    "}, {"location": "course-c/20-composite-types/structures/#structures-flexibles", "title": "Structures flexibles", "text": "

    Introduits avec C99, les membres de structures flexibles ou flexible array members (\u00a76.7.2.1) est un membre de type tableau d'une structure d\u00e9fini sans dimension. Ces membres ne peuvent appara\u00eetre qu'\u00e0 la fin d'une structure.

    struct Vector {\n    char name[16]; // name of the vector\n    size_t len; // length of the vector\n    double array[]; // flexible array member\n};\n

    Cette \u00e9criture permet par exemple de r\u00e9server un espace m\u00e9moire plus grand que la structure de base, et d'utiliser le reste de l'espace comme tableau flexible.

    struct Vector *vector = malloc(1024);\nstrcpy(vector->name, \"Mon vecteur\");\nvector->len = 1024 - 16 - 4;\nfor (int i = 0; i < vector->len; i++)\n    vector->array[i] = ...\n

    Ce type d'\u00e9criture est souvent utilis\u00e9 pour des contenus ayant un en-t\u00eate fixe comme des images BMP ou des fichiers sons WAVE.

    "}, {"location": "course-c/20-composite-types/structures/#structure-de-structures", "title": "Structure de structures", "text": "

    On comprend ais\u00e9ment que l'avantage des structures et le regroupement de variables. Une structure peut \u00eatre la composition d'autres types composites.

    Nous d\u00e9clarons ici une structure struct Line compos\u00e9e de struct Point :

    struct Line {\n    struct Point a;\n    struct Point b;\n};\n

    L'acc\u00e8s \u00e0 ces diff\u00e9rentes valeurs s'effectue de la fa\u00e7on suivante\u2009:

    struct Line line = {.a.x = 23, .a.y = 12, .b.z = 33};\nprintf(\"%g, %g\", line.a.x, line.b.x);\n
    "}, {"location": "course-c/20-composite-types/structures/#alignement-memoire", "title": "Alignement m\u00e9moire", "text": "

    Une structure est agenc\u00e9e en m\u00e9moire dans l'ordre de sa d\u00e9claration. C'est donc un agencement lin\u00e9aire en m\u00e9moire\u2009:

    struct Line lines[2]; // Chaque point est un double, cod\u00e9 sur 8 bytes.\n

    Ci-dessous est repr\u00e9sent\u00e9 l'offset m\u00e9moire (en bytes) \u00e0 laquelle est stock\u00e9 chaque membre de la structure, ainsi que l'\u00e9l\u00e9ment correspondant.

    0x0000 line[0].a.x\n0x0008 line[0].a.y\n0x0010 line[0].a.z\n0x0018 line[0].b.x\n0x0020 line[0].b.y\n0x0028 line[0].b.z\n0x0030 line[1].a.x\n0x0038 line[1].a.y\n0x0040 line[1].a.z\n0x0048 line[1].b.x\n0x0050 line[1].b.y\n0x0048 line[1].b.z\n

    N\u00e9anmoins dans certains cas, le compilateur se r\u00e9serve le droit d'optimiser l' alignement m\u00e9moire. Une architecture 32-bits aura plus de facilit\u00e9 \u00e0 acc\u00e9der \u00e0 des grandeurs de 32 bits or, une structure compos\u00e9e de plusieurs entiers 8-bits demanderait au processeur un co\u00fbt additionnel pour optimiser le stockage d'information. Consid\u00e9rons par exemple la structure suivante\u2009:

    struct NoAlign\n{\n    int8_t c;\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n};\n

    Imaginons pour comprendre qu'un casier m\u00e9moire sur une architecture 32-bits est assez grand pour y stocker 4 bytes. Tentons de repr\u00e9senter en m\u00e9moire cette structure en little-endian, en consid\u00e9rant des casiers de 32-bits\u2009:

     c    d             i              a\n\u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502c\u2502d\u2502d\u2502d\u2502 \u2502d\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502a\u2502a\u2502a\u2502\n\u25020\u25020\u25021\u25022\u2502 \u25023\u25020\u25021\u25022\u2502 \u25023\u25024\u25025\u25026\u2502 \u25027\u25020\u25021\u25022\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D\n

    On constate que la valeur d est \u00e0 cheval entre deux casiers. De m\u00eame que la valeur i est r\u00e9partie sur trois casiers au lieu de deux. Le processeur communique avec la m\u00e9moire en utilisant des bus m\u00e9moire, ils sont l'analogie d'une autoroute qui ne peut accueillir que des voitures, chacune ne pouvant transporter que 4 passagers. Un passager ne peut pas arpenter l'autoroute sans voiture. Le processeur est la gare de triage et s'occupe de r\u00e9assembler les passagers, et l'op\u00e9ration consistant \u00e0 demander \u00e0 un passager de sortir de la voiture B pour s'installer dans une autre, ou m\u00eame se d\u00e9placer de la place du conducteur \u00e0 la place du passager arri\u00e8re prend du temps.

    Le compilateur sera donc oblig\u00e9 de faire du z\u00e8le pour acc\u00e9der \u00e0 d. formellement l'acc\u00e8s \u00e0 d pourrait s'\u00e9crire ainsi\u2009:

    int32_t d = (data[0] << 8) | (data[1] & 0x0F);\n

    Pour \u00e9viter ces man\u0153uvres, le compilateur, selon l'architecture donn\u00e9e, va ins\u00e9rer des \u00e9l\u00e9ments de rembourrage (padding) pour forcer l'alignement m\u00e9moire et ainsi optimiser les lectures. Ce sont des \u00e9l\u00e9ments vides qui ne sont pas accessibles par l'utilisateur final. Dit autrement, c'est comme choisir de ne mettre qu'un seul passager dans la voiture. Ce n'est pas optimal sur le plan du bilan carbone, mais c'est plus rapide pour le processeur.

    La m\u00eame structure que ci-dessus sera fort probablement impl\u00e9ment\u00e9e de la fa\u00e7on suivante\u2009:

    struct Align\n{\n    int8_t c;\n    int8_t __pad1[3]; // Ins\u00e9r\u00e9 par le compilateur\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n    int8_t __pad2; // Ins\u00e9r\u00e9 par le compilateur\n};\n

    En reprenant notre analogie de voitures, le stockage est maintenant fait comme ceci\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502c\u2502 \u2502 \u2502 \u2502 \u2502d\u2502d\u2502d\u2502d\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502a\u2502a\u2502a\u2502 \u2502\n\u25020\u2502 \u2502 \u2502 \u2502 \u25020\u25021\u25022\u25023\u2502 \u25020\u25021\u25022\u25023\u2502 \u25024\u25025\u25026\u25027\u2502 \u25020\u25021\u25022\u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D         E\n

    Le compromis est qu'une voiture suppl\u00e9mentaire est n\u00e9cessaire, mais le processeur n'a plus besoin de r\u00e9agencer les passagers. L'acc\u00e8s \u00e0 d est ainsi facilit\u00e9 au d\u00e9triment d'une perte substantielle de l'espace de stockage.

    Ceci \u00e9tant, en changeant l'ordre des \u00e9l\u00e9ments dans la structure pour que chaque membre soit align\u00e9 sur 32-bits, il est possible d'obtenir un meilleur compromis\u2009:

    struct Align\n{\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n    int8_t c;\n};\n
    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502d\u2502d\u2502d\u2502d\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502a\u2502a\u2502a\u2502c\u2502\n\u25020\u25021\u25022\u25023\u2502 \u25020\u25021\u25022\u25023\u2502 \u25024\u25025\u25026\u25027\u2502 \u25020\u25021\u25022\u25023\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D\n

    L'option -Wpadded de GCC permet de lever une alerte lorsqu'une structure est align\u00e9e par le compilateur. Si l'on utilise par exemple une structure pour \u00e9crire un fichier binaire respectant un format pr\u00e9cis par exemple l'en-t\u00eate d'un fichier BMP. Et que cette structure BitmapFileHeader est enregistr\u00e9e avec fwrite(header, sizeof(BitmapFileHeader), ...). Si le compilateur rajoute des \u00e9l\u00e9ments de rembourrage, le fichier BMP serait alors compromis. Il faudrait donc consid\u00e9rer l'alerte Wpadded comme une erreur critique.

    Pour pallier \u00e0 ce probl\u00e8me, lorsqu'une structure m\u00e9moire doit \u00eatre respect\u00e9e dans un ordre pr\u00e9cis. Une option de compilation non standard existe. La directive #pragma pack permet de forcer un type d'alignement pour une certaine structure. Consid\u00e9rons par exemple la structure suivante\u2009:

    struct Test\n{\n    char a;\n    int b;\n    char c;\n};\n

    Elle serait tr\u00e8s probablement repr\u00e9sent\u00e9e en m\u00e9moire de la fa\u00e7on suivante\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502a\u2502 \u2502 \u2502 \u2502 \u2502b\u2502b\u2502b\u2502b\u2502 \u2502c\u2502 \u2502 \u2502 \u2502\n\u25020\u2502 \u2502 \u2502 \u2502 \u25020\u25021\u25022\u25023\u2502 \u25020\u2502 \u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C\n

    En revanche si elle est d\u00e9crite en utilisant un packing sur 8-bits, avec #pragma pack(1) on aura l'alignement m\u00e9moire suivant\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502a\u2502b\u2502b\u2502b\u2502 \u2502b\u2502c\u2502 \u2502 \u2502\n\u25020\u25020\u25021\u25022\u2502 \u25023\u25021\u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B\n
    ", "tags": ["Wpadded"]}, {"location": "course-c/20-composite-types/structures/#fichier-wav", "title": "Fichier WAV", "text": "

    Le format de fichier WAV est un format de fichier standard pour les fichiers audio num\u00e9riques. Il est bas\u00e9 sur le format RIFF (Resource Interchange File Format) qui est un format de fichier g\u00e9n\u00e9rique pour l'\u00e9change de donn\u00e9es. Ce format est tr\u00e8s ancien et a \u00e9t\u00e9 introduit par Microsoft en 1991. Il est n\u00e9anmoins encore utilis\u00e9 aujourd'hui pour stocker des fichiers audio non compress\u00e9s car il est simple et ne n\u00e9cessite pas de licence.

    Prenons l'exemple concr\u00eat de la structure d'un fichier audio WAV\u2009:

    Fichier WAV

    Cette structure peut \u00eatre d\u00e9finie de la fa\u00e7on suivante\u2009:

    #pragma pack(1)\nstruct WavHeader\n{\n    char riff_tag[4];\n    uint32_t file_size;\n    char wave_tag[4];\n    char fmt_tag[4];\n    uint32_t fmt_length;\n    uint16_t audio_format;\n    uint16_t num_channels;\n    uint32_t sample_rate;\n    uint32_t byte_rate;\n    uint16_t block_align;\n    uint16_t bits_per_sample;\n    char data_tag[4];\n    uint32_t data_size;\n};\n

    Conserver le bon alignement m\u00e9moire est ici crutial car l'objectif est d'\u00e9crire les donn\u00e9es dans un fichier audio. Voici l'exemple d'un programme qui g\u00e9n\u00e8re un fichier audio\u2009:

    #include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <math.h>\n\n#define SAMPLE_RATE 44100 // Fr\u00e9quence d'\u00e9chantillonnage [Hz]\n#define AMPLITUDE 32760   // Amplitude du signal (valeur maximale pour 16 bits)\n\n#pragma pack(1)\nstruct WavHeader\n{\n    char riff_tag[4];\n    uint32_t file_size;\n    char wave_tag[4];\n    char fmt_tag[4];\n    uint32_t fmt_length;\n    uint16_t audio_format;\n    uint16_t num_channels;\n    uint32_t sample_rate;\n    uint32_t byte_rate;\n    uint16_t block_align;\n    uint16_t bits_per_sample;\n    char data_tag[4];\n    uint32_t data_size;\n};\n\nint generate_sine_wave(const char *filename, double frequency, double duration)\n{\n    const int num_samples = (int)(SAMPLE_RATE * duration);\n    int16_t *buffer = (int16_t *)malloc(num_samples * sizeof(int16_t));\n\n    // Remplissage du buffer avec le signal sinusoidal\n    for (int i = 0; i < num_samples; ++i) {\n        buffer[i] = (int16_t)(AMPLITUDE * sin(2.0 * M_PI *\n                              frequency * i / SAMPLE_RATE));\n    }\n\n    // Initialisation de l'en-t\u00eate WAV avec une liste d'initialisation\n    struct WavHeader header = {\n        .riff_tag = {'R', 'I', 'F', 'F'},\n        .file_size = 36 + num_samples * sizeof(int16_t),\n        .wave_tag = {'W', 'A', 'V', 'E'},\n        .fmt_tag = {'f', 'm', 't', ' '},\n        .fmt_length = 16,\n        .audio_format = 1,\n        .num_channels = 1,\n        .sample_rate = SAMPLE_RATE,\n        .byte_rate = SAMPLE_RATE * sizeof(int16_t),\n        .block_align = sizeof(int16_t),\n        .bits_per_sample = 16,\n        .data_tag = {'d', 'a', 't', 'a'},\n        .data_size = num_samples * sizeof(int16_t)};\n\n    // \u00c9criture dans le fichier\n    FILE *file = fopen(filename, \"wb\");\n    if (!file)\n    {\n        printf(\"Erreur lors de l'ouverture du fichier %s\\n\", filename);\n        free(buffer);\n        return -1;\n    }\n\n    fwrite(&header, sizeof(struct WavHeader), 1, file);\n    fwrite(buffer, sizeof(int16_t), num_samples, file);\n    fclose(file);\n    free(buffer);\n    return 0;\n}\n\nint main() {\n    generate_sine_wave(\"sine.wav\", 440.0, 5.0);\n}\n
    "}, {"location": "course-c/20-composite-types/structures/#champs-de-bits", "title": "Champs de bits", "text": "

    Les champs de bits sont des structures dont une information suppl\u00e9mentaire est ajout\u00e9e\u2009: le nombre de bits utilis\u00e9s.

    Prenons l'exemple du module I2C du microcontr\u00f4leur TMS320F28335. Le registre I2CMDR d\u00e9crit \u00e0 la page 23 est un registre 16-bits qu'il conviendrait de d\u00e9crire avec un champ de bits\u2009:

    struct I2CMDR {\n    int  bc  :3;\n    bool fdf :1;\n    bool stb :1;\n    bool irs :1;\n    bool dlb :1;\n    bool rm  :1;\n    bool xa  :1;\n    bool trx :1;\n    bool mst :1;\n    bool stp :1;\n    bool _reserved :1;\n    bool stt  :1;\n    bool free :1;\n    bool nackmod :1;\n};\n

    Activer le bit stp (bit num\u00e9ro 12) devient une op\u00e9ration triviale\u2009:

    struct I2CMDR i2cmdr;\n\ni2cmdr.stp = true;\n

    Alors qu'elle demanderait une manipulation de bit sinon\u2009:

    int32_t i2cmdr;\n\ni2cmdr |= 1 << 12;\n

    Notons que les champs de bits, ainsi que les structures seront d\u00e9clar\u00e9s diff\u00e9remment selon que l'architecture cible est little-endian ou big-endian.

    Cette technique est particuli\u00e8rement utile pour manipuler des registres de microcontr\u00f4leurs ou des fichiers binaires, elle est souvent coupl\u00e9e \u00e0 des unions pour permettre un acc\u00e8s soit par champ de bits, soit par entier.

    ", "tags": ["I2CMDR", "stp"]}, {"location": "course-c/20-composite-types/structures/#compound-literals", "title": "Compound Literals", "text": "

    Na\u00efvement traduit en litt\u00e9raux compos\u00e9s, un compound literal est une m\u00e9thode de cr\u00e9ation d'un type compos\u00e9 \u00ab\u2009\u00e0 la vol\u00e9e\u2009\u00bb utilis\u00e9 de la m\u00eame fa\u00e7on que les transtypages.

    Reprenons notre structure Point struct Point vue plus haut. Si l'on souhaite changer la valeur du point p il faudrait on pourrait \u00e9crire ceci\u2009:

    struct Point p; // D\u00e9clar\u00e9 plus haut\n\n// ...\n\n{\n    struct Point q = {.x=1, .y=2, .z=3};\n    p = q;\n}\n

    Notons que passer par une variable interm\u00e9diaire q n'est pas tr\u00e8s utile. Il serait pr\u00e9f\u00e9rable d'\u00e9crire ceci\u2009:

    p = {.x=1, .y=2, .z=3};\n

    N\u00e9anmoins cette \u00e9criture m\u00e8nera \u00e0 une erreur de compilation, car le compilateur cherchera \u00e0 d\u00e9terminer le type de l'expression {.x=1, .y=2, .z=3}. Il est alors essentiel d'utiliser la notation suivante\u2009:

    p = (struct Point){.x=1, .y=2, .z=3};\n

    Cette notation de litt\u00e9raux compos\u00e9s peut \u00e9galement s'appliquer aux tableaux. L'exemple suivant montre l'initialisation d'un tableau \u00e0 la vol\u00e9e pass\u00e9 \u00e0 la fonction foo :

    void foo(int array[3]) {\n    for (int i = 0; i < 3; i++) printf(\"%d \", array[i]);\n}\n\nvoid main() {\n    foo((int []){1,2,3});\n}\n

    Exercice 1\u2009: Mendele\u00efev

    Chaque \u00e9l\u00e9ment du tableau p\u00e9riodique des \u00e9l\u00e9ments comporte les propri\u00e9t\u00e9s suivantes\u2009:

    • Un nom jusqu'\u00e0 20 lettres
    • Un symbole jusqu'\u00e0 2 lettres
    • Un num\u00e9ro atomique de 1 \u00e0 118 (2019)
    • Le type de l'\u00e9l\u00e9ment

      • M\u00e9taux (Alcalin, Alcalino-terreux, Lanthanides, Actinides, M\u00e9taux de transition, M\u00e9taux pauvres)
      • M\u00e9tallo\u00efdes
      • Non-m\u00e9taux (Autres, Halog\u00e8ne, Gaz noble)
    • La p\u00e9riode\u2009: un entier de 1 \u00e0 7

    • Le groupe\u2009: un entier de 1 \u00e0 18

    D\u00e9clarer une structure de donn\u00e9es permettant de stocker tous les \u00e9l\u00e9ments chimiques de telle fa\u00e7on qu'ils puissent \u00eatre acc\u00e9d\u00e9s comme\u2009:

    assert(strcmp(table.element[6].name, \"Helium\") == 0);\nassert(strcmp(table.element[54].type, \"Gaz noble\") == 0);\nassert(table.element[11].period == 3);\n\nElement *el = table.element[92];\nassert(el->atomic_weight == 92);\n
    ", "tags": ["foo"]}, {"location": "course-c/20-composite-types/structures/#creation-de-types", "title": "Cr\u00e9ation de types", "text": "

    Le mot cl\u00e9 typedef permet de d\u00e9clarer un nouveau type. Il est particuli\u00e8rement utilis\u00e9 conjointement avec les structures et les unions afin de s'affranchir de la lourdeur d'\u00e9criture (pr\u00e9fixe struct), et dans le but de cacher la complexit\u00e9 d'un type \u00e0 l'utilisateur qui le manipule.

    L'exemple suivant d\u00e9clare un type Point et un prototype de fonction permettant l'addition de deux points.

    typedef struct {\n    double x;\n    double y;\n} Point;\n\nPoint add(Point a, Point b);\n
    ", "tags": ["typedef", "Point", "struct"]}, {"location": "course-c/20-composite-types/unions/", "title": "Unions", "text": "

    Une union est une variable qui peut avoir plusieurs repr\u00e9sentations d'un m\u00eame contenu m\u00e9moire. Rappelez-vous, nous nous demandions quelle \u00e9tait l'interpr\u00e9tation d'un contenu m\u00e9moire donn\u00e9. Il est possible en C d'avoir toutes les interpr\u00e9tations \u00e0 la fois\u2009:

    #include <stdint.h>\n#include <stdio.h>\n\nunion Mixed\n{\n    int32_t signed32;\n    uint32_t unsigned32;\n    int8_t signed8[4];\n    int16_t signed16[2];\n    float float32;\n};\n\nint main(void) {\n    union Mixed m = {\n        .signed8 = {0b11011011, 0b0100100, 0b01001001, 0b01000000}\n    };\n\n    printf(\n        \"int32_t\\t%d\\n\"\n        \"uint32_t\\t%u\\n\"\n        \"char\\t%c, %c, %c, %c\\n\"\n        \"short\\t%hu, %hu\\n\"\n        \"float\\t%f\\n\",\n        m.signed32,\n        m.unsigned32,\n        m.signed8[0], m.signed8[1], m.signed8[2], m.signed8[3],\n        m.signed16[0], m.signed16[1],\n        m.float32\n    );\n}\n

    Les unions sont tr\u00e8s utilis\u00e9es en combinaison avec des champs de bits. Pour reprendre l'exemple du champ de bit \u00e9voqu\u00e9 plus haut, on peut souhaiter acc\u00e9der au registre soit sous la forme d'un entier 16-bits soit via chacun de ses bits ind\u00e9pendamment.

    union i2cmdr {\n    struct {\n        int  bc  :3;\n        bool fdf :1;\n        bool stb :1;\n        bool irs :1;\n        bool dlb :1;\n        bool rm  :1;\n        bool xa  :1;\n        bool trx :1;\n        bool mst :1;\n        bool stp :1;\n        bool _reserved :1;\n        bool stt  :1;\n        bool free :1;\n        bool nackmod :1;\n    } bits;\n    uint16_t all;\n};\n

    Dans cet exemple on peut soit acc\u00e9der \u00e0 l'ensemble des bits via le champ all soit \u00e0 chacun des bits via les champs bc, fdf, stb, etc.

    union i2cmdr cmdr = { .all = 0x1234 };\n\ncmdr.bits.bc = 0b101;\n\nuint16_t all = cmdr.all;\n

    Les unions peuvent \u00eatre imbriqu\u00e9es, c'est-\u00e0-dire contenir des unions elles-m\u00eames. Cela permet de d\u00e9finir des structures de donn\u00e9es complexes.

    union {\n    union {\n        int a;\n        int b;\n    } u1;\n    union {\n        int c;\n        int d;\n    } u2;\n} u;\n
    ", "tags": ["fdf", "all", "stb"]}, {"location": "course-c/20-composite-types/unions/#taille", "title": "Taille", "text": "

    La taille d'une union est \u00e9gale \u00e0 la taille de son plus grand champ. Donc dans l'exemple suivant, la taille de u est de 4 octets.

    union {\n    char c;\n    int i;\n} u;\n
    "}, {"location": "course-c/25-architecture-and-systems/computer/", "title": "L'ordinateur", "text": "

    Un ordinateur personnel (PC pour Personal Computer) est un appareil \u00e9lectronique de petite taille destin\u00e9 \u00e0 un usage individuel. Il se distingue des ordinateurs centraux (ou mainframes) et des serveurs, qui sont destin\u00e9s \u00e0 un usage professionnel ou collectif.

    N\u00e9anmoins, quelle que soit la taille de l'ordinateur, les composants de base sont les m\u00eames. Un ordinateur est compos\u00e9 de plusieurs \u00e9l\u00e9ments principaux\u2009:

    • Un processeur (ou CPU pour Central Processing Unit) qui ex\u00e9cute les instructions des programmes.
    • De la m\u00e9moire (ou RAM pour Random Access Memory) qui stocke les donn\u00e9es et les instructions des programmes en cours d'ex\u00e9cution.
    • Un disque dur (ou HDD pour Hard Disk Drive) qui stocke les donn\u00e9es de mani\u00e8re permanente.
    • Une carte graphique qui affiche les images \u00e0 l'\u00e9cran.
    • Une carte m\u00e8re qui relie tous les composants entre eux.
    "}, {"location": "course-c/25-architecture-and-systems/computer/#la-ram", "title": "La RAM", "text": "

    La m\u00e9moire vive est une m\u00e9moire de stockage temporaire, on l'appelle \u00e9galement m\u00e9moire non volatile. Le plus souvent une m\u00e9moire vive est amovible, il s'agit d'une barrette enfichable sur la carte m\u00e8re. Avec l'\u00e9volution de la technologie, ces m\u00e9moires sont car\u00e9n\u00e9es et munies d'un dissipateur thermique\u2009:

    2 x 16 GB DDR5 DIMM Corsair Vengeance

    Sous le cap\u00f4t, on peut voir les puces de m\u00e9moire\u2009:

    Crucial DDR4 16 GB

    Cette m\u00e9moire dispose de 16 Gibioctets de m\u00e9moire, soit \\(16 \\times 2^30 = 17179869184\\) octets. Chaque octet est compos\u00e9 de \\(8\\) bits, soit \\(17179869184 \\times 8 = 137438953472\\) bits. Comme nous voyons \\(4\\) puces de m\u00e9moire, chaque puce contient \\(4\\) Gibioctets.

    G\u00e9n\u00e9ralement, ces m\u00e9moires sont vendues en nombre de bits, soit ici 32 Gibibits.

    Sur le circuit \u00e9lectronique ou PCB (Printed Circuit Board), on voit les 4 puces de m\u00e9moire soud\u00e9es. Il s'agit d'un composant de la soci\u00e9t\u00e9 Micron, un MT40A1G8. La structure interne de cette m\u00e9moire est donn\u00e9e par la datasheet du composant\u2009:

    MT40A1G8

    Pour d\u00e9coder ce sch\u00e9ma, int\u00e9ressons-nous aux fl\u00e8ches de couleur. Il s'agit du bus d'adresse. Ce bus comporte 16 lignes en parall\u00e8le qui sont interfac\u00e9es \u00e0 deux blocs\u2009: le Row Address MUX et le Column address counter. Ces deux blocs permettent de s\u00e9lectionner une cellule m\u00e9moire selon la m\u00e9moire, une cellule peut valoir 4, 8, 16 ou 32 bits.

    Les cellules m\u00e9moires sont organis\u00e9es and matrice ligne/colonne et chaque matrice est organis\u00e9e en banque. C'est ce qu'on observe sur ce diagramme.

    Une m\u00e9moire volatile est une m\u00e9moire qui perd son contenu lorsqu'elle n'est plus aliment\u00e9e en \u00e9lectricit\u00e9. La raison est simple. Stocker un \u00e9tat \u00e9lectrique demande de l'\u00e9nergie pour accumuler des charges \u00e9lectriques. Si l'on fait l'analogie que l'\u00e9lectricit\u00e9 est de l'eau, alors chaque bit de la m\u00e9moire est un verre d'eau que l'on peut remplir ou vider. Le seul moyen de lire le contenu du verre est de voir s'il y a de l'eau dedans, c'est-\u00e0-dire de le vider. Si le verre est grand, alors il faut plus de temps pour le remplir et plus de temps pour le vider ceci pr\u00e9sente plusieurs inconv\u00e9nients\u2009:

    1. La vitesse de lecture est plus lente.
    2. La quantit\u00e9 d'eau (courant) pour remplir le verre est plus grande.
    3. L'encombrement est plus grand puisque le verre est plus volumineux.

    Aussi, le choix technologique est d'avoir des tout petits verres. Ils sont si petits que l'eau contenue s'\u00e9vapore tr\u00e8s vite. Pour \u00e9viter cela, on doit constamment remplir les verres. C'est ce que l'on appelle la rafra\u00eechissement de la m\u00e9moire. P\u00e9riodiquement, environ toutes les 64 ms, on doit r\u00e9\u00e9crire le contenu de la m\u00e9moire pour \u00e9viter que l'information ne se perde. Heureusement pour nous, cette op\u00e9ration est transparente pour l'utilisateur, c'est le contr\u00f4leur de m\u00e9moire qui s'en charge.

    Les caract\u00e9ristiques de la m\u00e9moire sont les suivantes\u2009:

    Caract\u00e9ristique Valeur Unit\u00e9 Capacit\u00e9 32 Gib Tension d'alimentation 1.2 V Fr\u00e9quence 1600 MHz Temps de rafra\u00eechissement 64 ms Nombre de banques 16 Technologie DDR4"}, {"location": "course-c/25-architecture-and-systems/computer/#technologies", "title": "Technologies", "text": "

    Il existe plusieurs technologies de m\u00e9moire vive. Les plus courantes sont\u2009: SDRAM, DDR, DDR2, DDR3, DDR4. Contrairement \u00e0 la SDRAM qui est une m\u00e9moire synchrone, les m\u00e9moires DDR (Double Data Rate) sont des m\u00e9moires asynchrones. Cela signifie que la m\u00e9moire peut lire et \u00e9crire des donn\u00e9es sur le flanc montant et descendant du signal d'horloge ce qui double la bande passante de la m\u00e9moire. Chaque g\u00e9n\u00e9ration am\u00e9liore les performances en augmentant la fr\u00e9quence de fonctionnement, la densit\u00e9 des puces m\u00e9moires et en r\u00e9duisant la tension d'alimentation.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#vitesse-de-la-lumiere", "title": "Vitesse de la lumi\u00e8re", "text": "

    Autoroute de l'information

    La vitesse de la lumi\u00e8re est de 299 792 458 m/s. Elle est fix\u00e9e par la convention du m\u00e8tre. C'est la vitesse maximale que peut atteindre un objet dans l'univers. Pour donner un ordre de grandeur, un signal \u00e9lectrique se propage dans un c\u00e2ble \u00e0 environ \u2154 de la vitesse de la lumi\u00e8re. Cela signifie que pour parcourir 1 m\u00e8tre, un signal \u00e9lectrique met environ 5 ns.

    Plus haut on a vu que le bus de donn\u00e9es de la m\u00e9moire est souvent de 64-bits. Cela correspond \u00e0 une autoroute de 64 voies avec quelques limitations\u2009:

    • Les voies sont unidirectionnelles, c'est-\u00e0-dire que l'on ne peut circuler que dans un sens.
    • Les voies sont s\u00e9par\u00e9es par des barri\u00e8res, c'est-\u00e0-dire que l'on ne peut pas changer de voie.
    • Les v\u00e9hicules se d\u00e9placent tous \u00e0 la vitesse d'environ 540 millions de km/h. Ils ne peuvent pas freiner, acc\u00e9l\u00e9rer ou s'arr\u00eater.

    Pour transmettre une information, par exemple un nombre entier de 64 bits (long long en C), il faut faire entrer 64 v\u00e9hicules sur chacune des voies. Chaque v\u00e9hicule repr\u00e9sente un bit. Pour que l'information soit transmise, il faut que les 64 v\u00e9hicules soient align\u00e9s et qu'ils arrivent tous au m\u00eame moment.

    Sur la figure suivante, on voit le routage d'un circuit \u00e9lectronique. En rose, ce sont les composants physiques. \u00c0 gauche un processeur et au milieu en bas deux circuits m\u00e9moire lab\u00e9lis\u00e9s DDR1 et DDR2. En bleu clair ce sont les lignes \u00e9lectriques qui relient les composants. On observe des tas de petites circonvolutions. Les lignes sont artificiellement rallong\u00e9es pour que la longueur de chaque voie de l'autoroute soit la m\u00eame, afin de garantir une vitesse de propagation identique pour chaque ligne de donn\u00e9e.

    Routage d'une m\u00e9moire

    Vous me direz, oui, mais 540 millions de km/h c'est super rapide et sur ce circuit les lignes ne font pas plus de 10 cm ce qui repr\u00e9sente 600 ps pour parcourir la distance. Oui, mais voil\u00e0, on communique sur cette autoroute \u00e0 2000 MT/s (m\u00e9gatransferts par seconde). Cela signifie que 2'000'000 de v\u00e9hicules entrent sur chaque voie de l'autoroute chaque seconde circuler sur chaque voie de l'autoroute chaque seconde. N'est-ce pas incroyable\u2009?

    Malgr\u00e9 ces performances, la m\u00e9moire reste un goulot d'\u00e9tranglement pour les processeurs. En effet, les processeurs sont de plus en plus rapides et les m\u00e9moires ne suivent pas le rythme. Un processeur qui calcule \u00e0 4 GHz peut ex\u00e9cuter 4 milliards d'instructions par seconde. Si chaque instruction n\u00e9cessite un acc\u00e8s m\u00e9moire et que cet acc\u00e8s prend 100 cycles d'horloge, alors le processeur ne pourra ex\u00e9cuter que 40 millions d'instructions par seconde. Cela signifie que le processeur ne sera utilis\u00e9 qu'\u00e0 1% de sa capacit\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#le-disque-dur", "title": "Le disque dur", "text": "

    Disque dur

    Le disque dur est un dispositif de stockage de masse. Il est compos\u00e9 de plusieurs plateaux magn\u00e9tiques qui tournent \u00e0 grande vitesse. Un bras m\u00e9canique se d\u00e9place sur les plateaux pour lire ou \u00e9crire les donn\u00e9es. Les disques durs sont lents par rapport \u00e0 la m\u00e9moire vive. Ils sont utilis\u00e9s pour stocker des donn\u00e9es de mani\u00e8re permanente.

    De nos jours ces disques sont remplac\u00e9s par des disques SSD (Solid State Drive) qui sont plus rapides et plus fiables. Les disques SSD sont compos\u00e9s de m\u00e9moire flash qui ne n\u00e9cessite pas de pi\u00e8ces mobiles. Contrairement \u00e0 la m\u00e9moire vive, les disques SSD sont des m\u00e9moires non volatiles. Cela signifie que les donn\u00e9es sont conserv\u00e9es m\u00eame lorsque l'alimentation est coup\u00e9e.

    SSD de 2 TiB

    Mais si les SSD peuvent stocker beaucoup plus de donn\u00e9es sur le m\u00eame espace, pourquoi sont-ils plus lents que la m\u00e9moire vive\u2009? La raison est simple. Les disques SSD sont organis\u00e9s en blocs de donn\u00e9es, que l'on appelle pages et clusters. Pour lire ou \u00e9crire une donn\u00e9e, il faut lire ou \u00e9crire tout le bloc. Cela signifie que si l'on veut lire un octet, il faut lire 4'096 octets. C'est ce que l'on appelle le page size.

    La communication entre le processeur et le disque SSD ou HDD utilise un protocole de communication s\u00e9rie appel\u00e9 SATA (Serial ATA). Ce protocole permet de transf\u00e9rer des donn\u00e9es \u00e0 une vitesse de 6 Gbit/s. Cela signifie que pour transf\u00e9rer un octet, il faut 8 bits, soit 8 ns. Cela semble rapide, mais si l'on veut lire un bloc de 4'096 octets, il faut 32'768 bits, soit 32'768 x 8 ns = 262'144 ns, soit 262 \u00b5s. C'est 262'144 fois plus lent que la m\u00e9moire vive.

    Pour interfacer le processeur avec le disque dur, on utilise un contr\u00f4leur de disque. Ce contr\u00f4leur est un circuit \u00e9lectronique qui g\u00e8re les acc\u00e8s disque. Il est compos\u00e9 lui-m\u00eame d'un microprocesseur, de m\u00e9moire vive et de m\u00e9moire flash.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#la-carte-mere", "title": "La carte m\u00e8re", "text": "

    Carte m\u00e8re

    La carte m\u00e8re est le composant principal de l'ordinateur. C'est elle qui relie tous les composants entre eux. Elle est compos\u00e9e d'un circuit imprim\u00e9 sur lequel sont soud\u00e9s les diff\u00e9rents composants et une grande quantit\u00e9 de connecteurs.

    Le c\u0153ur de la carte m\u00e8re est le chipset. C'est un ensemble de circuits \u00e9lectroniques qui g\u00e8re les communications entre les diff\u00e9rents composants. Il est compos\u00e9 de deux parties\u2009:

    • Le Northbridge qui g\u00e8re les communications entre le processeur, la m\u00e9moire vive et la carte graphique.
    • Le Southbridge qui g\u00e8re les communications entre les p\u00e9riph\u00e9riques de stockage, les ports USB, les ports SATA, etc.

    Le chipset est reli\u00e9 au processeur par un bus de donn\u00e9es appel\u00e9 FSB (Front Side Bus). Ce bus transporte les donn\u00e9es entre le processeur et le chipset. La configuration du chipset est stock\u00e9e dans une m\u00e9moire flash appel\u00e9e BIOS (Basic Input/Output System). Le BIOS est un logiciel qui permet de configurer les param\u00e8tres de la carte m\u00e8re.

    \u00c0 l'\u00e9poque le BIOS offrait un acc\u00e8s tr\u00e8s minimaliste \u00e0 l'utilisateur. On pouvait le configurer avec un clavier et un \u00e9cran qui n'affichait que des caract\u00e8res.

    De nos jours, le BIOS a \u00e9t\u00e9 remplac\u00e9 par l'UEFI (Unified Extensible Firmware Interface). L'UEFI est un logiciel plus \u00e9volu\u00e9 qui permet de configurer la carte m\u00e8re avec une interface graphique. Il est possible de configurer la carte m\u00e8re avec une souris et un \u00e9cran tactile.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#le-processeur", "title": "Le processeur", "text": "

    Le processeur est le cerveau de l'ordinateur. C'est lui qui ex\u00e9cute les instructions des programmes. La figure suivante montre un processeur Intel i7-12700K dans son format LGA 1700. C'est \u00e0 dire qu'il comporte 1700 broches pour se connecter \u00e0 la carte m\u00e8re.

    Processeur Intel i7

    Sur les 1700 broches on distingue plusieurs types de broches\u2009:

    • Les broches d'alimentation qui repr\u00e9sentent 40..60% des broches. Elles sont n\u00e9cessaires pour alimenter le processeur avec une tension de 1.2V.
    • Le contr\u00f4leur m\u00e9moire (DDR4/DDR5) qui permet de connecter la m\u00e9moire vive au processeur. Cela repr\u00e9sente environ 5..15% des broches.
    • Les interfaces PCIe qui permettent de connecter des cartes d'extension comme des cartes graphiques, des cartes r\u00e9seau, des cartes son, etc. Ce processeur supporte jusqu'\u00e0 20 lignes diff\u00e9rentielles soit 40 broches.
    • L'acc\u00e8s DMI, c'est l'interface entre lwe processeur et le chipset. Un DMI 4.0 x8 signifie qu'il y a 8 lignes (Rx/Tx), soit envron 16 broches.
    • L'USB, quelques dizaines de broches.
    • Le contr\u00f4leur graphique int\u00e9gr\u00e9 (iGPU) qui comporte des ports HDMI/DisplayPort pour connecter un \u00e9cran directement au processeur.
    • Les interconnexions sp\u00e9cifiques (I2C, SPI, etc.)

    Si on consulte le SDM (Software Developer Manual) d'Intel, un document de 5000 pages, on peut trouver des informations tr\u00e8s int\u00e9ressntes. Par exemple le chapitre sur les types num\u00e9riques montre les diff\u00e9rents type d'entiers ( byte, word, dword, qword), de flottants (half, single, double, extended) et de vecteurs (xmm, ymm, zmm, kmm). Il est expliqu\u00e9 que le processeur fonctionne avec le compl\u00e9ment \u00e0 2 pour les entiers et le IEEE 754 pour les flottants, qu'il est en little-endian et que les registres sont de 64 bits. Le langage C au final est tr\u00e8s proche de l'assembleur du processeur.

    ", "tags": ["xmm", "half", "zmm", "kmm", "ymm", "qword", "double", "byte", "single", "extended", "word", "dword"]}, {"location": "course-c/25-architecture-and-systems/computer/#protection-ring", "title": "Protection ring", "text": "

    Les protection rings (ou anneaux de protection) sont un m\u00e9canisme de s\u00e9curit\u00e9 utilis\u00e9 dans l'architecture des processeurs, principalement dans les syst\u00e8mes d'exploitation modernes, pour contr\u00f4ler l'acc\u00e8s aux ressources du syst\u00e8me par diff\u00e9rents types de code (comme les applications ou les composants du syst\u00e8me d'exploitation).

    L'id\u00e9e des anneaux de protection repose sur la s\u00e9paration des niveaux de privil\u00e8ge ou de contr\u00f4le en plusieurs couches. Chaque couche, ou ring (anneau), est un niveau de privil\u00e8ge qui d\u00e9termine ce qu\u2019un programme ou une instruction peut faire. Dans l'architecture x86 d'Intel, il y a g\u00e9n\u00e9ralement quatre anneaux (de 0 \u00e0 3), bien que les syst\u00e8mes d'exploitation modernes n\u2019utilisent souvent que deux de ces niveaux (Ring 0 et Ring 3).

    Anneaux de protection

    Le ring 0 correspond au noyau (kernel) du syst\u00e8me d'exploitation, qui a acc\u00e8s \u00e0 toutes les ressources mat\u00e9rielles de l'ordinateur. Il peut ex\u00e9cuter n'importe quelle instruction, acc\u00e9der \u00e0 la m\u00e9moire directement, et g\u00e9rer le mat\u00e9riel sans restriction. On parle souvent de mode superviseur ou mode noyau pour d\u00e9signer les op\u00e9rations effectu\u00e9es dans cet anneau. C\u2019est le niveau le plus privil\u00e9gi\u00e9.

    Les niveaux interm\u00e9diaires 1 et 2 peuvent \u00eatre utilis\u00e9s par certains syst\u00e8mes pour les pilotes ou des services du syst\u00e8me qui ont besoin d'un acc\u00e8s contr\u00f4l\u00e9 aux ressources, mais ne n\u00e9cessitent pas le m\u00eame niveau de privil\u00e8ge que le noyau. Toutefois, la plupart des syst\u00e8mes d'exploitation modernes ne les utilisent pas directement.

    Enfin, le niveau 3 est r\u00e9serv\u00e9 aux applications utilisateur. Il s\u2019agit du mode utilisateur (user mode), dans lequel les programmes n\u2019ont pas un acc\u00e8s direct au mat\u00e9riel ou \u00e0 la m\u00e9moire, et doivent passer par des appels syst\u00e8me pour demander des services au noyau. En cas de violation des r\u00e8gles (comme essayer d\u2019acc\u00e9der directement au mat\u00e9riel), une exception ou une erreur est g\u00e9n\u00e9r\u00e9e, et le programme est bloqu\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#bref-historique", "title": "Bref historique", "text": ""}, {"location": "course-c/25-architecture-and-systems/computer/#1978-processeurs-16-bits", "title": "1978\u2009: Processeurs 16 bits", "text": "

    L'introduction des processeurs 8086 et 8088 a marqu\u00e9 le d\u00e9but de l'architecture IA-32 avec des registres 16 bits et une adresse m\u00e9moire maximale de 1 Mo. La segmentation permettait d'adresser jusqu'\u00e0 256 Ko sans changement de segment, ouvrant la voie \u00e0 une gestion plus efficace de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1982-intel-286", "title": "1982\u2009: Intel 286", "text": "

    Le processeur Intel 286 a introduit le mode prot\u00e9g\u00e9, permettant une meilleure gestion de la m\u00e9moire avec un adressage sur 24 bits et la possibilit\u00e9 de g\u00e9rer jusqu'\u00e0 16 Mo de m\u00e9moire physique. Le mode prot\u00e9g\u00e9 apportait \u00e9galement des m\u00e9canismes de protection tels que la v\u00e9rification des limites des segments et plusieurs niveaux de privil\u00e8ges.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1985-intel-386", "title": "1985\u2009: Intel 386", "text": "

    Premi\u00e8re architecture v\u00e9ritablement 32 bits, l'Intel 386 a introduit des registres 32 bits et un bus d'adressage permettant de g\u00e9rer jusqu'\u00e0 4 Go de m\u00e9moire physique. Il proposait aussi un mode de m\u00e9moire virtuelle et un mod\u00e8le m\u00e9moire \u00e0 pages de 4 Ko, facilitant la gestion efficace de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1989-intel-486", "title": "1989\u2009: Intel 486", "text": "

    Le processeur Intel 486 a ajout\u00e9 des capacit\u00e9s de traitement parall\u00e8le avec cinq \u00e9tapes de pipeline d\u2019ex\u00e9cution, permettant l\u2019ex\u00e9cution simultan\u00e9e d'instructions. Il a \u00e9galement introduit un cache de 8 Ko sur la puce et un coprocesseur math\u00e9matique int\u00e9gr\u00e9 (FPU).

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1993-intel-pentium", "title": "1993\u2009: Intel Pentium", "text": "

    L'Intel Pentium a marqu\u00e9 une nouvelle avanc\u00e9e avec l'ajout de deux pipelines d'ex\u00e9cution, permettant l'ex\u00e9cution de deux instructions par cycle d'horloge. Il a \u00e9galement int\u00e9gr\u00e9 un syst\u00e8me de pr\u00e9diction de branchement et augment\u00e9 le bus de donn\u00e9es externe \u00e0 64 bits. Plus tard, la technologie MMX a \u00e9t\u00e9 introduite, optimisant le traitement parall\u00e8le de donn\u00e9es pour les applications multim\u00e9dia.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1995-famille-p6", "title": "1995\u2009: Famille P6", "text": "

    La famille P6 a apport\u00e9 une nouvelle microarchitecture superscalaire avec un processus de fabrication de 0,6 micron, am\u00e9liorant consid\u00e9rablement les performances tout en maintenant la compatibilit\u00e9 avec les technologies existantes.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2000-intel-pentium-4", "title": "2000\u2009: Intel Pentium 4", "text": "

    Bas\u00e9 sur l'architecture NetBurst, le Pentium 4 a introduit les extensions SIMD Streaming (SSE2), puis SSE3, pour acc\u00e9l\u00e9rer les calculs multim\u00e9dias. Le support du 64 bits avec l'Intel 64 architecture a \u00e9galement fait son apparition, ainsi que la technologie Hyper-Threading pour ex\u00e9cuter plusieurs threads simultan\u00e9ment.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2001-intel-xeon", "title": "2001\u2009: Intel Xeon", "text": "

    La gamme Xeon, bas\u00e9e \u00e9galement sur l'architecture NetBurst, a \u00e9t\u00e9 con\u00e7ue pour les serveurs multiprocesseurs et les stations de travail. Elle a introduit le multithreading (Hyper-Threading) et, plus tard, des processeurs multi-c\u0153urs pour augmenter les performances dans les environnements professionnels.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2008-intel-core-i7", "title": "2008\u2009: Intel Core i7", "text": "

    La microarchitecture Nehalem, utilis\u00e9e dans la premi\u00e8re g\u00e9n\u00e9ration d'Intel Core i7, a marqu\u00e9 l'av\u00e8nement du 45 nm, avec des fonctionnalit\u00e9s comme le Turbo Boost, l\u2019Hyper-Threading, un contr\u00f4leur m\u00e9moire int\u00e9gr\u00e9, et un cache Smart Cache de 8 Mo. Le lien QuickPath Interconnect (QPI) a remplac\u00e9 l\u2019ancien bus pour des \u00e9changes plus rapides avec le chipset.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2011-intel-core-sandy-bridge", "title": "2011\u2009: Intel Core Sandy Bridge", "text": "

    Cette g\u00e9n\u00e9ration, construite en 32 nm, a apport\u00e9 des am\u00e9liorations en termes de performance et d'efficacit\u00e9 \u00e9nerg\u00e9tique, avec des innovations comme l'int\u00e9gration des graphismes dans le processeur et l'Intel Quick Sync Video. La gamme incluait les processeurs Core i3, i5, et i7.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2012-intel-core-ivy-bridge", "title": "2012\u2009: Intel Core Ivy Bridge", "text": "

    L'Ivy Bridge a introduit une finesse de gravure de 22 nm, permettant une meilleure gestion de la consommation \u00e9nerg\u00e9tique tout en am\u00e9liorant les performances graphiques et g\u00e9n\u00e9rales du processeur. Cette g\u00e9n\u00e9ration a \u00e9galement marqu\u00e9 l'arriv\u00e9e de processeurs Xeon plus puissants pour les serveurs.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2013-intel-core-haswell", "title": "2013\u2009: Intel Core Haswell", "text": "

    La quatri\u00e8me g\u00e9n\u00e9ration, bas\u00e9e sur l\u2019architecture Haswell, a continu\u00e9 d'am\u00e9liorer les performances et l'efficacit\u00e9 \u00e9nerg\u00e9tique, tout en proposant des am\u00e9liorations comme l\u2019int\u00e9gration de la gestion de l\u2019alimentation et des performances graphiques am\u00e9lior\u00e9es pour r\u00e9pondre aux besoins des utilisateurs modernes.

    Ce r\u00e9sum\u00e9 souligne les progr\u00e8s constants en termes de puissance de traitement, de gestion m\u00e9moire, de parall\u00e9lisme, et d\u2019efficacit\u00e9 \u00e9nerg\u00e9tique des processeurs Intel au fil des g\u00e9n\u00e9rations.

    "}, {"location": "course-c/25-architecture-and-systems/files/", "title": "Fichiers", "text": ""}, {"location": "course-c/25-architecture-and-systems/files/#systeme-de-fichiers", "title": "Syst\u00e8me de fichiers", "text": "

    Dans un environnement POSIX tout est fichier. stdin est un fichier, une souris USB est un fichier, un clavier est un fichier, un terminal est un fichier, un programme est un fichier.

    Les fichiers sont organis\u00e9s dans une arborescence g\u00e9r\u00e9e par un syst\u00e8me de fichiers. Sous Windows l'arborescence classique est\u2009:

    C :\n\u251c\u2500\u2500 Program Files         Programmes install\u00e9s\n\u251c\u2500\u2500 Users                 Comptes utilisateur\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 John\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 Desktop\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 Documents\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 Music\n\u2514\u2500\u2500 Windows\n    \u251c\u2500\u2500 Fonts             Polices de caract\u00e8res\n    \u251c\u2500\u2500 System32          Syst\u00e8me d'exploitation 64-bits (oui, oui)\n    \u2514\u2500\u2500 Temp              Fichiers temporaires\n

    Il y a une arborescence par disque physique C:, D:, une arborescence par chemin r\u00e9seau \\\\eistore2, etc. Sous POSIX, la strat\u00e9gie est diff\u00e9rente, car il n'existe qu'un seul syst\u00e8me de fichier dont la racine est /.

    /\n\u251c\u2500\u2500 bin                   Programmes ex\u00e9cutables cruciaux\n\u251c\u2500\u2500 dev                   P\u00e9riph\u00e9riques (clavier, souris ...)\n\u251c\u2500\u2500 usr\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 bin               Programmes install\u00e9s\n\u251c\u2500\u2500 mnt                   Points de montage (disques r\u00e9seaux, CD, cl\u00e9 USB)\n\u2502   \u2514\u2500\u2500 eistore2\n\u251c\u2500\u2500 tmp                   Fichiers temporaires\n\u251c\u2500\u2500 home                  Comptes utilisateurs\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 john\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 documents\n\u2514\u2500\u2500 var                   Fichiers variables comme les logs ou les database\n

    Chaque \u00e9l\u00e9ment qui contient d'autres \u00e9l\u00e9ments est appel\u00e9 un r\u00e9pertoire ou dossier, en anglais directory. Chaque r\u00e9pertoire contient toujours au minimum deux fichiers sp\u00e9ciaux\u2009:

    .

    Un fichier qui symbolise le r\u00e9pertoire courant, celui dans lequel je me trouve

    ..

    Un fichier qui symbolise le r\u00e9pertoire parent, c'est \u00e0 dire home lorsque je suis dans john.

    La localisation d'un fichier au sein d'un syst\u00e8me de fichier peut \u00eatre soit absolue soit relative. Cette localisation s'appelle un chemin ou path. La convention est d'utiliser le symbole\u2009:

    • Slash / sous POSIX
    • Antislash \\ sous Windows

    Le chemin /usr/bin/.././bin/../../home/john/documents est correct, mais il n'est pas canonique, on dit qu'il n'est pas r\u00e9solu. La forme canonique est /home/john/documents.

    Un chemin peut \u00eatre relatif s'il ne commence pas par un /: ../bin. Sous Windows du m\u00eame acabit, mais la racine diff\u00e9remment selon le type de m\u00e9dia C:\\, \\\\network...

    Lorsqu'un programme s'ex\u00e9cute, son contexte d'ex\u00e9cution est toujours par rapport \u00e0 son emplacement dans le syst\u00e8me de fichier, donc le chemin peut \u00eatre soit relatif, soit absolu.

    ", "tags": ["stdin", "home", "john"]}, {"location": "course-c/25-architecture-and-systems/files/#navigation", "title": "Navigation", "text": "

    Sous Windows (PowerShell) ou un syst\u00e8me POSIX (Bash/Sh/Zsh), la navigation dans une arborescence peut \u00eatre effectu\u00e9e en ligne de commande \u00e0 l'aide des commandes (programmes) suivants\u2009:

    ls

    est un raccourci du nom list, ce programme permet d'afficher sur la sortie standard le contenu d'un r\u00e9pertoire.

    cd

    pour change directory permets de naviguer dans l'arborescence. Le programme prend en argument un chemin absolu ou relatif. En cas d'absence d'arguments, le programme redirige vers le r\u00e9pertoire de l'utilisateur courant.

    "}, {"location": "course-c/25-architecture-and-systems/files/#format-dun-fichier", "title": "Format d'un fichier", "text": "

    Un fichier peut avoir un contenu arbitraire\u2009; une suite de z\u00e9ro et d\u2019un binaire. Selon l'interpr\u00e9tation, un fichier pourrait contenir une image, un texte ou un programme. Le cas particulier ou le contenu est lisible par un \u00e9diteur de texte, on appelle ce fichier un fichier texte. C'est-\u00e0-dire que chaque caract\u00e8re est encod\u00e9 sur 8-bit et que la table ASCII est utilis\u00e9e pour traduire le contenu en un texte intelligible. Lorsque le contenu n'est pas du texte, on l'appelle un fichier binaire.

    La fronti\u00e8re est parfois assez mince, car parfois le fichier binaire peut contenir du texte intelligible, la preuve avec ce programme\u2009:

    #include <stdio.h>\n#include <string.h>\n\nint main(int* argc, char* argv[])\n{\n    static const char password[] = \"un mot de passe secret\";\n    return strcmp(argv[1], password);\n}\n

    Si nous le compilons et cherchons dans son code binaire\u2009:

    $ gcc example.c\n$ hexdump -C a.out | grep -C3 sec\n06f0  f3 c3 00 00 48 83 ec 08  48 83 c4 08 c3 00 00 00 | ....H...H....... |\n0700  01 00 02 00 00 00 00 00  00 00 00 00 00 00 00 00 | ................ |\n0710  75 6e 20 6d 6f 74 20 64  65 20 70 61 73 73 65 20 | un mot de passe  |\n0720  73 65 63 72 65 74 00 00  01 1b 03 3b 3c 00 00 00 | secret.....;<... |\n0730  06 00 00 00 e8 fd ff ff  88 00 00 00 08 fe ff ff | ................ |\n0740  b0 00 00 00 18 fe ff ff  58 00 00 00 22 ff ff ff | ........X...\"... |\n0750  c8 00 00 00 58 ff ff ff  e8 00 00 00 c8 ff ff ff | ....X........... |\n

    Sous un syst\u00e8me POSIX, il n'existe aucune distinction formelle entre un fichier binaire et un fichier texte. En revanche sous Windows, il existe une subtile diff\u00e9rence concernant surtout le caract\u00e8re de fin de ligne. La commande copy a.txt + b.txt c.txt consid\u00e8re des fichiers textes et ajoutera automatiquement une fin de ligne entre chaque partie concat\u00e9n\u00e9e, mais celle-ci copy /b a.bin + b.bin c.bin ne le fera pas.

    "}, {"location": "course-c/25-architecture-and-systems/files/#ouverture-dun-fichier", "title": "Ouverture d'un fichier", "text": "

    Sous POSIX, un programme doit demander au syst\u00e8me d'exploitation l'acc\u00e8s \u00e0 un fichier soit en lecture, soit en \u00e9criture soit les deux. Le syst\u00e8me d'exploitation retourne un descripteur de fichier qui est simplement un entier unique pour le programme.

    #include <fcntl.h>\n#include <stdio.h>\n#include <sys/stat.h>\n\nint main(void)\n{\n    int fd = open(\"toto\", O_RDONLY);\n    printf(\"%d\\n\", fd);\n    getchar();\n}\n

    Lorsque le programme ci-dessus est ex\u00e9cut\u00e9, il va demander l'ouverture du fichier toto en lecture et recevoir un descripteur de fichier fd (file descriptor) positif en cas de succ\u00e8s ou n\u00e9gatif en cas d'erreur.

    Dans l'exemple suivant, on compile, puis ex\u00e9cute en arri\u00e8re-plan le programme qui ne se terminera pas puisqu'il attend un caract\u00e8re d'entr\u00e9e. L'appel au programme ps permet de lister la liste des processus en cours et la recherche de test permet de noter le num\u00e9ro du processus, ici 6690. Dans l'arborescence de fichiers, il est possible d'aller consulter les descripteurs de fichiers ouverts pour le processus concern\u00e9.

    $ gcc test.c -o test && ./test &\n$ ps -u | grep test\nycr       6690  0.0  0.0  10540   556 pts/4    T    11:19   0:00 test\n$ ls /proc/6690/fd\n0  1  2  3\n

    On observe que trois descripteurs de fichiers sont ouverts.

    • 0 pour STDIN
    • 1 pour STDOUT
    • 2 pour STDERR
    • 3 pour le fichier toto ouvert en lecture seule

    La fonction open est en r\u00e9alit\u00e9 un appel syst\u00e8me qui n'est standardis\u00e9 que sous POSIX, c'est-\u00e0-dire que son utilisation n'est pas portable. L'exemple cit\u00e9 est principalement \u00e9voqu\u00e9 pour mieux comprendre le m\u00e9canisme de fond pour l'acc\u00e8s aux fichiers.

    En r\u00e9alit\u00e9 la biblioth\u00e8que standard, respectueuse de C99, dispose d'une fonction fopen pour file open qui offre plus de fonctionnalit\u00e9s. Ouvrir un fichier se r\u00e9sume donc \u00e0

    #include <stdio.h>\n\nint main(void)\n{\n    FILE *fp = fopen(\"toto\", \"r\");\n\n    if (fp == NULL) {\n        return -1; // Error the file cannot be accessed\n    }\n\n    // ...\n}\n

    Le mode d'ouverture du fichier peut \u00eatre\u2009:

    r

    Ouverture en lecture seule depuis le d\u00e9but du fichier.

    r+

    Ouverture pour lecture et \u00e9criture depuis le d\u00e9but du fichier.

    w

    Ouverture en \u00e9criture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0, sinon le contenu est effac\u00e9. Le pointeur de fichier est positionn\u00e9 au d\u00e9but de ce dernier.

    w+

    Ouverture en \u00e9criture et lecture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0. Le pointeur de fichier est positionn\u00e9 au d\u00e9but de ce dernier.

    a

    Ouverture du fichier pour insertion. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0. Le pointeur est positionn\u00e9 \u00e0 la fin du fichier.

    a+

    Ouverture du fichier pour lecture et \u00e9criture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0 et le pointeur du fichier est positionn\u00e9 \u00e0 la fin.

    Sous Windows et pour soucis de compatibilit\u00e9, selon la norme C99, le flag b pour binary existe. Pour ouvrir un fichier en mode binaire, on peut alors \u00e9crire rb+.

    L'ouverture d'un fichier cause, selon le mode, un acc\u00e8s exclusif au fichier. C'est-\u00e0-dire que d'autres programmes ne pourront pas acc\u00e9der \u00e0 ce fichier. Il est donc essentiel de toujours refermer l'acc\u00e8s \u00e0 un fichier d\u00e8s lors que l'op\u00e9ration de lecture ou d'\u00e9criture est termin\u00e9e\u2009:

    flose(fp);\n

    On peut noter que sous POSIX, \u00e9crire sur stdout ou stderr est exactement la m\u00eame chose qu'\u00e9crire sur un fichier, il n'y a aucune distinction.

    Exercice 1\u2009: Num\u00e9ro de ligne

    \u00c9crire un programme qui saisit le nom d'un fichier texte, ainsi qu'un texte \u00e0 rechercher. Le programme affiche ensuite le num\u00e9ro de toutes les lignes du fichier contenant le texte recherch\u00e9.

    $ ./search\nFichier: foo.txt\nRecherche: bulbe\n\n4\n5\n19\n132\n981\n

    Question subsidiaire\u2009: que fait le programme suivant\u2009:

    $ grep foo.txt bulbe\n
    ", "tags": ["open", "STDERR", "toto", "STDIN", "stderr", "test", "STDOUT", "stdout", "fopen"]}, {"location": "course-c/25-architecture-and-systems/files/#navigation-dans-un-fichier", "title": "Navigation dans un fichier", "text": "

    Lorsqu'un fichier est ouvert, un curseur virtuel est positionn\u00e9 soit au d\u00e9but soit \u00e0 la fin du fichier. Lorsque des donn\u00e9es sont lues ou \u00e9crites, c'est \u00e0 la position de ce curseur, lequel peut \u00eatre d\u00e9plac\u00e9 en utilisant plusieurs fonctions utilitaires.

    La navigation dans un fichier n'est possible que si le fichier est seekable. G\u00e9n\u00e9ralement les pointeurs de fichiers stdin, stdout et stderr ne sont pas seekable, et il n'est pas possible de se d\u00e9placer dans le fichier, mais seulement \u00e9crire dedans.

    ", "tags": ["stdin", "stdout", "stderr"]}, {"location": "course-c/25-architecture-and-systems/files/#fseek", "title": "fseek", "text": "

    La fonction fseek permet de d\u00e9placer le curseur dans un fichier ouvert. La signature de la fonction est la suivante\u2009:

    int fseek(FILE *stream, long int offset, int whence)\n

    Le manuel man fseek indique les trois constantes possibles pour whence:

    SEEK_SET

    Positionne le curseur au d\u00e9but du fichier.

    SEEK_CUR

    Position courante du curseur. Permets d'ajouter un offset relatif \u00e0 la position courante.

    SEEK_END

    Positionne le curseur \u00e0 la fin du fichier.

    Si un fichier est seekable, il est possible de se d\u00e9placer dans le fichier. Par exemple, pour lire le dernier caract\u00e8re d'un fichier\u2009:

    #include <stdio.h>\n\nint main(void)\n{\n    FILE *fp = fopen(\"toto\", \"r\");\n\n    if (fp == NULL) return -1;\n\n    fseek(fp, -1, SEEK_END);\n    char c = fgetc\n    printf(\"%c\\n\", c);\n}\n
    ", "tags": ["fseek", "SEEK_CUR", "SEEK_END", "SEEK_SET", "whence"]}, {"location": "course-c/25-architecture-and-systems/files/#ftell", "title": "ftell", "text": "

    Il est parfois utile de savoir o\u00f9 se trouve le curseur. ftell() retourne la position actuelle du curseur dans un fichier ouvert.

    char filename[] = \"foo\";\n\nFILE *fp = fopen(filename, 'r');\nfseek(fp, 0, SEEK_END);\nlong int size = ftell();\n\nprintf(\"The file %s has a size of %ld Bytes\\n\", filename, size);\n
    "}, {"location": "course-c/25-architecture-and-systems/files/#rewind", "title": "rewind", "text": "

    L'appel rewind() est \u00e9quivalent \u00e0 (void) fseek(stream, 0L, SEEK_SET) et permet de se positionner au d\u00e9but du fichier.

    "}, {"location": "course-c/25-architecture-and-systems/files/#lecture-ecriture", "title": "Lecture / \u00c9criture", "text": "

    La lecture, \u00e9criture dans un fichier s'effectue de mani\u00e8re analogue aux fonctions que nous avons d\u00e9j\u00e0 vues printf et scanf pour les flux standards (stdout, stderr), mais en utilisant les variantes pr\u00e9fix\u00e9es de f :

    Fonction Description int fscanf(FILE *stream, const char *format, ...) Lecture formatt\u00e9e int fprintf(FILE *stream, const char *format, ...) \u00c9criture formatt\u00e9e int fgetc(FILE *stream) Lecture d'un caract\u00e8re int fputc(FILE *stream, char char) \u00c9criture d'un caract\u00e8re char *fgets(char * restrict s, int n, FILE * restrict stream) Lecture d'une ligne int fputs(const char * restrict s, FILE * restrict stream) \u00c9criture d'une ligne

    L'utilisation avec stdin et stdout comme descripteur de fichier est possible, mais il est pr\u00e9f\u00e9rable dans ce cas d'utiliser les fonctions scanf et printf qui ont les m\u00eames fonctionnalit\u00e9s.

    Les nouvelles fonctions \u00e0 conna\u00eetre sont les suivantes\u2009:

    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)\n

    Elle permet une lecture arbitraire de nmemb * size bytes depuis le flux stream dans le buffer ptr:

    int32_t buffer[12] = {0};\nfread(buffer, 2, sizeof(int32_t), stdin);\nprintf(\"%x\\n%x\\n\", buffer[0], buffer[1]);\n

    Exemple d'utilisation\u2009:

    $ echo -e \"0123abcdefgh\" | ./a.out\n33323130\n64636261\n

    On notera au passage la nature little-endian du syst\u00e8me.

    La seconde fonction est\u2009:

    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)\n

    La fonction est similaire \u00e0 fread mais pour \u00e9crire sur un flux des donn\u00e9es brutes.

    ", "tags": ["scanf", "stream", "ptr", "stdin", "printf", "fread", "stdout"]}, {"location": "course-c/25-architecture-and-systems/files/#buffer-de-fichier", "title": "Buffer de fichier", "text": "

    Pour am\u00e9liorer les performances, C99 pr\u00e9voit (\u00a77.19.3-3), un espace tampon pour les descripteurs de fichiers qui peuvent \u00eatre\u2009:

    1. unbuffered (_IONBF) : Pas de buffer, les caract\u00e8res lus ou \u00e9crits sont achemin\u00e9s le plus vite possible de la source \u00e0 la destination.
    2. fully buffered (_IOFBF) : Le buffer est rempli \u00e0 chaque lecture ou \u00e9criture, puis vid\u00e9.
    3. line buffered (_IO_LBF) : Le buffer est rempli \u00e0 chaque retour \u00e0 la ligne.

    Il faut comprendre qu'\u00e0 chaque instant un programme souhaite \u00e9crire dans un fichier, il doit g\u00e9n\u00e9rer un appel syst\u00e8me et donc interrompre le noyau. Un programme qui \u00e9crirait caract\u00e8re par caract\u00e8re sur la sortie standard agirait de la m\u00eame mani\u00e8re qu'un employ\u00e9 des postes qui irait distribuer son courrier en ne prenant qu'une enveloppe \u00e0 la fois, de la centrale de distribution au destinataire.

    Par d\u00e9faut, un pointeur de fichier est fully buffered. C'est-\u00e0-dire que dans le cas du programme suivant devrait ex\u00e9cuter 10x l'appel syst\u00e8me write, une fois par caract\u00e8re.

    #include <stdio.h>\n#include <string.h>\n\nint main(int argc, char* argv[])\n{\n    if (argc > 1 && strcmp(\"--no-buffering\", argv[1]) == 0)\n        setvbuf(stdout, NULL, _IONBF, 0);\n\n    for (int i = 0; i < 10; i++)\n        putchar('c');\n}\n

    Cependant le comportement r\u00e9el est diff\u00e9rent. Seulement si le buffer est d\u00e9sactiv\u00e9, que le programme interrompt le noyau pour chaque caract\u00e8re\u2009:

    $ gcc buftest.c -o buftest\n\n$ strace ./buftest 2>&1 | grep write\nwrite(1, \"cccccccccc\", 10cccccccccc)              = 10\n\n$ strace ./buftest --no-buffering 2>&1 | grep write\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\n

    Le changement de mode peut \u00eatre effectu\u00e9 avec la fonction setbuf ou setvbuf:

    #include <stdio.h>\n\nint main(void) {\n    char buf[1024];\n\n    setbuf(stdout, buf);\n\n    fputs(\"Allo ?\");\n\n    fflush(stdout);\n}\n

    La fonction fflush force l'\u00e9criture malgr\u00e9 l'utilisation d'un buffer.

    ", "tags": ["_IO_LBF", "unbuffered", "setbuf", "_IOFBF", "fflush", "write", "setvbuf", "_IONBF"]}, {"location": "course-c/25-architecture-and-systems/files/#fichiers-et-flux", "title": "Fichiers et Flux", "text": "

    Historiquement les descripteurs de fichiers sont appel\u00e9s FILE alors qu'ils sont pr\u00e9f\u00e9rablement appel\u00e9s streams en C++. Un fichier au m\u00eame titre que stdin, stdout et stderr sont des flux de donn\u00e9es. La norme POSIX, d\u00e9crit que par d\u00e9faut les flux\u2009:

    Flux de donn\u00e9es standards Flux Num\u00e9ro Description stdin 0 Flux d'entr\u00e9e standard stdout 1 Flux de sortie standard stderr 2 Flux d'erreur standard

    Ces trois descripteurs de fichiers sont ouverts au d\u00e9but du programme. Le premier fichier ouvert par exemple avec fopen sera tr\u00e8s probablement assign\u00e9 \u00e0 l'identifiant 3, le suivant \u00e0 4, etc.

    Pour se convaincre de cela, on peut ex\u00e9cuter l'exemple suivant avec le programme strace:

    #include <stdio.h>\n\nint main(void) {\n    char c = fgetc(stdin);\n\n    FILE *fd = fopen(\"file\", \"w\");\n    fputc(c, fd);\n    fputc(c + 1, stdout);\n    fputc(c + 2, stderr);\n}\n

    Pour m\u00e9moire, strace permet de capturer les appels syst\u00e8me du programme pass\u00e9 en argument et de les afficher. Deux particularit\u00e9s de la commande ex\u00e9cut\u00e9e sont 2>&1 qui redirige stderr vers stdout afin de pouvoir rediriger le flux vers grep. Ensuite grep permet de filtrer la sortie pour n'afficher que les lignes contenant open, read, write ou close:

    $ echo k | strace ./a.out 2>&1 | grep -P 'open|read|write|close'\nread(0, \"k\\n\", 4096)                    = 2\nopenat(AT_FDCWD, \"file\", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3\nwrite(2, \"m\", 1m)                        = 1\nwrite(3, \"k\", 1)                        = 1\nwrite(1, \"l\", 1l)                        = 1\n

    On peut voir qu\u2019on lit k\\n sur le flux 0, soit stdin, puis que le fichier file est ouvert, il porte l'identifiant 3, enfin on \u00e9crit sur 1, 2 et 3.

    ", "tags": ["open", "fopen", "stdin", "read", "write", "strace", "stderr", "FILE", "grep", "file", "close", "stdout", "streams"]}, {"location": "course-c/25-architecture-and-systems/files/#formats-de-serialisation", "title": "Formats de s\u00e9rialisation", "text": "

    Souvent les fichiers sont utilis\u00e9s pour stocker de l'information organis\u00e9e en grille, par exemple, la liste des temp\u00e9ratures maximales par ville et par mois\u2009:

    Pays Ville 01 02 03 04 05 06 07 08 09 10 11 12 Suisse Z\u00fcrich 0.3 1.3 5.3 8.8 13.3 16.4 18.6 18.0 14.1 9.9 4.4 1.4 Italie Rome 7.5 8.2 10.2 12.6 17.2 21.1 24.1 24.5 20.8 16.4 11.4 8.4 Allemagne Berlin 0.6 2.3 5.1 10.2 14.8 17.9 20.3 19.7 15.3 10.5 6.0 1.33 Y\u00e9men Aden 25.7 26.0 27.2 28.9 31.0 32.7 32.7 31.5 31.6 28.9 27.1 26.01 Russie Yakutsk -38.6 -33.8 -20.1 -4.8 7.5 16.4 19.5 15.2 6.1 -7.8 -27.0 -37.6

    Il existe plusieurs mani\u00e8res d'\u00e9crire ces informations dans un fichier\u2009:

    • \u00c9criture tabul\u00e9e
    • \u00c9criture avec remplissage
    • Utiliser un langage de s\u00e9rialisation de haut niveau comme JSON, YAML ou XML
    "}, {"location": "course-c/25-architecture-and-systems/files/#format-tabule", "title": "Format tabul\u00e9", "text": "

    Un fichier dit tabul\u00e9, utilise une sentinelle, souvent le caract\u00e8re de tabulation \\t pour s\u00e9parer les donn\u00e9es. Chaque ligne du tableau est physiquement s\u00e9par\u00e9e de la suivante avec un \\n:

    Pays\\tVille\\t01\\t02\\t03\\t04\\t05\\t06\\t07\\t08\\t09\\t10\\n\nCH\\tZ\u00fcrich\\t0.3\\t1.3\\t5.3\\t8.8\\t13.3\\t16.4\\t18.6\\t18.0\\t14.1\\t9.9\\n\nIT\\tRome\\t7.5\\t8.2\\t10.2\\t12.6\\t17.2\\t21.1\\t24.1\\t24.5\\t20.8\\t16.4\\n\nDE\\tBerlin\\t0.6\\t2.3\\t5.1\\t10.2\\t14.8\\t17.9\\t20.3\\t19.7\\t15.3\\t10.5\\n\nYE\\tAden\\t25.7\\t26.0\\t27.2\\t28.9\\t31.0\\t32.7\\t32.7\\t31.5\\t31.6\\t28.9\\n\nRU\\tYakutsk\\t-38.6\\t-33.8\\t-20.1\\t-4.8\\t7.5\\t16.4\\t19.5\\t15.2\\t6.1\\t-7.8\\n\n

    Ce fichier peut \u00eatre observ\u00e9 avec un lecteur hexad\u00e9cimal\u2009:

    $ hexdump -C data.dat\n0000  50 61 79 73 09 56 69 6c  6c 65 09 30 31 09 30 32 |Pays.Ville.01.02|\n0010  09 30 33 09 30 34 09 30  35 09 30 36 09 30 37 09 |.03.04.05.06.07.|\n0020  30 38 09 30 39 09 31 30  09 31 31 09 31 32 0a 53 |08.09.10.11.12.S|\n0030  75 69 73 73 65 09 5a c3  bc 72 69 63 68 09 30 2e |uisse.Z..rich.0.|\n0040  33 09 31 2e 33 09 35 2e  33 09 38 2e 38 09 31 33 |3.1.3.5.3.8.8.13|\n0050  2e 33 09 31 36 2e 34 09  31 38 2e 36 09 31 38 2e |.3.16.4.18.6.18.|\n0060  30 09 31 34 2e 31 09 39  2e 39 09 34 2e 34 09 31 |0.14.1.9.9.4.4.1|\n0070  2e 34 0a 49 74 61 6c 69  65 09 52 6f 6d 65 09 37 |.4.Italie.Rome.7|\n0080  2e 35 09 38 2e 32 09 31  30 2e 32 09 31 32 2e 36 |.5.8.2.10.2.12.6|\n0090  09 31 37 2e 32 09 32 31  2e 31 09 32 34 2e 31 09 |.17.2.21.1.24.1.|\n00a0  32 34 2e 35 09 32 30 2e  38 09 31 36 2e 34 09 31 |24.5.20.8.16.4.1|\n00b0  31 2e 34 09 38 2e 34 0a  41 6c 6c 65 6d 61 67 6e |1.4.8.4.Allemagn|\n00c0  65 09 42 65 72 6c 69 6e  09 30 2e 36 09 32 2e 33 |e.Berlin.0.6.2.3|\n00d0  09 35 2e 31 09 31 30 2e  32 09 31 34 2e 38 09 31 |.5.1.10.2.14.8.1|\n00e0  37 2e 39 09 32 30 2e 33  09 31 39 2e 37 09 31 35 |7.9.20.3.19.7.15|\n00f0  2e 33 09 31 30 2e 35 09  36 2e 30 09 31 2e 33 33 |.3.10.5.6.0.1.33|\n0100  0a 59 c3 a9 6d 65 6e 09  41 64 65 6e 09 32 35 2e |.Y..men.Aden.25.|\n0110  37 09 32 36 2e 30 09 32  37 2e 32 09 32 38 2e 39 |7.26.0.27.2.28.9|\n0120  09 33 31 2e 30 09 33 32  2e 37 09 33 32 2e 37 09 |.31.0.32.7.32.7.|\n0130  33 31 2e 35 09 33 31 2e  36 09 32 38 2e 39 09 32 |31.5.31.6.28.9.2|\n0140  37 2e 31 09 32 36 2e 30  31 0a 52 75 73 73 69 65 |7.1.26.01.Russie|\n0150  09 59 61 6b 75 74 73 6b  09 2d 33 38 2e 36 09 2d |.Yakutsk.-38.6.-|\n0160  33 33 2e 38 09 2d 32 30  2e 31 09 2d 34 2e 38 09 |33.8.-20.1.-4.8.|\n0170  37 2e 35 09 31 36 2e 34  09 31 39 2e 35 09 31 35 |7.5.16.4.19.5.15|\n0180  2e 32 09 36 2e 31 09 2d  37 2e 38 09 2d 32 37 2e |.2.6.1.-7.8.-27.|\n0190  30 09 2d 33 37 2e 36 0a                          |0.-37.6.|\n0198\n

    L'inconv\u00e9nient de ce format est que pour obtenir directement la temp\u00e9rature du mois de mars \u00e0 Berlin, sachant que Berlin est la quatri\u00e8me ligne du fichier, il est n\u00e9cessaire de parcourir le fichier depuis le d\u00e9but, car la longueur des lignes n'est \u00e0 priori pas connue. On dit que la lecture s\u00e9quentielle est facilit\u00e9e, mais la lecture al\u00e9atoire est plus lente.

    "}, {"location": "course-c/25-architecture-and-systems/files/#format-avec-remplissage", "title": "Format avec remplissage", "text": "

    Pour pallier au d\u00e9faut du format tabul\u00e9, il est possible d'\u00e9crire le fichier en utilisant un caract\u00e8re de remplissage. Dans le fichier suivant, les mois de mai sont toujours align\u00e9s avec la 48e colonne\u2009:

     000000000011111111112222222222333333333344444444445555555555666666666\n 012345678901234567890123456789012345678901234567890123456789012345678\n+---------+-------+-----+-----+-----+----+----+----+----+----+----+--->\n\nPays      Ville   01    02    03    04   05   06   07   08   09   10\nSuisse    Z\u00fcrich  0.3   1.3   5.3   8.8  13.3 16.4 18.6 18.0 14.1 9.9\nItalie    Rome    7.5   8.2   10.2  12.6 17.2 21.1 24.1 24.5 20.8 16.4\nAllemagne Berlin  0.6   2.3   5.1   10.2 14.8 17.9 20.3 19.7 15.3 10.5\nY\u00e9men     Aden    25.7  26.0  27.2  28.9 31.0 32.7 32.7 31.5 31.6 28.9\nRussie    Yakutsk -38.6 -33.8 -20.1 -4.8 7.5  16.4 19.5 15.2 6.1  -7.8\n

    Id\u00e9alement on utilise comme caract\u00e8re de remplissage le caract\u00e8re nul \\0, mais le caract\u00e8re espace peut aussi convenir \u00e0 condition que les donn\u00e9es ne contiennent pas d'espace.

    La lecture al\u00e9atoire de ce type de fichier est facilit\u00e9e, car la position de chaque entr\u00e9e est connue \u00e0 l'avance, on sait par exemple que le pays est stock\u00e9 sur 11 caract\u00e8res, la ville sur 9 caract\u00e8res et chaque temp\u00e9rature sur 7 caract\u00e8res.

    L'utilisation de fseek est par cons\u00e9quent utile\u2009:

    int line = 2;\nint month = 3;\ndouble temperature;\n\nfseek(fd, line * (11 + 9 + 12 * 7 + 1), SEEK_SET);\nfseek(fd, 11 + 9 + month * 7 SEEK_CUR);\nfscanf(fd, \"%lf\", &temperature);\n

    L'inconv\u00e9nient de ce format de fichier est la place qu'il prend en m\u00e9moire. L'autre probl\u00e8me est que si le nom d'une ville d\u00e9passe les 9 caract\u00e8res allou\u00e9s, il faut r\u00e9\u00e9crire tout le fichier. G\u00e9n\u00e9ralement ce probl\u00e8me est contourn\u00e9 en allouant des champs d'une taille suffisante, par exemple 256 caract\u00e8res pour le nom des villes.

    ", "tags": ["fseek"]}, {"location": "course-c/25-architecture-and-systems/files/#format-serialise", "title": "Format s\u00e9rialis\u00e9", "text": "

    Des langages de s\u00e9rialisation permettent de structurer de l'information en utilisant un format sp\u00e9cifique. Ici JSON :

    [{ \"pays\": \"Suisse\",\n   \"ville\": \"Z\u00fcrich\",\n   \"mois\": {\n     \"janvier\": 0.3, \"f\u00e9vrier\": 1.3, \"mars\": 5.3, \"avril\": 8.8,\n     \"mai\": 13.3, \"juin\": 16.4, \"juillet\": 18.6, \"ao\u00fbt\": 18.0,\n     \"septembre\": 14.1, \"octobre\": 9.9, \"novembre\": 4.4, \"d\u00e9cembre\": 1.4}\n }, {\n   \"pays\": \"Italie\",\n   \"ville\": \"Rome\",\n   \"mois\": {\n     \"janvier\": 7.5, \"f\u00e9vrier\": 8.2, \"mars\": 10.2, \"avril\": 12.6,\n     \"mai\": 17.2, \"juin\": 21.1, \"juillet\": 24.1, \"ao\u00fbt\": 24.5,\n     \"septembre\": 20.8, \"octobre\": 16.4, \"novembre\": 11.4, \"d\u00e9cembre\": 8.4}\n}]\n

    L'avantage de ce type de format est qu'il est facilement modifiable avec un \u00e9diteur de texte et qu'il est tr\u00e8s interop\u00e9rable. C'est-\u00e0-dire qu'il est facilement lisible depuis diff\u00e9rents langages de programmation.

    En C, on pourra utiliser la biblioth\u00e8que logicielle json-c.

    "}, {"location": "course-c/25-architecture-and-systems/files/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Variantes

    Consid\u00e9rez les deux programmes ci-dessous tr\u00e8s similaires.

    #include <stdio.h>\n\nint main(void)\n{\n    char texte[80];\n\n    printf(\"Saisir un texte:\");\n    gets(texte);\n    printf(\"Texte: %s\\n\", texte);\n}\n
    #include <stdio.h>\n\nint main(void)\n{\n    char texte[80];\n\n    printf(\"Saisir un texte:\");\n    fgets(texte, 80, stdin);\n    printf(\"Texte: %s\\n\", texte);\n}\n
    1. Quelle est la diff\u00e9rence entre ces 2 programmes\u2009?
    2. Dans quel cas est-ce que ces programmes auront un comportement diff\u00e9rent\u2009?
    3. Quelle serait la meilleure solution\u2009?
    "}, {"location": "course-c/25-architecture-and-systems/mcu/", "title": "Syst\u00e8mes \u00e0 microcontr\u00f4leurs", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#introduction", "title": "Introduction", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#les-microcontroleurs", "title": "Les microcontr\u00f4leurs", "text": "

    Un microcontr\u00f4leur est un ordinateur sur une puce. Il est compos\u00e9 d'un processeur, de m\u00e9moire et de p\u00e9riph\u00e9riques d'entr\u00e9e/sortie. Les microcontr\u00f4leurs sont utilis\u00e9s dans de nombreux syst\u00e8mes embarqu\u00e9s, tels que les t\u00e9l\u00e9commandes, les jouets, les appareils \u00e9lectrom\u00e9nagers, les instruments de mesure, les syst\u00e8mes de contr\u00f4le de moteurs etc. Nous avons d\u00e9j\u00e0 \u00e9voqu\u00e9 la machine \u00e0 caf\u00e9 en d\u00e9but de cours qui est un bon exemple de syst\u00e8me embarqu\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#style-de-programmation", "title": "Style de programmation", "text": "

    D'ordinaire les petits microcontr\u00f4leurs sont programm\u00e9s en langage C, n\u00e9anmoins certaines possibilit\u00e9s du langage sont g\u00e9n\u00e9ralement prohib\u00e9es. Par exemple, les pointeurs de fonction sont rarement utilis\u00e9s, les fonctions r\u00e9cursives sont \u00e0 proscrire, les allocations dynamiques de m\u00e9moire sont interdites, etc. Les microcontr\u00f4leurs ont des ressources limit\u00e9es et le programmeur doit en tenir compte.

    L'allocation dynamique est interdite car elle peut entra\u00eener des fuites de m\u00e9moire ou de la fragmentation.

    L'ex\u00e9cution du programme est tr\u00e8s souvent dite bare-metal, c'est-\u00e0-dire sans syst\u00e8me d'exploitation. Le programme est ex\u00e9cut\u00e9 directement sur le microcontr\u00f4leur sans aucune couche interm\u00e9diaire. Cela permet d'avoir un contr\u00f4le total sur le mat\u00e9riel. Mais cela rend le programme non pr\u00e9emptif, c'est-\u00e0-dire qu'il n'y a pas de gestionnaire de t\u00e2ches qui peut interrompre le programme en cours d'ex\u00e9cution.

    Prenons l'exemple d'une montre \u00e0 aiguille \u00e9quip\u00e9e d'un microcontr\u00f4leur \u00e0 ultra basse consommation comme le Epson S1C17. Il ne serait pas raisonnable d'utiliser des boucles d'attentes actives (c'est-\u00e0-dire de compter un cerain nombre d'instructions correspondant \u00e0 une seconde), au lieu de cela, il est pr\u00e9f\u00e9rable d'utiliser un timer pour r\u00e9veiller le microcontr\u00f4leur toutes les secondes. Ce timer est un p\u00e9riph\u00e9rique mat\u00e9riel qui g\u00e9n\u00e8re une interruption toutes les secondes. L'interruption, via une table des vecteurs d'interruption, appelle une fonction qui met \u00e0 jour l'affichage de la montre. Aussi, c'est principalement dans cette fonction qu'aura lieu la majorit\u00e9 du programme, et il n'est pas rare d'avoir un programme de cette forme\u2009:

    void timer_isr() {\n    update_display();\n    sleep(); // Suspend l'ex\u00e9cution du programme jusqu'\u00e0 une interruption\n}\n\nint main() {\n    init_device();\n    init_timer(timer_isr);\n    enable_low_power_mode();\n    for(;;) {} // Boucle infinie\n}\n
    "}, {"location": "course-c/25-architecture-and-systems/mcu/#interruptions", "title": "Interruptions", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#ports", "title": "Ports", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#timers", "title": "Timers", "text": ""}, {"location": "course-c/25-architecture-and-systems/memory-management/", "title": "Gestion de la M\u00e9moire", "text": "

    Vous l'avez sans doute constat\u00e9 \u00e0 vos d\u00e9pens\u2009: l'erreur Segmentation fault (ou erreur de segmentation) est un fl\u00e9au fr\u00e9quent lors du d\u00e9veloppement. Ce chapitre se propose de plonger dans les arcanes de la gestion de la m\u00e9moire, en vulgarisant les concepts de segmentation et en d\u00e9taillant le m\u00e9canisme d'allocation dynamique.

    Sur un ordinateur, un programme ne peut s'arroger toute la m\u00e9moire disponible. Cette ressource pr\u00e9cieuse est partag\u00e9e entre plusieurs programmes ainsi que le syst\u00e8me d'exploitation, qui se charge de l'allouer de mani\u00e8re judicieuse \u00e0 chaque programme tout en veillant \u00e0 ce que ces derniers ne se chevauchent pas. On pourrait comparer ce processus \u00e0 un service cadastral charg\u00e9 de distribuer \u00e9quitablement des parcelles de m\u00e9moire\u2009: chaque programme dispose de son propre espace, strictement d\u00e9limit\u00e9.

    Cet espace m\u00e9moire allou\u00e9 \u00e0 un programme est structur\u00e9 en diff\u00e9rents segments, chacun d\u00e9di\u00e9 \u00e0 une fonction sp\u00e9cifique. Les principaux segments sont donn\u00e9s \u00e0 la section suivante.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#segments-de-memoire", "title": "Segments de m\u00e9moire", "text": ""}, {"location": "course-c/25-architecture-and-systems/memory-management/#text-ou-code", "title": ".text ou .code", "text": "

    Le Segment de code contient les instructions du programme ex\u00e9cutable, autrement dit le code machine. Ce segment est en lecture seule, ce qui signifie qu'un programme ne peut pas modifier son propre code durant son ex\u00e9cution, garantissant ainsi l'int\u00e9grit\u00e9 du code.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#rodata", "title": ".rodata", "text": "

    Le Segment de donn\u00e9es en lecture seule (Read-Only Data) est d\u00e9di\u00e9 aux constantes globales et aux cha\u00eenes de caract\u00e8res d\u00e9finies comme litt\u00e9rales dans le code source. Par exemple, une constante globale telle que const int = 13 ou une cha\u00eene de caract\u00e8res litt\u00e9rale comme \"abc\" sont stock\u00e9es dans ce segment. Ces donn\u00e9es, une fois d\u00e9finies, ne peuvent \u00eatre modifi\u00e9es en cours d'ex\u00e9cution.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#bss", "title": ".bss", "text": "

    Le Segment BSS (Block Started by Symbol) est une section en m\u00e9moire r\u00e9serv\u00e9e aux variables globales et statiques qui n'ont pas \u00e9t\u00e9 initialis\u00e9es explicitement. Historiquement, le terme \u00ab\u2009BSS\u2009\u00bb provient d'une directive utilis\u00e9e dans les premiers assembleurs d'IBM pour r\u00e9server un bloc de m\u00e9moire sans y affecter de valeur initiale. Par exemple, si vous d\u00e9clarez un tableau global int x[1024]; sans lui assigner de valeurs, il sera plac\u00e9 dans le segment .bss. Le syst\u00e8me d'exploitation se charge de l'initialiser \u00e0 z\u00e9ro lors du lancement du programme, assurant ainsi une base m\u00e9moire propre.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#data", "title": ".data", "text": "

    Le Segment de donn\u00e9es initialis\u00e9es stocke les variables globales et statiques qui ont \u00e9t\u00e9 initialis\u00e9es avec une valeur sp\u00e9cifique dans le code source. Contrairement au segment .bss, qui est r\u00e9serv\u00e9 aux variables non initialis\u00e9es, le segment .data contient des donn\u00e9es explicitement d\u00e9finies. Par exemple, si vous d\u00e9clarez int x = 42;, la valeur 42 sera stock\u00e9e dans ce segment, pr\u00eate \u00e0 \u00eatre utilis\u00e9e d\u00e8s le d\u00e9marrage du programme.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#heap", "title": ".heap", "text": "

    Le Tas (Heap) est une zone de m\u00e9moire \u00e0 taille dynamique. Lorsqu'un programme n\u00e9cessite de la m\u00e9moire suppl\u00e9mentaire durant son ex\u00e9cution, il peut en demander au syst\u00e8me d'exploitation via un appel syst\u00e8me. En C, cette interaction est g\u00e9n\u00e9ralement g\u00e9r\u00e9e par les fonctions malloc et free de la biblioth\u00e8que standard <stdlib.h>. Le tas est crucial pour l'allocation dynamique, permettant de r\u00e9server et de lib\u00e9rer de la m\u00e9moire selon les besoins sp\u00e9cifiques du programme.

    ", "tags": ["malloc", "free"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#stack", "title": ".stack", "text": "

    La Pile (Stack) est une zone de m\u00e9moire \u00e0 taille fixe, utilis\u00e9e principalement pour la gestion des appels de fonctions. Lorsqu'une fonction est appel\u00e9e, les variables locales, les param\u00e8tres, ainsi que les informations n\u00e9cessaires pour g\u00e9rer l'appel et le retour de la fonction sont empil\u00e9s sur la pile. La pile est \u00e9galement sollicit\u00e9e par la fonction alloca de la biblioth\u00e8que <malloc.h>, qui permet d'allouer de la m\u00e9moire de mani\u00e8re temporaire. L'ordre d'ex\u00e9cution des fonctions \u00e9tant impr\u00e9visible, la pile s'av\u00e8re indispensable pour une gestion fluide et dynamique des ressources.

    ", "tags": ["alloca"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#points-de-vigilance", "title": "Points de vigilance", "text": "

    Avant d'entrer dans le vif du sujet, il est important de souligner quelques points concernant la gestion de la m\u00e9moire en C\u2009:

    Segmentation fault

    Cette erreur survient g\u00e9n\u00e9ralement lorsque le programme tente d'acc\u00e9der \u00e0 une zone de m\u00e9moire qui ne lui est pas allou\u00e9e, violant ainsi les r\u00e8gles de segmentation impos\u00e9es par le syst\u00e8me d'exploitation.

    Finitude de la pile

    Il est important de noter que la pile a une capacit\u00e9 limit\u00e9e. Un usage excessif de la pile, par exemple via une r\u00e9cursion trop profonde, peut entra\u00eener un d\u00e9bordement de pile (stack overflow), provoquant potentiellement une interruption du programme.

    Gestion de la m\u00e9moire dynamique

    Lorsqu'une m\u00e9moire allou\u00e9e dynamiquement via malloc n'est plus n\u00e9cessaire, il est imp\u00e9ratif de la lib\u00e9rer avec free pour \u00e9viter des fuites de m\u00e9moire (memory leaks), o\u00f9 des portions de m\u00e9moire restent inutilisables pour le syst\u00e8me.

    En synth\u00e8se, la gestion de la m\u00e9moire est un art d\u00e9licat, o\u00f9 chaque segment joue un r\u00f4le pr\u00e9cis pour assurer la stabilit\u00e9 et l'efficacit\u00e9 du programme. Une bonne compr\u00e9hension de ces concepts est essentielle pour \u00e9crire du code robuste et performant.

    ", "tags": ["malloc", "free"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#memoire-de-programme", "title": "M\u00e9moire de programme", "text": "

    Nous avons vu plus haut que la m\u00e9moire d'un programme est divis\u00e9e en plusieurs segments. En r\u00e9alit\u00e9, l'organisation interne de la m\u00e9moire d'un programme est ind\u00e9pendante du syst\u00e8me d'exploitation. C'est le compilateur et surtout la biblioth\u00e8que standard qui se charge de g\u00e9rer les diff\u00e9rents segments m\u00e9moires.

    N\u00e9anmoins si l'on consid\u00e8re la biblioth\u00e8que standard libc qui est utilis\u00e9e par la plupart des programmes C, on peut observer que la m\u00e9moire d'un programme est organis\u00e9e de la mani\u00e8re suivante\u2009:

    Organisation de m\u00e9moire d'un programme

    On observe que le tas et la pile vont \u00e0 leur rencontre, et que lorsqu'ils se percutent c'est le crash avec l'erreur bien connue stack overflow.

    ", "tags": ["libc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-statique", "title": "Allocation statique", "text": "

    Jusqu'\u00e0 pr\u00e9sent, toutes les variables que nous avons d\u00e9clar\u00e9es l'ont \u00e9t\u00e9 de mani\u00e8re statique. Cela signifie que le compilateur peut, d\u00e8s la compilation, d\u00e9terminer la taille n\u00e9cessaire pour chaque variable et les organiser en m\u00e9moire dans les segments appropri\u00e9s. Cette m\u00e9thode est connue sous le nom d'allocation de m\u00e9moire statique.

    Pour les variables globales, le compilateur se charge de les initialiser \u00e0 z\u00e9ro et de les placer dans les segments .bss, .data ou .rodata selon qu'elles soient initialis\u00e9es ou non.

    Pour les variables locales (celles d\u00e9clar\u00e9es \u00e0 l'int\u00e9rieur d'une fonction), le compilateur les place sur la pile. La pile est un segment de m\u00e9moire dont la croissance est invers\u00e9e par rapport \u00e0 la m\u00e9moire g\u00e9n\u00e9rale\u2009: elle commence \u00e0 une adresse \u00e9lev\u00e9e et s'\u00e9tend vers les adresses plus basses. Lorsqu'une fonction est appel\u00e9e, ses variables locales sont empil\u00e9es sur cette pile, un peu comme on empilerait des assiettes dans une cuisine. Une fois la fonction termin\u00e9e, ces variables sont d\u00e9pil\u00e9es, lib\u00e9rant ainsi l'espace m\u00e9moire qu'elles occupaient.

    Consid\u00e9rons la d\u00e9claration suivante, qui alloue un tableau de 1024 entiers 64 bits, initialis\u00e9s \u00e0 z\u00e9ro et stock\u00e9s dans le segment .bss. Ce segment \u00e9tant d\u00e9di\u00e9 aux variables non initialis\u00e9es, le syst\u00e8me d'exploitation se chargera de les initialiser \u00e0 z\u00e9ro\u2009:

    static int64_t vector[1024];\n

    Dans cet exemple, le tableau vector occupe 64 Kio de m\u00e9moire (1024 entiers de 64 bits). Comme le tableau est d\u00e9clar\u00e9 sans valeur initiale explicite (en dehors de l'initialisation \u00e0 z\u00e9ro par d\u00e9faut), il est plac\u00e9 dans le segment .bss, ce qui signifie que le syst\u00e8me d'exploitation se chargera de le remplir de z\u00e9ros.

    En revanche, la d\u00e9claration suivante cr\u00e9e un tableau de 1024 entiers 64 bits, mais avec une initialisation partielle\u2009:

    static int64_t vector[1024] = {.[42]=1};\n

    Ici, l'\u00e9l\u00e9ment \u00e0 l'index 42 du tableau est initialis\u00e9 \u00e0 1, tandis que tous les autres \u00e9l\u00e9ments doivent \u00eatre explicitement initialis\u00e9s \u00e0 z\u00e9ro par le programme. Cette d\u00e9claration force le tableau \u00e0 \u00eatre plac\u00e9 dans le segment .data parce qu'il contient une valeur initiale sp\u00e9cifique autre que z\u00e9ro. Le programme est donc responsable de l'initialisation des autres \u00e9l\u00e9ments du tableau.

    Il faut noter que le compilateur peut optimiser l'allocation de m\u00e9moire statique en regroupant les variables globales et statiques similaires dans des zones m\u00e9moires contigu\u00ebs. Cela permet d'optimiser l'acc\u00e8s \u00e0 ces variables et de r\u00e9duire la fragmentation de la m\u00e9moire. Il n'y a donc aucune garantie que les variables globales d\u00e9clar\u00e9es les unes \u00e0 la suite des autres dans le code source seront stock\u00e9es dans des emplacements m\u00e9moires contigus.

    La pomme est plus verte chez le voisin

    Consid\u00e9rons le code suivant\u2009:

    static int32_t a = 42;\nstatic int32_t b = 23;\n\nint main() {\n  int32_t *p = &a;\n  p[1] = 666;\n\n  printf(\"%d\\n\", b);\n}\n

    Le programme compile sans erreur, et lors de l'ex\u00e9cution il est tr\u00e8s probable que vous obteniez la valeur 666 affich\u00e9e \u00e0 l'\u00e9cran. Pourquoi\u2009? Parce qu'il est tr\u00e8s probable que les variables a et b soient stock\u00e9es dans des emplacements m\u00e9moires contigus, et que l'op\u00e9ration p[1] = 666 modifie en r\u00e9alit\u00e9 la valeur de b.

    N\u00e9anmoins, le standard C ne garantit pas que les variables globales soient stock\u00e9es dans des emplacements m\u00e9moires contigus. Ce code est donc non portable et peut produire des r\u00e9sultats inattendus sur d'autres compilateurs ou architectures.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique", "title": "Allocation dynamique", "text": "

    Il est des circonstances ou un programme ne sait pas combien de m\u00e9moire il a besoin. Par exemple un programme qui compterait le nombre d'occurrences de chaque mot dans un texte devra se construire un index de tous les mots qu'il d\u00e9couvre lors de la lecture du fichier d'entr\u00e9e. A priori, ce fichier d'entr\u00e9e \u00e9tant inconnu au moment de l'ex\u00e9cution du programme, l'espace m\u00e9moire n\u00e9cessaire \u00e0 construire ce dictionnaire de mots est \u00e9galement inconnu.

    L'approche la plus na\u00efve serait d'anticiper le cas le plus d\u00e9favorable. Le dictionnaire Littr\u00e9 comporte environ 132'000 mots tandis que le Petit Larousse Illustr\u00e9 80'000 mots environ. Pour se donner une bonne marge de man\u0153uvre et anticiper les anglicismes et les noms propres. Il suffirait de r\u00e9server un tableau de 1 million de mots de 10 caract\u00e8res soit un peu plus de 100 MiB de m\u00e9moire quand bien m\u00eame le fichier qui serait lu ne comporterait que 2 mots\u2009: Hello World!.

    Vous l'avez devin\u00e9, l'approche na\u00efve est \u00e0 proscrire. D'une part parce que vous n'avez aucune garantie que le nombre de mots ne d\u00e9passera pas 1 million, d'autre part parce que vous allez probablement gaspiller de la m\u00e9moire laiss\u00e9e inutilis\u00e9e.

    L'approche correcte est d'allouer la m\u00e9moire au moment o\u00f9 on en a besoin, c'est ce que l'on appelle l'allocation dynamique de m\u00e9moire.

    Lorsqu'un programme a besoin de m\u00e9moire, il peut g\u00e9n\u00e9rer un appel syst\u00e8me pour demander au syst\u00e8me d'exploitation le besoin de disposer de plus de m\u00e9moire. En pratique on utilise trois fonctions de la biblioth\u00e8que standard <stdlib.h>:

    void *malloc(size_t size)

    Alloue dynamiquement un espace m\u00e9moire de size bytes. Le terme malloc d\u00e9coule de Memory ALLOCation.

    void *calloc(size_t nitems, size_t size)

    Fonctionne de fa\u00e7on similaire \u00e0 malloc mais initialise l'espace allou\u00e9 \u00e0 z\u00e9ro.

    void free(void *ptr)

    Lib\u00e8re un espace pr\u00e9alablement allou\u00e9 par malloc ou calloc

    Comme \u00e9voqu\u00e9 plus haut, l'allocation dynamique se fait sur le tas (segment .heap) qui est de taille variable. \u00c0 chaque fois qu'un espace m\u00e9moire est demand\u00e9, malloc recherche dans le segment un espace vide de taille suffisante, s'il ne parvient pas, il ex\u00e9cute l'appel syst\u00e8me sbrk qui permet de d\u00e9placer la fronti\u00e8re du segment m\u00e9moire et donc d'agrandir le segment.

    Consid\u00e9rons la figure suivante. En abscisse le temps et verticalement l'espace m\u00e9moire \u00e0 des instants donn\u00e9es. Le segment .heap cro\u00eet au fur et \u00e0 mesure des appels \u00e0 malloc. Au d\u00e9but on r\u00e9serve un espace de deux char dont l'adresse est r\u00e9cup\u00e9r\u00e9e dans le pointeur a. Puis, on r\u00e9serve un espace de 1 char suppl\u00e9mentaire pour le pointeur b. Lorsque free est appel\u00e9 sur a, l'espace m\u00e9moire est lib\u00e9r\u00e9 et peut \u00eatre r\u00e9utilis\u00e9 par un appel ult\u00e9rieur \u00e0 malloc. Enfin, on r\u00e9serve un espace de 1 char pour le pointeur c. \u00c0 l'issue de cette ex\u00e9cution, on peut constater que la m\u00e9moire a \u00e9t\u00e9 fragment\u00e9e. C'est-\u00e0-dire que l'espace m\u00e9moire allou\u00e9 au programme comporte des trous.

    Allocation et lib\u00e9ration m\u00e9moire

    \u00c0 la fin de l'ex\u00e9cution du programme, ce dernier consomme sur le heap trois bytes d'espace m\u00e9moire bien qu'il n'en utilise que deux. Imaginez un programme qui alloue et lib\u00e8re de la m\u00e9moire de mani\u00e8re r\u00e9p\u00e9t\u00e9e, il est probable que la fragmentation m\u00e9moire s'installe et que le programme consomme beaucoup plus de m\u00e9moire que n\u00e9cessaire. Ces probl\u00e8mes de fragmentation sont fr\u00e9quents dans de gros programmes. N'avez-vous jamais \u00e9t\u00e9 contraint de red\u00e9marrer votre navigateur web car il consommait trop de m\u00e9moire mais qu'aucun onglet n'\u00e9tait ouvert\u2009? C'est probablement d\u00fb \u00e0 un probl\u00e8me de fragmentation m\u00e9moire.

    En pratique, nous verrons que le syst\u00e8me d'exploitation est capable de g\u00e9rer la fragmentation m\u00e9moire dans une certaine mesure en d\u00e9pla\u00e7ant les blocs m\u00e9moires pour les regrouper grace \u00e0 un m\u00e9canisme appel\u00e9 MMU. Ce m\u00e9canisme n'existe cependant pas sur des petites architectures mat\u00e9rielles comme les microcontr\u00f4leurs. C'est pourquoi les d\u00e9veloppeurs \u00e9vitent autant que possible l'allocation dynamique de m\u00e9moire sur ces plateformes.

    ", "tags": ["calloc", "malloc", "size", "free", "char", "tas"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#la-pile", "title": "La pile", "text": "

    Lorsqu'un programme s'ex\u00e9cute, l'ordre dont les fonctions s'ex\u00e9cutent n'est pas connu \u00e0 priori. L'ordre d'ex\u00e9cution des fonctions dans l'exemple suivant est inconnu par le programme et donc les \u00e9ventuelles variables locales utilis\u00e9es par ces fonctions doivent dynamiquement \u00eatre allou\u00e9es. Elle fonctionne sur le principe LIFO (Last In First Out), c'est-\u00e0-dire que la derni\u00e8re variable allou\u00e9e est la premi\u00e8re \u00e0 \u00eatre lib\u00e9r\u00e9e.

    Comme \u00e9voqu\u00e9 la pile est un segment de m\u00e9moire \u00e0 taille fixe qui est utilis\u00e9e pour stocker les variables locales, les param\u00e8tres des fonctions, les informations de retour des fonctions, les donn\u00e9es retourn\u00e9es par les fonctions et les zones allou\u00e9es par la fonction alloca.

    Comme le montre la table suivante, Windows ne d\u00e9roge pas \u00e0 la r\u00e8gle en faisant bande \u00e0 part avec une taille de pile de 1 MiB. Les autres syst\u00e8mes d'exploitation ont plut\u00f4t une taille de pile de 8 MiB. Cette diff\u00e9rence est tr\u00e8s importante car elle pr\u00e9sente le risque pour un programme d\u00e9velopp\u00e9 et test\u00e9 sous Linux de ne pas fonctionner sous Windows en raison de l'utilisation de la pile.

    Taille de la pile pour quelques architectures Syst\u00e8me d'exploitation Taille de la pile Windows 1 MiB Linux 8 MiB macOS 8 MiB Solaris 8 MiB

    \u00c0 titre d'exemple, le programme suivant fonctionne tr\u00e8s bien sous Linux mais pas sous Windows ou l'erreur segmentation fault est lev\u00e9e.

    int main() {\n   char a[4 * 1024 * 1024];  // 4 MiB\n}\n

    Pour \u00e9viter ce genre de probl\u00e8me, il est recommand\u00e9 de ne pas utiliser la pile pour stocker des donn\u00e9es de grande taille, pr\u00e9f\u00e9rez l'allocation dynamique sur le tas.

    ", "tags": ["alloca"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#fonctionnement-de-la-pile", "title": "Fonctionnement de la pile", "text": "

    La pile utilise deux registres essentiels g\u00e9n\u00e9ralement stock\u00e9s directement dans le processeur. Il s'agit du Stack Pointer et du Frame Pointer.

    Le Stack Pointer (SP)

    Il pointe vers le sommet de la pile, c'est-\u00e0-dire l'adresse m\u00e9moire o\u00f9 la prochaine donn\u00e9e sera ajout\u00e9e ou retir\u00e9e. Dans une architecture x86, le Stack Pointer est g\u00e9n\u00e9ralement stock\u00e9 dans le registre esp (32 bits) ou rsp (64 bits).

    Le Frame Pointer (FP) ou Base Pointer (BP)

    Il sert de r\u00e9f\u00e9rence pour acc\u00e9der aux variables locales et aux param\u00e8tres d'une fonction. Il reste constant pendant toute la dur\u00e9e de l'ex\u00e9cution d'une fonction. Dans une architecture x86, le Frame Pointer est g\u00e9n\u00e9ralement stock\u00e9 dans le registre ebp (32 bits) ou rbp (64 bits).

    Lorsque vous appelez une fonction, les \u00e9l\u00e9ments suivants sont g\u00e9n\u00e9ralement ajout\u00e9s \u00e0 la pile (l'ordre peut l\u00e9g\u00e8rement varier selon l'architecture et le compilateur) :

    1. L'adresse de retour : L'adresse \u00e0 laquelle le programme doit revenir apr\u00e8s l'ex\u00e9cution de la fonction.
    2. Les param\u00e8tres de la fonction : Les arguments surnum\u00e9raires pass\u00e9s \u00e0 la fonction.
    3. Le Frame Pointer (ancien) : La sauvegarde de l'ancien Frame Pointer pour restaurer l'\u00e9tat de la pile lors du retour de la fonction.
    4. Les variables locales : L'espace pour les variables locales de la fonction est r\u00e9serv\u00e9 dans la pile.

    Prenons l'exemple du programme suivant qui calcule la conjecture de Syracuse (ou de Collatz) pour un nombre donn\u00e9. Ce programme r\u00e9cursif affiche les nombres de la s\u00e9quence de Collatz pour un nombre donn\u00e9 et retourne le nombre de niveaux de la s\u00e9quence. Si la profondeur de r\u00e9cursion d\u00e9passe 100, le programme renvoie -1 et si le nombre atteint 1, le programme renvoie le niveau actuel de la s\u00e9quence, c'est la condition de sortie de la r\u00e9cursion.

    #include <stdint.h>\n#include <stdio.h>\n\nint collatz(int64_t n, int64_t i) {\n   printf(\"%ld \", n);\n   int64_t level = i + 1;\n   if (n == 1) return level;\n   if (i > 100) return -1;\n   int64_t next = n % 2 == 0 ? n / 2 : 3 * n + 1;\n   return collatz(next, level);\n}\n\nint main() {\n   collatz(12, 0);\n   return 0;\n}\n

    Pour analyser comment la pile est utilis\u00e9e dans ce programme, nous allons utiliser le d\u00e9bogueur gdb. Tout d'abord il faut compiler le programme avec le flag -g pour inclure les informations de d\u00e9bogage\u2009:

    gcc -g collatz.c -o collatz\n

    Puis ex\u00e9cutez le programme avec gdb :

    gdb ./collatz\n

    Depuis GDB on va tout d'abord ajouter un point d'arr\u00eat \u00e0 la fin de l'ex\u00e9cution (ligne 15) puis lancer l'ex\u00e9cution du programme\u2009:

    (gdb) break 15\nBreakpoint 1 at 0x1179: file collatz.c, line 15.\n(gdb) run\nStarting program: /path/to/collatz\n12 6 3 10 5 16 8 4 2 1\n

    Le programme s'arr\u00eate \u00e0 la fin de l'ex\u00e9cution. Nous pouvons maintenant examiner la pile en affichant les 100 derni\u00e8res valeurs 64 bits l'adresse du Frame Pointer (FP) moins 0x300 octets. Ces valeurs sont un peu une devinette, il faut \u00ab\u2009fouiller\u2009\u00bb un peu pour identifier ce qui nous int\u00e9resse car on ne sait pas trop combien d'\u00e9l\u00e9ments ont \u00e9t\u00e9 ajout\u00e9s \u00e0 la pile. Voici comment cela peut \u00eatre fait\u2009:

    (gdb) x/100g $rbp - 0x300\n0x7fffffffda40: 0x7fffffffda70 0x555555555178\n0x7fffffffda50: 9       1\n0x7fffffffda60: 10      0                       << (3)\n0x7fffffffda70: 0x7fffffffdaa0 0x5555555551e6\n0x7fffffffda80: 8       2\n0x7fffffffda90: 9       1\n0x7fffffffdaa0: 0x7fffffffdad0 0x5555555551e6\n0x7fffffffdab0: 7       4\n0x7fffffffdac0: 8       2\n0x7fffffffdad0: 0x7fffffffdb00 0x5555555551e6\n0x7fffffffdae0: 6       8\n0x7fffffffdaf0: 7       4\n0x7fffffffdb00: 0x7fffffffdb30 0x5555555551e6\n0x7fffffffdb10: 5       16\n0x7fffffffdb20: 6       8\n0x7fffffffdb30: 0x7fffffffdb60 0x5555555551e6\n0x7fffffffdb40: 4       5\n0x7fffffffdb50: 5       16\n0x7fffffffdb60: 0x7fffffffdb90 0x5555555551e6\n0x7fffffffdb70: 3       10\n0x7fffffffdb80: 4       5\n0x7fffffffdb90: 0x7fffffffdbc0 0x5555555551e6\n0x7fffffffdba0: 2       3\n0x7fffffffdbb0: 3       10\n0x7fffffffdbc0: 0x7fffffffdbf0 0x5555555551e6\n0x7fffffffdbd0: 1       6\n0x7fffffffdbe0: 2       3                       << ...\n0x7fffffffdbf0: 0x7fffffffdc20 0x5555555551e6   << FP, adresse de retour (2)\n0x7fffffffdc00: 0       12                      << i, n (1)\n0x7fffffffdc10: 1       6                       << level, next\n0x7fffffffdc20: 0x7fffffffdc30 0x5555555551ff   << FP, adresse de retour\n0x7fffffffdc30: 0x7fffffffdcd0 0x7ffff7dc21ca\n0x7fffffffdc40: 0x7fffffffdc80 0x7fffffffdd58\n

    Avec la commande suivante, on prend note de la valeur actuelle du Frame Pointer. C'est la valeur de base au niveau du main. Les appels successifs \u00e0 collatz ont \u00e9t\u00e9 empil\u00e9s et comme la pile descend, on aura tout en bas l'adresse de retour de main :

    (gdb) info register rbp\nrbp            0x7fffffffdc30      0x7fffffffdc30\n

    \u00c0 partir de cela et en analysant la pile, on peut voir comment les valeurs sont empil\u00e9es et d\u00e9sempil\u00e9es lors des appels de fonctions. On y retrouve les variables locales level et next ainsi que les arguments sauvegard\u00e9s pour les appels r\u00e9cursifs de collatz (les valeurs de n et i). On peut facilement identifier les diff\u00e9rentes frames de pile avec la valeur sauvegard\u00e9e du Frame Pointer.

    En (1), il y a le passage initial des arguments depuis le main, collatz a \u00e9t\u00e9 appel\u00e9 avec collatz(12, 0). En (2), on voit l'adresse du pr\u00e9c\u00e9dent frame (0x7fffffffdc20). En (3) la derni\u00e8re valeur de n et i avant le retour de la fonction.

    La figure ci-dessous r\u00e9sume graphiquement le fonctionnement de la pile. On y voit deux frames li\u00e9es entre elles par le frame pointer. Les adresses de retours correspondent \u00e0 la ligne 10 et 14 du programme, soit l'emplacement des appels de fonctions.

    Pile d'ex\u00e9cution

    ", "tags": ["esp", "ebp", "gdb", "collatz", "main", "level", "rbp", "rsp", "next"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#passage-des-arguments", "title": "Passage des arguments", "text": "

    En pratique, les arguments des fonctions ne sont pas enti\u00e8rement pass\u00e9s sur la pile pour des questions de performances. Sur une architecture x86-64, les 6 premiers arguments sont pass\u00e9s dans les registres processeur rdi, rsi, rdx, rcx, r8 et r9. Les arguments suivants sont pass\u00e9s sur la pile. Dans notre exemple, nous voyons malgr\u00e9 tout que les arguments n et i sont pass\u00e9s sur la pile mais il s'agit d'une sauvegarde locale car la fonction est r\u00e9cursive et \u00e0 chaque appel, les registres seraient \u00e9cras\u00e9s.

    ", "tags": ["rdx", "rcx", "rdi", "rsi"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique-sur-le-tas", "title": "Allocation dynamique sur le tas", "text": "

    L'allocation dynamique permet de r\u00e9server - lors de l'ex\u00e9cution - une zone m\u00e9moire dont on vient de calculer la taille. La limite de m\u00e9moire est techniquement celle de la machine. Un ordinateur avec 32 Gio de RAM peut allouer 32 Gio de m\u00e9moire dynamique mais partag\u00e9e entre tous les programmes en cours d'ex\u00e9cution.

    C'est la fonction malloc (memory allocation) qui est utilis\u00e9e pour demander de la m\u00e9moire. Cette fonction est impl\u00e9ment\u00e9e dans la librairie standard du langage C. Elle peut si n\u00e9cessaire, demander au syst\u00e8me d'exploitation de lui allouer de la m\u00e9moire par l'interm\u00e9diaire d'un appel syst\u00e8me comme sbrk ou mmap. En effet, les appels syst\u00e8mes sont co\u00fbteux en temps et en ressources car le syst\u00e8me d'exploitation doit interrompre le programme changer de contexte pour ex\u00e9cuter le code du noyau puis revenir au programme. C'est pourquoi la fonction malloc ne demande pas syst\u00e9matiquement de la m\u00e9moire au syst\u00e8me d'exploitation. Elle dispose d'une m\u00e9moire tampon qu'elle alloue progressivement. Ce n'est que lorsque cette m\u00e9moire tampon est \u00e9puis\u00e9e, qu'elle demande alors de la m\u00e9moire au syst\u00e8me d'exploitation. Contrairement aux id\u00e9es re\u00e7ues, les appels \u00e0 malloc ne sont pas syst\u00e9matiquement co\u00fbteux.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation", "title": "Allocation", "text": "

    Pour utiliser l'allocation dynamique de m\u00e9moire il est n\u00e9cessaire d'inclure le fichier stdlib.h. Si l'on souhaite allouer un tableau de 1024 entiers 32 bits (soit 4 Kio), on \u00e9crira\u2009:

    int main() {\n   int *data = (int*)malloc(1024 * sizeof(int));\n   if (data == NULL) {\n      fprintf(stderr, \"Impossible d'allouer la m\u00e9moire\\n\");\n      return 1;\n   }\n   for (int i = 0; i < 1024; i++) data[i] = i;\n\n   free(data);\n}\n

    Dans cet exemple, la fonction malloc retourne un pointeur sur la zone de m\u00e9moire demand\u00e9e. Si l'allocation \u00e9choue, la fonction retourne NULL. Il est important de v\u00e9rifier que l'allocation a r\u00e9ussi avant d'utiliser le pointeur retourn\u00e9. Si l'allocation a \u00e9chou\u00e9, il est recommand\u00e9 de g\u00e9rer l'erreur et de ne pas continuer l'ex\u00e9cution du programme.

    L'allocation peut \u00e9chouer dans les cas suivants\u2009:

    • L'ordinateur n'a plus de m\u00e9moire disponible.
    • La taille demand\u00e9e est trop grande.
    • La m\u00e9moire est fragment\u00e9e et il n'y a pas de blocs contigus suffisamment grands pour satisfaire la demande.
    • Le syst\u00e8me d'exploitation a mis en place des quotas de m\u00e9moire pour les processus.

    Une fois que l'on s'est assur\u00e9 que *data contient bien une adresse m\u00e9moire valide, on peut l'utiliser comme un tableau classique. Notons au passage que la m\u00e9moire allou\u00e9e dynamiquement n'est pas initialis\u00e9e. Il est donc n\u00e9cessaire de l'initialiser avant de l'utiliser. Une mani\u00e8re simple est d'appeler la fonction memset de la librairie standard string.h :

    memset(data, 0, 1024 * sizeof(int));\n

    Cette fonction initialise la zone m\u00e9moire point\u00e9e par data avec des z\u00e9ros. Comme il est fr\u00e9quent de demander de la m\u00e9moire initialis\u00e9e, la fonction calloc est souvent utilis\u00e9e \u00e0 la place de malloc. Elle joue les deux r\u00f4les\u2009: allouer de la m\u00e9moire et l'initialiser \u00e0 z\u00e9ro.

    int *data = (int*)calloc(1024, sizeof(int));\n

    Fuite m\u00e9moire

    La difficult\u00e9 principale de ce m\u00e9canisme est que si l'on perd une r\u00e9f\u00e9rence sur la m\u00e9moire allou\u00e9e, il est impossible de la lib\u00e9rer. C'est ce que l'on appelle une fuite m\u00e9moire (memory leak). Cela peut arriver assez facilement\u2009:

    int main() {\n  int *data = (int*)malloc(1024 * sizeof(int));\n\n  data = (int*)42; // Fuite m\u00e9moire, on \u00e9crase le pointeur et on ne pourra plus lib\u00e9rer la m\u00e9moire allou\u00e9e\n}\n

    Dans cet exemple, le pointeur data est \u00e9cras\u00e9 par la valeur 42. La m\u00e9moire allou\u00e9e par malloc est perdue et ne peut plus \u00eatre lib\u00e9r\u00e9e.

    Notez que les protoypes de malloc (Memory ALLOCate) et calloc (Continuous ALLOCate) diff\u00e8rent l\u00e9g\u00e8rement\u2009:

    void *malloc(size_t size);\nvoid *calloc(size_t nitems, size_t size);\n

    L'un prend size correspondant \u00e0 la taille en octets de la m\u00e9moire \u00e0 allouer, l'autre prend nitems et size correspondant respectivement au nombre d'\u00e9l\u00e9ments et \u00e0 la taille en octets de chaque \u00e9l\u00e9ment.

    La justification principale de cette diff\u00e9rence est historique. malloc est plus ancienne et a \u00e9t\u00e9 introduite dans les premi\u00e8res versions du langage C. calloc est apparue plus tard et a \u00e9t\u00e9 con\u00e7ue pour simplifier l'allocation de tableaux. En effet, il est plus naturel de sp\u00e9cifier le nombre d'\u00e9l\u00e9ments et la taille de chaque \u00e9l\u00e9ment pour allouer un tableau. (c.f. SO.

    ", "tags": ["nitems", "calloc", "malloc", "size", "NULL", "data"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#liberation", "title": "Lib\u00e9ration", "text": "

    Une fois que le travail est termin\u00e9, il est n\u00e9cessaire de lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement. C'est la fonction free qui est utilis\u00e9e pour cela. Le prototype de la fonction est le suivant\u2009:

    void free(void *ptr);\n

    Elle prend en param\u00e8tre un pointeur vers la zone m\u00e9moire \u00e0 lib\u00e9rer.

    Dois-je appeler free \u00e0 la fin du main\u2009? Dans un programme C, appeler free pour lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement (par malloc, calloc, ou realloc) avant la fin du main est une bonne pratique, mais ce n'est pas strictement n\u00e9cessaire dans tous les cas.

    Un cas pour lequel il est n\u00e9cessaire d'appeler free avant la fin du main est lorsqu'un d\u00e9veloppeur utilise des outils d'analyse de m\u00e9moire comme Valgrind. Ces outils peuvent signaler des \u00ab\u2009fuites\u2009\u00bb de m\u00e9moire si des allocations ne sont pas lib\u00e9r\u00e9es \u00e0 la fin du programme, m\u00eame si le syst\u00e8me d'exploitation r\u00e9cup\u00e8re automatiquement toute la m\u00e9moire allou\u00e9e \u00e0 la fin du programme.

    En r\u00e9sum\u00e9, bien que ce ne soit pas strictement n\u00e9cessaire d'appeler free avant la fin du main dans tous les cas, le faire est consid\u00e9r\u00e9 comme une bonne pratique et contribue \u00e0 maintenir un code propre et sans fuites de m\u00e9moire.

    ", "tags": ["calloc", "malloc", "main", "free", "realloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#reallocation", "title": "R\u00e9allocation", "text": "

    Parfois, il est n\u00e9cessaire de modifier la taille d'une zone m\u00e9moire d\u00e9j\u00e0 allou\u00e9e. C'est la fonction realloc qui est utilis\u00e9e pour cela. Le prototype de la fonction est le suivant\u2009:

    void *realloc(void *ptr, size_t size);\n

    Elle prend en param\u00e8tre un pointeur vers la zone m\u00e9moire \u00e0 r\u00e9allouer et la nouvelle taille de la zone m\u00e9moire. La fonction retourne un pointeur vers la nouvelle zone m\u00e9moire allou\u00e9e comme pour malloc. Si la r\u00e9allocation \u00e9choue, la fonction retourne NULL et la zone m\u00e9moire initiale reste inchang\u00e9e. Aussi, pour \u00e9viter les fuites m\u00e9moire, on veillera \u00e0 ne pas perdre la r\u00e9f\u00e9rence sur la zone m\u00e9moire initiale avant d'avoir r\u00e9cup\u00e9r\u00e9 le pointeur de la zone m\u00e9moire r\u00e9allou\u00e9e.

    int main() {\n   // Allocation\n   int *data = (int*)malloc(1024 * sizeof(int));\n   if (data == NULL) return 1;\n\n   // R\u00e9allocation\n   int *new_data = (int*)realloc(data, 2048 * sizeof(int));\n   if (new_data == NULL) return 1;\n   data = new_data;\n\n   // Lib\u00e9ration\n   free(data);\n}\n

    Notons que la nouvelle zone m\u00e9moire allou\u00e9e par realloc n'est pas n\u00e9cessairement \u00e0 la m\u00eame adresse que la zone m\u00e9moire initiale. En effet, si la zone m\u00e9moire qui suit la zone m\u00e9moire initiale est libre, realloc peut simplement \u00e9tendre la zone m\u00e9moire initiale et la valeur du pointeur reste inchang\u00e9e. Dans le cas contraire, il allouera une nouvelle zone m\u00e9moire, copiera les donn\u00e9es de la zone m\u00e9moire initiale dans la nouvelle zone m\u00e9moire et lib\u00e9rera la zone m\u00e9moire initiale.

    En d'autres termes, des appels trop fr\u00e9quents \u00e0 realloc peuvent entra\u00eener des copies inutiles de donn\u00e9es et des performances m\u00e9diocres. Il est donc recommand\u00e9 de r\u00e9server la m\u00e9moire en une seule fois si possible. Prenons l'exemple suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   size_t size = 1;\n   char *data = (char *)malloc(size);\n   if (data == NULL) return 1;\n\n   while (!feof(stdin)) {\n      data[size++ - 1] = getchar();\n      {\n         char *new_data = (char *)realloc(data, size);\n         if (new_data == NULL) return 1;\n         data = new_data;\n      }\n   }\n\n   printf(\"%s\\n\", data);\n   free(data);\n}\n

    Le programme lit des caract\u00e8res depuis l'entr\u00e9e standard et les stocke dans un tableau dynamique. \u00c0 chaque it\u00e9ration, il r\u00e9alloue la m\u00e9moire pour stocker un caract\u00e8re suppl\u00e9mentaire. Cette approche est inefficace car elle entra\u00eene une copie potentielle de la m\u00e9moire \u00e0 chaque it\u00e9ration. Il est pr\u00e9f\u00e9rable de r\u00e9server la m\u00e9moire en une seule fois en utilisant une taille de m\u00e9moire suffisamment grande pour stocker tous les caract\u00e8res.

    En pratique on utilise fr\u00e9quemment un param\u00e8tre suppl\u00e9mentaire nomm\u00e9 facteur de croissance qui permet de r\u00e9allouer la m\u00e9moire en fonction de la taille actuelle. Avec un facteur de croissance de 2, lorsqu'on r\u00e9alloue la m\u00e9moire, on double la taille de la zone m\u00e9moire. Cela permet de limiter le nombre d'appels \u00e0 realloc et donc de limiter les copies inutiles de donn\u00e9es. Voici le m\u00eame programme que ci-dessus mais avec un facteur de croissance de 2\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   size_t size = 1;\n   size_t capacity = 128;\n   char *data = (char *)malloc(capacity);\n   if (data == NULL) return 1;\n\n   while (!feof(stdin)) {\n      data[size++ - 1] = getchar();\n      if (size >= capacity) {\n         capacity *= 2;\n         char *new_data = (char *)realloc(data, capacity);\n         if (new_data == NULL) return 1;\n         data = new_data;\n      }\n   }\n\n   printf(\"%s\\n\", data);\n   free(data);\n}\n

    Ici, deux variables sont utilis\u00e9es size qui correspond \u00e0 la taille actuelle du tableau et capacity qui correspond \u00e0 la capacit\u00e9 totale du tableau. Lorsque la taille du tableau atteint sa capacit\u00e9, on double la capacit\u00e9 du tableau et on r\u00e9alloue la m\u00e9moire.

    ", "tags": ["malloc", "size", "capacity", "NULL", "realloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique-sur-la-pile", "title": "Allocation dynamique sur la pile", "text": "

    L'allocation dynamique sur la pile est \u00e9quivalente \u00e0 l'allocation sur le tas sauf qu'elle est plus rapide (pas de recherche par le syst\u00e8me d'un espace suffisant et continu) et qu'elle ne n\u00e9cessite pas de lib\u00e9ration.

    On utilisera la fonction alloca (memory allocation) pour r\u00e9server de la m\u00e9moire. Cette fonction n'initialise pas la zone r\u00e9serv\u00e9e.

    void* alloca(size_t size);\n

    Il est n\u00e9cessaire d'inclure le fichier malloc.h pour utiliser cette fonction d'allocation m\u00e9moire sur la pile.

    L'avantage par rapport \u00e0 malloc est que la m\u00e9moire est lib\u00e9r\u00e9e automatiquement \u00e0 la sortie de la fonction. C'est donc une solution id\u00e9ale pour allouer de la m\u00e9moire temporaire dans une fonction. Par exemple, si vous avez besoin d'un tableau temporaire pour effectuer un calcul, vous pouvez utiliser alloca pour r\u00e9server la m\u00e9moire n\u00e9cessaire. N\u00e9anmoins vous devez vous assurer que la m\u00e9moire allou\u00e9e ne d\u00e9passe pas la taille de la pile qui est de 1 MiB sous Windows et 8 MiB sous Linux et macOS.

    Les inconv\u00e9nients sont que la fonction alloca n'est pas standardis\u00e9e et n'est pas disponible sur toutes les plateformes.

    Voici un exemple de fonctionnement\u2009:

    #include <alloca.h>\n\nvoid foo() {\n   int *data = (int*)alloca(1024 * sizeof(int));\n   for (int i = 0; i < 1024; ++i) data[i] = i;\n}\n

    Non standard

    Comme \u00e9voqu\u00e9 la fonction alloca n'est pas standardis\u00e9e et n'est pas disponible sur toutes les plateformes. Il est recommand\u00e9 de ne pas l'utiliser dans un code portable.

    ", "tags": ["malloc", "alloca"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#gestion-interne-de-la-memoire", "title": "Gestion interne de la m\u00e9moire", "text": "

    Pour mieux comprendre la tuyauterie de l'allocation dynamique de m\u00e9moire, attardons-nous un peu sur la gestion interne de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#fragmentation-memoire", "title": "Fragmentation m\u00e9moire", "text": "

    On peut observer \u00e0 la figure suivante montre qu'apr\u00e8s un appel successif de malloc et de free des espaces m\u00e9moire non utilis\u00e9s peuvent appara\u00eetre entre des r\u00e9gions utilis\u00e9es. Ces trous sont appel\u00e9s fragmentation m\u00e9moire.

    Dans la figure suivante, on suit l'\u00e9volution de l'utilisation du heap au cours de la vie d'un programme. Au d\u00e9but \u2780, la m\u00e9moire est libre. Tant que de la m\u00e9moire est allou\u00e9e sans lib\u00e9ration (free), aucun probl\u00e8me de fragmentation \u2781. N\u00e9anmoins, apr\u00e8s un certain temps la m\u00e9moire devient fragment\u00e9e \u2782\u2009; 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\u00e9server un espace de taille 9 malgr\u00e9 que l'espace cumul\u00e9 libre est suffisant.

    Fragmentation m\u00e9moire

    Dans une petite architecture, l'allocation et la lib\u00e9ration fr\u00e9quente d'espaces m\u00e9moire de taille arbitraire sont malvenues. Une fois que la fragmentation m\u00e9moire est install\u00e9e, il n'existe aucun moyen de soigner le mal si ce n'est au travers de l'ultime solution de l'informatique\u2009: \u00e9teindre puis red\u00e9marrer.

    ", "tags": ["malloc", "free"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#mmu", "title": "MMU", "text": "

    Les syst\u00e8mes d'exploitation modernes (Windows, Linux, macOS...) utilisent tous un dispositif mat\u00e9riel nomm\u00e9 MMU pour Memory Management Unit. La MMU est responsable de cr\u00e9er un espace m\u00e9moire virtuel entre l'espace physique. Cela cr\u00e9e une indirection suppl\u00e9mentaire, mais permet de r\u00e9organiser la m\u00e9moire physique sans compromettre le syst\u00e8me.

    En pratique l'espace de m\u00e9moire virtuelle est toujours beaucoup plus grand que l'espace physique. Cela permet de s'affranchir dans une large mesure de probl\u00e8mes de fragmentation, car si l'espace virtuel est suffisamment grand, il y aura statistiquement plus de chance d'y trouver un emplacement non utilis\u00e9.

    La programmation sur de petites architectures mat\u00e9rielles (microcontr\u00f4leurs, DSP) ne poss\u00e8de pas de MMU et d\u00e8s lors l'allocation dynamique est g\u00e9n\u00e9ralement \u00e0 proscrire \u00e0 moins qu'elle soit faite en connaissance de cause et en utilisant des m\u00e9canismes comme les memory pool.

    Dans la figure ci-dessous. La m\u00e9moire physique est repr\u00e9sent\u00e9e \u00e0 droite en termes de pages m\u00e9moires physiques (Physical Pages ou PP). Il s'agit de blocs m\u00e9moires contigus d'une taille fixe, par exemple 64 kb. Chaque page physique est mapp\u00e9e dans une table propre \u00e0 chaque processus (programme ex\u00e9cutable). On y retrouve quelques propri\u00e9t\u00e9s utiles \u00e0 savoir, est-ce que la page m\u00e9moire est accessible en \u00e9criture, est-ce qu'elle peut contenir du code ex\u00e9cutable\u2009? Une propri\u00e9t\u00e9 peut indiquer par exemple si la page m\u00e9moire est valide. Chacune de ces entr\u00e9es est consid\u00e9r\u00e9e comme une page m\u00e9moire virtuelle (virtual page VP).

    M\u00e9moire virtuelle

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#memoire-cache", "title": "M\u00e9moire cache", "text": "

    La m\u00e9moire cache est un dispositif mat\u00e9riel qui permet de stocker temporairement des donn\u00e9es fr\u00e9quemment utilis\u00e9es. Elle est utilis\u00e9e pour acc\u00e9l\u00e9rer l'acc\u00e8s \u00e0 la m\u00e9moire principale. La m\u00e9moire cache est g\u00e9n\u00e9ralement plus rapide que la m\u00e9moire principale, mais elle est aussi plus petite. Il existe plusieurs niveaux de cache, le cache de niveau 1 (L1) est le plus rapide mais aussi le plus petit, le cache de niveau 2 (L2) est plus lent mais plus grand, et ainsi de suite.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#erreurs-de-segmentation", "title": "Erreurs de segmentation", "text": "

    Lorsqu'un programme tente d'acc\u00e9der \u00e0 un espace m\u00e9moire qui n'est pas mapp\u00e9 dans la MMU, ou que cet espace m\u00e9moire ne permet pas le type d'acc\u00e8s souhait\u00e9\u2009: par exemple une \u00e9criture dans une page en lecture seule. Le syst\u00e8me d'exploitation tue le processus avec une erreur Segmentation Fault. C'est la raison pour laquelle, il n'est pas syst\u00e9matique d'avoir une erreur de segmentation en cas de jardinage m\u00e9moire. Tant que les valeurs modifi\u00e9es sont localis\u00e9es au sein d'un bloc m\u00e9moire autoris\u00e9, il n'y aura pas d'erreur.

    L'erreur de segmentation est donc g\u00e9n\u00e9r\u00e9e par le syst\u00e8me d'exploitation en levant le signal SIGSEGV (Violation d'acc\u00e8s \u00e0 un segment m\u00e9moire, ou erreur de segmentation).

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#memory-pool", "title": "Memory Pool", "text": "

    Un memory pool est une m\u00e9thode faisant appel \u00e0 de l'allocation dynamique de blocs de taille fixe. Lorsqu'un programme doit tr\u00e8s r\u00e9guli\u00e8rement allouer et d\u00e9sallouer de la m\u00e9moire, il est pr\u00e9f\u00e9rable que les blocs m\u00e9moires aient une taille fixe. De cette fa\u00e7on, apr\u00e8s un free, la m\u00e9moire lib\u00e9r\u00e9e est assez grande pour une allocation ult\u00e9rieure.

    Lorsqu'un programme est ex\u00e9cut\u00e9 sous Windows, macOS ou Linux, l'allocation dynamique standard malloc, calloc, realloc et free sont performant et le risque de crash d\u00fb \u00e0 une fragmentation m\u00e9moire est rare.

    En revanche, lors de l'utilisation sur de petites architectures (microcontr\u00f4leurs) qui n'ont pas de syst\u00e8me sophistiqu\u00e9 pour g\u00e9rer la m\u00e9moire, il est parfois n\u00e9cessaire d'\u00e9crire son propre syst\u00e8me de gestion de m\u00e9moire.

    ", "tags": ["calloc", "malloc", "free", "realloc"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/", "title": "Programmes et Processus", "text": "L'informatique est avant tout une science de l'abstraction. Il s'agit de cr\u00e9er le bon mod\u00e8le pour un probl\u00e8me et d'imaginer les bonnes techniques automatisables et appropri\u00e9es pour le r\u00e9soudre. Toutes les autres sciences consid\u00e8rent l'univers tel qu'il est. Par exemple, le travail d'un physicien est de comprendre le monde et non pas d'inventer un monde dans lequel les lois de la physique seraient plus simples et auxquelles il serait plus agr\u00e9able de se conformer. \u00c0 l'oppos\u00e9, les informaticiens doivent cr\u00e9er des abstractions des probl\u00e8mes du monde r\u00e9el qui pourraient \u00eatre repr\u00e9sent\u00e9es et manipul\u00e9es dans un ordinateur.Alfred Vaino Aho et Jeffrey David Ullman"}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#quest-ce-quun-programme", "title": "Qu'est-ce qu'un programme\u2009?", "text": "

    Un programme informatique est une suite d'instruction d\u00e9finissant des op\u00e9rations \u00e0 r\u00e9aliser sur des donn\u00e9es\u2009; des instructions destin\u00e9es \u00e0 \u00eatre ex\u00e9cut\u00e9es par un ordinateur. Un programme peut se d\u00e9cliner sous plusieurs formes\u2009:

    • le code source (C, C++, Python, Java, etc.) ;
    • le listing assembleur (.s, .asm) ;
    • l'ex\u00e9cutable binaire (ELF, .exe, .out, .dll, .so, etc.).

    Un processus est l'\u00e9tat d'un programme en cours d'ex\u00e9cution. Lorsqu'un programme est ex\u00e9cut\u00e9, il devient processus pendant un temps donn\u00e9. Les syst\u00e8mes d'exploitation tels que Windows sont dits multit\u00e2ches car ils peuvent faire s'ex\u00e9cuter plusieurs processus en parall\u00e8le. Le temps processeur est ainsi partag\u00e9 entre chaque processus.

    Quelque soit le langage de programmation utilis\u00e9, sur un ordinateur le processeur adopte un flot d'ex\u00e9cution s\u00e9quentiel. Les instructions sont ex\u00e9cut\u00e9es les unes apr\u00e8s les autres.

    Programmeuse en tenue d\u00e9contract\u00e9e \u00e0 c\u00f4t\u00e9 de 62'500 cartes perfor\u00e9es

    ", "tags": ["programme"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#code-source", "title": "Code source", "text": "

    Le code source est g\u00e9n\u00e9ralement \u00e9crit par un ing\u00e9nieur/d\u00e9veloppeur/informaticien. Il s'agit le plus souvent d'un fichier texte lisible par un \u00eatre humain et souvent pourvu de commentaires facilitant sa compr\u00e9hension. Selon le langage de programmation utilis\u00e9, la programmation peut \u00eatre graphique comme avec les diagrammes Ladder utilis\u00e9s dans les automates programmables et respectant la norme IEC 61131-3, ou LabView un outil de d\u00e9veloppement graphique.

    Le plus souvent le code source est organis\u00e9 en une arborescence de fichiers. Des programmes complexes comme le noyau Linux contiennent plus de 100'000 fichiers et 10 millions de lignes de code, pour la plupart \u00e9crites en C.

    "}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#executable-binaire", "title": "Ex\u00e9cutable binaire", "text": "

    Une fois compil\u00e9 en langage machine, il en r\u00e9sulte un fichier qui peut \u00eatre ex\u00e9cut\u00e9 soit par un syst\u00e8me d'exploitation, soit sur une plateforme embarqu\u00e9e \u00e0 microcontr\u00f4leur sans l'interm\u00e9diaire d'un syst\u00e8me d'exploitation. On dit que ce type de programme est bare metal, qu'il s'ex\u00e9cute \u00e0 m\u00eame le m\u00e9tal.

    Un ex\u00e9cutable binaire doit \u00eatre compil\u00e9 pour la bonne architecture mat\u00e9rielle. Un programme compil\u00e9 pour un processeur INTEL ne pourra pas s'ex\u00e9cuter sur un processeur ARM, c'est pourquoi on utilise diff\u00e9rents compilateurs en fonctions des architectures cibles. L'op\u00e9ration de compiler un programme pour une autre architecture, ou un autre syst\u00e8me d'exploitation que celui sur lequel est install\u00e9 le compilateur s'appelle la compilation crois\u00e9e (cross compilation).

    Prenons l'exemple du programme suivant qui calcule la suite des nombres de Fibonacci\u2009:

    #include <stdio.h>\n\nint main(void)\n{\n    int t1 = 0, t2 = 1;\n    int n, next_term;\n    printf(\"Enter the number of terms: \");\n    scanf(\"%d\", &n);\n    printf(\"Fibonacci Series: \");\n    for (size_t i = 1; i <= n; ++i)\n    {\n        printf(\"%d, \", t1);\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    printf(\"\\n\");\n}\n

    Une fois assembl\u00e9 le code source est converti en langage assembleur, une version interm\u00e9diaire entre le C et le langage machine. L'exemple est compil\u00e9 en utilisant gcc\u2009:

    gcc Fibonacci.c -o fibonacci.exe\nobjdump -d fibonacci.exe\n

    On obtient un fichier similaire \u00e0 ceci qui contient le code machine (48 83 ec 20), et l'\u00e9quivalent en langage assembleur (mov %fs:0x28,%rax):

    0000000000000680 <main>:\n680:   41 55                   push   %r13\n682:   41 54                   push   %r12\n684:   48 8d 35 59 02 00 00    lea    0x259(%rip),%rsi\n68b:   55                      push   %rbp\n68c:   53                      push   %rbx\n68d:   bf 01 00 00 00          mov    $0x1,%edi\n692:   48 83 ec 18             sub    $0x18,%rsp\n696:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax\n69d:   00 00\n69f:   48 89 44 24 08          mov    %rax,0x8(%rsp)\n6a4:   31 c0                   xor    %eax,%eax\n6a6:   e8 a5 ff ff ff          callq  650 <__printf_chk@plt>\n6ab:   48 8d 74 24 04          lea    0x4(%rsp),%rsi\n6b0:   48 8d 3d 49 02 00 00    lea    0x249(%rip),%rdi\n6b7:   31 c0                   xor    %eax,%eax\n6b9:   e8 a2 ff ff ff          callq  660 <__isoc99_scanf@plt>\n6be:   48 8d 35 3e 02 00 00    lea    0x23e(%rip),%rsi\n...\n72e:   00 00\n730:   75 0b                   jne    73d <main+0xbd>\n732:   48 83 c4 18             add    $0x18,%rsp\n736:   5b                      pop    %rbx\n737:   5d                      pop    %rbp\n738:   41 5c                   pop    %r12\n73a:   41 5d                   pop    %r13\n73c:   c3                      retq\n73d:   e8 fe fe ff ff          callq  640 <__stack_chk_fail@plt>\n742:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n749:   00 00 00\n74c:   0f 1f 40 00             nopl   0x0(%rax)\n

    Avec un visualiseur hexad\u00e9cimal, on peut extraire le langage machine du binaire ex\u00e9cutable. L'utilitaire hexdump est appel\u00e9 avec deux options -s pour sp\u00e9cifier l'adresse de d\u00e9but, on choisit ici celle du d\u00e9but de la fonction main 0x680, et -n pour n'extraire que les premiers 256 octets\u2009:

    $ hexdump -s0x680 -n256 a.out\n0000680 5541 5441 8d48 5935 0002 5500 bf53 0001\n0000690 0000 8348 18ec 4864 048b 2825 0000 4800\n00006a0 4489 0824 c031 a5e8 ffff 48ff 748d 0424\n00006b0 8d48 493d 0002 3100 e8c0 ffa2 ffff 8d48\n00006c0 3e35 0002 3100 bfc0 0001 0000 7fe8 ffff\n00006d0 8bff 2444 8504 74c0 4c3d 2d8d 0236 0000\n00006e0 bc41 0001 0000 01bd 0000 3100 0fdb 001f\n00006f0 da89 c031 894c bfee 0001 0000 8349 01c4\n0000700 4be8 ffff 8dff 2b04 eb89 c589 6348 2444\n0000710 4c04 e039 da73 0abf 0000 e800 ff10 ffff\n0000720 c031 8b48 244c 6408 3348 250c 0028 0000\n0000730 0b75 8348 18c4 5d5b 5c41 5d41 e8c3 fefe\n0000740 ffff 2e66 1f0f 0084 0000 0000 1f0f 0040\n0000750 ed31 8949 5ed1 8948 48e2 e483 50f0 4c54\n0000760 058d 016a 0000 8d48 f30d 0000 4800 3d8d\n0000770 ff0c ffff 15ff 0866 0020 0ff4 441f 0000\n

    Il est facile de voir la correspondance entre l'assembleur et l'ex\u00e9cutable binaire. Les valeurs 41 55 puis 41 54 puis 48 8d 35 59 se retrouvent directement dans le dump: 5541 5441 8d48. Si les valeurs sont interverties, c'est parce qu'un PC est little-endian (c.f. endianess), les octets de poids faible apparaissent par cons\u00e9quent en premier dans la m\u00e9moire.

    Sous Windows, l'extension des fichiers d\u00e9termine leur type. Un fichier avec l'extension .jpg sera un fichier image du Join Photographic Experts Group et ex\u00e9cuter ce fichier correspond \u00e0 l'ouvrir en utilisant l'application par d\u00e9faut pour visualiser les images de ce type. Un fichier avec l'extension .exe est un ex\u00e9cutable binaire, et il sera ex\u00e9cut\u00e9 en tant que programme par le syst\u00e8me d'exploitation.

    Sous POSIX (Linux, macOS, UNIX), les flags d'un fichier qualifient son type. Le programme ls permet de visualiser les flags du programme Fibonacci que nous avons compil\u00e9\u2009:

    $ ls -al a.out\n-rwxr-xr-x 1 root ftp 8.3K Jul 17 09:53 Fibonacci\n

    Les lettres r-x indiquent\u2009:

    r

    Lecture autoris\u00e9e

    w

    \u00c9criture autoris\u00e9e

    x

    Ex\u00e9cution autoris\u00e9e

    Ce programme peut-\u00eatre ex\u00e9cut\u00e9 par tout le monde, mais modifi\u00e9 que par l'utilisateur root.

    ", "tags": ["main", "Fibonacci", "root", "hexdump"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#entrees-sorties", "title": "Entr\u00e9es sorties", "text": "

    Tout programme doit pouvoir interagir avec son environnement. \u00c0 l'\u00e9poque des t\u00e9l\u00e9scripteurs, un programme interagissait avec un clavier et une imprimante matricielle. Avec l'arriv\u00e9e des syst\u00e8mes d'exploitation, le champ d'action fut r\u00e9duit \u00e0 des entr\u00e9es\u2009:

    • L'entr\u00e9e standard STDIN fournit au programme du contenu qui est g\u00e9n\u00e9ralement fourni par la sortie d'un autre programme.
    • Les arguments du programme ARGV
    • Les variables d'environnement ENVP

    Ainsi qu'\u00e0 des sorties\u2009:

    • La sortie standard STDOUT est g\u00e9n\u00e9ralement affich\u00e9e \u00e0 l'\u00e9cran
    • La sortie d'erreur standard STDERR contient des d\u00e9tails sur les \u00e9ventuelles erreurs d'ex\u00e9cution du programme.

    La figure suivante r\u00e9sume les interactions qu'un programme peut avoir sur son environnement. Les appels syst\u00e8me (syscall) sont des ordres transmis directement au syst\u00e8me d'exploitation. Ils permettent par exemple de lire des fichiers, d'\u00e9crire \u00e0 l'\u00e9cran, de mettre le programme en pause ou de terminer le programme.

    R\u00e9sum\u00e9 des interactions avec un programme

    ", "tags": ["STDERR", "STDIN", "STDOUT", "ARGV", "ENVP"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#signaux", "title": "Signaux", "text": "

    Lorsqu'un programme est en cours d'ex\u00e9cution, il peut recevoir de la part du syst\u00e8me d'exploitation des signaux. Il s'agit d'une notification asynchrone envoy\u00e9e \u00e0 un processus pour lui signaler l'apparition d'un \u00e9v\u00e8nement.

    Si, en utilisant Windows, vous vous rendez dans le gestionnaire de t\u00e2ches et que vous d\u00e9cidez de Terminer une t\u00e2che, le syst\u00e8me d'exploitation envoie un signal au programme lui demandant de se terminer.

    Sous Linux, habituellement, le shell relie certains raccourcis clavier \u00e0 des signaux particuliers\u2009:

    • Ctrl+C envoie le signal SIGINT pour interrompre l'ex\u00e9cution d'un programme
    • Ctrl+Z envoie le signal SIGTSTP pour suspendre l'ex\u00e9cution d'un programme
    • Ctrl+T envoie le signal SIGINFO permettant de visualiser certaines informations li\u00e9es \u00e0 l'ex\u00e9cution du processus.

    Si le programme suivant est ex\u00e9cut\u00e9, il sera bloquant, c'est-\u00e0-dire qu'\u00e0 moins d'envoyer un signal d'interruption, il ne sera pas possible d'interrompre le processus\u2009:

    int main(void)\n{\n    for(;;);\n}\n
    ", "tags": ["SIGINFO", "SIGTSTP", "SIGINT"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#arguments-et-options", "title": "Arguments et options", "text": "

    L'interpr\u00e9teur de commande cmd.exe sous Windows ou bash sous Linux, fonctionne de fa\u00e7on assez similaire. L'invite de commande nomm\u00e9e prompt en anglais invite l'utilisateur \u00e0 entrer une commande. Sous DOS puis sous Windows cet invite de commande ressemble \u00e0 ceci\u2009:

    C:\\>\n

    Sous Linux, le prompt est largement configurable et d\u00e9pend de la distribution install\u00e9e, mais le plus souvent il se termine par le caract\u00e8re $ ou #.

    Une commande d\u00e9bute par le nom de cette derni\u00e8re, qui peut \u00eatre le nom du programme que l'on souhaite ex\u00e9cuter puis vient les arguments et les options.

    • Une option est par convention un argument dont le pr\u00e9fixe est - sous Linux ou / sous Windows m\u00eame si le standard GNU gagne du terrain. Aussi, le consensus le plus large semble \u00eatre le suivant\u2009:
    • Une option peut \u00eatre exprim\u00e9e soit sous format court -o, -v, soit sous format long --output=, --verbose selon qu'elle commence par un ou deux tirets. Une option peut \u00eatre un bool\u00e9enne (pr\u00e9sence ou non de l'option), ou scalaire, c'est-\u00e0-dire \u00eatre associ\u00e9e \u00e0 une valeur --output=foo.o. Les options modifient le comportement interne d'un programme.

    Un argument est une cha\u00eene de caract\u00e8res utilis\u00e9e comme entr\u00e9e au programme. Un programme peut avoir plusieurs arguments.

    En C, c'est au d\u00e9veloppeur de distinguer les options des arguments, car ils sont tous pass\u00e9s par le param\u00e8tre argv:

    #include <stdio.h>\n\nint main(int argc, char *argv[]) {\n    printf(\"Liste des arguments et options pass\u00e9s au programme:\\n\");\n\n    for (size_t i = 0; i < argc; i++) {\n        printf(\"  %u. %s\\n\", i, argv[i]);\n    }\n}\n
    $ argverbose --help -h=12 3.14 'Baguette au beurre' $'\\t-Lait\\n\\t-Viande\\n\\t-Oeufs\\f'\nListe des arguments et options pass\u00e9s au programme :\n0. ./a.out\n1. --help\n2. -h=12\n3. 3.14\n4. Baguette au beurre\n5.  -Lait\n    -Viande\n    -\u0152ufs\n
    ", "tags": ["bash", "argv", "cmd.exe"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#norme-posix", "title": "Norme POSIX", "text": "

    Le standard POSIX d\u00e9crit une fa\u00e7on de distinguer des options pass\u00e9es \u00e0 un programme. Par exemple, le programme cowsay peut \u00eatre param\u00e9tr\u00e9 pour changer son comportement en utilisant des options standards comme -d. La fonction getopt disponible dans la biblioth\u00e8que <unistd.h> permet de facilement interpr\u00e9ter ces options.

    int getopt(int, char * const [], const char *);\n
    ", "tags": ["getopt", "options"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#extension-gnu", "title": "Extension GNU", "text": "

    Malheureusement, la norme POSIX ne sp\u00e9cifie que les options dites courtes (un tiret suivi d'un seul caract\u00e8re). Une extension GNU et son en-t\u00eate <getopt.h> permet l'acc\u00e8s \u00e0 la fonction getopt_long laquelle permet d'interpr\u00e9ter aussi les options longues --version qui sont devenues tr\u00e8s r\u00e9pandues.

    int getopt_long (int argc, char *const *argv, const char *shortopts,\n                 const struct option *longopts, int *longind);\n

    Ci-dessous une possible utilisation de cette fonction\u2009:

    #include <getopt.h>\n#include <stdbool.h>\n#include <stdio.h>\n#include <stdlib.h>\n\ntypedef struct Options {\n    bool is_verbose;\n\n    bool has_add;\n    bool has_append;\n    bool has_delete;\n    bool has_create;\n\n    char *create_name;\n    char *delete_name;\n    char *file_name;\n} Options;\n\nOptions parse_options(int argc, char *argv[])\n{\n    Options options = {0};\n\n    int c;\n    static int verbose_flag;  // Set by --verbose/--brief\n\n    for (;;) {\n        static struct option long_options[] = {\n            // These options set a flag.\n            {\"verbose\", no_argument, &verbose_flag, true},\n            {\"brief\", no_argument, &verbose_flag, false},\n\n            // These options don\u2019t set a flag. We distinguish them by their\n            // indices.\n            {\"add\", no_argument, 0, 'a'},\n            {\"append\", no_argument, 0, 'b'},\n            {\"delete\", required_argument, 0, 'd'},\n            {\"create\", required_argument, 0, 'c'},\n            {\"file\", required_argument, 0, 'f'},\n\n            // Sentinel marking the end of the structure.\n            {0, 0, 0, 0}};\n\n        // getopt_long stores the option index here.\n        int option_index = 0;\n\n        c = getopt_long(argc, argv, \"abc:d:f:\", long_options, &option_index);\n\n        // Detect the end of the options.\n        if (c == -1) break;\n\n        switch (c) {\n            case 'a':\n                options.has_add = true;\n                break;\n\n            case 'b':\n                options.has_append = true;\n                break;\n\n            case 'c':\n                options.create_name = optarg;\n                break;\n\n            case 'd':\n                options.delete_name = optarg;\n                break;\n\n            case 'f':\n                options.file_name = optarg;\n                break;\n\n            case '?':\n                // getopt_long already printed an error message.\n                break;\n\n            default:\n                abort();\n        }\n    }\n    options.is_verbose = verbose_flag;\n\n    // Parses the remaining command line arguments if got any\n    while (optind < argc) printf(\"%s\\n\", argv[optind++]);\n\n    return options;\n}\n\nint main(int argc, char **argv)\n{\n    Options options = parse_options(argc, argv);\n\n    // ...\n}\n
    ", "tags": ["getopt_long"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#windows", "title": "Windows", "text": "

    Windows utilise \u00e0 l'instar de RDOS ou OpenVMS, le caract\u00e8re slash pour identifier ses options. Alors que sous POSIX l'affichage de la liste des fichiers s'\u00e9crira peut-\u00eatre ls -l -s D*, sous Windows on utilisera dir /q d* /o:s.

    ", "tags": ["RDOS"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#fonction-main", "title": "Fonction main", "text": "

    Le standard d\u00e9finit une fonction nomm\u00e9e main comme \u00e9tant la fonction principale appel\u00e9e \u00e0 l'ex\u00e9cution du programme. Or, sur un syst\u00e8me d'exploitation, la fonction main a d\u00e9j\u00e0 \u00e9t\u00e9 appel\u00e9e il y a belle lurette lorsque l'ordinateur a \u00e9t\u00e9 allum\u00e9 et que le BIOS a charg\u00e9 le syst\u00e8me d'exploitation en m\u00e9moire. D\u00e8s lors la fonction main de notre programme Hello World n'est pas la premi\u00e8re, mais est appel\u00e9.

    ", "tags": ["main"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#qui-appelle-main", "title": "Qui appelle main\u2009?", "text": "

    Un ex\u00e9cutable binaire \u00e0 un format particulier appel\u00e9 ELF (Executable and Linkable Format) qui contient un point d'entr\u00e9e qui sera l'adresse m\u00e9moire de d\u00e9but du programme. Sous un syst\u00e8me POSIX ce point d'entr\u00e9e est nomm\u00e9 _init. C'est lui qui est responsable de r\u00e9colter les informations transmises par le syst\u00e8me d'exploitation. Ce dernier transmet sur la pile du programme\u2009:

    • Le nombre d'arguments argc
    • La liste des arguments argv
    • Les variables d'environnements envp
    • Les pointeurs de fichiers sur stdout, stdin, stderr

    C'est la fonction __libc_start_main de la biblioth\u00e8que standard qui a la responsabilit\u00e9 d'appeler la fonction main. Voici son prototype\u2009:

    int __libc_start_main(int (*main) (int, char**, char**),\n    int argc, char** ubp_av,\n    void (*init)(void),\n    void (*fini)(void),\n    void (*rtld_fini)(void),\n    void (*stack_end)\n);\n
    ", "tags": ["stdin", "stderr", "main", "argv", "envp", "stdout", "argc", "_init"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#valeur-de-retour", "title": "Valeur de retour", "text": "

    La fonction main renvoie toujours une valeur de retour qui agit comme le statut de sortie d'un programme (exit status). Sous POSIX et sous Windows, le programme parent s'attend \u00e0 recevoir une valeur 32-bits \u00e0 la fin de l'ex\u00e9cution d'un programme. L'interpr\u00e9tation est la suivante\u2009:

    0

    Succ\u00e8s, le programme s'est termin\u00e9 correctement.

    !0

    Erreur, le programme ne s'est pas termin\u00e9 correctement.

    Par exemple le programme printf retourne dans le cas pr\u00e9cis l'erreur 130\u2009:

    $ printf '%d' 42\n42\n$ echo $?\n0\n\n$ printf '%d' 'I am not a number'\nprintf: I am not a number: invalid number\n$ echo $?\n130\n
    ", "tags": ["main", "printf"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#entrees-sorties-standards", "title": "Entr\u00e9es sorties standards", "text": "

    Le fichier d'en-t\u00eate stdio.h (man stdio) permet de simplifier l'interaction avec les fichiers. Sous Linux et macOS principalement, mais d'une certaine mani\u00e8re \u00e9galement sous Windows, les canaux d'\u00e9changes entre un programme et son h\u00f4te (shell, gestionnaire de fen\u00eatre, autre programme), se font par l'interm\u00e9diaire de fichiers particuliers nomm\u00e9s stdin, stdout et stderr.

    La fonction de base est putchar qui \u00e9crit un caract\u00e8re sur stdout:

    #include <stdio.h>\n\nint main(void) {\n    putchar('H');\n    putchar('e');\n    putchar('l');\n    putchar('l');\n    putchar('o');\n    putchar('\\n');\n}\n

    Bien vite, on pr\u00e9f\u00e8rera utiliser printf qui simplifie le formatage de cha\u00eenes de caract\u00e8res et qui permet \u00e0 l'aide de marqueurs (tokens) de formater des variables\u2009:

    #include <stdio.h>\n\nint main(void) {\n    printf(\"Hello\\v\");\n    printf(\"%d, %s, %f\", 0x12, \"World!\", 3.1415);\n}\n

    Il peut \u00eatre n\u00e9cessaire, surtout lorsqu'il s'agit d'erreurs qui ne concernent pas la sortie standard du programme, d'utiliser le bon canal de communication, c'est-\u00e0-dire stderr au lieu de stdout. La fonction fprintf permet de sp\u00e9cifier le flux standard de sortie\u2009:

    #include <stdio.h>\n\nint main(void) {\n    fprintf(stdout, \"Sortie standard\\n\");\n    fprintf(stderr, \"Sortie d'erreur standard\\n\");\n}\n

    Pourquoi, me direz-vous, faut-il s\u00e9parer la sortie standard du canal d'erreur\u2009? Le plus souvent un programme n'est pas utilis\u00e9 seul, mais en conjonction avec d'autres programmes\u2009:

    $ echo \"Bonjour\" | tr 'A-Za-z' 'N-ZA-Mn-za-m' > data.txt\n$ cat data.txt\nObawbhe\n

    Dans cet exemple ci-dessus, le programme echo prend en argument la cha\u00eene de caract\u00e8re Bonjour qu'il envoie sur la sortie standard. Ce flux de sortie est reli\u00e9 au flux d'entr\u00e9e du programme tr qui effectue une op\u00e9ration de ROT13 et envoie le r\u00e9sultat sur la sortie standard. Ce flux est ensuite redirig\u00e9 sur le fichier data.txt. La commande suivante cat lis le contenu du fichier dont le nom est pass\u00e9 en argument et \u00e9crit le contenu sur la sortie standard.

    Rot13

    Dans le cas o\u00f9 un de ces programmes g\u00e9n\u00e8re une alerte (warning), le texte ne sera pas transmis le long de la cha\u00eene, mais simplement affich\u00e9 sur la console. Il est donc une bonne pratique que d'utiliser le bon flux de sortie\u2009: stdout pour la sortie standard et stderr pour les messages de diagnostic et les erreurs.

    ", "tags": ["stdin", "Bonjour", "data.txt", "echo", "putchar", "stderr", "stdio.h", "cat", "printf", "fprintf", "stdout"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#boucle-dattente", "title": "Boucle d'attente", "text": "

    Comme \u00e9voqu\u00e9, un programme est souvent destin\u00e9 \u00e0 tourner sur un syst\u00e8me d'exploitation. Un programme simple comme celui-ci\u2009:

    int main(void) {\n    for(;;) {}\n}\n

    consommera 100% des ressources du processeur. En d'autres termes, le processeur d\u00e9pensera toute son \u00e9nergie \u00e0 faire 150 millions de calculs par seconde, pour rien. Et les autres processus n'auront que tr\u00e8s peu de ressources disponibles pour tourner.

    Il est grandement pr\u00e9f\u00e9rable d'utiliser des appels syst\u00e8me pour indiquer au noyau du syst\u00e8me d'exploitation que le processus souhaite \u00eatre mis en pause pour un temps donn\u00e9. Le programme suivant utilise la fonction standard sleep pour demander au noyau d'\u00eatre mis en attente pour une p\u00e9riode de temps sp\u00e9cifi\u00e9e en param\u00e8tre.

    #include <unistd.h>\n\nint main(void) {\n    for(;;) {\n        sleep(1 /* seconds */);\n\n        ...\n    }\n}\n

    Alternativement, lorsqu'un programme attend un retour de l'utilisateur par exemple en demandant la saisie au clavier d'informations, le syst\u00e8me d'exploitation est \u00e9galement mis en attente et le processus ne consomme pas de ressources CPU. Le programme ci-dessous attend que l'utilisateur presse la touche entr\u00e9e.

    #include <stdio.h>\n\nint main(void) {\n    for(;;) {\n        getchar();\n        printf(\"Vous avez press\u00e9 la touche enter (\\\\n)\\n\");\n    }\n}\n

    Exercice 1\u2009: La fortune, la vache qui dit et le chat dr\u00f4le

    En rappelant l'historique des derni\u00e8res commandes ex\u00e9cut\u00e9es sur l'ordinateur du professeur pendant qu'il avait le dos tourn\u00e9, vous tombez sur cette commande\u2009:

    $ fortune | cowsay | lolcat\n

    Quelle est sa structure et que fait-elle\u2009?

    ", "tags": ["sleep"]}, {"location": "course-c/27-data-structures/", "title": "Conteneurs de donn\u00e9es", "text": "

    Un conteneur de donn\u00e9es, en programmation informatique, se d\u00e9finit comme une structure sophistiqu\u00e9e con\u00e7ue pour organiser et stocker des \u00e9l\u00e9ments de mani\u00e8re efficiente, facilitant ainsi leur acc\u00e8s, leur modification et leur gestion. \u00c0 travers cette structure, le d\u00e9veloppeur peut manipuler des ensembles de donn\u00e9es sans se soucier des d\u00e9tails basiques de stockage ou de gestion m\u00e9moire, ce qui lui permet de se concentrer sur la logique du programme.

    Il existe une vari\u00e9t\u00e9 de conteneurs, chacun avec des sp\u00e9cificit\u00e9s qui les rendent adapt\u00e9s \u00e0 des situations distinctes. Parmi les plus courants, nous retrouvons les tableaux, o\u00f9 les \u00e9l\u00e9ments sont stock\u00e9s en m\u00e9moire de fa\u00e7on contigu\u00eb, permettant un acc\u00e8s rapide\u2009; les listes cha\u00een\u00e9es, o\u00f9 chaque \u00e9l\u00e9ment pointe vers le suivant, offrant une flexibilit\u00e9 accrue au prix d'une l\u00e9g\u00e8re perte d'efficacit\u00e9\u2009; les piles (stack) et les files (queue), qui r\u00e9gissent l'ordre d'entr\u00e9e et de sortie des \u00e9l\u00e9ments selon des principes bien d\u00e9finis\u2009; et enfin, des structures plus complexes comme les arbres et les graphes, qui permettent de mod\u00e9liser des relations hi\u00e9rarchiques ou des r\u00e9seaux de d\u00e9pendances entre les donn\u00e9es.

    Dans la plupart des langages modernes, le programmeur b\u00e9n\u00e9ficie de biblioth\u00e8ques standard fournissant ces conteneurs, impl\u00e9ment\u00e9s de mani\u00e8re optimale pour r\u00e9pondre aux besoins courants. Ces biblioth\u00e8ques offrent une panoplie d'outils pr\u00eats \u00e0 l'emploi, \u00e9vitant ainsi au d\u00e9veloppeur de r\u00e9inventer la roue. Toutefois, cette facilit\u00e9 d'acc\u00e8s n'est pas universelle. En C, par exemple, bien que reconnu pour sa puissance et sa proximit\u00e9 avec la machine, le langage ne dispose pas, dans sa biblioth\u00e8que standard, d'impl\u00e9mentations de conteneurs de donn\u00e9es de haut niveau. Le d\u00e9veloppeur en C se trouve donc souvent face au choix d\u00e9licat de cr\u00e9er ses propres structures ou de se tourner vers des biblioth\u00e8ques tierces pour b\u00e9n\u00e9ficier de telles fonctionnalit\u00e9s.

    Ainsi, bien que le langage C exige du d\u00e9veloppeur une compr\u00e9hension profonde des m\u00e9canismes sous-jacents, cette connaissance se traduit par une ma\u00eetrise accrue des ressources mat\u00e9rielles, un atout inestimable dans des domaines o\u00f9 l'efficacit\u00e9 est primordiale. Mais il est ind\u00e9niable que l'absence de conteneurs de haut niveau dans la biblioth\u00e8que standard du C repr\u00e9sente un d\u00e9fi suppl\u00e9mentaire, que chaque programmeur doit relever avec rigueur et discernement.

    "}, {"location": "course-c/27-data-structures/dynamic-array/", "title": "Tableau dynamique", "text": ""}, {"location": "course-c/27-data-structures/dynamic-array/#definition", "title": "D\u00e9finition", "text": "

    Un tableau dynamique (aussi nomm\u00e9 vecteur en C++ ou liste en Python) est une structure de donn\u00e9es qui transcende les limitations rigides du tableau classique en offrant une flexibilit\u00e9 d\u2019utilisation accrue. Dans un tableau statique, la taille est d\u00e9termin\u00e9e d\u00e8s l'initialisation et ne peut plus \u00eatre modifi\u00e9e, ce qui impose au d\u00e9veloppeur de pr\u00e9dire, parfois avec difficult\u00e9, la quantit\u00e9 exacte d'espace n\u00e9cessaire. Un tableau dynamique, en revanche, s\u2019ajuste \u00e0 la croissance ou \u00e0 la diminution des donn\u00e9es qu\u2019il contient, r\u00e9pondant ainsi aux besoins \u00e9volutifs du programme.

    Le fonctionnement d\u2019un tableau dynamique repose sur un m\u00e9canisme fondamental\u2009: la gestion dynamique de la m\u00e9moire. Initialement, un tableau dynamique est allou\u00e9 avec une taille d\u00e9termin\u00e9e, souvent modeste, pour accueillir les premiers \u00e9l\u00e9ments. Cependant, lorsque le tableau atteint sa capacit\u00e9 maximale, une nouvelle allocation de m\u00e9moire plus vaste est effectu\u00e9e. Ce processus implique la cr\u00e9ation d'un nouveau tableau plus grand, la copie des \u00e9l\u00e9ments du tableau original vers ce nouveau tableau, et enfin la lib\u00e9ration de la m\u00e9moire allou\u00e9e au tableau initial.

    Cette flexibilit\u00e9 a un co\u00fbt, celui de la performance, car la r\u00e9allocation et la copie des \u00e9l\u00e9ments lors de l'agrandissement du tableau sont des op\u00e9rations co\u00fbteuses en temps, surtout si elles sont fr\u00e9quentes. Pour att\u00e9nuer cet inconv\u00e9nient, les tableaux dynamiques sont souvent con\u00e7us pour doubler leur capacit\u00e9 \u00e0 chaque r\u00e9allocation, r\u00e9duisant ainsi la fr\u00e9quence de ces op\u00e9rations.

    Outre la gestion de la capacit\u00e9, un tableau dynamique offre \u00e9galement des op\u00e9rations d'insertion et de suppression d'\u00e9l\u00e9ments plus souples que celles d'un tableau statique. L\u00e0 o\u00f9 un tableau statique n\u00e9cessiterait de d\u00e9placer manuellement les \u00e9l\u00e9ments pour ins\u00e9rer ou supprimer une valeur, le tableau dynamique g\u00e8re ces op\u00e9rations en interne, rendant son utilisation plus intuitive.

    N\u00e9anmoins, cette puissance de flexibilit\u00e9 s'accompagne d'une exigence de gestion rigoureuse. Le d\u00e9veloppeur doit rester conscient de la mani\u00e8re dont la m\u00e9moire est utilis\u00e9e, surtout dans des langages comme le C o\u00f9 le contr\u00f4le manuel de l'allocation et de la lib\u00e9ration de la m\u00e9moire est requis. Une mauvaise gestion de ces aspects peut conduire \u00e0 des fuites de m\u00e9moire ou \u00e0 des inefficacit\u00e9s critiques.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#exemple", "title": "Exemple", "text": "

    Prenons un exemple simple pour illustrer le concept de tableau dynamique en C. Imaginons que nous souhaitons cr\u00e9er un buffer pour stocker une s\u00e9quence de trois caract\u00e8res\u2009:

    char *buffer = malloc(3);\n\nbuffer[0] = 'h';\nbuffer[1] = 'e';\nbuffer[2] = 'l'; // Le buffer est plein...\n

    \u00c0 ce stade, le buffer a atteint sa capacit\u00e9 maximale. Si nous souhaitons ajouter davantage de caract\u00e8res, il nous faut augmenter la taille du buffer en r\u00e9allouant dynamiquement de l'espace m\u00e9moire\u2009:

    // Augmente dynamiquement la taille du buffer \u00e0 5 caract\u00e8res\nchar *tmp = realloc(buffer, 5);\nassert(tmp != NULL);\nbuffer = tmp;\n\n// Continue de remplir le buffer\nbuffer[3] = 'l';\nbuffer[4] = 'o'; // Le buffer est \u00e0 nouveau plein...\n

    Apr\u00e8s avoir utilis\u00e9 le buffer, il est crucial de lib\u00e9rer l'espace m\u00e9moire allou\u00e9 pour \u00e9viter les fuites de m\u00e9moire\u2009:

    free(buffer);\n

    Lors de la r\u00e9allocation, la taille du nouvel espace m\u00e9moire est souvent augment\u00e9e selon un facteur de croissance pr\u00e9d\u00e9fini. Ce facteur varie g\u00e9n\u00e9ralement entre 1,5 et 2, selon le langage de programmation ou le compilateur utilis\u00e9. Ainsi, si l\u2019on suit un facteur de 2, les tailles successives du tableau pourraient \u00eatre 1, 2, 4, 8, 16, 32, et ainsi de suite.

    Il est \u00e9galement possible de r\u00e9duire la taille allou\u00e9e lorsque le nombre d'\u00e9l\u00e9ments devient significativement inf\u00e9rieur \u00e0 la taille effective du tableau. Toutefois, en pratique, cette op\u00e9ration est rarement mise en \u0153uvre, car elle peut s\u2019av\u00e9rer inefficace et co\u00fbteuse en termes de performance, comme l\u2019explique cette r\u00e9ponse sur StackOverflow.

    Ainsi, la gestion dynamique de la m\u00e9moire \u00e0 travers des op\u00e9rations de r\u00e9allocation permet une flexibilit\u00e9 pr\u00e9cieuse, mais elle n\u00e9cessite une gestion attentive pour \u00e9viter des inefficacit\u00e9s et des probl\u00e8mes de performances.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#anatomie", "title": "Anatomie", "text": "

    Un tableau dynamique est repr\u00e9sent\u00e9 en m\u00e9moire comme un contenu s\u00e9quentiel qui poss\u00e8de un d\u00e9but et une fin. On appelle son d\u00e9but la t\u00eate (head) ou front et la fin du tableau, sa queue (tail) ou back. Selon que l'on souhaite ajouter des \u00e9l\u00e9ments au d\u00e9but ou \u00e0 la fin du tableau, la complexit\u00e9 n'est pas la m\u00eame.

    Nous d\u00e9finirons par la suite les op\u00e9rations suivantes\u2009:

    Op\u00e9rations sur un tableau dynamique

    Vocabulaire des actions sur un tableau dynamique Action Terme technique Ajout d'un \u00e9l\u00e9ment \u00e0 la t\u00eate du tableau push-front, unshift Ajout d'un \u00e9l\u00e9ment \u00e0 la queue du tableau push-back, push Suppression d'un \u00e9l\u00e9ment \u00e0 la t\u00eate du tableau pop-front, shift Suppression d'un \u00e9l\u00e9ment \u00e0 la queue du tableau pop-back, pop Insertion d'un \u00e9l\u00e9ment \u00e0 la position n insert Suppression d'un \u00e9l\u00e9ment \u00e0 la position n delete

    Nous comprenons rapidement qu'il est plus compliqu\u00e9 d'ajouter ou de supprimer un \u00e9l\u00e9ment depuis la t\u00eate du tableau, car il est n\u00e9cessaire ensuite de d\u00e9placer chaque \u00e9l\u00e9ment (l'\u00e9l\u00e9ment 0 devient l'\u00e9l\u00e9ment 1, l'\u00e9l\u00e9ment 1 devient l'\u00e9l\u00e9ment 2...).

    Un tableau dynamique peut \u00eatre repr\u00e9sent\u00e9 par la figure suivante\u2009:

    Tableau dynamique

    Un espace m\u00e9moire est r\u00e9serv\u00e9 dynamiquement sur le tas. Comme malloc ne retourne pas la taille de l'espace m\u00e9moire allou\u00e9, mais juste un pointeur sur cet espace, il est n\u00e9cessaire de conserver dans une variable la capacit\u00e9 du tableau. Notons qu'un tableau de 10 int32_t repr\u00e9sentera un espace m\u00e9moire de 4x10 bytes, soit 40 bytes. La m\u00e9moire ainsi r\u00e9serv\u00e9e par malloc n'est g\u00e9n\u00e9ralement pas vide, mais elle contient des valeurs, vestige d'une ancienne allocation m\u00e9moire d'un autre programme depuis que l'ordinateur a \u00e9t\u00e9 allum\u00e9. Pour conna\u00eetre le nombre d'\u00e9l\u00e9ments effectifs du tableau, il faut \u00e9galement le m\u00e9moriser. Enfin, le pointeur sur l'espace m\u00e9moire est aussi m\u00e9moris\u00e9.

    Les composants de cette structure de donn\u00e9e sont donc\u2009:

    La capacit\u00e9

    Un entier non sign\u00e9 size_t repr\u00e9sentant la capacit\u00e9 de stockage totale du tableau dynamique \u00e0 un instant T lorsqu'il est plein.

    Le nombre d'\u00e9l\u00e9ments

    Un entier non sign\u00e9 size_t repr\u00e9sentant le nombre d'\u00e9l\u00e9ments effectifs dans le tableau.

    Un pointeur de donn\u00e9es

    Un pointeur sur un entier int * contenant l'adresse m\u00e9moire de l'espace allou\u00e9 par malloc.

    Les donn\u00e9es

    Un espace m\u00e9moire allou\u00e9 par malloc et contenant les donn\u00e9es.

    Il peut \u00eatre d\u00e9clar\u00e9 sous forme de structure\u2009:

    typedef struct vector {\n    size_t capacity;\n    size_t size;\n    int* data;\n} Vector```\n
    ", "tags": ["pop", "size_t", "malloc", "shift", "insert", "delete", "unshift", "int32_t", "push"]}, {"location": "course-c/27-data-structures/dynamic-array/#pop-pop_back", "title": "Pop (pop_back)", "text": "

    L'op\u00e9ration pop retire l'\u00e9l\u00e9ment de la fin du tableau. Le nombre d'\u00e9l\u00e9ments est donc ajust\u00e9 en cons\u00e9quence.

    Suppression d'un \u00e9l\u00e9ment dans un tableau dynamique

    if (elements <= 0) exit(EXIT_FAILURE);\nint value = data[--elements];\n
    ", "tags": ["pop"]}, {"location": "course-c/27-data-structures/dynamic-array/#push-push_back", "title": "Push (push_back)", "text": "

    L'op\u00e9ration push ajoute un \u00e9l\u00e9ment \u00e0 la fin du tableau.

    Ajout d'un \u00e9l\u00e9ment dans un tableau dynamique

    if (elements >= capacity) exit(EXIT_FAILURE);\ndata[elements++] = value;\n
    ", "tags": ["push"]}, {"location": "course-c/27-data-structures/dynamic-array/#shift-pop_front", "title": "Shift (pop_front)", "text": "

    L'op\u00e9ration shift retire un \u00e9l\u00e9ment depuis le d\u00e9but. L'op\u00e9ration \u00e0 une complexit\u00e9 de O(n) puisqu'\u00e0 chaque op\u00e9ration il est n\u00e9cessaire de d\u00e9placer chaque \u00e9l\u00e9ment qu'il contient.

    Suppression du premier \u00e9l\u00e9ment dans un tableau dynamique

    if (elements <= 0) exit(EXIT_FAILURE);\nint value = data[0];\nfor (int k = 0; k < capacity; k++)\n    data[k] = data[k+1];\n

    Une optimisation peut \u00eatre faite en d\u00e9pla\u00e7ant le pointeur de donn\u00e9e de 1 permettant de r\u00e9duite la complexit\u00e9 \u00e0 O(1) :

    if (elements <= 0) exit(EXIT_FAILURE);\nif (capacity <= 0) exit(EXIT_FAILURE);\nint value = data[0];\ndata++;\ncapacity--;\n
    ", "tags": ["shift"]}, {"location": "course-c/27-data-structures/dynamic-array/#unshift-push_front", "title": "Unshift (push_front)", "text": "

    Enfin, l'op\u00e9ration unshift ajoute un \u00e9l\u00e9ment depuis le d\u00e9but du tableau\u2009:

    Ajout d'un \u00e9l\u00e9ment en d\u00e9but d'un tableau dynamique

    for (int k = elements; k >= 1; k--)\n    data[k] = data[k - 1];\ndata[0] = value;\n

    Dans le cas ou le nombre d'\u00e9l\u00e9ments atteint la capacit\u00e9 maximum du tableau, il est n\u00e9cessaire de r\u00e9allouer l'espace m\u00e9moire avec realloc. G\u00e9n\u00e9ralement on se contente de doubler l'espace allou\u00e9.

    if (elements >= capacity) {\n    data = realloc(data, capacity *= 2);\n}\n
    ", "tags": ["realloc", "unshift"]}, {"location": "course-c/27-data-structures/dynamic-array/#analyse-de-la-complexite", "title": "Analyse de la complexit\u00e9", "text": "Acc\u00e8s par index \\(O(1)\\)

    Cette op\u00e9ration est rapide et s'effectue en temps constant car les tableaux dynamiques, comme les tableaux statiques, permettent un acc\u00e8s direct \u00e0 chaque \u00e9l\u00e9ment via leur index.

    Push \\(O(1)\\) amorti

    En g\u00e9n\u00e9ral, ajouter un \u00e9l\u00e9ment \u00e0 la fin d'un tableau dynamique prend un temps constant \\(O(1)\\). Cependant, si la capacit\u00e9 du tableau est atteinte, le tableau doit \u00eatre redimensionn\u00e9, ce qui implique de copier tous les \u00e9l\u00e9ments existants vers un nouveau tableau, ce qui a un co\u00fbt \\(O(n)\\). Gr\u00e2ce \u00e0 l'amortissement, la complexit\u00e9 moyenne reste \\(O(1)\\) sur une s\u00e9rie d'op\u00e9rations d'insertion.

    Insertion au d\u00e9but (unshift) ou au milieu \\(O(n)\\)

    Ins\u00e9rer un \u00e9l\u00e9ment au d\u00e9but ou au milieu du tableau n\u00e9cessite de d\u00e9placer les \u00e9l\u00e9ments existants, ce qui implique un co\u00fbt proportionnel au nombre d'\u00e9l\u00e9ments \\(O(n)\\).

    Pop \\(O(1)\\)

    La suppression du dernier \u00e9l\u00e9ment est une op\u00e9ration constante, puisqu'il n'y a pas besoin de r\u00e9arranger les autres \u00e9l\u00e9ments, ou bien r\u00e9server davantage d'espace m\u00e9moire.

    Suppression au d\u00e9but (shift) ou au milieu \\(O(n)\\)

    Comme pour l'insertion, la suppression d'un \u00e9l\u00e9ment en d\u00e9but ou au milieu du tableau n\u00e9cessite de d\u00e9caler les \u00e9l\u00e9ments restants, ce qui entra\u00eene une complexit\u00e9 de \\(O(n)\\).

    Lors d'un redimensionnement il faut parfois copier les \u00e9l\u00e9ments dans un nouveau tableau si realloc ne parviens pas \u00e0 agrandir l'espace existant, ce qui a un co\u00fbt de \\(O(n)\\). Toutefois, cette op\u00e9ration ne se produit que de temps en temps. On dit que le co\u00fbt d'un redimensionnement est est amorti sur les nombreuses op\u00e9rations d'insertion. Prenons un exemple.

    \u00c9lements, Capacit\u00e9, Redimensionnements\n\n1         1\n2         2         1 (2)\n3         4         2 (4)\n4         4\n5         8         3 (8)\n6         8\n7         8\n8         8\n9..16     16        4 (16)\n17..32    32        5 (32)\n33..64    64        6 (64)\n

    Si on insert 64 \u00e9l\u00e9ments on a du redimensionner le tableau 6 fois et on \u00e0 du copier \\(64 + 32 + 16 + 8 + 4 + 2 = 126\\) \u00e9l\u00e9ments en totalit\u00e9. En moyenne c'est comme si chaque insertion avait co\u00fbt\u00e9 \\(126 / 64 \\approx 2\\), soit une constante que l'on \u00e9crit \\(O(1)\\). Comme ce co\u00fbt est amorti sur l'ensemble des \u00e9l\u00e9ments, on dit que la complexit\u00e9 d'insertion est de \\(O(1)\\) amorti.

    ", "tags": ["realloc"]}, {"location": "course-c/27-data-structures/dynamic-array/#pile", "title": "Pile", "text": "

    Une pile est une structure de donn\u00e9es utilis\u00e9e pour empiler des donn\u00e9es de mani\u00e8re temporaire. C'est ce que l'on appelle un LIFO (Last In, First Out).

    Pile ou *stack*

    Nous avons vu que certaines op\u00e9rations sont plus on\u00e9reuses que d'autres. Les op\u00e9rations push-back et pop-back sont les moins gourmandes. Puisque la pile n'a que deux op\u00e9rations, on peut en tirer parti.

    Une pile peut donc \u00eatre impl\u00e9ment\u00e9e sous forme d'un tableau dynamique dans lequel on ne conserverai que deux op\u00e9rations.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#queue", "title": "Queue", "text": "

    Une queue est une structure de donn\u00e9es utilis\u00e9e comme file d'attente. C'est ce que l'on nomme un FIFO (First In, First Out). Les \u00e9l\u00e9ments sont ajout\u00e9s \u00e0 la fin et retir\u00e9s au d\u00e9but. N\u00e9anmoins, afin d'\u00e9viter le d\u00e9placement constant des \u00e9l\u00e9ments en \\(O(n)\\), une file circulaire est souvent utilis\u00e9e.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#buffer-circulaire", "title": "Buffer circulaire", "text": "

    Un tampon circulaire aussi appel\u00e9 buffer circulaire ou ring buffer en anglais est g\u00e9n\u00e9ralement d'une taille fixe et poss\u00e8de deux pointeurs. L'un pointant sur le dernier \u00e9l\u00e9ment (tail) et l'un sur le premier \u00e9l\u00e9ment (head).

    Lorsqu'un \u00e9l\u00e9ment est supprim\u00e9 du buffer, le pointeur de fin est incr\u00e9ment\u00e9. Lorsqu'un \u00e9l\u00e9ment est ajout\u00e9, le pointeur de d\u00e9but est incr\u00e9ment\u00e9.

    Pour permettre la circulation, les indices sont calcul\u00e9s modulo la taille du buffer.

    Il est possible de repr\u00e9senter sch\u00e9matiquement ce buffer comme un cercle et ses deux pointeurs\u2009:

    Exemple d'un tampon circulaire

    Le nombre d'\u00e9l\u00e9ments dans le buffer est la diff\u00e9rence entre le pointeur de t\u00eate et le pointeur de queue, modulo la taille du buffer. N\u00e9anmoins, l'op\u00e9rateur % en C ne fonctionne que sur des nombres positifs et ne retourne pas le r\u00e9sidu positif le plus petit. En sommes, -2 % 5 devrait donner 3, ce qui est le cas en Python, mais en C, en C++ ou en PHP la valeur retourn\u00e9e est -2. Le modulo vrai, math\u00e9matiquement correct, peut \u00eatre calcul\u00e9 ainsi\u2009:

    ((A % M) + M) % M\n

    Les indices sont boucl\u00e9s sur la taille du buffer, l'\u00e9l\u00e9ment suivant est donc d\u00e9fini par\u2009:

    (i + 1) % SIZE\n

    Voici une impl\u00e9mentation possible du buffer circulaire\u2009:

    #define SIZE 16\n#define MOD(A, M) (((A % M) + M) % M)\n#define NEXT(A) (((A) + 1) % SIZE)\n\ntypedef struct Ring {\n    int buffer[SIZE];\n    int head;\n    int tail;\n} Ring;\n\nvoid init(Ring *ring) {\n    ring->head = ring->tail = 0;\n}\n\nint count(Ring *ring) {\n    return MOD(ring->head - ring->tail, size);\n}\n\nbool is_full(Ring *ring) {\n    return count(ring) == SIZE - 1;\n}\n\nbool is_empty(Ring *ring) {\n    return ring->tail == ring->head;\n}\n\nint* enqueue(Ring *ring, int value) {\n    if (is_full(ring)) return NULL;\n    ring->buffer[ring->head] = value;\n    int *el = &ring->buffer[ring->head];\n    ring->head = NEXT(ring->head);\n    return el;\n}\n\nint* dequeue(Ring *ring) {\n    if (is_empty(ring)) return NULL;\n    int *el = &ring->buffer[ring->tail];\n    ring->tail = NEXT(ring->tail);\n    return el;\n}\n
    "}, {"location": "course-c/27-data-structures/graphs/", "title": "Graphes", "text": "

    Un graphe est un ensemble de sommets reli\u00e9s par des ar\u00eates. Les graphes sont utilis\u00e9s pour mod\u00e9liser des relations entre des objets. Par exemple, un graphe peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau social, un r\u00e9seau de transport, un r\u00e9seau de distribution, etc.

    C'est une variante g\u00e9n\u00e9rale des arbres. Un arbre est un graphe particulier o\u00f9 chaque sommet est reli\u00e9 \u00e0 un autre sommet par un chemin unique. Un graphe peut avoir des cycles, c'est-\u00e0-dire des chemins qui reviennent \u00e0 leur point de d\u00e9part.

    "}, {"location": "course-c/27-data-structures/graphs/#types-de-graphes", "title": "Types de graphes", "text": ""}, {"location": "course-c/27-data-structures/graphs/#forets", "title": "For\u00eats", "text": "

    Un graphe sans cycle est appel\u00e9 une for\u00eat. Une for\u00eat est un ensemble d'arbres. Un arbre est un graphe connexe sans cycle.

    "}, {"location": "course-c/27-data-structures/graphs/#graphes-orientes", "title": "Graphes orient\u00e9s", "text": "

    Un graphe orient\u00e9 est un graphe dont les ar\u00eates ont une direction. Les graphes orient\u00e9s sont utilis\u00e9s pour mod\u00e9liser des relations asym\u00e9triques. Par exemple, un graphe orient\u00e9 peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau de transport o\u00f9 les ar\u00eates repr\u00e9sentent des routes \u00e0 sens unique.

    "}, {"location": "course-c/27-data-structures/graphs/#graphes-ponderes", "title": "Graphes pond\u00e9r\u00e9s", "text": "

    Un graphe pond\u00e9r\u00e9 est un graphe dont les ar\u00eates ont un poids. Les graphes pond\u00e9r\u00e9s sont utilis\u00e9s pour mod\u00e9liser des relations quantitatives. Par exemple, un graphe pond\u00e9r\u00e9 peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau de transport o\u00f9 les ar\u00eates repr\u00e9sentent des routes avec une longueur ou un co\u00fbt associ\u00e9.

    "}, {"location": "course-c/27-data-structures/graphs/#graphes-bipartis", "title": "Graphes bipartis", "text": "

    Un graphe biparti est un graphe dont les sommets peuvent \u00eatre divis\u00e9s en deux ensembles disjoints. Les ar\u00eates d'un graphe biparti relient les sommets des deux ensembles. Les graphes bipartis sont utilis\u00e9s pour mod\u00e9liser des relations binaires. Par exemple, un graphe biparti peut \u00eatre utilis\u00e9 pour repr\u00e9senter des relations d'adjacence entre deux ensembles d'objets.

    "}, {"location": "course-c/27-data-structures/graphs/#representation-des-graphes", "title": "Repr\u00e9sentation des graphes", "text": "

    Il existe plusieurs fa\u00e7ons de repr\u00e9senter un graphe en m\u00e9moire. Les deux repr\u00e9sentations les plus courantes sont les listes d'adjacence et les matrices d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#liste-dadjacence", "title": "Liste d'adjacence", "text": "

    Dans une liste d'adjacence, chaque sommet est associ\u00e9 \u00e0 une liste de ses voisins. Une liste d'adjacence est une structure de donn\u00e9es dynamique qui permet d'ajouter et de supprimer des ar\u00eates facilement. Cependant, elle n\u00e9cessite plus de m\u00e9moire que les matrices d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#matrice-dadjacence", "title": "Matrice d'adjacence", "text": "

    Dans une matrice d'adjacence, chaque sommet est associ\u00e9 \u00e0 une ligne et une colonne de la matrice. La valeur de la case (i, j) de la matrice indique s'il existe une ar\u00eate entre les sommets i et j. Une matrice d'adjacence est une structure de donn\u00e9es statique qui permet de v\u00e9rifier rapidement l'existence d'une ar\u00eate. Cependant, elle n\u00e9cessite plus de m\u00e9moire que les listes d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#parcours-de-graphes", "title": "Parcours de graphes", "text": "

    Il existe plusieurs algorithmes pour parcourir un graphe. Les deux algorithmes les plus courants sont le parcours en profondeur (DFS) et le parcours en largeur (BFS).

    "}, {"location": "course-c/27-data-structures/graphs/#dfs", "title": "DFS", "text": "

    Le parcours en profondeur (Depth-First Search) est un algorithme r\u00e9cursif qui explore le graphe en profondeur. Il commence par un sommet de d\u00e9part et explore tous les sommets accessibles depuis ce sommet avant de passer au suivant. L'algorithme DFS est utilis\u00e9 pour trouver des cycles dans un graphe, pour v\u00e9rifier la connexit\u00e9 d'un graphe, pour trouver des composantes fortement connexes, etc.

    "}, {"location": "course-c/27-data-structures/graphs/#bfs", "title": "BFS", "text": "

    Le parcours en largeur (Breadth-First Search) est un algorithme it\u00e9ratif qui explore le graphe en largeur. Il commence par un sommet de d\u00e9part et explore tous les sommets \u00e0 une distance k avant de passer \u00e0 la distance k+1. L'algorithme BFS est utilis\u00e9 pour trouver le plus court chemin entre deux sommets, pour trouver le nombre de composantes connexes, pour trouver le nombre de sommets \u00e0 une distance donn\u00e9e, etc.

    "}, {"location": "course-c/27-data-structures/graphs/#dijkstra", "title": "Dijkstra", "text": "

    L'algorithme de Dijkstra est un algorithme qui permet de trouver le plus court chemin entre un sommet de d\u00e9part et tous les autres sommets d'un graphe pond\u00e9r\u00e9. L'algorithme de Dijkstra est bas\u00e9 sur le parcours en largeur et utilise une file de priorit\u00e9 pour explorer les sommets dans l'ordre croissant de leur distance par rapport au sommet de d\u00e9part.

    "}, {"location": "course-c/27-data-structures/lists/", "title": "Lists", "text": ""}, {"location": "course-c/27-data-structures/lists/#listes-chainees", "title": "Listes cha\u00een\u00e9es", "text": ""}, {"location": "course-c/27-data-structures/lists/#definition", "title": "D\u00e9finition", "text": "

    Les listes cha\u00een\u00e9es, en informatique, repr\u00e9sentent une structure de donn\u00e9es d'une \u00e9l\u00e9gance discr\u00e8te, dont la simplicit\u00e9 apparente dissimule une puissance d'adaptation remarquable. Contrairement aux tableaux dynamiques, qui d\u00e9pendent d'une allocation contigu\u00eb en m\u00e9moire et n\u00e9cessitent des r\u00e9allocations co\u00fbteuses lorsque leur capacit\u00e9 est d\u00e9pass\u00e9e, les listes cha\u00een\u00e9es se distinguent par leur nature flexible et d\u00e9centralis\u00e9e.

    Une liste cha\u00een\u00e9e se compose d'une s\u00e9rie de n\u0153uds, chacun contenant un \u00e9l\u00e9ment de donn\u00e9es ainsi qu'un pointeur vers le n\u0153ud suivant. Cette architecture singuli\u00e8re permet \u00e0 la liste de cro\u00eetre et de se contracter sans n\u00e9cessiter de r\u00e9allocation massive de m\u00e9moire\u2009: il suffit d'ajouter ou de retirer des n\u0153uds au gr\u00e9 des besoins, sans d\u00e9placer les \u00e9l\u00e9ments existants. En d'autres termes, la m\u00e9moire n'est allou\u00e9e que lorsque cela est n\u00e9cessaire, \u00e9liminant ainsi les g\u00e2chis d'espace que peuvent entra\u00eener les tableaux dynamiques lorsque ceux-ci sont sous-utilis\u00e9s.

    L\u2019avantage principal de cette structure r\u00e9side dans sa capacit\u00e9 \u00e0 ins\u00e9rer ou supprimer des \u00e9l\u00e9ments avec une efficacit\u00e9 redoutable. L\u00e0 o\u00f9 un tableau dynamique doit parfois d\u00e9placer de grandes portions de donn\u00e9es pour ins\u00e9rer ou retirer un \u00e9l\u00e9ment, une liste cha\u00een\u00e9e ne demande que la modification des quelques pointeurs concern\u00e9s. Cette op\u00e9ration, d'une l\u00e9g\u00e8ret\u00e9 exemplaire, conf\u00e8re \u00e0 la liste cha\u00een\u00e9e une fluidit\u00e9 dans la manipulation des donn\u00e9es que les tableaux, m\u00eame dynamiques, ne sauraient \u00e9galer.

    Cependant, cette flexibilit\u00e9 n'est pas sans contrepartie. L'acc\u00e8s direct \u00e0 un \u00e9l\u00e9ment particulier est plus lent dans une liste cha\u00een\u00e9e que dans un tableau, car il faut parcourir les n\u0153uds un \u00e0 un, en suivant les pointeurs. Cette absence d'acc\u00e8s index\u00e9, qui fait la force du tableau, devient ici une faiblesse relative, surtout pour les op\u00e9rations qui n\u00e9cessitent de fr\u00e9quentes consultations des donn\u00e9es.

    "}, {"location": "course-c/27-data-structures/lists/#exemple", "title": "Exemple", "text": "

    Pour illustrer cette id\u00e9e, imaginons un tableau statique dans lequel chaque \u00e9l\u00e9ment est d\u00e9crit par la structure suivante\u2009:

    struct Element {\n    int value;\n    int index_next_element;\n};\n\nstruct Element elements[100];\n

    Consid\u00e9rons les dix premiers \u00e9l\u00e9ments de la s\u00e9quence de nombre A130826 dans un tableau statique. Ensuite, r\u00e9partissons ces valeurs al\u00e9atoirement dans notre tableau elements d\u00e9clar\u00e9 plus haut entre les indices 0 et 19.

    Construction d'une liste chain\u00e9e \u00e0 l'aide d'un tableau

    On observe sur la figure ci-dessus que les \u00e9l\u00e9ments n'ont plus besoin de se suivre en m\u00e9moire, car il est possible facilement de chercher l'\u00e9l\u00e9ment suivant de la liste avec cette relation\u2009:

    struct Element current = elements[4];\nstruct Element next = elements[current.index_next_element]\n

    De m\u00eame, ins\u00e9rer une nouvelle valeur 13 apr\u00e8s la valeur 42 est tr\u00e8s facile\u2009:

    // Recherche de l'\u00e9l\u00e9ment contenant la valeur 42\nstruct Element el = elements[0];\nwhile (el.value != 42 && el.index_next_element != -1) {\n    el = elements[el.index_next_element];\n}\nif (el.value != 42) abort();\n\n// Recherche d'un \u00e9l\u00e9ment libre\nconst int length = sizeof(elements) / sizeof(elements[0]);\nint k;\nfor (k = 0; k < length; k++)\n    if (elements[k].index_next_element == -1)\n        break;\nassert(k < length && elements[k].index_next_element == -1);\n\n// Cr\u00e9ation d'un nouvel \u00e9l\u00e9ment\nstruct Element new = (Element){\n    .value = 13,\n    .index_next_element = -1\n};\n\n// Insertion de l'\u00e9l\u00e9ment quelque part dans le tableau\nel.index_next_element = k;\nelements[el.index_next_element] = new;\n

    Cette solution d'utiliser un lien vers l'\u00e9l\u00e9ment suivant et s'appelle liste cha\u00een\u00e9e. Chaque \u00e9l\u00e9ment dispose d'un lien vers l'\u00e9l\u00e9ment suivant situ\u00e9 quelque part en m\u00e9moire. Les op\u00e9rations d'insertion et de suppression au milieu de la cha\u00eene sont maintenant effectu\u00e9es en \\(O(1)\\) contre \\(O(n)\\) pour un tableau standard. En revanche l'espace n\u00e9cessaire pour stocker ce tableau est doubl\u00e9 puisqu'il faut associer \u00e0 chaque valeur le lien vers l'\u00e9l\u00e9ment suivant.

    D'autre part, la solution propos\u00e9e n'est pas optimale\u2009:

    • L'\u00e9l\u00e9ment 0 est un cas particulier qu'il faut traiter diff\u00e9remment. Le premier \u00e9l\u00e9ment de la liste doit toujours \u00eatre positionn\u00e9 \u00e0 l'indice 0 du tableau. Ins\u00e9rer un nouvel \u00e9l\u00e9ment en d\u00e9but de tableau demande de d\u00e9placer cet \u00e9l\u00e9ment ailleurs en m\u00e9moire.
    • Rechercher un \u00e9l\u00e9ment libre prend du temps.
    • Supprimer un \u00e9l\u00e9ment dans le tableau laisse une place m\u00e9moire vide. Il devient alors difficile de savoir o\u00f9 sont les emplacements m\u00e9moires disponibles.

    Une liste cha\u00een\u00e9e est une structure de donn\u00e9es permettant de lier des \u00e9l\u00e9ments structur\u00e9s entre eux. La liste est caract\u00e9ris\u00e9e par\u2009:

    • un \u00e9l\u00e9ment de t\u00eate (head),
    • un \u00e9l\u00e9ment de queue (tail).

    Un \u00e9l\u00e9ment est caract\u00e9ris\u00e9 par\u2009:

    • un contenu (payload),
    • une r\u00e9f\u00e9rence vers l'\u00e9l\u00e9ment suivant et/ou pr\u00e9c\u00e9dent dans la liste.

    Les listes cha\u00een\u00e9es r\u00e9duisent la complexit\u00e9 li\u00e9e \u00e0 la manipulation d'\u00e9l\u00e9ments dans une liste. L'empreinte m\u00e9moire d'une liste cha\u00een\u00e9e est plus grande qu'avec un tableau, car \u00e0 chaque \u00e9l\u00e9ment de donn\u00e9e est associ\u00e9 un pointeur vers l'\u00e9l\u00e9ment suivant ou pr\u00e9c\u00e9dent.

    Ce surco\u00fbt est souvent part du compromis entre la complexit\u00e9 d'ex\u00e9cution du code et la m\u00e9moire utilis\u00e9e par ce programme.

    Co\u00fbt des op\u00e9rations dans des structures de donn\u00e9es r\u00e9cursives Structure de donn\u00e9e Pire cas Insertion Suppression Recherche (Tri\u00e9) Recherche (Non tri\u00e9) Tableau, pile, queue \\(O(n)\\) \\(O(n)\\) \\(O(\\log(n))\\) \\(O(n)\\) Liste cha\u00een\u00e9e simple \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\)", "tags": ["elements"]}, {"location": "course-c/27-data-structures/lists/#liste-simplement-chainee-linked-list", "title": "Liste simplement cha\u00een\u00e9e (linked-list)", "text": "

    La figure suivante illustre un set d'\u00e9l\u00e9ments li\u00e9s entre eux \u00e0 l'aide d'un pointeur rattach\u00e9 \u00e0 chaque \u00e9l\u00e9ment. On peut s'imaginer que chaque \u00e9l\u00e9ment peut se situer n'importe o\u00f9 en m\u00e9moire et qu'il n'est alors pas indispensable que les \u00e9l\u00e9ments se suivent dans l'ordre.

    Il est indispensable de bien identifier le dernier \u00e9l\u00e9ment de la liste gr\u00e2ce \u00e0 son pointeur associ\u00e9 \u00e0 la valeur NULL.

    Liste cha\u00een\u00e9e simple

    #include <stdio.h>\n#include <stdlib.h>\n\nstruct Point\n{\n    double x;\n    double y;\n    double z;\n};\n\nstruct Element\n{\n    struct Point point;\n    struct Element* next;\n};\n\nint main(void)\n{\n    struct Element a = {.point = {1,2,3}, .next = NULL};\n    struct Element b = {.point = {4,5,6}, .next = &a};\n    struct Element c = {.point = {7,8,9}, .next = &b};\n\n    a.next = &c;\n\n    struct Element* walk = &a;\n\n    for (size_t i = 0; i < 10; i++)\n    {\n        printf(\"%d. P(x, y, z) = %0.2f, %0.2f, %0.2f\\n\",\n            i,\n            walk->point.x,\n            walk->point.y,\n            walk->point.z\n        );\n\n        walk = walk->next;\n    }\n}\n
    ", "tags": ["NULL"]}, {"location": "course-c/27-data-structures/lists/#operations-sur-une-liste-chainee", "title": "Op\u00e9rations sur une liste cha\u00een\u00e9e", "text": "
    • Cr\u00e9ation
    • Nombre d'\u00e9l\u00e9ments
    • Recherche
    • Insertion
    • Suppression
    • Concat\u00e9nation
    • Destruction

    Lors de la cr\u00e9ation d'un \u00e9l\u00e9ment, on utilise principalement le m\u00e9canisme de l'allocation dynamique ce qui permet de r\u00e9cup\u00e9rer l'adresse de l'\u00e9l\u00e9ment et de faciliter sa manipulation au travers de la liste. \u00a0Ne pas oublier de lib\u00e9rer la m\u00e9moire allou\u00e9e pour les \u00e9l\u00e9ments lors de leur suppression\u2026

    "}, {"location": "course-c/27-data-structures/lists/#calcul-du-nombre-delements-dans-la-liste", "title": "Calcul du nombre d'\u00e9l\u00e9ments dans la liste", "text": "

    Pour \u00e9valuer le nombre d'\u00e9l\u00e9ments dans une liste, on effectue le parcours de la liste \u00e0 partir de la t\u00eate, et on passe d'\u00e9l\u00e9ment en \u00e9l\u00e9ment gr\u00e2ce au champ next de la structure Element. On incr\u00e9ment le nombre d'\u00e9l\u00e9ments jusqu'\u00e0 ce que le pointeur next soit \u00e9gal \u00e0 NULL.

    size_t count = 0;\n\nfor (Element *e = &head; e != NULL; e = e->next)\n    count++;\n
    ", "tags": ["NULL", "Element"]}, {"location": "course-c/27-data-structures/lists/#detection-des-boucles", "title": "D\u00e9tection des boucles", "text": "

    Attention, la technique pr\u00e9c\u00e9dente ne fonctionne pas dans tous les cas, sp\u00e9cialement lorsqu'il y a des boucles dans la liste cha\u00een\u00e9e. Prenons l'exemple suivant\u2009:

    Boucle dans une liste cha\u00een\u00e9e

    La liste se terminant par une boucle, il n'y aura jamais d'\u00e9l\u00e9ment de fin et le nombre d'\u00e9l\u00e9ments calcul\u00e9 sera infini. Or, cette liste a un nombre fixe d'\u00e9l\u00e9ments. Comment donc les compter\u2009?

    Il existe un algorithme nomm\u00e9 d\u00e9tection de cycle de Robert W. Floyd aussi appel\u00e9 algorithme du li\u00e8vre et de la tortue. Il consiste \u00e0 avoir deux pointeurs qui parcourent la liste cha\u00een\u00e9e. L'un avance deux fois plus vite que le second.

    Algorithme de d\u00e9tection de cycle de Robert W. Floyd

    size_t compute_length(Element* head)\n{\n    size_t count = 0;\n\n    Element* slow = head;\n    Element* fast = head;\n\n    while (fast != NULL && fast->next != NULL) {\n        slow = slow->next;\n        fast = fast->next->next;\n\n        count++;\n\n        if (slow == fast) {\n            // Collision\n            break;\n        }\n    }\n\n    // Case when no loops detected\n    if (fast == NULL || fast->next == NULL) {\n        return count;\n    }\n\n    // Move slow to head, keep fast at meeting point.\n    slow = head;\n    while (slow != fast) {\n        slow = slow->next;\n        fast = fast->next;\n\n        count--;\n    }\n\n    return count;\n}\n

    Astuce

    Une bonne id\u00e9e pour se simplifier la vie est simplement d'\u00e9viter la cr\u00e9ation de boucles.

    "}, {"location": "course-c/27-data-structures/lists/#insertion", "title": "Insertion", "text": "

    L'insertion d'un \u00e9l\u00e9ment dans une liste cha\u00een\u00e9e peut-\u00eatre impl\u00e9ment\u00e9e de la fa\u00e7on suivante\u2009:

    Element* insert_after(Element* e, void* payload)\n{\n    Element* new = malloc(sizeof(Element));\n\n    memcpy(new->payload, payload, sizeof(new->payload));\n\n    new->next = e->next;\n    e->next = new;\n\n    return new;\n}\n
    "}, {"location": "course-c/27-data-structures/lists/#suppression", "title": "Suppression", "text": "

    La suppression implique d'acc\u00e9der \u00e0 l'\u00e9l\u00e9ment parent, il n'est donc pas possible \u00e0 partir d'un \u00e9l\u00e9ment donn\u00e9 de le supprimer de la liste.

    void delete_after(Element* e)\n{\n    e->next = e->next->next;\n    free(e);\n}\n
    "}, {"location": "course-c/27-data-structures/lists/#recherche", "title": "Recherche", "text": "

    Rechercher dans une liste cha\u00een\u00e9e est une question qui peut-\u00eatre complexe et il est n\u00e9cessaire de ce poser un certain nombre de questions\u2009:

    • Est-ce que la liste est tri\u00e9e\u2009?
    • Combien d'espace m\u00e9moire puis-je utiliser\u2009?

    On sait qu'une recherche id\u00e9ale s'effectue en \\(O(log(n))\\), mais que la solution triviale en \\(O(n)\\) est la suivante\u2009:

    "}, {"location": "course-c/27-data-structures/lists/#liste-doublement-chainee", "title": "Liste doublement cha\u00een\u00e9e", "text": "

    La liste doublement cha\u00een\u00e9e n'est qu'une extension de la liste cha\u00een\u00e9e simple dans laquelle on rajoute un pointeur vers l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent. Cela augmente l'emprunte m\u00e9moire de chaque \u00e9l\u00e9ment mais permet de parcourir la liste dans les deux sens.

    Liste cha\u00een\u00e9e simple

    "}, {"location": "course-c/27-data-structures/lists/#liste-chainee-xor", "title": "Liste cha\u00een\u00e9e XOR", "text": "

    L'inconv\u00e9nient d'une liste doublement cha\u00een\u00e9e est le surco\u00fbt n\u00e9cessaire au stockage d'un \u00e9l\u00e9ment. Chaque \u00e9l\u00e9ment contient en effet deux pointeurs sur l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent (prev) et suivant (next).

    ...  A       B         C         D         E  ...\n        \u2013>  next \u2013>  next  \u2013>  next  \u2013>\n        <\u2013  prev <\u2013  prev  <\u2013  prev  <\u2013\n

    Cette liste cha\u00een\u00e9e particuli\u00e8re compresse les deux pointeurs en un seul en utilisant l'op\u00e9ration XOR (d\u00e9not\u00e9e \u2295).

    ...  A        B         C         D         E  ...\n        <\u2013>  A\u2295C  <->  B\u2295D  <->  C\u2295E  <->\n

    Lorsque la liste est travers\u00e9e de gauche \u00e0 droite, il est possible de facilement reconstruire le pointeur de l'\u00e9l\u00e9ment suivant \u00e0 partir de l'adresse de l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent.

    Les inconv\u00e9nients de cette structure sont\u2009:

    • Difficult\u00e9s de d\u00e9bogage
    • Complexit\u00e9 de mise en \u0153uvre

    L'avantage principal \u00e9tant le gain de place en m\u00e9moire.

    "}, {"location": "course-c/27-data-structures/lists/#liste-chainee-deroulee-unrolled-linked-list", "title": "Liste cha\u00een\u00e9e d\u00e9roul\u00e9e (Unrolled linked list)", "text": "

    Une liste cha\u00een\u00e9e d\u00e9roul\u00e9e rassemble les avantages d'un tableau et d'une liste cha\u00een\u00e9e. Elle permet d'accro\u00eetre les performances en r\u00e9duisant l'overhead de r\u00e9servation m\u00e9moire avec malloc.

    Liste cha\u00een\u00e9e d\u00e9roul\u00e9e

    typedef struct Node {\n    struct Node *next;\n    size_t count;  // Nombre d'\u00e9l\u00e9ments\n    int elements[]; // Membre flexible contenant les \u00e9l\u00e9ments\n} Node;\n
    ", "tags": ["malloc"]}, {"location": "course-c/27-data-structures/lists/#piles-ou-lifo-last-in-first-out", "title": "Piles ou LIFO (Last In First Out)", "text": "

    Une pile est une structure de donn\u00e9e tr\u00e8s similaire \u00e0 un tableau dynamique, mais dans laquelle les op\u00e9rations sont limit\u00e9es. Par exemple, il n'est possible que\u2009:

    • d'ajouter un \u00e9l\u00e9ment (push) ;
    • retirer un \u00e9l\u00e9ment (pop) ;
    • obtenir le dernier \u00e9l\u00e9ment ajout\u00e9 (peek) ;
    • tester si la pile est vide (is_empty) ;
    • tester si la pile est pleine avec (is_full).

    Une utilisation possible de pile sur des entiers serait la suivante\u2009:

    #include \"stack.h\"\n\nint main() {\n    Stack stack;\n    stack_init(&stack);\n\n    stack_push(42);\n    assert(stack_peek() == 42);\n\n    stack_push(23);\n    assert(!stack_is_empty());\n\n    assert(stack_pop() == 23);\n    assert(stack_pop() == 42);\n\n    assert(stack_is_empty());\n}\n

    Les piles peuvent \u00eatre impl\u00e9ment\u00e9es avec des tableaux dynamiques ou des listes cha\u00een\u00e9es (voir plus bas).

    "}, {"location": "course-c/27-data-structures/lists/#queues-ou-fifo-first-in-first-out", "title": "Queues ou FIFO (First In First Out)", "text": "

    Les queues sont aussi des structures tr\u00e8s similaires \u00e0 des tableaux dynamiques, mais elles ne permettent que les op\u00e9rations suivantes\u2009:

    • ajouter un \u00e9l\u00e9ment \u00e0 la queue (push) aussi nomm\u00e9 enqueue ;
    • supprimer un \u00e9l\u00e9ment au d\u00e9but de la queue (shift) aussi nomm\u00e9 dequeue ;
    • tester si la queue est vide (is_empty) ;
    • tester si la queue est pleine avec (is_full).

    Les queues sont souvent utilis\u00e9es lorsque des processus s\u00e9quentiels ou parall\u00e8les s'\u00e9changent des t\u00e2ches \u00e0 traiter\u2009:

    #include \"queue.h\"\n#include <stdio.h>\n\nvoid get_work(Queue *queue) {\n    while (!feof(stdin)) {\n        int n;\n        if (scanf(\"%d\", &n) == 1)\n            queue_enqueue(n);\n        scanf(\"%*[^\\n]%[\\n]\");\n    }\n}\n\nvoid process_work(Queue *queue) {\n    while (!is_empty(queue)) {\n        int n = queue_dequeue(queue);\n        printf(\"%d est %s\\n\", n, n % 2 ? \"impair\" : \"pair\";\n    }\n}\n\nint main() {\n    Queue* queue;\n\n    queue_init(&queue);\n    get_work(queue);\n    process_work(queue);\n    queue_free(queue);\n}\n
    "}, {"location": "course-c/27-data-structures/maps/", "title": "Tableaux de hachage", "text": "

    Les tableaux de hachage (Hash Table) sont une structure particuli\u00e8re dans laquelle une fonction dite de hachage est utilis\u00e9e pour transformer les entr\u00e9es en des indices d'un tableau.

    L'objectif est de stocker des cha\u00eenes de caract\u00e8res correspondant a des noms simples ici utilis\u00e9s pour l'exemple. Une possible r\u00e9partition serait la suivante\u2009:

    Tableau de hachage simple

    Si l'on cherche l'indice correspondant \u00e0 Ada, il convient de pouvoir calculer la valeur de l'indice correspondant \u00e0 partir de la valeur de la cha\u00eene de caract\u00e8re. Pour calculer cet indice aussi appel\u00e9 hash, il existe une infinit\u00e9 de m\u00e9thodes. Dans cet exemple, consid\u00e9rons une m\u00e9thode simple. Chaque lettre est identifi\u00e9e par sa valeur ASCII et la somme de toutes les valeurs ASCII est calcul\u00e9e. Le modulo 10 est ensuite calcul\u00e9 sur cette somme pour obtenir une valeur entre 0 et 9. Ainsi nous avons les calculs suivants\u2009:

    Nom    Valeurs ASCII     Somme  Modulo 10\n---    --------------    -----  ---------\nMia -> {77, 105, 97}  -> 279 -> 4\nTim -> {84, 105, 109} -> 298 -> 1\nBea -> {66, 101, 97}  -> 264 -> 0\nZoe -> {90, 111, 101} -> 302 -> 5\nJan -> {74, 97, 110}  -> 281 -> 6\nAda -> {65, 100, 97}  -> 262 -> 9\nLeo -> {76, 101, 111} -> 288 -> 2\nSam -> {83, 97, 109}  -> 289 -> 3\nLou -> {76, 111, 117} -> 304 -> 7\nMax -> {77, 97, 120}  -> 294 -> 8\nTed -> {84, 101, 100} -> 285 -> 10\n

    Pour trouver l'indice de \"Mia\" il suffit donc d'appeler la fonction suivante\u2009:

    int hash_str(char *s) {\n    int sum = 0;\n    while (*s != '\\0') sum += s++;\n    return sum % 10;\n}\n

    L'assertion suivante est donc vraie\u2009:

    assert(strcmp(table[hash_str(\"Mia\")], \"Mia\") == 0);\n

    Rechercher \"Mia\" et obtenir \"Mia\" n'est certainement pas l'exemple le plus utile. N\u00e9anmoins il est possible d'encoder plus qu'une cha\u00eene de caract\u00e8re et utiliser plut\u00f4t une structure de donn\u00e9e\u2009:

    struct Person {\n    char name[3 + 1 /* '\\0' */];\n    struct {\n        int month;\n        int day;\n        int year;\n    } born;\n    enum {\n        JOB_ASTRONOMER,\n        JOB_INVENTOR,\n        JOB_ACTRESS,\n        JOB_LOGICIAN,\n        JOB_BIOLOGIST\n    } job;\n    char country_code; // For example 41 for Switzerland\n};\n

    Dans ce cas, le calcul du hash se ferait sur la premi\u00e8re cl\u00e9 d'un \u00e9l\u00e9ment\u2009:

    int hash_person(struct Person person) {\n    int sum = 0;\n    while (*person.name != '\\0') sum += s++;\n    return sum % 10;\n}\n

    L'acc\u00e8s \u00e0 une personne \u00e0 partir de la cl\u00e9 se r\u00e9sout donc en O(1) car il n'y a aucune it\u00e9ration ou recherche \u00e0 effectuer.

    Cette vid\u00e9o YouTube explique bien le fonctionnement des tableaux de hachage.

    ", "tags": ["Ada"]}, {"location": "course-c/27-data-structures/maps/#collisions", "title": "Collisions", "text": "

    Lorsque la fonction de hachage est mal choisie, un certain nombre de collisions peuvent appara\u00eetre. Si l'on souhaite par exemple ajouter les personnes suivantes\u2009:

    Sue -> {83, 117, 101} -> 301 -> 4\nLen -> {76, 101, 110} -> 287 -> 1\n

    On voit que les positions 4 et 1 sont d\u00e9j\u00e0 occup\u00e9es par Mia et Tim.

    Une strat\u00e9gie de r\u00e9solution s'appelle Open adressing. Parmi les possibilit\u00e9s de cette strat\u00e9gie, le linear probing consiste \u00e0 v\u00e9rifier si la position du tableau est d\u00e9j\u00e0 occup\u00e9e et en cas de collision, chercher la prochaine place disponible dans le tableau\u2009:

    Person people[10] = {0}\n\n// Add Mia\nPerson mia = {.name=\"Mia\", .born={.day=1,.month=4,.year=1991}};\nint hash = hash_person(mia);\nwhile (people[hash].name[0] != '\\0') hash++;\npeople[hash] = mia;\n

    R\u00e9cup\u00e9rer une valeur dans le tableau demande une comparaison suppl\u00e9mentaire\u2009:

    char key[] = \"Mia\";\nint hash = hash_str(key)\nwhile (strcmp(people[hash], key) != 0) hash++;\nPerson person = people[hash];\n

    Lorsque le nombre de collisions est n\u00e9gligeable par rapport \u00e0 la table de hachage, la recherche d'un \u00e9l\u00e9ment est toujours en moyenne \u00e9gale \u00e0 \\(O(1)\\), mais lorsque le nombre de collisions est pr\u00e9pond\u00e9rant, la complexit\u00e9 se rapproche de celle de la recherche lin\u00e9aire \\(O(n)\\) et on perd tout avantage \u00e0 cette structure de donn\u00e9e.

    Dans le cas extr\u00eame, pour garantir un acc\u00e8s unitaire pour tous les noms de trois lettres, il faudrait un tableau de hachage d'une taille \\(26^3 = 17576\\) personnes. L'empreinte m\u00e9moire peut \u00eatre consid\u00e9rablement r\u00e9duite en stockant non pas une structure struct Person mais plut\u00f4t l'adresse vers cette structure\u2009:

    struct Person *people[26 * 26 * 26] = { NULL };\n

    Dans ce cas exag\u00e9r\u00e9, la fonction de hachage pourrait \u00eatre la suivante\u2009:

    int hash_name(char name[4]) {\n    int base = 26;\n    return\n        (name[0] - 'A') * 1 +\n        (name[1] - 'a') * 26 +\n        (name[2] - 'a') * 26 * 26;\n}\n
    "}, {"location": "course-c/27-data-structures/maps/#facteur-de-charge", "title": "Facteur de charge", "text": "

    Le facteur de charge d'une table de hachage est donn\u00e9 par la relation\u2009:

    \\[ \\text{Facteur de charge} = \\frac{\\text{Nombre total d'\u00e9l\u00e9ments}}{\\text{Taille de la table}} \\]

    Plus ce facteur de charge est \u00e9lev\u00e9, dans le cas du linear probing, moins bon sera la performance de la table de hachage.

    Certains algorithmes permettent de redimensionner dynamiquement la table de hachage pour conserver un facteur de charge le plus faible possible. Quand le facteur de charge d\u00e9passe un certain seuil (souvent 0.7), la table de hachage est agrandi (souvent doubl\u00e9e comme pour un tableau dynamique) et les \u00e9l\u00e9ments sont re-hach\u00e9s dans la nouvelle table.

    "}, {"location": "course-c/27-data-structures/maps/#chainage", "title": "Cha\u00eenage", "text": "

    Le cha\u00eenage ou chaining est une autre m\u00e9thode pour mieux g\u00e9rer les collisions. La table de hachage est coupl\u00e9e \u00e0 une liste cha\u00een\u00e9e.

    Cha\u00eenage d'une table de hachage

    "}, {"location": "course-c/27-data-structures/maps/#adressage-ouvert", "title": "Adressage ouvert", "text": "

    L'adressage ouvert est une autre m\u00e9thode pour g\u00e9rer les collisions. Lorsqu'une collision est d\u00e9tect\u00e9e, une autre position est calcul\u00e9e pour stocker l'\u00e9l\u00e9ment.

    Si une collision est d\u00e9tect\u00e9e, on regardera la position suivante dans la table. Si elle est libre on l'utilise, sinon la suitante, jusqu'\u00e0 trouver une position libre. Cette m\u00e9thode est appel\u00e9e linear probing.

    Une autre m\u00e9thode consiste \u00e0 utiliser une fonction de hachage secondaire pour calculer la position suivante. Cette m\u00e9thode est appel\u00e9e double hashing.

    Si la m\u00e9thode est plus facile \u00e0 impl\u00e9menter, l'op\u00e9ration de suppression est plus complexe. En effet, il est souvent n\u00e9cessaire de re-hacher les \u00e9l\u00e9ments pour maintenir la performance de la table de hachage.

    "}, {"location": "course-c/27-data-structures/maps/#fonction-de-hachage", "title": "Fonction de hachage", "text": "

    Nous avons vu plus haut une fonction de hachage calculant le modulo sur la somme des caract\u00e8res ASCII d'une cha\u00eene de caract\u00e8res. Nous avons \u00e9galement vu que cette fonction de hachage est source de nombreuses collisions. Les cha\u00eenes \"Rea\" ou \"Rae\" auront les m\u00eame hash puisqu'ils contiennent les m\u00eames lettres. De m\u00eame une fonction de hachage qui ne r\u00e9partit pas bien les \u00e9l\u00e9ments dans la table de hachage sera mauvaise. On sait par exemple que les voyelles sont nombreuses dans les mots et qu'il n'y en a que six et que la probabilit\u00e9 que nos noms de trois lettres contiennent une voyelle en leur milieu est tr\u00e8s \u00e9lev\u00e9e.

    L'id\u00e9e g\u00e9n\u00e9rale des fonctions de hachage est de r\u00e9partir uniform\u00e9ment les cl\u00e9s sur les indices de la table de hachage. L'approche la plus courante est de m\u00e9langer les bits de notre cl\u00e9 dans un processus reproductible.

    Une id\u00e9e mauvaise et \u00e0 ne pas retenir pourrait \u00eatre d'utiliser le caract\u00e8re pseudo-al\u00e9atoire de rand pour hacher nos noms\u2009:

    #include <stdlib.h>\n#include <stdio.h>\n\nint hash(char *str, int mod) {\n    int h = 0;\n    while(*str != '\\0') {\n        srand(h + *str++);\n        h = rand();\n    }\n    return h % mod;\n}\n\nint main() {\n    char *names[] = {\n        \"Bea\", \"Tim\", \"Len\", \"Sam\", \"Ada\", \"Mia\",\n        \"Sue\", \"Zoe\", \"Rae\", \"Lou\", \"Max\", \"Tod\"\n    };\n    for (int i = 0; i < sizeof(names) / sizeof(*names); i++)\n        printf(\"%s : %d\\n\", names[i], hash(names[i], 10));\n}\n

    Cette approche nous donne une assez bonne r\u00e9partition\u2009:

    $ ./a.out\nBea : 2\nTim : 3\nLen : 0\nSam : 3\nAda : 4\nMia : 3\nSue : 6\nZoe : 5\nRae : 8\nLou : 0\nMax : 3\nTod : 1\n

    Dans la pratique, on utilisera volontiers des fonctions de hachage utilis\u00e9es en cryptographies tels que MD5 ou SHA. Consid\u00e9rons par exemple la premi\u00e8re partie du po\u00e8me Chanson de Pierre Corneille\u2009:

    $ cat chanson.txt\nSi je perds bien des ma\u00eetresses,\nJ'en fais encor plus souvent,\nEt mes voeux et mes promesses\nNe sont que feintes caresses,\nEt mes voeux et mes promesses\nNe sont jamais que du vent.\n\n$ md5sum chanson.txt\n699bfc5c3fd42a06e99797bfa635f410  chanson.txt\n

    Le hash de ce texte est exprim\u00e9 en hexad\u00e9cimal\u2009:

    0x699bfc5c3fd42a06e99797bfa635f410\n

    Converti en d\u00e9cimal il peut \u00eatre r\u00e9duit en utilisant le modulo.

    140378864046454182829995736237591622672\n

    Voici un exemple en C\u2009:

    #include <stdlib.h>\n#include <stdio.h>\n#include <openssl/md5.h>\n#include <string.h>\n\nint hash(char* str, int mod) {\n    // Compute MD5\n    unsigned int output[4];\n    MD5_CTX md5;\n    MD5_Init(&md5);\n    MD5_Update(&md5, str, strlen(str));\n    MD5_Final((char*)output, &md5);\n\n    // 128-bits --> 32-bits\n    unsigned int h = 0;\n    for (int i = 0; i < sizeof(output)/sizeof(*output); i++) {\n        h ^= output[i];\n    }\n\n    // 32-bits --> mod\n    return h % mod;\n}\n\nint main() {\n    char *text[] = {\n        \"La poule ou l'\u0153uf?\",\n        \"Les pommes sont cuites!\",\n        \"Aussi lentement que possible\",\n        \"La poule ou l'\u0153uf.\",\n        \"La poule ou l'\u0153uf!\",\n        \"Aussi vite que n\u00e9cessaire\",\n        \"Il ne faut pas l\u00e2cher la proie pour l\u2019ombre.\",\n        \"Le mieux est l'ennemi du bien\",\n    };\n\n    for (int i = 0; i < sizeof(text) / sizeof(*text); i++)\n        printf(\"% 2d. %s\\n\", hash(text[i], 10), text[i]);\n}\n
    $ gcc hash.c -lcrypto\n$ ./a.out\n1. La poule ou l'\u0153uf?\n2. Les pommes sont cuites!\n3. Aussi lentement que possible\n4. La poule ou l'\u0153uf.\n5. La poule ou l'\u0153uf!\n6. Aussi vite que n\u00e9cessaire\n8. Il ne faut pas l\u00e2cher la proie pour l\u2019ombre.\n9. Le mieux est l'ennemi du bien\n

    On peut constater qu'ici les indices sont bien r\u00e9partis et que la fonction de hachage choisie semble uniforme.

    ", "tags": ["rand", "SHA"]}, {"location": "course-c/27-data-structures/maps/#fonction-de-hachage-affine", "title": "Fonction de hachage affine", "text": "

    Une autre m\u00e9thode pour calculer le hash consiste \u00e0 multiplier la valeur de chaque caract\u00e8re par une constante et de sommer le tout. Par exemple, la fonction de hachage suivante\u2009:

    int hash(char *str, int mod) {\n    const int a = 31;\n    int h = 0;\n    while(*str != '\\0')\n        h = (h * a + *str++) % mod;\n    return h;\n}\n

    La constante a est souvent choisie comme un nombre premier pour \u00e9viter les collisions.

    On peut \u00e9galement impl\u00e9menter cette fonction pour hacher des entiers\u2009:

    int hash_int(int n, int mod) {\n    const int a = 31;\n    return (a * n) % mod;\n}\n
    "}, {"location": "course-c/27-data-structures/maps/#murmurhash", "title": "MurmurHash", "text": "

    Une autre fonction de hachage tr\u00e8s populaire est MurmurHash. Elle est tr\u00e8s rapide et produit des r\u00e9sultats de qualit\u00e9. Voici un exemple en C\u2009:

    uint32_t murmur3_32(const char *key, uint32_t len, uint32_t seed) {\n    uint32_t h = seed;\n    if (len > 3) {\n        const uint32_t *key_x4 = (const uint32_t *)key;\n        size_t i = len >> 2;\n        do {\n            uint32_t k = *key_x4++;\n            k *= 0xcc9e2d51;\n            k = (k << 15) | (k >> 17);\n            k *= 0x1b873593;\n            h ^= k;\n            h = (h << 13) | (h >> 19);\n            h = h * 5 + 0xe6546b64;\n        } while (--i);\n        key = (const char *)key_x4;\n    }\n    if (len & 3) {\n        size_t i = len & 3;\n        uint32_t k = 0;\n        key = &key[i - 1];\n        do {\n            k <<= 8;\n            k |= *key--;\n        } while (--i);\n        k *= 0xcc9e2d51;\n        k = (k << 15) | (k >> 17);\n        k *= 0x1b873593;\n        h ^= k;\n    }\n    h ^= len;\n    h ^= h >> 16;\n    h *= 0x85ebca6b;\n    h ^= h >> 13;\n    h *= 0xc2b2ae35;\n    h ^= h >> 16;\n    return h;\n}\n

    Les valeurs de seed et len sont des valeurs arbitraires. La valeur de seed est souvent choisie al\u00e9atoirement. La valeur de len est la longueur de la cha\u00eene de caract\u00e8res \u00e0 hacher.

    On observe \u00e9galements des valeurs arbitraires pour les constantes 0xcc9e2d51, 0x1b873593, 0xe6546b64, 0x85ebca6b... Ces valeurs ont \u00e9t\u00e9 choisies pour leur qualit\u00e9 de m\u00e9lange des bits. Elles sont souvent d\u00e9termin\u00e9es empiriquement.

    ", "tags": ["len", "seed"]}, {"location": "course-c/27-data-structures/maps/#comparaison", "title": "Comparaison", "text": "

    Voici une compaison de diff\u00e9rentes fonctions de hachage\u2009:

    Comparaison des fonctions de hachage Fonction de hachage Qualit\u00e9 Vitesse Taille MD5 Bonne (cryptographie) Lente 128 bits SHA-1 Tr\u00e8s bonne (cryptographie) Lente 160 bits SHA-256 Excellente (cryptographie) Tr\u00e8s lente 256 bits MurmurHash3 Bonne (non cryptographique) Tr\u00e8s rapide 32/128 bits CityHash Tr\u00e8s bonne (non cryptographique) Tr\u00e8s rapide 64/128 bits FNV-1a Bonne (non cryptographique) Rapide 32/64 bits DJB2 Acceptable (non cryptographique) Tr\u00e8s rapide 32 bits CRC32 Bonne pour la d\u00e9tection d'erreurs Tr\u00e8s rapide 32 bits"}, {"location": "course-c/27-data-structures/maps/#perte-de-lordre", "title": "Perte de l'ordre", "text": "

    Il est important de noter que les tableaux de hachage ne conservent pas l'ordre des \u00e9l\u00e9ments. Comme ils peuvent \u00eatre ins\u00e9r\u00e9s dans n'importe quelle position du tableau, il n'est pas possible de les parcourir dans l'ordre d'insertion.

    Pire, selon l'algorithme utlis\u00e9, il est possible que si la fonction de hachage ou la taille de la table est modifi\u00e9e en cours de route, les \u00e9l\u00e9ments soient d\u00e9plac\u00e9s dans le tableau, et donc que l'ordre de parcours change.

    Python

    En Python les tables de hachages sont des structures de base du langage appel\u00e9es dict. Avant la version 3.7, l'ordre des \u00e9l\u00e9ments n'\u00e9tait pas conserv\u00e9. Depuis la version 3.7, l'ordre d'insertion est conserv\u00e9. Cela est d\u00fb \u00e0 l'impl\u00e9mentation de la table de hachage qui utilise une seconde structure de donn\u00e9e de type liste cha\u00een\u00e9e pour conserver l'ordre d'insertion. Cela a un impact sur la quantit\u00e9 de m\u00e9moire utilis\u00e9e et la performance de la table de hachage car \u00e0 chaque insertion il faut \u00e9galement mettre \u00e0 jour la liste cha\u00een\u00e9e.

    ", "tags": ["dict"]}, {"location": "course-c/27-data-structures/maps/#complexite-et-implementation", "title": "Complexit\u00e9 et impl\u00e9mentation", "text": "

    La caract\u00e9ristique principale recherch\u00e9e dans une table de hachage est de permettre un acc\u00e8s en temps constant \\(O(1)\\) pour les op\u00e9rations de recherche.

    La complexit\u00e9 de la recherche est en moyenne est donc de \\(O(1)\\), mais peut atteindre \\(O(n)\\) dans le pire des cas, par exemple si le facteur de charge est \u00e9lev\u00e9 et que la table de hachage est mal r\u00e9partie. Il y a donc un compromis \u00e0 trouver entre la m\u00e9moire utilis\u00e9e et la performance de la table de hachage.

    "}, {"location": "course-c/27-data-structures/performances/", "title": "Performances", "text": "

    Les diff\u00e9rentes structures de donn\u00e9es ne sont pas toutes \u00e9quivalentes en termes de performances. Il convient, selon l'application, d'opter pour la structure la plus adapt\u00e9e, et par cons\u00e9quent il est important de pouvoir comparer les diff\u00e9rentes structures de donn\u00e9es pour choisir la plus appropri\u00e9e. Est-ce que les donn\u00e9es doivent \u00eatre maintenues tri\u00e9es\u2009? Est-ce que la structure de donn\u00e9e est utilis\u00e9e comme une pile ou un tas\u2009? Quelle est la structure de donn\u00e9e avec le moins d'overhead pour les op\u00e9rations de push ou unshift ?

    L'indexation (indexing) est l'acc\u00e8s \u00e0 une certaine valeur du tableau par exemple avec a[k]. Dans un tableau statique et dynamique l'acc\u00e8s se fait par pointeur depuis le d\u00e9but du tableau soit\u2009: *((char*)a + sizeof(a[0]) * k) qui est \u00e9quivalant \u00e0 *(a + k). L'indexation par arithm\u00e9tique de pointeur n'est pas possible avec les listes cha\u00een\u00e9es dont il faut parcourir chaque \u00e9l\u00e9ment pour d\u00e9couvrir l'adresse du prochain \u00e9l\u00e9ment\u2009:

    int get(List *list) {\n    List *el = list->head;\n    for(int i = 0; i < k; i++)\n        el = el.next;\n    return el.value;\n}\n

    L'indexation d'une liste cha\u00een\u00e9e prend dans le cas le plus d\u00e9favorable \\(O(n)\\).

    Les arbres binaires ont une structure qui permet naturellement la dichotomique. Chercher l'\u00e9l\u00e9ment 5 prend 4 op\u00e9rations\u2009: 12 -> 4 -> 6 -> 5. L'indexation est ainsi possible en \\(O(log~n)\\).

                12\n             |\n         ----+----\n       /           \\\n      4            12\n     --            --\n   /    \\        /    \\\n  2      6      10    14\n / \\    / \\    / \\   /  \\\n1   3  5   7  9  11 13  15\n

    Le tableau suivant r\u00e9sume les performances obtenues pour les diff\u00e9rentes structures de donn\u00e9es que nous avons vues dans ce chapitre\u2009:

    Comparaison des performances des structures r\u00e9cursives Action Tableau Liste Buffer Arbre Hash Map Statique Dynamique cha\u00een\u00e9e circulaire binaire Indexing 1 1 n 1 log n Unshift/Shift n n 1 1 log n Push/Pop 1 1 amorti 1 1 log n Insert/Delete n n 1 n log n Search n n n n log n Sort n log n n log n n log n n log n 1", "tags": ["push", "unshift"]}, {"location": "course-c/27-data-structures/trees/", "title": "Arbres", "text": "

    Arbre binaire IRL

    Les arbres sont des structures de donn\u00e9es non lin\u00e9aires qui sont compos\u00e9es de n\u0153uds. Chaque n\u0153ud a un ou plusieurs enfants, sauf pour le n\u0153ud racine qui n'a pas de parent. Les arbres sont souvent utilis\u00e9s pour repr\u00e9senter des hi\u00e9rarchies, comme les syst\u00e8mes de fichiers, les arbres g\u00e9n\u00e9alogiques, les arbres de d\u00e9cision, etc.

    Voici un exemple d'arbre, il repr\u00e9sente par exemple une structure de documents stock\u00e9s sur un ordinateur. En haut on voit le disque C\u2009: qui contient des dossiers et des fichiers. Chaque dossier peut contenir d'autres dossiers ou des fichiers. Il y a donc une hi\u00e9rarchie entre les \u00e9l\u00e9ments. Chaque dossier peut contenir plusieurs \u00e9l\u00e9ments, mais chaque \u00e9l\u00e9ment ne peut \u00eatre contenu que dans un seul dossier.

    On appelle ce type d'arbre un arbre n-aire dirig\u00e9. C'est-\u00e0-dire que chaque n\u0153ud peut avoir plusieurs enfants. L'arbre est dirig\u00e9 car il y a un sens de la racine vers les feuilles. Il y a donc des fl\u00e8ches qui indiquent le sens de la hi\u00e9rarchie.

    %% Arbre n-aire dirig\u00e9\ngraph LR\n    C(C:)\n\n    C --> Program_Files(Program Files)\n    C --> Users(Utilisateurs)\n\n    Program_Files --> Microsoft(Microsoft)\n    Program_Files --> Adobe(Adobe)\n    Program_Files --> Google(Google)\n\n    Microsoft --> Office(Office)\n    Microsoft --> Edge(Edge)\n    Microsoft --> Teams(Teams)\n\n    Google --> Chrome(Chrome)\n    Google --> Drive(Drive)\n\n    Users --> Bob(Bob)\n    Users --> Alice(Alice)\n\n    Bob --> Documents(Documents)\n    Bob --> Downloads(Downloads)\n    Bob --> Music(Music)\n\n    Alice --> Documents_Alice(Documents)\n    Alice --> Downloads_Alice(Downloads)\n\n    Documents --> Resume(Resume.docx)\n    Documents --> Project(Project.docx)\n\n    Downloads --> Installer(Installer.exe)\n    Downloads --> Music_Bob(Music.mp3)\n\n    Music --> Album1(Album1)\n    Music --> Album2(Album2)\n\n    Album1 --> Song1(pink-floyd.mp3)\n    Album1 --> Song2(doroth\u00e9e.mp3)\n\n    Documents_Alice --> Thesis(Thesis.docx)\n    Documents_Alice --> Notes(Notes.txt)\n\n    Downloads_Alice --> App(App.exe)
    Arbre n-aire dirig\u00e9", "tags": ["hierarchie", "arbres"]}, {"location": "course-c/27-data-structures/trees/#arbre-binaire", "title": "Arbre binaire", "text": "

    Un arbre binaire est un arbre o\u00f9 chaque n\u0153ud a au plus deux enfants. Les enfants sont g\u00e9n\u00e9ralement appel\u00e9s le fils gauche et le fils droit. Les arbres binaires sont souvent utilis\u00e9s pour impl\u00e9menter des structures de donn\u00e9es comme les arbres de recherche binaires, les tas binaires, les arbres d'expression, etc.

    C'est une structure de donn\u00e9e tr\u00e8s utilis\u00e9e en informatique. En pratique, il est rare d'impl\u00e9menter un arbre binaire de mani\u00e8re explicite. On utilise plut\u00f4t des structures de donn\u00e9es qui sont bas\u00e9es sur des arbres binaires.

    Le C \u00e9tant un langage tr\u00e8s bas niveau, il n'y a pas de structure de donn\u00e9es arbre binaire dans la biblioth\u00e8que standard. En C++ en revanche il y a de nombreux conteneurs qui utilisent des arbres binaires comme std::set, std::map, std::multiset, std::multimap, std::priority_queue, etc.

    Voici l'exemple d'un arbre binaire. Chaque n\u0153ud est compos\u00e9 de deux enfants sauf pour les feuilles qui n'ont pas d'enfants. Le n\u0153ud 40 n'a lui que 1 enfant\u2009: l'enfant de droite.

    %% Arbre binaire\ngraph TD\n    classDef ghost display: none;\n\n    50((50))\n\n    50 --> 30((30))\n    50 --> 70((70))\n\n    30 --> 20((20))\n    30 --> 40((40))\n\n    70 --> 60((60))\n    70 --> 80((80))\n\n    20 --> 10((10))\n    20 --> 25((25))\n\n    40 --> ghost1(( ))\n    40 --> 35((35))\n\n    60 --> 55((55))\n    60 --> 65((65))\n\n    80 --> 75((75))\n    80 --> 90((90))\n\n    class ghost1 ghost;\n    linkStyle 8 display: none;
    Arbre binaire

    Un arbre peut \u00eatre \u00e9quilibr\u00e9 ou d\u00e9s\u00e9quilibr\u00e9. Un arbre est \u00e9quilibr\u00e9 si la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus un. Un arbre \u00e9quilibr\u00e9 est souvent plus efficace pour les op\u00e9rations de recherche, d'insertion et de suppression.

    Voici l'exemple d'un arbre d\u00e9s\u00e9quilibr\u00e9\u2009:

    %% Arbre binaire d\u00e9s\u00e9quilibr\u00e9\ngraph LR\n    classDef ghost display: none;\n\n    50((42))\n\n    50 --> 30((30))\n    50 --> 70((70))\n\n    30 --> 20((20))\n    30 --> 40((40))\n\n    70 --> 60((60))\n    70 --> 80((80))\n\n    20 --> 10((10))\n\n\n\n    60 --> 55((55))\n\n    10 --> 12((12))\n    10 --> 23((23))\n    23 --> 35((35))
    Arbre binaire d\u00e9s\u00e9quilibr\u00e9", "tags": ["arbre-binaire"]}, {"location": "course-c/27-data-structures/trees/#heap", "title": "Heap", "text": "

    La structure de donn\u00e9e heap aussi nomm\u00e9e tas ne doit pas \u00eatre confondue avec le tas utilis\u00e9 en allocation dynamique. Il s'agit d'une forme particuli\u00e8re de l'arbre binaire dit \u00ab\u2009presque complet\u2009\u00bb, dans lequel la diff\u00e9rence de niveau entre les feuilles n'exc\u00e8de pas 1. C'est-\u00e0-dire que toutes les feuilles sont \u00e0 une distance identique de la racine plus ou moins 1.

    Un tas peut ais\u00e9ment \u00eatre repr\u00e9sent\u00e9 sous forme de tableau en utilisant la r\u00e8gle suivante\u2009:

    Op\u00e9ration d'acc\u00e8s \u00e0 un \u00e9l\u00e9ment d'un hea Cible D\u00e9but \u00e0 0 D\u00e9but \u00e0 1 Enfant de gauche \\(2*k + 1\\) \\(2 * k\\) Enfant de droite \\(2*k + 2\\) \\(2 * k + 1\\) Parent \\(floor(k-1) / 2\\) \\(floor(k) / 2\\)

    Repr\u00e9sentation d'un *heap*

    ", "tags": ["heap"]}, {"location": "course-c/27-data-structures/trees/#min-heap", "title": "Min-heap", "text": "

    Un tas binaire est une structure de donn\u00e9es qui permet de stocker des \u00e9l\u00e9ments de mani\u00e8re ordonn\u00e9e. Un tas binaire est un arbre binaire complet o\u00f9 chaque n\u0153ud est plus petit que ses enfants. Un tas binaire est souvent utilis\u00e9 pour impl\u00e9menter une file de priorit\u00e9.

    Impl\u00e9mentation en C

    min-heap.h
    #pragma once\n\n#include <stddef.h>\n#include <stdio.h>\n\ntypedef struct MinHeap MinHeap;\n\nMinHeap *min_heap_create(size_t element_size,\n    int (*compare)(const void *, const void *),\n    void (*free_function)(void *));\nvoid min_heap_destroy(MinHeap *heap);\nint min_heap_insert(MinHeap *heap, void *element);\nvoid *min_heap_extract_min(MinHeap *heap);\nsize_t min_heap_size(MinHeap *heap);\n\nvoid min_heap_to_mermaid(MinHeap *heap, FILE *output, void (print_element)(void *element, FILE *output));\n
    min-heap.c
    #include \"min-heap.h\"\n#include \"vector.h\"\n\n#include <stdlib.h>\n#include <stdio.h>\n\nstruct MinHeap {\n    Vector *vector;\n    int (*compare)(const void *, const void *);\n};\n\nstatic void swap(void **a, void **b) {\n    void *temp = *a;\n    *a = *b;\n}\n\nstatic void heapify_up(MinHeap *heap, size_t index) {\n    if (index == 0) return;\n\n    size_t parent_index = (index - 1) / 2;\n    if (heap->compare(vector_get(heap->vector, index), vector_get(heap->vector, parent_index)) < 0) {\n        swap(&heap->vector->data[index], &heap->vector->data[parent_index]);\n        heapify_up(heap, parent_index);\n    }\n}\n\nstatic void heapify_down(MinHeap *heap, size_t index) {\n    size_t left_child = 2 * index + 1;\n    size_t right_child = 2 * index + 2;\n    size_t smallest = index;\n\n    if (left_child < vector_size(heap->vector) &&\n        heap->compare(vector_get(heap->vector, left_child), vector_get(heap->vector, smallest)) < 0) {\n        smallest = left_child;\n    }\n    if (right_child < vector_size(heap->vector) &&\n        heap->compare(vector_get(heap->vector, right_child), vector_get(heap->vector, smallest)) < 0) {\n        smallest = right_child;\n    }\n    if (smallest != index) {\n        swap(&heap->vector->data[index], &heap->vector->data[smallest]);\n        heapify_down(heap, smallest);\n    }\n}\n\nMinHeap *min_heap_create(size_t element_size, int (*compare)(const void *, const void *), void (*free_function)(void *)) {\n    MinHeap *heap = (MinHeap *)malloc(sizeof(MinHeap));\n    if (!heap) return NULL;\n\n    heap->vector = vector_create(element_size, free_function);\n    if (!heap->vector) {\n        free(heap);\n        return NULL;\n    }\n\n    heap->compare = compare;\n    return heap;\n}\n\nvoid min_heap_destroy(MinHeap *heap) {\n    if (heap) {\n        vector_destroy(heap->vector);\n        free(heap);\n    }\n}\n\nint min_heap_insert(MinHeap *heap, void *element) {\n    if (vector_push_back(heap->vector, element) != 0) return -1;\n    heapify_up(heap, vector_size(heap->vector) - 1);\n    return 0;\n}\n\nvoid *min_heap_extract_min(MinHeap *heap) {\n    if (vector_size(heap->vector) == 0) return NULL;\n\n    void *min_element = vector_get(heap->vector, 0);\n    void *last_element = vector_get(heap->vector, vector_size(heap->vector) - 1);\n    vector_set(heap->vector, 0, last_element);\n    heap->vector->size--; // Adjust size without deallocating the last element\n\n    heapify_down(heap, 0);\n    return min_element;\n}\n\nsize_t min_heap_size(MinHeap *heap) {\n    return vector_size(heap->vector);\n}\n\nvoid min_heap_to_mermaid(MinHeap *heap, FILE *output, void (print_element)(void *element, FILE *output)) {\n    fprintf(output, \"graph TD\\n\");\n    for (size_t i = 0; i < vector_size(heap->vector); i++) {\n        fprintf(output, \"  %zu((\\\"\", i);\n        if (print_element) {\n            print_element(vector_get(heap->vector, i), output);\n        } else {\n            fprintf(output, \"%d\", *(int *)vector_get(heap->vector, i));\n        }\n        fprintf(output, \"\\\"))\\n\");\n    }\n    for (size_t i = 0; i < vector_size(heap->vector); i++) {\n        if (i > 0) {\n            size_t parent = (i - 1) / 2;\n            fprintf(output, \"  %zu --> %zu\\n\", parent, i);\n        }\n    }\n}\n

    Le tas binaire utilise un tableau dynamique pour stocker les \u00e9l\u00e9ments. La r\u00e8gle est que chaque \u00e9l\u00e9ment voit son enfant de gauche \u00e0 l'indice 2 * k + 1 et l'enfant de droite \u00e0 l'indice 2 * k + 2. Le parent d'un \u00e9l\u00e9ment est \u00e0 l'indice (k - 1) / 2 quelque soit l'indice k.

    La propri\u00e9t\u00e9 principale du tas binaire est que chaque element de l'arbre est plus petit que ses enfants. Cela signifie que la racine de l'arbre est le plus petit \u00e9l\u00e9ment. Lorsqu'on retire un \u00e9l\u00e9ment du tas, on retire la racine et on la remplace par le dernier \u00e9l\u00e9ment du tableau il faut ensuite manipuler le tas pour que la propri\u00e9t\u00e9 soit respect\u00e9e. On appelle cette op\u00e9ration heapify.

    L'algorithme heapify est un algorithme r\u00e9cursif qui permet de r\u00e9tablir la propri\u00e9t\u00e9 du tas binaire. On part du dernier \u00e9l\u00e9ment de l'arbre qui poss\u00e8de au moins un enfant. On compare la valeur de l'\u00e9l\u00e9ment avec celle de son ou de ses enfants. Si la valeur de l'\u00e9l\u00e9ment est plus grande que celle de ses enfants, on \u00e9change les valeurs. On continue r\u00e9cursivement avec les enfants jusqu'\u00e0 ce que la propri\u00e9t\u00e9 soit respect\u00e9e.

    Si on insert un \u00e9l\u00e9ment dans le tableau dynamique, on l'ajoute \u00e0 la fin du tableau. Puis on doit r\u00e9tablir la propri\u00e9t\u00e9 du tas binaire. On compare la valeur de l'\u00e9l\u00e9ment avec celle de son parent. Si la valeur de l'\u00e9l\u00e9ment est plus petite que celle de son parent, on \u00e9change les valeurs. On continue r\u00e9cursivement avec le parent jusqu'\u00e0 ce que la propri\u00e9t\u00e9 soit respect\u00e9e, en remontant la branche.

    Prenons l'exemple initial de cet arbre stock\u00e9 en tableau\u2009:

    int a[] = {1, 3, 6, 5, 9, 8};\n
    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 6((6))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    6((6)) --> 8((8))

    On souhaite rajouter l'\u00e9l\u00e9ment 2. On commence par l'ajouter \u00e0 la fin\u2009:

    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 6((6))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    6((6)) --> 8((8))\n    6((6)) --> 2((2))

    On compare la valeur de 2 avec celle de son parent 6. Comme 2 est plus petit que 6, on \u00e9change les valeurs\u2009:

    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 2((2))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    2((2)) --> 8((8))\n    2((2)) --> 6((6))

    On continue avec le parent de 2, 1. Comme 2 est plus grand que 1, on s'arr\u00eate l\u00e0. Le tas binaire est maintenant r\u00e9tabli.

    Les utilisations les plus courantes de cette structure de donn\u00e9e sont\u2009:

    • Tri par tas (Heap sort)
    • Une queue prioritaire (Priority queue)
    • D\u00e9terminer le k-i\u00e8me \u00e9l\u00e9ment le plus petit d'une collection (k-th smallest element)

    Voici un tableau r\u00e9sumant les complexit\u00e9s des diff\u00e9rentes op\u00e9rations dans un tas binaire minimal\u2009:

    Op\u00e9ration Complexit\u00e9 Insertion \\(O(log n)\\) Extraction du minimum \\(O(log n)\\) Acc\u00e8s au minimum \\(O(1)\\) Construction \\(O(n)\\) ou \\(O(n log n)\\) Suppression \\(O(log n)\\) Mise \u00e0 jour d'un \u00e9l\u00e9ment \\(O(log n)\\)
    • Insertion : Lorsqu'un \u00e9l\u00e9ment est ajout\u00e9 au min-heap, il est ajout\u00e9 \u00e0 la fin et le processus de heapify up (ou bubble up) est effectu\u00e9 pour r\u00e9tablir la propri\u00e9t\u00e9 du tas. Ce processus implique de comparer et potentiellement d'\u00e9changer des \u00e9l\u00e9ments \u00e0 chaque niveau de l'arbre, ce qui prend \\(O(log n)\\) dans le pire des cas.

    • Extraction du minimum : L'extraction de l'\u00e9l\u00e9ment minimum implique de retirer la racine du tas (le plus petit \u00e9l\u00e9ment), de placer le dernier \u00e9l\u00e9ment de l'arbre \u00e0 la racine, puis d'effectuer heapify down (ou sift down) pour r\u00e9tablir la propri\u00e9t\u00e9 du tas. Cela prend \\(O(log~n)\\) car il peut n\u00e9cessiter de descendre jusqu'au niveau le plus bas de l'arbre.

    • Acc\u00e8s au minimum : L'acc\u00e8s au minimum est \\(O(1)\\) car l'\u00e9l\u00e9ment minimum est toujours \u00e0 la racine du tas.

    • Construction : La complexit\u00e9 de la construction d'un tas \u00e0 partir d'une liste non tri\u00e9e peut \u00eatre \\(O(n)\\) en utilisant une technique appel\u00e9e heapify (ou build-heap). Cependant, si vous ins\u00e9rez chaque \u00e9l\u00e9ment un par un en utilisant la m\u00e9thode d'insertion standard, la complexit\u00e9 serait \\(O(n~log~n)\\).

    • Suppression : La suppression d'un \u00e9l\u00e9ment (autre que la racine) implique de le remplacer par le dernier \u00e9l\u00e9ment du tas et d'effectuer heapify up ou heapify down selon le cas, ce qui prend O(log~n).

    • Mise \u00e0 jour d'un \u00e9l\u00e9ment : La mise \u00e0 jour d'un \u00e9l\u00e9ment peut n\u00e9cessiter soit heapify up soit heapify down pour r\u00e9tablir la propri\u00e9t\u00e9 du tas, ce qui prend \\(O(log n)\\).

    Ces complexit\u00e9s font des min-heaps une structure de donn\u00e9es efficace pour les files de priorit\u00e9 et les algorithmes n\u00e9cessitant des op\u00e9rations fr\u00e9quentes d'insertion et d'extraction du minimum.

    ", "tags": ["tas-binaire"]}, {"location": "course-c/27-data-structures/trees/#arbre-binaire-de-recherche", "title": "Arbre binaire de recherche", "text": "

    Un arbres binaires de recherche (Binary Search Tree, BST) est un arbre binaire dans lequel chaque n\u0153ud a une valeur et les valeurs des n\u0153uds de l'arbre sont ordonn\u00e9es. Pour chaque n\u0153ud, toutes les valeurs des n\u0153uds du sous-arbre gauche sont inf\u00e9rieures \u00e0 la valeur du n\u0153ud et toutes les valeurs des n\u0153uds du sous-arbre droit sont sup\u00e9rieures \u00e0 la valeur du n\u0153ud.

    L'impl\u00e9mentation d'un arbre binaire est souvent impl\u00e9ment\u00e9e avec une liste cha\u00een\u00e9e comportant deux enfants un left et un right :

    Arbre binaire \u00e9quilibr\u00e9

    Lorsqu'il est \u00e9quilibr\u00e9, un arbre binaire comporte autant d'\u00e9l\u00e9ments \u00e0 gauche qu'\u00e0 droite et lorsqu'il est correctement rempli, la valeur d'un \u00e9l\u00e9ment est toujours\u2009:

    • La valeur de l'enfant de gauche est inf\u00e9rieure \u00e0 celle de son parent
    • La valeur de l'enfant de droite est sup\u00e9rieure \u00e0 celle de son parent

    Cette propri\u00e9t\u00e9 est tr\u00e8s appr\u00e9ci\u00e9e pour rechercher et ins\u00e9rer des donn\u00e9es complexes. Admettons que l'on a un registre patient du type\u2009:

    struct patient {\n    size_t id;\n    char firstname[64];\n    char lastname[64];\n    uint8_t age;\n}\n\ntypedef struct node {\n    struct patient data;\n    struct node* left;\n    struct node* right;\n} Node;\n

    Si l'on cherche le patient num\u00e9ro 612, il suffit de parcourir l'arbre de fa\u00e7on dichotomique\u2009:

    Node* search(Node* node, size_t id)\n{\n    if (node == NULL)\n        return NULL;\n\n    if (node->data.id == id)\n        return node;\n\n    return search(node->data.id > id ? node->left : node->right, id);\n}\n

    L'insertion et la suppression d'\u00e9l\u00e9ments dans un arbre binaire font appel \u00e0 des rotations, puisque les \u00e9l\u00e9ments doivent \u00eatre ins\u00e9r\u00e9s dans le correct ordre et que l'arbre, pour \u00eatre performant, doit toujours \u00eatre \u00e9quilibr\u00e9. Ces rotations sont donc des m\u00e9canismes de r\u00e9\u00e9quilibrage de l'arbre ne sont pas triviaux, mais dont la complexit\u00e9 d'ex\u00e9cution reste simple, et donc performante.

    ", "tags": ["left", "right"]}, {"location": "course-c/27-data-structures/trees/#queue-prioritaire", "title": "Queue prioritaire", "text": "

    Une queue prioritaire ou priority queue, est une queue dans laquelle les \u00e9l\u00e9ments sont trait\u00e9s par ordre de priorit\u00e9. Imaginons des personnalit\u00e9s, toutes atteintes d'une rage de dents et qui font la queue chez un dentiste aux m\u0153urs discutables. Ce dernier ne prendra pas ses patients par ordre d'arriv\u00e9e, mais, par importance aristocratique.

    typedef struct Person {\n   char *name;\n   enum SocialStatus {\n       PEON;\n       WORKER;\n       ENGINEER;\n       DOCTOR;\n       PROFESSOR;\n       PRESIDENT;\n       SUPERHERO;\n   } status;\n} Person;\n\nint main() {\n    ProrityQueue queue;\n    queue_init(queue);\n\n    for(int i = 0; i < 100; i++) {\n       queue_enqueue(queue, (Person) {\n          .name = random_name(),\n          .status = random_status()\n       });\n\n       Person person;\n       queue_dequeue(queue, &person);\n       dentist_heal(person);\n    }\n}\n

    La queue prioritaire dispose donc aussi des m\u00e9thodes enqueue et dequeue mais le dequeue retournera l'\u00e9l\u00e9ment le plus prioritaire de la liste. Ceci se traduit par trier la file d'attente \u00e0 chaque op\u00e9ration enqueue ou dequeue. L'une de ces deux op\u00e9rations pourrait donc avoir une complexit\u00e9 de \\(O(n log n)\\). Heureusement, il existe des m\u00e9thodes de tris performantes si un tableau est d\u00e9j\u00e0 tri\u00e9 et qu'un seul nouvel \u00e9l\u00e9ment y est ajout\u00e9.

    L'impl\u00e9mentation de ce type de structure de donn\u00e9e s'appuie le plus souvent sur un heap, soit construite \u00e0 partir d'un tableau statique, soit un tableau dynamique.

    ", "tags": ["enqueue", "dequeue"]}, {"location": "course-c/27-data-structures/trees/#arbre-avl", "title": "Arbre AVL", "text": "

    Un arbre AVL est un arbre binaire de recherche \u00e9quilibr\u00e9. Il est \u00e9quilibr\u00e9 car la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus un. Cela signifie que la hauteur de l'arbre est en \\(O(log n)\\), ce qui rend les op\u00e9rations de recherche, d'insertion et de suppression en \\(O(log n)\\).

    AVL Tree

    AVL tire son nom de ses inventeurs Adelson-Velsky and Landis. C'est une structure de donn\u00e9es tr\u00e8s utilis\u00e9e en informatique pour impl\u00e9menter des dictionnaires, des bases de donn\u00e9es, des compilateurs, etc.

    Son impl\u00e9mentation compl\u00e8te sort du cadre de ce cours mais il est int\u00e9ressant de comprendre comment il fonctionne. L'arbre AVL est un arbre binaire de recherche o\u00f9 chaque n\u0153ud a un facteur d'\u00e9quilibre qui est la diff\u00e9rence entre la hauteur de son sous-arbre gauche et la hauteur de son sous-arbre droit. Si le facteur d'\u00e9quilibre d'un n\u0153ud est sup\u00e9rieur \u00e0 \\(1\\) ou inf\u00e9rieur \u00e0 \\(-1\\), l'arbre est d\u00e9s\u00e9quilibr\u00e9 et il faut le r\u00e9\u00e9quilibrer. Cela donne un crit\u00e8re de r\u00e9\u00e9quilibrage en fonction du facteur d'\u00e9quilibre.

    L'op\u00e9ration d'insertion dans un arbre AVL est similaire \u00e0 celle d'un arbre binaire de recherche. On ins\u00e8re le n\u0153ud \u00e0 la bonne place dans l'arbre. Puis on met \u00e0 jour le facteur d'\u00e9quilibre de chaque n\u0153ud sur le chemin de la racine. Si le facteur d'\u00e9quilibre d'un n\u0153ud est sup\u00e9rieur \u00e0 \\(1\\) ou inf\u00e9rieur \u00e0 \\(-1\\), on r\u00e9\u00e9quilibre l'arbre en effectuant des rotations.

    C'est cette op\u00e9ration de rotation qui est la plus complexe dans un arbre AVL. Il existe plusieurs types de rotations en fonction du facteur d'\u00e9quilibre du n\u0153ud. Il y a les rotations simples et les rotations doubles. Les rotations simples sont les rotations droite et gauche. Les rotations doubles sont les rotations gauche-droite et droite-gauche.

    "}, {"location": "course-c/27-data-structures/trees/#arbre-rouge-noir", "title": "Arbre rouge-noir", "text": "

    Un arbre rouge-noir est un arbre binaire de recherche \u00e9quilibr\u00e9. Il est \u00e9quilibr\u00e9 car la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus deux. Cela signifie que la hauteur de l'arbre est en \\(O(log n)\\), ce qui rend les op\u00e9rations de recherche, d'insertion et de suppression en \\(O(log n)\\).

    Arbre rouge et noir

    Contrairement \u00e0 l'arbre AVL, l'arbre rouge-noir est plus simple \u00e0 impl\u00e9menter. Il utilise un bit de couleur pour chaque n\u0153ud pour indiquer si le n\u0153ud est rouge ou noir. L'arbre rouge-noir a cinq propri\u00e9t\u00e9s\u2009:

    1. Chaque n\u0153ud est soit rouge, soit noir.
    2. La racine est noire.
    3. Toutes les feuilles (n\u0153uds NULL) sont noires.
    4. Si un n\u0153ud est rouge, alors ses deux enfants sont noirs. (Pas de deux rouges cons\u00e9cutifs sur un chemin vers une feuille)
    5. Tout chemin simple d'un n\u0153ud donn\u00e9 \u00e0 ses feuilles descendantes contient le m\u00eame nombre de n\u0153uds noirs.

    De la m\u00eame mani\u00e8re que l'arbre AVL, il y a des op\u00e9rations de rotation pour r\u00e9\u00e9quilibrer l'arbre rouge-noir. Les rotations sont plus simples que dans un arbre AVL car il n'y a que deux types de rotations\u2009: la rotation gauche et la rotation droite.

    "}, {"location": "course-c/27-data-structures/trees/#trie", "title": "Trie", "text": "

    Un trie est une structure de donn\u00e9es qui stocke un ensemble de cha\u00eenes de caract\u00e8res. Il est souvent utilis\u00e9 pour stocker des mots dans un dictionnaire ou pour rechercher des mots dans un texte. Un trie est donc un arbre o\u00f9 chaque n\u0153ud est associ\u00e9 \u00e0 une lettre et un marqueur de fin de mot. Un noeud peut avoir de 1 \u00e0 26 enfants, un pour chaque lettre de l'alphabet (si on se limite \u00e0 l'alphabet latin minuscule).

    Prenons l'exemple des mots suivants\u2009:

    char *words[] = {\n    \"cadeaux\", \"le\", \"car\", \"cette\", \"cadre\", \"cause\",\n    \"carte\", \"comme\", \"car\", \"ce\", \"caduc\", \"cadet\",\n    \"la\", \"la\", \"les\"};\n

    On peut construire le trie suivant\u2009:

    Trie

    En vert, les n\u0153uds qui marquent la fin d'un mot. En orange la racine de l'arbre. La structure de donn\u00e9es de chaque noeud pourrait \u00eatre la suivante\u2009:

    typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[26];  // Children nodes\n} Node;\n

    Exercice 1\u2009: Impl\u00e9mentation

    Vous avez un texte connu et vous voulez permettre de compter les occurences de chaque mot. Une fois que le trie est construit, il est en lecture seule. Comment allez-vous impl\u00e9menter le trie\u2009?

    • Comme une liste cha\u00een\u00e9e, chaque noeud est allou\u00e9 dynamiquement sur le heap.
    • Un tableau statique sur la pile ou chaque \u00e9l\u00e9ment est un noeud.
    • Un tableau dynamique sur le heap, l'allocation est amortie et chaque noeud contient un tableau de pointeurs sur ses enfants.
    • Un tableau dynamique sur le heap, l'allocation est amortie et chaque noeud contient non pas un pointeur des enfants mais l'indice de l'enfant dans le tableau.
    • Par chunks d'\u00e9l\u00e9ments, chaque chunk est allou\u00e9 dynamiquement sur le heap.

    Discutons de plusieurs impl\u00e9mentations possibles d'un noeud d'un trie\u2009:

    • Liste cha\u00een\u00e9e : Chaque noeud est allou\u00e9 dynamiquement sur le heap. C'est une solution simple mais qui peut \u00eatre co\u00fbteuse en m\u00e9moire et en temps d'allocation. N\u00e9anmoins le noeud peut prendre un tableau flexible pour les enfants. Ce qui permet de ne pas allouer de m\u00e9moire inutile.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[];  // Children nodes, variable size\n} Node;\n
    • Tableau dynamique : En stoquant tous les \u00e9l\u00e9ments dans le tableau dynamique, on ne peut plus utiliser de pointeurs car si le tableau est r\u00e9allou\u00e9, les pointeurs ne sont plus valides. On utilise donc des indices pour acc\u00e9der aux enfants, car ces derniers sont relatifs \u00e0 l'adresse de d\u00e9but du tableau. En revanche, on ne peut plus utiliser de tableau flexible pour les enfants car la taille de la structure doit \u00eatre connue \u00e0 la compilation. Ceci implique une utilisation de m\u00e9moire plus importante.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    size_t children_id[26];  // Children nodes\n} Node;\n
    • Chunks : Chaque chunk contient un certain nombre de noeuds. Un chaunk d'une taille donn\u00e9e est r\u00e9serv\u00e9e. Lorsque le chunk est plein, un nouveau chunk est allou\u00e9. Cela permet de r\u00e9duire le nombre d'appels \u00e0 malloc et de r\u00e9duire la fragmentation de la m\u00e9moire. Cette m\u00e9thode permet de r\u00e9duire le nombre d'appels \u00e0 malloc et de r\u00e9duire la fragmentation de la m\u00e9moire. Elle r\u00e9soud aussi le probl\u00e8me de la taille fixe du tableau des enfants en autorisant \u00e0 nouveau un tableau flexible.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[];  // Children nodes\n} Node;\n\ntypedef struct Chunk {\n    char *data[1024];\n    size_t used_bytes;\n    struct Chunk *next;\n} Chunk;\n

    Exemple d'impl\u00e9mentation\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#define ALPHABET_SIZE 26\n\ntypedef struct Node {\n    int occurrences;\n    struct Node *children[ALPHABET_SIZE];\n} Node;\n\nNode* createNode() {\n    Node *node = (Node *)malloc(sizeof(Node));\n    if (node) {\n        node->occurrences = 0;\n        for (int i = 0; i < ALPHABET_SIZE; i++) {\n            node->children[i] = NULL;\n        }\n    }\n    return node;\n}\n\nvoid insert(Node *root, const char *word) {\n    Node *current = root;\n    while (*word) {\n        if (!current->children[*word - 'a']) {\n            current->children[*word - 'a'] = createNode();\n        }\n        current = current->children[*word - 'a'];\n        word++;\n    }\n    current->occurrences++;\n}\n\nint search(Node *root, const char *word) {\n    Node *current = root;\n    while (*word) {\n        if (!current->children[*word - 'a']) {\n            return 0;\n        }\n        current = current->children[*word - 'a'];\n        word++;\n    }\n    return current->occurrences;\n}\n\nvoid freeTrie(Node *root) {\n    for (int i = 0; i < ALPHABET_SIZE; i++) {\n        if (root->children[i]) {\n            freeTrie(root->children[i]);\n        }\n    }\n    free(root);\n}\n\nint main() {\n    Node *root = createNode();\n\n    insert(root, \"frodo\");\n    insert(root, \"sam\");\n    insert(root, \"gandalf\");\n    // Add more words as needed\n\n    printf(\"Occurrences of 'frodo': %d\\n\", search(root, \"frodo\"));\n    printf(\"Occurrences of 'sam': %d\\n\", search(root, \"sam\"));\n    printf(\"Occurrences of 'gandalf': %d\\n\", search(root, \"gandalf\"));\n\n    freeTrie(root);\n}\n

    Exercice 2\u2009: Regroupement\u2009?

    Demandons-nous s'il ne serait pas pr\u00e9f\u00e9rable de regrouper les noeuds communs ensemble comme le montre la figure suivante\u2009:

    Trie\u2009: arbre avec noeuds communs

    D'apr\u00e8s vous est-ce une bonne id\u00e9e\u2009? Pourquoi\u2009?

    Solution

    Non, ce n'est pas une bonne id\u00e9e. D'une part la figure n'est plus un arbre mais un graphe. Un graph peut avoir des cycles et donc des boucles infinies. Ensuite, regrouper les \u00e9l\u00e9ments communs ne peut \u00eatre fait qu'\u00e0 la fin de la construction du trie, lorsqu'elle est d\u00e9j\u00e0 allou\u00e9e en m\u00e9moire. La complexit\u00e9 de l'optimisation n'est pas \u00e0 n\u00e9gliger. Si la contrainte est l'utilisation de la m\u00e9moire, il est pr\u00e9f\u00e9rable d'utiliser une autre structure de donn\u00e9e comme un radix trie.

    ", "tags": ["malloc"]}, {"location": "course-c/27-data-structures/trees/#radix-trie", "title": "Radix Trie", "text": "

    On l'a vu l'impl\u00e9mentation d'un trie est simple mais elle peut conduire \u00e0 une utilisation excessive de la m\u00e9moire. En effet, chaque noeud contient un tableau de 26 \u00e9l\u00e9ments, m\u00eame si un mot ne contient que quelques lettres. Pour r\u00e9duire la consommation de m\u00e9moire, on peut utiliser un radix trie. Cet arbre est \u00e9galement nomm\u00e9 PATRICIA trie pour Practical Algorithm to Retrieve Information Coded in Alphanumeric.

    Plut\u00f4t que de stocker une seule lettre par noeud, on stocke un pr\u00e9fixe commun \u00e0 plusieurs mots. On peut alors r\u00e9duire le nombre de noeuds et donc la consommation de m\u00e9moire.

    "}, {"location": "course-c/27-data-structures/trees/#navigation-dans-un-arbre", "title": "Navigation dans un arbre", "text": "

    La navigation dans un arbre binaire est une op\u00e9ration courante. Il existe plusieurs fa\u00e7ons de parcourir un arbre binaire\u2009:

    • Parcours en profondeur (DFS pour Depth First Search) On commence par la racine, puis on visite le sous-arbre gauche, puis le sous-arbre droit. On peut faire un parcours en profondeur en pr\u00e9-ordre, en in-ordre ou en post-ordre.
    • Parcours en largeur (BFS pour Breadth First Search) On visite les n\u0153uds de l'arbre de haut en bas et de gauche \u00e0 droite. On utilise une file pour stocker les n\u0153uds \u00e0 visiter.
    "}, {"location": "course-c/27-data-structures/trees/#parcours-en-profondeur", "title": "Parcours en profondeur", "text": "

    Le parcours en profondeur est une m\u00e9thode de parcours d'un arbre binaire qui commence par la racine, puis visite le sous-arbre gauche, puis le sous-arbre droit. Il existe trois fa\u00e7ons de parcourir un arbre en profondeur\u2009:

    • Pr\u00e9-ordre : On visite d'abord la racine, puis le sous-arbre gauche, puis le sous-arbre droit.
    • In-ordre : On visite d'abord le sous-arbre gauche, puis la racine, puis le sous-arbre droit.
    • Post-ordre : On visite d'abord le sous-arbre gauche, puis le sous-arbre droit, puis la racine.

    L'impl\u00e9mentation peut se faire de mani\u00e8re r\u00e9cursive ou it\u00e9rative. Voici un exemple d'impl\u00e9mentation r\u00e9cursive en C\u2009:

    int dfs(Node* node, (void)(*visit)(Node*))\n{\n    if (node == NULL)\n        return;\n\n    visit(node);\n    dfs(node->left, visit);\n    dfs(node->right, visit);\n}\n

    L'impl\u00e9mentation it\u00e9rative utilise une pile pour stocker les n\u0153uds \u00e0 visiter. Voici un exemple d'impl\u00e9mentation it\u00e9rative en C\u2009:

    int dfs(Node* node, (void)(*visit)(Node*))\n{\n    Stack stack;\n    stack_init(stack);\n    stack_push(stack, node);\n\n    while (!stack_empty(stack)) {\n        Node* current = stack_pop(stack);\n        visit(current);\n\n        if (current->right != NULL)\n            stack_push(stack, current->right);\n\n        if (current->left != NULL)\n            stack_push(stack, current->left);\n    }\n}\n
    "}, {"location": "course-c/30-modular-programming/", "title": "Programmation Modulaire", "text": "

    La programmation modulaire est une m\u00e9thode de conception de logiciels qui consiste \u00e0 diviser un programme en modules ind\u00e9pendants. Chaque module est une unit\u00e9 de code qui peut \u00eatre compil\u00e9e s\u00e9par\u00e9ment et r\u00e9utilis\u00e9e dans d'autres programmes. La programmation modulaire permet de simplifier la conception, la maintenance et l'\u00e9volution des logiciels en les divisant en petites parties coh\u00e9rentes et r\u00e9utilisables.

    "}, {"location": "course-c/30-modular-programming/libraries/", "title": "Biblioth\u00e8ques", "text": "

    Biblioth\u00e8que du Trinity College de Dublin

    Une biblioth\u00e8que informatique est une collection de fichiers comportant des fonctionnalit\u00e9s logicielles pr\u00eates \u00e0 l'emploi. La printf est une de ces fonctionnalit\u00e9s et offerte par le header <stdio.h> faisant partie de la biblioth\u00e8que libc6.

    L'anglicisme library, plus court \u00e0 prononcer et \u00e0 \u00e9crire est souvent utilis\u00e9 en lieu et place de biblioth\u00e8que tant il est omnipr\u00e9sent dans le monde logiciel. Le terme <stdlib.h> \u00e9tant la concat\u00e9nation de standard library par exemple. Notez que librairie n'est pas la traduction correcte de library qui est un faux ami.

    Une library, \u00e0 l'instar d'une biblioth\u00e8que, contient du contenu (livre \u00e9crit dans une langue donn\u00e9e) et un index (registre). En informatique il s'agit d'un fichier binaire compil\u00e9 pour une architecture donn\u00e9e ainsi qu'un ou plusieurs fichiers d'en-t\u00eate (header) contenant les d\u00e9finitions de cette biblioth\u00e8que.

    Dans ce chapitre on donnera plusieurs exemples sur un environnement POSIX. Sous Windows, les proc\u00e9dures choses sont plus compliqu\u00e9es, mais les concepts restent les m\u00eames.

    ", "tags": ["printf", "libc6"]}, {"location": "course-c/30-modular-programming/libraries/#exemple-libgmp", "title": "Exemple\u2009: libgmp", "text": "

    Voyons ensemble le cas de libgmp. Il s'agit d'une biblioth\u00e8que de fonctionnalit\u00e9s tr\u00e8s utilis\u00e9e et permettant le calcul arithm\u00e9tique multipr\u00e9cision en C. En observant le d\u00e9tail du paquet logiciel Debian on peut lire que libgmp est disponible pour diff\u00e9rentes architectures amd64, arm64, s390x, i386, ... Un d\u00e9veloppement sur un Raspberry-PI n\u00e9cessitera arm64 alors qu'un d\u00e9veloppement sur un PC utilisera amd64. En cliquant sur l'architecture d\u00e9sir\u00e9e on peut voir que ce paquet se compose des fichiers suivants (list r\u00e9duite aux fichiers concernant C):

    # Fichier d'en-t\u00eate C\n/usr/include/x86_64-linux-gnu/gmp.h\n\n# Biblioth\u00e8que compil\u00e9e pour l'architecture vis\u00e9e (ici amd64)\n/usr/lib/x86_64-linux-gnu/libgmp.a\n/usr/lib/x86_64-linux-gnu/libgmp.so\n\n# Documentation de la libgmp\n/usr/share/doc/libgmp-dev/AUTHORS\n/usr/share/doc/libgmp-dev/README\n/usr/share/doc/libgmp-dev/changelog.gz\n/usr/share/doc/libgmp-dev/copyright\n

    On a donc\u2009:

    gmp.h

    Fichier d'en-t\u00eate \u00e0 include dans un fichier source pour utiliser les fonctionnalit\u00e9s

    libgmp.a

    Biblioth\u00e8que statique qui contient l'impl\u00e9mentation en langage machine des fonctionnalit\u00e9s \u00e0 r\u00e9f\u00e9rer au linker lors de la compilation

    libgmp.so

    Biblioth\u00e8que dynamique qui contient aussi l'impl\u00e9mentation en langage machine des fonctionnalit\u00e9s

    Imaginons que l'on souhaite b\u00e9n\u00e9ficier des fonctionnalit\u00e9s de cette biblioth\u00e8que pour le calcul d'orbites pour un satellite d'observation de Jupyter. Pour prendre en main cet libary on \u00e9crit ceci\u2009:

    #include <gmp.h>\n#include <stdio.h>\n\nint main(void)\n{\n    unsigned int radix = 10;\n    char a[] = \"19810983098510928501928599999999999990\";\n\n    mpz_t n;\n\n    mpz_init(n);\n    mpz_set_ui(n, 0);\n\n    mpz_set_str(n, a, radix);\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_add_ui(n, n, 12); // Addition\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_mul(n, n, n); // Square\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_clear(n);\n}\n

    Puis on compile\u2009:

    $ gcc gmp.c\ngmp.c:1:10: fatal error: gmp.h: No such file or directory\n#include <gmp.h>\n        ^~~~~~~\ncompilation terminated.\n

    A\u00efe\u2009! La biblioth\u00e8que n'est pas install\u00e9e...

    Pour l'installer, cela d\u00e9pend de votre syst\u00e8me d'exploitation\u2009:

    UbuntumacOS
    $ sudo apt-get install libgmp-dev\n
    $ brew install gmp\n

    Deuxi\u00e8me tentative\u2009:

    $ gcc gmp.c\n/tmp/cc2FxDSy.o: In function `main':\ngmp.c:(.text+0x6f): undefined reference to `__gmpz_init'\ngmp.c:(.text+0x80): undefined reference to `__gmpz_set_ui'\ngmp.c:(.text+0x96): undefined reference to `__gmpz_set_str'\ngmp.c:(.text+0xb3): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0xd5): undefined reference to `__gmpz_add_ui'\ngmp.c:(.text+0xf2): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0x113): undefined reference to `__gmpz_mul'\ngmp.c:(.text+0x130): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0x146): undefined reference to `__gmpz_clear'\ncollect2: error: ld returned 1 exit status\n

    Cette fois-ci on peut lire que le compilateur \u00e0 fait sont travail, mais ne parvient pas \u00e0 trouver les symboles des fonctions que l'on utilise p.ex. __gmpz_add_ui. C'est normal parce que l'on n'a pas renseign\u00e9 la biblioth\u00e8que \u00e0 utiliser.

    $ gcc gmp.c -lgmp\n\n$ ./a.out\n19810983098510928501928599999999999990\n19810983098510928501928600000000000002\n392475051329485669436248957939688603493163430354043714007714400000000000004\n

    Cette mani\u00e8re de faire utilise le fichier libgmp.so qui est la biblioth\u00e8que dynamique, c'est-\u00e0-dire que ce fichier est n\u00e9cessaire pour que le programme puisse fonctionner. Si je donne mon ex\u00e9cutable \u00e0 un ami qui n'as pas install libgmp sur son ordinateur, il ne sera pas capable de l'ex\u00e9cuter.

    Alternativement on peut compiler le m\u00eame programme en utilisant la librairie statique

    $ gcc gmp.c /usr/lib/x86_64-linux-gnu/libgmp.a\n

    C'est-\u00e0-dire qu'\u00e0 la compilation toutes les fonctionnalit\u00e9s ont \u00e9t\u00e9 int\u00e9gr\u00e9es \u00e0 l'ex\u00e9cutable et il ne d\u00e9pend de plus rien d'autre que le syst\u00e8me d'exploitation. Je peux prendre ce fichier le donner \u00e0 quelqu'un qui utilise la m\u00eame architecture et il pourra l'ex\u00e9cuter. En revanche, la taille du programme est plus grosse\u2009:

    # ~167 KiB\n$ gcc gmp.c -l:libgmp.a\n$ size a.out\ntext    data     bss     dec     hex filename\n155494     808      56  156358   262c6 ./a.out\n\n# ~8.5 KiB\n$ gcc gmp.c -lgmp\n$ size a.out\ntext    data     bss     dec     hex filename\n2752     680      16    3448     d78 ./a.out\n
    ", "tags": ["__gmpz_add_ui", "i386", "amd64", "arm64", "s390x", "gmp.h", "libgmp.a", "libgmp.so", "libgmp"]}, {"location": "course-c/30-modular-programming/libraries/#exemple-ncurses", "title": "Exemple\u2009: ncurses", "text": "

    La biblioth\u00e8que ncurses traduction de nouvelles mal\u00e9dictions est une \u00e9volution de curses d\u00e9velopp\u00e9 originellement par Ken Arnold . Il s'agit d'une biblioth\u00e8que pour la cr\u00e9ation d'interfaces graphique en ligne de commande, toujours tr\u00e8s utilis\u00e9e.

    La biblioth\u00e8que permet le positionnement arbitraire dans la fen\u00eatre de commande, le dessin de fen\u00eatres, de menus, d'ombrage sous les fen\u00eatres, de couleurs ...

    Exemple d'interface graphique \u00e9crite avec ncurses. Ici la configuration du noyau Linux.

    L'\u00e9criture d'un programme Hello World avec cette biblioth\u00e8que pourrait \u00eatre\u2009:

    #include <ncurses.h>\n\nint main()\n{\n    initscr();              // Start curses mode\n    printw(\"hello, world\"); // Print Hello World\n    refresh();              // Print it on to the real screen\n    getch();                    // Wait for user input\n    endwin();               // End curses mode\n\n    return 0;\n}\n

    La compilation n'est possible que si\u2009:

    1. La biblioth\u00e8que est install\u00e9e sur l'ordinateur
    2. Le lien vers la biblioth\u00e8que dynamique est mentionn\u00e9 \u00e0 la compilation
    $ gcc ncurses-hello.c -ohello -lncurses\n
    ", "tags": ["ncurses"]}, {"location": "course-c/30-modular-programming/libraries/#bibliotheques-statiques", "title": "Biblioth\u00e8ques statiques", "text": "

    Une static library est un fichier binaire compil\u00e9 pour une architecture donn\u00e9e et portant les extensions\u2009:

    • .a sur un syst\u00e8me POSIX (Android, Mac OS, Linux, Unix)
    • .lib sous Windows

    Une biblioth\u00e8que statique n'est rien d'autre qu'une archive d\u2019un ou plusieurs objets. Rappelons-le un objet est le r\u00e9sultat d'une compilation.

    Par exemple si l'on souhaite \u00e9crire une biblioth\u00e8que statique pour le code de C\u00e9sar on \u00e9crira un fichier source caesar.c:

    caesar.c
    void caesar(char str[], unsigned key)\n{\n    key %= 26;\n\n    for (int i = 0; str[i]; i++)\n    {\n        char c = str[i];\n\n        if (c >= 'a' && c <= 'z')\n        {\n            str[i] = ((c + key > 'z') ? c - 'z' + 'a' - 1 : c) + key;\n        }\n        else if (c >= 'A' && c <= 'Z')\n        {\n            str[i] = ((c + key > 'Z') ? c - 'Z' + 'A' - 1 : c) + key;\n        }\n    }\n}\n

    Ainsi qu'un fichier d'en-t\u00eate caesar.h:

    caesar.h
    /**\n * Function that compute the Caesar cipher\n * @param str input string\n * @param key offset to add to each character\n */\nvoid caesar(char str[], unsigned key);\n

    Pour cr\u00e9er une biblioth\u00e8que statique rien de plus facile. Le compilateur cr\u00e9e l'objet, l'archiver cr\u00e9e l'amalgame\u2009:

    $ gcc -c -o caesar.o caesar.c\n$ ar rcs caesar.a caesar.o\n

    Puis il suffit d'\u00e9crire un programme pour utiliser cette biblioth\u00e8que\u2009:

    encrypt.c
    #include <caesar.h>\n#include <stdio.h>\n\n#define KEY 13\n\nint main(int argc, char *argv[])\n{\n    for (int i = 1; i < argc; i++)\n    {\n        caesar(argv[i], KEY);\n        printf(\"%s\\n\", argv[i]);\n    }\n}\n

    Et de compiler le tout. Ici on utilise -I. et -L. pour dire au compilateur de chercher le fichier d'en-t\u00eate et la biblioth\u00e8que dans le r\u00e9pertoire courant.

    $ gcc encrypt.c -I. -L. -l:caesar.a\n

    La proc\u00e9dure sous Windows est plus compliqu\u00e9e et ne sera pas d\u00e9crite ici.

    ", "tags": ["caesar.c", "caesar.h"]}, {"location": "course-c/30-modular-programming/libraries/#bibliotheques-dynamiques", "title": "Biblioth\u00e8ques dynamiques", "text": "

    Une dynamic library est un fichier binaire compil\u00e9 pour une architecture donn\u00e9e et portant les extensions\u2009:

    • .so sur un syst\u00e8me POSIX (Android, Mac OS, Linux, Unix)
    • .dll sous Windows

    L'avantage principal est de ne pas charger pour rien chaque ex\u00e9cutable compil\u00e9 de fonctionnalit\u00e9s qui pourraient tr\u00e8s bien \u00eatre partag\u00e9es. L'inconv\u00e9nient est que l'utilisateur du programme doit imp\u00e9rativement avoir install\u00e9 la biblioth\u00e8que. Dans un environnement POSIX les biblioth\u00e8ques dynamiques disposent d'un emplacement sp\u00e9cifique ou elles sont toute stock\u00e9es. Malheureusement sous Windows le consensus est plus partag\u00e9 et il n'est pas rare de voir plusieurs applications diff\u00e9rentes h\u00e9berger une copie des dll localement si bien que l'avantage de la biblioth\u00e8que dynamique est an\u00e9anti par un d\u00e9faut de coh\u00e9rence.

    Reprenant l'exemple de C\u00e9sar vu plus haut, on peut cr\u00e9er une biblioth\u00e8que dynamique\u2009:

    $ gcc -shared -o libcaesar.so caesar.o\n

    Puis compiler notre programme pour utiliser cette biblioth\u00e8que. Avec une biblioth\u00e8que dynamique, il faut sp\u00e9cifier au compilateur quels sont les chemins vers lesquels il pourra trouver les biblioth\u00e8ques install\u00e9es. Comme ici on ne souhaite pas installer la biblioth\u00e8que et la rendre disponible pour tous les programmes, il faut ajouter aux chemins par d\u00e9faut, le chemin local $(pwd .), en cr\u00e9ant une variable d'environnement nomm\u00e9e LIBRARY_PATH.

    $ LIBRARY_PATH=$(pwd .) gcc encrypt.c -I. -lcaesar\n

    Le probl\u00e8me est identique \u00e0 l'ex\u00e9cution, car il faut sp\u00e9cifier (ici avec LD_LIBRARY_PATH) le chemin ou le syst\u00e8me d'exploitation s'attendra \u00e0 trouver la biblioth\u00e8que.

    $ LD_LIBRARY_PATH=$(pwd .) ./a.out ferrugineux\nsreehtvarhk\n

    Car sinon c'est l'erreur\u2009:

    $ LIBRARY_PATH=$(pwd .) ./a.out Hey?\n./a.out: error while loading shared libraries: libcaesar.so :\ncannot open shared object file: No such file or directory\n
    ", "tags": ["LD_LIBRARY_PATH", "LIBRARY_PATH"]}, {"location": "course-c/30-modular-programming/translation-units/", "title": "Compilation s\u00e9par\u00e9e", "text": ""}, {"location": "course-c/30-modular-programming/translation-units/#unite-de-traduction", "title": "Unit\u00e9 de traduction", "text": "

    En programmation, on appelle translation unit (unit\u00e9 de traduction), un code qui peut \u00eatre compil\u00e9 en un objet sans autre d\u00e9pendance externe. Le plus souvent, une unit\u00e9 de traduction correspond \u00e0 un fichier C.

    "}, {"location": "course-c/30-modular-programming/translation-units/#diviser-pour-mieux-regner", "title": "Diviser pour mieux r\u00e9gner", "text": "

    De m\u00eame qu'un magazine illustr\u00e9 est divis\u00e9 en sections pour accro\u00eetre la lisibilit\u00e9 (sport, news, annonces, m\u00e9t\u00e9o) de m\u00eame un code source est organis\u00e9 en \u00e9l\u00e9ments fonctionnels le plus souvent s\u00e9par\u00e9s en plusieurs fichiers et ces derniers parfois maintenus par diff\u00e9rents d\u00e9veloppeurs.

    Rappelons-le (et c'est tr\u00e8s important) :

    • une fonction ne devrait pas d\u00e9passer un \u00e9cran de haut (~50 lignes) ;
    • un fichier ne devrait pas d\u00e9passer 1000 lignes\u2009;
    • une ligne ne devrait pas d\u00e9passer 80 caract\u00e8res.

    Donc \u00e0 un moment, il est essentiel de diviser son travail en cr\u00e9ant plusieurs fichiers.

    Ainsi, lorsque le programme commence \u00e0 \u00eatre volumineux, sa lecture, sa compr\u00e9hension et sa mise au point deviennent d\u00e9licates m\u00eame pour le plus aguerri des d\u00e9veloppeurs. Il est alors essentiel de scinder le code source en plusieurs fichiers. Prenons l'exemple d'un programme qui effectue des calculs sur les nombres complexes. Notre projet est donc constitu\u00e9 de trois fichiers\u2009:

    $ tree\n.\n\u251c\u2500\u2500 complex.c\n\u251c\u2500\u2500 complex.h\n\u2514\u2500\u2500 main.c\n

    Le programme principal et la fonction main est contenu dans main.c quant au module complex il est compos\u00e9 de deux fichiers\u2009: complex.h l'en-t\u00eate et complex.c, l'impl\u00e9mentation du module.

    Le fichier main.c devra inclure le fichier complex.h afin de pouvoir utiliser correctement les fonctions du module de gestion des nombres complexes. Exemple\u2009:

    // fichier main.c\n#include \"complex.h\"\n\nint main() {\n    Complex c1 = { .real = 1., .imag = -3. };\n    complex_fprint(stdout, c1);\n}\n
    // fichier complex.h\n#ifndef COMPLEX_H\n#define COMPLEX_H\n\n#include <stdio.h>\n\ntypedef struct Complex {\n    double real;\n    double imag;\n} Complex, *pComplex;\n\nvoid complex_fprint(FILE *fp, const Complex c);\n\n#endif // COMPLEX_H\n
    // fichier complex.c\n#include \"complex.h\"\n\nvoid complex_fprint(FILE* fp, const Complex c) {\n    fprintf(fp, \"%+.3lf + %+.3lf\\n\", c.real, c.imag);\n}\n

    Un des avantages majeurs \u00e0 la cr\u00e9ation de modules est qu'un module logiciel peut \u00eatre r\u00e9utilis\u00e9 pour d'autres applications. Plus besoin de r\u00e9inventer la roue \u00e0 chaque application\u2009!

    Cet exemple sera compil\u00e9 dans un environnement POSIX de la fa\u00e7on suivante\u2009:

    gcc -c complex.c -o complex.o\ngcc -c main.c -o main.o\ngcc complex.o main.o -oprogram -lm\n

    Nous verrons plus bas les \u00e9l\u00e9ments th\u00e9oriques vous permettant de mieux comprendre ces lignes.

    ", "tags": ["main", "main.c", "complex.h", "complex.c"]}, {"location": "course-c/30-modular-programming/translation-units/#module-logiciel", "title": "Module logiciel", "text": "

    Les applications modernes d\u00e9pendent souvent de nombreux modules logiciels externes aussi utilis\u00e9s dans d'autres projets. C'est avantageux \u00e0 plus d'un titre\u2009:

    • les modules externes sont sous la responsabilit\u00e9 d'autres d\u00e9veloppeurs et le programme a d\u00e9velopper comporte moins de code\u2009;
    • les modules externes sont souvent bien document\u00e9s et test\u00e9s et il est facile de les utiliser\u2009;
    • la lisibilit\u00e9 du programme est accrue, car il est bien d\u00e9coup\u00e9 en des ensembles fonctionnels\u2009;
    • les modules externes sont r\u00e9utilisables et ind\u00e9pendants, ils peuvent donc \u00eatre r\u00e9utilis\u00e9s sur plusieurs projets.

    Lorsque vous utilisez la fonction printf, vous d\u00e9pendez d'un module externe nomm\u00e9 stdio. En r\u00e9alit\u00e9 l'ensemble des modules stdio, stdlib, stdint, ctype... sont tous group\u00e9s dans une seule biblioth\u00e8que logicielle nomm\u00e9e libc disponible sur tous les syst\u00e8mes compatibles POSIX. Sous Linux, le pendant libre glibc est utilis\u00e9. Il s'agit de la biblioth\u00e8que GNU C Library.

    Un module logiciel peut se composer de fichiers sources, c'est-\u00e0-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\u00e8ques d\u00e9j\u00e0 compil\u00e9es sous la forme de fichiers .h, .a et .so. Sous Windows on rencontre fr\u00e9quemment l'extension .dll. Ces fichiers compil\u00e9s ne donnent pas acc\u00e8s au code source, mais permettent d'utiliser les fonctionnalit\u00e9s quelles offrent dans des programmes C en mettant \u00e0 disposition un ensemble de fonctions document\u00e9es.

    ", "tags": ["stdint", "glibc", "Makefile", "printf", "ctype", "libc", "stdio", "stdlib"]}, {"location": "course-c/30-modular-programming/translation-units/#compilation-avec-assemblage-differe", "title": "Compilation avec assemblage diff\u00e9r\u00e9", "text": "

    Lorsque nous avions compil\u00e9 notre premier exemple Hello World nous avions simplement appel\u00e9 gcc avec le fichier source hello.c qui nous avait cr\u00e9\u00e9 un ex\u00e9cutable a.out. En r\u00e9alit\u00e9, GCC est pass\u00e9 par plusieurs sous-\u00e9tapes de compilation\u2009:

    1. Pr\u00e9processing : les commentaires sont retir\u00e9s, les directives pr\u00e9processeur sont remplac\u00e9es par leur \u00e9quivalent C.
    2. Compilation : le code C d'une seule translation unit est converti en langage machine en un fichier objet .o.
    3. \u00c9dition des liens : aussi nomm\u00e9s link, les diff\u00e9rents fichiers objets sont r\u00e9unis en un seul ex\u00e9cutable.

    Lorsqu'un seul fichier est fourni \u00e0 GCC, les trois op\u00e9rations sont effectu\u00e9es en m\u00eame temps, mais ce n'est plus possible aussit\u00f4t que le programme est compos\u00e9 de plusieurs unit\u00e9s de translation (plusieurs fichiers C). Il est alors n\u00e9cessaire de compiler manuellement chaque fichier source et d'en cr\u00e9er.

    La figure suivante r\u00e9sume les diff\u00e9rentes \u00e9tapes de GCC. Les pointill\u00e9s indiquent \u00e0 quel niveau les op\u00e9rations peuvent s'arr\u00eater. Il est d\u00e8s lors possible de passer par des fichiers interm\u00e9diaires assembleur (.s) ou objets (.o) en utilisant la bonne commande.

    \u00c9tapes interm\u00e9diaires de compilation avec GCC

    Notons que ces \u00e9tapes existent, quel que soit le compilateur ou le syst\u00e8me d'exploitation. Nous retrouverons ces exactes m\u00eames \u00e9tapes 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).

    Notons que g\u00e9n\u00e9ralement, seul deux \u00e9tapes de GCC sont utilis\u00e9es\u2009:

    1. Compilation avec gcc -c <fichier.c>, ceci g\u00e9n\u00e8re automatiquement un fichier .o du m\u00eame nom que le fichier d'entr\u00e9e.
    2. \u00c9dition des liens avec gcc <fichier1.o> <fichier2.o> ..., ceci g\u00e9n\u00e8re automatiquement un fichier ex\u00e9cutable a.out.
    ", "tags": ["hello.c", "a.out", "gcc"]}, {"location": "course-c/30-modular-programming/translation-units/#fichiers-den-tete-header", "title": "Fichiers d'en-t\u00eate (header)", "text": "

    Les fichiers d'en-t\u00eate (.h) sont des fichiers \u00e9crits en langage C, mais qui ne contiennent pas d'impl\u00e9mentation de fonctions. Un tel fichier ne contient donc pas de while, de for ou m\u00eame de if. Par convention, ces fichiers ne contiennent que\u2009:

    • Des prototypes de fonctions (ou de variables).
    • Des d\u00e9clarations de types (typedef, struct).
    • Des d\u00e9finitions pr\u00e9processeur (#include, #define).

    Nous l'avons vu dans le chapitre sur le pr\u00e9processeur, la directive #include ne fais qu'inclure le contenu du fichier cible \u00e0 l'emplacement de la directive. Il est donc possible (mais fort d\u00e9conseill\u00e9), d'avoir la situation suivante\u2009:

    main.c
    int main() {\n   #include \"foobar.def\"\n}\n

    Et le fichier foobar.def pourrait contenir\u2009:

    foobar.def
    #ifdef FOO\nprintf(\"hello foo!\\n\");\n#else\nprintf(\"hello bar!\\n\");\n#endif\n

    Vous noterez que l'extension de foobar n'est pas .h puisque le contenu n'est pas un fichier d'en-t\u00eate. .def ou n'importe quelle autre extension pourrait donc faire l'affaire ici.

    Dans cet exemple, le pr\u00e9processeur ne fait qu'inclure le contenu du fichier foobar.def \u00e0 l'emplacement de la d\u00e9finition #include \"foobar.def\". Voyons-le en d\u00e9tail\u2009:

    $ cat << EOF > main.c\nint main() {\n    #include \"foobar.def\"\n    #include \"foobar.def\"\n}\nEOF\n\n$ cat << EOF > foobar.def\n#ifdef FOO\nprintf(\"hello foo!\\n\");\n#else\nprintf(\"hello bar!\\n\");\n#endif\nEOF\n\n$ gcc -E main.c | sed '/^#/ d'\nint main() {\nprintf(\"hello bar\\n\");\nprintf(\"hello bar\\n\");\n}\n

    Lorsque l'on observe le r\u00e9sultat du pr\u00e9processeur, on s'aper\u00e7oit que toutes les directives pr\u00e9processeur ont disparues et que la directive #include a \u00e9t\u00e9 remplac\u00e9e par de contenu de foobar.def. Remarquons que le fichier est inclus deux fois, nous verrons plus loin comme \u00e9viter cela.

    Nous avons vu au chapitre sur les prototypes de fonctions qu'il est possible de ne d\u00e9clarer que la premi\u00e8re ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est compos\u00e9 une fonction sans n\u00e9cessairement disposer de l'impl\u00e9mentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-t\u00eate des d\u00e9clarations en amont (forward declaration). Dans le fichier d'en-t\u00eate stdio.h on trouvera la ligne\u2009: int printf( const char *restrict format, ... );.

    $ cat << EOF > main.c\n\u2192 #include <stdio.h>\n\u2192 int main() { }\n\u2192 EOF\n$ gcc -E main.c | grep -P '\\bprintf\\b'\nextern int printf (const char *__restrict __format, ...);\n

    Notons qu'ici le prototype est pr\u00e9c\u00e9d\u00e9 par le mot cl\u00e9 extern. Il s'agit d'un mot cl\u00e9 optionnel permettant de renforcer l'intention du d\u00e9veloppeur que la fonction d\u00e9clar\u00e9e n'est pas inclue dans fichier courant, mais qu'elle est impl\u00e9ment\u00e9e ailleurs, dans un autre fichier. Et c'est le cas, car printf est d\u00e9j\u00e0 compil\u00e9e quelque part dans la biblioth\u00e8que libc inclue par d\u00e9faut lorsqu'un programme C est compil\u00e9 dans un environnement POSIX.

    Un fichier d'en-t\u00eate contiendra donc tout le n\u00e9cessaire utile \u00e0 pouvoir utiliser une biblioth\u00e8que externe.

    ", "tags": ["while", "struct", "foobar", "for", "stdio.h", "extern", "printf", "typedef", "libc", "foobar.def"]}, {"location": "course-c/30-modular-programming/translation-units/#protection-de-reentrance", "title": "Protection de r\u00e9entrance", "text": "

    La protection de r\u00e9entrence aussi nomm\u00e9e header guards est une solution au probl\u00e8me d'inclusion multiple. Si par exemple on d\u00e9finit dans un fichier d'en-t\u00eate un nouveau type et que l'on inclut ce fichier, mais que ce dernier est d\u00e9j\u00e0 inclus par une autre biblioth\u00e8que, une erreur de compilation appara\u00eetra\u2009:

    $ cat << EOF > main.c\n\u2192 #include \"foo.h\"\n\u2192 #include \"bar.h\"\n\u2192 int main() {\n\u2192    Bar bar = {0};\n\u2192    foo(bar);\n\u2192 }\n\u2192 EOF\n\n$ cat << EOF > foo.h\n\u2192 #include \"bar.h\"\n\u2192\n\u2192 extern void foo(Bar);\n\u2192 EOF\n\n$ cat << EOF > bar.h\n\u2192 typedef struct Bar {\n\u2192    int b, a, r;\n\u2192 } Bar;\n\u2192 EOF\n\n$ gcc main.c\nIn file included from main.c:2:0 :\nbar.h:1:16: error: redefinition of \u2018struct Bar\u2019\ntypedef struct Bar {\n                ^~~\nIn file included from foo.h:1:0,\n                from main.c:1 :\nbar.h:1:16: note: originally defined here\ntypedef struct Bar {\n                ^~~\nIn file included from main.c:2:0 :\nbar.h:3:3: error: conflicting types for \u2018Bar\u2019\n} Bar;\n^~~\n...\n

    Dans cet exemple l'utilisateur ne sait pas forc\u00e9ment que bar.h est d\u00e9j\u00e0 inclus avec foo.h et le r\u00e9sultat apr\u00e8s pr\u00e9-processing est le suivant\u2009:

    $ gcc -E main.c | sed '/^#/ d'\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n\nextern void foo(Bar);\ntypedef struct Bar {\nint b, a, r;\n} Bar;\nint main() {\nBar bar = {0};\nfoo(bar);\n}\n

    On y retrouve la d\u00e9finition de Bar deux fois et donc, le compilateur g\u00e9n\u00e8re une erreur.

    Une solution \u00e0 ce probl\u00e8me est d'ajouter des gardes d'inclusion multiple par exemple avec ceci\u2009:

    #ifndef BAR_H\n#define BAR_H\n\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n\n#endif // BAR_H\n

    Si aucune d\u00e9finition du type #define BAR_H n'existe, alors le fichier bar.h n'a jamais \u00e9t\u00e9 inclus auparavant et le contenu de la directive #ifndef BAR_H dans lequel on commence par d\u00e9finir BAR_H est ex\u00e9cut\u00e9. Lors d'une future inclusion de bar.h, la valeur de BAR_H aura d\u00e9j\u00e0 \u00e9t\u00e9 d\u00e9finie et le contenu de la directive #ifndef BAR_H ne sera jamais ex\u00e9cut\u00e9.

    Alternativement, il existe une solution non standard, mais support\u00e9e par la plupart des compilateurs. Elle fait intervenir un pragma\u2009:

    #pragma once\n\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n

    Cette solution est \u00e9quivalente \u00e0 la m\u00e9thode traditionnelle et pr\u00e9sente plusieurs avantages. C'est tout d'abord une solution atomique qui ne n\u00e9cessite pas un #endif \u00e0 la fin du fichier. Il n'y a ensuite pas de conflit avec la r\u00e8gle SSOT, car le nom du fichier bar.h n'appara\u00eet pas dans le fichier BAR_H.

    ", "tags": ["Bar", "bar.h", "BAR_H", "foo.h"]}, {"location": "course-c/30-modular-programming/translation-units/#en-profondeur", "title": "En profondeur", "text": "

    Pour mieux comprendre la compilation s\u00e9par\u00e9e, tentons d'observer le code assembleur g\u00e9n\u00e9r\u00e9. Consid\u00e9rons le fichier foo.c :

    int bar(int);\n\nint foo(int a) {\n    return bar(a) + 42;\n}\n

    Puisqu'il ne contient pas de fonction main, il n'est pas possible de compiler ce fichier en un ex\u00e9cutable car il manque un point d'entr\u00e9e\u2009:

    gcc foo.c\n/usr/bin/ld: /usr/lib/x86_64-linux-gnu/Scrt1.o: in function '_start':\n(.text+0x24): undefined reference to 'main'\ncollect2: error: ld returned 1 exit status\n

    Le linker se termine avec une erreur\u2009: r\u00e9f\u00e9rence \u00e0 'main' inexistante.

    En revanche, il est possible de compiler un objet, c'est \u00e0 dire g\u00e9n\u00e9rer les instructions assembleur. La fonction bar \u00e9tant manquante, le compilateur suppose qu'elle existe quelque part en m\u00e9moire et se contentera de dire moi j'appelle cette fonction ou qu'elle se trouve.

    $objdump -d foo.o\n\nfoo.o:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000000000 <foo>:\n 0:   f3 0f 1e fa       endbr64\n 4:   55                push   %rbp\n 5:   48 89 e5          mov    %rsp,%rbp\n 8:   48 83 ec 10       sub    $0x10,%rsp\n c:   89 7d fc          mov    %edi,-0x4(%rbp)\n f:   8b 45 fc          mov    -0x4(%rbp),%eax\n12:   89 c7             mov    %eax,%edi\n14:   e8 00 00 00 00    callq  19 <foo+0x19>\n19:   83 c0 2a          add    $0x2a,%eax\n1c:   c9                leaveq\n1d:   c3                retq\n

    On constate \u00e0 la ligne 19 que l'addition \u00e0 bien lieu eax + 42, et que l'appel de la fonction bar se produit \u00e0 la ligne 14.

    Maintenant, consid\u00e9rons le programme principal\u2009:

    #include <stdio.h>\n\nint foo(int);\n\nint bar(int a) {\n    return a * 2;\n}\n\nint main() {\n    printf(\"%d\", foo(42));\n}\n

    En g\u00e9n\u00e9rant l'objet gcc -c main.c, on peut \u00e9galement afficher l'assembleur g\u00e9n\u00e9r\u00e9 avec objdump :

    $objdump -d main.o\n\nmain.o:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000000000 <bar>:\n 0:   f3 0f 1e fa             endbr64\n 4:   55                      push   %rbp\n 5:   48 89 e5                mov    %rsp,%rbp\n 8:   89 7d fc                mov    %edi,-0x4(%rbp)\n b:   8b 45 fc                mov    -0x4(%rbp),%eax\n e:   01 c0                   add    %eax,%eax\n10:   5d                      pop    %rbp\n11:   c3                      retq\n\n0000000000000012 <main>:\n12:   f3 0f 1e fa             endbr64\n16:   55                      push   %rbp\n17:   48 89 e5                mov    %rsp,%rbp\n1a:   bf 2a 00 00 00          mov    $0x2a,%edi\n1f:   e8 00 00 00 00          callq  24 <main+0x12>\n24:   89 c6                   mov    %eax,%esi\n26:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi\n2d:   b8 00 00 00 00          mov    $0x0,%eax\n32:   e8 00 00 00 00          callq  37 <main+0x25>\n37:   b8 00 00 00 00          mov    $0x0,%eax\n3c:   5d                      pop    %rbp\n3d:   c3                      retq\n

    On observe l'appel de la fonction foo \u00e0 la ligne 1f et l'appel de printf \u00e0 la ligne 32.

    L'assemblage de ces deux fichiers en un ex\u00e9cutable r\u00e9sout les liens en modifiant les adresses d'appel des fonctions puisqu'elles sont maintenant connues (notons que certaines lignes ont \u00e9t\u00e9 retir\u00e9es pour plus de lisibilit\u00e9) :

    $ gcc foo.o main.o\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001149 <foo>:\n    1149:       f3 0f 1e fa             endbr64\n    114d:       55                      push   %rbp\n    114e:       48 89 e5                mov    %rsp,%rbp\n    1151:       48 83 ec 10             sub    $0x10,%rsp\n    1155:       89 7d fc                mov    %edi,-0x4(%rbp)\n    1158:       8b 45 fc                mov    -0x4(%rbp),%eax\n    115b:       89 c7                   mov    %eax,%edi\n    115d:       e8 05 00 00 00          callq  1167 <bar>\n    1162:       83 c0 2a                add    $0x2a,%eax\n    1165:       c9                      leaveq\n    1166:       c3                      retq\n\n0000000000001167 <bar>:\n    1167:       f3 0f 1e fa             endbr64\n    116b:       55                      push   %rbp\n    116c:       48 89 e5                mov    %rsp,%rbp\n    116f:       89 7d fc                mov    %edi,-0x4(%rbp)\n    1172:       8b 45 fc                mov    -0x4(%rbp),%eax\n    1175:       01 c0                   add    %eax,%eax\n    1177:       5d                      pop    %rbp\n    1178:       c3                      retq\n\n0000000000001179 <main>:\n    1179:       f3 0f 1e fa             endbr64\n    117d:       55                      push   %rbp\n    117e:       48 89 e5                mov    %rsp,%rbp\n    1181:       bf 2a 00 00 00          mov    $0x2a,%edi\n    1186:       e8 be ff ff ff          callq  1149 <foo>\n    118b:       89 c6                   mov    %eax,%esi\n    118d:       48 8d 3d 70 0e 00 00    lea    0xe70(%rip),%rdi\n    1194:       b8 00 00 00 00          mov    $0x0,%eax\n    1199:       e8 b2 fe ff ff          callq  1050 <printf@plt>\n    119e:       b8 00 00 00 00          mov    $0x0,%eax\n    11a3:       5d                      pop    %rbp\n    11a4:       c3                      retq\n    11a5:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    11ac:       00 00 00\n    11af:       90                      nop\n

    On constate que les appels de fonctions ont \u00e9t\u00e9 bien remplac\u00e9s par les bons noms\u2009:

    • 115d Appel de bar
    • 1186 Appel de foo
    • 1199 Appel de printf
    ", "tags": ["objdump", "foo", "bar", "foo.c", "printf"]}, {"location": "course-c/35-libraries/introduction/", "title": "Biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques sont des collections de fonctions et de types de donn\u00e9es qui peuvent \u00eatre utilis\u00e9es dans des programmes. Elles sont g\u00e9n\u00e9ralement fournies par le syst\u00e8me d'exploitation ou par des tiers. Les biblioth\u00e8ques sont utilis\u00e9es pour \u00e9tendre les fonctionnalit\u00e9s d'un programme sans avoir \u00e0 r\u00e9\u00e9crire le code \u00e0 partir de z\u00e9ro.

    "}, {"location": "course-c/35-libraries/standard-library/", "title": "Biblioth\u00e8ques standard", "text": "

    Aux premi\u00e8res heures de l'informatique (ann\u00e9es 1950 et 1960), les programmeurs \u00e9crivaient du code tr\u00e8s sp\u00e9cifique \u00e0 la machine, g\u00e9n\u00e9ralement en langage assembleur. Il n'y avait pas de biblioth\u00e8ques standard ou de frameworks, et les programmeurs devaient souvent \u00e9crire eux-m\u00eames des fonctionnalit\u00e9s de base comme la gestion des entr\u00e9es/sorties ou les op\u00e9rations math\u00e9matiques. L'id\u00e9e de r\u00e9utilisabilit\u00e9 de code \u00e9tait encore peu d\u00e9velopp\u00e9e. Les langages \u00e9taient souvent con\u00e7us pour une seule machine, ce qui limitait les possibilit\u00e9s de portabilit\u00e9.

    Le langage C est l'un des premiers langages \u00e0 introduire une biblioth\u00e8que standard appel\u00e9e C standard library (libc). Cette biblioth\u00e8que visait \u00e0 fournir un ensemble de fonctions de base pour faciliter le d\u00e9veloppement d'applications. Elle contenait des fonctions pour la gestion des cha\u00eenes de caract\u00e8res, des fichiers, de la m\u00e9moire, des maths, etc. Avant cela, ces fonctionnalit\u00e9s devaient \u00eatre \u00e9crites par chaque programmeur pour chaque projet. L'ajout de cette biblioth\u00e8que standard a permis de simplifier consid\u00e9rablement le d\u00e9veloppement en \u00e9vitant de r\u00e9inventer la roue pour chaque projet.

    Java, lanc\u00e9 par Sun Microsystems, a introduit une biblioth\u00e8que standard extr\u00eamement riche d\u00e8s sa premi\u00e8re version, connue sous le nom de Java Standard Library. Elle couvrait un large \u00e9ventail de domaines (gestion des entr\u00e9es/sorties, interfaces graphiques, r\u00e9seau, etc.). Le fait que Java soit livr\u00e9 avec une biblioth\u00e8que compl\u00e8te et uniforme a jou\u00e9 un r\u00f4le crucial dans sa popularit\u00e9. Java a \u00e9galement introduit des frameworks comme Swing pour les interfaces graphiques et a encourag\u00e9 l'utilisation d'APIs standardis\u00e9es. Python, cr\u00e9\u00e9 par Guido van Rossum en 1991, a aussi adopt\u00e9 tr\u00e8s t\u00f4t l'id\u00e9e d'une biblioth\u00e8que standard compl\u00e8te, appel\u00e9e Python Standard Library. Python est souvent lou\u00e9 pour son approche \u00ab\u2009batteries included\u2009\u00bb (\u00ab\u2009batteries incluses\u2009\u00bb), signifiant que le langage fournit une vaste gamme d'outils pr\u00eats \u00e0 l'emploi. Cela a fait de Python un langage tr\u00e8s populaire pour le d\u00e9veloppement rapide d'applications.

    Aujourd'hui, pratiquement tous les langages modernes (comme Rust, Go, Swift, Kotlin) sont livr\u00e9s avec des biblioth\u00e8ques standard \u00e9tendues, ainsi que des frameworks et des outils de gestion de paquets (comme npm pour JavaScript, pip pour Python, cargo pour Rust, etc.). Ces outils permettent aux d\u00e9veloppeurs d\u2019acc\u00e9der \u00e0 des milliers de biblioth\u00e8ques tierces et \u00e0 des frameworks qui simplifient la construction d\u2019applications complexes.

    En C, la situation n'a pas beaucoup \u00e9volu\u00e9e depuis les ann\u00e9es 70. Le langage C est un langage de bas niveau qui ne fournit toujours pas de biblioth\u00e8que standard \u00e9tendue. Si une des raison est la portabilit\u00e9 des programmes, une autre raison est que le langage C est un langage minimaliste. Il a \u00e9t\u00e9 con\u00e7u pour \u00eatre simple et efficace, et les concepteurs ont d\u00e9lib\u00e9r\u00e9ment choisi de ne pas inclure de fonctionnalit\u00e9s avanc\u00e9es dans le langage lui-m\u00eame. Il existe donc en C une seule biblioth\u00e8que la libc qui souffre de quelques lacunes et incoh\u00e9rences par le fait de son anciennet\u00e9 et de la n\u00e9cessit\u00e9 de conserver la compatibilit\u00e9 avec les anciennes versions.

    La libc reste n\u00e9anmoins un outil indispensable pour le d\u00e9veloppeur C. Nous allons voir dans ce chapitre les diff\u00e9rents fichiers d'en-t\u00eate et fonctions qu'elle propose en montrant quelques exemples d'utilisation.

    En-t\u00eates standard En-t\u00eate Description Standard <assert.h> Validation des pr\u00e9requis C89 <complex.h> Nombres complexes C99 <ctype.h> Tests C89 <errno.h> Gestion des erreurs C89 <fenv.h> Environnement de calcul flottant C99 <float.h> Constantes de pr\u00e9cision des types flottants C89 <inttypes.h> Types entiers format\u00e9s C99 <iso646.h> Alternative aux op\u00e9rateurs (and, or) C95 <limits.h> Limites des types entiers C89 <locale.h> Gestion des locales C89 <math.h> Fonctions math\u00e9matiques C89 <setjmp.h> Gestion des sauts C89 <signal.h> Gestion des signaux C89 <stdalign.h> Alignement des types C11 <stdarg.h> Arguments variables C89 <stdatomic.h> Op\u00e9rations atomiques C11 <stdbit.h> Macros pour les bits C23 <stdbool.h> Type bool\u00e9en C99 <stdckdint.h> Macros de tests pour les entiers C23 <stddef.h> Macros standard C89 <stdint.h> Types entiers standard C99 <stdio.h> Entr\u00e9es/sorties standard C89 <stdlib.h> Allocation dynamique C89 <stdnoreturn.h> Fonctions sans retour C11 <string.h> Manipulation des cha\u00eenes de caract\u00e8res C89 <tgmath.h> Fonctions math\u00e9matiques g\u00e9n\u00e9riques C99 <threads.h> Gestion des threads C11 <time.h> Date et heure C89 <uchar.h> Caract\u00e8res Unicode C11 <wchar.h> Caract\u00e8res larges C95 <wctype.h> Tests larges C95

    "}, {"location": "course-c/35-libraries/standard-library/#asserth", "title": "<assert.h>", "text": "

    On peut bien se demander \u00e0 quoi sert un en-t\u00eate <assert.h> qui ne contient qu'une seule fonction. La fonction assert est une fonction tr\u00e8s utile pour valider des pr\u00e9requis. Elle s'utilise principalement pour du d\u00e9bogage mais parfois pour s'assurer qu'une expression qui \u00e0 priori ne devrait jamais valoir false est bien vraie. L'en-t\u00eate offre deux prototypes qui sont en r\u00e9alit\u00e9 des macros\u2009:

    int assert(int expression);\nstatic_assert(int expression, \"message\");\n

    L'utilisation de assert permet de d\u00e9tecter les erreurs pendant la phase de d\u00e9veloppement ou de test. Si une condition critique n'est pas respect\u00e9e (par exemple, un pointeur nul ou une division par z\u00e9ro), le programme s'arr\u00eate avec une information pr\u00e9cieuse pour le d\u00e9bogage.

    #include <assert.h>\n#include <memory.h>\nint main() {\n    void *p = malloc(10);\n    assert(p != NULL);\n    ...\n}\n

    La grande force d'assert est qu'elle peut \u00eatre d\u00e9sactiv\u00e9e dans un environnement de production en d\u00e9finissant la macro NDEBUG. Lorsque NDEBUG est d\u00e9fini, toutes les assertions sont remplac\u00e9es par des expressions nulles (ne font rien), ce qui \u00e9limine toute surcharge due aux v\u00e9rifications. D'une fa\u00e7on simplifi\u00e9e, NDEBUG pourrait \u00eatre impl\u00e9ment\u00e9 comme ceci\u2009:

    #ifdef NDEBUG\n    #define assert(ignore) ((void)0)\n#else\n    #define assert(expr) \\\n        ((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__, __func__))\n#endif\n

    Si vous souhaitez d\u00e9sactiver les assertions, vous pouvez aussi le faire en ajoutant -DNDEBUG \u00e0 la ligne de commande du compilateur. Par exemple\u2009:

    gcc -DNDEBUG -o foo main.c\n

    Avant l'en-t\u00eate

    Il est important de d\u00e9clarer NDEBUG avant d'inclure l'en-t\u00eate <assert.h>. En effet, l'en-t\u00eate <assert.h> va d\u00e9finir la macro assert qui sera utilis\u00e9e dans le code. Si NDEBUG est d\u00e9fini apr\u00e8s l'inclusion de l'en-t\u00eate, la macro assert ne sera pas correctement d\u00e9finie.

    ", "tags": ["assert", "false", "NDEBUG"]}, {"location": "course-c/35-libraries/standard-library/#errnoh", "title": "<errno.h>", "text": "

    La biblioth\u00e8que <errno.h> est utilis\u00e9e pour g\u00e9rer les erreurs. Elle d\u00e9finit une variable globale errno qui est un entier qui contient le code de l'erreur modifi\u00e9 par certaines fonctions de la biblioth\u00e8que standard.

    Des macros sont \u00e9galement d\u00e9finies selon le standard POSIX pour les codes d'erreurs. Par exemple, EACCES pour une erreur d'acc\u00e8s, ENOENT pour un fichier ou r\u00e9pertoire inexistant, ENOMEM pour une erreur d'allocation m\u00e9moire, etc. La liste \u00e9tant relativement longue elle peut \u00eatre consult\u00e9e directmenet sur le standard POSIX. N\u00e9anmoins voici les erreurs les plus courantes\u2009:

    Codes d'erreurs POSIX les plus courants Code Description EACCES Permission refus\u00e9e (p.ex. sur un fichier) EADDRINUSE Adresse d\u00e9j\u00e0 utilis\u00e9e (socket) ECONNREFUSED Connexion refus\u00e9e (socket) EDOM Erreur de domaine math\u00e9matique EEXIST Fichier existe d\u00e9j\u00e0 EINVAL Argument invalide ENOENT Fichier ou r\u00e9pertoire inexistant ENOMEM Pas assez de m\u00e9moire disponible ENOSPC Plus d'espace disponible sur le p\u00e9riph\u00e9rique

    Par exemple, lors du calcul du logarithme d'un nombre n\u00e9gatif, la fonction log va d\u00e9finir errno \u00e0 EDOM pour indiquer une erreur de domaine. Il est possible de r\u00e9initialiser errno \u00e0 z\u00e9ro en utilisant la fonction clearerr ou errno = 0.

    #include <stdio.h>\n#include <math.h>\n#include <errno.h>\n#include <string.h>\n\nint main(void)\n{\n    errno = 0;\n    printf(\"log(-1.0) = %f\\n\", log(-1.0)); // Domain error\n    printf(\"%s\\n\\n\",strerror(errno));\n\n    errno = 0;\n    printf(\"log(0.0)  = %f\\n\", log(0.0));\n    printf(\"%s\\n\",strerror(errno)); // Numerical result out of range\n}\n

    ", "tags": ["ECONNREFUSED", "EADDRINUSE", "ENOENT", "EEXIST", "clearerr", "EACCES", "EDOM", "EINVAL", "log", "ENOSPC", "errno", "ENOMEM"]}, {"location": "course-c/35-libraries/standard-library/#mathh", "title": "<math.h>", "text": "

    La biblioth\u00e8que math\u00e9matique est une des plus utilis\u00e9es. Elle contient des fonctions pour les op\u00e9rations math\u00e9matiques de base. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement. Le fichier d'en-t\u00eate est le suivant et le flag de compilation est -lm.

    Constantes math\u00e9matiques Constantes Description M_PI Valeur de \\(\\pi\\) M_E Valeur de \\(e\\) M_SQRT1_2 Valeur de \\(1/\\sqrt(2)\\)

    Windows

    Attention, ces constantes ne sont pas d\u00e9finies par le standard C, mais par le standard POSIX. Il est donc possible que certaines impl\u00e9mentations ne les d\u00e9finissent pas, en particulier sous Windows.

    Fonctions math\u00e9matiques Fonction Description exp(x) Exponentielle \\(e^x\\) ldexp(x,n) Exposant d'un nombre flottant \\(x\\cdot2^n\\) log(x) Logarithme binaire \\(\\log_{2}(x)\\) log10(x) Logarithme d\u00e9cimal \\(\\log_{10}(x)\\) pow(x,y) Puissance \\(x^y\\) sqrt(x) Racine carr\u00e9e \\(\\sqrt(x)\\) cbrt(x) Racine cubique \\(\\sqrt[3](x)\\) hypot(x,y) Hypot\u00e9nuse optimis\u00e9 \\(\\sqrt(x^2 + y^2)\\) ceil Arrondi \u00e0 l'entier sup\u00e9rieur floor Arrondi \u00e0 l'entier inf\u00e9rieur

    Notons par exemple que la fonction hypot peut tr\u00e8s bien \u00eatre \u00e9mul\u00e9e facilement en utilisant la fonction sqrt. N\u00e9anmoins elle existe pour deux raisons \u00e9l\u00e9mentaires\u2009:

    1. \u00c9viter les d\u00e9passements (overflow).
    2. Une meilleure optimisation du code.

    Souvent, les processeurs sont \u00e9quip\u00e9s de coprocesseurs arithm\u00e9tiques capables de calculer certaines fonctions plus rapidement.

    Le standard C99 a introduit l'en-t\u00eate <tgmath.h> qui donne acc\u00e8s \u00e0 des fonctions g\u00e9n\u00e9riques. Par exemple, sin peut \u00eatre utilis\u00e9 pour des float, double et long double sans avoir \u00e0 choisir le nom de la fonction (sinf, sin, sinl), en outre les types complexes sont \u00e9galement support\u00e9s comme csin pour les complexes.

    ", "tags": ["floor", "sinf", "sqrt", "float", "M_E", "M_PI", "hypot", "sin", "sinl", "csin", "ceil", "M_SQRT1_2", "double"]}, {"location": "course-c/35-libraries/standard-library/#fenvh", "title": "<fenv.h>", "text": "

    La biblioth\u00e8que <fenv.h> est \u00e9troitement li\u00e9e aux calculs math\u00e9matique et permet de manipuler l'environnement de calcul flottant. Elle permet de contr\u00f4ler les modes de calculs, les exceptions et les arrondis. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement.

    La structure fenv_t contient l'\u00e9tat de l'environnement de calcul flottant et la structure fexcept_t contient les exceptions de calcul flottant. Ces structures sont opaques et ne doivent pas \u00eatre manipul\u00e9es directement, elles d\u00e9pendent de l'impl\u00e9mentation et peuvent varier d'un syst\u00e8me \u00e0 l'autre. N\u00e9anmoins pour X86, voici \u00e0 quoi elles pourraient ressembler pour les curieux.

    typedef struct {\n    // Mot de contr\u00f4le de la FPU.\n    union {\n        unsigned int __control_word;\n        struct {\n            // Masque des exceptions de la FPU\n            unsigned int IM : 1;  // Invalid Operation\n            unsigned int DM : 1;  // Denormalized Operand\n            unsigned int ZM : 1;  // Zero-Divide\n            unsigned int OM : 1;  // Overflow\n            unsigned int UM : 1;  // Underflow\n            unsigned int PM : 1;  // Precision\n            unsigned int _Reserved : 2;\n            // Gestion de l'arrondi et de la pr\u00e9cision\n            unsigned int PC : 2;  // Precision Control\n            unsigned int RC : 2;  // Rounding Control mode\n            unsigned int _FPUReserved : 3;\n            unsigned int IC : 1;  // Plus utilis\u00e9\n        };\n    };\n\n    // Word de statut de la FPU (indicateurs d'\u00e9tat et d'exception)\n    union {\n        unsigned int __status_word;\n        struct {\n            // Indicateurs d'exception\n            unsigned int IE : 1;  // Invalid Operation Exception\n            unsigned int DE : 1;  // Denormalized Operand Exception\n            unsigned int ZE : 1;  // Zero-Divide Exception\n            unsigned int OE : 1;  // Overflow Exception\n            unsigned int UE : 1;  // Underflow Exception\n            unsigned int PE : 1;  // Precision Exception\n            unsigned int SF : 1;  // Stack Fault\n            unsigned int ES : 1;  // Exception Summary Status\n\n            // Indicateurs d'\u00e9tat pour le stockage de valeurs\n            // interm\u00e9diaires durant les calculs\n            unsigned int C0 : 1;\n            unsigned int C1 : 1;\n            unsigned int C2 : 1;\n            unsigned int Top : 3;  // Position du sommet de la pile\n            unsigned int C3 : 1;\n            unsigned int Busy : 1;  // FPU occup\u00e9e\n        };\n    };\n    unsigned int __tag_word;\n    unsigned int __fpu_ip; // Instruction pointer\n    unsigned int __fpu_cs; // Code segment\n    unsigned int __opcode; // Opcode de l'op\u00e9ration en cours\n    unsigned int __fpu_dp; // Data pointer\n    unsigned int __mxcsr; // Registre MXCSR (contr\u00f4le des exceptions SSE)\n    unsigned int __mxcsr_mask; // Masque de contr\u00f4le MXCSR\n} fenv_t;\n
    ", "tags": ["double", "float", "fexcept_t", "fenv_t"]}, {"location": "course-c/35-libraries/standard-library/#controle-des-exceptions", "title": "Contr\u00f4le des exceptions", "text": "

    Il est possible de g\u00e9rer les exceptions de calculs flottants comme\u2009:

    • la division par z\u00e9ro,
    • le d\u00e9passement de capacit\u00e9 (overflow),
    • un r\u00e9sultat non num\u00e9rique (NaN),
    • un sous-d\u00e9passement de capacit\u00e9 (underflow),
    • une perte de pr\u00e9cision.

    Dans certains programmes, notamment ceux impliquant des calculs num\u00e9riques intensifs ou critiques (comme en science ou en ing\u00e9nierie), il est important de savoir si une op\u00e9ration en virgule flottante a \u00e9chou\u00e9 ou produit un r\u00e9sultat incorrect.

    #include <stdio.h>\n#include <fenv.h>\n#pragma STDC FENV_ACCESS ON  // NECESSAIRE\n\nint main() {\n    feclearexcept(FE_ALL_EXCEPT); // Efface anciennes exceptions\n\n    double result = 1.0 / 0.0; // Division par z\u00e9ro\n\n    if (fetestexcept(FE_DIVBYZERO)) {\n        printf(\"Erreur : division par z\u00e9ro d\u00e9tect\u00e9e\\n\");\n    }\n}\n
    "}, {"location": "course-c/35-libraries/standard-library/#controle-de-larrondi", "title": "Contr\u00f4le de l'arrondi", "text": "

    Il est aussi possible contr\u00f4ler la mani\u00e8re dont les r\u00e9sultats des op\u00e9rations en virgule flottante sont arrondis. Par d\u00e9faut, les op\u00e9rations en virgule flottante arrondissent au plus proche, mais vous pouvez modifier ce comportement pour arrondir vers z\u00e9ro, vers l'infini, ou vers moins l'infini.

    Nous avions vu pr\u00e9c\u00e9demment que l'arrondi d'un nombre est compliqu\u00e9. La norme IEEE 754 d\u00e9finit plusieurs modes d'arrondis. La fonction fesetround permet de d\u00e9finir le mode d'arrondi. Les modes possibles sont donn\u00e9s par la table suivante\u2009:

    Modes d'arrondis Mode Description \\(-3.5\\) $-2.5 $2.5 3.5$ FE_TONEAREST Arrondi bancaire \\(-4\\) \\(-2\\) \\(2\\) \\(4\\) FE_DOWNWARD Arrondi vers z\u00e9ro \\(-4\\) \\(-3\\) \\(2\\) \\(3\\) FE_UPWARD Arrondi vers l'infini \\(-3\\) \\(-2\\) \\(3\\) \\(4\\) FE_TOWARDZERO Arrondi vers moins l'infini \\(-3\\) \\(-2\\) \\(2\\) \\(3\\) round() Comparaison avec round \\(-4\\) \\(-3\\) \\(3\\) \\(4\\)
    fesetround(FE_TONEAREST);\nint rounded = nearbyint(3.5);\n

    L'arrondi bancaire minimise les biais d'arrondi lorsqu'on fait des calculs sur de grandes quantit\u00e9s de donn\u00e9es. En arrondissant vers l'entier pair dans les cas o\u00f9 un nombre tombe exactement \u00e0 mi-chemin entre deux entiers, cette m\u00e9thode r\u00e9duit l'accumulation d'erreurs statistiques qui peuvent survenir avec d'autres m\u00e9thodes d'arrondi. En effet les valeurs sont arrondies vers l'entier pair le plus proche. Voici quelques exemples\u2009:

    -1.5 -> -2\n-0.5 ->  0\n 0.5 ->  0\n 1.5 ->  2\n 2.5 ->  2\n 3.5 ->  4\n

    round

    La configuration du mode d'arrondi avec fesetround affecte les fonctions nearbyint, rint mais pas round.

    La fonction round utilise un arrondi sp\u00e9cifique appel\u00e9 round half away from zero qui arrondit les valeurs \u00e0 l'entier le plus proche en s'\u00e9loignant de z\u00e9ro.

    Notez que la diff\u00e9rence entre rint et nearbyint est que nearbyint ne g\u00e9n\u00e8re pas d'exception en cas de d\u00e9passement de capacit\u00e9 (overflow).

    ", "tags": ["rint", "FE_TONEAREST", "fesetround", "nearbyint", "round", "FE_UPWARD", "FE_TOWARDZERO", "FE_DOWNWARD"]}, {"location": "course-c/35-libraries/standard-library/#floath", "title": "<float.h>", "text": "

    La biblioth\u00e8que <float.h> contient des constantes qui d\u00e9finissent la pr\u00e9cision des types flottants sur l'architecture cible. Les constantes sont d\u00e9finies pour les types float, double et long double.

    On y retrouve FLT_ROUNDS qui indique le mode d'arrondi par d\u00e9faut utilis\u00e9 \u00e0 la compilation.

    Dans IEEE 754, l'exposant est de base 2, c'est ce qu'on appelle le radix. Il peut \u00eatre contr\u00f4l\u00e9 avec la macro FLT_RADIX. Les constantes DBL_DIG et LDBL_DIG indiquent le nombre de chiffres significatifs que l'on peut stocker dans un double et un long double respectivement.

    Autre base\u2009?

    Aujourd'hui quasiment 100 pour cent des ordinateurs utilisent le radix 2. N\u00e9anmoins, \u00e0 une certaine \u00e9poque le radix 10 \u00e9tait utilis\u00e9 sur certaines architectures comme le l'IBM 650 (1953).

    La norme IEEE 754-2008 permet d'utiliser le radix 16, 10 ou 2. Elle d\u00e9fini notament la reps\u00e9entation DFP (Decimal Floating Point) qui permet de repr\u00e9senter les nombres d\u00e9cimaux de mani\u00e8re exacte. Cependant l'impl\u00e9mentation physique d'une FPU en radix 10 est plus complexe et moins performante c'est pour cela que la vaste majorit\u00e9 des processeurs utilisent le radix 2 suffisant pour la plupart des applications.

    ", "tags": ["float", "DBL_DIG", "LDBL_DIG", "FLT_RADIX", "double", "FLT_ROUNDS"]}, {"location": "course-c/35-libraries/standard-library/#complexh", "title": "<complex.h>", "text": "

    La biblioth\u00e8que <complex.h> permet de manipuler les nombres complexes. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement.

    Un nombre complexe est d\u00e9fini par une partie r\u00e9elle et une partie imaginaire. Le type _Complex est un type de base, mais il peut \u00eatre utilis\u00e9 de mani\u00e8re plus simple avec la macro complex. Pour d\u00e9finir un nombre complexe, on utilise la notation a + bi o\u00f9 a est la partie r\u00e9elle et b la partie imaginaire.

    #include <complex.h>\n\nint main() {\n    double complex z = 1.0 + 2.0 * I;\n    double complex w = 3.0 + 4.0 * I;\n    double complex sum = z + w;\n    printf(\"Somme : %f + %fi\\n\", creal(sum), cimag(sum));\n\n    // Nombre imaginaire pur\n    double imaginary = -2.0 * I;\n}\n

    La constante I est bien entendue d\u00e9finie comme 1.0i. Toutes les fonctions complexes se d\u00e9clines en trois versions\u2009:

    Variantes de type complexe Type Exemple de fonction float cabsf, csinf double cabs, csin long double cabsl, csinl Fonctions complexes Fonction Description cabs(z) Module cacos(z) Arc cosinus cacosh(z) Arc cosinus hyperbolique carg(z) Argument \\(\\arg(z)\\) casin(z) Arc sinus casinh(z) Arc sinus hyperbolique catan(z) Arc tangente catanh(z) Arc tangente hyperbolique ccos(z) Cosinus ccosh(z) Cosinus hyperbolique cexp(z) Exponentielle \\(e^z\\) cimag(z) Partie imaginaire \\(\\Im z\\) clog(z) Logarithme \\(\\log(z)\\) conj(z) Conjugaison \\(\\bar(z)\\) cpow(z,w) Puissance \\(z^w\\) creal(z) Partie r\u00e9elle \\(\\Re z\\) csin(z) Sinus csinh(z) Sinus hyperbolique csqrt(z) Racine carr\u00e9e \\(\\sqrt{z}\\) ctan(z) Tangente ctanh(z) Tangente hyperbolique

    Certaines extensions pr\u00e9vue possiblement avec C23 am\u00e8nerait des fonctionnalit\u00e9s suppl\u00e9mentaires telles que cexp2, clog2, cexp10, clog10, crootn ...

    ", "tags": ["cabsl", "float", "cexp10", "cexp2", "clog2", "crootn", "csinl", "csinf", "_Complex", "csin", "complex", "double", "clog10", "cabsf", "cabs"]}, {"location": "course-c/35-libraries/standard-library/#iso646h", "title": "<iso646.h>", "text": "

    L'en-t\u00eate <iso646.h> est une extension du standard C95 qui d\u00e9finit des alternatives aux op\u00e9rateurs logiques. Les op\u00e9rateurs logiques sont d\u00e9finis avec des symbols (&&, ||, !) mais pour des raisons de lisibilit\u00e9, il est possible de les d\u00e9finir en anglais (and, or, not).

    Macros de l'ISO/IEC 646 Op\u00e9rateur Macro Description && and ET logique \\|\\| or OU logique ! not NON logique != not_eq Diff\u00e9rent de &= and_eq ET binaire \\|= or_eq OU binaire ^= xor_eq OU exclusif binaire ^ xor OU exclusif binaire ~ compl Compl\u00e9ment binaire

    Ces macros sont utiles pour les personnes qui ne peuvent pas taper certains caract\u00e8res sp\u00e9ciaux sur leur clavier. Elles sont \u00e9galement utiles pour les personnes qui veulent rendre leur code plus lisible. N\u00e9anmoins, elles ne sont pas tr\u00e8s utilis\u00e9es en pratique.

    #include <iso646.h>\n\nint foo(int a, int b, int c) {\n    return a and b or not c;\n}\n

    Je vous recommande personnellement de ne pas utiliser ces macros. Elles ne sont pas tr\u00e8s utilis\u00e9es et peuvent rendre le code moins lisible pour les autres d\u00e9veloppeurs.

    ", "tags": ["and", "xor", "compl", "not", "xor_eq", "or_eq", "not_eq", "and_eq"]}, {"location": "course-c/35-libraries/standard-library/#limitsh", "title": "<limits.h>", "text": "

    La biblioth\u00e8que <limits.h> contient des constantes qui d\u00e9finissent les limites des types entiers de base. Les constantes sont d\u00e9finies pour les types char, short, int, long, long long et float, double, long double.

    Limites des entiers de base Constante Description LP64 CHAR_BIT Nombre de bits dans un char 8 CHAR_MAX Valeur maximale d'un char 127 CHAR_MIN Valeur minimale d'un char -128 SCHAR_MAX Valeur maximale d'un signed char 127 SCHAR_MIN Valeur minimale d'un signed char -128 UCHAR_MAX Valeur maximale d'un unsigned char 255 SHRT_MAX Valeur maximale d'un short 32767 SHRT_MIN Valeur minimale d'un short -32768 USHRT_MAX Valeur maximale d'un unsigned short 65535 INT_MAX Valeur maximale d'un int 2147483647 INT_MIN Valeur minimale d'un int -2147483648 UINT_MAX Valeur maximale d'un unsigned int 4294967295 LONG_MAX Valeur maximale d'un long 9223372036854775807 LONG_MIN Valeur minimale d'un long -9223372036854775808 ULONG_MAX Valeur maximale d'un unsigned long 18446744073709551615 LLONG_MAX Valeur maximale d'un long long 9223372036854775807 LLONG_MIN Valeur minimale d'un long long -9223372036854775808 ULLONG_MAX Valeur maximale d'un unsigned long long 18446744073709551615

    ", "tags": ["ULONG_MAX", "int", "INT_MIN", "CHAR_BIT", "CHAR_MAX", "SHRT_MIN", "SHRT_MAX", "long", "double", "LLONG_MIN", "char", "ULLONG_MAX", "LONG_MAX", "UCHAR_MAX", "LONG_MIN", "SCHAR_MAX", "short", "USHRT_MAX", "LLONG_MAX", "SCHAR_MIN", "UINT_MAX", "float", "INT_MAX", "CHAR_MIN"]}, {"location": "course-c/35-libraries/standard-library/#localeh", "title": "<locale.h>", "text": "

    En jargon informatique, la locale est un ensemble de param\u00e8tres qui d\u00e9finissent les conventions culturelles d'une r\u00e9gion. Cela inclut la langue, le format de date, le format de nombre, etc. La biblioth\u00e8que <locale.h> permet de manipuler ces param\u00e8tres.

    Un syst\u00e8me d'exploitation d\u00e9fini g\u00e9n\u00e9ralement une locale par d\u00e9faut. Par exemple, un syst\u00e8me en fran\u00e7ais utilisera la locale fr_FR. Le premier param\u00e8tre est la langue de la locale et le second et le pays. Il existe en effet des diff\u00e9rences entre le fran\u00e7ais suisse fr_CH et le fran\u00e7ais canadien fr_CA.

    Ces conventions sont d\u00e9finie par la norme ISO 15897 et font de surcro\u00eet partie du standard POSIX.

    L'en-t\u00eate <locale.h> contient donc des fonctions pour manipuler les locales.

    Contenu de locale.h Fonction Description setlocale D\u00e9finit la locale localeconv R\u00e9cup\u00e8re les param\u00e8tres de la locale lconv Structure retourn\u00e9e par localeconv

    Voici un exemple\u2009:

    #include <locale.h>\n#include <stdio.h>\n\nint main() {\n    setlocale(LC_ALL, \"fr_FR\");\n    struct lconv *locale = localeconv();\n    printf(\"S\u00e9parateur de milliers : %s\\n\", locale->thousands_sep);\n    printf(\"S\u00e9parateur d\u00e9cimal : %s\\n\", locale->decimal_point);\n}\n

    La structure lconv est d\u00e9finie comme suit\u2009:

    struct lconv {\n    char *decimal_point;  // S\u00e9parateur d\u00e9cimal\n    char *thousands_sep;  // S\u00e9parateur de milliers\n    char *grouping;       // Taille des groupes de milliers\n    char *int_curr_symbol; // Symbole de la monnaie\n    char *currency_symbol; // Symbole de la monnaie\n    char *mon_decimal_point; // S\u00e9parateur d\u00e9cimal de la monnaie\n    char *mon_thousands_sep; // S\u00e9parateur de milliers de la monnaie\n    char *mon_grouping;      // Taille des groupes de milliers de la monnaie\n    char *positive_sign;     // Signe positif\n    char *negative_sign;     // Signe n\u00e9gatif\n    char int_frac_digits;    // D\u00e9cimales pour la monnaie\n    char frac_digits;        // D\u00e9cimales\n    char p_cs_precedes;      // Symbole avant la monnaie\n    char p_sep_by_space;     // Espace entre la monnaie et le montant\n    char n_cs_precedes;      // Symbole avant la monnaie n\u00e9gative\n    char n_sep_by_space;     // Espace entre la monnaie n\u00e9gative et le montant\n    char p_sign_posn;        // Position du signe positif\n    char n_sign_posn;        // Position du signe n\u00e9gatif\n};\n

    Voici un exemple plus complet\u2009:

    #include <locale.h>\n\nint main() {\n    char country[] = \"EN\";\n    printf(\"Vous parlez fran\u00e7ais, tant mieux. Quel est votre pays ? \");\n    if (scanf(\"%2s\", &country) != 1) {\n        printf(\"Erreur de saisie\\n\");\n        return 1;\n    }\n    for (int i = 0; country[i]; i++) country[i] = toupper(country[i]);\n\n    char locale[6];\n    snprintf(locale, 6, \"fr_%s\", country);\n\n    setlocale(LC_ALL, locale);\n\n    float apple_unit_price;\n    printf(\"Quel est le prix d'une pomme ? \");\n    if (scanf(\"%f\", &apple_unit_price) != 1) {\n        printf(\"Erreur de saisie\\n\");\n        return 1;\n    }\n    const int quantity = 10;\n    const float ten_apples_price = apple_unit_price * quantity;\n\n    printf(\"Le prix de %d pommes est de %.2f %s\\n\",\n        quantity, ten_apples_price, localeconv()->currency_symbol);\n}\n

    Avec la fonction setlocale, il est possible de ne d\u00e9finir qu'une partie des conventions \u00e0 utiliser. Par exemple, si on ne veut changer que le format de date, on peut utiliser setlocale(LC_TIME, \"fr_FR\").

    Cat\u00e9gories de locales Cat\u00e9gorie Description LC_ALL Toutes les cat\u00e9gories LC_COLLATE Comparaison de cha\u00eenes LC_CTYPE Caract\u00e8res et conversions LC_MONETARY Format mon\u00e9taire LC_NUMERIC Format num\u00e9rique LC_TIME Format de date et heure

    ", "tags": ["fr_FR", "LC_CTYPE", "LC_NUMERIC", "fr_CH", "fr_CA", "lconv", "LC_TIME", "LC_MONETARY", "LC_ALL", "localeconv", "setlocale", "LC_COLLATE"]}, {"location": "course-c/35-libraries/standard-library/#setjmph", "title": "<setjmp.h>", "text": "

    La biblioth\u00e8que <setjmp.h> permet de g\u00e9rer les exceptions en C. Elle fournit deux fonctions setjmp et longjmp qui permettent de sauvegarder l'\u00e9tat du programme et de le restaurer \u00e0 un point donn\u00e9.

    En pratique il est tr\u00e8s rare d'utiliser ces fonctions, elles sont aussi dangereuses que les goto et peuvent rendre le code difficile \u00e0 lire et \u00e0 maintenir. N\u00e9anmoins dans des cas tr\u00e8s sp\u00e9cifiques, elles peuvent s'av\u00e9rer tr\u00e8s utiles, notament pour simuler des exceptions avec des directives pr\u00e9processeur.

    Nous avons vu que le compilateur utilise la pile pour stocker les variables locales et le contexte d'appel des fonctions. Dans chaque frame de la pile, on trouve l'adresse de retour permettant de continuer l'ex\u00e9cution d'une fonction dans la fonction appelante une fois la fonction courrante termin\u00e9e. Ceci permet de communiquer hi\u00e9rarchiquement entre les fonctions. Il n'est pas possible par exemple de remonter \u00e0 la fonction main depuis une fonction baz appel\u00e9e par bar appel\u00e9e par foo, appel\u00e9e par main.

    Ce n'est pas possible... sauf si on triche un peu. La fonction setjmp permet de sauvegarder l'\u00e9tat du programme \u00e0 un point donn\u00e9. C'est-\u00e0-dire que si on sauve le contexte de main dans un espace m\u00e9moire s\u00e9par\u00e9 avant la cha\u00eene d'appel de fonctions enfants, on pourrait manipuler le stack pour revenir \u00e0 main depuis baz. C'est tr\u00e8s exactement ce que fait setjmp.

    La fonction setjmp prend un seul argument qui est une structure de type jmp_buf. Cette structure est opaque car elle d\u00e9pend du compilateur et de la mani\u00e8re dont le stack est impl\u00e9ment\u00e9. N\u00e9anmoins sur une architecture x86, elle pourrait ressembler \u00e0 ceci\u2009:

    typedef struct {\n    unsigned int __eip; // Instruction pointer\n    unsigned int __esp; // Stack pointer\n    unsigned int __ebp; // Base pointer\n    unsigned int __ebx; // Base register\n    unsigned int __esi; // Source index\n    unsigned int __edi; // Destination index\n} jmp_buf[1];\n

    Il s'agit des registres du processeur concern\u00e9s par la gestion du stack et le d\u00e9roulement du programme. Le registre eip par exemple est le pointeur d'instruction. Il contient l'adresse de la prochaine instruction \u00e0 ex\u00e9cuter. Si ce registre est modifi\u00e9, le programme saute \u00e0 une autre adresse. C'est ce que fait entre autre le goto, il modifie eip pour sauter \u00e0 une autre adresse.

    Pour marquer un point de retour, on utilise setjmp qui sauvegarde l'\u00e9tat du programme dans la structure jmp_buf. On peut ensuite revenir \u00e0 ce point avec longjmp. La fonction longjmp prend deux arguments, la structure jmp_buf et une valeur de retour qui peut \u00eatre utilis\u00e9e pour savoir pourquoi on est revenu \u00e0 ce point. Voici un exemple\u2009:

    #include <setjmp.h>\n\njmp_buf env; // Doit \u00eatre transverse \u00e0 toutes les fonctions, donc globale\n\nvoid foo() { longjmp(env, 42); }\nvoid bar() { foo(); }\nvoid baz() { bar(); }\nint main() {\n    int ret = setjmp(env); // Sauvegarde l'\u00e9tat du programme\n    if (ret == 0) {\n        printf(\"Premi\u00e8re ex\u00e9cution\\n\");\n        baz();\n    } else {\n        printf(\"Retour \u00e0 setjmp avec %d\\n\", ret);\n    }\n}\n

    Lors de l'appel de setjmp, la fonction retourne 0. Cette valeur peut \u00eatre utilis\u00e9e pour tester si c'est la premi\u00e8re fois que la fonction est appel\u00e9e ou si c'est un retour de longjmp. Dans ce cas, la fonction retourne la valeur pass\u00e9e \u00e0 longjmp.

    ", "tags": ["jmp_buf", "eip", "goto", "longjmp", "main", "bar", "foo", "baz", "setjmp"]}, {"location": "course-c/35-libraries/standard-library/#signalh", "title": "<signal.h>", "text": "

    Les signaux sont des m\u00e9canismes sp\u00e9cifiques aux syst\u00e8mes d'exploitations qui permettent de communiquer entre les processus (programmes) et le noyau. Un signal ne v\u00e9hicule pas de donn\u00e9es, il permet simplement de r\u00e9veiller un processus pour lui indiquer qu'un \u00e9v\u00e9nement s'est produit. Alternativement un signal peut \u00eatre \u00e9mis par un processus pour demander au noyau de r\u00e9aliser une action.

    Les signaux sont fondamentaux au sein d'un OS et de ce fait ils sont standardis\u00e9s par POSIX. Windows utilise \u00e9galement des signaux mais ils diff\u00e8rent un peu. En C, la biblioth\u00e8que <signal.h> permet de manipuler les signaux de mani\u00e8re portable avec quelques nuances. Voici les types de signaux les plus courants\u2009:

    Signaux POSIX (liste non exhaustive) Signal Description SIGABRT Abandon du processus SIGALRM Alarme horloge SIGFPE Erreur de calcul flottant SIGILL Instruction ill\u00e9gale SIGINT Interruption depuis le clavier SIGKILL Arr\u00eat forc\u00e9 du processus SIGPIPE \u00c9criture dans un tube sans lecteur SIGSEGV Violation de segmentation SIGTERM Demande d'arr\u00eat du processus SIGUSR1 Signal utilisateur 1 SIGUSR2 Signal utilisateur 2

    La fonction abort() disponible dans <stdlib.h> envoie un signal SIGABRT pour arr\u00eater le programme. Une violation d'acc\u00e8s \u00e0 un espace m\u00e9moire non allou\u00e9 envoie un signal SIGSEGV (la fameuse erreur de segmentation). Un d\u00e9passement de capacit\u00e9 en virgule flottante envoie un signal SIGFPE.

    Pour envoyer un signal \u00e0 un processus depuis le terminal, on utilise la commande kill. Par exemple, pour envoyer un signal SIGUSR1 au processus 1234, on utilise la commande kill -SIGUSR1 1234. Le 1234 est le PID (Process ID) du processus car chaque programme qui s'ex\u00e9cute a un identifiant unique attribu\u00e9 par le syst\u00e8me d'exploitation. Le nom de la commande kill peut \u00eatre trompeur car elle n'arr\u00eate pas le processus, elle lui envoie un signal. Le nom vient historiquement du fait que le signal par d\u00e9faut est SIGTERM qui demande au processus de s'arr\u00eater.

    Dans un programme C il est possible de capturer les signaux re\u00e7us en installant des handlers. Il s'agit d'une fonction qui sera appel\u00e9e de mani\u00e8re \u00e9v\u00e8nementielle lorsqu'un signal est re\u00e7u. Ces handlers court-circuitent donc le comportement normal du programme en interrompant l'action en cours. Voici un exemple pour capturer le signal SIGINT qui est envoy\u00e9 lorsqu'on appuie sur Ctrl+C pour interrompre un programme depuis le terminal\u2009:

    #include <signal.h>\n#include <stdio.h>\n\nvoid sigint_handler(int signum) {\n    printf(\"Vous partez d\u00e9j\u00e0 ? :(\\n\");\n    exit(0);\n}\n\nvoid jeudredi() {\n    char fruits[] = {\"banane\", \"kiwi\", \"ananas\", \"mangue\", \"cerise\"};\n    int i = 0;\n    while(1) {\n        printf(\"Il est tr\u00e8s bon ce coktail \u00e0 la %s !\\n\", fruits[i++ % 5]);\n        sleep(1); // Pause d'une seconde\n    }\n}\n\nint main() {\n    signal(SIGINT, sigint_handler);\n    jeudredi();\n}\n

    ", "tags": ["kill", "SIGFPE", "SIGABRT", "SIGUSR2", "SIGKILL", "SIGILL", "SIGUSR1", "SIGALRM", "SIGTERM", "SIGINT", "SIGPIPE", "SIGSEGV"]}, {"location": "course-c/35-libraries/standard-library/#stdalignh", "title": "<stdalign.h>", "text": "

    La biblioth\u00e8que <stdalign.h> fournit des fonctions pour manipuler l'alignement des donn\u00e9es en m\u00e9moire. L'alignement est une notion importante en informatique car les processeurs sont plus efficaces lorsqu'ils acc\u00e8dent \u00e0 des donn\u00e9es align\u00e9es. Imaginez un camion qui transporte des palettes de marchandises. La logistique est faite de mani\u00e8re \u00e0 ce que les palettes soient facile \u00e0 charger et d\u00e9charger du camion avec un minimum de manutention. Imaginez maintenant que vous voulez prendre un \u00e9l\u00e9ment d'une palette. Cela demande plus de travail parce que vous devez extraire l'\u00e9l\u00e9ment et trouver un autre outil pour le transporter. Un ordinateur 64-bits sur une architecture x86 a beaucoup de faciliter \u00e0 v\u00e9hiculer des mots de 8 octets et il s'arrangera en m\u00e9moire \u00e0 disposer les donn\u00e9es de la taille d'une palette (64-bits) de fa\u00e7on \u00e0 ce que son acc\u00e8s soit le plus rapide possible.

    La fonction alignof retourne l'alignement d'un type donn\u00e9. Par exemple, alignof(int) retourne 4 sur une architecture x86 32-bits et 8 sur une architecture x86 64-bits. La fonction alignas permet de sp\u00e9cifier l'alignement d'une variable ou d'une structure. Dans cet exemple, on demande \u00e0 ce que la variable a soit align\u00e9e sur 16 octets. L'adresse de a sera donc un multiple de 16. On peut le v\u00e9rifier avec la fonction alignof.

    #include <stdio.h>\n#include <stdalign.h>\n\nint main() {\n    alignas(16) int a;\n    printf(\"Alignement de int : %zu\\n\", alignof(int));\n    printf(\"Adresse de 'a' : %p\\n\", (void*)&a);\n}\n

    L'utilit\u00e9 de cette fonction est limit\u00e9e en pratique. Elle est principalement utilis\u00e9e pour manipuler des donn\u00e9es SIMD (Single Instruction Multiple Data) ou pour optimiser les performances de certaines structures de donn\u00e9es. On peut l'utiliser \u00e9galemenr pour accro\u00eetre l'interop\u00e9rabilit\u00e9 entre le C et d'autres langages de programmation.

    Une utilisation courante est avec des structures. L'exemple suivant montre une structure de 13 bytes mais le processeur, selon l'architecture pourrait d\u00e9cider de stocker le char sur 4 bytes et la structure serait donc de 16 bytes. On peut forcer l'alignement de la structure manuellement avec alignas(16).

    struct alignas(16) Data {\n    char c;    // 1 octet\n    int i;     // 4 octets\n    double d;  // 8 octets\n};\n

    ", "tags": ["alignof", "alignas", "char"]}, {"location": "course-c/35-libraries/standard-library/#stdargh", "title": "<stdarg.h>", "text": "

    Ne vous \u00eates-vous jamais demand\u00e9 quel est le prototype de printf ? Comment se fait-il que cette fonction puisse prendre un nombre variable d'arguments\u2009? La r\u00e9ponse est la biblioth\u00e8que <stdarg.h> qui permet de manipuler les arguments d'une fonction variable. Observons le prototype de printf :

    int printf(const char *format, ...);\n

    Notez les points de suspension ... apr\u00e8s le format. Il s'agit d'une fonction variadic, c'est-\u00e0-dire qu'elle peut prendre un nombre variable d'arguments. Rappelez-vous en relisant le chapitre sur la pile, que lorsqu'une fonction est appel\u00e9e, les diff\u00e9rents arguments sont empil\u00e9s sur la pile. Il est techniquement possible d'empiler autant d'arguments que l'on veut sans changer le comportement de la fonction. Cette derni\u00e8re lira simplements les arguments \u00e0 partir de la base du frame pointer.

    Le fichier d'en-t\u00eate d\u00e9clare 4 macros et un type\u2009:

    Macros pour les fonctions \u00e0 arguments variables Macro Description va_list Type de la liste d'arguments va_start Initialise la liste d'arguments va_arg R\u00e9cup\u00e8re un argument de la liste va_copy Copie une liste d'arguments va_end Termine la liste d'arguments

    Imaginons le cas de figure suivant. Nous souhaitons \u00e9crire une fonction qui affiche la somme des valeurs pass\u00e9es en arguments et nous ne savons pas le nombre de valeurs qui seront pass\u00e9es. Voici comment proc\u00e9der\u2009:

    #include <stdarg.h>\n#include <stdio.h>\n\nint sum(int count, ...) {\n    va_list args;\n    va_start(args, count);\n    int sum = 0;\n    for (int i = 0; i < count; i++) sum += va_arg(args, int);\n    va_end(args);\n    return sum;\n}\n\nint main() {\n    printf(\"Somme : %d\\n\", sum(3, 1, 2, 3));\n    printf(\"Somme : %d\\n\", sum(5, 1, 2, 3, 4, 5));\n}\n

    Dans une fonction dont le prototype autorise un nombre variable d'arguments, l'ellipse ... est utilis\u00e9e apr\u00e8s au moins un argument fixe qui pourrait repr\u00e9senter le nombre d'arguments \u00e0 suivre. Dans la fonction printf ce nombres est cach\u00e9 dans le format, en comptant le nombre de %. Dans cette fonction, une liste d'arguments args est d\u00e9clar\u00e9e. Lors de l'appel de va_start, la liste est initialis\u00e9e \u00e0 partir de l'argument suivant count. En pratique va_list est un pointeur sur la pile qui pointe sur l'argument suivant count. La fonction va_arg permet de r\u00e9cup\u00e9rer les arguments un par un de fa\u00e7on portable. En pratique elle d\u00e9r\u00e9f\u00e9rence le pointeur et retourne le type sp\u00e9cifi\u00e9 comme deuxi\u00e8me argument. Enfin, va_end termine la liste d'arguments.

    Voici ci-dessous comment elle pourrait \u00eatre impl\u00e9ment\u00e9e, n\u00e9anmoins ces macros utilisent des fonctions pr\u00e9compil\u00e9es (p. ex\u2009: __builtin_va_start) qui sont sp\u00e9cifiques \u00e0 chaque compilateur.

    typedef struct {\n    unsigned int gp_offset;  // Offset dans les registres g\u00e9n\u00e9raux\n    unsigned int fp_offset;  // Offset dans les registres flottants\n    void* overflow_arg_area; // Pointeur vers les arguments pass\u00e9s sur la pile\n    void* reg_save_area;     // Pointeur vers les registres sauvegard\u00e9s\n} va_list;\n\n// Macro g\u00e9n\u00e9rique pour r\u00e9cup\u00e9rer la taille d'un type\n#define type_size(type) _Generic((type), \\\n    int: sizeof(int), \\\n    double: sizeof(double), \\\n    float: sizeof(float), \\\n    char: sizeof(char), \\\n    default: sizeof(void*))\n\n#define va_start(ap, last) __va_start(ap, &last, type_size(last))\n#define va_arg(ap, type) \\\n    *((type*)(ap.overflow_arg_area)); \\\n    ap.overflow_arg_area += type_size(type)\n#define va_end(ap) (void)0\n\nvoid __va_start(va_list_hack* ap, void* last, size_t last_size) {\n    void *stack_pointer;\n    ap->overflow_arg_area = (void*)((char*)(&last) + last_size);\n    ap->gp_offset = 0;\n    ap->fp_offset = 0;\n    ap->reg_save_area = NULL;\n}\n

    ", "tags": ["args", "va_start", "va_end", "va_list", "printf", "count", "va_arg", "va_copy"]}, {"location": "course-c/35-libraries/standard-library/#stdatomich", "title": "<stdatomic.h>", "text": "

    Cet en-t\u00eate concerne la notion d'atomicit\u00e9 en programmation concurrente, et il pourrait s'agir d'un cours \u00e0 part enti\u00e8re. L'atomicit\u00e9 est la propri\u00e9t\u00e9 d'une op\u00e9ration qui est ex\u00e9cut\u00e9e en une seule \u00e9tape sans \u00eatre interrompue. En d'autres termes, une op\u00e9ration atomique est une op\u00e9ration qui est soit compl\u00e8tement ex\u00e9cut\u00e9e, soit pas du tout. Lorsqu'un programme utilise des threads (sous-programmes ex\u00e9cut\u00e9s en parall\u00e8le), il est possible que deux ex\u00e9cutions parall\u00e8les tentent de modifier la m\u00eame variable en m\u00eame temps. Cela peut poser de gros probl\u00e8mes de corruption de donn\u00e9es. Vous savez par exemple qu'un entier est stock\u00e9 sur 4 octets. On peut n\u00e9anmoins imaginer une fonction d'\u00e9change de deux variables un peu naive qui traite chaque octet s\u00e9par\u00e9ment.

    union Int { int i; char c[4]; };\n\nvoid swap(int *a, int *b) {\n    union Int tmp;\n    tmp.i = *a;\n    for (int i = 0; i < 4; i++) {\n        char t = ((union Int)*a).c[i];\n        ((union Int)*a).c[i] = ((union Int)*b).c[i];\n        ((union Int)*b).c[i] = t;\n    }\n}\n

    Dans le cas ou deux processus s\u00e9par\u00e9s essaient de traiter la valeur de a envoy\u00e9e, l'un \u00e0 swap et l'autre \u00e0 printf. Le r\u00e9sultat d\u00e9pendra et l'ordre d'ex\u00e9cution des instructions, et il se peut que printf affiche d\u00e9j\u00e0 quelque chose alors que swap n'a pas encore termin\u00e9. C'est ce qu'on appelle une condition de course. Bien entendu en pratique personne n'\u00e9crirait une fonction d'\u00e9change de variables de cette mani\u00e8re. Toutefois, pour r\u00e9soudre ce type de probl\u00e8me on utilise des fonctions atomiques qui ajoutent une couche de protection \u00e0 des variables partag\u00e9es entre plusieurs threads.

    Par cons\u00e9quent, il est n\u00e9cessaire d'utiliser un accesseur atomique pour lire et \u00e9crire la variable\u2009:

    #include <stdatomic.h>\n\nint main() {\n    atomic_int a = 42;\n    atomic_store(&a, 42);     // \u00c9criture atomique\n    int b = atomic_load(&a);  // Lecture atomique\n    atomic_fetch_add(&a, 1);  // Incr\u00e9mentation atomique de 1\n    atomic_fetch_sub(&a, 1);  // Soustraction atomique de 1\n    atomic_fetch_or(&a, 1);   // ...\n    atomic_fetch_and(&a, 1);\n    atomic_fetch_xor(&a, 1);\n    atomic_exchange(&a, 42);  // Notre fonction swap atomic\n    atomic_compare_exchange_strong(&a, &b, 42);\n    atomic_compare_exchange_weak(&a, &b, 42);\n}\n

    Pour de plus emples informations sur la programmation concurrente, je vous redirige sur un cours d\u00e9di\u00e9 \u00e0 ce sujet.

    ", "tags": ["printf", "swap"]}, {"location": "course-c/35-libraries/standard-library/#stdbith", "title": "<stdbit.h>", "text": "

    Cette biblioth\u00e8que a \u00e9t\u00e9 introduite avec le standard C23 et elle permet de manipuler les bits de mani\u00e8re portable en fournissant des macros pour les op\u00e9rations bit \u00e0 bit. Les macro suivantes sont disponibles\u2009:

    Macros bit \u00e0 bit dans C23 Macro Description stdc_popcount Compte le nombre de bits \u00e0 1 stdc_clz Compte le nombre de bits \u00e0 0 avant le premier bit \u00e0 1 stdc_ctz Compte le nombre de bits \u00e0 0 apr\u00e8s le dernier bit \u00e0 1 stdc_rotl Rotation \u00e0 gauche stdc_rotr Rotation \u00e0 droite stdc_bswap Inversion des octets

    Bien entendu pour ces op\u00e9rations, il est n\u00e9cessaire de conna\u00eetre la taille du type utilis\u00e9. C'est pourquoi ces macros sont g\u00e9n\u00e9riques et utilisent _Generic pour d\u00e9terminer la taille du type. Une rotation \u00e0 droite pourrait \u00eatre impl\u00e9ment\u00e9e na\u00efvement avec une macro\u2009:

    #define stdc_rotl(x, n) \\\n    ((x << (n % (sizeof(x) * CHAR_BIT))) | (x >> ((sizeof(x) * CHAR_BIT) -\n    (n % (sizeof(x) * CHAR_BIT)))))\n

    N\u00e9anmoins ces fonctions sont faites pour profiter des instructions sp\u00e9cifiques des processeurs modernes qui permettent de r\u00e9aliser ces op\u00e9rations de mani\u00e8re plus efficace. En effet dans l'architecture X86 par exemple il existe la directive assembleur ror pour la rotation \u00e0 droite et rol pour la rotation \u00e0 gauche. Ces instructions sont plus rapides que la m\u00e9thode na\u00efve ci-dessus mais elles n'existent pas n\u00e9cessairement dans toutes les architectures. Du reste, si on essaye de compiler cette macro avec gcc et observons l'assembler g\u00e9n\u00e9r\u00e9, on constate que le compilateur utilise bien l'instruction ror pour la rotation \u00e0 droite. Il est donc capable de comprendre le code et de l'optimiser en cons\u00e9quence.

    ", "tags": ["stdc_popcount", "ror", "rol", "_Generic", "stdc_bswap", "stdc_rotr", "stdc_rotl", "stdc_clz", "stdc_ctz"]}, {"location": "course-c/35-libraries/standard-library/#stdboolh", "title": "<stdbool.h>", "text": "

    Cette biblioth\u00e8que est apparue en C99 et apr\u00e8s 20 ans d'attente, elle introduit enfin le type bool\u00e9en bool et les valeurs true et false. Cet en-t\u00eate est par cons\u00e9quent l'un des plus simple de la biblioth\u00e8que standard, car il ne contient que trois lignes\u2009:

    #define bool _Bool\n#define true 1\n#define false 0\n

    Quelle belle mascarade\u2009! On d\u00e9finit bool comme _Bool... En r\u00e9alit\u00e9 ce dernier est un type natif du langage alors que bool n'est qu'une macro. C'est \u00e0 dire que sans include <stdbool.h> vous pouvez tout de m\u00eame d\u00e9finir un bool\u00e9en en utilisant _Bool. N\u00e9anmoins, l'int\u00e9r\u00eat de cette biblioth\u00e8que est de standardiser le type bool\u00e9en et les valeurs true et false pour une meilleure portabilit\u00e9 du code.

    On peut s'interroger pourquoi le standard \u00e0 d\u00e9cid\u00e9, plut\u00f4t que d'ajouter un nouveau type bool natif, que de le d\u00e9finir dans un en-t\u00eate suppl\u00e9mentaire et de d\u00e9finir le type avec un _ en pr\u00e9fixe. Le langage C a une longue histoire et de nombreux programmes n'ont pas attendu la sortie de cette biblioth\u00e8que pour d\u00e9finir leur propre type bool\u00e9en. Il \u00e9tait donc n\u00e9cessaire de ne pas casser la compatibilit\u00e9 avec les anciens programmes. C'est pourquoi le type _Bool a \u00e9t\u00e9 introduit sous cette nomenclature. L'histoire est similaire avec _Complex et _Imaginary de la biblioth\u00e8que <complex.h>.

    Notez que le _Bool est un type tr\u00e8s particulier car il est en r\u00e9alit\u00e9 souvent impl\u00e9ment\u00e9 comme un entier 8-bit. Rappelez-vous que le processeur aime manipuler des mots de la taille de son bus de donn\u00e9es. C'est pourquoi un bool\u00e9en est souvent stock\u00e9 sur 32-bits m\u00eame si un seul bit suffirait. C'est pourquoi on peut stocker des valeurs autres que true et false dans un bool\u00e9en. Par exemple, bool b = 42 est tout \u00e0 fait valide en C. En r\u00e9alit\u00e9, true et false sont des macros qui valent respectivement 1 et 0. C'est pourquoi on peut les utiliser pour initialiser des bool\u00e9ens.

    _Bool b = 1;\nprintf(\"%ld\\n\", sizeof(b)); // Affiche 1\n*((int*)&b) += 10; // Bypass le syst\u00e8me de typage\nprintf(\"%hhd\\n\", (int)b); // Affiche 11\n

    Il est n\u00e9cessaire de gruger pour ajouter 10 car na\u00efvement le compilateur impl\u00e9mente b += 10 avec\u2009:

    movs    r3, #1          // Sauve 1 et non 10 dans r3\n

    Dans le cas des tableaux, ne perdez pas \u00e0 l'esprit qu'un tableau de bool\u00e9ens est un tableau d'octets\u2009:

    bool bool_array[8] = {true, false, true, true, false, false, true, true};\nassert(sizeof(bool_array) == 8);\n

    ", "tags": ["_Imaginary", "true", "_Complex", "bool", "false", "_Bool"]}, {"location": "course-c/35-libraries/standard-library/#stdckdinth", "title": "<stdckdint.h>", "text": "

    Cette biblioth\u00e8que est apparue en C23 et propose des fonctions arithm\u00e9tiques pour les op\u00e9rations de base comme l'addition, la soustraction, et la multiplication, mais avec une d\u00e9tection explicite de l'overflow. L'abbr\u00e9viation ckd signifie checked. Les fonctions introduites par cet en-t\u00eate sont\u2009:

    Fonctions de d\u00e9tection de l'overflow Fonction Description ckd_add Addition avec d\u00e9tection de l'overflow ckd_sub Soustraction avec d\u00e9tection de l'overflow ckd_mul Multiplication avec d\u00e9tection de l'overflow

    Lorsque deux entiers sont additionn\u00e9s, il est possible que le r\u00e9sultat d\u00e9passe la capacit\u00e9 de stockage de l'entier. Par exemple, si on additionne \\(2^31 - 1\\) \u00e0 1, on obtient \\(-2^31\\). C'est ce qu'on appelle un overflow. En C, l'overflow est un comportement ind\u00e9fini, c'est-\u00e0-dire que le compilateur est libre de d\u00e9cider du comportement du programme. En pratique, la plupart des compilateurs vont simplement ignorer l'overflow et le r\u00e9sultat sera tronqu\u00e9. C'est pourquoi il est important de v\u00e9rifier les overflows lorsqu'on travaille avec des entiers.

    Avant l'introduction de cette biblioth\u00e8que, un d\u00e9passement de capacit\u00e9 devrait \u00eatre manuellement impl\u00e9ment\u00e9\u2009:

    int add(int a, int b) {\n    if (a > 0 && b > INT_MAX - a) {\n        // Overflow d\u00e9tect\u00e9\n    } else\n        return a + b;\n}\n

    Avec la biblioth\u00e8que <stdckdint.h>, il est possible de d\u00e9tecter l'overflow de mani\u00e8re plus \u00e9l\u00e9gante\u2009:

    #include <stdckdint.h>\n\nint add(int a, int b) {\n    int result;\n    if (ckd_add(a, b, &result)) {\n        // Overflow d\u00e9tect\u00e9\n    } else\n        return result;\n}\n

    ", "tags": ["ckd_mul", "ckd_sub", "ckd", "ckd_add"]}, {"location": "course-c/35-libraries/standard-library/#stddefh", "title": "<stddef.h>", "text": "

    La biblioth\u00e8que <stddef.h> fournit quelques d\u00e9finitions utiles tel que donn\u00e9 par la table suivante\u2009:

    D\u00e9finitions de stddef.h Macro Description NULL Pointeur nul offsetof Offset d'un membre d'une structure ptrdiff_t Type pour les diff\u00e9rences de pointeurs size_t Type pour les tailles d'objets wchar_t Type pour les caract\u00e8res larges nullptr_t Pointeur null (C23) max_align_t Type pour l'alignement maximal

    L'impl\u00e9mentation probable de ces macros est la suivante\u2009:

    #define NULL ((void*)0)\n#define offsetof(type, member) ((size_t)&((type*)0)->member)\ntypedef unsigned long size_t\ntypedef ptrdiff_t long\ntypedef int wchar_t`\n

    Concernant les pointeurs, s'il est parfaitement correct de tester si un pointeur vaut 0 (if (ptr == 0)), il est recommand\u00e9 d'utiliser NULL pour plus de clart\u00e9. Le standard C23 \u00e0 introduit le type nullptr_t pour les pointeurs nuls. Il est recommand\u00e9 de l'utiliser \u00e0 la place de NULL.

    max_align_t

    Il s'agit d'un type un type introduit en C11 pour repr\u00e9senter l'alignement maximal possible pour n'importe quel type. Il est utilis\u00e9 pour d\u00e9finir des types align\u00e9s de mani\u00e8re optimale. Par exemple, si on veut d\u00e9finir une structure align\u00e9e sur 16 octets, on peut utiliser max_align_t pour d\u00e9finir le type de la structure.

    size_t

    Il s'agit d'un type non sign\u00e9 qui est utilis\u00e9 pour repr\u00e9senter la taille d'un objet en m\u00e9moire. Il est utilis\u00e9 pour les fonctions qui retournent la taille d'un objet, comme strlen ou sizeof. Il est d\u00e9fini tel que sa taille soit suffisante, en pratique 64-bits sur une architecture 64-bits.

    ptrdiff_t

    Il s'agit d'un type sign\u00e9 qui est utilis\u00e9 pour repr\u00e9senter la diff\u00e9rence entre deux pointeurs. Lorsque l'on veut calculer ptr_p - ptr_q on obtient un entier dont la valeur maximale d\u00e9pend de la taille de la m\u00e9moire adressable.

    ", "tags": ["size_t", "offsetof", "nullptr_t", "max_align_t", "strlen", "ptrdiff_t", "NULL", "sizeof", "wchar_t"]}, {"location": "course-c/35-libraries/standard-library/#inttypesh-et-stdinth", "title": "<inttypes.h> et <stdint.h>", "text": "

    Ces deux biblioth\u00e8ques r\u00e9pondent au besoin d'avoir des types entiers d'une taille contr\u00f4l\u00e9e et surtout portable. En effet, nous avons vu que les types standards (int, short, long...) d\u00e9pendent du mod\u00e8le de donn\u00e9es de l'architecture cible. Un long n'aura pas la m\u00eame taille sur Linux ou Windows par exemple.

    L'en-t\u00eate <stint.h> fourni trois types de base\u2009:

    Cat\u00e9gories de types entiers portables Exemple Description int8_t Entier sign\u00e9 sur 8 bits int8_fast8_t Entier sign\u00e9 d'au moins 8 bits le plus rapide int8_least8_t Entier sign\u00e9 d'au moins 8 bits, le plus petit

    Ces cat\u00e9gories sont disponibles our les longueurs 8, 16, 32, 64 bits. Les types sont d\u00e9finis pour les entiers sign\u00e9s et non sign\u00e9s. Par exemple, int8_t est un entier sign\u00e9 sur 8 bits, uint8_t est un entier non sign\u00e9 sur 8 bits.

    Dans le cas ou on aurait besoin d'une variable pouvant contenir les valeurs de 0 \u00e0 255 mais que la taille de l'entier importe peu pour autant que le processeur n'ait pas de co\u00fbt suppl\u00e9mentaire \u00e0 manipuler la variable, on peut utiliser uint_fast8_t.

    \u00c0 l'inverse, si le besoin est d'avoir une variable qui peut contenir les valeurs de 0 \u00e0 255 avec la taille la plus petite possible (id\u00e9alement 8 bits), on utilisera uint_least8_t.

    Enfin, dans le cas (le plus rare) ou on aurait besoin exactement d'un entier non sign\u00e9 de 8 bits, on utilisera uint8_t. N\u00e9anmoins ce type pr\u00e9sente une contrainte importante car toutes les architectures ne sont pas n\u00e9cessairement pr\u00e9vues pour manipuler des entiers de 8 bits. Par exemple le SHARC d'Analog Devices est un processeur 32 bits qui n'a pas de support natif pour les entiers de 8 bits. L'utilisation de uint8_t r\u00e9sulterait en une erreur de compilation.

    L'en-t\u00eate <stdint.h> fournit \u00e9galement des macros utiles pour conna\u00eetre le choix de l'impl\u00e9mentation. Par exemple, INT_FAST8_WIDTH donne la largeur de l'entier le plus rapide selon la machine cible.

    On aura \u00e9galement les valeurs minimum et maximum que peut contenir chacun des types entiers. Par exemple, INT8_MIN et INT8_MAX pour les entiers sign\u00e9s sur 8 bits.

    Dans une boucle for op\u00e9rant sur un tableau de 100 \u00e9l\u00e9ments, il serait correct d'utiliser le type uint_fast8_t pour l'index de la boucle. N\u00e9anmoins pour des raisons de lisibilit\u00e9s, il est souvent pr\u00e9f\u00e9rable d'utiliser simplement int qui, selon le standard, garanti d'\u00eatre capable de contenir la taille du tableau.

    #include <stdint.h>\n\nint main() {\n    for (int_fast8_t i = 0; i < 100; i++) {\n        ...\n    }\n}\n

    En outre, pour des raisons de coh\u00e9rence, certaines normes pour l'avionique ou le m\u00e9dical imposent que les constantes litt\u00e9rales soient explicitement typ\u00e9es. On conna\u00eet d\u00e9j\u00e0 les suffixes u, ull pour les entiers de base, mais on peut \u00e9galement utiliser les macros de <stdint.h> pour les constantes litt\u00e9rales.

    uint8_t a = UINT8_C(42);\n

    L'utilisation de ces types sp\u00e9cifiques dans des fonctions d'entr\u00e9es sortie (p. ex. printf) doit aussi \u00eatre faite coh\u00e9rence. Un int32_t n'est pas compatible avec %d sur toutes les architectures. Il est pr\u00e9f\u00e9rable d'utiliser les macros de <inttypes.h> pour les sp\u00e9cifier.

    int32_t a = 42;\nprintf(\"%\" PRId32 \"\\n\", a);\n

    ", "tags": ["int", "INT_FAST8_WIDTH", "ull", "uint_fast8_t", "uint_least8_t", "int8_least8_t", "short", "uint8_t", "INT8_MIN", "for", "int32_t", "printf", "long", "int8_fast8_t", "INT8_MAX", "int8_t"]}, {"location": "course-c/35-libraries/standard-library/#stdioh", "title": "<stdio.h>", "text": "

    La biblioth\u00e8que <stdio.h> est l'une des biblioth\u00e8ques les plus importantes en C. Elle fournit des fonctions pour l'entr\u00e9e et les sorties, c'est-\u00e0-dire pour lire et \u00e9crire des donn\u00e9es depuis et vers la console. Elle fournit \u00e9galement des fonctions pour lire et \u00e9crire des fichiers.

    La plupart des fonctions de cette biblioth\u00e8que ont d\u00e9j\u00e0 \u00e9t\u00e9 abord\u00e9es dans les chapitres pr\u00e9c\u00e9dents. Voici ci-dessous un r\u00e9sum\u00e9 des fonctions qu'elle contient. Le (f) indique que la fonction peut \u00eatre utilis\u00e9e pour lire ou \u00e9crire depuis un fichier, elle prend un pointeur de type FILE en premier argument. Le (w) indique que la fonction est pr\u00e9vue pour les caract\u00e8res larges (wchar_t).

    Fonctions de stdio.h Fonction Description (f)get(w)c Lit un caract\u00e8re depuis l'entr\u00e9e standard ou un fichier (f)get(w)s Lit une ligne depuis l'entr\u00e9e standard ou un fichier (f)put(w)c \u00c9crit un caract\u00e8re vers l'entr\u00e9e standard ou un fichier (f)put(w)s \u00c9crit une cha\u00eene de caract\u00e8res vers la sortie standard ou un fichier clearerr R\u00e9initialise l'\u00e9tat d'erreur d'un flux fclose Ferme un fichier ouvert feof V\u00e9rifie si la fin du fichier est atteinte ferror V\u00e9rifie si une erreur est survenue dans le flux fflush Vide le tampon de sortie d'un flux fgetpos Obtient la position actuelle dans un fichier sous forme de fpos_t fileno Obtient le descripteur de fichier associ\u00e9 \u00e0 un flux flockfile Verrouille un flux pour les op\u00e9rations multithread\u00e9es fopen Ouvre un fichier pour la lecture, l'\u00e9criture ou l'ajout fread Lit des blocs d'octets depuis un flux freopen Ouvre \u00e0 nouveau un fichier sur un flux de fichier existant fscanf Lit des donn\u00e9es format\u00e9es depuis un fichier fseek Positionne le curseur de lecture/\u00e9criture dans un fichier fsetpos D\u00e9finit la position actuelle dans un fichier selon un objet fpos_t ftell Renvoie la position actuelle dans un fichier ftrylockfile Tente de verrouiller un flux pour les op\u00e9rations multithread\u00e9es funlockfile D\u00e9verrouille un flux verrouill\u00e9 fwrite \u00c9crit des blocs d'octets vers un flux perror Affiche un message d'erreur bas\u00e9 sur la derni\u00e8re erreur rencontr\u00e9e remove Supprime un fichier rename Renomme un fichier rewind Remet le curseur au d\u00e9but d'un fichier scanf Lit des donn\u00e9es format\u00e9es depuis l'entr\u00e9e standard setbuf D\u00e9finit un tampon pour un flux setvbuf D\u00e9finit le mode de tampon pour un flux sscanf Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene tmpfile Cr\u00e9e et ouvre un fichier temporaire qui est supprim\u00e9 \u00e0 la fermeture tmpnam G\u00e9n\u00e8re un nom de fichier temporaire unique ungetc Remet un caract\u00e8re dans le flux pour qu'il soit lu \u00e0 nouveau ungetwc Remet un caract\u00e8re large dans le flux Fonctions d'affichage format\u00e9 Fonction Description vfprintf \u00c9crit une sortie format\u00e9e sur un flux avec une liste variadiques vprintf \u00c9crit une sortie format\u00e9e sur stdout avec une liste variadiques vsprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene avec une liste variadiques vfwprintf Version large de vfprintf pour les caract\u00e8res larges (wchar_t) vwprintf Version large de vprintf pour les caract\u00e8res larges (wchar_t) vswprintf Version large de vsprintf pour les caract\u00e8res larges (wchar_t) fprintf \u00c9crit une sortie format\u00e9e dans un fichier printf \u00c9crit une sortie format\u00e9e sur stdout sprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene snprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene avec une taille limit\u00e9e fwprintf Version large de fprintf pour les caract\u00e8res larges (wchar_t) wprintf Version large de printf pour les caract\u00e8res larges (wchar_t) swprintf Version large de sprintf pour les caract\u00e8res larges (wchar_t) Constantes et types de stdio.h Constante/Type Description EOF Constante retourn\u00e9e par les fonctions de lecture lorsqu'une fin de fichier ou une erreur est rencontr\u00e9e NULL Pointeur nul utilis\u00e9 pour repr\u00e9senter l'absence d'objet FILENAME_MAX Longueur maximale d'un nom de fichier FOPEN_MAX Nombre maximal de fichiers pouvant \u00eatre ouverts simultan\u00e9ment L_tmpnam Longueur minimale d'un tampon pour tmpnam BUFSIZ Taille du tampon par d\u00e9faut pour les op\u00e9rations de lecture/\u00e9criture TMP_MAX Nombre maximal de noms uniques g\u00e9n\u00e9r\u00e9s par tmpnam SEEK_SET Indique le d\u00e9but du fichier pour fseek et fseeko SEEK_CUR Indique la position actuelle dans le fichier pour fseek et fseeko SEEK_END Indique la fin du fichier pour fseek et fseeko stderr Flux de sortie d'erreur standard stdin Flux d'entr\u00e9e standard stdout Flux de sortie standard FILE Type opaque repr\u00e9sentant un flux de fichier fpos_t Type utilis\u00e9 pour stocker la position dans un fichier

    ", "tags": ["fflush", "ferror", "vwprintf", "rename", "vsprintf", "tmpnam", "SEEK_END", "vfwprintf", "feof", "setbuf", "ftrylockfile", "scanf", "ungetwc", "sprintf", "clearerr", "fwrite", "funlockfile", "tmpfile", "snprintf", "fpos_t", "fseeko", "stderr", "perror", "sscanf", "NULL", "wchar_t", "fopen", "ftell", "FOPEN_MAX", "swprintf", "FILENAME_MAX", "fwprintf", "flockfile", "ungetc", "freopen", "vfprintf", "EOF", "TMP_MAX", "fgetpos", "fprintf", "fread", "L_tmpnam", "SEEK_SET", "BUFSIZ", "stdin", "vprintf", "remove", "rewind", "fscanf", "fclose", "fseek", "FILE", "wprintf", "setvbuf", "vswprintf", "printf", "SEEK_CUR", "fileno", "stdout", "fsetpos"]}, {"location": "course-c/35-libraries/standard-library/#stdlibh", "title": "<stdlib.h>", "text": "

    Cette biblioth\u00e8que contient des fonctions \u00e9parses qui ne sont pas assez importantes pour \u00eatre regroup\u00e9es dans une biblioth\u00e8que d\u00e9di\u00e9e. Contrairement aux langages plus r\u00e9cents (comme C++ ou Java), C n'a pas \u00e9t\u00e9 con\u00e7u avec une philosophie de modularit\u00e9 stricte pour les biblioth\u00e8ques. Les fonctions \u00e9taient rassembl\u00e9es par utilit\u00e9 pratique plut\u00f4t que par sujet sp\u00e9cifique, et les biblioth\u00e8ques \u00e9taient assez limit\u00e9es en nombre pour garder le langage simple et portable. On y retrouve les cat\u00e9gories suivantes\u2009:

    • Gestion de la m\u00e9moire
    • Conversion de cha\u00eenes en types num\u00e9riques
    • Gestion du programme
    • Nombres al\u00e9atoires
    • Algorithmes de recherche et de tri
    Fonctions diverses de stdlib.h Fonction Description abort Arr\u00eate le programme de mani\u00e8re anormale sans nettoyage des ressources exit Arr\u00eate le programme de mani\u00e8re normale avec nettoyage des ressources quick_exit Arr\u00eate le programme de mani\u00e8re normale sans nettoyage complet des ressources (C11) _Exit Arr\u00eate le programme de mani\u00e8re normale sans nettoyage des ressources (C99) atexit Enregistre une fonction \u00e0 appeler lors de l'appel \u00e0 exit at_quick_exit Enregistre une fonction \u00e0 appeler lors de l'appel \u00e0 quick_exit (C11) getenv R\u00e9cup\u00e8re la valeur d'une variable d'environnement setenv Ajoute ou modifie une variable d'environnement (POSIX, non standard) putenv Ajoute ou modifie une variable d'environnement unsetenv Supprime une variable d'environnement (POSIX, non standard) system Ex\u00e9cute une commande syst\u00e8me dans un shell rand G\u00e9n\u00e8re un nombre pseudo-al\u00e9atoire srand Initialise le g\u00e9n\u00e9rateur de nombres pseudo-al\u00e9atoires mblen Retourne le nombre d'octets d'un caract\u00e8re multioctet dans une cha\u00eene Gestion de la m\u00e9moire de stdlib.h Fonction Description malloc Alloue un bloc de m\u00e9moire calloc Alloue et initialise un bloc de m\u00e9moire realloc Redimensionne un bloc de m\u00e9moire pr\u00e9c\u00e9demment allou\u00e9 free Lib\u00e8re un bloc de m\u00e9moire pr\u00e9c\u00e9demment allou\u00e9 aligned_alloc Alloue un bloc de m\u00e9moire align\u00e9 (C11) free_sized Lib\u00e8re un bloc de m\u00e9moire de taille sp\u00e9cifi\u00e9e (C23) free_aligned_sized Lib\u00e8re un bloc de m\u00e9moire align\u00e9 de taille sp\u00e9cifi\u00e9e (C23) Algorithmes de recherche et de tri de stdlib.h Fonction Description bsearch Recherche un \u00e9l\u00e9ment dans un tableau tri\u00e9 en utilisant une fonction de comparaison qsort Trie un tableau en utilisant un algorithme de tri rapide (quick sort) Op\u00e9rations sur les nombres de stdlib.h Fonction Description abs Calcule la valeur absolue d'un entier (int) labs Calcule la valeur absolue d'un entier long (long) llabs Calcule la valeur absolue d'un long long (long long) (C99) div Effectue une division enti\u00e8re et retourne le quotient et le reste pour les int ldiv Effectue une division enti\u00e8re pour les long et retourne quotient et reste lldiv Effectue une division enti\u00e8re pour les long long et retourne quotient et reste (C99) Fonctions de conversion de cha\u00eenes de stdlib.h Fonction Description atof Convertit une cha\u00eene de caract\u00e8res en double (double) atoi Convertit une cha\u00eene de caract\u00e8res en entier (int) atol Convertit une cha\u00eene de caract\u00e8res en long (long) atoll Convertit une cha\u00eene de caract\u00e8res en long long (long long) (C99) mbstowcs Convertit une cha\u00eene multioctet en cha\u00eene de caract\u00e8res larges (wchar_t) mbtowc Convertit un caract\u00e8re multioctet en caract\u00e8re large (wchar_t) strtod Convertit une cha\u00eene en double (double) strtof Convertit une cha\u00eene en float (float) (C99) strtol Convertit une cha\u00eene en long (long), avec une base personnalisable strtold Convertit une cha\u00eene en long double (long double) (C99) strtoll Convertit une cha\u00eene en long long (long long) (C99) strtoul Convertit une cha\u00eene en unsigned long (unsigned long) strtoull Convertit une cha\u00eene en unsigned long long (unsigned long long) (C99) wcstombs Convertit une cha\u00eene de caract\u00e8res larges en cha\u00eene multioctet wctomb Convertit un caract\u00e8re large (wchar_t) en multioctet Constantes et types de stdlib.h Constante/Type Description EXIT_SUCCESS Indique une terminaison r\u00e9ussie du programme (valeur utilis\u00e9e avec exit) EXIT_FAILURE Indique une terminaison \u00e9chou\u00e9e du programme (valeur utilis\u00e9e avec exit) NULL Pointeur nul, utilis\u00e9 pour initialiser ou tester des pointeurs RAND_MAX Valeur maximale que peut retourner rand MB_CUR_MAX Taille maximale d'un caract\u00e8re multioctet pour la locale courante size_t Type pour repr\u00e9senter des tailles et des dimensions div_t Structure retourn\u00e9e par div contenant le quotient et le reste ldiv_t Structure retourn\u00e9e par ldiv contenant le quotient et le reste lldiv_t Structure retourn\u00e9e par lldiv (C99) contenant le quotient et le reste wchar_t Type pour repr\u00e9senter un caract\u00e8re large mbstate_t Type utilis\u00e9 pour conserver l'\u00e9tat entre conversions de caract\u00e8res multioctets et caract\u00e8res larges

    ", "tags": ["int", "qsort", "abs", "quick_exit", "setenv", "mblen", "bsearch", "ldiv_t", "free", "wctomb", "RAND_MAX", "free_sized", "long", "EXIT_FAILURE", "double", "mbstowcs", "EXIT_SUCCESS", "strtof", "mbtowc", "size_t", "srand", "unsetenv", "strtod", "abort", "ldiv", "atoi", "atoll", "at_quick_exit", "NULL", "lldiv", "wchar_t", "realloc", "atol", "mbstate_t", "putenv", "_Exit", "calloc", "getenv", "strtoll", "div", "strtoull", "rand", "div_t", "MB_CUR_MAX", "lldiv_t", "wcstombs", "aligned_alloc", "malloc", "system", "atof", "float", "strtoul", "atexit", "llabs", "exit", "strtol", "labs", "strtold"]}, {"location": "course-c/35-libraries/standard-library/#stdnoreturnh", "title": "<stdnoreturn.h>", "text": "

    Cette biblioth\u00e8que est apparue en C11 et elle introduit le type noreturn qui est utilis\u00e9 pour indiquer qu'une fonction ne retourne jamais. Cela permet au compilateur d'optimiser le code en supprimant les instructions de retour de la fonction. En pratique, cela permet de gagner quelques cycles d'horloge. Voici un exemple d'utilisation\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <stdnoreturn.h>\n\nnoreturn void exit_now(int i)\n{\n    if (i > 0)\n        exit(i);\n\n    // Si i <= 0, le comportement est ind\u00e9fini\n}\n\nint main(void)\n{\n    puts(\"Se pr\u00e9pare \u00e0 terminer le programme...\");\n    exit_now(2);\n    puts(\"On sait que ce code n'est jamais ex\u00e9cut\u00e9.\");\n}\n

    Avant C23, il fallait utiliser _Noreturn.

    ", "tags": ["noreturn", "_Noreturn"]}, {"location": "course-c/35-libraries/standard-library/#stringh", "title": "<string.h>", "text": "

    La biblioth\u00e8que <string.h> contient des fonctions pour manipuler les cha\u00eenes de caract\u00e8res. Les fonctions sont d\u00e9finies pour les cha\u00eenes de caract\u00e8res ASCII uniquement. On distingue deux famille de fonctions, les mem qui manipulent des r\u00e9gions m\u00e9moires et les str qui manipulent des cha\u00eenes de caract\u00e8res.

    La table suivante r\u00e9sume les fonctions les plus utilis\u00e9es. On notera que les lettres entre parenth\u00e8ses indiquent les variantes des fonctions. La fonction strcpy existe en version strncpy qui permet de copier une cha\u00eene en sp\u00e9cifiant la taille maximale \u00e0 copier. On notera n pour les fonctions dont la taille maximum de la cha\u00eene peut \u00eatre sp\u00e9cifi\u00e9e, r pour reverse et c pour not in.

    Fonctions sur les cha\u00eenes de caract\u00e8res Fonction Description memset Remplissage d'une r\u00e9gion m\u00e9moire memcpy Copie d'une r\u00e9gion m\u00e9moire memmove Copie d'une r\u00e9gion m\u00e9moire avec superposition memcmp Comparaison de deux r\u00e9gions m\u00e9moire memchr Recherche d'un caract\u00e8re dans une r\u00e9gion m\u00e9moire strlen Longueur de la cha\u00eene str(n)cpy Copie d'une cha\u00eene str(n)cat Concat\u00e9nation de deux cha\u00eenes str(n)cmp Comparaison de deux cha\u00eenes str(r)chr Recherche d'un caract\u00e8re dans une cha\u00eene strstr Recherche d'une sous-cha\u00eene dans une cha\u00eene strtok D\u00e9coupe une cha\u00eene en morceaux strspn Longueur du pr\u00e9fixe d'une cha\u00eene strcspn Longueur du pr\u00e9fixe qui ne contient pas certains caract\u00e8res strpbrk Recherche d'un caract\u00e8re dans une liste strcoll Comparaison de cha\u00eenes selon la locale strxfrm Transformation de cha\u00eenes selon la locale strerror Message d'erreur associ\u00e9 \u00e0 un code d'erreur", "tags": ["strspn", "strerror", "strxfrm", "memset", "str", "mem", "strncpy", "strpbrk", "strlen", "strtok", "strcspn", "memchr", "strcpy", "strcoll", "memcmp", "memmove", "memcpy", "strstr"]}, {"location": "course-c/35-libraries/standard-library/#memset", "title": "memset", "text": "

    La fonction memset permet de remplir une r\u00e9gion m\u00e9moire avec une valeur donn\u00e9e. Son prototype est\u2009:

    void *memset(void *s, int c, size_t n);\n

    Elle est utilis\u00e9e principalement pour initialiser ou r\u00e9initialiser un tableau en une seule instruction qui sera plus performante qu'une boucle. L'exemple suivant initialise un tableau de 100 \u00e9l\u00e9ments avec la valeur 42\u2009:

    char array[100];\nmemset(array, 42, sizeof(array));\n

    Notez que la valeur est un byte. Memset ne peut pas \u00eatre utilis\u00e9 pour initialiser un tableau avec une valeur de type int par exemple.

    ", "tags": ["int", "memset"]}, {"location": "course-c/35-libraries/standard-library/#memcpy-et-memmove", "title": "memcpy et memmove", "text": "

    Les deux fonctions permettent de copier des r\u00e9gions m\u00e9moires d'une adresse \u00e0 une autre. Leur prototype est le suivant\u2009:

    void *memmove(void *dest, const void *src, size_t n)\nvoid *memcpy(void *dest, const void *src, size_t n)\n

    La diff\u00e9rence principale est que memcpy ne traite pas les cas o\u00f9 les deux r\u00e9gions m\u00e9moire se superposent. memmove est plus s\u00fbr et plus lent que memcpy. En effet, memmove doit v\u00e9rifier si les deux r\u00e9gions m\u00e9moire se superposent et dans ce cas, elle doit copier les donn\u00e9es dans un buffer temporaire avant de les copier dans la r\u00e9gion de destination. memcpy ne fait pas cette v\u00e9rification et copie directement les donn\u00e9es.

    Pour comprendre ce probl\u00e8me de superposition prenons l'exemple ci-dessous. La fonction memcpy est r\u00e9impl\u00e9ment\u00e9e pour avoir une r\u00e9sultat pr\u00e9visible. Un pointeur p est d\u00e9clar\u00e9 et un pointeur q correspond \u00e0 l'adresse de p mais d\u00e9cal\u00e9 de deux \u00e9l\u00e9ments. Les deux espaces m\u00e9moire se superposent donc. Apr\u00e8s copie on devrait obtenir 12345 dans q mais apr\u00e8s la copie mais on obtient 12121. Il y a donc un probl\u00e8me.

    int mymemcpy(void *dest, const void *src, size_t n) {\n   char *d = dest;\n   const char *s = src;\n   for (size_t i = 0; i < n; i++) d[i] = s[i];\n   return 0;\n}\n\nint main() {\n   char s[] = \"12345..\";\n   char *p = s;\n   char *q = p + 2;\n   mymemcpy(q, p, 5);\n   for (int i = 0; i < 5; i++) printf(\"%c\", q[i]);\n   printf(\"\\n\");\n}\n

    Pour vous en convaincre, vous pouvez vous aider de la figure suivante.

    memcpy

    En r\u00e9alit\u00e9, le fonctionnement de memcpy n'est pas si simple. En effet, le compilateur peut optimiser le code et utiliser des instructions SIMD pour copier les donn\u00e9es. Cela va \u00e9galement d\u00e9pendre du niveau d'optimisation du compilateur. En ex\u00e9cutant le m\u00eame code avec memcpy, je n'obtiens pas le m\u00eame r\u00e9sultat, en observant le code assembleur g\u00e9n\u00e9r\u00e9 pour memcpy, on observe que les premiers 4 octets sont copi\u00e9s en une seule instruction, le cinqui\u00e8me octet est copi\u00e9 en une instruction s\u00e9par\u00e9e. Ce qui donne comme r\u00e9sultat 12343.

    mov rcx, qword ptr [rbp - 128]  ; Charge l'adresse de destination (q) dans rcx\nmov rdx, qword ptr [rbp - 120]  ; Charge l'adresse source (p) dans rdx\nmov esi, dword ptr [rdx]        ; Charge les 4 premiers octets dans esi\nmov dword ptr [rcx], esi        ; Copie ces 4 octets dans la destination\nmov r8b, byte ptr [rdx + 4]     ; Charge le 5e octet de la source dans r8b\nmov byte ptr [rcx + 4], r8b     ; Copie le 5e octet dans la destination\n

    Pour s'affranchir de ce type de probl\u00e8me, il est pr\u00e9f\u00e9rable d'utiliser memmove lorsque vous n'\u00eates pas s\u00fbr que les deux r\u00e9gions m\u00e9moire ne se superposent pas.

    ", "tags": ["memmove", "memcpy"]}, {"location": "course-c/35-libraries/standard-library/#memcmp", "title": "memcmp", "text": "

    La fonction memcmp permet de comparer deux r\u00e9gions m\u00e9moires. Son prototype est\u2009:

    int memcmp(const void *s1, const void *s2, size_t n);\n

    On utilise typiquement memcmp pour comparer deux structures. Il n'est en effet pas possible de comparer deux structures directement avec l'op\u00e9rateur ==. On utilisera plut\u00f4t\u2009:

    struct Person {\n    char firstname[64];\n    char lastname[64];\n    Genre genre;\n    int age;\n};\n\nPerson p, q;\n// Do something with p and q\n\nif (memcmp(&p, &p, sizeof(struct Person)) == 0)\n    printf(\"Les deux personnes sont identiques\\n\");\n

    Comparaison de cha\u00eenes de caract\u00e8res

    Attention n\u00e9anmoins au cas de figure donn\u00e9. Les champs firstname et lastname sont des buffers de 64 caract\u00e8res. Si les deux structures contiennent les m\u00eames pr\u00e9noms et noms il n'y a aucune garantie que les deux structures soient \u00e9gales. En effet, apr\u00e8s le caract\u00e8re '\\0', rien n'oblige l'utilisateur \u00e0 remplir le reste du buffer avec des '\\0'.

    Il serait ici pr\u00e9f\u00e9rable de tester individuellement les champs de la structure.

    ", "tags": ["lastname", "memcmp", "firstname"]}, {"location": "course-c/35-libraries/standard-library/#memchr-et-strrchr", "title": "memchr et str(r)chr", "text": "

    Les fonctions memchr, strchr et strrchr permettent de rechercher un caract\u00e8re dans une r\u00e9gion m\u00e9moire. Leurs prototypes sont\u2009:

    void *memchr(const void *s, int c, size_t n);\nchar *strchr(const char *s, int c);\nchar *strrchr(const char *s, int c);\n

    Une utilisaiton typique de strchr ou strrchr est de rechercher l'occurence d'un caract\u00e8re dans une cha\u00eene de caract\u00e8res. La fonction s'arr\u00eate d\u00e8s qu'elle trouve le caract\u00e8re recherch\u00e9 ou qu'elle atteint la fin de la cha\u00eene.

    char *s = \"Anticonsitutionnellement\";\nchar *p = strchr(s, 'i');\nassert(p != NULL); // Caract\u00e8re trouv\u00e9\nassert(*p == 'i'); // Caract\u00e8re trouv\u00e9 est 'i'\nassert(p - s == 3); // Position de 'i' dans la cha\u00eene\n\n// Recherche depuis la fin\np = strrchr(s, 'i');\nassert(p - s == 12); // Derni\u00e8re position de 'i' dans la cha\u00eene\n

    Dans le cas de memchr, il est possible de chercher n'importe quelle valeur de byte, y compris '\\0'. En revanche, il est n\u00e9cessaire de sp\u00e9cifier la taille de la r\u00e9gion m\u00e9moire \u00e0 parcourir.

    ", "tags": ["strchr", "strrchr", "memchr"]}, {"location": "course-c/35-libraries/standard-library/#strlen", "title": "strlen", "text": "

    La fonction strlen permet de calculer la longueur d'une cha\u00eene de caract\u00e8res. Son prototype est\u2009:

    size_t strlen(const char *s);\n

    Son impl\u00e9mentation pourrait \u00eatre la suivante\u2009:

    size_t strlen(const char *s) {\n    size_t i = 0;\n    while (s[i] != '\\0') i++;\n    return i;\n}\n
    ", "tags": ["strlen"]}, {"location": "course-c/35-libraries/standard-library/#strncpy", "title": "str(n)cpy", "text": "

    Les fonctions strcpy et strncpy permettent de copier une cha\u00eene de caract\u00e8res. Leur prototype sont\u2009:

    char *strcpy(char *dest, const char *src);\nchar *strncpy(char *dest, const char *src, size_t n);\n

    La fonction strcpy copie la cha\u00eene de caract\u00e8res src dans dest. La fonction strncpy copie au maximum n caract\u00e8res de src dans dest. Si la cha\u00eene src est plus longue que n, la cha\u00eene dest ne sera pas termin\u00e9e par '\\0'.

    ", "tags": ["strcpy", "dest", "src", "strncpy"]}, {"location": "course-c/35-libraries/standard-library/#strncat", "title": "str(n)cat", "text": "

    Les fonctions strcat et strncat permettent de concat\u00e9ner deux cha\u00eenes de caract\u00e8res. Leur prototype sont\u2009:

    char *strcat(char *dest, const char *src);\nchar *strncat(char *dest, const char *src, size_t n);\n

    La fonction strcat concat\u00e8ne la cha\u00eene de caract\u00e8res src \u00e0 la fin de dest. La fonction strncat concat\u00e8ne au maximum n caract\u00e8res de src \u00e0 la fin de dest. Si la cha\u00eene src est plus longue que n, la cha\u00eene dest ne sera pas termin\u00e9e par '\\0'.

    char dest[20] = \"Hello, \"; // dest est plus grand que src !\nchar src[] = \"World!\";\nstrcat(dest, src);\nprintf(\"%s\\n\", dest);  // Affiche \"Hello, World!\"\n

    L'impl\u00e9mentation de strcat pourrait \u00eatre la suivante\u2009:

    char *strcat(char *dest, const char *src) {\n    while (*dest != '\\0') ++dest;\n    while ((*dest++ = *src++) != '\\0');\n    return dest;\n}\n
    ", "tags": ["strncat", "src", "dest", "strcat"]}, {"location": "course-c/35-libraries/standard-library/#strcmp-et-strncmp", "title": "strcmp et strncmp", "text": "

    La fonction strcmp permet de comparer deux cha\u00eenes de caract\u00e8res. Son prototype est\u2009:

    int strcmp(const char *s1, const char *s2);\n

    Elle sera utilis\u00e9e principalement pour comparer deux cha\u00eenes de caract\u00e8res. Une utlisation typique est de tester si deux cha\u00eenes sont \u00e9gales. Par exemple\u2009:

    char *owner = \"John\";\nchar user[64];\nif (scanf(\"%63s\", user) == 1 && memcmp(owner, user) == 0) {\n    printf(\"Welcome John!\\n\");\n}\n

    Le strncmp permet de forcer la comparaison \u00e0 s'arr\u00eater apr\u00e8s un certain nombre de caract\u00e8res. C'est particuli\u00e8rement utile pour le traitement des arguments de ligne de commande. Par exemple\u2009:

    if (strncmp(argv[1], \"--filename=\", 11) == 0) {\n    printf(\"Ouverture du fichier %s\\n\", argv[1] + 11);\n}\n
    ", "tags": ["strcmp", "strncmp"]}, {"location": "course-c/35-libraries/standard-library/#strtok", "title": "strtok", "text": "

    La fonction strtok permet de d\u00e9couper une cha\u00eene de caract\u00e8res en morceaux. Il s'agit de l'abbr\u00e9viation de string token. Son prototype est\u2009:

    char *strtok(char *str, const char *delim);\n

    La premi\u00e8re fois que la fonction est appel\u00e9e, elle prend en param\u00e8tre la cha\u00eene \u00e0 d\u00e9couper. Les appels suivants doivent passer NULL en premier param\u00e8tre. La fonction retourne un pointeur sur le d\u00e9but du morceau suivant. La fonction modifie la cha\u00eene pass\u00e9e en param\u00e8tre en ins\u00e9rant des '\\0' \u00e0 la place des d\u00e9limiteurs.

    char s[] = \"mais,ou,est,donc,or,ni,car\";\nchar *token = strtok(s, \",\");\nwhile (token != NULL) {\n    printf(\"%s\\n\", token);\n    token = strtok(NULL, \",\");\n}\n
    ", "tags": ["strtok", "NULL"]}, {"location": "course-c/35-libraries/standard-library/#strspn-et-strcspn", "title": "strspn et strcspn", "text": "

    Les fonctions strspn et strcspn permettent de calculer la longueur du pr\u00e9fixe d'une cha\u00eene qui contient ou ne contient pas certains caract\u00e8res. Leur prototype est\u2009:

    size_t strspn(const char *s, const char *accept);\nsize_t strcspn(const char *s, const char *reject);\n

    Une utilisation typique de strspn est de valider une cha\u00eene de caract\u00e8res. Par exemple, pour valider un nombre entier\u2009:

    char *s = \"12345\";\nif (strspn(s, \"0123456789\") == strlen(s)) {\n    printf(\"La cha\u00eene %s est un nombre entier\\n\", s);\n}\n

    Inversement, strcspn permet de valider une cha\u00eene de caract\u00e8res qui ne contient pas certains caract\u00e8res. Par exemple pour aider Georges Perec \u00e0 \u00e9crire un lipogramme sans la lettre e :

    char *s = \"La disparition\";\nif (strcspn(s, \"e\") == strlen(s)) {\n    printf(\"La cha\u00eene %s ne contient pas la lettre 'e'\\n\", s);\n}\n
    ", "tags": ["strspn", "strcspn"]}, {"location": "course-c/35-libraries/standard-library/#strpbrk", "title": "strpbrk", "text": "

    La fonction strpbrk permet de rechercher un caract\u00e8re dans une liste de caract\u00e8res. Son prototype est\u2009:

    char *strpbrk(const char *s, const char *accept);\n

    Elle retourne un pointeur sur le premier caract\u00e8re de s qui appartient \u00e0 accept. Par exemple, pour chercher les op\u00e9rateurs utilis\u00e9s dans une cha\u00eene de caract\u00e8res\u2009:

    char *s = \"1 + 2 * 4 + 8\";\n\nwhile (s = strpbrk(s, \"+-*/%%\")) {\n    printf(\"Op\u00e9rateur trouv\u00e9 : %c\\n\", *s);\n    s++;\n}\n
    ", "tags": ["strpbrk", "accept"]}, {"location": "course-c/35-libraries/standard-library/#strcoll-et-strxfrm", "title": "strcoll et strxfrm", "text": "

    Les fonctions strcoll et strxfrm permettent de comparer des cha\u00eenes de caract\u00e8res selon la locale. Elles sont l'abbr\u00e9viation de string collate o\u00f9 collate fait r\u00e9f\u00e9rence au tri ou \u00e0 l'ordre de classement des cha\u00eenes de caract\u00e8res en fonction des conventions locales. strxfrm est l'abbr\u00e9viation de string transform et permet de transformer une cha\u00eene de caract\u00e8res en une cha\u00eene de caract\u00e8res qui peut \u00eatre compar\u00e9e avec strcmp. Leur prototype est\u2009:

    int strcoll(const char *s1, const char *s2);\nsize_t strxfrm(char *dest, const char *src, size_t n);\n

    Ces deux fonctions sont utilis\u00e9es pour comparer des cha\u00eenes de caract\u00e8res en tenant compte des conventions de tri locales, qui peuvent varier d'une langue ou d'un jeu de caract\u00e8res \u00e0 un autre. Elles sont principalement utilis\u00e9es pour des op\u00e9rations de tri ou de comparaison dans des contextes o\u00f9 il est important de respecter l'ordre d\u00e9fini par les param\u00e8tres r\u00e9gionaux (locales). Ces fonction utlisent donc les param\u00e8tres r\u00e9gionaux d\u00e9finis par la fonction setlocale de la biblioth\u00e8que <locale.h>.

    Pourquoi ces deux fonctions \u00e9tranges\u2009? Comparer deux cha\u00eenes n'est pas facile surtout s'il y a des diacritiques. Selon la langue, les cha\u00eenes de caract\u00e8res ne sont pas forc\u00e9ment compar\u00e9es de la m\u00eame mani\u00e8re. En fran\u00e7ais on consid\u00e8re alphab\u00e9tiquement que \u00e9 est juste apr\u00e8s e dans l'alphabet, en anglais les deux lettres sont \u00e9quivalentes. En su\u00e9dois, les lettres \u00e5, \u00e4 sont plac\u00e9es apr\u00e8s le z, et les r\u00e8gles sont nombreuses.

    strxfrm permet de transformer une cha\u00eene de caract\u00e8res en une cha\u00eene de caract\u00e8res dite \u00ab\u2009collationn\u00e9e\u2009\u00bb qui peut \u00eatre compar\u00e9e ensuite rapidement avec strcmp. Cela peut \u00eatre utile lors de comparaison r\u00e9p\u00e9t\u00e9es.

    Notons que ces fonctions ne sont plus vraiment utilis\u00e9es car elles se limitent au jeux de caract\u00e8res ISO-8859, et le support Unicode est limit\u00e9. Pour une gestion correcte il vaut mieux faire appel \u00e0 des biblioth\u00e8ques plus sp\u00e9cialis\u00e9es comme ICU qui offre la fonction ucol_strcoll pour comparer des cha\u00eenes de caract\u00e8res Unicode.

    ", "tags": ["strxfrm", "strcmp", "ucol_strcoll", "strcoll", "ICU", "setlocale"]}, {"location": "course-c/35-libraries/standard-library/#strerror", "title": "strerror", "text": "

    La fonction strerror permet de r\u00e9cup\u00e9rer un message d'erreur associ\u00e9 \u00e0 un code d'erreur. Son prototype est\u2009:

    char *strerror(int errnum);\n

    Elle est utilis\u00e9e principalement pour afficher des messages d'erreur associ\u00e9s \u00e0 des codes d'erreur. Par exemple\u2009:

    FILE *f = fopen(\"file.txt\", \"r\");\nif (f == NULL) {\n    fprintf(stderr, \"Erreur lors de l'ouverture du fichier : %s\\n\",\n      strerror(errno));\n}\n

    ", "tags": ["strerror"]}, {"location": "course-c/35-libraries/standard-library/#tgmathh", "title": "<tgmath.h>", "text": "

    La biblioth\u00e8que <tgmath.h> est une biblioth\u00e8que de type g\u00e9n\u00e9rique qui permet de d\u00e9finir des fonctions math\u00e9matiques qui acceptent des arguments de diff\u00e9rents types. Par exemple, la fonction sqrt peut accepter un argument de type float, double ou long double.

    Il est courant de ne pas utiliser la bonne fonction math\u00e9matique pour un type donn\u00e9. Par exemple, on peut appeler sqrt avec un argument de type float alors que la fonction sqrtf est plus adapt\u00e9e peut entra\u00eener une perte de performance, l'inverse peut entra\u00eener une perte de pr\u00e9cision. La biblioth\u00e8que <tgmath.h> permet de r\u00e9soudre ce probl\u00e8me en d\u00e9finissant des fonctions math\u00e9matiques g\u00e9n\u00e9riques qui acceptent des arguments de diff\u00e9rents types.

    Cette g\u00e9n\u00e9ricit\u00e9 est permise \u00e0 l'aide du mot cl\u00e9 _Generic introduit en C11.

    La biblioth\u00e8que red\u00e9fini les fonctions math\u00e9matiques de la biblioth\u00e8que <math.h>, pour l'utiliser il suffit d'inclure l'en-t\u00eate <tgmath.h> \u00e0 la place de <math.h>. Par exemple, pour calculer la racine carr\u00e9e d'un nombre, on peut utiliser la fonction sqrt de la biblioth\u00e8que <tgmath.h> :

    ", "tags": ["sqrtf", "float", "_Generic", "sqrt", "double"]}, {"location": "course-c/35-libraries/standard-library/#threadsh", "title": "<threads.h>", "text": "

    La biblioth\u00e8que <threads.h> contient des fonctions pour cr\u00e9er et g\u00e9rer des threads. Les threads sont aussi nomm\u00e9s des processus l\u00e9gers qui partagent le m\u00eame espace m\u00e9moire. Un thread peut \u00eatre vu comme un sous-programme parall\u00e8le tournant dans le m\u00eame programme. Les fonctions offertes par le standard sont les suivantes\u2009:

    Fonctions sur les threads Fonction Description thrd_create Cr\u00e9e un nouveau thread thrd_exit Termine le thread thrd_join Attend la fin d'un thread thrd_sleep Met le thread en sommeil thrd_yield Passe la main \u00e0 un autre thread mtx_init Initialise un mutex mtx_lock Verrouille un mutex mtx_trylock Tente de verrouiller un mutex mtx_unlock D\u00e9verrouille un mutex mtx_destroy D\u00e9truit un mutex cnd_init Initialise une variable de condition cnd_signal Signale une variable de condition cnd_broadcast Signale toutes les variables de condition cnd_wait Attend une variable de condition cnd_destroy D\u00e9truit une variable de condition

    Pour plus de d\u00e9tails sur le fonctionnement des threads, vous pouvez consulter un cours sp\u00e9cialis\u00e9 sur la programmation concurrente.

    ", "tags": ["thrd_yield", "mtx_lock", "cnd_wait", "cnd_init", "thrd_join", "thrd_sleep", "mtx_trylock", "cnd_signal", "thrd_create", "mtx_destroy", "cnd_broadcast", "thrd_exit", "mtx_init", "cnd_destroy", "mtx_unlock"]}, {"location": "course-c/35-libraries/standard-library/#timeh", "title": "<time.h>", "text": "

    La biblioth\u00e8que <time.h> contient des fonctions pour lire et convertir des dates et heures. Les fonctions sont d\u00e9finies pour les dates et heures en secondes depuis le 1er janvier 1970.

    Fonctions sur les dates et heures Fonction Description time Temps \u00e9coul\u00e9 depuis le 1er janvier 1970 localtime Convertit le temps en heure locale gmtime Convertit le temps en heure UTC asctime Convertit le temps en cha\u00eene de caract\u00e8res ctime Convertit le temps en cha\u00eene de caract\u00e8res strftime Convertit le temps en cha\u00eene de caract\u00e8res mktime Convertit une structure en temps difftime Diff\u00e9rence entre deux temps clock Temps CPU utilis\u00e9 par le programme", "tags": ["time", "asctime", "ctime", "mktime", "strftime", "clock", "localtime", "gmtime", "difftime"]}, {"location": "course-c/35-libraries/standard-library/#stucture-tm", "title": "Stucture tm", "text": "

    Les fonctions de date et d'heure utilisent la structure tm pour repr\u00e9senter les dates et heures. La structure est d\u00e9finie comme suit\u2009:

    struct tm {\n    int tm_sec;   // Secondes (0-59)\n    int tm_min;   // Minutes (0-59)\n    int tm_hour;  // Heures (0-23)\n    int tm_mday;  // Jour du mois (1-31)\n    int tm_mon;   // Mois (0-11)\n    int tm_year;  // Ann\u00e9e - 1900\n    int tm_wday;  // Jour de la semaine (0-6, dimanche = 0)\n    int tm_yday;  // Jour de l'ann\u00e9e (0-365)\n    int tm_isdst; // Heure d'\u00e9t\u00e9 (0, 1, -1)\n};\n
    "}, {"location": "course-c/35-libraries/standard-library/#time", "title": "time", "text": "

    La fonction time permet de r\u00e9cup\u00e9rer le temps \u00e9coul\u00e9 depuis le 1er janvier 1970. Elle prend en param\u00e8tre un pointeur sur un time_t qui contiendra le temps \u00e9coul\u00e9. Ce dernier peut \u00eatre NULL si on ne souhaite pas r\u00e9cup\u00e9rer le temps. Le prototype de la fonction est le suivant\u2009:

    time_t time(time_t *t);\n

    Un exemple d'utilisation est le suivant\u2009:

    time_t t;\ntime(&t);\nprintf(\"Time since 1st January 1970 : %ld seconds\\n\", t);\n\n// Ou sans r\u00e9cup\u00e9rer le temps\nprintf(\"Time since 1st January 1970 : %ld seconds\\n\", time(NULL));\n

    Pourquoi le 1er janvier 1970\u2009? C'est une convention qui remonte aux premiers syst\u00e8mes Unix. Le temps est stock\u00e9 en secondes depuis cette date. C'est ce qu'on appelle le temps Unix ou temps POSIX.

    Probl\u00e8me de l'an 2038

    Le temps Unix est stock\u00e9 sur 32 bits. Cela signifie que le temps Unix ne pourra plus \u00eatre stock\u00e9 sur 32 bits \u00e0 partir du 19 janvier 2038. C'est ce qu'on appelle le bug de l'an 2038. Il est donc n\u00e9cessaire de passer \u00e0 un temps stock\u00e9 sur 64 bits pour \u00e9viter ce probl\u00e8me.

    La taille de time_t d\u00e9pend de l'impl\u00e9mentation. Sur la plupart des syst\u00e8mes, time_t est un alias pour long. Sur les syst\u00e8mes 64 bits, time_t est un alias pour long long.

    Pourquoi avoir deux moyen de retourner le temps\u2009? C'est une question de style. Certains pr\u00e9f\u00e8rent r\u00e9cup\u00e9rer le temps dans une variable, d'autres pr\u00e9f\u00e8rent le r\u00e9cup\u00e9rer directement sans variable interm\u00e9diaire.

    ", "tags": ["NULL", "time", "time_t", "long"]}, {"location": "course-c/35-libraries/standard-library/#localtime-et-gmtime", "title": "localtime et gmtime", "text": "

    Ces deux fonctions permettent de convertir un temps en heure locale ou en heure UTC. Leur prototype est le suivant\u2009:

    struct tm *localtime(const time_t *timep);\nstruct tm *gmtime(const time_t *timep);\n

    localtime se base sur les param\u00e8tres r\u00e9gionaux fix\u00e9s dans le syst\u00e8me pour d\u00e9terminer le fuseau horaire. Elle tient compte de l'ajustement pour l'heure d'\u00e9t\u00e9. gmtime en revanche se base sur le fuseau horaire UTC et ne tient pas compte de l'heure d'\u00e9t\u00e9.

    Un exemple d'utilisation est le suivant\u2009:

    time_t t;\ntime(&t);\nstruct tm *tm = localtime(&t);\nprintf(\"Heure locale : %d:%d:%d\\n\", tm->tm_hour, tm->tm_min, tm->tm_sec);\n
    ", "tags": ["gmtime", "localtime"]}, {"location": "course-c/35-libraries/standard-library/#asctime-et-ctime", "title": "asctime et ctime", "text": "

    Les fonctions asctime et ctime permettent de convertir un temps en cha\u00eene de caract\u00e8res. Leur prototype est le suivant\u2009:

    char *asctime(const struct tm *tm);\nchar *ctime(const time_t *timep);\n

    L'une prend en param\u00e8tre une structure tm et l'autre un temps. Elles retournent une cha\u00eene de caract\u00e8res repr\u00e9sentant le temps. Par exemple\u2009:

    time_t current_time = time(NULL);\nstruct tm *local_tm = localtime(&current_time);\nprintf(\"Heure locale : %s\", asctime(local_tm));\n// Affiche par exemple \"Sun Sep 16 01:03:52 1973\\n\" (locale en anglais)\n//          \"Dimanche 16 Septembre 01:03:52 1973\\n\" (locale en fran\u00e7ais)\n

    Pour afficher l'heure actuelle, on peut \u00e9galement utiliser ctime :

    time_t current_time = time(NULL);\nprintf(\"Heure locale : %s\", ctime(&current_time));\n
    ", "tags": ["ctime", "asctime"]}, {"location": "course-c/35-libraries/standard-library/#strftime", "title": "strftime", "text": "

    La fonction strftime permet de convertir un temps en cha\u00eene de caract\u00e8res en utilisant un format sp\u00e9cifique. Son prototype est le suivant\u2009:

    size_t strftime(char *s, size_t maxsize, const char *format,\n                const struct tm *tm);\n

    Elle prend en param\u00e8tre un pointeur sur une cha\u00eene de caract\u00e8res, la taille de la cha\u00eene, un format et une structure tm. Elle retourne le nombre de caract\u00e8res \u00e9crits dans la cha\u00eene.

    Format de strftime Format Description Exemple de sortie %A Nom complet du jour de la semaine \"Sunday\" %a Nom abr\u00e9g\u00e9 du jour de la semaine \"Sun\" %B Nom complet du mois \"January\" %b Nom abr\u00e9g\u00e9 du mois \"Jan\" %C Si\u00e8cle (les deux premiers chiffres de l'ann\u00e9e) \"20\" pour 2024 %d Jour du mois (01-31) \"17\" %D Date au format MM/DD/YY \"09/17/24\" %e Jour du mois (1-31, avec espace si un chiffre) \"17\" ou \" 7\" %F Date au format YYYY-MM-DD \"2024-09-17\" %H Heure (00-23, format 24 heures) \"14\" %I Heure (01-12, format 12 heures) \"02\" %j Jour de l'ann\u00e9e (001-366) \"260\" %k Heure (0-23, avec espace si chiffre) \" 2\" %l Heure (1-12, avec espace si chiffre, 12 heures) \" 2\" %M Minutes (00-59) \"05\" %m Mois (01-12) \"09\" %n Saut de ligne \"\\n\" %p Indicateur AM ou PM \"PM\" %P Indicateur am ou pm (minuscule) \"pm\" %r Heure au format 12 heures (hh:mm:ss AM/PM) \"02:05:45 PM\" %R Heure au format 24 heures (hh:mm) \"14:05\" %S Secondes (00-60) \"45\" %T Heure au format 24 heures (hh:mm:ss) \"14:05:45\" %u Num\u00e9ro du jour de la semaine (1-7, lundi = 1) \"2\" pour mardi %U Num\u00e9ro de la semaine (00-53, dimanche) \"37\" %W Num\u00e9ro de la semaine (00-53, lundi) \"37\" %V Num\u00e9ro de la semaine ISO 8601 (01-53, lundi) \"38\" %w Num\u00e9ro du jour de la semaine (0-6, dimanche = 0) \"0\" %x Repr\u00e9sentation locale de la date \"09/17/24\" %X Repr\u00e9sentation locale de l'heure \"14:05:45\" %y Ann\u00e9e (00-99, deux derniers chiffres) \"24\" %Y Ann\u00e9e (tous les chiffres) \"2024\" %z D\u00e9calage UTC (format +hhmm) \"+0200\" (UTC+2) %Z Nom du fuseau horaire \"CEST\" %% Symbole % \"%\"

    Queqlues notes sur les formats\u2009:

    • Le %S peut retourner 60 lorsqu'une seconde intercalaire est ins\u00e9r\u00e9e. Une second intercalaire est une seconde ajout\u00e9e \u00e0 la fin d'une minute pour compenser la rotation de la Terre.
    • La diff\u00e9rence entre %U et %W est que %U commence la semaine le dimanche alors que %W commence la semaine le lundi. Les am\u00e9ricains utilisent %U alors que les europ\u00e9ens utilisent %V.

    Voici un exemple d'utilisation\u2009:

    #include <stdio.h>\n#include <time.h>\n\nint main() {\n    // Obtenir l'heure locale\n    time_t current_time = time(NULL);\n    struct tm *local_tm = localtime(&current_time);\n\n    // Formater la date et l'heure\n    char buffer[100];\n    strftime(buffer, sizeof(buffer),\n      \"Aujourd'hui, c'est %A, %d %B %Y, et il est %T.\", local_tm);\n\n    printf(\"%s\\n\", buffer);\n}\n

    Il pourrait afficher\u2009:

    Aujourd'hui, c'est vendredi, 17 septembre 2024, et il est 14:05:45.\n

    ", "tags": ["strftime"]}, {"location": "course-c/35-libraries/standard-library/#ucharh", "title": "<uchar.h>", "text": "

    Apparue avec la norme C11, cette biblioth\u00e8que contient des fonctions pour g\u00e9rer les caract\u00e8res Unicode. Elle contient des fonctions pour convertir des caract\u00e8res en minuscules ou majuscules, pour tester si un caract\u00e8re est un chiffre, une lettre, etc.

    Un caract\u00e8re multi-octets (multibyte) est un caract\u00e8re qui n\u00e9cessite plus d'un octet pour \u00eatre stock\u00e9. Nous avons que la norme Unicode d\u00e9finit un jeu de caract\u00e8res universel qui peut \u00eatre repr\u00e9sent\u00e9 en binaire avec des caract\u00e8res de 8-bit (UTF-8). Cela permet de stocker th\u00e9oriquement jusqu'\u00e0 4 294 967 295 caract\u00e8res diff\u00e9rents.

    Le C \u00e9tant un langage ancien, il a \u00e9t\u00e9 con\u00e7u \u00e0 une \u00e9poque o\u00f9 seul la table ASCII existait. N\u00e9anmoins, certaines langues comme le chinois n\u00e9cessitaient plus de 256 caract\u00e8res. Pour cela, le C a introduit le concept de caract\u00e8res larges (wide characters) qui \u00e9taient initialement stock\u00e9s sur 16-bits (short). N\u00e9anmoins, avec l'arriv\u00e9e de l'Unicode, il n'est pas rare de trouver des caract\u00e8res qui n\u00e9cessitent 32-bits. Or, les wide-chars historiques du C ne sont que sur 16-bits (sous Windows) et 32-bits (sous Unix). Pour palier \u00e0 ce probl\u00e8me de portabilit\u00e9, la norme C11 a introduit la biblioth\u00e8que <uchar.h> qui permet de g\u00e9rer les caract\u00e8res Unicode convenablement.

    La biblioth\u00e8que d\u00e9finit deux types suppl\u00e9mentaires\u2009:

    char16_t; // 16-bit pour l'UTF-16\nchar32_t; // 32-bit pour l'UTF-32\n

    Contrairement \u00e0 UTF-8 qui est un encodage variable\u2009: de 1 \u00e0 4 bytes, l'UTF-16 et l'UTF-32 sont des encodages fixes (\u00e0 moins d'utiliser des surrogatges). Comme la plupart des syst\u00e8mes utilisent massivement l'UTF-8, la biblioth\u00e8que offre des fonctions de conversion entre les diff\u00e9rents encodages.

    Fonctions de conversion de caract\u00e8res Fonction Description c16rtomb Convertit un caract\u00e8re 16-bit en UTF-8 c32rtomb Convertit un caract\u00e8re 32-bit en UTF-8 mbrtoc16 Convertit un caract\u00e8re UTF-8 en 16-bit mbrtoc32 Convertit un caract\u00e8re UTF-8 en 32-bit c16rtowc Convertit un caract\u00e8re 16-bit en wide char c32rtowc Convertit un caract\u00e8re 32-bit en wide char wctoc16 Convertit un wide char en 16-bit wctoc32 Convertit un wide char en 32-bit

    Le standard C nomme mb (multibyte) pour se r\u00e9f\u00e9rer \u00e0 UTF-8.

    L'inconv\u00e9nient majeur d'UTF-8 c'est qu'il est impossible d'\u00e9diter un caract\u00e8re \u00e0 un endroit pr\u00e9cis sans devoir possiblement d\u00e9caler tous les caract\u00e8res suivants. Remplacer un e (stock\u00e9 sur 1 byte) par un \u00e9moji (stock\u00e9 sur 4 bytes), n\u00e9cessite de d\u00e9caler tout le texte de 3 bytes. Suivant la taille de la cha\u00eene cela peut \u00eatre fastidieux. C'est pourquoi l'UTF-32 est souvent utilis\u00e9 pour les traitements internes. On perd de la place m\u00e9moire car un texte en UTF-32 est jusqu'\u00e0 4 fois plus gros qu'en UTF-8, mais on gagne en temps de traitement car aucun d\u00e9clage n'est n\u00e9cessaire. En outre, le processeur \u00e9tant plus \u00e0 l'aise avec les donn\u00e9es align\u00e9es sur 32-bits, les traitements sont plus rapides.

    Prenons l'exemple d'un algorithme qui inverse une cha\u00eene de caract\u00e8res UTF-8 et affiche le r\u00e9sultat. Sans cette biblioth\u00e8que, il n'est pas trivial de le faire car les caract\u00e8res unicode peuvent \u00eatre stock\u00e9s sur plusieurs bytes. Ici on commence par convertir la cha\u00eene UTF-8 en UTF-32 pour avoir une cha\u00eene simple \u00e0 traiter, on inverse ensuite la cha\u00eene UTF-32, puis on la reconvertit en UTF-8 pour l'affichage. Une impl\u00e9mentation est donn\u00e9e dans la section algorithmes.

    ", "tags": ["c32rtomb", "mbrtoc16", "wctoc16", "short", "c16rtomb", "c32rtowc", "wctoc32", "multibyte", "c16rtowc", "mbrtoc32"]}, {"location": "course-c/35-libraries/standard-library/#wcharh", "title": "<wchar.h>", "text": "

    Cette biblioth\u00e8que suppl\u00e9mente d'autres biblioth\u00e8ques pour g\u00e9rer les caract\u00e8res larges (wide characters). Ci-dessous la table d'\u00e9quilvalence\u2009:

    Fonctions li\u00e9es aux caract\u00e8res larges Fonction Description \u00c9quivalent wcstol Convertit une cha\u00eene en long strtol wcstoul Convertit une cha\u00eene en unsigned long strtoul wcstoll Convertit une cha\u00eene en long long strtoll wcstoull Convertit une cha\u00eene en unsigned long long strtoull wcstof Convertit une cha\u00eene en float strtof wcstod Convertit une cha\u00eene en double strtod wcstold Convertit une cha\u00eene en long double strtold wcscpy Copie une cha\u00eene strcpy wcsncpy Copie une cha\u00eene strncpy wcscat Concat\u00e8ne deux cha\u00eenes strcat wcsncat Concat\u00e8ne deux cha\u00eenes strncat wcscmp Compare deux cha\u00eenes strcmp wcsncmp Compare deux cha\u00eenes strncmp wcschr Recherche un caract\u00e8re dans une cha\u00eene strchr wcsrchr Recherche un caract\u00e8re dans une cha\u00eene strrchr wcscoll Compare deux cha\u00eenes strcoll wcslen Calcule la longueur d'une cha\u00eene strlen wcsxfrm Transforme une cha\u00eene strxfrm wmemcmp Compare deux r\u00e9gions m\u00e9moire memcmp wmemchr Recherche un caract\u00e8re dans une r\u00e9gion m\u00e9moire memchr wmemcpy Copie une r\u00e9gion m\u00e9moire memcpy wmemmove Copie une r\u00e9gion m\u00e9moire memmove wmemset Remplit une r\u00e9gion m\u00e9moire memset

    ", "tags": ["strxfrm", "wcsncat", "wcstod", "wcstoul", "strchr", "wmemmove", "wcsncpy", "strrchr", "memmove", "wcscpy", "strtof", "wcstold", "wcscmp", "memset", "strtod", "wcscat", "memcmp", "wcstof", "wcscoll", "strncmp", "wcschr", "strcmp", "strncat", "strlen", "wcstoull", "strtoll", "memchr", "wcstoll", "strcoll", "wmemcpy", "memcpy", "strtoull", "wcsncmp", "strcat", "strncpy", "wcsrchr", "wcslen", "strtoul", "wmemchr", "wcstol", "strcpy", "strtol", "wmemcmp", "wmemset", "strtold", "wcsxfrm"]}, {"location": "course-c/35-libraries/standard-library/#wctypeh", "title": "<(w)ctype.h>", "text": "

    La biblioth\u00e8que <ctype.h> contient des fonctions pour tester et convertir des caract\u00e8res. Les fonctions sont d\u00e9finies pour les caract\u00e8res ASCII uniquement, elle ne s'applique pas aux caract\u00e8res Unicode, ni aux caract\u00e8res \u00e9tendus (au-del\u00e0 de 127). La biblioth\u00e8que <wctype.h> est similaire mais pour les caract\u00e8res larges (wide characters).

    Fonctions de test de caract\u00e8res ctype wctype Description isalnum iswalnum une lettre ou un chiffre isalpha iswalpha une lettre iscntrl iswcntrl un caract\u00e8re de commande isdigit iswdigit un chiffre d\u00e9cimal isgraph iswgraph un caract\u00e8re imprimable ou le blanc islower iswlower une lettre minuscule isprint iswprint un caract\u00e8re imprimable (pas le blanc) ispunct iswpunct un caract\u00e8re imprimable pas isalnum isspace iswspace un caract\u00e8re d'espace blanc isupper iswupper une lettre majuscule isxdigit iswxdigit un chiffre hexad\u00e9cimal

    En plus de ces fonctions de test, il existe des fonctions de conversion de casse d\u00e9finies dans <wctype.h> :

    Fonctions de conversion de casse Fonction Description towlower Convertit une lettre en minuscule towupper Convertit une lettre en majuscule towctrans Convertit un caract\u00e8re selon la locale LC_CTYPE", "tags": ["LC_CTYPE", "iscntrl", "iswprint", "isdigit", "iswalnum", "isxdigit", "iswxdigit", "isalpha", "isprint", "isalnum", "iswalpha", "iswspace", "iswgraph", "iswlower", "iswcntrl", "islower", "ispunct", "isupper", "isspace", "iswdigit", "towctrans", "iswpunct", "towlower", "iswupper", "isgraph", "towupper"]}, {"location": "course-c/35-libraries/standard-library/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Arc-cosinus

    La fonction Arc-Cosinus acos est-elle d\u00e9finie par le standard et dans quel fichier d'en-t\u00eate est-elle d\u00e9clar\u00e9e\u2009? Un fichier d'en-t\u00eate se termine avec l'extension .h.

    Solution

    En cherchant man acos header dans Google, on trouve que la fonction acos est d\u00e9finie dans le header <math.h>.

    Une autre solution est d'utiliser sous Linux la commande apropos:

    $ apropos acos\nacos (3)     - arc cosine function\nacosf (3)    - arc cosine function\nacosh (3)    - inverse hyperbolic cosine function\nacoshf (3)   - inverse hyperbolic cosine function\nacoshl (3)   - inverse hyperbolic cosine function\nacosl (3)    - arc cosine function\ncacos (3)    - complex arc cosine\ncacosf (3)   - complex arc cosine\ncacosh (3)   - complex arc hyperbolic cosine\ncacoshf (3)  - complex arc hyperbolic cosine\ncacoshl (3)  - complex arc hyperbolic cosine\ncacosl (3)   - complex arc cosine\n

    Le premier r\u00e9sultat permet ensuite de voir\u2009:

    $ man acos | head -10\nACOS(3)    Linux Programmer's Manual         ACOS(3)\n\nNAME\n    acos, acosf, acosl - arc cosine function\n\nSYNOPSIS\n    #include <math.h>\n\n    double acos(double x);\n    float acosf(float x);\n

    La r\u00e9ponse est donc <math.h>.

    Sous Windows avec Visual Studio, il suffit d'\u00e9crire acos dans un fichier source et d'appuyer sur F1. L'IDE redirige l'utilisateur sur l'aide Microsoft acos-acosf-acosl qui indique que le header source est <math.h>.

    Exercice 2\u2009: Date

    Lors du formatage d'une date, on y peut y lire %w, par quoi sera remplac\u00e9 ce token ?

    ", "tags": ["acos", "apropos"]}, {"location": "course-c/35-libraries/third-party-libraries/", "title": "Autres biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques tierces constituent des ensembles coh\u00e9rents de fonctions et de types de donn\u00e9es, con\u00e7us pour \u00eatre int\u00e9gr\u00e9s dans des programmes informatiques. Leur objectif est d'\u00e9tendre les fonctionnalit\u00e9s d'un projet sans n\u00e9cessiter la r\u00e9\u00e9criture de code existant. Provenant g\u00e9n\u00e9ralement de d\u00e9veloppeurs ou d'organisations externes, ces biblioth\u00e8ques permettent de gagner en efficacit\u00e9 tout en garantissant la robustesse et la maintenance du code.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#installation-de-bibliotheques-tierces", "title": "Installation de biblioth\u00e8ques tierces", "text": "

    Sous Linux, les biblioth\u00e8ques tierces sont g\u00e9n\u00e9ralement disponibles via les gestionnaires de paquets de la distribution. Il suffit de rechercher le nom de la biblioth\u00e8que et de l'installer \u00e0 l'aide de la commande appropri\u00e9e. Par exemple, pour installer la biblioth\u00e8que curl il faut ajouter le pr\u00e9fixe lib et le suffixe -dev . Le prefixe indique qu'il s'agit d'une biblioth\u00e8que et le suffixe indique qu'il s'agit de la version de d\u00e9veloppement incluant les fichiers d'en-t\u00eate n\u00e9cessaires pour la compilation.

    sudo apt-get install libcurl-dev\n

    Sous Windows, l'installation de biblioth\u00e8ques tierces peut \u00eatre plus complexe, car il n'existe pas de gestionnaire de paquets standardis\u00e9. Il est souvent n\u00e9cessaire de t\u00e9l\u00e9charger les fichiers d'installation depuis le site officiel de la biblioth\u00e8que et de suivre les instructions sp\u00e9cifiques \u00e0 chaque biblioth\u00e8que.

    ", "tags": ["lib", "curl"]}, {"location": "course-c/35-libraries/third-party-libraries/#libc-bibliotheque-standard-du-c", "title": "libc (Biblioth\u00e8que standard du C)", "text": "

    La biblioth\u00e8que standard du C, ou libc, est essentielle \u00e0 tout programme \u00e9crit en C. Elle regroupe les fonctions de base du langage, telles que la gestion de la m\u00e9moire, les op\u00e9rations sur les cha\u00eenes de caract\u00e8res, ou encore les entr\u00e9es/sorties. G\u00e9n\u00e9ralement incluse dans l'environnement de d\u00e9veloppement, elle est fournie par le syst\u00e8me d'exploitation ou le compilateur et repr\u00e9sente un pilier fondamental de l'\u00e9cosyst\u00e8me C.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#glib-bibliotheque-de-base-de-gnome", "title": "Glib (Biblioth\u00e8que de base de GNOME)", "text": "

    Glib est une biblioth\u00e8que polyvalente, d\u00e9velopp\u00e9e dans le cadre du projet GNOME, mais largement utilis\u00e9e au-del\u00e0. Elle propose une panoplie de fonctions pour la gestion de la m\u00e9moire, la manipulation des cha\u00eenes de caract\u00e8res, des structures de donn\u00e9es avanc\u00e9es telles que les listes, arbres, tables de hachage, ainsi que la gestion des signaux et des threads. Son architecture robuste en fait un choix privil\u00e9gi\u00e9 pour de nombreux projets open source en qu\u00eate de fiabilit\u00e9.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#securite", "title": "S\u00e9curit\u00e9", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#openssl", "title": "OpenSSL", "text": "

    OpenSSL est une biblioth\u00e8que incontournable dans le domaine de la cryptographie et de la s\u00e9curit\u00e9 des communications. Adopt\u00e9e par une multitude de projets, tant open source que commerciaux, elle propose des outils performants pour la gestion des certificats SSL, le chiffrement des donn\u00e9es, ainsi que la v\u00e9rification des signatures num\u00e9riques. Sa large adoption t\u00e9moigne de sa fiabilit\u00e9 et de sa capacit\u00e9 \u00e0 s\u00e9curiser les \u00e9changes en ligne.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#bases-de-donnees", "title": "Bases de donn\u00e9es", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#sqlite", "title": "SQLite", "text": "

    SQLite se distingue par son moteur de base de donn\u00e9es relationnelle compact et performant, largement utilis\u00e9 dans les applications mobiles et web. Malgr\u00e9 sa l\u00e9g\u00e8ret\u00e9, il offre des fonctionnalit\u00e9s avanc\u00e9es telles que la prise en charge des transactions ACID, des index, des vues, des d\u00e9clencheurs, et des fonctions SQL sophistiqu\u00e9es. Sa simplicit\u00e9 d'int\u00e9gration et son efficacit\u00e9 en font un choix privil\u00e9gi\u00e9 pour le stockage des donn\u00e9es dans des environnements vari\u00e9s.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#reseau", "title": "R\u00e9seau", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libcurl", "title": "Libcurl", "text": "

    Libcurl est une biblioth\u00e8que sp\u00e9cialis\u00e9e dans le transfert de donn\u00e9es sur le r\u00e9seau. Elle prend en charge une multitude de protocoles, dont HTTP, HTTPS, FTP, SFTP, et bien d'autres encore. Utilis\u00e9e tant par des projets open source que par des entreprises, elle est indispensable pour toute application n\u00e9cessitant des transferts de fichiers ou la communication avec des serveurs distants via des protocoles s\u00e9curis\u00e9s ou non.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#traitement-dimages", "title": "Traitement d'images", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#opencv", "title": "OpenCV", "text": "

    OpenCV est une biblioth\u00e8que de r\u00e9f\u00e9rence dans le domaine du traitement d'images et de vid\u00e9os. Utilis\u00e9e pour des applications aussi diverses que la reconnaissance faciale, la d\u00e9tection d'objets ou la vision par ordinateur, elle supporte une vaste gamme de formats d'images tels que JPEG, PNG, TIFF, et BMP. Sa richesse fonctionnelle la rend incontournable pour les projets n\u00e9cessitant une manipulation avanc\u00e9e des images.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libpng", "title": "LibPNG", "text": "

    LibPNG est sp\u00e9cialis\u00e9e dans la manipulation des images au format PNG. Elle permet de lire, \u00e9crire et modifier des images tout en g\u00e9rant les sp\u00e9cificit\u00e9s de ce format, telles que la transparence ou la compression. Sa robustesse et sa compatibilit\u00e9 avec les normes en font un choix solide pour toute application manipulant des images PNG.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libjpeg", "title": "LibJPEG", "text": "

    LibJPEG est la biblioth\u00e8que de r\u00e9f\u00e9rence pour le traitement des images JPEG. Elle prend en charge des op\u00e9rations complexes telles que la compression, la d\u00e9compression, et la manipulation d'images avec diff\u00e9rents niveaux de qualit\u00e9. Sa large adoption t\u00e9moigne de son efficacit\u00e9 dans la gestion de ce format d'image tr\u00e8s r\u00e9pandu.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#traitement-video", "title": "Traitement vid\u00e9o", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#ffmpeg", "title": "FFmpeg", "text": "

    FFmpeg est une biblioth\u00e8que puissante d\u00e9di\u00e9e au traitement des fichiers vid\u00e9o et audio. Elle supporte un large \u00e9ventail de formats tels que AVI, MP4, MOV, et MP3, et permet de r\u00e9aliser des op\u00e9rations complexes comme la conversion, le transcodage, ou encore la diffusion en direct. Sa polyvalence en fait un outil de choix pour les professionnels et les amateurs du multim\u00e9dia.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gestion-des-evenements", "title": "Gestion des \u00e9v\u00e9nements", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libevent", "title": "Libevent", "text": "

    Libevent propose des fonctionnalit\u00e9s avanc\u00e9es pour la gestion des \u00e9v\u00e9nements asynchrones, une composante essentielle des applications r\u00e9seau performantes. Elle permet de g\u00e9rer efficacement les connexions r\u00e9seau, les entr\u00e9es/sorties, et d'autres \u00e9v\u00e9nements syst\u00e8me critiques, garantissant ainsi une r\u00e9activit\u00e9 optimale.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libuv", "title": "Libuv", "text": "

    Libuv est une autre biblioth\u00e8que sp\u00e9cialis\u00e9e dans la gestion des \u00e9v\u00e9nements asynchrones, souvent utilis\u00e9e en conjonction avec Node.js. Elle se distingue par sa capacit\u00e9 \u00e0 g\u00e9rer les connexions r\u00e9seau et les entr\u00e9es/sorties de mani\u00e8re non bloquante, assurant des performances \u00e9lev\u00e9es dans les applications n\u00e9cessitant une gestion efficace de nombreux \u00e9v\u00e9nements simultan\u00e9s.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gestion-de-la-memoire", "title": "Gestion de la m\u00e9moire", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#jemalloc", "title": "Jemalloc", "text": "

    Jemalloc est r\u00e9put\u00e9e pour ses performances en mati\u00e8re de gestion de la m\u00e9moire, notamment dans les applications o\u00f9 la gestion efficace des allocations est cruciale. Elle propose des fonctionnalit\u00e9s avanc\u00e9es comme la fragmentation r\u00e9duite, les statistiques de m\u00e9moire d\u00e9taill\u00e9es, et les profils de m\u00e9moire, ce qui en fait un choix privil\u00e9gi\u00e9 pour les syst\u00e8mes \u00e0 haute performance.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#compression", "title": "Compression", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#zlib", "title": "Zlib", "text": "

    Zlib est une biblioth\u00e8que de compression polyvalente, largement utilis\u00e9e pour compresser et d\u00e9compresser des fichiers, flux de donn\u00e9es ou archives. Elle impl\u00e9mente les algorithmes DEFLATE, GZIP et ZLIB, offrant un excellent compromis entre vitesse et taux de compression.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#liblzma", "title": "LibLZMA", "text": "

    LibLZMA se sp\u00e9cialise dans la compression au format LZMA, r\u00e9put\u00e9 pour son taux de compression \u00e9lev\u00e9. Elle est utilis\u00e9e dans des contextes o\u00f9 l'efficacit\u00e9 du stockage et la r\u00e9duction de la taille des donn\u00e9es sont primordiales, tout en maintenant des performances \u00e9lev\u00e9es en mati\u00e8re de d\u00e9compression.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#serialisation", "title": "S\u00e9rialisation", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libyaml", "title": "LibYAML", "text": "

    LibYAML offre des outils robustes pour la s\u00e9rialisation et la d\u00e9s\u00e9rialisation de donn\u00e9es au format YAML. Utilis\u00e9e pour manipuler des configurations ou des donn\u00e9es structur\u00e9es, elle g\u00e8re efficacement les diff\u00e9rents \u00e9l\u00e9ments de ce format, tels que les s\u00e9quences, les scalaires, et les paires cl\u00e9-valeur.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libxml2", "title": "LibXML2", "text": "

    LibXML2 est une biblioth\u00e8que puissante pour le traitement des documents XML, supportant les normes et standards associ\u00e9s comme XSLT, XPath, ou XML Schema. Elle est couramment utilis\u00e9e pour l'analyse, la validation et la g\u00e9n\u00e9ration de documents XML dans des environnements o\u00f9 la pr\u00e9cision et la conformit\u00e9 aux standards sont cruciales.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#utilitaires", "title": "Utilitaires", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#pcre-perl-compatible-regular-expressions", "title": "PCRE (Perl Compatible Regular Expressions)", "text": "

    PCRE est la r\u00e9f\u00e9rence pour le traitement des expressions r\u00e9guli\u00e8res compatibles avec Perl. Elle est utilis\u00e9e pour effectuer des recherches complexes, des remplacements, ou pour valider des cha\u00eenes de texte selon des motifs avanc\u00e9s, offrant ainsi une flexibilit\u00e9 in\u00e9gal\u00e9e dans la manipulation de texte.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gmp-gnu-multiple-precision-arithmetic-library", "title": "GMP (GNU Multiple Precision Arithmetic Library)", "text": "

    GMP est sp\u00e9cialis\u00e9e dans les calculs arithm\u00e9tiques \u00e0 pr\u00e9cision arbitraire, indispensable dans des domaines exigeants comme la cryptographie ou les calculs scientifiques. Elle permet des op\u00e9rations sur des entiers, rationnels et flottants avec une pr\u00e9cision que les biblioth\u00e8ques standards ne peuvent atteindre.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#ncurses", "title": "ncurses", "text": "

    ncurses est la biblioth\u00e8que par excellence pour la cr\u00e9ation d'interfaces utilisateur en mode texte dans les environnements de terminal. Elle propose des outils pour g\u00e9rer les fen\u00eatres, les couleurs, les panneaux, ainsi que des fonctionnalit\u00e9s avanc\u00e9es comme la capture des touches de fonction et de contr\u00f4le.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#posix-c-library", "title": "POSIX C Library", "text": "

    Le standard C ne d\u00e9finit que le minimum vital et qui est valable sur toutes les architectures pour autant que la toolchain soit compatible C99. Il existe n\u00e9anmoins toute une collection d'autres fonctions manquantes\u2009:

    • La communication entre les processus (deux programmes qui souhaitent communiquer entre eux)

    • <sys/socket.h>

    • <sharedmemory.h>

    • La communication sur le r\u00e9seau e.g. internet

    • <sys/socket.h>

    • <arpa/inet.h>
    • <net/if.h>

    • Les t\u00e2ches

    • <thread.h>

    • Les traductions de cha\u00eenes p.ex. fran\u00e7ais vers anglais

    • <iconv.h>

    • Les fonctions avanc\u00e9es de recherche de texte

    • <regex.h>

    • Le log centralis\u00e9 des messages (d'erreur)

    • <syslog.h>

    Toutes ces biblioth\u00e8ques additionnelles ne sont pas n\u00e9cessairement disponibles sur votre ordinateur ou pour le syst\u00e8me cible, surtout si vous convoitez une application bare-metal. Elles d\u00e9pendent grandement du syst\u00e8me d'exploitation utilis\u00e9, mais une tentative de normalisation existe et se nomme POSIX (ISO/IEC 9945).

    G\u00e9n\u00e9ralement la vaste majorit\u00e9 des distributions Linux et Unix sont compatibles avec le standard POSIX et les biblioth\u00e8ques ci-dessus seront disponibles \u00e0 moins que vous ne visiez une architecture diff\u00e9rente de celle sur laquelle s'ex\u00e9cute votre compilateur.

    Le support POSIX sous Windows (Win32) n'est malheureusement que partiel et il n'est pas standardis\u00e9.

    Un point d'entr\u00e9e de l'API POSIX est la biblioth\u00e8que <unistd.h>.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gnu-glibc", "title": "GNU GLIBC", "text": "

    La biblioth\u00e8que portable GNULIB est la biblioth\u00e8que standard r\u00e9f\u00e9renc\u00e9e sous Linux par libc6.

    ", "tags": ["bibliotheque", "libc6"]}, {"location": "course-c/35-libraries/third-party-libraries/#windows-c-library", "title": "Windows C library", "text": "

    La biblioth\u00e8que Windows Windoes API offre une interface au syst\u00e8me de fichier, au registre Windows, aux imprimantes, \u00e0 l'interface de fen\u00eatrage, \u00e0 la console et au r\u00e9seau.

    L'acc\u00e8s \u00e0 cet API est offert par un unique point d'entr\u00e9e windows.h qui regroupe certains en-t\u00eates standards (<stdarg.h>, <string.h>, ...), mais pas tous (\ud83d\ude14) ainsi que les en-t\u00eates sp\u00e9cifiques \u00e0 Windows tels que\u2009:

    <winreg.h>

    Pour l'acc\u00e8s au registre Windows

    <wincon.h>

    L'acc\u00e8s \u00e0 la console

    La documentation est disponible en ligne depuis le site de Microsoft, mais n'est malheureusement pas compl\u00e8te et souvent il est difficile de savoir sur quel site trouver la bonne version de la bonne documentation. Par exemple, il n'y a aucune documentation claire de LSTATUS pour la fonction RegCreateKeyExW permettant de cr\u00e9er une entr\u00e9e dans la base de registre.

    Un bon point d'entr\u00e9e est le Microsoft API and reference catalog.

    Quelques observations\u2009:

    • Officiellement Windows est compatible avec C89 (ANSI C) (c.f. C Language Reference)
    • L'API Windows n'est pas officiellement compatible avec C99, mais elle s'en approche, il n'y pas ou peu de documents expliquant les diff\u00e9rences.
    • Microsoft n'a aucune priorit\u00e9 pour d\u00e9velopper son support C, il se focalise davantage sur C++ et C#, c'est pourquoi certains \u00e9l\u00e9ments du langage ne sont pas ou peu document\u00e9s.
    • Les types standards Windows diff\u00e9rent de ceux propos\u00e9s par C99. Par exemple, LONG32 remplace int32_t.
    ", "tags": ["windows", "LSTATUS", "int32_t", "api", "LONG32", "windows.h"]}, {"location": "course-c/40-algorithms/", "title": "Algorithmes", "text": "

    Les algorithmes constituent l'essence m\u00eame de l'informatique. Ces ensembles d'instructions, d\u00e9finis avec rigueur et pr\u00e9cision, offrent des solutions claires \u00e0 une multitude de probl\u00e8mes, allant du tri d'une simple liste de nombres \u00e0 la d\u00e9termination du chemin le plus court entre deux points. Chaque algorithme, par sa structure et sa logique, permet d'accomplir des t\u00e2ches vari\u00e9es avec une efficacit\u00e9 souvent surprenante.

    Ce chapitre se propose d'explorer les fondements des algorithmes, en s'attardant sur des aspects cruciaux qui sous-tendent leur conception et leur utilisation. Nous d\u00e9buterons par une analyse de la complexit\u00e9 algorithmique, un outil indispensable pour \u00e9valuer et comparer la performance des diff\u00e9rents algorithmes. Cette notion de complexit\u00e9, qu'elle soit en termes de temps ou d'espace, est primordiale pour juger de l'efficacit\u00e9 d'une solution face \u00e0 un probl\u00e8me donn\u00e9.

    La r\u00e9cursion, technique puissante mais exigeante, fera l'objet de notre deuxi\u00e8me section. En effet, si elle permet de r\u00e9soudre des probl\u00e8mes en les d\u00e9composant en sous-probl\u00e8mes plus simples, elle n'est pas sans risques\u2009: mal ma\u00eetris\u00e9e, elle peut entra\u00eener des consommations excessives de ressources, compromettant ainsi l'efficacit\u00e9 de l'algorithme. Nous examinerons donc en d\u00e9tail les m\u00e9thodes pour analyser et optimiser la complexit\u00e9 des algorithmes r\u00e9cursifs.

    Nous aborderons ensuite les algorithmes de recherche et de tri, deux cat\u00e9gories parmi les plus fr\u00e9quemment rencontr\u00e9es en informatique. La recherche, qu'elle soit lin\u00e9aire ou binaire, et le tri, sous ses diff\u00e9rentes formes, repr\u00e9sentent des op\u00e9rations fondamentales dont la ma\u00eetrise est indispensable pour tout informaticien. Nous en d\u00e9taillerons les m\u00e9canismes, en soulignant les avantages et les inconv\u00e9nients de chaque approche.

    Enfin, nous conclurons par l'examen de quelques algorithmes classiques, embl\u00e9matiques de leur domaine, et dont l'application d\u00e9passe souvent le cadre purement th\u00e9orique. Leur \u00e9tude permettra d'illustrer la port\u00e9e pratique des concepts abord\u00e9s et d'ancrer ces derniers dans la r\u00e9alit\u00e9 des probl\u00e8mes rencontr\u00e9s quotidiennement.

    Ce chapitre vous guidera vers une compr\u00e9hension approfondie des algorithmes, en vous donnant les cl\u00e9s pour concevoir des solutions \u00e9l\u00e9gantes et efficaces \u00e0 des probl\u00e8mes complexes. Que vous soyez novice en la mati\u00e8re ou que vous cherchiez \u00e0 affiner vos comp\u00e9tences, vous trouverez ici de quoi enrichir votre savoir et affronter les d\u00e9fis de l'informatique moderne avec une approche m\u00e9thodique et optimis\u00e9e.

    "}, {"location": "course-c/40-algorithms/automata/", "title": "Automates finis", "text": "

    Dans le contexte de l'algorithmique, il est int\u00e9ressant de d'aborder les automates finis. Les automates finis sont des machines abstraites qui peuvent \u00eatre dans un nombre fini d'\u00e9tats. Ils sont utilis\u00e9s pour mod\u00e9liser des syst\u00e8mes de transitions d'\u00e9tats tr\u00e8s utile dans l'\u00e9laboration d'algorithmes notament pour le traitement de langage naturel, la reconnaissance de formes, o\u00f9 l'analyse de donn\u00e9es.

    Hi\u00e9rarchie des automates

    Nous avions abord\u00e9 en introduction de cet ouvrage la machine de Turing, ce mod\u00e8le th\u00e9orique permettant de r\u00e9aliser n'importe quel algorithme. Bien que la tr\u00e8s grande majorit\u00e9 des langages de programmation sont dit Turing-complet, il est parfois plus int\u00e9ressant d'utiliser des automates finis pour des probl\u00e8mes sp\u00e9cifiques car il existe une r\u00e8gle en informatique qui dit qu'il est g\u00e9n\u00e9ralement optimum d'aligner la complexit\u00e9 d'un langage avec la complexit\u00e9 du probl\u00e8me \u00e0 r\u00e9soudre. L'id\u00e9e sous-jacente est que si un probl\u00e8me a une structure ou une complexit\u00e9 faible, il est g\u00e9n\u00e9ralement plus efficace de le r\u00e9soudre avec un mod\u00e8le computationnel ou un langage de programmation dont la complexit\u00e9 est align\u00e9e avec celle du probl\u00e8me. Utiliser un outil plus puissant ou plus complexe que n\u00e9cessaire peut entra\u00eener des inefficacit\u00e9s, des erreurs, ou des solutions surdimensionn\u00e9es.

    On rencontre les automates finis dans de tr\u00e8s nombreux domaines, du distributeur automatique de boisson aux ascenceurs en passant par les feux de circulation.

    Contrairement \u00e0 un syst\u00e8me turing-complete, un automate fini ne peut pas r\u00e9aliser n'importe quel algorithme. Il est limit\u00e9 par sa structure et son nombre fini d'\u00e9tats. Voici l'exemple d'un automate fini simple qui mod\u00e9lise un ascenseur\u2009:

    Ascenseur

    Ces \u00e9tats peuvent \u00e9galement \u00eatre repr\u00e9sent\u00e9s par un tableau de transition\u2009:

    \u00c9tat actuel Entr\u00e9e \u00c9tat suivant Sortie Arr\u00eat\u00e9 Ouvrir porte Porte ouverte La porte s'ouvre Arr\u00eat\u00e9 Appel En mouvement Se d\u00e9place \u00e0 l'\u00e9tage En mouvement Arr\u00eat Porte ouverte Arriv\u00e9 \u00e0 l'\u00e9tage, la porte s'ouvre En mouvement Arriv\u00e9 Arr\u00eat\u00e9 Arrive \u00e0 l'\u00e9tage Porte ouverte Fermer porte Arr\u00eat\u00e9 La porte se ferme Porte ouverte Appel En mouvement La porte se ferme, se d\u00e9place \u00e0 l'\u00e9tage"}, {"location": "course-c/40-algorithms/automata/#la-chevre-et-le-chou", "title": "La ch\u00e8vre et le chou", "text": "

    Un autre exemple classique d'automate fini est le probl\u00e8me de la ch\u00e8vre, du chou et du loup. Il s'agit d'un probl\u00e8me de logique dans lequel un fermier doit transporter une ch\u00e8vre, un chou et un loup d'une rive \u00e0 l'autre d'une rivi\u00e8re. Le fermier ne peut transporter qu'un seul \u00e9l\u00e9ment \u00e0 la fois et ne peut pas laisser la ch\u00e8vre seule avec le loup ou le chou seul avec la ch\u00e8vre. Le probl\u00e8me est de trouver une s\u00e9quence de d\u00e9placements qui permet de transporter les trois \u00e9l\u00e9ments d'une rive \u00e0 l'autre sans enfreindre les r\u00e8gles.

    Ce probl\u00e8me peut \u00eatre mod\u00e9lis\u00e9 par un automate fini. Chaque \u00e9tat est nomm\u00e9 selon les \u00e9l\u00e9ments pr\u00e9sents sur la rive d'arriv\u00e9e. Par exemple l'\u00e9tat CFS signifie que la ch\u00e8vre, le loup et le fermier sont sur la rive d'arriv\u00e9e. Les transitions indiquent les \u00e9l\u00e9ments qui transitent d'une rive \u00e0 l'autre. Notons que l'on appelle S le chou (comme Salade), pour \u00e9viter la confusion avec la ch\u00e8vre. Voici le diagramme d'\u00e9tats\u2009:

    La ch\u00e8vre, le loup et le chou

    "}, {"location": "course-c/40-algorithms/automata/#implementation", "title": "Impl\u00e9mentation", "text": "

    G\u00e9n\u00e9ralement on commence par d\u00e9finir les \u00e9tats et les transitions de l'automate. On ajoute volontairement une entr\u00e9e mangl\u00e9e COUNT pour facilement conna\u00eetre le nombre d'\u00e9l\u00e9ments dans l'\u00e9num\u00e9ration.

    typedef enum {\n    STATE_IDLE,\n    STATE_MOVING,\n    STATE_DOOR_OPEN,\n    _STATE_COUNT\n} State;\n\ntypedef enum {\n    INPUT_OPEN_DOOR,\n    INPUT_CALL,\n    INPUT_STOP,\n    INPUT_ARRIVAL,\n    INPUT_CLOSE_DOOR,\n    _INPUT_COUNT\n} Input;\n\nvoid openDoor() { printf(\"The door opens.\\n\"); }\nvoid closeDoor() { printf(\"The door closes.\\n\"); }\nvoid moveElevator() { printf(\"The elevator is moving.\\n\"); }\nvoid stopElevator() { printf(\"The elevator stops.\\n\"); }\n

    L'objectif est de pouvoir \u00e9mettre des transitions simplement\u2009:

    int main() {\n    State currentState = STATE_IDLE;\n\n    // Exemples de transitions\n    currentState = transition(currentState, INPUT_CALL);\n    currentState = transition(currentState, INPUT_STOP);\n    currentState = transition(currentState, INPUT_CLOSE_DOOR);\n    currentState = transition(currentState, INPUT_CALL);\n    currentState = transition(currentState, INPUT_ARRIVAL);\n}\n

    Les automates finis peuvent \u00eatre impl\u00e9ment\u00e9s de diff\u00e9rentes mani\u00e8res. Une solution courante est d'utiliser un switch-case. Voici l'exemple en C\u2009:

    State transition(State currentState, Input input) {\n    switch (currentState) {\n        case STATE_IDLE:\n            switch (input) {\n                case INPUT_OPEN_DOOR:\n                    openDoor();\n                    return STATE_DOOR_OPEN;\n                case INPUT_CALL:\n                    moveElevator();\n                    return STATE_MOVING;\n                default:\n                    return currentState;\n            }\n\n        case STATE_MOVING:\n            switch (input) {\n                case INPUT_STOP:\n                    stopElevator();\n                    openDoor();\n                    return STATE_DOOR_OPEN;\n                case INPUT_ARRIVAL:\n                    stopElevator();\n                    return STATE_IDLE;\n                default:\n                    return currentState;\n            }\n\n        case STATE_DOOR_OPEN:\n            switch (input) {\n                case INPUT_CLOSE_DOOR:\n                    closeDoor();\n                    return STATE_IDLE;\n                case INPUT_CALL:\n                    closeDoor();\n                    moveElevator();\n                    return STATE_MOVING;\n                default:\n                    return currentState;\n            }\n\n        default:\n            return currentState;\n    }\n}\n

    Une autre approche est d'utiliser un table de transition. Cela permet de s\u00e9parer la logique de transition de l'impl\u00e9mentation. Voici un exemple\u2009:

    typedef void (*ActionFunction)();\n\ntypedef struct transition {\n    State nextState;\n    ActionFunction action;\n} Transition;\n\nTransition stateTransitionTable[_STATE_COUNT][_INPUT_COUNT] = {\n    [STATE_IDLE][INPUT_OPEN_DOOR] = {STATE_DOOR_OPEN, openDoor},\n    [STATE_IDLE][INPUT_CALL] = {STATE_MOVING, moveElevator},\n    [STATE_IDLE][INPUT_STOP] = {STATE_IDLE, NULL},\n    [STATE_IDLE][INPUT_ARRIVAL] = {STATE_IDLE, NULL},\n    [STATE_IDLE][INPUT_CLOSE_DOOR] = {STATE_IDLE, NULL},\n\n    [STATE_MOVING][INPUT_OPEN_DOOR] = {STATE_MOVING, NULL},\n    [STATE_MOVING][INPUT_CALL] = {STATE_MOVING, NULL},\n    [STATE_MOVING][INPUT_STOP] = {STATE_DOOR_OPEN, openDoor},\n    [STATE_MOVING][INPUT_ARRIVAL] = {STATE_IDLE, stopElevator},\n    [STATE_MOVING][INPUT_CLOSE_DOOR] = {STATE_MOVING, NULL},\n\n    [STATE_DOOR_OPEN][INPUT_OPEN_DOOR] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_CALL] = {STATE_MOVING, moveElevator},\n    [STATE_DOOR_OPEN][INPUT_STOP] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_ARRIVAL] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_CLOSE_DOOR] = {STATE_IDLE, closeDoor},\n};\n\nState transition(State currentState, Input input) {\n    Transition transition = stateTransitionTable[currentState][input];\n    if (transition.action != NULL) {\n        transition.action();\n    }\n    return transition.nextState;\n}\n
    ", "tags": ["COUNT"]}, {"location": "course-c/40-algorithms/automata/#dfa-et-nfa", "title": "DFA et NFA", "text": "

    Il existe deux types d'automates finis\u2009: les automates finis d\u00e9terministes (DFA) et les automates finis non d\u00e9terministes (NFA). Les DFA sont des automates finis dont les transitions sont d\u00e9termin\u00e9es par l'\u00e9tat actuel et l'entr\u00e9e. Les NFA sont des automates finis dont les transitions peuvent \u00eatre non d\u00e9terministes, c'est-\u00e0-dire qu'il peut y avoir plusieurs transitions possibles pour un \u00e9tat et une entr\u00e9e donn\u00e9s.

    Pour un ordinateur, il est plus facile de traiter un automate fini d\u00e9terministe qu'un automate fini non d\u00e9terministe. Cependant, les NFA sont plus puissants que les DFA, car ils peuvent repr\u00e9senter des langages plus complexes. Heureusement il existe des algorithmes pour convertir un NFA en un DFA \u00e9quivalent, mais cela peut entra\u00eener une explosion de l'espace d'\u00e9tat.

    Un cas typique d'utilisation de ces diagrammes d'\u00e9tats sont les expressions r\u00e9guli\u00e8res. Pour rappel, une expression r\u00e9guli\u00e8re est une cha\u00eene de caract\u00e8re qui d\u00e9crit un ensemble de cha\u00eenes de caract\u00e8res. On les utilises pour rechercher des motifs complexes.

    L'objectif n'est pas de rentrer dans le d\u00e9tail mais de vous donner un aper\u00e7u de ce qu'il est possible de faire avec les automates finis. Admettons que l'on souhaite rechercher des motifs de texte contenant les lettres A et B. On peut avoir plusieurs combinaisons par exemple on recherche soit un A ou un B /A|B/ ou bien un A suivi d'un B /AB/. En utilisant l'\u00e9toile de Kleene * on peut \u00e9galement rechercher z\u00e9ro ou plusieurs occurences de A: /A*/.

    Dans ces exemples, \u00e0 chaque \u00e9tat un caract\u00e8re est captur\u00e9 sur la cha\u00eene de recherche (le curseur avance d'un caract\u00e8re) :

    DFA simples

    \u00c0 partir de ces \u00e9l\u00e9ments simples, il est possible de construire une expression plus complexe comme /Z|X(X|Y)*/ : la lettre Z seule ou bien X suivi de A ou B z\u00e9ro ou plusieurs fois. Pour le cas de figure de l'\u00e9toile de Kleene, il n'est pas \u00e9vident de constuire cette expression. Pour r\u00e9soudre ce probl\u00e8me on introduit la notion de transition epsilon \u03b5 qui permet de passer d'un \u00e9tat \u00e0 un autre sans consommer de caract\u00e8re. On peut en mettre autant que l'on veut\u2009:

    NFA avec epsilon

    Ce diagramme d'\u00e9tat peut \u00eatre repr\u00e9sent\u00e9 sous forme de tableau de transition\u2009:

    Table de transition X Y Z \u03b5 >1 2 - 5 1 2 - - - 2,3,5 3 4 4 - 3 4 - - - 4,5 (5) - - - 5

    Dans le but de simplifier on peut utiliser l'algorithme de Thompson-McNaughton-Yamada pour convertir l'automate non d\u00e9terministe en un automate d\u00e9terministe. Pour ce faire on utilise la table de transition suivante qui utilise les epsilon-closures :

    Table de transition simplifi\u00e9e X\u03b5* Y\u03b5* Z\u03b5* >1 2,3,(5) - (5) 2,3,(5) 3,4,(5) 3,4,(5) - (5) - - - 3,4,(5) 3,4,(5) 3,4,(5) -

    Ceci nous donne un automate fini d\u00e9terministe (DFA):

    DFA

    "}, {"location": "course-c/40-algorithms/automata/#implementation_1", "title": "Impl\u00e9mentation", "text": "

    Si nous souhaitons impl\u00e9menter un moteur d'expression r\u00e9guli\u00e8res simple qui prend en compte les \u00e9l\u00e9ments suivants\u2009:

    • . : n'importe quel caract\u00e8re
    • | : ou
    • ( ) : groupe
    • * : z\u00e9ro ou plusieurs occurences
    • + : une ou plusieurs occurences
    • Les autres caract\u00e8res sont litt\u00e9raux

    Les \u00e9tapes de l'algorithmes sont les suivants\u2009:

    1. Convertir l'expression d'entr\u00e9e en un NFA
    2. Appliquer l'algorithme de Thompson pour convertir le NFA en un DFA
    3. Utiliser le DFA pour rechercher les motifs dans le texte
    "}, {"location": "course-c/40-algorithms/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/40-algorithms/introduction/#introduction", "title": "Introduction", "text": "... conduire par ordre mes pens\u00e9es, en commen\u00e7ant par les objets les plus simples et les plus ais\u00e9s \u00e0 conna\u00eetre, pour monter peu \u00e0 peu, comme par degr\u00e9s, jusques \u00e0 la connaissance des plus compos\u00e9s; et supposant m\u00eame de l'ordre entre ceux qui ne se pr\u00e9c\u00e8dent point naturellement les uns les autres.Ren\u00e9 Descartes, Discours de la m\u00e9thode

    L'algorithmique est le domaine scientifique qui \u00e9tudie les algorithmes, une suite finie et non ambigu\u00eb d'op\u00e9rations ou d'instructions permettant de r\u00e9soudre un probl\u00e8me ou de traiter des donn\u00e9es.

    Un algorithme peut \u00eatre \u00e9galement consid\u00e9r\u00e9 comme \u00e9tant n'importe quelle s\u00e9quence d'op\u00e9rations pouvant \u00eatre simul\u00e9es par un syst\u00e8me Turing-complet. Un syst\u00e8me est d\u00e9clar\u00e9 Turing-complet s'il peut simuler n'importe quelle machine de Turing. For heureusement, le langage C est Turing-complet puisqu'il poss\u00e8de tous les ingr\u00e9dients n\u00e9cessaires \u00e0 la simulation de ces machines, soit compter, comparer, lire, \u00e9crire...

    Dans le cas qui concerne cet ouvrage, un algorithme est une recette exprim\u00e9e en une liste d'instructions et permettant de r\u00e9soudre un probl\u00e8me informatique. Cette recette contient \u00e0 peu de choses pr\u00e8s les \u00e9l\u00e9ments programmatiques que nous avons d\u00e9j\u00e0 entre aper\u00e7us\u2009: des structures de contr\u00f4le, des variables, etc.

    G\u00e9n\u00e9ralement un algorithme peut \u00eatre exprim\u00e9 graphiquement en utilisant un organigramme (flowchart) ou un structogramme (Nassi-Shneiderman diagram) afin de s'affranchir du langage de programmation cible.

    La conception aussi appel\u00e9e Architecture logicielle est l'art de penser un programme avant son impl\u00e9mentation. La phase de conception fait bien souvent appel \u00e0 des algorithmes.

    Pour \u00eatre qualifi\u00e9es d'algorithmes, certaines propri\u00e9t\u00e9s doivent \u00eatre respect\u00e9es\u2009:

    1. Entr\u00e9es, un algorithme doit poss\u00e9der 0 ou plus d'entr\u00e9es en provenance de l'ext\u00e9rieur de l'algorithme.
    2. Sorties, un algorithme doit poss\u00e9der au moins une sortie.
    3. Rigueur, chaque \u00e9tape d'un algorithme doit \u00eatre claire et bien d\u00e9finie.
    4. Finitude, un algorithme doit comporter un nombre fini d'\u00e9tapes.
    5. R\u00e9p\u00e9table, un algorithme doit fournir un r\u00e9sultat r\u00e9p\u00e9table.
    "}, {"location": "course-c/40-algorithms/introduction/#complexite-algorithmique", "title": "Complexit\u00e9 Algorithmique", "text": "

    Il est souvent utile de savoir quelle est la performance d'un algorithme afin de le comparer \u00e0 un autre algorithme \u00e9quivalent. On peut s'int\u00e9resser \u00e0 deux indicateurs\u2009:

    • La complexit\u00e9 en temps : combien de temps CPU consomme un algorithme pour s'ex\u00e9cuter.
    • La complexit\u00e9 en m\u00e9moire : combien de m\u00e9moire tampon consomme un algorithme pour s'ex\u00e9cuter.

    Bien \u00e9videmment, la complexit\u00e9 d'un algorithme d\u00e9pend des donn\u00e9es en entr\u00e9e. Par exemple si on vous donne \u00e0 corriger un examen de 100 copies, et le protocol de correction associ\u00e9, votre temps de travail d\u00e9pendra du nombre de copies \u00e0 corriger, je sais de quoi je parle...

    La complexit\u00e9 en temps et en m\u00e9moire d'un algorithme est souvent exprim\u00e9e en utilisant la notation en O (Big O notation). Cette notation a \u00e9t\u00e9 introduite par le math\u00e9maticien et informaticien allemand Paul Bachmann en 1984 dans son ouvrage Analytische Zahlentheorie. Cependant, c'est le math\u00e9maticien austro-hongrois Edmund Landau qui a popularis\u00e9 cette notation dans le contexte de la th\u00e9orie des nombres.

    En substance, la complexit\u00e9 en temps d'un algorithme qui demanderait 10 \u00e9tapes pour \u00eatre r\u00e9solu s'\u00e9crirait\u2009:

    \\[ O(10) \\]

    Un algorithme qui ferait une recherche dichotomique sur un tableau de \\(n\\) \u00e9l\u00e9ments \u00e0 une complexit\u00e9 \\(O(log(n))\\). La recherche dichotomique c'est comme chercher un mot dans un dictionnaire. Vous ouvrez le dictionnaire \u00e0 la moiti\u00e9, si le mot est avant, vous r\u00e9p\u00e9tez l'op\u00e9ration sur la premi\u00e8re moiti\u00e9, sinon sur la seconde. Vous r\u00e9p\u00e9tez l'op\u00e9ration jusqu'\u00e0 trouver le mot. \u00c0 chaque \u00e9tape vous \u00e9liminez la moiti\u00e9 restante des mots du dictionnaire. Si le dictionnaire contient 100'000 mots, vous aurez trouv\u00e9 le mot en un nombre d'\u00e9tapes \u00e9quivalent \u00e0\u2009:

    \\[ \\log_2(100'000) = 16.6 \\]

    C'est bien plus rapide que de parcourir le dictionnaire de mani\u00e8re lin\u00e9aire, en tournant les pages une \u00e0 une.

    Prenons un algoritme qui prend un certain temps pour s'ex\u00e9cuter. L'algorithme \\(A\\) prend \\(f(n)\\) unit\u00e9s de temps pour une entr\u00e9e de taille \\(n\\). On peut dire que \\(A\\) est en \\(O(g(n))\\) si \\(f(n) \\leq c \\cdot g(n)\\) pour tout \\(n \\geq n_0\\), o\u00f9 \\(c\\) est une constante et \\(n_0\\) est un entier.

    On pourrait faire le raccourcis que la complexit\u00e9 algorithmique mesure le nombre d'op\u00e9rations \u00e9l\u00e9mentaires n\u00e9cessaires pour r\u00e9soudre un probl\u00e8me. Cepender, la complexit\u00e9 algorithmique ne mesure ni le temps en secondes ni le nombre d'op\u00e9rations \u00e9l\u00e9mentaires. Elle mesure la croissance du nombre d'op\u00e9rations \u00e9l\u00e9mentaires en fonction de la taille de l'entr\u00e9e. C'est tr\u00e8s diff\u00e9rent. Prenons l'exemple suivant\u2009:

    for (int i, i < n, i++) {\n    printf(\"%d \", a[i]);\n}\n\nfor (int i, i < m, i++) {\n    printf(\"%d \", b[i]);\n}\n

    On it\u00e8re sur deux tableaux a et b respectivement de taille n et m. La complexit\u00e9 de cet algorithme est \\(O(n + m)\\). Maintenant consid\u00e9rons l'exemple suivant\u2009:

    for (int i, i < n, i++) {\n    for (int j, j < m, j++) {\n        printf(\"%d \", a[i] + b[j]);\n    }\n}\n

    Cette fois-ci on observe une imbrication de deux boucles for. La complexit\u00e9 de cet algorithme est \\(O(n \\cdot m)\\). On peut dire que la complexit\u00e9 de cet algorithme est quadratique.

    Enfin, imaginons que nous devons appliquer 10 op\u00e9rations sur chaque \u00e9l\u00e9ment avant l'affichage\u2009:

    for (int i, i < n, i++) {\n    for (int j, j < m, j++) {\n        for (int k, k < 10, k++) {\n            printf(\"%d \", a[i] + b[j]);\n        }\n    }\n}\n

    Na\u00efvement on pourrait penser que la complexit\u00e9 de cet algorithme est \\(O(10 \\cdot n \\cdot m) = O(n \\cdot m)\\). Cependant, la complexit\u00e9 de cet algorithme est \\(O(10 \\cdot n \\cdot m) = O(n \\cdot m)\\). En effet, la constante 10 n'a pas d'impact sur la croissance de la complexit\u00e9 de l'algorithme. On peut dire que la complexit\u00e9 de cet algorithme est quadratique.

    ", "tags": ["for"]}, {"location": "course-c/40-algorithms/introduction/#suppression-des-constantes", "title": "Suppression des constantes", "text": "

    Dans la notation Big O, les constantes sont ignor\u00e9es. Par exemple, si un algorithme prend \\(5n^2 + 3n + 2\\) unit\u00e9s de temps pour une entr\u00e9e de taille \\(n\\), on dira que cet algorithme est en \\(O(n^2)\\). En effet, pour de grandes valeurs de \\(n\\), la croissance de \\(5n^2 + 3n + 2\\) est domin\u00e9e par \\(n^2\\).

    Suppression des constantes Complexit\u00e9 Complexit\u00e9 sans les constantes \\(O(5n^2)\\) \\(O(n^2)\\) \\(O(3n)\\) \\(O(n)\\) \\(O(2)\\) \\(O(1)\\) \\(O(n^2 + n)\\) \\(O(n^2)\\) \\(O(5n^2 + 3n + 2)\\) \\(O(n^2)\\) \\(O(n + \\log(n))\\) \\(O(n)\\)

    Cette r\u00e8gle est valable our l'addition, mais pour la multiplication, les constantes ne sont pas ignor\u00e9es. Dans le cas de \\(O(n\\cdot\\log(n))\\) la valeur de \\(\\log(n)\\) est importante.

    "}, {"location": "course-c/40-algorithms/introduction/#indicateurs-de-landau", "title": "Indicateurs de Landau", "text": "

    Il existe diff\u00e9rents indicateurs de Landau\u2009:

    Notation Big O (O)

    Utilis\u00e9e pour d\u00e9crire une borne sup\u00e9rieure asymptotique. Cela signifie qu'une fonction \\( f(n) \\) est en \\( O(g(n)) \\) s'il existe des constantes \\( c > 0 \\) et \\( n_0 \\) telles que \\( f(n) \\leq c \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une limite sup\u00e9rieure sur le comportement de \\( f(n) \\) pour de grandes valeurs de \\( n \\).

    Big O est souvent utilis\u00e9e pour d\u00e9crire le pire cas.

    Notation Big Omega (\u03a9)

    Utilis\u00e9e pour d\u00e9crire une borne inf\u00e9rieure asymptotique. Cela signifie qu'une fonction \\( f(n) \\) est en \\( \\Omega(g(n)) \\) s'il existe des constantes \\( c > 0 \\) et \\( n_0 \\) telles que \\( f(n) \\geq c \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une limite inf\u00e9rieure sur le comportement de \\( f(n) \\) pour de grandes valeurs de \\( n \\).

    Big Omega est souvent utilis\u00e9e pour d\u00e9crire le meilleur cas.

    Notation Big Theta (\u0398)

    Utilis\u00e9e pour d\u00e9crire une borne asymptotique stricte. Cela signifie qu'une fonction \\( f(n) \\) est en \\( \\Theta(g(n)) \\) s'il existe des constantes \\( c_1, c_2 > 0 \\) et \\( n_0 \\) telles que \\( c_1 \\cdot g(n) \\leq f(n) \\leq c_2 \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une approximation asymptotique exacte de \\( f(n) \\).

    Big Theta est utilis\u00e9e pour d\u00e9crire un comportement asymptotique pr\u00e9cis, souvent interpr\u00e9t\u00e9 comme le cas moyen.

    Exercice 1\u2009: Quelle Complexit\u00e9\u2009?

    Quelle est la complexit\u00e9 en temps de cet algorithme\u2009?

    void foo(int a[], int n) {\n    int sum = 0, product = 1;\n    for (int i = 0; i < n; i++) {\n        sum += a[i];\n    }\n    for (int i = 0; i < n; i++) {\n        product *= a[i];\n    }\n    printf(\"Sum: %d, Product: %d\\n\", sum, product);\n}\n
    • \\(O(2n)\\)
    • \\(O(n)\\)
    • \\(O(n^2)\\)
    • \\(O(n \\log(n))\\)

    Identifier les valeurs paires et impaires

    \u00c0 titre d'exemple, le programme suivant qui se charge de remplacer les valeurs paires d'un tableau par un \\(0\\) et les valeurs impaires par un \\(1\\) \u00e0 une complexit\u00e9 en temps de \\(O(n)\\) o\u00f9 n est le nombre d'\u00e9l\u00e9ments du tableau.

    void discriminate(int* array, size_t length)\n{\n    for (size_t i = 0; i < length; i++)\n    {\n        array[i] %= 2;\n    }\n}\n

    Somme des entiers

    Si on vous demande d'\u00e9crire un algorithme permettant de conna\u00eetre la somme des entiers de \\(1\\) \u00e0 \\(n\\), vous pourriez \u00e9crire un algorithme en \\(O(n)\\) :

    CPython
    int sum(int n) {\n    int sum = 0;\n    for (int i = 1; i <= n; i++) {\n        sum += i;\n    }\n    return sum;\n}\n
    def sum(n):\n    return [i for i in range(1, n + 1)]\n

    Ensuite vous vous posez la question de savoir si vous pouvez faire mieux. En effet, il existe une formule math\u00e9matique permettant de calculer la somme des entiers de \\(1\\) \u00e0 \\(n\\) :

    \\[ \\sum_{i=1}^{n} i = \\frac{n \\cdot (n + 1)}{2} \\]

    Cette formule est en \\(O(1)\\).

    CPython
    int sum(int n) {\n    return n * (n + 1) / 2;\n}\n
    def sum(n):\n    return n * (n + 1) // 2\n

    D'une mani\u00e8re g\u00e9n\u00e9rale, la plupart des algorithmes que l'ing\u00e9nieur \u00e9crira appartiendront \u00e0 ces cat\u00e9gories exprim\u00e9es du meilleur au plus mauvais\u2009:

    Temps pour diff\u00e9rentes complexit\u00e9s d'algorithmes Complexit\u00e9 \\(n = 100000\\) i7 (100'000 MIPS) \\(O(log(n))\\) 11 0.11 ns \\(O(n)\\) 100'000 1 us \\(O(n log(n))\\) 1'100'000 11 us \\(O(n^2)\\) 10'000'000'000 100 ms (un battement de cil) \\(O(2^n)\\) tr\u00e8s tr\u00e8s grand Le soleil devenu g\u00e9ante rouge aura ingurgit\u00e9 la terre $O(n\u2009!)` trop trop grand La galaxie ne sera plus que poussi\u00e8re

    Les diff\u00e9rentes complexit\u00e9s peuvent \u00eatre r\u00e9sum\u00e9es sur la figure suivante\u2009:

    Diff\u00e9rentes complexit\u00e9s d'algorithmes

    Un algorithme en \\(O(n^2)\\), doit \u00e9veiller chez le d\u00e9veloppeur la volont\u00e9 de voir s'il n'y a pas moyen d'optimiser l'algorithme en r\u00e9duisant sa complexit\u00e9, souvent on s'aper\u00e7oit qu'un algorithme peut \u00eatre optimis\u00e9 et s'int\u00e9resser \u00e0 sa complexit\u00e9 est un excellent point d'entr\u00e9e.

    Attention toutefois \u00e0 ne pas mal \u00e9valuer la complexit\u00e9 d'un algorithme. Voyons par exemple les deux algorithmes suivants\u2009:

    int min = MAX_INT;\nint max = MIN_INT;\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n    if (array[i] < min) {\n        min = array[i];\n    }\n    if (array[i] > min) {\n        max = array[i];\n    }\n}\n
    int min = MAX_INT;\nint max = MIN_INT;\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++)\n{\n    if (array[i] < min) {\n        min = array[i];\n    }\n}\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++)\n{\n    if (array[i] > min) {\n        max = array[i];\n    }\n}\n

    Exercice 2\u2009: Triangle \u00e9vanescent

    Quel serait l'algorithme permettant d'afficher\u2009:

    *****\n****\n***\n**\n*\n

    et dont la taille peut varier\u2009?

    Exercice 3\u2009: L'entier manquant

    On vous donne un gros fichier de 3'000'000'000 entiers positifs 32-bits, il vous faut g\u00e9n\u00e9rer un entier qui n'est pas dans la liste. Le hic, c'est que vous n'avez que 500 MiB de m\u00e9moire de travail. Quel algorithme proposez-vous\u2009?

    Une fois le travail termin\u00e9, votre manager vient vous voir pour vous annoncer que le cahier des charges a \u00e9t\u00e9 modifi\u00e9. Le client dit qu'il n'a que 10 MiB. Pensez-vous pouvoir r\u00e9soudre le probl\u00e8me quand m\u00eame\u2009?

    "}, {"location": "course-c/40-algorithms/introduction/#diagrammes-visuels", "title": "Diagrammes visuels", "text": "
    • Diagrammes en flux
    • Structogrammes
    • Diagramme d'activit\u00e9s
    • Machines d'\u00e9tats (UML state machine)
    • BPMN (Business Process Model and Notation)
    "}, {"location": "course-c/40-algorithms/introduction/#type-dalgorithmes", "title": "Type d'algorithmes", "text": ""}, {"location": "course-c/40-algorithms/introduction/#algorithmes-en-ligne-incremental", "title": "Algorithmes en ligne (incr\u00e9mental)", "text": "

    Un algorithme incr\u00e9mental ou online est un algorithme qui peut s'ex\u00e9cuter sur un flux de donn\u00e9es continu en entr\u00e9e. C'est-\u00e0-dire qu'il est en mesure de prendre des d\u00e9cisions sans avoir besoin d'une visibilit\u00e9 compl\u00e8te sur le set de donn\u00e9es.

    Un exemple typique est le probl\u00e8me de la secr\u00e9taire. On souhaite recruter une nouvelle secr\u00e9taire et le recruteur voit d\u00e9filer les candidats. Il doit d\u00e9cider \u00e0 chaque entretien s'il engage ou non le candidat et ne peut pas attendre la fin du processus d'entretiens pour obtenir le score attribu\u00e9 \u00e0 chaque candidat. Il ne peut comparer la performance de l'un qu'\u00e0 celle de deux d\u00e9j\u00e0 entrevus. L'objectif est de trouver la meilleure strat\u00e9gie.

    La solution \u00e0 ce probl\u00e8me est de laisser passer 37% des candidats sans les engager. Ceci correspond \u00e0 une proportion de \\(1/e\\). Ensuite il suffit d'attendre un ou une candidate meilleure que tous ceux/celles du premier \u00e9chantillon.

    "}, {"location": "course-c/40-algorithms/introduction/#methodes-de-resolution", "title": "M\u00e9thodes de r\u00e9solution", "text": "

    Il existe deux m\u00e9thodes quasiment infaillibles pour r\u00e9soudre un probl\u00e8me complexe\u2009:

    1. La r\u00e9duction du probl\u00e8me en sous-probl\u00e8mes plus simples.
    2. Le raisonnement par l'inverse.

    La r\u00e9diction aussi appel\u00e9e Divide and Conquer consiste \u00e0 diviser un probl\u00e8me en sous-probl\u00e8mes plus simples, les r\u00e9soudre et combiner les solutions pour obtenir la solution du probl\u00e8me initial. C'est une m\u00e9thode tr\u00e8s utilis\u00e9e en informatique pour r\u00e9soudre des probl\u00e8mes complexes. Par exemple, le tri fusion, le tri rapide, la recherche dichotomique sont des m\u00e9thodes de r\u00e9solution de probl\u00e8mes bas\u00e9es sur la r\u00e9duction.

    Le raisonnement par l'inverse consiste \u00e0 partir de la solution pour remonter au probl\u00e8me en posant des hypoth\u00e8ses. Par exemple, si vous avez un probl\u00e8me de recherche de chemin, vous pouvez partir de la destination pour remonter au point de d\u00e9part. C'est une m\u00e9thode tr\u00e8s utilis\u00e9e en math\u00e9matiques pour r\u00e9soudre des probl\u00e8mes complexes. Par exemple, la m\u00e9thode de Newton pour trouver les racines d'une fonction est bas\u00e9e sur le raisonnement par l'inverse.

    "}, {"location": "course-c/40-algorithms/introduction/#les-problemes-np-et-np-complet", "title": "Les Probl\u00e8mes NP et NP-Complet", "text": "

    La th\u00e9orie de la complexit\u00e9 computationnelle est une branche fascinante de l'informatique th\u00e9orique qui s'int\u00e9resse \u00e0 la classification des probl\u00e8mes en fonction de la difficult\u00e9 \u00e0 les r\u00e9soudre. Au c\u0153ur de cette th\u00e9orie se trouvent les classes de probl\u00e8mes NP et NP-complet, concepts essentiels pour comprendre pourquoi certains probl\u00e8mes sont si difficiles \u00e0 r\u00e9soudre. Pour les n\u00e9ophytes, ces termes peuvent sembler abstraits, mais leur compr\u00e9hension r\u00e9v\u00e8le des enjeux fondamentaux pour la science et l'ing\u00e9nierie modernes.

    "}, {"location": "course-c/40-algorithms/introduction/#quest-ce-quun-probleme-np", "title": "Qu'est-ce qu'un probl\u00e8me NP\u2009?", "text": "

    Pour saisir ce qu'est un probl\u00e8me NP, il faut d'abord comprendre la notion de temps de calcul. Le temps de calcul d'un algorithme correspond au nombre d'\u00e9tapes n\u00e9cessaires pour r\u00e9soudre un probl\u00e8me, en fonction de la taille de l'entr\u00e9e. Par exemple, trier une liste de mille \u00e9l\u00e9ments prend g\u00e9n\u00e9ralement plus de temps que trier une liste de dix \u00e9l\u00e9ments.

    Un probl\u00e8me est dit \u00ab\u2009en P\u2009\u00bb (pour \u00ab\u2009Polynomial\u2009\u00bb) si une solution peut \u00eatre trouv\u00e9e par un algorithme en un temps raisonnable, c'est-\u00e0-dire en un temps qui cro\u00eet de mani\u00e8re polynomiale avec la taille de l'entr\u00e9e. Par exemple, l'algorithme qui permet de trier une liste en utilisant un tri par insertion appartient \u00e0 la classe P car son temps de calcul est proportionnel au carr\u00e9 de la taille de la liste.

    Cependant, certains probl\u00e8mes semblent beaucoup plus complexes \u00e0 r\u00e9soudre. Un probl\u00e8me est dit \u00ab\u2009en NP\u2009\u00bb (pour \u00ab\u2009Nondeterministic Polynomial time\u2009\u00bb) si, bien qu'il puisse \u00eatre difficile de trouver une solution, il est relativement facile de v\u00e9rifier la validit\u00e9 d'une solution donn\u00e9e. Autrement dit, si on vous fournit une solution suppos\u00e9e correcte \u00e0 un probl\u00e8me en NP, vous pouvez v\u00e9rifier cette solution en temps polynomial. Le terme \u00ab\u2009nondeterministic\u2009\u00bb fait r\u00e9f\u00e9rence \u00e0 l'id\u00e9e th\u00e9orique d'une machine qui pourrait essayer simultan\u00e9ment toutes les solutions possibles et choisir la bonne.

    Un exemple classique de probl\u00e8me en NP est le probl\u00e8me du Knapsack (ou probl\u00e8me du sac \u00e0 dos). Ce probl\u00e8me consiste \u00e0 d\u00e9terminer quels objets, parmi une collection donn\u00e9e, doivent \u00eatre plac\u00e9s dans un sac \u00e0 dos de capacit\u00e9 limit\u00e9e de mani\u00e8re \u00e0 maximiser la valeur totale des objets. Trouver la meilleure combinaison d'objets \u00e0 mettre dans le sac peut \u00eatre tr\u00e8s difficile car le nombre de combinaisons possibles cro\u00eet de mani\u00e8re exponentielle avec le nombre d'objets. En revanche, si quelqu'un vous donne une combinaison pr\u00e9tendument optimale, vous pouvez rapidement v\u00e9rifier si elle respecte la capacit\u00e9 du sac et si sa valeur est maximale.

    "}, {"location": "course-c/40-algorithms/introduction/#les-problemes-np-complet", "title": "Les Probl\u00e8mes NP-Complet", "text": "

    Parmi les probl\u00e8mes en NP, certains sont particuli\u00e8rement redoutables\u2009: ce sont les probl\u00e8mes NP-complet. Un probl\u00e8me est dit NP-complet s'il est \u00e0 la fois en NP et au moins aussi difficile que tous les autres probl\u00e8mes en NP. En d'autres termes, si vous pouviez trouver une m\u00e9thode efficace pour r\u00e9soudre un probl\u00e8me NP-complet, alors vous pourriez utiliser cette m\u00e9thode pour r\u00e9soudre tous les autres probl\u00e8mes en NP.

    Le probl\u00e8me du Knapsack que nous avons mentionn\u00e9 plus t\u00f4t est un exemple de probl\u00e8me NP-complet. D'autres exemples bien connus incluent le probl\u00e8me du voyageur de commerce (TSP), o\u00f9 il s'agit de trouver le chemin le plus court pour visiter un ensemble de villes une fois et revenir au point de d\u00e9part, ou encore le probl\u00e8me de la coloration de graphe, qui consiste \u00e0 d\u00e9terminer le nombre minimum de couleurs n\u00e9cessaires pour colorier les sommets d'un graphe de mani\u00e8re que deux sommets adjacents n'aient pas la m\u00eame couleur.

    "}, {"location": "course-c/40-algorithms/introduction/#le-probleme-p-np", "title": "Le Probl\u00e8me P = NP", "text": "

    Le probl\u00e8me mill\u00e9naire P = NP, formul\u00e9 par Stephen Cook en 1971, est l'une des questions les plus c\u00e9l\u00e8bres et les plus importantes de la science informatique. Il se demande si tous les probl\u00e8mes qui peuvent \u00eatre v\u00e9rifi\u00e9s rapidement (c'est-\u00e0-dire en temps polynomial) peuvent \u00e9galement \u00eatre r\u00e9solus rapidement. En d'autres termes, P est-il \u00e9gal \u00e0 NP\u2009?

    Si P = NP, cela signifierait qu'il existe un algorithme rapide pour r\u00e9soudre chaque probl\u00e8me dont la solution peut \u00eatre rapidement v\u00e9rifi\u00e9e. Cela bouleverserait notre compr\u00e9hension de la complexit\u00e9 computationnelle et aurait des implications \u00e9normes dans de nombreux domaines, de la cryptographie \u00e0 la logistique. \u00c0 ce jour, personne n'a r\u00e9ussi \u00e0 prouver ou \u00e0 r\u00e9futer cette conjecture, et elle reste l'un des grands myst\u00e8res non r\u00e9solus de la science.

    Les probl\u00e8mes NP-complet sont essentiels car ils nous montrent les limites de ce que nous pouvons r\u00e9soudre efficacement avec des ordinateurs. Ils nous forcent \u00e0 accepter qu'il existe des probl\u00e8mes pour lesquels, en l'\u00e9tat actuel de nos connaissances, il n'existe pas de solution rapide, ce qui a des cons\u00e9quences pratiques. Par exemple, en cryptographie, la s\u00e9curit\u00e9 des syst\u00e8mes repose souvent sur l'hypoth\u00e8se que certains probl\u00e8mes sont difficiles \u00e0 r\u00e9soudre, ce qui les rend r\u00e9sistants aux attaques.

    En outre, comprendre ces probl\u00e8mes nous pousse \u00e0 d\u00e9velopper de nouvelles techniques pour les r\u00e9soudre ou les contourner. Parfois, cela signifie trouver des algorithmes approximatifs, qui fournissent des solutions proches de l'optimum en un temps raisonnable. D'autres fois, cela peut conduire \u00e0 l'innovation en mati\u00e8re de mat\u00e9riel informatique, comme l'exploration des ordinateurs quantiques, qui pourraient potentiellement r\u00e9soudre certains de ces probl\u00e8mes beaucoup plus rapidement que les ordinateurs classiques.

    "}, {"location": "course-c/40-algorithms/introduction/#conclusion", "title": "Conclusion", "text": "

    La classification des probl\u00e8mes en P, NP, et NP-complet est une pierre angulaire de l'informatique th\u00e9orique. Les probl\u00e8mes NP-complet, en particulier, repr\u00e9sentent certains des d\u00e9fis les plus redoutables auxquels nous sommes confront\u00e9s dans le domaine. Ils ne sont pas seulement des \u00e9nigmes abstraites pour les th\u00e9oriciens\u2009; ils ont des implications profondes et tr\u00e8s concr\u00e8tes pour la science, la technologie, et m\u00eame notre vie quotidienne. Comprendre ces concepts, c'est plonger au c\u0153ur de ce que signifie la complexit\u00e9 et la difficult\u00e9 dans le monde des algorithmes, et reconna\u00eetre les limites actuelles de ce que nous pouvons accomplir avec des machines de calcul.

    "}, {"location": "course-c/40-algorithms/introduction/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 4\u2009: Int\u00e9grateur de Kahan

    L'int\u00e9grateur de Kahan (Kahan summation algorithm) est une solution \u00e9l\u00e9gante pour pallier \u00e0 la limite de r\u00e9solution des types de donn\u00e9es.

    L'algorithme pseudo-code peut \u00eatre exprim\u00e9 comme\u2009:

    function kahan_sum(input)\n    var sum = 0.0\n    var c = 0.0\n    for i = 1 to input.length do\n        var y = input[i] - c\n        var t = sum + y\n        c = (t - sum) - y\n        sum = t\n    next i\n    return sum\n
    1. Impl\u00e9menter cet algorithme en C compte tenu du prototype\u2009:

      float kahan_sum(float value, float sum, float c);\n
    2. Expliquer comment fonctionne cet algorithme.

    3. Donner un exemple montrant l'avantage de cet algorithme sur une simple somme.

    Exercice 5\u2009: Robot aspirateur affam\u00e9

    Un robot aspirateur souhaite se rassasier et cherche le frigo, le probl\u00e8me c'est qu'il ne sait pas o\u00f9 il est. Elle serait la strat\u00e9gie de recherche du robot pour se rendre \u00e0 la cuisine\u2009?

    Le robot dispose de plusieurs fonctionnalit\u00e9s\u2009:

    • Avancer
    • Tourner \u00e0 droite de 90\u00b0
    • D\u00e9tection de sa position absolue p. ex. P5

    \u00c9laborer un algorithme de recherche.

        \u2502 A \u2502 B \u2502 C \u2502 D \u2502 E \u2502 F \u2502 G \u2502 H \u2502 I \u2502 J \u2502 K \u2502 L \u2502 M \u2502 O \u2502 P \u2502 Q \u2502\n\u2500\u2500\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n1 \u2503                     x \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503             F1: Frigo \u2503       \u2503               \u2503               \u2503\n2 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n3 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n4 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n5 \u2503       \u2503               \u2503       \u2503               \u2503      <--o     \u2503\n\u2500\u2500\u2503       \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501   \u2501\u2501\u2501\u2501\u2501\u252b       \u2503               \u2503     P5: Robot \u2503\n6 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n7 \u2503                       \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503                       \u2503       \u2503               \u2503               \u2503\n8 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501    \u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b\n9 \u2503                                                       \u2503       \u2503\n\u2500\u2500\u2503                                                       \u2503       \u2503\n10\u2503                                                               \u2503\n\u2500\u2500\u2503                                                               \u2503\n11\u2503                                                       \u2503       \u2503\n\u2500\u2500\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
    "}, {"location": "course-c/40-algorithms/recursion/", "title": "R\u00e9cursivit\u00e9", "text": "To understand what recursion is, you must first understand recursion.Internet

    La r\u00e9cursivit\u00e9 est une technique de programmation dans laquelle une fonction s'appelle elle-m\u00eame pour r\u00e9soudre un probl\u00e8me. Cela signifie que la fonction r\u00e9sout une partie du probl\u00e8me et appelle ensuite la fonction elle-m\u00eame pour r\u00e9soudre le reste du probl\u00e8me.

    La r\u00e9cursivit\u00e9 est utilis\u00e9e pour r\u00e9soudre des probl\u00e8mes qui peuvent \u00eatre d\u00e9compos\u00e9s en probl\u00e8mes plus petits de la m\u00eame nature. Par exemple, la factorielle d'un nombre est le produit de tous les entiers positifs inf\u00e9rieurs ou \u00e9gaux \u00e0 ce nombre. La factorielle d'un nombre n est n! = n * (n-1)!.

    Au chapitre sur les fonctions, nous avions donn\u00e9 l'exemple du calcul de la somme de la suite de Fibonacci jusqu'\u00e0 n :

    int fib(int n)\n{\n    int sum = 0;\n    int t1 = 0, t2 = 1;\n    int next_term;\n    for (int i = 1; i <= n; i++)\n    {\n        sum += t1;\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    return sum;\n}\n

    Il peut sembler plus logique de raisonner de fa\u00e7on r\u00e9cursive. Quelle que soit l'it\u00e9ration \u00e0 laquelle l'on soit, l'assertion suivante est valable\u2009:

    \\[fib(n) == fib(n - 1) + fib(n - 2)\\]

    Donc pourquoi ne pas r\u00e9\u00e9crire cette fonction en employant ce caract\u00e8re r\u00e9cursif\u2009?

    int fib(int n)\n{\n    if (n < 2) return 1;\n    return fib(n - 1) + fib(n - 2);\n}\n

    Le code est beaucoup plus simple \u00e0 \u00e9crire, et m\u00eame \u00e0 lire. N\u00e9anmoins cet algorithme est notoirement connu pour \u00eatre tr\u00e8s mauvais en termes de performance. Calculer fib(5) revient \u00e0 la cha\u00eene d'appel suivant.

    Cette cha\u00eene d'appel repr\u00e9sente le nombre de fois que fib est appel\u00e9 et \u00e0 quel niveau elle est appel\u00e9e. Par exemple fib(4) est appel\u00e9 dans fib(5) :

    %% Arbre d'appel de Fibonacci\ngraph TD\n\n5((\"fib(5)\")) --> 41((\"fib(4)\"))\n5 --> 31((\"fib(3)\"))\n\n41((\"fib(4)\")) --> 32((\"fib(3)\"))\n41 --> 21((\"fib(2)\"))\n\n21 --> 11((\"fib(1)\"))\n\n31 --> 22((\"fib(2)\"))\n31 --> 12((\"fib(1)\"))\n\n22 --> 13((\"fib(1)\"))\n32((\"fib(3)\")) --> 23((\"fib(2)\"))\n32 --> 14((\"fib(1)\"))\n\n23 --> 15((\"fib(1)\"))
    Arbre d'appel de Fibonacci

    Au final, fib(1) est appel\u00e9 5 fois, fib(2) 3 fois, fib(3) 2 fois, fib(4) et fib(5) 1 fois. Ce sont donc 12 appels \u00e0 la fonction fib pour calculer fib(5).

    Calcul Appels fib(1) 1 fib(2) 2 fib(3) 4 fib(4) 7 fib(5) 12 fib(6) 20 fib(7) 33 fib(8) 54 fib(9) 88 fib(10) 143 ... ... fib(30) 2'178'308 fib(40) 267'914'295 fib(50) 32'951'280'098 fib(100) 927'372'692'193'078'999'175

    Il s'agit de la suite A000071 de l'OEIS. On constate que le nombre d'appels est exponentiel. Pour fib(100) il faudra neuf cent vingt-sept quintillions trois cent soixante-douze quadrillions six cent quatre-vingt-douze trillions cent quatre-vingt-treize milliards soixante-dix-huit millions neuf cent quatre-vingt-dix-neuf mille cent soixante-quinze appels \u00e0 la fonction fib. Pour un processeur capable de calculer 100 GFLOPS (milliards d'op\u00e9rations par seconde), il faudra tout de m\u00eame 294 ans. C'est un peu long...

    La complexit\u00e9 algorithmique de cette fonction est dite \\(O(2^n)\\). C'est-\u00e0-dire que le nombre d'appels suit une relation exponentielle. La r\u00e9elle complexit\u00e9 est donn\u00e9e par la relation\u2009:

    En revanche, dans l'approche it\u00e9rative, on constate qu'une seule boucle for. C'est-\u00e0-dire qu'il faudra seulement 100 it\u00e9rations pour calculer la somme.

    G\u00e9n\u00e9ralement les algorithmes r\u00e9cursifs (s'appelant eux-m\u00eames) sont moins performants que les algorithmes it\u00e9ratifs (utilisant des boucles). N\u00e9anmoins il est parfois plus facile d'\u00e9crire un algorithme r\u00e9cursif.

    Notons que tout algorithme r\u00e9cursif peut \u00eatre \u00e9crit en un algorithme it\u00e9ratif, mais ce n'est pas toujours facile.

    ", "tags": ["fib", "for"]}, {"location": "course-c/40-algorithms/recursion/#les-tours-de-hanoi", "title": "Les tours de Hano\u00ef", "text": "

    Les tours de Hano\u00ef est un jeu de r\u00e9flexion invent\u00e9 par le math\u00e9maticien fran\u00e7ais \u00c9douard Lucas en 1889 et publi\u00e9 dans le tome 3 de ses R\u00e9cr\u00e9ations math\u00e9matiques. Le jeu est compos\u00e9 de trois tiges et d'un certain nombre de disques de diam\u00e8tres diff\u00e9rents qui peuvent \u00eatre empil\u00e9s sur une tige. Le but du jeu est de d\u00e9placer tous les disques d'une tige \u00e0 une autre, en respectant les r\u00e8gles suivantes\u2009:

    1. On ne peut d\u00e9placer qu'un seul disque \u00e0 la fois.
    2. Un disque ne peut \u00eatre plac\u00e9 que sur un disque plus grand que lui ou sur une tige vide.

    Tours de Hano\u00ef

    Ce probl\u00e8me se pr\u00eate tr\u00e8s bien \u00e0 une r\u00e9solution r\u00e9cursive. En effet, pour d\u00e9placer n disques de la tige A \u00e0 la tige C, il suffit de d\u00e9placer n-1 disques de la tige A \u00e0 la tige B, puis de d\u00e9placer le disque restant de la tige A \u00e0 la tige C, et enfin de d\u00e9placer les n-1 disques de la tige B \u00e0 la tige C.

    Algorithme R\u00e9cursifAlgorithme It\u00e9ratif
    #include <stdio.h>\n\nvoid hanoi(int n, char from, char to, char aux) {\n    if (n == 1) {\n        printf(\"D\u00e9placer le disque 1 de %c \u00e0 %c\\n\", from, to);\n        return;\n    }\n    hanoi(n - 1, from, aux, to);\n    printf(\"D\u00e9placer le disque %d de %c \u00e0 %c\\n\", n, from, to);\n    hanoi(n - 1, aux, to, from);\n}\n\nint main() {\n    int n = 3;\n    hanoi(n, 'A', 'C', 'B');\n}\n
    #include <stdio.h>\n#include <stdlib.h>\n\ntypedef struct {\n    int n;\n    char from;\n    char to;\n    char aux;\n    int stage;\n} Frame;\n\ntypedef struct {\n    Frame *frames;\n    int top;\n    int max_size;\n} Stack;\n\nvoid init_stack(Stack *stack, int max_size) {\n    stack->frames = (Frame *)malloc(max_size * sizeof(Frame));\n    stack->top = -1;\n    stack->max_size = max_size;\n}\n\nvoid push(Stack *stack, Frame frame) {\n    if (stack->top < stack->max_size - 1) {\n        stack->frames[++stack->top] = frame;\n    }\n}\n\nFrame pop(Stack *stack) {\n    if (stack->top >= 0) {\n        return stack->frames[stack->top--];\n    } else {\n        Frame empty = {0, '\\0', '\\0', '\\0', 0};\n        return empty;\n    }\n}\n\nint is_empty(Stack *stack) {\n    return stack->top == -1;\n}\n\nvoid hanoi_iterative(int n, char from, char to, char aux) {\n    Stack stack;\n    init_stack(&stack, 100);  // Assuming the stack size to be 100, adjust if needed\n\n    Frame initial_frame = {n, from, to, aux, 0};\n    push(&stack, initial_frame);\n\n    while (!is_empty(&stack)) {\n        Frame current_frame = pop(&stack);\n\n        switch (current_frame.stage) {\n            case 0:\n                if (current_frame.n == 1) {\n                    printf(\"D\u00e9placer le disque 1 de %c \u00e0 %c\\n\", current_frame.from, current_frame.to);\n                } else {\n                    current_frame.stage = 1;\n                    push(&stack, current_frame);\n\n                    Frame new_frame = {current_frame.n - 1, current_frame.from, current_frame.aux, current_frame.to, 0};\n                    push(&stack, new_frame);\n                }\n                break;\n\n            case 1:\n                printf(\"D\u00e9placer le disque %d de %c \u00e0 %c\\n\", current_frame.n, current_frame.from, current_frame.to);\n\n                current_frame.stage = 2;\n                push(&stack, current_frame);\n\n                Frame new_frame = {current_frame.n - 1, current_frame.aux, current_frame.to, current_frame.from, 0};\n                push(&stack, new_frame);\n                break;\n        }\n    }\n    free(stack.frames);\n}\n\nint main() {\n    int n = 3;\n    hanoi_iterative(n, 'A', 'C', 'B');\n}\n

    Ce qui donne le r\u00e9sultat suivant\u2009:

    D\u00e9placer le disque 1 de A \u00e0 C\nD\u00e9placer le disque 2 de A \u00e0 B\nD\u00e9placer le disque 1 de C \u00e0 B\nD\u00e9placer le disque 3 de A \u00e0 C\nD\u00e9placer le disque 1 de B \u00e0 A\nD\u00e9placer le disque 2 de B \u00e0 C\nD\u00e9placer le disque 1 de A \u00e0 C\n

    On voit que l'impl\u00e9mentation it\u00e9rative est bien plus complexe que l'impl\u00e9mentation r\u00e9cursive. C'est pourquoi il est souvent plus simple d'\u00e9crire un algorithme r\u00e9cursif, mais pas n\u00e9cessairement plus performant.

    "}, {"location": "course-c/40-algorithms/recursion/#utilisation-du-stack", "title": "Utilisation du stack", "text": "

    En C, la r\u00e9cursivit\u00e9 est g\u00e9r\u00e9e par le stack. Chaque appel de fonction est empil\u00e9 sur le stack. Lorsque la fonction retourne, elle est d\u00e9pil\u00e9e du stack. Il est important de noter que le stack a une taille limit\u00e9e. Par d\u00e9faut, sous Linux la taille du stack est de 8 Mio (donn\u00e9 par la commande ulimit -s), sous Windows c'est 1 Mio. Si la r\u00e9cursivit\u00e9 est trop profonde, il y a un risque de stack overflow.

    D'autre part, une fonction r\u00e9cursive qui utilise beaucoup de variables locales et beaucoup de param\u00e8tres seront tous empil\u00e9s sur le stack. Cela peut rapidement saturer la m\u00e9moire.

    Prenons l'exemple suivant d'une fonction r\u00e9cursive qui d\u00e9clare un tableau de 1Mio de caract\u00e8res\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint recurse(int n, int stack_size)\n{\n    char array[1024*1024] = {0};\n    printf(\"used stack: %d kiB\\n\", stack_size / 1024);\n    if (n == 0) return 0;\n    return recurse(n - 1, stack_size + sizeof(array));\n}\n\nint main(int argc, char* argv[])\n{\n    recurse(atoi(argv[1]), 0);\n}\n

    \u00c0 l'ex\u00e9cution, on obtient\u2009:

    used stack: 0 kiB\nused stack: 1024 kiB\nused stack: 2048 kiB\nused stack: 3072 kiB\nused stack: 4096 kiB\nused stack: 5120 kiB\nused stack: 6144 kiB\nSegmentation fault (stack overflow)\n

    Avertissement

    Avant d'impl\u00e9menter une fonction r\u00e9cursive, il est important de v\u00e9rifier que la profondeur de la r\u00e9cursivit\u00e9 ne d\u00e9passe pas la taille du stack.

    Limitez l'utilisation du stack en utilisant soit des variables globales, soit des variables statiques, soit des allocations dynamiques.

    "}, {"location": "course-c/40-algorithms/recursion/#memoisation", "title": "M\u00e9mo\u00efsation", "text": "

    En informatique la m\u00e9mo\u00efsation est une technique d'optimisation du code souvent utilis\u00e9e conjointement avec des algorithmes r\u00e9cursifs. Cette technique est largement utilis\u00e9e en programmation dynamique.

    Nous l'avons vu pr\u00e9c\u00e9demment, l'algorithme r\u00e9cursif du calcul de la somme de la suite de Fibonacci n'est pas efficace du fait que les m\u00eames appels sont r\u00e9p\u00e9t\u00e9s un nombre inutile de fois. La parade est de m\u00e9moriser pour chaque appel de fib, la sortie correspondante \u00e0 l'entr\u00e9e.

    Dans cet exemple nous utiliserons un m\u00e9canisme compos\u00e9 de trois fonctions\u2009:

    • int memoize(Cache *cache, int input, int output)
    • bool memoize_has(Cache *cache, int input)
    • int memoize_get(Cache *cache, int input)

    La premi\u00e8re fonction m\u00e9morise la valeur de sortie output li\u00e9e \u00e0 la valeur d'entr\u00e9e input. Pour des raisons de simplicit\u00e9 d'utilisation, la fonction retourne la valeur de sortie output.

    La seconde fonction memoize_has v\u00e9rifie si une valeur de correspondance existe pour l'entr\u00e9e input. Elle retourne true en cas de correspondance et false sinon.

    La troisi\u00e8me fonction memoize_get retourne la valeur de sortie correspondante \u00e0 la valeur d'entr\u00e9e input.

    Notre fonction r\u00e9cursive sera ainsi modifi\u00e9e comme suit\u2009:

    int fib(int n)\n{\n    if (memoize_has(n)) return memoize_get(n);\n    if (n < 2) return 1;\n    return memoize(n, fib(n - 1) + fib(n - 2));\n}\n

    Quant aux trois fonctions utilitaires, voici une proposition d'impl\u00e9mentation. Notons que cette impl\u00e9mentation est tr\u00e8s \u00e9l\u00e9mentaire et n'est valable que pour des entr\u00e9es inf\u00e9rieures \u00e0 1000. Il sera possible ult\u00e9rieurement de perfectionner ces fonctions, mais nous aurons pour cela besoin de concepts qui n'ont pas encore \u00e9t\u00e9 abord\u00e9s, tels que les structures de donn\u00e9es complexes.

    #define SIZE 1000\n\nbool cache_input[SIZE] = { false };\nint cache_output[SIZE];\n\nint memoize(int input, int output) {\n    cache_input[input % SIZE] = true;\n    cache_output[input % SIZE] = output;\n    return output;\n}\n\nbool memoize_has(int input) {\n    return cache_input[input % SIZE];\n}\n\nint memoize_get(int input) {\n    return cache_output[input % SIZE];\n}\n

    Exercice 1\u2009: La plus petite diff\u00e9rence

    Soit deux tableaux d'entiers, trouver la paire de valeurs (une dans chaque tableau) ayant la plus petite diff\u00e9rence (positive).

    Exemple\u2009:

    int a[] = {5, 3, 14, 11, 2};\nint b[] = {24, 128, 236, 20, 8};\n\nint diff = 3 // pair 11, 8\n
    1. Proposer une impl\u00e9mentation
    2. Quelle est la complexit\u00e9 de votre algorithme\u2009?
    ", "tags": ["memoize_has", "true", "input", "output", "false", "fib", "memoize_get"]}, {"location": "course-c/40-algorithms/recursion/#programmation-dynamique", "title": "Programmation dynamique", "text": "

    La programmation dynamique est une m\u00e9thode algorithmique datant des ann\u00e9es 1950, mais devenue populaire ces derni\u00e8res ann\u00e9es. Elle permet de coupler des algorithmes r\u00e9cursifs avec le concept de m\u00e9mo\u00efsation.

    Prenons par exemple l'algorithme de Fibonacci r\u00e9cursif\u2009:

    int fibonacci(int n) {\n    if (n <= 1) return n;\n    return fibonacci(n - 1) + fibonacci(n - 2);\n}\n

    Le probl\u00e8me de cet algorithme est sa performance. Appeler fibonacci(50) demandera de calculer fibonacci(49) et fibonacci(48) mais pour calculer fibonacci(49) il faudra recalculer fibonacci(48). On voit qu'on effectue du travail \u00e0 double. En r\u00e9alit\u00e9 c'est bien pire que \u00e7a. La complexit\u00e9 est de \\(O(2^n)\\). Donc pour calculer la valeur 50 il faudra effectuer \\(1 125 899 906 842 624\\) op\u00e9rations. Avec un ordinateur capable de calculer 1 milliard d'op\u00e9rations par seconde, il faudra tout de m\u00eame plus d'un million de secondes. Cet algorithme est donc tr\u00e8s mauvais\u2009!

    En revanche, si l'on est capable de m\u00e9moriser dans une table les r\u00e9sultats pr\u00e9c\u00e9dents des appels de Fibonacci, les performances seront bien meilleures.

    Voici l'algorithme modifi\u00e9\u2009:

    int fibonacci(int n) {\n    static int memo[1000] = {0};\n    if (memo[n]) return memo[n];\n    if (n <= 1) return n;\n    return memo[n] = fibonacci(n - 1) + fibonacci(n - 2);\n}\n

    Sa complexit\u00e9 est ainsi r\u00e9duite \u00e0 \\(O(2\\cdot n)\\) et donc \\(O(n)\\). En revanche, l'approche dynamique demande un espace m\u00e9moire suppl\u00e9mentaire. On n'a rien sans rien et l'\u00e9ternel dilemme m\u00e9moire versus performance s'applique toujours.

    "}, {"location": "course-c/40-algorithms/recursion/#backtracking", "title": "Backtracking", "text": "

    Le backtracking est une technique algorithmique qui consiste \u00e0 explorer toutes les solutions possibles d'un probl\u00e8me en testant chaque solution partielle. Lorsqu'une solution partielle ne peut pas \u00eatre compl\u00e9t\u00e9e pour former une solution valide, on revient en arri\u00e8re (backtrack) pour explorer une autre branche de l'arbre de recherche. Cette approche est souvent utilis\u00e9e pour rechercher une solution optimale \u00e0 un probl\u00e8me combinatoire.

    Par exemple, le probl\u00e8me des huit dames consiste \u00e0 placer huit dames sur un \u00e9chiquier de mani\u00e8re \u00e0 ce qu'aucune dame ne puisse attaquer une autre dame. Le backtracking est une m\u00e9thode efficace pour r\u00e9soudre ce type de probl\u00e8me.

    "}, {"location": "course-c/40-algorithms/recursion/#les-huit-dames", "title": "Les huit dames", "text": "

    Le probl\u00e8me des huit dames est un probl\u00e8me classique de placement de huit dames sur un \u00e9chiquier de 8x8 cases de mani\u00e8re \u00e0 ce qu'aucune dame ne puisse attaquer une autre dame. Une dame peut attaquer une autre dame si elles se trouvent sur la m\u00eame ligne, la m\u00eame colonne ou la m\u00eame diagonale.

    Les huit dames

    La solution na\u00efve est de tester toutes les combinaisons possibles de placement des huit dames et de v\u00e9rifier si elles sont valides. Cependant, cette approche est inefficace car le nombre de combinaisons possibles est tr\u00e8s \u00e9lev\u00e9. Si nous consid\u00e9rons simplement toutes les mani\u00e8res de placer 8 dames sur un \u00e9chiquier 8x8 sans tenir compte des contraintes d'attaque (c'est-\u00e0-dire sans tenir compte des lignes, colonnes ou diagonales), le nombre de configurations possibles est donn\u00e9 par le nombre de combinaisons de 64 cases (l'\u00e9chiquier) prises 8 \u00e0 la fois soit\u2009:

    \\[\\binom{64}{8} = \\frac{64\u2009!}{8\u2009!(64-8)!} = 4'426'165'368\\]

    N\u00e9anmoins ce probl\u00e8me qui est connu admet 92 solutions. C'est un probl\u00e8me de recherche exhaustive. On peut le r\u00e9soudre en utilisant une approche de backtracking.

    #include <stdbool.h>\n#include <stdio.h>\n\n#define N 8\n\n#define EMPTY false\n#define QUEEN (!EMPTY)\n\nvoid printSolution(bool board[N][N]) {\n   static int k = 0;\n   printf(\"Solution %d:\\n\", ++k);\n   for (int i = 0; i < N; ++i) {\n      for (int j = 0; j < N; ++j) printf(\"%s \", board[i][j] ? \"\u265b\" : \".\");\n      printf(\"\\n\");\n   }\n   printf(\"\\n\");\n}\n\nbool is_safe(bool board[N][N], int row, int col) {\n   // Column check\n   // Line check can be omitted because we are filling\n   // the board row by row\n   for (int i = 0; i < row; i++)\n      if (board[i][col] || board[row][i]) return false;\n\n   // Diagonal upper left\n   for (int i = row, j = col; i >= 0 && j >= 0; --i, --j)\n      if (board[i][j]) return false;\n\n   // Diagonal upper right\n   for (int i = row, j = col; i >= 0 && j < N; --i, ++j)\n      if (board[i][j]) return false;\n\n   return true;\n}\n\nbool solve(bool board[N][N], int row) {\n   // We have reached the end, this is a solution\n   if (row >= N) {\n      printSolution(board);\n      return true;\n   }\n\n   bool is_solution = false;\n   for (int col = 0; col < N; col++) {\n      if (is_safe(board, row, col)) {\n         board[row][col] = QUEEN;\n         is_solution = solve(board, row + 1) || is_solution;\n         board[row][col] = EMPTY;  // BACKTRACK\n      }\n   }\n\n   return is_solution;\n}\n\nint main() {\n   // False means empty, true means queen\n   bool board[N][N] = {0};\n\n   if (!solve(board, 0)) printf(\"Solution does not exist\");\n}\n

    On commence par d\u00e9finir un \u00e9chiquier de NxN cases sous la forme d'une tableau bidimensionnel bool board[N][N]. Chaque case de l'\u00e9chiquier peut contenir une dame (true) ou \u00eatre vide (false).

    La fonction is_safe prend en param\u00e8tre l'\u00e9chiquier actuel et les coordonn\u00e9es (row, col) d'une nouvelle dame \u00e0 placer. Elle v\u00e9rifie si la nouvelle dame peut \u00eatre plac\u00e9e sans \u00eatre attaqu\u00e9e par une autre dame d\u00e9j\u00e0 plac\u00e9e sur l'\u00e9chiquier. Pour cela, elle v\u00e9rifie les colonnes et les diagonales de la nouvelle dame pour s'assurer qu'aucune autre dame ne peut l'attaquer. Il n'est pas n\u00e9cessaire de v\u00e9rifier les lignes car chaque dame est plac\u00e9e sur une ligne diff\u00e9rente.

    Le coeur de l'algorithme est la fonction solve qui utilise une approche de backtracking pour placer les huit dames sur l'\u00e9chiquier. C'est une fonction r\u00e9cursive. La fonction prend en param\u00e8tre l'\u00e9chiquier actuel et la ligne courante row \u00e0 explorer. Comme la fonction s'appelle elle-m\u00eame il faut imp\u00e9rativement une condition de sortie. Dans notre cas si row est \u00e9gal \u00e0 N alors toutes les dames ont \u00e9t\u00e9 plac\u00e9es et on peut afficher la solution.

    Pour chaque ligne, on explore chaque colonne en commencant par la premi\u00e8re. Si la case est s\u00fbre, on place une dame et on appelle r\u00e9cursivement la fonction solve pour explorer la ligne suivante. A chaque appel successif de solve on place une dame suppl\u00e9mentaire. Si d'avenure on se rend compte que la configuration n'est pas valide, on retire la dame et on explore une autre colonne.

    Pour mieux comprendre, r\u00e9duisons le probl\u00e8me \u00e0 un \u00e9chiquier 4x4 et ajoutons un affichage dans solve. Pour conna\u00eetre la profondeur de la r\u00e9cursion, on passe un param\u00e8tre depth \u00e0 la fonction solve qui sera incr\u00e9mente \u00e0 chaque appel.

    #define PAD(depth)                                   \\\n   {                                                 \\\n      for (int i = 0; i < depth; i++) printf(\"   \"); \\\n   }\n\nvoid solve(bool board[N][N], int row, int depth) {\n   if (row >= N) {\n      PAD(depth);\n      printf(\"END\\n\");\n      return;\n   }\n\n   PAD(depth);\n   for (int col = 0; col < N; col++) {\n      printf(\"%c%d \", 'A' + col, row);\n      if (is_safe(board, row, col)) {\n         board[row][col] = QUEEN;\n         printf(\"\\n\");\n         solve(board, row + 1, depth + 1);\n         board[row][col] = EMPTY;  // BACKTRACK\n      }\n   }\n   printf(\"\\n\");\n   PAD(depth - 1);\n}\n

    L'ex\u00e9cution comment\u00e9e donne ceci\u2009:

    ------------------------> Niveau de r\u00e9cursion\nA0                    Une dame est plac\u00e9e puis on explore la ligne suivante\n   A1 B1 C1           Les positions A1, B1 sont invalides mais C1 est valide\n      A2 B2 C2 D2     Explore la ligne 2 mais aucune position n'est valide\n   D1                 On backtrack et explore la position suivante D1\n      A2 B2           Qui est valide, donc on explore la ligne 2\n         A3 B3 C3 D3  Et la ligne 3 car B2 \u00e9tait valide\n      C2 D2\n... Jusqu'ici aucune solution valide alors on backtrack\n\nB0\n   A1 B1 C1 D1\n      A2\n         A3 B3 C3\n            END       Une solution trouv\u00e9e avec B0, D1, A2, C3\nD3\n      B2 C2 D2\nC0\n   A1\n      A2 B2 C2 D2\n         A3 B3\n            END       Une autre solution trouv\u00e9e avec C0, A1, D2, B3\nC3 D3\n   B1 C1 D1\nD0\n   A1\n      A2 B2 C2\n         A3 B3 C3 D3\n      D2\n   B1\n      A2 B2 C2 D2\n   C1 D1\n

    Le probl\u00e8me des 8 dames peut \u00eatre aussi impl\u00e9ment\u00e9 sous forme it\u00e9rative. La technique est toujours la m\u00eame, on utilise une pile pour stocker les positions des dames. On notera que le code est bien plus complexe que la version r\u00e9cursive.

    void solve(bool board[N][N]) {\n   int row = 0, col = 0;\n   int stack[N] = {0};  // Stack to store column positions\n\n   while (row >= 0) {\n      bool placed = false;\n      while (col < N) {\n         if (is_safe(board, row, col)) {\n            board[row][col] = QUEEN;\n            stack[row] = col;\n            placed = true;\n            break;\n         }\n         col++;\n      }\n\n      if (placed) {\n         if (row == N - 1) {\n            printSolution(board);\n            board[row][col] = EMPTY;  // Backtrack\n            col = stack[row] + 1;     // Try next column in the same row\n         } else {\n            row++;\n            col = 0;\n         }\n      } else {\n         row--;\n         if (row >= 0) {\n            col = stack[row] + 1;  // Backtrack\n            board[row][stack[row]] = EMPTY;\n         }\n      }\n   }\n}\n
    ", "tags": ["is_safe", "true", "solve", "depth", "row", "false"]}, {"location": "course-c/40-algorithms/searching/", "title": "Recherche", "text": "

    La recherche est une op\u00e9ration courante en informatique. Il existe plusieurs algorithmes de recherche, chacun ayant ses avantages et inconv\u00e9nients. Les algorithmes de recherche les plus courants sont la recherche lin\u00e9aire et la recherche binaire.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-lineaire", "title": "Recherche lin\u00e9aire", "text": "

    La recherche lin\u00e9aire est une m\u00e9thode simple pour trouver un \u00e9l\u00e9ment dans un tableau. Elle consiste \u00e0 parcourir na\u00efvement tous les \u00e9l\u00e9ments du tableau, un par un, jusqu'\u00e0 trouver l'\u00e9l\u00e9ment recherch\u00e9. Si l'\u00e9l\u00e9ment est trouv\u00e9, la recherche s'arr\u00eate et renvoie la position de l'\u00e9l\u00e9ment dans le tableau. Si l'\u00e9l\u00e9ment n'est pas trouv\u00e9, la recherche renvoie une valeur sp\u00e9ciale pour indiquer que l'\u00e9l\u00e9ment est absent.

    Une impl\u00e9mentation pour une recherche d'entier serait la suivante\u2009:

    int linear_search(int *array, int size, int search) {\n    for (int i = 0; i < size; ++i)\n        if (array[i] == search)\n            return i;\n    return -1;\n}\n

    Une impl\u00e9mentation plus g\u00e9n\u00e9rique pour une recherche d'\u00e9l\u00e9ment de type variable peut \u00eatre r\u00e9alis\u00e9e en utilisant une fonction de comparaison laquelle retourne 0 si les deux \u00e9l\u00e9ments sont \u00e9gaux, 1 si le premier est plus grand et -1 si le premier est plus petit.

    int cmp_int(void *a, void *b) {\n    return *(int *)a - *(int *)b;\n}\n\nint linear_search(void *array, int size,\n    int element_size, void *search, int (*cmp)(void *, void *)) {\n    for (int i = 0; i < size; ++i)\n        if (cmp(array + i * element_size, search) == 0)\n            return i;\n    return -1;\n}\n

    La complexit\u00e9 de la recherche lin\u00e9aire est en \\(O(n)\\), o\u00f9 \\(n\\) est la taille du tableau. Si les recherches sont fr\u00e9quentes, et que la fonction de comparaison est complexe, cette m\u00e9thode n'est pas la plus efficace.

    L'utilisation d'une fonction de comparaison permet de r\u00e9aliser des recherches sur des tableaux qui ne se limites pas \u00e0 des entiers. On peut ainsi rechercher des cha\u00eenes de caract\u00e8res, des structures ou des objets plus complexes. Voici par exemple une fonction de comparaison pour des cha\u00eenes de caract\u00e8res\u2009:

    int cmp_str(void *a, void *b) {\n    return strcmp((char *)a, (char *)b);\n}\n
    "}, {"location": "course-c/40-algorithms/searching/#recherche-dichotomique", "title": "Recherche dichotomique", "text": "

    La recherche dichotomique est une m\u00e9thode plus efficace pour trouver un \u00e9l\u00e9ment dans un tableau mais elle impose que ce dernier soit tri\u00e9. Elle consiste \u00e0 diviser le tableau en deux parties \u00e9gales et \u00e0 comparer l'\u00e9l\u00e9ment recherch\u00e9 avec l'\u00e9l\u00e9ment au milieu du tableau. Si l'\u00e9l\u00e9ment est \u00e9gal \u00e0 l'\u00e9l\u00e9ment au milieu, la recherche s'arr\u00eate. Sinon, si l'\u00e9l\u00e9ment est plus petit, la recherche continue dans la premi\u00e8re moiti\u00e9 du tableau, sinon dans la seconde moiti\u00e9.

    Cette m\u00e9thode est celle que vous appliquez quand on demande de devinez un nombre choisi par un tier entre 1 et 100. Plut\u00f4t que choisir une valeur al\u00e9atoire \u00e0 chaque essai, vous annoncez en premier 50, puis 25 ou 75, puis 12 ou 37 ou 62 ou 87, etc. \u00c0 chaque essai vous \u00e9liminez la moiti\u00e9 des possibilit\u00e9s. On dit que la progression est logarithmique en base 2.

    Une impl\u00e9mentation pour une recherche d'entier serait la suivante\u2009:

    int binary_search(int *array, int size, int search) {\n    int left = 0, right = size - 1;\n    while (left <= right) {\n        int mid = left + (right - left) / 2;\n        if (array[mid] == search)\n            return mid;\n        if (array[mid] < search)\n            left = mid + 1;\n        else\n            right = mid - 1;\n    }\n    return -1;\n}\n

    Elle peut \u00e9galement \u00eatre g\u00e9n\u00e9ralis\u00e9e avec une fonction de comparaison ou sp\u00e9cialis\u00e9e pour un type particulier. La complexit\u00e9 de la recherche dichotomique est en \\(O(\\log n)\\), o\u00f9 \\(n\\) est la taille du tableau. Cette m\u00e9thode est donc beaucoup plus efficace que la recherche lin\u00e9aire pour des tableaux de grande taille mais elle impose un tri pr\u00e9alable en \\(O(n \\log n)\\).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-par-hashage", "title": "Recherche par hashage", "text": "

    L'utilisation d'un tableau de hachage permet de r\u00e9aliser des recherches en temps constant, en moyenne (\\(O(1)\\)). C'est donc une m\u00e9thode encore plus performante que la recherche dichotomique. Elle n\u00e9cessite \u00e9galement un pr\u00e9traitement pour construire la table de hachage, en \\(O(n)\\), mais elle est particuli\u00e8rement adapt\u00e9e pour des recherches fr\u00e9quentes dans des ensembles de donn\u00e9es volumineux.

    La performance de la recherche d\u00e9pend grandement de la fonction de hachage utilis\u00e9e et du facteur de charge de la table de hachage. Si le facteur de charge est trop \u00e9lev\u00e9, la recherche peut devenir lin\u00e9aire.

    Il est possible de s'affranchir du risque de collision en utilisant une table de hachage parfaite, mais cela implique une complexit\u00e9 bien plus grande pour la construction de la table. Une fonction de hashage parfaite est une fonction injective, c'est-\u00e0-dire qu'elle associe un \u00e9l\u00e9ment unique \u00e0 chaque cl\u00e9 sans risque de collision. Cette approche est particuli\u00e8rement adapt\u00e9s pour des ensemble de donn\u00e9es statiques (connus \u00e0 l'avance) et de petite taille.

    "}, {"location": "course-c/40-algorithms/searching/#localite-des-donnees", "title": "Localit\u00e9 des donn\u00e9es", "text": "

    La complexit\u00e9 de recherche n'est pas le seul crit\u00e8re \u00e0 prendre en compte pour choisir un algorithme de recherche. Le fonctionnement de l'ordinateur impose des contraintes mat\u00e9rielles qui peuvent influencer les performances. Par exemple, la recherche lin\u00e9aire peut \u00eatre plus rapide que la recherche dichotomique pour des tableaux de petite taille, car elle exploite mieux la localit\u00e9 des donn\u00e9es en m\u00e9moire.

    Nous l'avons abord\u00e9 lors de l'explication du fonctionnement de la m\u00e9moire que l'acc\u00e8s \u00e0 la RAM est lent par rapport au processeur. Pour ce faire le processeur fait appel \u00e0 une m\u00e9moire cache tr\u00e8s rapide mais beaucoup plus petite. Lorsque vous parcourez un tableau, les \u00e9l\u00e9ments sont charg\u00e9s en m\u00e9moire cache et la recherche lin\u00e9aire exploite mieux cette localit\u00e9 des donn\u00e9es. Si le tableau est tr\u00e8s grand, \u00e0 chaque saut de la recherche dichotomique vous ne profitez pas de la m\u00e9moire cache et le temps d'acc\u00e8s \u00e0 la RAM devient pr\u00e9pond\u00e9rant. Aussi dans l'\u00e9laboration d'un algorithme on cherche \u00e0 optimiser la localit\u00e9 des donn\u00e9es, \u00e0 la foi spatiale (les \u00e9l\u00e9ments sont proches en m\u00e9moire) et temporelle (les \u00e9l\u00e9ments sont utilis\u00e9s dans un court laps de temps) afin de minimiser les temps d'acc\u00e8s \u00e0 la RAM.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-sur-de-gros-volumes-de-donnees", "title": "Recherche sur de gros volumes de donn\u00e9es", "text": "

    Si on prend l'exemple d'un moteur de recherche sur internet, la recherche dichotomique n'est pas adapt\u00e9e. En effet, les donn\u00e9es sont stock\u00e9es sur des serveurs distants et le temps d'acc\u00e8s \u00e0 ces donn\u00e9es est bien plus long que le temps de calcul de l'algorithme. Dans ce cas, la recherche par hashage est plus adapt\u00e9e car elle permet de r\u00e9duire le temps d'acc\u00e8s aux donn\u00e9es en les regroupant localement. D'autre part, ce type de probl\u00e8me n'est pas impl\u00e9ment\u00e9 avec un tableau d'entiers programm\u00e9 en C. Lorsque la collection et le traitement des donn\u00e9es deviennent trop volumineux, on utilise des bases de donn\u00e9es car d'une part elles offrent les outils n\u00e9cessaire \u00e0 la gestion des donn\u00e9es et d'autre part elles permettent le stockage des donn\u00e9es sur un disque dur.

    Dans les probl\u00e8mes courant d'ing\u00e9nierie, une base de donn\u00e9e SqlLite peut \u00eatre un excellent choix pour stocker des donn\u00e9es structur\u00e9es et effectuer des recherches complexes. Voici un exemple de recherche sur une base de donn\u00e9e SqlLite\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <sqlite3.h>\n\nint create_and_insert(sqlite3 *db) {\n    // Cr\u00e9er la table Personnes\n    const char *sql_create_table =\n        \"CREATE TABLE IF NOT EXISTS people(\"\n        \"id INTEGER PRIMARY KEY AUTOINCREMENT,\"\n        \"age INT,\"\n        \"firstname TEXT,\"\n        \"lastname TEXT,\"\n        \"salary REAL);\";\n\n    rc = sqlite3_exec(db, sql_create_table, 0, 0, &err_msg);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"SQL error: %s\\n\", err_msg);\n        sqlite3_free(err_msg);\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Ins\u00e9rer des donn\u00e9es dans la table\n    const char *sql_insert =\n        \"INSERT INTO people (age, lastname, firstname, salary) VALUES \"\n        \"(25, 'Anderson', 'Alexandre', 4500),\"\n        \"(35, 'Dupont',   'Fran\u00e7ois',  3200),\"\n        \"(28, 'Martin',   'Annabelle', 4800),\"\n        \"(42, 'Favre',    'Fabrice',   5200),\"\n        \"(30, 'Armand',   'Aurelie',   4100);\";\n\n    rc = sqlite3_exec(db, sql_insert, 0, 0, &err_msg);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"SQL error: %s\\n\", err_msg);\n        sqlite3_free(err_msg);\n        sqlite3_close(db);\n        return 1;\n    }\n    return 0;\n}\n\nint main(void) {\n    sqlite3 *db;\n    char *err_msg = 0;\n    int rc;\n\n    // Ouvrir ou cr\u00e9er la base de donn\u00e9es\n    rc = sqlite3_open(\"people.db\", &db);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"Cannot open database: %s\\n\", sqlite3_errmsg(db));\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Cr\u00e9er et ins\u00e9rer des donn\u00e9es dans la base de donn\u00e9es\n    if (create_and_insert(db)) {\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Requ\u00eate pour trouver les personnes correspondant aux crit\u00e8res\n    const char *sql_query =\n        \"SELECT * FROM people WHERE \"\n        \"age BETWEEN 23 AND 42 AND \"\n        \"(lastname LIKE 'A%' OR nom LIKE 'F%') AND \"\n        \"LENGTH(firstname) = 8 AND \"\n        \"salary > 4000;\";\n\n    sqlite3_stmt *stmt;\n    rc = sqlite3_prepare_v2(db, sql_query, -1, &stmt, 0);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"Failed to execute query: %s\\n\", sqlite3_errmsg(db));\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Ex\u00e9cuter la requ\u00eate et afficher les r\u00e9sultats\n    printf(\"R\u00e9sultats de la recherche:\\n\");\n    while (sqlite3_step(stmt) == SQLITE_ROW) {\n        int id = sqlite3_column_int(stmt, 0);\n        int age = sqlite3_column_int(stmt, 1);\n        const unsigned char *nom = sqlite3_column_text(stmt, 2);\n        const unsigned char *prenom = sqlite3_column_text(stmt, 3);\n        double salaire = sqlite3_column_double(stmt, 4);\n\n        printf(\"ID: %d, Age: %d, Nom: %s, Pr\u00e9nom: %s, Salaire: %.2f\\n\",\n               id, age, lastname, firstname, salary);\n    }\n\n    sqlite3_finalize(stmt);\n    sqlite3_close(db);\n}\n
    "}, {"location": "course-c/40-algorithms/searching/#la-malediction-de-la-dimensionnalite", "title": "La Mal\u00e9diction de la Dimensionnalit\u00e9", "text": "

    L'un des principaux probl\u00e8mes rencontr\u00e9s lorsqu'on travaille avec des donn\u00e9es en plusieurs dimensions est ce que l'on appelle aussi le \u00ab\u2009fl\u00e9au de la dimension\u2009\u00bb. Ce terme invent\u00e9 par Richard Bellman en 1961 d\u00e9crit plusieurs ph\u00e9nom\u00e8nes qui rendent la manipulation des donn\u00e9es multidimensionnelles plus complexe que celle des donn\u00e9es unidimensionnelles. En particulier, les points de donn\u00e9es deviennent de plus en plus dispers\u00e9s dans l'espace au fur et \u00e0 mesure que la dimension augmente, ce qui rend plus difficile la localisation rapide d'un voisin proche.

    La difficult\u00e9 r\u00e9side dans l'imposibilit\u00e9 d'inf\u00e9rer des relations entre les donn\u00e9es pour optimiser la recherche. Par exemple, si vous avez un tableau de 1000 \u00e9l\u00e9ments, vous pouvez diviser le tableau en 10 blocs de 100 \u00e9l\u00e9ments et effectuer une recherche dichotomique sur chaque bloc. Si vous avez un tableau de 1000 \u00e9l\u00e9ments en 2 dimensions, vous ne pouvez pas diviser le tableau en 10 blocs de 100 \u00e9l\u00e9ments dans chaque dimension. Vous devez diviser le tableau en 100 blocs de 10 \u00e9l\u00e9ments dans chaque dimension, soit 1000 blocs au total. La recherche dichotomique n'est plus efficace.

    Le probl\u00e8me est encore plus complexe lorsque la recherche porte sur une combinaison de plusieurs dimensions. Par exemple, vous stocker des coordonn\u00e9es g\u00e9ographiques (latitude et longitude) et vous cherchez \u00e0 trouver les points les plus proches d'un point donn\u00e9. La recherche dichotomique ne fonctionne pas dans ce cas. Une approche courante pour ce type de recherche est d'utiliser des arbres de recherche multidimensionnels, tels que les arbres KD (K-dimensional trees) ou les arbres R (R-trees).

    "}, {"location": "course-c/40-algorithms/searching/#k-d-tree", "title": "K-D Tree", "text": "

    Le K-D Tree est une structure de donn\u00e9es arborescente qui permet de stocker des points en plusieurs dimensions. Il est utilis\u00e9 pour effectuer des recherches spatiales efficaces, telles que la recherche des points les plus proches d'un point donn\u00e9. Le principe de l'arbre KD est de diviser l'espace en deux \u00e0 chaque niveau de l'arbre, en alternant les dimensions.

    Repr\u00e9sentation du K-D Tree

    Lors de l'insertion de chaque point, le plan est divis\u00e9 en deux parties \u00e9gales (gauche/droite) ou (haut/bas) selon la parit\u00e9 du niveau de l'arbre. Au niveau stockage, on utilise un arbre binaire\u2009:

    Arbre binaire du K-D Tree

    On sait qu'un arbre binaire peut \u00eatre repr\u00e9sent\u00e9 sous forme d'une liste, la repr\u00e9sentation ci-dessus peut-\u00eatre ainsi repr\u00e9sent\u00e9e avec avec\u2009:

    int nodes[] = {1, 2, 3, 5, -1, 4, -1, 6, 8, -1, -1, -1, -1, -1, 7};\n

    La valeur -1 indique un noeud vide. La racine de l'arbre est le premier \u00e9l\u00e9ment de la liste, ici 1. Les enfants d'un noeud i sont 2*i et 2*i+1. Par exemple, les enfants de 1 sont 2 et 3. Si l'arbre n'est pas complet et fortement d\u00e9s\u00e9quilibr\u00e9, le stockage ne sera pas optimal car la plupart des noeuds seront vides. Dans ce cas il est plus \u00e9l\u00e9gant de repr\u00e9senter l'arbre avec une liste cha\u00een\u00e9e.

    Bien entendu, les noeuds seront plus complexes que des entiers, un noeud peut par exemple \u00eatre associ\u00e9 \u00e0 un identifiant, et ses coordonn\u00e9es X et Y\u2009:

    typedef struct node {\n    int id;\n    double x, y;\n} Node;\n

    Dans notre exemple pour \u00e9conomiser de l'espace, nous pouvons utiliser une liste pour les noeuds, et un tableau pour l'arbre\u2009:

    Node nodes[] = {\n    {1, 10.6, 4.6},\n    {2,  7.1, 3.9},\n    {3, 12.6, 6.7},\n    {4, 14.6, 1.6},\n    {5,  3.6, 2.6},\n    {6,  2.6, 0.6},\n    {7,  2.4, 1.6},\n    {8,  4.6, 1.4}\n};\n\nint tree[] = {0, 1, 2, 4, -1, 3, -1, 5, 7, -1, -1, -1, -1, -1, 6};\n

    L'acc\u00e8s \u00e0 un \u00e9l\u00e9ment s'\u00e9crirait alors\u2009: nodes[tree[1]] pour acc\u00e9der au noeud 2, \u00e0 condition que le noeud ne soit pas vide.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-du-voisin-le-plus-proche", "title": "Recherche du voisin le plus proche", "text": "

    Pour rechercher le voisin le plus proche d'un point donn\u00e9 dans un K-D Tree, on utilise une m\u00e9thode de recherche r\u00e9cursive qui exploite la structure de l'arbre pour r\u00e9duire efficacement l'espace de recherche. Le processus commence par la recherche du point cible en parcourant l'arbre, en suivant les divisions du plan \u00e0 chaque niveau. Lorsque le point cible est trouv\u00e9 ou que l'on atteint une feuille, on remonte l'arbre en v\u00e9rifiant \u00e0 chaque n\u0153ud si celui-ci est plus proche du point cible que les \\(k\\) points actuellement dans la liste des plus proches voisins. Pour maintenir cette liste, on utilise g\u00e9n\u00e9ralement une file de priorit\u00e9 (ou un max-heap) qui garde toujours les \\(k\\) points les plus proches trouv\u00e9s jusqu'\u00e0 pr\u00e9sent.

    Lors de la remont\u00e9e, on compare \u00e9galement la distance entre le plan de s\u00e9paration du n\u0153ud courant et le point cible. Si cette distance est inf\u00e9rieure \u00e0 la distance du plus \u00e9loign\u00e9 des \\(k\\) voisins actuels, cela signifie qu'il pourrait y avoir un voisin plus proche de l'autre c\u00f4t\u00e9 du plan. Dans ce cas, la recherche est \u00e9galement effectu\u00e9e dans la sous-arborescence oppos\u00e9e.

    Prenons un exemple concret. Sur la figure suivante, on prend le point P \\((3.2, 2.3)\\) comme point cible comme pr\u00e9sent\u00e9 sur la figure suivante\u2009:

    Recherche d'une zone dans un K-D Tree

    Pour trouver le voisin le plus proche voici les op\u00e9rations\u2009:

    1. Descente dans l'arbre (recherche initiale):
    2. On commence par comparer le point \\( P \\) avec le n\u0153ud de la racine en fonction de la dimension actuelle (par exemple, si l'arbre est divis\u00e9 selon les coordonn\u00e9es \\( x \\) et \\( y \\), on commence par comparer les \\( x \\)).
    3. On continue de descendre dans l'arbre en suivant les enfants gauche ou droit en fonction de la position du point \\( P \\) par rapport au plan de s\u00e9paration de chaque n\u0153ud. \u00c0 chaque \u00e9tape, on r\u00e9duit l'espace de recherche en alternant les dimensions, jusqu'\u00e0 atteindre une feuille. Cette feuille correspond au point de l'arbre qui est le plus proche dans l'une des dimensions de \\( P \\).

    4. Remont\u00e9e dans l'arbre (recherche du point le plus proche):

    5. Une fois la feuille atteinte, on enregistre ce point comme le point le plus proche trouv\u00e9 jusqu'\u00e0 pr\u00e9sent. Cependant, il est possible que le point le plus proche ne soit pas dans la sous-arborescence de cette feuille, mais plut\u00f4t dans une autre branche de l'arbre.
    6. En remontant l'arbre, on \u00e9value chaque n\u0153ud parent pour v\u00e9rifier s'il pourrait contenir un point plus proche que le point le plus proche actuel. Pour cela, on calcule la distance entre \\( P \\) et le point stock\u00e9 dans le n\u0153ud courant, ainsi que la distance entre \\( P \\) et le plan de s\u00e9paration de ce n\u0153ud.

    7. Si la distance du plan de s\u00e9paration est inf\u00e9rieure \u00e0 la distance au point le plus proche actuel, cela signifie que l'autre sous-arborescence (c\u00f4t\u00e9 oppos\u00e9 du plan) pourrait contenir un point plus proche de \\( P \\). On doit donc explorer cette autre sous-arborescence, ce qui implique de descendre dans cette sous-arborescence comme on l'a fait initialement.

    8. Exploration de la sous-arborescence oppos\u00e9e:

    9. Lors de l'exploration de cette sous-arborescence oppos\u00e9e, on suit un processus similaire en descendant jusqu'\u00e0 une feuille et en comparant chaque n\u0153ud avec le point le plus proche actuellement connu.
    10. Si un point plus proche est trouv\u00e9 dans cette sous-arborescence, il remplace le point le plus proche actuel.

    11. Terminaison:

    12. Le processus se poursuit jusqu'\u00e0 ce que l'on ait remont\u00e9 jusqu'\u00e0 la racine, en explorant potentiellement plusieurs sous-arborescences oppos\u00e9es en fonction de la distance au plan de s\u00e9paration.
    13. \u00c0 la fin de ce processus, le point le plus proche trouv\u00e9 sera le plus proche de \\( P \\) dans l'ensemble du K-D Tree.

    En pratique, suivant notre exemple, on part de (1). Au premier niveau de l'arbre on sait que les enfants correspondent \u00e0 l'axe des abscisses et comme \\(3.2 < 10.6\\) on prend le fils gauche. On descend donc jusqu'\u00e0 (2). Cette fois-ci c'es selon l'axe des ordonn\u00e9es que l'on compare ce qui nous emm\u00e8ne \u00e0 (5), puis (6), puis (7). La descente s'arr\u00eate l\u00e0 car on \u00e0 atteint une feuille. \u00c0 partir de maintenant on entre dans la seconde phase. On remonte alors l'arbre en v\u00e9rifiant si le sous-arbre oppos\u00e9 pourrait contenir des points plus proches.Comme le plan de s\u00e9paration de (6) est plus \u00e9loign\u00e9 que (7) on explore pas la r\u00e9gion au dessus de (6). On continue la remont\u00e9e jusqu'\u00e0 (5). Cette fois-ci la distance de s\u00e9paration est plus proche (abs(P.x - 7.x) = 0.8 et abs(P.x - 5,x) = 0.4) donc, il se peut que la r\u00e9gion \u00e0 droite de (5) contienne un point plus proche. On entre alors dans a troisi\u00e8me phase\u2009: l'exploration de la partie droite de (5). On descend alors jusqu'\u00e0 (8) qui est une feuille, et qui de surcro\u00eet est plus \u00e9loign\u00e9 que (7).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-des-k-voisins-les-plus-proches", "title": "Recherche des k-voisins les plus proches", "text": "

    Pour rechercher les \\(k\\) voisins les plus proches d'un point donn\u00e9 dans un K-D Tree, on utilise une m\u00e9thode similaire \u00e0 la recherche du voisin le plus proche, mais en maintenant une liste des \\(k\\) points les plus proches trouv\u00e9s jusqu'\u00e0 pr\u00e9sent. Lors de la remont\u00e9e dans l'arbre, on compare chaque n\u0153ud parent avec les points de la liste des \\(k\\) voisins les plus proches. Si le n\u0153ud parent est plus proche que le point le plus \u00e9loign\u00e9 de la liste, on l'ajoute \u00e0 la liste et on retire le point le plus \u00e9loign\u00e9. On continue ce processus jusqu'\u00e0 ce que l'arbre soit enti\u00e8rement explor\u00e9. Cela correspond \u00e0 utiliser une structure de donn\u00e9es de file de priorit\u00e9 (ou max-heap) pour maintenir les \\(k\\) points les plus proches. La m\u00e9thode est donc tr\u00e8s similaire \u00e0 la recherche du voisin le plus proche, mais avec une gestion plus complexe de la liste des voisins.

    La recherche des \\(k\\) voisins est donc une op\u00e9ration en \\(O(k \\log n)\\).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-des-voisins-dans-un-cercle", "title": "Recherche des voisins dans un cercle", "text": "

    Reconsid\u00e9rons notre figure pr\u00e9c\u00e9dente. On souhaite maintenant chercher les points \u00e0 l'int\u00e9rieur du cercle jaune de diam\u00e8tre \\(3.75\\). Pour cela on commence par chercher le point le plus proche de \\(P\\) comme pr\u00e9c\u00e9demment. On remonte l'arbre en v\u00e9rifiant si le plan de s\u00e9paration est \u00e0 une distance inf\u00e9rieure \u00e0 \\(3.75\\) du point \\(P\\). Dit autrement, est-ce que le cercle coupe le plan de s\u00e9paration. Si c'est le cas on explore l'autre sous-arbre. On continue ce processus jusqu'\u00e0 ce que l'on ait explor\u00e9 tous les sous-arbres dont le plan de s\u00e9paration est \u00e0 une distance inf\u00e9rieure \u00e0 \\(3.75\\) de \\(P\\).

    "}, {"location": "course-c/40-algorithms/searching/#implementation", "title": "Impl\u00e9mentation", "text": "

    L'impl\u00e9mentation passe par la d\u00e9finition d'un noeud\u2009:

    typedef struct Node {\n   int id;\n   double x, y;\n   struct Node *left, *right;\n} Node;\n\nNode* insert(Node* root, Node* new_node, int depth) {\n   if (root == NULL) return new_node;\n\n   int cd = depth % 2;\n\n   if ((cd == 0 && new_node->x < root->x) || (cd == 1 && new_node->y < root->y))\n      root->left = insert(root->left, new_node, depth + 1);\n   else\n      root->right = insert(root->right, new_node, depth + 1);\n\n   return root;\n}\n\nvoid search_closest(Node* root, double x, double y, int depth,\n                    ClosestNode* closest) {\n   if (root == NULL) return;\n\n   double d = squared_distance(root->x, root->y, x, y);\n   if (d < closest->distance) {\n      closest->node = root;\n      closest->distance = d;\n   }\n\n   int cd = depth % 2;\n\n   // Quelle sous-arborescence est la plus proche ?\n   Node* nearer_subtree = NULL;\n   Node* farther_subtree = NULL;\n   if ((cd == 0 && x < root->x) || (cd == 1 && y < root->y)) {\n      nearer_subtree = root->left;\n      farther_subtree = root->right;\n   } else {\n      nearer_subtree = root->right;\n      farther_subtree = root->left;\n   }\n\n   // Rechercher d'abord dans la sous-arborescence la plus proche\n   search_closest(nearer_subtree, x, y, depth + 1, closest);\n\n   // V\u00e9rifier si nous devons explorer la sous-arborescence plus \u00e9loign\u00e9e\n   double dist_to_plane = (cd == 0) ? (x - root->x) : (y - root->y);\n   if (dist_to_plane * dist_to_plane < closest->distance)\n      search_closest(farther_subtree, x, y, depth + 1, closest);\n}\n

    Pour ins\u00e9rer un noeud \u00e0 la position x, y, on utilisera\u2009:

    Node* new_node = (Node*)malloc(sizeof(Node));\nnew_node->x = x;\nnew_node->y = y;\nnew_node->left = new_node->right = NULL;\n*root = insert(*root, new_node, 0);\n

    Et pour rechercher l'\u00e9l\u00e9ment le plus proche de x, y on utilisera\u2009:

    ClosestNode closest;\nclosest.node = NULL;\nclosest.distance = DBL_MAX;\n\nsearch_closest(root, x, y, 0, &closest);\n
    "}, {"location": "course-c/40-algorithms/utilities/", "title": "Algorithmes sur cha\u00eenes", "text": ""}, {"location": "course-c/40-algorithms/utilities/#slurp", "title": "Slurp", "text": "

    Il est souvent n\u00e9cessaire de lire l'int\u00e9gralit\u00e9 de l'entr\u00e9e standard dans une cha\u00eene de caract\u00e8res. Cependant, comme l'entr\u00e9e standard (stdin) n'est pas seekable, c'est-\u00e0-dire qu'il est impossible de se d\u00e9placer librement dans le flux ou d'en d\u00e9terminer la taille \u00e0 l'avance, il devient impossible d'allouer pr\u00e9cis\u00e9ment la m\u00e9moire n\u00e9cessaire \u00e0 l'avance. Une strat\u00e9gie commune consiste \u00e0 lire le flux par fragments et \u00e0 utiliser un tableau dynamique, redimensionn\u00e9 de fa\u00e7on progressive (via un facteur de croissance), pour stocker l'int\u00e9gralit\u00e9 du contenu. C'est pr\u00e9cis\u00e9ment l'objectif de la fonction slurp pr\u00e9sent\u00e9e ci-dessous. Slurp est un terme argotique qui signifie \u00ab\u2009aspirer\u2009\u00bb ou \u00ab\u2009engloutir\u2009\u00bb en anglais, c'est \u00e9galement un terme utilis\u00e9 en informatique pour d\u00e9signer le fait de lire un fichier en entier, notamment en Perl.

    slurp.h
    #pragma once\n\n#include <stdio.h>\nchar *slurp(FILE *file);\n
    slurp.c
    #include <stdio.h>\n#include <stdlib.h>\n\nchar *slurp(FILE *file) {\n   size_t size = 256;\n   size_t len = 0;\n   char *input = (char *)malloc(size);\n\n   if (input == NULL) {\n      fprintf(stderr, \"Memory allocation failed\\n\");\n      exit(1);\n   }\n\n   size_t bytesRead;\n   while ((bytesRead = fread(input + len, 1, size - len - 1, file)) > 0) {\n      len += bytesRead;\n      if (len + 1 >= size) {\n         size *= 2;\n         char *newInput = (char *)realloc(input, size);\n         if (newInput == NULL) {\n            free(input);\n            fprintf(stderr, \"Memory reallocation failed\\n\");\n            exit(1);\n         }\n         input = newInput;\n      }\n   }\n\n   if (ferror(file)) {\n      free(input);\n      fprintf(stderr, \"Error reading file\\n\");\n      exit(1);\n   }\n\n   input[len] = '\\0';\n   return input;\n}\n
    ", "tags": ["stdin", "slurp"]}, {"location": "course-c/40-algorithms/utilities/#analyse-et-alternatives-possibles", "title": "Analyse et alternatives possibles", "text": ""}, {"location": "course-c/40-algorithms/utilities/#taille-predeterminee", "title": "Taille pr\u00e9d\u00e9termin\u00e9e", "text": "

    Une premi\u00e8re alternative consisterait \u00e0 allouer un espace m\u00e9moire suffisamment grand pour que la probabilit\u00e9 que le fichier exc\u00e8de cette taille soit tr\u00e8s faible. Cependant, cette approche est loin d'\u00eatre robuste. Elle se base sur des hypoth\u00e8ses qui pourraient \u00e9chouer sur des syst\u00e8mes avec des contraintes m\u00e9moire strictes, ou lorsque la taille du fichier d\u00e9passe largement les pr\u00e9visions. De plus, elle introduit un risque de gaspillage de m\u00e9moire si l'estimation d\u00e9passe largement la taille r\u00e9elle du fichier.

    "}, {"location": "course-c/40-algorithms/utilities/#utilisation-dun-fichier-temporaire", "title": "Utilisation d'un fichier temporaire", "text": "

    Une autre approche consisterait \u00e0 rediriger le flux non seekable vers un fichier temporaire. Une fois que le flux a \u00e9t\u00e9 enti\u00e8rement consomm\u00e9, il serait alors possible de rouvrir ce fichier temporaire et d'en d\u00e9terminer la taille exacte avec fseek. On pourrait ensuite allouer pr\u00e9cis\u00e9ment la m\u00e9moire requise en une seule op\u00e9ration, charger le fichier en m\u00e9moire, puis supprimer le fichier temporaire. Toutefois, cette solution, bien qu'ing\u00e9nieuse, pr\u00e9sente \u00e9galement des inconv\u00e9nients\u2009: elle repose sur le syst\u00e8me de fichiers, ce qui ajoute une complexit\u00e9 inutile et la rend sensiblement plus lente que la solution initiale.

    ", "tags": ["fseek"]}, {"location": "course-c/40-algorithms/utilities/#analyse-de-la-solution-proposee", "title": "Analyse de la solution propos\u00e9e", "text": "

    La solution actuelle a n\u00e9anmoins des aspects qui m\u00e9ritent d'\u00eatre discut\u00e9s. Tout d'abord, la taille du tampon initial est fix\u00e9e arbitrairement \u00e0 256 caract\u00e8res. Pour de grands fichiers, cette taille modeste entra\u00eenera un nombre \u00e9lev\u00e9 d'appels syst\u00e8mes \u00e0 read, ce qui peut affecter les performances globales. Une meilleure approche serait de permettre \u00e0 l'utilisateur de sp\u00e9cifier la taille initiale du tampon en tant qu'argument optionnel de la fonction, avec une valeur par d\u00e9faut appropri\u00e9e si un argument nul ou z\u00e9ro est fourni.

    Par ailleurs, la fonction pourrait \u00e9chouer dans le cas o\u00f9 le fichier \u00e0 lire est trop volumineux pour tenir en m\u00e9moire. Une am\u00e9lioration serait de permettre la d\u00e9finition d'une taille maximale \u00e0 charger en m\u00e9moire, transmise en param\u00e8tre. Si le fichier d\u00e9passe cette limite, la fonction pourrait retourner un pointeur NULL, signalant ainsi au programme appelant que le fichier ne peut pas \u00eatre trait\u00e9 enti\u00e8rement en m\u00e9moire.

    Un autre aspect discutable de l'impl\u00e9mentation actuelle r\u00e9side dans la gestion des erreurs. En cas d'\u00e9chec d'allocation m\u00e9moire (malloc ou realloc), la fonction appelle exit(), ce qui interrompt brutalement l'ex\u00e9cution du programme. Il serait pr\u00e9f\u00e9rable d'adopter une approche plus flexible en retournant un code d'erreur ou un pointeur NULL, laissant ainsi le programme appelant d\u00e9cider de la mani\u00e8re de g\u00e9rer l'erreur. Cela permettrait une meilleure gestion des erreurs au niveau applicatif, notamment dans les cas o\u00f9 une terminaison imm\u00e9diate du programme n'est pas souhaitable.

    Enfin, on notera que l'espace allou\u00e9 n'est pas r\u00e9duit \u00e0 la taille exacte du fichier apr\u00e8s lecture. Cela peut entra\u00eener un gaspillage de m\u00e9moire si l'algorithme vient de r\u00e9allouer la m\u00e9moire. Une am\u00e9lioration possible serait de r\u00e9duire la taille du tampon \u00e0 la taille exacte du fichier apr\u00e8s lecture, en utilisant realloc pour lib\u00e9rer l'espace exc\u00e9dentaire.

    La solution propos\u00e9e est fonctionnelle mais perfectible, comme tout code informatique. Gardons \u00e0 l'esprit ici que l'objectif est de comprendre le concept de l'algorithme et de saisir les principes de base de sa mise en \u0153uvre.

    ", "tags": ["read", "NULL", "malloc", "realloc"]}, {"location": "course-c/40-algorithms/utilities/#split", "title": "Split", "text": "

    Un besoin assez courant concernant les cha\u00eenes de caract\u00e8res est de les diviser en sous-cha\u00eenes en fonction d'un d\u00e9limiteur. Prenons l'exemple d'un fichier CSV (Comma-Separated Values) o\u00f9 les champs sont s\u00e9par\u00e9s par des virgules ou point virgules. Pour chaque ligne, l'objectif est de parcourir la cha\u00eene \u00e0 la recherche du d\u00e9limiteur et de copier le champ dans un tableau. Pour conserver un code g\u00e9n\u00e9rique, l'op\u00e9ration de stockage des champs peut \u00eatre confi\u00e9e \u00e0 une fonction de type callback, qui sera appel\u00e9e pour chaque champ trouv\u00e9.

    Pour l'impl\u00e9mentation il est possible de profiter de la fonction strtok de la biblioth\u00e8que standard C, qui permet de d\u00e9couper une cha\u00eene en fonction d'un d\u00e9limiteur. Cependant, strtok est une fonction stateful, c'est-\u00e0-dire qu'elle conserve l'\u00e9tat interne entre les appels. On pr\u00e9f\u00e8rera donc utiliser une version s\u00e9curis\u00e9e thread-safe de cette fonction, strtok_s, qui prend en param\u00e8tre un pointeur vers un pointeur de cha\u00eene de caract\u00e8res. Cela permet de d\u00e9couper une cha\u00eene en plusieurs sous-cha\u00eenes sans interf\u00e9rer avec d'autres appels \u00e0 la fonction. Voici une impl\u00e9mentation possible de la fonction split :

    #include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n\ntypedef void (*FieldCallback)(const char *field);\n\nvoid split(const char *str, const char *delim, FieldCallback callback) {\n    if (str == NULL || delim == NULL || callback == NULL) {\n        fprintf(stderr, \"Invalid argument(s) passed to split.\\n\");\n        return;\n    }\n\n    // Copies str to avoid modifying the original. strtok is destructive, as\n    // it adds null terminators to the original string.\n    char *strCopy = strdup(str);\n    if (strCopy == NULL) {\n        fprintf(stderr, \"Memory allocation failed.\\n\");\n        return;\n    }\n\n    char *saveptr;\n    char *token = strtok_r(strCopy, delim, &saveptr);\n    while (token != NULL) {\n        callback(token);\n        token = strtok_r(NULL, delim, &saveptr);\n    }\n    free(strCopy);\n}\n\nvoid printField(const char *field) { printf(\"%s\\n\", field); }\n\nint main() {\n    const char *csvLine = \"apple,banana,orange,grape\";\n    split(csvLine, \",\", printField);\n}\n

    Dans le cas ou la cha\u00eene originale est non constante, et qu'elle peut \u00eatre modifi\u00e9e sans risque, il n'est pas n\u00e9cessaire de copier la cha\u00eene avant de l'envoyer \u00e0 strtok_r et le code de split peut \u00eatre simplifi\u00e9.

    Notez \u00e9galement que si la fonction callback utilise un m\u00e9canisme de longjmp pour sortir de la fonction, le free(strCopy) pourrait ne jamais \u00eatre appel\u00e9, ce qui entra\u00eenerait une fuite de m\u00e9moire.

    ", "tags": ["CSV", "strtok_r", "strtok_s", "strtok", "longjmp", "split", "callback"]}, {"location": "course-c/40-algorithms/utilities/#join", "title": "Join", "text": "

    L'op\u00e9ration de jointure est l'op\u00e9ration inverse de split. Elle consiste \u00e0 concat\u00e9ner plusieurs cha\u00eenes de caract\u00e8res en une seule, en les s\u00e9parant par un d\u00e9limiteur. Cette op\u00e9ration est couramment utilis\u00e9e pour g\u00e9n\u00e9rer des cha\u00eenes de requ\u00eates SQL, des URL, des cha\u00eenes de formatage, etc. Voici une impl\u00e9mentation possible de la fonction join en utilisant la biblioth\u00e8que standard C\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstatic size_t get_total_length(const char **strings, size_t count,\n                               const char *delimiter) {\n   size_t total_length = 0;\n   size_t delimiter_length = strlen(delimiter);\n\n   for (size_t i = 0; i < count; i++) {\n      total_length += strlen(strings[i]);\n      if (i < count - 1) total_length += delimiter_length;\n   }\n   return total_length + sizeof('\\0');\n}\n\nchar *join(const char **strings, size_t count, const char *delimiter) {\n   if (strings == NULL || count == 0) return NULL;\n\n   size_t total_length = get_total_length(strings, count, delimiter);\n\n   char *result = (char *)malloc(total_length);\n   if (result == NULL) {\n      fprintf(stderr, \"Memory allocation failed in %s\\n\", __func__);\n      return NULL;\n   }\n\n   result[0] = '\\0';\n   for (size_t i = 0; i < count; i++) {\n      strcat(result, strings[i]);\n      if (i < count - 1) strcat(result, delimiter);\n   }\n   return result;\n}\n\n// Exemple d'utilisation de la fonction `join`\nint main() {\n   const char *strings[] = {\"apple\", \"banana\", \"orange\", \"grape\"};\n   char *result = join(strings, 4, \", \");\n\n   if (result == NULL) fprintf(stderr, \"Error joining strings\\n\");\n\n   printf(\"%s\\n\", result);\n   free(result);  // Because `join` allocates memory,\n                  // it should be freed after use\n}\n
    ", "tags": ["join", "split"]}, {"location": "course-c/40-algorithms/utilities/#trim", "title": "Trim", "text": "

    La fonction trim permet de supprimer les espaces en d\u00e9but et en fin de cha\u00eene. Cette op\u00e9ration est couramment utilis\u00e9e pour nettoyer les cha\u00eenes de caract\u00e8res avant de les traiter. Voici une impl\u00e9mentation possible de la fonction trim.

    La fonction modifie la cha\u00eene en place en d\u00e9placant les caract\u00e8res non blancs vers le d\u00e9but de la cha\u00eene, puis en ajoutant un caract\u00e8re nul \u00e0 la fin de la cha\u00eene.

    #include <stdio.h>\n#include <ctype.h>\n\nvoid trim(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to trim.\\n\");\n        return;\n    }\n\n    // Identify the start of the string\n    char *start = str;\n    while (*start != '\\0' && isspace(*start)) start++;\n\n    // Shift the string to the left\n    char *end = start;\n    while (*end != '\\0') *(str++) = *(end++);\n    *str = '\\0';\n\n    // Remove trailing whitespaces\n    if (str > start) {\n        str--;\n        while (str >= start && isspace(*str)) *(str--) = '\\0';\n    }\n}\n

    Notons que cette impl\u00e9mentation n'est pas n\u00e9cessairement optimale car elle parcours la cha\u00eene de caract\u00e8res. Dans le cas o\u00f9 il est possible de modifier le pointeur de la cha\u00eene, il est possible de grandement simplifier l'algorithme en d\u00e9pla\u00e7ant directement le pointeur de d\u00e9but et en rajoutant une sentinelle \u00e0 la fin de la cha\u00eene.

    ", "tags": ["trim"]}, {"location": "course-c/40-algorithms/utilities/#chomp", "title": "Chomp", "text": "

    La fonction chomp permet de supprimer le caract\u00e8re de fin de ligne d'une cha\u00eene de caract\u00e8res. Ce caract\u00e8re est g\u00e9n\u00e9ralement un retour \u00e0 la ligne (\\n) ou un retour chariot (\\r). Le terme vient de l'action de \u00ab\u2009manger\u2009\u00bb le caract\u00e8re de fin de ligne, il a \u00e9t\u00e9 popularis\u00e9 par le langage de programmation Perl qui dispose d'une fonction chomp pour effectuer cette op\u00e9ration.

    Voici une impl\u00e9mentation possible de la fonction chomp :

    #include <stdio.h>\n#include <string.h>\n\nvoid chomp(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to chomp.\\n\");\n        return;\n    }\n\n    size_t len = strlen(str);\n    if (len > 0 && (str[len - 1] == '\\n' || str[len - 1] == '\\r')) {\n        str[len - 1] = '\\0';\n    }\n}\n
    ", "tags": ["chomp"]}, {"location": "course-c/40-algorithms/utilities/#reverse", "title": "Reverse", "text": "

    La fonction reverse permet d'inverser une cha\u00eene de caract\u00e8res. Cette op\u00e9ration est couramment utilis\u00e9e pour inverser le contenu d'une cha\u00eene, par exemple pour afficher un texte \u00e0 l'envers. N\u00e9anmoins notons que ce n'est pas une op\u00e9ration triviale en C.

    Sans informations suppl\u00e9mentaires sur la cha\u00eene de caract\u00e8re, il n'est pas possible de savoir si elle contient des caract\u00e8res multioctets (UTF-8, UTF-16, etc.) ou des caract\u00e8res de contr\u00f4le. Dans le cas de caract\u00e8res multioctets, il est n\u00e9cessaire de traiter la cha\u00eene de caract\u00e8res en tant que s\u00e9quence de caract\u00e8res UTF-32 et non pas en tant que s\u00e9quence d'octets. D'autre pas si la cha\u00eene de caract\u00e8res contient des caract\u00e8res de contr\u00f4le comme \\r\\n il ne suffit pas d'inverser les deux caract\u00e8res car \\n\\r n'est pas n\u00e9cessairement reconnu par votre syst\u00e8me.

    ", "tags": ["reverse"]}, {"location": "course-c/40-algorithms/utilities/#implementation-ascii", "title": "Impl\u00e9mentation ASCII", "text": "

    Voyons tout d'abord le cas trivial o\u00f9 la cha\u00eene de caract\u00e8res ne contient que des caract\u00e8res ASCII :

    #include <stdio.h>\n\nvoid swap(char *a, char *b) {\n    char tmp = *a;\n    *a = *b;\n    *b = tmp;\n}\n\nvoid reverse(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to reverse.\\n\");\n        return;\n    }\n\n    char *start = str, *end = str;\n    while (*end != '\\0') end++;\n    end--;\n\n    while (start < end) swap(start++, end--);\n}\n

    "}, {"location": "course-c/40-algorithms/utilities/#implementation-utf-8", "title": "Impl\u00e9mentation UTF-8", "text": "

    Dans cette impl\u00e9mentation plus compliqu\u00e9e, il est n\u00e9cessaire de convertir les caract\u00e8res UTF-8 multioctets en caract\u00e8res UTF-32 pour pouvoir les inverser correctement. Voici un exemple d'impl\u00e9mentation de la fonction reverse pour les cha\u00eenes de caract\u00e8res UTF-8. Elle utilise les fonctions mbrtoc32 et c32rtomb de la biblioth\u00e8que standard C pour la conversion entre UTF-8 et UTF-32\u2009:

    #include <locale.h>\n#include <stdio.h>\n#include <stdlib.h>  // Pour MB_CUR_MAX\n#include <string.h>\n#include <uchar.h>\n\nint main() {\n   setlocale(LC_ALL, \"\");  // Initialiser la locale pour UTF-8\n\n   char utf8_str[] = \"Salut \u0393\u03b9\u03ce\u03c1\u03b3\u03bf\u03c2, comment \u00e7a va ? As-tu re\u00e7u mon \ud83d\udce7 ?\";\n   size_t utf8_len = strlen(utf8_str);\n\n   // Convertir UTF-8 en UTF-32\n   char32_t utf32_str[utf8_len];\n   size_t utf32_len = 0;\n   {\n      mbstate_t state = {0};\n      size_t ret;\n      const char *p = utf8_str;\n      while (*p != '\\0') {\n         size_t ret = mbrtoc32(&utf32_str[utf32_len], p, MB_CUR_MAX, &state);\n         if (ret == (size_t)-1) {\n            perror(\"Erreur de conversion UTF-8 vers UTF-32\");\n            return 1;\n         } else if (ret == (size_t)-2) {\n            // S\u00e9quence multioctet incompl\u00e8te, passer \u00e0 l'octet suivant\n            break;\n         } else if (ret == 0) {\n            // Fin de la cha\u00eene UTF-8 atteinte\n            break;\n         }\n         p += ret;\n         utf32_len++;\n      }\n   }\n\n   // Inverser la cha\u00eene UTF-32\n   for (size_t i = 0, j = utf32_len - 1; i < j; i++, j--) {\n      char32_t tmp = utf32_str[i];\n      utf32_str[i] = utf32_str[j];\n      utf32_str[j] = tmp;\n   }\n\n   // Conversion inverse UTF-32 vers UTF-8\n   {\n      mbstate_t state = {0};\n      char *utf8_ptr = utf8_str;\n      const char32_t *utf32_ptr = utf32_str;\n      size_t utf8_total_len = 0;\n      size_t ret;\n      while (utf32_len--) {\n         ret = c32rtomb(utf8_ptr, *utf32_ptr++, &state);\n         if (ret == (size_t)-1) {\n            perror(\"Erreur de conversion UTF-32 vers UTF-8\");\n            return 1;\n         }\n         utf8_ptr += ret;  // Avancer dans le buffer UTF-8\n         utf8_total_len += ret;\n      }\n      utf8_str[utf8_total_len] = '\\0';\n   }\n\n   printf(\"%s\\n\", utf8_str);\n}\n
    ", "tags": ["c32rtomb", "reverse", "mbrtoc32"]}, {"location": "course-c/40-algorithms/popular-algorithms/a-star/", "title": "A-Star", "text": "

    L'algorithme A* (A-Star) est un algorithme de recherche de chemin qui permet de trouver le chemin le plus court entre un point de d\u00e9part et un point d'arriv\u00e9e. Il est tr\u00e8s utilis\u00e9 en intelligence artificielle, en robotique, en jeux vid\u00e9o, etc.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/conway/", "title": "Jeu de la vie de Conway", "text": "

    Le jeu de la vie de Conway est un automate cellulaire invent\u00e9 par le math\u00e9maticien John Horton Conway en 1970. C'est une simulation qui se d\u00e9roule sur une grille bidimensionnelle infinie. Chaque cellule de la grille peut \u00eatre dans un \u00e9tat mort ou vivant. L'\u00e9tat des cellules \u00e9volue en fonction de r\u00e8gles simples.

    Un automate cellulaire est un mod\u00e8le math\u00e9matique qui consiste en une grille de cellules qui peuvent \u00eatre dans un \u00e9tat donn\u00e9. Chaque cellule interagit avec ses voisines en fonction de r\u00e8gles pr\u00e9d\u00e9finies. Les automates cellulaires sont utilis\u00e9s pour mod\u00e9liser des ph\u00e9nom\u00e8nes naturels, des syst\u00e8mes biologiques, des simulations, etc.

    Chaque cellule peut \u00eatre dans un \u00e9tat 0 (morte) ou 1 (vivante) en fonction de r\u00e8gles de transition. Les r\u00e8gles de transition d\u00e9finissent comment l'\u00e9tat d'une cellule \u00e9volue en fonction de l'\u00e9tat de ses voisines. Dans un automate bidimentionnel, donc une grille bidimensionnelle, chaque cellule a 8 voisines. On nomme ces voisines le voisinage de Moore o\u00f9 chaque voisin est num\u00e9rot\u00e9 ainsi\u2009:

    Voisinage de Moore

    Le format B/S est utilis\u00e9 pour d\u00e9finir les r\u00e8gles de transition. B signifie birth (naissance) et S signifie survival (survie). Les r\u00e8gles sont d\u00e9finies par une liste de chiffres qui indiquent le nombre de voisins n\u00e9cessaires pour qu'une cellule naisse ou survive. Par exemple, la r\u00e8gle B3/S23 signifie qu'une cellule na\u00eet si elle a exactement 3 voisins et survit si elle a 2 ou 3 voisins. Certaines r\u00e8gles ont des noms sp\u00e9cifiques\u2009:

    Nom R\u00e8gle Description Game of Life B3/S23 La r\u00e8gle classique de Conway Mazes B3/S12345 Dessine une sorte de labirynthe Mazectric B3/S1234 Une autre variante HighLife B36/S23 Une variante de Conway Day & Night B3678/S34678 Une autre variante"}, {"location": "course-c/40-algorithms/popular-algorithms/conway/#implementation", "title": "Impl\u00e9mentation", "text": "

    Pour impl\u00e9menter le jeu de la vie de Conway, il faut une grille et un pas temporel pour faire \u00e9voluer les cellules. On peut utiliser un tableau \u00e0 deux dimensions pour repr\u00e9senter la grille. Chaque cellule est repr\u00e9sent\u00e9e par un 0 (mort) ou un 1 (vivant). On peut bien entendu utiliser un tableau de taille fixe ou un tableau dynamique pour repr\u00e9senter la grille.

    A chaque pas de temps, on applique les r\u00e8gles de transition pour chaque cellule. On peut utiliser un tableau temporaire pour stocker les nouvelles valeurs des cellules. On peut aussi utiliser un seul tableau pour stocker les valeurs actuelles et futures des cellules. Il suffit de basculer entre les deux tableaux \u00e0 chaque pas de temps.

    La complexit\u00e9 de l'algorithme est en \\(O(n^2)\\) o\u00f9 \\(n\\) est le nombre de cellules dans la grille. On sait que ce type d'algorithme est tr\u00e8s gourmand en ressources et on est en droit de se demander s'il est possible de faire mieux.

    L'algorithme de Hashlife est une optimisation de l'algorithme de Conway qui permet de r\u00e9duire la complexit\u00e9 de l'algorithme \u00e0 \\(O(n \\log n)\\). Il est bas\u00e9 sur une structure de donn\u00e9es appel\u00e9e quadtree qui permet de stocker les cellules vivantes de mani\u00e8re compacte. L'algorithme de Hashlife est plus complexe \u00e0 impl\u00e9menter mais il permet de g\u00e9rer des grilles de taille importante de mani\u00e8re plus efficace au d\u00e9triments de la complexit\u00e9 de l'algorithme et d'un espace de stockage plus important.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-exp/", "title": "Exponentiation rapide", "text": "

    Cet algorithme permet de calculer rapidement des puissances enti\u00e8res (\\(a^n\\)). La m\u00e9thode na\u00efve consiste \u00e0 calculer les puissances avec une boucle\u2009:

    long long pow(long long a, long long n) {\n    for (int i = 0; i < n - 1; i++) {\n        a *= a;\n    }\n}\n

    La complexit\u00e9 de cet algorithme est \\(O(n)\\). Il est possible de faire mieux en \\(O(n log n)\\).

    long long bin_pow(long long a, long long b) {\n    if (b == 0) return 1;\n    long long res = bin_pow(a, b / 2);\n    return res * res * (b % 2 ? a : 1);\n}\n

    Comme \u00e9voqu\u00e9 plus haut, un algorithme r\u00e9cursif est souvent moins performant que sa variante it\u00e9rative. Voici l'impl\u00e9mentation it\u00e9rative\u2009:

    long long bin_pow(long long a, long long b) {\n    long long res = 1;\n    while (b > 0) {\n        if (b & 1) res = res * a;\n        a *= a;\n        b /= 2;\n    }\n    return res;\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/", "title": "Racine carr\u00e9e inverse rapide", "text": "

    Quake III Arena

    Cet algorithme a \u00e9t\u00e9 d\u00e9velopp\u00e9 chez Silicon Graphics au d\u00e9but des ann\u00e9es 90. Il a \u00e9t\u00e9 utilis\u00e9 dans des jeux vid\u00e9os comme Quake III Arena pour am\u00e9liorer la performance du calcul des angles d'incidence dans la r\u00e9flexion des lumi\u00e8res et est attribu\u00e9 \u00e0 John Carmack, un des fondateurs de id Software, qui a publi\u00e9 le code source de Quake III Arena en 2005.

    Il est utilis\u00e9 pour les vecteurs normaux dans les calculs de r\u00e9flexion de la lumi\u00e8re.

    R\u00e9flexion de la lumi\u00e8re

    float Q_rsqrt(float number)\n{\n    const float threehalfs = 1.5F;\n\n    float x2 = number * 0.5F;\n    float y = number;\n    long i = *(long *) &y; // Evil floating point bit level hacking\n    i = 0x5f3759df - (i >> 1); // What the fuck?\n    y = *(float *) &i;\n    y = y * (threehalfs - (x2 * y * y)); // 1st iteration\n#if BETTER\n    y = y * (threehalfs - (x2 * y * y)); // 2nd iteration\n#endif\n    return y;\n}\n

    Cet algorithme de racine carr\u00e9e inverse rapide utilise une constante magique 0x5f3759df. L'impl\u00e9mentation propos\u00e9e ci-dessus est extraite du code source du jeu Quake III arena (q_math.c) disponible sur GitHub.

    Ce n'est pas un algorithme tr\u00e8s acad\u00e9mique, il s'agit d'un kludge, une solution irrespectueuse des r\u00e8gles de l'art de la programmation, car la valeur y est transtyp\u00e9e en un long (i = *(long *)&y. C'est cette astuce qui permet de tirer avantage que les valeurs en virgule flottantes sont exprim\u00e9es en puissances de 2.

    ", "tags": ["long"]}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/", "title": "Fast sin", "text": ""}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#sinus-rapide", "title": "Sinus rapide", "text": "

    Dans des architectures sans support pour les nombres r\u00e9els (comme les processeurs ne supportant pas les op\u00e9rations en virgule flottante IEEE 754), il est courant d'utiliser des approximations polynomiales pour calculer des fonctions math\u00e9matiques comme le sinus. Calculer un sinus n'est pas une simple op\u00e9ration, comme l'addition ou la multiplication, et il n'existe pas d'algorithme trivial pour obtenir une valeur exacte.

    Les processeurs modernes, lorsqu'ils calculent le sinus, utilisent souvent des tables de valeurs pr\u00e9d\u00e9finies (appel\u00e9es tables de sinus) stock\u00e9es en m\u00e9moire. Ces tables contiennent des valeurs pr\u00e9-calcul\u00e9es du sinus pour diff\u00e9rents angles, g\u00e9n\u00e9ralement entre 0 et \\(\\pi/2\\). Lorsqu'un sinus doit \u00eatre calcul\u00e9, le processeur se base sur la valeur dans la table la plus proche de l'angle donn\u00e9, puis utilise une interpolation souvent lin\u00e9aire pour obtenir un r\u00e9sultat plus pr\u00e9cis. Cette m\u00e9thode permet d'\u00e9conomiser du temps de calcul au prix d'une petite perte de pr\u00e9cision. Voici une mani\u00e8re de faire\u2009:

    \\[ \\sin(\\theta) \\approx \\sin(\\theta_1) + \\frac{\\theta - \\theta_1}{\\theta_2 - \\theta_1} \\times (\\sin(\\theta_2) - \\sin(\\theta_1)) \\]

    o\u00f9 \\(\\sin(\\theta_1)\\) et \\(\\sin(\\theta_2)\\) sont les valeurs de sinus les plus proches de \\(\\theta\\) dans la table, \\(\\theta\\) est l'angle pour lequel le sinus doit \u00eatre calcul\u00e9. La recherche dans la table peut \u00eatre simplement une table de hachage pour un acc\u00e8s en \\(O(1)\\). La table serait pr\u00e9-calcul\u00e9e et stock\u00e9e sous forme d'un tableau.

    #include <stdio.h>\n\n#define TABLE_SIZE 1024\n\ndouble sin_table[TABLE_SIZE];\n\n// Computed on a powerful machine (with floating point support)\nvoid init_sin_table() {\n    for (int i = 0; i < TABLE_SIZE; ++i)\n        sin_table[i] = sin((double)i / TABLE_SIZE * M_PI / 2);\n}\n\ndouble sin_fast(double angle) {\n    double x = angle / (M_PI / 2) * TABLE_SIZE;\n    int i = (int)x;\n    double frac = x - i;\n    return sin_table[i] + frac * (sin_table[i + 1] - sin_table[i]);\n}\n

    Dans une architecture l\u00e9g\u00e8re qui ne dispose pas de support pour les nombres en virgule flottante, on utilise plut\u00f4t des approximations en virgule fixe. Ces approximations polynomiales, comme le d\u00e9veloppement de Taylor, permettent de calculer des sinus avec une pr\u00e9cision acceptable, tout en restant dans le domaine des entiers.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#approximation-du-cinquieme-ordre", "title": "Approximation du cinqui\u00e8me ordre", "text": "

    L'objectif p\u00e9dagogique est de montrer que les math\u00e9matiques peuvent \u00eatre tr\u00e8s utiles dans l'\u00e9laboration d'un algorithme. Nous allons voir comment approximer un sinus en utilisant un polyn\u00f4me de degr\u00e9 5 en utilisant les technologies suivantes\u2009:

    • Syst\u00e8mes d'\u00e9quations lin\u00e9aires pour r\u00e9soudre les coefficients du polyn\u00f4me\u2009;
    • Calcul int\u00e9gral pour minimiser l'erreur moyenne (moindre carr\u00e9s);
    • Virgule fixe pour repr\u00e9senter les nombres en m\u00e9moire.

    Le domaine du sinus est infini mais il est possible de le r\u00e9duire \u00e0 l'intervalle ci-dessous car toutes les autres sorties peuvent \u00eatre obtenues en utilisant les propri\u00e9t\u00e9s de sym\u00e9trie du sinus. En effet il suffit du dessin du quart de sinus pour obtenir le sinus complet.

    \\[ x \\in [0, \\frac{\\pi}{2}] \\]

    D'autre part, le sinus est une fonction impaire, c'est-\u00e0-dire que \\(\\sin(-x) = -\\sin(x)\\). Cette propri\u00e9t\u00e9 induit, par exemple lors d'une d\u00e9composition en s\u00e9rie de Fourier, que les coefficients de la s\u00e9rie pour les termes pairs sont nuls. Cela signifie aussi que le sinus peut \u00eatre approxim\u00e9 par un polyn\u00f4me de degr\u00e9 impair. On peut donc tenter d'approximer un sinus par un polyn\u00f4me de degr\u00e9 5 en ayant seulement 3 coefficients \u00e0 calculer. D'autre part, afin de faciliter les calculs et la repr\u00e9sentation en virgule fixe on peut r\u00e9duire l'intervalle de calcul \u00e0 \\([0, 1]\\) en posant que\u2009:

    \\[ z = \\frac{2x}{\\pi} \\]

    Le polyn\u00f4me de degr\u00e9 5 peut \u00eatre exprim\u00e9 de la mani\u00e8re suivante\u2009:

    \\[ \\sin(z) \\approx S_5(z) = cz - bz^**3** + az^5 \\]

    Pour trouver les coefficients de ce polyn\u00f4me, on peut utiliser les propri\u00e9t\u00e9s du sinus et de ses d\u00e9riv\u00e9es. Nous allons donc raisonner sur ces deux fonctions\u2009:

    \\[ \\begin{cases} S_5(z) = az^5 - bz^3 + cz \\\\ S_5'(z) = 5az^4 - 3bz^2 + c \\end{cases} \\]

    On peut noter quelques conditions aux bords de l'intervalle \\([0, 1]\\) :

    • \\(S_5(1) = 1\\) qui est \u00e9quivalent \u00e0 \\(\\sin(\\frac{\\pi}{2}) = 1\\);
    • \\(S_5'(1) = 0\\) car la pente est nulle en son sommet\u2009;
    • \\(S_5'(0) = \\frac{\\pi}{2}\\) car la pente est maximale en 0.

    Ces conditions nous permettent d'obtenir trois \u00e9quations lin\u00e9aires qu'il est trivial de r\u00e9soudre\u2009:

    \\[ \\begin{cases} 1 = a - b + c \\\\ 0 = 5a - 3b + 5c \\\\ \\frac{\\pi}{2} = a \\end{cases} \\]

    Ce qui nous donne les coefficients suivants\u2009:

    \\[ a = \\frac{\\pi}{2},\\quad b = \\pi-\\frac{5}{2},\\quad c = \\frac{\\pi}{2}-\\frac{3}{2} \\]

    Que l'on peut simplifier en fonction de \\(a\\) :

    \\[ a = \\frac{\\pi}{2},\\quad b = 2a-\\frac{5}{2},\\quad c = a-\\frac{3}{2} \\]

    Cette solution triviale n'est g\u00e9n\u00e9ralement pas optimale car un crit\u00e8re fondamental n'a pas \u00e9t\u00e9 respect\u00e9, celui de minimiser l'erreur moyenne avec le sacrifice potentiel de la pr\u00e9cision des valeurs extr\u00eames. Une approche plus rigoureuse consiste donc minimiser l'erreur moyenne sur l'intervalle \\([0, 1]\\). On utilise pour ce faire la m\u00e9thode des moindre carr\u00e9s qui consiste \u00e0 minimiser l'erreur quadratique est la somme des carr\u00e9s des diff\u00e9rences entre la fonction cible \\( \\sin\\left(\\frac{\\pi}{2}x\\right) \\) et l'approximation polynomiale \\( p(x) \\). Pour ce faire, on calcule l'int\u00e9grale suivante\u2009:

    \\[ E(a, b, c) = \\int_0^1 \\left( \\sin\\left(\\frac{\\pi}{2}x\\right) - (az^5 - bz^3 + cz) \\right)^2 dx \\]

    Pour minimiser l'erreur nous avons besoin de trouver les coefficients \\(a\\), \\(b\\) et \\(c\\) qui minimisent cette int\u00e9grale. Cela revient \u00e0 r\u00e9soudre en prenant les d\u00e9riv\u00e9es partielles de l'erreur quadratique \\(E(a, b, c)\\) par rapport \u00e0 \\(a\\), \\(b\\) et \\(c\\). Cela nous donne trois \u00e9quations\u2009:

    \\[ \\begin{cases} \\frac{\\partial E}{\\partial a} = 0 \\\\ \\frac{\\partial E}{\\partial b} = 0 \\\\ \\frac{\\partial E}{\\partial c} = 0 \\end{cases} \\]

    Ces trois \u00e9quations sont ind\u00e9pendantes, ce qui signifie qu'il est possible de r\u00e9soudre ce syst\u00e8me.

    Pratiquement on aura plut\u00f4t recours \u00e0 une r\u00e9solution num\u00e9rique par exemple avec Python\u2009:

    import numpy as np\nfrom scipy.integrate import quad\nfrom scipy.linalg import solve\n\n\ndef sin_pi_over_2(x):\n    return np.sin(np.pi * x / 2)\n\n\ndef z_power_n(n, x):\n    return x**n\n\n\nintegrals_matrix = np.zeros((3, 3))\nfor i, n1 in enumerate([5, 3, 1]):\n    for j, n2 in enumerate([5, 3, 1]):\n        integrals_matrix[i, j] = quad(lambda x: z_power_n(n1 + n2, x), 0, 1)[0]\n\nintegrals_rhs = np.zeros(3)\nfor i, n in enumerate([5, 3, 1]):\n    integrals_rhs[i] = quad(lambda x: sin_pi_over_2(x) * z_power_n(n, x), 0, 1)[0]\n\na, b, c = solve(integrals_matrix, integrals_rhs)\nprint(a, b, c)\n

    Ce qui nous donnes les coefficients suivants\u2009:

    \\[ a = 1.5704372550337553, \\quad b = 0.6427098943803898, \\quad c = 0.07243339903924052 \\]

    Une autre approche (j'ai toujours pas compris) est la suivante\u2009:

    \\[ a = 4(\\frac{3}{\\pi}-\\frac{9}{16}), \\quad b = 2a-\\frac{5}{2}, \\quad c = a-\\frac{3}{2} \\]

    Sur une architecture en virgule fixe comme un microcontr\u00f4leur MSP430 16-bit, on peut par exemple utiliser un entier de 16-bit pour repr\u00e9senter un angle. Ce dernier peut \u00eatre exprim\u00e9 en radians en compl\u00e9ment \u00e0 deux et en format Q15.1. Cela signifie que la partie enti\u00e8re repr\u00e9sente les radians et la partie fractionnaire repr\u00e9sente les d\u00e9cimales. Par exemple, 1.0 en Q15.1 est repr\u00e9sent\u00e9 par 0x8000. En termes binaire notre \u00e9quation devra \u00eatre multipli\u00e9e par \\(2^{15}\\) pour obtenir un r\u00e9sultat correct. On peut rendre g\u00e9n\u00e9rique cette d\u00e9finition en d\u00e9clarant \\(o\\) l'exponent de la puissance de 2 \u00e0 appliquer. Notre \u00e9quation compl\u00e8te est donc\u2009:

    \\[ fpsin_5(x) = \\left. 2^o\\left(az - bz^3 + cz^5\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Or, l'\u00e9quation doit pouvoir \u00eatre r\u00e9\u00e9crite seulement en terme de multiplications d'entiers, d'additions et de d\u00e9calages. En outre, il est essentiel de factoriser au maximum l'\u00e9quation pour \u00e9viter tout calcul redondant. On commence par factoriser l'\u00e9quation\u2009:

    \\[ fpsin_5(x) = \\left. z2^o\\left(a - z^2\\left(b + cz^2\\right)\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Comme \\(z\\in[0, 1]\\) n'est pas repr\u00e9sentable par un entier, on peut r\u00e9\u00e9crire \\(z\\) comme \\(y/2^o\\) avec \\(y\\in[0, 2^o]\\) et o\u00f9 \\(o\\) est le nombre de bits de la partie fractionnaire du domaine d'entr\u00e9e. On obtient alors\u2009:

    \\[ fpsin_5(x) = \\left. \\left[\\frac{y}{2^n}\\right]2^o\\left(a - \\left[\\frac{y}{2^n}\\right]^2\\left(b + c\\left[\\frac{y}{2^n}\\right]^2\\right)\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Apr\u00e8s simplification on obtient\u2009:

    \\[ fpsin_5(x) = \\left. y2^{o-n}\\Big(a-y2^{-n}y2^{-n}\\left(b-y2^{-2n}cy\\right)\\Big)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Lors d'une multiplication en virgule fixe, on s'int\u00e9resse \u00e0 la partie haute de la multiplication. En effet, pour un produit standard de deux entiers 8-bits, le r\u00e9sultat est un entier 16-bits. C'est d'ailleurs la raison pour laquelle les ALU offrent un registre de r\u00e9sultat deux fois plus grand que les registres d'entr\u00e9e. N\u00e9anmoins, ce sont les 8-bits de poids faible qui sont conserv\u00e9s car si la multiplication n'a pas de d\u00e9passement, le r\u00e9sultat tiendra dans les 8-bits de poids faible. On supprime donc les 8-bit de pods fort. Or, dans un calcul en virgule fixe, la logique est diff\u00e9rente. Un nombre en Q1.7 peut exprimer des grandeurs entre -1 et 1 avec une pr\u00e9cision de 1/128. En multipliant deux nombre en Q1.7, on obtient un r\u00e9sultat en Q2.14. Or, dans ce r\u00e9sultat 16-bit, l'octet de poid faible ne contient que les chiffres apr\u00e8s la virgule et none la partie int\u00e9ressante du calcul. On peut donc supprimer cet octet de poids faible et ne conserver que les 8-bits de poids fort formant un nombre en Q2.6. En C standard, il n'est pas possible d'ordonner au compilateur de choisir le mot de poids fort ou faible. En assembleur en revanche de nombreux processeurs offrent cette possibilit\u00e9s. Un ADSP-218x par exemple offre des instructions de multiplication en virgule fixe avec un mot de r\u00e9sultat de 32-bits. On peut alors choisir de conserver le mot de poids fort ou faible.

    Une m\u00e9thode pour maximiser la pr\u00e9cision des calculs est d'ajouter des facteurs d'\u00e9chelle. Par exemple, le coefficient \\(a\\) vaut environ \\(1.57\\) ce qui repr\u00e9sente pour la partie enti\u00e8re 1 bit. Avec une ALU d'une profondeur \\(m\\): 32-bits, on peut se permettre de d\u00e9caler \u00e0 gauche ce nombre tel que l'\u00e9quation suivante est satisfaite\u2009:

    \\[ \\text{sign}(a)\\cdot\\lceil |\\log_2(a)| \\rceil + k <= m \\]

    o\u00f9 \\(k\\) est le d\u00e9calage en bits.

    Si l'on ajoute un facteur d'\u00e9chelle, il doit n\u00e9cessairement \u00eatre compens\u00e9 dans l'\u00e9quation. Commencon\u00e7

    en virgule fixe on a int\u00e9r\u00eat \u00e0 le multiplier

    Afin de maximiser la pr\u00e9cision, on a besoin que nos multiplications occupent le plus de bits possibles pour le type choisi. \u00c0 cette fin, on peut introduire des facteurs d'\u00e9chelle \\(2^p\\), \\(2^q\\) et \\(2^r\\) sachant qu'ils s'annuleront et n'affecterons pas le r\u00e9sultat final. On peut alors r\u00e9\u00e9crire l'\u00e9quation en introduisant ces facteurs d'\u00e9chelle\u2009:

    \\[ \\begin{aligned} = y2^{p-n}\\left(a-y2^{-n}y{2^-n}\\left[b-2^{-r}y2^{-2n}2^rcy\\right]\\right)2^{a}\\\\ = y2^{p-n}\\left(a-2^{-p}y2^{-n}y{2^-n}\\left[2^Pb-2^{-r}y2^{-2n}2^{r+p}cy\\right]\\right)2^{a}\\\\ = y2^{-n}\\left(2^qa-2^{q-p}y2^{-n}\\left[2^Pb-2^{-r}y2^{-n}2^{r+p-n}cy\\right]\\right)2^{a-q}\\\\ \\end{aligned} \\]

    En red\u00e9finissant les constantes \\(A\\), \\(B\\) et \\(C\\) comme suit\u2009:

    \\[ A = 2^qa B = 2^Pb C = 2^{r+p-n}c \\]

    On obtient l'\u00e9quation finale\u2009:

    \\[ = y2^{-n}\\left(A-2^{q-p}y2^{-n}y2^{-n}\\left[B-2^{-r}y2^{-n}Cy\\right]\\right)2^{a-q} \\]

    On peut maintenant essayer de maximiser chaque multiplication en travaillant depuis l'int\u00e9rieur de l'\u00e9quation en direction de l'ext\u00e9rieur de fa\u00e7on \u00e0 ce que le produit soit exactement 32-bit. On peut \u00e9galement ajuster l'\u00e9chelle du r\u00e9sultat en virgule fixe pour obtenir une pr\u00e9cision maximale. Le r\u00e9sultat final est\u2009:

    a = 12;\nn = 13;\np = 32;\nq = 31;\nr = 3;\n\nA = 3370945099;\nB = 2746362156;\nC = 292421;\n

    Enfin, on peut impl\u00e9menter cette approximation en C\u2009:

    #include <stdio.h>\n#include <stdint.h>\n#include <math.h>\n\nint16_t fpsin(int16_t i) {\n   i <<= 1;\n   uint8_t c = i < 0;\n\n   if (i == (i | 0x4000)) i = (1 << 15) - i;\n   i = (i & 0x7FFF) >> 1;\n\n   enum { A1 = 3370945099UL, B1 = 2746362156UL, C1 = 292421UL };\n   enum { n = 13, p = 32, q = 31, r = 3, a = 12 };\n\n   uint32_t y = (C1 * ((uint32_t)i)) >> n;\n   y = B1 - (((uint32_t)i * y) >> r);\n   y = (uint32_t)i * (y >> n);\n   y = (uint32_t)i * (y >> n);\n   y = A1 - (y >> (p - q));\n   y = (uint32_t)i * (y >> n);\n   y = (y + (1UL << (q - a - 1))) >> (q - a);\n\n   return c ? -y : y;\n}\n\nint main() {\n    // Quelques angles : (0, pi/2, pi, 3pi/2, 2pi)\n    const int16_t angles[] = {0, 16384, 32768, 49152, 65536};\n\n    printf(\"Angle (deg)\\tFixed-Point Sin\\tStandard Sin\\tError\\n\");\n    for (int i = 0; i < 5; ++i) {\n        int16_t angle = angles[i];\n        double radians = (double)angle / 65536.0 * M_PI;\n\n        int16_t fpsin_result = fpsin(angle);\n        double sin_result = sin(radians);\n        double sin_fixed_point = sin_result * 4096.0;\n        double error = fpsin_result - sin_fixed_point;\n\n        printf(\"%d\\t\\t%d\\t\\t%.2f\\t\\t%.2f\\n\", (int)(radians * 180.0 / M_PI),\n            fpsin_result, sin_fixed_point, error);\n    }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#demonstration-graphique-avec-sdl", "title": "D\u00e9monstration graphique avec SDL", "text": "

    Voici un exemple qui utilise SDL pour afficher un graphe montrant le sinus en virgule fixe et l'erreur par rapport \u00e0 la fonction sinus standard.

    #include <SDL2/SDL.h>\n#include <math.h>\n\n#define SCREEN_WIDTH 640\n#define SCREEN_HEIGHT 480\n\nvoid draw_graph(SDL_Renderer *renderer) {\n    // Dessine le sinus en virgule fixe et la version standard\n    for (int x = 0; x < SCREEN_WIDTH; x++) {\n        // Mappe x \u00e0 un angle (entre 0 et 2pi)\n        double angle = (double)x / SCREEN_WIDTH * 2.0 * M_PI;\n        int y_fixed = (int)((fpsin((int16_t)(angle * 65536.0 / (2.0 * M_PI))) / 4096.0) * SCREEN_HEIGHT / 2 + SCREEN_HEIGHT / 2);\n        int y_standard = (int)((sin(angle) * SCREEN_HEIGHT / 2) + SCREEN_HEIGHT / 2);\n\n        // Sinus standard en rouge\n        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);\n        SDL_RenderDrawPoint(renderer, x, y_standard);\n\n        // Sinus en virgule fixe en vert\n        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);\n        SDL_RenderDrawPoint(renderer, x, y_fixed);\n\n        // Erreur en bleu\n        int error = y_fixed - y_standard;\n        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);\n        SDL_RenderDrawPoint(renderer, x, SCREEN_HEIGHT / 2 - error);\n    }\n}\n\nint main(int argc, char *argv[]) {\n    SDL_Window *window = NULL;\n    SDL_Renderer *renderer = NULL;\n\n    if (SDL_Init(SDL_INIT_VIDEO) < 0) {\n        printf(\"SDL could not initialize! SDL_Error: %s\\n\", SDL_GetError());\n        return 1;\n    }\n\n    window = SDL_CreateWindow(\"Sinus Approximation\", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);\n    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n\n    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);\n    SDL_RenderClear(renderer);\n\n    draw_graph(renderer);\n    SDL_RenderPresent(renderer);\n\n    SDL_Event e;\n    int quit = 0;\n    while (!quit) {\n        while (SDL_PollEvent(&e) != 0) {\n            if (e.type == SDL_QUIT) {\n                quit = 1;\n            }\n        }\n    }\n\n    SDL_DestroyRenderer(renderer);\n    SDL_DestroyWindow(window);\n    SDL_Quit();\n\n    return 0;\n}\n

    from sympy import symbols, Eq, solve, pi

    a, b, c = symbols('a b c')

    solution = solve([ Eq(a - b + c, 1), Eq(a - 3*b + 5*c, 0), Eq(a/2 - b/4 + c/6, 2/pi) ], (a, b, c))

    solution

    "}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/", "title": "Codage de Huffman", "text": "

    Le codage de Huffman est un algorithme de compression sans perte qui permet de r\u00e9duire la taille des fichiers en utilisant des codes de longueur variable pour repr\u00e9senter les caract\u00e8res. L'algorithme repose sur l'id\u00e9e que les caract\u00e8res les plus fr\u00e9quents dans un texte peuvent \u00eatre repr\u00e9sent\u00e9s par des codes plus courts, tandis que les caract\u00e8res les moins fr\u00e9quents sont repr\u00e9sent\u00e9s par des codes plus longs.

    Il est utilis\u00e9 dans de nombreux formats de fichiers comme le format PNG, JPEG et MP3.

    Prenons le texte ABRACADABRA. Il y a des lettres qui reviennent plus souvent que d'autres et des lettres de l'alphabet qui sont absentes. Pourquoi donc repr\u00e9senter chaque caract\u00e8re sur 1 octet\u2009? On pourrait utiliser un code de longueur variable. Par exemple, la lettre A pourrait \u00eatre repr\u00e9sent\u00e9e par 0, la lettre B par 10 et la lettre R par 11. Il faudrait \u00e9galement d\u00e9finir une table de correspondance pour d\u00e9coder le texte. C'est le principe de l'abre de Huffman.

    ", "tags": ["ABRACADABRA"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#comment-ca-marche", "title": "Comment \u00e7a marche\u2009?", "text": "

    Pour notre entr\u00e9e ABRACADABRA, nous allons suivre les \u00e9tapes suivantes\u2009:

    ", "tags": ["ABRACADABRA"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#nombre-doccurences", "title": "Nombre d'occurences", "text": "

    On commence par compter la fr\u00e9quence de chaque caract\u00e8re. On obtient\u2009:

    Fr\u00e9quence de Huffman Caract\u00e8re Fr\u00e9quence A 5 B 2 R 2 C 1 D 1

    Chaque \u00e9l\u00e9ment est un noeud qui est plac\u00e9 dans une file de priorit\u00e9 (min-heap) o\u00f9 la priorit\u00e9 est la fr\u00e9quence du caract\u00e8re. Voici le pseudo code du tas minimum\u2009:

    Min-Heap : [(1, 'C'), (1, 'D'), (2, 'B'), (2, 'R'), (5, 'A')]\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#fusion", "title": "Fusion", "text": "

    On va fusionner les deux noeuds de plus faible fr\u00e9quence, c'est facile parce qu'un min-heap nous permet de r\u00e9cup\u00e9rer les deux \u00e9l\u00e9ments de plus faible fr\u00e9quence en temps constant. Apr\u00e8s fusion, on obtient une cha\u00eene de caract\u00e8re qui repr\u00e9sente les deux noeuds fusionn\u00e9s. On ajoute ce nouveau noeud \u00e0 la file de priorit\u00e9 en prenant en compte la fr\u00e9quence totale des deux noeuds fusionn\u00e9s.

    Notons que qu'en cas de priorit\u00e9 \u00e9gale, on peut choisir arbitrairement l'ordre des noeuds.

    Les noeuds C et D sont les deux noeuds de plus faible fr\u00e9quence. On les fusionne pour obtenir CD qui a une priorit\u00e9 de 2.

    Min-Heap : [(2, 'CD'), (2, 'B'), (2, 'R'), (5, 'A')]\n
    graph TD\nCD(\"CD (2)\") --> C(\"C (1)\")\nCD --> D(\"D (1)\")

    Il nous reste des noeuuds \u00e0 fusionner. On fusionne les noeuds CD et B pour obtenir CDB qui a une priorit\u00e9 de 4.

    Min-Heap : [(2, 'R'), (4, 'CDB'), (5, 'A')]\n
    graph TD\n    CDB(\"CDB (4)\") --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")

    On continue car il nous reste des noeuds \u00e0 fusionner. On fusionne les noeuds CDB et R pour obtenir CDBR qui a une priorit\u00e9 de 6.

    Min-Heap : [(5, 'A'), (6, 'RCDB')]\n
    graph TD\n    RCDB(\"RCDB (6)\") --> R(\"R (2)\")\n    RCDB --> CDB(\"CDB (4)\")\n    CDB --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")

    Enfin, on fusionne les noeuds RCDB et A pour obtenir ACDBR qui a une priorit\u00e9 de 11.

    Min-Heap : [(11, 'ARCDB')]\n
    graph TD\n    ARCDB(\"ARCDB (11)\") --> A(\"A (5)\")\n    ARCDB --> RCDB(\"RCDB (6)\")\n    RCDB --> R(\"R (2)\")\n    RCDB --> CDB(\"CDB (4)\")\n    CDB --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")
    ", "tags": ["RCDB", "CDBR", "ACDBR", "CDB"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#generation-des-codes", "title": "G\u00e9n\u00e9ration des codes", "text": "

    Pour g\u00e9n\u00e9rer les codes, on parcourt l'arbre de Huffman en partant de la racine. On ajoute un 0 \u00e0 chaque fois qu'on descend \u00e0 gauche et un 1 \u00e0 chaque fois qu'on descend \u00e0 droite. Les noeuds fusionn\u00e9s sont des noeuds internes, on ne les prend pas en compte.

    Caract\u00e8re Code A 0 R 10 B 111 C 1100 D 1101"}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#encodage-du-texte", "title": "Encodage du texte", "text": "

    Une fois les codes g\u00e9n\u00e9r\u00e9s, on peut encoder le texte en rempla\u00e7ant chaque caract\u00e8re par son code.

    A B   R  A C    A C    A B   R  A\n0 111 10 0 1100 0 1100 0 111 10 0\n

    Comme les donn\u00e9es sont n\u00e9cessairement align\u00e9es sur des octets en m\u00e9moire, il est possible que le dernier octet contienne des bits inutilis\u00e9s, on les remplace par des z\u00e9ros.

    01111001'10001100'01111000\n                         - Remplissage (padding)\n

    Bien entendu il est n\u00e9cessaire d'encoder \u00e9galement la table de Huffmann pour pouvoir d\u00e9coder le texte. Une m\u00e9thode courante est d'encoder directement la table de Huffmann dans le fichier compress\u00e9.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/", "title": "Algorithmes d'infographie", "text": "

    Les algorithmes d'infographie sont des algorithmes utilis\u00e9s pour g\u00e9n\u00e9rer des images num\u00e9riques. Ils sont utilis\u00e9s dans de nombreux domaines, tels que les jeux vid\u00e9o, les films d'animation, la r\u00e9alit\u00e9 virtuelle, etc.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#bressenham", "title": "Bressenham", "text": "

    L'algorithme de Bresenham est une m\u00e9thode efficace pour tracer des lignes droites sur une grille de pixels en utilisant uniquement des op\u00e9rations enti\u00e8res. Il est particuli\u00e8rement adapt\u00e9 aux \u00e9crans d'ordinateur o\u00f9 les positions des pixels sont discr\u00e8tes. L'algorithme d\u00e9termine quels pixels doivent \u00eatre allum\u00e9s pour former la meilleure approximation possible d'une ligne droite entre deux points.

    Algorithme de Bressenham

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#principe-de-lalgorithme", "title": "Principe de l'algorithme", "text": "

    Consid\u00e9rons le trac\u00e9 d'une ligne entre deux points\u2009:

    $\\(p=(x_0, y_0), q=(x_1, y_1)\\)

    Tel que pour chaque position \\(x\\), la valeur de \\(y\\) qui correspond le mieux \u00e0 la ligne id\u00e9ale.

    L'algorithme utilise une variable d'erreur pour d\u00e9cider quand incr\u00e9menter \\(y\\). Cette variable repr\u00e9sente la distance entre la position r\u00e9elle de la ligne id\u00e9ale et la position actuelle sur la grille de pixels. L'\u00e9quation de la variable d'erreur est mise \u00e0 jour \u00e0 chaque it\u00e9ration pour refl\u00e9ter cette distance. Cet algorithme est un outil puissant pour le trac\u00e9 efficace de lignes sur une grille de pixels. En utilisant uniquement des op\u00e9rations enti\u00e8res, il optimise les performances, ce qui est crucial pour les applications graphiques en temps r\u00e9el.

    void bresenhamLine(Point p, Point q, SDL_Renderer *renderer) {\n   int dx = abs(q.x - p.x), dy = abs(q.y - p.y);\n   int sx = p.x < q.x ? 1 : -1, sy = p.y < q.y ? 1 : -1;\n   int err = dx - dy;\n\n   for (;;) {\n      setPixel(renderer, p.x, p.y, 1.0f);\n      if (p.x == q.x && p.y == q.y) break;\n      int e2 = 2 * err;\n      if (e2 > -dy) {\n         err -= dy;\n         p.x += sx;\n      }\n      if (e2 < dx) {\n         err += dx;\n         p.y += sy;\n      }\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#xiaolin-wu", "title": "Xiaolin Wu", "text": "

    L'algorithme de Xiaolin Wu est une m\u00e9thode am\u00e9lior\u00e9e pour le trac\u00e9 d'antialiasing de lignes. Il utilise des techniques de subpixel pour rendre les lignes plus lisses et plus pr\u00e9cises. L'algorithme de Xiaolin Wu est bas\u00e9 sur l'algorithme de Bresenham, mais il ajoute des \u00e9tapes suppl\u00e9mentaires pour g\u00e9rer les valeurs de couleurs partielles des pixels. Il fut publi\u00e9 en 1991 dans le journal Computer Graphics (An Efficient Antialiasing Technique).

    Algorithme de Xiaolin Wu

    void wuLine(Point p0, Point p1, SDL_Renderer *renderer) {\n   bool steep = abs(p1.y - p0.y) > abs(p1.x - p0.x);\n\n   if (steep) {\n      swapXY(&p0);\n      swapXY(&p1);\n   }\n\n   if (p0.x > p1.x) swapPoints(&p0, &p1);\n\n   int dx = p1.x - p0.x;\n   int dy = p1.y - p0.y;\n   float gradient = dx == 0 ? 1 : (float)dy / (float)dx;\n\n   // Premi\u00e8re extr\u00e9mit\u00e9\n   float xEnd = p0.x;\n   float yEnd = p0.y + gradient * (xEnd - p0.x);\n   float xGap = 1.0f;\n   int xPixel1 = (int)xEnd;\n   int yPixel1 = (int)yEnd;\n\n   if (steep) {\n      setPixel(renderer, yPixel1, xPixel1, (1 - (yEnd - yPixel1)) * xGap);\n      setPixel(renderer, yPixel1 + 1, xPixel1, (yEnd - yPixel1) * xGap);\n   } else {\n      setPixel(renderer, xPixel1, yPixel1, (1 - (yEnd - yPixel1)) * xGap);\n      setPixel(renderer, xPixel1, yPixel1 + 1, (yEnd - yPixel1) * xGap);\n   }\n\n   float intery = yEnd + gradient;\n\n   // Deuxi\u00e8me extr\u00e9mit\u00e9\n   xEnd = p1.x;\n   yEnd = p1.y + gradient * (xEnd - p1.x);\n   xGap = 1.0f;\n   int xPixel2 = (int)xEnd;\n   int yPixel2 = (int)yEnd;\n\n   if (steep) {\n      setPixel(renderer, yPixel2, xPixel2, (1 - (yEnd - yPixel2)) * xGap);\n      setPixel(renderer, yPixel2 + 1, xPixel2, (yEnd - yPixel2) * xGap);\n   } else {\n      setPixel(renderer, xPixel2, yPixel2, (1 - (yEnd - yPixel2)) * xGap);\n      setPixel(renderer, xPixel2, yPixel2 + 1, (yEnd - yPixel2) * xGap);\n   }\n\n   // Tracer les pixels entre les deux extr\u00e9mit\u00e9s\n   if (steep) {\n      for (int x = xPixel1 + 1; x < xPixel2; ++x) {\n         setPixel(renderer, (int)intery, x, (1 - (intery - (int)intery)));\n         setPixel(renderer, (int)intery + 1, x, intery - (int)intery);\n         intery += gradient;\n      }\n   } else {\n      for (int x = xPixel1 + 1; x < xPixel2; ++x) {\n         setPixel(renderer, x, (int)intery, (1 - (intery - (int)intery)));\n         setPixel(renderer, x, (int)intery + 1, intery - (int)intery);\n         intery += gradient;\n      }\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#courbes-de-bezier", "title": "Courbes de B\u00e9zier", "text": "

    Les courbes de B\u00e9zier sont des courbes math\u00e9matiques utilis\u00e9es pour repr\u00e9senter des formes lisses et r\u00e9guli\u00e8res. Elles sont largement utilis\u00e9es en infographie pour le trac\u00e9 de courbes.

    Courbes de B\u00e9zier

    Le calcul de B\u00e9zier est donn\u00e9 par l'algorithme suivant\u2009:

    Point bezier(Point p, Point c, Point q, float t) {\n   const float it = 1.f - t;\n   return (Point){.x = it * it * p.x + 2 * it * t * c.x + t * t * q.x,\n                  .y = it * it * p.y + 2 * it * t * c.y + t * t * q.y};\n}\n

    Le point \\(p\\) est le point de d\u00e9part de la courbe, le point \\(q\\) est le point d'arriv\u00e9e et le point \\(c\\) est le point de contr\u00f4le. La valeur de \\(t\\) varie de 0 \u00e0 1 pour d\u00e9terminer la position le long de la courbe.

    En pratique, la valeur \\(t\\) est incr\u00e9ment\u00e9e \u00e0 chaque it\u00e9ration pour tracer des tron\u00e7ons de la courbes par des segments de droite.

    void drawBezierCurve(SDL_Renderer *renderer, Point p, Point q, Point c) {\n   const float precision = 0.01;\n   Point prev = p;\n   for (float t = 0.0; t <= 1.0; t += precision) {\n      Point current = bezier(p, c, q, t);\n      draw_line(prev, current);\n      prev = current;\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#floyd-stenberg", "title": "Floyd-Stenberg", "text": ""}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/", "title": "L-Syst\u00e8me (Lindenmayer System)", "text": "

    Un L-Syst\u00e8me est un syst\u00e8me de r\u00e9\u00e9criture de cha\u00eenes de caract\u00e8res invent\u00e9 en 1968 par le biologiste hongrois Astrid Lindenmayer. Il est utilis\u00e9 pour mod\u00e9liser la croissance des plantes, la morphogen\u00e8se, la fractale, etc. On peut d\u00e9finir ce syt\u00e8me comme une forme de grammaire g\u00e9n\u00e9rative.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#definition", "title": "D\u00e9finition", "text": "

    Un L-Syst\u00e8me est d\u00e9fini par un alphabet, un axiome et un ensemble de r\u00e8gles de r\u00e9\u00e9criture. L'axiome est la cha\u00eene de d\u00e9part. Les r\u00e8gles de r\u00e9\u00e9criture d\u00e9finissent comment les symboles de l'axiome sont remplac\u00e9s par d'autres symboles.

    Nous avons donc\u2009:

    Axiome

    C'est le point de d\u00e9part de la r\u00e9\u00e9criture. C'est une cha\u00eene de caract\u00e8res.

    R\u00e8gles de production

    Chaque r\u00e8gle s\u00e9par\u00e9e par des virgules d\u00e9finissent comment un symbole est remplac\u00e9 par une autre cha\u00eene de caract\u00e8res.

    Angle

    C'est l'angle de rotation pour les symboles + et -. \u00c0 chaque fois que l'on rencontre un +, on tourne \u00e0 droite de l'angle. \u00c0 chaque fois que l'on rencontre un -, on tourne \u00e0 gauche de l'angle. Les angles peuvent \u00eatre cumulatifs\u2009: ++ signifie tourner deux fois \u00e0 droite.

    It\u00e9rations

    C'est le nombre de fois que l'on applique les r\u00e8gles de r\u00e9\u00e9criture \u00e0 l'axiome.

    Longueur

    C'est la longueur de chaque segment de la courbe pour chaque symbole.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#grammaire", "title": "Grammaire", "text": "

    En 3 dimensions, on dispose des coordonn\u00e9es \\(x\\), \\(y\\) et \\(z\\) ainsi que des rotations \\(\\alpha\\), \\(\\beta\\) et \\(\\gamma\\).

    • + : tourner \u00e0 droite autour de l'axe \\(z\\) de l'angle \\(\\alpha\\)
    • - : tourner \u00e0 gauche autour de l'axe \\(z\\) de l'angle \\(\\alpha\\)
    • & : tourner en bas autour de l'axe \\(y\\) de l'angle \\(\\beta\\)
    • ^ : tourner en haut autour de l'axe \\(y\\) de l'angle \\(\\beta\\)
    • \\ : tourner \u00e0 droite autour de l'axe \\(x\\) de l'angle \\(\\gamma\\)
    • / : tourner \u00e0 gauche autour de l'axe \\(x\\) de l'angle \\(\\gamma\\)
    • | : tourner de 180\u00b0 autour de l'axe \\(z\\), correspond \u00e0 ++ ou --.
    • [ : sauvegarder la position et l'angle
    • ] : restaurer la position et l'angle
    • Les lettres sont des symboles de dessin
    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#exemple", "title": "Exemple", "text": "

    Prenons cette d\u00e9finition\u2009:

    name: Koch snowflake\naxiom: F\nangle: 90\niterations: 3\nlength: 20\nrules:\n  F: F+F-F-F+F\n

    L'axiome est F. On a une r\u00e8gle de r\u00e9\u00e9criture F -> F+F-F-F+F. On applique cette r\u00e8gle 3 fois.

    1. It\u00e9ration 0\u2009: F
    2. It\u00e9ration 1\u2009: F+F-F-F+F
    3. It\u00e9ration 2\u2009: F+F-F-F+F + F+F-F-F+F - F+F-F-F+F - F+F-F-F+F + F+F-F-F+F
    4. Et ainsi de suite...

    Imaginez que vous tenez un crayon et que vous suivez les instructions pour dessiner la courbe. \u00c0 chaque F, vous avancez de 20 pixels. \u00c0 chaque +, vous tournez de 90\u00b0 \u00e0 droite. \u00c0 chaque -, vous tournez de 90\u00b0 \u00e0 gauche.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#evolution", "title": "\u00c9volution", "text": "

    On peut imaginer de g\u00e9n\u00e9raliser le L-Syst\u00e8me en ajoutant des variables, des fonctions, des conditions, etc. On peut \u00e9galement ajouter des r\u00e8gles de r\u00e9\u00e9criture conditionnelles, des r\u00e8gles de r\u00e9\u00e9criture probabilistes.

    Imaginons une d\u00e9finition formelle en YAML :

    name: Nom du L-Syst\u00e8me\naxiom: Cha\u00eene de d\u00e9part\nangle: Angle de rotation par d\u00e9faut\niterations: Nombre d'it\u00e9rations\nlength: Longueur de chaque segment par d\u00e9faut\nrules:\n    A: A+B\n    B: A-B\nactions:\n

    Imaginons les actions suivantes\u2009:

    actions:\n    forward(length): Avancer de length, length est optionnel\n    rotate(alpha, beta, gamma): Tourner de angle\n    color(r, g, b): Changer la couleur du crayon\n    color(name): Changer la couleur du crayon par une couleur nomm\u00e9e\n    save(): Sauvegarder la position et l'angle\n    restore(): Restaurer la position et l'angle\n    width(size): Changer la taille du crayon\nvariables:\n    level: Niveau de r\u00e9cursion\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/rabin-karp/", "title": "Algorithme de Rabin-Karp", "text": "

    Cet algorithme Rabin-Karp permet la recherche d'une sous-cha\u00eene de caract\u00e8re dans une cha\u00eene plus grande. Sa complexit\u00e9 moyenne est \\(O(n + m)\\).

    L'algorithme se base sur le principe de la fonction de hachage. Il consiste \u00e0 calculer le hash de la cha\u00eene \u00e0 rechercher et de la cha\u00eene dans laquelle on recherche. Si les deux hash sont \u00e9gales, on compare les deux cha\u00eenes caract\u00e8re par caract\u00e8re.

    L'algorithme fait glisser une fen\u00eatre de la taille de la cha\u00eene \u00e0 rechercher sur la cha\u00eene dans laquelle on recherche. \u00c0 chaque it\u00e9ration, on calcule le hash de la fen\u00eatre et on le compare au hash de la cha\u00eene \u00e0 rechercher. Si les deux hash sont \u00e9gaux, on compare les deux cha\u00eenes caract\u00e8re par caract\u00e8re.

    La performance de l'algorithme d\u00e9pend de la fonction de hachage. Si la fonction de hachage est bien choisie, l'algorithme est tr\u00e8s performant. Si la fonction de hachage est mal choisie, l'algorithme peut \u00eatre lent.

    Ici la fonction de hachage est tr\u00e8s simple, on utilise un nombre premier.

    rabin-karp.c
    #include <stdio.h>\n#include <string.h>\n#include <assert.h>\n\n#define CHARS_IN_ALPHABET 256\n\n/**\n * Rabin-Karp algorithm\n * @param needle Motif \u00e0 rechercher\n * @param haystack Texte d'entr\u00e9e\n * @param matches La liste des occurences trouv\u00e9es\n * @param size La taille du tableau matches\n * @return Le nombre d'occurences trouv\u00e9es\n */\nint search(char needle[], char haystack[], int matches[], size_t size)\n{\n    const int q = 101; // A prime number\n    const int M = strlen(needle);\n    const int N = strlen(haystack);\n\n    int h = 1;\n    for (int i = 0; i < M - 1; i++)\n        h = (h * CHARS_IN_ALPHABET) % q;\n\n    // Compute the hash value of pattern and first\n    // window of text\n    int p = 0; // Hash value for pattern\n    int t = 0; // Hash value for haystack\n    for (int i = 0; i < M; i++) {\n        p = (CHARS_IN_ALPHABET * p + needle[i]) % q;\n        t = (CHARS_IN_ALPHABET * t + haystack[i]) % q;\n    }\n\n    // Slide the pattern over text one by one\n    size_t k = 0;\n    for (int i = 0; i <= N - M; i++) {\n        // Check the hash values of current window of text\n        // and pattern. If the hash values match then only\n        // check for characters on by one\n        if (p == t) {\n            // Check for characters one by one\n            int j = 0;\n            while (haystack[i + j] == needle[j] && j < M)\n                j++;\n\n            // Save the position found\n            if (j == M)\n                if (k < size)\n                    matches[k++] = i;\n                else\n                    return k;\n        }\n\n        // Calculate hash value for next window of text.\n        // Remove leading digit and add trailing digit.\n        if (i < (N - M)) {\n            t = (CHARS_IN_ALPHABET *\n                (t - haystack[i] * h) + haystack[i + M]) % q;\n            t += t < 0 ? q : 0;\n        }\n    }\n    return k;\n}\n\nint test_search()\n{\n    char text[] =\n        \"Le courage n'est pas l'absence de peur, \"\n        \"mais la capacit\u00e9 de vaincre ce qui fait peur.\"\n        \"On ne peut vaincre sa destin\u00e9e.\"\n        \"A vaincre sans barils, on triomphe sans boire.\";\n\n    int matches[10];\n    int k = search(\"vaincre\", text, matches, sizeof(matches)/sizeof(matches[0]));\n    assert(k == 3);\n    assert(matches[0] == 61);\n    assert(matches[1] == 97);\n    assert(matches[2] == 120);\n}\n\nint main() {\n    test_search();\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/random/", "title": "G\u00e9n\u00e9rateur congruentiel lin\u00e9aire", "text": "

    Le g\u00e9n\u00e9rateur congruentiel lin\u00e9aire (GCL) est un algorithme simple pour g\u00e9n\u00e9rer des nombres pseudo-al\u00e9atoires. Il est d\u00e9fini par la relation de r\u00e9currence suivante\u2009:

    \\[ X_{n+1} = (a \\cdot X_n + c) \\mod m \\]

    O\u00f9\u2009:

    \\(X_n\\)

    est la s\u00e9quence de nombres pseudo-al\u00e9atoires

    \\(a\\)

    est le multiplicateur

    \\(c\\)

    est l'incr\u00e9ment

    \\(m\\)

    est le modulo

    Les informaticiens ont remarqu\u00e9 que ce g\u00e9n\u00e9rateur fonctionne bien pour certaines valeurs de \\(a\\), \\(c\\) et \\(m\\). Par exemple, si \\(m = 2^k\\), le g\u00e9n\u00e9rateur est dit \u00e0 \u00ab\u2009congruence binaire\u2009\u00bb. Si \\(c = 0\\), le g\u00e9n\u00e9rateur est dit \u00ab\u2009multiplicatif\u2009\u00bb.

    En C c'est un g\u00e9n\u00e9rateur congruentiel lin\u00e9aire qui est utilis\u00e9 pour la fonction rand(). Voici une impl\u00e9mentation de ce g\u00e9n\u00e9rateur\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nstatic uint32_t seed = 0;\n\nvoid srand(uint32_t s) {\n    seed = s;\n}\n\nuint32_t rand() {\n    const uint32_t a = 1103515245;\n    const uint32_t c = 12345;\n    const uint32_t m = 1ULL << 31;\n    seed = (a * seed + c) % m;\n    return seed;\n}\n

    La valeur statique seed permet de conserver l'\u00e9tat du g\u00e9n\u00e9rateur entre les appels \u00e0 la fonction rand(). Si elle n'est pas initialis\u00e9e, le g\u00e9n\u00e9rateur retournera toujours la m\u00eame s\u00e9quence de nombres pseudo-al\u00e9atoires. La fonction srand() permet donc d'initialiser la graine du g\u00e9n\u00e9rateur.

    En pratique, on utilise srand() avec comme valeur d'initialisation le temps courant en secondes pour obtenir une s\u00e9quence de nombres pseudo-al\u00e9atoires diff\u00e9rente \u00e0 chaque ex\u00e9cution du programme\u2009:

    #include <time.h>\n\nint main() {\n    srand(time(NULL));\n    for (int i = 0; i < 10; ++i) {\n        printf(\"%d\\n\", rand());\n    }\n}\n

    N\u00e9anmoins si vous rappelez votre programme durant la m\u00eame seconde, vous obtiendrez la m\u00eame s\u00e9quence de nombres pseudo-al\u00e9atoires. Pour obtenir une s\u00e9quence diff\u00e9rente \u00e0 chaque ex\u00e9cution, vous pouvez utiliser inclure le PID du processus\u2009:

    #include <unistd.h>\n#include <time.h>\n\nint main() {\n    srand(time(NULL) ^ getpid());\n    for (int i = 0; i < 10; ++i) {\n        printf(\"%d\\n\", rand());\n    }\n}\n
    ", "tags": ["seed"]}, {"location": "course-c/40-algorithms/popular-algorithms/random/#valeurs-remarquables", "title": "Valeurs remarquables", "text": "

    Voici quelques valeurs de \\(a\\), \\(c\\) et \\(m\\) qui donnent de bons r\u00e9sultats\u2009:

    Source \\(m\\) \\(a\\) \\(c\\) ANSI C \\(2^{31}\\) \\(1103515245\\) \\(12345\\) Borland C \\(2^{32}\\) \\(22695477\\) \\(1\\) MMIX de Donald Knuth \\(2^{64}\\) \\(6364136223846793005\\) \\(1442695040888963407\\) Java \\(2^{48}\\) \\(25214903917\\) \\(11\\)"}, {"location": "course-c/40-algorithms/popular-algorithms/shunting-yard/", "title": "Algorithme de Shunting Yard", "text": "

    L'algorithme de Shunting Yard est un algorithme de parsing d'expression math\u00e9matique. Il permet de transformer une expression math\u00e9matique en notation infix\u00e9e en une expression postfix\u00e9e. L'algorithme a \u00e9t\u00e9 invent\u00e9 par Edsger Dijkstra en 1961.

    Imaginez que vous ayez une expression math\u00e9matique sous forme d'une cha\u00eene de caract\u00e8res\u2009:

    2 + 3 * 8 - 2 * ( 2 - 4 / ( 3 * 8 ) )\n

    Comment calculer cette expression\u2009? Si vous proc\u00e9dez de gauche \u00e0 droite, vous allez rencontrer des parenth\u00e8ses et des priorit\u00e9s d'op\u00e9rations.

    L'algorithme se compose de deux files d'attente (FIFO) et d'une pile (LIFO). La file d'attente de sortie contiendra l'expression postfix\u00e9e. La pile contiendra les op\u00e9rateurs.

    Algorithme de shunting yard

    Commen\u00e7ons par quelques d\u00e9finitions\u2009:

    TOKEN

    Un token est un \u00e9l\u00e9ment de l'expression math\u00e9matique. Il peut s'agir d'un nombre, d'un op\u00e9rateur ou d'une parenth\u00e8se.

    INPUT

    Une file d'attente contenant les TOKENS de l'expression math\u00e9matique \u00e0 traiter.

    OUTPUT

    Une file d'attente qui contiendra les TOKENS de l'expression postfix\u00e9e.

    STACK

    Une pile qui contiendra les op\u00e9rateurs.

    Voici le pseudo code de l'algorithme\u2009:

    • Tant qu'il y a des TOKEN \u00e0 lire\u2009:
    • Lire le TOKEN Si le TOKEN est\u2009:
      • Un nombre\u2009:
      • Le d\u00e9placer sur OUTPUT.
      • Un op\u00e9rateur \\(O_1\\)
      • Tant que:
        • Il y a un op\u00e9rateur \\(O_2\\) sur le dessus STACK,
        • et que ce n'est pas une parenth\u00e8se gauche,
        • et que \\(O_2\\) a une plus grande priorit\u00e9 que \\(O_1\\),
        • ou que \\(O_2\\) a la m\u00eame priorit\u00e9 que \\(O_1\\),
        • et que \\(O_1\\) est associatif \u00e0 gauche.
        • Alors:
        • D\u00e9placer \\(O_2\\) de STACK sur OUTPUT.
        • D\u00e9placer \\(O_1\\) sur le STACK.
      • Une parenth\u00e8se gauche (():
      • D\u00e9placer sur le STACK.
      • Une parenth\u00e8se droite ()):
      • Tant que l'op\u00e9rateur sur le dessus du STACK n'est pas une parenth\u00e8se gauche.
        • Alors, d\u00e9placer le dernier op\u00e9rateur du STACK sur OUTPUT.
        • Supprimer la parenth\u00e8se gauche du STACK.
    • Tant qu'il y a des TOKEN sur le STACK:
    • D\u00e9placer le TOKEN de STACK sur OUTPUT

    On observe que si on dispose de fonctions pour ajouter/supprimer des \u00e9l\u00e9ments d'une file d'attente et d'une pile, l'algorithme est relativement simple \u00e0 impl\u00e9menter. Voici l'impl\u00e9mentation en C\u2009:

    main.cqueue.hstack.hqueue.cstack.c
    #include \"queue.h\"\n#include \"stack.h\"\n\n#include <stdio.h>\n#include <ctype.h>\n\nint main() {\n    Stack stack = stack_create();\n    Queue output = queue_create();\n    Queue input = queue_create();\n\n    // Load expression in input queue\n    while (!feof(stdin)) {\n        char token;\n        scanf(\"%c\", &token);\n        if (isspace(token))\n            continue;\n        queue_push(&input, token);\n    }\n\n    // Shunting yard algorithm\n    while (!queue_empty(&input)) {\n        char token = queue_pop(&input);\n        if (isdigit(token))\n            queue_push(&output, token);\n        else if (token == '(')\n            stack_push(&stack, token);\n        else if (token == ')') {\n            while (stack_top(&stack) != '(')\n                queue_push(&output, stack_pop(&stack));\n            stack_pop(&stack);\n        } else {\n            while (!stack_empty(&stack) && stack_top(&stack) != '(')\n                queue_push(&output, stack_pop(&stack));\n            stack_push(&stack, token);\n        }\n    }\n    while (!stack_empty(&stack))\n        queue_push(&output, stack_pop(&stack));\n\n    // Display output queue\n    while (!queue_empty(&output))\n        printf(\"%c\", queue_pop(&output));\n}\n
    #pragma once\n\ntypedef struct QueueNode {\n    char data;\n    struct QueueNode *next;\n} QueueNode;\n\ntypedef struct {\n    QueueNode *front;\n    QueueNode *rear;\n} Queue;\n\nQueue queue_create();\nvoid queue_push(Queue *queue, char value);\nchar queue_pop(Queue *queue);\nint queue_empty(Queue *queue);\n
    #pragma once\n\ntypedef struct StackNode {\n    char data;\n    struct StackNode *next;\n} StackNode;\n\ntypedef struct {\n    StackNode *top;\n} Stack;\n\nStack stack_create();\nvoid stack_push(Stack *stack, char value);\nchar stack_pop(Stack *stack);\nchar stack_top(Stack *stack);\nint stack_empty(Stack *stack);\n
    #include \"queue.h\"\n\n#include <stdlib.h>\n\nQueue queue_create() {\n    Queue queue;\n    queue.front = NULL;\n    queue.rear = NULL;\n    return queue;\n}\n\nvoid queue_push(Queue *queue, char value) {\n    QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));\n    newNode->data = value;\n    newNode->next = NULL;\n    if (queue->rear)\n        queue->rear->next = newNode;\n    else\n        queue->front = newNode;\n    queue->rear = newNode;\n}\n\nchar queue_pop(Queue *queue) {\n    if (queue->front == NULL)\n        return '\\0';\n    QueueNode *node = queue->front;\n    char value = node->data;\n    queue->front = node->next;\n    if (queue->front == NULL)\n        queue->rear = NULL;\n    free(node);\n    return value;\n}\n\nint queue_empty(Queue *queue) {\n    return queue->front == NULL;\n}\n
    #include \"stack.h\"\n\n#include <stdlib.h>\n\nStack stack_create() {\n    Stack stack;\n    stack.top = NULL;\n    return stack;\n}\n\nvoid stack_push(Stack *stack, char value) {\n    StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));\n    newNode->data = value;\n    newNode->next = stack->top;\n    stack->top = newNode;\n}\n\nchar stack_pop(Stack *stack) {\n    if (stack->top == NULL)\n        return '\\0';\n    StackNode *node = stack->top;\n    char value = node->data;\n    stack->top = node->next;\n    free(node);\n    return value;\n}\n\nchar stack_top(Stack *stack) {\n    if (stack->top == NULL)\n        return '\\0';\n    return stack->top->data;\n}\n\nint stack_empty(Stack *stack) {\n    return stack->top == NULL;\n}\n

    Notation polonaise inverse

    La notation polonaise inverse (RPN) est une notation math\u00e9matique dans laquelle chaque op\u00e9rateur suit ses op\u00e9randes. Par exemple, l'expression 3 + 4 s'\u00e9crira 3 4 +. Cette notation a \u00e9t\u00e9 invent\u00e9e par le math\u00e9maticien polonais Jan \u0141ukasiewicz.

    Elle a \u00e9t\u00e9 longtemps utilis\u00e9e par les calculatrices Hewlett-Packard et permet de s'affranchir des parenth\u00e8ses. En effet, l'expression 3 + 4 * 5 s'\u00e9crira 3 4 5 * +.

    La calculatrice mythique HP-42s apparue en 1988 \u00e9tait utilis\u00e9e par beaucoup d'ing\u00e9nieurs et de scientifiques. On y remarque l'absence de touche \u00e9gale =. Pour calculer une expression, il suffisait de taper les op\u00e9randes et les op\u00e9rateurs dans l'ordre. La calculatrice se chargeait de calculer le r\u00e9sultat.

    HP-42s

    La notation polonaise inverse est \u00e9galement tr\u00e8s pratique pour les ordinateurs. En effet, il est plus facile de traiter une expression postfix\u00e9e qu'une expression infix\u00e9e. L'algorithme de Shunting Yard permet de transformer une expression infix\u00e9e en une expression postfix\u00e9e.

    "}, {"location": "course-c/40-algorithms/sorting/count-sort/", "title": "Counting Sort", "text": "

    Le tri par d\u00e9nombrement est un algorithme de tri particulier. Il est utilis\u00e9 pour trier des \u00e9l\u00e9ments dont la valeur est connue \u00e0 l'avance. Il est donc inutile pour trier des \u00e9l\u00e9ments dont la valeur est inconnue ou al\u00e9atoire.

    Nous l'avons vu pr\u00e9c\u00e9demment, il n'est pas possible de trier un tableau mieux qu'en O(nlogn). En revanche cette assertion n'est pas tout \u00e0 fait juste dans le cas ou les donn\u00e9es brutes poss\u00e8dent des propri\u00e9t\u00e9s remarquables.

    Pour que cet algorithme soit utilisable, imaginons un contexte o\u00f9 les donn\u00e9es d'entr\u00e9es poss\u00e8dent des propri\u00e9t\u00e9s remarquables.

    Ici, les donn\u00e9es d'entr\u00e9es seront g\u00e9n\u00e9r\u00e9es entre 0 et 51\u2009; chaque valeur repr\u00e9sentera une carte \u00e0 jouer selon la r\u00e8gle suivante\u2009:

    cards

    Cette s\u00e9rie de valeurs dispose de plusieurs propri\u00e9t\u00e9s int\u00e9ressantes\u2009:

    1. La valeur d'une carte est identifi\u00e9e par n % 13.
    2. La couleur est identifi\u00e9e par n / 13.
    3. Il n'y a donc que 13 valeurs par couleur et 4 couleurs.
    4. Bien qu'un entier soit stock\u00e9 sur 4 bytes, il suffit en r\u00e9alit\u00e9 de 6 bits pour encoder la valeur d'une carte.

    L'algorithme counting-sort est int\u00e9ressant, car il ne fait pas appel \u00e0 des comparaisons de valeurs.

    L'algorithme it\u00e9ratif est le suivant\u2009:

    1. On cr\u00e9e un tableau statique de 13 positions correspondant au nombre de valeurs possibles dans notre set de donn\u00e9e. Ce tableau peut \u00eatre nomm\u00e9 counts
    2. On parcourt le tableau \u00e0 trier lin\u00e9airement et on compte le nombre d'occurrences de chaque valeur dans counts. On aura donc \u00e0 la position 0 le nombre d'as contenus dans le tableau \u00e0 trier et \u00e0 la positon 10 le nombre de valets dans le jeu de cartes fourni.
    3. Une fois ces comptes termin\u00e9s, une op\u00e9ration de somme cumul\u00e9e est faite sur ce tableau (p.ex. le tableau {1, 2, 2, 0, 8, 1} sera somm\u00e9 comme suit {1, 3, 5, 5, 13, 14}).
    4. Le tableau \u00e0 trier est parcouru lin\u00e9airement de la fin vers le d\u00e9but et on copie la valeur \u00e0 la bonne position dans le tableau des valeurs tri\u00e9es. Si votre tableau d'entr\u00e9e non tri\u00e9 est nomm\u00e9 a et votre tableau de sortie tri\u00e9 b vous aurez pour chaque \u00e9l\u00e9ment i : b[c[a[i]--] - 1] = a[i]

    \u00c0 l'issue de cet algorithme, vous aurez dans b un tableau tri\u00e9 par valeurs.

    Notez qu'ici les couleurs ne sont pas tri\u00e9es.

    Voici un exemple de tri sur des entiers entre 0 et 9\u2009:

    1 0 9 3 8 1 4 8 7 5  Tableau d'entr\u00e9e `a` non tri\u00e9 et contenant que des chiffres\n                     Il y a 10 valeurs possibles par chiffre, donc le tableau\n                     `counts` aura 10 positions:\n\n1 2 0 1 1 1 0 1 2 1  Tableau `counts` calcul\u00e9, il faut lire : une occurrence\n                     de 0, deux occurrences de 2, zero occurrences de 3...\n\n1 3 3 4 5 6 6 1 9 B  Somme cumul\u00e9e (affich\u00e9 en hexad\u00e9cimal)\n\nPour trouver la position tri\u00e9e d'une valeur, par exemple le 3 :\n\n1 0 9 3 8 1 4 8 7 5  a[i] == 3 (on part du tableau a)\n      \u21a7\n1 3 3 4 5 6 6 1 9 B  counts[a[i]] == 4 (on consulte la somme cumul\u00e9e)\n      \u21a7\n_ _ _ 3 _ _ _ _ _ _  b[counts[a[i]]-- - 1] == 3 (on insert la valeur tri\u00e9e)\n\n0 1 1 3 4 5 7 8 8 9  Et ainsi de suite jusqu'\u00e0 tri complet du tableau\n

    On constate en effet que 3 est \u00e0 la bonne position dans b et qu'il y aura 0 1 1 devant.

    Tel quel, cet algorithme ne permet pas de modifier directement le tableau d'origine puisqu'il ne fait pas intervenir de permutations (swap). Il requiert donc un buffer suppl\u00e9mentaire et donc \u00e0 une complexit\u00e9 en espace de O(n).

    Concernant les cartes \u00e0 jouer, voici un exemple de tri\u2009:

    41 23 00 15 26 39 13 02 28  Tableau d'entr\u00e9e\n00 26 39 13 41 15 02 28 23  Tableau tri\u00e9\n\u00c0\u2663 \u00c0\u2665 A\u2660 A\u2666 3\u2660 3\u2666 3\u2663 3\u2665 V\u2666  Valeurs interpr\u00e9t\u00e9es des cartes\n

    Un avantage de cet algorithme est qu'il est stable, c'est-\u00e0-dire que l'ordre des \u00e9l\u00e9ments \u00e9gaux est conserv\u00e9. Donc on peut ensuite retrier les cartes par couleur.

    ", "tags": ["counts", "swap"]}, {"location": "course-c/40-algorithms/sorting/heap-sort/", "title": "Heap Sort", "text": "

    L'algorithme Heap Sort aussi appel\u00e9 \u00ab\u2009Tri par tas\u2009\u00bb est l'un des algorithmes de tri les plus performants offrant une complexit\u00e9 en temps de \\(O(n\\cdot log(n))\\) et une complexit\u00e9 en espace de \\(O(1)\\). Il s'appuie sur le concept d'arbre binaire.

    Prenons l'exemple du tableau ci-dessous et deux r\u00e8gles suivantes\u2009:

    • l'enfant de gauche est donn\u00e9 par 2 * k + 1 ;
    • l'enfant de droite est donn\u00e9 par 2 * k + 2.
      1   2       3                  4\n\u251e\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2526\n\u250208\u250204\u250212\u250220\u250206\u250242\u250214\u250211\u250203\u250235\u250207\u250209\u250211\u250250\u250216\u2502\n\u2514\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2518\n  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  (indice)\n

    La premi\u00e8re valeur du tableau est appel\u00e9e la racine root. C'est le premier \u00e9l\u00e9ment de l'arbre. Puisqu'il s'agit d'un arbre binaire, chaque n\u0153ud peut comporter jusqu'\u00e0 2 enfants. L'enfant de gauche est calcul\u00e9 \u00e0 partir de l'indice k de l'\u00e9l\u00e9ment courant. Ainsi les deux enfants de l'\u00e9l\u00e9ment 4 seront 2 * 4 + 1 = 9 et 2 * 4 + 2 == a.

    Ce tableau lin\u00e9aire en m\u00e9moire pourra \u00eatre repr\u00e9sent\u00e9 visuellement comme un arbre binaire\u2009:

                 8\n             |\n         ----+----\n       /           \\\n      4            12\n   /    \\        /    \\\n  20     6      42    14\n / \\    / \\    / \\   /  \\\n11  3  35  7  9  11 50  16\n

    Le c\u0153ur de cet algorithme est le sous-algorithme nomm\u00e9 heapify. Ce dernier \u00e0 pour objectif de satisfaire une exigence suppl\u00e9mentaire de notre arbre\u2009: chaque enfant doit \u00eatre plus petit que son parent. Le principe est donc simple. On part du dernier \u00e9l\u00e9ment de l'arbre qui poss\u00e8de au moins un enfant\u2009: la valeur 14 (indice 6). Le plus grand des enfants est \u00e9chang\u00e9 avec la valeur du parent. Ici 50 sera \u00e9chang\u00e9 avec 14. Ensuite on applique r\u00e9cursivement ce m\u00eame algorithme pour tous les enfants qui ont \u00e9t\u00e9 \u00e9chang\u00e9s. Comme 14 (anciennement 50) n'a pas d'enfant, on s'arr\u00eate l\u00e0.

    L'algorithme continue en remontant jusqu'\u00e0 la racine de l'arbre. La valeur suivante analys\u00e9e est donc 42, comme les deux enfants sont petits on continue avec la valeur 6. Cette fois-ci 35 qui est plus grand est alors \u00e9chang\u00e9. Comme 6 n'a plus d'enfant, on continue avec 20, puis 12. \u00c0 cette \u00e9tape, notre arbre ressemble \u00e0 ceci\u2009:

                 8\n             |\n         ----+----\n       /           \\\n      4            12\n   /    \\        /    \\\n  20    35      42    50\n / \\    / \\    / \\   /  \\\n11  3  6   7  9  11 14  16\n

    La valeur 12 est plus petite que 50 et est donc \u00e9chang\u00e9e. Mais puisque 12 contient deux enfants (14 et 16), l'algorithme continue. 16 est \u00e9chang\u00e9 avec 12. L'algorithme se poursuit avec 4 et se terminera avec la racine 8. Finalement l'arbre ressemblera \u00e0 ceci\u2009:

                35\n             |\n         ----+----\n       /           \\\n     20            50\n   /    \\        /    \\\n  11     7      42    16\n / \\    / \\    / \\   /  \\\n8   3  6   4  9  11 14  12\n

    On peut observer que chaque n\u0153ud de l'arbre satisfait \u00e0 l'exigence susmentionn\u00e9e\u2009: tous les enfants sont inf\u00e9rieurs \u00e0 leurs parents.

    Une fois que cette propri\u00e9t\u00e9 est respect\u00e9e, on a l'assurance que la racine de l'arbre est maintenant le plus grand \u00e9l\u00e9ment du tableau. Il est alors \u00e9chang\u00e9 avec le dernier \u00e9l\u00e9ment du tableau 12, qui devient \u00e0 son tour la racine.

    Le dernier \u00e9l\u00e9ment est sorti du tableau et notre arbre ressemble maintenant \u00e0 ceci\u2009:

    1   2       3                  4\n\u251e\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2526\u2500\u2500\u2526\n\u250212\u250220\u250250\u250211\u2502 7\u250242\u250216\u2502 8\u2502 3\u2502 6\u2502 4\u2502 9\u250211\u250214\u250235\u2502\n\u2514\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2518\n  0  1  2  3  4  5  6  7  8  9  a  b  c  d     (indice)\n\n            12\n             |\n         ----+----\n       /           \\\n     20            50\n   /    \\        /    \\\n  11     7      42    16\n / \\    / \\    / \\   /\n8   3  6   4  9  11 14\n

    \u00c0 ce moment on recommence\u2009:

    1. heapify
    2. \u00c9change du premier \u00e9l\u00e9ment avec le dernier.
    3. Sortie du dernier \u00e9l\u00e9ment de l'arbre.
    4. Retour \u00e0 (1) jusqu'\u00e0 ce que tous les \u00e9l\u00e9ments soient sortis de l'arbre.
    ", "tags": ["heapify"]}, {"location": "course-c/40-algorithms/sorting/quick-sort/", "title": "Quick Sort", "text": "

    Le tri rapide est l'algorithme de tri par r\u00e9f\u00e9rence dans la plupart des langage de programmation. Selon le compilateur C que vous utilisez, la fonction qsort impl\u00e9mente cette m\u00e9thode de tri en \\(O(n log n)\\).

    Quick sort est th\u00e9oriquement plus lent que le Heap sort avec dans le pire des cas en \\(O(n^2)\\). N\u00e9anmoins, en s'appuyant que cette r\u00e9ponse StackOverflow, quick sort reste meilleur pour de grands tableaux car les embranchements sont moins fr\u00e9quents, et le cache processeur est donc mieux utilis\u00e9.

    Cet algorithme utilise la notion de pivot. Le pivot est un \u00e9l\u00e9ment qui est choisi pour \u00eatre le point autour duquel sont agenc\u00e9 les \u00e9l\u00e9ments. La m\u00e9thode de tri est la suivante\u2009:

    1. Choix d'un pivot
    2. Paritionnement\u2009: tous les \u00e9l\u00e9ments plus petit que le pivot sont d\u00e9plac\u00e9 \u00e0 gauche et tous les \u00e9l\u00e9ments plus grands sont \u00e0 droite. L'\u00e9l\u00e9ment pivot est ainsi positionn\u00e9 \u00e0 sa bonne place dans le tableau.
    3. Appel r\u00e9cursif pour la partie gauche et droite.

    Consid\u00e9rons le tableau suivant. Les valeurs ne sont pas tri\u00e9es. La premi\u00e8re \u00e9tape consiste \u00e0 choisir un pivot. Il existe plusieurs technique\u2009:

    • Choisir le premier \u00e9l\u00e9ment comme pivot
    • Choisir le dernier \u00e9l\u00e9ment comme pivot
    • Choisir l'\u00e9l\u00e9ment m\u00e9dian comme pivot

    Dans cet exemple, le dernier \u00e9l\u00e9ment 6 sera arbitrairement choisi comme pivot.

    Repr\u00e9sentation du tableau \u00e0 trier avec son pivot.

    L'\u00e9tape de paritionnement utilise l'algorithme suivant\u2009:

    int partition (int a[], int low, int high, int pivot)\n{\n    int i = low;\n    for (int j = low; j < high; j++)\n        if (a[j] < a[pivot])\n            swap(&a[i++], &a[j]);\n    swap(&a[i], &a[pivot]);\n    return i;\n}\n

    Voici comment partition(a, 0, 10, 10) modifie le tableau (voir code source) :

    2 9 4 1 b 5 a 7 3 8 6\n2 4 9 1 b 5 a 7 3 8 6\n2 4 1 9 b 5 a 7 3 8 6\n2 4 1 5 b 9 a 7 3 8 6\n2 4 1 5 3 9 a 7 b 8 6\n2 4 1 5 3 6 a 7 b 8 9\n

    On constate que la valeur 6 choisie comme pivot est maintenant \u00e0 sa bonne place. L'algorithme est donc appel\u00e9 r\u00e9cursivement pour les \u00e9l\u00e9ments 0 \u00e0 4 et `` 6`` \u00e0 a.

    Tri rapide apr\u00e8s le premier partitionnement.

    Voici une autre repr\u00e9sentation (voir code source) :

    1  9  5  2  b  4  a  7  3  8 [6]\n1  5  2  4  3 [6] a  7  b  8  9\n1  5  2  4 [3]\n1  2 [3] 4  5\n1 [2]\n1 [2]\n        4 [5]\n        4 [5]\n                a  7  b  8 [9]\n                7  8 [9] a  b\n                7 [8]\n                7 [8]\n                        a [b]\n                        a [b]\n
    ", "tags": ["qsort"]}, {"location": "course-c/45-software-design/", "title": "Introduction", "text": "

    Dans cette partie, nous allons aborder une partie fondamentale et souvent n\u00e9glig\u00e9e dans les cours d'informatique\u2009: la conception logicielle ou en anglais Software Design.

    Nous aborderons \u00e9galement de fa\u00e7on plus g\u00e9n\u00e9rale la gestion de projets informatiques. Travailler seul ou en \u00e9quipe sur un projet informatique n\u00e9cessite une organisation rigoureuse pour garantir la qualit\u00e9, le respect des d\u00e9lais et le succ\u00e8s du projet. La gestion de projets informatiques est une discipline \u00e0 part enti\u00e8re, qui repose sur des m\u00e9thodes, des outils et des bonnes pratiques sp\u00e9cifiques.

    Plus un projet \u00e9volue plus il devient complexe et difficile \u00e0 appr\u00e9hender. Sans un effort constant de la part de l'\u00e9quipe, de la dette de code s'accumule, des bugs apparaissent, des fonctionnalit\u00e9s sont oubli\u00e9es, des d\u00e9lais sont d\u00e9pass\u00e9s, etc. Pour \u00e9viter ces \u00e9cueils, nous allons voir comment organiser un projet informatique de mani\u00e8re efficace et structur\u00e9e.

    "}, {"location": "course-c/45-software-design/contribute/", "title": "Projet open-source", "text": "

    Le langage C est encore beaucoup utilis\u00e9 dans de gros projets qui demande une grande performance. On peut notament citer les projets suivants\u2009:

    Projet Watch Fork Stars Commits Linux 7940 53k 177k 1.3M Git 2402 25.4k 51.6k 74k Vim 676 5.4k 35.9k 20k SQLite 119 944k 6.2k 30k Gimp - 434 310k 53k FFmpeg 1438 12k 44.5k 117k Wireshark 300 1.8k 7k 93k Nginx 994 6.7k 21k 8.2k Apache 238 1.1k 3.5k 34k Le noyau Linux

    \u00c9tant donn\u00e9 qu'il est le noyau de la plupart des distributions Linux et que Linux est pr\u00e9sent sur des millions de serveurs, smartphones (via Android), et appareils IoT, on peut parler de milliards d'utilisateurs indirects.

    Git

    Comme le syst\u00e8me de contr\u00f4le de version le plus populaire, utilis\u00e9 par GitHub, GitLab, Bitbucket, et d'autres plateformes, il est utilis\u00e9 par des dizaines de millions de d\u00e9veloppeurs dans le monde.

    Vim

    En tant qu'\u00e9diteur de texte, il est tr\u00e8s populaire parmi les d\u00e9veloppeurs et les administrateurs syst\u00e8me. Il est difficile de quantifier pr\u00e9cis\u00e9ment son nombre d'utilisateurs, mais il est inclus par d\u00e9faut dans de nombreuses distributions Linux et est couramment utilis\u00e9 par des millions de d\u00e9veloppeurs.

    SQLite

    Utilis\u00e9 dans des milliards de dispositifs, que ce soit dans les navigateurs web, les t\u00e9l\u00e9phones mobiles, ou les syst\u00e8mes embarqu\u00e9s. C\u2019est probablement le SGBD le plus d\u00e9ploy\u00e9 au monde.

    Gimp

    C'est une alternative open-source populaire \u00e0 des logiciels comme Adobe Photoshop. Son nombre d'utilisateurs est dans l'ordre des millions, surtout parmi les amateurs de design graphique open-source.

    FFmpeg

    Tr\u00e8s utilis\u00e9 dans l'industrie pour le traitement de vid\u00e9os, souvent int\u00e9gr\u00e9 dans d'autres logiciels, ce qui rend difficile le comptage. Toutefois, son usage est tr\u00e8s r\u00e9pandu dans les syst\u00e8mes multim\u00e9dia, donc potentiellement des centaines de millions d'utilisateurs finaux.

    Wireshark

    Cet outil de capture et d'analyse de paquets r\u00e9seau est indispensable pour les ing\u00e9nieurs r\u00e9seau et les chercheurs en s\u00e9curit\u00e9, comptant probablement des millions d'utilisateurs dans ces domaines sp\u00e9cialis\u00e9s.

    Nginx

    Utilis\u00e9 comme serveur web ou reverse proxy, il alimente une grande partie des sites web dans le monde. Selon certaines estimations, il g\u00e8re plus de 30 % des sites web actifs, ce qui se traduit par des centaines de millions d'utilisateurs finaux.

    Apache HTTP Server

    Autre serveur web tr\u00e8s populaire, il est utilis\u00e9 sur une part significative des sites web. Bien que Nginx l'ait surpass\u00e9 en popularit\u00e9 dans certains cas, Apache reste extr\u00eamement r\u00e9pandu.

    "}, {"location": "course-c/45-software-design/contribute/#cas-de-figure-git", "title": "Cas de figure Git", "text": "

    Vous souhaitez contribuer au projet Git. La premi\u00e8re \u00e9tape est de trouver le code source. Ce n'est pas une grosse difficult\u00e9. Google donne rapidement l'information que Git est h\u00e9berg\u00e9 sur GitHub\u2009:

    Il suffit alors de cloner le projet Git sur votre machine locale sans historique. Cela permet de gagner du temps et de l'espace disque.

    git clone --depth=1 https://github.com/git/git.git\ncd git\n

    Un bref per\u00e7u du contenu du r\u00e9pertoire clon\u00e9 montre des fichiers sources C, un Makefile et un fichier INSTALL. C'est tout ce qu'il nous faut.

    $ ls\nCODE_OF_CONDUCT.md  editor.h            merge-ort.c        reset.h\nCOPYING             entry.c             merge-ort.h        resolve-undo.c\nDocumentation       entry.h             merge-recursive.c  resolve-undo.h\nGIT-VERSION-GEN     environment.c       merge-recursive.h  revision.c\nINSTALL             environment.h       merge.c            revision.h\nLGPL-2.1            ewah                merge.h            run-command.c\nMakefile            exec-cmd.c          mergesort.h        run-command.h\nREADME.md           exec-cmd.h          mergetools         sane-ctype.h\nRelNotes            fetch-negotiator.c  midx-write.c       scalar.c\nSECURITY.md         fetch-negotiator.h  midx.c             send-pack.c\nabspath.c           fetch-pack.c        midx.h             send-pack.h\n...\n

    On notera que le projet contient les fichiers bien connus\u2009:

    • .editorconfig pour d\u00e9finir les r\u00e8gles de formatage du code
    • .gitattributes pour d\u00e9finir les attributs des fichiers
    • .clang-format pour d\u00e9finir les r\u00e8gles de formatage du code C
    • .gitignore pour ignorer les fichiers inutiles
    • .gitmodules pour d\u00e9finir les sous-modules Git
    • LICENSE pour la licence du projet
    • COPYING pour la licence du projet
    • Makefile pour la compilation du projet
    • README.md pour la documentation du projet

    Avec less INSTALL, on peut prendre connaissance des instructions d'installation.

    less INSTALL\n                Git installation\n\nNormally you can just do \"make\" followed by \"make install\", and that\nwill install the git programs in your own ~/bin/ directory. ...\n\nAlternatively you can use autoconf generated ./configure script to\nset up install paths (via config.mak.autogen), so you can write instead\n\n        $ make configure ;# as yourself\n        $ ./configure --prefix=/usr ;# as yourself\n        $ make all doc ;# as yourself\n        # make install install-doc install-html;# as root\n...\n

    Nous allons donc cr\u00e9er un pr\u00e9fixe local pour ne pas polluer notre syst\u00e8me.

    mkdir _install\nmake configure prefix=$(pwd)/_install\n

    Le script configure est maintenant g\u00e9n\u00e9r\u00e9, il nous permet de configurer le projet, c'est \u00e0 dire de choisir les options de compilation. Si l'on ex\u00e9cute configure avec l'option --help, on obtient la liste des options disponibles.

    ./configure --help\n...\n  --with-libpcre          synonym for --with-libpcre2\n  --with-libpcre2         support Perl-compatible regexes via libpcre2\n                          (default is NO)\n...\n

    On peut voir par exemple que Git est compil\u00e9 par d\u00e9faut sans libpcre. Cette biblioth\u00e8que permet le support avanc\u00e9 des expressions r\u00e9guli\u00e8res Perl. Souvent les connaisseurs de Perl pr\u00e9f\u00e8re cette senteur aux expressions r\u00e9guli\u00e8res POSIX. Pour pouvoir l'utiliser il faut donc activer cette option, et n\u00e9cessairement disposer de la biblioth\u00e8que libpcre2 sur le syst\u00e8me.

    On commence donc par installer cette biblioth\u00e8que\u2009:

    sudo apt install libpcre2-dev\n

    Puis configurer le projet\u2009:

    $ ./configure --with-libpcre2 --prefix=$(pwd)/_install\nconfigure: Setting lib to 'lib' (the default)\nconfigure: Will try -pthread then -lpthread to enable POSIX Threads.\nconfigure: CHECKS for site configuration\nchecking for gcc... gcc\nchecking whether the C compiler works... yes\nchecking for C compiler default output file name... a.out\nchecking for suffix of executables...\nchecking whether we are cross compiling... no\nchecking for suffix of object files... o\nchecking whether the compiler supports GNU C... yes\nchecking whether gcc accepts -g... yes\nchecking for gcc option to enable C11 features... none needed\nchecking for stdio.h... yes\nchecking for stdlib.h... yes\n...\nchecking for pcre2_config_8 in -lpcre2-8... yes\n...\nconfigure: creating ./config.status\nconfig.status: creating config.mak.autogen\nconfig.status: executing config.mak.autogen commands\n

    A pr\u00e9sent, on peut compiler le projet\u2009:

    make -j16 V=1\n

    L'option -j16 permet de compiler en parall\u00e8le sur 16 threads. Cela acc\u00e9l\u00e8re la compilation en utilisant mieux les ressources de la machine. V=1 active le mode verbeux pour afficher les commandes ex\u00e9cut\u00e9es ce qui peut \u00eatre plus int\u00e9ressant pour comprendre ce qui se passe.

    Analysons un peu la sortie de la commande make:

    $ make -j16 V=1\nGIT_VERSION = 2.46.GIT\n    * new build flags\n    * new link flags\n    * new prefix flags\n/bin/sh ./generate-cmdlist.sh \\\n         \\\n        command-list.txt >command-list.h\ngcc -o hex.o -c -MF ./.depend/hex.o.d -MQ hex.o -MMD -MP    -g -O2  -I. \\\n-DHAVE_SYSINFO -DGIT_HOST_CPU=\"\\\"x86_64\\\"\" -DUSE_LIBPCRE2 -DHAVE_ALLOCA_H \\\n-DUSE_CURL_FOR_IMAP_SEND -DSUPPORTS_SIMPLE_IPC -DSHA1_DC \\\n-DSHA1DC_NO_STANDARD_INCLUDES -DSHA1DC_INIT_SAFE_HASH_DEFAULT=0 \\\n-DSHA1DC_CUSTOM_INCLUDE_SHA1_C=\"\\\"git-compat-util.h\\\"\" -DSHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C=\"\\\"git-compat-util.h\\\"\" -DSHA256_BLK  \\\n-DHAVE_PATHS_H -DHAVE_STRINGS_H -DHAVE_DEV_TTY -DHAVE_CLOCK_GETTIME \\\n-DHAVE_CLOCK_MONOTONIC -DHAVE_SYNC_FILE_RANGE -DHAVE_GETDELIM \\\n'-DPROCFS_EXECUTABLE_PATH=\"/proc/self/exe\"' -DFREAD_READS_DIRECTORIES \\\n-DSHELL_PATH='\"/bin/sh\"'  hex.c\n...\n

    On observe que Make compile tous les fichiers en appelant gcc avec des options sp\u00e9cifiques. On en connait d\u00e9j\u00e0 quelques unes.

    gcc\n   -o hex.o         # Nom du fichier objet de sortie\n                    # (g\u00e9n\u00e9ralement le nom du fichier source avec .o)\n   -c               # Compile sans lier, juste pour g\u00e9n\u00e9rer l'objet\n\n   # Gestion des d\u00e9pendances (pour la recompilation automatique)\n   -MF ./.depend/hex.o.d -MQ hex.o -MMD -MP\n\n   -g   # G\u00e9n\u00e8re des informations de d\u00e9bogage\n   -O2  # Optimisation de niveau 2\n   -I.  # Ajoute le r\u00e9pertoire courant pour les #include\n\n   -DUSE_LIBPCRE2  # D\u00e9finit le symbole USE_LIBPCRE2, puisque nous\n                   # avons activ\u00e9 l'option --with-libpcre2\n\n    hex.c          # Fichier source \u00e0 compiler\n

    Les options -D peuvent \u00eatre assez nombreuses selon le projet, elles permettent de \u00ab\u2009communiquer\u2009\u00bb avec le code source via des directives de pr\u00e9processeur. Dans le code source pour d\u00e9tecter si le support libpcre2 est activ\u00e9 on peut trouver des lignes comme\u2009:

    #ifdef USE_LIBPCRE2\n#  include <pcre2.h>\n#endif\n

    \u00c0 la fin de la compilation, le Makefile appellera le linker pour g\u00e9n\u00e9rer l'ex\u00e9cutable final. On reconnait facilement la ligne car c'est celle qui contient tous les fichiers objets g\u00e9n\u00e9r\u00e9s.

    gcc -g -O2 -I.\n    -o git \\\n    git.o builtin/add.o builtin/am.o builtin/annotate.o builtin/apply.o \\\n    builtin/archive.o builtin/bisect.o builtin/blame.o builtin/branch.o \\\n    builtin/bugreport.o builtin/bundle.o builtin/cat-file.o builtin/check-attr.o\n    builtin/check-ignore.o builtin/check-mailmap.o builtin/check-ref-format.o \\\n    builtin/checkout--worker.o builtin/checkout-index.o builtin/checkout.o \\\n    builtin/clean.o builtin/clone.o builtin/column.o builtin/commit-graph.o \\\n    ...\n    libgit.a xdiff/lib.a reftable/libreftable.a libgit.a \\\n    -lpcre2-8 -lz  -lrt\n

    On voit que le projet est li\u00e9 avec libpcre2-8, z et rt. La biblioth\u00e8que z est la biblioth\u00e8que standard de compression zlib. La biblioth\u00e8que rt est la biblioth\u00e8que de temps r\u00e9el. Les biblioth\u00e8ques statiques libgit.a, xdiff/lib.a, reftable/libreftable.a sont des biblioth\u00e8ques internes au projet qui ont \u00e9t\u00e9 compil\u00e9es en m\u00eame temps que le projet.

    Enfin, on peut installer le projet dans le r\u00e9pertoire _install et ex\u00e9cuter Git\u2009:

    $ make install\n$ ./_install/bin/git --version\ngit version 2.46.GIT\n

    Comme pour la plupart des projets, ce dernier dispose d'une panoplie de tests unitaires et fonctionnels. Pour les ex\u00e9cuter, il suffit de lancer la commande make test:

    make test\n    SUBDIR git-gui\n    SUBDIR gitk-git\n    SUBDIR templates\nmake -C t/ all\nmake[1]: Entering directory '/home/ycr/git/t'\nrm -f -r 'test-results'\nGIT_TEST_EXT_CHAIN_LINT=0 && export GIT_TEST_EXT_CHAIN_LINT && make aggregate-results-and-cleanup\nmake[2]: Entering directory '/home/ycr/git/t'\n*** t0000-basic.sh ***\nok 1 - verify that the running shell supports \"local\"\nok 2 - .git/objects should be empty after git init in an empty repo\nok 3 - .git/objects should have 3 subdirectories\n...\nok 2598 - checkout attr= ident aeol=crlf core.autocrlf=true core.eol= file=LF_mix_CR\nok 2599 - checkout attr= ident aeol=crlf core.autocrlf=true core.eol= file=LF_nul\nok 2600 - ls-files --eol -d -z\n# passed all 2600 test(s)\n

    Ce projet est assez simple \u00e0 compiler et \u00e0 tester. Il est bien document\u00e9 et les tests sont clairs. N\u00e9anmoins il ne d\u00e9pend pas de beaucoup de biblioth\u00e8ques externes. Pour des projets plus complexes, la gestion des d\u00e9pendances peut \u00eatre un vrai casse-t\u00eate. Prenons un autre exemple, celui de Gimp.

    ", "tags": ["zlib", "gcc", "README.md", "make", "configure", "libpcre", "libpcre2", "LICENSE", "COPYING", "Makefile", "libgit.a", "_install"]}, {"location": "course-c/45-software-design/contribute/#cas-de-figure-gimp", "title": "Cas de figure Gimp", "text": "

    Gimp est un logiciel de retouche d'image tr\u00e8s populaire. C'est la version libre de Adobe Photoshop. Il est \u00e9crit en C et utilise de nombreuses biblioth\u00e8ques externes. Pour compiler Gimp, il faut donc installer toutes ces biblioth\u00e8ques. La liste des d\u00e9pendances est longue et varie selon les distributions.

    Gimp

    En cherchant sous Google \u00ab\u2009gimp source code\u2009\u00bb, on tombe sur le site officiel\u2009:

    https://www.gimp.org/source/

    Il est indiqu\u00e9 que le code source est h\u00e9berg\u00e9 sur le r\u00e9f\u00e9rentiel de GNOME, un environnement de bureau libre pour les syst\u00e8mes Unix. Ce site redirige sur https://developer.gimp.org/ o\u00f9 l'on trouve les instructions pour les d\u00e9veloppeurs. De liens en liens on arrive sur la page Building GIMP qui donne les instructions pour compiler Gimp.

    Il est indiqu\u00e9 que Gimp utilise l'environnement Meson pour la compilation. Meson est un syst\u00e8me de build open-source qui permet de g\u00e9n\u00e9rer des fichiers de configuration pour les projets C/C++. Il est \u00e9crit en Python et est tr\u00e8s rapide.

    Les instructions indiquent certaines d\u00e9pendences\u2009:

    pkg-config

    Pkg-config est un outil qui permet de r\u00e9cup\u00e9rer les options de compilation et de liens pour les biblioth\u00e8ques install\u00e9es sur le syst\u00e8me. Il sera utilis\u00e9 par Meson pour trouver les biblioth\u00e8ques n\u00e9cessaires \u00e0 la compilation.

    gettext

    Gettext est une biblioth\u00e8que qui permet de g\u00e9rer les cha\u00eenes de caract\u00e8res multilingues. Elle est utilis\u00e9e par Gimp pour les traductions. C'est la biblioth\u00e8que de facto utilis\u00e9e pour l'internationalisation des logiciels libres.

    gegl et babl

    GEGL (Generic Graphics Library) est une biblioth\u00e8que de traitement d'image non-destructif. Elle est utilis\u00e9e par Gimp pour les op\u00e9rations de traitement d'image. BABL est une biblioth\u00e8que de conversion de couleurs. Elle est utilis\u00e9e par GEGL pour les conversions de couleurs. Ces biblioth\u00e8ques sont intrins\u00e8quement li\u00e9es \u00e0 Gimp.

    gtk3

    GTK3 est la biblioth\u00e8que graphique utilis\u00e9e par Gimp pour l'interface utilisateur. Elle est bas\u00e9e sur le langage de programmation C et est tr\u00e8s populaire pour les applications graphiques sous Linux. GTK3 d\u00e9pend \u00e9galement de glib et Pango.

    Comme il s'agit d'un gros projet, nous allons devoir installer un certain nombre de d\u00e9pendances qui ne seront pas n\u00e9cessairement utiles ensuite sur notre ordinateur. La bonne approche est de s'isoler dans un environnement virtuel. Docker est une bonne alternative. Comme Gimp est d\u00e9velopp\u00e9 sous Debian, nous allons utiliser une image Docker Debian pour compiler le projet.

    $ docker run -it debian:latest\nUnable to find image 'debian:latest' locally\nlatest: Pulling from library/debian\n903681d87777: Pull complete\nDigest: sha256:aadf411dc9ed5199bc7dab48b3e6ce18f8bbee4f170127f5ff1b75cd8035eb36\nStatus: Downloaded newer image for debian:latest\nroot@8c6a00ea1cf7:/#\n

    La premi\u00e8re \u00e9tape Preparing for Building propose d'installer le projet dans un r\u00e9pertoire local. Nous allons commencer par cr\u00e9er un dossier gimp et fixer quelques variables d'environnement\u2009:

    apt update\napt install build-essential cmake make git meson ninja-build bison flex pkg-config gettext\n
    mkdir gimp && cd gimp\nexport GIMP_DIR=$(pwd)\nexport GIMP_PREFIX=$GIMP_DIR/_install\n\n# Assuming that you have toolchain installed\n# If this don't work, so manually set the dirs\nLIB_DIR=$(cc -print-multi-os-directory | sed 's/\\.\\.\\///g')\ncc -print-multiarch | grep . && LIB_SUBDIR=$(echo $(cc -print-multiarch)'/')\n\n# Used to detect the build dependencies\nexport PKG_CONFIG_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}\npkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}\"\n\n# Used to find the libraries at runtime\nexport LD_LIBRARY_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}$\n{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\"\n\n# Used to find the glib-introspection dependencies\nexport XDG_DATA_DIRS=\"${GIMP_PREFIX}/share:/usr/share\n${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}\"\n\n# Used to find introspection files\nexport GI_TYPELIB_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}\ngirepository-1.0${GI_TYPELIB_PATH:+:$GI_TYPELIB_PATH}\"\n\n# Used by Autotools to find its tools\nexport ACLOCAL_FLAGS=\"-I $GIMP_PREFIX/share/aclocal $ACLOCAL_FLAGS\"\n\nexport XDG_DATA_DIRS=\"${GIMP_PREFIX}/share:/usr/local/share:/usr/share\"\nexport PATH=\"${GIMP_PREFIX}/bin:$PATH\"\n

    En suivant la documentation, on installe quelques d\u00e9pendences, sachant qu'il y en aura probablement d'autres \u00e0 installer au fur et \u00e0 mesure\u2009:

    apt install graphviz libswscale-dev libsuitesparse-dev libpng-dev\nlibtiff-dev librsvg2-dev \\\nliblcms2-dev libmypaint-dev mypaint-brushes  libmng-dev libwmf-dev \\\nlibaa1-dev libgs-dev libheif-dev gobject-introspection libgirepository1.0-dev\n

    500 MB plus tard, nous avons install\u00e9 les d\u00e9pendances de base. Nous allons maintenant cloner le projet Gimp\u2009:

    ", "tags": ["Pango", "gimp", "glib"]}, {"location": "course-c/45-software-design/contribute/#installation-de-babl", "title": "Installation de BABL", "text": "
    git clone --depth=1 https://gitlab.gnome.org/GNOME/babl.git\ncd babl\nmeson setup --prefix $GIMP_PREFIX -Denable-gir=true _build\nninja -C _build install\n
    "}, {"location": "course-c/45-software-design/contribute/#installation-de-gegl", "title": "Installation de GEGL", "text": "
    git clone --depth=1 https://gitlab.gnome.org/GNOME/gegl.git\ncd gegl\nmeson setup --prefix $GIMP_PREFIX -Dintrospection=true -Dcairo=enabled -Dumfpack=enabled -Dworkshop=true _build\nninja -C _build install\n
    "}, {"location": "course-c/45-software-design/contribute/#installation-de-gimp", "title": "Installation de GIMP", "text": "
    git clone --depth=1 --shallow-submodules  https://gitlab.gnome.org/GNOME/gimp.git\ncd gimp\ngit submodule update --init\nmeson setup --prefix $GIMP_PREFIX _build\nRun-time dependency atk found: NO (tried pkgconfig and cmake)\n\nmeson.build:364:0: ERROR: Dependency \"atk\" not found, tried pkgconfig and cmake\n

    Nous avons un probl\u00e8me. La d\u00e9pendance atk n'est pas trouv\u00e9e. Un rapide coup d'oeil \u00e0 la documentation nous indique que atk est une biblioth\u00e8que graphique et la version 2.4.0 est n\u00e9cessaire. Or la version que l'on trouve sous Debian est libatk1.0-dev qui est la 2.30.0. Comme seule la version mineure est diff\u00e9rente il ne devrait pas y avoir de probl\u00e8me de compatibilit\u00e9. Nous allons donc installer la version 2.30.0 et voir si cela fonctionne.

    apt install libatk1.0-dev\n

    Notons que la biblioth\u00e8que se nomme libatk1.0-dev le 1.0 est la version de l'API mais cela ne signifie pas que la version de la biblioth\u00e8que est 1.0. En fait tant que l'API est compatible, la version install\u00e9e peut \u00eatre plus r\u00e9cente. On peut noter toutefois que normalement si une version majeur change, cela signifie une rupture de compatibilit\u00e9 dans l'API. Ici on voit que les d\u00e9veloppeurs ont d\u00e9cid\u00e9 de ne pas rompre la compatibilit\u00e9.

    Rebolote, on relance la configuration de Gimp\u2009:

    meson setup --prefix $GIMP_PREFIX 'python-fu-eval' is n_build\nRun-time dependency exiv2 found: NO (tried pkgconfig and cmake)\n

    On va donc installer libexiv2-dev et libgexiv2-dev. Au prochain probl\u00e8me, on installera la d\u00e9pendance manquante et ainsi de suite jusqu'\u00e0 ce que la configuration se passe sans erreur\u2009:

    apt install libgirepository1.0-dev libgtk-3-dev glib-networking appstream \\\nlibappstream-glib-dev libxmu-dev libbz2-dev libpoppler-glib-dev python3-gi \\\nxsltproc xvfb  libc6-dev gi-docgen gjs luajit libxml2-utils \\\ndesktop-file-utils iso-codes libopenjp2-7-dev libjxl-dev libasound2-dev \\\nlibgudev-1.0-dev libcfitsio-dev xdg-utils libunwind-dev lua5.1 libxpm-dev \\\nlibopenexr-dev libvala-0.56-dev valac qoi python3-gi python3-gi-cairo gir1.2-gtk-3.0\n

    Une fois configur\u00e9 il suffit de compiler le projet\u2009:

    ninja -C _build install\n

    Et voil\u00e0, Gimp est install\u00e9 dans le r\u00e9pertoire _install.

    ", "tags": ["atk", "_install"]}, {"location": "course-c/45-software-design/contribute/#reproduction", "title": "Reproduction", "text": "

    Si vous essayez de reproduire cet exemple vous aurez probablement d'autres erreurs car vous n'avez pas les m\u00eames version. Dans cet exemple, nous avons utilis\u00e9 Debian Bookworm (12)

    gimp

    e89bb33408c5ed006b7b92fbb7c793dde169b02a

    gegl

    72d86816289c11fdd871b701827f8bf0016a7f4f

    babl

    c5f97c86224a473cc3fea231f17adef84580f2cc

    "}, {"location": "course-c/45-software-design/contribute/#dependances", "title": "D\u00e9pendances", "text": "

    On voit apr\u00e8s cet exemple que certains projets peuvent \u00eatre complexes \u00e0 compiler. Les probl\u00e8mes r\u00e9currents sont\u2009:

    • Vous ne lisez pas la documentation
    • Vous \u00eates trop press\u00e9 et vous ne lisez pas les messages d'erreur
    • Vous pensez avoir lu la documentation mais vous n'avez pas vraiment lu la documentation

    En dehors de ces quelques probl\u00e8mes, il y a certaines d\u00e9pendances qui d\u00e9pendent de la distribution Linux/Unix que vous avez, les noms de biblioth\u00e8ques ne sont pas n\u00e9cessairement les m\u00eames, ou les version g\u00e9n\u00e8rent des conflits.

    Dans cet exemple, nous avons \u00e9t\u00e9 contraint de compiler GEGL et BABL depuis les sources avec des options sp\u00e9cifiques pour \u00eatre compatible avec la derni\u00e8re version de Gimp sous Git

    Ce que nous avons vu ici c'est que les d\u00e9pendances dans un projet logiciel peuvent \u00eatre nombreuses.

    Vous pouvez vous demander pourquoi utiliser autant de biblioth\u00e8ques externes\u2009? La r\u00e9ponse est simple\u2009: pour ne pas r\u00e9inventer la roue. Les biblioth\u00e8ques externes sont souvent des projets open-source tr\u00e8s bien maintenus et test\u00e9s. Elles permettent de gagner du temps et de l'argent en r\u00e9utilisant du code existant. Par exemple dans le cas de Gimp, la biblioth\u00e8que aa est utilis\u00e9e pour convertir des images en ASCII art. La biblioth\u00e8que iso-codes permet de conna\u00eetre les noms des pays dans diff\u00e9rentes langues. La biblioth\u00e8que heif permet le support des images en format natif des iPhones r\u00e9cents. On peut citer lcms2 pour la gestion des couleurs via le standard ICC, ou la biblioth\u00e8que mypaint pour la gestion des pinceaux et des calques de peinture.

    ", "tags": ["heif", "lcms2", "mypaint"]}, {"location": "course-c/45-software-design/dependencies/", "title": "Gestion des d\u00e9pendances", "text": "

    Les d\u00e9pendances sont des biblioth\u00e8ques ou des modules qui sont utilis\u00e9s dans un projet. Elles peuvent \u00eatre des biblioth\u00e8ques tierces, des modules internes ou des fichiers de configuration. La gestion des d\u00e9pendances est un aspect important du d\u00e9veloppement logiciel, car elle permet de g\u00e9rer les d\u00e9pendances entre les diff\u00e9rents composants d'un projet.

    Lorsque vous travaillez sur un projet en C ou d'ailleurs dans un autre langage, vous aurez souvent besoin d'utiliser des biblioth\u00e8ques tierces pour \u00e9tendre les fonctionnalit\u00e9s de votre programme et ne nous le cachons pas c'est toujours un enfer de g\u00e9rer les d\u00e9pendances. Nous allons voir quelques outils qui peuvent vous aider \u00e0 g\u00e9rer les d\u00e9pendances de votre projet.

    "}, {"location": "course-c/45-software-design/dependencies/#gestionnaire-de-dependances", "title": "Gestionnaire de d\u00e9pendances", "text": "

    Imagions le projet suivant. C'est un fichier unique qui utilise les biblioth\u00e8ques Curl, SQLite et PCRE. Voici le code source\u2009:

    #include <stdio.h>\n#include <curl/curl.h>\n#include <sqlite3.h>\n#include <pcre.h>\n\nint main() {\n    CURL *curl = curl_easy_init();\n    if(curl) {\n        curl_easy_setopt(curl, CURLOPT_URL, \"http://example.com\");\n        curl_easy_perform(curl);\n        curl_easy_cleanup(curl);\n    }\n\n    sqlite3 *db;\n    if (sqlite3_open(\":memory:\", &db) == SQLITE_OK) {\n        printf(\"SQLite database opened successfully.\\n\");\n        sqlite3_close(db);\n    }\n\n    const char *error;\n    int erroffset;\n    pcre *re = pcre_compile(\"hello\", 0, &error, &erroffset, NULL);\n    if (re) {\n        printf(\"PCRE compiled successfully.\\n\");\n        pcre_free(re);\n    }\n}\n

    Pour g\u00e9rer les d\u00e9pendances de ce projet il est possible d'utiliser diff\u00e9rents outils tels que CMake, Meson, Conan, etc.

    "}, {"location": "course-c/45-software-design/dependencies/#meson", "title": "Meson", "text": "

    Meson est un syst\u00e8me de build open-source con\u00e7u pour \u00eatre rapide, facile \u00e0 utiliser et extensible. Il est \u00e9crit en Python et est compatible avec de nombreux langages de programmation, y compris C, C++, Rust, Java, etc. Meson est particuli\u00e8rement utile pour les projets C/C++ car il prend en charge la compilation crois\u00e9e, la g\u00e9n\u00e9ration de fichiers de configuration et la gestion des d\u00e9pendances. Il se base sur Ninja pour la compilation. Il s'agit d'une alternative \u00e0 Make plus moderne et plus rapide.

    Commencez par installer Meson sur votre syst\u00e8me\u2009:

    sudo apt-get install meson ninja-build \\\n     libcurl4-openssl-dev libsqlite3-dev libpcre3-dev\n

    Puis cr\u00e9er un fichier meson.build pour configurer le projet\u2009:

    meson.build
    project('my_project', 'c', version: '0.1.0')\n\n# D\u00e9pendances\nlibcurl = dependency('libcurl', version: '>=8.5')\nsqlite = dependency('sqlite3', version: '>=3.1')\npcre = dependency('libpcre', version: '>=8.0')\n\n# Sources\nexecutable('my_project',\n  sources: files('main.c'),\n  dependencies: [libcurl, sqlite, pcre]\n)\n

    Enfin, configurez et compilez le projet avec Meson\u2009:

    $ meson setup build\n$ ninja -C build\n

    Meson ne g\u00e8re pas les d\u00e9pendances tierces, il faut donc les installer manuellement. Dans notre cas, nous avons install\u00e9 les d\u00e9pendances avec apt-get avant de configurer le projet.

    ", "tags": ["meson.build"]}, {"location": "course-c/45-software-design/dependencies/#conan", "title": "Conan", "text": "

    Conan est un gestionnaire de d\u00e9pendances C/C++ open-source, d\u00e9centralis\u00e9 et multiplateforme. Il permet de g\u00e9rer les d\u00e9pendances de vos projets C/C++ en automatisant le processus de t\u00e9l\u00e9chargement, de compilation et d'installation des biblioth\u00e8ques tierces. Il est particuli\u00e8rement utile sous Windows o\u00f9 la gestion des d\u00e9pendances est souvent plus complexe.

    Conan est \u00e9crit en Python et est compatible avec de nombreux syst\u00e8mes de build, tels que CMake, Make, Visual Studio, Xcode, etc. Pour l'installer sur votre syst\u00e8me utilisez la commande suivante\u2009:

    pip install conan\n

    Imaginons que nous voulons cr\u00e9er un projet qui d\u00e9pend des biblioth\u00e8ques SQlite, Curl et PCRE. Le projet aura la structure suivante\u2009:

    project/\n\u251c\u2500\u2500 CMakeLists.txt\n\u251c\u2500\u2500 conanfile.txt\n\u251c\u2500\u2500 main.c\n\u2514\u2500\u2500 build/  (cr\u00e9\u00e9 apr\u00e8s la configuration)\n

    Le fichier conanfile.txt contiendra les d\u00e9pendances du projet\u2009:

    [requires]\nlibcurl/8.9.1\nsqlite3/3.43.2\npcre/8.45\n\n[generators]\nCMakeToolchain\nCMakeDeps\n\n[options]\nlibcurl/*:shared=True\nsqlite3/*:shared=True\npcre/*:shared=True\n

    Enfin le fichier CMakeLists.txt contiendra les instructions pour inclure les biblioth\u00e8ques dans le projet\u2009:

    cmake_minimum_required(VERSION 3.15)\nproject(MyProject C)\n\ninclude(${CMAKE_BINARY_DIR}/conan_toolchain.cmake)\n\nadd_executable(my_project main.c)\n\nfind_package(CURL REQUIRED)\nfind_package(sqlite3 REQUIRED)\nfind_package(PCRE REQUIRED)\n\ntarget_link_libraries(my_project CURL::libcurl sqlite3::sqlite3 PCRE::PCRE)\n

    Commencez par compiler les d\u00e9pendances avec Conan\u2009:

    conan profile detect --force\ndetect_api: Found cc=gcc- 13.2.0\ndetect_api: gcc>=5, using the major as version\ndetect_api: gcc C++ standard library: libstdc++11\n\nDetected profile:\n[settings]\narch=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.cppstd=gnu17\ncompiler.libcxx=libstdc++11\ncompiler.version=13\nos=Linux\n
    $ conan install . --output-folder=build --build=missing\n...\n======== Computing dependency graph ========\nsqlite3/3.43.2: Not found in local cache, looking in remotes...\nsqlite3/3.43.2: Checking remote: conancenter\nsqlite3/3.43.2: Downloaded recipe revision c850734dc9724ee9aa9e5d95efd0b100\npcre/8.45: Not found in local cache, looking in remotes...\npcre/8.45: Checking remote: conancenter\npcre/8.45: Downloaded recipe revision 64cdfd792761c32817cd31d7967c3709\nbzip2/1.0.8: Not found in local cache, looking in remotes...\nbzip2/1.0.8: Checking remote: conancenter\nbzip2/1.0.8: Downloaded recipe revision 457c272f7da34cb9c67456dd217d36c4\n...\nconanfile.txt: Generating aggregated env files\nconanfile.txt: Generated aggregated env files: ['conanbuild.sh', 'conanrun.sh']\nInstall finished successfully\n

    Ensuite, configurez le projet avec CMake\u2009:

    $ cmake -B build\n
    ", "tags": ["CMakeLists.txt", "conanfile.txt"]}, {"location": "course-c/45-software-design/licenses/", "title": "Les Licences", "text": ""}, {"location": "course-c/45-software-design/licenses/#introduction", "title": "Introduction", "text": "

    Une licence logicielle est un contrat l\u00e9gal qui d\u00e9termine comment un logiciel peut \u00eatre utilis\u00e9, modifi\u00e9, et redistribu\u00e9 par les utilisateurs. Elle d\u00e9finit les droits et les obligations des utilisateurs vis-\u00e0-vis du cr\u00e9ateur du logiciel. Les licences logicielles sont cruciales parce qu'elles \u00e9tablissent les r\u00e8gles de propri\u00e9t\u00e9 intellectuelle, garantissent la protection des int\u00e9r\u00eats des auteurs tout en r\u00e9gulant l'acc\u00e8s \u00e0 la technologie, et favorisent l'innovation et la collaboration dans le cadre de projets open-source ou commerciaux.

    Les licences peuvent couvrir une large gamme d'autorisations, allant de licences strictes o\u00f9 l'utilisateur doit payer pour acc\u00e9der et utiliser le logiciel, \u00e0 des licences plus ouvertes qui permettent une libre utilisation et modification du code. La diversit\u00e9 de ces licences refl\u00e8te les diff\u00e9rentes philosophies et besoins du monde du d\u00e9veloppement logiciel.

    "}, {"location": "course-c/45-software-design/licenses/#historique", "title": "Historique", "text": ""}, {"location": "course-c/45-software-design/licenses/#logiciel-proprietaire", "title": "Logiciel Propri\u00e9taire", "text": "

    Dans les ann\u00e9es 1960 et 1970, les logiciels \u00e9taient souvent vendus avec le mat\u00e9riel informatique, et les codes sources \u00e9taient parfois fournis sans restrictions significatives. Cependant, avec l'\u00e9volution du march\u00e9 du logiciel, des entreprises ont commenc\u00e9 \u00e0 comprendre la valeur commerciale des logiciels. Ainsi, les premi\u00e8res licences logicielles restrictives ont \u00e9t\u00e9 d\u00e9velopp\u00e9es pour prot\u00e9ger les int\u00e9r\u00eats commerciaux des entreprises, restreignant l'acc\u00e8s au code source et limitant les droits des utilisateurs \u00e0 simplement utiliser le logiciel sans en voir les coulisses.

    "}, {"location": "course-c/45-software-design/licenses/#emergence-de-lopen-source", "title": "\u00c9mergence de l'Open Source", "text": "

    Avec l'av\u00e8nement de l'Internet et le besoin croissant de collaboration entre d\u00e9veloppeurs, une nouvelle approche a \u00e9merg\u00e9\u2009: le logiciel libre. Le projet GNU, lanc\u00e9 par Richard Stallman en 1983, est le fer de lance de cette r\u00e9volution. La licence GNU General Public License (GPL) est l'une des premi\u00e8res licences \u00e0 promouvoir l'id\u00e9e que les logiciels devraient \u00eatre libres d'utilisation, de modification, et de redistribution, \u00e0 condition que toutes les versions modifi\u00e9es soient \u00e9galement distribu\u00e9es sous la m\u00eame licence.

    Cependant, dans les ann\u00e9es 1990, une s\u00e9rie de conflits bien connus sous le nom de \u00ab\u2009guerres de licences\u2009\u00bb ont eu lieu entre diff\u00e9rents camps du mouvement open-source. La licence BSD, originaire du projet Berkeley Software Distribution, offrait une approche plus permissive que la GPL, permettant l'incorporation du code BSD dans des projets propri\u00e9taires sans exiger la lib\u00e9ration du code source. Cela contrastait fortement avec la philosophie de la GPL, qui impose un partage en retour (copyleft), obligeant toute d\u00e9rivation du code \u00e0 rester libre et open-source. Ce diff\u00e9rend a mis en lumi\u00e8re les diff\u00e9rentes visions de ce que devrait \u00eatre l'open-source\u2009: la libert\u00e9 pour les utilisateurs de faire ce qu'ils veulent avec le code, y compris le rendre propri\u00e9taire (BSD), versus la garantie que le code reste libre \u00e0 perp\u00e9tuit\u00e9 (GPL).

    Ce d\u00e9bat est \u00e0 la g\u00e9n\u00e8se de Linux, le noyau du syst\u00e8me d'exploitation GNU/Linux, qui a adopt\u00e9 la GPL pour garantir que le code source reste libre et ouvert. Aujourd'hui, la GPL reste l'une des licences open-source les plus populaires, mais d'autres licences plus permissives, comme la licence MIT et la licence Apache, ont \u00e9galement gagn\u00e9 en popularit\u00e9.

    La licence MIT, extr\u00eamement populaire dans la communaut\u00e9 open-source, adopte une approche encore plus simplifi\u00e9e et permissive que la licence BSD. Elle permet \u00e0 quiconque d'utiliser, copier, modifier, fusionner, publier, distribuer, sous-licencier et/ou vendre des copies du logiciel, avec comme seule obligation de mentionner l\u2019auteur original. Ce type de licence est tr\u00e8s appr\u00e9ci\u00e9 pour sa simplicit\u00e9 et sa flexibilit\u00e9.

    Les licences Creative Commons (CC), bien qu'initialement con\u00e7ues pour les \u0153uvres cr\u00e9atives non logicielles (comme Wikipedia), ont \u00e9galement influenc\u00e9 le domaine du logiciel en fournissant des cadres l\u00e9gaux pour la diffusion de contenu, incluant parfois du code source, avec diff\u00e9rents niveaux de protection des droits d'auteur.

    "}, {"location": "course-c/45-software-design/licenses/#licences-populaires", "title": "Licences Populaires", "text": "

    Voici un aper\u00e7u des licences logicielles les plus couramment utilis\u00e9es\u2009:

    GNU General Public License (GPL)

    Garantit que le logiciel reste libre \u00e0 jamais, encourage la collaboration, mais incompatibilit\u00e9 avec les licences plus permissives, contraignante pour les projets commerciaux.

    GNU Lesser General Public License (LGPL)

    Permet l'utilisation de biblioth\u00e8ques open-source dans des logiciels propri\u00e9taires mais moins contraignante que la GPL, mais impose des restrictions sur les modifications du code source.

    MIT License

    Simplicit\u00e9, grande permissivit\u00e9, utilis\u00e9e dans une multitude de projets, mais permet la r\u00e9utilisation du code dans des logiciels propri\u00e9taires sans obligation de partage.

    Apache License 2.0

    Clauses explicites sur les brevets, permissive mais prot\u00e8ge les contributeurs, mais moins simple que la licence MIT, moins contraignante que la GPL pour garantir la libert\u00e9 du logiciel.

    Berkeley Software Distribution (BSD) License

    Tr\u00e8s permissive, flexible pour les int\u00e9grations dans des logiciels propri\u00e9taires mais ne garantit pas que les versions d\u00e9riv\u00e9es resteront libres.

    Creative Commons (CC0, BY, BY-SA, etc.)

    Id\u00e9al pour la diffusion de contenus cr\u00e9atifs, permet une large diffusion avec un choix de niveaux de protection, mais moins couramment utilis\u00e9e pour les logiciels purs, difficult\u00e9 d\u2019adaptation pour certains projets techniques.

    Le choix d'une licence d\u00e9pend donc des objectifs du projet. On peut citer par exemple les cas de figures suivant\u2009:

    D\u00e9veloppement Commercial

    Pour les entreprises souhaitant conserver le contr\u00f4le total sur leur code, une licence propri\u00e9taire ou une licence permissive comme MIT ou BSD est souvent conseill\u00e9e.

    Protection de la Propri\u00e9t\u00e9 Intellectuelle

    La GPL est id\u00e9ale si l'objectif est de garantir que le logiciel reste libre et open-source, prot\u00e9geant ainsi les droits des utilisateurs \u00e0 modifier et redistribuer le code.

    Collaboration Ouverte

    La licence Apache ou MIT est souvent choisie pour des projets collaboratifs open-source, o\u00f9 la flexibilit\u00e9 et l\u2019ouverture sont primordiales, tout en assurant une protection contre les litiges de brevets (dans le cas d\u2019Apache).

    Projets Acad\u00e9miques et Exp\u00e9rimentaux

    Pour favoriser la diffusion rapide et libre des innovations, une licence tr\u00e8s permissive comme la licence MIT ou CC0 peut \u00eatre privil\u00e9gi\u00e9e.

    Contenu Cr\u00e9atif ou Documentation

    Les licences Creative Commons sont id\u00e9ales pour prot\u00e9ger et partager des documentations, des illustrations, ou des contenus audiovisuels associ\u00e9s aux projets logiciels.

    "}, {"location": "course-c/45-software-design/licenses/#brevets-logiciels", "title": "Brevets Logiciels", "text": "

    Un brevet logiciel est une protection juridique accord\u00e9e \u00e0 une invention logicielle, permettant \u00e0 son d\u00e9tenteur de contr\u00f4ler l'utilisation, la production, et la vente de cette invention pendant une p\u00e9riode donn\u00e9e, g\u00e9n\u00e9ralement 20 ans. Contrairement aux licences logicielles, qui r\u00e9gissent l'utilisation d'un logiciel, un brevet prot\u00e8ge une id\u00e9e ou une m\u00e9thode sp\u00e9cifique impl\u00e9ment\u00e9e dans le logiciel. Les brevets logiciels sont particuli\u00e8rement controvers\u00e9s en raison de leur complexit\u00e9, de leur co\u00fbt, et de leurs implications sur l'innovation technologique.

    Les brevets logiciels conf\u00e8rent \u00e0 leurs d\u00e9tenteurs un monopole sur une certaine technologie ou m\u00e9thode, leur permettant d'emp\u00eacher d'autres entreprises ou individus d'utiliser cette technologie sans autorisation. Cela peut \u00eatre avantageux pour les entreprises qui investissent massivement en R&D, car un brevet peut fournir un retour sur investissement en prot\u00e9geant leur innovation des concurrents.

    Cependant, ces brevets peuvent \u00e9galement freiner l'innovation en emp\u00eachant d'autres d\u00e9veloppeurs de construire sur des id\u00e9es existantes. Ils peuvent cr\u00e9er un environnement juridique complexe o\u00f9 les entreprises doivent naviguer prudemment pour \u00e9viter les poursuites en contrefa\u00e7on de brevet. Cela est particuli\u00e8rement probl\u00e9matique dans le d\u00e9veloppement de logiciels, o\u00f9 de nombreuses innovations sont incr\u00e9mentales et construites \u00e0 partir d'id\u00e9es pr\u00e9existantes.

    Le d\u00e9p\u00f4t d'un brevet logiciel est un processus co\u00fbteux, tant en termes de temps que d'argent. Les co\u00fbts incluent\u2009:

    Frais de d\u00e9p\u00f4t

    Les frais de d\u00e9p\u00f4t peuvent varier de quelques milliers \u00e0 plusieurs dizaines de milliers d'euros/dollars, selon le pays et la complexit\u00e9 du brevet.

    Frais juridiques

    L'assistance d'avocats sp\u00e9cialis\u00e9s est souvent n\u00e9cessaire pour pr\u00e9parer et d\u00e9poser un brevet, ce qui peut co\u00fbter plusieurs milliers d'euros suppl\u00e9mentaires.

    Frais de maintenance

    Une fois accord\u00e9, le brevet doit \u00eatre maintenu en vigueur, ce qui implique des frais p\u00e9riodiques (annuaires) qui peuvent \u00e9galement \u00eatre \u00e9lev\u00e9s.

    En raison de ces co\u00fbts, le d\u00e9p\u00f4t de brevets logiciels est souvent hors de port\u00e9e des petites entreprises et des d\u00e9veloppeurs individuels. M\u00eame pour les grandes entreprises, le co\u00fbt et la gestion des portefeuilles de brevets peuvent devenir un fardeau.

    D'autre part un brevet n'est pas universellement reconnu, et leur statut juridique varie d'un pays \u00e0 l'autre. Aux \u00c9tats-Unis par exemple, les brevets logiciels sont largement reconnus et accord\u00e9s par l'Office des brevets et des marques de commerce des \u00c9tats-Unis (USPTO). Cependant, une s\u00e9rie de d\u00e9cisions judiciaires r\u00e9centes, comme l'affaire Alice Corp. v. CLS Bank International en 2014, a restreint la port\u00e9e des brevets logiciels en exigeant que les inventions brevetables ne soient pas de simples \u00ab\u2009id\u00e9es abstraites\u2009\u00bb. En Europe, les brevets logiciels sont plus restrictifs. L'Office europ\u00e9en des brevets (OEB) n'accorde des brevets logiciels que si l'invention apporte une \u00ab\u2009contribution technique\u2009\u00bb au-del\u00e0 de l'algorithme ou du code lui-m\u00eame.

    Ces diff\u00e9rences de traitement rendent complexe la protection globale d'une innovation logicielle par un brevet, obligeant les entreprises \u00e0 adapter leurs strat\u00e9gies en fonction des juridictions.

    Pour ces raisons, beaucoup d'entreprises choisissent d'\u00e9viter les brevets logiciels. V\u00e9rifier la violation d'un brevet logiciel est notoirement difficile. Les logiciels peuvent \u00eatre \u00e9crits de plusieurs fa\u00e7ons pour atteindre un m\u00eame objectif fonctionnel, ce qui permet aux d\u00e9veloppeurs de contourner un brevet en modifiant l\u00e9g\u00e8rement leur code. Cela limite l'efficacit\u00e9 des brevets logiciels en tant que m\u00e9canisme de protection.

    De nombreuses entreprises pr\u00e9f\u00e8rent recourir \u00e0 d'autres strat\u00e9gies de protection telles que le secret commercial, qui est souvent moins co\u00fbteux et ne n\u00e9cessite pas de divulgation publique de l'invention, contrairement aux brevets.

    En conclusion, les brevets logiciels repr\u00e9sentent une arme \u00e0 double tranchant dans le domaine du g\u00e9nie logiciel. D'un c\u00f4t\u00e9, ils offrent une protection potentielle des innovations, permettant aux entreprises de capitaliser sur leurs investissements en R&D. De l'autre c\u00f4t\u00e9, leur co\u00fbt \u00e9lev\u00e9, la complexit\u00e9 juridique, les restrictions internationales, et la difficult\u00e9 \u00e0 v\u00e9rifier et \u00e0 faire respecter les brevets r\u00e9duisent leur attrait pour de nombreuses entreprises, en particulier dans un secteur en rapide \u00e9volution comme celui du d\u00e9veloppement logiciel.

    Ainsi, de nombreuses entreprises choisissent de ne pas breveter leurs logiciels, pr\u00e9f\u00e9rant d'autres formes de protection, ou simplement adopter une approche plus ouverte et collaborative. Les brevets logiciels demeurent un sujet controvers\u00e9, avec des implications profondes pour l'innovation, la concurrence, et la croissance \u00e9conomique dans le domaine des technologies de l'information.

    "}, {"location": "course-c/45-software-design/licenses/#conclusion", "title": "Conclusion", "text": "

    Les licences logicielles jouent un r\u00f4le central dans le d\u00e9veloppement et la distribution des logiciels. Elles influencent non seulement les droits et obligations des utilisateurs, mais aussi la mani\u00e8re dont les logiciels peuvent \u00e9voluer, \u00eatre partag\u00e9s, ou int\u00e9gr\u00e9s dans d'autres projets. Le choix d'une licence doit toujours \u00eatre guid\u00e9 par une compr\u00e9hension claire des objectifs du projet, des valeurs du cr\u00e9ateur, et des besoins des utilisateurs finaux. En fin de compte, la licence choisie peut d\u00e9terminer la port\u00e9e, l'impact et la durabilit\u00e9 d'un logiciel.

    "}, {"location": "course-c/45-software-design/software-project/", "title": "Organisation d'un projet", "text": ""}, {"location": "course-c/45-software-design/software-project/#introduction", "title": "Introduction", "text": "

    Dans ce chapitre, nous allons voir comment organiser un projet logiciel. Nous allons voir comment structurer un projet, comment g\u00e9rer les d\u00e9pendances, comment g\u00e9rer les tests unitaires et comment g\u00e9rer les tests fonctionnels.

    "}, {"location": "course-c/45-software-design/software-project/#structure-dun-projet", "title": "Structure d'un projet", "text": "

    La structure d'un projet logiciel est un \u00e9l\u00e9ment important pour sa maintenabilit\u00e9. Une bonne structure permet de retrouver facilement les fichiers sources, les fichiers d'en-t\u00eate, les tests, etc.

    Si votre projet est accessible par d'autres d\u00e9veloppeurs, il y a certaines conventions \u00e0 respecter pour que tout le monde puisse s'y retrouver.

    Tout commence avec un dossier racine. Ce dossier racine contient tous les fichiers sources, les fichiers d'en-t\u00eate, les tests, les d\u00e9pendances, etc. Encore faut-il bien nommer ce projet. Un nom de projet doit \u00eatre court, explicite et unique. Il est recommand\u00e9 de ne pas utiliser d'espaces, de caract\u00e8res sp\u00e9ciaux ou de majuscules.

    Les Majuscules

    Les noms de fichiers et de dossiers sont sensibles \u00e0 la casse sur les syst\u00e8mes POSIX mais pas sous Windows. Cela cr\u00e9e certains probl\u00e8mes de compatibilit\u00e9 entre les syst\u00e8mes d'exploitation.

    D'autre part, l'usage des majuscules peut cr\u00e9er des ambigu\u00eft\u00e9s. Par exemple, NomDuFichier et nomdufichier sont deux noms diff\u00e9rents. Comment allez-vous expliquer par t\u00e9l\u00e9phone \u00e0 un coll\u00e8gue que le fichier s'\u00e9crit de cette mani\u00e8re\u2009? L'ennui c'est les acronymes. Par exemple, XMLParser, XmlParser ou XMLparser\u2009? Vous aurez tendance \u00e0 choisir la troisi\u00e8me solution pour que XML ressort bien mais vous \u00eates incoh\u00e9rent puisque vous avez pas utilis\u00e9 de majuscul pour Parser.

    Le probl\u00e8me est bien r\u00e9solu avec l'utilisation d'underscores ou de tirets (notation kekbab-case ou snake_case). Par exemple, xml_parser est plus lisible et plus facile \u00e0 expliquer.

    Une convention en voque est de nommer les fichiers en minuscules et d'utiliser des tirets pour s\u00e9parer les mots. Par exemple, xml-parser. C'est la convention utilis\u00e9e sur GitHub pour le nom des d\u00e9p\u00f4ts.

    Voici une structure de projet classique\u2009:

    projet/\n\u251c\u2500\u2500 src/\n\u2502   \u251c\u2500\u2500 main.c\n\u2502   \u251c\u2500\u2500 foo.c\n\u2502   \u2514\u2500\u2500 bar.c\n\u251c\u2500\u2500 include/\n\u2502   \u251c\u2500\u2500 foo.h\n\u2502   \u2514\u2500\u2500 bar.h\n\u251c\u2500\u2500 tests/\n\u2502   \u251c\u2500\u2500 test_foo.c\n\u2502   \u2514\u2500\u2500 test_bar.c\n\u251c\u2500\u2500 Makefile\n\u2514\u2500\u2500 README.md\n

    Le point d'entr\u00e9e pour le d\u00e9veloppeur c'est le fichier README.md. Ce fichier contient une description du projet, des instructions pour l'installation, des instructions pour la compilation, des instructions pour les tests, etc.

    ", "tags": ["XML", "README.md", "xml_parser", "NomDuFichier", "nomdufichier", "Parser"]}, {"location": "course-c/45-software-design/software-project/#readmemd", "title": "README.md", "text": "

    Jadis, le fichier README \u00e9tait un fichier texte simple. Aujourd'hui, c'est un fichier Markdown. Le Markdown est un langage de balisage l\u00e9ger cr\u00e9\u00e9 en 2004 par John Gruber et Aaron Swartz. Il est facile \u00e0 lire et \u00e0 \u00e9crire. Il est utilis\u00e9 sur de tr\u00e8s nombreux supports\u2009: GitHub, GitLab, Bitbucket, Reddit, Stack Overflow, etc. C'est d'ailleurs le format utilis\u00e9 pour r\u00e9diger cet ouvrage.

    Voici un exemple de fichier README.md :

    # Nom du Projet\n\nDescription du projet.\n\n## Installation\n\nComment installer le projet :\n\n```bash\ngit clone http://...\ncd projet\nmake\n
    ", "tags": ["README.md", "README", "Markdown"]}, {"location": "course-c/45-software-design/software-project/#utilisation", "title": "Utilisation", "text": "

    Comment utiliser le projet\u2009:

    ./projet\n

    Le fichier README.md est un fichier important. Il doit \u00eatre \u00e0 jour et bien r\u00e9dig\u00e9 car c'est la premi\u00e8re chose que l'on voit lorsqu'on arrive sur le d\u00e9p\u00f4t du projet. Il doit permettre \u00e0 l'utilisateur rapidement\u2009:

    1. Que fait le projet et quelle est son utilit\u00e9\u2009?
    2. Est-ce que ce projet est fait pour moi\u2009?
    3. Comment l'installer\u2009?
    4. Comment l'utiliser\u2009?
    5. Comment contribuer\u2009?
    ", "tags": ["README.md"]}, {"location": "course-c/45-software-design/software-project/#dotfiles", "title": "Dotfiles", "text": "

    Dans un syst\u00e8me POSIX, les fichiers commen\u00e7ant par un point sont des fichiers cach\u00e9s. C'est une convention utilis\u00e9e pour les fichiers de configuration. Ces fichiers sont appel\u00e9s dotfiles.

    On va trouver toute une panoplie de fichiers de configuration dans un projet logiciel tel que\u2009:

    • .gitignore : liste des fichiers \u00e0 ignorer par Git.
    • .editorconfig : conventions de codage pour les \u00e9diteurs de texte.
    • .github/ : dossier contenant l'int\u00e9gration continue, les actions GitHub, etc.
    • .env : variables d'environnement pour le projet.
    ", "tags": ["dotfiles"]}, {"location": "course-c/45-software-design/software-project/#makefile", "title": "Makefile", "text": "

    Le Makefile est un fichier de configuration pour le programme make. C'est g\u00e9n\u00e9ralement le point d'entr\u00e9e pour construire un projet. Si vous trouvez un Makefile, vous savez que vous pourrez tr\u00e8s probablement simplement utiliser la commande make pour construire le projet.

    N\u00e9anmoins, parfois le Makefile n'est pas suffisant car votre projet est plus complexe et d\u00e9pend de plusieurs biblioth\u00e8ques tierces qui peuvent \u00eatre fastidieuses \u00e0 installer. C'est l\u00e0 qu'interviennent les gestionnaires de d\u00e9pendances que nous verrons plus tard.

    Alternativement, on peut trouver simplement un script build.sh ou build.bat pour construire le projet que vous appelerer avec ./build.sh ou .\\build.bat.

    ", "tags": ["make", "Makefile", "build.sh", "build.bat"]}, {"location": "course-c/45-software-design/software-project/#organisation-des-fichiers-sources", "title": "Organisation des fichiers sources", "text": "

    Lorsque le projet d\u00e9passe 5 \u00e0 10 fichiers, il est habituel de les d\u00e9placer dans un dossier pour ne pas encombrer la racine du projet. On cr\u00e9e un dossier src pour les fichiers sources qui va contenir tous les fichiers .c.

    Faut-il s\u00e9parer les fichiers sources des fichiers d'en-t\u00eate\u2009? C'est une question de go\u00fbt. Certains d\u00e9veloppeurs pr\u00e9f\u00e8rent tout mettre dans un seul dossier src pour simplifier la navigation. D'autres pr\u00e9f\u00e8rent s\u00e9parer les fichiers sources des fichiers d'en-t\u00eate. Cela permet de mieux organiser le projet et de mieux g\u00e9rer les d\u00e9pendances. Il n'y a pas de consensus \u00e9tabli mais on peut noter plusieurs points.

    Lorsqu'un projet est destin\u00e9 \u00e0 devenir une biblioth\u00e8que partag\u00e9e, vous devez fournir les fichiers d'en-t\u00eate pour que les autres d\u00e9veloppeurs puissent utiliser votre biblioth\u00e8que. S'ils sont s\u00e9par\u00e9s, c'est plus facile de les extraire et les distribuer.

    N\u00e9anmoins, s\u00e9placer les fichiers d'en-t\u00eate dans un dossier include n\u00e9cessite d'informer le compilateur de l'emplacement des fichiers d'en-t\u00eate. C'est g\u00e9n\u00e9ralement fait avec l'option -I de GCC\u2009:

    gcc -Iinclude -c src/*.c\n
    ", "tags": ["include", "src"]}, {"location": "course-c/45-software-design/software-project/#repertoire-de-construction", "title": "R\u00e9pertoire de construction", "text": "

    Lorsque vous compilez un projet, vous allez g\u00e9n\u00e9rer des fichiers interm\u00e9diaires (fichiers objets) et des fichiers finaux (ex\u00e9cutables, biblioth\u00e8ques). Selon le projet il peut y avoir beaucoup de fichiers. Afin d'\u00e9viter de ne voir ces fichiers dans votre explorateur de fichiers, on cr\u00e9e un dossier build pour les stocker.

    Ceci est une convention. Vous pouvez choisir un autre nom pour ce dossier. Par exemple, bin, out, dist, etc.

    N\u00e9anmoins, il faut d'une part penser \u00e0 ajouter ce dossier dans le .gitignore pour ne pas le versionner, mais \u00e9galement modifier la configuration de votre compilateur pour qu'il place les fichiers objets dans ce dossier\u2009:

    mkdir -p build\ngcc -Iinclude -c src/*.c -o build/*.o\n
    ", "tags": ["build", "out", "bin", "dist"]}, {"location": "course-c/45-software-design/software-project/#en-tetes-de-fichiers", "title": "En-t\u00eates de fichiers", "text": "

    Historiquement, les d\u00e9veloppeurs C utilisait l'outil Doxygen pour g\u00e9n\u00e9rer de la documentation \u00e0 partir des commentaires dans le code source. Cela a donn\u00e9 naissance \u00e0 une convention pour les commentaires de documentation qui est souvent utilis\u00e9e de mani\u00e8re abusive.

    Aujourd'hui, avec les \u00e9diteurs modernes, il est plus facile de naviguer dans le code source et de trouver des informations sur les fonctions et les structures. Les commentaires de documentation sont souvent redondants et inutiles, ils polluent plus qu'ils n'aident. N\u00e9anmoins, je peux vous donner un exemple de commentaires de documentation pour un fichier d'en-t\u00eate\u2009:

    /**\n * @brief D\u00e9finition des fonctions pour manipuler des nombres\n * @file numbers.h\n * @date 2024-09-01\n * @author John Doe\n * @version 1.0\n * @copyright Copyright (c) 2021\n *\n * Description d\u00e9taill\u00e9e.\n */\n\n/*********************************************************/\n/*                       INCLUDES                        */\n/*********************************************************/\n#include <stdio.h>\n\n/*********************************************************/\n/*                       STRUCTURES                      */\n/*********************************************************/\ntypedef struct point {\n    int x; //!< Abscisse du point.\n    int y; //!< Ordonn\u00e9e du point.\n} Point; //!< Structure point.\n\n/*********************************************************/\n/*                       FONCTIONS                       */\n/*********************************************************/\n\n/**\n * @brief Fonction d'addition de deux entiers.\n * @param a Premier entier.\n * @param b Deuxi\u00e8me entier.\n * @return La somme des deux entiers.\n *\n * Cette fonction permet d'additionner deux entiers.\n */\nint add(int a, int b) {\n    return a + b;\n}\n\n/*********************************************************/\n/*                       FIN                             */\n/*********************************************************/\n

    Nous pouvons maintenant discuter de la pertinence de ces commentaires. Int\u00e9ressons-nous tout d'abord \u00e0 l'en-t\u00eate du fichier\u2009:

    @file

    Le nom du fichier est en redondance avec le vrai nom du fichier. Si vous renommez le fichier, vous devrez \u00e9galement changer ce commentaire. C'est d'une part une source d'erreur mais surtout pour Git, cela implique que le fichier n'est pas simplement renomm\u00e9 mais supprim\u00e9 et recr\u00e9\u00e9. Cela peut poser des probl\u00e8mes de fusion et d'acc\u00e8s \u00e0 l'historique. Aussi, si vous utilisez Git, cette information est parfaitement inutile.

    @date

    La date est inutile car si vous avez besoin de la date, vous pouvez utiliser Git pour voir la date de la derni\u00e8re modification du fichier. D'autre part, il est courant qu'un fichier soit modifi\u00e9 sans que la date soit mise \u00e0 jour. Cela peut \u00eatre une source de confusion.

    @author

    Si vous utilisez Git, vous poss\u00e9dez d\u00e9j\u00e0 cette information et de mani\u00e8re beaucoup plus fine. Vous pouvez voir qui a modifi\u00e9 chaque ligne du fichier. G\u00e9rer cette information dans le fichier c'est \u00e9galement s'astreindre \u00e0 ajouter son nom, m\u00eame si vous n'avez ajout\u00e9 qu'une ligne. Et puis, si vous aimez tant le fichier que vous venez tant \u00e0 le modifier que vous avez chang\u00e9 toutes les lignes, devez-vous supprimer le nom du vrai auteur pour mettre le v\u00f4tre\u2009? C'est une source de conflit, est c'est beaucoup plus simple de laisser Git g\u00e9rer cette information.

    @version

    M\u00eame rengaine et m\u00eame punition. Si vous utilisez Git, vous avez d\u00e9j\u00e0 cette information. Git g\u00e8re les version avec les tags et les branches. Vous pouvez voir l'historique des modifications et les diff\u00e9rences entre les versions. D'autre part, pour les m\u00eame raisons qu'\u00e9voqu\u00e9es, \u00e0 chaque nouvelle version de votre projet, vous devez modifier ce commentaire dans tous les fichiers. Cela implique que tous ces fichiers vont changer et vous allez vous perdre dans l'historique.

    @brief

    Enfin une information utile. C'est une excellente pratique que d'avoir une description courte du fichier. N\u00e9anmoins avec Doxygen il existe l'option JAVADOC_AUTOBRIEF qui permet de g\u00e9n\u00e9rer automatiquement le @brief \u00e0 partir de la premi\u00e8re phrase du commentaire si elle se termine par un point. C'est une option \u00e0 activer pour \u00e9viter de r\u00e9p\u00e9ter la m\u00eame chose.

    @copyright

    C'est une information importante. Elle permet de savoir comment le fichier peut \u00eatre utilis\u00e9. N\u00e9anmoins, il est rare que pour un projet donn\u00e9, le copyright et la licence changent d'un fichier \u00e0 l'autre, il est donc plus judicieux de mettre cette information dans le README ou mieux dans un fichier LICENSE.

    Maintenant que nous avons balay\u00e9 l'en-t\u00eate, concentrons-nous sur la fonction add. Cette fonction fait 3 lignes et le commentaire 8 lignes. Est-ce r\u00e9ellement n\u00e9cessaire de documenter une fonction que tout le monde peut comprendre\u2009?

    La r\u00e9ponse n'est pas si simple. Si vous publier le code source avec votre biblioth\u00e8que, la r\u00e9ponse est non car n'importe qui pourra la comprendre. N\u00e9anmoins votre biblioth\u00e8que est publi\u00e9e sans le code source, la seule chose que votre utlisateur aura c'est le fichier d'en-t\u00eate (.h) :

    int add(int a, int a);\n

    C'est peut-\u00eatre un peu court et un compl\u00e9ment d'information pourrait \u00eatre utile. Mais faut-il documenter tous les param\u00e8tres\u2009? Si le brief dit que la fonction additionne deux entiers, et que vous avez acc\u00e8s aux types des param\u00e8tres, est-ce vraiment n\u00e9cessaire de documenter les param\u00e8tres\u2009? La r\u00e9ponse est non. Je le r\u00e9p\u00e8te souvent, un commentaire est fait pour expliquer le pourquoi, pas le comment. Si vous avez besoin de documenter le comment, c'est que votre code n'est pas assez clair. On pourrait par exemple r\u00e9\u00e9crire la fonction de la mani\u00e8re suivante\u2009:

    int add_two_integers_together(int first, int second) {\n    return first + second;\n}\n

    Je vous l'accorde, c'est un peu long, mais \u00e7a \u00e0 le m\u00e9rire d'\u00eatre parfaitement clair.

    Questionnons maintenant les s\u00e9parateurs de contenu. Lorsque vous d\u00e9butez en programmation, vous avez tendance \u00e0 vous faire mousser un peu en \u00e9crivant plus que le n\u00e9cessaire, \u00e9crire des commentaires c'est sans risque et cela rallonge votre beau programme. Alors pour bien structurer votre code, vous avez mis des s\u00e9parateurs de contenu. N\u00e9anmoins ils n'apportent pas grand chose. En Python par exemple, la norme propose d'ajouter deux retour \u00e0 la ligne entre chaque fonction. C'est une convention qui est largement suffisante pour s\u00e9parer les diff\u00e9rents \u00e9l\u00e9ments du code.

    Enfin, la derni\u00e8re question est de savoir si le commentaire de la structure Point est utile. Pour cette structure qui est tr\u00e8s simple, on pourrait se passer de commentaires, mais dans le cas ou les \u00e9l\u00e9ments n\u00e9cessitent d'\u00eatre expliqu\u00e9s, c'est la notation Doxygen qui peut \u00eatre discut\u00e9e.

    Doxygen utilise la notation //! pour les commentaires de documentation. C'est une notation sp\u00e9ciale qui a la facheuse tendance s'afficher en rouge vif dans certains \u00e9diteurs de texte. Si vous n'utilisez pas Doxygen, il est pr\u00e9f\u00e9rable de ne pas utiliser cette notation. Il est pr\u00e9f\u00e9rable de rester sur la notation // qui est plus universelle.

    Apr\u00e8s ces longues explications, je vous propose de vous redonner l'exemple simplifi\u00e9\u2009:

    /**\n * D\u00e9finition des fonctions pour manipuler des nombres\n *\n * Description d\u00e9taill\u00e9e.\n */\n#include <stdio.h>\n\ntypedef struct point {\n    int x; //!< Abscisse du point.\n    int y; //!< Ordonn\u00e9e du point.\n} Point; //!< Structure point.\n\nint add_two_integers(int a, int b) {\n    return a + b;\n}\n

    Le commentaire de trop

    /*********************************************************/\n/*                    FIN DU FICHIER                     */\n/*********************************************************/\n

    Diable\u2009! Pourquoi trouve-t-on toujours un commentaire FIN \u00e0 la fin des fichiers\u2009? C'est une pratique qui remonte \u00e0 l'\u00e9poque des cartes perfor\u00e9es. Les cartes perfor\u00e9es \u00e9taient utilis\u00e9es pour stocker les programmes. Chaque carte avait un num\u00e9ro de ligne et un num\u00e9ro de colonne. Pour \u00e9viter de m\u00e9langer les cartes, on mettait un commentaire FIN \u00e0 la fin de chaque carte. C'est une pratique ancestrale qui a perdur\u00e9 jusqu'\u00e0 aujourd'hui. Les d\u00e9veloppeurs r\u00e9p\u00e8tent inlassablement ces habitudes sans toutefois les remettre en question.

    Aujourd'hui, un fichier est fini au sens que lorsqu'on en atteint la fin, on le sait. Il n'est alors pas pertinent de le pr\u00e9ciser.

    C'est un peu la m\u00eame chose que ces pages dans les livres laiss\u00e9es blanches, ou presque, avec un commentaire \u00ab\u2009Cette page a \u00e9t\u00e9 intentionnellement laiss\u00e9e blanche\u2009\u00bb qui remontait \u00e0 l'\u00e9poque de l'impression en offset. Lorsqu'on imprimait un livre, on imprimait les pages par feuille de 16 pages. Si le livre faisait 200 pages, il fallait ajouter 4 pages blanches \u00e0 la fin pour que l'impression soit correcte. Aujourd'hui, les livres sont imprim\u00e9s en num\u00e9rique et il n'y a plus besoin de ces pages blanches.

    ", "tags": ["add", "README", "Point", "FIN", "LICENSE"]}, {"location": "course-c/45-software-design/software-project/#gestion-de-configuration", "title": "Gestion de configuration", "text": "

    La gestion de configuration est un \u00e9l\u00e9ment important pour un projet logiciel. Elle permet de stocker des informations qui peuvent varier d'un environnement \u00e0 un autre. Par exemple, les informations de connexion \u00e0 une base de donn\u00e9es, les cl\u00e9s d'API, les adresses IP, etc.

    Il existe plusieurs solutions pour g\u00e9rer la configuration d'un projet\u2009:

    • Les variables d'environnement.
    • Les fichiers de configuration.
    "}, {"location": "course-c/45-software-design/software-project/#variables-denvironnement", "title": "Variables d'environnement", "text": "

    Les variables d'environnement sont des variables stock\u00e9es dans l'environnement d'ex\u00e9cution d'un programme. Elles sont accessibles par le programme et peuvent \u00eatre modifi\u00e9es par l'utilisateur. Ces variables sont propag\u00e9es \u00e0 tous les processus enfants pour autant qu'elles ait \u00e9t\u00e9 export\u00e9es. Elles peuvent \u00eatre utile dans un projet pour stocker des informations sensibles qui ne devraient pas \u00eatre versionn\u00e9es.

    Prenons l'exemple d'une cl\u00e9 d'API. Cette cl\u00e9 vous permet d'acc\u00e9der \u00e0 un service tiers depuis votre programme. Chaque utilisateur de votre programme doit avoir sa propre cl\u00e9 d'API. Si vous stockez cette cl\u00e9 dans le code source, vous risquez de la versionner et de la rendre publique. Si vous stockez cette cl\u00e9 dans un fichier de configuration, vous risquez de le versionner et de le rendre public \u00e9galement. Une bonne solution est de stocker cette cl\u00e9 dans une variable d'environnement.

    Il se peut d'ailleurs que votre projet n\u00e9cessite plusieurs variables d'environnement. On utilise couramment un fichier .env.example pour lister les variables d'environnement n\u00e9cessaires au projet. Ce fichier est versionn\u00e9 et contient des valeurs par d\u00e9faut. L'utilisateur doit copier ce fichier en .env et renseigner les valeurs.

    .env.example
    API_KEY=your_api_key\nDATABASE_URL=your_database_url\n
    .env
    API_KEY=1234567890\nDATABASE_URL=postgres://user:password@localhost:5432/database\n

    Pour d\u00e9ployer votre environnement de d\u00e9veloppement, vous devez exporter ces variables d'environnement. Vous pouvez le faire dans votre terminal ou dans un fichier de configuration de votre terminal (.bashrc, .zshrc, etc.).

    export $(cat .env | xargs)\n

    Sous Windows vous pouvez utiliser la commande set :

    for /f \"delims== tokens=1,2\" %i in (.env) do set %i=%j\n

    Ces commandes sont g\u00e9n\u00e9ralement int\u00e9gr\u00e9es dans un script start.sh ou start.bat pour d\u00e9marrer votre projet, ou alors directement dans le Makefile.

    ", "tags": ["start.bat", "start.sh", "Makefile", "set"]}, {"location": "course-c/45-software-design/software-project/#fichier-den-tete", "title": "Fichier d'en-t\u00eate", "text": "

    On trouve tr\u00e8s souvent dans un projet C un fichier d'en-t\u00eate .h qui contient des d\u00e9finitions de configuration. Par exemple, un fichier config.h qui contient des constantes, des macros, des structures, etc.

    #ifndef CONFIG_H\n#define CONFIG_H\n\n#define VERSION \"1.0.0\"\n#define AUTHOR \"John Doe\"\n\n#define USE_FEATURE_A\n#define USE_FEATURE_B\n\n#define API_KEY \"your_api_key\"\n#define DATABASE_URL \"your_database_url\"\n

    Ce fichier est inclus dans les fichiers sources qui ont besoin de ces informations. Il est versionn\u00e9 et donc ne devrait pas contenir d'informations sensibles. Si vous avez des informations sensibles, vous pouvez les stocker dans un fichier .env et les exporter dans les variables d'environnement. Ces variables peuvent \u00eatre utilis\u00e9e dans votre Makefile pour d\u00e9clarer des variables de compilation.

    include .env\n\nCFLAGS += -DAPI_KEY=\\\"$(API_KEY)\\\"\nCFLAGS += -DDATABASE_URL=\\\"$(DATABASE_URL)\\\"\n
    ", "tags": ["config.h"]}, {"location": "course-c/45-software-design/teamwork/", "title": "Travail en \u00e9quipe", "text": ""}, {"location": "course-c/45-software-design/teamwork/#introduction", "title": "Introduction", "text": "

    Dans le monde du d\u00e9veloppement logiciel, le travail en \u00e9quipe est incontournable. Tr\u00e8s rarement vous travaillerez seul sur un projet, surtout dans un contexte professionnel. La collaboration avec d'autres d\u00e9veloppeurs, testeurs, chefs de projet, graphistes, commerciaux, et autres acteurs est essentielle pour r\u00e9ussir. Savoir travailler en \u00e9quipe est donc une comp\u00e9tence cruciale \u00e0 d\u00e9velopper pour tout professionnel du secteur.

    "}, {"location": "course-c/45-software-design/teamwork/#travail-en-entreprise", "title": "Travail en entreprise", "text": "

    Le travail en entreprise, qu'il s'agisse d'une petite ou d'une grande structure, pr\u00e9sente des dynamiques et des d\u00e9fis uniques. Selon la taille de l'entreprise, les m\u00e9thodes de travail, la communication et la gestion des responsabilit\u00e9s peuvent varier consid\u00e9rablement. Il est important de comprendre ces diff\u00e9rences pour mieux s'adapter aux environnements professionnels vari\u00e9s et pour anticiper les d\u00e9fis potentiels. Une fois sorti de l'\u00e9cole, vous aurez le choix entre travailler dans une start-up, une PME, une grande entreprise ou m\u00eame en freelance. Chacun de ces environnements a ses avantages et ses inconv\u00e9nients.

    "}, {"location": "course-c/45-software-design/teamwork/#petite-entreprise", "title": "Petite entreprise", "text": "

    Dans une petite entreprise, les \u00e9quipes sont souvent r\u00e9duites (souvent 2 \u00e0 5 personnes), ce qui favorise une communication directe et une grande flexibilit\u00e9. Les r\u00f4les peuvent \u00eatre moins d\u00e9finis et plus polyvalents, chaque membre de l'\u00e9quipe \u00e9tant susceptible de porter plusieurs casquettes. Cette polyvalence peut \u00eatre stimulante, car elle offre l'opportunit\u00e9 d'acqu\u00e9rir une vari\u00e9t\u00e9 de comp\u00e9tences et de participer \u00e0 divers aspects du projet. En qualit\u00e9 d'\u00e9lectronicien vous serez amen\u00e9 \u00e0 g\u00e9rer des projets de A \u00e0 Z, et vous investir aussi dans le d\u00e9veloppement logiciel. Vous pourriez \u00eatre amen\u00e9 \u00e0 faire vous m\u00eame les choix technologiques selon vos crit\u00e8res de s\u00e9lection.

    La prise de d\u00e9cision dans une petite entreprise est aussi g\u00e9n\u00e9ralement plus rapide. Les hi\u00e9rarchies sont souvent plates, et il est plus facile de collaborer directement avec les dirigeants ou les fondateurs. Cette proximit\u00e9 permet de r\u00e9agir rapidement aux changements, d'innover plus facilement, et de mettre en \u0153uvre des id\u00e9es sans passer par de longs processus de validation. En outre, les relations de confiance peuvent permettre d'avoir carte blanche pour explorer de nouvelles id\u00e9es et de prendre des initiatives.

    Cependant, cette flexibilit\u00e9 a aussi ses limites. Le manque de ressources peut parfois conduire \u00e0 une surcharge de travail, o\u00f9 les employ\u00e9s doivent jongler entre plusieurs responsabilit\u00e9s. Cela peut cr\u00e9er des tensions si les priorit\u00e9s ne sont pas bien g\u00e9r\u00e9es. De plus, l'absence de processus formalis\u00e9s peut entra\u00eener des conflits de responsabilit\u00e9, surtout lorsque les t\u00e2ches ne sont pas clairement assign\u00e9es.

    Une petite entreprise ou start/up vit souvent au jour le jour, et les priorit\u00e9s peuvent changer rapidement. L'attitude est \u00e0 la survie, la recherche de financement, et la croissance. Les projets sont souvent courts, et les \u00e9quipes sont amen\u00e9es \u00e0 pivoter rapidement en fonction des retours clients.

    En termes de d\u00e9veloppement logiciel, cela se traduit par des projets agiles, des it\u00e9rations rapides, et une forte collaboration entre les membres de l'\u00e9quipe. La communication est essentielle pour s'assurer que tout le monde est sur la m\u00eame longueur d'onde et que les objectifs sont clairs. Exploiter au mieux la communit\u00e9 open-source est souvent une strat\u00e9gie gagnante pour les petites entreprises, qui peuvent ainsi b\u00e9n\u00e9ficier de logiciels et de biblioth\u00e8ques de qualit\u00e9 sans avoir \u00e0 les d\u00e9velopper elles-m\u00eames ce qui se traduit par un consid\u00e9rable gain de temps. N\u00e9anmoins, il est fondamental de bien comprendre les licences open-source pour \u00e9viter tout probl\u00e8me de conformit\u00e9.

    Le processus d'engagement de nouveaux employ\u00e9s est souvent rapide et bas\u00e9 sur le feeling humain davantage que par le niveau de qualification. Les comp\u00e9tences techniques sont importantes, mais la personnalit\u00e9 et la capacit\u00e9 \u00e0 s'int\u00e9grer dans une \u00e9quipe sont souvent des crit\u00e8res de s\u00e9lection tout aussi importants. D'autre part, les petites entreprises n'ont pas toujours les moyens de proposer des salaires comp\u00e9titifs, mais elles peuvent offrir des avantages en nature, des opportunit\u00e9s de croissance rapide, et une ambiance de travail conviviale.

    "}, {"location": "course-c/45-software-design/teamwork/#grande-entreprise", "title": "Grande entreprise", "text": "

    Dans une grande entreprise (300..50'000 employ\u00e9s), les processus sont beaucoup plus formalis\u00e9s et structur\u00e9s. Bien que cela puisse assurer une certaine coh\u00e9rence et standardisation dans les pratiques de travail, cela condut in\u00e9vitablement \u00e0 une inertie organisationnelle. Les d\u00e9cisions transitent par plusieurs niveaux hi\u00e9rarchiques, ce qui peut ralentir l'innovation et rendre l'adaptation aux changements plus difficile.

    Par ailleurs de nombreuses grandes entreprises adh\u00e8rent \u00e0 des normes de qualit\u00e9 et de s\u00e9curit\u00e9 strictes, ce qui peut \u00eatre un avantage pour les projets critiques. Les processus de validation et de test sont g\u00e9n\u00e9ralement plus rigoureux, ce qui garantit un niveau de qualit\u00e9 \u00e9lev\u00e9. Cependant, cela peut aussi ralentir le cycle de d\u00e9veloppement et rendre les projets moins flexibles. La compliance avec la norme ISO 9001 est souvent un pr\u00e9requis pour travailler avec des grandes entreprises. Elle impose une nomenclature stricte, des processus de validation et de test, et une documentation compl\u00e8te des projets qui sont on un impact non n\u00e9gligeable sur le temps de d\u00e9veloppement et la motivation des \u00e9quipes qui peuvent se sentir brid\u00e9es dans leur cr\u00e9ativit\u00e9 et peuvent sembler faire trop de paperasse administrative sans valeur ajout\u00e9e.

    Le cloisonnement (ou \u00ab\u2009silos\u2009\u00bb) est un ph\u00e9nom\u00e8ne courant dans les grandes entreprises. Les diff\u00e9rents d\u00e9partements ou \u00e9quipes peuvent fonctionner de mani\u00e8re relativement autonome, avec peu d'interactions entre eux. Ce cloisonnement est souvent la source de tensions lorsqu'il y a un manque de communication ou de coordination entre les \u00e9quipes. Par exemple, une \u00e9quipe de d\u00e9veloppement peut se retrouver en conflit avec une \u00e9quipe de marketing si les objectifs ne sont pas align\u00e9s ou si les informations ne sont pas partag\u00e9es efficacement. Il n'est pas rare que le d\u00e9partement des ventes promette des fonctionnalit\u00e9s qui ne sont pas encore d\u00e9velopp\u00e9es, ou que le d\u00e9partement marketing communique sur des dates de sortie qui ne sont pas encore fix\u00e9es ou qui ne sont pas r\u00e9alistes. Ces d\u00e9calages peuvent entra\u00eener des frustrations et des malentendus, et nuire \u00e0 la qualit\u00e9 du produit final, notament en coupant les coins ronds pour respecter des d\u00e9lais irr\u00e9alistes. Cela se traduit dans le d\u00e9veloppement par des dettes techniques croissantes et des retours de flamme parfois catastrophiques.

    Le cloisonnement peut \u00e9galement entra\u00eener des conflits de responsabilit\u00e9. Dans une grande entreprise, il peut \u00eatre difficile de savoir qui est responsable de quoi, surtout lorsque plusieurs \u00e9quipes travaillent sur un m\u00eame projet. Les probl\u00e8mes de coordination peuvent mener \u00e0 des malentendus, des retards, et m\u00eame \u00e0 des conflits internes lorsque des t\u00e2ches importantes sont n\u00e9glig\u00e9es ou mal ex\u00e9cut\u00e9es. Les d\u00e9cisions se prennent souvent en comit\u00e9 de direction sans l'avis des \u00e9quipes techniques qui sont souvent les plus \u00e0 m\u00eame de comprendre les implications techniques des d\u00e9cisions prises. Cela m\u00e8ne souvent \u00e0 des d\u00e9cisions non optimales.

    La complexit\u00e9 des structures hi\u00e9rarchiques dans les grandes entreprises peut aussi engendrer des frustrations. Les employ\u00e9s peuvent se sentir \u00e9loign\u00e9s des d\u00e9cideurs et avoir l'impression que leurs contributions individuelles passent inaper\u00e7ues. Par ailleurs, le besoin de se conformer \u00e0 des processus rigides peut parfois \u00e9touffer la cr\u00e9ativit\u00e9 et l'innovation, rendant les employ\u00e9s moins enclins \u00e0 proposer de nouvelles id\u00e9es. Les employ\u00e9s peuvent se sentir n'\u00eatre que des num\u00e9ros. N\u00e9anmoins c'est une ligne d\u00e9cisionnelle de s'assurer qu'un employ\u00e9 ne devienne pas indispensable pour l'entreprise, et que le savoir soit partag\u00e9 entre les membres de l'\u00e9quipe afin d'\u00e9viter le \u00ab\u2009bus factor\u2009\u00bb (si un employ\u00e9 se fait renverser par un bus, l'entreprise ne doit pas s'arr\u00eater de fonctionner).

    Cependant, travailler dans une grande entreprise offre aussi de nombreux avantages. Les ressources disponibles sont souvent plus importantes, permettant un meilleur acc\u00e8s \u00e0 des formations, des technologies avanc\u00e9es, et des opportunit\u00e9s de d\u00e9veloppement de carri\u00e8re. La sp\u00e9cialisation des r\u00f4les peut aussi permettre aux employ\u00e9s de devenir experts dans un domaine particulier, avec un soutien organisationnel plus structur\u00e9. L'acc\u00e8s \u00e0 des logiciels de pointe et \u00e0 des ressources de formation est souvent plus facile dans une grande entreprise, qui peut se permettre d'investir.

    Le processus d'engagement dans une grande entreprise est souvent plus formel et structur\u00e9, avec des entretiens multiples, des tests techniques, et des \u00e9valuations approfondies. Les comp\u00e9tences techniques, notament en g\u00e9nie logiciel sont \u00e9valu\u00e9es par un code interview qui n\u00e9cessite de r\u00e9soudre des probl\u00e8mes algorithmiques en temps limit\u00e9.

    "}, {"location": "course-c/45-software-design/teamwork/#comparaison-des-dynamiques", "title": "Comparaison des dynamiques", "text": "

    La diff\u00e9rence fondamentale entre une petite et une grande entreprise r\u00e9side dans la mani\u00e8re dont les processus et la communication sont g\u00e9r\u00e9s. Dans une petite entreprise, la rapidit\u00e9 et la flexibilit\u00e9 sont souvent privil\u00e9gi\u00e9es, mais cela peut se faire au d\u00e9triment de la clart\u00e9 des r\u00f4les et des responsabilit\u00e9s. \u00c0 l'inverse, dans une grande entreprise, la structure et les processus sont plus \u00e9tablis, mais cela peut engendrer de l'inertie et des d\u00e9fis en termes de collaboration inter-\u00e9quipes.

    "}, {"location": "course-c/45-software-design/teamwork/#equipe-de-developpement", "title": "\u00c9quipe de d\u00e9veloppement", "text": "

    Dans le domaine du d\u00e9veloppement logiciel, une \u00e9quipe de d\u00e9veloppement r\u00e9unit plusieurs personnes ayant des comp\u00e9tences diverses et compl\u00e9mentaires. Dans les grandes entreprises ces r\u00f4les sont tr\u00e8s souvent cloisonn\u00e9s, alors que dans les petites entreprises, les membres de l'\u00e9quipe peuvent \u00eatre amen\u00e9s \u00e0 porter plusieurs casquettes. N\u00e9anmoins on retrouve g\u00e9n\u00e9ralement les r\u00f4les suivants\u2009:

    Chef de projet

    Responsable de la gestion globale du projet, il assure la coordination des \u00e9quipes et veille \u00e0 ce que le projet soit livr\u00e9 dans les d\u00e9lais, dans le respect du budget et avec le niveau de qualit\u00e9 attendu. Il est le point de contact principal entre l'\u00e9quipe technique et les parties prenantes.

    D\u00e9veloppeur

    Il a pour mission de concevoir, \u00e9crire, tester et documenter le code du logiciel. Les d\u00e9veloppeurs peuvent \u00eatre sp\u00e9cialis\u00e9s dans diff\u00e9rents domaines (front-end, back-end, full-stack, etc.), chacun apportant une expertise particuli\u00e8re au projet.

    Testeur

    Sp\u00e9cialis\u00e9 dans l'assurance qualit\u00e9, le testeur v\u00e9rifie que le logiciel fonctionne comme pr\u00e9vu, en identifiant les bugs et en s'assurant que toutes les fonctionnalit\u00e9s respectent les exigences d\u00e9finies. Le r\u00f4le du testeur est essentiel pour garantir un produit final fiable et performant.

    DevOps

    Le DevOps est charg\u00e9 de la gestion de l'infrastructure n\u00e9cessaire au d\u00e9ploiement du logiciel. Il travaille \u00e0 l'int\u00e9gration continue, \u00e0 la livraison continue, et \u00e0 l'automatisation des processus de d\u00e9ploiement, garantissant ainsi que le code peut \u00eatre d\u00e9ploy\u00e9 rapidement et en toute s\u00e9curit\u00e9.

    Designer UX/UI

    Bien que non mentionn\u00e9 initialement, il est important de noter que le designer UX/UI joue un r\u00f4le cl\u00e9 dans la conception de l'interface utilisateur et dans l'exp\u00e9rience utilisateur globale. Son travail est crucial pour s'assurer que le logiciel est non seulement fonctionnel mais aussi agr\u00e9able et intuitif \u00e0 utiliser.

    Lorsque vous \u00eates mendat\u00e9 pour \u00e9valuer l'effort de d\u00e9veloppement sur un projet, vous vous concentrez sur l'aspect technique du projet tout en restant tr\u00e8s optimiste sur les d\u00e9lais. Vous avez tendance \u00e0 sous-estimer le temps n\u00e9cessaire pour r\u00e9soudre les probl\u00e8mes techniques et \u00e0 surestimer votre capacit\u00e9 \u00e0 les r\u00e9soudre rapidement. Vous avez tendance \u00e0 ignorer les aspects non techniques du projet, comme la documentation, les tests, et la communication avec les parties prenantes. En r\u00e9alit\u00e9, chacun des r\u00f4les pr\u00e9c\u00e9demment mentionn\u00e9s \u00e0 une implication dans le projet. Il n'y a pas de r\u00e8gle g\u00e9n\u00e9rale mais une bonne approximation du temps entre les r\u00f4les pour le d\u00e9veloppement d'un logiciel est \u2153 pour le d\u00e9veloppement, \u2153 pour les tests, et \u2153 pour la documentation et la communication, d'o\u00f9 le facteur \\(\\pi\\) fr\u00e9quemment utilis\u00e9 dans le calcul de l'effort de d\u00e9veloppement.

    "}, {"location": "course-c/45-software-design/teamwork/#methodes-de-travail", "title": "M\u00e9thodes de travail", "text": "

    Le choix de la m\u00e9thode de travail est d\u00e9terminant pour la r\u00e9ussite d'un projet. Voici un aper\u00e7u des m\u00e9thodes les plus couramment utilis\u00e9es en d\u00e9veloppement logiciel\u2009:

    M\u00e9thode en cascade

    Traditionnellement, la m\u00e9thode en cascade segmente le projet en plusieurs phases lin\u00e9aires (analyse, conception, d\u00e9veloppement, test, d\u00e9ploiement). Chaque phase doit \u00eatre compl\u00e9t\u00e9e avant de passer \u00e0 la suivante. Cette approche convient bien aux projets o\u00f9 les exigences sont claires et peu susceptibles de changer.

    M\u00e9thode agile

    Les m\u00e9thodes agiles sont plus flexibles et permettent de s'adapter aux changements fr\u00e9quents des exigences. Le projet est divis\u00e9 en plusieurs cycles courts appel\u00e9s \u00ab\u2009sprints\u2009\u00bb (en Scrum) ou est g\u00e9r\u00e9 en flux continu (en Kanban). Les it\u00e9rations successives permettent de livrer rapidement des fonctionnalit\u00e9s et de recevoir des retours en continu, facilitant ainsi l'am\u00e9lioration progressive du produit.

    Scrum

    Un cadre agile populaire qui repose sur des it\u00e9rations r\u00e9guli\u00e8res (sprints) de 2 \u00e0 4 semaines, au cours desquelles une version du produit potentiellement livrable est d\u00e9velopp\u00e9e. Scrum met l'accent sur la collaboration, l'adaptation et l'am\u00e9lioration continue.

    Kanban

    Une autre m\u00e9thode agile qui se concentre sur la visualisation du flux de travail et la gestion continue des t\u00e2ches. Le Kanban est particuli\u00e8rement utile pour les \u00e9quipes cherchant \u00e0 am\u00e9liorer l'efficacit\u00e9 et \u00e0 r\u00e9duire les goulots d'\u00e9tranglement.

    "}, {"location": "course-c/45-software-design/teamwork/#outils-de-travail", "title": "Outils de travail", "text": "

    Pour une collaboration efficace en \u00e9quipe, l'utilisation d'outils adapt\u00e9s est indispensable. Voici quelques-uns des outils les plus couramment utilis\u00e9s\u2009:

    Gestion de version avec Git

    Git est un syst\u00e8me de gestion de versions distribu\u00e9, permettant \u00e0 plusieurs d\u00e9veloppeurs de travailler simultan\u00e9ment sur le m\u00eame projet sans risquer de perdre des modifications ou d'\u00e9craser le travail des autres. Il est essentiel pour la collaboration en \u00e9quipe, facilitant le suivi de l'\u00e9volution du code, la cr\u00e9ation de branches pour des fonctionnalit\u00e9s sp\u00e9cifiques, et la gestion des contributions gr\u00e2ce aux pull requests.

    Gestion de projet avec Trello

    Trello est un outil visuel de gestion de projet bas\u00e9 sur des tableaux, des listes et des cartes. Chaque membre de l'\u00e9quipe peut suivre l'avancement des t\u00e2ches, ajouter des commentaires, des pi\u00e8ces jointes, et organiser le travail de mani\u00e8re claire et intuitive. Trello est particuli\u00e8rement utile pour les \u00e9quipes qui utilisent une m\u00e9thode agile comme Kanban.

    Communication avec Slack/Teams/Discord

    Ces outils de communication sont essentiels pour maintenir une communication fluide au sein de l'\u00e9quipe, surtout dans un contexte de t\u00e9l\u00e9travail. Ils permettent non seulement de discuter en temps r\u00e9el, mais aussi de partager des fichiers, de coordonner des r\u00e9unions et de centraliser les \u00e9changes sur des canaux d\u00e9di\u00e9s \u00e0 diff\u00e9rents aspects du projet.

    Int\u00e9gration continue et d\u00e9ploiement continu (CI/CD) avec Jenkins/GitLab CI

    Pour automatiser le processus de construction, de test, et de d\u00e9ploiement du logiciel, des outils de CI/CD comme Jenkins ou GitLab CI sont souvent utilis\u00e9s. Ils permettent de r\u00e9duire les erreurs humaines, d'acc\u00e9l\u00e9rer le cycle de d\u00e9veloppement, et de s'assurer que le code reste toujours dans un \u00e9tat d\u00e9ployable.

    "}, {"location": "course-c/45-software-design/teamwork/#gestion-des-conflits", "title": "Gestion des conflits", "text": "

    Le d\u00e9veloppement logiciel, nous ne cessons de le r\u00e9p\u00e9ter, est un travail collaboratif, lequel peut parfois donner lieu \u00e0 des conflits. Ces conflits peuvent d\u00e9couler de diff\u00e9rences de vision, de priorit\u00e9s ou de m\u00e9thodes de travail entre les membres de l'\u00e9quipe. Ils apparaissent lorsque le niveau d'implication ou d'engagement transgresse la barri\u00e8re de l'\u00e9motionnel. Une grande implication dans un projet peut entra\u00eener des jalousies. La frustration d'avoir l'impression d'en faire davantage que les autres est une source courante de conflits car elle peut \u00eatre per\u00e7ue comme une injustice. Elle peut pousser un employ\u00e9 \u00e0 d'imisser dans les affaires des autres, \u00e0 critiquer leur travail, ou \u00e0 les d\u00e9nigrer. Apprendre \u00e0 identifier, g\u00e9rer et r\u00e9soudre ces conflits de mani\u00e8re efficace est crucial pour maintenir une dynamique de travail productive et harmonieuse.

    "}, {"location": "course-c/45-software-design/teamwork/#identifier-les-sources-de-conflits", "title": "Identifier les sources de conflits", "text": "

    Les sources de conflits peuvent varier selon le contexte de l'entreprise, mais certaines causes sont r\u00e9currentes dans le d\u00e9veloppement logiciel\u2009:

    Diff\u00e9rences de vision ou d'objectifs

    Dans les grandes entreprises, les diff\u00e9rents d\u00e9partements (d\u00e9veloppement, marketing, ventes) peuvent avoir des objectifs qui ne s'alignent pas toujours, cr\u00e9ant des tensions sur la direction \u00e0 prendre pour le projet. Dans une petite entreprise, ces conflits peuvent survenir entre les membres de l'\u00e9quipe qui ont des id\u00e9es divergentes sur les priorit\u00e9s du projet.

    Probl\u00e8mes de communication

    Le manque de communication est une source fr\u00e9quente de conflits. Dans une petite entreprise, la communication directe peut parfois manquer de formalisme, ce qui peut entra\u00eener des malentendus. Dans une grande entreprise, le cloisonnement entre les \u00e9quipes peut emp\u00eacher la circulation d'informations cruciales, exacerbant les tensions. C'est pourquoi il est essentiel de documenter les d\u00e9cisions prises et de les communiquer clairement \u00e0 l'ensemble de l'\u00e9quipe par des communiqu\u00e9s concis.

    Conflits de responsabilit\u00e9s

    Les zones grises concernant les responsabilit\u00e9s peuvent provoquer des conflits, surtout dans les grandes entreprises o\u00f9 les r\u00f4les sont tr\u00e8s sp\u00e9cialis\u00e9s. Dans les petites entreprises, o\u00f9 les membres de l'\u00e9quipe doivent souvent assumer plusieurs r\u00f4les, la r\u00e9partition floue des t\u00e2ches peut \u00e9galement \u00eatre source de d\u00e9saccords.

    Pression des d\u00e9lais

    La pression pour respecter les d\u00e9lais peut intensifier les conflits, particuli\u00e8rement lorsque les attentes sont mal g\u00e9r\u00e9es. Cela est particuli\u00e8rement vrai dans les grandes entreprises o\u00f9 les processus rigides peuvent ajouter une pression suppl\u00e9mentaire, mais c'est aussi un d\u00e9fi dans les petites entreprises o\u00f9 les ressources sont souvent limit\u00e9es.

    "}, {"location": "course-c/45-software-design/teamwork/#techniques-de-resolution-de-conflits", "title": "Techniques de r\u00e9solution de conflits", "text": "

    Une fois les sources de conflits identifi\u00e9es, il est essentiel d'avoir des techniques efficaces pour les r\u00e9soudre. Voici quelques approches courantes\u2009:

    Communication ouverte

    Encourager un dialogue ouvert et honn\u00eate est la premi\u00e8re \u00e9tape pour r\u00e9soudre un conflit. Dans une petite entreprise, cela peut signifier organiser des r\u00e9unions r\u00e9guli\u00e8res o\u00f9 chaque membre peut exprimer ses pr\u00e9occupations. Dans une grande entreprise, cela peut n\u00e9cessiter la mise en place de canaux de communication formels pour s'assurer que les pr\u00e9occupations sont entendues et trait\u00e9es.

    Mise en place de processus clairs

    \u00c9tablir des processus clairs pour la gestion des responsabilit\u00e9s et des t\u00e2ches peut pr\u00e9venir de nombreux conflits. Dans les grandes entreprises, cela implique souvent l'utilisation d'outils de gestion de projet et de documentation pr\u00e9cise. Dans les petites entreprises, il peut \u00eatre utile de formaliser certains aspects de la collaboration pour \u00e9viter les malentendus.

    Compromis et n\u00e9gociation

    Souvent, la r\u00e9solution des conflits n\u00e9cessite un compromis. Apprendre \u00e0 n\u00e9gocier des solutions qui satisfont les diff\u00e9rentes parties est une comp\u00e9tence cl\u00e9. Dans les grandes entreprises, cela peut impliquer des discussions entre les d\u00e9partements pour aligner les objectifs. Dans les petites entreprises, cela peut signifier que les membres de l'\u00e9quipe doivent \u00eatre pr\u00eats \u00e0 ajuster leurs attentes et priorit\u00e9s.

    Feedback constructif

    Donner et recevoir du feedback de mani\u00e8re constructive peut aider \u00e0 d\u00e9samorcer les conflits avant qu'ils ne s'intensifient. Dans un environnement de petite entreprise, o\u00f9 les relations sont souvent plus personnelles, le feedback doit \u00eatre donn\u00e9 avec tact pour maintenir une bonne ambiance de travail. Dans les grandes entreprises, des syst\u00e8mes formels de feedback peuvent \u00eatre mis en place pour encourager une communication r\u00e9guli\u00e8re.

    "}, {"location": "course-c/45-software-design/teamwork/#importance-de-lempathie", "title": "Importance de l'empathie", "text": "

    L'empathie joue un r\u00f4le crucial dans la gestion des conflits, surtout dans un cadre collaboratif comme le d\u00e9veloppement logiciel. \u00catre capable de se mettre \u00e0 la place de ses coll\u00e8gues et de comprendre leurs perspectives permet de mieux aborder les conflits de mani\u00e8re constructive.

    L'\u00e9coute active est une comp\u00e9tence essentielle pour manifester de l'empathie. Cela signifie \u00e9couter non seulement les mots de l'autre personne, mais aussi essayer de comprendre les \u00e9motions et les motivations sous-jacentes. Dans une petite entreprise, o\u00f9 les relations sont souvent plus proches, cela peut contribuer \u00e0 renforcer la coh\u00e9sion d'\u00e9quipe. Dans une grande entreprise, o\u00f9 les interactions peuvent \u00eatre plus formelles, l'\u00e9coute active peut aider \u00e0 d\u00e9passer les barri\u00e8res organisationnelles.

    La reconnaissance des besoins des autres est aussi fondamentale. Reconna\u00eetre que chaque membre de l'\u00e9quipe a des besoins et des contraintes diff\u00e9rentes est essentiel. Par exemple, un d\u00e9veloppeur peut \u00eatre sous pression pour livrer du code rapidement, tandis qu'un testeur peut avoir besoin de plus de temps pour assurer la qualit\u00e9. Dans une grande entreprise, la reconnaissance de ces diff\u00e9rences peut aider \u00e0 aligner les \u00e9quipes sur des objectifs communs. Dans une petite entreprise, cela peut \u00e9viter les frustrations li\u00e9es \u00e0 des attentes irr\u00e9alistes.

    "}, {"location": "course-c/45-software-design/teamwork/#gerer-un-projet-logiciel", "title": "G\u00e9rer un projet logiciel", "text": "

    Comme nous l'avons vu pr\u00e9c\u00e9demment, la gestion d'un projet logiciel implique de nombreuses \u00e9tapes qu'il est important de ne pas courtcircuiter. Cela passe souvent par une analyse fonctionnelle et l'\u00e9laboration d'un cahier des charges fonctionnel.

    Le processus est it\u00e9ratif et r\u00e9cursif. Bien qu'il y ait un ordre d'\u00e9tapes \u00e0 suivre, il est souvent n\u00e9cessaire de revenir en arri\u00e8re pour ajuster les sp\u00e9cifications en fonction des retours utilisateurs ou des contraintes techniques.

    "}, {"location": "course-c/45-software-design/teamwork/#cahier-des-charges-fonctionnel", "title": "Cahier des charges fonctionnel", "text": ""}, {"location": "course-c/45-software-design/teamwork/#analyse-des-cas-dutilisation", "title": "Analyse des cas d'utilisation", "text": "

    La premi\u00e8re \u00e9tape est l'analyse des cas d'utilisation, qui consiste d'une part \u00e0 identifier les utilisateurs du syst\u00e8me et d'autre part les interactions qu'ils auront avec le syst\u00e8me. Cette analyse permet in fine de d\u00e9marrer l'analyse des besoins.

    On peut repr\u00e9senter les cas d'utilisation sous forme de diagrammes de cas d'utilisation qui fait partie du standard UML (Unified Modeling Language). Un diagramme de cas d'utilisation est un diagramme qui montre les acteurs, les cas d'utilisation et les interactions entre eux.

    Imagions que nous devons d\u00e9velopper un syst\u00e8me qui prendra la forme d'une machine \u00e0 caf\u00e9 connect\u00e9e. Identifier les acteurs doit \u00eatre la premi\u00e8re \u00e9tape.

    Il ne faut pas h\u00e9siter faire preuve d'imagination et d'\u00eatre exausitif. Dans notre exemple, les acteurs pourraient \u00eatre\u2009:

    • l'utilisateur qui souhaite boire un caf\u00e9\u2009;
    • le technicien qui doit entretenir la machine\u2009;
    • le service client qui doit r\u00e9pondre aux questions des utilisateurs\u2009;
    • l'entreprise qui veut encourager la productivit\u00e9 au travail\u2009;
    • l'op\u00e9rateur de service.

    Le diagramme suivant montre la repr\u00e9sentation de ces cas d'utilisation.

    Diagramme de cas d'utilisation

    \u00c0 partir de ce diagramme, on peut d\u00e9duire les besoins fonctionnels du syst\u00e8me. Par exemple, le cas d'utilisation \u00ab\u2009R\u00e8glage de la mouture\u2009\u00bb implique que le syst\u00e8me doit \u00eatre capable d'offir une interface utilisateur permettant de r\u00e9gler la mouture du caf\u00e9. Le cas d'utilisation \u00ab\u2009Maintenance\u2009\u00bb implique que le syst\u00e8me doit \u00eatre capable de d\u00e9tecter les pannes et de les signaler \u00e0 l'utilisateur.

    Il se peut que des utilisateurs ou des actions inutiles ait \u00e9t\u00e9 ajout\u00e9es ou qu'il en manque. Ce n'est pas un probl\u00e8me \u00e0 ce stade de l'exercice car le processus de r\u00e9flexion est it\u00e9ratifs. On peut revenir plus tard sur le diagramme pour l'ajuster et consolider les hypoth\u00e8ses initiales.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-du-besoin", "title": "Analyse du besoin", "text": "

    L'analyse du besoin est la seconde \u00e9tape dans la gestion d'un projet d'ing\u00e9nierie ou de g\u00e9nie logiciel. Elle consiste \u00e0 Elle consiste \u00e0 identifier les besoins des utilisateurs finaux qui ont \u00e9t\u00e9 identifi\u00e9s plus haut. Ceci permettant par la suite d'identifier les fonctionnalit\u00e9s du logiciel en cons\u00e9quence. Cette \u00e9tape permet de s'assurer que le logiciel r\u00e9pondra aux attentes des utilisateurs et qu'il sera adapt\u00e9 \u00e0 leur contexte d'utilisation.

    L'analyse du besoin utilise deux armes redoutables, la question \u00ab\u2009Comment\u2009\u00bb et la question \u00ab\u2009Pourquoi\u2009\u00bb. Hi\u00e9rarchiquement un \u00ab\u2009Comment\u2009\u00bb permet de rentrer dans le d\u00e9tail, on descend dans le niveau de granularit\u00e9. Un \u00ab\u2009Pourquoi\u2009\u00bb permet de remonter dans la hi\u00e9rarchie, de comprendre les motivations et les objectifs des utilisateurs. L'exercice doit pouvoir se r\u00e9p\u00e9ter jusqu'\u00e0 ce que la r\u00e9ponse sorte du cadre du projet. Cela permet de fixer le cadre g\u00e9n\u00e9ral du projet.

    1. Pourquoi l'utilisateur veut-il boire un caf\u00e9\u2009? Parce qu'il aime le caf\u00e9\u2009!
    2. Pourquoi il aime le caf\u00e9\u2009? Parce qu'il a besoin de se r\u00e9veiller le matin\u2009!
    3. Pourquoi il a besoin de se r\u00e9veiller le matin\u2009? Parce qu'il doit aller travailler\u2009!
    4. Pourquoi il doit aller travailler\u2009? Parce qu'il a besoin d'argent\u2009!
    5. Pourquoi il a besoin d'argent\u2009? Parce qu'il a besoin de payer son loyer\u2009!
    6. Pourquoi il a besoin de payer son loyer\u2009? Parce qu'il a besoin de survivre\u2009!

    Dans le cadre du syst\u00e8me auquel on s'int\u00e9resse, il n'est pas essentiel de r\u00e9pondre \u00e0 toutes les questions. Certaines sortent du cadre, mais cela permet de comprendre les motivations des utilisateurs et de s'assurer que le syst\u00e8me r\u00e9pondra \u00e0 leurs besoins. Cela permet de faire des parall\u00e8les avec d'autres acteurs du syst\u00e8me. Car un utilisateur r\u00e9veill\u00e9 est plus productif, et une entreprise qui a des employ\u00e9s productifs est plus rentable. L'exercice peut se r\u00e9p\u00e9ter pour chaque acteur du syst\u00e8me.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-fast", "title": "Analyse FAST", "text": "

    En pratique une bonne analyse est l'analyse FAST pour Function Analysis System Technique. Elle permet de d\u00e9composer les fonctions du syst\u00e8me en sous-fonctions, en sous-sous-fonctions, etc. jusqu'\u00e0 obtenir des fonctions \u00e9l\u00e9mentaires. Cela permet de d\u00e9finir les besoins fonctionnels du syst\u00e8me de mani\u00e8re exhaustive.

    Un diagramme FAST est bidimensionnel. Horizontalement sont repr\u00e9sent\u00e9s des fonctions du syst\u00e8mes. En se dirigeant \u00e0 droite, on r\u00e9pond \u00e0 la question \u00ab\u2009Comment\u2009\u00bb, en se dirigeant \u00e0 gauche on r\u00e9pond \u00e0 la question \u00ab\u2009Pourquoi\u2009\u00bb. Verticalement on peut repr\u00e9senter le \u00ab\u2009Quand\u2009\u00bb ou l'ordre de priorit\u00e9 des fonctions.

    Diagramme FAST

    Une fonction s'\u00e9crit sous la forme d'un verbe \u00e0 l'infinitif suivi d'un compl\u00e9ment d'objet. Par exemple \u00ab\u2009Pr\u00e9parer du caf\u00e9\u2009\u00bb ou \u00ab\u2009D\u00e9tecter une panne\u2009\u00bb.

    Cette analyse se r\u00e9alise en \u00e9quipe. La discussion doit \u00eatre ouverte et constructive sans contraintes hi\u00e9rarchiques. Chaque membre de l'\u00e9quipe doit pouvoir s'exprimer librement et proposer des id\u00e9es. L'objectif est d'\u00eatre exaustif et de ne rien laisser de c\u00f4t\u00e9.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-du-besoin_1", "title": "Analyse du besoin", "text": "

    Une fois l'analyse FAST r\u00e9alis\u00e9e, le besoin de chaque utilisateur peut \u00eatre identifi\u00e9. Un besoin d'exprime sous la forme \"J'ai besoin de fonction\". Les besoins des utilisateurs. Par soucis de redondance, le libell\u00e9 du besoin sera simplement la fonction.

    On exprime les besoins sous forme d'une table, o\u00f9 chaque ligne est num\u00e9rot\u00e9e. Le num\u00e9ro peut \u00eatre hi\u00e9rarchique mais il est pr\u00e9fix\u00e9 par un identifiant unique. Par exemple le besoin \u00ab\u2009N3.2\u2009\u00bb, avec N pour Need en anglais.

    Besoin de l'utilisateur ID Besoin N1.1 \u00catre r\u00e9veill\u00e9 au travail N1.2 \u00catre productif au travail N1.3 Partager un moment avec ses coll\u00e8gues N1.4 Appr\u00e9cier l'exp\u00e9rience

    \u00c0 chaque instant, chaque besoin peut \u00eatre questionn\u00e9. On doit pouvoir r\u00e9pondre \u00e0 aux deux questions \u00ab\u2009Comment\u2009\u00bb et \u00ab\u2009Pourquoi\u2009\u00bb. Si un besoin ne peut pas \u00eatre justifi\u00e9, il n'a pas sa place dans la liste.

    L'op\u00e9ration est r\u00e9p\u00e9t\u00e9e pour chaque acteur du syst\u00e8me en veillant \u00e0 ce que les besoins soient coh\u00e9rents entre eux.

    Il n'est pas toujours \u00e9vident de positionner le besoin. Par exemple la question \u00ab\u2009Pourquoi dois-je \u00eatre productif au travail\u2009\u00bb peut mener \u00e0 des questions philosophiques ou existentielle sur le sens de la vie, la place de la soci\u00e9t\u00e9 capitaliste, etc. Bien que ces questions soient pertinentes dans un cadre plus large, il est primordial d'identifier le cadre du projet et de ne pas s'en \u00e9carter.

    Un aspect important est \u00e9galement les contraintes ext\u00e9rieures. Souvent un client contacte une entreprise avec une id\u00e9e de projet en t\u00eate. Le client pense savoir ce qu'il veut, il a d\u00e9j\u00e0 r\u00e9alis\u00e9 des \u00e9tudes de march\u00e9, des \u00e9tude de design ou d'ergonomie. Malheuresement l'exp\u00e9rience montre que le client n'a pas toujours raison. Il n'est pas rare qu'il ait courtcircuit\u00e9 l'analyse fonctionnelle de son produit et mal identifi\u00e9 ses besoins. M\u00eame si ce n'est pas toujours possible, ni diplomatiquement \u00e9vident, il est essentiel de remettre en question les besoins du client pour les consolider.

    Une fois les besoins des utilisateurs identifi\u00e9s, l'\u00e9tape suivante est d'identifier les besoins du syst\u00e8me \u00e0 concevoir. Chaque besoin doit pouvoir \u00eatre reli\u00e9 \u00e0 un besoin utilisateur. Si on identifie \u00e0 posteriori un besoin du syst\u00e8me qui n'est pas reli\u00e9 \u00e0 un besoin utilisateur, il est probable que ce besoin du syst\u00e8me n'a pas sa place dans le projet ou alors que l'on ait oubli\u00e9 un besoin utilisateur ce qui peut conduire \u00e0 une nouvelle it\u00e9ration du processus d'analyse.

    Les besoins du syst\u00e8me pourraient \u00eatre\u2009:

    Besoin du syst\u00e8me ID Besoin Reli\u00e9 \u00e0 N6.1 Pr\u00e9parer du caf\u00e9 N1.1, N3.4 N6.2 Diagnostique automatique de panne N1.1, N2.2 N6.3 Offir une boisson de qualit\u00e9 N1.4 N6.4 Proposer des boissons vari\u00e9es N1.4 N6.5 Disposer d'une interface utilisateur intuitive N1.4 N6.6 Produire un caf\u00e9 rapidement N1.3, N5.4 N6.7 Produire du caf\u00e9 \u00e0 la cha\u00eene ...

    Il est important de noter qu'un syst\u00e8me m\u00eame complexe peut \u00eatre r\u00e9duit \u00e0 une liste de besoins \u00e9l\u00e9mentaires qui tiennent sur les doigts d'une ou de deux mains. \u00c0 ce stade de l'analyse il n'y a pas de place pour des d\u00e9tails techniques ou les grandeurs physiques.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-fonctionnelle", "title": "Analyse fonctionnelle", "text": "

    L'analyse fonctionnelle est la troisi\u00e8me \u00e9tape. Elle consiste \u00e0 d\u00e9finir les fonctions du syst\u00e8me \u00e0 concevoir pour r\u00e9pondre aux besoins du syst\u00e8me et donc des utilisateurs. Cette \u00e9tape permet de d\u00e9terminer les diff\u00e9rentes t\u00e2ches que le syst\u00e8me devra r\u00e9aliser pour satisfaire les besoins identifi\u00e9s. Une fonction est exprim\u00e9e sous forme d'une exigence fonctionnelle. Elle doit \u00eatre claire, pr\u00e9cise et non ambigu\u00eb. Elle comportera une dizaine de mots maximum et sera r\u00e9dig\u00e9e sous la forme d'un verbe \u00e0 l'infinitif suivi d'un compl\u00e9ment d'objet. Les verbes tels que d\u00e9finis par les Directives ISO/IEC Partie 2\u2009: Principes er r\u00e8gles de structure et de r\u00e9daction des documents ISO et IEC (ISO/IEC DIR 2) est une excellente base, \u00e0 la fois pour la r\u00e9daction du cahier des charges et pour la r\u00e9daction des exigences fonctionnelles. Le chapitre 7.2 d\u00e9fini comment une exigence doit \u00eatre r\u00e9dig\u00e9e. On notera\u2009:

    Exigences fonctionnelles Forme verbale Description Doit Obligation de disposer de la fonctionnalit\u00e9 Peut, Devrait Exigence optionnelle, facultative Ne doit pas Interdiction de disposer de la fonctionnalit\u00e9 Ne devrait pas Recommendation de ne pas disposer de la fonctionnalit\u00e9

    Les exigences fonctionnelles sont \u00e9galement num\u00e9rot\u00e9e par exemple avec le pr\u00e9fixe F pour Function. Une exigence fonctionnelle doit pouvoir \u00eatre reli\u00e9e par la question \u00ab\u2009Pourquoi\u2009\u00bb \u00e0 un besoin du syst\u00e8me, lequel par la m\u00eame question \u00eatre amen\u00e9 aux besoins des utilisateurs. Cela permet de s'assurer que chaque exigence fonctionnelle est justifi\u00e9e par un besoin utilisateur.

    Dans le cadre de notre exemple, les exigences fonctionnelles pourraient \u00eatre\u2009:

    Exigence fonctionnelle du syst\u00e8me ID Exigence fonctionnelle Reli\u00e9 \u00e0 F1.1 Doit pouvoir moudre des grains de caf\u00e9 N6.3 F1.2 Doit disposer d'une r\u00e9serve suffisante de grains N6.6 F1.3 Doit pouvoir \u00eatre reli\u00e9 \u00e0 une source d'eau N6.7

    Dans une analyse fonctionnelle on d\u00e9coupe g\u00e9n\u00e9ralement le syst\u00e8me en diff\u00e9rentes cat\u00e9gories\u2009:

    Fonctions de service

    Les fonctions de service sont les fonctions qui permettent au syst\u00e8me de fonctionner correctement. Elles sont souvent invisibles pour l'utilisateur final mais essentielles pour le bon fonctionnement du syst\u00e8me. Par exemple, la fonction \u00ab\u2009F1.3\u2009\u00bb qui permet au syst\u00e8me d'\u00eatre reli\u00e9 \u00e0 une source d'eau.

    Fonctions de contrainte

    Les fonctions de contrainte sont les fonctions qui imposent des contraintes sur le syst\u00e8me. Elles peuvent \u00eatre li\u00e9es \u00e0 des normes, des r\u00e9glementations ou des contraintes techniques, comme l'assurance qualit\u00e9 ou la s\u00e9curit\u00e9, ou de l'hygi\u00e8ne.

    Fonctions de support

    Les fonctions de support sont les fonctions qui permettent au syst\u00e8me de s'adapter \u00e0 son environnement. Elles peuvent \u00eatre li\u00e9es \u00e0 l'ergonomie, \u00e0 l'interface utilisateur, ou \u00e0 la maintenance du syst\u00e8me.

    Selon le projet et la complexit\u00e9 du syst\u00e8me, il peut \u00eatre n\u00e9cessaire de d\u00e9finir d'autres cat\u00e9gories de fonctions.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-organique", "title": "Analyse organique", "text": "

    L'analyse organique est la quatri\u00e8me \u00e9tape. Elle consiste \u00e0 d\u00e9finir les organes du syst\u00e8me \u00e0 concevoir pour r\u00e9aliser les fonctions identifi\u00e9es. Cette \u00e9tape permet de d\u00e9terminer les diff\u00e9rentes parties du syst\u00e8me qui devront \u00eatre con\u00e7ues pour r\u00e9aliser les fonctions identifi\u00e9es. Un organe est une partie du syst\u00e8me qui r\u00e9alise une ou plusieurs fonctions. Il peut \u00eatre un composant mat\u00e9riel, un logiciel, un sous-syst\u00e8me, ou une combinaison de ces \u00e9l\u00e9ments. Chaque organe doit \u00eatre clairement identifi\u00e9 et d\u00e9crit dans le cahier des charges.

    Chaque organe identifi\u00e9 peut lui-m\u00eame disposer de ses propres besoins, ses propres fonctions et ses propres exigences. Il est important de s'assurer que chaque organe est coh\u00e9rent avec les fonctions qu'il doit r\u00e9aliser et qu'il est en mesure de satisfaire les exigences qui lui sont associ\u00e9es. De surcro\u00eet les questions \u00ab\u2009Comment\u2009\u00bb et \u00ab\u2009Pourquoi\u2009\u00bb doivent permettre de suivre chaque organe dans sa hi\u00e9rarchie.

    "}, {"location": "course-c/45-software-design/teamwork/#specification-technique", "title": "Sp\u00e9cification technique", "text": "

    La cinqui\u00e8me \u00e9tape est l'identification des sp\u00e9cifications techniques attendues pour le syst\u00e8me, et pour chaque organe du syst\u00e8me. Cette \u00e9tape permet de d\u00e9terminer les caract\u00e9ristiques techniques du syst\u00e8me, les contraintes techniques \u00e0 respecter, et les normes \u00e0 suivre.

    Une sp\u00e9cification doit \u00eatre v\u00e9rifiable, c'est \u00e0 dire qu'\u00e0 la fin du projet, on doit pouvoir v\u00e9rifier que chaque \u00e9l\u00e9ment de la sp\u00e9cification a \u00e9t\u00e9 respect\u00e9 et est dans les normes acceptables.

    Table\u2009: Sp\u00e9cifications techniques

    ID Sp\u00e9cification technique Min Nom Max Unit\u00e9 Reli\u00e9 \u00e0 S1.1 Dur\u00e9e de mouture pour un caf\u00e9 20 30 s F... S1.2 Capacit\u00e9 de la r\u00e9serve 1 2 3 kg F... S1.3 Pression de l'eau 1 2 5 bar F...

    On aura g\u00e9n\u00e9ralement diff\u00e9rentes cat\u00e9gories de sp\u00e9cifications techniques\u2009:

    • Sp\u00e9cifications fonctionnelles
    • Sp\u00e9cifications de performance
    • Sp\u00e9cification \u00e9lectriques
    • Sp\u00e9cifications m\u00e9caniques
    • Sp\u00e9cifications logicielles

    \u00c0 partir de cette sp\u00e9cification pr\u00e9liminaire il est possible, enfin, de rentrer dans la technique et de d\u00e9marrer l'\u00e9tude de d\u00e9veloppement en proposant une solution technique. Cela peut \u00eatre un prototype, un plan, un sch\u00e9ma, un diagramme, etc. qui permettra de valider la faisabilit\u00e9 du projet.

    "}, {"location": "course-c/45-software-design/teamwork/#etude-des-solutions", "title": "\u00c9tude des solutions", "text": "

    Une fois le cahier des charges fonctionnel r\u00e9alis\u00e9, on conna\u00eet les utilisateurs, les besoins, les fonctions du syst\u00e8me, ses organes internes et les sp\u00e9cifications techniques attendues. Il est alors possible de proposer une solution technique pour le syst\u00e8me. Cette solution technique doit permettre de r\u00e9aliser les fonctions identifi\u00e9es, de satisfaire les besoins des utilisateurs, et de respecter les sp\u00e9cifications techniques d\u00e9finies.

    En termes logiciels cela peut \u00eatre la proposition d'une architecture logicielle, d'une base de donn\u00e9es, d'une interface utilisateur, des choix technologiques (langage de programmation, framework, etc.), des outils de d\u00e9veloppement, etc.

    En termes mat\u00e9riels cela peut \u00eatre la proposition d'un sch\u00e9ma \u00e9lectrique, d'un plan m\u00e9canique, d'un choix de composants, d'une architecture mat\u00e9rielle, etc.

    "}, {"location": "course-c/45-software-design/teamwork/#developpement", "title": "D\u00e9veloppement", "text": "

    Une fois la solution technique valid\u00e9e, il est possible de passer \u00e0 l'\u00e9tape de d\u00e9veloppement du syst\u00e8me. Cette \u00e9tape consiste \u00e0 r\u00e9aliser les organes du syst\u00e8me, \u00e0 les assembler, \u00e0 les tester, et \u00e0 les valider. C'est \u00e0 ce moment que l'on passe de la th\u00e9orie \u00e0 la pratique, de la sp\u00e9cification \u00e0 la r\u00e9alisation.

    "}, {"location": "course-c/45-software-design/teamwork/#modeles-de-developpement", "title": "Mod\u00e8les de d\u00e9veloppement", "text": "

    Il existe de nombreux mod\u00e8les de d\u00e9veloppement logiciel, chacun ayant ses avantages et ses inconv\u00e9nients. Voici les deux mod\u00e8les les plus couramment utilis\u00e9s\u2009:

    "}, {"location": "course-c/45-software-design/teamwork/#modele-en-cascade", "title": "Mod\u00e8le en cascade", "text": "

    Le mod\u00e8le en cascade est un mod\u00e8le lin\u00e9aire qui divise le projet en plusieurs phases distinctes (analyse, conception, d\u00e9veloppement, test, d\u00e9ploiement). Chaque phase doit \u00eatre compl\u00e9t\u00e9e avant de passer \u00e0 la suivante. Ce mod\u00e8le convient bien aux projets o\u00f9 les exigences sont claires et peu susceptibles de changer.

    Le mod\u00e8le en cascade suivant r\u00e9sume le cycle de d\u00e9veloppement d'un programme. Il s'agit d'un mod\u00e8le simple, mais qu'il faut garder \u00e0 l'esprit que ce soit pour le d\u00e9veloppement d'un produit logiciel que durant les travaux pratiques li\u00e9s \u00e0 ce cours.

    Mod\u00e8le en cascade

    "}, {"location": "course-c/45-software-design/teamwork/#modele-en-v", "title": "Mod\u00e8le en V", "text": "

    Le mod\u00e8le en V est une extension du mod\u00e8le en cascade qui met l'accent sur la validation et la v\u00e9rification \u00e0 chaque \u00e9tape du processus. Chaque phase de d\u00e9veloppement est associ\u00e9e \u00e0 une phase de test correspondante

    "}, {"location": "course-c/45-software-design/testing/", "title": "Qualit\u00e9 et Testabilit\u00e9", "text": "

    Bogue de l'an 2000

    Surveiller et assurer la qualit\u00e9 d'un code est primordial dans toute institution et quelques soit le produit. Dans l'industrie automobile par exemple, un bogue qui serait d\u00e9couvert plusieurs ann\u00e9es apr\u00e8s la commercialisation d'un mod\u00e8le d'automobile aurait des cons\u00e9quences catastrophiques.

    Voici quelques exemples c\u00e9l\u00e8bres de rat\u00e9s logiciels\u2009:

    La sonde martienne Mariner

    En 1962, un bogue logiciel a caus\u00e9 l'\u00e9chec de la mission avec la destruction de la fus\u00e9e apr\u00e8s qu'elle ait diverg\u00e9 de sa trajectoire. Une formule a mal \u00e9t\u00e9 retranscrite depuis le papier en code ex\u00e9cutable. Des tests suffisants auraient \u00e9vit\u00e9 cet \u00e9chec.

    Un pipeline sovi\u00e9tique de gaz

    En 1982, un bogue a \u00e9t\u00e9 introduit dans un ordinateur canadien achet\u00e9 pour le contr\u00f4le d'un pipeline de gaz transsib\u00e9rien. L'erreur est report\u00e9e comme la plus large explosion jamais enregistr\u00e9e d'origine non nucl\u00e9aire.

    Le g\u00e9n\u00e9rateur de nombre pseudo-al\u00e9atoire Kerberos

    Kerberos est un syst\u00e8me de s\u00e9curit\u00e9 utilis\u00e9 par Microsoft pour chiffrer les mots de passe des comptes Windows. Une erreur de code lors de la g\u00e9n\u00e9ration d'une graine al\u00e9atoire a permis de fa\u00e7on triviale pendant 8 ans de p\u00e9n\u00e9trer n'importe quel ordinateur utilisant une authentification Kerberos.

    La division enti\u00e8re sur Pentium

    En 1993, une erreur sur le silicium des processeurs Pentium, fleuron technologique de l'\u00e9poque, menait \u00e0 des erreurs de calcul en virgule flottante. Par exemple la division \\(4195835.0/3145727.0\\) menait \u00e0 \\(1.33374\\) au lieu de \\(1.33382\\)

    "}, {"location": "course-c/45-software-design/testing/#square", "title": "SQuaRE", "text": "

    La norme ISO/IEC 25010 (qui remplace ISO/IEC 9126-1) d\u00e9crit les caract\u00e9ristiques d\u00e9finissant la qualit\u00e9 d'un logiciel. L'acronyme SQuaRE (Software product Quality Requirements and Evaluation) d\u00e9finit le standard international. Voici quelques crit\u00e8res d'un code de qualit\u00e9\u2009:

    • Maintenabili\u00e9
    • Modifiabilit\u00e9
    • Testabilit\u00e9
    • Analisabilit\u00e9
    • Stabilit\u00e9
    • Changeabilit\u00e9
    • R\u00e9utilisabilit\u00e9
    • Compr\u00e9hensibilit\u00e9
    "}, {"location": "course-c/45-software-design/testing/#hacking", "title": "Hacking", "text": ""}, {"location": "course-c/45-software-design/testing/#buffer-overflow", "title": "Buffer overflow", "text": "

    L'attaque par buffer overflow est un type d'attaque typique permettant de modifier le comportement d'un programme en exploitant \u00ab\u2009le jardinage m\u00e9moire\u2009\u00bb. Lorsqu'un programme a mal \u00e9t\u00e9 con\u00e7u et que les tests de d\u00e9passement n'ont pas \u00e9t\u00e9 correctement impl\u00e9ment\u00e9s, il est souvent possible d'acc\u00e9der \u00e0 des comportements de programmes impr\u00e9vus.

    Consid\u00e9rons le programme suivant\u2009:

    #include <stdio.h>\n#include <string.h>\n\nint check_password(char *str) {\n    if(strcmp(str, \"starwars\"))\n    {\n        printf (\"Wrong Password \\n\");\n        return 0;\n    }\n\n    printf (\"Correct Password \\n\");\n    return 1;\n}\n\nint main(void)\n{\n    char buffer[15];\n    int is_authorized = 0;\n\n    printf(\"Password: \");\n    gets(buffer);\n    is_authorized = check_password(buffer);\n\n    if(is_authorized)\n    {\n        printf (\"Now, you have the root access! \\n\");\n    }\n}\n

    \u00c0 priori, c'est un programme tout \u00e0 fait correct. Si l'utilisateur entre le bon mot de passe, il se voit octroyer des privil\u00e8ges administrateurs. Testons ce programme\u2009:

    $ gcc u.c -fno-stack-protector\n$ ./a.out\nPassword: starwars\nCorrect Password\nNow, you have the root access!\n

    Tr\u00e8s bien, maintenant testons avec un mauvais mot de passe\u2009:

    $ ./a.out\nPassword: startrek\nWrong Password\n

    Et maintenant, essayons avec un mot de passe magique...

    "}, {"location": "course-c/45-software-design/testing/#tests-unitaires", "title": "Tests unitaires", "text": "

    Un test unitaire est une proc\u00e9dure permettant de v\u00e9rifier le bon fonctionnement d'une unit\u00e9 de code. Une unit\u00e9 de code est la plus petite partie d'un programme qui peut \u00eatre test\u00e9e de mani\u00e8re isol\u00e9e. En C, une unit\u00e9 de code est souvent une fonction.

    Lorsque l'on travaille selon la philosophie du TDD (Test Driven Development), on commence par \u00e9crire les tests unitaires avant d'\u00e9crire le code. Voici par exemple un test unitaire pour la r\u00e9solution d'une \u00e9quation du second degr\u00e9\u2009:

    #include <stdio.h>\n\nbool quadratic_solver(double a, double b, double c, double *x1, double *x2)\n{\n    double delta = b * b - 4 * a * c;\n\n    if (delta < 0)\n        return false;\n\n    *x1 = (-b + sqrt(delta)) / (2 * a);\n    *x2 = (-b - sqrt(delta)) / (2 * a);\n\n    return true;\n}\n\nvoid test_quadratic_solver(void)\n{\n    double x1, x2;\n\n    // Cas 1 : Deux racines r\u00e9elles et distinctes\n    assert(quadratic_solver(1, -3, 2, &x1, &x2) == true);\n    assert(fabs(x1 - 2.0) < 1e-6);\n    assert(fabs(x2 - 1.0) < 1e-6);\n\n    // Cas 2 : Deux racines r\u00e9elles et \u00e9gales\n    assert(quadratic_solver(1, -2, 1, &x1, &x2) == true);\n    assert(fabs(x1 - 1.0) < 1e-6);\n    assert(fabs(x2 - 1.0) < 1e-6);\n\n    // Cas 3 : Racines complexes (pas de solution r\u00e9elle)\n    assert(quadratic_solver(1, 0, 1, &x1, &x2) == false);\n\n    // Cas 4 : a = 0 (ce n'est pas une \u00e9quation quadratique)\n    assert(quadratic_solver(0, 2, 1, &x1, &x2) == false);\n\n    // Cas 5 : Equation triviale (b = 0 et c = 0)\n    assert(quadratic_solver(1, 0, 0, &x1, &x2) == true);\n    assert(fabs(x1 - 0.0) < 1e-6);\n    assert(fabs(x2 - 0.0) < 1e-6);\n}\n

    En pratique on aimerait lancer les tests unitaires automatiquement \u00e0 chaque modification du code source. Pour cela, on utilisera des outils d'int\u00e9gration continue comme Travis CI ou GitHub Actions.

    Souvent on utilise des frameworks de tests unitaires pour automatiser les tests. En C, on peut citer Unity.

    Reprenons l'exemple pr\u00e9c\u00e9dent avec Unity\u2009:

    #include \"unity.h\"\n#include \"quadratic_solver.h\"\n\n#include <stdio.h>\n\nvoid setUp(void) {}\n\nvoid tearDown(void) {}\n\nvoid test_quadratic_solver(void)\n{\n    double x1, x2;\n\n    // Cas 1 : Deux racines r\u00e9elles et distinctes\n    TEST_ASSERT_TRUE(quadratic_solver(1, -3, 2, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 2.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 1.0);\n\n    // Cas 2 : Deux racines r\u00e9elles et \u00e9gales\n    TEST_ASSERT_TRUE(quadratic_solver(1, -2, 1, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 1.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 1.0);\n\n    // Cas 3 : Racines complexes (pas de solution r\u00e9elle)\n    TEST_ASSERT_FALSE(quadratic_solver(1, 0, 1, &x1, &x2));\n\n    // Cas 4 : a = 0 (ce n'est pas une \u00e9quation quadratique)\n    TEST_ASSERT_FALSE(quadratic_solver(0, 2, 1, &x1, &x2));\n\n    // Cas 5 : Equation triviale (b = 0 et c = 0)\n    TEST_ASSERT_TRUE(quadratic_solver(1, 0, 0, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 0.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 0.0);\n}\n
    "}, {"location": "course-c/45-software-design/testing/#tests-fonctionnels", "title": "Tests fonctionnels", "text": "

    Les tests fonctionnels permettent de v\u00e9rifier le bon fonctionnement d'une application dans son ensemble. Ils sont souvent utilis\u00e9s pour tester des applications web ou des applications mobiles. Les tests fonctionnels sont souvent automatis\u00e9s et peuvent \u00eatre lanc\u00e9s \u00e0 chaque modification du code source.

    Dans le cadre de ce cours on utilise le framework Baygon pour r\u00e9aliser des tests fonctionnels.

    Pour l'utiliser il suffit de cr\u00e9er un fichier tests.yml \u00e0 la racine de votre projet\u2009:

    version: 1\ntests:\n  - name: Arguments check\n    tests:\n      - name: No errors if two arguments\n        args: [1, 2]\n        exit: 0\n      - name: Error if less than two arguments\n        args: [1]\n        exit: 1\n  - name: Stdout is the sum of arguments\n    args: [1, 2]\n    stdout: []\n  - name: Version on stderr\n    args: ['--version']\n    stderr:\n      - regex: '\\b\\d\\.\\d\\.\\d\\b'\n      - contains: 'Version'\n

    Il faut ensuite installer Baygon\u2009:

    $ pip install baygon\n

    Et lancer les tests\u2009:

    $ baygon program\n
    ", "tags": ["tests.yml"]}, {"location": "course-c/47-gui/", "title": "Interfaces graphiques", "text": "

    Sans doute la premi\u00e8re frustration des \u00e9tudiants est de ne pas r\u00e9aliser d'interface graphique d\u00e8s les premiers cours d'informatique. C'est pourtant un domaine indispensable mais ce n'est pas le sujet le plus simple \u00e0 aborder. La programmation d'interfaces graphiques est un domaine complexe qui n\u00e9cessite avant tout une bonne connaissance de la programmation, des patrons de conceptions, des biblioth\u00e8ques graphiques et des syst\u00e8mes d'exploitation.

    Les interfaces graphiques sont le plus souvent r\u00e9alis\u00e9es avec des biblioth\u00e8ques tierces qui permettent de cr\u00e9er des fen\u00eatres, des boutons, des champs de texte, etc. Ces biblioth\u00e8ques sont g\u00e9n\u00e9ralement sp\u00e9cifiques \u00e0 un syst\u00e8me d'exploitation ou \u00e0 un langage de programmation et ne sont pas toujours portables. C'est \u00e0 dire qu'un programme \u00e9crit avec une biblioth\u00e8que graphique sp\u00e9cifique \u00e0 Windows ne pourra pas \u00eatre ex\u00e9cut\u00e9 sur un syst\u00e8me d'exploitation Linux ou MacOS sans modification.

    Les d\u00e9veloppeurs d'interfaces graphiques pr\u00e9f\u00e8reront souvent utiliser des langages plus adapt\u00e9s \u00e0 la cr\u00e9ation d'interfaces graphiques, comme Java, C#, ou m\u00eame de la programmation web. Cependant, il est tout \u00e0 fait possible de cr\u00e9er des interfaces graphiques en C, mais cela n\u00e9cessite un peu plus de travail.

    Maintenant que nous avons vu comment g\u00e9rer un projet complexe ainsi que ses d\u00e9pendances, nous allons aborder la programmation d'interfaces graphiques en C.

    "}, {"location": "course-c/47-gui/gtk/", "title": "GTK", "text": "

    GTK (Gimp ToolKit) est une biblioth\u00e8que logicielle libre qui permet de cr\u00e9er des interfaces graphiques. Elle est utilis\u00e9e par de nombreux logiciels, dont le bureau GNOME, GIMP, Inkscape, etc. C'est la biblioth\u00e8que graphique la plus utilis\u00e9e sous Linux.

    Architecture GTK

    Elle repose grandement sur GLib, la biblioth\u00e8que de base de GNOME, qui fournit des types de donn\u00e9es, des macros, des structures et des fonctions de base pour la programmation en C. Avec Glib on peut par exemple simplifier consid\u00e9rablement le d\u00e9veloppement en C, en fournissant des fonctions pour la gestion de la m\u00e9moire, des cha\u00eenes de caract\u00e8res, des listes, des tableaux, des arbres, des files d'attente, des piles, des tables de hachage etc. Voici un exemple simple d'utilisation de GLib\u2009:

    #include <glib.h>\n\nint main(int argc, char **argv) {\n    // Cr\u00e9er et parcourir une liste\n    GList *list = NULL;\n    list = g_list_append(list, \"Hello\");\n    list = g_list_append(list, \"World\");\n    g_list_foreach(list, (GFunc)g_print, NULL);\n    g_list_free(list);\n\n    // G\u00e9n\u00e9rer un nombre al\u00e9atoire\n    g_random_int();\n\n    // Simplifier la gestion des cha\u00eenes de caract\u00e8res\n    GString *string = g_string_new(\"Hello\");\n    g_string_append(string, \" World\");\n\n    // etc.\n}\n

    GDK (Gimp Drawing Kit) est une biblioth\u00e8que qui fournit des fonctions pour la gestion des fen\u00eatres, des \u00e9v\u00e9nements, des images, des polices de caract\u00e8res, etc. Elle est utilis\u00e9e par GTK pour dessiner les \u00e9l\u00e9ments graphiques.

    GSK (Gimp Scene Kit) est une biblioth\u00e8que qui fournit des fonctions pour la gestion des animations, des transitions, des effets graphiques, etc. Elle est utilis\u00e9e par GTK pour animer les \u00e9l\u00e9ments graphiques.

    Enfin PANGO est une biblioth\u00e8que qui fournit des fonctions pour la gestion des polices de caract\u00e8res, des textes, des langues, etc. Elle est utilis\u00e9e par GTK pour afficher du texte.

    Ces 4 biblioth\u00e8ques sont les composants principaux de GTK et elles se reposent sur diff\u00e9rentes librairies comme CAIRO pour le rendu graphique.

    Cairo est une biblioth\u00e8que de dessin 2D qui fournit des fonctions pour dessiner des lignes, des courbes, des formes, des images, des textes, etc. Elle est utilis\u00e9e par GDK pour dessiner les \u00e9l\u00e9ments graphiques.

    OpenGL et Wayland sont utilis\u00e9es pour le rendu graphique et l'int\u00e9gration avec le gestionnaire de fen\u00eatres.

    "}, {"location": "course-c/47-gui/gtk/#glade", "title": "Glade", "text": "

    Glade est un logiciel graphique qui permet de cr\u00e9er des interfaces graphiques pour GTK de mani\u00e8re interactive. Il permet de dessiner les fen\u00eatres, les boutons, les champs de texte, etc. et de g\u00e9n\u00e9rer le code source correspondant.

    Il est tr\u00e8s utile pour les d\u00e9butants qui ne connaissent pas encore bien GTK, car il permet de voir en temps r\u00e9el le rendu de l'interface graphique et de g\u00e9n\u00e9rer le code source correspondant. Il est \u00e9galement tr\u00e8s utile pour les d\u00e9veloppeurs exp\u00e9riment\u00e9s, car il permet de gagner du temps en \u00e9vitant de devoir \u00e9crire le code source \u00e0 la main.

    "}, {"location": "course-c/47-gui/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/47-gui/introduction/#quest-ce-quune-interface-graphique", "title": "Qu'est-ce qu'une interface graphique\u2009?", "text": "

    L'interface graphique, couramment d\u00e9sign\u00e9e sous l'acronyme GUI (Graphical User Interface), est la couche visuelle par laquelle un utilisateur interagit avec un syst\u00e8me informatique. Contrairement aux interfaces en ligne de commande (CLI Client Line Interface), o\u00f9 l'utilisateur doit saisir des commandes textuelles, une GUI propose une interaction plus intuitive, fond\u00e9e sur des \u00e9l\u00e9ments visuels tels que des fen\u00eatres, des ic\u00f4nes, des menus et des boutons.

    \u00c0 la naissance de l'informatique, les interactions \u00e9taient principalement r\u00e9serv\u00e9es \u00e0 des sp\u00e9cialistes capables de comprendre et de manipuler des commandes complexes. L'av\u00e8nement des interfaces graphiques, au d\u00e9but des ann\u00e9es 1980, a marqu\u00e9 un tournant fondamental dans l'accessibilit\u00e9 des syst\u00e8mes informatiques, ouvrant la voie \u00e0 une d\u00e9mocratisation rapide de ces technologies.

    Le Smaky, d\u00e9velopp\u00e9 par le professeur Jean-Daniel Nicoud au LAMI \u00e0 l'EPFL, est un des premiers ordinateurs grand public \u00e0 proposer une interface graphique. Il a \u00e9t\u00e9 commercialis\u00e9 d\u00e8s 1978. C'est le premier ordinateur personnel \u00e0 disposer d'une souris en standard laquelle a \u00e9t\u00e9 d\u00e9velopp\u00e9e en 1962 par Douglas Engelbart. Nicoud ayant \u00e9t\u00e9 s\u00e9duit par l'id\u00e9e l'a perfectionn\u00e9e au sein de son laboratoire. C'est Andr\u00e9 Guignard aussi l'origine du robot Khepera qui d\u00e9veloppa pour Nicoud la premi\u00e8re souris optom\u00e9canique fabriqu\u00e9e par Depraz et commercialis\u00e9e par Logitech.

    Souris Depraz

    Depuis lors, les interfaces graphiques ont non seulement rendu les ordinateurs plus accessibles \u00e0 un public non technique, mais elles ont \u00e9galement transform\u00e9 l'exp\u00e9rience utilisateur. L'interface graphique permet de manipuler les objets num\u00e9riques comme s'ils \u00e9taient des objets physiques. Cette analogie, que l'on appelle m\u00e9taphore d'interface, est l'un des piliers de la conception des GUI. Par exemple, la corbeille, o\u00f9 l'on d\u00e9pose les fichiers pour les supprimer, est une m\u00e9taphore simple, mais efficace qui transforme un concept abstrait en une action que tout utilisateur peut comprendre.

    En outre, les GUI facilitent les interactions complexes en cachant la complexit\u00e9 du code sous une couche de simplicit\u00e9 visuelle. Chaque clic sur un bouton, chaque interaction avec un menu d\u00e9clenche des op\u00e9rations en arri\u00e8re-plan, rendant l'utilisation du syst\u00e8me plus fluide et plus intuitive.

    Scarab\u00e9 Julodimorpha bakewelli

    Donald Hoffmann dans son excellent TED Talk intitul\u00e9 Do we see reality as it is\u2009? aborde la question de la perception de la r\u00e9alit\u00e9. Il explique que notre cerveau ne per\u00e7oit pas la r\u00e9alit\u00e9 telle qu'elle est, mais qu'il la mod\u00e9lise pour nous permettre de survivre. Les interfaces graphiques sont une forme de mod\u00e9lisation de la r\u00e9alit\u00e9 num\u00e9rique qui nous permet de manipuler des objets virtuels de mani\u00e8re intuitive.

    En effet, lorsque l'on observe nos interfaces modernes, nous y voyons des boutons, des curseurs, des fen\u00eatres. Est-ce la r\u00e9alit\u00e9 de notre perception\u2009? Si l'on prend une loupe et que l'on regarde de plus pr\u00e8s, on se rend compte que ces \u00e9l\u00e9ments ne sont que des pixels color\u00e9s sur un \u00e9cran\u2009: c'est la r\u00e9alit\u00e9 de l'interface graphique, mais qui, \u00e0 cette \u00e9chelle de perception ne nous permet pas de comprendre ce que nous manipulons.

    Le scarab\u00e9e Julodimorpha bakewelli est un exemple de la complexit\u00e9 de la r\u00e9alit\u00e9. Il est massif, brillant et brun. La femelle est incapable de voler. Le m\u00e2le vole, \u00e0 sa recherche. Il convoite une femelle s\u00e9duisante. Homo sapiens, dans un but similaire, se r\u00e9unit en groupe dans l'Outback australien accompagn\u00e9 de boissons ferment\u00e9es qui une fois vite sont laiss\u00e9es \u00e0 l'abandon. Or, il se trouve que ces bouteilles en verre sont massives, brillantes et brunes\u2009: une femelle s\u00e9duisante. Les m\u00e2les scarab\u00e9es se ruent sur les bouteilles pour tenter de s'accoupler comme illustr\u00e9 sur la figure ci-dessus. Ils perdent tout int\u00e9r\u00eat pour les femelles r\u00e9elles et l'esp\u00e8ce a failli dispara\u00eetre. L'Australie a d\u00fb modifier ses bouteilles pour sauver ses scarab\u00e9es.

    Pourrait-on consid\u00e9rer une seconde que ces scarab\u00e9es per\u00e7oivent la r\u00e9alit\u00e9 telle qu'elle est\u2009? Du point de vue du scarab\u00e9e il n'y a aucun doute, mais de notre perception la situation nous apparait comme risible et d\u00e9nu\u00e9e de sens. L'\u00e9volution leur a fourni un raccourci\u2009; une femelle, c'est tout ce qui est massif, brillant et brun, et plus c'est gros, mieux c'est. Le point de vue peut naturellement \u00eatre g\u00e9n\u00e9ralis\u00e9 et des \u00eatres venus d'ailleurs qui nous observeraient nous humains \u00e0 cliquer sur des boutons pour faire clignoter de petites lumi\u00e8res color\u00e9es agenc\u00e9es en grille pourraient se demander si nous percevons la r\u00e9alit\u00e9 telle qu'elle est. Vous voyez que la question est bien plus philosophique et profonde.

    Toutes ces consid\u00e9rations doivent nous amener \u00e0 nous interroger sur la nature m\u00eame de l'utilit\u00e9 d'une interface graphique. Dois \u00eatre elle s\u00e9duisante et donner l'illusion qu'elle se substitue au r\u00e9el ou doit \u00eatre fonctionnelle et servir un besoin pr\u00e9cis d'\u00eatre plus performante qu'une interface plus rudimentaire dans l'interaction entre l'homme et la machine\u2009?

    La technologie et l'av\u00e8nement de la souris ont craft\u00e9 une nouvelle mani\u00e8re de penser l'interaction entre l'homme et la machine. L'histoire des interfaces graphiques remonte \u00e0 1963, avec la conception du \u00ab\u2009Sketchpad\u2009\u00bb par Ivan Sutherland, un des premiers syst\u00e8mes \u00e0 exploiter des concepts graphiques.

    sketchpad

    Cependant, c'est Xerox qui, dans les ann\u00e9es 1970, avec son Xerox Alto, pose les bases des GUI modernes. La v\u00e9ritable r\u00e9volution survient avec l'introduction des interfaces graphiques par Apple en 1984, via le Macintosh, suivi de pr\u00e8s par Microsoft avec Windows. Ces syst\u00e8mes d'exploitation grand public int\u00e8grent les concepts de fen\u00eatres, d'ic\u00f4nes et de menus, offrant une alternative conviviale \u00e0 l'interface en ligne de commande.

    Xerox Alto

    Depuis, les interfaces graphiques n'ont cess\u00e9 d'\u00e9voluer, s'adaptant aux nouvelles technologies (touches tactiles, interfaces vocales) et aux besoins des utilisateurs. Aujourd'hui, les GUI se retrouvent non seulement sur les ordinateurs de bureau, mais aussi sur les appareils mobiles, les objets connect\u00e9s, et m\u00eame les syst\u00e8mes embarqu\u00e9s.

    Que seront-elles demain\u2009? La r\u00e9alit\u00e9 augment\u00e9e, l'intelligence artificielle, le neurofeedback avec des interfaces cerveau-ordinateur qui commencent \u00e0 \u00e9merger notamment avec le Neuralink d'Elon Musk.

    "}, {"location": "course-c/47-gui/introduction/#la-technologie-actuelle", "title": "La technologie actuelle", "text": "

    Nous voyons que d\u00e9finir une interface graphique est un probl\u00e8me bien plus vaste qui conteste notre perception du r\u00e9el, les objectifs pragmatiques d'un probl\u00e8me d'ing\u00e9nierie, les innovations technologiques et les perspectives d'avenir. N\u00e9anmoins s'il est important de comprendre ces enjeux, il est tout aussi important de savoir comment construire une interface graphique avec les technologies actuelles.

    "}, {"location": "course-c/47-gui/introduction/#types-dinterfaces-graphiques", "title": "Types d'interfaces graphiques", "text": "

    Le d\u00e9fi technique de la construction d'une interface graphique outre les crit\u00e8res d'efficacit\u00e9 et d'ergonomie est la portabilit\u00e9 de l'interface. Tout au long de se cours, nous abordons les probl\u00e8mes de compatibilit\u00e9 entre les syst\u00e8mes d'exploitation et les diff\u00e9rentes technologies d'interfaces. On peut citer aujourd'hui plusieurs cat\u00e9gories fondamentales d'interfaces graphiques\u2009:

    "}, {"location": "course-c/47-gui/introduction/#interfaces-specialisees-dequipements", "title": "Interfaces sp\u00e9cialis\u00e9es d'\u00e9quipements", "text": "

    Les interfaces graphiques de commandes d'\u00e9quipements professionnels se substituent aujourd'hui aux tableaux de bord et \u00e0 leurs boutons physiques par des \u00e9crans tactiles et des interfaces graphiques configurables et \u00e9volutives. Ces interfaces permettent de contr\u00f4ler des machines complexes, de visualiser des donn\u00e9es en temps r\u00e9el et de surveiller des processus industriels. On observe sur les deux figures ci-dessous une table de mixage traditionnelle et une table de mixage num\u00e9rique ou la transition technologique est clairement visible.

    Table de mixage traditionnelle

    Table de mixage num\u00e9rique Lawo

    Ces interfaces propri\u00e9taires n'ont pas vocation \u00e0 \u00eatre portable, elles sont con\u00e7ues pour un \u00e9quipement sp\u00e9cifique et sont souvent d\u00e9velopp\u00e9es dans des langages de haut niveau comme le C++ le Java ou le C#. Les composants graphiques sont souvent d\u00e9velopp\u00e9s sur mesure pour r\u00e9pondre aux besoins sp\u00e9cifiques de l'\u00e9quipement.

    "}, {"location": "course-c/47-gui/introduction/#interfaces-portables-de-commande", "title": "Interfaces portables de commande", "text": "

    De la m\u00eame mani\u00e8re, dans la commande de robots industriels, les t\u00e9l\u00e9commandes physiques ont \u00e9t\u00e9 remplac\u00e9es par des interfaces graphiques sur tablettes. Les besoins sont plus g\u00e9n\u00e9riques et les interfaces sont souvent d\u00e9velopp\u00e9es en utilisant des technologies web (HTML5, CSS, JavaScript) ou des technologies multiplateformes comme Flutter ou React Native.

    Interface d'un robot Kuka

    "}, {"location": "course-c/47-gui/introduction/#interfaces-de-controle-de-systemes-embarques", "title": "Interfaces de contr\u00f4le de syst\u00e8mes embarqu\u00e9s", "text": "

    L\u00e0 o\u00f9 le co\u00fbt de d\u00e9veloppement et la puissance de calcul limit\u00e9e des architectures embarqu\u00e9e est un enjeu, les interfaces graphiques sont r\u00e9duites \u00e0 un simple \u00e9cran tactile. La soci\u00e9t\u00e9 Decent Espresso a d\u00e9velopp\u00e9 par exemple une machine \u00e0 caf\u00e9 avec une interface graphique brisant les codes des machines \u00e0 caf\u00e9 traditionnelles ou de gros boutons et cadrans physiques \u00e9taient gage de qualit\u00e9 et de prestige.

    Interface d'une machine \u00e0 caf\u00e9 Decent

    Le prestige \u00e9tant une part importante du besoin de l'\u00e9quipement, l'interface graphique est un \u00e9l\u00e9ment cl\u00e9 qui doit \u00eatre soign\u00e9 tant au niveau de l'ergonomie que de l'esth\u00e9tisme et de la fluidit\u00e9 de l'interaction. Ces interfaces sont majoritairement d\u00e9velopp\u00e9es en utilisant des technologies web ou des technologies multiplateformes (QT, HTML, JavaScript).

    "}, {"location": "course-c/47-gui/introduction/#applications-embarquees", "title": "Applications embarqu\u00e9es", "text": "

    Embedded Wizard

    "}, {"location": "course-c/47-gui/introduction/#applications-mobiles", "title": "Applications mobiles", "text": "

    Interface de commande du robot Spot de Boston Dynamics

    "}, {"location": "course-c/47-gui/introduction/#applications-pc-multiplateformes", "title": "Applications PC multiplateformes", "text": "

    AutoCAD 2025

    "}, {"location": "course-c/47-gui/introduction/#ergonomie", "title": "Ergonomie", "text": "

    L'ergonomie des interfaces graphiques est au c\u0153ur de la conception des logiciels modernes. Il ne s'agit pas simplement d'esth\u00e9tique, mais bien de la mani\u00e8re dont une interface facilite ou entrave l'utilisation par l'utilisateur. Une bonne interface doit \u00eatre intuitive, efficace et accessible, permettant \u00e0 l'utilisateur d'atteindre ses objectifs rapidement et sans frustration.

    L'ergonomie, dans le cadre des interfaces graphiques, vise \u00e0 optimiser l'interaction entre l'utilisateur et le logiciel. Une interface bien pens\u00e9e doit respecter certains principes cl\u00e9s\u2009:

    Clart\u00e9

    Les \u00e9l\u00e9ments visuels doivent \u00eatre organis\u00e9s de mani\u00e8re claire et lisible. Chaque bouton, chaque menu, doit \u00eatre facilement identifiable et ses fonctions, explicites.

    Coh\u00e9rence

    Les interactions et les comportements des composants doivent \u00eatre uniformes \u00e0 travers l'interface. Par exemple, si un bouton annule une action dans une partie de l'application, il doit le faire dans toutes les autres. Un manque de coh\u00e9rence tr\u00e8s connu est les param\u00e8tres de Windows. Sous Windows 11 on retrouve parfois en cliquant sur \u00ab\u2009Param\u00e8tres avanc\u00e9s\u2009\u00bb une fen\u00eatre de param\u00e8tres tr\u00e8s diff\u00e9rents h\u00e9rit\u00e9e des anciennes versions de Windows.

    Simplicit\u00e9

    L'interface doit \u00e9viter de surcharger l'utilisateur avec trop d'options ou d'informations. Un design \u00e9pur\u00e9 permet \u00e0 l'utilisateur de se concentrer sur les t\u00e2ches essentielles. Apple est un exemple de cette philosophie avec ses interfaces minimalistes.

    Accessibilit\u00e9

    L'interface doit \u00eatre accessible \u00e0 tous les utilisateurs, y compris ceux ayant des limitations physiques ou cognitives. Cela inclut l'utilisation de tailles de police ad\u00e9quates, de couleurs contrast\u00e9es et la possibilit\u00e9 de naviguer \u00e0 l'aide du clavier ou de dispositifs d'assistance.

    "}, {"location": "course-c/47-gui/introduction/#le-c-et-les-interfaces-graphiques", "title": "Le C et les interfaces graphiques", "text": "

    Je dois l'admettre, le C n'est pas le langage le plus adapt\u00e9 pour le d\u00e9veloppement d'interfaces graphiques. Les GUI modernes sont souvent construits en utilisant des langages de haut niveau comme C++, Java, C#, Python ou JavaScript, qui offrent des biblioth\u00e8ques et des frameworks d\u00e9di\u00e9s \u00e0 la cr\u00e9ation d'interfaces graphiques bas\u00e9e sur des paradigmes objet, asynchrone et \u00e9v\u00e9nementiel.

    Je n'ai pas beaucoup d'exemples d'applications graphiques compl\u00e8tes en C \u00e0 vous montrer mise \u00e0 part peut \u00eatre Gimp, un logiciel de retouche d'image open source qui utilise GTK (GIMP Toolkit). En outre, la plupart des biblioth\u00e8ques graphiques sont \u00e9crites en C++. L'offre pour le C est donc limit\u00e9e, mais elle n'est pas inexistante.

    La biblioth\u00e8que la plus connue pour le d\u00e9veloppement d'interfaces graphiques en C est GTK, qui est utilis\u00e9e par de nombreuses applications open source, telles que GIMP, Inkscape, ou encore GNOME. GTK est une biblioth\u00e8que multiplateforme qui offre des composants graphiques, des outils de dessin, et une gestion des \u00e9v\u00e9nements. Elle est \u00e9crite en C, mais elle propose des bindings pour de nombreux autres langages, comme Python ou JavaScript. GTK est un excellent choix si vous souhaitez d\u00e9velopper des applications graphiques en C, de plus la biblioth\u00e8que est portable et bien document\u00e9e.

    Une autre biblioth\u00e8que populaire est SDL, Simple DirectMedia Layer, qui est une biblioth\u00e8que multim\u00e9dia utilis\u00e9e pour le d\u00e9veloppement de jeux vid\u00e9o, mais qui peut \u00e9galement \u00eatre utilis\u00e9e pour cr\u00e9er des interfaces graphiques. SDL est \u00e9crite en C, mais elle propose des bindings pour de nombreux autres langages, comme C++, Python, ou Lua. SDL est une biblioth\u00e8que l\u00e9g\u00e8re et portable, qui offre des fonctionnalit\u00e9s de base pour la cr\u00e9ation d'interfaces graphiques.

    Un autre choix un peu moins populaire, mais qui m\u00e9rite d'\u00eatre cit\u00e9 pour sa longue histoire est la biblioth\u00e8que Allero. Elle a \u00e9t\u00e9 originellement cr\u00e9\u00e9e pour les ordinateurs Atari dans les ann\u00e9es 90. Allero est une biblioth\u00e8que multim\u00e9dia qui offre des fonctionnalit\u00e9s de dessin 2D, de son, de gestion d'entr\u00e9es utilisateur.

    "}, {"location": "course-c/47-gui/opengl/", "title": "OpenGL", "text": ""}, {"location": "course-c/47-gui/opengl/#versions", "title": "Versions", "text": "

    On distingues plusieurs versions d'OpenGL\u2009:

    OpenGL 1.0\u2009: Premi\u00e8re version d'OpenGL sortie en 1992. OpenGL 2.0\u2009: Version sortie en 2004 qui introduit les shaders. OpenGL 3.0\u2009: Version sortie en 2008 qui introduit les shaders de g\u00e9om\u00e9trie. OpenGL 4.0\u2009: Version sortie en 2010 qui introduit les shaders de tessellation. OpenGL 4.3\u2009: Version sortie en 2012 qui introduit les shaders de calcul. OpenGL 4.6\u2009: Version sortie en 2017 qui introduit les shaders de t\u00e2ches.

    OpenGL ES 2.0\u2009: Version d'OpenGL pour les syst\u00e8mes embarqu\u00e9s. Vulkan\u2009: API graphique bas niveau qui succ\u00e8de \u00e0 OpenGL.

    Wayland\u2009: Protocole de communication entre le serveur et les clients. X11\u2009: Protocole de communication entre le serveur et les clients.

    "}, {"location": "course-c/47-gui/opengl/#fonctionnement", "title": "Fonctionnement", "text": ""}, {"location": "course-c/47-gui/opengl/#vertex", "title": "Vertex", "text": "

    La premi\u00e8re \u00e9tape du pipeline graphique est la transformation des coordonn\u00e9es des sommets des objets g\u00e9om\u00e9triques en coordonn\u00e9es de l'\u00e9cran. Cette \u00e9tape est appel\u00e9e vertex processing et consiste \u00e0 appliquer des transformations g\u00e9om\u00e9triques (comme la translation, la rotation, l'\u00e9chelle) aux sommets des objets. Les coordonn\u00e9es des sommets sont g\u00e9n\u00e9ralement d\u00e9finies dans un espace 3D, mais elles doivent \u00eatre transform\u00e9es en coordonn\u00e9es 2D pour l'affichage \u00e0 l'\u00e9cran.

    Imaginons que l'on souhaite dessiner une pyramide en 3D. Chaque sommet de la pyramide est d\u00e9fini par ses coordonn\u00e9es (x, y, z) dans l'espace 3D. Les donn\u00e9es sont \u00e9crites dans un buffer de sommets comme dans l'exemple suivant\u2009:

    struct Vertex { float x, y, z; }[] = {\n   {0.0f, 1.0f, 0.0f},   // Sommet 0\n   {-1.0f, -1.0f, 1.0f}, // Sommet 1\n   {1.0f, -1.0f, 1.0f},  // Sommet 2\n   {1.0f, -1.0f, -1.0f}, // Sommet 3\n   {-1.0f, -1.0f, -1.0f} // Sommet 4\n};\n

    Un vertex (sommet) peut alternativement contenir des informations suppl\u00e9mentaires comme la couleur, la texture ou la normale. Cette derni\u00e8re est une information importante pour le calcul de l'\u00e9clairage.

    Dans une carte graphique toute forme g\u00e9om\u00e9trique est d\u00e9finie par des sommets, lesquels forment des triangles. Les triangles sont les formes les plus simples \u00e0 dessiner et sont utilis\u00e9s pour repr\u00e9senter des surfaces planes. Il faut deux triangles pour dessiner un rectangle, trois pour un quadrilat\u00e8re, et un certain nombre pour dessiner un cercle. Il est int\u00e9ressant de noter qu'une carte graphique n'est pas capable de dessiner des cercles \u00e0 partir de coordonn\u00e9es simples, ces derniers seront toujours apprixim\u00e9s par des triangles.

    "}, {"location": "course-c/47-gui/opengl/#assemblage-des-primitives", "title": "Assemblage des primitives", "text": "

    Les sommets seuls ne forment pas encore une g\u00e9om\u00e9trie compl\u00e8te. Ils doivent \u00eatre assembl\u00e9s en primitives (triangles, lignes, points). L'assemblage des primitives consiste \u00e0 prendre des groupes de sommets et \u00e0 les combiner pour former des triangles ou d'autres formes g\u00e9om\u00e9triques. La primitive la plus courante est le triangle, c'est d'ailleurs celle que nous avons utilis\u00e9e pour d\u00e9finir la pyramide pr\u00e9c\u00e9dente. N\u00e9anmoins il existe d'autres primitives comme le point, la ligne, le triangle strip, le triangle fan, etc. Les deux derni\u00e8res primitives sont utilis\u00e9es pour optimiser le nombre de sommets \u00e0 envoyer \u00e0 la carte graphique, certains sommets peuvent \u00eatre partag\u00e9s entre plusieurs triangles.

    "}, {"location": "course-c/47-gui/opengl/#traitement-des-sommets", "title": "Traitement des sommets", "text": "

    Une fois les primitives assembl\u00e9es, les sommets sont trait\u00e9s par le vertex shader. Le vertex shader est un petit programme \u00e9crit en langage de shader (comme GLSL pour OpenGL ou HLSL pour DirectX sous Windows) qui s'ex\u00e9cute sur chaque sommet de la primitive. Le GLSL est un langage tr\u00e8s proche du C mais beaucoup plus limit\u00e9. En revanche il est capable de tirer parti des capacit\u00e9s de calcul parall\u00e8le des cartes graphiques et donc d'\u00eatre tr\u00e8s performant pour certaines op\u00e9rations.

    Le vertex shader est responsable de la transformation des coordonn\u00e9es des sommets, de l'application des textures, de l'\u00e9clairage, et d'autres op\u00e9rations g\u00e9om\u00e9triques.

    Par exemple, notre pyramide devra \u00eatre orient\u00e9e dans l'espace 3D. Selon la position de la cam\u00e9ra, la pyramide devra \u00eatre tourn\u00e9e, d\u00e9plac\u00e9e, et \u00e9ventuellement \u00e9clair\u00e9e. Par l'application de matrices de transformation (translation, rotation, mise \u00e0 l'\u00e9chelle) le vertex shader permet de passer de coordonn\u00e9es locales aux coordonn\u00e9es de l'espace de la cam\u00e9ra, puis aux coordonn\u00e9es de l'\u00e9cran.

    Selon l'\u00e9clairage de la sc\u00e8ne, le vertex shader peut \u00e9galement calculer la couleur de chaque sommet en fonction de la position de la lumi\u00e8re. Cette couleur est ensuite interpol\u00e9e entre les sommets pour obtenir une couleur lisse sur toute la surface du triangle.

    Enfin le vertex shader peut \u00e9galement calculer les coordonn\u00e9es de texture pour chaque sommet. Une texture est une image (comme une photo ou une illustration) qui est appliqu\u00e9e \u00e0 la surface d'un objet pour lui donner un aspect r\u00e9aliste.

    Dans notre exemple, imagions que notre pyramide est celle de Kheops, de Kh\u00e9phren ou de Myk\u00e9rinos et que notre cam\u00e9ra est un drone qui survole le Caire. Il est 17h, le soleil est bas sur l'horizon et \u00e9claire la pyramide.

    Pyramide depuis le Caire

    (donner un exemple concret avec les donn\u00e9es de la cam\u00e9ra, de la lumi\u00e8re et une texture de brique, une transformation de perspective de la lentille de la cam\u00e9ra, let normales des faces pour le calcul de l'\u00e9clairage. Donner un exemple en GLSL du vertex shader pour la pyramide.)

    "}, {"location": "course-c/47-gui/opengl/#tessellation", "title": "Tessellation", "text": "

    La tessellation est une \u00e9tape optionnelle du pipeline graphique qui permet de subdiviser les primitives en triangles plus petits. Cette technique est utilis\u00e9e pour augmenter la densit\u00e9 de triangles dans les zones de la sc\u00e8ne qui n\u00e9cessitent plus de d\u00e9tails, comme les courbes ou les surfaces complexes. La tessellation est particuli\u00e8rement utile pour le rendu de surfaces lisses et organiques, comme les visages humains ou les paysages naturels.

    "}, {"location": "course-c/47-gui/opengl/#geometrie", "title": "G\u00e9om\u00e9trie", "text": "

    L'\u00e9tape de g\u00e9om\u00e9trie est une autre \u00e9tape optionnelle du pipeline graphique qui permet de g\u00e9n\u00e9rer de nouveaux sommets \u00e0 partir des sommets d'entr\u00e9e. Cette \u00e9tape est utile pour ajouter des d\u00e9tails suppl\u00e9mentaires \u00e0 la g\u00e9om\u00e9trie, comme des ar\u00eates suppl\u00e9mentaires, des plis ou des d\u00e9formations. La g\u00e9om\u00e9trie est souvent utilis\u00e9e pour g\u00e9n\u00e9rer des ombres, des reflets ou des effets sp\u00e9ciaux dans les jeux vid\u00e9o et les applications graphiques.

    Par exemple si vous souhaitez dessiner 1000 triangles \u00e0 l'\u00e9cran, chacun avec une orientation diff\u00e9rente, il n'est pas n\u00e9cessaire de sp\u00e9cifier les 3000 sommets correspondants. Il suffit de donner les centres des triangles et les orientations. Le geometry shader se chargera de g\u00e9n\u00e9rer les sommets correspondants. Voici un exemple ci-dessous de code GLSL pour un geometry shader qui g\u00e9n\u00e8re des triangles \u00e0 partir de points. Le programme main sera appel\u00e9 par la carte graphique pour chaque point du buffer d'entr\u00e9e, il s'executera donc en parall\u00e8le pour chaque point. Un vec4 est un vecteur de 4 composantes, ici les coordonn\u00e9es \\(x\\), \\(y\\), \\(z\\) et \\(w\\). Le \\(w\\) est une composante suppl\u00e9mentaire dont nous n'avons pas besoin ici.

    #version 330 core\n\nlayout (points) in;\nlayout (triangle_strip, max_vertices = 3) out;\n\nvoid main() {\n   gl_Position = gl_in[0].gl_Position + vec4(-0.1, -0.1, 0.0, 0.0);\n   EmitVertex();\n\n   gl_Position = gl_in[0].gl_Position + vec4(0.1, -0.1, 0.0, 0.0);\n   EmitVertex();\n\n   gl_Position = gl_in[0].gl_Position + vec4(0.0, 0.1, 0.0, 0.0);\n   EmitVertex();\n\n   EndPrimitive();\n}\n
    ", "tags": ["main", "vec4"]}, {"location": "course-c/47-gui/opengl/#clipping", "title": "Clipping", "text": "

    L'\u00e9tape de clipping consiste \u00e0 \u00e9liminer les parties de la g\u00e9om\u00e9trie qui ne sont pas visibles \u00e0 l'\u00e9cran. Cette \u00e9tape est n\u00e9cessaire pour \u00e9viter de dessiner des objets qui sont en dehors du champ de vision de la cam\u00e9ra. Le clipping est g\u00e9n\u00e9ralement effectu\u00e9 en coordonn\u00e9es d'\u00e9cran, apr\u00e8s la projection des sommets en 2D. Le frustrum est une forme tronqu\u00e9e qui repr\u00e9sente le champ de vision de la cam\u00e9ra. Les parties de la g\u00e9om\u00e9trie qui se trouvent en dehors du frustrum sont \u00e9limin\u00e9es.

    Cette \u00e9tape n'a pas besoin d'\u00eatre g\u00e9r\u00e9e manuellement par le d\u00e9veloppeur, elle est g\u00e9n\u00e9ralement effectu\u00e9e par le mat\u00e9riel graphique de mani\u00e8re transparente.

    "}, {"location": "course-c/47-gui/opengl/#rasterisation", "title": "Rasterisation", "text": "

    La rasterisation est l'\u00e9tape du pipeline graphique qui transforme les primitives g\u00e9om\u00e9triques en pixels \u00e0 afficher \u00e0 l'\u00e9cran. Cette \u00e9tape consiste \u00e0 d\u00e9terminer quels pixels sont couverts par les primitives et \u00e0 calculer la couleur de chaque pixel en fonction de la couleur des sommets. La rasterisation est une op\u00e9ration complexe qui n\u00e9cessite de calculer l'intersection des primitives avec les pixels de l'\u00e9cran et d'appliquer des algorithmes de remplissage pour d\u00e9terminer la couleur de chaque pixel.

    Par exemple \u00e0 partir des sommets de notre pyramide, la rasterisation va calculer les pixels couverts par les triangles form\u00e9s par les sommets de fa\u00e7on \u00e0 remplir les faces de la pyramide. Les pixels couverts par les triangles sont ensuite color\u00e9s en fonction de la couleur des sommets et de l'\u00e9clairage de la sc\u00e8ne.

    On dit que les primitives sont rasteris\u00e9es lorsqu'elles sont transform\u00e9es en fragments. Un fragment est un pixel potentiel qui doit \u00eatre color\u00e9.

    "}, {"location": "course-c/47-gui/opengl/#fragment", "title": "Fragment", "text": "

    \u00c0 cette \u00e9tape du pipeline graphique, nos sommets ont \u00e9t\u00e9 convertis en des milliers de fragments. Chaque fragment correspond \u00e0 un pixel de l'\u00e9cran et doit \u00eatre color\u00e9. Le fragment shader est un autre programme \u00e9crit en langage de shader (GLSL) qui s'ex\u00e9cute sur chaque fragment de la primitive.

    Le fragment shader est responsable de calculer la couleur finale de chaque pixel en fonction de la couleur des sommets, de la texture, de l'\u00e9clairage, et d'autres param\u00e8tres. Le programme retourne une couleur pour chaque fragment.

    (donner un exemple concret avec les donn\u00e9es de la cam\u00e9ra, de la lumi\u00e8re, une texture de brique, les normales des faces pour le calcul de l'\u00e9clairage. Donner un exemple en GLSL du fragment shader pour la pyramide.)

    "}, {"location": "course-c/47-gui/opengl/#test-de-profondeur", "title": "Test de profondeur", "text": "

    Une fois les fragments calcul\u00e9s, ils subissent une s\u00e9rie de tests pour d\u00e9terminer s'ils doivent \u00eatre affich\u00e9s \u00e0 l'\u00e9cran. Le test de profondeur est un test qui compare la profondeur de chaque fragment avec la profondeur des fragments d\u00e9j\u00e0 affich\u00e9s \u00e0 l'\u00e9cran. Si le fragment est plus proche de la cam\u00e9ra que les fragments d\u00e9j\u00e0 affich\u00e9s, il est affich\u00e9 \u00e0 l'\u00e9cran. Sinon, il est rejet\u00e9. Ce test est appel\u00e9 le Z-Test.

    Il existe \u00e9galement le Stencil Test qui permet de d\u00e9finir une zone de l'\u00e9cran o\u00f9 les fragments peuvent \u00eatre affich\u00e9s. Cela permet de cr\u00e9er des effets sp\u00e9ciaux comme des ombres, des reflets, ou des effets de transparence.

    Enfin le Blending permet de m\u00e9langer le fragment avec les fragments d\u00e9j\u00e0 affich\u00e9s \u00e0 l'\u00e9cran. Cela permet de cr\u00e9er des effets de transparence, de luminosit\u00e9, ou de flou.

    "}, {"location": "course-c/47-gui/opengl/#ecriture-dans-le-framebuffer", "title": "\u00c9criture dans le framebuffer", "text": "

    Une fois que tous les tests et calculs ont \u00e9t\u00e9 effectu\u00e9s, les fragments restants sont convertis en pixels et \u00e9crits dans le framebuffer (la m\u00e9moire vid\u00e9o). Le framebuffer contient les pixels qui seront envoy\u00e9s \u00e0 l\u2019\u00e9cran pour \u00eatre affich\u00e9s.

    "}, {"location": "course-c/47-gui/opengl/#opengl-et-vulkan", "title": "OpenGL et Vulkan", "text": "

    OpenGL, ou Open Graphics Library, est une API graphique multiplateforme utilis\u00e9e principalement pour le rendu 2D et 3D dans des applications interactives. Elle est tr\u00e8s r\u00e9pandue dans l'industrie des jeux vid\u00e9o, la visualisation scientifique, la mod\u00e9lisation 3D, et les simulations interactives. Con\u00e7ue \u00e0 l'origine pour permettre l'acc\u00e9l\u00e9ration graphique en temps r\u00e9el via des cartes graphiques, OpenGL a marqu\u00e9 une r\u00e9volution en facilitant le d\u00e9veloppement d'applications graphiques de haute performance tout en masquant les d\u00e9tails sp\u00e9cifiques au mat\u00e9riel.

    OpenGL a \u00e9t\u00e9 initialement d\u00e9velopp\u00e9 par Silicon Graphics, Inc. (SGI) en 1992. \u00c0 l'\u00e9poque, SGI dominait le march\u00e9 des stations de travail graphiques de haute performance, utilis\u00e9es dans des domaines comme la mod\u00e9lisation 3D et la simulation scientifique. SGI voulait une API standardis\u00e9e qui permettrait aux d\u00e9veloppeurs de concevoir des applications ind\u00e9pendantes des sp\u00e9cificit\u00e9s mat\u00e9rielles des diff\u00e9rentes cartes graphiques, tout en tirant parti de l'acc\u00e9l\u00e9ration mat\u00e9rielle.

    Le but d'OpenGL \u00e9tait de fournir une interface simple et uniforme qui fonctionne sur divers syst\u00e8mes d'exploitation (Windows, Linux, macOS) et plateformes mat\u00e9rielles, permettant ainsi une portabilit\u00e9 accrue des applications graphiques. Depuis, OpenGL a \u00e9volu\u00e9 au fil des ann\u00e9es, en int\u00e9grant de nombreuses fonctionnalit\u00e9s graphiques modernes, comme les shaders et les buffers de trames.

    Bien que d'autres API graphiques comme DirectX (pour Windows) ou Direct3D (pour les jeux vid\u00e9o) soient \u00e9galement tr\u00e8s populaires, OpenGL reste une API de choix pour de nombreux d\u00e9veloppeurs en raison de sa portabilit\u00e9, de sa flexibilit\u00e9 et de sa compatibilit\u00e9 avec un large \u00e9ventail de mat\u00e9riels.

    N\u00e9anmoins, certaines limitations ont conduit le Khronos Group qui g\u00e8re OpenGL a d\u00e9velopp\u00e9 Vulkan, une nouvelle API graphique et de calcul, publi\u00e9e en 2016. Vulkan est consid\u00e9r\u00e9 comme le successeur d'OpenGL et offre de nombreuses am\u00e9liorations qui r\u00e9pondent aux besoins modernes des d\u00e9veloppeurs de jeux et d\u2019applications graphiques.

    Les jeux vid\u00e9os modernes comme FarCry, The Witcher 3, Red Dead Redemption 2, ou encore Cyberpunk 2077 utilisent des moteurs graphiques bas\u00e9s sur les API Vulkan ou DirectX 12 pour tirer parti des performances des cartes graphiques r\u00e9centes.

    En 2024, sur Windows, DirectX est l'API dominante. N\u00e9anmoins avec la technologie DXVK qui est une couche de traduction permettant \u00e0 des applications Vulkan de fonctionner avec DirectX 12.

    "}, {"location": "course-c/47-gui/opengl/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    OpenGL et Vulkan fonctionnent sur le principe de la programmation par \u00e9tats. Cela signifie que l'application configure l'\u00e9tat de l'API graphique en d\u00e9finissant des param\u00e8tres comme la couleur, la texture, la lumi\u00e8re, la perspective, etc. Une fois l'\u00e9tat configur\u00e9, l'application envoie des commandes graphiques \u00e0 l'API pour dessiner des objets g\u00e9om\u00e9triques, des textures, des effets visuels, etc.

    Ces API offrent une abstraction du mat\u00e9riel graphique sous-jacent, permettant aux d\u00e9veloppeurs de concevoir des applications graphiques sans se soucier des d\u00e9tails sp\u00e9cifiques \u00e0 la carte graphique. En effet, une carte graphique contient des milliers de c\u0153urs de calcul qui peuvent \u00eatre programm\u00e9s pour effectuer des calculs parall\u00e8les. Heureusement pour les d\u00e9veloppeurs, OpenGL et Vulkan fournissent des interfaces de haut niveau pour exploiter ces capacit\u00e9s de calcul sans avoir \u00e0 g\u00e9rer les d\u00e9tails complexes du mat\u00e9riel.

    L'abstraction consiste principalement \u00e0 ce que l'on nomme le pipeline graphique. Le pipeline graphique est une s\u00e9quence d'\u00e9tapes qui transforme les donn\u00e9es g\u00e9om\u00e9triques en pixels affich\u00e9s \u00e0 l'\u00e9cran. Ces \u00e9tapes incluent la transformation des coordonn\u00e9es, l'application des textures, l'\u00e9clairage, la perspective, et bien d'autres. Chaque \u00e9tape du pipeline est configurable par l'application, permettant ainsi de personnaliser le rendu graphique en fonction des besoins.

    "}, {"location": "course-c/47-gui/opengl/#carte-graphique", "title": "Carte graphique", "text": ""}, {"location": "course-c/47-gui/opengl/#pipeline", "title": "Pipeline", "text": "

    Le pipeline graphique d'OpenGL et de Vulkan est compos\u00e9 de plusieurs \u00e9tapes, chacune effectuant une transformation sp\u00e9cifique sur les donn\u00e9es graphiques. Voici les \u00e9tapes principales du pipeline graphique\u2009:

    "}, {"location": "course-c/47-gui/opengl/#double-buffer", "title": "Double Buffer", "text": "

    OpenGL utilise un double buffer pour afficher les images. Le double buffer est compos\u00e9 de deux buffers\u2009: un buffer de dessin et un buffer d'affichage. Le buffer de dessin est utilis\u00e9 pour dessiner les images et le buffer d'affichage est utilis\u00e9 pour afficher les images. Lorsque l'image est dessin\u00e9e dans le buffer de dessin, elle est ensuite copi\u00e9e dans le buffer d'affichage. Cela permet d'\u00e9viter les probl\u00e8mes de scintillement. C'est une pratique tr\u00e8s courante dans les applications graphiques.

    "}, {"location": "course-c/47-gui/opengl/#vsync", "title": "Vsync", "text": "

    La synchronisation verticale (Vsync) est une technique qui permet de synchroniser le taux de rafra\u00eechissement de l'\u00e9cran avec le taux de rafra\u00eechissement de l'application. Cela permet d'\u00e9viter les probl\u00e8mes de d\u00e9chirure d'\u00e9cran. La synchronisation verticale est g\u00e9n\u00e9ralement activ\u00e9e par d\u00e9faut dans les applications graphiques.

    "}, {"location": "course-c/47-gui/opengl/#gflw", "title": "GFLW", "text": "

    La biblioth\u00e8que GLFW est une biblioth\u00e8que C qui permet de cr\u00e9er des fen\u00eatres avec OpenGL. Elle est compatible avec OpenGL ES et Vulkan. Elle est utilis\u00e9e pour cr\u00e9er des fen\u00eatres et g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre. On pourrait tr\u00e8s bien utiliser GTK n\u00e9anmoins GLFW est plus simple et plus adapt\u00e9 \u00e0 OpenGL.

    Pour installer GLFW sur Ubuntu, il suffit d'installer la bibloth\u00e8que avec les fichiers d'en-t\u00eate\u2009:

    sudo apt install libglfw3-dev\n

    Pour compiler un programme avec GLFW, il est n\u00e9cessaire de lier la biblioth\u00e8que avec le programme. Pour cela, il suffit d'ajouter l'option -lglfw \u00e0 la commande de compilation.

    Un programme simple qui cr\u00e9e une fen\u00eatre avec GLFW\u2009:

    #include <GLFW/glfw3.h>\n\nint main() {\n   if (!glfwInit()) return -1;\n   GLFWwindow* window = glfwCreateWindow(800, 400, \"Window\", NULL, NULL);\n   if (!window) {\n      glfwTerminate();\n      return -2;\n   }\n   glfwMakeContextCurrent(window);\n   while (!glfwWindowShouldClose(window)) {\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   }\n}\n

    On observe que la biblioth\u00e8que est tout d'abord initialis\u00e9e avec la fonction glfwInit(). Ensuite, une fen\u00eatre est cr\u00e9\u00e9e avec un titre. Les deux derniers param\u00e8tres laiss\u00e9s \u00e0 NULL sont des pointeurs sur le moniteur sur laquelle la fen\u00eatre est affich\u00e9e GLFWmonitor et la fen\u00eatre du parent GLFWwindow utilis\u00e9e dans le cas d'une application multi-fen\u00eatres. Dans notre cas on laisse ces param\u00e8tres \u00e0 NULL car nous n'avons pas besoin de ces fonctionnalit\u00e9s.

    Si la fen\u00eatre n'a pas pu \u00eatre cr\u00e9\u00e9e, le programme se termine. Sinon, la fen\u00eatre est affich\u00e9e avec la fonction glfwMakeContextCurrent(window).

    Enfin, une boucle est cr\u00e9\u00e9e pour afficher la fen\u00eatre tant que l'utilisateur ne la ferme pas. La fonction glfwSwapBuffers(window) permet de copier le contenu du buffer de dessin dans le buffer d'affichage. La fonction glfwPollEvents() permet de g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre.

    Le grand avantage de ce programme est qu'il est portable. Il fonctionne sur Windows, Linux et MacOS.

    GLFW permet \u00e9galement de g\u00e9rer les joystick et gamepads, les \u00e9v\u00e8nements du clavier ou de la souris ainsi que le curseurs de la souris. Cela permet de cr\u00e9er des applications graphiques interactives sans n\u00e9cessit\u00e9 d'avoir recours \u00e0 d'autres biblioth\u00e8ques.

    Nous n'approfonfirons pas plus GLFW dans ce cours n\u00e9anmoins, vous avez toujours la possibilit\u00e9 de consulter la documentation officielle de GLFW qui est tr\u00e8s compl\u00e8te.

    ", "tags": ["GLFWwindow", "NULL", "GLFWmonitor"]}, {"location": "course-c/47-gui/opengl/#glew", "title": "GLEW", "text": "

    La biblioth\u00e8que GLEW (OpenGL Extension Wrangler Library) est une biblioth\u00e8que qui facilite le chargement des extensions OpenGL. OpenGL a un ensemble de fonctionnalit\u00e9s qui peut varier selon le mat\u00e9riel graphique et le syst\u00e8me d'exploitation, et GLEW est utilis\u00e9 pour acc\u00e9der \u00e0 ces fonctionnalit\u00e9s de mani\u00e8re portable. En sommes, la biblioth\u00e8que permet de charger dynamiquement des fonctions OpenGL qui ne sont pas directement accessibles par le syst\u00e8me, surtout pour les versions modernes d'OpenGL.

    Pour disposer d'un contexte OpenGL utilsable nous devons compl\u00e9ter notre programme pr\u00e9c\u00e9dent avec GLEW. D'abord des messages d'erreurs plus explicites sont affich\u00e9s en cas d'erreur. Certains param\u00e8tres sont ajout\u00e9s pour activer l'anticr\u00e9nelage et sp\u00e9cifier la version d'OpenGL que nous voulons utiliser.

    Dans la boucle principale, la touche echape permet maintenant de quitter le programme.

    #include <GL/glew.h>\n#include <GLFW/glfw3.h>\n#include <stdbool.h>\n#include <stdio.h>\n\nint main() {\n   if (!glfwInit()) {\n      fprintf(stderr, \"Failed to initialize GLFW\\n\");\n      return -1;\n   }\n   glfwWindowHint(GLFW_SAMPLES, 4);                // 4x antialiasing\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // We want OpenGL 3.3\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n   glfwWindowHint(GLFW_OPENGL_PROFILE,\n                  GLFW_OPENGL_CORE_PROFILE);  // We don't want the old OpenGL\n\n   // Open a window and create its OpenGL context\n   GLFWwindow* window = glfwCreateWindow(800, 400, \"OpenGL\", NULL, NULL);\n   if (window == NULL) {\n      fprintf(stderr, \"Error: Failed to open GLFW window.\\n\");\n      glfwTerminate();\n      return -1;\n   }\n   glfwMakeContextCurrent(window);\n   if (glewInit() != GLEW_OK) {\n      fprintf(stderr, \"Error: Failed to initialize GLEW\\n\");\n      return -1;\n   }\n   glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);\n   do {\n      glClear(GL_COLOR_BUFFER_BIT);\n\n      // ...\n\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   } while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&\n            glfwWindowShouldClose(window) == 0);\n}\n
    "}, {"location": "course-c/47-gui/opengl/#glut", "title": "GLUT", "text": "

    GLUT (OpenGL Utility Toolkit) est une biblioth\u00e8que qui facilite la cr\u00e9ation de fen\u00eatres OpenGL. Elle est plus ancienne que GLFW et elle est moins utilis\u00e9e. GLUT est une biblioth\u00e8que portable qui permet de cr\u00e9er des fen\u00eatres OpenGL sur Windows, Linux et MacOS. Elle permet \u00e9galement de g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre, les \u00e9v\u00e9nements de clavier et de souris, et les \u00e9v\u00e9nements de redimensionnement de fen\u00eatre. Pr\u00e9f\u00e9rez GLFW \u00e0 GLUT pour vos projets OpenGL.

    "}, {"location": "course-c/47-gui/opengl/#coordonnees", "title": "Coordonn\u00e9es", "text": "

    Le syst\u00e8me de coordonn\u00e9es d'OpenGL est un peu particulier. L'origine du syst\u00e8me de coordonn\u00e9es est au centre de la fen\u00eatre. Les coordonn\u00e9es x et y vont de -1 \u00e0 1. Les coordonn\u00e9es z vont de -1 \u00e0 1. Les coordonn\u00e9es x et y sont en 2D et la coordonn\u00e9e z est en 3D.

    On notera qu'il n'y a pas de notion de pixels dans le syst\u00e8me de coordonn\u00e9es d'OpenGL car les coordonn\u00e9es sont normalis\u00e9es et repr\u00e9sent\u00e9e par un flottant 32-bit. Nous verrons que pour dessiner des formes g\u00e9om\u00e9triques, la projection doit \u00eatre ajust\u00e9e car si la fen\u00eatre n'est pas carr\u00e9e, les formes g\u00e9om\u00e9triques seront d\u00e9form\u00e9es.

    Le \\(x\\) positif est \u00e0 droite, le \\(y\\) positif est en haut et le \\(z\\) positif est vers l'observateur. C'est la r\u00e8gle de la main droite issue de la physique o\u00f9 l'axe \\(z\\) est la direction du pouce, l'axe \\(x\\) est l'index et l'axe \\(y\\) est le majeur.

    Si nous souhaitons dessiner un triangle isoc\u00e8le centr\u00e9 dans l'espace, nous devons d\u00e9finir les coordonn\u00e9es des sommets du triangle.

    static const GLfloat verticles[] = {\n   -1.0f, -1.0f, 0.0f, // Bottom left\n   1.0f,  -1.0f, 0.0f, // Bottom right\n   0.0f,  1.0f,  0.0f, // Top\n};\n
    "}, {"location": "course-c/47-gui/opengl/#vertex_1", "title": "Vertex", "text": "

    Un vertex est un sommet dans l'espace 3D. Un vertex est d\u00e9fini par ses coordonn\u00e9es \\(x\\), \\(y\\) et \\(z\\) mais il peut \u00e9galement avoir d'autres attributs comme la couleur, la normale, la texture, etc. La normale est un vecteur perpendiculaire \u00e0 la surface du sommet, c'est une information importante pour les calculs d'\u00e9clairage. En effet, une surface va refl\u00e9ter la lumi\u00e8re diff\u00e9remment selon l'angle d'incidence de la lumi\u00e8re. OpenGL est assez flexible pour d\u00e9finir les attributs d'un vertex.

    D\u00e9finissons les primitives de base d'un vertex. Il peut contenir un point\u2009:

    typedef union Point2D {\n   struct {\n      GLfloat x;\n      GLfloat y;\n   };\n   GLfloat data[2];\n} Point2D;\n\ntypedef union Point3D {\n   struct {\n      GLfloat x;\n      GLfloat y;\n      GLfloat z;\n   };\n   GLfloat data[3];\n} Point3D;\n

    Il peut contenir une normale qui est un vecteur. Formellement un vecteur n'est pas un point mais il peut \u00eatre repr\u00e9sent\u00e9 par un point\u2009:

    typedef Point3D Vector3D;\n

    Il peut contenir une couleur. En OpenGL une couleur peut \u00eatre repr\u00e9sent\u00e9e par un vecteur de trois composantes rouge, vert et bleu, ou par un vecteur de quatre composantes rouge, vert, bleu et alpha pour g\u00e9rer la transparence\u2009:

    typedef union ColorRGB {\n   struct {\n      GLfloat r;\n      GLfloat g;\n      GLfloat b;\n   };\n   GLfloat data[3];\n} ColorRGB;\n\ntypedef union ColorRGBA {\n   struct {\n      GLfloat r;\n      GLfloat g;\n      GLfloat b;\n      GLfloat a;\n   };\n   GLfloat data[4];\n} ColorRGBA;\n

    Enfin, on peut imaginer diff\u00e9rents types de vertex\u2009:

    typedef union VertexA {\n   struct {\n      Point3D position;\n      ColorRGB color;\n      Vector3D normal;\n      Point2D texture_coordinates;\n   };\n   GLfloat data[11];\n} VertexA;\n\ntypedef union VertexB {\n   struct {\n      Point2D position;\n   };\n   GLfloat data[2];\n} VertexB;\n\ntypedef union VertexC {\n   struct {\n      ColorRGBA color;\n      Vector3D normal;\n      Point3D position;\n   };\n   GLfloat data[10];\n} VertexC;\n

    Une chose est s\u00fbre, un vertex est un tableau de flottants 32-bits et il est rarement seul. Il est souvent regroup\u00e9 dans un tableau de vertex pour former un objet g\u00e9om\u00e9trique.

    Comme l'ordre des attributs d'un vertex configurable, il est n\u00e9cessaire d'informer OpenGL de la mani\u00e8re dont les attributs sont structur\u00e9s. Pour configurer notre VertexC nous utiliserons par exemple\u2009:

    // Position\nglEnableVertexAttribArray(0);\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(6 * sizeof(GLfloat)));\n\n// Color\nglEnableVertexAttribArray(1);\nglVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(0 * sizeof(GLfloat)));\n\n// Normal\nglEnableVertexAttribArray(2);\nglVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(3 * sizeof(GLfloat)));\n

    Chaque vertex \u00e0 donc 3 attributs\u2009: la position, la couleur et la normale. L'attribut 0 est la couleur, l'attribut 1 est la normale et l'attribut 2 est la position. J'ai volontairement invers\u00e9 l'ordre des attributs pour montrer qu'il n'a pas d'importance, vous faites comme vous voulez.

    Le dernier argument de glVertexAttribPointer peut paraitre \u00e9trange. Pourquoi ne s'agit-il pas d'un uintptr_t mais d'un GLvoid* ? C'est une question de compatibilit\u00e9 avec les anciennes versions d'OpenGL. GLvoid* est un pointeur g\u00e9n\u00e9rique qui peut pointer sur n'importe quel type de donn\u00e9es. C'est en r\u00e9alit\u00e9 un void* en C. N\u00e9anmoins, ce n'est pas vraiment un pointeur non plus car il ne peut pas \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9. Il y a parfois des questions d'h\u00e9ritage dans les API ou cette derni\u00e8re \u00e0 \u00e9volu\u00e9e mais que pour des raisons de compatibilit\u00e9, certaines d\u00e9cisions historiques sont conserv\u00e9es.

    ", "tags": ["VertexC", "uintptr_t"]}, {"location": "course-c/47-gui/opengl/#vbo", "title": "VBO", "text": "

    Un VBO (Vertex Buffer Object) est un espace m\u00e9moire qui peut \u00eatre utilis\u00e9 pour stocker des donn\u00e9es de sommets. C'est en g\u00e9n\u00e9ral cet objet qui est partag\u00e9 entre le CPU et le GPU. \u00c9videmment plus le buffer est grand plus le temps de transfert peut \u00eatre long, surtout si l'op\u00e9ration est r\u00e9p\u00e9t\u00e9e \u00e0 chaque image.

    Avant de pouvoir utiliser un tel buffer il faut en faire la requ\u00eate \u00e0 OpenGL. Dans l'exemple suivant on demande \u00e0 OpenGL de nous allouer un seul buffer\u2009:

    Gluint vbo = 0;\nglGenBuffers(1, &vbo);\n

    La variable vbo contiendra l'identifiant du buffer allou\u00e9 par OpenGL. Une fois allou\u00e9 il sera important plus tard d'utiliser glDeleteBuffers(1, &vbo) pour lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement.

    Afin d'\u00e9viter de passer l'identifiant du vbo \u00e0 toutes les fonctions OpenGL, qui traitent les buffers, la strat\u00e9gie adopt\u00e9e est de lier le buffer \u00e0 un contexte OpenGL. Cela se fait avec la fonction glBindBuffer :

    glBindBuffer(GL_ARRAY_BUFFER, vbo);\n

    Ici on indique que le vbo cr\u00e9\u00e9 sera utilis\u00e9 pour stocker des GL_ARRAY_BUFFER, c'est-\u00e0-dire des donn\u00e9es de sommets, et que ce vbo sera d\u00e9sormais le buffer actif pour toutes les fonctions du type glBufferData, glBufferSubData, glMapBuffer, etc. Et ce jusqu'\u00e0 ce qu'un autre buffer soit li\u00e9 ou que le buffer soit supprim\u00e9.

    Plus haut, nous avions d\u00e9fini les coordonn\u00e9es d'un triangle en 3 dimensions. Il est maintenant temps de les envoyer \u00e0 la carte graphique. Pour cela nous utilisons la fonction glBufferData apr\u00e8s que le buffer ait \u00e9t\u00e9 li\u00e9 au contexte\u2009:

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n

    La constante GL_STATIC_DRAW indique \u00e0 OpenGL que les donn\u00e9es ne changeront pas souvent. Il existe d'autres constantes pour indiquer \u00e0 OpenGL que les donn\u00e9es seront modifi\u00e9es fr\u00e9quemment ou rarement. Cela permet \u00e0 OpenGL d'optimiser le stockage des donn\u00e9es en m\u00e9moire.

    ", "tags": ["glBufferSubData", "GL_ARRAY_BUFFER", "glBindBuffer", "glBufferData", "vbo", "GL_STATIC_DRAW", "glMapBuffer"]}, {"location": "course-c/47-gui/opengl/#vao", "title": "VAO", "text": "

    Nous avons vu plus haut qu'un vertex peut \u00eatre plus ou moins complexe en fonction des attributs qu'il contient. Un VAO (Vertex Array Object) est un tableau qui stocke l'\u00e9tat des attributs de vertex. Nous avons vu \u00e9galement que ces attributs doivent \u00eatre configur\u00e9s pour \u00eatre utilis\u00e9s par OpenGL. C'est le r\u00f4le du VAO de stocker cette configuration.

    De la m\u00eame mani\u00e8re que pour le VBO, on demande \u00e0 OpenGL de nous allouer un VAO, puis on le lie au contexte actif. Ensuite on peut configurer les attributs du vertex et les lier au VBO.

    Gluint vao = 0;\nglGenVertexArrays(1, &vao);\nglBindVertexArray(vao);\n

    Comme notre triangle ne contient qu'une position en 3D, nous devons configurer l'attribut 0 pour qu'il pointe vers la position du vertex.

    glEnableVertexAttribArray(0);\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,\n    3 * sizeof(GLfloat), (GLvoid*)0);\n

    Ici on indique \u00e0 OpenGL que l'attribut 0 est un vecteur de 3 flottants, que les donn\u00e9es sont de type GL_FLOAT, que les donn\u00e9es ne sont pas normalis\u00e9es, que chaque vertex est s\u00e9par\u00e9 de 3 flottants et que le premier vertex commence \u00e0 l'indice 0.

    ", "tags": ["GL_FLOAT"]}, {"location": "course-c/47-gui/opengl/#shader", "title": "Shader", "text": "

    Un shader est un programme qui s'ex\u00e9cute sur la carte graphique. Il est \u00e9crit en langage GLSL (OpenGL Shading Language) pour OpenGL ou en HLSL (High-Level Shading Language) pour DirectX. Comme pour un programme en C, un shader doit \u00eatre compil\u00e9 avant d'\u00eatre ex\u00e9cut\u00e9 mais contrairement \u00e0 un programme en C il sera compil\u00e9 \u00e0 chaque ex\u00e9cution du programme. Cela permet de s'adapter \u00e0 la configuration mat\u00e9rielle de la carte graphique car l'architecture des cartes graphiques peut varier d'un PC \u00e0 l'autre.

    Le GLSL est tr\u00e8s similaire au C du point de vue de la syntaxe mais il est beaucoup plus limit\u00e9. Il n'y a pas de pointeurs, pas de structures, pas de fonctions r\u00e9cursives, pas de boucles infinies, pas de gestion de la m\u00e9moire, etc. En effet, les processeurs graphiques sont des processeurs sp\u00e9cialis\u00e9s qui n'ont pas besoin de ces fonctionnalit\u00e9s. Ils sont tr\u00e8s simples mais tr\u00e8s nombreux.

    Dans une carte graphique de type GTX 3090 sortie en 2020 et capable de faire tourner un jeu comme Cyberpunk 2077 en 4K, il y a 82 streaming multiprocessors ou SMs. Chaque SM contient 128 CUDA cores et donc la carte graphique contient 10496 CUDA cores. Chaque CUDA core peut ex\u00e9cuter diff\u00e9rent type de shaders\u2009: vertex, tessellation, geometry, fragment, etc. En outre, cette carte graphique tourne \u00e0 1.7 GHz, c'est \u00e0 dire que chaque CUDA core peut ex\u00e9cuter 1.7 milliard d'instructions par seconde.

    En d'autres termes, un shader GLSL de 10 instructions qui sera ex\u00e9cut\u00e9 pour chaque pixel d'un \u00e9cran de 3840x2160 pixels sera ex\u00e9cut\u00e9 en\u2009:

    \\[ \\frac{x \\times y}{\\text{cores}} \\times \\text{instructions} \\times \\frac{1}{\\text{freq}} \\frac{3840 \\times 2160}{10496} \\times 10 \\times \\frac{1}{1.7 \\times 10^9} = 4\\mu s \\]

    Le premier shader du pipeline graphique est le vertex shader. Il est ex\u00e9cut\u00e9 pour chaque vertex d'un objet g\u00e9om\u00e9trique. Il sera utilis\u00e9 pour transformer les coordonn\u00e9es des sommets du mod\u00e8le en coordonn\u00e9es de l'\u00e9cran car rappelez-vous que les coordonn\u00e9es des sommets sont normalis\u00e9es.

    Un shader de mani\u00e8re g\u00e9n\u00e9rique poss\u00e8de des entr\u00e9es et des sorties. Les entr\u00e9es sont les donn\u00e9es que le shader re\u00e7oit et les sorties sont les donn\u00e9es que le shader transmera \u00e0 l'\u00e9tape suivante du pipeline graphique. En plus des entr\u00e9es et des sorties, un shader peut avoir des uniformes. Une uniforme est une variable du programme principal qui est constante pour tous les vertex ou tous les fragments au m\u00eame instant de rendu. Une uniforme est typiquement utilis\u00e9e pour passer des matrices de transformation (rotation, translation, \u00e9chelle).

    Il faut \u00e9galement noter que le langage GLSL poss\u00e8de des types un peu diff\u00e9rents de C. Il existe des types de base comme int, float, vec2, vec3, vec4, mat2, mat3, mat4, etc. Il existe \u00e9galement des types de structures et des types de tableaux. Un vec3 est un vecteur de 3 flottants, un mat4 est une matrice de 4x4 flottants.

    Enfin, le langage GLSL \u00e0 beaucoup \u00e9volu\u00e9 depuis sa premi\u00e8re version en 2004. La mani\u00e8re de transmettre les entr\u00e9es et les sorties d'un shader a beaucoup chang\u00e9. Il est maintenant possible de d\u00e9clarer les entr\u00e9es et les sorties d'un shader avec des mots-cl\u00e9s comme in, out, uniform, layout, etc.

    Le vertex shader le plus typique que l'on puisse \u00e9crire est le suivant\u2009:

    #version 330 core\n\nlayout(location = 0) in vec3 position;\n\nvoid main() {\n   gl_Position = vec4(position, 1.0);\n}\n

    Ce shader prend en entr\u00e9e un vecteur de 3 flottants position issue de l'attribut 0 du vertex. Il transforme ce vecteur en un vecteur de 4 flottants gl_Position qui est la position du vertex dans l'espace de l'\u00e9cran. La quatri\u00e8me coordonn\u00e9e est li\u00e9e \u00e0 l'utlisation de coordonn\u00e9es homog\u00e8nes dans les transformations grpahiques en 3D. Cette quatri\u00e8me coordonn\u00e9e nomm\u00e9e w a un r\u00f4le crucial dans les transformations g\u00e9om\u00e9triques. En math\u00e9matiques et en infographie, les coordonn\u00e9es homog\u00e8nes permettent d'utiliser des transformations affines (comme la translation, la rotation, l'\u00e9chelle) et des transformations projectives (comme la projection en perspective) de mani\u00e8re plus simple et uniforme. Lors d'une transformation projective, la quatri\u00e8me coordonn\u00e9e est utilis\u00e9e pour d\u00e9terminer la profondeur du vertex dans l'espace 3D. Elle permet de simuler l'effet de rapetissement des objets \u00e9loign\u00e9s dans une sc\u00e8ne 3D. Pour une sc\u00e8ne en 2D ou une sc\u00e8ne en 3D sans perspective, la quatri\u00e8me coordonn\u00e9e est \u00e9gale \u00e0 1.0f.

    Le deuxi\u00e8me type de shader dont nous aurons besoin est nomm\u00e9 fragment shader. Un fragment est un \u00e9l\u00e9ment de base d'un objet g\u00e9om\u00e9trique qui contient une information de couleur mais aussi de la position sur l'\u00e9cran, la coordonn\u00e9e d'une texture, des valeurs d'interpolation ou m\u00eame une information de profondeur en \\(z\\). Chaque fragment repr\u00e9sente une peitite portion d'une primitive (triangle, carr\u00e9, cercle, etc.) souvent associ\u00e9e \u00e0 un pixel de l'\u00e9cran. Le fragment shader le plus simple que l'on puisse \u00e9crire est le suivant\u2009:

    #version 330 core\n\nout vec4 FragColor;\n\nvoid main() {\n   FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n}\n

    Pour chaque fragment, on retourne la couleur \\([1.0, 1.0, 1.0, 1.0]\\) qui est le blanc. Donc chaque pixel contenu dans la primitive dessin\u00e9e sera blanc. Il conviendra d'avoir du noir pour la couleur de fond de la fen\u00eatre pour voir notre triangle.

    Maintenant que nous avons nos deux shaders, il est temps de les compiler et de les lier \u00e0 un programme OpenGL. Un programme OpenGL est un ensemble de shaders qui sont li\u00e9s ensemble pour former un programme graphique. Un programme graphique est un ensemble de shaders qui sont ex\u00e9cut\u00e9s \u00e0 chaque image pour dessiner des primitives.

    Si nous reprenons notre triangle pr\u00e9c\u00e9dent, le vertex shader transformera les coordonn\u00e9es du triangle en coordonn\u00e9es de l'\u00e9cran sans les d\u00e9former et le fragment shader colorera chaque pixel du triangle en blanc.

    Tout d'abord chaque shader est stock\u00e9 sous forme d'une cha\u00eene de caract\u00e8res, puis chaque shader est compil\u00e9 et enfin les shaders sont li\u00e9s ensemble pour former un programme graphique\u2009:

    const char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = vec4(position, 1.0);\\n\"\n    \"}\\n\";\n\nconst char* fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"out vec4 FragColor;\\n\"\n    \"void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); }\\n\";\n\nGLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);\nglShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\nglCompileShader(vertexShader);\n\nGLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\nglShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\nglCompileShader(fragmentShader);\n\nGLuint shaderProgram = glCreateProgram();\nglAttachShader(shaderProgram, vertexShader);\nglAttachShader(shaderProgram, fragmentShader);\nglLinkProgram(shaderProgram);\nglUseProgram(shaderProgram);\n\nglDeleteShader(vertexShader);\nglDeleteShader(fragmentShader);\n

    Notez que cette m\u00e9thode est tr\u00e8s basique et ne g\u00e8re pas les erreurs de compilation ou de liaison des shaders, nous verrons plus tard un exemple plus complet.

    ", "tags": ["vec3", "int", "out", "mat2", "mat3", "float", "position", "vec2", "layout", "mat4", "gl_Position", "vec4", "uniform"]}, {"location": "course-c/47-gui/opengl/#rendu", "title": "Rendu", "text": "

    Si vous \u00eates arriv\u00e9 jusqu'ici, vous avez compris que nous utilisons GLFW pour cr\u00e9er une fen\u00eatre graphique et GLEW pour charger les extensions OpenGL. Nous avons compris la notion de double buffer et de coordonn\u00e9es normalis\u00e9es avec la r\u00e8gle de la main droite. Les notions de VBO et de VAO n'ont plus de secret pour vous et vous avez compris l'utilit\u00e9 d'un vertex shader et d'un fragment shader.

    Il est temps de dessiner notre triangle.

    #include <GL/glew.h>\n#include <GLFW/glfw3.h>\n#include <stdbool.h>\n#include <stdio.h>\n\nstatic const GLfloat vertices[] = {\n    -1.0f, -1.0f, 0.0f,  // Bottom left\n    1.0f,  -1.0f, 0.0f,  // Bottom right\n    0.0f,  1.0f,  0.0f,  // Top\n};\n\nconst char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = vec4(position, 1.0);\\n\"\n    \"}\\n\";\n\nconst char* fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"out vec4 FragColor;\\n\"\n    \"void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); }\\n\";\n\nint main() {\n   // Initialise GLFW\n   if (!glfwInit()) {\n      fprintf(stderr, \"Failed to initialize GLFW\\n\");\n      return -1;\n   }\n   glfwWindowHint(GLFW_SAMPLES, 4);                // 4x antialiasing\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // We want OpenGL 3.3\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n   glfwWindowHint(GLFW_OPENGL_PROFILE,\n                  GLFW_OPENGL_CORE_PROFILE);  // We don't want the old OpenGL\n\n   // Open a window and create its OpenGL context\n   GLFWwindow* window =\n       glfwCreateWindow(800, 400, \"White Triangle\", NULL, NULL);\n   if (window == NULL) {\n      fprintf(stderr, \"Error: Failed to open GLFW window.\\n\");\n      glfwTerminate();\n      return -1;\n   }\n   glfwMakeContextCurrent(window);\n   if (glewInit() != GLEW_OK) {\n      fprintf(stderr, \"Error: Failed to initialize GLEW\\n\");\n      return -1;\n   }\n   glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);\n\n   // Compile and link shaders\n   unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);\n   glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\n   glCompileShader(vertexShader);\n\n   unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\n   glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\n   glCompileShader(fragmentShader);\n\n   unsigned int shaderProgram = glCreateProgram();\n   glAttachShader(shaderProgram, vertexShader);\n   glAttachShader(shaderProgram, fragmentShader);\n   glLinkProgram(shaderProgram);\n   glUseProgram(shaderProgram);\n\n   glDeleteShader(vertexShader);\n   glDeleteShader(fragmentShader);\n\n   // Setup VAO/VBO\n   unsigned int VBO = 0, VAO = 0;\n   glGenVertexArrays(1, &VAO);\n   glGenBuffers(1, &VBO);\n   glBindVertexArray(VAO);\n\n   glBindBuffer(GL_ARRAY_BUFFER, VBO);\n   glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\n   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\n   glEnableVertexAttribArray(0);\n\n   //  Main loop\n   do {\n      glClear(GL_COLOR_BUFFER_BIT);\n      glDrawArrays(GL_TRIANGLES, 0, 3);\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   } while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&\n            glfwWindowShouldClose(window) == 0);\n}\n

    Apr\u00e8s compilation avec la commande suivante, on obtient un triangle blanc comme illustr\u00e9 ci-dessous

    gcc -o triangle triangle.c -lglfw -lGLEW -lGL -lm\n

    Triangle Blanc

    ", "tags": ["GLEW", "GLFW"]}, {"location": "course-c/47-gui/opengl/#matrices", "title": "Matrices", "text": "

    Le risque avec l'OpenGL bas niveau c'est qu'\u00e0 un moment donn\u00e9 on se retrouve \u00e0 devoir g\u00e9rer des matrices de transformation et donc de faire des maths. Si nous voulons rendre notre exemple plus interfactif, nous pourrions par exemple ajouter une rotation au triangle, d'abord dans le plan \\(xy\\) puis plus tard en 3D.

    Pour m\u00e9moire, en math\u00e9mathique, un vecteur est une matrice de \\(n \\times 1\\) soit avec une seule colonne, tandiqu'un point est une matrice de \\(1 \\times n\\) soit avec une seule ligne. La matrice qui n'a pas d'effet sur un vecteur est la matrice identit\u00e9. La matrice identit\u00e9 est une matrice carr\u00e9e de taille \\(n \\times n\\) avec des \\(1\\) sur la diagonale principale et des \\(0\\) ailleurs. La matrice identit\u00e9 est not\u00e9e \\(I_n\\). En 3D nous avons donc\u2009:

    \\[ I_3 = \\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 1 \\end{bmatrix} \\]

    Une matrice de rotation en 3D est une matrice qui permet de faire tourner un vecteur autour d'un axe. Il existe trois types de rotation en 3D\u2009: la rotation autour de l'axe \\(x\\), la rotation autour de l'axe \\(y\\) et la rotation autour de l'axe \\(z\\). La rotation autour de l'axe \\(x\\) est donn\u00e9e par la matrice\u2009:

    \\[ R_x(\\theta) = \\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & \\cos(\\theta) & -\\sin(\\theta) \\\\ 0 & \\sin(\\theta) & \\cos(\\theta) \\end{bmatrix} \\]

    Ces deux matrices sont des matrices 3x3. Dans OpenGL on utilise des coordonn\u00e9es homog\u00e8nes pour les transformations g\u00e9om\u00e9triques. Une matrice de transformation homog\u00e8ne est une matrice 4x4 qui permet de faire des transformations affines et projectives en 3D. La matrice de rotation peut \u00eatre r\u00e9\u00e9crite de la forme\u2009:

    \\[ R_x(\\theta) = \\begin{bmatrix} 1 & 0 & 0 & 0 \\\\ 0 & \\cos(\\theta) & -\\sin(\\theta) & 0 \\\\ 0 & \\sin(\\theta) & \\cos(\\theta) & 0 \\\\ 0 & 0 & 0 & 1 \\end{bmatrix} \\]

    On peut facilement calculer une matrice de rotation en impl\u00e9mentant une fonction en C\u2009:

    void identityMatrix(GLfloat matrix[16]) {\n   for (int i = 0; i < 16; i++) matrix[i] = 0;\n   matrix[0] = 1;\n   matrix[5] = 1;\n   matrix[10] = 1;\n   matrix[15] = 1;\n}\n\nvoid rotationMatrix(GLfloat matrix[16], GLfloat angle, GLfloat x, GLfloat y, GLfloat z) {\n   GLfloat c = cos(angle);\n   GLfloat s = sin(angle);\n   GLfloat t = 1 - c;\n   matrix[0] = x * x * t + c;\n   matrix[1] = x * y * t - z * s;\n   matrix[2] = x * z * t + y * s;\n   matrix[3] = 0;\n   matrix[4] = y * x * t + z * s;\n   matrix[5] = y * y * t + c;\n   matrix[6] = y * z * t - x * s;\n   matrix[7] = 0;\n   matrix[8] = x * z * t - y * s;\n   matrix[9] = y * z * t + x * s;\n   matrix[10] = z * z * t + c;\n   matrix[11] = 0;\n   matrix[12] = 0;\n   matrix[13] = 0;\n   matrix[14] = 0;\n   matrix[15] = 1;\n}\n

    Une mani\u00e8re plus simple est de faire appel \u00e0 une biblioth\u00e8que de math\u00e9matiques comme CGLM que vous pouvez installer avec la commande suivante\u2009:

    sudo apt-get install libcglm-dev\n
    ", "tags": ["CGLM"]}, {"location": "course-c/47-gui/opengl/#model-view-projection", "title": "Model View Projection", "text": "

    La matrice de transformation homog\u00e8ne la plus courante est la matrice de transformation Model View Projection ou MVP. La matrice Model est la matrice qui transforme les coordonn\u00e9es du mod\u00e8le en coordonn\u00e9es du monde. La matrice View est la matrice qui transforme les coordonn\u00e9es du monde en coordonn\u00e9es de la cam\u00e9ra. La matrice Projection est la matrice qui transforme les coordonn\u00e9es de la cam\u00e9ra en coordonn\u00e9es de l'\u00e9cran.

    Souvenez-vous dans Futurama lorsque Cubert Farnsworth r\u00e9alise comment fonctionne les moteurs du vaisseau spatial de son p\u00e8re. Il dit\u2009:

    I understand how the engines work now. It came to me in a dream. The engines don't move the ship at all. The ship stays where it is and the engines move the universe around it.

    En fran\u00e7ais

    J'ai compris comment fonctionnent les moteurs maintenant. C'est venu \u00e0 moi dans un r\u00eave. Les moteurs ne d\u00e9placent pas le vaisseau du tout. Le vaisseau reste o\u00f9 il est et les moteurs d\u00e9placent l'univers autour de lui.

    En OpenGL il n'y pas de cam\u00e9ra qui se d\u00e9place, de longueur focale ou de distance de vue. C'est l'univers qui se d\u00e9place autour de la cam\u00e9ra. Cette derni\u00e8re reste fixe avec ses coordonn\u00e9es \\((0, 0, 0)\\) et son orientation \\((0, 0, -1)\\).

    "}, {"location": "course-c/47-gui/opengl/#transformation", "title": "Transformation", "text": "

    Ce n'est pas tr\u00e8s joli d'avoir chaque sommet du triangle coll\u00e9 au bord de la fen\u00eatre. Nous allons donc d\u00e9placer le triangle au centre de la fen\u00eatre. Pour cela nous allons utiliser une matrice de transformation. On peut d\u00e9finir par exemple que le triangle fera 70% de la hauteur de la fen\u00eatre et son rapport largeur/hauteur sera de 1.0. Vous l'avez compris on utilisera une matrice de transformation homog\u00e8ne qui sera appliqu\u00e9e par le vertex shader \u00e0 chaque sommet du triangle.

    Un autre probl\u00e8me est que les dimensions de la fen\u00eatre viewport peuvent changer si l'utilisateur d\u00e9cide de la redimensionner. Or, comme les coordonn\u00e9es des sommets du triangle sont normalis\u00e9es, dans le cas d'une fen\u00eatre deux fois plus large que haute. \\(1.0\\) en \\(x\\) sera deux fois plus grand que \\(1.0\\) en \\(y\\).

    Pour \u00e9viter cela, une mani\u00e8re est d'utiliser une matrice de projection orthographique homog\u00e8ne d\u00e9finie comme\u2009:

    \\[ \\begin{bmatrix} \\frac{2}{width} & 0 & 0 & 0 \\\\ 0 & \\frac{2}{height} & 0 & 0 \\\\ 0 & 0 & -1 & 0 \\\\ -1 & -1 & 0 & 1 \\end{bmatrix} \\]

    Pour se faire on aura besoin de modifier le vertex shader pour qu'il prenne en compte une matrice de transformation\u2009:

    const char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"uniform mat4 model;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = model * vec4(position, 1.0);\\n\"\n    \"}\\n\";\n
    "}, {"location": "course-c/47-gui/sdl/", "title": "SDL", "text": "

    SDL (Simple DirectMedia Layer) est une biblioth\u00e8que multiplateforme qui permet de cr\u00e9er des applications graphiques. Elle est utilis\u00e9e dans de nombreux jeux vid\u00e9o et applications multim\u00e9dia. Elle est \u00e9crite en C, mais des bindings existent pour de nombreux langages, dont Python ou C++.

    La biblith\u00e8que a \u00e9t\u00e9 cr\u00e9\u00e9e en 1998 par Sam Lantinga, un d\u00e9veloppeur alors employ\u00e9 par Loki Software, une soci\u00e9t\u00e9 sp\u00e9cialis\u00e9e dans le portage de jeux vid\u00e9o sur Linux. \u00c0 l\u2019\u00e9poque, il \u00e9tait difficile pour les d\u00e9veloppeurs de cr\u00e9er des jeux multiplateformes, car chaque syst\u00e8me d'exploitation (Windows, Linux, macOS) utilisait des biblioth\u00e8ques et des API diff\u00e9rentes pour g\u00e9rer les graphismes, le son et les entr\u00e9es. SDL a \u00e9t\u00e9 d\u00e9velopp\u00e9e pour offrir une interface unique et simple permettant d'acc\u00e9der \u00e0 ces fonctionnalit\u00e9s sur plusieurs plateformes, facilitant ainsi le d\u00e9veloppement de jeux et d'applications multim\u00e9dias multiplateformes.

    On peut citer quelques projets qui utilisent SDL.

    • The Battle for Wesnoth
    • Faster Than Light
    • Doom 3
    • ScummVM

    La plupart de ces projets sont d\u00e9velopp\u00e9s en C++ car le C n'est pas un langage tr\u00e8s adapt\u00e9 pour le d\u00e9veloppement d'applications complexes et graphiques.

    La biblioth\u00e8que est plus qu'une simple abstraction des fonctionnalit\u00e9s graphiques des syst\u00e8mes d'exploitation. Elle offre \u00e9galement des fonctionnalit\u00e9s notament\u2009:

    • Gestion des fen\u00eatres
    • Gestion des \u00e9v\u00e9nements (clavier, souris, joystick)
    • Gestion du son
    • Multi-threading
    • Timers
    • Acc\u00e9l\u00e9ration 2D
    • Support 3D (Vulkan, Metal, OpenGL)

    La biblioth\u00e8que est plus compl\u00e8te que GLFW et est bien adapt\u00e9e \u00e0 des projets complexes en C.

    "}, {"location": "course-c/47-gui/sdl/#premiers-pas", "title": "Premiers pas", "text": "

    Pour installer SDL sur votre syst\u00e8me, vous pouvez utiliser le gestionnaire de paquets de votre distribution. Sous Ubuntu\u2009:

    sudo apt install libsdl2-dev \"libsdl2-*\"\n

    Une extension de SDL nomm\u00e9e GFX est \u00e9galement disponible. Elle ajoute des fonctionnalit\u00e9s graphiques suppl\u00e9mentaires comme des fonctions de dessin de primitives (lignes, rectangles, cercles, etc.).

    #include <SDL2/SDL.h>\n#include <SDL2/SDL2_gfxPrimitives.h>\n#include <stdbool.h>\n#include <stdio.h>\n\n#define S(i) (sin(2 * M_PI / 3 * i))\n#define C(i) (cos(2 * M_PI / 3 * i))\n\nint main(int argc, char *argv[]) {\n   if (SDL_Init(SDL_INIT_VIDEO) < 0) {\n      fprintf(stderr, \"Error: SDL Initialization: %s\\n\", SDL_GetError());\n      exit(1);\n   }\n   SDL_Window *window =\n       SDL_CreateWindow(\"Circles\", SDL_WINDOWPOS_CENTERED,\n                        SDL_WINDOWPOS_CENTERED, 800, 400, SDL_WINDOW_SHOWN);\n   if (!window) {\n      fprintf(stderr, \"Error: CreateWindow: %s\\n\", SDL_GetError());\n      exit(2);\n   }\n   SDL_Renderer *r = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n   if (!r) {\n      fprintf(stderr, \"Error: Renderer: %s\\n\", SDL_GetError());\n      exit(3);\n   }\n   SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_ADD);\n   bool running = true;\n   while (running) {\n      SDL_Event event;\n      while (SDL_PollEvent(&event))\n         if (event.type == SDL_QUIT) running = false;\n      SDL_SetRenderDrawColor(r, 255, 255, 255, 255);\n      SDL_RenderClear(r);\n      Sint16 cx = 400, cy = 200, d = 50;\n      filledCircleRGBA(r, cx + d * C(1), cy + d * S(1), 100, 255, 0, 0, 100);\n      filledCircleRGBA(r, cx + d * C(2), cy + d * S(2), 100, 0, 255, 0, 100);\n      filledCircleRGBA(r, cx + d * C(3), cy + d * S(3), 100, 0, 0, 255, 100);\n      SDL_RenderPresent(r);\n   }\n   SDL_DestroyRenderer(r);\n   SDL_DestroyWindow(window);\n   SDL_Quit();\n}\n

    Intersections de cercles

    "}, {"location": "course-c/47-gui/sdl/#polygones", "title": "Polygones", "text": "

    Voici un exemple d'un programme de dessin de polygones en utilisant SDL.

    Programme de dessin de polygones

    #include <SDL2/SDL.h>\n#include <SDL2/SDL2_gfxPrimitives.h>  // Inclure SDL2_gfx\n#include <math.h>\n#include <stdbool.h>\n#include <stdio.h>\n\n#define GRIDSTEP 10\n#define MAX_POINTS 100\nconst int WINDOW_WIDTH = 800, WINDOW_HEIGHT = 400;\n\nconst SDL_Color GRID_COLOR = {80, 80, 80, 200};\nconst SDL_Color BACKGROUND_COLOR = {30, 30, 30, 255};\nconst SDL_Color LINE_COLOR = {255, 0, 128, 255};\nconst SDL_Color ANCHOR_COLOR = {0, 255, 255, 150};\n\ntypedef struct Point {\n   float x, y;\n} Point;\n\ntypedef struct Segment {\n   Point p, q;\n} Segment;\n\ntypedef struct State {\n   bool drawing;\n   Point start, last, current;\n   size_t segment_count;\n   Segment segments[MAX_POINTS];\n} State;\n\n#define COLORARGS(color) color.r, color.g, color.b, color.a\n\nvoid clear(SDL_Renderer *renderer) {\n   SDL_SetRenderDrawColor(renderer, COLORARGS(BACKGROUND_COLOR));\n   SDL_RenderClear(renderer);\n}\n\nvoid drawGrid(SDL_Renderer *renderer) {\n   for (int x = 0; x < WINDOW_WIDTH; x += GRIDSTEP) {\n      SDL_Color color = GRID_COLOR;\n      color.a = x % 100 ? 100 : 200;\n      SDL_SetRenderDrawColor(renderer, COLORARGS(color));\n      SDL_RenderDrawLine(renderer, x, 0, x, WINDOW_HEIGHT);\n   }\n   for (int y = 0; y < WINDOW_HEIGHT; y += GRIDSTEP) {\n      SDL_Color color = GRID_COLOR;\n      color.a = y % 100 ? 100 : 200;\n      SDL_SetRenderDrawColor(renderer, COLORARGS(color));\n      SDL_RenderDrawLine(renderer, 0, y, WINDOW_WIDTH, y);\n   }\n}\n\nvoid drawAnchor(SDL_Renderer *r, Point p, SDL_Color color) {\n   const int size = 6, half = size / 2;\n   SDL_SetRenderDrawColor(r, COLORARGS(color));\n   SDL_RenderFillRect(r, &(SDL_Rect){p.x - half, p.y - half, size, size});\n}\n\nvoid drawSegment(SDL_Renderer *r, Segment s) {\n   SDL_SetRenderDrawColor(r, COLORARGS(LINE_COLOR));\n   SDL_RenderDrawLine(r, s.p.x, s.p.y, s.q.x, s.q.y);\n   drawAnchor(r, s.p, ANCHOR_COLOR);\n   drawAnchor(r, s.q, ANCHOR_COLOR);\n}\n\nvoid drawSegments(SDL_Renderer *renderer, Segment *segments, size_t count) {\n   for (int i = 0; i < count; i++) drawSegment(renderer, segments[i]);\n}\n\nvoid addSegment(State *state) {\n   if (state->segment_count >= MAX_POINTS) {\n      fprintf(stderr, \"Reached maximum number of points\\n\");\n      return;\n   }\n   state->segments[state->segment_count++] =\n       (Segment){.p = state->last, .q = state->current};\n}\n\nvoid endSegment(State *state) {\n   state->drawing = false;\n   addSegment(state);\n}\n\nbool isClose(Point p, Point q, int threshold) {\n   return (fabs(p.x - q.x) < threshold) && (fabs(p.y - q.y) < threshold);\n}\n\nbool onStartAnchor(State state) {\n   return isClose(state.start, state.current, 10);\n}\n\nint main(int argc, char *argv[]) {\n   SDL_Init(SDL_INIT_VIDEO);\n   SDL_Window *window = SDL_CreateWindow(\"Polygons\", SDL_WINDOWPOS_CENTERED,\n                                         SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH,\n                                         WINDOW_HEIGHT, SDL_WINDOW_SHOWN);\n   SDL_Renderer *renderer =\n       SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n\n   SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);\n   State state = {0};\n   bool running = true;\n   while (running) {\n      SDL_Event event;\n      while (SDL_PollEvent(&event)) {\n         if (event.type == SDL_QUIT) {\n            running = false;\n            continue;\n         }\n         if (event.type == SDL_MOUSEMOTION) {\n            state.current = (Point){event.button.x, event.button.y};\n            continue;\n         }\n         if (event.type == SDL_MOUSEBUTTONDOWN &&\n             event.button.button == SDL_BUTTON_LEFT) {\n            if (!state.drawing) {\n               state.drawing = true;\n               state.start = state.current;\n            } else if (onStartAnchor(state)) {\n               state.current = state.start;\n               endSegment(&state);\n            } else {\n               addSegment(&state);\n            }\n            state.last = state.current;\n         }\n      }\n      clear(renderer);\n      drawGrid(renderer);\n      drawSegments(renderer, state.segments, state.segment_count);\n      if (state.start.x >= 0 && state.start.y >= 0)\n         drawAnchor(renderer, state.start, ANCHOR_COLOR);\n      if (state.drawing)\n         drawSegment(renderer, (Segment){.p = state.last, .q = state.current});\n      SDL_RenderPresent(renderer);\n   }\n   SDL_DestroyRenderer(renderer);\n   SDL_DestroyWindow(window);\n   SDL_Quit();\n}\n
    "}, {"location": "course-c/47-gui/window/", "title": "Fen\u00eatre graphique", "text": "

    Comme nous l'avons vu, un programme informatique standard dispose de plusieurs flux\u2009: un flux d'entr\u00e9e (stdin) et deux flux de sortie (stdout et stderr). Ces flux sont g\u00e9n\u00e9ralement associ\u00e9s \u00e0 la console texte, mais ne sont pas directement reli\u00e9s \u00e0 une fen\u00eatre graphique permettant des interactions visuelles. Pourtant, de nombreuses applications modernes proposent une interface graphique pour permettre une meilleure interaction avec l'utilisateur.

    Sous Windows comme sous Linux, l'interface utilisateur graphique, compos\u00e9e de fen\u00eatres, de menus et d'autres \u00e9l\u00e9ments interactifs, est g\u00e9r\u00e9e par ce que l'on appelle un window manager. Ce gestionnaire de fen\u00eatres est responsable de la cr\u00e9ation, de la gestion et de la disposition des diff\u00e9rentes fen\u00eatres \u00e0 l'\u00e9cran. Il agit comme une couche interm\u00e9diaire entre le noyau du syst\u00e8me d'exploitation et les applications graphiques.

    Le window manager est un processus distinct qui fonctionne en parall\u00e8le de votre programme. Il est donc essentiel, d'une part, qu'il soit d\u00e9j\u00e0 lanc\u00e9 avant l'ex\u00e9cution de votre application (ce qui est g\u00e9n\u00e9ralement le cas dans un environnement de bureau standard) et, d'autre part, que votre programme soit capable de communiquer avec ce gestionnaire de mani\u00e8re appropri\u00e9e. Sous Linux, cette communication se fait le plus souvent par l'interm\u00e9diaire le protocole X11 ou plus r\u00e9emment Wayland, \u00e9galement connu sous le nom de \u00ab\u2009X Window System\u2009\u00bb. Sous Windows c'est l'API Win32 qui est utilis\u00e9e mais des alternatives comme GTK ou Qt sont \u00e9galement disponibles via MinGW ou MSYS.

    ", "tags": ["stdin", "stdout", "stderr"]}, {"location": "course-c/47-gui/window/#le-protocole-x11", "title": "Le protocole X11", "text": "

    X11 est un protocole r\u00e9seau qui permet \u00e0 un programme de dessiner des fen\u00eatres sur l'\u00e9cran, qu'il soit local ou distant. Il a la particularit\u00e9 d'\u00eatre ind\u00e9pendant de la machine o\u00f9 l'application s'ex\u00e9cute, permettant ainsi de contr\u00f4ler graphiquement des applications sur d'autres ordinateurs. Cependant, interagir directement avec X11 est un processus complexe, car cela implique de g\u00e9rer de nombreux param\u00e8tres relatifs aux fen\u00eatres\u2009: dimensions, position, gestion des \u00e9v\u00e9nements, et autres aspects li\u00e9s \u00e0 l'interaction utilisateur.

    Le protocole X11 est bas\u00e9 sur un mod\u00e8le client-serveur. Un programme se connecte donc au serveur en utilisant des sockets, typiquement des sockets Unix (Unix Domain Socket). Ce socket est un canal de communication avec le serveur dans lequel des ordre du type \u00ab\u2009dessine-moi un rectangle \u00e0 telle position\u2009\u00bb ou \u00ab\u2009affiche ce texte\u2009\u00bb sont envoy\u00e9s. Le serveur X11 est responsable de la gestion des fen\u00eatres, des \u00e9v\u00e9nements, des polices, des couleurs, etc.

    Comme la communication passe par les sockets, il est possible de profiter de l'encapsulation TCP/IP et de dialoguer avec un server distant via internet ou un r\u00e9seau local. Avant l'av\u00e8nement WSLg (Windows Subsystem for Linux GUI), il \u00e9tait possible d'afficher des fen\u00eatres graphiques Linux en utilisant un serveur X11 install\u00e9 directement sous Windows comme VcXsrv. D'ailleurs si vous utilisez Docker, ou que vous vous connectez \u00e0 un Raspberry Pi, vous pouvez \u00e9galement profiter d'une interface graphique depuis une connection SSH par exemple (via l'option -X).

    Pour illustrer la complexit\u00e9 de l'usage direct de X11, voici un exemple de programme en C qui ouvre une simple fen\u00eatre de 100 pixels par 100 pixels\u2009:

    #include <X11/Xlib.h>\n#include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   Display *d = XOpenDisplay(NULL);\n   if (d == NULL) {\n      fprintf(stderr, \"Impossible d'ouvrir le display\\n\");\n      exit(1);\n   }\n\n   // Create window\n   int s = DefaultScreen(d);\n   Window w = XCreateSimpleWindow(d, RootWindow(d, s), 10, 10,  // x, y\n                                  100, 100,                     // width, height\n                                  1,                            // border width\n                                  BlackPixel(d, s), WhitePixel(d, s));\n\n   XSelectInput(d, w, ExposureMask | KeyPressMask);  // Select event to watch\n   XMapWindow(d, w);                                 // Display the window\n\n   // Event loop\n   XEvent e;\n   do {\n      XNextEvent(d, &e);\n      if (e.type == Expose)\n         XFillRectangle(d, w, DefaultGC(d, s), 20, 20, 10, 10);\n   } while (e.type != KeyPress);\n\n   XCloseDisplay(d);\n}\n

    Pour compiler et ex\u00e9cuter ce programme vous aurez besoin de la biblioth\u00e8que X11. Sous Ubuntu, vous pouvez l'installer avec la commande suivante\u2009:

    sudo apt-get install libx11-dev\n

    Ensuite, vous pouvez compiler le programme avec la commande suivante\u2009:

    gcc window.c -lX11\n

    Le programme ainsi g\u00e9n\u00e9r\u00e9 ouvre une fen\u00eatre minimaliste, mais il est \u00e9vident qu'impl\u00e9menter des interfaces graphiques sophistiqu\u00e9es en utilisant uniquement X11 est un travail consid\u00e9rable. La gestion manuelle des \u00e9v\u00e9nements, des composants graphiques et des interactions utilisateurs devient rapidement fastidieuse. D'autre part ce protocol n'est pas portable, il est sp\u00e9cifique \u00e0 Linux et n'est pas disponible sur d'autres syst\u00e8mes d'exploitation.

    "}, {"location": "course-c/47-gui/window/#wayland", "title": "Wayland", "text": "

    Wayland est un protocole graphique plus r\u00e9cent que X11, con\u00e7u pour remplacer ce dernier en tant que gestionnaire de fen\u00eatres par d\u00e9faut sous Linux. Wayland est plus moderne, plus s\u00e9curis\u00e9 et plus performant que X11, mais il est \u00e9galement plus restrictif en termes de fonctionnalit\u00e9s. Wayland est con\u00e7u pour \u00eatre plus simple en limitant l'acc\u00e8s des applications aux ressources syst\u00e8me et en isolant les processus les uns des autres.

    "}, {"location": "course-c/47-gui/window/#lapi-win32", "title": "L'API Win32", "text": "

    Sous Windows, l'\u00e9quivalent de X11 est l'API Win32, qui permet de cr\u00e9er des applications graphiques pour le syst\u00e8me d'exploitation de Microsoft. L'API Win32 est une interface de programmation d'applications (API) bas\u00e9e sur des messages, qui permet de cr\u00e9er des fen\u00eatres, des contr\u00f4les, des menus, et d'autres \u00e9l\u00e9ments d'interface utilisateur. L'API Win32 est plus complexe que X11, mais elle offre des fonctionnalit\u00e9s plus avanc\u00e9es et une meilleure int\u00e9gration avec le syst\u00e8me d'exploitation. Voici \u00e0 titre d'exemple le m\u00eame programme que pr\u00e9c\u00e9demment, mais cette fois-ci en utilisant l'API Win32\u2009:

    #include <windows.h>\n\nLRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wParam,\n                                 LPARAM lParam) {\n   switch (msg) {\n      case WM_DESTROY:\n         PostQuitMessage(0);\n         break;\n      default:\n         return DefWindowProc(hwnd, msg, wParam, lParam);\n   }\n   return 0;\n}\n\nint WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,\n                   LPSTR lpCmdLine, int nCmdShow) {\n   // D\u00e9finition de la classe de fen\u00eatre\n   WNDCLASS wc = {0};\n   wc.lpfnWndProc = WindowProcedure;  // Fonction callback pour les messages\n   wc.hInstance = hInstance;\n   wc.lpszClassName = \"MaFenetreClass\";\n\n   if (!RegisterClass(&wc)) {\n      return -1;\n   }\n\n   // Cr\u00e9ation de la fen\u00eatre\n   HWND hwnd = CreateWindowEx(0, \"MaFenetreClass\", \"Ma Fenetre\",\n                              WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,\n                              100, 100, NULL, NULL, hInstance, NULL);\n\n   if (hwnd == NULL) {\n      return -1;\n   }\n\n   ShowWindow(hwnd, nCmdShow);\n   UpdateWindow(hwnd);\n\n   // Event loop\n   MSG msg = {0};\n   while (GetMessage(&msg, NULL, 0, 0)) {\n      TranslateMessage(&msg);\n      DispatchMessage(&msg);\n   }\n\n   return msg.wParam;\n}\n

    Pour compiler ce programme, vous pouvez utiliser le compilateur MinGW (Minimalist GNU for Windows) qui fournit les outils n\u00e9cessaires pour compiler des programmes C sous Windows. Vous pouvez installer MinGW via le gestionnaire de paquets MSYS2, qui fournit un environnement de d\u00e9veloppement similaire \u00e0 celui de Linux.

    gcc window.c -lgdi32\n

    Alternativement, vous pouvez utiliser l'environnement officiel de d\u00e9veloppement de Microsoft Visual Studio, qui fournit un ensemble complet d'outils pour le d\u00e9veloppement d'applications Windows mais son installation est plus lourde et d\u00e9passe le cadre de ce cours.

    "}, {"location": "course-c/47-gui/window/#gtk", "title": "GTK", "text": "

    GUI avec GTK

    "}, {"location": "course-c/47-gui/window/#rendu-logiciel-ou-materiel", "title": "Rendu logiciel ou mat\u00e9riel", "text": "

    Les premiers ordinateurs personnels, comme le Macintosh d'Apple ou l'Amiga d'Atari, utilisaient un rendu graphique purement logiciel. Cela signifie que le processeur central (CPU) \u00e9tait responsable de dessiner les pixels \u00e0 l'\u00e9cran, en calculant les couleurs, les textures, les ombres, et autres effets visuels. Historiquement avant les syst\u00e8mes d'exploitation multit\u00e2ches, pour afficher un pixel \u00e0 l'\u00e9cran, il suffisait ou presque d'\u00e9crire une valeur dans la m\u00e9moire vid\u00e9o.

    Depuis ces ages imm\u00e9moriaux, les syst\u00e8mes d'exploitations sont devenus beaucoup plus restrictifs et n'autorisent plus l'acc\u00e8s direct \u00e0 la m\u00e9moire vid\u00e9o. D'ailleurs les cartes graphiques modernes fonctionnent tr\u00e8s diff\u00e9remment. N\u00e9anmoins beaucoup de biblioth\u00e8ques graphiques de haut niveau comme GTK, Qt, SDL ou Allegro peuvent encore utiliser un rendu logiciel pour dessiner certains \u00e9l\u00e9ments graphiques \u00e0 l'\u00e9cran. Plut\u00f4t qu'acc\u00e9der \u00e0 la m\u00e9moire vid\u00e9o ces biblioth\u00e8ques utilisent un framebuffer. Il s'agit d'une zone de m\u00e9moire correspondant \u00e0 l'int\u00e9rieur de la fen\u00eatre graphique o\u00f9 les pixels sont dessin\u00e9s. Une fois que le dessin est termin\u00e9, le contenu du framebuffer est copi\u00e9 dans la m\u00e9moire vid\u00e9o par le window manager.

    Le rendu mat\u00e9riel par opposition n'est pas g\u00e9r\u00e9 par le CPU mais par la carte graphique. Si vous \u00e9tiez paysan au moyen-\u00e2ge pour planter une carottes, vous retroussiez vos manches, preniez votre b\u00eache et en avant la besogne. Vous auriez \u00e9t\u00e9 dans le champ (m\u00e9moire vid\u00e9o) pour y planter (allumer) une carotte (pixel). Une carte graphique c'est des milliers de processeurs, c'est une entreprise colossale plus proche des 12 travaux d'Asterix que de la culture ancestrale de la carotte. Coordonner les bonnes personne pour qu'elles aille pour vous planter une carotte \u00e0 l'endroit souhait\u00e9 c'est forc\u00e9ment plus compliqu\u00e9. Le rendu mat\u00e9riel c'est ordonner \u00e0 la carte graphique de planter les carottes \u00e0 votre place. Pendant ce temps vous pouvez faire autre chose. C'est plus rapide, plus efficace, mais forc\u00e9ment plus complexe.

    Le rendu mat\u00e9riel est particuli\u00e8rement plus efficace lorsque des effets graphiques sont utilis\u00e9s (animations 3D, effets d'ombres ou de flous, etc.). L'exemple suivant utlise le rendu mat\u00e9riel pour animer une th\u00e9i\u00e8re en 3D\u2009:

    #include <GL/glut.h>\n#include <math.h>\n\nfloat angle = 0.0f;\n\nvoid display() {\n   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n   glLoadIdentity();\n   gluLookAt(1.0f, 2.0f, 5.0f,   // Position de la cam\u00e9ra\n             0.0f, 0.0f, 0.0f,   // Point de mire\n             0.0f, 1.0f, 0.0f);  // Vecteur \"up\"\n\n   glLightfv(GL_LIGHT0, GL_POSITION, (GLfloat[]){1.0f, 1.0f, 1.0f, 0.0f});\n   glRotatef(angle, 0.0f, 1.0f, 0.0f);  // Rotation autour de l'axe Y\n   glColor3f(0.8f, 0.2f, 0.2f);\n   glutSolidTeapot(1.0);\n   glutSwapBuffers();\n}\n\nvoid reshape(int width, int height) {\n   if (height == 0) height = 1;\n   glViewport(0, 0, width, height);\n   glMatrixMode(GL_PROJECTION);\n   glLoadIdentity();\n   gluPerspective(45.0f, (float)width / (float)height, 1.0f, 100.0f);\n   glMatrixMode(GL_MODELVIEW);\n}\n\nvoid initOpenGL() {\n   glEnable(GL_DEPTH_TEST);\n   glEnable(GL_LIGHTING);\n   glEnable(GL_LIGHT0);\n   glShadeModel(GL_SMOOTH);\n   glClearColor(0.0f, 0.0f, 0.0f, 1.0f);\n}\n\nvoid update(int value) {\n   if ((angle += 2.0f) > 360) angle -= 360;\n   glutPostRedisplay();\n   glutTimerFunc(16 /* ms */, update, 0);\n}\n\nint main(int argc, char** argv) {\n   glutInit(&argc, argv);\n   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);\n   glutInitWindowSize(800, 400);\n   glutCreateWindow(\"Animated Teapot with Shadows\");\n   initOpenGL();\n   glutDisplayFunc(display);\n   glutReshapeFunc(reshape);\n   glutTimerFunc(16, update, 0);\n   glutMainLoop();\n}\n

    Th\u00e9i\u00e8re en 3D

    La biblioth\u00e8que Cairo est un exemple de biblioth\u00e8que de rendu 2D qui peut fonctionner en mode logiciel ou mat\u00e9riel. Cairo est utilis\u00e9 par GTK pour le rendu graphique de ses composants. Il est \u00e9galement utilis\u00e9 par d'autres applications comme Inkscape, Firefox, et WebKit. Voici l'exemple d'un programme qui trace une ligne noir sous la souris\u2009:

    #include <gtk/gtk.h>\n\n#define WIDTH 800\n#define HEIGHT 400\n\ngboolean on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data) {\n   cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);  // White\n   cairo_paint(cr);\n   return FALSE;\n}\n\ngboolean on_mouse_move(GtkWidget *widget, GdkEventMotion *event, gpointer d) {\n   static int last_x = -1, last_y = -1;\n   cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(widget));\n   int x = (int)event->x, y = (int)event->y;\n   if (last_x != -1 && last_y != -1) {\n      cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);\n      cairo_move_to(cr, last_x, last_y);\n      cairo_line_to(cr, x, y);\n      cairo_stroke(cr);\n   }\n   last_x = x;\n   last_y = y;\n   cairo_destroy(cr);\n   return TRUE;  // Stop event propagation\n}\n\nint main(int argc, char *argv[]) {\n   gtk_init(&argc, &argv);\n   GtkWidget *w = gtk_window_new(GTK_WINDOW_TOPLEVEL);\n\n   gtk_window_set_title(GTK_WINDOW(w), \"Frame Buffer Example\");\n   gtk_window_set_default_size(GTK_WINDOW(w), WIDTH, HEIGHT);\n\n   GdkWindow *gdk_window = gtk_widget_get_window(w);\n   GdkDisplay *display = gdk_window_get_display(gdk_window);\n   GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_ARROW);\n   gdk_window_set_cursor(gdk_window, cursor);\n   g_object_unref(cursor);\n\n   g_signal_connect(w, \"destroy\", G_CALLBACK(gtk_main_quit), NULL);\n   g_signal_connect(w, \"draw\", G_CALLBACK(on_draw_event), NULL);\n   g_signal_connect(w, \"motion-notify-event\", G_CALLBACK(on_mouse_move), NULL);\n   gtk_widget_add_events(w, GDK_POINTER_MOTION_MASK);\n   gtk_widget_show_all(w);\n   gtk_main();\n}\n

    Pour compiler ce programme vous aurez besoin de la biblioth\u00e8que GTK. Or cette bibloth\u00e8que a de nombreuses d\u00e9pendances (pango, glib, harfbuzz, freetype, libpng, webp, at...). Pkg-config se r\u00e9v\u00e8le tr\u00e8s utile pour r\u00e9cup\u00e9rer les flags de compilation et de liens. Voici comment compiler ce programme\u2009:

    gcc `pkg-config --cflags gtk+-3.0` x.c `pkg-config --libs gtk+-3.0`\n

    Rien de sorcier, les backticks permettent d'ex\u00e9cuter des sous commandes. On ex\u00e9cute donc pkg-config deux fois. Une pour r\u00e9cup\u00e9rer les drapeaux de compilation et l'autre pour r\u00e9cup\u00e9rer les biblioth\u00e8ques \u00e0 lier. On aurait bien entendu pu le faire \u00e0 la main, mais vous conviendrez que c'est plus simple avec pkg-config.

    gcc -I/usr/include/gtk-3.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0\n-I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/harfbuzz\n-I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount\n-I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo\n-I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0\n-I/usr/include/x86_64-linux-gnu -I/usr/include/webp -I/usr/include/gio-unix-2.0\n-I/usr/include/atk-1.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0\n-I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -pthread\nx.c -lgtk-3 -lgdk-3 -lz -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0\n-lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0\n

    Hello trac\u00e9 avec la souris

    "}, {"location": "course-c/47-gui/window/#bibliotheques-graphiques-de-haut-niveau", "title": "Biblioth\u00e8ques graphiques de haut niveau", "text": "

    Pour faciliter le d\u00e9veloppement d'interfaces graphiques, des biblioth\u00e8ques de plus haut niveau ont \u00e9t\u00e9 cr\u00e9\u00e9es. Celles-ci encapsulent les d\u00e9tails complexes du protocole X11 et fournissent des composants graphiques pr\u00eats \u00e0 l'emploi, comme des boutons, des champs de texte, des bo\u00eetes de dialogue, et bien plus.

    Sous Linux, l'une des biblioth\u00e8ques graphiques les plus populaires est GTK (GIMP Toolkit). GTK est une biblioth\u00e8que open source \u00e9crite en C, largement utilis\u00e9e pour d\u00e9velopper des interfaces graphiques multiplateformes. Elle est notamment \u00e0 la base de l'environnement de bureau GNOME et est utilis\u00e9e dans des logiciels libres majeurs tels que GIMP et Inkscape.

    GTK simplifie \u00e9norm\u00e9ment la cr\u00e9ation d'interfaces graphiques gr\u00e2ce \u00e0 une large gamme de widgets pr\u00eats \u00e0 l'emploi et \u00e0 une gestion int\u00e9gr\u00e9e des \u00e9v\u00e9nements utilisateurs (comme les clics de souris ou les frappes clavier). Par exemple, cr\u00e9er une fen\u00eatre avec des boutons, des listes d\u00e9roulantes, ou des champs de texte est beaucoup plus simple et rapide qu'avec X11 pur.

    #include <gtk/gtk.h>\n\nstatic void on_activate(GtkApplication *app, gpointer user_data) {\n   GtkWidget *window = gtk_application_window_new(app);\n   gtk_window_set_title(GTK_WINDOW(window), \"Exemple GTK4\");\n   gtk_window_set_default_size(GTK_WINDOW(window), 100, 100);\n   gtk_window_present(GTK_WINDOW(window));\n}\n\nint main(int argc, char **argv) {\n   GtkApplication *app =\n       gtk_application_new(\"org.gtk.example\", G_APPLICATION_DEFAULT_FLAGS);\n   g_signal_connect(app, \"activate\", G_CALLBACK(on_activate), NULL);\n   int status = g_application_run(G_APPLICATION(app), argc, argv);\n   g_object_unref(app);\n\n   return status;\n}\n

    Cet exemple montre \u00e0 quel point GTK rend les choses plus simples\u2009: une seule fonction pour ouvrir une fen\u00eatre avec des param\u00e8tres de base.

    "}, {"location": "course-c/47-gui/window/#autres-bibliotheques-graphiques", "title": "Autres biblioth\u00e8ques graphiques", "text": "

    Selon les besoins de votre application, d'autres biblioth\u00e8ques graphiques peuvent \u00eatre plus adapt\u00e9es. Par exemple, Qt est une biblioth\u00e8que graphique populaire pour le d\u00e9veloppement d'applications multiplateformes, notamment en C++. Il s'agit davantage d'un framework complet et des outils tiers pour le design interactif d'interfaces graphiques. De base QT (prononc\u00e9 cute) est un framework objet, il n'est donc pas tr\u00e8s adapt\u00e9 \u00e0 la programmation en C.

    Les biblioth\u00e8ques les plus utilis\u00e9es pour la programmation graphique en C sont les suivantes\u2009:

    • SDL (Simple DirectMedia Layer)
    • Allegro
    "}, {"location": "course-c/47-gui/window/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    La plupart des biblioth\u00e8ques graphiques de haut niveau reposent sur les m\u00eames principes de base pour cr\u00e9er des interfaces graphiques interactives\u2009:

    1. Initialisation\u2009: la biblioth\u00e8que est initialis\u00e9e et les ressources n\u00e9cessaires sont allou\u00e9es.
    2. Cr\u00e9ation de la fen\u00eatre\u2009: une fen\u00eatre graphique est cr\u00e9\u00e9e avec les dimensions et les param\u00e8tres souhait\u00e9s.
    3. Cr\u00e9ation des composants graphiques\u2009: des widgets (boutons, champs de texte, etc.) sont ajout\u00e9s \u00e0 la fen\u00eatre.
    4. Une boucle principale est lanc\u00e9e pour g\u00e9rer les \u00e9v\u00e9nements utilisateur (clics de souris, frappes clavier, etc.) et mettre \u00e0 jour l'affichage en cons\u00e9quence. Les \u00e9v\u00e8nements arrivent dans une file d'attente et sont trait\u00e9s un par un. L'\u00e9cran est rafra\u00eechi \u00e0 chaque it\u00e9ration de la boucle pour afficher les changements.
    5. Lib\u00e9ration des ressources\u2009: une fois l'application termin\u00e9e, les ressources allou\u00e9es sont lib\u00e9r\u00e9es et la fen\u00eatre est ferm\u00e9e.

    Lorsque diff\u00e9rentes fen\u00eatres sont n\u00e9cessaires, le programme peut cr\u00e9er des contextes isol\u00e9s comme des threads pour g\u00e9rer les \u00e9v\u00e9nements de chaque fen\u00eatre. Cela permet de garder l'interface utilisateur r\u00e9active m\u00eame si une fen\u00eatre est bloqu\u00e9e par une op\u00e9ration longue.

    "}, {"location": "course-c/47-gui/window/#allegro", "title": "Allegro", "text": "

    Allegro est une biblioth\u00e8que graphique multiplateforme qui fournit des fonctionnalit\u00e9s pour la cr\u00e9ation de jeux vid\u00e9o et d'applications multim\u00e9dia. Elle est plus orient\u00e9e vers les jeux vid\u00e9o que les interfaces graphiques traditionnelles, mais elle peut \u00eatre utilis\u00e9e pour des applications plus g\u00e9n\u00e9rales. Allegro fournit des fonctionnalit\u00e9s pour la cr\u00e9ation de fen\u00eatres, la gestion des \u00e9v\u00e9nements utilisateur, le rendu graphique, la lecture de sons et de musiques, et bien plus encore. Elle est \u00e9crite en C et est compatible avec de nombreux syst\u00e8mes d'exploitation, y compris Windows, Linux, macOS, iOS et Android et elle est utilisable sur de nombreux langages de programmation, dont le C.

    Voici un exemple simple d'utilisation d'Allegro pour cr\u00e9er une fen\u00eatre graphique avec un cercle rouge qui rebondit sur les bords de la fen\u00eatre\u2009:

    Balle qui rebondit

    #include <allegro5/allegro.h>\n#include <allegro5/allegro_image.h>\n#include <allegro5/allegro_primitives.h>\n#include <stdio.h>\n\nconst float FPS = 60.0;\nconst int SCREEN_W = 640;\nconst int SCREEN_H = 480;\nconst int BALL_SIZE = 20;\n\nint main(int argc, char **argv) {\n   float ball_x = SCREEN_W / 2.0 - BALL_SIZE / 2.0;\n   float ball_y = SCREEN_H / 2.0 - BALL_SIZE / 2.0;\n   float ball_dx = -4.0, ball_dy = 4.0;\n\n   if (!al_init()) {\n      fprintf(stderr, \"Erreur d'initialisation d'Allegro.\\n\");\n      return -1;\n   }\n\n   al_init_primitives_addon();\n   al_install_keyboard();\n\n   ALLEGRO_TIMER *timer = al_create_timer(1.0 / FPS);\n   if (!timer) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation du timer.\\n\");\n      return -1;\n   }\n\n   ALLEGRO_DISPLAY *display = al_create_display(SCREEN_W, SCREEN_H);\n   if (!display) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation de la fen\u00eatre.\\n\");\n      al_destroy_timer(timer);\n      return -1;\n   }\n\n   ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();\n   if (!event_queue) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation de la file d'\u00e9v\u00e9nements.\\n\");\n      al_destroy_display(display);\n      al_destroy_timer(timer);\n      return -1;\n   }\n\n   al_register_event_source(event_queue, al_get_display_event_source(display));\n   al_register_event_source(event_queue, al_get_timer_event_source(timer));\n   al_register_event_source(event_queue, al_get_keyboard_event_source());\n\n   al_start_timer(timer);\n\n   bool redraw = true;\n   for (;;) {\n      ALLEGRO_EVENT ev;\n      al_wait_for_event(event_queue, &ev);\n\n      if (ev.type == ALLEGRO_EVENT_TIMER) {\n         // Move\n         ball_x += ball_dx;\n         ball_y += ball_dy;\n         // Bounce\n         if (ball_x <= 0 || ball_x >= SCREEN_W - BALL_SIZE) ball_dx = -ball_dx;\n         if (ball_y <= 0 || ball_y >= SCREEN_H - BALL_SIZE) ball_dy = -ball_dy;\n         redraw = true;\n      } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE)\n         break;\n\n      if (redraw && al_is_event_queue_empty(event_queue)) {\n         redraw = false;\n         al_clear_to_color(al_map_rgb(0, 0, 0));\n         al_draw_filled_circle(ball_x + BALL_SIZE / 2, ball_y + BALL_SIZE / 2,\n                               BALL_SIZE / 2, al_map_rgb(255, 0, 0));\n         al_flip_display();  // Refresh\n      }\n   }\n\n   // Free resources\n   al_destroy_timer(timer);\n   al_destroy_display(display);\n   al_destroy_event_queue(event_queue);\n}\n
    "}, {"location": "course-c/47-gui/window/#conclusion", "title": "Conclusion", "text": "

    En r\u00e9sum\u00e9, bien que X11 soit le fondement de l'affichage graphique sous Linux, son utilisation directe est rarement n\u00e9cessaire pour les d\u00e9veloppeurs modernes. Des biblioth\u00e8ques comme GTK permettent de se concentrer sur la logique de l'application et l'exp\u00e9rience utilisateur, tout en masquant les d\u00e9tails techniques sous-jacents. Il est recommand\u00e9 d'utiliser ces outils de plus haut niveau pour cr\u00e9er des interfaces graphiques compl\u00e8tes, flexibles et ergonomiques.

    "}, {"location": "course-c/48-network/", "title": "Introduction", "text": "

    Le r\u00e9seau est une sacr\u00e9e gal\u00e8re. C'est un domaine de l'ing\u00e9nierie en soi, et il n'est pas possible de tout couvrir en un seul chapitre. N\u00e9anmoins, je pense qu'il est essentiel \u00e0 tout ing\u00e9nieur qu'il soit \u00e9lectronicien ou informaticien d'avoir quelques notions de base sur le sujet. Cette partie se concentre sur le mod\u00e8le OSI, comprendre la pile de protocoles r\u00e9seau et se familiariser avec quelques protocoles. Ensuite, quelques exemples d'utilisations de sockets en C sont donn\u00e9s.

    "}, {"location": "course-c/48-network/applications/", "title": "Applications r\u00e9seau", "text": "

    Ce chapitre donne quelques exemples tr\u00e8s simples d'applications r\u00e9seau en C.

    "}, {"location": "course-c/48-network/applications/#serveur-web", "title": "Serveur Web", "text": "

    Le plus simple en C est d'utiliser la biblioth\u00e8que Mongoose qui est tr\u00e8s l\u00e9g\u00e8re et utilisable dans des applications embarqu\u00e9es. Elle fonctionne sous Windows, Linux, macOS, Windows, Android et sur diff\u00e9rents microcontr\u00f4leurs (STM32, NXP, ESP32, NRF52, ...).

    Le plus magique c'est que cette biblioth\u00e8que est monolithique, c'est-\u00e0-dire qu'elle ne d\u00e9pend d'aucune autre biblioth\u00e8que. Il suffit de rajouter les fichiers mongoose.c et mongoose.h dans votre projet et de les compiler avec votre programme.

    L'utilisation est tr\u00e8s simple. Le programme d'exemple ci-dessous cr\u00e9e un serveur web qui \u00e9coute sur le port 8090. Il r\u00e9pond \u00e0 quatre requ\u00eates diff\u00e9rentes\u2009:

    • / : Retourne Hello, world!, c'est la page d'accueil.
    • /source : Retourne le contenu du dossier www situ\u00e9 dans le r\u00e9pertoire courant.
    • /api/time/get : Retourne l'heure actuelle en JSON, il simule une API.
    • /teapot : Retourne I'm a teapot, c'est une blague.
    • Tout autre URI retourne une erreur 500.
    #include \"mongoose.h\"\n\nvoid ev_handler(struct mg_connection *c, int ev, void *ev_data) {\n   if (ev == MG_EV_HTTP_MSG) {\n      struct mg_http_message *hm = (struct mg_http_message *)ev_data;\n      if (mg_match(hm->uri, mg_str(\"/\"), NULL)) {\n         mg_http_reply(c, 200, \"Content-Type: text/plain\\r\\n\",\n            \"Hello, world!\\n\");\n      } else if (mg_match(hm->uri, mg_str(\"/source\"), NULL)) {\n         struct mg_http_serve_opts opts = {.root_dir = \"./www\"};\n         mg_http_serve_dir(c, hm, &opts);\n      } else if (mg_match(hm->uri, mg_str(\"/api/time/get\"), NULL)) {\n         mg_http_reply(c, 200, \"Content-Type: application/json\\r\\n\",\n            \"{%m:%lu}\\n\", MG_ESC(\"time\"), time(NULL));\n      } else if (mg_match(hm->uri, mg_str(\"/teapot\"), NULL)) {\n         mg_http_reply(c, 418, \"\", \"I'm a teapot\\n\");\n      } else {\n         mg_http_reply(c, 500, \"Content-Type: application/json\\r\\n\",\n            \"{%m:%m}\\n\", MG_ESC(\"error\"), MG_ESC(\"Unsupported URI\"));\n      }\n   }\n}\n\nint main(void) {\n   struct mg_mgr mgr;\n   mg_mgr_init(&mgr);\n   mg_http_listen(&mgr, \"http://0.0.0.0:8090\", ev_handler, NULL);\n   for (;;) mg_mgr_poll(&mgr, 1000);\n}\n
    ", "tags": ["mongoose.h", "mongoose.c", "www"]}, {"location": "course-c/48-network/applications/#requete-http", "title": "Requ\u00eate HTTP", "text": "

    Il est parfois utile pour un programme de r\u00e9cup\u00e9rer des informations sur un serveur web. Le d\u00e9veloppeur recherchera une solution impliquant une API simple retournant un contenu JSON. Le JSON est un format de s\u00e9rialisation de donn\u00e9es tr\u00e8s populaire, et il est facile \u00e0 lire et \u00e0 \u00e9crire pour les humains.

    Imaginons que vous souhaitez r\u00e9aliser un programme qui affiche un fait incroyable sur les chats. Heureusement, vous avez \u00e0 votre disposition l'API Cat Facts. Une requ\u00eate HTTP GET sera envoy\u00e9e \u00e0 l'URL https://catfact.ninja/fact pour obtenir un fait al\u00e9atoire en JSON.

    Voici un exemple de requ\u00eate\u2009:

    GET /fact HTTP/1.1\nHost: catfact.ninja\n

    Et voici un exemple de r\u00e9ponse\u2009:

    {\n    \"fact\": \"The cat who holds the record for the longest non-fatal fall is\n    Andy. He fell from the 16th floor of an apartment building\n    (about 200 ft/.06 km) and survived.\",\n    \"length\": 157\n}\n

    Il y a plusieurs mani\u00e8res de r\u00e9aliser ce programme en C tout d'abord sans aucune d\u00e9pendance. Le protocole HTTP sera \u00e9mul\u00e9 en utilisant des sockets et la r\u00e9ponse JSON sera d\u00e9coup\u00e9e \u00e0 la main. Notez que ce code n'est pas du tout un bon exemple. Tout d'abord aucune erreur n'est v\u00e9rifi\u00e9e, et le code est tr\u00e8s fragile. Si la r\u00e9ponse du serveur change, le programme ne fonctionnera plus. N\u00e9anmoins l'exercice ici est de comprendre que HTTP n'est qu'un simple protocole texte.

    #define BUFFER_SIZE 4096\n\nint main() {\n   char *request =\n       \"GET /fact HTTP/1.1\\r\\n\"\n       \"Host: catfact.ninja\\r\\n\"\n       \"Connection: close\\r\\n\"\n       \"\\r\\n\";\n\n   struct addrinfo hints = {0}, *res;\n   hints.ai_family = AF_UNSPEC;  // Allow IPv4 or IPv6\n   hints.ai_socktype = SOCK_STREAM;\n   getaddrinfo(\"catfact.ninja\", \"80\", &hints, &res);\n\n   int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);\n   connect(sockfd, res->ai_addr, res->ai_addrlen);\n   freeaddrinfo(res);\n   send(sockfd, request, strlen(request), 0);\n\n   char buffer[BUFFER_SIZE];\n   int bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);\n   if (bytes_received < 0) perror(\"Erreur lors de la r\u00e9ception de la r\u00e9ponse\");\n   close(sockfd);\n\n   printf(\"R\u00e9ponse re\u00e7ue : %.*s\\n\", bytes_received, buffer);\n\n   char *json_start = strstr(buffer, \"\\r\\n\\r\\n\");\n   if (json_start) {\n      json_start += 4;\n      char *fact_start = strstr(json_start, \"\\\"fact\\\":\\\"\");\n      if (fact_start) {\n         fact_start += 8;\n         char *fact_end = strchr(fact_start, '\"');\n         if (fact_end) {\n            *fact_end = '\\0';\n            printf(\"%s\\n\", fact_start);\n         }\n      }\n   } else {\n      printf(\"R\u00e9ponse HTTP invalide ou absence de JSON.\\n\");\n   }\n}\n

    H\u00e9las, si ce code fonctionnait bien il y a quelques ann\u00e9es, la r\u00e9ponse que l'on obtient est la suivante\u2009:

    R\u00e9ponse re\u00e7ue : HTTP/1.1 301 Moved Permanently\nServer: nginx/1.24.0\nDate: Mon, 16 Sep 2024 12:58:04 GMT\nContent-Type: text/html\nContent-Length: 169\nConnection: close\nLocation: https://catfact.ninja/fact\n\n<html>\n<head><title>301 Moved Permanently</title></head>\n<body>\n<center><h1>301 Moved Permanently</h1></center>\n<hr><center>nginx/1.24.0</center>\n</body>\n</html>\n

    Le code de r\u00e9ponse est 301 parce que le site a \u00e9t\u00e9 d\u00e9plac\u00e9 vers un nouvel emplacement et cet emplacement est https://catfact.ninja/fact. Notez la pr\u00e9sence du s \u00e0 http. Cela signifie que le serveur utilise le protocole HTTPS et non HTTP. Sans biblioth\u00e8que suppl\u00e9mentaire, il est impossible de r\u00e9aliser une requ\u00eate HTTPS car cela n\u00e9cessite une couche de chiffrement et des certificats.

    Voyons voir comment faire la m\u00eame chose convenablement, en utilisant les bonnes d\u00e9pendances. Nous aurons besoin de quelques biblioth\u00e8ques\u2009:

    sudo apt install libcurl4-openssl-dev libjson-c-dev\n

    Voici le code C. La biblioth\u00e8que libcurl est utilis\u00e9e pour effectuer la requ\u00eate HTTP(S) et la biblioth\u00e8que libjson-c est utilis\u00e9e pour analyser la r\u00e9ponse JSON\u2009:

    #include <curl/curl.h>\n#include <json-c/json.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstruct response { char *memory; size_t size; };\n\nstatic size_t write_callback(void *data, size_t size, size_t nmemb, void *userp) {\n   size_t realsize = size * nmemb;\n   struct response *mem = (struct response *)userp;\n   char *ptr = realloc(mem->memory, mem->size + realsize + 1);\n   if (ptr == NULL) {\n      printf(\"Not enough memory (realloc failed)\\n\");\n      return 0;\n   }\n   mem->memory = ptr;\n   memcpy(&(mem->memory[mem->size]), data, realsize);\n   mem->size += realsize;\n   mem->memory[mem->size] = '\\0';\n   return realsize;\n}\n\nint main() {\n   struct response chunk = {.memory = malloc(1), .size = 0};\n\n   curl_global_init(CURL_GLOBAL_DEFAULT);\n   CURL *curl = curl_easy_init();\n   if (!curl) {\n      fprintf(stderr, \"Erreur cURL: impossible d'initialiser\\n\");\n      return 1;\n   }\n\n   curl_easy_setopt(curl, CURLOPT_URL, \"https://catfact.ninja/fact\");\n   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n   curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);\n   CURLcode res = curl_easy_perform(curl);\n\n   if (res != CURLE_OK) {\n      fprintf(stderr, \"Erreur cURL: %s\\n\", curl_easy_strerror(res));\n      return 1;\n   }\n\n   struct json_object *parsed_json = json_tokener_parse(chunk.memory);\n   struct json_object *fact;\n   json_object_object_get_ex(parsed_json, \"fact\", &fact);\n   printf(\"%s\\n\", json_object_get_string(fact));\n   json_object_put(parsed_json);\n\n   curl_easy_cleanup(curl);\n   free(chunk.memory);\n   curl_global_cleanup();\n}\n

    N'oubliez pas lors de la compilation de rajouter les biblioth\u00e8ques\u2009:

    gcc catfact.c -lcurl -ljson-c\n
    ", "tags": ["libcurl"]}, {"location": "course-c/48-network/osi/", "title": "Mod\u00e8le OSI", "text": ""}, {"location": "course-c/48-network/osi/#introduction", "title": "Introduction", "text": "

    Le mod\u00e8le OSI (Open Systems Interconnection) est un mod\u00e8le de r\u00e9f\u00e9rence pour les communications entre ordinateurs. Il a \u00e9t\u00e9 cr\u00e9\u00e9 par l'ISO (International Organization for Standardization) en 1984.

    Le mod\u00e8le OSI est divis\u00e9 en 7 couches, chacune ayant un r\u00f4le sp\u00e9cifique dans le processus de communication. Chaque couche communique avec les couches adjacentes pour transmettre les donn\u00e9es.

    "}, {"location": "course-c/48-network/osi/#les-couches", "title": "Les couches", "text": "

    La figure suivante illustre les 7 couches du mod\u00e8le OSI en comparaison du PDU (Protocol Data Unit) associ\u00e9 \u00e0 chaque couche et du mod\u00e8le internet TCP/IP qui est apparu avant la standardisation du mod\u00e8le OSI. En effet ce mod\u00e8le est plus ancien et ne comporte que 4 couches et ses couches ne sont pas conformes au mod\u00e8le OSI. En effet, comme nous le verrons plus tard, une pile de protocoles ne doit pas d\u00e9pendre des protocoles des autres couches, or dans TCP/IP la somme de contr\u00f4le de la couche de transport fait intervenir une partie de l'en-t\u00eate IP.

    Mod\u00e8le OSI

    1. Couche physique (Physical Layer): Cette couche est responsable de la transmission des donn\u00e9es brutes sur le support de communication. Elle d\u00e9finit les caract\u00e9ristiques \u00e9lectriques, m\u00e9caniques et fonctionnelles du mat\u00e9riel de communication.

    2. Couche liaison de donn\u00e9es (Data Link Layer): Cette couche est responsable de la transmission des donn\u00e9es entre les n\u0153uds voisins sur le r\u00e9seau local. Elle g\u00e8re les erreurs de transmission, le contr\u00f4le de flux et l'acc\u00e8s au support.

    3. Couche r\u00e9seau (Network Layer): Cette couche est responsable de la transmission des donn\u00e9es entre les n\u0153uds distants sur le r\u00e9seau. Elle g\u00e8re le routage des donn\u00e9es \u00e0 travers le r\u00e9seau.

    4. Couche transport (Transport Layer): Cette couche est responsable de la transmission des donn\u00e9es de bout en bout entre les applications. Elle g\u00e8re le contr\u00f4le de flux, la segmentation et le r\u00e9assemblage des donn\u00e9es.

    5. Couche session (Session Layer): Cette couche est responsable de l'\u00e9tablissement, de la gestion et de la fin des sessions de communication entre les applications.

    6. Couche pr\u00e9sentation (Presentation Layer): Cette couche est responsable de la traduction, de la compression et du chiffrement des donn\u00e9es pour la communication entre les applications.

    7. Couche application (Application Layer): Cette couche est responsable de la communication entre les applications. Elle fournit des services de haut niveau tels que l'authentification, la messagerie et le partage de fichiers.

    "}, {"location": "course-c/48-network/osi/#encapsulation", "title": "Encapsulation", "text": "

    L'interop\u00e9rabilit\u00e9 entre diff\u00e9rents syst\u00e8mes est souvent rendue possible gr\u00e2ce \u00e0 un concept nomm\u00e9 encapsulation. C'est un concept \u00e9galement cl\u00e9 en programmation orient\u00e9e objet. L'encapsulation consiste \u00e0 dissimuler de la complexit\u00e9 non n\u00e9cessaire \u00e0 comprendre la partie int\u00e9ressante d'un probl\u00e8me dans un ensemble isol\u00e9 et qui peut \u00eatre consid\u00e9r\u00e9 comme une bo\u00eete noire.

    En communication r\u00e9seau, par exemple lorsque vous \u00eates au t\u00e9l\u00e9phone, vous n'avez pas besoin de savoir quelle est la nature de la ligne de transport qui v\u00e9hicule votre voix. Est-ce une ligne de cuivre, une fibre optique, une liaison satellite, cela n'a pas d'importance. N\u00e9anmoins vous n'\u00eates que l'utilisateur final de cette communication, il y a d'autres \u00e9tapes interm\u00e9diaires. Celle qui v\u00e9hicule r\u00e9ellement votre voix accompagn\u00e9e \u00e9ventuellement de votre vid\u00e9o et des touches que vous appuyez sur votre t\u00e9l\u00e9phone. Cette \u00e9tape est la couche application, son besoin c'est de pouvoir communiquer d'un t\u00e9l\u00e9phone \u00e0 l'autre, le reste d'a pas d'importance. En dessous de cette couche, il y a la couche transport qui va s'occuper de d\u00e9couper les diff\u00e9rentes donn\u00e9es en paquets qui pourront \u00eatre achemin\u00e9s par diff\u00e9rentes routes (via le Wi-Fi, le r\u00e9seau cellulaire, etc.). Cette couche ne sait pas ce qu'elle transporte comme donn\u00e9es et ne sait pas non plus sur quoi sont transport\u00e9es ces donn\u00e9es. Tout en bas de la pile, il y a la couche physique, c'est pr\u00e9cis\u00e9ment le type de m\u00e9dia qui est utilis\u00e9 pour transporter les donn\u00e9es (c\u00e2ble, fibre optique ...).

    Vous imaginez probablement que les donn\u00e9es sont transmises sous forme de bits, ce qui est vrai en partie. En effet, en transmission par c\u00e2ble ou sans fil, les donn\u00e9es ne sont pas simplement des \u00e9tats logiques 0 ou 1, souvent elles sont modul\u00e9es pour \u00eatre transport\u00e9es plus efficacement. Cela donne d'ailleurs le nom aux dispositifs qui s'occupent de moduler et d\u00e9moduler les signaux, les modems.

    L'encapsulation ici sert donc \u00e0 d\u00e9couper les responsabilit\u00e9s. Si vous souhaitez v\u00e9hiculer des bits d'un point \u00e0 un autre, vous n'avez pas besoin de savoir comment cela se fait, vous devez juste vous reposer sur la couche physique.

    "}, {"location": "course-c/48-network/osi/#protocoles-internet", "title": "Protocoles Internet", "text": "

    La pile de protocoles Internet est assez complexe, mais elle est tr\u00e8s bien document\u00e9e. Les RFC (Request For Comments) sont des documents qui d\u00e9crivent les protocoles Internet. Ils sont publi\u00e9s par l'IETF (Internet Engineering Task Force) et sont des standards de facto. Chaque protocole que l'on utilise pour communiquer sur un r\u00e9seau informatique est d\u00e9crit dans un RFC. Par exemple, le protocole HTTP (HyperText Transfer Protocol) responsable de communiquer les pages web visibles dans votre navigateur est d\u00e9crit dans le RFC 2616. Ce protocole est encapsul\u00e9 dans le protocole TCP normalis\u00e9 par le RFC 793 (Transmission Control Protocol) qui lui-m\u00eame est encapsul\u00e9 dans le protocole IP (RFC 791). Ce dernier est \u00e9galement encapsul\u00e9 dans le protocole MAC RFC 3422 (Media Access Control) qui est lui-m\u00eame encapsul\u00e9 dans le protocole Ethernet (RFC 894).

    RFC

    Un fait notable avec les RFC est le format de r\u00e9daction. Ces documents sont \u00e9crits en pur texte ASCII sans outils de mise en page. Ce n'est pas du Markdown, du Word, du LaTeX, c'est du texte brut mais tr\u00e8s bien structur\u00e9 avec des figures, des tables des mati\u00e8res, des r\u00e9f\u00e9rences crois\u00e9es, etc.

    La raison \u00e0 cela est que les RFC sont des documents de r\u00e9f\u00e9rence et doivent \u00eatre accessibles par tous et \u00e0 toute \u00e9poque. Les informaticiens savaient d\u00e9j\u00e0 que les formats de fichiers sont \u00e9ph\u00e9m\u00e8res et que les outils de traitement de texte ne sont pas toujours compatibles entre eux. Le texte brut est donc le format le plus s\u00fbr pour garantir la p\u00e9rennit\u00e9 des documents. Et ils ont raison car depuis 50 ans, les RFC sont toujours lisibles et utilisables.

    En termes informatiques, et surtout en programmation, chaque protocole dispose de ses propres champs de donn\u00e9es. Par exemple voici le la figure 3 du RFC 793 telle que publi\u00e9e en 1981\u2009:

     0                   1                   2                   3\n 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|          Source Port          |       Destination Port        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                        Sequence Number                        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Acknowledgment Number                      |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|  Data |           |U|A|P|R|S|F|                               |\n| Offset| Reserved  |R|C|S|S|Y|I|            Window             |\n|       |           |G|K|H|T|N|N|                               |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|           Checksum            |         Urgent Pointer        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Options                    |    Padding    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                             data                              |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n

    Ce sch\u00e9ma d\u00e9crit l'en-t\u00eate du protocole TCP. Il est compos\u00e9 de plusieurs champs qui sont utilis\u00e9s pour la communication entre les machines. On voit par exemple que ce protocole utilise la notion de port pour identifier les applications qui communiquent. Les ports sont des num\u00e9ros qui sont attribu\u00e9s \u00e0 chaque application. Par exemple, le port 80 est utilis\u00e9 pour le protocole HTTP, le port 443 pour le protocole HTTPS, le 21 est utilis\u00e9 pour le protocole FTP tandis que le port 22 est utilis\u00e9 pour le protocole SSH. Outre les ports, les donn\u00e9es peuvent \u00eatre fragment\u00e9es en plusieurs paquets c'est pourquoi chaque paquet contient un num\u00e9ro de s\u00e9quence et un num\u00e9ro d'acquittement. \u00c0 la fin de l'en-t\u00eate se trouvent les donn\u00e9es des couches sup\u00e9rieures.

    Le protocole de transport TCP est bas\u00e9 sur le protocole IP. On trouve \u00e9galement dans son standard (RFC 791, figure 4) un en-t\u00eate similaire qui va contenir l'adresse IP (p. ex. 192.168.45.20) source et destination, le protocole utilis\u00e9 en dessus (TCP, UDP, ICMP, etc.), la taille du paquet, etc.

     0                   1                   2                   3\n 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|Version|  IHL  |Type of Service|          Total Length         |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|         Identification        |Flags|      Fragment Offset    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|  Time to Live |    Protocol   |         Header Checksum       |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                       Source Address                          |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Destination Address                        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Options                    |    Padding    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n

    En C, si l'on devait r\u00e9impl\u00e9menter ces protocoles, on pourrait utiliser des structures. Voici par exemple comment on pourrait d\u00e9finir les en-t\u00eates des protocoles MAC et IP\u2009:

    struct mac_header {\n    uint8_t destination_mac[6];  // Adresse MAC de destination\n    uint8_t source_mac[6];       // Adresse MAC source\n    uint16_t ethertype;  // Protocole encapsul\u00e9 (p. ex. 0x0800 pour IPv4)\n};\n\nstruct ip_header {\n    uint8_t version_ihl;\n    uint8_t dscp_ecn;\n    uint16_t total_length;\n    uint16_t identification;\n    uint16_t flags_fragment_offset;\n    uint8_t ttl;                 // Time To Live (dur\u00e9e de vie)\n    uint8_t protocol;            // Protocole de couche sup\u00e9rieure (ex. TCP = 6)\n    uint16_t checksum;\n    uint32_t source_ip;          // Adresse IP source\n    uint32_t destination_ip;     // Adresse IP de destination\n};\n

    En pratique c'est plus complexe que cela, d'une part par que les donn\u00e9es sont v\u00e9hicul\u00e9es en big-endian (octets de poids fort en premier) et que les champs de donn\u00e9es sont de taille variable. Par exemple, l'adresse IP est cod\u00e9e sur 32 bits, mais elle peut \u00eatre repr\u00e9sent\u00e9e en IPv4 (4 octets) ou en IPv6 (16 octets).

    L'id\u00e9e ici est juste de montrer l'int\u00e9r\u00eat des structures dans ce type de situation.

    "}, {"location": "course-c/48-network/protocoles/", "title": "Protocoles", "text": "

    Cet ouvrage n'\u00e9tant pas un cours consacr\u00e9 au r\u00e9seau, il n'est pas raisonnable d'y consacrer plus d'un chapitre. N\u00e9anmoins, l'ing\u00e9nieur d\u00e9veloppeur en C devra tout de m\u00eame comprendre, du moins conceptuellement les diff\u00e9rences entre les diff\u00e9rents protocoles de communication, comment ils sont imbriqu\u00e9s et \u00e0 quoi ils servent. in fine en programmation vous aurez probablement simplement besoin de savoir comment ouvrir une connexion TCP ou UDP, comment envoyer des donn\u00e9es et comment les recevoir.

    "}, {"location": "course-c/48-network/protocoles/#mac", "title": "MAC", "text": "

    Chaque carte r\u00e9seau install\u00e9e dans un ordinateur, un t\u00e9l\u00e9phone portable ou une tablette poss\u00e8de une adresse physique unique appel\u00e9e adresse MAC (Media Access Control). Cette adresse est cod\u00e9e sur 48 bits (6 octets) et est attribu\u00e9e par le fabricant de la carte. Ce dernier obtient un OUI (Organizationally Unique Identifier) qui est un pr\u00e9fixe de 24 bits attribu\u00e9 par l'IEEE (Institute of Electrical and Electronics Engineers) \u00e0 une organisation. Avec un OUI, le fabricant peut g\u00e9rer jusqu'\u00e0 16 millions d'adresses MAC. Les 24 bits restants sont attribu\u00e9s par le fabricant lui-m\u00eame. Obtenir un OUI co\u00fbte environ 2 700 dollars. Ceci explique pourquoi les Raspberry PI ont des adresses MAC qui commencent par b8:27:eb qui est l'OUI de la fondation Raspberry PI.

    \u00c0 moins de vouloir communiquer directement avec une carte r\u00e9seau, l'adresse MAC n'est pas tr\u00e8s utile pour un d\u00e9veloppeur. En effet, c'est le syst\u00e8me d'exploitation qui se charge de g\u00e9rer les communications r\u00e9seau et qui va utiliser l'adresse IP pour identifier les machines. L'adresse MAC c'est un peu comme la coordonn\u00e9e GPS d'une maison. Elle est utile au cadastraliste pour identifier un terrain et y attribuer une adresse postale, mais pour le facteur, c'est l'adresse postale qui est utile.

    "}, {"location": "course-c/48-network/protocoles/#ip", "title": "IP", "text": "

    Un r\u00e9seau informatique c'est un groupement de machines qui communiquent entre elles. Mettez 8 milliards de personnes dans la m\u00eame salle, donnez-leur un porte-voix assez puissant et vous aurez un r\u00e9seau. Vous admettrez n\u00e9anmoins qu'arbitrer les \u00e9changes pour que chacun puisse parler \u00e0 son tour est irr\u00e9alisable. En outre, cela ne vous int\u00e9resse probablement gu\u00e8re de savoir que Madame Michu a gagn\u00e9 5 francs au Tribolo. C'est pourquoi les r\u00e9seaux informatiques sont organis\u00e9s en sous-r\u00e9seau.

    Les ing\u00e9nieurs syst\u00e8me utilisent le protocole IP pour associer \u00e0 une adresse MAC donn\u00e9e une adresse IP. L'adresse IP est une adresse logique attribu\u00e9e \u00e0 chaque machine sur un r\u00e9seau. Elle est historiquement cod\u00e9e sur 32-bits et permettant ainsi d'identifier environ 4 milliards de machines. Avec l'explosion de l'Internet, cette taille est devenue tr\u00e8s largement insuffisante. C'est pourquoi un nouveau protocole, IPv6 (version 6) a \u00e9t\u00e9 cr\u00e9\u00e9. Ce protocole utilise des adresses cod\u00e9es sur 128 bits et permet d'identifier 340 sextillions de machines. C'est largement suffisant pour attribuer une adresse IP \u00e0 chaque grain de sable sur Terre. N\u00e9anmoins les adresses IPv6 sont moins facilement m\u00e9morisables que les adresses IPv4. C'est pourquoi ces derni\u00e8res sont encore largement utilis\u00e9es dans des r\u00e9seaux priv\u00e9s ou d'entreprise.

    Un p\u00e9riph\u00e9rique IP peut \u00eatre cloisonn\u00e9 facilement en plusieurs sous-r\u00e9seaux gr\u00e2ce \u00e0 un masque de sous-r\u00e9seau. Par exemple, le p\u00e9riph\u00e9rique \u00e0 l'adresse IP 192.168.1.42 avec un masque de sous-r\u00e9seau 255.255.255.0 ne va s'int\u00e9resser qu'aux p\u00e9riph\u00e9riques de la plage 192.168.1.0 \u00e0 192.168.1.255. Cela permet \u00e0 la carte r\u00e9seau de filtrer les paquets qui lui sont destin\u00e9s et de ne pas s'int\u00e9resser aux autres. Pour reprendre notre analogie, c'est comme si vous aviez sur vos oreilles un casque antibruit qui ne laisse passer que certaines voix.

    Il y a deux types d'adresses IP, celles dites publiques et routables sur Internet et celles dites priv\u00e9es qui sont utilis\u00e9es dans les r\u00e9seaux locaux. Les adresses priv\u00e9es sont d\u00e9finies par la RFC 1918 et sont les suivantes\u2009:

    Plage d'adresses Masque de sous-r\u00e9seau Classe 10.0.0.0 10.255.255.255 (10/8) A 172.16.0.0 172.31.255.255 (172.16/12) B 192.168.0.0 192.168.255.255 (192.168/16) C

    Il est tr\u00e8s probable que l'adresse IP de votre ordinateur ou de votre t\u00e9l\u00e9phone sur le r\u00e9seau de votre domicile ou celui de votre travail soit l'une de ces adresses. Elles ne sont pas routables c'est-\u00e0-dire qu'elles ne peuvent pas \u00eatre transport\u00e9es en dehors du r\u00e9seau local.

    Pour communiquer publiquement, il faut utiliser une adresse IP publique. Ces adresses sont attribu\u00e9es par les fournisseurs d'acc\u00e8s \u00e0 Internet (FAI) et sont g\u00e9r\u00e9es par l'IANA (Internet Assigned Numbers Authority). Chez vous, vous avez probablement une adresse IP publique attribu\u00e9e par votre FAI (Salt, Swisscom, Sunrise, Orange...) c'est l'adresse attribu\u00e9e \u00e0 votre routeur ou box.

    \u00c0 votre travail ou dans votre \u00e9cole, c'est pareil. Une \u00e9cole n'a pas les moyens ni l'int\u00e9r\u00eat d'avoir une adresse IP publique pour chaque ordinateur. Elle dispose d'une certaine quantit\u00e9 d'adresses IP publiques utilis\u00e9es pour communiquer vers l'ext\u00e9rieur. Si par exemple vous allez sur le site showmyip.com, vous pouvez voir l'adresse IP publique utilis\u00e9e pour communiquer sur internet.

    "}, {"location": "course-c/48-network/protocoles/#nat-pat", "title": "NAT / PAT", "text": "

    On peut se demander comment les donn\u00e9es en provenance d'internet peuvent arriver jusqu'\u00e0 votre ordinateur si tout le monde dans une m\u00eame institution \u00e0 la m\u00eame adresse. C'est l\u00e0 qu'intervient le NAT (Network Address Translation), plus pr\u00e9cis\u00e9ment dans notre cas le PAT (Port Address Translation). Il s'agit d'un dispositif qui va traduire les adresses IP priv\u00e9es en adresses IP publiques. Admettons que j'aimerais envoyer un message \u00e0 Monsieur Google \u00e0 l'adresse 1600 Amphi-theatre Parkway, Mountain View, CA 94043, USA. Pour simplifier disons simplement que son adresse est\u2009: 142.250.203.110:80443. Le 80443 est le num\u00e9ro du port utilis\u00e9. C'est un peu comme le num\u00e9ro de l'appartement dans un immeuble. Comme je crois dur comme fer \u00e0 l'encapsulation, je vais indiquer sur l'enveloppe au dos de l'enveloppe l'adresse de l'exp\u00e9diteur, c'est-\u00e0-dire mon adresse. Je vais donc \u00e9crire quelque chose comme\u2009: 192.168.1.42:7878. Or, vous l'aurez compris, cette adresse n'est pas routable sur internet et Monsieur Google ne saura pas o\u00f9 me r\u00e9pondre. C'est pareil que dire que le tr\u00e9sor se cache sous la troisi\u00e8me pierre \u00e0 droite du vieux ch\u00eane. Sans savoir dans quelle for\u00eat, dans quelle contr\u00e9e et dans quel pays chercher, vous ne deviendrez pas riche.

    N\u00e9anmoins, rappelez-vous du NAT. Vous avez un gentil facteur qui, avant que votre lettre ne soit post\u00e9e en direction des Am\u00e9riques, va r\u00e9\u00e9crire l'adresse de l'exp\u00e9diteur. Il va mettre par exemple\u2009: 193.134.219.72:57898. Il s'agit de l'adresse IP publique de votre institution. Quant au port, il a \u00e9t\u00e9 choisit al\u00e9atoirement par le facteur. C'est un peu comme si le facteur avait mis un num\u00e9ro de bo\u00eete postale disponible. Bien entendu il notera dans un cahier que cette bo\u00eete postale (ce port) est r\u00e9serv\u00e9e \u00e0 la r\u00e9ponse de Monsieur Google et qu'il vous est destin\u00e9.

    Vous l'aurez compris, il n'existe pas de facteur. C'est un dispositif informatique nomm\u00e9 routeur qui se charge de faire cette traduction. C'est le routeur qui se charge de cette t\u00e2che. Il va traduire l'adresse IP priv\u00e9e en adresse IP publique et va conserver dans une table de traduction l'adresse IP priv\u00e9e et le port utilis\u00e9. Lorsque la r\u00e9ponse de Monsieur Google arrivera, le routeur va regarder dans sa table de traduction et va savoir \u00e0 qui renvoyer la r\u00e9ponse. C'est un peu comme si le facteur avait un cahier avec les noms des destinataires et les num\u00e9ros de bo\u00eetes postales.

    Sur votre machine Linux, vous pouvez voir les connexions en cours avec conntrack, on y voir le type de socket (TCP, UDP), l'adresse IP source et destination, le port source et destination, et la traduction. Pour la premi\u00e8re connexion, on voit une adresse source priv\u00e9e, mais une adresse de destination publique. C'est une connexion sortante. Le port de destination 34230 est probablement attribu\u00e9 par le m\u00e9canisme de PAT du routeur du destinataire.

    \u279c sudo conntrack -L\ntcp      6 95 SYN_SENT src=172.24.167.101 dst=169.254.169.254 sport=34230\n         dport=80 [UNREPLIED] src=169.254.169.254 dst=172.24.167.101\n         sport=80 dport=34230 mark=0 use=1\nudp      17 23 src=10.255.255.254 dst=10.255.255.254 sport=41319\n         dport=53 src=10.255.255.254 dst=10.255.255.254\n         sport=53 dport=41319 mark=0 use=1\ntcp      6 99 SYN_SENT src=172.24.167.101 dst=169.254.169.254 sport=57376\n         dport=80 [UNREPLIED] src=169.254.169.254 dst=172.24.167.101\n         sport=80 dport=57376 mark=0 use=1\n...\n

    Ce m\u00e9canisme de NAT/PAT intervient \u00e0 de multiples strates. Tout d'abord au niveau d'un programme complexe comme Docker qui va cr\u00e9er des r\u00e9seaux virtuels pour isoler les conteneurs. Ensuite au niveau de votre syst\u00e8me d'exploitation pour g\u00e9rer les connexions entre les diff\u00e9rents programmes. En effet votre PC ou votre Mac \u00e0 une seule carte r\u00e9seau partag\u00e9e par tous les programmes. Enfin, au niveau de votre routeur qui va g\u00e9rer les connexions entre votre r\u00e9seau local et internet. La situation se r\u00e9p\u00e8te de l'autre c\u00f4t\u00e9 de l'atlantique chez Monsieur Google (du routeur \u00e0 la machine, de la machine au programme...).

    ", "tags": ["conntrack"]}, {"location": "course-c/48-network/protocoles/#dhcp", "title": "DHCP", "text": "

    Lorsque vous connectez un p\u00e9riph\u00e9rique \u00e0 un r\u00e9seau local, il a besoin d'une adresse IP. Dans de rares cas, c'est de votre responsabilit\u00e9 de choisir une adresse, mais comme vous ne connaissez probablement pas la configuration du r\u00e9seau, vous ne saurez pas choisir une adresse valide qui n'est pas utilis\u00e9e par quelqu'un d'autre. C'est le r\u00f4le du protocole DHCP (Dynamic Host Configuration Protocol) qui permet \u00e0 un p\u00e9riph\u00e9rique de demander une adresse IP. Le serveur DHCP va attribuer une adresse IP \u00e0 ce p\u00e9riph\u00e9rique pour une dur\u00e9e d\u00e9termin\u00e9e. Une fois allou\u00e9e, vous pouvez communiquer sur le r\u00e9seau.

    Le serveur DHCP va stocker dans une table la correspondance entre l'adresse MAC du p\u00e9riph\u00e9rique et l'adresse IP attribu\u00e9e. Ces informations sont compl\u00e9t\u00e9es par un bail DHCP (DHCP Lease) qui est la dur\u00e9e pendant laquelle l'adresse IP est attribu\u00e9e. Une fois le bail expir\u00e9, le p\u00e9riph\u00e9rique doit renouveler son bail pour continuer \u00e0 communiquer. Selon la configuration du serveur DHCP, il peut attribuer la m\u00eame adresse IP ou une nouvelle adresse. C'est pour cette raison que parfois votre adresse IP change toute seule.

    Ce que vous devez savoir en tant que d\u00e9veloppeur, c'est que vous pouvez soit disposer d'une adresse IP statique, c'est-\u00e0-dire que votre adresse est stock\u00e9e en dur dans la configuration de votre machine, soit d'une adresse IP dynamique, c'est-\u00e0-dire que votre adresse est attribu\u00e9e par un serveur DHCP. Dans la tr\u00e8s grande majorit\u00e9 des cas, c'est la deuxi\u00e8me option qui est utilis\u00e9e.

    La plupart du temps un serveur DHCP r\u00e9cup\u00e8re \u00e9galement le nom de l'h\u00f4te (hostname), c'est-\u00e0-dire le nom de la machine.

    "}, {"location": "course-c/48-network/protocoles/#arp", "title": "ARP", "text": "

    Nous avons vu qu'une fois dans un r\u00e9seau local et que chacun a une adresse IP, il est possible de communiquer. On profite de l'encapsulation des protocoles pour faire transiter les donn\u00e9es d'une machine \u00e0 une autre sans se soucier des couches inf\u00e9rieures. N\u00e9anmoins, il reste un probl\u00e8me \u00e0 r\u00e9soudre.

    IP est bas\u00e9 sur le protocole Ethernet. Chaque paquet IP est encapsul\u00e9 dans un paquet Ethernet lequel contient l'adresse MAC source et destination. Vous devez donc conna\u00eetre l'adresse MAC de la machine \u00e0 qui vous souhaitez envoyer des donn\u00e9es.

    C'est le protocole ARP (Address Resolution Protocol) qui permet de faire la correspondance entre une adresse IP et une adresse MAC. Lorsque vous souhaitez communiquer avec une machine, vous envoyez un message ARP en broadcast sur le r\u00e9seau (\u00e0 tout le monde). Toutes les machines du r\u00e9seau re\u00e7oivent ce message et celle qui poss\u00e8de l'adresse IP demand\u00e9e r\u00e9pond avec son adresse MAC.

    \u00c9videmment si dix-mille ordinateurs sont dans le m\u00eame sous r\u00e9seau, ils ne vont pas envoyer un message broadcast avant chaque transmission. Chaque ordinateur conserve sa propre table ARP qui contient les correspondances entre les adresses IP et les adresses MAC qu'il conna\u00eet. C'est seulement lorsque l'adresse MAC n'est pas dans la table ou lorsque l'adresse IP change qu'une requ\u00eate ARP sera envoy\u00e9e.

    ARP Flood

    Il existe une attaque informatique nomm\u00e9e ARP Flood qui consiste \u00e0 envoyer un grand nombre de requ\u00eates ARP sur un r\u00e9seau pour saturer les tables ARP des machines. Cela peut \u00eatre utilis\u00e9 pour emp\u00eacher les machines de communiquer entre elles.

    Puisque les requ\u00eates ARP sont diffus\u00e9es en broadcast, tous les appareils du r\u00e9seau les re\u00e7oivent.

    Heureusement les routeurs et les switchs modernes sont capables de d\u00e9tecter et de bloquer ce type d'attaque en limitant le nombre de requ\u00eates ARP par seconde ou en blocant les adresses MAC suspectes.

    Sous Linux vous pouvez consulter la table ARP avec la commande ip neigh show et sous Windows vous pouvez utiliser arp -a.

    "}, {"location": "course-c/48-network/protocoles/#icmp", "title": "ICMP", "text": "

    Le protocole ICMP (Internet Control Message Protocol) est un protocole qui permet de communiquer des messages de contr\u00f4le et d'erreur entre les machines. Par exemple, si vous essayez de communiquer avec une machine qui n'existe pas, vous allez recevoir un message ICMP de type Destination Unreachable.

    C'est ce protocole qui permet de faire des pings pour tester la connectivit\u00e9 entre deux machines. Le ping est un message ICMP de type Echo Request qui est envoy\u00e9 \u00e0 une machine. Si la machine est connect\u00e9e et qu'elle est configur\u00e9e pour r\u00e9pondre aux pings, elle va renvoyer un message ICMP de type Echo Reply.

    Dans la majorit\u00e9 des cas en ing\u00e9nierie et durant le d\u00e9veloppement, les machines sont configur\u00e9es pour r\u00e9pondre aux pings. Si vous connectez un Raspberry PI sur votre r\u00e9seau local, vous pourrez le pinger avec la commande ping 192.168.1.142 (o\u00f9 l'adresse IP est celle de votre Raspberry PI).

    Vous pouvez \u00e9galement envoyer une requ\u00eate ICMP sur un serveur public comme Google. On voit ci-dessous que le serveur r\u00e9pond aux pings et que le temps de r\u00e9ponse est d'environ 3 ms. On d\u00e9couvre \u00e9galement que l'adresse IP est 142.250.203.110

    \u279c ping google.com\nPING google.com (142.250.203.110) 56(84) bytes of data.\n64 bytes from zrh04s16.net (142.250.203.110): icmp_seq=1 ttl=111 time=3.38 ms\n64 bytes from zrh04s16.net (142.250.203.110): icmp_seq=2 ttl=111 time=3.35 ms\n^C\n--- google.com ping statistics ---\n2 packets transmitted, 2 received, 0% packet loss, time 2203ms\n

    Des services de localisation permettent de savoir o\u00f9 se trouve une adresse IP. Par exemple, l'adresse IP 142.250.203.110 est attribu\u00e9e \u00e0 Google et est situ\u00e9e \u00e0 Zurich en Suisse. Il s'agit de l'adresse d'un datacenter.

    Lorsque vous mettez un serveur priv\u00e9 sur internet. Vous voulez probablement qu'il ne r\u00e9ponde pas aux pings. En effet, un attaquant pourrait envoyer des pings \u00e0 des adresses IP al\u00e9atoires pour d\u00e9couvrir les serveurs actifs. Une fois qu'il sait que votre machine r\u00e9pond, il peut essayer de le pirater. D\u00e9sactiver le ping sous Linux se fait en modifiant le fichier /etc/sysctl.conf et en ajoutant la ligne net.ipv4.icmp_echo_ignore_all = 1.

    Traceroute

    Le protocole ICMP est \u00e9galement utilis\u00e9 par la commande traceroute qui permet de suivre le chemin emprunt\u00e9 par un paquet entre deux machines. La commande envoie des paquets ICMP avec un TTL (Time To Live) de 1, 2, 3, ... jusqu'\u00e0 ce que le paquet atteigne sa destination. Chaque routeur sur le chemin va d\u00e9cr\u00e9menter le TTL et lorsqu'il atteint 0, il va renvoyer un message ICMP de type Time Exceeded \u00e0 l'exp\u00e9diteur. C'est ainsi que l'on peut voir le chemin emprunt\u00e9 par un paquet entre deux machines.

    traceroute to 142.250.203.110, 30 hops max, 60 byte packets\n1  172.24.160.1  0.246 ms  0.216 ms  0.207 ms\n2  10.192.77.1  0.444 ms  0.535 ms  0.425 ms\n3  10.193.255.34  0.815 ms  0.806 ms  0.799 ms\n4  10.193.255.102 0.989 ms  0.977 ms  0.966 ms\n5  185.144.39.4  1.176 ms  1.164 ms  1.155 ms\n6  rchon01-te-0-0-2_81.as203130.ch  1.046 ms  1.253 ms  1.224 ms\n7  swiyv2-ge-0-0-0-0.hessoadm.ch  2.078 ms  1.937 ms  1.822 ms\n8  swiNE1-10GE-0-0-2-3.switch.ch  2.401 ms  2.179 ms  2.285 ms\n9  swiNE2-10GE-0-0-1-1.switch.ch  4.419 ms  4.409 ms  4.403 ms\n10  swiBI1-10GE-0-0-0-18.switch.ch  3.170 ms  2.996 ms  3.333 ms\n11  swiZH3-100GE-0-0-0-1.switch.ch  3.776 ms  3.660 ms  3.626 ms\n12  72.14.242.82  3.825 ms  3.817 ms  3.814 ms\n13  * * *\n14  172.253.50.20  4.565 ms 172.253.50.22 5.579 ms\n    zrh04s16-in-f14.1e100.net (142.250.203.110)  3.852 ms\n

    Le * * * indique que le routeur ne r\u00e9pond pas aux requ\u00eates ICMP. C'est une configuration courante pour les routeurs de gros fournisseurs de services internet. Ci-dessus on voit que de 1 \u00e0 4, les routeurs sont dans le r\u00e9seau local. 185.144.39.4 appartient \u00e0 la HEIG-VD puis elle transite par la HES-SO en 7 pour arriver chez Switch \u00e0 Zurich en 8. En 12 on voit que le paquet est transmis \u00e0 Google.

    ", "tags": ["traceroute"]}, {"location": "course-c/48-network/protocoles/#udp", "title": "UDP", "text": "

    Le protocole UDP (User Datagram Protocol) est un protocole de transport qui permet de transmettre des donn\u00e9es sans garantie de livraison. C'est un protocole simple qui ne g\u00e8re pas la retransmission des paquets perdus. Il est utilis\u00e9 pour les applications qui n'ont pas besoin d'une communication fiable. Par exemple, le protocole DNS utilise UDP pour transmettre les requ\u00eates de r\u00e9solution de noms de domaines. Les flux vid\u00e9os (streaming) utilisent \u00e9galement UDP pour transmettre les donn\u00e9es, car il n'est pas grave de perdre quelques images si cela permet de r\u00e9duire la latence et le trafic r\u00e9seau.

    Le protocole est relativement simple. Il est d\u00e9fini par la RFC 768 son en-t\u00eate est tr\u00e8s simple\u2009:

                      0      7 8     15 16    23 24    31\n                 +--------+--------+--------+--------+\n                 |          source address           |\n                 +--------+--------+--------+--------+\n                 |        destination address        |\n                 +--------+--------+--------+--------+\n                 |  zero  |   17   |   UDP length    |\n                 +--------+--------+--------+--------+\n                               Trame IP\n\n                  0      7 8     15 16    23 24    31\n                 +--------+--------+--------+--------+\n                 |   Source Port   |   Dest. Port    |\n                 +--------+--------+--------+--------+\n                 |     Length      |    Checksum     |\n                 +--------+--------+--------+--------+\n                 |                                   |\n                 |          Data (payload)           |\n                 |                                   |\n                 +-----------------------------------+\n                              Trame UDP\n

    On y trouve l'adresse IP source et destination, un champ r\u00e9serv\u00e9 \u00e0 z\u00e9ro, le protocole utilis\u00e9 (17 pour UDP), et la longueur du paquet sur 16 bits. Un paquet UDP \u00e0 donc une taille maximale de 65'535 octets (ou 65 Kio). Avec un en-t\u00eate de 8 octets, cela laisse 65'527 octets pour les donn\u00e9es. En pratique le MTU (Maximum Transmission Unit) des r\u00e9seaux Ethernet est de 1500 octets, c'est-\u00e0-dire qu'un paquet UDP plus grand sera fragment\u00e9 au niveau de la couche IP. Comme UDP n'a pas de m\u00e9canisme pour garantir la livraison des paquets, dans le cas o\u00f9 un fragment est perdu, l'int\u00e9gralit\u00e9 du paquet l'est \u00e9galement.

    L'en-t\u00eate UDP contient le port de source et le port de destination. On peut noter \u00e9galement que le paquet UDP contient un champ length correspondant \u00e0 la taille des donn\u00e9es et de l'en-t\u00eate. N\u00e9anmoins, on constate qu'il y a d\u00e9j\u00e0 cette information dans la trame IP. Cette redondance est le r\u00e9sultat de l'encapsulation des protocoles et de la s\u00e9paration des responsabilit\u00e9s. Cela cr\u00e9e de l'overhead, c'est-\u00e0-dire que la quantit\u00e9 d'information utile doit \u00eatre augment\u00e9e pour contenir les informations de routage et de contr\u00f4le. Au final c'est la m\u00eame chose avec les lettres papier. Le poids de l'encre par rapport au support (le papier, l'enveloppe, le timbre) et au camion du postier est tr\u00e8s faible.

    ", "tags": ["length"]}, {"location": "course-c/48-network/protocoles/#tcp", "title": "TCP", "text": "

    Le protocole TCP (Transmission Control Protocol) est un protocole de transport qui permet de transmettre des donn\u00e9es de mani\u00e8re fiable. On dit que le protocole est connect\u00e9 (connection-oriented), car il \u00e9tablit une connexion entre les deux machines avant de transmettre les donn\u00e9es. C'est un protocole complexe qui g\u00e8re la retransmission des paquets perdus, le contr\u00f4le de flux, le contr\u00f4le de congestion, etc.

    L'\u00e9tablissement d'une connexion TCP se fait avec un 3-way handshake (une poign\u00e9e de main \u00e0 trois). Le client envoie un paquet SYN (synchronize) \u00e0 la machine distante. La machine distante r\u00e9pond avec un paquet SYN-ACK (synchronize-acknowledge). Enfin, le client r\u00e9pond avec un paquet ACK (acknowledge) et la connexion est \u00e9tablie. Une fois la connexion \u00e9tablie, du point de vue d'un programme c'est comme si un tuyau avait \u00e9t\u00e9 pos\u00e9 entre les deux machines. Pour envoyer des donn\u00e9es, il suffit de les \u00e9crire dans le tuyau et elles sortiront de l'autre c\u00f4t\u00e9. Bien entendu les donn\u00e9es pourraient \u00eatre perdues, fragment\u00e9es en plusieurs paquets, \u00e9mises par c\u00e2ble sous-marin ou par satellite, mais pour le programme c'est transparent. Lui, il a la garantie que les donn\u00e9es arriveront dans l'ordre et sans erreur.

    C'est pourquoi le protocole TCP est le plus utilis\u00e9. De nombreux protocoles l'utilisent\u2009:

    • HTTP (port 80) pour les pages web\u2009;
    • HTTPS (port 443) pour les pages web s\u00e9curis\u00e9es\u2009;
    • FTP (port 21) pour le transfert de fichiers\u2009;
    • SSH (port 22) pour les connexions s\u00e9curis\u00e9e \u00e0 distance\u2009;
    • SMTP (port 25) pour l'envoi de courriels\u2009;
    • MQTT (port 1883) pour l'IoT, etc.
    "}, {"location": "course-c/48-network/protocoles/#dns", "title": "DNS", "text": "

    Le protocole DNS (Domain Name System) est un protocole bas\u00e9 sur UDP qui permet de faire la correspondance entre un nom de domaine et une adresse IP. Par exemple, lorsque vous tapez google.com dans votre navigateur, votre ordinateur va envoyer une requ\u00eate DNS pour obtenir l'adresse IP de Google. Une fois l'adresse IP obtenue, votre ordinateur va pouvoir communiquer avec le serveur de Google pour obtenir la page d'accueil.

    Les noms de domaines sont arbitr\u00e9s par l'ICANN (Internet Corporation for Assigned Names and Numbers) qui attribue les noms de domaines de premier niveau (TLD) comme .com, .org, .net, etc. Les noms de domaines de deuxi\u00e8me niveau (SLD) sont attribu\u00e9s par des registres de noms de domaines. Par exemple, le registre de noms de domaines pour la Suisse est SWITCH qui attribue les noms de domaines en .ch.

    Le protocole DNS est bas\u00e9 sur le protocole UDP et utilise le port 53. Lorsque vous voulez disposer de votre propre serveur, vous allez d\u00e9poser une requ\u00eate aupr\u00e8s votre NIC (Network Information Center) pour obtenir un nom de domaine. Vous allez ensuite configurer votre serveur DNS pour faire la correspondance entre votre nom de domaine et une adresse IP.

    On parle de r\u00e9solution de nom lorsque l'on veut obtenir l'adresse IP d'un nom de domaine et de r\u00e9solution inverse lorsque l'on veut obtenir le nom de domaine \u00e0 partir d'une adresse IP. Chaque institution dispose de son propre serveur DNS qui va g\u00e9rer les requ\u00eates pour les noms de domaines qu'il conna\u00eet. Depuis votre ordinateur assis \u00e0 votre bureau, vous pouvez envoyer une requ\u00eate DNS \u00e0 votre serveur DNS local, lequel va se charger de faire la r\u00e9solution pour vous. Il pourrait r\u00e9pondre directement ou bien il pourrait envoyer une requ\u00eate \u00e0 un serveur DNS de niveau sup\u00e9rieur. Il y a donc une hi\u00e9rarchie de serveurs DNS qui permet de r\u00e9soudre les noms de domaines. Le probl\u00e8me c'est combien de temps est-ce qu'une entr\u00e9e DNS est conserv\u00e9e en cache. Si vous changez l'adresse IP de votre serveur, vous allez mettre \u00e0 jour les informations du domaine, mais il faudra attendre que les caches DNS soient invalid\u00e9s pour que tout le monde puisse acc\u00e9der \u00e0 votre site. Ce param\u00e8tre est appel\u00e9 le TTL (Time To Live) et est configur\u00e9 par le propri\u00e9taire du domaine. Il est souvent de 24 heures pour les sites web, mais il peut \u00eatre plus court pour les services critiques.

    Une entr\u00e9e DNS peut \u00eatre de la forme suivante ou le nom de domaine HEIG-VD est associ\u00e9 \u00e0 l'adresse IP 193.134.219.72 avec un temps de vie de 86400 secondes (24 heures) :

    www.heig-vd.ch.  86400  IN  A  193.134.219.72\n

    Un serveur DNS peut \u00e9galement fonctionner au sein d'une institution pour g\u00e9rer les noms des serveurs internes. Par exemple, si vous avez un serveur de base de donn\u00e9es nomm\u00e9 eistore0 dans votre entreprise, vous pouvez configurer votre serveur DNS pour que ce nom soit associ\u00e9 \u00e0 l'adresse IP de votre serveur.

    ", "tags": ["google.com", "eistore0"]}, {"location": "course-c/48-network/protocoles/#http", "title": "HTTP", "text": "

    Le protocole HTTP (HyperText Transfer Protocol) est un protocole de la couche application qui permet de transf\u00e9rer des donn\u00e9es sur le web. Il est bas\u00e9 sur le protocole TCP et utilise le port 80. Le protocole est d\u00e9fini par la RFC 2616 et est un protocole texte. C'est-\u00e0-dire que les donn\u00e9es sont transmises en clair et que les en-t\u00eates sont des cha\u00eenes de caract\u00e8res.

    Lorsque vous demandez la page d'accueil de Google, votre navigateur va envoyer une requ\u00eate HTTP de type GET \u00e0 l'adresse http://www.google.com/. Cette requ\u00eate aura possiblement la forme suivante\u2009:

    GET / HTTP/1.1\nHost: www.google.com\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\nAccept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nUpgrade-Insecure-Requests: 1\n

    La r\u00e9ponse du serveur sera un message HTTP de type 200 OK avec le contenu de la page d'accueil.

    HTTP/1.1 200 OK\nDate: Tue, 15 Jun 2021 08:00:00 GMT\nServer: Apache/2.4.46 (Unix)\nLast-Modified: Tue, 15 Jun 2021 07:00:00 GMT\nContent-Length: 1234\nContent-Type: text/html\n\n<!DOCTYPE html>\n...\n

    Le code de retour 200 indique que la requ\u00eate a \u00e9t\u00e9 trait\u00e9e avec succ\u00e8s. Il existe de nombreux autres codes organis\u00e9s en cinq cat\u00e9gories\u2009:

    • 1xx\u2009: Information
    • 2xx\u2009: Succ\u00e8s
    • 3xx\u2009: Redirection
    • 4xx\u2009: Erreur client
    • 5xx\u2009: Erreur serveur

    On retrouve l'erreur 404 pour indiquer que la page n'a pas \u00e9t\u00e9 trouv\u00e9e, 403 pour indiquer que l'acc\u00e8s est interdit, 500 pour indiquer une erreur interne du serveur, etc. Plus amusant, on trouve le code 418 pour indiquer que le serveur est une th\u00e9i\u00e8re. C'est un code d'erreur qui n'est pas vraiment utilis\u00e9, mais qui est d\u00e9fini par la RFC 2324 (Hyper Text Coffee Pot Control Protocol).

    "}, {"location": "course-c/48-network/protocoles/#mqtt", "title": "MQTT", "text": "

    Le protocole MQTT (Message Queuing Telemetry Transport) est un protocole de messagerie bas\u00e9 sur le protocole TCP. Il est utilis\u00e9 pour les applications IoT (Internet of Things) pour transmettre des messages entre les capteurs et les serveurs. Le protocole est bas\u00e9 sur le principe de publication/abonnement. Un client peut publier un message sur un topic et un autre client peut s'abonner \u00e0 ce topic pour recevoir les messages. Le protocole est tr\u00e8s l\u00e9ger et est utilis\u00e9 pour les applications qui n\u00e9cessitent une faible consommation d'\u00e9nergie et une faible bande passante. La sp\u00e9cification du protocole est d\u00e9finie par la norme OASIS MQTT et le protocole utilise le port 1883.

    Par exemple un capteur de temp\u00e9rature peut publier un message sur le topic :

    /switzerland/vaud/weather/temperature/42\n

    avec la valeur 25.5 et un client peut s'abonner \u00e0 ce topic pour recevoir les valeurs de temp\u00e9rature en temps r\u00e9el. MQTT est beaucoup utilis\u00e9 pour les applications de l'internet des objets, car il permet de transmettre des messages de mani\u00e8re asynchrone et de mani\u00e8re fiable via un serveur interm\u00e9diaire nomm\u00e9 broker (courtier).

    "}, {"location": "course-c/48-network/sockets/", "title": "Sockets", "text": "

    Un socket (prise en fran\u00e7ais) est un point de communication entre deux processus sur un r\u00e9seau. Il permet l'\u00e9change de donn\u00e9es entre les processus en utilisant majoritairement les protocoles de communication TCP ou UDP.

    H\u00e9las, les sockets ne sont pas disponibles dans la biblioth\u00e8que standard de C, ils ne sont donc pas standardis\u00e9s et donc chaque syst\u00e8me d'exploitation a sa propre impl\u00e9mentation. Cependant, la plupart des syst\u00e8mes d'exploitation modernes supportent (avec de l\u00e9g\u00e8res variations) les sockets BSD (Berkeley Software Distribution).

    Un serveur web est un programme qui utilise les sockets TCP pour \u00e9couter les connexions entrantes et r\u00e9pondre aux requ\u00eates des clients. Un client web est un programme qui utilise les sockets TCP pour se connecter \u00e0 un serveur web et envoyer des requ\u00eates. Une base de donn\u00e9es MySQL est un programme qui expose un socket TCP pour permettre aux clients de se connecter et d'envoyer des requ\u00eates SQL. De m\u00eames, Docker expose un socket Unix pour permettre aux clients de se connecter et de contr\u00f4ler les conteneurs. Les sockets sont partout.

    "}, {"location": "course-c/48-network/sockets/#fonctionnement-des-sockets", "title": "Fonctionnement des sockets", "text": "

    Les sockets sont une couche d'abstraction de la communication entre deux processus (programmes), que ce soit sur le m\u00eame appareil ou sur des machines distantes. Le principe de base d'un socket est d'agir comme un canal de communication pour \u00e9changer des donn\u00e9es binaires. On qualifie souvent un programme de serveur s'il \u00e9coute les connexions entrantes et de client s'il initie une connexion. Une fois la connexion \u00e9tablie, les deux processus peuvent envoyer et recevoir des donn\u00e9es de mani\u00e8re bidirectionnelle.

    Les sockets sont identifi\u00e9s par une adresse IP et un num\u00e9ro de port. L'adresse IP identifie l'appareil sur le r\u00e9seau et le num\u00e9ro de port identifie le processus sur l'appareil. L'organisme IANA (Internet Assigned Numbers Authority) est responsable du maintien des affectations des num\u00e9ros de port pour les communications TCP et UDP. On notera que les ports de 0 \u00e0 1023 sont r\u00e9serv\u00e9s pour les services syst\u00e8me, les ports de 1024 \u00e0 49151 sont r\u00e9serv\u00e9s pour les applications utilisateur et les ports de 49152 \u00e0 65535 sont r\u00e9serv\u00e9s pour les connexions dynamiques (NAT / PAT / UPnP...).

    Le port de loin le plus utilis\u00e9 est le port 80 pour les connexions HTTP, suivi du port 443 pour les connexions HTTPS. Les ports 20 et 21 \u00e9taient utilis\u00e9s jadis pour les connexions FTP. Les services de messagerie (e-mail) utilisent les ports 25, 465 et 587 etc.

    Pour \u00e9tablir un socket, un programme commence par cr\u00e9er un socket avec la fonction socket(). C'est l\u00e0 qu'est d\u00e9fini la famille d'adresse (IPv4 ou IPv6), le type de socket (TCP/UDP) et le protocole. Le socket est ensuite li\u00e9 \u00e0 une adresse et \u00e0 un port sp\u00e9cifique. Une fois reli\u00e9, la fonction listen() permet d'\u00e9couter sur le socket. Un serveur \u00e9coute un socket lorsqu'il attend des connexions entrantes. Une fois une tentative de connexion d\u00e9tect\u00e9e, la fonction accept() est utilis\u00e9e pour \u00e9tablie la connexion et permettre l'\u00e9change de donn\u00e9es. Enfin, les fonctions read(), write() sont utilis\u00e9es pour \u00e9changer des donn\u00e9es. Lorsque la communication est termin\u00e9e, le socket est ferm\u00e9 avec close().

    "}, {"location": "course-c/48-network/sockets/#types-de-sockets", "title": "Types de sockets", "text": "

    Il existe principalement trois cat\u00e9gories de sockets encore utilis\u00e9es\u2009: AF_INET utilis\u00e9 pour les connexions IPv4, AF_INET6 utilis\u00e9 pour les connexions IPv6 et AF_UNIX Utilis\u00e9 pour les connexions Unix. Le pr\u00e9fixe AF signifie Address Family. Si l'on j\u00e8te un oeil \u00e0 <socket.h> on peut constater d'autres familles peu int\u00e9ressantes comme AF_AX25 utilis\u00e9 par les radio amateurs, AF_APPLETALK utilis\u00e9 par les r\u00e9seaux Macintosh entre 1985 et 1995 ou encore AF_AAL5 utilis\u00e9 par les r\u00e9seaux ATM de 1990 \u00e0 2000. On constate l'h\u00e9ritage de l'histoire des r\u00e9seaux dans les sockets.

    Une fois la famille choisie, il faut choisir le type de socket. Il existe trois types de sockets principaux\u2009: SOCK_STREAM pour une connexion TCP, c'est un socket de type flux orient\u00e9 connexion. Il garantit la livraison des donn\u00e9es dans l'ordre et sans perte. SOCK_DGRAM pour une connexion UDP. Il ne garantit pas la livraison des donn\u00e9es ni l'ordre. Il n'y a pas besoin de connexion pr\u00e9alable. Le type SOCK_RAW permet d'acc\u00e9der directement aux trames r\u00e9seau au niveau IP, au-dessus de la couche liaison. Utilis\u00e9 pour impl\u00e9menter des protocoles r\u00e9seau au niveau utilisateur ou pour des outils de diagnostic r\u00e9seau (comme ping, traceroute). Il est g\u00e9n\u00e9ralement utilis\u00e9 par des programmes ayant des privil\u00e8ges \u00e9lev\u00e9s (root, sudo). Les autres types de sockets sont moins utilis\u00e9s et ne m\u00e9ritent pas d'\u00eatre mentionn\u00e9s ici.

    ", "tags": ["AF_AX25", "AF_APPLETALK", "AF_AAL5", "sudo"]}, {"location": "course-c/48-network/sockets/#portabilite", "title": "Portabilit\u00e9", "text": "

    Comme il a \u00e9t\u00e9 \u00e9voqu\u00e9, les sockets sont une API de bas niveau et ne sont pas standardis\u00e9s. Chaque syst\u00e8me d'exploitation a sa propre impl\u00e9mentation des sockets. Heureusement, pour simplifier la portabilit\u00e9 entre syst\u00e8mes, il existe des biblioth\u00e8ques externes qui offrent une abstraction (oui, encore une...) des sockets. La biblioth\u00e8que la plus populaire est libuv qui a \u00e9t\u00e9 d\u00e9velopp\u00e9e initialement pour Node.js. Elle est aujourd'hui utilis\u00e9e par de nombreux projets open-source et est disponible pour la plupart des syst\u00e8mes d'exploitation.

    Lorsque vous d\u00e9veloppez un programme utilisant des sockets, il n'est pas recommand\u00e9 d'utiliser directement les appels syst\u00e8me, ou les fonctions sp\u00e9cifiques \u00e0 votre syst\u00e8me d'exploitation. Pr\u00e9f\u00e9rez utiliser une biblioth\u00e8que externe qui vous simplifiera la vie.

    Notons que si vous d\u00e9veloppez une application graphique avec SDL, GTK, Qt, etc. vous n'aurez pas besoin de g\u00e9rer les sockets directement. Ces biblioth\u00e8ques offrent des fonctions pour g\u00e9rer les connexions r\u00e9seau de mani\u00e8re plus simple et \u00e9galement portable.

    "}, {"location": "course-c/48-network/sockets/#creation-dun-socket", "title": "Cr\u00e9ation d'un socket", "text": "

    L'exercice du jour est la cr\u00e9ation de deux programmes\u2009: un serveur et un client, pour illustrer l'utilisation des sockets en C. Bien entendu cet exemple sera r\u00e9alis\u00e9 sur Linux, sans biblioth\u00e8que externe.

    Le serveur va \u00e9couter les connexions entrantes et le client va se connecter au serveur pour envoyer et recevoir des donn\u00e9es. Pour les besoins de l'exemple, le serveur va simplement r\u00e9pondre \u00e0 ping par pong.

    Voici le serveur\u2009:

    #include <netinet/in.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <unistd.h>\n\n#define PORT 8080\n\nint main() {\n   int server_fd;\n   if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {\n      perror(\"socket failed\");\n      exit(EXIT_FAILURE);\n   }\n\n   struct sockaddr_in address = {.sin_family = AF_INET,\n                                 .sin_addr.s_addr = INADDR_ANY,\n                                 .sin_port = htons(PORT)};\n   const int addrlen = sizeof(address);\n\n   if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {\n      perror(\"bind failed\");\n      exit(EXIT_FAILURE);\n   }\n\n   if (listen(server_fd, 3) < 0) {\n      perror(\"listen\");\n      exit(EXIT_FAILURE);\n   }\n\n   while (1) {\n      int new_socket;\n      if ((new_socket = accept(server_fd, (struct sockaddr *)&address,\n                               (socklen_t *)&addrlen)) < 0) {\n         perror(\"accept\");\n         exit(EXIT_FAILURE);\n      }\n\n      char buffer[1024] = {0};\n      char *response = \"pong\";\n      read(new_socket, buffer, 1024);\n      printf(\"Received: %s\\n\", buffer);\n      send(new_socket, response, strlen(response), 0);\n      printf(\"Response sent\\n\");\n\n      close(new_socket);\n   }\n}\n

    et voici le client\u2009:

    #include <arpa/inet.h>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\n\n#define PORT 8080\nint main() {\n   int sock = 0;\n   if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {\n      printf(\"Socket creation error\\n\");\n      return -1;\n   }\n\n   struct sockaddr_in serv_addr;\n   serv_addr.sin_family = AF_INET;\n   serv_addr.sin_port = htons(PORT);\n\n   if (inet_pton(AF_INET, \"127.0.0.1\", &serv_addr.sin_addr) <= 0) {\n      printf(\"Invalid address / Address not supported\\n\");\n      return -1;\n   }\n\n   if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {\n      printf(\"Connection failed\\n\");\n      return -1;\n   }\n\n   const char *ping_msg = \"ping\";\n\n   char buffer[1024] = {0};\n   send(sock, ping_msg, strlen(ping_msg), 0);\n   printf(\"Ping sent\\n\");\n   int valread = read(sock, buffer, sizeof(buffer));\n   if (valread > 0) printf(\"Server: %s\\n\", buffer);\n\n   close(sock);\n}\n

    Un descripteur de fichier server_fd est cr\u00e9\u00e9 pour le serveur. Il s'agit d'un vrai file descriptor selon le principe de Unix\u2009:everything is a file. Le socket est cr\u00e9\u00e9 sur la famille IPv4 en mode TCP (SOCK_STREAM). Si le socket ne peut pas \u00eatre cr\u00e9\u00e9, la valeur -1 est retourn\u00e9e et le programme se termine.

    Ensuite une structure sockaddr_in est cr\u00e9\u00e9e pour d\u00e9finir l'adresse et le port du serveur. L'adresse est d\u00e9finie \u00e0 INADDR_ANY pour \u00e9couter sur toutes les interfaces r\u00e9seau. Le port est d\u00e9fini \u00e0 8080. La fonction bind() est utilis\u00e9e pour lier le socket \u00e0 l'adresse et au port. Si la fonction \u00e9choue, le programme se termine. Pour le type et la famille de socket choisie, nous utilisons ici la structure sockaddr_in qui est sp\u00e9cifique \u00e0 IPv4. Elle contiendra l'adresse IP et le port du serveur. le port 8080 utilis\u00e9 ici sera converti en big-endian avec la fonction htons() puisque le r\u00e9seau utilise le big-endian, et l'adresse IP est d\u00e9finie \u00e0 INADDR_ANY pour \u00e9couter sur toutes les interfaces r\u00e9seau.

    L'\u00e9tape suivante est de lier le socket \u00e0 l'adresse avec la fonction bind(). Si la fonction \u00e9choue, le programme se termine. Si une erreur doit se produire, c'est tr\u00e8s souvent le bind qui \u00e9choue. En effet, il ne peut y avoir qu'un seul programme \u00e9coutant sur un port donn\u00e9. Si un autre programme \u00e9coute d\u00e9j\u00e0 sur le port 8080, le bind \u00e9chouera avec l'erreur\u2009: Address already in use. C'est une erreur aga\u00e7ante parce qu'il faut chercher quel programme \u00e9coute sur ce port. La commande ss -tulnp permet de lister les programmes \u00e9coutant sur les ports TCP.

    La fonction listen() est ensuite utilis\u00e9e pour d\u00e9marrer l'\u00e9coute. Cette fonction prend en param\u00e8tre le descripteur de fichier du socket et la taille souhait\u00e9e de la file d'attente des connexions entrantes. La taille de la file d'attente est fix\u00e9e \u00e0 3 ici, donc trois clients pourraient \u00eatre en attente de connexion. Si un quatri\u00e8me client tente de se connecter, il recevra un message d'erreur.

    Lorsqu'une demande de connexion est d\u00e9tect\u00e9e, la fonction accept() est utilis\u00e9e pour accepter la connexion. Cette fonction retourne un nouveau descripteur de fichier new_socket qui est utilis\u00e9 pour envoyer et recevoir des donn\u00e9es. La fonction accept() est bloquante, c'est-\u00e0-dire qu'elle attend qu'une connexion soit \u00e9tablie. Si aucune connexion n'est en attente, le programme est mis en pause jusqu'\u00e0 ce qu'une connexion soit \u00e9tablie.

    La fonction read est similaire \u00e0 celle utilis\u00e9e pour lire un fichier. Elle prend en param\u00e8tre le descripteur de fichier, un tampon pour stocker les donn\u00e9es lues et la taille du tampon. Comme pour les fichiers et l'entr\u00e9e standard, la fonction est bloquante. Elle attend que des donn\u00e9es soient disponibles pour les lire. Si la connexion est ferm\u00e9e par le client, la fonction read retourne 0.

    Pourquoi avoir besoin d'un deuxi\u00e8me descripteur de fichier new_socket ? Parce que le socket server_fd est utilis\u00e9 pour \u00e9couter les connexions entrantes, mais une fois une connexion \u00e9tablie, il faut un nouveau socket pour \u00e9changer des donn\u00e9es avec le client. C'est le socket new_socket qui est utilis\u00e9 pour envoyer et recevoir des donn\u00e9es. Comme analogie server_fd est le bureau de r\u00e9ception o\u00f9 les clients arrivent pour se connecter. Il reste ouvert pour accepter de nouveaux clients et new_socket est comme la cabine t\u00e9l\u00e9phonique o\u00f9 le client et le serveur peuvent \u00e9changer des donn\u00e9es.

    ", "tags": ["bind", "pong", "INADDR_ANY", "read", "ping", "server_fd", "new_socket", "sockaddr_in"]}, {"location": "course-c/48-network/sockets/#exemple-portable", "title": "Exemple portable", "text": "

    Pour un exemple portable, nous allons utiliser la biblioth\u00e8que libuv. Cette biblioth\u00e8que est bas\u00e9e sur un mod\u00e8le de programmation asynchrone et \u00e9v\u00e9nementielle. C'est-\u00e0-dire que le programme ne bloque pas lorsqu'il attend une connexion ou des donn\u00e9es, mais qu'il fonctionne avec des callback. C'est un mod\u00e8le de programmation tr\u00e8s populaire pour les applications r\u00e9seau et les serveurs web.

    N\u00e9cessairement l'exemple donn\u00e9 est un peu plus complexe que l'exemple pr\u00e9c\u00e9dent. Il est cependant plus robuste et portable. Voici le serveur\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <uv.h>\n\n#define PORT 8080\n\nvoid alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {\n   buf->base = (char *)malloc(suggested_size);\n   buf->len = suggested_size;\n}\n\nvoid on_write(uv_write_t *req, int status) {\n   if (status) fprintf(stderr, \"Write error: %s\\n\", uv_strerror(status));\n   printf(\"Response sent\\n\");\n   uv_close((uv_handle_t *)req->handle, NULL);\n   free(req);\n}\n\nvoid on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {\n   if (nread > 0) {\n      printf(\"Received: %s\\n\", buf->base);\n      uv_buf_t wrbuf = uv_buf_init(\"pong\", 4);\n      uv_write_t *req = (uv_write_t *)malloc(sizeof(uv_write_t));\n      uv_write(req, client, &wrbuf, 1, on_write);\n   } else if (nread < 0) {\n      if (nread != UV_EOF)\n         fprintf(stderr, \"Read error: %s\\n\", uv_err_name(nread));\n      uv_close((uv_handle_t *)client, NULL);\n   }\n   free(buf->base);\n}\n\nvoid on_new_connection(uv_stream_t *server, int status) {\n   if (status < 0) {\n      fprintf(stderr, \"New connection error: %s\\n\", uv_strerror(status));\n      return;\n   }\n   uv_tcp_t *client = (uv_tcp_t *)malloc(sizeof(uv_tcp_t));\n   uv_tcp_init(uv_default_loop(), client);\n   if (uv_accept(server, (uv_stream_t *)client) == 0)\n      uv_read_start((uv_stream_t *)client, alloc_buffer, on_read);\n   else\n      uv_close((uv_handle_t *)client, NULL);\n}\n\nint main() {\n   uv_loop_t *loop = uv_default_loop();\n   uv_tcp_t server;\n   uv_tcp_init(loop, &server);\n   struct sockaddr_in addr;\n   uv_ip4_addr(\"0.0.0.0\", PORT, &addr);\n   uv_tcp_bind(&server, (const struct sockaddr *)&addr, 0);\n   int r = uv_listen((uv_stream_t *)&server, 128, on_new_connection);\n   if (r) {\n      fprintf(stderr, \"Error listening: %s\\n\", uv_strerror(r));\n      return 1;\n   }\n   printf(\"Server listening on port %d\\n\", PORT);\n   return uv_run(loop, UV_RUN_DEFAULT);\n}\n

    Et voici le client

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <uv.h>\n\n#define PORT 8080\n\nuv_loop_t *loop;\nuv_tcp_t socket_client;\nuv_connect_t connect_req;\n\nvoid alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {\n   buf->base = (char *)malloc(suggested_size);\n   buf->len = suggested_size;\n}\n\nvoid on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {\n   if (nread > 0) {\n      printf(\"Server: %s\\n\", buf->base);\n   } else if (nread < 0) {\n      fprintf(stderr, \"Read error: %s\\n\", uv_err_name(nread));\n      uv_close((uv_handle_t *)client, NULL);\n   }\n\n   free(buf->base);\n}\n\nvoid on_write(uv_write_t *req, int status) {\n   if (status) fprintf(stderr, \"Write error: %s\\n\", uv_strerror(status));\n   printf(\"Ping sent to server\\n\");\n   uv_read_start((uv_stream_t *)req->handle, alloc_buffer, on_read);\n   free(req);\n}\n\nvoid on_connect(uv_connect_t *req, int status) {\n   if (status < 0) {\n      fprintf(stderr, \"Connection error: %s\\n\", uv_strerror(status));\n      return;\n   }\n   printf(\"Connected to server\\n\");\n\n   uv_write_t *write_req = (uv_write_t *)malloc(sizeof(uv_write_t));\n   const char *ping_msg = \"ping\";\n   uv_buf_t buffer = uv_buf_init((char *)ping_msg, strlen(ping_msg));\n   uv_write(write_req, req->handle, &buffer, 1, on_write);\n}\n\nint main() {\n   loop = uv_default_loop();\n   uv_tcp_init(loop, &socket_client);\n   struct sockaddr_in dest;\n   uv_ip4_addr(\"127.0.0.1\", PORT, &dest);\n   uv_tcp_connect(&connect_req, &socket_client, (const struct sockaddr *)&dest,\n                  on_connect);\n   return uv_run(loop, UV_RUN_DEFAULT);  // Boucle d'\u00e9v\u00e9nements\n}\n
    "}, {"location": "course-c/48-network/sockets/#erreurs-courantes", "title": "Erreurs courantes", "text": "

    Les erreurs les plus courantes lors de la cr\u00e9ation d'un socket sont\u2009:

    Address already in use

    Un autre programme \u00e9coute d\u00e9j\u00e0 sur le port sp\u00e9cifi\u00e9. Utilisez la commande ss -tulnp pour lister les programmes \u00e9coutant sur les ports TCP ou sous Windows netstat -aon. Essayez de changer de port.

    Permission denied

    Vous n'avez pas les droits pour \u00e9couter ou sur le port sp\u00e9cifi\u00e9. Essayez de lancer le programme avec les droits root ou sudo. Sous Windows, ex\u00e9cutez le programme en tant qu'administrateur et sous Linux, utilisez sudo. Il faut savoir que les ports de 0 \u00e0 1023 sont r\u00e9serv\u00e9s pour les services syst\u00e8me et n\u00e9cessitent des privil\u00e8ges \u00e9lev\u00e9s pour \u00eatre utilis\u00e9s. Si votre objectif est d'\u00e9crire un programme de test, utilisez un port sup\u00e9rieur \u00e0 1024. Dans le cas d'un socket Unix, v\u00e9rifiez que le fichier de socket n'existe pas d\u00e9j\u00e0 ou si vous vous connectez dessus, v\u00e9rifiez que vous avez les droits n\u00e9cessaires.

    Connection refused

    Le serveur n'accepte pas la connexion. Cela peut \u00eatre d\u00fb \u00e0 un pare-feu, \u00e0 une erreur dans le code du serveur ou \u00e0 une erreur dans l'adresse IP ou le port du client. Il faut investiguer pour trouver la cause.

    Le probl\u00e8me de l'adresse d\u00e9j\u00e0 utilis\u00e9e peut appara\u00eetre m\u00eame si le programme a \u00e9t\u00e9 arr\u00eat\u00e9. Cela est d\u00fb au fait que le syst\u00e8me d'exploitation conserve les sockets en \u00e9tat TIME_WAIT pendant un certain temps (entre 30 secondes et 2 minutes) apr\u00e8s la fermeture du programme. Cela permet de s'assurer que tous les paquets ont \u00e9t\u00e9 re\u00e7us et envoy\u00e9s. Si vous avez besoin de r\u00e9utiliser un port imm\u00e9diatement apr\u00e8s la fermeture du programme, vous pouvez utiliser l'option SO_REUSEADDR avec la fonction setsockopt() :

    int opt = 1;\nif (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {\n    perror(\"setsockopt failed\");\n    exit(EXIT_FAILURE);\n}\n

    Une autre solution est de forcer la fermeture du socket imm\u00e9diatement apr\u00e8s la fermeture du programme avec l'option SO_LINGER :

    struct linger so_linger;\nso_linger.l_onoff = 1;   // Activer SO_LINGER\nso_linger.l_linger = 0;  // Fermer imm\u00e9diatement sans d\u00e9lai\n\nif (setsockopt(server_fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)) < 0) {\n    perror(\"setsockopt failed\");\n    exit(EXIT_FAILURE);\n}\n

    Le d\u00e9lai d'attente sous Linux est d\u00e9termin\u00e9 par le param\u00e8tre net.ipv4.tcp_fin_timeout du noyau. Vous pouvez le modifier avec la commande sysctl -w net.ipv4.tcp_fin_timeout=30 pour le mettre \u00e0 30 secondes par exemple. Vous pouvez \u00e9galement le lire avec la commande sysctl net.ipv4.tcp_fin_timeout. Sous Ubuntu 24.04, le d\u00e9lai est de 60 secondes par d\u00e9faut.

    La derni\u00e8re solution est de lister les connexions en \u00e9tat TIME_WAIT avec la commande suivante et tuer les connexions qui posent probl\u00e8me\u2009:

    ss -o state time-wait '( sport = :8080 )'\n
    ", "tags": ["SO_REUSEADDR", "root", "TIME_WAIT", "sudo", "SO_LINGER"]}, {"location": "course-c/60-safety/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/60-safety/introduction/#securite-du-langage-c", "title": "S\u00e9curit\u00e9 du langage C", "text": "

    Le langage C est un langage de programmation qui permet de manipuler directement la m\u00e9moire de l'ordinateur. Cela permet de r\u00e9aliser des programmes tr\u00e8s performants, mais cela peut aussi \u00eatre dangereux. En effet, si un programme \u00e9crit en C contient une erreur, il peut provoquer des bugs, des plantages, voire des failles de s\u00e9curit\u00e9.

    Le langage n'est pas \u00e9quip\u00e9 de m\u00e9canismes de s\u00e9curit\u00e9 avanc\u00e9s comme le Java ou le C#, qui sont des langages de programmation plus modernes. Il n'y a pas de gestion de d\u00e9passerment de tampon, de v\u00e9rification de la m\u00e9moire, de v\u00e9rification des types, etc. Il est facile de jardiner la m\u00e9moire, c'est-\u00e0-dire d'\u00e9crire dans des zones m\u00e9moires qui ne nous appartiennent pas.

    Des langages plus modernes comme le Zig ou le Rust ont \u00e9t\u00e9 cr\u00e9\u00e9s pour pallier \u00e0 ces probl\u00e8mes. Ils sont plus s\u00e9curis\u00e9s, mais aussi plus complexes \u00e0 apprendre.

    Par ailleurs le C souffre de son anciennet\u00e9 et de l'imp\u00e9rieuse n\u00e9cessit\u00e9 de conserver une compatibilit\u00e9 ascendante. Cela signifie que des fonctionnalit\u00e9s obsol\u00e8tes ou dangereuses sont toujours pr\u00e9sentes dans le langage. Des correctifs ont \u00e9t\u00e9 apport\u00e9s principalement par l'ajout de nouvelles biblioth\u00e8ques ou fonctions.

    "}, {"location": "course-c/60-safety/introduction/#fonctions-dangereuses", "title": "Fonctions dangereuses", "text": "

    Vous avez peut \u00eatre vu des fonctions en C se terminant par le suffixe _s comme strcpy_s, strcat_s, sprintf_s, etc. Ces fonctions sont des versions s\u00e9curis\u00e9es des fonctions classiques strcpy, strcat, sprintf, etc. Elles v\u00e9rifient que la taille des buffers est suffisante pour contenir les donn\u00e9es \u00e0 copier. Si ce n'est pas le cas, elles ne copient pas les donn\u00e9es et retournent une erreur. Ces fonctions s\u00e9curis\u00e9es sont plus lentes que les fonctions classiques, mais elles sont plus s\u00fbres.

    Voici la liste des fonctions s\u00e9curis\u00e9es offertes par le langage, jusqu'\u00e0 C23\u2009:

    Fonctions s\u00e9curis\u00e9es du langage C Fonction Description strcpy_s Copie une cha\u00eene de caract\u00e8res dans un buffer, avec v\u00e9rification de la taille du buffer strcat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res avec v\u00e9rification de la taille du buffer sprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec v\u00e9rification de la taille du buffer strncpy_s Copie une cha\u00eene de caract\u00e8res dans un buffer avec une taille maximale strncat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res avec une taille maximale et v\u00e9rification de la taille du buffer snprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec une taille maximale et v\u00e9rification memcpy_s Copie une zone m\u00e9moire dans une autre zone m\u00e9moire, avec v\u00e9rification de la taille du buffer memmove_s Copie une zone m\u00e9moire dans une autre zone m\u00e9moire, m\u00eame si les zones se chevauchent, avec v\u00e9rification memset_s Remplit une zone m\u00e9moire avec une valeur donn\u00e9e, avec v\u00e9rification du tampon fopen_s Ouvre un fichier de mani\u00e8re s\u00e9curis\u00e9e freopen_s Rouvre un fichier existant de mani\u00e8re s\u00e9curis\u00e9e tmpfile_s Cr\u00e9e un fichier temporaire s\u00e9curis\u00e9 getc_s Lit un caract\u00e8re d'un fichier de mani\u00e8re s\u00e9curis\u00e9e fgets_s Lit une ligne de texte d'un fichier de mani\u00e8re s\u00e9curis\u00e9e, avec gestion de la taille du buffer fread_s Lit des blocs de donn\u00e9es d'un fichier dans un buffer s\u00e9curis\u00e9, avec v\u00e9rification des tailles strerror_s Renvoie un message d'erreur de mani\u00e8re s\u00e9curis\u00e9e bsearch_s Recherche un \u00e9l\u00e9ment dans un tableau tri\u00e9 de mani\u00e8re s\u00e9curis\u00e9e qsort_s Trie un tableau de mani\u00e8re s\u00e9curis\u00e9e fscanf_s Lit des donn\u00e9es format\u00e9es depuis un fichier de mani\u00e8re s\u00e9curis\u00e9e sscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e vsnprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec une taille maximale et v\u00e9rification vsscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e vswscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e wcscpy_s Copie une cha\u00eene de caract\u00e8res larges dans un buffer, avec v\u00e9rification de la taille du buffer wcscat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res larges avec v\u00e9rification de la taille du buffer wcsncpy_s Copie une cha\u00eene de caract\u00e8res larges dans un buffer avec une taille maximale strtok_s D\u00e9coupe une cha\u00eene de caract\u00e8re en tokens de mani\u00e8re s\u00e9curis\u00e9e", "tags": ["qsort_s", "wcsncpy_s", "memset_s", "strncpy_s", "getc_s", "strtok_s", "fgets_s", "strcpy_s", "sprintf_s", "sprintf", "strncat_s", "vsnprintf_s", "wcscat_s", "vswscanf_s", "sscanf_s", "fopen_s", "memcpy_s", "fread_s", "bsearch_s", "strerror_s", "snprintf_s", "vsscanf_s", "strcat", "fscanf_s", "tmpfile_s", "memmove_s", "strcpy", "strcat_s", "wcscpy_s", "freopen_s"]}, {"location": "course-c/60-safety/introduction/#gets", "title": "gets", "text": "

    La fonction gets est une fonction dangereuse qui a \u00e9t\u00e9 retir\u00e9e du langage avec C11 et marqu\u00e9e obsol\u00e8te en C99. Elle lit une ligne de texte depuis l'entr\u00e9e standard et la stocke dans un buffer. Le probl\u00e8me, entre autre partag\u00e9 par d'autres fonctions, est que la fonction ne v\u00e9rifie pas la taille du buffer, ce qui peut provoquer un d\u00e9passement de tampon. Il est recommand\u00e9 d'utiliser la fonction fgets \u00e0 la place, qui prend en param\u00e8tre la taille du buffer. Pour comprendre le probl\u00e8me, voici un exemple de code vuln\u00e9rable\u2009:

    #include <stdio.h>\n\nint main() {\n   char buffer[10];\n   char password[10];\n   gets(buffer);\n\n   printf(\"Buffer :   '%s'\\n\", buffer);\n   printf(\"Password : '%s'\\n\", password);\n}\n

    Dans cet exemple, l'utilisateur peut entrer une cha\u00eene de caract\u00e8res de plus de 10 caract\u00e8res, ce qui provoquera un d\u00e9passement de tampon. La fonction gets ne v\u00e9rifie pas la taille du buffer et \u00e9crira dans la m\u00e9moire adjacente, en l'occurrence la variable password. Cela peut \u00eatre exploit\u00e9 par un attaquant pour \u00e9crire du code malveillant dans la m\u00e9moire et ex\u00e9cuter ce code.

    ", "tags": ["password", "gets", "fgets"]}, {"location": "course-c/60-safety/introduction/#scanf", "title": "scanf", "text": "

    La fonction scanf est une autre fonction dangereuse comme gets ne v\u00e9rifie pas la taille du buffer. L'erreur classique se produit avec le format %s qui lit une cha\u00eene de caract\u00e8res sans v\u00e9rifier la taille du buffer. Une solution est d'utiliser %10s pour limiter la taille de la cha\u00eene \u00e0 10 caract\u00e8res plus le caract\u00e8re nul de fin de cha\u00eene.

    ", "tags": ["gets", "scanf"]}, {"location": "course-c/60-safety/introduction/#mauvaises-pratiques", "title": "Mauvaises pratiques", "text": "

    Cette section regroupe les mauvaises pratiques constat\u00e9es lors mon exp\u00e9rience professionnelle de d\u00e9veloppeurs ainsi que les pi\u00e8ges dans lesquels mes \u00e9tudiants tombent r\u00e9guli\u00e8rement.

    "}, {"location": "course-c/60-safety/introduction/#retour-de-scanf", "title": "Retour de scanf", "text": "

    Bon sang, c'est probablement ce que je r\u00e9p\u00e8te le plus souvent\u2009: Toujours v\u00e9rifier la valeur de retour de scanf. Cette fonction retroune le nombre d'\u00e9l\u00e9ments qui a \u00e9t\u00e9 lus, ou EOF si la fin du fichier est atteinte. Si vous attendez de lire 3 variables, vous devez v\u00e9rifier que le retour est bien \u00e9gal \u00e0 3. Sinon, vous avez un probl\u00e8me.

    int a, b, c;\nif (scanf(\"%d %d %d\", &a, &b, &c) != 3) {\n    fprintf(stderr, \"Erreur de lecture\\n\");\n    return 1;\n}\n
    ", "tags": ["EOF", "scanf"]}, {"location": "course-c/60-safety/introduction/#realloc", "title": "Realloc", "text": "

    La fonction realloc est une fonction dangereuse car elle peut \u00e9chouer et retourner NULL. Si vous ne stocker pas le retour de realloc dans une variable temporaire, vous allez perdre le pointeur sur la m\u00e9moire allou\u00e9e pr\u00e9c\u00e9demment et vous ne pourrez plus jamais la lib\u00e9rer. C'est pourquoi la valeur de retour de realloc doit toujours \u00eatre stock\u00e9es dans un pointeur temporaire et v\u00e9rifi\u00e9e.

    int *tab = malloc(10 * sizeof(int));\nif (tab == NULL) {\n    fprintf(stderr, \"Erreur d'allocation\\n\");\n    return 1;\n}\n\n{\n    int *tmp = realloc(tab, 20 * sizeof(int));\n    if (tmp == NULL) {\n        fprintf(stderr, \"Erreur de r\u00e9allocation\\n\");\n        free(tab);\n        return 1;\n    }\n    tab = tmp;\n}\n
    ", "tags": ["realloc", "NULL"]}, {"location": "course-c/60-safety/introduction/#erreur-et-free", "title": "Erreur et free", "text": "

    Lorsqu'une erreur survient dans une fonction, il est important de lib\u00e9rer les ressources allou\u00e9es avant de quitter la fonction. Si vous oubliez de lib\u00e9rer la m\u00e9moire allou\u00e9e, vous aurez une fuite de m\u00e9moire. Cela peut \u00eatre critique dans un programme qui tourne en continu, car la m\u00e9moire allou\u00e9e ne sera jamais lib\u00e9r\u00e9e et le programme finira par planter. Lorsque vous avez plusieurs \u00e9l\u00e9ments \u00e0 nettoyer, c'est l'une des seule fois o\u00f9 il est acceptable d'utiliser un goto:

    void function() {\n    if (test_error1) goto error;\n    // ...\n    if (test_error2) goto error;\n    // ...\n    if (test_error3) goto error;\n    // ...\n\n  error:\n    free(ptr1);\n    free(ptr2);\n    free(ptr3);\n    return;\n}\n
    ", "tags": ["goto"]}, {"location": "course-c/60-safety/introduction/#vulnerabilites", "title": "Vuln\u00e9rabilit\u00e9s", "text": "

    Ce chapitre aborde les vuln\u00e9rabilit\u00e9s classiques en C tel que le buffer overflow, le remote code execution, l'attaque par format de cha\u00eene, l'attaque de type, etc.

    "}, {"location": "course-c/60-safety/introduction/#buffer-overflow", "title": "Buffer overflow", "text": "

    Les attaquants peuvent exploiter les d\u00e9passements de tampon pour \u00e9craser les adresses de retour sur la pile, permettant ainsi l'ex\u00e9cution de code arbitraire. Il s'agit de l'une des failles de s\u00e9curit\u00e9 les plus critiques dans les programmes C. Par exemple, dans une fonction vuln\u00e9rable, l'attaquant peut injecter un shellcode dans le tampon et modifier l'adresse de retour pour pointer vers ce shellcode.

    "}, {"location": "course-c/60-safety/introduction/#remote-code-execution", "title": "Remote code execution", "text": "

    Les d\u00e9passements de tampon, mal g\u00e9r\u00e9s, peuvent permettre \u00e0 un attaquant de contr\u00f4ler \u00e0 distance le flux d'ex\u00e9cution d'un programme C, menant \u00e0 une ex\u00e9cution de code arbitraire \u00e0 distance.

    "}, {"location": "course-c/60-safety/introduction/#attaque-par-format-de-chaine", "title": "Attaque par format de cha\u00eene", "text": "

    En utilisant des cha\u00eenes de format mal s\u00e9curis\u00e9es dans des fonctions comme printf, un attaquant peut acc\u00e9der \u00e0 des zones de m\u00e9moire sensibles, voire ex\u00e9cuter du code arbitraire.

    char userInput[100];\nscanf(\"%s\", userInput);\nprintf(userInput);  // Vuln\u00e9rabilit\u00e9 de format\n
    ", "tags": ["printf"]}, {"location": "course-c/60-safety/introduction/#attaque-de-type", "title": "Attaque de type", "text": "

    L'attaque use-after-free consiste \u00e0 exploiter un pointeur qui pointe vers une zone m\u00e9moire qui a \u00e9t\u00e9 lib\u00e9r\u00e9e. L'attaquant peut alors r\u00e9allouer cette zone m\u00e9moire et \u00e9crire du code malveillant dedans.

    "}, {"location": "course-c/60-safety/introduction/#thread-safety", "title": "Thread Safety", "text": "

    Un programme ou une fonction est dit thread-safe lorsqu'il peut \u00eatre utilis\u00e9 simultan\u00e9ment par plusieurs threads sans risque de corruption de donn\u00e9es, d'interblocage ou de conditions de concurrence. En C, la gestion de la concurrence est laiss\u00e9e \u00e0 la charge du programmeur. Il est donc de la responsabilit\u00e9 du d\u00e9veloppeur de garantir la thread-safety de son code. N\u00e9anmoins le d\u00e9veloppeur n'a pas une ma\u00eetrise compl\u00e8te de son code code, certaines fonctions de la biblioth\u00e8que standard sont pr\u00e9compil\u00e9es et l'impl\u00e9mentation est g\u00e9n\u00e9ralement opaque \u00e0 l'utilisateur. Un certain nombre de ces fonctions ne sont pas thread-safe.

    En effet, certaines fonctions sont dites stateful, c'est-\u00e0-dire qu'elles utilisent des variables globales pour conserver un \u00e9tat entre les appels. Cela peut poser probl\u00e8me si plusieurs threads utilisent la m\u00eame fonction en m\u00eame temps. L'exemple ls plus \u00e9vident est la fonction rand qui utilise une variable globale pour conserver l'\u00e9tat du g\u00e9n\u00e9rateur de nombres pseudo-al\u00e9atoires. Chaque appel \u00e0 rand modifie cette variable globale et donc influence le r\u00e9sultat des appels suivants. Une autre fonction bien connue est strtok qui utilise \u00e9galement un \u00e9tat interne et qui n'est par cons\u00e9quent pas thread-safe. Pour cette derni\u00e8re, il est recommand\u00e9 d'utiliser la fonction strtok_r \u00e0 la place, qui est la version s\u00e9curis\u00e9e.

    Certains langages plus modernes comme le Rust ou le Go int\u00e8grent nativement la gestion de la concurrence dans le langage. En Rust, par exemple, le compilateur v\u00e9rifie \u00e0 la compilation que le code est thread-safe et ne contient pas de conditions de concurrence. En Go, la gestion de la concurrence est simplifi\u00e9e par l'utilisation de goroutines et de canaux, mais en C, il faut g\u00e9rer la concurrence manuellement.

    Ce qu'il faut retenir c'est que la majorit\u00e9 des fonctions de la biblioth\u00e8que standard du C ne sont pas thread-safe. Il est donc important de faire les recherches pr\u00e9alables avant de les utiliser dans un contexte multithread\u00e9.

    ", "tags": ["rand", "strtok", "strtok_r"]}, {"location": "course-c/60-safety/introduction/#normes", "title": "Normes", "text": "

    Dans le domaine du d\u00e9veloppement en C, la s\u00e9curit\u00e9 et la qualit\u00e9 du code sont r\u00e9gies par des normes rigoureuses qui visent \u00e0 assurer la fiabilit\u00e9 et la robustesse des syst\u00e8mes, notamment dans les secteurs critiques tels que l\u2019automobile, le m\u00e9dical et l\u2019a\u00e9rospatial. Ces normes permettent de pr\u00e9venir les erreurs fatales et de garantir un code de haute qualit\u00e9, en particulier dans des environnements o\u00f9 les cons\u00e9quences d\u2019une d\u00e9faillance peuvent \u00eatre graves. Elles d\u00e9finissent un ensemble de bonnes pratiques et de contraintes visant \u00e0 minimiser les comportements ind\u00e9finis, les vuln\u00e9rabilit\u00e9s, et \u00e0 maximiser la s\u00fbret\u00e9 fonctionnelle.

    "}, {"location": "course-c/60-safety/introduction/#misra-c", "title": "MISRA C", "text": "

    MISRA (Motor Industry Software Reliability Association) est un ensemble de r\u00e8gles de codage destin\u00e9es \u00e0 am\u00e9liorer la s\u00e9curit\u00e9 et la qualit\u00e9 du code en langage C, initialement d\u00e9velopp\u00e9 pour l'industrie automobile. Au fil des ann\u00e9es, MISRA C est devenu une r\u00e9f\u00e9rence non seulement dans le domaine de l\u2019automobile, mais aussi dans d\u2019autres industries o\u00f9 la s\u00e9curit\u00e9 est primordiale.

    Les r\u00e8gles de MISRA C ont pour but d\u2019\u00e9viter des constructions de code dangereuses, de limiter l\u2019utilisation de certaines fonctionnalit\u00e9s du langage C susceptibles de provoquer des comportements ind\u00e9finis, et d\u2019am\u00e9liorer la lisibilit\u00e9 et la maintenabilit\u00e9 du code. Il existe plusieurs versions de la norme (p. ex. : MISRA C:1998, MISRA C:2004, MISRA C:2012), chacune apportant des \u00e9volutions pour prendre en compte les nouvelles r\u00e9alit\u00e9s du d\u00e9veloppement logiciel.

    Les r\u00e8gles de MISRA C sont class\u00e9es en trois cat\u00e9gories\u2009:

    R\u00e8gles obligatoires (Mandatory)

    Elles doivent imp\u00e9rativement \u00eatre respect\u00e9es sans exception. Leur violation peut conduire \u00e0 des comportements ind\u00e9termin\u00e9s, des bugs critiques, voire des failles de s\u00e9curit\u00e9. Voici un exemple de r\u00e8gle obligatoire\u2009:

    Ne jamais utiliser la fonction dangereuse malloc() dans un contexte temps r\u00e9el sans v\u00e9rifier si la m\u00e9moire a bien \u00e9t\u00e9 allou\u00e9e.

    R\u00e8gles n\u00e9cessaires (Required)

    Ces r\u00e8gles doivent \u00eatre suivies, mais elles permettent une certaine flexibilit\u00e9 si des justifications solides sont fournies. Si une r\u00e8gle n\u2019est pas respect\u00e9e, il est imp\u00e9ratif de documenter la raison de l'\u00e9cart et de prouver que la s\u00e9curit\u00e9 n'en est pas compromise. Voici un exemple\u2009:

    Limiter l\u2019usage des conversions implicites de types pour \u00e9viter des erreurs de pr\u00e9cision.

    R\u00e8gles recommand\u00e9es (Advisory)

    Elles sont fortement encourag\u00e9es pour am\u00e9liorer la qualit\u00e9 g\u00e9n\u00e9rale du code, mais leur non-application n\u2019est pas forc\u00e9ment dangereuse, \u00e0 condition qu\u2019elle soit justifi\u00e9e, par exemple\u2009:

    Pr\u00e9f\u00e9rer l\u2019utilisation de macros \u00e0 la place des constantes magiques dans le code pour am\u00e9liorer la lisibilit\u00e9.

    Un concept cl\u00e9 de la norme MISRA C est la matrice de justification. Lorsqu\u2019une r\u00e8gle Required ou Advisory n\u2019est pas respect\u00e9e, il est indispensable d\u2019expliquer pourquoi et de justifier la d\u00e9cision. Cette documentation pr\u00e9cise pourquoi l\u2019\u00e9cart est acceptable et quelles pr\u00e9cautions ont \u00e9t\u00e9 prises pour att\u00e9nuer les risques associ\u00e9s.

    Par exemple, si une \u00e9quipe de d\u00e9veloppement choisit de ne pas respecter une r\u00e8gle concernant l'utilisation d'une fonction sp\u00e9cifique dans un syst\u00e8me embarqu\u00e9 critique, une analyse approfondie doit \u00eatre effectu\u00e9e pour d\u00e9montrer que cette d\u00e9cision n\u2019aura pas d\u2019impact n\u00e9gatif sur la s\u00e9curit\u00e9 globale du syst\u00e8me.

    MISRA C favorise donc une approche pragmatique o\u00f9 l\u2019objectif n\u2019est pas de suivre aveugl\u00e9ment les r\u00e8gles, mais de les appliquer intelligemment, en justifiant tout \u00e9cart d\u2019une mani\u00e8re rigoureuse et tra\u00e7able.

    Certaines r\u00e8gles MISRA peuvent sembler restrictives ou contraignantes et ne servir ni la lisibilit\u00e9 du code, ni la s\u00e9curit\u00e9. Par exemple, la r\u00e8gle recommand\u00e9e MISRA-C:2012 15.5 d\u00e9cr\u00e8te que\u2009: A function should have a single point of exit at the end. Dit autremement, une fonction ne peut pas avoir plusieurs return.

    Le point de retour unique est requis par la norme IEC 61508, qui concerne les syst\u00e8mes embarqu\u00e9s critiques ainsi que l'ISO 26262 pour l'industrie automobile. L'argumentaire est qu'un retour pr\u00e9matur\u00e9 peut mener \u00e0 des omission involontaires de nettoyage de la m\u00e9moire ou de lib\u00e9ration de ressources.

    Ces r\u00e8gles, nombreuses, ont des rationels toujours fond\u00e9s mais qui peuvent parfois \u00eatre en contradiction avec les bonnes pratiques de d\u00e9veloppement moderne. Prenons l'exemple suivant de cette fonction qui n'a qu'un point de retour. Elle implique un sch\u00e9ma plus compliqu\u00e9 et une variable suppl\u00e9mentaire pour stocker le r\u00e9sultat de retour.

    int foo(FILE *fp, char *buffer, size_t size) {\n    int error = 0;\n    if (fp != NULL) {\n        if (buffer != NULL) {\n            if (size > 0) {\n                fread(buffer, 1, size, fp);\n            } else {\n                error = 4;\n            }\n        } else {\n            error = 3;\n        }\n    } else {\n        error = 2;\n    }\n    return error;\n}\n

    En s'autorisant plusieurs points de retour, le code est plus lisible et plus concis\u2009:

    int foo(FILE *fp, char *buffer, size_t size) {\n    if (fp == NULL) return 2;\n    if (buffer == NULL) return 3;\n    if (size == 0) return 4;\n    fread(buffer, 1, size, fp);\n    return 0;\n}\n

    L'objectif n'est pas ici de vous monter contre MISRA-C qui est une excellente norme, mais de vous montrer qu'il est important d'avoir un esprit critque et de ne pas suivre aveugl\u00e9ment les r\u00e8gles. Cette r\u00e8gle en question n'est que recommand\u00e9e, la norme vous pousse \u00e0 vous interroger sur la pertinence de l'appliquer ou non, de s'assurer que vous comprenez les risques et les b\u00e9n\u00e9fices.

    Voici pour information quelques r\u00e8gles de la norme MISRA C:2012\u2009:

    Exemples de r\u00e8gles MISRA C:2012 R\u00e8gle Description Cat\u00e9gorie Rule 1.1 Les fichiers source ne doivent pas contenir de code non standard Mandatory Rule 1.2 Les fichiers source doivent \u00eatre conformes \u00e0 la norme ISO 9899:1999 Mandatory Rule 2.1 Tout code inutile doit \u00eatre supprim\u00e9 Required Rule 8.7 Les objets non utilis\u00e9s doivent \u00eatre supprim\u00e9s Required Rule 10.1 Les types de donn\u00e9es ne doivent pas \u00eatre m\u00e9lang\u00e9s dans les expressions Required Rule 11.3 Un cast entre des pointeurs de types diff\u00e9rents ne doit pas \u00eatre effectu\u00e9 Required Rule 14.3 Les contr\u00f4les de boucles doivent \u00eatre constants et d\u00e9finis Required Rule 15.5 Il doit y avoir une clause default dans chaque instruction switch Required Rule 16.7 Les arguments de fonction ne doivent pas \u00eatre ignor\u00e9s Required Rule 17.2 Les index des tableaux doivent \u00eatre dans les limites du tableau Required Rule 18.1 La m\u00e9moire dynamique (malloc, free) ne doit pas \u00eatre utilis\u00e9e Required Rule 8.13 Les variables automatiques doivent \u00eatre initialis\u00e9es avant utilisation Advisory Rule 17.5 Ne pas acc\u00e9der directement \u00e0 un tableau avec des pointeurs Advisory Rule 20.4 Ne pas utiliser printf, scanf, sprintf de la biblioth\u00e8que standard Advisory", "tags": ["sprintf", "scanf", "malloc", "switch", "default", "free", "printf", "return"]}, {"location": "course-c/60-safety/introduction/#iso-26262", "title": "ISO 26262", "text": "

    La norme ISO 26262 est une norme internationale pour la s\u00e9curit\u00e9 fonctionnelle (functional safety) dans l'industrie automobile. Elle couvre le cycle de vie entier du d\u00e9veloppement des syst\u00e8mes embarqu\u00e9s dans les v\u00e9hicules, de la conception initiale \u00e0 la production et la maintenance. Cette norme est cruciale dans les v\u00e9hicules modernes o\u00f9 l\u2019\u00e9lectronique et le logiciel jouent un r\u00f4le fondamental dans la s\u00e9curit\u00e9 (syst\u00e8mes d\u2019assistance \u00e0 la conduite, gestion moteur, etc.).

    Les exigences cl\u00e9s de cette norme sont\u2009:

    Classification ASIL (Automotive Safety Integrity Level)

    Elle indique le niveau de criticit\u00e9 du syst\u00e8me. Il est class\u00e9 de A (le plus faible) \u00e0 D (le plus critique), et les exigences de d\u00e9veloppement augmentent avec le niveau de criticit\u00e9. Par exemple, un syst\u00e8me de freinage ABS est class\u00e9 ASIL D, car une d\u00e9faillance de ce syst\u00e8me peut entra\u00eener des accidents graves.

    Tests et couverture du code

    La norme impose la r\u00e9alisation de tests rigoureux pour garantir que le code est exempt de bugs critiques. Un niveau de couverture du code \u00e9lev\u00e9 est exig\u00e9, souvent plus de 95 pour cent, avec des tests unitaires, des tests de stress et des tests d\u2019int\u00e9gration qui couvrent aussi bien les chemins normaux que les cas extr\u00eames.

    V\u00e9rification et validation

    Un processus rigoureux de v\u00e9rification et de validation doit \u00eatre mis en place pour s\u2019assurer que toutes les exigences de s\u00e9curit\u00e9 sont respect\u00e9es. Cela inclut des revues de code, des tests de s\u00e9curit\u00e9, des analyses de risques, etc.

    Rappelez-vous, d\u00e9velopper du code pour l'automobile c'est lent, c'est cher, c'est compliqu\u00e9. C'est lent car chaque changement doit \u00eatre valid\u00e9, chaque ligne de code doit \u00eatre test\u00e9e. C'est cher car les tests sont co\u00fbteux et les outils sont chers (parce qu'ils sont certifi\u00e9s). C'est compliqu\u00e9 car il faut respecter des normes, des standards, des processus.

    "}, {"location": "course-c/60-safety/introduction/#iec-62304", "title": "IEC 62304", "text": "

    La norme IEC 62304 est une norme internationale d\u00e9di\u00e9e aux logiciels de dispositifs m\u00e9dicaux. Elle sp\u00e9cifie les processus de d\u00e9veloppement, de maintenance et de gestion des risques associ\u00e9s aux logiciels critiques utilis\u00e9s dans les appareils m\u00e9dicaux, tels que les syst\u00e8mes de monitoring, les appareils d\u2019imagerie m\u00e9dicale ou encore les pacemakers.

    Elle est tr\u00e8s similaires \u00e0 l'ISO 26262, mais adapt\u00e9e aux dispositifs m\u00e9dicaux. Les exigences de s\u00e9curit\u00e9 sont tout aussi strictes, car une d\u00e9faillance d\u2019un logiciel m\u00e9dical peut avoir des cons\u00e9quences dramatiques pour les patients. La classification m\u00e9dicale est similaire \u00e0 l'ASIL de l'ISO 26262, allant de A (le moins critique) \u00e0 C (le plus critique).

    "}, {"location": "course-c/60-safety/introduction/#compilateur-certifie", "title": "Compilateur certifi\u00e9", "text": "

    Dans le contexte de l\u2019application des normes comme l\u2019ISO 26262 et l\u2019IEC 62304, le choix du compilateur est critique. Un compilateur non certifi\u00e9 peut introduire des comportements non d\u00e9terministes ou des optimisations dangereuses qui ne respectent pas les contraintes de s\u00fbret\u00e9.

    Contrairement \u00e0 ce que l\u2019on pourrait penser, des compilateurs populaires comme GCC ou Clang ne sont pas certifi\u00e9s pour une utilisation dans des syst\u00e8mes critiques n\u00e9cessitant une conformit\u00e9 aux normes de s\u00e9curit\u00e9. Cela signifie que, bien qu\u2019ils soient extr\u00eamement performants et largement utilis\u00e9s dans l\u2019industrie g\u00e9n\u00e9rale, ils ne peuvent pas \u00eatre utilis\u00e9s tels quels dans des syst\u00e8mes r\u00e9pondant \u00e0 des normes comme l\u2019ISO 26262 ou l\u2019IEC 62304.

    Pour les projets critiques, des compilateurs certifi\u00e9s tels que Green Hills, IAR Systems, ou Tasking sont souvent utilis\u00e9s. Ces compilateurs sont conformes aux exigences de s\u00e9curit\u00e9, et leur comportement est rigoureusement v\u00e9rifi\u00e9 pour garantir qu\u2019ils ne produisent pas de code incorrect ou dangereux dans des environnements critiques. Ces compilateurs offrent \u00e9galement des fonctionnalit\u00e9s de suivi et d\u2019audit qui facilitent la conformit\u00e9 avec les normes de s\u00e9curit\u00e9. Ils int\u00e8grent g\u00e9n\u00e9ralement MISRA C et d\u2019autres r\u00e8gles de codage directement.

    "}, {"location": "course-c/70-philosophy/funny/", "title": "Humour d'informaticien", "text": "There are only two kinds of programming languages: those people always bitch about and those nobody uses.Bjarne Stroustrup

    L'humour des informaticiens, souvent empreint de subtilit\u00e9 et d'ironie, s'est peu \u00e0 peu impos\u00e9 comme un langage \u00e0 part enti\u00e8re au sein de la communaut\u00e9 des d\u00e9veloppeurs. Popularis\u00e9 par des personnalit\u00e9s et des \u0153uvres embl\u00e9matiques telles que XKCD, le c\u00e9l\u00e8bre webcomic de Randall Munroe, cet humour n'est pas simplement un moyen de divertissement, mais un outil pr\u00e9cieux pour les professionnels du code. D'autres figures marquantes, comme The Oatmeal, Dilbert ou encore CommitStrip, ont \u00e9galement su capturer l'essence de l'exp\u00e9rience informatique, transformant les d\u00e9fis techniques et les absurdit\u00e9s bureaucratiques en sources de rires partag\u00e9s.

    Ce type d'humour, bienveillant et souvent auto-d\u00e9risoire, joue un r\u00f4le fondamental dans la vie d'un d\u00e9veloppeur. Il permet de prendre du recul face \u00e0 la complexit\u00e9 et \u00e0 la frustration inh\u00e9rentes au m\u00e9tier. Les blagues sur les bugs r\u00e9calcitrants, les interminables r\u00e9unions ou les fonctions improbables deviennent des catalyseurs de coh\u00e9sion au sein des \u00e9quipes. En riant ensemble de leurs d\u00e9boires quotidiens, les d\u00e9veloppeurs cr\u00e9ent un environnement de travail plus l\u00e9ger et plus humain, o\u00f9 l'erreur n'est pas un \u00e9chec, mais une \u00e9tape normale du processus d'apprentissage.

    Cet humour est \u00e9galement une forme d'\u00e9vasion, un moyen de d\u00e9dramatiser les situations stressantes et de rappeler \u00e0 chacun que, malgr\u00e9 la rigueur technique n\u00e9cessaire \u00e0 leur travail, il est important de ne pas se prendre trop au s\u00e9rieux. Il favorise la cr\u00e9ativit\u00e9, l'innovation et un \u00e9tat d'esprit ouvert, essentiels \u00e0 la r\u00e9solution de probl\u00e8mes complexes. En somme, l'humour des informaticiens est bien plus qu'une simple plaisanterie\u2009: c'est un levier de bien-\u00eatre et de productivit\u00e9, au c\u0153ur m\u00eame de la culture du d\u00e9veloppement logiciel.

    Voici quelques exemples de blagues et de comics qui illustrent ces propos.

    "}, {"location": "course-c/70-philosophy/funny/#une-nouvelle-norme", "title": "Une nouvelle norme\u2009?", "text": "

    Parfois un d\u00e9veloppeur a une id\u00e9e brillante pour une nouvelle technologie, un nouveau protocol de communication, un nouveau format de fichier, etc. Il est persuad\u00e9 que ce nouveau standard plus simple, plus efficace, plus rapide, plus s\u00e9curis\u00e9, etc. va r\u00e9volutionner le monde de l'informatique et remplacer de sucro\u00eet plusieurs standards existants.

    H\u00e9las, le constat est souvent le m\u00eame\u2009: le nouveau standard ne sera qu'un \u00e9ni\u00e8me standard \u00e0 ajouter \u00e0 la liste des standards existants.

    XKCD Standards

    "}, {"location": "course-c/70-philosophy/funny/#unicode_1", "title": "Unicode", "text": "

    Unicode est dirig\u00e9 par un consortium de grandes entreprises technologiques et de parties prenantes. Les fondateurs d'Unicode comprennent Joe Becker, qui travaillait pour Xerox dans les ann\u00e9es 80. Il avait une barbe et il se pourrait bien qu'il est le personnage figurant dans le premier et le troisi\u00e8me panneaux.

    Les emoji ont \u00e9t\u00e9 initialement ajout\u00e9s pour \u00eatre compatibles avec les encodages de messages texte au Japon. Mais ce n'est plus le cas. L'emoji homard (\ud83e\udd9e) a \u00e9t\u00e9 approuv\u00e9 en 2018, date de sortie de ce comics.

    XKCD Unicode

    "}, {"location": "course-c/70-philosophy/funny/#vrais-programmeurs", "title": "Vrais programmeurs", "text": "

    Un rappel \u00e0 aux guerres d'\u00e9diteurs de texte, en particulier Vim et Emacs... De toute mani\u00e8re Vim \u00e0 gagn\u00e9 la guerre, non\u2009?

    XKCD Real Programmers

    "}, {"location": "course-c/70-philosophy/funny/#compilation", "title": "Compilation", "text": "

    Compiler un programme est une t\u00e2che qui peut prendre du temps. Parfois, il est n\u00e9cessaire de compiler un programme plusieurs fois pour obtenir un programme fonctionnel. Selon la taille du programme, le d\u00e9veloppeur peut avoir le temps de faire une pause ou plusieurs, de prendre un caf\u00e9 ou plusieurs, de lire un livre, ou de justifier n'importe quelle autre activit\u00e9...

    XKCD Compiling

    "}, {"location": "course-c/70-philosophy/funny/#le-code-des-autres", "title": "Le code des autres", "text": "

    Le code des autres est souvent difficile \u00e0 comprendre. Parfois, il est m\u00eame difficile de comprendre son propre code. C'est pourquoi il est important de commenter son code, de le documenter, de le tester, de le relire, de le refactoriser, etc.

    Le bon code

    "}, {"location": "course-c/70-philosophy/funny/#la-faute-a-personne", "title": "La faute \u00e0 personne", "text": "

    La collaboration au sein d'une \u00e9quipe de d\u00e9veloppement est cruciale pour le succ\u00e8s d'un projet. Lorsque chaque membre se contente de respecter les sp\u00e9cifications des API sans communication r\u00e9elle, les composants logiciels risquent de ne pas fonctionner ensemble. Chacun pense avoir bien fait, et le bl\u00e2me se d\u00e9place de l'un \u00e0 l'autre, tandis que personne ne veut corriger le probl\u00e8me. Ce dilemme, souvent li\u00e9 aux co\u00fbts du travail, rappelle que la solution ne r\u00e9side pas dans la recherche d'un coupable, mais dans un compromis et une coop\u00e9ration pour le bien commun.

    La faute \u00e0 personne

    "}, {"location": "course-c/70-philosophy/funny/#commentaire-de-commit", "title": "Commentaire de commit", "text": "

    La tentation est grande de miniser le temps pass\u00e9 \u00e0 r\u00e9diger des commentaires de commit. Pourtant, ces messages sont essentiels pour comprendre l'\u00e9volution du code, pour suivre les modifications apport\u00e9es, pour identifier les erreurs, etc. Un bon commentaire de commit est clair, concis, informatif et utile. Il permet de retracer l'historique du code, de faciliter la collaboration entre les d\u00e9veloppeurs, de documenter les changements, etc.

    Commentaire de commit

    "}, {"location": "course-c/70-philosophy/philosophy/", "title": "Philosophie", "text": "

    La philosophie d'un bon d\u00e9veloppeur repose sur plusieurs principes de programmation relevant majoritairement du bon sens de l'ing\u00e9nieur. Les vaudois l'appelant parfois\u2009: le bon sens paysan comme l'aurait sans doute confirm\u00e9 Jean Villard dit Gilles.

    "}, {"location": "course-c/70-philosophy/philosophy/#rasoir-dockham", "title": "Rasoir d'Ockham", "text": "

    Illustration humoristique du rasoir d'Ockham

    Le rasoir d'Ockham expose en substance que les multiples ne doivent pas \u00eatre utilis\u00e9s sans n\u00e9cessit\u00e9. C'est un principe d'\u00e9conomie, de simplicit\u00e9 et de parcimonie. Il peut \u00eatre r\u00e9sum\u00e9 par la devise Shadok, non sans une pointe d'ironie\u2009: \u00ab\u2009Pourquoi faire simple quand on peut faire compliqu\u00e9\u2009?\u2009\u00bb

    En philosophie, un rasoir est une m\u00e9thode heuristique visant \u00e0 \u00e9liminer les explications invraisemblables d'un ph\u00e9nom\u00e8ne donn\u00e9. Ce principe tire son nom de Guillaume d'Ockham, un penseur du XIVe si\u00e8cle, bien que son origine remonte probablement \u00e0 Emp\u00e9docle (\u1f18\u03bc\u03c0\u03b5\u03b4\u03bf\u03ba\u03bb\u1fc6\u03c2), aux environs de 450 avant J.-C.

    Ce principe trouve une r\u00e9sonance particuli\u00e8re en programmation, domaine o\u00f9 le d\u00e9veloppeur ne peut appr\u00e9hender la totalit\u00e9 d'un logiciel, intrins\u00e8quement insaisissable \u00e0 l'\u0153il humain. Seuls la simplicit\u00e9 et l'art de la conception logicielle peuvent le pr\u00e9server du chaos, car un programme, quel qu'en soit l'envergure, peut demeurer limpide pour peu que chaque strate de son architecture reste claire et intelligible pour quiconque souhaiterait contribuer \u00e0 l'\u0153uvre d'autrui.

    "}, {"location": "course-c/70-philosophy/philosophy/#leffet-dunning-kruger", "title": "L'effet Dunning-Kruger", "text": "

    L' est un biais cognitif qui se manifeste par une surestimation des comp\u00e9tences d'une personne. Les personnes les moins comp\u00e9tentes dans un domaine ont tendance \u00e0 surestimer leurs comp\u00e9tences, tandis que les personnes les plus comp\u00e9tentes ont tendance \u00e0 les sous-estimer.

    L'effet Dunning-Kruger est un biais cognitif o\u00f9 les individus tendent \u00e0 surestimer leurs comp\u00e9tences. Paradoxalement, ce sont les moins exp\u00e9riment\u00e9s dans un domaine qui s'illusionnent le plus sur leurs capacit\u00e9s, tandis que les experts, eux, ont tendance \u00e0 sous-\u00e9valuer leur ma\u00eetrise.

    Illustration satirique de l'effet Dunning-Kruger

    J'ai souvent observ\u00e9 ce biais de surconfiance chez mes \u00e9tudiants et coll\u00e8gues, et je n'en ai pas \u00e9t\u00e9 exempt moi-m\u00eame. Il est en effet difficile de jauger avec pr\u00e9cision son propre niveau de comp\u00e9tence, et ce n'est qu'en se confrontant au regard critique de ses pairs que l'on prend v\u00e9ritablement conscience de ses lacunes. Soumettre son code \u00e0 l'examen d'autrui peut \u00eatre une d\u00e9marche intimidante, mais elle s'av\u00e8re \u00eatre une source d'enrichissement inestimable.

    Note

    L'effet Dunning-Kruger ne fait pas consensus au sein de la communaut\u00e9 scientifique, mais il est souvent cit\u00e9 en psychologie populaire.

    "}, {"location": "course-c/70-philosophy/philosophy/#ultracrepidarianisme", "title": "Ultracr\u00e9pidarianisme", "text": "

    L'ultracr\u00e9pidarianisme, terme rare mais puissant, d\u00e9signe l'art de s'exprimer avec assurance sur des sujets que l'on ne ma\u00eetrise gu\u00e8re. Ce ph\u00e9nom\u00e8ne, aussi ancien que la parole elle-m\u00eame, se manifeste lorsque des individus, ignorants des nuances et des subtilit\u00e9s d'un domaine, s'\u00e9rigent en experts. \u00c9tienne Klein, physicien et philosophe des sciences, a popularis\u00e9 ce mot en France, mettant en garde contre les dangers de cette posture, notamment \u00e0 l'\u00e9poque du Covid-19, o\u00f9 les voix des v\u00e9ritables sp\u00e9cialistes furent souvent noy\u00e9es par le vacarme de ceux qui, en sachant moins, parlaient davantage.

    Dans le domaine de l'informatique, ce travers est particuli\u00e8rement pr\u00e9gnant. Les d\u00e9veloppeurs, en premi\u00e8re ligne de la complexit\u00e9 technique, se voient fr\u00e9quemment dict\u00e9s leur conduite par ceux qui ne partagent ni leur expertise ni leur compr\u00e9hension des enjeux. Directeurs, managers, clients \u2013 tous s'autorisent \u00e0 \u00e9mettre des avis, \u00e0 imposer des choix, souvent au m\u00e9pris des r\u00e9alit\u00e9s techniques. Cette immixtion, loin d'\u00eatre anodine, est une source constante de frustration. Pire encore, elle peut mener \u00e0 des d\u00e9sastres techniques, notamment lorsque des d\u00e9cisions mal avis\u00e9es entra\u00eenent une accumulation de dette technique, cette gangr\u00e8ne silencieuse du code que l'on reporte de corriger, jusqu'au jour o\u00f9 l'effort n\u00e9cessaire pour la r\u00e9sorber devient titanesque, voire impossible.

    Ainsi, l'ultracr\u00e9pidarianisme, en s'infiltrant dans les rouages du d\u00e9veloppement logiciel, menace l'\u00e9quilibre d\u00e9licat entre cr\u00e9ation et rigueur, innovation et solidit\u00e9. Il est un rappel de l'importance de la modestie, de la n\u00e9cessit\u00e9 d'\u00e9couter ceux qui savent, et de la sagesse qu'il y a \u00e0 reconna\u00eetre ses propres limites.

    "}, {"location": "course-c/70-philosophy/philosophy/#philosophie-de-conception", "title": "Philosophie de conception", "text": "

    Ces principes constituent des lignes directrices essentielles pour aider le d\u00e9veloppeur \u00e0 structurer son code de mani\u00e8re \u00e0 le rendre plus lisible, plus maintenable et moins susceptible de contenir des erreurs humaines.

    Il ne suffit pas qu'un programme fonctionne correctement ou qu'il satisfasse les attentes d'un sup\u00e9rieur hi\u00e9rarchique\u2009; l'attitude du programmeur va bien au-del\u00e0 du simple acte de coder. Cet \u00e9tat d'esprit ne s'enseigne pas, il s'acquiert avec l'exp\u00e9rience.

    Voici les quatre principes les plus embl\u00e9matiques\u2009:

    DRY

    Ne vous r\u00e9p\u00e9tez pas. (Do not repeat yourself.)

    KISS

    Restez simple, stupide. (Keep it simple, stupid.)

    SSOT

    Une seule source de v\u00e9rit\u00e9. (Single source of truth.)

    YAGNI

    Vous n'en aurez pas besoin. (You ain't gonna need it.)

    "}, {"location": "course-c/70-philosophy/philosophy/#dry_1", "title": "DRY", "text": "

    Ne vous r\u00e9p\u00e9tez pas (Don't Repeat Yourself) ! Je le r\u00e9p\u00e8te\u2009: ne vous r\u00e9p\u00e9tez pas ! Ce principe fondamental du d\u00e9veloppement logiciel vise \u00e0 \u00e9viter la redondance de code. Dans leur ouvrage incontournable, The Pragmatic Programmer, Andrew Hunt et David Thomas le formulent ainsi\u2009:

    Dans un syst\u00e8me, toute connaissance doit avoir une repr\u00e9sentation unique, non ambigu\u00eb et faisant autorit\u00e9.

    En d'autres termes, le programmeur doit rester constamment vigilant, pr\u00eat \u00e0 entendre une alarme mentale, rouge, vive et bruyante d\u00e8s qu'il s'appr\u00eate \u00e0 utiliser la combinaison Ctrl+C (ou Cmd+C), suivie de Ctrl+V (ou Cmd+V). Dupliquer du code, quelle que soit la quantit\u00e9, est toujours une mauvaise pratique, car cela trahit souvent un code smell, signal \u00e9vident que le code pourrait \u00eatre simplifi\u00e9 et optimis\u00e9.

    L'exemple de code suivant pr\u00e9sente une violation du principe DRY : la fonction display est appel\u00e9e deux fois. Dans les deux cas, elle re\u00e7oit un pointeur sur un fichier, ce qui indique qu'une simplification est possible.

    FILE *fp = NULL;\nif (argc > 1) {\n    fp = fopen(argv[1], \"r\");\n    display(fp);\n}\nelse {\n    display(stdin);\n}\n

    Voici la version corrig\u00e9e\u2009:

    FILE *fp = argc > 1 ? fopen(argv[1], \"r\") : stdin;\ndisplay(fp);\n

    ", "tags": ["display"]}, {"location": "course-c/70-philosophy/philosophy/#kiss_1", "title": "KISS", "text": "

    Keep it simple, stupid est une ligne directrice de conception qui encourage la simplicit\u00e9 d'un d\u00e9veloppement. Elle est similaire au rasoir d'Ockham, mais plus commune en informatique. \u00c9nonc\u00e9 par Eric Steven Raymond puis par le Zen de Python un programme ne doit faire qu'une chose, et une chose simple. C'est une philosophie grandement respect\u00e9e dans l'univers Unix/Linux. Chaque programme de base du shell (ls, cat, echo, grep...) ne fait qu'une t\u00e2che simple, le nom est court et simple \u00e0 retenir.

    La fonction suivante n'est pas KISS car elle est responsable de plusieurs t\u00e2ches\u2009: v\u00e9rifier les valeurs d'un set de donn\u00e9e et les afficher\u2009:

    int process(Data *data, size_t size) {\n    // Check consistency and display\n    for (int i = 0; i < size; i++) {\n        if (data[i].value <= 0)\n            data[i].value = 1;\n\n        printf(\"%lf\\n\", 20 * log10(data[i].value));\n    }\n}\n

    Il serait pr\u00e9f\u00e9rable de la d\u00e9couper en deux sous-fonctions\u2009:

    #define TO_LOG(a) (20 * log10(a))\n\nint fix_data(Data *data, const size_t size) {\n    for (int i = 0; i < size; i++) {\n        if (data[i].value <= 0)\n            data[i].value = 1;\n    }\n}\n\nint display(const Data *data, const size_t size) {\n    for (int i = 0; i < size; i++)\n        printf(\"%lf\\n\", TO_LOG(data[i].value));\n}\n

    ", "tags": ["cat", "echo", "grep"]}, {"location": "course-c/70-philosophy/philosophy/#yagni_1", "title": "YAGNI", "text": "

    YAGNI est un anglicisme de you ain't gonna need it qui peut \u00eatre traduit par\u2009: vous n'en aurez pas besoin. C'est un principe tr\u00e8s connu en d\u00e9veloppent Agile XP (Extreme Programming) qui stipule qu'un d\u00e9veloppeur logiciel ne devrait pas impl\u00e9menter une fonctionnalit\u00e9 \u00e0 un logiciel tant que celle-ci n'est pas absolument n\u00e9cessaire.

    Ce principe combat le biais du d\u00e9veloppeur \u00e0 vouloir sans cesse d\u00e9marrer de nombreux chantiers sans se focaliser sur l'essentiel strictement n\u00e9cessaire d'un programme et permettant de satisfaire au cahier des charges convenu avec le partenaire/client.

    "}, {"location": "course-c/70-philosophy/philosophy/#ssot_1", "title": "SSOT", "text": "

    Ce principe tient son acronyme de single source of truth. Il adresse principalement un d\u00e9faut de conception relatif aux m\u00e9tadonn\u00e9es que peuvent \u00eatre les param\u00e8tres d'un algorithme, le mod\u00e8le d'une base de donn\u00e9es ou la m\u00e9thode usit\u00e9e d'un programme \u00e0 collecter des donn\u00e9es.

    Un programme qui respecte ce principe \u00e9vite la duplication des donn\u00e9es. Des d\u00e9fauts courants de conception sont\u2009:

    • indiquer le nom d'un fichier source dans le fichier source\u2009;

      /**\n * @file main.c\n */\n
    • stocker la m\u00eame image, le m\u00eame document dans diff\u00e9rents formats\u2009;

      convert input.jpg -resize 800x800 image-small.jpg\nconvert input.jpg -resize 400x400 image-smaller.jpg\nconvert input.jpg -resize 10x10 image-tiny.jpg\ngit add image-small.jpg image-smaller.jpg image-tiny.jpg\n
    • stocker dans une base de donn\u00e9es le nom Doe, pr\u00e9nom John ainsi que le nom complet\u2009;

      INSERT INTO users (first_name, last_name, full_name)\nVALUES ('John', 'Doe', 'John Doe');\n
    • avoir un commentaire C ayant deux v\u00e9rit\u00e9s contradictoires\u2009;

      int height = 206; // Size of Haf\u00fe\u00f3r J\u00fal\u00edus Bj\u00f6rnsson which is 205 cm\n
    • conserver une copie des m\u00eames donn\u00e9es sous des formats diff\u00e9rents (un tableau de donn\u00e9es brutes et un tableau des m\u00eames donn\u00e9es, mais tri\u00e9es).

      ssconvert data.csv data.xlsx\nlibreoffice --headless --convert-to pdf fichier.csv\ngit add data.csv data.xlsx data.pdf # Beurk !\ngit commit -m \"Add all data formats\"\n
    "}, {"location": "course-c/70-philosophy/philosophy/#zen-de-python", "title": "Zen de Python", "text": "

    Le Zen de Python est un ensemble de 19 principes publi\u00e9s en 1999 par Tim Peters. Largement accept\u00e9 par la communaut\u00e9 de d\u00e9veloppeurs et il est connu sous le nom de PEP 20.

    Voici le texte original anglais\u2009:

    Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one\u2014and preferably only one\u2014obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than right now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea\u2014let's do more of those\u2009!

    Un code est meilleur s'il est beau, esth\u00e9tique, que les noms des variables, l'alignement et la mise en forme sont coh\u00e9rents et forment une unit\u00e9.

    Un code se doit \u00eatre explicite, et r\u00e9ellement traduire l'intention du d\u00e9veloppeur. Il est ainsi pr\u00e9f\u00e9rable d'\u00e9crire u = v / 4 plut\u00f4t que u >>= 2. De la m\u00eame mani\u00e8re, d\u00e9tecter si un nombre est pair est plus explicite avec if (n % 2 == 0) que if (n & 1).

    "}, {"location": "course-c/70-philosophy/philosophy/#the-code-taste", "title": "The code taste", "text": "

    Voici une version am\u00e9lior\u00e9e de votre texte, en tenant compte du style que vous appr\u00e9ciez\u2009:

    Lors d'une conf\u00e9rence TED en 2016, le cr\u00e9ateur de Linux, Linus Torvalds, introduisit un concept qu'il nomma code taste, que l'on pourrait traduire par le go\u00fbt du code.

    Il pr\u00e9senta l'exemple de code C suivant, interrogeant son auditoire sur le fait de savoir si ce code \u00e9tait de bon go\u00fbt\u2009:

    void remove_list_entry(List* list, Entry* entry)\n{\n    Entry* prev = NULL;\n    Entry* walk = list->head;\n\n    while (walk != entry) {\n        prev = walk;\n        walk = walk->next;\n    }\n\n    if (!prev)\n        list->head = entry->next;\n    else\n        prev->next = entry->next;\n}\n

    Torvalds r\u00e9pondit sans d\u00e9tour que ce code \u00e9tait de mauvais go\u00fbt, le qualifiant de vilain et moche. En effet, ce test plac\u00e9 apr\u00e8s la boucle while d\u00e9note par rapport au reste du code. Cette dissonance esth\u00e9tique sugg\u00e8re qu'il existe une impl\u00e9mentation plus \u00e9l\u00e9gante, car lorsque le code para\u00eet laid, il y a fort \u00e0 parier qu'une solution de meilleur go\u00fbt peut \u00eatre trouv\u00e9e. On dit dans ces cas-l\u00e0 que le code sent : ce test est superflu, et il doit exister une mani\u00e8re d'\u00e9viter de traiter un cas particulier en choisissant un algorithme mieux con\u00e7u.

    En r\u00e9alit\u00e9, retirer un \u00e9l\u00e9ment d'une liste cha\u00een\u00e9e requiert de g\u00e9rer deux cas distincts\u2009:

    • Si l'\u00e9l\u00e9ment se trouve au d\u00e9but de la liste, il faut ajuster le pointeur head.
    • Dans le cas contraire, il convient de modifier prev->next.

    Apr\u00e8s avoir questionn\u00e9 l'auditoire, Torvalds d\u00e9voila une nouvelle impl\u00e9mentation\u2009:

    void remove_list_entry(List* list, Entry* entry)\n{\n    Entry** indirect = &head;\n\n    while ((*indirect) != entry)\n        indirect = &(*indirect)->next;\n\n    *indirect = entry->next;\n}\n

    La fonction originellement \u00e9tal\u00e9e sur dix lignes est d\u00e9sormais r\u00e9duite \u00e0 quatre. Bien que le nombre de lignes importe moins que la lisibilit\u00e9, cette nouvelle version \u00e9limine la gestion des cas particuliers gr\u00e2ce \u00e0 un adressage indirect beaucoup plus raffin\u00e9.

    Cependant, un programmeur novice en C pourrait \u00eatre d\u00e9concert\u00e9 par l'emploi des doubles pointeurs et juger la premi\u00e8re version plus lisible. Cela illustre \u00e0 quel point la connaissance des structures de donn\u00e9es et des algorithmes est cruciale pour \u00e9crire du code de qualit\u00e9\u2009: on ne s'improvise pas d\u00e9veloppeur, c'est un art qui demande patience et apprentissage.

    Un exemple similaire, plus accessible, est pr\u00e9sent\u00e9 par Brian Barto dans un article publi\u00e9 sur Medium. Il y discute de l'initialisation \u00e0 z\u00e9ro de la bordure d'un tableau bidimensionnel\u2009:

    for (size_t row = 0; row < GRID_SIZE; ++row)\n{\n    for (size_t col = 0; col < GRID_SIZE; ++col)\n    {\n        if (row == 0)\n            grid[row][col] = 0; // Top Edge\n\n        if (col == 0)\n            grid[row][col] = 0; // Left Edge\n\n        if (col == GRID_SIZE - 1)\n            grid[row][col] = 0; // Right Edge\n\n        if (row == GRID_SIZE - 1)\n            grid[row][col] = 0; // Bottom Edge\n    }\n}\n

    On constate plusieurs fautes de go\u00fbt\u2009:

    1. GRID_SIZE pourrait \u00eatre diff\u00e9rent de la r\u00e9elle taille de grid
    2. Les valeurs d'initialisation sont dupliqu\u00e9es
    3. La complexit\u00e9 de l'algorithme est de \\(O(n^2)\\) alors que l'on ne s'int\u00e9resse qu'\u00e0 la bordure du tableau.

    Voici une solution plus \u00e9l\u00e9gante\u2009:

    const size_t length = sizeof(grid[0]) / sizeof(grid[0][0]);\nconst int init = 0;\n\nfor (size_t i = 0; i < length; i++)\n{\n    grid[i][0] = grid[0][i] = init; // Top and Left\n    grid[length - 1][i] = grid[i][length - 1] = init; // Bottom and Right\n}\n
    ", "tags": ["grid", "head", "GRID_SIZE", "while"]}, {"location": "course-c/70-philosophy/philosophy/#lodeur-du-code-code-smell", "title": "L'odeur du code (code smell)", "text": "

    Un code sent si certains indicateurs sont au rouge. On appelle ces indicateurs des antipatterns. Voici quelques indicateurs les plus courants\u2009:

    Mastodonte

    Une fonction est plus longue qu'un \u00e9cran de haut (~50 lignes)

    Titan

    Un fichier est plus long que 1000 lignes.

    Ligne Dieu

    Une ligne beaucoup trop longue et de facto illisible.

    Usine \u00e0 gaz

    Une fonction \u00e0 plus de trois param\u00e8tres

    void make_coffee(int size, int mode, int mouture, int cup_size,\n    bool with_milk, bool cow_milk, int number_of_sugars);\n
    Miroir magique

    Du code est dupliqu\u00e9. Du code est dupliqu\u00e9.

    Pamphlets touristiques

    Les commentaires expliquent le comment du code et non le pourquoi

    // Additionne une constante avec une autre pour ensuite l'utiliser\ndouble u = (a + cst);\nu /= 1.11123445143; // division par une constante inf\u00e9rieure \u00e0 2\n
    Arbre de No\u00ebl

    plus de deux structures de contr\u00f4les sont impliqu\u00e9es

    if (a > 2) {\n    if (b < 8) {\n        if (c ==12) {\n            if (d == 0) {\n                exception(a, b, c, d);\n            }\n        }\n    }\n}\n
    T\u00e9l\u00e9portation sauvage

    Usage de goto, o\u00f9 quand le code saute d'un endroit \u00e0 l'autre sans logique apparente.

    loop:\n    i +=1;\n    if (i > 100)\n        goto end;\nhappy:\n    happy();\n    if (j > 10):\n        goto sad;\nsad:\n    sad();\n    if (k < 50):\n        goto happy;\nend:\n
    Jumeaux diaboliques

    Plusieurs variables avec des noms tr\u00e8s similaires

    int advice = 11;\nint advise = 12;\n
    Action \u00e0 distance

    Une fonction qui tire les ficelles \u00e0 distance gr\u00e2ce aux variables globales.

    void make_coffee() {\n    powerup_nuclear_reactor = true;\n    number_of_coffee_beans_to_harvest = 62;\n    ...\n}\n
    Ancre de bateau

    Un composant inutilis\u00e9, mais gard\u00e9 dans le logiciel pour des raisons politiques (YAGNI)

    Cyclomatisme aigu

    Quand trop de structures de contr\u00f4les sont n\u00e9cessaires pour traiter un probl\u00e8me apparemment simple

    Attente active

    Une boucle qui ne contient qu'une instruction de test, attendant la condition

    while (true) {\n    if (finished) break;\n}\n
    Valeur Bulgare

    Popularis\u00e9 par Jacques-Andr\u00e9 Porchet, une grandeur Bulgare est une valeur magique qui n'a pas de sens pour un non-initi\u00e9 et qui semble \u00eatre sortie de nulle part.

    double it_works_with_this_value = 1.11123445143;\n
    Objet divin

    Quand un composant logiciel assure trop de fonctions essentielles (KISS)

    Coul\u00e9e de lave

    Lorsqu'un code immature est mis en production

    En novembre 1990 \u00e0 14h25, la soci\u00e9t\u00e9 AT&T effecue une mise \u00e0 jour de son r\u00e9seau t\u00e9l\u00e9phonique. Un bug dans le code de mise \u00e0 jour provoque un crash du r\u00e9seau entra\u00eenant une interruption de service de 9 heures affectant quelque 50 millions d'appels et co\u00fbtant \u00e0 l'entreprise 60 millions de dollars. (Source\u2009: The 1990 AT&T Long Distance Network Collapse)

    Chirurgie au fusil de chasse

    Quand l'ajout d'une fonctionnalit\u00e9 logicielle demande des changements multiples et disparates dans le code (Shotgun surgery).

    ", "tags": ["goto"]}, {"location": "course-c/70-philosophy/philosophy/#conclusion", "title": "Conclusion", "text": "

    La qu\u00eate d'un code de qualit\u00e9 repose sur des principes philosophiques et m\u00e9thodologiques essentiels. Le rasoir d'Ockham encourage la simplicit\u00e9 en \u00e9liminant les \u00e9l\u00e9ments superflus, tandis que l'effet Dunning-Kruger met en garde contre la surestimation de ses comp\u00e9tences, rappelant l'importance de la remise en question et du retour critique. Des doctrines telles que DRY (ne vous r\u00e9p\u00e9tez pas), KISS (restez simple), SSOT (une seule source de v\u00e9rit\u00e9) et YAGNI (vous n'en aurez pas besoin) guident le d\u00e9veloppeur vers une conception \u00e9pur\u00e9e et efficace.

    Le Zen de Python illustre cette philosophie par dix-neuf aphorismes valorisant la beaut\u00e9, la lisibilit\u00e9 et la simplicit\u00e9 du code. Linus Torvalds, cr\u00e9ateur de Linux et de Git, a soulign\u00e9 l'importance du code taste ou \u00ab\u2009go\u00fbt du code\u2009\u00bb, d\u00e9montrant qu'une impl\u00e9mentation \u00e9l\u00e9gante r\u00e9sulte souvent d'une r\u00e9flexion profonde sur les structures de donn\u00e9es et les algorithmes, plut\u00f4t que d'une complexit\u00e9 inutile.

    Enfin, reconna\u00eetre et \u00e9viter les odeurs de code (code smells), ces indicateurs de mauvaise conception comme les fonctions tentaculaires, les duplications ou les commentaires mal pens\u00e9s, est crucial. En identifiant ces antipatterns \u2014 qu'ils se manifestent sous la forme d'un \u00ab\u2009Arbre de No\u00ebl\u2009\u00bb de structures conditionnelles imbriqu\u00e9es ou d'une \u00ab\u2009T\u00e9l\u00e9portation sauvage\u2009\u00bb via des goto intempestifs\u2014le d\u00e9veloppeur s'assure de produire un code maintenable, clair et performant.

    ", "tags": ["goto"]}, {"location": "course-c/90-exercises/", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Mot du jour

    \u00c9crire un programme qui retourne un mot parmi une liste de mot, de fa\u00e7on al\u00e9atoire.

    #include <time.h>\n#include <stdlib.h>\n\nchar *words[] = {\"Alb\u00e9do\", \"Bigre\", \"Maringouin\", \"Pluripotent\", \"Entrechat\",\n    \"Caracoler\" \"Palinodie\", \"S\u00e9millante\", \"Atavisme\", \"Cyclothymie\",\n    \"Idiosyncratique\", \"Ent\u00e9l\u00e9chie\"};\n\n#if 0\n    srand(time(NULL));   // Initialization, should only be called once.\n    size_t r = rand() % sizeof(words) / sizeof(char*); // Generate random value\n#endif\n
    Solution
    #include <time.h>\n#include <stdlib.h>\n\nchar *words[] = {\n    \"Alb\u00e9do\", \"Bigre\", \"Maringouin\", \"Pluripotent\", \"Entrechat\",\n    \"Caracoler\" \"Palinodie\", \"S\u00e9millante\", \"Atavisme\", \"Cyclothymie\",\n    \"Idiosyncratique\", \"Ent\u00e9l\u00e9chie\"};\n\nint main(void)\n{\n    srand(time(NULL));\n    puts(words[rand() % (sizeof(words) / sizeof(char*))]);\n}\n
    "}, {"location": "course-concurrent/arch/", "title": "Architecture processeur", "text": ""}, {"location": "course-concurrent/arch/#introduction", "title": "Introduction", "text": "

    L'architecture d'un processeur est l'ensemble des \u00e9l\u00e9ments qui le compose et qui lui permettent de fonctionner. C'est un peu comme le corps humain, il y a des organes qui ont chacun un r\u00f4le bien pr\u00e9cis.

    "}, {"location": "course-concurrent/arch/#historique", "title": "Historique", "text": "

    Les premiers processeurs \u00e9taient tr\u00e8s simples, ils \u00e9taient compos\u00e9s de quelques milliers de transistors et avaient une fr\u00e9quence de quelques MHz. Aujourd'hui, les processeurs sont compos\u00e9s de plusieurs milliards de transistors et ont une fr\u00e9quence de plusieurs GHz.

    Alain Turing a \u00e9t\u00e9 l'un des premiers \u00e0 imaginer un ordinateur, il a con\u00e7u un mod\u00e8le th\u00e9orique d'ordinateur appel\u00e9 \u00ab\u2009Machine de Turing\u2009\u00bb. C'est un mod\u00e8le abstrait qui a permis de poser les bases de l'informatique moderne.

    La machine de Turing permet de comprendre la notion de programme, de m\u00e9moire, de calcul, etc. Son fonctionnement est tr\u00e8s simple\u2009:

    • Elle poss\u00e8de une bande de papier infinie sur laquelle sont stock\u00e9es des donn\u00e9es.
    • Une t\u00eate de lecture/\u00e9criture qui peut lire et \u00e9crire des donn\u00e9es sur la bande.
    • Un \u00e9tat interne qui permet de savoir ce que la machine est en train de faire.
    • Un ensemble de r\u00e8gles qui permettent de changer l'\u00e9tat interne en fonction des donn\u00e9es lues.

    Avec ces quelques \u00e9l\u00e9ments simples, on peut montrer que l'on peut r\u00e9soudre n'importe quel probl\u00e8me algorithmique. C'est ce qu'on appelle la \u00ab\u2009th\u00e8se de Church-Turing\u2009\u00bb. La difficult\u00e9 principale de ce mod\u00e8le est de comment \u00e9crire les r\u00e8gles pour r\u00e9soudre un probl\u00e8me donn\u00e9. C'est l\u00e0 qu'intervient la programmation.

    Lorsque les premiers ordinateurs ont vus le jours, il \u00e9taient compos\u00e9s d'\u00e9l\u00e9ments simples\u2009:

    • Une m\u00e9moire volatile pour stocker les donn\u00e9es et des \u00e9tats interm\u00e9diaires.
    • Une m\u00e9moire non-volatile pour stocker des instructions a ex\u00e9cuter.
    • Un processeur pour ex\u00e9cuter les instructions.

    Ce processeur \u00e9tait \u00e9galement tr\u00e8s simple. Il se composait d'une unit\u00e9 de calcul, d'une unit\u00e9 de contr\u00f4le et d'une unit\u00e9 de gestion de la m\u00e9moire.

    L'unit\u00e9 de calcul nomm\u00e9 ALU (Arithmetic Logic Unit) permet de faire des op\u00e9rations arithm\u00e9tiques et logiques (addition, soustraction, multiplication, division, ET, OU, etc). L'unit\u00e9 de contr\u00f4le permet de lire les instructions en m\u00e9moire et de les ex\u00e9cuter. L'unit\u00e9 de gestion de la m\u00e9moire permet de lire et \u00e9crire des donn\u00e9es en m\u00e9moire. Le jeu d'instruction \u00e9tait tr\u00e8s simple, il se composait de quelques instructions seulement\u2009:

    1. D\u00e9placer une donn\u00e9e de la m\u00e9moire vers l'ALU.
    2. D\u00e9placer une donn\u00e9e de l'ALU vers la m\u00e9moire.
    3. Faire une op\u00e9ration arithm\u00e9tique ou logique sur les donn\u00e9es de l'ALU.
    4. Aller chercher une instruction \u00e0 une adresse donn\u00e9e en m\u00e9moire et la charger dans l'unit\u00e9 de contr\u00f4le.
    5. Tester si une valeur est nulle et sauter \u00e0 une adresse donn\u00e9e si c'est le cas.

    Avec les \u00e9volutions technologiques, de nombreux syst\u00e8mes complexes ont \u00e9t\u00e9 ajout\u00e9s.

    Le premier constat de ces premiers ordinateurs est que le processeur \u00e9tait souvent plus rapide que la m\u00e9moire et qu'aller chercher une valeur en m\u00e9moire n\u00e9cessitait plusieurs \u00e9tapes. Durant ces \u00e9tapes, le processeur devait attendre et n'utilisait pas son plein potentiel de calcul. On a alors ajout\u00e9 un composant nomm\u00e9 pipeline qui permet d'ex\u00e9cuter les diff\u00e9rentes \u00e9tapes de la lecture d'une instruction en parall\u00e8le. Cela permet de r\u00e9duire le temps d'attente du processeur. En effet, puisque le programme est ex\u00e9cut\u00e9 s\u00e9quentiellement, on sait que la prochaine instruction \u00e0 ex\u00e9cuter est la suivante de celle en cours d'ex\u00e9cution. Si l'on dispose d'un pointeur int *p sur cett instruction, on sait que l'instruction suivante est localis\u00e9e \u00e0 p + 1. On peut alors anticiper la lecture de cette instruction en avance et la charger dans l'unit\u00e9 de contr\u00f4le. C'est ce qu'on appelle le pr\u00e9chargement. Typiquement un processeur dispose de 3 \u00e0 5 \u00e9tages de pipeline dont chaque \u00e9tage correspond \u00e0 une \u00e9tape de la lecture d'une instruction\u2009:

    1. Pr\u00e9chargement de l'instruction.
    2. D\u00e9codage de l'instruction.
    3. Ex\u00e9cution de l'instruction.
    4. Acc\u00e8s \u00e0 la m\u00e9moire.
    5. \u00c9criture du r\u00e9sultat.

    H\u00e9las, le pipeline n'est pas sans inconv\u00e9nient. En effet, si une instruction d\u00e9pend du r\u00e9sultat d'une autre instruction, il faut attendre que cette derni\u00e8re soit termin\u00e9e pour pouvoir ex\u00e9cuter la premi\u00e8re. C'est ce qu'on appelle un hazard. Il existe plusieurs types de hazard\u2009:

    • Data hazard : lorsque deux instructions d\u00e9pendent du m\u00eame registre.
    • Control hazard : lorsque le r\u00e9sultat d'une instruction conditionnelle n'est pas encore connu.

    Le control hazard appara\u00eet typiquement lorsque l'on fait un saut conditionnel (if, for, while...). En effet, le processeur ne sait pas si le saut doit \u00eatre effectu\u00e9 ou non avant d'avoir ex\u00e9cut\u00e9 l'instruction conditionnelle. Sur des microcontrolleurs simples, une des deux branches de la condition est privil\u00e9gi\u00e9e. On pr\u00e9charge cette branche dans le pipeline et si la condition est fausse, on annule les r\u00e9sultats de l'ex\u00e9cution ce qui \u00e9quivaut \u00e0 vider int\u00e9gralement le pipeline. Le co\u00fbt est important car le processeur doit alors attendre que le pipeline se remplisse \u00e0 nouveau avant de pouvoir ex\u00e9cuter la prochaine instruction.

    Avec la complexification des processeurs modernes, le pipeline est devenu de plus en plus long. Aujourd'hui, un processeur moderne dispose de 15 \u00e0 20 \u00e9tages de pipeline. Le probl\u00e8me des hazards est devenu de plus en plus important.

    Un processeur moderne s'est donc \u00e9quip\u00e9 d'un autre m\u00e9canisme pour pallier ce probl\u00e8me que l'on nomme le predicteur d'embranchement. En effet, si le processeur peut pr\u00e9dire si un saut doit \u00eatre effectu\u00e9 ou non, il peut pr\u00e9charger la bonne instruction dans le pipeline. Si la pr\u00e9diction est bonne, on gagne du temps, sinon on perd du temps. Il existe plusieurs strat\u00e9gies de pr\u00e9diction de branchement\u2009:

    • Pr\u00e9diction statique : on pr\u00e9dit que le saut est toujours effectu\u00e9 ou non.
    • Pr\u00e9diction dynamique : on pr\u00e9dit le saut en fonction de l'historique des sauts pr\u00e9c\u00e9dents.

    Les processeurs modernes conservent donc une table de pr\u00e9diction de branchement qui permet de stocker l'historique des sauts pr\u00e9c\u00e9dents pour chaque instruction de conditions. Si un saut est souvent effectu\u00e9, on pr\u00e9dit qu'il le sera \u00e0 nouveau.

    N\u00e9anmoins, il reste une difficult\u00e9 majeure \u00e0 r\u00e9soudre, les latences m\u00e9moires. En effet, plus la m\u00e9moire est grosse et \u00e9loign\u00e9e du processeur, plus le temps d'acc\u00e8s est long.

    A titre d'exemple, une m\u00e9moire DDR4 a une latence de 15 ns alors qu'un processeur moderne a une fr\u00e9quence de 3 GHz soit une latence de 0.33 ns. Cela signifie que le processeur doit attendre 45 cycles pour acc\u00e9der \u00e0 la m\u00e9moire. En pratique, c'est encore pire car la m\u00e9moire est souvent partag\u00e9e entre plusieurs processeurs et doit \u00eatre synchronis\u00e9e. D'autre part, les m\u00e9moires sont organis\u00e9es en pages m\u00e9moires, changer de page demande un temps suppl\u00e9mentaire. Le d\u00e9lai peut \u00eatre de plusieurs centaines de cycles pour lire une donn\u00e9e isol\u00e9e en RAM.

    Pour palier \u00e0 ce probl\u00e8me, les processeurs se sont \u00e9quip\u00e9s de m\u00e9moires interm\u00e9diaires nomm\u00e9es m\u00e9moires caches. On distingue ajourd'hui 3 niveaux de caches\u2009:

    • L1\u2009: cache de niveau 1, tr\u00e8s rapide, souvent int\u00e9gr\u00e9 dans le coeur du processeur et \u00e0 proximit\u00e9 directe de l'ALU.
    • L2\u2009: cache de niveau 2, plus lent que le L1 mais plus gros.
    • L3\u2009: cache de niveau 3, plus lent que le L2 mais plus gros et partag\u00e9 entre plusieurs coeurs processeur.

    Ceci nous am\u00e8ne aux architectures modernes multicoeurs. Un processeur Intel ou AMD moderne comporte tr\u00e8s souvent 6 ou 8 processeurs ind\u00e9pendants reli\u00e9s entre eux par une m\u00e9moire cache de niveau L3. Chaque coeur dispose de sa propre m\u00e9moire cache de niveau L1 et L2.

    ", "tags": ["while", "pipeline", "for"]}, {"location": "course-concurrent/arch/#processeur-moderne", "title": "Processeur moderne", "text": "

    La figure suivante repr\u00e9sente la vue a\u00e9rienne d'un processeur moderne. Le die ou substrat en silicium fait environ 1 \u00e0 2 cm de c\u00f4t\u00e9 et comporte plusieurs milliards de transistors. Le savoir faire des ing\u00e9nieurs est tr\u00e8s gard\u00e9 mais en observant la structure du die, on peut deviner les diff\u00e9rents composants qui le compose.

    cpu

    On peut voir sur cette figure la m\u00e9moire cache de niveau L3 facilement identifiable \u00e0 son pattern de grille. On peut \u00e9galement voir les diff\u00e9rents coeurs qui l'entourent, ils se ressemblent tous et on voit qu'ils sont \u00e9galement compos\u00e9s d'un motif r\u00e9p\u00e9titif qui est la m\u00e9moire cache de niveau L1 et L2. Souvent ces processeurs int\u00e8grent \u00e9galement une partie GPU qui est utilis\u00e9e pour les calculs graphiques lorsqu'il n'y a pas de carte graphique int\u00e9gr\u00e9e.

    Si l'on s'int\u00e9resse \u00e0 un coeur en particulier, on peut voir qu'il est compos\u00e9 de plusieurs \u00e9l\u00e9ments. Tout d'abord \u00e0 droite on trouve la m\u00e9moire cache L2 qui repr\u00e9sente environ 20% de la surface du coeur. Ensuite le pr\u00e9dicteur d'embranchement tr\u00e8s proche du cache L1 contenant les prochaines instructions \u00e0 ex\u00e9cuter. Le d\u00e9codeur d'instructions est \u00e0 proximit\u00e9 du I-Cache et du pr\u00e9dicteur d'embranchement. Il est coupl\u00e9 \u00e0 un ordonnanceur de micro-op\u00e9rations qui adresse chaque calcul soit sur l'ALU pour de la virgule fixe, soit sur la FPU pour les calculs en virgule flottante. Dans la partie inf\u00e9rieure, on trouve l'ALU 64-bits, le cache de donn\u00e9es L1 et le gestionnaire de m\u00e9moire permettant de lire/\u00e9crire des donn\u00e9es en m\u00e9moire.

    core

    "}, {"location": "course-concurrent/async/", "title": "Programmation Asynchrone", "text": "

    Un paradigme de programmation asynchrone est un style de programmation concurrente qui permet de g\u00e9rer des \u00e9v\u00e9nements de mani\u00e8re non bloquante. C'est une mani\u00e8re de g\u00e9rer des \u00e9v\u00e9nements qui ne sont pas n\u00e9cessairement li\u00e9s \u00e0 l'ex\u00e9cution du programme et une am\u00e9lioation de la programmation concurrente classique utilisant explicitement des threads.

    Ce paradigme est devenu populaire avec l'arriv\u00e9e des applications web et des interfaces graphiques. En effet, ces applications ont besoin de r\u00e9agir \u00e0 des \u00e9v\u00e9nements de mani\u00e8re asynchrone pour ne pas bloquer l'interface utilisateur. Le langage JavaScript est un exemple de langage qui utilise ce paradigme.

    Prenons l'exemple d'une page web qui affiche des donn\u00e9es provenant d'un serveur. Lorsque la page est charg\u00e9e, une requ\u00eate est envoy\u00e9e au serveur pour r\u00e9cup\u00e9rer les donn\u00e9es. Si la requ\u00eate \u00e9tait bloquante, l'interface utilisateur serait fig\u00e9e jusqu'\u00e0 ce que les donn\u00e9es soient re\u00e7ues. Avec la programmation asynchrone, la requ\u00eate est envoy\u00e9e et le programme continue de s'ex\u00e9cuter. Lorsque les donn\u00e9es sont re\u00e7ues, un \u00e9v\u00e9nement est d\u00e9clench\u00e9 et le programme r\u00e9agit \u00e0 cet \u00e9v\u00e9nement en affichant les donn\u00e9es. Voici un exemple web simplifi\u00e9\u2009:

    <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"fr\" lang=\"fr\">\n<head>\n    <title>Exemple de programmation asynchrone</title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n</head>\n<body>\n    <h1>Donn\u00e9es</h1>\n    <div id=\"data\">Chargement des donn\u00e9es en cours...</div>\n    <script>\n        // Envoi de la requ\u00eate au serveur\n        fetch('https://jsonplaceholder.typicode.com/posts')\n            .then(response => response.json())\n            .then(data => {\n                // Affichage des donn\u00e9es\n                document.getElementById('data').innerText = JSON.stringify(data, null, 2);\n            });\n    </script>\n</body>\n</html>\n

    Vous pouvez ex\u00e9cuter cet exemple en enregistrant le code dans un fichier HTML et en l'ouvrant dans un navigateur. Vous devriez voir les donn\u00e9es affich\u00e9es dans la page.

    Dans cet exemple, la requ\u00eate est envoy\u00e9e au serveur avec la fonction fetch, qui renvoie une promesse. Lorsque la promesse est r\u00e9solue (c'est-\u00e0-dire lorsque les donn\u00e9es sont re\u00e7ues), la fonction then est appel\u00e9e pour afficher les donn\u00e9es dans la page.

    JavaScript utilise une notation tr\u00e8s particuli\u00e8re qui permet de cha\u00eener les actions. C'est impl\u00e9ment\u00e9 en retournant l'objet courant (this en C++).

    La m\u00e9thode fetch retourne donc une instance de Promise qui est un objet repr\u00e9sentant la r\u00e9solution ou le rejet d'une valeur asynchrone. Une promesse peut \u00eatre dans l'un des trois \u00e9tats suivants\u2009:

    1. en attente,
    2. r\u00e9solue ou
    3. rejet\u00e9e.

    Lorsqu'une promesse est r\u00e9solue, la m\u00e9thode then est appel\u00e9e avec la valeur de la promesse. Si la promesse est rejet\u00e9e, la m\u00e9thode catch est appel\u00e9e avec l'erreur.

    En JavaScript les fonctions lambda aussi appel\u00e9es fonctions fl\u00e9ch\u00e9es (=>) sont tr\u00e8s utilis\u00e9es. Elles permettent de d\u00e9finir des fonctions de mani\u00e8re plus concise. Par exemple le code traduit en C++ pourrait ressembler \u00e0 ceci\u2009:

    fetch(\"https://jsonplaceholder.typicode.com/posts\")\n    .then([](Response response) {\n        return response.json();\n    })\n    .then([](Data data) {\n        document.getElementById(\"data\").innerText = JSON.stringify(data, nullptr, 2);\n    });\n

    On note donc que la fonction fetch n'est pas bloquante, une action est associ\u00e9e lorsque la promesse sera r\u00e9solue. On voit ici une cha\u00eene d'actions qui se d\u00e9clenchent les unes apr\u00e8s les autres.

    1. On fait la promesse qu'une r\u00e9ponse sera re\u00e7ue
    2. Une fois les donn\u00e9es re\u00e7u, on promet de les transformer en JSON
    3. Une fois les donn\u00e9es transform\u00e9es, on promet de les afficher dans la page

    C'est une mani\u00e8re de g\u00e9rer des \u00e9v\u00e9nements de mani\u00e8re asynchrone sans bloquer le programme.

    ", "tags": ["fetch", "catch", "then", "this", "Promise"]}, {"location": "course-concurrent/async/#c-et-la-programmation-asynchrone", "title": "C++ et la programmation asynchrone", "text": "

    En C++ la notion de promesse et de future ont \u00e9t\u00e9 introduites en C++11. Leur fonctionnement est similaire \u00e0 celui des promesses en JavaScript.

    Une future est un objet qui contient une valeur qui sera disponible dans le futur. Elle est associ\u00e9e \u00e0 une promesse qui est l'objet qui promet de fournir la valeur.

    L'exemple donn\u00e9 en JavaScript n'est pas directement transposable en C++ car le langage ne poss\u00e8de pas de fonction fetch qui permet de faire des requ\u00eates HTTP de mani\u00e8re asynchrone. N\u00e9anmoins l'utilisation de la biblioth\u00e8que CURL et nlohmann/json peut nous aider.

    sudo apt install libcurl4 libcurl4-openssl-dev nlohmann-json3-dev\n
    #include <iostream>\n#include <string>\n#include <curl/curl.h>\n#include <nlohmann/json.hpp>\n#include <future>\n#include <thread>\n\nusing json = nlohmann::json;\n\nstatic size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {\n    ((std::string*)userp)->append((char*)contents, size * nmemb);\n    return size * nmemb;\n}\n\nstd::string fetchUrl(const std::string& url) {\n    CURL *curl = curl_easy_init();\n    if(!curl) return \"\";\n\n    std::string readBuffer;\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);\n    CURLcode res = curl_easy_perform(curl);\n    curl_easy_cleanup(curl);\n\n    if(res != CURLE_OK)\n        throw std::runtime_error(\"CURL failed: \" + std::string(curl_easy_strerror(res)));\n    return readBuffer;\n}\n\nint main() {\n    std::string url = \"https://jsonplaceholder.typicode.com/posts\";\n\n    // Cr\u00e9ation de la promesse et du futur\n    std::promise<std::string> promise;\n    std::future<std::string> future = promise.get_future();\n\n    // Ex\u00e9cution dans un thread s\u00e9par\u00e9\n    std::jthread([url, &promise]() {\n        try {\n            std::string result = fetchUrl(url);\n            promise.set_value(result);\n        } catch(...) {\n            promise.set_exception(std::current_exception());\n        }\n    });\n\n    try {\n        // Attendre et obtenir la valeur future\n        std::string result = future.get();\n        json j = json::parse(result);\n        std::cout << j.dump(4) << std::endl;\n    } catch(const std::exception& e) {\n        std::cerr << \"Error: \" << e.what() << std::endl;\n    }\n}\n

    On peut observer que le code est moins concis que le code JavaScript. L'asynchronisme est plus explicite et n\u00e9cessite la cr\u00e9ation d'un thread pour effectuer la requ\u00eate. La promesse est utilis\u00e9e pour transmettre le r\u00e9sultat de la requ\u00eate au thread principal.

    ", "tags": ["fetch"]}, {"location": "course-concurrent/async/#stdasync", "title": "std\u2009::async", "text": "

    Async est une fonction qui permet de lancer une fonction de mani\u00e8re asynchrone, autrement dit dans un thread s\u00e9par\u00e9 mais sans avoir \u00e0 g\u00e9rer la cr\u00e9ation du thread.

    Elle retourne une future qui contient le r\u00e9sultat de la fonction. C'est une mani\u00e8re plus simple de lancer une fonction de mani\u00e8re asynchrone sans avoir \u00e0 g\u00e9rer la cr\u00e9ation d'un thread.

    #include <iostream>\n#include <string>\n#include <curl/curl.h>\n#include <nlohmann/json.hpp>\n#include <future>\n\nusing json = nlohmann::json;\n\nstatic size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {\n    ((std::string*)userp)->append((char*)contents, size * nmemb);\n    return size * nmemb;\n}\n\nstd::string fetchUrl(const std::string& url) {\n    CURL *curl = curl_easy_init();\n    if(!curl) return \"\";\n\n    std::string readBuffer;\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);\n    CURLcode res = curl_easy_perform(curl);\n    curl_easy_cleanup(curl);\n\n    if(res != CURLE_OK)\n        throw std::runtime_error(\"CURL failed: \" + std::string(curl_easy_strerror(res)));\n    return readBuffer;\n}\n\nint main() {\n    // Lancement asynchrone de la requ\u00eate HTTP\n    std::future<std::string> future = std::async(\n        std::launch::async, fetchUrl, \"https://jsonplaceholder.typicode.com/posts\");\n\n    try {\n        // Attendre et obtenir la valeur future\n        std::string result = future.get();\n        json j = json::parse(result);\n        std::cout << j.dump(4) << std::endl;\n    } catch(const std::exception& e) {\n        std::cerr << \"Error: \" << e.what() << std::endl;\n    }\n}\n
    "}, {"location": "course-concurrent/memory/", "title": "M\u00e9moire cache", "text": "

    Chaque processeur poss\u00e8de une m\u00e9moire cache qui permet de stocker des donn\u00e9es et des instructions pour les rendre plus rapidement accessibles. La m\u00e9moire cache est plus rapide que la m\u00e9moire principale (RAM) mais elle est aussi plus petite. Il existe plusieurs niveaux de cache (L1, L2, L3) qui sont de plus en plus grands et de plus en plus lents. Le dernier rampart avant la RAM est aussi appel\u00e9 le cache de dernier niveau (LLC).

    Le principal probl\u00e8me des ordinateurs modernes est que l'acc\u00e8s \u00e0 la m\u00e9moire RAM est extr\u00eamement lent (50 \u00e0 200 cycles) par rapport \u00e0 la vitesse du processeur. Il y a plusieurs raison \u00e0 cela, la premi\u00e8re est que la m\u00e9moire RAM est souvent d\u00e9port\u00e9e du processeur (sur une carte m\u00e8re) et que la vitesse de la lumi\u00e8re est limit\u00e9e\u2009: avec une distance de 20 cm (40 cm all\u00e9 retour) il faut 1.33 nanosecond soit d\u00e9j\u00e0 entre 5 et 10 cycles processeur. La seconde raison est que la m\u00e9moire RAM est compos\u00e9e de condensateurs qui se d\u00e9chargent et qui doivent \u00eatre recharg\u00e9s \u00e0 chaque lecture, on appelle cela le rafraichissement\u2009: toutes les 7.8us un ordinateur \u00e0 un hoquet et les acc\u00e8s RAM sont ralentis. Ces deux raisons ajout\u00e9 \u00e0 tous les circuits logiques qui doivent \u00eatre configur\u00e9s pour acheminer l'information d'un point \u00e0 l'autre font que l'acc\u00e8s \u00e0 la m\u00e9moire RAM est tr\u00e8s lente.

    Pour palier \u00e0 ce probl\u00e8me la notion de m\u00e9moire cache a \u00e9t\u00e9 introduite. Le premier processeur dot\u00e9 d'une m\u00e9moire cache est le Motorola 68000 en 1979.

    Un ennui majeur est que plus une m\u00e9moire est rapide plus elle est volumineuse et plus elle est volumineuse plus elle est ch\u00e8re. Une solution \u00e0 ce probl\u00e8me est d'utiliser plusieurs niveaux de cache. Le cache est tr\u00e8s rapide mais aussi tr\u00e8s volumineux en surface de silicium, il est donc tr\u00e8s cher. Il repr\u00e9sente environ 20% de la surface d'un processeur.

    "}, {"location": "course-concurrent/memory/#organisation-de-la-memoire-cache", "title": "Organisation de la m\u00e9moire cache", "text": "

    La m\u00e9moire cache est organis\u00e9e en lignes de cache. Chaque ligne de cache identifi\u00e9e par un index contient un bloc de donn\u00e9es et un tag. Le tag est un identifiant unique pour chaque bloc de donn\u00e9es. Lorsqu'un processeur veut acc\u00e9der \u00e0 une donn\u00e9e, il va d'abord chercher le tag dans le cache, si le tag est trouv\u00e9 alors le processeur a trouv\u00e9 la donn\u00e9e et il peut l'utiliser. Si le tag n'est pas trouv\u00e9 alors le processeur doit aller chercher la donn\u00e9e dans la m\u00e9moire RAM, ou dans un autre cache.

    Typiquement un cache L1 sur un processeur x86 contient 32 Ko de donn\u00e9es et 32 Ko d'instructions. Une ligne de cache contient g\u00e9n\u00e9ralement 64 octets.

    Lorsqu'une donn\u00e9e est charg\u00e9e en m\u00e9moire, par exemple l'acc\u00e8s \u00e0 un \u00e9l\u00e9ment d'un tableau, le processeur profite de la localit\u00e9 spatiale pour charger en m\u00e9moire les donn\u00e9es voisines. La localit\u00e9 spatiale est le fait que les donn\u00e9es voisines d'une donn\u00e9e sont souvent utilis\u00e9es en m\u00eame temps. Parall\u00e8lement, on appelle localit\u00e9 temporelle le fait que les donn\u00e9es sont souvent r\u00e9utilis\u00e9es plusieurs fois.

    Prenons l'exemple d'un tableau de 128 \u00e9l\u00e9ments de 4 bytes (int). Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 0, le processeur va automatiquement charger en m\u00e9moire les \u00e9l\u00e9ments 0 \u00e0 15 dans le cache. Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 16, le processeur doit aller chercher les \u00e9l\u00e9ments 16 \u00e0 31 dans la m\u00e9moire RAM. Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 0 une seconde fois, le processeur n'a pas besoin d'aller chercher les donn\u00e9es dans la m\u00e9moire RAM car elles sont d\u00e9j\u00e0 dans le cache, ceci jusqu'\u00e0 concurrence de la taille du cache.

    "}, {"location": "course-concurrent/memory/#cache-miss-et-cache-hit", "title": "Cache miss et cache hit", "text": "

    Lorsqu'un processeur cherche une donn\u00e9e dans le cache, il peut y avoir deux cas de figure\u2009: le cache hit et le cache miss.

    Un cache hit est le cas o\u00f9 le processeur trouve la donn\u00e9e dans le cache. C'est le cas le plus rapide car le processeur n'a pas besoin d'aller chercher la donn\u00e9e dans la m\u00e9moire RAM.

    Un cache miss est le cas o\u00f9 le processeur ne trouve pas la donn\u00e9e dans le cache. C'est le cas le plus lent car le processeur doit aller chercher la donn\u00e9e dans la m\u00e9moire RAM. Il existe plusieurs types de cache miss, citons-en quelques uns\u2009:

    1. Compulsory miss: C'est le premier acc\u00e8s \u00e0 une donn\u00e9e. Le processeur ne peut pas savoir \u00e0 l'avance si la donn\u00e9e est dans le cache ou non. Il doit donc aller chercher la donn\u00e9e dans la m\u00e9moire RAM. Ce type de cache miss est in\u00e9vitable.
    2. Capacity miss: Le cache est plein et le processeur doit remplacer une ligne de cache pour pouvoir stocker une nouvelle donn\u00e9e. Ce type de cache miss est in\u00e9vitable.
    3. Conflict miss: Deux donn\u00e9es diff\u00e9rentes ont le m\u00eame tag. Ce type de cache miss est \u00e9vitable en choisissant judicieusement les tags.
    4. Coherence miss: Un autre processeur a modifi\u00e9 la donn\u00e9e. Ce type de cache miss est \u00e9vitable en utilisant des protocoles de coh\u00e9rence de cache.

    Plusieurs raisons peuvent expliquer un cache miss.

    "}, {"location": "course-concurrent/memory/#true-sharing", "title": "True sharing", "text": "

    Le true sharing est le cas o\u00f9 deux processeurs partagent la m\u00eame donn\u00e9e. Cela peut causer des ralentissements importants dans un programme concurrent car les deux processeurs doivent se synchroniser pour \u00e9viter les probl\u00e8mes de coh\u00e9rence de cache \u00e0 chaque acc\u00e8s \u00e0 la donn\u00e9e partag\u00e9e.

    "}, {"location": "course-concurrent/memory/#false-sharing", "title": "False sharing", "text": "

    Le false sharing est un probl\u00e8me qui survient lorsqu'un processeur modifie une donn\u00e9e non partag\u00e9e mais sur la m\u00eame ligne de cache qu'une autre donn\u00e9e. Cela survient typiquement dans le cas de la programmation concurrente. Lorsqu'un processeur modifie une donn\u00e9e, il doit invalider la ligne de cache de l'autre processeur. Si les deux processeurs modifient des donn\u00e9es qui sont sur la m\u00eame ligne de cache, alors les deux processeurs doivent se synchroniser.

    On appelle cela le false sharing car les deux processeurs ne partagent pas la m\u00eame donn\u00e9e mais ils partagent la m\u00eame ligne de cache.

    En C++17 le concept de std::hardware_destructive_interference_size permet de conna\u00eetre la taille d'une ligne de cache. Il n'est n\u00e9anmoins pas disponible dans tous les compilateurs. On peut utiliser la valeur 64 octets pour la plupart des processeurs.

    "}, {"location": "course-concurrent/memory/#compteur-de-performance", "title": "Compteur de performance", "text": "

    Un processeur moderne poss\u00e8de des compteurs de performance qui permettent de mesurer le comportement du processeeur. A chaque mauvaise pr\u00e9diction d'embranchement, \u00e0 chaque ralentissement d'un calcul FPU, \u00e0 chaque \u00e9v\u00e8nement du cache (miss, hit, etc) un compteur est incr\u00e9ment\u00e9. Ces compteurs sont accessibles via l'outil perf sur Linux et VTune sur Windows. Ce sont des outils indispensables pour mesurer les performances d'un programme.

    ", "tags": ["VTune", "perf"]}, {"location": "course-concurrent/memory/#exemples", "title": "Exemples", "text": "

    Dans ce chapitre, l'objectif est de sensibilier le lecteur aux ralentissements caus\u00e9s par le mat\u00e9riel. Il ne suffit pas de faire de la programmation concurrente en cherchant \u00e0 paralleliser un maximum de t\u00e2ches pour obtenir un programme plus rapide. Il est n\u00e9cessaire de prendre en compte les probl\u00e8mes de coh\u00e9rence de cache et de localit\u00e9 spatiale.

    Plusieurs exemples vont \u00eatre donn\u00e9s\u2009:

    • Rafraichissement de la m\u00e9moire
    • Localit\u00e9 spatiale
    • True sharing
    • False sharing
    • Pr\u00e9diction de branchement

    Pour chaque exemple, il est important de d\u00e9sactiver certaines fonctionnalit\u00e9s du processeur pour \u00e9viter des comportement inattendus.

    1. D\u00e9sactiver la randomisation de l'espace d'adressage, c'est \u00e0 dire que les adresses m\u00e9moires sont toujours les m\u00eames \u00e0 chaque ex\u00e9cution du programme.
    $ sudo bash -c \"echo 0 > /proc/sys/kernel/randomize_va_space\"\n
    1. D\u00e9sactiver le turbo mode, c'est \u00e0 dire que le processeur ne peut pas augmenter sa fr\u00e9quence pour acc\u00e9l\u00e9rer les calculs.
    $ sudo bash -c \"echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo\"\n
    1. D\u00e9sactiver l'hyper-threading, c'est \u00e0 dire que chaque coeur du processeur est utilis\u00e9 par un seul thread. Cela doit \u00eatre fait pour chaque coeur du processeur.

    bash $ sudo bash -c \"echo 0 > /sys/devices/system/cpu/cpuX/online\"

    1. D\u00e9sactiver le C-state, c'est \u00e0 dire que le processeur ne peut pas passer en mode veille pour \u00e9conomiser de l'\u00e9nergie.

      $ sudo cpupower frequency-set --governor performance\n
    "}, {"location": "course-concurrent/memory/#localite-spatiale", "title": "Localit\u00e9 spatiale", "text": "

    Prenons l'exemple d'une matrice de NxN \u00e9l\u00e9ments. Si on parcourt la matrice par ligne, on profite de la localit\u00e9 spatiale car les \u00e9l\u00e9ments d'une ligne sont souvent utilis\u00e9s en m\u00eame temps.

    En revanche si on parcourt la matrice par colonne, on ne profite pas de la localit\u00e9 spatiale car les \u00e9l\u00e9ments d'une colonne ne sont pas utilis\u00e9s en m\u00eame temps. Le cache est rapidement plein et le processeur doit aller chercher les donn\u00e9es dans la m\u00e9moire RAM.

    Le code suivant met en \u00e9vidence la diff\u00e9rence de performance entre un parcours par ligne et un parcours par colonne\u2009:

    #include <iostream>\n#include <vector>\n#include <chrono>\n\nconst int N = 10'000;\n\nint main() {\n    std::vector<std::vector<int>> matrix(N, std::vector<int>(N, 0));\n\n    for (size_t i = 0; i < N; i++)\n        for (size_t j = 0; j < N; j++)\n            matrix[i][j] = i * N + j;\n\n    auto start = std::chrono::high_resolution_clock::now();\n    long long sum = 0;\n    for (size_t i = 0; i < N; i++)\n        for (size_t j = 0; j < N; j++)\n#ifdef LINE\n            sum += matrix[i][j];\n#else\n            sum += matrix[j][i];\n#endif\n    auto end = std::chrono::high_resolution_clock::now();\n\n    std::chrono::duration<double> time = end - start;\n    std::cout << \"Somme en ligne : \" << sum << \"\\n\";\n    std::cout << \"Temps d'acc\u00e8s: \" << time.count() << \" s\" << std::endl;\n}\n

    Pour le cas d'une matrice de 10'000x10'000 entiers 32-bits, on obtient les r\u00e9sultats suivants\u2009:

    $ g++ -DCOLUMN -O3 locality-line.cpp && ./a.out\nSomme en ligne : 4999999950000000\nTemps d'acc\u00e8s: 0.772923 s\n\n$ g++ -DLINE -O3 locality-line.cpp && ./a.out\nSomme en ligne : 4999999950000000\nTemps d'acc\u00e8s: 0.0275642 s\n

    On observe que le parcours par ligne est 28 fois plus rapide que le parcours par colonne.

    "}, {"location": "course-concurrent/memory/#references", "title": "R\u00e9f\u00e9rences", "text": "
    • https://medium.com/@techhara/speed-up-c-false-sharing-44b56fffe02b
    • https://github.com/Kobzol/hardware-effects
    • https://blog.cloudflare.com/every-7-8us-your-computers-memory-has-a-hiccup/
    "}, {"location": "course-concurrent/mutex/", "title": "Exclusion Mutuelle", "text": "

    Les exclusions mutuelles sont un concept fondamental en programmation concurrente et parall\u00e8le. L'id\u00e9e est de garantir que certaines parties du code ne sont pas ex\u00e9cut\u00e9es simultan\u00e9ment par plusieurs threads, afin d'\u00e9viter des conditions de concurrence et des r\u00e9sultats impr\u00e9visibles.

    Les race conditions ou en fran\u00e7ais conditions de course sont des situations o\u00f9 le r\u00e9sultat d'une op\u00e9ration d\u00e9pend de l'ordre d'ex\u00e9cution des threads, ce qui peut conduire \u00e0 des r\u00e9sultats incorrects ou incoh\u00e9rents. Les exclusions mutuelles permettent de prot\u00e9ger les sections critiques du code pour \u00e9viter ces probl\u00e8mes.

    En C++, les exclusions mutuelles sont souvent mises en \u0153uvre \u00e0 l'aide de verrous (locks) fournis par la biblioth\u00e8que standard. Ces verrous sont des objets qui peuvent \u00eatre verrouill\u00e9s (locked) par un thread pour emp\u00eacher d'autres threads d'acc\u00e9der \u00e0 une section critique, puis d\u00e9verrouill\u00e9s (unlocked) pour permettre \u00e0 d'autres threads d'acc\u00e9der \u00e0 cette section.

    En principe les v\u00e9rrous sont utilis\u00e9s pour\u2009:

    • Prot\u00e9ger les donn\u00e9es partag\u00e9es entre plusieurs threads
    • Prot\u00e9ger les ressources partag\u00e9es entre plusieurs threads (fichiers, sockets, etc.)
    • Synchroniser l'acc\u00e8s \u00e0 des sections critiques du code
    • Synchroniser la communication entre threads
    • Synchroniser la terminaison de threads
    • etc.
    "}, {"location": "course-concurrent/mutex/#histoire", "title": "Histoire", "text": "

    Le concept d'exclusion mutuelle a \u00e9t\u00e9 introduit par Edsger Dijkstra en 1965. Il a invent\u00e9 le terme \u00ab\u2009exclusion mutuelle\u2009\u00bb pour d\u00e9crire une propri\u00e9t\u00e9 souhaitable des syst\u00e8mes informatiques, \u00e0 savoir que deux processus ne peuvent pas \u00eatre simultan\u00e9ment dans une section critique. Dijkstra a \u00e9galement invent\u00e9 l'algorithme du \u00ab\u2009banquier\u2009\u00bb pour \u00e9viter les interblocages dans les syst\u00e8mes informatiques.

    Dans son article (Solution of a Problem in Concurrent Programming Control)[https://www.cs.utexas.edu/users/EWD/ewd01xx/EWD123.PDF], Dijkstra a introduit le concept de s\u00e9maphores, qui sont des variables enti\u00e8res utilis\u00e9es pour la synchronisation entre processus ou threads. Les s\u00e9maphores peuvent \u00eatre utilis\u00e9s pour impl\u00e9menter l'exclusion mutuelle, ainsi que d'autres formes de synchronisation.

    Dekker a introduit un autre algorithme pour l'exclusion mutuelle entre deux processus en 1968. Cet algorithme est similaire \u00e0 l'algorithme de Peterson, mais utilise des variables suppl\u00e9mentaires pour \u00e9viter les probl\u00e8mes de synchronisation.

    Peterson a introduit un algorithme pour l'exclusion mutuelle entre deux processus en 1981. Cet algorithme est souvent utilis\u00e9 pour illustrer les concepts d'exclusion mutuelle et de synchronisation dans les cours d'informatique.

    \u00c0 cette \u00e9poque les processeurs multi-c\u0153urs n'existaient pas, et les syst\u00e8mes informatiques \u00e9taient g\u00e9n\u00e9ralement des syst\u00e8mes \u00e0 un seul processeur. Les probl\u00e8mes de synchronisation et d'exclusion mutuelle \u00e9taient donc moins complexes que dans les syst\u00e8mes modernes. Un processeur moderne dispose de plusieurs c\u0153urs, et chaque c\u0153ur peut ex\u00e9cuter plusieurs threads simultan\u00e9ment. La synchronisation et l'exclusion mutuelle sont donc des probl\u00e8mes plus complexes dans les syst\u00e8mes modernes.

    "}, {"location": "course-concurrent/mutex/#exemple-du-compteur-partage", "title": "Exemple du compteur partag\u00e9", "text": "

    Consid\u00e9rons un exemple simple o\u00f9 deux threads partagent un compteur. Chaque thread incr\u00e9mente le compteur 1000000 fois. Si les threads ne sont pas synchronis\u00e9s, le r\u00e9sultat final d\u00e9pendra de l'ordre d'ex\u00e9cution des threads.

    En effet, si les deux threads tentent d'incr\u00e9menter le compteur simultan\u00e9ment, le r\u00e9sultat final sera impr\u00e9visible car l'ex\u00e9cution des instructions n'est pas atomique. Le processeur va devoir d\u00e9composer l'op\u00e9ration d'incr\u00e9mentation en plusieurs \u00e9tapes\u2009:

    1. Charger la valeur actuelle du compteur depuis la m\u00e9moire dans un registre
    2. Incr\u00e9menter la valeur dans le registre
    3. \u00c9crire la nouvelle valeur du registre dans la m\u00e9moire

    Si un autre thread modifie la valeur du compteur entre ces \u00e9tapes, le r\u00e9sultat final sera incorrect.

    Pour \u00e9viter cela, nous devons synchroniser l'acc\u00e8s au compteur pour que les threads ne puissent pas l'incr\u00e9menter simultan\u00e9ment. La partie d'incr\u00e9mentation est nomm\u00e9e section critique.

    // counter.cpp\n#include <iostream>\n#include <thread>\n#include <vector>\n\nsize_t counter = 0;\n\nclass Worker {\npublic:\n    Worker(size_t max_iterations = 1000000) : max_iterations(max_iterations) {}\n    const size_t max_iterations;\n    void operator() () {\n        for (size_t i = 0; i < max_iterations; ++i)\n        { // section critique\n            ++counter;\n        }\n    }\n};\n\nint main() {\n    const int num_threads = std::thread::hardware_concurrency();\n\n    std::vector<std::jthread> threads;\n    Worker worker;\n    for (int i = 0; i < num_threads; ++i)\n        threads.emplace_back(std::jthread(worker));\n\n    for (auto& t : threads) t.join();\n\n    std::cout << \"Number of threads: \" << num_threads << std::endl;\n    std::cout << \"Expected Counter: \" << worker.max_iterations * num_threads << std::endl;\n    std::cout << \"Actual Counter: \" << counter << std::endl;\n}\n
    "}, {"location": "course-concurrent/mutex/#verrous-mutex", "title": "Verrous Mutex", "text": "

    En C++, les verrous sont g\u00e9n\u00e9ralement impl\u00e9ment\u00e9s \u00e0 l'aide de la classe std::mutex de la biblioth\u00e8que standard. Un verrou de type std::mutex peut \u00eatre verrouill\u00e9 \u00e0 l'aide de la m\u00e9thode lock() et d\u00e9verrouill\u00e9 \u00e0 l'aide de la m\u00e9thode unlock().

    "}, {"location": "course-concurrent/mutex/#exemple-simple", "title": "Exemple simple", "text": "
    #include <iostream>\n#include <thread>\n\nstatic int x = 0;\n\nint a() { for(;;) { x = 5; std::cout << x; } }\nint b() { for(;;) x = 7; }\n\nint main() {\n    std::thread ta(a);\n    std::thread tb(b);\n    ta.join();\n    tb.join();\n}\n
    $ g++ sync.cpp\n$ timeout 10s ./a.out\n$ grep -o 7 data | wc -l\n$ wc -c < data\n

    On peut voir qu'il y a environ 1 change sur 10000 d'avoir un 7.

    "}, {"location": "course-concurrent/mutex/#autre-exemple", "title": "Autre Exemple", "text": "

    Dans cet exemple, nous avons deux threads a et b qui partagent une variable globale x. Le thread a affecte la valeur 5 \u00e0 x et l'affiche, tandis que le thread b affecte la valeur 7 \u00e0 x. Les threads s'ex\u00e9cutent ind\u00e9pendamment et peuvent modifier x simultan\u00e9ment, ce qui peut entra\u00eener des r\u00e9sultats impr\u00e9visibles.

    #include <iostream>\n#include <thread>\n\nstatic int x = 0;\n\nint a() { for(;;) { x = 5; std::cout << x; } }\nint b() { for(;;) x = 7; }\n\nint main() {\n    std::thread ta(a);\n    std::thread tb(b);\n    ta.join();\n    tb.join();\n}\n

    Exercice\u2009: Testez quelle est la probablilit\u00e9 d'avoir un 7 dans la sortie.

    Pour cela faite tourner le programme en redirigeant la sortie standard vers un fichier, puis comptez le nombre de 7 dans le fichier. Ex\u00e9cutez le programme pendant 5 secondes avec la commande timeout.

    Attention, le fichier peut \u00eatre tr\u00e8s volumineux, la commande grep pour chercher les 7 peut \u00eatre utilis\u00e9e comme suit\u2009:

    $ timeout 5s ./a.out > data\n$ grep -o 5 data | wc -l\n$ grep -o 7 data | wc -l\n
    ", "tags": ["timeout"]}, {"location": "course-concurrent/os/", "title": "Syst\u00e8me d'exploitation (POSIX)", "text": ""}, {"location": "course-concurrent/os/#processus", "title": "Processus", "text": "

    Un processus est un programme en cours d'ex\u00e9cution. Il est compos\u00e9 d'un espace d'adressage, d'un ensemble de ressources (fichiers, sockets, etc.) et d'un ou plusieurs threads. Un processus est identifi\u00e9 par un PID (Process IDentifier).

    "}, {"location": "course-concurrent/os/#etats-dun-processus", "title": "\u00c9tats d'un processus", "text": ""}, {"location": "course-concurrent/os/#etat-du-processus", "title": "Etat du processus", "text": "

    Nouveau (New) : Un processus vient d'\u00eatre cr\u00e9\u00e9 mais n'a pas encore \u00e9t\u00e9 admis par l'ordonnanceur du syst\u00e8me d'exploitation pour ex\u00e9cution.

    Pr\u00eat (Ready) : Le processus est charg\u00e9 en m\u00e9moire et attend qu'un processeur lui soit attribu\u00e9 par l'ordonnanceur pour ex\u00e9cuter ses instructions.

    Ex\u00e9cution (Running) : Le processus est en cours d'ex\u00e9cution sur un processeur. Dans cet \u00e9tat, le processus effectue les op\u00e9rations pour lesquelles il a \u00e9t\u00e9 con\u00e7u.

    En attente (Waiting ou Blocked) : Le processus ne peut pas ex\u00e9cuter tant qu'un certain \u00e9v\u00e9nement n'a pas lieu, comme la fin d'une op\u00e9ration d'entr\u00e9e/sortie. Dans cet \u00e9tat, le processus est suspendu et attend que la condition bloquante soit lev\u00e9e.

    Termin\u00e9 (Terminated ou Zombie) : Un processus entre dans cet \u00e9tat une fois qu'il a fini de s'ex\u00e9cuter. Cependant, il reste dans la table des processus jusqu'\u00e0 ce que son processus parent r\u00e9cup\u00e8re son code de sortie, permettant au syst\u00e8me de lib\u00e9rer les ressources associ\u00e9es. Un processus en \u00e9tat \u00ab\u2009zombie\u2009\u00bb a termin\u00e9 son ex\u00e9cution mais attend que son processus parent appelle wait() pour r\u00e9cup\u00e9rer son statut de sortie.

    Orphelin (Orphan) : Un processus devient orphelin si son processus parent se termine avant lui. Les processus orphelins sont adopt\u00e9s par le processus init (PID 1), qui r\u00e9cup\u00e8re automatiquement leur statut de sortie, \u00e9vitant ainsi la cr\u00e9ation de zombies.

    Interrompu (Stopped) : Un processus peut \u00eatre mis en pause (ou arr\u00eat\u00e9 temporairement) par un signal (par exemple, SIGSTOP). Il peut \u00eatre repris plus tard (par exemple, avec le signal SIGCONT).

    Zombie (Zombie) : Comme mentionn\u00e9 pr\u00e9c\u00e9demment, un processus \u00ab\u2009zombie\u2009\u00bb ou \u00ab\u2009processus d\u00e9funt\u2009\u00bb est un processus qui a termin\u00e9 son ex\u00e9cution mais reste dans la table des processus parce que son processus parent n'a pas encore r\u00e9cup\u00e9r\u00e9 son statut de sortie. Cela permet au parent de r\u00e9cup\u00e9rer les informations sur le statut de fin de son enfant. Un processus zombie ne consomme pas de ressources \u00e0 part une entr\u00e9e dans la table des processus.

    "}, {"location": "course-concurrent/os/#creation-dun-processus", "title": "Cr\u00e9ation d'un processus", "text": "

    Sous les syst\u00e8mes d'exploitation POSIX, il existe principalement deux fa\u00e7ons de cr\u00e9er un processus\u2009:

    "}, {"location": "course-concurrent/os/#fork", "title": "fork()", "text": "

    La fonction fork() est utilis\u00e9e pour cr\u00e9er un nouveau processus, appel\u00e9 processus enfant, qui est une copie du processus appelant (processus parent). L'enfant re\u00e7oit une copie des donn\u00e9es du parent, mais les deux processus ont des espaces d'adressage s\u00e9par\u00e9s. Apr\u00e8s le fork(), les deux processus (parent et enfant) continuent leur ex\u00e9cution \u00e0 partir de l'instruction suivante apr\u00e8s l'appel fork(). La principale diff\u00e9rence entre eux est la valeur de retour de fork(): 0 pour l'enfant et l'ID du processus enfant (PID) pour le parent, ou -1 en cas d'\u00e9chec.

    "}, {"location": "course-concurrent/os/#exec", "title": "exec()", "text": "

    La famille de fonctions exec() est utilis\u00e9e pour ex\u00e9cuter un nouveau programme dans l'espace d'adressage d'un processus. Cela remplace l'image du processus actuel par une nouvelle image de programme. Les diff\u00e9rentes variantes de exec() (comme execl(), execp(), execv(), etc.) permettent de sp\u00e9cifier le programme \u00e0 ex\u00e9cuter et de passer des arguments de diff\u00e9rentes mani\u00e8res. Habituellement, exec() est appel\u00e9 par un processus enfant apr\u00e8s un fork() pour remplacer son image par celle du programme \u00e0 ex\u00e9cuter, permettant ainsi au processus parent de continuer \u00e0 ex\u00e9cuter son code original tout en lan\u00e7ant un nouveau programme dans le processus enfant. Ces deux fonctions sont souvent utilis\u00e9es ensemble pour cr\u00e9er un nouveau processus et ex\u00e9cuter un nouveau programme au sein de ce processus. Le mod\u00e8le \u00ab\u2009fork-exec\u2009\u00bb est un motif commun dans les syst\u00e8mes POSIX pour la cr\u00e9ation de processus et l'ex\u00e9cution de programmes. Ce m\u00e9canisme permet une grande flexibilit\u00e9 dans la gestion des processus, en permettant \u00e0 un processus parent de contr\u00f4ler l'ex\u00e9cution de processus enfants et de r\u00e9cup\u00e9rer leur statut de sortie une fois qu'ils ont termin\u00e9.

    "}, {"location": "course-concurrent/os/#exemple-de-zombie", "title": "Exemple de zombie", "text": "
    #include <iostream>\n#include <unistd.h> // Pour fork(), getpid(), sleep()\n#include <sys/wait.h> // Pour wait()\n\nint main() {\n    pid_t pid = fork(); // Cr\u00e9e un nouveau processus\n\n    if (pid == -1) {\n        // En cas d'\u00e9chec du fork()\n        std::cerr << \"\u00c9chec du fork()\" << std::endl;\n        return 1;\n    } else if (pid > 0) {\n        // Code ex\u00e9cut\u00e9 par le processus parent\n        std::cout << \"Je suis le processus parent (PID: \" << getpid() << \"), et mon enfant a le PID \" << pid << std::endl;\n        std::cout << \"Je dors 20 secondes. V\u00e9rifiez l'\u00e9tat du processus enfant avec 'ps -l'.\" << std::endl;\n        sleep(20); // Le parent dort, laissant l'enfant devenir un zombie\n        std::cout << \"Le parent se r\u00e9veille et se termine. Le processus enfant devrait \u00eatre nettoy\u00e9.\" << std::endl;\n    } else {\n        // Code ex\u00e9cut\u00e9 par le processus enfant\n        std::cout << \"Je suis le processus enfant (PID: \" << getpid() << \") et je me termine.\" << std::endl;\n        // L'enfant se termine imm\u00e9diatement, devenant un zombie jusqu'\u00e0 ce\n        // que le parent termine son sommeil\n    }\n}\n
    "}, {"location": "course-concurrent/os/#status-des-processus-avec-ps", "title": "Status des processus avec ps", "text": "

    Lorsque vous utilisez la commande ps -l sur un syst\u00e8me UNIX ou Linux pour lister les processus avec des informations d\u00e9taill\u00e9es, vous pouvez observer diff\u00e9rents \u00e9tats de processus repr\u00e9sent\u00e9s par des lettres. Voici les principaux \u00e9tats que vous pourriez voir\u2009:

    D: Non interrompible (uninterruptible sleep) - Le processus est en attente d'une op\u00e9ration d'entr\u00e9e/sortie et ne peut pas \u00eatre interrompu.

    R: Ex\u00e9cutable (running or runnable) - Le processus est en cours d'ex\u00e9cution sur un processeur ou en attente d'\u00eatre ex\u00e9cut\u00e9.

    S : Endormi (interruptible sleep) - Le processus attend un \u00e9v\u00e9nement pour se poursuivre.

    T : Arr\u00eat\u00e9 (stopped) - Le processus a \u00e9t\u00e9 arr\u00eat\u00e9, g\u00e9n\u00e9ralement par un signal de contr\u00f4le comme SIGSTOP.

    Z : Zombie (defunct) - Le processus s'est termin\u00e9, mais des informations sont toujours conserv\u00e9es dans la table des processus car le processus parent n'a pas encore r\u00e9cup\u00e9r\u00e9 son statut de sortie.

    I : Processus inactif (idle) - Utilis\u00e9 pour les t\u00e2ches du noyau.

    W : Pagin\u00e9 - Un \u00e9tat obsol\u00e8te non utilis\u00e9 dans les versions modernes de Unix/Linux, indiquant qu'un processus \u00e9tait \u00e9chang\u00e9 (swapped out). L'\u00e9tat du processus est une information cl\u00e9 pour comprendre ce que fait un processus \u00e0 un moment donn\u00e9. Les \u00e9tats comme \u00ab\u2009D\u2009\u00bb et \u00ab\u2009S\u2009\u00bb indiquent qu'un processus attend quelque chose pour continuer, tandis que \u00ab\u2009R\u2009\u00bb signifie que le processus est soit en cours d'ex\u00e9cution, soit pr\u00eat \u00e0 \u00eatre ex\u00e9cut\u00e9 d\u00e8s qu'un processeur est disponible. Les \u00e9tats \u00ab\u2009T\u2009\u00bb et \u00ab\u2009Z\u2009\u00bb sont des cas sp\u00e9ciaux, indiquant respectivement un processus arr\u00eat\u00e9 et un processus qui s'est termin\u00e9 mais dont les ressources ne sont pas encore totalement lib\u00e9r\u00e9es par le syst\u00e8me.

    "}, {"location": "course-concurrent/os/#communication-inter-processus-ipc", "title": "Communication inter-processus (IPC)", "text": "

    Les processus dans les syst\u00e8mes d'exploitation POSIX peuvent communiquer entre eux par plusieurs m\u00e9canismes de communication interprocessus (IPC - Inter-Process Communication). Ces m\u00e9canismes permettent aux processus de partager des donn\u00e9es et de coordonner leurs actions. Voici les principaux moyens de communication interprocessus\u2009:

    Pipes (tuyaux) : Un pipe permet \u00e0 un flux de donn\u00e9es de passer d'un processus \u00e0 l'autre. Les pipes anonymes sont utilis\u00e9s pour la communication entre un processus parent et son enfant ou entre enfants d'un m\u00eame parent. Les pipes nomm\u00e9s (FIFOs) peuvent \u00eatre utilis\u00e9s entre n'importe quels processus sur le syst\u00e8me.

    Signaux : Les signaux sont des messages simples envoy\u00e9s \u00e0 un processus pour lui indiquer qu'un \u00e9v\u00e9nement particulier s'est produit. Bien qu'ils ne transportent pas de grandes quantit\u00e9s de donn\u00e9es, ils sont utiles pour contr\u00f4ler les op\u00e9rations des processus et pour la gestion d'\u00e9v\u00e9nements asynchrones.

    M\u00e9moire partag\u00e9e : La m\u00e9moire partag\u00e9e est un segment de m\u00e9moire qui peut \u00eatre acc\u00e9d\u00e9 par plusieurs processus. C'est un moyen efficace pour \u00e9changer de grandes quantit\u00e9s de donn\u00e9es entre processus, car elle \u00e9vite la copie de donn\u00e9es d'un espace d'adressage \u00e0 l'autre.

    S\u00e9maphores : Les s\u00e9maphores sont utilis\u00e9s pour synchroniser l'acc\u00e8s \u00e0 des ressources partag\u00e9es par plusieurs processus. Ils peuvent \u00eatre utilis\u00e9s pour \u00e9viter les conditions de course en contr\u00f4lant l'acc\u00e8s \u00e0 des ressources telles que les fichiers ou les segments de m\u00e9moire partag\u00e9e.

    Files de messages : Les files de messages permettent aux processus d'envoyer et de recevoir des messages sous forme de files d'attente. Chaque message est une structure de donn\u00e9es qui peut contenir des informations vari\u00e9es. Les files de messages sont utiles pour \u00e9changer de petites quantit\u00e9s de donn\u00e9es de mani\u00e8re asynchrone.

    Sockets : Les sockets permettent la communication entre processus sur le m\u00eame ordinateur ou entre processus sur des ordinateurs diff\u00e9rents dans un r\u00e9seau. Ils supportent la communication en mode connect\u00e9 (TCP) et non connect\u00e9 (UDP) et sont la base de nombreuses communications r\u00e9seau, y compris le Web.

    Ces m\u00e9canismes offrent divers degr\u00e9s d'abstraction et peuvent \u00eatre choisis en fonction des besoins sp\u00e9cifiques en mati\u00e8re de communication interprocessus, comme la quantit\u00e9 de donn\u00e9es \u00e0 transf\u00e9rer, la n\u00e9cessit\u00e9 de synchronisation ou la pr\u00e9f\u00e9rence entre la communication locale et la communication en r\u00e9seau.

    "}, {"location": "course-concurrent/os/#exemple", "title": "Exemple", "text": "
    #include <iostream>\n#include <unistd.h>\n#include <sys/wait.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n#include <signal.h>\n#include <vector>\n\nconstexpr int SHM_SIZE = 1024; // Taille du segment de m\u00e9moire partag\u00e9e\nconstexpr int NUM_CHILDREN = 10; // Nombre de processus enfants \u00e0 cr\u00e9er\n\n// Handler de signal pour les processus enfants\nvoid signalHandler(int sig) {\n    std::cout << \"Processus \" << getpid() << \" lit la valeur.\" << std::endl;\n    // Ici, vous acc\u00e9deriez \u00e0 la m\u00e9moire partag\u00e9e pour lire la valeur\n    // C'est une simplification; l'acc\u00e8s r\u00e9el n\u00e9cessiterait des pointeurs et une synchronisation\n}\n\nint main() {\n    int shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);\n    if (shm_id < 0) {\n        std::cerr << \"\u00c9chec de la cr\u00e9ation de la m\u00e9moire partag\u00e9e.\" << std::endl;\n        return 1;\n    }\n    int* shared_memory = (int*)shmat(shm_id, nullptr, 0);\n    *shared_memory = 0; // Initialisation de la m\u00e9moire partag\u00e9e\n\n    signal(SIGUSR1, signalHandler); // Configuration du handler de signal pour tous les processus\n\n    std::vector<pid_t> children;\n\n    // Cr\u00e9ation de 10 processus enfants\n    for (int i = 0; i < NUM_CHILDREN; ++i) {\n        pid_t pid = fork();\n        if (pid == 0) { // Enfant\n            // Boucle infinie pour que l'enfant reste actif\n            while (true) pause(); // Attend le signal\n            exit(0);\n        } else if (pid > 0) { // Parent\n            children.push_back(pid);\n        } else {\n            std::cerr << \"\u00c9chec du fork().\" << std::endl;\n            return 1;\n        }\n    }\n\n    // Code du processus parent pour modifier la m\u00e9moire partag\u00e9e et envoyer des signaux\n    while (true) {\n        int val;\n        std::cout << \"Entrez une valeur \u00e0 \u00e9crire dans la m\u00e9moire partag\u00e9e: \";\n        std::cin >> val;\n        *shared_memory = val; // \u00c9crit dans la m\u00e9moire partag\u00e9e\n        for (pid_t child : children) {\n            kill(child, SIGUSR1); // Envoie un signal \u00e0 chaque enfant\n        }\n    }\n\n    // Nettoyage (pas atteint dans cet exemple simplifi\u00e9)\n    for (pid_t child : children) {\n        int status;\n        waitpid(child, &status, 0);\n    }\n    shmdt(shared_memory);\n    shmctl(shm_id, IPC_RMID, nullptr);\n}\n

    Avec htop, vous pouvez observer les processus enfants en attente de signal. Utilisez la fonction t pour afficher le mode arbre. Vous pouvez \u00e9galement observer la m\u00e9moire partag\u00e9e avec ipcs -m.

    Dans cet exemple, un segment de m\u00e9moire partag\u00e9e est cr\u00e9\u00e9 pour stocker une valeur enti\u00e8re. Le processus parent \u00e9crit une valeur dans la m\u00e9moire partag\u00e9e et envoie un signal \u00e0 chaque processus enfant. Chaque processus enfant est configur\u00e9 pour ex\u00e9cuter un gestionnaire de signal qui lit la valeur de la m\u00e9moire partag\u00e9e. Cela permet de communiquer des donn\u00e9es entre le processus parent et les processus enfants de mani\u00e8re asynchrone.

    N\u00e9anmoins il faut d'avantage pour g\u00e9rer la synchronisation et la communication entre les processus. Les exemples ci-dessus sont des simplifications pour illustrer les concepts de base.

    ", "tags": ["htop"]}, {"location": "course-concurrent/os/#synchronisation-de-la-memoire-partagee", "title": "Synchronisation de la m\u00e9moire partag\u00e9e", "text": "

    Dans cet exemple, on se contente d'informer l'utilisateur qu'une valeur a \u00e9t\u00e9 d\u00e9pos\u00e9e dans la m\u00e9moire partag\u00e9e mais on ne peut pas la lire directement. On a besoin d'un m\u00e9canisme de synchronisation.

    #include <iostream>\n#include <unistd.h>\n#include <sys/wait.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n#include <signal.h>\n#include <vector>\n#include <semaphore.h>\n\nconstexpr int SHM_SIZE = sizeof(int); // Taille du segment de m\u00e9moire partag\u00e9e pour stocker un entier\nconstexpr int NUM_CHILDREN = 10; // Nombre de processus enfants \u00e0 cr\u00e9er\n\nint* shared_memory; // Pointeur vers la m\u00e9moire partag\u00e9e\nsem_t* sem; // Pointeur vers le s\u00e9maphore\n\nvoid signalHandler(int sig) {\n    sem_wait(sem); // Attendre pour acc\u00e9der \u00e0 la section critique\n    std::cout << \"Processus \" << getpid() << \" lit la valeur \" << *shared_memory << \".\" << std::endl;\n    sem_post(sem); // Lib\u00e9rer l'acc\u00e8s \u00e0 la section critique\n}\n\nint main() {\n    // Cr\u00e9ation de la m\u00e9moire partag\u00e9e\n    int shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);\n    if (shm_id < 0) {\n        std::cerr << \"\u00c9chec de la cr\u00e9ation de la m\u00e9moire partag\u00e9e.\" << std::endl;\n        return 1;\n    }\n    shared_memory = (int*)shmat(shm_id, nullptr, 0);\n\n    // Initialisation du s\u00e9maphore dans la m\u00e9moire partag\u00e9e\n    sem = new sem_t;\n    if (sem_init(sem, 1, 1) == -1) { // 1 pour partage entre processus\n        std::cerr << \"Erreur d'initialisation du s\u00e9maphore.\" << std::endl;\n        return 1;\n    }\n\n    signal(SIGUSR1, signalHandler); // Configuration du gestionnaire de signal\n\n    std::vector<pid_t> children;\n\n    // Cr\u00e9ation des processus enfants\n    for (int i = 0; i < NUM_CHILDREN; ++i) {\n        pid_t pid = fork();\n        if (pid == 0) { // Enfant\n            while (true) pause(); // Attend le signal\n            exit(0);\n        } else if (pid > 0) { // Parent\n            children.push_back(pid);\n        } else {\n            std::cerr << \"\u00c9chec du fork().\" << std::endl;\n            return 1;\n        }\n    }\n\n    // Code du processus parent pour lire et mettre \u00e0 jour la m\u00e9moire partag\u00e9e\n    while (true) {\n        int val;\n        std::cout << \"Entrez une valeur \u00e0 \u00e9crire dans la m\u00e9moire partag\u00e9e: \";\n        std::cin >> val;\n        sem_wait(sem); // Acc\u00e9der \u00e0 la section critique\n        *shared_memory = val; // \u00c9crire dans la m\u00e9moire partag\u00e9e\n        sem_post(sem); // Lib\u00e9rer l'acc\u00e8s\n        for (pid_t child : children) {\n            kill(child, SIGUSR1); // Signaler aux enfants de lire la valeur\n        }\n    }\n\n    // Nettoyage (non atteint dans cet exemple)\n    for (pid_t child : children) {\n        int status;\n        waitpid(child, &status, 0);\n    }\n    shmdt(shared_memory);\n    shmctl(shm_id, IPC_RMID, nullptr);\n    sem_destroy(sem);\n    delete sem;\n}\n
    "}, {"location": "course-concurrent/os/#processus-leger-versus-processus-clonefork", "title": "Processus l\u00e9ger versus processus (clone/fork)", "text": "

    Sous Linux, l'appel syst\u00e8me utilis\u00e9 pour cr\u00e9er des threads est clone() ou, dans certains cas, des variantes comme fork() ou vfork(). Cependant, clone() est plus flexible et permet une plus grande personnalisation du partage d'espace d'adressage, des fichiers, des signaux, etc., ce qui le rend particuli\u00e8rement adapt\u00e9 pour la cr\u00e9ation de threads.

    clone() : Cet appel syst\u00e8me est utilis\u00e9 pour cr\u00e9er un processus l\u00e9ger (un thread, dans ce contexte). Contrairement \u00e0 fork(), qui duplique le processus appelant dans un nouveau processus avec un espace d'adressage s\u00e9par\u00e9, clone() permet de sp\u00e9cifier exactement quels \u00e9l\u00e9ments (espace d'adressage, table des fichiers ouverts, espace des signaux, etc.) doivent \u00eatre partag\u00e9s entre le thread appelant et le nouveau thread. Cela permet une cr\u00e9ation de thread plus efficace et un partage de ressources entre les threads.

    "}, {"location": "course-concurrent/os/#processus_1", "title": "Processus", "text": "

    Un processus est une instance d'un programme en cours d'ex\u00e9cution. Chaque processus poss\u00e8de son propre espace d'adressage virtuel, ses propres ressources (comme des descripteurs de fichiers) et son propre contexte d'ex\u00e9cution (registres, pile, etc.). Les processus sont isol\u00e9s les uns des autres, ce qui signifie que sans m\u00e9canismes de communication inter-processus (IPC), ils ne peuvent pas directement partager de donn\u00e9es. L'isolation entre les processus assure la s\u00e9curit\u00e9 et la stabilit\u00e9 du syst\u00e8me, car l'erreur dans un processus ne peut pas corrompre directement l'espace m\u00e9moire d'un autre processus.

    "}, {"location": "course-concurrent/os/#thread-processus-leger", "title": "Thread (Processus l\u00e9ger)", "text": "

    Un thread, souvent appel\u00e9 un processus l\u00e9ger, est une unit\u00e9 d'ex\u00e9cution qui peut \u00eatre cr\u00e9\u00e9e au sein d'un processus. Contrairement aux processus, les threads d'un m\u00eame processus partagent le m\u00eame espace d'adressage et peuvent acc\u00e9der aux m\u00eames donn\u00e9es en m\u00e9moire. Cela facilite le partage d'informations et la communication entre les threads, mais requiert \u00e9galement une synchronisation soign\u00e9e pour \u00e9viter les conditions de course et les incoh\u00e9rences de donn\u00e9es. Les threads ont leur propre pile d'ex\u00e9cution et leur propre contexte de registres, mais partagent d'autres ressources avec les threads du m\u00eame processus, comme les descripteurs de fichiers et les segments de m\u00e9moire allou\u00e9s.

    "}, {"location": "course-concurrent/os/#differences-cles", "title": "Diff\u00e9rences cl\u00e9s", "text": "

    Isolation\u2009: Les processus sont isol\u00e9s les uns des autres, tandis que les threads partagent l'espace d'adressage du processus qui les a cr\u00e9\u00e9s. Cr\u00e9ation et terminaison\u2009: Cr\u00e9er un nouveau processus est g\u00e9n\u00e9ralement plus co\u00fbteux en termes de ressources et de temps que cr\u00e9er un thread. De m\u00eame, terminer un processus et nettoyer ses ressources est plus lourd que terminer un thread.

    Communication\u2009: La communication entre processus n\u00e9cessite l'utilisation de m\u00e9canismes IPC, tels que les files de messages, les tubes (pipes), la m\u00e9moire partag\u00e9e, etc. En revanche, les threads peuvent communiquer directement en lisant et \u00e9crivant dans la m\u00e9moire partag\u00e9e du processus, bien que cela n\u00e9cessite une synchronisation, comme l'utilisation de verrous ou de s\u00e9maphores. Utilisation\u2009: Les processus sont utiles pour ex\u00e9cuter des t\u00e2ches isol\u00e9es et s\u00e9curis\u00e9es, sans risque d'interf\u00e9rence directe entre elles. Les threads sont pr\u00e9f\u00e9r\u00e9s pour les t\u00e2ches qui n\u00e9cessitent un partage intensif de donn\u00e9es ou une communication rapide entre les unit\u00e9s d'ex\u00e9cution au sein d'une m\u00eame application.

    En r\u00e9sum\u00e9, bien que les threads (ou processus l\u00e9gers) et les processus servent tous deux \u00e0 ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente, ils diff\u00e8rent par leur niveau d'isolation, leurs co\u00fbts de cr\u00e9ation et de gestion, ainsi que par leur mani\u00e8re de communiquer et de partager des donn\u00e9es.

    "}, {"location": "course-concurrent/os/#appel-systeme-clone", "title": "Appel syst\u00e8me clone()", "text": "
    #include <sched.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <sys/wait.h>\n#include <unistd.h>\n\n// Taille de la pile allou\u00e9e pour le thread cr\u00e9\u00e9 par clone\n#define STACK_SIZE (1024 * 1024)\n\n// La fonction \u00e0 ex\u00e9cuter dans le thread\nint threadFunction(void *arg) {\n    printf(\"Bonjour du thread!\\n\");\n    return 0;\n}\n\nint main() {\n    // Alloue de l'espace pour la pile du thread\n    char *stack = (char *)malloc(STACK_SIZE);\n    if (stack == NULL) {\n        perror(\"malloc\");\n        exit(1);\n    }\n\n    // Pr\u00e9pare le sommet de la pile pour le nouveau thread (clone grandit vers le bas)\n    char *stackTop = stack + STACK_SIZE;\n\n    // Cr\u00e9e le thread. Les flags d\u00e9terminent quels \u00e9l\u00e9ments sont partag\u00e9s.\n    // CLONE_VM partage l'espace d'adressage, et SIGCHLD est utilis\u00e9 pour que waitpid() puisse \u00eatre utilis\u00e9.\n    pid_t pid = clone(threadFunction, stackTop, CLONE_VM | SIGCHLD, NULL);\n\n    if (pid == -1) {\n        perror(\"clone\");\n        exit(1);\n    }\n\n    // Attend que le thread (processus) cr\u00e9\u00e9 se termine\n    if (waitpid(pid, NULL, 0) == -1) {\n        perror(\"waitpid\");\n        exit(1);\n    }\n\n    // Nettoie\n    free(stack);\n\n    printf(\"Thread termin\u00e9\\n\");\n}\n
    "}, {"location": "course-concurrent/os/#signaux", "title": "Signaux", "text": "

    Sous les syst\u00e8mes d'exploitation POSIX, les signaux sont des messages logiciels envoy\u00e9s \u00e0 un processus pour lui indiquer qu'un \u00e9v\u00e9nement particulier s'est produit. Les signaux sont utilis\u00e9s pour g\u00e9rer les interruptions, les erreurs et les \u00e9v\u00e9nements asynchrones, et peuvent \u00eatre envoy\u00e9s par le noyau, par d'autres processus ou par le processus lui-m\u00eame.

    Voici les signaux POSIX standard et leurs descriptions\u2009:

    Signal Description SIGHUP(1) Terminaison du terminal ou du processus contr\u00f4lant SIGINT(2) Interruption depuis le clavier SIGQUIT(3) Interruption depuis le clavier avec un core dump SIGILL(4) Instruction ill\u00e9gale SIGTRAP(5) Trace ou point d'arr\u00eat SIGABRT(6) Signal d'abandon SIGBUS(7) Erreur de bus SIGFPE(8) Erreur arithm\u00e9tique SIGKILL(9) Terminaison forc\u00e9e SIGUSR1(10) Signal utilisateur 1 SIGSEGV(11) Violation de la segmentation SIGUSR2(12) Signal utilisateur 2 SIGPIPE(13) \u00c9criture sur un tube sans lecteur SIGALRM(14) Alarme horloge SIGTERM(15) Terminaison SIGSTKFLT(16) Erreur de pile SIGCHLD(17) Enfant termin\u00e9 ou arr\u00eat\u00e9 SIGCONT(18) Continuer l'ex\u00e9cution, si arr\u00eat\u00e9 SIGSTOP(19) Arr\u00eat de l'ex\u00e9cution du processus SIGTSTP(20) Arr\u00eat de l'ex\u00e9cution du processus depuis le clavier SIGTTIN(21) Lecture depuis le terminal en arri\u00e8re-plan SIGTTOU(22) \u00c9criture sur le terminal en arri\u00e8re-plan SIGURG(23) Donn\u00e9es urgentes sur le socket SIGXCPU(24) Temps CPU \u00e9coul\u00e9 SIGXFSZ(25) Taille de fichier maximale d\u00e9pass\u00e9e SIGVTALRM(26) Alarme virtuelle SIGPROF(27) Profilage du signal SIGWINCH(28) Changement de taille de fen\u00eatre SIGIO(29) \u00c9v\u00e9nement d'entr\u00e9e/sortie asynchrone SIGPWR(30) \u00c9v\u00e9nement d'alimentation SIGSYS(31) Erreur syst\u00e8me

    Les signaux les plus couramment utilis\u00e9s sont SIGINT (interruption depuis le clavier), SIGTERM (terminaison) et SIGKILL (terminaison forc\u00e9e). Ces signaux sont souvent utilis\u00e9s pour demander \u00e0 un processus de se terminer ou de r\u00e9agir \u00e0 des \u00e9v\u00e9nements utilisateur.

    ", "tags": ["SIGKILL", "SIGTERM", "SIGINT"]}, {"location": "course-concurrent/os/#information-sur-un-processus", "title": "Information sur un processus", "text": ""}, {"location": "course-concurrent/os/#pstree", "title": "pstree", "text": "

    Pour afficher l'arborescence des processus, vous pouvez utiliser la commande pstree. Par exemple, pour afficher l'arborescence des processus pour l'utilisateur actuel, vous pouvez ex\u00e9cuter\u2009:

    Exercice\u2009:

    Essayez de lancer un programme en arri\u00e8re-plan (par exemple, ./myprogram &) et utilisez pstree pour voir comment il est li\u00e9 \u00e0 d'autres processus.

    $ pstree\n
    ", "tags": ["pstree"]}, {"location": "course-concurrent/os/#proc", "title": "proc", "text": "

    Ex\u00e9cutez le programme suivant\u2009:

    // Read string and print it\n#include <iostream>\n\nint main()\n{\n    std::string s;\n    std::cin >> s;\n    std::cout << s << std::endl;\n    return 0;\n}\n
    $ g++ -o read read.cpp\n$ ./read &\n[1] 12345\n$ cat /proc/12345/status\n$ cat /proc/12345/maps\n$ cat /proc/12345/smaps\n
    ", "tags": ["proc"]}, {"location": "course-concurrent/os/#appels-systemes-utiles", "title": "Appels syst\u00e8mes utiles", "text": "Appel syst\u00e8me Description fork() Cr\u00e9e un nouveau processus en dupliquant le processus appelant. exec() Ex\u00e9cute un nouveau programme dans le contexte du processus appelant. wait() Attend la terminaison d'un processus enfant. waitpid() Attend la terminaison d'un processus enfant sp\u00e9cifique. clone() Cr\u00e9e un nouveau processus l\u00e9ger (thread) avec des options de partage personnalis\u00e9es. kill() Envoie un signal \u00e0 un processus. signal() Configure un gestionnaire de signal pour un signal donn\u00e9. pipe() Cr\u00e9e un tube (pipe) pour la communication entre processus. shmget() Alloue un segment de m\u00e9moire partag\u00e9e. shmat() Attache un segment de m\u00e9moire partag\u00e9e \u00e0 l'espace d'adressage d'un processus. sem_init() Initialise un s\u00e9maphore pour la synchronisation entre processus. mmap() Mappe un fichier ou un p\u00e9riph\u00e9rique dans l'espace d'adressage d'un processus. mprotect() Modifie les protections d'acc\u00e8s pour une r\u00e9gion de m\u00e9moire. munmap() Supprime un mappage de m\u00e9moire. nice() Modifie la priorit\u00e9 de planification d'un processus. sched_setaffinity() Modifie l'affinit\u00e9 du processeur pour un processus. getpriority() Obtient la priorit\u00e9 de planification d'un processus. setpriority() Modifie la priorit\u00e9 de planification d'un processus."}, {"location": "course-concurrent/semaphores/", "title": "Semaphore", "text": "

    Le semaphore a \u00e9t\u00e9 introduit par Edsger Dijkstra en 1965. Il s'agit d'une variable enti\u00e8re non n\u00e9gative qui peut \u00eatre utilis\u00e9e pour synchroniser l'acc\u00e8s \u00e0 une ressource partag\u00e9e. Un s\u00e9maphore est un objet de synchronisation qui permet \u00e0 plusieurs threads d'acc\u00e9der \u00e0 une ressource partag\u00e9e en m\u00eame temps.

    Historiquement, puisque Dijkstra \u00e9tait N\u00e9erlandais, il a choisi comme noms de signaux\u2009:

    • P pour \u00ab\u2009Proberen\u2009\u00bb (essayer en n\u00e9erlandais)
    • V pour \u00ab\u2009Verhogen\u2009\u00bb (augmenter en n\u00e9erlandais)

    Un s\u00e9maphore \u00e9tait vu comme la brique de base pour la synchronisation de threads. Il est toujours utilis\u00e9 dans les syst\u00e8mes d'exploitation modernes pour impl\u00e9menter des m\u00e9canismes de synchronisation tels que les mutex, les moniteurs, les barri\u00e8res, etc.

    N\u00e9anmoins, on peut \u00e9muler un s\u00e9maphore avec un mutex et une variable condition. C'est ce que fait la classe std::counting_semaphore de la biblioth\u00e8que standard C++20. Voici comment on peut \u00e9muler un s\u00e9maphore avec un mutex et une variable condition\u2009:

    #include <mutex>\n#include <condition_variable>\n\nclass Semaphore {\n    std::mutex mtx;\n    std::condition_variable cv;\n    int count;\n\npublic:\n    Semaphore(int count = 0) : count(count) {}\n\n    // Proberen\n    void notify() {\n        std::unique_lock<std::mutex> lock(mtx);\n        ++count;\n        cv.notify_one();\n    }\n\n    // Verhogen\n    void wait() {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this] { return count > 0; });\n        --count;\n    }\n};\n

    Dans cet exemple, la m\u00e9thode notify incr\u00e9mente le compteur du s\u00e9maphore et notifie un thread en attente. La m\u00e9thode wait attend que le compteur soit sup\u00e9rieur \u00e0 z\u00e9ro, puis le d\u00e9cr\u00e9mente.

    Il est parfois utile pour compter des ressources de fournir aux m\u00e9thodes notify et wait un argument n pour incr\u00e9menter ou d\u00e9cr\u00e9menter le compteur de n unit\u00e9s. Cela permet de lib\u00e9rer ou d'acqu\u00e9rir plusieurs ressources en une seule op\u00e9ration. Par exemple, si n est \u00e9gal \u00e0 3, cela signifie que trois ressources sont lib\u00e9r\u00e9es ou acquises en une seule op\u00e9ration.

    #include <mutex>\n#include <condition_variable>\n\nclass Semaphore {\n    std::mutex mtx;\n    std::condition_variable cv;\n    int count;\n\npublic:\n    Semaphore(int count = 0) : count(count) {}\n\n    void notify(int n = 1) {\n        std::unique_lock<std::mutex> lock(mtx);\n        count += n;\n        cv.notify_all();\n    }\n\n    void wait(int n = 1) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this, n] { return count >= n; });\n        count -= n;\n    }\n};\n
    ", "tags": ["notify", "wait"]}, {"location": "course-concurrent/semaphores/#probleme-du-producteur-et-du-consommateur", "title": "Probl\u00e8me du producteur et du consommateur", "text": "

    Le probl\u00e8me du producteur et du consommateur est un probl\u00e8me classique de synchronisation de threads. Il s'agit de synchroniser un ou plusieurs threads producteurs qui produisent des donn\u00e9es et un ou plusieurs threads consommateurs qui consomment ces donn\u00e9es.

    Le probl\u00e8me du producteur et du consommateur peut \u00eatre r\u00e9solu \u00e0 l'aide de deux s\u00e9maphores\u2009:

    • Un s\u00e9maphore empty pour compter le nombre d'emplacements vides dans le tampon
    • Un s\u00e9maphore full pour compter le nombre d'emplacements pleins dans le tampon
    ", "tags": ["full", "empty"]}, {"location": "course-concurrent/summary/", "title": "R\u00e9sum\u00e9", "text": "

    POSIX (Portable Operating System Interface) est une norme qui d\u00e9finit une interface syst\u00e8me standard pour les syst\u00e8mes d'exploitation de type UNIX. Cette norme a \u00e9t\u00e9 d\u00e9finie par l'IEEE (Institute of Electrical and Electronics Engineers) et l'ISO (International Organization for Standardization). Elle a \u00e9t\u00e9 invent\u00e9e en 1988 pour permettre la portabilit\u00e9 des applications entre les syst\u00e8mes UNIX.

    "}, {"location": "course-concurrent/summary/#os", "title": "OS", "text": ""}, {"location": "course-concurrent/summary/#appels-systemes", "title": "Appels Syst\u00e8mes", "text": "
    • fork : Cr\u00e9e un nouveau processus en copiant le processus appelant.
    • clone : Cr\u00e9e un nouveau processus l\u00e9ger en copiant le processus appelant.
    • exec : Remplace l'image m\u00e9moire du processus appelant par un nouveau programme.
    • wait : Attend la fin d'un processus.
    • exit : Termine le processus appelant.
    • open : Ouvre un fichier.
    • close : Ferme un fichier.
    • read : Lit des donn\u00e9es depuis un fichier.
    • write : Ecrit des donn\u00e9es dans un fichier.
    • mmap : Mappe un fichier en m\u00e9moire (new, malloc).
    • ...
    ", "tags": ["open", "mmap", "exec", "clone", "fork", "read", "wait", "write", "exit", "close"]}, {"location": "course-concurrent/summary/#processus", "title": "Processus", "text": "

    Un processus c'est un programme en cours d'ex\u00e9cution. Il poss\u00e8de un espace d'adressage, un identifiant unique (PID), un \u00e9tat (running, waiting, sleeping, zombie), un parent (PPID), des ressources (fichiers ouverts, m\u00e9moire allou\u00e9e, ...).

    Pour cr\u00e9er un processus on a vu que l'appel syst\u00e8me fork permet de dupliquer le processus appelant. Le processus fils h\u00e9rite de l'espace d'adressage du processus parent. A ce moment l\u00e0 la m\u00e9moire est copi\u00e9e en mode \u00ab\u2009copy-on-write\u2009\u00bb. Cela signifie que la m\u00e9moire n'est pas copi\u00e9e imm\u00e9diatement mais seulement lorsqu'elle est modifi\u00e9e. Et les deux processus sont ind\u00e9pendants. Donc c'est lourd en m\u00e9moire.

    ", "tags": ["fork"]}, {"location": "course-concurrent/summary/#processus-legers-ou-threads", "title": "Processus l\u00e9gers ou threads", "text": "

    Un processus l\u00e9ger se cr\u00e9e avec l'appel syst\u00e8me clone. Il est plus l\u00e9ger qu'un processus car il partage le m\u00eame espace d'adressage que le processus parent. Cela signifie que les threads partagent les m\u00eames variables globales, les m\u00eames fichiers ouverts, les m\u00eames signaux, ... Mais cela peut poser des probl\u00e8mes de synchronisation\u2009: des acc\u00e8s concurrents \u00e0 des ressources partag\u00e9es.

    ", "tags": ["clone"]}, {"location": "course-concurrent/summary/#mecanismes-de-synchronisation", "title": "M\u00e9canismes de synchronisation", "text": "

    Pour synchroniser les threads on utilise des primitives de synchronisation. Les plus courantes sont les mutex, les s\u00e9maphores et les moniteurs.

    La notion de concurrence a \u00e9t\u00e9 imagin\u00e9e par Edsger Dijkstra en 1965 pour r\u00e9soudre les probl\u00e8mes de synchronisation dans les syst\u00e8mes d'exploitation. Il \u00e9tait d'origine n\u00e9erlandaise et a travaill\u00e9 pour la compagnie Philips.

    Il a invent\u00e9 le concept de s\u00e9maphore. Un s\u00e9maphore est un objet de synchronisation qui poss\u00e8de un compteur. Il poss\u00e8de deux op\u00e9rations atomiques\u2009: wait et post. wait d\u00e9cr\u00e9mente le compteur et bloque si le compteur est n\u00e9gatif. post incr\u00e9mente le compteur et r\u00e9veille un thread en attente si le compteur est n\u00e9gatif. Historiquement les noms donn\u00e9s par Dijkstra \u00e9taient P et V pour Proberen et Verhogen en n\u00e9erlandais.

    Le s\u00e9maphore compte des ressources disponibles. Si le s\u00e9maphore est \u00e0 z\u00e9ro, il n'y a plus de ressources disponibles. Si le s\u00e9maphore est \u00e0 un, il y a une ressource disponible. Si le s\u00e9maphore est \u00e0 deux...

    Plus tard sont venu des op\u00e9rations atomiques dans les processeurs comme le test-and-set ou le compare-and-swap. Ces op\u00e9rations permettent de r\u00e9aliser des primitives de synchronisation plus complexes comme les mutex. Le gros avantage c'est l'absence d'attente active. L'attente active c'est une boucle qui teste une condition en permanence. C'est tr\u00e8s gourmand en CPU.

    Une op\u00e9ration atomique dans le cadre de l'informatique c'est une op\u00e9ration qui s'ex\u00e9cute en une seule instruction machine. Cela signifie que l'op\u00e9ration est indivisible. C'est soit tout ou rien. C'est soit l'op\u00e9ration s'ex\u00e9cute compl\u00e8tement, soit elle ne s'ex\u00e9cute pas du tout.

    ", "tags": ["post", "Verhogen", "wait", "Proberen"]}, {"location": "course-concurrent/summary/#mutex", "title": "Mutex", "text": "

    En C++ le mutex est une classe qui permet de prot\u00e9ger des ressources partag\u00e9es entre plusieurs threads. Il poss\u00e8de deux m\u00e9thodes\u2009: lock et unlock. lock bloque le mutex si il est d\u00e9j\u00e0 verrouill\u00e9. unlock d\u00e9verrouille le mutex.

    #include <iostream>\n#include <thread>\n#include <mutex>\n\nstd::mutex mtx;\n\nvoid func() {\n    mtx.lock();\n    // Section critique\n    std::cout << \"Hello\" << std::endl;\n    mtx.unlock();\n}\n\nint main() {\n    std::thread t(func);\n    t.join();\n}\n

    On appelle section critique une portion de code qui acc\u00e8de \u00e0 des ressources partag\u00e9es. Il est important de prot\u00e9ger cette section critique avec un mutex pour \u00e9viter les acc\u00e8s concurrents.

    En g\u00e9n\u00e9ral on utilise pas le lock/unlock directement mais plut\u00f4t un std::lock_guard qui est un RAII (Resource Acquisition Is Initialization). C'est une classe qui s'occupe de lib\u00e9rer les ressources automatiquement \u00e0 la fin du bloc.

    void func() {\n    {\n        std::lock_guard<std::mutex> lock(mtx);\n        // Section critique\n        std::cout << \"Hello\" << std::endl;\n    }\n}\n

    On peut impl\u00e9menter tr\u00e8s facilement un lock_guard de la mani\u00e8re suivante\u2009:

    struct LockGuard {\n    LockGuard(std::mutex &mtx) : mtx(mtx) { mtx.lock(); }\n    ~LockGuard() { mtx.unlock(); }\nprivate:\n    std::mutex &mtx;\n};\n

    Aternativement au lock_guard on peut utiliser un std::unique_lock. La diff\u00e9rence c'est qu'un unique_lock est un verrou de port\u00e9e manuelle. Il peut \u00eatre d\u00e9verrouill\u00e9 et verrouill\u00e9 \u00e0 nouveau.

    void func() {\n    std::unique_lock<std::mutex> lock(mtx);\n    // Section critique\n    std::cout << \"Hello\" << std::endl;\n}\n

    Le unique_lock sera utilis\u00e9 par exemple dans les variables conditions.

    ", "tags": ["unlock", "lock_guard", "unique_lock", "lock"]}, {"location": "course-concurrent/summary/#variable-condition", "title": "Variable condition", "text": "

    Une variable condition est un objet de synchronisation qui permet de mettre un thread en attente tant qu'une condition n'est pas remplie. Elle utilise un mutex pour prot\u00e9ger la condition et permet de notifier des threads.

    Les m\u00e9thodes les plus courantes sont wait, wait_for, wait_until, notify_one et notify_all.

    int beers_in_fridge = 0;\nstd::mutex mtx;\nstd::variable_condition cv;\n\nvoid drinker() {\n    while(true) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, []{ return beers_in_fridge > 0; });\n        beers_in_fridge--;\n    }\n}\n\nvoid gf() {\n    while(true) {\n        std::unique_lock<std::mutex> lock(mtx);\n        beers_in_fridge++;\n        cv.notify_one();\n    }\n}\n\nint main() {\n    std::thread t1(drinker);\n    std::thread t2(gf);\n    t1.join();\n    t2.join();\n}\n

    Dans cet exemple, il y a un probl\u00e8me c'est que la copine (gf) peut mettre une bi\u00e8re dans le frigo alors qu'il est d\u00e9j\u00e0 plein. Il faudrait ajouter un s\u00e9maphore pour g\u00e9rer le nombre de bi\u00e8res dans le frigo. Puisqu'un s\u00e9maphore est un compteur de ressources.

    Mais ici, on est dans un cas de producteur-consommateur. Un producteur met des bi\u00e8res dans le frigo et un consommateur les boit.

    ", "tags": ["notify_one", "notify_all", "wait_until", "wait_for", "wait"]}, {"location": "course-concurrent/summary/#producteur-consommateur", "title": "Producteur-Consommateur", "text": "

    Le probl\u00e8me du producteur-consommateur est un probl\u00e8me classique de synchronisation.

    On peut le r\u00e9soudre de deux mani\u00e8res\u2009:

    1. Avec deux s\u00e9maphores\u2009: un pour le producteur et un pour le consommateur.
    2. Avec une variable condition.
    "}, {"location": "course-concurrent/summary/#moniteur", "title": "Moniteur", "text": "

    Un moniteur est un objet de synchronisation qui encapsule des donn\u00e9es et des op\u00e9rations sur ces donn\u00e9es. Il permet de prot\u00e9ger les donn\u00e9es et de synchroniser les threads qui acc\u00e8dent \u00e0 ces donn\u00e9es.

    Concr\u00e8tement c'est un classe qui contient des donn\u00e9es et des m\u00e9thodes pour acc\u00e9der \u00e0 ces donn\u00e9es. Les m\u00e9thodes sont prot\u00e9g\u00e9es par un mutex pour \u00e9viter les acc\u00e8s concurrents.

    class Fridge {\npublic:\n    void put(int beer) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this]{ return beers.size() < MAX; });\n        beers.push_back(beer);\n        cv.notify_one();\n    }\n\n    int get() {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this]{ return beers.size() > 0; });\n        int beer = beers.back();\n        beers.pop_back();\n        cv.notify_one();\n        return beer;\n    }\nprivate:\n    std::vector<int> beers;\n    std::mutex mtx;\n    std::variable_condition cv;\n};\n

    Si on veut l'utiliser dans l'exemple pr\u00e9c\u00e9dent on peut faire\u2009:

    void drinker(Fridge &fridge) {\n    while(true) { int beer = fridge.get(); }\n}\n\nvoid gf(Fridge &fridge) {\n    while(true) { fridge.put(1); }\n}\n\nint main() {\n    Fridge fridge;\n    std::thread t1(drinker, std::ref(fridge));\n    std::thread t2(gf, std::ref(fridge));\n    t1.join();\n    t2.join();\n}\n
    "}, {"location": "course-concurrent/summary/#deadlock-et-starvation", "title": "Deadlock et starvation", "text": "

    Le deadlock est une situation o\u00f9 deux threads se bloquent mutuellement en attendant une ressource que l'autre thread poss\u00e8de. C'est un probl\u00e8me classique de synchronisation.

    Typiquement si la copine (gf) attend que le frigo soit vide pour mettre une bi\u00e8re et que le buveur (drinker) attend que le frigo soit plein pour boire une bi\u00e8re. C'est un deadlock.

    C'est aussi le cas dans le dilemme des philosophes. Chaque philosophe a besoin de deux fourchettes pour manger. Si chaque philosophe prend une fourchette et attend que l'autre fourchette soit libre, il y a un deadlock\u2009: y'a cinq philosophes, une fouchette \u00e0 gauche et une fourchette \u00e0 droite, la table est ronde. Donc on a 5 fourchettes et 5 philosophes ce qui ne permet pas que tout le monde mange en m\u00eame temps.

    En th\u00e9orie de programmation concurrente ce probl\u00e8me des philosphophe peut \u00eatre d\u00e9crit comme ayant un cycle dans le graphe des ressources. Chaque philosophe est un noeud et chaque fourchette est une ar\u00eate. Si on a un cycle dans le graphe, il y a un deadlock.

    La starvation (famine) est une situation o\u00f9 un thread n'arrive pas \u00e0 acc\u00e9der \u00e0 une ressource partag\u00e9e \u00e0 cause de l'ordonnancement du syst\u00e8me. C'est un probl\u00e8me de synchronisation.

    Un exmple de famine serait si le buveur (drinker) boit toutes les bi\u00e8res et que la copine (gf) ne peut jamais mettre de bi\u00e8re dans le frigo. C'est une famine.

    "}, {"location": "course-concurrent/summary/#termes-de-programmation-concurrente", "title": "Termes de programmation concurrente", "text": "
    • Section Critique: Une portion de code qui acc\u00e8de \u00e0 des ressources partag\u00e9es, dans laquelle un seul thread doit s'ex\u00e9cuter \u00e0 la fois sinon il y a risque de corruption des donn\u00e9es.
    • Deadlock: Situation o\u00f9 deux threads se bloquent mutuellement en attendant une ressource que l'autre thread poss\u00e8de.
    • Starvation: Situation o\u00f9 un thread n'arrive pas \u00e0 acc\u00e9der \u00e0 une ressource partag\u00e9e \u00e0 cause de l'ordonnancement du syst\u00e8me
    • Condition de course: Situation o\u00f9 le r\u00e9sultat d'une op\u00e9ration d\u00e9pend de l'ordre d'ex\u00e9cution des threads.
    "}, {"location": "course-concurrent/summary/#memoire-cache", "title": "M\u00e9moire Cache", "text": "

    La m\u00e9moire cache est une m\u00e9moire rapide situ\u00e9e entre le processeur et la m\u00e9moire principale. Elle permet d'acc\u00e9l\u00e9rer l'acc\u00e8s aux donn\u00e9es en stockant les donn\u00e9es les plus fr\u00e9quemment utilis\u00e9es par le processeur.

    Les probl\u00e8mes typiques que l'on rencontre en programmation concurrente sont\u2009:

    • Cache Miss: Situation o\u00f9 une donn\u00e9e n'est pas pr\u00e9sente dans le cache et doit \u00eatre charg\u00e9e depuis la m\u00e9moire principale.
    • False Sharing: Situation o\u00f9 deux threads acc\u00e8dent \u00e0 des donn\u00e9es qui sont stock\u00e9es dans la m\u00eame ligne de cache, ce qui entra\u00eene des acc\u00e8s m\u00e9moire inutiles.
    • True Sharing: Situation o\u00f9 deux threads acc\u00e8dent \u00e0 des donn\u00e9es qui sont stock\u00e9es dans la m\u00eame ligne de cache, mais o\u00f9 les donn\u00e9es sont r\u00e9ellement partag\u00e9es entre les threads.

    Pour r\u00e9soudre ces probl\u00e8mes on va s'assurer que l'alignement m\u00e9moire que l'on utilise dans nos thread ne cr\u00e9e pas de situation de false sharing. On peut aussi utiliser des attributs de compilation pour forcer l'alignement m\u00e9moire.

    "}, {"location": "course-concurrent/summary/#programmation-asynchrone", "title": "Programmation Asynchrone", "text": "

    La programmation asynchrone est un style de programmation qui permet d'ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente sans bloquer le thread principal. Cela permet d'am\u00e9liorer les performances en \u00e9vitant les attentes inutiles.

    En C++ on peut utiliser les threads, les promesses et les t\u00e2ches pour r\u00e9aliser de la programmation asynchrone.

    • std::async : Ex\u00e9cute une fonction de mani\u00e8re asynchrone et renvoie un std::future qui permet de r\u00e9cup\u00e9rer le r\u00e9sultat de la fonction.
    • std::promise : Permet de communiquer entre deux threads en envoyant une valeur d'un thread \u00e0 un autre.
    • std::future : Permet de r\u00e9cup\u00e9rer le r\u00e9sultat d'une fonction asynchrone.
    "}, {"location": "course-concurrent/summary/#programmation-parallele", "title": "Programmation Parall\u00e8le", "text": "

    La programmation parall\u00e8le est un style de programmation qui permet d'ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente sur plusieurs processeurs ou c\u0153urs de processeur. Cela permet d'am\u00e9liorer les performances en r\u00e9partissant la charge de calcul sur plusieurs unit\u00e9s de calcul.

    En C++ on va souvent utiliser std::hardware_concurrency pour conna\u00eetre le nombre de coeurs disponibles sur la machine. Pourquoi on va cr\u00e9er uniquement le nombre de thread qui correspond au nombre de coeurs\u2009? Parce que si on cr\u00e9e plus de threads que de coeurs, on va cr\u00e9er des threads qui vont se partager le temps de calcul des coeurs. On va perdre beaucoup de temps dans les changements de contexte.

    En pratique il est rare d'utiliser directement les thread pour parall\u00e9liser des t\u00e2ches. On va plut\u00f4t utiliser des librairies comme OpenMP ou des frameworks comme Qt.

    Avec OpenMP on peut parall\u00e9liser des boucles for, des sections de code ou des t\u00e2ches. C'est tr\u00e8s simple \u00e0 utiliser et tr\u00e8s efficace.

    #include <iostream>\n#include <omp.h>\n\nint main() {\n    #pragma omp parallel for\n    for(int i = 0; i < 10; i++) {\n        std::cout << i << std::endl;\n    }\n}\n

    Alternativement, on peut confier l'ex\u00e9cution de certain probl\u00e8mes paralleles \u00e0 des GPU. Les GPU sont des unit\u00e9s de calcul massivement parall\u00e8les qui permettent d'acc\u00e9l\u00e9rer les calculs en parall\u00e9lisant les t\u00e2ches.

    "}, {"location": "course-concurrent/thread-pool/", "title": "Thread Pool", "text": "

    Un thread pool est un module logiciel qui permet de g\u00e9rer un ensemble de threads. Il permet de limiter le nombre de threads actifs et de r\u00e9utiliser les threads existants. Un thread pool est compos\u00e9 de deux \u00e9l\u00e9ments principaux\u2009:

    • une file d'attente de t\u00e2ches\u2009;
    • un ensemble de threads.

    Les t\u00e2ches sont ajout\u00e9es \u00e0 la file d'attente et les threads les r\u00e9cup\u00e8rent et les ex\u00e9cutent. Lorsqu'un thread a termin\u00e9 une t\u00e2che, il en r\u00e9cup\u00e8re une autre dans la file d'attente. Cela permet de r\u00e9duire le temps de cr\u00e9ation et de destruction des threads, et d'\u00e9viter les probl\u00e8mes de concurrence li\u00e9s \u00e0 la gestion des threads.

    "}, {"location": "course-concurrent/thread-pool/#cas-dutilisation", "title": "Cas d'utilisation", "text": "

    Les thread pools sont utilis\u00e9s dans de nombreux domaines, notamment dans les serveurs web, les serveurs d'applications, les bases de donn\u00e9es, les syst\u00e8mes d'exploitation, etc. Ils permettent de g\u00e9rer efficacement un grand nombre de connexions simultan\u00e9es, en limitant le nombre de threads actifs et en r\u00e9utilisant les threads existants.

    Par exemple le projet OpenCV utilise Intel TBB (Threading Building Blocks) pour g\u00e9rer les threads. TBB est une biblioth\u00e8que C++ qui fournit des primitives de parall\u00e9lisme de haut niveau, telles que les thread pools, les t\u00e2ches parall\u00e8les, les boucles parall\u00e8les, etc. Elle permet de tirer parti des processeurs multi-c\u0153urs et de r\u00e9duire le temps d'ex\u00e9cution des applications parall\u00e8les.

    Le logiciel Blender permettant la cr\u00e9ation de contenu 3D utilise \u00e9galement un thread pool pour g\u00e9rer les t\u00e2ches de rendu. Cela permet de r\u00e9partir les t\u00e2ches de rendu sur plusieurs threads et d'acc\u00e9l\u00e9rer le processus de rendu.

    "}, {"location": "course-concurrent/threads/", "title": "Threads", "text": ""}, {"location": "course-concurrent/threads/#introduction", "title": "Introduction", "text": "

    La gestion de l'ordonnancement des processus et des threads (processus l\u00e9gers) par le noyau d'un syst\u00e8me d'exploitation a \u00e9volu\u00e9 avec le temps, notamment sous les syst\u00e8mes UNIX et Linux. Historiquement, en effet, les threads \u00e9taient souvent g\u00e9r\u00e9s au niveau utilisateur par des biblioth\u00e8ques de threads, comme la biblioth\u00e8que POSIX Threads (pthreads) sous UNIX. Cette approche est connue sous le nom de threads au niveau utilisateur (User-Level Threads). Cependant, les syst\u00e8mes modernes, y compris Linux, utilisent une approche diff\u00e9rente qui permet une meilleure int\u00e9gration avec l'ordonnanceur du noyau.

    "}, {"location": "course-concurrent/threads/#threads-au-niveau-utilisateur", "title": "Threads au Niveau Utilisateur", "text": "

    Dans les mod\u00e8les de threads au niveau utilisateur, l'ordonnancement des threads est g\u00e9r\u00e9 enti\u00e8rement par la biblioth\u00e8que de threads dans l'espace utilisateur. Le noyau n'est pas conscient de la pr\u00e9sence de threads au sein des processus\u2009; il ne voit et n'ordonnance que des processus. Cette approche permet une grande flexibilit\u00e9 et une portabilit\u00e9 entre diff\u00e9rents syst\u00e8mes d'exploitation, mais pr\u00e9sente plusieurs inconv\u00e9nients\u2009:

    • Manque de Connaissance du Noyau : comme le noyau n'est pas conscient des threads, il ne peut pas prendre de d\u00e9cisions d'ordonnancement bas\u00e9es sur l'\u00e9tat de tous les threads dans le syst\u00e8me. Cela peut conduire \u00e0 une utilisation sous-optimale des ressources, notamment sur les syst\u00e8mes multiprocesseurs.
    • Blocage au Niveau du Processus : Si un thread effectue un appel syst\u00e8me bloquant, tout le processus (et donc tous ses threads) peut \u00eatre bloqu\u00e9, ce qui n'est pas id\u00e9al pour la concurrence.
    "}, {"location": "course-concurrent/threads/#threads-au-niveau-noyau", "title": "Threads au Niveau Noyau", "text": "

    Pour surmonter ces limitations, la plupart des syst\u00e8mes d'exploitation modernes, y compris Linux, g\u00e8rent d\u00e9sormais les threads au niveau du noyau (Kernel-Level Threads). Dans ce mod\u00e8le, le noyau est pleinement conscient de chaque thread et peut les ordonnancer ind\u00e9pendamment. Cela permet une meilleure utilisation des ressources sur les syst\u00e8mes multic\u0153urs, car le noyau peut r\u00e9partir les threads sur diff\u00e9rents processeurs.

    • Meilleure Concurrency : Chaque thread peut \u00eatre ordonnanc\u00e9 ind\u00e9pendamment, permettant \u00e0 un processus de continuer \u00e0 s'ex\u00e9cuter m\u00eame si l'un de ses threads est bloqu\u00e9.
    • Gestion des Priorit\u00e9s : Le noyau peut ajuster les priorit\u00e9s des threads individuellement, offrant une granularit\u00e9 fine dans la gestion de l'ordonnancement.
    • Support Multiprocesseur : Le noyau peut r\u00e9partir les threads d'un m\u00eame processus sur plusieurs c\u0153urs, exploitant pleinement le mat\u00e9riel multiprocesseur.
    "}, {"location": "course-concurrent/threads/#modele-nm", "title": "Mod\u00e8le N:M", "text": "

    Il existe \u00e9galement un mod\u00e8le hybride, le mod\u00e8le N:M, qui tente de combiner les avantages des threads au niveau utilisateur et au niveau noyau. Dans ce mod\u00e8le, N threads au niveau utilisateur sont mapp\u00e9s sur M threads au niveau noyau. Cela permet une certaine flexibilit\u00e9 dans la gestion des threads et peut am\u00e9liorer la performance en r\u00e9duisant le nombre de changements de contexte n\u00e9cessaires. Cependant, ce mod\u00e8le est complexe \u00e0 impl\u00e9menter et n'est pas largement utilis\u00e9 dans les syst\u00e8mes d'exploitation modernes.

    "}, {"location": "course-concurrent/threads/#conclusion", "title": "Conclusion", "text": "

    L'approche moderne de la gestion de l'ordonnancement des processus et des threads par le noyau offre une flexibilit\u00e9, une efficacit\u00e9 et une scalabilit\u00e9 significativement meilleures par rapport aux m\u00e9thodes historiques. Les threads au niveau noyau permettent une utilisation optimale du mat\u00e9riel multiprocesseur et multic\u0153ur, tout en offrant une gestion fine de la concurrence et de la parall\u00e9lisation au sein des applications.

    C++20 lui-m\u00eame n'introduit pas de nouvelles fonctionnalit\u00e9s sp\u00e9cifiquement con\u00e7ues pour les threads au niveau utilisateur, mais il continue de supporter le multithreading \u00e0 travers sa biblioth\u00e8que standard, notamment avec <thread>, <mutex>, <future>, <atomic>, et d'autres facilit\u00e9s de synchronisation et de communication entre threads.

    Les threads g\u00e9r\u00e9s par la biblioth\u00e8que standard C++ sont des threads au niveau syst\u00e8me (ou noyau), ce qui signifie que leur cr\u00e9ation, destruction, et ordonnancement sont g\u00e9r\u00e9s par le syst\u00e8me d'exploitation. Cela offre des avantages en termes de performances et d'utilisation sur des syst\u00e8mes multic\u0153urs, mais peut ne pas \u00eatre id\u00e9al pour tous les cas d'utilisation, particuli\u00e8rement ceux n\u00e9cessitant un grand nombre de threads avec des changements de contexte fr\u00e9quents.

    "}, {"location": "course-cpp/cpp/", "title": "C++", "text": "C vous permet de vous tirer une balle dans le pied facilement ; C++ rend cela plus difficile, mais quand vous y arrivez, \u00e7a vous arrache toute la jambe.Bjarne Stroustrup"}, {"location": "course-cpp/cpp/#introduction", "title": "Introduction", "text": "

    Dans le vaste univers des langages de programmation, il est des \u00e9toiles qui brillent d\u2019un \u00e9clat singulier, non par leur simplicit\u00e9, mais par la puissance et la profondeur qu\u2019elles conf\u00e8rent \u00e0 ceux qui osent s\u2019y aventurer. C++ est sans doute l\u2019une de ces constellations. Langage aux multiples facettes, oscillant entre l\u2019h\u00e9ritage brut de C et les abstractions sophistiqu\u00e9es de la programmation orient\u00e9e objet, C++ trouve son origine dans les ann\u00e9es 1980, sous la plume inspir\u00e9e de Bjarne Stroustrup, un informaticien danois dont l\u2019ambition \u00e9tait aussi pragmatique que visionnaire.

    Dans les ann\u00e9es 1970 et 1980, les langages de programmation n\u2019\u00e9taient pas encore aussi vari\u00e9s et sp\u00e9cialis\u00e9s qu\u2019aujourd\u2019hui. C, un langage imp\u00e9ratif et structur\u00e9, dominait le paysage, notamment dans les domaines des syst\u00e8mes d\u2019exploitation et du d\u00e9veloppement de logiciels \u00e0 hautes performances. Toutefois, malgr\u00e9 sa rapidit\u00e9 et son efficacit\u00e9, C poss\u00e9dait des limitations flagrantes, en particulier dans la gestion des concepts abstraits et complexes propres aux grands syst\u00e8mes logiciels. Stroustrup, alors chercheur aux laboratoires Bell, se trouvait confront\u00e9 \u00e0 une question existentielle\u2009: comment allier la performance brute d\u2019un langage bas niveau, capable de manipuler le mat\u00e9riel avec une pr\u00e9cision chirurgicale, \u00e0 l\u2019\u00e9l\u00e9gance et \u00e0 la modularit\u00e9 d\u2019un langage orient\u00e9 objet, tel que Simula, qui \u00e9tait d\u00e9j\u00e0 pris\u00e9 pour ses capacit\u00e9s \u00e0 mod\u00e9liser des syst\u00e8mes complexes\u2009?

    L\u2019id\u00e9e de C with Classes, qui deviendra par la suite C++, \u00e9tait de conserver la robustesse de C tout en y introduisant les concepts de la programmation orient\u00e9e objet (POO), un paradigme qui, \u00e0 l\u2019\u00e9poque, commen\u00e7ait \u00e0 d\u00e9montrer son efficacit\u00e9 pour organiser le code, le rendre plus r\u00e9utilisable, et g\u00e9rer la complexit\u00e9 croissante des syst\u00e8mes logiciels modernes.

    Bjarne Stroupstrup

    Le paradigme objet qui a fait sa grande entr\u00e9e avec Simula en 1967, puis Smalltalk en 1972, \u00e9tait une r\u00e9volution dans la mani\u00e8re de concevoir les programmes. Il permettait de regrouper les donn\u00e9es et les fonctions qui les manipulent dans des entit\u00e9s coh\u00e9rentes appel\u00e9es objets, et de d\u00e9finir des relations entre ces objets, en termes d\u2019h\u00e9ritage, de polymorphisme, et d\u2019encapsulation. C++ allait donc s\u2019inscrire dans cette lign\u00e9e, en apportant sa propre vision de la POO, plus proche de C que de Simula, mais tout aussi puissante.

    "}, {"location": "course-cpp/cpp/#equilibre-entre-pouvoir-et-simplicite", "title": "\u00c9quilibre entre pouvoir et simplicit\u00e9", "text": "

    Le dessein de Stroustrup n\u2019\u00e9tait pas de cr\u00e9er un langage compl\u00e8tement nouveau, mais plut\u00f4t d\u2019\u00e9tendre un outil d\u00e9j\u00e0 existant, C, pour en faire un instrument plus flexible, plus puissant, capable de s\u2019adapter \u00e0 des applications vari\u00e9es, allant des syst\u00e8mes d\u2019exploitation aux simulations scientifiques. Cependant, il \u00e9tait clair d\u00e8s le d\u00e9but que cet \u00e9quilibre ne serait pas facile \u00e0 atteindre. \u00c0 la mani\u00e8re d\u2019un architecte concevant une cath\u00e9drale o\u00f9 chaque vo\u00fbte porte la promesse d\u2019un effondrement potentiel, Stroustrup devait naviguer entre les contraintes de performances de bas niveau et la promesse d\u2019une abstraction \u00e9lev\u00e9e.

    C++ introduisit donc des classes, l\u2019h\u00e9ritage, l\u2019encapsulation, et bient\u00f4t, des concepts comme les mod\u00e8les (templates), qui allaient permettre d\u2019abstraire le code tout en conservant une efficacit\u00e9 optimale. Toutefois, cette flexibilit\u00e9 et cette ambition eurent aussi leurs revers.

    Toute naissance est imparfaite. Et C++, bien que prometteur, n\u2019\u00e9chappa pas aux vicissitudes de l\u2019\u00e9volution. Parmi les premiers d\u00e9fis que les d\u00e9veloppeurs rencontr\u00e8rent en adoptant ce langage se trouvait ce qui allait \u00eatre nomm\u00e9 avec un certain humour noir, le \u201cMost Vexing Parse\u201d \u2014 une des nombreuses subtilit\u00e9s de la syntaxe de C++ qui pouvait amener m\u00eame les plus exp\u00e9riment\u00e9s \u00e0 se gratter la t\u00eate. En raison de l\u2019ambigu\u00eft\u00e9 entre la d\u00e9claration de variables et la d\u00e9finition de fonctions, certaines constructions valides en apparence pouvaient se r\u00e9v\u00e9ler incompr\u00e9hensibles ou sources d\u2019erreurs.

    De m\u00eame, la gestion de la m\u00e9moire, h\u00e9rit\u00e9e de C, restait un terrain min\u00e9. Alors que C++ proposait des m\u00e9canismes comme les destructeurs pour automatiser la lib\u00e9ration des ressources, l\u2019absence de garbage collector (collecteur de d\u00e9chets) natif, \u00e0 l\u2019instar de Java ou C#, signifiait que l\u2019erreur humaine demeurait un facteur crucial dans la gestion des ressources\u2009: n'avez-vous jamais oubli\u00e9 un free apr\u00e8s un malloc en C\u2009?

    Ces \u00ab\u2009erreurs de jeunesse\u2009\u00bb ne faisaient pas qu'alourdir la t\u00e2che du programmeur\u2009; elles rappelaient aussi que C++ n'\u00e9tait pas un langage de novices. Il exigeait discipline, rigueur, et une compr\u00e9hension intime de ses m\u00e9canismes internes. Il y avait une beaut\u00e9 dans cette complexit\u00e9, un plaisir intellectuel pour ceux qui ma\u00eetrisaient ses arcanes.

    Malgr\u00e9 ses d\u00e9buts tumultueux, C++ gagna rapidement en popularit\u00e9. Sa capacit\u00e9 \u00e0 \u00eatre \u00e0 la fois bas niveau et abstrait en faisait l\u2019outil privil\u00e9gi\u00e9 pour de nombreux domaines, des moteurs de jeux vid\u00e9o aux simulations financi\u00e8res. Mais, comme tout langage en \u00e9volution rapide, il n\u00e9cessitait un cadre rigoureux pour \u00e9viter le chaos. C\u2019est ainsi qu\u2019en 1998, l'ISO standardisa officiellement C++, posant les bases de ce qui deviendrait une longue lign\u00e9e de versions standardis\u00e9es.

    Voici les principales \u00e9tapes de cette \u00e9volution\u2009:

    C++98 (ISO/IEC 14882:1998)

    La premi\u00e8re norme officielle de C++, qui formalise les ajouts du langage, notamment les templates et les exceptions.

    C++03 (ISO/IEC 14882:2003)

    Une r\u00e9vision mineure visant \u00e0 corriger des erreurs et clarifier certaines ambigu\u00eft\u00e9s dans C++98.

    C++11 (ISO/IEC 14882:2011)

    Une r\u00e9volution pour le langage, avec l\u2019ajout des expressions lambda, des smart pointers, des threads natifs, et de nombreuses am\u00e9liorations en termes de performance et de simplicit\u00e9 d\u2019utilisation.

    C++14 (ISO/IEC 14882:2014)

    Une r\u00e9vision de C++11, qui affine certains concepts et introduit quelques nouvelles fonctionnalit\u00e9s mineures.

    C++17 (ISO/IEC 14882:2017)

    Un standard qui introduit des fonctionnalit\u00e9s plus avanc\u00e9es comme std::optional, std::variant, et des am\u00e9liorations pour le travail avec les cha\u00eenes de caract\u00e8res.

    C++20 (ISO/IEC 14882:2020)

    Une mise \u00e0 jour majeure, avec des concepts, des coroutines, et des ranges, renfor\u00e7ant encore davantage les capacit\u00e9s de programmation g\u00e9n\u00e9rique et moderne de C++.

    C++23

    Dernier en date au moment de cet \u00e9crit, qui continue d\u2019affiner et de simplifier l\u2019exp\u00e9rience des d\u00e9veloppeurs, en int\u00e9grant des fonctionnalit\u00e9s comme des modules et des am\u00e9liorations pour la concurrence.

    \u00c0 travers chaque standardisation, C++ a su \u00e9voluer sans jamais renier ses principes fondateurs\u2009: performance, flexibilit\u00e9 et un contr\u00f4le pr\u00e9cis des ressources.

    "}, {"location": "course-cpp/cpp/#un-heritage-a-double-tranchant", "title": "Un h\u00e9ritage \u00e0 double tranchant", "text": "

    En conclusion, C++ n\u2019est pas un langage pour ceux qui cherchent la facilit\u00e9. Il est le produit d\u2019une vision grandiose, o\u00f9 chaque ligne de code peut atteindre des sommets de performance, mais o\u00f9 chaque erreur peut avoir des cons\u00e9quences drastiques. Ses subtilit\u00e9s syntaxiques, ses m\u00e9canismes de m\u00e9moire et sa philosophie de contr\u00f4le total sont autant de d\u00e9fis que de promesses pour le programmeur aguerri.

    Mais c\u2019est aussi un langage vivant, \u00e9voluant constamment avec ses utilisateurs, capable d'adapter ses structures aux paradigmes modernes tout en conservant sa puissance brute. C++, en somme, est \u00e0 l\u2019image de son cr\u00e9ateur, Bjarne Stroustrup\u2009: une \u0153uvre \u00e0 la fois rationnelle et passionn\u00e9e, o\u00f9 le code devient plus qu\u2019un outil \u2014 il devient un art.

    "}, {"location": "course-cpp/garbage-collection/", "title": "Collecteur de d\u00e9chets (garbage collector)", "text": "

    Le C est un langage primitif qui ne g\u00e8re pas automatiquement la lib\u00e9ration des ressources allou\u00e9es dynamiquement. L'exemple suivant est \u00e9vocateur\u2009:

    int* get_number() {\n    int *num = malloc(sizeof(int));\n    *num = rand();\n}\n\nint main() {\n    for (int i = 0; i < 100; i++) {\n        printf(\"%d\\n\", *get_number());\n    }\n}\n

    La fonction get_number alloue dynamiquement un espace de la taille d'un entier et lui assigne une valeur al\u00e9atoire. Dans le programme principal, l'adresse retourn\u00e9e est d\u00e9r\u00e9f\u00e9renc\u00e9e pour \u00eatre affich\u00e9e sur la sortie standard.

    A la fin de l'ex\u00e9cution de la boucle for, une centaine d'espaces m\u00e9moire sont maintenant dans les limbes. Comme le pointeur retourn\u00e9 n'a jamais \u00e9t\u00e9 m\u00e9moris\u00e9, il n'est plus possible de lib\u00e9rer cet espace m\u00e9moire avec free.

    On dit que le programme \u00e0 une fuite m\u00e9moire. En admettant que ce programme reste r\u00e9sidant en m\u00e9moire, il peut arriver un moment o\u00f9 le programme peut aller jusqu'\u00e0 utiliser toute la RAM disponible. Dans ce cas, il est probable que malloc retourne NULL et qu'une erreur de segmentation apparaisse lors du printf.

    Allons plus loin dans notre exemple et consid\u00e9rons le code suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint foo(int *new_value) {\n    static int *values[10] = { NULL };\n    static int count = 0;\n\n    if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {\n        values[count++] = new_value;\n    }\n\n    if (count > 0)\n        printf(\"Foo aime %d\\n\", *values[rand() % count]);\n}\n\nint bar(int *new_value) {\n    static int *values[10] = { NULL };\n    static int count = 0;\n\n    if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {\n        values[count++] = new_value;\n    }\n\n    if (count > 0)\n        printf(\"Bar aime %d\\n\", *values[rand() % count]);\n}\n\nint* get_number() {\n    int *number = malloc(sizeof(int));\n    *number = rand() % 1000;\n    return number;\n}\n\nint main() {\n    int experiment_iterations = 10;\n    for (int i = 0; i < experiment_iterations; i++) {\n        int *num = get_number();\n        foo(num);\n        bar(num);\n        #if 0 // ...\n            free(num) ??\n        #endif\n    };\n}\n

    La fonction get_number alloue dynamiquement un espace m\u00e9moire et assigne un nombre al\u00e9atoire. Les fonctions foo et bar re\u00e7oivent en param\u00e8tre un pointeur sur un entier. Chacune \u00e0 le choix de m\u00e9moriser ce pointeur et de clamer sur stdout qu'elle aime un des nombres m\u00e9moris\u00e9s.

    Au niveau du #if 0 dans la fonction main, il est impossible de savoir si l'adresse point\u00e9e par num est encore utilis\u00e9e ou non. Il se peut que foo et bar utilisent cet espace m\u00e9moire, comme il se peut qu'aucun des deux ne l'utilise.

    Comment peut-on savoir si il est possible de lib\u00e9rer ou non num ?

    Une solution couramment utilis\u00e9e 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\u00e9f\u00e9rences utilis\u00e9es. De cette mani\u00e8re il est possible en tout temps de savoir si le pointeur est r\u00e9f\u00e9renc\u00e9 quelque part. Dans le cas o\u00f9 le nombre de r\u00e9f\u00e9rence tombe \u00e0 z\u00e9ro, il est possible de lib\u00e9rer la ressource.

    Dans un certain nombre de langage de programmation comme Python ou Java, il existe un m\u00e9canisme automatique nomm\u00e9 Garbage Collector et qui, p\u00e9riodiquement, fait un tour de toutes les allocations dynamique pour savoir si elle sont encore r\u00e9f\u00e9renc\u00e9es ou non. Le cas \u00e9ch\u00e9ant, le gc d\u00e9cide lib\u00e9rer la ressource m\u00e9moire. De cette mani\u00e8re il n'est plus n\u00e9cessaire de faire la chasse aux ressources allou\u00e9es.

    En revanche, en C, il n'existe aucun m\u00e9canisme aussi sophistiqu\u00e9 alors prenez garde \u00e0 bien lib\u00e9rer les ressources utilis\u00e9es et \u00e0 \u00e9viter d'\u00e9crire des fonctions qui allouent du contenu m\u00e9moire dynamiquement.

    ", "tags": ["num", "get_number", "malloc", "foo", "bar", "main", "free", "NULL", "printf", "stdout"]}, {"location": "course-cpp/object-oriented/", "title": "Orient\u00e9 objet", "text": ""}, {"location": "course-cpp/object-oriented/#un-voyage-vers-labstraction-structuree", "title": "Un voyage vers l'abstraction structur\u00e9e", "text": "

    \u00c0 l'aube de la programmation informatique, le code n'\u00e9tait qu'une suite d'instructions lin\u00e9aires, un simple flux s\u00e9quentiel qui, dans les meilleurs des cas, reproduisait des sch\u00e9mas logiques simples. Mais au fur et \u00e0 mesure que les syst\u00e8mes devenaient plus complexes, l'organisation de ces instructions demandait plus de structure, plus de modularit\u00e9. C'est l\u00e0 que les paradigmes de programmation commenc\u00e8rent \u00e0 \u00e9merger.

    "}, {"location": "course-cpp/object-oriented/#une-maniere-de-penser-la-programmation", "title": "Une mani\u00e8re de penser la programmation", "text": "

    Un paradigme en informatique peut \u00eatre d\u00e9fini comme un mod\u00e8le ou un style de programmation. Il impose une mani\u00e8re sp\u00e9cifique de penser et d\u2019organiser le code, de r\u00e9soudre des probl\u00e8mes en suivant des r\u00e8gles bien d\u00e9finies. Le paradigme imp\u00e9ratif, par exemple, ordonne les instructions, pas \u00e0 pas, tandis que le paradigme fonctionnel privil\u00e9gie les expressions math\u00e9matiques pures. Chaque paradigme propose un point de vue diff\u00e9rent, un cadre conceptuel particulier pour aborder la r\u00e9solution des probl\u00e8mes.

    Parmi ces paradigmes, un se distingue par sa capacit\u00e9 \u00e0 mod\u00e9liser des syst\u00e8mes complexes en s\u2019inspirant du monde r\u00e9el\u2009: le paradigme orient\u00e9 objet.

    Le paradigme orient\u00e9 objet (OO) propose une approche o\u00f9 le programme est con\u00e7u comme un ensemble d\u2019entit\u00e9s autonomes appel\u00e9es objets. Ces objets interagissent entre eux, chacun repr\u00e9sentant une part sp\u00e9cifique du monde ou du probl\u00e8me \u00e0 mod\u00e9liser. L'id\u00e9e sous-jacente est de rendre le code plus modulaire, r\u00e9utilisable, et compr\u00e9hensible en refl\u00e9tant la structure des entit\u00e9s du monde r\u00e9el ou de l'application simul\u00e9e.

    L'OO permet de repr\u00e9senter des concepts abstraits tout en conservant une organisation claire et hi\u00e9rarchis\u00e9e des donn\u00e9es et des comportements. Cette approche est apparue en r\u00e9ponse \u00e0 la complexit\u00e9 croissante des syst\u00e8mes logiciels et des besoins d'une meilleure gestion de cette complexit\u00e9.

    Historiquement, l\u2019OO trouve ses racines dans des langages pionniers comme Simula (ann\u00e9es 1960), qui introduit les concepts d\u2019objets et de classes, et Smalltalk (ann\u00e9es 1970), qui formalise cette approche. Ces langages marqu\u00e8rent un tournant dans la fa\u00e7on de concevoir les programmes. Par la suite, des langages comme Java, Python, et C#, ainsi que C++, port\u00e8rent ce paradigme \u00e0 des niveaux plus \u00e9lev\u00e9s de popularit\u00e9 et de sophistication.

    L'OO repose sur quelques concepts cl\u00e9s qui constituent son fondement et qui permettent d\u2019organiser le code de mani\u00e8re robuste, modulaire et extensible\u2009:

    Objet

    Un objet est une entit\u00e9 ind\u00e9pendante qui poss\u00e8de un \u00e9tat (sous forme de donn\u00e9es ou attributs) et des comportements (sous forme de m\u00e9thodes ou fonctions). Il est l\u2019\u00e9l\u00e9ment fondamental du paradigme objet, l\u2019unit\u00e9 de base avec laquelle on construit tout le syst\u00e8me.

    Classe

    Une classe est un mod\u00e8le, ou un plan, qui d\u00e9finit les attributs et les comportements des objets. Elle permet de cr\u00e9er plusieurs objets similaires partageant la m\u00eame structure et les m\u00eames fonctionnalit\u00e9s. Les objets sont ainsi des instances d\u2019une classe.

    Encapsulation

    Ce principe consiste \u00e0 cacher les d\u00e9tails internes d'un objet et \u00e0 n'exposer que ce qui est n\u00e9cessaire pour son utilisation. Cela se traduit par la s\u00e9paration des donn\u00e9es priv\u00e9es, que l\u2019on prot\u00e8ge, et des m\u00e9thodes publiques qui permettent d\u2019interagir avec l\u2019objet. L\u2019encapsulation r\u00e9duit les interf\u00e9rences non souhait\u00e9es avec l\u2019\u00e9tat interne de l\u2019objet et am\u00e9liore la robustesse du code.

    Abstraction

    L\u2019abstraction consiste \u00e0 repr\u00e9senter uniquement les aspects essentiels d\u2019un objet tout en ignorant les d\u00e9tails superflus. C\u2019est une forme de simplification du code, permettant de manipuler les objets \u00e0 un niveau \u00e9lev\u00e9 sans se soucier de leur impl\u00e9mentation d\u00e9taill\u00e9e.

    H\u00e9ritage

    L\u2019h\u00e9ritage permet de cr\u00e9er de nouvelles classes \u00e0 partir de classes existantes, en r\u00e9utilisant et en sp\u00e9cialisant leurs propri\u00e9t\u00e9s et m\u00e9thodes. Une classe d\u00e9riv\u00e9e (ou sous-classe) h\u00e9rite ainsi des attributs et comportements d\u2019une classe parente tout en pouvant les \u00e9tendre ou les modifier. Cela favorise la r\u00e9utilisation du code et la cr\u00e9ation de hi\u00e9rarchies d'objets.

    Polymorphisme

    Le polymorphisme permet \u00e0 des objets de diff\u00e9rentes classes de r\u00e9pondre \u00e0 un m\u00eame message ou appel de m\u00e9thode de mani\u00e8res distinctes. Il peut \u00eatre de deux types\u2009: polymorphisme statique (ou surcharge) comme plusieurs m\u00e9thodes portant le m\u00eame nom mais avec des signatures diff\u00e9rentes ou le polymorphisme dynamique (ou substitutivit\u00e9) lorsqu'une m\u00e9thode peut \u00eatre red\u00e9finie dans une sous-classe pour se comporter diff\u00e9remment tout en conservant le m\u00eame nom.

    Ces concepts, bien que simples en apparence, offrent une immense flexibilit\u00e9 pour mod\u00e9liser des syst\u00e8mes logiciels complexes tout en assurant une gestion claire de la structure et du comportement des objets.

    "}, {"location": "course-cpp/object-oriented/#les-limites-dune-approche-manuelle", "title": "Les limites d\u2019une approche manuelle", "text": "

    Avant l'av\u00e8nement des langages orient\u00e9s objet, le langage C, bien qu\u2019efficace pour la programmation syst\u00e8me, ne poss\u00e9dait pas ces abstractions de haut niveau. Cependant, certains programmeurs cr\u00e9atifs r\u00e9ussirent \u00e0 simuler certains concepts de l\u2019OO dans C, bien que d\u2019une mani\u00e8re moins naturelle et plus complexe.

    Prenons l'exemple des structures en C. Une structure (ou struct) est un regroupement de donn\u00e9es sous un m\u00eame type. On peut y voir une \u00e9bauche d\u2019objet\u2009: elle permet d\u2019associer plusieurs donn\u00e9es li\u00e9es sous un seul type composite. Par exemple\u2009:

    struct Point {\n    int x;\n    int y;\n};\n

    Pour ajouter du comportement \u00e0 cette structure, des pointeurs de fonctions peuvent \u00eatre utilis\u00e9s afin d'associer des fonctions sp\u00e9cifiques \u00e0 des structures. Voici comment un embryon de m\u00e9thode pourrait \u00eatre impl\u00e9ment\u00e9 en C\u2009:

    struct Point {\n    int x;\n    int y;\n    void (*move)(struct Point* self, int dx, int dy);\n};\n\nvoid move_point(struct Point* self, int dx, int dy) {\n    self->x += dx;\n    self->y += dy;\n}\n\nint main() {\n    struct Point p = {0, 0, move_point};\n    p.move(&p, 5, 10); // D\u00e9place le point\n}\n

    Ici, on simule une m\u00e9thode move \u00e0 l\u2019aide d\u2019un pointeur de fonction, ce qui permet \u00e0 un objet (la structure Point) d\u2019avoir un comportement, \u00e0 la mani\u00e8re d\u2019un objet dans un langage OO. Toutefois, cette approche pr\u00e9sente des limites consid\u00e9rables\u2009:

    Manque de s\u00e9curit\u00e9

    Rien ne garantit que les fonctions seront correctement associ\u00e9es aux structures. Le typage est plus l\u00e2che, et le programmeur doit manuellement g\u00e9rer ces associations, augmentant le risque d'erreurs.

    Absence d\u2019h\u00e9ritage et de polymorphisme

    En C, il n\u2019y a pas de moyen natif de cr\u00e9er des hi\u00e9rarchies de types ou de surcharger des fonctions. Toute tentative d'imiter cela implique des bricolages fastidieux.

    Ainsi, bien que C permette une certaine approximation de l'OO, il ne fournit pas les abstractions n\u00e9cessaires pour exploiter pleinement ce paradigme.

    ", "tags": ["Point", "move", "struct"]}, {"location": "course-cpp/object-oriented/#les-classes-une-abstraction-naturelle", "title": "Les classes, une abstraction naturelle", "text": "

    Avec l\u2019introduction de C++, ces approximations manuelles deviennent superflues. Le langage fournit directement des classes, qui englobent non seulement des donn\u00e9es, mais aussi les m\u00e9thodes qui leur sont associ\u00e9es. Une classe en C++ remplace la structure et le pointeur de fonction, tout en introduisant l\u2019encapsulation, l\u2019h\u00e9ritage, et le polymorphisme.

    Voici un exemple \u00e9quivalent en C++ :

    struct Point {\n    int x, y;\n\n    Point(int x = 0, int y = 0) {\n        this->x = x;\n        this->y = y;\n    }\n\n    void move(int dx, int dy) {\n        x += dx;\n        y += dy;\n    }\n};\n\nint main() {\n    Point p(0, 0);\n    p.move(5, 10);  // D\u00e9place le point\n}\n

    Dans cet exemple, la classe Point encapsule les donn\u00e9es (x, y) et les comportements (move) en une seule entit\u00e9 coh\u00e9rente. L'utilisation des constructeurs permet d'initialiser directement l'objet, et la gestion du comportement devient plus intuitive et s\u00e9curis\u00e9e.

    En r\u00e9sum\u00e9, le passage de C \u00e0 C++ repr\u00e9sente un saut qualitatif immense dans la gestion des abstractions. Tandis que C permettait de bricoler des objets avec des structures et des pointeurs de fonction, C++ offre un cadre natif pour la programmation orient\u00e9e objet, avec toutes les garanties et la souplesse que cela implique. Ce qui \u00e9tait laborieux et sujet \u00e0 erreurs dans C devient naturel, \u00e9l\u00e9gant et puissant dans C++.

    ", "tags": ["move", "Point"]}, {"location": "course-cpp/object-oriented/#le-vocabulaire-oriente-objet", "title": "Le vocabulaire orient\u00e9 objet", "text": "

    La programmation orient\u00e9e objet (OO) repose sur un ensemble de concepts cl\u00e9s qui forment un langage commun, essentiel pour comprendre et manipuler les syst\u00e8mes logiciels modernes. Ce vocabulaire constitue la pierre angulaire de la conception OO et permet d'aborder la complexit\u00e9 de mani\u00e8re structur\u00e9e et modulaire. Voici un aper\u00e7u des termes les plus importants de ce paradigme.

    "}, {"location": "course-cpp/object-oriented/#classe", "title": "Classe", "text": "

    Une classe est un mod\u00e8le ou un plan qui d\u00e9finit la structure et le comportement des objets. Elle d\u00e9crit les attributs (ou propri\u00e9t\u00e9s) et les m\u00e9thodes (ou fonctions) que ses instances, appel\u00e9es objets, poss\u00e9deront. Voici un exemple de classe en C++:

    class Animal {\npublic:\n    string nom;\n    int age;\n    void eat() { cout << \"L'animal mange\" << endl; }\n};\n

    Ici, la classe Animal d\u00e9finit deux attributs, nom et age, ainsi qu'une m\u00e9thode eat.

    ", "tags": ["Animal", "eat", "age", "nom"]}, {"location": "course-cpp/object-oriented/#instance", "title": "Instance", "text": "

    Une instance est cr\u00e9\u00e9 \u00e0 partir d'une classe. Chaque instance poss\u00e8de ses propres valeurs pour les attributs de la classe et peut ex\u00e9cuter les m\u00e9thodes d\u00e9finies par celle-ci. Si la classe est le plan d'une maison, une instance est une maison r\u00e9elle construite selon ce plan.

    "}, {"location": "course-cpp/object-oriented/#objet", "title": "Objet", "text": "

    Un objet est une instance d'une classe. Il repr\u00e9sente une entit\u00e9 r\u00e9elle ou abstraite avec laquelle le programme peut interagir. Chaque objet poss\u00e8de son propre \u00e9tat (valeurs des attributs) et peut ex\u00e9cuter les comportements d\u00e9finis par sa classe.

    Animal chat;\nchat.nom = \"Mimi\";\nchat.eat();\n

    Dans cet exemple, chat est un objet de la classe Animal qui poss\u00e8de ses propres valeurs pour nom et age.

    ", "tags": ["Animal", "age", "nom", "chat"]}, {"location": "course-cpp/object-oriented/#attribut", "title": "Attribut", "text": "

    Les attributs (ou champs, propri\u00e9t\u00e9s) sont les variables d\u00e9finies dans une classe qui repr\u00e9sentent l'\u00e9tat ou les caract\u00e9ristiques de l'objet. Chaque objet a ses propres copies de ces attributs.

    int age;   // attribut qui stocke l'\u00e2ge d'un objet de la classe Animal\n
    "}, {"location": "course-cpp/object-oriented/#methode", "title": "M\u00e9thode", "text": "

    Une m\u00e9thode est une fonction d\u00e9finie \u00e0 l'int\u00e9rieur d'une classe, qui repr\u00e9sente un comportement ou une action que les objets de cette classe peuvent accomplir. Les m\u00e9thodes peuvent manipuler les attributs de l'objet ou interagir avec d'autres objets.

    void manger() {\n    cout << \"L'animal mange\" << endl;\n}\n
    "}, {"location": "course-cpp/object-oriented/#encapsulation", "title": "Encapsulation", "text": "

    L'encapsulation consiste \u00e0 restreindre l'acc\u00e8s direct aux attributs d'un objet et \u00e0 contr\u00f4ler cet acc\u00e8s via des m\u00e9thodes. Cela permet de prot\u00e9ger l'\u00e9tat interne de l'objet et de d\u00e9finir clairement les points d'interaction avec celui-ci. En C++, cela se traduit par l'utilisation de modificateurs d'acc\u00e8s comme public, private, et protected. Exemple\u2009:

    class Animal {\nprivate:\n    string nom;  // Attribut priv\u00e9\n\npublic:\n    void setNom(string n) { nom = n; }  // M\u00e9thode publique pour modifier l'attribut\n    string getNom() { return nom; }     // M\u00e9thode publique pour acc\u00e9der \u00e0 l'attribut\n};\n

    Dans cet exemple, l'attribut nom est priv\u00e9, et ne peut \u00eatre directement modifi\u00e9 ou lu que via les m\u00e9thodes publiques setNom et getNom.

    ", "tags": ["setNom", "protected", "nom", "public", "getNom", "private"]}, {"location": "course-cpp/object-oriented/#abstraction", "title": "Abstraction", "text": "

    L'abstraction est le concept de simplification en se concentrant sur les caract\u00e9ristiques essentielles d'un objet tout en cachant les d\u00e9tails non pertinents. Cela permet de manipuler des objets \u00e0 un niveau conceptuel, sans se soucier de leur impl\u00e9mentation interne. En C++, les classes abstraites et les interfaces sont utilis\u00e9es pour fournir des mod\u00e8les conceptuels sans impl\u00e9mentation concr\u00e8te imm\u00e9diate.

    class Forme {\npublic:\n    virtual double aire() = 0;  // M\u00e9thode virtuelle pure, d\u00e9finissant un comportement sans impl\u00e9mentation\n};\n

    Cette classe Forme repr\u00e9sente une abstraction de toute forme g\u00e9om\u00e9trique sans se pr\u00e9occuper de sa nature exacte. Les sous-classes devront fournir leur propre impl\u00e9mentation de la m\u00e9thode aire.

    ", "tags": ["Forme", "aire"]}, {"location": "course-cpp/object-oriented/#heritage", "title": "H\u00e9ritage", "text": "

    L'h\u00e9ritage est un m\u00e9canisme qui permet \u00e0 une classe de d\u00e9river d'une autre classe. La classe d\u00e9riv\u00e9e h\u00e9rite des attributs et m\u00e9thodes de la classe parent, tout en pouvant ajouter ou red\u00e9finir ses propres fonctionnalit\u00e9s. Cela favorise la r\u00e9utilisation du code et permet de cr\u00e9er des hi\u00e9rarchies de classes.

    class Chien : public Animal {  // Chien h\u00e9rite d'Animal\npublic:\n    void aboyer() {\n        cout << \"Le chien aboie\" << endl;\n    }\n};\n

    Ici, Chien h\u00e9rite des attributs et m\u00e9thodes de Animal, tout en ajoutant son propre comportement aboyer.

    ", "tags": ["Animal", "aboyer", "Chien"]}, {"location": "course-cpp/object-oriented/#polymorphisme", "title": "Polymorphisme", "text": "

    Le polymorphisme est la capacit\u00e9 d'une m\u00e9thode ou d'un objet \u00e0 se comporter de mani\u00e8re diff\u00e9rente en fonction du contexte. Il permet \u00e0 une classe d\u00e9riv\u00e9e de red\u00e9finir des m\u00e9thodes de la classe parent. Le polymorphisme peut \u00eatre statique (surcharge de m\u00e9thodes) ou dynamique (via l'utilisation de m\u00e9thodes virtuelles).

    class Animal {\npublic:\n    virtual void faireDuBruit() { cout << \"L'animal fait du bruit\" << endl; }\n};\n\nclass Chien : public Animal {\npublic:\n    void faireDuBruit() override { cout << \"Le chien aboie\" << endl; }\n};\n\nAnimal* a = new Chien();\na->faireDuBruit();  // Appelle la m\u00e9thode faireDuBruit() de la classe Chien\n

    Gr\u00e2ce au polymorphisme dynamique, m\u00eame si a est de type Animal, l'appel de la m\u00e9thode faireDuBruit() ex\u00e9cutera celle de Chien.

    ", "tags": ["Animal", "Chien"]}, {"location": "course-cpp/object-oriented/#constructeur-et-destructeur", "title": "Constructeur et Destructeur", "text": "

    Un constructeur est une m\u00e9thode sp\u00e9ciale appel\u00e9e lors de la cr\u00e9ation d'un objet. Il initialise les attributs de l'objet. Un destructeur est une m\u00e9thode appel\u00e9e lors de la destruction de l'objet, qui permet de lib\u00e9rer des ressources ou de nettoyer l'\u00e9tat.

    class Animal {\npublic:\n    Animal() { cout << \"L'animal est cr\u00e9\u00e9\" << endl; }  // Constructeur\n    ~Animal() { cout << \"L'animal est d\u00e9truit\" << endl; }  // Destructeur\n};\n
    "}, {"location": "course-cpp/object-oriented/#interface-et-classe-abstraite", "title": "Interface et classe abstraite", "text": "

    Une interface est une classe qui ne contient que des m\u00e9thodes virtuelles pures, c\u2019est-\u00e0-dire des m\u00e9thodes sans impl\u00e9mentation, que les classes d\u00e9riv\u00e9es doivent impl\u00e9menter. Une classe abstraite est une classe qui peut contenir \u00e0 la fois des m\u00e9thodes impl\u00e9ment\u00e9es et des m\u00e9thodes virtuelles pures.

    class IAnimal {\npublic:\n    virtual void faireDuBruit() = 0;  // Interface : m\u00e9thode pure\n};\n

    Toute classe qui impl\u00e9mente cette interface doit fournir une impl\u00e9mentation de faireDuBruit().

    "}, {"location": "course-cpp/object-oriented/#surcharge", "title": "Surcharge", "text": "

    La surcharge est une forme de polymorphisme statique o\u00f9 plusieurs m\u00e9thodes peuvent partager le m\u00eame nom mais avec des signatures diff\u00e9rentes (param\u00e8tres distincts).

    class Math {\npublic:\n    int additionner(int a, int b) { return a + b; }\n    double additionner(double a, double b) { return a + b; }  // Surcharge de la m\u00e9thode\n};\n
    "}, {"location": "course-cpp/object-oriented/#redefinition", "title": "Red\u00e9finition", "text": "

    La red\u00e9finition est le fait qu\u2019une classe d\u00e9riv\u00e9e puisse fournir sa propre version d'une m\u00e9thode d\u00e9finie dans une classe parent. Cela se fait g\u00e9n\u00e9ralement via les m\u00e9thodes virtuelles.

    class Animal {\npublic:\n    virtual void faireDuBruit() { cout << \"L'animal fait du bruit\" << endl; }\n};\n\nclass Chat : public Animal {\npublic:\n    void faireDuBruit() override { cout << \"Le chat miaule\" << endl; }  // Red\u00e9finition\n};\n
    "}, {"location": "course-cpp/shared-memory/", "title": "Shared memory", "text": "", "tags": ["mmap"]}, {"location": "course-cpp/shared-memory/#memoire-partagee", "title": "M\u00e9moire partag\u00e9e", "text": "

    Nous le verrons plus loin au chapitre sur la MMU, mais la m\u00e9moire d'un processus m\u00e9moire (programme) ne peut pas \u00eatre acc\u00e9d\u00e9e par un autre programme. Le syst\u00e8me d'exploitation l'en emp\u00eache.

    Lorsque l'on souhaite communiquer entre plusieurs programmes, il est possible d'utiliser diff\u00e9rentes m\u00e9thodes\u2009:

    • les flux (fichiers, stdin, stdout...)
    • la m\u00e9moire partag\u00e9e
    • les sockets

    Vous avez d\u00e9j\u00e0 vu les flux au chapitre pr\u00e9c\u00e9dent, et les sockets ne font pas partie de ce cours d'introduction.

    Notons que la m\u00e9moire partag\u00e9e est un m\u00e9canisme propre \u00e0 chaque syst\u00e8me d'exploitation. Sous POSIX elle est normalis\u00e9e et donc un programme compatible POSIX et utilisant la m\u00e9moire partag\u00e9e pourra fonctionner sous Linux, WSL ou macOS, mais pas sous Windows.

    C'est principalement l'appel syst\u00e8me mmap qui est utilis\u00e9. Il permet de mapper ou d\u00e9mapper des fichiers ou des p\u00e9riph\u00e9riques dans la m\u00e9moire.

    void *mmap(\n    void *addr,\n    size_t length, // Size in bytes\n    int prot,      // Access protection (read/write/execute)\n    int flags,     // Attributs (shared/private/anonymous...)\n    int fd,\n    int offset\n);\n

    Voici un exemple permettant de r\u00e9server un espace partag\u00e9 en \u00e9criture et en lecture entre deux processus\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <sys/mman.h>\n\nvoid* create_shared_memory(size_t size) {\n    // Accessible en lecture et \u00e9criture\n    int protection = PROT_READ | PROT_WRITE;\n\n    // D'autres processus peuvent acc\u00e9der \u00e0 cet espace\n    // lequel est anonyme\n    // so only this process and its children will be able to use it:\n    int visibility = MAP_SHARED | MAP_ANONYMOUS;\n\n    // The remaining parameters to `mmap()` are not important for this use case,\n    // but the manpage for `mmap` explains their purpose.\n    return mmap(NULL, size, protection, visibility, -1, 0);\n}\n
    "}, {"location": "course-cpp/shared-memory/#file-memory-mapping", "title": "File memory mapping", "text": "

    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\u00e9cessaire, ce fichier est copi\u00e9 en m\u00e9moire\u2009:

    FILE *fp = fopen(\"foo\", \"r\");\nfseek(fp, 0, SEEK_END);\nint filesize = ftell(fp);\nfseek(fp, 0, SEEK_SET);\nchar *file = malloc(filesize);\nfread(file, filesize, sizeof(char), fp);\nfclose(fp);\n

    Cette copie n'est pas n\u00e9cessairement n\u00e9cessaire. Une approche POSIX, qui n'est donc pas couverte par le standard C99 consiste \u00e0 lier le fichier dans un espace m\u00e9moire partag\u00e9.

    Ceci n\u00e9cessite l'utilisation de fonctions bas niveau.

    #include <stdio.h>\n#include <stdlib.h>\n#include <sys/mman.h>\n\nint main() {\n    int fd = open(\"foo.txt\", O_RDWR, 0600);\n    char *addr = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n    printf(\"Espace mapp\u00e9 \u00e0 %p\\n\", addr);\n    printf(\"Premiers caract\u00e8res du fichiers : %.*s...\\n\", 20, addr);\n}\n

    Les avantages de cette m\u00e9thode sont\u2009:

    • pas n\u00e9cessaire de copier l'int\u00e9gralit\u00e9 du fichier en m\u00e9moire\u2009;
    • possibilit\u00e9 de partager le m\u00eame fichier ouvert entre plusieurs processus\u2009;
    • possibilit\u00e9 laiss\u00e9e au syst\u00e8me d'exploitation d'utiliser la RAM ou non si les ressources m\u00e9moires deviennent tendues.
    ", "tags": ["fopen"]}, {"location": "summary/summary/", "title": "R\u00e9sum\u00e9", "text": ""}, {"location": "summary/summary/#introduction", "title": "Introduction", "text": "

    Le langage C a cr\u00e9\u00e9 en 1972 par Brian Kernighan et Dennis Ritchie est s'est d\u00e8s lors impos\u00e9 comme le standard industriel pour la programmation embarqu\u00e9e et pour tout d\u00e9veloppement n\u00e9cessitant de hautes performances.

    Le langage est standardis\u00e9 par l'ISO (standardisation internationale) et le standard le plus couramment utilis\u00e9 en 2019 est encore C99.

    Il faut retenir que le C est un langage dit\u2009:

    • Imp\u00e9ratif: programmation en s\u00e9quences de commandes
    • Structur\u00e9: programmation imp\u00e9rative avec des structures de contr\u00f4le imbriqu\u00e9es
    • Proc\u00e9dural: programmation imp\u00e9rative avec appels de proc\u00e9dures

    Ce sont ses paradigmes de programmation

    "}, {"location": "summary/summary/#cycle-de-developpement", "title": "Cycle de d\u00e9veloppement", "text": "

    Le cycle de d\u00e9veloppement se compose toujours des phases\u2009: \u00e9tude, \u00e9criture du cahier des charges, de l'\u00e9criture des tests, de la conception du logiciel, du codage \u00e0 proprement parler et des validations finales. Le mod\u00e8le en cascade est un bon r\u00e9sum\u00e9 tr\u00e8s utilis\u00e9 dans l'industrie.

    "}, {"location": "summary/summary/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    Faire \u00e9voluer un logiciel est aussi un processus it\u00e9ratif\u2009:

    • Editer le code avec un \u00e9diteur comme vi ou vscode
    • Compilation et pr\u00e9traitement console $ gcc -std=c99 -O2 -Wall -c foo.c -o foo.o $ gcc -std=c99 -O2 -Wall -c bar.c -o bar.o
    • Edition des liens console $ gcc -o foobar foo.o bar.o -lm
    • Tests
    ", "tags": ["vscode"]}, {"location": "summary/summary/#make", "title": "Make", "text": "

    Souvent, pour s'\u00e9viter de r\u00e9p\u00e9ter les m\u00eames commandes les d\u00e9veloppeurs utilisent un outil comme make qui tire des r\u00e8gles de compilations d'un fichier nomm\u00e9 Makefile. Cet outil permet d'automatiquement recompiler les fichiers qui ne sont plus \u00e0 jour et r\u00e9g\u00e9n\u00e9rer automatiquement l'ex\u00e9cutable. Certaines recettes de make sont souvent utilis\u00e9es comme\u2009:

    • make all Pour compiler tout le projet
    • make clean Pour supprimer tous les fichiers interm\u00e9diaires g\u00e9n\u00e9r\u00e9s
    • make mrproper Pour supprimer tous les fichiers interm\u00e9diaires ainsi que les ex\u00e9cutables produits.
    • make test Pour ex\u00e9cuter les tests de validation

    D'autres recettes peuvent \u00eatre \u00e9crites dans le fichier Makefile, mais la courbe d'apprentissage du langage de make est raide.

    ", "tags": ["make", "Makefile"]}, {"location": "summary/summary/#linuxposix", "title": "Linux/POSIX", "text": "

    Un certain nombre de commandes sont utilis\u00e9es durant ce cours et voici un r\u00e9sum\u00e9 de ces derni\u00e8res

    Commande Description cat Affiche sur stdout le contenu d'un fichier ls Liste le contenu du r\u00e9pertoire courant ls -al Liste le contenu du r\u00e9pertoire courant avec plus de d\u00e9tails echo Affiche sur stdout les \u00e9l\u00e9ments pass\u00e9s par argument au programme make Outil d'aide \u00e0 la compilation utilisant le fichier Makefile gcc Compilateur open source largement utilis\u00e9 dans l'industrie vi \u00c9diteur de texte ultra puissant, mais difficile \u00e0 apprendre", "tags": ["gcc", "make", "echo", "cat", "Makefile", "stdout"]}, {"location": "summary/summary/#programmation", "title": "Programmation", "text": ""}, {"location": "summary/summary/#diagramme-de-flux", "title": "Diagramme de flux", "text": "

    Le diagramme de flux est beaucoup utilis\u00e9 pour exprimer un algorithme comme celui d'Euclide pour chercher le plus grand diviseur commun.

    :::{figure} {assets}/figures/dist/algorithm/euclide-gcd.* Algorithme de calcul du PGCD d'Euclide. :::

    "}, {"location": "summary/summary/#langage-c", "title": "Langage C", "text": ""}, {"location": "summary/summary/#caracteres-non-imprimables", "title": "Caract\u00e8res non imprimables", "text": "Expression Nom Nom anglais Description \\n LF Line feed Retour \u00e0 la ligne \\v VT Vertical tab Tabulation verticale (entre les paragraphes) \\f FF New page Saut de page \\t TAB Horizontal tab Tabulation horizontale \\r CR Carriage return Retour charriot \\b BS Backspace Retour en arri\u00e8re, effacement d'un caract\u00e8re", "tags": ["TAB"]}, {"location": "summary/summary/#fin-de-lignes", "title": "Fin de lignes", "text": "

    Les caract\u00e8res de fin de ligne d\u00e9pendent du syst\u00e8me d'exploitation et sont appel\u00e9s EOL: End Of Line.

    Expression Nom Syst\u00e8me d'exploitation \\r\\n CRLF Windows \\r CR Anciens Macintoshs (\\< 2000) \\n LF Linux/Unix/POSIX", "tags": ["CRLF"]}, {"location": "summary/summary/#identificateurs", "title": "Identificateurs", "text": "

    :::{figure} {assets}/figures/dist/grammar/identifier.* Grammaire d'un identificateur C :::

    Le format des identificateurs peut \u00e9galement \u00eatre exprim\u00e9 par une expression r\u00e9guli\u00e8re\u2009:

    ^[a-zA-Z_][a-zA-Z0-9_]*$\n
    "}, {"location": "summary/summary/#variable", "title": "Variable", "text": "

    Une variable poss\u00e8de 6 param\u00e8tres\u2009: nom, type, valeur, adresse, port\u00e9e, visibilit\u00e9.

    Elle peut \u00eatre\u2009: globale et dans ce cas elle est automatiquement initialis\u00e9e \u00e0 0\u2009:

    int foo;\n\nint main(void) {\n    return foo;\n}\n

    Ou elle peut \u00eatre locale et dans ce cas il est n\u00e9cessaire de l'initialiser \u00e0 une valeur\u2009:

    int main(void) {\n    int foo = 0;\n    return foo;\n}\n

    Il est possible de d\u00e9clarer plusieurs variables d'un m\u00eame type sur la m\u00eame ligne\u2009:

    int i, j, k;\nint m = 32, n = 22;\n

    Les conventions de nommage pour une variable sont\u2009: camelCase et snake_case, certains utilisent la notation PascalCase.

    Les termes toto, tata, foo, bar sont souvent utilis\u00e9s comme noms g\u00e9n\u00e9riques et sont appel\u00e9s termes m\u00e9tasyntaxiques.

    ", "tags": ["tata", "PascalCase", "toto", "foo", "bar", "camelCase", "snake_case"]}, {"location": "summary/summary/#constantes-litterales", "title": "Constantes litt\u00e9rales", "text": "

    Une constante litt\u00e9rale est une grandeur exprimant une valeur donn\u00e9e qui n'est pas calcul\u00e9e \u00e0 l'ex\u00e9cution\u2009:

    Expression Type Description 6 int Valeur d\u00e9cimale 12u unsigned int Valeur non sign\u00e9e en notation d\u00e9cimale 6l long Valeur longue en notation d\u00e9cimale 010 int Valeur octale 0xa int Valeur hexad\u00e9cimale 0b111 int Valeur binaire (uniquement gcc, pas standard C99) 12. double Nombre r\u00e9el 'a' char Caract\u00e8re \"salut\" char* Cha\u00eene de caract\u00e8re", "tags": ["int", "gcc", "long", "double", "char"]}, {"location": "summary/summary/#commentaires", "title": "Commentaires", "text": "

    Il existe deux types de commentaires\u2009:

    • Les commentaires de lignes (depuis C99)
    // This is a single line comment.\n
    • Les commentaires de blocs
    /* This is a\n   Multi-line comment */\n
    "}, {"location": "summary/summary/#fonction-main", "title": "Fonction main", "text": "

    La fonction main peut s'\u00e9rire sous deux formes\u2009:

    int main(void) {\n    return 0;\n}\n
    int main(int argc, char *argv[]) {\n    return 0;\n}\n
    "}, {"location": "summary/summary/#numeration", "title": "Num\u00e9ration", "text": "

    Les donn\u00e9es dans l'ordinateur sont stock\u00e9es sous forme binaire et le type d'une variable permet de d\u00e9finir son interpr\u00e9tation.

    • Une valeur enti\u00e8re et non sign\u00e9e est exprim\u00e9e sous la forme binaire pure\u2009: text \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 0b1010011 = 83 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    • Une valeur enti\u00e8re et sign\u00e9e est exprim\u00e9e en compl\u00e9ment \u00e0 deux\u2009: text \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u25021\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = ! \u25020\u25020\u25021\u25020\u25021\u25021\u25020\u25020\u2502 = (-1) * (0b101100 + 1) = -45 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    • Une valeur r\u00e9elle ou flottante est exprim\u00e9e selon le standard IEEE-754 et comporte un bit de signe, un exposant et une mantisse. \u250c Signe 1 bit \u2502 \u250c Exposant 8 bits \u2502 \u2502 \u250c Mantisse 23 bits \u2534 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526 \u25020\u25020\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25020\u25021\u25020\u25020\u25021\u25020\u25020\u25020\u2502\u25021\u25021\u25020\u25021\u25021\u25021\u25021\u25021\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25020\u25021\u2502 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    "}, {"location": "summary/summary/#operateurs", "title": "Op\u00e9rateurs", "text": "

    Les op\u00e9rateurs appliquent une op\u00e9ration entre une ou plusieurs valeurs\u2009:

    • Les op\u00e9rateurs unaire s'appliquent \u00e0 un seul op\u00e9rande (!12, ~23)
    • Les op\u00e9rateurs standards s'appliquent \u00e0 deux op\u00e9randes (12 ^ 32)

    Les op\u00e9rateurs ont une priorit\u00e9 et une direction d'associativit\u00e9\u2009:

    u = ++a + b * c++ >> 3 ^ 2\n\nRang  Op\u00e9rateur  Associativit\u00e9\n----  ---------  -------------\n 1    ()++       -->\n 2    ++()       <--\n 2    +          <--\n 2    *          <--\n 5    >>         -->\n 9    ^          -->\n14    =          -->\n

    Donc la priorit\u00e9 de ces op\u00e9rations sera\u2009:

    (u = ((((++a) + (b * (c++))) >> 3) ^ 2))\n

    Dans le cas des op\u00e9rateurs de pr\u00e9 et post incr\u00e9mentation, ils sont en effet les plus prioritaires mais leur action est d\u00e9cal\u00e9e dans le temps au pr\u00e9c\u00e9dent/suivant point de s\u00e9quence. C'est-\u00e0-dire\u2009:

    a += 1;\n(u = (((a + (b * c)) >> 3) ^ 2));\nc += 1;\n
    "}, {"location": "summary/summary/#valeur-gauche", "title": "Valeur gauche", "text": "

    Une valeur gauche lvalue d\u00e9fini ce qui peut se trouver \u00e0 gauche d'une affectation. C'est un terme qui appara\u00eet souvent dans les erreurs de compilation. L'exemple suivant retourne l'erreur\u2009: lvalue required as increment operand car le r\u00e9sultat de a + b n'a pas d'emplacement m\u00e9moire et il n'est pas possible de l'assigner \u00e0 quelque chose pour effectuer l'op\u00e9ration de pr\u00e9-incr\u00e9mentation.

    c = ++(a + b);\n

    Dans cet exemple c est une valeur gauche

    "}, {"location": "summary/summary/#types-de-donnees", "title": "Types de donn\u00e9es", "text": "

    Dans 90% des cas, voici les types qu'un d\u00e9veloppeur utilisera en C et sur le mod\u00e8le de donn\u00e9e LP64

    Type Profondeur Description char 8-bit Caract\u00e8re ou valeur d\u00e9cimale int 32-bit Entier sign\u00e9 unsigned int 32-bit Entier non sign\u00e9 long long 64-bit Entier sign\u00e9 float 32-bit Nombre r\u00e9el (23 bit de mantisse) double 64-bit Nombre r\u00e9el (54 bit de mantisse)

    Pour s'assurer d'une taille donn\u00e9e on peut utiliser les types standard C99 en incluant la biblioth\u00e8que <stdint.h>

    #include <stdint.h>\n\nint main(void) {\n    int8_t foo = 0;  // Valeur sign\u00e9e sur 8-bit\n    uint32_t bar = 0;  // Valeur non sign\u00e9e sur 32-bit\n\n    uint_least16_t = 0;  // Valeur non sign\u00e9e d'au moins 16-bit\n}\n

    Les valeurs sign\u00e9es sont exprim\u00e9es en compl\u00e9ment \u00e0 deux c'est-\u00e0-dire que les valeurs maximales et minimales sont pour un entier 8-bit de -128 \u00e0 +128.

    La construction des types standards\u2009:

    :::{figure} {assets}/figures/dist/datatype/ansi-integers.* :alt\u2009: \"Entiers standardis\\xE9s C89\" :width\u2009: 100 % :::

    La construction des types portables\u2009:

    :::{figure} {assets}/figures/dist/datatype/c99-integers.* :alt\u2009: \"Entiers standardis\\xE9s C99\" :width\u2009: 100 % :::

    ", "tags": ["int", "float", "char", "double"]}, {"location": "summary/summary/#caracteres", "title": "Caract\u00e8res", "text": "

    Un caract\u00e8re est une valeur binaire cod\u00e9e sur 8-bit et dont l'interpr\u00e9tation est confi\u00e9e \u00e0 une table de correspondance nomm\u00e9e ASCII :

    :::{figure} {assets}/figures/dist/encoding/ascii.* Table ANSI INCITS 4-1986 (standard actuel) :::

    Seul ces valeurs sont garanties d'\u00eatre stock\u00e9es sur 8-bit. Pour les caract\u00e8res accentu\u00e9s ou les \u00e9motic\u00f4nes, la mani\u00e8re dont ils sont cod\u00e9s en m\u00e9moire d\u00e9pend de l'encodage des caract\u00e8res. Souvent on utilise le type d'encodage utf-8.

    Les \u00e9critures suivantes sont donc strictement identiques\u2009:

    char a;\n\na = 'a';\na = 0x61;\na = 97;\na = 0141;\n
    "}, {"location": "summary/summary/#chaine-de-caractere", "title": "Cha\u00eene de caract\u00e8re", "text": "

    Une cha\u00eene de caract\u00e8re est exprim\u00e9e avec des guillemets double. Une cha\u00eene de caract\u00e8re comporte toujours un caract\u00e8re terminal \\0.

    char str[] = \"Hello\";\n

    La taille en m\u00e9moire de cette cha\u00eene de caract\u00e8re est de 6 bytes, 5 caract\u00e8res et un caract\u00e8re de terminaison.

    "}, {"location": "summary/summary/#booleens", "title": "Bool\u00e9ens", "text": "

    En C la valeur 0 est consid\u00e9r\u00e9e comme fausse (false) et une valeur diff\u00e9rente de 0 est consid\u00e9r\u00e9e comme vraie (true). Toutes les assertions suivantes sont vraies\u2009:

    if (42) { /* ... */ }\nif (!0) { /* ... */ }\nif (true && true || false) { /* ... */ }\n

    Pour utiliser les mots cl\u00e9s true et false il faut utiliser la biblioth\u00e8que <stdbool.h>

    ", "tags": ["false", "true"]}, {"location": "summary/summary/#promotion-implicite", "title": "Promotion implicite", "text": "

    Un type est automatiquement et tacitement promu dans le type le plus g\u00e9n\u00e9ral\u2009:

    char a;\nint b;\nlong long c;\nunsigned int d;\n\na + b // R\u00e9sultat promu en `int`\na + c // R\u00e9sultat promu en `long long`\nb + d // R\u00e9sultat promu en `int`\n

    Attention aux valeurs en virgule flottante\u2009:

    int a = 9, b = 2;\ndouble b;\n\na / b;  // R\u00e9sultat de type entier, donc 4 et non 4.5\n(float)a / b;  // R\u00e9sultat de type float donc 4.5\nb / a;  // R\u00e9sultat en type double (promotion)\n
    "}, {"location": "summary/summary/#transtypage", "title": "Transtypage", "text": "

    Pr\u00e9fixer une variable ou une valeur avec (int) comme dans\u2009: (int)a permet de convertir explicitement cette variable dans le type donn\u00e9.

    Le transtypage peut \u00eatre implicite par exemple dans int a = 4.5

    Ou plus sp\u00e9cifiquement dans\u2009:

    float u = 0.0;\nprintf(\"%f\", b); // Promotion implicite de `float` en `double`\n
    "}, {"location": "summary/summary/#structure-de-controle", "title": "Structure de contr\u00f4le", "text": ""}, {"location": "summary/summary/#sequence", "title": "S\u00e9quence", "text": "

    Une s\u00e9quence est d\u00e9termin\u00e9e par un bloc de code entre accolades\u2009:

    {\n    int a = 12;\n    b += a;\n}\n
    "}, {"location": "summary/summary/#si-sinon", "title": "Si, sinon", "text": "
    if (condition)\n{\n    // Si vrai\n}\nelse\n{\n    // Sinon\n}\n
    "}, {"location": "summary/summary/#si-sinon-si-sinon", "title": "Si, sinon si, sinon", "text": "
    if (condition)\n{\n    // Si vrai\n}\nelse if (autre_condition)\n{\n    // Sinon si autre condition valide\n}\nelse\n{\n    // Sinon\n}\n
    "}, {"location": "summary/summary/#boucle-for", "title": "Boucle For", "text": "
    for (int i = 0; i < 10; i++)\n{\n    // Block ex\u00e9cut\u00e9 10 fois\n}\n\nk = i; // Erreur car `i` n'est plus accessible ici...\n
    "}, {"location": "summary/summary/#boucle-while", "title": "Boucle While", "text": "
    int i = 10;\n\nwhile (i > 0) {\n    i--;\n}\n
    "}, {"location": "summary/summary/#programmes-et-processus", "title": "Programmes et Processus", "text": "\u00c9l\u00e9ment Description stdin Entr\u00e9e standard stdout Sortie standard stderr Sortie d'erreur standard argc Nombre d'arguments argv Valeurs des arguments exit-status Status de sortie d'un programme $? signaux Interaction avec le syst\u00e8me d'exploitation

    :::{figure} {assets}/figures/dist/process/program.* R\u00e9sum\u00e9 des interactions avec un programme :::

    ", "tags": ["stdin", "stderr", "argv", "stdout", "argc", "signaux"]}, {"location": "summary/summary/#entrees-sorties", "title": "Entr\u00e9es Sorties", "text": ""}, {"location": "summary/summary/#printf", "title": "printf", "text": "

    Les sorties format\u00e9es utilisent printf dont le format est\u2009:

    %[parameter][flags][width][.precision][length]type\n
    parameter (optionnel)

    Num\u00e9ro de param\u00e8tre \u00e0 utiliser

    flags (optionnel)

    Modificateurs\u2009: pr\u00e9fixe, signe plus, alignement \u00e0 gauche ...

    width (optionnel)

    Nombre minimum de caract\u00e8res \u00e0 utiliser pour l'affichage de la sortie.

    .precision (optionnel)

    Nombre minimum de caract\u00e8res affich\u00e9s \u00e0 droite de la virgule. Essentiellement, valide pour les nombres \u00e0 virgule flottante.

    length (optionnel)

    Longueur en m\u00e9moire. Indique la longueur de la repr\u00e9sentation binaire.

    type

    Type de formatage souhait\u00e9

    :::{figure} {assets}/figures/dist/string/formats.* Formatage d'un marqueur :::

    ", "tags": ["type", "parameter", "printf", "flags", "length", "width"]}, {"location": "summary/summary/#techniques-de-programmation", "title": "Techniques de programmation", "text": ""}, {"location": "summary/summary/#masque-binaire", "title": "Masque binaire", "text": "

    Pour tester si un bit est \u00e0 un\u2009:

    if (c & 0x040)\n

    Pour forcer un bit \u00e0 z\u00e9ro\u2009:

    c &= ~0x02;\n

    Pour forcer un bit \u00e0 un\u2009:

    c |= 0x02;\n
    "}, {"location": "summary/summary/#permuter-deux-variables-sans-valeur-intermediaire", "title": "Permuter deux variables sans valeur interm\u00e9diaire", "text": "
    a = b ^ c;\nb = a ^ c;\na = b ^ c;\n
    "}, {"location": "tools/analysis/analysis/", "title": "Diagnostiques", "text": "

    Les outils de diagnostiques permettent de comprendre le comportement d'un programme en cours d'ex\u00e9cution. Ils permettent de voir les appels syst\u00e8mes, les appels de fonctions, les appels de biblioth\u00e8ques, les appels de fonctions syst\u00e8me, les appels de fonctions de biblioth\u00e8ques.

    "}, {"location": "tools/analysis/analysis/#time", "title": "time", "text": "

    time est une commande Unix qui permet de mesurer le temps d'ex\u00e9cution d'un programme. Elle est utilis\u00e9e pour mesurer le temps d'ex\u00e9cution d'un programme et les ressources utilis\u00e9es par ce programme. L'utilisation typique est la suivante\u2009:

    time ./a.out\n./a.out 1.23s user 0.10s system 78% cpu 1.23 total\n

    On note plusieurs m\u00e9triques\u2009:

    • user est le temps pass\u00e9 dans l'espace utilisateur, c'est le temps r\u00e9el pass\u00e9 par le programme \u00e0 ex\u00e9cuter des instructions.
    • system est le temps pass\u00e9 dans l'espace noyau, c'est le temps pass\u00e9 par le programme \u00e0 ex\u00e9cuter des appels syst\u00e8mes.
    • wall ou real est le temps r\u00e9el pass\u00e9 par le programme \u00e0 s'ex\u00e9cuter. C'est le temps qui s'\u00e9coule entre le lancement du programme et sa fin.
    • cpu est le pourcentage de temps CPU utilis\u00e9 par le programme.

    Un programme multi-thread\u00e9 peut utiliser plus de 100% de CPU si plusieurs threads sont ex\u00e9cut\u00e9s en parall\u00e8le. Par exemple un programme qui s'ex\u00e9cuterait de mani\u00e8re parall\u00e8le sur 5 processeurs et dont le temps mur serait de 1 secondes, aurait un temps CPU de 500% et un temps user de 5 seconde.

    ", "tags": ["real", "time", "cpu", "system", "wall", "user"]}, {"location": "tools/analysis/analysis/#strace", "title": "strace", "text": "

    Strace a \u00e9t\u00e9 initialement \u00e9crit pour SunOS par Paul Kranenburg en 1991. Il a \u00e9t\u00e9 port\u00e9 sur Linux par Branko Lankester en 1992. Strace est un outil de diagnostique qui permet de suivre les appels syst\u00e8mes et les signaux re\u00e7us par un processus. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution.

    Son code source est bien entendu disponible sur GitHub et il est \u00e9crit en C.

    L'utilisation typique est la suivante, o\u00f9 PID est le num\u00e9ro de processus du programme \u00e0 tracer.

    strace -o /tmp/strace.log -f -e trace=all -p PID\n

    Il est \u00e9galement possible d'ex\u00e9cuter un programme avec strace directement.

    strace ./a.out\n
    ", "tags": ["PID", "strace"]}, {"location": "tools/analysis/analysis/#cas-dutilisation", "title": "Cas d'utilisation", "text": ""}, {"location": "tools/analysis/analysis/#analyse-de-performance", "title": "Analyse de performance", "text": "

    Les performances d'un programmes peuvent \u00eatre impact\u00e9es par des appels syst\u00e8mes trop nombreux. Lorsqu'un programme communique avec le syst\u00e8me d'exploitation (lecture disque, r\u00e9seau, etc.), il doit passer par des appels syst\u00e8mes. Ces appels peuvent \u00eatre co\u00fbteux en temps et en ressources car ils n\u00e9cessitent un changement de contexte entre le mode utilisateur et le mode noyau. C'est \u00e0 dire que le programme doit se mettre en pause pour laisser le noyau effectuer l'op\u00e9ration demand\u00e9e. Lors de la mesure du temps d'ex\u00e9cution d'un programme, par exemple avec time, on peut noter le temps system qui correspond au temps pass\u00e9 dans le noyau. Si ce temps est trop \u00e9lev\u00e9, cela peut indiquer que le programme n'est pas efficace car il passe son temps \u00e0 attendre des op\u00e9rations d'entr\u00e9e/sortie. strace permet dans ce cas de mesurer le nombre d'appels syst\u00e8mes et de les analyser pour comprendre pourquoi le programme est lent.

    \u00c0 titre d'exemple, prenons la sortie standard. Dans le standard POSIX, un programme n'est pas directement connect\u00e9 \u00e0 la sortie standard. La biblioth\u00e8que standard C utiliser un tampon pour stocker les donn\u00e9es avant de les envoyer \u00e0 la sortie standard. Ce tampon est automatiquement vid\u00e9 selon certaines conditions notamment\u2009:

    • lorsqu'un caract\u00e8re de fin de ligne est \u00e9crit,
    • lorsque le programme se termine,
    • lorsque le tampon est plein,
    • lorsque le programme appelle fflush ou fclose.

    Si vous \u00e9crivez par exemple le code suivant, ce n'est que lorsque le caract\u00e8re \\n est \u00e9crit que le tampon est vid\u00e9 et que l'appel syst\u00e8me write est effectu\u00e9. On peut le v\u00e9rifier avec strace

    printf(\"foo\");\nprintf(\"bar\");\nprintf(\"\\n\");\n

    Cette ex\u00e9cution est report\u00e9e avec la ligne suivante. On observe que les trois op\u00e9rations printf sont regroup\u00e9es dans un seul appel syst\u00e8me.

    write(1, \"foobar\\n\", 7)                 = 7\n

    En revanche, si le programme est modifi\u00e9 pour vider le tampon apr\u00e8s chaque caract\u00e8re, on observe davantage d'appels syst\u00e8mes\u2009:

    int main() {\n   char *str = \"foobar\\n\";\n   while (*str) {\n      putchar(*(str++));\n      fflush(stdout);\n   }\n}\n
    write(1, \"f\", 1f)     = 1\nwrite(1, \"o\", 1o)     = 1\nwrite(1, \"o\", 1o)     = 1\nwrite(1, \"b\", 1b)     = 1\nwrite(1, \"a\", 1a)     = 1\nwrite(1, \"r\", 1r)     = 1\nwrite(1, \"\\n\", 1)     = 1\n
    ", "tags": ["time", "fflush", "fclose", "write", "strace", "printf"]}, {"location": "tools/analysis/analysis/#suivi-des-operations-dentreesortie", "title": "Suivi des op\u00e9rations d'entr\u00e9e/sortie", "text": "

    strace permet de suivre les op\u00e9rations d'entr\u00e9e/sortie d'un programme. Cela peut \u00eatre utile pour comprendre pourquoi un programme ne fonctionne pas correctement. Par exemple, si un programme lit un fichier et que le fichier n'est pas trouv\u00e9, strace permet de voir l'appel syst\u00e8me open et le code d'erreur ENOENT qui indique que le fichier n'existe pas.

    Dans ce cas on ne sera sensible qu'aux appels syst\u00e8mes open et write:

    strace -e trace=open,read,write ./a.out\n
    ", "tags": ["open", "ENOENT", "write", "strace"]}, {"location": "tools/analysis/analysis/#resolution-des-dependances", "title": "R\u00e9solution des d\u00e9pendances", "text": "

    Au d\u00e9but de l'ex\u00e9cution du programme, avant que le main ne soit ex\u00e9cut\u00e9, la biblioth\u00e8que standard C doit \u00eatre charg\u00e9e. strace permet de voir les d\u00e9pendances du programme et les biblioth\u00e8ques qui sont charg\u00e9es. Cela peut \u00eatre utile pour comprendre pourquoi un programme ne fonctionne pas correctement ou pourquoi il ne trouve pas une biblioth\u00e8que. On observera typiquement au d\u00e9but de la sortie de strace quelque chose de similaire \u00e0 ceci\u2009:

    execve(\"./a.out\", [\"./a.out\"], 0x7ffec3bd4ef0 /* 50 vars */) = 0\n\nbrk(NULL) = 0x56475a63a000\nmmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2143f6f000\naccess(\"/etc/ld.so.preload\", R_OK) = -1 ENOENT (No such file or directory)\n\nopenat(AT_FDCWD, \"/etc/ld.so.cache\", O_RDONLY|O_CLOEXEC) = 3\nfstat(3, {st_mode=S_IFREG|0644, st_size=87775, ...}) = 0\nmmap(NULL, 87775, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2143f59000\nclose(3) = 0\n\nopenat(AT_FDCWD, \"/lib/x86_64-linux-gnu/libc.so.6\", O_RDONLY|O_CLOEXEC) = 3\nread(3, \"\\177ELF\\2\\(...)\"..., 832) = 832\npread64(3, \"\\6\\0\\0\\0\\4\\0\\0\\0@\\0(...)\"..., 784, 64) = 784\nfstat(3, {st_mode=S_IFREG|0755, st_size=2125328, ...}) = 0\npread64(3, \"\\6\\0\\0\\0\\4\\0\\0\\0@(...)\"..., 784, 64) = 784\nmmap(NULL, 2170256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2143d47000\nmmap(0x7f2143d6f000, ...)\nmmap(0x7f2143ef7000, ...)\nmmap(0x7f2143f46000, ...)\nmmap(0x7f2143f4c000, ...)\nclose(3)\n

    Analysons en d\u00e9tail les premi\u00e8res lignes de la sortie de strace:

    1. execve d\u00e9marre l'ex\u00e9cution du programme ./a.out. C'est le tout premier appel syst\u00e8me qui est effectu\u00e9.
    2. brk ajuste la taille du heap du programme. Appel\u00e9 avec NULL, le noyau retourne la limite actuelle du heap.
    3. mmap alloue de la m\u00e9moire, ici 8 kio pour des besoins internes.
    4. access(\"/etc/ld.so.preload\") permet d'injecter des biblioth\u00e8ques avant le chargement des biblioth\u00e8ques standard. Ici, le fichier n'existe pas. C'est notament utilis\u00e9 pour les outils de profiling. On observe l'erreur ENOENT qui signifie que le fichier n'existe pas.
    5. openat ouvre le fichier /etc/ld.so.cache qui contient les chemins des biblioth\u00e8ques partag\u00e9es pr\u00e9compil\u00e9es. Le syst\u00e8me y acc\u00e8de pour localiser rapidement les biblioth\u00e8ques dynamiques n\u00e9cessaires plut\u00f4t que de les charger depuis le disque
    6. mmap alloue de l'espace m\u00e9moire pour le fichier /etc/ld.so.cache.
    7. close ferme le fichier /etc/ld.so.cache une fois charg\u00e9.
    8. openat permet de charger la biblioth\u00e8que standard C libc.so.6 qui est situ\u00e9e dans /lib/x86_64-linux-gnu/.
    9. Les autres mmap permettent de charger la biblioth\u00e8que standard C en m\u00e9moire afin de pouvoir \u00eatre utilis\u00e9es par le programme.

    On observe donc des \u00e9l\u00e9ments importants de l'ex\u00e9cution d'un programme. Les biblioth\u00e8ques dynamiques sont charg\u00e9es en m\u00e9moire au d\u00e9but de l'ex\u00e9cution du programme. Si ces biblioth\u00e8ques sont absentes du syst\u00e8me, il ne pourra pas \u00eatre ex\u00e9cut\u00e9.

    ", "tags": ["mmap", "heap", "ENOENT", "strace", "main", "NULL", "openat", "brk", "libc.so.6", "close", "execve"]}, {"location": "tools/analysis/analysis/#suivi-des-threads", "title": "Suivi des threads", "text": "

    Lors d'un programme concurrent, il est possible de suivre la cr\u00e9ation et la terminaison des threads avec strace, notament au moyen des appels syst\u00e8mes fork, clone ou exec. Les appels \u00e0 futex permettent de voir les op\u00e9rations de synchronisation entre les threads utilis\u00e9s par les mutex et les variables de condition.

    ", "tags": ["exec", "clone", "fork", "futex", "strace"]}, {"location": "tools/analysis/analysis/#fonctionnement-interne", "title": "Fonctionnement interne", "text": "

    Le fonctionnement de cet outil repose sur ptrace qui est une fonction du noyau Linux qui permet de suivre l'ex\u00e9cution d'un processus. strace utilise cette fonction pour intercepter les appels syst\u00e8mes et les signaux. ptrace est notament utilis\u00e9 par les d\u00e9bogueurs comme gdb. \u00c0 chaque fois qu'un processus enfant execute un appel syst\u00e8me, le processus traceur est notifi\u00e9 et peut inspecter les registres du processus enfant.

    Pour mieux comprendre comment fonctionn strace, voici un exemple simple d'un programme C qui utilise ptrace pour tracer l'appel syst\u00e8me write notament utilis\u00e9 par printf.

    #include <errno.h>\n#include <stdio.h>\n#include <sys/ptrace.h>\n#include <sys/syscall.h>\n#include <sys/types.h>\n#include <sys/user.h>\n#include <sys/wait.h>\n#include <unistd.h>\n\nint main() {\n   pid_t child;\n   child = fork();\n\n   if (child == 0) {  // Child (executed under trace)\n      // Allow parent to trace this process\n      ptrace(PTRACE_TRACEME, 0, NULL, NULL);\n      raise(SIGSTOP);  // Stop child to let parent catch up\n      printf(\"Hello from the child process!\\n\");\n\n   } else {  // Parent\n      int status;\n      waitpid(child, &status, 0);\n\n      ptrace(PTRACE_SYSCALL, child, NULL, NULL);\n\n      while (1) {\n         struct user_regs_struct regs;\n\n         waitpid(child, &status, 0);\n         if (WIFEXITED(status)) break;\n\n         ptrace(PTRACE_GETREGS, child, NULL, &regs);\n\n         if (regs.orig_rax == SYS_write) {\n            printf(\"Intercepted write syscall!\\n\");\n            printf(\"File descriptor: %lld\\n\", regs.rdi);\n            printf(\"Buffer address: %lld\\n\", regs.rsi);\n            printf(\"Buffer size: %lld\\n\", regs.rdx);\n         }\n         ptrace(PTRACE_SYSCALL, child, NULL, NULL);  // Let child continue\n         waitpid(child, &status, 0);                 // Wait for syscall exit\n         if (WIFEXITED(status)) break;               // Child exited?\n\n         ptrace(PTRACE_SYSCALL, child, NULL, NULL);\n      }\n   }\n}\n

    La sortie de ce programme est la suivante\u2009:

    Intercepted write syscall!\nFile descriptor: 1\nBuffer address: 93866916508320\nBuffer size: 30\nHello from the child process!\n

    Ce programme utilise deux processus l\u00e9gers (threads), l'enfant simule le processus trac\u00e9 et le parent est le traceur. Au moment du fork le programme est dupliqu\u00e9 et s'ex\u00e9cute en parall\u00e8le. Pour distinguer le parent de l'enfant, on utilise la valeur de retour de fork. Si la valeur est 0, c'est que le processus est l'enfant, sinon c'est le parent. Il y a donc deux chemins possibles dans le programme.

    L'enfant autorise le parent \u00e0 le tracer avec PTRACE_TRACEME et se met en pause avec raise(SIGSTOP);. Le parent attend que l'enfant soit en pause avec waitpid(child, &status, 0); et commence \u00e0 tracer les appels syst\u00e8mes avec PTRACE_SYSCALL, child. Cette instruction demande au noyau de laisser l'enfant continuer son ex\u00e9cution, mais avec une interception \u00e0 chaque entr\u00e9e et sortie d'un appel syst\u00e8me. Cela signifie que chaque fois que l'enfant effectue un appel syst\u00e8me, il sera suspendu, et le parent sera notifi\u00e9. Le point de sortie de la boucle while est le test de WIFEXITED qui v\u00e9rifie si le processus enfant s'est termin\u00e9, dans ce cas, le parent sort de la boucle. L'appel PTRACE_GETREGS permet au parent de lire les registres du processus enfant. Ces registres contiennent des informations cruciales comme le num\u00e9ro de l'appel syst\u00e8me dans le registre orig_rax pour les syst\u00e8mes x86_64. Si l'appel syst\u00e8me intercept\u00e9 est SYS_write, le parent affiche les informations sur l'appel syst\u00e8me. Enfin, le parent laisse l'enfant continuer son ex\u00e9cution avec PTRACE_SYSCALL et attend le suivant.

    ", "tags": ["WIFEXITED", "fork", "gdb", "ptrace", "PTRACE_TRACEME", "SYS_write", "x86_64", "write", "strace", "orig_rax", "printf", "PTRACE_GETREGS", "PTRACE_SYSCALL"]}, {"location": "tools/analysis/analysis/#ltrace", "title": "ltrace", "text": "

    ltrace (pour library trace) est un outil de diagnostique qui permet de suivre les appels aux fonctions des biblioth\u00e8ques partag\u00e9es. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution. ltrace est un outil plus simple que strace car il ne suit que les appels de fonctions et non les appels syst\u00e8mes. Prenons l'exemple du programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n    char *buffer = (char *)malloc(100);\n    printf(\"Hello, World!\\n\");\n    free(buffer);\n}\n

    L'appel de ltrace sur ce programme donne la sortie suivante\u2009:

    ltrace ./a.out\nmalloc(100)  = 0x55af7c6d42a0\nputs(\"Hello, World!\"Hello, World!)  = 14\nfree(0x55af7c6d42a0) = <void>\n+++ exited (status 0) +++\n

    On peut voir notament que le compilateur \u00e0 remplac\u00e9 printf par puts car aucun format n'est utilis\u00e9.

    ", "tags": ["puts", "printf", "strace", "ltrace"]}, {"location": "tools/analysis/analysis/#perf", "title": "perf", "text": "

    perf est un outil tr\u00e8s puissant de diagnostique qui permet de suivre les performances d'un programme en analysant les compteurs mat\u00e9riels du processeur.

    Le processeur est un organe tr\u00e8s complexe qui contient de nombreux compteurs mat\u00e9riels qui permettent de mesurer des m\u00e9triques comme le nombre de cycles d'horloge, l'utilisation de la m\u00e9moire cache, le predicteur d'embranchement, les changements de contexte, etc. Ces compteurs sont tr\u00e8s utiles pour comprendre le comportement d'un programme et identifier les goulots d'\u00e9tranglement.

    Les PMU (Performance Monitoring Units) sont des composants mat\u00e9riels qui permettent de mesurer ces m\u00e9triques. Les PMU sont sp\u00e9cifiques \u00e0 chaque processeur et sont g\u00e9n\u00e9ralement accessibles via des instructions sp\u00e9cifiques. Ils d\u00e9pendent grandement du processeur utilis\u00e9. Par exemple, un processeur Intel Core i7 de 10e g\u00e9n\u00e9ration ne dispose pas des m\u00eames compteurs qu'un processeur AMD Ryzen 9. L'installation de perf n'est donc pas triviale, surtout sur des syst\u00e8mes hypervis\u00e9s ou virtualis\u00e9s.

    Par exemple sur WSL2, l'installation de perf n\u00e9cessite de compiler un noyau Linux avec les options de d\u00e9bogage activ\u00e9es. Sur des syst\u00e8mes comme macOS, perf n'est pas disponible car le noyau XNU ne fournit pas les m\u00eames fonctionnalit\u00e9s que le noyau Linux.

    ", "tags": ["perf"]}, {"location": "tools/analysis/analysis/#utilisation", "title": "Utilisation", "text": "

    Une utilisation typique de perf est d'analyser les pertes de performances li\u00e9 \u00e0 la m\u00e9moire cache. Prenons l'exemple d'une matrice 1000x1000 ou chaque \u00e9l\u00e9ment est multipli\u00e9 par 2. Le programme suivant effectue cette op\u00e9ration\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\n#define SIZE 10000\n\nint main() {\n   // Allocate\n   int **matrix = (int **)malloc(SIZE * sizeof(int *));\n   for (int i = 0; i < SIZE; i++) matrix[i] = (int *)malloc(SIZE * sizeof(int));\n\n   // Init\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[i][j] = i + j;\n\n         // Multiply\n#if DIRECTION == 0\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[i][j] *= 2;\n#else\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[j][i] *= 2;\n#endif\n\n   // Cleanup\n   for (int i = 0; i < SIZE; i++) free(matrix[i]);\n   free(matrix);\n}\n

    En compilant chaque programme avec la variable DIRECTION \u00e9gale \u00e0 0 ou 1, on peut observer les diff\u00e9rences de performances. En effet, la m\u00e9moire est stock\u00e9e de mani\u00e8re lin\u00e9aire en m\u00e9moire, et en parcourant la matrice ligne par ligne ou colonne par colonne, on peut observer des diff\u00e9rences de performances.

    $ gcc -DDIRECTION=0 x.c && perf stat -e cache-misses,cycles,instructions ./a.out\n\n       11048991      cache-misses:u\n     1502929728      cycles:u\n     4304906724      instructions:u\n\n    0.384328204 seconds time elapsed\n\n    0.314080000 seconds user\n    0.070921000 seconds sys\n\n$ gcc -DDIRECTION=1 x.c && perf stat -e cache-misses,cycles,instructions ./a.out\n\n       23466037      cache-misses:u\n     4320403836      cycles:u\n     4304906756      instructions:u\n\n    0.966821928 seconds time elapsed\n\n    0.886317000 seconds user\n    0.080574000 seconds sys\n

    Dans un cas, le compteur cache-misses est bien plus \u00e9lev\u00e9 que dans l'autre. Ce la arrive lors de probl\u00e8mes de localit\u00e9s spatiales ou temporelles. Dans le premier cas, les donn\u00e9es sont charg\u00e9es dans le cache et restent en cache, alors que dans le second cas, le cache ne peut pas \u00eatre utilis\u00e9 efficacement. Le programme est 3x plus lent dans le second cas, pour une simple inversion dans la boucle.

    ", "tags": ["DIRECTION"]}, {"location": "tools/analysis/analysis/#wsl2", "title": "WSL2", "text": "

    Malheureusement perf ne fonctionne pas nativement sous WSL2. WSL2 ex\u00e9cute Linux dans une machine virtuelle l\u00e9g\u00e8re (Hyper-V), mais n'a pas d'acc\u00e8s direct aux compteurs mat\u00e9riels, essentiels pour perf. Cela signifie que les \u00e9v\u00e9nements li\u00e9s aux performances mat\u00e9rielles, tels que les cycles d'horloge du processeur, les instructions, et les caches, ne sont pas accessibles.

    La premi\u00e8re \u00e9tape est d'installer les paquets n\u00e9cessaires\u2009:

    sudo apt install build-essential flex bison libssl-dev \\\nlibelf-dev libpfm4-dev libtraceevent-dev asciidoc xmlto\n

    On clone ensuite le noyau Linux de WSL2 en n'oubliant pas le --depth=1 pour ne r\u00e9cup\u00e9rer que le dernier commit.

    git clone --depth=1 https://github.com/microsoft/WSL2-Linux-Kernel.git\n

    L'outil perf est inclus dans le noyau, et l'objectif est de le compiler. Pour ce faire, on se place dans le r\u00e9pertoire du noyau et on copie la configuration du noyau actuel.

    cd WSL2-Linux-Kernel/tools/perf\nmake\nsudo make prefix=/usr install\n

    Vous pouvez d\u00e9sormais utiliser perf pour diagnostiquer les performances de vos programmes sous WSL2. Il est possible de v\u00e9rifier les \u00e9v\u00e9nements disponibles avec perf list. Il se peut que sur votre syst\u00e8me, certains \u00e9v\u00e9nements ne soient pas disponibles.

    $ perf list\n  branch-instructions OR branches      [Hardware event]\n  branch-misses                        [Hardware event]\n  bus-cycles                           [Hardware event]\n  cache-misses                         [Hardware event]\n  cache-references                     [Hardware event]\n  cpu-cycles OR cycles                 [Hardware event]\n  instructions                         [Hardware event]\n  ref-cycles                           [Hardware event]\n  alignment-faults                     [Software event]\n  bpf-output                           [Software event]\n  cgroup-switches                      [Software event]\n  context-switches OR cs               [Software event]\n  cpu-clock                            [Software event]\n  cpu-migrations OR migrations         [Software event]\n  dummy                                [Software event]\n  emulation-faults                     [Software event]\n  major-faults                         [Software event]\n  minor-faults                         [Software event]\n  page-faults OR faults                [Software event]\n  task-clock                           [Software event]\n  duration_time                        [Tool event]\n  user_time                            [Tool event]\n  system_time                          [Tool event]\n
    ", "tags": ["perf"]}, {"location": "tools/analysis/analysis/#valgrind", "title": "valgrind", "text": "

    valgrind est un outil de diagnostique qui permet de suivre les erreurs de m\u00e9moire dans un programme. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution. valgrind est un outil plus simple que strace car il ne suit que les erreurs de m\u00e9moire et non les appels syst\u00e8mes.

    L'outil est disponible sur Ubuntu et d\u00e9riv\u00e9s avec la commande suivante\u2009:

    sudo apt install valgrind\n

    L'utilisation typique est de lancer un programme avec valgrind pour d\u00e9tecter les erreurs de m\u00e9moire. Par exemple, pour un programme a.out:

    valgrind ./a.out\n

    Voici une liste des fonctionnalit\u00e9s typiques de valgrind:

    • D\u00e9tection des fuites de m\u00e9moire : Valgrind permet de trouver les allocations de m\u00e9moire qui ne sont pas lib\u00e9r\u00e9es, aidant \u00e0 identifier les fuites de m\u00e9moire dans les programmes.

    • D\u00e9tection des erreurs de m\u00e9moire : Il d\u00e9tecte les acc\u00e8s ill\u00e9gaux \u00e0 la m\u00e9moire, tels que l'acc\u00e8s \u00e0 des zones m\u00e9moire non allou\u00e9es, l'utilisation de m\u00e9moire apr\u00e8s qu'elle a \u00e9t\u00e9 lib\u00e9r\u00e9e (use-after-free), ou encore les buffer overflows.

    • D\u00e9tection des erreurs d'initialisation de m\u00e9moire : Valgrind rep\u00e8re l'utilisation de variables non initialis\u00e9es en m\u00e9moire, ce qui peut provoquer des comportements impr\u00e9visibles.

    • Profilage de la gestion de la m\u00e9moire : Il vous montre comment la m\u00e9moire est allou\u00e9e et lib\u00e9r\u00e9e pendant l'ex\u00e9cution, permettant d'optimiser l'utilisation de la m\u00e9moire et de d\u00e9tecter les probl\u00e8mes de fragmentation.

    • D\u00e9bogage multithread : Valgrind aide \u00e0 d\u00e9tecter les erreurs de synchronisation dans les programmes multithread, comme les data races (acc\u00e8s concurrents non prot\u00e9g\u00e9s \u00e0 des variables partag\u00e9es) ou les verrous non lib\u00e9r\u00e9s.

    • V\u00e9rification des appels syst\u00e8me incorrects : Valgrind d\u00e9tecte les mauvais usages des appels syst\u00e8mes, comme la fermeture de descripteurs de fichiers qui ne sont pas ouverts.

    • Analyse des performances : Avec l'outil Callgrind (inclus dans Valgrind), il permet de profiler les programmes en comptant les instructions ex\u00e9cut\u00e9es et en g\u00e9n\u00e9rant des informations sur l'utilisation du CPU et des fonctions, utile pour l'optimisation des performances.

    • Simulation de cache CPU : Avec Cachegrind, Valgrind peut simuler le comportement des caches CPU et montrer le nombre de cache misses (d\u00e9fauts de cache) pour aider \u00e0 am\u00e9liorer l'efficacit\u00e9 des programmes en mati\u00e8re de gestion de cache.

    • D\u00e9tection des erreurs de pile : Il v\u00e9rifie l'utilisation correcte de la pile (stack), d\u00e9tectant les d\u00e9bordements et erreurs de gestion dans la pile d'appels.

    ", "tags": ["a.out", "valgrind", "strace"]}, {"location": "tools/analysis/binutils/", "title": "Binutils", "text": "

    Les outils binaires (binutils) sont une collection de programmes install\u00e9s avec un compilateur et permettant d'aider au d\u00e9veloppement et au d\u00e9bogage. Certains de ces outils sont tr\u00e8s pratiques, mais nombreux sont les d\u00e9veloppeurs qui ne les connaissent pas.

    nm

    Liste tous les symboles dans un fichier objet (binaire). Ce programme appliqu\u00e9 sur le programme hello world de l'introduction donne\u2009:

    $ nm a.out\n0000000000200dc8 d _DYNAMIC\n0000000000200fb8 d _GLOBAL_OFFSET_TABLE_\n00000000000006f0 R _IO_stdin_used\n                w _ITM_deregisterTMCloneTable\n                w _ITM_registerTMCloneTable\n\n...\n\n                U __libc_start_main@@GLIBC_2.2.5\n0000000000201010 D _edata\n0000000000201018 B _end\n00000000000006e4 T _fini\n00000000000004f0 T _init\n0000000000000540 T _start\n\n...\n\n000000000000064a T main\n                 U printf@@GLIBC_2.2.5\n00000000000005b0 t register_tm_clones\n

    On observe notamment que la fonction printf est en provenance de la biblioth\u00e8que GLIBC 2.2.5, et qu'il y a une fonction main.

    strings

    Liste toutes les cha\u00eenes de caract\u00e8res imprimables dans un fichier binaire. On observe tous les symboles de d\u00e9bogue qui sont par d\u00e9faut int\u00e9gr\u00e9s au fichier ex\u00e9cutable. On lit \u00e9galement la cha\u00eene de caract\u00e8re hello, world. Attention donc \u00e0 ne pas laisser les \u00e9ventuels mots de passes ou num\u00e9ro de licence en clair dans un fichier binaire.

    $ strings a.out\n/lib64/ld-linux-x86-64.so.2\nlibc.so.6\nprintf\n\n...\n\nAUATL\n[]A\\A]A^A_\nhello, world\n;*3$\"\nGCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0\n\n...\n\n_IO_stdin_used\n__libc_csu_init\n__bss_start\nmain\n__TMC_END__\n_ITM_registerTMCloneTable\n__cxa_finalize@@GLIBC_2.2.5\n.symtab\n.strtab\n\n...\n\n.data\n.bss\n.comment\n
    size

    Lister la taille des segments m\u00e9moires utilis\u00e9s. Ici le programme repr\u00e9sente 1517 bytes, les donn\u00e9es initialis\u00e9es 8 bytes, les donn\u00e9es variables 600 bytes, soit une somme d\u00e9cimale de 2125 bytes ou 84d bytes.

    $ size a.out\ntext    data     bss     dec     hex filename\n1517     600       8    2125     84d a.out\n
    ", "tags": ["main", "printf", "size", "strings"]}, {"location": "tools/arch/filesystem/", "title": "Syst\u00e8me de fichier", "text": "

    Fourmi portant des donn\u00e9es

    Un syst\u00e8me de fichier est une structure de donn\u00e9es qui permet de stocker des fichiers et des r\u00e9pertoires sur un support de stockage. Les syst\u00e8mes de fichiers sont utilis\u00e9s pour organiser les donn\u00e9es sur les disques durs, les cl\u00e9s USB, les cartes m\u00e9moire, etc.

    Nous savons maintenant que les donn\u00e9es informatiques sont stock\u00e9es sous forme binaire. Sur une cl\u00e9 USB ou un disque dur, c'est pareil. On aura des 0 et des 1 stock\u00e9s \u00e0 perte de vue sur le silicium. Si votre disque dur de 1 Tio est la surface de la terre, sans les oc\u00e9ans et d'environ \\(148.9\\) millions de \\(\\text{km}^2\\). Notre disque contient \\(2^{40}\\) octets, soit \\(2^{43}\\) bits\u2009:

    \\[ N_{bits} = 8'796'093'022'208 \\]

    Cela revient \u00e0 stocker environ 16 bits par millim\u00e8tre carr\u00e9. Une fourmi aurait sur son dos 2 \u00e0 3 octets de donn\u00e9es. Imaginez vous un instant prendre l'avion, le train, rev\u00eatir votre tenue d'explorateur pour chercher les fourmis qui portent les donn\u00e9es que vous cherchez\u2009?

    Oui ce n'est pas tr\u00e8s r\u00e9aliste, et le calcul que nous avons fourni est tr\u00e8s simplifi\u00e9. Mais cela permet de comprendre que les donn\u00e9es stock\u00e9es doivent \u00eatre organis\u00e9es car elles ne sont pas accessibles imm\u00e9diatement. Pour un disque dur, la t\u00eate de lecture doit parcourir le disque pour atteindre les donn\u00e9es. Sur une cl\u00e9 USB, la m\u00e9moire doit \u00eatre configur\u00e9e pour acc\u00e9der une r\u00e9gion pr\u00e9cise.

    Nous avons donc besoin d'une carte pour savoir o\u00f9 sont stock\u00e9es les donn\u00e9es. C'est le r\u00f4le du syst\u00e8me de fichier. Il permet de stocker les fichiers, les organiser en dossiers. N'importe qui n'aura pas acc\u00e8s \u00e0 toutes les donn\u00e9es, il faut les droits d'acc\u00e8s. Et les donn\u00e9es peuvent \u00eatre corrompues, d\u00e9truites par des rayonnements cosmiques. Il faut aussi g\u00e9rer les erreurs.

    Le premier syst\u00e8me de fichier a \u00e9t\u00e9 invent\u00e9 par IBM en 1956 pour le disque dur IBM 305 RAMAC. Il s'appelait le syst\u00e8me de fichier \u00e0 index. Il a \u00e9t\u00e9 invent\u00e9 par Hans Peter Luhn. Il permettait de stocker 5 Mo de donn\u00e9es sur un disque de 24 pouces. Depuis

    "}, {"location": "tools/arch/filesystem/#fat-et-les-autres", "title": "FAT et les autres", "text": "

    Le syst\u00e8me de fichier FAT (File Allocation Table) est un syst\u00e8me de fichier simple et robuste. Il a \u00e9t\u00e9 invent\u00e9 par Microsoft en 1977 pour le syst\u00e8me d'exploitation MS-DOS. Il est toujours utilis\u00e9 aujourd'hui pour les cl\u00e9s USB, les cartes m\u00e9moire, etc. C'est tr\u00e8s certainement celui-ci qu'un ing\u00e9nieur embarqu\u00e9 utiliserait sur une carte SD interfac\u00e9e avec un microcontr\u00f4leur.

    L'API de FAT, c'est \u00e0 dire les fonctions primitives pour contr\u00f4ler le syst\u00e8me de fichier contient par exemple les fonctions suivantes\u2009:

    • fopen : Ouvrir un fichier \u00e0 partir d'un chemin
    • fread : Lire de donn\u00e9es depuis un fichier ouvert
    • fseek : Se d\u00e9placer dans un fichier ouvert
    • fsize : Obtenir la taille d'un fichier
    • fopendir : Ouvrir un r\u00e9pertoire
    • frename : Renommer un fichier
    • fmkdir : Cr\u00e9er un r\u00e9pertoire
    • ...

    Les syst\u00e8mes de fichiers les plus utilis\u00e9s aujourd'hui sont\u2009:

    Quelques syst\u00e8mes de fichiers Syst\u00e8me de fichier Ann\u00e9e Utilisation FAT 1977 Cl\u00e9s USB, cartes m\u00e9moire FAT32 1996 Disques durs NTFS 1993 Windows ext4 2006 Linux APFS 2017 macOS Btrfs 2009 Linux ZFS 2005 Solaris, FreeBSD", "tags": ["fopendir", "fsize", "fmkdir", "frename", "fseek", "fread", "fopen"]}, {"location": "tools/arch/filesystem/#organisation", "title": "Organisation", "text": "

    Un syst\u00e8me de fichier comporte en g\u00e9n\u00e9ral\u2009:

    • Des r\u00e9pertoires (des dossiers) qui contiennent des fichiers ou d'autres r\u00e9pertoires.
    • Des fichiers qui contiennent des donn\u00e9es.

    Les r\u00e9pertoires sont organis\u00e9s en arborescence de mani\u00e8re hi\u00e9rarchique. Si vous avez dans votre maison, dans votre cuisine, dans votre frigo, au second rayonnage, sur la droite, une pomme. Vous pourriez \u00e9crire\u2009:

    maison -> cuisine -> frigo -> rayonnage2 -> droite -> pomme\n

    Mais la pomme est un fruit, comment le savoir avec seulement un nom. C'est peut-\u00eatre une pomme de terre, une pomme de pin ou une pomme de douche. G\u00e9n\u00e9ralement, on ajoute une extension, c'est un suffixe qui permet de savoir de quel type de fichier il s'agit. Par exemple, .txt pour un fichier texte, .jpg pour une image, .c pour un fichier source en langage C, etc.

    Enfin, utiliser -> pour indiquer la hi\u00e9rarchie n'est pas pratique. Tous les syst\u00e8mes de fichiers du monde utilisent le slash / pour s\u00e9parer les r\u00e9pertoires. Tous... sauf un qui r\u00e9siste encore et toujours \u00e0 l'envahisseur. Sur Windows, la convention est diff\u00e9rente, et c'est le backslash qui est utilis\u00e9 \\. C'est une source de confusion pour les d\u00e9veloppeurs qui doivent \u00e9crire des programmes compatibles avec les deux syst\u00e8mes d'exploitation.

    Reprenons. Notre pomme serait un fruit, une image de fruit. On indiquerait alors son chemin complet\u2009:

    /maison/cuisine/frigo/rayonnage2/droite/pomme.jpg\n

    Tient\u2009? Pourquoi un slash au d\u00e9but\u2009? C'est pour indiquer le r\u00e9pertoire racine\u2009: le r\u00e9pertoire d'origine, le point de d\u00e9part de l'arborescence. Evidemment si vous vous trouvez d\u00e9j\u00e0 dans la cuisine, vous n'avez pas besoin de pr\u00e9ciser /maison/cuisine. Vous pouvez simplement \u00e9crire\u2009: frigo/rayonnage2/droite/pomme.jpg. Donc un chemin peut, ou non avoir un slash au d\u00e9but. On dit qu'il est absolu ou relatif.

    Windows

    Sur Windows, le r\u00e9pertoire racine est C:\\ pour le disque dur principal. Donc un chemin absolu sur Windows ressemblerait \u00e0\u2009:

    C:\\maison\\cuisine\\frigo\\rayonnage2\\droite\\pomme.jpg\n

    L'usage de C est historique. \u00c0 l'origine il n'y avait pas de disques durs mais des disquettes. Ceux qui avaient la chance d'avoir un lecteur de disquette avaient un unique lecteur A. Ceux qui voulaient faire des copies de disquettes devaient en avoir un deuxi\u00e8me, c'\u00e9tait le lecteur B. En g\u00e9n\u00e9ral, le disque dur \u00e9tait le troisi\u00e8me lecteur, le lecteur C. Et depuis Windows 95, c'est rest\u00e9 comme \u00e7a.

    ", "tags": ["slash"]}, {"location": "tools/arch/filesystem/#navigation", "title": "Navigation", "text": "

    Nous savons qu'un chemin est une suite de r\u00e9pertoires s\u00e9par\u00e9s par des slashes mais ce que nous ne savons pas c'est o\u00f9 on se trouve. Lorsque vous ouvrez un terminal, ou une application vous \u00eates dans un r\u00e9pertoire, c'est le r\u00e9pertoire courant, ou le r\u00e9pertoire de travail (working directory). Pour savoir o\u00f9 vous trouvez vous pouvez ouvrir un terminal et taper\u2009:

    POSIXWindows
    $ pwd\n/home/username\n
    > cd\nC:\\Users\\username\n

    Mais si je suis dans la maison et que je veux aller dans le jardin\u2009? Je ne vais pas indiquer le chemin complet\u2009:

    /universe/\n    galaxy/\n        solar-system/\n            earth/\n                europe/\n                    switzerland/\n                        vaud/\n                            yverdon-les-bains/\n                                maison/\n                                    jardin\n

    Je ne peux pas non plus utiliser le chemin relatif jardin car le jardin n'est pas dans la maison. Ni d'ailleurs /jardin car celui-ci n'est pas \u00e0 la racine de l'univers. On voit qu'il est essentiel d'introduire une notion de parent\u00e9\u2009; ce que je souhaite faire c'est remonter d'un niveau pour aller dans le jardin.

    Il existe un fichier sp\u00e9cial qui permet de remonter d'un niveau c'est le fichier ...

    Et si je suis dans le jardin et que j'aimerais tondre la pelouse. J'aimerais appeler le programme tondre en lui donnant le chemin jusque l\u00e0 o\u00f9 je me trouve. Je pourrais \u00e9crire\u2009: ../jardin mais c'est redondant, et cela implique de conna\u00eetre le nom de l\u00e0 ou je me trouve. Il existe pour cela un deuxi\u00e8me fichier sp\u00e9cial qui permet de d\u00e9signer le r\u00e9pertoire courant, c'est le fichier ..

    Enfin, si je souhaite me d\u00e9placer dans l'arborescence, je peux utiliser la commande cd pour change directory. Ce programme prend en argument un chemin absolu ou relatif.

    ", "tags": ["jardin", "tondre"]}, {"location": "tools/arch/filesystem/#commandes-utiles", "title": "Commandes utiles", "text": "POSIXWindows CMDWindows PowerShell Commande Description Exemple pwd Affiche le r\u00e9pertoire courant /home/username cd Change de r\u00e9pertoire cd /home/username ls Liste les fichiers et r\u00e9pertoires ls mkdir Cr\u00e9e un r\u00e9pertoire mkdir -p /home/username rmdir Supprime un r\u00e9pertoire rmdir /home/username touch Cr\u00e9e un fichier vide touch /home/username/file.txt rm Supprime un fichier rm /home/username/file.txt mv D\u00e9place un fichier mv /home/username/file.txt /home/username/backup/ cp Copie un fichier cp /home/username/file.txt /home/username/backup/ Commande Description Exemple cd Affiche le r\u00e9pertoire courant cd cd Change de r\u00e9pertoire cd C:\\Users\\username dir Liste les fichiers et r\u00e9pertoires dir mkdir Cr\u00e9e un r\u00e9pertoire mkdir C:\\Users\\username\\backup rmdir Supprime un r\u00e9pertoire rmdir C:\\Users\\username\\backup echo Cr\u00e9e un fichier vide echo. > C:\\Users\\username\\file.txt del Supprime un fichier del C:\\Users\\username\\file.txt move D\u00e9place un fichier move C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ copy Copie un fichier copy C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ Commande Description Exemple pwd Affiche le r\u00e9pertoire courant pwd cd Change de r\u00e9pertoire cd C:\\Users\\username ls Liste les fichiers et r\u00e9pertoires ls mkdir Cr\u00e9e un r\u00e9pertoire mkdir C:\\Users\\username\\backup rmdir Supprime un r\u00e9pertoire rmdir C:\\Users\\username\\backup echo Cr\u00e9e un fichier vide echo. > C:\\Users\\username\\file.txt rm Supprime un fichier rm C:\\Users\\username\\file.txt mv D\u00e9place un fichier mv C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ cp Copie un fichier cp C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\

    Windows

    On voit que Microsoft a appris de ses erreurs et a introduit pwd et ls dans PowerShell. C'est une bonne chose car ces commandes sont tr\u00e8s utiles. PowerShell est un shell plus moderne que CMD et plus puissant. Il est bas\u00e9 sur le framework .NET et permet d'interagir avec des objets .NET.

    Avertissement

    Les commandes rm, mv, cp sont tr\u00e8s dangereuses. Elles peuvent d\u00e9truire des donn\u00e9es. Il est important de faire attention \u00e0 ce que vous faites. Il est recommand\u00e9 de faire des sauvegardes r\u00e9guli\u00e8res de vos donn\u00e9es.

    ", "tags": ["mkdir", "rmdir", "echo", "dir", "copy", "del", "touch", "pwd", "move"]}, {"location": "tools/arch/filesystem/#permissions", "title": "Permissions", "text": "

    Les syst\u00e8mes de fichiers modernes permettent de d\u00e9finir des permissions sur les fichiers et les r\u00e9pertoires. Ces permissions permettent de contr\u00f4ler qui peut lire, \u00e9crire ou ex\u00e9cuter un fichier. Les permissions sont d\u00e9finies pour trois cat\u00e9gories d'utilisateurs\u2009:

    • Le propri\u00e9taire du fichier
    • Le groupe auquel appartient le fichier
    • Les autres utilisateurs

    Les permissions sont d\u00e9finies pour trois actions\u2009:

    • Lire le fichier
    • \u00c9crire dans le fichier
    • Ex\u00e9cuter le fichier

    Les permissions sont repr\u00e9sent\u00e9es par des lettres\u2009:

    • r pour lire
    • w pour \u00e9crire
    • x pour ex\u00e9cuter

    Les permissions sont affich\u00e9es par la commande ls -l dans POSIX. Sous Windows ce n'est pas aussi simple. Les permissions sont affich\u00e9es par la commande icacls mais le format est diff\u00e9rent.

    NTFS

    Le syst\u00e8me de fichier NTFS (New Technology File System) de Microsoft permet de d\u00e9finir des permissions tr\u00e8s fines sur les fichiers et les r\u00e9pertoires. Il permet de d\u00e9finir des permissions pour des utilisateurs individuels ou des groupes d'utilisateurs. Il permet aussi de chiffrer les donn\u00e9es pour les prot\u00e9ger contre les acc\u00e8s non autoris\u00e9s.

    Les commandes sont donc beaucoup plus complexes et ne se r\u00e9duisent pas aux permissions que l'on explique ici.

    Cette granularit\u00e9 est tr\u00e8s utile dans un environnement professionnel o\u00f9 les donn\u00e9es sont sensibles mais cela peut \u00eatre un cauchemar pour les administrateurs syst\u00e8me et surtout cela rend le syst\u00e8me plus lent car il doit v\u00e9rifier toutes les permissions \u00e0 chaque acc\u00e8s disque.

    Pour changer une permission, on utilise la commande chmod dans POSIX. Sous Windows il est pr\u00e9f\u00e9rable de passer par l'interface graphique.

    Commandes utiles pour les permissions Description Exemple Ajouter l'ex\u00e9cution sur un fichier $ chmod +x program Retirer l'ex\u00e9cution sur un fichier $ chmod -x program Ajouter l'\u00e9criture pour le groupe $ chmod g+w file Retirer l'\u00e9criture pour les autres $ chmod o-w file Ajouter la lecture pour tous $ chmod a+r file Retirer tous les droits pour les autres $ chmod o-rwx file Changer les permissions pour tous $ chmod 777 file

    Repr\u00e9sentation octale

    Dans un environnement POSIX, on peut repr\u00e9senter les permissions avec des chiffres. Chaque permission est repr\u00e9sent\u00e9e par un bit\u2009:

    • r : 4
    • w : 2
    • x : 1

    On additionne les bits pour obtenir la permission\u2009:

    • rwx : 7
    • rw- : 6
    • r-x : 5

    Chaque chiffre repr\u00e9sente dans l'ordre le propri\u00e9taire, le groupe et les autres. Ainsi la permission du fichier ~/.ssh/id_rsa est 400 ce qui signifie que le propri\u00e9taire a le droit de lire le fichier mais ni d'\u00e9crire ni d'ex\u00e9cuter. Le groupe et les autres n'ont aucun droit. C'est normal c'est la cl\u00e9 priv\u00e9e SSH, elle ne doit \u00eatre lue que par le propri\u00e9taire.

    ", "tags": ["icacls", "rwx", "chmod"]}, {"location": "tools/arch/filesystem/#proprietaire-et-groupe", "title": "Propri\u00e9taire et groupe", "text": "

    Chaque fichier a un propri\u00e9taire et un groupe. Le propri\u00e9taire est l'utilisateur qui a cr\u00e9\u00e9 le fichier. Le groupe est le groupe auquel appartient le fichier. Les utilisateurs peuvent appartenir \u00e0 plusieurs groupes. Les groupes permettent de d\u00e9finir des permissions pour plusieurs utilisateurs. Par exemple, un groupe admin pourrait avoir des droits d'\u00e9criture sur un r\u00e9pertoire.

    Pour changer le propri\u00e9taire d'un fichier, on utilise la commande chown dans POSIX. Sous Windows, il est pr\u00e9f\u00e9rable de passer par l'interface graphique.

    Commandes utiles pour les propri\u00e9taires et les groupes Description Exemple Changer le propri\u00e9taire d'un fichier $ chown username file Changer le groupe d'un fichier $ chgrp group file Ajouter un utilisateur \u00e0 un groupe $ usermod -aG group username Retirer un utilisateur d'un groupe $ gpasswd -d username group Cr\u00e9er un groupe $ groupadd group Supprimer un groupe $ groupdel group Lister les groupes d'un utilisateur $ groups username", "tags": ["admin", "chown"]}, {"location": "tools/arch/filesystem/#acl-access-control-list", "title": "ACL (Access Control List)", "text": "

    Les syst\u00e8mes de fichiers modernes permettent de d\u00e9finir des ACL (Access Control List) sur les fichiers et les r\u00e9pertoires. Les ACL permettent de d\u00e9finir des permissions plus fines que les permissions POSIX. Les ACL permettent de d\u00e9finir des permissions pour des utilisateurs individuels ou des groupes d'utilisateurs. Les ACL sont plus complexes \u00e0 g\u00e9rer que les permissions POSIX mais elles permettent de d\u00e9finir des permissions plus pr\u00e9cises.

    En g\u00e9n\u00e9ral, les ACL sont g\u00e9r\u00e9es par des outils graphiques ou des commandes sp\u00e9cifiques. Les ACL sont stock\u00e9es dans les m\u00e9tadonn\u00e9es des fichiers et des r\u00e9pertoires. Les ACL sont utilis\u00e9es dans les environnements professionnels o\u00f9 les donn\u00e9es sont sensibles et o\u00f9 il est n\u00e9cessaire de d\u00e9finir des permissions tr\u00e8s pr\u00e9cises. Cela permet de se rapprocher de syst\u00e8mes de fichiers comme NTFS de Microsoft.

    Sur votre ordinateur ou dans votre carri\u00e8re professionnelle vous n'aurez tr\u00e8s certainement jamais besoin de g\u00e9rer des ACL. C'est un sujet complexe et r\u00e9serv\u00e9 aux administrateurs syst\u00e8me. Mais pour les plus curieux voici quelques exemples\u2009:

    Description Exemple Afficher les ACL d'un fichier $ getfacl file Modifier les ACL d'un fichier $ setfacl -m u:username:rwx file Supprimer les ACL d'un fichier $ setfacl -b file Copier les ACL d'un fichier $ getfacl file1 | setfacl --set-file=- file2 Sauvegarder les ACL d'un fichier $ getfacl file > file.acl Restaurer les ACL d'un fichier $ setfacl --restore=file.acl"}, {"location": "tools/arch/filesystem/#manipulation-bas-niveau", "title": "Manipulation bas niveau", "text": "

    Une question que l'on peut se poser est s'il est possible d'acc\u00e9der aux donn\u00e9es brutes d'un disque dur (les 1 et les 0) : la r\u00e9ponse est oui. Il est possible d'acc\u00e9der aux donn\u00e9es brutes d'un disque dur en utilisant des outils sp\u00e9cifiques. Ces outils permettent de lire et d'\u00e9crire des donn\u00e9es directement sur le disque dur sans passer par le syst\u00e8me de fichiers.

    La commande dd dans POSIX permet de lire et d'\u00e9crire des donn\u00e9es brutes sur un disque dur. Cette commande est tr\u00e8s puissante et tr\u00e8s dangereuse. Elle permet de lire et d'\u00e9crire des donn\u00e9es directement sur le disque dur. Il est tr\u00e8s facile de d\u00e9truire des donn\u00e9es avec cette commande.

    On peut utiliser des fichiers sp\u00e9ciaux comme /dev/zero ou /dev/random pour g\u00e9n\u00e9rer des fichiers de donn\u00e9es.

    Par exemple cr\u00e9er un fichier de 1 Mio rempli de z\u00e9ros\u2009:

    $ dd if=/dev/zero of=file bs=1M count=1\n

    Ou cr\u00e9er un fichier de 100 bytes rempli de donn\u00e9es al\u00e9atoires\u2009:

    $ dd if=/dev/random of=file bs=100 count=1\n

    On peut aussi lire des donn\u00e9es brutes sur un disque dur. Par exemple lire les 100 premiers bytes d'un disque dur\u2009:

    $ dd if=/dev/sda of=file bs=100 count=1\n
    "}, {"location": "tools/arch/filesystem/#arborescence-posix", "title": "Arborescence POSIX", "text": "

    L'arborescence POSIX est une convention pour organiser les fichiers et les r\u00e9pertoires sur un syst\u00e8me de fichiers. L'arborescence POSIX est utilis\u00e9e par la plupart des syst\u00e8mes d'exploitation bas\u00e9s sur UNIX. L'arborescence POSIX est organis\u00e9e de la mani\u00e8re suivante\u2009:

    • / : Le r\u00e9pertoire racine
    • /bin : Les programmes de base
    • /boot : Les fichiers de d\u00e9marrage
    • /dev : Les fichiers de p\u00e9riph\u00e9riques
    • /etc : Les fichiers de configuration
    • /home : Les r\u00e9pertoires des utilisateurs
    • /lib : Les biblioth\u00e8ques partag\u00e9es
    • /media : Les points de montage des p\u00e9riph\u00e9riques amovibles
    • /mnt : Les points de montage des p\u00e9riph\u00e9riques
    • /opt : Les logiciels optionnels
    • /proc : Les informations sur les processus
    • /root : Le r\u00e9pertoire de l'administrateur
    • /tmp : Les fichiers temporaires
    • /usr : Les programmes et les fichiers partag\u00e9s
    • /var : Les fichiers variables
    • /srv : Les donn\u00e9es des services
    • /sys : Les informations sur le noyau

    Windows

    Malheureusement Windows n'a pas vraiment de convention rigide pour l'organisation des fichiers et des r\u00e9pertoires. Chaque programme peut d\u00e9cider o\u00f9 il veut stocker ses fichiers de configuration, ses fichiers de donn\u00e9es, etc. Cela rend la maintenance et la sauvegarde des donn\u00e9es plus difficile. On peut n\u00e9anmoins retrouver quelques r\u00e9pertoires communs\u2009:

    • C:\\Program Files : Les programmes install\u00e9s
    • C:\\Program Files (x86) : Les programmes 32 bits install\u00e9s sur un syst\u00e8me 64 bits
    • C:\\Users : Les r\u00e9pertoires des utilisateurs
    • C:\\Windows : Les fichiers du syst\u00e8me d'exploitation
    • C:\\Windows\\System32 : Les fichiers du syst\u00e8me d'exploitation 64 bits
    • C:\\Windows\\SysWOW64 : Les fichiers du syst\u00e8me d'exploitation 32 bits sur un syst\u00e8me 64 bits
    • C:\\Temp : Les fichiers temporaires
    • C:\\Users\\username\\AppData : Les fichiers de configuration des programmes
    • C:\\Users\\username\\Documents : Les documents de l'utilisateur
    • C:\\Users\\username\\Downloads : Les fichiers t\u00e9l\u00e9charg\u00e9s
    "}, {"location": "tools/build-system/cmake/", "title": "CMake", "text": ""}, {"location": "tools/build-system/cmake/#introduction", "title": "Introduction", "text": "

    CMake, cr\u00e9ature singuli\u00e8re dans l\u2019univers du d\u00e9veloppement logiciel, est un outil de compilation destin\u00e9 \u00e0 automatiser et simplifier la g\u00e9n\u00e9ration des fichiers de configuration n\u00e9cessaires \u00e0 la compilation d\u2019un projet. N\u00e9 \u00e0 la fin des ann\u00e9es 1990, \u00e0 l\u2019\u00e9poque o\u00f9 la prolif\u00e9ration des syst\u00e8mes d\u2019exploitation et des plateformes mat\u00e9rielles complexifiait le processus de construction des logiciels, CMake s\u2019inscrit dans une qu\u00eate pragmatique\u2009: rendre le d\u00e9veloppement multi-plateformes plus fluide et plus unifi\u00e9.

    \u00c0 une \u00e9poque o\u00f9 les syst\u00e8mes de build comme Make \u00e9taient omnipr\u00e9sents, ces outils \u00e9taient souvent tr\u00e8s sp\u00e9cifiques \u00e0 un syst\u00e8me particulier, et les d\u00e9veloppeurs devaient \u00e9crire des fichiers de configuration diff\u00e9rents pour chaque environnement. CMake a donc \u00e9t\u00e9 con\u00e7u par Kitware, avec pour objectif de g\u00e9n\u00e9rer des fichiers de build qui pourraient s\u2019adapter \u00e0 diff\u00e9rents compilateurs et plateformes sans avoir \u00e0 r\u00e9\u00e9crire manuellement de nombreuses configurations. Ce besoin est apparu principalement dans le cadre de projets de grande envergure, tels que les logiciels scientifiques ou industriels, qui devaient tourner aussi bien sur des syst\u00e8mes Unix que sur Windows ou encore macOS.

    D\u00e8s ses d\u00e9buts, CMake s\u2019est impos\u00e9 comme une solution de configuration multi-plateforme flexible. Son avantage par rapport \u00e0 ses concurrents r\u00e9side dans sa capacit\u00e9 \u00e0 g\u00e9n\u00e9rer des fichiers de construction adapt\u00e9s aux environnements vari\u00e9s, que ce soit pour Make, Ninja, ou encore des outils sp\u00e9cifiques \u00e0 Visual Studio. Cette abstraction permet de s\u2019affranchir de l'\u00e9criture manuelle de fichiers de build complexes pour chaque plateforme.

    Compar\u00e9 \u00e0 ses concurrents directs comme Autotools, CMake brille par sa relative simplicit\u00e9 \u00e0 prendre en main et \u00e0 configurer des projets multi-syst\u00e8mes, tout en restant assez puissant pour s\u2019adapter \u00e0 des projets de grande envergure. Contrairement \u00e0 des outils plus anciens, il se concentre sur l'abstraction du processus de construction et permet de g\u00e9rer plus facilement les d\u00e9pendances externes et internes d'un projet. En cela, CMake surpasse souvent les syst\u00e8mes de build traditionnels par sa flexibilit\u00e9.

    Cependant, s\u2019il poss\u00e8de un net avantage pour un projet complexe et multi-plateforme, il souffre aussi de certains travers. Et c\u2019est ici que les d\u00e9veloppeurs grimacent souvent.

    "}, {"location": "tools/build-system/cmake/#une-syntaxe-austere-et-rebutante", "title": "Une syntaxe aust\u00e8re et rebutante", "text": "

    L\u2019un des reproches les plus fr\u00e9quents adress\u00e9s \u00e0 CMake est sa syntaxe. Elle est souvent d\u00e9crite comme aust\u00e8re, voire \u00ab\u2009immonde\u2009\u00bb, pour reprendre l\u2019expression que beaucoup lui pr\u00eatent dans des cercles de d\u00e9veloppeurs chevronn\u00e9s. La langue de CMake est une sorte de dialecte d\u00e9riv\u00e9 d\u2019une forme rudimentaire de script, mais dont la lisibilit\u00e9 et la clart\u00e9 laissent \u00e0 d\u00e9sirer.

    Prenons par exemple un fichier typique de configuration, appel\u00e9 CMakeLists.txt. Ce fichier est une s\u00e9quence de directives qui sont interpr\u00e9t\u00e9es pour g\u00e9n\u00e9rer les fichiers de build. Voici un exemple minimaliste\u2009:

    cmake_minimum_required(VERSION 3.15)\nproject(MonProjet LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 17)\nset(SOURCES main.cpp util.cpp)\n\nadd_executable(MonExecutable ${SOURCES})\n

    \u00c0 premi\u00e8re vue, il peut sembler relativement simple. Mais lorsqu\u2019on plonge dans un projet plus cons\u00e9quent, la syntaxe devient rapidement verbeuse et d\u00e9nu\u00e9e de la concision ou de l\u2019\u00e9l\u00e9gance syntaxique qu\u2019on pourrait esp\u00e9rer dans un langage de configuration moderne. Les fonctions sont souvent longues et les macros omnipr\u00e9sentes, rendant difficile le d\u00e9bogage et la maintenance d\u2019un script CMake complexe.

    Compar\u00e9 \u00e0 des outils modernes comme Meson, qui se veut plus concis et plus explicite, CMake trahit son origine dans les ann\u00e9es 1990. Il conserve un certain bagage historique, une sorte de legacy syntaxique qui, \u00e0 d\u00e9faut d\u2019\u00eatre intuitive, a tout de m\u00eame l\u2019avantage d\u2019\u00eatre \u00e9prouv\u00e9e et tr\u00e8s bien document\u00e9e.

    ", "tags": ["CMakeLists.txt"]}, {"location": "tools/build-system/cmake/#exemple", "title": "Exemple", "text": "

    L'utilisation de CMake, bien que l\u00e9g\u00e8rement d\u00e9routante au d\u00e9part, suit des principes relativement simples. Voici comment se d\u00e9roule typiquement le processus\u2009:

    1. \u00c9crire le fichier CMakeLists.txt : Ce fichier contient les instructions qui d\u00e9crivent les sources, les d\u00e9pendances, les biblioth\u00e8ques, et d\u2019autres options de configuration.

    2. G\u00e9n\u00e9rer les fichiers de build :

      Depuis la ligne de commande, vous ex\u00e9cutez une commande comme\u2009:

      cmake -S . -B build\n

      Cela cr\u00e9e un r\u00e9pertoire build o\u00f9 sont plac\u00e9s les fichiers de configuration pour l\u2019outil de construction choisi (Make, Ninja, etc.).

    3. Compiler le projet :

      Vous pouvez alors ex\u00e9cuter\u2009:

      cmake --build build\n

      Et CMake utilisera les fichiers g\u00e9n\u00e9r\u00e9s pour compiler votre projet.

    4. Facilit\u00e9 de gestion des d\u00e9pendances :

      L\u2019un des atouts majeurs de CMake r\u00e9side dans sa gestion des d\u00e9pendances via find_package(), qui permet de d\u00e9tecter et lier automatiquement les biblioth\u00e8ques externes.

    Par exemple, si vous voulez lier votre projet \u00e0 Boost, vous pouvez simplement ajouter ceci dans votre CMakeLists.txt :

    find_package(Boost 1.75 REQUIRED)\ntarget_link_libraries(MonExecutable Boost::Boost)\n

    C\u2019est cette capacit\u00e9 \u00e0 manipuler ais\u00e9ment des d\u00e9pendances complexes qui a permis \u00e0 CMake de supplanter d\u2019autres syst\u00e8mes.

    ", "tags": ["build", "CMakeLists.txt"]}, {"location": "tools/build-system/cmake/#conclusion", "title": "Conclusion", "text": "

    En d\u00e9finitive, CMake incarne un compromis d\u00e9licat entre flexibilit\u00e9 et simplicit\u00e9. N\u00e9 d\u2019un besoin imp\u00e9rieux de rationaliser la compilation multi-plateforme, il a su s\u2019imposer comme une r\u00e9f\u00e9rence malgr\u00e9 ses imperfections syntaxiques. Si sa syntaxe d\u00e9plait \u00e0 beaucoup et peut rebuter au premier abord, il n\u2019en reste pas moins que ses capacit\u00e9s \u00e0 g\u00e9rer des projets d\u2019envergure, \u00e0 s\u2019adapter \u00e0 une multitude de syst\u00e8mes et \u00e0 orchestrer la compilation de mani\u00e8re automatis\u00e9e en font un outil indispensable dans le paysage actuel du d\u00e9veloppement logiciel.

    Un outil imparfait, certes, mais ind\u00e9niablement efficace dans sa mission.

    "}, {"location": "tools/build-system/make/", "title": "Make", "text": ""}, {"location": "tools/build-system/make/#introduction", "title": "Introduction", "text": "

    make est un outil de gestion de projet qui permet de compiler des programmes C/C++ de mani\u00e8re efficace. make utilise un fichier Makefile qui contient les r\u00e8gles de compilation.

    Le langage Make est tr\u00e8s ancien (1976) et n'est pas tr\u00e8s lisible aux yeux des d\u00e9butants. Cependant, il est tr\u00e8s puissant et permet de g\u00e9rer des projets de grande envergure. Voici un exemple de Makefile g\u00e9n\u00e9rique pour compiler un programme en C\u2009:

    CC=gcc\nCFLAGS=-std=c17 -O3 -Wall -Werror -pedantic\nLDFLAGS=-lm\nEXEC=main\nSRCS=$(wildcard *.c)\nOBJS=$(SRCS:.c=.o)\n\nall: $(EXEC)\n\n-include $(OBJS:.o=.d)\n\n$(EXEC): $(OBJS)\n    $(CC) -o $@ $^ $(LDFLAGS)\n\n%.o: %.c\n    $(CC) -o $@ -c $< $(CFLAGS) -MMD -MP\n\nclean:\n    $(RM) -f $(OBJS) $(EXEC) $(OBJS:.o=.d)\n\n.PHONY: all clean\n

    Make utilise des variables sp\u00e9ciales et l'appel de macros. Les variables sont d\u00e9finies avec VAR=valeur et appel\u00e9es avec $(VAR).

    Variables Make Variable Description CC Compilateur (convention) CFLAGS Options de compilation (convention) LDFLAGS Options d'\u00e9dition de liens (convention) EXEC Nom du fichier ex\u00e9cutable (convention) SRCS Liste des fichiers sources OBJS Liste des fichiers objets $@ Nom de la cible $^ Liste des d\u00e9pendances $< Premi\u00e8re d\u00e9pendance %.o Jalon g\u00e9n\u00e9rique pour tous les fichiers en .o $(wildcard *.foo) Liste des fichiers .foo dans le r\u00e9pertoire courant $(RM) Commande de suppression de fichiers sur le syst\u00e8me courant

    Le fonctionnement de Make est en soi assez simple. Des r\u00e8gles sont d\u00e9finies avec cible: d\u00e9pendances et les commandes \u00e0 ex\u00e9cuter pour g\u00e9n\u00e9rer la cible.

    Dans l'exemple ci-dessus, la r\u00e8gle all d\u00e9pend de $(EXEC), autrement dit le fichier main. Pour g\u00e9n\u00e9rer main, Make recherche une autre r\u00e8gle du m\u00eame nom. Il trouve main: $(OBJS) qui d\u00e9pend de tous les fichiers objets. Pour g\u00e9n\u00e9rer un fichier objet, Make recherche une autre r\u00e8gle permettant de g\u00e9n\u00e9rer les fichiers objets n\u00e9cessaires. Il trouve %.o: %.c qui d\u00e9pend de tous les fichiers sources. Une fois les fichiers objets g\u00e9n\u00e9r\u00e9s, Make peut g\u00e9n\u00e9rer l'ex\u00e9cutable main.

    Make fonctionne de mani\u00e8re incr\u00e9mentale. Si un fichier source est modifi\u00e9, Make ne recompile que les fichiers objets n\u00e9cessaires. Cela permet de gagner du temps lors du d\u00e9veloppement. Il se base sur les dates de modification des fichiers pour d\u00e9terminer si un fichier doit \u00eatre recompil\u00e9 ou non.

    Ce mode de fonctionnement peut cr\u00e9er des probl\u00e8mes avec les fichiers d'en-t\u00eate. En effet, si un fichier d'en-t\u00eate est modifi\u00e9, Make ne recompile pas les fichiers sources qui incluent ce fichier d'en-t\u00eate puisque les fichiers d'en-t\u00eate n'apparaissent dans aucune r\u00e8gles. Pour r\u00e9soudre ce probl\u00e8me, il est possible de g\u00e9n\u00e9rer des fichiers de d\u00e9pendances avec l'option -MMD -MP du compilateur GCC. Ces fichiers de d\u00e9pendances sont inclus dans le Makefile avec l'option !include $(OBJS:.o=.d). Ils contiennent la liste des fichiers d'en-t\u00eate inclus par chaque fichier source qui indique par exemple que l'objet main.o d\u00e9pend du fichier add.h. Ainsi, si add.h est modifi\u00e9, Make recompile main.o et reg\u00e9n\u00e8re l'ex\u00e9cutable main.

    ", "tags": ["EXEC", "SRCS", "make", "OBJS", "all", "main", "LDFLAGS", "add.h", "Makefile", "main.o", "CFLAGS"]}, {"location": "tools/build-system/make/#utilisation", "title": "Utilisation", "text": "

    Pour utiliser Make, il suffit de cr\u00e9er un fichier Makefile dans le r\u00e9pertoire du projet. Ensuite, il suffit de taper la commande make dans un terminal pour compiler le projet. Pour nettoyer les fichiers temporaires, il suffit de taper la commande make clean.

    Voici l'exemple de cr\u00e9ation d'un programme en C avec deux fichiers C et un fichier d'en-t\u00eate\u2009:

    main.cadd.hadd.c
    #include \"add.h\"\n#include <stdio.h>\n\nint main() {\n    const int a = 2, b = 3;\n    printf(\"Somme de %d+%d = %d\\n\", a, b, add(a, b));\n}\n
    #pragma once\nint add(int a, int b);\n
    int add(int a, int b) {\n    return a + b;\n}\n

    Votre Makefile devrait ressembler \u00e0 ceci\u2009:

    CC=gcc\nCFLAGS=-std=c17 -O3 -Wall -Werror -pedantic\nLDFLAGS=-lm # Si vous utilisez la librairie math\u00e9matique\nEXEC=main\n\nall: $(EXEC)\n\n$(EXEC): main.o add.o\n    $(CC) -o $@ $^ $(LDFLAGS)\n\n%.o: %.c | add.h\n    $(CC) -o $@ -c $< $(CFLAGS)\n\nclean:\n    $(RM) -f *.o $(EXEC)\n
    ", "tags": ["make", "Makefile"]}, {"location": "tools/build-system/meson/", "title": "Meson", "text": ""}, {"location": "tools/build-system/meson/#quest-ce-que-meson", "title": "Qu'est-ce que Meson\u2009?", "text": "

    Meson est un syst\u00e8me de build open-source con\u00e7u pour \u00eatre simple, rapide, et multiplateforme. Il est utilis\u00e9 principalement pour g\u00e9rer des projets en C, C++, mais il supporte \u00e9galement d'autres langages comme Python, Java, Rust, et plus encore. Meson a \u00e9t\u00e9 cr\u00e9\u00e9 avec un objectif clair\u2009: acc\u00e9l\u00e9rer le processus de compilation, tout en simplifiant la gestion des projets et en r\u00e9duisant les probl\u00e8mes li\u00e9s \u00e0 la configuration.

    Il g\u00e9n\u00e8re des fichiers de build pour diff\u00e9rents syst\u00e8mes de build, comme Ninja (qui est le plus couramment utilis\u00e9 avec Meson pour sa vitesse). Contrairement \u00e0 des outils comme CMake ou Autotools, Meson offre une syntaxe de configuration plus simple et plus lisible, et il g\u00e8re mieux les projets complexes gr\u00e2ce \u00e0 sa gestion automatique des d\u00e9pendances.

    "}, {"location": "tools/build-system/meson/#histoire-et-motivation-derriere-meson", "title": "Histoire et motivation derri\u00e8re Meson", "text": "

    Meson a \u00e9t\u00e9 cr\u00e9\u00e9 par Jussi Pakkanen en 2012, avec pour objectif de r\u00e9pondre \u00e0 plusieurs limitations des outils de build traditionnels comme Make, Autotools ou m\u00eame CMake. Il visait \u00e0 r\u00e9soudre des probl\u00e8mes de complexit\u00e9 excessive, de vitesse de compilation, et \u00e0 offrir une meilleure exp\u00e9rience utilisateur avec\u2009:

    1. Une syntaxe plus claire et concise pour les fichiers de configuration.
    2. Une meilleure int\u00e9gration avec Ninja pour des builds plus rapides.
    3. Un support am\u00e9lior\u00e9 pour la compilation incr\u00e9mentale.
    4. Une portabilit\u00e9 facile entre Linux, Windows, et macOS.
    5. Une gestion simplifi\u00e9e des d\u00e9pendances et des sous-projets.

    Meson a \u00e9t\u00e9 rapidement adopt\u00e9 par de grands projets open-source, tels que GNOME, X.org, GStreamer, ou encore systemd, en raison de sa simplicit\u00e9 et de ses performances.

    "}, {"location": "tools/build-system/meson/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    Meson fonctionne en deux \u00e9tapes principales\u2009:

    1. Configuration : Meson configure le projet, g\u00e9n\u00e8re un ensemble de fichiers de build pour Ninja (ou d'autres syst\u00e8mes), et v\u00e9rifie les d\u00e9pendances et les param\u00e8tres du compilateur.
    2. Build : Ninja (ou un autre backend) est ensuite utilis\u00e9 pour compiler et lier les fichiers sources.

    Voici quelques caract\u00e9ristiques cl\u00e9s\u2009:

    • Syntaxe simple : Les fichiers meson.build sont simples \u00e0 \u00e9crire et \u00e0 comprendre.
    • Multiplateforme : Supporte les syst\u00e8mes Linux, macOS, Windows, etc.
    • Optimisation du parall\u00e9lisme : Utilise Ninja pour tirer parti des syst\u00e8mes multi-c\u0153urs.
    • Configuration rapide : Par rapport \u00e0 des outils comme CMake ou Autotools, Meson est con\u00e7u pour des configurations plus rapides.
    • Test et Benchmark : Meson int\u00e8gre directement des outils pour les tests unitaires et les benchmarks.
    ", "tags": ["meson.build"]}, {"location": "tools/build-system/meson/#installation", "title": "Installation", "text": "

    Meson est \u00e9crit en Python, donc son installation est simple avec pip :

    pip install meson\n

    Ou sur certaines distributions Linux comme Ubuntu\u2009:

    sudo apt-get install meson\n

    Sur macOS avec Homebrew\u2009:

    brew install meson\n
    ", "tags": ["pip"]}, {"location": "tools/build-system/meson/#utilisation", "title": "Utilisation", "text": "

    Meson utilise des fichiers appel\u00e9s meson.build pour d\u00e9finir la configuration de build. Prenons l'exemple d'un projet typique ayant la structure suivante\u2009:

    project/\n  src/\n    main.c\n    util.c\n    util.h\n  meson.build\n  meson_options.txt\n
    ", "tags": ["meson.build"]}, {"location": "tools/build-system/meson/#exemple", "title": "Exemple", "text": "

    Le fichier meson.build utilise la syntaxe Meson bas\u00e9e sur Python pour configurer le projet. Voici un exemple simple\u2009:

    project('super-rocket', 'c',\n  version: '1.0',\n  default_options: ['warning_level=3', 'c_std=gnu11'])\n\nexecutable('super-rocket',\n  sources: ['src/main.c', 'src/server.c'],\n  dependencies: [dependency('libuv')]\n)\n

    Frustrations

    Les d\u00e9veloppeurs de meson ont fait quelques choix que vous pourriez trouver frustrants. Par exemple, les fichiers de configuration sont \u00e9crits en Python, mais ils ne sont pas des scripts Python valides. Cela signifie que vous ne pouvez pas utiliser de variables, de boucles ou de conditions Python dans les fichiers de configuration.

    Les param\u00e8tres de configuration sont des cha\u00eenes de caract\u00e8res dont aucune aide possible n'est fournie par l'IDE. Cela peut rendre la configuration plus difficile pour les nouveaux utilisateurs.

    La liste des fichiers source doit \u00eatre \u00e9crite manuellement, ce qui peut \u00eatre fastidieux pour les projets de grande taille. Il n'y a pas de moyen d'utiliser des expressions r\u00e9guli\u00e8res ou un glob (comme *.c) pour inclure automatiquement tous les fichiers source. C'est un choix d\u00e9lib\u00e9r\u00e9 pour \u00e9viter les probl\u00e8mes de r\u00e9p\u00e9tabilit\u00e9, mais cela peut \u00eatre un inconv\u00e9nient pour certains projets.

    Ce fichier fait plusieurs choses\u2009: il d\u00e9finit le nom du projet (my_program) et le langage utilis\u00e9 (C), ainsi que quelques options par d\u00e9faut, il sp\u00e9cifie que l'ex\u00e9cutable my_program sera construit \u00e0 partir des fichiers main.c et server.c enfin, il sp\u00e9cifie que le programme d\u00e9pend de la biblioth\u00e8que libuv, en utilisant dependency('libuv') pour lier automatiquement cette biblioth\u00e8que au projet.

    Nous prendrons pour l'exemple les fichiers suivants\u2009:

    • Le fichier main.c est le point d'entr\u00e9e du programme.

      #include <stdio.h>\n#include \"server.h\"\n\nint main() { start_server(); }\n
    • Le fichier server.h d\u00e9clare la fonction start_server.

      #pragma once\nvoid start_server();\n
    • Le fichier server.c impl\u00e9mente la logique du serveur, en utilisant libuv pour cr\u00e9er un serveur TCP.

      #include <stdio.h>\n#include <stdlib.h>\n#include <uv.h> // Requires -std=gnu11\n#include \"server.h\"\n\nstatic void on_new_connection(uv_stream_t *server, int status) {\n    if (status < 0) {\n        fprintf(stderr, \"New connection error %s\\n\", uv_strerror(status));\n        return;\n    }\n    uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));\n    uv_tcp_init(uv_default_loop(), client);\n    if (uv_accept(server, (uv_stream_t*) client) == 0)\n        printf(\"Accepted new connection\\n\");\n    else\n        uv_close((uv_handle_t*) client, NULL);\n}\n\nvoid start_server() {\n    uv_tcp_t server;\n    uv_tcp_init(uv_default_loop(), &server);\n    struct sockaddr_in addr;\n    uv_ip4_addr(\"0.0.0.0\", 7000, &addr);\n    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);\n    int r = uv_listen((uv_stream_t*) &server, 128, on_new_connection);\n    if (r) {\n        fprintf(stderr, \"Listen error %s\\n\", uv_strerror(r));\n        return;\n    }\n    printf(\"Listening on port 7000\\n\");\n    uv_run(uv_default_loop(), UV_RUN_DEFAULT);\n}\n

    Si aucun meson.build n'existe, vous pouvez en cr\u00e9er un en utilisant la commande meson init :

    meson init\n

    Pour compiler ce projet on appelle la commande meson avec l'option setup pour configurer le projet, puis compile pour compiler les fichiers sources\u2009:

    $ meson setup builddir\nThe Meson build system\nVersion: 1.3.2\nSource dir: /home/ycr/meson-test\nBuild dir: /home/ycr/meson-test/builddir\nBuild type: native build\nProject name: super-rocket\nProject version: 1.0\nC compiler for the host machine: cc (gcc 13.2.0\n    \"cc (Ubuntu 13.2.0-23ubuntu4) 13.2.0\")\nC linker for the host machine: cc ld.bfd 2.42\nHost machine cpu family: x86_64\nHost machine cpu: x86_64\nFound pkg-config: YES (/usr/bin/pkg-config) 1.8.1\nRun-time dependency libuv found: YES 1.48.0\nRun-time dependency threads found: YES\nBuild targets in project: 1\n\nFound ninja-1.11.1 at /usr/bin/ninja\n

    Cette commande configure le projet dans un r\u00e9pertoire de build (builddir) et g\u00e9n\u00e8re les fichiers n\u00e9cessaires pour Ninja qui est le backend par d\u00e9faut de Meson. Ensuite, on peut compiler le projet avec\u2009:

    $ meson compile -C builddir\nINFO: autodetecting backend as ninja\nINFO: calculating backend command to run: /usr/bin/ninja\n    -C /home/ycr/meson-test/builddir\nninja: Entering directory `/home/ycr/meson-test/builddir'\n[3/3] Linking target super-rocket\n

    De la m\u00eame mani\u00e8re que CMake, meson utilise un r\u00e9pertoire de build s\u00e9par\u00e9 pour isoler les fichiers g\u00e9n\u00e9r\u00e9s du code source. Cela permet de nettoyer facilement les fichiers de build en supprimant simplement le r\u00e9pertoire de build, mais cela demande une \u00e9tape suppl\u00e9mentaire lors du build.

    Une fois compil\u00e9, le programme peut \u00eatre ex\u00e9cut\u00e9\u2009:

    ./builddir/super-rocket\n

    Cela d\u00e9marre le serveur, et il \u00e9coutera les connexions TCP sur le port 7000.

    Extension vscode

    Une extension Meson existe pour Visual Studio Code, qui fournit une coloration syntaxique pour les fichiers meson.build et des fonctionnalit\u00e9s de compl\u00e9tion automatique pour les options Meson.

    ", "tags": ["my_program", "server.h", "compile", "start_server", "meson.build", "setup", "meson", "server.c", "main.c", "builddir"]}, {"location": "tools/build-system/meson/#cheatsheet", "title": "Cheatsheet", "text": "

    Voici une liste de commandes Meson couramment utilis\u00e9es\u2009:

    Commande Meson Description meson setup builddir Configure le projet dans le r\u00e9pertoire de build builddir. meson compile -C builddir Compile le projet dans le r\u00e9pertoire de build builddir. meson test -C builddir Ex\u00e9cute les tests unitaires du projet. meson install -C builddir Installe les fichiers du projet sur le syst\u00e8me. meson configure -Doption=value Configure le projet avec une option sp\u00e9cifique. meson introspect Affiche des informations sur le projet.", "tags": ["builddir"]}, {"location": "tools/build-system/ninja/", "title": "Ninja", "text": "

    Ninja est un outil de build syst\u00e8me con\u00e7u pour \u00eatre rapide. Il est souvent utilis\u00e9 pour compiler de gros projets C/C++ comme LLVM, Chromium ou Android. Ninja est \u00e9crit en C++ et est distribu\u00e9 sous licence Apache 2.0. Il a \u00e9t\u00e9 con\u00e7u pour g\u00e9rer efficacement de tr\u00e8s gros projets, tels que Chromium, LLVM ou Android, o\u00f9 des milliers de fichiers peuvent \u00eatre compil\u00e9s et li\u00e9s. Il a \u00e9t\u00e9 cr\u00e9\u00e9 en tant qu'alternative \u00e0 Make, pour r\u00e9soudre des probl\u00e8mes de performance dans des sc\u00e9narios de compilation \u00e0 grande \u00e9chelle. Ninja est particuli\u00e8rement optimis\u00e9 pour la vitesse d'ex\u00e9cution, en \u00e9vitant de faire plus de travail que n\u00e9cessaire.

    L'objectif de Ninja est simple\u2009: minimiser le temps n\u00e9cessaire \u00e0 la compilation incr\u00e9mentale, c'est-\u00e0-dire le temps requis pour recompiler uniquement les fichiers qui ont \u00e9t\u00e9 modifi\u00e9s ou leurs d\u00e9pendances, sans avoir \u00e0 recompiler tout le projet.

    "}, {"location": "tools/build-system/ninja/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    Ninja fonctionne avec des fichiers de build appel\u00e9s build.ninja, qui contiennent les r\u00e8gles de compilation. Ces fichiers sont semblables aux Makefile, mais beaucoup plus simples et compacts. Voici comment fonctionne Ninja\u2009:

    1. Fichiers d'entr\u00e9e simplifi\u00e9s : Ninja ne g\u00e9n\u00e8re pas ses fichiers de configuration de mani\u00e8re manuelle, contrairement \u00e0 Make, o\u00f9 les d\u00e9veloppeurs doivent souvent \u00e9crire de longs et complexes Makefile. Les fichiers build.ninja sont g\u00e9n\u00e9ralement g\u00e9n\u00e9r\u00e9s par un outil de configuration externe (comme CMake, GN ou Meson).

    2. Graphes de d\u00e9pendance : Ninja cr\u00e9e un graphe des d\u00e9pendances pour d\u00e9terminer quelles parties du projet doivent \u00eatre reconstruites en fonction des changements dans les fichiers source.

    3. Compilation incr\u00e9mentale rapide : Ninja d\u00e9tecte les fichiers modifi\u00e9s et ne recompile que ce qui est n\u00e9cessaire. Il optimise \u00e9galement le parall\u00e9lisme en construisant plusieurs fichiers simultan\u00e9ment, en fonction des c\u0153urs CPU disponibles.

    4. Pas de fonctionnalit\u00e9s inutiles : Ninja se concentre uniquement sur le build. Il n\u2019inclut pas de fonctionnalit\u00e9s additionnelles comme l'installation, le nettoyage (clean), etc., qui sont souvent pr\u00e9sentes dans d'autres syst\u00e8mes comme Make. Il d\u00e9l\u00e8gue ces t\u00e2ches \u00e0 des outils externes.

    ", "tags": ["Makefile", "build.ninja"]}, {"location": "tools/build-system/ninja/#avantages-par-rapport-a-make", "title": "Avantages par rapport \u00e0 Make", "text": "
    1. Vitesse : Ninja est con\u00e7u pour \u00eatre plus rapide que Make, particuli\u00e8rement pour les builds incr\u00e9mentaux. Il minimise le temps de reconfiguration et de relance des builds en \u00e9vitant des v\u00e9rifications inutiles.

    2. Optimisation du parall\u00e9lisme : Ninja est plus efficace que Make pour tirer parti des syst\u00e8mes multi-c\u0153urs. Il calcule mieux les d\u00e9pendances et s'assure que le maximum de t\u00e2ches est lanc\u00e9 en parall\u00e8le.

    3. Simplicit\u00e9 des fichiers de build : Les fichiers build.ninja sont simples, \u00e9pur\u00e9s et plus rapides \u00e0 lire pour l'outil. Cela contraste avec les Makefile, souvent longs et difficiles \u00e0 maintenir.

    4. Moins d'ambigu\u00eft\u00e9 : Ninja \u00e9vite certains pi\u00e8ges de Make, comme la n\u00e9cessit\u00e9 de suivre des conventions implicites. Ninja est explicite et ne laisse pas de place \u00e0 des comportements ambigus.

    5. Aucun overhead de fonctionnalit\u00e9s inutiles : Ninja se concentre exclusivement sur la compilation et ne se charge pas d'autres t\u00e2ches comme l'installation ou le nettoyage. Cela r\u00e9duit la complexit\u00e9 du processus de build.

    6. Build d\u00e9terministe : Ninja est con\u00e7u pour \u00eatre d\u00e9terministe, c'est-\u00e0-dire que si l'\u00e9tat du syst\u00e8me est identique, le build produit sera identique, ce qui n'est pas toujours le cas avec Make.

    Ci-dessous un tableau comparatif entre Make et Ninja.

    Comparaison entre Make et Ninja Caract\u00e9ristiques Make Ninja Performance Plus lent pour les gros projets Con\u00e7u pour \u00eatre extr\u00eamement rapide Configuration Fichiers Makefile manuellement \u00e9crits Fichiers build.ninja souvent meta-g\u00e9n\u00e9r\u00e9s Parall\u00e9lisme Support, mais moins optimis\u00e9 Tr\u00e8s performant avec des builds parall\u00e8les Complexit\u00e9 Peut \u00eatre tr\u00e8s complexe et verbeux Fichiers simples et explicites Objectif Outil g\u00e9n\u00e9raliste Con\u00e7u uniquement pour la compilation rapide D\u00e9pendances Manuelle, souvent avec des outils externes D\u00e9tection automatique des d\u00e9pendances Fonctionnalit\u00e9s Supporte des t\u00e2ches additionnelles Focus uniquement sur la compilation", "tags": ["Makefile", "build.ninja"]}, {"location": "tools/build-system/ninja/#installation", "title": "Installation", "text": "

    Pour installer Ninja, il suffit de t\u00e9l\u00e9charger l'ex\u00e9cutable ou d'utiliser un gestionnaire de paquets. Par exemple\u2009:

    UbuntumacOSWindows
    sudo apt-get install ninja-build\n
    brew install ninja\n

    Aucune id\u00e9e... \u00c0 vous de chercher.

    "}, {"location": "tools/build-system/ninja/#exemple-de-fichier-buildninja", "title": "Exemple de fichier build.ninja", "text": "

    Un fichier simple build.ninja pourrait ressembler \u00e0 ceci\u2009:

    rule cc\n  command = gcc -c $in -o $out\n  description = Compiling $in\n\nrule link\n  command = gcc $in -o $out\n  description = Linking $out\n\nbuild foo.o: cc foo.c\nbuild bar.o: cc bar.c\nbuild my_program: link foo.o bar.o\n

    Dans cet exemple, Ninja ex\u00e9cute deux \u00e9tapes principales\u2009:

    • Compilation (cc), qui compile les fichiers .c en .o.
    • Linkage (link), qui prend les fichiers objets .o et les lie pour produire l'ex\u00e9cutable final my_program.

    Une fois que le fichier build.ninja est g\u00e9n\u00e9r\u00e9, vous pouvez compiler votre projet en ex\u00e9cutant simplement la commande\u2009:

    ninja\n

    Cela lancera la compilation en fonction des r\u00e8gles d\u00e9finies dans le fichier build.ninja, avec des optimisations pour la compilation incr\u00e9mentale et le parall\u00e9lisme.

    ", "tags": ["link", "build.ninja", "my_program"]}, {"location": "tools/build-system/ninja/#discussion", "title": "Discussion", "text": "

    Contrairement \u00e0 make, ninja ne permet pas de d\u00e9finir des r\u00e8gles de mani\u00e8re dynamique. Cela signifie que vous ne pouvez pas g\u00e9n\u00e9rer des r\u00e8gles de compilation en fonction de l'\u00e9tat du syst\u00e8me ou d'autres param\u00e8tres. Cela peut \u00eatre un inconv\u00e9nient pour certains projets, mais cela permet \u00e9galement de garantir une certaine d\u00e9terminisme dans le processus de build.

    Par exemple avec Make, on peut d\u00e9finir la r\u00e8gle suivante qui est une r\u00e8gle g\u00e9n\u00e9rique pour compiler tous les fichiers .c en fichiers .o.

    %.o: %.c\n    $(CC) -o $@ -c $< $(CFLAGS) -MMD -MP\n

    Cette r\u00e8gle est souvent associ\u00e9e \u00e0 une autre r\u00e8gle pour r\u00e9cup\u00e9rer les fichiers sources automatiquement\u2009:

    SRCS := $(wildcard *.c)\nOBJS := $(SRCS:.c=.o)\n

    Avec Ninja, vous devez d\u00e9finir explicitement chaque r\u00e8gle de compilation, ce qui peut \u00eatre fastidieux pour les projets de grande taille. C'est pour cette raison que l'outil est souvent utilis\u00e9 en conjonction avec des outils de configuration comme CMake ou Meson qui g\u00e9n\u00e8rent les fichiers build.ninja pour vous.

    Ninja n'est donc pas un outil destin\u00e9 \u00e0 \u00eatre utilis\u00e9 manuellement pour chaque projet, mais plut\u00f4t comme un outil de build syst\u00e8me pour des projets plus importants o\u00f9 la vitesse et l'efficacit\u00e9 sont des priorit\u00e9s.

    ", "tags": ["build.ninja"]}, {"location": "tools/c-cpp/conan/", "title": "Conan", "text": "

    Conan est un gestionnaire de paquets C/C++ multiplateforme. Il permet de g\u00e9rer les d\u00e9pendances de vos projets C/C++ de mani\u00e8re simple et efficace. Il est multiplateforme, open-source et supporte de nombreux gestionnaires de build (CMake, Visual Studio, Meson, etc.).

    Il est souvent utilis\u00e9 conjointement avec Meson, un autre outil de build C/C++ moderne.

    Conan est d\u00e9velopp\u00e9 avec Python et est disponible sur PyPI. Pour l'installer, vous pouvez utiliser pip :

    pipx install conan\n

    Il utilise un fichier de configuration conanfile.txt ou conanfile.py pour d\u00e9clarer les d\u00e9pendances de votre projet. Voici un exemple de fichier conanfile.txt :

    [requires]\npoco/1.10.1\nsdl2/2.0.10\n\n[generators]\ncmake\n

    Alternativement vous pouvez ajouter une d\u00e9pendance \u00e0 un projet existant avec la commande conan install :

    conan install conan install Poco/1.13.3@pocoproject/stable --build missing\n
    ", "tags": ["conanfile.py", "pip", "conanfile.txt"]}, {"location": "tools/c-cpp/gcc/", "title": "Compilateur C/C++", "text": "

    Compiler un programme en C ou en C++ requiert un compilateur. Il s'agit d'un programme qui permet de traduire le code source en langage C ou C++ en un fichier objet (extension .o ou .obj), ou un fichier ex\u00e9cutable (extension .exe sous Windows ou sans extension dans un syst\u00e8me POSIX).

    Le compilateur le plus utilis\u00e9 pour le C est gcc (GNU Compiler Collection) et pour le C++ est g++. Ces deux compilateurs sont inclus dans la suite logicielle gcc. Pour installer gcc sous Ubuntu, il suffit de taper la commande suivante dans un terminal\u2009:

    sudo apt-get install build-essential\n

    Sous Windows, c'est plus compliqu\u00e9. GCC \u00e9tant un logiciel pr\u00e9vu pour un syst\u00e8me POSIX, il n'est pas directement compatible avec Windows. Il existe cependant des ports de GCC pour Windows, comme MinGW, Cygwin ou MSYS2. Ces ports permettent d'installer GCC sur Windows, mais il est plus \u00e9l\u00e9gant et plus pratique d'utiliser WSL si les applications que vous d\u00e9veloppez sont destin\u00e9es \u00e0 vos \u00e9tudes et ne seront pas distribu\u00e9es \u00e0 des utilisateurs Windows.

    Si vous souhaitez compiler sous Windows avec un compilateur Windows, vous pouvez utiliser Visual Studio qu'il ne faut pas confondre avec Visual Studio Code. Visual Studio est un IDE complet qui inclut un compilateur C/C++. Ce compilateur nomm\u00e9 cl est un compilateur propri\u00e9taire de Microsoft. Il est inclus dans Visual Studio et n'est pas disponible s\u00e9par\u00e9ment. Ce compilateur ne respecte pas toujours le standard C (ISO/IEC 9899) mais couvre presque en totalit\u00e9 la norme C++ (ISO/IEC 14882).

    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    La compilation requiert plusieurs \u00e9tapes\u2009:

    1. Pr\u00e9traitement : Le pr\u00e9processeur remplace les macros par leur d\u00e9finition, inclut les fichiers d'en-t\u00eate, etc.
    2. Compilation : La compilation converti le code source en assembleur.
    3. Assemblage : L'assembleur converti le code assembleur en code objet, il lie les biblioth\u00e8ques statiques et r\u00e9soud les adresses des fonctions externes.
    4. \u00c9dition de liens : Le lien \u00e9dite les fichiers objets pour cr\u00e9er un fichier ex\u00e9cutable.

    Chacune de ces \u00e9tapes peut \u00eatre r\u00e9alis\u00e9e s\u00e9par\u00e9ment. Par exemple, pour compiler un programme en C, il est possible de g\u00e9n\u00e9rer le fichier objet sans l'\u00e9diter de liens. Cela permet de gagner du temps lors du d\u00e9veloppement, car seule la partie modifi\u00e9e du code est recompil\u00e9e\u2009:

    # G\u00e9n\u00e8re un fichier C\n$ cat > main.c <<EOF\n#include <stdio.h>\nint main() {\n    printf(\"hello, world!\\n\");\n}\nEOF\n\n# \u00c9tape de pr\u00e9processeur\n$ gcc -E main.c -o main.i\n\n# \u00c9tape de compilation\n$ gcc -S main.i -o main.s\n\n# \u00c9tape d'assemblage\n$ gcc -c main.s -o main.o\n\n# \u00c9tape d'\u00e9dition de liens\n$ gcc main.o -o main\n
    "}, {"location": "tools/c-cpp/gcc/#compilation-separee", "title": "Compilation s\u00e9par\u00e9e", "text": "

    La compilation s\u00e9par\u00e9e est une technique de d\u00e9veloppement qui consiste \u00e0 compiler chaque fichier source s\u00e9par\u00e9ment. Cela permet de r\u00e9duire le temps de compilation lors du d\u00e9veloppement, car seuls les fichiers modifi\u00e9s sont recompil\u00e9s. Cela permet \u00e9galement de r\u00e9duire la taille des fichiers objets, car les fonctions non utilis\u00e9es ne sont pas incluses dans le fichier objet.

    # G\u00e9n\u00e8re un fichier C\n$ cat > main.c <<EOF\n#include \"add.h\"\n#include <stdio.h>\nint main() {\n    const int a = 2, b = 3;\n    printf(\"Somme de %d+%d = %d\\n\", a, b, add(a, b));\n}\nEOF\n\n$ cat > add.h <<EOF\nint add(int a, int b);\nEOF\n\n$ cat > add.c <<EOF\nint add(int a, int b) {\n    return a + b;\n}\nEOF\n\n# Compilation s\u00e9par\u00e9e des objets\n$ gcc -c main.c -o main.o\n$ gcc -c add.c -o add.o\n\n# \u00c9dition de liens\n$ gcc main.o add.o -o main\n

    Notez que si vous avez deux fichiers C mais que vous ne souhaitez pas les compiler s\u00e9par\u00e9ment, vous pouvez les compiler en une seule commande\u2009:

    $ gcc main.c add.c -o main\n
    "}, {"location": "tools/c-cpp/gcc/#options-de-compilation", "title": "Options de compilation", "text": "

    gcc et g++ acceptent de nombreuses options de compilation. Les plus courantes sont\u2009:

    Options de compilation de GCC Option Description -c Compile le code source en un fichier objet sans l'\u00e9diter de liens. -o Sp\u00e9cifie le nom du fichier de sortie. -I Sp\u00e9cifie un r\u00e9pertoire o\u00f9 chercher les fichiers d'en-t\u00eate. -L Sp\u00e9cifie un r\u00e9pertoire o\u00f9 chercher les biblioth\u00e8ques. -l Sp\u00e9cifie une biblioth\u00e8que \u00e0 lier. -Wall Active tous les avertissements. -Werror Traite les avertissements comme des erreurs. -g Inclut des informations de d\u00e9bogage dans le fichier objet. -O Optimise le code. -std Sp\u00e9cifie la norme du langage. -pedantic Respecte strictement la norme. -D D\u00e9finit une macro. -U Undefine une macro. -E Arr\u00eate apr\u00e8s l'\u00e9tape de pr\u00e9processeur. -S Arr\u00eate apr\u00e8s l'\u00e9tape de compilation. -v Affiche les commandes ex\u00e9cut\u00e9es par le compilateur.

    Pour compiler un programme avec les optimisations maximales, dans la norme C17, avec tous les avertissements activ\u00e9s et trait\u00e9s comme des erreurs, vous pouvez utiliser la commande suivante\u2009:

    $ gcc -std=c17 -O3 -Wall -Werror -pedantic main.c -o main\n
    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#optimisation", "title": "Optimisation", "text": "

    L'optimisation est une technique qui vise \u00e0 am\u00e9liorer les performances d'un programme en r\u00e9duisant le temps d'ex\u00e9cution et/ou la consommation de m\u00e9moire. Les compilateurs gcc et g++ offrent plusieurs niveaux d'optimisation, chacun ayant des caract\u00e9ristiques propres.

    -O0

    Aucune optimisation. C'est le niveau par d\u00e9faut. Le code C est traduit en langage assembleur sans tentative d'am\u00e9lioration des performances. Cela facilite le d\u00e9bogage, car le code g\u00e9n\u00e9r\u00e9 reste tr\u00e8s proche du code source. Toutefois, les performances peuvent \u00eatre deux \u00e0 trois fois inf\u00e9rieures \u00e0 celles du code optimis\u00e9.

    -O1

    Optimisation l\u00e9g\u00e8re. Le compilateur applique des optimisations locales comme la suppression des instructions inutiles, la propagation des constantes, et la simplification des expressions constantes. Ce niveau d'optimisation \u00e9quilibre le gain de performances avec la vitesse de compilation.

    -O2

    Optimisation standard. \u00c0 ce niveau, le compilateur applique des optimisations plus agressives, telles que la r\u00e9duction des boucles, la suppression des appels de fonctions inutiles et l'am\u00e9lioration de la gestion des expressions. Cela peut allonger le temps de compilation et augmenter la taille du code, mais les performances g\u00e9n\u00e9r\u00e9es sont significativement am\u00e9lior\u00e9es.

    -O3

    Optimisation maximale. Le compilateur applique toutes les optimisations du niveau -O2 ainsi que des optimisations suppl\u00e9mentaires comme l'optimisation des boucles (loop unrolling), l'inlining de fonctions plus importantes, et l'am\u00e9lioration de la gestion des appels de fonctions. Ce niveau maximise les performances, mais peut \u00e9galement accro\u00eetre la taille du code et le temps de compilation.

    -Os

    Optimisation pour la taille. Le compilateur se concentre sur la r\u00e9duction de la taille du fichier ex\u00e9cutable. Cela peut r\u00e9duire les performances, mais est souvent utile pour les environnements \u00e0 ressources limit\u00e9es, comme les syst\u00e8mes embarqu\u00e9s, ou pour les applications devant \u00eatre distribu\u00e9es via des r\u00e9seaux \u00e0 faible bande passante.

    -Ofast

    Optimisation rapide. Le compilateur applique les optimisations du niveau -O3, mais avec des assouplissements sur le respect des normes du langage C. Certaines v\u00e9rifications sont d\u00e9sactiv\u00e9es pour am\u00e9liorer encore les performances, ce qui peut entra\u00eener des comportements non conformes \u00e0 la norme dans certains cas. Ce niveau peut \u00eatre tr\u00e8s performant, mais il doit \u00eatre utilis\u00e9 avec pr\u00e9caution.

    -ffast-math

    Optimisation agressive des calculs math\u00e9matiques. Le compilateur effectue des optimisations pouss\u00e9es sur les op\u00e9rations math\u00e9matiques en ignorant certaines pr\u00e9cautions sur la pr\u00e9cision et les exceptions. Cela peut consid\u00e9rablement acc\u00e9l\u00e9rer les calculs, mais augmente \u00e9galement le risque de r\u00e9sultats incorrects ou inattendus. Cette option est \u00e0 utiliser uniquement si la pr\u00e9cision des calculs n'est pas critique.

    -flto

    Optimisation intermodulaire (Link-Time Optimization). Le compilateur fusionne les fichiers objets avant l'\u00e9dition de liens, permettant ainsi des optimisations globales \u00e0 l'\u00e9chelle du programme. Cela inclut l'inlining entre fichiers, la suppression des fonctions inutilis\u00e9es, la propagation des constantes et la fusion des boucles. Cette optimisation est particuli\u00e8rement efficace sur les gros projets avec plusieurs fichiers source, permettant d'am\u00e9liorer les performances globales du programme.

    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#bibliotheques", "title": "Biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques sont des fichiers contenant des fonctions et des variables pr\u00e9d\u00e9finies. Il existe deux types de biblioth\u00e8ques\u2009: les biblioth\u00e8ques statiques et les biblioth\u00e8ques partag\u00e9es. Les biblioth\u00e8ques statiques ont l'extension .a sous POSIX et .lib sous Windows. Les biblioth\u00e8ques partag\u00e9es ont l'extension .so sous POSIX et .dll sous Windows.

    La biblioth\u00e8que standard du langage C est libc. Elle est incluse par d\u00e9faut dans tous les programmes C. Pour inclure une biblioth\u00e8que, il suffit de sp\u00e9cifier son nom avec l'option -l. Par exmple la biblioth\u00e8que m contient des fonctions math\u00e9matiques. Elle s'appelle libm sous Unix/Linux.

    Aussi si vous souhaitez calculer le sinus de 3.14, vous pouvez utiliser la fonction sin de la biblioth\u00e8que m, et par cons\u00e9quent vous devez lier cette biblioth\u00e8que \u00e0 votre programme\u2009:

    $ gcc main.c -o main -lm\n

    Notez que l'option -lm doit \u00eatre plac\u00e9e apr\u00e8s le nom du fichier source. En effet, gcc traite les options dans l'ordre o\u00f9 elles apparaissent sur la ligne de commande. Si l'option -lm est plac\u00e9e avant le nom du fichier source, gcc ne trouvera pas la fonction sin et \u00e9chouera. Dans la table ci-dessous vous trouverez quelques biblioth\u00e8ques couramment utilis\u00e9es.

    Biblioth\u00e8ques couramment utilis\u00e9es Biblioth\u00e8que Description libc Biblioth\u00e8que standard du langage C. libm Fonctions math\u00e9matiques. libpthread Fonctions de threads POSIX. libcurl Client HTTP. libssl Biblioth\u00e8que de chiffrement SSL. libcrypto Biblioth\u00e8que de chiffrement. libz Compression de donn\u00e9es. libpng Traitement d'images PNG. libsqlite3 Base de donn\u00e9es SQLite.", "tags": ["libsqlite3", "libcurl", "gcc", "libpng", "libcrypto", "libz", "libm", "libssl", "sin", "libc", "libpthread"]}, {"location": "tools/c-cpp/gdb/", "title": "GDB (GNU Debugger)", "text": "

    GDB est un d\u00e9bogueur en ligne de commande. Il permet de suivre l'ex\u00e9cution d'un programme pas \u00e0 pas, de mettre des points d'arr\u00eat, d'inspecter la m\u00e9moire, de modifier le contenu des variables, etc.

    "}, {"location": "tools/c-cpp/vcpkg/", "title": "VcPKG", "text": "

    VcPKG est un gestionnaire de paquets C++ multiplateforme qui permet d'installer et de g\u00e9rer des biblioth\u00e8ques tierces. Il est utilis\u00e9 pour simplifier le processus d'installation de biblioth\u00e8ques C++ sur Windows, Linux et macOS.

    Il est similaire \u00e0 Conan, mais il est plus orient\u00e9 vers les biblioth\u00e8ques C++ et est souvent utilis\u00e9 avec Visual Studio.

    "}, {"location": "tools/dev/bash/", "title": "Bash (Ligne de commande)", "text": "

    La ma\u00eetrise de la ligne de commande n'est pas indispensable pour ce cours, mais la compr\u00e9hension de quelques commandes est utile pour bien comprendre les exemples donn\u00e9s.

    Dans un environnement POSIX l'interaction avec le syst\u00e8me s'effectue pour la plupart du temps via un terminal. Le programme utilis\u00e9 pour interagir avec le syst\u00e8me d'exploitation est appel\u00e9 un interpr\u00e9teur de commande. Sous Windows vous utilisez le programme cmd.exe ou PowerShell.exe. Sous Linux vous utilisez tr\u00e8s souvent un d\u00e9riv\u00e9 de Bourne shell nom \u00e9ponyme de Stephen Bourne et apparu en 1979. La compatibilit\u00e9 est toujours maintenue aujourd'hui via son successeur Bash dont le nom est un acronyme de Bourne-again shell.

    Bash est \u00e9crit en C et les sources sont naturellement disponibles sur internet. Lorsque vous lancez Bash, vous aurez un simple prompt\u2009:

    $\n

    Ce dernier vous invite \u00e0 taper une commande laquelle est le plus souvent le nom d'un programme. Voici un exemple\u2009:

    $ cat foo.txt | hexdump -C -n100\n
    ", "tags": ["PowerShell.exe", "cmd.exe"]}, {"location": "tools/dev/bash/#navigation", "title": "Navigation", "text": "

    Pour naviguer dans l'arborescence, le programme cd est utilis\u00e9. Il est l'acronyme de change directory. Ce programme prend en argument un chemin relatif ou absolu.

    $ cd /usr\n/usr$ cd bin\n/usr/bin$ cd .\n/usr/bin$ cd ..\n/usr/$ cd /var/tmp\n/var/tmp$ cd\n~$\n

    La derni\u00e8re commande est singuli\u00e8re\u2009: si cd est appel\u00e9 sans argument, il nous ram\u00e8ne dans notre r\u00e9pertoire personnel nomm\u00e9 home et abbr\u00e9g\u00e9 ~.

    "}, {"location": "tools/dev/bash/#affichage", "title": "Affichage", "text": "

    L'affichage du contenu courant de l'arborescence est possible avec le programme ls pour list structure.

    $ ls /usr/bin/as*\n/usr/bin/as                 /usr/bin/asciitopgm     /usr/bin/assistant\n/usr/bin/asan_symbolize     /usr/bin/aspell         /usr/bin/asy\n/usr/bin/asan_symbolize-10  /usr/bin/aspell-import\n$ ls -al /usr/bin/as*\n-rwxr-xr-x 1 root root  38K 2020-04-20 07:12 /usr/bin/asan_symbolize-10\n-rwxr-xr-x 1 root root 9.9K 2016-04-23 13:53 /usr/bin/asciitopgm\n-rwxr-xr-x 1 root root 167K 2020-03-22 16:33 /usr/bin/aspell\n-rwxr-xr-x 1 root root 2.0K 2020-03-22 16:33 /usr/bin/aspell-import\nlrwxrwxrwx 1 root root    9 2020-03-22 16:55 /usr/bin/assistant -> qtchooser\n-rwxr-xr-x 1 root root 4.3M 2020-02-10 15:52 /usr/bin/asy\n

    On utilise souvent les options a (pour all) et l (pour long) permettant d'afficher les r\u00e9sultats avec plus de d\u00e9tails. Dans l'ordre on peut lire les permissions du fichier, le propri\u00e9taire, le groupe, la taille du fichier, sa date de derni\u00e8re modification et enfin son nom.

    "}, {"location": "tools/dev/bash/#pipe", "title": "Pipe", "text": "

    Le signe pipe | permet de rediriger le flux de sortie d'un programme vers le flux d'entr\u00e9e d'un autre programme et ainsi les ex\u00e9cuter \u00e0 la cha\u00eene.

    % code-block\u2009::text % % $ echo \u00ab\u2009Bonjour\u2009\u00bb | cowsay

    Il se peut que vous souhaitiez rediriger la sortie d'erreur vers la sortie standard et ainsi concat\u00e9ner les deux flux sur l'entr\u00e9e standard d'un autre programme.

    % code-block\u2009::text % % $ echo \u00ab\u2009Bonjour\u2009\u00bb 2>&1 | cowsay

    "}, {"location": "tools/dev/config/", "title": "Fichiers de configuration", "text": ""}, {"location": "tools/dev/config/#introduction", "title": "Introduction", "text": "

    Dans un projet, vous aurez tr\u00e8s souvent un tas de fichiers de configuration. Ils commencent g\u00e9n\u00e9ralement par un point (.) pour les cacher dans le r\u00e9pertoire. C'est la mani\u00e8re dont les fichiers sont cach\u00e9s dans les syst\u00e8mes de fichiers Unix.

    "}, {"location": "tools/dev/config/#fichiers-populaires", "title": "Fichiers populaires", "text": ""}, {"location": "tools/dev/config/#clang-format", "title": ".clang-format", "text": "

    Ce fichier est au format YAML et contient des directives pour formater votre code automatiquement soit \u00e0 partir de VsCode si vous avez install\u00e9 l'extension Clang-Format et l'ex\u00e9cutable clang-format (sudo apt install -y clang-format). Clang-format est un utilitaire de la suite LLVM, proposant Clang un compilateur alternatif \u00e0 GCC.

    On voit que le texte pass\u00e9 sur stdin (jusqu'\u00e0 EOF) est ensuite format\u00e9 proprement\u2009:

    $ clang-format --style=mozilla <<EOF\n#include <stdio.h>\nint\nmain\n()\n{printf(\"hello, world\\n\");}\nEOF\n#include <stdio.h>\nint\nmain()\n{\nprintf(\"hello, world\\n\");\n}\n

    Par d\u00e9faut clang-format utilise le fichier de configuration nomm\u00e9 .clang-format qu'il trouve.

    Vous pouvez g\u00e9n\u00e9rer votre propre configuration facilement depuis un configurateur tel que clang-format configurator.

    ", "tags": ["stdin"]}, {"location": "tools/dev/config/#editor_config", "title": ".editor_config", "text": "

    Ce fichier au format YAML permet de sp\u00e9cifier des recommandations pour l'\u00e9dition de fichiers sources. Vous pouvez y sp\u00e9cifier le type de fin de lignes CR ou CRLF, le type d'indentation (espaces ou tabulations) et le type d'encodage (ASCII ou UTF-8) pour chaque type de fichiers. EditorConfig est aujourd'hui support\u00e9 par la plupart des \u00e9diteurs de textes qui cherchent automatiquement un fichier de configuration nomm\u00e9 .editor_config.

    Dans Visual Studio Code, il faut installer l'extension EditorConfig for VS Code pour b\u00e9n\u00e9ficier de ce fichier.

    Pour les travaux pratiques, on se contente de sp\u00e9cifier les directives suivantes\u2009:

    root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ncharset = utf-8\n\n[*.{json,yaml}]\nindent_style = space\nindent_size = 2\n\n[Makefile]\nindent_style = tab\n\n[*.{cmd,bat}]\nend_of_line = crlf\n
    "}, {"location": "tools/dev/config/#gitattributes", "title": ".gitattributes", "text": "

    Ce fichier permet \u00e0 Git de r\u00e9soudre certains probl\u00e8mes dans l'\u00e9dition de fichiers sous Windows ou POSIX lorsque le type de fichiers n'a pas le bon format. On se contente de d\u00e9finir quelle sera la fin de ligne standard pour certains types de fichiers\u2009:

    * text=auto eol=lf\n*.{cmd,[cC][mM][dD]} text eol=crlf\n*.{bat,[bB][aA][tT]} text eol=crlf\n
    "}, {"location": "tools/dev/config/#gitignore", "title": ".gitignore", "text": "

    Ce fichier de configuration permet \u00e0 Git d'ignorer par d\u00e9faut certains fichiers et ainsi \u00e9viter qu'ils ne soient ajout\u00e9s par erreur au r\u00e9f\u00e9rentiel. Ici, on souhaite \u00e9viter d'ajouter les fichiers objets .o et les ex\u00e9cutables *.out :

    *.out\n*.o\n*.d\n*.so\n*.lib\n
    "}, {"location": "tools/dev/config/#vscodelaunchjson", "title": ".vscode/launch.json", "text": "

    Ce fichier permet \u00e0 Visual Studio Code de savoir comment ex\u00e9cuter le programme en mode d\u00e9bogue. Il est au format JSON. Les lignes importantes sont program qui contient le nom de l'ex\u00e9cutable \u00e0 lancer args qui sp\u00e9cifie les arguments pass\u00e9s \u00e0 ce programme et MiMode qui est le nom du d\u00e9bogueur que vous utiliserez. Par d\u00e9faut nous utilisons GDB.

    {\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Launch Main\",\n            \"type\": \"cppdbg\",\n            \"request\": \"launch\",\n            \"program\": \"${workspaceFolder}/a.out\",\n            \"args\": [\"--foobar\", \"filename\", \"<<<\", \"hello, world\"],\n            \"stopAtEntry\": true,\n            \"cwd\": \"${workspaceFolder}\",\n            \"environment\": [],\n            \"externalConsole\": false,\n            \"MIMode\": \"gdb\",\n            \"setupCommands\": [\n                {\n                    \"description\": \"Enable pretty-printing for gdb\",\n                    \"text\": \"-enable-pretty-printing\",\n                    \"ignoreFailures\": true\n                }\n            ],\n            \"preLaunchTask\": \"Build Main\"\n        }\n    ]\n}\n
    ", "tags": ["args", "program", "MiMode"]}, {"location": "tools/dev/config/#vscodetasksjson", "title": ".vscode/tasks.json", "text": "

    Ce fichier contient les directives de compilation utilis\u00e9es par Visual Studio Code lors de l'ex\u00e9cution de la t\u00e2che build accessible par la touche <F5>. On y voit que la commande ex\u00e9cut\u00e9e est make. Donc la mani\u00e8re dont l'ex\u00e9cutable est g\u00e9n\u00e9r\u00e9 d\u00e9pend d'un Makefile.

    {\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Build Main\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            }\n        },\n        {\n            \"label\": \"Clean\",\n            \"type\": \"shell\",\n            \"command\": \"make clean\"\n        }\n    ]\n}\n
    ", "tags": ["make", "Makefile"]}, {"location": "tools/dev/docker/", "title": "Docker", "text": ""}, {"location": "tools/dev/docker/#introduction", "title": "Introduction", "text": "

    Docker

    Docker est une plateforme logicielle qui permet de cr\u00e9er, de tester et de d\u00e9ployer des applications dans des conteneurs logiciels. Un conteneur est une unit\u00e9 logicielle qui contient une application et toutes ses d\u00e9pendances. Les conteneurs sont l\u00e9gers, portables et auto-suffisants. Ils sont ex\u00e9cut\u00e9s dans un environnement isol\u00e9 de l'h\u00f4te.

    C'est une alternative \u00e0 la virtualisation (comme VirtualBox ou VMware) vue comme beaucoup plus l\u00e9g\u00e8re et plus rapide. Docker est devenu un outil incontournable pour les d\u00e9veloppeurs et les administrateurs syst\u00e8me.

    Sous Windows, Docker d\u00e9pend de la technologie Hyper-V qui permet de cr\u00e9er des machines virtuelles. Sous Linux, Docker utilise les fonctionnalit\u00e9s de virtualisation du noyau Linux. Dans les versions r\u00e9centes, Docker est bas\u00e9 sur WSL 2 (Windows Subsystem for Linux 2) qui permet d'ex\u00e9cuter un noyau Linux complet dans Windows.

    Pour utiliser docker il y a deux notions \u00e0 comprendre\u2009:

    • Images : une image est un mod\u00e8le de conteneur. C'est un fichier binaire qui contient toutes les d\u00e9pendances n\u00e9cessaires pour ex\u00e9cuter une application. Les images sont stock\u00e9es dans un registre (comme Docker Hub) et peuvent \u00eatre partag\u00e9es.
    • Conteneurs : un conteneur est une instance d'une image. C'est une application en cours d'ex\u00e9cution avec son propre environnement isol\u00e9. Les conteneurs peuvent \u00eatre cr\u00e9\u00e9s, d\u00e9marr\u00e9s, arr\u00eat\u00e9s, d\u00e9plac\u00e9s et supprim\u00e9s.

    Imaginons que nous souhaitions ex\u00e9cuter une application Python qui utilise numpy sur une machine qui n'a pas Python d'install\u00e9. Nous pourrions cr\u00e9er une image Docker qui contient Python et numpy, puis ex\u00e9cuter un conteneur bas\u00e9 sur cette image. L'application fonctionnera sans probl\u00e8me, m\u00eame si Python n'est pas install\u00e9 sur la machine h\u00f4te.

    docker run python:3.8-slim python -c \"import numpy; print(numpy.__version__)\"\n
    "}, {"location": "tools/dev/environment/", "title": "L'environnement de travail", "text": "

    Vous \u00eates \u00e9tudiante ou \u00e9tudiant et vous \u00eates perdus avec l'utilisation de Python, LaTeX, Git, etc., sous Windows, Linux ou encore Docker Ce document est fait pour vous. Il vous guidera dans l'installation et l'utilisation de ces outils. L'objectif est de comprendre les avantages et les inconv\u00e9nients de chaque outil et de vous permettre de les utiliser de mani\u00e8re efficace.

    "}, {"location": "tools/dev/environment/#preambule", "title": "Pr\u00e9ambule", "text": "

    Les applications utilis\u00e9es typiquement par un ing\u00e9nieur aujourd'hui sont Python, Git, GCC, LaTeX ou m\u00eame Docker. Ces applications ont un point commun, c'est qu'elles ont d'abord \u00e9t\u00e9 \u00e9crites pour un environnement POSIX (Unix).

    POSIX est une norme internationale (IEEE 1003) qui d\u00e9finit l'interface de programmation d'un syst\u00e8me d'exploitation. Elle est bas\u00e9e sur UNIX. Elle est utilis\u00e9e pour les syst\u00e8mes d'exploitation de type UNIX. Windows n'est pas un syst\u00e8me qui respecte cette norme, ce qui complique l'utilisation de ces applications.

    Afin de pouvoir porter Python ou Git sous Windows, il a fallu ajouter une couche d'abstraction pour rendre compatible le monde Linux avec le monde Windows. Historiquement le projet Cygwin n\u00e9 en 1995 a \u00e9t\u00e9 le premier \u00e0 proposer une telle couche. Il s'agissait d'un environnement POSIX pour Windows muni d'un terminal, d'un gestionnaire de paquets et d'une biblioth\u00e8que d'\u00e9mulation POSIX. Les outils en ligne de commande type ls, cat ou m\u00eame grep \u00e9taient propos\u00e9s. N\u00e9anmoins, Cygwin n'\u00e9tait pas parfait, il n\u00e9cessitait son propre environnement de travail et n'\u00e9tait pas bien int\u00e9gr\u00e9 \u00e0 Windows. Le projet MSYS a \u00e9t\u00e9 cr\u00e9\u00e9 en 2003 pour pallier \u00e0 ces probl\u00e8mes. Il s'agissait d'une couche d'abstraction POSIX pour Windows qui se voulait plus l\u00e9g\u00e8re. Au lieu de compiler des applications Linux qui devaient imp\u00e9rativement \u00eatre lanc\u00e9es sous Cygwin, MSYS int\u00e9grait la couche d'abstraction dans les applications elles-m\u00eames, ces derni\u00e8res \u00e9taient compil\u00e9es en .exe et pouvaient \u00eatre lanc\u00e9es directement depuis l'explorateur Windows. MSYS a \u00e9t\u00e9 un franc succ\u00e8s et a \u00e9t\u00e9 int\u00e9gr\u00e9 dans le projet MinGW (Minimalist GNU for Windows) qui est un portage de GCC pour Windows.

    Lorsque vous installez Git sous Windows et que vous visitez l'emplacement d'installation (C:\\Program Files\\Git), vous verrez des dossiers aux noms compatibles POSIX comme bin, etc, lib, usr, etc. Le dossier bin qui contient les ex\u00e9cutables contient bash qui n'est rien d'autre que le terminal utilis\u00e9 sous Linux. La raison est que Git ou m\u00eame Python sont des outils avant tout d\u00e9velopp\u00e9s pour les environnements Unix/Linux.

    Le choix de l'environnement de travail est donc compliqu\u00e9. Faut-il travailler sous Windows avec les limitations que le portage des applications Linux implique ou faut-il travailler sous Linux directement\u2009? Un ing\u00e9nieur reste aujourd'hui attach\u00e9 au monde Windows car il d\u00e9pend de logiciels comme SolidWorks ou Altium Designer qui ne sont pas disponibles sous Linux. Il est donc n\u00e9cessaire de trouver un compromis.

    En 2016, Microsoft a annonc\u00e9 le support de Linux dans Windows 10. Il s'agissait d'une couche d'abstraction qui permettait de faire tourner des applications Linux directement sous Windows. Cette couche d'abstraction s'appelle Windows Subsystem for Linux (WSL). Elle est bas\u00e9e sur une technologie de virtualisation de conteneurs. WSL a \u00e9t\u00e9 un franc succ\u00e8s et il a tr\u00e8s vite \u00e9t\u00e9 adopt\u00e9 par les d\u00e9veloppeurs Web. Il fut de surcro\u00eet propos\u00e9 comme une alternative \u00e0 Cygwin et MSYS.

    WSL a \u00e9t\u00e9 am\u00e9lior\u00e9 au fil des ann\u00e9es et en 2019, Microsoft a annonc\u00e9 WSL 2. WSL 2 est bas\u00e9 sur une technologie de virtualisation de type 2 (hyperviseur) et non plus de type 1 (noyau Linux). WSL 2 est donc plus performant que WSL 1. Il est possible de faire tourner un serveur web ou m\u00eame un serveur de base de donn\u00e9es directement sous WSL 2. WSL 2 est maintenant une alternative cr\u00e9dible \u00e0 Linux.

    Il rend possible de travailler sous Windows et de faire tourner des applications Linux directement sous Windows.

    Le choix donn\u00e9 aux ing\u00e9nieurs est donc\u2009:

    1. Choix facile, mais source d'incoh\u00e9rences: Travailler exclusivement sous Windows et installer Python, Git, LaTeX sous Windows. L'inconv\u00e9nient est que chacune de ses applications ne profite pas d'une unit\u00e9 de travail commune. \u00c0 force d'installer des applications, vous aurez dans votre syst\u00e8me plusieurs installations de Python, plusieurs ex\u00e9cutables Git ce qui peut vite devenir compliqu\u00e9 \u00e0 g\u00e9rer.
    2. Choix plus difficile, mais offrant davantage de flexibilit\u00e9: Travailler sous WSL 2 et de faire tourner Python, Git, LaTeX sous Linux. L'avantage est que vous aurez une unit\u00e9 de travail commune. Vous pourrez installer des applications Linux directement depuis le gestionnaire de paquets de votre distribution Linux. N\u00e9anmoins vous devrez vous familiariser avec la ligne de commande Linux.
    ", "tags": ["usr", "bash", "Git", "etc", "POSIX", "grep", "cat", "SolidWorks", "bin", "Python", "lib", "LaTeX"]}, {"location": "tools/dev/environment/#le-terminal", "title": "Le terminal", "text": "

    Historiquement sous Windows, le terminal \u00e9tait une application graphique appel\u00e9e cmd. Elle n'a pas \u00e9volu\u00e9 depuis Windows NT. Son interface est limit\u00e9e \u00e0 un nombre fini de caract\u00e8res par ligne et ne supportait que quelques couleurs. Elle ne supportait pas les caract\u00e8res Unicode et ne supportait pas les raccourcis clavier comme Ctrl+C ou Ctrl+V.

    Heureusement Windows a \u00e9volu\u00e9 et propose PowerShell. PowerShell est un terminal plus moderne qui supporte les couleurs, les raccourcis clavier, les caract\u00e8res Unicode, etc. PowerShell est un terminal plus puissant que cmd.

    L'interface du terminal \u00e9tait \u00e9galement rudimentaire (pas d'onglets, pas de s\u00e9parateurs, etc.). Heureusement Windows propose depuis 2019 Windows Terminal. Windows Terminal est un terminal moderne multionglets qui supporte plusieurs terminaux (cmd, PowerShell, WSL, etc.). S'il n'est pas install\u00e9, vous pouvez le faire via le Microsoft Store.

    Interface de cmd.exe dans Windows Terminal

    Interface de PowerShell dans Windows Terminal

    Interface de Ubuntu dans Windows Terminal

    ", "tags": ["cmd"]}, {"location": "tools/dev/environment/#variables-denvironnement", "title": "Variables d'environnement", "text": "

    Que vous soyez sous POSIX ou Windows, votre syst\u00e8me d'exploitation dispose de variables d'environnement. Il s'agit de variables qui sont accessibles par toutes les applications. Elles sont utilis\u00e9es pour stocker des informations comme le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable, le nom de l'utilisateur, le r\u00e9pertoire de travail, etc.

    La variable la plus importante est PATH. Elle contient une liste de chemins d'acc\u00e8s aux ex\u00e9cutables. Lorsque vous tapez une commande dans un terminal, le syst\u00e8me d'exploitation parcourt les chemins d'acc\u00e8s de la variable PATH pour trouver l'ex\u00e9cutable correspondant \u00e0 la commande. Si vous avez install\u00e9 Python, Git, LaTeX, etc., dans des r\u00e9pertoires diff\u00e9rents, il est n\u00e9cessaire de les ajouter \u00e0 la variable PATH. Parfois les installateurs le font automatiquement, parfois non. Il est donc n\u00e9cessaire de v\u00e9rifier manuellement.

    Une variable d'environnement n'est propag\u00e9e \u00e0 un processus que si ce dernier est lanc\u00e9 apr\u00e8s la modification de la variable. Si vous modifiez la variable, PATH les processus d\u00e9j\u00e0 lanc\u00e9s ne verront pas la modification. Il est n\u00e9cessaire de fermer le terminal et de le rouvrir (relancer Visual Studio Code, votre terminal, etc.).

    Parfois si vous installez plusieurs versions d'un m\u00eame logiciel comme Python vous pourriez avoir plusieurs variables d'environnement qui pointent vers des versions diff\u00e9rentes de Python. C'est une source de confusion et c'est un probl\u00e8me fr\u00e9quent sous Windows. Vous pouvez v\u00e9rifier quel est le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable avec la commande where sous Windows et which sous Linux.

    Linux/WSL/MacOSWindows CMDPowerShell
    $ which python\n/usr/bin/python\n
    PS C:\\> where python\nC:\\Python39\\python.exe\n
    PS C:\\> Get-Command python\n
    ", "tags": ["which", "where", "Python", "PATH"]}, {"location": "tools/dev/environment/#latex", "title": "LaTeX", "text": "

    Sous Linux/WSL, le plus simple est d'installer LaTeX avec le gestionnaire de paquets de votre distribution Linux. Ouvrez un terminal et tapez la commande suivante\u2009:

    sudo apt install texlive-full latexmk\n

    Sous Windows c'est plus compliqu\u00e9. Il existe plusieurs distributions LaTeX pour Windows. La plus courante est MiKTeX. T\u00e9l\u00e9chargez l'installateur et suivez les instructions.

    "}, {"location": "tools/dev/environment/#commandes-principales", "title": "Commandes principales", "text": ""}, {"location": "tools/dev/environment/#gcc", "title": "GCC", "text": "Commande Description gcc Compilateur C g++ Compilateur C++ make Gestionnaire de compilation

    Pour compiler un programme\u2009:

    CC++Plusieurs fichiers CCompilation s\u00e9par\u00e9e
    gcc -o hello hello.c\n
    g++ -o hello hello.cpp\n
    gcc -o hello main.c functions.c\n
    gcc -c functions.c\ngcc -c main.c\ngcc -o hello main.o functions.o\n
    ", "tags": ["make", "gcc"]}, {"location": "tools/dev/environment/#linuxwsl", "title": "Linux/WSL", "text": "Commande Description ls Liste les fichiers du r\u00e9pertoire courant cd Change de r\u00e9pertoire pwd Affiche le r\u00e9pertoire courant cat Affiche le contenu d'un fichier less Affiche le contenu d'un fichier page par page grep Recherche une cha\u00eene de caract\u00e8res dans un fichier find Recherche un fichier dans un r\u00e9pertoire man Affiche le manuel d'une commande which Affiche le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable", "tags": ["which", "less", "man", "grep", "cat", "find", "pwd"]}, {"location": "tools/dev/environment/#afficher-les-fichiers-du-repertoire-courant", "title": "Afficher les fichiers du r\u00e9pertoire courant", "text": "
    ls -al # Par noms\nls -lt # Par date de modification\nls -lh # En format lisible\n
    "}, {"location": "tools/dev/environment/#python", "title": "Python", "text": "Commande Description python Lance l'interpr\u00e9teur Python pip Gestionnaire de paquets Python ipython Lance l'interpr\u00e9teur IPython jupyter lab Lance l'environnement Jupyter Lab", "tags": ["ipython", "python", "pip"]}, {"location": "tools/dev/environment/#latex_1", "title": "LaTeX", "text": "Commande Description latexmk -xelatex Compile un document LaTeX"}, {"location": "tools/dev/git/", "title": "Git", "text": "

    Fanart Git

    "}, {"location": "tools/dev/git/#introduction", "title": "Introduction", "text": "

    Git est un outil de gestion de versions. Il a \u00e9t\u00e9 invent\u00e9 par Linus Torvalds en 2005 pour g\u00e9rer le d\u00e9veloppement du noyau Linux qui contient des millions de lignes de code devant \u00eatre modifi\u00e9es par des centaines de milliers de d\u00e9veloppeurs. Git a \u00e9t\u00e9 con\u00e7u pour \u00eatre rapide, efficace et pour g\u00e9rer des projets de toutes tailles. Il est aujourd'hui le syst\u00e8me de gestion de version le plus utilis\u00e9 au monde.

    Git est la suite logique des outils comme CVS ou Subversion qui \u00e9taient utilis\u00e9s pour g\u00e9rer des projets de d\u00e9veloppement de logiciels.

    Git est avant tout un outil en ligne de commande. Son utilisation passe par la commande git suivi d'une sous-commande (clone, pull, push, commit, etc.). Il est possible de l'utiliser avec une interface graphique mais l'interface en ligne de commande est plus puissante, plus rapide et plus flexible.

    Un d\u00e9p\u00f4t (r\u00e9f\u00e9rentiel) Git est un dossier qui contient un dossier cach\u00e9 .git. Ce dossier contient l'historique des modifications du projet. Ne supprimez pas ce dossier, vous perdriez l'historique de votre projet. Lorsque vous faites des commit (sauvegarde incr\u00e9mentationnelle), Git enregistre les modifications dans ce dossier cach\u00e9 et lorsque vous faites un push (envoi des modifications sur un serveur distant), Git envoie les modifications \u00e0 un serveur distant (GitHub, GitLab, Bitbucket, etc.).

    Par cons\u00e9quent, l'utilisation de Git est intrinc\u00e8quement li\u00e9e \u00e0 l'utilisation d'un serveur distant. Il est possible de travailler en local mais l'int\u00e9r\u00eat de Git est de pouvoir travailler en \u00e9quipe. Il est possible de travailler sur une branche (version parall\u00e8le du projet) et de fusionner les modifications avec la branche principale (master).

    Ceci implique de comprendre comment Git communique avec un serveur distant. Notons qu'il est possible de travailler avec plusieurs serveurs distants. Ils se configure avec la commande git remote. Deux protocoles de communication existent\u2009: SSH et HTTPS. Le protocole SSH est plus s\u00e9curis\u00e9 que HTTPS mais n\u00e9cessite l'\u00e9change de cl\u00e9s cryptographiques. Le protocole HTTPS est plus simple \u00e0 mettre en place mais demande un mot de passe ou un jeton d'authentification \u00e0 chaque communication avec le serveur distant. La solution recommand\u00e9e est d'utiliser SSH.

    ", "tags": ["clone", "git", "pull", "CVS", "HTTPS", "commit", "push", "SSH"]}, {"location": "tools/dev/git/#installation-de-git", "title": "Installation de Git", "text": "LinuxWindowsMacOS

    Sous Linux, et particuli\u00e8rement Ubuntu, Git est probablement d\u00e9j\u00e0 install\u00e9 par d\u00e9faut. Si ce n'est pas le cas, avec Ubuntu ouvrez un terminal Bash et tapez la commande suivante\u2009:

    sudo apt install git\n

    [!NOTE] Selon la distribution Linux que vous utilisez, les gestionnaires de paquets n'ont pas le m\u00eame nom. Sous Fedora, le gestionnaire de paquets est dnf et sous Arch Linux, le gestionnaire de paquets est pacman. N\u00e9anmoins la commande reste tr\u00e8s similaire.

    Pour installer Git sous Windows, le plus simple est d'utiliser le nouveau gestionnaire de paquet de Windows appel\u00e9 winget. Ouvrez un terminal PowerShell et tapez la commande suivante issue de Winget Git/Git

    winget install -e --id Git.Git\n

    Sous MacOS, Git est probablement d\u00e9j\u00e0 install\u00e9. Si ce n'est pas le cas, vous pouvez installer Git avec Homebrew en tapant la commande suivante dans un terminal\u2009:

    brew install git\n
    ", "tags": ["dnf", "PowerShell", "pacman", "winget"]}, {"location": "tools/dev/git/#configuration-de-git", "title": "Configuration de Git", "text": ""}, {"location": "tools/dev/git/#utilisateur-et-adresse-e-mail", "title": "Utilisateur et adresse e-mail", "text": "

    La premi\u00e8re chose \u00e0 faire apr\u00e8s avoir install\u00e9 Git est de le configurer. Ouvrez un terminal et tapez les commandes suivantes en veillant bien \u00e0 remplacer John Doe par votre nom et john.doe@acme.com par votre adresse e-mail.

    git config --global user.name \"John Doe\"\ngit config --global user.email john.doe@acme.com\n

    Ces informations sont utilis\u00e9es pour chaque commit que vous ferez. Elles sont importantes car elles permettent de savoir qui a fait une modification dans le projet. Ne vous trompez pas dans votre nom ou votre adresse e-mail, il est difficile de changer ces informations une fois qu'elles ont \u00e9t\u00e9 enregistr\u00e9es dans un commit, et surtout si elle ont \u00e9t\u00e9 envoy\u00e9es sur un serveur distant.

    "}, {"location": "tools/dev/git/#methode-de-fusion-merge", "title": "M\u00e9thode de fusion (merge)", "text": "

    Lorsque vous utilisez la commande git pull pour r\u00e9cup\u00e9rer les modifications d'un serveur distant, Git peut \u00eatre amen\u00e9 \u00e0 fusionner des modifications. Il est possible de choisir la m\u00e9thode de fusion. Les deux m\u00e9thodes les plus courantes sont merge et rebase. La m\u00e9thode merge est la m\u00e9thode par d\u00e9faut. La m\u00e9thode rebase est plus avanc\u00e9e et est utilis\u00e9e pour r\u00e9\u00e9crire l'historique du projet. Elle est plus propre mais elle peut \u00eatre source de probl\u00e8mes si elle est mal utilis\u00e9e. Git s'attend \u00e0 que vous configuriez laquelle des deux m\u00e9thodes vous pr\u00e9f\u00e9rez. Vous pouvez le faire avec la commande suivante\u2009:

    Pour le rebase\u2009:

    git config --global pull.rebase true\n

    Pour le merge\u2009:

    git config --global pull.rebase false\n

    Note

    La m\u00e9thode rebase est plus propre mais elle peut \u00eatre source de probl\u00e8mes si elle est mal utilis\u00e9e. Elle est recommand\u00e9e pour les projets personnels mais pas pour les projets en \u00e9quipe. En cas de doute privil\u00e9giez la m\u00e9thode merge.

    ", "tags": ["rebase", "merge"]}, {"location": "tools/dev/git/#affichage-de-lhistorique-des-commits-log", "title": "Affichage de l'historique des commits (log)", "text": "

    Lorsque vous utilisez la commande git log pour afficher l'historique des commits, Git affiche les commits dans un format compact. Il est possible de personnaliser l'affichage de l'historique des commits. Vous pouvez le faire avec la commande suivante\u2009:

    git config --global alias.lg \"log -n30 --boundary --graph --pretty=format:'%C(bold blue)%h%C(bold green)%<|(20)% ar%C(reset)%C(white)% s %C(dim white)-% an%C(reset)%C(auto)% d%C(bold red)% N' --abbrev-commit --date=relative\"\n

    D\u00e8s lors vous utiliserez le raccourcis lg pour afficher l'historique des commits (git lg).

    Pour afficher la date en format ISO 8601, vous pouvez utiliser la commande suivante\u2009:

    git config --global alias.lga \"log -n30 --boundary --graph --pretty=format:'%C(bold blue)%h%C(bold green)%<|(20)% ai%C(reset)%C(white)% s %C(dim white)-% an%C(reset)%C(auto)% d%C(bold red)' --abbrev-commit --date=iso\"\n
    "}, {"location": "tools/dev/git/#configuration-ssh", "title": "Configuration SSH", "text": "

    Comme nous l'avons vu, Git peut communiquer avec un serveur distant en utilisant le protocole SSH. Pour cela, il est n\u00e9cessaire de configurer une cl\u00e9 cryptographique.

    La premi\u00e8re chose \u00e0 faire est de v\u00e9rifier si vous avez d\u00e9j\u00e0 une cl\u00e9 SSH. Ouvrez un terminal et tapez la commande suivante\u2009:

    $ ls -al ~/.ssh/*.pub\n-rw-r--r-- 1 ycr ycr 393 2023-09-06 08:38 /home/ycr/.ssh/id_rsa.pub\n

    Le fichier id_rsa.pub est votre cl\u00e9 publique. Si vous ne voyez pas ce fichier, c'est que vous n'avez pas de cl\u00e9 SSH. Vous pouvez en g\u00e9n\u00e9rer une avec la commande suivante\u2009:

    ssh-keygen\n

    Sous Windows, la commande est la m\u00eame mais vous devez lancer Git Bash pour l'ex\u00e9cuter. Vous pouvez lancer Git Bash en tapant Git Bash dans le menu de recherche de Windows.

    ", "tags": ["SSH", "id_rsa.pub"]}, {"location": "tools/dev/git/#configuration-de-github", "title": "Configuration de GitHub", "text": "

    Si vous utilisez GitHub, il est n\u00e9cessaire de configurer votre cl\u00e9 SSH dans votre compte GitHub. Pour cela, ouvrez un terminal et tapez la commande suivante\u2009:

    $ cat ~/.ssh/id_rsa.pub\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+yNp7af6zI8NINIFX1aRj+nzKksZ6XzBSkgA/\niuPpYIGz5SSZOkwkvN0DnX8J42DcuEK/mnu3+f9Wh746823gxhXqt+7Wv9z9DJ9O9qrsYlnxIMip\noqepE/Xt+jE5Yv8ullIdsvZdzY611R5DFwrVswslz9OdmpH6nWCmnY/cGZva79ngdcvJLKFk++fl\n+Be1xshWt24svawRH7Fdxn8VyUKmP2Twy6iMo3MT9xGe5leV1CiTXfkzLYntNV50/dtzQN+pwcwR\nBdXBP9FdO9+IzieY6bUGttT6t2VcWoK6jFF+i94Chl/FeGvRU1X/QzSP3SYT2biNRNmznSIa2VD\nycr@heig-vd\n

    Copiez la cl\u00e9 publique et collez-la dans votre compte GitHub. Pour cela, ouvrez votre navigateur et allez sur GitHub. Connectez-vous \u00e0 votre compte et cliquez sur votre photo de profil en haut \u00e0 droite. Cliquez sur Settings puis sur SSH and GPG keys. Cliquez sur New SSH key et collez votre cl\u00e9 publique dans le champ Key. Donnez un nom \u00e0 votre cl\u00e9 et cliquez sur Add SSH key.

    ", "tags": ["Settings", "Key"]}, {"location": "tools/dev/git/#commandes-utiles", "title": "Commandes utiles", "text": "Commandes Git les plus courantes Commande Description git init Initialise un d\u00e9p\u00f4t Git dans le r\u00e9pertoire courant git clone <address> Clone un d\u00e9p\u00f4t distant dans le r\u00e9pertoire courant git status Affiche l'\u00e9tat du d\u00e9p\u00f4t git add <file> Ajoute des fichiers \u00e0 l'index git commit -am \"message\" Enregistre les modifications dans l'historique du d\u00e9p\u00f4t git pull R\u00e9cup\u00e8re les modifications du serveur distant git push Envoie les modifications sur le serveur distant git log Affiche l'historique des commits git lg Affiche l'historique des commits de mani\u00e8re plus lisible"}, {"location": "tools/dev/windows/", "title": "Windows", "text": ""}, {"location": "tools/dev/windows/#msvc-microsoft-visual-c", "title": "MSVC (Microsoft Visual C++)", "text": "

    MSVC est le compilateur C++ de Microsoft. Il est inclus dans Visual Studio et est souvent utilis\u00e9 pour compiler des programmes Windows. Historiquement, MSVC a toujours \u00e9t\u00e9 un peu en retrait par rapport au support des standards modernes du langage C, notamment C99, C11, C18 et maintenant C23. Alors que le compilateur suit de tr\u00e8s pr\u00e8s les standards C++, il ne supporte pas aussi bien les \u00e9volutions du langage C.

    Pour installer MSVC, vous devez t\u00e9l\u00e9charger Visual Studio depuis le site web de Microsoft. L'installation de Visual Studio est assez lourde, car elle installe de nombreux composants qui ne sont pas n\u00e9cessaires pour la compilation de programmes C/C++.

    "}, {"location": "tools/dev/windows/#msys2", "title": "MSYS2", "text": "

    MSYS2 est un environnement de d\u00e9veloppement qui fournit un shell Unix-like et des outils GNU pour Windows. Il est bas\u00e9 sur Cygwin et utilise le gestionnaire de paquets Pacman de Arch Linux.

    MSYS2 est un excellent choix pour compiler des programmes en C/C++ qui peuvent \u00eatre ex\u00e9cut\u00e9s nativement sous Windows. Le compilateur MinGW-w64 est une variante de GCC qui permet de compiler des binaires au format PE (Portable Executable) pour Windows.

    L'installation de MSYS2 est assez simple. Une approche consiste \u00e0 t\u00e9l\u00e9charger l'ex\u00e9cutables d'installation depuis le site web de MSYS2 et \u00e0 l'ex\u00e9cuter. Ma pr\u00e9f\u00e9rence va pour winget disponible nativement sur Windows 10/11.

    winget install -e --id MSYS2.MSYS2\n

    Une fois install\u00e9, vous pouvez ouvrir le programme MSYS2 MINGW64 qui tourne dans un terminal mintty un peu archa\u00efque. Il est pr\u00e9f\u00e9rable d'utiliser Windows Terminal. Pour ce faire il faut configurer un profil pour MSYS2.

    1. Ouvrir Windows Terminal
    2. Utiliser ++Ctrl+Shift+,++ pour ouvrir le fichier de configuration JSON
    3. Allez \u00e0 la section \"profiles\" et ajouter un nouvel \u00e9l\u00e9ment pour MSYS2\u2009:

      {\n    \"colorScheme\": \"Campbell\",\n    \"commandline\": \"\\\"C:\\\\\\\\msys64\\\\\\\\usr\\\\\\\\bin\\\\\\\\bash.exe\\\" --login -i -c \\\"cd ~ && /usr/bin/env MSYSTEM=MINGW64 bash\\\"\\r\",\n    \"guid\": \"{cffe7ad8-1ccd-43ea-99d9-d3dff62b8a02}\",\n    \"hidden\": false,\n    \"icon\": \"C:\\\\msys64\\\\mingw64.ico\",\n    \"name\": \"MinGW\",\n    \"startingDirectory\": \"~\"\n}\n
    4. Attention \u00e0 bien adapter le chemin et veillez \u00e0 ce que le fichier JSON soit bien form\u00e9, c'est \u00e0 dire que les virgules soient bien plac\u00e9es.

    5. Sauvegarder le fichier JSON et fermer Windows Terminal
    6. Ouvrir Windows Terminal et s\u00e9lectionner le profil MinGW dans le menu d\u00e9roulant.

    Le terminal que vous avez est tr\u00e8s similaire \u00e0 celui de WSL. L'installation de paquets est sensiblement diff\u00e9rente de celle de apt ou yum. Pour installer un paquet, il suffit de lancer la commande pacman -S nom-du-paquet. Par exemple, pour installer git, il suffit de lancer la commande suivante\u2009:

    pacman -S git\n

    Pour rechercher un paquet, vous pouvez utiliser la commande pacman -Ss nom-du-paquet.

    Afin de pouvoir r\u00e9cup\u00e9rer des projets GitHub avec votre cl\u00e9 SSH, vous devez soit\u2009:

    1. G\u00e9n\u00e9rer une nouvelle cl\u00e9 SSH pour MSYS2 et l'ajouter \u00e0 votre compte GitHub.
    2. Utiliser la cl\u00e9 SSH existante en la copiant dans le dossier ~/.ssh de MSYS2.

    La premi\u00e8re option est pr\u00e9f\u00e9rable. Pour g\u00e9n\u00e9rer une nouvelle cl\u00e9 SSH, vous pouvez utiliser les commande suivantes.

    ssh-keygen\ncat ~/.ssh/id_rsa.pub\n
    ", "tags": ["git", "yum", "mintty", "winget", "apt"]}, {"location": "tools/dev/wsl/", "title": "Windows Subsystem for Linux", "text": "

    Windows Subsystem for Linux est un outil qui permet d'ex\u00e9cuter des applications Linux sur Windows. Il est disponible sur Windows 10 et Windows Server 2019.

    Face \u00e0 l'augmentation de la popularit\u00e9 de Linux, Microsoft a d\u00e9cid\u00e9 de cr\u00e9er un outil permettant d'ex\u00e9cuter des applications Linux sur Windows. Cela permet aux d\u00e9veloppeurs de travailler sur des projets Linux sans avoir \u00e0 quitter Windows.

    Cela tombe bien, la plupart des outils de d\u00e9veloppement sont disponibles sur Linux. Par exemple, vous pouvez utiliser des outils comme Git, Node.js, Python, Ruby, etc.

    "}, {"location": "tools/dev/wsl/#resolution-des-problemes", "title": "R\u00e9solution des probl\u00e8mes", "text": ""}, {"location": "tools/dev/wsl/#jai-oublie-mon-mot-de-passe", "title": "J'ai oubli\u00e9 mon mot de passe", "text": "

    Si vous \u00eates sur un vrai linux et que vous avez oubli\u00e9 votre mot de passe utilisateur, vous pouvez utiliser le compte root, et si vous avez oubli\u00e9 le mot de passe de root vous \u00eates dans la mouise.

    Sur Windows, il n'y a pas de mot de passe root, et pour entrer dans ce mode voici les \u00e9tapes\u2009:

    1. Ex\u00e9cutez une fen\u00eatre cmd.exe en tant qu'administrateur
    2. Ex\u00e9cutez wsl -l pour lister les distributions WSL install\u00e9es
    3. Ex\u00e9cutez la commande wsl -d nom_distribution -u root (remplacez nom_distribution par le nom de votre distribution). Si vous avez install\u00e9 Ubuntu-24.04, ce sera wsl -d Ubuntu-24.04 -u root
    4. Vous \u00eates maintenant dans le mode root et vous pouvez changer le mot de passe de n'importe quel utilisateur avec passwd nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur)
    5. Si ne vous rappellez pas du nom de l'utilisateur, vous pouvez lister les utilisateurs avec cat /etc/passwd | cut -d: -f1 | tail -n5, votre utilisateur devrait \u00eatre un des cinq derniers.

    Une fois le mot de passe chang\u00e9, vous pouvez vous reconnecter avec votre utilisateur.

    ", "tags": ["root", "cmd.exe", "nom_utilisateur"]}, {"location": "tools/dev/wsl/#en-root-par-defaut", "title": "En root par d\u00e9faut", "text": "

    Si, lorsque vous lancer WSL, vous \u00eates en mode root, c'est que vous avez \u00e9chou\u00e9 \u00e0 cr\u00e9er un utilisateur lors de l'installation de WSL, probablement en annulant la saisie du mot de passe.

    Pour r\u00e9soudre ce probl\u00e8me\u2009:

    1. Ex\u00e9cutez Ubuntu (vous serez en mode super-utilisateur)
    2. Cr\u00e9ez un utilisateur avec adduser nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur), choisissez un nom d'utilisateur simple, en minusucule, sans espace ni caract\u00e8res sp\u00e9ciaux
    3. Suivez les instructions pour d\u00e9finir un mot de passe
    4. Ajoutez l'utilisateur au groupe sudo avec usermod -aG sudo nom_utilisateur
    5. Pour configurer cet utilisateur comme utilisateur par d\u00e9faut, ex\u00e9cutez ubuntu config --default-user nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur)
    ", "tags": ["sudo", "root", "nom_utilisateur"]}, {"location": "tools/editor/vim/", "title": "Vim", "text": "

    Vi IMproved est un \u00e9diteur de texte tr\u00e8s populaire parmi les geek et les d\u00e9veloppeurs. Il est une am\u00e9lioration de Vi, un \u00e9diteur de texte en mode texte tr\u00e8s populaire sur les syst\u00e8mes Unix. Vim est un \u00e9diteur de texte en mode texte, c'est-\u00e0-dire qu'il fonctionne dans un terminal. Il est tr\u00e8s puissant et tr\u00e8s rapide. Il est tr\u00e8s populaire parmi les d\u00e9veloppeurs pour sa rapidit\u00e9 et sa puissance.

    Le fonctionnement de Vi est d\u00e9routant pour les nouveaux utilisateurs, mais une fois que vous avez compris les bases c'est un incontournable depuis un terminal.

    Vim est un \u00e9diteur modal, c'est \u00e0 dire que le clavier peut \u00eatre configur\u00e9 pour avoir des comportements diff\u00e9rents en fonction du mode dans lequel il se trouve. Il y a plusieurs modes dans Vim\u2009:

    • Normal : c'est le mode par d\u00e9faut. Il permet de naviguer dans le fichier, de copier, coller, supprimer, etc.
    • Insertion : dans ce mode, vous pouvez taper du texte.
    • Visual : dans ce mode, vous pouvez s\u00e9lectionner du texte.
    • Commande : dans ce mode, vous pouvez taper des commandes.
    • S\u00e9lection : dans ce mode, vous pouvez s\u00e9lectionner du texte avec la souris.
    "}, {"location": "tools/editor/vscode/", "title": "Visual Studio Code", "text": "

    Visual Studio Code est un \u00e9diteur de code source d\u00e9velopp\u00e9 par Microsoft. Il est gratuit, open-source et multiplateforme. Il est tr\u00e8s populaire pour le d\u00e9veloppement de logiciels, notamment pour les langages de programmation tels que C, C++, Python, Java, etc.

    Il s'inscrit dans une tr\u00e8s longue liste d'\u00e9diteurs de code source. La table suivante pr\u00e9sente quelques \u00e9diteurs de code source populaires.

    \u00c9diteurs de code source populaires \u00c9diteur Ann\u00e9e Commentaire Inspir\u00e9 de ed 1971 \u00c9diteur primitif en mode texte d\u00e9fini dans la norme POSIX - Vi 1976 \u00c9diteur en mode texte d\u00e9fini dans la norme POSIX ed Emacs 1976 \u00c9diteur extensible et personnalisable. TECO Vim 1991 Am\u00e9lioration de Vi, tr\u00e8s populaire des geek et devloppeurs Vi Nano 1999 \u00c9diteur simple et convivial en ligne de commande - Sublime Text 2008 \u00c9diteur propri\u00e9taire avec une version gratuite Vim Atom 2014 \u00c9diteur open-source d\u00e9velopp\u00e9 par GitHub Sublime Text Visual Studio Code 2015 \u00c9diteur open-source d\u00e9velopp\u00e9 par Microsoft Atom

    Outre ces \u00e9diteurs on peut citer d'autres \u00e9diteurs de texte tels que Notepad++, TextPad, UltraEdit, etc. Si vous les utilisez, demandez-vous pourquoi...

    "}, {"location": "tools/editor/vscode/#raccourcis-clavier", "title": "Raccourcis clavier", "text": "

    Parmis les tr\u00e8s nombreux raccourcis clavier de Visual Studio Code, vous trouverez ci-dessous une liste des raccourcis les plus utiles.

    Raccourcis clavier de Visual Studio Code Raccourci Description Ctrl+P Ouvrir un fichier (fuzzy search) Ctrl+Shift+P Ouvrir la palette de commandes (fuzzy search) Ctrl+Shift+N Nouvelle fen\u00eatre Ctrl+Shift+F Rechercher dans tous les fichiers Ctrl+Shift+G Ouvrir le contr\u00f4le de code source (pour faire un Git commit) Ctrl+Shift+D Ouvrir le contr\u00f4le de d\u00e9bogage Ctrl+Shift+X Ouvrir le gestionnaire d'extensions Ctrl+Shift+V Ouvrir un aper\u00e7u du fichier Markdown Ctrl+K V Ouvrir un aper\u00e7u c\u00f4te \u00e0 c\u00f4te du fichier Markdown Ctrl+K Z Activer/d\u00e9sactiver le mode Zen (plein \u00e9cran) Ctrl+K S Enregistrer sous... Ctrl+K R Ouvrir le dossier du fichier actuel Ctrl+K Ctrl+O Ouvre un dossier Ctrl+D S\u00e9lectionner le mot suivant (multicurseur) (r\u00e9p\u00e9ter) Ctrl+U Annuler la derni\u00e8re s\u00e9lection Ctrl+J Ouvrir un terminal int\u00e9gr\u00e9 Ctrl+L S\u00e9lectionner la ligne enti\u00e8re (r\u00e9p\u00e9ter) Ctrl+Shift+L S\u00e9lectionner toutes les occurrences du mot s\u00e9lectionn\u00e9 (multicurseur) Alt+Click Ins\u00e9rer un curseur Ctrl+Alt+Up Ins\u00e9rer un curseur au-dessus"}, {"location": "tools/editor/vscode/#installation", "title": "Installation", "text": "

    Pour installer Visual Studio Code, rendez-vous sur la page https://code.visualstudio.com/ et t\u00e9l\u00e9chargez la version correspondant \u00e0 votre syst\u00e8me d'exploitation. Une fois t\u00e9l\u00e9charg\u00e9, installez-le en suivant les instructions.

    Alternativement, utilisez winget depuis PowerShell\u2009:

    winget install -e --id Microsoft.VisualStudioCode\n
    ", "tags": ["winget"]}, {"location": "tools/editor/vscode/#extensions", "title": "Extensions", "text": ""}, {"location": "tools/editor/vscode/#wsl", "title": "WSL", "text": "

    Si vous utilisez WSL vous devez installer l'extension Remote - WSL pour Visual Studio Code. Cette extension permet d'ouvrir un terminal int\u00e9gr\u00e9 dans WSL, d'ex\u00e9cuter des commandes dans WSL, de d\u00e9boguer des programmes dans WSL, etc.

    "}, {"location": "tools/editor/vscode/#remote-ssh", "title": "Remote - SSH", "text": "

    Si vous utilisez SSH pour vous connecter \u00e0 un serveur distant par exemple vous connecter directement sur votre RaspberryPI, vous devez installer l'extension Remote - SSH pour Visual Studio Code. Cette extension permet d'ouvrir un terminal int\u00e9gr\u00e9 sur un serveur distant, d'ex\u00e9cuter des commandes sur le serveur distant, de d\u00e9boguer des programmes sur le serveur distant, etc.

    "}, {"location": "tools/editor/vscode/#cc", "title": "C/C++", "text": "

    Si vous d\u00e9veloppez en C ou en C++, vous devez installer l'extension C/C++ pour Visual Studio Code. Cette extension permet d'ajouter des fonctionnalit\u00e9s pour le d\u00e9veloppement en C et en C++ telles que la coloration syntaxique, l'autocompl\u00e9tion, la compilation, le d\u00e9bogage, etc.

    "}, {"location": "tools/editor/vscode/#python", "title": "Python", "text": "

    Si vous d\u00e9veloppez en Python, vous devez installer l'extension Python pour Visual Studio Code. Cette extension permet d'ajouter des fonctionnalit\u00e9s pour le d\u00e9veloppement en Python telles que la coloration syntaxique, l'autocompl\u00e9tion, la compilation, le d\u00e9bogage, etc.

    ", "tags": ["Python"]}, {"location": "tools/editor/vscode/#configuration-pour-le-debogueur", "title": "Configuration pour le debogueur", "text": "

    Visual Studio Code n'a pas la notion de projet mais d'espace de travail workspace. Un espace de travail est simplement un r\u00e9pertoire. \u00c0 l'int\u00e9rieur de ce r\u00e9pertoire, on y trouvera\u2009:

    .\n\u251c\u2500\u2500 .vscode\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 launch.json\n\u2514\u2500\u2500 main.c\n

    Visual Studio Code peut en g\u00e9n\u00e9ral g\u00e9n\u00e9rer automatiquement le fichier .vscode/launch.json qui contient tout ce qu'il faut pour compiler et ex\u00e9cuter le programme\u2009:

    {\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"gcc\",\n      \"type\": \"cppdbg\",\n      \"request\": \"launch\",\n      \"program\": \"${fileDirname}\\\\${fileBasenameNoExtension}.exe\",\n      \"args\": [],\n      \"stopAtEntry\": false,\n      \"cwd\": \"${workspaceFolder}\",\n      \"environment\": [],\n      \"externalConsole\": false,\n      \"MIMode\": \"gdb\",\n      \"miDebuggerPath\":\n        \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\\\\gdb.exe\",\n      \"setupCommands\": [\n        {\n          \"description\": \"Enable pretty-printing for gdb\",\n          \"text\": \"-enable-pretty-printing\",\n          \"ignoreFailures\": true\n        }\n      ],\n      \"preLaunchTask\": \"gcc.exe build active file\"\n    }\n  ]\n}\n
    {\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"shell\",\n      \"label\": \"gcc.exe build active file\",\n      \"command\":\n        \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\\\\gcc.exe\",\n      \"args\": [\n        \"-g\",\n        \"${file}\",\n        \"-o\",\n        \"${fileDirname}\\\\${fileBasenameNoExtension}.exe\"\n      ],\n      \"options\": {\n        \"cwd\":\n          \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\"\n      },\n      \"problemMatcher\": [\n        \"$gcc\"\n      ]\n    }\n  ]\n}\n
    "}, {"location": "tools/python/python/", "title": "Python", "text": "

    Un python informaticien

    "}, {"location": "tools/python/python/#introduction", "title": "Introduction", "text": "

    Python est un langage de programmation comme le C mais il est plus haut niveau. Cela signifie d'une que Python est plus facile \u00e0 apprendre et \u00e0 utiliser que le C. Python est un langage interpr\u00e9t\u00e9, car le code source est ex\u00e9cut\u00e9 directement par un interpr\u00e9teur sans n\u00e9cessairement passer par une \u00e9tape de compilation.

    C'est un langage tr\u00e8s populaire pour l'enseignement de la programmation et pour la science des donn\u00e9es. Python est utilis\u00e9 dans de nombreux domaines, tels que l'intelligence artificielle, l'apprentissage automatique, l'analyse de donn\u00e9es, la programmation web, la programmation syst\u00e8me, etc.

    Guido van Rossum a cr\u00e9\u00e9 Python en 1991. Il en a \u00e9t\u00e9 le BDFL (dictateur bienveillant \u00e0 vie) jusqu'en 2018 o\u00f9 il a d\u00e9cid\u00e9 de se retirer de la direction du projet en raison de d\u00e9saccords avec la communaut\u00e9 Python. Cette date marque un tournant dans l'histoire de Python qui devient un langage communautaire avec ses avantages et ses inconv\u00e9nients. L'avantage est que Python est beaucoup plus dynamique et innovant. L'inconv\u00e9nient est qu'il change rapidement et que les versions ne sont pas toujours compatibles entre elles.

    "}, {"location": "tools/python/python/#installation-de-python", "title": "Installation de Python", "text": "

    Sous Linux/WSL, l'installation de Python est tr\u00e8s simple. Ouvrez un terminal et tapez la commande suivante\u2009:

    UbuntuMacOSWindows
    sudo apt install python3\n
    brew install python3\n
    winget install -e --id Python.Python.3.12\n

    Sous Windows, vous devez choisir la version de Python que vous souhaitez installer. Il est recommand\u00e9 d'installer la version 3.12 de Python. Utilisez le gestionnaire de paquets winget pour installer Python.

    ", "tags": ["winget"]}, {"location": "tools/python/python/#configuration-des-variables-denvironnement", "title": "Configuration des variables d'environnement", "text": "

    Selon la m\u00e9thode utilis\u00e9e pour installer Python, il est possible que les variables d'environnement ne soient pas configur\u00e9es automatiquement. Il y a deux entr\u00e9es \u00e0 configurer dans la variable PATH :

    1. Le chemin vers l'ex\u00e9cutable Python. Sous Linux/WSL, le chemin sera d\u00e9j\u00e0 configur\u00e9 (/usr/bin). Sous Windows, le chemin diff\u00e8re selon les \u00e9poques, les installateurs et les versions.
    2. Le chemin vers les paquets locaux install\u00e9s avec pip.

    Pour configurer sous Linux/WSL le chemin vers les paquets locaux install\u00e9s avec pip, ouvrez un terminal et tapez la commande suivante\u2009:

    Linux/WSLWindows
    echo \"export PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"\" >> ~/.bashrc\nsource ~/.bashrc\n

    Sous Windows, c'est plus compliqu\u00e9. Selon l'installation de Python vous devez identifier le chemin vers le dossier Scripts qui contient les ex\u00e9cutables Python install\u00e9s avec pip.

    Les conventions \u00e9voluent avec le temps. Voici un chemin possible\u2009:

    C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python3x\\Scripts\n
    ", "tags": ["Scripts", "pip", "PATH"]}, {"location": "tools/python/python/#installation-des-paquets-principaux", "title": "Installation des paquets principaux", "text": "

    Installons les paquets les plus couramment utilis\u00e9s avec Python. Ouvrez un terminal et tapez les commandes suivantes\u2009:

    pip install ipython numpy matplotlib pandas jupyterlab black flake8\n
    "}, {"location": "tools/python/python/#environnement-virtuel", "title": "Environnement virtuel", "text": "

    Lorsque vous installez des paquets avec PIP, un syst\u00e8me complexe de gestion de d\u00e9pendances est mis en place. Par exemple, si vous installez numpy, matplotlib sera install\u00e9 automatiquement. Chaque paquet d\u00e9pend d'autres paquets.

    Il n'est pas rare d'avoir des conflits entre les versions des paquets. Par exemple, si vous avez un projet qui utilise numpy en version 1.20 et un autre projet qui utilise numpy en version 1.21, vous aurez des probl\u00e8mes.

    Pour \u00e9viter ces probl\u00e8mes, il est recommand\u00e9 d'utiliser un environnement virtuel.

    Un environnement virtuel est un dossier local au projet sur lequel vous travaillez qui contient une installation de Python, un gestionnaire de paquets PIP et un ensemble de paquets. Chaque environnement virtuel est ind\u00e9pendant des autres. Vous pouvez avoir autant d'environnements virtuels que vous le souhaitez.

    La gestion d'environnement virtuel a beaucoup \u00e9volu\u00e9 avec les versions de Python. Il existe plusieurs outils que l'on peut confondre\u2009:

    • venv est un module de la biblioth\u00e8que standard de Python depuis la version 3.3 qui permet de cr\u00e9er des environnements virtuels. Il est recommand\u00e9 d'utiliser venv pour les projets personnels.
    • virtualenv est un outil plus ancien disponible pour Python 2 et 3. Il n'est pas recommand\u00e9 de l'utiliser.
    • poetry est un outil externe. Utile pour tester des projets sur diff\u00e9rentes versions de Python\u2009; facilite la migration entre les versions.

    Pour cr\u00e9er un environnement virtuel avec venv, ouvrez un terminal et tapez les commandes suivantes\u2009:

    Linux/WSLWindows
    python3 -m venv venv\nsource venv/bin/activate\n
    python -m venv venv\n.\\venv\\Scripts\\Activate\n
    ", "tags": ["numpy", "virtualenv", "venv", "matplotlib", "poetry"]}, {"location": "tools/python/python/#poetry", "title": "Poetry", "text": "

    Poetry est un outil de gestion de d\u00e9pendances pour Python. Il permet de g\u00e9rer les d\u00e9pendances de votre projet, de cr\u00e9er des environnements virtuels, de g\u00e9rer les versions de Python, de publier des paquets sur PyPI, etc.

    Il est une excellente alternative \u00e0 venv et pip car en plus de g\u00e9rer les d\u00e9pandances, il peut g\u00e9rer diff\u00e9rentes versions de Python.

    Si vous voulez cr\u00e9er un projet Python avec Poetry, ouvrez un terminal et tapez les commandes suivantes\u2009:

    mkdir myproject\ncd myproject\npoetry init\n

    Ensuite pour ajouter des d\u00e9pendances \u00e0 votre projet, utilisez poetry add :

    poetry add numpy matplotlib pandas\n

    Il est \u00e9galement possible d'ajouter des paquets de d\u00e9veloppement avec poetry add --dev :

    poetry add --group dev black flake8\n

    Ces commandes vont cr\u00e9er un fichier de configuration nomm\u00e9 pyproject.toml qui contient toutes les informations sur votre projet et ses d\u00e9pendances. L'avantage est qu'en mettant ce fichier sous Git, lorsque vous clonez votre projet sur une autre machine, il suffit de taper poetry install pour installer toutes les d\u00e9pendances.

    Par d\u00e9faut poetry ne cr\u00e9e pas d'enviroonement virtuel. Pour activer l'environnement virtuel, tapez la commande suivante\u2009:

    poetry shell\n

    Alternativement si l'objectif est simplement d'ex\u00e9cuter une commande dans l'environnement virtuel, utilisez poetry run :

    poetry run python main.py\n
    ", "tags": ["venv", "pyproject.toml", "pip"]}, {"location": "tools/python/python/#pipx", "title": "Pipx", "text": "

    pipx est un outil qui permet d'installer des paquets Python en dehors de l'environnement virtuel. Cela permet d'installer des paquets Python comme des ex\u00e9cutables. Par exemple, si vous voulez installer black ou flake8 pour formater votre code, vous pouvez les installer avec pipx :

    pipx install black\n

    Par d\u00e9faut \u00e0 partir de Ubuntu 24.04, pip n'est plus conseill\u00e9 pour installer des paquets Python. Le PEP 668 ayant \u00e9t\u00e9 accep\u00e9, la notion de Externally Managed Environment a \u00e9t\u00e9 introduite.

    Il y a plusieurs m\u00e9thodes pour installer des paquets dans une distributions Linux\u2009:

    1. pip install ...
    2. apt install python3-...

    La premi\u00e8re m\u00e9thode est la plus courante mais elle peut cr\u00e9er des probl\u00e8mes de compatibilit\u00e9 entre les paquets. Comme ces derniers sont install\u00e9s pour l'utilisateur courant, une version plus r\u00e9cente de numpy ne serait possiblement pas compatible avec un paquet Ubuntu qui d\u00e9pendrait d'une version plus ancienne de numpy.

    Il est pr\u00e9f\u00e9rable alors d'utiliser la version apt autant que possible.

    Alternativement, pour les outils en ligne de commande, il est possible d'utiliser pipx qui installe les paquets dans un environnement virtuel et cr\u00e9e des liens symboliques vers les ex\u00e9cutables dans le r\u00e9pertoire ~/.local/bin.

    ", "tags": ["numpy", "flake8", "pipx", "pip", "black", "apt"]}, {"location": "tools/python/python/#analyse-syntaxique-et-formatage", "title": "Analyse syntaxique et formatage", "text": "

    Il y a pl\u00e9tore d'outils pour analyser la syntaxe et formater le code Python. Les paquets flake8, pylint, isort, bandit ont \u00e9t\u00e9 tr\u00e8s populaires mais il est aujourd'hui recommand\u00e9 d'utiliser\u2009:

    1. black pour le formatage du code
    2. ruff pour l'analyse syntaxique
    3. mypy pour le typage statique
    ", "tags": ["pylint", "flake8", "mypy", "black", "bandit", "isort", "ruff"]}]} \ No newline at end of file +{"config": {"lang": ["fr"], "separator": "[\\s\\-]+", "pipeline": ["stopWordFilter"], "fields": {"title": {"boost": 1000.0}, "text": {"boost": 1.0}, "tags": {"boost": 1000000.0}}}, "docs": [{"location": "", "title": "Home", "text": "

    Bienvenue sur le cours de programmation

    ", "tags": ["InfoMicro"]}, {"location": "#preface", "title": "Pr\u00e9face", "text": "

    Cet ouvrage est destin\u00e9 aux \u00e9tudiants de premi\u00e8re ann\u00e9e Bachelor HEIG-VD, d\u00e9partement TIN et fili\u00e8res G\u00e9nie \u00e9lectrique. Il est une introduction \u00e0 la programmation en C. Il couvre la mati\u00e8re vue durant le cycle des cours Info1 et Info2 .

    Le contenu de ce cours est calqu\u00e9 sur les fiches d'unit\u00e9s de cours et de modules suivantes\u2009:

    • Module InfoMicro (InfoMicro)
    • Unit\u00e9 Informatique 1 (Info1 )
    • Unit\u00e9 Informatique 2 (Info2 )
    "}, {"location": "appendix/bibliography/", "title": "Bibliographie", "text": "

    Les r\u00e9f\u00e9rences utilis\u00e9es dans cet ouvrage sont les suivantes.

    "}, {"location": "appendix/bibliography/#normes", "title": "Normes", "text": "
    • ISO norme C 1999 - ISO/IEC 9899:1999 (9899:1999 pdf)
    • ISO norme C 2011 - ISO/IEC 9899:2011 (9899:201x pdf)
    • ISO norme C 2018 - ISO/IEC 9899:2018 (9899:202x pdf)
    • POSIX - IEEE Std 1003.1-2017
    • Unicode - Unicode 13.0
    • IEEE 754 - IEEE 754-2008
    • UML - OMG Unified Modeling Language (OMG UML), Superstructure, Version 2.5.1
    • BPMN - OMG Business Process Model and Notation (BPMN), Version 2.0
    "}, {"location": "appendix/bibliography/#livres", "title": "Livres", "text": "
    • Le guide complet du langage C - Claude Delanoy, 844 pages (ISBN-13 978-2212140125)
    • Le Langage C 2e \u00e9dition - K&R, 304 pages (ISBN-13 978-2100715770)
    • Cracking the coding interview - Gayle Laakmann, 687 pages (ISBN-13 978-0984782857)
    • C\u2009: The Complete Reference, 4th Ed. - Osborne, 2.5 pounds (ISBN-13 978-0070411838)
    • C Programming Absolute Beginner's Guide - Perry, 432 pages (ISBN-13 978-0789751980)
    • Clean Code\u2009: A Handbook of Agile Software Craftsmanship - Robert C. Martin, 464 pages (ISBN-13 978-0132350884)
    • Code Complete\u2009: A Practical Handbook of Software Construction - Steve McConnell, 960 pages (ISBN-13 978-0735619678)
    • The Mythical Man-Month\u2009: Essays on Software Engineering - Frederick P. Brooks Jr., 336 pages (ISBN-13 978-0201835953)
    • G\u00f6del, Escher, Bach\u2009: An Eternal Golden Braid - Douglas R. Hofstadter, 824 pages (ISBN-13 978-0465026562)
    • The Art of Computer Programming - Donald E. Knuth, 3168 pages (ISBN-13 978-0201896831)
    • Numerical Recipes in C\u2009: The Art of Scientific Computing - William H. Press, 1232 pages (ISBN-13 978-0521750332)
    • Introduction \u00e0 la programmation - Karl Tombre, 178 pages - v1.3 - \u00c9cole des mines de Nancy
    • Concepts fondamentaux de l'informatique - Alfred Vaino Aho et Jeffrey David Ullman, 872 pages (ISBN-13 978-2100031276)
    "}, {"location": "appendix/bibliography/#sites-web", "title": "Sites Web", "text": "
    • Un r\u00e9sum\u00e9 du langage C - Learn X in Y minutes
    • R\u00e9f\u00e9rence C compl\u00e8te - C Reference
    • Encyclop\u00e9die libre - Wikipedia
    • Questions R\u00e9ponses - StackOverflow
    • Bac \u00e0 sable pour expressions r\u00e9guli\u00e8res - Regex101
    • Documentation officielle - GNU C Library
    • Documentation officielle - GNU Compiler Collection
    • Documentation officielle - Clang
    "}, {"location": "appendix/bibliography/#problemes-en-lignes", "title": "Probl\u00e8mes en lignes", "text": "
    • CSES, 300 probl\u00e8mes d'algorithmique
    • Exercism
    • LeetCode
    • CodeWars
    • Project Euler
    • Advent of Code
    • CodeAbbey
    • Codingame
    "}, {"location": "appendix/grammar/", "title": "Grammaire C", "text": "

    Yacc (Yet Another COmpiler-Compiler) est un logiciel utilis\u00e9 pour \u00e9crire des analyseurs syntaxiques de code. Il prend en entr\u00e9e une grammaire.

    Parce que les informaticiens ont de l'humour, Yacc \u00e0 son pendant GNU Bison plus r\u00e9cent (1985) mais toujours activement d\u00e9velopp\u00e9.

    Voici \u00e0 titre d'information la d\u00e9finition formelle du langage C99\u2009:

    %{\n#include \"ast.h\"\n#include <stdio.h>\nvoid yyerror(const char *s);\nint yylex(void);\nextern int yylineno;\nASTNode *root;\n%}\n\n%token IDENTIFIER I_CONSTANT F_CONSTANT STRING_LITERAL FUNC_NAME SIZEOF\n%token PTR_OP INC_OP DEC_OP LEFT_OP RIGHT_OP LE_OP GE_OP EQ_OP NE_OP\n%token AND_OP OR_OP MUL_ASSIGN DIV_ASSIGN MOD_ASSIGN ADD_ASSIGN\n%token SUB_ASSIGN LEFT_ASSIGN RIGHT_ASSIGN AND_ASSIGN\n%token XOR_ASSIGN OR_ASSIGN\n%token TYPEDEF_NAME ENUMERATION_CONSTANT\n\n%token TYPEDEF EXTERN STATIC AUTO REGISTER INLINE\n%token CONST RESTRICT VOLATILE\n%token BOOL CHAR SHORT INT LONG SIGNED UNSIGNED FLOAT DOUBLE VOID\n%token COMPLEX IMAGINARY\n%token STRUCT UNION ENUM ELLIPSIS\n\n%token CASE DEFAULT IF ELSE SWITCH WHILE DO FOR GOTO CONTINUE BREAK RETURN\n\n%token ALIGNAS ALIGNOF ATOMIC GENERIC NORETURN STATIC_ASSERT THREAD_LOCAL\n\n%start translation_unit\n%%\n\nprimary_expression\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | constant {\n        $$ = $1;\n    }\n    | string {\n        $$ = $1;\n    }\n    | '(' expression ')' {\n        $$ = $2;\n    }\n    | generic_selection {\n        $$ = $1;\n    }\n    ;\n\nconstant\n    : I_CONSTANT {\n        $$ = create_constant_node($1);\n    }\n    | F_CONSTANT {\n        $$ = create_constant_node($1);\n    }\n    | ENUMERATION_CONSTANT {\n        $$ = create_identifier_node($1);\n    }\n    ;\n\nstring\n    : STRING_LITERAL {\n        $$ = create_string_node($1);\n    }\n    | FUNC_NAME {\n        $$ = create_string_node($1);\n    }\n    ;\n\ngeneric_selection\n    : GENERIC '(' assignment_expression ',' generic_assoc_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ngeneric_assoc_list\n    : generic_association\n    | generic_assoc_list ',' generic_association\n    ;\n\ngeneric_association\n    : type_name ':' assignment_expression\n    | DEFAULT ':' assignment_expression\n    ;\n\npostfix_expression\n    : primary_expression {\n        $$ = $1;\n    }\n    | postfix_expression '[' expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '(' argument_expression_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression '.' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression PTR_OP IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | postfix_expression INC_OP {\n        $$ = create_unary_op_node(INC_OP, $1);\n    }\n    | postfix_expression DEC_OP {\n        $$ = create_unary_op_node(DEC_OP, $1);\n    }\n    | '(' type_name ')' '{' initializer_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' type_name ')' '{' initializer_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nargument_expression_list\n    : assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | argument_expression_list ',' assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nunary_expression\n    : postfix_expression {\n        $$ = $1;\n    }\n    | INC_OP unary_expression {\n        $$ = create_unary_op_node(INC_OP, $2);\n    }\n    | DEC_OP unary_expression {\n        $$ = create_unary_op_node(DEC_OP, $2);\n    }\n    | unary_operator cast_expression {\n        $$ = create_unary_op_node($1, $2);\n    }\n    | SIZEOF unary_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SIZEOF '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ALIGNOF '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nunary_operator\n    : '&' {\n        $$ = '&';\n    }\n    | '*' {\n        $$ = '*';\n    }\n    | '+' {\n        $$ = '+';\n    }\n    | '-' {\n        $$ = '-';\n    }\n    | '~' {\n        $$ = '~';\n    }\n    | '!' {\n        $$ = '!';\n    }\n    ;\n\ncast_expression\n    : unary_expression {\n        $$ = $1;\n    }\n    | '(' type_name ')' cast_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nmultiplicative_expression\n    : cast_expression {\n        $$ = $1;\n    }\n    | multiplicative_expression '*' cast_expression {\n        $$ = create_binary_op_node('*', $1, $3);\n    }\n    | multiplicative_expression '/' cast_expression {\n        $$ = create_binary_op_node('/', $1, $3);\n    }\n    | multiplicative_expression '%' cast_expression {\n        $$ = create_binary_op_node('%', $1, $3);\n    }\n    ;\n\nadditive_expression\n    : multiplicative_expression {\n        $$ = $1;\n    }\n    | additive_expression '+' multiplicative_expression {\n        $$ = create_binary_op_node('+', $1, $3);\n    }\n    | additive_expression '-' multiplicative_expression {\n        $$ = create_binary_op_node('-', $1, $3);\n    }\n    ;\n\nshift_expression\n    : additive_expression {\n        $$ = $1;\n    }\n    | shift_expression LEFT_OP additive_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | shift_expression RIGHT_OP additive_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nrelational_expression\n    : shift_expression {\n        $$ = $1;\n    }\n    | relational_expression '<' shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression '>' shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression LE_OP shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | relational_expression GE_OP shift_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nequality_expression\n    : relational_expression {\n        $$ = $1;\n    }\n    | equality_expression EQ_OP relational_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | equality_expression NE_OP relational_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nand_expression\n    : equality_expression {\n        $$ = $1;\n    }\n    | and_expression '&' equality_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexclusive_or_expression\n    : and_expression {\n        $$ = $1;\n    }\n    | exclusive_or_expression '^' and_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninclusive_or_expression\n    : exclusive_or_expression {\n        $$ = $1;\n    }\n    | inclusive_or_expression '|' exclusive_or_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlogical_and_expression\n    : inclusive_or_expression {\n        $$ = $1;\n    }\n    | logical_and_expression AND_OP inclusive_or_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlogical_or_expression\n    : logical_and_expression {\n        $$ = $1;\n    }\n    | logical_or_expression OR_OP logical_and_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nconditional_expression\n    : logical_or_expression {\n        $$ = $1;\n    }\n    | logical_or_expression '?' expression ':' conditional_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nassignment_expression\n    : conditional_expression {\n        $$ = $1;\n    }\n    | unary_expression assignment_operator assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nassignment_operator\n    : '=' {\n        $$ = '=';\n    }\n    | MUL_ASSIGN {\n        $$ = MUL_ASSIGN;\n    }\n    | DIV_ASSIGN {\n        $$ = DIV_ASSIGN;\n    }\n    | MOD_ASSIGN {\n        $$ = MOD_ASSIGN;\n    }\n    | ADD_ASSIGN {\n        $$ = ADD_ASSIGN;\n    }\n    | SUB_ASSIGN {\n        $$ = SUB_ASSIGN;\n    }\n    | LEFT_ASSIGN {\n        $$ = LEFT_ASSIGN;\n    }\n    | RIGHT_ASSIGN {\n        $$ = RIGHT_ASSIGN;\n    }\n    | AND_ASSIGN {\n        $$ = AND_ASSIGN;\n    }\n    | XOR_ASSIGN {\n        $$ = XOR_ASSIGN;\n    }\n    | OR_ASSIGN {\n        $$ = OR_ASSIGN;\n    }\n    ;\n\nexpression\n    : assignment_expression {\n        $$ = $1;\n    }\n    | expression ',' assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nconstant_expression\n    : conditional_expression {\n        $$ = $1;\n    }\n    ;\n\ndeclaration\n    : declaration_specifiers ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers init_declarator_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | static_assert_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclaration_specifiers\n    : storage_class_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | storage_class_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | function_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | function_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | alignment_specifier declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | alignment_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninit_declarator_list\n    : init_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | init_declarator_list ',' init_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninit_declarator\n    : declarator '=' initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstorage_class_specifier\n    : TYPEDEF {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | EXTERN {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | STATIC {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | THREAD_LOCAL {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | AUTO {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | REGISTER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_specifier\n    : VOID {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CHAR {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SHORT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | INT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | LONG {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FLOAT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DOUBLE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SIGNED {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | UNSIGNED {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | BOOL {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | COMPLEX {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | IMAGINARY {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | atomic_type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enum_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | TYPEDEF_NAME {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_or_union_specifier\n    : struct_or_union '{' struct_declaration_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union IDENTIFIER '{' struct_declaration_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_or_union IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_or_union\n    : STRUCT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | UNION {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declaration_list\n    : struct_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_declaration_list struct_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declaration\n    : specifier_qualifier_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | specifier_qualifier_list struct_declarator_list ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | static_assert_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nspecifier_qualifier_list\n    : type_specifier specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_specifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declarator_list\n    : struct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | struct_declarator_list ',' struct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstruct_declarator\n    : ':' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator ':' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenum_specifier\n    : ENUM '{' enumerator_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM '{' enumerator_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER '{' enumerator_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER '{' enumerator_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ENUM IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenumerator_list\n    : enumerator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enumerator_list ',' enumerator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nenumerator\n    : enumeration_constant '=' constant_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | enumeration_constant {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\natomic_type_specifier\n    : ATOMIC '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_qualifier\n    : CONST {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RESTRICT {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | VOLATILE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ATOMIC {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nfunction_specifier\n    : INLINE {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | NORETURN {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nalignment_specifier\n    : ALIGNAS '(' type_name ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | ALIGNAS '(' constant_expression ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclarator\n    : pointer direct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndirect_declarator\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | '(' declarator ')' {\n        $$ = $2;\n    }\n    | direct_declarator '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_declarator '(' identifier_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\npointer\n    : '*' type_qualifier_list pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' type_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '*' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_qualifier_list\n    : type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | type_qualifier_list type_qualifier {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_type_list\n    : parameter_list ',' ELLIPSIS {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | parameter_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_list\n    : parameter_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | parameter_list ',' parameter_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nparameter_declaration\n    : declaration_specifiers declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nidentifier_list\n    : IDENTIFIER {\n        $$ = create_identifier_node($1);\n    }\n    | identifier_list ',' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntype_name\n    : specifier_qualifier_list abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | specifier_qualifier_list {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nabstract_declarator\n    : pointer direct_abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | pointer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndirect_abstract_declarator\n    : '(' abstract_declarator ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' '*' ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' STATIC type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list STATIC assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' type_qualifier_list ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '[' assignment_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '(' ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | direct_abstract_declarator '(' parameter_type_list ')' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninitializer\n    : '{' initializer_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '{' initializer_list ',' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | assignment_expression {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ninitializer_list\n    : designation initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer_list ',' designation initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | initializer_list ',' initializer {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignation\n    : designator_list '=' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignator_list\n    : designator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | designator_list designator {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndesignator\n    : '[' constant_expression ']' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '.' IDENTIFIER {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstatic_assert_declaration\n    : STATIC_ASSERT '(' constant_expression ',' STRING_LITERAL ')' ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nstatement\n    : labeled_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | expression_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | selection_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | iteration_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | jump_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nlabeled_statement\n    : IDENTIFIER ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CASE constant_expression ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DEFAULT ':' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ncompound_statement\n    : '{' '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | '{' block_item_list '}' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nblock_item_list\n    : block_item {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | block_item_list block_item {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nblock_item\n    : declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexpression_statement\n    : ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | expression ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nselection_statement\n    : IF '(' expression ')' statement ELSE statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | IF '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | SWITCH '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\niteration_statement\n    : WHILE '(' expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | DO statement WHILE '(' expression ')' ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' expression_statement expression_statement ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' expression_statement expression_statement expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' declaration expression_statement ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | FOR '(' declaration expression_statement expression ')' statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\njump_statement\n    : GOTO IDENTIFIER ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | CONTINUE ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | BREAK ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RETURN ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | RETURN expression ';' {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ntranslation_unit\n    : external_declaration {\n        root = $1;\n    }\n    | translation_unit external_declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nexternal_declaration\n    : function_definition {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\nfunction_definition\n    : declaration_specifiers declarator declaration_list compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_specifiers declarator compound_statement {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\ndeclaration_list\n    : declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    | declaration_list declaration {\n        // Cr\u00e9ez le n\u0153ud correspondant\n    }\n    ;\n\n%%\n#include <stdio.h>\n\nvoid yyerror(const char *s) {\n    fprintf(stderr, \"Error: %s at line %d\\n\", s, yylineno);\n}\n\nint yylex(void);\n\nint main(int argc, char **argv) {\n    if (argc > 1) {\n        FILE *file = fopen(argv[1], \"r\");\n        if (!file) {\n            fprintf(stderr, \"Could not open file %s\\n\", argv[1]);\n            return 1;\n        }\n        yyin = file;\n    }\n    yyparse();\n    if (root) {\n        print_ast(root);\n    }\n    return 0;\n}\n

    A partir de cette grammaire, Bison g\u00e9n\u00e8re un fichier c99.tab.c qui contient le code C de l'analyseur syntaxique.

    Pour la cr\u00e9er vous-m\u00eame, vous pouvez utiliser la commande suivante\u2009:

    bison -d -o c99.tab.c c99.y\n
    ", "tags": ["c99.tab.c"]}, {"location": "appendix/laboratories/", "title": "Laboratoires", "text": "

    Les laboratoires sont des travaux pratiques permettant \u00e0 l'\u00e9tudiant d'attaquer des probl\u00e8mes de programmation plus difficiles que les exercices faits en classe.

    "}, {"location": "appendix/laboratories/#protocole", "title": "Protocole", "text": "
    1. R\u00e9cup\u00e9rer le r\u00e9f\u00e9rentiel du laboratoire en utilisant GitHub Classroom.
    2. Prendre connaissance du cahier des charges.
    3. R\u00e9diger le code.
    4. Le tester.
    5. R\u00e9diger votre rapport de test si demand\u00e9.
    6. Le soumettre avant la date butoir.
    "}, {"location": "appendix/laboratories/#evaluation", "title": "\u00c9valuation", "text": "

    Une grille d'\u00e9valuation est int\u00e9gr\u00e9e \u00e0 tous les laboratoires. Elle prend la forme d'un fichier criteria.yml que l'\u00e9tudiant peut consulter en tout temps.

    ", "tags": ["criteria.yml"]}, {"location": "appendix/laboratories/#directives", "title": "Directives", "text": "
    • La recherche sur internet est autoris\u00e9e et conseill\u00e9e.
    • Le plagiat n'est pas autoris\u00e9, et sanctionn\u00e9 si d\u00e9couvert par la note de 1.0.
    • Le rendu pass\u00e9 la date butoir est sanctionn\u00e9 \u00e0 raison de 1 point puis 1/24 de point par heure de retard.
    "}, {"location": "appendix/laboratories/#format-de-rendu", "title": "Format de rendu", "text": "
    • Fin de lignes\u2009: LF ('\\n').
    • Encodage\u2009: UTF-8 sans BOM.
    • Code source respectueux de ISO/IEC 9899:1999.
    • Le code doit comporter un exemple d'utilisation et une documentation mise \u00e0 jour dans README.md.
    • Lorsqu'un rapport est demand\u00e9, vous le placerez dans REPORT.md.
    ", "tags": ["README.md", "REPORT.md"]}, {"location": "appendix/laboratories/#anatomie-dun-travail-pratique", "title": "Anatomie d'un travail pratique", "text": "

    Un certain nombre de fichiers vous sont donn\u00e9s, il est utile de les conna\u00eetre. Un r\u00e9f\u00e9rentiel sera g\u00e9n\u00e9ralement compos\u00e9 des \u00e9l\u00e9ments suivants\u2009:

    $ tree\n.\n\u251c\u2500\u2500 .clang-format\n\u251c\u2500\u2500 .devcontainer\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 Dockerfile\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 devcontainer.json\n\u251c\u2500\u2500 .editorconfig\n\u251c\u2500\u2500 .gitattributes\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 .vscode\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 launch.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 tasks.json\n\u251c\u2500\u2500 Makefile\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 assets\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 test.txt\n\u251c\u2500\u2500 foo.c\n\u251c\u2500\u2500 foo.h\n\u251c\u2500\u2500 main.c\n\u251c\u2500\u2500 criteria.yml\n\u2514\u2500\u2500 tests\n    \u251c\u2500\u2500 Makefile\n \u00a0\u00a0 \u2514\u2500\u2500 test_foo.c\n
    "}, {"location": "appendix/laboratories/#readmemd", "title": "README.md", "text": "

    Il s'agit de la documentation principale de votre r\u00e9f\u00e9rentiel. Elle contient la donn\u00e9e du travail pratique en format Markdown. Ce fichier est \u00e9galement utilis\u00e9 par d\u00e9faut dans GitHub. Il contient notamment le titre du laboratoire, la dur\u00e9e, le d\u00e9lai de rendu et le format individuel ou de groupe\u2009:

    # Laboratoire <!-- omit in toc -->\n- **Dur\u00e9e**: 2 p\u00e9riodes + environ 3h \u00e0 la maison\n- **Date de rendu**: dimanche avant minuit\n- **Format**: travail individuel\n...\n
    "}, {"location": "appendix/laboratories/#criteriayml", "title": "criteria.yml", "text": "

    Ce fichier contient les directives d'\u00e9valuation du travail pratique. Il est au format YAML. Pour chaque point \u00e9valu\u00e9 une description est donn\u00e9e avec la cl\u00e9 description et un nombre de points est sp\u00e9cifi\u00e9. Une exigence peut avoir soit un nombre de points positifs soit n\u00e9gatifs. Les points n\u00e9gatifs agissent comme une p\u00e9nalit\u00e9. Ce choix d'avoir des points et des p\u00e9nalit\u00e9s permet de ne pas diluer les exigences au travers d'autres crit\u00e8res importants, mais normalement respect\u00e9s des \u00e9tudiants.

    Des points bonus sont donn\u00e9s si le programme dispose d'une aide et d'une version et si la fonctionnalit\u00e9 du programme est \u00e9tendue.

    # Crit\u00e8res d'\u00e9valuation du travail pratique\n%YAML 1.2\n---\ntests:\n    build:\n        description: Le programme compile sans erreurs ni warning\n        points: 0/-4\n        test: test_build\n    unit-testing:\n        function_foo:\n        points: 0/10\n        test: test_foo\n        function_bar:\n        points: 0/10\n        test: test_bar\n    functional-testing:\n        arguments:\n        description: La lecture des arguments fonctionne comme demand\u00e9\n        points: 0/7\n        test: test_arguments\n        output-display:\n        description: Affichage sur stdout/stderr comme sp\u00e9cifi\u00e9\n        points: 0/3\n        test: test_output\n        errors:\n        description: Le programme affiche des erreurs si rencontr\u00e9es\n        points: 0/2\n        test: test_errors\nreport:\n    introduction:\n        description: Le rapport de test contient une introduction\n        points: 0/2\n    conclusion:\n        description: Le rapport de test contient une conclusion\n        points: 0/2\n    analysis:\n        description: Le rapport de test contient une analyse du comportement\n        points: 0/3\ncode:\n    specifications:\n        prototypes:\n            description: Les prototypes des fonctions demand\u00e9es sont respect\u00e9s\n            points: 0/3\n        main:\n            description: Le programme principal est minimaliste\n            points: 0/3\n        algorithm:\n            description: L'algorithme de encode/decode est bien pens\u00e9\n            points: 0/5\n    comments:\n        header:\n        description: Un en-t\u00eate programme est clairement d\u00e9fini\n        points: 0/2\n        purpose:\n        description: Les commentaires sont pertinents\n        points: 0/-2\n        commented-code:\n        description: Du code est comment\u00e9\n        points: 0/-2\n    variables:\n        naming:\n        description: Le noms des variables est minimaliste et explicite\n        points: 0/2\n        scope:\n        description: La port\u00e9e des variables est r\u00e9duite au minimum\n        points: 0/2\n        type:\n        description: Le type des variables est appropri\u00e9\n        points: 0/2\n    functions:\n        length:\n        description: La longueur des fonctions est raisonnable\n        points: 0/-4\n    control-flow:\n        description: Les structures de contr\u00f4le sont appropri\u00e9es\n        points: 0/4\n    overall:\n        dry:\n        description: Pas de r\u00e9p\u00e9tition dans le code\n        points: 0/-5\n        kiss:\n        description: Le code est minimaliste et simple\n        points: 0/-5\n        ssot:\n        description: Pas de r\u00e9p\u00e9tition d'information\n        points: 0/-5\n        indentation:\n        description: L'indentation du code est coh\u00e9rente\n        points: 0/-5\nbonus:\n    help:\n        description: Le programme dispose d'une aide\n        bonus: 0/1\n        test: test_help\n    version:\n        description: La version du programme peut \u00eatre affich\u00e9e\n        bonus: 0/1\n        test: test_version\n    extension:\n        description: La fonctionnalit\u00e9 du programme est \u00e9tendue\n        bonus: 0/3\n    english:\n        description: Usage de l'anglais\n        bonus: 0/1\n

    Ce fichier est utilis\u00e9 par des tests automatique pour faciliter la correction du travail pratique.

    ", "tags": ["description"]}, {"location": "appendix/refcards/", "title": "Cartes de r\u00e9f\u00e9rence", "text": "

    Des cartes de r\u00e9f\u00e9rences A4 recto-verso ont \u00e9t\u00e9 r\u00e9alis\u00e9es pour diff\u00e9rents sujets d'informatique. Elles sont disponibles en version num\u00e9rique et en version papier \u00e0 la HEIG-VD.

    Ces cartes sont normalement autoris\u00e9es durant les examens et travaux \u00e9crits. Pri\u00e8re de v\u00e9rifier avec les enseignants concern\u00e9s avant de les utiliser.

    "}, {"location": "appendix/refcards/#carte-de-reference-c", "title": "Carte de r\u00e9f\u00e9rence C", "text": "

    Carte de r\u00e9f\u00e9rence C

    "}, {"location": "appendix/refcards/#carte-de-reference-c_1", "title": "Carte de r\u00e9f\u00e9rence C++", "text": "

    Carte de r\u00e9f\u00e9rence C

    "}, {"location": "appendix/refcards/#carte-de-reference-de-programmation-concurrente", "title": "Carte de r\u00e9f\u00e9rence de programmation concurrente", "text": "

    Carte de r\u00e9f\u00e9rence Programmation concurrente

    "}, {"location": "appendix/refcards/#carte-de-reference-python", "title": "Carte de r\u00e9f\u00e9rence Python", "text": "

    Carte de r\u00e9f\u00e9rence Python

    "}, {"location": "appendix/refcards/#carte-de-reference-latex", "title": "Carte de r\u00e9f\u00e9rence LaTeX", "text": "

    Carte de r\u00e9f\u00e9rence LaTeX

    "}, {"location": "appendix/unit/", "title": "Fiches d'unit\u00e9s de cours", "text": "

    Les fiches d'unit\u00e9s sont les documents de r\u00e9f\u00e9rence pour les cours d'info1 et d'info2, ici pr\u00e9sent\u00e9es sous forme de donn\u00e9es brutes au format YAML.

    "}, {"location": "appendix/unit/#informatique-1", "title": "Informatique 1", "text": "
    # Version formelle et \u00e9tendue de la fiche d'unit\u00e9 de cours disponible sur GAPS\n# http://gaps.heig-vd.ch/public/fiches/uv/uv.php?id=5637\n---\ntitle:\n  name: Informatique 1\n  tag: info1\n  id: 6488\ndomain: Ing\u00e9nierie et Architecture\nfili\u00e8re: G\u00e9nie \u00e9lectrique\norientations:\n  EAI:\n  EEM:\n  MI:\nformation: Plein temps\nvalidityDate:\n  - 2021-2022\nauthor: Pierre Bressy\ncharge:\n  academicHours: 150\n  inClassAcademicHours: 96\nplanning:\n  s1:\n    class: # 48\n      - hours: 4\n        chapters:\n          - Introduction.\n          - Aper\u00e7u du fonctionnement de l'ordinateur.\n          - Codage de l'information\n      - hours: 2\n        chapters:\n          - Pr\u00e9sentation du langage C\n      - hours: 12\n        chapters:\n          - Types de donn\u00e9es de base\n          - Variables\n          - Constantes\n          - Op\u00e9rateurs\n          - Entr\u00e9es et sorties console\n      - hours: 8\n        chapters:\n          - Structures de contr\u00f4le\n          - Branchements\n          - Boucles\n      - hours: 6\n        chapters:\n          - Fonctions\n          - Passage par valeur et par adresse\n      - hours: 8\n        chapters:\n          - Tableaux\n          - Cha\u00eenes de caract\u00e8res\n      - hours: 4\n        chapters:\n          - Introduction \u00e0 l'analyse et \u00e0 la conception (d\u00e9coupage du probl\u00e8me)\n      - hours: 4\n        chapters:\n          - Contr\u00f4le continu et corrections\n    laboratory:\n      - hours: 2\n        chapters:\n          - Mise en place de l'environnement de travail.\n      - hours: 2\n        chapters:\n          - Environnement de d\u00e9veloppement int\u00e9gr\u00e9\n            - Installation\n            - Configuration\n            - \u00c9dition\n            - Compilation\n      - hours: 8\n        chapters:\n          - Dialogues utilisateurs\n      - hours: 10\n        chapters:\n          - Th\u00e9orie sur les instructions if, for, while, do..while, switch.\n          - Utilisation des structures de contr\u00f4le\n      - hours: 8\n        chapters:\n          - Type de donn\u00e9es compos\u00e9s\n      - hours: 8\n        chapters:\n          - Mini projet.\nprerequisites: |\n  L'\u00e9tudiant-e doit conna\u00eetre et savoir utiliser les notions suivantes\n    - utilisation g\u00e9n\u00e9rale d'un syst\u00e8me d'exploitation graphique notamment\n      la gestion de fichiers et les bases des outils de bureautique,\n    - notation binaire, octale et hexad\u00e9cimale et de l'alg\u00e8bre bool\u00e9enne\n    \u00e9l\u00e9mentaire.\ngoals:\n  classroom:\n    - |\n      Expliquer les principes g\u00e9n\u00e9raux de repr\u00e9sentation de l'information\n      dans les ordinateurs.\n    - |\n      D\u00e9crire la marche \u00e0 suivre et les outils n\u00e9cessaires pour cr\u00e9er\n      un programme ex\u00e9cutable.\n    - |\n      Assurer la tra\u00e7abilit\u00e9 du code source de la conception \u00e0 la\n      livraison du programme.\n    - |\n      Citer les \u00e9l\u00e9ments syntaxiques du langage C utilis\u00e9 couramment pour\n      \u00e9crire des programmes.\n    - |\n      Choisir le type de donn\u00e9es le plus adapt\u00e9 pour repr\u00e9senter une\n      information physique.\n    - |\n      Concevoir et programmer un dialogue op\u00e9rateur en mode console.\n    - |\n      Formater un affichage sur la sortie standard pour le rendre lisible.\n    - |\n      Calculer la valeur d'une expression construite avec diff\u00e9rents\n      op\u00e9rateurs du langage C et en d\u00e9terminer le type de stockage r\u00e9sultant.\n    - |\n      Choisir la structure de contr\u00f4le appropri\u00e9e pour r\u00e9soudre un probl\u00e8me\n      algorithmique simple.\n    - |\n      Concevoir et impl\u00e9menter un algorithme imbriquant jusqu'\u00e0 trois niveaux\n      de structure de contr\u00f4le.\n    - |\n      Cr\u00e9er une fonction impliquant un passage de param\u00e8tre par valeur et\n      par adresse.\n    - |\n      Utiliser le \u00ab type tableau \u00bb multidimensionnel et manipuler ses\n      \u00e9l\u00e9ments constituants.\n    - |\n      Manipulation simple de cha\u00eenes de caract\u00e8res en utilisant la\n      biblioth\u00e8que standard.\n    - |\n      Mettre en \u0153uvre des algorithmes utilisant des fonctions math\u00e9matiques\n      de la biblioth\u00e8que standard.\n    - D\u00e9boguer un programme informatique en utilisant des points d'arr\u00eat.\n    - |\n      Interagir avec un programme ex\u00e9cutable via les arguments et les flux\n      d'entr\u00e9es sorties.\n    - Conna\u00eetre les idiomes (patron d'impl\u00e9mentation) de base (SSOT, DRY, KISS).\n  laboratory:\n    - |\n      Installer et configurer un environnement de d\u00e9veloppement int\u00e9gr\u00e9 (IDE)\n      pour le langage C.\n    - |\n      Cr\u00e9er des programmes avec un IDE et compiler un programme en ligne de\n      commande.\n    - Construire une liste d'arguments.\n    - |\n      Cr\u00e9er un programme g\u00e9rant un menu en mode console et affichant des\n      r\u00e9sultats sous forme structur\u00e9e.\n    - |\n      Mettre au point it\u00e9rativement un programme pour atteindre un\n      fonctionnement fiable et ergonomique.\n    - |\n      Comprendre un cahier des charges, identifier et clarifier les exigences\n      importantes, et s'y conformer.\n    - |\n      Analyser de mani\u00e8re autonome les probl\u00e8mes rencontr\u00e9s et proposer\n      une solution impl\u00e9mentable.\n    - Livrer un logiciel en assurant sa tra\u00e7abilit\u00e9 en respectant un d\u00e9lai.\n    - |\n      Citer des applications pratiques de la programmation en relation avec\n      ses futurs d\u00e9bouch\u00e9s professionnels.\n    - Chercher des solutions par soi-m\u00eame en utilisant internet.\nplan:\n  - Num\u00e9ration\n    - Bases (syst\u00e8me d\u00e9cimal, hexad\u00e9cimal, octal, binaire)\n    - Conversion de bases\n    - Compl\u00e9ment \u00e0 un\n    - Compl\u00e9ment \u00e0 deux\n    - Arithm\u00e9tique binaire (et, ou, ou exclusif, n\u00e9gation)\n  - Processus de d\u00e9veloppement\n    - Outils\n    - Environnement int\u00e9gr\u00e9 (IDE)\n    - Compilateur (*compiler*)\n    - Cha\u00eene de d\u00e9veloppement (*toolchain*)\n    - Cycle de d\u00e9veloppement\n    - Cycle de compilation\n    - Installation d'un environnement de d\u00e9veloppement\n    - Programmes et processus\n  - G\u00e9n\u00e9ralit\u00e9s du langage C\n    - S\u00e9quences\n    - Embranchements (if, switch)\n    - Boucles (while, do..while, for)\n    - Sauts (break, continue, return, goto)\n  - Types de donn\u00e9es\n    - Typage\n    - Stockage des donn\u00e9es en m\u00e9moire\n    - Entiers naturels\n    - Entiers relatifs\n    - Nombres r\u00e9els (virgule flottante)\n    - Caract\u00e8res\n    - Table ASCII\n    - Cha\u00eenes de caract\u00e8res\n    - Bool\u00e9ens\n  - Interaction utilisateur en mode console\n    - Entr\u00e9e standard\n    - Sortie standard\n    - Sortie d'erreur standard\n    - Questions/R\u00e9ponses avec `printf` et `scanf`\n    - Formater un r\u00e9sultat sous forme tabul\u00e9e et lisible\n    - Menu (choix multiples)\n  - Op\u00e9rateurs\n    - Op\u00e9rateurs du langage C\n    - Priorit\u00e9 des op\u00e9rateurs\n    - Expressions\n    - Promotion et promotion implicite\n  - Conception\n    - Choix des structures de contr\u00f4les adapt\u00e9es \u00e0 des probl\u00e8mes\n    - Algorithmes simple (min, max, moyenne, ...)\n    - Manipulation de cha\u00eenes\n    - Manipulation de tableaux\n    - Manipulation de bits\n  - Algorithmie\n    - Complexit\u00e9 d'un algorithme\n    - Exemples d'algorithmes\n    - Algorithmes de tri (tri \u00e0 bulle)\n  - Fonctions\n    - Passage par valeur et par adresse\n    - Utilisation de la valeur de retour\n    - Prototypes de fonctions\n  - Types de donn\u00e9es compos\u00e9es\n    - Structures\n    - Unions\n    - Tableaux\n    - \u00c9num\u00e9rations\n  - Biblioth\u00e8ques standard\n    - <math.h>\n    - Fonctions trigonom\u00e9triques\n    - Exponentielle\n    - Logarithme\n    - <string.h>\n    - Comparaison de cha\u00eenes de caract\u00e8res\n    - Concat\u00e9nation de cha\u00eenes de caract\u00e8res\n    - Copie de cha\u00eenes de caract\u00e8res\n    - Longueur d'une cha\u00eene de caract\u00e8res\n    - Recherche d'une sous-cha\u00eene dans une cha\u00eene de caract\u00e8res\n    - <stdio.h>\n    - printf\n    - scanf\n    - putchar\n    - getchar\n    - puts\n    - gets\n  - Structure du code\n    - Corriger les erreurs de syntaxes\n    - Corriger les erreurs s\u00e9mantiques\n    - Indentation du code\n    - Commentaires\n
    "}, {"location": "appendix/unit/#panification-du-semestre-dhiver", "title": "Panification du semestre d'hiver", "text": "Semaine Acad\u00e9mique Cours Labo 38 1 Introduction 00 Familiarisation 39 2 Num\u00e9ration 01 Premier pas en C 40 3 Fondements du C 02 \u00c9quation quadratique 41 4 Variables, op\u00e9rateurs 03 Fl\u00e9chettes 42 5 Types, entr\u00e9es sorties 04 Pneus 43 Vacances d'automne 44 6 Entr\u00e9es sorties 05 Monte-Carlo 45 7 TE1 06 Tables Multiplications 46 8 Structure de contr\u00f4les 07 Cha\u00eenes (par \u00e9quipe) 47 9 Fonctions 08 Nombre d'Armstrong 48 10 Tableaux et structures 09 Sudoku 49 11 Programmes et processus 50 12 Algorithmique Labo Test 51 13 Pointeurs 10 Galton 52 Vacances de No\u00ebl 1 2 14 Ergonomie et dialogues 12 Tableau des scores 3 15 TE2 4 16 Exercices de r\u00e9vision 5 Pr\u00e9paration Examens 6 Examens 7 Rel\u00e2ches"}, {"location": "appendix/unit/#informatique-2", "title": "Informatique 2", "text": "
    # Version formelle et \u00e9tendue de la fiche d'unit\u00e9 de cours disponible sur GAPS\n# https://gaps.heig-vd.ch/consultation/fiches/uv/uv.php?id=6491\n---\ntitle:\n  name: Informatique 2\n  tag: info2\n  id: 6491\ndomain: Ing\u00e9nierie et Architecture\nfili\u00e8re: G\u00e9nie \u00e9lectrique\norientations:\n  EAI:\n  EEM:\n  MI:\nformation: Plein temps\nvalidityDate:\n  - 2021-2022\nauthor: Pierre Bressy\ncharge:\n  academicHours: 120\n  inClassAcademicHours: 80\nplanning:\n  s1:\n    class: # 48\n      - hours: 4\n        chapters:\n          - \"Pr\u00e9processeur (#include, #define, #if, #pragma)\"\n      - hours: 4\n        chapters:\n          - Classes de stockage (static, volatile, extern)\n      - hours: 8\n        chapters:\n          - Conception de type de donn\u00e9es abstraits simples\n          - Cr\u00e9ation de biblioth\u00e8ques\n      - hours: 10\n        chapters:\n          - Pointeurs, arithm\u00e9tique de pointeurs\n          - Allocation dynamique\n          - Segments m\u00e9moire (stack, heap)\n      - hours: 6\n        chapters:\n          - Impl\u00e9mentation des listes\n          - Queues et files d'attente bas\u00e9e sur les tableaux\n      - hours: 8\n        chapters:\n          - Type de donn\u00e9es r\u00e9cursifs, queues et files d'attente\n      - hours: 4\n        chapters:\n          - Gestion des flux (stdin, stdout, stderr)\n          - Fichiers binaires et textes\n      - hours: 4\n        chapters:\n          - Contr\u00f4les continus\n    laboratory:\n      - hours: 6\n        chapters:\n          - >\n            Mise en \u0153uvre de type de donn\u00e9es compos\u00e9es\n            (structures, tableaux multidimensionnels)\n      - hours: 4\n        chapters:\n          - Lecture et \u00e9criture de fichiers texte et binaire en mode s\u00e9quentiel\n      - hours: 4\n        chapters:\n          - Mise en \u0153uvre de l'allocation dynamique de m\u00e9moire\n      - hours: 2\n        chapters:\n          - Compilation s\u00e9par\u00e9e et impl\u00e9mentation de biblioth\u00e8ques\n      - hours: 4\n        chapters:\n          - Impl\u00e9mentation de types de donn\u00e9es abstraits, type simple, liste tableau\n      - hours: 6\n        chapters:\n          - Impl\u00e9mentation de types de donn\u00e9es abstraits, file, pile\n      - hours: 6\n        chapters:\n          - Mini-projet\nprerequisites: |\n  L'\u00e9tudiant-e doit conna\u00eetre et savoir utiliser les notions suivantes\n      - bases de la programmation en C : types de base, structures\n        de contr\u00f4le et sous-programmes,\n      - utilisation d'un environnement de d\u00e9veloppement,\n        compilation et ex\u00e9cution de programmes.\n  L'unit\u00e9 d'enseignement Informatique 1 permet d'acqu\u00e9rir ces connaissances.\n\ngoals:\n  class:\n    - >\n      D\u00e9composer un algorithme selon l'approche descendante (raffinage successif)\n      et ascendante.\n    - D\u00e9composer une application de complexit\u00e9 moyenne en algorithmes \u00e9l\u00e9mentaires.\n    - Concevoir un type de donn\u00e9es abstrait simple et les fonctions pour le manipuler.\n    - Concevoir une biblioth\u00e8que de fonctions en utilisant la compilation s\u00e9par\u00e9e.\n    - \u00c9crire un programme qui manipule (lecture/\u00e9criture) des fichiers binaires\n    - Lire et g\u00e9n\u00e9rer un fichier de donn\u00e9es tabul\u00e9es (p.ex. csv),\n    - Mettre en \u0153uvre un tableau dynamique avec facteur de croissance,\n    - D\u00e9finir et manipuler un type de donn\u00e9es r\u00e9cursif e.g. liste cha\u00een\u00e9e,\n    - Comprendre le fonctionnement d'un algorithme de tri en O(n log n),\n    - Savoir impl\u00e9menter une recherche dichotomique\n    - Comprendre le fonctionnement du pr\u00e9processeur C\n    - Conna\u00eetre et savoir quand utiliser les diff\u00e9rentes classes de stockage\n    - Conna\u00eetre en d\u00e9tail la notion de pointeur et savoir les utiliser\n    - >\n      Utiliser les fonctions standard de recherche et de manipulation\n      de cha\u00eene de caract\u00e8res (p.ex. strstr, strchr, qsort).\n  laboratory:\n    - R\u00e9unir un ensemble de fonctions dans un module logiciel et l'utiliser\n    - Programmer et mettre au point des algorithmes de complexit\u00e9 moyenne\n    - >\n      R\u00e9aliser une application de taille et de complexit\u00e9 moyennes,\n      m\u00ealants diff\u00e9rents aspects de la programmation\n    - D\u00e9velopper un programme en utilisant un outil de gestion de version\n    - Utiliser un syst\u00e8me de test automatique pour valider le fonctionnement d'un programme.\n\nplan:\n  - Algorithmie\n    - Raffinage successif\n    - D\u00e9composition en \u00e9l\u00e9ments fonctionnels simples\n    - Conception d'algorithmes de complexit\u00e9 moyenne\n  - Types compos\u00e9s\n    - Manipulation d'une structure (struct)\n    - Passage par copie et adresse\n    - Cr\u00e9ation de types (typedef)\n  - Biblioth\u00e8que\n    - Concevoir une biblioth\u00e8que statique\n    - Utilisation d'une biblioth\u00e8que statique dans un programme\n  - Fichiers\n    - Types de fichiers\n    - Binaire\n    - Textes\n    - Donn\u00e9es tabul\u00e9es\n    - Donn\u00e9es index\u00e9es\n    - Syst\u00e8me de fichier\n    - Arborescence\n    - Dossiers\n    - Chemins relatifs et absolus\n    - Manipulation de fichiers\n    - Pointeur de fichier (ftell, fseek)\n    - Lecture (fread, fscanf)\n    - \u00c9criture (fwrite, fprintf)\n  - Gestion de la m\u00e9moire\n    - Pointeurs\n    - R\u00e8gle gauche droite\n    - Arithm\u00e9tique de pointeurs\n    - Types de pointeurs imbriqu\u00e9s (p.ex. int**[])\n    - Allocation dynamique\n    - malloc\n    - calloc\n    - free\n    - Cr\u00e9ation de tableaux dynamiques\n    - Comprendre la diff\u00e9rence entre le stack et le heap\n  - Types de donn\u00e9es r\u00e9cursifs\n    - Liste simplement cha\u00een\u00e9e\n    - Liste doublement cha\u00een\u00e9e\n  - Alignement m\u00e9moire\n    - Unions\n    - Champs de bits\n  - Livraison\n    - Pr\u00e9parer le code \u00e0 la livraison\n    - Construire une biblioth\u00e8que document\u00e9e\n    - Utiliser GitHub pour tracer le d\u00e9veloppement\n    - Utiliser une biblioth\u00e8que de test unitaire\nbibliographie:\n  - author: Jean-Michel L\u00e9ry\n    title: Algorithmique, Applications en C, C++ et Java\n    editor: Pearson\n    year: 2013\n  - standard: ISO/IEC 9899:2011\n    title: Langage de programmation C, ISO/IEC\n    year: 2011\n  - author:\n    Brian Kernighan:\n    Dennis Ritchie:\n    title: Le langage C\n    edition: 2nd\n    editor: Dunod\n    year: 2014\n    isbn: 978-2100715770\n  - author: Claude Delannoy\n    title: Programmer en langage C, Cours et exercices corrig\u00e9s\n    editor: Eyrolles\n    year: 2016\n  - author: Stephen Kochan\n    title: Programming in C\n    edition: 4\n    editor: Pearson\n    year: 2014\n    isbn: 978-0321776419\n
    "}, {"location": "appendix/unit/#panification-du-semestre-de-printemps", "title": "Panification du semestre de printemps", "text": "Semaine Acad\u00e9mique Cours Labo 8 1 Introduction GitHub - WSL 9 2 Fichiers Proust (partie 1) 10 3 Allocation dynamique Proust (partie 2) 11 4 Allocation dynamique M\u00e9t\u00e9o (partie 1) 12 5 Compilation s\u00e9par\u00e9e M\u00e9t\u00e9o (partie 2) 13 6 Pr\u00e9processeur Tableau dynamique (\u00bd) 14 7 Unions, champs de bits Tableau dynamique (2/2) 15 8 Usage biblioth\u00e8ques St\u00e9ganographie 16 Vacances de P\u00e2ques 17 9 TE1 Wave (partie 1) 18 10 Algorithmique Big-O Wave (partie 2) 19 11 Tris Quick-Sort / Merge-Sort 20 12 Queues et piles Tries 21 13 Sockets Labo Test 22 14 TE2 Shunting-yard 23 15 Arbres binaires Tries (partie 1) 24 16 Exercices de r\u00e9vision Tries (partie 2) 25 Pr\u00e9paration Examens 26 Examens"}, {"location": "appendix/unit/#modalites-devaluation-et-de-validation", "title": "Modalit\u00e9s d'\u00e9valuation et de validation", "text": "

    Le cours se compose de\u2009:

    • Travaux \u00e9crits not\u00e9s (coefficient 100%)
    • Quiz not\u00e9s ou non (coefficient 10% ou 0%)
    • S\u00e9ries d'exercices
    • Travaux pratiques, 2 \u00e0 3 labos not\u00e9s (laboratoires)
    • Labo test not\u00e9 comptabilis\u00e9 comme un labo

    La note finale est donn\u00e9e par l'expression\u2009:

    FormuleProgramme C \\[ \\text{final} = \\frac{ \\sum\\limits_{t=1}^\\text{T}{\\text{TE}_t} + 10\\% \\cdot \\sum\\limits_{q=1}^\\text{Q}{\\text{Quiz}_q} }{ 4 \\cdot (\\text{T} + 10\\% \\cdot \\text{Q}) } + \\frac{1}{4 L} \\sum\\limits_{l=1}^L \\text{Labo}_l + \\frac{1}{2} \\text{Exam} \\]
    #define QUIZ_WEIGHT (.1) // Percent\n#define EXAM_WEIGHT (.5) // Percent\n\ntypedef struct notes {\n    size_t size;\n    float values[];\n} Notes;\n\nfloat sum(Notes *notes) {\n    float s = 0;\n    for (int i = 0; i < notes->size; i++)\n        s += notes->values[i];\n    return s;\n}\n\nfloat mark(Notes tes, Notes quizzes, Notes labs, float exam) {\n    return (\n    sum(tes) + QUIZ_WEIGHT * sum(quizzes)\n    ) / (\n        (EXAM_WEIGHT / 2.) * (tes.size + QUIZ_WEIGHT * quizzes.size)\n    ) +\n            (EXAM_WEIGHT / 2.) * sum(labs) / labs.size +\n        EXAM_WEIGHT * exam;\n}\n
    "}, {"location": "appendix/unit/#directives", "title": "Directives", "text": "
    • En cas d'absence \u00e0 un quiz, la note de 1.0 est donn\u00e9e.
    • En cas de plagiat, le dilemme du prisonnier s'applique.
    "}, {"location": "course-c/00-preface/", "title": "Avant-Propos", "text": ""}, {"location": "course-c/00-preface/#a-qui-sadresse-cet-ouvrage", "title": "\u00c0 qui s'adresse cet ouvrage\u2009?", "text": "

    Con\u00e7u comme un r\u00e9sum\u00e9 du savoir n\u00e9cessaire \u00e0 l'ing\u00e9nieur pour s'initier \u00e0 la programmation et prendre en main le langage C, cet ouvrage n'est pas un manuel de r\u00e9f\u00e9rence. Il se r\u00e9f\u00e8re \u00e0 de nombreuses ressources internet et livres que le lecteur pourra consulter au besoin pour approfondir certains concepts.

    Chaque chapitre est compos\u00e9 d'exercices, mais \u00e0 des fins p\u00e9dagogiques, l'int\u00e9gralit\u00e9 des solutions ne sont pas fournies\u2009; certains exercices sont destin\u00e9s \u00e0 \u00eatre faits en \u00e9tudes.

    Cet ouvrage est destin\u00e9 aux \u00e9tudiants futurs ing\u00e9nieurs de premi\u00e8re ann\u00e9e n'ayant aucune exp\u00e9rience en programmation.

    "}, {"location": "course-c/00-preface/#cours-dinformatique-cursus-bachelor", "title": "Cours d'informatique cursus bachelor", "text": "

    Ce cours d'informatique \u00e0 la HEIG-VD est donn\u00e9 par le d\u00e9partement TIN dans les cours du cursus Bachelor en G\u00e9nie \u00c9lectrique. Il concerne tout particuli\u00e8rement les \u00e9tudiants des cours suivants\u2009:

    • Informatique 1 (INFO1) - 101 Premi\u00e8re ann\u00e9e
    • Informatique 2 (INFO2) - 102 Premi\u00e8re ann\u00e9e
    • Microinformatique (MICROINFO) - 101 Premi\u00e8re ann\u00e9e
    "}, {"location": "course-c/00-preface/#quel-programmeur-etes-vous", "title": "Quel programmeur \u00eates-vous\u2009?", "text": "

    Les \u00e9tudes en \u00e9coles d'ing\u00e9nieurs sont souvent cloisonn\u00e9es. On observe, entre les diff\u00e9rentes facult\u00e9s (\u00e9lectronique, informatique, etc.), que l'enseignement de l'informatique s'inscrit dans une culture distincte avec un langage sp\u00e9cifique. Les informaticiens, dot\u00e9s d'un esprit d'abstraction remarquable, acqui\u00e8rent des connaissances approfondies du fonctionnement interne des syst\u00e8mes d'exploitation et poss\u00e8dent une expertise \u00e9tendue en programmation. N\u00e9anmoins, ils manquent parfois d'une exp\u00e9rience pratique avec le mat\u00e9riel \u00e9lectronique et les contraintes impos\u00e9es par des architectures mat\u00e9rielles l\u00e9g\u00e8res (syst\u00e8mes embarqu\u00e9s, microcontr\u00f4leurs, etc.). Les \u00e9lectroniciens, quant \u00e0 eux, disposent d'une compr\u00e9hension approfondie des syst\u00e8mes \u00e0 bas niveau. Ils ont une vision pragmatique des syst\u00e8mes et des contraintes mat\u00e9rielles. Cependant, ils manquent souvent de connaissances pouss\u00e9es en programmation et en algorithmique.

    Ces deux profils, bien que compl\u00e9mentaires, ont souvent du mal \u00e0 se comprendre. Les informaticiens per\u00e7oivent les \u00e9lectroniciens comme trop terre-\u00e0-terre, tandis que les \u00e9lectroniciens jugent les informaticiens trop abstraits. Des divergences d'opinions peuvent \u00e9merger de ces diff\u00e9rences culturelles, notamment dans des notions communes dont les d\u00e9finitions varient. Par exemple, la notion de temps r\u00e9el diff\u00e8re pour un informaticien et un \u00e9lectronicien. Pour un informaticien, le temps r\u00e9el d\u00e9signe un syst\u00e8me qui r\u00e9pond dans un d\u00e9lai d\u00e9termin\u00e9 pour un utilisateur (environ 100 ms). Pour un \u00e9lectronicien, le temps r\u00e9el d\u00e9signe un syst\u00e8me qui r\u00e9pond dans un d\u00e9lai d\u00e9termin\u00e9 et qui est d\u00e9terministe (environ 100 \u00b5s).

    Un autre exemple est la complexit\u00e9 algorithmique. Pour un informaticien, la complexit\u00e9 algorithmique mesure la performance d'un algorithme en termes g\u00e9n\u00e9raux. Un acc\u00e8s \u00e0 un dictionnaire est en \\(O(1)\\), m\u00eame s'il implique le calcul d'un sha256, une op\u00e9ration triviale sur un ordinateur. Pour un \u00e9lectronicien, il est impossible de r\u00e9aliser un sha256 sur un microcontr\u00f4leur 8 bits, ce qui l'incite \u00e0 rechercher des optimisations profondes de l'algorithme, quitte \u00e0 le rendre moins g\u00e9n\u00e9rique et modulaire.

    Cet ouvrage a pour objectif de rapprocher ces deux cultures en fournissant aux \u00e9lectroniciens les bases de la v\u00e9ritable informatique, de la programmation et de sa culture, en rendant accessibles des concepts complexes tels que les arbres et les graphes.

    ", "tags": ["sha256"]}, {"location": "course-c/00-preface/#organisation-de-louvrage", "title": "Organisation de l'ouvrage", "text": ""}, {"location": "course-c/00-preface/#recherche", "title": "Recherche", "text": "

    Pour faciliter la recherche d'informations, vous pouvez utiliser la barre de recherche en haut \u00e0 droite de la page. Vous pouvez \u00e9galement utiliser les raccourcis clavier pour naviguer plus rapidement (voir ci-dessous). Notez que recherche est instantan\u00e9e et vous permet de trouver des informations dans le texte, les titres et les liens.

    "}, {"location": "course-c/00-preface/#theme", "title": "Theme", "text": "

    Selon votre pr\u00e9f\u00e9rence, vous pouvez choisir entre deux th\u00e8mes pour la lecture de ce livre (clair ou sombre ). Pour changer de th\u00e8me, cliquez sur l'ic\u00f4ne \u00e0 gauche de la barre de recherche.

    "}, {"location": "course-c/00-preface/#raccourcis-clavier", "title": "Raccourcis clavier", "text": "

    Pour am\u00e9liorer votre navigation sur ce site, voici quelques raccourcis clavier que vous pouvez utiliser\u2009:

    F, S, /

    Ouvre la barre de recherche

    P, ,

    Va \u00e0 la page pr\u00e9c\u00e9dente

    N, .

    Va \u00e0 la page suivante

    B

    Afficher/cacher les tables des mati\u00e8res

    M

    Afficher/cacher le menu

    H

    Afficher/cacher la table des mati\u00e8res

    "}, {"location": "course-c/00-preface/#cookies", "title": "Cookies", "text": "

    Ce site utilise des cookies pour sauvegarder vos pr\u00e9f\u00e9rences de th\u00e8me ainsi que votre progression dans les exercices.

    Des information d'analyse de fr\u00e9quentation sont \u00e9galement collect\u00e9es pour am\u00e9liorer le contenu de ce livre.

    "}, {"location": "course-c/00-preface/#conventions-decriture", "title": "Conventions d'\u00e9criture", "text": ""}, {"location": "course-c/00-preface/#encodage-de-caractere", "title": "Encodage de caract\u00e8re", "text": "

    Il sera souvent fait mention dans cet ouvrage la notation du type 1F4A9, il s'agit d'une notation Unicode qui ne d\u00e9pend pas d'un quelconque encodage. Parler du caract\u00e8re ASCII 234 est incorrect, car cela d\u00e9pend de la table d'encodage utilis\u00e9e\u2009; en revanche, la notation Unicode est plus pr\u00e9cise.

    La notation est cliquable et vous redirigera vers le site symbl.cc.

    "}, {"location": "course-c/00-preface/#expressions-regulieres", "title": "Expressions r\u00e9guli\u00e8res", "text": "

    Les expressions r\u00e9guli\u00e8res sont utilis\u00e9es pour d\u00e9crire des motifs de texte. Elles sont utilis\u00e9es pour rechercher, remplacer ou valider des cha\u00eenes de caract\u00e8res. Les expressions r\u00e9guli\u00e8res sont utilis\u00e9es dans de nombreux langages de programmation, d'outils de recherche et de traitement de texte.

    Aussi dans cet ouvrage, les expressions r\u00e9guli\u00e8res sont mises en \u00e9vidence avec /regex/. Le lien m\u00e8ne au site regex101.com. Pour tester les expressions r\u00e9guli\u00e8res, il vous suffit alors d'ajouter votre propre texte pour tester l'exemple donn\u00e9.

    "}, {"location": "course-c/00-preface/#symbole-degalite", "title": "Symbole d'\u00e9galit\u00e9", "text": "

    Nous verrons que le signe d'\u00e9galit\u00e9 = peut ais\u00e9ment \u00eatre confondu avec l'op\u00e9rateur d'affectation du langage C qui s'\u00e9crit de la m\u00eame mani\u00e8re. Dans certains exemples o\u00f9 l'on montre une \u00e9galit\u00e9 entre diff\u00e9rentes \u00e9critures, le signe d'\u00e9galit\u00e9 triple 2261 sera utilis\u00e9 pour dissiper toute ambigu\u00eft\u00e9 \u00e9ventuelle\u2009:

    'a' \u2261 0b1100001 \u2261 97 \u2261 0x61 \u2261 00141\n
    "}, {"location": "course-c/00-preface/#symbole-de-remplissage", "title": "Symbole de remplissage", "text": "

    Dans les exemples qui seront donn\u00e9s, on pourra voir while (condition) { \u301c } ou le caract\u00e8re \u301c 3030 indique une continuit\u00e9 logique d'op\u00e9ration. Le symbole exprime ainsi ... (points de suspension ou ellipsis). Or, pour ne pas confondre avec le symbole C ... utilis\u00e9 dans les fonctions \u00e0 arguments variables tels que printf.

    ", "tags": ["printf"]}, {"location": "course-c/00-preface/#types-de-donnees", "title": "Types de donn\u00e9es", "text": "

    Les conventions C s'appliquent \u00e0 la mani\u00e8re d'exprimer les grandeurs suivantes\u2009:

    • 0xABCD pour les valeurs hexad\u00e9cimales /0x[0-9a-f]+/i
    • 00217 pour les valeurs octales /0[0-7]+/
    • 'c' pour les caract\u00e8res /'([^']|\\\\[nrftvba'])'/
    • 123 pour les grandeurs enti\u00e8res /-?[1-9][0-9]*/
    • 12. pour les grandeurs r\u00e9elles en virgule flottante
    "}, {"location": "course-c/00-preface/#encadres", "title": "Encadr\u00e9s", "text": "

    Des encadr\u00e9s sont utilis\u00e9s pour mettre en avant des informations compl\u00e9mentaires ou des astuces. Ils sont \u00e9galement utilis\u00e9s pour donner des informations sur des concepts avanc\u00e9s ou des d\u00e9tails techniques.

    Info

    Fait historique o\u00f9 information compl\u00e9mentaire pour ceux qui voudraient en savoir plus.

    Avertissement

    Point important \u00e0 faire attention qui source d'erreur fr\u00e9quente.

    Danger

    Note importante qui comporte des risques \u00e0 consid\u00e9rer.

    Exemple

    Exemple pratique pour illustrer un concept.

    Note

    Corollaire \u00e0 retenir.

    Astuce

    Truc ou Astuce pour faciliter la compr\u00e9hension.

    Bogue

    Limitations ou bugs possibles d'une m\u00e9thode propos\u00e9e.

    Exercice 1\u2009: Quelle ic\u00f4ne\u2009?

    Quelle ic\u00f4ne est utilis\u00e9e pour les exercices\u2009?

    "}, {"location": "course-c/00-preface/#anglicismes", "title": "Anglicismes", "text": "

    Parler l'informatique ou de technologies sans utiliser d'anglicismes est un exercice difficile. Il est parfois moins lourd de parler de hardware que de mat\u00e9riel informatique. Certains termes n'ont pas de traduction en fran\u00e7ais. Par exemple, le terme set appliqu\u00e9 \u00e0 un ensemble de donn\u00e9es n'a pas de traduction cr\u00e9dible en fran\u00e7ais. La table ci-dessous montre quelques termes qui seront utilis\u00e9s dans cet ouvrage\u2009:

    Anglicismes Anglais Fran\u00e7ais Pr\u00e9f\u00e9rence byte octet byte debug d\u00e9verminer debug hardware mat\u00e9riel informatique hardware listing extrait de code listing pipe tube pipe process processus processus seekable positionnable seekable set ensemble set software logiciel informatique software stream flux de donn\u00e9es stream

    Notons que byte et octet ne sont pas exactement synonymes. Un byte est un ensemble g\u00e9n\u00e9ralement admis de 8 bits mais dont la taille a pu varier selon les ann\u00e9es, alors qu'un octet est un ensemble de 8 bits sans exception. En pratique, les deux termes sont souvent utilis\u00e9s de mani\u00e8re interchangeable. En anglais il n'existe pas de mot pour octet.

    Les termes anglais sont g\u00e9n\u00e9ralement indiqu\u00e9s en italique.

    "}, {"location": "course-c/00-preface/#copyright-et-references", "title": "Copyright et r\u00e9f\u00e9rences", "text": "

    Le contenu de ce livre est sous licence Creative Commons. Vous \u00eates libre de partager et d'adapter ce contenu pour toute utilisation, m\u00eame commerciale, \u00e0 condition de citer l'auteur et de partager vos travaux d\u00e9riv\u00e9s sous la m\u00eame licence.

    De nombreuses r\u00e9f\u00e9rences et sources de ce livre sont issues de Wikipedia, de la documentation officielle de la norme C, de StackOverflow, de forums de discussion et de blogs.

    "}, {"location": "course-c/00-preface/#comment-contribuer", "title": "Comment contribuer\u2009?", "text": "

    Vous avez remarqu\u00e9 une erreur, une faute de frappe ou une information manquante\u2009? Vous auriez d\u00e9sir\u00e9 une explication plus d\u00e9taill\u00e9e sur un sujet\u2009? Vous pouvez contribuer \u00e0 l'am\u00e9lioration de ce livre en soumettant une issue. Alternativement, vous pouvez faire un fork du projet et proposer une pull request. Ce travail est donc une \u0153uvre vivante qui \u00e9volue avec le temps et les contributions de chacun.

    La version imprim\u00e9e sera n\u00e9anmoins fig\u00e9e \u00e0 un moment donn\u00e9, mais la version en ligne sera toujours mise \u00e0 jour.

    "}, {"location": "course-c/00-preface/#colophon", "title": "Colophon", "text": "

    Ce livre est \u00e9crit en Markdown et g\u00e9n\u00e9r\u00e9 en HTML par MkDocs. Le th\u00e8me utilis\u00e9 est Material for MkDocs. Les sources sont disponibles sur GitHub et l'h\u00e9bergement est assur\u00e9 par GitHub Pages.

    La plupart des illustrations sont r\u00e9alis\u00e9es avec Draw.io, un outil de dessin vectoriel en ligne. Les sch\u00e9mas sont rendus dans le navigateur avec GraphViewer. Les diagrammes utilisent la technologie Mermaid. Les autres sources d'images sont issues en grande partie de Wikimedia Commons et Wikipedia.

    La g\u00e9n\u00e9ration de l'ouvrage en PDF utilise son propre convertisseur vers LaTeX. Les extraits de code sources sont color\u00e9s avec Pygments en utilisant le paquet minted.

    L'orthographe et la grammaire ont \u00e9t\u00e9 revues avec Antidote.

    ", "tags": ["GraphViewer"]}, {"location": "course-c/05-introduction/c-lang/", "title": "Le langage C", "text": "C is quirky, flawed, and an enormous success.Dennis Ritchie

    Le langage C est l'un des premiers langages de programmation. Il se situe tr\u00e8s pr\u00e8s de l'assembleur, ce langage de bas niveau utilis\u00e9 par les processeurs. Le C permet ainsi de concevoir des applications extr\u00eamement performantes, et il est exploit\u00e9 dans une multitude de domaines informatiques, que ce soit pour une montre connect\u00e9e, un stimulateur cardiaque (pacemaker) ou encore une machine \u00e0 caf\u00e9.

    Bien qu'il soit ancien (il date de 1972), le langage C reste largement employ\u00e9 et enseign\u00e9, gr\u00e2ce \u00e0 son efficacit\u00e9 et sa capacit\u00e9 \u00e0 inculquer les bases fondamentales de la programmation.

    En v\u00e9rit\u00e9, en 2024, il n'existe gu\u00e8re d'alternative aussi m\u00fbre et \u00e9prouv\u00e9e que le C pour d\u00e9velopper des applications embarqu\u00e9es \u00e0 haute performance ou pour le noyau des syst\u00e8mes d'exploitation. Des langages plus r\u00e9cents tels que Rust ou Zig commencent certes \u00e0 \u00e9merger, mais ils peinent encore \u00e0 s'imposer dans l'industrie.

    ", "tags": ["1972", "rust", "zig"]}, {"location": "course-c/05-introduction/c-lang/#historique", "title": "Historique", "text": "

    En 1964 na\u00eet, d'une collaboration entre les laboratoires Bell (Bell Telephone Laboratories), General Electric et le MIT, le projet Multics (Multiplexed Information and Computing Service), qui vise \u00e0 d\u00e9velopper un nouveau syst\u00e8me d'exploitation.

    Cependant, la fin de la d\u00e9cennie est marqu\u00e9e par des remous. Les laboratoires Bell, d\u00e9sillusionn\u00e9s par les promesses de Multics, d\u00e9cident de se retirer du projet pour \u00e9laborer leur propre syst\u00e8me d'exploitation. Un groupe informel, dirig\u00e9 notamment par Ken Thompson et Dennis Ritchie, entreprend de revoir certains concepts de Multics qui leur d\u00e9plaisaient, notamment le langage de programmation PL/I (Programming Language number 1), alors pr\u00e9dominant pour l\u2019\u00e9criture de syst\u00e8mes d\u2019exploitation. Thompson d\u00e9veloppe un langage baptis\u00e9 B, inspir\u00e9 du BCPL, dans lequel il ne conserve que les \u00e9l\u00e9ments qu'il juge essentiels pour fonctionner sur de petites machines. \u00c0 ce stade, B ne comporte qu\u2019un seul type de donn\u00e9e, le \u00ab\u2009mot\u2009\u00bb (word).

    BCPL, con\u00e7u par Martin Richards au MIT dans les ann\u00e9es 1960, est l'anc\u00eatre de B, et par extension, l'arri\u00e8re-grand-p\u00e8re du C. Dennis Ritchie, alors coll\u00e8gue de Thompson, retravaille le langage B pour y ajouter la gestion des types de donn\u00e9es.

    Le syst\u00e8me d'exploitation que Thompson et Ritchie d\u00e9veloppent aux laboratoires Bell s\u2019appelle d'abord UNICS, par opposition \u00e0 Multics, o\u00f9 Multiplexed est remplac\u00e9 par Uniplexed. Le nom \u00e9volue ensuite pour devenir UNIX, un pilier dans l'histoire de l'informatique.

    Plus tard, Brian Kernighan contribue \u00e0 la popularisation de ce nouveau langage. Il est l'auteur principal du livre The C Programming Language, tandis que Dennis Ritchie s\u2019est concentr\u00e9 sur les annexes.

    Les \u00e9volutions du C continuent, notamment avec Bjarne Stroustrup qui, dans les ann\u00e9es 1980, \u00e9tend le langage en y apportant la programmation orient\u00e9e objet (OOP). Ce concept sera \u00e9tudi\u00e9 dans un autre cours. La figure suivante pr\u00e9sente le trio fondateur du langage C.

    Les p\u00e8res fondateurs du C

    Il faut attendre 1989 pour que le langage C soit normalis\u00e9 par l\u2019ANSI (American National Standards Institute). L\u2019ann\u00e9e suivante, l\u2019ISO (International Organization for Standardization) ratifie le standard ISO/IEC 9899:1990, commun\u00e9ment appel\u00e9 C90. D\u00e8s lors, le C devient un standard international et s\u2019impose comme le langage dominant dans le domaine de l\u2019informatique.

    Les langages de programmation se nourrissent souvent les uns des autres, et le C ne fait pas exception. La figure suivante illustre quelques-unes des influences entre langages\u2009:

    %% Influences des langages de programmation\nflowchart LR\n    COBOL --> PLI[\"PL/I\"]\n    FORTRAN --> ALGOL\n    ALGOL --> CPL\n    CPL --> BCPL\n    ALGOL --> SIMULA\n    ALGOL --> PASCAL\n    FORTRAN --> PASCAL\n    FORTRAN --> PLI\n    BCPL --> B\n    ALGOL --> PLI\n    PLI --> C(\"C\")\n    B --> C
    Influences des langages de programmation

    Cinquante ans plus tard, le C reste l'un des langages les plus utilis\u00e9s par les ing\u00e9nieurs. Alliant une vision de haut niveau avec la possibilit\u00e9 de manipulations de bas niveau, il s\u2019av\u00e8re \u00eatre un choix privil\u00e9gi\u00e9 pour les applications embarqu\u00e9es sur microcontr\u00f4leurs, ou pour l\u2019optimisation de code afin d\u2019obtenir des performances maximales, comme dans les noyaux de syst\u00e8mes d'exploitation tels que le noyau Linux (Kernel) ou Windows.

    Retenons simplement que C est un langage \u00e0 la fois simple et puissant. Votre machine \u00e0 caf\u00e9, votre voiture, vos \u00e9couteurs Bluetooth ont probablement \u00e9t\u00e9, au moins partiellement, programm\u00e9s en C.

    ", "tags": ["martin-richards", "mit", "bell", "bcpl", "kernel", "unix", "linux", "multics", "1960", "1964", "1989", "general-electric", "noyau"]}, {"location": "course-c/05-introduction/c-lang/#standardisation", "title": "Standardisation", "text": "

    Comme nous l'avons vu, le langage C a un long historique. Il a fallu attendre pr\u00e8s de vingt ans apr\u00e8s sa cr\u00e9ation pour qu\u2019il fasse l\u2019objet d\u2019une normalisation internationale.

    Le standard le plus couramment utilis\u00e9 en 2024 reste sans doute C99. C11 commence \u00e0 le remplacer dans l'industrie, mais l'\u00e9volution se poursuit avec C17, C18 et C23. La figure suivante r\u00e9sume les diff\u00e9rents standards internationaux du C\u2009:

    Normes internationales du langage C Notation courte Standard international Date C n/a 1972 K&R C n/a 1978 C89 (ANSI C) ANSI X3.159-1989 1989 C90 ISO/IEC 9899:1990 1990 C99 ISO/IEC 9899:1999 1999 C11 ISO/IEC 9899:2011 2011 C17/C18 ISO/IEC 9899:2018 2018 C23 ISO/IEC 9899:2023 2023

    En substance, C18 n'apporte pas de nouvelles fonctionnalit\u00e9s majeures au langage, mais se concentre sur la clarification des ambigu\u00eft\u00e9s laiss\u00e9es par C11, qui, lui-m\u00eame, n\u2019introduisait que peu de changements fondamentaux pour le d\u00e9veloppement sur microcontr\u00f4leurs.

    Info

    Vous entendrez ou lirez souvent des r\u00e9f\u00e9rences \u00e0 ANSI C ou K&R. Privil\u00e9giez toutefois une compatibilit\u00e9 avec C99 au minimum.

    Le standard C est, il faut bien l\u2019admettre, dense et ardu \u00e0 lire. Avec ses quelque 552 pages pour C99, il est peu probable que vous y trouviez un grand plaisir. Et pourtant, il est parfois n\u00e9cessaire de s\u2019y plonger pour d\u00e9m\u00ealer certaines subtilit\u00e9s du langage, rarement explicit\u00e9es dans les manuels. Vous vous retrouverez un jour ou l\u2019autre confront\u00e9 \u00e0 des probl\u00e8mes non document\u00e9s, et c\u2019est souvent dans le standard que se trouve la solution.

    Comme mentionn\u00e9 plus haut, bien que C99 soit le standard le plus utilis\u00e9 en 2024, il a d\u00e9j\u00e0 plus de 25 ans. Vous vous demandez peut-\u00eatre pourquoi l'industrie semble si en retard face aux derni\u00e8res versions. Contrairement au domaine des logiciels grand public, o\u00f9 chaque mise \u00e0 jour est adopt\u00e9e avec enthousiasme, le secteur industriel est r\u00e9gi par des processus de validation stricts. Migrer vers un nouveau standard est une op\u00e9ration co\u00fbteuse, tant en termes de tests que de conformit\u00e9, et les entreprises pr\u00e9f\u00e8rent souvent s\u2019en tenir \u00e0 des versions \u00e9prouv\u00e9es plut\u00f4t que de risquer de co\u00fbteuses erreurs, notamment dans des domaines critiques comme l\u2019a\u00e9ronautique ou la m\u00e9decine.

    ", "tags": ["c23", "c18", "normalisation", "c17", "c11"]}, {"location": "course-c/05-introduction/c-lang/#le-c-et-les-autres", "title": "Le C et les autres...", "text": "

    Si ce cours se concentre principalement sur le langage C, il est loin d'\u00eatre le seul langage de programmation, et ce n'est certainement pas le seul que vous apprendrez au cours de votre carri\u00e8re. La table suivante pr\u00e9sente une liste non exhaustive de langages de programmation ainsi que leur ann\u00e9e de cr\u00e9ation. Cette liste permet de mieux comprendre l'\u00e9volution des langages de programmation et leurs usages typiques\u2009:

    Langages de programmation et leur ann\u00e9e de cr\u00e9ation Langage de programmation Ann\u00e9e Utilisation Fortran 1957 Calcul scientifique Lisp 1958 Intelligence artificielle Cobol 1959 Finance, banque Basic 1964 Enseignement Pascal 1970 Enseignement C 1972 Syst\u00e8mes embarqu\u00e9s C++ 1985 Applications lourdes Perl 1987 Scripts Python 1991 Ing\u00e9nierie, sciences Ruby 1995 Scripts, Web Java 1995 Applications lourdes PHP 1995 Web C# 2000 Applications graphiques Go 2009 Syst\u00e8mes distribu\u00e9s Rust 2010 Syst\u00e8mes embarqu\u00e9s Swift 2014 Applications mobiles Zig 2016 Syst\u00e8mes embarqu\u00e9s

    L'index TIOBE constitue un excellent indicateur de la popularit\u00e9 des langages de programmation. Il est mis \u00e0 jour mensuellement et permet de suivre l'\u00e9volution de la popularit\u00e9 des diff\u00e9rents langages. En 2024, le classement des 10 langages de programmation les plus populaires est pr\u00e9sent\u00e9 dans la table suivante\u2009:

    Top 10 des langages de programmation Top 10 Langage de programmation 1 Python 2 C++ 3 C 4 Java 5 C# 6 JavaScript 7 Go 8 SQL 9 Visual Basic 10 Fortran

    Sur le podium, Python est un langage de tr\u00e8s haut niveau simple \u00e0 apprendre, mais \u00e9loign\u00e9 du mat\u00e9riel. C++ est un langage de programmation orient\u00e9e objet, tr\u00e8s puissant, mais complexe \u00e0 dompter. Avec la m\u00e9daille d'argent, C est un excellent compromis entre les deux, il est simple, mais permet de comprendre les bases de la programmation et de la manipulation du mat\u00e9riel. C'est pour cela que ce cours est bas\u00e9 sur le langage C. Ai-je r\u00e9ussi \u00e0 vous convaincre\u2009?

    ", "tags": ["c", "python"]}, {"location": "course-c/05-introduction/c-lang/#programmation-texte-structuree", "title": "Programmation texte structur\u00e9e", "text": "

    Le C comme la plupart des langages de programmation utilise du texte structur\u00e9, c'est-\u00e0-dire que le langage peut \u00eatre d\u00e9fini par un vocabulaire, une grammaire et se compose d'un alphabet. \u00c0 l'inverse des langages naturels comme le Fran\u00e7ais, un langage de programmation est un langage formel et se veut exact dans sa grammaire et son vocabulaire, il n'y a pas de cas particuliers ni d'ambigu\u00eft\u00e9s possibles dans l'\u00e9criture. Les compilateurs sont ainsi construits autour d'une grammaire du langage qui est r\u00e9duite au minimum par souci d'\u00e9conomie de m\u00e9moire, pour taire les ambigu\u00eft\u00e9s et accro\u00eetre la productivit\u00e9 du d\u00e9veloppeur.

    Pour mieux comprendre, voici un exemple sous forme de pseudo-code utilisant une grammaire simple\u2009:

    POUR CHAQUE \u0153uf DANS le panier :\n    jaune, blanc \u2190 CASSER(\u0153uf)\n    omelette \u2190 MELANGER(jaune, blanc)\n    omelette_cuite \u2190 CUIRE(omelette)\n\nSERVIR(omelette_cuite)\n

    La structure de la phrase permettant de traiter tous les \u00e9l\u00e9ments d'un ensemble d'\u00e9l\u00e9ments (les \u0153ufs d'un panier) peut alors s'\u00e9crire de fa\u00e7on g\u00e9n\u00e9rique comme suit\u2009:

    POUR CHAQUE \u301c DANS \u301c:\n    \u301c\n

    o\u00f9 les \u301c sont des marques substitutives (placeholder) qui seront remplac\u00e9es par le d\u00e9veloppeur par ce qui convient.

    Les grammaires des langages de programmation sont souvent formalis\u00e9es \u00e0 l'aide d'un m\u00e9talangage, c'est-\u00e0-dire un langage qui permet de d\u00e9crire un langage. On l'appelle la grammaire du langage C. C'est un peu le Bescherelle du C. On observe dans ce formalisme une syntaxe rigoureuse, l'utilisation de termes en majuscules, la s\u00e9paration de mots par des virgules, la pr\u00e9sence de parenth\u00e8ses et de fl\u00e8ches (\u2190). Cette syntaxe diff\u00e8re d'un langage \u00e0 l'autre, mais selon le paradigme du langage de grandes similarit\u00e9s peuvent exister.

    "}, {"location": "course-c/05-introduction/c-lang/#les-paradigmes-de-programmation", "title": "Les paradigmes de programmation", "text": "

    Chaque langage de programmation que ce soit C, C++, Python, ADA, COBOL et Lisp sont d'une mani\u00e8re g\u00e9n\u00e9rale assez proche les uns des autres. Nous citions par exemple le langage B, pr\u00e9curseur du C (c. f. [thompson]{c-history}). Ces deux langages, et bien que leurs syntaxes soient diff\u00e9rentes, ils demeurent assez proches, comme l'espagnol et l'italien qui partagent des racines latines. En programmation on dit que ces langages partagent le m\u00eame paradigme de programmation.

    Certains paradigmes sont plus adapt\u00e9s que d'autres \u00e0 la r\u00e9solution de certains probl\u00e8mes et de nombreux langages de programmation sont dit multi-paradigmes, c'est-\u00e0-dire qu'ils supportent diff\u00e9rents paradigmes.

    Nous citions plus haut le C++ qui permet la programmation orient\u00e9e objet, laquelle est un paradigme de programmation qui n'existe pas en C. Ce qu'il est essentiel de retenir c'est qu'un langage de programmation peut ais\u00e9ment \u00eatre substitu\u00e9 par un autre pour autant qu'ils s'appuient sur les m\u00eames paradigmes.

    Le langage C r\u00e9pond aux paradigmes suivants\u2009:

    Imp\u00e9ratif

    Programmation en s\u00e9quences de commandes, qui se lisent dans un ordre donn\u00e9 (de haut en bas).

    Structur\u00e9

    Programmation imp\u00e9rative poss\u00e9dant des structures de d\u00e9cision imbriqu\u00e9es comme les boucles et les conditions.

    Proc\u00e9dural

    Programmation imp\u00e9rative poss\u00e9dant des appels de proc\u00e9dures isol\u00e9es qui regroupent une s\u00e9quence d'instructions.

    D'autres langages comme le C++ apportent les paradigmes suppl\u00e9mentaires \u00e0 C\u2009:

    Fonctionnel

    Programmation bas\u00e9e sur l'appel de fonction. Utilis\u00e9 dans les langages Lisp, Haskell, Erlang.

    Orient\u00e9 objet

    Programmation bas\u00e9e sur la d\u00e9finition de classes et d'objets. Utilis\u00e9 dans les langages C++, Java, Python. Une classe associe des donn\u00e9es \u00e0 des actions qui manipulent ces donn\u00e9es.

    Des langages de plus haut niveau comme Python ou C# apportent davantage de paradigmes comme la programmation r\u00e9flective ou la programmation \u00e9v\u00e9nementielle.

    Ce que nous devons retenir c'est que le langage C est imp\u00e9ratif et proc\u00e9dural, c'est-\u00e0-dire qu'il est bas\u00e9 sur des s\u00e9quences d'instructions s\u00e9par\u00e9es les unes des autres qui s'ex\u00e9cutent dans un ordre donn\u00e9 et lesquelles peuvent \u00eatre regroup\u00e9es en proc\u00e9dures. En reprenant notre exemple d'omelette, si nous souhaitons cette fois-ci r\u00e9aliser une bonne p\u00e2te \u00e0 cr\u00eapes, nous pourrions \u00e9crire\u2009:

    POUR REALISER un \u0153uf:\n    CHERCHER poule\n    \u0153uf \u2190 PONDRE(poule)\n\nPOUR REALISER du lait:\n    CHERCHER vache\n    lait \u2190 TRAITRE(vache)\n\nPOUR REALISER de la farine:\n    PLANTER bl\u00e9\n    ATTENDRE 6 mois\n    moisson \u2190 MOISSONNER(bl\u00e9)\n    farine \u2190 MOUDRE(moisson)\n\nPOUR REALISER une p\u00e2te \u00e0 cr\u00e8pes:\n    \u0153uf \u2190 REALISER(\u0153uf)\n    jaune, blanc \u2190 CASSER(\u0153uf)\n    \u0153uf-liquide \u2190 MELANGER(jaune, blanc)\n    farine \u2190 REALISER(farine)\n    lait \u2190 REALISER(lait)\n    p\u00e2te \u2190 MELANGER(\u0153uf-liquide, farine, lait)\n

    Dans cet exemple, les s\u00e9quences d'instructions ont \u00e9t\u00e9 regroup\u00e9es en proc\u00e9dures, c'est de la programmation proc\u00e9durale. Les proc\u00e9dures permettent de d\u00e9couper un programme en morceaux plus petits, plus faciles \u00e0 comprendre et \u00e0 maintenir.

    "}, {"location": "course-c/05-introduction/c-lang/#cycle-de-developpement", "title": "Cycle de d\u00e9veloppement", "text": "

    Savoir \u00e9crire un programme en C n'est qu'une facette de la programmation. Il est important de comprendre que la programmation est un processus it\u00e9ratif qui n\u00e9cessite de suivre un cycle de d\u00e9veloppement logiciel. Ce cycle de d\u00e9veloppement comprend des \u00e9tapes menant de l'\u00e9tude \u00e0 l'analyse d'un probl\u00e8me jusqu'\u00e0 la r\u00e9alisation d'un programme informatique ex\u00e9cutable. Dans l'industrie, il existe de nombreux mod\u00e8les comme le Cycle en V ou le mod\u00e8le en cascade que nous verrons plus en d\u00e9tail plus tard (Mod\u00e8les de d\u00e9veloppement). Quel que soit le mod\u00e8le utilis\u00e9, il comprendra les \u00e9tapes suivantes\u2009:

    1. \u00c9tude et analyse du probl\u00e8me
    2. \u00c9criture d'un cahier des charges (sp\u00e9cifications)
    3. \u00c9criture de tests \u00e0 r\u00e9aliser pour tester le fonctionnement du programme
    4. Conception d'un algorithme
    5. Transcription de cet algorithme en utilisant le langage C
    6. Compilation du code et g\u00e9n\u00e9ration d'un ex\u00e9cutable
    7. Test de fonctionnement
    8. V\u00e9rification que le cahier des charges est respect\u00e9
    9. Livraison du programme

    Mis \u00e0 part la derni\u00e8re \u00e9tape o\u00f9 il n'y a pas de retour en arri\u00e8re possible, les autres \u00e9tapes sont it\u00e9ratives. Il est tr\u00e8s rare d'\u00e9crire un programme juste du premier coup. Durant tout le cycle de d\u00e9veloppement logiciel, des it\u00e9rations successives sont faites pour permettre d'optimiser le programme, de r\u00e9soudre des bogues, d'affiner les sp\u00e9cifications, d'\u00e9crire davantage de tests pour renforcer l'assurance d'un bon fonctionnement du programme et d\u2019\u00e9viter une coul\u00e9e de lave.

    "}, {"location": "course-c/05-introduction/c-lang/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    Le langage C \u00e0 une particularit\u00e9 que d'autres langages n'ont pas, il comporte une double grammaire. Le processus de compilation s'effectue donc en deux \u00e9tapes.

    1. Le pr\u00e9processeur qui enl\u00e8ve les commentaires du d\u00e9veloppeur et regroupe en un fichier les diff\u00e9rentes parties du programme.
    2. La compilation \u00e0 proprement parler du code source en un fichier binaire.

    Vient ensuite la phase d'\u00e9dition des liens ou linkage lors de laquelle le programme ex\u00e9cutable est cr\u00e9\u00e9 \u00e0 partir des fichiers binaires g\u00e9n\u00e9r\u00e9s lors de la compilation. La figure suivante illustre le cycle de compilation d'un programme C.

    Cycle de compilation illustr\u00e9

    "}, {"location": "course-c/05-introduction/c-lang/#preprocesseur-pre-processing", "title": "Pr\u00e9processeur (pre-processing)", "text": "

    La phase de preprocessing permet de g\u00e9n\u00e9rer un fichier interm\u00e9diaire en langage C dans lequel toutes les instructions n\u00e9cessaires \u00e0 la phase suivante sont pr\u00e9sentes. Le preprocessing r\u00e9alise le remplacement des directives du pr\u00e9processeur de d\u00e9finitions par leurs valeurs r\u00e9sultantes. Ce pr\u00e9processeur permet d'inclure des fichiers externes, de d\u00e9finir des valeurs constantes ou de conditionner l'ex\u00e9cution de certaines parties du code par exemple avec des options de configuration. Avec le compilateur gcc il est possible de demander uniquement cette \u00e9tape avec l'option -E. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    Processus de pr\u00e9processing

    Lorsque vous \u00e9crivez votre programme, vous le faites en utilisant des fichiers sources avec l'extension .c. N\u00e9anmoins, dans votre programme, vous vous basez sur de nombreuses biblioth\u00e8ques logicielles qui donnent acc\u00e8s \u00e0 des fonctions pr\u00e9d\u00e9finies. Ces biblioth\u00e8ques sont incluses dans votre programme \u00e0 l'aide de la directive #include. Lors de la compilation, le pr\u00e9processeur va remplacer ces directives par le contenu des fichiers d'en-t\u00eate correspondants. Par exemple, la directive #include <stdio.h> sera remplac\u00e9e par le contenu du fichier stdio.h qui contient les d\u00e9clarations des fonctions de la biblioth\u00e8que standard d'entr\u00e9es sorties. Cette proc\u00e9dure prend donc en entr\u00e9e un fichier source et un ou plusieurs fichiers d'en-t\u00eate et le transforme en un fichier source unique.

    ", "tags": ["stdio.h", "gcc"]}, {"location": "course-c/05-introduction/c-lang/#compilation-build", "title": "Compilation (build)", "text": "

    La phase de compilation consiste en une analyse syntaxique du fichier \u00e0 compiler selon la grammaire du langage puis en sa traduction en langage assembleur pour le processeur cible. Le fichier g\u00e9n\u00e9r\u00e9 est un fichier binaire (extension .o ou .obj) qui sera utilis\u00e9 pour la phase suivante. Lors de la compilation, des erreurs peuvent survenir et emp\u00eacher le d\u00e9roulement complet de la g\u00e9n\u00e9ration de l'ex\u00e9cutable final. L\u00e0 encore, la correction des erreurs passe toujours par un examen minutieux des messages d'erreur.

    \u00c0 l'instar de l'option -E vue plus haut, il est aussi possible de ne demander que l'assemblage d'un code avec l'option -S. \u00c0 partir d'un fichier pr\u00e9-process\u00e9, le compilateur g\u00e9n\u00e8re un fichier assembleur qui est un fichier texte lisible par un humain (qui conna\u00eet l'assembleur) et qui contient les instructions du processeur cible. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    Assemblage d'un programme C pr\u00e9-process\u00e9 en assembleur

    Une fois g\u00e9n\u00e9r\u00e9 le fichier assembleur, il doit encore est transform\u00e9 en langage machine, c'est-\u00e0-dire en un fichier binaire. Cette \u00e9tape est r\u00e9alis\u00e9e par un programme appel\u00e9 as qui prend en entr\u00e9e le fichier assembleur et g\u00e9n\u00e8re un fichier binaire comme le montre la figure suivante.

    Traduction d'un programme C pr\u00e9-process\u00e9 en objet binaire

    "}, {"location": "course-c/05-introduction/c-lang/#edition-de-liens-link", "title": "\u00c9dition de liens (link)", "text": "

    L'\u00e9dition de liens permet d'assembler ensemble les diff\u00e9rents fichiers binaires (.o) issus de la compilation et d'autres fichiers binaires n\u00e9cessaires au programme pour former un ex\u00e9cutable complet. Ces autres fichiers binaires sont appel\u00e9s des biblioth\u00e8ques ou plus commun\u00e9ment librairies. Elles peuvent appartenir au syst\u00e8me d'exploitation, ou avoir \u00e9t\u00e9 install\u00e9es manuellement avec l'environnement de d\u00e9veloppement. L'\u00e9dition de liens a pour r\u00f4le de r\u00e9soudre les r\u00e9f\u00e9rences entre les diff\u00e9rents fichiers binaires et de g\u00e9n\u00e9rer un ex\u00e9cutable complet.

    Imaginez un livre dont vous \u00eates le h\u00e9ros. Plusieurs auteurs diff\u00e9rents peuvent prendre en charge des chapitres diff\u00e9rents et lors des choix laissez des marques substitutives pour le num\u00e9ro de page o\u00f9 le lecteur doit se rendre\u2009:

    Face \u00e0 cette horde de cr\u00e9atures, vous avez le choix entre\u2009: attaquer le Gol\u00e8me qui semble \u00eatre le chef, rendez-vous \u00e0 la page XXX, ou fuir par la porte d\u00e9rob\u00e9e, rendez-vous \u00e0 la page XXX.

    Naturellement vous ne conna\u00eetrez le num\u00e9ro de page exact qu'une fois que tous les chapitres seront r\u00e9unis. L'\u00e9dition de liens est un peu comme l'assemblage de tous les chapitres pour former un livre complet, elle s'occupe de remplacer les marques substitutives par les bons num\u00e9ros de pages. Cette \u00e9tape est illustr\u00e9e dans la figure suivante.

    \u00c9dition des liens de plusieurs objets

    "}, {"location": "course-c/05-introduction/c-lang/#hello-world_1", "title": "Hello World\u2009!", "text": "

    Il est traditionnellement coutume depuis la publication en 1978 du livre The C Programming Language de reprendre l'exemple de Brian Kernighan comme premier programme.

    hello.c
    #include <stdio.h>\n\nint main(void)\n{\n    printf(\"hello, world\\n\");\n    return 0;\n}\n

    Ce programme est compos\u00e9 de deux parties. L'inclusion de la biblioth\u00e8que standard d'entr\u00e9es sorties (STandarD Inputs Outputs) \u00e0 l'aide d'une directive pr\u00e9processeur qui d\u00e9finit l'existence de la fonction printf qui vous permet d'\u00e9crire sur le terminal. Le programme principal est nomm\u00e9 main et tout ce qui se situe \u00e0 l'int\u00e9rieur des accolades { } appartient \u00e0 ce dernier. L'ensemble que d\u00e9finit main et ses accolades est appel\u00e9 une fonction, et la t\u00e2che de cette fonction est ici d'appeler une autre fonction printf. On prend soin de terminer chaque instruction par un point-virgule ;.

    L'appel d'une fonction comme printf peut prendre des param\u00e8tres comme ici le texte Hello world!\\n dont le \\n repr\u00e9sente un retour \u00e0 la ligne.

    Une fois ce code \u00e9crit, il faut le compiler. Pour bien comprendre ce que l'on fait, utilisons la ligne de commande\u2009; plus tard vous utiliserez votre \u00e9diteur de texte favori pour \u00e9crire vos programmes.

    Pour obtenir un invit\u00e9 de commande, vous devez ouvrir un terminal. Comme nous avons choisi de travailler sur un syst\u00e8me compatible POSIX, sur n'importe quel syst\u00e8me d'exploitation vous lancez un terminal et sous Windows vous devez installer WSL2. Une fois lanc\u00e9e la console ressemble \u00e0 ceci\u2009:

    $\n

    C'est intimidant si l'on n\u2019en a pas l'habitude, mais vraiment puissant, croyez-moi\u2009! La premi\u00e8re \u00e9tape est de s'assurer que le fichier test.c contient bien notre programme. Pour ce faire on utilise un autre programme cat qui ne fait rien d'autre que lire le fichier pass\u00e9 en argument et de l'afficher sur la console\u2009:

    $ cat hello.c\n#include <stdio.h>\n\nint main()\n{\n    printf(\"hello, world\");\n}\n

    \u00c9videmment, vous devez avoir \u00e9crit le programme hello.c au pr\u00e9alable. Alternativement vous pouvez utiliser la commande suivante pour cr\u00e9er le fichier hello.c :

    echo '#include <stdio.h>\\n\\nint main()\\n{\\n  printf(\"hello, world\");\\n}' > hello.c\n

    \u00c0 pr\u00e9sent on peut utiliser notre compilateur par d\u00e9faut\u2009: cc pour C Compiler. Ce compilateur prend en argument un fichier C et sans autre option, il g\u00e9n\u00e8rera un fichier a.out pour assembler output. C'est un fichier ex\u00e9cutable que l'on peut donc ex\u00e9cuter.

    Utilisez donc la commande suivante pour compiler votre programme\u2009:

    $ gcc hello.c\n

    Rien ne s'est affich\u00e9\u2009? C'est une excellente nouvelle\u2009! La philosophie POSIX veut qu'un programme soit aussi discret que possible\u2009: si tout s'est bien d\u00e9roul\u00e9, il n'est pas n\u00e9cessaire d'en informer l'utilisateur. Toutefois, cela ne signifie pas que la commande n'a eu aucun effet. En r\u00e9alit\u00e9, vous devriez maintenant trouver dans le r\u00e9pertoire courant votre fichier source ainsi que le r\u00e9sultat de la compilation, \u00e0 savoir le fichier a.out. Pour v\u00e9rifier cela, nous allons utiliser le programme ls, qui liste les fichiers pr\u00e9sents dans un r\u00e9pertoire\u2009:

    $ ls\nhello.c       a.out\n

    Parfait, nous avons bien les deux fichiers. Maintenant, ex\u00e9cutons le programme en prenant soin de pr\u00e9fixer le nom par ./, car les programmes g\u00e9n\u00e9r\u00e9s localement, comme a.out, ne peuvent pas \u00eatre lanc\u00e9s directement par leur nom pour des raisons de s\u00e9curit\u00e9. En effet, imaginez qu'un pirate malintentionn\u00e9 cr\u00e9e un programme nomm\u00e9 ls dans ce r\u00e9pertoire, qui effacerait toutes vos donn\u00e9es. Si vous ex\u00e9cutez la commande ls pour voir le contenu du r\u00e9pertoire, vous lanceriez involontairement ce programme malveillant avec des cons\u00e9quences d\u00e9sastreuses. Pour \u00e9viter ce type de probl\u00e8me, tout programme local doit \u00eatre explicitement pr\u00e9fix\u00e9 par ./ pour pouvoir \u00eatre ex\u00e9cut\u00e9. \u00c0 vous de jouer\u2009:

    $ ./a.out\nhello, world\n

    F\u00e9licitations, le programme s'est ex\u00e9cut\u00e9 correctement\u2009! Mais \u00e0 pr\u00e9sent, nous pouvons en apprendre davantage sur ce fichier. Par exemple, nous pourrions examiner la date de cr\u00e9ation du programme ainsi que l'espace qu'il occupe sur votre disque. Encore une fois, ls nous sera utile, cette fois-ci avec l'option l :

    $ ls -l a.out\n-rwxr-xr-- 1 ycr iai 8.2K Jul 24 09:50 a.out*\n

    Voyons ensemble le d\u00e9tail de ce charabia lu de gauche \u00e0 droite\u2009:

    -             Il s'agit d'un fichier\nrwx           Lisible (r), \u00c9ditable (w) et Ex\u00e9cutable (x) par le propri\u00e9taire\nr-x           Lisible (r) et Ex\u00e9cutable (x) par le groupe\nr--           Lisible (r) par les autres utilisateurs\n1             Nombre de liens mat\u00e9riels pour ce fichier\nycr           Nom du propri\u00e9taire\niai           Nom du groupe\n8.2K          Taille du fichier, soit 8200 bytes soit 65'600 bits\nJul 24 09:50  Date de cr\u00e9ation du fichier\na.out         Nom du fichier\n

    hello, world

    Les puristes peuvent se demander s'il est pr\u00e9f\u00e9rable d'\u00e9crire hello, world, hello, world! ou Hello, world!\\n. Dans son livre, Brian Kernighan a opt\u00e9 pour hello, world\\n, et c'est cette version que nous avons reprise ici.

    Au-del\u00e0 de ce souci du d\u00e9tail, il est important de souligner que la casse des caract\u00e8res a une grande importance en informatique. Hello n'est pas \u00e9quivalent \u00e0 hello, car le stockage en m\u00e9moire diff\u00e8re, et par cons\u00e9quent, le r\u00e9sultat de l'ex\u00e9cution d'un programme peut varier.

    Il est donc primordial de pr\u00eater attention \u00e0 ces subtilit\u00e9s. Vous le constaterez au fil du temps\u2009: vous d\u00e9velopperez une aisance naturelle pour rep\u00e9rer les ; manquants, les {} mal plac\u00e9es ou encore les == qui auraient d\u00fb \u00eatre =.

    Cependant, ce qui prime avant tout, c'est la coh\u00e9rence de l'ensemble. Si vous choisissez d'\u00e9crire Hello, World!, veillez \u00e0 le faire de mani\u00e8re uniforme, que ce soit dans vos exemples, vos commentaires ou l'ensemble de votre documentation.

    ", "tags": ["Hello", "posix", "hello.c", "brian-kernighan", "printf", "bibliotheque-standard", "1978", "compiler", "hello", "a.out", "test.c", "main"]}, {"location": "course-c/05-introduction/c-lang/#conclusion", "title": "Conclusion", "text": "

    Le langage C, invent\u00e9 dans les ann\u00e9es 70 par des pionniers de l'informatique, reste aujourd'hui un pilier fondamental dans le monde de la programmation, notamment pour le d\u00e9veloppement d'applications embarqu\u00e9es et de syst\u00e8mes d'exploitation. Son efficacit\u00e9, sa proximit\u00e9 avec le mat\u00e9riel, et sa capacit\u00e9 \u00e0 offrir un contr\u00f4le pr\u00e9cis des ressources en font un langage toujours pertinent, malgr\u00e9 l'\u00e9mergence de concurrents modernes comme Rust ou Zig.

    Son histoire, riche et marqu\u00e9e par des figures embl\u00e9matiques telles que Dennis Ritchie et Ken Thompson, ainsi que son influence sur de nombreux autres langages, t\u00e9moigne de sa long\u00e9vit\u00e9 et de son importance. Apprendre le C, c'est non seulement saisir les bases de la programmation, mais aussi acqu\u00e9rir des comp\u00e9tences indispensables pour tout d\u00e9veloppeur d\u00e9sireux de ma\u00eetriser les rouages du mat\u00e9riel et des syst\u00e8mes informatiques.

    Le d\u00e9veloppement en C suit un cycle rigoureux, comportant plusieurs \u00e9tapes que chaque d\u00e9veloppeur doit comprendre. Maintenant que vous avez r\u00e9ussi \u00e0 compiler votre premier programme, vous \u00eates pr\u00eat pour la suite...

    ", "tags": ["rust", "zig"]}, {"location": "course-c/05-introduction/c-lang/#exercices-de-revision", "title": "Exercices de R\u00e9vision", "text": "

    Exercice 1\u2009: Exercice

    Ouvrez le standard C99 et cherchez la valeur maximale possible de la constante ULLONG_MAX. Que vaut-elle\u2009?

    Solution

    Au paragraphe \u00a75.2.4.2.1-1 on peut lire que ULLONG_MAX est encod\u00e9 sur 64-bits et donc que sa valeur est \\(2^{64}-1\\) donc 18'446'744'073'709'551'615.

    Exercice 2\u2009: Hello World

    Pouvez-vous \u00e9crire, puis compiler votre premier programme en C\u2009? R\u00e9diger le programme hello.c qui affiche Hello, World! \u00e0 l'\u00e9cran.

    Ex\u00e9cutez le programme et v\u00e9rifiez que le message s'affiche bien.

    Exercice 3\u2009: Auteurs

    Qui a invent\u00e9 le C\u2009?

    • Ken Thompson
    • Brian Kernighan
    • Bjarne Stroustrup
    • Linus Torvalds
    • Dennis Ritchie
    • Guido van Rossum

    Exercice 4\u2009: Standard International

    Quel est le standard C \u00e0 utiliser dans l'industrie en 2024 et pourquoi\u2009?

    • C89
    • C99
    • C11
    • C17
    • C23
    Solution

    Le standard industriel, malgr\u00e9 que nous soyons en 2024 est toujours ISO/IEC 9899:2017, car peu de changements majeurs ont \u00e9t\u00e9 apport\u00e9s au langage depuis et les entreprises pr\u00e9f\u00e8rent migrer sur C++ plut\u00f4t que d'adopter un standard plus r\u00e9cent qui n'apporte que peu de changements.

    Exercice 5\u2009: Paradigmes

    Quels est le paradigme de programmation support\u00e9s par C\u2009?

    • Fonctionnel
    • Orient\u00e9 objet
    • R\u00e9flectif
    • Imp\u00e9ratif
    • D\u00e9claratif
    Solution

    C supporte les paradigmes imp\u00e9ratifs, structur\u00e9s et proc\u00e9dural.

    Exercice 6\u2009: Langage imp\u00e9ratif

    Pourriez-vous d\u00e9finir ce qu'est la programmation imp\u00e9rative\u2009?

    Solution

    La programmation imp\u00e9rative consiste en des s\u00e9quences de commandes ordonn\u00e9es. C'est-\u00e0-dire que les s\u00e9quences sont ex\u00e9cut\u00e9es dans un ordre d\u00e9finis les unes \u00e0 la suite d\u2019autres.

    Exercice 7\u2009: Coul\u00e9e de lave

    Qu'est-ce qu'une coul\u00e9e de lave en informatique\u2009?

    Solution

    Lorsqu'un code immature est mis en production, l'industriel qui le publie risque un retour de flamme d\u00fb aux bogues et m\u00e9contentement des clients. Afin d'\u00e9viter une coul\u00e9e de lave il est important qu'un programme soit test\u00e9 et soumis \u00e0 une \u00e9quipe de beta-testing qui s'assure qu'outre le respect des sp\u00e9cifications initiales, le programme soit utilisable facilement par le public cible. Il s'agit aussi d'\u00e9tudier l'ergonomie du programme.

    Un programme peut respecter le cahier des charges, \u00eatre convenablement test\u00e9, fonctionner parfaitement, mais \u00eatre difficile \u00e0 l'utilisation, car certaines fonctionnalit\u00e9s sont peu ou pas document\u00e9es. La surcharge du service de support par des clients perdus peut \u00e9galement \u00eatre assimil\u00e9e \u00e0 une coul\u00e9e de lave.

    Exercice 8\u2009: Cat

    Qu'est-ce que cat?

    • Un programme de chat
    • Un programme de compilation
    • Un programme d'affichage de fichiers
    • Un programme de copie de fichiers
    • Un programme de recherche de fichiers
    Solution

    cat est un programme normalis\u00e9 POSIX prenant en entr\u00e9e un fichier et l'affichant \u00e0 l'\u00e9cran. Il est utilis\u00e9 notamment dans cet ouvrage pour montrer que le contenu du fichier hello.c est bel et bien celui attendu.

    ", "tags": ["cat", "ULLONG_MAX", "hello.c"]}, {"location": "course-c/05-introduction/code-of-conduct/", "title": "D\u00e9veloppement logiciel", "text": "Les programmes doivent \u00eatre \u00e9crits pour \u00eatre lus par des humains, et seulement accessoirement pour \u00eatre ex\u00e9cut\u00e9s par des machines.Harold Abelson

    Objectifs

    • Comprendre les valeurs et les bonnes pratiques du d\u00e9veloppement logiciel.
    • Saisir l'importance d'apprendre par soi-m\u00eame.
    • Reconna\u00eetre l'importance de l'anglais en informatique.
    • Comprendre le r\u00f4le fondamental de la communaut\u00e9 des d\u00e9veloppeurs.

    Devenir d\u00e9veloppeur logiciel, que ce soit professionnellement ou par passion, ne se r\u00e9sume pas simplement \u00e0 \u00e9crire du code. C\u2019est une discipline qui exige une certaine finesse dans l\u2019ex\u00e9cution, le respect de r\u00e8gles, de consensus partag\u00e9s, et l\u2019adoption de bonnes pratiques.

    J\u2019ai souvent observ\u00e9, tant dans les milieux acad\u00e9miques que professionnels, des individus se revendiquant experts ou professeurs, inculquant \u00e0 leurs \u00e9l\u00e8ves ou coll\u00e8gues des pratiques dogmatiques issues de croyances personnelles ou d'habitudes d\u00e9su\u00e8tes. L\u2019informatique est une discipline vivante, fond\u00e9e sur la collaboration, l\u2019\u00e9coute et l\u2019introspection. Ainsi, il est primordial d\u2019avoir l\u2019esprit ouvert et de faire preuve d\u2019humilit\u00e9.

    On ne d\u00e9veloppe pas sur la base de certitudes fig\u00e9es, mais en s\u2019appuyant sur des principes et des valeurs qui \u00e9voluent avec le temps et varient en fonction du contexte. Un d\u00e9veloppeur web n\u2019adoptera pas les m\u00eames approches qu\u2019un scientifique utilisant Python, ou qu\u2019un programmeur embarqu\u00e9.

    Dans le cadre de projets personnels, vous pouvez coder de mani\u00e8re isol\u00e9e. Cependant, dans une entreprise, vous faites partie d\u2019une \u00e9quipe. Le code que vous \u00e9crivez doit pouvoir perdurer apr\u00e8s votre d\u00e9part. Il doit \u00eatre lisible, maintenable, testable, \u00e9volutif. Il doit \u00eatre conforme aux standards de l\u2019entreprise, respecter les conventions de codage, les bonnes pratiques, les r\u00e8gles de s\u00e9curit\u00e9 et les normes de qualit\u00e9. Il doit \u00eatre document\u00e9, comment\u00e9, versionn\u00e9 et archiv\u00e9. En somme, il doit pouvoir \u00eatre partag\u00e9, diffus\u00e9, \u00e9chang\u00e9. Pour cela, il existe des m\u00e9thodes de travail bien \u00e9tablies que nous aborderons dans ce cours.

    Cependant, les valeurs humaines fondamentales du d\u00e9veloppement logiciel transcendent les consid\u00e9rations purement techniques et m\u00e9thodologiques. Elles sont immuables, les m\u00eames qui r\u00e9gissent la soci\u00e9t\u00e9 humaine depuis des mill\u00e9naires. Parmi ces valeurs, on trouve l\u2019ouverture d\u2019esprit, l\u2019humilit\u00e9, la curiosit\u00e9, la rigueur, la patience, la pers\u00e9v\u00e9rance, l\u2019\u00e9coute, l\u2019entraide et le partage.

    ", "tags": ["developpeur", "curiosite", "patience", "discipline"]}, {"location": "course-c/05-introduction/code-of-conduct/#les-regles-evoluent", "title": "Les r\u00e8gles \u00e9voluent", "text": "

    En 1750 av. J.-C., le roi Hammurabi de Babylone a grav\u00e9 sur une st\u00e8le de basalte le premier code de lois connu de l\u2019histoire. Ce code, qui comprend 282 lois, r\u00e9gissait la vie quotidienne en M\u00e9sopotamie. Bien que ces lois soient consid\u00e9r\u00e9es comme un jalon important vers une justice \u00e9quitable, elles imposaient des sanctions souvent s\u00e9v\u00e8res\u2009: ch\u00e2timents corporels, mutilations, esclavage, voire ex\u00e9cutions. La c\u00e9l\u00e8bre loi du talion, \u00ab\u2009\u0153il pour \u0153il, dent pour dent\u2009\u00bb, en est un exemple embl\u00e9matique.

    Code d'Hammurabi (1750 av. J.-C.)

    Ce que l'on doit retenir de cette analogie avec le d\u00e9veloppement logiciel, c\u2019est que, tout comme les conventions sociales, les r\u00e8gles et les consensus en informatique \u00e9voluent avec le temps. Les bonnes pratiques d'aujourd'hui seront probablement diff\u00e9rentes demain.

    H\u00e9las, l\u2019inertie des institutions, des entreprises et des individus conduit \u00e0 la perp\u00e9tuation des habitudes et \u00e0 l\u2019\u00e9tablissement de dogmes sans que l'on s'en aper\u00e7oive. Il est donc indispensable de faire preuve d'ouverture d\u2019esprit, de remise en question et de curiosit\u00e9 pour s\u2019adapter \u00e0 un monde en perp\u00e9tuelle \u00e9volution.

    En d\u2019autres termes, ce que je vous transmets aujourd'hui n'est pas une v\u00e9rit\u00e9 absolue. Elle est teint\u00e9e par mon v\u00e9cu, mes exp\u00e9riences et mes valeurs. Il vous appartient donc de la remettre en question, d\u2019y r\u00e9fl\u00e9chir avec un esprit critique.

    ", "tags": ["hammurabi-de-babylone", "loi-du-talion", "1750", "mesopotamie"]}, {"location": "course-c/05-introduction/code-of-conduct/#langlais", "title": "L'Anglais", "text": "

    La langue, une barri\u00e8re

    En programmation, quel que soit le langage utilis\u00e9, l\u2019anglais est omnipr\u00e9sent. Les mots-cl\u00e9s des langages de programmation sont majoritairement issus de l'anglais, et bon nombre d'outils de d\u00e9veloppement sont exclusivement disponibles dans cette langue. Pourquoi cela\u2009? Tout comme un article de journal local n\u2019int\u00e9ressera que peu des lecteurs \u00e0 l\u2019autre bout du globe, un code informatique doit pouvoir \u00eatre r\u00e9utilis\u00e9 pour \u00e9conomiser les co\u00fbts de d\u00e9veloppement, et donc s\u2019affranchir des barri\u00e8res linguistiques.

    On r\u00e9utilise ainsi volontiers un algorithme \u00e9crit par un illustre programmeur japonais, ou une biblioth\u00e8que de calcul matriciel d\u00e9velopp\u00e9e en Am\u00e9rique du Sud. Pour que chacun puisse corriger, maintenir ou am\u00e9liorer le code des autres, une langue commune est n\u00e9cessaire, et l'anglais est naturellement devenu cette langue.

    Dans ce cours, bien que r\u00e9dig\u00e9 en fran\u00e7ais, l\u2019anglais sera privil\u00e9gi\u00e9 pour les exemples de code et les noms des symboles (variables, constantes, etc.). Les termes techniques seront traduits lorsqu'un consensus existe, mais sinon l'anglicisme sera pr\u00e9f\u00e9r\u00e9. En effet, bien que l\u2019on pourrait parler de \u00ab\u2009feu d\u2019alerte\u2009\u00bb \u00e0 la place de warning, le terme perdrait sa pertinence technique. J'opte donc, m\u00eame au risque de froisser l\u2019Acad\u00e9mie, pour pr\u00e9server les usages \u00e9tablis parmi les d\u00e9veloppeurs.

    Un autre point m\u00e9rite d'\u00eatre mentionn\u00e9\u2009: l\u2019interaction constante d\u2019un d\u00e9veloppeur avec Internet pour y puiser exemples, conseils ou assistance dans l\u2019utilisation d'outils d\u00e9velopp\u00e9s par d'autres. La majorit\u00e9 de ces ressources sont en anglais.

    Apprenez les langues

    Ne n\u00e9gligez pas les cours de langues. Partez \u00e0 l\u2019\u00e9tranger, lisez des livres en anglais, regardez des films en version originale, \u00e9coutez des podcasts, des conf\u00e9rences et des tutoriels en anglais\u2009: cela vous ouvrira les portes de la connaissance.

    De surcro\u00eet, sans cette comp\u00e9tence, trouver un emploi pourra s\u2019av\u00e9rer plus difficile, car les entreprises sont souvent internationales et les \u00e9quipes de d\u00e9veloppement multiculturelles.

    ", "tags": ["anglais"]}, {"location": "course-c/05-introduction/code-of-conduct/#apprendre-a-pecher", "title": "Apprendre \u00e0 p\u00eacher", "text": "

    Un p\u00e8re et son fils p\u00eachant

    Un jeune homme s'en va \u00e0 la mer avec son p\u00e8re et lui demande\u2009: \u00ab\u2009papa, j'ai faim, comment ram\u00e8nes-tu du poisson\u2009?\u2009\u00bb Le p\u00e8re, fier, lance sa ligne \u00e0 la mer et lui ram\u00e8ne un beau poisson. Plus tard, alors que le jeune homme revient d'une balade sur les estrans, il demande \u00e0 son p\u00e8re\u2009: \u00ab\u2009papa, j'ai faim, me ram\u00e8nerais-tu du poisson\u2009?\u2009\u00bb Le p\u00e8re, sort de son \u00e9tui sa plus belle canne et l'\u00e9quipant d'un bel hame\u00e7on, lance sa ligne \u00e0 la mer et ram\u00e8ne un gros poisson. Durant longtemps, le jeune homme mange ainsi \u00e0 sa faim cependant que le p\u00e8re ram\u00e8ne du poisson pour son fils.

    Un jour, alors que le fils invective son p\u00e8re l'estomac vide, le p\u00e8re annonce. \u00ab\u2009Fils, il est temps pour toi d'apprendre \u00e0 p\u00eacher, je peux te montrer encore longtemps comment je ram\u00e8ne du poisson, mais ce ne serait pas t'aider, voici donc cette canne et cet hame\u00e7on.\u2009\u00bb

    Le jeune homme tente de r\u00e9p\u00e9ter les gestes de son p\u00e8re, mais il ne parvient pas \u00e0 ramener le poisson qui le rassasierait. Il demande \u00e0 son p\u00e8re de l'aide que ce dernier refuse. \u00ab\u2009Fils, c'est par la pratique et avec la faim au ventre que tu parviendras \u00e0 prendre du poisson, pers\u00e9v\u00e8re et tu deviendras meilleur p\u00eacheur que moi, la lign\u00e9e sera ainsi assur\u00e9e de toujours manger \u00e0 sa faim\u2009\u00bb.

    La morale de cette histoire est plus que jamais applicable en programmation, confier aux exp\u00e9riment\u00e9s l'\u00e9criture d'algorithmes compliqu\u00e9s, ou se contenter d'observer les r\u00e9ponses des exercices pour se dire\u2009: j'ai compris ce n'est pas si compliqu\u00e9, est une erreur, car p\u00eacher ou expliquer comment p\u00eacher n'est pas la m\u00eame chose.

    Aussi, cet ouvrage se veut \u00eatre un guide pour apprendre \u00e0 apprendre le d\u00e9veloppement logiciel et non un guide exhaustif du langage, car le standard C99/C11 est disponible sur internet ainsi que le K&R qui reste l'ouvrage de r\u00e9f\u00e9rence pour apprendre le C. Il est donc inutile de paraphraser les exemples donn\u00e9s quand internet apporte toutes les r\u00e9ponses, pour tous les publics du profane r\u00e9serv\u00e9 au hacker passionn\u00e9.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#une-affaire-de-consensus", "title": "Une affaire de consensus", "text": "

    En informatique, tout comme dans la soci\u00e9t\u00e9, il existe des religieux, des pros\u00e9lytes, des dogmatiques, des fanatiques, des contestataires et des maximalistes. Leurs querelles portent souvent sur les outils qu'ils utilisent ou sur des pratiques dont on ne doit pas d\u00e9vier. Ils d\u00e9battent parfois \u00e2prement des conventions de codage, de l'encodage des fichiers, du choix de la fin de ligne, de l'interdiction du goto, ou encore du strict respect des r\u00e8gles MISRA.

    Ces \u00ab\u2009guerres de croyances\u2009\u00bb, qui perdurent parfois depuis des g\u00e9n\u00e9rations, se nourrissent d\u2019un manque d\u2019ouverture d\u2019esprit et d\u2019un attachement dogmatique \u00e0 des habitudes. Cela s\u2019explique souvent par le biais d\u2019ancrage mental qui s\u2019installe d\u00e8s l\u2019\u00e9cole, l\u00e0 o\u00f9 l'on inculque parfois des certitudes fig\u00e9es.

    L'enseignant se doit d'\u00eatre sensible \u00e0 ces questions et doit encourager l'impartialit\u00e9, ainsi qu'un \u00e9tat d'esprit ouvert, guid\u00e9 par le bon sens de l'ing\u00e9nieur.

    Un exemple c\u00e9l\u00e8bre est celui des guerres d'\u00e9diteurs qui opposent, depuis les ann\u00e9es 1970, les adeptes de vi aux fervents d\u00e9fenseurs d'emacs. Ces deux \u00e9diteurs de texte, extr\u00eamement puissants et complexes \u00e0 ma\u00eetriser, polarisent les opinions de mani\u00e8re radicale. Ces guerres, entretenues \u00e0 l'origine par un esprit de plaisanterie, ont progressivement pris une tournure \u00e9motionnelle qui d\u00e9passe parfois le simple cadre de l'outil.

    S\u2019enfermer dans une zone de confort renforce le biais du Marteau de Maslow, car lorsqu'on est un marteau, on finit par voir tous les probl\u00e8mes comme des clous. Ce confort devient alors un ennemi qui freine le regard critique et le pragmatisme, pourtant essentiels. Il faut accepter l\u2019existence de diverses approches pour r\u00e9soudre un probl\u00e8me donn\u00e9, car le d\u00e9veloppement logiciel, plus que tout autre domaine technique, est une aventure collaborative qui ne devrait jamais \u00eatre soumise \u00e0 des emprises \u00e9motionnelles.

    Un programme se doit d'\u00eatre neutre, impartial, et minimaliste. L\u2019essentiel n\u2019est pas de s'attarder sur des questions esth\u00e9tiques comme la position des accolades, l\u2019utilisation d'espaces ou de tabulations, ou le choix d\u2019un \u00e9diteur sur un autre.

    La cl\u00e9 de la bonne attitude c'est d'\u00eatre \u00e0 l'\u00e9coute du consensus et de ne pas sombrer au biais d'attention. Il faut non seulement \u00eatre sensible au consensus local direct\u2009: son entreprise, son \u00e9cole, son \u00e9quipe de travail, mais surtout au consensus plan\u00e9taire dont l'acc\u00e8s ne peut se faire que par l'interaction directe avec la communaut\u00e9 de d\u00e9veloppeurs, soit par les forums de discussions (Reddit, stackoverflow), soit par le code lui-m\u00eame. Vous avez un doute sur la bonne m\u00e9thode pour \u00e9crire tel algorithme ou sur la fa\u00e7on dont votre programme devrait \u00eatre structur\u00e9\u2009? Plongez-vous dans le code des autres, multipliez vos exp\u00e9riences, observez les disparit\u00e9s et les oppositions, et apprenez \u00e0 ne pas y \u00eatre sensible.

    Vous verrez qu'au d\u00e9but, un programme ne vous semble lisible que s'il respecte vos habitudes, la taille de vos indentations pr\u00e9f\u00e9r\u00e9es, la police de caract\u00e8re qui vous sied le mieux, l'\u00e9diteur qui supporte les ligatures...

    Par la suite, et \u00e0 la relecture de cette section, vous apprendrez \u00e0 faire fi de cette zone de confort qui vous \u00e9tait si cher et que l'important n'est plus la forme, mais le fond. Vous aurez comme N\u00e9o, lib\u00e9r\u00e9 votre esprit et serez capable de voir la matrice sans filtre ni biais.

    En somme, restez ouvert aux autres points de vue, cherchez \u00e0 adopter le consensus majoritaire qui dynamise au mieux votre \u00e9quipe de d\u00e9veloppement, qui s'encadre le mieux dans votre strat\u00e9gie de croissance et de collaboration et surtout, abreuvez-vous de code, faites-en des indigestions, r\u00eavez-en la nuit. Vous tradez du Bitcoin, allez lire le code source, vous aimez Linux, plongez-vous dans le code source du kernel, certains coll\u00e8gues ou amis vous ont parl\u00e9 de Git, allez voir ses entrailles... Oui, tous ces projets sont \u00e9crits en C, n'est-ce pas merveilleux\u2009?

    ", "tags": ["goto", "emacs"]}, {"location": "course-c/05-introduction/code-of-conduct/#lopen-source", "title": "L'open source", "text": "

    Au d\u00e9but de l'informatique, les programmes \u00e9taient distribu\u00e9s avec leur code source, car les ordinateurs \u00e9taient rares et co\u00fbteux et que les utilisateurs \u00e9taient souvent des d\u00e9veloppeurs. Avec l'arriv\u00e9e des ordinateurs personnels, les \u00e9diteurs de logiciels ont commenc\u00e9 \u00e0 distribuer des programmes compil\u00e9s, car les utilisateurs n'\u00e9taient plus des d\u00e9veloppeurs et que le code source \u00e9tait devenu un secret industriel mon\u00e9tisable. C'est ainsi que le logiciel propri\u00e9taire est n\u00e9. Les \u00e9diteurs de logiciels ont tir\u00e9 parti de cette situation pour verrouiller leurs clients dans un \u00e9cosyst\u00e8me propri\u00e9taire, les emp\u00eachant de modifier le logiciel, de le partager ou de le vendre.

    Dans les ann\u00e9es 1980, Richard Stallman, un informaticien am\u00e9ricain, a lanc\u00e9 le projet GNU pour cr\u00e9er un syst\u00e8me d'exploitation libre, c'est-\u00e0-dire un syst\u00e8me d'exploitation dont le code source est librement accessible, modifiable et redistribuable. N\u00e9anmoins la licence GPL (GNU Public License) qui prot\u00e8ge le code source de GNU est tr\u00e8s contraignante et ne permet pas de cr\u00e9er des logiciels propri\u00e9taires bas\u00e9s sur du code source GPL. C'est un frein pour les entreprises qui souhaitent prot\u00e9ger leur propri\u00e9t\u00e9 intellectuelle.

    En 1991, Linus Torvalds, un \u00e9tudiant finlandais, a cr\u00e9\u00e9 le noyau Linux, qui est devenu le noyau du syst\u00e8me d'exploitation GNU/Linux. Depuis lors, de nombreux logiciels libres ont \u00e9t\u00e9 d\u00e9velopp\u00e9s, notamment le navigateur web Firefox, le serveur web Apache, le syst\u00e8me de gestion de base de donn\u00e9es MySQL, le langage de programmation Python, le syst\u00e8me de gestion de versions Git, etc.

    Cette philosophie du logiciel libre a \u00e9t\u00e9 popularis\u00e9e par le hacker am\u00e9ricain Eric Raymond dans son essai \u00ab\u2009La cath\u00e9drale et le bazar\u2009\u00bb qui d\u00e9crit deux mod\u00e8les de d\u00e9veloppement logiciel\u2009: le mod\u00e8le de la cath\u00e9drale, o\u00f9 le code source est d\u00e9velopp\u00e9 en interne par une \u00e9quipe restreinte, et le mod\u00e8le du bazar, o\u00f9 le code source est d\u00e9velopp\u00e9 de mani\u00e8re collaborative par une communaut\u00e9 de d\u00e9veloppeurs.

    L'expression open source s'est largement impos\u00e9e dans le monde de l'informatique pour d\u00e9signer les logiciels libres, car elle est plus neutre et moins id\u00e9ologique que l'expression logiciel libre. De grandes soci\u00e9t\u00e9s comme Google, Facebook, Microsoft, IBM, Oracle, etc., ont adopt\u00e9 la philosophie du logiciel libre et contribuent activement \u00e0 de nombreux projets open source. Par exemple, Google a d\u00e9velopp\u00e9 le syst\u00e8me d'exploitation Android, qui est bas\u00e9 sur le noyau Linux, et qui est utilis\u00e9 par la plupart des smartphones dans le monde. Facebook a d\u00e9velopp\u00e9 le framework React, qui est utilis\u00e9 par de nombreux sites web pour cr\u00e9er des interfaces utilisateur interactives. Microsoft a rachet\u00e9 GitHub, la plateforme de d\u00e9veloppement collaboratif la plus populaire au monde, et a ouvert le code source de nombreux projets, notamment le framework .NET. IBM a rachet\u00e9 Red Hat, l'\u00e9diteur de la distribution Linux Red Hat Enterprise Linux, et contribue activement \u00e0 de nombreux projets open source.

    Mettre un logiciel ou une partie de logiciel en open source c'est permettre \u00e0 d'autres d\u00e9veloppeurs de contribuer au projet, de corriger des bogues, d'ajouter des fonctionnalit\u00e9s, de traduire le logiciel dans d'autres langues, etc. C'est aussi un moyen de faire conna\u00eetre son travail, de se faire un nom dans la communaut\u00e9 des d\u00e9veloppeurs, de trouver un emploi, de cr\u00e9er une entreprise, etc. Mais c'est aussi un moyen de partager ses connaissances, de contribuer \u00e0 l'\u00e9ducation, \u00e0 la recherche, \u00e0 la culture, \u00e0 l'humanit\u00e9.

    L'open source est devenu un mod\u00e8le \u00e9conomique viable pour de nombreuses entreprises, qui vendent des services autour de logiciels open source, comme le support, la formation, la personnalisation, l'h\u00e9bergement, etc. C'est aussi un moyen de r\u00e9duire les co\u00fbts de d\u00e9veloppement, de mutualiser les efforts, de partager les risques, de favoriser l'innovation et de promouvoir la transparence.

    Pourquoi ne pas faire d'open source\u2009? C'est une question que vous vous poserez t\u00f4t ou tard dans votre carri\u00e8re de d\u00e9veloppeur. Vous avez peut-\u00eatre peur de la concurrence, de la critique, du piratage, de la perte de contr\u00f4le, de la complexit\u00e9, de l'engagement, de la responsabilit\u00e9, de la r\u00e9putation, de la l\u00e9galit\u00e9, de la s\u00e9curit\u00e9, de la confidentialit\u00e9, de la propri\u00e9t\u00e9 intellectuelle, etc. De nombreuses soci\u00e9t\u00e9s ont fait le choix de prot\u00e9ger leur propri\u00e9t\u00e9 intellectuelle en gardant leur code source secret, mais cela \u00e0 un co\u00fbt. Les logiciels doivent \u00eatre prot\u00e9g\u00e9s par des licences, des brevets, le code doit \u00eatre crypt\u00e9, les serveurs doivent \u00eatre s\u00e9curis\u00e9s, les employ\u00e9s doivent \u00eatre surveill\u00e9s, les clients doivent \u00eatre contr\u00f4l\u00e9s, etc. C'est un cercle vicieux qui peut conduire \u00e0 la parano\u00efa, \u00e0 la m\u00e9fiance.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#la-communaute", "title": "La communaut\u00e9", "text": "

    Se passionner pour le d\u00e9veloppement logiciel c'est aussi se passionner pour la communaut\u00e9 des d\u00e9veloppeurs. Avant internet, les d\u00e9veloppeurs se rencontraient dans des clubs d'informatique, des associations d'utilisateurs, des conf\u00e9rences, des salons, des formations, des hackathons, des meetups, etc. Moi-m\u00eame, \u00e0 douze ans, je suis rentr\u00e9 au Mac Club de Gen\u00e8ve. Un club d'informatique pour les passionn\u00e9s de Macintosh. J'ai fait mes premiers pas sur internet avec des modem rudimentaires.

    Avec internet, les d\u00e9veloppeurs se rencontrent maintenant sur des forums (Stack Overflow, Reddit...), des listes de diffusion, des chats, des blogs, des r\u00e9seaux sociaux, des plateformes de d\u00e9veloppement collaboratif, etc.

    GitHub a \u00e9t\u00e9 cr\u00e9\u00e9 en 2008 par Tom Preston-Werner, Chris Wanstrath, PJ Hyett et Scott Chacon pour faciliter le d\u00e9veloppement collaboratif de logiciels open source. GitHub est devenu la plateforme de d\u00e9veloppement collaboratif la plus populaire au monde, avec plus de 100 millions de d\u00e9p\u00f4ts de code source, plus de 40 millions de d\u00e9veloppeurs. On y trouve de tout, il suffit de chercher.

    Pour les questions techniques, il y a Stack Overflow, un site de questions-r\u00e9ponses cr\u00e9\u00e9 en 2008 par Jeff Atwood et Jo\u00ebl Spolsky. Stack Overflow est devenu le site de questions-r\u00e9ponses le plus populaire au monde, avec plus de 10 millions de questions, plus de 20 millions de r\u00e9ponses, plus de 10 millions de membres. Je vous encourage personnellement \u00e0 y contribuer, cela commence par cr\u00e9er un compte, poser des questions, r\u00e9pondre \u00e0 des questions, voter pour des questions, voter pour des r\u00e9ponses.

    Voici quelques liens utiles\u2009:

    Stack Overflow

    Aujourd'hui le plus grand portail de questions/r\u00e9ponses d\u00e9di\u00e9 \u00e0 la programmation logicielle

    GitHub

    Un portail de partage de code

    Google Scholar

    Un point d'entr\u00e9e essentiel pour la recherche d'articles scientifiques

    Man Pages

    La documentation (man pages) des commandes et outils les plus utilis\u00e9s dans les environnements macOS/Linux/Unix et POSIX compatible.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#la-revue-de-code", "title": "La revue de code", "text": "

    Enfin, je voudrais terminer cette introduction par un point essentiel du d\u00e9veloppement logiciel\u2009: la revue de code, qui est trop souvent n\u00e9glig\u00e9e.

    La revue de code est une pratique essentielle pour am\u00e9liorer la qualit\u00e9 du code source, la productivit\u00e9 des d\u00e9veloppeurs, la s\u00e9curit\u00e9 des logiciels, la satisfaction des clients. La revue de code consiste \u00e0 examiner le code source d'un d\u00e9veloppeur par un autre d\u00e9veloppeur pour d\u00e9tecter des erreurs, des anomalies, des incoh\u00e9rences, des inefficacit\u00e9s, des non-conformit\u00e9s, des risques, des opportunit\u00e9s. La revue de code peut \u00eatre formelle ou informelle, manuelle ou automatique, individuelle ou collective, interne ou externe, syst\u00e9matique ou al\u00e9atoire, planifi\u00e9e ou impromptue, etc.

    Dans les entreprises c'est un des plus gros probl\u00e8mes. Les d\u00e9veloppeurs n'aiment pas qu'on critique leur code, les chefs de projet n'aiment pas perdre du temps \u00e0 examiner le code, les clients n'aiment pas payer pour la revue de code, les managers n'aiment pas les conflits entre d\u00e9veloppeurs, les commerciaux n'aiment pas les retards de livraison, les juristes n'aiment pas les risques de litige. Les gens manquent d'humilit\u00e9 et d'ouverture d'esprit. Pourtant, s'ouvrir \u00e0 la critique, cela permet de s'am\u00e9liorer et d'apprendre.

    "}, {"location": "course-c/05-introduction/code-of-conduct/#conclusion", "title": "Conclusion", "text": "

    En r\u00e9sum\u00e9, devenir d\u00e9veloppeur logiciel, que ce soit en tant que professionnel ou par passion, est bien plus qu\u2019une simple question d'\u00e9criture de code. C'est un art qui demande une compr\u00e9hension profonde des principes, des bonnes pratiques et des valeurs fondamentales qui r\u00e9gissent cette discipline en constante \u00e9volution. Trop souvent, les dogmes acad\u00e9miques et les habitudes obsol\u00e8tes polluent l'apprentissage et la pratique, au d\u00e9triment de l'esprit collaboratif et critique indispensable \u00e0 la r\u00e9ussite dans ce domaine.

    Le d\u00e9veloppement logiciel ne repose pas sur des acquis immuables, mais sur des principes adaptatifs et des valeurs humaines intemporelles telles que l'ouverture d'esprit, l'humilit\u00e9, la curiosit\u00e9, la rigueur, la patience, la pers\u00e9v\u00e9rance, l'\u00e9coute, l'entraide et le partage. Ces valeurs, qui ont permis \u00e0 l'humanit\u00e9 de prosp\u00e9rer, sont tout aussi essentielles dans le monde du d\u00e9veloppement logiciel.

    Il est crucial de comprendre que le code que vous \u00e9crivez aujourd'hui doit pouvoir \u00eatre compris, maintenu et \u00e9volu\u00e9 par d'autres demain. Ainsi, il doit respecter les standards de l'entreprise, les conventions de codage, les bonnes pratiques et les r\u00e8gles de s\u00e9curit\u00e9. Cela n\u00e9cessite un engagement envers des m\u00e9thodes de travail \u00e9prouv\u00e9es, mais aussi une attitude de remise en question constante et d'ouverture \u00e0 l'innovation.

    L'anglais, langue universelle de la programmation, est un outil indispensable pour naviguer dans cet univers globalis\u00e9. Il permet de partager des connaissances, d'acc\u00e9der \u00e0 des ressources et de collaborer avec des d\u00e9veloppeurs du monde entier. Ne sous-estimez pas l'importance de ma\u00eetriser cette langue pour votre carri\u00e8re.

    Apprendre \u00e0 p\u00eacher plut\u00f4t qu'\u00e0 se faire donner du poisson est une le\u00e7on cl\u00e9 dans l'apprentissage du d\u00e9veloppement logiciel. La pratique, la pers\u00e9v\u00e9rance et l'exp\u00e9rimentation personnelle sont indispensables pour acqu\u00e9rir les comp\u00e9tences n\u00e9cessaires et devenir autonomes.

    L'informatique est aussi une affaire de consensus. Les guerres de croyances et les dogmes ne font que limiter la croissance et l'innovation. Adopter une attitude pragmatique et ouverte, en se basant sur le consensus de la communaut\u00e9 mondiale des d\u00e9veloppeurs, est essentiel pour progresser et s'\u00e9panouir dans ce m\u00e9tier.

    L'open source incarne parfaitement l'esprit de partage et de collaboration qui est au c\u0153ur du d\u00e9veloppement logiciel. Il permet non seulement de contribuer \u00e0 des projets d'envergure mondiale, mais aussi de se faire un nom, d'apprendre des autres et de donner en retour.

    Enfin, la communaut\u00e9 des d\u00e9veloppeurs est une ressource inestimable. Participer activement \u00e0 des forums, des plateformes collaboratives, des conf\u00e9rences et des meetups enrichit non seulement vos comp\u00e9tences, mais aussi votre r\u00e9seau professionnel.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/", "title": "Mon ordinateur et moi", "text": "

    Vous \u00eates devant votre ordinateur, vous avez certainement devant vous un clavier, une souris \u00e0 droite de votre clavier, et un ou plusieurs \u00e9crans. Votre ordinateur d\u00e9marre et vous voyez appara\u00eetre soit\u2009:

    • une pomme croqu\u00e9e ( Apple);
    • une fen\u00eatre \u00e0 carreaux ( Windows);
    • un manchot Ad\u00e9lie ( Linux).

    Le responsable de cet \u00e9cran de d\u00e9marrage, c'est votre syst\u00e8me d'exploitation et peu importe lequel vous utilisez, la bonne nouvelle c'est que vous pourrez \u00e9crire vos premiers programmes.

    Familiarisons-nous un peu avec l'ordinateur, le voulez-vous bien\u2009?

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#systeme-dexploitation", "title": "Syst\u00e8me d'exploitation", "text": "

    Dans cet ouvrage, la plupart des exemples seront pr\u00e9sent\u00e9s sous Linux. C'est un choix dogmatique parce que je pr\u00e9f\u00e8re Linux \u00e0 Windows, mais c'est aussi pour des raisons objectives et respectables. D'une part, Linux a l'avantage d'\u00eatre normalis\u00e9. Il respecte en grande partie le standard POSIX, comme d'ailleurs Apple macOS. D'autre part, m\u00eame sous Windows, il est possible \u00e0 tout utilisateur d'installer d'un sous-syst\u00e8me Linux nomm\u00e9 WSL2 (Windows Subsystem for Linux), facilitant ainsi l'ex\u00e9cution de programmes Linux sur un environnement Windows. Aussi quelque soit votre ob\u00e9dience geeko-spirituelle, vous aurez toujours la possibilit\u00e9 de suivre les exemples de ce cours.

    Notons qu'un syst\u00e8me d'exploitation en lui-m\u00eame n'est rien d'autre qu'un programme sophistiqu\u00e9 qui sert d'interm\u00e9diaire entre le mat\u00e9riel et les autres logiciels. On peut le comparer \u00e0 un chef d'orchestre, coordonnant les ressources de votre ordinateur, lan\u00e7ant les programmes, g\u00e9rant les fichiers, et supervisant les utilisateurs. C'est une couche d'abstraction qui permet \u00e0 votre machine de fonctionner harmonieusement, en masquant la complexit\u00e9 du mat\u00e9riel. Vous n'avez pas \u00e0 vous soucier des milliards de changements d'\u00e9tats \u00e9lectroniques par seconde survenant dans le processeur ou des quelque deux millions de pixels de votre \u00e9cran qui peuvent \u00eatre configur\u00e9s selon 16 millions de couleurs diff\u00e9rentes environ soixante fois par seconde.

    Votre syst\u00e8me d'exploitation c'est votre interface coh\u00e9rente et conviviale humain-machine.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#editeur-de-code-source", "title": "\u00c9diteur de code source", "text": "

    Pour \u00e9crire un programme, vous aurez besoin d'un \u00e9diteur de code, c'est un programme (oui, lui aussi) qui vous permet d'\u00e9crire du texte et de le sauvegarder dans un fichier\u2009; il en existe des centaines, certains plus aboutis que d'autres.

    Si vous trouvez une Dolor\u00e9ane munie d'un convecteur temporel, et que vous d\u00e9passez les 88 miles \u00e0 l'heure, avec une \u00e9nergie de 2.21 Gigot-Watt vous pouvez vous rendre en 1973 et utiliser un \u00e9diteur de code qui s'appelle ed (prononc\u00e9 \u00ab\u2009idi\u2009\u00bb) \u00e9crit par Ken Thompson (un des cr\u00e9ateurs d'Unix et du langage C, mais nous reviendrons sur lui plus tard).

    C'est un \u00e9diteur de texte qui a \u00e9t\u00e9 \u00e9crit \u00e0 l'\u00e9poque des t\u00e9l\u00e9types et qui curieusement a travers\u00e9 les \u00e2ges, car il est encore int\u00e9gr\u00e9 au standard POSIX. Il est par cons\u00e9quent toujours disponible sur nos syst\u00e8mes d'exploitation modernes. Toutefois \u00e0 cette \u00e9poque, il n'y avait pas d'\u00e9cran, et nos homologues de cette \u00e9poque utilisaient des imprimantes pour afficher un r\u00e9sultat. Cet \u00e9diteur primitif n'\u00e9tait donc pas tr\u00e8s interactif.

    Autre fait notable c'est que ed est l'un des premiers \u00e9diteurs dit modale. En effet, son utilisation \u00e9tant assez d\u00e9routante puisqu'il n'y a pas de retour visuel imm\u00e9diat (n'imaginez tout de m\u00eame pas qu'\u00e0 cette \u00e9poque nous imprimions chaque lettre frapp\u00e9e au clavier sur du papier), donc pour saisir du texte il fallait entrer taper des commandes, certaines pour sauvegarder, pour quitter, pour rechercher et remplacer, etc. Un exemple vaut mieux qu'un long discours. Imaginons que nous voulions saisir le po\u00e8me \u00ab\u2009L'albatros\u2009\u00bb de Charles Baudelaire dans un fichier nomm\u00e9 albatros.txt. Voici comment nous pourrions proc\u00e9der\u2009:

    $ ed\na\nSouvent, pour s'amuser, les homes d'\u00e9quipage\nPrennent des albatros,\nvastes oiseaux des mers,\nLe navire glissant sur les gouffres amers.\n.\ni\nQui suivent, indolents compagnons de voyage,\n.\n2,3j\n,p\nSouvent, pour s'amuser, les homes d'\u00e9quipage\n%s/homes/hommes/g\nw albatros.txt\n164\nq\n

    Dans les \u00e9tapes ci-dessus, nous avons lanc\u00e9 l'\u00e9diteur ed puis bascul\u00e9 en mode d'insertion avec la commande a pour ajouter du texte. Nous avons ensuite ajout\u00e9 un texte, mais comme il n'y a pas de retour visuel, nous ne sommes pas certains d'avoir orthographi\u00e9 juste tous les mots. \u00c0 la fin de la saisie, on revient en mode commande avec . puis nous d\u00e9cidons de poursuivre en ins\u00e9rant une nouvelle ligne i. Passons maintenant \u00e0 la correction. On sait d\u00e9j\u00e0 que l'on a ajout\u00e9 un retour \u00e0 la ligne en trop entre la ligne 2 et 3. Nous pouvons les joindre avec 2,3j (joindre lignes 2 et 3). Enfin, nous imprimons (physiquement sur une imprimante) la premi\u00e8re ligne avec 1p (print ligne 1). Constatant l'erreur, nous rempla\u00e7ons homes par hommes avec %s/homes/hommes/g. Enfin, on sauvegarde le fichier avec w albatros.txt, la commande retourne (sur l'imprimante) le nombre de caract\u00e8res sauvegard\u00e9s, soit 164. \u00c0 la fin de ce laborieux exercice, nous quittons ed avec la commande q.

    Tr\u00eave de plaisanteries, je vous rassure, vous n'allez probablement pas utiliser ed au quotidien, ni probablement m\u00eame jamais. Cependant conna\u00eetre son existence permet de mieux comprendre le contexte g\u00e9n\u00e9ral. Aussi, je vous propose de continuer un peu notre voyage spatio-temporel...

    En 1991 na\u00eet un \u00e9diteur de code qui va r\u00e9volutionner le monde de la programmation, il s'appelle vim (Vi Improved). C'est un \u00e9diteur de code qui est ultra puissant, mais dont la courbe d'apprentissage assez velue. Il est toujours tr\u00e8s utilis\u00e9 de nos jours, et il est disponible sur tous les syst\u00e8mes d'exploitation. En outre, la plupart des \u00e9diteurs modernes disposent d'une extension pour \u00e9muler, du moins en partie vim. Comme ed, c'est un \u00e9diteur modal\u2009: un mode pour \u00e9crire du texte, un mode pour \u00e9diter du texte, un mode pour naviguer dans le texte, un mode pour saisir des commandes, etc.

    Puisque nous nommons Vim, je dois aussi nommer son plus f\u00e9roce concurrent\u2009: Emacs. Emacs est un \u00e9diteur de code invent\u00e9 par Richard Stallman, le p\u00e8re fondateur de l'open source. Rival de Vim depuis des d\u00e9cennies, Emacs est un \u00e9diteur qui est aussi incroyablement puissant, mais il semble un peu moins utilis\u00e9 de nos jours. Si je prends le soin de mentionner les deux \u00e9diteurs, c'est que leurs utilisateurs sont souvent tr\u00e8s passionn\u00e9s et tr\u00e8s engag\u00e9s dans leur choix d'\u00e9diteurs. Il y a m\u00eame des blagues d'informaticiens sur le sujet\u2009:

    Guerre d'\u00e9diteurs

    Je fais volontiers l'impasse sur d'autres \u00e9diteurs qui ont aussi \u00e9t\u00e9 populaires en leurs temps, mais qui me semblent technologiquement d\u00e9pass\u00e9s\u2009: TextPad, UltraEdit, Sublime Text, Atom, NotePad++... L'Usain Bolt, le Michael Phelps des \u00e9diteurs c'est Visual Studio Code, l'\u00e9diteur phare de Microsoft qui a conquis les doigts agiles des d\u00e9veloppeurs du monde entier. Il est gratuit, open source, et il est disponible sur tous les syst\u00e8mes d'exploitation. Il dispose de nombreuses extensions (notamment l'extension Vim utilis\u00e9e par 6'700'000 personnes et l'extension Emacs utilis\u00e9e par 55'000 personnes). Il est tr\u00e8s rapide, tr\u00e8s puissant, et il est tr\u00e8s bien int\u00e9gr\u00e9 la plupart des outils tiers que nous utiliserons dans ce cours. Il est donc l'\u00e9diteur que je vous recommande jusqu'\u00e0 la prochaine r\u00e9volution.

    De mani\u00e8re plus factuelle, le r\u00e9sultat de l'\u00e9tude annuelle 2023 de Stackoverflow donne une id\u00e9e g\u00e9n\u00e9ralement assez bonne de la popularit\u00e9 des \u00e9diteurs et environnements de d\u00e9veloppement int\u00e9gr\u00e9s les plus utilis\u00e9s par les d\u00e9veloppeurs de logiciels\u2009:

    %% Utilisation des \u00e9diteurs de code\npie\n    \"Visual Studio Code\" : 73.3\n    \"Visual Studio\" : 28.4\n    \"IntelliJ IDEA\" : 26.8\n    \"Notepad++\": 24.5\n    \"Vim\": 22.3\n    \"Emacs\": 4.69\n    \"Eclipse\": 9.9\n    \"Nano\": 8.98
    Utilisation des \u00e9diteurs de code", "tags": ["albatros.txt", "hommes", "homes", "vim"]}, {"location": "course-c/05-introduction/me-and-my-computer/#fonctionnalites-attendues", "title": "Fonctionnalit\u00e9s attendues", "text": "

    Les \u00e9diteurs de code modernes contrairement \u00e0 des outils comme notepad sous Windows, disposent de nombreuses fonctionnalit\u00e9s qui facilitent la vie des d\u00e9veloppeurs. Voici quelques-unes des fonctionnalit\u00e9s que vous pouvez attendre d'un \u00e9diteur de code moderne\u2009:

    Coloration Synatxique (syntax highlighting)

    L'\u00e9diteur de code colorise les mots-cl\u00e9s du langage de programmation que vous utilisez, les parenth\u00e8ses, les erreurs. Cela permet de mieux visualiser la structure du code.

    Correspondance des parenth\u00e8ses (brace matching)

    L'\u00e9diteur de code vous permet de voir la correspondance des parenth\u00e8ses, accolades, crochets, etc. Cela permet de voir en un tournemain si vous avez oubli\u00e9 une parenth\u00e8se fermante.

    Indentation automatique (auto-indent)

    L'\u00e9diteur de code vous permet d'indenter automatiquement votre code. Cela permet de voir la structure du code. Il est consensuellement admis qu'une r\u00e9gion de code s\u00e9lectionn\u00e9e peut \u00eatre indent\u00e9e avec Tab et d\u00e9sindent\u00e9e avec Shift+Tab.

    Repli de code (code folding)

    L'\u00e9diteur de code vous permet de replier le code. En cliquant sur une petite fl\u00e8che \u00e0 gauche du code, vous pouvez regrouper les \u00e9l\u00e9ments hi\u00e9rarchiques de votre code pour mieux visualiser la structure.

    Structure du code (outline)

    L'\u00e9diteur de code vous permet de voir dans une fen\u00eatre s\u00e9par\u00e9e les \u00e9l\u00e9ments cl\u00e9 de votre programme. Cela permet de naviguer plus rapidement.

    Navigation hi\u00e9rarchique (go to definition)

    L'\u00e9diteur de code vous permet de naviguer rapidement entre diff\u00e9rents fichiers. En cliquant sur un mot-cl\u00e9, vous pouvez vous rendre \u00e0 la d\u00e9finition de ce mot-cl\u00e9 situ\u00e9e ailleurs dans un projet. Habituellement Alt+Left vous permet de revenir en arri\u00e8re l\u00e0 o\u00f9 vous \u00e9tiez.

    Expressions r\u00e9guli\u00e8res (regular expressions)

    L'\u00e9diteur de code vous permet de rechercher ou remplacer des \u00e9l\u00e9ments en utilisant des expressions r\u00e9guli\u00e8res. Par exemple, si vous voulez inverser l'ordre des mots \u00e9crits, vous activez le mode regex (\u25aa\u20f0 dans vscode). Vous pouvez alors utiliser l'expression r\u00e9guli\u00e8re suivante\u2009:

    /(M.|Mme.)\\s+([^ ]+)\\s+([^ ]+)/\\1 \\3 \\2/\n\nQui permet d'inverser le pr\u00e9nom et le nom.\n\nM. Yves Chevallier` --> M. Chevallier Yves\n
    Multicurseurs (multi-cursor)

    L'\u00e9diteur de code vous permet de placer plusieurs curseurs dans votre code. Cela permet de modifier plusieurs lignes ou mots en m\u00eame temps. Dans vscode vous pouvez ajouter un curseur avec la touche Alt. Vous pouvez aussi s\u00e9lectionner le prochain mot identique avec Ctrl+D.

    Compl\u00e9tion automatique (auto-completion)

    L'\u00e9diteur de code vous permet de compl\u00e9ter automatiquement le code en utilisant la touche Tab. Il utilise une technologie nomm\u00e9e IntelliSense qui, ayant connaissance des mots-cl\u00e9s du langage de programmation et de ce que vous avez d\u00e9j\u00e0 \u00e9crit, vous propose les possibilit\u00e9s de compl\u00e9tion.

    Intelligence artificielle (AI)

    L'\u00e9diteur de code vous permet de compl\u00e9ter automatiquement le code en utilisant une intelligence artificielle comme GitHub Copilot. Cette technologie propose des suggestions de code en fonction de ce que vous avez d\u00e9j\u00e0 \u00e9crit bas\u00e9 sur des millions programmes open-source disponibles sur internet.

    Gestion d'extensions (extensions)

    L'\u00e9diteur de code vous permet d'ajouter des extensions permettant d'ajouter des fonctionnalit\u00e9s \u00e0 votre \u00e9diteur de code tel que l'extension Vim ou Emacs, celle de GitHub Copilot, ou encore celle pour d\u00e9velopper en langage C.

    Int\u00e9gration du terminal (terminal integration)

    L'\u00e9diteur de code vous permet d'int\u00e9grer un terminal (TTY) dans votre \u00e9diteur de code pour lancer directement des commandes. Cela permet d'ex\u00e9cuter votre programme dans la m\u00eame interface et r\u00e9cup\u00e9rer les r\u00e9sultats produits.

    Gestion de version (git integration)

    L'\u00e9diteur de code vous permet d'int\u00e9grer Git, l'outil dominant pour g\u00e9rer les diff\u00e9rentes versions de votre programme.

    ", "tags": ["Vim", "Emacs"]}, {"location": "course-c/05-introduction/me-and-my-computer/#compilateur", "title": "Compilateur", "text": "

    Un compilateur est un programme qui permet de transformer un programme \u00e9crit dans un langage de programmation en un programme ex\u00e9cutable. Il existe de nombreux compilateurs, et chaque langage de programmation a son propre compilateur pour autant qu'il ne soit pas interpr\u00e9t\u00e9 (comme Python, Ruby, JavaScript, etc.).

    Parmi quelques compilateurs populaires, on peut citer\u2009:

    GCC

    Un compilateur open-source utilis\u00e9 sous Linux et macOS. Il est sous licence GPL.

    CLANG

    Un compilateur open-source gagnant en popularit\u00e9, une alternative \u00e0 GCC. Il est sous licence Apache et utilise la biblioth\u00e8que LLVM.

    IAR

    Un compilateur propri\u00e9taire assez on\u00e9reux utilis\u00e9 pour les syst\u00e8mes m\u00e9dicaux, ou les syst\u00e8mes embarqu\u00e9s critiques.

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#ide", "title": "IDE", "text": "

    Un IDE est un Integrated Development Environment, c'est un environnement de d\u00e9veloppement int\u00e9gr\u00e9. C'est un programme qui vous permet d'\u00e9crire du code, de le compiler, de le d\u00e9boguer, de le tester, de le d\u00e9ployer, etc.

    Tous les \u00e9diteurs ne sont pas des IDE, mais tous les IDE sont des \u00e9diteurs. En fin de compte, un IDE est un \u00e9diteur qui poss\u00e8de des fonctionnalit\u00e9s suppl\u00e9mentaires telles que\u2009:

    • un compilateur pour g\u00e9n\u00e9rer un programme ex\u00e9cutable\u2009;
    • un d\u00e9bogueur avec des points d'arr\u00eat pour ex\u00e9cuter le programme ligne par ligne\u2009;
    • une gestion de param\u00e8tres par projet pour compiler le programme avec des options sp\u00e9cifiques\u2009;
    • une gestion de d\u00e9pendances logicielles pour inclure des ressources externes d\u00e9velopp\u00e9es par d'autres d\u00e9veloppeurs\u2009;
    • une gestion de versions pour suivre l'\u00e9volution du code et collaborer avec d'autres d\u00e9veloppeurs.

    La figure suivante illustre les relations entre les diff\u00e9rents outils que nous avons \u00e9voqu\u00e9s jusqu'\u00e0 pr\u00e9sent.

    Repr\u00e9sentation graphique des notions de compilateur, IDE, toolchain...

    Parmi les plus connus on peut citer IntelliJ IDEA, Eclipse, Visual Studio, Visual Studio Code, Xcode, etc.

    On notera que l'ensemble des outils n\u00e9cessaires \u00e0 cr\u00e9er un logiciel ex\u00e9cutable est appel\u00e9 cha\u00eene de compilation, plus commun\u00e9ment appel\u00e9e toolchain. Cette derni\u00e8re est commun\u00e9ment associ\u00e9e \u00e0 une SDK (Software Development Kit), un ensemble d'outils logiciels permettant de d\u00e9velopper des logiciels pour une cible donn\u00e9e (microcontr\u00f4leur, Raspberry Pi, smartphones, etc.).

    "}, {"location": "course-c/05-introduction/me-and-my-computer/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Norme

    est la norme respect\u00e9e par la plupart syst\u00e8mes d'exploitation modernes sauf Windows\u2009? Elle unifie les syst\u00e8mes d'exploitation en d\u00e9finissant une interface standardis\u00e9e pour les programmes.

    Solution

    La norme POSIX (Portable Operating System Interface) est une norme qui d\u00e9finit une interface standardis\u00e9e pour les syst\u00e8mes d'exploitation. Elle est respect\u00e9e en grande partie par Unix, Linux, Solaris, BSD, macOS, Android, QNX, Cygwin, Haiku, VxWorks, RTEMS, etc.

    H\u00e9las, Windows ne respecte pas cette norme ce qui le positionne en marge des autres syst\u00e8mes d'exploitation.

    Submit

    Exercice 2\u2009: Eclipse

    Un ami vous parle d'un outil utilis\u00e9 pour le d\u00e9veloppement logiciel nomm\u00e9 Eclipse. De quel type d'outil s'agit-il\u2009?

    Solution

    Eclipse est un IDE. Il n'int\u00e8gre donc pas de cha\u00eene de compilation et donc aucun compilateur.

    Exercice 3\u2009: Stack Overflow

    Combien y a-t-il eu de questions pos\u00e9es en C sur le site Stack Overflow\u2009?

    Solution

    Il suffit pour cela de se rendre sur le site de Stackoverflow et d'acc\u00e9der \u00e0 la liste des tags. En 2019/07 il y eut 307'669 questions pos\u00e9es.

    Seriez-vous capable de r\u00e9pondre \u00e0 une question pos\u00e9e\u2009?

    Exercice 4\u2009: Quel syst\u00e8me d'exploitation\u2009?

    Quel syst\u00e8me d'exploitation doit-on utiliser pour ex\u00e9cuter un programme \u00e9crit en C\u2009?

    • Windows
    • Linux
    • macOS
    • N'imorte lequel

    Exercice 5\u2009: Copilot

    Qu'est-ce que Copilot\u2009?

    • Une intelligence artificielle
    • Un \u00e9diteur de code
    • Un compilateur
    • Un IDE

    Exercice 6\u2009: POSIX sous Windows\u2009?

    Si je souhaite pouvoir d\u00e9velopper des programmes en C sous Windows compatibles avec la norme POSIX, que dois-je faire\u2009?

    • Rien, Windows est compatible POSIX
    • Installer un sous-syst\u00e8me Linux comme WSL2
    • Installer un compilateur GCC
    • Installer un IDE
    • Changer de syst\u00e8me d'exploitation
    "}, {"location": "course-c/05-introduction/programming/", "title": "Programmation", "text": "La programmation, c'est l'art d'organiser la complexit\u00e9.E. Dijkstra

    Objectifs

    • D\u00e9finir les concepts d'algorithmique et de programmation.
    • Exemples d'algorithmes.
    • Origine de la programmation.
    • Bref historique de l'informatique.
    • Introduction \u00e0 la machine de Turing.

    La programmation, \u00e9galement appel\u00e9e codage, est l'art subtil et rigoureux de transformer des concepts abstraits en instructions ex\u00e9cutables par une machine. \u00c0 travers cette discipline, le programmeur devient l'architecte d'un univers logique, o\u00f9 chaque ligne de code est une brique ajout\u00e9e \u00e0 une structure plus vaste, guid\u00e9e par un plan pr\u00e9cis\u2009: l'algorithme. Ce dernier, semblable \u00e0 une partition musicale, dicte la succession des op\u00e9rations que la machine, fid\u00e8le ex\u00e9cutante, doit suivre sans faillir.

    L'essence m\u00eame de la programmation r\u00e9side donc dans la traduction de ces algorithmes en un langage formel, une sorte de langage commun, \u00e9pur\u00e9 et sans ambigu\u00eft\u00e9, o\u00f9 l'esprit humain et le processeur se rencontrent. Cette activit\u00e9, \u00e0 la crois\u00e9e des chemins entre la science, l'ing\u00e9nierie et l'art, est avant tout une qu\u00eate de pr\u00e9cision, d'efficacit\u00e9, et d'\u00e9l\u00e9gance.

    Dans le cadre d'un enseignement acad\u00e9mique, on parle souvent de cours d'Algorithmique et de Programmation, soulignant ainsi la dualit\u00e9 indissociable entre la conception d'une solution (l'algorithme) et sa mise en \u0153uvre concr\u00e8te (la programmation). Ces deux notions, bien que distinctes, s'entrelacent pour former le c\u0153ur battant de l'informatique, o\u00f9 l'abstraction des id\u00e9es prend forme dans la rigueur du code. C'est \u00e0 ce croisement que nous allons maintenant nous attarder, pour \u00e9claircir ces concepts et en d\u00e9voiler toute la richesse.

    L'un des premiers ordinateurs\u2009: l'Eniac

    "}, {"location": "course-c/05-introduction/programming/#algorithmique", "title": "Algorithmique", "text": "

    L'algorithmique, et non l'algorithmie (qui n'a pas sa place dans la langue fran\u00e7aise), est la science qui se consacre \u00e0 l'\u00e9laboration des r\u00e8gles et techniques r\u00e9gissant la cr\u00e9ation et la conception des algorithmes. Ce domaine, que nous explorerons plus en d\u00e9tail dans le chapitre d\u00e9di\u00e9 aux algorithmes et \u00e0 leur conception, d\u00e9passe largement le cadre de l'informatique. L'algorithmique ne se cantonne pas aux ordinateurs\u2009; elle est omnipr\u00e9sente dans notre quotidien, se manifestant dans des contextes aussi vari\u00e9s que\u2009:

    • l'art de concocter une recette de cuisine,
    • la ma\u00eetrise du tissage des tapis persans,
    • la r\u00e9solution de casse-t\u00eate comme le Rubik's Cube,
    • l'\u00e9laboration de tactiques sportives,
    • ou encore dans les m\u00e9andres des proc\u00e9dures administratives.

    Ainsi, l'algorithmique n'est rien de moins que l'essence m\u00eame de la pens\u00e9e organis\u00e9e, une discipline qui transcende les fronti\u00e8res du num\u00e9rique pour s'infiltrer dans les moindres recoins de la vie courante, l\u00e0 o\u00f9 la logique et la m\u00e9thode s'imposent comme les guides naturels de toute action efficace.

    "}, {"location": "course-c/05-introduction/programming/#algorithme-deuclide", "title": "Algorithme d'Euclide", "text": "

    Dans le cadre math\u00e9matique et scientifique qui nous occupe, l'algorithme d'Euclide, datant probablement de 300 av. J.-C., constitue un exemple embl\u00e9matique. Cet algorithme, d'une \u00e9l\u00e9gance intemporelle, permet de d\u00e9terminer le plus grand commun diviseur (PGCD) de deux nombres. Sa logique, simple, mais puissante, se pr\u00eate parfaitement \u00e0 une repr\u00e9sentation sous forme de diagramme de flux comme repr\u00e9sent\u00e9 sur cette figure\u2009:

    Algorithme de calcul du PGCD d'Euclide

    Les informaticiens et ing\u00e9nieurs appr\u00e9cient particuli\u00e8rement l'usage des diagrammes pour synth\u00e9tiser et clarifier leurs id\u00e9es complexes. Le diagramme de flux, en tant qu'outil de communication visuelle, permet de repr\u00e9senter un processus de mani\u00e8re structur\u00e9e et accessible. Dans ce type de diagramme, les formes g\u00e9om\u00e9triques symbolisent des \u00e9tapes du processus, tandis que les fl\u00e8ches en indiquent le d\u00e9roulement. Par convention, les formes ovales marquent le d\u00e9but et la fin du processus, les rectangles d\u00e9signent les op\u00e9rations de traitement, et les losanges repr\u00e9sentent les d\u00e9cisions \u00e0 prendre. Une forme de d\u00e9cision pose une question et offre deux chemins possibles, chacun correspondant \u00e0 une r\u00e9ponse sp\u00e9cifique. Comme nous le verrons plus tard, tout processus de traitement d'information comporte n\u00e9cessairement une entr\u00e9e et une sortie, illustrant ainsi la dynamique intrins\u00e8que de l'algorithme.

    Ainsi, l'algorithme d'Euclide, par sa simplicit\u00e9 de conception et sa pertinence universelle, demeure un exemple parfait de la mani\u00e8re dont les id\u00e9es abstraites peuvent \u00eatre traduites en instructions claires, tant pour l'esprit que pour la machine.

    Si l'on d\u00e9sire d\u00e9terminer le plus grand commun diviseur de 42 et 30, il suffit de suivre pas \u00e0 pas l'algorithme d'Euclide, depuis le d\u00e9but jusqu'\u00e0 la conclusion comme le montre le tableau ci-dessous\u2009:

    Exemple de calcul du PGCD entre 42 et 30 \u00c9tape \\(a\\) \\(b\\) \\(r\\) Prendre deux entiers naturels \\(a\\) et \\(b\\) 42 30 non d\u00e9fini Est-ce que \\(b\\) est nul\u2009? non\u2009! 42 30 non d\u00e9fini Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 42 30 12 Remplacer \\(a\\) par \\(b\\) 30 30 12 Remplacer \\(b\\) par \\(r\\) 30 12 12 Est-ce que \\(b\\) est nul\u2009? non\u2009! 30 12 12 Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 30 12 6 Remplacer \\(a\\) par \\(b\\) 12 12 6 Remplacer \\(b\\) par \\(r\\) 12 6 6 Est-ce que \\(b\\) est nul\u2009? non\u2009! 12 6 6 Calculer le reste de la division euclidienne de \\(a\\) par \\(b\\) 12 6 0 Remplacer \\(a\\) par \\(b\\) 6 6 0 Remplacer \\(b\\) par \\(r\\) 6 0 0 Est-ce que \\(b\\) est nul\u2009? oui\u2009! 6 0 0 Le PGCD de 42 et 30 est 6 6 0 0

    Exercice 1\u2009: Algorithme d'Euclide

    Appliquer l'algorithme d'Euclide aux entr\u00e9es \\(a\\) et \\(b\\) suivantes.

    Que vaut \\(a, b\\) et \\(r\\) \u00e0 la fin de l'algorithme, et quel est le plus grand diviseur commun\u2009?

    \\[a = 1260, b = 630\\]"}, {"location": "course-c/05-introduction/programming/#tri-a-bulles", "title": "Tri \u00e0 bulles", "text": "

    Un autre algorithme c\u00e9l\u00e8bre est celui du tri \u00e0 bulles, un proc\u00e9d\u00e9 de tri simple qui consiste \u00e0 comparer les \u00e9l\u00e9ments adjacents et \u00e0 les \u00e9changer si n\u00e9cessaire afin de les organiser dans l'ordre souhait\u00e9.

    Pour mieux l'illustrer, imaginez que vous avez un jeu de 54 cartes m\u00e9lang\u00e9 et que vous souhaitez le trier par ordre croissant (As, 2, 3, ..., 10, Valet, Dame, Roi). Vous disposez les cartes en ligne et proc\u00e9dez par \u00e9changes successifs de deux cartes adjacentes mal plac\u00e9es, r\u00e9p\u00e9tant l'op\u00e9ration jusqu'\u00e0 ce que l'ensemble du jeu soit correctement ordonn\u00e9.

    Voici un diagramme de flux repr\u00e9sentant l'algorithme du tri \u00e0 bulles\u2009:

    Algorithme de tri \u00e0 bulles.

    Soit un tableau de \\(N = 5\\) valeurs \u00e0 trier donn\u00e9 ci-dessous, le cycle se r\u00e9p\u00e8te jusqu'\u00e0 ce que le tableau soit compl\u00e8tement tri\u00e9. Si \\(s\\) est \u00e9gal \u00e0 0, il n'y a pas eu d'\u00e9change lors du parcours du tableau et le tableau est donc tri\u00e9.

    \\[T = {5, 3, 8, 4, 2}\\]

    Les diff\u00e9rentes \u00e9tapes du tri \u00e0 bulles sont illustr\u00e9es ci-dessous\u2009:

    \u00c9tape par \u00e9tape du tri \u00e0 bulles.

    Pour les cycles \\(3\\) et \\(4\\), nous ne montrons pas les \u00e9tapes ou il n'y a pas eu d'\u00e9change. Au cinqui\u00e8me cycle, aucun \u00e9change n'est n\u00e9cessaire, l'algorithme se termine.

    On peut compter le nombre de cycles assez facilement. Pour ce tableau de \\(N = 5\\) valeurs, il y a \\(5\\) cycles. Durant un cycle, il faut regarder \\(N - 1\\) paires d'\u00e9l\u00e9ments. Donc pour un tableau de \\(N\\) valeurs, il y a \\(N^2 - N\\) comparaisons. Ce type d'algorithme est dit de complexit\u00e9 \\(O(N^2)\\). Cela signifie que le nombre d'op\u00e9rations \u00e0 effectuer est proportionnel au carr\u00e9 du nombre d'\u00e9l\u00e9ments \u00e0 trier. Nous verrons plus tard que la complexit\u00e9 d'un algorithme est un crit\u00e8re important. Nous verrons comment le calculer.

    "}, {"location": "course-c/05-introduction/programming/#conclusion", "title": "Conclusion", "text": "

    Les algorithmes se d\u00e9clinent en une multitude de formes, des plus simples aux plus complexes, et trouvent leur utilit\u00e9 dans des domaines aussi vari\u00e9s que la cryptographie, la bio-informatique, la finance ou encore la robotique.

    En tant que d\u00e9veloppeur, vous serez souvent amen\u00e9 \u00e0 concevoir des algorithmes pour r\u00e9soudre divers probl\u00e8mes. Avant de plonger t\u00eate baiss\u00e9e dans l'\u00e9criture du code, il est essentiel de prendre un moment pour r\u00e9fl\u00e9chir pos\u00e9ment. Sortez une feuille de papier, un crayon, et laissez vos neurones travailler. Comprendre profond\u00e9ment le probl\u00e8me est une \u00e9tape cruciale, souvent n\u00e9glig\u00e9e par les jeunes d\u00e9veloppeurs qui, press\u00e9s de passer \u00e0 l'action, se jettent dans le code sans plan pr\u00e9cis, \u00ab\u2009touillant\u2009\u00bb leur syntaxe \u00e0 la vaudoise \u00e0 la recherche d'une solution miraculeuse qui na\u00eetrait du hasard. Prenez le temps de m\u00fbrir votre r\u00e9flexion, imaginez des exemples concrets, testez vos hypoth\u00e8ses, et vous d\u00e9couvrirez que la programmation, loin d'\u00eatre un combat, peut devenir un jeu d'enfant, empreint de logique et de clart\u00e9.

    "}, {"location": "course-c/05-introduction/programming/#programmation_1", "title": "Programmation", "text": "

    Parlons couture\u2009! La machine Jacquard est un m\u00e9tier \u00e0 tisser mis au point par Joseph Marie Jacquard en 1801. Il constitue le premier syst\u00e8me m\u00e9canique programmable avec cartes perfor\u00e9es.

    M\u00e9canisme Jacquard au Mus\u00e9e des arts et m\u00e9tiers de Paris.

    Les cartes perfor\u00e9es, ici des rouleaux de papier, contiennent donc la suite des actions guidant les crochets permettant de tisser des motifs complexes. Elles sont donc le programme de la machine et dont le format (largeur, dimension des trous, etc.) est sp\u00e9cifique \u00e0 la machine. En termes informatiques, on dirait que les cartes perfor\u00e9es sont \u00e9crites en langage machine.

    La r\u00e9volte des canuts

    L'av\u00e8nement de la machine Jacquard a r\u00e9volutionn\u00e9 l'industrie textile mais a aussi eu des cons\u00e9quences sociales. L'automatisation d'un travail qui jadis \u00e9tait effectu\u00e9 manuellement causa une vague de ch\u00f4mage menant \u00e0 la R\u00e9volte des canuts en 1831.

    La programmation d\u00e9finit toute activit\u00e9 menant \u00e0 l'\u00e9criture de programmes. En informatique, un programme est d\u00e9fini comme un ensemble ordonn\u00e9 d'instructions cod\u00e9es avec un langage donn\u00e9 et d\u00e9crivant les \u00e9tapes menant \u00e0 la r\u00e9solution d'un probl\u00e8me. Comme nous l'avons vu, il s'agit le plus souvent d'une \u00e9criture formelle d'un algorithme par l'interm\u00e9diaire d'un langage de programmation.

    Les informaticiens-tisserands responsables de la cr\u00e9ation des cartes perfor\u00e9es auraient pu se poser la question de comment simplifier leur travail en cr\u00e9ant un langage formel pour cr\u00e9er des motifs complexes et dont les composants de base se r\u00e9p\u00e8tent d'un travail \u00e0 l'autre. Prenons par exemple un ouvrier sp\u00e9cialis\u00e9 en h\u00e9raldique et devant cr\u00e9er des motifs complexes de blasons.

    Armoiries des ducs de Mayenne

    Nul n'est sans savoir que l'h\u00e9raldique a son langage parfois obscur et celui qui le ma\u00eetrise voudrait par exemple l'utiliser au lieu de manuellement percer les cartes pour chaque point de couture. Ainsi l'anachronique informaticien-tisserand souhaitant tisser le motif des armoiries duc de Mayenne aurait sans doute r\u00e9dig\u00e9 un programme informatique en utilisant sa langue. Le programme aurait pu ressembler \u00e0 ceci\u2009:

    \u00c9cartel\u00e9, en 1 et 4 :\n    coup\u00e9 et parti en 3,\n        au premier fasc\u00e9 de gueules et d'argent,\n        au deuxi\u00e8me d'azur sem\u00e9 de lys d'or\n            et au lambel de gueules,\n        au troisi\u00e8me d'argent \u00e0 la croix potenc\u00e9e d'or,\n            cantonn\u00e9e de quatre croisettes du m\u00eame,\n        au quatri\u00e8me d'or aux quatre pals de gueules,\n        au cinqui\u00e8me d'azur sem\u00e9 de lys d'or\n            et \u00e0 la bordure de gueules,\n        au sixi\u00e8me d'azur au lion contourn\u00e9 d'or,\n            arm\u00e9,\n            lampass\u00e9 et couronn\u00e9 de gueules,\n        au septi\u00e8me d'or au lion de sable,\n            arm\u00e9,\n            lampass\u00e9 de gueules,\n        au huiti\u00e8me d'azur sem\u00e9 de croisettes d'or\n            et aux deux bar d'or.\n    Sur le tout d'or \u00e0 la bande de gueules\n        charg\u00e9 de trois al\u00e9rions d'argent\n    Le tout bris\u00e9 d'un lambel de gueules ;\nEn 2 et 3 contre-\u00e9cartel\u00e9 :\n    en 1 et 4 d'azur,\n        \u00e0 l'aigle d'argent, becqu\u00e9e,\n        langu\u00e9e et couronn\u00e9e d'or et en 2 et 3 d'azur,\n        \u00e0 trois fleurs de lys d'or,\n        \u00e0 la bordure endent\u00e9e de gueules et d'or.\n

    Tout l'art est de pouvoir traduire ce texte compr\u00e9hensible par tout h\u00e9raldiste en un programme en langage machine compr\u00e9hensible par un m\u00e9tier \u00e0 tisser. Cette traduction est le r\u00f4le du compilateur que nous verrons plus tard. Quant au texte, et bien qu'il nous vient tout droit du moyen-\u00e2ge, il partage avec les langages de programmation modernes des caract\u00e9ristiques communes\u2009:

    Lexique

    le texte est compos\u00e9 de mots et de symboles qui ont un sens pr\u00e9cis, les couleurs (\u00e9maux) ont des termes sp\u00e9cifiques (gueules pour le rouge, azur pour le bleu, sable pour le noir, etc.), les figures (meubles) aussi (lys, croix, lion, aigle, etc.).

    Syntaxe

    le texte suit une structure grammaticale pr\u00e9cise, le fond (champ) est toujours mentionn\u00e9 en premier, les figures en second suivi de leurs attributs.

    S\u00e9mantique

    les termes peuvent adopter une certaine morphologie, par exemple le lion peut \u00eatre lampass\u00e9 (langue de couleur diff\u00e9rente), couronn\u00e9 (avec une couronne), arm\u00e9 (avec des griffes et des dents de couleur diff\u00e9rente). Cette s\u00e9mantique implique l'adjonction de pr\u00e9fixes ou de suffixes.

    Grammaire

    le texte est organis\u00e9 en phrases, les phrases sont organis\u00e9es en paragraphes, les paragraphes en sections, les symboles vont \u00eatre interpr\u00e9t\u00e9s en fonction de leur position dans le texte, de leur contexte.

    De gueules

    Notons que gueules signifie rouge. Le drapeau suisse est donc de gueules, \u00e0 la croix al\u00e9s\u00e9e d'argent.

    ", "tags": ["semantique", "morphologie", "syntaxe", "heraldique", "grammaire", "langage-machine", "compilateur", "mayenne-duc-de", "lexique"]}, {"location": "course-c/05-introduction/programming/#langage-de-programmation", "title": "Langage de programmation", "text": "

    Traduire un algorithme en une suite d'ordres compr\u00e9hensibles par une machine est donc le travail du programmeur. Il existe de nombreux langages de programmation, mais la plupart se regroupent en deux cat\u00e9gories\u2009:

    1. Les langages textuels qui utilisent du texte pour d\u00e9crire les instructions.
    2. Les langages visuels qui utilisent des \u00e9l\u00e9ments graphiques pour d\u00e9crire les instructions.

    L'\u00eatre humain a appris depuis des mill\u00e9naires \u00e0 communiquer avec des symboles, il stocke son savoir dans des livres ou \u00e0 une certaine \u00e9poque, sur des tablettes de cire. Au d\u00e9but de l'\u00e8re de l'informatique, l'ordinateur ne pouvait communiquer que par du texte. Les premiers langages de programmation \u00e9taient donc textuels. Avec l'av\u00e8nement des interfaces graphiques, les langages visuels ont vu le jour, mais ils sont davantage r\u00e9serv\u00e9s pour enseigner la programmation aux enfants ou pour faciliter la programmation de robots ou de jeux vid\u00e9os.

    Scratch

    Scratch est un langage de programmation visuel d\u00e9velopp\u00e9 par le MIT. Il est utilis\u00e9 pour enseigner les bases de la programmation aux enfants. Il permet de cr\u00e9er des animations, des jeux et des histoires interactives.

    Interface de scratch

    LabView

    LabView est un langage de programmation visuel d\u00e9velopp\u00e9 par National Instruments. Il est utilis\u00e9 pour la programmation de syst\u00e8mes de mesure et de contr\u00f4le. Il est tr\u00e8s utilis\u00e9 dans l'industrie et la recherche.

    Interface de LabView

    Son interface est compos\u00e9e de blocs graphiques que l'on relie entre eux pour cr\u00e9er un programme.

    Common Lisp

    Common Lisp est un langage de programmation invent\u00e9 en 1984. C'est un langage de programmation textuel de type fonctionnel. Voici un exemple de programme en Common Lisp pour r\u00e9soudre le probl\u00e8me des tours de Hano\u00ef :

    (defun hanoi (n source target auxiliary)\n  (when (> n 0)\n    (hanoi (- n 1) source auxiliary target)\n    (format t \"~%Move disk from ~A to ~A\" source target)\n    (hanoi (- n 1) auxiliary target source)))\n\n(defun solve-hanoi (n)\n  (hanoi n 'A 'C 'B))\n\n(solve-hanoi 3)\n

    Pour ce cours, et pour l'enseignement de la programmation en g\u00e9n\u00e9ral, nous utiliserons des langages textuels.

    ", "tags": ["lisp", "scratch", "labview"]}, {"location": "course-c/05-introduction/programming/#calculateur", "title": "Calculateur", "text": "

    Un calculateur du latin calculare: calculer avec des cailloux, originellement appel\u00e9s abaque, \u00e9tait un dispositif permettant de faciliter les calculs math\u00e9matiques.

    Les os d'Ishango dat\u00e9s de 20'000 ans sont des art\u00e9facts arch\u00e9ologiques attestant la pratique de l'arithm\u00e9tique dans l'histoire de l'humanit\u00e9.

    Os d'Ishango

    Si les anglophones ont d\u00e9tourn\u00e9 le verbe compute (calculer) en un nom computer, un ordinateur est g\u00e9n\u00e9ralement plus qu'un simple calculateur, car m\u00eame une calculatrice de poche doit g\u00e9rer en plus des calculs un certain nombre de p\u00e9riph\u00e9riques comme\u2009:

    • l'interface de saisie (pav\u00e9 num\u00e9rique);
    • l'affichage du r\u00e9sultat (\u00e9cran \u00e0 cristaux liquides).

    Notons qu'\u00e0 l'instar de notre diagramme de flux, un calculateur dispose aussi d'une entr\u00e9e, d'une sortie et d'\u00e9tats internes.

    ", "tags": ["calculateur"]}, {"location": "course-c/05-introduction/programming/#ordinateur", "title": "Ordinateur", "text": "

    Le terme ordinateur est tr\u00e8s r\u00e9cent, il daterait de 1955, cr\u00e9\u00e9 par Jacques Perret \u00e0 la demande d'IBM France (c.f. 2014\u2009: 100 ans d'IBM en France). Voici la lettre de Jacques Perret \u00e0 IBM France\u2009:

    \u00ab Le 16 IV 1955, Cher Monsieur,

    Que diriez-vous d\u2019ordinateur? C\u2019est un mot correctement form\u00e9, qui se trouve m\u00eame dans le Littr\u00e9 comme adjectif d\u00e9signant Dieu qui met de l\u2019ordre dans le monde. Un mot de ce genre a l\u2019avantage de donner ais\u00e9ment un verbe ordiner, un nom d\u2019action ordination. L\u2019inconv\u00e9nient est que ordination d\u00e9signe une c\u00e9r\u00e9monie religieuse\u2009; mais les deux champs de signification (religion et comptabilit\u00e9) sont si \u00e9loign\u00e9s et la c\u00e9r\u00e9monie d\u2019ordination connue, je crois, de si peu de personnes que l\u2019inconv\u00e9nient est peut-\u00eatre mineur. D\u2019ailleurs votre machine serait ordinateur (et non-ordination) et ce mot est tout \u00e0 fait sorti de l\u2019usage th\u00e9ologique. Syst\u00e9mateur serait un n\u00e9ologisme, mais qui ne me para\u00eet pas offensant\u2009; il permet syst\u00e9matis\u00e9\u2009; \u2014 mais syst\u00e8me ne me semble gu\u00e8re utilisable \u2014 Combinateur a l\u2019inconv\u00e9nient du sens p\u00e9joratif de combine\u2009; combiner est usuel donc peu capable de devenir technique\u2009; combination ne me para\u00eet gu\u00e8re viable \u00e0 cause de la proximit\u00e9 de combinaison. Mais les Allemands ont bien leurs combinats (sorte de trusts, je crois), si bien que le mot aurait peut-\u00eatre des possibilit\u00e9s autres que celles qu\u2019\u00e9voque combine.

    Congesteur, digesteur \u00e9voquent trop congestion et digestion. Synth\u00e9tiseur ne me para\u00eet pas un mot assez neuf pour d\u00e9signer un objet sp\u00e9cifique, d\u00e9termin\u00e9 comme votre machine.

    En relisant les brochures que vous m\u2019avez donn\u00e9es, je vois que plusieurs de vos appareils sont d\u00e9sign\u00e9s par des noms d\u2019agent f\u00e9minins (trieuse, tabulatrice). Ordinatrice serait parfaitement possible et aurait m\u00eame l\u2019avantage de s\u00e9parer plus encore votre machine du vocabulaire de la th\u00e9ologie. Il y a possibilit\u00e9 aussi d\u2019ajouter \u00e0 un nom d\u2019agent un compl\u00e9ment\u2009: ordinatrice d\u2019\u00e9l\u00e9ments complexes ou un \u00e9l\u00e9ment de composition, par exemple\u2009: s\u00e9lecto-syst\u00e9mateur. S\u00e9lecto-ordinateur a l\u2019inconv\u00e9nient de deux o en hiatus, comme \u00e9lectro-ordonnatrice.

    Il me semble que je pencherais pour ordonnatrice \u00e9lectronique. Je souhaite que ces suggestions stimulent, orientent vos propres facult\u00e9s d\u2019invention. N\u2019h\u00e9sitez pas \u00e0 me donner un coup de t\u00e9l\u00e9phone si vous avez une id\u00e9e qui vous paraisse requ\u00e9rir l\u2019avis d\u2019un philologue.

    V\u00f4tre, Jacques Perret \u00bb

    ", "tags": ["jacques-perret", "ordinateur"]}, {"location": "course-c/05-introduction/programming/#la-machine-de-turing", "title": "La machine de Turing", "text": "

    Il est impossible d'introduire les notions d'ordinateur, de programmes et d'algorithmes sans \u00e9voquer la figure embl\u00e9matique d'Alan Turing. Ce math\u00e9maticien britannique, v\u00e9ritable pionnier de l'informatique, a jou\u00e9 un r\u00f4le crucial dans l'histoire, notamment en d\u00e9chiffrant le code de la machine Enigma utilis\u00e9e par les forces allemandes pendant la Seconde Guerre mondiale.

    La machine de Turing est un mod\u00e8le th\u00e9orique fondamental qui repr\u00e9sente la conception d'un ordinateur. Imagin\u00e9e comme une bande infinie divis\u00e9e en cases, elle est dot\u00e9e d'une t\u00eate de lecture/\u00e9criture et d'un ensemble fini d'\u00e9tats. Cette machine peut lire et \u00e9crire des symboles sur la bande, se d\u00e9placer \u00e0 gauche ou \u00e0 droite, et changer d'\u00e9tat en fonction des instructions re\u00e7ues. Capable de simuler n'importe quel algorithme, la machine de Turing est un mod\u00e8le abstrait qui a permis de d\u00e9finir la notion de calculabilit\u00e9 et de poser les bases de l'informatique th\u00e9orique.

    Lorsqu'on parle d'un ordinateur Turing-complet, on fait r\u00e9f\u00e9rence \u00e0 un dispositif capable de simuler n'importe quel algorithme, condition sine qua non pour les ordinateurs modernes. Ces machines se composent d'un programme et d'une m\u00e9moire\u2009: le programme, une suite d'instructions pr\u00e9cises, est ex\u00e9cut\u00e9 par le processeur, tandis que la m\u00e9moire sert d'espace de stockage pour les donn\u00e9es et les instructions.

    Prenons l'exemple d'un programme visant \u00e0 ajouter 1 \u00e0 un nombre n en binaire. L'algorithme correspondant pourrait \u00eatre d\u00e9crit ainsi\u2009:

    Algorithme d'addition binaire

    On commence par l'\u00e9tat de gauche, on lit un symbole sur la bande. Tant que ce symbole est 0 ou 1 on avance \u00e0 droite. Lorsque l'on rencontre une case vide, on se d\u00e9place \u00e0 gauche et on entre dans le second \u00e9tat. Tant qu\u2019on lit un 1, on le remplace par un 0 et on avance \u00e0 gauche. Lorsqu\u2019on lit un 0 ou une case vide, on le remplace par un 1 et on se d\u00e9place \u00e0 gauche. On revient \u00e0 l'\u00e9tat initial et on continue jusqu'\u00e0 ce que l'on rencontre une case vide.

    Sur la figure ci-dessous, on peut voir l'ex\u00e9cution de l'algorithme sur une bande apr\u00e8s chaque \u00e9tape. La case centrale est celle sous la t\u00eate de lecture/\u00e9criture. On voit bien qu'au d\u00e9but on a le nombre 101 (5) et \u00e0 la fin on obtient le nombre 110 (6). L'algorithme a bien fonctionn\u00e9.

    Ex\u00e9cution de l'algorithme sur une bande

    On peut essayer de traduire cet algorithme dans un langage formel\u2009:

    Pseudo codeLangage formel de TuringC
    d\u00e9but:\n    lire symbole\n    si symbole = 0 ou 1 alors\n        avancer \u00e0 droite\n        aller \u00e0 d\u00e9but\n    sinon si symbole = vide alors\n        se d\u00e9placer \u00e0 gauche\n        aller retenue\nretenue:\n    lire symbole\n    si symbole = 1 alors\n        \u00e9crire 0\n        se d\u00e9placer \u00e0 gauche\n        aller \u00e0 retenue\n    sinon si symbole = 0 ou vide alors\n        \u00e9crire 1\n        se d\u00e9placer \u00e0 gauche\n
    input: '101'\ntable:\n  right:\n    [1,0]: R\n    ' '  : {L: carry}\n  carry:\n    1      : {write: 0, L}\n    [0,' ']: {write: 1, L: done}\ndone:\n
    #include <stdio.h>\n#include <string.h>\n\n#define BAND_SIZE 1000\n\nint main() {\n    char tape[BAND_SIZE] = {0};\n    int head = BAND_SIZE / 2; // Position au milieu de la bande\n\n    scanf(\"%s\", tape + head); // Saisie du nombre d'entr\u00e9e\n\n    // Algorithme d'addition\n    char c = tape[head];\n    while (c == '0' || c == '1')\n        c = tape[++head];\n    c =  tape[--head];\n    while (c == '1') {\n        tape[head--] = '0';\n        c = tape[head];\n    }\n    tape[head] = '1';\n\n    // Recherche de la position du premier symbole non nul\n    while (tape[head]) head--;\n    head++;\n    printf(\"%s\\n\", tape + head);\n}\n

    "}, {"location": "course-c/05-introduction/programming/#lordinateur-dantan", "title": "L'ordinateur d'antan", "text": "

    T\u00e9l\u00e9scripteur Siemens T100

    Le t\u00e9l\u00e9scripteur Siemens T100 est un exemple d'ordinateur des ann\u00e9es 1960. Il \u00e9tait utilis\u00e9 pour la transmission de messages t\u00e9l\u00e9graphiques. Il \u00e9tait compos\u00e9 d'un clavier et d'une imprimante. Il \u00e9tait capable de lire et d'\u00e9crire des messages sur une bande de papier. Il \u00e9tait programm\u00e9 en utilisant des cartes perfor\u00e9es.

    On les appelait aussi t\u00e9l\u00e9type ou abr\u00e9g\u00e9 TTY. Ce terme est rest\u00e9 aujourd'hui pour d\u00e9signer une console de terminal.

    "}, {"location": "course-c/05-introduction/programming/#lordinateur-moderne", "title": "L'ordinateur moderne", "text": "

    Les ordinateurs modernes sont des machines complexes qui contiennent plusieurs composants. Les composants principaux d'un ordinateur sont\u2009:

    Le processeur (CPU)

    c'est le cerveau de l'ordinateur. Il ex\u00e9cute les ordres du programme.

    La m\u00e9moire (RAM)

    c'est l'espace de stockage temporaire des donn\u00e9es et des instructions du programme.

    Le disque dur (HDD/SSD)

    c'est l'espace de stockage permanent des donn\u00e9es.

    Les p\u00e9riph\u00e9riques d'entr\u00e9e/sortie

    ce sont les interfaces qui permettent \u00e0 l'ordinateur de communiquer avec l'utilisateur (clavier, souris, \u00e9cran, imprimante, etc.).

    Contrairement \u00e0 la machine de Turing, les ordinateurs sont \u00e9quip\u00e9s d'une m\u00e9moire \u00e0 acc\u00e8s al\u00e9atoire qui permet d'acc\u00e9der n'importe quel \u00e9l\u00e9ment de la m\u00e9moire sans avoir \u00e0 parcourir toute la bande. \u00c9galement, ces ordinateurs disposent d'un processeur capable de calculer des op\u00e9rations arithm\u00e9tiques et logiques en un temps tr\u00e8s court. Ces processeurs peuvent m\u00eame calculer des fonctions trigonom\u00e9triques, exponentielles et logarithmiques facilement. En reprenant notre programme d'addition binaire, il est beaucoup plus facile de l'\u00e9crire en C\u2009:

    #include <stdio.h>\nint main() {\n    int n;\n    scanf(\"%d\", &n);\n    printf(\"%d\", n + 1);\n}\n

    N\u00e9anmoins, il est important de comprendre que ce programme est traduit en langage machine par un programme appel\u00e9 compilateur. Une \u00e9tape interm\u00e9diaire est la traduction du programme en langage assembleur. Le langage assembleur est un langage de plus bas niveau qui permet de contr\u00f4ler directement le processeur. Ce sont les instructions primitives du processeur. Le programme ci-dessus sera converti en assembleur X86 comme suit\u2009:

    .LC0:\n  .string \"%d\"\nmain:\n  sub     rsp, 24\n  mov     edi, OFFSET FLAT:.LC0\n  xor     eax, eax\n  lea     rsi, [rsp+12]\n  call    scanf\n  mov     eax, DWORD PTR [rsp+12]\n  mov     edi, OFFSET FLAT:.LC0\n  lea     esi, [rax+1]\n  xor     eax, eax\n  call    printf\n  xor     eax, eax\n  add     rsp, 24\n  ret\n

    Ce programme assembleur peut ensuite \u00eatre converti en langage machine binaire qui est le langage compris par le processeur.

    48 83 ec 18\nbf 00 00 00 00\n31 c0\n48 8d 74 24 0c\ne8 00 00 00 00\n8b 44 24 0c\nbf 00 00 00 00\n48 8d 70 01\n31 c0\ne8 00 00 00 00\n31 c0\n48 83 c4 18\nc3\n

    In fine, ce programme sera \u00e9crit en m\u00e9moire avec des 1 et des 0\u2009:

    01001000100000111110110000011000101111110000000000000000000000000000000000110001\n11000000010010001000110101110100001001000000110011101000000000000000000000000000\n00000000100010110100010000100100000011001011111100000000000000000000000000000000\n01001000100011010111000000000001001100011100000011101000000000000000000000000000\n0000000000110001110000000100100010000011110001000001100011000011\n

    "}, {"location": "course-c/05-introduction/programming/#les-systemes-a-microcontroleurs", "title": "Les syst\u00e8mes \u00e0 microcontr\u00f4leurs", "text": "

    Les microcontr\u00f4leurs sont des ordinateurs complets int\u00e9gr\u00e9s dans un seul circuit int\u00e9gr\u00e9. Ils sont omnipr\u00e9sents dans notre vie quotidienne. Que ce soit la t\u00e9l\u00e9vision, le t\u00e9l\u00e9phone portable, les machines \u00e0 caf\u00e9, les voitures, les jouets, les montres ou les appareils \u00e9lectrom\u00e9nagers, ils contiennent tous un ou plusieurs microcontr\u00f4leurs.

    Ces derniers sont aussi programm\u00e9s en impl\u00e9mentant des algorithmes. Le plus souvent ces algorithmes sont \u00e9crits en langage C car c'est un langage de programmation tr\u00e8s proche du langage machine. Les microcontr\u00f4leurs sont souvent utilis\u00e9s pour contr\u00f4ler des syst\u00e8mes en temps r\u00e9el. Ils sont capables de lire des capteurs, de contr\u00f4ler des actionneurs et de communiquer avec d'autres syst\u00e8mes.

    Machine \u00e0 caf\u00e9 Citiz de Nespresso

    Prenons l'exemple de cette machine \u00e0 caf\u00e9. C'est une machine qui co\u00fbte environ 100 CHF. Elle est \u00e9quip\u00e9e d'un microcontr\u00f4leur \u00e0 30 centimes qui contr\u00f4le le chauffage, la pompe \u00e0 eau et les leds. Le microcontr\u00f4leur est programm\u00e9 pour lire les boutons de commande, contr\u00f4ler les actionneurs et afficher des messages \u00e0 l'utilisateur.

    Sch\u00e9ma bloc de la machine \u00e0 caf\u00e9 Citiz

    Derri\u00e8re se cache un programme, bien complexe. Si vous avez une de ces machines mettez l\u00e0 en service, vous verrez que s'il manque de l'eau vous aurez un message d'erreur. Au d\u00e9marrage, les LEDs clignotent le temps que la machine chauffe. Une fois en temp\u00e9rature, vous pouvez l'utiliser. Ce sont des algorithmes qui sont derri\u00e8re tout cela.

    "}, {"location": "course-c/05-introduction/programming/#historique", "title": "Historique", "text": "

    Historique

    Pour mieux se situer dans l'histoire de l'informatique, voici quelques dates cl\u00e9s\u2009:

    87 av. J.-C.

    La machine d'Anticyth\u00e8re consid\u00e9r\u00e9 comme le premier calculateur analogique pour positions astronomiques permettant de pr\u00e9dire des \u00e9clipses. Cette machine encore si myst\u00e9rieuse a inspir\u00e9 de nombreux sc\u00e9narios comme le film Indiana Jones et le Cadran de la destin\u00e9e. Elle a \u00e9t\u00e9 d\u00e9couverte en 1901 dans une \u00e9pave au large de l'\u00eele d'Anticyth\u00e8re. Gr\u00e2ce aux techniques modernes de radiographie, on a pu reconstruire une partie de son m\u00e9canisme.

    1642

    La Pascaline: machine d'arithm\u00e9tique de Blaise Pascal, premi\u00e8re machine \u00e0 calculer. Elle permettait d'effectuer des additions et des soustractions en utilisant des roues dent\u00e9es.

    1801

    M\u00e9tier \u00e0 tisser Jacquard programmable avec des cartes perfor\u00e9es.

    1837

    Machine \u00e0 calculer programmable de Charles Babbage. Charles Babbage est consid\u00e9r\u00e9 comme le p\u00e8re de l'informatique. Il a con\u00e7u la machine analytique qui est consid\u00e9r\u00e9e comme le premier ordinateur programmable. Ada Lovelace, fille de Lord Byron, est consid\u00e9r\u00e9e comme la premi\u00e8re programmeuse de l'histoire.

    1936

    La machine de Turing est un mod\u00e8le th\u00e9orique d'un ordinateur capable de simuler n'importe quel algorithme. Elle a \u00e9t\u00e9 invent\u00e9e par Alan Turing.

    1937

    l'ASCC (Automatic Sequence Controlled Calculator Mark I) d'IBM, le premier grand calculateur num\u00e9rique. Il \u00e9tait constitu\u00e9 de 765'000 pi\u00e8ces, dont des interrupteurs, des relais, des arbres m\u00e9caniques et des embrayages. Les ordres \u00e9taient lus \u00e0 partir d'une bande perfor\u00e9e. Une seconde bande perfor\u00e9e contenait les donn\u00e9es d'entr\u00e9e. Les instructions \u00e9tant simples, pour r\u00e9p\u00e9ter un algorithme en boucle comme l'algorithme d'Euclide, on pouvait typiquement cr\u00e9er une boucle dans la bande perfor\u00e9e.

    • 4500 kg
    • 6 secondes par multiplication \u00e0 23 chiffres d\u00e9cimaux
    • Cartes perfor\u00e9es
    1945

    L'ENIAC, de Presper Eckert et John William Mauchly. C'est le premier ordinateur Turing-complet enti\u00e8rement \u00e9lectronique et fonctionnant avec des diodes et des tubes \u00e0 vide. Il \u00e9tait programm\u00e9 en branchant des c\u00e2bles et en changeant des interrupteurs. Il \u00e9tait utilis\u00e9 pour des calculs balistiques.

    • 160 kW
    • 100 kHz
    • Tubes \u00e0 vide
    • 100'000 additions/seconde
    • 357 multiplications/seconde
    1965

    Premier ordinateur \u00e0 circuits int\u00e9gr\u00e9s, le PDP-8

    • 12 bits
    • m\u00e9moire de 4096 mots
    • Temps de cycle de 1.5 \u00b5s
    • Fortran et BASIC
    2018

    Le Behold Summit est un superordinateur construit par IBM.

    • 200'000'000'000'000'000 multiplications par seconde
    • simple ou double pr\u00e9cision
    • 14.668 GFlops/watt
    • 600 GiB de m\u00e9moire RAM
    2022

    Le Frontier ou OLCF-5 est le premier ordinateur exaflopique du monde.

    • 1,714,810,000,000,000,000 multiplications par seconde (1.1 exaflops)
    • 9472 processeurs Trento \u00e0 64 c\u0153urs de 2 GHz (606 208 c\u0153urs)
    • 37888 processeurs graphiques MI250x (8 335 360 coeurs)
    • 22.7 MW (5 locomotives \u00e9lectriques ou 56'750 foyers europ\u00e9ens)
    • 62.68 GFlops/watt
    "}, {"location": "course-c/05-introduction/programming/#conclusion_1", "title": "Conclusion", "text": "

    Les algorithmes existent depuis fort longtemps et sont utilis\u00e9s dans de nombreux domaines. Ils sont la base de la programmation et de l'informatique.

    Les hommes ont cherch\u00e9 \u00e0 pouvoir automatiser leurs t\u00e2ches, d'abord avec des machines m\u00e9caniques comme le m\u00e9tier \u00e0 tisser Jacquard. Puis, apr\u00e8s l'invention de la micro\u00e9lectronique, il a \u00e9t\u00e9 possible de complexifier ces machines pour en faire des ordinateurs.

    Pour les contr\u00f4ler, les informaticiens \u00e9crivent des programmes qui impl\u00e9mentent des algorithmes. Ces programmes sont ensuite traduits en langage machine par un compilateur.

    Aujourd'hui, les superordinateurs sont capables de r\u00e9aliser des milliards de milliards d'op\u00e9rations par seconde, mais ils sont toujours programm\u00e9s de la m\u00eame mani\u00e8re\u2009: avec du texte.

    "}, {"location": "course-c/05-introduction/programming/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Ordinateur

    Quelle est l'\u00e9tymologie du mot ordinateur ?

    • calculateur
    • ordonnateur
    • syst\u00e9mateur
    • ordiner

    Exercice 3\u2009: Machine de Turing

    Qu'est-ce que la machine de Turing\u2009?

    • Une bombe r\u00e9alis\u00e9e pour casser le code de la machine Enigma.
    • Un mod\u00e8le th\u00e9orique d'un ordinateur capable de simuler n'importe quel algorithme.
    • Le premier ordinateur \u00e9lectronique.
    • Un mod\u00e8le th\u00e9orique d'un ordinateur ne pouvant pas simuler n'importe quel algorithme.

    Exercice 4\u2009: Machine \u00e0 caf\u00e9

    Une machine \u00e0 caf\u00e9 est \u00e9quip\u00e9e d'un , qui est l'organe de contr\u00f4le de la machine. Ce dernier comporte des comme les boutons de commande ou les capteurs ainsi que des comme les LEDs et les actionneurs. Une permet de stocker les param\u00e8tres de configuration de la machine ainsi que son programme.

    Submit

    "}, {"location": "course-c/10-numeration/", "title": "Num\u00e9ration", "text": "

    La num\u00e9ration d\u00e9signe le mode de repr\u00e9sentation des nombres (p. ex. cardinaux, ordinaux), leur base (syst\u00e8me binaire, ternaire, quinaire, d\u00e9cimal ou vic\u00e9simal), ainsi que leur codification comme IEEE 754, compl\u00e9ment \u00e0 un, compl\u00e9ment \u00e0 deux. Bien comprendre les bases de la num\u00e9ration est important pour l'ing\u00e9nieur d\u00e9veloppeur, car il est souvent amen\u00e9 \u00e0 effectuer des op\u00e9rations de bas niveau sur les nombres.

    Ce chapitre n'est strictement essentiel qu'au programmeur de bas niveau, l'\u00e9lectronicien ou l'informaticien technique. Bien comprendre la mani\u00e8re dont les nombres sont repr\u00e9sent\u00e9s dans un ordinateur et de mani\u00e8re plus g\u00e9n\u00e9rale l'information trait\u00e9e par ces machines est tr\u00e8s utile pour \u00e9crire des programmes performants et \u00e9laborer des algorithmes. En somme, la num\u00e9ration permet de mieux se repr\u00e9senter la mani\u00e8re dont l'ordinateur traite les donn\u00e9es au niveau le plus fondamental\u2009: le bit.

    ", "tags": ["complement-a-un", "bit", "numeration", "complement-a-deux"]}, {"location": "course-c/10-numeration/bases/", "title": "Bases", "text": "Il y a 10 types de personnes dans le monde, celles qui comprennent le binaire, et celles qui ne le comprennent pas.M\u00e8me internet

    Une base d\u00e9signe la valeur dont les puissances successives interviennent dans l'\u00e9criture des nombres dans la num\u00e9ration positionnelle, laquelle est un proc\u00e9d\u00e9 par lequel l'\u00e9criture des nombres est compos\u00e9e de chiffres ou symboles reli\u00e9s \u00e0 leur position voisine par un multiplicateur, appel\u00e9 base du syst\u00e8me de num\u00e9ration.

    Sans cette connaissance \u00e0 priori du syst\u00e8me de num\u00e9ration utilis\u00e9, il vous est impossible d'interpr\u00e9ter les nombres suivants\u2009:

    69128\n11027\nj4b12\n>>!!0\n\u4e5d\u5343\u5341\u516b\n\u4e5d\u5343 \u96f6\u5341\u516b\n

    En effet, au-del\u00e0 de l'ordre des symboles (de gauche \u00e0 droite), la base du syst\u00e8me utilis\u00e9 est cruciale pour interpr\u00e9ter ces nombres. Cette base d\u00e9termine le nombre de symboles distincts qui peuvent \u00eatre employ\u00e9s pour chaque position, et par cons\u00e9quent, elle r\u00e9git la structure m\u00eame du nombre. Par exemple, une base dix (d\u00e9cimale) utilise dix symboles (0-9), tandis qu'une base deux (binaire) n'en utilise que deux (0 et 1). Sans cette compr\u00e9hension, les nombres demeurent incompr\u00e9hensibles et d\u00e9pourvus de signification.

    Exercice 1\u2009: Symboles binaires

    Dans la notation binaire, compos\u00e9s de 1 et de 0, combien de symboles existent et combien de positions y-a-t-il dans le nombre 11001 ?

    Solution

    Le nombre 11001 est compos\u00e9 de 5 positions et de deux symboles possibles par position\u2009: 1 et 0. La quantit\u00e9 d'information est donc e 5 bits.

    ", "tags": ["base"]}, {"location": "course-c/10-numeration/bases/#systeme-decimal", "title": "Syst\u00e8me d\u00e9cimal", "text": "

    Le syst\u00e8me d\u00e9cimal est le syst\u00e8me de num\u00e9ration utilisant la base dix et le plus utilis\u00e9 par l'humanit\u00e9 au vingt et uni\u00e8me si\u00e8cle, ce qui n'a pas toujours \u00e9t\u00e9 le cas. Par exemple, les anciennes civilisations de M\u00e9sopotamie (Sumer ou Babylone) utilisaient un syst\u00e8me positionnel de base sexag\u00e9simale (60) toujours utilis\u00e9 pour la repr\u00e9sentation des heures ou des angles, la civilisation maya utilisait un syst\u00e8me de base 20 encore ancr\u00e9e dans la culture fran\u00e7aise de m\u00eame que certaines langues celtiques dont il reste aujourd'hui quelques traces en fran\u00e7ais avec la d\u00e9nomination quatre-vingts.

    L'exemple suivant montre l'\u00e9criture de 1506 en \u00e9criture hi\u00e9roglyphique de\u2009:

    \\[ 1000+100+100+100+100+100+1+1+1+1+1+1\\]

    Il s'agit d'une num\u00e9ration additive.

    1506 en \u00e9criture hi\u00e9roglyphique

    Notre syst\u00e8me de repr\u00e9sentation des nombres d\u00e9cimaux est le syst\u00e8me de num\u00e9ration indo-arabe qui emploie une notation positionnelle et dix chiffres (ou symboles) allant de z\u00e9ro \u00e0 neuf et un nombre peut se d\u00e9composer en puissance successive\u2009:

    \\[ 1506_{10} = 1 \\cdot 10^{3} + 5 \\cdot 10^{2} + 0 \\cdot 10^{1} + 6 \\cdot 10^{0} \\]

    Nous l'avons vu au chapitre pr\u00e9c\u00e9dent, la base dix n'est pas utilis\u00e9e dans les ordinateurs, car elle n\u00e9cessite la manipulation de dix \u00e9tats, ce qui est difficile avec les syst\u00e8mes logiques \u00e0 deux \u00e9tats\u2009; le stockage d'un bit en m\u00e9moire \u00e9tant g\u00e9n\u00e9ralement assur\u00e9 par des transistors.

    Exercice 2\u2009: Deux mains

    Un dessin repr\u00e9sentant deux mains humaines (compos\u00e9es chacune de cinq doigts) est utilis\u00e9 pour repr\u00e9senter un chiffre. Les doigts peuvent \u00eatre soit lev\u00e9s, soit baiss\u00e9s mais un seul doigt peut \u00eatre lev\u00e9. Quelle est la base utilis\u00e9e\u2009?

    Solution

    Deux mains de cinq doigts forment une paire compos\u00e9e de 10 doigts. Il existe donc dix possibilit\u00e9s, la base est donc d\u00e9cimale\u2009: 10.

    Si plusieurs doigts peuvent \u00eatre lev\u00e9s \u00e0 la fois, il faut r\u00e9duire le syst\u00e8me \u00e0 l'unit\u00e9 de base \u00ab\u2009le doigt\u2009\u00bb pouvant prendre deux \u00e9tats\u2009: lev\u00e9 ou baiss\u00e9. Avec dix doigts (dix positions) et 2 symboles par doigts, un ombre binaire est ainsi repr\u00e9sent\u00e9.

    ", "tags": ["systeme-decimal", "sexagesimale", "indo-arabe"]}, {"location": "course-c/10-numeration/bases/#systeme-binaire", "title": "Syst\u00e8me binaire", "text": "

    Le syst\u00e8me binaire est similaire au syst\u00e8me d\u00e9cimal, mais utilise la base deux. Les symboles utilis\u00e9s pour exprimer ces deux \u00e9tats possibles sont d'ailleurs emprunt\u00e9s au syst\u00e8me indo-arabe\u2009:

    \\[ \\begin{bmatrix} 0\\\\ 1 \\end{bmatrix} = \\begin{bmatrix} \\text{true}\\\\ \\text{false} \\end{bmatrix} = \\begin{bmatrix} T\\\\ F \\end{bmatrix} \\]

    En termes techniques ces \u00e9tats sont le plus souvent repr\u00e9sent\u00e9s par des signaux \u00e9lectriques dont souvent l'un des deux \u00e9tats est dit r\u00e9cessif tandis que l'autre est dit dominant. Par exemple si l'\u00e9tat 0 est symbolis\u00e9 par un verre vide et l'\u00e9tat 1 par un verre contenant du liquide. L'\u00e9tat dominant est l'\u00e9tat 1. En effet, si le verre contient d\u00e9j\u00e0 du liquide, en rajouter ne changera pas l'\u00e9tat actuel, il y aura juste plus de liquide dans le verre.

    Un nombre binaire peut \u00eatre \u00e9galement d\u00e9compos\u00e9 en puissance successive\u2009:

    \\[ 1101_{2} = 1 \\cdot 2^{3} + 1 \\cdot 2^{2} + 0 \\cdot 2^{1} + 1 \\cdot 2^{0} \\]

    Le nombre de possibilit\u00e9s pour un nombre de positions \\(E\\) et une quantit\u00e9 de symboles (ou base) \\(b\\) de 2 est simplement exprim\u00e9 par\u2009:

    \\[ N = b^E \\]

    Avec un seul bit il est donc possible d'exprimer 2 valeurs distinctes.

    Exercice 3\u2009: Base 2

    Combien de valeurs d\u00e9cimales peuvent \u00eatre repr\u00e9sent\u00e9es avec 10-bits\u2009?

    Solution

    Avec une base binaire 2 et 10 bits, le total repr\u00e9sentable est\u2009:

    \\[2^10 = 1024\\]

    Soit les nombres de 0 \u00e0 1023.

    ", "tags": ["bit", "systeme-binaire"]}, {"location": "course-c/10-numeration/bases/#systeme-octal", "title": "Syst\u00e8me octal", "text": "

    Invent\u00e9 par Charles XII de Su\u00e8de , le syst\u00e8me de num\u00e9ration octal utilise 8 symboles emprunt\u00e9s au syst\u00e8me indo-arabe. Ce syst\u00e8me pourrait avoir \u00e9t\u00e9 utilis\u00e9 par l'homme en comptant soit les jointures des phalanges proximales (trous entre les doigts), ou les doigts diff\u00e9rents des pouces.

    0 1 2 3 4 5 6 7\n

    Notons que l'utilisation des 8 premiers symboles du syst\u00e8me indo-arabe est une convention d'usage bien pratique, car tout humain occidental est familier de ces symboles. L'inconv\u00e9nient est qu'un nombre \u00e9crit en octal pourrait \u00eatre confondu avec un nombre \u00e9crit en d\u00e9cimal. Comme pour le syst\u00e8me d\u00e9cimal, un nombre octal peut \u00e9galement \u00eatre d\u00e9compos\u00e9 en puissance successive\u2009:

    \\[ 1607_{8} = 1 \\cdot 8^{3} + 6 \\cdot 8^{2} + 0 \\cdot 8^{1} + 7 \\cdot 8^{0} \\]

    Au d\u00e9but de l'informatique, la base octale fut tr\u00e8s utilis\u00e9e, car il est tr\u00e8s facile de la construire \u00e0 partir de la num\u00e9ration binaire, en regroupant les chiffres par triplets\u2009:

    010'111'100'001\u2082 = 2741\u2088\n

    En C, un nombre octal est \u00e9crit en pr\u00e9fixant la valeur \u00e0 repr\u00e9senter d'un z\u00e9ro. Attention donc \u00e0 ne pas confondre\u2009:

    int octal = 042; // (1)!\nint decimal = 42;\n\nassert(octal != decimal);\n
    1. La valeur 042 est un nombre octal, soit \\(4 \\cdot 8^1 + 2 \\cdot 8^0 = 34\\) en d\u00e9cimal. En C un nombre octal est pr\u00e9fix\u00e9 par un z\u00e9ro.

    Il est \u00e9galement possible de faire r\u00e9f\u00e9rence \u00e0 un caract\u00e8re en utilisant l'\u00e9chappement octal dans une cha\u00eene de caract\u00e8re\u2009:

    char cr = '\\015';\nchar msg = \"Hell\\0157\\040World!\";\n

    Important

    N'essayez pas de pr\u00e9fixer vos nombres avec des z\u00e9ros lorsque vous programmer car ces nombres seraient alors interpr\u00e9t\u00e9s en octal et non en d\u00e9cimal.

    "}, {"location": "course-c/10-numeration/bases/#systeme-hexadecimal", "title": "Syst\u00e8me hexad\u00e9cimal", "text": "

    Ce syst\u00e8me de num\u00e9ration positionnel en base 16 est le plus utilis\u00e9 en informatique pour exprimer des grandeurs binaires. Il utilise les dix symboles du syst\u00e8me indo-arabe, plus les lettres de A \u00e0 F. Il n'y a pas de r\u00e9el consensus quant \u00e0 la casse des lettres qui peuvent \u00eatre soit majuscules ou minuscules. Veillez n\u00e9anmoins \u00e0 respecter une certaine coh\u00e9rence, ne m\u00e9langez pas les casses (majuscules/minuscules) dans un m\u00eame projet.

    0 1 2 3 4 5 6 7 8 9 A B C D E F\n

    Comme pour les autres bases, l'\u00e9criture peut \u00e9galement \u00eatre d\u00e9compos\u00e9e en puissance successive\u2009:

    \\[ 1AC7_{16} = (1 \\cdot 16^{3} + 10 \\cdot 16^{2} + 12 \\cdot 16^{1} + 7 \\cdot 16^{0})_{10} = 41415_{10} \\]

    La notation hexad\u00e9cimale est tr\u00e8s pratique en \u00e9lectronique et en informatique, car chaque chiffre hexad\u00e9cimal repr\u00e9sente un quadruplet de bits, soit deux caract\u00e8res hexad\u00e9cimaux par octet\u2009:

    0101'1110'0001\u2082 = 5E1\u2081\u2086\n

    Tout ing\u00e9nieur devrait conna\u00eetre par c\u0153ur la correspondance hexad\u00e9cimale de tous les quadruplets aussi bien que ses tables de multiplication (qu'il conna\u00eet d'ailleurs parfaitement, n'est-ce pas\u2009?). La table suivante vous aidera \u00e0 convertir rapidement un nombre hexad\u00e9cimal en d\u00e9cimal\u2009:

    Correspondance binaire, octale, hexad\u00e9cimale Binaire Hexad\u00e9cimal Octal D\u00e9cimal 0b0000 0x0 00 0 0b0001 0x1 01 1 0b0010 0x2 02 2 0b0011 0x3 03 3 0b0100 0x4 04 4 0b0101 0x5 05 5 0b0110 0x6 06 6 0b0111 0x7 07 7 0b1000 0x8 10 8 0b1001 0x9 11 0 0b1010 0xA 12 10 0b1011 0xB 13 11 0b1100 0xC 14 12 0b1101 0xD 15 13 0b1110 0xE 16 14 0b1111 0xF 17 15

    Le fichier albatros.txt (r\u00e9dig\u00e9 avec ed, rappelez-vous) contient un extrait du po\u00e8me de Baudelaire. Un ing\u00e9nieur en proie \u00e0 un bogue li\u00e9 \u00e0 de l'encodage de caract\u00e8re cherche \u00e0 le r\u00e9soudre et utilise le programme hexdump pour lister le contenu hexad\u00e9cimal de son fichier. Il obtient la sortie suivante sur son terminal\u2009:

    $ hexdump -C albatros.txt\n0000  53 6f 75 76 65 6e 74 2c  20 70 6f 75 72 20 73 27  |Souvent, pour s'|\n0010  61 6d 75 73 65 72 2c 20  6c 65 73 20 68 6f 6d 6d  |amuser, les homm|\n0020  65 73 20 64 27 c3 a9 71  75 69 70 61 67 65 0d 0a  |es d'..quipage..|\n0030  50 72 65 6e 6e 65 6e 74  20 64 65 73 20 61 6c 62  |Prennent des alb|\n0040  61 74 72 6f 73 2c 20 76  61 73 74 65 73 20 6f 69  |atros, vastes oi|\n0050  73 65 61 75 78 20 64 65  73 20 6d 65 72 73 2c 0d  |seaux des mers,.|\n0060  0a 51 75 69 20 73 75 69  76 65 6e 74 2c 20 69 6e  |.Qui suivent, in|\n0070  64 6f 6c 65 6e 74 73 20  63 6f 6d 70 61 67 6e 6f  |dolents compagno|\n0080  6e 73 20 64 65 20 76 6f  79 61 67 65 2c 0d 0a 4c  |ns de voyage,..L|\n0090  65 20 6e 61 76 69 72 65  20 67 6c 69 73 73 61 6e  |e navire glissan|\n00a0  74 20 73 75 72 20 6c 65  73 20 67 6f 75 66 66 72  |t sur les gouffr|\n00b0  65 73 20 61 6d 65 72 73  2e 0d 0a 0d 0a 2e 2e 2e  |es amers........|\n00c0  0d 0a 0d 0a 43 65 20 76  6f 79 61 67 65 75 72 20  |....Ce voyageur |\n00d0  61 69 6c 65 cc 81 2c 20  63 6f 6d 6d 65 20 69 6c  |aile.., comme il|\n00e0  20 65 73 74 20 67 61 75  63 68 65 20 65 74 20 76  | est gauche et v|\n00f0  65 75 6c 65 e2 80 af 21  0d 0a 4c 75 69 2c 20 6e  |eule...!..Lui, n|\n0100  61 67 75 c3 a8 72 65 20  73 69 20 62 65 61 75 2c  |agu..re si beau,|\n0110  20 71 75 27 69 6c 20 65  73 74 20 63 6f 6d 69 71  | qu'il est comiq|\n0120  75 65 20 65 74 20 6c 61  69 64 e2 80 af 21 0d 0a  |ue et laid...!..|\n0130  4c 27 75 6e 20 61 67 61  63 65 20 73 6f 6e 20 62  |L'un agace son b|\n0140  65 63 20 61 76 65 63 20  75 6e 20 62 72 c3 bb 6c  |ec avec un br..l|\n0150  65 2d 67 75 65 75 6c 65  2c 0d 0a 4c 27 61 75 74  |e-gueule,..L'aut|\n0160  72 65 20 6d 69 6d 65 2c  20 65 6e 20 62 6f 69 74  |re mime, en boit|\n0170  61 6e 74 2c 20 6c 27 69  6e 66 69 72 6d 65 20 71  |ant, l'infirme q|\n0180  75 69 20 76 6f 6c 61 69  74 e2 80 af 21           |ui volait...!|\n018d\n

    Il lit \u00e0 gauche sur la premi\u00e8re colonne l'offset m\u00e9moire de chaque ligne, au milieu le contenu hexad\u00e9cimal, chaque caract\u00e8re encod\u00e9 sur 8 bits \u00e9tant symbolis\u00e9s par deux caract\u00e8res hexad\u00e9cimaux, et \u00e0 droite le texte o\u00f9 chaque caract\u00e8re non imprimable est remplac\u00e9 par un point. On observe notamment ici que\u2009:

    • \u00e9 de \u00e9quipage est encod\u00e9 avec \\xc3\\xa9 ce qui est le caract\u00e8re Unicode 0065
    • \u00e9 de ail\u00e9 est encod\u00e9 avec e\\xcc\\x81, soit le caract\u00e8re e suivi du diacritique \u00b4 0301
    • Une espace fine ins\u00e9cable \\xe2\\x80\\xaf est utilis\u00e9e avant les !, ce qui est le caract\u00e8re Unicode 202F, conform\u00e9ment \u00e0 la recommandation de l'Acad\u00e9mie fran\u00e7aise.

    Ce fichier est donc encod\u00e9 en UTF-8 quant au bogue de notre ami ing\u00e9nieur, il concerne tr\u00e8s probablement les deux mani\u00e8res distinctes utilis\u00e9es pour encoder le \u00e9. La saisie du po\u00e8me manque donc de coh\u00e9rence et le diable est dans les d\u00e9tails.

    Par cet exercice, on observe n\u00e9anmoins l'\u00e9l\u00e9gance de l'encodage hexad\u00e9cimal qui permet de visualiser facilement, par groupe de 8 bits, le contenu du fichier, ce qui aurait \u00e9t\u00e9 beaucoup moins \u00e9vident en binaire.

    Exercice 4\u2009: Les chiffres hexad\u00e9cimaux

    Calculez la valeur d\u00e9cimale des nombres suivants et donnez le d\u00e9tail du calcul\u2009:

    0xaaaa\n0b1100101\n0x1010\n129\n0216\n
    Solution
    0xaaaa    \u2261 43690\n0b1100101 \u2261   101\n0x1010    \u2261  4112\n129       \u2261   129 (n'est-ce pas ?)\n0216      \u2261   142\n

    Exercice 5\u2009: Albatros

    Tentez de r\u00e9cup\u00e9rer vous m\u00eame le po\u00e8me l'Albatros de Baudelaire et d'afficher le m\u00eame r\u00e9sultat que ci-dessus depuis un terminal de commande Linux.

    $ wget https://.../albatros.txt\n$ hexdump -C albatros.txt\n

    Si vous n'avez pas les outils wget ou hexdump, tentez de les installer ia la commande apt-get install wget hexdump sous Ubuntu.

    ", "tags": ["albatros.txt", "hexdump", "wget"]}, {"location": "course-c/10-numeration/bases/#conversions-de-bases", "title": "Conversions de bases", "text": "

    La conversion d'une base quelconque en syst\u00e8me d\u00e9cimal utilise la relation suivante\u2009:

    \\[ \\sum_{i=0}^{n-1} h_i\\cdot b^i \\]

    o\u00f9\u2009:

    \\(n\\)

    Le nombre de chiffres (ou positions)

    \\(b\\)

    La base du syst\u00e8me d'entr\u00e9e (ou nombre de symboles)

    \\(h_i\\)

    La valeur du chiffre \u00e0 la position \\(i\\)

    Ainsi, la valeur AP7 exprim\u00e9e en base tritrigesimale (base 33) et utilis\u00e9e pour repr\u00e9senter les plaques des v\u00e9hicules \u00e0 Hong Kong peut se convertir en d\u00e9cimales apr\u00e8s avoir pris connaissance de la correspondance d'un symbole tritrigesimal vers le syst\u00e8me d\u00e9cimal\u2009:

    Tritrigesimal -> D\u00e9cimal :\n\n 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F\n 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15\n\n G  H  I  K  L  M  N  P  R  S  T  U  V  W  X  Y  Z\n16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32\n\nConversion :\n\nAP7 -> 10 * pow(33, 2) + 23 * pow(33, 1) + 7 * pow(33, 0) -> 11'656\n

    La conversion d'une grandeur d\u00e9cimale vers une base quelconque est malheureusement plus compliqu\u00e9e et n\u00e9cessite d'appliquer un algorithme.

    La conversion d'un nombre du syst\u00e8me d\u00e9cimal au syst\u00e8me binaire s'effectue simplement par une suite de divisions pour lesquelles on notera le reste.

    Pour chaque division par 2, on note le reste et tant que le quotient n'est pas nul, on it\u00e8re l'op\u00e9ration. Le r\u00e9sultat en binaire est la suite des restes lus dans le sens inverse\u2009:

    n = 209\n\n209 / 2 == 104, 209 % 2 == 1  ^ sens de lecture des restes\n104 / 2 ==  52, 104 % 2 == 0  |\n 52 / 2 ==  26,  52 % 2 == 0  |\n 26 / 2 ==  13,  26 % 2 == 0  |\n 13 / 2 ==   6,  13 % 2 == 1  |\n  6 / 2 ==   3,   6 % 2 == 0  |\n  3 / 2 ==   1,   3 % 2 == 1  |\n  1 / 2 ==   0,   1 % 2 == 1  |\n\n209 == 0b11010001\n

    Exercice 6\u2009: La num\u00e9ration Shadock

    Les Shadocks

    Les Shadocks ne connaissent que quatre mots\u2009: GA, BU, ZO, MEU. La vid\u00e9o \u00e9ducative comment compter comme les Shadocks en explique le principe. Ils utilisent par cons\u00e9quent une base quaternaire.

    Convertir \u2212\u2a3c\u25cb\u25ff\u25cb (BU ZO GA MEU GA) en d\u00e9cimal.

    Solution

    Le syst\u00e8me Shadock est un syst\u00e8me quaternaire similaire au syst\u00e8me du g\u00e9nome humain bas\u00e9 sur quatre bases nucl\u00e9iques. Assignons donc aux symboles Shadocks les symboles du syst\u00e8me indo-arabe que nous connaissons mieux\u2009:

    0 \u25cb (GA)\n1 \u2212 (BU)\n2 \u2a3c (ZO)\n3 \u25ff (MEU)\n

    Le nombre d'entr\u00e9e \u2212\u2a3cO\u25ffO peut ainsi s'exprimer\u2009:

    \u2212\u2a3c\u25cb\u25ff\u25cb \u2261 12030\u2084\n

    En appliquant la m\u00e9thode du cours, on obtient\u2009:

    \\[ 1 \\cdot 4^4 + 2 \\cdot 4^3 + 0 \\cdot 4^2 + 3 \\cdot 4^1 + 0 \\cdot 4^0 = 396_{10} \\]

    Notons que depuis un terminal Python vous pouvez simplement utiliser\u2009:

    int(\"12030\", 4)\n
    ", "tags": ["base-tritrigesimale", "MEU", "AP7"]}, {"location": "course-c/10-numeration/bases/#autres-bases", "title": "Autres bases", "text": "

    Une autre base couramment utilis\u00e9e est la base64, qui utilise les 26 lettres de l'alphabet latin (majuscules et minuscules), les 10 chiffres et deux symboles additionnels. Cette base est souvent utilis\u00e9e pour encoder des donn\u00e9es binaires en ASCII, par exemple pour les pi\u00e8ces jointes des courriels.

    Elle n'est pas \u00e0 proprement parler une base fondamentale, mais plut\u00f4t une m\u00e9thode de codage qui utilise 64 caract\u00e8res imprimables.

    On peut transmettre de l'information en binaire, mais cela implique de pouvoir g\u00e9rer un contenu arbitraire qui n'est pas toujours \u00e9vident dans des environnements pr\u00e9vus pour des caract\u00e8res imprimables. On pourrait se dire qu'on utilise la repr\u00e9sentation ASCII des caract\u00e8res, mais de nombreux caract\u00e8res ne sont pas imprimables. La base64 est une solution \u00e9l\u00e9gante pour encoder des donn\u00e9es binaires en ASCII.

    Prenons l'exemple de la phrase suivante\u2009:

    La fleur en bouquet f\u00e2ne... et jamais ne renait !\n

    Si l'on affiche le contenu hexad\u00e9cimal de cette phrase, on obtient\u2009:

    $ echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait !'  | hexdump -C\n0000  4c 61 20 66 6c 65 75 72  20 65 6e 20 62 6f 75 71  |La fleur en bouq|\n0010  75 65 74 20 66 c3 a2 6e  65 2e 2e 2e 20 65 74 20  |uet f..ne... et |\n0020  6a 61 6d 61 69 73 20 6e  65 20 72 65 6e 61 69 74  |jamais ne renait|\n0030  20 21                                             | !|\n0032\n

    En base64, le message est d\u00e9coup\u00e9 en mot de 6 bits, soit 64 valeurs possibles. Chaque mot de 6 bits est ensuite converti en un caract\u00e8re ASCII avec la table de codage suivante\u2009:

    0  000000 A    17 010001 R    34 100010 i    51 110011 z\n1  000001 B    18 010010 S    35 100011 j    52 110100 0\n2  000010 C    19 010011 T    36 100100 k    53 110101 1\n3  000011 D    20 010100 U    37 100101 l    54 110110 2\n4  000100 E    21 010101 V    38 100110 m    55 110111 3\n5  000101 F    22 010110 W    39 100111 n    56 111000 4\n6  000110 G    23 010111 X    40 101000 o    57 111001 5\n7  000111 H    24 011000 Y    41 101001 p    58 111010 6\n8  001000 I    25 011001 Z    42 101010 q    59 111011 7\n9  001001 J    26 011010 a    43 101011 r    60 111100 8\n10 001010 K    27 011011 b    44 101100 s    61 111101 9\n11 001011 L    28 011100 c    45 101101 t    62 111110 +\n12 001100 M    29 011101 d    46 101110 u    63 111111 /\n13 001101 N    30 011110 e    47 101111 v\n14 001110 O    31 011111 f    48 110000 w    (compl\u00e9ment) =\n15 001111 P    32 100000 g    49 110001 x\n16 010000 Q    33 100001 h    50 110010 y\n

    Ainsi le message commence par 4c612 ou en binaire 01001100 01100001 0010. D\u00e9coup\u00e9 en paquet de 6 bits 010011 000110 000100 10, on utilise selon la table de codage TGE. Comme il s'agit d'un encodage courant, il existe des outils pour le faire automatiquement\u2009:

    echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait !' | base64\nTGEgZmxldXIgZW4gYm91cXVldCBmw6JuZS4uLiBldCBqYW1haXMgbmUgcmVuYWl0ICE=\n

    Si le message n'est pas un multiple de \\(4\\times 6\\) bits, il est compl\u00e9t\u00e9 avec des z\u00e9ros et le caract\u00e8re = est ajout\u00e9 \u00e0 la fin du message pour indiquer le nombre de z\u00e9ros ajout\u00e9s. Notre message \u00e0 une longueur de 50 caract\u00e8res, ou bien 400 bits, qui n'est pas divisible par 24. On compl\u00e8te donc avec =.

    echo -ne 'La fleur en bouquet f\u00e2ne... et jamais ne renait'  | wc -c\n50\n
    ", "tags": ["ascii", "base64", "TGE"]}, {"location": "course-c/10-numeration/data/", "title": "L'information", "text": ""}, {"location": "course-c/10-numeration/data/#linformation", "title": "L'information", "text": "L'informatique ne concerne pas plus les ordinateurs que l'astronomie ne concerne les t\u00e9lescopes... La science ne concerne pas les outils. Elle concerne la mani\u00e8re dont nous les utilisons et ce que nous d\u00e9couvrons en les utilisant.Edsger W. Dijkstra

    L'information est au c\u0153ur de l'informatique. Elle est stock\u00e9e, transmise, trait\u00e9e, et transform\u00e9e par les ordinateurs. Comprendre comment l'information est repr\u00e9sent\u00e9e et manipul\u00e9e est essentiel pour tout d\u00e9veloppeur logiciel. Nous verrons que l'information d\u00e9signe non seulement un message, mais \u00e9galement les symboles utilis\u00e9s pour l'\u00e9crire.

    "}, {"location": "course-c/10-numeration/data/#quantite-dinformation-bit", "title": "Quantit\u00e9 d'information (bit)", "text": "

    Un bit est l'unit\u00e9 d'information fondamentale qui ne peut prendre que deux \u00e9tats\u2009: 1 ou 0. En \u00e9lectronique, cette information peut \u00eatre stock\u00e9e dans un \u00e9l\u00e9ment m\u00e9moire par une charge \u00e9lectrique. Dans le monde r\u00e9el, on peut stocker un bit avec une pi\u00e8ce de monnaie d\u00e9pos\u00e9e sur le c\u00f4t\u00e9 pile ou face. La combinaison de plusieurs bits permet de former des messages plus complexes.

    Le bit est l'abr\u00e9viation de binary digit (chiffre binaire) et il est central \u00e0 la th\u00e9orie de l'information. C'est un concept a \u00e9t\u00e9 popularis\u00e9 par Claude Shannon dans son article fondateur de la th\u00e9orie de l'information en 1948\u2009: A Mathematical Theory of Communication. Shannon y introduit le bit comme unit\u00e9 fondamentale de mesure de l'information.

    S'il existe un meuble avec huit casiers assez grands pour une pomme, et que l'on souhaite conna\u00eetre le nombre de possibilit\u00e9s de rangement, on sait que chaque casier peut contenir soit une pomme, soit aucune. Le nombre de possibilit\u00e9s d'agencer des pommes dans ce meuble est de \\(2^8 = 256\\). \\(2\\) repr\u00e9sente le nombre d'\u00e9tat que peut prendre un casier (pomme ou pas pomme) et \\(8\\) est le nombre de casiers. On d\u00e9finit que la quantit\u00e9 d'information n\u00e9cessaire \u00e0 conna\u00eetre l'\u00e9tat du meuble est de 8 bits.

    On pourrait tr\u00e8s bien utiliser ce meuble et ces pommes pour repr\u00e9senter son \u00e2ge. Un individu de 42 ans n'aurait pas besoin de 42 pommes, mais seulement de 3. En effet, si on repr\u00e9sente l'absence de pomme par 0 et la pr\u00e9sence d'une pomme par 1, on peut repr\u00e9senter l'\u00e2ge de 42 ans par\u2009:

    0 0 1 0 1 0 1 0\n

    De fa\u00e7on analogue, si l'on souhaite repr\u00e9senter l'\u00e9tat d'un meuble beaucoup plus grand, par exemple un meuble de 64 casiers, la quantit\u00e9 d'information repr\u00e9sentable serait de\u2009:

    \\[2^{64} = 18'446'744'073'709'551'616\\]

    ou 64 bits. Cela permet de repr\u00e9senter le nombre de grains de sable sur Terre, le nombre de secondes dans 584'942 ann\u00e9es, ou le nombre de combinaisons possibles pour un mot de passe de 8 caract\u00e8res. Alternativement, si on admet qu'un individu peut vivre jusqu'\u00e0 127 ans maximum, 7 bits suffisent \u00e0 repr\u00e9senter l'\u00e2ge d'une personne. Avec 64 pommes maximum, on peut dont donc repr\u00e9senter l'\u00e2ge d'environ 9 personnes.

    Ce nombre peu importe la mani\u00e8re dont il est interpr\u00e9t\u00e9 repr\u00e9sente une certaine quantit\u00e9 d'information qui peut s'exprimer par la formule g\u00e9n\u00e9rale suivante\u2009:

    \\[I = \\log_2(N)\\]

    o\u00f9 \\(I\\) est la quantit\u00e9 d'information en bits, et \\(N\\) est le nombre de possibilit\u00e9s.

    Les informaticiens ont l'habitude d'agencer les bits par groupe de 8 pour former ce que l'on appelle un octet. Un octet peut donc repr\u00e9senter \\(256\\) valeurs diff\u00e9rentes. Un octet est souvent appel\u00e9 un byte en anglais, mais ce terme reste ambigu, car il peut \u00e9galement d\u00e9signer un groupe de bits de taille variable. Historiquement les ordinateurs ont utilis\u00e9 des bytes de 6, 7, ou 8 bits, mais aujourd'hui l'octet est \u00e9quivalent au byte.

    Lorsque vous achetez un disque de stockage pour votre ordinateur, vous pouvez par exemple lire sur l'emballage que l'unit\u00e9 de stockage dispose d'une capacit\u00e9 de 1 Tio (T\u00e9bi-octet). Un T\u00e9bi-octet est \u00e9gal \u00e0 \\(2^{40}\\) octets, soit \\(1'099'511'627'776\\) octets. Un octet \u00e9tant \u00e9gal \u00e0 8 bits, donc un t\u00e9bi (millier de milliards) d'octet est \u00e9gal \u00e0 \\(8'796'093'022'208\\) bits. \u00c0 titre d'information l'enti\u00e8ret\u00e9 d'encyclop\u00e9die libre Wikip\u00e9dia en p\u00e8se environ 22 Go (Giga-octet). On peut affirmer que notre disque de 1 Tio, achet\u00e9 environ 50 dollars, permettrait de stocker 45 copies de Wikip\u00e9dia.

    Pour repr\u00e9senter l'\u00e9tat de Wikip\u00e9dia, il suffirait donc d'avoir \\(10'225'593'776'312\\) pommes et bien entendu de l'armoire idoine.

    Exercice 1\u2009: Pile ou face

    Lors d'un tir \u00e0 pile ou face de l'engagement d'un match de football, l'arbitre lance une pi\u00e8ce de monnaie qu'il rattrape et d\u00e9pose sur l'envers de sa main. Lorsqu'il annonce le r\u00e9sultat de ce tir, quelle quantit\u00e9 d'information transmet-il\u2009?

    Solution

    Il transmet un seul 1 bit d'information\u2009: \u00e9quipe A ou pile ou 1, \u00e9quipe B ou face ou 0. Il faut n\u00e9anmoins encore d\u00e9finir \u00e0 quoi correspond cette information.

    Entropie

    On entends souvent que l'entropie est la mesure du d\u00e9sordre d'un syst\u00e8me. En thermodynamique, l'entropie est une mesure de l'\u00e9nergie non disponible. En informatique, l'entropie est une mesure de l'incertitude d'une information. Plus une information est incertaine, plus elle contient d'entropie. L'entropie est souvent mesur\u00e9e en bits, et est utilis\u00e9e en cryptographie pour mesurer la qualit\u00e9 d'un g\u00e9n\u00e9rateur de nombres al\u00e9atoires.

    N\u00e9anmoins l'entropie peut \u00e9galement \u00eatre utilis\u00e9e pour mesurer la quantit\u00e9 d'information transmise par un message. Plus un message est incertain, plus il contient d'entropie. Par exemple, si un message est compos\u00e9 de 8 bits, il contient 8 bits d'entropie. Si le message est compos\u00e9 de 16 bits, il contient 16 bits d'entropie.

    ", "tags": ["entropie", "chiffre", "octet", "claude-shannon"]}, {"location": "course-c/10-numeration/data/#les-prefixes", "title": "Les pr\u00e9fixes", "text": "

    Comme \u00e9voqu\u00e9, le nombre de bits peut devenir rapidement colossal, m\u00eame divis\u00e9 par 8 pour obtenir un nombre d'octets. Il est difficile avec des nombres simples de repr\u00e9senter ces quantit\u00e9s et surtout de se les repr\u00e9senter. C'est pourquoi on utilise des pr\u00e9fixes. Un agriculteur parle de tonnes de bl\u00e9s, d'hectares de terres, un informaticien parle de giga de m\u00e9moire, de mega de donn\u00e9es, ou de kilo de bits par seconde dans un d\u00e9bit de connexion.

    Avec le syst\u00e8me international d'unit\u00e9s, nous utilisons tous des pr\u00e9fixes pour exprimer des multiples de dix. Par exemple, un kilogramme est \u00e9gal \u00e0 1000 grammes. De la m\u00eame mani\u00e8re, une tonne est \u00e9gale \u00e0 1000 kilogrammes et un hectare est \u00e9gal \u00e0 10'000 m\u00e8tres carr\u00e9s.

    L'informatique qui s'appuie sur l'unit\u00e9 fondamentale d'information en utilisant le syst\u00e8me binaire en puissance de deux, l'op\u00e9ration de rajouter un bit \u00e0 une quantit\u00e9 d'information double cette derni\u00e8re. On pr\u00e9f\u00e8rera donc des pr\u00e9fixes qui sont des multiples de 2. Or, par d\u00e9finition, un kilo-octet est \u00e9gal \u00e0 1000 octets \\(10^3\\). Le kibi-octet en revanche est \u00e9gal \u00e0 1024 octets \\(2^10\\). Les pr\u00e9fixes binaires sont normalis\u00e9s et d\u00e9finis par l'IEC (International Electrotechnical Commission) Voici un tableau des pr\u00e9fixes les plus courants\u2009:

    Pr\u00e9fixes standardsPr\u00e9fixes binaires Pr\u00e9fixes standards Pr\u00e9fixe Symbole \\(10^n\\) Kilo K \\(10^3\\) M\u00e9ga M \\(10^6\\) Giga G \\(10^9\\) T\u00e9ra T \\(10^{12}\\) Peta P \\(10^{15}\\) Exa E \\(10^{18}\\) Zetta Z \\(10^{21}\\) Yotta Y \\(10^{24}\\) Pr\u00e9fixes binaires Pr\u00e9fixe Symbole \\(2^{10n}\\) Kibi Ki \\(2^{10}\\) M\u00e9bi Mi \\(2^{20}\\) Gibi Gi \\(2^{30}\\) T\u00e9bi Ti \\(2^{40}\\) P\u00e9bi Pi \\(2^{50}\\) Exbi Ei \\(2^{60}\\) Zebi Zi \\(2^{70}\\) Yobi Yi \\(2^{80}\\)

    Info

    Les pr\u00e9fixes binaires restent m\u00e9connus et peu utilis\u00e9s. Les disques durs sont souvent vendus en Go (Giga-octets) alors que les syst\u00e8mes d'exploitation les affichent en Gio (Gibi-octets). Il est donc important de bien comprendre la diff\u00e9rence entre ces deux unit\u00e9s et de les utiliser correctement pour \u00e9viter toute confusion.

    ", "tags": ["prefixes-binaires"]}, {"location": "course-c/10-numeration/data/#notation-positionnelle", "title": "Notation positionnelle", "text": "

    La num\u00e9ration est la science de la repr\u00e9sentation des nombres. La num\u00e9ration d\u00e9cimale est un syst\u00e8me de base 10, c'est-\u00e0-dire que chaque chiffre peut prendre 10 valeurs diff\u00e9rentes\u2009: \\(0, 1, 2, 3, 4, 5, 6, 7, 8, 9\\). La position des chiffres dans un nombre d\u00e9cimal indique la puissance de 10 \u00e0 laquelle il est multipli\u00e9. Par exemple, le nombre 123 est \u00e9gal \u00e0\u2009:

    \\[1 \\times 10^2 + 2 \\times 10^1 + 3 \\times 10^0\\]

    On parle ici de notation positionnelle, car la position des chiffres est importante, comme nos pommes dans nos casiers. Le chiffre le plus \u00e0 droite est le chiffre des unit\u00e9s, le chiffre \u00e0 sa gauche est le chiffre des dizaines, puis des centaines, etc. Cela peut vous sembler d'une grande trivialit\u00e9, car notre civilisation moderne y est familiaris\u00e9e depuis des si\u00e8cles. N\u00e9anmoins, les syst\u00e8mes de num\u00e9ration les plus anciens, comme ceux bas\u00e9s sur les os d'Ishango (datant d'environ 20'000 ans avant notre \u00e8re), n'utilisaient pas ce concept. Ces syst\u00e8mes se contentaient souvent de repr\u00e9senter des quantit\u00e9s par des marques ou des symboles sans utiliser la position pour indiquer des valeurs diff\u00e9rentes.

    La v\u00e9ritable apparition de la notation positionnelle est attribu\u00e9e aux math\u00e9maticiens indiens, autour du 5^e si\u00e8cle de notre \u00e8re. Le syst\u00e8me de num\u00e9ration indien utilisait dix symboles (\\(0\\) \u00e0 \\(9\\)), et la position de chaque chiffre dans un nombre indiquait sa valeur multiplicative par une puissance de dix. Ce syst\u00e8me a \u00e9t\u00e9 r\u00e9volutionnaire, car il simplifiait grandement les calculs, rendant les op\u00e9rations arithm\u00e9tiques plus efficaces.

    Ce syst\u00e8me indien a ensuite \u00e9t\u00e9 transmis aux Arabes, qui l'ont adopt\u00e9 et perfectionn\u00e9 avant de le diffuser en Europe au cours du Moyen \u00c2ge. C'est ce syst\u00e8me, connu aujourd'hui sous le nom de \u00ab\u2009syst\u00e8me d\u00e9cimal\u2009\u00bb ou \u00ab\u2009syst\u00e8me indo-arabe\u2009\u00bb, qui est \u00e0 la base de la notation positionnelle utilis\u00e9e universellement de nos jours.

    Le choix du nombre de symboles est bien entendu arbitraire. On pourrait utiliser deux, trois ou cinquante symboles diff\u00e9rents pour autant que la position de ces symboles indique la valeur multiplicative par une puissance \u00e9quivalente au nombre de symboles. Ce concept c'est ce que nous appellerons la base du syst\u00e8me de num\u00e9ration.

    En informatique, nous utilisons deux symboles et donc une base de deux nomm\u00e9e base binaire. En binaire on nomme LSB (Least Significant Bit) le bit de poids faible et MSB (Most Significant Bit) le bit de poids fort. Le bit de poids faible est le bit le plus \u00e0 droite, et le bit de poids fort est le bit le plus \u00e0 gauche. Il est remarquable de noter que le LSB permet de savoir si le nombre est pair ou impair, si le LSB est \u00e0 0, le nombre est pair, et s'il est \u00e0 1, le nombre est impair\u2009:

    bool is_even(int n) {\n    return n & 1 == 0;\n}\n

    Le MSB quant \u00e0 lui permet de savoir si le nombre est positif ou n\u00e9gatif dans un nombre sign\u00e9 utilisant le compl\u00e9ment \u00e0 deux. Si le MSB est \u00e0 0, le nombre est positif, et s'il est \u00e0 1, le nombre est n\u00e9gatif (on pr\u00e9f\u00e8rera plut\u00f4t utiliser n < 0 pour v\u00e9rifier si un nombre est n\u00e9gatif).

    bool is_negative(int32_t n) {\n    return n & 0x80000000 == 0x80000000;\n}\n

    Exercice 2\u2009: Nature de ces nombres\u2009?

    Pour les nombres suivants stock\u00e9s sur 8-bit, pouvez-vous dire s'ils sont pairs ou impairs, positifs ou n\u00e9gatifs\u2009?

    1. 0b01100000 est et de signe
    2. 0b00001001 est et de signe
    3. 0b10000000 est et de signe
    4. 0b11011011 est et de signe

    Submit

    ", "tags": ["least-significant-bit", "most-significant-bit", "base-10"]}, {"location": "course-c/10-numeration/data/#codification-de-linformation", "title": "Codification de l'information", "text": "

    On a vu plus haut que les nombres en informatiques sont stock\u00e9s sous forme de bits agenc\u00e9s en octets et dont l'ordre est important. Cette m\u00e9thode permet de repr\u00e9senter des nombres entiers positifs, mais pour repr\u00e9senter les nombres n\u00e9gatifs ou les nombres \u00e0 virgule, il faut utiliser des m\u00e9thodes de codification sp\u00e9cifiques.

    La norme IEEE 754 est utilis\u00e9e pour repr\u00e9senter les nombres \u00e0 virgule et le compl\u00e9ment \u00e0 deux pour repr\u00e9senter les nombres n\u00e9gatifs. Ces deux m\u00e9thodes sur lesquels nous reviendrons plus tard sont essentielles pour comprendre comment les nombres sont stock\u00e9s en m\u00e9moire et comment les op\u00e9rations arithm\u00e9tiques sont effectu\u00e9es. En outre, une succession de bits peut repr\u00e9senter bien plus que des nombres. Ils peuvent repr\u00e9senter du texte, des images ou des programmes, mais in fine tout est stock\u00e9 sous forme de bits, et c'est \u00e0 l'interpr\u00e9tation de ces bits que l'on peut en extraire un contenu utile.

    "}, {"location": "course-c/10-numeration/data/#transmission-de-linformation", "title": "Transmission de l'information", "text": "

    Il est fondamental de comprendre que le stockage de l'information n'acquiert toute sa valeur que lorsque cette information peut \u00eatre efficacement transmise et re\u00e7ue. Ce processus ne se limite pas \u00e0 la simple conservation des donn\u00e9es\u2009: le protocole d'encodage joue un r\u00f4le tout aussi crucial. L'histoire regorge d'exemples o\u00f9 les vestiges des civilisations pass\u00e9es nous ont transmis des messages \u00e9nigmatiques, dont le d\u00e9chiffrement reste incertain. Parmi ces exemples, on peut citer les hi\u00e9roglyphes \u00e9gyptiens, les tablettes cun\u00e9iformes sum\u00e9riennes ou encore les manuscrits de la mer Morte, qui ont \u00e9t\u00e9 partiellement r\u00e9v\u00e9l\u00e9s gr\u00e2ce aux travaux \u00e9rudits de Jean-Fran\u00e7ois Champollion sur la pierre de Rosette, d'Henry Rawlinson sur l'inscription de Behistun, ou encore de William F. Albright sur les manuscrits de Qumr\u00e2n. Toutefois, il subsiste des \u00e9critures anciennes qui demeurent herm\u00e9tiques, comme l'\u00e9criture rongorongo de l'\u00eele de P\u00e2ques. Les khipus, ces cordes nou\u00e9es par les Incas, constituent un autre exemple fascinant d'un syst\u00e8me d'encodage dont le secret nous \u00e9chappe encore.

    L'\u00e9volution des moyens de communication nous permet aujourd'hui de transmettre des informations sur des distances inimaginables \u00e0 des vitesses vertigineuses. Par exemple, la transmission d'un signal entre la Terre et Mars, \u00e0 une distance moyenne d'environ 225 millions de kilom\u00e8tres, prend environ 12,5 minutes. Cette dur\u00e9e, bien que rapide \u00e0 l'\u00e9chelle cosmique, impose des contraintes significatives pour les missions spatiales, obligeant \u00e0 une planification m\u00e9ticuleuse et \u00e0 une anticipation des \u00e9changes. Un autre exemple marquant est la communication avec la sonde Voyager 1, situ\u00e9e actuellement \u00e0 plus de 23 milliards de kilom\u00e8tres de la Terre. Les signaux radio, voyageant \u00e0 la vitesse de la lumi\u00e8re, mettent plus de 21 heures pour atteindre notre plan\u00e8te, illustrant les d\u00e9fis de la transmission \u00e0 travers les vastes \u00e9tendues de l'espace.

    En ce qui concerne la quantit\u00e9 d'informations, l'exp\u00e9rience internationale de mesure utilisant des radiot\u00e9lescopes, telle que l'observation des trous noirs via l'Event Horizon Telescope (EHT), a g\u00e9n\u00e9r\u00e9 des volumes de donn\u00e9es si immenses qu'il a \u00e9t\u00e9 plus rapide de transporter les disques durs contenant des p\u00e9taoctets d'informations par avion que de les transmettre via les r\u00e9seaux de communication. Cette r\u00e9alit\u00e9 t\u00e9moigne des limites actuelles des infrastructures de t\u00e9l\u00e9communication face \u00e0 l'immensit\u00e9 des donn\u00e9es g\u00e9n\u00e9r\u00e9es par les sciences modernes. Aussi, la transmission de l'information est un enjeu majeur, non seulement dans sa capacit\u00e9 \u00e0 franchir les distances, mais aussi dans son aptitude \u00e0 pr\u00e9server et \u00e0 interpr\u00e9ter les donn\u00e9es cod\u00e9es, que ce soit \u00e0 travers le temps ou l'espace.

    ", "tags": ["dechiffrement", "rongorongo", "khipus"]}, {"location": "course-c/10-numeration/data/#perennite-de-linformation", "title": "P\u00e9rennit\u00e9 de l'information", "text": "

    La p\u00e9rennit\u00e9 de l'information repr\u00e9sente un d\u00e9fi tout aussi crucial que sa transmission. En effet, les supports physiques sur lesquels nous conservons nos donn\u00e9es sont souvent fragiles et p\u00e9rissables. Le papier, qui a servi de base \u00e0 la transmission du savoir pendant des si\u00e8cles, s'use et se d\u00e9grade au fil du temps, m\u00eame lorsqu'il est conserv\u00e9 dans des conditions optimales. Les bandes magn\u00e9tiques, autrefois utilis\u00e9es pour stocker de grandes quantit\u00e9s de donn\u00e9es num\u00e9riques, ont une dur\u00e9e de vie limit\u00e9e, avec une d\u00e9t\u00e9rioration progressive de l'information qu'elles contiennent. De m\u00eame, les CD et les DVD, longtemps per\u00e7us comme des solutions de stockage robustes, ont montr\u00e9 leurs limites\u2009: leur surface peut se corroder, entra\u00eenant une perte irr\u00e9m\u00e9diable des donn\u00e9es.

    Arctic World Archive

    Consciente de ces limitations, l'humanit\u00e9 a entrepris de consolider ses connaissances en les stockant dans des endroits sp\u00e9cialement con\u00e7us pour r\u00e9sister \u00e0 l'\u00e9preuve du temps. Un des projets les plus ambitieux en la mati\u00e8re est l'Arctic World Archive (AWA), situ\u00e9 dans l'archipel de Svalbard, en Norv\u00e8ge, \u00e0 proximit\u00e9 du Global Seed Vault. L'Arctic World Archive, ouvert en 2017, est une installation s\u00e9curis\u00e9e dans une ancienne mine de charbon, enfouie sous des centaines de m\u00e8tres de perg\u00e9lisol. Ce projet vise \u00e0 pr\u00e9server les donn\u00e9es num\u00e9riques pour des si\u00e8cles, voire des mill\u00e9naires, en utilisant une technologie de stockage sur PiqlFilm, un film photosensible sp\u00e9cialement con\u00e7u pour garantir la durabilit\u00e9 des informations sans n\u00e9cessiter d'\u00e9lectricit\u00e9 ou de maintenance continue.

    Des institutions du monde entier, telles que les Archives nationales du Br\u00e9sil, l'Agence spatiale europ\u00e9enne (ESA) et m\u00eame GitHub, y ont d\u00e9j\u00e0 d\u00e9pos\u00e9 des donn\u00e9es importantes. Par exemple, GitHub a archiv\u00e9 21 t\u00e9raoctets de donn\u00e9es provenant de l'ensemble des d\u00e9p\u00f4ts publics actifs de la plateforme, garantissant ainsi la p\u00e9rennit\u00e9 de pr\u00e9cieuses ressources en code source pour les g\u00e9n\u00e9rations futures (voir GitHub Archive Program). Ainsi, Svalbard, avec l'Arctic World Archive, s'affirme comme un bastion de la conservation des connaissances num\u00e9riques, soulignant l'importance de la pr\u00e9servation des donn\u00e9es dans un monde o\u00f9 la technologie \u00e9volue rapidement, mais o\u00f9 la fiabilit\u00e9 des supports de stockage demeure un enjeu majeur.

    "}, {"location": "course-c/10-numeration/numbers/", "title": "Nombres", "text": "Les nombres gouvernent le monde.Pythagore

    Vous avez tous appris dans votre enfance \u00e0 compter, puis vous avez appris que les nombres se classifient dans des ensembles. Les math\u00e9maticiens ont d\u00e9fini des ensembles de nombres pour lesquels des propri\u00e9t\u00e9s particuli\u00e8res sont v\u00e9rifi\u00e9es\u2009; ces ensembles sont imbriqu\u00e9s les uns dans les autres, et chaque ensemble est un sous-ensemble de l'ensemble suivant. La figure suivante illustre cette hi\u00e9rarchie.

    \\[ \\mathbb{N} \\in \\mathbb{Z} \\in \\mathbb{Q} \\in \\mathbb{R} \\in \\mathbb{C} \\in \\mathbb{H} \\in \\mathbb{O} \\in \\mathbb{S} \\]

    Ensemble des nombres

    Les ensembles de nombres sont\u2009:

    • \\(\\mathbb{N}\\) : ensemble des entiers naturels (0, 1, 2, 3, ...)
    • \\(\\mathbb{Z}\\) : ensemble des entiers relatifs (..., -3, -2, -1, 0, 1, 2, 3, ...)
    • \\(\\mathbb{D}\\) : ensemble des d\u00e9cimaux (-0.1, 0, 0.1, 0.2, 0.3, ...)
    • \\(\\mathbb{Q}\\) : ensemble des rationnels (0, 1, \u00bd, \u2153, \u00bc, ...)
    • \\(\\mathbb{R}\\) : ensemble des r\u00e9els (\\(\\pi\\), \\(\\sqrt{2}\\), ...)
    • \\(\\mathbb{C}\\) : ensemble des complexes (\\(i\\), \\(1 + i\\), ...)
    • \\(\\mathbb{H}\\) : ensemble des quaternions (\\(1 + i + j + k\\), ...)
    • \\(\\mathbb{O}\\) : ensemble des octonions
    • \\(\\mathbb{S}\\) : ensemble des s\u00e9d\u00e9nions

    Quaternions, octonions et s\u00e9d\u00e9nions

    Les quaternions, octonions et s\u00e9d\u00e9nions sont des nombres hypercomplexes qui g\u00e9n\u00e9ralisent les nombres complexes. Ils sont utilis\u00e9s en physique pour d\u00e9crire les rotations dans l'espace.

    Les quaternions sont utilis\u00e9s en informatique pour repr\u00e9senter les rotations en 3D. Les octonions et s\u00e9d\u00e9nions sont des g\u00e9n\u00e9ralisations des quaternions, mais ils sont moins utilis\u00e9s en pratique.

    \u00c0 chaque fois que s'\u00e9loigne du r\u00e9el (et c'est une mani\u00e8re amusante de le dire), on perd des propri\u00e9t\u00e9s int\u00e9ressantes. Les nombres complexes ne sont pas ordonn\u00e9s, les quaternions ne sont pas commutatifs, les octonions ne sont pas associatifs, et les s\u00e9d\u00e9nions ne sont m\u00eame pas alternatifs. Un nombre alternatif est un nombre pour lequel la formule suivante est v\u00e9rifi\u00e9e\u2009:

    \\[ (a \\cdot a) \\cdot b = a \\cdot (a \\cdot b) \\]

    En pratique dans une carri\u00e8re d'ing\u00e9nieur, vous n'aurez jamais \u00e0 manipuler ni des quaternions, ni des octonions ou des s\u00e9d\u00e9nions. Les nombres complexes sont n\u00e9anmoins une extension des nombres r\u00e9els qui sont utilis\u00e9s en physique et en math\u00e9matiques et qui peuvent \u00eatre utilis\u00e9s en C sous certaines conditions.

    Archim\u00e8de disait\u2009: \u0394\u03cc\u03c2 \u03bc\u03bf\u03b9 \u03c0\u1fb6 \u03c3\u03c4\u1ff6 \u03ba\u03b1\u1f76 \u03c4\u1f70\u03bd \u03b3\u1fb6\u03bd \u03ba\u03b9\u03bd\u03ac\u03c3\u03c9 (donnez-moi un point d'appui et je soul\u00e8verai le monde). Le Cr\u00e9ateur, s'il existe, aurait pu dire\u2009: donnez-moi un nombre et je vous construirai un univers\u2009! Bien entendu la quantit\u00e9 d'information dans l'univers est gargantuesque, elle cro\u00eet avec l'entropie et donc avec le temps qui passe, mais \u00e0 sa gen\u00e8se \u00e0 l'origine du temps et de l'espace, il n'est pas impensable que l'univers ait pu \u00eatre cr\u00e9\u00e9 \u00e0 partir d'un nombre. C'est une id\u00e9e qui a \u00e9t\u00e9 explor\u00e9e par Stephen Wolfram dans son livre A New Kind of Science. Cette vision repose sur l'id\u00e9e que l'univers pourrait \u00eatre vu comme une sorte de syst\u00e8me informatique ou algorithmique, o\u00f9 des lois fondamentales simples \u00e9voluent pour produire la diversit\u00e9 des ph\u00e9nom\u00e8nes que nous observons.

    Dans le jeu Minecraft, lorsque vous cr\u00e9ez un monde, vous pouvez utiliser une graine pour g\u00e9n\u00e9rer un monde al\u00e9atoire. Cette graine est un nombre fini qui sert de base \u00e0 l'algorithme de g\u00e9n\u00e9ration de monde. Si vous utilisez la m\u00eame graine, vous obtiendrez le m\u00eame monde. La graine -5584399987456711267 permet par exemple d'obtenir de merveilleux cerisiers en fleurs qui rappelle la saison de Sakura \u00e0 Kyoto. Mais pour que cela fonctionne il vous faut le code source de Minecraft, lui aussi c'est une succession de 0 et de 1, et donc c'est un nombre, lui aussi fini.

    Monde correspondant \u00e0 la graine -5584399987456711267

    Lorsque vous jouez, vos actions g\u00e9n\u00e8rent de l'information qui influence le monde, et donc la quantit\u00e9 d'information dans le monde cro\u00eet avec l'entropie que vous injectez dans le syst\u00e8me. C'est pour cela que plus vous jouez, plus la sauvegarde de votre monde devient grande, mais vous pouvez toujours la repr\u00e9senter aussi avec un nombre fini\u2009: une succession de 0 et de 1.

    \u00c0 noter que les m\u00e9moires des ordinateurs ne sont pas infinies, elles sont limit\u00e9es par la quantit\u00e9 de transistors qui les composent. Il n'est donc pas possible d'y stocker n'importe quel nombre. \\(\\pi\\) ne peut pas \u00eatre stock\u00e9 en m\u00e9moire, mais une approximation de \\(\\pi\\) peut l'\u00eatre. Aussi, l'informatique impose certaines limitations sur les nombres que l'on peut manipuler. Les nombres entiers sont les plus simples \u00e0 manipuler, mais ils sont limit\u00e9s par la taille de la m\u00e9moire et la mani\u00e8re dont on les enregistre en m\u00e9moire. Il est donc utile de se fixer des limites, de d\u00e9finir des bornes en fonction de l'usage que l'on veut en faire. La graine de Minecraft est par exemple un nombre de 64 bits et c'est un nombre entier.

    ", "tags": ["minecraft", "entiers-relatifs", "sedenions", "sakura", "monde", "a-new-kind-of-science", "nombres-complexes", "kyoto", "stephen-wolfram", "octonions", "quaternions", "nombres-hypercomplexes", "archimede", "entiers-naturels", "graine"]}, {"location": "course-c/10-numeration/numbers/#entiers-naturels", "title": "Entiers naturels", "text": "

    En math\u00e9matiques, un entier naturel est un nombre positif ou nul. Chaque nombre \u00e0 un successeur unique et peut s'\u00e9crire avec une suite finie de chiffres en notation d\u00e9cimale positionnelle, et donc sans signe et sans virgule. L'ensemble des entiers naturels est d\u00e9fini de la fa\u00e7on suivante\u2009:

    \\[ \\mathbb{N} = {0, 1, 2, 3, ...} \\]

    Les entiers sont les premiers types de donn\u00e9es manipul\u00e9s par les ordinateurs. Ils sont stock\u00e9s en m\u00e9moire sous forme de bits. En choisissant la taille de stockage des entiers, on d\u00e9termine la plage de valeurs que l'on peut repr\u00e9senter. Un entier de 8 bits peut par exemple repr\u00e9senter \\(2^8 = 256\\) valeurs diff\u00e9rentes, de 0 \u00e0 255. Un entier de 16 bits peut quant \u00e0 lui repr\u00e9senter \\(2^{16} = 65536\\) valeurs diff\u00e9rentes, de 0 \u00e0 65535. \u00c0 chaque bit suppl\u00e9mentaire, on double la plage de valeurs repr\u00e9sentables.

    Exemple

    Le nombre 142 peut s'\u00e9crire sur 8 bits en binaire, avec une notation positionnelle (o\u00f9 les bits sont align\u00e9s par poids d\u00e9croissants) on peut \u00e9crire\u2009:

    \\[ \\begin{array}{cccccccc} 2^7 & 2^6 & 2^5 & 2^4 & 2^3 & 2^2 & 2^1 & 2^0 \\\\ 1 & 0 & 0 & 0 & 1 & 1 & 1 & 0 \\\\ \\end{array} \\]

    La taille de stockage d'un entier d\u00e9termine donc ses limites. Si cette mani\u00e8re est \u00e9l\u00e9gante, elle ne permet h\u00e9las pas de repr\u00e9senter des valeurs n\u00e9gatives. Pour cela, on aura recours aux entiers relatifs.

    "}, {"location": "course-c/10-numeration/numbers/#entiers-relatifs", "title": "Entiers relatifs", "text": "

    Math\u00e9matiquement un entier relatif appartient \u00e0 l'ensemble \\(\\mathbb{Z}\\):

    \\[ \\mathbb{Z} = {..., -3, -2, -1, 0, 1, 2, 3, ...} \\]

    Vous le savez maintenant, l'interpr\u00e9tation d'une valeur binaire n'est possible qu'en ayant connaissance de son encodage et s'agissant d'entiers, on peut se demander comment stocker des valeurs n\u00e9gatives, car manque une information permettant d'encoder le symbole pour le signe - (ni m\u00eame d'ailleurs +).

    Une approche na\u00efve serait de r\u00e9server une partie de la m\u00e9moire pour des entiers positifs et une autre pour des entiers n\u00e9gatifs et stocker la correspondance binaire/d\u00e9cimale simplement. Un peu comme si vous aviez deux bo\u00eetes chez vous, l'une pour les choses qui se mangent (le frigo) et une pour les choses qui ne se mangent plus (la poubelle).

    L'ennui pour les variables c'est que le contenu peut changer et qu'un nombre n\u00e9gatif pourrait tr\u00e8s bien devenir positif apr\u00e8s un calcul. Il faudrait alors le d\u00e9placer d'une r\u00e9gion m\u00e9moire \u00e0 une autre. Ce n'est donc pas la meilleure m\u00e9thode.

    On pourrait alors renseigner la nature du nombre, c'est-\u00e0-dire son signe avec sa valeur.

    ", "tags": ["nombre-negatif", "encodage"]}, {"location": "course-c/10-numeration/numbers/#bit-de-signe", "title": "Bit de signe", "text": "

    Pourquoi ne pas se r\u00e9server un bit de signe, par exemple le 8e bit de notre nombre de 8 bits, pour indiquer si le nombre est positif ou n\u00e9gatif\u2009? C'est cet exemple qui est montr\u00e9 ici\u2009:

    \u250c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u2502\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = (0 * (-1)) * 0b1010011 = 83\n\u2514\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25021\u2502\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = (1 * (-1)) * 0b1010011 = -83\n\u2514\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Cette m\u00e9thode impose le sacrifice d'un bit et donc l'intervalle repr\u00e9sentable est n'est plus que de [-127..127]. N\u00e9anmoins, elle pr\u00e9sente un autre inconv\u00e9nient majeur\u2009: la repr\u00e9sentation du z\u00e9ro.

    Dans cette repr\u00e9sentation, il existe deux z\u00e9ros\u2009: le z\u00e9ro n\u00e9gatif 0b00000000, et le z\u00e9ro positif 0b10000000 ce qui peut poser des probl\u00e8mes pour les comparaisons. Est-ce que \\(0\\) est \u00e9gal \\(-0\\) ? En un sens oui, mais en termes de l'information stock\u00e9e, ce n'est pas le m\u00eame nombre.

    En termes de calculs, l'addition ne fonctionne plus si on raisonne sur les bits. Car si on additionne au z\u00e9ro positif (0b10000000) la valeur 1 on aura 1, mais si on additionne au z\u00e9ro n\u00e9gatif (0b00000000) la valeur 1 on obtiendra -1 et c'est un peu d\u00e9routant\u2009:

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n\n000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500>  \u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500> M\u00e9thode du bit de signe\n 0     1     2     3     0    -1    -2    -3\n

    Il faudrait donc trouver une m\u00e9thode qui permettrait de conserver la possibilit\u00e9 de faire les op\u00e9rations directement en binaire. En d'autres termes, on souhaiterait pouvoir calculer en base deux sans se soucier du signe\u2009:

      00000010 (2)\n- 00000101 (5)\n----------\n  11111101 (-125)    2 - 5 != -125\n

    Si on r\u00e9sume, la solution propos\u00e9e qui utilise un bit de signe pose deux probl\u00e8mes\u2009:

    1. Les op\u00e9rations ne sont plus triviales, et un algorithme particulier doit \u00eatre mis en place pour les g\u00e9rer.
    2. Le double z\u00e9ro (positif et n\u00e9gatif) est g\u00eanant.
    ", "tags": ["addition", "bit-de-signe", "zero"]}, {"location": "course-c/10-numeration/numbers/#complement-a-un", "title": "Compl\u00e9ment \u00e0 un", "text": "

    Le compl\u00e9ment \u00e0 un est une m\u00e9thode plus maline utilis\u00e9e dans les premiers ordinateurs comme le CDC 6600 (1964) ou le UNIVAC 1107 (1962). Il existe \u00e9galement un bit de signe, mais il est implicite.

    Le compl\u00e9ment \u00e0 un tire son nom de sa d\u00e9finition g\u00e9n\u00e9rique nomm\u00e9e radix-complement ou compl\u00e9ment de base et s'exprime par\u2009:

    \\[ b^n - y \\]

    o\u00f9

    \\(b\\)

    La base du syst\u00e8me positionnel utilis\u00e9

    \\(n\\)

    Le nombre de chiffres maximal du nombre consid\u00e9r\u00e9

    \\(y\\)

    La valeur \u00e0 compl\u00e9menter.

    Ainsi, il est facile d'\u00e9crire le compl\u00e9ment \u00e0 neuf d'un nombre en base dix, car on s'arrange pour que chaque chiffre composant le nombre on trouve un autre chiffre dont la somme est \u00e9gale \u00e0 neuf.

      0 1 2 3 4 5 6 7 8 9\n          |\n          | Compl\u00e9ment \u00e0 9\n          v\n+ 9 8 7 6 5 4 3 2 1 0\n  -------------------\n  9 9 9 9 9 9 9 9 9 9\n

    On notera avec beaucoup d'int\u00e9r\u00eat qu'un calcul est possible avec cette m\u00e9thode. Sur l'exemple suivant, \u00e0 gauche, on montre une soustraction classique, \u00e0 droite on remplace la soustraction par une addition ainsi que les valeurs n\u00e9gatives par leur compl\u00e9ment \u00e0 9. Le r\u00e9sultat 939 correspond apr\u00e8s compl\u00e9ment \u00e0 un \u00e0 60.

      150      150\n- 210    + 789\n-----    -----\n  -60      939\n

    Notons que le cas pr\u00e9cis de l'inversion des chiffres correspond au compl\u00e9ment de la base, moins un. L'inversion des bits binaire est donc le compl\u00e9ment \u00e0 \\((2-1) = 1\\).

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n\n000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500> <\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500 compl\u00e9ment \u00e0 un\n 0     1     2     3    -3    -2    -1     0\n

    Reprenons l'exemple pr\u00e9c\u00e9dent de soustraction, on notera que l'op\u00e9ration fonctionne en soustrayant 1 au r\u00e9sultat du calcul.

      00000010 (2)\n+ 11111010 (-5)\n----------\n  11111101 (-3)\n-        1\n----------\n  11111011 (-4)\n

    Pour r\u00e9sumer les avantages et inconv\u00e9nients du compl\u00e9ment \u00e0 un\u2009:

    1. Les op\u00e9rations redeviennent presque triviales, mais il est n\u00e9cessaire de soustraire 1 au r\u00e9sultat (c'est dommage).
    2. Le double z\u00e9ro (positif et n\u00e9gatif) est g\u00eanant.

    ", "tags": ["complement-a-un", "1964", "complement-a-neuf", "1962"]}, {"location": "course-c/10-numeration/numbers/#complement-a-deux", "title": "Compl\u00e9ment \u00e0 deux", "text": "

    Le compl\u00e9ment \u00e0 deux n'est rien d'autre que le compl\u00e9ment \u00e0 un plus un. C'est donc une amusante plaisanterie des informaticiens. Car dans un syst\u00e8me binaire, le nombre de symboles et de 2 (0 et 1). On ne peut pas trouver un chiffre tel que la somme donne 2. C'est la m\u00eame id\u00e9e que de demander le compl\u00e9ment \u00e0 10 en base 10. Vous ne pouvez pas sur la base d'un chiffre unique obtenir un autre chiffre dont la somme est \u00e9gale \u00e0 10 sans avoir recours \u00e0 un autre chiffre.

    Pour r\u00e9aliser ce compl\u00e9ment \u00e0 deux (compl\u00e9ment \u00e0 un plus un), il y a deux \u00e9tapes\u2009:

    1. Calculer le compl\u00e9ment \u00e0 un du nombre d'entr\u00e9es.
    2. Ajouter 1 au r\u00e9sultat.

    Oui, et alors, en quoi cela change le Schmilblick ? Surprenamment, on r\u00e9sout tous les probl\u00e8mes amen\u00e9s par le compl\u00e9ment \u00e0 un. Prenons les diff\u00e9rentes repr\u00e9sentations que nous avons vues jusqu'\u00e0 pr\u00e9sent\u2009:

    000   001   010   011   100   101   110   111\n\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500>\n 0     1     2     3     4     5     6     7     sans compl\u00e9ment\n 0     1     2     3     0    -1    -2    -3     avec bit de signe\n 0     1     2     3    -3    -2    -1     0     compl\u00e9ment \u00e0 un\n 0     1     2     3    -4    -3    -2    -1     compl\u00e9ment \u00e0 deux\n

    On peut \u00e9galement les repr\u00e9senter sous forme d'un cercle comme illustr\u00e9 sur la figure suivante\u2009:

    Cercle des nombres

    Avec le bit de signe, on observe deux ruptures dans la continuit\u00e9 de la repr\u00e9sentation. Un saut de 3,0 et un autre -3,0. Avec le compl\u00e9ment \u00e0 un, on n'observe toujours deux sauts 0,0 et -3,-3. Avec le compl\u00e9ment \u00e0 deux, on n'observe plus qu'un seul saut 3, -4, et la continuit\u00e9 est assur\u00e9e de -1 \u00e0 0. Par ailleurs, le z\u00e9ro n'a plus de double repr\u00e9sentation.

    Au niveau du calcul, l'addition et la soustraction fonctionnent de mani\u00e8re identique. Prenons l'exemple de la soustraction suivante\u2009:

      2        00000010\n- 5      + 11111011   (~0b101 + 1 == 0b11111011)\n---     -----------\n -3        11111101   (~0b11111101 + 1 == 0b11 == 3)\n

    Cette notation est donc tr\u00e8s \u00e9l\u00e9gante, car\u2009:

    1. Les op\u00e9rations sont triviales.
    2. Le probl\u00e8me du double z\u00e9ro est r\u00e9solu.
    3. On gagne une valeur n\u00e9gative [-128..+127] contre [-127..+127] avec les m\u00e9thodes pr\u00e9c\u00e9demment \u00e9tudi\u00e9es.

    Vous l'aurez compris, le compl\u00e9ment \u00e0 deux est bien le m\u00e9canisme de repr\u00e9sentation des nombres n\u00e9gatifs qui a \u00e9t\u00e9 retenu par les informaticiens, et le plus utilis\u00e9 de nos jours dans les ordinateurs. Gardez cependant \u00e0 l'esprit que ces m\u00e9canismes ne sont qu'une interpr\u00e9tation d'un contenu binaire stock\u00e9 en m\u00e9moire.

    ", "tags": ["complement-a-deux"]}, {"location": "course-c/10-numeration/numbers/#les-nombres-reels", "title": "Les nombres r\u00e9els", "text": "

    Math\u00e9matiquement, les nombres r\u00e9els \\(\\mathbb{R}\\), sont des nombres qui peuvent \u00eatre repr\u00e9sent\u00e9s par une partie enti\u00e8re, et une liste finie ou infinie de d\u00e9cimales. En informatique, stocker une liste infinie de d\u00e9cimale demanderait une quantit\u00e9 infinie de m\u00e9moire et donc, la pr\u00e9cision arithm\u00e9tique est contrainte.

    Au d\u00e9but de l'\u00e8re des ordinateurs, il n'\u00e9tait possible de stocker que des nombres entiers, mais le besoin de pouvoir stocker des nombres r\u00e9els s'est rapidement fait sentir et la transition s'est faite progressivement. D'abord par l'apparition de la virgule fixe, puis par la virgule flottante.

    Le premier ordinateur avec une capacit\u00e9 de calcul en virgule flottante date de 1942 (ni vous ni moi n'\u00e9tions probablement n\u00e9s) avec le Zuse's Z4, du nom de son inventeur Konrad Zuse.

    Attardons-nous un peu sur ces concepts de virgule fixe et de virgule flottante.

    ", "tags": ["virgule-flottante", "virgule-fixe"]}, {"location": "course-c/10-numeration/numbers/#virgule-fixe", "title": "Virgule fixe", "text": "

    Pour illustrer notre propos, prenons l'exemple d'un nombre entier exprim\u00e9 sur 8-bits, on peut admettre facilement que bien qu'il s'agisse d'un nombre entier, une virgule pourrait \u00eatre ajout\u00e9e au bit z\u00e9ro sans en modifier sa signification. Dans cet exemple, ajoutons une virgule \u00e0 la position 0\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2^6 + 2^4 + 2^1 + 2^0 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n                , / 2^0     ----> 83 / 1 = 83\n

    Imaginons \u00e0 pr\u00e9sent que nous d\u00e9placions cette virgule virtuelle de trois \u00e9l\u00e9ments sur la gauche. En admettant que deux ing\u00e9nieurs se mettent d'accord pour consid\u00e9rer ce nombre 0b01010011 avec une virgule fixe positionn\u00e9e \u00e0 droite du quatri\u00e8me bit, l'interpr\u00e9tation de cette grandeur serait alors la valeur enti\u00e8re divis\u00e9e par 8 (\\(2^3\\)). On parvient alors \u00e0 exprimer une grandeur r\u00e9elle comportant une partie d\u00e9cimale\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2076 + 2\u2074 + 2\u00b9 + 2\u2070 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n          ,       / 2\u00b3     ----> 83 / 8 = 10.375\n

    Cependant, il manque une information. Un ordinateur, sans yeux et sans bon sens, est incapable sans information additionnelle d'interpr\u00e9ter correctement la position de la virgule puisque sa position n'est encod\u00e9e nulle part dans le nombre. En outre, puisque la position de cette virgule est dans l'intervalle [0..7], il serait possible d'utiliser trois bits suppl\u00e9mentaires \u00e0 cette fin\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2076 + 2\u2074 + 2\u00b9 + 2\u2070 = 64 + 16 + 2 + 1 = 83\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n          \u250c\u2500\u252c\u2500\u252c\u2500\u2510\n          \u25020\u25021\u25021\u2502 / 2\u00b3     ----> 83 / 8 = 10.375\n          \u2514\u2500\u2534\u2500\u2534\u2500\u2518\n

    Cette solution est \u00e9l\u00e9gante, mais demande \u00e0 pr\u00e9sent 11-bits contre 8-bits initialement. Un ordinateur n'\u00e9tant dou\u00e9 que pour manipuler des paquets de bits souvent sup\u00e9rieurs \u00e0 8, il faudrait soit \u00e9tendre inutilement le nombre de bits utilis\u00e9s pour la position de la virgule \u00e0 8, soit tenter d'int\u00e9grer cette information, dans les 8-bits initiaux.

    "}, {"location": "course-c/10-numeration/numbers/#virgule-flottante", "title": "Virgule flottante", "text": "

    Depuis l'exemple pr\u00e9c\u00e9dent, imaginons que l'on sacrifie 3 bits sur les 8 pour encoder l'information de la position de la virgule. Appelons l'espace r\u00e9serv\u00e9 pour positionner la virgule l' exposant et le reste de l'information la mantisse, qui en math\u00e9matique repr\u00e9sente la partie d\u00e9cimale d'un logarithme (\u00e0 ne pas confondre avec la mantis shrimp, une quille ou crevette-mante boxeuse aux couleurs particuli\u00e8rement chatoyantes).

      exp.  mantisse\n\u251e\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 2\u2074 + 2\u00b9 + 2\u2070 = 16 + 2 + 1 = 19\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> / 2\u00b9 ----> 19 / 2 = 9.5\n

    Notre construction nous permet toujours d'exprimer des grandeurs r\u00e9elles, mais avec ce sacrifice, il n'est maintenant plus possible d'exprimer que les grandeurs comprises entre \\(1\\cdot2^{7}=0.0078125\\) et \\(63\\). Ce probl\u00e8me peut \u00eatre ais\u00e9ment r\u00e9solu en augmentant la profondeur m\u00e9moire \u00e0 16 ou 32-bits. Ajoutons par ailleurs que cette solution n'est pas \u00e0 m\u00eame d'exprimer des grandeurs n\u00e9gatives.

    Poursuivons notre raisonnement. Cette fois-ci nous choisissons d'\u00e9tendre notre espace de stockage \u00e0 4 octets. Un bit de signe est r\u00e9serv\u00e9 pour exprimer les grandeurs n\u00e9gatives, 8 bits pour l'exposant et 23 bits pour la mantisse :

     \u250c Signe 1 bit\n \u2502        \u250c Exposant 8 bits\n \u2502        \u2502                             \u250c Mantisse 23 bits\n \u2534 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25020\u25021\u25020\u25020\u25021\u25020\u25020\u25020\u2502\u25021\u25021\u25020\u25021\u25021\u25021\u25021\u25021\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25020\u25021\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Peu \u00e0 peu, nous nous rapprochons du Standard for Floating-Point Arithmetic (IEEE 754). La formule de base est la suivante\u2009:

    \\[ x = s\\cdot b^e\\sum_{k=1}^p f_k\\cdot b^{-k},\\; e_{\\text{min}} \\le e \\le e_{\\text{max}} \\]

    Avec\u2009:

    \\(s\\)

    Signe (\\(\\pm1\\))

    \\(b\\)

    Base de l'exposant, un entier \\(>1\\).

    \\(e\\)

    Exposant, un entier entre \\(e_\\text{min}\\) et \\(e_\\text{max}\\)

    \\(p\\)

    Pr\u00e9cision, nombre de digits en base \\(b\\) de la mantisse

    \\(f_k\\)

    Entier non n\u00e9gatif plus petit que la base \\(b\\).

    \u00c9tant donn\u00e9 que les ordinateurs sont plus \u00e0 l'aise \u00e0 la manipulation d'entr\u00e9es binaire, la base est 2 et la norme IEEE nomme ces nombres binary16, binary32 ou binary64, selon le nombre de bits utilis\u00e9 pour coder l'information. Les termes de Single precision ou Double precision sont aussi couramment utilis\u00e9s.

    Les formats support\u00e9 par un ordinateur ou qu'un microcontr\u00f4leur \u00e9quip\u00e9 d'une unit\u00e9 de calcul en virgule flottante (FPU pour Floating point unit) sont les suivants\u2009:

    Formats de nombres en virgule flottante IEEE-754 Exposant Mantisse Signe binary32 8 bits 23 bits 1 bit binary64 11 bits 52 bits 1 bit

    Il est temps de faire quelques observations\u2009:

    • une valeur encod\u00e9e en virgule flottante sera toujours une approximation d'une grandeur r\u00e9elle\u2009;
    • la pr\u00e9cision est d'autant plus grande que le nombre de bits de la mantisse est grand\u2009;
    • la base ayant \u00e9t\u00e9 fix\u00e9e \u00e0 2, il est possible d'exprimer \\(1/1024\\) sans erreur de pr\u00e9cision, mais pas \\(1/1000\\) ;
    • un ordinateur qui n'est pas \u00e9quip\u00e9 d'une FPU sera beaucoup plus lent (10 \u00e0 100x) pour faire des calculs en virgule flottante\u2009;
    • bien que le standard C99 d\u00e9finisse les types virgule flottante float, double et long double, ils ne d\u00e9finissent pas la pr\u00e9cision avec laquelle ces nombres sont exprim\u00e9s, car cela d\u00e9pend de l'architecture du processeur utilis\u00e9.
    ", "tags": ["double", "binary64", "mantisse", "binary32", "float", "exposant", "binary16"]}, {"location": "course-c/10-numeration/numbers/#simple-precision", "title": "Simple pr\u00e9cision", "text": "

    Le type float aussi dit \u00e0 pr\u00e9cision simple utilise un espace de stockage de 32-bits organis\u00e9 en 1 bit de signe, 8 bits pour l'exposant et 23 bits pour la mantisse. Les valeurs pouvant \u00eatre exprim\u00e9es sont de\u2009:

    • \\(\\pm\\inf\\) lorsque l'exposant vaut 0xff
    • \\((-1)^{\\text{sign}}\\cdot2^{\\text{exp} - 127}\\cdot1.\\text{mantisse}\\)
    • \\(0\\) lorsque la mantisse vaut 0x00000

    Voici quelques exemples. Tout d'abord, la valeur de 1.0 est encod\u00e9e de la mani\u00e8re suivante\u2009:

    \\[ \\begin{aligned} 0\\:01111111\\:00000000000000000000000_2 &= \\text{3f80}\\: \\text{0000}_{16} \\\\ &= (-1)^0 \\cdot 2^{127-127} \\cdot \\frac{(2^{23} + 0)}{2^{23}} \\\\ &= 2^{0} \\cdot 1.0 = 1.0 \\end{aligned} \\]

    La valeur positive maximale exprimable est lorsque l'exposant vaut 0xfe et la mantisse 0x7fffff :

    \\[ \\begin{aligned} 0\\:11111110\\:11111111111111111111111_2 &= \\text{7f7f}\\: \\text{ffff}_{16} \\\\ &= (-1)^0 \\cdot 2^{254-127} \\cdot \\frac{(2^{23} + 838'607)}{2^{23}} \\\\ &\u2248 2^{127} \\cdot 1.9999998807 \\\\ &\u2248 3.4028234664 \\cdot 10^{38} \\end{aligned} \\]

    La valeur de \\(-\\pi\\) (pi) est\u2009:

    \\[ \\begin{aligned} 1\\:10000000\\:10010010000111111011011_2 &= \\text{4049}\\: \\text{0fdb}_{16} \\\\ &= (-1)^1 \\cdot 2^{128-127} \\cdot \\frac{(2^{23} + 4'788'187)}{2^{23}} \\\\ &\u2248 -1 \\cdot 2^{1} \\cdot 1.5707963 \\\\ &\u2248 -3.14159274101 \\end{aligned} \\]

    On peut encore noter quelques valeurs particuli\u00e8res\u2009:

    0 00000000 00000000000000000000000\u2082 \u2261 0000 0000\u2081\u2086 \u2261 0\n0 11111111 00000000000000000000000\u2082 \u2261 7f80 0000\u2081\u2086 \u2261 inf\n1 11111111 00000000000000000000000\u2082 \u2261 ff80 0000\u2081\u2086 \u2261 \u2212inf\n

    D\u00e9passement de capacit\u00e9

    Il ne faut pas oublier que la repr\u00e9sentation des nombres en virgule flottante n'est pas exacte et il est possible de d\u00e9passer la capacit\u00e9 de stockage d'un nombre en virgule flottante. Quant \u00e0 la pr\u00e9cision maximale d'un nombre en virgule flottante, il d\u00e9pend de sa mantisse.

    Par exemple, si l'on souhaite r\u00e9aliser un int\u00e9grateur simple, nous disposons d'un compteur u initialis\u00e9 \u00e0 1.0. \u00c0 chaque it\u00e9ration, on incr\u00e9mente u de 1.0. Lorsque la valeur cesse de cro\u00eetre, on affiche la valeur de u.

    #include <stdio.h>\nint main() {\n    float u = 1.0, v;\n    do { v = u++; } while (u > v);\n    printf(\"%f\\n\", u);\n}\n

    Vous pourriez vous attendre \u00e0 ce que le programme tourne \u00e0 l'infini, o\u00f9 du moins jusqu'\u00e0 une limite tr\u00e8s grande, mais en r\u00e9alit\u00e9, il s'arr\u00eate \u00e0 16777216.0. C'est parce que la pr\u00e9cision de la mantisse est de 23 bits, et que le nombre 16777217.0 est le premier nombre entier qui ne peut pas \u00eatre repr\u00e9sent\u00e9 avec une pr\u00e9cision de 23 bits.

    Les nombres subnormaux

    On l'a vu un nombre en virgule flottante simple pr\u00e9cision s'\u00e9crit sous la forme\u2009:

    \\[ (-1)^s \\times (1.m) \\times 2^{(e - Bias)} \\]

    Les nombres subnormaux sont des nombres qui ne respectent pas la norme IEEE 754, mais qui sont tout de m\u00eame repr\u00e9sentables. Ils sont utilis\u00e9s pour repr\u00e9senter des nombres tr\u00e8s petits, proches de z\u00e9ro. En effet, la norme IEEE 754 impose que le premier bit de la mantisse soit toujours \u00e9gal \u00e0 1, ce qui implique que le nombre 0 ne peut pas \u00eatre repr\u00e9sent\u00e9. Les nombres subnormaux permettent de repr\u00e9senter des nombres tr\u00e8s proches de z\u00e9ro, en diminuant la pr\u00e9cision de la mantisse.

    ", "tags": ["float"]}, {"location": "course-c/10-numeration/numbers/#double-precision", "title": "Double pr\u00e9cision", "text": "

    La double pr\u00e9cision est similaire \u00e0 la simple pr\u00e9cision, mais avec une mantisse \u00e0 52 bits et 11 bits d'exposants. Le nombre est donc repr\u00e9sentable sur 64 bits. La valeur maximale est de \\(1.7976931348623157 \\times 10^{308}\\) et la valeur minimale de \\(2.2250738585072014 \\times 10^{-308}\\). La r\u00e9solution en nombre de chiffres significatifs est de 15 \u00e0 16 chiffres contre 6 \u00e0 7 pour la simple pr\u00e9cision. Cette notation est donc tr\u00e8s pertinente pour les calculs scientifiques, mais elle requiert aussi plus de m\u00e9moire.

    Exercice 1\u2009: Expressions arithm\u00e9tiques flottantes

    Donnez la valeur des expressions ci-dessous\u2009:

    25. + 10. + 7. \u2013 3.\n5. / 2.\n24. + 5. / 2.\n25. / 5. / 2.\n25. / (5. / 2.)\n2. * 13. % 7.\n1.3E30 + 1.\n
    "}, {"location": "course-c/10-numeration/numbers/#quadruple-precision", "title": "Quadruple pr\u00e9cision", "text": "

    Bien que ce soit marginal dans le monde de l'informatique, la quadruple pr\u00e9cision est une norme d\u00e9finie dans IEEE 754 qui utilise 128 bits pour stocker les nombres r\u00e9els. Elle est utilis\u00e9e pour des calculs scientifiques n\u00e9cessitant une tr\u00e8s grande pr\u00e9cision comme au CERN ou pour l'\u00e9tude de mod\u00e8les cosmologiques. La quadruple pr\u00e9cision offre une pr\u00e9cision de 34 chiffres significatifs, soit environ 112 bits de pr\u00e9cision.

    Seul un nombre r\u00e9duit de langages de programmation peut g\u00e9rer nativement cette notation, et la grande majorit\u00e9 des processeurs n'est pas pr\u00e9vue pour les traiter efficacement. Il est n\u00e9anmoins possible de l'utiliser avec certains compilateurs C comme GCC en utilisant le type __float128 de la biblioth\u00e8que <quadmath.h>.

    Lenteurs

    Utiliser la quadruple pr\u00e9cision ralenti consid\u00e9rablement les calculs, car les processeurs actuels ne sont pas optimis\u00e9s pour travailler sur 128 bits. Un processeur peut faire des calculs sur 64 bits en une seule op\u00e9ration, mais pour des calculs en quadruple pr\u00e9cision, l'effort est consid\u00e9rablement plus grand.

    ", "tags": ["cern", "quadruple-precision", "ieee-754", "__float128"]}, {"location": "course-c/10-numeration/numbers/#nombres-complexes", "title": "Nombres complexes", "text": "

    En C, il est possible de d\u00e9finir des nombres complexes en utilisant le type complex de la biblioth\u00e8que <complex.h>. Les nombres complexes sont compos\u00e9s de deux parties, la partie r\u00e9elle et la partie imaginaire. Ils sont souvent utilis\u00e9s en math\u00e9matiques pour repr\u00e9senter des nombres qui ne peuvent pas \u00eatre exprim\u00e9s avec des nombres r\u00e9els. Ils ont \u00e9t\u00e9 introduits avec la version C99 du standard C.

    N\u00e9anmoins les nombres complexes ne sont pas support\u00e9s par les op\u00e9rateurs du langage, il est donc n\u00e9cessaire d'utiliser des fonctions sp\u00e9cifiques pour effectuer des op\u00e9rations complexes.

    Note

    Dans des langages plus haut niveau comme le C++, le C# ou Python, les nombres complexes sont support\u00e9s nativement.

    Exemple en Python\u2009:

    from math import sqrt\na, b, c = 1, 2, 3\ndelta = b**2 - 4*a*c # Calcul du discriminant qui sera n\u00e9gatif\nx1, x1 = (-b + sqrt(delta)) / (2*a), (-b - sqrt(delta)) / (2*a)\n

    x1 et x2 sont des nombres complexes.

    Voici un exemple en C\u2009:

    #include <stdio.h>\n#include <complex.h>\n\nint main() {\n    double complex z1 = 1.0 + 2.0*I;\n    double complex z2 = 3.0 + 4.0*I;\n\n    printf(\"z1 = %.1f + %.1fi\\n\", creal(z1), cimag(z1));\n    printf(\"z2 = %.1f + %.1fi\\n\", creal(z2), cimag(z2));\n\n    double complex sum = z1 + z2;\n    double complex product = z1 * z2;\n\n    printf(\"sum = %.1f + %.1fi\\n\", creal(sum), cimag(sum));\n    printf(\"product = %.1f + %.1fi\\n\", creal(product), cimag(product));\n\n    return 0;\n}\n
    ", "tags": ["complex"]}, {"location": "course-c/10-numeration/numbers/#format-q-virgule-fixe", "title": "Format Q (virgule fixe)", "text": "

    Le format Q est une notation en virgule fixe dans laquelle le format d'un nombre est repr\u00e9sent\u00e9 par la lettre Q suivie de deux nombres\u2009:

    1. Le nombre de bits entiers.
    2. Le nombre de bits fractionnaires.

    Ainsi, un registre 16 bits contenant un nombre allant de +0.999 \u00e0 -1.0 s'exprimera Q1.15 soit 1 + 15 valant 16 bits.

    Pour exprimer la valeur pi (3.1415...) il faudra au minimum 3 bits pour repr\u00e9senter la partie enti\u00e8re, car le bit de signe doit rester \u00e0 z\u00e9ro. Le format sur 16 bits sera ainsi Q4.12.

    La construction de ce nombre est facile\u2009:

    1. Prendre le nombre r\u00e9el \u00e0 encoder (\\(3.1415926535\\))
    2. Le multiplier par 2 \u00e0 la puissance du nombre de bits (\\(2^{12} * 3.1415926535 = 12867.963508736\\))
    3. Prendre la partie enti\u00e8re (\\(12867\\))

    Pour convertir un nombre Q4.12 en sa valeur r\u00e9elle il faut\u2009:

    1. Prendre le nombre encod\u00e9 en Q4.12 (\\(12867\\))
    2. Diviser sa valeur 2 \u00e0 la puissance du nombre de bits (\\(12867 / 2^{12} = 3.141357421875\\))

    On peut noter une perte de pr\u00e9cision puisqu'il n'est pas possible d'encoder un tel nombre dans seulement 16 bits. L'incr\u00e9ment positif minimal serait\u2009: \\(1 / 2^12 = 0.00024\\). Il convient alors d'arrondir le nombre \u00e0 la troisi\u00e8me d\u00e9cimale, soit \\(3.141\\).

    Les op\u00e9rations arithm\u00e9tiques restent triviales entre des nombres de m\u00eames types. Le chapitre sur les algorithmes d\u00e9crit une impl\u00e9mentation de calcul de sinus en utilisant ce format.

    ", "tags": ["virgule-fixe"]}, {"location": "course-c/10-numeration/numbers/#addition", "title": "Addition", "text": "

    L'addition peut se faire avec ou sans saturation\u2009:

    typedef int16_t Q;\ntypedef Q Q12;\n\nQ q_add(Q a, Q b) {\n    return a + b;\n}\n\nQ q_add_sat(Q a, Q b) {\n    int32_t res = (int32_t)a + (int32_t)b;\n    res = res > 0x7FFF ? 0x7FFF : res\n    res = res < -1 * 0x8000 ? -1 * 0x8000 : res;\n    return (Q)res;\n}\n
    "}, {"location": "course-c/10-numeration/numbers/#multiplication", "title": "Multiplication", "text": "

    Soit deux nombres 0.9 et 3.141\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25021\u25021\u25021\u25020\u2502\u25020\u25021\u25021\u25020\u25020\u25021\u25021\u25020\u2502 Q4.12 (0.9) 3686\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25021\u25021\u25020\u25020\u25021\u25020\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25021\u25021\u2502 Q4.12 (3.141) 12867\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Multiplier ces deux valeurs revient \u00e0 une multiplication sur 2 fois la taille. Le r\u00e9sultat doit \u00eatre obtenu sur 32-bits sachant que les nombres Q s'additionnent comme Q4.12 x Q4.12 donnera Q8.24.

    On voit imm\u00e9diatement que la partie enti\u00e8re vaut 2, donc 90% de 3.14 donnera une valeur en dessous de 3. Pour reconstruire une valeur Q8.8 il convient de supprimer les 16-bits de poids faible.

    3686 * 12867 = 47227762\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25020\u25020\u25021\u25020\u2502\u25021\u25021\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25021\u25020\u25021\u25020\u25020\u25020\u25021\u25021\u2502\u25020\u25021\u25021\u25021\u25020\u25020\u25021\u25020\u2502 Q8.24\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u25020\u25020\u25020\u25020\u25020\u25020\u25021\u25020\u2502\u25021\u25021\u25020\u25021\u25020\u25020\u25020\u25020\u2502 Q8.8\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n
    inline Q q_sat(int32_t x) {\n    x = x > 0x7FFF ? 0x7FFF : x\n    x = x < -1 * 0x8000 ? -1 * 0x8000 : x;\n    return (Q)x;\n}\n\ninline int16_t q_mul(int16_t a, int16_t b, char q)\n{\n    int32_t c = (int32_t)a * (int32_t)b;\n    c += 1 << (q - 1);\n    return sat(c >> q);\n}\n\ninline int16_t q12_mul(int16_t a, int16_t b)\n{\n    return q_mul(a, b, 12);\n}\n
    "}, {"location": "course-c/15-fundations/control-structures/", "title": "Structures de contr\u00f4le", "text": "Tout probl\u00e8me informatique peut \u00eatre r\u00e9solu en introduisant un niveau d'indirection suppl\u00e9mentaire.David J. Wheeler / Butler Lampson

    Les structures de contr\u00f4le appartiennent aux langages de programmation imp\u00e9ratifs et structur\u00e9. Elles permettent de modifier l'ordre des op\u00e9rations lors de l'ex\u00e9cution du code. On peut citer les cat\u00e9gories suivantes\u2009:

    Les s\u00e9quences

    On d\u00e9fini par s\u00e9quences les instructions qui s'ex\u00e9cutent les unes apr\u00e8s les autres. Celles-ci peuvent \u00eatre jalonn\u00e9es explicitement par une instruction de fin d'instruction, implicitement par un point de s\u00e9quence ou regroup\u00e9 dans un bloc. On peut distinguer trois types de s\u00e9quences\u2009:

    • les s\u00e9quences de code (;);
    • les blocs de code ({});
    • les points de s\u00e9quences.
    Les s\u00e9lections ou sauts

    Il existe des instructions qui permettent de modifier le flux d'ex\u00e9cution du programme, c'est-\u00e0-dire de sauter \u00e0 une autre partie du code. Les sauts conditionnels d\u00e9pendent d'une condition, tandis que les sauts inconditionnels sont toujours ex\u00e9cut\u00e9s. On peut distinguer les sauts d\u2019instructions suivantes\u2009:

    • sauts conditionnels (if, switch);
    • sauts inconditionnels (break, continue, goto, return).
    Les it\u00e9rations ou boucles

    Une boucle est une structure de contr\u00f4le qui permet de r\u00e9p\u00e9ter une instruction ou un bloc d'instructions tant qu'une condition est vraie. On peut distinguer les boucles suivantes\u2009:

    • boucles it\u00e9ratives sur une valeur connue for;
    • boucles sur condition while;
    • boucles sur condition avec test \u00e0 la fin do...while.

    Sans structure de contr\u00f4le, un programme se comportera toujours de la m\u00eame mani\u00e8re et ne pourra pas \u00eatre sensible \u00e0 des \u00e9v\u00e8nements ext\u00e9rieurs puisque le flux d'ex\u00e9cution ne pourra pas \u00eatre modifi\u00e9 conditionnellement. L'intelligence d'un programme r\u00e9side donc dans sa capacit\u00e9 \u00e0 prendre des d\u00e9cisions en fonction de l'\u00e9tat du syst\u00e8me et des donn\u00e9es qu'il manipule. Les structures de contr\u00f4le permettent de d\u00e9finir ces d\u00e9cisions, un peu comme un livre dont vous \u00eates le h\u00e9ros o\u00f9 chaque choix vous m\u00e8ne \u00e0 une page diff\u00e9rente par un saut.

    Historiquement, les premiers langages de programmation ne disposaient pas de structures de contr\u00f4le \u00e9volu\u00e9es. Au niveau assembleur on il est possible d'\u00eatre Turing complet avec deux types de sauts\u2009: inconditionnel (jmp) et conditionnel jz (jump if zero: saut si la valeur de la condition est nulle). Avec plus de 100 ans de recul, et des milliers de langages de programmation, la programmation imp\u00e9rative n'a pas beaucoup \u00e9volu\u00e9e. Les structures de contr\u00f4le sont rest\u00e9es les m\u00eames, seules les syntaxes ont \u00e9volu\u00e9. Certains langages comme le Python on m\u00eame d\u00e9cid\u00e9 de simplifier certaines structures de contr\u00f4le comme le do...while qui n'existe pas.

    On peut n\u00e9anmoins citer certaines fonctions d'ordre sup\u00e9rieur en programmation fonctionnelle (p. ex. map, filter, reduce) qui permettent de manipuler des s\u00e9quences de donn\u00e9es sans utiliser de boucles explicites. Ces fonctions sont souvent plus expressives et plus s\u00fbres que les boucles traditionnelles, mais elles ne remplacent pas les structures de contr\u00f4le classiques et elles n'existent pas en C. Les monades en Haskell sont un autre exemple de structures de contr\u00f4le avanc\u00e9es qui permettent de g\u00e9rer des effets de bord de mani\u00e8re s\u00fbre et contr\u00f4l\u00e9e. Des langages comme Kotlin ou JavaScript ont introduit des concepts similaires comme les coroutines ou les promesses pour g\u00e9rer de mani\u00e8re asynchrone des t\u00e2ches longues, mais une fois de plus ce sont des concepts qui n'existent pas en C.

    ", "tags": ["switch", "goto", "break", "map", "for", "do...while", "filter", "while", "reduce", "jmp", "return", "continue"]}, {"location": "course-c/15-fundations/control-structures/#sequences", "title": "S\u00e9quences", "text": "

    En informatique, une s\u00e9quence repr\u00e9sente la forme la plus fondamentale de structure de contr\u00f4le dans les langages de programmation imp\u00e9ratifs. Elle d\u00e9finit un ordre d'ex\u00e9cution lin\u00e9aire o\u00f9 les instructions sont ex\u00e9cut\u00e9es les unes apr\u00e8s les autres, suivant l'ordre dans lequel elles apparaissent dans le code source. Cette ex\u00e9cution s\u00e9quentielle est essentielle pour garantir la pr\u00e9visibilit\u00e9 et la coh\u00e9rence du comportement d'un programme.

    Formellement, une s\u00e9quence peut \u00eatre vue comme une composition de plusieurs instructions \u00e9l\u00e9mentaires \\(S_1\u2009; S_2\u2009; \\dots\u2009; S_n\\), o\u00f9 chaque instruction \\(S_i\\) est ex\u00e9cut\u00e9e apr\u00e8s la pr\u00e9c\u00e9dente. Dans ce mod\u00e8le, le flux de contr\u00f4le passe implicitement d'une instruction \u00e0 la suivante sans interruption, sauf si une structure de contr\u00f4le (comme une boucle ou une condition) modifie ce flux.

    La notion de s\u00e9quence est au c\u0153ur de la programmation structur\u00e9e, qui pr\u00e9conise l'utilisation de structures de contr\u00f4le bien d\u00e9finies (s\u00e9quences, s\u00e9lections, it\u00e9rations) pour am\u00e9liorer la lisibilit\u00e9, la maintenabilit\u00e9 et la fiabilit\u00e9 du code. En \u00e9vitant les sauts non structur\u00e9s comme le goto, les programmes deviennent plus faciles \u00e0 comprendre et \u00e0 v\u00e9rifier formellement.

    En pratique, les s\u00e9quences en code source sont souvent d\u00e9limit\u00e9es par des symboles sp\u00e9cifiques ou des conventions syntaxiques du langage utilis\u00e9. Par exemple\u2009: en C, les instructions sont termin\u00e9es par un point-virgule ;, et les blocs de code sont d\u00e9limit\u00e9s par des accolades {}, en Python, l'indentation d\u00e9finit les blocs de code, et chaque instruction est g\u00e9n\u00e9ralement \u00e9crite sur une nouvelle ligne.

    Il est important de noter que m\u00eame si les s\u00e9quences repr\u00e9sentent l'ex\u00e9cution lin\u00e9aire de code, elles peuvent contenir des appels \u00e0 des fonctions ou des proc\u00e9dures qui encapsulent elles-m\u00eames des structures de contr\u00f4le plus complexes. Ainsi, la s\u00e9quence constitue le fondement sur lequel sont b\u00e2ties les constructions plus \u00e9labor\u00e9es d'un programme.

    Au sein d'une m\u00eame fonction (ou d'un m\u00eame bloc de code) on retrouve l'ordre s\u00e9quentiel des instructions\u2009:

    int main() {\n    /* 1 */ int a = 5;\n    /* 2 */ int b = 10;\n    /* 3 */ int sum = a + b;\n    /* 4 */ printf(\"%d\\n\", sum);\n}\n

    ", "tags": ["goto"]}, {"location": "course-c/15-fundations/control-structures/#sequences-de-code", "title": "S\u00e9quences de code", "text": "

    En C, chaque instruction est s\u00e9par\u00e9e de la suivante par un point-virgule ; 003B. On appelle ce caract\u00e8re le d\u00e9limiteur d'instruction. Nous noterons que le retour \u00e0 la ligne n'est pas un d\u00e9limiteur d'instruction, mais un s\u00e9parateur visuel qui permet de rendre le code plus lisible. Il est donc possible d'\u00e9crire plusieurs instructions sur une seule ligne\u2009:

    #include <stdio.h> // doit \u00eatre sur une seule ligne\nint main() { char hello[] = \"hello\"; printf(\"%s, world\", hello); return 42; }\n

    Seuls les directives du pr\u00e9processeur (qui commencent par #) et les commentaires de lignes (//) d\u00e9pendent du retour \u00e0 la ligne.

    Le point-virgule grec

    N'allez pas confondre le point virgule ; (003B) avec le \u037e (037E), le point d'interrogation grec (\u03b5\u03c1\u03c9\u03c4\u03b7\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc). Certains farceurs aiment \u00e0 le remplacer dans le code de camarades ce qui g\u00e9n\u00e8re naturellement des erreurs de compilation.

    "}, {"location": "course-c/15-fundations/control-structures/#sequence-bloc", "title": "S\u00e9quence bloc", "text": "

    Une s\u00e9quence bloc ou instruction compos\u00e9e (compound statement) est une suite d'instructions regroup\u00e9es en un bloc mat\u00e9rialis\u00e9 par des accolades {}:

    {\n    double pi = 3.14;\n    area = pi * radius * radius;\n}\n

    Il est possible d'ajouter autant de blocs que vous voulez, mais il est recommand\u00e9 de ne pas imbriquer les blocs de mani\u00e8re excessive. Un bloc est une unit\u00e9 de code qui peut \u00eatre trait\u00e9 comme une seule instruction, mais qui n'est pas termin\u00e9 par un point-virgule.

    Il est possible de d\u00e9clarer des variables locales dans un bloc, ces variables n'\u00e9tant accessibles que dans le bloc o\u00f9 elles sont d\u00e9clar\u00e9es. L'exemple suivant montre plusieurs variables locales dont la visibilit\u00e9 est limit\u00e9e \u00e0 leur bloc respectif\u2009:

    {\n    int a = 1;\n    {\n        int b = 2;\n        {\n            int c = 3;\n        }\n        // c n'est pas accessible ici\n    }\n    // b et c ne sont pas accessibles ici\n}\n// a, b et c ne sont pas accessibles ici\n

    Limites de profondeur

    Le standard C99 \u00a75.2.4.1 impose qu'un compilateur C doit supporter au moins 127 niveaux d'imbrication de blocs, ce qui est emplement suffisant. Cette valeur n'a pas \u00e9t\u00e9 introduite par hasard, 127 est la valeur maximale d'un entier sign\u00e9 sur 8 bits (char) et les ordinateurs ne savent pas manipuler efficacement des types de donn\u00e9es plus petits.

    Ceci \u00e9tant, le nombre d'imbrication de structures conditionnelles est limit\u00e9 \u00e0 63, ce qui est d\u00e9j\u00e0 beaucoup trop. Si vous avez besoin de plus de 63 niveaux d'imbrication, il est temps de revoir votre conception\u2009!

    Notons n\u00e9anmoins que les compilateurs modernes ne limitent pas le nombre d'imbrication de blocs et de structures conditionnelles.

    ", "tags": ["char"]}, {"location": "course-c/15-fundations/control-structures/#point-de-sequence", "title": "Point de s\u00e9quence", "text": ""}, {"location": "course-c/15-fundations/control-structures/#points-de-sequence", "title": "Points de s\u00e9quence", "text": "

    Un point de s\u00e9quence, ou sequence point, est une notion d\u00e9finie dans l'annexe du standard C, qui garantit que certains ordres d'\u00e9valuation sont respect\u00e9s lors de l'ex\u00e9cution d'instructions. En d'autres termes, un point de s\u00e9quence marque un moment o\u00f9 tous les effets secondaires d'expressions pr\u00e9c\u00e9dentes doivent \u00eatre achev\u00e9s avant d'entamer l'\u00e9valuation d'expressions suivantes.

    Les r\u00e8gles relatives aux points de s\u00e9quence sont les suivantes\u2009:

    Appel de fonction

    L'\u00e9valuation des arguments d'une fonction est enti\u00e8rement termin\u00e9e avant l'ex\u00e9cution de cette fonction elle-m\u00eame. Autrement dit, l'ordre d'\u00e9valuation des arguments peut \u00eatre ind\u00e9termin\u00e9, mais tous les arguments doivent \u00eatre \u00e9valu\u00e9s avant l'appel de la fonction.

    Op\u00e9rateurs conditionnels et logiques

    Les op\u00e9rateurs &&, ||, ? :, et , introduisent des points de s\u00e9quence entre leurs op\u00e9randes. Par exemple, dans l'expression a() && b(), si a() retourne false, b() ne sera jamais \u00e9valu\u00e9e, car le r\u00e9sultat global de l'expression est d\u00e9termin\u00e9 sans avoir besoin de calculer b(). Ce m\u00e9canisme, appel\u00e9 \u00ab\u2009court-circuit\u2009\u00bb, optimise le traitement logique.

    Entr\u00e9es-sorties

    Un point de s\u00e9quence est pr\u00e9sent avant et apr\u00e8s toute op\u00e9ration d'entr\u00e9e/sortie (I/O). Cela garantit que les effets li\u00e9s \u00e0 ces op\u00e9rations, comme l'\u00e9criture sur un fichier ou l'affichage \u00e0 l'\u00e9cran, se produisent dans un ordre pr\u00e9visible.

    Il est essentiel de noter que l'op\u00e9rateur d'affectation = n'est pas un point de s\u00e9quence. Par cons\u00e9quent, l'\u00e9valuation d'une expression telle que (a = 2) + a + (a = 2) est ind\u00e9termin\u00e9e. Selon l'ordre d'\u00e9valuation des sous-expressions, le r\u00e9sultat peut varier, car les modifications de la variable a peuvent intervenir de mani\u00e8re impr\u00e9visible.

    Pour mieux saisir la notion de point de s\u00e9quence, il est essentiel de comprendre que, lorsqu'un programme en C est compil\u00e9, il est traduit en instructions assembleur qui seront ensuite ex\u00e9cut\u00e9es par le processeur. Le compilateur, afin d'optimiser les performances, peut r\u00e9ordonner certaines instructions ou les parall\u00e9liser dans l'unit\u00e9 arithm\u00e9tique et logique (ALU). Toutefois, ces optimisations peuvent entra\u00eener des comportements ind\u00e9termin\u00e9s si elles interf\u00e8rent avec l'ordre d'ex\u00e9cution attendu par le programmeur.

    Les points de s\u00e9quence jouent un r\u00f4le crucial en imposant des barri\u00e8res explicites dans le flux d'instructions, garantissant qu'\u00e0 ces points pr\u00e9cis, tous les effets des calculs pr\u00e9c\u00e9dents sont achev\u00e9s avant de passer \u00e0 l'\u00e9valuation des instructions suivantes. En d'autres termes, ils emp\u00eachent le r\u00e9ordonnancement des instructions au-del\u00e0 d'un point donn\u00e9, assurant ainsi un comportement pr\u00e9visible et conforme aux attentes du programmeur.

    ", "tags": ["point-de-sequence", "false"]}, {"location": "course-c/15-fundations/control-structures/#les-sauts-conditionnels", "title": "Les sauts conditionnels", "text": "

    Les embranchements sont des instructions de contr\u00f4le permettant au programme de prendre des d\u00e9cisions en fonction de conditions sp\u00e9cifiques. Une prise de d\u00e9cision est dite binaire lorsqu'elle repose sur un choix entre deux alternatives\u2009: vrai ou faux. Elle est dite multiple lorsque la condition \u00e9value une variable scalaire, conduisant \u00e0 plusieurs chemins possibles. En langage C, il existe deux types principaux d'instructions d'embranchement\u2009:

    1. if et if else pour les d\u00e9cisions binaires,
    2. switch pour la s\u00e9lection parmi plusieurs cas possibles.

    Ces types d'embranchements peuvent \u00eatre repr\u00e9sent\u00e9s visuellement \u00e0 l'aide de diagrammes de flux, comme les diagrammes BPMN (Business Process Model and Notation) ou les structogrammes NSD (Nassi-Shneiderman Diagrams). Ces repr\u00e9sentations graphiques permettent de mod\u00e9liser les choix conditionnels de mani\u00e8re intuitive.

    Voici un exemple de diagrammes BPMN et NSD illustrant un embranchement binaire\u2009:

    Diagrammes BPMN

    Les embranchements reposent sur des s\u00e9quences d'instructions, car chaque branche, qu'elle soit choisie ou non, est elle-m\u00eame une s\u00e9quence de commandes \u00e0 ex\u00e9cuter selon l'\u00e9valuation de la condition.

    ", "tags": ["switch"]}, {"location": "course-c/15-fundations/control-structures/#if_1", "title": "if", "text": "

    L'instruction if traduite par si est de loin la plus utilis\u00e9e. L'exemple suivant illustre un embranchement binaire. Il affiche odd si le nombre est impair et even s'il est pair\u2009:

    if (value % 2)\n{\n    printf(\"odd\\n\");\n}\nelse\n{\n    printf(\"even\\n\");\n}\n

    Notons que les blocs sont facultatifs. L'instruction if s'attend \u00e0 une instruction ou une instruction compos\u00e9e. Il est recommand\u00e9 de toujours utiliser les blocs pour \u00e9viter les erreurs de logique. N\u00e9anmoins le code suivant est parfaitement valide\u2009:

    if (value % 2)\n    printf(\"odd\\n\");\nelse\n    printf(\"even\\n\");\n

    De m\u00eame que comme des ; s\u00e9parent les instructions, on peut aussi \u00e9crire\u2009:

    if (value % 2) printf(\"odd\\n\"); else printf(\"even\\n\");\n

    Dans certaines normes pour le m\u00e9dical ou l'a\u00e9ronautique, comme MISRA, l'absence d'accollades est interdite pour \u00e9viter les erreurs de logique. C'est g\u00e9n\u00e9ralement une bonne pratique \u00e0 suivre sauf lorsque la lisibilit\u00e9 du code est am\u00e9lior\u00e9e par l'absence d'accollades.

    Info

    Dans l'exemple ci-dessus, l'instruction ternaire est plus \u00e9l\u00e9gante\u2009:

    printf(\"%s\\n\", value % 2 ? \"odd\" : \"even\");\n

    Le mot cl\u00e9 else est facultatif. Si l'on ne souhaite pas ex\u00e9cuter d'instruction lorsque la condition est fausse, il est possible de ne pas la sp\u00e9cifier.

    int a = 42;\nint b = 0;\n\nif (b == 0) {\n    printf(\"Division par z\u00e9ro impossible\\n\");\n    exit(EXIT_FAILURE);\n}\n\nprintf(\"a / b = %d\\n\", a / b);\n

    En C il n'y a pas d'instruction if..else if comme on peut le trouver dans d'autres langages de programmation (p. ex. Python avec elif). Faire suivre une sous condition \u00e0 else est n\u00e9anmoins possible puisque if est une instruction comme une autre la preuve est donn\u00e9e par la grammaire du langage qui d\u00e9termine qu'une instruction de s\u00e9lection (selection_statement), qui est une instruction (statement), peut \u00eatre suivie d'une autre instruction, et donc d'une autre instruction de s\u00e9lection.

    selection_statement\n    : IF '(' expression ')' statement\n    | IF '(' expression ')' statement ELSE statement\n    | SWITCH '(' expression ')' statement\n    ;\n

    Voici un exemple d'imbriquement de conditions\u2009:

    if (value < 0) {\n    printf(\"La valeur est n\u00e9gative\\n\");\n}\nelse {\n    if (value == 0) {\n        printf(\"La valeur est nulle\\n\");\n    }\n    else {\n        printf(\"La valeur est positive\\n\");\n    }\n}\n

    N\u00e9anmoins, comme il n'y a qu'une instruction if apr\u00e8s le premier else, le bloc peut \u00eatre omis. En outre, il est correct de faire figurer le if sur la m\u00eame ligne que le else :

    if (value < 0) {\n    printf(\"La valeur est n\u00e9gative\\n\");\n}\nelse if (value == 0) {\n    printf(\"La valeur est nulle\\n\");\n}\nelse {\n    printf(\"La valeur est positive\\n\");\n}\n

    Une condition n'est pas n\u00e9cessairement unique, elle peut-\u00eatre la concat\u00e9nation logique de plusieurs conditions s\u00e9par\u00e9es\u2009:

    if((0 < x && x < 10) || (100 < x && x < 110) || (200 < x && x < 210))\n{\n    printf(\"La valeur %d est valide\", x);\n    is_valid = true;\n}\nelse\n{\n    printf(\"La valeur %d n'est pas valide\", x);\n    is_valid = false;\n}\n

    Remarquons que cet exemple peut \u00eatre simplifi\u00e9 pour diminuer la complexit\u00e9 cyclomatique :

    bool is_valid = (0 < x && x < 10) ||\n                (100 < x && x < 110) ||\n                (200 < x && x < 210);\n\nif (is_valid) {\n    printf(\"La valeur %d est valide\", x);\n}\nelse {\n    printf(\"La valeur %d n'est pas valide\", x);\n}\n

    Allman, Stroustrup ou K&R ?

    Il existe plusieurs conventions de style pour \u00e9crire les blocs de code. Les plus connues sont les styles Allman, Stroustrup et K&R. Le style Allman place les accolades sur des lignes s\u00e9par\u00e9es, le style Stroustrup les place sur la m\u00eame ligne que l'instruction de contr\u00f4le, et le style K&R les place sur la m\u00eame ligne que l'instruction de contr\u00f4le mais avec un d\u00e9calage.

    Chacun de ces styles a ses partisans et ses d\u00e9tracteurs, et il est important de choisir un style coh\u00e9rent pour un projet donn\u00e9.

    Le style de codage est pris\u00e9 par les managers qui ne savent pas programmer, ils ont appris \u00e0 rep\u00e9rer les incoh\u00e9rences de style et pense qu'il s'agit d'un indicateur de qualit\u00e9 du code. C'est un peu comme si un chef cuisinier jugeait la qualit\u00e9 d'un plat \u00e0 la couleur de l'assiette. Peu importe la couleur de l'assiette, ce qui compte c'est le go\u00fbt du plat. N\u00e9anmoins un restaurant qui n'aurait pas de coh\u00e9rence dans la couleur de ses assiettes pourrait \u00eatre per\u00e7u comme n\u00e9glig\u00e9.

    Point virgule en trop

    Il peut arriver par reflexe d'ajouter un point virgule derri\u00e8re un if qui a pour effet de terminer pr\u00e9matur\u00e9ment le bloc. Par exemple\u2009:

    if (z == 0);\nprintf(\"z est nul\"); // ALWAYS executed\n

    C'est une erreur classique qui peut \u00eatre difficile \u00e0 rep\u00e9rer.

    Affectation dans un test

    Le test de la valeur d'une variable s'\u00e9crit avec l'op\u00e9rateur d'\u00e9galit\u00e9 == et non l'op\u00e9rateur d'affectation =. Ici, l'\u00e9valuation de la condition vaut la valeur affect\u00e9e \u00e0 la variable.

    if (z = 0)               // set z to zero !!\n    printf(\"z est nul\"); // NEVER executed\n

    L'oubli des accolades

    Dans le cas ou vous souhaitez ex\u00e9cuter plusieurs instructions, vous devez imp\u00e9rativement d\u00e9clarer un bloc d'instructions. Si vous omettez les accolades, seule la premi\u00e8re instruction sera ex\u00e9cut\u00e9e puisque la s\u00e9quence se termine par un point virgule ou un bloc.

    if (z == 0)\n    printf(\"z est nul\");\n    is_valid = false;  // Ne fait par partie du bloc et s'ex\u00e9cute toujours\n

    On peut utiliser des conditions multiples pour d\u00e9terminer le comportement d'un programme. Par exemple, le programme suivant affiche un message diff\u00e9rent en fonction de la valeur de value :

    if (value % 2)\n{\n    printf(\"La valeur est impaire.\");\n}\nelse if (value > 500)\n{\n    printf(\"La valeur est paire et sup\u00e9rieure \u00e0 500.\");\n}\nelse if (!(value % 5))\n{\n    printf(\"La valeur est paire, inf\u00e9rieur \u00e0 500 et divisible par 5.\");\n}\nelse\n{\n    printf(\"La valeur ne satisfait aucune condition \u00e9tablie.\");\n}\n

    Exercice 1\u2009: Et si\u2009?

    Comment se comporte l'exemple suivant\u2009:

    if (!(i < 8) && !(i > 8))\n    printf(\"i is %d\\n\", i);\n

    Exercice 2\u2009: D'autres si\u2009?

    Compte tenu de la d\u00e9claration int i = 8;, indiquer pour chaque expression si elles impriment ou non i vaut 8:

    1. if (!(i < 8) && !(i > 8)) then\n    printf(\"i vaut 8\\n\");\n
    2. if (!(i < 8) && !(i > 8))\n    printf(\"i vaut 8\");\n    printf(\"\\n\");\n
    3. if !(i < 8) && !(i > 8)\n    printf(\"i vaut 8\\n\");\n
    4. if (!(i < 8) && !(i > 8))\n    printf(\"i vaut 8\\n\");\n
    5. if (i = 8) printf(\"i vaut 8\\n\");\n
    6. if (i & (1 << 3)) printf(\"i vaut 8\\n\");\n
    7. if (i ^ 8) printf(\"i vaut 8\\n\");\n
    8. if (i - 8) printf(\"i vaut 8\\n\");\n
    9. if (i == 1 << 3) printf (\"i vaut 8\\n\");\n
    10. if (!((i < 8) || (i > 8)))\n    printf(\"i vaut 8\\n\");\n

    ", "tags": ["value", "odd", "elif", "even", "statement", "else"]}, {"location": "course-c/15-fundations/control-structures/#switch_1", "title": "switch", "text": "

    L'instruction switch n'est pas fondamentale et certains langages de programmation ne la d\u00e9finissent pas. Elle permet essentiellement de simplifier l'\u00e9criture pour minimiser les r\u00e9p\u00e9titions. On l'utilise lorsque les conditions multiples portent toujours sur la m\u00eame variable. Par exemple, le code suivant peut \u00eatre r\u00e9\u00e9crit plus simplement en utilisant un switch :

    Switch Case BPMN

    if (defcon == 1)\n    printf(\"Guerre nucl\u00e9aire imminente\");\nelse if (defcon == 2)\n    printf(\"Prochaine \u00e9tape, guerre nucl\u00e9aire\");\nelse if (defcon == 3)\n    printf(\"Accroissement de la pr\u00e9paration des forces\");\nelse if (defcon == 4)\n    printf(\"Mesures de s\u00e9curit\u00e9 renforc\u00e9es et renseignements accrus\");\nelse if (defcon == 5\n    printf(\"Rien \u00e0 signaler, temps de paix\");\nelse\n    printf(\"ERREUR: Niveau d'alerte DEFCON invalide\");\n

    Voici la m\u00eame s\u00e9quence utilisant switch. Notez que chaque condition est plus claire\u2009:

    switch (defcon)\n{\n    case 1 :\n        printf(\"Guerre nucl\u00e9aire imminente\");\n        break;\n    case 2 :\n        printf(\"Prochaine \u00e9tape, guerre nucl\u00e9aire\");\n        break;\n    case 3 :\n        printf(\"Accroissement de la pr\u00e9paration des forces\");\n        break;\n    case 4 :\n        printf(\"Mesures de s\u00e9curit\u00e9 renforc\u00e9es et renseignements accrus\");\n        break;\n    case 5 :\n        printf(\"Rien \u00e0 signaler, temps de paix\");\n        break;\n    default :\n        printf(\"ERREUR: Niveau d'alerte DEFCON invalide\");\n}\n

    La valeur par d\u00e9faut default est optionnelle, mais recommand\u00e9e pour traiter les cas d'erreurs possibles.

    La structure d'un switch est compos\u00e9e d'une condition switch (condition) suivie d'une s\u00e9quence {}. Les instructions de cas case 42: sont appel\u00e9es \u00e9tiquettes (labels). Notez la pr\u00e9sence de l'instruction break qui est n\u00e9cessaire pour terminer l'ex\u00e9cution de chaque condition. Par ailleurs, les labels peuvent \u00eatre cha\u00een\u00e9s sans instructions interm\u00e9diaires ni break:

    switch (coffee)\n{\n    case IRISH_COFFEE :\n        add_whisky();\n\n    case CAPPUCCINO :\n    case MACCHIATO :\n        add_milk();\n\n    case ESPRESSO :\n    case AMERICANO :\n        add_coffee();\n        break;\n\n    default :\n        printf(\"ERREUR 418: Type de caf\u00e9 inconnu\");\n}\n

    Notons que le compilateur est capable d'optimiser les switch en fonction des valeurs des \u00e9tiquettes. Il n'est pas n\u00e9cessaire que les \u00e9tiquettes soient tri\u00e9es, car le compilateur peut r\u00e9ordonner les cas pour optimiser l'ex\u00e9cution. N\u00e9anmoins des \u00e9tiquettes \u00e0 progression logique p. ex. {12, 13, 14, 15} sont plus efficaces que des \u00e9tiquettes al\u00e9atoires p. ex. {1, 109, 9, 42}.

    La construction d'une \u00e9tiquette case implique une constante litt\u00e9rale. Il n'est pas possible d'utiliser une expression ou une variable. En outre, il ne peut y avoir qu'une seule \u00e9tiquette par ligne, car cette derni\u00e8re doit \u00eatre situ\u00e9e apr\u00e8s un retour \u00e0 la ligne. Voici un exemple permettant de d\u00e9terminer le nombre de jours dans un mois\u2009:

    int ndays = -1;\nswitch (month) {\n    case 1:  // JAN\n    case 3:  // MAR\n    case 5:  // MAY\n    case 7:  // JUL\n    case 8:  // AUG\n    case 10: // OCT\n    case 12: // DEC\n        ndays = 31;\n        break;\n    case 4:  // APR\n    case 6:  // JUN\n    case 9:  // SEP\n    case 11: // NOV\n        ndays = 30;\n        break;\n    case 2:  // FEB\n        if (year % 400 == 0)\n            ndays = 29;\n        else if (year % 100 == 0)\n            ndays = 28;\n        else if (year % 4 == 0)\n            ndays = 29;\n        else\n            ndays = 28;\n        break;\n    default:\n        // Erreur : mois invalide\n}\n
    ", "tags": ["switch", "case", "default", "break"]}, {"location": "course-c/15-fundations/control-structures/#declaration-de-variables", "title": "D\u00e9claration de variables", "text": "

    Il faut comprendre que la structure switch est un peu particuli\u00e8re. Le switch agit comme un bloc, la d\u00e9claration de variable est possible \u00e0 n'importe quel endroit du bloc, mais toutes les lignes de ce bloc ne seront pas ex\u00e9cut\u00e9es puisque le switch utilisera les labels pour sauter \u00e0 la bonne instruction. Consid\u00e9rons l'exemple suivant\u2009:

    int main(int argc) {\n   switch (argc) {\n      int i = 23;\n      case 1:\n         int j = 42;\n         printf(\"0. %d\\n\", i + j);\n         break;\n      case 2:\n         int j = 23;\n         printf(\"1. %d\\n\", i + j);\n         break;\n   }\n}\n

    \u00c0 la compilation on notera l'erreur suivante\u2009:

    test.c: In function \u2018main\u2019:\ntest.c:5:11: warning: statement will never be executed [-Wswitch-unreachable]\n    5 |       int i = 23;\n      |           ^\n

    En effet, cette instruction se trouve avant le premier label case et ne sera donc jamais ex\u00e9cut\u00e9. La variable est belle et bien d\u00e9clar\u00e9e, mais elle ne sera pas initialis\u00e9e.

    En outre, la d\u00e9claration de j = 23 pose \u00e9galement probl\u00e8me, l'erreur de compilation suivante appara\u00eet\u2009:

    test.c: In function \u2018main\u2019:\ntest.c:11:14: error: redefinition of \u2018j\u2019\n   11 |          int j = 23;\n      |              ^\ntest.c:7:14: note: previous definition of \u2018j\u2019 with type \u2018int\u2019\n    7 |          int j = 42;\n      |            ^\n

    Vous savez qu'il n'est pas possible de red\u00e9clarer une variable d\u00e9j\u00e0 existante dans le m\u00eame bloc. La solution \u00e0 ce probl\u00e8me est de d\u00e9clarer les variables propres \u00e0 un cas dans un bloc s\u00e9par\u00e9. Notez que la variable k n'\u00e9tant utilis\u00e9e qu'une fois, elle peut \u00eatre dans le contexte global du switch mais situ\u00e9 apr\u00e8s le premier label case. En pratique, n'essayez pas de jouer avec les limites de la syntaxe, cela ne fera que rendre votre code plus difficile \u00e0 lire et \u00e0 maintenir.

    #include <stdio.h>\n\nint main(int argc) {\n   switch (argc) {\n      case 1:\n         int k = 10;\n         {\n            int i = 23;\n            int j = 42;\n            printf(\"0. %d\\n\", i + j + k);\n            break;\n         }\n      case 2: {\n         int i = 23;\n         int j = 23;\n         printf(\"1. %d\\n\", i + j);\n         break;\n      }\n   }\n}\n
    ", "tags": ["switch", "case"]}, {"location": "course-c/15-fundations/control-structures/#imbrication", "title": "Imbrication", "text": "

    Il est possible d'imbriquer diff\u00e9rents niveaux dans un switch\u2009:

    switch(a) {\n    case 100:\n        switch(b) {\n            case 200:\n                printf(\"a=100, b=200\\n\");\n                break;\n            case 300:\n                printf(\"a=100, b=300\\n\");\n                break;\n        }\n        break;\n}\n
    "}, {"location": "course-c/15-fundations/control-structures/#appareil-de-duff", "title": "Appareil de Duff", "text": "

    Le Duff's device est une technique d'optimisation assez originale en langage C, qui permet de d\u00e9rouler une boucle de mani\u00e8re partiellement manuelle, dans le but de gagner en performance, notamment sur des architectures mat\u00e9rielles plus anciennes. Il a \u00e9t\u00e9 invent\u00e9 par Tom Duff en 1983 lorsqu'il travaillait chez Lucasfilm.

    L'objectif du Duff's device est de d\u00e9rouler manuellement une boucle afin de r\u00e9duire le nombre d'it\u00e9rations et d'optimiser l'ex\u00e9cution, notamment dans les situations o\u00f9 le co\u00fbt d'un saut conditionnel dans une boucle pouvait \u00eatre \u00e9lev\u00e9. Cette optimisation est souvent appel\u00e9e unrolling, o\u00f9 plusieurs it\u00e9rations de la boucle sont \u00ab\u2009fusionn\u00e9es\u2009\u00bb en une seule.

    La particularit\u00e9 du Duff's device est qu'il combine \u00e0 la fois une structure de boucle while et un switch de mani\u00e8re surprenante et astucieuse. Voici \u00e0 quoi ressemble le code original\u2009:

    register int count = (n + 7) / 8;  // Nombre d'it\u00e9rations par paquet de 8\nregister short *to = dest;\nregister short *from = src;\n\nswitch (n % 8) {  // D\u00e9termine le point d'entr\u00e9e initial dans la boucle\n    case 0: do { *to = *from++;\n    case 7:      *to = *from++;\n    case 6:      *to = *from++;\n    case 5:      *to = *from++;\n    case 4:      *to = *from++;\n    case 3:      *to = *from++;\n    case 2:      *to = *from++;\n    case 1:      *to = *from++;\n            } while (--count > 0);\n}\n
    ", "tags": ["while", "switch"]}, {"location": "course-c/15-fundations/control-structures/#resume-des-points-cles", "title": "R\u00e9sum\u00e9 des points cl\u00e9s", "text": "
    • La structure switch bien qu'elle puisse toujours \u00eatre remplac\u00e9e par une structure if..else if est g\u00e9n\u00e9ralement plus \u00e9l\u00e9gante et plus lisible. Elle \u00e9vite par ailleurs de r\u00e9p\u00e9ter la condition plusieurs fois (c.f. DRY).
    • Le compilateur est mieux \u00e0 m\u00eame d'optimiser un choix multiple lorsque les valeurs scalaires de la condition tri\u00e9es se suivent directement p. ex. {12, 13, 14, 15}.
    • L'ordre des cas d'un switch n'a pas d'importance, le compilateur peut m\u00eame choisir de r\u00e9ordonner les cas pour optimiser l'ex\u00e9cution.
    • Les \u00e9tiquettes case ne peuvent \u00eatre que des constantes litt\u00e9rales, il n'est pas possible d'utiliser des expressions ou des variables.
    • Les \u00e9tiquettes case doivent \u00eatre s\u00e9par\u00e9es par un retour \u00e0 la ligne, il n'est pas possible d'avoir plusieurs \u00e9tiquettes sur une m\u00eame ligne.
    • Il est possible de cha\u00eener les \u00e9tiquettes sans break pour ex\u00e9cuter plusieurs instructions.
    ", "tags": ["switch", "case", "break"]}, {"location": "course-c/15-fundations/control-structures/#les-boucles", "title": "Les boucles", "text": "

    Bien choisir sa structure de contr\u00f4le

    Une boucle est une structure it\u00e9rative permettant de r\u00e9p\u00e9ter l'ex\u00e9cution d'une s\u00e9quence. En C il existe trois types de boucles\u2009:

    1. for
    2. while
    3. do .. while

    Elles peuvent \u00eatre repr\u00e9sent\u00e9es par les diagrammes de flux suivants\u2009:

    Aper\u00e7u des trois structures de boucles

    On observe que quelque soit la structure de boucle, une condition de maintien est n\u00e9cessaire. Cette condition est \u00e9valu\u00e9e avant ou apr\u00e8s l'ex\u00e9cution de la s\u00e9quence. Si la condition est fausse, la s\u00e9quence est interrompue et le programme continue son ex\u00e9cution.

    "}, {"location": "course-c/15-fundations/control-structures/#while", "title": "while", "text": "

    La structure while r\u00e9p\u00e8te une s\u00e9quence tant que la condition est vraie. Dans l'exemple suivant, tant que le poids d'un objet d\u00e9pos\u00e9 sur une balance est inf\u00e9rieur \u00e0 une valeur constante, une masse est ajout\u00e9e et le syst\u00e8me patiente avant stabilisation.

    while (get_weight() < 420 /* newtons */) {\n    add_one_kg();\n    wait(5 /* seconds */);\n}\n

    S\u00e9quentiellement une boucle while teste la condition, puis ex\u00e9cute la s\u00e9quence associ\u00e9e.

    Exercice 3\u2009: Tant que...

    Comment se comportent ces programmes\u2009?

    1. size_t i=0; while(i<11) { i+=2; printf(\"%i\\n\",i); }
    2. i = 11; while(i--){ printf(\"%i\\n\",i--); }
    3. i = 12; while(i--){ printf(\"%i\\n\",--i); }
    4. i = 1; while ( i <= 5 ){ printf ( \"%i\\n\", 2 * i++ );}
    5. i = 1; while ( i != 9 ) { printf ( \"%i\\n\", i = i + 2 ); }
    6. i = 1; while ( i < 9 ) { printf ( \"%i\\n\", i += 2 ); break; }
    7. i = 0; while ( i < 10 ) { continue; printf ( \"%i\\n\", i += 2 ); }

    On utilise une boucle while lorsque le nombre d'it\u00e9rations n'est pas connu \u00e0 l'avance. Si la s\u00e9quence doit \u00eatre ex\u00e9cut\u00e9e au moins une fois, on utilise une boucle do...while.

    ", "tags": ["while"]}, {"location": "course-c/15-fundations/control-structures/#dowhile", "title": "do..while", "text": "

    De temps en temps il est n\u00e9cessaire de tester la condition \u00e0 la sortie de la s\u00e9quence et non \u00e0 l'entr\u00e9e. La boucle do...while permet justement ceci\u2009:

    size_t i = 10;\n\ndo {\n    printf(\"Veuillez attendre encore %d seconde(s)\\r\\n\", i);\n    i -= 1;\n} while (i);\n

    Contrairement \u00e0 la boucle while, la s\u00e9quence est ici ex\u00e9cut\u00e9e au moins une fois.

    Notez ci-dessus la pr\u00e9sence d'un ; apr\u00e8s le while. La structure do...while est un peu particuli\u00e8re, car elle est la seule structure de contr\u00f4le \u00e0 se terminer par un point-virgule.

    ", "tags": ["while"]}, {"location": "course-c/15-fundations/control-structures/#for", "title": "for", "text": "

    La boucle for est un while am\u00e9lior\u00e9 qui permet en une ligne de r\u00e9sumer les conditions de la boucle\u2009:

    for (/* expression 1 */; /* expression 2 */; /* expression 3 */) {\n    /* s\u00e9quence */\n}\n
    Expression 1

    Ex\u00e9cut\u00e9e une seule fois \u00e0 l'entr\u00e9e dans la boucle, c'est l'expression d'initialisation permettant par exemple de d\u00e9clarer une variable et de l'initialiser \u00e0 une valeur particuli\u00e8re.

    Expression 2

    Condition de validit\u00e9 (ou de maintien de la boucle). Tant que la condition est vraie, la boucle est ex\u00e9cut\u00e9e.

    Expression 3

    Action de fin de tour. \u00c0 la fin de l'ex\u00e9cution de la s\u00e9quence, cette action est ex\u00e9cut\u00e9e avant le tour suivant. Cette action permet par exemple d'incr\u00e9menter une variable.

    Voici comment r\u00e9p\u00e9ter 10x un bloc de code\u2009:

    for (size_t i = 0; i < 10; i++) {\n    something();\n}\n

    Notons que les portions de for sont optionnels et que la structure suivante est strictement identique \u00e0 la boucle while:

    for (; get_weight() < 420 ;) {\n    /* ... */\n}\n

    Exercice 4\u2009: Pour quelques tours

    Comment est-ce que ces expressions se comportent-elles\u2009?

    int i, k;\n
    1. for (i = 'a'; i < 'd'; printf (\"%i\\n\", ++i));
    2. for (i = 'a'; i < 'd'; printf (\"%c\\n\", ++i));
    3. for (i = 'a'; i++ < 'd'; printf (\"%c\\n\", i ));
    4. for (i = 'a'; i <= 'a' + 25; printf (\"%c\\n\", i++ ));
    5. for (i = 1 / 3; i ; printf(\"%i\\n\", i++ ));
    6. for (i = 0; i != 1 ; printf(\"%i\\n\", i += 1 / 3 ));
    7. for (i = 12, k = 1; k++ < 5 ; printf(\"%i\\n\", i-- ));
    8. for (i = 12, k = 1; k++ < 5 ; k++, printf(\"%i\\n\", i-- ));

    Exercice 5\u2009: Erreur

    Identifier les deux erreurs dans ce code suivant\u2009:

    for (size_t = 100; i >= 0; --i)\n    printf(\"%d\\n\", i);\n

    Exercice 6\u2009: De un \u00e0 cent

    \u00c9crivez un programme affichant les entiers de 1 \u00e0 100 en employant\u2009:

    1. Une boucle for
    2. Une boucle while
    3. Une boucle do..while

    Quelle est la structure de contr\u00f4le la plus adapt\u00e9e \u00e0 cette situation\u2009?

    L'op\u00e9rateur , est un op\u00e9rateur de s\u00e9quence qui permet de s\u00e9parer des expressions. Il est souvent utilis\u00e9 dans les boucles for pour ex\u00e9cuter plusieurs instructions dans les diff\u00e9rentes parties de la boucle. Par exemple pour d\u00e9finir deux variables i et j dans la partie d'initialisation de la boucle. Voici par exemple comment afficher les lettres de l'alphabet en alternance z-a y-b x-c... :

    for (char i = 'z', j = 'a'; i > j; i--, j++) {\n    printf(\"%c-%c \", i, j);\n}\n

    Le programme affiche\u2009:

    z-a y-b x-c w-d v-e u-f t-g s-h r-i q-j p-k o-l n-m\n

    Variable d'induction non sign\u00e9e

    Il est recommand\u00e9 d'utiliser des variables d'induction sign\u00e9es pour les boucles for afin d'\u00e9viter des erreurs de logique. En effet, si vous utilisez une variable d'induction non sign\u00e9e et que vous la d\u00e9cr\u00e9mentez, vous risquez de cr\u00e9er une boucle infinie\u2009:

    for (size_t i = 10; i >= 0; i--) {\n    printf(\"%d\\n\", i);\n}\n

    Dans cet exemple, i est une variable non sign\u00e9e de type size_t. Lorsque i atteint 0, la condition i >= 0 est toujours vraie, car size_t est un type non sign\u00e9 et ne peut pas \u00eatre n\u00e9gatif. Par cons\u00e9quent, la boucle ne se termine jamais et entra\u00eene un d\u00e9bordement de la variable i.

    En pratique on utilisera simplement un int pour les variables d'induction n\u00e9anmoins pour une grande portabilit\u00e9 on utilisera int_fast32_t ou int_fast64_t pour garantir une taille de variable optimale.

    Exercice 7\u2009: Op\u00e9rateur virgule dans une boucle

    Expliquez quelle est la fonctionnalit\u00e9 globale du programme ci-dessous\u2009:

    int main(void) {\n    for(size_t i = 0, j = 0; i * i < 1000; i++, j++, j %= 26, printf(\"\\n\"))\n        printf(\"%c\", 'a' + (char)j);\n}\n

    Proposer une meilleure impl\u00e9mentation de ce programme.

    ", "tags": ["int_fast32_t", "size_t", "do..while", "for", "while", "int_fast64_t", "int"]}, {"location": "course-c/15-fundations/control-structures/#boucles-infinies", "title": "Boucles infinies", "text": "

    Une boucle infinie n'est jamais termin\u00e9e. On rencontre souvent ce type de boucle dans ce que l'on appelle \u00e0 tort La boucle principale aussi nomm\u00e9e run loop. Lorsqu'un programme est ex\u00e9cut\u00e9 bare-metal, c'est \u00e0 dire directement \u00e0 m\u00eame le microcontr\u00f4leur et sans syst\u00e8me d'exploitation, il est fr\u00e9quent d'y trouver une fonction main telle que\u2009:

    void main_loop() {\n    // Boucle principale\n}\n\nint main(void) {\n    for (;;)\n    {\n        main_loop();\n    }\n}\n

    Il y a diff\u00e9rentes variantes de boucles infinies\u2009:

    for (;;) { }\n\nwhile (true) { }\n\ndo { } while (true);\n

    Notions que l'expression while (1) que l'on rencontre fr\u00e9quemment dans des exemples n'est pas syntaxiquement exacte. Une condition de validit\u00e9 devrait \u00eatre un bool\u00e9en, soit vrai, soit faux. Or, la valeur scalaire 1 devrait pr\u00e9alablement \u00eatre transform\u00e9e en une valeur bool\u00e9enne. Il est donc plus juste d'\u00e9crire while (1 == 1) ou simplement while (true). D'ailleurs pourquoi utiliser 1 et non pas 42 ? Moi j'aime bien while (42), c'est plus fun...

    Certains d\u00e9veloppeurs pr\u00e9f\u00e8rent l'\u00e9criture for (;;) qui ne fait pas intervenir de conditions ext\u00e9rieures ou de valeurs bulgares, car, avant C99 d\u00e9finir la valeur true \u00e9tait \u00e0 la charge du d\u00e9veloppeur et on pourrait s'imaginer cette plaisanterie de mauvais go\u00fbt\u2009:

    _Bool true = 0;\n\nwhile (true) { /* ... */ }\n

    Lorsque l'on a besoin d'une boucle infinie, il est g\u00e9n\u00e9ralement pr\u00e9f\u00e9rable de permettre au programme de se terminer correctement lorsqu'il est interrompu par le signal SIGINT (c. f. signals). On rajoute alors une condition de sortie \u00e0 la boucle principale\u2009:

    #include <stdlib.h>\n#include <signal.h>\n#include <stdbool.h>\n\nstatic volatile bool is_running = true;\n\nvoid sigint_handler(int dummy)\n{\n    is_running = false;\n}\n\nint main(void)\n{\n    signal(SIGINT, sigint_handler);\n\n    while (is_running)\n    {\n       /* ... */\n    }\n\n    return EXIT_SUCCESS;\n}\n

    ", "tags": ["true", "main"]}, {"location": "course-c/15-fundations/control-structures/#les-sauts", "title": "Les sauts", "text": "

    Il existe 4 instructions en C permettant de contr\u00f4ler le d\u00e9roulement de l'ex\u00e9cution d'un programme. Elles d\u00e9clenchent un saut inconditionnel vers un autre endroit du programme.

    break

    Cette instruction nterrompt la structure de contr\u00f4le en cours. Elle est valide pour while, do...while, for et switch.

    continue

    Cette instruction interrompt le cycle en cours et passe directement au cycle suivant. Elle est valide pour while, do...while et for.

    goto

    La redout\u00e9e instruction goto interrompt l'ex\u00e9cution et saute \u00e0 un label situ\u00e9 ailleurs dans la m\u00eame fonction.

    return

    Cette instruction interrompt l'ex\u00e9cution de la fonction en cours et retourne \u00e0 son point d'appel.

    ", "tags": ["goto", "break", "return", "for", "while", "switch", "continue"]}, {"location": "course-c/15-fundations/control-structures/#goto", "title": "goto", "text": "

    Il s'agit de l'instruction la plus controvers\u00e9e en C. Cherchez sur internet et les d\u00e9tracteurs sont nombreux, et ils ont partiellement raison, car dans la tr\u00e8s vaste majorit\u00e9 des cas o\u00f9 vous pensez avoir besoin de goto, une autre solution plus \u00e9l\u00e9gante existe.

    N\u00e9anmoins, il est important de comprendre que goto \u00e9tait dans certains langages de programmation comme BASIC, la seule structure de contr\u00f4le disponible permettant de faire des sauts. Elle est par ailleurs le reflet du langage machine, car la plupart des processeurs ne connaissent que cette instruction souvent appel\u00e9e JUMP. Il est par cons\u00e9quent possible d'imiter le comportement de n'importe quelle structure de contr\u00f4le si l'on dispose de if et de goto.

    goto effectue un saut inconditionnel \u00e0 un label d\u00e9fini en C par un identificateur suivi d'un :.

    L'un des seuls cas de figure autoris\u00e9s est celui d'un traitement d'erreur centralis\u00e9 lorsque de multiples points de retours existent dans une fonction ceci \u00e9vitant de r\u00e9p\u00e9ter du code\u2009:

    #include <time.h>\n\nint parse_message(int message)\n{\n    struct tm *t = localtime(time(NULL));\n    if (t->tm_hour < 7) {\n        goto error;\n    }\n\n    if (message > 1000) {\n        goto error;\n    }\n\n    /* ... */\n\n    return 0;\n\n    error:\n        printf(\"ERROR: Une erreur a \u00e9t\u00e9 commise\\n\");\n        return -1;\n}\n
    ", "tags": ["JUMP", "goto"]}, {"location": "course-c/15-fundations/control-structures/#continue", "title": "continue", "text": "

    Le mot cl\u00e9 continue ne peut exister qu'\u00e0 l'int\u00e9rieur d'une boucle. Il permet d'interrompre le cycle en cours et directement passer au cycle suivant.

    uint8_t airplane_seat = 100;\n\nwhile (--airplane_seat)\n{\n    if (airplane_seat == 13) {\n        continue;\n    }\n\n    printf(\"Dans cet avion il y a un si\u00e8ge num\u00e9ro %d\\n\", airplane_seat);\n}\n

    Cette structure est \u00e9quivalente \u00e0 l'utilisation d'un goto avec un label plac\u00e9 \u00e0 la fin de la s\u00e9quence de boucle, mais promettez-moi que vous n'utiliserez jamais cet exemple\u2009:

    while (true)\n{\n    if (condition) {\n        goto next;\n    }\n\n    /* ... */\n\n    next:\n}\n
    ", "tags": ["continue"]}, {"location": "course-c/15-fundations/control-structures/#break", "title": "break", "text": "

    Le mot-cl\u00e9 break peut \u00eatre utilis\u00e9 dans une boucle ou dans un switch. Il permet d'interrompre l'ex\u00e9cution de la boucle ou de la structure switch la plus proche. Nous avions d\u00e9j\u00e0 \u00e9voqu\u00e9 l'utilisation dans un switch (c.f. switch).

    ", "tags": ["switch", "break"]}, {"location": "course-c/15-fundations/control-structures/#return", "title": "return", "text": "

    Le mot cl\u00e9 return suivi d'une valeur de retour ne peut appara\u00eetre que dans une fonction dont le type de retour n'est pas void. Ce mot-cl\u00e9 permet de stopper l'ex\u00e9cution d'une fonction et de retourner \u00e0 son point d'appel.

    void unlock(int password)\n{\n    static tries = 0;\n\n    if (password == 4710 /* MacGuyver: A Retrospective 1986 */) {\n        open_door();\n        tries = 0;\n        return;\n    }\n\n    if (tries++ == 3)\n    {\n        alert_security_guards();\n    }\n}\n
    ", "tags": ["return", "void"]}, {"location": "course-c/15-fundations/control-structures/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 8\u2009: Faute d'erreur

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    long i = 0;\ndouble x = 100.0;\n

    Indiquer la nature de l'erreur dans les expressions suivantes\u2009:

    1.

    do\n    x = x / 2.0;\n    i++;\nwhile (x > 1.0);\n
    2.
    if (x = 0)\n    printf(\"0 est interdit !\\n\");\n
    3.
    switch(x) {\n    case 100 :\n        printf(\"Bravo.\\n\");\n        break;\n    default :\n        printf(\"Pas encore.\\n\");\n\n}\n
    4.
    for (i = 0 ; i < 10 ; i++);\n    printf(\"%d\\n\", i);\n
    5.
    while i < 100 {\n    printf(\"%d\", ++i);\n}\n

    Exercice 9\u2009: Cas appropri\u00e9s

    Parmi les cas suivants, quelle structure de contr\u00f4le utiliser\u2009?

    1. Test qu'une variable est dans un intervalle donn\u00e9.
    2. Actions suivant un choix multiple de l'utilisateur
    3. Rechercher un caract\u00e8re particulier dans une cha\u00eene de caract\u00e8re
    4. It\u00e9rer toutes les valeurs paires sur un intervalle donn\u00e9
    5. Demander la ligne suivante du t\u00e9l\u00e9gramme \u00e0 l'utilisateur jusqu'\u00e0 STOP
    Solution
    1. Le cas est circonscrit \u00e0 un intervalle de valeur donn\u00e9e, le if est appropri\u00e9\u2009:

      if (i > min && i < max) { /* ... */ }\n
    2. Dans ce cas un switch semble le plus appropri\u00e9

      switch(choice) {\n    case 0 :\n        /* ... */\n        break;\n    case 1 :\n        /* ... */\n}\n
    3. \u00c0 reformuler tant que le caract\u00e8re n'est pas trouv\u00e9 ou que la fin de la cha\u00eene n'est pas atteinte. On se retrouve donc avec une boucle \u00e0 deux conditions de sorties.

      size_t pos;\nwhile (pos < strlen(str) && str[pos] != c) {\n    pos++;\n}\nif (pos == strlen(str)) {\n    // Not found\n} else {\n    // Found `c` in `str` at position `pos`\n}\n
    4. La boucle for semble ici la plus adapt\u00e9e

      for (size_t i = 100; i < 200; i += 2) {\n    /* ... */\n}\n
    5. Il est n\u00e9cessaire ici d'assurer au moins un tour de boucle\u2009:

      const size_t max_line_length = 64;\nchar format[32];\nsnprintf(format, sizeof(format), \"%%%zus\", max_line_length - 1);\nunsigned int line = 0;\nchar buffer[max_lines][max_line_length];\ndo {\n    printf(\"%d. \", line);\n} while (\n    scanf(format, buffer[line]) == 1 &&\n    strcmp(buffer[line], \"STOP\") &&\n    ++line < max_lines\n);\n

    Exercice 10\u2009: Comptons sur les caract\u00e8res

    Un texte est pass\u00e9 \u00e0 un programme par stdin. Comptez le nombre de caract\u00e8res transmis.

    $ echo \"hello world\" | count-this\n11\n

    Exercice 11\u2009: Esperluette conditionnelle

    Quel est le probl\u00e8me avec cette ligne de code\u2009?

    if (x&mask==bits)\n
    Solution

    La priorit\u00e9 de l'op\u00e9rateur unitaire & est plus \u00e9lev\u00e9e que == ce qui se traduit par\u2009:

    if (x & (mask == bits))\n

    Le d\u00e9veloppeur voulait probablement appliquer le masque \u00e0 x puis le comparer au motif bits. La bonne r\u00e9ponse devrait alors \u00eatre\u2009:

    if ((x & mask) == bits)\n
    ", "tags": ["STOP", "stdin", "for", "switch", "bits"]}, {"location": "course-c/15-fundations/darkside/", "title": "Le c\u00f4t\u00e9 obscure", "text": ""}, {"location": "course-c/15-fundations/darkside/#shadowy-escape", "title": "Shadowy escape", "text": "

    Exercice 1\u2009: Shadowy escape

    Que retourne la fonction f ?

    ```c

    Exercice 2\u2009: Curieux entier

    Que vaut x

    int32_t x = '0123';\n
    • Erreur de compilation
    • 123
    • 0x30313233
    • 4817243
    • Warning multi-character character constant

    Exercice 3\u2009: Vilains crochets

    Que vaut x

    char x = \"hello\"[4];\n
    • Erreur de compilation
    • 'o'
    • 'h'
    • 0

    Exercice 4\u2009: La r\u00e8gle gourmande

    Que vaut x

    int x = 0;\nx = x+++x++;\n
    • 0
    • 1
    • 2
    • 3
    Solution

    L'expression x+++x++ est \u00e9quivalente \u00e0 x++ + x++. L'op\u00e9rateur ++ est associatif \u00e0 droite, donc x++ est \u00e9valu\u00e9 en premier, puis x++ est \u00e9valu\u00e9. x est incr\u00e9ment\u00e9 deux fois, donc x vaut 2. On parle de greedy lexer rule. Le parseur est gourmand et prend le plus grand nombre de caract\u00e8res possible pour former un jeton.

    Cela ne fonctionne pas \u00e0 tous les coups. L'expression x+++++y est invalide car l'op\u00e9rateur ++ ne peut pas \u00eatre suivi d'un autre op\u00e9rateur ++. Il est n\u00e9cessaire dans ce cas de s\u00e9parer les op\u00e9rateurs par des espaces pour que le code soit valide\u2009: x+++ ++y.

    Exercice 5\u2009: Collision globale

    Qu'affiche le programme\u2009?

    a.c
    int x = 42;\n
    b.c
    int x;\n{\n    printf(\"%d\\n\", x);\n}\n
    $ gcc a.c b.c && ./a.out\n
    • 42
    • 0
    • Ind\u00e9fini
    • Erreur de compilation
    "}, {"location": "course-c/15-fundations/darkside/#include", "title": "include", "text": "

    int x = 42\u2009; int f() { int x = 23\u2009; { extern int x\u2009; return x\u2009; } }

    • 42
    • 23
    • 0
    • 1
    "}, {"location": "course-c/15-fundations/datatype/", "title": "Types de donn\u00e9es", "text": "Well-typed programs don\u2019t go wrong.Robin Milner

    Inh\u00e9rent au fonctionnement interne d\u2019un ordinateur, un langage de programmation op\u00e8re \u00e0 un certain degr\u00e9 d\u2019abstraction par rapport au mode de stockage des donn\u00e9es dans la m\u00e9moire. De la m\u00eame fa\u00e7on qu\u2019il est impossible, dans la vie quotidienne, de rendre la monnaie \u00e0 une fraction de centime pr\u00e8s, un ordinateur ne peut enregistrer des informations num\u00e9riques avec une pr\u00e9cision infinie. Ce principe est intrins\u00e8que aux limites mat\u00e9rielles et au mod\u00e8le math\u00e9matique des nombres.

    Les langages de programmation se divisent ainsi en deux grandes cat\u00e9gories\u2009: ceux que l\u2019on qualifie de typ\u00e9s, o\u00f9 le programmeur a la charge explicite de d\u00e9finir la mani\u00e8re dont les donn\u00e9es seront stock\u00e9es, et ceux dits non typ\u00e9s, o\u00f9 ce choix est g\u00e9r\u00e9 implicitement. Chaque approche pr\u00e9sente des avantages et des inconv\u00e9nients. Reprenons l\u2019exemple du rendu de monnaie\u2009: s\u2019il \u00e9tait possible d\u2019enregistrer des montants avec une pr\u00e9cision sup\u00e9rieure \u00e0 celle des pi\u00e8ces en circulation, disons \u00e0 la fraction de centime, cela poserait probl\u00e8me pour qu\u2019un caissier puisse rendre la monnaie correctement. Dans de telles situations, un langage typ\u00e9 s\u2019av\u00e8re plus adapt\u00e9, car il permet de fixer des bornes pertinentes \u00e0 la pr\u00e9cision des donn\u00e9es. C, en ce sens, est un langage fortement typ\u00e9, ce qui convient particuli\u00e8rement \u00e0 la manipulation rigoureuse des donn\u00e9es financi\u00e8res, entre autres.

    Il convient de noter que les types de donn\u00e9es ne se limitent pas aux seules informations num\u00e9riques. On trouve des types plus \u00e9labor\u00e9s, capables de repr\u00e9senter des caract\u00e8res individuels comme A ou B, ou m\u00eame des structures plus complexes. Ce chapitre a pour vocation de familiariser le lecteur avec les diff\u00e9rents types de donn\u00e9es disponibles en C et leur utilisation optimale.

    Standard ISO 80000-2

    Les ing\u00e9nieurs ont une pr\u00e9dilection marqu\u00e9e pour les standards, et cela d'autant plus lorsqu\u2019ils sont de port\u00e9e internationale. Pour pr\u00e9venir des erreurs aussi regrettables que le crash d'une fus\u00e9e d\u00fb \u00e0 une incompr\u00e9hension entre deux ing\u00e9nieurs de nations diff\u00e9rentes, il existe des normes telles que l'ISO 80000-2, qui d\u00e9finit avec rigueur ce que l'on entend par un entier (incluant ou non le z\u00e9ro), la nature des nombres r\u00e9els, et bien d'autres concepts math\u00e9matiques fondamentaux. Il va sans dire que les compilateurs, lorsqu\u2019ils sont correctement con\u00e7us, s'efforcent de respecter ces normes internationales au plus pr\u00e8s. Et vous, en tant que d\u00e9veloppeur, faites-vous de m\u00eame\u2009?

    ", "tags": ["iso-80000-2"]}, {"location": "course-c/15-fundations/datatype/#stockage-et-interpretation", "title": "Stockage et interpr\u00e9tation", "text": "

    Ancrez-vous \u00e7a bien dans la cabosse\u2009: un ordinateur ne peut stocker l'information que sous forme binaire et il ne peut manipuler ces informations que par paquets d'octets.

    Un ordinateur 64-bits manipulera avec aisance des paquets de 64-bits, mais plus difficilement des paquets de 32-bits. Un microcontr\u00f4leur 8-bit devra quant \u00e0 lui faire plusieurs manipulations pour lire une donn\u00e9e 32-bits. Il est donc important par souci d'efficacit\u00e9 d'utiliser la taille appropri\u00e9e \u00e0 la quantit\u00e9 d'information que l'on souhaite stocker.

    Quant \u00e0 la repr\u00e9sentation, consid\u00e9rons le paquet de 32-bit suivant, \u00eates-vous \u00e0 m\u00eame d'en donner une signification\u2009?

    01000000 01001001 00001111 11011011\n

    Il y a une infinit\u00e9 d'interpr\u00e9tations possibles, mais voici quelques pistes les plus probables\u2009:

    1. 4 caract\u00e8res de 8-bits\u2009: 01000000 @, 01001001 I, 00001111 \\x0f et 11011011 \u00db.
    2. 4 nombres de 8-bits\u2009: 64, 73, 15, 219.
    3. Deux nombres de 16-bits 18752 et 56079.
    4. Un seul nombre de 32-bit 3675212096.
    5. Peut-\u00eatre le nombre -40331460896358400.000000 lu en little endian.
    6. Ou encore 3.141592 lu en big endian.

    Qu'en pensez-vous\u2009?

    Lorsque l'on souhaite programmer \u00e0 bas niveau, vous voyez que la notion de type de donn\u00e9e est essentielle, car en dehors d'une interpr\u00e9tation subjective\u2009: \u00ab\u2009c'est forc\u00e9ment PI la bonne r\u00e9ponse\u2009\u00bb, rien ne permet \u00e0 l'ordinateur d'interpr\u00e9ter convenablement l'information enregistr\u00e9e en m\u00e9moire. Le typage permet de r\u00e9soudre toute ambigu\u00eft\u00e9.

    \u00c0 titre d'exemple, le programme suivant reprend notre question pr\u00e9c\u00e9dente et affiche les diff\u00e9rentes interpr\u00e9tations possibles selon diff\u00e9rents types de donn\u00e9es du langage C. Vous n'avez pas encore vu tous les \u00e9l\u00e9ments pour comprendre ce programme, mais vous pouvez d\u00e9j\u00e0 en deviner le sens et surtout vous pouvez d\u00e9j\u00e0 essayer de l'ex\u00e9cuter pour voir si vos hypoth\u00e8ses \u00e9taient correctes.

    int main() {\n    union {\n        uint8_t u8[4];\n        uint16_t u16[2];\n        uint32_t u32;\n        float f32;\n    } u = { 0b01000000, 0b01001001, 0b00001111, 0b11011011 };\n\n    printf(\"'%c', '%c', '%c', '%c'\\n\", u.u8[0], u.u8[1], u.u8[2], u.u8[3]);\n    printf(\"%hhu, %hhu, %hhu, %hhu\\n\", u.u8[0], u.u8[1], u.u8[2], u.u8[3]);\n    printf(\"%hu, %hu\\n\", u.u16[0], u.u16[1]);\n    printf(\"%u\\n\", u.u32);\n    printf(\"%f\\n\", u.f32);\n    u.u32 = (\n        ((u.u32 >> 24) & 0xff) | // move byte 3 to byte 0\n        ((u.u32 << 8) & 0xff0000) | // move byte 1 to byte 2\n        ((u.u32 >> 8) & 0xff00) | // move byte 2 to byte 1\n        ((u.u32 << 24) & 0xff000000) // byte 0 to byte 3\n    );\n    printf(\"%f\\n\", u.f32);\n}\n

    "}, {"location": "course-c/15-fundations/datatype/#boutisme", "title": "Boutisme", "text": "

    Boutisme par J. J. Grandville (1838)

    La hantise de l\u2019ing\u00e9nieur bas-niveau, c\u2019est le concept de boutisme, ou endianess en anglais. Ce terme, popularis\u00e9 par l\u2019informaticien Danny Cohen, fait r\u00e9f\u00e9rence au livre Les Voyages de Gulliver de Jonathan Swift. Dans cette satire, les habitants de Lilliput se divisent en deux factions\u2009: ceux qui mangent leurs \u0153ufs \u00e0 la coque en commen\u00e7ant par le petit bout (les Little Endians) et ceux qui pr\u00e9f\u00e8rent le gros bout (les Big Endians), engendrant un conflit absurde.

    En informatique, cette question, loin d\u2019\u00eatre triviale, persiste dans le monde des microprocesseurs. Certains fonctionnent en big endian, o\u00f9 les octets sont stock\u00e9s en m\u00e9moire du plus significatif au moins significatif, tandis que d'autres adoptent le format little endian, inversant cet ordre. Imaginons qu\u2019une donn\u00e9e soit enregistr\u00e9e en m\u00e9moire ainsi\u2009:

    [0x40, 0x49, 0x0F, 0xDB]\n

    Doit-on lire ces octets de gauche \u00e0 droite (comme en big endian) ou de droite \u00e0 gauche (comme en little endian) ? Ce probl\u00e8me, bien qu\u2019il semble anodin, devient crucial dans des contextes internationaux. Si ce texte \u00e9tait \u00e9crit en arabe, une langue lue de droite \u00e0 gauche, votre perception pourrait \u00eatre diff\u00e9rente.

    Prenons un exemple plus concret. Un microcontr\u00f4leur big endian 8 bits envoie via Bluetooth la valeur 1'111'704'645 \u2013 repr\u00e9sentant, par exemple, le nombre de photons d\u00e9tect\u00e9s par un capteur optique. Il transmet les octets suivants\u2009: 0x42, 0x43, 0x44, 0x45. Cependant, l\u2019ordinateur qui re\u00e7oit ces donn\u00e9es en mode little endian interpr\u00e8te cette s\u00e9quence comme 1'162'101'570. Ce d\u00e9calage dans la lecture est un probl\u00e8me courant auquel les ing\u00e9nieurs en \u00e9lectronique se heurtent fr\u00e9quemment dans leur carri\u00e8re.

    Le boutisme intervient donc dans la mani\u00e8re de stocker et transmettre des donn\u00e9es, chaque approche ayant ses avantages et inconv\u00e9nients. Personnellement, en mati\u00e8re d\u2019\u0153ufs, je pr\u00e9f\u00e8re le gros boutisme : je trouve qu\u2019il est plus pratique de manger un \u0153uf \u00e0 la coque en le commen\u00e7ant par le gros bout. En informatique, cependant, les arguments des deux camps se valent, et les choix d\u00e9pendent souvent des exigences du syst\u00e8me.

    Pour mieux illustrer ce concept, prenons un exemple en base 10, plus accessible. Imaginez que je doive transmettre un nombre, comme 532, par un tuyau dans lequel une seule boule peut passer \u00e0 la fois, chaque boule repr\u00e9sentant un chiffre. Dois-je envoyer la boule marqu\u00e9e 5, puis 3, puis 2 ? Ou devrais-je commencer par la boule marqu\u00e9e 2 et terminer par celle portant le 5 ? Dans notre culture, nous lisons de gauche \u00e0 droite, mais lorsque les donn\u00e9es sont stock\u00e9es dans un nombre fixe de bits, comme c\u2019est le cas en informatique, les deux m\u00e9thodes se justifient.

    Par exemple, si je vous transmets le nombre 7 et vous dis qu'il est inf\u00e9rieur \u00e0 10, en big endian, vous devrez attendre deux boules suppl\u00e9mentaires (0, 0, 7), tandis qu\u2019en little endian, vous saurez imm\u00e9diatement que c'est 7 (7, 0, 0). C\u2019est cette raison de simplicit\u00e9 dans la gestion des petites valeurs qui a largement contribu\u00e9 \u00e0 la popularit\u00e9 du little endian dans les syst\u00e8mes modernes.

    Techniquement parlant, voici la repr\u00e9sentation en m\u00e9moire de trois entiers 32 bits\u2009:

    16909060, 42, 10000\n01 02 03 04  00 00 00 2a  00 00 27 10 (big endian)\n04 03 02 01  2a 00 00 00  10 27 00 00 (little endian)\n

    Ainsi, le boutisme n'est pas qu'un d\u00e9tail technique\u2009: c'est une question cruciale pour l'interop\u00e9rabilit\u00e9 des syst\u00e8mes num\u00e9riques.

    Organisation par type

    Selon le boutisme, ce n'est pas toute l'information qui est invers\u00e9e, mais l'ordre des octets au sein de chaque nombre.

    R\u00e9seau informatique

    Le r\u00e9seau informatique comme les protocoles TCP/IP, UDP, Wi-Fi utilisent le network byte order qui impose le big endian pour l'envoi des donn\u00e9es. Cela remonte aux premi\u00e8res normes dont l'id\u00e9e \u00e9tait d'adopter une convention unique et standardis\u00e9e \u00e0 une \u00e9poque ou les ordinateurs big endian \u00e9taient majoritaires. Or aujourd'hui la tr\u00e8s vaste majorit\u00e9 des ordinateurs sont en little endian et donc les donn\u00e9es transmises et r\u00e9ceptionn\u00e9es doivent \u00eatre converties. C'est le r\u00f4le de la fonction htonl (host to network long) qui convertit un entier 32 bits en big endian ou ntohl (network to host long) qui fait l'op\u00e9ration inverse.

    Pour vous lecteurs, cela n'a pas de grande importance, car nous n'allons pas approfondir le fonctionnement du r\u00e9seau informatique dans cet ouvrage.

    ", "tags": ["ntohl", "htonl"]}, {"location": "course-c/15-fundations/datatype/#les-nombres-entiers", "title": "Les nombres entiers", "text": "

    Les nombres entiers que nous avons d\u00e9finis plus t\u00f4t peuvent \u00eatre n\u00e9gatifs, nuls ou positifs. En C, il existe plusieurs types de donn\u00e9es pour les repr\u00e9senter, chacun ayant ses propres caract\u00e9ristiques.

    "}, {"location": "course-c/15-fundations/datatype/#les-entiers-naturels", "title": "Les entiers naturels", "text": "

    En informatique, les entiers naturels de l'ensemble \\(\\mathbb{N}\\) sont non sign\u00e9s, et peuvent prendre des valeurs comprises entre \\(0\\) et \\(2^N-1\\) o\u00f9 \\(N\\) correspond au nombre de bits avec lesquels la valeur num\u00e9rique sera stock\u00e9e en m\u00e9moire. Il faut naturellement que l'ordinateur sur lequel s'ex\u00e9cute le programme soit capable de supporter le nombre de bits demand\u00e9 par le programmeur. En C, on nomme ce type de donn\u00e9e unsigned int, int \u00e9tant le d\u00e9nominatif du latin integer signifiant \u00ab\u2009entier\u2009\u00bb.

    Voici quelques exemples des valeurs minimales et maximales possibles selon le nombre de bits utilis\u00e9s pour coder l'information num\u00e9rique\u2009:

    Stockage d'un entier non sign\u00e9 sur diff\u00e9rentes profondeurs Profondeur Minimum Maximum 8 bits 0 255 (\\(2^8 - 1\\)) 16 bits 0 65'535 (\\(2^{16} - 1\\)) 32 bits 0 4'294'967'295 (\\(2^{32} - 1\\)) 64 bits 0 18'446'744'073'709'551'616 (\\(2^{64} - 1\\))

    Notez l'importance du \\(-1\\) dans la d\u00e9finition du maximum, car la valeur minimum \\(0\\) fait partie de l'information m\u00eame si elle repr\u00e9sente une quantit\u00e9 nulle. Il y a donc 256 valeurs possibles pour un nombre entier non sign\u00e9 8-bits, bien que la valeur maximale ne soit que de 255.

    ", "tags": ["int"]}, {"location": "course-c/15-fundations/datatype/#les-entiers-bornes-signes", "title": "Les entiers born\u00e9s sign\u00e9s", "text": "

    Les entiers sign\u00e9s peuvent \u00eatre n\u00e9gatifs, nuls ou positifs et peuvent prendre des valeurs comprises entre \\(-2^{N-1}\\) et \\(+2^{N-1}-1\\) o\u00f9 \\(N\\) correspond au nombre de bits avec lesquels la valeur num\u00e9rique sera stock\u00e9e en m\u00e9moire. Notez l'asym\u00e9trie entre la borne positive et n\u00e9gative.

    Comme il sont sign\u00e9s (signed en anglais), il est par cons\u00e9quent correct d'\u00e9crire signed int bien que le pr\u00e9fixe signed soit optionnel, car le standard d\u00e9finit qu'un entier est par d\u00e9faut sign\u00e9. La raison \u00e0 cela rel\u00e8ve plus du lourd historique de C qu'\u00e0 des pr\u00e9ceptes logiques et rationnels.

    Voici quelques exemples de valeurs minimales et maximales selon le nombre de bits utilis\u00e9s pour coder l'information\u2009:

    Stockage d'un entier sign\u00e9 sur diff\u00e9rentes profondeurs Profondeur Minimum Maximum 8 bits -128 +127 16 bits -32'768 +32'767 32 bits -2'147'483'648 +2'147'483'647 64 bits -9'223'372'036'854'775'808 +9'223'372'036'854'775'807

    En m\u00e9moire, ces nombres sont stock\u00e9s en utilisant le compl\u00e9ment \u00e0 deux que nous avons d\u00e9j\u00e0 \u00e9voqu\u00e9.

    ", "tags": ["signed"]}, {"location": "course-c/15-fundations/datatype/#les-entiers-bornes", "title": "Les entiers born\u00e9s", "text": "

    Comme nous l'avons vu, les degr\u00e9s de libert\u00e9 pour d\u00e9finir un entier sont\u2009:

    • Sign\u00e9 ou non sign\u00e9
    • Nombre de bits avec lesquels l'information est stock\u00e9e en m\u00e9moire

    \u00c0 l'origine le standard C restait flou quant au nombre de bits utilis\u00e9s pour chacun des types et aucune r\u00e9elle coh\u00e9rence n'existait pour la construction d'un type. Le modificateur signed \u00e9tait optionnel, le pr\u00e9fixe long ne pouvait s'appliquer qu'au type int et long et la confusion entre long (pr\u00e9fixe) et long (type) restait possible. En fait, la plupart des d\u00e9veloppeurs s'y perdaient et s'y perd toujours ce qui menait \u00e0 des probl\u00e8mes de compatibilit\u00e9s des programmes entre eux.

    ", "tags": ["int", "long", "signed"]}, {"location": "course-c/15-fundations/datatype/#types-standards", "title": "Types standards", "text": "

    La construction d'un type entier C peut \u00eatre r\u00e9sum\u00e9e par la figure suivante\u2009:

    Entiers standardis\u00e9s

    Le pr\u00e9fixe signed est implicite, mais il est possible de l'utiliser pour plus de clart\u00e9. En pratique il sera rarement utilis\u00e9. De m\u00eame, lorsque short, long ou long long est utils\u00e9, le suffixe int est implicite.

    Les types suivants sont donc des synonymes\u2009:

    // Entier sign\u00e9\nsigned int, int, signed\n\n// Entier non sign\u00e9\nunsigned int, unsigned\n\n// Entier court sign\u00e9\nsigned short int, short, signed short\n\n// Entier court non sign\u00e9\nunsigned short int, unsigned short\n\n// Entier long sign\u00e9\nsigned long int, long, long int, signed long\n\n// Entier tr\u00e8s long sign\u00e9\nsigned long long int, long long, signed long long\n

    En revanche char est un type \u00e0 part enti\u00e8re sign\u00e9 par d\u00e9faut qui n'aura pas de suffixe int, mais il est possible de le d\u00e9clarer unsigned char.

    Ci-dessous la table des entiers standards en C. Le format est celui utilis\u00e9 par la fonction printf de la biblioth\u00e8que standard C.

    Table des types entiers en C Type Signe Profondeur Format char, signed char signed au moins 8 bits %c unsigned char unsigned au moins 8 bits %uc short, short int, ... signed au moins 16 bits %hi unsigned short, ... unsigned au moins 16 bits %hu unsigned, unsigned int unsigned au moins 32 bits %u int, signed, signed int signed au moins 32 bits %d unsigned, unsigned int, ... unsigned au moins 32 bits %u long, long int, ... signed au moins 32 bits %li unsigned long, .. unsigned au moins 32 bits %lu long long, ... signed au moins 64 bits %lli unsigned long long, ... unsigned au moins 64 bits %llu

    Avec l'av\u00e8nement de C99, une meilleure coh\u00e9sion des types a \u00e9t\u00e9 propos\u00e9e dans le fichier d'en-t\u00eate stdint.h. Cette biblioth\u00e8que standard offre les types suivants\u2009:

    Flux de construction d'un entier standardis\u00e9

    ", "tags": ["stdint.h", "char", "printf", "signed", "short", "long", "int", "unsigned"]}, {"location": "course-c/15-fundations/datatype/#nouveaux-types-standard", "title": "Nouveaux types standard", "text": "

    Avec l'av\u00e8nement de C99, une meilleure coh\u00e9sion des types a \u00e9t\u00e9 propos\u00e9e dans le fichier d'en-t\u00eate stdint.h. Cette biblioth\u00e8que standard offre les types suivants. Comme nous l'avons vu, la taille des types historiques n'est pas pr\u00e9cis\u00e9ment d\u00e9finie par le standard. On sait qu'un int contient au moins 16-bits, mais il peut, selon l'architecture, et aussi le mod\u00e8le de donn\u00e9e, prendre n'importe quelle valeur sup\u00e9rieure. Ceci pose des probl\u00e8mes de portabilit\u00e9 possibles si le d\u00e9veloppeur n'est pas suffisamment consciencieux et qu'il ne s'appuie pas sur une batterie de tests automatis\u00e9s. En cons\u00e9quence, il est recommand\u00e9 d'utiliser les types de <stdint.h> lorsque la taille du type doit \u00eatre garantie.

    Attention cependant \u00e0 noter que garantir un type \u00e0 taille fixe n'est pas toujours la meilleure solution. En effet, si vous avez besoin d'un entier de 32-bits, il est pr\u00e9f\u00e9rable d'utiliser int qui sera adapt\u00e9 \u00e0 l'architecture mat\u00e9rielle. Si vous utilisez int32_t vous risquez de perdre en performance si l'architecture mat\u00e9rielle est capable de traiter des entiers 64-bits de mani\u00e8re plus efficace. Voici les types \u00e0 taille fixe de <stdint.h> :

    Entiers standard d\u00e9fini par stdint Type Signe Profondeur Format uint8_t unsigned 8 bits %c int8_t signed 8 bits %c uint16_t unsigned 16 bits %hu int16_t signed 16 bits %hi uint32_t unsigned 32 bits %u int32_t signed 32 bits %d uint64_t unsigned 64 bits %llu int64_t signed 64 bits %lli

    \u00c0 ces types s'ajoutent les types rapides (fast) et minimums (least). Un type nomm\u00e9 uint_least32_t garanti l'utilisation du type de donn\u00e9e utilisant le moins de m\u00e9moire et garantissant une profondeur d'au minimum 32 bits. Les types rapides, moins utilis\u00e9s, vont automatiquement choisir le type adapt\u00e9 le plus rapide \u00e0 l'ex\u00e9cution. Par exemple, si l'architecture mat\u00e9rielle permet un calcul natif sur 48-bits, elle sera privil\u00e9gi\u00e9e par rapport au type 32-bits.

    Exercice 1\u2009: D\u00e9bordement

    Quel sera le contenu de j apr\u00e8s l'ex\u00e9cution de l'instruction suivante\u2009:

    uint16_t j = 1024 * 64;\n
    • 0
    • 1
    • 64
    • 1024
    • 65536

    ", "tags": ["int32_t", "uint32_t", "int8_t", "uint16_t", "int16_t", "int64_t", "uint_least32_t", "uint8_t", "uint64_t", "stdint.h", "int"]}, {"location": "course-c/15-fundations/datatype/#modele-de-donnee", "title": "Mod\u00e8le de donn\u00e9e", "text": "

    Comme nous l'avons \u00e9voqu\u00e9 plus haut, la taille des entiers short, int, ... n'est pas pr\u00e9cis\u00e9ment d\u00e9finie par le standard. On sait qu'un int contient au moins 16-bits, mais il peut, selon l'architecture, et aussi le mod\u00e8le de donn\u00e9e, prendre n'importe quelle valeur sup\u00e9rieure.

    Admettons que ce d\u00e9veloppeur sans scrupule d\u00e9veloppe un programme complexe sur sa machine 64-bits en utilisant un int comme valeur de comptage allant au-del\u00e0 de dix milliards. Apr\u00e8s tests, son programme fonctionne sur sa machine, ainsi que celle de son coll\u00e8gue. Mais lorsqu'il livre le programme \u00e0 son client, le processus crash. En effet, la taille du int sur l'ordinateur du client est de 32-bits. Comment peut-on s'affranchir de ce type de probl\u00e8me\u2009?

    La premi\u00e8re solution est de toujours utiliser les types propos\u00e9s par <stdint.h> lorsque la taille du type n\u00e9cessaire est sup\u00e9rieure \u00e0 la valeur garantie. L'autre solution est de se fier au mod\u00e8le de donn\u00e9es. Le mod\u00e8le de donn\u00e9es est une convention qui d\u00e9finit la taille des types de donn\u00e9es de base. Il est d\u00e9termin\u00e9 par l'architecture mat\u00e9rielle et le syst\u00e8me d'exploitation. Voici un tableau r\u00e9sumant les mod\u00e8les de donn\u00e9es les plus courants\u2009:

    Mod\u00e8le de donn\u00e9es Mod\u00e8le short int long long long size_t Syst\u00e8me d'exploitation LP32 16 16 32 32 Windows 16-bits, Apple Macintosh ILP32 16 32 32 64 32 Windows x86, Linux 32-bits LLP64 16 32 32 64 64 Microsoft Windows x86-64 LP64 16 32 64 64 64 Unix, Linux, macOS ILP64 16 64 64 64 64 HAL (SPARC) SILP64 64 64 64 64 64 UNICOS (Super ordinateur)

    Taille usuelle des types de base Type Taille Windows Linux char habituellement 8 bits 1 1 short au moins 16 bits 2 2 int taille naturelle pour l'architecture 4 4 long au moins 32 bits 4 8 long long au moins 64 bits 8 8 float normalement 32 bits 4 4 double normalement 64 bits 8 8 long double au moins 63 bits 8 16

    Les troisi\u00e8me et quatri\u00e8me colonnes repr\u00e9sentent la taille des types de base sur des machines modernes 64-bits. On notera que la taille des types long et long double varie selon l'architecture mat\u00e9rielle et le syst\u00e8me d'exploitation. On voit donc que selon le mod\u00e8le les types n'ont pas la m\u00eame taille et donc que la portabilit\u00e9 des programmes est un enjeu majeur. Aussi, pour s'assurer qu'un type est de la taille souhait\u00e9e, il est recommand\u00e9 d'utiliser les nouveaux types standards de <stdint.h>. Ainsi pour s'assurer qu'un type soit au moins de 32-bits, on utilisera uint_least32_t.

    ", "tags": ["short", "long", "int", "uint_least32_t"]}, {"location": "course-c/15-fundations/datatype/#les-caracteres", "title": "Les caract\u00e8res", "text": "

    Les caract\u00e8res, ceux que vous voyez dans cet ouvrage, sont g\u00e9n\u00e9ralement repr\u00e9sent\u00e9s par des grandeurs exprim\u00e9es sur 1 octet (8-bits):

    97 \u2261 0b1100001 \u2261 'a'\n

    Un caract\u00e8re du clavier enregistr\u00e9 en m\u00e9moire c'est donc un nombre entier de 8-bits. En C, le type de donn\u00e9e char est utilis\u00e9 pour stocker un caract\u00e8re.

    Mais comment un ordinateur sait-il que 97 correspond \u00e0 a ? C'est l\u00e0 que la notion d'encodage entre en jeu.

    ", "tags": ["char"]}, {"location": "course-c/15-fundations/datatype/#la-table-ascii", "title": "La table ASCII", "text": "

    Historiquement, alors que les informations dans un ordinateur ne sont que des 1 et des 0, il a fallu \u00e9tablir une correspondance entre une grandeur binaire et le caract\u00e8re associ\u00e9. Un standard a \u00e9t\u00e9 propos\u00e9 en 1963 par l'ASA (American Standards Association) aujourd'hui ANSI qui ne d\u00e9finissait alors que 63 caract\u00e8res imprimables. Comme la m\u00e9moire \u00e0 cette \u00e9poque \u00e9tait tr\u00e8s cher, un caract\u00e8re n'\u00e9tait cod\u00e9 que sur 7 bits. La premi\u00e8re table ASCII d\u00e9finissait donc 128 caract\u00e8res et est donn\u00e9e par la figure suivante\u2009:

    Table ASCII ASA X3.4 \u00e9tablie en 1963

    En 1986, la table ASCII a \u00e9t\u00e9 \u00e9tendue pour couvrir les caract\u00e8res majuscules et minuscules. Cette r\u00e9forme est donn\u00e9e par la figure suivante. Il s'agit de la table ASCII standard actuelle.

    Table ANSI INCITS 4-1986 (standard actuel)

    Ainsi qu'\u00e9voqu\u00e9 plusieurs fois dans cet ouvrage, chaque pays et chaque langue utilise ses propres caract\u00e8res et il a fallu trouver un moyen de satisfaire tout le monde. Il a \u00e9t\u00e9 alors convenu d'encoder les caract\u00e8res sur 8-bits au lieu de 7 et de profiter des 128 nouvelles positions offertes pour ajouter les caract\u00e8res manquants telles que les caract\u00e8res accentu\u00e9s, le signe euro, la livre sterling et d'autres. Le standard ISO/IEC 8859 aussi appel\u00e9 standard Latin d\u00e9finit 16 tables d'extension selon les besoins des pays. Les plus courantes en Europe occidentale sont les tables ISO-8859-1 ou (latin1) et ISO-8859-15 (latin9). Voici la table d'extension de l'ISO-8859-1 et de l'ISO-8859-15 :

    Table d'extension ISO-8859-1 (haut) et ISO-8859-15 (bas)

    Ce standard a g\u00e9n\u00e9r\u00e9 durant des d\u00e9cennies de grandes frustrations et de profondes incompr\u00e9hensions chez les d\u00e9veloppeurs et utilisateurs d'ordinateur. Ne vous est-il jamais arriv\u00e9 d'ouvrir un fichier texte et de ne plus voir les accents convenablement\u2009? C'est un probl\u00e8me typique d'encodage.

    Pour tenter de rem\u00e9dier \u00e0 ce standard incompatible entre les pays, Microsoft a propos\u00e9 un standard nomm\u00e9 Windows-1252 s'inspirant de ISO-8859-1. En voulant rassembler en proposant un standard plus g\u00e9n\u00e9ral, Microsoft n'a contribu\u00e9 qu'\u00e0 proposer un standard suppl\u00e9mentaire venant s'inscrire dans une liste d\u00e9j\u00e0 trop longue. Et l'histoire n'est pas termin\u00e9e...

    C'est pourquoi, en 1991, l'ISO a propos\u00e9 un standard universel nomm\u00e9 Unicode qui est capable d'encoder tous les caract\u00e8res de toutes les langues du monde.

    ", "tags": ["iso-8859-15", "ascii", "iso-8859-1"]}, {"location": "course-c/15-fundations/datatype/#unicode", "title": "Unicode", "text": "

    Avec l'arriv\u00e9e d'internet et les \u00e9changes entre les Arabes (\u0639\u064e\u0631\u064e\u0628), les Cor\u00e9ens (\ud55c\uad6d\uc5b4), les Japonais qui poss\u00e8dent deux alphabets ainsi que des caract\u00e8res chinois (\u65e5\u672c\u8a9e), sans oublier l'ourdou (\u067e\u0627\u06a9\u0650\u0633\u062a\u0627\u0646) pakistanais et tous ceux que l'on ne mentionnera pas, il a fallu bien plus que 256 caract\u00e8res et quelques tables de correspondance. Ce pr\u00e9sent ouvrage, ne pourrait d'ailleurs par \u00eatre \u00e9crit sans avoir pu r\u00e9soudre, au pr\u00e9alable, ces probl\u00e8mes d'encodage\u2009; la preuve \u00e9tant, vous parvenez \u00e0 voir ces caract\u00e8res qui ne vous sont pas familiers.

    Un consensus plan\u00e9taire a \u00e9t\u00e9 atteint en 2008 avec l'adoption majoritaire du standard Unicode (Universal Coded Character Set) et son encodage UTF-8 (Unicode Transformation Format). Ce standard est capable d'encoder tous les caract\u00e8res de toutes les langues du monde. Il est utilis\u00e9 par la plupart des syst\u00e8mes d'exploitation, des navigateurs web et des applications informatiques. Il est capable d'encoder 1'112'064 caract\u00e8res en utilisant de 1 \u00e0 4 octets. La figure suivante montre la tendance de l'adoption de 2001 \u00e0 2012. Cette tendance est accessible ici.

    Tendances sur l'encodage des pages web en faveur de UTF-8 d\u00e8s 2001, donn\u00e9es collect\u00e9es par Google et Erik van der Poel

    Ken Thompson, dont nous avons d\u00e9j\u00e0 parl\u00e9 en introduction, est \u00e0 l'origine de ce standard. Par exemple le devanagari caract\u00e8re \u0939 utilis\u00e9 en Sanskrit poss\u00e8de la d\u00e9nomination Unicode 0939 et s'encode sur 3 octets\u2009: 0xE0 0xA4 0xB9

    En programmation C, un caract\u00e8re char ne peut exprimer sans ambig\u00fcit\u00e9 que les 128 caract\u00e8res de la table ASCII standard et selon les conventions locales, les 128 caract\u00e8res d'extension. C'est-\u00e0-dire que vous ne pouvez pas exprimer un caract\u00e8re Unicode en utilisant un char. Pour cela, il faudra utiliser un tableau de caract\u00e8res char ou un tableau de caract\u00e8res wchar_t qui est capable de stocker un caract\u00e8re Unicode, mais nous verrons cela plus tard.

    Voici par exemple comment d\u00e9clarer une variable contenant le caract\u00e8re dollar\u2009:

    char c = '$';\n

    3 ou '3'

    Attention \u00e0 la pr\u00e9sence des guillemets simples car le caract\u00e8re '3' n'est pas \u00e9gal au nombre 3. Le caract\u00e8re 3 correspond selon la table ASCII standard \u00e0 la valeur 0x33 et donc au nombre 51 en d\u00e9cimal.

    #include <stdio.h>\n\nint main(void) {\n    char c = '3';\n    printf(\"Le caract\u00e8re %c vaut 0x%x en hexad\u00e9cimal ou %d en d\u00e9cimal.\\n\",\n        c, c, c);\n    return 0;\n}\n
    ", "tags": ["char", "wchar_t"]}, {"location": "course-c/15-fundations/datatype/#les-emojis", "title": "Les emojis", "text": "

    Les emojis sont des caract\u00e8res sp\u00e9ciaux qui ont \u00e9t\u00e9 introduits en 2010 par le standard Unicode 6.0. Ils sont donc cod\u00e9s sur 4 octets et permettent de repr\u00e9senter des \u00e9motions, des objets, des animaux, des symboles ou des \u00e9trons (\ud83d\udca9).

    Les \u00e9motic\u00f4nes que vous pouvez envoyer \u00e0 votre grand-m\u00e8re via WhatsApp sont donc des caract\u00e8res Unicode et non des images. Si vous dites \u00e0 votre grand-maman que vous l'aimez en lui envoyant un c\u0153ur, elle recevra le caract\u00e8re 2764 qui est le caract\u00e8re \u2764. Mais les navigateurs web et les applications informatiques remplacent \u00e0 la vol\u00e9e ces caract\u00e8res par des images.

    Ceci est vrai, mais encore faut-il que la police d'\u00e9criture utilis\u00e9e par votre ch\u00e8re grand-maman soit capable d'afficher ce caract\u00e8re. Si ce n'est pas le cas, elle verra probablement le caract\u00e8re \ufffd qui est un caract\u00e8re de remplacement tr\u00e8s disgracieux et qui ne d\u00e9montre pas tout l'amour que vous lui portez.

    ", "tags": ["emojis"]}, {"location": "course-c/15-fundations/datatype/#chaine-de-caracteres", "title": "Cha\u00eene de caract\u00e8res", "text": "

    Une cha\u00eene de caract\u00e8res est simplement la suite contigu\u00eb de plusieurs caract\u00e8res dans une zone m\u00e9moire donn\u00e9e. Afin de savoir lorsque cette cha\u00eene se termine, le standard impose que le dernier caract\u00e8re d'une cha\u00eene soit NUL ou \\0. On appelle ce caract\u00e8re le caract\u00e8re de fin de cha\u00eene. Il s'agit d'une sentinelle.

    Les l\u00e9gumes et les choux

    Imaginez que l'on vous demande de vous placer dans un champ et de d\u00e9terrer n'importe quel l\u00e9gume sauf un chou. Votre algorithme est\u2009:

    %% Algorithme de d\u00e9terrage de l\u00e9gumes\nflowchart LR\n    start(D\u00e9but) --> pick[D\u00e9terrer]\n    pick --> if{Choux?}\n    if --Non--> step[Avancer de 1 pas]\n    step --> pick\n    if --Oui--> stop(Fin)
    Algorithme de d\u00e9terrage de l\u00e9gumes

    Si vous trouvez un chou, vous savez que vous \u00eates arriv\u00e9s au bout du champ. Le chou fait office de sentinelle.

    Sans sentinelle, vous \u00eates oblig\u00e9 de conna\u00eetre \u00e0 l'avance le nombre de pas \u00e0 faire pour arriver au bout du champ. Vous devez donc stocker en m\u00e9moire cette information additionnelle ce qui n'est pas pratique.

    La cha\u00eene de caract\u00e8re Hello sera en m\u00e9moire stock\u00e9e en utilisant les codes ASCII suivants.

    char string[] = \"Hello\";\n
      H   E   L   L   O  \\0\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 72\u2502101\u2502108\u2502108\u2502111\u2502 0 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n\n 0x00  0b01001000\n 0x01  0b01100101\n 0x02  0b01101100\n 0x03  0b01101100\n 0x04  0b01101111\n 0x05  0b00000000\n

    On utilise le caract\u00e8re nul \\0 pour plusieurs raisons\u2009:

    1. Il est facilement reconnaissable.
    2. Dans un test il vaut false.
    3. Il n'est pas imprimable.

    Avertissement

    Ne pas confondre le caract\u00e8re nul \\0 avec le caract\u00e8re 0. Le premier est un caract\u00e8re de fin de cha\u00eene, le second est un caract\u00e8re num\u00e9rique qui vaut 0x30. Le caract\u00e8re nul est la valeur 0 selon la table ASCII.

    ", "tags": ["NUL", "sentinelle", "Hello", "false"]}, {"location": "course-c/15-fundations/datatype/#booleens", "title": "Bool\u00e9ens", "text": "

    Un bool\u00e9en est un type de donn\u00e9e \u00e0 deux \u00e9tats consensuellement nomm\u00e9s vrai (true) et faux (false) et destin\u00e9s \u00e0 repr\u00e9senter les \u00e9tats en logique bool\u00e9enne (Nom venant de George Boole, fondateur de l'alg\u00e8bre \u00e9ponyme).

    La convention est d'utiliser 1 pour m\u00e9moriser un \u00e9tat vrai, et 0 pour un \u00e9tat faux, c'est d'ailleurs de cette mani\u00e8re que les bool\u00e9ens sont encod\u00e9s en C.

    Les bool\u00e9ens ont \u00e9t\u00e9 introduits formellement en C avec C99 et n\u00e9cessitent l'inclusion du fichier d'en-t\u00eate <stdbool.h>. Avant cela le type bool\u00e9en \u00e9tait _Bool et d\u00e9finir les \u00e9tats vrais et faux \u00e9tait \u00e0 la charge du d\u00e9veloppeur.

    #include <stdbool.h>\n\nbool is_enabled = false;\nbool has_tail = true;\n

    Afin de faciliter la lecture du code, il est courant de pr\u00e9fixer les variables bool\u00e9ennes avec les pr\u00e9fixes is_ ou has_. \u00c0 titre d'exemple, si l'on souhaite stocker le genre d'un individu (m\u00e2le, ou femelle), on pourrait utiliser la variable is_male.

    Bien qu'un bool\u00e9en puisse \u00eatre stock\u00e9 sur un seul bit, en pratique, il est stock\u00e9 sur un octet, voire m\u00eame sur un mot de 32 ou 64 bits. Cela est d\u00fb \u00e0 la mani\u00e8re dont les processeurs manipulent les donn\u00e9es en m\u00e9moire. Sur une architecture LP64, un bool\u00e9en sera stock\u00e9 sur 8 octets. Les valeurs true et false vaudront donc\u2009:

    00 00 00 00 00 00 00 00   false\n00 00 00 00 00 00 00 01   true\n

    N\u00e9anmoins, il est possible d'utiliser le type char pour stocker un bool\u00e9en. On peut \u00e9galement utiliser de l'arithm\u00e9tique binaire pour stocker 8 bool\u00e9en sur un uint8_t. Voici un exemple de stockage de 8 bool\u00e9ens sur un uint8_t :

    #include <stdint.h>\n\nuint8_t flags = 0b00000000;\n\nint main (void) {\n    flags |= 1 << 3; // Mettre le quatri\u00e8me bit \u00e0 1\n    flags &= ~(1 << 3); // Mettre le quatri\u00e8me bit \u00e0 0\n}\n
    ", "tags": ["_Bool", "true", "has_", "is_male", "uint8_t", "is_", "char", "false"]}, {"location": "course-c/15-fundations/datatype/#enumerations", "title": "\u00c9num\u00e9rations", "text": "

    Une \u00e9num\u00e9ration est un type de donn\u00e9e un peu particulier qui permet de d\u00e9finir un ensemble de valeurs possibles associ\u00e9es \u00e0 des noms symboliques. Ce style d'\u00e9criture permet de d\u00e9finir un type de donn\u00e9es contenant un nombre fini de valeurs. Ces valeurs sont nomm\u00e9es textuellement et d\u00e9finies num\u00e9riquement dans le type \u00e9num\u00e9r\u00e9.

    enum ColorCode {\n    COLOR_BLACK, // Vaut z\u00e9ro par d\u00e9faut\n    COLOR_BROWN,\n    COLOR_RED,\n    COLOR_ORANGE,\n    COLOR_YELLOW,\n    COLOR_GREEN,\n    COLOR_BLUE,\n    COLOR_PURPLE,\n    COLOR_GRAY,\n    COLOR_WHITE\n};\n

    Le type d'une \u00e9num\u00e9ration est apparent\u00e9 \u00e0 un entier int. Sans pr\u00e9cision, la premi\u00e8re valeur vaut 0, la suivante 1, etc. Il est n\u00e9anmoins possible de forcer les valeurs de la mani\u00e8re suivante\u2009:

    typedef enum country_codes {\n    CODE_SWITZERLAND=41,\n    CODE_BELGIUM=32,\n    CODE_FRANCE, // Sera 33...\n    CODE_SPAIN,  // Sera 34...\n    CODE_US=1\n} CountryCodes;\n

    Pour ne pas confondre un type \u00e9num\u00e9r\u00e9 avec une variable, on utilise souvent la convention d'une notation en capitales. Pour \u00e9viter d\u2019\u00e9ventuelles collisions avec d'autres types, un pr\u00e9fixe est souvent ajout\u00e9 ce qu'on appelle un espace de nommage.

    L'utilisation d'un type \u00e9num\u00e9r\u00e9 peut \u00eatre la suivante\u2009:

    void call(enum country_codes code) {\n    switch(code) {\n    case CODE_SWITZERLAND :\n        printf(\"Calling Switzerland, please wait...\\n\");\n        break;\n    case CODE_BELGIUM :\n        printf(\"Calling Belgium, please wait...\\n\");\n        break;\n    case CODE_FRANCE :\n        printf(\"Calling France, please wait...\\n\");\n        break;\n    default :\n        printf(\"No calls to this country are allowed yet!\\n\");\n    }\n}\n
    ", "tags": ["int"]}, {"location": "course-c/15-fundations/datatype/#type-incomplet", "title": "Type incomplet", "text": "

    En C, un type incomplet est un type de donn\u00e9es dont la taille n'est pas encore compl\u00e8tement d\u00e9finie au moment de sa d\u00e9claration. En d'autres termes, le compilateur sait qu'un type existe, mais ne conna\u00eet pas encore la totalit\u00e9 des d\u00e9tails n\u00e9cessaires pour allouer de la m\u00e9moire ou effectuer certaines op\u00e9rations sur ce type. Un type incomplet peut appara\u00eetre dans le cas des structures ou des tableaux, notamment pour l'abstraction de donn\u00e9es. Certains types comme void sont \u00e9galement incomplets.

    ", "tags": ["type-incomplet", "void"]}, {"location": "course-c/15-fundations/datatype/#vlq", "title": "VLQ", "text": "

    Dans certains syst\u00e8mes, on peut stocker des nombres entiers \u00e0 taille variable. C'est-\u00e0-dire que l'on s'arrange pour r\u00e9server un bit suppl\u00e9mentaire dans le nombre pour indiquer si le nombre se poursuit sur un autre octet. C'est le cas des nombres entiers VLQ utilis\u00e9s dans le protocole MIDI

    On peut stocker un nombre VLQ en m\u00e9moire, mais on ne sait pas de combien d'octets on aura besoin. On peut donc d\u00e9finir un type incomplet pour ce type de donn\u00e9e, mais nous aurons besoin de notions que nous n'avons pas encore vues pour le manipuler, les structures et les unions.

    ", "tags": ["midi", "vlq"]}, {"location": "course-c/15-fundations/datatype/#type-vide-void", "title": "Type vide (void)", "text": "

    Le type void est particulier. Il s'agit d'un type dit incomplet, car la taille de l'objet qu'il repr\u00e9sente en m\u00e9moire n'est pas connue. Il est utilis\u00e9 comme type de retour pour les fonctions qui ne retournent rien\u2009:

    void shout() {\n    printf(\"Hey!\\n\");\n}\n

    Il peut \u00eatre \u00e9galement utilis\u00e9 comme type g\u00e9n\u00e9rique comme la fonction de copie m\u00e9moire memcpy :

    void *memcpy(void * restrict dest, const void * restrict src, size_t n);\n

    Le mot cl\u00e9 void ne peut \u00eatre utilis\u00e9 que dans les contextes suivants\u2009:

    • Comme param\u00e8tre unique d'une fonction, indiquant que cette fonction n'a pas de param\u00e8tres int main(void)
    • Comme type de retour pour une fonction indiquant que cette fonction ne retourne rien void display(char c)
    • Comme pointeur dont le type de destination n'est pas sp\u00e9cifi\u00e9 void* ptr
    ", "tags": ["void", "memcpy"]}, {"location": "course-c/15-fundations/datatype/#transtypage", "title": "Transtypage", "text": ""}, {"location": "course-c/15-fundations/datatype/#promotion-implicite", "title": "Promotion implicite", "text": "

    G\u00e9n\u00e9ralement le type int est de la m\u00eame largeur que le bus m\u00e9moire de donn\u00e9e d'un ordinateur. C'est-\u00e0-dire que c'est souvent, le type le plus optimis\u00e9 pour v\u00e9hiculer de l'information au sein du processeur. Les registres du processeur, autrement dit ses casiers m\u00e9moires, sont au moins assez grand pour contenir un int.

    Aussi, la plupart des types de taille inf\u00e9rieure \u00e0 int sont automatiquement et implicitement promus en int. Le r\u00e9sultat de a + b lorsque a et b sont des char sera automatiquement un int.

    Promotion num\u00e9rique Type source Type cible char int short int int long long float float double

    Notez qu'il n'y a pas de promotion num\u00e9rique vers le type short. On passe directement \u00e0 un type int.

    Exercice 2\u2009: Promotion num\u00e9rique

    Repr\u00e9sentez les promotions num\u00e9riques qui surviennent lors de l'\u00e9valuation des expressions ci-dessous\u2009:

    char c;\nshort sh;\nint i;\nfloat f;\ndouble d;\n
    1. c * sh - f / i + d;
    2. c * (sh \u2013 f) / i + d;
    3. c * sh - f - i + d;
    4. c + sh * f / i + d;

    Exercice 3\u2009: Expressions mixtes

    Soit les instructions suivantes\u2009:

    int n = 10;\nint p = 7;\nfloat x = 2.5;\n

    Donnez le type et la valeur des expressions suivantes\u2009:

    1. x + n % p
    2. x + p / n
    3. (x + p) / n
    4. .5 * n
    5. .5 * (float)n
    6. (int).5 * n
    7. (n + 1) / n
    8. (n + 1.0) / n
    ", "tags": ["char", "int"]}, {"location": "course-c/15-fundations/datatype/#promotion-explicite", "title": "Promotion explicite", "text": "

    Il est possible de forcer la promotion d'un type vers un autre en utilisant un transtypage explicite. Par exemple, pour forcer la promotion d'un int vers un double :

    int n = 10;\ndouble x = (double)n;\n

    Le changement de type forc\u00e9 (transtypage) entre des variables de diff\u00e9rents types engendre des effets de bord qu'il faut conna\u00eetre. Lors d'un changement de type vers un type dont le pouvoir de repr\u00e9sentation est plus important, il n'y a pas de probl\u00e8me. \u00c0 l'inverse, on peut rencontrer des erreurs sur la pr\u00e9cision ou une modification radicale de la valeur repr\u00e9sent\u00e9e\u2009!

    ", "tags": ["double", "int", "transtypage"]}, {"location": "course-c/15-fundations/datatype/#transtypage-dun-entier-en-flottant", "title": "Transtypage d'un entier en flottant", "text": "

    Par exemple, la conversion d'un nombre flottant (double ou float) en entier (sign\u00e9) doit \u00eatre \u00e9tudi\u00e9e pour \u00e9viter tout probl\u00e8me. Le type entier doit \u00eatre capable de recevoir la valeur (attention aux valeurs maxi).

    double d=3.9;\nlong l=(long)d; // valeur : 3 => perte de pr\u00e9cision\n

    A l'ex\u00e9cution, la valeur de \\(l\\) sera la partie enti\u00e8re de \\(d\\). Il n'y a pas d'arrondi.

    double d=0x12345678;\nshort sh=(short)d; // valeur : 0x5678 => changement de valeur\n

    La variable sh (short sur 16 bit) ne peut contenir la valeur r\u00e9elle. Lors du transtypage, il y a modification de la valeur ce qui conduit \u00e0 des erreurs de calculs par la suite.

    double d=-123;\nunsigned short sh=(unsigned short)d; // valeur : 65413 => changement de valeur\n

    L'utilisation d'un type non sign\u00e9 pour convertir un nombre r\u00e9el conduit \u00e9galement \u00e0 une modification de la valeur num\u00e9rique.

    "}, {"location": "course-c/15-fundations/datatype/#transtypage-dun-double-en-float", "title": "Transtypage d'un double en float", "text": "

    La conversion d'un nombre r\u00e9el de type double en r\u00e9el de type float pose un probl\u00e8me de pr\u00e9cision de calcul.

    double d=0.1111111111111111;\nfloat f=(float)d; // valeur : 0.1111111119389533 => perte de pr\u00e9cision\n

    \u00c0 l'ex\u00e9cution, il y a une perte de pr\u00e9cision lors de la conversion, ce qui peut, lors d'un calcul it\u00e9ratif induire des erreurs de calcul.

    Exercice 4\u2009: Conversion de types

    On consid\u00e8re les d\u00e9clarations suivantes\u2009:

    float x;\nshort i;\nunsigned short j;\nlong k;\nunsigned long l;\n

    Identifiez les expressions ci-dessous dont le r\u00e9sultat n'est pas math\u00e9matiquement correct.

    x = 1e6;\ni = x;\nj = -20;\nk = x;\nl = k;\nk = -20;\nl = k;\n
    Solution
    x = 1e6;\ni = x;    // Incorrect, i peut-\u00eatre limit\u00e9 \u00e0 -32767..+32767 (C99 \u00a75.2.4.2.1)\nj = -20;  // Incorrect, valeur sign\u00e9e dans un conteneur non sign\u00e9\nk = x;\nl = k;\nk = -20;\nl = k;    // Incorrect, valeur sign\u00e9e dans un conteneur non sign\u00e9\n

    Exercice 5\u2009: Un casting explicite

    Que valent les valeurs de p, x et n:

    float x;\nint n, p;\n\np = 2;\nx = (float)15 / p;\nn = x + 1.1;\n
    Solution
    p \u2261 2\nx = 7.5\nn = 8\n

    Exercice 6\u2009: Op\u00e9rateurs de relation et op\u00e9rateurs logiques

    Soit les d\u00e9clarations suivantes\u2009:

    float x, y;\nbool condition;\n

    R\u00e9\u00e9crire l'expression ci-dessous en mettant des parenth\u00e8ses montrant l'ordre des op\u00e9rations\u2009:

    condition = x >= 0 && x <= 20 && y > x || y == 50 && x == 2 || y == 60;\n

    Donner la valeur de condition \u00e9valu\u00e9e avec les valeurs suivantes de x et y:

    1. x = -1.0; y = 60.;
    2. x = 0; y = 1.;
    3. x = 19.0; y = 1.0;
    4. x = 0.0; y = 50.0;
    5. x = 2.0; y = 50.0;
    6. x = -10.0; y = 60.0;
    Solution
    condition = (\n    (x >= 0) && (x <= 20) && (y > x))\n    ||\n    ((y == 50) && (x == 2))\n    ||\n    (y == 60)\n);\n
    1. true
    2. true
    3. false
    4. true
    5. true
    6. true

    Exercice 7\u2009: Casse-t\u00eate

    Vous participez \u00e0 une revue de code et tombez sur quelques perles laiss\u00e9es par quelques coll\u00e8gues. Comment proposeriez-vous de corriger ces \u00e9critures\u2009? Le code est \u00e9crit pour un mod\u00e8le de donn\u00e9e LLP64.

    Pour chaque exemple, donner la valeur des variables apr\u00e8s ex\u00e9cution du code.

    1. unsigned short i = 32767;\ni++;\n
    2. short i = 32767;\ni++;\n
    3. short i = 0;\ni = i--;\ni = --i;\ni = i--;\n
    ", "tags": ["true", "false", "condition"]}, {"location": "course-c/15-fundations/datatype/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 8\u2009: \u00c9valuation d'expressions

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    char c = 3;\nshort s = 7;\nint i = 3;\nlong l = 4;\nfloat f = 3.3;\ndouble d = 7.7;\n

    Que vaut le type et la valeur des expressions suivantes\u2009?

    1. c / 2
    2. sh + c / 10
    3. lg + i / 2.0
    4. d + f
    5. (int)d + f
    6. (int)d + lg
    7. c << 2
    8. sh & 0xF0
    9. sh && 0xF0
    10. sh == i + lg
    11. d + f == sh + lg

    Exercice 9\u2009: Pr\u00e9cision des flottants

    Que vaut x?

    float x = 10000000. + 0.1;\n
    Solution

    Le format float est stock\u00e9 sur 32-bits avec 23-bits de mantisse et 8-bits d'exposants. Sa pr\u00e9cision est donc limit\u00e9e \u00e0 environ 6 d\u00e9cimales. Pour repr\u00e9senter 10'000'000.1 il faut plus que 6 d\u00e9cimales et l'addition est donc caduc\u2009:

    #include <stdio.h>\n\nint main(void) {\n    float x = 10000000. + 0.1;\n    printf(\"%f\\n\", x);\n}\n
    $ ./a.out\n10000000.000000\n

    Exercice 10\u2009: Type de donn\u00e9e idoine

    Pour chaque entr\u00e9e suivante, indiquez le nom et le type des variables que vous utiliseriez pour repr\u00e9senter les donn\u00e9es dans ce programme\u2009:

    1. Gestion d'un parking\u2009: nombre de voitures pr\u00e9sente
    2. Station m\u00e9t\u00e9o a. Temp\u00e9rature moyenne de la journ\u00e9e b. Nombre de valeurs utilis\u00e9es pour la moyenne
    3. Montant disponible sur un compte en banque
    4. Programme de calcul de d'\u00e9nergie produite dans une centrale nucl\u00e9aire
    5. Programme de conversion d\u00e9cimal, hexad\u00e9cimal, binaire
    6. Produit scalaire de deux vecteurs plans
    7. Nombre d'impulsions re\u00e7ues par un capteur de position incr\u00e9mental

    Exercice 11\u2009: Construction d'expressions

    On consid\u00e8re un disque, divis\u00e9 en 12 secteurs angulaires \u00e9gaux, num\u00e9rot\u00e9s de 0 \u00e0 11. On mesure l\u2019angle de rotation du disque en degr\u00e9s, sous la forme d\u2019un nombre entier non sign\u00e9. Une fl\u00e8che fixe d\u00e9signe un secteur. Entre 0 et 29 \u00b0, le secteur d\u00e9sign\u00e9 est le n\u00b0 0, entre 30 \u00b0 et 59 \u00b0, c\u2019est le secteur 1, ...

    Donnez une expression arithm\u00e9tique permettant, en fonction d\u2019un angle donn\u00e9, d\u2019indiquer quel est le secteur du disque se trouve devant la fl\u00e8che. Note\u2009: l\u2019angle de rotation peut \u00eatre sup\u00e9rieur \u00e0 360 \u00b0. V\u00e9rifiez cette expression avec les angles de 0, 15, 29, 30, 59, 60, 360, 389, 390 degr\u00e9s.

    \u00c9crivez un programme demandant l\u2019angle et affichant le num\u00e9ro de secteur correspondant.

    Exercice 12\u2009: Somme des entiers

    Il est prouv\u00e9 math\u00e9matiquement que la somme des entiers strictement positifs pris dans l'ordre croissant peut \u00eatre exprim\u00e9e comme\u2009:

    \\[ \\sum_{k=1}^n k = \\frac{n(n+1)}{2} \\]

    \u0b9a\u0bc0\u0ba9\u0bbf\u0bb5\u0bbe\u0b9a \u0b87\u0bb0\u0bbe\u0bae\u0bbe\u0ba9\u0bc1\u0b9c\u0ba9\u0bcd, un grand math\u00e9maticien (Srinivasa Ramanujan) \u00e0 d\u00e9montr\u00e9 que ce la somme \u00e0 l'infini donne\u2009:

    \\[ \\sum_{k=1}^{\\inf} k = -\\frac{1}{12} \\]

    Vous ne le croyez pas et d\u00e9cider d'utiliser le superordinateur Pens\u00e9es Profondes pour faire ce calcul. Comme vous n'avez pas acc\u00e8s \u00e0 cet ordinateur pour l'instant (et probablement vos enfants n'auront pas acc\u00e8s \u00e0 cet ordinateur non plus), \u00e9crivez un programme simple pour tester votre algorithme et prenant en param\u00e8tre la valeur n \u00e0 laquelle s'arr\u00eater.

    Tester ensuite votre programme avec des valeurs de plus en plus grandes et analyser les performances avec le programme time:

    $ time ./a.out 1000000000\n500000000500000000\n\nreal    0m0.180s\nuser    0m0.172s\nsys     0m0.016s\n

    \u00c0 partir de quelle valeur, le temps de calcul devient significativement palpable\u2009?

    Solution
    #include <stdio.h>\n#include <stdlib.h>\n\nint main(int argc, char *argv[]) {\n    long long n = atoi(argv[1]);\n    long long sum = 0;\n    for(size_t i = 0; i < n; i++, sum += i);\n    printf(\"%lld\\n\", sum);\n}\n

    Exercice 13\u2009: Syst\u00e8me de vision industriel

    La soci\u00e9t\u00e9 japonaise Nakain\u0153il d\u00e9veloppe des syst\u00e8mes de vision industriels pour l'inspection de pi\u00e8ces dans une ligne d'assemblage. Le programme du syst\u00e8me de vision comporte les variables internes suivantes\u2009:

    uint32_t inspected_parts, bad_parts;\nfloat percentage_good_parts;\n

    \u00c0 un moment du programme, on peut lire\u2009:

    percentage_good_parts = (inspected_parts - bad_parts) / inspected_parts;\n

    Sachant que inspected_parts = 2000 et bad_parts = 200:

    1. Quel r\u00e9sultat le d\u00e9veloppeur s'attend-il \u00e0 obtenir\u2009?
    2. Qu'obtient-il en pratique\u2009?
    3. Pourquoi\u2009?
    4. Corrigez les \u00e9ventuelles erreurs.
    Solution
    1. Le d\u00e9veloppeur s'attend \u00e0 obtenir le pourcentage de bonnes pi\u00e8ces avec plusieurs d\u00e9cimales apr\u00e8s la virgule.
    2. En pratique, il obtient un entier, c'est \u00e0 dire toujours 0.
    3. La promotion implicite des entiers peut \u00eatre d\u00e9coup\u00e9e comme suit\u2009:

      (uint32_t)numerator = (uint32_t)inspected_parts - (uint32_t)bad_parts;\n(uint32_t)percentage = (uint32_t)numerator / (uint32_t)inspected_parts;\n(float)percentage_good_parts = (uint32_t)percentage;\n

    La division est donc appliqu\u00e9e \u00e0 des entiers et non des flottants.

    1. Une possible correction consiste \u00e0 forcer le type d'un des membres de la division\u2009:

      percentage_good_parts = (float)(inspected_parts - bad_parts) / inspected_parts;\n

    Exercice 14\u2009: Missile Patriot

    Durant la guerre du Golfe le 25 f\u00e9vrier 1991, une batterie de missile am\u00e9ricaine \u00e0 Dharan en Arabie saoudite \u00e0 \u00e9chou\u00e9 \u00e0 intercepter un missile irakien Scud. Cet \u00e9chec tua 28 soldats am\u00e9ricains et en blessa 100 autres. L'erreur sera imput\u00e9e \u00e0 un probl\u00e8me de type de donn\u00e9e sera longuement discut\u00e9e dans le rapport GAO/OMTEC-92-26 du commandement g\u00e9n\u00e9ral.

    Un registre 24-bit est utilis\u00e9 pour le stockage du temps \u00e9coul\u00e9 depuis le d\u00e9marrage du logiciel de contr\u00f4le indiquant le temps en dixi\u00e8me de secondes. D\u00e8s lors il a fallait multiplier ce temps par 1/10 pour obtenir le temps en seconde. La valeur 1/10 \u00e9tait tronqu\u00e9e \u00e0 la 24^e d\u00e9cimale apr\u00e8s la virgule. Des erreurs d'arrondi sont apparue menant \u00e0 un d\u00e9calage de pr\u00e8s de 1 seconde apr\u00e8s 100 heures de fonction. Or, cette erreur d'une seconde s'est traduite par 600 m\u00e8tres d'erreur lors de la tentative d'interception.

    Le stockage de la valeur 0.1 est donn\u00e9 par\u2009:

    \\[ 0.1_{10} \\approx \\lfloor 0.1_{10}\\cdot 2^{23} \\rfloor = 11001100110011001100_{2} \\approx 0.09999990463256836 \\]

    Un registre contient donc le nombre d'heures \u00e9coul\u00e9es exprim\u00e9es en dixi\u00e8me de seconde soit pour 100 heures\u2009:

    \\[ 100 \\cdot 60 \\cdot 60 \\cdot 10 = 3'600'000 \\]

    En termes de virgule fixe, la premi\u00e8re valeur est exprim\u00e9e en Q1.23 tandis que la seconde en Q0.24. Multiplier les deux valeurs entre elles donne Q1.23 x Q0.24 = Q1.47 le r\u00e9sultat est donc exprim\u00e9 sur 48 bits. Il faut donc diviser le r\u00e9sultat du calcul par :math\u2009:2^{47} pour obtenir le nombre de secondes \u00e9coul\u00e9es depuis le d\u00e9but la mise sous tension du syst\u00e8me.

    Quel est l'erreur en seconde cumul\u00e9e sur les 100 heures de fonctionnement\u2009?

    Exercice 15\u2009: Expressions arithm\u00e9tiques enti\u00e8res

    Donnez la valeur des expressions ci-dessous\u2009:

    25 + 10 + 7 - 3\n5 / 2\n24 + 5 / 2\n(24 + 5) / 2\n25 / 5 / 2\n25 / (5 / 2)\n72 % 5 - 5\n72 / 5 - 5\n8 % 3\n-8 % 3\n8 % -3\n-8 % -3\n
    ", "tags": ["time"]}, {"location": "course-c/15-fundations/functions/", "title": "Functions", "text": ""}, {"location": "course-c/15-fundations/functions/#fonctions", "title": "Fonctions", "text": "

    Margaret Hamilton, directrice projet AGC (1969), photo du MIT Museum

    Margaret Hamilton la directrice du projet Apollo Guidance Computer (AGC) \u00e0 c\u00f4t\u00e9 du code du projet.

    \u00c0 l'\u00e9poque d'Apollo 11, les fonctions n'existaient pas, le code n'\u00e9tait qu'une suite monolithique d'instruction \u00e9sot\u00e9rique dont les sources du Apollo Guidance Computer ont \u00e9t\u00e9 publi\u00e9es sur GitHub. Le langage est l'assembler yaYUL dispose de sous-routines, ou proc\u00e9dures qui sont des fonctions sans param\u00e8tres. Ce type de langage est proc\u00e9dural.

    N\u00e9anmoins, dans ce langage assembleur \u00e9trange, le code reste monolithique et toutes les variables sont globales.

    Un programme convenablement structur\u00e9 est d\u00e9coup\u00e9 en \u00e9l\u00e9ments fonctionnels qui disposent pour chacun d'entr\u00e9es et de sorties. De la m\u00eame mani\u00e8re qu'un t\u00e9lenc\u00e9phale hautement d\u00e9velopp\u00e9 et son pouce pr\u00e9henseur aime organiser sa maison en pi\u00e8ces d\u00e9di\u00e9es \u00e0 des occupations particuli\u00e8res et que chaque pi\u00e8ce dispose de rangements assign\u00e9s les uns \u00e0 des assiettes, les autres \u00e0 des couverts, le d\u00e9veloppeur organisera son code en blocs fonctionnels et cherchera \u00e0 minimiser les effets de bord.

    Agencement de fonctions

    Une fonction est donc un ensemble de code ex\u00e9cutable d\u00e9limit\u00e9 du programme principal et disposant\u2009:

    • D'un identifiant unique
    • D'une valeur de retour
    • De param\u00e8tres d'appel

    L'utilisation des fonctions permet\u2009:

    • De d\u00e9composer un programme complexe en t\u00e2ches plus simples
    • De r\u00e9duire la redondance de code
    • De maximiser la r\u00e9utilisation du code
    • De s'abstraire des d\u00e9tails d'impl\u00e9mentation
    • D'augmenter la lisibilit\u00e9 du code
    • D'accro\u00eetre la tra\u00e7abilit\u00e9 \u00e0 l'ex\u00e9cution

    En revanche, une fonction apporte quelques d\u00e9savantages qui \u00e0 l'\u00e9chelle des ordinateurs moderne sont parfaitement n\u00e9gligeables. L'appel \u00e0 une fonction ou sous-routine requiert du housekeeping, qui se compose d'un pr\u00e9lude et d'un aboutissant et dans lequel le contexte doit \u00eatre sauvegard\u00e9.

    "}, {"location": "course-c/15-fundations/functions/#conventions-dappel", "title": "Conventions d'appel", "text": "

    Dans le Voyage de Chihiro (\u5343\u3068\u5343\u5c0b\u306e\u795e\u96a0\u3057) de Hayao Miyazaki, le vieux Kamaji (\u91dc\u723a) travaille dans la chaudi\u00e8re des bains pour l'alimenter en charbon et pr\u00e9parer les d\u00e9coctions d'herbes pour parfumer les bains des clients.

    Le vieux Kamaji et ses bras extensibles.

    Je vous propose de b\u00e2tir une m\u00e9taphore du changement de contexte en s'inspirant de cette illustration. Les murs de la chaudi\u00e8re sont emplis de casiers contenant diff\u00e9rentes herbes, ces casiers peuvent \u00eatre apparent\u00e9s \u00e0 la m\u00e9moire de l'ordinateur, et les diff\u00e9rentes herbes, des types de donn\u00e9es diff\u00e9rents. De son pupitre Kamaji dispose de plusieurs mortiers dans lequel il m\u00e9lange les herbes\u2009; ils sont \u00e0 l'instar de l'ALU d'un ordinateur le si\u00e8ge d'op\u00e9rations transformant, \u00e0 l'aide du pilon, plusieurs entr\u00e9es en une seule sortie\u2009: le m\u00e9lange d'herbes servant \u00e0 la d\u00e9coction. Bien qu'il ait six bras et afin de s'\u00e9viter des manipulations inutiles, il garde de petites r\u00e9serves d'herbes \u00e0 c\u00f4t\u00e9 de son pupitre dans de petits casiers, similaires aux registres du processeur.

    Il profite de son temps libre, pendant que les bains sont ferm\u00e9s pour pr\u00e9parer certains m\u00e9langes d'herbes les plus populaires et il place ce stock dans un casier du mur. Pr\u00e9parer un m\u00e9lange est tr\u00e8s similaire \u00e0 un programme informatique dans lequel une suite d'op\u00e9ration repr\u00e9sente une recette donn\u00e9e. Le vieux Kamaji \u00e0 une tr\u00e8s grande m\u00e9moire, et il ne dispose pas de livre de recettes, mais vous, moi, n'importe qui, aurions besoin d'instructions claires du type\u2009:

    AUTUMN_TONIC_TEA :\n\n  MOVE  R1 @B4      # D\u00e9place de la grande ortie du casier B4 au registre R1\n  MOVE  R2 @A8      # D\u00e9place la menthe verte (Mentha spicata) du casier\n                    # A8 au registre R2\n  MOVE  R3 @C7      # D\u00e9place le gingembre du casier C7 au registre R3\n  ...\n  CHOP  R4 R3, FINE # Coupe tr\u00e8s finement le gingembre et le place dans R4\n  ...\n  LEAV  R2 R5       # D\u00e9tache les feuilles des tiges de la menthe\n                    # verte, place les feuilles en R5\n  ...\n  ADD   R8 R1 R5    # Pilonne le contenu de R1 et R2 et place dans R8\n  ADD   R8 R8 R4\n  ...\n  STO   R8 @F6      # Place le m\u00e9lange d'herbe automnale tonic dans le casier F6\n

    Souvent, le vieux Kamaji r\u00e9p\u00e8te les m\u00eames suites d'op\u00e9ration et ce, peu importe les herbes qu'il manipule, une fois plac\u00e9es dans les petits casiers (registres), il pourrait travailler les yeux ferm\u00e9s.

    On pourrait r\u00e9sumer ce travail par une fonction C, ici prenant un rhizome et deux herbes en entr\u00e9e et g\u00e9n\u00e9rant un m\u00e9lange en sortie.

    blend slice_and_blend(rootstock a, herb b, herb c);\n

    Pour des recettes complexes, il se pourrait que la fonction slice_and_blend soit appel\u00e9e plusieurs fois \u00e0 la suite, mais avec des ingr\u00e9dients diff\u00e9rents. De m\u00eame que cette fonction fait appel \u00e0 une autre fonction plus simple tel que slice (d\u00e9couper) ou blend_together (incorporer).

    Et le contexte dans tout cela\u2009? Il existe selon le langage de programmation et l'architecture processeur ce que l'on appelle les conventions d'appel. C'est-\u00e0-dire les r\u00e8gles qui r\u00e9gissent les interactions entre les appels de fonctions. Dans notre exemple, on adoptera peut-\u00eatre la convention que n'importe quelle fonction trouvera ses ingr\u00e9dients d'entr\u00e9es dans les casiers R1, R2 et R3 et que le r\u00e9sultat de la fonction, ici le blend, sera plac\u00e9 dans le casier R8. Ainsi peu importe les herbes en entr\u00e9e, le vieux Kamaji peut travailler les yeux ferm\u00e9s, piochant simplement dans R1, R2 et R3.

    On observe n\u00e9anmoins dans la recette \u00e9voqu\u00e9e plus haut qu'il utilise d'autres casiers, R4, et R5. Il faut donc faire tr\u00e8s attention \u00e0 ce qu'une autre fonction peut-\u00eatre la fonction slice, n'utilise pas dans sa propre recette le casier R5, car sinon, c'est la catastrophe.

    herb slice(herb a);\n

    Kamaji entrepose temporairement les feuilles de menthe verte dans R5 et lorsqu'il en a besoin, plus tard, apr\u00e8s avoir d\u00e9coup\u00e9 les fleurs de mol\u00e8ne que R5 contient des tiges d'une autre plante.

    Dans les conventions d'appel, il faut donc \u00e9galement donner la responsabilit\u00e9 \u00e0 quelqu'un de ne pas utiliser certains casiers, ou alors d'en sauvegarder ou de restaurer le contenu au d\u00e9but et \u00e0 la fin de la recette. Dans les conventions d'appel, il y a en r\u00e9alit\u00e9 plusieurs cat\u00e9gories de registres\u2009:

    • ceux utilis\u00e9s pour les param\u00e8tres de la fonction,
    • ceux utilis\u00e9s pour les valeurs de retour,
    • ceux qui peuvent \u00eatre utilis\u00e9s librement par une fonction (la sauvegarde est \u00e0 la charge du caller, la fonction qui appelle une autre fonction),
    • ceux qui doivent \u00eatre sauvegard\u00e9s par le callee (la fonction qui est appel\u00e9e).

    En C, ce m\u00e9canisme est parfaitement automatique, le programmeur n'a pas \u00e0 se soucier du processeur, du nom des registres, de la correspondance entre le nom des herbes et le casier ou elles sont entrepos\u00e9es. N\u00e9anmoins, l'\u00e9lectronicien d\u00e9veloppeur, proche du mat\u00e9riel, doit parfois bien comprendre ces m\u00e9canismes et ce qu'ils co\u00fbtent (en temps et en place m\u00e9moire) \u00e0 l'ex\u00e9cution d'un programme.

    ", "tags": ["slice", "slice_and_blend", "blend_together"]}, {"location": "course-c/15-fundations/functions/#overhead", "title": "Overhead", "text": "

    L'appel de fonction co\u00fbte \u00e0 l'ex\u00e9cution, car avant chaque fonction, le compilateur ajoute automatiquement des instructions de sauvegarde et de restauration des registres utilis\u00e9s\u2009:

    Sauvegarde des registres du processeur et convention d'appel de fonction.

    Ce co\u00fbt est faible, tr\u00e8s faible, un ordinateur fonctionnant \u00e0 3 GHz et une fonction complexe utilisant tous les registres disponibles, mettons 10 registres, consommera entre l'appel de la fonction et son retour 0.000'000'003 seconde, \u00e7a va, c'est raisonnable. Sauf que, si la fonction ne comporte qu'une seule op\u00e9ration comme ci-dessous, l'overhead sera aussi plus faible.

    int add(int a, int b) {\n    return a + b;\n}\n
    "}, {"location": "course-c/15-fundations/functions/#stack", "title": "Stack", "text": "

    En fran\u00e7ais la pile d'ex\u00e9cution, est un emplacement m\u00e9moire utilis\u00e9 pour sauvegarder les registres du processeur entre les appels de fonctions, sauvegarder les adresses de retour des fonctions qui sont analogue \u00e0 sauvegarder le num\u00e9ro de page du livre de recettes\u2009: p 443. Recette du Bras de V\u00e9nus\u2009: commencer par r\u00e9aliser une g\u00e9noise de 300g (p. 225). Une fois la g\u00e9noise termin\u00e9e, il faut se rappeler de retourner \u00e0 la page 443. Enfin le stack est utilis\u00e9 pour m\u00e9moriser les param\u00e8tres des fonctions suppl\u00e9mentaires qui ne tiendraient pas dans les registres d'entr\u00e9es. La convention d'appel de la plupart des architectures pr\u00e9voit g\u00e9n\u00e9ralement 3 registres pour les param\u00e8tres d'entr\u00e9es, si bien qu'une fonction \u00e0 4 param\u00e8tres pourrait bien aussi utiliser le stack:

    double quaternion_norm(double a1, double b1, double c1, double d1);\n

    La pile d'ex\u00e9cution est, comme son nom l'indique, une pile sur laquelle sont empil\u00e9s et d\u00e9pil\u00e9s les \u00e9l\u00e9ments au besoin. \u00c0 chaque appel d'une fonction, la valeur des registres \u00e0 sauvegarder est empil\u00e9e et au retour d'une fonction les registres sont d\u00e9pil\u00e9s si bien que la fonction d'appel retrouve le stack dans le m\u00eame \u00e9tat qu'il \u00e9tait avant l'appel d'une fonction enfant.

    "}, {"location": "course-c/15-fundations/functions/#prototype", "title": "Prototype", "text": "

    Le prototype d'une fonction est son interface avec le monde ext\u00e9rieur. Il d\u00e9clare la fonction, son type de retour et ses param\u00e8tres d'appel. Le prototype est souvent utilis\u00e9 dans un fichier d'en-t\u00eate pour construire des biblioth\u00e8ques logicielles. La fonction printf que nous ne cessons pas d'utiliser voit son prototype r\u00e9sider dans le fichier <stdio.h> et il est d\u00e9clar\u00e9 sous la forme\u2009:

    \u200bint printf(const char* format, ...);\n

    Notons qu'il n'y a pas d'accolades ici.

    Rappelons-le, C est un langage imp\u00e9ratif et d\u00e9claratif, c'est-\u00e0-dire que les instructions sont s\u00e9quentielles et que les d\u00e9clarations du code sont interpr\u00e9t\u00e9es dans l'ordre ou elles apparaissent. Si bien si je veux appeler la fonction make_coffee, il faut qu'elle ait \u00e9t\u00e9 d\u00e9clar\u00e9e avant, c'est \u00e0 dire plus haut.

    Le code suivant fonctionne\u2009:

    int make_coffee(void) {\n    printf(\"Please wait...\\n)\";\n}\n\nint main(void) {\n    make_coffee();\n}\n

    Mais celui-ci ne fonctionnera pas, car make_coffee n'est pas connu au moment de l'appel\u2009:

    int main(void) {\n    make_coffee();\n}\n\nint make_coffee(void) {\n    printf(\"Please wait...\\n)\";\n}\n

    Si pour une raison connue seule du d\u00e9veloppeur on souhaite d\u00e9clarer la fonction apr\u00e8s main, on peut ajouter le prototype de la fonction avant cette derni\u00e8re. C'est ce que l'on appelle la d\u00e9claration avanc\u00e9e ou forward declaration.

    int make_coffee(void);\n\nint main(void) {\n    make_coffee();\n}\n\nint make_coffee(void) {\n    printf(\"Please wait...\\n\");\n}\n

    Un prototype de fonction diff\u00e8re de son impl\u00e9mentation par le fait qu'il ne dispose pas du code, mais simplement sa d\u00e9finition, permettant au compilateur d'\u00e9tablir les conventions d'appel de la fonction.

    ", "tags": ["make_coffee", "main", "printf"]}, {"location": "course-c/15-fundations/functions/#syntaxe", "title": "Syntaxe", "text": "

    La syntaxe d'\u00e9criture d'une fonction peut \u00eatre assez compliqu\u00e9e et la source de v\u00e9rit\u00e9 est issue de la grammaire du langage, qui n'est pas n\u00e9cessairement accessible au profane. Or, depuis C99, une fonction prend la forme\u2009:

    <storage-class> <return-type> <function-name> (\n    <parameter-type> <parameter-name>, ... )\n
    <storage-class>

    Classe de stockage, elle n'est pas utile \u00e0 ce stade du cours, nous aborderons plus tard les mots cl\u00e9s extern, static et inline.

    <return-type>

    Le type de retour de la fonction, s'agit-il d'un int, d'un float ? Le type de retour est anonyme, il n'a pas de nom et ce n'est pas n\u00e9cessaire.

    <function-name>

    Il s'agit d'un identificateur qui repr\u00e9sente le nom de la fonction. G\u00e9n\u00e9ralement on pr\u00e9f\u00e8re choisir un verbe, quelquefois associ\u00e9 \u00e0 un nom\u2009: compute_norm, make_coffee, ... N\u00e9anmoins, lorsqu'il n'y a pas d'ambig\u00fcit\u00e9, on peut choisir des termes plus simples tels que main, display ou dot_product.

    <parameter-type> <parameter-name>

    La fonction peut prendre en param\u00e8tre z\u00e9ro \u00e0 plusieurs param\u00e8tres o\u00f9 chaque param\u00e8tre est d\u00e9fini par son type et son nom tel que\u2009: double real, double imag pour une fonction qui prendrait en param\u00e8tre un nombre complexe.

    Apr\u00e8s la fermeture de la parenth\u00e8se de la liste des param\u00e8tres, deux possibilit\u00e9s\u2009:

    Prototype

    On clos la d\u00e9claration avec un ;

    Impl\u00e9mentation

    On poursuit avec l'impl\u00e9mentation du code { ... }

    ", "tags": ["inline", "make_coffee", "static", "compute_norm", "display", "float", "dot_product", "int", "main", "extern"]}, {"location": "course-c/15-fundations/functions/#void", "title": "void", "text": "

    Le type void est \u00e0 une signification particuli\u00e8re dans la syntaxe d'une fonction. Il peut \u00eatre utilis\u00e9 de trois mani\u00e8res diff\u00e9rentes\u2009:

    • Pour indiquer l'absence de valeur de retour\u2009:

      void foo(int a, int b);\n
    • Pour indiquer l'absence de param\u00e8tres\u2009:

      int bar(void);\n
    • Pour indiquer que la valeur de retour n'est pas utilis\u00e9e par le parent\u2009:

      (void) foo(23, 11);\n

    La d\u00e9claration suivante est formellement fausse, car la fonction ne poss\u00e8de pas un prototype complet. En effet, le nombre de param\u00e8tres n'est pas contraint et le code suivant est valide au sens de C99.

    void dummy() {}\n\nint main(void) {\n    dummy(1, 2, 3);\n    dummy(120, 144);\n}\n

    Aussi, il est imp\u00e9ratif de toujours \u00e9crire des prototypes complets et d'explicitement utiliser void lorsque la fonction ne prend aucun param\u00e8tre en entr\u00e9e. Si vous utilisez un compilateur C++, une d\u00e9claration incompl\u00e8te g\u00e9n\u00e8rera une erreur.

    ", "tags": ["void"]}, {"location": "course-c/15-fundations/functions/#parametres", "title": "Param\u00e8tres", "text": "

    Comme nous l'avons vu plus haut, pour de meilleures performances \u00e0 l'ex\u00e9cution, il est pr\u00e9f\u00e9rable de s'en tenir \u00e0 un maximum de trois param\u00e8tres, c'est \u00e9galement plus lisible pour le d\u00e9veloppeur, mais rien n'emp\u00eache d'en avoir plus.

    En plus de cela, les param\u00e8tres peuvent \u00eatre pass\u00e9s de deux mani\u00e8res\u2009:

    • Par valeur
    • Par r\u00e9f\u00e9rence

    En C, fondamentalement, tous les param\u00e8tres sont pass\u00e9s par valeur, c'est-\u00e0-dire que la valeur d'une variable est copi\u00e9e \u00e0 l'appel de la fonction. Dans l'exemple suivant, la valeur affich\u00e9e sera bel et bien 33 et non. 42

    void alter(int a) {\n    a = a + 9;\n}\n\nvoid main(void) {\n    int a = 33;\n    alter(a);\n    printf(\"%d\\n\", a);\n}\n

    Dans certains cas, on souhaite utiliser plus d'une valeur de retour et l'on peut utiliser un tableau. Dans l'exemple suivant, la valeur affich\u00e9e sera cette fois-ci 42 et non 33.

    void alter(int array[]) {\n    array[0] += 9;\n}\n\nvoid main(void) {\n    int array[] = {33, 34, 35};\n    alter(array);\n    printf(\"%d\\n\", array[0]);\n}\n

    Par abus de langage et en comparaison avec d'autres langages de programmation, on appellera ceci un passage par r\u00e9f\u00e9rence, car ce n'est pas une copie du tableau qui est pass\u00e9e \u00e0 la fonction alter, mais seulement une r\u00e9f\u00e9rence sur ce tableau.

    En des termes plus corrects, mais nous verrons cela au chapitre sur les pointeurs, c'est bien un passage par valeur dans lequel la valeur d'un pointeur sur un tableau est pass\u00e9e \u00e0 la fonction alter.

    Retenez simplement que lors d'un passage par r\u00e9f\u00e9rence, on cherche \u00e0 rendre la valeur pass\u00e9e en param\u00e8tre modifiable par le caller.

    ", "tags": ["alter"]}, {"location": "course-c/15-fundations/functions/#exemples-de-fonctions", "title": "Exemples de fonctions", "text": ""}, {"location": "course-c/15-fundations/functions/#suite-de-fibonacci", "title": "Suite de Fibonacci", "text": "

    La suite de Fibonacci est une suite d'entiers dans laquelle chaque terme est la somme des deux termes pr\u00e9c\u00e9dents. La suite commence par 0 et 1. La suite commence donc par 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

    Voici une impl\u00e9mentation de la suite de Fibonacci en utilisant une approche it\u00e9rative\u2009:

    int fib(int n)\n{\n    int sum = 0;\n    int t1 = 0, t2 = 1;\n    int next_term;\n    for (int i = 1; i <= n; i++)\n    {\n        sum += t1;\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    return sum;\n}\n
    "}, {"location": "course-c/15-fundations/functions/#syntaxe-traditionnelle", "title": "Syntaxe traditionnelle", "text": "

    Historiquement, la syntaxe des fonctions en C \u00e9tait diff\u00e9rente de celle que nous avons vue jusqu'\u00e0 pr\u00e9sent. Consid\u00e9rons la fonction suivante\u2009:

    double func(double x, double y, int z) {\n    return x + y + z;\n}\n

    En C89, la syntaxe de cette fonction \u00e9tait la suivante\u2009:

    double\nfunc(x, y, z)\ndouble x, y;\nint z;\n{ return x + y + z; }\n

    Cette syntaxe est toujours valide dans les versions plus r\u00e9centes du langage, mais elle est d\u00e9conseill\u00e9e car elle est moins lisible que la syntaxe moderne.

    "}, {"location": "course-c/15-fundations/functions/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Dans la moyenne

    \u00c9crire une fonction mean qui re\u00e7oit 3 param\u00e8tres r\u00e9els et qui retourne la moyenne.

    Solution
    double mean(double a, double b, double c) {\n    return (a + b + c) / 3.;\n}\n

    Exercice 2\u2009: Le plus petit

    \u00c9crire une fonction min qui re\u00e7oit 3 param\u00e8tres r\u00e9els et qui retourne la plus petite valeur.

    Solution
    double min(double a, double b, double c) {\n    double min_value = a;\n    if (b < min_value)\n        min_value = b;\n    if (c < min_value)\n        min_value = c;\n    return min_value;\n}\n

    Une mani\u00e8re plus compacte, mais moins lisible serait\u2009:

    double min(double a, double b, double c) {\n    return (a = (a < b ? a : b)) < c ? a : c;\n}\n

    Exercice 3\u2009: Algorithme de retour de monnaie

    On consid\u00e8re le cas d'une caisse automatique de parking. Cette caisse d\u00e9livre des tickets au prix unique de CHF 0.50 et dispose d'un certain nombre de pi\u00e8ces de 10 et 20 centimes pour le rendu de monnaie.

    Dans le code du programme, les trois variables suivantes seront utilis\u00e9es\u2009:

    // Available coins in the parking ticket machine\nunsigned int ncoin_10, ncoin_20;\n\n// How much money the user inserted into the machine (in cents)\nunsigned int amount_payed;\n

    \u00c9crivez l'algorithme de rendu de la monnaie tenant compte du nombre de pi\u00e8ces de 10 et 20 centimes restants dans l'appareil. Voici un exemple du fonctionnement du programme\u2009:

    $ echo \"10 10 20 20 20\" | ./ptm 30 1\nticket\n20\n10\n

    Le programme re\u00e7oit sur stdin les pi\u00e8ces introduites dans la machine. Les deux arguments pass\u00e9s au programme ptm sont 1. le nombre de pi\u00e8ces de 10 centimes disponibles et 2. le nombre de pi\u00e8ces de 20 centimes disponibles. stdout contient les valeurs rendues \u00e0 l'utilisateur. La valeur ticket correspond au ticket distribu\u00e9.

    Le cas \u00e9ch\u00e9ant, s'il n'est possible de rendre la monnaie, aucun ticket n'est distribu\u00e9 et l'argent donn\u00e9 est rendu.

    Solution

    Voici une solution partielle\u2009:

    #define TICKET_PRICE 50\n\nvoid give_coin(unsigned int value) { printf(\"%d\\n\", value); }\nvoid give_ticket(void) { printf(\"ticket\\n\"); }\n\nbool no_ticket = amount_payed < TICKET_PRICE;\n\nint amount_to_return = amount_payed - TICKET_PRICE;\ndo {\n    while (amount_to_return > 0) {\n        if (amount_to_return >= 20 && ncoin_20 > 0) {\n            give_coin(20);\n            amount_to_return -= 20;\n            ncoin_20--;\n        } else if (amount_to_return >= 10 && ncoin_10 > 0) {\n            give_coin(10);\n            amount_to_return -= 10;\n            ncoin_10--;\n        } else {\n            no_ticket = true;\n            break;\n        }\n    }\n} while (amount_to_return > 0);\n\nif (!no_ticket) {\n    give_ticket();\n}\n

    Exercice 4\u2009: La fonction f

    Consid\u00e9rons le programme suivant\u2009:

    int f(float x) {\n    int i;\n    if (x > 0.0)\n        i = (int)(x + 0.5);\n    else\n        i = (int)(x - 0.5);\n    return i;\n}\n

    Quel sont les types et les valeurs retourn\u00e9es par les expressions ci-dessous\u2009?

    f(1.2)\nf(-1.2)\nf(1.6)\nf(-1.6)\n

    Quel est votre conclusion sur cette fonction\u2009?

    Exercice 5\u2009: Mauvaise somme

    Le programme suivant compile sans erreurs graves, mais ne fonctionne pas correctement.

    #include <stdio.h>\n#include <stdlib.h>\n#include <math.h>\n\nlong get_integer()\n{\n    bool ok;\n    long result;\n    do\n    {\n        printf(\"Enter a integer value: \");\n        fflush(stdin); // Empty input buffer\n        ok = (bool)scanf(\"%ld\", &result);\n        if (!ok)\n            printf(\"Incorrect value.\\n\");\n    }\n    while (!ok);\n    return result;\n}\n\nint main(void)\n{\n    long a = get_integer;\n    long b = get_integer;\n\n    printf(\"%d\\n\", a + b);\n}\n

    Quel est le probl\u00e8me\u2009? \u00c0 titre d'information voici ce que le programme donne, notez que l'invit\u00e9 de saisie n'est jamais apparu\u2009:

    $ ./sum\n8527952\n
    ", "tags": ["stdin", "min", "mean", "ticket", "stdout", "ptm"]}, {"location": "course-c/15-fundations/grammar/", "title": "Grammar", "text": "", "tags": ["declarator", "pointer"]}, {"location": "course-c/15-fundations/grammar/#la-grammaire", "title": "La grammaire", "text": "

    Dans un langage formel, la grammaire est l'ensemble des r\u00e8gles qui r\u00e9gissent la construction des phrases. Elle est essentielle pour comprendre et produire un texte correctement. En fran\u00e7ais, la grammaire est complexe et comporte de nombreuses r\u00e8gles et exceptions.

    En programmation informatique, la grammaire est \u00e9galement tr\u00e8s importante. Elle d\u00e9finit la syntaxe du langage de programmation, c'est-\u00e0-dire la mani\u00e8re dont les instructions doivent \u00eatre \u00e9crites pour \u00eatre comprises par l'ordinateur. Une erreur de syntaxe peut emp\u00eacher un programme de fonctionner correctement, voire de compiler.

    L'ordinateur ne lit pas une phrase comme vous, elle ne peut pas ignorer les fautes de frappe, les erreurs de syntaxe ou les erreurs d'accord, car le compilateur doit \u00eatre capable d'interpr\u00e9ter le code source sans aucune ambigu\u00eft\u00e9.

    La grammaire d'un langage de programmation est g\u00e9n\u00e9ralement d\u00e9finie par un document appel\u00e9 \u00ab\u2009grammaire formelle\u2009\u00bb. Elle va d\u00e9finir les composants des \u00e9l\u00e9ments de votre code. Prenons par exemple le programme suivant\u2009:

    int main() {\n    const int n = 23 + 42;\n    for (int j = 0; j < n; ++j) {\n        printf(\"%d \", j);\n    }\n    return 0;\n}\n

    La figure suivante montre comment il serait possible de hi\u00e9rarchiser les \u00e9l\u00e9ments de ce programme. On constate une imbriquation des \u00e9l\u00e9ments.

    Exemple d'arbre syntaxique (AST)

    La grammaire formelle du langage C est tr\u00e8s complexe et comporte de nombreuses r\u00e8gles. Elle est d\u00e9finie par le standard du langage C, qui est un document officiel publi\u00e9 par l'ANSI (American National Standards Institute) et l'ISO (International Organization for Standardization).

    Une grammaire formelle est souvent \u00e9crite en utilisant une notation appel\u00e9e \u00ab\u2009Backus-Naur Form\u2009\u00bb (BNF). Cette notation est tr\u00e8s pr\u00e9cise et permet de d\u00e9crire de mani\u00e8re formelle la syntaxe d'un langage de programmation. Pour le C voici un extrait de la grammaire utilis\u00e9e par le compilateur\u2009:

    <translation_unit> ::= {<external_declaration>}*\n\n<external_declaration> ::= <function_definition>\n                        | <declaration>\n\n<function_definition> ::= {<declaration_specifier>}* <declarator> {<declaration>}* <compound_statement>\n\n<declaration_specifier> ::= <storage_class_specifier>\n                            | <type_specifier>\n                            | <type_qualifier>\n\n<storage_class_specifier> ::= \"auto\"\n                            | \"register\"\n                            | \"static\"\n                            | \"extern\"\n                            | \"typedef\"\n\n<type_specifier> ::= \"void\"\n                    | \"char\"\n                    | \"short\"\n                    | \"int\"\n                    | \"long\"\n                    | \"float\"\n                    | \"double\"\n                    | \"signed\"\n                    | \"unsigned\"\n                    | <struct_or_union_specifier>\n                    | <enum_specifier>\n                    | <typedef_name>\n\n<struct_or_union_specifier> ::= <struct_or_union> <identifier> {\n            {<struct_declaration>}+ }\n            | <struct_or_union> { {<struct_declaration>}+ }\n            | <struct_or_union> <identifier>\n\n<struct_or_union> ::= \"struct\"\n                    | \"union\"\n\n<struct_declaration> ::= {<specifier_qualifier>}* <struct_declarator_list>\n\n<specifier_qualifier> ::= <type_specifier>\n                        | <type_qualifier>\n\n<struct_declarator_list> ::= <struct_declarator>\n                            | <struct_declarator_list> \",\" <struct_declarator>\n\n<struct_declarator> ::= <declarator>\n                        | <declarator> \":\" <constant_expression>\n                        | \":\" <constant_expression>\n\n<declarator> ::= {<pointer>}? <direct_declarator>\n\n<pointer> ::= * {<type_qualifier>}* {<pointer>}?\n\n<type_qualifier> ::= \"const\"\n                    | \"volatile\"\n\n<direct_declarator> ::= <identifier>\n                        | ( <declarator> )\n                        | <direct_declarator> \"[\" {<constant_expression>}? \"]\"\n                        | <direct_declarator> \"(\" <parameter_type_list> \")\"\n                        | <direct_declarator> \"(\" {<identifier>}* \")\"\n

    Ce que l'on observe par exemple c'est que la grammaire du C est r\u00e9cursive. Cela signifie que l'on peut d\u00e9finir un \u00e9l\u00e9ment en fonction de lui-m\u00eame. Par exemple, un declarator peut contenir un pointer qui peut lui-m\u00eame contenir un pointer.

    Comment est-ce que cela fonctionne derri\u00e8re les coulisses\u2009?

    Le compilateur va tout d'abord convertir votre code source en un arbre de syntaxe abstraite (AST) qui va repr\u00e9senter la structure de votre programme (c'est grosso modo la figure montr\u00e9e plus haut). Ensuite, il va v\u00e9rifier que cet arbre respecte les r\u00e8gles du standard C. Si ce n'est pas le cas, il va vous renvoyer une erreur de compilation. Si cela passe \u00e0 cette \u00e9tape, il va ensuite pouvoir assembler votre programme en convertissant cette repr\u00e9sentation interne en du code assembleur. Ce dernier peut ensuite \u00eatre optimis\u00e9 pour \u00eatre plus rapide ou plus petit.

    La grammaire est donc un \u00e9l\u00e9ment important de la programmation et elle explique pourquoi un simple ; manquant peut vous co\u00fbter quelques cheveux arrach\u00e9s car l'analyseur syntaxique ne sait pas comment d\u00e9couper votre code en phrases.

    Pour les curieux, vous pouvez consulter la grammaire compl\u00e8te du C dans le standard du langage C (ISO/IEC 9899:2018) dans l'annexe A.

    "}, {"location": "course-c/15-fundations/grammar/#definir-mon-propre-langage", "title": "D\u00e9finir mon propre langage", "text": "

    Imaginons que l'on souaite r\u00e9aliser notre propre langage formel, par exemple pour analyser une expression math\u00e9matique de la forme suivante\u2009:

    3 + 4 * 5 + ( sin(3.14) + sqrt(2) / 8 )\n

    On pourrait d\u00e9finir une grammaire formelle pour ce langage en utilisant la notation BNF :

    <expression> ::= <term> { \"+\" <term> | \"-\" <term> }*\n\n<term> ::= <factor> { \"*\" <factor> | \"/\" <factor> }*\n\n<factor> ::= <number> | \"(\" <expression> \")\" | <function> \"(\" <expression> \")\"\n\n<number> ::= [0-9]+(\".\"[0-9]*)?\n<function> ::= \"sin\" | \"cos\" | \"sqrt\"\n

    Des outils comme lex et yacc populaires sur les syst\u00e8mes Unix permettent de g\u00e9n\u00e9rer un analyseur lexical et un analyseur syntaxique utilisables en C \u00e0 partir de cette grammaire. Ces outils sont tr\u00e8s puissants et sont utilis\u00e9s dans de nombreuses biblioth\u00e8ques et logiciels pour analyser des fichiers de configuration ou des syntaxes sp\u00e9cifiques.

    ", "tags": ["yacc", "lex"]}, {"location": "course-c/15-fundations/operators/", "title": "Op\u00e9rateurs", "text": "L'un de mes jours les plus productifs a \u00e9t\u00e9 lorsque j'ai supprim\u00e9 1 000 lignes de code. Nous n'avons pas besoin de plus d'op\u00e9rateurs, nous en avons besoin de moins.Ken Thompson

    En programmation, un op\u00e9rateur est une fonction qui effectue une op\u00e9ration sur des valeurs. Les op\u00e9rateurs utilisent des identificateurs sp\u00e9cifiques propres \u00e0 chaque langage de programmation, ce qui permet de simplifier l'\u00e9criture des expressions. Par exemple, l'op\u00e9rateur d'addition + permet d'additionner deux valeurs.

    L'unit\u00e9 de calcul arithm\u00e9tique du processeur (ALU) est responsable d'effectuer les op\u00e9rations fondamentales. Un ordinateur \u00e0 2 GHz pourrait par exemple effectuer plus de 2 milliards (2'000'000'000) d'op\u00e9rations par seconde.

    Un op\u00e9rateur prend habituellement deux op\u00e9randes et retourne un r\u00e9sultat. On dit alors que cette classe d'op\u00e9rateurs a une arit\u00e9 de 2. Il existe \u00e9galement des op\u00e9rateurs \u00e0 arit\u00e9 de 1, aussi appel\u00e9s op\u00e9rateurs unaires comme pour obtenir l'oppos\u00e9 d'un nombre (\\(-x\\)). Connaissant le compl\u00e9ment \u00e0 deux, on sait que pour obtenir l'oppos\u00e9 d'un nombre, il suffit d'inverser tous les bits et d'ajouter 1. C'est-\u00e0-dire de faire l'op\u00e9ration de n\u00e9gation ~ puis de faire une addition +1.

    Un op\u00e9rateur poss\u00e8de plusieurs propri\u00e9t\u00e9s\u2009:

    Une priorit\u00e9

    La multiplication * est plus prioritaire que l'addition +

    Une associativit\u00e9

    L'op\u00e9rateur d'affectation = poss\u00e8de une associativit\u00e9 \u00e0 droite, c'est-\u00e0-dire que l'op\u00e9rande \u00e0 droite de l'op\u00e9rateur sera \u00e9valu\u00e9 en premier

    Un point de s\u00e9quence

    Certains op\u00e9rateurs comme &&, ||, ? ou , poss\u00e8dent un point de s\u00e9quence garantissant que l'ex\u00e9cution s\u00e9quentielle du programme sera respect\u00e9e avant et apr\u00e8s ce point. Par exemple si dans l'expression i < 12 && j > 2 la valeur de i est plus grande que 12, le test j > 2 ne sera jamais effectu\u00e9. L'op\u00e9rateur && garantit l'ordre des choses, ce qui n'est pas le cas avec l'affectation =.

    "}, {"location": "course-c/15-fundations/operators/#alu-arithmetic-logic-unit", "title": "ALU (Arithmetic Logic Unit)", "text": "

    Dans un ordinateur, ou sur un microcontr\u00f4leur, c'est l'unit\u00e9 de calcul arithm\u00e9tique ALU qui est en charge d'effectuer les op\u00e9rations fondamentales. Cette unit\u00e9 de calcul est consensuellement repr\u00e9sent\u00e9e comme illustr\u00e9e \u00e0 la figure suivante\u2009:

    ALU

    L'unit\u00e9 de calcul arithm\u00e9tique (ALU) repr\u00e9sent\u00e9e est compos\u00e9e de deux entr\u00e9es A et B, d'une sortie C et d'un mode op\u00e9ratoire O. Sur de petites architectures mat\u00e9rielles, l'ALU peut \u00eatre limit\u00e9 aux op\u00e9rations d'addition +, d'inversion bit \u00e0 bit ~, de d\u00e9calage vers la gauche << et vers la droite >> et de l'op\u00e9ration bit \u00e0 bit logique & pour la conjonction ainsi que | pour la disjonction.

    Si l'on souhaite faire une addition, on peut \u00e9crire en C\u2009:

    c = a + b;\n

    "}, {"location": "course-c/15-fundations/operators/#types-doperateurs", "title": "Types d'op\u00e9rateurs", "text": "

    Le langage C d\u00e9finit un certain nombre d'op\u00e9rateurs qui peuvent \u00eatre class\u00e9s en plusieurs cat\u00e9gories\u2009:

    • Les op\u00e9rateurs arithm\u00e9tiques
    • Les op\u00e9rateurs relationnels
    • Les op\u00e9rateurs logiques
    • Les op\u00e9rateurs bit \u00e0 bit
    • Les op\u00e9rateurs d'affectation
    • Les op\u00e9rateurs de pointeurs
    • Les op\u00e9rateurs de taille
    • Les op\u00e9rateurs de s\u00e9quence
    • Les op\u00e9rateurs de pr\u00e9/post-incr\u00e9mentation
    • Les op\u00e9rateurs de condition

    Nous allons tous les voir un par un...

    "}, {"location": "course-c/15-fundations/operators/#operateurs-arithmetiques", "title": "Op\u00e9rateurs arithm\u00e9tiques", "text": "

    Aux 4 op\u00e9rations de base (+, -, \u00d7, \u00f7) le C ajoute l'op\u00e9ration modulo, qui est le reste d'une division enti\u00e8re.

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur Abr\u00e9viation Description Assertion vraie + add Addition 5 == 2 + 3 - sub Soustraction 8 == 12 - 4 * mul Multiplication 42 == 21 * 2 / div Division 2 == 5 / 2 % mod Modulo 13 % 4 == 1

    Lors d'op\u00e9rations, il faut faire attention aux types des variables impliqu\u00e9es. La division 5 / 2 donnera 2 et non, 2.5 car les deux valeurs fournies sont enti\u00e8res et le r\u00e9sultat est donc un entier. Pour obtenir un r\u00e9sultat flottant, il faut que l'une des valeurs soit un flottant, ici le 5 est exprim\u00e9 en double, la propagation de type fera que le r\u00e9sultat sera aussi un double :

    int a = 5 / 2;      // 2\ndouble b = 5.0 / 2; // 2.5\n

    Le modulo (mod, %) est le reste de la division enti\u00e8re. L'assertion suivante est donc vraie, car 13 divis\u00e9 par 4 \u00e9gal 3 et il reste 1\u2009:

    assert(13 % 4 == 1)\n
    \\[ \\begin{array}{rr|l} 1 & 3 & 4 \\\\ \\hline - & 8 & \\textbf{2} \\\\ & \\textbf{5} & \\\\ & & \\end{array} \\]

    Il est important de noter aussi que les op\u00e9rateurs arithm\u00e9tiques sont tributaires des types sur lesquels ils s'appliquent. Par exemple, l'addition de deux entiers 8 bits 120 + 120 ne fera pas, 240 car le type ne permet pas de stocker des valeurs plus grandes que 127 :

    int8_t too_small = 120 + 120;\nassert(too_small != 120 + 120);\n

    Nous l'avons tous appris dans les petites \u00e9coles, les op\u00e9rations arithm\u00e9tiques s'effectuent de droite \u00e0 gauche et chiffre \u00e0 chiffre. Lorsque le r\u00e9sultat de l'op\u00e9ration d\u00e9passe la capacit\u00e9 d'un chiffre, on retient une unit\u00e9 et on la reporte \u00e0 la colonne suivante. L'addition de \\(123\\) et \\(89\\) en base \\(10\\) donne \\(212\\).

    \\[ \\begin{array}{lrrr} \\phantom{1}& _1 & _1 & \\\\ & 1 & 2 & 3_{~10} \\\\ + & \\phantom{0} & 8 & 9_{~10} \\\\ \\hline & 2 & 1 & 2_{~10} \\\\ \\end{array} \\]

    L'exemple reste valable quelque soit la base, en binaire par exemple, on commence par additionner les bits de poids faible et on reporte les retenues. Ainsi en premier lieu on aura \\(1_2 + 1_2 = 10_2\\). Donc le r\u00e9sultat est \\(0\\) et la retenue (carry) est \\(1\\) :

    \\[ \\begin{array}{lrrrrrrrr} & _1 & _1 & _1 & _1 & & _1 & _1 & \\\\ & & 1 & 1 & 1 & 1 & 0 & 1 & 1_{~2} \\\\ + & & 1 & 0 & 1 & 1 & 0 & 0 & 1_{~2} \\\\ \\hline &1 & 1 & 0 & 1 & 0 & 1 & 0 & 0_{~2} \\\\ \\end{array} \\]

    En alg\u00e8bre de Boole, l'addition de deux chiffres n'a que \\(2^2 = 4\\) cas de figure (contre \\(10^2=100\\) en base \\(10\\)).

    L'addition de deux bits \\(A\\) et \\(B\\) est donn\u00e9e par la table suivante o\u00f9 C est la retenue engendr\u00e9e par l'addition\u2009:

    Addition binaire A B A + B C 0 0 0 0 0 1 1 0 1 0 1 0 1 1 0 1

    Le cas de la soustraction

    La soustraction reste une addition mais elle s'effectue sur les nombres repr\u00e9sent\u00e9s en compl\u00e9ment \u00e0 deux. On pourrait s'amuser \u00e0 soustraire deux nombres en base \\(10\\) en les repr\u00e9sentant en compl\u00e9ment \u00e0 neuf plus 1. Par exemple, pour soustraire \\(23\\) de \\(12\\) il faut repr\u00e9senter \\(12\\) en compl\u00e9ment \u00e0 neuf plus un.

    La m\u00e9thode est la m\u00eame, on effectue le compl\u00e9ment \u00e0 \\(9\\) de \\(12\\), soit \\(87\\) et on ajoute \\(1\\) pour obtenir \\(88\\). Ensuite on peut faire l'addition en \u00e9liminant le chiffre suppl\u00e9mentaire qui d\u00e9passe\u2009:

    \\[ \\begin{array}{lrrr} & _1 & _1 & \\\\ & 0 & 2 & 3_{~10} \\\\ + & 0 & 8 & 8_{~10} \\\\ \\hline & 1 & 1 & 1_{~10} \\\\ \\end{array} \\]

    L'int\u00e9r\u00eat de cette m\u00e9thode c'est qu'il s'agisse d'une addition ou d'une soustraction, c'est la m\u00eame op\u00e9ration calcul\u00e9e par l'unit\u00e9 arithm\u00e9tique et logique.

    Exercice 1\u2009: Additions binaires

    Une unit\u00e9 de calcul arithm\u00e9tique (ALU) est capable d'effectuer les 4 op\u00e9rations de bases comprenant additions et soustractions.

    Traduisez les op\u00e9randes ci-dessous en binaire, puis poser l'addition en binaire.

    1. \\(1 + 51\\)
    2. \\(51 - 7\\)
    3. \\(204 + 51\\)
    4. \\(204 + 204\\) (sur 8-bits)
    Solution

    Voici la solution du calcul en binaire\u2009:

    1. \\(1 + 51\\)

              \u00b9\u00b9\n         1\u2082\n+   110011\u2082  (2\u2075 + 2\u2074 + 2\u00b9+ 2\u2070 \u2261 51)\n----------\n    110100\u2082\n
    2. \\(51 - 7\\)

        \u2026\u00b9\u00b9\u00b9  \u00b9\u00b9\n  \u2026000110011\u2082  (2\u2075 + 2\u2074 + 2\u00b9 + 2\u2070 \u2261 51)\n+ \u2026111111001\u2082  (compl\u00e9ment \u00e0 deux, 2\u00b3 + 2\u00b9 + 2\u2070 \u2261 111\u2082 \u2192 !7 + 1 \u2261 \u2026111001\u2082)\n  -----------\n  \u2026000101100\u2082  (2\u2075 + 2\u00b3 + 2\u2082 \u2261 44)\n
    3. \\(204 + 51\\)

          11001100\u2082\n+     110011\u2082\n  -----------\n  \u2026011111111\u2082  (2\u2078 - 1 \u2261 255)\n
    4. \\(204 + 204\\) (sur 8-bits)

          \u00b9|\u00b9  \u00b9\u00b9\n     |11001100\u2082\n +   |11001100\u2082\n  ---+--------\n    1|10011000\u2082  (152, le r\u00e9sultat complet devrait \u00eatre 2\u2078 + 152 \u2261 408)\n
    ", "tags": ["double"]}, {"location": "course-c/15-fundations/operators/#operateurs-relationnels", "title": "Op\u00e9rateurs relationnels", "text": "

    Les op\u00e9rateurs relationnels permettent de comparer deux valeurs. Le r\u00e9sultat d'un op\u00e9rateur relationnel est toujours un bool\u00e9en c'est-\u00e0-dire que le r\u00e9sultat d'une comparaison est soit vrai, soit faux.

    Rappelons qu'en C et dans la plupart des langages de programmation, une valeur vraie est repr\u00e9sent\u00e9e par 1 et une valeur fausse par 0.

    Les op\u00e9rateurs relationnels sont les suivants\u2009:

    Op\u00e9rateurs relationnels Op\u00e9rateur Abr\u00e9viation Description Exemple vrai == eq \u00c9gal 42 == 0x101010 != ne Diff\u00e9rent 'a' != 'c' >= ge Sup\u00e9rieur ou \u00e9gal 9 >= 9 <= le Inf\u00e9rieur ou \u00e9gal -8 <= 8 > gt Strictement sup\u00e9rieur 0x31 > '0' < lg Strictement inf\u00e9rieur 8 < 12.33

    Un op\u00e9rateur relationnel est plus prioritaire qu'un op\u00e9rateur d'affectation et donc l'\u00e9criture suivante applique le test d'\u00e9galit\u00e9 entre a et b et le r\u00e9sultat de ce test 1 ou 0 sera affect\u00e9 \u00e0 la variable c :

    int a = 2, b = 3;\nint c = a == b;\n

    Les op\u00e9rateurs relationnels sont le plus souvent utilis\u00e9s dans des structures de contr\u00f4les\u2009:

    if (a == b) {\n    printf(\"Les op\u00e9randes sont \u00e9gaux.\\n\");\n} else {\n    printf(\"Les op\u00e9randes ne sont pas \u00e9gaux.\\n\");\n}\n

    Astuce

    Programmer c'est \u00eatre minimaliste, d\u00e8s lors il serait possible de simplifier l'\u00e9criture ci-dessus de la fa\u00e7on suivante\u2009:

    printf(\"Les op\u00e9randes %s \u00e9gaux.\\n\", a == b ? \"sont\" : \"ne sont pas\");\n

    Dans se cas on utilise l'op\u00e9rateur ternaire ? : qui permet de s'affranchir d'une structure de contr\u00f4le explicite.

    Attention lors de l'utilisation du test d'\u00e9galit\u00e9 avec des valeurs flottantes, ces derni\u00e8res sont des approximations et il est possible que deux valeurs qui devraient \u00eatre \u00e9gales ne le soient pas.

    Par exemple, cette assertion est fausse\u2009:

    assert(0.1 + 0.2 == 0.3) // false\n

    Pour comparer des valeurs flottantes, il est recommand\u00e9 d'utiliser une fonction de comparaison qui prend en compte une marge d'erreur. Par exemple, on pourrait \u00e9crire une fonction float_eq qui compare deux valeurs flottantes avec une marge d'erreur de 0.0001 :

    bool float_eq(float a, float b) {\n    return fabs(a - b) < 0.0001;\n}\n

    Alternativement on peut utiliser la d\u00e9finition FLT_EPSILON qui est la plus petite valeur positive telle que 1.0 + FLT_EPSILON != 1.0 :

    assert(fabs(0.1 + 0.2 - 0.3) < FLT_EPSILON);\n

    Voici une d\u00e9monstration\u2009:

    #include <stdio.h>\n#include <float.h>\n#include <math.h>\nint main() {\n    double u1 = 0.3, u2 = 0.1 + 0.2;\n    long long int i1 = *(long int*)&u1;\n    long long int i2 = *(long int*)&u2;\n    printf(\"Hex value of 0.3:\\t\\t0x%x\\n\", i1);\n    printf(\"Hex value of 0.3:\\t\\t0x%x\\n\", i2);\n\n    printf(\"Float value of 0.3:\\t\\t%.20f\\n\", u1);\n    printf(\"Float value of 0.1 + 0.2:\\t%.20f\\n\", u2);\n\n    printf(\"0.1 + 0.2 == 0.3: %d\\n\", u1 == u2);\n    printf(\"0.1 + 0.2 == 0.3: %d\\n\", fabs(u1 - u2) < DBL_EPSILON );\n}\n

    Le r\u00e9sultat de ce programme est le suivant\u2009:

    Hex value of 0.3:               0x33333333\nHex value of 0.3:               0x33333334\nFloat value of 0.3:             0.29999999999999998890\nFloat value of 0.1 + 0.2:       0.30000000000000004441\n0.1 + 0.2 == 0.3: 0\n0.1 + 0.2 == 0.3: 1\n

    Confusion = et ==

    L'erreur est si vite commise, mais souvent fatale\u2009:

    if (c = 'o') {\n\n}\n

    L'effet contre-intuitif est que le test retourne toujours VRAI, car 'o' > 0. Ajoutons que la valeur de c est modifi\u00e9 au passage.

    Observations\u2009:

    • Pour \u00e9viter toute ambigu\u00eft\u00e9, \u00e9viter les affectations dans les structures conditionnelles.

    Triple \u00e9galit\u00e9\u2009?

    Dans certains langages comme le JavaScript, il existe un op\u00e9rateur de comparaison === qui compare non seulement les valeurs mais aussi les types.

    En C, il n'existe pas d'op\u00e9rateur de comparaison de type, il faut donc faire attention \u00e0 ce que les types des op\u00e9randes soient compatibles.

    Voici une diff\u00e9rence entre C et JavaScript\u2009:

    CJavaScript
    assert('4' == 4); // false\nassert(4 == 4.0); // true\n
    assert('4' == 4); // true\nassert('4' === 4); // false\n
    ", "tags": ["FLT_EPSILON", "float_eq"]}, {"location": "course-c/15-fundations/operators/#operateurs-bit-a-bit", "title": "Op\u00e9rateurs bit \u00e0 bit", "text": "

    Les op\u00e9rations bit \u00e0 bit (bitwise) agissent sur chaque bit d'une valeur. Les disponibles en C sont les suivantes\u2009:

    Op\u00e9rateurs bit \u00e0 bit Op\u00e9rateur Description Exemple & Conjonction (ET) (0b1101 & 0b1010) == 0b1000 | Disjonction (OU) (0b1101 | 0b1010) == 0b1111 ^ XOR binaire (0b1101 ^ 0b1010) == 0b0111 ~ Compl\u00e9ment \u00e0 un ~0b11011010 == 0b00100101 << D\u00e9calage \u00e0 gauche (0b1101 << 3) == 0b1101000 >> D\u00e9calage \u00e0 droite (0b1101 >> 2) == 0b11

    Important

    Ne pas confondre l'op\u00e9rateur ! et l'op\u00e9rateur ~. Le premier est la n\u00e9gation d'un nombre tandis que l'autre est l'inversion bit \u00e0 bit. La n\u00e9gation d'un nombre diff\u00e9rent de z\u00e9ro donnera toujours 0 et la n\u00e9gation de z\u00e9ro donnera toujours 1.

    "}, {"location": "course-c/15-fundations/operators/#conjonction", "title": "Conjonction", "text": "

    La conjonction ou ET logique (\\(\\wedge\\)) est identique \u00e0 la multiplication appliqu\u00e9e bit \u00e0 bit et ne g\u00e9n\u00e8re pas de retenue.

    Conjonction bit \u00e0 bit \\(A \u2227 B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 0 \\(B=1\\) 0 1

    Avec cette op\u00e9ration l'\u00e9tat dominant est le 0 et l'\u00e9tat r\u00e9cessif est le 1. Il suffit qu'une seule valeur soit \u00e0 z\u00e9ro pour forcer le r\u00e9sultat \u00e0 z\u00e9ro\u2009:

    assert(0b1100 & 0b0011 == 0b0000)\n

    Cet op\u00e9rateur est d'ailleurs souvent utilis\u00e9 pour imposer une valeur nulle suivant une condition. Dans l'exemple suivant, le Balrog est r\u00e9duit \u00e0 n\u00e9ant par Gandalf le gris\u2009:

    balrog = 0b1100110101;\ngandalf = 0;\n\nbalrog = balrog & gandalf; // You shall not pass!\n

    "}, {"location": "course-c/15-fundations/operators/#disjonction", "title": "Disjonction", "text": "

    La disjonction ou OU logique (\\(\\lor\\)) s'apparente \u00e0 l'op\u00e9ration +.

    Disjonction bit \u00e0 bit \\(A \u2228 B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 1 \\(B=1\\) 1 1

    Ici l'\u00e9tat dominant est le 1 car il force n'importe quel 0 \u00e0 changer d'\u00e9tat\u2009:

    bool student = false; // Veut pas faire ses devoirs ?\nbool teacher = true;\n\nstudent = student | teacher; // Tes devoirs tu feras...\n

    "}, {"location": "course-c/15-fundations/operators/#disjonction-exclusive", "title": "Disjonction exclusive", "text": "

    Le OU exclusif (\\(\\oplus\\) ou \\(\\veebar\\)) est une op\u00e9ration curieuse, mais extr\u00eamement puissante et utilis\u00e9e massivement en cryptographie.

    En \u00e9lectronique sur les symboles CEI, l'op\u00e9ration logique est nomm\u00e9e, =1 car si le r\u00e9sultat de l'addition des deux op\u00e9randes est diff\u00e9rent de 1, la sortie sera nulle. Lorsque A et B valent 1 la somme vaut 2 et donc la sortie est nulle.

    Disjonction exclusive \\(A \\veebar B\\) \\(A=0\\) \\(A=1\\) \\(B=0\\) 0 1 \\(B=1\\) 1 0

    L'op\u00e9ration pr\u00e9sente une propri\u00e9t\u00e9 tr\u00e8s int\u00e9ressante\u2009: elle est r\u00e9versible.

    assert(1542 ^ 42 ^ 42 == 1542)\n

    Par exemple il est possible d'inverser la valeur de deux variables simplement\u2009:

    int a = 123;\nint b = 651;\n\na ^= b;\nb ^= a;\na ^= b;\n\nassert(a == 651);\nassert(b == 123);\n

    Attention

    Attention avec cet exemple, il ne fonctionne que si les deux valeurs a et b ont des adresses m\u00e9moires diff\u00e9rentes. Si les deux variables pointent vers la m\u00eame adresse m\u00e9moire, le r\u00e9sultat sera 0.

    int a = 123;\nint *b = &a;\n\na ^= *b;\n*b ^= a;\na ^= *b;\n\nassert(a == 0);\nassert(*b == 0);\n

    "}, {"location": "course-c/15-fundations/operators/#complement-a-un", "title": "Compl\u00e9ment \u00e0 un", "text": "

    Le compl\u00e9ment \u00e0 un (\\(\\lnot\\)) est simplement la valeur qui permet d'inverser bit \u00e0 bit une valeur\u2009:

    Compl\u00e9ment \u00e0 un \\(A\\) \\(\\lnot~A\\) 0 1 1 0

    Info

    Pourquoi l'op\u00e9ration s'appelle le compl\u00e9ment \u00e0 un\u2009? Compl\u00e9menter \u00e0 \\(N\\) signifie trouver la valeur telle que \\(A + \\lnot A = N\\). Par exemple, en base 10, pour compl\u00e9menter \u00e0 \\(A = 9\\) il suffit de faire \\(9 - A\\).

    En base 10, le symbole le plus grand est 9. En binaire le symbole le plus grand est 1. Donc pour inverser un nombre on compl\u00e9mente chaque bit \u00e0 un.

    "}, {"location": "course-c/15-fundations/operators/#decalages", "title": "D\u00e9calages", "text": "

    Les op\u00e9rations de d\u00e9calage permettent de d\u00e9placer les bits d'une valeur vers la gauche ou vers la droite. Les bits d\u00e9cal\u00e9s sont perdus et remplac\u00e9s par des z\u00e9ros dans le cas d'une valeur non sign\u00e9e et par le bit de signe dans le cas d'une valeur sign\u00e9e.

    assert(0b0000'1101 << 2 == 0b0011'0100)\nassert(0b0001'0000 >> 4 == 0b0000'0001)\n\nchar a = 0b1000'0000;\nchar b = a << 1;\nassert(b == 0x0000'0000);\n\nassert(-8 >> 1 == -4) // 0b1111'1000 >> 1 == 0b1111'1100\n

    Avertissement

    Le standard ne d\u00e9finit pas le comportement des d\u00e9calages pour des valeurs de d\u00e9calage n\u00e9gatives (a >> -2). N\u00e9anmoins il n'y aura pas d'erreur de compilation, le comportement est simplement ind\u00e9fini et le r\u00e9sultat d\u00e9pend donc du compilateur utlis\u00e9.

    "}, {"location": "course-c/15-fundations/operators/#tester-un-bit", "title": "Tester un bit", "text": "

    En micro-informatique, il est fr\u00e9quent de tester l'\u00e9tat d'un bit. Pour cela on utilise l'op\u00e9ration ET logique & avec un masque. Par exemple, pour tester le bit de poids faible d'une valeur, a on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint mask = 0b0000'0001;\n\nif (a & mask) {\n    printf(\"Le bit de poids faible est \u00e0 1.\\n\");\n} else {\n    printf(\"Le bit de poids faible est \u00e0 0.\\n\");\n}\n

    En pratique il est pr\u00e9f\u00e9rable de num\u00e9roter le bit que l'on souhaite tester pour plus de clart\u00e9. On peut positionner un bit \u00e0 la position souhait\u00e9e avec l'op\u00e9ration de d\u00e9calage << :

    int a = 0b1101'1010;\nint bit = 1;\nprintf(\"Le bit %d \u00e0 %d.\\n\", bit, a & (1 << bit));\n
    "}, {"location": "course-c/15-fundations/operators/#inverser-un-bit", "title": "Inverser un bit", "text": "

    Pour inverser un bit, on utilise l'op\u00e9ration XOR ^ avec un masque. Par exemple, pour inverser le bit de poids faible d'une valeur a on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 3;\nint b = a ^ (1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#forcer-un-bit-a-un", "title": "Forcer un bit \u00e0 un", "text": "

    Pour forcer un bit \u00e0 un, on utilise l'op\u00e9ration OU | avec un masque. Par exemple, pour forcer le bit de poids faible d'une valeur a \u00e0 un on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 2;\nint b = a | (1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#forcer-un-bit-a-zero", "title": "Forcer un bit \u00e0 z\u00e9ro", "text": "

    Pour forcer un bit \u00e0 z\u00e9ro, on utilise l'op\u00e9ration ET & avec un masque invers\u00e9. Par exemple, pour forcer le bit de poids faible d'une valeur a \u00e0 z\u00e9ro on peut \u00e9crire\u2009:

    int a = 0b1101'1010;\nint bit = 4;\nint b = a & ~(1 << bit);\n
    "}, {"location": "course-c/15-fundations/operators/#operations-logiques", "title": "Op\u00e9rations logiques", "text": "

    Les op\u00e9rateurs logiques sont au nombre de deux et ne doivent pas \u00eatre confondus avec leur petits fr\u00e8res & et |.

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur ISO646 Description Assertion vraie && and ET logique true && false == false || or OU logique true || false == true

    Le r\u00e9sultat d'une op\u00e9ration logique est toujours un bool\u00e9en (valeur 0 ou 1). Ainsi l'expression suivante affecte 1 \u00e0 x : x = 12 && 3 + 2.

    La priorit\u00e9 des op\u00e9rateurs logiques est plus faible que celle des op\u00e9rateurs de comparaison et plus forte que celle des op\u00e9rateurs d'affectation. Ainsi l'expression a == b && c == d est \u00e9quivalente \u00e0 (a == b) && (c == d). Les parenth\u00e8ses sont facultatives, mais permettent de clarifier l'expression.

    Avertissement

    La priorit\u00e9 de l'op\u00e9rateur && est plus forte que celle de l'op\u00e9rateur ||. Ainsi l'expression a || b && c est \u00e9quivalente \u00e0 a || (b && c). C'est un pi\u00e8ge classique en programmation, pour l'\u00e9viter il est recommand\u00e9 d'utiliser des parenth\u00e8ses.

    Confusion & et &&

    Confondre le ET logique et le ET binaire est courant. Dans l'exemple suivant, le if n'est jamais ex\u00e9cut\u00e9\u2009:

    int a = 0xA;\nint b = 0x5;\n\nif(a & b) {\n\n}\n
    ", "tags": ["bool\u00e9en"]}, {"location": "course-c/15-fundations/operators/#operateurs-daffectation", "title": "Op\u00e9rateurs d'affectation", "text": "

    Les op\u00e9rateurs d'affectation permettent d'assigner de nouvelles valeurs \u00e0 une variable. En C, il existe des sucres syntaxiques permettant de simplifier l'\u00e9criture lorsqu'une affectation est coupl\u00e9e \u00e0 un autre op\u00e9rateur.

    Originellement, la syntaxe h\u00e9rit\u00e9e de l'Algol-68 \u00e9tait de positionner le symbole = \u00e0 gauche suivi de l'op\u00e9rateur arithm\u00e9tique. Cette forme \u00e9tait confuse, car elle pouvait mener \u00e0 des incoh\u00e9rences d'\u00e9criture. Par exemple, l'expression x =- 3 peut \u00eatre confondue avec x = -3. Pour \u00e9viter ces ambigu\u00eft\u00e9s, le C a invers\u00e9 la logique en pla\u00e7ant l'op\u00e9rateur arithm\u00e9tique \u00e0 gauche de l'op\u00e9rateur d'affectation. L'histoire apporte parfois des r\u00e9ponses l\u00e0 o\u00f9 la logique \u00e9choue...

    Voici la liste des diff\u00e9rents op\u00e9rateurs d'affectation\u2009:

    Op\u00e9rateurs d'affectation Op\u00e9rateur Description Exemple \u00c9quivalence = Affectation simple x = y x = y += Affectation par addition x += y x = x + y -= Affectation par soustraction x -= y x = x - y *= Affectation par multiplication x *= y x = x * y /= Affectation par division x /= y x = x / y %= Affectation par modulo x %= y x = x % y &= Affectation par conjonction x &= y x = x & y |= Affectation par disjonction x |= y x = x | y ^= Affectation par XOR x ^= y x = x ^ y <<= Affectation par d\u00e9calage gauche x <<= y x = x << y >>= Affectation par d\u00e9calage droite x >>= y x = x >> y

    Un op\u00e9rateur d'affectation implique que la valeur \u00e0 gauche de l'\u00e9galit\u00e9 soit modifiable (lvalue). Ainsi l'expression 3 += 2 est incorrecte, car 3 est une constante et ne peut \u00eatre modifi\u00e9e.

    Exercice 2\u2009: R-value

    Est-ce que l'expression suivante est valide\u2009?

    int a, b, c = 42;\na + b = c;\n
    • Oui car la destination est une lvalue
    • Non car la destination est une rvalue
    Solution

    L'op\u00e9ration + entre deux nombre retourne une rvalue et ne peut donc pas \u00eatre affect\u00e9. L'expression est donc invalide.

    "}, {"location": "course-c/15-fundations/operators/#operateurs-dincrementation", "title": "Op\u00e9rateurs d'incr\u00e9mentation", "text": "

    Les op\u00e9rateurs d'incr\u00e9mentation sont r\u00e9guli\u00e8rement un motif primaire d'arrachage de cheveux pour les \u00e9tudiants. En effet, ces op\u00e9rateurs sont tr\u00e8s particuliers en ce sens qu'il se d\u00e9composent en deux \u00e9tapes\u2009: l'affectation et l'obtention du r\u00e9sultat. Il existe 4 op\u00e9rateurs d'incr\u00e9mentation\u2009:

    Op\u00e9rateurs arithm\u00e9tiques Op\u00e9rateur Description Assertion vraie ()++ Post-incr\u00e9mentation i++ ++() Pr\u00e9-incr\u00e9mentation ++i ()-- Post-d\u00e9cr\u00e9mentation i-- --() Pr\u00e9-d\u00e9cr\u00e9mentation --i

    Ces op\u00e9rateurs furent con\u00e7us initialement par Ken Thompson pour le langage B, le pr\u00e9d\u00e9cesseur du C. Ils ont \u00e9t\u00e9 repris par Dennis Ritchie pour le C. Une croyance est que ces op\u00e9rateurs furent rest\u00e9s dans le langage, car le PDP-11, la machine sur laquelle le C fut d\u00e9velopp\u00e9, poss\u00e9dait des instructions sp\u00e9cifiques pour ces op\u00e9rations.

    La pr\u00e9incr\u00e9mentation ou pr\u00e9d\u00e9cr\u00e9mentation effectue en premier la modification de la variable impliqu\u00e9e puis retourne le r\u00e9sultat de cette variable modifi\u00e9e. Dans le cas de la post-incr\u00e9mentation ou pr\u00e9d\u00e9cr\u00e9mentation, la valeur actuelle de la variable est d'abord retourn\u00e9e, puis dans un second temps cette variable est incr\u00e9ment\u00e9e.

    Notons qu'on peut toujours d\u00e9composer ces op\u00e9rateurs en deux instructions explicites. Le code\u2009:

    Forme r\u00e9duiteForme \u00e9tendue
    y = x++;\n\ny = ++x;\n
    y = x;\nx = x + 1;\n\nx = x + 1;\ny = x;\n

    Astuce

    Pour r\u00e9soudre les ambigu\u00eft\u00e9s, on proc\u00e8de par \u00e9tape. Par exemple l'expression suivante n'est pas tr\u00e8s claire\u2009:

    k = i++ * 4 + --j * 2\n
    1. On commence par r\u00e9soudre les pr\u00e9-incr\u00e9mentation et pr\u00e9-d\u00e9cr\u00e9mentation\u2009:

      j = j - 1;\nk = i++ * 4 + j * 2\n
    2. Ensuite on r\u00e9sout les post-incr\u00e9mentation\u2009:

      j = j - 1;\nk = i * 4 + j * 2\ni = i + 1;\n
    3. On peut utiliser les sucres syntaxiques pour simplifier l'\u00e9criture\u2009:

      j -= 1;\nk = i * 4 + j * 2\ni += 1;\n

    \u00c9criture d\u00e9routante

    Selon la table de pr\u00e9c\u00e9dences on aura i-- calcul\u00e9 en premier suivi de - -j:

    k = i----j;\n

    Observations\u2009:

    • \u00c9viter les formes ambig\u00fces d'\u00e9criture
    • Favoriser la pr\u00e9c\u00e9dence explicite en utilisant des parenth\u00e8ses
    • S\u00e9parez vos op\u00e9rations par des espaces pour plus de lisibilit\u00e9\u2009: k = i-- - -j

    Astuce

    Il est g\u00e9n\u00e9ralement pr\u00e9f\u00e9rable d'utiliser la pr\u00e9-incr\u00e9mentation ou la pr\u00e9-d\u00e9cr\u00e9mentation car elles sont plus efficaces. En effet, la post-incr\u00e9mentation ou la post-d\u00e9cr\u00e9mentation n\u00e9cessitent de stocker la valeur actuelle de la variable pour la retourner apr\u00e8s l'incr\u00e9mentation ou la d\u00e9cr\u00e9mentation.

    C'est particuli\u00e8rement le cas en C++ o\u00f9 la post-incr\u00e9mentation ou la post-d\u00e9cr\u00e9mentation n\u00e9cessitent de cr\u00e9er une copie de la variable avant de l'incr\u00e9menter ou de la d\u00e9cr\u00e9menter.

    En C++ on utilise la surcharge d'op\u00e9rateur pour d\u00e9finir le comportement de l'op\u00e9rateur ++ et -- pour les classes personnalis\u00e9es. Ajouter \u00e0 une classe ce type de surcharge se fait comme ceci\u2009:

    class MyClass {\npublic:\n    // Pr\u00e9-incr\u00e9mentation\n    auto operator++() {\n        return *this;\n    }\n\n    // Post-incr\u00e9mentation\n    auto operator++(int) {\n        MyClass tmp(*this);\n        operator++();\n        return tmp;\n    }\n};\n

    On voit que la post-incr\u00e9mentation cr\u00e9e une copie de l'objet avant de l'incr\u00e9menter, elle est donc moins efficace.

    Donc dans une boucle for on pr\u00e9f\u00e9rera\u2009:

    for (int i = 0; i < 10; ++i) { }\n

    Plut\u00f4t que

    for (int i = 0; i < 10; i++) { }\n

    ", "tags": ["for"]}, {"location": "course-c/15-fundations/operators/#operateur-ternaire", "title": "Op\u00e9rateur ternaire", "text": "

    L'op\u00e9rateur ternaire aussi appel\u00e9 op\u00e9rateur conditionnel permet de faire un test et de retourner soit le second op\u00e9rande, soit le troisi\u00e8me op\u00e9rande. C'est le seul op\u00e9rateur du C avec une arit\u00e9 de 3. Chacun des op\u00e9randes est symbolis\u00e9 avec une paire de parenth\u00e8ses\u2009:

    ()?():()\n

    Cet op\u00e9rateur permet sur une seule ligne d'\u00e9valuer une expression et de renvoyer une valeur ou une autre selon que l'expression est vraie ou fausse.

    valeur = (condition ? valeur si condition vraie : valeur si condition fausse);\n

    Note

    Seule la valeur utilis\u00e9e pour le r\u00e9sultat est \u00e9valu\u00e9e. Par exemple, dans le code x > y ? ++y : ++x, seulement x ou y sera incr\u00e9ment\u00e9.

    On utilise volontiers cet op\u00e9rateur lorsque dans les deux cas d'un embranchement, la m\u00eame valeur est modifi\u00e9e\u2009:

    if (a > b)\n    max = a;\nelse\n    min = b;\n

    On remarque dans cet exemple une r\u00e9p\u00e9tition max =. Une fa\u00e7on plus \u00e9l\u00e9gante et permettant de r\u00e9duire l'\u00e9criture est d'utiliser l'op\u00e9rateur ternaire\u2009:

    max = a > b ? a : b;\n

    Avertissement

    Ne pas utiliser l'op\u00e9rateur ternaire si vous ne modifiez pas une valeur. L'op\u00e9rateur ternaire est un op\u00e9rateur de s\u00e9lection et non de modification.

    Bon exempleMauvais exemple
    int max = a > b ? a : b;\n
    a > b ? max = a : min = b;\n

    Cela va de m\u00eame pour afficher une valeur\u2009:

    Bon exempleMauvais exemple
    printf(\"Le maximum est %d\\n\", a > b ? a : b);\n
    a > b ? printf(\"Le maximum est %d\\n\", a) : printf(\"Le maximum est %d\\n\", b);\n

    Enfin, on notera que le r\u00e9sultat de l'op\u00e9rateur ternaire est une rvalue et ne peut donc pas \u00eatre modifi\u00e9e.

    ", "tags": ["arit\u00e9"]}, {"location": "course-c/15-fundations/operators/#operateur-de-transtypage", "title": "Op\u00e9rateur de transtypage", "text": "

    Le transtypage ou cast permet de modifier explicitement le type apparent d'une variable. C'est un op\u00e9rateur particulier, car son premier op\u00e9rande doit \u00eatre un type et le second une valeur.

    (type)(valeur)\n

    Dans l'exemple suivant, le r\u00e9sultat de la division est un entier, car la promotion implicite de type reste un entier int. La valeur c vaudra donc le r\u00e9sultat de la division enti\u00e8re alors que dans le second cas, b est cast\u00e9 en un double ce qui force une division en virgule flottante.

    int a = 5, b = 2;\ndouble c = a / b;\ndouble d = a / (double)(b);\nassert(c == 2.0 && d == 2.5);\n

    ", "tags": ["double", "int", "transtypage"]}, {"location": "course-c/15-fundations/operators/#operateur-sequentiel", "title": "Op\u00e9rateur s\u00e9quentiel", "text": "

    L'op\u00e9rateur s\u00e9quentiel (comma operator) permet l'ex\u00e9cution ordonn\u00e9e d'op\u00e9rations, et retourne la derni\u00e8re valeur. Son utilisation est couramment limit\u00e9e, soit aux d\u00e9clarations de variables, soit au boucles for:

    for (size_t i = 0, j = 10; i != j; i++, j--) { /* ... */ }\n

    Dans le cas ci-dessus, il n'est pas possible de s\u00e9parer les instructions i++ et j-- par un point-virgule, l'op\u00e9rateur virgule permet alors de combiner plusieurs instructions en une seule.

    Une particularit\u00e9 de cet op\u00e9rateur est que seule la derni\u00e8re valeur est retourn\u00e9e\u2009:

    assert(3 == (1, 2, 3))\n

    L'op\u00e9rateur agit \u00e9galement comme un Point de s\u00e9quence , c'est-\u00e0-dire que l'ordre des \u00e9tapes est respect\u00e9.

    Exercice 3\u2009: Op\u00e9rateur s\u00e9quentiel

    Que sera-t-il affich\u00e9 \u00e0 l'\u00e9cran\u2009?

    int i = 0;\nprintf(\"%d\", (++i, i++, ++i));\n
    ", "tags": ["for"]}, {"location": "course-c/15-fundations/operators/#operateur-sizeof", "title": "Op\u00e9rateur sizeof", "text": "

    Cet op\u00e9rateur est unaire et retourne la taille en byte de la variable ou du type pass\u00e9 en argument. Il n'existe pas de symbole particulier et son usage est tr\u00e8s similaire \u00e0 l'appel d'une fonction\u2009:

    int32_t foo = 42;\nassert(sizeof(foo) == 4);\nassert(sizeof(int64_t) == 64 / 8);\n

    L'op\u00e9rateur sizeof est tr\u00e8s utile durant le d\u00e9bogage pour conna\u00eetre la taille en m\u00e9moire d'une variable ou celle d'un type. On l'utilise en pratique pour conna\u00eetre la taille d'un tableau lors d'une boucle it\u00e9rative\u2009:

    int32_t array[128];\nfor (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n   array[i] = i * 10;\n}\n

    Dans l'exemple ci-dessus, sizeof(array) retourne la taille de l'espace m\u00e9moire occup\u00e9 par le tableau array, soit \\(128 \\cdot 4\\) bytes. Pour obtenir le nombre d'\u00e9l\u00e9ments dans le tableau, il faut alors diviser ce r\u00e9sultat par la taille effective de chaque \u00e9l\u00e9ment du tableau. L'\u00e9l\u00e9ment array[0] est donc un int32_t et sa taille vaut donc 4 bytes.

    Note

    Dans l'exemple ci-dessus, il est possible de s'affranchir de la taille effective du tableau en utilisant une sentinelle. Si le dernier \u00e9l\u00e9ment du tableau \u00e0 une valeur particuli\u00e8re et que le reste est initialis\u00e9 \u00e0 z\u00e9ro, il suffit de parcourir le tableau jusqu'\u00e0 cette valeur\u2009:

    int32_t array[128] = { [127]=-1 };\nint i = 0;\nwhile (array[i] != -1) {\n    array[i++] = i * 10;\n}\n

    Cette \u00e9criture reste malgr\u00e9 tout tr\u00e8s mauvaise, car le tableau de 128 \u00e9l\u00e9ments doit \u00eatre initialis\u00e9 \u00e0 priori, ce qui m\u00e8ne aux m\u00eames performances. D'autre part l'histoire racont\u00e9e par le d\u00e9veloppeur est moins claire que la premi\u00e8re impl\u00e9mentation.

    ", "tags": ["array", "int32_t", "sizeof"]}, {"location": "course-c/15-fundations/operators/#priorite-des-operateurs", "title": "Priorit\u00e9 des op\u00e9rateurs", "text": "

    La pr\u00e9c\u00e9dence est un anglicisme de precedence (priorit\u00e9) qui concerne la priorit\u00e9 des op\u00e9rateurs, ou l'ordre dans lequel les op\u00e9rateurs sont ex\u00e9cut\u00e9s.

    Chacun conna\u00eet la priorit\u00e9 des quatre op\u00e9rateurs de base (+, -, *, /), mais le C et ses nombreux op\u00e9rateurs sont bien plus complexes.

    La table suivante indique les r\u00e8gles \u00e0 suivre pour les pr\u00e9c\u00e9dences des op\u00e9rateurs en C.

    Priorit\u00e9 des op\u00e9rateurs Priorit\u00e9 Op\u00e9rateur Description Associativit\u00e9 1 ++, -- Postfix incr\u00e9ments/d\u00e9cr\u00e9ments Gauche \u00e0 Droite () Appel de fonction [] Indexage des tableaux . \u00c9l\u00e9ment d'une structure -> \u00c9l\u00e9ment d'une structure 2 ++, -- Pr\u00e9fixe incr\u00e9ments/d\u00e9cr\u00e9ments Droite \u00e0 Gauche +, - Signe !, ~ NON logique et NON binaire (type) Cast (Transtypage) * Indirection, d\u00e9r\u00e9f\u00e9rencement & Adresse de... sizeof Taille de... 3 *, /, % Multiplication, Division, Mod Gauche \u00e0 Droite 4 +, - Addition, soustraction 5 <<, >> D\u00e9calages binaires 6 <, <= Comparaison plus petit que >, >= Comparaison plus grand que 7 ==, != \u00c9galit\u00e9, non \u00e9galit\u00e9 8 & ET binaire 9 ^ OU exclusif binaire 10 | OU inclusif binaire 11 && ET logique 12 || OU logique 13 ?: Op\u00e9rateur ternaire Droite \u00e0 Gauche 14 = Assignation simple +=, -= Assignation par somme/diff *=, /=, %= Assignation par produit/quotient/modulo <<=, >>= Assignation par d\u00e9calage binaire 15 , Virgule Gauche \u00e0 Droite

    Consid\u00e9rons l'exemple suivant\u2009:

    int i[2] = {10, 20};\nint y = 3;\n\nx = 5 + 23 + 34 / ++i[0] & 0xFF << y;\n

    Selon la pr\u00e9c\u00e9dence de chaque op\u00e9rateur ainsi que son associativit\u00e9 on a\u2009:

    []  1\n++  2\n/   3\n+   4\n+   4\n<<  5\n&   8\n=   14\n

    Notation polonaise invers\u00e9e

    La notation polonaise invers\u00e9e (Reverse Polish Notation) est une notation math\u00e9matique o\u00f9 les op\u00e9rateurs sont plac\u00e9s apr\u00e8s leurs op\u00e9randes.

    L'\u00e9criture en notation polonaise invers\u00e9e donnerait alors\u2009:

    34, i, 0, [], ++,  /, 5, 23, +, +, 0xFF, y, <<, &, x, =\n

    C'est une notation tr\u00e8s utilis\u00e9e en informatique pour les calculatrices et les compilateurs car elle permet de simplifier l'\u00e9criture des expressions math\u00e9matiques, et surtout s'affranchir du probl\u00e8me des priorit\u00e9s d'op\u00e9rateurs.

    L'algorithme de Shunting Yard permet de convertir une expression en notation infix\u00e9e en une expression en notation polonaise invers\u00e9e.

    ", "tags": ["sizeof"]}, {"location": "course-c/15-fundations/operators/#associativite", "title": "Associativit\u00e9", "text": "

    L'associativit\u00e9 des op\u00e9rateurs (operator associativity) d\u00e9crit la mani\u00e8re dont sont \u00e9valu\u00e9es les expressions.

    Une associativit\u00e9 \u00e0 gauche pour l'op\u00e9rateur ~ signifie que l'expression a ~ b ~ c sera \u00e9valu\u00e9e ((a) ~ b) ~ c alors qu'une associativit\u00e9 \u00e0 droite sera a ~ (b ~ (c)).

    Note qu'il ne faut pas confondre l'associativit\u00e9 \u00e9valu\u00e9e de gauche \u00e0 droite qui est une associativit\u00e9 \u00e0 gauche.

    "}, {"location": "course-c/15-fundations/operators/#promotion-de-type", "title": "Promotion de type", "text": "

    Nous avons vu au chapitre sur les types de donn\u00e9es que les types C d\u00e9finis par d\u00e9faut sont repr\u00e9sent\u00e9s en m\u00e9moire sur 1, 2, 4 ou 8 octets. On comprend ais\u00e9ment que plus cette taille est importante, plus on gagne en pr\u00e9cision ou en grandeur repr\u00e9sentable. La promotion num\u00e9rique r\u00e9git les conversions effectu\u00e9es implicitement par le langage C lorsqu'on convertit une donn\u00e9e d'un type vers un autre. Cette promotion tend \u00e0 conserver le maximum de pr\u00e9cision lorsqu'on effectue des calculs entre types diff\u00e9rents (p.ex\u2009: l'addition d'un int avec un double donne un type double). Voici les r\u00e8gles de base\u2009:

    • les op\u00e9rateurs ne peuvent agir que sur des types identiques\u2009;
    • quand les types sont diff\u00e9rents, il y a conversion automatique vers le type ayant le plus grand pouvoir de repr\u00e9sentation\u2009;
    • les conversions ne sont faites qu'au fur et \u00e0 mesure des besoins.

    La promotion est l'action de promouvoir un type de donn\u00e9e en un autre type de donn\u00e9e plus g\u00e9n\u00e9ral. On parle de promotion implicite des entiers lorsqu'un type est promu en un type plus grand automatiquement par le compilateur.

    ", "tags": ["double", "int"]}, {"location": "course-c/15-fundations/operators/#lois-de-de-morgan", "title": "Lois de De Morgan", "text": "

    Les lois de De Morgan sont des identit\u00e9s logiques formul\u00e9es il y a pr\u00e8s de deux si\u00e8cles par Augustus De Morgan (1806-1871). \u00c0 noter que l'on peut prononcer d\u0259 m\u0254\u0281.g\u0251\u0303 (de Mort Gant) ou d\u0259 m\u0254\u0281.\u0261an (de Morgane).

    En logique classique, la n\u00e9gation d'une conjonction implique la disjonction des n\u00e9gations et la conjonction de n\u00e9gations implique la n\u00e9gation d'une disjonction. On peut donc \u00e9crire les relations suivantes\u2009:

    \\[ \\begin{aligned} & \\overline{P \\land Q} &\\Rightarrow~& \\overline{P} \\lor \\overline{Q} \\\\ & \\overline{P} \\land \\overline{Q} &\\Rightarrow~& \\overline{P \\lor Q} \\end{aligned} \\]

    Ces op\u00e9rations logiques sont tr\u00e8s utiles en programmation o\u00f9 elles permettent de simplifier certains algorithmes.

    \u00c0 titre d'exemple, les op\u00e9rations suivantes sont \u00e9quivalentes\u2009:

    int a = 0b110010011;\nint b = 0b001110101;\n\nassert(a | b == ~a & ~b);\nassert(~a & ~b == ~(a | b));\n

    En logique bool\u00e9enne on exprime la n\u00e9gation par une barre p.ex. \\(\\overline{P}\\).

    Exercice 4\u2009: De Morgan

    Utiliser les relations de De Morgan pour simplifier l'expression suivante

    \\[ D \\cdot E + \\overline{D} + \\overline{E} \\] Solution

    Si l'on applique De Morgan (\\(\\overline{XY} = \\overline{X} + \\overline{Y}\\)):

    \\[ D \\cdot E + \\overline{D} + \\overline{E} \\]

    "}, {"location": "course-c/15-fundations/operators/#arrondis", "title": "Arrondis", "text": "

    En programmation, la notion d'arrondi (rounding) est beaucoup plus d\u00e9licate que l'on peut l'imaginer de prime abord.Un nombre r\u00e9el dans \\(\\mathbb{R}\\) peut \u00eatre converti en un nombre entier de plusieurs mani\u00e8res. Les m\u00e9thodes les plus courantes sont donn\u00e9es dans la table suivante.

    M\u00e9thodes d'arrondi M\u00e9thode Description truncate Suppression de la partie fractionnaire ceiling Arrondi \u00e0 l'entier sup\u00e9rieur floor Arrondi \u00e0 l'entier inf\u00e9rieur towards zero Arrondi en direction du z\u00e9ro away from zero Arrondi loin du z\u00e9ro to the nearest integer Arrondi au plus proche entier rounding half up Arrondi la moiti\u00e9 en direction de l'infini rounding half to even Arrondi la moiti\u00e9 vers l'entier pair le plus proche

    Selon le langage de programmation et la m\u00e9thode utilis\u00e9e, le m\u00e9canisme d'arrondi sera diff\u00e9rent. En C, la biblioth\u00e8que math\u00e9matique offre les fonctions ceil pour l'arrondi au plafond (entier sup\u00e9rieur), floor pour arrondi au plancher (entier inf\u00e9rieur) et round pour l'arrondi au plus proche (nearest). Il existe \u00e9galement une fonction trunc qui tronque la valeur en supprimant la partie fractionnaire.

    Le fonctionnement de la fonction round n'est pas unanime entre les math\u00e9maticiens et les programmeurs. C utilise l'arrondi au plus proche, c'est-\u00e0-dire que -23.5 donne -24 et 23.5 donnent 24.

    La m\u00e9thode rounding half to even est aussi nomm\u00e9e bankers' rounding. Elle est utilis\u00e9e dans de nombreux langages de programmation, car elle minimise les erreurs d'arrondi. Cette m\u00e9thode arrondit les nombres \u00e0 l'entier pair le plus proche. Par exemple, 0.5 est arrondi \u00e0 0 et 1.5 est arrondi \u00e0 2. C'est la m\u00e9thode d'arrondi conseill\u00e9e par l'IEEE 754 pour les calculs en virgule flottante. Cette m\u00e9thode est pr\u00e9f\u00e9r\u00e9e pour deux raisons principales\u2009:

    1. R\u00e9duction du biais cumulatif : Lorsque vous arrondissez toujours vers le haut ou vers le bas en cas de valeur \u00e0 mi-chemin (comme 0.5), cela introduit un biais syst\u00e9matique dans vos donn\u00e9es. Par exemple, si vous arrondissez toujours 0.5 vers le haut, la somme des valeurs arrondies sera syst\u00e9matiquement plus grande que la somme des valeurs originales.

    2. Statistiques plus pr\u00e9cises : En arrondissant les valeurs \u00e0 la paire la plus proche, vous distribuez les erreurs d'arrondissement de mani\u00e8re plus \u00e9quitable, ce qui donne des statistiques globales plus pr\u00e9cises.

    Supposons que nous avons les montants suivants\u2009:

    3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5\n

    En utilisant l'arrondi classique (toujours vers le haut \u00e0 0.5), nous obtenons\u2009:

    4 + 5 + 6 + 7 + 8 + 9 + 10 = 49\n

    En utilisant l'arrondi commercial, nous obtenons\u2009:

    4 + 4 + 6 + 6 + 8 + 8 + 10 = 46\n

    Si on compare \u00e0 la somme r\u00e9elle des valeurs, on obtient\u2009:

    3.5 + 4.5 + 5.5 + 6.5 + 7.5 + 8.5 + 9.5 = 45.5\n

    La m\u00e9thode round half to even donne une somme arrondie (46) qui est plus proche de la somme r\u00e9elle (45.5) que la m\u00e9thode classique (49).

    L'utilisation cette m\u00e9thode est particuli\u00e8rement utile dans les domaines o\u00f9 l'exactitude statistique est cruciale et o\u00f9 les erreurs d'arrondissement peuvent s'accumuler sur de grands ensembles de donn\u00e9es, comme en finance, en analyse de donn\u00e9es, et en statistiques.

    ", "tags": ["floor", "round", "ceil", "trunc"]}, {"location": "course-c/15-fundations/operators/#valeurs-gauches", "title": "Valeurs gauches", "text": "

    Une valeur gauche (lvalue) est une particularit\u00e9 de certains langages de programmation qui d\u00e9finissent ce qui peut se trouver \u00e0 gauche d'une affectation. Ainsi dans x = y, x est une valeur gauche. N\u00e9anmoins, l'expression x = y est aussi une valeur gauche\u2009:

    int x, y, z;\n\nx = y = z;    // 1\n(x = y) = z;  // 2\n
    1. L'associativit\u00e9 de = est \u00e0 droite donc cette expression est \u00e9quivalente \u00e0 x = (y = (z)) qui \u00e9vite toute ambigu\u00eft\u00e9.

    2. En for\u00e7ant l'associativit\u00e9 \u00e0 gauche, on essaie d'assigner z \u00e0 une lvalue et le compilateur s'en plaint\u2009:

    4:8: error: lvalue required as left operand of assignment\n  (x = y) = z;\n          ^\n

    Voici quelques exemples de valeurs gauches\u2009:

    • x /= y
    • ++x
    • (x ? y : z)

    Par analogie une rvalue est une valeur qui ne peut se trouver \u00e0 gauche d'une affectation. Ainsi x + y est une rvalue car elle ne peut \u00eatre affect\u00e9e. De m\u00eame que x++ est une rvalue car elle ne peut \u00eatre affect\u00e9e.

    ", "tags": ["lvalue"]}, {"location": "course-c/15-fundations/operators/#optimisation", "title": "Optimisation", "text": "

    Le compilateur est en r\u00e8gle g\u00e9n\u00e9ral plus malin que le d\u00e9veloppeur. L'optimiseur de code (lorsque compil\u00e9 avec -O2 sous gcc), va regrouper certaines instructions, modifier l'ordre de certaines d\u00e9clarations pour r\u00e9duire soit l'empreinte m\u00e9moire du code, soit acc\u00e9l\u00e9rer son ex\u00e9cution.

    Ainsi l'expression suivante, ne sera pas calcul\u00e9e \u00e0 l'ex\u00e9cution, mais \u00e0 la compilation\u2009:

    int num = (4 + 7 * 10) >> 2;\n

    De m\u00eame que ce test n'effectuera pas une division, mais testera simplement le dernier bit de a:

    if (a % 2) {\n    puts(\"Pair\");\n} else {\n    puts(\"Impair\");\n}\n
    ", "tags": ["gcc"]}, {"location": "course-c/15-fundations/operators/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 5\u2009: Parenth\u00e8ses superflues

    Dans les expressions suivantes, lesquelles contiennent des parenth\u00e8ses superflues qui peuvent \u00eatre retir\u00e9es sans changer le sens de l'expression\u2009?

    • a = (b + c) * d
    • a = b + (c * d)
    • (a < b) && (c > d)
    • a = (b + c) / (d + e)
    • (a && b) || (c && d)
    • (a || b) && (c || d)
    • a = (b + c) % (d + e)

    Exercice 6\u2009: Quelle priorit\u00e9

    Quel est l'op\u00e9rateur qui a la priorit\u00e9 la plus basse\u2009?

    • +
    • *
    • ||
    • &&

    Exercice 7\u2009: Masque binaire

    Soit les d\u00e9clarations suivantes\u2009:

    char m, n = 2, d = 0x55, e = 0xAA;\n

    Repr\u00e9senter en binaire et en hexad\u00e9cimal la valeur de tous les bits de la variable m apr\u00e8s ex\u00e9cution de chacune des instructions suivantes\u2009:

    1. m = 1 << n;
    2. m = ~1 << n;
    3. m = ~(1 << n);
    4. m = d | (1 << n);
    5. m = e | (1 << n);
    6. m = d ^ (1 << n);
    7. m = e ^ (1 << n);
    8. m = d & ~(1 << n);
    9. m = e & ~(1 << n);

    Exercice 8\u2009: Registre syst\u00e8me

    Pour programmer les registres 16-bits d'un composant \u00e9lectronique charg\u00e9 de g\u00e9rer des sorties tout ou rien, on doit \u00eatre capable d'effectuer les op\u00e9rations suivantes\u2009:

    • mettre \u00e0 1 le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;
    • mettre \u00e0 0 le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;
    • inverser le bit num\u00e9ro n, n \u00e9tant un entier entre 0 et 15\u2009;

    Pour des questions d'efficacit\u00e9, ces op\u00e9rations ne doivent utiliser que les op\u00e9rateurs bit \u00e0 bit ou d\u00e9calage. On appelle r0 la variable d\u00e9signant le registre en m\u00e9moire et n la variable contenant le num\u00e9ro du bit \u00e0 modifier. \u00c9crivez les expressions permettant d'effectuer les op\u00e9rations demand\u00e9es.

    Exercice 9\u2009: Recherche d'expressions

    Consid\u00e9rant les d\u00e9clarations suivantes\u2009:

    float a, b;\nint m, n;\n

    Traduire en C les expressions math\u00e9matiques ci-dessous\u2009; pour chacune, proposer plusieurs \u00e9critures diff\u00e9rentes lorsque c'est possible. Le symbole \\(\\leftarrow\\) signifie assignation

    1. \\(n \\leftarrow 8 \\cdot n\\)
    2. \\(a \\leftarrow a + 2\\)
    3. \\(n \\leftarrow \\left\\{\\begin{array}{lr}m & : m > 0\\\\ 0 & : \\text{sinon}\\end{array}\\right.\\)
    4. \\(a \\leftarrow n\\)
    5. \\(n \\leftarrow \\left\\{\\begin{array}{lr}0 & : m~\\text{pair}\\\\ 1 & : m~\\text{impair}\\end{array}\\right.\\)
    6. \\(n \\leftarrow \\left\\{\\begin{array}{lr}1 & : m~\\text{pair}\\\\ 0 & : m~\\text{impair}\\end{array}\\right.\\)
    7. \\(m \\leftarrow 2\\cdot m + 2\\cdot n\\)
    8. \\(n \\leftarrow n + 1\\)
    9. \\(a \\leftarrow \\left\\{\\begin{array}{lr}-a & : b < 0\\\\ a & : \\text{sinon}\\end{array}\\right.\\)
    10. \\(n \\leftarrow \\text{la valeur des 4 bits de poids faible de}~n\\)

    Exercice 10\u2009: Nombres narcissiques

    Un nombre narcissique ou nombre d'Amstrong est un entier naturel n non nul qui est \u00e9gal \u00e0 la somme des puissances p-i\u00e8mes de ses chiffres en base dix, o\u00f9 p d\u00e9signe le nombre de chiffres de n:

    \\[ n=\\sum_{k=0}^{p-1}x_k10^k=\\sum_{k=0}^{p-1}(x_k)^p\\quad\\text{avec}\\quad x_k\\in\\{0,\\ldots,9\\}\\quad\\text{et}\\quad x_{p-1}\\ne 0 \\]

    Par exemple\u2009:

    • 9 est un nombre narcissique, car \\(9 = 9^1 = 9\\)
    • 153 est un nombre narcissique, car \\(153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153\\)
    • 10 n'est pas un nombre narcissique, car \\(10 \\ne 1^2 + 0^2 = 1\\)

    Implanter un programme permettant de v\u00e9rifier si un nombre d'entr\u00e9es est narcissique ou non. L'ex\u00e9cution est la suivante\u2009:

    $ ./armstrong 153\n1\n\n$ ./armstrong 154\n0\n

    Exercice 11\u2009: Swap sans valeur interm\u00e9diaire

    Soit deux variables enti\u00e8res a et b, chacune contenant une valeur diff\u00e9rente. \u00c9crivez les instructions permettant d'\u00e9changer les valeurs de a et de b sans utiliser de valeurs interm\u00e9diaires. Indice\u2009: utilisez l'op\u00e9rateur XOR ^.

    Testez votre solution...

    Solution
    a ^= b;\nb ^= a;\na ^= b;\n
    "}, {"location": "course-c/15-fundations/preprocessor/", "title": "Pr\u00e9processeur", "text": "

    Illustration du m\u00e9canisme de pr\u00e9-processing avant la compilation

    Comme nous l'avons vu en introduction, le langage C est bas\u00e9 sur une double grammaire, c'est-\u00e0-dire qu'avant la compilation du code, un autre processus est appel\u00e9 visant \u00e0 pr\u00e9parer le code source avant la compilation. Le c\u0153ur de cette op\u00e9ration est appel\u00e9 pr\u00e9processeur. Les instructions du pr\u00e9processeur C sont faciles \u00e0 reconna\u00eetre, car elles d\u00e9butent toutes par le croisillon # 0023, hash (ou she) en anglais et utilis\u00e9es r\u00e9cemment comme hashtag sur les r\u00e9seaux sociaux. Notons au passage que ce caract\u00e8re \u00e9tait historiquement utilis\u00e9 par les Anglais sous le d\u00e9nominatif pound (livre). Lorsqu'il est apparu en Europe, il a \u00e9t\u00e9 confondu avec le caract\u00e8re di\u00e8se (sharp) \u266f 266F pr\u00e9sent sur les pav\u00e9s num\u00e9riques de t\u00e9l\u00e9phone.

    Le vocabulaire du pr\u00e9processeur se compose de directives d\u00e9marrant par un croisillon. Notons que ces directives (\u00e0 l'exception des op\u00e9rateurs de concat\u00e9nation de conversion en cha\u00eene de caract\u00e8re) sont des instructions de ligne (line-wise), c'est-\u00e0-dire qu'elles doivent se terminer par un caract\u00e8re de fin de ligne. Le point-virgule n'a pas d'effet sur le pr\u00e9processeur. En outre, il est possible d'ins\u00e9rer des espaces et des tabulations entre le croisillon et la directive. Il est commun\u00e9ment admis d'utiliser cette fonctionnalit\u00e9 pour g\u00e9rer l'indentation des directives pr\u00e9processeur, car certaines conventions imposent que le croisillon soit en premi\u00e8re colonne. La table suivante r\u00e9sume les directives du pr\u00e9processeur.

    Vocabulaire du pr\u00e9processeur Terme Description #include Inclus un fichier dans le fichier courant #define Cr\u00e9e une d\u00e9finition (Macro) #undef D\u00e9truit une d\u00e9finition existante #if defined Teste si une d\u00e9finition existe #if .. #endif Test conditionnel # Op\u00e9rateur de conversion en cha\u00eene de caract\u00e8res ## Op\u00e9rateur de concat\u00e9nation de cha\u00eenes #line Directive de ligne #error \"error message\" G\u00e9n\u00e8re une erreur #pragma Directive sp\u00e9cifique au compilateur

    Le pr\u00e9processeur C est ind\u00e9pendant du langage C, c'est-\u00e0-dire qu'il peut \u00eatre ex\u00e9cut\u00e9 sur n'importe quel type de fichier. Pour le prouver, prenons l'exemple d'une lettre g\u00e9n\u00e9rique d'un cabinet dentaire\u2009:

    #ifdef FEMALE\n#    define NAME Madame\n#else\n#    define NAME Monsieur\n#endif\nBonjour NAME,\n\nVeuillez noter votre prochain rendez-vous le DATE, \u00e0 HOUR heure.\n\nVeuillez agr\u00e9er, NAME, nos meilleures salutations,\n\n#ifdef IS_BOSS\nLe directeur\n#elif defined IS_ASSISTANT\nLa secr\u00e9taire du directeur\n#elif defined OWNER_NAME\nOWNER_NAME\n#else\n#    error \"Lettre sans signature\"\n#endif\n

    Il est possible d'appeler le pr\u00e9processeur directement avec l'option -E de gcc. Des directives define additionnelles peuvent \u00eatre renseign\u00e9es depuis la ligne de commande avec le drapeau -D. Voici un exemple d'utilisation\u2009:

    $ gcc -xc -E test.txt \\\n    -DDATE=22 -DHOUR=9:00 \\\n    -DFEMALE \\\n    -DOWNER_NAME=\"Adam\" -DPOSITION=employee\n# 1 \"test.txt\"\n# 1 \"<built-in>\"\n# 1 \"<command-line>\"\n# 1 \"/usr/include/stdc-predef.h\" 1 3 4\n# 1 \"<command-line>\" 2\n# 1 \"test.txt\"\nBonjour Madame,\n\nVeuillez noter votre prochain rendez-vous le 22, \u00e0 9:00 heure.\n\nVeuillez agr\u00e9er, Madame, nos meilleures salutations,\n\nAdam\n

    En sortie il reste des directives # nomm\u00e9es linemarkers ou marqueurs de lignes qui sont des commentaires utilis\u00e9s pour le d\u00e9verminage. Ces directives de lignes contiennent en premi\u00e8re position le num\u00e9ro de la ligne du fichier originel suivi d'informations sont utiles pour le d\u00e9bogage, car elles permettent de retrouver la source des erreurs. Le format est sp\u00e9cifique \u00e0 GCC. Un marqueur de ligne \u00e0 le format suivant\u2009:

    # linenum filename flags\n

    Les drapeaux peuvent \u00eatre\u2009: 1 pour indiquer le d\u00e9but d'un nouveau fichier inclus, 2 pour indiquer la fin d'un fichier inclus, 3 pour indiquer que le texte suivant provient d'un fichier syst\u00e8me et 4 pour indiquer que le texte doit \u00eatre trait\u00e9 comme un bloc implicite.

    Suppression des marqueurs de lignes

    Il est possible de supprimer les directives de lignes g\u00e9n\u00e9r\u00e9es par le pr\u00e9processeur avec l'option -P de gcc. Le -xc indique que le fichier est un fichier C ce qui \u00e9vite un message d'erreur su l'extension du fichier d'entr\u00e9e n'est pas .c.

    $ gcc -xc -E test.txt -P\n
    ", "tags": ["define", "gcc"]}, {"location": "course-c/15-fundations/preprocessor/#phases-de-traduction", "title": "Phases de traduction", "text": "

    Le standard C99 \u00a75.1.1.2 alin\u00e9a 1 d\u00e9finit 8 phases de traductions dont les 4 premi\u00e8res concernent le pr\u00e9processeur\u2009:

    1. Remplacement des multicaract\u00e8res\u2009: les caract\u00e8res Unicode et autres caract\u00e8res sp\u00e9ciaux sont remplac\u00e9s par des caract\u00e8res ASCII. Les trigraphes sont \u00e9galement interpr\u00e9t\u00e9s.

    2. Remplacement des backslash de fin de ligne. Lorsqu'un backslash est suivi d'un saut de ligne, le saut de ligne est supprim\u00e9. Cela permet d'\u00e9crire des lignes longues sur plusieurs lignes.

    3. Suppression des commentaires. Chaque commentaire est remplac\u00e9 par une espace. Les commentaires de type // et /* */ sont par cons\u00e9quent supprim\u00e9s.

    4. Les directives de pr\u00e9traitement sont ex\u00e9cut\u00e9es, les invocations de macros sont \u00e9tendues et les expressions avec l'op\u00e9rateur unaire _Pragma sont ex\u00e9cut\u00e9es ainsi que les directives #include appliqu\u00e9es r\u00e9cursivement.

    Voici un exemple de code C avec des directives de pr\u00e9traitement\u2009:

    int no\u00ebl[] = ??< 23, 42 ??> ; // Array of integers\nchar *\ud83d\udca9 = /* WTF */ \"Pile of Poo\";\n

    Apr\u00e8s pr\u00e9traitement avec gcc -std=c99 -E -P on obtient le code suivant\u2009:

    int no\\U000000ebl[] = { 23, 42 } ;\nchar *\\U0001f4a9 = \"Pile of Poo\";\n

    Notons que dans le standard C23, les trigraphes ont \u00e9t\u00e9 supprim\u00e9s c'est pourquoi l'exemple ci-dessus ne fonctionne que si le flag -std=c99 est utilis\u00e9.

    ", "tags": ["_Pragma"]}, {"location": "course-c/15-fundations/preprocessor/#extensions-des-fichiers", "title": "Extensions des fichiers", "text": "

    Pour s'y retrouver, une convention existe sur les extensions des fichiers. Rappelons que selon POSIX un fichier n'a pas n\u00e9cessairement d'extension, c'est une convention libre \u00e0 l'utilisateur n\u00e9anmoins GCC utilise les extensions pour d\u00e9terminer le type de fichier. Selon le standard GNU, les extensions suivantes sont en vigueur\u2009:

    .h

    Fichier d'en-t\u00eate ne comportant que des d\u00e9finitions du pr\u00e9processeur, des d\u00e9clarations (structures, unions ...) et des prototypes de fonction, mais aucun code ex\u00e9cutable. Ce fichier sera soumis au pr\u00e9processeur s'il est inclus dans un fichier source.

    .c

    Fichier source C comportant les impl\u00e9mentations de fonctions et les variables globales. Ce fichier sera soumis au pr\u00e9processeur.

    .i

    Fichier source C qui a d\u00e9j\u00e0 \u00e9t\u00e9 pr\u00e9trait\u00e9 qui ne sera pas soumis au pr\u00e9processeur\u2009: gcc -E -o foo.i foo.c

    .s

    Fichier assembleur non soumis au pr\u00e9processeur.

    .S

    Fichier assembleur soumis au pr\u00e9processeur. Notons toutefois que cette convention n'est pas applicable sous Windows, car le syst\u00e8me de fichier n'est pas sensible \u00e0 la casse et un programme ne peut pas savoir si le fichier est .s ou .S (merci Windows pour toutes ces frustrations que tu me cr\u00e9es).

    .o

    Fichier objet g\u00e9n\u00e9r\u00e9 par le compilateur apr\u00e8s compilation

    .a

    Biblioth\u00e8que statique. Similaire \u00e0 un fichier .o mais peut contenir plusieurs fichiers objets.

    .so

    Biblioth\u00e8que dynamique. Fichier objet partag\u00e9.

    "}, {"location": "course-c/15-fundations/preprocessor/#inclusion-include", "title": "Inclusion (#include)", "text": "

    La directive #include permet d'incorporer le contenu d\u2019un fichier dans un autre de mani\u00e8re r\u00e9cursive, facilitant ainsi la modularisation et la lisibilit\u00e9 du code. Cette approche permet de structurer un programme en plusieurs fichiers, favorisant une meilleure organisation et maintenabilit\u00e9.

    Deux formes d'inclusion existent\u2009: locale et globale. C'est d\u2019ailleurs une question r\u00e9currente sur StackOverflow.

    Inclusion globale\u2009: #include <filename>

    Dans ce cas, le pr\u00e9processeur recherche le fichier \u00e0 inclure dans les chemins syst\u00e8me pr\u00e9d\u00e9finis (/usr/include, etc.), ainsi que dans ceux sp\u00e9cifi\u00e9s par les options -I du compilateur ou la variable d'environnement C_INCLUDE_PATH.

    Inclusion locale\u2009: #include \"filename\"

    Ici, la recherche d\u00e9bute dans le r\u00e9pertoire courant, puis se poursuit dans les chemins d\u00e9finis par les options -I et la variable C_INCLUDE_PATH.

    L\u2019inclusion de fichiers est un processus simple de concat\u00e9nation\u2009: le contenu du fichier inclus est copi\u00e9 \u00e0 l\u2019emplacement de la directive #include. Toutefois, cela peut mener \u00e0 des d\u00e9pendances cycliques, provoquant des inclusions infinies. Par exemple, si un fichier foo.h s'inclut lui-m\u00eame, le pr\u00e9processeur entrera dans une boucle sans fin, aboutissant \u00e0 une erreur apr\u00e8s un certain seuil\u2009:

    $ echo '#include \"foo.h\"' > foo.h\n$ gcc -E foo.h\nhead.h:1:18: error: #include nested depth 200 exceeds maximum of 200 (use\n-fmax-include-depth=DEPTH to increase the maximum)\n    1 | #include \"foo.h\"\n
    ", "tags": ["foo.h", "C_INCLUDE_PATH"]}, {"location": "course-c/15-fundations/preprocessor/#prevenir-les-inclusions-multiples", "title": "Pr\u00e9venir les inclusions multiples", "text": "

    Pour \u00e9viter ce genre de probl\u00e8me, il est courant d\u2019utiliser des include guards (ou header guards). Ce m\u00e9canisme consiste \u00e0 encapsuler le contenu d\u2019un fichier d\u2019en-t\u00eate avec une macro unique, garantissant ainsi que le fichier ne sera inclus qu'une seule fois au cours de la compilation. Ce proc\u00e9d\u00e9 est d\u00e9taill\u00e9 plus loin.

    "}, {"location": "course-c/15-fundations/preprocessor/#chevrons-ou-guillemets", "title": "Chevrons ou guillemets\u2009?", "text": "

    Une question souvent pos\u00e9e est de savoir si l'inclusion doit se faire avec des guillemets ou des chevrons. La r\u00e9ponse d\u00e9pend du contexte du projet. Pour les projets de petite envergure, les biblioth\u00e8ques standard sont incluses avec des chevrons, tandis que les fichiers locaux, souvent situ\u00e9s dans le m\u00eame r\u00e9pertoire que les fichiers sources, sont inclus via des chemins relatifs avec des guillemets. Dans le cas de projets plus cons\u00e9quents, o\u00f9 les fichiers sources et les en-t\u00eates sont souvent s\u00e9par\u00e9s, il peut \u00eatre inappropri\u00e9 d\u2019utiliser des chemins relatifs tels que #include \"../include/foo.h\". Une meilleure pratique consiste \u00e0 utiliser l\u2019option GCC -Iinclude/, qui permet d\u2019indiquer au compilateur o\u00f9 trouver les fichiers d\u2019en-t\u00eate. Bien qu'il soit techniquement possible d\u2019utiliser des chevrons dans ce cas, cela peut pr\u00eater \u00e0 confusion pour les lecteurs du code, qui pourraient penser qu\u2019il s\u2019agit d\u2019une biblioth\u00e8que standard.

    "}, {"location": "course-c/15-fundations/preprocessor/#ordre-des-inclusions", "title": "Ordre des inclusions", "text": "

    Il est recommand\u00e9 de respecter un ordre pr\u00e9cis lors de l'inclusion des fichiers d'en-t\u00eate, pour \u00e9viter des d\u00e9pendances implicites et des conflits potentiels\u2009:

    1. Les fichiers d'en-t\u00eate propres au projet.
    2. Les en-t\u00eates des biblioth\u00e8ques externes.
    3. Les en-t\u00eates de la biblioth\u00e8que standard.

    Ainsi, on pourrait avoir le code suivant\u2009:

    #include \"foo.h\"\n\n#include <SDL2/SDL.h>\n\n#include <stdio.h>\n#include <stdlib.h>\n

    Cet ordre garantit que les d\u00e9pendances des biblioth\u00e8ques externes ou standard ne soient pas r\u00e9solues par inadvertance gr\u00e2ce \u00e0 une inclusion locale. Pour mieux illustrer, consid\u00e9rons un fichier stack.h qui d\u00e9finit une structure de pile\u2009:

    #pragma once\n\nbool stack_push(int_least32_t value);\nbool stack_pop(int_least32_t *value);\n

    Ici, les types bool et int_least32_t ne sont pas d\u00e9finis dans ce fichier, car les inclusions de <stdbool.h> et <stdint.h> sont absentes. Si le fichier main.c les inclut avant stack.h :

    #include <stdbool.h>\n#include <stdint.h>\n#include <stdio.h>\n\n#include \"stack.h\"\n

    Aucune erreur ne sera g\u00e9n\u00e9r\u00e9e. Cependant, stack.h d\u00e9pend d'inclusions externes, ce qui est g\u00e9n\u00e9ralement \u00e0 \u00e9viter. En incluant les fichiers locaux en premier, on s'assure de leur autonomie.

    ", "tags": ["stack.h", "bool", "int_least32_t", "main.c"]}, {"location": "course-c/15-fundations/preprocessor/#macros-define", "title": "Macros (#define)", "text": "

    Les macros sont des symboles g\u00e9n\u00e9ralement \u00e9crits en majuscule et qui sont remplac\u00e9s par le pr\u00e9processeur. Ces d\u00e9finitions peuvent \u00eatre utiles pour d\u00e9finir des constantes globales qui sont d\u00e9finies \u00e0 la compilation et qui peuvent \u00eatre utilis\u00e9es comme des options de compilation. Par exemple, pour d\u00e9finir la taille d'une fen\u00eatre de filtrage\u2009:

    #ifndef WINDOW_SIZE\n#    define WINDOW_SIZE 10\n#endif\n\nint tab[WINDOW_SIZE];\n\nvoid init(void) {\n    for(size_t i = 0; i < WINDOW_SIZE; i++)\n        tab[i] = i;\n}\n

    L'ajout de la directive conditionnelle #ifndef permet d'autoriser la d\u00e9finition de la taille du tableau \u00e0 la compilation directement depuis GCC\u2009:

    $ gcc main.c -DWINDOW_SIZE=42\n

    Notons que durant le pr\u00e9processing toute occurrence d'un symbole d\u00e9fini est remplac\u00e9e par le contenu de sa d\u00e9finition. C'est un remplacement de cha\u00eene b\u00eate, idiot et na\u00eff. Il est par cons\u00e9quent possible d'\u00e9crire\u2009:

    #define MAIN int main(\n#define BEGIN ) {\n#define END return 0; }\n#define EOF \"\\n\"\n\nMAIN\nBEGIN\n    printf(\"Hello\" EOF);\nEND\n

    Par ailleurs, on rel\u00e8vera qu'il est aussi possible de commettre certaines erreurs en utilisant les d\u00e9finitions. Prenons l'exemple d'une macro d\u00e9clar\u00e9e sans parenth\u00e8ses\u2009:

    #define ADD a + b\n\nint a = 12;\nint b = 23;\nint c = ADD * ADD\n

    Apr\u00e8s pr\u00e9traitement le comportement ne sera pas celui attendu, car la multiplication devrait \u00eatre plus prioritaire que l'addition\u2009:

    int a = 12;\nint b = 23;\nint c = a + b * a + b\n

    Pour se pr\u00e9munir contre ces \u00e9ventuelles coquilles, on prot\u00e8gera toujours les d\u00e9finitions avec des parenth\u00e8ses

    #define ADD (a + b)\n#define PI (3.14159265358979323846)\n

    Espace n\u00e9cessaire

    Il est important de noter que l'espace entre le nom de la macro et les parenth\u00e8ses est strictement n\u00e9cessaire. En effet, si on \u00e9crit #define ADD(a + b), le pr\u00e9processeur g\u00e9n\u00e9rera une erreur car il consid\u00e8re que ADD est une macro.

    ", "tags": ["ADD"]}, {"location": "course-c/15-fundations/preprocessor/#linearisation", "title": "Lin\u00e9arisation", "text": "

    Le processus d'expansion des macros est une \u00e9tape cl\u00e9 dans la fa\u00e7on dont le pr\u00e9processeur C interpr\u00e8te et remplace les macros d\u00e9finies avec #define. Ce processus est appel\u00e9 lin\u00e9arisation, car les macros sont substitu\u00e9es avant la compilation en suivant une logique lin\u00e9aire et r\u00e9cursive. Prenons l'exemple de macros imbriqu\u00e9es\u2009:

    #define DOUBLE(x) (2 * (x))\n#define SQUARE(x) ((x) * (x))\n\nint main() {\n    int result = SQUARE(DOUBLE(3));\n}\n
    "}, {"location": "course-c/15-fundations/preprocessor/#macros-avec-arguments", "title": "Macros avec arguments", "text": "

    Une macro peut prendre des param\u00e8tres et permettre de g\u00e9n\u00e9rer du code \u00e0 la compilation par simple substitution de texte. Les macros sont souvent employ\u00e9es pour d\u00e9finir des fonctions simples qui ne n\u00e9cessitent pas de typage explicite. Par exemple, pour impl\u00e9menter une macro MIN qui retourne la valeur minimale entre deux arguments\u2009:

    #define MIN(x, y) ((x) < (y) ? (x) : (y))\n

    Le m\u00e9canisme de la macro repose uniquement sur un remplacement textuel\u2009:

    $ cat test.c\n#define MIN(x, y) ((x) < (y) ? (x) : (y))\nint main(void) { return MIN(23, 12); }\n\n$ gcc -E -P test.c -o-\nint main(void) { return ((23) < (12) ? (23) : (12)); }\n

    Notez que l'absence d'espace entre le nom de la macro et la parenth\u00e8se qui suit est cruciale. Une macro incorrectement d\u00e9finie, comme dans l'exemple ci-dessous, entra\u00eene un comportement inattendu\u2009:

    $ cat test.c\n#define ADD (x, y) ((x) + (y))\nint main(void) { return ADD(23, 12); }\n\n$ gcc -E -P test.c -o-\nint main(void) { return (x, y) ((x) + (y))(23, 12); }\n

    Une macro peut contenir plusieurs instructions, comme dans l'exemple suivant\u2009:

    #define ERROR(str) printf(\"Erreur: %s\\r\\n\", str); log(str);\n\nif (y < 0)\n    ERROR(\"Zero division\");\nelse\n    x = x / y;\n

    Dans cet exemple, l'absence d'accolades dans l'instruction if fait que seule la premi\u00e8re instruction est ex\u00e9cut\u00e9e lorsque la condition y < 0 est vraie. Or, la macro ERROR contient deux instructions distinctes, ce qui conduit \u00e0 un comportement incorrect.

    Une solution triviale consisterait \u00e0 encapsuler les instructions de la macro dans des accolades\u2009:

    #define ERROR(str) { printf(\"Erreur: %s\\r\\n\", str); log(str); }\n

    Cependant, cette approche pr\u00e9sente des probl\u00e8mes lorsqu\u2019elle est utilis\u00e9e dans des structures conditionnelles, comme un if-else. En effet, l'appel \u00e0 la macro est suivi d'un point-virgule, et un bloc d\u00e9limit\u00e9 par des accolades constitue une instruction compos\u00e9e (compound statement). L'ajout du point-virgule apr\u00e8s la macro provoque alors une terminaison anticip\u00e9e de l'instruction if, laissant le else sans correspondance\u2009:

    if (y < 0)\n    ERROR(\"Zero division\");\nelse\n    x = x / y;\n

    Pour contourner ce probl\u00e8me, il est pr\u00e9f\u00e9rable d'envelopper la macro dans une structure do { ... } while (0) afin de la transformer en une seule instruction indivisible, quel que soit le contexte d'utilisation\u2009:

    #define ERROR(str) do { \\\n    printf(\"Erreur: %s\\r\\n\", str); \\\n    log(str); \\\n} while (0)\n

    Cette construction cr\u00e9e une boucle vide qui s'ex\u00e9cute une seule fois, garantissant que la macro se comporte comme une instruction unique, m\u00eame si elle contient plusieurs lignes. De plus, l'utilisation de \\ permet d'\u00e9chapper le retour \u00e0 la ligne pour conserver une lisibilit\u00e9 optimale tout en faisant comprendre au pr\u00e9processeur que tout le bloc est une seule ligne logique.

    M\u00eame avec une bonne encapsulation des macros, certains pi\u00e8ges subsistent, notamment l'utilisation des post/pr\u00e9-incr\u00e9ments dans les arguments. Par exemple, consid\u00e9rons la macro ABS qui calcule la valeur absolue d\u2019un nombre\u2009:

    #define ABS(x) ((x) >= 0 ? (x) : -(x))\n\nreturn ABS(x++);\n

    Dans cet exemple, la variable x est post-incr\u00e9ment\u00e9e plusieurs fois, car l'argument de la macro est r\u00e9\u00e9valu\u00e9 \u00e0 chaque usage\u2009:

    return ((x++) >= 0 ? (x++) : -(x++));\n

    Ici, x est incr\u00e9ment\u00e9 trois fois au lieu d'une seule, ce qui peut entra\u00eener des comportements ind\u00e9sirables.

    Rappel des r\u00e8gles de bonnes pratiques\u2009:

    1. Prot\u00e9ger les param\u00e8tres de macro avec des parenth\u00e8ses pour garantir une \u00e9valuation correcte de l'expression.
    2. Encapsuler les macros \u00e0 plusieurs instructions dans une boucle vide pour \u00e9viter des erreurs dans les structures de contr\u00f4le comme if-else.
    3. \u00c9viter les post/pr\u00e9-incr\u00e9ments dans les macros, car ils peuvent provoquer des r\u00e9\u00e9valuations impr\u00e9vues et des erreurs difficiles \u00e0 d\u00e9tecter.

    Exercice 1\u2009: Macro compromise\u2009?

    Que retourne la fonction foo lors de son ex\u00e9cution avec le code suivant\u2009?

    #define ABS(x) x >= 0 ? x: -x\nint foo(void) { return ABS(5 - 8); }\n
    • 3
    • -3
    • -13
    • 5 - 8
    • 0

    ", "tags": ["else", "ERROR", "ABS", "foo", "MIN"]}, {"location": "course-c/15-fundations/preprocessor/#_static_assert", "title": "_Static_assert", "text": "

    Le mot-cl\u00e9 _Static_assert est une directive de pr\u00e9processeur introduite dans le standard C11. Elle permet de v\u00e9rifier des conditions \u00e0 la compilation. Contrairement \u00e0 #if, _Static_assert g\u00e9n\u00e8re une erreur de compilation si la condition n'est pas v\u00e9rifi\u00e9e. Par exemple, pour v\u00e9rifier que la taille d'un tableau est sup\u00e9rieure \u00e0 10\u2009:

    _Static_assert(sizeof(tab) > 10, \"La taille du tableau est inf\u00e9rieure \u00e0 10\");\n

    ", "tags": ["_Static_assert"]}, {"location": "course-c/15-fundations/preprocessor/#varadiques", "title": "Varadiques", "text": "

    Les macros varadiques permettent de d\u00e9finir des macros qui prennent un nombre variable d'arguments. Par exemple, pour d\u00e9finir une macro qui affiche un message avec un nombre variable d'arguments. Il s'agit d'une extension du standard C99. Pour cela on utilise l'op\u00e9rateur ... suivi de __VA_ARGS__ qui repr\u00e9sente les arguments pass\u00e9s \u00e0 la macro.

    C'est utilis\u00e9 par exemple pour cr\u00e9er des fonctions de d\u00e9bogage\u2009:

    #define DEBUG_LOG(level, fmt, ...) \\\n    printf(\"[%s] \" fmt \"\\n\", level, __VA_ARGS__)\n\nint main() {\n    DEBUG_LOG(\"\\033[32mINFO\\033[0m\", \"User %s logged in\", \"Alice\");\n    DEBUG_LOG(\"\\033[31mERROR\\033[0m\", \"Failed to open file %s\", \"data.txt\");\n}\n
    ", "tags": ["__VA_ARGS__"]}, {"location": "course-c/15-fundations/preprocessor/#terminologie", "title": "Terminologie", "text": "

    Que ce soit en anglais ou en fran\u00e7ais il n'est pas tr\u00e8s clair de comment nommer\u2009:

    1. #define FOO 42
    2. #define FOO(x) (x * x)

    Formellement les deux sont des d\u00e9finitions de pr\u00e9processeur mais \u00e9galement des macros. En informatique une macro est un modif de substitution de texte pouvant prendre des arguments.

    Dans un sens plus large la premi\u00e8re d\u00e9finition est souvent qualifi\u00e9e de constante symbolique bien que ce ne soit pas une vraie constante (const) elle rempli un r\u00f4le similaire.

    ", "tags": ["const"]}, {"location": "course-c/15-fundations/preprocessor/#directive-conditionnelle-if", "title": "Directive conditionnelle (#if)", "text": "

    La directive conditionnelle #if permet de tester des expressions \u00e0 la compilation. Elle est souvent utilis\u00e9e pour d\u00e9finir des options de compilation en fonction des besoins.

    Prenons l'exemple d'un programme qui g\u00e8re une grosse masse de messages. Ces derniers sont tri\u00e9s selon un crit\u00e8re sp\u00e9cifique et pour ce faire une fonction de tri est utilis\u00e9e. Nous verrons plus tard que le tri est un sujet complexe et qu'un crit\u00e8re du choix d'un algorithme de tri est la stabilit\u00e9. Un tri stable pr\u00e9serve l'ordre relatif des \u00e9l\u00e9ments qui ne sont pas distingu\u00e9s par le crit\u00e8re de tri. Par exemple, si on trie des personnes par leur \u00e2ge, un tri stable pr\u00e9servera l'ordre des personnes de m\u00eame \u00e2ge. Si la stabilit\u00e9 n'est pas un crit\u00e8re, un tri instable peut \u00eatre plus rapide. Pour cette raison, selon la n\u00e9cessit\u00e9 d'utilisation du programme, il peut \u00eatre pertinent de d\u00e9finir lors de la compilation s'il faut \u00eatre tr\u00e8s performant mais instable ou plus lent mais stable. Cela pourrait se faire avec\u2009:

    #ifdef STABLE_SORT\n#    define SORT merge_sort\n#else\n#    define SORT quick_sort\n#endif\n

    La configuration peut se faire soit directement dans le code source avec une directive pr\u00e9processeur\u2009:

    #define STABLE_SORT\n

    soit depuis la ligne de commande\u2009:

    $ gcc main.c -DSTABLE_SORT\n

    L'instruction #ifdef est un sucre syntaxique pour #if defined, autrement dit\u2009: si la d\u00e9finition est d\u00e9clar\u00e9e et quelque soit sa valeur. En effet, une d\u00e9claration sans valeur est tout \u00e0 fait possible. N\u00e9anmoins cela peut cr\u00e9er de la confusion. Penons l'exemple suivant pour lequel le message Dynamic allocation is allowed sera affich\u00e9 bien que la valeur de ALLOW_DYNAMIC_ALLOCATION soit 0 :

    #define ALLOW_DYNAMIC_ALLOCATION 0\n\n#if defined ALLOW_DYNAMIC_ALLOCATION\nprintf(\"Dynamic allocation is allowed\");\n#else\nprintf(\"Dynamic allocation is not allowed\");\n#endif\n

    Pour se pr\u00e9munir de ce genre de probl\u00e8me, il est recommand\u00e9 de toujours d\u00e9finir une valeur \u00e0 une d\u00e9claration et de tester si cette valeur est vraie\u2009:

    #define ALLOW_DYNAMIC_ALLOCATION 0\n\n#if ALLOW_DYNAMIC_ALLOCATION\n\u301c\n

    Une bonne pratique est soit de lever une erreur si la valeur n'est pas d\u00e9finie, soit de d\u00e9finir une valeur par d\u00e9faut\u2009:

    #define YES 1\n#define NO 0\n\n#define STRICT YES\n#define ALLOW_DYNAMIC_ALLOCATION YES\n\n#ifndef ALLOW_DYNAMIC_ALLOCATION\n#    if defined STRICT && STRICT == YES\n#        error \"ALLOW_DYNAMIC_ALLOCATION is not defined\"\n#    else\n#        define ALLOW_DYNAMIC_ALLOCATION NO\n#    endif\n#endif\n

    Par analogie \u00e0 l'instruction if, il est possible d'utiliser #else (else)et #elif (else if) pour d\u00e9finir des alternatives\u2009:

    #define MODE_UPPERCASE 0\n#define MODE_LOWERCASE 1\n#define MODE_ARABIC 2\n\n#ifndef DISPLAY_MODE\n#   define DISPLAY_MODE MODE_UPPERCASE\n#endif\n\nvoid display(char value) {\n#if DISPLAY_MODE == MODE_UPPERCASE\n    printf(\"%c\", 'A' + (value % 26));\n#elif DISPLAY_MODE == MODE_LOWERCASE\n    printf(\"%c\", 'a' + (value % 26));\n#elif DISPLAY_MODE == MODE_ARABIC\n    printf(\"%d\", value);\n#else\n#   error \"DISPLAY_MODE is not defined\"\n#endif\n}\n

    ", "tags": ["else"]}, {"location": "course-c/15-fundations/preprocessor/#suppression-undef", "title": "Suppression (#undef)", "text": "

    Un symbole d\u00e9fini soit par la ligne de commande -DFOO=1, soit par la directive #define FOO 1 ne peut pas \u00eatre red\u00e9fini\u2009:

    $ cat test.c\n#define ANSWER 42\n#define ANSWER 23\n$ gcc -E test.c\ntest.c:2: warning: \"ANSWER\" redefined\n    2 | #define ANSWER 23\n      |\ntest.c:1: note: this is the location of the previous definition\n    1 | #define ANSWER 42\n      |\n

    C'est pourquoi il peut \u00eatre utile d'utiliser #undef pour supprimer une directive pr\u00e9processeur\u2009:

    #ifdef FOO\n#   undef FOO\n#endif\n#define FOO 1\n

    L'utilisation de #undef dans un programme est tout \u00e0 fait l\u00e9gitime dans certains cas, mais elle doit \u00eatre manipul\u00e9e avec pr\u00e9caution.

    On peut citer par exemple la fameuse constante M_PI qui n'est pas d\u00e9finie par le standard C mais que la plupart des compilateurs d\u00e9finissent. Si vous avez besoin d'utiliser la valeur \\(\\pi\\) dans votre programme, vous pouvez soit utiliser par d\u00e9faut la valeur d\u00e9finie par la biblioth\u00e8que si elle existe sinon la v\u00f4tre\u2009:

    #ifndef M_PI\n#    define M_PI 3.14159265358979323846\n#endif\n

    Ou alors, vous pourriez forcer la valeur de \\(\\pi\\) \u00e0 celle que vous souhaitez\u2009:

    #undef M_PI\n#define M_PI 3.14159265358979323846\n

    ", "tags": ["M_PI"]}, {"location": "course-c/15-fundations/preprocessor/#erreur-error", "title": "Erreur (#error)", "text": "

    La directive #error g\u00e9n\u00e8re une erreur avec le texte qui suit la directive et arr\u00eate la compilation. Elle est souvent utilis\u00e9e pour s'assurer que certaines conditions sont remplies avant de compiler le code. Par exemple, pour s'assurer que la taille du noyau d'un filtre est impair\u2009:

    #if !(KERNEL_SIZE % 2)\n#    error Le noyau du filtre est pair\n#endif\n

    On peut l'utiliser \u00e9galement pour s'assurer que le compilateur est compatible avec une version sp\u00e9cifique du standard C\u2009:

    #if __STDC_VERSION__ < 201112L\n    #error \"C11 support required\"\n#endif\n

    Warning

    Certains compilateurs comme GCC ou Clang permettent d'utiliser #warning pour g\u00e9n\u00e9rer un avertissement, bien que cette directive ne soit pas standard.

    #warning \"This is a warning\"\n
    "}, {"location": "course-c/15-fundations/preprocessor/#macros-predefinies", "title": "Macros pr\u00e9d\u00e9finies", "text": "

    Le standard d\u00e9finit certains symboles utiles pour le d\u00e9bogage\u2009:

    __LINE__

    Est remplac\u00e9 par le num\u00e9ro de la ligne sur laquelle est plac\u00e9 ce symbole

    __FILE__

    Est remplac\u00e9 par le nom du fichier sur lequel est plac\u00e9 ce symbole

    __func__

    Est remplac\u00e9 par le nom de la fonction du bloc dans lequel la directive se trouve

    __STDC__

    Est remplac\u00e9 par 1 pour indiquer que l'impl\u00e9mentation est compatible avec C90

    __STDC_VERSION__

    Est remplac\u00e9 par la version du standard C utilis\u00e9e par le compilateur, il s'agit d'un entier de la forme AAAAMM o\u00f9 AAAA est l'ann\u00e9e et MM le mois. Par exemple pour C11, la valeur est 201112L

    __DATE__

    Est remplac\u00e9 par la date sous la forme \"Mmm dd yyyy\"

    __TIME__

    Est remplac\u00e9 par l'heure au moment du pre-processing \"hh:mm:ss\"

    __COUNTER__

    Est remplac\u00e9 par un entier qui s'incr\u00e9mente \u00e0 chaque fois qu'il est utilis\u00e9. C'est une directive non standard mais disponible dans GCC et Clang.

    On peut par exemple cr\u00e9er une macro pour afficher des messages de d'erreur avec le nom du fichier le num\u00e9ro de ligne et la date de compilation\u2009:

    #define ERROR(msg) fprintf(stderr, \"%s:%d %s %s\\n\", \\\n    __FILE__, __LINE__, __DATE__, msg)\n
    ", "tags": ["__DATE__", "__STDC__", "__LINE__", "__COUNTER__", "AAAAMM", "__func__", "__FILE__", "AAAA", "__TIME__"]}, {"location": "course-c/15-fundations/preprocessor/#caractere-dechappement", "title": "Caract\u00e8re d'\u00e9chappement", "text": "

    La plupart des instructions pr\u00e9processeur sont des instructions de ligne, c'est-\u00e0-dire qu'elles se terminent par un saut de ligne or parfois (notamment pour les macros), on souhaiterait \u00e9crire une instruction sur plusieurs lignes.

    L'anti-slash (backslash) suivi directement d'un retour \u00e0 la ligne est interpr\u00e9t\u00e9 par le pr\u00e9processeur comme un saut de ligne virtuel. Il permet par exemple de casser les longues lignes\u2009:

    #define TRACE printf(\"Le programme est pass\u00e9 \" \\\n    \" dans le fichier %s\" \\\n    \" ligne %d\\n\", \\\n    __FILE__, __LINE__);\n

    ", "tags": ["backslash"]}, {"location": "course-c/15-fundations/preprocessor/#directive-de-ligne", "title": "Directive de ligne", "text": "

    La directive #line permet de modifier le num\u00e9ro de ligne et le nom du fichier pour les directives de d\u00e9bogage. En pratique elle est peu utilis\u00e9e, car les compilateurs modernes g\u00e8rent correctement les num\u00e9ros de ligne et les noms de fichiers.

    #line 42 \"foo.c\"\n

    "}, {"location": "course-c/15-fundations/preprocessor/#concatenation-de-chaines", "title": "Concat\u00e9nation de cha\u00eenes", "text": "

    Parfois il est utile de vouloir concat\u00e9ner deux symboles comme si ce n'\u00e9tait qu'un seul. L'op\u00e9rateur de concat\u00e9nation ## permet de concat\u00e9ner deux arguments dans une macro et de les combiner en un seul token. Cet op\u00e9rateur est particuli\u00e8rement utilis\u00e9 dans des macros avanc\u00e9es pour g\u00e9n\u00e9rer du code automatiquement \u00e0 partir d'une combinaison d'arguments.

    Un cas d'usage typique est la cr\u00e9ation de noms dynamiques.

    #define CONCAT(a, b) a##b\n\nint main() {\n    int var1 = 10, var2 = 20;\n\n    CONCAT(var, 1) = 30;  // devient var1 = 30\n    CONCAT(var, 2) = 40;  // devient var2 = 40\n\n    printf(\"%d, %d\\n\", var1, var2);  // 30, 40\n}\n

    Cela peut \u00eatre utile par exemple pour g\u00e9rer des traductions\u2009:

    #define LANGUAGE FR\n\n#define CONCAT(a, b) a##b\n\nvoid greet_FR(void) { printf(\"Bonjour\\n\"); }\nvoid greet_EN(void) { printf(\"Hello\\n\"); }\n\n#define GREET CONCAT(greet_, LANGUAGE)\n\nint main() {\n    GREET();\n}\n

    Un autre usage courant est de d\u00e9finir le mangling des noms de fonctions pour une biblioth\u00e8que. Par exemple, si vous avez une biblioth\u00e8que qui d\u00e9finit une fonction foo et que vous voulez \u00e9viter les conflits de noms, vous pourriez d\u00e9finir une macro pour ajouter un pr\u00e9fixe et un suffixe\u2009:

    #define MANGLE(name) prefix_ ## name ## _suffix\n\nvoid MANGLE(foo)(void) {\n    printf(\"Hello\");\n}\n\nint main() {\n    MANGLE(foo)();\n}\n

    Rappelez-vous que le langage C ne permet pas de d\u00e9finir des fonctions avec le m\u00eame nom m\u00eame si elles ont des signatures diff\u00e9rentes et m\u00eame si elles sont dans deux fichiers s\u00e9par\u00e9s. Un param\u00e8tre de configuration MANGLE permettrait de sp\u00e9cifier \u00e0 la compilation d'une biblioth\u00e8que le pr\u00e9fixe \u00e0 ajouter \u00e0 toutes les fonctions de la biblioth\u00e8que.

    ", "tags": ["MANGLE", "foo"]}, {"location": "course-c/15-fundations/preprocessor/#conversion-en-chaine", "title": "Conversion en cha\u00eene", "text": "

    Il est possible de convertir un symbole en cha\u00eene de caract\u00e8res avec l'op\u00e9rateur # :

    #define STRINGIFY(x) #x\nprintf(STRINGIFY(42));\n

    Le plus souvent cet op\u00e9rateur est utilis\u00e9 pour traiter des messages d'erreurs\u2009:

    #define WARN_IF_NEGATIVE(x) \\\n    if (x < 0) { \\\n        printf(\"Warning: the value of \" #x \" is negative (%d)\\n\", x); \\\n    }\n\nint main() {\n    int value = -5;\n    WARN_IF_NEGATIVE(value);\n}\n

    Ou par exemple pour afficher la valeur d'une variable\u2009:

    #define PRINT_VAR(var) printf(#var \" = %d\\n\", var)\n\nint main() {\n    int a = 10;\n    PRINT_VAR(a);\n}\n
    "}, {"location": "course-c/15-fundations/preprocessor/#desactivation-de-code", "title": "D\u00e9sactivation de code", "text": "

    Je vois trop souvent des d\u00e9veloppeurs commenter des sections de code pour le d\u00e9bogage. Cette pratique n'est pas recommand\u00e9e, car les outils de refactoring (r\u00e9usinage de code), ne parviendront pas \u00e0 interpr\u00e9ter le code en commentaire jugeant qu'il ne s'agit pas de code, mais de texte insignifiant. Une m\u00e9thode plus robuste et plus sure consiste \u00e0 utiliser une directive conditionnelle\u2009:

    #if 0 // TODO: Check if this code is still required.\nif (x < 0) {\n    x = 0;\n}\n#endif\n

    "}, {"location": "course-c/15-fundations/preprocessor/#include-guard", "title": "Include guard", "text": "

    Comme \u00e9voqu\u00e9 plus haut, les fichiers d'en-t\u00eate peuvent \u00eatre inclus plusieurs fois dans un programme ce qui peut poser des probl\u00e8mes de red\u00e9finition de symboles ou d'erreurs de d\u00e9pendences cycliques. En informatique on parle volontiers d'idempotence pour d\u00e9signer une op\u00e9ration qui peut \u00eatre appliqu\u00e9e plusieurs fois sans changer le r\u00e9sultat au-del\u00e0 de la premi\u00e8re application. Inclure une ou plusieurs fois un m\u00eame fichier d'en-t\u00eate ne doit pas changer le r\u00e9sultat de la compilation.

    Pour ce faire, on utilise des garde-fous (guards) pour prot\u00e9ger les fichiers d'en-t\u00eate. Imaginons que la constante M_PI soit d\u00e9finie dans le header <math.h>:

    #define M_PI  3.14159265358979323846\n

    Si ce fichier d'en-t\u00eate est inclus \u00e0 nouveau, le pr\u00e9processeur g\u00e9n\u00e9rera une erreur, car le symbole est d\u00e9j\u00e0 d\u00e9fini. Pour \u00e9viter ce genre d'erreur, les fichiers d'en-t\u00eate sont prot\u00e9g\u00e9s par un garde\u2009:

    #ifndef MATH_H\n#define MATH_H\n\n...\n\n#endif // MATH_H\n

    Si le fichier a d\u00e9j\u00e0 \u00e9t\u00e9 inclus, la d\u00e9finition MATH_H sera d\u00e9j\u00e0 d\u00e9clar\u00e9e et le fichier d'en-t\u00eate ne sera pas r\u00e9-inclus.

    Le consensus veut que le nom du garde soit le nom du fichier en majuscule avec des underscores \u00e0 la place des points et des tirets. Par exemple, pour le fichier foo/bar.h on utilisera FOO_BAR_H. On ajoutera \u00e9galement un commentaire pour indiquer la fin du garde. J'ai personnellement un avis assez d\u00e9favorable sur cette pratique, car elle engeandre un probl\u00e8me de source de v\u00e9rit\u00e9. En effet, si le nom du fichier change, il faudra \u00e9galement changer le nom du garde et je peux vous garantir que bien souvent l'un est r\u00e9alis\u00e9 sans l'autre. Cela pose \u00e9galement un probl\u00e8me de suivi de modifications sous Git.

    Alternativement une solution est d'avoir une cha\u00eene de caract\u00e8re unique pour chaque fichier d'en-t\u00eate. Cela peut \u00eatre r\u00e9alis\u00e9 en s'\u00e9nervant sur le clavier. Il y a peu de chance de retrouver une telle cha\u00eene dans un autre fichier. N\u00e9anmoins, il est aussi possible de g\u00e9n\u00e9rer une telle cha\u00eene de mani\u00e8re automatique avec un outil comme uuidgen ou openssl rand -hex 16.

    #ifndef FJHJFDKLHSKIOUZEZEUWEHDLKSH\n#define FJHJFDKLHSKIOUZEZEUWEHDLKSH\n\n#endif // FJHJFDKLHSKIOUZEZEUWEHDLKSH\n

    On pr\u00e9f\u00e8rera utiliser la directive #pragma once qui est plus simple \u00e0 l'usage et \u00e9vite une collision de nom. N\u00e9anmoins et bien que cette directive ne soit pas standardis\u00e9e par l'ISO, elle est compatible avec la tr\u00e8s grande majorit\u00e9 des compilateurs C.

    #pragma once\n\n...\n

    ", "tags": ["FOO_BAR_H", "M_PI", "MATH_H", "uuidgen"]}, {"location": "course-c/15-fundations/preprocessor/#pragmas-pragma", "title": "Pragmas (#pragma)", "text": "

    Le terme pragma est une abr\u00e9viation de pragmatic information (information pragmatique). Les pragmas sont des instructions sp\u00e9cifiques \u00e0 un compilateur qui permettent de contr\u00f4ler le comportement du compilateur. Ils sont souvent utilis\u00e9s pour d\u00e9sactiver des avertissements, sp\u00e9cifier l'alignement m\u00e9moire ou encore pour optimiser le code. La directive #pragma permet donc de passer des options sp\u00e9cifiques au compilateur, elle n'est par cons\u00e9quent pas standardis\u00e9e.

    Une utilisation possible avec GCC serait forcer la d\u00e9sactivation d'un avertissement\u2009:

    #pragma GCC diagnostic ignored \"-Wformat\"\n

    On utilise \u00e9galement un #pragma pour forcer l'alignement m\u00e9moire d'une structure\u2009:

    #pragma pack(push, 1)\ntypedef struct {\n    char a;\n    int b;\n} MyStruct;\n#pragma pack(pop)\n

    Comme les pragmas ne sont pas standardis\u00e9es, il est recommand\u00e9 de les utiliser avec parcimonie et de les documenter correctement.

    Notons que si l'on souhaite d\u00e9finir un pragma au sein d'une directive pr\u00e9processeur tel qu'une macro, il faudra utiliser l'op\u00e9rateur unaire _Pragma :

    #define PRAGMA(x) _Pragma(#x)\n

    ", "tags": ["_Pragma"]}, {"location": "course-c/15-fundations/preprocessor/#simulation-dexceptions", "title": "Simulation d'exceptions", "text": "

    Dans des langages de plus haut niveau comme le C++, le Python ou le Java, il existe un m\u00e9canisme nomm\u00e9 exception qui permet de g\u00e9rer des erreurs plus efficacement. Au lieu de retourner une valeur d'erreur, on l\u00e8ve une exception qui sera attrap\u00e9e plus haut dans la cha\u00eene d'appel.

    En C il n'existe pas de m\u00e9canisme d'exception, mais il est possible de simuler ce comportement avec des macros et l'instruction setjmp et longjmp de la librairie standard. setjmp permet de sauvegarder l'\u00e9tat du programme \u00e0 un endroit donn\u00e9 et longjmp permet de revenir \u00e0 cet endroit en sautant les appels de fonctions interm\u00e9diaires. Il faut voir longjmp comme un goto encore plus dangereux.

    Cette utilisation n'est pas recommend\u00e9e car elle peut rendre le code plus obscure et plus difficile \u00e0 maintenir. N\u00e9anmoins dans certains cas de figure, notament pour des programmes embarqu\u00e9s, cette technique peut se r\u00e9v\u00e9ler tr\u00e8s utile.

    On d\u00e9finit tout d'abord les macros suivantes dans un fichier exception.h :

    #pragma once\n#include <setjmp.h>\n\n#define TRY do { jmp_buf ex_buf__; if (setjmp(ex_buf__) == 0) {\n#define CATCH } else {\n#define ETRY } } while (0)\n#define THROW longjmp(ex_buf__, 1)\n

    On peut ensuite utiliser ces macros pour g\u00e9rer des erreurs\u2009:

    #include \"exception.h\"\n\nvoid qux(void) {\n    printf(\"qux\\n\");\n    THROW; // Simulate an error !\n}\n\nvoid bar(void) { printf(\"bar\\n\"); qux(); }\nvoid foo(void) { printf(\"foo\\n\"); bar(); }\n\nint main(void) {\n    TRY {\n        foo();\n    } CATCH {\n        printf(\"An error occured\\n\");\n    } ETRY;\n}\n

    Dans cet exemple, si la fonction qux l\u00e8ve une exception, le programme sautera \u00e0 la ligne CATCH et affichera An error occured en court-circuitant les appels de fonctions interm\u00e9diaires.

    Notez que cette technique est tr\u00e8s dangereuse dans le cas de programmes utilisant l'allocation dynamique de m\u00e9moire. En effet, si une exception est lev\u00e9e alors que de la m\u00e9moire a \u00e9t\u00e9 allou\u00e9e, il y aura des fuites m\u00e9moires.

    ", "tags": ["CATCH", "goto", "setjmp", "qux", "exception.h", "longjmp"]}, {"location": "course-c/15-fundations/scope/", "title": "Port\u00e9e et visibilit\u00e9", "text": "

    Ce chapitre se concentre sur quatre caract\u00e9ristiques d'une variable\u2009:

    • La port\u00e9e
    • La visibilit\u00e9
    • La dur\u00e9e de vie
    • Son qualificatif de type

    Dans les quatre cas, elles d\u00e9crivent l'accessibilit\u00e9, c'est \u00e0 dire jusqu'\u00e0 ou jusqu'\u00e0 quand une variable est accessible, et de quelle mani\u00e8re

    Brouillard matinal sur le Golden Gate Bridge, San Francisco

    "}, {"location": "course-c/15-fundations/scope/#espace-de-nommage", "title": "Espace de nommage", "text": "

    L'espace de nommage ou namespace est un concept diff\u00e9rent de celui existant dans d'autres langages tel que C++. Le standard C99 d\u00e9crit 4 types possibles pour un identifiant\u2009:

    • fonction et labels
    • noms de structures (struct), d'unions (union), d'\u00e9num\u00e9ration (enum),
    • identifiants
    ", "tags": ["namespace", "struct", "union", "enum"]}, {"location": "course-c/15-fundations/scope/#portee", "title": "Port\u00e9e", "text": "

    La port\u00e9e ou scope d\u00e9crit jusqu'\u00e0 o\u00f9 une variable est accessible.

    Une variable est globale, c'est-\u00e0-dire accessible partout, si elle est d\u00e9clar\u00e9e en dehors d'une fonction\u2009:

    int global_variable = 23;\n

    Une variable est locale si elle est d\u00e9clar\u00e9e \u00e0 l'int\u00e9rieur d'un bloc, ou \u00e0 l'int\u00e9rieur d'une fonction. Elle sera ainsi visible de sa d\u00e9claration jusqu'\u00e0 la fin du bloc courant\u2009:

    int main(int)\n{\n    {\n        int i = 12;\n        i += 2; // Valide\n    }\n    i++; // Invalide, `i` n'est plus visible.\n}\n
    "}, {"location": "course-c/15-fundations/scope/#variable-shadowing", "title": "Variable shadowing", "text": "

    On dit qu'une variable est shadowed ou masqu\u00e9e si sa d\u00e9claration masque une variable pr\u00e9alablement d\u00e9clar\u00e9e\u2009:

    int i = 23;\n\nfor(size_t i = 0; i < 10; i++) {\n    printf(\"%ld\", i); // Acc\u00e8s \u00e0 `i` courant et non \u00e0 `i = 23`\n}\n\nprintf(\"%d\", i); // Acc\u00e8s \u00e0 `i = 23`\n
    "}, {"location": "course-c/15-fundations/scope/#visibilite", "title": "Visibilit\u00e9", "text": "

    Selon l'endroit o\u00f9 est d\u00e9clar\u00e9e une variable, elle ne sera pas n\u00e9cessairement visible partout ailleurs. Une variable locale n'est accessible qu'\u00e0 partir de sa d\u00e9claration et jusqu'\u00e0 la fin du bloc dans laquelle elle est d\u00e9clar\u00e9e.

    L'exemple suivant montre la visibilit\u00e9 de plusieurs variables\u2009:

                      //   a\nvoid foo(int a) { //   \u252c b\n    int b;        //   \u2502 \u252c\n    ...           //   \u2502 \u2502\n    {             //   \u2502 \u2502 c\n       int c;     //   \u2502 \u2502 \u252c\n       ...        //   \u2502 \u2502 \u2502 d\n       int d;     //   \u2502 \u2502 \u2502 \u252c\n       ...        //   \u2502 \u2502 \u2502 \u2502\n    }             //   \u2502 \u2502 \u2534 \u2534\n    ...           //   \u2502 \u2502\n}                 //   \u2534 \u2534\n

    Une variable d\u00e9clar\u00e9e globalement, c'est \u00e0 dire en dehors d'une fonction \u00e0 une dur\u00e9e de vie sur l'entier du module (translation unit) quel que soit l'endroit o\u00f9 elle est d\u00e9clar\u00e9e, en revanche elle n'est visible que depuis l'endroit ou elle est d\u00e9clar\u00e9e. Les deux variables i et j sont globales au module, c'est-\u00e0-dire qu'elles peuvent \u00eatre acc\u00e9d\u00e9es depuis n'importe quelle fonction contenue dans ce module.

    En revanche la variable j, bien qu'elle ait ait une dur\u00e9e de vie sur toute l'ex\u00e9cution du programme et que sa port\u00e9e est globale, elle ne pourra \u00eatre acc\u00e9d\u00e9e depuis, main car elle n'est pas visible.

    #include <stdio.h>\n                             //  i\nint i;                       //  \u252c\n                             //  \u2502\nint main() {                 //  \u2502\n    printf(\"%d %d\\n\", i, j); //  \u2502\n}                            //  \u2502\n                             //  \u2502 j\nint j;                       //  \u2534 \u252c\n

    Le mot cl\u00e9 extern permet non pas de d\u00e9clarer la variable, j mais de renseigner le compilateur qu'il existe ailleurs une variable j. C'est ce que l'on appelle une d\u00e9claration avanc\u00e9e ou forward-declaration. Dans ce cas, bien que j soit d\u00e9clar\u00e9e apr\u00e8s la fonction principale, elle est maintenant visible.

    #include <stdio.h>\n\n                        // j\nextern int j;           // \u252c   D\u00e9claration en amont de `j`\n                        // \u2502\nint main() {            // \u2502\n    printf(\"%d\\n\", j);  // \u2502\n}                       // \u2502\n                        // \u2502\nint j;                  // \u2502\n                        // \u2502\n

    Une particularit\u00e9 en C est que tout symbole global (variable ou fonction) a une accessibilit\u00e9 transversale. C'est-\u00e0-dire que dans le cas de la compilation s\u00e9par\u00e9e, une variable d\u00e9clar\u00e9e dans un fichier, peut \u00eatre acc\u00e9d\u00e9e depuis un autre fichier, il en va de m\u00eame pour les fonctions.

    L'exemple suivant implique deux fichiers foo.c et main.c. Dans l'un deux symboles sont d\u00e9clar\u00e9s, une variable et une fonction.

    foo.c
    int foo;\n\nvoid do_foo() {\n    printf(\"Foo does...\");\n}\n

    Depuis le programme principal, il est possible d'acc\u00e9der \u00e0 des symboles \u00e0 condition de renseigner sur le prototype de la fonction et l'existence de la variable\u2009:

    main.c
    extern int foo;\nextern void do_foo(); // Non obligatoire\n\nint main() {\n    foo();\n}\n

    Dans le cas o\u00f9 l'on voudrait restreindre l'accessibilit\u00e9 d'une variable au module dans lequel elle est d\u00e9clar\u00e9e, l'usage du mot cl\u00e9 static s'impose.

    En \u00e9crivant static int foo; dans foo.c, la variable n'est plus accessible en dehors du module m\u00eame avec une d\u00e9claration en avance. On dit que sa port\u00e9e est r\u00e9duite au module.

    ", "tags": ["extern", "main.c", "static", "main", "foo.c"]}, {"location": "course-c/15-fundations/scope/#qualificatif-de-type", "title": "Qualificatif de type", "text": "

    Les variables en C peuvent \u00eatre cr\u00e9\u00e9es de diff\u00e9rentes mani\u00e8res. Selon la mani\u00e8re dont elles pourront \u00eatre utilis\u00e9es, il est courant de les classer en cat\u00e9gories.

    Une classe de stockage peut \u00eatre implicite \u00e0 une d\u00e9claration de variable ou explicite, en ajoutant un attribut devant la d\u00e9claration de celle-ci.

    "}, {"location": "course-c/15-fundations/scope/#auto", "title": "auto", "text": "

    Cette classe est utilis\u00e9e par d\u00e9faut lorsqu'aucune autre classe n'est pr\u00e9cis\u00e9e. Les variables automatiques sont visibles uniquement dans le bloc o\u00f9 elles sont d\u00e9clar\u00e9es. Ces variables sont habituellement cr\u00e9\u00e9es sur la pile (stack), mais peuvent \u00eatre aussi stock\u00e9es dans les registres du processeur. C'est un choix qui incombe au compilateur.

    auto type identificateur = valeur_initiale;\n

    Pour les variables automatiques, le mot-cl\u00e9 auto n'est pas obligatoire, et n'est pas recommand\u00e9 en C99, car son utilisation est implicite.

    ", "tags": ["auto"]}, {"location": "course-c/15-fundations/scope/#register", "title": "register", "text": "

    Ce mot cl\u00e9 incite le compilateur \u00e0 utiliser un registre processeur pour stocker la variable. Ceci permet de gagner en temps d'ex\u00e9cution, car la variable n'a pas besoin d'\u00eatre charg\u00e9e depuis et \u00e9crite vers la m\u00e9moire.

    Jadis, ce mot cl\u00e9 \u00e9tait utilis\u00e9 devant toutes les variables d'it\u00e9rations de boucles. La traditionnelle variable i utilis\u00e9e dans les boucles for \u00e9tait d\u00e9clar\u00e9e register int i = 0;. Les compilateurs modernes savent aujourd'hui identifier les variables les plus souvent utilis\u00e9es. L'usage de ce mot cl\u00e9 n'est donc plus recommand\u00e9 depuis C99.

    ", "tags": ["for", "register"]}, {"location": "course-c/15-fundations/scope/#const", "title": "const", "text": "

    Ce mot cl\u00e9 rend une d\u00e9claration non modifiable par le programme lui-m\u00eame. N\u00e9anmoins il ne s'agit pas de constantes au sens strict du terme, car une variable de type const pourrait tr\u00e8s bien \u00eatre modifi\u00e9e par erreur en jardinant la m\u00e9moire. Quand ce mot cl\u00e9 est appliqu\u00e9 \u00e0 une structure, aucun des champs de la structure n'est accessible en \u00e9criture. Bien qu'il puisse para\u00eetre \u00e9trange de vouloir rendre \u00ab constante \u00bb une \u00ab variable \u00bb, ce mot cl\u00e9 a une utilit\u00e9. En particulier, il permet de faire du code plus s\u00fbr.

    ", "tags": ["const"]}, {"location": "course-c/15-fundations/scope/#static", "title": "static", "text": "

    Elle permet de d\u00e9clarer des variables dont le contenu est pr\u00e9serv\u00e9 m\u00eame lorsque l'on sort du bloc o\u00f9 elles ont \u00e9t\u00e9 d\u00e9clar\u00e9es.

    Elles ne sont donc initialis\u00e9es qu'une seule fois. L'exemple suivant est une fonction qui retourne \u00e0 chaque fois une valeur diff\u00e9rente, incr\u00e9ment\u00e9e de 1. La variable i agit ici comme une variable globale, elle n'est initialis\u00e9e qu'une seule fois \u00e0 0 et donc s'incr\u00e9mente d'appel en appel. En revanche, elle n'est pas accessible en dehors de la fonction\u2009; c'est donc une variable locale.

    int iterate() {\n    static int i = 0;\n    return i++;\n}\n

    Il n'est pas rare de voir des variables globales, ou des fonctions pr\u00e9c\u00e9d\u00e9es du mot cl\u00e9 static. Ces variables sont dites statiques au module. Elles ne sont donc pas accessibles depuis un autre module (translation unit)

    La fonction suivante est statique au module dans lequel elle est d\u00e9clar\u00e9e. Il ne sera donc pas possible d'y acc\u00e9der depuis un autre fichier C.

    static int add(int a, int b) { return a + b; }\n

    On utilisera volontiers le mot cl\u00e9 static pour marquer les fonctions qui ne sont pas exportables, c'est-\u00e0-dire qui ne sont pas destin\u00e9es \u00e0 \u00eatre utilis\u00e9es par d'autres modules. Il s'agit des fonctions utlitaires, des fonctions internes \u00e0 un module. Il en va de m\u00eame pour les variables. On \u00e9vitera dans tous les cas d'utiliser des variables globales mais si cela est n\u00e9cessaire, on les marquera static pour \u00e9viter qu'elle ne soient accessibles depuis un autre fichier.

    ", "tags": ["static"]}, {"location": "course-c/15-fundations/scope/#volatile", "title": "volatile", "text": "

    Cette classe de stockage indique au compilateur qu'il ne peut faire aucune hypoth\u00e8se d'optimisation concernant cette variable. Elle indique que son contenu peut \u00eatre modifi\u00e9 en tout temps en arri\u00e8re-plan par le syst\u00e8me d'exploitation ou le mat\u00e9riel. Ce mot cl\u00e9 est davantage utilis\u00e9 en programmation syst\u00e8me, ou sur microcontr\u00f4leurs.

    L'usage de cette classe de stockage r\u00e9duit les performances d'un programme puisqu'elle emp\u00eache l'optimisation du code et le contenu de cette variable devra \u00eatre recharg\u00e9 \u00e0 chaque utilisation

    Consid\u00e9rons le cas du programme suivant\u2009:

    #include <stdio.h>\n\nint main() {\n    int i = 0;\n\n    i = 1;\n    i = 0;\n    i = 1;\n    i = 0;\n\n    printf(\"%d\", i);\n}\n

    On notera que les 4 lignes o\u00f9 i successivement assign\u00e9 \u00e0 1 et 0 sont inutiles, car dans tous les cas, la valeur 0 sera affich\u00e9e. Si le programme est compil\u00e9, on obtient le listing suivant\u2009:

    $ gcc main.c\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001149 <main>:\n    1149:       f3 0f 1e fa             endbr64\n    114d:       55                      push   %rbp\n    114e:       48 89 e5                mov    %rsp,%rbp\n    1151:       48 83 ec 10             sub    $0x10,%rsp\n    1155:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    115c:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)\n    1163:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    116a:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)\n    1171:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)\n    1178:       8b 45 fc                mov    -0x4(%rbp),%eax\n    117b:       89 c6                   mov    %eax,%esi\n    117d:       48 8d 3d 80 0e 00 00    lea    0xe80(%rip),%rdi\n    1184:       b8 00 00 00 00          mov    $0x0,%eax\n    1189:       e8 c2 fe ff ff          callq  1050 <printf@plt>\n    118e:       b8 00 00 00 00          mov    $0x0,%eax\n    1193:       c9                      leaveq\n    1194:       c3                      retq\n    1195:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    119c:       00 00 00\n    119f:       90                      nop\n

    Les lignes 1155 \u00e0 1171 refl\u00e8tent bien le comportement attendu. En revanche, si le programme est compil\u00e9 avec l'optimisation, notez la diff\u00e9rence\u2009:

    $ gcc main.c -O2\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001060 <main>:\n    1060:       f3 0f 1e fa             endbr64\n    1064:       48 83 ec 08             sub    $0x8,%rsp\n    1068:       31 d2                   xor    %edx,%edx\n    106a:       48 8d 35 93 0f 00 00    lea    0xf93(%rip),%rsi\n    1071:       31 c0                   xor    %eax,%eax\n    1073:       bf 01 00 00 00          mov    $0x1,%edi\n    1078:       e8 d3 ff ff ff          callq  1050 <__printf_chk@plt>\n    107d:       31 c0                   xor    %eax,%eax\n    107f:       48 83 c4 08             add    $0x8,%rsp\n    1083:       c3                      retq\n    1084:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    108b:       00 00 00\n    108e:       66 90                   xchg   %ax,%ax\n

    Les lignes ont disparu\u2009!

    Afin d'\u00e9viter cette optimisation, il faut marquer la variable i comme volatile:

    #include <stdio.h>\n\nint main() {\n    volatile int i = 0;\n\n    i = 1;\n    i = 0;\n    i = 1;\n    i = 0;\n\n    printf(\"%d\", i);\n}\n
    ", "tags": ["volatile"]}, {"location": "course-c/15-fundations/scope/#extern", "title": "extern", "text": "

    Cette classe est utilis\u00e9e pour signaler que la variable ou la fonction associ\u00e9e est d\u00e9clar\u00e9e dans un autre module (autre fichier). Ainsi le code suivant ne d\u00e9clare pas une nouvelle variable, foo mais s'attend \u00e0 ce que cette variable ait \u00e9t\u00e9 d\u00e9clar\u00e9e dans un autre fichier.

    extern int foo;\n
    ", "tags": ["foo", "extern"]}, {"location": "course-c/15-fundations/scope/#restrict", "title": "restrict", "text": "

    En C, le mot cl\u00e9 restrict, apparu avec C99, est utilis\u00e9 uniquement pour des pointeurs. Ce qualificatif de type informe le compilateur que pour toute la dur\u00e9e de vie du pointeur, aucun autre pointeur ne pointera que sur la valeur qu'il pointe ou une valeur d\u00e9riv\u00e9e de lui-m\u00eame (p. ex\u2009: p + 1).

    En d'autres termes, le qualificatif indique au compilateur que deux pointeurs diff\u00e9rents ne peuvent pas pointer sur les m\u00eames r\u00e9gions m\u00e9moires.

    Prenons l'exemple simple d'une fonction qui met \u00e0 jour deux pointeurs avec une valeur pass\u00e9e en param\u00e8tre\u2009:

    void update_ptr(size_t *a, size_t *b, const size_t *value) {\n    *a += *value;\n    *b += *value;\n}\n

    Le compilateur, n'ayant aucune information sur les pointeurs fournis, ne peut faire aucune hypoth\u00e8se d'optimisation. En effet, ces deux pointeurs a et b ainsi que value pourraient tr\u00e8s bien pointer sur la m\u00eame r\u00e9gion m\u00e9moire, et dans ce cas *a += *value aurait pour effet d'incr\u00e9menter value. En revanche, dans le cas o\u00f9 la fonction est d\u00e9clar\u00e9e de la fa\u00e7on suivante\u2009:

    void update_ptr(size_t *restrict a, size_t * restrict b,\n                const size_t *restrict value) {\n    *a += *value;\n    *b += *value;\n}\n

    le compilateur est inform\u00e9 qu'il peut faire l'hypoth\u00e8se que les trois pointeurs fournis en param\u00e8tres sont ind\u00e9pendants les uns des autres. Dans ce cas il peut optimiser le code. Voir restrict sur Wikipedia pour plus de d\u00e9tails.

    ", "tags": ["value", "restrict"]}, {"location": "course-c/15-fundations/stdio/", "title": "Entr\u00e9es Sorties", "text": "Le probl\u00e8me fondamental de la communication est celui de reproduire en un point, soit exactement, soit approximativement, un message s\u00e9lectionn\u00e9 \u00e0 un autre point.Claude Shannon

    Un programme informatique se compose d'entr\u00e9es (stdin) et de sorties (stdout et stderr).

    Pour faciliter la vie du programmeur, les biblioth\u00e8ques standard offrent toute une panoplie de fonctions pour formater les sorties et interpr\u00e9ter les entr\u00e9es.

    Les fonctions phares sont printf pour le formatage de cha\u00eene de caract\u00e8res et scanf pour la lecture de cha\u00eenes de caract\u00e8res. Ces derni\u00e8res fonctions se d\u00e9clinent en plusieurs variantes que nous verrons plus tard. La liste cit\u00e9e est non exhaustive, mais largement document\u00e9e ici\u2009: <stdio.h>.

    Les fonctions que nous allons aborder dans ce chapitre sont donn\u00e9es par la table suivante. Pour davantage de fonctions, vous pouvez vous rendre au chapitre traitant de la biblioth\u00e8que standard stdio.

    Fonctions d'entr\u00e9es/sorties principales Fonction Type Description putchar Sortie \u00c9crit un caract\u00e8re sur la sortie standard puts Sortie \u00c9crit une cha\u00eene de caract\u00e8res sur la sortie standard printf Sortie \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e sur la sortie standard getchar Entr\u00e9e Lit un caract\u00e8re sur l'entr\u00e9e standard gets Entr\u00e9e Lit une cha\u00eene de caract\u00e8res sur l'entr\u00e9e standard scanf Entr\u00e9e Lit une cha\u00eene de caract\u00e8res format\u00e9e sur l'entr\u00e9e standard", "tags": ["stdin", "printf", "stderr", "stdout", "scanf"]}, {"location": "course-c/15-fundations/stdio/#sorties-non-formatees", "title": "Sorties non format\u00e9es", "text": "

    Ces fonctions sont tr\u00e8s basiques et permettent d'\u00e9crire des caract\u00e8res ou des cha\u00eenes de caract\u00e8res sur la sortie standard.

    "}, {"location": "course-c/15-fundations/stdio/#putchar_1", "title": "Putchar", "text": "

    Cette fonction prend en param\u00e8tre un seul caract\u00e8re et l'\u00e9crit sur la sortie standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h.

    #include <stdio.h>\n\nint main() {\n    putchar('H');\n    putchar('e');\n    putchar('l');\n    putchar('l');\n    putchar('o');\n    putchar('\\n');\n}\n

    Avertissement

    Attention \u00e0 utiliser des apostrophes simples ' pour les caract\u00e8res. Si vous utilisez des guillemets doubles \" vous obtiendrez une erreur de compilation.

    On sait que les caract\u00e8res sont des entiers, donc on peut \u00e9crire putchar(65) pour \u00e9crire le caract\u00e8re A. Donc le programme suivant \u00e9crit la m\u00eame chose que le pr\u00e9c\u00e9dent\u2009:

    #include <stdio.h>\n\nint main() {\n    putchar(72);\n    putchar(101);\n    putchar(108);\n    putchar(108);\n    putchar(111);\n    putchar('\\n');\n}\n

    ", "tags": ["stdio.h"]}, {"location": "course-c/15-fundations/stdio/#puts_1", "title": "Puts", "text": "

    La fonction puts est une fonction de la biblioth\u00e8que standard C qui permet d'\u00e9crire une cha\u00eene de caract\u00e8res sur la sortie standard. Notons qu'elle ajoute automatiquement un retour \u00e0 la ligne \u00e0 la fin de la cha\u00eene.

    #include <stdio.h>\n\nint main() {\n    puts(\"hello, world\"); // Un retour \u00e0 la ligne est ajout\u00e9 automatiquement\n}\n

    Notez ici qu'on utilise des guillemets doubles \" pour les cha\u00eenes de caract\u00e8res.

    ", "tags": ["puts"]}, {"location": "course-c/15-fundations/stdio/#sorties-formatees", "title": "Sorties format\u00e9es", "text": "

    Convertir un nombre en une cha\u00eene de caract\u00e8res n'est pas trivial. Prenons l'exemple de la valeur 123. Il faut pour cela diviser it\u00e9rativement le nombre par 10 et calculer le reste\u2009:

    Etape  Op\u00e9ration  Resultat  Reste\n-----  ---------  --------  -----\n1      123 / 10   12        3\n2      12 / 10    1         2\n3      1 / 10     0         1\n

    Comme on ne sait pas \u00e0 priori combien de caract\u00e8res on aura, et que ces caract\u00e8res sont fournis depuis le chiffre le moins significatif, il faudra inverser la cha\u00eene de caract\u00e8res produite.

    Voici un exemple possible d'impl\u00e9mentation\u2009:

    #include <stdlib.h>\n#include <stdbool.h>\n\nvoid swap(char* a, char* b)\n{\n    char old_a = a;\n    a = b;\n    b = old_a;\n}\n\nvoid reverse(char* str, size_t length)\n{\n    for (size_t start = 0, end = length - 1; start < end; start++, end--)\n    {\n        swap(str + start, str + end);\n    }\n}\n\nvoid my_itoa(int num, char* str)\n{\n    const unsigned int base = 10;\n    bool is_negative = false;\n    size_t i = 0;\n\n    if (num == 0) {\n        str[i++] = '0';\n        str[i] = '\\0';\n        return;\n    }\n\n    if (num < 0) {\n        is_negative = true;\n        num = -num;\n    }\n\n    while (num != 0) {\n        int rem = num % 10;\n        str[i++] = rem + '0';\n        num /= base;\n    }\n\n    if (is_negative)\n        str[i++] = '-';\n\n    str[i] = '\\0';\n\n    reverse(str, i);\n}\n

    Cette impl\u00e9mentation pourrait \u00eatre utilis\u00e9e de la fa\u00e7on suivante\u2009:

    #include <stdlib.h>\n\nint main(void)\n{\n    int num = 123;\n    char buffer[10];\n\n    itoa(num, buffer);\n}\n

    "}, {"location": "course-c/15-fundations/stdio/#printf", "title": "Printf", "text": "

    Vous conviendrez que devoir manuellement convertir chaque valeur n'est pas des plus pratique, c'est pourquoi printf rend l'op\u00e9ration bien plus ais\u00e9e en utilisant des marques substitutives (placeholder). Ces sp\u00e9cifi\u00e9 d\u00e9butent par le caract\u00e8re % suivi du formatage que l'on veut appliquer \u00e0 une variable pass\u00e9e en param\u00e8tres. L'exemple suivant utilise %d pour formater un entier non sign\u00e9.

    #include <stdio.h>\n\nint main()\n{\n    int32_t earth_perimeter = 40075;\n    printf(\"La circonf\u00e9rence de la terre vaut vaut %d km\", earth_perimeter);\n}\n

    Le standard C d\u00e9fini le prototype de printf comme \u00e9tant\u2009:

    int printf(const char *restrict format, ...);\n

    Il d\u00e9finit que la fonction printf prend en param\u00e8tre un format suivi de .... La fonction printf comme toutes celles de la m\u00eame cat\u00e9gorie sont dites variadiques, c'est-\u00e0-dire qu'elles peuvent prendre un nombre variable d'arguments. Il y aura autant d'arguments additionnels que de marqueurs utilis\u00e9s dans le format. Ainsi le format \"Mes nombres pr\u00e9f\u00e9r\u00e9s sont %d et %d, mais surtout %s\" demandera trois param\u00e8tres additionnels\u2009:

    La fonction retourne le nombre de caract\u00e8res format\u00e9s ou -1 en cas d'erreur.

    La construction d'un marqueur est loin d'\u00eatre simple, mais heureusement on n'a pas besoin de tout conna\u00eetre et la page Wikip\u00e9dia printf format string est d'une grande aide. Le format de construction est le suivant\u2009:

    %[parameter][flags][width][.precision][length]type\n
    parameter (optionnel)

    Num\u00e9ro de param\u00e8tre \u00e0 utiliser

    flags (optionnel)

    Modificateurs\u2009: pr\u00e9fixe, signe plus, alignement \u00e0 gauche ...

    width (optionnel)

    Nombre minimum de caract\u00e8res \u00e0 utiliser pour l'affichage de la sortie.

    .precision (optionnel)

    Nombre minimum de caract\u00e8res affich\u00e9s \u00e0 droite de la virgule. Essentiellement, valide pour les nombres \u00e0 virgule flottante.

    length (optionnel)

    Longueur en m\u00e9moire. Indique la longueur de la repr\u00e9sentation binaire.

    type

    Type de formatage souhait\u00e9

    Formatage d'un marqueur

    Voici quelques exemples\u2009:

    Exemple de formatage avec printf Exemple Sortie Taille printf(\"%c\", 'c') c 1 printf(\"%d\", 1242) 1242 4 printf(\"%10d\", 42) 42 10 printf(\"%07d\", 42) 0000042 7 printf(\"%+-5dfr\", 23) +23 fr 6 printf(\"%5.3f\", 314.15) 314.100 7 printf(\"%*.*f\", 4, 2, 102.1) 102.10 7 printf(\"%8x\", 57005) dead 6 printf(\"%s\", \"Hello\") Hello 5

    On peut s'int\u00e9resser \u00e0 comment printf fonctionne en interne. Le premier argument est une cha\u00eene de caract\u00e8re qui est le motif de formatage. Il peut contenir des caract\u00e8res sp\u00e9ciaux placeholder qui seront intercept\u00e9s par printf pour \u00eatre remplac\u00e9s par les arguments suivants apr\u00e8s avoir \u00e9t\u00e9 convertis.

    Pour bien comprendre, on peut imaginer une impl\u00e9mentation na\u00efve de printf que nous appellerons my_printf et qui se basera sur une fonction de sortie non format\u00e9e putchar.

    Cette fonction ne sera capable que de traiter les marqueurs %d et %c, c'est suffisant pour comprendre le principe. \u00c9galement, elle prendra toujours deux arguments, donc une valeur \u00e0 afficher, ceci pour ne pas s'encombrer de la gestion de la liste variable d'arguments qui est un sujet avanc\u00e9.

    void my_printf(char format[], int a) {\n    // On parcourt la cha\u00eene de caract\u00e8res tant que l'on ne rencontre\n    // pas le caract\u00e8re de fin de cha\u00eene\n    for (int i = 0; format[i] != '\\0'; i++) {\n        // Si on rencontre un caract\u00e8re %, on regarde le caract\u00e8re suivant\n        if (format[i] == '%') {\n            // Est-ce que ce caract\u00e8re est sp\u00e9cial ?\n            switch (format[++i]) {\n                case 'd': {\n                    char str[32] = {0};\n                    my_itoa(int a, str);\n                    for (int j = 0; str[j] != '\\0'; j++) {\n                        putchar(str[j]);\n                    }\n                    break;\n                }\n                case 'c':\n                    // Affiche le caract\u00e8re en ASCII\n                    putchar(a);\n                    break;\n                default:\n                    // On affiche le caract\u00e8re tel quel,\n                    // ce qui permet d'afficher le caract\u00e8re %\n                    putchar(format[i]);\n            }\n        } else {\n            putchar(format[i]);\n        }\n    }\n}\n

    Exercice 1\u2009: Exercice

    Indiquez les erreurs dans les instructions suivantes\u2009:

    printf(\"%d%d\\n\", 10, 20);\nprintf(\"%d, %d, %d\\n\", 10, 20);\nprintf(\"%d, %d, %d, %d\\n\", 10, 20, 30, 40.);\nprintf(\"%*d, %*d\\n\", 10, 20);\nprintf(\"%6.2f\\n\", 10);\nprintf(\"%10s\\n\", 0x9f);\n
    ", "tags": ["Hello", "dead", "parameter", "printf", "flags", "my_printf", "length", "width", "putchar", "type"]}, {"location": "course-c/15-fundations/stdio/#entrees-non-formatees", "title": "Entr\u00e9es non format\u00e9es", "text": ""}, {"location": "course-c/15-fundations/stdio/#getchar_1", "title": "Getchar", "text": "

    La fonction getchar est une fonction de la biblioth\u00e8que standard C qui permet de lire un caract\u00e8re sur l'entr\u00e9e standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h. Elle retourne un entier qui correspond \u00e0 la valeur ASCII du caract\u00e8re lu.

    #include <stdio.h>\n\nint main() {\n    int c;\n    while ((c = getchar()) != EOF) {\n        printf(\"Caract\u00e8re lu : %c\\n\", c);\n    }\n}\n

    Notez ici l'utilisation de EOF qui est une constante d\u00e9finie dans la biblioth\u00e8que stdio.h et qui signifie End Of File. Elle est utilis\u00e9e pour d\u00e9tecter la fin d'un fichier.

    Lorsque vous ex\u00e9cutez ce programme, vous pouvez saisir des caract\u00e8res au clavier. Pour terminer la saisie, vous pouvez utiliser la combinaison de touches ++Ctrl+D++ sur Linux ou ++Ctrl+Z++ sur Windows.

    ", "tags": ["stdio.h", "EOF", "getchar"]}, {"location": "course-c/15-fundations/stdio/#gets_1", "title": "Gets", "text": "

    La fonction gets est une fonction de la biblioth\u00e8que standard C qui permet de lire une cha\u00eene de caract\u00e8res sur l'entr\u00e9e standard. Elle est d\u00e9finie dans la biblioth\u00e8que stdio.h.

    Elle est d\u00e9conseill\u00e9e, car elle ne permet pas de sp\u00e9cifier la taille maximale de la cha\u00eene \u00e0 lire. Cela peut entra\u00eener des d\u00e9bordements de m\u00e9moire si un utilisateur saisit une cha\u00eene de caract\u00e8res trop longue que le programme ne peut pas stocker.

    #include <stdio.h>\n\nint main() {\n    char str[128];\n    gets(str);\n    printf(\"Cha\u00eene lue : %s\\n\", str);\n}\n

    Avertissement

    La fonction gets est d\u00e9conseill\u00e9e. Il est pr\u00e9f\u00e9rable d'utiliser la fonction fgets qui permet de sp\u00e9cifier la taille maximale de la cha\u00eene \u00e0 lire.

    ", "tags": ["stdio.h", "fgets", "gets"]}, {"location": "course-c/15-fundations/stdio/#entrees-formatees", "title": "Entr\u00e9es format\u00e9es", "text": "

    Les fonctions de lecture de cha\u00eenes de caract\u00e8res sont plus complexes que les fonctions d'\u00e9criture. En effet, il est n\u00e9cessaire de sp\u00e9cifier le format de la cha\u00eene \u00e0 lire.

    "}, {"location": "course-c/15-fundations/stdio/#scanf_1", "title": "Scanf", "text": "

    \u00c0 l'instar de la sortie format\u00e9e, il est possible de lire les saisies au clavier ou parser une cha\u00eene de caract\u00e8res, c'est-\u00e0-dire faire une analyse syntaxique de son contenu pour en extraire de l'information.

    La fonction scanf est par exemple utilis\u00e9e \u00e0 cette fin\u2009:

    #include <stdio.h>\n\nint main()\n{\n    int favorite;\n\n    printf(\"Quelle est votre nombre favori ? \");\n    scanf(\"%d\", &favorite);\n\n    printf(\"Saviez-vous que votre nombre favori, %d, est %s ?\\n\",\n        favorite,\n        favorite % 2 ? \"impair\" : \"pair\");\n}\n

    Cette fonction utilise l'entr\u00e9e standard stdin. Il est donc possible soit d'ex\u00e9cuter ce programme en mode interactif\u2009:

    $ ./a.out\nQuelle est votre nombre favori ? 2\nSaviez-vous que votre nombre favori, 2, est pair ?\n

    soit d'ex\u00e9cuter ce programme en fournissant le n\u00e9cessaire \u00e0 stdin\u2009:

    $ echo \"23\" | ./a.out\nQuel est votre nombre favori ? Saviez-vous que votre nombre favori, 23, est impair ?\n

    On observe ici un comportement diff\u00e9rent, car le retour clavier lorsque la touche enter est press\u00e9e n'est pas transmis au programme, mais c'est le shell qui l'intercepte.

    Le format de scanf se rapproche de printf mais en plus simple. Le man scanf ou m\u00eame la page Wikip\u00e9dia de scanf renseigne sur son format.

    Cette fonction tient son origine une nouvelle fois de ALGOL 68 (readf), elle est donc tr\u00e8s ancienne.

    La compr\u00e9hension de scanf n'est pas \u00e9vidente et il est utile de se familiariser sur son fonctionnement \u00e0 l'aide de quelques exemples.

    Le programme suivant lit un entier et le place dans la variable n. scanf retourne le nombre d'assignements r\u00e9ussis. Ici, il n'y a qu'un placeholder, on s'attend naturellement \u00e0 lire 1 si la fonction r\u00e9ussit. Le programme \u00e9crit ensuite les nombres dans l'ordre d'apparition.

    #include <stdio.h>\n\nint main(void)\n{\n    int i = 0, n;\n\n    while (scanf(\"%d\", &n) == 1)\n        printf(\"%i\\t%d\\n\", ++i, n);\n    return 0;\n}\n

    Si le code est ex\u00e9cut\u00e9 avec une suite arbitraire de nombres\u2009:

    456 123 789     456 12\n456 1\n    2378\n

    il affichera chacun des nombres dans l'ordre d'apparition\u2009:

    $ cat << EOF | ./a.out\n456 123 789     456 12\n456 1\n    2378\nEOF\n1       456\n2       123\n3       789\n4       456\n5       12\n6       456\n7       1\n8       2378\n

    Voyons un exemple plus complexe (c.f. C99 \u00a77.19.6.2-19).

    int count;\nfloat quantity;\nchar units[21], item[21];\n\ndo {\n    count = scanf(\"%f%20s de %20s\", &quant, units, item);\n    scanf(\"%*[^\\n]\");\n} while (!feof(stdin) && !ferror(stdin));\n

    Lorsqu'ex\u00e9cut\u00e9 avec ce contenu\u2009:

    2 litres de lait\n-12.8degr\u00e9s Celsius\nbeaucoup de chance\n10.0KG de\npoussi\u00e8re\n100ergs d\u2019\u00e9nergie\n

    Le programme se d\u00e9roule comme suit\u2009:

    quantity = 2; strcpy(units, \"litres\"); strcpy(item, \"lait\");\ncount = 3;\n\nquantity = -12.8; strcpy(units, \"degrees\");\ncount = 2; // \"C\" \u00e9choue lors du test de \"d\" (de)\n\ncount = 0; // \"b\" de \"beaucoup\" \u00e9choue contre \"%f\" s'attendant \u00e0 un float\n\nquantity = 10.0; strcpy(units, \"KG\"); strcpy(item, \"poussi\u00e8re\");\ncount = 3;\n\ncount = 0; // \"100e\" \u00e9choue contre \"%f\", car \"100e3\" serait un nombre valable\ncount = EOF; // Fin de fichier\n

    Dans cet exemple, la boucle do... while est utilis\u00e9e, car il n'est pas simplement possible de traiter le cas while(scanf(...) > 0 puisque l'exemple cherche \u00e0 montrer les cas particuliers o\u00f9 justement, la capture \u00e9choue. Il est n\u00e9cessaire alors de faire appel \u00e0 des fonctions de plus bas niveau feof pour d\u00e9tecter si la fin du fichier est atteinte, et ferror pour d\u00e9tecter une \u00e9ventuelle erreur sur le flux d'entr\u00e9e.

    La directive scanf(\"%*[^\\n]\"); \u00e9tant un peu particuli\u00e8re, il peut valoir la peine de s'y attarder un peu. Le flag *, diff\u00e9rent de printf indique d'ignorer la capture en cours. L'exemple suivant montre comment ignorer un mot.

    #include <assert.h>\n#include <stdio.h>\n\nint main(void) {\n    int a, b;\n    char str[] = \"24 kayaks 42\";\n\n    sscanf(str, \"%d%*s%d\", &a, &b);\n    assert(a == 24);\n    assert(b == 42);\n}\n

    Ensuite, [^\\n]. Le marqueur [, termin\u00e9 par ] cherche \u00e0 capturer une s\u00e9quence de caract\u00e8res parmi une liste de caract\u00e8res accept\u00e9s. Cette syntaxe est inspir\u00e9e des expressions r\u00e9guli\u00e8res tr\u00e8s utilis\u00e9es en informatique. Le caract\u00e8re ^ \u00e0 une signification particuli\u00e8re, il indique que l'on cherche \u00e0 capturer une s\u00e9quence de caract\u00e8res parmi une liste de caract\u00e8res qui ne sont pas accept\u00e9s. C'est une sorte de n\u00e9gation. Dans le cas pr\u00e9sent, cette directive scanf cherche \u00e0 consommer tous les caract\u00e8res jusqu'\u00e0 une fin de ligne, car, dans le cas ou la capture \u00e9choue \u00e0 C de Celsius, le pointeur de fichier est bloqu\u00e9 au caract\u00e8re C et au prochain tour de boucle, scanf \u00e9chouera au m\u00eame endroit. Cette instruction est donc utilis\u00e9e pour repartir sur des bases saines en sautant \u00e0 la prochaine ligne.

    Exercice 2\u2009: scanf sur des entiers et des r\u00e9els

    Consid\u00e9rant les d\u00e9clarations\u2009:

    int i, j, k;\nfloat f;\n

    Donnez les valeurs de chacune des variables apr\u00e8s ex\u00e9cution. Chaque ligne est ind\u00e9pendante des autres.

    i = sscanf(\"1 12.5\", \"%d %d, &j, &k);\nsscanf(\"12.5\", \"%d %f\", &j, %f);\ni = sscanf(\"123 123\", \"%d %f\", &j, &f);\ni = sscanf(\"123a 123\", \"%d %f\", &j, &f);\ni = sscanf(\"%2d%2d%f\", &j, &k, &f);\n

    Exercice 3\u2009: Saisie de valeurs

    Consid\u00e9rant les d\u00e9clarations suivantes, donner la valeur des variables apr\u00e8s l'ex\u00e9cution des instructions donn\u00e9es avec les captures associ\u00e9es\u2009:

    int i = 0, j = 0, n = 0;\nfloat x = 0;\n
    no Expression Entr\u00e9e 1 n = scanf(\"%1d%1d\", &i, &j); 12\\n 2 n = scanf(\"%d%d\", &i, &j); 1 , 2\\n 3 n = scanf(\"%d%d\", &i, &j); -1 -2\\n 4 n = scanf(\"%d%d\", &i, &j); - 1 - 2\\n 5 n = scanf(\"%d,%d\", &i, &j); 1 , 2\\n 6 n = scanf(\"%d ,%d\", &i, &j); 1 , 2\\n 7 n = scanf(\"%4d %2d\", &i, &j); 1 234\\n 8 n = scanf(\"%4d %2d\", &i, &j); 1234567\\n 9 n = scanf(\"%d%*d%d\", &i, &j); 123 456 789\\n 10 n = scanf(\"i=%d , j=%d\", &i, &j); 1 , 2\\n 11 n = scanf(\"i=%d , j=%d\", &i, &j); i=1, j=2\\n 12 n = scanf(\"%d%d\", &i, &j); 1.23 4.56\\n 13 n = scanf(\"%d.%d\", &i, &j); 1.23 4.56\\n 14 n = scanf(\"%x%x\", &i, &j); 12 2a\\n 15 n = scanf(\"%x%x\", &i, &j); 0x12 0X2a\\n 16 n = scanf(\"%o%o\", &i, &j); 12 018\\n 17 n = scanf(\"%f\", &x); 123\\n 18 n = scanf(\"%f\", &x); 1.23\\n 19 n = scanf(\"%f\", &x); 123E4\\n 20 n = scanf(\"%e\", &x); 12\\n Solution Q i j n Remarque 1 1 2 2 2 1 0 1. j n'est pas lue car arr\u00eat pr\u00e9matur\u00e9 sur , 3 -1 -2 2 4 0 0 0. i n'est pas lue car arr\u00eat pr\u00e9matur\u00e9 sur - 5 1 0 1. 6 1 2 2 7 1 23 2 8 1234 56 2 9 123 789 2 10 0 0 0 11 1 2 2 12 1 0 1 13 1 23 2 14 18 42 2 15 10 1 2. Le chiffre 8 interdit en octal provoque un arr\u00eat x n 16 123. 1 17 1.23 1 18 1.23E6 1 19 12 1

    Exercice 4\u2009: Cha\u00eenes de formats

    1. Saisir 3 caract\u00e8res cons\u00e9cutifs dans des variables i, j, k.
    2. Saisir 3 nombres de type float s\u00e9par\u00e9s par un point-virgule et un nombre quelconque d'espaces dans des variables x, y et z.
    3. Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe =, dans des variables t, u et v.
    Solution
    1. Saisir 3 caract\u00e8res cons\u00e9cutifs dans des variables i, j, k.

      scanf(\"%c%c%c\", &i, &j, &k);\n
    2. Saisir 3 nombres de type float s\u00e9par\u00e9s par un point-virgule et un nombre quelconque d'espaces dans des variables x, y et z.

      scanf(\"%f ;%f ;%f\", &x, &y, &z);\n
    3. Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe =, dans des variables t, u et v.

      printf(\"t=\"); scanf(\"%f\", &t);\nprintf(\"u=\"); scanf(\"%f\", &u);\nprintf(\"v=\"); scanf(\"%f\", &v);\n
    ", "tags": ["stdin", "readf", "printf", "while", "Celsius", "feof", "ferror", "scanf"]}, {"location": "course-c/15-fundations/stdio/#saisie-de-chaine-de-caracteres", "title": "Saisie de cha\u00eene de caract\u00e8res", "text": "

    Lors d'une saisie de cha\u00eene de caract\u00e8res, il est n\u00e9cessaire de toujours indiquer une taille maximum de cha\u00eene comme %20s qui limite la capture \u00e0 20 caract\u00e8res, soit une cha\u00eene de 21 caract\u00e8res avec son \\0. Sinon, il y a risque de fuite m\u00e9moire :

    int main(void) {\n    char a[6];\n    char b[10] = \"R\u00e2teau\";\n\n    char str[] = \"jardinage\";\n    sscanf(str, \"%s\", a);\n\n    printf(\"a. %s\\nb. %s\\n\", a, b);\n}\n
    $ ./a.out\na. jardinage\nb. age\n

    Ici la variable b contient age alors qu'elle devrait contenir r\u00e2teau. La raison est que le mot captur\u00e9 jardinage est trop long pour la variable a qui n'est dispos\u00e9e \u00e0 stocker que 5 caract\u00e8res imprimables. Il y a donc d\u00e9passement de m\u00e9moire et comme vous le constatez, le compilateur ne g\u00e9n\u00e8re aucune erreur. La bonne m\u00e9thode est donc de prot\u00e9ger la saisie ici avec %5s.

    En m\u00e9moire, ces deux variables sont adjacentes et naturellement a[7] est \u00e9quivalente \u00e0 dire la septi\u00e8me case m\u00e9moire \u00e0 partir du d\u00e9but de ``a``.

         a[6]              b[10]\n\u251e\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\u251e\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526\n\u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502\u2502R\u2502\u00e2\u2502t\u2502e\u2502a\u2502u\u2502 \u2502 \u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n
    ", "tags": ["jardinage", "r\u00e2teau", "age"]}, {"location": "course-c/15-fundations/stdio/#saisie-arbitraire", "title": "Saisie arbitraire", "text": "

    Comme bri\u00e8vement \u00e9voqu\u00e9 plus haut, il est possible d'utiliser le marqueur [ pour capturer une s\u00e9quence de caract\u00e8res. Imaginons que je souhaite capturer un nombre en tetrasexagesimal (base 64). Je peux \u00e9crire\u2009:

    char input[] = \"Q2hvY29sYXQ\";\nchar output[128];\nsscanf(input, \"%127[0-9A-Za-z+/]\", &output);\n

    Dans cet exemple je capture les nombres de 0 \u00e0 9 0-9 (10), les caract\u00e8res majuscules et minuscules A-Za-z (52), ainsi que les caract\u00e8res +, / (2), soit 64 caract\u00e8res. Le buffer d'entr\u00e9e \u00e9tant fix\u00e9 \u00e0 128 positions, la saisie est contrainte \u00e0 127 caract\u00e8res imprimables.

    Exercice 5\u2009: Bugs

    Parmi les instructions ci-dessous, indiquez celles qui sont correctes et celle qui comporte des erreurs. Pour celles comportant des erreurs, d\u00e9taillez la nature des anomalies.

    short i;\nlong j;\nunsigned short u;\nfloat x;\ndouble y;\nprintf(i);\nscanf(&i);\nprintf(\"%d\", &i);\nscanf(\"%d\", &i);\nprintf(\"%d%ld\", i, j, u);\nscanf(\"%d%ld\", &i, j);\nprintf(\"%u\", &u);\nscanf(\"%d\", &u);\nprintf(\"%f\", x);\nscanf(\"%f\", &x);\nprintf(\"%f\", y);\nscanf(\"%f\", &y);\n
    Solution
    // Incorrect ! Le premier param\u00e8tre de printf doit \u00eatre la cha\u00eene de format.\nprintf(i);\n\n// Incorrect ! Le premier param\u00e8tre de scanf doit \u00eatre la cha\u00eene de format.\nscanf(&i);\n\n// Correct, mais surprenant.\n// Cette instruction affichera l\u2019adresse de I, et non pas sa valeur !\nprintf(\"%d\", &i);\n\n// Incorrect. Le param\u00e8tre i est de type short, alors que la cha\u00eene de\n// format sp\u00e9cifie un type int. Fonctionnera sur les machines dont le type\n// short et int sont identiques\nscanf(\"%d\", &i);\n\n// Incorrect, la troisi\u00e8me variable pass\u00e9e en param\u00e8tre ne sera pas affich\u00e9e.\nprintf(\"%d%ld\", i, j, u);\n\n// Incorrect ! Le premier param\u00e8tre est de type short alors que int\n// est sp\u00e9cifi\u00e9 dans la cha\u00eene de format.\n// Le deuxi\u00e8me param\u00e8tre n\u2019est pas pass\u00e9 par adresse, ce qui va\n// probablement causer une erreur fatale.\nscanf(\"%d%ld\", &i, j);\n\n// Correct, mais \u00e9tonnant. Affiche l\u2019adresse de la variable u.\nprintf(\"%u\", &u);\n\n// Incorrect ! Le param\u00e8tre est de type unsigned short, alors que\n// la cha\u00eene de format sp\u00e9cifie int. Fonctionnera pour les valeurs\n// positives sur les machines dont le type short et int sont identiques.\n// Pour les valeurs n\u00e9gatives, le r\u00e9sultat sera l\u2019interpr\u00e9tation non\n// sign\u00e9e de la valeur en compl\u00e9ment \u00e0 2.\nscanf(\"%d\", &u);\n\n// Correct, mais x est trait\u00e9 comme double.\nprintf(\"%f\", x);\n\n// Correct.\nscanf(\"%f\", &x);\n\n// Correct ! %f est trait\u00e9 comme double par printf !\nprintf(\"%f\", y);\n\n// Incorrect ! La cha\u00eene de format sp\u00e9cifie float,\n// le param\u00e8tre pass\u00e9 est l\u2019adresse d\u2019une variable de type double.\nscanf(\"%f\", &y);\n

    Exercice 6\u2009: Test de saisir correcte

    \u00c9crivez un programme d\u00e9clarant des variables r\u00e9elles x, y et z, permettant de saisir leur valeur en une seule instruction, et v\u00e9rifiant que les 3 valeurs ont bien \u00e9t\u00e9 assign\u00e9es. Dans le cas contraire, afficher un message du type \u00ab\u2009donn\u00e9es invalides\u2009\u00bb.

    Solution
    int n;\nfloat x, y, z;\nprintf(\"Donnez les valeurs de x, y et z :\");\nn = scanf(\"%f%f%f\", &x, &y, &z);\nif (n != 3)\nprintf(\"Erreur de saisie.\\n\");\n

    Exercice 7\u2009: Produit scalaire

    \u00c9crire un programme effectuant les op\u00e9rations suivantes\u2009:

    • Saisir les coordonn\u00e9es r\u00e9elles x1 et y1 d\u2019un vecteur v1.
    • Saisir les coordonn\u00e9es r\u00e9elles x2 et y2 d\u2019un vecteur v2.
    • Calculer le produit scalaire. Afficher un message indiquant si les vecteurs sont orthogonaux ou non.
    Solution
    #include <stdio.h>\n#include <stdlib.h>\n\nint main(void)\n{\n    float x1, y1\n    printf(\"Coordonn\u00e9es du vecteur v1 s\u00e9par\u00e9es par un \\\";\\\" :\\n\");\n    scanf(\"%f ;%f\", &x1, &y1);\n\n    float x2, y2;\n    printf(\"Coordonn\u00e9es du vecteur v2 s\u00e9par\u00e9es par un \\\";\\\" :\\n\");\n    scanf(\"%f ;%f\", &x2, &y2);\n\n    float dot_product = x1 * x2 + y1 * y2;\n    printf(\"Produit scalaire : %f\\n\", dot_product);\n    if (dot_product == 0.0)\n        printf(\"Les vecteurs sont orthogonaux.\\n\");\n}\n

    Ce programme risque de ne pas bien d\u00e9tecter l\u2019orthogonalit\u00e9 de certains vecteurs, car le test d\u2019\u00e9galit\u00e9 \u00e0 0 avec les virgules flottantes pourrait mal fonctionner. En effet, pour deux vecteurs orthogonaux, les erreurs de calcul en virgule flottante pourraient amener \u00e0 un produit scalaire calcul\u00e9 tr\u00e8s proche, mais cependant diff\u00e9rent de z\u00e9ro. On peut corriger ce probl\u00e8me en modifiant le test pour v\u00e9rifier si le produit scalaire est tr\u00e8s petit, par exemple compris entre -0.000001 et +0.000001:

    if (dot_product >= -1E-6 && dot_product <= 1E-6)\n

    Ce qui peut encore s\u2019\u00e9crire en utilisant la fonction valeur absolue\u2009:

    if (fabs(dot_product) <= 1E-6)\n

    Exercice 8\u2009: Crampes de doigts

    Votre coll\u00e8gue n'a pas cess\u00e9 de se plaindre de crampes... aux doigts... Il a \u00e9crit le programme suivant avant de prendre cong\u00e9 pour se rendre chez son m\u00e9decin.

    Gr\u00e2ce \u00e0 votre esprit affut\u00e9 et votre \u0153il per\u00e7ant, vous identifiez 13 erreurs. Lesquelles sont-elles\u2009?

    #include <std_io.h>\n#jnclude <stdlib.h>\nINT Main()\n{\nint a, sum;\nprintf(\"Addition de 2 entiers a et b.\\n\");\n\nprintf(\"a: \")\nscanf(\"%d\", a);\n\nprintf(\"b: \");\nscanf(\"%d\", &b);\n\n/* Affichage du r\u00e9sultat\nsomme = a - b;\nPrintf(\"%d + %d = %d\\n\", a, b, sum);\n\nretturn EXIT_FAILURE;\n}\n}\n
    Solution

    Une fois la correction effectu\u00e9e, vous utilisez l'outil de diff pour montrer les diff\u00e9rences\u2009:

    1,3c1,3\n<         #include <stdio.h>\n<         #include <stdlib.h>\n<         int main()\n---\n>         #include <std_io.h>\n>         #jnclude <stdlib.h>\n>         INT Main()\n5c5\n<         int a, b, sum;\n---\n>         int a, sum;\n9c9\n<         scanf(\"%d\", &a);\n---\n>         scanf(\"%d\", a);\n14,16c14,16\n<         /* Affichage du r\u00e9sultat */\n<         sum = a + b;\n<         printf(\"%d + %d = %d\\n\", a, b, sum);\n---\n>         /* Affichage du r\u00e9sultat\n>         somme = a - b;\n>         Printf(\"%d + %d = %d\\n\", a, b, sum);\n18c18,19\n<         return EXIT_SUCCESS;\n---\n>         return EXIT_FAILURE;\n>         }\n

    Exercice 9\u2009: G\u00e9om\u00e9trie affine

    Consid\u00e9rez le programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main(void)\n{\n    float a;\n    printf(\"a = \");\n    scanf(\"%f\", &a);\n\n    float b;\n    printf(\"b = \");\n    scanf(\"%f\", &b);\n\n    float x;\n    printf(\"x = \");\n    scanf(\"%f\", &x);\n\n    float y = a * x + b;\n\n    printf(\"y = %f\\n\", y);\n}\n
    1. \u00c0 quelle ligne commence l'ex\u00e9cution de ce programme\u2009?
    2. Dans quel ordre s'ex\u00e9cutent les instructions\u2009?
    3. D\u00e9crivez ce que fait ce programme \u00e9tape par \u00e9tape
    4. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?
    5. Quelle est l'utilit\u00e9 de ce programme\u2009?
    Solution
    1. Ligne 6
    2. C est un langage imp\u00e9ratif, l'ordre est s\u00e9quentiel du haut vers le bas
    3. Les \u00e9tapes sont les suivantes\u2009:

      1. Demande de la valeur de a \u00e0 l'utilisateur
      2. Demande de la valeur de b \u00e0 l'utilisateur
      3. Demande de la valeur de x \u00e0 l'utilisateur
      4. Calcul de l'image affine de x (\u00e9quation de droite)
      5. Affichage du r\u00e9sultat
    4. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?

      • Il verra y = 12 pour a = 2; x = 5; b = 2
    5. Quelle est l'utilit\u00e9 de ce programme\u2009?

      • Le calcul d'un point d'une droite

    Exercice 10\u2009: \u00c9quation de droite

    L'exercice pr\u00e9c\u00e9dent souffre de nombreux d\u00e9fauts. Sauriez-vous les identifier et perfectionner l'impl\u00e9mentation de ce programme\u2009?

    Solution

    Citons les d\u00e9fauts de ce programme\u2009:

    • Le programme ne peut pas \u00eatre utilis\u00e9 avec les arguments, uniquement en mode interactif
    • Les invit\u00e9s de dialogue a =, b = ne sont pas clair, a et b sont associ\u00e9s \u00e0 quoi\u2009?
    • La valeur de retour n'est pas exploitable directement.
    • Le nom des variables utilis\u00e9 n'est pas clair.
    • Aucune valeur par d\u00e9faut.

    Une solution possible serait\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main(int argc, char* argv[])\n{\n    float x;\n    float offset;\n    float slope;\n\n    if (argc > 2) {\n        offset = atof(argv[1]);\n        slope = atof(argv[2]);\n    } else {\n        float offset_default = 0.;\n        printf(\"Offset? [%f]: \", offset_default);\n        if (!scanf(\"%f\", &offset)) {\n            offset = offset_default;\n        }\n\n        float slope_default = 1.;\n        printf(\"Pente? [%f]: \", slope_default);\n        if (!scanf(\"%f\", &slope)) {\n            slope = slope_default;\n        }\n    }\n\n    if (argc == 2 || argc > 3) {\n        slope = atof(argv[argc == 2 ? 2: 3]);\n    } else {\n        float x_default = 0;\n        printf(\"x (abscisse) [%f]:\", x_default);\n        if (!scanf(\"%f\", &x)) {\n            x = x_default;\n        }\n    }\n\n    printf(\"%f\\n\", slope * x + offset);\n\n    return 0;\n}\n

    Exercice 11\u2009: Loi d'Ohm

    \u00c9crivez un programme demandant deux r\u00e9els tension et r\u00e9sistance, et affichez ensuite le courant. Pr\u00e9voir un test pour le cas o\u00f9 la r\u00e9sistance serait nulle.

    Exercice 12\u2009: Tour Eiffel

    Consid\u00e9rons le programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <math.h>\n\nint main()\n{\n    printf(\"Quel angle mesurez-vous en visant le sommet du b\u00e2timent (en degr\u00e9s): \");\n    float angle_degre;\n    scanf(\"%f\", &angle_degrees);\n    float angle_radian = angle_degrees * M_PI / 45.;\n\n    printf(\"\u00c0 quelle distance vous trouvez vous du b\u00e2timent (en m\u00e8tres): \");\n    float distance;\n    scanf(\"%f\", &distance);\n\n    float height = distance / tan(angle_radian);\n    printf(\"La hauteur du b\u00e2timent est : %g m\u00e8tres.\\n\", height);\n}\n
    1. Que fait le programme \u00e9tape par \u00e9tape\u2009?
    2. Que verra l'utilisateur \u00e0 l'\u00e9cran\u2009?
    3. \u00c0 quoi sert ce programme\u2009?
    4. Euh, mais\u2009? Ce programme comporte des erreurs, lesquelles\u2009?
    5. Impl\u00e9mentez-le et testez-le.

    Exercice 13\u2009: Hyperloop

    Hyperloop (aussi orthographi\u00e9 Hyperl\u221ep) est un projet ambitieux d'Elon Musk visant \u00e0 construire un moyen de transport ultra rapide utilisant des capsules voyageant dans un tube sous vide. Ce projet est analogue \u00e0 celui \u00e9tudi\u00e9 en suisse et nomm\u00e9 Swissmetro, mais abandonn\u00e9 en 2009.

    N\u00e9anmoins, les ing\u00e9nieurs suisses avaient \u00e0 l'\u00e9poque \u00e9crit un programme pour calculer, compte tenu d'une vitesse donn\u00e9e, le temps de parcours entre deux villes de Suisse.

    \u00c9crire un programme pour calculer la distance entre deux villes de suisse parmi lesquelles propos\u00e9es sont\u2009:

    • Gen\u00e8ve
    • Z\u00fcrich
    • B\u00e2le
    • Bern
    • St-Galle

    Consid\u00e9rez une acc\u00e9l\u00e9ration de 0.5 g pour le calcul de mouvement, et une vitesse maximale de 1220 km/h.

    ", "tags": ["courant", "tension", "diff", "r\u00e9sistance"]}, {"location": "course-c/15-fundations/stdio/#portabilite-des-formats", "title": "Portabilit\u00e9 des formats", "text": "

    Les formats de scanf et printf sont d\u00e9pendants de la plateforme. Par exemple, %d est un entier sign\u00e9, %u un entier non sign\u00e9, %ld est un entier long sign\u00e9. N\u00e9anmoins ces formats ne sont pas portables, car selon le mod\u00e8le de donn\u00e9es de la machine, un entier long peut \u00eatre de 32 bits ou de 64 bits.

    Cela n'a pas une grande importance si vous utilisez les types standards (comme int, long, short, char), mais si vous utilisez des types sp\u00e9cifiques comme int32_t, int64_t, uint32_t, uint64_t, vous devez utiliser les formats sp\u00e9cifiques de la biblioth\u00e8que inttypes.h. Voici la table de correspondance des formats\u2009:

    Formats portables Type Format int8_t PRId8 int16_t PRId16 int32_t PRId32 int64_t PRId64 uint8_t PRIu8 uint16_t PRIu16 uint32_t PRIu32 uint64_t PRIu64

    On peut ajouter des options \u00e0 ces formats, par exemple pour afficher un entier en hexad\u00e9cimal, on utilise %PRIx32 pour un entier 32 bits. Pour la valeur en octal, on utilise %PRIo32.

    Options des formats portables Option Description x Hexad\u00e9cimal o Octal u Non sign\u00e9 d Sign\u00e9

    L'utilisation est particuli\u00e8re car il faut utiliser la macro PRI pour d\u00e9finir le format. Par exemple, pour afficher un entier 32 bits en hexad\u00e9cimal, on utilise\u2009:

    #include <inttypes.h>\n#include <stdio.h>\n\nint main(void)\n{\n    int32_t i = 0x12345678;\n    printf(\"i = %\" PRIx32 \"\\n\", i);\n}\n

    Exercice 14\u2009: Constantes litt\u00e9rales caract\u00e9rielles

    Indiquez si les constantes litt\u00e9rales suivantes sont valides ou invalides.

    1. 'a'
    2. 'A'
    3. 'ab'
    4. '\\x41'
    5. '\\041'
    6. '\\0x41'
    7. '\\n'
    8. '\\w'
    9. '\\t'
    10. '\\xp2'
    11. \"abcdef\"
    12. \"\\abc\\ndef\"
    13. \"\\'\\\"\\\\\"
    14. \"hello \\world!\\n\"

    Exercice 15\u2009: Cha\u00eenes de formatage

    Pour les instructions ci-dessous, indiquer quel est l'affichage obtenu.

    char a = 'a';\nshort sh1 = 5;\nfloat f1 = 7.0f;\nint i1 = 7, i2 = 'a';\n
    1. printf(\"Next char: %c.\\n\", a + 1);
    2. printf(\"Char: %3c.\\n\", a);
    3. printf(\"Char: %-3c.\\n\", a);
    4. printf(\"Chars: \\n-%c.\\n-%c.\\n\", a, 'z' - 1);
    5. printf(\"Sum: %i\\n\", i1 + i2 - a);
    6. printf(\"Taux d\u2019erreur\\t%i %%\\n\", i1);
    7. printf(\"Quel charabia horrible:\\\\\\a\\a\\a%g\\b\\a%%\\a\\\\\\n\", f1);
    8. printf(\"Inventaire: %i4 pieces\\n\", i1);
    9. printf(\"Inventory: %i %s\\n\", i1, \"pieces\");
    10. printf(\"Inventaire: %4i pieces\\n\", i1);
    11. printf(\"Inventaire: %-4i pieces\\n\", i1);
    12. printf(\"Mixed sum: %f\\n\", sh1 + i1 + f1);
    13. printf(\"Tension: %5.2f mV\\n\", f1);
    14. printf(\"Tension: %5.2e mV\\n\", f1);
    15. printf(\"Code: %X\\n\", 12);
    16. printf(\"Code: %x\\n\", 12);
    17. printf(\"Code: %o\\n\", 12);
    18. printf(\"Value: %i\\n\", -1);
    19. printf(\"Value: %hi\\n\", 65535u);
    20. printf(\"Value: %hu\\n\", -1);
    ", "tags": ["int32_t", "inttypes.h", "uint32_t", "char", "int64_t", "printf", "PRI", "uint64_t", "short", "scanf", "long", "int"]}, {"location": "course-c/15-fundations/syntax/", "title": "Syntaxe", "text": "Tout devrait \u00eatre rendu aussi simple que possible, mais pas plus simple.Albert Einstein

    Ce chapitre traite des \u00e9l\u00e9ments constitutifs et fondamentaux du langage C. Il traite des g\u00e9n\u00e9ralit\u00e9s propres au langage, mais aussi des notions \u00e9l\u00e9mentaires permettant d'interpr\u00e9ter du code source. Notons que ce chapitre est transversal, \u00e0 la sa premi\u00e8re lecture, le profane ne pourra tout comprendre sans savoir lu et ma\u00eetris\u00e9 les chapitres suivants, n\u00e9anmoins il retrouvera ici les aspects fondamentaux du langage.

    "}, {"location": "course-c/15-fundations/syntax/#lalphabet", "title": "L'alphabet", "text": "

    Heureusement pour nous occidentaux, l'alphabet de C est compos\u00e9 de 52 caract\u00e8res latins et de 10 chiffres indo-arabes :

    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z\na b c d e f g h i j k l m n o p q r s t u v w x y z\n0 1 2 3 4 5 6 7 8 9\n

    Pour comparaison, le syst\u00e8me d'\u00e9criture cor\u00e9en (Hangul) est alphasyllabique, c'est-\u00e0-dire que chaque caract\u00e8re repr\u00e9sente une syllabe. Les lettres de base sont compos\u00e9es de 14 consonnes de base et 10 voyelles. Quant aux chiffres, ils sont les m\u00eames qu'en occident.

    g n d r/l m b s ng j ch k t p h\n\u3131 \u3134 \u3137 \u3139 \u3141 \u3142 \u3145 \u3147 \u3148 \u314a \u314b \u314c \u314d \u314e\n\na ya eo yeo o yo u yu eu i\n\u314f \u3151 \u3153 \u3155 \u3157 \u315b \u315c \u3160 \u3161 \u3163\n

    Les Japonais quant \u00e0 eux utilisent trois syst\u00e8mes d'\u00e9criture, le Hiragana, le Katakana et le Kanji. Les deux premiers sont des syllabaires et le dernier est un syst\u00e8me d'\u00e9criture logographique. Le Hiragana et Katakana ont tous deux 46 caract\u00e8res de base. Voici l'exemple du Katakana\u2009:

    \u3042 (a),  \u3044 (i),   \u3046 (u),   \u3048 (e),  \u304a (o)\n\u304b (ka), \u304d (ki),  \u304f (ku),  \u3051 (ke), \u3053 (ko)\n\u3055 (sa), \u3057 (shi), \u3059 (su),  \u305b (se), \u305d (so)\n\u305f (ta), \u3061 (chi), \u3064 (tsu), \u3066 (te), \u3068 (to)\n\u306a (na), \u306b (ni),  \u306c (nu),  \u306d (ne), \u306e (no)\n\u306f (ha), \u3072 (hi),  \u3075 (fu),  \u3078 (he), \u307b (ho)\n\u307e (ma), \u307f (mi),  \u3080 (mu),  \u3081 (me), \u3082 (mo)\n\u3084 (ya), \u3086 (yu),  \u3088 (yo)\n\u3089 (ra), \u308a (ri),  \u308b (ru),  \u308c (re), \u308d (ro)\n\u308f (wa), \u3092 (wo)\n\u3093 (n)\n

    On notera ici que l'alphabet latin est tout particuli\u00e8rement adapt\u00e9 \u00e0 la programmation, car, d'une part ce fut le premier alphabet \u00e0 \u00eatre utilis\u00e9 pour l'\u00e9criture de programmes informatiques et d'autre part, il reste assez simple pour \u00eatre utilis\u00e9 par des machines. On peut noter en outre que les pays qui utilisent leur propre alphabet doivent imp\u00e9rativement apprendre et ma\u00eetriser l'alphabet latin pour pouvoir programmer. Ceci implique qu'ils doivent \u00e9galement disposer d'un clavier latin pour pouvoir saisir leur code. Ayez \u00e0 l'esprit que nous sommes des privil\u00e9gi\u00e9s de ne pas devoir jongler avec plusieurs alphabets pour \u00e9crire du code...

    Outre ces caract\u00e8res, la s\u00e9paration des symboles est assur\u00e9e par une espace, une tabulation horizontale, une tabulation verticale, et un caract\u00e8re de retour \u00e0 la ligne. Ces caract\u00e8res ne sont pas imprimables, c'est-\u00e0-dire qu'ils ne sont pas directement visibles ni \u00e0 l'\u00e9cran ni \u00e0 l'impression (ni sur le papier d'ailleurs). Microsoft Word et d'autres \u00e9diteurs utilisent g\u00e9n\u00e9ralement le pied-de-mouche \u00b6 00B6 pour indiquer les fins de paragraphes qui sont \u00e9galement des caract\u00e8res non imprimables.

    La convention est de nommer les caract\u00e8res non imprimables soit par leur acronyme LF pour Line Feed ou soit par leur convention C \u00e9chapp\u00e9e par un backslash \\n:

    Caract\u00e8res non imprimables Acronyme \u00c9chapp\u00e9 Description LF \\n Retour \u00e0 la ligne VT \\v Tabulation verticale FF \\f Nouvelle page TAB \\t Tabulation horizontale CR \\r Retour charriot SPACE \\040 Espace NUL \\0 Caract\u00e8re nul

    Voici en pratique comment ces caract\u00e8res peuvent \u00eatre utilis\u00e9s\u2009:

    Distinction de diff\u00e9rents caract\u00e8res non imprimables

    La ponctuation utilise les 29 symboles graphiques suivants\u2009:

    ! # % ^ & * ( _ ) - + = ~ [ ] ' | \\ ; : \" { } , . < > / ?\n
    ", "tags": ["hiragana", "hangul", "tabulation", "ecriture-logographique", "katakana", "alphasyllabique", "kanji"]}, {"location": "course-c/15-fundations/syntax/#la-machine-a-ecrire", "title": "La machine \u00e0 \u00e9crire", "text": "

    Peut-\u00eatre avez-vous d\u00e9j\u00e0 \u00e9t\u00e9 confront\u00e9 \u00e0 une machine \u00e0 \u00e9crire m\u00e9canique. Elles disposent d'un levier sur la gauche du chariot qui dispose de deux fonctionnalit\u00e9s. D'une part il permet de faire revenir le chariot au d\u00e9but de la ligne, mais \u00e9galement de faire avancer le papier d'une ligne par une rotation du cylindre. C'est ce levier de retour chariot qui a donn\u00e9 son nom au caract\u00e8re de retour \u00e0 la ligne CR pour Carriage Return. Quant au caract\u00e8re de nouvelle ligne LF pour Line Feed, il est associ\u00e9 \u00e0 la rotation du rouleau qui entra\u00eene la feuille de papier.

    Machine \u00e0 \u00e9crire Herm\u00e8s 3000 h\u00e9bra\u00efque

    Historiquement il y a donc bien une distinction entre ces deux caract\u00e8res, mais aujourd'hui, cela n'a plus vraiment de sens. Un autre point que l'on peut relever est que pour souligner un texte, on utilisait le caract\u00e8re de soulignement (tiret bas, ou underscore) _ pour mettre en emphase du texte d\u00e9j\u00e0 \u00e9crit. De m\u00eame pour barrer un texte, on utilisait le caract\u00e8re - pour faire reculer le chariot d'une demi-case et ensuite frapper le m\u00eame caract\u00e8re. Enfin, pour ajouter un accent circonflexe, il fallait utiliser la touche ^ pour faire reculer le chariot d'une demi-case et ensuite frapper la lettre \u00e0 accentuer.

    Ces subtilit\u00e9s de la machine \u00e0 \u00e9crire ont \u00e9t\u00e9 partiellement reprises dans le format Unicode. Aussi pour \u00e9crire un accent aigu, il y a aujourd'hui plusieurs fa\u00e7ons de le faire.

    1. Utiliser le caract\u00e8re e 0065 suivi du caract\u00e8re \u00b4 0301 aussi appel\u00e9 combining acute accent pour obtenir \u00e9.
    2. Utiliser le caract\u00e8re \u00e9 00E9 directement.

    Ces h\u00e9ritages historiques font qu'il est difficile aujourd'hui de traiter sans bogue les textes multilingues. Les cas particuliers sont nombreux et bien souvent, les informaticiens utilisent des biblioth\u00e8ques logicielles pour g\u00e9rer ces cas particuliers.

    Un fait historique int\u00e9ressant est que les premiers ordinateurs ne disposaient pas d'un clavier ayant tous ces symboles et la commission responsable de standardiser C a int\u00e9gr\u00e9 au standard les trigraphes et plus tard les digraphes qui sont des combinaisons de caract\u00e8res de base qui remplacent les caract\u00e8res impossibles \u00e0 saisir directement. Ainsi <: est le digraphe de [ et ??< est le trigraphe de {. N\u00e9anmoins vous conviendrez cher lecteur que ces alternatives ne devraient \u00eatre utilis\u00e9es que dans des cas extr\u00eames et justifiables. Par ailleurs, le standard C et C++ songent \u00e0 les retirer.

    Retenez que C peut \u00eatre un langage extr\u00eamement cryptique tant il est permissif sur sa syntaxe. Il existe d'ailleurs un concours international d'obfuscation, le The International Obfuscated C Code Contest qui prime des codes les plus subtils et illisibles comme le code suivant \u00e9crit par Chris Mills. Il s'agit d'ailleurs d'un exemple qui compile parfaitement sur la plupart des compilateurs.

        int I=256,l,c, o,O=3; void e(\nint L){ o=0; for( l=8; L>>++l&&\n16>l;                           o+=l\n<<l-                            1) ;\no+=l                     *L-(l<<l-1); { ; }\nif (                    pread(3,&L,3,O+o/8)<\n2)/*                    */exit(0);  L>>=7&o;\nL%=1                     <<l; L>>8?256-L?e(\nL-1)                            ,c||\n(e(c                            =L),\nc=0)                            :( O\n+=(-I&7)*l+o+l>>3,I=L):putchar(\n    L); }int main(int l,char**o){\n                for(\n            /*          ////      */\n            open(1[o],0); ; e(I++\n            ))                    ;}\n
    Exemple grivois

    Ce code \u00e9galement issu du IOCCC est un exemple pas tr\u00e8s gracieux de ce qu'il est possible de faire en C. Il est \u00e0 noter que ce code compile parfaitement et affiche un message pas tr\u00e8s Catholique.

    main(i){for(i=160;i--;putchar(i%32?\"\u0153\u2122c\u00e6RJ\"\"\\\\J\u2022\u00e4RJ\"\"\u0153]d\u00e4\"[i/8]&1<<i%8?42:32:10));}\n
    ", "tags": ["tiret-bas"]}, {"location": "course-c/15-fundations/syntax/#fin-de-lignes-eol", "title": "Fin de lignes (EOL)", "text": "

    \u00c0 l'instar des premi\u00e8res machines \u00e0 \u00e9crire, les t\u00e9l\u00e9scripteurs poss\u00e9daient de nombreux caract\u00e8res de d\u00e9placement qui sont depuis tomb\u00e9s en d\u00e9su\u00e9tude et pr\u00eatent aujourd'hui \u00e0 confusion m\u00eame pour le plus aguerri des programmeurs. Maintenant que les ordinateurs poss\u00e8dent des \u00e9crans, la notion originale du terme retour chariot est compromise et comme il y a autant d'avis que d'ing\u00e9nieurs, les premiers PC IBM compatibles ont choisi qu'une nouvelle ligne d\u00fbt toujours se composer de deux caract\u00e8res\u2009: un retour chariot (CR) et une nouvelle ligne (LF) ou en C \\r\\n. Les premiers Macintosh d'Apple jugeant inutile de gaspiller deux caract\u00e8res pour chaque nouvelle ligne dans un fichier et ont d\u00e9cid\u00e9 d'associer le retour chariot et la nouvelle ligne dans le caract\u00e8re \\r. Enfin, les ordinateurs UNIX ont eu le m\u00eame raisonnement, mais ils ont choisi de ne garder que \\n.

    Heureusement, depuis qu'Apple a migr\u00e9 son syst\u00e8me sur une base BSD (UNIX) en 2001, les syst\u00e8mes d'exploitation modernes ont adopt\u00e9 le standard UNIX et il n'y a plus de probl\u00e8me de compatibilit\u00e9 entre les syst\u00e8mes. En sommes, il existe aujourd'hui deux types de fin de ligne\u2009:

    • LF ou \\n sur tous les ordinateurs de la plan\u00e8te \u00e0 l'exception de Windows,
    • CRLF ou \\r\\n sur les ordinateurs Windows.

    Il n'y a pas de consensus \u00e9tabli sur lesquels des deux types de fin de ligne (EOL: End Of Line) il faut utiliser, faite preuve de bon sens et surtout, soyez coh\u00e9rent.

    ", "tags": ["EOL", "CRLF"]}, {"location": "course-c/15-fundations/syntax/#mots-cles", "title": "Mots cl\u00e9s", "text": "

    Le langage de programmation C tel que d\u00e9fini par C17 comporte 44 mots cl\u00e9s\u2009:

    auto       break      case       char       const\ncontinue   default    do         double     else\nenum       extern     float      for        goto\nif         inline     int        long       register\nrestrict   return     short      signed     sizeof\nstatic     struct     switch     typedef    union\nunsigned   void       volatile   while      _Alignas\n_Alignof   _Atomic    _Bool      _Complex   _Generic\n_Imaginary _Noreturn  _Static_assert        _Thread_local\n

    Dans ce cours, l'usage des mots cl\u00e9s suivants est d\u00e9courag\u00e9, car leur utilisation pourrait pr\u00eater \u00e0 confusion ou mener \u00e0 des in\u00e9l\u00e9gances d'\u00e9criture.

    _Bool, _imaginary, auto, goto, inline, long, register, restrict, short\n

    Il n'y a donc plus que 35 mots cl\u00e9s \u00e0 conna\u00eetre pour \u00eatre un bon d\u00e9veloppeur C.

    Notons que les mots cl\u00e9s true et false ne sont pas standardis\u00e9s en C, mais ils le sont en C++.

    Ces mots cl\u00e9s font partie int\u00e9grante de la grammaire du langage et ne peuvent \u00eatre utilis\u00e9s pour identifier des variables, des fonctions ou des \u00e9tiquettes.

    Nombre de mots cl\u00e9s

    On peut se demander s'il est pr\u00e9f\u00e9rable pour un langage d'avoir plus ou moins de mots cl\u00e9s. En effet, plus il y a de mots cl\u00e9s, plus il est difficile d'apprendre le langage, mais plus il y a de mots cl\u00e9s, plus il est facile de comprendre le code des autres.

    C'est le m\u00eame dilemme entre les architectures processeur RISC et CISC. Les architectures RISC ont moins d'instructions, mais elles sont plus complexes \u00e0 utiliser, tandis que les architectures CISC ont plus d'instructions, mais elles sont plus simples \u00e0 utiliser.

    Le Perl par exemple n'a environ que 20 mots cl\u00e9s, mais il est r\u00e9put\u00e9 pour \u00eatre un langage difficile \u00e0 apprendre. Le C++ dans sa version 2020 en a plus de 84.

    ", "tags": ["true", "false"]}, {"location": "course-c/15-fundations/syntax/#identificateurs", "title": "Identificateurs", "text": "

    Un identificateur est une s\u00e9quence de caract\u00e8res repr\u00e9sentant une entit\u00e9 du programme et \u00e0 laquelle il est possible de se r\u00e9f\u00e9rer. Un identificateur est d\u00e9fini par une grammaire r\u00e9guli\u00e8re qui peut \u00eatre exprim\u00e9e comme suit\u2009:

    Grammaire d'un identificateur C

    La notation /[a-z]/ signifie que l'on peut utiliser n'importe quelle lettre minuscule de l'alphabet latin, /[A-Z]/ pour les lettres majuscules, /[0-9]/ pour les chiffres et _ pour le caract\u00e8re soulign\u00e9.

    En addition de cette grammaire, voici quelques r\u00e8gles\u2009:

    1. Un identificateur ne peut pas \u00eatre l'un des mots cl\u00e9s du langage.
    2. Les identificateurs sont sensibles \u00e0 la casse (majuscule/minuscule).
    3. Le standard C99, se r\u00e9serve l'usage de tous les identificateurs d\u00e9butant par _ suivi d'une lettre majuscule ou un autre underscore _.
    4. Le standard POSIX, se r\u00e9serve l'usage de tous les identificateurs finissant par _t.

    Expression r\u00e9guli\u00e8re

    Il est possible d'exprimer la syntaxe d'un identificateur \u00e0 l'aide de l'expression r\u00e9guli\u00e8re suivante\u2009:

    /^[a-zA-Z_][a-zA-Z0-9_]*$/

    Exercice 1\u2009: Validit\u00e9 des identificateurs

    Pour chacune des suites de caract\u00e8res ci-dessous, indiquez s'il s'agit d'un identificateur valide et utilisable en C. Justifier votre r\u00e9ponse.

    • [ ] 2_pi
    • [x] x_2
    • [x] x___3
    • [ ] x 2
    • [x] positionRobot
    • [x] piece_presente
    • [x] _commande_vanne
    • [ ] -courant_sortie
    • [x] _alarme_
    • [ ] panne#2
    • [ ] int
    • [ ] d\u00e9faillance
    • [ ] f'
    • [x] INT
    Solution

    Une excellente approche serait d'utiliser directement l'expression r\u00e9guli\u00e8re fournie et d'utiliser l'outil en ligne regex101.com.

    1. 2_pi invalide, car commence par un chiffre
    2. x_2 valide
    3. x___3 valide
    4. x 2 invalide, car comporte un espace
    5. positionRobot valide, notation camelCase
    6. piece_presente valide, notation snake_case
    7. _commande_vanne valide
    8. -courant_sortie invalide, un identificateur ne peut pas commencer par le signe -
    9. _alarme_ valide
    10. panne#2 invalide, le caract\u00e8re # n'est pas autoris\u00e9
    11. int invalide, int est un mot r\u00e9serv\u00e9 du langage
    12. d\u00e9faillance invalide, uniquement les caract\u00e8res imprimable ASCII sont autoris\u00e9s
    13. f' invalide l'apostrophe n'est pas autoris\u00e9e
    14. INT valide
    ", "tags": ["piece_presente", "_commande_vanne", "int", "_alarme_", "INT", "positionRobot", "d\u00e9faillance", "x_2", "x___3"]}, {"location": "course-c/15-fundations/syntax/#variables", "title": "Variables", "text": "

    Une variable est un symbole qui associe un nom (identificateur) \u00e0 une valeur. Comme son nom l'indique, une variable peut voir son contenu varier au cours du temps.

    Une variable est d\u00e9finie par\u2009:

    • Son nom (name), c'est-\u00e0-dire l'identificateur associ\u00e9 au symbole.
    • Son type (type), qui est la convention d'interpr\u00e9tation du contenu binaire en m\u00e9moire.
    • Sa valeur (value), qui est le contenu interpr\u00e9t\u00e9 connaissant son type.
    • Son adresse (address) qui est l'emplacement m\u00e9moire ou la repr\u00e9sentation binaire sera enregistr\u00e9e.
    • Sa port\u00e9e (scope) qui est la portion de code ou le symbole est d\u00e9finie et accessible.
    • Sa visibilit\u00e9 (visibility) qui ne peut \u00eatre que public en C.

    Pour mieux comprendre ce concept fondamental, imaginons la plage de Donnant \u00e0 Belle-\u00cele-en-Mer. Quelqu'un a \u00e9crit sur le sable, bien visible depuis la colline adjacente, le mot COIN. L'identificateur c'est Donnant, la valeur c'est COIN, le type permet de savoir comment interpr\u00e9ter la valeur. Cela peut s'agir d'une pi\u00e8ce de monnaie en anglais, du coin d'une table en fran\u00e7ais ou du lapin en n\u00e9erlandais.

    L'adresse est l'emplacement exact de la plage o\u00f9 le mot a \u00e9t\u00e9 \u00e9crit (47.32638670571\u00b0 N, 3.2363350691522\u00b0 W), la port\u00e9e est la plage de Donnant et la visibilit\u00e9 est la colline adjacente.

    On voit que sans conna\u00eetre le type de la variable, il est impossible de savoir comment interpr\u00e9ter sa valeur.

    La plage de Donnant

    En pratique l'adresse sera plut\u00f4t de la forme 0x7fffbf7f1b4c, la valeur serait plut\u00f4t 0100001101001111010010010100111000000000 et le type serait une cha\u00eene de caract\u00e8res char[].

    Variables initialis\u00e9es

    Le fait de d\u00e9clarer des variables dans en langage C implique que le logiciel doit r\u00e9aliser l'initialisation de ces variables au tout d\u00e9but de son ex\u00e9cution. De fait, on peut remarquer deux choses. Il y a les variables initialis\u00e9es \u00e0 la valeur z\u00e9ro et les variables initialis\u00e9es \u00e0 des valeurs diff\u00e9rentes de z\u00e9ro. Le compilateur regroupe en m\u00e9moire ces variables en deux cat\u00e9gories et ajoute un bout de code au d\u00e9but de votre application (qui est ex\u00e9cut\u00e9 avant le main).

    Ce code (que l'on n'a pas \u00e0 \u00e9crire) effectue les op\u00e9rations suivantes\u2009:

    • mise \u00e0 z\u00e9ro du bloc m\u00e9moire contenant les variables ayant \u00e9t\u00e9 d\u00e9clar\u00e9es avec une valeur d'initialisation \u00e0 z\u00e9ro,
    • recopie d'une zone m\u00e9moire contenant les valeurs initiales des variables ayant \u00e9t\u00e9 d\u00e9clar\u00e9es avec une valeur d'initialisation diff\u00e9rente de z\u00e9ro vers la zone de ces m\u00eames variables.

    Par ce fait, d\u00e8s que l'ex\u00e9cution du logiciel est effectu\u00e9e, on a, lors de l'ex\u00e9cution du main, des variables correctement initialis\u00e9es.

    ", "tags": ["symbole", "COIN", "variable", "Donnant", "main"]}, {"location": "course-c/15-fundations/syntax/#declaration", "title": "D\u00e9claration", "text": "

    Avant de pouvoir \u00eatre utilis\u00e9e, une variable doit \u00eatre d\u00e9clar\u00e9e afin que le compilateur puisse r\u00e9server un emplacement en m\u00e9moire pour stocker sa valeur.

    Voici quelques d\u00e9clarations valides\u2009:

    char c = '\u20ac';\nint temperature = 37;\nfloat neptune_stone_height = 376.86;\nchar message[] = \"Jarvis, il faut parfois savoir \"\n                 \"courir avant de savoir marcher.\";\n

    Il n'est pas n\u00e9cessaire d'associer une valeur initiale \u00e0 une variable, une d\u00e9claration peut se faire sans initialisation comme montr\u00e9 dans l'exemple suivant dans lequel on r\u00e9serve trois variables i, j, k.

    int i, j, k;\n

    Exercice 2\u2009: Affectation de variables

    Consid\u00e9rons les d\u00e9clarations suivantes\u2009:

    int a, b, c;\nfloat x;\n

    Notez apr\u00e8s chaque affectation, le contenu des diff\u00e9rentes variables\u2009:

    Ligne Instruction a b c x 1 a = 5; 2 b = c; 3 c = a; 4 a = a + 1; 5 x = a - ++c; 6 b = c = x; 7 x + 2. = 7.; Solution Ligne Instruction a b c x 1 a = 5; 5 ? ? ? 2 b = c; 5 ? ? ? 3 c = a; 5 ? 5 ? 4 a = a + 1; 6 ? 5 ? 5 x = a - ++c; 6 ? 6 12 6 b = c = x; 6 12 12 12 7 x + 2. = 7.; - - - -", "tags": ["initialisation", "declaration"]}, {"location": "course-c/15-fundations/syntax/#convention-de-nommage", "title": "Convention de nommage", "text": "

    Diff\u00e9rentes casses illustr\u00e9es

    Il existe autant de conventions de nommage qu'il y a de d\u00e9veloppeurs, mais un consensus majoritaire, que l'on retrouve dans d'autres langages de programmation dit que\u2009:

    • la longueur du nom d'une variable est g\u00e9n\u00e9ralement proportionnelle \u00e0 sa port\u00e9e et donc il est d'autant plus court que l'utilisation d'une variable est localis\u00e9e\u2009;
    • le nom doit \u00eatre concis et pr\u00e9cis et ne pas laisser place \u00e0 une quelconque ambigu\u00eft\u00e9\u2009;
    • le nom doit participer \u00e0 l'autodocumentation du code et permettre \u00e0 un lecteur de comprendre facilement le programme qu'il lit.

    Selon les standards adopt\u00e9s, chaque soci\u00e9t\u00e9 on trouve ceux qui pr\u00e9f\u00e8rent nommer les variables en utilisant un underscore (_) comme s\u00e9parateur et ceux qui pr\u00e9f\u00e8rent nommer une variable en utilisant des majuscules comme s\u00e9parateurs de mots.

    Conventions de nommage Convention Nom fran\u00e7ais Exemple camelcase Casse de chameau userLoginCount snakecase Casse de serpent user_login_count pascalcase Casse de Pascal UserLoginCount kebabcase Casse de kebab user-login-count

    Note

    La casse de kebab n'est pas accept\u00e9e par le standard C car les noms form\u00e9s ne sont pas des identificateurs valides. N\u00e9anmoins cette notation est beaucoup utilis\u00e9e par exemple sur GitHub.

    ", "tags": ["UserLoginCount", "userLoginCount"]}, {"location": "course-c/15-fundations/syntax/#variable-metasyntaxique", "title": "Variable m\u00e9tasyntaxique", "text": "

    Souvent lors d'exemples donn\u00e9s en programmation, on utilise des variables g\u00e9n\u00e9riques dites m\u00e9tasyntaxiques. En fran\u00e7ais les valeurs toto, titi, tata et tutu sont r\u00e9guli\u00e8rement utilis\u00e9es tandis qu'en anglais foo, bar, baz et qux sont r\u00e9guli\u00e8rement utilis\u00e9s. Les valeurs spam, ham et eggs sont quant \u00e0 elles souvent utilis\u00e9e en Python, en r\u00e9f\u00e9rence au sketch Spam des Monthy Python.

    Leur usage est conseill\u00e9 pour appuyer le cadre g\u00e9n\u00e9rique d'un exemple sans lui donner la consonance d'un probl\u00e8me plus sp\u00e9cifique.

    On trouvera une table des diff\u00e9rents noms les plus courants utilis\u00e9s dans diff\u00e9rentes langues.

    Foo, Bar, Titi et Toto

    L'origine de foo et bar remonte \u00e0 la deuxi\u00e8me guerre mondiale o\u00f9 les militaires am\u00e9ricains utilisaient ces termes pour d\u00e9signer des objets non identifi\u00e9s.

    Titi et Toto sont des personnages de bande dessin\u00e9e cr\u00e9\u00e9s par Maurice Cuvillier en 1931.

    ", "tags": ["tutu", "titi", "bar", "baz", "eggs", "ham", "spam", "qux", "foo", "toto", "tata"]}, {"location": "course-c/15-fundations/syntax/#les-constantes", "title": "Les constantes", "text": "

    Une constante par opposition \u00e0 une variable voit son contenu fixe et immuable. Il s'agit d'un espace m\u00e9moire qui ne peut \u00eatre modifi\u00e9 apr\u00e8s son initialisation. Formellement, une constante se d\u00e9clare comme une variable, mais pr\u00e9fix\u00e9e du mot-cl\u00e9 const.

    const double scale_factor = 12.67;\n

    Une constante est principalement utilis\u00e9e pour indiquer au d\u00e9veloppeur que la valeur ne doit pas \u00eatre modifi\u00e9e. Le compilateur peut \u00e9galement s'en servir pour mieux optimiser le code et donc am\u00e9liorer les performances d'ex\u00e9cution.

    Avertissement

    Il ne faut pas confondre la constante qui est une variable immuable, stock\u00e9e en m\u00e9moire et une macro qui appartient au pr\u00e9processeur. Sur certaines plateformes, le fichier d'en-t\u00eate math.h d\u00e9finit par exemple la constante M_PI sous forme de macro.

    #define M_PI 3.14159265358979323846\n

    Cette m\u00eame constante peut \u00eatre d\u00e9finie comme une variable constante\u2009:

    const double pi = 3.14159265358979323846;\n

    En r\u00e9sum\u00e9, les constantes sont utilis\u00e9es pour\u2009:

    • \u00c9viter les erreurs de programmation en \u00e9vitant de modifier une valeur qui ne devrait pas l'\u00eatre.
    • Indiquer au compilateur que la valeur ne changera pas et qu'il peut optimiser le code en cons\u00e9quence.
    • Indiquer au d\u00e9veloppeur que la valeur ne sera pas modifi\u00e9e plus tard dans le programme.
    ", "tags": ["M_PI", "constante", "const", "math.h"]}, {"location": "course-c/15-fundations/syntax/#constantes-litterales", "title": "Constantes litt\u00e9rales", "text": "

    Les constantes litt\u00e9rales repr\u00e9sentent des grandeurs scalaires num\u00e9riques ou de caract\u00e8res et initialis\u00e9es lors de la phase de compilation.

    En effet, lorsque l'on veut saisir un nombre, on ne veut pas que le compilateur la comprenne comme un identificateur, mais bien comme une valeur num\u00e9rique. C'est d'ailleurs la raison pour laquelle un identificateur ne peut pas commencer par un chiffre.

    Les constantes litt\u00e9rales sont g\u00e9n\u00e9ralement identifi\u00e9es avec des pr\u00e9fixes et des suffixes pour indiquer leur nature. Voici quelques exemples\u2009:

    6      // Le nombre d'heures sur l'horloge du Palais du Quirinal \u00e0 Rome\n12u    // Grandeur non sign\u00e9e\n6l     // Grandeur enti\u00e8re sign\u00e9e cod\u00e9e sur un entier long\n42ul   // Grandeur enti\u00e8re non sign\u00e9e cod\u00e9e sur un entier long\n010    // Grandeur octale valant 8 en d\u00e9cimal\n0xa    // Grandeur hexad\u00e9cimale valant 10 en d\u00e9cimal\n0b111  // Grandeur binaire valant 7 en d\u00e9cimal\n1.     // Grandeur r\u00e9elle exprim\u00e9e en virgule flottante\n'0'    // Grandeur caract\u00e8re valant 48 en d\u00e9cimal\n2e3    // Grandeur r\u00e9elle exprim\u00e9e en notation scientifique\n

    Nous l'avons vu plus haut, le type d'une variable est important pour d\u00e9terminer comment une valeur est stock\u00e9e en m\u00e9moire.

    Comme vu dans le chapitre sur la num\u00e9ration, les valeurs num\u00e9riques peuvent \u00eatre stock\u00e9es en m\u00e9moire de diff\u00e9rentes mani\u00e8res. Ainsi, une valeur 48 peut \u00eatre stock\u00e9e sur un octet, un mot de 16 bits, un mot de 32 bits ou un mot de 64 bits. De plus, la valeur peut faire r\u00e9f\u00e9rence au caract\u00e8re 0 en ASCII, mais aussi au nombre 72 s'il est exprim\u00e9 en hexad\u00e9cimal.

    On utilisera un pr\u00e9fixe devant un nombre 0x pour indiquer qu'il est en hexad\u00e9cimal, 0b pour indiquer qu'il est en binaire et 0 pour indiquer qu'il est en octal. Sans pr\u00e9fixe il s'agit d'un nombre d\u00e9cimal (base 10).

    On utilisera un suffixe u pour indiquer que le nombre est non sign\u00e9 (n'admettant pas de valeurs n\u00e9gatives) et l pour indiquer qu'il est long ou ll pour indiquer qu'il est tr\u00e8s long.

    Quant aux guillemets simples ', ils sont utilis\u00e9s pour d\u00e9limiter un caract\u00e8re de la table ASCII.

    Expressions r\u00e9guli\u00e8res

    Il est plus facile pour un informaticien de comprendre la syntaxe des constantes litt\u00e9rales en utilisant des expressions r\u00e9guli\u00e8res. Voici les expressions r\u00e9guli\u00e8res qui d\u00e9finissent les diff\u00e9rentes constantes litt\u00e9rales\u2009:

    Type Expression r\u00e9guli\u00e8re Exemple Nombre sign\u00e9 /[1-9][0-9]*/ 42 Nombre non sign\u00e9 /[1-9][0-9]*u/ 42u Nombre hexad\u00e9cimal /0x[0-9a-fA-F]+/ 0x2a Nombre octal /0[0-7]+/ 052

    Vous pouvez essayer de les tester sur regex101.com.

    Exercice 3\u2009: Constances litt\u00e9rales

    Pour les entr\u00e9es suivantes, indiquez lesquelles sont correctes.

    • [x] 12.3
    • [x] 12E03
    • [x] 12u
    • [ ] 12.0u
    • [ ] 1L
    • [ ] 1.0L
    • [x] .9
    • [x] 9.
    • [ ] .
    • [x] 0x33
    • [ ] 0xefg
    • [x] 0xef
    • [x] 0xeF
    • [ ] 0x0.2
    • [x] 09
    • [x] 02

    La notation scientifique, aussi appel\u00e9e notation exponentielle, est une mani\u00e8re d'\u00e9crire des nombres tr\u00e8s grands ou tr\u00e8s petits de mani\u00e8re plus compacte. Par exemple, 1.23e3 est \u00e9quivalent \u00e0 1230. et 1.23e-3 est \u00e9quivalent \u00e0 0.00123. Le caract\u00e8re e est utilis\u00e9 pour indiquer la puissance de 10 par laquelle le nombre doit \u00eatre multipli\u00e9. Il tire probablement son origine du Fortran qui l'utilisait d\u00e9j\u00e0 en 1957.

    Pas Euler

    Il ne faut pas confondre l'exponentiation avec le nombre d'Euler (2.71828...) e avec la notation scientifique e qui est utilis\u00e9e pour indiquer une puissance de 10.

    La notation scientifique est un double

    En C, la notation scientifique est toujours un nombre en virgule flottante de type double. Ainsi, 1e3 est un double et non un int. Ne prenez donc pas l'habitude d'\u00e9crire int i = 1e3;, mais plut\u00f4t int i = 1000;.

    ", "tags": ["notation-exponentielle", "double", "int", "notation-scientifique"]}, {"location": "course-c/15-fundations/syntax/#operateur-daffectation", "title": "Op\u00e9rateur d'affectation", "text": "

    Dans les exemples ci-dessus, on utilise l'op\u00e9rateur d'affectation pour associer une valeur \u00e0 une variable. Historiquement, et malheureusement, le symbole choisi pour cet op\u00e9rateur est le signe \u00e9gal = or, l'\u00e9galit\u00e9 est une notion math\u00e9matique qui n'est en aucun cas reli\u00e9e \u00e0 l'affectation.

    Pour mieux saisir la nuance, consid\u00e9rons le programme suivant\u2009:

    a = 42;\na = b;\n

    Math\u00e9matiquement, la valeur de b devrait \u00eatre \u00e9gale \u00e0 42 ce qui n'est pas le cas en C o\u00f9 il faut lire, s\u00e9quentiellement l'ex\u00e9cution du code, car oui, C est un langage imp\u00e9ratif. Ainsi, dans l'ordre, on lit\u2009:

    1. J'assigne la valeur 42 \u00e0 la variable symbolis\u00e9e par a
    2. Puis, j'assigne la valeur de la variable b au contenu de a.

    Comme on ne conna\u00eet pas la valeur de b, avec cet exemple, on ne peut pas conna\u00eetre la valeur de a. Certains langages de programmation ont \u00e9t\u00e9 sensibilis\u00e9s \u00e0 l'importance de cette distinction et dans les langages F#, OCaml, R ou S, l'op\u00e9rateur d'affectation est <- et une affectation pourrait s'\u00e9crire par exemple\u2009: a <- 42 ou 42 -> a.

    En C, l'op\u00e9rateur d'\u00e9galit\u00e9 que nous verrons plus loin s'\u00e9crit == (deux = concat\u00e9n\u00e9s).

    Remarquez ici que l'op\u00e9rateur d'affectation de C agit toujours de droite \u00e0 gauche c'est-\u00e0-dire que la valeur \u00e0 droite de l'op\u00e9rateur est affect\u00e9e \u00e0 la variable situ\u00e9e \u00e0 gauche de l'op\u00e9rateur. S'agissant d'un op\u00e9rateur il est possible de cha\u00eener les op\u00e9rations, comme on le ferait avec l'op\u00e9rateur + et dans l'exemple suivant il faut lire que 42 est assign\u00e9 \u00e0 c, que la valeur de c est ensuite assign\u00e9 \u00e0 b et enfin la valeur de b est assign\u00e9e \u00e0 a. Nous verrons plus tard comment l'ordre des op\u00e9rations et l'associativit\u00e9 de chaque op\u00e9rateur.

    a = b = c = 42;\n

    Exercice 4\u2009: Affectations simples

    Donnez les valeurs de x, n, p apr\u00e8s l'ex\u00e9cution des instructions ci-dessous\u2009:

    float x;\nint n, p;\n\np = 2;\nx = 15 / p;\nn = x + 0.5;\n
    Solution
    p \u2261 2\nx \u2261 7\nn \u2261 7\n

    Exercice 5\u2009: Trop d'\u00e9galit\u00e9s

    On consid\u00e8re les d\u00e9clarations suivantes\u2009:

    int i, j, k;\n

    Donnez les valeurs des variables i, j et k apr\u00e8s l'ex\u00e9cution de chacune des expressions ci-dessous. Qu'en pensez-vous\u2009?

    /* 1 */ i = (k = 2) + (j = 3);\n/* 2 */ i = (k = 2) + (j = 2) + j * 3 + k * 4;\n/* 3 */ i = (i = 3) + (k = 2) + (j = i + 1) + (k = j + 2) + (j = k - 1);\n
    Solution

    Selon la table de priorit\u00e9 des op\u00e9rateurs, on note\u2009:

    • () priorit\u00e9 1 associativit\u00e9 \u00e0 droite
    • * priorit\u00e9 3 associativit\u00e9 \u00e0 gauche
    • + priorit\u00e9 4 associativit\u00e9 \u00e0 droite
    • = priorit\u00e9 14 associativit\u00e9 \u00e0 gauche

    En revanche rien n'est dit sur les point de s\u00e9quences <https://en.wikipedia.org/wiki/Sequence_point>__. L'op\u00e9rateur d'affectation n'est pas un point de s\u00e9quence, autrement dit le standard C99 (Annexe C) ne d\u00e9finit pas l'ordre dans lequel les assignations sont effectu\u00e9es.

    Ainsi, seul le premier point poss\u00e8de une solution, les deux autres sont ind\u00e9termin\u00e9s

    1. i = (k = 2) + (j = 3)

      • i = 5
      • j = 3
      • k = 2
    2. i = (k = 2) + (j = 2) + j * 3 + k * 4

      • R\u00e9sultat ind\u00e9termin\u00e9
    3. i = (i = 3) + (k = 2) + (j = i + 1) + (k = j + 2) + (j = k - 1)

      • R\u00e9sultat ind\u00e9termin\u00e9
    "}, {"location": "course-c/15-fundations/syntax/#espaces-de-noms", "title": "Espaces de noms", "text": "

    En C, il est possible d'utiliser le m\u00eame identificateur pour autant qu'il n'appartient pas au m\u00eame espace de nom. Il existe en C plusieurs espaces de noms\u2009:

    • \u00e9tiquettes utilis\u00e9es pour l'instruction goto;
    • tag de structures, d'\u00e9num\u00e9rations et d'union\u2009;
    • membres de structures, d'\u00e9num\u00e9rations et d'union\u2009;
    • identificateurs de variable ou fonctions.

    Ces espaces de noms sont ind\u00e9pendants les uns des autres, il est donc possible d'utiliser le m\u00eame nom sans conflit. Par exemple\u2009:

    typedef struct x {  // Espace de nom des structures\n    int x;  // Membre de la structure point\n} x;  // Espace de nom des types\n\nint main() {\n    x x = {.x = 42};  // x est une variable de type x\n}\n
    ", "tags": ["goto"]}, {"location": "course-c/15-fundations/syntax/#commentaires", "title": "Commentaires", "text": "

    Comme en fran\u00e7ais et ainsi qu'illustr\u00e9 par la figure suivante, il est possible d'annoter un programme avec des commentaires. Les commentaires n'ont pas d'incidence sur le fonctionnement d'un programme et ne peuvent \u00eatre lu que par le d\u00e9veloppeur qui poss\u00e8de le code source. Par ailleurs, comme nous l'avons vu en introduction, le pr\u00e9processeur C supprime les commentaires du code source avant la compilation.

    Les carafes dans la Vivonne

    Il existe deux mani\u00e8res d'\u00e9crire un commentaire en C, les commentaires de lignes apparus avec le C++ et C99, ainsi que les commentaires de blocs. Les commentaires de blocs sont plus anciens et sont compatibles avec les versions ant\u00e9rieures du langage.

    Commentaire de ligneCommentaire de bloc
    // This is a single line comment.\n
    /* This is a\n   Multi-line comment */\n

    Il est important de rappeler que les commentaires sont trait\u00e9s par le pr\u00e9processeur, aussi ils n'influencent pas le fonctionnement d'un programme, mais seulement sa lecture. Rappelons qu'un code est plus souvent lu qu'\u00e9crit, car on ne l'\u00e9crit qu'une seule fois, mais comme tout d\u00e9veloppement doit \u00eatre si possible r\u00e9utilisable, il est plus probable qu'il soit lu part d'autres d\u00e9veloppeurs.

    En cons\u00e9quence, il est important de clarifier toute zone d'ombre lorsque l'on s'\u00e9loigne des consensus \u00e9tablis, ou lorsque le code seul n'est pas suffisant pour bien comprendre son fonctionnement.

    D'une fa\u00e7on g\u00e9n\u00e9rale, les commentaires servent \u00e0 expliquer pourquoi et non comment. Un bon programme devrait pouvoir se passer de commentaires, mais un programme sans commentaires n'est pas n\u00e9cessairement un bon programme.

    Note

    Il est pr\u00e9f\u00e9rable d'utiliser le commentaire de ligne d\u00e8s que possible, car d'une part il y a moins de caract\u00e8res \u00e0 \u00e9crire, mais surtout les commentaires de blocs ne sont pas imbriquables (nestable).

    /*\n// Autoris\u00e9\n*/\n
    /*\n/* Interdit */\n*/\n

    Les commentaires de blocs peuvent \u00eatre utilis\u00e9s pour documenter une fonction ou un bloc de code, mais \u00e9galement pour ins\u00e9rer un commentaire \u00e0 l'int\u00e9rieur d'une ligne\u2009:

    int deep_throught /* Name of the computer */ = 42; // The answer\n
    "}, {"location": "course-c/15-fundations/syntax/#commenter-du-code", "title": "Commenter du code\u2009?", "text": "

    Lorsque vous d\u00e9veloppez, vous avez souvent besoin de d\u00e9sactiver des portions de code pour des raisons de d\u00e9bogage ou de test. Il est tentant de commenter ces portions de code plut\u00f4t que de les supprimer. N\u00e9anmoins une r\u00e8gle \u00e0 retenir est que l'on ne commente jamais des portions de code, et ce pour plusieurs raisons\u2009:

    1. Les outils de refactoring ne pourront pas acc\u00e9der du code comment\u00e9.
    2. La syntaxe ne pourra plus \u00eatre v\u00e9rifi\u00e9e par l'IDE.
    3. Les outils de gestion de configuration (e.g. Git) devraient \u00eatre utilis\u00e9s \u00e0 cette fin.

    Si d'aventure vous souhaitez quand m\u00eame exclure temporairement du code de la compilation, il est recommand\u00e9 d'utiliser la directive de pr\u00e9processeur suivante, et n'oubliez pas d'expliquer pourquoi vous avez souhait\u00e9 d\u00e9sactiver cette portion de code.

    #if 0 // TODO: Check if divisor could still be null at this point.\nif (divisor == 0) {\n    return -1; // Error\n}\n#endif\n
    "}, {"location": "course-c/15-fundations/syntax/#quelques-conseils", "title": "Quelques conseils", "text": "

    D'une mani\u00e8re g\u00e9n\u00e9rale l'utilisation des commentaires ne devrait pas \u00eatre utilis\u00e9e pour\u2009:

    • d\u00e9sactiver temporairement une portion de code sans l'effacer\u2009;
    • expliquer le comment du fonctionnement du code\u2009;
    • faire dans le dithyrambique pompeux et notarial, des phrases \u00e0 rallonge bien trop romanesques\u2009;
    • cr\u00e9er de jolies s\u00e9parations telles que /*************************/.

    Il n'est pas rare de voir au d\u00e9but d'un fichier un commentaire de la forme suivante\u2009:

    /**\n * @brief Short description of the translation unit.\n *\n * @author John Doe <john@doe.com>\n * @date 2021-09-01\n * @file main.c\n *\n * Long description of the translation unit.\n *\n * NOTE: Important notes about this code\n * TODO: Things to fix...\n */\n

    SSOT

    Vous verrez souvent, trop souvent le nom de l'auteur et du fichiers dans les en-t\u00eates. Ce n'est pas une bonne pratique si vous utilisez Git, car ces informations sont d\u00e9j\u00e0 pr\u00e9sentes dans les m\u00e9tadonn\u00e9es du fichier.

    Voici un exemple de ce qu'il ne faut pas faire\u2009:

    Pas bienBien
    /*****************************************************\n                                           _..._\n _   _    _    ____    _                .'     '.      _\n| \\ | |  / \\  / ___|  / \\              /    .-\"\"-\\   _/ \\\n|  \\| | / _ \\ \\___ \\ / _ \\          .-|   /:.   |  |   |\n| |\\  |/ ___ \\ ___) / ___ \\         |  \\  |:.   /.-'-./\n|_| \\_/_/   \\_\\____/_/   \\_\\        | .-'-;:__.'    =/\n                                  .'=  *=|NASA _.='\n                                 /   _.  |    ;\n                                ;-.-'|    \\   |\n                               /   | \\    _\\  _\\\n                               \\__/'._;.  ==' ==\\\n                                        \\    \\   |\n                                 jgs    /    /   /\n                                        /-._/-._/\nNational Aeronautics and Space          \\   `\\  \\\nAdministration.                          `-._/._/\n\n@file appolo11.c\n@brief Launch control module\n@author Margaret Hamilton\n@date 1969-07-16\n\nThis module is responsible for the launch of the Apollo\n11 mission. It is a critical part of the mission and\nshould not be modified.\n*******************************************************/\n
    /**\n * Launch control module.\n *\n * This module is responsible for the launch of the Apollo\n * 11 mission. It is a critical part of the mission and\n * should not be modified.\n */\n

    Le format des commentaires est par essence libre au d\u00e9veloppeur, mais il est g\u00e9n\u00e9ralement souhait\u00e9 que\u2009: Les commentaires soient concis et pr\u00e9cis et qu'ils soient \u00e9crits en anglais.

    Exercice 6\u2009: Verbosit\u00e9

    Comment r\u00e9cririez-vous ce programme\u2009?

    for (register unsigned int the_element_index = 0;\n    the_element_index < number_of_elements; the_element_index += 1)\n    array_of_elements[the_element_index] =  the_element_index;\n
    Solution

    Une r\u00e8gle de programmation\u2009: le nom identifieurs doit \u00eatre proportionnel \u00e0 leur contexte. Plus le contexte de la variable est r\u00e9duit, plus le nom peut \u00eatre court. Le m\u00eame programme pourrait \u00eatre \u00e9crit comme suit\u2009:

    for (size_t i; i < nelems; i++)\n    elem[i] = i;\n

    Un consensus assez bien \u00e9tabli est qu'une variable commen\u00e7ant par n peut signifier number of.

    "}, {"location": "course-c/20-composite-types/", "title": "Types Composites", "text": "

    Un type composite est un type de donn\u00e9es qui est construit \u00e0 partir d'autres types de donn\u00e9es plus simples ou primitifs (comme int, char, etc.). Ces types permettent de regrouper plusieurs \u00e9l\u00e9ments de donn\u00e9es sous une seule entit\u00e9, ce qui est essentiel pour organiser des structures de donn\u00e9es plus complexes dans les programmes. En C on retrouve les types composites suivants\u2009:

    Les tableaux

    Un tableau est une collection d'\u00e9l\u00e9ments de m\u00eame type organis\u00e9e de mani\u00e8re continu\u00eb en m\u00e9moire.

    Les structures (struct)

    Une structure est un type composite qui regroupe des \u00e9l\u00e9ments de donn\u00e9es, appel\u00e9s membres qui peuvent \u00eatre de types diff\u00e9rents.

    Les unions

    Une union est similaire \u00e0 une structure, mais tous les membres partagent la m\u00eame zone m\u00e9moire. Cela signifie qu'une union ne peut stocker qu'une seule valeur \u00e0 la fois parmi ses membres.

    Les \u00e9num\u00e9rations (enum)

    Une \u00e9num\u00e9ration est un type composite qui associe des noms symboliques \u00e0 des valeurs int\u00e9grales. Bien que techniquement les \u00e9num\u00e9rations soient des types scalaires, elles sont souvent consid\u00e9r\u00e9es dans le cadre des types composites en raison de leur capacit\u00e9 \u00e0 repr\u00e9senter des ensembles de valeurs possibles.

    Les cha\u00eenes de caract\u00e8res

    Les cha\u00eenes de caract\u00e8res sont techniquement des tableaux de caract\u00e8res (char), mais elles peuvent \u00eatre consid\u00e9r\u00e9es comme un type composite en raison de la mani\u00e8re dont elles sont manipul\u00e9es et utilis\u00e9es pour repr\u00e9senter du texte.

    En combinant des tableaux, des structures avec des pointeurs on peut cr\u00e9er des types composites encore plus complexes que l'on appellera des conteneurs de donn\u00e9es.

    ", "tags": ["char", "int"]}, {"location": "course-c/20-composite-types/arrays/", "title": "Tableaux", "text": "

    Les tableaux (arrays) repr\u00e9sentent une s\u00e9quence finie d'\u00e9l\u00e9ments d'un type donn\u00e9 que l'on peut acc\u00e9der par leur position (indice) dans la s\u00e9quence. Un tableau est par cons\u00e9quent une liste index\u00e9e de variables du m\u00eame type.

    Un exemple typique d'utilisation d'un tableau est le Crible d'\u00c9ratosth\u00e8ne qui permet de trouver tous les nombres premiers inf\u00e9rieurs \u00e0 un entier donn\u00e9. Dans cet algorithme, un tableau de bool\u00e9ens est utilis\u00e9 pour marquer les nombres qui ne sont pas premiers. Le code est en 4 parties. D'abord la capture d'une valeur donn\u00e9e par l'utilisateur stock\u00e9e dans n, puis l'initialisation des valeurs du tableau \u00e0 true avec une boucle, suivi de l'algorithme du crible qui contient deux boucles imbriqu\u00e9es et enfin l'affichage du r\u00e9sultat. Notons que la plus ancienne r\u00e9f\u00e9rence connue au crible (en grec ancien\u2009: \u03ba\u03cc\u03c3\u03ba\u03b9\u03bd\u03bf\u03bd \u1f18\u03c1\u03b1\u03c4\u03bf\u03c3\u03b8\u03ad\u03bd\u03bf\u03c5\u03c2, k\u00f3skinon Eratosth\u00e9nous) se trouve dans l'Introduction \u00e0 l'arithm\u00e9tique de Nicomachus de G\u00e9rasa, un ouvrage du d\u00e9but du II\u1d49 si\u00e8cle de notre \u00e8re, qui l'attribue \u00e0 \u00c9ratosth\u00e8ne de Cyr\u00e8ne, un math\u00e9maticien grec du III\u1d49 si\u00e8cle avant J.-C., bien qu'il d\u00e9crive le criblage par les nombres impairs plut\u00f4t que par les nombres premiers.

    #define MAX 1000\n\nint main(int argc, char *argv[]) {\n   if (argc != 2) return -1;\n   int n = atoi(argv[1]);\n   if (n > MAX) return -2;\n\n   // At start, all numbers are prime numbers\n   bool primes[MAX];\n   for (int i = 0; i <= n; i++) primes[i] = true;\n\n   // \u00c9ratosth\u00e8ne sieve algorithm\n   primes[0] = primes[1] = false;  // 0 et 1 are not prime numbers\n   for (int p = 2; p <= sqrt(n); p++)\n      if (primes[p])\n         for (int i = p * p; i <= n; i += p) primes[i] = false;\n\n   // Display prime numbers\n   for (int i = 2; i <= n; i++)\n      if (primes[i]) printf(\"%d \", i);\n   printf(\"\\n\");\n}\n

    L'op\u00e9rateur crochet [] est utilis\u00e9 \u00e0 la fois pour le d\u00e9r\u00e9f\u00e9rencement (acc\u00e8s \u00e0 un indice du tableau) et pour l'assignation d'une taille \u00e0 un tableau\u2009:

    La d\u00e9claration d'un tableau d'entiers de dix \u00e9l\u00e9ments s'\u00e9crit de la fa\u00e7on suivante\u2009:

    int array[10];\n

    Par la suite il est possible d'acc\u00e9der aux diff\u00e9rents \u00e9l\u00e9ments ici l'\u00e9l\u00e9ment 1 et 3 (deuxi\u00e8me et quatri\u00e8me position du tableau) :

    array[1];\narray[5 - 2];\n

    Imaginons un tableau de int16_t de 5 \u00e9l\u00e9ments. En m\u00e9moire ce tableau est une succession de 10 bytes (5 \u00e9l\u00e9ments de 2 bytes chacun).

    Tableau en m\u00e9moire

    int16_t array[5] = {0x0201, 0x0403, 0x0605, 0x0807, 0x0A09};\n

    Rappelez-vous que les entiers sont stock\u00e9s en m\u00e9moire en little-endian, c'est-\u00e0-dire que l'octet de poids faible est stock\u00e9 en premier. Ainsi, l'entier 0x0201 est stock\u00e9 en m\u00e9moire 0x01 puis 0x02. Lorsque vous acc\u00e9dez \u00e0 un \u00e9l\u00e9ment du tableau. Chaque \u00e9l\u00e9ment en m\u00e9moire poss\u00e8de une adresse qui lui est propre. N\u00e9anmoins lorsque l'on se r\u00e9f\u00e8re au tableau dans son ensemble (ici array), c'est l'adresse du premier \u00e9l\u00e9ment qui est retourn\u00e9e soit 0xffacb10.

    Comme le tableau est de type int16_t, chaque \u00e9l\u00e9ment est de taille 2 bytes, donc lorsque l'on acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 3, une arithm\u00e9tique sur les adresse est effectu\u00e9e\u2009:

    \\[ \\begin{aligned} \\text{array} & = 0xffacb10 \\\\ \\text{array[3]} & = 0xffacb10 + 3 \\times 2 = 0xffacb16 \\end{aligned} \\]

    L'op\u00e9rateur sizeof qui permet de retourner la taille d'une structure de donn\u00e9e en m\u00e9moire est tr\u00e8s utile pour les tableaux. Cependant, cet op\u00e9rateur retourne la taille du tableau en bytes, et non le nombre d'\u00e9l\u00e9ments qui le compose. Dans l'exemple suivant sizeof(array) retourne \\(5\\cdot2 = 40\\) tandis que sizeof(array[0]) retourne la taille d'un seul \u00e9l\u00e9ment \\(2\\); et donc, sizeof(array) / sizeof(array[0]) est le nombre d'\u00e9l\u00e9ments de ce tableau, soit 5.

    size_t length = sizeof(array) / sizeof(array[0]);\nassert (length == 5);\n

    L'indice z\u00e9ro

    L'index d'un tableau commence toujours \u00e0 z\u00e9ro et par cons\u00e9quent l'index maximum d'un tableau de 5 \u00e9l\u00e9ments sera 4. Il est donc fr\u00e9quent dans une boucle d'utiliser < et non <=:

    for(size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n/* ... */\n}\n

    Nous le verrons plus tard lorsque nous parlerons des pointeurs, mais un tableau est en r\u00e9alit\u00e9 un pointeur, c'est-\u00e0-dire la position m\u00e9moire \u00e0 laquelle se trouvent les \u00e9l\u00e9ments du tableau (ici l'adresse 0xffacb10). Ce qu'il est important de retenir c'est que lorsqu'un tableau est pass\u00e9 \u00e0 une fonction comme dans l'exemple suivant, ce n'est pas l'int\u00e9gralit\u00e9 des donn\u00e9es du tableau qui sont copi\u00e9es sur la pile, mais seulement l'adresse de ce dernier. On dit que le tableau est pass\u00e9 par r\u00e9f\u00e9rence.

    Une preuve est que le contenu du tableau peut \u00eatre modifi\u00e9 \u00e0 distance\u2009:

    void function(int i[5]) {\n   i[2] = 12\n}\n\nint main(void) {\n   int array[5] = {0};\n   function(array);\n   assert(array[2] == 12);\n}\n

    Un fait remarquable est que l'op\u00e9rateur [] est commutatif. En effet, l'op\u00e9rateur crochet est un sucre syntaxique d\u00e9finit comme\u2009:

    a[b] == *(a + b)\n

    Et cela fonctionne de la m\u00eame mani\u00e8re avec les tableaux \u00e0 plusieurs dimensions\u2009:

    a[1][2] == *(*(a + 1) + 2))\n

    Pour r\u00e9sumer, un tableau permet de regrouper dans un m\u00eame conteneur une liste d'\u00e9l\u00e9ments du m\u00eame type. Il est possible d'acc\u00e9der \u00e0 ces \u00e9l\u00e9ments par leur indice, et il est possible de passer un tableau \u00e0 une fonction par r\u00e9f\u00e9rence.

    La taille est une constante litt\u00e9rale

    La taille d'un tableau doit \u00eatre une constante litt\u00e9rale. Il n'est pas possible de d\u00e9clarer un tableau avec une taille variable. Par exemple, l'\u00e9criture suivante est incorrecte\u2009:

    size_t size = 10;\nint array[size];\n

    En pratique, cet exemple va compiler mais en utilisant une fonctionnalit\u00e9 nomm\u00e9e VLA (Variable Length Array) qui n'est pas recommand\u00e9e. Les tableaux de taille variable sont une source de bugs potentiels et ne sont pas support\u00e9s par tous les compilateurs.

    Limites

    En C il n'y a pas de v\u00e9rification de limites lors de l'acc\u00e8s \u00e0 un tableau. Il est donc possible d'acc\u00e9der \u00e0 un \u00e9l\u00e9ment qui n'existe pas. Par exemple, si un tableau de 5 \u00e9l\u00e9ments est d\u00e9clar\u00e9, il est possible d'acc\u00e9der \u00e0 l'\u00e9l\u00e9ment 6 sans g\u00e9n\u00e9rer d'erreur. Cela peut \u00eatre source de bugs tr\u00e8s difficiles \u00e0 d\u00e9tecter.

    int array[5] = {0};\narray[6] = 42; // Pas d'erreur !\n

    C'est au programmeur de s'assurer que les indices utilis\u00e9s sont valides. Le plus souvent, ce type d'erreur ne m\u00e8ne pas \u00e0 un crash imm\u00e9diat, mais \u00e0 un comportement ind\u00e9termin\u00e9 car l'acc\u00e8s m\u00e9moire est en dehors de la zone allou\u00e9e au tableau et correspond \u00e0 une zone m\u00e9moire qui peut \u00eatre utilis\u00e9e par une autre variable. C'est ce que l'on appelle un buffer overflow. Dans l'exemple suivant, les tableaux a et b sont d\u00e9clar\u00e9s l'un apr\u00e8s l'autre sur la pile, et l'\u00e9criture en dehors de a modifie la valeur de b :

    int main() {\n    int a[4] = {0};\n    int b[4] = {0};\n    a[4] = 42;\n    printf(\"%d\\n\", b[0]); // Affiche 42 !\n}\n
    ", "tags": ["true", "int16_t", "array", "sizeof"]}, {"location": "course-c/20-composite-types/arrays/#initialisation", "title": "Initialisation", "text": "

    Lors de la d\u00e9claration d'un tableau, le compilateur r\u00e9serve un espace m\u00e9moire de la taille suffisante pour contenir tous les \u00e9l\u00e9ments du tableau. La d\u00e9claration suivante r\u00e9serve un espace pour 6 entiers, chacun d'une taille de 32-bits (4 bytes). L'espace m\u00e9moire r\u00e9serv\u00e9 est donc de 24 bytes.

    int32_t even[6];\n

    Compte tenu de cette d\u00e9claration, il n'est pas possible de conna\u00eetre la valeur des diff\u00e9rents \u00e9l\u00e9ments car ce tableau n'a pas \u00e9t\u00e9 initialis\u00e9 et le contenu m\u00e9moire est non pr\u00e9dictible puisqu'il peut contenir les vestiges d'un ancien programme ayant tant\u00f4t r\u00e9sid\u00e9 dans cette r\u00e9gion m\u00e9moire.

    Pour d\u00e9finir un contenu, il est n\u00e9cessaire d'initialiser le tableau en affectant une valeur \u00e0 chaque \u00e9l\u00e9ment comme suit\u2009:

    int32_t sequence[6];\nsequence[0] = 4;\nsequence[1] = 8;\nsequence[2] = 15;\nsequence[3] = 16;\nsequence[4] = 23;\nsequence[5] = 42;\n

    Cette \u00e9criture n'est certainement pas la plus optimis\u00e9e, car l'initialisation du tableau n'est pas r\u00e9alis\u00e9e \u00e0 la compilation, mais \u00e0 l'ex\u00e9cution du programme\u2009; et ce sera pas moins de six op\u00e9rations qui seront n\u00e9cessaires \u00e0 l'initialiser. En pratique on utilise la notation par accolades {} pour initialiser un tableau\u2009:

    int32_t sequence[6] = {4, 8, 15, 16, 23, 42};\n

    Ici, les accolades ne forment pas un bloc de code, mais une liste d'\u00e9l\u00e9ments, chacun s\u00e9par\u00e9 par une virgule.

    Dans cette derni\u00e8re \u00e9criture, on notera une redondance d'information. La liste d'initialisation {4, 8, 15, 16, 23, 42} comporte six \u00e9l\u00e9ments et le tableau est d\u00e9clar\u00e9 avec six \u00e9l\u00e9ments [6]. Pour \u00e9viter une double source de v\u00e9rit\u00e9, il est ici possible d'omettre la taille du tableau\u2009:

    int32_t sequence[] = {4, 8, 15, 16, 23, 42};\n

    Le compilateur peut inf\u00e9rer la taille du tableau en fonction du nombre d'\u00e9l\u00e9ments de la liste d'initialisation. N\u00e9anmoins, une liste d'initialisation n'initialise pas n\u00e9cessairement tous les \u00e9l\u00e9ments du tableau. Il est possible par exemple de d\u00e9clarer un tableau de 100 \u00e9l\u00e9ments o\u00f9 seul les premiers sont initialis\u00e9s\u2009:

    int32_t sequence[100] = {4, 8, 15, 16, 23, 42 /* le reste vaudra z\u00e9ro */ };\n

    Dans ce cas, les \u00e9l\u00e9ments 6 \u00e0 99 seront initialis\u00e9s \u00e0 z\u00e9ro. Pour s'en donner la preuve, observons le code assembleur g\u00e9n\u00e9r\u00e9 par le compilateur\u2009:

    mov     rbp, rsp\nsub     rsp, 280\nlea     rdx, [rbp-400]    ; adresse du tableau\nmov     eax, 0            ; valeur \u00e0 initialiser\nmov     ecx, 50           ; nombre de mots de 8 bytes \u00e0 initialiser\nmov     rdi, rdx          ; destination\nrep stosq                 ; r\u00e9p\u00e8te l'op\u00e9ration de stockage 50 fois\n\n; Tous les \u00e9l\u00e9ments du tableau sont maintenant initialis\u00e9s \u00e0 z\u00e9ro.\n; Ensuite, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s explicitement.\n\nmov     DWORD PTR [rbp-400], 4\nmov     DWORD PTR [rbp-396], 8\nmov     DWORD PTR [rbp-392], 15\nmov     DWORD PTR [rbp-388], 16\nmov     DWORD PTR [rbp-384], 23\nmov     DWORD PTR [rbp-380], 42\n

    Donc, lors de l'initialisation d'un tableau de 100 \u00e9l\u00e9ments dans une fonction on peut noter\u2009:

    1. Le tableau est d\u00e9clar\u00e9 sur la pile.
    2. Tous les \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro.
    3. Puis, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s explicitement.

    Le langage C permet \u00e9galement d'initialiser un tableau de fa\u00e7on partielle. Dans l'exemple suivant, les \u00e9l\u00e9ments 0 \u00e0 5 sont initialis\u00e9s, les autres \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro\u2009:

    int32_t sequence[100] = {[0]=4, [2]=8, [4]=15, [6]=16, [8]=23, [10]=42};\n

    Notons que lorsque la notation []= est utilis\u00e9e, les valeurs qui suivent seront positionn\u00e9es aux indices suivants. On peut donc \u00e9crire\u2009:

    int32_t sequence[6] = {[0]=4, 8, [3]=16, 23, 42};\n

    Dans l'exemple ci-dessus sequence[2] vaudra z\u00e9ro.

    Initialisation tardive

    Il n'est pas possible d'initialiser un tableau apr\u00e8s sa d\u00e9claration. L'initialisation doit \u00eatre r\u00e9alis\u00e9e lors de la d\u00e9claration du tableau. L'\u00e9criture suivante est donc incorrecte\u2009:

    int array[10];\n\n// Erreur: l'initialisation tardive n'est pas autoris\u00e9e.\narray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n
    "}, {"location": "course-c/20-composite-types/arrays/#initialisation-a-zero", "title": "Initialisation \u00e0 z\u00e9ro", "text": "

    Pour initialiser un tableau \u00e0 z\u00e9ro, on devrait \u00eatre autoris\u00e9 \u00e0 \u00e9crire le code suivant puisque tous les \u00e9l\u00e9ments non explicitement initialis\u00e9s sont initialis\u00e9s \u00e0 z\u00e9ro\u2009:

    int32_t sequence[6] = {};\n

    Cependant cette \u00e9criture n'est pas autoris\u00e9e selon la norme car au moins un \u00e9l\u00e9ment doit \u00eatre initialis\u00e9. Il est donc n\u00e9cessaire d'initialiser au moins un \u00e9l\u00e9ment \u00e0 z\u00e9ro\u2009:

    int32_t sequence[6] = {0};\n

    Initialisation \u00e0 42

    L'\u00e9criture ci-dessus ne veut pas dire que tout le tableau est initialis\u00e9 \u00e0 z\u00e9ro de la m\u00eame mani\u00e8re l'\u00e9criture suivante ne veut pas dire que tout le tableau est initialis\u00e9 \u00e0 42\u2009:

    int32_t sequence[6] = {42};\n

    Seul le premier \u00e9l\u00e9ment est initialis\u00e9 \u00e0 42, les autres \u00e9l\u00e9ments sont initialis\u00e9s \u00e0 z\u00e9ro.

    "}, {"location": "course-c/20-composite-types/arrays/#initialisation-a-une-valeur-particuliere", "title": "Initialisation \u00e0 une valeur particuli\u00e8re", "text": "

    Cette \u00e9criture n'est pas normalis\u00e9e C99, mais est g\u00e9n\u00e9ralement compatible avec la majorit\u00e9 des compilateurs.

    int array[1024] = { [ 0 ... 1023 ] = -1 };\n

    En C99, il n'est pas possible d'initialiser un type compos\u00e9 \u00e0 une valeur unique. La mani\u00e8re traditionnelle reste la boucle it\u00e9rative\u2009:

    for (size_t i = 0; i < sizeof(array)/sizeof(array[0]); ++i)\n    array[i] = -1;\n
    "}, {"location": "course-c/20-composite-types/arrays/#tableaux-non-modifiables", "title": "Tableaux non modifiables", "text": "

    Maintenant que nous savons initialiser un tableau, il peut \u00eatre utile de d\u00e9finir un tableau avec un contenu qui n'est pas modifiable. Le mot cl\u00e9 const est utilis\u00e9 \u00e0 cette fin.

    const int32_t sequence[6] = {4, 8, 15, 16, 23, 42};\nsequence[2] = 12; // Interdit !\n

    Dans l'exemple ci-dessus, la seconde ligne g\u00e9n\u00e8rera l'erreur suivante\u2009:

    error: assignment of read-only location \u2018sequence[2]\u2019\n

    Notons que lors de l'utilisation de pointeurs, il serait possible, de fa\u00e7on d\u00e9tourn\u00e9e, de modifier ce tableau malgr\u00e9 tout\u2009:

    int *p = sequence;\np[2] = 12;\n

    Dans ce cas, ce n'est pas une erreur, mais une alerte du compilateur qui survient\u2009:

    warning: initialization discards \u2018const\u2019 qualifier from pointer\ntarget type [-Wdiscarded-qualifiers]\n
    ", "tags": ["const"]}, {"location": "course-c/20-composite-types/arrays/#tableaux-multidimensionnels", "title": "Tableaux multidimensionnels", "text": "

    Il est possible de d\u00e9clarer un tableau \u00e0 plusieurs dimensions. Si par exemple on souhaite d\u00e9finir une grille de jeu du tic-tac-toe ou morpion, il faudra une grille de 3x3.

    Pour ce faire, il est possible de d\u00e9finir un tableau de 6 \u00e9l\u00e9ments comme vu auparavant, et utiliser un artifice pour adresser les lignes et les colonnes\u2009:

    char game[6] = {0};\nint row = 1;\nint col = 2;\ngame[row * 3 + col] = 'x';\n

    N\u00e9anmoins, cette \u00e9criture n'est pas pratique et le langage C dispose heureusement de la syntaxe idoine pour all\u00e9ger l'\u00e9criture. La grille de jeu sera simplement initialis\u00e9e comme suit\u2009:

    char game[3][3] = {0};\n

    Jouer x au centre \u00e9quivaut \u00e0 \u00e9crire\u2009:

    game[1][1] = 'x';\n

    De la m\u00eame fa\u00e7on, il est possible de d\u00e9finir une structure tridimensionnelle\u2009:

    int volume[10][4][8];\n

    L'initialisation des tableaux multidimensionnelle est tr\u00e8s similaire aux tableaux standards, mais il est possible d'utiliser plusieurs niveaux d'accolades.

    Ainsi le jeu de morpion suivant\u2009:

     o | x | x\n---+---+---\n x | o | o\n---+---+---\n x | o | x\n

    Peut s'initialiser comme suit\u2009:

    char game[][3] = {{'o', 'x', 'x'}, {'x', 'o', 'o'}, {'x', 'o', 'x'}};\n

    Notons que l'\u00e9criture suivante est aussi accept\u00e9e car un tableau multidimensionnel est toujours repr\u00e9sent\u00e9 en m\u00e9moire de fa\u00e7on lin\u00e9aire\u2009: comme un tableau \u00e0 une dimension\u2009:

    char game[][3] = {'o', 'x', 'x', 'x', 'o', 'o', 'x', 'o', 'x'};\n

    Nous l'avons vu plus haut, il n'est pas n\u00e9cessaire de fournir toutes les informations de taille du tableau lors de l'initialisation. Dans l'exemple du tic-tac-toe, la valeur de la premi\u00e8re dimension est omise car elle peut \u00eatre inf\u00e9r\u00e9e du nombre d'\u00e9l\u00e9ments de la liste d'initialisation.

    Prenons l'exemple du tableau suivant\u2009:

    int array[2][3][4];\n

    On peut le repr\u00e9senter graphiquement comme suit\u2009:

    Tableau multidimensionnel 2x3x4

    N\u00e9anmoins en m\u00e9moire, ce tableau est toujours repr\u00e9sent\u00e9 de fa\u00e7on lin\u00e9aire. L'association des coordonn\u00e9es x, y et z est subjective et d\u00e9pend de la mani\u00e8re dont le tableau est utilis\u00e9. N\u00e9anmoins, on pourrait s'accorder sur une repr\u00e9sentation en m\u00e9moire logique. Dans le cas de cette figure, l'axe horizontal est l'axe des x, l'axe vertical est l'axe des y et la profondeur est l'axe des z. L'acc\u00e8s se fera avec [z][y][x]:

    int array[][3][4] = {\n    {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}},\n    {{13,14,15,16}, {17,18,19,20}, {21,22,23,24}}\n};\n\nint x = 0, y = 0, z = 0;\nassert(array[z][y][x] == 1);\n

    Si on d\u00e9cide l'agencement [x][y][z] qui peut sembler plus naturel, il sera d\u00e9clar\u00e9 comme suit\u2009:

    int array[4][3][2] = {\n    {{1,13}, {5,17}, {9,21}},\n    {{2,14}, {6,18}, {10,22}},\n    {{3,15}, {7,19}, {11,23}},\n    {{4,16}, {8,20}, {12,24}}\n};\n

    Pour cette raison, il est plus courant d'inverser les dimensions pour les tableaux multidimensionnels. Un tableau x-y \u00e0 deux dimensions est souvent d\u00e9clar\u00e9 array[y][x] pour faciliter l'initialisation.

    "}, {"location": "course-c/20-composite-types/arrays/#tableaux-et-fonctions", "title": "Tableaux et fonctions", "text": "

    Le passage d'un tableau \u00e0 une fonction est un peu particulier. En effet, comme nous l'avons \u00e9voqu\u00e9, un tableau est en r\u00e9alit\u00e9 un pointeur, c'est-\u00e0-dire l'adresse m\u00e9moire du premier \u00e9l\u00e9ment du tableau.

    Pour le cas le plus simple, lorsqu'une fonction re\u00e7oit un tableau, il est utile de passer la taille du tableau en param\u00e8tre, ceci permet de ne pas d\u00e9border du tableau. La fonction suivante re\u00e7oit un tableau de 5 entiers, pass\u00e9 par r\u00e9f\u00e9rence\u2009:

    void function(int array[5]);\n

    N\u00e9anmoins, la taille du tableau n'est pas n\u00e9cessaire, car un tableau est un pointeur et seul l'adresse du premier \u00e9l\u00e9ment et le type des \u00e9l\u00e9ments sont n\u00e9cessaires pour calculer les adresses des \u00e9l\u00e9ments suivants. La fonction suivante est donc strictement identique \u00e0 la pr\u00e9c\u00e9dente\u2009:

    void function(int array[]);\n

    N\u00e9anmoins, pour un tableau multidimensionnel, il est n\u00e9cessaire de passer la taille des dimensions suivantes afin de pouvoir calculer les adresses. En effet pour un tableau de 3x4x5 d\u00e9clar\u00e9 int array[3][4][5], si on souhaite acc\u00e9der \u00e0 l'\u00e9l\u00e9ment array[2][3][4], le compilateur va effectuer le calcul suivant\u2009:

    int* p = array + 2 * sizeof(int[4][5]) + 3 * sizeof(int[5]) + 4 * sizeof(int);\n     p = array + 2 * (4 * 5 * 4)       + 3 * (5 * 4)        + 4 * (4);\n     p = array + 2 * (80)              + 3 * (20)           + 4 * 4;\n

    Le seul \u00e9l\u00e9ment qui n'est pas n\u00e9cessaire c'est la premi\u00e8re dimension.

    "}, {"location": "course-c/20-composite-types/arrays/#allocation-memoire", "title": "Allocation m\u00e9moire", "text": "

    Jusqu'ici lorsque l'on d\u00e9clarait un tableau, au sein d'une fonction, il est d\u00e9clar\u00e9 sur la pile. Cette derni\u00e8re est limit\u00e9e en taille et il est possible de d\u00e9border et d'obtenir l'erreur tant redout\u00e9e stack smashing detected aussi appel\u00e9e stack overflow.

    En pratique lorsque l'on a besoin de grands espaces m\u00e9moire, il est pr\u00e9f\u00e9rable de d\u00e9clarer le tableau sur le tas (allocation dynamique) ou en variable globale.

    char a[1024];           // D\u00e9claration globale dans le segment de donn\u00e9es\n\nint main() {\n    char b[1024];           // D\u00e9claration sur la pile\n    char* c = malloc(1024); // D\u00e9claration sur le tas\n    free(c);                // Lib\u00e9ration de la m\u00e9moire\n}\n

    Ces trois d\u00e9clarations sont \u00e9quivalentes en termes de taille et d'utilisation du tableau. N\u00e9anmoins, leur utilisation est diff\u00e9rente.

    D\u00e9claration globale

    La m\u00e9moire est allou\u00e9e lors de la compilation et est disponible tout au long de l'ex\u00e9cution du programme. Il est possible de modifier le contenu de la m\u00e9moire \u00e0 tout moment. La visibilit\u00e9 de la variable est globale, c'est \u00e0 dire que la variable est accessible depuis n'importe quelle fonction du programme ce qui peut \u00eatre source de bogues.

    D\u00e9claration sur la pile

    La m\u00e9moire est allou\u00e9e lors de l'appel de la fonction et est lib\u00e9r\u00e9e \u00e0 la fin de la fonction. C'est une excellente m\u00e9thode pour de petits tableaux mais comme la pile (stack) \u00e0 une taille limit\u00e9e, cette m\u00e9thode ne doit pas \u00eatre utilis\u00e9e pour de grands tableaux.

    D\u00e9claration sur le tas

    La m\u00e9moire est allou\u00e9e dynamiquement lors de l'ex\u00e9cution du programme. La m\u00e9moire est disponible jusqu'\u00e0 ce que le programme lib\u00e8re l'espace m\u00e9moire. C'est la m\u00e9thode la plus flexible mais elle n'est pas utilisable sur des architectures embarqu\u00e9es car l'allocation dynamique de m\u00e9moire peut \u00eatre source de fragmentation de la m\u00e9moire et de risque de fuite m\u00e9moire.

    "}, {"location": "course-c/20-composite-types/arrays/#tableau-de-longueur-variable-vla", "title": "Tableau de longueur variable (VLA)", "text": "

    Un VLA (Variable Length Array) est un tableau dont la taille est d\u00e9termin\u00e9e \u00e0 l'ex\u00e9cution du programme. Cette fonctionnalit\u00e9 est disponible depuis le standard C99.

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n

    C'est une fonctionnalit\u00e9 qui peut \u00eatre tr\u00e8s utile, mais elle n'est pas sans risque. En effet, la taille du tableau est d\u00e9termin\u00e9e \u00e0 l'ex\u00e9cution du programme et il est possible de d\u00e9border la pile si les pr\u00e9cautions n\u00e9cessaires ne sont pas prises.

    D\u00e9bordement de pile

    L'exemple suivant illustre un d\u00e9bordement de pile. La fonction foo alloue un tableau de n \u00e9l\u00e9ments. Si n est tr\u00e8s grand, il est possible de d\u00e9border la pile et de provoquer un crash du programme.

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n\nint main() {\n    foo(1000000);\n}\n

    Dans cet exemple, la fonction foo alloue un tableau de 4 millions d'octets (4 Mo) sur la pile. Si la taille de la pile est de 1 Mo, le programme crashera.

    \u00c0 priori l'utilisateur ne sait pas ce que fait la fonction foo et il ne sait donc pas qu'un VLA est allou\u00e9 sur la pile.

    Pire, on pourrait imaginer le programme suivant\u2009:

    void foo(unsigned int n) {\n    int array[n];\n    ...\n}\n\nint main() {\n    unsigned int n;\n    scanf(\"%u\", &n);\n    foo(n);\n}\n

    Cette fois, l'utilisateur peut choisir la taille du tableau allou\u00e9 sur la pile. Vous voyez le probl\u00e8me\u2009?

    Pour les raisons \u00e9voqu\u00e9es, les VLA sont tr\u00e8s controvers\u00e9s et il est recommand\u00e9 de ne pas les utiliser. Il est pr\u00e9f\u00e9rable d'utiliser l'allocation dynamique de m\u00e9moire pour allouer des tableaux de taille variable. Du reste, avec C11, le support des VLA sont devenus optionnels, c'est \u00e0 dire qu'un compilateur peut \u00eatre compatible C11 sans supporter les VLA.

    ", "tags": ["foo"]}, {"location": "course-c/20-composite-types/arrays/#copie-dun-tableau", "title": "Copie d'un tableau", "text": "

    Imagions le code suivant\u2009:

    char a[5] = {1, 2, 3, 4, 5};\nchar b[5] = {0};\n

    Pour copier le contenu du tableau a dans le tableau b, il est n\u00e9cessaire de copier chaque \u00e9l\u00e9ment un par un. Il n'existe pas d'affectation directe entre deux tableaux.

    void copy(char dest[], char src[], size_t size) {\n    for (size_t i = 0; i < size; i++)\n        dest[i] = src[i];\n}\n

    Notez ici que la d\u00e9claration de la fonction utilise la notation [] pour les tableaux car la taille des donn\u00e9es \u00e0 copier n'est pas connue \u00e0 la compilation. C'est pour cette raison qu'on utilise un param\u00e8tre size pour indiquer la taille des tableaux. Ainsi pour copier a dans b, il suffit d'appeler la fonction copy :

    copy(b, a, 5);\n

    En pratique on utilisera la fonction memcpy de la biblioth\u00e8que standard qui est plus rapide et plus s\u00fbre que la fonction copy que nous avons \u00e9crite.

    #include <string.h>\n\nmemcpy(b, a, 5);\n
    ", "tags": ["size", "copy", "memcpy"]}, {"location": "course-c/20-composite-types/arrays/#exercices", "title": "Exercices", "text": "

    Exercice 1\u2009: Assignation

    \u00c9crire un programme qui lit la taille d'un tableau de cinquante entiers de 8 bytes et assigne \u00e0 chaque \u00e9l\u00e9ment la valeur de son indice.

    Solution
    int8_t a[50];\nfor (size_t i = 0; i < sizeof(a) / sizeof(a[0]; i++) {\n    a[i] = i;\n}\n

    Exercice 2\u2009: Premi\u00e8re position

    Soit un tableau d'entiers, \u00e9crire une fonction retournant la position de la premi\u00e8re occurrence d'une valeur dans le tableau.

    Traitez les cas particuliers.

    int index_of(int *array, size_t size, int search);\n
    Solution
    int index_of(int *array, size_t size, int search) {\n    int i = 0;\n    while (i < size && array[i++] != search);\n    return i == size ? -1 : i;\n}\n

    Exercice 3\u2009: D\u00e9clarations de tableaux

    Consid\u00e9rant les d\u00e9clarations suivantes\u2009:

    #define LIMIT 10\nconst int twelve = 12;\nint i = 3;\n

    Indiquez si les d\u00e9clarations suivantes (qui n'ont aucun lien entre elles), sont correctes ou non.

    int t(3);\nint k, t[3], l;\nint i[3], l = 2;\nint t[LIMITE];\nint t[i];\nint t[douze];\nint t[LIMITE + 3];\nfloat t[3, /* five */ 5];\nfloat t[3]        [5];\n

    Exercice 4\u2009: Comparaisons

    Soit deux tableaux char u[] et char v[], \u00e9crire une fonction comparant leur contenu et retournant\u2009:

    0 La somme des deux tableaux est \u00e9gale.

    -1 La somme du tableau de gauche est plus petite que le tableau de droite

    1 La somme du tableau de droite est plus grande que le tableau de gauche

    Le prototype de la fonction \u00e0 \u00e9crire est\u2009:

    int comp(char a[], char b[], size_t length);\n
    Solution
    int comp(char a[], char b[], size_t length) {\n    int sum_a = 0, sum_b = 0;\n\n    for (size_t i = 0; i < length; i++) {\n        sum_a += a[i];\n        sum_b += b[i];\n    }\n\n    return sum_b - sum_a;\n}\n

    Exercice 5\u2009: Le plus grand et le plus petit

    Dans le canton de Gen\u00e8ve, il existe une tradition ancestrale\u2009: l'Escalade. En comm\u00e9moration de la victoire de la r\u00e9publique protestante sur les troupes du duc de Savoie suite \u00e0 l'attaque lanc\u00e9e contre Gen\u00e8ve dans la nuit du 11 au 12 d\u00e9cembre 1602 (selon le calendrier julien), une traditionnelle marmite en chocolat est bris\u00e9e par l'ain\u00e9 et le cadet apr\u00e8s la r\u00e9citation de la phrase rituelle \u00ab\u2009Ainsi p\u00e9rirent les ennemis de la R\u00e9publique\u2009!\u2009\u00bb.

    Pour gagner du temps et puisque l'assembl\u00e9e est grande, il vous est demand\u00e9 d'\u00e9crire un programme pour identifier le doyen et le benjamin de l'assistance.

    Un fichier contenant les ann\u00e9es de naissance de chacun vous est donn\u00e9, il ressemble \u00e0 ceci\u2009:

    1931\n1986\n1996\n1981\n1979\n1999\n2004\n1978\n1964\n

    Votre programme sera ex\u00e9cut\u00e9 comme suit\u2009:

    $ cat years.txt | marmite\n2004\n1931\n

    Exercice 6\u2009: L'index magique

    Un indice magique d'un tableau A[0..n-1] est d\u00e9fini tel que la valeur A[i] == i. \u00c9tant donn\u00e9 que le tableau est tri\u00e9 avec des entiers distincts (sans r\u00e9p\u00e9tition), \u00e9crire une m\u00e9thode pour trouver un indice magique s'il existe.

    Exemple\u2009:

        0   1   2   3   4   5   6   7   8   9   10\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502-90\u2502-33\u2502 -5\u2502 1 \u2502 2 \u2502 4 \u2502 5 \u2502 7 \u2502 10\u2502 12\u2502 14\u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n                                ^\n
    Solution

    Une solution triviale consiste \u00e0 it\u00e9rer tous les \u00e9l\u00e9ments jusqu'\u00e0 trouver l'indice magique\u2009:

    int magic_index(int[] array) {\n    const size_t size = sizeof(array) / sizeof(array[0]);\n\n    size_t i = 0;\n\n    while (i < size && array[i] != i) i++;\n\n    return i == size ? -1 : i;\n}\n

    La complexit\u00e9 de cet algorithme est :math\u2009:O(n) or, la donn\u00e9e du probl\u00e8me indique que le tableau est tri\u00e9. Cela veut dire que probablement, cette information n'est pas donn\u00e9e par hasard.

    Pour mieux se repr\u00e9senter le probl\u00e8me, prenons l'exemple d'un tableau\u2009:

        0   1   2   3   4   5   6   7   8   9   10\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502-90\u2502-33\u2502 -5\u2502 1 \u2502 2 \u2502 4 \u2502 5 \u2502 7 \u2502 10\u2502 12\u2502 14\u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n                                ^\n

    La premi\u00e8re valeur magique est 7. Est-ce qu'une approche dichotomique est possible\u2009?

    Prenons le milieu du tableau A[5] = 4. Est-ce qu'une valeur magique peut se trouver \u00e0 gauche du tableau\u2009? Dans le cas le plus favorable qui serait\u2009:

        0   1   2   3   4\n\u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 -1\u2502 0 \u2502 1 \u2502 2 \u2502 3 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n

    On voit qu'il est impossible que la valeur se trouve \u00e0 gauche, car les valeurs dans le tableau sont distinctes et il n'y a pas de r\u00e9p\u00e9titions. La r\u00e8gle que l'on peut poser est A[mid] < mid o\u00f9 mid est la valeur m\u00e9diane.

    Il est possible de r\u00e9p\u00e9ter cette approche de fa\u00e7on dichotomique\u2009:

    int magic_index(int[] array) {\n    return _magic_index(array, 0, sizeof(array) / sizeof(array[0]) - 1);\n}\n\nint _magic_index(int[] array, size_t start, size_t end) {\n    if (end < start) return -1;\n    int mid = (start + end) / 2;\n    if (array[mid] == mid) {\n        return mid;\n    } else if (array[mid] > mid) {\n        return _magic_index(array, start, mid - 1);\n    } else {\n        return _magic_index(array, mid + 1, end);\n    }\n}\n

    Exercice 7\u2009: D\u00e9tectives priv\u00e9s

    Voici les d\u00e9penses de service annuelles d'un c\u00e9l\u00e8bre bureau de d\u00e9tectives priv\u00e9s\u2009:

    Mois Bosley Sabrina Jill Kelly Janvier 414.38 222.72 99.17 153.81 F\u00e9vrier 403.41 390.61 174.39 18.11 Mars 227.55 73.86 291.08 416.55 Avril 220.20 342.25 139.45 86.98 Mai 13.46 172.66 252.33 265.32 Juin 259.37 378.72 173.02 208.43 Juillet 327.06 16.53 391.05 266.84 Ao\u00fbt 50.82 3.37 201.71 170.84 Septembre 450.78 9.33 111.63 337.07 Octobre 434.45 77.80 459.46 479.17 Novembre 420.13 474.69 343.64 273.28 D\u00e9cembre 147.76 250.73 201.47 9.75

    Afin de laisser plus de temps aux d\u00e9tectives \u00e0 r\u00e9soudre des affaires, vous \u00eates mandat\u00e9 pour \u00e9crire une fonction qui re\u00e7oit en param\u00e8tre le tableau de r\u00e9els ci-dessus format\u00e9 comme suit\u2009:

    double accounts[][] = {\n    {414.38, 222.72,  99.17, 153.81, 0},\n    {403.41, 390.61, 174.39, 18.11,  0},\n    {227.55,  73.86, 291.08, 416.55, 0},\n    {220.20, 342.25, 139.45, 86.98,  0},\n    {13.46 , 172.66, 252.33, 265.32, 0},\n    {259.37, 378.72, 173.02, 208.43, 0},\n    {327.06,  16.53, 391.05, 266.84, 0},\n    {50.82 ,   3.37, 201.71, 170.84, 0},\n    {450.78,   9.33, 111.63, 337.07, 0},\n    {434.45,  77.80, 459.46, 479.17, 0},\n    {420.13, 474.69, 343.64, 273.28, 0},\n    {147.76, 250.73, 201.47, 9.75,   0},\n    {  0,      0,      0,    0,      0}\n};\n

    Et laquelle compl\u00e8te les valeurs manquantes.

    Exercice 8\u2009: Pot de peinture

    \u00c0 l'instar de l'outil pot de peinture des \u00e9diteurs d'image, il vous est demand\u00e9 d'impl\u00e9menter une fonctionnalit\u00e9 similaire.

    L'image est repr\u00e9sent\u00e9e par un tableau bidimensionnel contenant des couleurs index\u00e9es\u2009:

    typedef enum { BLACK, RED, PURPLE, BLUE, GREEN YELLOW, WHITE } Color;\n\n#if 0 // Image declaration example\nColor image[100][100];\n#endif\n\nboolean paint(Color* image, size_t rows, size_t cols, Color fill_color);\n

    Hint

    Deux approches int\u00e9ressantes sont possibles\u2009: DFS (Depth-First-Search) ou BFS (Breadth-First-Search), toutes deux r\u00e9cursives.

    ", "tags": ["mid"]}, {"location": "course-c/20-composite-types/generic-programming/", "title": "Programmation g\u00e9n\u00e9rique", "text": "

    La programmation g\u00e9n\u00e9rique est une technique de programmation qui permet de d\u00e9finir des algorithmes et des structures de donn\u00e9es qui peuvent \u00eatre utilis\u00e9s avec diff\u00e9rents types de donn\u00e9es. En C, la programmation g\u00e9n\u00e9rique est r\u00e9alis\u00e9e \u00e0 l'aide de macros, de types et de fonctions inline.

    \u00c9crire un code g\u00e9n\u00e9rique est le Graal de tout programmeur. Cela permet de r\u00e9utiliser du code et de le rendre plus robuste. En effet, un code g\u00e9n\u00e9rique est un code qui peut \u00eatre utilis\u00e9 avec diff\u00e9rents types de donn\u00e9es et diff\u00e9rentes configuration sans modification. Cela permet de r\u00e9duire la duplication de code et d'am\u00e9liorer la maintenabilit\u00e9 du code.

    N\u00e9anmoins, la programmation g\u00e9n\u00e9rique en C est limit\u00e9e par le fait que le langage n'est pas orient\u00e9 objet et ne poss\u00e8de pas de couche de m\u00e9ta-programmation comme les templates en C++. Certains d\u00e9veloppeurs ont \u00e9t\u00e9 jusqu'\u00e0 inventer un nouveau langage comme le Vala pour palier \u00e0 ces limitations. Vala est un meta-langage qui g\u00e9n\u00e8re du code C \u00e0 partir de code Vala. Il apporte le paradigme de la programmation orient\u00e9e objet et de la programmation g\u00e9n\u00e9rique \u00e0 C. Cependant, Vala n'est pas un langage tr\u00e8s r\u00e9pandu en dehors de la communaut\u00e9 GNOME.

    "}, {"location": "course-c/20-composite-types/generic-programming/#fonctions-generiques", "title": "Fonctions g\u00e9n\u00e9riques", "text": "

    Prenons l'exemple de la fonction d'addition suivante. Elle est \u00e9crite pour des entiers et ne fonctionnera donc pas pour des flottants. Il faudrait la r\u00e9\u00e9crire pour les flottants mais cela implique une collision de nom de fonction. Il faudrait alors d\u00e9finir autant de fonctions que de types avec des suffixes diff\u00e9rents (add_int, add_float, etc.).

    int add(int a, int b) { return a + b; }\n

    Alternativement, on peut utiliser des macros pour d\u00e9finir des fonctions g\u00e9n\u00e9riques. Par exemple, la macro suivante permet de d\u00e9finir une fonction d'addition pour n'importe quel type de donn\u00e9es\u2009:

    #define add(a, b) ((a) + (b))\n

    N\u00e9anmoins cette macro ne fonctionne que pour des fonctions simples. Pour des fonctions plus complexes, il est n\u00e9cessaire de d\u00e9finir des fonctions s\u00e9par\u00e9es pour chaque type de donn\u00e9es comme discut\u00e9 pr\u00e9c\u00e9demment\u2009:

    int add_int(int a, int b) { return a + b; }\nfloat add_float(float a, float b) { return a + b; }\n

    Pour obtenir un comportement g\u00e9n\u00e9rique, on peut utiliser le mot-cl\u00e9 _Generic introduit dans C11. _Generic permet de d\u00e9finir une fonction en fonction du type de donn\u00e9es pass\u00e9 en param\u00e8tre. Par exemple, la fonction suivante permet d'additionner des entiers ou des flottants\u2009:

    #define add(a, b) _Generic((a), \\\n    int: add_int, \\\n    float: add_float \\\n)(a, b)\n

    Le fonctionnement de _Generic est le suivant\u2009: le premier argument est une expression qui est \u00e9valu\u00e9e pour d\u00e9terminer le type de donn\u00e9es. Ensuite, _Generic compare le type de donn\u00e9es avec les types d\u00e9finis dans la liste. Si le type de donn\u00e9es correspond \u00e0 un type d\u00e9fini, alors la fonction correspondante est appel\u00e9e avec les arguments a et b. L'exemple donn\u00e9 n'est pas tr\u00e8s pertinent car la solution simple avec une macro est plus simple et plus lisible. N\u00e9anmoins, _Generic est tr\u00e8s utile pour des fonctions plus complexes. Prenons l'exemple de la fonction print qui affiche un entier ou un flottant\u2009:

    #include <stdio.h>\n\nvoid print_int(int x) { printf(\"Entier : %d\\n\", x); }\nvoid print_float(float x) { printf(\"Flottant : %.2f\\n\", x); }\nvoid print_string(const char *x) { printf(\"Cha\u00eene : %s\\n\", x); }\n\n#define print(x) _Generic((x), \\\n    int: print_int, \\\n    float: print_float, \\\n    const char*: print_string \\\n)(x)\n\nint main() {\n    int a = 10;\n    float b = 5.5;\n    const char *c = \"Bonjour\";\n\n    print(a);\n    print(b);\n    print(c);\n}\n

    Dans le standard C, l'en-t\u00eate <tgmath.h> fournit par exemple des fonctions g\u00e9n\u00e9riques pour les fonctions math\u00e9matiques. Il n'y a donc plus \u00e0 ce soucier du type de donn\u00e9es pass\u00e9 en param\u00e8tre ce qui pourrait en th\u00e9orie cr\u00e9er moins d'erreurs de programmation.

    ", "tags": ["_Generic", "add_float", "add_int", "print"]}, {"location": "course-c/20-composite-types/generic-programming/#tableaux-a-taille-variable", "title": "Tableaux \u00e0 taille variable", "text": "

    Nous avons vu qu'il est parfaitement correct d'\u00e9crire\u2009:

    int sum(int array[42]);\n

    Cependant, cette fonction n'est valable que pour des tableaux de taille fixe \u00e0 42 \u00e9l\u00e9ments. Pour des tableaux de taille variable, il est n\u00e9cessaire de passer la taille du tableau en param\u00e8tre.

    int sum(int *array, size_t size);\n

    Notez que la notation est pass\u00e9e de int array[] \u00e0 int *array. Cela met l'accent sur le fait que array est un pointeur et non un type tableau. L'avantage du pointeur est que l'on peut le parcourir avec une boucle for en utilisant l'arithm\u00e9tique des pointeurs.

    int sum(int *array, size_t size) {\n   int sum = 0, *end = array + size;\n   while (array < end) sum += *(array++);\n   return sum;\n}\n
    ", "tags": ["array", "for"]}, {"location": "course-c/20-composite-types/generic-programming/#fonction-de-callback", "title": "Fonction de callback", "text": "

    Dans des algorithmes comme le tri d'un tableau. Il est possible de parcourir le tableau facilement si l'on connait sa taille et la taille du type de donn\u00e9e. Cependant, pour comparer deux valeurs, il est n\u00e9cessaire de conna\u00eetre la m\u00e9thode de comparaison car elle d\u00e9pend du type. On peut utiliser _Generic mais cela fonctionnera que pour des types simples et connus.

    Une fonction de tri g\u00e9n\u00e9rique ne peut pas conna\u00eetre tous les types de donn\u00e9es. Par exemple imaginons un type comme \u00e9tant une structure contenant une personne\u2009:

    typedef struct {\n    char *name;\n    int age;\n} Person;\n

    Trier un tableau de personnes demande de savoir comment comparer deux personnes. On peut imaginer une fonction de comparaison qui compare deux personnes en fonction de leur \u00e2ge ou de leur nom\u2009:

    int compare_age(Person a, Person b) { return a.age - b.age; }\nint compare_name(Person a, Person b) { return strcmp(a.name, b.name); }\n

    L'astuce consiste \u00e0 passer une fonction de comparaison en param\u00e8tre de la fonction de tri. Cela permet de trier n'importe quel type de donn\u00e9es. Par exemple, la fonction de tri suivante permet de trier un tableau de n'importe quel type de donn\u00e9es\u2009:

    void swap(void *a, void *b, size_t size) {\n    char tmp[size];\n    memcpy(tmp, a, size);\n    memcpy(a, b, size);\n    memcpy(b, tmp, size);\n}\n\nvoid sort(void *array, size_t size, size_t count,\n    int (*comp)(const void *, const void *))\n{\n    for (size_t i = 0; i < count; i++) {\n        for (size_t j = i + 1; j < count; j++) {\n            void *a = (char *)array + i * size;\n            void *b = (char *)array + j * size;\n            if (comp(a, b) > 0) swap(a, b, size);\n        }\n    }\n}\n

    L'utilisation de fonctions de callback est un concept fondamental en programmation g\u00e9n\u00e9rique. En C il implique le plus souvent de passer par un pointeur g\u00e9n\u00e9rique void * afin de pouvoir d\u00e9finir un prototype de fonction \u00e9galement g\u00e9n\u00e9rique.

    La biblioth\u00e8que standard C fournit plusieurs fonctions g\u00e9n\u00e9riques telles que\u2009:

    // Copie d'une r\u00e9gion m\u00e9moire\nvoid memcpy(void *dest, const void *src, size_t n);\n// Tri rapide d'un tableau\nvoid qsort(void *base, size_t nmemb, size_t size,\n    int (*compar)(const void *, const void *));\n// Recherche dichotomique\nvoid bsearch(const void *key, const void *base, size_t nmemb, size_t size,\n    int (*compar)(const void *, const void *));\n
    ", "tags": ["_Generic"]}, {"location": "course-c/20-composite-types/generic-programming/#types-de-donnees-abstraits", "title": "Types de donn\u00e9es abstraits", "text": "

    Un type de donn\u00e9e abstrait (ADT pour Abstract Data Type) cache g\u00e9n\u00e9ralement une structure dont le contenu n'est pas connu de l'utilisateur final. Ceci est rendu possible par le standard (C99 \u00a76.2.5) par l'usage de types incomplets.

    Pour m\u00e9moire, un type incomplet d\u00e9crit un objet dont on ne conna\u00eet pas sa taille en m\u00e9moire. L'exemple suivant d\u00e9clare un nouveau type structure qui n'est alors pas (encore) connu dans le fichier courant\u2009:

    typedef struct Unknown *Known;\n\nint main() {\n    Known foo; // Autoris\u00e9, le type est incomplet\n\n    foo + 1; // Impossible car la taille de foo est inconnue.\n    foo->key; // Impossible car le type est incomplet.\n}\n

    De fa\u00e7on g\u00e9n\u00e9rale, les types abstraits sont utilis\u00e9s dans l'\u00e9criture de biblioth\u00e8ques logicielles lorsqu'il est important que l'utilisateur final ne puisse pas compromettre le contenu du type et en for\u00e7ant cet utilisateur \u00e0 ne passer que par des fonctions d'acc\u00e8s.

    Prenons le cas du fichier foobar.c lequel d\u00e9crit une structure struct Foo et un type Foo. Notez que le type peut \u00eatre d\u00e9clar\u00e9 avant la structure. Foo restera abstrait jusqu'\u00e0 la d\u00e9claration compl\u00e8te de la structure struct Foo permettant de conna\u00eetre sa taille. Ce fichier contient \u00e9galement trois fonctions\u2009:

    • init permet d'initialiser la structure\u2009;
    • get permet de r\u00e9cup\u00e9rer la valeur contenue dans Foo ;
    • set permet d'assigner une valeur \u00e0 Foo.

    En plus, il existe un compteur d'acc\u00e8s count qui s'incr\u00e9mente lorsque l'on assigne une valeur et se d\u00e9cr\u00e9mente lorsque l'on r\u00e9cup\u00e8re une valeur.

    #include <stdlib.h>\n\ntypedef struct Foo Foo;\n\nstruct Foo {\n    int value;\n    int count;\n};\n\nvoid init(Foo** foo) {\n    *foo = malloc(sizeof(Foo)); // Allocation dynamique\n    (*foo)->count = (*foo)->value = 0;\n}\n\nint get(Foo* foo) {\n    foo->count--;\n    return foo->value;\n}\n\nvoid set(Foo* foo, int value) {\n    foo->count++;\n    foo->value = value;\n}\n

    \u00c9videmment, on ne souhaite pas qu'un petit malin compromette ce compteur en \u00e9crivant maladroitement\u2009:

    foo->count = 42; // Hacked this !\n

    Pour s'en prot\u00e9ger, on a recours \u00e0 la compilation s\u00e9par\u00e9e (voir chapitre sur la compilation s\u00e9par\u00e9e [TranslationUnits]) dans laquelle le programme est d\u00e9coup\u00e9 en plusieurs fichiers. Le fichier foobar.h contiendra tout ce qui doit \u00eatre connu du programme principal, \u00e0 savoir les prototypes des fonctions, et le type abstrait\u2009:

    #pragma once\n\ntypedef struct Foo Foo;\n\nvoid init(Foo** foo);\nint get(Foo* foo);\nvoid set(Foo* foo, int value);\n

    Ce fichier sera inclus dans le programme principal main.c :

    #include \"foobar.h\"\n#include <stdio.h>\n\nint main() {\n    Foo *foo;\n\n    init(&foo);\n    set(foo, 23);\n    printf(\"%d\\n\", get(foo));\n}\n

    Un type abstrait peut \u00eatre vu comme une bo\u00eete noire et est par cons\u00e9quent une technique de programmation g\u00e9n\u00e9rique. Il est possible de d\u00e9finir des types abstraits pour des structures plus complexes comme des listes cha\u00een\u00e9es, des arbres, des graphes, mais que l'utilisateur final ne doit pas n\u00e9cessairement conna\u00eetre.

    ", "tags": ["Foo", "main.c", "abstract-data-type", "foobar.c", "init", "count", "set", "get", "foobar.h"]}, {"location": "course-c/20-composite-types/pointers/", "title": "Pointers", "text": "", "tags": ["lettre", "gps_position"]}, {"location": "course-c/20-composite-types/pointers/#pointeurs", "title": "Pointeurs", "text": "

    Attention les v\u00e9los, nous nous aventurons aujourd'hui sur un terrain d\u00e9licat, subtil et parfois d\u00e9routant, mais \u00f4 combien fondamental pour quiconque aspire \u00e0 ma\u00eetriser l'art de la programmation. Un sujet d'une importance cruciale, presque incontournable\u2009: les pointeurs.

    Les pointeurs sont des variables d'une nature singuli\u00e8re\u2009: au lieu de contenir directement une valeur, ils stockent une adresse m\u00e9moire. \u00c0 quoi bon, me demanderez-vous\u2009? L'objectif est de permettre des indirections, d'optimiser la gestion des donn\u00e9es et de rendre l'ex\u00e9cution du code plus fluide et efficiente.

    Imaginons une sc\u00e8ne tir\u00e9e des intrigues galantes du XVIIIe si\u00e8cle. Le Vicomte de Valmont, s\u00e9ducteur inv\u00e9t\u00e9r\u00e9, s'appr\u00eate \u00e0 \u00e9crire une missive enflamm\u00e9e \u00e0 la Marquise de Merteuil. Apr\u00e8s avoir r\u00e9dig\u00e9 sa lettre, il la scelle soigneusement avant de la d\u00e9poser dans sa bo\u00eete aux lettres, en esp\u00e9rant que le facteur l'acheminera \u00e0 bon port. Ce simple geste pourrait se traduire dans le langage des machines par la d\u00e9claration suivante\u2009:

    char lettre[] = \"Ch\u00e8re Marquise, ...\";\n

    Cette variable lettre est alors stock\u00e9e en m\u00e9moire \u00e0 une adresse sp\u00e9cifique, disons 0x12345abc, qui correspondrait \u00e0 l'emplacement de la bo\u00eete aux lettres du Vicomte dans ce grand r\u00e9seau qu'est la m\u00e9moire.

    Le facteur, fid\u00e8le \u00e0 son devoir mais pas \u00e0 l'abri des al\u00e9as du quotidien, d\u00e9couvre avec horreur que la chaleur \u00e9touffante a fait fondre le sceau de cire, collant irr\u00e9m\u00e9diablement la lettre au fond de la bo\u00eete. En s'effor\u00e7ant de la d\u00e9tacher, il finit par la d\u00e9chirer, r\u00e9v\u00e9lant par inadvertance son contenu.

    Bien entendu, il faut admettre que cette pirouette est une m\u00e9taphore pour illustrer le fait qu'une valeur en m\u00e9moire ne peut \u00eatre transport\u00e9e simplement. Notre pauvre facteur, dont la m\u00e9moire n'est plus ce qu'elle \u00e9tait, d\u00e9cide de m\u00e9moriser laborieusement les premiers mots de la lettre\u2009: Ch\u00e8re Ma. Il enfourche alors son v\u00e9lo tout nickel\u00e9, fait un premier voyage et les retranscrit dans la bo\u00eete de la Marquise de Merteuil. Cette op\u00e9ration se r\u00e9p\u00e8te encore et encore, jusqu'\u00e0 ce que la lettre soit enti\u00e8rement copi\u00e9e.

    Nous obtenons alors une copie de la lettre dans la bo\u00eete de la Marquise\u2009:

    char valmont_mailbox[] = \"Ch\u00e8re Marquise, ...\";\nchar merteuil_mailbox[] = \"Ch\u00e8re Marquise, ...\";\n

    Mais la chaleur persistante et les imperfections de cette m\u00e9thode ne satisfont gu\u00e8re la Marquise, qui d\u00e9cide de r\u00e9soudre le probl\u00e8me en se rendant \u00e0 Tarente \u2013 choix discutable en pleine canicule. L\u00e0-bas, elle grave sa r\u00e9ponse sur le mur sud du Castello Aragonese, prenant soin de noter avec pr\u00e9cision les coordonn\u00e9es GPS du mur\u2009:0x8F313233 (en r\u00e9alit\u00e9 8FGMPXJ7+2V selon l'OLC).

    char castello_wall[] = \"Cher Vicomte ...\";\nchar (*gps_position)[] = &castello_wall;\n

    De retour chez elle, elle confie au facteur l'adresse en m\u00e9moire (0x30313233), un message que celui-ci, soulag\u00e9, peut enfin retenir sans effort.

    Ainsi, la variable gps_position ne contient pas directement le message, mais uniquement l'adresse o\u00f9 celui-ci est stock\u00e9\u2009: un pointeur sur un tableau de caract\u00e8res.

    Pendant ce temps, le Vicomte, moins dispos\u00e9 \u00e0 l'effort, s'est muni d'un t\u00e9l\u00e9scripteur capable d'interpr\u00e9ter le code C. En r\u00e9cup\u00e9rant l'adresse fournie, il parvient \u00e0 lire le message de la Marquise\u2009:

    printf(\"%s\", *gps_position);\n

    S'il avait omis l'ast\u00e9risque (*, 002A) dans cette ligne, il n'aurait vu que 0123, l'adresse elle-m\u00eame, au lieu du message qu'elle contient.

    L'ast\u00e9risque joue donc un r\u00f4le essentiel\u2009: celui du d\u00e9r\u00e9f\u00e9rencement, c'est-\u00e0-dire l'acte de demander au facteur d'aller chercher le contenu \u00e0 l'adresse sp\u00e9cifi\u00e9e.

    Mais pourquoi donc avons-nous utilis\u00e9 l'esperluette (&, 0026) pour obtenir cette adresse\u2009: &castello_wall ? L'esperluette, pr\u00e9c\u00e9dant une variable, se traduit par l'adresse de cette variable, tout comme la Marquise avait relev\u00e9 la position GPS du mur.

    Quant \u00e0 l'ast\u00e9risque dans (*gps_position)[], il ne signifie pas un d\u00e9r\u00e9f\u00e9rencement dans ce contexte, mais participe \u00e0 la d\u00e9claration du pointeur. C'est souvent ici que les novices perdent le fil. Revenons \u00e0 l'essentiel.

    En C, l'ast\u00e9risque peut signifier plusieurs choses\u2009:

    1. Multiplication : a * b,
    2. D\u00e9r\u00e9f\u00e9rencement d'un pointeur : *ptr,
    3. D\u00e9claration d'un pointeur : int *ptr.

    Dans notre cas, nous d\u00e9clarons un pointeur. En appliquant la r\u00e8gle de lecture gauche-droite\u2009:

    char (*gps_position)[]\n       ^^^^^^^^^^^^        1. gps_position est\n                   ^       2. ...\n      ^                    3. un pointeur sur\n                    ^^     4. un tableau de\n^^^^                       5. caract\u00e8res\n                           6. PROFIT...\n

    En r\u00e9sum\u00e9\u2009:

    • Un pointeur est une variable.
    • Il contient une adresse m\u00e9moire.
    • Il peut \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9 pour obtenir la valeur de l'\u00e9l\u00e9ment point\u00e9.
    • L'adresse d'une variable peut \u00eatre obtenue avec une esperluette (&).

    Ainsi, nous voyons que les pointeurs, loin d'\u00eatre une simple abstraction technique, peuvent se r\u00e9v\u00e9ler de pr\u00e9cieux alli\u00e9s dans l'\u00e9criture d'un code \u00e0 la fois efficace et \u00e9l\u00e9gant.

    "}, {"location": "course-c/20-composite-types/pointers/#pointeur-simple", "title": "Pointeur simple", "text": "

    Le format le plus simple d'un pointeur sur un entier s'\u00e9crit avec l'ast\u00e9risque *:

    int *ptr = NULL;\n

    La valeur NULL correspond \u00e0 l'adresse nulle 0x00000000. On utilise cette convention et non 0 pour bien indiquer qu'il s'agit d'une adresse et non d'une valeur scalaire.

    nul, null, nulll

    Attention \u00e0 l'\u00e9criture de NULL:

    • nul est le caract\u00e8re de fin de cha\u00eene de caract\u00e8res '\\0' ;
    • null est une adresse qui pointe nulle part\u2009;
    • nulll veut dire que vous avez fait une faute de frappe.

    \u00c0 tout moment, la valeur du pointeur peut \u00eatre assign\u00e9e \u00e0 l'adresse d'un entier puisque nous avons d\u00e9clar\u00e9 un pointeur sur un entier. Dans l'exemple suivant, deux variables boiling et freezing sont d\u00e9clar\u00e9es et un pointeur ptr est assign\u00e9 soit \u00e0 l'adresse l'une, soit \u00e0 l'autre selon que i est pair ou impair\u2009:

    int boiling = 100;\nint freezing = 0;\n\nfor (char i = 0; i < 10; i++) {\n    int *ptr = i % 2 ? &boiling : &freezing;\n    printf(\"%d\", *ptr);\n}\n

    Lorsque nous avions vu les tableaux, nous \u00e9crivions pour d\u00e9clarer un tableau d'entiers la notation ci-dessous\u2009:

    int array[10] = {0,1,2,3,4,5,6,7,8,9};\n

    Vous ne le saviez pas, mais \ud834\udd3d plot twist \ud834\udd3d la variable array est un pointeur, et la preuve est que array peut \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9\u2009:

    printf(\"%d\", *array); // Affiche 0\n

    La diff\u00e9rence entre un tableau et un pointeur est la suivante\u2009:

    • Il n'est pas possible d'assigner une adresse \u00e0 un tableau
    • Il n'est pas possible d'assigner des valeurs \u00e0 un pointeur

    D'ailleurs, l'op\u00e9rateur crochet [] n'est rien d'autre qu'un sucre syntaxique et les deux codes suivants sont \u00e9quivalents\u2009:

    a[b] \u2261 *(a + b);\n

    Par ailleurs, et bien que ce soit une tr\u00e8s mauvaise id\u00e9e, il est tout \u00e0 fait possible d'\u00e9crire le code suivant puisque l'addition est commutative\u2009:

    assert(4[a] == a[4]);\n

    Asterix de gauche ou de droite\u2009?

    Lors de la d\u00e9claration d'un pointeur, l'ast\u00e9risque peut \u00eatre plac\u00e9 \u00e0 gauche ou \u00e0 droite du type. Les d\u00e9clarations suivantes sont \u00e9quivalentes\u2009:

    int *ptr;\nint* ptr;\nint*ptr;\n

    N\u00e9anmoins il est recommand\u00e9 de placer l'ast\u00e9risque \u00e0 droite du type pour \u00e9viter toute confusion. En effet, lorsque vous utilisez l'op\u00e9rateur virgule , pour d\u00e9clarer plusieurs variables, il est facile de penser que int* a, b; d\u00e9clare deux pointeurs, alors qu'en r\u00e9alit\u00e9 seul a est un pointeur.

    int* a, b;  // a est un pointeur, b est un entier\nint *a, *b; // a et b sont des pointeurs\n
    ", "tags": ["nulll", "boiling", "array", "freezing", "ptr", "nul", "NULL", "null"]}, {"location": "course-c/20-composite-types/pointers/#dereferencement-et-adresse", "title": "D\u00e9r\u00e9f\u00e9rencement et adresse", "text": "

    Avec les pointeurs, certains op\u00e9rateurs sont recycl\u00e9s pour de nouvelles fonctions. C'est d\u00e9routant au d\u00e9but mais le nombre de caract\u00e8re sp\u00e9ciaux dans la table ASCII \u00e9tant limit\u00e9, il faut bien faire avec. Dans le cas pr\u00e9cis des pointeurs deux op\u00e9rateurs sont \u00e0 conna\u00eetre\u2009:

    Op\u00e9rateurs de pointeurs Op\u00e9rateur Description * D\u00e9r\u00e9f\u00e9rencement & Adresse d'une variable"}, {"location": "course-c/20-composite-types/pointers/#adresse-dune-variable", "title": "Adresse d'une variable", "text": "

    L'op\u00e9rateur & utilis\u00e9 comme op\u00e9rateur unaire permet d'obtenir l'adresse m\u00e9moire d'une variable. C'est \u00e0 dire que si a est une variable, &a est l'adresse m\u00e9moire de cette variable. Par exemple\u2009:

    int a = 42;\nprintf(\"%p\", &a);  // Affiche l'adresse m\u00e9moire de la variable a\n

    Ici le %p est un format de cha\u00eene de caract\u00e8res pour afficher une adresse m\u00e9moire. L'adresse m\u00e9moire est affich\u00e9e en hexad\u00e9cimal. Par exemple 0x12345678. On pourrait aussi \u00e9crire de mani\u00e8re \u00e9quivalente\u2009:

    printf(\"0x%08lx\", (uintptr_t)&a);\n

    Le type uintptr_t est un type entier non sign\u00e9 qui est assez grand pour contenir une adresse m\u00e9moire. Il est d\u00e9fini dans le fichier d'en-t\u00eate stdint.h et il est g\u00e9n\u00e9ralement d\u00e9fini comme un alias pour unsigned long. Il est utilis\u00e9 g\u00e9n\u00e9ralement pour faire un cast d'une adresse m\u00e9moire en un entier.

    ", "tags": ["stdint.h", "uintptr_t"]}, {"location": "course-c/20-composite-types/pointers/#dereferencement", "title": "D\u00e9r\u00e9f\u00e9rencement", "text": "

    Le d\u00e9r\u00e9f\u00e9rencement correspond \u00e0 l'op\u00e9ration de lecture de la valeur point\u00e9e par un pointeur. C'est \u00e0 dire que si ptr est un pointeur sur un entier, *ptr est la valeur de cet entier. Dit autrement, si vous avez un post-it avec l'adresse d'un magasin de chaussures, la simple pocession de ce post-it ne vous permet pas d'avoir de nouvelles chaussures. Il vous faut vous rendre \u00e0 l'adresse indiqu\u00e9e pour obtenir les chaussures. C'est exactement ce que fait l'op\u00e9rateur *.

    char* shoe = \"Nike Mag\";\n\nprintf(\"%p\", shoe);  // Affiche d'une chaussure (p.ex. 0x12345678)\nprintf(\"%s\", *shoe);  // Affiche la chaussure (c-\u00e0-d. Nike Mag)\n

    Amusons-nous. Prenons le post-it de l'exemple pr\u00e9c\u00e9dent et d\u00e9cidons de le d\u00e9chirer et allons le cacher quelque part. Prenons l'adresse de cet emplacement et \u00e9crivons-la sur un nouveau post-it. Nous avons maintenant un post-it avec l'adresse d'un post-it qui contient l'adresse d'un magasin de chaussures. Pour obtenir les chaussures, il nous faut d\u00e9r\u00e9f\u00e9rencer deux fois\u2009:

    char *shoe = \"Nike Mag\";\nchar **p = &shoe;\n\nprintf(\"%s\", **p);  // Affiche la chaussure (c-\u00e0-d. Nike Mag)\n

    Cette op\u00e9ration est appel\u00e9e d\u00e9r\u00e9f\u00e9rencement multiple. Elle peut \u00eatre r\u00e9p\u00e9t\u00e9e autant de fois que n\u00e9cessaire\u2009:

    int a = 42;\nint *b = &a;\nint **c = &b;\nint ***d = &c;\nint ****e = &d;\n\nprintf(\"%d\", ****e);  // Affiche 42\nprintf(\"%d\", ***d);   // Affiche 42\nprintf(\"%d\", **c);    // Affiche 42\nprintf(\"%d\", *b);     // Affiche 42\nprintf(\"%d\", a);      // Affiche 42\n

    Comme l'op\u00e9ration & et la r\u00e9ciproque de l'op\u00e9ration *, il est possible de les combiner\u2009:

    int a = 42;\nint *b = &a;\nint c = *&*&*&*b;\n\nassert(c == a);\n
    ", "tags": ["ptr"]}, {"location": "course-c/20-composite-types/pointers/#arithmetique-de-pointeurs", "title": "Arithm\u00e9tique de pointeurs", "text": "

    Fondamentalement un pointeur n'est rien d'autre qu'une variable qui contient une adresse m\u00e9moire. D\u00e8s lors on peut imaginer vouloir incr\u00e9menter cette adresse pour r\u00e9v\u00e9ler la valeur suivante en m\u00e9moire. On dit qu'un pointeur est un ordinal.

    Dans l'exemple suivant, la boucle for affiche les caract\u00e8res de la cha\u00eene de caract\u00e8res str jusqu'\u00e0 ce qu'il rencontre le caract\u00e8re nul '\\0' :

    char str[] = \"Le vif z\u00e9phyr jubile sur les kumquats du clown gracieux\";\n\nfor (char* ptr = str; *ptr; ptr++) { putchar(*ptr); }\n

    En pratique cette \u00e9criture serait plus \u00e9l\u00e9gante avec une boucle while car le nombre d'it\u00e9ration est inconnu\u2009: il d\u00e9pend de la longueur de la cha\u00eene de caract\u00e8res. On pr\u00e9f\u00e8re donc\u2009:

    char* ptr = str;\nwhile (*ptr) { putchar(*ptr++); }\n

    Si nous devions nous arr\u00eater \u00e0 cette \u00e9tape, il serait tentant de croire que l'arithm\u00e9tique des pointeurs se r\u00e9duit \u00e0 une simple incr\u00e9mentation de l'adresse m\u00e9moire de 1 octet \u00e0 chaque \u00e9tape. Cependant, la r\u00e9alit\u00e9 est autrement plus nuanc\u00e9e. Vous \u00eates-vous jamais interrog\u00e9 sur la raison pour laquelle un pointeur est toujours associ\u00e9 \u00e0 un type, tel qu\u2019un int ou un char ? Apr\u00e8s tout, stocker une adresse m\u00e9moire ne requiert pas, en soi, de conna\u00eetre la nature de l'information qu'elle d\u00e9signe. Cette association r\u00e9pond pourtant \u00e0 deux exigences fondamentales.

    Assistance au compilateur\u2009: Le compilateur, pour produire le code machine appropri\u00e9, doit imp\u00e9rativement conna\u00eetre le type de la variable point\u00e9e. En effet, il doit savoir combien d'octets lire en m\u00e9moire pour obtenir la valeur lors d'un d\u00e9r\u00e9f\u00e9rencement. Ainsi, lorsqu\u2019un pointeur d\u00e9signe un double, le compilateur sait qu\u2019il devra lire 8 octets en m\u00e9moire pour restituer la valeur.

    Arithm\u00e9tique des pointeurs\u2009: Pour garantir une arithm\u00e9tique coh\u00e9rente, celle-ci est d\u00e9finie en fonction du type du pointeur. Si ptr pointe vers un entier, l'op\u00e9ration ptr++ n'incr\u00e9mente pas simplement l'adresse d\u2019un octet, mais bien de sizeof(int) octets. De m\u00eame, l'expression ptr + 2 augmente l'adresse de 2 * sizeof(int) octets.

    Ainsi, selon le type du pointeur, l'arithm\u00e9tique des pointeurs s'ajuste\u2009:

    int32_t *p = (int32_t *)1;\nassert(p + 2 == (int32_t *)9);\n\nint64_t *q = (int64_t *)1;\nassert(q + 2 == (int64_t *)17);\n

    Il convient de souligner que l'arithm\u00e9tique des pointeurs se limite aux op\u00e9rations d'addition et de soustraction. Les autres op\u00e9rations arithm\u00e9tiques, telles que la multiplication ou la division, ne sont pas d\u00e9finies pour les pointeurs.

    ", "tags": ["double", "ptr", "for", "str", "while", "char", "int"]}, {"location": "course-c/20-composite-types/pointers/#carre-magique", "title": "Carr\u00e9 magique", "text": "

    Prenons un autre exemple. Imaginons que l'on souhaite repr\u00e9senter le carr\u00e9 magique suivant\u2009:

    \u250c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2510\n\u2502 4 \u2502 9 \u2502 2 \u2502\n\u251c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2524\n\u2502 3 \u2502 5 \u2502 7 \u2502\n\u251c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2524\n\u2502 8 \u2502 1 \u2502 6 \u2502\n\u2514\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2518\n

    On peut le repr\u00e9senter en m\u00e9moire lin\u00e9airement et utiliser de l'arithm\u00e9tique de pointeur pour le dessiner. C'est \u00e0 dire qu'il n'est pas n\u00e9cessaire de d\u00e9clarer explicitement un tableau \u00e0 deux dimensions. On peut le repr\u00e9senter de la mani\u00e8re suivante\u2009:

    char magic[] = \"492\" \"357\" \"816\"; // \u00c9quivalent \u00e0 \"492357816\"\n\nchar *ptr = magic;\n\nfor (size_t row = 0; row < 3; ++row) {\n    for (size_t col = 0; col < 3; ++col)\n        putchar(*(ptr + row * 3 + col));\n    putchar('\\n');\n}\n

    Vous l'arez probablement remarqu\u00e9 l'\u00e9criture *(ptr + row * 3 + col) est \u00e9quivalente \u00e0 ptr[row * 3 + col]. N\u00e9anmoins pour pouvoir utiliser la notation bidiensionnelle, le compilateur doit conna\u00eetre la taille de la deuxi\u00e8me dimension, et celle-ci n'est pas explicitement d\u00e9clar\u00e9e. Si vous pr\u00e9f\u00e9rez la notation traditionnelle, l'exemple suivant est \u00e9quivalent \u00e0 celui ci-dessus\u2009:

    char magic[][3] = {\"792\", \"357\", \"816\"};\n\nfor (size_t row = 0; row < 3; row++) {\n    for (size_t col = 0; col < 3; col++)\n        putchar(magic[row][col]);\n    putchar('\\n');\n}\n

    On peut pousser l'exemple plus loin. Imaginons que vous avez des donn\u00e9es en m\u00e9moires, agenc\u00e9es lin\u00e9airements, mais que vous ne connaissez pas \u00e0 priori la taille de la matrice, c'est le cas de la d\u00e9claration char magic[] = \"492357816\";. Dans ce cas, seul la premi\u00e8re solution est envisageable. Cependant, la seconde est bien plus \u00e9l\u00e9gante et plus lisible. Peut-on avoir le beurre et l'argent du beurre\u2009? Oui, avec un peu de ruse.

    On peut d\u00e9clarer un nouveau pointeur de type tableau \u00e0 deux dimensions et le caster sur le pointeur magic. C'est \u00e0 dire que l'on va dire au compilateur que le pointeur magic est en r\u00e9alit\u00e9 un tableau \u00e0 deux dimensions. C'est une pratique courante en C.

    #include <stdio.h>\n#include <stdlib.h>\n#include <stdint.h>\n#include <assert.h>\n#include <string.h>\n\nchar *magic = \"492357816\";\nconst int size = 3;\n\nint main() {\n\n    assert(strlen(magic) == size * size);\n\n    char(*p)[size] = (char(*)[size])magic;\n    for (size_t row = 0; row < 3; row++) {\n        for (size_t col = 0; col < 3; col++)\n            putchar(p[row][col]);\n        putchar('\\n');\n    }\n}\n

    Tentons de d\u00e9cortiquer la d\u00e9claration ambigu\u00eb\u2009:

    char               // Type du pointeur\n    (*p)           // D\u00e9claration du pointeur\n        [size]     // Tableau de taille size\n=\n(char\n     (*)\n        [size])    // Identique mais via un cast\nmagic;\n

    Aprp\u00e8s l'assignation =, on a simplement le m\u00eame type permettant de transtyper le pointeur magic vers le type souhait\u00e9. C'est n\u00e9cessaire pour \u00e9viter que le compilateur ne g\u00e9n\u00e8re une alterte indiquant qu'un pointeur d'un certain type soit assign\u00e9 \u00e0 un pointeur d'un autre type. Dans le cast, le nom de la variable p est simplement ignor\u00e9.

    Avant l'assignation, la d\u00e9claration compliqu\u00e9e \u00e0 comprendre est la parenth\u00e8se (*p)[size]. En effet, le compilateur ne sait pas si *p[size] est un tableau de pointeurs ou un pointeur sur un tableau. Il est donc n\u00e9cessaire de placer des parenthjsons pour indiquer que *p est un pointeur sur un tableau de taille size. Rappelez-vous de la priorit\u00e9 des op\u00e9rateurs en C. L'op\u00e9rateur crochet [] a une priorit\u00e9 plus \u00e9lev\u00e9e que l'op\u00e9rateur ast\u00e9risque *. Donc\u2009:

    char *p[size]; // Tableau de taille size sur des pointeurs de type char\nchar (*p)[size]; // Pointeur sur un tableau de char de taille size\n

    Pour terminer l'exemple, on peut mentionner que l'on peut simplifier l'\u00e9criture en s'affranchissant du pointeur interm\u00e9diaire. On peut directement caster le pointeur magic dans la boucle for :

    putchar(((char(*)[size])magic)[row][col]);\n
    ", "tags": ["size", "for", "magic"]}, {"location": "course-c/20-composite-types/pointers/#cas-dun-tableau", "title": "Cas d'un tableau", "text": "

    Cela n'aura plus de secret pour vous, un tableau est un pointeur. N\u00e9anmoins il existe quelques subtilit\u00e9s. D'une part un tableau est un pointeur constant, c'est \u00e0 dire que l'adresse m\u00e9moire ne peut \u00eatre modifi\u00e9e. L'\u00e9criture suivante est donc incorrecte\u2009:

    int array[10];\n\nint *ptr = array;\nptr += 1; // Correct\n\narray += 1; // Erreur de compilation\n

    Prenons le cas d'un tableau \u00e0 deux dimensions\u2009:

    int array[3][3] = {\n    {4, 9, 2},\n    {3, 5, 7},\n    {8, 1, 6}\n};\n

    On peut r\u00e9sumer les types de pointeurs associ\u00e9s \u00e0 chaque \u00e9l\u00e9ment de ce tableau\u2009:

    Types de pointeurs associ\u00e9s \u00e0 un tableau \u00e0 deux dimensions D\u00e9claration Type du pointeur Description array int (*)[3] Pointeur sur un tableau de 3 entiers &array int (*)[3][3] Pointeur sur un tableau de 3 tableaux de 3 entiers array[0] int * Pointeur sur un entier array[0][0] int Entier

    Vous avez vonstat\u00e9 lors de l'exemple du carr\u00e9 magique qu'il n'\u00e9tait pas possible d'\u00e9crire magic[row][col] \u00e0 partir de la d\u00e9claration de pointeur char *magic car le compilateur ne connaissait pas la taille de la deuxi\u00e8me dimension. Dans le cas d'une d\u00e9claration de type int (*)[3] le compilateur sait que la deuxi\u00e8me dimension est de taille 3. On peut donc \u00e9crire array[row][col] sans probl\u00e8me.

    L'\u00e9criture &array m\u00e9rite une explication. En effet, &array est un pointeur sur un tableau de 3 tableaux de 3 entiers. C'est \u00e0 dire que &array est \u00e9quivalent \u00e0 int (*)[3][3]. C'est une subtilit\u00e9 de la syntaxe C. En effet, &array est l'adresse du tableau array qui est un tableau de 3 tableaux de 3 entiers, alors que array est simplement un pointeur sur un tableau de 3 entiers. Pourquoi cette subtilit\u00e9\u2009?

    Lorsque l'on utilise des pointeurs, l'objectif est d'utiliser l'arithm\u00e9tique de pointeurs pour parcourir les donn\u00e9es en m\u00e9moire. Donc pour un tableau on s'attends \u00e0 qu'ajouter une unit\u00e9 au pointeur nous m\u00e8ne \u00e0 la valeur suivante. Pour le cas int array[3][3], ajouter une unit\u00e9 au pointeur array nous m\u00e8ne \u00e0 la premi\u00e8re valeur du tableau suivant. C'est \u00e0 dire que array + 1 pointe sur le tableau array[1] et array + 2 pointe sur le tableau array[2], ce qui confirme le sucre syntaxique de l'op\u00e9rateur crochet [] qui est \u00e9quivalent \u00e0 l'arithm\u00e9tique de pointeur *(array + i).

    N\u00e9anmoins si on consid\u00e8re l'enti\u00e8ret\u00e9 de array et que l'on souhaite avec +1 aller apr\u00e8s le dernier \u00e9l\u00e9ment du tableau, il est n\u00e9cessaire de conna\u00eetre la taille totale du tableau c'est \u00e0 dire 9 \u00e9l\u00e9ments. Demander explicitement l'adresse de array retourne un pointeur sur un tableau de 3 tableaux de 3 entiers. C'est \u00e0 dire que &array + 1 pointe sur le tableau suivant de 3 tableaux de 3 entiers, car on pourrait tr\u00e8s bien imaginer array comme un \u00e9l\u00e9ment d'un tableau plus grand\u2009:

    int matrices[2][3][3] = {\n    {{4, 9, 2},\n        {3, 5, 7},\n        {8, 1, 6}},\n    {{1, 2, 3},\n        {4, 5, 6},\n        {7, 8, 9}}};\n\nint(*array)[3][3] = &(matrices[0]); // Parenth\u00e8ses facultatives [] > &\n
    ", "tags": ["array", "int"]}, {"location": "course-c/20-composite-types/pointers/#resume", "title": "R\u00e9sum\u00e9", "text": "

    L'arithm\u00e9tique de pointeur est donc chose courante avec les tableaux. \u00c0 vrai dire, les deux concepts sont interchangeables\u2009:

    Arithm\u00e9tique sur tableau unidimensionnel \u00c9l\u00e9ment Premier Deuxi\u00e8me Troisi\u00e8me n i\u00e8me Acc\u00e8s tableau a[0] a[1] a[2] a[n - 1] Acc\u00e8s pointeur *a *(a + 1) *(a + 2) *(a + n - 1)

    De m\u00eame, l'exercice peut \u00eatre r\u00e9p\u00e9t\u00e9 avec des tableaux \u00e0 deux dimensions\u2009:

    Arithm\u00e9tique sur tableau bidimensionnel \u00c9l\u00e9ment Premier Deuxi\u00e8me n ligne m colonne Acc\u00e8s tableau a[0][0] a[1][1] a[n - 1][m - 1] Acc\u00e8s pointeur *(*(a+0)+0) *(*(a+1)+1) *(*(a+i-1)+j-1)"}, {"location": "course-c/20-composite-types/pointers/#chaines-de-caracteres", "title": "Cha\u00eenes de caract\u00e8res", "text": "

    Les cha\u00eenes de caract\u00e8res sont des tableaux de caract\u00e8res termin\u00e9s par un caract\u00e8re nul '\\0'. Il est donc possible de d\u00e9clarer une cha\u00eene de caract\u00e8res de la mani\u00e8re suivante\u2009:

    static const char* conjonctions[] = {\n    \"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"\n};\n

    Pointeur sur une cha\u00eene de caract\u00e8re

    Dans ce cas, les cha\u00eenes \"mais\", \"ou\"... sont des constantes litt\u00e9rales de type const char*. Elles sont stock\u00e9es dans un segment de m\u00e9moire en lecture seule. Le tableau conjonctions est donc un tableau de pointeurs sur des cha\u00eenes de caract\u00e8res.

    Cette structure est tr\u00e8s exactement la m\u00eame que pour les arguments transmis \u00e0 la fonction main: la d\u00e9finition char *argv[].

    On se retrouve donc avec une indirection sur un tableau de pointeurs. Pour acc\u00e9der \u00e0 un \u00e9l\u00e9ment de la cha\u00eene de caract\u00e8res, on utilisera l'op\u00e9rateur crochet []. Il faut noter que dans ce cas, rien ne garanti que les \u00e9l\u00e9ments soient contigus en m\u00e9moire. Consid\u00e9rons l'exemple suivant\u2009:

    char *a = \"mais\";\nchar *b = \"ou\";\nchar *c = \"est\";\nchar *d = \"donc\";\nchar *e = \"or\";\nchar *f = \"ni\";\nchar *g = \"car\";\n\nchar *conjonctions[] = {a, b, c, d, e, f, g};\n

    Chaque variable a \u00e0 g sont des pointeurs sur des cha\u00eenes de caract\u00e8res mais vous ne savez pas n\u00e9cessairement o\u00f9 elles sont stock\u00e9es en m\u00e9moire.

    On est en droit de se demander quel est l'avantage de passer par des pointeurs, on pourrait tr\u00e8s bien d\u00e9clarer le tableau de conjonctions comme un char conjonctions[][5] et avoir ceci\u2009:

    char conjonctions[][5] = {\n    \"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"\n};\n

    N\u00e9anmoins dans ce cas, de l'espace m\u00e9moire est gaspill\u00e9 car chaque cha\u00eene de caract\u00e8res doit avoir la m\u00eame taille, et pour d\u00e9clarer le tableau il faut conna\u00eetre la taille de la plus grande cha\u00eene de caract\u00e8res. En revanche cette m\u00e9thode est de r\u00e9duire le niveau d'imbrications. Il n'y a plus de tableau interm\u00e9diaire de pointeurs.

    ", "tags": ["conjonctions", "main"]}, {"location": "course-c/20-composite-types/pointers/#structures", "title": "Structures", "text": "

    Les pointeurs peuvent bien entendu \u00eatre utilis\u00e9s avec n'importe quel type de donn\u00e9es. Les structures ne font pas exception \u00e0 la r\u00e8gle. On peut d\u00e9finir une structure de donn\u00e9es et d\u00e9clarer un pointeur sur cette structure\u2009:

    typedef struct Date {\n    unsigned char day;\n    unsigned char month;\n    unsigned int year;\n} Date;\n\nDate date;\nDate *p;  // Pointeur sur un type Date\n\np = &date;  // Assignation de l'adresse de date \u00e0 p\n

    Le pointeur reste un pointeur, soit un espace m\u00e9moire qui contient une adresse vers la structure Date. En cons\u00e9quence, la taille de ce pointeur est de 8 bytes sur une machine LP64 (64 bits) :

    Date *p;\nassert(sizeof(p) == 8);\n

    Le langage C introduit un autre sucre syntaxique pour d\u00e9r\u00e9f\u00e9rencer un \u00e9l\u00e9ment d'une structure. En effet, il est possible d'\u00e9crire p->day pour acc\u00e9der au champ day de la structure point\u00e9e par p. C'est \u00e9quivalent \u00e0 (*p).day :

    a->b \u2261 (*a).b\n

    Ajoutons que concernant l'arithm\u00e9tique de pointeur, il est important de noter que l'op\u00e9ration p + 1 incr\u00e9mente le pointeur de sizeof(Date) bytes. C'est \u00e0 dire que p + 1 pointe sur la structure suivante de type Date. On peut donc avoir un tableau de structures de la mani\u00e8re suivante\u2009:

    #define MAX 10\nDate dates[MAX];\nDate *p = dates;\n\nfor (size_t i = 0; i < MAX; i++) {\n    p->day = i;\n    p->month = i;\n    p->year = 2021 + i;\n    p++;\n}\n

    Cette \u00e9criture est d\u00e9routante et peut \u00eatre une cause fr\u00e9quente de trichotillomanie (arraquage de cheveux). Prenons l'exemple de deux fonctions, l'une prenant un pointeur vers une date et l'autre une structure date\u2009:

    void print_date1(Date *date) {\n    printf(\"%d/%d/%d\\n\", date->day, date->month, date->year);\n}\n\nvoid print_date2(Date date) {\n    printf(\"%d/%d/%d\\n\", date.day, date.month, date.year);\n}\n

    Lorsque vous d\u00e9cidez de modifier le type de l'argument de la fonction vous devez ajuster les d\u00e9r\u00e9f\u00e9rencements. C'est \u00e0 dire que si vous d\u00e9cidez de passer de Date \u00e0 Date* vous devez modifier . en -> et vice versa. En pratique il est toujours pr\u00e9f\u00e9rable de passer par des pointeurs pour \u00e9viter de copier des structures sur la pile. Pour \u00e9viter de les modifier, il est possible de d\u00e9clarer les structures comme const:

    void print_date(const Date *date) {\n    printf(\"%d/%d/%d\\n\", date->day, date->month, date->year);\n}\n
    ", "tags": ["day", "Date", "const"]}, {"location": "course-c/20-composite-types/pointers/#structure-recursive", "title": "Structure r\u00e9cursive", "text": "

    Lorsqu'on utilise des structures de donn\u00e9es plus complexes comme les listes cha\u00een\u00e9es, on a besoin de cr\u00e9er une structure contenant des donn\u00e9es ainsi qu'un pointeur sur l'\u00e9l\u00e9mnent suivant. On peut d\u00e9finir une structure r\u00e9cursive de la mani\u00e8re suivante\u2009:

    typedef struct Element {\n  struct Element *next;  // Pointeur sur l'\u00e9l\u00e9ment suivant\n  unsigned long data;  // Donn\u00e9e\n} Element;\n

    Exemple d'utilisation\u2009:

    Element e[3];\n\n// Premier \u00e9l\u00e9ment de la liste\ne[0].prev = NULL;\ne[0].next = &e[1];\n\n// Second \u00e9l\u00e9ment de la liste\ne[1].prev = &e[0];\ne[1].next = &e[2];\n\n// troisi\u00e8me \u00e9l\u00e9ment de la liste\ne[2].prev = &e[1];\ne[2].next = NULL;\n

    On peut parcourir cette liste de la mani\u00e8re suivante\u2009:

    Element *walk = &e[0];\nwhile (walk) {\n    printf(\"%lu\\n\", walk->data);\n    walk = walk->next;\n}\n

    En effet, tant que le pointeur n'est pas NULL, on peut continuer \u00e0 parcourir la liste. Le dernier \u00e9l\u00e9ment de la liste pointe sur NULL qui fait office de valeur sentinelle pour indiquer la fin de la liste.

    Les structures r\u00e9cursives sont tr\u00e8s utilis\u00e9es en informatique pour repr\u00e9senter des donn\u00e9es hi\u00e9rarchiques. Nous verrons plus tard les notions d'arbres et de graphes qui reposent sur ce concept.

    ", "tags": ["NULL"]}, {"location": "course-c/20-composite-types/pointers/#arguments-de-fonctions", "title": "Arguments de fonctions", "text": "

    Lors de l'introduction aux fonctions nous avons vu que ces derni\u00e8res peuvent recevoir des arguments. Ces arguments peuvent \u00eatre de n'importe quel type, y compris des pointeurs et dans de nombreux cas de figure le passage par pointeur est pr\u00e9f\u00e9rable. Voici quelques cas de figure o\u00f9 le passage par pointeur est recommand\u00e9\u2009:

    Modification de la valeur d'une variable dans une fonction

    void increment(int *a) {\n    (*a)++;\n}\n\nint main() {\n    int a = 0;\n    increment(&a);\n    printf(\"%d\\n\", a);  // Affiche 1\n}\n

    Le cas le plus notable est celui de la fonction scanf qui modifie la valeur de la variable pass\u00e9e en argument. En effet, scanf attend un pointeur sur la variable \u00e0 modifier. Jusqu'ici nous vous avions expliqu\u00e9 qu'il faut toujours utiliser l'esperluette & lors de l'utilisation de scanf. Maintenant vous savez que & est n\u00e9cessaire car scanf attend un pointeur puisque cette fonction modifie la valeur de la variable pass\u00e9e en argument.

    Eviter la copie de donn\u00e9es

    Dans certains cas de figure, les donn\u00e9es pass\u00e9es en argument \u00e0 une fonction peuvent \u00eatre tr\u00e8s grandes et elles sont copi\u00e9es sur la pile \u00e0 chaque appel. C'est le cas pour les structures. Consid\u00e9rons la structure suivante\u2009:

    struct Data {\n    int a[1000];\n};\n\nvoid process(struct Data *data) {\n    // Traitement des donn\u00e9es\n}\n

    Si la structure Data est tr\u00e8s grande, lors de l'appel d'une fonction la structure sera copi\u00e9e int\u00e9gralement sur la pile \u00e0 chaque appel. En revanche, si on passe un pointeur seul l'adresse de la structure sera copi\u00e9e. En pratique on pr\u00e9f\u00e8rera toujours passer des pointeurs pour les structures\u2009:

    process(struct Data data)  // Copie sur la pile\nprocess(struct Data *data) // Passage par r\u00e9f\u00e9rence (adresse)\n
    ", "tags": ["Data", "scanf"]}, {"location": "course-c/20-composite-types/pointers/#plusieurs-valeurs-de-retour", "title": "Plusieurs valeurs de retour", "text": "

    Lorsqu'une fonction doit retourner plusieurs valeurs, il est possible de passer des pointeurs en argument pour stocker les valeurs de retour. Par exemple la fonction compute retourne un statut d'ex\u00e9cution et une valeur calcul\u00e9e\u2009:

    int compute(double x, double *result) {\n    if (x == 0) return -1;\n    *result = 42. / x;\n    return 0;\n}\n

    On observe qu'il faut d\u00e9r\u00e9f\u00e9rencer le pointeur result pour modifier la valeur de la variable point\u00e9e. On peut appeler cette fonction de la mani\u00e8re suivante\u2009:

    double result = 4823.;\nint status = compute(0, &result);\nif (status == 0) {\n    printf(\"Result: %f\\n\", result);\n} else {\n    printf(\"Error: Division by zero\\n\");\n}\n
    ", "tags": ["result", "compute"]}, {"location": "course-c/20-composite-types/pointers/#transtypage-cast", "title": "Transtypage (cast)", "text": "

    Nous avons expliqu\u00e9 plus haut qu'un pointeur est g\u00e9n\u00e9ralement associ\u00e9 \u00e0 un type permettant l'arithm\u00e9tique de pointeurs. N\u00e9anmoins il existe un cas particulier, celui du type void. Un pointeur sur void est un pointeur neutre, c'est \u00e0 dire qu'il peut pointer sur n'importe quel type de donn\u00e9es. Comme le type n'est pas connu, l'arithm\u00e9tique de pointeurs n'est pas possible. Il est n\u00e9cessaire alors de transtyper le pointeur pour pouvoir l'utiliser.

    int a = 42;\nvoid *ptr = &a;\nptr++;  // Erreur de compilation\n

    La fonction memcpy est un exemple typique de l'utilisation de pointeurs sur void. Cette fonction permet de copier une r\u00e9gion m\u00e9moire vers une autre. Elle est d\u00e9clar\u00e9e de la mani\u00e8re suivante\u2009:

    void *memcpy(void *dest, const void *src, size_t n);\n

    Elle peut \u00eatre appel\u00e9e avec n'importe quel type de donn\u00e9es. Par exemple pour copier un tableau d'entiers, une structure ou m\u00eame un tableau de structures. En sp\u00e9cifiant le type explicite du pointeur, il faudrait autant de fonctions memcpy que de type possible, ce qui n'est ni raisonnable, ni m\u00eame imaginable. Face \u00e0 ce dilemme, on utilise un pointeur neutre. Consid\u00e9rons ces diff\u00e9rents types\u2009:

    char message[] = \"Mind the gap, please!\";\nint array[128];\nstruct { int a; char b; float c[3] } elements[128];\n

    On peut assigner l'adresse de ces variables \u00e0 un pointeur void mais on perd au passage l'arithm\u00e9tique de pointeurs\u2009:

    void *ptr;\n\nptr = message;\nptr = array;\nptr = elements;\n

    Ce pointeur neutre peut ensuite \u00eatre transtyp\u00e9 pour \u00eatre utilis\u00e9. La cl\u00e9 est dans le standard ISO/IEC 9899:2011 section 6.3.2.3 page 55\u2009:

    A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again\u2009; the result shall compare equal to the original pointer.

    Autrement dit, il n'est pas n\u00e9cessaire ni recommand\u00e9 de faire un transtypage explicite pour convertir vers et en provenance d'un pointeur sur void. Et donc, l'astuce de memcpy est que la fonction accepte n'importe quel type de pointeur. Et quant \u00e0 l'impl\u00e9mentation de cette fonction me direz-vous\u2009? Une possibilit\u00e9 serait\u2009:

    void memcpy(void *dest, void *src, size_t n)\n{\n    char* csrc = src;\n    char* cdest = dest;\n\n    for (size_t i = 0; i < n; i++)\n        cdest[i] = csrc[i];\n}\n

    O\u00f9 plus concis\u2009:

    void memcpy(void *dest, void *src, size_t n)\n{\n    for (size_t i = 0; i < n; i++)\n        ((char*)dst)[i] = ((char*)src)[i];\n}\n

    En r\u00e9alit\u00e9 ce n'est pas la fa\u00e7on dont memcpy est impl\u00e9ment\u00e9e. En effet, cette fonction est tr\u00e8s utilis\u00e9e et doit \u00eatre extr\u00eamement performante. Il est donc n\u00e9cessaire de tenir compte de l'architecture du processeur et de la taille des donn\u00e9es \u00e0 copier. L'impl\u00e9mentation de memcpy est une affaire de cuisine interne du compilateur. L'impl\u00e9mentation d\u00e9pend donc de l'architecture cible et doit tenir compte des \u00e9ventuels effets de bords. Par exemple s'il faut copier un tableau de 9 x 32 bits. Une architecture 64-bits aura une grande facilit\u00e9 \u00e0 copier les 8 premiers octets, mais quant au dernier, il s'agit d'un cas particulier. Vous \u00eates comme Thomas l'ap\u00f4tre, et ne me croyez pas\u2009? Alors, digressons et essayons\u2009:

    #include <string.h>\n#include <stdio.h>\n\nint main(void)\n{\n    char a[] = \"La Broye c'est fantastique!\";\n    char b[sizeof(a)];\n\n    memcpy(a, b, sizeof(a));\n\n    printf(\"%s %s\", a, b);\n}\n

    En observant l'assembleur cr\u00e9\u00e9 \u00e0 la compilation (avec gcc -S -O3), on peut voir que la copie est effectu\u00e9e en 6 instructions. Par ailleurs, il n'y a aucun appel de fonction \u00e0 memcpy comme c'est le cas pour printf (bl printf). Voici le code assembleur g\u00e9n\u00e9r\u00e9\u2009:

    main :\n    // Entry\n    str     lr, [sp, #-4]!\n    sub     sp, sp, #60\n\n    // Inline memcpy\n    mov     ip, sp      // Destination address\n    add     lr, sp, #28 // Source address (char b located 28 octets after a)\n\n    ldmia   lr!, {r0, r1, r2, r3}   // Load 4 x 32-bits\n    stmia   ip!, {r0, r1, r2, r3}   // Store 4 x 32-bits\n\n    ldm     lr, {r0, r1, r2}        // Load 3 x 32-bits\n    stm     ip, {r0, r1, r2}        // Store 3 x 32-bits\n\n    // Display (printf)\n    add     r2, sp, #28\n    mov     r1, sp\n    ldr     r0, .L4\n    bl      printf\n\n    // Exit\n    mov     r0, #0\n    add     sp, sp, #60\n    ldr     pc, [sp], #4\n.L4 :\n    .word   .LC0\n.LC0 :\n    .ascii  \"La Broye c'est fantastique!\\000\"\n

    Vous pouvez jouer avec cet exemple sur le site godbolt.

    Arithm\u00e9tique et void*

    Prenons l'exemple suivant\u2009:

    int main() {\n    int a = 42;\n    void *p = &a;\n    p++; // <--\n}\n

    Formellement ceci devrait mener \u00e0 une erreur de compilation, car void n'a pas de substance, et donc aucune taille associ\u00e9e. N\u00e9anmoins gcc est tr\u00e8s permissif de base et (\u00e0 ma grande surprise), il ne g\u00e9n\u00e8re par d\u00e9faut ni warning, ni erreurs sans l'option -Wpointer-arith.

    En compilant ce code avec gcc -Wall -Wextra -pedantic on obtient\u2009:

    warning: pointer of type 'void *' used in arithmetic [-Wpointer-arith]\n

    N\u00e9anmoins sans l'option -Wpointer-arith aucune erreur n'est g\u00e9n\u00e9r\u00e9e. C'est pourquoi il est recommand\u00e9 de toujours compiler avec les options -Wall -Wextra -pedantic pour obtenir un code plus robuste.

    Exercice 1\u2009: Void*

    Que pensez-vous que sizeof(void*) devrait retourner sur une architecture 64-bits\u2009?

    • 1
    • 2
    • 4
    • 8
    • 0
    Solution

    Un pointeur reste un pointeur, c'est une variable qui contient une adresse m\u00e9moire. Sur une architecture 64-bits, un pointeur est cod\u00e9 sur 8 bytes. sizeof(void*) retourne donc 8.

    ", "tags": ["void", "memcpy", "gcc", "printf"]}, {"location": "course-c/20-composite-types/pointers/#pointeurs-de-fonctions", "title": "Pointeurs de fonctions", "text": "

    Un pointeur peut pointer n'importe o\u00f9 en m\u00e9moire, et donc il peut \u00e9galement pointer non pas sur une variable, mais sur une fonction. Les pointeurs de fonctions sont tr\u00e8s utiles pour des fonctions de rappel (callback).

    Par exemple si on veut appliquer une transformation sur tous les \u00e9l\u00e9ments d'un tableau, mais que la transformation n'est pas connue \u00e0 l'avance. On pourrait alors \u00e9crire\u2009:

    int is_odd(int n) {\n    return !(n % 2);\n}\n\nvoid map(int array[], int (*callback)(int), size_t length) {\n    for (size_t i = 0; i < length; i++)\n        array[i] = callback(array[i]);\n}\n\nvoid main(void) {\n    int array[] = {1,2,3,4,5};\n    map(array, is_odd);\n}\n

    Avec la r\u00e8gle gauche droite, on parvient \u00e0 d\u00e9cortiquer la d\u00e9claration\u2009:

    int (*callback)(int)\n      ^^^^^^^^        callback est\n              ^\n     ^                un pointeur sur\n               ^^^^^  une function prenant un int\n^^^                   et retournant un int\n

    Les pointeurs de fonctions sont beaucoup utilis\u00e9s en programmation fonctionnelle. Ils permettent de passer des actions en argument \u00e0 d'autres fonctions. Par exemple la fonction qsort de la biblioth\u00e8que standard C permet de trier un tableau. Elle prend en argument un pointeur sur le tableau \u00e0 trier, le nombre d'\u00e9l\u00e9ments, la taille d'un \u00e9l\u00e9ment et une fonction de comparaison.

    Cette fonction de comparaison est un pointeur sur une fonction qui prend deux \u00e9l\u00e9ments et retourne un entier n\u00e9gatif si le premier est inf\u00e9rieur au second, z\u00e9ro s'ils sont \u00e9gaux et un entier positif si le premier est sup\u00e9rieur au second.

    int compare(const void *a, const void *b) {\n    return (*(int*)a - *(int*)b);\n}\n\nint main(void) {\n    int array[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};\n    qsort(array, sizeof(array) / sizeof(array[0]), sizeof(array[0]), compare);\n}\n

    Les pointeurs de fonctions sont \u00e9galement utilis\u00e9s pour effectuer des op\u00e9rations diff\u00e9rentes selon des crit\u00e8res. Admettons que l'on souhaite r\u00e9aliser un parseur d'expressions math\u00e9matiques en format infix\u00e9. C'est \u00e0 dire que les op\u00e9rateurs sont plac\u00e9s apr\u00e8s les nombres. 2+3*8-2 s'\u00e9crirait 238*+2-.

    Les op\u00e9rateurs selon la table ASCII sont + (43), - (45), * (42) et / (47). Un tableau de correspondance peut \u00eatre cr\u00e9\u00e9 pour associer un op\u00e9rateur \u00e0 une fonction\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nvoid display(double a, double b, char op) {\n    printf(\"%f %c %f\\n\", a, op, b);\n}\n\ndouble add(double a, double b) { display(a, b, '+'); return a + b; }\ndouble sub(double a, double b) { display(a, b, '-'); return a - b; }\ndouble mul(double a, double b) { display(a, b, '*'); return a * b; }\ndouble divide(double a, double b) { display(a, b, '/'); return a / b; }\n\ndouble (*operations[])(double, double) = {\n    /* 42 */ mul,   // *\n    /* 43 */ add,   // +\n    /* 44 */ NULL,  // ,\n    /* 45 */ sub,   // -\n    /* 46 */ mul,   // .\n    /* 47 */ divide // /\n};\n\nint parse(char *expression) {\n    int stack[128];\n    int top = -1;\n\n    while (*expression) {\n        const char c = *expression;\n        if (c >= '0' && c <= '9')\n            stack[++top] = (double)(c - '0');\n        else if (top > 0 && c >= '*' && c <= '/') {\n            const int index = c - '*';\n            int a = stack[top--], b = stack[top--];\n            stack[++top] = operations[index](b, a);\n        }\n        else {\n            printf(\"Invalid expression (stack: %d)\\n\", top);\n            exit(EXIT_FAILURE);\n        }\n        expression++;\n    }\n    return stack[top];\n}\n\nint main(void) {\n    char expression[] = \"238*+4-\";\n    printf(\"%d\\n\", parse(expression));\n}\n

    Le programme ci-dessus affiche\u2009:

    ./a.out\n3.000000 * 8.000000\n2.000000 + 24.000000\n26.000000 - 4.000000\n22\n
    ", "tags": ["qsort"]}, {"location": "course-c/20-composite-types/pointers/#la-regle-gauche-droite", "title": "La r\u00e8gle gauche-droite", "text": "

    Cette r\u00e8gle est une recette magique permettant de correctement d\u00e9cortiquer une d\u00e9claration C contenant des pointeurs. Il faut tout d'abord lire\u2009:

    R\u00e8gles gauche droite Symbole Traduction Direction * pointeur sur Toujours \u00e0 gauche [] tableau de Toujours \u00e0 droite () fonction retournant Toujours \u00e0 droite Premi\u00e8re \u00e9tape

    Trouver l'identifiant et se dire L'identifiant est.

    Deuxi\u00e8me \u00e9tape

    Chercher le symbole \u00e0 droite de l'identifiant. Si vous trouvez un (), vous savez que cet identifiant est une fonction et vous avez L'identifiant est une fonction retournant. Si vous trouvez un [] vous dites alors L'identifiant est un tableau de. Continuez \u00e0 droite jusqu'\u00e0 ce que vous \u00eates \u00e0 court de symboles, OU que vous trouvez une parenth\u00e8se fermante ).

    Troisi\u00e8me \u00e9tape

    Regardez le symbole \u00e0 gauche de l'identifiant. S\u2019il n'est aucun des symboles pr\u00e9c\u00e9dents, dites quelque chose comme int. Sinon, convertissez le symbole en utilisant la table de correspondance. Continuez d'aller \u00e0 gauche jusqu'\u00e0 ce que vous \u00eates \u00e0 court de symboles OU que vous rencontrez une parenth\u00e8se ouvrante (.

    Ensuite...

    Continuez les \u00e9tapes 2 et 3 jusqu'\u00e0 ce que vous avez une d\u00e9claration compl\u00e8te.

    Cet algorithme peut \u00eatre repr\u00e9sent\u00e9 par le diagramme suivant\u2009:

    Diagramme de la r\u00e8gle gauche-droite

    Voici quelques exemples\u2009:

    int *p[];\n
    1. Trouver l'identifiant\u2009: p: p est

      int *p[];\n     ^\n
    2. Se d\u00e9placer \u00e0 droite: p est un tableau de

      int *p[];\n      ^^\n
    3. Se d\u00e9placer \u00e0 gauche: p est un tableau de pointeurs sur

      int *p[];\n    ^\n
    4. Continuer \u00e0 gauche: p est un tableau de pointeurs sur un int

      int *p[];\n^^^\n
    ", "tags": ["int"]}, {"location": "course-c/20-composite-types/pointers/#cdecl", "title": "cdecl", "text": "

    Il existe un programme nomm\u00e9 cdecl qui permet de d\u00e9coder de complexes d\u00e9claration c\u2009:

    $ cdecl 'char (*(*x[3])())[5]'\ndeclare x as array 3 of pointer to function returning pointer to array 5 of char\n

    Une version en ligne est \u00e9galement disponible.

    Manuellement cette d\u00e9claration serait\u2009:

    char (*(*foo[3])())[5]\n         ^^^            L'identifiant `foo` est\n            ^^^         un tableau de 3\n        ^      <        pointeurs sur\n       >        ^^      une fonction prenant les arguments `` et retournant un\n      *           <     pointeur sur\n     >             ^^^  un tableau de 5\n^^^^                  < char\n
    "}, {"location": "course-c/20-composite-types/pointers/#implementation", "title": "Impl\u00e9mentation", "text": "

    Pour l'exemple, voici une impl\u00e9mentation tr\u00e8s rudimentaire de cdecl. Elle n'est pas compl\u00e8te mais elle donne le principe de fonctionnement\u2009:

    #include <ctype.h>\n#include <stdbool.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nint is_type(char *str) {\n   const char *type[] = {\"int\",   \"char\",   \"short\", \"long\",\n                         \"float\", \"double\", \"void\"};\n   const int n = sizeof(type) / sizeof(type[0]);\n   for (int i = 0; i < n; i++) {\n      const int len = strlen(type[i]);\n      if (strncmp(str, type[i], len) == 0) return len;\n   }\n   return 0;\n}\n\nchar *find_identifier(char *decl, size_t *len) {\n   // Find first string that is not a type\n   while (*decl) {\n      if (isalpha(*decl)) {\n         int len = is_type(decl);\n         if (len) {\n            decl += len;\n            continue;\n         }\n         break;\n      }\n      decl++;\n   }\n\n   // Find end of identifier and its length\n   char *c = decl;\n   while (isalnum(*c) || *c == '_') c++;\n   *len = c - decl;\n   return decl;\n}\n\nchar *explore_right(char *right) {\n   while (*right && *right != ')') {\n      if (*right == '[') {\n         right++;\n         printf(\"un tableau de \");\n         while (*right != ']') {\n            putchar(*right++);\n         }\n         putchar(' ');\n      } else if (*right == '(') {\n         right++;\n         printf(\"une fonction aux arguments '\");\n         while (*right != ')') putchar(*right++);\n         printf(\"' et retournant un \");\n      }\n      right++;\n   }\n   return right;\n}\n\nchar *explore_left(char *left, char *start) {\n   while (left >= start && *left != '(') {\n      if (*left == '*') printf(\"pointeur sur \");\n      if (*left == ']') {\n         printf(\"un tableau de \");\n         while (left >= start && *left != '[') left--;\n      }\n      if (*left == ')') {\n         printf(\"une fonction retournant \");\n         while (left >= start && *left != '(') left--;\n      } else if (isalpha(*left)) {\n         int len = 0;\n         while (left >= start && isalpha(*left)) {\n            left--;\n            len++;\n         }\n         printf(\"%.*s \", len, left + 1);\n      }\n      left--;\n   }\n   return left;\n}\n\nvoid cdecl(char *decl) {\n   // Step 1 : Find identifier\n   size_t len;\n   char *left = find_identifier(decl, &len);\n   printf(\"L'identifiant '%.*s' est \", (int)len, left);\n\n   // Step 2 and 3 : Explore right and left\n   char *right = left + len;\n   left--;\n   do {\n      right = explore_right(right) + 1;\n      left = explore_left(left, decl) - 1;\n   } while (left > decl && *right);\n   putchar('\\n');\n}\n\nint main() { cdecl(\"char (*(*foo[3])(int a))[5]\"); }\n

    Les am\u00e9liorations sur ce code seraient\u2009:

    • G\u00e9rer les erreurs de syntaxe (parenth\u00e8ses manquantes, etc.)
    • Afficher les arguments et la taille des tableaux en explorant \u00e0 gauche
    • G\u00e9rer les pluriels (tableau, tableaux, etc.)
    ", "tags": ["cdecl"]}, {"location": "course-c/20-composite-types/pointers/#initialisation-par-transtypage", "title": "Initialisation par transtypage", "text": "

    L'utilisation de structure peut \u00eatre utile pour initialiser un type de donn\u00e9e en utilisant un autre type de donn\u00e9e. Nous citons ici deux exemples.

    int i = *(int*)(struct { char a; char b; char c; char d; }){'a', 'b', 'c', 'd'};\n
    union {\n    int matrix[10][10];\n    int vector[100];\n} data;\n
    "}, {"location": "course-c/20-composite-types/pointers/#enchevetrement-ou-aliasing", "title": "Enchev\u00eatrement ou Aliasing", "text": "

    Travailler avec les pointeurs demande une attention particuli\u00e8re \u00e0 tous les probl\u00e8mes d'alisasing dans lesquels diff\u00e9rents pointeurs pointent sur une m\u00eame r\u00e9gion m\u00e9moire.

    Mettons que l'on souhaite simplement d\u00e9placer une r\u00e9gion m\u00e9moire vers une nouvelle r\u00e9gion m\u00e9moire. On pourrait impl\u00e9menter le code suivant\u2009:

    void memory_move(char *dst, char*src, size_t size) {\n    for (int i = 0; i < size; i++)\n        *dst++ = *src++;\n}\n

    Ce code est tr\u00e8s simple mais il peut poser probl\u00e8me selon les cas. Imaginons que l'on dispose d'un tableau simple de dix \u00e9l\u00e9ments et de deux pointeurs *src et *dst. Pour d\u00e9placer la r\u00e9gion du tableau de 4 \u00e9l\u00e9ments vers la droite. On se dirait que le code suivant pourrait fonctionner\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u25027\u25028\u25029\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n ^*src ^*dst\n      \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n      \u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502\n      \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n       \u2193 \u2193 \u2193 \u2193 \u2193 \u2193 \u2193\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Na\u00efvement l'ex\u00e9cution suivante devrait fonctionner, mais les deux pointeurs source et destination s'enchev\u00eatrent et le r\u00e9sultat n'est pas celui escompt\u00e9.

    char array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\nchar *src = &array[0];\nchar *dst = &array[3];\n\nmemory_move(b, a, 7);\n
    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u25027\u25028\u25029\u2502 Tableau d'origine\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25020\u25021\u25022\u25020\u2502 Op\u00e9ration avec `memory_move`\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u25020\u25021\u25022\u25020\u25021\u25022\u25023\u25024\u25025\u25026\u2502 Op\u00e9ration avec `memmove` (fonction standard)\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n

    Notre simple fonction de d\u00e9placement m\u00e9moire ne fonctionne pas avec des r\u00e9gions m\u00e9moires qui s'enchev\u00eatrent. En revanche, la fonction standard memmove de <stdlib.h> fonctionne, car elle autorise, au d\u00e9triment d'une plus grande complexit\u00e9, de g\u00e9rer ce type de situation.

    Notons que sa fonction voisine memcpy ne dois jamais \u00eatre utilis\u00e9e en cas d'aliasing. Cette fonction se veut performante, c'est-\u00e0-dire qu'elle peut \u00eatre impl\u00e9ment\u00e9e en suivant le m\u00eame principe que notre exemple memory_move. Le standard C99 ne d\u00e9finit pas le comportement de memcpy pour des pointeurs qui se chevauchent.

    ", "tags": ["memcpy", "memory_move", "memmove"]}, {"location": "course-c/20-composite-types/pointers/#double-pointeurs", "title": "Double Pointeurs", "text": "

    Nous avons vu qu'un pointeur peut r\u00e9f\u00e9rencer un autre pointeur. Dans le langage C, il est courant de rencontrer des fonctions acceptant des double pointeurs, comme illustr\u00e9 ci-dessous\u2009:

    void function(int **ptr);\n

    Un double pointeur est, par d\u00e9finition, un pointeur qui pointe vers un autre pointeur. Ce m\u00e9canisme est particuli\u00e8rement utile lorsqu'il s'agit de modifier la valeur d'un pointeur \u00e0 l'int\u00e9rieur d'une fonction. En effet, si une fonction re\u00e7oit un pointeur simple (int *ptr) en argument, la valeur de ce pointeur est copi\u00e9e sur la pile, ce qui signifie que toute modification faite \u00e0 ptr \u00e0 l'int\u00e9rieur de la fonction n'affectera pas le pointeur d'origine. \u00c0 l'inverse, si la fonction re\u00e7oit un double pointeur (int **ptr), elle re\u00e7oit une copie de l'adresse du pointeur, ce qui permet de modifier directement la valeur du pointeur d'origine.

    Pour illustrer cette notion, imaginons une analogie simple. Supposons que vous \u00eates un peintre, et que votre patron vous donne un post-it avec l'adresse d'une maison \u00e0 peindre. En suivant cette adresse (en d\u00e9r\u00e9f\u00e9ren\u00e7ant le pointeur), vous pouvez vous rendre \u00e0 la maison et la peindre (modifier la valeur point\u00e9e). Cependant, si vous constatez que l'adresse est incorrecte, vous pouvez rectifier l'information sur votre post-it, mais vous ne pouvez pas informer directement votre patron de cette correction, car vous ne modifiez pas le post-it qu'il poss\u00e8de.

    En revanche, imaginez que votre patron vous donne un post-it o\u00f9 est inscrite l'adresse d'un autre post-it contenant l'adresse de la maison \u00e0 peindre. Par exemple, il vous remet un post-it sur lequel est \u00e9crit\u2009: \u00ab\u2009L'adresse se trouve sur le post-it rose coll\u00e9 sur la vitre de la vitrine de gauche dans mon bureau.\u2009\u00bb Si vous constatez une erreur, vous pouvez vous rendre dans le bureau de votre patron, corriger l'adresse sur le post-it rose, et ainsi, \u00e0 son retour, il aura acc\u00e8s \u00e0 la bonne information. Cette situation refl\u00e8te exactement l'utilit\u00e9 d'un double pointeur en C.

    Dans le code, cela pourrait se traduire ainsi\u2009:

    void painter(House **address) {\n    if (!is_correct(*address))\n        *address = get_new_address();  // Modification de l'adresse point\u00e9e\n    paint(*address);  // Peindre la maison \u00e0 l'adresse correcte\n}\n\nint main(void) {\n    House *address = get_address();  // Obtenir l'adresse initiale\n    painter(&address); // Le patron transmet l'adresse du post-it\n}\n
    ", "tags": ["ptr"]}, {"location": "course-c/20-composite-types/pointers/#cas-dutilisation", "title": "Cas d'utilisation", "text": "

    Les double pointeurs sont employ\u00e9s dans plusieurs sc\u00e9narios en programmation C\u2009:

    1. Allocation dynamique de m\u00e9moire pour des tableaux 2D : Un double pointeur est souvent utilis\u00e9 pour g\u00e9rer des tableaux dynamiques \u00e0 deux dimensions. Par exemple, int **matrix permet de cr\u00e9er une matrice dont les dimensions peuvent \u00eatre modifi\u00e9es \u00e0 l'ex\u00e9cution.

    2. Manipulation de cha\u00eenes de caract\u00e8res : Les double pointeurs sont utilis\u00e9s pour manipuler des tableaux de cha\u00eenes de caract\u00e8res (tableaux de pointeurs vers des caract\u00e8res), souvent employ\u00e9s pour traiter des arguments de programmes (char **argv).

    3. Passage par r\u00e9f\u00e9rence pour modifier un pointeur : Comme dans l'exemple pr\u00e9c\u00e9dent, lorsqu'une fonction doit modifier un pointeur pass\u00e9 en argument, on utilise un double pointeur pour que le changement soit effectif en dehors de la fonction.

    4. Listes cha\u00een\u00e9es ou autres structures dynamiques : Les double pointeurs sont utilis\u00e9s pour ins\u00e9rer ou supprimer des \u00e9l\u00e9ments dans des structures de donn\u00e9es dynamiques telles que des listes cha\u00een\u00e9es, o\u00f9 la t\u00eate de liste peut \u00eatre modifi\u00e9e.

    Nous verrons certains de ces cas d'utilisation dans des sections ult\u00e9rieures.

    "}, {"location": "course-c/20-composite-types/pointers/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Esperluettes cascad\u00e9es

    Quel est le type de\u2009:

    *&*&*&*&*&*&(int)x;\n

    Exercice 3\u2009: Passage par adresse

    Donnez les valeurs affich\u00e9es par ce programme pour les variables a \u00e0 e.

    #include <stdio.h>\n#include <stdlib.h>\n\nint test(int a, int * b, int * c, int * d) {\n    a = *b;\n    *b = *b + 5;\n    *c = a + 2;\n    d = c;\n    return *d;\n}\n\nint main(void) {\n    int a = 0, b = 100, c = 200, d = 300, e = 400;\n    e = test(a, &b, &c, &d);\n    printf(\"a:%d, b:%d, c:%d, d:%d, e:%d\\n\", a, b, c, d, e);\n}\n
    Solution
    a:0, b:105, c:102, d:300, e:102\n
    "}, {"location": "course-c/20-composite-types/strings/", "title": "Cha\u00eenes de caract\u00e8res", "text": "

    Une cha\u00eene de caract\u00e8res est repr\u00e9sent\u00e9e en m\u00e9moire comme une succession de bytes, chacun repr\u00e9sentant un caract\u00e8re ASCII sp\u00e9cifique. La cha\u00eene de caract\u00e8re hello contient donc 5 caract\u00e8res et sera stock\u00e9e en m\u00e9moire sur 5 bytes. Une cha\u00eene de caract\u00e8re est donc \u00e9quivalente \u00e0 un tableau de char.

    En C, un artifice est utilis\u00e9 pour faciliter les op\u00e9rations sur les cha\u00eenes de caract\u00e8res. Tous les caract\u00e8res de 1 \u00e0 255 sont utilisables sauf le 0 qui est utilis\u00e9 comme sentinelle. Lors de la d\u00e9claration d'une cha\u00eene comme ceci\u2009:

    char str[] = \"hello, world!\";\n

    Le compilateur ajoutera automatiquement un caract\u00e8re de terminaison '\\0' \u00e0 la fin de la cha\u00eene. Pour en comprendre l'utilit\u00e9, imaginons une fonction qui permet de compter la longueur de la cha\u00eene. Elle aurait comme prototype ceci\u2009:

    size_t strlen(const char str[]);\n

    On peut donc lui passer un tableau dont la taille n'est pas d\u00e9finie et par cons\u00e9quent, il n'est pas possible de conna\u00eetre la taille de la cha\u00eene pass\u00e9e sans le b\u00e9n\u00e9fice d'une sentinelle.

    size_t strlen(const char str[]) {\n    size_t len = 0,\n    while (str[len++] != '\\0') {}\n    return len;\n}\n

    Une cha\u00eene de caract\u00e8re est donc strictement identique \u00e0 un tableau de char.

    Ainsi une cha\u00eene de caract\u00e8re est initialis\u00e9e comme suit\u2009:

    char str[] = \"Pulp Fiction\";\n

    La taille de ce tableau sera donc de 12 caract\u00e8res plus une sentinelle '\\0' ins\u00e9r\u00e9e automatiquement. Cette \u00e9criture est donc identique \u00e0\u2009:

    char str[] = {'P', 'u', 'l', 'p', ' ', 'F', 'i', 'c', 't', 'i', 'o', 'n', '\\0'};\n
    ", "tags": ["char", "hello"]}, {"location": "course-c/20-composite-types/strings/#tableaux-de-chaines-de-caracteres", "title": "Tableaux de cha\u00eenes de caract\u00e8res", "text": "

    Un tableau de cha\u00eene de caract\u00e8res est identique \u00e0 un tableau multidimensionnel\u2009:

    char conjunctions[][10] = {\"mais\", \"ou\", \"est\", \"donc\", \"or\", \"ni\", \"car\"};\n

    Il est ici n\u00e9cessaire de d\u00e9finir la taille de la seconde dimension, comme pour les tableaux. C'est \u00e0 dire que la variable conjunctions aura une taille de 7x10 caract\u00e8res et le contenu m\u00e9moire de conjunctions[1] sera \u00e9quivalent \u00e0\u2009:

    {'o', 'u', 0, 0, 0, 0, 0, 0, 0, 0}\n

    D'ailleurs, ce tableau aurait pu \u00eatre initialis\u00e9 d'une tout autre fa\u00e7on\u2009:

    char conjunctions[][10] = {\n    'm', 'a', 'i', 's', 0, 0, 0, 0, 0, 0, 'o', 'u', 0, 0, 0,\n    0, 0, 0, 0, 0, 'e', 's', 't', 0, 0, 0, 0, 0, 0 , 0, 'd',\n    'o', 'n', 'c', 0, 0, 0, 0, 0 , 0, 'o', 'r', 0, 0, 0, 0,\n    0, 0, 0, 0, 'n', 'i', 0, 0, 0, 0, 0, 0, 0, 0, 'c', 'a',\n    'r', 0, 0, 0, 0, 0, 0, 0,\n};\n

    Notons que la valeur 0 est strictement identique au caract\u00e8re 0 de la table ASCII '\\0'. La cha\u00eene de caract\u00e8re \"mais\" aura une taille de 5 caract\u00e8res, ponctu\u00e9e de la sentinelle \\0.

    ", "tags": ["conjunctions"]}, {"location": "course-c/20-composite-types/strings/#wide-chars", "title": "Wide-chars", "text": "

    Les cha\u00eenes de caract\u00e8res larges sont des cha\u00eenes de caract\u00e8res qui utilisent plus d'un byte pour repr\u00e9senter un caract\u00e8re. En C, les cha\u00eenes de caract\u00e8res larges sont repr\u00e9sent\u00e9es par le type wchar_t. Pour d\u00e9clarer une cha\u00eene de caract\u00e8res larges, il est n\u00e9cessaire d'utiliser le pr\u00e9fixe L :

    wchar_t wstr[] = L\"Hello, \u4e16\u754c\";\n

    Ce type de cha\u00eene de caract\u00e8res est typiquement utilis\u00e9 pour repr\u00e9senter des caract\u00e8res Unicode. Comme nous l'avons vu au chapitre sur les types de donn\u00e9es, les caract\u00e8res qui ne sont pas dans la table ASCII sont repr\u00e9sent\u00e9s par plusieurs bytes. La fameuse \u00e9moji \ud83d\udc4b est repr\u00e9sent\u00e9e par 5 bytes 1F44B.

    L'en-t\u00eate wchar.h contient les fonctions pour manipuler les cha\u00eenes de caract\u00e8res larges. Par exemple, pour obtenir la longueur d'une cha\u00eene de caract\u00e8res larges, on utilise la fonction wcslen :

    #include <wchar.h>\n\nint main(void) {\n    wchar_t wstr[] = L\"Hello, \u4e16\u754c\";\n    size_t len = wcslen(wstr);\n    assert(len == 9);\n\n    char str[] = L\"Hello, \u4e16\u754c\";\n    size_t len = strlen(str);\n    assert(len == 13);\n}\n

    En pratique les cha\u00eenes de caract\u00e8res larges sont peu utilis\u00e9es en C, car elles sont moins efficaces que les cha\u00eenes de caract\u00e8res ASCII. En effet, les cha\u00eenes de caract\u00e8res larges n\u00e9cessitent plus de m\u00e9moire pour stocker les caract\u00e8res et les op\u00e9rations sur ces cha\u00eenes sont plus lentes.

    De plus, elles ne sont pas portables, car la taille d'un wchar_t d\u00e9pend de l'impl\u00e9mentation. Par exemple, sur Windows, un wchar_t est de 16 bits, alors que sur Linux, il est de 32 bits. Et comme nous l'avons les emojis peuvent \u00eatre cod\u00e9s sur plus de 16 bits.

    Pour ces raisons, on pr\u00e9f\u00e8re utiliser l'UTF-8 pour repr\u00e9senter les caract\u00e8res Unicode en C. L'UTF-8 est un encodage de caract\u00e8res Unicode qui utilise un \u00e0 quatre bytes pour repr\u00e9senter un caract\u00e8re. Il est compatible avec l'ASCII et est plus efficace que les cha\u00eenes de caract\u00e8res larges. Il fonctionne de la mani\u00e8re suivante\u2009:

    • Les caract\u00e8res 0000 \u00e0 007F sont cod\u00e9s sur un byte.
    • Les caract\u00e8res au del\u00e0 de 007F sont cod\u00e9s sur plusieurs bytes.
    0x00 - 0x7F         0b0xxxxxxx o\u00f9 x est le code ASCII sur 7-bits\n0x80 - 0x7FF        0b110xxxxx 0b10xxxxxx\n0x800 - 0xFFFF      0b1110xxxx 0b10xxxxxx 0b10xxxxxx\n0x10000 - 0x10FFFF  0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx\n

    Prenons l'exemple du caract\u00e8re \ud83d\udc4b. Sa repr\u00e9sentation binaire utilise 21 bits et n\u00e9cessite donc une s\u00e9quence de 4 octets.

    U+1F44B = 0b0001 1111 0100 0100 1011\n

    En UTF-8, il sera repr\u00e9sent\u00e9 par la s\u00e9quence de bytes suivante\u2009:

    0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx\n       000     011111     010001     001011\n\n0xF0       0x9F       0x91       0x8B\n

    Donc la longueur de la cha\u00eene de caract\u00e8re avec strlen sera de 4 et non de 1.

    char str[] = \"\ud83d\udc4b\";\nsize_t len = strlen(str);\nassert(len == 4);\n

    Pour conna\u00eetre la longueur d'une cha\u00eene de caract\u00e8res en UTF-8, il est n\u00e9cessaire de la faire \u00e0 la main\u2009:

    #include <stdio.h>\n\nsize_t utf8_strlen(const char *s) {\n    size_t length = 0;\n\n    while (*s) {\n        // Si on trouve 0b10xxxxxx, c'est un byte de continuation\n        // on l'ignore...\n        if ((*s & 0xC0) != 0x80) length++;\n        s++;\n    }\n\n    return length;\n}\n\nint main() {\n    const char *str = \"Hello, \ud83d\udc4b World!\";\n    size_t len = utf8_strlen(str);\n    printf(\"The string length is: %zu characters\\n\", len);\n}\n
    ", "tags": ["strlen", "wchar_t", "wchar.h", "wcslen"]}, {"location": "course-c/20-composite-types/strings/#buffer-tampon", "title": "Buffer (tampon)", "text": "

    Bien souvent, les cha\u00eenes de caract\u00e8res sont manipul\u00e9es dans des buffers. Un buffer est un tableau de caract\u00e8res d'une taille fixe utilis\u00e9 pour stocker des donn\u00e9es interm\u00e9diaires. Un cas typique est la lecture depuis l'entr\u00e9e standard.

    Admettons que l'on souhaite lire le nom d'un utilisateur depuis l'entr\u00e9e standard. On peut d\u00e9finir un buffer de 256 caract\u00e8res pour stocker le nom de l'utilisateur\u2009:

    #include <stdio.h>\n\nint main(void) {\n    char name[32];\n    printf(\"Enter your name: \");\n    scanf(\"%31s\", name);\n}\n

    Dans cet exemple, name est un buffer de 32 caract\u00e8res. La fonction scanf lit au maximum 31 caract\u00e8res depuis l'entr\u00e9e standard et les stocke dans name. La taille du buffer est de 32 caract\u00e8res pour laisser de la place pour la sentinelle \\0. Sans cette s\u00e9curit\u00e9, il serait possible de d\u00e9border le buffer et d'\u00e9crire dans des zones m\u00e9moires qui ne nous appartiennent pas.

    ", "tags": ["scanf", "name"]}, {"location": "course-c/20-composite-types/strings/#chaine-de-caracteres-multi-lignes", "title": "Cha\u00eene de caract\u00e8res multi-lignes", "text": "

    En C, il n'est pas possible de d\u00e9clarer une cha\u00eene de caract\u00e8res sur plusieurs lignes. Pour ce faire, il est n\u00e9cessaire de concat\u00e9ner plusieurs cha\u00eenes de caract\u00e8res\u2009:

    char *str = \"Hello, \"\n            \"World!\";\n

    Dans cet exemple, str contiendra la cha\u00eene de caract\u00e8res Hello, World!.

    ", "tags": ["str"]}, {"location": "course-c/20-composite-types/strings/#chaine-de-caracteres-constantes", "title": "Cha\u00eene de caract\u00e8res constantes", "text": "

    Les cha\u00eenes de caract\u00e8res constantes sont des cha\u00eenes de caract\u00e8res qui ne peuvent pas \u00eatre modifi\u00e9es. Elles sont stock\u00e9es dans la section .rodata de la m\u00e9moire. Pour d\u00e9clarer une cha\u00eene de caract\u00e8res constante, il est n\u00e9cessaire d'utiliser le mot-cl\u00e9 const :

    const char *str = \"Hello, World!\";\n

    Notez que la d\u00e9claration est un peu diff\u00e9rente de la d\u00e9claration d'une cha\u00eene de caract\u00e8res classique. Ici nous n'utilisons plus la notation [] mais un pointeur *.

    ", "tags": ["const"]}, {"location": "course-c/20-composite-types/structures/", "title": "Structures", "text": "

    Les structures sont des d\u00e9clarations sp\u00e9cifiques permettant de regrouper une liste de variables dans un m\u00eame bloc m\u00e9moire et permettant de s'y r\u00e9f\u00e9rer \u00e0 partir d'une r\u00e9f\u00e9rence commune. Historiquement le type struct a \u00e9t\u00e9 d\u00e9riv\u00e9 de ALGOL 68. Il est \u00e9galement utilis\u00e9 en C++ et est similaire \u00e0 une classe.

    Il faut voir une structure comme un conteneur \u00e0 variables qu'il est possible de v\u00e9hiculer comme un tout.

    La structure suivante d\u00e9crit un agr\u00e9gat de trois grandeurs scalaires formant un point tridimensionnel\u2009:

    struct {\n    double x;\n    double y;\n    double z;\n};\n

    Il ne faut pas confondre l'\u00e9criture ci-dessus avec ceci, dans lequel il y a un bloc de code avec trois variables locales d\u00e9clar\u00e9es\u2009:

    {\n    double x;\n    double y;\n    double z;\n};\n

    En utilisant le mot-cl\u00e9 struct devant un bloc, les variables d\u00e9clar\u00e9es au sein de ce bloc ne seront pas r\u00e9serv\u00e9es en m\u00e9moire. Autrement dit, il ne sera pas possible d'acc\u00e9der \u00e0 x puisqu'il n'existe pas de variable x. En revanche, un nouveau conteneur contenant trois variables est d\u00e9fini, mais pas encore d\u00e9clar\u00e9.

    La structure ainsi d\u00e9clar\u00e9e n'est pas tr\u00e8s utile telle quelle, en revanche elle peut-\u00eatre utilis\u00e9e pour d\u00e9clarer une variable de type struct :

    struct {\n    double x;\n    double y;\n    double z;\n} point;\n

    \u00c0 pr\u00e9sent on a d\u00e9clar\u00e9 une variable point de type struct contenant trois \u00e9l\u00e9ments de type double. L'affectation d'une valeur \u00e0 cette variable utilise l'op\u00e9rateur . :

    point.x = 3060426.957;\npoint.y = 3192003.220;\npoint.z = 4581359.381;\n

    Comme point n'est pas une primitive standard, mais un conteneur \u00e0 primitive, il n'est pas correct d'\u00e9crire point = 12. Il est essentiel d'indiquer quel \u00e9l\u00e9ment de ce conteneur on souhaite acc\u00e9der.

    Ces coordonn\u00e9es sont un clin d'\u0153il aux Pierres du Niton qui sont deux blocs de roche erratiques d\u00e9pos\u00e9s par le glacier du Rh\u00f4ne lors de son retrait apr\u00e8s la derni\u00e8re glaciation. Les coordonn\u00e9es sont exprim\u00e9es selon un rep\u00e8re g\u00e9ocentr\u00e9\u2009; l'origine \u00e9tant le centre de la Terre. Ces pierres sont donc situ\u00e9es \u00e0 4.5 km du centre de la Terre, une valeur qui aurait repr\u00e9sent\u00e9, pour \u00eatre convenablement d\u00e9termin\u00e9e, un sacr\u00e9 d\u00e9fi pour Axel Lidenbrock et son fulmicoton.

    G\u00e9n\u00e9ralement les structures sont utilis\u00e9es pour communiquer des donn\u00e9es complexes entre diff\u00e9rentes fonctions d'un m\u00eame fichier ou entre diff\u00e9rents fichiers. C'est pour cette raison que l'on retrouve g\u00e9n\u00e9ralement ces d\u00e9finitions en dehors de toute fonction\u2009:

    struct Point {\n    double x;\n    double y;\n};\n\nstruct Point point_add(struct Point a, struct Point b) {\n    return (struct Point){\n        .x = a.x + b.x,\n        .y = a.y + b.y\n    };\n}\n\nvoid point_print(struct Point p) {\n    printf(\"(%g,%g)\", p.x, p.y);\n}\n\nint main(void) {\n    struct Point p = {1., 2.};\n    struct Point q = {3., 4.};\n    struct Point r = point_add(p, q);\n    point_print(r);\n}\n
    ", "tags": ["point", "struct", "double"]}, {"location": "course-c/20-composite-types/structures/#structures-nommees", "title": "Structures nomm\u00e9es", "text": "

    L'\u00e9criture que l'on a vue initialement struct { ... }; est appel\u00e9e structure anonyme, c'est-\u00e0-dire qu'elle n'a pas de nom. Telle quelle elle ne peut pas \u00eatre utilis\u00e9e et elle ne sert donc pas \u00e0 grand chose. En revanche, il est possible de d\u00e9clarer une variable de ce type en ajoutant un identificateur \u00e0 la fin de la d\u00e9claration struct { ... } nom;. N\u00e9anmoins la structure est toujours anonyme.

    Le langage C pr\u00e9voit la possibilit\u00e9 de nommer une structure pour une utilisation ult\u00e9rieure en rajoutant un nom apr\u00e8s le mot cl\u00e9 struct :

    struct Point {\n    double x;\n    double y;\n    double z;\n};\n

    Pour ne pas confondre un nom de structure avec un nom de variable, on pr\u00e9f\u00e9rera un identificateur en capitales ou en \u00e9criture camel-case. Maintenant qu'elle est nomm\u00e9e, il est possible de d\u00e9clarer plusieurs variables de ce type ailleurs dans le code\u2009:

    struct Point foo;\nstruct Point bar;\n

    Dans cet exemple, on d\u00e9clare deux variables foo et bar de type struct Point. Il est donc possible d'acc\u00e9der \u00e0 foo.x ou bar.z.

    Rien n'emp\u00eache de d\u00e9clarer une structure nomm\u00e9e et d'\u00e9galement d\u00e9clarer une variable par la m\u00eame occasion\u2009:

    struct Point {\n    double x;\n    double y;\n    double z;\n} foo;\nstruct Point bar;\n

    Notons que les noms de structures sont stock\u00e9s dans un espace de noms diff\u00e9rent de celui des variables. C'est-\u00e0-dire qu'il n'y a pas de collision possible et qu'un identifiant de fonction ou de variable ne pourra jamais \u00eatre compar\u00e9 \u00e0 un identifiant de structure. Aussi, l'\u00e9criture suivante, bien que perturbante, est tout \u00e0 fait possible\u2009:

    struct point { double x; double y; double z; };\nstruct point point;\npoint.x = 42;\n
    ", "tags": ["struct", "bar", "foo.x", "bar.z", "foo"]}, {"location": "course-c/20-composite-types/structures/#initialisation", "title": "Initialisation", "text": "

    Une structure se comporte \u00e0 peu de chose pr\u00e8s comme un tableau sauf que les \u00e9l\u00e9ments de la structure ne s'acc\u00e8dent pas avec l'op\u00e9rateur crochet, [] mais avec l'op\u00e9rateur .. N\u00e9anmoins une structure est repr\u00e9sent\u00e9e en m\u00e9moire comme un contenu lin\u00e9aire. Notre structure struct Point serait identique \u00e0 un tableau de trois double et par cons\u00e9quent l'initialisation suivante est possible\u2009:

    struct Point point = { 3060426.957, 3192003.220, 4581359.381 };\n

    N\u00e9anmoins on pr\u00e9f\u00e8rera la notation suivante, \u00e9quivalente\u2009:

    struct Point point = { .x=3060426.957, .y=3192003.220, .z=4581359.381 };\n

    Comme pour un tableau, les valeurs omises sont initialis\u00e9es \u00e0 z\u00e9ro. Et de la m\u00eame mani\u00e8re qu'un tableau, il est possible d'initialiser une structure \u00e0 z\u00e9ro avec = {0};.

    Il faut savoir que C99 restreint l'ordre dans lequel les \u00e9l\u00e9ments peuvent \u00eatre initialis\u00e9s. Ce dernier doit \u00eatre l'ordre dans lequel les variables sont d\u00e9clar\u00e9es dans la structure.

    Notons que des structures comportant des types diff\u00e9rents peuvent aussi \u00eatre initialis\u00e9es de la m\u00eame mani\u00e8re\u2009:

    struct Product {\n    int weight; // Grams\n    double price; // Swiss francs\n    int category;\n    char name[64];\n}\n\nstruct Product apple = {321, 0.75, 24, \"Pomme Golden\"};\n
    ", "tags": ["double"]}, {"location": "course-c/20-composite-types/structures/#tableaux-de-structures", "title": "Tableaux de structures", "text": "

    Une structure est un type comme un autre. Tout ce qui peut \u00eatre fait avec char ou double peut donc \u00eatre fait avec struct. Et donc, il est aussi possible de d\u00e9clarer un tableau de structures. Ici, donnons l'exemple d'un tableau de points initialis\u00e9s\u2009:

    struct Point points[3] = {\n    {.x=1, .y=2, .z=3},\n    {.z=1, .x=2, .y=3},\n    {.y=1}\n};\n

    Assigner une nouvelle valeur \u00e0 un point est facile\u2009:

    point[2].x = 12;\n
    ", "tags": ["char", "struct", "double"]}, {"location": "course-c/20-composite-types/structures/#structures-en-parametres", "title": "Structures en param\u00e8tres", "text": "

    L'int\u00e9r\u00eat d'une structure est de pouvoir passer ou retourner un ensemble de donn\u00e9es \u00e0 une fonction. On a vu qu'une fonction ne permet de retourner qu'une seule primitive. Une structure est ici consid\u00e9r\u00e9e comme un seul conteneur et l'\u00e9criture suivante est possible\u2009:

    struct Point generate_point(void) {\n    struct Point p = {\n        .x = rand(),\n        .y = rand(),\n        .z = rand()\n    };\n\n    return p;\n}\n

    Il est \u00e9galement possible de passer une structure en param\u00e8tre d'une fonction\u2009:

    double norm(struct point p) {\n    return sqrt(p.x * p.x + p.y * p.y + p.z * p.z);\n}\n\nint main(void) {\n    struct Point p = { .x = 12.54, .y = -8.12, .z = 0.68 };\n\n    double n = norm(p);\n}\n

    Contrairement aux tableaux, les structures sont toujours pass\u00e9es par valeur, c'est-\u00e0-dire que l'entier du contenu de la structure sera copi\u00e9 sur la pile (stack) en cas d'appel \u00e0 une fonction. En revanche, en cas de passage par pointeur, seule l'adresse de la structure est pass\u00e9e \u00e0 la fonction appel\u00e9e qui peut d\u00e8s lors modifier le contenu\u2009:

    struct Point {\n    double x;\n    double y;\n};\n\nvoid foo(struct Point m, struct Point *n) {\n    m.x++;\n    n->x++;\n}\n\nint main(void) {\n    struct Point p = {0}, q = {0};\n    foo(p, &q);\n    printf(\"%g, %g\\n\", p.x, q.x);\n}\n

    Le r\u00e9sultat affich\u00e9 sera 0.0, 1.0. Seule la seconde valeur est modifi\u00e9e.

    Hint

    Lorsqu'un membre d'une structure est acc\u00e9d\u00e9, via son pointeur, on utilise la notation -> au lieu de . car il est n\u00e9cessaire de d\u00e9r\u00e9f\u00e9rencer le pointeur. Il s'agit d'un sucre syntaxique permettant d'\u00e9crire p->x au lieu de (*p).x

    "}, {"location": "course-c/20-composite-types/structures/#structures-flexibles", "title": "Structures flexibles", "text": "

    Introduits avec C99, les membres de structures flexibles ou flexible array members (\u00a76.7.2.1) est un membre de type tableau d'une structure d\u00e9fini sans dimension. Ces membres ne peuvent appara\u00eetre qu'\u00e0 la fin d'une structure.

    struct Vector {\n    char name[16]; // name of the vector\n    size_t len; // length of the vector\n    double array[]; // flexible array member\n};\n

    Cette \u00e9criture permet par exemple de r\u00e9server un espace m\u00e9moire plus grand que la structure de base, et d'utiliser le reste de l'espace comme tableau flexible.

    struct Vector *vector = malloc(1024);\nstrcpy(vector->name, \"Mon vecteur\");\nvector->len = 1024 - 16 - 4;\nfor (int i = 0; i < vector->len; i++)\n    vector->array[i] = ...\n

    Ce type d'\u00e9criture est souvent utilis\u00e9 pour des contenus ayant un en-t\u00eate fixe comme des images BMP ou des fichiers sons WAVE.

    "}, {"location": "course-c/20-composite-types/structures/#structure-de-structures", "title": "Structure de structures", "text": "

    On comprend ais\u00e9ment que l'avantage des structures et le regroupement de variables. Une structure peut \u00eatre la composition d'autres types composites.

    Nous d\u00e9clarons ici une structure struct Line compos\u00e9e de struct Point :

    struct Line {\n    struct Point a;\n    struct Point b;\n};\n

    L'acc\u00e8s \u00e0 ces diff\u00e9rentes valeurs s'effectue de la fa\u00e7on suivante\u2009:

    struct Line line = {.a.x = 23, .a.y = 12, .b.z = 33};\nprintf(\"%g, %g\", line.a.x, line.b.x);\n
    "}, {"location": "course-c/20-composite-types/structures/#alignement-memoire", "title": "Alignement m\u00e9moire", "text": "

    Une structure est agenc\u00e9e en m\u00e9moire dans l'ordre de sa d\u00e9claration. C'est donc un agencement lin\u00e9aire en m\u00e9moire\u2009:

    struct Line lines[2]; // Chaque point est un double, cod\u00e9 sur 8 bytes.\n

    Ci-dessous est repr\u00e9sent\u00e9 l'offset m\u00e9moire (en bytes) \u00e0 laquelle est stock\u00e9 chaque membre de la structure, ainsi que l'\u00e9l\u00e9ment correspondant.

    0x0000 line[0].a.x\n0x0008 line[0].a.y\n0x0010 line[0].a.z\n0x0018 line[0].b.x\n0x0020 line[0].b.y\n0x0028 line[0].b.z\n0x0030 line[1].a.x\n0x0038 line[1].a.y\n0x0040 line[1].a.z\n0x0048 line[1].b.x\n0x0050 line[1].b.y\n0x0048 line[1].b.z\n

    N\u00e9anmoins dans certains cas, le compilateur se r\u00e9serve le droit d'optimiser l' alignement m\u00e9moire. Une architecture 32-bits aura plus de facilit\u00e9 \u00e0 acc\u00e9der \u00e0 des grandeurs de 32 bits or, une structure compos\u00e9e de plusieurs entiers 8-bits demanderait au processeur un co\u00fbt additionnel pour optimiser le stockage d'information. Consid\u00e9rons par exemple la structure suivante\u2009:

    struct NoAlign\n{\n    int8_t c;\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n};\n

    Imaginons pour comprendre qu'un casier m\u00e9moire sur une architecture 32-bits est assez grand pour y stocker 4 bytes. Tentons de repr\u00e9senter en m\u00e9moire cette structure en little-endian, en consid\u00e9rant des casiers de 32-bits\u2009:

     c    d             i              a\n\u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502c\u2502d\u2502d\u2502d\u2502 \u2502d\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502a\u2502a\u2502a\u2502\n\u25020\u25020\u25021\u25022\u2502 \u25023\u25020\u25021\u25022\u2502 \u25023\u25024\u25025\u25026\u2502 \u25027\u25020\u25021\u25022\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D\n

    On constate que la valeur d est \u00e0 cheval entre deux casiers. De m\u00eame que la valeur i est r\u00e9partie sur trois casiers au lieu de deux. Le processeur communique avec la m\u00e9moire en utilisant des bus m\u00e9moire, ils sont l'analogie d'une autoroute qui ne peut accueillir que des voitures, chacune ne pouvant transporter que 4 passagers. Un passager ne peut pas arpenter l'autoroute sans voiture. Le processeur est la gare de triage et s'occupe de r\u00e9assembler les passagers, et l'op\u00e9ration consistant \u00e0 demander \u00e0 un passager de sortir de la voiture B pour s'installer dans une autre, ou m\u00eame se d\u00e9placer de la place du conducteur \u00e0 la place du passager arri\u00e8re prend du temps.

    Le compilateur sera donc oblig\u00e9 de faire du z\u00e8le pour acc\u00e9der \u00e0 d. formellement l'acc\u00e8s \u00e0 d pourrait s'\u00e9crire ainsi\u2009:

    int32_t d = (data[0] << 8) | (data[1] & 0x0F);\n

    Pour \u00e9viter ces man\u0153uvres, le compilateur, selon l'architecture donn\u00e9e, va ins\u00e9rer des \u00e9l\u00e9ments de rembourrage (padding) pour forcer l'alignement m\u00e9moire et ainsi optimiser les lectures. Ce sont des \u00e9l\u00e9ments vides qui ne sont pas accessibles par l'utilisateur final. Dit autrement, c'est comme choisir de ne mettre qu'un seul passager dans la voiture. Ce n'est pas optimal sur le plan du bilan carbone, mais c'est plus rapide pour le processeur.

    La m\u00eame structure que ci-dessus sera fort probablement impl\u00e9ment\u00e9e de la fa\u00e7on suivante\u2009:

    struct Align\n{\n    int8_t c;\n    int8_t __pad1[3]; // Ins\u00e9r\u00e9 par le compilateur\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n    int8_t __pad2; // Ins\u00e9r\u00e9 par le compilateur\n};\n

    En reprenant notre analogie de voitures, le stockage est maintenant fait comme ceci\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502c\u2502 \u2502 \u2502 \u2502 \u2502d\u2502d\u2502d\u2502d\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502a\u2502a\u2502a\u2502 \u2502\n\u25020\u2502 \u2502 \u2502 \u2502 \u25020\u25021\u25022\u25023\u2502 \u25020\u25021\u25022\u25023\u2502 \u25024\u25025\u25026\u25027\u2502 \u25020\u25021\u25022\u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D         E\n

    Le compromis est qu'une voiture suppl\u00e9mentaire est n\u00e9cessaire, mais le processeur n'a plus besoin de r\u00e9agencer les passagers. L'acc\u00e8s \u00e0 d est ainsi facilit\u00e9 au d\u00e9triment d'une perte substantielle de l'espace de stockage.

    Ceci \u00e9tant, en changeant l'ordre des \u00e9l\u00e9ments dans la structure pour que chaque membre soit align\u00e9 sur 32-bits, il est possible d'obtenir un meilleur compromis\u2009:

    struct Align\n{\n    int32_t d;\n    int64_t i;\n    int8_t a[3];\n    int8_t c;\n};\n
    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502d\u2502d\u2502d\u2502d\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502i\u2502i\u2502i\u2502i\u2502 \u2502a\u2502a\u2502a\u2502c\u2502\n\u25020\u25021\u25022\u25023\u2502 \u25020\u25021\u25022\u25023\u2502 \u25024\u25025\u25026\u25027\u2502 \u25020\u25021\u25022\u25023\u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C         D\n

    L'option -Wpadded de GCC permet de lever une alerte lorsqu'une structure est align\u00e9e par le compilateur. Si l'on utilise par exemple une structure pour \u00e9crire un fichier binaire respectant un format pr\u00e9cis par exemple l'en-t\u00eate d'un fichier BMP. Et que cette structure BitmapFileHeader est enregistr\u00e9e avec fwrite(header, sizeof(BitmapFileHeader), ...). Si le compilateur rajoute des \u00e9l\u00e9ments de rembourrage, le fichier BMP serait alors compromis. Il faudrait donc consid\u00e9rer l'alerte Wpadded comme une erreur critique.

    Pour pallier \u00e0 ce probl\u00e8me, lorsqu'une structure m\u00e9moire doit \u00eatre respect\u00e9e dans un ordre pr\u00e9cis. Une option de compilation non standard existe. La directive #pragma pack permet de forcer un type d'alignement pour une certaine structure. Consid\u00e9rons par exemple la structure suivante\u2009:

    struct Test\n{\n    char a;\n    int b;\n    char c;\n};\n

    Elle serait tr\u00e8s probablement repr\u00e9sent\u00e9e en m\u00e9moire de la fa\u00e7on suivante\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502a\u2502 \u2502 \u2502 \u2502 \u2502b\u2502b\u2502b\u2502b\u2502 \u2502c\u2502 \u2502 \u2502 \u2502\n\u25020\u2502 \u2502 \u2502 \u2502 \u25020\u25021\u25022\u25023\u2502 \u25020\u2502 \u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B         C\n

    En revanche si elle est d\u00e9crite en utilisant un packing sur 8-bits, avec #pragma pack(1) on aura l'alignement m\u00e9moire suivant\u2009:

    \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\n\u2502a\u2502b\u2502b\u2502b\u2502 \u2502b\u2502c\u2502 \u2502 \u2502\n\u25020\u25020\u25021\u25022\u2502 \u25023\u25021\u2502 \u2502 \u2502\n\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\n    A         B\n
    ", "tags": ["Wpadded"]}, {"location": "course-c/20-composite-types/structures/#fichier-wav", "title": "Fichier WAV", "text": "

    Le format de fichier WAV est un format de fichier standard pour les fichiers audio num\u00e9riques. Il est bas\u00e9 sur le format RIFF (Resource Interchange File Format) qui est un format de fichier g\u00e9n\u00e9rique pour l'\u00e9change de donn\u00e9es. Ce format est tr\u00e8s ancien et a \u00e9t\u00e9 introduit par Microsoft en 1991. Il est n\u00e9anmoins encore utilis\u00e9 aujourd'hui pour stocker des fichiers audio non compress\u00e9s car il est simple et ne n\u00e9cessite pas de licence.

    Prenons l'exemple concr\u00eat de la structure d'un fichier audio WAV\u2009:

    Fichier WAV

    Cette structure peut \u00eatre d\u00e9finie de la fa\u00e7on suivante\u2009:

    #pragma pack(1)\nstruct WavHeader\n{\n    char riff_tag[4];\n    uint32_t file_size;\n    char wave_tag[4];\n    char fmt_tag[4];\n    uint32_t fmt_length;\n    uint16_t audio_format;\n    uint16_t num_channels;\n    uint32_t sample_rate;\n    uint32_t byte_rate;\n    uint16_t block_align;\n    uint16_t bits_per_sample;\n    char data_tag[4];\n    uint32_t data_size;\n};\n

    Conserver le bon alignement m\u00e9moire est ici crutial car l'objectif est d'\u00e9crire les donn\u00e9es dans un fichier audio. Voici l'exemple d'un programme qui g\u00e9n\u00e8re un fichier audio\u2009:

    #include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <math.h>\n\n#define SAMPLE_RATE 44100 // Fr\u00e9quence d'\u00e9chantillonnage [Hz]\n#define AMPLITUDE 32760   // Amplitude du signal (valeur maximale pour 16 bits)\n\n#pragma pack(1)\nstruct WavHeader\n{\n    char riff_tag[4];\n    uint32_t file_size;\n    char wave_tag[4];\n    char fmt_tag[4];\n    uint32_t fmt_length;\n    uint16_t audio_format;\n    uint16_t num_channels;\n    uint32_t sample_rate;\n    uint32_t byte_rate;\n    uint16_t block_align;\n    uint16_t bits_per_sample;\n    char data_tag[4];\n    uint32_t data_size;\n};\n\nint generate_sine_wave(const char *filename, double frequency, double duration)\n{\n    const int num_samples = (int)(SAMPLE_RATE * duration);\n    int16_t *buffer = (int16_t *)malloc(num_samples * sizeof(int16_t));\n\n    // Remplissage du buffer avec le signal sinusoidal\n    for (int i = 0; i < num_samples; ++i) {\n        buffer[i] = (int16_t)(AMPLITUDE * sin(2.0 * M_PI *\n                              frequency * i / SAMPLE_RATE));\n    }\n\n    // Initialisation de l'en-t\u00eate WAV avec une liste d'initialisation\n    struct WavHeader header = {\n        .riff_tag = {'R', 'I', 'F', 'F'},\n        .file_size = 36 + num_samples * sizeof(int16_t),\n        .wave_tag = {'W', 'A', 'V', 'E'},\n        .fmt_tag = {'f', 'm', 't', ' '},\n        .fmt_length = 16,\n        .audio_format = 1,\n        .num_channels = 1,\n        .sample_rate = SAMPLE_RATE,\n        .byte_rate = SAMPLE_RATE * sizeof(int16_t),\n        .block_align = sizeof(int16_t),\n        .bits_per_sample = 16,\n        .data_tag = {'d', 'a', 't', 'a'},\n        .data_size = num_samples * sizeof(int16_t)};\n\n    // \u00c9criture dans le fichier\n    FILE *file = fopen(filename, \"wb\");\n    if (!file)\n    {\n        printf(\"Erreur lors de l'ouverture du fichier %s\\n\", filename);\n        free(buffer);\n        return -1;\n    }\n\n    fwrite(&header, sizeof(struct WavHeader), 1, file);\n    fwrite(buffer, sizeof(int16_t), num_samples, file);\n    fclose(file);\n    free(buffer);\n    return 0;\n}\n\nint main() {\n    generate_sine_wave(\"sine.wav\", 440.0, 5.0);\n}\n
    "}, {"location": "course-c/20-composite-types/structures/#champs-de-bits", "title": "Champs de bits", "text": "

    Les champs de bits sont des structures dont une information suppl\u00e9mentaire est ajout\u00e9e\u2009: le nombre de bits utilis\u00e9s.

    Prenons l'exemple du module I2C du microcontr\u00f4leur TMS320F28335. Le registre I2CMDR d\u00e9crit \u00e0 la page 23 est un registre 16-bits qu'il conviendrait de d\u00e9crire avec un champ de bits\u2009:

    struct I2CMDR {\n    int  bc  :3;\n    bool fdf :1;\n    bool stb :1;\n    bool irs :1;\n    bool dlb :1;\n    bool rm  :1;\n    bool xa  :1;\n    bool trx :1;\n    bool mst :1;\n    bool stp :1;\n    bool _reserved :1;\n    bool stt  :1;\n    bool free :1;\n    bool nackmod :1;\n};\n

    Activer le bit stp (bit num\u00e9ro 12) devient une op\u00e9ration triviale\u2009:

    struct I2CMDR i2cmdr;\n\ni2cmdr.stp = true;\n

    Alors qu'elle demanderait une manipulation de bit sinon\u2009:

    int32_t i2cmdr;\n\ni2cmdr |= 1 << 12;\n

    Notons que les champs de bits, ainsi que les structures seront d\u00e9clar\u00e9s diff\u00e9remment selon que l'architecture cible est little-endian ou big-endian.

    Cette technique est particuli\u00e8rement utile pour manipuler des registres de microcontr\u00f4leurs ou des fichiers binaires, elle est souvent coupl\u00e9e \u00e0 des unions pour permettre un acc\u00e8s soit par champ de bits, soit par entier.

    ", "tags": ["I2CMDR", "stp"]}, {"location": "course-c/20-composite-types/structures/#compound-literals", "title": "Compound Literals", "text": "

    Na\u00efvement traduit en litt\u00e9raux compos\u00e9s, un compound literal est une m\u00e9thode de cr\u00e9ation d'un type compos\u00e9 \u00ab\u2009\u00e0 la vol\u00e9e\u2009\u00bb utilis\u00e9 de la m\u00eame fa\u00e7on que les transtypages.

    Reprenons notre structure Point struct Point vue plus haut. Si l'on souhaite changer la valeur du point p il faudrait on pourrait \u00e9crire ceci\u2009:

    struct Point p; // D\u00e9clar\u00e9 plus haut\n\n// ...\n\n{\n    struct Point q = {.x=1, .y=2, .z=3};\n    p = q;\n}\n

    Notons que passer par une variable interm\u00e9diaire q n'est pas tr\u00e8s utile. Il serait pr\u00e9f\u00e9rable d'\u00e9crire ceci\u2009:

    p = {.x=1, .y=2, .z=3};\n

    N\u00e9anmoins cette \u00e9criture m\u00e8nera \u00e0 une erreur de compilation, car le compilateur cherchera \u00e0 d\u00e9terminer le type de l'expression {.x=1, .y=2, .z=3}. Il est alors essentiel d'utiliser la notation suivante\u2009:

    p = (struct Point){.x=1, .y=2, .z=3};\n

    Cette notation de litt\u00e9raux compos\u00e9s peut \u00e9galement s'appliquer aux tableaux. L'exemple suivant montre l'initialisation d'un tableau \u00e0 la vol\u00e9e pass\u00e9 \u00e0 la fonction foo :

    void foo(int array[3]) {\n    for (int i = 0; i < 3; i++) printf(\"%d \", array[i]);\n}\n\nvoid main() {\n    foo((int []){1,2,3});\n}\n

    Exercice 1\u2009: Mendele\u00efev

    Chaque \u00e9l\u00e9ment du tableau p\u00e9riodique des \u00e9l\u00e9ments comporte les propri\u00e9t\u00e9s suivantes\u2009:

    • Un nom jusqu'\u00e0 20 lettres
    • Un symbole jusqu'\u00e0 2 lettres
    • Un num\u00e9ro atomique de 1 \u00e0 118 (2019)
    • Le type de l'\u00e9l\u00e9ment

      • M\u00e9taux (Alcalin, Alcalino-terreux, Lanthanides, Actinides, M\u00e9taux de transition, M\u00e9taux pauvres)
      • M\u00e9tallo\u00efdes
      • Non-m\u00e9taux (Autres, Halog\u00e8ne, Gaz noble)
    • La p\u00e9riode\u2009: un entier de 1 \u00e0 7

    • Le groupe\u2009: un entier de 1 \u00e0 18

    D\u00e9clarer une structure de donn\u00e9es permettant de stocker tous les \u00e9l\u00e9ments chimiques de telle fa\u00e7on qu'ils puissent \u00eatre acc\u00e9d\u00e9s comme\u2009:

    assert(strcmp(table.element[6].name, \"Helium\") == 0);\nassert(strcmp(table.element[54].type, \"Gaz noble\") == 0);\nassert(table.element[11].period == 3);\n\nElement *el = table.element[92];\nassert(el->atomic_weight == 92);\n
    ", "tags": ["foo"]}, {"location": "course-c/20-composite-types/structures/#creation-de-types", "title": "Cr\u00e9ation de types", "text": "

    Le mot cl\u00e9 typedef permet de d\u00e9clarer un nouveau type. Il est particuli\u00e8rement utilis\u00e9 conjointement avec les structures et les unions afin de s'affranchir de la lourdeur d'\u00e9criture (pr\u00e9fixe struct), et dans le but de cacher la complexit\u00e9 d'un type \u00e0 l'utilisateur qui le manipule.

    L'exemple suivant d\u00e9clare un type Point et un prototype de fonction permettant l'addition de deux points.

    typedef struct {\n    double x;\n    double y;\n} Point;\n\nPoint add(Point a, Point b);\n
    ", "tags": ["struct", "typedef", "Point"]}, {"location": "course-c/20-composite-types/unions/", "title": "Unions", "text": "

    Une union est une variable qui peut avoir plusieurs repr\u00e9sentations d'un m\u00eame contenu m\u00e9moire. Rappelez-vous, nous nous demandions quelle \u00e9tait l'interpr\u00e9tation d'un contenu m\u00e9moire donn\u00e9. Il est possible en C d'avoir toutes les interpr\u00e9tations \u00e0 la fois\u2009:

    #include <stdint.h>\n#include <stdio.h>\n\nunion Mixed\n{\n    int32_t signed32;\n    uint32_t unsigned32;\n    int8_t signed8[4];\n    int16_t signed16[2];\n    float float32;\n};\n\nint main(void) {\n    union Mixed m = {\n        .signed8 = {0b11011011, 0b0100100, 0b01001001, 0b01000000}\n    };\n\n    printf(\n        \"int32_t\\t%d\\n\"\n        \"uint32_t\\t%u\\n\"\n        \"char\\t%c, %c, %c, %c\\n\"\n        \"short\\t%hu, %hu\\n\"\n        \"float\\t%f\\n\",\n        m.signed32,\n        m.unsigned32,\n        m.signed8[0], m.signed8[1], m.signed8[2], m.signed8[3],\n        m.signed16[0], m.signed16[1],\n        m.float32\n    );\n}\n

    Les unions sont tr\u00e8s utilis\u00e9es en combinaison avec des champs de bits. Pour reprendre l'exemple du champ de bit \u00e9voqu\u00e9 plus haut, on peut souhaiter acc\u00e9der au registre soit sous la forme d'un entier 16-bits soit via chacun de ses bits ind\u00e9pendamment.

    union i2cmdr {\n    struct {\n        int  bc  :3;\n        bool fdf :1;\n        bool stb :1;\n        bool irs :1;\n        bool dlb :1;\n        bool rm  :1;\n        bool xa  :1;\n        bool trx :1;\n        bool mst :1;\n        bool stp :1;\n        bool _reserved :1;\n        bool stt  :1;\n        bool free :1;\n        bool nackmod :1;\n    } bits;\n    uint16_t all;\n};\n

    Dans cet exemple on peut soit acc\u00e9der \u00e0 l'ensemble des bits via le champ all soit \u00e0 chacun des bits via les champs bc, fdf, stb, etc.

    union i2cmdr cmdr = { .all = 0x1234 };\n\ncmdr.bits.bc = 0b101;\n\nuint16_t all = cmdr.all;\n

    Les unions peuvent \u00eatre imbriqu\u00e9es, c'est-\u00e0-dire contenir des unions elles-m\u00eames. Cela permet de d\u00e9finir des structures de donn\u00e9es complexes.

    union {\n    union {\n        int a;\n        int b;\n    } u1;\n    union {\n        int c;\n        int d;\n    } u2;\n} u;\n
    ", "tags": ["fdf", "all", "stb"]}, {"location": "course-c/20-composite-types/unions/#taille", "title": "Taille", "text": "

    La taille d'une union est \u00e9gale \u00e0 la taille de son plus grand champ. Donc dans l'exemple suivant, la taille de u est de 4 octets.

    union {\n    char c;\n    int i;\n} u;\n
    "}, {"location": "course-c/25-architecture-and-systems/abi/", "title": "ABI", "text": "

    Une Application Binary Interface (ABI) est une interface entre deux modules de code binaire, souvent un programme et une biblioth\u00e8que, au niveau de l'assembleur. L'ABI d\u00e9finit comment les fonctions, les donn\u00e9es et les structures sont organis\u00e9es et accessibles dans le code binaire. Elle sp\u00e9cifie des conventions pour les appels de fonctions, la gestion de la m\u00e9moire, le format des donn\u00e9es, etc.

    Nous avons vu par exemple que l'appel d'une fonction en C peut \u00eatre r\u00e9alis\u00e9 en utilisant la pile. Mais comment est-ce que cela fonctionne derri\u00e8re les coulisses\u2009? L'ABI d\u00e9finit comment les arguments sont pass\u00e9s \u00e0 une fonction, comment les valeurs de retour sont retourn\u00e9es, comment les variables locales sont stock\u00e9es, etc. Elle d\u00e9finit quels sont les registres qui doivent \u00eatre sauvegard\u00e9s avant un appel de fonction et ceux qui sont utilis\u00e9s pour passer des arguments. En somme, c'est une convention qui permet \u00e0 des modules de code binaire de communiquer entre eux.

    En outre, sur un syst\u00e8me d'exploitation, un programme communique avec le noyau du syst\u00e8me d'exploitation en utilisant des appels syst\u00e8me. L'ABI d\u00e9finit comment ces appels syst\u00e8me sont r\u00e9alis\u00e9s. Par exemple, sur Linux, les appels syst\u00e8me sont r\u00e9alis\u00e9s en utilisant le registre eax pour passer le num\u00e9ro de l'appel syst\u00e8me et les arguments sont pass\u00e9s dans les registres ebx, ecx, edx, esi, edi, ebp. Le r\u00e9sultat de l'appel syst\u00e8me est retourn\u00e9 dans le registre eax.

    L'ABI est donc sp\u00e9cifique \u00e0 une architecture mat\u00e9rielle et \u00e0 un syst\u00e8me d'exploitation. Par exemple, l'ABI pour les programmes Linux sur une architecture x86-64 est diff\u00e9rente de celle pour les programmes Windows sur la m\u00eame architecture. Cela signifie que les programmes compil\u00e9s pour une architecture et un syst\u00e8me d'exploitation ne peuvent pas \u00eatre ex\u00e9cut\u00e9s sur une autre architecture ou un autre syst\u00e8me d'exploitation car ces conventions ne sont pas respect\u00e9es. C'est pourquoi il est important de conna\u00eetre l'ABI de la plateforme cible lors de la compilation d'un programme.

    ", "tags": ["ecx", "edi", "esi", "edx", "eax", "ebp", "ebx"]}, {"location": "course-c/25-architecture-and-systems/abi/#elf", "title": "ELF", "text": "

    Le format de fichier ex\u00e9cutable et de liaison (Executable and Linkable Format, ELF) est un format de fichier binaire standard pour les fichiers ex\u00e9cutables, les fichiers objet, les biblioth\u00e8ques partag\u00e9es et les fichiers de base de donn\u00e9es de d\u00e9bogage. Il est utilis\u00e9 sur la plupart des syst\u00e8mes d'exploitation Unix et Unix-like, y compris Linux, Solaris, FreeBSD, etc.

    Lorsque vous compilez un programme avec GCC, le compilateur g\u00e9n\u00e8re un fichier binaire au format ELF. Vous pouvez en avoir la preuve avec la commande file sur un programme compil\u00e9 avec gcc sous Linux\u2009:

    $ file a.out\na.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),\ndynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,\nBuildID[sha1]=2066ecb2b4fed0b91adbcc63d0e7c13a8bea14a8,\nfor GNU/Linux 3.2.0, not stripped\n

    On peut \u00e9galement consulter le contenu d'un fichier ELF avec la commande readelf:

    $ readelf -h a.out\nELF Header:\n  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00\n  Class:                             ELF64\n  Data:                              2's complement, little endian\n  Version:                           1 (current)\n  OS/ABI:                            UNIX - System V\n  ABI Version:                       0\n  Type:                              DYN (Position-Independent Executable file)\n  Machine:                           Advanced Micro Devices X86-64\n  Version:                           0x1\n  Entry point address:               0x1060\n  Start of program headers:          64 (bytes into file)\n  Start of section headers:          13968 (bytes into file)\n  Flags:                             0x0\n  Size of this header:               64 (bytes)\n  Size of program headers:           56 (bytes)\n  Number of program headers:         13\n  Size of section headers:           64 (bytes)\n  Number of section headers:         31\n  Section header string table index: 30\n

    Ce que l'on constate c'est que le format ELF est compos\u00e9 de plusieurs parties, notamment un en-t\u00eate ELF, des en-t\u00eates de programme, des en-t\u00eates de section, des tables de symboles, des r\u00e9implantations dynamiques, etc. Chaque partie a un r\u00f4le sp\u00e9cifique dans la gestion des fichiers binaires et des biblioth\u00e8ques partag\u00e9es. Dans cet exemple on observe que cet elf est pour l'ABI UNIX - System V.

    Le System V est une version d'Unix d\u00e9velopp\u00e9e par AT&T et commercialis\u00e9e par Sun Microsystems. Elle a \u00e9t\u00e9 l'une des premi\u00e8res versions d'Unix \u00e0 \u00eatre largement utilis\u00e9e dans l'industrie. Le format ELF est donc un standard pour les syst\u00e8mes Unix et Unix-like qui suit les conventions de l'ABI System V.

    Sous Windows, le format de fichier ex\u00e9cutable est le format Portable Executable (PE) qui est diff\u00e9rent du format ELF. Cela signifie que les programmes compil\u00e9s pour Windows ne peuvent pas \u00eatre ex\u00e9cut\u00e9s sur un syst\u00e8me Unix et vice versa sans une couche de compatibilit\u00e9 sp\u00e9cifique.

    Sous macOS, le format de fichier ex\u00e9cutable est le format Mach-O (Mach Object) qui est \u00e9galement diff\u00e9rent du format ELF. Cela signifie que les programmes compil\u00e9s pour macOS ne peuvent pas \u00eatre ex\u00e9cut\u00e9s sur un syst\u00e8me Unix ou Windows sans une couche de compatibilit\u00e9 sp\u00e9cifique.

    ", "tags": ["file", "elf", "readelf"]}, {"location": "course-c/25-architecture-and-systems/abi/#eabi", "title": "EABI", "text": "

    L'ABI embarqu\u00e9 (Embedded ABI, EABI) est une version de l'ABI con\u00e7ue pour les syst\u00e8mes embarqu\u00e9s, c'est-\u00e0-dire des syst\u00e8mes informatiques sp\u00e9cialis\u00e9s qui sont int\u00e9gr\u00e9s dans des appareils \u00e9lectroniques. Les syst\u00e8mes embarqu\u00e9s sont souvent limit\u00e9s en termes de ressources mat\u00e9rielles et logicielles, ce qui n\u00e9cessite des conventions sp\u00e9cifiques pour les appels de fonctions, la gestion de la m\u00e9moire, le format des donn\u00e9es, etc.

    "}, {"location": "course-c/25-architecture-and-systems/computer/", "title": "L'ordinateur", "text": "

    Un ordinateur personnel (PC pour Personal Computer) est un appareil \u00e9lectronique de petite taille destin\u00e9 \u00e0 un usage individuel. Il se distingue des ordinateurs centraux (ou mainframes) et des serveurs, qui sont destin\u00e9s \u00e0 un usage professionnel ou collectif.

    N\u00e9anmoins, quelle que soit la taille de l'ordinateur, les composants de base sont les m\u00eames. Un ordinateur est compos\u00e9 de plusieurs \u00e9l\u00e9ments principaux\u2009:

    • Un processeur (ou CPU pour Central Processing Unit) qui ex\u00e9cute les instructions des programmes.
    • De la m\u00e9moire (ou RAM pour Random Access Memory) qui stocke les donn\u00e9es et les instructions des programmes en cours d'ex\u00e9cution.
    • Un disque dur (ou HDD pour Hard Disk Drive) qui stocke les donn\u00e9es de mani\u00e8re permanente.
    • Une carte graphique qui affiche les images \u00e0 l'\u00e9cran.
    • Une carte m\u00e8re qui relie tous les composants entre eux.
    "}, {"location": "course-c/25-architecture-and-systems/computer/#la-ram", "title": "La RAM", "text": "

    La m\u00e9moire vive est une m\u00e9moire de stockage temporaire, on l'appelle \u00e9galement m\u00e9moire non volatile. Le plus souvent une m\u00e9moire vive est amovible, il s'agit d'une barrette enfichable sur la carte m\u00e8re. Avec l'\u00e9volution de la technologie, ces m\u00e9moires sont car\u00e9n\u00e9es et munies d'un dissipateur thermique\u2009:

    2 x 16 GB DDR5 DIMM Corsair Vengeance

    Sous le cap\u00f4t, on peut voir les puces de m\u00e9moire\u2009:

    Crucial DDR4 16 GB

    Cette m\u00e9moire dispose de 16 Gibioctets de m\u00e9moire, soit \\(16 \\times 2^30 = 17179869184\\) octets. Chaque octet est compos\u00e9 de \\(8\\) bits, soit \\(17179869184 \\times 8 = 137438953472\\) bits. Comme nous voyons \\(4\\) puces de m\u00e9moire, chaque puce contient \\(4\\) Gibioctets.

    G\u00e9n\u00e9ralement, ces m\u00e9moires sont vendues en nombre de bits, soit ici 32 Gibibits.

    Sur le circuit \u00e9lectronique ou PCB (Printed Circuit Board), on voit les 4 puces de m\u00e9moire soud\u00e9es. Il s'agit d'un composant de la soci\u00e9t\u00e9 Micron, un MT40A1G8. La structure interne de cette m\u00e9moire est donn\u00e9e par la datasheet du composant\u2009:

    MT40A1G8

    Pour d\u00e9coder ce sch\u00e9ma, int\u00e9ressons-nous aux fl\u00e8ches de couleur. Il s'agit du bus d'adresse. Ce bus comporte 16 lignes en parall\u00e8le qui sont interfac\u00e9es \u00e0 deux blocs\u2009: le Row Address MUX et le Column address counter. Ces deux blocs permettent de s\u00e9lectionner une cellule m\u00e9moire selon la m\u00e9moire, une cellule peut valoir 4, 8, 16 ou 32 bits.

    Les cellules m\u00e9moires sont organis\u00e9es and matrice ligne/colonne et chaque matrice est organis\u00e9e en banque. C'est ce qu'on observe sur ce diagramme.

    Une m\u00e9moire volatile est une m\u00e9moire qui perd son contenu lorsqu'elle n'est plus aliment\u00e9e en \u00e9lectricit\u00e9. La raison est simple. Stocker un \u00e9tat \u00e9lectrique demande de l'\u00e9nergie pour accumuler des charges \u00e9lectriques. Si l'on fait l'analogie que l'\u00e9lectricit\u00e9 est de l'eau, alors chaque bit de la m\u00e9moire est un verre d'eau que l'on peut remplir ou vider. Le seul moyen de lire le contenu du verre est de voir s'il y a de l'eau dedans, c'est-\u00e0-dire de le vider. Si le verre est grand, alors il faut plus de temps pour le remplir et plus de temps pour le vider ceci pr\u00e9sente plusieurs inconv\u00e9nients\u2009:

    1. La vitesse de lecture est plus lente.
    2. La quantit\u00e9 d'eau (courant) pour remplir le verre est plus grande.
    3. L'encombrement est plus grand puisque le verre est plus volumineux.

    Aussi, le choix technologique est d'avoir des tout petits verres. Ils sont si petits que l'eau contenue s'\u00e9vapore tr\u00e8s vite. Pour \u00e9viter cela, on doit constamment remplir les verres. C'est ce que l'on appelle la rafra\u00eechissement de la m\u00e9moire. P\u00e9riodiquement, environ toutes les 64 ms, on doit r\u00e9\u00e9crire le contenu de la m\u00e9moire pour \u00e9viter que l'information ne se perde. Heureusement pour nous, cette op\u00e9ration est transparente pour l'utilisateur, c'est le contr\u00f4leur de m\u00e9moire qui s'en charge.

    Les caract\u00e9ristiques de la m\u00e9moire sont les suivantes\u2009:

    Caract\u00e9ristique Valeur Unit\u00e9 Capacit\u00e9 32 Gib Tension d'alimentation 1.2 V Fr\u00e9quence 1600 MHz Temps de rafra\u00eechissement 64 ms Nombre de banques 16 Technologie DDR4"}, {"location": "course-c/25-architecture-and-systems/computer/#technologies", "title": "Technologies", "text": "

    Il existe plusieurs technologies de m\u00e9moire vive. Les plus courantes sont\u2009: SDRAM, DDR, DDR2, DDR3, DDR4. Contrairement \u00e0 la SDRAM qui est une m\u00e9moire synchrone, les m\u00e9moires DDR (Double Data Rate) sont des m\u00e9moires asynchrones. Cela signifie que la m\u00e9moire peut lire et \u00e9crire des donn\u00e9es sur le flanc montant et descendant du signal d'horloge ce qui double la bande passante de la m\u00e9moire. Chaque g\u00e9n\u00e9ration am\u00e9liore les performances en augmentant la fr\u00e9quence de fonctionnement, la densit\u00e9 des puces m\u00e9moires et en r\u00e9duisant la tension d'alimentation.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#vitesse-de-la-lumiere", "title": "Vitesse de la lumi\u00e8re", "text": "

    Autoroute de l'information

    La vitesse de la lumi\u00e8re est de 299 792 458 m/s. Elle est fix\u00e9e par la convention du m\u00e8tre. C'est la vitesse maximale que peut atteindre un objet dans l'univers. Pour donner un ordre de grandeur, un signal \u00e9lectrique se propage dans un c\u00e2ble \u00e0 environ \u2154 de la vitesse de la lumi\u00e8re. Cela signifie que pour parcourir 1 m\u00e8tre, un signal \u00e9lectrique met environ 5 ns.

    Plus haut on a vu que le bus de donn\u00e9es de la m\u00e9moire est souvent de 64-bits. Cela correspond \u00e0 une autoroute de 64 voies avec quelques limitations\u2009:

    • Les voies sont unidirectionnelles, c'est-\u00e0-dire que l'on ne peut circuler que dans un sens.
    • Les voies sont s\u00e9par\u00e9es par des barri\u00e8res, c'est-\u00e0-dire que l'on ne peut pas changer de voie.
    • Les v\u00e9hicules se d\u00e9placent tous \u00e0 la vitesse d'environ 540 millions de km/h. Ils ne peuvent pas freiner, acc\u00e9l\u00e9rer ou s'arr\u00eater.

    Pour transmettre une information, par exemple un nombre entier de 64 bits (long long en C), il faut faire entrer 64 v\u00e9hicules sur chacune des voies. Chaque v\u00e9hicule repr\u00e9sente un bit. Pour que l'information soit transmise, il faut que les 64 v\u00e9hicules soient align\u00e9s et qu'ils arrivent tous au m\u00eame moment.

    Sur la figure suivante, on voit le routage d'un circuit \u00e9lectronique. En rose, ce sont les composants physiques. \u00c0 gauche un processeur et au milieu en bas deux circuits m\u00e9moire lab\u00e9lis\u00e9s DDR1 et DDR2. En bleu clair ce sont les lignes \u00e9lectriques qui relient les composants. On observe des tas de petites circonvolutions. Les lignes sont artificiellement rallong\u00e9es pour que la longueur de chaque voie de l'autoroute soit la m\u00eame, afin de garantir une vitesse de propagation identique pour chaque ligne de donn\u00e9e.

    Routage d'une m\u00e9moire

    Vous me direz, oui, mais 540 millions de km/h c'est super rapide et sur ce circuit les lignes ne font pas plus de 10 cm ce qui repr\u00e9sente 600 ps pour parcourir la distance. Oui, mais voil\u00e0, on communique sur cette autoroute \u00e0 2000 MT/s (m\u00e9gatransferts par seconde). Cela signifie que 2'000'000 de v\u00e9hicules entrent sur chaque voie de l'autoroute chaque seconde circuler sur chaque voie de l'autoroute chaque seconde. N'est-ce pas incroyable\u2009?

    Malgr\u00e9 ces performances, la m\u00e9moire reste un goulot d'\u00e9tranglement pour les processeurs. En effet, les processeurs sont de plus en plus rapides et les m\u00e9moires ne suivent pas le rythme. Un processeur qui calcule \u00e0 4 GHz peut ex\u00e9cuter 4 milliards d'instructions par seconde. Si chaque instruction n\u00e9cessite un acc\u00e8s m\u00e9moire et que cet acc\u00e8s prend 100 cycles d'horloge, alors le processeur ne pourra ex\u00e9cuter que 40 millions d'instructions par seconde. Cela signifie que le processeur ne sera utilis\u00e9 qu'\u00e0 1% de sa capacit\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#le-disque-dur", "title": "Le disque dur", "text": "

    Disque dur

    Le disque dur est un dispositif de stockage de masse. Il est compos\u00e9 de plusieurs plateaux magn\u00e9tiques qui tournent \u00e0 grande vitesse. Un bras m\u00e9canique se d\u00e9place sur les plateaux pour lire ou \u00e9crire les donn\u00e9es. Les disques durs sont lents par rapport \u00e0 la m\u00e9moire vive. Ils sont utilis\u00e9s pour stocker des donn\u00e9es de mani\u00e8re permanente.

    De nos jours ces disques sont remplac\u00e9s par des disques SSD (Solid State Drive) qui sont plus rapides et plus fiables. Les disques SSD sont compos\u00e9s de m\u00e9moire flash qui ne n\u00e9cessite pas de pi\u00e8ces mobiles. Contrairement \u00e0 la m\u00e9moire vive, les disques SSD sont des m\u00e9moires non volatiles. Cela signifie que les donn\u00e9es sont conserv\u00e9es m\u00eame lorsque l'alimentation est coup\u00e9e.

    SSD de 2 TiB

    Mais si les SSD peuvent stocker beaucoup plus de donn\u00e9es sur le m\u00eame espace, pourquoi sont-ils plus lents que la m\u00e9moire vive\u2009? La raison est simple. Les disques SSD sont organis\u00e9s en blocs de donn\u00e9es, que l'on appelle pages et clusters. Pour lire ou \u00e9crire une donn\u00e9e, il faut lire ou \u00e9crire tout le bloc. Cela signifie que si l'on veut lire un octet, il faut lire 4'096 octets. C'est ce que l'on appelle le page size.

    La communication entre le processeur et le disque SSD ou HDD utilise un protocole de communication s\u00e9rie appel\u00e9 SATA (Serial ATA). Ce protocole permet de transf\u00e9rer des donn\u00e9es \u00e0 une vitesse de 6 Gbit/s. Cela signifie que pour transf\u00e9rer un octet, il faut 8 bits, soit 8 ns. Cela semble rapide, mais si l'on veut lire un bloc de 4'096 octets, il faut 32'768 bits, soit 32'768 x 8 ns = 262'144 ns, soit 262 \u00b5s. C'est 262'144 fois plus lent que la m\u00e9moire vive.

    Pour interfacer le processeur avec le disque dur, on utilise un contr\u00f4leur de disque. Ce contr\u00f4leur est un circuit \u00e9lectronique qui g\u00e8re les acc\u00e8s disque. Il est compos\u00e9 lui-m\u00eame d'un microprocesseur, de m\u00e9moire vive et de m\u00e9moire flash.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#la-carte-mere", "title": "La carte m\u00e8re", "text": "

    Carte m\u00e8re

    La carte m\u00e8re est le composant principal de l'ordinateur. C'est elle qui relie tous les composants entre eux. Elle est compos\u00e9e d'un circuit imprim\u00e9 sur lequel sont soud\u00e9s les diff\u00e9rents composants et une grande quantit\u00e9 de connecteurs.

    Le c\u0153ur de la carte m\u00e8re est le chipset. C'est un ensemble de circuits \u00e9lectroniques qui g\u00e8re les communications entre les diff\u00e9rents composants. Il est compos\u00e9 de deux parties\u2009:

    • Le Northbridge qui g\u00e8re les communications entre le processeur, la m\u00e9moire vive et la carte graphique.
    • Le Southbridge qui g\u00e8re les communications entre les p\u00e9riph\u00e9riques de stockage, les ports USB, les ports SATA, etc.

    Le chipset est reli\u00e9 au processeur par un bus de donn\u00e9es appel\u00e9 FSB (Front Side Bus). Ce bus transporte les donn\u00e9es entre le processeur et le chipset. La configuration du chipset est stock\u00e9e dans une m\u00e9moire flash appel\u00e9e BIOS (Basic Input/Output System). Le BIOS est un logiciel qui permet de configurer les param\u00e8tres de la carte m\u00e8re.

    \u00c0 l'\u00e9poque le BIOS offrait un acc\u00e8s tr\u00e8s minimaliste \u00e0 l'utilisateur. On pouvait le configurer avec un clavier et un \u00e9cran qui n'affichait que des caract\u00e8res.

    De nos jours, le BIOS a \u00e9t\u00e9 remplac\u00e9 par l'UEFI (Unified Extensible Firmware Interface). L'UEFI est un logiciel plus \u00e9volu\u00e9 qui permet de configurer la carte m\u00e8re avec une interface graphique. Il est possible de configurer la carte m\u00e8re avec une souris et un \u00e9cran tactile.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#le-processeur", "title": "Le processeur", "text": "

    Le processeur est le cerveau de l'ordinateur. C'est lui qui ex\u00e9cute les instructions des programmes. La figure suivante montre un processeur Intel i7-12700K dans son format LGA 1700. C'est \u00e0 dire qu'il comporte 1700 broches pour se connecter \u00e0 la carte m\u00e8re.

    Processeur Intel i7

    Sur les 1700 broches on distingue plusieurs types de broches\u2009:

    • Les broches d'alimentation qui repr\u00e9sentent 40..60% des broches. Elles sont n\u00e9cessaires pour alimenter le processeur avec une tension de 1.2V.
    • Le contr\u00f4leur m\u00e9moire (DDR4/DDR5) qui permet de connecter la m\u00e9moire vive au processeur. Cela repr\u00e9sente environ 5..15% des broches.
    • Les interfaces PCIe qui permettent de connecter des cartes d'extension comme des cartes graphiques, des cartes r\u00e9seau, des cartes son, etc. Ce processeur supporte jusqu'\u00e0 20 lignes diff\u00e9rentielles soit 40 broches.
    • L'acc\u00e8s DMI, c'est l'interface entre lwe processeur et le chipset. Un DMI 4.0 x8 signifie qu'il y a 8 lignes (Rx/Tx), soit envron 16 broches.
    • L'USB, quelques dizaines de broches.
    • Le contr\u00f4leur graphique int\u00e9gr\u00e9 (iGPU) qui comporte des ports HDMI/DisplayPort pour connecter un \u00e9cran directement au processeur.
    • Les interconnexions sp\u00e9cifiques (I2C, SPI, etc.)

    Si on consulte le SDM (Software Developer Manual) d'Intel, un document de 5000 pages, on peut trouver des informations tr\u00e8s int\u00e9ressntes. Par exemple le chapitre sur les types num\u00e9riques montre les diff\u00e9rents type d'entiers ( byte, word, dword, qword), de flottants (half, single, double, extended) et de vecteurs (xmm, ymm, zmm, kmm). Il est expliqu\u00e9 que le processeur fonctionne avec le compl\u00e9ment \u00e0 2 pour les entiers et le IEEE 754 pour les flottants, qu'il est en little-endian et que les registres sont de 64 bits. Le langage C au final est tr\u00e8s proche de l'assembleur du processeur.

    ", "tags": ["half", "double", "extended", "zmm", "byte", "ymm", "kmm", "xmm", "word", "single", "qword", "dword"]}, {"location": "course-c/25-architecture-and-systems/computer/#protection-ring", "title": "Protection ring", "text": "

    Les protection rings (ou anneaux de protection) sont un m\u00e9canisme de s\u00e9curit\u00e9 utilis\u00e9 dans l'architecture des processeurs, principalement dans les syst\u00e8mes d'exploitation modernes, pour contr\u00f4ler l'acc\u00e8s aux ressources du syst\u00e8me par diff\u00e9rents types de code (comme les applications ou les composants du syst\u00e8me d'exploitation).

    L'id\u00e9e des anneaux de protection repose sur la s\u00e9paration des niveaux de privil\u00e8ge ou de contr\u00f4le en plusieurs couches. Chaque couche, ou ring (anneau), est un niveau de privil\u00e8ge qui d\u00e9termine ce qu\u2019un programme ou une instruction peut faire. Dans l'architecture x86 d'Intel, il y a g\u00e9n\u00e9ralement quatre anneaux (de 0 \u00e0 3), bien que les syst\u00e8mes d'exploitation modernes n\u2019utilisent souvent que deux de ces niveaux (Ring 0 et Ring 3).

    Anneaux de protection

    Le ring 0 correspond au noyau (kernel) du syst\u00e8me d'exploitation, qui a acc\u00e8s \u00e0 toutes les ressources mat\u00e9rielles de l'ordinateur. Il peut ex\u00e9cuter n'importe quelle instruction, acc\u00e9der \u00e0 la m\u00e9moire directement, et g\u00e9rer le mat\u00e9riel sans restriction. On parle souvent de mode superviseur ou mode noyau pour d\u00e9signer les op\u00e9rations effectu\u00e9es dans cet anneau. C\u2019est le niveau le plus privil\u00e9gi\u00e9.

    Les niveaux interm\u00e9diaires 1 et 2 peuvent \u00eatre utilis\u00e9s par certains syst\u00e8mes pour les pilotes ou des services du syst\u00e8me qui ont besoin d'un acc\u00e8s contr\u00f4l\u00e9 aux ressources, mais ne n\u00e9cessitent pas le m\u00eame niveau de privil\u00e8ge que le noyau. Toutefois, la plupart des syst\u00e8mes d'exploitation modernes ne les utilisent pas directement.

    Enfin, le niveau 3 est r\u00e9serv\u00e9 aux applications utilisateur. Il s\u2019agit du mode utilisateur (user mode), dans lequel les programmes n\u2019ont pas un acc\u00e8s direct au mat\u00e9riel ou \u00e0 la m\u00e9moire, et doivent passer par des appels syst\u00e8me pour demander des services au noyau. En cas de violation des r\u00e8gles (comme essayer d\u2019acc\u00e9der directement au mat\u00e9riel), une exception ou une erreur est g\u00e9n\u00e9r\u00e9e, et le programme est bloqu\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#bref-historique", "title": "Bref historique", "text": ""}, {"location": "course-c/25-architecture-and-systems/computer/#1978-processeurs-16-bits", "title": "1978\u2009: Processeurs 16 bits", "text": "

    L'introduction des processeurs 8086 et 8088 a marqu\u00e9 le d\u00e9but de l'architecture IA-32 avec des registres 16 bits et une adresse m\u00e9moire maximale de 1 Mo. La segmentation permettait d'adresser jusqu'\u00e0 256 Ko sans changement de segment, ouvrant la voie \u00e0 une gestion plus efficace de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1982-intel-286", "title": "1982\u2009: Intel 286", "text": "

    Le processeur Intel 286 a introduit le mode prot\u00e9g\u00e9, permettant une meilleure gestion de la m\u00e9moire avec un adressage sur 24 bits et la possibilit\u00e9 de g\u00e9rer jusqu'\u00e0 16 Mo de m\u00e9moire physique. Le mode prot\u00e9g\u00e9 apportait \u00e9galement des m\u00e9canismes de protection tels que la v\u00e9rification des limites des segments et plusieurs niveaux de privil\u00e8ges.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1985-intel-386", "title": "1985\u2009: Intel 386", "text": "

    Premi\u00e8re architecture v\u00e9ritablement 32 bits, l'Intel 386 a introduit des registres 32 bits et un bus d'adressage permettant de g\u00e9rer jusqu'\u00e0 4 Go de m\u00e9moire physique. Il proposait aussi un mode de m\u00e9moire virtuelle et un mod\u00e8le m\u00e9moire \u00e0 pages de 4 Ko, facilitant la gestion efficace de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1989-intel-486", "title": "1989\u2009: Intel 486", "text": "

    Le processeur Intel 486 a ajout\u00e9 des capacit\u00e9s de traitement parall\u00e8le avec cinq \u00e9tapes de pipeline d\u2019ex\u00e9cution, permettant l\u2019ex\u00e9cution simultan\u00e9e d'instructions. Il a \u00e9galement introduit un cache de 8 Ko sur la puce et un coprocesseur math\u00e9matique int\u00e9gr\u00e9 (FPU).

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1993-intel-pentium", "title": "1993\u2009: Intel Pentium", "text": "

    L'Intel Pentium a marqu\u00e9 une nouvelle avanc\u00e9e avec l'ajout de deux pipelines d'ex\u00e9cution, permettant l'ex\u00e9cution de deux instructions par cycle d'horloge. Il a \u00e9galement int\u00e9gr\u00e9 un syst\u00e8me de pr\u00e9diction de branchement et augment\u00e9 le bus de donn\u00e9es externe \u00e0 64 bits. Plus tard, la technologie MMX a \u00e9t\u00e9 introduite, optimisant le traitement parall\u00e8le de donn\u00e9es pour les applications multim\u00e9dia.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#1995-famille-p6", "title": "1995\u2009: Famille P6", "text": "

    La famille P6 a apport\u00e9 une nouvelle microarchitecture superscalaire avec un processus de fabrication de 0,6 micron, am\u00e9liorant consid\u00e9rablement les performances tout en maintenant la compatibilit\u00e9 avec les technologies existantes.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2000-intel-pentium-4", "title": "2000\u2009: Intel Pentium 4", "text": "

    Bas\u00e9 sur l'architecture NetBurst, le Pentium 4 a introduit les extensions SIMD Streaming (SSE2), puis SSE3, pour acc\u00e9l\u00e9rer les calculs multim\u00e9dias. Le support du 64 bits avec l'Intel 64 architecture a \u00e9galement fait son apparition, ainsi que la technologie Hyper-Threading pour ex\u00e9cuter plusieurs threads simultan\u00e9ment.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2001-intel-xeon", "title": "2001\u2009: Intel Xeon", "text": "

    La gamme Xeon, bas\u00e9e \u00e9galement sur l'architecture NetBurst, a \u00e9t\u00e9 con\u00e7ue pour les serveurs multiprocesseurs et les stations de travail. Elle a introduit le multithreading (Hyper-Threading) et, plus tard, des processeurs multi-c\u0153urs pour augmenter les performances dans les environnements professionnels.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2008-intel-core-i7", "title": "2008\u2009: Intel Core i7", "text": "

    La microarchitecture Nehalem, utilis\u00e9e dans la premi\u00e8re g\u00e9n\u00e9ration d'Intel Core i7, a marqu\u00e9 l'av\u00e8nement du 45 nm, avec des fonctionnalit\u00e9s comme le Turbo Boost, l\u2019Hyper-Threading, un contr\u00f4leur m\u00e9moire int\u00e9gr\u00e9, et un cache Smart Cache de 8 Mo. Le lien QuickPath Interconnect (QPI) a remplac\u00e9 l\u2019ancien bus pour des \u00e9changes plus rapides avec le chipset.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2011-intel-core-sandy-bridge", "title": "2011\u2009: Intel Core Sandy Bridge", "text": "

    Cette g\u00e9n\u00e9ration, construite en 32 nm, a apport\u00e9 des am\u00e9liorations en termes de performance et d'efficacit\u00e9 \u00e9nerg\u00e9tique, avec des innovations comme l'int\u00e9gration des graphismes dans le processeur et l'Intel Quick Sync Video. La gamme incluait les processeurs Core i3, i5, et i7.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2012-intel-core-ivy-bridge", "title": "2012\u2009: Intel Core Ivy Bridge", "text": "

    L'Ivy Bridge a introduit une finesse de gravure de 22 nm, permettant une meilleure gestion de la consommation \u00e9nerg\u00e9tique tout en am\u00e9liorant les performances graphiques et g\u00e9n\u00e9rales du processeur. Cette g\u00e9n\u00e9ration a \u00e9galement marqu\u00e9 l'arriv\u00e9e de processeurs Xeon plus puissants pour les serveurs.

    "}, {"location": "course-c/25-architecture-and-systems/computer/#2013-intel-core-haswell", "title": "2013\u2009: Intel Core Haswell", "text": "

    La quatri\u00e8me g\u00e9n\u00e9ration, bas\u00e9e sur l\u2019architecture Haswell, a continu\u00e9 d'am\u00e9liorer les performances et l'efficacit\u00e9 \u00e9nerg\u00e9tique, tout en proposant des am\u00e9liorations comme l\u2019int\u00e9gration de la gestion de l\u2019alimentation et des performances graphiques am\u00e9lior\u00e9es pour r\u00e9pondre aux besoins des utilisateurs modernes.

    Ce r\u00e9sum\u00e9 souligne les progr\u00e8s constants en termes de puissance de traitement, de gestion m\u00e9moire, de parall\u00e9lisme, et d\u2019efficacit\u00e9 \u00e9nerg\u00e9tique des processeurs Intel au fil des g\u00e9n\u00e9rations.

    "}, {"location": "course-c/25-architecture-and-systems/files/", "title": "Fichiers", "text": ""}, {"location": "course-c/25-architecture-and-systems/files/#systeme-de-fichiers", "title": "Syst\u00e8me de fichiers", "text": "

    Dans un environnement POSIX tout est fichier. stdin est un fichier, une souris USB est un fichier, un clavier est un fichier, un terminal est un fichier, un programme est un fichier.

    Les fichiers sont organis\u00e9s dans une arborescence g\u00e9r\u00e9e par un syst\u00e8me de fichiers. Sous Windows l'arborescence classique est\u2009:

    C :\n\u251c\u2500\u2500 Program Files         Programmes install\u00e9s\n\u251c\u2500\u2500 Users                 Comptes utilisateur\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 John\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 Desktop\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 Documents\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 Music\n\u2514\u2500\u2500 Windows\n    \u251c\u2500\u2500 Fonts             Polices de caract\u00e8res\n    \u251c\u2500\u2500 System32          Syst\u00e8me d'exploitation 64-bits (oui, oui)\n    \u2514\u2500\u2500 Temp              Fichiers temporaires\n

    Il y a une arborescence par disque physique C:, D:, une arborescence par chemin r\u00e9seau \\\\eistore2, etc. Sous POSIX, la strat\u00e9gie est diff\u00e9rente, car il n'existe qu'un seul syst\u00e8me de fichier dont la racine est /.

    /\n\u251c\u2500\u2500 bin                   Programmes ex\u00e9cutables cruciaux\n\u251c\u2500\u2500 dev                   P\u00e9riph\u00e9riques (clavier, souris ...)\n\u251c\u2500\u2500 usr\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 bin               Programmes install\u00e9s\n\u251c\u2500\u2500 mnt                   Points de montage (disques r\u00e9seaux, CD, cl\u00e9 USB)\n\u2502   \u2514\u2500\u2500 eistore2\n\u251c\u2500\u2500 tmp                   Fichiers temporaires\n\u251c\u2500\u2500 home                  Comptes utilisateurs\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 john\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 documents\n\u2514\u2500\u2500 var                   Fichiers variables comme les logs ou les database\n

    Chaque \u00e9l\u00e9ment qui contient d'autres \u00e9l\u00e9ments est appel\u00e9 un r\u00e9pertoire ou dossier, en anglais directory. Chaque r\u00e9pertoire contient toujours au minimum deux fichiers sp\u00e9ciaux\u2009:

    .

    Un fichier qui symbolise le r\u00e9pertoire courant, celui dans lequel je me trouve

    ..

    Un fichier qui symbolise le r\u00e9pertoire parent, c'est \u00e0 dire home lorsque je suis dans john.

    La localisation d'un fichier au sein d'un syst\u00e8me de fichier peut \u00eatre soit absolue soit relative. Cette localisation s'appelle un chemin ou path. La convention est d'utiliser le symbole\u2009:

    • Slash / sous POSIX
    • Antislash \\ sous Windows

    Le chemin /usr/bin/.././bin/../../home/john/documents est correct, mais il n'est pas canonique, on dit qu'il n'est pas r\u00e9solu. La forme canonique est /home/john/documents.

    Un chemin peut \u00eatre relatif s'il ne commence pas par un /: ../bin. Sous Windows du m\u00eame acabit, mais la racine diff\u00e9remment selon le type de m\u00e9dia C:\\, \\\\network...

    Lorsqu'un programme s'ex\u00e9cute, son contexte d'ex\u00e9cution est toujours par rapport \u00e0 son emplacement dans le syst\u00e8me de fichier, donc le chemin peut \u00eatre soit relatif, soit absolu.

    ", "tags": ["stdin", "john", "home"]}, {"location": "course-c/25-architecture-and-systems/files/#navigation", "title": "Navigation", "text": "

    Sous Windows (PowerShell) ou un syst\u00e8me POSIX (Bash/Sh/Zsh), la navigation dans une arborescence peut \u00eatre effectu\u00e9e en ligne de commande \u00e0 l'aide des commandes (programmes) suivants\u2009:

    ls

    est un raccourci du nom list, ce programme permet d'afficher sur la sortie standard le contenu d'un r\u00e9pertoire.

    cd

    pour change directory permets de naviguer dans l'arborescence. Le programme prend en argument un chemin absolu ou relatif. En cas d'absence d'arguments, le programme redirige vers le r\u00e9pertoire de l'utilisateur courant.

    "}, {"location": "course-c/25-architecture-and-systems/files/#format-dun-fichier", "title": "Format d'un fichier", "text": "

    Un fichier peut avoir un contenu arbitraire\u2009; une suite de z\u00e9ro et d\u2019un binaire. Selon l'interpr\u00e9tation, un fichier pourrait contenir une image, un texte ou un programme. Le cas particulier ou le contenu est lisible par un \u00e9diteur de texte, on appelle ce fichier un fichier texte. C'est-\u00e0-dire que chaque caract\u00e8re est encod\u00e9 sur 8-bit et que la table ASCII est utilis\u00e9e pour traduire le contenu en un texte intelligible. Lorsque le contenu n'est pas du texte, on l'appelle un fichier binaire.

    La fronti\u00e8re est parfois assez mince, car parfois le fichier binaire peut contenir du texte intelligible, la preuve avec ce programme\u2009:

    #include <stdio.h>\n#include <string.h>\n\nint main(int* argc, char* argv[])\n{\n    static const char password[] = \"un mot de passe secret\";\n    return strcmp(argv[1], password);\n}\n

    Si nous le compilons et cherchons dans son code binaire\u2009:

    $ gcc example.c\n$ hexdump -C a.out | grep -C3 sec\n06f0  f3 c3 00 00 48 83 ec 08  48 83 c4 08 c3 00 00 00 | ....H...H....... |\n0700  01 00 02 00 00 00 00 00  00 00 00 00 00 00 00 00 | ................ |\n0710  75 6e 20 6d 6f 74 20 64  65 20 70 61 73 73 65 20 | un mot de passe  |\n0720  73 65 63 72 65 74 00 00  01 1b 03 3b 3c 00 00 00 | secret.....;<... |\n0730  06 00 00 00 e8 fd ff ff  88 00 00 00 08 fe ff ff | ................ |\n0740  b0 00 00 00 18 fe ff ff  58 00 00 00 22 ff ff ff | ........X...\"... |\n0750  c8 00 00 00 58 ff ff ff  e8 00 00 00 c8 ff ff ff | ....X........... |\n

    Sous un syst\u00e8me POSIX, il n'existe aucune distinction formelle entre un fichier binaire et un fichier texte. En revanche sous Windows, il existe une subtile diff\u00e9rence concernant surtout le caract\u00e8re de fin de ligne. La commande copy a.txt + b.txt c.txt consid\u00e8re des fichiers textes et ajoutera automatiquement une fin de ligne entre chaque partie concat\u00e9n\u00e9e, mais celle-ci copy /b a.bin + b.bin c.bin ne le fera pas.

    "}, {"location": "course-c/25-architecture-and-systems/files/#ouverture-dun-fichier", "title": "Ouverture d'un fichier", "text": "

    Sous POSIX, un programme doit demander au syst\u00e8me d'exploitation l'acc\u00e8s \u00e0 un fichier soit en lecture, soit en \u00e9criture soit les deux. Le syst\u00e8me d'exploitation retourne un descripteur de fichier qui est simplement un entier unique pour le programme.

    #include <fcntl.h>\n#include <stdio.h>\n#include <sys/stat.h>\n\nint main(void)\n{\n    int fd = open(\"toto\", O_RDONLY);\n    printf(\"%d\\n\", fd);\n    getchar();\n}\n

    Lorsque le programme ci-dessus est ex\u00e9cut\u00e9, il va demander l'ouverture du fichier toto en lecture et recevoir un descripteur de fichier fd (file descriptor) positif en cas de succ\u00e8s ou n\u00e9gatif en cas d'erreur.

    Dans l'exemple suivant, on compile, puis ex\u00e9cute en arri\u00e8re-plan le programme qui ne se terminera pas puisqu'il attend un caract\u00e8re d'entr\u00e9e. L'appel au programme ps permet de lister la liste des processus en cours et la recherche de test permet de noter le num\u00e9ro du processus, ici 6690. Dans l'arborescence de fichiers, il est possible d'aller consulter les descripteurs de fichiers ouverts pour le processus concern\u00e9.

    $ gcc test.c -o test && ./test &\n$ ps -u | grep test\nycr       6690  0.0  0.0  10540   556 pts/4    T    11:19   0:00 test\n$ ls /proc/6690/fd\n0  1  2  3\n

    On observe que trois descripteurs de fichiers sont ouverts.

    • 0 pour STDIN
    • 1 pour STDOUT
    • 2 pour STDERR
    • 3 pour le fichier toto ouvert en lecture seule

    La fonction open est en r\u00e9alit\u00e9 un appel syst\u00e8me qui n'est standardis\u00e9 que sous POSIX, c'est-\u00e0-dire que son utilisation n'est pas portable. L'exemple cit\u00e9 est principalement \u00e9voqu\u00e9 pour mieux comprendre le m\u00e9canisme de fond pour l'acc\u00e8s aux fichiers.

    En r\u00e9alit\u00e9 la biblioth\u00e8que standard, respectueuse de C99, dispose d'une fonction fopen pour file open qui offre plus de fonctionnalit\u00e9s. Ouvrir un fichier se r\u00e9sume donc \u00e0

    #include <stdio.h>\n\nint main(void)\n{\n    FILE *fp = fopen(\"toto\", \"r\");\n\n    if (fp == NULL) {\n        return -1; // Error the file cannot be accessed\n    }\n\n    // ...\n}\n

    Le mode d'ouverture du fichier peut \u00eatre\u2009:

    r

    Ouverture en lecture seule depuis le d\u00e9but du fichier.

    r+

    Ouverture pour lecture et \u00e9criture depuis le d\u00e9but du fichier.

    w

    Ouverture en \u00e9criture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0, sinon le contenu est effac\u00e9. Le pointeur de fichier est positionn\u00e9 au d\u00e9but de ce dernier.

    w+

    Ouverture en \u00e9criture et lecture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0. Le pointeur de fichier est positionn\u00e9 au d\u00e9but de ce dernier.

    a

    Ouverture du fichier pour insertion. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0. Le pointeur est positionn\u00e9 \u00e0 la fin du fichier.

    a+

    Ouverture du fichier pour lecture et \u00e9criture. Le fichier est cr\u00e9\u00e9 s'il n'existe pas d\u00e9j\u00e0 et le pointeur du fichier est positionn\u00e9 \u00e0 la fin.

    Sous Windows et pour soucis de compatibilit\u00e9, selon la norme C99, le flag b pour binary existe. Pour ouvrir un fichier en mode binaire, on peut alors \u00e9crire rb+.

    L'ouverture d'un fichier cause, selon le mode, un acc\u00e8s exclusif au fichier. C'est-\u00e0-dire que d'autres programmes ne pourront pas acc\u00e9der \u00e0 ce fichier. Il est donc essentiel de toujours refermer l'acc\u00e8s \u00e0 un fichier d\u00e8s lors que l'op\u00e9ration de lecture ou d'\u00e9criture est termin\u00e9e\u2009:

    flose(fp);\n

    On peut noter que sous POSIX, \u00e9crire sur stdout ou stderr est exactement la m\u00eame chose qu'\u00e9crire sur un fichier, il n'y a aucune distinction.

    Exercice 1\u2009: Num\u00e9ro de ligne

    \u00c9crire un programme qui saisit le nom d'un fichier texte, ainsi qu'un texte \u00e0 rechercher. Le programme affiche ensuite le num\u00e9ro de toutes les lignes du fichier contenant le texte recherch\u00e9.

    $ ./search\nFichier: foo.txt\nRecherche: bulbe\n\n4\n5\n19\n132\n981\n

    Question subsidiaire\u2009: que fait le programme suivant\u2009:

    $ grep foo.txt bulbe\n
    ", "tags": ["STDOUT", "STDERR", "open", "fopen", "stderr", "toto", "stdout", "test", "STDIN"]}, {"location": "course-c/25-architecture-and-systems/files/#navigation-dans-un-fichier", "title": "Navigation dans un fichier", "text": "

    Lorsqu'un fichier est ouvert, un curseur virtuel est positionn\u00e9 soit au d\u00e9but soit \u00e0 la fin du fichier. Lorsque des donn\u00e9es sont lues ou \u00e9crites, c'est \u00e0 la position de ce curseur, lequel peut \u00eatre d\u00e9plac\u00e9 en utilisant plusieurs fonctions utilitaires.

    La navigation dans un fichier n'est possible que si le fichier est seekable. G\u00e9n\u00e9ralement les pointeurs de fichiers stdin, stdout et stderr ne sont pas seekable, et il n'est pas possible de se d\u00e9placer dans le fichier, mais seulement \u00e9crire dedans.

    ", "tags": ["stdin", "stdout", "stderr"]}, {"location": "course-c/25-architecture-and-systems/files/#fseek", "title": "fseek", "text": "

    La fonction fseek permet de d\u00e9placer le curseur dans un fichier ouvert. La signature de la fonction est la suivante\u2009:

    int fseek(FILE *stream, long int offset, int whence)\n

    Le manuel man fseek indique les trois constantes possibles pour whence:

    SEEK_SET

    Positionne le curseur au d\u00e9but du fichier.

    SEEK_CUR

    Position courante du curseur. Permets d'ajouter un offset relatif \u00e0 la position courante.

    SEEK_END

    Positionne le curseur \u00e0 la fin du fichier.

    Si un fichier est seekable, il est possible de se d\u00e9placer dans le fichier. Par exemple, pour lire le dernier caract\u00e8re d'un fichier\u2009:

    #include <stdio.h>\n\nint main(void)\n{\n    FILE *fp = fopen(\"toto\", \"r\");\n\n    if (fp == NULL) return -1;\n\n    fseek(fp, -1, SEEK_END);\n    char c = fgetc\n    printf(\"%c\\n\", c);\n}\n
    ", "tags": ["SEEK_SET", "whence", "fseek", "SEEK_CUR", "SEEK_END"]}, {"location": "course-c/25-architecture-and-systems/files/#ftell", "title": "ftell", "text": "

    Il est parfois utile de savoir o\u00f9 se trouve le curseur. ftell() retourne la position actuelle du curseur dans un fichier ouvert.

    char filename[] = \"foo\";\n\nFILE *fp = fopen(filename, 'r');\nfseek(fp, 0, SEEK_END);\nlong int size = ftell();\n\nprintf(\"The file %s has a size of %ld Bytes\\n\", filename, size);\n
    "}, {"location": "course-c/25-architecture-and-systems/files/#rewind", "title": "rewind", "text": "

    L'appel rewind() est \u00e9quivalent \u00e0 (void) fseek(stream, 0L, SEEK_SET) et permet de se positionner au d\u00e9but du fichier.

    "}, {"location": "course-c/25-architecture-and-systems/files/#lecture-ecriture", "title": "Lecture / \u00c9criture", "text": "

    La lecture, \u00e9criture dans un fichier s'effectue de mani\u00e8re analogue aux fonctions que nous avons d\u00e9j\u00e0 vues printf et scanf pour les flux standards (stdout, stderr), mais en utilisant les variantes pr\u00e9fix\u00e9es de f :

    Fonction Description int fscanf(FILE *stream, const char *format, ...) Lecture formatt\u00e9e int fprintf(FILE *stream, const char *format, ...) \u00c9criture formatt\u00e9e int fgetc(FILE *stream) Lecture d'un caract\u00e8re int fputc(FILE *stream, char char) \u00c9criture d'un caract\u00e8re char *fgets(char * restrict s, int n, FILE * restrict stream) Lecture d'une ligne int fputs(const char * restrict s, FILE * restrict stream) \u00c9criture d'une ligne

    L'utilisation avec stdin et stdout comme descripteur de fichier est possible, mais il est pr\u00e9f\u00e9rable dans ce cas d'utiliser les fonctions scanf et printf qui ont les m\u00eames fonctionnalit\u00e9s.

    Les nouvelles fonctions \u00e0 conna\u00eetre sont les suivantes\u2009:

    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)\n

    Elle permet une lecture arbitraire de nmemb * size bytes depuis le flux stream dans le buffer ptr:

    int32_t buffer[12] = {0};\nfread(buffer, 2, sizeof(int32_t), stdin);\nprintf(\"%x\\n%x\\n\", buffer[0], buffer[1]);\n

    Exemple d'utilisation\u2009:

    $ echo -e \"0123abcdefgh\" | ./a.out\n33323130\n64636261\n

    On notera au passage la nature little-endian du syst\u00e8me.

    La seconde fonction est\u2009:

    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)\n

    La fonction est similaire \u00e0 fread mais pour \u00e9crire sur un flux des donn\u00e9es brutes.

    ", "tags": ["stdin", "ptr", "stream", "printf", "fread", "stdout", "scanf"]}, {"location": "course-c/25-architecture-and-systems/files/#buffer-de-fichier", "title": "Buffer de fichier", "text": "

    Pour am\u00e9liorer les performances, C99 pr\u00e9voit (\u00a77.19.3-3), un espace tampon pour les descripteurs de fichiers qui peuvent \u00eatre\u2009:

    1. unbuffered (_IONBF) : Pas de buffer, les caract\u00e8res lus ou \u00e9crits sont achemin\u00e9s le plus vite possible de la source \u00e0 la destination.
    2. fully buffered (_IOFBF) : Le buffer est rempli \u00e0 chaque lecture ou \u00e9criture, puis vid\u00e9.
    3. line buffered (_IO_LBF) : Le buffer est rempli \u00e0 chaque retour \u00e0 la ligne.

    Il faut comprendre qu'\u00e0 chaque instant un programme souhaite \u00e9crire dans un fichier, il doit g\u00e9n\u00e9rer un appel syst\u00e8me et donc interrompre le noyau. Un programme qui \u00e9crirait caract\u00e8re par caract\u00e8re sur la sortie standard agirait de la m\u00eame mani\u00e8re qu'un employ\u00e9 des postes qui irait distribuer son courrier en ne prenant qu'une enveloppe \u00e0 la fois, de la centrale de distribution au destinataire.

    Par d\u00e9faut, un pointeur de fichier est fully buffered. C'est-\u00e0-dire que dans le cas du programme suivant devrait ex\u00e9cuter 10x l'appel syst\u00e8me write, une fois par caract\u00e8re.

    #include <stdio.h>\n#include <string.h>\n\nint main(int argc, char* argv[])\n{\n    if (argc > 1 && strcmp(\"--no-buffering\", argv[1]) == 0)\n        setvbuf(stdout, NULL, _IONBF, 0);\n\n    for (int i = 0; i < 10; i++)\n        putchar('c');\n}\n

    Cependant le comportement r\u00e9el est diff\u00e9rent. Seulement si le buffer est d\u00e9sactiv\u00e9, que le programme interrompt le noyau pour chaque caract\u00e8re\u2009:

    $ gcc buftest.c -o buftest\n\n$ strace ./buftest 2>&1 | grep write\nwrite(1, \"cccccccccc\", 10cccccccccc)              = 10\n\n$ strace ./buftest --no-buffering 2>&1 | grep write\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\nwrite(1, \"c\", 1c)                        = 1\n

    Le changement de mode peut \u00eatre effectu\u00e9 avec la fonction setbuf ou setvbuf:

    #include <stdio.h>\n\nint main(void) {\n    char buf[1024];\n\n    setbuf(stdout, buf);\n\n    fputs(\"Allo ?\");\n\n    fflush(stdout);\n}\n

    La fonction fflush force l'\u00e9criture malgr\u00e9 l'utilisation d'un buffer.

    ", "tags": ["_IONBF", "unbuffered", "setbuf", "write", "_IO_LBF", "_IOFBF", "setvbuf", "fflush"]}, {"location": "course-c/25-architecture-and-systems/files/#fichiers-et-flux", "title": "Fichiers et Flux", "text": "

    Historiquement les descripteurs de fichiers sont appel\u00e9s FILE alors qu'ils sont pr\u00e9f\u00e9rablement appel\u00e9s streams en C++. Un fichier au m\u00eame titre que stdin, stdout et stderr sont des flux de donn\u00e9es. La norme POSIX, d\u00e9crit que par d\u00e9faut les flux\u2009:

    Flux de donn\u00e9es standards Flux Num\u00e9ro Description stdin 0 Flux d'entr\u00e9e standard stdout 1 Flux de sortie standard stderr 2 Flux d'erreur standard

    Ces trois descripteurs de fichiers sont ouverts au d\u00e9but du programme. Le premier fichier ouvert par exemple avec fopen sera tr\u00e8s probablement assign\u00e9 \u00e0 l'identifiant 3, le suivant \u00e0 4, etc.

    Pour se convaincre de cela, on peut ex\u00e9cuter l'exemple suivant avec le programme strace:

    #include <stdio.h>\n\nint main(void) {\n    char c = fgetc(stdin);\n\n    FILE *fd = fopen(\"file\", \"w\");\n    fputc(c, fd);\n    fputc(c + 1, stdout);\n    fputc(c + 2, stderr);\n}\n

    Pour m\u00e9moire, strace permet de capturer les appels syst\u00e8me du programme pass\u00e9 en argument et de les afficher. Deux particularit\u00e9s de la commande ex\u00e9cut\u00e9e sont 2>&1 qui redirige stderr vers stdout afin de pouvoir rediriger le flux vers grep. Ensuite grep permet de filtrer la sortie pour n'afficher que les lignes contenant open, read, write ou close:

    $ echo k | strace ./a.out 2>&1 | grep -P 'open|read|write|close'\nread(0, \"k\\n\", 4096)                    = 2\nopenat(AT_FDCWD, \"file\", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3\nwrite(2, \"m\", 1m)                        = 1\nwrite(3, \"k\", 1)                        = 1\nwrite(1, \"l\", 1l)                        = 1\n

    On peut voir qu\u2019on lit k\\n sur le flux 0, soit stdin, puis que le fichier file est ouvert, il porte l'identifiant 3, enfin on \u00e9crit sur 1, 2 et 3.

    ", "tags": ["stdin", "FILE", "file", "read", "open", "write", "streams", "stderr", "fopen", "grep", "strace", "close", "stdout"]}, {"location": "course-c/25-architecture-and-systems/files/#formats-de-serialisation", "title": "Formats de s\u00e9rialisation", "text": "

    Souvent les fichiers sont utilis\u00e9s pour stocker de l'information organis\u00e9e en grille, par exemple, la liste des temp\u00e9ratures maximales par ville et par mois\u2009:

    Pays Ville 01 02 03 04 05 06 07 08 09 10 11 12 Suisse Z\u00fcrich 0.3 1.3 5.3 8.8 13.3 16.4 18.6 18.0 14.1 9.9 4.4 1.4 Italie Rome 7.5 8.2 10.2 12.6 17.2 21.1 24.1 24.5 20.8 16.4 11.4 8.4 Allemagne Berlin 0.6 2.3 5.1 10.2 14.8 17.9 20.3 19.7 15.3 10.5 6.0 1.33 Y\u00e9men Aden 25.7 26.0 27.2 28.9 31.0 32.7 32.7 31.5 31.6 28.9 27.1 26.01 Russie Yakutsk -38.6 -33.8 -20.1 -4.8 7.5 16.4 19.5 15.2 6.1 -7.8 -27.0 -37.6

    Il existe plusieurs mani\u00e8res d'\u00e9crire ces informations dans un fichier\u2009:

    • \u00c9criture tabul\u00e9e
    • \u00c9criture avec remplissage
    • Utiliser un langage de s\u00e9rialisation de haut niveau comme JSON, YAML ou XML
    "}, {"location": "course-c/25-architecture-and-systems/files/#format-tabule", "title": "Format tabul\u00e9", "text": "

    Un fichier dit tabul\u00e9, utilise une sentinelle, souvent le caract\u00e8re de tabulation \\t pour s\u00e9parer les donn\u00e9es. Chaque ligne du tableau est physiquement s\u00e9par\u00e9e de la suivante avec un \\n:

    Pays\\tVille\\t01\\t02\\t03\\t04\\t05\\t06\\t07\\t08\\t09\\t10\\n\nCH\\tZ\u00fcrich\\t0.3\\t1.3\\t5.3\\t8.8\\t13.3\\t16.4\\t18.6\\t18.0\\t14.1\\t9.9\\n\nIT\\tRome\\t7.5\\t8.2\\t10.2\\t12.6\\t17.2\\t21.1\\t24.1\\t24.5\\t20.8\\t16.4\\n\nDE\\tBerlin\\t0.6\\t2.3\\t5.1\\t10.2\\t14.8\\t17.9\\t20.3\\t19.7\\t15.3\\t10.5\\n\nYE\\tAden\\t25.7\\t26.0\\t27.2\\t28.9\\t31.0\\t32.7\\t32.7\\t31.5\\t31.6\\t28.9\\n\nRU\\tYakutsk\\t-38.6\\t-33.8\\t-20.1\\t-4.8\\t7.5\\t16.4\\t19.5\\t15.2\\t6.1\\t-7.8\\n\n

    Ce fichier peut \u00eatre observ\u00e9 avec un lecteur hexad\u00e9cimal\u2009:

    $ hexdump -C data.dat\n0000  50 61 79 73 09 56 69 6c  6c 65 09 30 31 09 30 32 |Pays.Ville.01.02|\n0010  09 30 33 09 30 34 09 30  35 09 30 36 09 30 37 09 |.03.04.05.06.07.|\n0020  30 38 09 30 39 09 31 30  09 31 31 09 31 32 0a 53 |08.09.10.11.12.S|\n0030  75 69 73 73 65 09 5a c3  bc 72 69 63 68 09 30 2e |uisse.Z..rich.0.|\n0040  33 09 31 2e 33 09 35 2e  33 09 38 2e 38 09 31 33 |3.1.3.5.3.8.8.13|\n0050  2e 33 09 31 36 2e 34 09  31 38 2e 36 09 31 38 2e |.3.16.4.18.6.18.|\n0060  30 09 31 34 2e 31 09 39  2e 39 09 34 2e 34 09 31 |0.14.1.9.9.4.4.1|\n0070  2e 34 0a 49 74 61 6c 69  65 09 52 6f 6d 65 09 37 |.4.Italie.Rome.7|\n0080  2e 35 09 38 2e 32 09 31  30 2e 32 09 31 32 2e 36 |.5.8.2.10.2.12.6|\n0090  09 31 37 2e 32 09 32 31  2e 31 09 32 34 2e 31 09 |.17.2.21.1.24.1.|\n00a0  32 34 2e 35 09 32 30 2e  38 09 31 36 2e 34 09 31 |24.5.20.8.16.4.1|\n00b0  31 2e 34 09 38 2e 34 0a  41 6c 6c 65 6d 61 67 6e |1.4.8.4.Allemagn|\n00c0  65 09 42 65 72 6c 69 6e  09 30 2e 36 09 32 2e 33 |e.Berlin.0.6.2.3|\n00d0  09 35 2e 31 09 31 30 2e  32 09 31 34 2e 38 09 31 |.5.1.10.2.14.8.1|\n00e0  37 2e 39 09 32 30 2e 33  09 31 39 2e 37 09 31 35 |7.9.20.3.19.7.15|\n00f0  2e 33 09 31 30 2e 35 09  36 2e 30 09 31 2e 33 33 |.3.10.5.6.0.1.33|\n0100  0a 59 c3 a9 6d 65 6e 09  41 64 65 6e 09 32 35 2e |.Y..men.Aden.25.|\n0110  37 09 32 36 2e 30 09 32  37 2e 32 09 32 38 2e 39 |7.26.0.27.2.28.9|\n0120  09 33 31 2e 30 09 33 32  2e 37 09 33 32 2e 37 09 |.31.0.32.7.32.7.|\n0130  33 31 2e 35 09 33 31 2e  36 09 32 38 2e 39 09 32 |31.5.31.6.28.9.2|\n0140  37 2e 31 09 32 36 2e 30  31 0a 52 75 73 73 69 65 |7.1.26.01.Russie|\n0150  09 59 61 6b 75 74 73 6b  09 2d 33 38 2e 36 09 2d |.Yakutsk.-38.6.-|\n0160  33 33 2e 38 09 2d 32 30  2e 31 09 2d 34 2e 38 09 |33.8.-20.1.-4.8.|\n0170  37 2e 35 09 31 36 2e 34  09 31 39 2e 35 09 31 35 |7.5.16.4.19.5.15|\n0180  2e 32 09 36 2e 31 09 2d  37 2e 38 09 2d 32 37 2e |.2.6.1.-7.8.-27.|\n0190  30 09 2d 33 37 2e 36 0a                          |0.-37.6.|\n0198\n

    L'inconv\u00e9nient de ce format est que pour obtenir directement la temp\u00e9rature du mois de mars \u00e0 Berlin, sachant que Berlin est la quatri\u00e8me ligne du fichier, il est n\u00e9cessaire de parcourir le fichier depuis le d\u00e9but, car la longueur des lignes n'est \u00e0 priori pas connue. On dit que la lecture s\u00e9quentielle est facilit\u00e9e, mais la lecture al\u00e9atoire est plus lente.

    "}, {"location": "course-c/25-architecture-and-systems/files/#format-avec-remplissage", "title": "Format avec remplissage", "text": "

    Pour pallier au d\u00e9faut du format tabul\u00e9, il est possible d'\u00e9crire le fichier en utilisant un caract\u00e8re de remplissage. Dans le fichier suivant, les mois de mai sont toujours align\u00e9s avec la 48e colonne\u2009:

     000000000011111111112222222222333333333344444444445555555555666666666\n 012345678901234567890123456789012345678901234567890123456789012345678\n+---------+-------+-----+-----+-----+----+----+----+----+----+----+--->\n\nPays      Ville   01    02    03    04   05   06   07   08   09   10\nSuisse    Z\u00fcrich  0.3   1.3   5.3   8.8  13.3 16.4 18.6 18.0 14.1 9.9\nItalie    Rome    7.5   8.2   10.2  12.6 17.2 21.1 24.1 24.5 20.8 16.4\nAllemagne Berlin  0.6   2.3   5.1   10.2 14.8 17.9 20.3 19.7 15.3 10.5\nY\u00e9men     Aden    25.7  26.0  27.2  28.9 31.0 32.7 32.7 31.5 31.6 28.9\nRussie    Yakutsk -38.6 -33.8 -20.1 -4.8 7.5  16.4 19.5 15.2 6.1  -7.8\n

    Id\u00e9alement on utilise comme caract\u00e8re de remplissage le caract\u00e8re nul \\0, mais le caract\u00e8re espace peut aussi convenir \u00e0 condition que les donn\u00e9es ne contiennent pas d'espace.

    La lecture al\u00e9atoire de ce type de fichier est facilit\u00e9e, car la position de chaque entr\u00e9e est connue \u00e0 l'avance, on sait par exemple que le pays est stock\u00e9 sur 11 caract\u00e8res, la ville sur 9 caract\u00e8res et chaque temp\u00e9rature sur 7 caract\u00e8res.

    L'utilisation de fseek est par cons\u00e9quent utile\u2009:

    int line = 2;\nint month = 3;\ndouble temperature;\n\nfseek(fd, line * (11 + 9 + 12 * 7 + 1), SEEK_SET);\nfseek(fd, 11 + 9 + month * 7 SEEK_CUR);\nfscanf(fd, \"%lf\", &temperature);\n

    L'inconv\u00e9nient de ce format de fichier est la place qu'il prend en m\u00e9moire. L'autre probl\u00e8me est que si le nom d'une ville d\u00e9passe les 9 caract\u00e8res allou\u00e9s, il faut r\u00e9\u00e9crire tout le fichier. G\u00e9n\u00e9ralement ce probl\u00e8me est contourn\u00e9 en allouant des champs d'une taille suffisante, par exemple 256 caract\u00e8res pour le nom des villes.

    ", "tags": ["fseek"]}, {"location": "course-c/25-architecture-and-systems/files/#format-serialise", "title": "Format s\u00e9rialis\u00e9", "text": "

    Des langages de s\u00e9rialisation permettent de structurer de l'information en utilisant un format sp\u00e9cifique. Ici JSON :

    [{ \"pays\": \"Suisse\",\n   \"ville\": \"Z\u00fcrich\",\n   \"mois\": {\n     \"janvier\": 0.3, \"f\u00e9vrier\": 1.3, \"mars\": 5.3, \"avril\": 8.8,\n     \"mai\": 13.3, \"juin\": 16.4, \"juillet\": 18.6, \"ao\u00fbt\": 18.0,\n     \"septembre\": 14.1, \"octobre\": 9.9, \"novembre\": 4.4, \"d\u00e9cembre\": 1.4}\n }, {\n   \"pays\": \"Italie\",\n   \"ville\": \"Rome\",\n   \"mois\": {\n     \"janvier\": 7.5, \"f\u00e9vrier\": 8.2, \"mars\": 10.2, \"avril\": 12.6,\n     \"mai\": 17.2, \"juin\": 21.1, \"juillet\": 24.1, \"ao\u00fbt\": 24.5,\n     \"septembre\": 20.8, \"octobre\": 16.4, \"novembre\": 11.4, \"d\u00e9cembre\": 8.4}\n}]\n

    L'avantage de ce type de format est qu'il est facilement modifiable avec un \u00e9diteur de texte et qu'il est tr\u00e8s interop\u00e9rable. C'est-\u00e0-dire qu'il est facilement lisible depuis diff\u00e9rents langages de programmation.

    En C, on pourra utiliser la biblioth\u00e8que logicielle json-c.

    "}, {"location": "course-c/25-architecture-and-systems/files/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 2\u2009: Variantes

    Consid\u00e9rez les deux programmes ci-dessous tr\u00e8s similaires.

    #include <stdio.h>\n\nint main(void)\n{\n    char texte[80];\n\n    printf(\"Saisir un texte:\");\n    gets(texte);\n    printf(\"Texte: %s\\n\", texte);\n}\n
    #include <stdio.h>\n\nint main(void)\n{\n    char texte[80];\n\n    printf(\"Saisir un texte:\");\n    fgets(texte, 80, stdin);\n    printf(\"Texte: %s\\n\", texte);\n}\n
    1. Quelle est la diff\u00e9rence entre ces 2 programmes\u2009?
    2. Dans quel cas est-ce que ces programmes auront un comportement diff\u00e9rent\u2009?
    3. Quelle serait la meilleure solution\u2009?
    "}, {"location": "course-c/25-architecture-and-systems/mcu/", "title": "Syst\u00e8mes \u00e0 microcontr\u00f4leurs", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#introduction", "title": "Introduction", "text": ""}, {"location": "course-c/25-architecture-and-systems/mcu/#les-microcontroleurs", "title": "Les microcontr\u00f4leurs", "text": "

    Un microcontr\u00f4leur est un ordinateur sur une puce. Il est compos\u00e9 d'un processeur, de m\u00e9moire et de p\u00e9riph\u00e9riques d'entr\u00e9e/sortie. Les microcontr\u00f4leurs sont utilis\u00e9s dans de nombreux syst\u00e8mes embarqu\u00e9s, tels que les t\u00e9l\u00e9commandes, les jouets, les appareils \u00e9lectrom\u00e9nagers, les instruments de mesure, les syst\u00e8mes de contr\u00f4le de moteurs etc. Nous avons d\u00e9j\u00e0 \u00e9voqu\u00e9 la machine \u00e0 caf\u00e9 en d\u00e9but de cours qui est un bon exemple de syst\u00e8me embarqu\u00e9.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#style-de-programmation", "title": "Style de programmation", "text": "

    D'ordinaire les petits microcontr\u00f4leurs sont programm\u00e9s en langage C, n\u00e9anmoins certaines possibilit\u00e9s du langage sont g\u00e9n\u00e9ralement prohib\u00e9es. Par exemple, les pointeurs de fonction sont rarement utilis\u00e9s, les fonctions r\u00e9cursives sont \u00e0 proscrire, les allocations dynamiques de m\u00e9moire sont interdites, etc. Les microcontr\u00f4leurs ont des ressources limit\u00e9es et le programmeur doit en tenir compte.

    L'allocation dynamique est interdite car elle peut entra\u00eener des fuites de m\u00e9moire ou de la fragmentation.

    L'ex\u00e9cution du programme est tr\u00e8s souvent dite bare-metal, c'est-\u00e0-dire sans syst\u00e8me d'exploitation. Le programme est ex\u00e9cut\u00e9 directement sur le microcontr\u00f4leur sans aucune couche interm\u00e9diaire. Cela permet d'avoir un contr\u00f4le total sur le mat\u00e9riel. Mais cela rend le programme non pr\u00e9emptif, c'est-\u00e0-dire qu'il n'y a pas de gestionnaire de t\u00e2ches qui peut interrompre le programme en cours d'ex\u00e9cution.

    Prenons l'exemple d'une montre \u00e0 aiguille \u00e9quip\u00e9e d'un microcontr\u00f4leur \u00e0 ultra basse consommation comme le Epson S1C17. Il ne serait pas raisonnable d'utiliser des boucles d'attentes actives (c'est-\u00e0-dire de compter un cerain nombre d'instructions correspondant \u00e0 une seconde), au lieu de cela, il est pr\u00e9f\u00e9rable d'utiliser un timer pour r\u00e9veiller le microcontr\u00f4leur toutes les secondes. Ce timer est un p\u00e9riph\u00e9rique mat\u00e9riel qui g\u00e9n\u00e8re une interruption toutes les secondes. L'interruption, via une table des vecteurs d'interruption, appelle une fonction qui met \u00e0 jour l'affichage de la montre. Aussi, c'est principalement dans cette fonction qu'aura lieu la majorit\u00e9 du programme, et il n'est pas rare d'avoir un programme de cette forme\u2009:

    void timer_isr() {\n    update_display();\n    sleep(); // Suspend l'ex\u00e9cution du programme jusqu'\u00e0 une interruption\n}\n\nint main() {\n    init_device();\n    init_timer(timer_isr);\n    enable_low_power_mode();\n    for(;;) {} // Boucle infinie\n}\n
    "}, {"location": "course-c/25-architecture-and-systems/mcu/#interruptions", "title": "Interruptions", "text": "

    Dans un syst\u00e8me embarqu\u00e9 ou un microcontr\u00f4leur, une interruption est un m\u00e9canisme qui permet \u00e0 un processeur de suspendre temporairement l\u2019ex\u00e9cution du programme en cours afin de r\u00e9pondre \u00e0 un \u00e9v\u00e9nement particulier, souvent externe ou d'une urgence particuli\u00e8re. En d'autres termes, l'interruption interrompt le flux normal d'instructions pour traiter un \u00e9v\u00e9nement prioritaire, comme si quelqu\u2019un frappait \u00e0 la porte pendant que vous lisez un livre captivant. Il faut alors poser le livre pour ouvrir la porte.

    Voici comment cela se passe, \u00e9tape par \u00e9tape, de mani\u00e8re plus technique\u2009:

    1. D\u00e9tection de l'\u00e9v\u00e9nement : Une interruption est d\u00e9clench\u00e9e par un \u00e9v\u00e9nement sp\u00e9cifique. Cela peut \u00eatre, par exemple, l'appui sur un bouton, l'arriv\u00e9e d'une donn\u00e9e par un port s\u00e9rie, ou encore une alarme temporelle. Ces \u00e9v\u00e9nements sont surveill\u00e9s en arri\u00e8re-plan, pendant que le microcontr\u00f4leur poursuit normalement l'ex\u00e9cution de son programme, c'est \u00e0 dire ex\u00e9cuter les instructions les unes apr\u00e8s les autres.

    2. Suspension du programme principal : Lorsqu'une interruption est d\u00e9tect\u00e9e, le microcontr\u00f4leur interrompt son programme principal. Il m\u00e9morise l'endroit pr\u00e9cis o\u00f9 il s\u2019est arr\u00eat\u00e9 (l\u2019adresse de l\u2019instruction suivante) dans une pile (ou stack). Cela permet de reprendre exactement l\u00e0 o\u00f9 il s'\u00e9tait arr\u00eat\u00e9 une fois l\u2019interruption g\u00e9r\u00e9e.

    3. Ex\u00e9cution de la routine d\u2019interruption : Apr\u00e8s avoir suspendu l\u2019ex\u00e9cution du programme principal, le microcontr\u00f4leur saute vers une fonction sp\u00e9ciale appel\u00e9e Routine de Service d\u2019Interruption ou ISR (Interrupt Service Routine). Cette routine est un petit programme d\u00e9di\u00e9 qui est con\u00e7u pour traiter l\u2019\u00e9v\u00e9nement en question. Par exemple, si un bouton a \u00e9t\u00e9 press\u00e9, l\u2019ISR peut augmenter un compteur ou effectuer une autre action sp\u00e9cifique.

    4. Reprise du programme principal : Une fois l\u2019interruption trait\u00e9e, le microcontr\u00f4leur restaure son \u00e9tat pr\u00e9c\u00e9dent gr\u00e2ce aux informations stock\u00e9es dans la pile. Il reprend alors l\u2019ex\u00e9cution du programme principal exactement l\u00e0 o\u00f9 il s'\u00e9tait arr\u00eat\u00e9, comme si de rien n'\u00e9tait.

    ", "tags": ["interruption"]}, {"location": "course-c/25-architecture-and-systems/mcu/#pourquoi-est-ce-si-utile", "title": "Pourquoi est-ce si utile\u2009?", "text": "

    Les interruptions permettent au microcontr\u00f4leur de r\u00e9agir imm\u00e9diatement \u00e0 des \u00e9v\u00e9nements ext\u00e9rieurs sans avoir \u00e0 constamment v\u00e9rifier leur occurrence (ce que l\u2019on appelle le polling, un processus inefficace et gourmand en ressources). Gr\u00e2ce aux interruptions, un microcontr\u00f4leur peut effectuer plusieurs t\u00e2ches de mani\u00e8re efficace sans n\u00e9gliger aucun \u00e9v\u00e9nement important.

    Imaginons que vous devez surveiller en permanence si la pizza est cuite. Si l'algorithme est de faire pause apr\u00e8s avoir d\u00e9gomm\u00e9 5 zombies \u00e0 cet addictif jeu pour allez prendre connaissance de la cuisson, ce n'est pas efficace. Au contraire, en r\u00e9glant un compte \u00e0 rebours (timer) sur 10 minutes, il suffit de se laisser interrompre apr\u00e8s le d\u00e9compte...

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#les-types-dinterruptions", "title": "Les types d'interruptions", "text": "

    Il existe g\u00e9n\u00e9ralement deux types d'interruptions\u2009:

    Interruptions mat\u00e9rielles

    Elles sont g\u00e9n\u00e9r\u00e9es par des \u00e9v\u00e9nements ext\u00e9rieurs au processeur, tels qu'un signal provenant d'un capteur, un bouton poussoir, ou une transmission de donn\u00e9es par un p\u00e9riph\u00e9rique. Dans un ordinateur personnel, les interruptions mat\u00e9rielles peuvent \u00eatre g\u00e9n\u00e9r\u00e9es par des p\u00e9riph\u00e9riques tels que le clavier, la souris, le disque dur, etc.

    Interruptions logicielles

    Ces interruptions sont g\u00e9n\u00e9r\u00e9es par le programme lui-m\u00eame, souvent pour organiser des t\u00e2ches complexes ou pour indiquer qu\u2019une certaine t\u00e2che est termin\u00e9e. En C ABI on peut g\u00e9n\u00e9rer une interruption logicielle en utilisant l\u2019instruction raise(SIGINT) par exemple.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#un-cout", "title": "Un co\u00fbt", "text": "

    Les interruptions ne sont pas gratuites. Elles ont un co\u00fbt en termes de temps de traitement et de ressources. Lorsqu'une interruption se produit, le microcontr\u00f4leur doit\u2009:

    1. sauver le contexte du processeur (tous les registres utilis\u00e9s par C et l'\u00e9tat du programme) ;
    2. sauver l'adresse de l'instruction suivante \u00e0 ex\u00e9cuter\u2009;
    3. vider les pipelines d'instructions\u2009;
    4. ex\u00e9cuter la routine d'interruption\u2009;
    5. vider les pipelines d'instructions \u00e0 nouveau\u2009;
    6. restaurer le contexte du processeur\u2009;
    7. reprendre l'ex\u00e9cution du programme principal.

    Il est de coutume de dire qu'une interruption doit \u00eatre la plus courte possible, mais une interruption d'une seule ligne de code en co\u00fbte d\u00e9j\u00e0 plusieurs dizaines.

    Dans un contexte de microncontr\u00f4leur il n'est pas rare de devoir d\u00e9sactiver les interruptions pendant certaines op\u00e9rations critiques, comme la modification d'une structure de donn\u00e9es partag\u00e9e entre l'ISR et le programme principal. Cela \u00e9vite les probl\u00e8mes de concurrence et de corruption de donn\u00e9es. Il n'est pas rare non plus d'ex\u00e9cuter l'int\u00e9gralit\u00e9 du programme dans une ISR.

    Voici un exemple de code pour une montre \u00e0 pile\u2009:

    struct clock {\n    int hours;\n    int minutes;\n    int seconds;\n} clock;\n\ninterrupt void timer_isr() {\n    update_display(clock);\n    if (++clock.seconds == 60) {\n        clock.seconds = 0;\n        if (++clock.minutes == 60) {\n            clock.minutes = 0;\n            if (++clock.hours == 24)\n                clock.hours = 0;\n        }\n    }\n    sleep();\n}\n\nint main() {\n    init_device();\n    init_timer(timer_isr, 1'000'000 /* us */);\n    enable_low_power_mode();\n    sleep(); // Suspend l'ex\u00e9cution du programme jusqu'\u00e0 une interruption\n    for(;;) {} // Boucle infinie\n}\n

    On observe d'une part l'utilisation d'un contexte global (variable globale) pour stocker l'heure, et d'autre part l'ex\u00e9cution de l'int\u00e9gralit\u00e9 du programme dans l'ISR. Cela est possible car le programme est tr\u00e8s simple et ne n\u00e9cessite pas de traitement en dehors de l'ISR.

    La variable globale est ici in\u00e9vitable car il n'est g\u00e9n\u00e9ralement pas possible de passer des arguments \u00e0 une ISR.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#conclusion", "title": "Conclusion", "text": "

    Les interruptions sont une cl\u00e9 de vo\u00fbte des syst\u00e8mes embarqu\u00e9s modernes, leur permettant de traiter des \u00e9v\u00e9nements en temps r\u00e9el sans sacrifier les performances. Elles constituent une forme de gestion multit\u00e2che simplifi\u00e9e, car elles donnent au microcontr\u00f4leur la capacit\u00e9 de suspendre son travail pour traiter un \u00e9v\u00e9nement impr\u00e9vu, puis de reprendre l\u00e0 o\u00f9 il s'\u00e9tait arr\u00eat\u00e9, un peu comme un pianiste capable d\u2019interrompre son morceau pour r\u00e9pondre \u00e0 une note inattendue, avant de reprendre son jeu avec la m\u00eame fluidit\u00e9.

    Qu'il s'agisse de microcontr\u00f4leur, de syst\u00e8me embarqu\u00e9 ou d'ordinateur personnel, les interruptions sont centrales pour garantir une r\u00e9activit\u00e9 et une efficacit\u00e9 maximales.

    Si je devais conclure sur une note litt\u00e9raire\u2009: les interruptions sont comme ces moments o\u00f9 la r\u00e9alit\u00e9 frappe \u00e0 la porte de la contemplation, exigeant une r\u00e9ponse imm\u00e9diate et pressante, avant que la marche des choses ne reprenne son cours, inexorablement.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#timers", "title": "Timers", "text": "

    Un timer est un compteur interne au microcontr\u00f4leur qui s\u2019incr\u00e9mente de mani\u00e8re r\u00e9guli\u00e8re, en fonction d\u2019un signal d\u2019horloge. Ce signal d\u2019horloge provient souvent d\u2019un oscillateur externe, comme un quartz. Les timers sont utilis\u00e9s pour mesurer le temps qui s\u2019\u00e9coule, g\u00e9n\u00e9rer des intervalles pr\u00e9cis ou d\u00e9clencher des \u00e9v\u00e9nements \u00e0 des moments d\u00e9finis. En ce sens, ils sont un peu comme des horloges invisibles, comptant patiemment les cycles de l\u2019oscillateur pour d\u00e9clencher une action lorsque le moment est venu.

    Dans un microcontr\u00f4leur, les oscillateurs ou quartz fournissent la cadence \u00e0 laquelle toutes les op\u00e9rations internes se d\u00e9roulent. Ce sont eux qui dictent la fr\u00e9quence \u00e0 laquelle le microcontr\u00f4leur ex\u00e9cute ses instructions. Par exemple, un quartz de 16 MHz signifie que le microcontr\u00f4leur ex\u00e9cute des cycles d'horloge \u00e0 la fr\u00e9quence de 16 millions de fois par seconde.

    En pratique cet oscillateur passe par une PLL (Phase-Locked Loop) qui permet de multiplier la fr\u00e9quence de l'oscillateur pour obtenir une fr\u00e9quence plus \u00e9lev\u00e9e. Un microcontr\u00f4leur avec un oscillateur de 20 MHz pourrait tr\u00e8s bien \u00eatre cadenc\u00e9 \u00e0 400 MHz.

    Ceci \u00e9tant, pour mesurer des intervalles de temps plus longs (comme une seconde, une milliseconde, etc.), un microcontr\u00f4leur a besoin de compter un certain nombre de ces cycles d'horloge. Le timer est donc ce m\u00e9canisme interne qui s'incr\u00e9mente \u00e0 chaque cycle d'horloge, et il peut \u00eatre configur\u00e9 pour d\u00e9clencher une action apr\u00e8s un certain nombre de cycles.

    Imaginons que vous vouliez mesurer une seconde avec un microcontr\u00f4leur cadenc\u00e9 \u00e0 100 MHz. Une seconde repr\u00e9sente 100 millions de cycles. Un timer configur\u00e9 pour s\u2019incr\u00e9menter \u00e0 chaque cycle d\u2019horloge pourra compter jusqu\u2019\u00e0 100 millions, et une fois arriv\u00e9 \u00e0 ce chiffre, il d\u00e9clenchera une action, par exemple une interruption, pour signaler que l\u2019intervalle de temps est \u00e9coul\u00e9.

    Cependant, atteindre un tel compte d\u2019un seul coup n\u2019est pas toujours pratique. C'est pourquoi les microcontr\u00f4leurs utilisent souvent des prescalers, qui divisent la fr\u00e9quence de l\u2019horloge pour permettre au timer de fonctionner \u00e0 des fr\u00e9quences plus basses. Par exemple, avec un prescaler de 8, l'horloge d'entr\u00e9e pour le timer sera r\u00e9duite \u00e0 12.5 MHz (100 MHz / 8), ce qui permet de compter plus lentement et donc plus pr\u00e9cis\u00e9ment pour des intervalles de temps plus longs. N\u00e9anmoins, pour obtenir une seconde cela reste compliqu\u00e9.

    Il est un cas particulier ou on utilise des quartz \u00e0 la fr\u00e9quence singuli\u00e8re de 32.768 kHz. Les modules RTC (Real-Time Clock, ou horloge temps r\u00e9el) parfois int\u00e9gr\u00e9s \u00e0 un microcontr\u00f4leur permettent de calculer automatiquement l'heure, la date, l'exquinoxe... sans passer par le CPU, et donc sans le r\u00e9veiller s'il dort. Cette fr\u00e9quence de 32.768 kHz est une fr\u00e9quence qui, une fois divis\u00e9e par \\(2^15\\) (32'768 = \\(2^15\\)), donne pr\u00e9cis\u00e9ment 1 Hz, soit un cycle par seconde. Cela signifie qu\u2019un timer qui fonctionne avec un quartz de 32.768 kHz et qui divise son signal d\u2019horloge par 32 768 peut fournir un tic par seconde, ce qui est id\u00e9al pour maintenir le temps avec une grande pr\u00e9cision.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#fonctionnement", "title": "Fonctionnement", "text": "

    Le timer est un p\u00e9riph\u00e9rique souvent ind\u00e9pendant du processeur principal, qui poss\u00e8de des registres de configuration, et des lignes d'interruptions.

    Le timer est aliment\u00e9 par le signal d'horloge du microcontr\u00f4leur (souvent apr\u00e8s un passage par un prescaler, qui divise la fr\u00e9quence). \u00c0 chaque cycle de l'horloge, le compteur interne du timer s\u2019incr\u00e9mente. Un Seuil de comparaison peut \u00eatre configur\u00e9 pour d\u00e9clencher une action lorsqu\u2019il atteint une valeur sp\u00e9cifique (ce qu\u2019on appelle un match). Par exemple, on peut configurer un timer pour qu\u2019il d\u00e9clenche une interruption toutes les 1000 millisecondes (1 seconde), ou apr\u00e8s un certain nombre de cycles d'horloge.Lorsque le timer atteint cette valeur pr\u00e9d\u00e9finie, il d\u00e9clenche soit une interruption, soit une autre action d\u00e9finie (mise \u00e0 jour d'une variable, allumage d'un LED, etc.).

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#cas-dutilisation", "title": "Cas d'utilisation", "text": "

    Les timers dans un microcontr\u00f4leur sont utilis\u00e9s pour une vari\u00e9t\u00e9 d\u2019applications\u2009:

    G\u00e9n\u00e9rer des signaux p\u00e9riodiques

    Les timers peuvent produire des signaux r\u00e9guliers, utiles pour des t\u00e2ches comme la g\u00e9n\u00e9ration de PWM (Pulse Width Modulation) pour contr\u00f4ler la vitesse d\u2019un moteur ou la luminosit\u00e9 d'une LED.

    Mesurer des intervalles de temps

    Ils peuvent mesurer des d\u00e9lais pr\u00e9cis, essentiels pour des protocoles de communication comme UART ou I2C.

    Cr\u00e9er des horloges temps r\u00e9el

    Comme \u00e9voqu\u00e9 avec les quartz de 32.768 kHz, les timers peuvent \u00eatre utilis\u00e9s pour maintenir le temps, m\u00eame lorsque le reste du microcontr\u00f4leur est inactif.

    D\u00e9clencher des \u00e9v\u00e9nements asynchrones

    Par exemple, tu peux demander \u00e0 un timer de g\u00e9n\u00e9rer une interruption toutes les millisecondes pour effectuer une mise \u00e0 jour d\u2019affichage ou une action r\u00e9currente dans ton programme.

    "}, {"location": "course-c/25-architecture-and-systems/mcu/#ports", "title": "Ports", "text": "

    Un port sur un microcontr\u00f4leur repr\u00e9sente une interface de communication physique entre le monde ext\u00e9rieur (les p\u00e9riph\u00e9riques, les capteurs, les actionneurs, etc.) et le microcontr\u00f4leur lui-m\u00eame. En termes simples, il s\u2019agit d\u2019un ensemble de broches ou de pins sur le microcontr\u00f4leur, que l\u2019on peut configurer pour envoyer ou recevoir des signaux \u00e9lectriques. Ces signaux, souvent des tensions logiques (0 ou 1, basse ou haute tension), permettent au microcontr\u00f4leur d\u2019interagir avec l\u2019ext\u00e9rieur.

    Un port est souvent group\u00e9 en plusieurs broches, g\u00e9n\u00e9ralement 8 (on parle alors d\u2019un port sur 8 bits, mais il peut aussi y en avoir 16 ou d'autres configurations). Chaque broche d\u2019un port peut \u00eatre contr\u00f4l\u00e9e ind\u00e9pendamment pour \u00eatre configur\u00e9e en entr\u00e9e ou en sortie.

    Chaque broche d\u2019un port peut g\u00e9n\u00e9ralement \u00eatre configur\u00e9e en deux modes principaux\u2009: entr\u00e9e ou sortie. On parle alors d'un GPIO (General Purpose Input/Output) pour une broche qui peut \u00eatre configur\u00e9e en entr\u00e9e ou en sortie.

    Dans le microcontr\u00f4leur, les ports sont contr\u00f4l\u00e9s par des registres (variables internes \u00e0 l\u2019architecture). On trouve g\u00e9n\u00e9ralement trois types de registres associ\u00e9s \u00e0 chaque port\u2009:

    Registre de direction

    Ce registre permet de configurer chaque broche d\u2019un port en mode entr\u00e9e ou sortie.

    Registre de donn\u00e9es

    Ce registre contient les donn\u00e9es que tu envoies ou lis sur les broches. Si la broche est en mode sortie, ce registre d\u00e9termine si tu envoies un signal haut ou bas. Si la broche est en mode entr\u00e9e, il lit la valeur du signal re\u00e7u.

    Registre d'\u00e9tat

    Ce registre permet de lire l'\u00e9tat actuel des broches configur\u00e9es en mode entr\u00e9e. Si tu as un bouton ou un capteur connect\u00e9 \u00e0 une broche d\u2019entr\u00e9e, tu pourras v\u00e9rifier son \u00e9tat via ce registre.

    Souvent, il n'est pas possible de modifier l'\u00e9tat d'un bit d'un port de mani\u00e8re ind\u00e9pendante. On aura alors recours \u00e0 l'alg\u00e8bre de Boole pour modifier un bit sans modifier les autres.

    // Mettre \u00e0 1 le bit 2 du port B\nPORTB |= (1 << 2);\n\n// Mettre \u00e0 0 le bit 2 du port B\nPORTB &= ~(1 << 2);\n\n// Inverser le bit 2 du port B\nPORTB ^= (1 << 2);\n
    "}, {"location": "course-c/25-architecture-and-systems/memory-management/", "title": "Gestion de la M\u00e9moire", "text": "

    Vous l'avez sans doute constat\u00e9 \u00e0 vos d\u00e9pens\u2009: l'erreur Segmentation fault (ou erreur de segmentation) est un fl\u00e9au fr\u00e9quent lors du d\u00e9veloppement. Ce chapitre se propose de plonger dans les arcanes de la gestion de la m\u00e9moire, en vulgarisant les concepts de segmentation et en d\u00e9taillant le m\u00e9canisme d'allocation dynamique.

    Sur un ordinateur, un programme ne peut s'arroger toute la m\u00e9moire disponible. Cette ressource pr\u00e9cieuse est partag\u00e9e entre plusieurs programmes ainsi que le syst\u00e8me d'exploitation, qui se charge de l'allouer de mani\u00e8re judicieuse \u00e0 chaque programme tout en veillant \u00e0 ce que ces derniers ne se chevauchent pas. On pourrait comparer ce processus \u00e0 un service cadastral charg\u00e9 de distribuer \u00e9quitablement des parcelles de m\u00e9moire\u2009: chaque programme dispose de son propre espace, strictement d\u00e9limit\u00e9.

    Cet espace m\u00e9moire allou\u00e9 \u00e0 un programme est structur\u00e9 en diff\u00e9rents segments, chacun d\u00e9di\u00e9 \u00e0 une fonction sp\u00e9cifique. Les principaux segments sont donn\u00e9s \u00e0 la section suivante.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#segments-de-memoire", "title": "Segments de m\u00e9moire", "text": ""}, {"location": "course-c/25-architecture-and-systems/memory-management/#text-ou-code", "title": ".text ou .code", "text": "

    Le Segment de code contient les instructions du programme ex\u00e9cutable, autrement dit le code machine. Ce segment est en lecture seule, ce qui signifie qu'un programme ne peut pas modifier son propre code durant son ex\u00e9cution, garantissant ainsi l'int\u00e9grit\u00e9 du code.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#rodata", "title": ".rodata", "text": "

    Le Segment de donn\u00e9es en lecture seule (Read-Only Data) est d\u00e9di\u00e9 aux constantes globales et aux cha\u00eenes de caract\u00e8res d\u00e9finies comme litt\u00e9rales dans le code source. Par exemple, une constante globale telle que const int = 13 ou une cha\u00eene de caract\u00e8res litt\u00e9rale comme \"abc\" sont stock\u00e9es dans ce segment. Ces donn\u00e9es, une fois d\u00e9finies, ne peuvent \u00eatre modifi\u00e9es en cours d'ex\u00e9cution.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#bss", "title": ".bss", "text": "

    Le Segment BSS (Block Started by Symbol) est une section en m\u00e9moire r\u00e9serv\u00e9e aux variables globales et statiques qui n'ont pas \u00e9t\u00e9 initialis\u00e9es explicitement. Historiquement, le terme \u00ab\u2009BSS\u2009\u00bb provient d'une directive utilis\u00e9e dans les premiers assembleurs d'IBM pour r\u00e9server un bloc de m\u00e9moire sans y affecter de valeur initiale. Par exemple, si vous d\u00e9clarez un tableau global int x[1024]; sans lui assigner de valeurs, il sera plac\u00e9 dans le segment .bss. Le syst\u00e8me d'exploitation se charge de l'initialiser \u00e0 z\u00e9ro lors du lancement du programme, assurant ainsi une base m\u00e9moire propre.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#data", "title": ".data", "text": "

    Le Segment de donn\u00e9es initialis\u00e9es stocke les variables globales et statiques qui ont \u00e9t\u00e9 initialis\u00e9es avec une valeur sp\u00e9cifique dans le code source. Contrairement au segment .bss, qui est r\u00e9serv\u00e9 aux variables non initialis\u00e9es, le segment .data contient des donn\u00e9es explicitement d\u00e9finies. Par exemple, si vous d\u00e9clarez int x = 42;, la valeur 42 sera stock\u00e9e dans ce segment, pr\u00eate \u00e0 \u00eatre utilis\u00e9e d\u00e8s le d\u00e9marrage du programme.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#heap", "title": ".heap", "text": "

    Le Tas (Heap) est une zone de m\u00e9moire \u00e0 taille dynamique. Lorsqu'un programme n\u00e9cessite de la m\u00e9moire suppl\u00e9mentaire durant son ex\u00e9cution, il peut en demander au syst\u00e8me d'exploitation via un appel syst\u00e8me. En C, cette interaction est g\u00e9n\u00e9ralement g\u00e9r\u00e9e par les fonctions malloc et free de la biblioth\u00e8que standard <stdlib.h>. Le tas est crucial pour l'allocation dynamique, permettant de r\u00e9server et de lib\u00e9rer de la m\u00e9moire selon les besoins sp\u00e9cifiques du programme.

    ", "tags": ["free", "malloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#stack", "title": ".stack", "text": "

    La Pile (Stack) est une zone de m\u00e9moire \u00e0 taille fixe, utilis\u00e9e principalement pour la gestion des appels de fonctions. Lorsqu'une fonction est appel\u00e9e, les variables locales, les param\u00e8tres, ainsi que les informations n\u00e9cessaires pour g\u00e9rer l'appel et le retour de la fonction sont empil\u00e9s sur la pile. La pile est \u00e9galement sollicit\u00e9e par la fonction alloca de la biblioth\u00e8que <malloc.h>, qui permet d'allouer de la m\u00e9moire de mani\u00e8re temporaire. L'ordre d'ex\u00e9cution des fonctions \u00e9tant impr\u00e9visible, la pile s'av\u00e8re indispensable pour une gestion fluide et dynamique des ressources.

    ", "tags": ["alloca"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#points-de-vigilance", "title": "Points de vigilance", "text": "

    Avant d'entrer dans le vif du sujet, il est important de souligner quelques points concernant la gestion de la m\u00e9moire en C\u2009:

    Segmentation fault

    Cette erreur survient g\u00e9n\u00e9ralement lorsque le programme tente d'acc\u00e9der \u00e0 une zone de m\u00e9moire qui ne lui est pas allou\u00e9e, violant ainsi les r\u00e8gles de segmentation impos\u00e9es par le syst\u00e8me d'exploitation.

    Finitude de la pile

    Il est important de noter que la pile a une capacit\u00e9 limit\u00e9e. Un usage excessif de la pile, par exemple via une r\u00e9cursion trop profonde, peut entra\u00eener un d\u00e9bordement de pile (stack overflow), provoquant potentiellement une interruption du programme.

    Gestion de la m\u00e9moire dynamique

    Lorsqu'une m\u00e9moire allou\u00e9e dynamiquement via malloc n'est plus n\u00e9cessaire, il est imp\u00e9ratif de la lib\u00e9rer avec free pour \u00e9viter des fuites de m\u00e9moire (memory leaks), o\u00f9 des portions de m\u00e9moire restent inutilisables pour le syst\u00e8me.

    En synth\u00e8se, la gestion de la m\u00e9moire est un art d\u00e9licat, o\u00f9 chaque segment joue un r\u00f4le pr\u00e9cis pour assurer la stabilit\u00e9 et l'efficacit\u00e9 du programme. Une bonne compr\u00e9hension de ces concepts est essentielle pour \u00e9crire du code robuste et performant.

    ", "tags": ["free", "malloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#memoire-de-programme", "title": "M\u00e9moire de programme", "text": "

    Nous avons vu plus haut que la m\u00e9moire d'un programme est divis\u00e9e en plusieurs segments. En r\u00e9alit\u00e9, l'organisation interne de la m\u00e9moire d'un programme est ind\u00e9pendante du syst\u00e8me d'exploitation. C'est le compilateur et surtout la biblioth\u00e8que standard qui se charge de g\u00e9rer les diff\u00e9rents segments m\u00e9moires.

    N\u00e9anmoins si l'on consid\u00e8re la biblioth\u00e8que standard libc qui est utilis\u00e9e par la plupart des programmes C, on peut observer que la m\u00e9moire d'un programme est organis\u00e9e de la mani\u00e8re suivante\u2009:

    Organisation de m\u00e9moire d'un programme

    On observe que le tas et la pile vont \u00e0 leur rencontre, et que lorsqu'ils se percutent c'est le crash avec l'erreur bien connue stack overflow.

    ", "tags": ["libc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-statique", "title": "Allocation statique", "text": "

    Jusqu'\u00e0 pr\u00e9sent, toutes les variables que nous avons d\u00e9clar\u00e9es l'ont \u00e9t\u00e9 de mani\u00e8re statique. Cela signifie que le compilateur peut, d\u00e8s la compilation, d\u00e9terminer la taille n\u00e9cessaire pour chaque variable et les organiser en m\u00e9moire dans les segments appropri\u00e9s. Cette m\u00e9thode est connue sous le nom d'allocation de m\u00e9moire statique.

    Pour les variables globales, le compilateur se charge de les initialiser \u00e0 z\u00e9ro et de les placer dans les segments .bss, .data ou .rodata selon qu'elles soient initialis\u00e9es ou non.

    Pour les variables locales (celles d\u00e9clar\u00e9es \u00e0 l'int\u00e9rieur d'une fonction), le compilateur les place sur la pile. La pile est un segment de m\u00e9moire dont la croissance est invers\u00e9e par rapport \u00e0 la m\u00e9moire g\u00e9n\u00e9rale\u2009: elle commence \u00e0 une adresse \u00e9lev\u00e9e et s'\u00e9tend vers les adresses plus basses. Lorsqu'une fonction est appel\u00e9e, ses variables locales sont empil\u00e9es sur cette pile, un peu comme on empilerait des assiettes dans une cuisine. Une fois la fonction termin\u00e9e, ces variables sont d\u00e9pil\u00e9es, lib\u00e9rant ainsi l'espace m\u00e9moire qu'elles occupaient.

    Consid\u00e9rons la d\u00e9claration suivante, qui alloue un tableau de 1024 entiers 64 bits, initialis\u00e9s \u00e0 z\u00e9ro et stock\u00e9s dans le segment .bss. Ce segment \u00e9tant d\u00e9di\u00e9 aux variables non initialis\u00e9es, le syst\u00e8me d'exploitation se chargera de les initialiser \u00e0 z\u00e9ro\u2009:

    static int64_t vector[1024];\n

    Dans cet exemple, le tableau vector occupe 64 Kio de m\u00e9moire (1024 entiers de 64 bits). Comme le tableau est d\u00e9clar\u00e9 sans valeur initiale explicite (en dehors de l'initialisation \u00e0 z\u00e9ro par d\u00e9faut), il est plac\u00e9 dans le segment .bss, ce qui signifie que le syst\u00e8me d'exploitation se chargera de le remplir de z\u00e9ros.

    En revanche, la d\u00e9claration suivante cr\u00e9e un tableau de 1024 entiers 64 bits, mais avec une initialisation partielle\u2009:

    static int64_t vector[1024] = {.[42]=1};\n

    Ici, l'\u00e9l\u00e9ment \u00e0 l'index 42 du tableau est initialis\u00e9 \u00e0 1, tandis que tous les autres \u00e9l\u00e9ments doivent \u00eatre explicitement initialis\u00e9s \u00e0 z\u00e9ro par le programme. Cette d\u00e9claration force le tableau \u00e0 \u00eatre plac\u00e9 dans le segment .data parce qu'il contient une valeur initiale sp\u00e9cifique autre que z\u00e9ro. Le programme est donc responsable de l'initialisation des autres \u00e9l\u00e9ments du tableau.

    Il faut noter que le compilateur peut optimiser l'allocation de m\u00e9moire statique en regroupant les variables globales et statiques similaires dans des zones m\u00e9moires contigu\u00ebs. Cela permet d'optimiser l'acc\u00e8s \u00e0 ces variables et de r\u00e9duire la fragmentation de la m\u00e9moire. Il n'y a donc aucune garantie que les variables globales d\u00e9clar\u00e9es les unes \u00e0 la suite des autres dans le code source seront stock\u00e9es dans des emplacements m\u00e9moires contigus.

    La pomme est plus verte chez le voisin

    Consid\u00e9rons le code suivant\u2009:

    static int32_t a = 42;\nstatic int32_t b = 23;\n\nint main() {\n  int32_t *p = &a;\n  p[1] = 666;\n\n  printf(\"%d\\n\", b);\n}\n

    Le programme compile sans erreur, et lors de l'ex\u00e9cution il est tr\u00e8s probable que vous obteniez la valeur 666 affich\u00e9e \u00e0 l'\u00e9cran. Pourquoi\u2009? Parce qu'il est tr\u00e8s probable que les variables a et b soient stock\u00e9es dans des emplacements m\u00e9moires contigus, et que l'op\u00e9ration p[1] = 666 modifie en r\u00e9alit\u00e9 la valeur de b.

    N\u00e9anmoins, le standard C ne garantit pas que les variables globales soient stock\u00e9es dans des emplacements m\u00e9moires contigus. Ce code est donc non portable et peut produire des r\u00e9sultats inattendus sur d'autres compilateurs ou architectures.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique", "title": "Allocation dynamique", "text": "

    Il est des circonstances ou un programme ne sait pas combien de m\u00e9moire il a besoin. Par exemple un programme qui compterait le nombre d'occurrences de chaque mot dans un texte devra se construire un index de tous les mots qu'il d\u00e9couvre lors de la lecture du fichier d'entr\u00e9e. A priori, ce fichier d'entr\u00e9e \u00e9tant inconnu au moment de l'ex\u00e9cution du programme, l'espace m\u00e9moire n\u00e9cessaire \u00e0 construire ce dictionnaire de mots est \u00e9galement inconnu.

    L'approche la plus na\u00efve serait d'anticiper le cas le plus d\u00e9favorable. Le dictionnaire Littr\u00e9 comporte environ 132'000 mots tandis que le Petit Larousse Illustr\u00e9 80'000 mots environ. Pour se donner une bonne marge de man\u0153uvre et anticiper les anglicismes et les noms propres. Il suffirait de r\u00e9server un tableau de 1 million de mots de 10 caract\u00e8res soit un peu plus de 100 MiB de m\u00e9moire quand bien m\u00eame le fichier qui serait lu ne comporterait que 2 mots\u2009: Hello World!.

    Vous l'avez devin\u00e9, l'approche na\u00efve est \u00e0 proscrire. D'une part parce que vous n'avez aucune garantie que le nombre de mots ne d\u00e9passera pas 1 million, d'autre part parce que vous allez probablement gaspiller de la m\u00e9moire laiss\u00e9e inutilis\u00e9e.

    L'approche correcte est d'allouer la m\u00e9moire au moment o\u00f9 on en a besoin, c'est ce que l'on appelle l'allocation dynamique de m\u00e9moire.

    Lorsqu'un programme a besoin de m\u00e9moire, il peut g\u00e9n\u00e9rer un appel syst\u00e8me pour demander au syst\u00e8me d'exploitation le besoin de disposer de plus de m\u00e9moire. En pratique on utilise trois fonctions de la biblioth\u00e8que standard <stdlib.h>:

    void *malloc(size_t size)

    Alloue dynamiquement un espace m\u00e9moire de size bytes. Le terme malloc d\u00e9coule de Memory ALLOCation.

    void *calloc(size_t nitems, size_t size)

    Fonctionne de fa\u00e7on similaire \u00e0 malloc mais initialise l'espace allou\u00e9 \u00e0 z\u00e9ro.

    void free(void *ptr)

    Lib\u00e8re un espace pr\u00e9alablement allou\u00e9 par malloc ou calloc

    Comme \u00e9voqu\u00e9 plus haut, l'allocation dynamique se fait sur le tas (segment .heap) qui est de taille variable. \u00c0 chaque fois qu'un espace m\u00e9moire est demand\u00e9, malloc recherche dans le segment un espace vide de taille suffisante, s'il ne parvient pas, il ex\u00e9cute l'appel syst\u00e8me sbrk qui permet de d\u00e9placer la fronti\u00e8re du segment m\u00e9moire et donc d'agrandir le segment.

    Consid\u00e9rons la figure suivante. En abscisse le temps et verticalement l'espace m\u00e9moire \u00e0 des instants donn\u00e9es. Le segment .heap cro\u00eet au fur et \u00e0 mesure des appels \u00e0 malloc. Au d\u00e9but on r\u00e9serve un espace de deux char dont l'adresse est r\u00e9cup\u00e9r\u00e9e dans le pointeur a. Puis, on r\u00e9serve un espace de 1 char suppl\u00e9mentaire pour le pointeur b. Lorsque free est appel\u00e9 sur a, l'espace m\u00e9moire est lib\u00e9r\u00e9 et peut \u00eatre r\u00e9utilis\u00e9 par un appel ult\u00e9rieur \u00e0 malloc. Enfin, on r\u00e9serve un espace de 1 char pour le pointeur c. \u00c0 l'issue de cette ex\u00e9cution, on peut constater que la m\u00e9moire a \u00e9t\u00e9 fragment\u00e9e. C'est-\u00e0-dire que l'espace m\u00e9moire allou\u00e9 au programme comporte des trous.

    Allocation et lib\u00e9ration m\u00e9moire

    \u00c0 la fin de l'ex\u00e9cution du programme, ce dernier consomme sur le heap trois bytes d'espace m\u00e9moire bien qu'il n'en utilise que deux. Imaginez un programme qui alloue et lib\u00e8re de la m\u00e9moire de mani\u00e8re r\u00e9p\u00e9t\u00e9e, il est probable que la fragmentation m\u00e9moire s'installe et que le programme consomme beaucoup plus de m\u00e9moire que n\u00e9cessaire. Ces probl\u00e8mes de fragmentation sont fr\u00e9quents dans de gros programmes. N'avez-vous jamais \u00e9t\u00e9 contraint de red\u00e9marrer votre navigateur web car il consommait trop de m\u00e9moire mais qu'aucun onglet n'\u00e9tait ouvert\u2009? C'est probablement d\u00fb \u00e0 un probl\u00e8me de fragmentation m\u00e9moire.

    En pratique, nous verrons que le syst\u00e8me d'exploitation est capable de g\u00e9rer la fragmentation m\u00e9moire dans une certaine mesure en d\u00e9pla\u00e7ant les blocs m\u00e9moires pour les regrouper grace \u00e0 un m\u00e9canisme appel\u00e9 MMU. Ce m\u00e9canisme n'existe cependant pas sur des petites architectures mat\u00e9rielles comme les microcontr\u00f4leurs. C'est pourquoi les d\u00e9veloppeurs \u00e9vitent autant que possible l'allocation dynamique de m\u00e9moire sur ces plateformes.

    ", "tags": ["tas", "malloc", "free", "size", "calloc", "char"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#la-pile", "title": "La pile", "text": "

    Lorsqu'un programme s'ex\u00e9cute, l'ordre dont les fonctions s'ex\u00e9cutent n'est pas connu \u00e0 priori. L'ordre d'ex\u00e9cution des fonctions dans l'exemple suivant est inconnu par le programme et donc les \u00e9ventuelles variables locales utilis\u00e9es par ces fonctions doivent dynamiquement \u00eatre allou\u00e9es. Elle fonctionne sur le principe LIFO (Last In First Out), c'est-\u00e0-dire que la derni\u00e8re variable allou\u00e9e est la premi\u00e8re \u00e0 \u00eatre lib\u00e9r\u00e9e.

    Comme \u00e9voqu\u00e9 la pile est un segment de m\u00e9moire \u00e0 taille fixe qui est utilis\u00e9e pour stocker les variables locales, les param\u00e8tres des fonctions, les informations de retour des fonctions, les donn\u00e9es retourn\u00e9es par les fonctions et les zones allou\u00e9es par la fonction alloca.

    Comme le montre la table suivante, Windows ne d\u00e9roge pas \u00e0 la r\u00e8gle en faisant bande \u00e0 part avec une taille de pile de 1 MiB. Les autres syst\u00e8mes d'exploitation ont plut\u00f4t une taille de pile de 8 MiB. Cette diff\u00e9rence est tr\u00e8s importante car elle pr\u00e9sente le risque pour un programme d\u00e9velopp\u00e9 et test\u00e9 sous Linux de ne pas fonctionner sous Windows en raison de l'utilisation de la pile.

    Taille de la pile pour quelques architectures Syst\u00e8me d'exploitation Taille de la pile Windows 1 MiB Linux 8 MiB macOS 8 MiB Solaris 8 MiB

    \u00c0 titre d'exemple, le programme suivant fonctionne tr\u00e8s bien sous Linux mais pas sous Windows ou l'erreur segmentation fault est lev\u00e9e.

    int main() {\n   char a[4 * 1024 * 1024];  // 4 MiB\n}\n

    Pour \u00e9viter ce genre de probl\u00e8me, il est recommand\u00e9 de ne pas utiliser la pile pour stocker des donn\u00e9es de grande taille, pr\u00e9f\u00e9rez l'allocation dynamique sur le tas.

    ", "tags": ["alloca"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#fonctionnement-de-la-pile", "title": "Fonctionnement de la pile", "text": "

    La pile utilise deux registres essentiels g\u00e9n\u00e9ralement stock\u00e9s directement dans le processeur. Il s'agit du Stack Pointer et du Frame Pointer.

    Le Stack Pointer (SP)

    Il pointe vers le sommet de la pile, c'est-\u00e0-dire l'adresse m\u00e9moire o\u00f9 la prochaine donn\u00e9e sera ajout\u00e9e ou retir\u00e9e. Dans une architecture x86, le Stack Pointer est g\u00e9n\u00e9ralement stock\u00e9 dans le registre esp (32 bits) ou rsp (64 bits).

    Le Frame Pointer (FP) ou Base Pointer (BP)

    Il sert de r\u00e9f\u00e9rence pour acc\u00e9der aux variables locales et aux param\u00e8tres d'une fonction. Il reste constant pendant toute la dur\u00e9e de l'ex\u00e9cution d'une fonction. Dans une architecture x86, le Frame Pointer est g\u00e9n\u00e9ralement stock\u00e9 dans le registre ebp (32 bits) ou rbp (64 bits).

    Lorsque vous appelez une fonction, les \u00e9l\u00e9ments suivants sont g\u00e9n\u00e9ralement ajout\u00e9s \u00e0 la pile (l'ordre peut l\u00e9g\u00e8rement varier selon l'architecture et le compilateur) :

    1. L'adresse de retour : L'adresse \u00e0 laquelle le programme doit revenir apr\u00e8s l'ex\u00e9cution de la fonction.
    2. Les param\u00e8tres de la fonction : Les arguments surnum\u00e9raires pass\u00e9s \u00e0 la fonction.
    3. Le Frame Pointer (ancien) : La sauvegarde de l'ancien Frame Pointer pour restaurer l'\u00e9tat de la pile lors du retour de la fonction.
    4. Les variables locales : L'espace pour les variables locales de la fonction est r\u00e9serv\u00e9 dans la pile.

    Prenons l'exemple du programme suivant qui calcule la conjecture de Syracuse (ou de Collatz) pour un nombre donn\u00e9. Ce programme r\u00e9cursif affiche les nombres de la s\u00e9quence de Collatz pour un nombre donn\u00e9 et retourne le nombre de niveaux de la s\u00e9quence. Si la profondeur de r\u00e9cursion d\u00e9passe 100, le programme renvoie -1 et si le nombre atteint 1, le programme renvoie le niveau actuel de la s\u00e9quence, c'est la condition de sortie de la r\u00e9cursion.

    #include <stdint.h>\n#include <stdio.h>\n\nint collatz(int64_t n, int64_t i) {\n   printf(\"%ld \", n);\n   int64_t level = i + 1;\n   if (n == 1) return level;\n   if (i > 100) return -1;\n   int64_t next = n % 2 == 0 ? n / 2 : 3 * n + 1;\n   return collatz(next, level);\n}\n\nint main() {\n   collatz(12, 0);\n   return 0;\n}\n

    Pour analyser comment la pile est utilis\u00e9e dans ce programme, nous allons utiliser le d\u00e9bogueur gdb. Tout d'abord il faut compiler le programme avec le flag -g pour inclure les informations de d\u00e9bogage\u2009:

    gcc -g collatz.c -o collatz\n

    Puis ex\u00e9cutez le programme avec gdb :

    gdb ./collatz\n

    Depuis GDB on va tout d'abord ajouter un point d'arr\u00eat \u00e0 la fin de l'ex\u00e9cution (ligne 15) puis lancer l'ex\u00e9cution du programme\u2009:

    (gdb) break 15\nBreakpoint 1 at 0x1179: file collatz.c, line 15.\n(gdb) run\nStarting program: /path/to/collatz\n12 6 3 10 5 16 8 4 2 1\n

    Le programme s'arr\u00eate \u00e0 la fin de l'ex\u00e9cution. Nous pouvons maintenant examiner la pile en affichant les 100 derni\u00e8res valeurs 64 bits l'adresse du Frame Pointer (FP) moins 0x300 octets. Ces valeurs sont un peu une devinette, il faut \u00ab\u2009fouiller\u2009\u00bb un peu pour identifier ce qui nous int\u00e9resse car on ne sait pas trop combien d'\u00e9l\u00e9ments ont \u00e9t\u00e9 ajout\u00e9s \u00e0 la pile. Voici comment cela peut \u00eatre fait\u2009:

    (gdb) x/100g $rbp - 0x300\n0x7fffffffda40: 0x7fffffffda70 0x555555555178\n0x7fffffffda50: 9       1\n0x7fffffffda60: 10      0                       << (3)\n0x7fffffffda70: 0x7fffffffdaa0 0x5555555551e6\n0x7fffffffda80: 8       2\n0x7fffffffda90: 9       1\n0x7fffffffdaa0: 0x7fffffffdad0 0x5555555551e6\n0x7fffffffdab0: 7       4\n0x7fffffffdac0: 8       2\n0x7fffffffdad0: 0x7fffffffdb00 0x5555555551e6\n0x7fffffffdae0: 6       8\n0x7fffffffdaf0: 7       4\n0x7fffffffdb00: 0x7fffffffdb30 0x5555555551e6\n0x7fffffffdb10: 5       16\n0x7fffffffdb20: 6       8\n0x7fffffffdb30: 0x7fffffffdb60 0x5555555551e6\n0x7fffffffdb40: 4       5\n0x7fffffffdb50: 5       16\n0x7fffffffdb60: 0x7fffffffdb90 0x5555555551e6\n0x7fffffffdb70: 3       10\n0x7fffffffdb80: 4       5\n0x7fffffffdb90: 0x7fffffffdbc0 0x5555555551e6\n0x7fffffffdba0: 2       3\n0x7fffffffdbb0: 3       10\n0x7fffffffdbc0: 0x7fffffffdbf0 0x5555555551e6\n0x7fffffffdbd0: 1       6\n0x7fffffffdbe0: 2       3                       << ...\n0x7fffffffdbf0: 0x7fffffffdc20 0x5555555551e6   << FP, adresse de retour (2)\n0x7fffffffdc00: 0       12                      << i, n (1)\n0x7fffffffdc10: 1       6                       << level, next\n0x7fffffffdc20: 0x7fffffffdc30 0x5555555551ff   << FP, adresse de retour\n0x7fffffffdc30: 0x7fffffffdcd0 0x7ffff7dc21ca\n0x7fffffffdc40: 0x7fffffffdc80 0x7fffffffdd58\n

    Avec la commande suivante, on prend note de la valeur actuelle du Frame Pointer. C'est la valeur de base au niveau du main. Les appels successifs \u00e0 collatz ont \u00e9t\u00e9 empil\u00e9s et comme la pile descend, on aura tout en bas l'adresse de retour de main :

    (gdb) info register rbp\nrbp            0x7fffffffdc30      0x7fffffffdc30\n

    \u00c0 partir de cela et en analysant la pile, on peut voir comment les valeurs sont empil\u00e9es et d\u00e9sempil\u00e9es lors des appels de fonctions. On y retrouve les variables locales level et next ainsi que les arguments sauvegard\u00e9s pour les appels r\u00e9cursifs de collatz (les valeurs de n et i). On peut facilement identifier les diff\u00e9rentes frames de pile avec la valeur sauvegard\u00e9e du Frame Pointer.

    En (1), il y a le passage initial des arguments depuis le main, collatz a \u00e9t\u00e9 appel\u00e9 avec collatz(12, 0). En (2), on voit l'adresse du pr\u00e9c\u00e9dent frame (0x7fffffffdc20). En (3) la derni\u00e8re valeur de n et i avant le retour de la fonction.

    La figure ci-dessous r\u00e9sume graphiquement le fonctionnement de la pile. On y voit deux frames li\u00e9es entre elles par le frame pointer. Les adresses de retours correspondent \u00e0 la ligne 10 et 14 du programme, soit l'emplacement des appels de fonctions.

    Pile d'ex\u00e9cution

    ", "tags": ["level", "rsp", "rbp", "gdb", "next", "ebp", "esp", "main", "collatz"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#passage-des-arguments", "title": "Passage des arguments", "text": "

    En pratique, les arguments des fonctions ne sont pas enti\u00e8rement pass\u00e9s sur la pile pour des questions de performances. Sur une architecture x86-64, les 6 premiers arguments sont pass\u00e9s dans les registres processeur rdi, rsi, rdx, rcx, r8 et r9. Les arguments suivants sont pass\u00e9s sur la pile. Dans notre exemple, nous voyons malgr\u00e9 tout que les arguments n et i sont pass\u00e9s sur la pile mais il s'agit d'une sauvegarde locale car la fonction est r\u00e9cursive et \u00e0 chaque appel, les registres seraient \u00e9cras\u00e9s.

    ", "tags": ["rcx", "rdi", "rdx", "rsi"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique-sur-le-tas", "title": "Allocation dynamique sur le tas", "text": "

    L'allocation dynamique permet de r\u00e9server - lors de l'ex\u00e9cution - une zone m\u00e9moire dont on vient de calculer la taille. La limite de m\u00e9moire est techniquement celle de la machine. Un ordinateur avec 32 Gio de RAM peut allouer 32 Gio de m\u00e9moire dynamique mais partag\u00e9e entre tous les programmes en cours d'ex\u00e9cution.

    C'est la fonction malloc (memory allocation) qui est utilis\u00e9e pour demander de la m\u00e9moire. Cette fonction est impl\u00e9ment\u00e9e dans la librairie standard du langage C. Elle peut si n\u00e9cessaire, demander au syst\u00e8me d'exploitation de lui allouer de la m\u00e9moire par l'interm\u00e9diaire d'un appel syst\u00e8me comme sbrk ou mmap. En effet, les appels syst\u00e8mes sont co\u00fbteux en temps et en ressources car le syst\u00e8me d'exploitation doit interrompre le programme changer de contexte pour ex\u00e9cuter le code du noyau puis revenir au programme. C'est pourquoi la fonction malloc ne demande pas syst\u00e9matiquement de la m\u00e9moire au syst\u00e8me d'exploitation. Elle dispose d'une m\u00e9moire tampon qu'elle alloue progressivement. Ce n'est que lorsque cette m\u00e9moire tampon est \u00e9puis\u00e9e, qu'elle demande alors de la m\u00e9moire au syst\u00e8me d'exploitation. Contrairement aux id\u00e9es re\u00e7ues, les appels \u00e0 malloc ne sont pas syst\u00e9matiquement co\u00fbteux.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation", "title": "Allocation", "text": "

    Pour utiliser l'allocation dynamique de m\u00e9moire il est n\u00e9cessaire d'inclure le fichier stdlib.h. Si l'on souhaite allouer un tableau de 1024 entiers 32 bits (soit 4 Kio), on \u00e9crira\u2009:

    int main() {\n   int *data = (int*)malloc(1024 * sizeof(int));\n   if (data == NULL) {\n      fprintf(stderr, \"Impossible d'allouer la m\u00e9moire\\n\");\n      return 1;\n   }\n   for (int i = 0; i < 1024; i++) data[i] = i;\n\n   free(data);\n}\n

    Dans cet exemple, la fonction malloc retourne un pointeur sur la zone de m\u00e9moire demand\u00e9e. Si l'allocation \u00e9choue, la fonction retourne NULL. Il est important de v\u00e9rifier que l'allocation a r\u00e9ussi avant d'utiliser le pointeur retourn\u00e9. Si l'allocation a \u00e9chou\u00e9, il est recommand\u00e9 de g\u00e9rer l'erreur et de ne pas continuer l'ex\u00e9cution du programme.

    L'allocation peut \u00e9chouer dans les cas suivants\u2009:

    • L'ordinateur n'a plus de m\u00e9moire disponible.
    • La taille demand\u00e9e est trop grande.
    • La m\u00e9moire est fragment\u00e9e et il n'y a pas de blocs contigus suffisamment grands pour satisfaire la demande.
    • Le syst\u00e8me d'exploitation a mis en place des quotas de m\u00e9moire pour les processus.

    Une fois que l'on s'est assur\u00e9 que *data contient bien une adresse m\u00e9moire valide, on peut l'utiliser comme un tableau classique. Notons au passage que la m\u00e9moire allou\u00e9e dynamiquement n'est pas initialis\u00e9e. Il est donc n\u00e9cessaire de l'initialiser avant de l'utiliser. Une mani\u00e8re simple est d'appeler la fonction memset de la librairie standard string.h :

    memset(data, 0, 1024 * sizeof(int));\n

    Cette fonction initialise la zone m\u00e9moire point\u00e9e par data avec des z\u00e9ros. Comme il est fr\u00e9quent de demander de la m\u00e9moire initialis\u00e9e, la fonction calloc est souvent utilis\u00e9e \u00e0 la place de malloc. Elle joue les deux r\u00f4les\u2009: allouer de la m\u00e9moire et l'initialiser \u00e0 z\u00e9ro.

    int *data = (int*)calloc(1024, sizeof(int));\n

    Fuite m\u00e9moire

    La difficult\u00e9 principale de ce m\u00e9canisme est que si l'on perd une r\u00e9f\u00e9rence sur la m\u00e9moire allou\u00e9e, il est impossible de la lib\u00e9rer. C'est ce que l'on appelle une fuite m\u00e9moire (memory leak). Cela peut arriver assez facilement\u2009:

    int main() {\n  int *data = (int*)malloc(1024 * sizeof(int));\n\n  data = (int*)42; // Fuite m\u00e9moire, on \u00e9crase le pointeur et on ne pourra plus lib\u00e9rer la m\u00e9moire allou\u00e9e\n}\n

    Dans cet exemple, le pointeur data est \u00e9cras\u00e9 par la valeur 42. La m\u00e9moire allou\u00e9e par malloc est perdue et ne peut plus \u00eatre lib\u00e9r\u00e9e.

    Notez que les protoypes de malloc (Memory ALLOCate) et calloc (Continuous ALLOCate) diff\u00e8rent l\u00e9g\u00e8rement\u2009:

    void *malloc(size_t size);\nvoid *calloc(size_t nitems, size_t size);\n

    L'un prend size correspondant \u00e0 la taille en octets de la m\u00e9moire \u00e0 allouer, l'autre prend nitems et size correspondant respectivement au nombre d'\u00e9l\u00e9ments et \u00e0 la taille en octets de chaque \u00e9l\u00e9ment.

    La justification principale de cette diff\u00e9rence est historique. malloc est plus ancienne et a \u00e9t\u00e9 introduite dans les premi\u00e8res versions du langage C. calloc est apparue plus tard et a \u00e9t\u00e9 con\u00e7ue pour simplifier l'allocation de tableaux. En effet, il est plus naturel de sp\u00e9cifier le nombre d'\u00e9l\u00e9ments et la taille de chaque \u00e9l\u00e9ment pour allouer un tableau. (c.f. SO.

    ", "tags": ["malloc", "nitems", "size", "NULL", "calloc", "data"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#liberation", "title": "Lib\u00e9ration", "text": "

    Une fois que le travail est termin\u00e9, il est n\u00e9cessaire de lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement. C'est la fonction free qui est utilis\u00e9e pour cela. Le prototype de la fonction est le suivant\u2009:

    void free(void *ptr);\n

    Elle prend en param\u00e8tre un pointeur vers la zone m\u00e9moire \u00e0 lib\u00e9rer.

    Dois-je appeler free \u00e0 la fin du main\u2009? Dans un programme C, appeler free pour lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement (par malloc, calloc, ou realloc) avant la fin du main est une bonne pratique, mais ce n'est pas strictement n\u00e9cessaire dans tous les cas.

    Un cas pour lequel il est n\u00e9cessaire d'appeler free avant la fin du main est lorsqu'un d\u00e9veloppeur utilise des outils d'analyse de m\u00e9moire comme Valgrind. Ces outils peuvent signaler des \u00ab\u2009fuites\u2009\u00bb de m\u00e9moire si des allocations ne sont pas lib\u00e9r\u00e9es \u00e0 la fin du programme, m\u00eame si le syst\u00e8me d'exploitation r\u00e9cup\u00e8re automatiquement toute la m\u00e9moire allou\u00e9e \u00e0 la fin du programme.

    En r\u00e9sum\u00e9, bien que ce ne soit pas strictement n\u00e9cessaire d'appeler free avant la fin du main dans tous les cas, le faire est consid\u00e9r\u00e9 comme une bonne pratique et contribue \u00e0 maintenir un code propre et sans fuites de m\u00e9moire.

    ", "tags": ["malloc", "free", "realloc", "calloc", "main"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#reallocation", "title": "R\u00e9allocation", "text": "

    Parfois, il est n\u00e9cessaire de modifier la taille d'une zone m\u00e9moire d\u00e9j\u00e0 allou\u00e9e. C'est la fonction realloc qui est utilis\u00e9e pour cela. Le prototype de la fonction est le suivant\u2009:

    void *realloc(void *ptr, size_t size);\n

    Elle prend en param\u00e8tre un pointeur vers la zone m\u00e9moire \u00e0 r\u00e9allouer et la nouvelle taille de la zone m\u00e9moire. La fonction retourne un pointeur vers la nouvelle zone m\u00e9moire allou\u00e9e comme pour malloc. Si la r\u00e9allocation \u00e9choue, la fonction retourne NULL et la zone m\u00e9moire initiale reste inchang\u00e9e. Aussi, pour \u00e9viter les fuites m\u00e9moire, on veillera \u00e0 ne pas perdre la r\u00e9f\u00e9rence sur la zone m\u00e9moire initiale avant d'avoir r\u00e9cup\u00e9r\u00e9 le pointeur de la zone m\u00e9moire r\u00e9allou\u00e9e.

    int main() {\n   // Allocation\n   int *data = (int*)malloc(1024 * sizeof(int));\n   if (data == NULL) return 1;\n\n   // R\u00e9allocation\n   int *new_data = (int*)realloc(data, 2048 * sizeof(int));\n   if (new_data == NULL) return 1;\n   data = new_data;\n\n   // Lib\u00e9ration\n   free(data);\n}\n

    Notons que la nouvelle zone m\u00e9moire allou\u00e9e par realloc n'est pas n\u00e9cessairement \u00e0 la m\u00eame adresse que la zone m\u00e9moire initiale. En effet, si la zone m\u00e9moire qui suit la zone m\u00e9moire initiale est libre, realloc peut simplement \u00e9tendre la zone m\u00e9moire initiale et la valeur du pointeur reste inchang\u00e9e. Dans le cas contraire, il allouera une nouvelle zone m\u00e9moire, copiera les donn\u00e9es de la zone m\u00e9moire initiale dans la nouvelle zone m\u00e9moire et lib\u00e9rera la zone m\u00e9moire initiale.

    En d'autres termes, des appels trop fr\u00e9quents \u00e0 realloc peuvent entra\u00eener des copies inutiles de donn\u00e9es et des performances m\u00e9diocres. Il est donc recommand\u00e9 de r\u00e9server la m\u00e9moire en une seule fois si possible. Prenons l'exemple suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   size_t size = 1;\n   char *data = (char *)malloc(size);\n   if (data == NULL) return 1;\n\n   while (!feof(stdin)) {\n      data[size++ - 1] = getchar();\n      {\n         char *new_data = (char *)realloc(data, size);\n         if (new_data == NULL) return 1;\n         data = new_data;\n      }\n   }\n\n   printf(\"%s\\n\", data);\n   free(data);\n}\n

    Le programme lit des caract\u00e8res depuis l'entr\u00e9e standard et les stocke dans un tableau dynamique. \u00c0 chaque it\u00e9ration, il r\u00e9alloue la m\u00e9moire pour stocker un caract\u00e8re suppl\u00e9mentaire. Cette approche est inefficace car elle entra\u00eene une copie potentielle de la m\u00e9moire \u00e0 chaque it\u00e9ration. Il est pr\u00e9f\u00e9rable de r\u00e9server la m\u00e9moire en une seule fois en utilisant une taille de m\u00e9moire suffisamment grande pour stocker tous les caract\u00e8res.

    En pratique on utilise fr\u00e9quemment un param\u00e8tre suppl\u00e9mentaire nomm\u00e9 facteur de croissance qui permet de r\u00e9allouer la m\u00e9moire en fonction de la taille actuelle. Avec un facteur de croissance de 2, lorsqu'on r\u00e9alloue la m\u00e9moire, on double la taille de la zone m\u00e9moire. Cela permet de limiter le nombre d'appels \u00e0 realloc et donc de limiter les copies inutiles de donn\u00e9es. Voici le m\u00eame programme que ci-dessus mais avec un facteur de croissance de 2\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   size_t size = 1;\n   size_t capacity = 128;\n   char *data = (char *)malloc(capacity);\n   if (data == NULL) return 1;\n\n   while (!feof(stdin)) {\n      data[size++ - 1] = getchar();\n      if (size >= capacity) {\n         capacity *= 2;\n         char *new_data = (char *)realloc(data, capacity);\n         if (new_data == NULL) return 1;\n         data = new_data;\n      }\n   }\n\n   printf(\"%s\\n\", data);\n   free(data);\n}\n

    Ici, deux variables sont utilis\u00e9es size qui correspond \u00e0 la taille actuelle du tableau et capacity qui correspond \u00e0 la capacit\u00e9 totale du tableau. Lorsque la taille du tableau atteint sa capacit\u00e9, on double la capacit\u00e9 du tableau et on r\u00e9alloue la m\u00e9moire.

    ", "tags": ["malloc", "size", "NULL", "realloc", "capacity"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#allocation-dynamique-sur-la-pile", "title": "Allocation dynamique sur la pile", "text": "

    L'allocation dynamique sur la pile est \u00e9quivalente \u00e0 l'allocation sur le tas sauf qu'elle est plus rapide (pas de recherche par le syst\u00e8me d'un espace suffisant et continu) et qu'elle ne n\u00e9cessite pas de lib\u00e9ration.

    On utilisera la fonction alloca (memory allocation) pour r\u00e9server de la m\u00e9moire. Cette fonction n'initialise pas la zone r\u00e9serv\u00e9e.

    void* alloca(size_t size);\n

    Il est n\u00e9cessaire d'inclure le fichier malloc.h pour utiliser cette fonction d'allocation m\u00e9moire sur la pile.

    L'avantage par rapport \u00e0 malloc est que la m\u00e9moire est lib\u00e9r\u00e9e automatiquement \u00e0 la sortie de la fonction. C'est donc une solution id\u00e9ale pour allouer de la m\u00e9moire temporaire dans une fonction. Par exemple, si vous avez besoin d'un tableau temporaire pour effectuer un calcul, vous pouvez utiliser alloca pour r\u00e9server la m\u00e9moire n\u00e9cessaire. N\u00e9anmoins vous devez vous assurer que la m\u00e9moire allou\u00e9e ne d\u00e9passe pas la taille de la pile qui est de 1 MiB sous Windows et 8 MiB sous Linux et macOS.

    Les inconv\u00e9nients sont que la fonction alloca n'est pas standardis\u00e9e et n'est pas disponible sur toutes les plateformes.

    Voici un exemple de fonctionnement\u2009:

    #include <alloca.h>\n\nvoid foo() {\n   int *data = (int*)alloca(1024 * sizeof(int));\n   for (int i = 0; i < 1024; ++i) data[i] = i;\n}\n

    Non standard

    Comme \u00e9voqu\u00e9 la fonction alloca n'est pas standardis\u00e9e et n'est pas disponible sur toutes les plateformes. Il est recommand\u00e9 de ne pas l'utiliser dans un code portable.

    ", "tags": ["alloca", "malloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#gestion-interne-de-la-memoire", "title": "Gestion interne de la m\u00e9moire", "text": "

    Pour mieux comprendre la tuyauterie de l'allocation dynamique de m\u00e9moire, attardons-nous un peu sur la gestion interne de la m\u00e9moire.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#fragmentation-memoire", "title": "Fragmentation m\u00e9moire", "text": "

    On peut observer \u00e0 la figure suivante montre qu'apr\u00e8s un appel successif de malloc et de free des espaces m\u00e9moire non utilis\u00e9s peuvent appara\u00eetre entre des r\u00e9gions utilis\u00e9es. Ces trous sont appel\u00e9s fragmentation m\u00e9moire.

    Dans la figure suivante, on suit l'\u00e9volution de l'utilisation du heap au cours de la vie d'un programme. Au d\u00e9but \u2780, la m\u00e9moire est libre. Tant que de la m\u00e9moire est allou\u00e9e sans lib\u00e9ration (free), aucun probl\u00e8me de fragmentation \u2781. N\u00e9anmoins, apr\u00e8s un certain temps la m\u00e9moire devient fragment\u00e9e \u2782\u2009; 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\u00e9server un espace de taille 9 malgr\u00e9 que l'espace cumul\u00e9 libre est suffisant.

    Fragmentation m\u00e9moire

    Dans une petite architecture, l'allocation et la lib\u00e9ration fr\u00e9quente d'espaces m\u00e9moire de taille arbitraire sont malvenues. Une fois que la fragmentation m\u00e9moire est install\u00e9e, il n'existe aucun moyen de soigner le mal si ce n'est au travers de l'ultime solution de l'informatique\u2009: \u00e9teindre puis red\u00e9marrer.

    ", "tags": ["free", "malloc"]}, {"location": "course-c/25-architecture-and-systems/memory-management/#mmu", "title": "MMU", "text": "

    Les syst\u00e8mes d'exploitation modernes (Windows, Linux, macOS...) utilisent tous un dispositif mat\u00e9riel nomm\u00e9 MMU pour Memory Management Unit. La MMU est responsable de cr\u00e9er un espace m\u00e9moire virtuel entre l'espace physique. Cela cr\u00e9e une indirection suppl\u00e9mentaire, mais permet de r\u00e9organiser la m\u00e9moire physique sans compromettre le syst\u00e8me.

    En pratique l'espace de m\u00e9moire virtuelle est toujours beaucoup plus grand que l'espace physique. Cela permet de s'affranchir dans une large mesure de probl\u00e8mes de fragmentation, car si l'espace virtuel est suffisamment grand, il y aura statistiquement plus de chance d'y trouver un emplacement non utilis\u00e9.

    La programmation sur de petites architectures mat\u00e9rielles (microcontr\u00f4leurs, DSP) ne poss\u00e8de pas de MMU et d\u00e8s lors l'allocation dynamique est g\u00e9n\u00e9ralement \u00e0 proscrire \u00e0 moins qu'elle soit faite en connaissance de cause et en utilisant des m\u00e9canismes comme les memory pool.

    Dans la figure ci-dessous. La m\u00e9moire physique est repr\u00e9sent\u00e9e \u00e0 droite en termes de pages m\u00e9moires physiques (Physical Pages ou PP). Il s'agit de blocs m\u00e9moires contigus d'une taille fixe, par exemple 64 kb. Chaque page physique est mapp\u00e9e dans une table propre \u00e0 chaque processus (programme ex\u00e9cutable). On y retrouve quelques propri\u00e9t\u00e9s utiles \u00e0 savoir, est-ce que la page m\u00e9moire est accessible en \u00e9criture, est-ce qu'elle peut contenir du code ex\u00e9cutable\u2009? Une propri\u00e9t\u00e9 peut indiquer par exemple si la page m\u00e9moire est valide. Chacune de ces entr\u00e9es est consid\u00e9r\u00e9e comme une page m\u00e9moire virtuelle (virtual page VP).

    M\u00e9moire virtuelle

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#memoire-cache", "title": "M\u00e9moire cache", "text": "

    La m\u00e9moire cache est un dispositif mat\u00e9riel qui permet de stocker temporairement des donn\u00e9es fr\u00e9quemment utilis\u00e9es. Elle est utilis\u00e9e pour acc\u00e9l\u00e9rer l'acc\u00e8s \u00e0 la m\u00e9moire principale. La m\u00e9moire cache est g\u00e9n\u00e9ralement plus rapide que la m\u00e9moire principale, mais elle est aussi plus petite. Il existe plusieurs niveaux de cache, le cache de niveau 1 (L1) est le plus rapide mais aussi le plus petit, le cache de niveau 2 (L2) est plus lent mais plus grand, et ainsi de suite.

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#erreurs-de-segmentation", "title": "Erreurs de segmentation", "text": "

    Lorsqu'un programme tente d'acc\u00e9der \u00e0 un espace m\u00e9moire qui n'est pas mapp\u00e9 dans la MMU, ou que cet espace m\u00e9moire ne permet pas le type d'acc\u00e8s souhait\u00e9\u2009: par exemple une \u00e9criture dans une page en lecture seule. Le syst\u00e8me d'exploitation tue le processus avec une erreur Segmentation Fault. C'est la raison pour laquelle, il n'est pas syst\u00e9matique d'avoir une erreur de segmentation en cas de jardinage m\u00e9moire. Tant que les valeurs modifi\u00e9es sont localis\u00e9es au sein d'un bloc m\u00e9moire autoris\u00e9, il n'y aura pas d'erreur.

    L'erreur de segmentation est donc g\u00e9n\u00e9r\u00e9e par le syst\u00e8me d'exploitation en levant le signal SIGSEGV (Violation d'acc\u00e8s \u00e0 un segment m\u00e9moire, ou erreur de segmentation).

    "}, {"location": "course-c/25-architecture-and-systems/memory-management/#memory-pool", "title": "Memory Pool", "text": "

    Un memory pool est une m\u00e9thode faisant appel \u00e0 de l'allocation dynamique de blocs de taille fixe. Lorsqu'un programme doit tr\u00e8s r\u00e9guli\u00e8rement allouer et d\u00e9sallouer de la m\u00e9moire, il est pr\u00e9f\u00e9rable que les blocs m\u00e9moires aient une taille fixe. De cette fa\u00e7on, apr\u00e8s un free, la m\u00e9moire lib\u00e9r\u00e9e est assez grande pour une allocation ult\u00e9rieure.

    Lorsqu'un programme est ex\u00e9cut\u00e9 sous Windows, macOS ou Linux, l'allocation dynamique standard malloc, calloc, realloc et free sont performant et le risque de crash d\u00fb \u00e0 une fragmentation m\u00e9moire est rare.

    En revanche, lors de l'utilisation sur de petites architectures (microcontr\u00f4leurs) qui n'ont pas de syst\u00e8me sophistiqu\u00e9 pour g\u00e9rer la m\u00e9moire, il est parfois n\u00e9cessaire d'\u00e9crire son propre syst\u00e8me de gestion de m\u00e9moire.

    ", "tags": ["free", "malloc", "realloc", "calloc"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/", "title": "Programmes et Processus", "text": "L'informatique est avant tout une science de l'abstraction. Il s'agit de cr\u00e9er le bon mod\u00e8le pour un probl\u00e8me et d'imaginer les bonnes techniques automatisables et appropri\u00e9es pour le r\u00e9soudre. Toutes les autres sciences consid\u00e8rent l'univers tel qu'il est. Par exemple, le travail d'un physicien est de comprendre le monde et non pas d'inventer un monde dans lequel les lois de la physique seraient plus simples et auxquelles il serait plus agr\u00e9able de se conformer. \u00c0 l'oppos\u00e9, les informaticiens doivent cr\u00e9er des abstractions des probl\u00e8mes du monde r\u00e9el qui pourraient \u00eatre repr\u00e9sent\u00e9es et manipul\u00e9es dans un ordinateur.Alfred Vaino Aho et Jeffrey David Ullman"}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#quest-ce-quun-programme", "title": "Qu'est-ce qu'un programme\u2009?", "text": "

    Un programme informatique est une suite d'instruction d\u00e9finissant des op\u00e9rations \u00e0 r\u00e9aliser sur des donn\u00e9es\u2009; des instructions destin\u00e9es \u00e0 \u00eatre ex\u00e9cut\u00e9es par un ordinateur. Un programme peut se d\u00e9cliner sous plusieurs formes\u2009:

    • le code source (C, C++, Python, Java, etc.) ;
    • le listing assembleur (.s, .asm) ;
    • l'ex\u00e9cutable binaire (ELF, .exe, .out, .dll, .so, etc.).

    Un processus est l'\u00e9tat d'un programme en cours d'ex\u00e9cution. Lorsqu'un programme est ex\u00e9cut\u00e9, il devient processus pendant un temps donn\u00e9. Les syst\u00e8mes d'exploitation tels que Windows sont dits multit\u00e2ches car ils peuvent faire s'ex\u00e9cuter plusieurs processus en parall\u00e8le. Le temps processeur est ainsi partag\u00e9 entre chaque processus.

    Quelque soit le langage de programmation utilis\u00e9, sur un ordinateur le processeur adopte un flot d'ex\u00e9cution s\u00e9quentiel. Les instructions sont ex\u00e9cut\u00e9es les unes apr\u00e8s les autres.

    Programmeuse en tenue d\u00e9contract\u00e9e \u00e0 c\u00f4t\u00e9 de 62'500 cartes perfor\u00e9es

    ", "tags": ["programme"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#code-source", "title": "Code source", "text": "

    Le code source est g\u00e9n\u00e9ralement \u00e9crit par un ing\u00e9nieur/d\u00e9veloppeur/informaticien. Il s'agit le plus souvent d'un fichier texte lisible par un \u00eatre humain et souvent pourvu de commentaires facilitant sa compr\u00e9hension. Selon le langage de programmation utilis\u00e9, la programmation peut \u00eatre graphique comme avec les diagrammes Ladder utilis\u00e9s dans les automates programmables et respectant la norme IEC 61131-3, ou LabView un outil de d\u00e9veloppement graphique.

    Le plus souvent le code source est organis\u00e9 en une arborescence de fichiers. Des programmes complexes comme le noyau Linux contiennent plus de 100'000 fichiers et 10 millions de lignes de code, pour la plupart \u00e9crites en C.

    "}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#executable-binaire", "title": "Ex\u00e9cutable binaire", "text": "

    Une fois compil\u00e9 en langage machine, il en r\u00e9sulte un fichier qui peut \u00eatre ex\u00e9cut\u00e9 soit par un syst\u00e8me d'exploitation, soit sur une plateforme embarqu\u00e9e \u00e0 microcontr\u00f4leur sans l'interm\u00e9diaire d'un syst\u00e8me d'exploitation. On dit que ce type de programme est bare metal, qu'il s'ex\u00e9cute \u00e0 m\u00eame le m\u00e9tal.

    Un ex\u00e9cutable binaire doit \u00eatre compil\u00e9 pour la bonne architecture mat\u00e9rielle. Un programme compil\u00e9 pour un processeur INTEL ne pourra pas s'ex\u00e9cuter sur un processeur ARM, c'est pourquoi on utilise diff\u00e9rents compilateurs en fonctions des architectures cibles. L'op\u00e9ration de compiler un programme pour une autre architecture, ou un autre syst\u00e8me d'exploitation que celui sur lequel est install\u00e9 le compilateur s'appelle la compilation crois\u00e9e (cross compilation).

    Prenons l'exemple du programme suivant qui calcule la suite des nombres de Fibonacci\u2009:

    #include <stdio.h>\n\nint main(void)\n{\n    int t1 = 0, t2 = 1;\n    int n, next_term;\n    printf(\"Enter the number of terms: \");\n    scanf(\"%d\", &n);\n    printf(\"Fibonacci Series: \");\n    for (size_t i = 1; i <= n; ++i)\n    {\n        printf(\"%d, \", t1);\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    printf(\"\\n\");\n}\n

    Une fois assembl\u00e9 le code source est converti en langage assembleur, une version interm\u00e9diaire entre le C et le langage machine. L'exemple est compil\u00e9 en utilisant gcc\u2009:

    gcc Fibonacci.c -o fibonacci.exe\nobjdump -d fibonacci.exe\n

    On obtient un fichier similaire \u00e0 ceci qui contient le code machine (48 83 ec 20), et l'\u00e9quivalent en langage assembleur (mov %fs:0x28,%rax):

    0000000000000680 <main>:\n680:   41 55                   push   %r13\n682:   41 54                   push   %r12\n684:   48 8d 35 59 02 00 00    lea    0x259(%rip),%rsi\n68b:   55                      push   %rbp\n68c:   53                      push   %rbx\n68d:   bf 01 00 00 00          mov    $0x1,%edi\n692:   48 83 ec 18             sub    $0x18,%rsp\n696:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax\n69d:   00 00\n69f:   48 89 44 24 08          mov    %rax,0x8(%rsp)\n6a4:   31 c0                   xor    %eax,%eax\n6a6:   e8 a5 ff ff ff          callq  650 <__printf_chk@plt>\n6ab:   48 8d 74 24 04          lea    0x4(%rsp),%rsi\n6b0:   48 8d 3d 49 02 00 00    lea    0x249(%rip),%rdi\n6b7:   31 c0                   xor    %eax,%eax\n6b9:   e8 a2 ff ff ff          callq  660 <__isoc99_scanf@plt>\n6be:   48 8d 35 3e 02 00 00    lea    0x23e(%rip),%rsi\n...\n72e:   00 00\n730:   75 0b                   jne    73d <main+0xbd>\n732:   48 83 c4 18             add    $0x18,%rsp\n736:   5b                      pop    %rbx\n737:   5d                      pop    %rbp\n738:   41 5c                   pop    %r12\n73a:   41 5d                   pop    %r13\n73c:   c3                      retq\n73d:   e8 fe fe ff ff          callq  640 <__stack_chk_fail@plt>\n742:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n749:   00 00 00\n74c:   0f 1f 40 00             nopl   0x0(%rax)\n

    Avec un visualiseur hexad\u00e9cimal, on peut extraire le langage machine du binaire ex\u00e9cutable. L'utilitaire hexdump est appel\u00e9 avec deux options -s pour sp\u00e9cifier l'adresse de d\u00e9but, on choisit ici celle du d\u00e9but de la fonction main 0x680, et -n pour n'extraire que les premiers 256 octets\u2009:

    $ hexdump -s0x680 -n256 a.out\n0000680 5541 5441 8d48 5935 0002 5500 bf53 0001\n0000690 0000 8348 18ec 4864 048b 2825 0000 4800\n00006a0 4489 0824 c031 a5e8 ffff 48ff 748d 0424\n00006b0 8d48 493d 0002 3100 e8c0 ffa2 ffff 8d48\n00006c0 3e35 0002 3100 bfc0 0001 0000 7fe8 ffff\n00006d0 8bff 2444 8504 74c0 4c3d 2d8d 0236 0000\n00006e0 bc41 0001 0000 01bd 0000 3100 0fdb 001f\n00006f0 da89 c031 894c bfee 0001 0000 8349 01c4\n0000700 4be8 ffff 8dff 2b04 eb89 c589 6348 2444\n0000710 4c04 e039 da73 0abf 0000 e800 ff10 ffff\n0000720 c031 8b48 244c 6408 3348 250c 0028 0000\n0000730 0b75 8348 18c4 5d5b 5c41 5d41 e8c3 fefe\n0000740 ffff 2e66 1f0f 0084 0000 0000 1f0f 0040\n0000750 ed31 8949 5ed1 8948 48e2 e483 50f0 4c54\n0000760 058d 016a 0000 8d48 f30d 0000 4800 3d8d\n0000770 ff0c ffff 15ff 0866 0020 0ff4 441f 0000\n

    Il est facile de voir la correspondance entre l'assembleur et l'ex\u00e9cutable binaire. Les valeurs 41 55 puis 41 54 puis 48 8d 35 59 se retrouvent directement dans le dump: 5541 5441 8d48. Si les valeurs sont interverties, c'est parce qu'un PC est little-endian (c.f. endianess), les octets de poids faible apparaissent par cons\u00e9quent en premier dans la m\u00e9moire.

    Sous Windows, l'extension des fichiers d\u00e9termine leur type. Un fichier avec l'extension .jpg sera un fichier image du Join Photographic Experts Group et ex\u00e9cuter ce fichier correspond \u00e0 l'ouvrir en utilisant l'application par d\u00e9faut pour visualiser les images de ce type. Un fichier avec l'extension .exe est un ex\u00e9cutable binaire, et il sera ex\u00e9cut\u00e9 en tant que programme par le syst\u00e8me d'exploitation.

    Sous POSIX (Linux, macOS, UNIX), les flags d'un fichier qualifient son type. Le programme ls permet de visualiser les flags du programme Fibonacci que nous avons compil\u00e9\u2009:

    $ ls -al a.out\n-rwxr-xr-x 1 root ftp 8.3K Jul 17 09:53 Fibonacci\n

    Les lettres r-x indiquent\u2009:

    r

    Lecture autoris\u00e9e

    w

    \u00c9criture autoris\u00e9e

    x

    Ex\u00e9cution autoris\u00e9e

    Ce programme peut-\u00eatre ex\u00e9cut\u00e9 par tout le monde, mais modifi\u00e9 que par l'utilisateur root.

    ", "tags": ["hexdump", "root", "main", "Fibonacci"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#entrees-sorties", "title": "Entr\u00e9es sorties", "text": "

    Tout programme doit pouvoir interagir avec son environnement. \u00c0 l'\u00e9poque des t\u00e9l\u00e9scripteurs, un programme interagissait avec un clavier et une imprimante matricielle. Avec l'arriv\u00e9e des syst\u00e8mes d'exploitation, le champ d'action fut r\u00e9duit \u00e0 des entr\u00e9es\u2009:

    • L'entr\u00e9e standard STDIN fournit au programme du contenu qui est g\u00e9n\u00e9ralement fourni par la sortie d'un autre programme.
    • Les arguments du programme ARGV
    • Les variables d'environnement ENVP

    Ainsi qu'\u00e0 des sorties\u2009:

    • La sortie standard STDOUT est g\u00e9n\u00e9ralement affich\u00e9e \u00e0 l'\u00e9cran
    • La sortie d'erreur standard STDERR contient des d\u00e9tails sur les \u00e9ventuelles erreurs d'ex\u00e9cution du programme.

    La figure suivante r\u00e9sume les interactions qu'un programme peut avoir sur son environnement. Les appels syst\u00e8me (syscall) sont des ordres transmis directement au syst\u00e8me d'exploitation. Ils permettent par exemple de lire des fichiers, d'\u00e9crire \u00e0 l'\u00e9cran, de mettre le programme en pause ou de terminer le programme.

    R\u00e9sum\u00e9 des interactions avec un programme

    ", "tags": ["ENVP", "STDOUT", "STDERR", "STDIN", "ARGV"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#signaux", "title": "Signaux", "text": "

    Lorsqu'un programme est en cours d'ex\u00e9cution, il peut recevoir de la part du syst\u00e8me d'exploitation des signaux. Il s'agit d'une notification asynchrone envoy\u00e9e \u00e0 un processus pour lui signaler l'apparition d'un \u00e9v\u00e8nement.

    Si, en utilisant Windows, vous vous rendez dans le gestionnaire de t\u00e2ches et que vous d\u00e9cidez de Terminer une t\u00e2che, le syst\u00e8me d'exploitation envoie un signal au programme lui demandant de se terminer.

    Sous Linux, habituellement, le shell relie certains raccourcis clavier \u00e0 des signaux particuliers\u2009:

    • Ctrl+C envoie le signal SIGINT pour interrompre l'ex\u00e9cution d'un programme
    • Ctrl+Z envoie le signal SIGTSTP pour suspendre l'ex\u00e9cution d'un programme
    • Ctrl+T envoie le signal SIGINFO permettant de visualiser certaines informations li\u00e9es \u00e0 l'ex\u00e9cution du processus.

    Si le programme suivant est ex\u00e9cut\u00e9, il sera bloquant, c'est-\u00e0-dire qu'\u00e0 moins d'envoyer un signal d'interruption, il ne sera pas possible d'interrompre le processus\u2009:

    int main(void)\n{\n    for(;;);\n}\n
    ", "tags": ["SIGINT", "SIGTSTP", "SIGINFO"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#arguments-et-options", "title": "Arguments et options", "text": "

    L'interpr\u00e9teur de commande cmd.exe sous Windows ou bash sous Linux, fonctionne de fa\u00e7on assez similaire. L'invite de commande nomm\u00e9e prompt en anglais invite l'utilisateur \u00e0 entrer une commande. Sous DOS puis sous Windows cet invite de commande ressemble \u00e0 ceci\u2009:

    C:\\>\n

    Sous Linux, le prompt est largement configurable et d\u00e9pend de la distribution install\u00e9e, mais le plus souvent il se termine par le caract\u00e8re $ ou #.

    Une commande d\u00e9bute par le nom de cette derni\u00e8re, qui peut \u00eatre le nom du programme que l'on souhaite ex\u00e9cuter puis vient les arguments et les options.

    • Une option est par convention un argument dont le pr\u00e9fixe est - sous Linux ou / sous Windows m\u00eame si le standard GNU gagne du terrain. Aussi, le consensus le plus large semble \u00eatre le suivant\u2009:
    • Une option peut \u00eatre exprim\u00e9e soit sous format court -o, -v, soit sous format long --output=, --verbose selon qu'elle commence par un ou deux tirets. Une option peut \u00eatre un bool\u00e9enne (pr\u00e9sence ou non de l'option), ou scalaire, c'est-\u00e0-dire \u00eatre associ\u00e9e \u00e0 une valeur --output=foo.o. Les options modifient le comportement interne d'un programme.

    Un argument est une cha\u00eene de caract\u00e8res utilis\u00e9e comme entr\u00e9e au programme. Un programme peut avoir plusieurs arguments.

    En C, c'est au d\u00e9veloppeur de distinguer les options des arguments, car ils sont tous pass\u00e9s par le param\u00e8tre argv:

    #include <stdio.h>\n\nint main(int argc, char *argv[]) {\n    printf(\"Liste des arguments et options pass\u00e9s au programme:\\n\");\n\n    for (size_t i = 0; i < argc; i++) {\n        printf(\"  %u. %s\\n\", i, argv[i]);\n    }\n}\n
    $ argverbose --help -h=12 3.14 'Baguette au beurre' $'\\t-Lait\\n\\t-Viande\\n\\t-Oeufs\\f'\nListe des arguments et options pass\u00e9s au programme :\n0. ./a.out\n1. --help\n2. -h=12\n3. 3.14\n4. Baguette au beurre\n5.  -Lait\n    -Viande\n    -\u0152ufs\n
    ", "tags": ["bash", "cmd.exe", "argv"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#norme-posix", "title": "Norme POSIX", "text": "

    Le standard POSIX d\u00e9crit une fa\u00e7on de distinguer des options pass\u00e9es \u00e0 un programme. Par exemple, le programme cowsay peut \u00eatre param\u00e9tr\u00e9 pour changer son comportement en utilisant des options standards comme -d. La fonction getopt disponible dans la biblioth\u00e8que <unistd.h> permet de facilement interpr\u00e9ter ces options.

    int getopt(int, char * const [], const char *);\n
    ", "tags": ["options", "getopt"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#extension-gnu", "title": "Extension GNU", "text": "

    Malheureusement, la norme POSIX ne sp\u00e9cifie que les options dites courtes (un tiret suivi d'un seul caract\u00e8re). Une extension GNU et son en-t\u00eate <getopt.h> permet l'acc\u00e8s \u00e0 la fonction getopt_long laquelle permet d'interpr\u00e9ter aussi les options longues --version qui sont devenues tr\u00e8s r\u00e9pandues.

    int getopt_long (int argc, char *const *argv, const char *shortopts,\n                 const struct option *longopts, int *longind);\n

    Ci-dessous une possible utilisation de cette fonction\u2009:

    #include <getopt.h>\n#include <stdbool.h>\n#include <stdio.h>\n#include <stdlib.h>\n\ntypedef struct Options {\n    bool is_verbose;\n\n    bool has_add;\n    bool has_append;\n    bool has_delete;\n    bool has_create;\n\n    char *create_name;\n    char *delete_name;\n    char *file_name;\n} Options;\n\nOptions parse_options(int argc, char *argv[])\n{\n    Options options = {0};\n\n    int c;\n    static int verbose_flag;  // Set by --verbose/--brief\n\n    for (;;) {\n        static struct option long_options[] = {\n            // These options set a flag.\n            {\"verbose\", no_argument, &verbose_flag, true},\n            {\"brief\", no_argument, &verbose_flag, false},\n\n            // These options don\u2019t set a flag. We distinguish them by their\n            // indices.\n            {\"add\", no_argument, 0, 'a'},\n            {\"append\", no_argument, 0, 'b'},\n            {\"delete\", required_argument, 0, 'd'},\n            {\"create\", required_argument, 0, 'c'},\n            {\"file\", required_argument, 0, 'f'},\n\n            // Sentinel marking the end of the structure.\n            {0, 0, 0, 0}};\n\n        // getopt_long stores the option index here.\n        int option_index = 0;\n\n        c = getopt_long(argc, argv, \"abc:d:f:\", long_options, &option_index);\n\n        // Detect the end of the options.\n        if (c == -1) break;\n\n        switch (c) {\n            case 'a':\n                options.has_add = true;\n                break;\n\n            case 'b':\n                options.has_append = true;\n                break;\n\n            case 'c':\n                options.create_name = optarg;\n                break;\n\n            case 'd':\n                options.delete_name = optarg;\n                break;\n\n            case 'f':\n                options.file_name = optarg;\n                break;\n\n            case '?':\n                // getopt_long already printed an error message.\n                break;\n\n            default:\n                abort();\n        }\n    }\n    options.is_verbose = verbose_flag;\n\n    // Parses the remaining command line arguments if got any\n    while (optind < argc) printf(\"%s\\n\", argv[optind++]);\n\n    return options;\n}\n\nint main(int argc, char **argv)\n{\n    Options options = parse_options(argc, argv);\n\n    // ...\n}\n
    ", "tags": ["getopt_long"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#windows", "title": "Windows", "text": "

    Windows utilise \u00e0 l'instar de RDOS ou OpenVMS, le caract\u00e8re slash pour identifier ses options. Alors que sous POSIX l'affichage de la liste des fichiers s'\u00e9crira peut-\u00eatre ls -l -s D*, sous Windows on utilisera dir /q d* /o:s.

    ", "tags": ["RDOS"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#fonction-main", "title": "Fonction main", "text": "

    Le standard d\u00e9finit une fonction nomm\u00e9e main comme \u00e9tant la fonction principale appel\u00e9e \u00e0 l'ex\u00e9cution du programme. Or, sur un syst\u00e8me d'exploitation, la fonction main a d\u00e9j\u00e0 \u00e9t\u00e9 appel\u00e9e il y a belle lurette lorsque l'ordinateur a \u00e9t\u00e9 allum\u00e9 et que le BIOS a charg\u00e9 le syst\u00e8me d'exploitation en m\u00e9moire. D\u00e8s lors la fonction main de notre programme Hello World n'est pas la premi\u00e8re, mais est appel\u00e9.

    ", "tags": ["main"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#qui-appelle-main", "title": "Qui appelle main\u2009?", "text": "

    Un ex\u00e9cutable binaire \u00e0 un format particulier appel\u00e9 ELF (Executable and Linkable Format) qui contient un point d'entr\u00e9e qui sera l'adresse m\u00e9moire de d\u00e9but du programme. Sous un syst\u00e8me POSIX ce point d'entr\u00e9e est nomm\u00e9 _init. C'est lui qui est responsable de r\u00e9colter les informations transmises par le syst\u00e8me d'exploitation. Ce dernier transmet sur la pile du programme\u2009:

    • Le nombre d'arguments argc
    • La liste des arguments argv
    • Les variables d'environnements envp
    • Les pointeurs de fichiers sur stdout, stdin, stderr

    C'est la fonction __libc_start_main de la biblioth\u00e8que standard qui a la responsabilit\u00e9 d'appeler la fonction main. Voici son prototype\u2009:

    int __libc_start_main(int (*main) (int, char**, char**),\n    int argc, char** ubp_av,\n    void (*init)(void),\n    void (*fini)(void),\n    void (*rtld_fini)(void),\n    void (*stack_end)\n);\n
    ", "tags": ["stdin", "envp", "main", "argc", "_init", "stderr", "stdout", "argv"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#valeur-de-retour", "title": "Valeur de retour", "text": "

    La fonction main renvoie toujours une valeur de retour qui agit comme le statut de sortie d'un programme (exit status). Sous POSIX et sous Windows, le programme parent s'attend \u00e0 recevoir une valeur 32-bits \u00e0 la fin de l'ex\u00e9cution d'un programme. L'interpr\u00e9tation est la suivante\u2009:

    0

    Succ\u00e8s, le programme s'est termin\u00e9 correctement.

    !0

    Erreur, le programme ne s'est pas termin\u00e9 correctement.

    Par exemple le programme printf retourne dans le cas pr\u00e9cis l'erreur 130\u2009:

    $ printf '%d' 42\n42\n$ echo $?\n0\n\n$ printf '%d' 'I am not a number'\nprintf: I am not a number: invalid number\n$ echo $?\n130\n
    ", "tags": ["main", "printf"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#entrees-sorties-standards", "title": "Entr\u00e9es sorties standards", "text": "

    Le fichier d'en-t\u00eate stdio.h (man stdio) permet de simplifier l'interaction avec les fichiers. Sous Linux et macOS principalement, mais d'une certaine mani\u00e8re \u00e9galement sous Windows, les canaux d'\u00e9changes entre un programme et son h\u00f4te (shell, gestionnaire de fen\u00eatre, autre programme), se font par l'interm\u00e9diaire de fichiers particuliers nomm\u00e9s stdin, stdout et stderr.

    La fonction de base est putchar qui \u00e9crit un caract\u00e8re sur stdout:

    #include <stdio.h>\n\nint main(void) {\n    putchar('H');\n    putchar('e');\n    putchar('l');\n    putchar('l');\n    putchar('o');\n    putchar('\\n');\n}\n

    Bien vite, on pr\u00e9f\u00e8rera utiliser printf qui simplifie le formatage de cha\u00eenes de caract\u00e8res et qui permet \u00e0 l'aide de marqueurs (tokens) de formater des variables\u2009:

    #include <stdio.h>\n\nint main(void) {\n    printf(\"Hello\\v\");\n    printf(\"%d, %s, %f\", 0x12, \"World!\", 3.1415);\n}\n

    Il peut \u00eatre n\u00e9cessaire, surtout lorsqu'il s'agit d'erreurs qui ne concernent pas la sortie standard du programme, d'utiliser le bon canal de communication, c'est-\u00e0-dire stderr au lieu de stdout. La fonction fprintf permet de sp\u00e9cifier le flux standard de sortie\u2009:

    #include <stdio.h>\n\nint main(void) {\n    fprintf(stdout, \"Sortie standard\\n\");\n    fprintf(stderr, \"Sortie d'erreur standard\\n\");\n}\n

    Pourquoi, me direz-vous, faut-il s\u00e9parer la sortie standard du canal d'erreur\u2009? Le plus souvent un programme n'est pas utilis\u00e9 seul, mais en conjonction avec d'autres programmes\u2009:

    $ echo \"Bonjour\" | tr 'A-Za-z' 'N-ZA-Mn-za-m' > data.txt\n$ cat data.txt\nObawbhe\n

    Dans cet exemple ci-dessus, le programme echo prend en argument la cha\u00eene de caract\u00e8re Bonjour qu'il envoie sur la sortie standard. Ce flux de sortie est reli\u00e9 au flux d'entr\u00e9e du programme tr qui effectue une op\u00e9ration de ROT13 et envoie le r\u00e9sultat sur la sortie standard. Ce flux est ensuite redirig\u00e9 sur le fichier data.txt. La commande suivante cat lis le contenu du fichier dont le nom est pass\u00e9 en argument et \u00e9crit le contenu sur la sortie standard.

    Rot13

    Dans le cas o\u00f9 un de ces programmes g\u00e9n\u00e8re une alerte (warning), le texte ne sera pas transmis le long de la cha\u00eene, mais simplement affich\u00e9 sur la console. Il est donc une bonne pratique que d'utiliser le bon flux de sortie\u2009: stdout pour la sortie standard et stderr pour les messages de diagnostic et les erreurs.

    ", "tags": ["stdin", "cat", "data.txt", "printf", "echo", "stderr", "Bonjour", "fprintf", "stdio.h", "stdout", "putchar"]}, {"location": "course-c/25-architecture-and-systems/programs-and-processes/#boucle-dattente", "title": "Boucle d'attente", "text": "

    Comme \u00e9voqu\u00e9, un programme est souvent destin\u00e9 \u00e0 tourner sur un syst\u00e8me d'exploitation. Un programme simple comme celui-ci\u2009:

    int main(void) {\n    for(;;) {}\n}\n

    consommera 100% des ressources du processeur. En d'autres termes, le processeur d\u00e9pensera toute son \u00e9nergie \u00e0 faire 150 millions de calculs par seconde, pour rien. Et les autres processus n'auront que tr\u00e8s peu de ressources disponibles pour tourner.

    Il est grandement pr\u00e9f\u00e9rable d'utiliser des appels syst\u00e8me pour indiquer au noyau du syst\u00e8me d'exploitation que le processus souhaite \u00eatre mis en pause pour un temps donn\u00e9. Le programme suivant utilise la fonction standard sleep pour demander au noyau d'\u00eatre mis en attente pour une p\u00e9riode de temps sp\u00e9cifi\u00e9e en param\u00e8tre.

    #include <unistd.h>\n\nint main(void) {\n    for(;;) {\n        sleep(1 /* seconds */);\n\n        ...\n    }\n}\n

    Alternativement, lorsqu'un programme attend un retour de l'utilisateur par exemple en demandant la saisie au clavier d'informations, le syst\u00e8me d'exploitation est \u00e9galement mis en attente et le processus ne consomme pas de ressources CPU. Le programme ci-dessous attend que l'utilisateur presse la touche entr\u00e9e.

    #include <stdio.h>\n\nint main(void) {\n    for(;;) {\n        getchar();\n        printf(\"Vous avez press\u00e9 la touche enter (\\\\n)\\n\");\n    }\n}\n

    Exercice 1\u2009: La fortune, la vache qui dit et le chat dr\u00f4le

    En rappelant l'historique des derni\u00e8res commandes ex\u00e9cut\u00e9es sur l'ordinateur du professeur pendant qu'il avait le dos tourn\u00e9, vous tombez sur cette commande\u2009:

    $ fortune | cowsay | lolcat\n

    Quelle est sa structure et que fait-elle\u2009?

    ", "tags": ["sleep"]}, {"location": "course-c/27-data-structures/", "title": "Conteneurs de donn\u00e9es", "text": "

    Un conteneur de donn\u00e9es, en programmation informatique, se d\u00e9finit comme une structure sophistiqu\u00e9e con\u00e7ue pour organiser et stocker des \u00e9l\u00e9ments de mani\u00e8re efficiente, facilitant ainsi leur acc\u00e8s, leur modification et leur gestion. \u00c0 travers cette structure, le d\u00e9veloppeur peut manipuler des ensembles de donn\u00e9es sans se soucier des d\u00e9tails basiques de stockage ou de gestion m\u00e9moire, ce qui lui permet de se concentrer sur la logique du programme.

    Il existe une vari\u00e9t\u00e9 de conteneurs, chacun avec des sp\u00e9cificit\u00e9s qui les rendent adapt\u00e9s \u00e0 des situations distinctes. Parmi les plus courants, nous retrouvons les tableaux, o\u00f9 les \u00e9l\u00e9ments sont stock\u00e9s en m\u00e9moire de fa\u00e7on contigu\u00eb, permettant un acc\u00e8s rapide\u2009; les listes cha\u00een\u00e9es, o\u00f9 chaque \u00e9l\u00e9ment pointe vers le suivant, offrant une flexibilit\u00e9 accrue au prix d'une l\u00e9g\u00e8re perte d'efficacit\u00e9\u2009; les piles (stack) et les files (queue), qui r\u00e9gissent l'ordre d'entr\u00e9e et de sortie des \u00e9l\u00e9ments selon des principes bien d\u00e9finis\u2009; et enfin, des structures plus complexes comme les arbres et les graphes, qui permettent de mod\u00e9liser des relations hi\u00e9rarchiques ou des r\u00e9seaux de d\u00e9pendances entre les donn\u00e9es.

    Dans la plupart des langages modernes, le programmeur b\u00e9n\u00e9ficie de biblioth\u00e8ques standard fournissant ces conteneurs, impl\u00e9ment\u00e9s de mani\u00e8re optimale pour r\u00e9pondre aux besoins courants. Ces biblioth\u00e8ques offrent une panoplie d'outils pr\u00eats \u00e0 l'emploi, \u00e9vitant ainsi au d\u00e9veloppeur de r\u00e9inventer la roue. Toutefois, cette facilit\u00e9 d'acc\u00e8s n'est pas universelle. En C, par exemple, bien que reconnu pour sa puissance et sa proximit\u00e9 avec la machine, le langage ne dispose pas, dans sa biblioth\u00e8que standard, d'impl\u00e9mentations de conteneurs de donn\u00e9es de haut niveau. Le d\u00e9veloppeur en C se trouve donc souvent face au choix d\u00e9licat de cr\u00e9er ses propres structures ou de se tourner vers des biblioth\u00e8ques tierces pour b\u00e9n\u00e9ficier de telles fonctionnalit\u00e9s.

    Ainsi, bien que le langage C exige du d\u00e9veloppeur une compr\u00e9hension profonde des m\u00e9canismes sous-jacents, cette connaissance se traduit par une ma\u00eetrise accrue des ressources mat\u00e9rielles, un atout inestimable dans des domaines o\u00f9 l'efficacit\u00e9 est primordiale. Mais il est ind\u00e9niable que l'absence de conteneurs de haut niveau dans la biblioth\u00e8que standard du C repr\u00e9sente un d\u00e9fi suppl\u00e9mentaire, que chaque programmeur doit relever avec rigueur et discernement.

    "}, {"location": "course-c/27-data-structures/dynamic-array/", "title": "Tableau dynamique", "text": ""}, {"location": "course-c/27-data-structures/dynamic-array/#definition", "title": "D\u00e9finition", "text": "

    Un tableau dynamique (aussi nomm\u00e9 vecteur en C++ ou liste en Python) est une structure de donn\u00e9es qui transcende les limitations rigides du tableau classique en offrant une flexibilit\u00e9 d\u2019utilisation accrue. Dans un tableau statique, la taille est d\u00e9termin\u00e9e d\u00e8s l'initialisation et ne peut plus \u00eatre modifi\u00e9e, ce qui impose au d\u00e9veloppeur de pr\u00e9dire, parfois avec difficult\u00e9, la quantit\u00e9 exacte d'espace n\u00e9cessaire. Un tableau dynamique, en revanche, s\u2019ajuste \u00e0 la croissance ou \u00e0 la diminution des donn\u00e9es qu\u2019il contient, r\u00e9pondant ainsi aux besoins \u00e9volutifs du programme.

    Le fonctionnement d\u2019un tableau dynamique repose sur un m\u00e9canisme fondamental\u2009: la gestion dynamique de la m\u00e9moire. Initialement, un tableau dynamique est allou\u00e9 avec une taille d\u00e9termin\u00e9e, souvent modeste, pour accueillir les premiers \u00e9l\u00e9ments. Cependant, lorsque le tableau atteint sa capacit\u00e9 maximale, une nouvelle allocation de m\u00e9moire plus vaste est effectu\u00e9e. Ce processus implique la cr\u00e9ation d'un nouveau tableau plus grand, la copie des \u00e9l\u00e9ments du tableau original vers ce nouveau tableau, et enfin la lib\u00e9ration de la m\u00e9moire allou\u00e9e au tableau initial.

    Cette flexibilit\u00e9 a un co\u00fbt, celui de la performance, car la r\u00e9allocation et la copie des \u00e9l\u00e9ments lors de l'agrandissement du tableau sont des op\u00e9rations co\u00fbteuses en temps, surtout si elles sont fr\u00e9quentes. Pour att\u00e9nuer cet inconv\u00e9nient, les tableaux dynamiques sont souvent con\u00e7us pour doubler leur capacit\u00e9 \u00e0 chaque r\u00e9allocation, r\u00e9duisant ainsi la fr\u00e9quence de ces op\u00e9rations.

    Outre la gestion de la capacit\u00e9, un tableau dynamique offre \u00e9galement des op\u00e9rations d'insertion et de suppression d'\u00e9l\u00e9ments plus souples que celles d'un tableau statique. L\u00e0 o\u00f9 un tableau statique n\u00e9cessiterait de d\u00e9placer manuellement les \u00e9l\u00e9ments pour ins\u00e9rer ou supprimer une valeur, le tableau dynamique g\u00e8re ces op\u00e9rations en interne, rendant son utilisation plus intuitive.

    N\u00e9anmoins, cette puissance de flexibilit\u00e9 s'accompagne d'une exigence de gestion rigoureuse. Le d\u00e9veloppeur doit rester conscient de la mani\u00e8re dont la m\u00e9moire est utilis\u00e9e, surtout dans des langages comme le C o\u00f9 le contr\u00f4le manuel de l'allocation et de la lib\u00e9ration de la m\u00e9moire est requis. Une mauvaise gestion de ces aspects peut conduire \u00e0 des fuites de m\u00e9moire ou \u00e0 des inefficacit\u00e9s critiques.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#exemple", "title": "Exemple", "text": "

    Prenons un exemple simple pour illustrer le concept de tableau dynamique en C. Imaginons que nous souhaitons cr\u00e9er un buffer pour stocker une s\u00e9quence de trois caract\u00e8res\u2009:

    char *buffer = malloc(3);\n\nbuffer[0] = 'h';\nbuffer[1] = 'e';\nbuffer[2] = 'l'; // Le buffer est plein...\n

    \u00c0 ce stade, le buffer a atteint sa capacit\u00e9 maximale. Si nous souhaitons ajouter davantage de caract\u00e8res, il nous faut augmenter la taille du buffer en r\u00e9allouant dynamiquement de l'espace m\u00e9moire\u2009:

    // Augmente dynamiquement la taille du buffer \u00e0 5 caract\u00e8res\nchar *tmp = realloc(buffer, 5);\nassert(tmp != NULL);\nbuffer = tmp;\n\n// Continue de remplir le buffer\nbuffer[3] = 'l';\nbuffer[4] = 'o'; // Le buffer est \u00e0 nouveau plein...\n

    Apr\u00e8s avoir utilis\u00e9 le buffer, il est crucial de lib\u00e9rer l'espace m\u00e9moire allou\u00e9 pour \u00e9viter les fuites de m\u00e9moire\u2009:

    free(buffer);\n

    Lors de la r\u00e9allocation, la taille du nouvel espace m\u00e9moire est souvent augment\u00e9e selon un facteur de croissance pr\u00e9d\u00e9fini. Ce facteur varie g\u00e9n\u00e9ralement entre 1,5 et 2, selon le langage de programmation ou le compilateur utilis\u00e9. Ainsi, si l\u2019on suit un facteur de 2, les tailles successives du tableau pourraient \u00eatre 1, 2, 4, 8, 16, 32, et ainsi de suite.

    Il est \u00e9galement possible de r\u00e9duire la taille allou\u00e9e lorsque le nombre d'\u00e9l\u00e9ments devient significativement inf\u00e9rieur \u00e0 la taille effective du tableau. Toutefois, en pratique, cette op\u00e9ration est rarement mise en \u0153uvre, car elle peut s\u2019av\u00e9rer inefficace et co\u00fbteuse en termes de performance, comme l\u2019explique cette r\u00e9ponse sur StackOverflow.

    Ainsi, la gestion dynamique de la m\u00e9moire \u00e0 travers des op\u00e9rations de r\u00e9allocation permet une flexibilit\u00e9 pr\u00e9cieuse, mais elle n\u00e9cessite une gestion attentive pour \u00e9viter des inefficacit\u00e9s et des probl\u00e8mes de performances.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#anatomie", "title": "Anatomie", "text": "

    Un tableau dynamique est repr\u00e9sent\u00e9 en m\u00e9moire comme un contenu s\u00e9quentiel qui poss\u00e8de un d\u00e9but et une fin. On appelle son d\u00e9but la t\u00eate (head) ou front et la fin du tableau, sa queue (tail) ou back. Selon que l'on souhaite ajouter des \u00e9l\u00e9ments au d\u00e9but ou \u00e0 la fin du tableau, la complexit\u00e9 n'est pas la m\u00eame.

    Nous d\u00e9finirons par la suite les op\u00e9rations suivantes\u2009:

    Op\u00e9rations sur un tableau dynamique

    Vocabulaire des actions sur un tableau dynamique Action Terme technique Ajout d'un \u00e9l\u00e9ment \u00e0 la t\u00eate du tableau push-front, unshift Ajout d'un \u00e9l\u00e9ment \u00e0 la queue du tableau push-back, push Suppression d'un \u00e9l\u00e9ment \u00e0 la t\u00eate du tableau pop-front, shift Suppression d'un \u00e9l\u00e9ment \u00e0 la queue du tableau pop-back, pop Insertion d'un \u00e9l\u00e9ment \u00e0 la position n insert Suppression d'un \u00e9l\u00e9ment \u00e0 la position n delete

    Nous comprenons rapidement qu'il est plus compliqu\u00e9 d'ajouter ou de supprimer un \u00e9l\u00e9ment depuis la t\u00eate du tableau, car il est n\u00e9cessaire ensuite de d\u00e9placer chaque \u00e9l\u00e9ment (l'\u00e9l\u00e9ment 0 devient l'\u00e9l\u00e9ment 1, l'\u00e9l\u00e9ment 1 devient l'\u00e9l\u00e9ment 2...).

    Un tableau dynamique peut \u00eatre repr\u00e9sent\u00e9 par la figure suivante\u2009:

    Tableau dynamique

    Un espace m\u00e9moire est r\u00e9serv\u00e9 dynamiquement sur le tas. Comme malloc ne retourne pas la taille de l'espace m\u00e9moire allou\u00e9, mais juste un pointeur sur cet espace, il est n\u00e9cessaire de conserver dans une variable la capacit\u00e9 du tableau. Notons qu'un tableau de 10 int32_t repr\u00e9sentera un espace m\u00e9moire de 4x10 bytes, soit 40 bytes. La m\u00e9moire ainsi r\u00e9serv\u00e9e par malloc n'est g\u00e9n\u00e9ralement pas vide, mais elle contient des valeurs, vestige d'une ancienne allocation m\u00e9moire d'un autre programme depuis que l'ordinateur a \u00e9t\u00e9 allum\u00e9. Pour conna\u00eetre le nombre d'\u00e9l\u00e9ments effectifs du tableau, il faut \u00e9galement le m\u00e9moriser. Enfin, le pointeur sur l'espace m\u00e9moire est aussi m\u00e9moris\u00e9.

    Les composants de cette structure de donn\u00e9e sont donc\u2009:

    La capacit\u00e9

    Un entier non sign\u00e9 size_t repr\u00e9sentant la capacit\u00e9 de stockage totale du tableau dynamique \u00e0 un instant T lorsqu'il est plein.

    Le nombre d'\u00e9l\u00e9ments

    Un entier non sign\u00e9 size_t repr\u00e9sentant le nombre d'\u00e9l\u00e9ments effectifs dans le tableau.

    Un pointeur de donn\u00e9es

    Un pointeur sur un entier int * contenant l'adresse m\u00e9moire de l'espace allou\u00e9 par malloc.

    Les donn\u00e9es

    Un espace m\u00e9moire allou\u00e9 par malloc et contenant les donn\u00e9es.

    Il peut \u00eatre d\u00e9clar\u00e9 sous forme de structure\u2009:

    typedef struct vector {\n    size_t capacity;\n    size_t size;\n    int* data;\n} Vector```\n
    ", "tags": ["malloc", "shift", "pop", "int32_t", "size_t", "insert", "unshift", "push", "delete"]}, {"location": "course-c/27-data-structures/dynamic-array/#pop-pop_back", "title": "Pop (pop_back)", "text": "

    L'op\u00e9ration pop retire l'\u00e9l\u00e9ment de la fin du tableau. Le nombre d'\u00e9l\u00e9ments est donc ajust\u00e9 en cons\u00e9quence.

    Suppression d'un \u00e9l\u00e9ment dans un tableau dynamique

    if (elements <= 0) exit(EXIT_FAILURE);\nint value = data[--elements];\n
    ", "tags": ["pop"]}, {"location": "course-c/27-data-structures/dynamic-array/#push-push_back", "title": "Push (push_back)", "text": "

    L'op\u00e9ration push ajoute un \u00e9l\u00e9ment \u00e0 la fin du tableau.

    Ajout d'un \u00e9l\u00e9ment dans un tableau dynamique

    if (elements >= capacity) exit(EXIT_FAILURE);\ndata[elements++] = value;\n
    ", "tags": ["push"]}, {"location": "course-c/27-data-structures/dynamic-array/#shift-pop_front", "title": "Shift (pop_front)", "text": "

    L'op\u00e9ration shift retire un \u00e9l\u00e9ment depuis le d\u00e9but. L'op\u00e9ration \u00e0 une complexit\u00e9 de O(n) puisqu'\u00e0 chaque op\u00e9ration il est n\u00e9cessaire de d\u00e9placer chaque \u00e9l\u00e9ment qu'il contient.

    Suppression du premier \u00e9l\u00e9ment dans un tableau dynamique

    if (elements <= 0) exit(EXIT_FAILURE);\nint value = data[0];\nfor (int k = 0; k < capacity; k++)\n    data[k] = data[k+1];\n

    Une optimisation peut \u00eatre faite en d\u00e9pla\u00e7ant le pointeur de donn\u00e9e de 1 permettant de r\u00e9duite la complexit\u00e9 \u00e0 O(1) :

    if (elements <= 0) exit(EXIT_FAILURE);\nif (capacity <= 0) exit(EXIT_FAILURE);\nint value = data[0];\ndata++;\ncapacity--;\n
    ", "tags": ["shift"]}, {"location": "course-c/27-data-structures/dynamic-array/#unshift-push_front", "title": "Unshift (push_front)", "text": "

    Enfin, l'op\u00e9ration unshift ajoute un \u00e9l\u00e9ment depuis le d\u00e9but du tableau\u2009:

    Ajout d'un \u00e9l\u00e9ment en d\u00e9but d'un tableau dynamique

    for (int k = elements; k >= 1; k--)\n    data[k] = data[k - 1];\ndata[0] = value;\n

    Dans le cas ou le nombre d'\u00e9l\u00e9ments atteint la capacit\u00e9 maximum du tableau, il est n\u00e9cessaire de r\u00e9allouer l'espace m\u00e9moire avec realloc. G\u00e9n\u00e9ralement on se contente de doubler l'espace allou\u00e9.

    if (elements >= capacity) {\n    data = realloc(data, capacity *= 2);\n}\n
    ", "tags": ["realloc", "unshift"]}, {"location": "course-c/27-data-structures/dynamic-array/#analyse-de-la-complexite", "title": "Analyse de la complexit\u00e9", "text": "Acc\u00e8s par index \\(O(1)\\)

    Cette op\u00e9ration est rapide et s'effectue en temps constant car les tableaux dynamiques, comme les tableaux statiques, permettent un acc\u00e8s direct \u00e0 chaque \u00e9l\u00e9ment via leur index.

    Push \\(O(1)\\) amorti

    En g\u00e9n\u00e9ral, ajouter un \u00e9l\u00e9ment \u00e0 la fin d'un tableau dynamique prend un temps constant \\(O(1)\\). Cependant, si la capacit\u00e9 du tableau est atteinte, le tableau doit \u00eatre redimensionn\u00e9, ce qui implique de copier tous les \u00e9l\u00e9ments existants vers un nouveau tableau, ce qui a un co\u00fbt \\(O(n)\\). Gr\u00e2ce \u00e0 l'amortissement, la complexit\u00e9 moyenne reste \\(O(1)\\) sur une s\u00e9rie d'op\u00e9rations d'insertion.

    Insertion au d\u00e9but (unshift) ou au milieu \\(O(n)\\)

    Ins\u00e9rer un \u00e9l\u00e9ment au d\u00e9but ou au milieu du tableau n\u00e9cessite de d\u00e9placer les \u00e9l\u00e9ments existants, ce qui implique un co\u00fbt proportionnel au nombre d'\u00e9l\u00e9ments \\(O(n)\\).

    Pop \\(O(1)\\)

    La suppression du dernier \u00e9l\u00e9ment est une op\u00e9ration constante, puisqu'il n'y a pas besoin de r\u00e9arranger les autres \u00e9l\u00e9ments, ou bien r\u00e9server davantage d'espace m\u00e9moire.

    Suppression au d\u00e9but (shift) ou au milieu \\(O(n)\\)

    Comme pour l'insertion, la suppression d'un \u00e9l\u00e9ment en d\u00e9but ou au milieu du tableau n\u00e9cessite de d\u00e9caler les \u00e9l\u00e9ments restants, ce qui entra\u00eene une complexit\u00e9 de \\(O(n)\\).

    Lors d'un redimensionnement il faut parfois copier les \u00e9l\u00e9ments dans un nouveau tableau si realloc ne parviens pas \u00e0 agrandir l'espace existant, ce qui a un co\u00fbt de \\(O(n)\\). Toutefois, cette op\u00e9ration ne se produit que de temps en temps. On dit que le co\u00fbt d'un redimensionnement est est amorti sur les nombreuses op\u00e9rations d'insertion. Prenons un exemple.

    \u00c9lements, Capacit\u00e9, Redimensionnements\n\n1         1\n2         2         1 (2)\n3         4         2 (4)\n4         4\n5         8         3 (8)\n6         8\n7         8\n8         8\n9..16     16        4 (16)\n17..32    32        5 (32)\n33..64    64        6 (64)\n

    Si on insert 64 \u00e9l\u00e9ments on a du redimensionner le tableau 6 fois et on \u00e0 du copier \\(64 + 32 + 16 + 8 + 4 + 2 = 126\\) \u00e9l\u00e9ments en totalit\u00e9. En moyenne c'est comme si chaque insertion avait co\u00fbt\u00e9 \\(126 / 64 \\approx 2\\), soit une constante que l'on \u00e9crit \\(O(1)\\). Comme ce co\u00fbt est amorti sur l'ensemble des \u00e9l\u00e9ments, on dit que la complexit\u00e9 d'insertion est de \\(O(1)\\) amorti.

    ", "tags": ["realloc"]}, {"location": "course-c/27-data-structures/dynamic-array/#pile", "title": "Pile", "text": "

    Une pile est une structure de donn\u00e9es utilis\u00e9e pour empiler des donn\u00e9es de mani\u00e8re temporaire. C'est ce que l'on appelle un LIFO (Last In, First Out).

    Pile ou *stack*

    Nous avons vu que certaines op\u00e9rations sont plus on\u00e9reuses que d'autres. Les op\u00e9rations push-back et pop-back sont les moins gourmandes. Puisque la pile n'a que deux op\u00e9rations, on peut en tirer parti.

    Une pile peut donc \u00eatre impl\u00e9ment\u00e9e sous forme d'un tableau dynamique dans lequel on ne conserverai que deux op\u00e9rations.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#queue", "title": "Queue", "text": "

    Une queue est une structure de donn\u00e9es utilis\u00e9e comme file d'attente. C'est ce que l'on nomme un FIFO (First In, First Out). Les \u00e9l\u00e9ments sont ajout\u00e9s \u00e0 la fin et retir\u00e9s au d\u00e9but. N\u00e9anmoins, afin d'\u00e9viter le d\u00e9placement constant des \u00e9l\u00e9ments en \\(O(n)\\), une file circulaire est souvent utilis\u00e9e.

    "}, {"location": "course-c/27-data-structures/dynamic-array/#buffer-circulaire", "title": "Buffer circulaire", "text": "

    Un tampon circulaire aussi appel\u00e9 buffer circulaire ou ring buffer en anglais est g\u00e9n\u00e9ralement d'une taille fixe et poss\u00e8de deux pointeurs. L'un pointant sur le dernier \u00e9l\u00e9ment (tail) et l'un sur le premier \u00e9l\u00e9ment (head).

    Lorsqu'un \u00e9l\u00e9ment est supprim\u00e9 du buffer, le pointeur de fin est incr\u00e9ment\u00e9. Lorsqu'un \u00e9l\u00e9ment est ajout\u00e9, le pointeur de d\u00e9but est incr\u00e9ment\u00e9.

    Pour permettre la circulation, les indices sont calcul\u00e9s modulo la taille du buffer.

    Il est possible de repr\u00e9senter sch\u00e9matiquement ce buffer comme un cercle et ses deux pointeurs\u2009:

    Exemple d'un tampon circulaire

    Le nombre d'\u00e9l\u00e9ments dans le buffer est la diff\u00e9rence entre le pointeur de t\u00eate et le pointeur de queue, modulo la taille du buffer. N\u00e9anmoins, l'op\u00e9rateur % en C ne fonctionne que sur des nombres positifs et ne retourne pas le r\u00e9sidu positif le plus petit. En sommes, -2 % 5 devrait donner 3, ce qui est le cas en Python, mais en C, en C++ ou en PHP la valeur retourn\u00e9e est -2. Le modulo vrai, math\u00e9matiquement correct, peut \u00eatre calcul\u00e9 ainsi\u2009:

    ((A % M) + M) % M\n

    Les indices sont boucl\u00e9s sur la taille du buffer, l'\u00e9l\u00e9ment suivant est donc d\u00e9fini par\u2009:

    (i + 1) % SIZE\n

    Voici une impl\u00e9mentation possible du buffer circulaire\u2009:

    #define SIZE 16\n#define MOD(A, M) (((A % M) + M) % M)\n#define NEXT(A) (((A) + 1) % SIZE)\n\ntypedef struct Ring {\n    int buffer[SIZE];\n    int head;\n    int tail;\n} Ring;\n\nvoid init(Ring *ring) {\n    ring->head = ring->tail = 0;\n}\n\nint count(Ring *ring) {\n    return MOD(ring->head - ring->tail, size);\n}\n\nbool is_full(Ring *ring) {\n    return count(ring) == SIZE - 1;\n}\n\nbool is_empty(Ring *ring) {\n    return ring->tail == ring->head;\n}\n\nint* enqueue(Ring *ring, int value) {\n    if (is_full(ring)) return NULL;\n    ring->buffer[ring->head] = value;\n    int *el = &ring->buffer[ring->head];\n    ring->head = NEXT(ring->head);\n    return el;\n}\n\nint* dequeue(Ring *ring) {\n    if (is_empty(ring)) return NULL;\n    int *el = &ring->buffer[ring->tail];\n    ring->tail = NEXT(ring->tail);\n    return el;\n}\n
    "}, {"location": "course-c/27-data-structures/graphs/", "title": "Graphes", "text": "

    Un graphe est un ensemble de sommets reli\u00e9s par des ar\u00eates. Les graphes sont utilis\u00e9s pour mod\u00e9liser des relations entre des objets. Par exemple, un graphe peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau social, un r\u00e9seau de transport, un r\u00e9seau de distribution, etc.

    C'est une variante g\u00e9n\u00e9rale des arbres. Un arbre est un graphe particulier o\u00f9 chaque sommet est reli\u00e9 \u00e0 un autre sommet par un chemin unique. Un graphe peut avoir des cycles, c'est-\u00e0-dire des chemins qui reviennent \u00e0 leur point de d\u00e9part.

    "}, {"location": "course-c/27-data-structures/graphs/#types-de-graphes", "title": "Types de graphes", "text": ""}, {"location": "course-c/27-data-structures/graphs/#forets", "title": "For\u00eats", "text": "

    Un graphe sans cycle est appel\u00e9 une for\u00eat. Une for\u00eat est un ensemble d'arbres. Un arbre est un graphe connexe sans cycle.

    Exemple de for\u00eat

    ", "tags": ["connexe", "graphe", "foret"]}, {"location": "course-c/27-data-structures/graphs/#graphes-orientes", "title": "Graphes orient\u00e9s", "text": "

    Un graphe orient\u00e9 est un graphe dont les ar\u00eates ont une direction. Les graphes orient\u00e9s sont utilis\u00e9s pour mod\u00e9liser des relations asym\u00e9triques. Par exemple, un graphe orient\u00e9 peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau de transport o\u00f9 les ar\u00eates repr\u00e9sentent des routes \u00e0 sens unique.

    Exemple de graph orient\u00e9

    "}, {"location": "course-c/27-data-structures/graphs/#graphes-ponderes", "title": "Graphes pond\u00e9r\u00e9s", "text": "

    Un graphe pond\u00e9r\u00e9 est un graphe dont les ar\u00eates ont un poids. Les graphes pond\u00e9r\u00e9s sont utilis\u00e9s pour mod\u00e9liser des relations quantitatives. Par exemple, un graphe pond\u00e9r\u00e9 peut \u00eatre utilis\u00e9 pour repr\u00e9senter un r\u00e9seau de transport o\u00f9 les ar\u00eates repr\u00e9sentent des routes avec une longueur ou un co\u00fbt associ\u00e9.

    Exemple de graph pond\u00e9r\u00e9

    "}, {"location": "course-c/27-data-structures/graphs/#graphes-bipartis", "title": "Graphes bipartis", "text": "

    Un graphe biparti est un graphe dont les sommets peuvent \u00eatre divis\u00e9s en deux ensembles disjoints. Les ar\u00eates d'un graphe biparti relient les sommets des deux ensembles. Les graphes bipartis sont utilis\u00e9s pour mod\u00e9liser des relations binaires. Par exemple, un graphe biparti peut \u00eatre utilis\u00e9 pour repr\u00e9senter des relations d'adjacence entre deux ensembles d'objets.

    Exemple de graphe biparti

    "}, {"location": "course-c/27-data-structures/graphs/#representation-des-graphes", "title": "Repr\u00e9sentation des graphes", "text": "

    Il existe plusieurs fa\u00e7ons de repr\u00e9senter un graphe en m\u00e9moire. Les deux repr\u00e9sentations les plus courantes sont les listes d'adjacence et les matrices d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#liste-dadjacence", "title": "Liste d'adjacence", "text": "

    Dans une liste d'adjacence, chaque sommet est associ\u00e9 \u00e0 une liste de ses voisins. Une liste d'adjacence est une structure de donn\u00e9es dynamique qui permet d'ajouter et de supprimer des ar\u00eates facilement. Cependant, elle n\u00e9cessite plus de m\u00e9moire que les matrices d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#matrice-dadjacence", "title": "Matrice d'adjacence", "text": "

    Dans une matrice d'adjacence, chaque sommet est associ\u00e9 \u00e0 une ligne et une colonne de la matrice. La valeur de la case (i, j) de la matrice indique s'il existe une ar\u00eate entre les sommets i et j. Une matrice d'adjacence est une structure de donn\u00e9es statique qui permet de v\u00e9rifier rapidement l'existence d'une ar\u00eate. Cependant, elle n\u00e9cessite plus de m\u00e9moire que les listes d'adjacence.

    "}, {"location": "course-c/27-data-structures/graphs/#parcours-de-graphes", "title": "Parcours de graphes", "text": "

    Il existe plusieurs algorithmes pour parcourir un graphe. Les deux algorithmes les plus courants sont le parcours en profondeur (DFS) et le parcours en largeur (BFS).

    "}, {"location": "course-c/27-data-structures/graphs/#dfs", "title": "DFS", "text": "

    Le parcours en profondeur (Depth-First Search) est un algorithme r\u00e9cursif qui explore le graphe en profondeur. Il commence par un sommet de d\u00e9part et explore tous les sommets accessibles depuis ce sommet avant de passer au suivant. L'algorithme DFS est utilis\u00e9 pour trouver des cycles dans un graphe, pour v\u00e9rifier la connexit\u00e9 d'un graphe, pour trouver des composantes fortement connexes, etc.

    void dfs(int u) {\n    visited[u] = true;\n    for (int v : adj[u]) {\n        if (!visited[v]) {\n            dfs(v);\n        }\n    }\n}\n
    "}, {"location": "course-c/27-data-structures/graphs/#bfs", "title": "BFS", "text": "

    Le parcours en largeur (Breadth-First Search) est un algorithme it\u00e9ratif qui explore le graphe en largeur. Il commence par un sommet de d\u00e9part et explore tous les sommets \u00e0 une distance k avant de passer \u00e0 la distance k+1. L'algorithme BFS est utilis\u00e9 pour trouver le plus court chemin entre deux sommets, pour trouver le nombre de composantes connexes, pour trouver le nombre de sommets \u00e0 une distance donn\u00e9e, etc.

    void bfs(int u) {\n    Queue q = INIT_QUEUE;\n    q.push(u);\n    visited[u] = true;\n    while (!q.empty()) {\n        int v = q.front();\n        q.pop();\n        for (int w : adj[v]) {\n            if (!visited[w]) {\n                q.push(w);\n                visited[w] = true;\n            }\n        }\n    }\n}\n
    "}, {"location": "course-c/27-data-structures/graphs/#dijkstra", "title": "Dijkstra", "text": "

    L'algorithme de Dijkstra est un algorithme qui permet de trouver le plus court chemin entre un sommet de d\u00e9part et tous les autres sommets d'un graphe pond\u00e9r\u00e9. L'algorithme de Dijkstra est bas\u00e9 sur le parcours en largeur et utilise une file de priorit\u00e9 pour explorer les sommets dans l'ordre croissant de leur distance par rapport au sommet de d\u00e9part.

    void dijkstra(int u) {\n    PriorityQueue pq = INIT_PRIORITY_QUEUE;\n    pq.push({0, u});\n    dist[u] = 0;\n    while (!pq.empty()) {\n        int d = -pq.top().first;\n        int v = pq.top().second;\n        pq.pop();\n        if (d > dist[v]) continue;\n        for (auto [w, c] : adj[v]) {\n            if (dist[v] + c < dist[w]) {\n                dist[w] = dist[v] + c;\n                pq.push({-dist[w], w});\n            }\n        }\n    }\n}\n
    "}, {"location": "course-c/27-data-structures/lists/", "title": "Lists", "text": ""}, {"location": "course-c/27-data-structures/lists/#listes-chainees", "title": "Listes cha\u00een\u00e9es", "text": ""}, {"location": "course-c/27-data-structures/lists/#definition", "title": "D\u00e9finition", "text": "

    Les listes cha\u00een\u00e9es, en informatique, repr\u00e9sentent une structure de donn\u00e9es d'une \u00e9l\u00e9gance discr\u00e8te, dont la simplicit\u00e9 apparente dissimule une puissance d'adaptation remarquable. Contrairement aux tableaux dynamiques, qui d\u00e9pendent d'une allocation contigu\u00eb en m\u00e9moire et n\u00e9cessitent des r\u00e9allocations co\u00fbteuses lorsque leur capacit\u00e9 est d\u00e9pass\u00e9e, les listes cha\u00een\u00e9es se distinguent par leur nature flexible et d\u00e9centralis\u00e9e.

    Une liste cha\u00een\u00e9e se compose d'une s\u00e9rie de n\u0153uds, chacun contenant un \u00e9l\u00e9ment de donn\u00e9es ainsi qu'un pointeur vers le n\u0153ud suivant. Cette architecture singuli\u00e8re permet \u00e0 la liste de cro\u00eetre et de se contracter sans n\u00e9cessiter de r\u00e9allocation massive de m\u00e9moire\u2009: il suffit d'ajouter ou de retirer des n\u0153uds au gr\u00e9 des besoins, sans d\u00e9placer les \u00e9l\u00e9ments existants. En d'autres termes, la m\u00e9moire n'est allou\u00e9e que lorsque cela est n\u00e9cessaire, \u00e9liminant ainsi les g\u00e2chis d'espace que peuvent entra\u00eener les tableaux dynamiques lorsque ceux-ci sont sous-utilis\u00e9s.

    L\u2019avantage principal de cette structure r\u00e9side dans sa capacit\u00e9 \u00e0 ins\u00e9rer ou supprimer des \u00e9l\u00e9ments avec une efficacit\u00e9 redoutable. L\u00e0 o\u00f9 un tableau dynamique doit parfois d\u00e9placer de grandes portions de donn\u00e9es pour ins\u00e9rer ou retirer un \u00e9l\u00e9ment, une liste cha\u00een\u00e9e ne demande que la modification des quelques pointeurs concern\u00e9s. Cette op\u00e9ration, d'une l\u00e9g\u00e8ret\u00e9 exemplaire, conf\u00e8re \u00e0 la liste cha\u00een\u00e9e une fluidit\u00e9 dans la manipulation des donn\u00e9es que les tableaux, m\u00eame dynamiques, ne sauraient \u00e9galer.

    Cependant, cette flexibilit\u00e9 n'est pas sans contrepartie. L'acc\u00e8s direct \u00e0 un \u00e9l\u00e9ment particulier est plus lent dans une liste cha\u00een\u00e9e que dans un tableau, car il faut parcourir les n\u0153uds un \u00e0 un, en suivant les pointeurs. Cette absence d'acc\u00e8s index\u00e9, qui fait la force du tableau, devient ici une faiblesse relative, surtout pour les op\u00e9rations qui n\u00e9cessitent de fr\u00e9quentes consultations des donn\u00e9es.

    "}, {"location": "course-c/27-data-structures/lists/#exemple", "title": "Exemple", "text": "

    Pour illustrer cette id\u00e9e, imaginons un tableau statique dans lequel chaque \u00e9l\u00e9ment est d\u00e9crit par la structure suivante\u2009:

    struct Element {\n    int value;\n    int index_next_element;\n};\n\nstruct Element elements[100];\n

    Consid\u00e9rons les dix premiers \u00e9l\u00e9ments de la s\u00e9quence de nombre A130826 dans un tableau statique. Ensuite, r\u00e9partissons ces valeurs al\u00e9atoirement dans notre tableau elements d\u00e9clar\u00e9 plus haut entre les indices 0 et 19.

    Construction d'une liste chain\u00e9e \u00e0 l'aide d'un tableau

    On observe sur la figure ci-dessus que les \u00e9l\u00e9ments n'ont plus besoin de se suivre en m\u00e9moire, car il est possible facilement de chercher l'\u00e9l\u00e9ment suivant de la liste avec cette relation\u2009:

    struct Element current = elements[4];\nstruct Element next = elements[current.index_next_element]\n

    De m\u00eame, ins\u00e9rer une nouvelle valeur 13 apr\u00e8s la valeur 42 est tr\u00e8s facile\u2009:

    // Recherche de l'\u00e9l\u00e9ment contenant la valeur 42\nstruct Element el = elements[0];\nwhile (el.value != 42 && el.index_next_element != -1) {\n    el = elements[el.index_next_element];\n}\nif (el.value != 42) abort();\n\n// Recherche d'un \u00e9l\u00e9ment libre\nconst int length = sizeof(elements) / sizeof(elements[0]);\nint k;\nfor (k = 0; k < length; k++)\n    if (elements[k].index_next_element == -1)\n        break;\nassert(k < length && elements[k].index_next_element == -1);\n\n// Cr\u00e9ation d'un nouvel \u00e9l\u00e9ment\nstruct Element new = (Element){\n    .value = 13,\n    .index_next_element = -1\n};\n\n// Insertion de l'\u00e9l\u00e9ment quelque part dans le tableau\nel.index_next_element = k;\nelements[el.index_next_element] = new;\n

    Cette solution d'utiliser un lien vers l'\u00e9l\u00e9ment suivant et s'appelle liste cha\u00een\u00e9e. Chaque \u00e9l\u00e9ment dispose d'un lien vers l'\u00e9l\u00e9ment suivant situ\u00e9 quelque part en m\u00e9moire. Les op\u00e9rations d'insertion et de suppression au milieu de la cha\u00eene sont maintenant effectu\u00e9es en \\(O(1)\\) contre \\(O(n)\\) pour un tableau standard. En revanche l'espace n\u00e9cessaire pour stocker ce tableau est doubl\u00e9 puisqu'il faut associer \u00e0 chaque valeur le lien vers l'\u00e9l\u00e9ment suivant.

    D'autre part, la solution propos\u00e9e n'est pas optimale\u2009:

    • L'\u00e9l\u00e9ment 0 est un cas particulier qu'il faut traiter diff\u00e9remment. Le premier \u00e9l\u00e9ment de la liste doit toujours \u00eatre positionn\u00e9 \u00e0 l'indice 0 du tableau. Ins\u00e9rer un nouvel \u00e9l\u00e9ment en d\u00e9but de tableau demande de d\u00e9placer cet \u00e9l\u00e9ment ailleurs en m\u00e9moire.
    • Rechercher un \u00e9l\u00e9ment libre prend du temps.
    • Supprimer un \u00e9l\u00e9ment dans le tableau laisse une place m\u00e9moire vide. Il devient alors difficile de savoir o\u00f9 sont les emplacements m\u00e9moires disponibles.

    Une liste cha\u00een\u00e9e est une structure de donn\u00e9es permettant de lier des \u00e9l\u00e9ments structur\u00e9s entre eux. La liste est caract\u00e9ris\u00e9e par\u2009:

    • un \u00e9l\u00e9ment de t\u00eate (head),
    • un \u00e9l\u00e9ment de queue (tail).

    Un \u00e9l\u00e9ment est caract\u00e9ris\u00e9 par\u2009:

    • un contenu (payload),
    • une r\u00e9f\u00e9rence vers l'\u00e9l\u00e9ment suivant et/ou pr\u00e9c\u00e9dent dans la liste.

    Les listes cha\u00een\u00e9es r\u00e9duisent la complexit\u00e9 li\u00e9e \u00e0 la manipulation d'\u00e9l\u00e9ments dans une liste. L'empreinte m\u00e9moire d'une liste cha\u00een\u00e9e est plus grande qu'avec un tableau, car \u00e0 chaque \u00e9l\u00e9ment de donn\u00e9e est associ\u00e9 un pointeur vers l'\u00e9l\u00e9ment suivant ou pr\u00e9c\u00e9dent.

    Ce surco\u00fbt est souvent part du compromis entre la complexit\u00e9 d'ex\u00e9cution du code et la m\u00e9moire utilis\u00e9e par ce programme.

    Co\u00fbt des op\u00e9rations dans des structures de donn\u00e9es r\u00e9cursives Structure de donn\u00e9e Pire cas Insertion Suppression Recherche (Tri\u00e9) Recherche (Non tri\u00e9) Tableau, pile, queue \\(O(n)\\) \\(O(n)\\) \\(O(\\log(n))\\) \\(O(n)\\) Liste cha\u00een\u00e9e simple \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\)", "tags": ["elements"]}, {"location": "course-c/27-data-structures/lists/#liste-simplement-chainee-linked-list", "title": "Liste simplement cha\u00een\u00e9e (linked-list)", "text": "

    La figure suivante illustre un set d'\u00e9l\u00e9ments li\u00e9s entre eux \u00e0 l'aide d'un pointeur rattach\u00e9 \u00e0 chaque \u00e9l\u00e9ment. On peut s'imaginer que chaque \u00e9l\u00e9ment peut se situer n'importe o\u00f9 en m\u00e9moire et qu'il n'est alors pas indispensable que les \u00e9l\u00e9ments se suivent dans l'ordre.

    Il est indispensable de bien identifier le dernier \u00e9l\u00e9ment de la liste gr\u00e2ce \u00e0 son pointeur associ\u00e9 \u00e0 la valeur NULL.

    Liste cha\u00een\u00e9e simple

    #include <stdio.h>\n#include <stdlib.h>\n\nstruct Point\n{\n    double x;\n    double y;\n    double z;\n};\n\nstruct Element\n{\n    struct Point point;\n    struct Element* next;\n};\n\nint main(void)\n{\n    struct Element a = {.point = {1,2,3}, .next = NULL};\n    struct Element b = {.point = {4,5,6}, .next = &a};\n    struct Element c = {.point = {7,8,9}, .next = &b};\n\n    a.next = &c;\n\n    struct Element* walk = &a;\n\n    for (size_t i = 0; i < 10; i++)\n    {\n        printf(\"%d. P(x, y, z) = %0.2f, %0.2f, %0.2f\\n\",\n            i,\n            walk->point.x,\n            walk->point.y,\n            walk->point.z\n        );\n\n        walk = walk->next;\n    }\n}\n
    ", "tags": ["NULL"]}, {"location": "course-c/27-data-structures/lists/#operations-sur-une-liste-chainee", "title": "Op\u00e9rations sur une liste cha\u00een\u00e9e", "text": "
    • Cr\u00e9ation
    • Nombre d'\u00e9l\u00e9ments
    • Recherche
    • Insertion
    • Suppression
    • Concat\u00e9nation
    • Destruction

    Lors de la cr\u00e9ation d'un \u00e9l\u00e9ment, on utilise principalement le m\u00e9canisme de l'allocation dynamique ce qui permet de r\u00e9cup\u00e9rer l'adresse de l'\u00e9l\u00e9ment et de faciliter sa manipulation au travers de la liste. \u00a0Ne pas oublier de lib\u00e9rer la m\u00e9moire allou\u00e9e pour les \u00e9l\u00e9ments lors de leur suppression\u2026

    "}, {"location": "course-c/27-data-structures/lists/#calcul-du-nombre-delements-dans-la-liste", "title": "Calcul du nombre d'\u00e9l\u00e9ments dans la liste", "text": "

    Pour \u00e9valuer le nombre d'\u00e9l\u00e9ments dans une liste, on effectue le parcours de la liste \u00e0 partir de la t\u00eate, et on passe d'\u00e9l\u00e9ment en \u00e9l\u00e9ment gr\u00e2ce au champ next de la structure Element. On incr\u00e9ment le nombre d'\u00e9l\u00e9ments jusqu'\u00e0 ce que le pointeur next soit \u00e9gal \u00e0 NULL.

    size_t count = 0;\n\nfor (Element *e = &head; e != NULL; e = e->next)\n    count++;\n
    ", "tags": ["Element", "NULL"]}, {"location": "course-c/27-data-structures/lists/#detection-des-boucles", "title": "D\u00e9tection des boucles", "text": "

    Attention, la technique pr\u00e9c\u00e9dente ne fonctionne pas dans tous les cas, sp\u00e9cialement lorsqu'il y a des boucles dans la liste cha\u00een\u00e9e. Prenons l'exemple suivant\u2009:

    Boucle dans une liste cha\u00een\u00e9e

    La liste se terminant par une boucle, il n'y aura jamais d'\u00e9l\u00e9ment de fin et le nombre d'\u00e9l\u00e9ments calcul\u00e9 sera infini. Or, cette liste a un nombre fixe d'\u00e9l\u00e9ments. Comment donc les compter\u2009?

    Il existe un algorithme nomm\u00e9 d\u00e9tection de cycle de Robert W. Floyd aussi appel\u00e9 algorithme du li\u00e8vre et de la tortue. Il consiste \u00e0 avoir deux pointeurs qui parcourent la liste cha\u00een\u00e9e. L'un avance deux fois plus vite que le second.

    Algorithme de d\u00e9tection de cycle de Robert W. Floyd

    size_t compute_length(Element* head)\n{\n    size_t count = 0;\n\n    Element* slow = head;\n    Element* fast = head;\n\n    while (fast != NULL && fast->next != NULL) {\n        slow = slow->next;\n        fast = fast->next->next;\n\n        count++;\n\n        if (slow == fast) {\n            // Collision\n            break;\n        }\n    }\n\n    // Case when no loops detected\n    if (fast == NULL || fast->next == NULL) {\n        return count;\n    }\n\n    // Move slow to head, keep fast at meeting point.\n    slow = head;\n    while (slow != fast) {\n        slow = slow->next;\n        fast = fast->next;\n\n        count--;\n    }\n\n    return count;\n}\n

    Astuce

    Une bonne id\u00e9e pour se simplifier la vie est simplement d'\u00e9viter la cr\u00e9ation de boucles.

    "}, {"location": "course-c/27-data-structures/lists/#insertion", "title": "Insertion", "text": "

    L'insertion d'un \u00e9l\u00e9ment dans une liste cha\u00een\u00e9e peut-\u00eatre impl\u00e9ment\u00e9e de la fa\u00e7on suivante\u2009:

    Element* insert_after(Element* e, void* payload)\n{\n    Element* new = malloc(sizeof(Element));\n\n    memcpy(new->payload, payload, sizeof(new->payload));\n\n    new->next = e->next;\n    e->next = new;\n\n    return new;\n}\n
    "}, {"location": "course-c/27-data-structures/lists/#suppression", "title": "Suppression", "text": "

    La suppression implique d'acc\u00e9der \u00e0 l'\u00e9l\u00e9ment parent, il n'est donc pas possible \u00e0 partir d'un \u00e9l\u00e9ment donn\u00e9 de le supprimer de la liste.

    void delete_after(Element* e)\n{\n    e->next = e->next->next;\n    free(e);\n}\n
    "}, {"location": "course-c/27-data-structures/lists/#recherche", "title": "Recherche", "text": "

    Rechercher dans une liste cha\u00een\u00e9e est une question qui peut-\u00eatre complexe et il est n\u00e9cessaire de ce poser un certain nombre de questions\u2009:

    • Est-ce que la liste est tri\u00e9e\u2009?
    • Combien d'espace m\u00e9moire puis-je utiliser\u2009?

    On sait qu'une recherche id\u00e9ale s'effectue en \\(O(log(n))\\), mais que la solution triviale en \\(O(n)\\) est la suivante\u2009:

    "}, {"location": "course-c/27-data-structures/lists/#liste-doublement-chainee", "title": "Liste doublement cha\u00een\u00e9e", "text": "

    La liste doublement cha\u00een\u00e9e n'est qu'une extension de la liste cha\u00een\u00e9e simple dans laquelle on rajoute un pointeur vers l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent. Cela augmente l'emprunte m\u00e9moire de chaque \u00e9l\u00e9ment mais permet de parcourir la liste dans les deux sens.

    Liste cha\u00een\u00e9e simple

    "}, {"location": "course-c/27-data-structures/lists/#liste-chainee-xor", "title": "Liste cha\u00een\u00e9e XOR", "text": "

    L'inconv\u00e9nient d'une liste doublement cha\u00een\u00e9e est le surco\u00fbt n\u00e9cessaire au stockage d'un \u00e9l\u00e9ment. Chaque \u00e9l\u00e9ment contient en effet deux pointeurs sur l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent (prev) et suivant (next).

    ...  A       B         C         D         E  ...\n        \u2013>  next \u2013>  next  \u2013>  next  \u2013>\n        <\u2013  prev <\u2013  prev  <\u2013  prev  <\u2013\n

    Cette liste cha\u00een\u00e9e particuli\u00e8re compresse les deux pointeurs en un seul en utilisant l'op\u00e9ration XOR (d\u00e9not\u00e9e \u2295).

    ...  A        B         C         D         E  ...\n        <\u2013>  A\u2295C  <->  B\u2295D  <->  C\u2295E  <->\n

    Lorsque la liste est travers\u00e9e de gauche \u00e0 droite, il est possible de facilement reconstruire le pointeur de l'\u00e9l\u00e9ment suivant \u00e0 partir de l'adresse de l'\u00e9l\u00e9ment pr\u00e9c\u00e9dent.

    Les inconv\u00e9nients de cette structure sont\u2009:

    • Difficult\u00e9s de d\u00e9bogage
    • Complexit\u00e9 de mise en \u0153uvre

    L'avantage principal \u00e9tant le gain de place en m\u00e9moire.

    "}, {"location": "course-c/27-data-structures/lists/#liste-chainee-deroulee-unrolled-linked-list", "title": "Liste cha\u00een\u00e9e d\u00e9roul\u00e9e (Unrolled linked list)", "text": "

    Une liste cha\u00een\u00e9e d\u00e9roul\u00e9e rassemble les avantages d'un tableau et d'une liste cha\u00een\u00e9e. Elle permet d'accro\u00eetre les performances en r\u00e9duisant l'overhead de r\u00e9servation m\u00e9moire avec malloc.

    Liste cha\u00een\u00e9e d\u00e9roul\u00e9e

    typedef struct Node {\n    struct Node *next;\n    size_t count;  // Nombre d'\u00e9l\u00e9ments\n    int elements[]; // Membre flexible contenant les \u00e9l\u00e9ments\n} Node;\n
    ", "tags": ["malloc"]}, {"location": "course-c/27-data-structures/lists/#piles-ou-lifo-last-in-first-out", "title": "Piles ou LIFO (Last In First Out)", "text": "

    Une pile est une structure de donn\u00e9e tr\u00e8s similaire \u00e0 un tableau dynamique, mais dans laquelle les op\u00e9rations sont limit\u00e9es. Par exemple, il n'est possible que\u2009:

    • d'ajouter un \u00e9l\u00e9ment (push) ;
    • retirer un \u00e9l\u00e9ment (pop) ;
    • obtenir le dernier \u00e9l\u00e9ment ajout\u00e9 (peek) ;
    • tester si la pile est vide (is_empty) ;
    • tester si la pile est pleine avec (is_full).

    Une utilisation possible de pile sur des entiers serait la suivante\u2009:

    #include \"stack.h\"\n\nint main() {\n    Stack stack;\n    stack_init(&stack);\n\n    stack_push(42);\n    assert(stack_peek() == 42);\n\n    stack_push(23);\n    assert(!stack_is_empty());\n\n    assert(stack_pop() == 23);\n    assert(stack_pop() == 42);\n\n    assert(stack_is_empty());\n}\n

    Les piles peuvent \u00eatre impl\u00e9ment\u00e9es avec des tableaux dynamiques ou des listes cha\u00een\u00e9es (voir plus bas).

    "}, {"location": "course-c/27-data-structures/lists/#queues-ou-fifo-first-in-first-out", "title": "Queues ou FIFO (First In First Out)", "text": "

    Les queues sont aussi des structures tr\u00e8s similaires \u00e0 des tableaux dynamiques, mais elles ne permettent que les op\u00e9rations suivantes\u2009:

    • ajouter un \u00e9l\u00e9ment \u00e0 la queue (push) aussi nomm\u00e9 enqueue ;
    • supprimer un \u00e9l\u00e9ment au d\u00e9but de la queue (shift) aussi nomm\u00e9 dequeue ;
    • tester si la queue est vide (is_empty) ;
    • tester si la queue est pleine avec (is_full).

    Les queues sont souvent utilis\u00e9es lorsque des processus s\u00e9quentiels ou parall\u00e8les s'\u00e9changent des t\u00e2ches \u00e0 traiter\u2009:

    #include \"queue.h\"\n#include <stdio.h>\n\nvoid get_work(Queue *queue) {\n    while (!feof(stdin)) {\n        int n;\n        if (scanf(\"%d\", &n) == 1)\n            queue_enqueue(n);\n        scanf(\"%*[^\\n]%[\\n]\");\n    }\n}\n\nvoid process_work(Queue *queue) {\n    while (!is_empty(queue)) {\n        int n = queue_dequeue(queue);\n        printf(\"%d est %s\\n\", n, n % 2 ? \"impair\" : \"pair\";\n    }\n}\n\nint main() {\n    Queue* queue;\n\n    queue_init(&queue);\n    get_work(queue);\n    process_work(queue);\n    queue_free(queue);\n}\n
    "}, {"location": "course-c/27-data-structures/maps/", "title": "Tableaux de hachage", "text": "

    Les tableaux de hachage (Hash Table) sont une structure particuli\u00e8re dans laquelle une fonction dite de hachage est utilis\u00e9e pour transformer les entr\u00e9es en des indices d'un tableau.

    L'objectif est de stocker des cha\u00eenes de caract\u00e8res correspondant a des noms simples ici utilis\u00e9s pour l'exemple. Une possible r\u00e9partition serait la suivante\u2009:

    Tableau de hachage simple

    Si l'on cherche l'indice correspondant \u00e0 Ada, il convient de pouvoir calculer la valeur de l'indice correspondant \u00e0 partir de la valeur de la cha\u00eene de caract\u00e8re. Pour calculer cet indice aussi appel\u00e9 hash, il existe une infinit\u00e9 de m\u00e9thodes. Dans cet exemple, consid\u00e9rons une m\u00e9thode simple. Chaque lettre est identifi\u00e9e par sa valeur ASCII et la somme de toutes les valeurs ASCII est calcul\u00e9e. Le modulo 10 est ensuite calcul\u00e9 sur cette somme pour obtenir une valeur entre 0 et 9. Ainsi nous avons les calculs suivants\u2009:

    Nom    Valeurs ASCII     Somme  Modulo 10\n---    --------------    -----  ---------\nMia -> {77, 105, 97}  -> 279 -> 4\nTim -> {84, 105, 109} -> 298 -> 1\nBea -> {66, 101, 97}  -> 264 -> 0\nZoe -> {90, 111, 101} -> 302 -> 5\nJan -> {74, 97, 110}  -> 281 -> 6\nAda -> {65, 100, 97}  -> 262 -> 9\nLeo -> {76, 101, 111} -> 288 -> 2\nSam -> {83, 97, 109}  -> 289 -> 3\nLou -> {76, 111, 117} -> 304 -> 7\nMax -> {77, 97, 120}  -> 294 -> 8\nTed -> {84, 101, 100} -> 285 -> 10\n

    Pour trouver l'indice de \"Mia\" il suffit donc d'appeler la fonction suivante\u2009:

    int hash_str(char *s) {\n    int sum = 0;\n    while (*s != '\\0') sum += s++;\n    return sum % 10;\n}\n

    L'assertion suivante est donc vraie\u2009:

    assert(strcmp(table[hash_str(\"Mia\")], \"Mia\") == 0);\n

    Rechercher \"Mia\" et obtenir \"Mia\" n'est certainement pas l'exemple le plus utile. N\u00e9anmoins il est possible d'encoder plus qu'une cha\u00eene de caract\u00e8re et utiliser plut\u00f4t une structure de donn\u00e9e\u2009:

    struct Person {\n    char name[3 + 1 /* '\\0' */];\n    struct {\n        int month;\n        int day;\n        int year;\n    } born;\n    enum {\n        JOB_ASTRONOMER,\n        JOB_INVENTOR,\n        JOB_ACTRESS,\n        JOB_LOGICIAN,\n        JOB_BIOLOGIST\n    } job;\n    char country_code; // For example 41 for Switzerland\n};\n

    Dans ce cas, le calcul du hash se ferait sur la premi\u00e8re cl\u00e9 d'un \u00e9l\u00e9ment\u2009:

    int hash_person(struct Person person) {\n    int sum = 0;\n    while (*person.name != '\\0') sum += s++;\n    return sum % 10;\n}\n

    L'acc\u00e8s \u00e0 une personne \u00e0 partir de la cl\u00e9 se r\u00e9sout donc en O(1) car il n'y a aucune it\u00e9ration ou recherche \u00e0 effectuer.

    Cette vid\u00e9o YouTube explique bien le fonctionnement des tableaux de hachage.

    ", "tags": ["Ada"]}, {"location": "course-c/27-data-structures/maps/#collisions", "title": "Collisions", "text": "

    Lorsque la fonction de hachage est mal choisie, un certain nombre de collisions peuvent appara\u00eetre. Si l'on souhaite par exemple ajouter les personnes suivantes\u2009:

    Sue -> {83, 117, 101} -> 301 -> 4\nLen -> {76, 101, 110} -> 287 -> 1\n

    On voit que les positions 4 et 1 sont d\u00e9j\u00e0 occup\u00e9es par Mia et Tim.

    Une strat\u00e9gie de r\u00e9solution s'appelle Open adressing. Parmi les possibilit\u00e9s de cette strat\u00e9gie, le linear probing consiste \u00e0 v\u00e9rifier si la position du tableau est d\u00e9j\u00e0 occup\u00e9e et en cas de collision, chercher la prochaine place disponible dans le tableau\u2009:

    Person people[10] = {0}\n\n// Add Mia\nPerson mia = {.name=\"Mia\", .born={.day=1,.month=4,.year=1991}};\nint hash = hash_person(mia);\nwhile (people[hash].name[0] != '\\0') hash++;\npeople[hash] = mia;\n

    R\u00e9cup\u00e9rer une valeur dans le tableau demande une comparaison suppl\u00e9mentaire\u2009:

    char key[] = \"Mia\";\nint hash = hash_str(key)\nwhile (strcmp(people[hash], key) != 0) hash++;\nPerson person = people[hash];\n

    Lorsque le nombre de collisions est n\u00e9gligeable par rapport \u00e0 la table de hachage, la recherche d'un \u00e9l\u00e9ment est toujours en moyenne \u00e9gale \u00e0 \\(O(1)\\), mais lorsque le nombre de collisions est pr\u00e9pond\u00e9rant, la complexit\u00e9 se rapproche de celle de la recherche lin\u00e9aire \\(O(n)\\) et on perd tout avantage \u00e0 cette structure de donn\u00e9e.

    Dans le cas extr\u00eame, pour garantir un acc\u00e8s unitaire pour tous les noms de trois lettres, il faudrait un tableau de hachage d'une taille \\(26^3 = 17576\\) personnes. L'empreinte m\u00e9moire peut \u00eatre consid\u00e9rablement r\u00e9duite en stockant non pas une structure struct Person mais plut\u00f4t l'adresse vers cette structure\u2009:

    struct Person *people[26 * 26 * 26] = { NULL };\n

    Dans ce cas exag\u00e9r\u00e9, la fonction de hachage pourrait \u00eatre la suivante\u2009:

    int hash_name(char name[4]) {\n    int base = 26;\n    return\n        (name[0] - 'A') * 1 +\n        (name[1] - 'a') * 26 +\n        (name[2] - 'a') * 26 * 26;\n}\n
    "}, {"location": "course-c/27-data-structures/maps/#facteur-de-charge", "title": "Facteur de charge", "text": "

    Le facteur de charge d'une table de hachage est donn\u00e9 par la relation\u2009:

    \\[ \\text{Facteur de charge} = \\frac{\\text{Nombre total d'\u00e9l\u00e9ments}}{\\text{Taille de la table}} \\]

    Plus ce facteur de charge est \u00e9lev\u00e9, dans le cas du linear probing, moins bon sera la performance de la table de hachage.

    Certains algorithmes permettent de redimensionner dynamiquement la table de hachage pour conserver un facteur de charge le plus faible possible. Quand le facteur de charge d\u00e9passe un certain seuil (souvent 0.7), la table de hachage est agrandi (souvent doubl\u00e9e comme pour un tableau dynamique) et les \u00e9l\u00e9ments sont re-hach\u00e9s dans la nouvelle table.

    "}, {"location": "course-c/27-data-structures/maps/#chainage", "title": "Cha\u00eenage", "text": "

    Le cha\u00eenage ou chaining est une autre m\u00e9thode pour mieux g\u00e9rer les collisions. La table de hachage est coupl\u00e9e \u00e0 une liste cha\u00een\u00e9e.

    Cha\u00eenage d'une table de hachage

    "}, {"location": "course-c/27-data-structures/maps/#adressage-ouvert", "title": "Adressage ouvert", "text": "

    L'adressage ouvert est une autre m\u00e9thode pour g\u00e9rer les collisions. Lorsqu'une collision est d\u00e9tect\u00e9e, une autre position est calcul\u00e9e pour stocker l'\u00e9l\u00e9ment.

    Si une collision est d\u00e9tect\u00e9e, on regardera la position suivante dans la table. Si elle est libre on l'utilise, sinon la suitante, jusqu'\u00e0 trouver une position libre. Cette m\u00e9thode est appel\u00e9e linear probing.

    Une autre m\u00e9thode consiste \u00e0 utiliser une fonction de hachage secondaire pour calculer la position suivante. Cette m\u00e9thode est appel\u00e9e double hashing.

    Si la m\u00e9thode est plus facile \u00e0 impl\u00e9menter, l'op\u00e9ration de suppression est plus complexe. En effet, il est souvent n\u00e9cessaire de re-hacher les \u00e9l\u00e9ments pour maintenir la performance de la table de hachage.

    "}, {"location": "course-c/27-data-structures/maps/#fonction-de-hachage", "title": "Fonction de hachage", "text": "

    Nous avons vu plus haut une fonction de hachage calculant le modulo sur la somme des caract\u00e8res ASCII d'une cha\u00eene de caract\u00e8res. Nous avons \u00e9galement vu que cette fonction de hachage est source de nombreuses collisions. Les cha\u00eenes \"Rea\" ou \"Rae\" auront les m\u00eame hash puisqu'ils contiennent les m\u00eames lettres. De m\u00eame une fonction de hachage qui ne r\u00e9partit pas bien les \u00e9l\u00e9ments dans la table de hachage sera mauvaise. On sait par exemple que les voyelles sont nombreuses dans les mots et qu'il n'y en a que six et que la probabilit\u00e9 que nos noms de trois lettres contiennent une voyelle en leur milieu est tr\u00e8s \u00e9lev\u00e9e.

    L'id\u00e9e g\u00e9n\u00e9rale des fonctions de hachage est de r\u00e9partir uniform\u00e9ment les cl\u00e9s sur les indices de la table de hachage. L'approche la plus courante est de m\u00e9langer les bits de notre cl\u00e9 dans un processus reproductible.

    Une id\u00e9e mauvaise et \u00e0 ne pas retenir pourrait \u00eatre d'utiliser le caract\u00e8re pseudo-al\u00e9atoire de rand pour hacher nos noms\u2009:

    #include <stdlib.h>\n#include <stdio.h>\n\nint hash(char *str, int mod) {\n    int h = 0;\n    while(*str != '\\0') {\n        srand(h + *str++);\n        h = rand();\n    }\n    return h % mod;\n}\n\nint main() {\n    char *names[] = {\n        \"Bea\", \"Tim\", \"Len\", \"Sam\", \"Ada\", \"Mia\",\n        \"Sue\", \"Zoe\", \"Rae\", \"Lou\", \"Max\", \"Tod\"\n    };\n    for (int i = 0; i < sizeof(names) / sizeof(*names); i++)\n        printf(\"%s : %d\\n\", names[i], hash(names[i], 10));\n}\n

    Cette approche nous donne une assez bonne r\u00e9partition\u2009:

    $ ./a.out\nBea : 2\nTim : 3\nLen : 0\nSam : 3\nAda : 4\nMia : 3\nSue : 6\nZoe : 5\nRae : 8\nLou : 0\nMax : 3\nTod : 1\n

    Dans la pratique, on utilisera volontiers des fonctions de hachage utilis\u00e9es en cryptographies tels que MD5 ou SHA. Consid\u00e9rons par exemple la premi\u00e8re partie du po\u00e8me Chanson de Pierre Corneille\u2009:

    $ cat chanson.txt\nSi je perds bien des ma\u00eetresses,\nJ'en fais encor plus souvent,\nEt mes voeux et mes promesses\nNe sont que feintes caresses,\nEt mes voeux et mes promesses\nNe sont jamais que du vent.\n\n$ md5sum chanson.txt\n699bfc5c3fd42a06e99797bfa635f410  chanson.txt\n

    Le hash de ce texte est exprim\u00e9 en hexad\u00e9cimal\u2009:

    0x699bfc5c3fd42a06e99797bfa635f410\n

    Converti en d\u00e9cimal il peut \u00eatre r\u00e9duit en utilisant le modulo.

    140378864046454182829995736237591622672\n

    Voici un exemple en C\u2009:

    #include <stdlib.h>\n#include <stdio.h>\n#include <openssl/md5.h>\n#include <string.h>\n\nint hash(char* str, int mod) {\n    // Compute MD5\n    unsigned int output[4];\n    MD5_CTX md5;\n    MD5_Init(&md5);\n    MD5_Update(&md5, str, strlen(str));\n    MD5_Final((char*)output, &md5);\n\n    // 128-bits --> 32-bits\n    unsigned int h = 0;\n    for (int i = 0; i < sizeof(output)/sizeof(*output); i++) {\n        h ^= output[i];\n    }\n\n    // 32-bits --> mod\n    return h % mod;\n}\n\nint main() {\n    char *text[] = {\n        \"La poule ou l'\u0153uf?\",\n        \"Les pommes sont cuites!\",\n        \"Aussi lentement que possible\",\n        \"La poule ou l'\u0153uf.\",\n        \"La poule ou l'\u0153uf!\",\n        \"Aussi vite que n\u00e9cessaire\",\n        \"Il ne faut pas l\u00e2cher la proie pour l\u2019ombre.\",\n        \"Le mieux est l'ennemi du bien\",\n    };\n\n    for (int i = 0; i < sizeof(text) / sizeof(*text); i++)\n        printf(\"% 2d. %s\\n\", hash(text[i], 10), text[i]);\n}\n
    $ gcc hash.c -lcrypto\n$ ./a.out\n1. La poule ou l'\u0153uf?\n2. Les pommes sont cuites!\n3. Aussi lentement que possible\n4. La poule ou l'\u0153uf.\n5. La poule ou l'\u0153uf!\n6. Aussi vite que n\u00e9cessaire\n8. Il ne faut pas l\u00e2cher la proie pour l\u2019ombre.\n9. Le mieux est l'ennemi du bien\n

    On peut constater qu'ici les indices sont bien r\u00e9partis et que la fonction de hachage choisie semble uniforme.

    ", "tags": ["rand", "SHA"]}, {"location": "course-c/27-data-structures/maps/#fonction-de-hachage-affine", "title": "Fonction de hachage affine", "text": "

    Une autre m\u00e9thode pour calculer le hash consiste \u00e0 multiplier la valeur de chaque caract\u00e8re par une constante et de sommer le tout. Par exemple, la fonction de hachage suivante\u2009:

    int hash(char *str, int mod) {\n    const int a = 31;\n    int h = 0;\n    while(*str != '\\0')\n        h = (h * a + *str++) % mod;\n    return h;\n}\n

    La constante a est souvent choisie comme un nombre premier pour \u00e9viter les collisions.

    On peut \u00e9galement impl\u00e9menter cette fonction pour hacher des entiers\u2009:

    int hash_int(int n, int mod) {\n    const int a = 31;\n    return (a * n) % mod;\n}\n
    "}, {"location": "course-c/27-data-structures/maps/#murmurhash", "title": "MurmurHash", "text": "

    Une autre fonction de hachage tr\u00e8s populaire est MurmurHash. Elle est tr\u00e8s rapide et produit des r\u00e9sultats de qualit\u00e9. Voici un exemple en C\u2009:

    uint32_t murmur3_32(const char *key, uint32_t len, uint32_t seed) {\n    uint32_t h = seed;\n    if (len > 3) {\n        const uint32_t *key_x4 = (const uint32_t *)key;\n        size_t i = len >> 2;\n        do {\n            uint32_t k = *key_x4++;\n            k *= 0xcc9e2d51;\n            k = (k << 15) | (k >> 17);\n            k *= 0x1b873593;\n            h ^= k;\n            h = (h << 13) | (h >> 19);\n            h = h * 5 + 0xe6546b64;\n        } while (--i);\n        key = (const char *)key_x4;\n    }\n    if (len & 3) {\n        size_t i = len & 3;\n        uint32_t k = 0;\n        key = &key[i - 1];\n        do {\n            k <<= 8;\n            k |= *key--;\n        } while (--i);\n        k *= 0xcc9e2d51;\n        k = (k << 15) | (k >> 17);\n        k *= 0x1b873593;\n        h ^= k;\n    }\n    h ^= len;\n    h ^= h >> 16;\n    h *= 0x85ebca6b;\n    h ^= h >> 13;\n    h *= 0xc2b2ae35;\n    h ^= h >> 16;\n    return h;\n}\n

    Les valeurs de seed et len sont des valeurs arbitraires. La valeur de seed est souvent choisie al\u00e9atoirement. La valeur de len est la longueur de la cha\u00eene de caract\u00e8res \u00e0 hacher.

    On observe \u00e9galements des valeurs arbitraires pour les constantes 0xcc9e2d51, 0x1b873593, 0xe6546b64, 0x85ebca6b... Ces valeurs ont \u00e9t\u00e9 choisies pour leur qualit\u00e9 de m\u00e9lange des bits. Elles sont souvent d\u00e9termin\u00e9es empiriquement.

    ", "tags": ["len", "seed"]}, {"location": "course-c/27-data-structures/maps/#comparaison", "title": "Comparaison", "text": "

    Voici une compaison de diff\u00e9rentes fonctions de hachage\u2009:

    Comparaison des fonctions de hachage Fonction de hachage Qualit\u00e9 Vitesse Taille MD5 Bonne (cryptographie) Lente 128 bits SHA-1 Tr\u00e8s bonne (cryptographie) Lente 160 bits SHA-256 Excellente (cryptographie) Tr\u00e8s lente 256 bits MurmurHash3 Bonne (non cryptographique) Tr\u00e8s rapide 32/128 bits CityHash Tr\u00e8s bonne (non cryptographique) Tr\u00e8s rapide 64/128 bits FNV-1a Bonne (non cryptographique) Rapide 32/64 bits DJB2 Acceptable (non cryptographique) Tr\u00e8s rapide 32 bits CRC32 Bonne pour la d\u00e9tection d'erreurs Tr\u00e8s rapide 32 bits"}, {"location": "course-c/27-data-structures/maps/#perte-de-lordre", "title": "Perte de l'ordre", "text": "

    Il est important de noter que les tableaux de hachage ne conservent pas l'ordre des \u00e9l\u00e9ments. Comme ils peuvent \u00eatre ins\u00e9r\u00e9s dans n'importe quelle position du tableau, il n'est pas possible de les parcourir dans l'ordre d'insertion.

    Pire, selon l'algorithme utlis\u00e9, il est possible que si la fonction de hachage ou la taille de la table est modifi\u00e9e en cours de route, les \u00e9l\u00e9ments soient d\u00e9plac\u00e9s dans le tableau, et donc que l'ordre de parcours change.

    Python

    En Python les tables de hachages sont des structures de base du langage appel\u00e9es dict. Avant la version 3.7, l'ordre des \u00e9l\u00e9ments n'\u00e9tait pas conserv\u00e9. Depuis la version 3.7, l'ordre d'insertion est conserv\u00e9. Cela est d\u00fb \u00e0 l'impl\u00e9mentation de la table de hachage qui utilise une seconde structure de donn\u00e9e de type liste cha\u00een\u00e9e pour conserver l'ordre d'insertion. Cela a un impact sur la quantit\u00e9 de m\u00e9moire utilis\u00e9e et la performance de la table de hachage car \u00e0 chaque insertion il faut \u00e9galement mettre \u00e0 jour la liste cha\u00een\u00e9e.

    ", "tags": ["dict"]}, {"location": "course-c/27-data-structures/maps/#complexite-et-implementation", "title": "Complexit\u00e9 et impl\u00e9mentation", "text": "

    La caract\u00e9ristique principale recherch\u00e9e dans une table de hachage est de permettre un acc\u00e8s en temps constant \\(O(1)\\) pour les op\u00e9rations de recherche.

    La complexit\u00e9 de la recherche est en moyenne est donc de \\(O(1)\\), mais peut atteindre \\(O(n)\\) dans le pire des cas, par exemple si le facteur de charge est \u00e9lev\u00e9 et que la table de hachage est mal r\u00e9partie. Il y a donc un compromis \u00e0 trouver entre la m\u00e9moire utilis\u00e9e et la performance de la table de hachage.

    "}, {"location": "course-c/27-data-structures/performances/", "title": "Performances", "text": "

    Les diff\u00e9rentes structures de donn\u00e9es ne sont pas toutes \u00e9quivalentes en termes de performances. Il convient, selon l'application, d'opter pour la structure la plus adapt\u00e9e, et par cons\u00e9quent il est important de pouvoir comparer les diff\u00e9rentes structures de donn\u00e9es pour choisir la plus appropri\u00e9e. Est-ce que les donn\u00e9es doivent \u00eatre maintenues tri\u00e9es\u2009? Est-ce que la structure de donn\u00e9e est utilis\u00e9e comme une pile ou un tas\u2009? Quelle est la structure de donn\u00e9e avec le moins d'overhead pour les op\u00e9rations de push ou unshift ?

    L'indexation (indexing) est l'acc\u00e8s \u00e0 une certaine valeur du tableau par exemple avec a[k]. Dans un tableau statique et dynamique l'acc\u00e8s se fait par pointeur depuis le d\u00e9but du tableau soit\u2009: *((char*)a + sizeof(a[0]) * k) qui est \u00e9quivalant \u00e0 *(a + k). L'indexation par arithm\u00e9tique de pointeur n'est pas possible avec les listes cha\u00een\u00e9es dont il faut parcourir chaque \u00e9l\u00e9ment pour d\u00e9couvrir l'adresse du prochain \u00e9l\u00e9ment\u2009:

    int get(List *list) {\n    List *el = list->head;\n    for(int i = 0; i < k; i++)\n        el = el.next;\n    return el.value;\n}\n

    L'indexation d'une liste cha\u00een\u00e9e prend dans le cas le plus d\u00e9favorable \\(O(n)\\).

    Les arbres binaires ont une structure qui permet naturellement la dichotomique. Chercher l'\u00e9l\u00e9ment 5 prend 4 op\u00e9rations\u2009: 12 -> 4 -> 6 -> 5. L'indexation est ainsi possible en \\(O(log~n)\\).

                12\n             |\n         ----+----\n       /           \\\n      4            12\n     --            --\n   /    \\        /    \\\n  2      6      10    14\n / \\    / \\    / \\   /  \\\n1   3  5   7  9  11 13  15\n

    Le tableau suivant r\u00e9sume les performances obtenues pour les diff\u00e9rentes structures de donn\u00e9es que nous avons vues dans ce chapitre\u2009:

    Comparaison des performances des structures r\u00e9cursives Action Tableau Liste Buffer Arbre Hash Map Statique Dynamique cha\u00een\u00e9e circulaire binaire Indexing 1 1 n 1 log n Unshift/Shift n n 1 1 log n Push/Pop 1 1 amorti 1 1 log n Insert/Delete n n 1 n log n Search n n n n log n Sort n log n n log n n log n n log n 1", "tags": ["push", "unshift"]}, {"location": "course-c/27-data-structures/trees/", "title": "Arbres", "text": "

    Arbre binaire IRL

    Les arbres sont des structures de donn\u00e9es non lin\u00e9aires qui sont compos\u00e9es de n\u0153uds. Chaque n\u0153ud a un ou plusieurs enfants, sauf pour le n\u0153ud racine qui n'a pas de parent. Les arbres sont souvent utilis\u00e9s pour repr\u00e9senter des hi\u00e9rarchies, comme les syst\u00e8mes de fichiers, les arbres g\u00e9n\u00e9alogiques, les arbres de d\u00e9cision, etc.

    Voici un exemple d'arbre, il repr\u00e9sente par exemple une structure de documents stock\u00e9s sur un ordinateur. En haut on voit le disque C\u2009: qui contient des dossiers et des fichiers. Chaque dossier peut contenir d'autres dossiers ou des fichiers. Il y a donc une hi\u00e9rarchie entre les \u00e9l\u00e9ments. Chaque dossier peut contenir plusieurs \u00e9l\u00e9ments, mais chaque \u00e9l\u00e9ment ne peut \u00eatre contenu que dans un seul dossier.

    On appelle ce type d'arbre un arbre n-aire dirig\u00e9. C'est-\u00e0-dire que chaque n\u0153ud peut avoir plusieurs enfants. L'arbre est dirig\u00e9 car il y a un sens de la racine vers les feuilles. Il y a donc des fl\u00e8ches qui indiquent le sens de la hi\u00e9rarchie.

    %% Arbre n-aire dirig\u00e9\ngraph LR\n    C(C:)\n\n    C --> Program_Files(Program Files)\n    C --> Users(Utilisateurs)\n\n    Program_Files --> Microsoft(Microsoft)\n    Program_Files --> Adobe(Adobe)\n    Program_Files --> Google(Google)\n\n    Microsoft --> Office(Office)\n    Microsoft --> Edge(Edge)\n    Microsoft --> Teams(Teams)\n\n    Google --> Chrome(Chrome)\n    Google --> Drive(Drive)\n\n    Users --> Bob(Bob)\n    Users --> Alice(Alice)\n\n    Bob --> Documents(Documents)\n    Bob --> Downloads(Downloads)\n    Bob --> Music(Music)\n\n    Alice --> Documents_Alice(Documents)\n    Alice --> Downloads_Alice(Downloads)\n\n    Documents --> Resume(Resume.docx)\n    Documents --> Project(Project.docx)\n\n    Downloads --> Installer(Installer.exe)\n    Downloads --> Music_Bob(Music.mp3)\n\n    Music --> Album1(Album1)\n    Music --> Album2(Album2)\n\n    Album1 --> Song1(pink-floyd.mp3)\n    Album1 --> Song2(doroth\u00e9e.mp3)\n\n    Documents_Alice --> Thesis(Thesis.docx)\n    Documents_Alice --> Notes(Notes.txt)\n\n    Downloads_Alice --> App(App.exe)
    Arbre n-aire dirig\u00e9", "tags": ["arbres", "hierarchie"]}, {"location": "course-c/27-data-structures/trees/#arbre-binaire", "title": "Arbre binaire", "text": "

    Un arbre binaire est un arbre o\u00f9 chaque n\u0153ud a au plus deux enfants. Les enfants sont g\u00e9n\u00e9ralement appel\u00e9s le fils gauche et le fils droit. Les arbres binaires sont souvent utilis\u00e9s pour impl\u00e9menter des structures de donn\u00e9es comme les arbres de recherche binaires, les tas binaires, les arbres d'expression, etc.

    C'est une structure de donn\u00e9e tr\u00e8s utilis\u00e9e en informatique. En pratique, il est rare d'impl\u00e9menter un arbre binaire de mani\u00e8re explicite. On utilise plut\u00f4t des structures de donn\u00e9es qui sont bas\u00e9es sur des arbres binaires.

    Le C \u00e9tant un langage tr\u00e8s bas niveau, il n'y a pas de structure de donn\u00e9es arbre binaire dans la biblioth\u00e8que standard. En C++ en revanche il y a de nombreux conteneurs qui utilisent des arbres binaires comme std::set, std::map, std::multiset, std::multimap, std::priority_queue, etc.

    Voici l'exemple d'un arbre binaire. Chaque n\u0153ud est compos\u00e9 de deux enfants sauf pour les feuilles qui n'ont pas d'enfants. Le n\u0153ud 40 n'a lui que 1 enfant\u2009: l'enfant de droite.

    %% Arbre binaire\ngraph TD\n    classDef ghost display: none;\n\n    50((50))\n\n    50 --> 30((30))\n    50 --> 70((70))\n\n    30 --> 20((20))\n    30 --> 40((40))\n\n    70 --> 60((60))\n    70 --> 80((80))\n\n    20 --> 10((10))\n    20 --> 25((25))\n\n    40 --> ghost1(( ))\n    40 --> 35((35))\n\n    60 --> 55((55))\n    60 --> 65((65))\n\n    80 --> 75((75))\n    80 --> 90((90))\n\n    class ghost1 ghost;\n    linkStyle 8 display: none;
    Arbre binaire

    Un arbre peut \u00eatre \u00e9quilibr\u00e9 ou d\u00e9s\u00e9quilibr\u00e9. Un arbre est \u00e9quilibr\u00e9 si la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus un. Un arbre \u00e9quilibr\u00e9 est souvent plus efficace pour les op\u00e9rations de recherche, d'insertion et de suppression.

    Voici l'exemple d'un arbre d\u00e9s\u00e9quilibr\u00e9\u2009:

    %% Arbre binaire d\u00e9s\u00e9quilibr\u00e9\ngraph LR\n    classDef ghost display: none;\n\n    50((42))\n\n    50 --> 30((30))\n    50 --> 70((70))\n\n    30 --> 20((20))\n    30 --> 40((40))\n\n    70 --> 60((60))\n    70 --> 80((80))\n\n    20 --> 10((10))\n\n\n\n    60 --> 55((55))\n\n    10 --> 12((12))\n    10 --> 23((23))\n    23 --> 35((35))
    Arbre binaire d\u00e9s\u00e9quilibr\u00e9", "tags": ["arbre-binaire"]}, {"location": "course-c/27-data-structures/trees/#heap", "title": "Heap", "text": "

    La structure de donn\u00e9e heap aussi nomm\u00e9e tas ne doit pas \u00eatre confondue avec le tas utilis\u00e9 en allocation dynamique. Il s'agit d'une forme particuli\u00e8re de l'arbre binaire dit \u00ab\u2009presque complet\u2009\u00bb, dans lequel la diff\u00e9rence de niveau entre les feuilles n'exc\u00e8de pas 1. C'est-\u00e0-dire que toutes les feuilles sont \u00e0 une distance identique de la racine plus ou moins 1.

    Un tas peut ais\u00e9ment \u00eatre repr\u00e9sent\u00e9 sous forme de tableau en utilisant la r\u00e8gle suivante\u2009:

    Op\u00e9ration d'acc\u00e8s \u00e0 un \u00e9l\u00e9ment d'un hea Cible D\u00e9but \u00e0 0 D\u00e9but \u00e0 1 Enfant de gauche \\(2*k + 1\\) \\(2 * k\\) Enfant de droite \\(2*k + 2\\) \\(2 * k + 1\\) Parent \\(floor(k-1) / 2\\) \\(floor(k) / 2\\)

    Repr\u00e9sentation d'un *heap*

    ", "tags": ["heap"]}, {"location": "course-c/27-data-structures/trees/#min-heap", "title": "Min-heap", "text": "

    Un tas binaire est une structure de donn\u00e9es qui permet de stocker des \u00e9l\u00e9ments de mani\u00e8re ordonn\u00e9e. Un tas binaire est un arbre binaire complet o\u00f9 chaque n\u0153ud est plus petit que ses enfants. Un tas binaire est souvent utilis\u00e9 pour impl\u00e9menter une file de priorit\u00e9.

    Impl\u00e9mentation en C

    min-heap.h
    #pragma once\n\n#include <stddef.h>\n#include <stdio.h>\n\ntypedef struct MinHeap MinHeap;\n\nMinHeap *min_heap_create(size_t element_size,\n    int (*compare)(const void *, const void *),\n    void (*free_function)(void *));\nvoid min_heap_destroy(MinHeap *heap);\nint min_heap_insert(MinHeap *heap, void *element);\nvoid *min_heap_extract_min(MinHeap *heap);\nsize_t min_heap_size(MinHeap *heap);\n\nvoid min_heap_to_mermaid(MinHeap *heap, FILE *output, void (print_element)(void *element, FILE *output));\n
    min-heap.c
    #include \"min-heap.h\"\n#include \"vector.h\"\n\n#include <stdlib.h>\n#include <stdio.h>\n\nstruct MinHeap {\n    Vector *vector;\n    int (*compare)(const void *, const void *);\n};\n\nstatic void swap(void **a, void **b) {\n    void *temp = *a;\n    *a = *b;\n}\n\nstatic void heapify_up(MinHeap *heap, size_t index) {\n    if (index == 0) return;\n\n    size_t parent_index = (index - 1) / 2;\n    if (heap->compare(vector_get(heap->vector, index), vector_get(heap->vector, parent_index)) < 0) {\n        swap(&heap->vector->data[index], &heap->vector->data[parent_index]);\n        heapify_up(heap, parent_index);\n    }\n}\n\nstatic void heapify_down(MinHeap *heap, size_t index) {\n    size_t left_child = 2 * index + 1;\n    size_t right_child = 2 * index + 2;\n    size_t smallest = index;\n\n    if (left_child < vector_size(heap->vector) &&\n        heap->compare(vector_get(heap->vector, left_child), vector_get(heap->vector, smallest)) < 0) {\n        smallest = left_child;\n    }\n    if (right_child < vector_size(heap->vector) &&\n        heap->compare(vector_get(heap->vector, right_child), vector_get(heap->vector, smallest)) < 0) {\n        smallest = right_child;\n    }\n    if (smallest != index) {\n        swap(&heap->vector->data[index], &heap->vector->data[smallest]);\n        heapify_down(heap, smallest);\n    }\n}\n\nMinHeap *min_heap_create(size_t element_size, int (*compare)(const void *, const void *), void (*free_function)(void *)) {\n    MinHeap *heap = (MinHeap *)malloc(sizeof(MinHeap));\n    if (!heap) return NULL;\n\n    heap->vector = vector_create(element_size, free_function);\n    if (!heap->vector) {\n        free(heap);\n        return NULL;\n    }\n\n    heap->compare = compare;\n    return heap;\n}\n\nvoid min_heap_destroy(MinHeap *heap) {\n    if (heap) {\n        vector_destroy(heap->vector);\n        free(heap);\n    }\n}\n\nint min_heap_insert(MinHeap *heap, void *element) {\n    if (vector_push_back(heap->vector, element) != 0) return -1;\n    heapify_up(heap, vector_size(heap->vector) - 1);\n    return 0;\n}\n\nvoid *min_heap_extract_min(MinHeap *heap) {\n    if (vector_size(heap->vector) == 0) return NULL;\n\n    void *min_element = vector_get(heap->vector, 0);\n    void *last_element = vector_get(heap->vector, vector_size(heap->vector) - 1);\n    vector_set(heap->vector, 0, last_element);\n    heap->vector->size--; // Adjust size without deallocating the last element\n\n    heapify_down(heap, 0);\n    return min_element;\n}\n\nsize_t min_heap_size(MinHeap *heap) {\n    return vector_size(heap->vector);\n}\n\nvoid min_heap_to_mermaid(MinHeap *heap, FILE *output, void (print_element)(void *element, FILE *output)) {\n    fprintf(output, \"graph TD\\n\");\n    for (size_t i = 0; i < vector_size(heap->vector); i++) {\n        fprintf(output, \"  %zu((\\\"\", i);\n        if (print_element) {\n            print_element(vector_get(heap->vector, i), output);\n        } else {\n            fprintf(output, \"%d\", *(int *)vector_get(heap->vector, i));\n        }\n        fprintf(output, \"\\\"))\\n\");\n    }\n    for (size_t i = 0; i < vector_size(heap->vector); i++) {\n        if (i > 0) {\n            size_t parent = (i - 1) / 2;\n            fprintf(output, \"  %zu --> %zu\\n\", parent, i);\n        }\n    }\n}\n

    Le tas binaire utilise un tableau dynamique pour stocker les \u00e9l\u00e9ments. La r\u00e8gle est que chaque \u00e9l\u00e9ment voit son enfant de gauche \u00e0 l'indice 2 * k + 1 et l'enfant de droite \u00e0 l'indice 2 * k + 2. Le parent d'un \u00e9l\u00e9ment est \u00e0 l'indice (k - 1) / 2 quelque soit l'indice k.

    La propri\u00e9t\u00e9 principale du tas binaire est que chaque element de l'arbre est plus petit que ses enfants. Cela signifie que la racine de l'arbre est le plus petit \u00e9l\u00e9ment. Lorsqu'on retire un \u00e9l\u00e9ment du tas, on retire la racine et on la remplace par le dernier \u00e9l\u00e9ment du tableau il faut ensuite manipuler le tas pour que la propri\u00e9t\u00e9 soit respect\u00e9e. On appelle cette op\u00e9ration heapify.

    L'algorithme heapify est un algorithme r\u00e9cursif qui permet de r\u00e9tablir la propri\u00e9t\u00e9 du tas binaire. On part du dernier \u00e9l\u00e9ment de l'arbre qui poss\u00e8de au moins un enfant. On compare la valeur de l'\u00e9l\u00e9ment avec celle de son ou de ses enfants. Si la valeur de l'\u00e9l\u00e9ment est plus grande que celle de ses enfants, on \u00e9change les valeurs. On continue r\u00e9cursivement avec les enfants jusqu'\u00e0 ce que la propri\u00e9t\u00e9 soit respect\u00e9e.

    Si on insert un \u00e9l\u00e9ment dans le tableau dynamique, on l'ajoute \u00e0 la fin du tableau. Puis on doit r\u00e9tablir la propri\u00e9t\u00e9 du tas binaire. On compare la valeur de l'\u00e9l\u00e9ment avec celle de son parent. Si la valeur de l'\u00e9l\u00e9ment est plus petite que celle de son parent, on \u00e9change les valeurs. On continue r\u00e9cursivement avec le parent jusqu'\u00e0 ce que la propri\u00e9t\u00e9 soit respect\u00e9e, en remontant la branche.

    Prenons l'exemple initial de cet arbre stock\u00e9 en tableau\u2009:

    int a[] = {1, 3, 6, 5, 9, 8};\n
    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 6((6))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    6((6)) --> 8((8))

    On souhaite rajouter l'\u00e9l\u00e9ment 2. On commence par l'ajouter \u00e0 la fin\u2009:

    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 6((6))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    6((6)) --> 8((8))\n    6((6)) --> 2((2))

    On compare la valeur de 2 avec celle de son parent 6. Comme 2 est plus petit que 6, on \u00e9change les valeurs\u2009:

    graph TD\n    1((1)) --> 3((3))\n    1((1)) --> 2((2))\n    3((3)) --> 5((5))\n    3((3)) --> 9((9))\n    2((2)) --> 8((8))\n    2((2)) --> 6((6))

    On continue avec le parent de 2, 1. Comme 2 est plus grand que 1, on s'arr\u00eate l\u00e0. Le tas binaire est maintenant r\u00e9tabli.

    Les utilisations les plus courantes de cette structure de donn\u00e9e sont\u2009:

    • Tri par tas (Heap sort)
    • Une queue prioritaire (Priority queue)
    • D\u00e9terminer le k-i\u00e8me \u00e9l\u00e9ment le plus petit d'une collection (k-th smallest element)

    Voici un tableau r\u00e9sumant les complexit\u00e9s des diff\u00e9rentes op\u00e9rations dans un tas binaire minimal\u2009:

    Op\u00e9ration Complexit\u00e9 Insertion \\(O(log n)\\) Extraction du minimum \\(O(log n)\\) Acc\u00e8s au minimum \\(O(1)\\) Construction \\(O(n)\\) ou \\(O(n log n)\\) Suppression \\(O(log n)\\) Mise \u00e0 jour d'un \u00e9l\u00e9ment \\(O(log n)\\)
    • Insertion : Lorsqu'un \u00e9l\u00e9ment est ajout\u00e9 au min-heap, il est ajout\u00e9 \u00e0 la fin et le processus de heapify up (ou bubble up) est effectu\u00e9 pour r\u00e9tablir la propri\u00e9t\u00e9 du tas. Ce processus implique de comparer et potentiellement d'\u00e9changer des \u00e9l\u00e9ments \u00e0 chaque niveau de l'arbre, ce qui prend \\(O(log n)\\) dans le pire des cas.

    • Extraction du minimum : L'extraction de l'\u00e9l\u00e9ment minimum implique de retirer la racine du tas (le plus petit \u00e9l\u00e9ment), de placer le dernier \u00e9l\u00e9ment de l'arbre \u00e0 la racine, puis d'effectuer heapify down (ou sift down) pour r\u00e9tablir la propri\u00e9t\u00e9 du tas. Cela prend \\(O(log~n)\\) car il peut n\u00e9cessiter de descendre jusqu'au niveau le plus bas de l'arbre.

    • Acc\u00e8s au minimum : L'acc\u00e8s au minimum est \\(O(1)\\) car l'\u00e9l\u00e9ment minimum est toujours \u00e0 la racine du tas.

    • Construction : La complexit\u00e9 de la construction d'un tas \u00e0 partir d'une liste non tri\u00e9e peut \u00eatre \\(O(n)\\) en utilisant une technique appel\u00e9e heapify (ou build-heap). Cependant, si vous ins\u00e9rez chaque \u00e9l\u00e9ment un par un en utilisant la m\u00e9thode d'insertion standard, la complexit\u00e9 serait \\(O(n~log~n)\\).

    • Suppression : La suppression d'un \u00e9l\u00e9ment (autre que la racine) implique de le remplacer par le dernier \u00e9l\u00e9ment du tas et d'effectuer heapify up ou heapify down selon le cas, ce qui prend O(log~n).

    • Mise \u00e0 jour d'un \u00e9l\u00e9ment : La mise \u00e0 jour d'un \u00e9l\u00e9ment peut n\u00e9cessiter soit heapify up soit heapify down pour r\u00e9tablir la propri\u00e9t\u00e9 du tas, ce qui prend \\(O(log n)\\).

    Ces complexit\u00e9s font des min-heaps une structure de donn\u00e9es efficace pour les files de priorit\u00e9 et les algorithmes n\u00e9cessitant des op\u00e9rations fr\u00e9quentes d'insertion et d'extraction du minimum.

    ", "tags": ["tas-binaire"]}, {"location": "course-c/27-data-structures/trees/#arbre-binaire-de-recherche", "title": "Arbre binaire de recherche", "text": "

    Un arbres binaires de recherche (Binary Search Tree, BST) est un arbre binaire dans lequel chaque n\u0153ud a une valeur et les valeurs des n\u0153uds de l'arbre sont ordonn\u00e9es. Pour chaque n\u0153ud, toutes les valeurs des n\u0153uds du sous-arbre gauche sont inf\u00e9rieures \u00e0 la valeur du n\u0153ud et toutes les valeurs des n\u0153uds du sous-arbre droit sont sup\u00e9rieures \u00e0 la valeur du n\u0153ud.

    L'impl\u00e9mentation d'un arbre binaire est souvent impl\u00e9ment\u00e9e avec une liste cha\u00een\u00e9e comportant deux enfants un left et un right :

    Arbre binaire \u00e9quilibr\u00e9

    Lorsqu'il est \u00e9quilibr\u00e9, un arbre binaire comporte autant d'\u00e9l\u00e9ments \u00e0 gauche qu'\u00e0 droite et lorsqu'il est correctement rempli, la valeur d'un \u00e9l\u00e9ment est toujours\u2009:

    • La valeur de l'enfant de gauche est inf\u00e9rieure \u00e0 celle de son parent
    • La valeur de l'enfant de droite est sup\u00e9rieure \u00e0 celle de son parent

    Cette propri\u00e9t\u00e9 est tr\u00e8s appr\u00e9ci\u00e9e pour rechercher et ins\u00e9rer des donn\u00e9es complexes. Admettons que l'on a un registre patient du type\u2009:

    struct patient {\n    size_t id;\n    char firstname[64];\n    char lastname[64];\n    uint8_t age;\n}\n\ntypedef struct node {\n    struct patient data;\n    struct node* left;\n    struct node* right;\n} Node;\n

    Si l'on cherche le patient num\u00e9ro 612, il suffit de parcourir l'arbre de fa\u00e7on dichotomique\u2009:

    Node* search(Node* node, size_t id)\n{\n    if (node == NULL)\n        return NULL;\n\n    if (node->data.id == id)\n        return node;\n\n    return search(node->data.id > id ? node->left : node->right, id);\n}\n

    L'insertion et la suppression d'\u00e9l\u00e9ments dans un arbre binaire font appel \u00e0 des rotations, puisque les \u00e9l\u00e9ments doivent \u00eatre ins\u00e9r\u00e9s dans le correct ordre et que l'arbre, pour \u00eatre performant, doit toujours \u00eatre \u00e9quilibr\u00e9. Ces rotations sont donc des m\u00e9canismes de r\u00e9\u00e9quilibrage de l'arbre ne sont pas triviaux, mais dont la complexit\u00e9 d'ex\u00e9cution reste simple, et donc performante.

    ", "tags": ["right", "left"]}, {"location": "course-c/27-data-structures/trees/#queue-prioritaire", "title": "Queue prioritaire", "text": "

    Une queue prioritaire ou priority queue, est une queue dans laquelle les \u00e9l\u00e9ments sont trait\u00e9s par ordre de priorit\u00e9. Imaginons des personnalit\u00e9s, toutes atteintes d'une rage de dents et qui font la queue chez un dentiste aux m\u0153urs discutables. Ce dernier ne prendra pas ses patients par ordre d'arriv\u00e9e, mais, par importance aristocratique.

    typedef struct Person {\n   char *name;\n   enum SocialStatus {\n       PEON;\n       WORKER;\n       ENGINEER;\n       DOCTOR;\n       PROFESSOR;\n       PRESIDENT;\n       SUPERHERO;\n   } status;\n} Person;\n\nint main() {\n    ProrityQueue queue;\n    queue_init(queue);\n\n    for(int i = 0; i < 100; i++) {\n       queue_enqueue(queue, (Person) {\n          .name = random_name(),\n          .status = random_status()\n       });\n\n       Person person;\n       queue_dequeue(queue, &person);\n       dentist_heal(person);\n    }\n}\n

    La queue prioritaire dispose donc aussi des m\u00e9thodes enqueue et dequeue mais le dequeue retournera l'\u00e9l\u00e9ment le plus prioritaire de la liste. Ceci se traduit par trier la file d'attente \u00e0 chaque op\u00e9ration enqueue ou dequeue. L'une de ces deux op\u00e9rations pourrait donc avoir une complexit\u00e9 de \\(O(n log n)\\). Heureusement, il existe des m\u00e9thodes de tris performantes si un tableau est d\u00e9j\u00e0 tri\u00e9 et qu'un seul nouvel \u00e9l\u00e9ment y est ajout\u00e9.

    L'impl\u00e9mentation de ce type de structure de donn\u00e9e s'appuie le plus souvent sur un heap, soit construite \u00e0 partir d'un tableau statique, soit un tableau dynamique.

    ", "tags": ["dequeue", "enqueue"]}, {"location": "course-c/27-data-structures/trees/#arbre-avl", "title": "Arbre AVL", "text": "

    Un arbre AVL est un arbre binaire de recherche \u00e9quilibr\u00e9. Il est \u00e9quilibr\u00e9 car la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus un. Cela signifie que la hauteur de l'arbre est en \\(O(log n)\\), ce qui rend les op\u00e9rations de recherche, d'insertion et de suppression en \\(O(log n)\\).

    AVL Tree

    AVL tire son nom de ses inventeurs Adelson-Velsky and Landis. C'est une structure de donn\u00e9es tr\u00e8s utilis\u00e9e en informatique pour impl\u00e9menter des dictionnaires, des bases de donn\u00e9es, des compilateurs, etc.

    Son impl\u00e9mentation compl\u00e8te sort du cadre de ce cours mais il est int\u00e9ressant de comprendre comment il fonctionne. L'arbre AVL est un arbre binaire de recherche o\u00f9 chaque n\u0153ud a un facteur d'\u00e9quilibre qui est la diff\u00e9rence entre la hauteur de son sous-arbre gauche et la hauteur de son sous-arbre droit. Si le facteur d'\u00e9quilibre d'un n\u0153ud est sup\u00e9rieur \u00e0 \\(1\\) ou inf\u00e9rieur \u00e0 \\(-1\\), l'arbre est d\u00e9s\u00e9quilibr\u00e9 et il faut le r\u00e9\u00e9quilibrer. Cela donne un crit\u00e8re de r\u00e9\u00e9quilibrage en fonction du facteur d'\u00e9quilibre.

    L'op\u00e9ration d'insertion dans un arbre AVL est similaire \u00e0 celle d'un arbre binaire de recherche. On ins\u00e8re le n\u0153ud \u00e0 la bonne place dans l'arbre. Puis on met \u00e0 jour le facteur d'\u00e9quilibre de chaque n\u0153ud sur le chemin de la racine. Si le facteur d'\u00e9quilibre d'un n\u0153ud est sup\u00e9rieur \u00e0 \\(1\\) ou inf\u00e9rieur \u00e0 \\(-1\\), on r\u00e9\u00e9quilibre l'arbre en effectuant des rotations.

    C'est cette op\u00e9ration de rotation qui est la plus complexe dans un arbre AVL. Il existe plusieurs types de rotations en fonction du facteur d'\u00e9quilibre du n\u0153ud. Il y a les rotations simples et les rotations doubles. Les rotations simples sont les rotations droite et gauche. Les rotations doubles sont les rotations gauche-droite et droite-gauche.

    "}, {"location": "course-c/27-data-structures/trees/#arbre-rouge-noir", "title": "Arbre rouge-noir", "text": "

    Un arbre rouge-noir est un arbre binaire de recherche \u00e9quilibr\u00e9. Il est \u00e9quilibr\u00e9 car la hauteur de ses sous-arbres gauche et droit diff\u00e8re d'au plus deux. Cela signifie que la hauteur de l'arbre est en \\(O(log n)\\), ce qui rend les op\u00e9rations de recherche, d'insertion et de suppression en \\(O(log n)\\).

    Arbre rouge et noir

    Contrairement \u00e0 l'arbre AVL, l'arbre rouge-noir est plus simple \u00e0 impl\u00e9menter. Il utilise un bit de couleur pour chaque n\u0153ud pour indiquer si le n\u0153ud est rouge ou noir. L'arbre rouge-noir a cinq propri\u00e9t\u00e9s\u2009:

    1. Chaque n\u0153ud est soit rouge, soit noir.
    2. La racine est noire.
    3. Toutes les feuilles (n\u0153uds NULL) sont noires.
    4. Si un n\u0153ud est rouge, alors ses deux enfants sont noirs. (Pas de deux rouges cons\u00e9cutifs sur un chemin vers une feuille)
    5. Tout chemin simple d'un n\u0153ud donn\u00e9 \u00e0 ses feuilles descendantes contient le m\u00eame nombre de n\u0153uds noirs.

    De la m\u00eame mani\u00e8re que l'arbre AVL, il y a des op\u00e9rations de rotation pour r\u00e9\u00e9quilibrer l'arbre rouge-noir. Les rotations sont plus simples que dans un arbre AVL car il n'y a que deux types de rotations\u2009: la rotation gauche et la rotation droite.

    "}, {"location": "course-c/27-data-structures/trees/#trie", "title": "Trie", "text": "

    Un trie est une structure de donn\u00e9es qui stocke un ensemble de cha\u00eenes de caract\u00e8res. Il est souvent utilis\u00e9 pour stocker des mots dans un dictionnaire ou pour rechercher des mots dans un texte. Un trie est donc un arbre o\u00f9 chaque n\u0153ud est associ\u00e9 \u00e0 une lettre et un marqueur de fin de mot. Un noeud peut avoir de 1 \u00e0 26 enfants, un pour chaque lettre de l'alphabet (si on se limite \u00e0 l'alphabet latin minuscule).

    Prenons l'exemple des mots suivants\u2009:

    char *words[] = {\n    \"cadeaux\", \"le\", \"car\", \"cette\", \"cadre\", \"cause\",\n    \"carte\", \"comme\", \"car\", \"ce\", \"caduc\", \"cadet\",\n    \"la\", \"la\", \"les\"};\n

    On peut construire le trie suivant\u2009:

    Trie

    En vert, les n\u0153uds qui marquent la fin d'un mot. En orange la racine de l'arbre. La structure de donn\u00e9es de chaque noeud pourrait \u00eatre la suivante\u2009:

    typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[26];  // Children nodes\n} Node;\n

    Exercice 1\u2009: Impl\u00e9mentation

    Vous avez un texte connu et vous voulez permettre de compter les occurences de chaque mot. Une fois que le trie est construit, il est en lecture seule. Comment allez-vous impl\u00e9menter le trie\u2009?

    • Comme une liste cha\u00een\u00e9e, chaque noeud est allou\u00e9 dynamiquement sur le heap.
    • Un tableau statique sur la pile ou chaque \u00e9l\u00e9ment est un noeud.
    • Un tableau dynamique sur le heap, l'allocation est amortie et chaque noeud contient un tableau de pointeurs sur ses enfants.
    • Un tableau dynamique sur le heap, l'allocation est amortie et chaque noeud contient non pas un pointeur des enfants mais l'indice de l'enfant dans le tableau.
    • Par chunks d'\u00e9l\u00e9ments, chaque chunk est allou\u00e9 dynamiquement sur le heap.

    Discutons de plusieurs impl\u00e9mentations possibles d'un noeud d'un trie\u2009:

    • Liste cha\u00een\u00e9e : Chaque noeud est allou\u00e9 dynamiquement sur le heap. C'est une solution simple mais qui peut \u00eatre co\u00fbteuse en m\u00e9moire et en temps d'allocation. N\u00e9anmoins le noeud peut prendre un tableau flexible pour les enfants. Ce qui permet de ne pas allouer de m\u00e9moire inutile.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[];  // Children nodes, variable size\n} Node;\n
    • Tableau dynamique : En stoquant tous les \u00e9l\u00e9ments dans le tableau dynamique, on ne peut plus utiliser de pointeurs car si le tableau est r\u00e9allou\u00e9, les pointeurs ne sont plus valides. On utilise donc des indices pour acc\u00e9der aux enfants, car ces derniers sont relatifs \u00e0 l'adresse de d\u00e9but du tableau. En revanche, on ne peut plus utiliser de tableau flexible pour les enfants car la taille de la structure doit \u00eatre connue \u00e0 la compilation. Ceci implique une utilisation de m\u00e9moire plus importante.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    size_t children_id[26];  // Children nodes\n} Node;\n
    • Chunks : Chaque chunk contient un certain nombre de noeuds. Un chaunk d'une taille donn\u00e9e est r\u00e9serv\u00e9e. Lorsque le chunk est plein, un nouveau chunk est allou\u00e9. Cela permet de r\u00e9duire le nombre d'appels \u00e0 malloc et de r\u00e9duire la fragmentation de la m\u00e9moire. Cette m\u00e9thode permet de r\u00e9duire le nombre d'appels \u00e0 malloc et de r\u00e9duire la fragmentation de la m\u00e9moire. Elle r\u00e9soud aussi le probl\u00e8me de la taille fixe du tableau des enfants en autorisant \u00e0 nouveau un tableau flexible.

      typedef struct Node {\n    int occurences;  // Number of occurences of the word\n    struct Node *children[];  // Children nodes\n} Node;\n\ntypedef struct Chunk {\n    char *data[1024];\n    size_t used_bytes;\n    struct Chunk *next;\n} Chunk;\n

    Exemple d'impl\u00e9mentation\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#define ALPHABET_SIZE 26\n\ntypedef struct Node {\n    int occurrences;\n    struct Node *children[ALPHABET_SIZE];\n} Node;\n\nNode* createNode() {\n    Node *node = (Node *)malloc(sizeof(Node));\n    if (node) {\n        node->occurrences = 0;\n        for (int i = 0; i < ALPHABET_SIZE; i++) {\n            node->children[i] = NULL;\n        }\n    }\n    return node;\n}\n\nvoid insert(Node *root, const char *word) {\n    Node *current = root;\n    while (*word) {\n        if (!current->children[*word - 'a']) {\n            current->children[*word - 'a'] = createNode();\n        }\n        current = current->children[*word - 'a'];\n        word++;\n    }\n    current->occurrences++;\n}\n\nint search(Node *root, const char *word) {\n    Node *current = root;\n    while (*word) {\n        if (!current->children[*word - 'a']) {\n            return 0;\n        }\n        current = current->children[*word - 'a'];\n        word++;\n    }\n    return current->occurrences;\n}\n\nvoid freeTrie(Node *root) {\n    for (int i = 0; i < ALPHABET_SIZE; i++) {\n        if (root->children[i]) {\n            freeTrie(root->children[i]);\n        }\n    }\n    free(root);\n}\n\nint main() {\n    Node *root = createNode();\n\n    insert(root, \"frodo\");\n    insert(root, \"sam\");\n    insert(root, \"gandalf\");\n    // Add more words as needed\n\n    printf(\"Occurrences of 'frodo': %d\\n\", search(root, \"frodo\"));\n    printf(\"Occurrences of 'sam': %d\\n\", search(root, \"sam\"));\n    printf(\"Occurrences of 'gandalf': %d\\n\", search(root, \"gandalf\"));\n\n    freeTrie(root);\n}\n

    Exercice 2\u2009: Regroupement\u2009?

    Demandons-nous s'il ne serait pas pr\u00e9f\u00e9rable de regrouper les noeuds communs ensemble comme le montre la figure suivante\u2009:

    Trie\u2009: arbre avec noeuds communs

    D'apr\u00e8s vous est-ce une bonne id\u00e9e\u2009? Pourquoi\u2009?

    Solution

    Non, ce n'est pas une bonne id\u00e9e. D'une part la figure n'est plus un arbre mais un graphe. Un graph peut avoir des cycles et donc des boucles infinies. Ensuite, regrouper les \u00e9l\u00e9ments communs ne peut \u00eatre fait qu'\u00e0 la fin de la construction du trie, lorsqu'elle est d\u00e9j\u00e0 allou\u00e9e en m\u00e9moire. La complexit\u00e9 de l'optimisation n'est pas \u00e0 n\u00e9gliger. Si la contrainte est l'utilisation de la m\u00e9moire, il est pr\u00e9f\u00e9rable d'utiliser une autre structure de donn\u00e9e comme un radix trie.

    ", "tags": ["malloc"]}, {"location": "course-c/27-data-structures/trees/#radix-trie", "title": "Radix Trie", "text": "

    On l'a vu l'impl\u00e9mentation d'un trie est simple mais elle peut conduire \u00e0 une utilisation excessive de la m\u00e9moire. En effet, chaque noeud contient un tableau de 26 \u00e9l\u00e9ments, m\u00eame si un mot ne contient que quelques lettres. Pour r\u00e9duire la consommation de m\u00e9moire, on peut utiliser un radix trie. Cet arbre est \u00e9galement nomm\u00e9 PATRICIA trie pour Practical Algorithm to Retrieve Information Coded in Alphanumeric.

    Plut\u00f4t que de stocker une seule lettre par noeud, on stocke un pr\u00e9fixe commun \u00e0 plusieurs mots. On peut alors r\u00e9duire le nombre de noeuds et donc la consommation de m\u00e9moire.

    "}, {"location": "course-c/27-data-structures/trees/#navigation-dans-un-arbre", "title": "Navigation dans un arbre", "text": "

    La navigation dans un arbre binaire est une op\u00e9ration courante. Il existe plusieurs fa\u00e7ons de parcourir un arbre binaire\u2009:

    • Parcours en profondeur (DFS pour Depth First Search) On commence par la racine, puis on visite le sous-arbre gauche, puis le sous-arbre droit. On peut faire un parcours en profondeur en pr\u00e9-ordre, en in-ordre ou en post-ordre.
    • Parcours en largeur (BFS pour Breadth First Search) On visite les n\u0153uds de l'arbre de haut en bas et de gauche \u00e0 droite. On utilise une file pour stocker les n\u0153uds \u00e0 visiter.
    "}, {"location": "course-c/27-data-structures/trees/#parcours-en-profondeur", "title": "Parcours en profondeur", "text": "

    Le parcours en profondeur est une m\u00e9thode de parcours d'un arbre binaire qui commence par la racine, puis visite le sous-arbre gauche, puis le sous-arbre droit. Il existe trois fa\u00e7ons de parcourir un arbre en profondeur\u2009:

    • Pr\u00e9-ordre : On visite d'abord la racine, puis le sous-arbre gauche, puis le sous-arbre droit.
    • In-ordre : On visite d'abord le sous-arbre gauche, puis la racine, puis le sous-arbre droit.
    • Post-ordre : On visite d'abord le sous-arbre gauche, puis le sous-arbre droit, puis la racine.

    L'impl\u00e9mentation peut se faire de mani\u00e8re r\u00e9cursive ou it\u00e9rative. Voici un exemple d'impl\u00e9mentation r\u00e9cursive en C\u2009:

    int dfs(Node* node, (void)(*visit)(Node*))\n{\n    if (node == NULL)\n        return;\n\n    visit(node);\n    dfs(node->left, visit);\n    dfs(node->right, visit);\n}\n

    L'impl\u00e9mentation it\u00e9rative utilise une pile pour stocker les n\u0153uds \u00e0 visiter. Voici un exemple d'impl\u00e9mentation it\u00e9rative en C\u2009:

    int dfs(Node* node, (void)(*visit)(Node*))\n{\n    Stack stack;\n    stack_init(stack);\n    stack_push(stack, node);\n\n    while (!stack_empty(stack)) {\n        Node* current = stack_pop(stack);\n        visit(current);\n\n        if (current->right != NULL)\n            stack_push(stack, current->right);\n\n        if (current->left != NULL)\n            stack_push(stack, current->left);\n    }\n}\n
    "}, {"location": "course-c/30-modular-programming/", "title": "Programmation Modulaire", "text": "

    La programmation modulaire est une m\u00e9thode de conception de logiciels qui consiste \u00e0 diviser un programme en modules ind\u00e9pendants. Chaque module est une unit\u00e9 de code qui peut \u00eatre compil\u00e9e s\u00e9par\u00e9ment et r\u00e9utilis\u00e9e dans d'autres programmes. La programmation modulaire permet de simplifier la conception, la maintenance et l'\u00e9volution des logiciels en les divisant en petites parties coh\u00e9rentes et r\u00e9utilisables.

    "}, {"location": "course-c/30-modular-programming/libraries/", "title": "Biblioth\u00e8ques", "text": "

    Biblioth\u00e8que du Trinity College de Dublin

    Une biblioth\u00e8que informatique est une collection de fichiers comportant des fonctionnalit\u00e9s logicielles pr\u00eates \u00e0 l'emploi. La printf est une de ces fonctionnalit\u00e9s et offerte par le header <stdio.h> faisant partie de la biblioth\u00e8que libc6.

    L'anglicisme library, plus court \u00e0 prononcer et \u00e0 \u00e9crire est souvent utilis\u00e9 en lieu et place de biblioth\u00e8que tant il est omnipr\u00e9sent dans le monde logiciel. Le terme <stdlib.h> \u00e9tant la concat\u00e9nation de standard library par exemple. Notez que librairie n'est pas la traduction correcte de library qui est un faux ami.

    Une library, \u00e0 l'instar d'une biblioth\u00e8que, contient du contenu (livre \u00e9crit dans une langue donn\u00e9e) et un index (registre). En informatique il s'agit d'un fichier binaire compil\u00e9 pour une architecture donn\u00e9e ainsi qu'un ou plusieurs fichiers d'en-t\u00eate (header) contenant les d\u00e9finitions de cette biblioth\u00e8que.

    Dans ce chapitre on donnera plusieurs exemples sur un environnement POSIX. Sous Windows, les proc\u00e9dures choses sont plus compliqu\u00e9es, mais les concepts restent les m\u00eames.

    ", "tags": ["libc6", "printf"]}, {"location": "course-c/30-modular-programming/libraries/#exemple-libgmp", "title": "Exemple\u2009: libgmp", "text": "

    Voyons ensemble le cas de libgmp. Il s'agit d'une biblioth\u00e8que de fonctionnalit\u00e9s tr\u00e8s utilis\u00e9e et permettant le calcul arithm\u00e9tique multipr\u00e9cision en C. En observant le d\u00e9tail du paquet logiciel Debian on peut lire que libgmp est disponible pour diff\u00e9rentes architectures amd64, arm64, s390x, i386, ... Un d\u00e9veloppement sur un Raspberry-PI n\u00e9cessitera arm64 alors qu'un d\u00e9veloppement sur un PC utilisera amd64. En cliquant sur l'architecture d\u00e9sir\u00e9e on peut voir que ce paquet se compose des fichiers suivants (list r\u00e9duite aux fichiers concernant C):

    # Fichier d'en-t\u00eate C\n/usr/include/x86_64-linux-gnu/gmp.h\n\n# Biblioth\u00e8que compil\u00e9e pour l'architecture vis\u00e9e (ici amd64)\n/usr/lib/x86_64-linux-gnu/libgmp.a\n/usr/lib/x86_64-linux-gnu/libgmp.so\n\n# Documentation de la libgmp\n/usr/share/doc/libgmp-dev/AUTHORS\n/usr/share/doc/libgmp-dev/README\n/usr/share/doc/libgmp-dev/changelog.gz\n/usr/share/doc/libgmp-dev/copyright\n

    On a donc\u2009:

    gmp.h

    Fichier d'en-t\u00eate \u00e0 include dans un fichier source pour utiliser les fonctionnalit\u00e9s

    libgmp.a

    Biblioth\u00e8que statique qui contient l'impl\u00e9mentation en langage machine des fonctionnalit\u00e9s \u00e0 r\u00e9f\u00e9rer au linker lors de la compilation

    libgmp.so

    Biblioth\u00e8que dynamique qui contient aussi l'impl\u00e9mentation en langage machine des fonctionnalit\u00e9s

    Imaginons que l'on souhaite b\u00e9n\u00e9ficier des fonctionnalit\u00e9s de cette biblioth\u00e8que pour le calcul d'orbites pour un satellite d'observation de Jupyter. Pour prendre en main cet libary on \u00e9crit ceci\u2009:

    #include <gmp.h>\n#include <stdio.h>\n\nint main(void)\n{\n    unsigned int radix = 10;\n    char a[] = \"19810983098510928501928599999999999990\";\n\n    mpz_t n;\n\n    mpz_init(n);\n    mpz_set_ui(n, 0);\n\n    mpz_set_str(n, a, radix);\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_add_ui(n, n, 12); // Addition\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_mul(n, n, n); // Square\n\n    mpz_out_str(stdout, radix, n);\n    putchar('\\n');\n\n    mpz_clear(n);\n}\n

    Puis on compile\u2009:

    $ gcc gmp.c\ngmp.c:1:10: fatal error: gmp.h: No such file or directory\n#include <gmp.h>\n        ^~~~~~~\ncompilation terminated.\n

    A\u00efe\u2009! La biblioth\u00e8que n'est pas install\u00e9e...

    Pour l'installer, cela d\u00e9pend de votre syst\u00e8me d'exploitation\u2009:

    UbuntumacOS
    $ sudo apt-get install libgmp-dev\n
    $ brew install gmp\n

    Deuxi\u00e8me tentative\u2009:

    $ gcc gmp.c\n/tmp/cc2FxDSy.o: In function `main':\ngmp.c:(.text+0x6f): undefined reference to `__gmpz_init'\ngmp.c:(.text+0x80): undefined reference to `__gmpz_set_ui'\ngmp.c:(.text+0x96): undefined reference to `__gmpz_set_str'\ngmp.c:(.text+0xb3): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0xd5): undefined reference to `__gmpz_add_ui'\ngmp.c:(.text+0xf2): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0x113): undefined reference to `__gmpz_mul'\ngmp.c:(.text+0x130): undefined reference to `__gmpz_out_str'\ngmp.c:(.text+0x146): undefined reference to `__gmpz_clear'\ncollect2: error: ld returned 1 exit status\n

    Cette fois-ci on peut lire que le compilateur \u00e0 fait sont travail, mais ne parvient pas \u00e0 trouver les symboles des fonctions que l'on utilise p.ex. __gmpz_add_ui. C'est normal parce que l'on n'a pas renseign\u00e9 la biblioth\u00e8que \u00e0 utiliser.

    $ gcc gmp.c -lgmp\n\n$ ./a.out\n19810983098510928501928599999999999990\n19810983098510928501928600000000000002\n392475051329485669436248957939688603493163430354043714007714400000000000004\n

    Cette mani\u00e8re de faire utilise le fichier libgmp.so qui est la biblioth\u00e8que dynamique, c'est-\u00e0-dire que ce fichier est n\u00e9cessaire pour que le programme puisse fonctionner. Si je donne mon ex\u00e9cutable \u00e0 un ami qui n'as pas install libgmp sur son ordinateur, il ne sera pas capable de l'ex\u00e9cuter.

    Alternativement on peut compiler le m\u00eame programme en utilisant la librairie statique

    $ gcc gmp.c /usr/lib/x86_64-linux-gnu/libgmp.a\n

    C'est-\u00e0-dire qu'\u00e0 la compilation toutes les fonctionnalit\u00e9s ont \u00e9t\u00e9 int\u00e9gr\u00e9es \u00e0 l'ex\u00e9cutable et il ne d\u00e9pend de plus rien d'autre que le syst\u00e8me d'exploitation. Je peux prendre ce fichier le donner \u00e0 quelqu'un qui utilise la m\u00eame architecture et il pourra l'ex\u00e9cuter. En revanche, la taille du programme est plus grosse\u2009:

    # ~167 KiB\n$ gcc gmp.c -l:libgmp.a\n$ size a.out\ntext    data     bss     dec     hex filename\n155494     808      56  156358   262c6 ./a.out\n\n# ~8.5 KiB\n$ gcc gmp.c -lgmp\n$ size a.out\ntext    data     bss     dec     hex filename\n2752     680      16    3448     d78 ./a.out\n
    ", "tags": ["i386", "libgmp", "libgmp.so", "s390x", "gmp.h", "arm64", "amd64", "__gmpz_add_ui", "libgmp.a"]}, {"location": "course-c/30-modular-programming/libraries/#exemple-ncurses", "title": "Exemple\u2009: ncurses", "text": "

    La biblioth\u00e8que ncurses traduction de nouvelles mal\u00e9dictions est une \u00e9volution de curses d\u00e9velopp\u00e9 originellement par Ken Arnold . Il s'agit d'une biblioth\u00e8que pour la cr\u00e9ation d'interfaces graphique en ligne de commande, toujours tr\u00e8s utilis\u00e9e.

    La biblioth\u00e8que permet le positionnement arbitraire dans la fen\u00eatre de commande, le dessin de fen\u00eatres, de menus, d'ombrage sous les fen\u00eatres, de couleurs ...

    Exemple d'interface graphique \u00e9crite avec ncurses. Ici la configuration du noyau Linux.

    L'\u00e9criture d'un programme Hello World avec cette biblioth\u00e8que pourrait \u00eatre\u2009:

    #include <ncurses.h>\n\nint main()\n{\n    initscr();              // Start curses mode\n    printw(\"hello, world\"); // Print Hello World\n    refresh();              // Print it on to the real screen\n    getch();                    // Wait for user input\n    endwin();               // End curses mode\n\n    return 0;\n}\n

    La compilation n'est possible que si\u2009:

    1. La biblioth\u00e8que est install\u00e9e sur l'ordinateur
    2. Le lien vers la biblioth\u00e8que dynamique est mentionn\u00e9 \u00e0 la compilation
    $ gcc ncurses-hello.c -ohello -lncurses\n
    ", "tags": ["ncurses"]}, {"location": "course-c/30-modular-programming/libraries/#bibliotheques-statiques", "title": "Biblioth\u00e8ques statiques", "text": "

    Une static library est un fichier binaire compil\u00e9 pour une architecture donn\u00e9e et portant les extensions\u2009:

    • .a sur un syst\u00e8me POSIX (Android, Mac OS, Linux, Unix)
    • .lib sous Windows

    Une biblioth\u00e8que statique n'est rien d'autre qu'une archive d\u2019un ou plusieurs objets. Rappelons-le un objet est le r\u00e9sultat d'une compilation.

    Par exemple si l'on souhaite \u00e9crire une biblioth\u00e8que statique pour le code de C\u00e9sar on \u00e9crira un fichier source caesar.c:

    caesar.c
    void caesar(char str[], unsigned key)\n{\n    key %= 26;\n\n    for (int i = 0; str[i]; i++)\n    {\n        char c = str[i];\n\n        if (c >= 'a' && c <= 'z')\n        {\n            str[i] = ((c + key > 'z') ? c - 'z' + 'a' - 1 : c) + key;\n        }\n        else if (c >= 'A' && c <= 'Z')\n        {\n            str[i] = ((c + key > 'Z') ? c - 'Z' + 'A' - 1 : c) + key;\n        }\n    }\n}\n

    Ainsi qu'un fichier d'en-t\u00eate caesar.h:

    caesar.h
    /**\n * Function that compute the Caesar cipher\n * @param str input string\n * @param key offset to add to each character\n */\nvoid caesar(char str[], unsigned key);\n

    Pour cr\u00e9er une biblioth\u00e8que statique rien de plus facile. Le compilateur cr\u00e9e l'objet, l'archiver cr\u00e9e l'amalgame\u2009:

    $ gcc -c -o caesar.o caesar.c\n$ ar rcs caesar.a caesar.o\n

    Puis il suffit d'\u00e9crire un programme pour utiliser cette biblioth\u00e8que\u2009:

    encrypt.c
    #include <caesar.h>\n#include <stdio.h>\n\n#define KEY 13\n\nint main(int argc, char *argv[])\n{\n    for (int i = 1; i < argc; i++)\n    {\n        caesar(argv[i], KEY);\n        printf(\"%s\\n\", argv[i]);\n    }\n}\n

    Et de compiler le tout. Ici on utilise -I. et -L. pour dire au compilateur de chercher le fichier d'en-t\u00eate et la biblioth\u00e8que dans le r\u00e9pertoire courant.

    $ gcc encrypt.c -I. -L. -l:caesar.a\n

    La proc\u00e9dure sous Windows est plus compliqu\u00e9e et ne sera pas d\u00e9crite ici.

    ", "tags": ["caesar.c", "caesar.h"]}, {"location": "course-c/30-modular-programming/libraries/#bibliotheques-dynamiques", "title": "Biblioth\u00e8ques dynamiques", "text": "

    Une dynamic library est un fichier binaire compil\u00e9 pour une architecture donn\u00e9e et portant les extensions\u2009:

    • .so sur un syst\u00e8me POSIX (Android, Mac OS, Linux, Unix)
    • .dll sous Windows

    L'avantage principal est de ne pas charger pour rien chaque ex\u00e9cutable compil\u00e9 de fonctionnalit\u00e9s qui pourraient tr\u00e8s bien \u00eatre partag\u00e9es. L'inconv\u00e9nient est que l'utilisateur du programme doit imp\u00e9rativement avoir install\u00e9 la biblioth\u00e8que. Dans un environnement POSIX les biblioth\u00e8ques dynamiques disposent d'un emplacement sp\u00e9cifique ou elles sont toute stock\u00e9es. Malheureusement sous Windows le consensus est plus partag\u00e9 et il n'est pas rare de voir plusieurs applications diff\u00e9rentes h\u00e9berger une copie des dll localement si bien que l'avantage de la biblioth\u00e8que dynamique est an\u00e9anti par un d\u00e9faut de coh\u00e9rence.

    Reprenant l'exemple de C\u00e9sar vu plus haut, on peut cr\u00e9er une biblioth\u00e8que dynamique\u2009:

    $ gcc -shared -o libcaesar.so caesar.o\n

    Puis compiler notre programme pour utiliser cette biblioth\u00e8que. Avec une biblioth\u00e8que dynamique, il faut sp\u00e9cifier au compilateur quels sont les chemins vers lesquels il pourra trouver les biblioth\u00e8ques install\u00e9es. Comme ici on ne souhaite pas installer la biblioth\u00e8que et la rendre disponible pour tous les programmes, il faut ajouter aux chemins par d\u00e9faut, le chemin local $(pwd .), en cr\u00e9ant une variable d'environnement nomm\u00e9e LIBRARY_PATH.

    $ LIBRARY_PATH=$(pwd .) gcc encrypt.c -I. -lcaesar\n

    Le probl\u00e8me est identique \u00e0 l'ex\u00e9cution, car il faut sp\u00e9cifier (ici avec LD_LIBRARY_PATH) le chemin ou le syst\u00e8me d'exploitation s'attendra \u00e0 trouver la biblioth\u00e8que.

    $ LD_LIBRARY_PATH=$(pwd .) ./a.out ferrugineux\nsreehtvarhk\n

    Car sinon c'est l'erreur\u2009:

    $ LIBRARY_PATH=$(pwd .) ./a.out Hey?\n./a.out: error while loading shared libraries: libcaesar.so :\ncannot open shared object file: No such file or directory\n
    ", "tags": ["LIBRARY_PATH", "LD_LIBRARY_PATH"]}, {"location": "course-c/30-modular-programming/translation-units/", "title": "Compilation s\u00e9par\u00e9e", "text": ""}, {"location": "course-c/30-modular-programming/translation-units/#unite-de-traduction", "title": "Unit\u00e9 de traduction", "text": "

    En programmation, on appelle translation unit (unit\u00e9 de traduction), un code qui peut \u00eatre compil\u00e9 en un objet sans autre d\u00e9pendance externe. Le plus souvent, une unit\u00e9 de traduction correspond \u00e0 un fichier C.

    "}, {"location": "course-c/30-modular-programming/translation-units/#diviser-pour-mieux-regner", "title": "Diviser pour mieux r\u00e9gner", "text": "

    De m\u00eame qu'un magazine illustr\u00e9 est divis\u00e9 en sections pour accro\u00eetre la lisibilit\u00e9 (sport, news, annonces, m\u00e9t\u00e9o) de m\u00eame un code source est organis\u00e9 en \u00e9l\u00e9ments fonctionnels le plus souvent s\u00e9par\u00e9s en plusieurs fichiers et ces derniers parfois maintenus par diff\u00e9rents d\u00e9veloppeurs.

    Rappelons-le (et c'est tr\u00e8s important) :

    • une fonction ne devrait pas d\u00e9passer un \u00e9cran de haut (~50 lignes) ;
    • un fichier ne devrait pas d\u00e9passer 1000 lignes\u2009;
    • une ligne ne devrait pas d\u00e9passer 80 caract\u00e8res.

    Donc \u00e0 un moment, il est essentiel de diviser son travail en cr\u00e9ant plusieurs fichiers.

    Ainsi, lorsque le programme commence \u00e0 \u00eatre volumineux, sa lecture, sa compr\u00e9hension et sa mise au point deviennent d\u00e9licates m\u00eame pour le plus aguerri des d\u00e9veloppeurs. Il est alors essentiel de scinder le code source en plusieurs fichiers. Prenons l'exemple d'un programme qui effectue des calculs sur les nombres complexes. Notre projet est donc constitu\u00e9 de trois fichiers\u2009:

    $ tree\n.\n\u251c\u2500\u2500 complex.c\n\u251c\u2500\u2500 complex.h\n\u2514\u2500\u2500 main.c\n

    Le programme principal et la fonction main est contenu dans main.c quant au module complex il est compos\u00e9 de deux fichiers\u2009: complex.h l'en-t\u00eate et complex.c, l'impl\u00e9mentation du module.

    Le fichier main.c devra inclure le fichier complex.h afin de pouvoir utiliser correctement les fonctions du module de gestion des nombres complexes. Exemple\u2009:

    // fichier main.c\n#include \"complex.h\"\n\nint main() {\n    Complex c1 = { .real = 1., .imag = -3. };\n    complex_fprint(stdout, c1);\n}\n
    // fichier complex.h\n#ifndef COMPLEX_H\n#define COMPLEX_H\n\n#include <stdio.h>\n\ntypedef struct Complex {\n    double real;\n    double imag;\n} Complex, *pComplex;\n\nvoid complex_fprint(FILE *fp, const Complex c);\n\n#endif // COMPLEX_H\n
    // fichier complex.c\n#include \"complex.h\"\n\nvoid complex_fprint(FILE* fp, const Complex c) {\n    fprintf(fp, \"%+.3lf + %+.3lf\\n\", c.real, c.imag);\n}\n

    Un des avantages majeurs \u00e0 la cr\u00e9ation de modules est qu'un module logiciel peut \u00eatre r\u00e9utilis\u00e9 pour d'autres applications. Plus besoin de r\u00e9inventer la roue \u00e0 chaque application\u2009!

    Cet exemple sera compil\u00e9 dans un environnement POSIX de la fa\u00e7on suivante\u2009:

    gcc -c complex.c -o complex.o\ngcc -c main.c -o main.o\ngcc complex.o main.o -oprogram -lm\n

    Nous verrons plus bas les \u00e9l\u00e9ments th\u00e9oriques vous permettant de mieux comprendre ces lignes.

    ", "tags": ["complex.h", "complex.c", "main", "main.c"]}, {"location": "course-c/30-modular-programming/translation-units/#module-logiciel", "title": "Module logiciel", "text": "

    Les applications modernes d\u00e9pendent souvent de nombreux modules logiciels externes aussi utilis\u00e9s dans d'autres projets. C'est avantageux \u00e0 plus d'un titre\u2009:

    • les modules externes sont sous la responsabilit\u00e9 d'autres d\u00e9veloppeurs et le programme a d\u00e9velopper comporte moins de code\u2009;
    • les modules externes sont souvent bien document\u00e9s et test\u00e9s et il est facile de les utiliser\u2009;
    • la lisibilit\u00e9 du programme est accrue, car il est bien d\u00e9coup\u00e9 en des ensembles fonctionnels\u2009;
    • les modules externes sont r\u00e9utilisables et ind\u00e9pendants, ils peuvent donc \u00eatre r\u00e9utilis\u00e9s sur plusieurs projets.

    Lorsque vous utilisez la fonction printf, vous d\u00e9pendez d'un module externe nomm\u00e9 stdio. En r\u00e9alit\u00e9 l'ensemble des modules stdio, stdlib, stdint, ctype... sont tous group\u00e9s dans une seule biblioth\u00e8que logicielle nomm\u00e9e libc disponible sur tous les syst\u00e8mes compatibles POSIX. Sous Linux, le pendant libre glibc est utilis\u00e9. Il s'agit de la biblioth\u00e8que GNU C Library.

    Un module logiciel peut se composer de fichiers sources, c'est-\u00e0-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\u00e8ques d\u00e9j\u00e0 compil\u00e9es sous la forme de fichiers .h, .a et .so. Sous Windows on rencontre fr\u00e9quemment l'extension .dll. Ces fichiers compil\u00e9s ne donnent pas acc\u00e8s au code source, mais permettent d'utiliser les fonctionnalit\u00e9s quelles offrent dans des programmes C en mettant \u00e0 disposition un ensemble de fonctions document\u00e9es.

    ", "tags": ["stdint", "glibc", "libc", "stdlib", "printf", "ctype", "stdio", "Makefile"]}, {"location": "course-c/30-modular-programming/translation-units/#compilation-avec-assemblage-differe", "title": "Compilation avec assemblage diff\u00e9r\u00e9", "text": "

    Lorsque nous avions compil\u00e9 notre premier exemple Hello World nous avions simplement appel\u00e9 gcc avec le fichier source hello.c qui nous avait cr\u00e9\u00e9 un ex\u00e9cutable a.out. En r\u00e9alit\u00e9, GCC est pass\u00e9 par plusieurs sous-\u00e9tapes de compilation\u2009:

    1. Pr\u00e9processing : les commentaires sont retir\u00e9s, les directives pr\u00e9processeur sont remplac\u00e9es par leur \u00e9quivalent C.
    2. Compilation : le code C d'une seule translation unit est converti en langage machine en un fichier objet .o.
    3. \u00c9dition des liens : aussi nomm\u00e9s link, les diff\u00e9rents fichiers objets sont r\u00e9unis en un seul ex\u00e9cutable.

    Lorsqu'un seul fichier est fourni \u00e0 GCC, les trois op\u00e9rations sont effectu\u00e9es en m\u00eame temps, mais ce n'est plus possible aussit\u00f4t que le programme est compos\u00e9 de plusieurs unit\u00e9s de translation (plusieurs fichiers C). Il est alors n\u00e9cessaire de compiler manuellement chaque fichier source et d'en cr\u00e9er.

    La figure suivante r\u00e9sume les diff\u00e9rentes \u00e9tapes de GCC. Les pointill\u00e9s indiquent \u00e0 quel niveau les op\u00e9rations peuvent s'arr\u00eater. Il est d\u00e8s lors possible de passer par des fichiers interm\u00e9diaires assembleur (.s) ou objets (.o) en utilisant la bonne commande.

    \u00c9tapes interm\u00e9diaires de compilation avec GCC

    Notons que ces \u00e9tapes existent, quel que soit le compilateur ou le syst\u00e8me d'exploitation. Nous retrouverons ces exactes m\u00eames \u00e9tapes 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).

    Notons que g\u00e9n\u00e9ralement, seul deux \u00e9tapes de GCC sont utilis\u00e9es\u2009:

    1. Compilation avec gcc -c <fichier.c>, ceci g\u00e9n\u00e8re automatiquement un fichier .o du m\u00eame nom que le fichier d'entr\u00e9e.
    2. \u00c9dition des liens avec gcc <fichier1.o> <fichier2.o> ..., ceci g\u00e9n\u00e8re automatiquement un fichier ex\u00e9cutable a.out.
    ", "tags": ["a.out", "gcc", "hello.c"]}, {"location": "course-c/30-modular-programming/translation-units/#fichiers-den-tete-header", "title": "Fichiers d'en-t\u00eate (header)", "text": "

    Les fichiers d'en-t\u00eate (.h) sont des fichiers \u00e9crits en langage C, mais qui ne contiennent pas d'impl\u00e9mentation de fonctions. Un tel fichier ne contient donc pas de while, de for ou m\u00eame de if. Par convention, ces fichiers ne contiennent que\u2009:

    • Des prototypes de fonctions (ou de variables).
    • Des d\u00e9clarations de types (typedef, struct).
    • Des d\u00e9finitions pr\u00e9processeur (#include, #define).

    Nous l'avons vu dans le chapitre sur le pr\u00e9processeur, la directive #include ne fais qu'inclure le contenu du fichier cible \u00e0 l'emplacement de la directive. Il est donc possible (mais fort d\u00e9conseill\u00e9), d'avoir la situation suivante\u2009:

    main.c
    int main() {\n   #include \"foobar.def\"\n}\n

    Et le fichier foobar.def pourrait contenir\u2009:

    foobar.def
    #ifdef FOO\nprintf(\"hello foo!\\n\");\n#else\nprintf(\"hello bar!\\n\");\n#endif\n

    Vous noterez que l'extension de foobar n'est pas .h puisque le contenu n'est pas un fichier d'en-t\u00eate. .def ou n'importe quelle autre extension pourrait donc faire l'affaire ici.

    Dans cet exemple, le pr\u00e9processeur ne fait qu'inclure le contenu du fichier foobar.def \u00e0 l'emplacement de la d\u00e9finition #include \"foobar.def\". Voyons-le en d\u00e9tail\u2009:

    $ cat << EOF > main.c\nint main() {\n    #include \"foobar.def\"\n    #include \"foobar.def\"\n}\nEOF\n\n$ cat << EOF > foobar.def\n#ifdef FOO\nprintf(\"hello foo!\\n\");\n#else\nprintf(\"hello bar!\\n\");\n#endif\nEOF\n\n$ gcc -E main.c | sed '/^#/ d'\nint main() {\nprintf(\"hello bar\\n\");\nprintf(\"hello bar\\n\");\n}\n

    Lorsque l'on observe le r\u00e9sultat du pr\u00e9processeur, on s'aper\u00e7oit que toutes les directives pr\u00e9processeur ont disparues et que la directive #include a \u00e9t\u00e9 remplac\u00e9e par de contenu de foobar.def. Remarquons que le fichier est inclus deux fois, nous verrons plus loin comme \u00e9viter cela.

    Nous avons vu au chapitre sur les prototypes de fonctions qu'il est possible de ne d\u00e9clarer que la premi\u00e8re ligne d'une fonction. Ce prototype permet au compilateur de savoir combien d'arguments est compos\u00e9 une fonction sans n\u00e9cessairement disposer de l'impl\u00e9mentation de cette fonction. Aussi on trouve dans tous les fichiers d'en-t\u00eate des d\u00e9clarations en amont (forward declaration). Dans le fichier d'en-t\u00eate stdio.h on trouvera la ligne\u2009: int printf( const char *restrict format, ... );.

    $ cat << EOF > main.c\n\u2192 #include <stdio.h>\n\u2192 int main() { }\n\u2192 EOF\n$ gcc -E main.c | grep -P '\\bprintf\\b'\nextern int printf (const char *__restrict __format, ...);\n

    Notons qu'ici le prototype est pr\u00e9c\u00e9d\u00e9 par le mot cl\u00e9 extern. Il s'agit d'un mot cl\u00e9 optionnel permettant de renforcer l'intention du d\u00e9veloppeur que la fonction d\u00e9clar\u00e9e n'est pas inclue dans fichier courant, mais qu'elle est impl\u00e9ment\u00e9e ailleurs, dans un autre fichier. Et c'est le cas, car printf est d\u00e9j\u00e0 compil\u00e9e quelque part dans la biblioth\u00e8que libc inclue par d\u00e9faut lorsqu'un programme C est compil\u00e9 dans un environnement POSIX.

    Un fichier d'en-t\u00eate contiendra donc tout le n\u00e9cessaire utile \u00e0 pouvoir utiliser une biblioth\u00e8que externe.

    ", "tags": ["struct", "typedef", "libc", "for", "foobar", "printf", "foobar.def", "while", "stdio.h", "extern"]}, {"location": "course-c/30-modular-programming/translation-units/#protection-de-reentrance", "title": "Protection de r\u00e9entrance", "text": "

    La protection de r\u00e9entrence aussi nomm\u00e9e header guards est une solution au probl\u00e8me d'inclusion multiple. Si par exemple on d\u00e9finit dans un fichier d'en-t\u00eate un nouveau type et que l'on inclut ce fichier, mais que ce dernier est d\u00e9j\u00e0 inclus par une autre biblioth\u00e8que, une erreur de compilation appara\u00eetra\u2009:

    $ cat << EOF > main.c\n\u2192 #include \"foo.h\"\n\u2192 #include \"bar.h\"\n\u2192 int main() {\n\u2192    Bar bar = {0};\n\u2192    foo(bar);\n\u2192 }\n\u2192 EOF\n\n$ cat << EOF > foo.h\n\u2192 #include \"bar.h\"\n\u2192\n\u2192 extern void foo(Bar);\n\u2192 EOF\n\n$ cat << EOF > bar.h\n\u2192 typedef struct Bar {\n\u2192    int b, a, r;\n\u2192 } Bar;\n\u2192 EOF\n\n$ gcc main.c\nIn file included from main.c:2:0 :\nbar.h:1:16: error: redefinition of \u2018struct Bar\u2019\ntypedef struct Bar {\n                ^~~\nIn file included from foo.h:1:0,\n                from main.c:1 :\nbar.h:1:16: note: originally defined here\ntypedef struct Bar {\n                ^~~\nIn file included from main.c:2:0 :\nbar.h:3:3: error: conflicting types for \u2018Bar\u2019\n} Bar;\n^~~\n...\n

    Dans cet exemple l'utilisateur ne sait pas forc\u00e9ment que bar.h est d\u00e9j\u00e0 inclus avec foo.h et le r\u00e9sultat apr\u00e8s pr\u00e9-processing est le suivant\u2009:

    $ gcc -E main.c | sed '/^#/ d'\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n\nextern void foo(Bar);\ntypedef struct Bar {\nint b, a, r;\n} Bar;\nint main() {\nBar bar = {0};\nfoo(bar);\n}\n

    On y retrouve la d\u00e9finition de Bar deux fois et donc, le compilateur g\u00e9n\u00e8re une erreur.

    Une solution \u00e0 ce probl\u00e8me est d'ajouter des gardes d'inclusion multiple par exemple avec ceci\u2009:

    #ifndef BAR_H\n#define BAR_H\n\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n\n#endif // BAR_H\n

    Si aucune d\u00e9finition du type #define BAR_H n'existe, alors le fichier bar.h n'a jamais \u00e9t\u00e9 inclus auparavant et le contenu de la directive #ifndef BAR_H dans lequel on commence par d\u00e9finir BAR_H est ex\u00e9cut\u00e9. Lors d'une future inclusion de bar.h, la valeur de BAR_H aura d\u00e9j\u00e0 \u00e9t\u00e9 d\u00e9finie et le contenu de la directive #ifndef BAR_H ne sera jamais ex\u00e9cut\u00e9.

    Alternativement, il existe une solution non standard, mais support\u00e9e par la plupart des compilateurs. Elle fait intervenir un pragma\u2009:

    #pragma once\n\ntypedef struct Bar {\nint b, a, r;\n} Bar;\n

    Cette solution est \u00e9quivalente \u00e0 la m\u00e9thode traditionnelle et pr\u00e9sente plusieurs avantages. C'est tout d'abord une solution atomique qui ne n\u00e9cessite pas un #endif \u00e0 la fin du fichier. Il n'y a ensuite pas de conflit avec la r\u00e8gle SSOT, car le nom du fichier bar.h n'appara\u00eet pas dans le fichier BAR_H.

    ", "tags": ["Bar", "bar.h", "BAR_H", "foo.h"]}, {"location": "course-c/30-modular-programming/translation-units/#en-profondeur", "title": "En profondeur", "text": "

    Pour mieux comprendre la compilation s\u00e9par\u00e9e, tentons d'observer le code assembleur g\u00e9n\u00e9r\u00e9. Consid\u00e9rons le fichier foo.c :

    int bar(int);\n\nint foo(int a) {\n    return bar(a) + 42;\n}\n

    Puisqu'il ne contient pas de fonction main, il n'est pas possible de compiler ce fichier en un ex\u00e9cutable car il manque un point d'entr\u00e9e\u2009:

    gcc foo.c\n/usr/bin/ld: /usr/lib/x86_64-linux-gnu/Scrt1.o: in function '_start':\n(.text+0x24): undefined reference to 'main'\ncollect2: error: ld returned 1 exit status\n

    Le linker se termine avec une erreur\u2009: r\u00e9f\u00e9rence \u00e0 'main' inexistante.

    En revanche, il est possible de compiler un objet, c'est \u00e0 dire g\u00e9n\u00e9rer les instructions assembleur. La fonction bar \u00e9tant manquante, le compilateur suppose qu'elle existe quelque part en m\u00e9moire et se contentera de dire moi j'appelle cette fonction ou qu'elle se trouve.

    $objdump -d foo.o\n\nfoo.o:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000000000 <foo>:\n 0:   f3 0f 1e fa       endbr64\n 4:   55                push   %rbp\n 5:   48 89 e5          mov    %rsp,%rbp\n 8:   48 83 ec 10       sub    $0x10,%rsp\n c:   89 7d fc          mov    %edi,-0x4(%rbp)\n f:   8b 45 fc          mov    -0x4(%rbp),%eax\n12:   89 c7             mov    %eax,%edi\n14:   e8 00 00 00 00    callq  19 <foo+0x19>\n19:   83 c0 2a          add    $0x2a,%eax\n1c:   c9                leaveq\n1d:   c3                retq\n

    On constate \u00e0 la ligne 19 que l'addition \u00e0 bien lieu eax + 42, et que l'appel de la fonction bar se produit \u00e0 la ligne 14.

    Maintenant, consid\u00e9rons le programme principal\u2009:

    #include <stdio.h>\n\nint foo(int);\n\nint bar(int a) {\n    return a * 2;\n}\n\nint main() {\n    printf(\"%d\", foo(42));\n}\n

    En g\u00e9n\u00e9rant l'objet gcc -c main.c, on peut \u00e9galement afficher l'assembleur g\u00e9n\u00e9r\u00e9 avec objdump :

    $objdump -d main.o\n\nmain.o:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000000000 <bar>:\n 0:   f3 0f 1e fa             endbr64\n 4:   55                      push   %rbp\n 5:   48 89 e5                mov    %rsp,%rbp\n 8:   89 7d fc                mov    %edi,-0x4(%rbp)\n b:   8b 45 fc                mov    -0x4(%rbp),%eax\n e:   01 c0                   add    %eax,%eax\n10:   5d                      pop    %rbp\n11:   c3                      retq\n\n0000000000000012 <main>:\n12:   f3 0f 1e fa             endbr64\n16:   55                      push   %rbp\n17:   48 89 e5                mov    %rsp,%rbp\n1a:   bf 2a 00 00 00          mov    $0x2a,%edi\n1f:   e8 00 00 00 00          callq  24 <main+0x12>\n24:   89 c6                   mov    %eax,%esi\n26:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi\n2d:   b8 00 00 00 00          mov    $0x0,%eax\n32:   e8 00 00 00 00          callq  37 <main+0x25>\n37:   b8 00 00 00 00          mov    $0x0,%eax\n3c:   5d                      pop    %rbp\n3d:   c3                      retq\n

    On observe l'appel de la fonction foo \u00e0 la ligne 1f et l'appel de printf \u00e0 la ligne 32.

    L'assemblage de ces deux fichiers en un ex\u00e9cutable r\u00e9sout les liens en modifiant les adresses d'appel des fonctions puisqu'elles sont maintenant connues (notons que certaines lignes ont \u00e9t\u00e9 retir\u00e9es pour plus de lisibilit\u00e9) :

    $ gcc foo.o main.o\n$ objdump -d a.out\n\na.out:     file format elf64-x86-64\n\nDisassembly of section .text:\n\n0000000000001149 <foo>:\n    1149:       f3 0f 1e fa             endbr64\n    114d:       55                      push   %rbp\n    114e:       48 89 e5                mov    %rsp,%rbp\n    1151:       48 83 ec 10             sub    $0x10,%rsp\n    1155:       89 7d fc                mov    %edi,-0x4(%rbp)\n    1158:       8b 45 fc                mov    -0x4(%rbp),%eax\n    115b:       89 c7                   mov    %eax,%edi\n    115d:       e8 05 00 00 00          callq  1167 <bar>\n    1162:       83 c0 2a                add    $0x2a,%eax\n    1165:       c9                      leaveq\n    1166:       c3                      retq\n\n0000000000001167 <bar>:\n    1167:       f3 0f 1e fa             endbr64\n    116b:       55                      push   %rbp\n    116c:       48 89 e5                mov    %rsp,%rbp\n    116f:       89 7d fc                mov    %edi,-0x4(%rbp)\n    1172:       8b 45 fc                mov    -0x4(%rbp),%eax\n    1175:       01 c0                   add    %eax,%eax\n    1177:       5d                      pop    %rbp\n    1178:       c3                      retq\n\n0000000000001179 <main>:\n    1179:       f3 0f 1e fa             endbr64\n    117d:       55                      push   %rbp\n    117e:       48 89 e5                mov    %rsp,%rbp\n    1181:       bf 2a 00 00 00          mov    $0x2a,%edi\n    1186:       e8 be ff ff ff          callq  1149 <foo>\n    118b:       89 c6                   mov    %eax,%esi\n    118d:       48 8d 3d 70 0e 00 00    lea    0xe70(%rip),%rdi\n    1194:       b8 00 00 00 00          mov    $0x0,%eax\n    1199:       e8 b2 fe ff ff          callq  1050 <printf@plt>\n    119e:       b8 00 00 00 00          mov    $0x0,%eax\n    11a3:       5d                      pop    %rbp\n    11a4:       c3                      retq\n    11a5:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)\n    11ac:       00 00 00\n    11af:       90                      nop\n

    On constate que les appels de fonctions ont \u00e9t\u00e9 bien remplac\u00e9s par les bons noms\u2009:

    • 115d Appel de bar
    • 1186 Appel de foo
    • 1199 Appel de printf
    ", "tags": ["bar", "objdump", "printf", "foo", "foo.c"]}, {"location": "course-c/35-libraries/introduction/", "title": "Biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques sont des collections de fonctions et de types de donn\u00e9es qui peuvent \u00eatre utilis\u00e9es dans des programmes. Elles sont g\u00e9n\u00e9ralement fournies par le syst\u00e8me d'exploitation ou par des tiers. Les biblioth\u00e8ques sont utilis\u00e9es pour \u00e9tendre les fonctionnalit\u00e9s d'un programme sans avoir \u00e0 r\u00e9\u00e9crire le code \u00e0 partir de z\u00e9ro.

    "}, {"location": "course-c/35-libraries/standard-library/", "title": "Biblioth\u00e8ques standard", "text": "

    Aux premi\u00e8res heures de l'informatique (ann\u00e9es 1950 et 1960), les programmeurs \u00e9crivaient du code tr\u00e8s sp\u00e9cifique \u00e0 la machine, g\u00e9n\u00e9ralement en langage assembleur. Il n'y avait pas de biblioth\u00e8ques standard ou de frameworks, et les programmeurs devaient souvent \u00e9crire eux-m\u00eames des fonctionnalit\u00e9s de base comme la gestion des entr\u00e9es/sorties ou les op\u00e9rations math\u00e9matiques. L'id\u00e9e de r\u00e9utilisabilit\u00e9 de code \u00e9tait encore peu d\u00e9velopp\u00e9e. Les langages \u00e9taient souvent con\u00e7us pour une seule machine, ce qui limitait les possibilit\u00e9s de portabilit\u00e9.

    Le langage C est l'un des premiers langages \u00e0 introduire une biblioth\u00e8que standard appel\u00e9e C standard library (libc). Cette biblioth\u00e8que visait \u00e0 fournir un ensemble de fonctions de base pour faciliter le d\u00e9veloppement d'applications. Elle contenait des fonctions pour la gestion des cha\u00eenes de caract\u00e8res, des fichiers, de la m\u00e9moire, des maths, etc. Avant cela, ces fonctionnalit\u00e9s devaient \u00eatre \u00e9crites par chaque programmeur pour chaque projet. L'ajout de cette biblioth\u00e8que standard a permis de simplifier consid\u00e9rablement le d\u00e9veloppement en \u00e9vitant de r\u00e9inventer la roue pour chaque projet.

    Java, lanc\u00e9 par Sun Microsystems, a introduit une biblioth\u00e8que standard extr\u00eamement riche d\u00e8s sa premi\u00e8re version, connue sous le nom de Java Standard Library. Elle couvrait un large \u00e9ventail de domaines (gestion des entr\u00e9es/sorties, interfaces graphiques, r\u00e9seau, etc.). Le fait que Java soit livr\u00e9 avec une biblioth\u00e8que compl\u00e8te et uniforme a jou\u00e9 un r\u00f4le crucial dans sa popularit\u00e9. Java a \u00e9galement introduit des frameworks comme Swing pour les interfaces graphiques et a encourag\u00e9 l'utilisation d'APIs standardis\u00e9es. Python, cr\u00e9\u00e9 par Guido van Rossum en 1991, a aussi adopt\u00e9 tr\u00e8s t\u00f4t l'id\u00e9e d'une biblioth\u00e8que standard compl\u00e8te, appel\u00e9e Python Standard Library. Python est souvent lou\u00e9 pour son approche \u00ab\u2009batteries included\u2009\u00bb (\u00ab\u2009batteries incluses\u2009\u00bb), signifiant que le langage fournit une vaste gamme d'outils pr\u00eats \u00e0 l'emploi. Cela a fait de Python un langage tr\u00e8s populaire pour le d\u00e9veloppement rapide d'applications.

    Aujourd'hui, pratiquement tous les langages modernes (comme Rust, Go, Swift, Kotlin) sont livr\u00e9s avec des biblioth\u00e8ques standard \u00e9tendues, ainsi que des frameworks et des outils de gestion de paquets (comme npm pour JavaScript, pip pour Python, cargo pour Rust, etc.). Ces outils permettent aux d\u00e9veloppeurs d\u2019acc\u00e9der \u00e0 des milliers de biblioth\u00e8ques tierces et \u00e0 des frameworks qui simplifient la construction d\u2019applications complexes.

    En C, la situation n'a pas beaucoup \u00e9volu\u00e9e depuis les ann\u00e9es 70. Le langage C est un langage de bas niveau qui ne fournit toujours pas de biblioth\u00e8que standard \u00e9tendue. Si une des raison est la portabilit\u00e9 des programmes, une autre raison est que le langage C est un langage minimaliste. Il a \u00e9t\u00e9 con\u00e7u pour \u00eatre simple et efficace, et les concepteurs ont d\u00e9lib\u00e9r\u00e9ment choisi de ne pas inclure de fonctionnalit\u00e9s avanc\u00e9es dans le langage lui-m\u00eame. Il existe donc en C une seule biblioth\u00e8que la libc qui souffre de quelques lacunes et incoh\u00e9rences par le fait de son anciennet\u00e9 et de la n\u00e9cessit\u00e9 de conserver la compatibilit\u00e9 avec les anciennes versions.

    La libc reste n\u00e9anmoins un outil indispensable pour le d\u00e9veloppeur C. Nous allons voir dans ce chapitre les diff\u00e9rents fichiers d'en-t\u00eate et fonctions qu'elle propose en montrant quelques exemples d'utilisation.

    En-t\u00eates standard En-t\u00eate Description Standard <assert.h> Validation des pr\u00e9requis C89 <complex.h> Nombres complexes C99 <ctype.h> Tests C89 <errno.h> Gestion des erreurs C89 <fenv.h> Environnement de calcul flottant C99 <float.h> Constantes de pr\u00e9cision des types flottants C89 <inttypes.h> Types entiers format\u00e9s C99 <iso646.h> Alternative aux op\u00e9rateurs (and, or) C95 <limits.h> Limites des types entiers C89 <locale.h> Gestion des locales C89 <math.h> Fonctions math\u00e9matiques C89 <setjmp.h> Gestion des sauts C89 <signal.h> Gestion des signaux C89 <stdalign.h> Alignement des types C11 <stdarg.h> Arguments variables C89 <stdatomic.h> Op\u00e9rations atomiques C11 <stdbit.h> Macros pour les bits C23 <stdbool.h> Type bool\u00e9en C99 <stdckdint.h> Macros de tests pour les entiers C23 <stddef.h> Macros standard C89 <stdint.h> Types entiers standard C99 <stdio.h> Entr\u00e9es/sorties standard C89 <stdlib.h> Allocation dynamique C89 <stdnoreturn.h> Fonctions sans retour C11 <string.h> Manipulation des cha\u00eenes de caract\u00e8res C89 <tgmath.h> Fonctions math\u00e9matiques g\u00e9n\u00e9riques C99 <threads.h> Gestion des threads C11 <time.h> Date et heure C89 <uchar.h> Caract\u00e8res Unicode C11 <wchar.h> Caract\u00e8res larges C95 <wctype.h> Tests larges C95

    "}, {"location": "course-c/35-libraries/standard-library/#asserth", "title": "<assert.h>", "text": "

    On peut bien se demander \u00e0 quoi sert un en-t\u00eate <assert.h> qui ne contient qu'une seule fonction. La fonction assert est une fonction tr\u00e8s utile pour valider des pr\u00e9requis. Elle s'utilise principalement pour du d\u00e9bogage mais parfois pour s'assurer qu'une expression qui \u00e0 priori ne devrait jamais valoir false est bien vraie. L'en-t\u00eate offre deux prototypes qui sont en r\u00e9alit\u00e9 des macros\u2009:

    int assert(int expression);\nstatic_assert(int expression, \"message\");\n

    L'utilisation de assert permet de d\u00e9tecter les erreurs pendant la phase de d\u00e9veloppement ou de test. Si une condition critique n'est pas respect\u00e9e (par exemple, un pointeur nul ou une division par z\u00e9ro), le programme s'arr\u00eate avec une information pr\u00e9cieuse pour le d\u00e9bogage.

    #include <assert.h>\n#include <memory.h>\nint main() {\n    void *p = malloc(10);\n    assert(p != NULL);\n    ...\n}\n

    La grande force d'assert est qu'elle peut \u00eatre d\u00e9sactiv\u00e9e dans un environnement de production en d\u00e9finissant la macro NDEBUG. Lorsque NDEBUG est d\u00e9fini, toutes les assertions sont remplac\u00e9es par des expressions nulles (ne font rien), ce qui \u00e9limine toute surcharge due aux v\u00e9rifications. D'une fa\u00e7on simplifi\u00e9e, NDEBUG pourrait \u00eatre impl\u00e9ment\u00e9 comme ceci\u2009:

    #ifdef NDEBUG\n    #define assert(ignore) ((void)0)\n#else\n    #define assert(expr) \\\n        ((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__, __func__))\n#endif\n

    Si vous souhaitez d\u00e9sactiver les assertions, vous pouvez aussi le faire en ajoutant -DNDEBUG \u00e0 la ligne de commande du compilateur. Par exemple\u2009:

    gcc -DNDEBUG -o foo main.c\n

    Avant l'en-t\u00eate

    Il est important de d\u00e9clarer NDEBUG avant d'inclure l'en-t\u00eate <assert.h>. En effet, l'en-t\u00eate <assert.h> va d\u00e9finir la macro assert qui sera utilis\u00e9e dans le code. Si NDEBUG est d\u00e9fini apr\u00e8s l'inclusion de l'en-t\u00eate, la macro assert ne sera pas correctement d\u00e9finie.

    ", "tags": ["assert", "NDEBUG", "false"]}, {"location": "course-c/35-libraries/standard-library/#errnoh", "title": "<errno.h>", "text": "

    La biblioth\u00e8que <errno.h> est utilis\u00e9e pour g\u00e9rer les erreurs. Elle d\u00e9finit une variable globale errno qui est un entier qui contient le code de l'erreur modifi\u00e9 par certaines fonctions de la biblioth\u00e8que standard.

    Des macros sont \u00e9galement d\u00e9finies selon le standard POSIX pour les codes d'erreurs. Par exemple, EACCES pour une erreur d'acc\u00e8s, ENOENT pour un fichier ou r\u00e9pertoire inexistant, ENOMEM pour une erreur d'allocation m\u00e9moire, etc. La liste \u00e9tant relativement longue elle peut \u00eatre consult\u00e9e directmenet sur le standard POSIX. N\u00e9anmoins voici les erreurs les plus courantes\u2009:

    Codes d'erreurs POSIX les plus courants Code Description EACCES Permission refus\u00e9e (p.ex. sur un fichier) EADDRINUSE Adresse d\u00e9j\u00e0 utilis\u00e9e (socket) ECONNREFUSED Connexion refus\u00e9e (socket) EDOM Erreur de domaine math\u00e9matique EEXIST Fichier existe d\u00e9j\u00e0 EINVAL Argument invalide ENOENT Fichier ou r\u00e9pertoire inexistant ENOMEM Pas assez de m\u00e9moire disponible ENOSPC Plus d'espace disponible sur le p\u00e9riph\u00e9rique

    Par exemple, lors du calcul du logarithme d'un nombre n\u00e9gatif, la fonction log va d\u00e9finir errno \u00e0 EDOM pour indiquer une erreur de domaine. Il est possible de r\u00e9initialiser errno \u00e0 z\u00e9ro en utilisant la fonction clearerr ou errno = 0.

    #include <stdio.h>\n#include <math.h>\n#include <errno.h>\n#include <string.h>\n\nint main(void)\n{\n    errno = 0;\n    printf(\"log(-1.0) = %f\\n\", log(-1.0)); // Domain error\n    printf(\"%s\\n\\n\",strerror(errno));\n\n    errno = 0;\n    printf(\"log(0.0)  = %f\\n\", log(0.0));\n    printf(\"%s\\n\",strerror(errno)); // Numerical result out of range\n}\n

    ", "tags": ["EEXIST", "log", "EACCES", "ENOENT", "ECONNREFUSED", "EDOM", "EADDRINUSE", "ENOSPC", "EINVAL", "ENOMEM", "errno", "clearerr"]}, {"location": "course-c/35-libraries/standard-library/#mathh", "title": "<math.h>", "text": "

    La biblioth\u00e8que math\u00e9matique est une des plus utilis\u00e9es. Elle contient des fonctions pour les op\u00e9rations math\u00e9matiques de base. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement. Le fichier d'en-t\u00eate est le suivant et le flag de compilation est -lm.

    Constantes math\u00e9matiques Constantes Description M_PI Valeur de \\(\\pi\\) M_E Valeur de \\(e\\) M_SQRT1_2 Valeur de \\(1/\\sqrt(2)\\)

    Windows

    Attention, ces constantes ne sont pas d\u00e9finies par le standard C, mais par le standard POSIX. Il est donc possible que certaines impl\u00e9mentations ne les d\u00e9finissent pas, en particulier sous Windows.

    Fonctions math\u00e9matiques Fonction Description exp(x) Exponentielle \\(e^x\\) ldexp(x,n) Exposant d'un nombre flottant \\(x\\cdot2^n\\) log(x) Logarithme binaire \\(\\log_{2}(x)\\) log10(x) Logarithme d\u00e9cimal \\(\\log_{10}(x)\\) pow(x,y) Puissance \\(x^y\\) sqrt(x) Racine carr\u00e9e \\(\\sqrt(x)\\) cbrt(x) Racine cubique \\(\\sqrt[3](x)\\) hypot(x,y) Hypot\u00e9nuse optimis\u00e9 \\(\\sqrt(x^2 + y^2)\\) ceil Arrondi \u00e0 l'entier sup\u00e9rieur floor Arrondi \u00e0 l'entier inf\u00e9rieur

    Notons par exemple que la fonction hypot peut tr\u00e8s bien \u00eatre \u00e9mul\u00e9e facilement en utilisant la fonction sqrt. N\u00e9anmoins elle existe pour deux raisons \u00e9l\u00e9mentaires\u2009:

    1. \u00c9viter les d\u00e9passements (overflow).
    2. Une meilleure optimisation du code.

    Souvent, les processeurs sont \u00e9quip\u00e9s de coprocesseurs arithm\u00e9tiques capables de calculer certaines fonctions plus rapidement.

    Le standard C99 a introduit l'en-t\u00eate <tgmath.h> qui donne acc\u00e8s \u00e0 des fonctions g\u00e9n\u00e9riques. Par exemple, sin peut \u00eatre utilis\u00e9 pour des float, double et long double sans avoir \u00e0 choisir le nom de la fonction (sinf, sin, sinl), en outre les types complexes sont \u00e9galement support\u00e9s comme csin pour les complexes.

    ", "tags": ["double", "M_PI", "floor", "sinl", "ceil", "sin", "M_SQRT1_2", "sinf", "M_E", "hypot", "float", "csin", "sqrt"]}, {"location": "course-c/35-libraries/standard-library/#fenvh", "title": "<fenv.h>", "text": "

    La biblioth\u00e8que <fenv.h> est \u00e9troitement li\u00e9e aux calculs math\u00e9matique et permet de manipuler l'environnement de calcul flottant. Elle permet de contr\u00f4ler les modes de calculs, les exceptions et les arrondis. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement.

    La structure fenv_t contient l'\u00e9tat de l'environnement de calcul flottant et la structure fexcept_t contient les exceptions de calcul flottant. Ces structures sont opaques et ne doivent pas \u00eatre manipul\u00e9es directement, elles d\u00e9pendent de l'impl\u00e9mentation et peuvent varier d'un syst\u00e8me \u00e0 l'autre. N\u00e9anmoins pour X86, voici \u00e0 quoi elles pourraient ressembler pour les curieux.

    typedef struct {\n    // Mot de contr\u00f4le de la FPU.\n    union {\n        unsigned int __control_word;\n        struct {\n            // Masque des exceptions de la FPU\n            unsigned int IM : 1;  // Invalid Operation\n            unsigned int DM : 1;  // Denormalized Operand\n            unsigned int ZM : 1;  // Zero-Divide\n            unsigned int OM : 1;  // Overflow\n            unsigned int UM : 1;  // Underflow\n            unsigned int PM : 1;  // Precision\n            unsigned int _Reserved : 2;\n            // Gestion de l'arrondi et de la pr\u00e9cision\n            unsigned int PC : 2;  // Precision Control\n            unsigned int RC : 2;  // Rounding Control mode\n            unsigned int _FPUReserved : 3;\n            unsigned int IC : 1;  // Plus utilis\u00e9\n        };\n    };\n\n    // Word de statut de la FPU (indicateurs d'\u00e9tat et d'exception)\n    union {\n        unsigned int __status_word;\n        struct {\n            // Indicateurs d'exception\n            unsigned int IE : 1;  // Invalid Operation Exception\n            unsigned int DE : 1;  // Denormalized Operand Exception\n            unsigned int ZE : 1;  // Zero-Divide Exception\n            unsigned int OE : 1;  // Overflow Exception\n            unsigned int UE : 1;  // Underflow Exception\n            unsigned int PE : 1;  // Precision Exception\n            unsigned int SF : 1;  // Stack Fault\n            unsigned int ES : 1;  // Exception Summary Status\n\n            // Indicateurs d'\u00e9tat pour le stockage de valeurs\n            // interm\u00e9diaires durant les calculs\n            unsigned int C0 : 1;\n            unsigned int C1 : 1;\n            unsigned int C2 : 1;\n            unsigned int Top : 3;  // Position du sommet de la pile\n            unsigned int C3 : 1;\n            unsigned int Busy : 1;  // FPU occup\u00e9e\n        };\n    };\n    unsigned int __tag_word;\n    unsigned int __fpu_ip; // Instruction pointer\n    unsigned int __fpu_cs; // Code segment\n    unsigned int __opcode; // Opcode de l'op\u00e9ration en cours\n    unsigned int __fpu_dp; // Data pointer\n    unsigned int __mxcsr; // Registre MXCSR (contr\u00f4le des exceptions SSE)\n    unsigned int __mxcsr_mask; // Masque de contr\u00f4le MXCSR\n} fenv_t;\n
    ", "tags": ["double", "float", "fenv_t", "fexcept_t"]}, {"location": "course-c/35-libraries/standard-library/#controle-des-exceptions", "title": "Contr\u00f4le des exceptions", "text": "

    Il est possible de g\u00e9rer les exceptions de calculs flottants comme\u2009:

    • la division par z\u00e9ro,
    • le d\u00e9passement de capacit\u00e9 (overflow),
    • un r\u00e9sultat non num\u00e9rique (NaN),
    • un sous-d\u00e9passement de capacit\u00e9 (underflow),
    • une perte de pr\u00e9cision.

    Dans certains programmes, notamment ceux impliquant des calculs num\u00e9riques intensifs ou critiques (comme en science ou en ing\u00e9nierie), il est important de savoir si une op\u00e9ration en virgule flottante a \u00e9chou\u00e9 ou produit un r\u00e9sultat incorrect.

    #include <stdio.h>\n#include <fenv.h>\n#pragma STDC FENV_ACCESS ON  // NECESSAIRE\n\nint main() {\n    feclearexcept(FE_ALL_EXCEPT); // Efface anciennes exceptions\n\n    double result = 1.0 / 0.0; // Division par z\u00e9ro\n\n    if (fetestexcept(FE_DIVBYZERO)) {\n        printf(\"Erreur : division par z\u00e9ro d\u00e9tect\u00e9e\\n\");\n    }\n}\n
    "}, {"location": "course-c/35-libraries/standard-library/#controle-de-larrondi", "title": "Contr\u00f4le de l'arrondi", "text": "

    Il est aussi possible contr\u00f4ler la mani\u00e8re dont les r\u00e9sultats des op\u00e9rations en virgule flottante sont arrondis. Par d\u00e9faut, les op\u00e9rations en virgule flottante arrondissent au plus proche, mais vous pouvez modifier ce comportement pour arrondir vers z\u00e9ro, vers l'infini, ou vers moins l'infini.

    Nous avions vu pr\u00e9c\u00e9demment que l'arrondi d'un nombre est compliqu\u00e9. La norme IEEE 754 d\u00e9finit plusieurs modes d'arrondis. La fonction fesetround permet de d\u00e9finir le mode d'arrondi. Les modes possibles sont donn\u00e9s par la table suivante\u2009:

    Modes d'arrondis Mode Description \\(-3.5\\) $-2.5 $2.5 3.5$ FE_TONEAREST Arrondi bancaire \\(-4\\) \\(-2\\) \\(2\\) \\(4\\) FE_DOWNWARD Arrondi vers z\u00e9ro \\(-4\\) \\(-3\\) \\(2\\) \\(3\\) FE_UPWARD Arrondi vers l'infini \\(-3\\) \\(-2\\) \\(3\\) \\(4\\) FE_TOWARDZERO Arrondi vers moins l'infini \\(-3\\) \\(-2\\) \\(2\\) \\(3\\) round() Comparaison avec round \\(-4\\) \\(-3\\) \\(3\\) \\(4\\)
    fesetround(FE_TONEAREST);\nint rounded = nearbyint(3.5);\n

    L'arrondi bancaire minimise les biais d'arrondi lorsqu'on fait des calculs sur de grandes quantit\u00e9s de donn\u00e9es. En arrondissant vers l'entier pair dans les cas o\u00f9 un nombre tombe exactement \u00e0 mi-chemin entre deux entiers, cette m\u00e9thode r\u00e9duit l'accumulation d'erreurs statistiques qui peuvent survenir avec d'autres m\u00e9thodes d'arrondi. En effet les valeurs sont arrondies vers l'entier pair le plus proche. Voici quelques exemples\u2009:

    -1.5 -> -2\n-0.5 ->  0\n 0.5 ->  0\n 1.5 ->  2\n 2.5 ->  2\n 3.5 ->  4\n

    round

    La configuration du mode d'arrondi avec fesetround affecte les fonctions nearbyint, rint mais pas round.

    La fonction round utilise un arrondi sp\u00e9cifique appel\u00e9 round half away from zero qui arrondit les valeurs \u00e0 l'entier le plus proche en s'\u00e9loignant de z\u00e9ro.

    Notez que la diff\u00e9rence entre rint et nearbyint est que nearbyint ne g\u00e9n\u00e8re pas d'exception en cas de d\u00e9passement de capacit\u00e9 (overflow).

    ", "tags": ["FE_UPWARD", "FE_TOWARDZERO", "rint", "round", "fesetround", "FE_DOWNWARD", "nearbyint", "FE_TONEAREST"]}, {"location": "course-c/35-libraries/standard-library/#floath", "title": "<float.h>", "text": "

    La biblioth\u00e8que <float.h> contient des constantes qui d\u00e9finissent la pr\u00e9cision des types flottants sur l'architecture cible. Les constantes sont d\u00e9finies pour les types float, double et long double.

    On y retrouve FLT_ROUNDS qui indique le mode d'arrondi par d\u00e9faut utilis\u00e9 \u00e0 la compilation.

    Dans IEEE 754, l'exposant est de base 2, c'est ce qu'on appelle le radix. Il peut \u00eatre contr\u00f4l\u00e9 avec la macro FLT_RADIX. Les constantes DBL_DIG et LDBL_DIG indiquent le nombre de chiffres significatifs que l'on peut stocker dans un double et un long double respectivement.

    Autre base\u2009?

    Aujourd'hui quasiment 100 pour cent des ordinateurs utilisent le radix 2. N\u00e9anmoins, \u00e0 une certaine \u00e9poque le radix 10 \u00e9tait utilis\u00e9 sur certaines architectures comme le l'IBM 650 (1953).

    La norme IEEE 754-2008 permet d'utiliser le radix 16, 10 ou 2. Elle d\u00e9fini notament la reps\u00e9entation DFP (Decimal Floating Point) qui permet de repr\u00e9senter les nombres d\u00e9cimaux de mani\u00e8re exacte. Cependant l'impl\u00e9mentation physique d'une FPU en radix 10 est plus complexe et moins performante c'est pour cela que la vaste majorit\u00e9 des processeurs utilisent le radix 2 suffisant pour la plupart des applications.

    ", "tags": ["double", "DBL_DIG", "FLT_ROUNDS", "FLT_RADIX", "float", "LDBL_DIG"]}, {"location": "course-c/35-libraries/standard-library/#complexh", "title": "<complex.h>", "text": "

    La biblioth\u00e8que <complex.h> permet de manipuler les nombres complexes. Les fonctions sont d\u00e9finies pour les types float, double et long double avec les pr\u00e9fixes f, l et sans pr\u00e9fixe respectivement.

    Un nombre complexe est d\u00e9fini par une partie r\u00e9elle et une partie imaginaire. Le type _Complex est un type de base, mais il peut \u00eatre utilis\u00e9 de mani\u00e8re plus simple avec la macro complex. Pour d\u00e9finir un nombre complexe, on utilise la notation a + bi o\u00f9 a est la partie r\u00e9elle et b la partie imaginaire.

    #include <complex.h>\n\nint main() {\n    double complex z = 1.0 + 2.0 * I;\n    double complex w = 3.0 + 4.0 * I;\n    double complex sum = z + w;\n    printf(\"Somme : %f + %fi\\n\", creal(sum), cimag(sum));\n\n    // Nombre imaginaire pur\n    double imaginary = -2.0 * I;\n}\n

    La constante I est bien entendue d\u00e9finie comme 1.0i. Toutes les fonctions complexes se d\u00e9clines en trois versions\u2009:

    Variantes de type complexe Type Exemple de fonction float cabsf, csinf double cabs, csin long double cabsl, csinl Fonctions complexes Fonction Description cabs(z) Module cacos(z) Arc cosinus cacosh(z) Arc cosinus hyperbolique carg(z) Argument \\(\\arg(z)\\) casin(z) Arc sinus casinh(z) Arc sinus hyperbolique catan(z) Arc tangente catanh(z) Arc tangente hyperbolique ccos(z) Cosinus ccosh(z) Cosinus hyperbolique cexp(z) Exponentielle \\(e^z\\) cimag(z) Partie imaginaire \\(\\Im z\\) clog(z) Logarithme \\(\\log(z)\\) conj(z) Conjugaison \\(\\bar(z)\\) cpow(z,w) Puissance \\(z^w\\) creal(z) Partie r\u00e9elle \\(\\Re z\\) csin(z) Sinus csinh(z) Sinus hyperbolique csqrt(z) Racine carr\u00e9e \\(\\sqrt{z}\\) ctan(z) Tangente ctanh(z) Tangente hyperbolique

    Certaines extensions pr\u00e9vue possiblement avec C23 am\u00e8nerait des fonctionnalit\u00e9s suppl\u00e9mentaires telles que cexp2, clog2, cexp10, clog10, crootn ...

    ", "tags": ["double", "cexp2", "crootn", "clog10", "cexp10", "cabs", "_Complex", "complex", "cabsl", "csinl", "clog2", "csinf", "float", "csin", "cabsf"]}, {"location": "course-c/35-libraries/standard-library/#iso646h", "title": "<iso646.h>", "text": "

    L'en-t\u00eate <iso646.h> est une extension du standard C95 qui d\u00e9finit des alternatives aux op\u00e9rateurs logiques. Les op\u00e9rateurs logiques sont d\u00e9finis avec des symbols (&&, ||, !) mais pour des raisons de lisibilit\u00e9, il est possible de les d\u00e9finir en anglais (and, or, not).

    Macros de l'ISO/IEC 646 Op\u00e9rateur Macro Description && and ET logique \\|\\| or OU logique ! not NON logique != not_eq Diff\u00e9rent de &= and_eq ET binaire \\|= or_eq OU binaire ^= xor_eq OU exclusif binaire ^ xor OU exclusif binaire ~ compl Compl\u00e9ment binaire

    Ces macros sont utiles pour les personnes qui ne peuvent pas taper certains caract\u00e8res sp\u00e9ciaux sur leur clavier. Elles sont \u00e9galement utiles pour les personnes qui veulent rendre leur code plus lisible. N\u00e9anmoins, elles ne sont pas tr\u00e8s utilis\u00e9es en pratique.

    #include <iso646.h>\n\nint foo(int a, int b, int c) {\n    return a and b or not c;\n}\n

    Je vous recommande personnellement de ne pas utiliser ces macros. Elles ne sont pas tr\u00e8s utilis\u00e9es et peuvent rendre le code moins lisible pour les autres d\u00e9veloppeurs.

    ", "tags": ["and", "xor_eq", "not", "compl", "and_eq", "xor", "or_eq", "not_eq"]}, {"location": "course-c/35-libraries/standard-library/#limitsh", "title": "<limits.h>", "text": "

    La biblioth\u00e8que <limits.h> contient des constantes qui d\u00e9finissent les limites des types entiers de base. Les constantes sont d\u00e9finies pour les types char, short, int, long, long long et float, double, long double.

    Limites des entiers de base Constante Description LP64 CHAR_BIT Nombre de bits dans un char 8 CHAR_MAX Valeur maximale d'un char 127 CHAR_MIN Valeur minimale d'un char -128 SCHAR_MAX Valeur maximale d'un signed char 127 SCHAR_MIN Valeur minimale d'un signed char -128 UCHAR_MAX Valeur maximale d'un unsigned char 255 SHRT_MAX Valeur maximale d'un short 32767 SHRT_MIN Valeur minimale d'un short -32768 USHRT_MAX Valeur maximale d'un unsigned short 65535 INT_MAX Valeur maximale d'un int 2147483647 INT_MIN Valeur minimale d'un int -2147483648 UINT_MAX Valeur maximale d'un unsigned int 4294967295 LONG_MAX Valeur maximale d'un long 9223372036854775807 LONG_MIN Valeur minimale d'un long -9223372036854775808 ULONG_MAX Valeur maximale d'un unsigned long 18446744073709551615 LLONG_MAX Valeur maximale d'un long long 9223372036854775807 LLONG_MIN Valeur minimale d'un long long -9223372036854775808 ULLONG_MAX Valeur maximale d'un unsigned long long 18446744073709551615

    ", "tags": ["LONG_MAX", "INT_MAX", "SCHAR_MAX", "LLONG_MAX", "ULLONG_MAX", "LONG_MIN", "LLONG_MIN", "USHRT_MAX", "SHRT_MAX", "ULONG_MAX", "CHAR_MIN", "long", "int", "double", "UINT_MAX", "SHRT_MIN", "char", "float", "UCHAR_MAX", "SCHAR_MIN", "CHAR_MAX", "CHAR_BIT", "INT_MIN", "short"]}, {"location": "course-c/35-libraries/standard-library/#localeh", "title": "<locale.h>", "text": "

    En jargon informatique, la locale est un ensemble de param\u00e8tres qui d\u00e9finissent les conventions culturelles d'une r\u00e9gion. Cela inclut la langue, le format de date, le format de nombre, etc. La biblioth\u00e8que <locale.h> permet de manipuler ces param\u00e8tres.

    Un syst\u00e8me d'exploitation d\u00e9fini g\u00e9n\u00e9ralement une locale par d\u00e9faut. Par exemple, un syst\u00e8me en fran\u00e7ais utilisera la locale fr_FR. Le premier param\u00e8tre est la langue de la locale et le second et le pays. Il existe en effet des diff\u00e9rences entre le fran\u00e7ais suisse fr_CH et le fran\u00e7ais canadien fr_CA.

    Ces conventions sont d\u00e9finie par la norme ISO 15897 et font de surcro\u00eet partie du standard POSIX.

    L'en-t\u00eate <locale.h> contient donc des fonctions pour manipuler les locales.

    Contenu de locale.h Fonction Description setlocale D\u00e9finit la locale localeconv R\u00e9cup\u00e8re les param\u00e8tres de la locale lconv Structure retourn\u00e9e par localeconv

    Voici un exemple\u2009:

    #include <locale.h>\n#include <stdio.h>\n\nint main() {\n    setlocale(LC_ALL, \"fr_FR\");\n    struct lconv *locale = localeconv();\n    printf(\"S\u00e9parateur de milliers : %s\\n\", locale->thousands_sep);\n    printf(\"S\u00e9parateur d\u00e9cimal : %s\\n\", locale->decimal_point);\n}\n

    La structure lconv est d\u00e9finie comme suit\u2009:

    struct lconv {\n    char *decimal_point;  // S\u00e9parateur d\u00e9cimal\n    char *thousands_sep;  // S\u00e9parateur de milliers\n    char *grouping;       // Taille des groupes de milliers\n    char *int_curr_symbol; // Symbole de la monnaie\n    char *currency_symbol; // Symbole de la monnaie\n    char *mon_decimal_point; // S\u00e9parateur d\u00e9cimal de la monnaie\n    char *mon_thousands_sep; // S\u00e9parateur de milliers de la monnaie\n    char *mon_grouping;      // Taille des groupes de milliers de la monnaie\n    char *positive_sign;     // Signe positif\n    char *negative_sign;     // Signe n\u00e9gatif\n    char int_frac_digits;    // D\u00e9cimales pour la monnaie\n    char frac_digits;        // D\u00e9cimales\n    char p_cs_precedes;      // Symbole avant la monnaie\n    char p_sep_by_space;     // Espace entre la monnaie et le montant\n    char n_cs_precedes;      // Symbole avant la monnaie n\u00e9gative\n    char n_sep_by_space;     // Espace entre la monnaie n\u00e9gative et le montant\n    char p_sign_posn;        // Position du signe positif\n    char n_sign_posn;        // Position du signe n\u00e9gatif\n};\n

    Voici un exemple plus complet\u2009:

    #include <locale.h>\n\nint main() {\n    char country[] = \"EN\";\n    printf(\"Vous parlez fran\u00e7ais, tant mieux. Quel est votre pays ? \");\n    if (scanf(\"%2s\", &country) != 1) {\n        printf(\"Erreur de saisie\\n\");\n        return 1;\n    }\n    for (int i = 0; country[i]; i++) country[i] = toupper(country[i]);\n\n    char locale[6];\n    snprintf(locale, 6, \"fr_%s\", country);\n\n    setlocale(LC_ALL, locale);\n\n    float apple_unit_price;\n    printf(\"Quel est le prix d'une pomme ? \");\n    if (scanf(\"%f\", &apple_unit_price) != 1) {\n        printf(\"Erreur de saisie\\n\");\n        return 1;\n    }\n    const int quantity = 10;\n    const float ten_apples_price = apple_unit_price * quantity;\n\n    printf(\"Le prix de %d pommes est de %.2f %s\\n\",\n        quantity, ten_apples_price, localeconv()->currency_symbol);\n}\n

    Avec la fonction setlocale, il est possible de ne d\u00e9finir qu'une partie des conventions \u00e0 utiliser. Par exemple, si on ne veut changer que le format de date, on peut utiliser setlocale(LC_TIME, \"fr_FR\").

    Cat\u00e9gories de locales Cat\u00e9gorie Description LC_ALL Toutes les cat\u00e9gories LC_COLLATE Comparaison de cha\u00eenes LC_CTYPE Caract\u00e8res et conversions LC_MONETARY Format mon\u00e9taire LC_NUMERIC Format num\u00e9rique LC_TIME Format de date et heure

    ", "tags": ["fr_FR", "LC_CTYPE", "LC_TIME", "LC_MONETARY", "lconv", "LC_COLLATE", "LC_NUMERIC", "fr_CH", "LC_ALL", "setlocale", "localeconv", "fr_CA"]}, {"location": "course-c/35-libraries/standard-library/#setjmph", "title": "<setjmp.h>", "text": "

    La biblioth\u00e8que <setjmp.h> permet de g\u00e9rer les exceptions en C. Elle fournit deux fonctions setjmp et longjmp qui permettent de sauvegarder l'\u00e9tat du programme et de le restaurer \u00e0 un point donn\u00e9.

    En pratique il est tr\u00e8s rare d'utiliser ces fonctions, elles sont aussi dangereuses que les goto et peuvent rendre le code difficile \u00e0 lire et \u00e0 maintenir. N\u00e9anmoins dans des cas tr\u00e8s sp\u00e9cifiques, elles peuvent s'av\u00e9rer tr\u00e8s utiles, notament pour simuler des exceptions avec des directives pr\u00e9processeur.

    Nous avons vu que le compilateur utilise la pile pour stocker les variables locales et le contexte d'appel des fonctions. Dans chaque frame de la pile, on trouve l'adresse de retour permettant de continuer l'ex\u00e9cution d'une fonction dans la fonction appelante une fois la fonction courrante termin\u00e9e. Ceci permet de communiquer hi\u00e9rarchiquement entre les fonctions. Il n'est pas possible par exemple de remonter \u00e0 la fonction main depuis une fonction baz appel\u00e9e par bar appel\u00e9e par foo, appel\u00e9e par main.

    Ce n'est pas possible... sauf si on triche un peu. La fonction setjmp permet de sauvegarder l'\u00e9tat du programme \u00e0 un point donn\u00e9. C'est-\u00e0-dire que si on sauve le contexte de main dans un espace m\u00e9moire s\u00e9par\u00e9 avant la cha\u00eene d'appel de fonctions enfants, on pourrait manipuler le stack pour revenir \u00e0 main depuis baz. C'est tr\u00e8s exactement ce que fait setjmp.

    La fonction setjmp prend un seul argument qui est une structure de type jmp_buf. Cette structure est opaque car elle d\u00e9pend du compilateur et de la mani\u00e8re dont le stack est impl\u00e9ment\u00e9. N\u00e9anmoins sur une architecture x86, elle pourrait ressembler \u00e0 ceci\u2009:

    typedef struct {\n    unsigned int __eip; // Instruction pointer\n    unsigned int __esp; // Stack pointer\n    unsigned int __ebp; // Base pointer\n    unsigned int __ebx; // Base register\n    unsigned int __esi; // Source index\n    unsigned int __edi; // Destination index\n} jmp_buf[1];\n

    Il s'agit des registres du processeur concern\u00e9s par la gestion du stack et le d\u00e9roulement du programme. Le registre eip par exemple est le pointeur d'instruction. Il contient l'adresse de la prochaine instruction \u00e0 ex\u00e9cuter. Si ce registre est modifi\u00e9, le programme saute \u00e0 une autre adresse. C'est ce que fait entre autre le goto, il modifie eip pour sauter \u00e0 une autre adresse.

    Pour marquer un point de retour, on utilise setjmp qui sauvegarde l'\u00e9tat du programme dans la structure jmp_buf. On peut ensuite revenir \u00e0 ce point avec longjmp. La fonction longjmp prend deux arguments, la structure jmp_buf et une valeur de retour qui peut \u00eatre utilis\u00e9e pour savoir pourquoi on est revenu \u00e0 ce point. Voici un exemple\u2009:

    #include <setjmp.h>\n\njmp_buf env; // Doit \u00eatre transverse \u00e0 toutes les fonctions, donc globale\n\nvoid foo() { longjmp(env, 42); }\nvoid bar() { foo(); }\nvoid baz() { bar(); }\nint main() {\n    int ret = setjmp(env); // Sauvegarde l'\u00e9tat du programme\n    if (ret == 0) {\n        printf(\"Premi\u00e8re ex\u00e9cution\\n\");\n        baz();\n    } else {\n        printf(\"Retour \u00e0 setjmp avec %d\\n\", ret);\n    }\n}\n

    Lors de l'appel de setjmp, la fonction retourne 0. Cette valeur peut \u00eatre utilis\u00e9e pour tester si c'est la premi\u00e8re fois que la fonction est appel\u00e9e ou si c'est un retour de longjmp. Dans ce cas, la fonction retourne la valeur pass\u00e9e \u00e0 longjmp.

    ", "tags": ["baz", "goto", "bar", "jmp_buf", "setjmp", "eip", "foo", "longjmp", "main"]}, {"location": "course-c/35-libraries/standard-library/#signalh", "title": "<signal.h>", "text": "

    Les signaux sont des m\u00e9canismes sp\u00e9cifiques aux syst\u00e8mes d'exploitations qui permettent de communiquer entre les processus (programmes) et le noyau. Un signal ne v\u00e9hicule pas de donn\u00e9es, il permet simplement de r\u00e9veiller un processus pour lui indiquer qu'un \u00e9v\u00e9nement s'est produit. Alternativement un signal peut \u00eatre \u00e9mis par un processus pour demander au noyau de r\u00e9aliser une action.

    Les signaux sont fondamentaux au sein d'un OS et de ce fait ils sont standardis\u00e9s par POSIX. Windows utilise \u00e9galement des signaux mais ils diff\u00e8rent un peu. En C, la biblioth\u00e8que <signal.h> permet de manipuler les signaux de mani\u00e8re portable avec quelques nuances. Voici les types de signaux les plus courants\u2009:

    Signaux POSIX (liste non exhaustive) Signal Description SIGABRT Abandon du processus SIGALRM Alarme horloge SIGFPE Erreur de calcul flottant SIGILL Instruction ill\u00e9gale SIGINT Interruption depuis le clavier SIGKILL Arr\u00eat forc\u00e9 du processus SIGPIPE \u00c9criture dans un tube sans lecteur SIGSEGV Violation de segmentation SIGTERM Demande d'arr\u00eat du processus SIGUSR1 Signal utilisateur 1 SIGUSR2 Signal utilisateur 2

    La fonction abort() disponible dans <stdlib.h> envoie un signal SIGABRT pour arr\u00eater le programme. Une violation d'acc\u00e8s \u00e0 un espace m\u00e9moire non allou\u00e9 envoie un signal SIGSEGV (la fameuse erreur de segmentation). Un d\u00e9passement de capacit\u00e9 en virgule flottante envoie un signal SIGFPE.

    Pour envoyer un signal \u00e0 un processus depuis le terminal, on utilise la commande kill. Par exemple, pour envoyer un signal SIGUSR1 au processus 1234, on utilise la commande kill -SIGUSR1 1234. Le 1234 est le PID (Process ID) du processus car chaque programme qui s'ex\u00e9cute a un identifiant unique attribu\u00e9 par le syst\u00e8me d'exploitation. Le nom de la commande kill peut \u00eatre trompeur car elle n'arr\u00eate pas le processus, elle lui envoie un signal. Le nom vient historiquement du fait que le signal par d\u00e9faut est SIGTERM qui demande au processus de s'arr\u00eater.

    Dans un programme C il est possible de capturer les signaux re\u00e7us en installant des handlers. Il s'agit d'une fonction qui sera appel\u00e9e de mani\u00e8re \u00e9v\u00e8nementielle lorsqu'un signal est re\u00e7u. Ces handlers court-circuitent donc le comportement normal du programme en interrompant l'action en cours. Voici un exemple pour capturer le signal SIGINT qui est envoy\u00e9 lorsqu'on appuie sur Ctrl+C pour interrompre un programme depuis le terminal\u2009:

    #include <signal.h>\n#include <stdio.h>\n\nvoid sigint_handler(int signum) {\n    printf(\"Vous partez d\u00e9j\u00e0 ? :(\\n\");\n    exit(0);\n}\n\nvoid jeudredi() {\n    char fruits[] = {\"banane\", \"kiwi\", \"ananas\", \"mangue\", \"cerise\"};\n    int i = 0;\n    while(1) {\n        printf(\"Il est tr\u00e8s bon ce coktail \u00e0 la %s !\\n\", fruits[i++ % 5]);\n        sleep(1); // Pause d'une seconde\n    }\n}\n\nint main() {\n    signal(SIGINT, sigint_handler);\n    jeudredi();\n}\n

    ", "tags": ["SIGSEGV", "SIGTERM", "kill", "SIGUSR1", "SIGALRM", "SIGINT", "SIGUSR2", "SIGFPE", "SIGILL", "SIGABRT", "SIGKILL", "SIGPIPE"]}, {"location": "course-c/35-libraries/standard-library/#stdalignh", "title": "<stdalign.h>", "text": "

    La biblioth\u00e8que <stdalign.h> fournit des fonctions pour manipuler l'alignement des donn\u00e9es en m\u00e9moire. L'alignement est une notion importante en informatique car les processeurs sont plus efficaces lorsqu'ils acc\u00e8dent \u00e0 des donn\u00e9es align\u00e9es. Imaginez un camion qui transporte des palettes de marchandises. La logistique est faite de mani\u00e8re \u00e0 ce que les palettes soient facile \u00e0 charger et d\u00e9charger du camion avec un minimum de manutention. Imaginez maintenant que vous voulez prendre un \u00e9l\u00e9ment d'une palette. Cela demande plus de travail parce que vous devez extraire l'\u00e9l\u00e9ment et trouver un autre outil pour le transporter. Un ordinateur 64-bits sur une architecture x86 a beaucoup de faciliter \u00e0 v\u00e9hiculer des mots de 8 octets et il s'arrangera en m\u00e9moire \u00e0 disposer les donn\u00e9es de la taille d'une palette (64-bits) de fa\u00e7on \u00e0 ce que son acc\u00e8s soit le plus rapide possible.

    La fonction alignof retourne l'alignement d'un type donn\u00e9. Par exemple, alignof(int) retourne 4 sur une architecture x86 32-bits et 8 sur une architecture x86 64-bits. La fonction alignas permet de sp\u00e9cifier l'alignement d'une variable ou d'une structure. Dans cet exemple, on demande \u00e0 ce que la variable a soit align\u00e9e sur 16 octets. L'adresse de a sera donc un multiple de 16. On peut le v\u00e9rifier avec la fonction alignof.

    #include <stdio.h>\n#include <stdalign.h>\n\nint main() {\n    alignas(16) int a;\n    printf(\"Alignement de int : %zu\\n\", alignof(int));\n    printf(\"Adresse de 'a' : %p\\n\", (void*)&a);\n}\n

    L'utilit\u00e9 de cette fonction est limit\u00e9e en pratique. Elle est principalement utilis\u00e9e pour manipuler des donn\u00e9es SIMD (Single Instruction Multiple Data) ou pour optimiser les performances de certaines structures de donn\u00e9es. On peut l'utiliser \u00e9galemenr pour accro\u00eetre l'interop\u00e9rabilit\u00e9 entre le C et d'autres langages de programmation.

    Une utilisation courante est avec des structures. L'exemple suivant montre une structure de 13 bytes mais le processeur, selon l'architecture pourrait d\u00e9cider de stocker le char sur 4 bytes et la structure serait donc de 16 bytes. On peut forcer l'alignement de la structure manuellement avec alignas(16).

    struct alignas(16) Data {\n    char c;    // 1 octet\n    int i;     // 4 octets\n    double d;  // 8 octets\n};\n

    ", "tags": ["alignas", "char", "alignof"]}, {"location": "course-c/35-libraries/standard-library/#stdargh", "title": "<stdarg.h>", "text": "

    Ne vous \u00eates-vous jamais demand\u00e9 quel est le prototype de printf ? Comment se fait-il que cette fonction puisse prendre un nombre variable d'arguments\u2009? La r\u00e9ponse est la biblioth\u00e8que <stdarg.h> qui permet de manipuler les arguments d'une fonction variable. Observons le prototype de printf :

    int printf(const char *format, ...);\n

    Notez les points de suspension ... apr\u00e8s le format. Il s'agit d'une fonction variadic, c'est-\u00e0-dire qu'elle peut prendre un nombre variable d'arguments. Rappelez-vous en relisant le chapitre sur la pile, que lorsqu'une fonction est appel\u00e9e, les diff\u00e9rents arguments sont empil\u00e9s sur la pile. Il est techniquement possible d'empiler autant d'arguments que l'on veut sans changer le comportement de la fonction. Cette derni\u00e8re lira simplements les arguments \u00e0 partir de la base du frame pointer.

    Le fichier d'en-t\u00eate d\u00e9clare 4 macros et un type\u2009:

    Macros pour les fonctions \u00e0 arguments variables Macro Description va_list Type de la liste d'arguments va_start Initialise la liste d'arguments va_arg R\u00e9cup\u00e8re un argument de la liste va_copy Copie une liste d'arguments va_end Termine la liste d'arguments

    Imaginons le cas de figure suivant. Nous souhaitons \u00e9crire une fonction qui affiche la somme des valeurs pass\u00e9es en arguments et nous ne savons pas le nombre de valeurs qui seront pass\u00e9es. Voici comment proc\u00e9der\u2009:

    #include <stdarg.h>\n#include <stdio.h>\n\nint sum(int count, ...) {\n    va_list args;\n    va_start(args, count);\n    int sum = 0;\n    for (int i = 0; i < count; i++) sum += va_arg(args, int);\n    va_end(args);\n    return sum;\n}\n\nint main() {\n    printf(\"Somme : %d\\n\", sum(3, 1, 2, 3));\n    printf(\"Somme : %d\\n\", sum(5, 1, 2, 3, 4, 5));\n}\n

    Dans une fonction dont le prototype autorise un nombre variable d'arguments, l'ellipse ... est utilis\u00e9e apr\u00e8s au moins un argument fixe qui pourrait repr\u00e9senter le nombre d'arguments \u00e0 suivre. Dans la fonction printf ce nombres est cach\u00e9 dans le format, en comptant le nombre de %. Dans cette fonction, une liste d'arguments args est d\u00e9clar\u00e9e. Lors de l'appel de va_start, la liste est initialis\u00e9e \u00e0 partir de l'argument suivant count. En pratique va_list est un pointeur sur la pile qui pointe sur l'argument suivant count. La fonction va_arg permet de r\u00e9cup\u00e9rer les arguments un par un de fa\u00e7on portable. En pratique elle d\u00e9r\u00e9f\u00e9rence le pointeur et retourne le type sp\u00e9cifi\u00e9 comme deuxi\u00e8me argument. Enfin, va_end termine la liste d'arguments.

    Voici ci-dessous comment elle pourrait \u00eatre impl\u00e9ment\u00e9e, n\u00e9anmoins ces macros utilisent des fonctions pr\u00e9compil\u00e9es (p. ex\u2009: __builtin_va_start) qui sont sp\u00e9cifiques \u00e0 chaque compilateur.

    typedef struct {\n    unsigned int gp_offset;  // Offset dans les registres g\u00e9n\u00e9raux\n    unsigned int fp_offset;  // Offset dans les registres flottants\n    void* overflow_arg_area; // Pointeur vers les arguments pass\u00e9s sur la pile\n    void* reg_save_area;     // Pointeur vers les registres sauvegard\u00e9s\n} va_list;\n\n// Macro g\u00e9n\u00e9rique pour r\u00e9cup\u00e9rer la taille d'un type\n#define type_size(type) _Generic((type), \\\n    int: sizeof(int), \\\n    double: sizeof(double), \\\n    float: sizeof(float), \\\n    char: sizeof(char), \\\n    default: sizeof(void*))\n\n#define va_start(ap, last) __va_start(ap, &last, type_size(last))\n#define va_arg(ap, type) \\\n    *((type*)(ap.overflow_arg_area)); \\\n    ap.overflow_arg_area += type_size(type)\n#define va_end(ap) (void)0\n\nvoid __va_start(va_list_hack* ap, void* last, size_t last_size) {\n    void *stack_pointer;\n    ap->overflow_arg_area = (void*)((char*)(&last) + last_size);\n    ap->gp_offset = 0;\n    ap->fp_offset = 0;\n    ap->reg_save_area = NULL;\n}\n

    ", "tags": ["va_end", "va_start", "printf", "count", "va_copy", "va_arg", "va_list", "args"]}, {"location": "course-c/35-libraries/standard-library/#stdatomich", "title": "<stdatomic.h>", "text": "

    Cet en-t\u00eate concerne la notion d'atomicit\u00e9 en programmation concurrente, et il pourrait s'agir d'un cours \u00e0 part enti\u00e8re. L'atomicit\u00e9 est la propri\u00e9t\u00e9 d'une op\u00e9ration qui est ex\u00e9cut\u00e9e en une seule \u00e9tape sans \u00eatre interrompue. En d'autres termes, une op\u00e9ration atomique est une op\u00e9ration qui est soit compl\u00e8tement ex\u00e9cut\u00e9e, soit pas du tout. Lorsqu'un programme utilise des threads (sous-programmes ex\u00e9cut\u00e9s en parall\u00e8le), il est possible que deux ex\u00e9cutions parall\u00e8les tentent de modifier la m\u00eame variable en m\u00eame temps. Cela peut poser de gros probl\u00e8mes de corruption de donn\u00e9es. Vous savez par exemple qu'un entier est stock\u00e9 sur 4 octets. On peut n\u00e9anmoins imaginer une fonction d'\u00e9change de deux variables un peu naive qui traite chaque octet s\u00e9par\u00e9ment.

    union Int { int i; char c[4]; };\n\nvoid swap(int *a, int *b) {\n    union Int tmp;\n    tmp.i = *a;\n    for (int i = 0; i < 4; i++) {\n        char t = ((union Int)*a).c[i];\n        ((union Int)*a).c[i] = ((union Int)*b).c[i];\n        ((union Int)*b).c[i] = t;\n    }\n}\n

    Dans le cas ou deux processus s\u00e9par\u00e9s essaient de traiter la valeur de a envoy\u00e9e, l'un \u00e0 swap et l'autre \u00e0 printf. Le r\u00e9sultat d\u00e9pendra et l'ordre d'ex\u00e9cution des instructions, et il se peut que printf affiche d\u00e9j\u00e0 quelque chose alors que swap n'a pas encore termin\u00e9. C'est ce qu'on appelle une condition de course. Bien entendu en pratique personne n'\u00e9crirait une fonction d'\u00e9change de variables de cette mani\u00e8re. Toutefois, pour r\u00e9soudre ce type de probl\u00e8me on utilise des fonctions atomiques qui ajoutent une couche de protection \u00e0 des variables partag\u00e9es entre plusieurs threads.

    Par cons\u00e9quent, il est n\u00e9cessaire d'utiliser un accesseur atomique pour lire et \u00e9crire la variable\u2009:

    #include <stdatomic.h>\n\nint main() {\n    atomic_int a = 42;\n    atomic_store(&a, 42);     // \u00c9criture atomique\n    int b = atomic_load(&a);  // Lecture atomique\n    atomic_fetch_add(&a, 1);  // Incr\u00e9mentation atomique de 1\n    atomic_fetch_sub(&a, 1);  // Soustraction atomique de 1\n    atomic_fetch_or(&a, 1);   // ...\n    atomic_fetch_and(&a, 1);\n    atomic_fetch_xor(&a, 1);\n    atomic_exchange(&a, 42);  // Notre fonction swap atomic\n    atomic_compare_exchange_strong(&a, &b, 42);\n    atomic_compare_exchange_weak(&a, &b, 42);\n}\n

    Pour de plus emples informations sur la programmation concurrente, je vous redirige sur un cours d\u00e9di\u00e9 \u00e0 ce sujet.

    ", "tags": ["swap", "printf"]}, {"location": "course-c/35-libraries/standard-library/#stdbith", "title": "<stdbit.h>", "text": "

    Cette biblioth\u00e8que a \u00e9t\u00e9 introduite avec le standard C23 et elle permet de manipuler les bits de mani\u00e8re portable en fournissant des macros pour les op\u00e9rations bit \u00e0 bit. Les macro suivantes sont disponibles\u2009:

    Macros bit \u00e0 bit dans C23 Macro Description stdc_popcount Compte le nombre de bits \u00e0 1 stdc_clz Compte le nombre de bits \u00e0 0 avant le premier bit \u00e0 1 stdc_ctz Compte le nombre de bits \u00e0 0 apr\u00e8s le dernier bit \u00e0 1 stdc_rotl Rotation \u00e0 gauche stdc_rotr Rotation \u00e0 droite stdc_bswap Inversion des octets

    Bien entendu pour ces op\u00e9rations, il est n\u00e9cessaire de conna\u00eetre la taille du type utilis\u00e9. C'est pourquoi ces macros sont g\u00e9n\u00e9riques et utilisent _Generic pour d\u00e9terminer la taille du type. Une rotation \u00e0 droite pourrait \u00eatre impl\u00e9ment\u00e9e na\u00efvement avec une macro\u2009:

    #define stdc_rotl(x, n) \\\n    ((x << (n % (sizeof(x) * CHAR_BIT))) | (x >> ((sizeof(x) * CHAR_BIT) -\n    (n % (sizeof(x) * CHAR_BIT)))))\n

    N\u00e9anmoins ces fonctions sont faites pour profiter des instructions sp\u00e9cifiques des processeurs modernes qui permettent de r\u00e9aliser ces op\u00e9rations de mani\u00e8re plus efficace. En effet dans l'architecture X86 par exemple il existe la directive assembleur ror pour la rotation \u00e0 droite et rol pour la rotation \u00e0 gauche. Ces instructions sont plus rapides que la m\u00e9thode na\u00efve ci-dessus mais elles n'existent pas n\u00e9cessairement dans toutes les architectures. Du reste, si on essaye de compiler cette macro avec gcc et observons l'assembler g\u00e9n\u00e9r\u00e9, on constate que le compilateur utilise bien l'instruction ror pour la rotation \u00e0 droite. Il est donc capable de comprendre le code et de l'optimiser en cons\u00e9quence.

    ", "tags": ["stdc_ctz", "stdc_rotl", "ror", "stdc_rotr", "stdc_bswap", "stdc_popcount", "_Generic", "rol", "stdc_clz"]}, {"location": "course-c/35-libraries/standard-library/#stdboolh", "title": "<stdbool.h>", "text": "

    Cette biblioth\u00e8que est apparue en C99 et apr\u00e8s 20 ans d'attente, elle introduit enfin le type bool\u00e9en bool et les valeurs true et false. Cet en-t\u00eate est par cons\u00e9quent l'un des plus simple de la biblioth\u00e8que standard, car il ne contient que trois lignes\u2009:

    #define bool _Bool\n#define true 1\n#define false 0\n

    Quelle belle mascarade\u2009! On d\u00e9finit bool comme _Bool... En r\u00e9alit\u00e9 ce dernier est un type natif du langage alors que bool n'est qu'une macro. C'est \u00e0 dire que sans include <stdbool.h> vous pouvez tout de m\u00eame d\u00e9finir un bool\u00e9en en utilisant _Bool. N\u00e9anmoins, l'int\u00e9r\u00eat de cette biblioth\u00e8que est de standardiser le type bool\u00e9en et les valeurs true et false pour une meilleure portabilit\u00e9 du code.

    On peut s'interroger pourquoi le standard \u00e0 d\u00e9cid\u00e9, plut\u00f4t que d'ajouter un nouveau type bool natif, que de le d\u00e9finir dans un en-t\u00eate suppl\u00e9mentaire et de d\u00e9finir le type avec un _ en pr\u00e9fixe. Le langage C a une longue histoire et de nombreux programmes n'ont pas attendu la sortie de cette biblioth\u00e8que pour d\u00e9finir leur propre type bool\u00e9en. Il \u00e9tait donc n\u00e9cessaire de ne pas casser la compatibilit\u00e9 avec les anciens programmes. C'est pourquoi le type _Bool a \u00e9t\u00e9 introduit sous cette nomenclature. L'histoire est similaire avec _Complex et _Imaginary de la biblioth\u00e8que <complex.h>.

    Notez que le _Bool est un type tr\u00e8s particulier car il est en r\u00e9alit\u00e9 souvent impl\u00e9ment\u00e9 comme un entier 8-bit. Rappelez-vous que le processeur aime manipuler des mots de la taille de son bus de donn\u00e9es. C'est pourquoi un bool\u00e9en est souvent stock\u00e9 sur 32-bits m\u00eame si un seul bit suffirait. C'est pourquoi on peut stocker des valeurs autres que true et false dans un bool\u00e9en. Par exemple, bool b = 42 est tout \u00e0 fait valide en C. En r\u00e9alit\u00e9, true et false sont des macros qui valent respectivement 1 et 0. C'est pourquoi on peut les utiliser pour initialiser des bool\u00e9ens.

    _Bool b = 1;\nprintf(\"%ld\\n\", sizeof(b)); // Affiche 1\n*((int*)&b) += 10; // Bypass le syst\u00e8me de typage\nprintf(\"%hhd\\n\", (int)b); // Affiche 11\n

    Il est n\u00e9cessaire de gruger pour ajouter 10 car na\u00efvement le compilateur impl\u00e9mente b += 10 avec\u2009:

    movs    r3, #1          // Sauve 1 et non 10 dans r3\n

    Dans le cas des tableaux, ne perdez pas \u00e0 l'esprit qu'un tableau de bool\u00e9ens est un tableau d'octets\u2009:

    bool bool_array[8] = {true, false, true, true, false, false, true, true};\nassert(sizeof(bool_array) == 8);\n

    ", "tags": ["_Bool", "true", "bool", "_Complex", "false", "_Imaginary"]}, {"location": "course-c/35-libraries/standard-library/#stdckdinth", "title": "<stdckdint.h>", "text": "

    Cette biblioth\u00e8que est apparue en C23 et propose des fonctions arithm\u00e9tiques pour les op\u00e9rations de base comme l'addition, la soustraction, et la multiplication, mais avec une d\u00e9tection explicite de l'overflow. L'abbr\u00e9viation ckd signifie checked. Les fonctions introduites par cet en-t\u00eate sont\u2009:

    Fonctions de d\u00e9tection de l'overflow Fonction Description ckd_add Addition avec d\u00e9tection de l'overflow ckd_sub Soustraction avec d\u00e9tection de l'overflow ckd_mul Multiplication avec d\u00e9tection de l'overflow

    Lorsque deux entiers sont additionn\u00e9s, il est possible que le r\u00e9sultat d\u00e9passe la capacit\u00e9 de stockage de l'entier. Par exemple, si on additionne \\(2^31 - 1\\) \u00e0 1, on obtient \\(-2^31\\). C'est ce qu'on appelle un overflow. En C, l'overflow est un comportement ind\u00e9fini, c'est-\u00e0-dire que le compilateur est libre de d\u00e9cider du comportement du programme. En pratique, la plupart des compilateurs vont simplement ignorer l'overflow et le r\u00e9sultat sera tronqu\u00e9. C'est pourquoi il est important de v\u00e9rifier les overflows lorsqu'on travaille avec des entiers.

    Avant l'introduction de cette biblioth\u00e8que, un d\u00e9passement de capacit\u00e9 devrait \u00eatre manuellement impl\u00e9ment\u00e9\u2009:

    int add(int a, int b) {\n    if (a > 0 && b > INT_MAX - a) {\n        // Overflow d\u00e9tect\u00e9\n    } else\n        return a + b;\n}\n

    Avec la biblioth\u00e8que <stdckdint.h>, il est possible de d\u00e9tecter l'overflow de mani\u00e8re plus \u00e9l\u00e9gante\u2009:

    #include <stdckdint.h>\n\nint add(int a, int b) {\n    int result;\n    if (ckd_add(a, b, &result)) {\n        // Overflow d\u00e9tect\u00e9\n    } else\n        return result;\n}\n

    ", "tags": ["ckd_mul", "ckd_add", "ckd_sub", "ckd"]}, {"location": "course-c/35-libraries/standard-library/#stddefh", "title": "<stddef.h>", "text": "

    La biblioth\u00e8que <stddef.h> fournit quelques d\u00e9finitions utiles tel que donn\u00e9 par la table suivante\u2009:

    D\u00e9finitions de stddef.h Macro Description NULL Pointeur nul offsetof Offset d'un membre d'une structure ptrdiff_t Type pour les diff\u00e9rences de pointeurs size_t Type pour les tailles d'objets wchar_t Type pour les caract\u00e8res larges nullptr_t Pointeur null (C23) max_align_t Type pour l'alignement maximal

    L'impl\u00e9mentation probable de ces macros est la suivante\u2009:

    #define NULL ((void*)0)\n#define offsetof(type, member) ((size_t)&((type*)0)->member)\ntypedef unsigned long size_t\ntypedef ptrdiff_t long\ntypedef int wchar_t`\n

    Concernant les pointeurs, s'il est parfaitement correct de tester si un pointeur vaut 0 (if (ptr == 0)), il est recommand\u00e9 d'utiliser NULL pour plus de clart\u00e9. Le standard C23 \u00e0 introduit le type nullptr_t pour les pointeurs nuls. Il est recommand\u00e9 de l'utiliser \u00e0 la place de NULL.

    max_align_t

    Il s'agit d'un type un type introduit en C11 pour repr\u00e9senter l'alignement maximal possible pour n'importe quel type. Il est utilis\u00e9 pour d\u00e9finir des types align\u00e9s de mani\u00e8re optimale. Par exemple, si on veut d\u00e9finir une structure align\u00e9e sur 16 octets, on peut utiliser max_align_t pour d\u00e9finir le type de la structure.

    size_t

    Il s'agit d'un type non sign\u00e9 qui est utilis\u00e9 pour repr\u00e9senter la taille d'un objet en m\u00e9moire. Il est utilis\u00e9 pour les fonctions qui retournent la taille d'un objet, comme strlen ou sizeof. Il est d\u00e9fini tel que sa taille soit suffisante, en pratique 64-bits sur une architecture 64-bits.

    ptrdiff_t

    Il s'agit d'un type sign\u00e9 qui est utilis\u00e9 pour repr\u00e9senter la diff\u00e9rence entre deux pointeurs. Lorsque l'on veut calculer ptr_p - ptr_q on obtient un entier dont la valeur maximale d\u00e9pend de la taille de la m\u00e9moire adressable.

    ", "tags": ["nullptr_t", "sizeof", "size_t", "NULL", "wchar_t", "max_align_t", "offsetof", "strlen", "ptrdiff_t"]}, {"location": "course-c/35-libraries/standard-library/#inttypesh-et-stdinth", "title": "<inttypes.h> et <stdint.h>", "text": "

    Ces deux biblioth\u00e8ques r\u00e9pondent au besoin d'avoir des types entiers d'une taille contr\u00f4l\u00e9e et surtout portable. En effet, nous avons vu que les types standards (int, short, long...) d\u00e9pendent du mod\u00e8le de donn\u00e9es de l'architecture cible. Un long n'aura pas la m\u00eame taille sur Linux ou Windows par exemple.

    L'en-t\u00eate <stint.h> fourni trois types de base\u2009:

    Cat\u00e9gories de types entiers portables Exemple Description int8_t Entier sign\u00e9 sur 8 bits int8_fast8_t Entier sign\u00e9 d'au moins 8 bits le plus rapide int8_least8_t Entier sign\u00e9 d'au moins 8 bits, le plus petit

    Ces cat\u00e9gories sont disponibles our les longueurs 8, 16, 32, 64 bits. Les types sont d\u00e9finis pour les entiers sign\u00e9s et non sign\u00e9s. Par exemple, int8_t est un entier sign\u00e9 sur 8 bits, uint8_t est un entier non sign\u00e9 sur 8 bits.

    Dans le cas ou on aurait besoin d'une variable pouvant contenir les valeurs de 0 \u00e0 255 mais que la taille de l'entier importe peu pour autant que le processeur n'ait pas de co\u00fbt suppl\u00e9mentaire \u00e0 manipuler la variable, on peut utiliser uint_fast8_t.

    \u00c0 l'inverse, si le besoin est d'avoir une variable qui peut contenir les valeurs de 0 \u00e0 255 avec la taille la plus petite possible (id\u00e9alement 8 bits), on utilisera uint_least8_t.

    Enfin, dans le cas (le plus rare) ou on aurait besoin exactement d'un entier non sign\u00e9 de 8 bits, on utilisera uint8_t. N\u00e9anmoins ce type pr\u00e9sente une contrainte importante car toutes les architectures ne sont pas n\u00e9cessairement pr\u00e9vues pour manipuler des entiers de 8 bits. Par exemple le SHARC d'Analog Devices est un processeur 32 bits qui n'a pas de support natif pour les entiers de 8 bits. L'utilisation de uint8_t r\u00e9sulterait en une erreur de compilation.

    L'en-t\u00eate <stdint.h> fournit \u00e9galement des macros utiles pour conna\u00eetre le choix de l'impl\u00e9mentation. Par exemple, INT_FAST8_WIDTH donne la largeur de l'entier le plus rapide selon la machine cible.

    On aura \u00e9galement les valeurs minimum et maximum que peut contenir chacun des types entiers. Par exemple, INT8_MIN et INT8_MAX pour les entiers sign\u00e9s sur 8 bits.

    Dans une boucle for op\u00e9rant sur un tableau de 100 \u00e9l\u00e9ments, il serait correct d'utiliser le type uint_fast8_t pour l'index de la boucle. N\u00e9anmoins pour des raisons de lisibilit\u00e9s, il est souvent pr\u00e9f\u00e9rable d'utiliser simplement int qui, selon le standard, garanti d'\u00eatre capable de contenir la taille du tableau.

    #include <stdint.h>\n\nint main() {\n    for (int_fast8_t i = 0; i < 100; i++) {\n        ...\n    }\n}\n

    En outre, pour des raisons de coh\u00e9rence, certaines normes pour l'avionique ou le m\u00e9dical imposent que les constantes litt\u00e9rales soient explicitement typ\u00e9es. On conna\u00eet d\u00e9j\u00e0 les suffixes u, ull pour les entiers de base, mais on peut \u00e9galement utiliser les macros de <stdint.h> pour les constantes litt\u00e9rales.

    uint8_t a = UINT8_C(42);\n

    L'utilisation de ces types sp\u00e9cifiques dans des fonctions d'entr\u00e9es sortie (p. ex. printf) doit aussi \u00eatre faite coh\u00e9rence. Un int32_t n'est pas compatible avec %d sur toutes les architectures. Il est pr\u00e9f\u00e9rable d'utiliser les macros de <inttypes.h> pour les sp\u00e9cifier.

    int32_t a = 42;\nprintf(\"%\" PRId32 \"\\n\", a);\n

    ", "tags": ["int8_fast8_t", "int8_least8_t", "int32_t", "ull", "int8_t", "for", "printf", "INT8_MIN", "uint8_t", "uint_least8_t", "INT8_MAX", "short", "uint_fast8_t", "long", "int", "INT_FAST8_WIDTH"]}, {"location": "course-c/35-libraries/standard-library/#stdioh", "title": "<stdio.h>", "text": "

    La biblioth\u00e8que <stdio.h> est l'une des biblioth\u00e8ques les plus importantes en C. Elle fournit des fonctions pour l'entr\u00e9e et les sorties, c'est-\u00e0-dire pour lire et \u00e9crire des donn\u00e9es depuis et vers la console. Elle fournit \u00e9galement des fonctions pour lire et \u00e9crire des fichiers.

    La plupart des fonctions de cette biblioth\u00e8que ont d\u00e9j\u00e0 \u00e9t\u00e9 abord\u00e9es dans les chapitres pr\u00e9c\u00e9dents. Voici ci-dessous un r\u00e9sum\u00e9 des fonctions qu'elle contient. Le (f) indique que la fonction peut \u00eatre utilis\u00e9e pour lire ou \u00e9crire depuis un fichier, elle prend un pointeur de type FILE en premier argument. Le (w) indique que la fonction est pr\u00e9vue pour les caract\u00e8res larges (wchar_t).

    Fonctions de stdio.h Fonction Description (f)get(w)c Lit un caract\u00e8re depuis l'entr\u00e9e standard ou un fichier (f)get(w)s Lit une ligne depuis l'entr\u00e9e standard ou un fichier (f)put(w)c \u00c9crit un caract\u00e8re vers l'entr\u00e9e standard ou un fichier (f)put(w)s \u00c9crit une cha\u00eene de caract\u00e8res vers la sortie standard ou un fichier clearerr R\u00e9initialise l'\u00e9tat d'erreur d'un flux fclose Ferme un fichier ouvert feof V\u00e9rifie si la fin du fichier est atteinte ferror V\u00e9rifie si une erreur est survenue dans le flux fflush Vide le tampon de sortie d'un flux fgetpos Obtient la position actuelle dans un fichier sous forme de fpos_t fileno Obtient le descripteur de fichier associ\u00e9 \u00e0 un flux flockfile Verrouille un flux pour les op\u00e9rations multithread\u00e9es fopen Ouvre un fichier pour la lecture, l'\u00e9criture ou l'ajout fread Lit des blocs d'octets depuis un flux freopen Ouvre \u00e0 nouveau un fichier sur un flux de fichier existant fscanf Lit des donn\u00e9es format\u00e9es depuis un fichier fseek Positionne le curseur de lecture/\u00e9criture dans un fichier fsetpos D\u00e9finit la position actuelle dans un fichier selon un objet fpos_t ftell Renvoie la position actuelle dans un fichier ftrylockfile Tente de verrouiller un flux pour les op\u00e9rations multithread\u00e9es funlockfile D\u00e9verrouille un flux verrouill\u00e9 fwrite \u00c9crit des blocs d'octets vers un flux perror Affiche un message d'erreur bas\u00e9 sur la derni\u00e8re erreur rencontr\u00e9e remove Supprime un fichier rename Renomme un fichier rewind Remet le curseur au d\u00e9but d'un fichier scanf Lit des donn\u00e9es format\u00e9es depuis l'entr\u00e9e standard setbuf D\u00e9finit un tampon pour un flux setvbuf D\u00e9finit le mode de tampon pour un flux sscanf Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene tmpfile Cr\u00e9e et ouvre un fichier temporaire qui est supprim\u00e9 \u00e0 la fermeture tmpnam G\u00e9n\u00e8re un nom de fichier temporaire unique ungetc Remet un caract\u00e8re dans le flux pour qu'il soit lu \u00e0 nouveau ungetwc Remet un caract\u00e8re large dans le flux Fonctions d'affichage format\u00e9 Fonction Description vfprintf \u00c9crit une sortie format\u00e9e sur un flux avec une liste variadiques vprintf \u00c9crit une sortie format\u00e9e sur stdout avec une liste variadiques vsprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene avec une liste variadiques vfwprintf Version large de vfprintf pour les caract\u00e8res larges (wchar_t) vwprintf Version large de vprintf pour les caract\u00e8res larges (wchar_t) vswprintf Version large de vsprintf pour les caract\u00e8res larges (wchar_t) fprintf \u00c9crit une sortie format\u00e9e dans un fichier printf \u00c9crit une sortie format\u00e9e sur stdout sprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene snprintf \u00c9crit une sortie format\u00e9e dans une cha\u00eene avec une taille limit\u00e9e fwprintf Version large de fprintf pour les caract\u00e8res larges (wchar_t) wprintf Version large de printf pour les caract\u00e8res larges (wchar_t) swprintf Version large de sprintf pour les caract\u00e8res larges (wchar_t) Constantes et types de stdio.h Constante/Type Description EOF Constante retourn\u00e9e par les fonctions de lecture lorsqu'une fin de fichier ou une erreur est rencontr\u00e9e NULL Pointeur nul utilis\u00e9 pour repr\u00e9senter l'absence d'objet FILENAME_MAX Longueur maximale d'un nom de fichier FOPEN_MAX Nombre maximal de fichiers pouvant \u00eatre ouverts simultan\u00e9ment L_tmpnam Longueur minimale d'un tampon pour tmpnam BUFSIZ Taille du tampon par d\u00e9faut pour les op\u00e9rations de lecture/\u00e9criture TMP_MAX Nombre maximal de noms uniques g\u00e9n\u00e9r\u00e9s par tmpnam SEEK_SET Indique le d\u00e9but du fichier pour fseek et fseeko SEEK_CUR Indique la position actuelle dans le fichier pour fseek et fseeko SEEK_END Indique la fin du fichier pour fseek et fseeko stderr Flux de sortie d'erreur standard stdin Flux d'entr\u00e9e standard stdout Flux de sortie standard FILE Type opaque repr\u00e9sentant un flux de fichier fpos_t Type utilis\u00e9 pour stocker la position dans un fichier

    ", "tags": ["freopen", "EOF", "vfprintf", "SEEK_SET", "stdin", "ungetc", "remove", "fwprintf", "fopen", "NULL", "FILENAME_MAX", "fwrite", "ftrylockfile", "fgetpos", "fileno", "stdout", "L_tmpnam", "FILE", "fclose", "setbuf", "tmpnam", "vswprintf", "fseek", "vfwprintf", "snprintf", "swprintf", "setvbuf", "ftell", "flockfile", "SEEK_CUR", "vprintf", "fseeko", "sscanf", "printf", "wprintf", "wchar_t", "fpos_t", "feof", "rewind", "clearerr", "SEEK_END", "sprintf", "FOPEN_MAX", "tmpfile", "vsprintf", "perror", "fscanf", "ungetwc", "fread", "stderr", "TMP_MAX", "funlockfile", "rename", "fprintf", "BUFSIZ", "fflush", "ferror", "scanf", "fsetpos", "vwprintf"]}, {"location": "course-c/35-libraries/standard-library/#stdlibh", "title": "<stdlib.h>", "text": "

    Cette biblioth\u00e8que contient des fonctions \u00e9parses qui ne sont pas assez importantes pour \u00eatre regroup\u00e9es dans une biblioth\u00e8que d\u00e9di\u00e9e. Contrairement aux langages plus r\u00e9cents (comme C++ ou Java), C n'a pas \u00e9t\u00e9 con\u00e7u avec une philosophie de modularit\u00e9 stricte pour les biblioth\u00e8ques. Les fonctions \u00e9taient rassembl\u00e9es par utilit\u00e9 pratique plut\u00f4t que par sujet sp\u00e9cifique, et les biblioth\u00e8ques \u00e9taient assez limit\u00e9es en nombre pour garder le langage simple et portable. On y retrouve les cat\u00e9gories suivantes\u2009:

    • Gestion de la m\u00e9moire
    • Conversion de cha\u00eenes en types num\u00e9riques
    • Gestion du programme
    • Nombres al\u00e9atoires
    • Algorithmes de recherche et de tri
    Fonctions diverses de stdlib.h Fonction Description abort Arr\u00eate le programme de mani\u00e8re anormale sans nettoyage des ressources exit Arr\u00eate le programme de mani\u00e8re normale avec nettoyage des ressources quick_exit Arr\u00eate le programme de mani\u00e8re normale sans nettoyage complet des ressources (C11) _Exit Arr\u00eate le programme de mani\u00e8re normale sans nettoyage des ressources (C99) atexit Enregistre une fonction \u00e0 appeler lors de l'appel \u00e0 exit at_quick_exit Enregistre une fonction \u00e0 appeler lors de l'appel \u00e0 quick_exit (C11) getenv R\u00e9cup\u00e8re la valeur d'une variable d'environnement setenv Ajoute ou modifie une variable d'environnement (POSIX, non standard) putenv Ajoute ou modifie une variable d'environnement unsetenv Supprime une variable d'environnement (POSIX, non standard) system Ex\u00e9cute une commande syst\u00e8me dans un shell rand G\u00e9n\u00e8re un nombre pseudo-al\u00e9atoire srand Initialise le g\u00e9n\u00e9rateur de nombres pseudo-al\u00e9atoires mblen Retourne le nombre d'octets d'un caract\u00e8re multioctet dans une cha\u00eene Gestion de la m\u00e9moire de stdlib.h Fonction Description malloc Alloue un bloc de m\u00e9moire calloc Alloue et initialise un bloc de m\u00e9moire realloc Redimensionne un bloc de m\u00e9moire pr\u00e9c\u00e9demment allou\u00e9 free Lib\u00e8re un bloc de m\u00e9moire pr\u00e9c\u00e9demment allou\u00e9 aligned_alloc Alloue un bloc de m\u00e9moire align\u00e9 (C11) free_sized Lib\u00e8re un bloc de m\u00e9moire de taille sp\u00e9cifi\u00e9e (C23) free_aligned_sized Lib\u00e8re un bloc de m\u00e9moire align\u00e9 de taille sp\u00e9cifi\u00e9e (C23) Algorithmes de recherche et de tri de stdlib.h Fonction Description bsearch Recherche un \u00e9l\u00e9ment dans un tableau tri\u00e9 en utilisant une fonction de comparaison qsort Trie un tableau en utilisant un algorithme de tri rapide (quick sort) Op\u00e9rations sur les nombres de stdlib.h Fonction Description abs Calcule la valeur absolue d'un entier (int) labs Calcule la valeur absolue d'un entier long (long) llabs Calcule la valeur absolue d'un long long (long long) (C99) div Effectue une division enti\u00e8re et retourne le quotient et le reste pour les int ldiv Effectue une division enti\u00e8re pour les long et retourne quotient et reste lldiv Effectue une division enti\u00e8re pour les long long et retourne quotient et reste (C99) Fonctions de conversion de cha\u00eenes de stdlib.h Fonction Description atof Convertit une cha\u00eene de caract\u00e8res en double (double) atoi Convertit une cha\u00eene de caract\u00e8res en entier (int) atol Convertit une cha\u00eene de caract\u00e8res en long (long) atoll Convertit une cha\u00eene de caract\u00e8res en long long (long long) (C99) mbstowcs Convertit une cha\u00eene multioctet en cha\u00eene de caract\u00e8res larges (wchar_t) mbtowc Convertit un caract\u00e8re multioctet en caract\u00e8re large (wchar_t) strtod Convertit une cha\u00eene en double (double) strtof Convertit une cha\u00eene en float (float) (C99) strtol Convertit une cha\u00eene en long (long), avec une base personnalisable strtold Convertit une cha\u00eene en long double (long double) (C99) strtoll Convertit une cha\u00eene en long long (long long) (C99) strtoul Convertit une cha\u00eene en unsigned long (unsigned long) strtoull Convertit une cha\u00eene en unsigned long long (unsigned long long) (C99) wcstombs Convertit une cha\u00eene de caract\u00e8res larges en cha\u00eene multioctet wctomb Convertit un caract\u00e8re large (wchar_t) en multioctet Constantes et types de stdlib.h Constante/Type Description EXIT_SUCCESS Indique une terminaison r\u00e9ussie du programme (valeur utilis\u00e9e avec exit) EXIT_FAILURE Indique une terminaison \u00e9chou\u00e9e du programme (valeur utilis\u00e9e avec exit) NULL Pointeur nul, utilis\u00e9 pour initialiser ou tester des pointeurs RAND_MAX Valeur maximale que peut retourner rand MB_CUR_MAX Taille maximale d'un caract\u00e8re multioctet pour la locale courante size_t Type pour repr\u00e9senter des tailles et des dimensions div_t Structure retourn\u00e9e par div contenant le quotient et le reste ldiv_t Structure retourn\u00e9e par ldiv contenant le quotient et le reste lldiv_t Structure retourn\u00e9e par lldiv (C99) contenant le quotient et le reste wchar_t Type pour repr\u00e9senter un caract\u00e8re large mbstate_t Type utilis\u00e9 pour conserver l'\u00e9tat entre conversions de caract\u00e8res multioctets et caract\u00e8res larges

    ", "tags": ["mbtowc", "abort", "rand", "strtold", "setenv", "ldiv", "mblen", "qsort", "mbstowcs", "unsetenv", "NULL", "div_t", "system", "at_quick_exit", "bsearch", "atol", "div", "malloc", "atof", "lldiv", "EXIT_FAILURE", "strtod", "quick_exit", "exit", "labs", "lldiv_t", "strtol", "strtoul", "abs", "strtoll", "long", "int", "double", "MB_CUR_MAX", "free", "aligned_alloc", "RAND_MAX", "EXIT_SUCCESS", "strtof", "putenv", "realloc", "calloc", "wchar_t", "wcstombs", "srand", "atoi", "float", "atoll", "free_sized", "ldiv_t", "atexit", "size_t", "getenv", "llabs", "_Exit", "wctomb", "strtoull", "mbstate_t"]}, {"location": "course-c/35-libraries/standard-library/#stdnoreturnh", "title": "<stdnoreturn.h>", "text": "

    Cette biblioth\u00e8que est apparue en C11 et elle introduit le type noreturn qui est utilis\u00e9 pour indiquer qu'une fonction ne retourne jamais. Cela permet au compilateur d'optimiser le code en supprimant les instructions de retour de la fonction. En pratique, cela permet de gagner quelques cycles d'horloge. Voici un exemple d'utilisation\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <stdnoreturn.h>\n\nnoreturn void exit_now(int i)\n{\n    if (i > 0)\n        exit(i);\n\n    // Si i <= 0, le comportement est ind\u00e9fini\n}\n\nint main(void)\n{\n    puts(\"Se pr\u00e9pare \u00e0 terminer le programme...\");\n    exit_now(2);\n    puts(\"On sait que ce code n'est jamais ex\u00e9cut\u00e9.\");\n}\n

    Avant C23, il fallait utiliser _Noreturn.

    ", "tags": ["noreturn", "_Noreturn"]}, {"location": "course-c/35-libraries/standard-library/#stringh", "title": "<string.h>", "text": "

    La biblioth\u00e8que <string.h> contient des fonctions pour manipuler les cha\u00eenes de caract\u00e8res. Les fonctions sont d\u00e9finies pour les cha\u00eenes de caract\u00e8res ASCII uniquement. On distingue deux famille de fonctions, les mem qui manipulent des r\u00e9gions m\u00e9moires et les str qui manipulent des cha\u00eenes de caract\u00e8res.

    La table suivante r\u00e9sume les fonctions les plus utilis\u00e9es. On notera que les lettres entre parenth\u00e8ses indiquent les variantes des fonctions. La fonction strcpy existe en version strncpy qui permet de copier une cha\u00eene en sp\u00e9cifiant la taille maximale \u00e0 copier. On notera n pour les fonctions dont la taille maximum de la cha\u00eene peut \u00eatre sp\u00e9cifi\u00e9e, r pour reverse et c pour not in.

    Fonctions sur les cha\u00eenes de caract\u00e8res Fonction Description memset Remplissage d'une r\u00e9gion m\u00e9moire memcpy Copie d'une r\u00e9gion m\u00e9moire memmove Copie d'une r\u00e9gion m\u00e9moire avec superposition memcmp Comparaison de deux r\u00e9gions m\u00e9moire memchr Recherche d'un caract\u00e8re dans une r\u00e9gion m\u00e9moire strlen Longueur de la cha\u00eene str(n)cpy Copie d'une cha\u00eene str(n)cat Concat\u00e9nation de deux cha\u00eenes str(n)cmp Comparaison de deux cha\u00eenes str(r)chr Recherche d'un caract\u00e8re dans une cha\u00eene strstr Recherche d'une sous-cha\u00eene dans une cha\u00eene strtok D\u00e9coupe une cha\u00eene en morceaux strspn Longueur du pr\u00e9fixe d'une cha\u00eene strcspn Longueur du pr\u00e9fixe qui ne contient pas certains caract\u00e8res strpbrk Recherche d'un caract\u00e8re dans une liste strcoll Comparaison de cha\u00eenes selon la locale strxfrm Transformation de cha\u00eenes selon la locale strerror Message d'erreur associ\u00e9 \u00e0 un code d'erreur", "tags": ["strncpy", "mem", "strcspn", "memmove", "memset", "str", "memchr", "strspn", "strpbrk", "strcoll", "strcpy", "memcpy", "strtok", "strstr", "strlen", "strxfrm", "memcmp", "strerror"]}, {"location": "course-c/35-libraries/standard-library/#memset", "title": "memset", "text": "

    La fonction memset permet de remplir une r\u00e9gion m\u00e9moire avec une valeur donn\u00e9e. Son prototype est\u2009:

    void *memset(void *s, int c, size_t n);\n

    Elle est utilis\u00e9e principalement pour initialiser ou r\u00e9initialiser un tableau en une seule instruction qui sera plus performante qu'une boucle. L'exemple suivant initialise un tableau de 100 \u00e9l\u00e9ments avec la valeur 42\u2009:

    char array[100];\nmemset(array, 42, sizeof(array));\n

    Notez que la valeur est un byte. Memset ne peut pas \u00eatre utilis\u00e9 pour initialiser un tableau avec une valeur de type int par exemple.

    ", "tags": ["int", "memset"]}, {"location": "course-c/35-libraries/standard-library/#memcpy-et-memmove", "title": "memcpy et memmove", "text": "

    Les deux fonctions permettent de copier des r\u00e9gions m\u00e9moires d'une adresse \u00e0 une autre. Leur prototype est le suivant\u2009:

    void *memmove(void *dest, const void *src, size_t n)\nvoid *memcpy(void *dest, const void *src, size_t n)\n

    La diff\u00e9rence principale est que memcpy ne traite pas les cas o\u00f9 les deux r\u00e9gions m\u00e9moire se superposent. memmove est plus s\u00fbr et plus lent que memcpy. En effet, memmove doit v\u00e9rifier si les deux r\u00e9gions m\u00e9moire se superposent et dans ce cas, elle doit copier les donn\u00e9es dans un buffer temporaire avant de les copier dans la r\u00e9gion de destination. memcpy ne fait pas cette v\u00e9rification et copie directement les donn\u00e9es.

    Pour comprendre ce probl\u00e8me de superposition prenons l'exemple ci-dessous. La fonction memcpy est r\u00e9impl\u00e9ment\u00e9e pour avoir une r\u00e9sultat pr\u00e9visible. Un pointeur p est d\u00e9clar\u00e9 et un pointeur q correspond \u00e0 l'adresse de p mais d\u00e9cal\u00e9 de deux \u00e9l\u00e9ments. Les deux espaces m\u00e9moire se superposent donc. Apr\u00e8s copie on devrait obtenir 12345 dans q mais apr\u00e8s la copie mais on obtient 12121. Il y a donc un probl\u00e8me.

    int mymemcpy(void *dest, const void *src, size_t n) {\n   char *d = dest;\n   const char *s = src;\n   for (size_t i = 0; i < n; i++) d[i] = s[i];\n   return 0;\n}\n\nint main() {\n   char s[] = \"12345..\";\n   char *p = s;\n   char *q = p + 2;\n   mymemcpy(q, p, 5);\n   for (int i = 0; i < 5; i++) printf(\"%c\", q[i]);\n   printf(\"\\n\");\n}\n

    Pour vous en convaincre, vous pouvez vous aider de la figure suivante.

    memcpy

    En r\u00e9alit\u00e9, le fonctionnement de memcpy n'est pas si simple. En effet, le compilateur peut optimiser le code et utiliser des instructions SIMD pour copier les donn\u00e9es. Cela va \u00e9galement d\u00e9pendre du niveau d'optimisation du compilateur. En ex\u00e9cutant le m\u00eame code avec memcpy, je n'obtiens pas le m\u00eame r\u00e9sultat, en observant le code assembleur g\u00e9n\u00e9r\u00e9 pour memcpy, on observe que les premiers 4 octets sont copi\u00e9s en une seule instruction, le cinqui\u00e8me octet est copi\u00e9 en une instruction s\u00e9par\u00e9e. Ce qui donne comme r\u00e9sultat 12343.

    mov rcx, qword ptr [rbp - 128]  ; Charge l'adresse de destination (q) dans rcx\nmov rdx, qword ptr [rbp - 120]  ; Charge l'adresse source (p) dans rdx\nmov esi, dword ptr [rdx]        ; Charge les 4 premiers octets dans esi\nmov dword ptr [rcx], esi        ; Copie ces 4 octets dans la destination\nmov r8b, byte ptr [rdx + 4]     ; Charge le 5e octet de la source dans r8b\nmov byte ptr [rcx + 4], r8b     ; Copie le 5e octet dans la destination\n

    Pour s'affranchir de ce type de probl\u00e8me, il est pr\u00e9f\u00e9rable d'utiliser memmove lorsque vous n'\u00eates pas s\u00fbr que les deux r\u00e9gions m\u00e9moire ne se superposent pas.

    ", "tags": ["memmove", "memcpy"]}, {"location": "course-c/35-libraries/standard-library/#memcmp", "title": "memcmp", "text": "

    La fonction memcmp permet de comparer deux r\u00e9gions m\u00e9moires. Son prototype est\u2009:

    int memcmp(const void *s1, const void *s2, size_t n);\n

    On utilise typiquement memcmp pour comparer deux structures. Il n'est en effet pas possible de comparer deux structures directement avec l'op\u00e9rateur ==. On utilisera plut\u00f4t\u2009:

    struct Person {\n    char firstname[64];\n    char lastname[64];\n    Genre genre;\n    int age;\n};\n\nPerson p, q;\n// Do something with p and q\n\nif (memcmp(&p, &p, sizeof(struct Person)) == 0)\n    printf(\"Les deux personnes sont identiques\\n\");\n

    Comparaison de cha\u00eenes de caract\u00e8res

    Attention n\u00e9anmoins au cas de figure donn\u00e9. Les champs firstname et lastname sont des buffers de 64 caract\u00e8res. Si les deux structures contiennent les m\u00eames pr\u00e9noms et noms il n'y a aucune garantie que les deux structures soient \u00e9gales. En effet, apr\u00e8s le caract\u00e8re '\\0', rien n'oblige l'utilisateur \u00e0 remplir le reste du buffer avec des '\\0'.

    Il serait ici pr\u00e9f\u00e9rable de tester individuellement les champs de la structure.

    ", "tags": ["firstname", "lastname", "memcmp"]}, {"location": "course-c/35-libraries/standard-library/#memchr-et-strrchr", "title": "memchr et str(r)chr", "text": "

    Les fonctions memchr, strchr et strrchr permettent de rechercher un caract\u00e8re dans une r\u00e9gion m\u00e9moire. Leurs prototypes sont\u2009:

    void *memchr(const void *s, int c, size_t n);\nchar *strchr(const char *s, int c);\nchar *strrchr(const char *s, int c);\n

    Une utilisaiton typique de strchr ou strrchr est de rechercher l'occurence d'un caract\u00e8re dans une cha\u00eene de caract\u00e8res. La fonction s'arr\u00eate d\u00e8s qu'elle trouve le caract\u00e8re recherch\u00e9 ou qu'elle atteint la fin de la cha\u00eene.

    char *s = \"Anticonsitutionnellement\";\nchar *p = strchr(s, 'i');\nassert(p != NULL); // Caract\u00e8re trouv\u00e9\nassert(*p == 'i'); // Caract\u00e8re trouv\u00e9 est 'i'\nassert(p - s == 3); // Position de 'i' dans la cha\u00eene\n\n// Recherche depuis la fin\np = strrchr(s, 'i');\nassert(p - s == 12); // Derni\u00e8re position de 'i' dans la cha\u00eene\n

    Dans le cas de memchr, il est possible de chercher n'importe quelle valeur de byte, y compris '\\0'. En revanche, il est n\u00e9cessaire de sp\u00e9cifier la taille de la r\u00e9gion m\u00e9moire \u00e0 parcourir.

    ", "tags": ["strrchr", "memchr", "strchr"]}, {"location": "course-c/35-libraries/standard-library/#strlen", "title": "strlen", "text": "

    La fonction strlen permet de calculer la longueur d'une cha\u00eene de caract\u00e8res. Son prototype est\u2009:

    size_t strlen(const char *s);\n

    Son impl\u00e9mentation pourrait \u00eatre la suivante\u2009:

    size_t strlen(const char *s) {\n    size_t i = 0;\n    while (s[i] != '\\0') i++;\n    return i;\n}\n
    ", "tags": ["strlen"]}, {"location": "course-c/35-libraries/standard-library/#strncpy", "title": "str(n)cpy", "text": "

    Les fonctions strcpy et strncpy permettent de copier une cha\u00eene de caract\u00e8res. Leur prototype sont\u2009:

    char *strcpy(char *dest, const char *src);\nchar *strncpy(char *dest, const char *src, size_t n);\n

    La fonction strcpy copie la cha\u00eene de caract\u00e8res src dans dest. La fonction strncpy copie au maximum n caract\u00e8res de src dans dest. Si la cha\u00eene src est plus longue que n, la cha\u00eene dest ne sera pas termin\u00e9e par '\\0'.

    ", "tags": ["strncpy", "src", "strcpy", "dest"]}, {"location": "course-c/35-libraries/standard-library/#strncat", "title": "str(n)cat", "text": "

    Les fonctions strcat et strncat permettent de concat\u00e9ner deux cha\u00eenes de caract\u00e8res. Leur prototype sont\u2009:

    char *strcat(char *dest, const char *src);\nchar *strncat(char *dest, const char *src, size_t n);\n

    La fonction strcat concat\u00e8ne la cha\u00eene de caract\u00e8res src \u00e0 la fin de dest. La fonction strncat concat\u00e8ne au maximum n caract\u00e8res de src \u00e0 la fin de dest. Si la cha\u00eene src est plus longue que n, la cha\u00eene dest ne sera pas termin\u00e9e par '\\0'.

    char dest[20] = \"Hello, \"; // dest est plus grand que src !\nchar src[] = \"World!\";\nstrcat(dest, src);\nprintf(\"%s\\n\", dest);  // Affiche \"Hello, World!\"\n

    L'impl\u00e9mentation de strcat pourrait \u00eatre la suivante\u2009:

    char *strcat(char *dest, const char *src) {\n    while (*dest != '\\0') ++dest;\n    while ((*dest++ = *src++) != '\\0');\n    return dest;\n}\n
    ", "tags": ["strncat", "src", "dest", "strcat"]}, {"location": "course-c/35-libraries/standard-library/#strcmp-et-strncmp", "title": "strcmp et strncmp", "text": "

    La fonction strcmp permet de comparer deux cha\u00eenes de caract\u00e8res. Son prototype est\u2009:

    int strcmp(const char *s1, const char *s2);\n

    Elle sera utilis\u00e9e principalement pour comparer deux cha\u00eenes de caract\u00e8res. Une utlisation typique est de tester si deux cha\u00eenes sont \u00e9gales. Par exemple\u2009:

    char *owner = \"John\";\nchar user[64];\nif (scanf(\"%63s\", user) == 1 && memcmp(owner, user) == 0) {\n    printf(\"Welcome John!\\n\");\n}\n

    Le strncmp permet de forcer la comparaison \u00e0 s'arr\u00eater apr\u00e8s un certain nombre de caract\u00e8res. C'est particuli\u00e8rement utile pour le traitement des arguments de ligne de commande. Par exemple\u2009:

    if (strncmp(argv[1], \"--filename=\", 11) == 0) {\n    printf(\"Ouverture du fichier %s\\n\", argv[1] + 11);\n}\n
    ", "tags": ["strncmp", "strcmp"]}, {"location": "course-c/35-libraries/standard-library/#strtok", "title": "strtok", "text": "

    La fonction strtok permet de d\u00e9couper une cha\u00eene de caract\u00e8res en morceaux. Il s'agit de l'abbr\u00e9viation de string token. Son prototype est\u2009:

    char *strtok(char *str, const char *delim);\n

    La premi\u00e8re fois que la fonction est appel\u00e9e, elle prend en param\u00e8tre la cha\u00eene \u00e0 d\u00e9couper. Les appels suivants doivent passer NULL en premier param\u00e8tre. La fonction retourne un pointeur sur le d\u00e9but du morceau suivant. La fonction modifie la cha\u00eene pass\u00e9e en param\u00e8tre en ins\u00e9rant des '\\0' \u00e0 la place des d\u00e9limiteurs.

    char s[] = \"mais,ou,est,donc,or,ni,car\";\nchar *token = strtok(s, \",\");\nwhile (token != NULL) {\n    printf(\"%s\\n\", token);\n    token = strtok(NULL, \",\");\n}\n
    ", "tags": ["NULL", "strtok"]}, {"location": "course-c/35-libraries/standard-library/#strspn-et-strcspn", "title": "strspn et strcspn", "text": "

    Les fonctions strspn et strcspn permettent de calculer la longueur du pr\u00e9fixe d'une cha\u00eene qui contient ou ne contient pas certains caract\u00e8res. Leur prototype est\u2009:

    size_t strspn(const char *s, const char *accept);\nsize_t strcspn(const char *s, const char *reject);\n

    Une utilisation typique de strspn est de valider une cha\u00eene de caract\u00e8res. Par exemple, pour valider un nombre entier\u2009:

    char *s = \"12345\";\nif (strspn(s, \"0123456789\") == strlen(s)) {\n    printf(\"La cha\u00eene %s est un nombre entier\\n\", s);\n}\n

    Inversement, strcspn permet de valider une cha\u00eene de caract\u00e8res qui ne contient pas certains caract\u00e8res. Par exemple pour aider Georges Perec \u00e0 \u00e9crire un lipogramme sans la lettre e :

    char *s = \"La disparition\";\nif (strcspn(s, \"e\") == strlen(s)) {\n    printf(\"La cha\u00eene %s ne contient pas la lettre 'e'\\n\", s);\n}\n
    ", "tags": ["strcspn", "strspn"]}, {"location": "course-c/35-libraries/standard-library/#strpbrk", "title": "strpbrk", "text": "

    La fonction strpbrk permet de rechercher un caract\u00e8re dans une liste de caract\u00e8res. Son prototype est\u2009:

    char *strpbrk(const char *s, const char *accept);\n

    Elle retourne un pointeur sur le premier caract\u00e8re de s qui appartient \u00e0 accept. Par exemple, pour chercher les op\u00e9rateurs utilis\u00e9s dans une cha\u00eene de caract\u00e8res\u2009:

    char *s = \"1 + 2 * 4 + 8\";\n\nwhile (s = strpbrk(s, \"+-*/%%\")) {\n    printf(\"Op\u00e9rateur trouv\u00e9 : %c\\n\", *s);\n    s++;\n}\n
    ", "tags": ["accept", "strpbrk"]}, {"location": "course-c/35-libraries/standard-library/#strcoll-et-strxfrm", "title": "strcoll et strxfrm", "text": "

    Les fonctions strcoll et strxfrm permettent de comparer des cha\u00eenes de caract\u00e8res selon la locale. Elles sont l'abbr\u00e9viation de string collate o\u00f9 collate fait r\u00e9f\u00e9rence au tri ou \u00e0 l'ordre de classement des cha\u00eenes de caract\u00e8res en fonction des conventions locales. strxfrm est l'abbr\u00e9viation de string transform et permet de transformer une cha\u00eene de caract\u00e8res en une cha\u00eene de caract\u00e8res qui peut \u00eatre compar\u00e9e avec strcmp. Leur prototype est\u2009:

    int strcoll(const char *s1, const char *s2);\nsize_t strxfrm(char *dest, const char *src, size_t n);\n

    Ces deux fonctions sont utilis\u00e9es pour comparer des cha\u00eenes de caract\u00e8res en tenant compte des conventions de tri locales, qui peuvent varier d'une langue ou d'un jeu de caract\u00e8res \u00e0 un autre. Elles sont principalement utilis\u00e9es pour des op\u00e9rations de tri ou de comparaison dans des contextes o\u00f9 il est important de respecter l'ordre d\u00e9fini par les param\u00e8tres r\u00e9gionaux (locales). Ces fonction utlisent donc les param\u00e8tres r\u00e9gionaux d\u00e9finis par la fonction setlocale de la biblioth\u00e8que <locale.h>.

    Pourquoi ces deux fonctions \u00e9tranges\u2009? Comparer deux cha\u00eenes n'est pas facile surtout s'il y a des diacritiques. Selon la langue, les cha\u00eenes de caract\u00e8res ne sont pas forc\u00e9ment compar\u00e9es de la m\u00eame mani\u00e8re. En fran\u00e7ais on consid\u00e8re alphab\u00e9tiquement que \u00e9 est juste apr\u00e8s e dans l'alphabet, en anglais les deux lettres sont \u00e9quivalentes. En su\u00e9dois, les lettres \u00e5, \u00e4 sont plac\u00e9es apr\u00e8s le z, et les r\u00e8gles sont nombreuses.

    strxfrm permet de transformer une cha\u00eene de caract\u00e8res en une cha\u00eene de caract\u00e8res dite \u00ab\u2009collationn\u00e9e\u2009\u00bb qui peut \u00eatre compar\u00e9e ensuite rapidement avec strcmp. Cela peut \u00eatre utile lors de comparaison r\u00e9p\u00e9t\u00e9es.

    Notons que ces fonctions ne sont plus vraiment utilis\u00e9es car elles se limitent au jeux de caract\u00e8res ISO-8859, et le support Unicode est limit\u00e9. Pour une gestion correcte il vaut mieux faire appel \u00e0 des biblioth\u00e8ques plus sp\u00e9cialis\u00e9es comme ICU qui offre la fonction ucol_strcoll pour comparer des cha\u00eenes de caract\u00e8res Unicode.

    ", "tags": ["ICU", "strcmp", "setlocale", "ucol_strcoll", "strcoll", "strxfrm"]}, {"location": "course-c/35-libraries/standard-library/#strerror", "title": "strerror", "text": "

    La fonction strerror permet de r\u00e9cup\u00e9rer un message d'erreur associ\u00e9 \u00e0 un code d'erreur. Son prototype est\u2009:

    char *strerror(int errnum);\n

    Elle est utilis\u00e9e principalement pour afficher des messages d'erreur associ\u00e9s \u00e0 des codes d'erreur. Par exemple\u2009:

    FILE *f = fopen(\"file.txt\", \"r\");\nif (f == NULL) {\n    fprintf(stderr, \"Erreur lors de l'ouverture du fichier : %s\\n\",\n      strerror(errno));\n}\n

    ", "tags": ["strerror"]}, {"location": "course-c/35-libraries/standard-library/#tgmathh", "title": "<tgmath.h>", "text": "

    La biblioth\u00e8que <tgmath.h> est une biblioth\u00e8que de type g\u00e9n\u00e9rique qui permet de d\u00e9finir des fonctions math\u00e9matiques qui acceptent des arguments de diff\u00e9rents types. Par exemple, la fonction sqrt peut accepter un argument de type float, double ou long double.

    Il est courant de ne pas utiliser la bonne fonction math\u00e9matique pour un type donn\u00e9. Par exemple, on peut appeler sqrt avec un argument de type float alors que la fonction sqrtf est plus adapt\u00e9e peut entra\u00eener une perte de performance, l'inverse peut entra\u00eener une perte de pr\u00e9cision. La biblioth\u00e8que <tgmath.h> permet de r\u00e9soudre ce probl\u00e8me en d\u00e9finissant des fonctions math\u00e9matiques g\u00e9n\u00e9riques qui acceptent des arguments de diff\u00e9rents types.

    Cette g\u00e9n\u00e9ricit\u00e9 est permise \u00e0 l'aide du mot cl\u00e9 _Generic introduit en C11.

    La biblioth\u00e8que red\u00e9fini les fonctions math\u00e9matiques de la biblioth\u00e8que <math.h>, pour l'utiliser il suffit d'inclure l'en-t\u00eate <tgmath.h> \u00e0 la place de <math.h>. Par exemple, pour calculer la racine carr\u00e9e d'un nombre, on peut utiliser la fonction sqrt de la biblioth\u00e8que <tgmath.h> :

    ", "tags": ["double", "sqrtf", "_Generic", "float", "sqrt"]}, {"location": "course-c/35-libraries/standard-library/#threadsh", "title": "<threads.h>", "text": "

    La biblioth\u00e8que <threads.h> contient des fonctions pour cr\u00e9er et g\u00e9rer des threads. Les threads sont aussi nomm\u00e9s des processus l\u00e9gers qui partagent le m\u00eame espace m\u00e9moire. Un thread peut \u00eatre vu comme un sous-programme parall\u00e8le tournant dans le m\u00eame programme. Les fonctions offertes par le standard sont les suivantes\u2009:

    Fonctions sur les threads Fonction Description thrd_create Cr\u00e9e un nouveau thread thrd_exit Termine le thread thrd_join Attend la fin d'un thread thrd_sleep Met le thread en sommeil thrd_yield Passe la main \u00e0 un autre thread mtx_init Initialise un mutex mtx_lock Verrouille un mutex mtx_trylock Tente de verrouiller un mutex mtx_unlock D\u00e9verrouille un mutex mtx_destroy D\u00e9truit un mutex cnd_init Initialise une variable de condition cnd_signal Signale une variable de condition cnd_broadcast Signale toutes les variables de condition cnd_wait Attend une variable de condition cnd_destroy D\u00e9truit une variable de condition

    Pour plus de d\u00e9tails sur le fonctionnement des threads, vous pouvez consulter un cours sp\u00e9cialis\u00e9 sur la programmation concurrente.

    ", "tags": ["cnd_signal", "cnd_broadcast", "cnd_wait", "thrd_join", "mtx_unlock", "thrd_exit", "thrd_yield", "mtx_trylock", "mtx_destroy", "thrd_create", "mtx_lock", "cnd_init", "thrd_sleep", "cnd_destroy", "mtx_init"]}, {"location": "course-c/35-libraries/standard-library/#timeh", "title": "<time.h>", "text": "

    La biblioth\u00e8que <time.h> contient des fonctions pour lire et convertir des dates et heures. Les fonctions sont d\u00e9finies pour les dates et heures en secondes depuis le 1er janvier 1970.

    Fonctions sur les dates et heures Fonction Description time Temps \u00e9coul\u00e9 depuis le 1er janvier 1970 localtime Convertit le temps en heure locale gmtime Convertit le temps en heure UTC asctime Convertit le temps en cha\u00eene de caract\u00e8res ctime Convertit le temps en cha\u00eene de caract\u00e8res strftime Convertit le temps en cha\u00eene de caract\u00e8res mktime Convertit une structure en temps difftime Diff\u00e9rence entre deux temps clock Temps CPU utilis\u00e9 par le programme", "tags": ["clock", "ctime", "gmtime", "mktime", "asctime", "localtime", "time", "difftime", "strftime"]}, {"location": "course-c/35-libraries/standard-library/#stucture-tm", "title": "Stucture tm", "text": "

    Les fonctions de date et d'heure utilisent la structure tm pour repr\u00e9senter les dates et heures. La structure est d\u00e9finie comme suit\u2009:

    struct tm {\n    int tm_sec;   // Secondes (0-59)\n    int tm_min;   // Minutes (0-59)\n    int tm_hour;  // Heures (0-23)\n    int tm_mday;  // Jour du mois (1-31)\n    int tm_mon;   // Mois (0-11)\n    int tm_year;  // Ann\u00e9e - 1900\n    int tm_wday;  // Jour de la semaine (0-6, dimanche = 0)\n    int tm_yday;  // Jour de l'ann\u00e9e (0-365)\n    int tm_isdst; // Heure d'\u00e9t\u00e9 (0, 1, -1)\n};\n
    "}, {"location": "course-c/35-libraries/standard-library/#time", "title": "time", "text": "

    La fonction time permet de r\u00e9cup\u00e9rer le temps \u00e9coul\u00e9 depuis le 1er janvier 1970. Elle prend en param\u00e8tre un pointeur sur un time_t qui contiendra le temps \u00e9coul\u00e9. Ce dernier peut \u00eatre NULL si on ne souhaite pas r\u00e9cup\u00e9rer le temps. Le prototype de la fonction est le suivant\u2009:

    time_t time(time_t *t);\n

    Un exemple d'utilisation est le suivant\u2009:

    time_t t;\ntime(&t);\nprintf(\"Time since 1st January 1970 : %ld seconds\\n\", t);\n\n// Ou sans r\u00e9cup\u00e9rer le temps\nprintf(\"Time since 1st January 1970 : %ld seconds\\n\", time(NULL));\n

    Pourquoi le 1er janvier 1970\u2009? C'est une convention qui remonte aux premiers syst\u00e8mes Unix. Le temps est stock\u00e9 en secondes depuis cette date. C'est ce qu'on appelle le temps Unix ou temps POSIX.

    Probl\u00e8me de l'an 2038

    Le temps Unix est stock\u00e9 sur 32 bits. Cela signifie que le temps Unix ne pourra plus \u00eatre stock\u00e9 sur 32 bits \u00e0 partir du 19 janvier 2038. C'est ce qu'on appelle le bug de l'an 2038. Il est donc n\u00e9cessaire de passer \u00e0 un temps stock\u00e9 sur 64 bits pour \u00e9viter ce probl\u00e8me.

    La taille de time_t d\u00e9pend de l'impl\u00e9mentation. Sur la plupart des syst\u00e8mes, time_t est un alias pour long. Sur les syst\u00e8mes 64 bits, time_t est un alias pour long long.

    Pourquoi avoir deux moyen de retourner le temps\u2009? C'est une question de style. Certains pr\u00e9f\u00e8rent r\u00e9cup\u00e9rer le temps dans une variable, d'autres pr\u00e9f\u00e8rent le r\u00e9cup\u00e9rer directement sans variable interm\u00e9diaire.

    ", "tags": ["NULL", "time_t", "time", "long"]}, {"location": "course-c/35-libraries/standard-library/#localtime-et-gmtime", "title": "localtime et gmtime", "text": "

    Ces deux fonctions permettent de convertir un temps en heure locale ou en heure UTC. Leur prototype est le suivant\u2009:

    struct tm *localtime(const time_t *timep);\nstruct tm *gmtime(const time_t *timep);\n

    localtime se base sur les param\u00e8tres r\u00e9gionaux fix\u00e9s dans le syst\u00e8me pour d\u00e9terminer le fuseau horaire. Elle tient compte de l'ajustement pour l'heure d'\u00e9t\u00e9. gmtime en revanche se base sur le fuseau horaire UTC et ne tient pas compte de l'heure d'\u00e9t\u00e9.

    Un exemple d'utilisation est le suivant\u2009:

    time_t t;\ntime(&t);\nstruct tm *tm = localtime(&t);\nprintf(\"Heure locale : %d:%d:%d\\n\", tm->tm_hour, tm->tm_min, tm->tm_sec);\n
    ", "tags": ["gmtime", "localtime"]}, {"location": "course-c/35-libraries/standard-library/#asctime-et-ctime", "title": "asctime et ctime", "text": "

    Les fonctions asctime et ctime permettent de convertir un temps en cha\u00eene de caract\u00e8res. Leur prototype est le suivant\u2009:

    char *asctime(const struct tm *tm);\nchar *ctime(const time_t *timep);\n

    L'une prend en param\u00e8tre une structure tm et l'autre un temps. Elles retournent une cha\u00eene de caract\u00e8res repr\u00e9sentant le temps. Par exemple\u2009:

    time_t current_time = time(NULL);\nstruct tm *local_tm = localtime(&current_time);\nprintf(\"Heure locale : %s\", asctime(local_tm));\n// Affiche par exemple \"Sun Sep 16 01:03:52 1973\\n\" (locale en anglais)\n//          \"Dimanche 16 Septembre 01:03:52 1973\\n\" (locale en fran\u00e7ais)\n

    Pour afficher l'heure actuelle, on peut \u00e9galement utiliser ctime :

    time_t current_time = time(NULL);\nprintf(\"Heure locale : %s\", ctime(&current_time));\n
    ", "tags": ["asctime", "ctime"]}, {"location": "course-c/35-libraries/standard-library/#strftime", "title": "strftime", "text": "

    La fonction strftime permet de convertir un temps en cha\u00eene de caract\u00e8res en utilisant un format sp\u00e9cifique. Son prototype est le suivant\u2009:

    size_t strftime(char *s, size_t maxsize, const char *format,\n                const struct tm *tm);\n

    Elle prend en param\u00e8tre un pointeur sur une cha\u00eene de caract\u00e8res, la taille de la cha\u00eene, un format et une structure tm. Elle retourne le nombre de caract\u00e8res \u00e9crits dans la cha\u00eene.

    Format de strftime Format Description Exemple de sortie %A Nom complet du jour de la semaine \"Sunday\" %a Nom abr\u00e9g\u00e9 du jour de la semaine \"Sun\" %B Nom complet du mois \"January\" %b Nom abr\u00e9g\u00e9 du mois \"Jan\" %C Si\u00e8cle (les deux premiers chiffres de l'ann\u00e9e) \"20\" pour 2024 %d Jour du mois (01-31) \"17\" %D Date au format MM/DD/YY \"09/17/24\" %e Jour du mois (1-31, avec espace si un chiffre) \"17\" ou \" 7\" %F Date au format YYYY-MM-DD \"2024-09-17\" %H Heure (00-23, format 24 heures) \"14\" %I Heure (01-12, format 12 heures) \"02\" %j Jour de l'ann\u00e9e (001-366) \"260\" %k Heure (0-23, avec espace si chiffre) \" 2\" %l Heure (1-12, avec espace si chiffre, 12 heures) \" 2\" %M Minutes (00-59) \"05\" %m Mois (01-12) \"09\" %n Saut de ligne \"\\n\" %p Indicateur AM ou PM \"PM\" %P Indicateur am ou pm (minuscule) \"pm\" %r Heure au format 12 heures (hh:mm:ss AM/PM) \"02:05:45 PM\" %R Heure au format 24 heures (hh:mm) \"14:05\" %S Secondes (00-60) \"45\" %T Heure au format 24 heures (hh:mm:ss) \"14:05:45\" %u Num\u00e9ro du jour de la semaine (1-7, lundi = 1) \"2\" pour mardi %U Num\u00e9ro de la semaine (00-53, dimanche) \"37\" %W Num\u00e9ro de la semaine (00-53, lundi) \"37\" %V Num\u00e9ro de la semaine ISO 8601 (01-53, lundi) \"38\" %w Num\u00e9ro du jour de la semaine (0-6, dimanche = 0) \"0\" %x Repr\u00e9sentation locale de la date \"09/17/24\" %X Repr\u00e9sentation locale de l'heure \"14:05:45\" %y Ann\u00e9e (00-99, deux derniers chiffres) \"24\" %Y Ann\u00e9e (tous les chiffres) \"2024\" %z D\u00e9calage UTC (format +hhmm) \"+0200\" (UTC+2) %Z Nom du fuseau horaire \"CEST\" %% Symbole % \"%\"

    Queqlues notes sur les formats\u2009:

    • Le %S peut retourner 60 lorsqu'une seconde intercalaire est ins\u00e9r\u00e9e. Une second intercalaire est une seconde ajout\u00e9e \u00e0 la fin d'une minute pour compenser la rotation de la Terre.
    • La diff\u00e9rence entre %U et %W est que %U commence la semaine le dimanche alors que %W commence la semaine le lundi. Les am\u00e9ricains utilisent %U alors que les europ\u00e9ens utilisent %V.

    Voici un exemple d'utilisation\u2009:

    #include <stdio.h>\n#include <time.h>\n\nint main() {\n    // Obtenir l'heure locale\n    time_t current_time = time(NULL);\n    struct tm *local_tm = localtime(&current_time);\n\n    // Formater la date et l'heure\n    char buffer[100];\n    strftime(buffer, sizeof(buffer),\n      \"Aujourd'hui, c'est %A, %d %B %Y, et il est %T.\", local_tm);\n\n    printf(\"%s\\n\", buffer);\n}\n

    Il pourrait afficher\u2009:

    Aujourd'hui, c'est vendredi, 17 septembre 2024, et il est 14:05:45.\n

    ", "tags": ["strftime"]}, {"location": "course-c/35-libraries/standard-library/#ucharh", "title": "<uchar.h>", "text": "

    Apparue avec la norme C11, cette biblioth\u00e8que contient des fonctions pour g\u00e9rer les caract\u00e8res Unicode. Elle contient des fonctions pour convertir des caract\u00e8res en minuscules ou majuscules, pour tester si un caract\u00e8re est un chiffre, une lettre, etc.

    Un caract\u00e8re multi-octets (multibyte) est un caract\u00e8re qui n\u00e9cessite plus d'un octet pour \u00eatre stock\u00e9. Nous avons que la norme Unicode d\u00e9finit un jeu de caract\u00e8res universel qui peut \u00eatre repr\u00e9sent\u00e9 en binaire avec des caract\u00e8res de 8-bit (UTF-8). Cela permet de stocker th\u00e9oriquement jusqu'\u00e0 4 294 967 295 caract\u00e8res diff\u00e9rents.

    Le C \u00e9tant un langage ancien, il a \u00e9t\u00e9 con\u00e7u \u00e0 une \u00e9poque o\u00f9 seul la table ASCII existait. N\u00e9anmoins, certaines langues comme le chinois n\u00e9cessitaient plus de 256 caract\u00e8res. Pour cela, le C a introduit le concept de caract\u00e8res larges (wide characters) qui \u00e9taient initialement stock\u00e9s sur 16-bits (short). N\u00e9anmoins, avec l'arriv\u00e9e de l'Unicode, il n'est pas rare de trouver des caract\u00e8res qui n\u00e9cessitent 32-bits. Or, les wide-chars historiques du C ne sont que sur 16-bits (sous Windows) et 32-bits (sous Unix). Pour palier \u00e0 ce probl\u00e8me de portabilit\u00e9, la norme C11 a introduit la biblioth\u00e8que <uchar.h> qui permet de g\u00e9rer les caract\u00e8res Unicode convenablement.

    La biblioth\u00e8que d\u00e9finit deux types suppl\u00e9mentaires\u2009:

    char16_t; // 16-bit pour l'UTF-16\nchar32_t; // 32-bit pour l'UTF-32\n

    Contrairement \u00e0 UTF-8 qui est un encodage variable\u2009: de 1 \u00e0 4 bytes, l'UTF-16 et l'UTF-32 sont des encodages fixes (\u00e0 moins d'utiliser des surrogatges). Comme la plupart des syst\u00e8mes utilisent massivement l'UTF-8, la biblioth\u00e8que offre des fonctions de conversion entre les diff\u00e9rents encodages.

    Fonctions de conversion de caract\u00e8res Fonction Description c16rtomb Convertit un caract\u00e8re 16-bit en UTF-8 c32rtomb Convertit un caract\u00e8re 32-bit en UTF-8 mbrtoc16 Convertit un caract\u00e8re UTF-8 en 16-bit mbrtoc32 Convertit un caract\u00e8re UTF-8 en 32-bit c16rtowc Convertit un caract\u00e8re 16-bit en wide char c32rtowc Convertit un caract\u00e8re 32-bit en wide char wctoc16 Convertit un wide char en 16-bit wctoc32 Convertit un wide char en 32-bit

    Le standard C nomme mb (multibyte) pour se r\u00e9f\u00e9rer \u00e0 UTF-8.

    L'inconv\u00e9nient majeur d'UTF-8 c'est qu'il est impossible d'\u00e9diter un caract\u00e8re \u00e0 un endroit pr\u00e9cis sans devoir possiblement d\u00e9caler tous les caract\u00e8res suivants. Remplacer un e (stock\u00e9 sur 1 byte) par un \u00e9moji (stock\u00e9 sur 4 bytes), n\u00e9cessite de d\u00e9caler tout le texte de 3 bytes. Suivant la taille de la cha\u00eene cela peut \u00eatre fastidieux. C'est pourquoi l'UTF-32 est souvent utilis\u00e9 pour les traitements internes. On perd de la place m\u00e9moire car un texte en UTF-32 est jusqu'\u00e0 4 fois plus gros qu'en UTF-8, mais on gagne en temps de traitement car aucun d\u00e9clage n'est n\u00e9cessaire. En outre, le processeur \u00e9tant plus \u00e0 l'aise avec les donn\u00e9es align\u00e9es sur 32-bits, les traitements sont plus rapides.

    Prenons l'exemple d'un algorithme qui inverse une cha\u00eene de caract\u00e8res UTF-8 et affiche le r\u00e9sultat. Sans cette biblioth\u00e8que, il n'est pas trivial de le faire car les caract\u00e8res unicode peuvent \u00eatre stock\u00e9s sur plusieurs bytes. Ici on commence par convertir la cha\u00eene UTF-8 en UTF-32 pour avoir une cha\u00eene simple \u00e0 traiter, on inverse ensuite la cha\u00eene UTF-32, puis on la reconvertit en UTF-8 pour l'affichage. Une impl\u00e9mentation est donn\u00e9e dans la section algorithmes.

    ", "tags": ["c32rtomb", "c32rtowc", "multibyte", "c16rtowc", "c16rtomb", "wctoc32", "mbrtoc16", "short", "wctoc16", "mbrtoc32"]}, {"location": "course-c/35-libraries/standard-library/#wcharh", "title": "<wchar.h>", "text": "

    Cette biblioth\u00e8que suppl\u00e9mente d'autres biblioth\u00e8ques pour g\u00e9rer les caract\u00e8res larges (wide characters). Ci-dessous la table d'\u00e9quilvalence\u2009:

    Fonctions li\u00e9es aux caract\u00e8res larges Fonction Description \u00c9quivalent wcstol Convertit une cha\u00eene en long strtol wcstoul Convertit une cha\u00eene en unsigned long strtoul wcstoll Convertit une cha\u00eene en long long strtoll wcstoull Convertit une cha\u00eene en unsigned long long strtoull wcstof Convertit une cha\u00eene en float strtof wcstod Convertit une cha\u00eene en double strtod wcstold Convertit une cha\u00eene en long double strtold wcscpy Copie une cha\u00eene strcpy wcsncpy Copie une cha\u00eene strncpy wcscat Concat\u00e8ne deux cha\u00eenes strcat wcsncat Concat\u00e8ne deux cha\u00eenes strncat wcscmp Compare deux cha\u00eenes strcmp wcsncmp Compare deux cha\u00eenes strncmp wcschr Recherche un caract\u00e8re dans une cha\u00eene strchr wcsrchr Recherche un caract\u00e8re dans une cha\u00eene strrchr wcscoll Compare deux cha\u00eenes strcoll wcslen Calcule la longueur d'une cha\u00eene strlen wcsxfrm Transforme une cha\u00eene strxfrm wmemcmp Compare deux r\u00e9gions m\u00e9moire memcmp wmemchr Recherche un caract\u00e8re dans une r\u00e9gion m\u00e9moire memchr wmemcpy Copie une r\u00e9gion m\u00e9moire memcpy wmemmove Copie une r\u00e9gion m\u00e9moire memmove wmemset Remplit une r\u00e9gion m\u00e9moire memset

    ", "tags": ["strtold", "wcsncmp", "wcscpy", "wmemchr", "wcscoll", "strcpy", "wcsncat", "wcschr", "strncat", "strchr", "strrchr", "memchr", "wcstod", "wcscmp", "wcstoll", "strncpy", "strncmp", "wmemcpy", "strtod", "wcscat", "wcsxfrm", "wcstold", "memset", "wcstol", "strcmp", "strtoul", "strtol", "strlen", "strtoll", "strxfrm", "wcsrchr", "wcstoul", "wmemset", "wcstoull", "wmemcmp", "strtof", "memmove", "wcsncpy", "wcstof", "memcpy", "strcoll", "strtoull", "memcmp", "wmemmove", "wcslen", "strcat"]}, {"location": "course-c/35-libraries/standard-library/#wctypeh", "title": "<(w)ctype.h>", "text": "

    La biblioth\u00e8que <ctype.h> contient des fonctions pour tester et convertir des caract\u00e8res. Les fonctions sont d\u00e9finies pour les caract\u00e8res ASCII uniquement, elle ne s'applique pas aux caract\u00e8res Unicode, ni aux caract\u00e8res \u00e9tendus (au-del\u00e0 de 127). La biblioth\u00e8que <wctype.h> est similaire mais pour les caract\u00e8res larges (wide characters).

    Fonctions de test de caract\u00e8res ctype wctype Description isalnum iswalnum une lettre ou un chiffre isalpha iswalpha une lettre iscntrl iswcntrl un caract\u00e8re de commande isdigit iswdigit un chiffre d\u00e9cimal isgraph iswgraph un caract\u00e8re imprimable ou le blanc islower iswlower une lettre minuscule isprint iswprint un caract\u00e8re imprimable (pas le blanc) ispunct iswpunct un caract\u00e8re imprimable pas isalnum isspace iswspace un caract\u00e8re d'espace blanc isupper iswupper une lettre majuscule isxdigit iswxdigit un chiffre hexad\u00e9cimal

    En plus de ces fonctions de test, il existe des fonctions de conversion de casse d\u00e9finies dans <wctype.h> :

    Fonctions de conversion de casse Fonction Description towlower Convertit une lettre en minuscule towupper Convertit une lettre en majuscule towctrans Convertit un caract\u00e8re selon la locale LC_CTYPE", "tags": ["LC_CTYPE", "towupper", "iswalnum", "isgraph", "iswdigit", "iswspace", "iswgraph", "isdigit", "iswupper", "towlower", "islower", "iswlower", "isalpha", "iscntrl", "iswalpha", "iswpunct", "isxdigit", "iswcntrl", "iswprint", "isalnum", "ispunct", "isprint", "isupper", "iswxdigit", "towctrans", "isspace"]}, {"location": "course-c/35-libraries/standard-library/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Arc-cosinus

    La fonction Arc-Cosinus acos est-elle d\u00e9finie par le standard et dans quel fichier d'en-t\u00eate est-elle d\u00e9clar\u00e9e\u2009? Un fichier d'en-t\u00eate se termine avec l'extension .h.

    Solution

    En cherchant man acos header dans Google, on trouve que la fonction acos est d\u00e9finie dans le header <math.h>.

    Une autre solution est d'utiliser sous Linux la commande apropos:

    $ apropos acos\nacos (3)     - arc cosine function\nacosf (3)    - arc cosine function\nacosh (3)    - inverse hyperbolic cosine function\nacoshf (3)   - inverse hyperbolic cosine function\nacoshl (3)   - inverse hyperbolic cosine function\nacosl (3)    - arc cosine function\ncacos (3)    - complex arc cosine\ncacosf (3)   - complex arc cosine\ncacosh (3)   - complex arc hyperbolic cosine\ncacoshf (3)  - complex arc hyperbolic cosine\ncacoshl (3)  - complex arc hyperbolic cosine\ncacosl (3)   - complex arc cosine\n

    Le premier r\u00e9sultat permet ensuite de voir\u2009:

    $ man acos | head -10\nACOS(3)    Linux Programmer's Manual         ACOS(3)\n\nNAME\n    acos, acosf, acosl - arc cosine function\n\nSYNOPSIS\n    #include <math.h>\n\n    double acos(double x);\n    float acosf(float x);\n

    La r\u00e9ponse est donc <math.h>.

    Sous Windows avec Visual Studio, il suffit d'\u00e9crire acos dans un fichier source et d'appuyer sur F1. L'IDE redirige l'utilisateur sur l'aide Microsoft acos-acosf-acosl qui indique que le header source est <math.h>.

    Exercice 2\u2009: Date

    Lors du formatage d'une date, on y peut y lire %w, par quoi sera remplac\u00e9 ce token ?

    ", "tags": ["apropos", "acos"]}, {"location": "course-c/35-libraries/third-party-libraries/", "title": "Autres biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques tierces constituent des ensembles coh\u00e9rents de fonctions et de types de donn\u00e9es, con\u00e7us pour \u00eatre int\u00e9gr\u00e9s dans des programmes informatiques. Leur objectif est d'\u00e9tendre les fonctionnalit\u00e9s d'un projet sans n\u00e9cessiter la r\u00e9\u00e9criture de code existant. Provenant g\u00e9n\u00e9ralement de d\u00e9veloppeurs ou d'organisations externes, ces biblioth\u00e8ques permettent de gagner en efficacit\u00e9 tout en garantissant la robustesse et la maintenance du code.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#installation-de-bibliotheques-tierces", "title": "Installation de biblioth\u00e8ques tierces", "text": "

    Sous Linux, les biblioth\u00e8ques tierces sont g\u00e9n\u00e9ralement disponibles via les gestionnaires de paquets de la distribution. Il suffit de rechercher le nom de la biblioth\u00e8que et de l'installer \u00e0 l'aide de la commande appropri\u00e9e. Par exemple, pour installer la biblioth\u00e8que curl il faut ajouter le pr\u00e9fixe lib et le suffixe -dev . Le prefixe indique qu'il s'agit d'une biblioth\u00e8que et le suffixe indique qu'il s'agit de la version de d\u00e9veloppement incluant les fichiers d'en-t\u00eate n\u00e9cessaires pour la compilation.

    sudo apt-get install libcurl-dev\n

    Sous Windows, l'installation de biblioth\u00e8ques tierces peut \u00eatre plus complexe, car il n'existe pas de gestionnaire de paquets standardis\u00e9. Il est souvent n\u00e9cessaire de t\u00e9l\u00e9charger les fichiers d'installation depuis le site officiel de la biblioth\u00e8que et de suivre les instructions sp\u00e9cifiques \u00e0 chaque biblioth\u00e8que.

    ", "tags": ["curl", "lib"]}, {"location": "course-c/35-libraries/third-party-libraries/#libc-bibliotheque-standard-du-c", "title": "libc (Biblioth\u00e8que standard du C)", "text": "

    La biblioth\u00e8que standard du C, ou libc, est essentielle \u00e0 tout programme \u00e9crit en C. Elle regroupe les fonctions de base du langage, telles que la gestion de la m\u00e9moire, les op\u00e9rations sur les cha\u00eenes de caract\u00e8res, ou encore les entr\u00e9es/sorties. G\u00e9n\u00e9ralement incluse dans l'environnement de d\u00e9veloppement, elle est fournie par le syst\u00e8me d'exploitation ou le compilateur et repr\u00e9sente un pilier fondamental de l'\u00e9cosyst\u00e8me C.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#glib-bibliotheque-de-base-de-gnome", "title": "Glib (Biblioth\u00e8que de base de GNOME)", "text": "

    Glib est une biblioth\u00e8que polyvalente, d\u00e9velopp\u00e9e dans le cadre du projet GNOME, mais largement utilis\u00e9e au-del\u00e0. Elle propose une panoplie de fonctions pour la gestion de la m\u00e9moire, la manipulation des cha\u00eenes de caract\u00e8res, des structures de donn\u00e9es avanc\u00e9es telles que les listes, arbres, tables de hachage, ainsi que la gestion des signaux et des threads. Son architecture robuste en fait un choix privil\u00e9gi\u00e9 pour de nombreux projets open source en qu\u00eate de fiabilit\u00e9.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#securite", "title": "S\u00e9curit\u00e9", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#openssl", "title": "OpenSSL", "text": "

    OpenSSL est une biblioth\u00e8que incontournable dans le domaine de la cryptographie et de la s\u00e9curit\u00e9 des communications. Adopt\u00e9e par une multitude de projets, tant open source que commerciaux, elle propose des outils performants pour la gestion des certificats SSL, le chiffrement des donn\u00e9es, ainsi que la v\u00e9rification des signatures num\u00e9riques. Sa large adoption t\u00e9moigne de sa fiabilit\u00e9 et de sa capacit\u00e9 \u00e0 s\u00e9curiser les \u00e9changes en ligne.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#bases-de-donnees", "title": "Bases de donn\u00e9es", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#sqlite", "title": "SQLite", "text": "

    SQLite se distingue par son moteur de base de donn\u00e9es relationnelle compact et performant, largement utilis\u00e9 dans les applications mobiles et web. Malgr\u00e9 sa l\u00e9g\u00e8ret\u00e9, il offre des fonctionnalit\u00e9s avanc\u00e9es telles que la prise en charge des transactions ACID, des index, des vues, des d\u00e9clencheurs, et des fonctions SQL sophistiqu\u00e9es. Sa simplicit\u00e9 d'int\u00e9gration et son efficacit\u00e9 en font un choix privil\u00e9gi\u00e9 pour le stockage des donn\u00e9es dans des environnements vari\u00e9s.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#reseau", "title": "R\u00e9seau", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libcurl", "title": "Libcurl", "text": "

    Libcurl est une biblioth\u00e8que sp\u00e9cialis\u00e9e dans le transfert de donn\u00e9es sur le r\u00e9seau. Elle prend en charge une multitude de protocoles, dont HTTP, HTTPS, FTP, SFTP, et bien d'autres encore. Utilis\u00e9e tant par des projets open source que par des entreprises, elle est indispensable pour toute application n\u00e9cessitant des transferts de fichiers ou la communication avec des serveurs distants via des protocoles s\u00e9curis\u00e9s ou non.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#traitement-dimages", "title": "Traitement d'images", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#opencv", "title": "OpenCV", "text": "

    OpenCV est une biblioth\u00e8que de r\u00e9f\u00e9rence dans le domaine du traitement d'images et de vid\u00e9os. Utilis\u00e9e pour des applications aussi diverses que la reconnaissance faciale, la d\u00e9tection d'objets ou la vision par ordinateur, elle supporte une vaste gamme de formats d'images tels que JPEG, PNG, TIFF, et BMP. Sa richesse fonctionnelle la rend incontournable pour les projets n\u00e9cessitant une manipulation avanc\u00e9e des images.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libpng", "title": "LibPNG", "text": "

    LibPNG est sp\u00e9cialis\u00e9e dans la manipulation des images au format PNG. Elle permet de lire, \u00e9crire et modifier des images tout en g\u00e9rant les sp\u00e9cificit\u00e9s de ce format, telles que la transparence ou la compression. Sa robustesse et sa compatibilit\u00e9 avec les normes en font un choix solide pour toute application manipulant des images PNG.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libjpeg", "title": "LibJPEG", "text": "

    LibJPEG est la biblioth\u00e8que de r\u00e9f\u00e9rence pour le traitement des images JPEG. Elle prend en charge des op\u00e9rations complexes telles que la compression, la d\u00e9compression, et la manipulation d'images avec diff\u00e9rents niveaux de qualit\u00e9. Sa large adoption t\u00e9moigne de son efficacit\u00e9 dans la gestion de ce format d'image tr\u00e8s r\u00e9pandu.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#traitement-video", "title": "Traitement vid\u00e9o", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#ffmpeg", "title": "FFmpeg", "text": "

    FFmpeg est une biblioth\u00e8que puissante d\u00e9di\u00e9e au traitement des fichiers vid\u00e9o et audio. Elle supporte un large \u00e9ventail de formats tels que AVI, MP4, MOV, et MP3, et permet de r\u00e9aliser des op\u00e9rations complexes comme la conversion, le transcodage, ou encore la diffusion en direct. Sa polyvalence en fait un outil de choix pour les professionnels et les amateurs du multim\u00e9dia.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gestion-des-evenements", "title": "Gestion des \u00e9v\u00e9nements", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libevent", "title": "Libevent", "text": "

    Libevent propose des fonctionnalit\u00e9s avanc\u00e9es pour la gestion des \u00e9v\u00e9nements asynchrones, une composante essentielle des applications r\u00e9seau performantes. Elle permet de g\u00e9rer efficacement les connexions r\u00e9seau, les entr\u00e9es/sorties, et d'autres \u00e9v\u00e9nements syst\u00e8me critiques, garantissant ainsi une r\u00e9activit\u00e9 optimale.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libuv", "title": "Libuv", "text": "

    Libuv est une autre biblioth\u00e8que sp\u00e9cialis\u00e9e dans la gestion des \u00e9v\u00e9nements asynchrones, souvent utilis\u00e9e en conjonction avec Node.js. Elle se distingue par sa capacit\u00e9 \u00e0 g\u00e9rer les connexions r\u00e9seau et les entr\u00e9es/sorties de mani\u00e8re non bloquante, assurant des performances \u00e9lev\u00e9es dans les applications n\u00e9cessitant une gestion efficace de nombreux \u00e9v\u00e9nements simultan\u00e9s.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gestion-de-la-memoire", "title": "Gestion de la m\u00e9moire", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#jemalloc", "title": "Jemalloc", "text": "

    Jemalloc est r\u00e9put\u00e9e pour ses performances en mati\u00e8re de gestion de la m\u00e9moire, notamment dans les applications o\u00f9 la gestion efficace des allocations est cruciale. Elle propose des fonctionnalit\u00e9s avanc\u00e9es comme la fragmentation r\u00e9duite, les statistiques de m\u00e9moire d\u00e9taill\u00e9es, et les profils de m\u00e9moire, ce qui en fait un choix privil\u00e9gi\u00e9 pour les syst\u00e8mes \u00e0 haute performance.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#compression", "title": "Compression", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#zlib", "title": "Zlib", "text": "

    Zlib est une biblioth\u00e8que de compression polyvalente, largement utilis\u00e9e pour compresser et d\u00e9compresser des fichiers, flux de donn\u00e9es ou archives. Elle impl\u00e9mente les algorithmes DEFLATE, GZIP et ZLIB, offrant un excellent compromis entre vitesse et taux de compression.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#liblzma", "title": "LibLZMA", "text": "

    LibLZMA se sp\u00e9cialise dans la compression au format LZMA, r\u00e9put\u00e9 pour son taux de compression \u00e9lev\u00e9. Elle est utilis\u00e9e dans des contextes o\u00f9 l'efficacit\u00e9 du stockage et la r\u00e9duction de la taille des donn\u00e9es sont primordiales, tout en maintenant des performances \u00e9lev\u00e9es en mati\u00e8re de d\u00e9compression.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#serialisation", "title": "S\u00e9rialisation", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#libyaml", "title": "LibYAML", "text": "

    LibYAML offre des outils robustes pour la s\u00e9rialisation et la d\u00e9s\u00e9rialisation de donn\u00e9es au format YAML. Utilis\u00e9e pour manipuler des configurations ou des donn\u00e9es structur\u00e9es, elle g\u00e8re efficacement les diff\u00e9rents \u00e9l\u00e9ments de ce format, tels que les s\u00e9quences, les scalaires, et les paires cl\u00e9-valeur.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#libxml2", "title": "LibXML2", "text": "

    LibXML2 est une biblioth\u00e8que puissante pour le traitement des documents XML, supportant les normes et standards associ\u00e9s comme XSLT, XPath, ou XML Schema. Elle est couramment utilis\u00e9e pour l'analyse, la validation et la g\u00e9n\u00e9ration de documents XML dans des environnements o\u00f9 la pr\u00e9cision et la conformit\u00e9 aux standards sont cruciales.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#utilitaires", "title": "Utilitaires", "text": ""}, {"location": "course-c/35-libraries/third-party-libraries/#pcre-perl-compatible-regular-expressions", "title": "PCRE (Perl Compatible Regular Expressions)", "text": "

    PCRE est la r\u00e9f\u00e9rence pour le traitement des expressions r\u00e9guli\u00e8res compatibles avec Perl. Elle est utilis\u00e9e pour effectuer des recherches complexes, des remplacements, ou pour valider des cha\u00eenes de texte selon des motifs avanc\u00e9s, offrant ainsi une flexibilit\u00e9 in\u00e9gal\u00e9e dans la manipulation de texte.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gmp-gnu-multiple-precision-arithmetic-library", "title": "GMP (GNU Multiple Precision Arithmetic Library)", "text": "

    GMP est sp\u00e9cialis\u00e9e dans les calculs arithm\u00e9tiques \u00e0 pr\u00e9cision arbitraire, indispensable dans des domaines exigeants comme la cryptographie ou les calculs scientifiques. Elle permet des op\u00e9rations sur des entiers, rationnels et flottants avec une pr\u00e9cision que les biblioth\u00e8ques standards ne peuvent atteindre.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#ncurses", "title": "ncurses", "text": "

    ncurses est la biblioth\u00e8que par excellence pour la cr\u00e9ation d'interfaces utilisateur en mode texte dans les environnements de terminal. Elle propose des outils pour g\u00e9rer les fen\u00eatres, les couleurs, les panneaux, ainsi que des fonctionnalit\u00e9s avanc\u00e9es comme la capture des touches de fonction et de contr\u00f4le.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#posix-c-library", "title": "POSIX C Library", "text": "

    Le standard C ne d\u00e9finit que le minimum vital et qui est valable sur toutes les architectures pour autant que la toolchain soit compatible C99. Il existe n\u00e9anmoins toute une collection d'autres fonctions manquantes\u2009:

    • La communication entre les processus (deux programmes qui souhaitent communiquer entre eux)

    • <sys/socket.h>

    • <sharedmemory.h>

    • La communication sur le r\u00e9seau e.g. internet

    • <sys/socket.h>

    • <arpa/inet.h>
    • <net/if.h>

    • Les t\u00e2ches

    • <thread.h>

    • Les traductions de cha\u00eenes p.ex. fran\u00e7ais vers anglais

    • <iconv.h>

    • Les fonctions avanc\u00e9es de recherche de texte

    • <regex.h>

    • Le log centralis\u00e9 des messages (d'erreur)

    • <syslog.h>

    Toutes ces biblioth\u00e8ques additionnelles ne sont pas n\u00e9cessairement disponibles sur votre ordinateur ou pour le syst\u00e8me cible, surtout si vous convoitez une application bare-metal. Elles d\u00e9pendent grandement du syst\u00e8me d'exploitation utilis\u00e9, mais une tentative de normalisation existe et se nomme POSIX (ISO/IEC 9945).

    G\u00e9n\u00e9ralement la vaste majorit\u00e9 des distributions Linux et Unix sont compatibles avec le standard POSIX et les biblioth\u00e8ques ci-dessus seront disponibles \u00e0 moins que vous ne visiez une architecture diff\u00e9rente de celle sur laquelle s'ex\u00e9cute votre compilateur.

    Le support POSIX sous Windows (Win32) n'est malheureusement que partiel et il n'est pas standardis\u00e9.

    Un point d'entr\u00e9e de l'API POSIX est la biblioth\u00e8que <unistd.h>.

    "}, {"location": "course-c/35-libraries/third-party-libraries/#gnu-glibc", "title": "GNU GLIBC", "text": "

    La biblioth\u00e8que portable GNULIB est la biblioth\u00e8que standard r\u00e9f\u00e9renc\u00e9e sous Linux par libc6.

    ", "tags": ["libc6", "bibliotheque"]}, {"location": "course-c/35-libraries/third-party-libraries/#windows-c-library", "title": "Windows C library", "text": "

    La biblioth\u00e8que Windows Windoes API offre une interface au syst\u00e8me de fichier, au registre Windows, aux imprimantes, \u00e0 l'interface de fen\u00eatrage, \u00e0 la console et au r\u00e9seau.

    L'acc\u00e8s \u00e0 cet API est offert par un unique point d'entr\u00e9e windows.h qui regroupe certains en-t\u00eates standards (<stdarg.h>, <string.h>, ...), mais pas tous (\ud83d\ude14) ainsi que les en-t\u00eates sp\u00e9cifiques \u00e0 Windows tels que\u2009:

    <winreg.h>

    Pour l'acc\u00e8s au registre Windows

    <wincon.h>

    L'acc\u00e8s \u00e0 la console

    La documentation est disponible en ligne depuis le site de Microsoft, mais n'est malheureusement pas compl\u00e8te et souvent il est difficile de savoir sur quel site trouver la bonne version de la bonne documentation. Par exemple, il n'y a aucune documentation claire de LSTATUS pour la fonction RegCreateKeyExW permettant de cr\u00e9er une entr\u00e9e dans la base de registre.

    Un bon point d'entr\u00e9e est le Microsoft API and reference catalog.

    Quelques observations\u2009:

    • Officiellement Windows est compatible avec C89 (ANSI C) (c.f. C Language Reference)
    • L'API Windows n'est pas officiellement compatible avec C99, mais elle s'en approche, il n'y pas ou peu de documents expliquant les diff\u00e9rences.
    • Microsoft n'a aucune priorit\u00e9 pour d\u00e9velopper son support C, il se focalise davantage sur C++ et C#, c'est pourquoi certains \u00e9l\u00e9ments du langage ne sont pas ou peu document\u00e9s.
    • Les types standards Windows diff\u00e9rent de ceux propos\u00e9s par C99. Par exemple, LONG32 remplace int32_t.
    ", "tags": ["windows.h", "int32_t", "api", "LONG32", "LSTATUS", "windows"]}, {"location": "course-c/40-algorithms/", "title": "Algorithmes", "text": "

    Les algorithmes constituent l'essence m\u00eame de l'informatique. Ces ensembles d'instructions, d\u00e9finis avec rigueur et pr\u00e9cision, offrent des solutions claires \u00e0 une multitude de probl\u00e8mes, allant du tri d'une simple liste de nombres \u00e0 la d\u00e9termination du chemin le plus court entre deux points. Chaque algorithme, par sa structure et sa logique, permet d'accomplir des t\u00e2ches vari\u00e9es avec une efficacit\u00e9 souvent surprenante.

    Ce chapitre se propose d'explorer les fondements des algorithmes, en s'attardant sur des aspects cruciaux qui sous-tendent leur conception et leur utilisation. Nous d\u00e9buterons par une analyse de la complexit\u00e9 algorithmique, un outil indispensable pour \u00e9valuer et comparer la performance des diff\u00e9rents algorithmes. Cette notion de complexit\u00e9, qu'elle soit en termes de temps ou d'espace, est primordiale pour juger de l'efficacit\u00e9 d'une solution face \u00e0 un probl\u00e8me donn\u00e9.

    La r\u00e9cursion, technique puissante mais exigeante, fera l'objet de notre deuxi\u00e8me section. En effet, si elle permet de r\u00e9soudre des probl\u00e8mes en les d\u00e9composant en sous-probl\u00e8mes plus simples, elle n'est pas sans risques\u2009: mal ma\u00eetris\u00e9e, elle peut entra\u00eener des consommations excessives de ressources, compromettant ainsi l'efficacit\u00e9 de l'algorithme. Nous examinerons donc en d\u00e9tail les m\u00e9thodes pour analyser et optimiser la complexit\u00e9 des algorithmes r\u00e9cursifs.

    Nous aborderons ensuite les algorithmes de recherche et de tri, deux cat\u00e9gories parmi les plus fr\u00e9quemment rencontr\u00e9es en informatique. La recherche, qu'elle soit lin\u00e9aire ou binaire, et le tri, sous ses diff\u00e9rentes formes, repr\u00e9sentent des op\u00e9rations fondamentales dont la ma\u00eetrise est indispensable pour tout informaticien. Nous en d\u00e9taillerons les m\u00e9canismes, en soulignant les avantages et les inconv\u00e9nients de chaque approche.

    Enfin, nous conclurons par l'examen de quelques algorithmes classiques, embl\u00e9matiques de leur domaine, et dont l'application d\u00e9passe souvent le cadre purement th\u00e9orique. Leur \u00e9tude permettra d'illustrer la port\u00e9e pratique des concepts abord\u00e9s et d'ancrer ces derniers dans la r\u00e9alit\u00e9 des probl\u00e8mes rencontr\u00e9s quotidiennement.

    Ce chapitre vous guidera vers une compr\u00e9hension approfondie des algorithmes, en vous donnant les cl\u00e9s pour concevoir des solutions \u00e9l\u00e9gantes et efficaces \u00e0 des probl\u00e8mes complexes. Que vous soyez novice en la mati\u00e8re ou que vous cherchiez \u00e0 affiner vos comp\u00e9tences, vous trouverez ici de quoi enrichir votre savoir et affronter les d\u00e9fis de l'informatique moderne avec une approche m\u00e9thodique et optimis\u00e9e.

    "}, {"location": "course-c/40-algorithms/automata/", "title": "Automates finis", "text": "

    Dans le contexte de l'algorithmique, il est int\u00e9ressant de d'aborder les automates finis. Les automates finis sont des machines abstraites qui peuvent \u00eatre dans un nombre fini d'\u00e9tats. Ils sont utilis\u00e9s pour mod\u00e9liser des syst\u00e8mes de transitions d'\u00e9tats tr\u00e8s utile dans l'\u00e9laboration d'algorithmes notament pour le traitement de langage naturel, la reconnaissance de formes, o\u00f9 l'analyse de donn\u00e9es.

    Hi\u00e9rarchie des automates

    Nous avions abord\u00e9 en introduction de cet ouvrage la machine de Turing, ce mod\u00e8le th\u00e9orique permettant de r\u00e9aliser n'importe quel algorithme. Bien que la tr\u00e8s grande majorit\u00e9 des langages de programmation sont dit Turing-complet, il est parfois plus int\u00e9ressant d'utiliser des automates finis pour des probl\u00e8mes sp\u00e9cifiques car il existe une r\u00e8gle en informatique qui dit qu'il est g\u00e9n\u00e9ralement optimum d'aligner la complexit\u00e9 d'un langage avec la complexit\u00e9 du probl\u00e8me \u00e0 r\u00e9soudre. L'id\u00e9e sous-jacente est que si un probl\u00e8me a une structure ou une complexit\u00e9 faible, il est g\u00e9n\u00e9ralement plus efficace de le r\u00e9soudre avec un mod\u00e8le computationnel ou un langage de programmation dont la complexit\u00e9 est align\u00e9e avec celle du probl\u00e8me. Utiliser un outil plus puissant ou plus complexe que n\u00e9cessaire peut entra\u00eener des inefficacit\u00e9s, des erreurs, ou des solutions surdimensionn\u00e9es.

    On rencontre les automates finis dans de tr\u00e8s nombreux domaines, du distributeur automatique de boisson aux ascenceurs en passant par les feux de circulation.

    Contrairement \u00e0 un syst\u00e8me turing-complete, un automate fini ne peut pas r\u00e9aliser n'importe quel algorithme. Il est limit\u00e9 par sa structure et son nombre fini d'\u00e9tats. Voici l'exemple d'un automate fini simple qui mod\u00e9lise un ascenseur\u2009:

    Ascenseur

    Ces \u00e9tats peuvent \u00e9galement \u00eatre repr\u00e9sent\u00e9s par un tableau de transition\u2009:

    \u00c9tat actuel Entr\u00e9e \u00c9tat suivant Sortie Arr\u00eat\u00e9 Ouvrir porte Porte ouverte La porte s'ouvre Arr\u00eat\u00e9 Appel En mouvement Se d\u00e9place \u00e0 l'\u00e9tage En mouvement Arr\u00eat Porte ouverte Arriv\u00e9 \u00e0 l'\u00e9tage, la porte s'ouvre En mouvement Arriv\u00e9 Arr\u00eat\u00e9 Arrive \u00e0 l'\u00e9tage Porte ouverte Fermer porte Arr\u00eat\u00e9 La porte se ferme Porte ouverte Appel En mouvement La porte se ferme, se d\u00e9place \u00e0 l'\u00e9tage"}, {"location": "course-c/40-algorithms/automata/#la-chevre-et-le-chou", "title": "La ch\u00e8vre et le chou", "text": "

    Un autre exemple classique d'automate fini est le probl\u00e8me de la ch\u00e8vre, du chou et du loup. Il s'agit d'un probl\u00e8me de logique dans lequel un fermier doit transporter une ch\u00e8vre, un chou et un loup d'une rive \u00e0 l'autre d'une rivi\u00e8re. Le fermier ne peut transporter qu'un seul \u00e9l\u00e9ment \u00e0 la fois et ne peut pas laisser la ch\u00e8vre seule avec le loup ou le chou seul avec la ch\u00e8vre. Le probl\u00e8me est de trouver une s\u00e9quence de d\u00e9placements qui permet de transporter les trois \u00e9l\u00e9ments d'une rive \u00e0 l'autre sans enfreindre les r\u00e8gles.

    Ce probl\u00e8me peut \u00eatre mod\u00e9lis\u00e9 par un automate fini. Chaque \u00e9tat est nomm\u00e9 selon les \u00e9l\u00e9ments pr\u00e9sents sur la rive d'arriv\u00e9e. Par exemple l'\u00e9tat CFS signifie que la ch\u00e8vre, le loup et le fermier sont sur la rive d'arriv\u00e9e. Les transitions indiquent les \u00e9l\u00e9ments qui transitent d'une rive \u00e0 l'autre. Notons que l'on appelle S le chou (comme Salade), pour \u00e9viter la confusion avec la ch\u00e8vre. Voici le diagramme d'\u00e9tats\u2009:

    La ch\u00e8vre, le loup et le chou

    "}, {"location": "course-c/40-algorithms/automata/#implementation", "title": "Impl\u00e9mentation", "text": "

    G\u00e9n\u00e9ralement on commence par d\u00e9finir les \u00e9tats et les transitions de l'automate. On ajoute volontairement une entr\u00e9e mangl\u00e9e COUNT pour facilement conna\u00eetre le nombre d'\u00e9l\u00e9ments dans l'\u00e9num\u00e9ration.

    typedef enum {\n    STATE_IDLE,\n    STATE_MOVING,\n    STATE_DOOR_OPEN,\n    _STATE_COUNT\n} State;\n\ntypedef enum {\n    INPUT_OPEN_DOOR,\n    INPUT_CALL,\n    INPUT_STOP,\n    INPUT_ARRIVAL,\n    INPUT_CLOSE_DOOR,\n    _INPUT_COUNT\n} Input;\n\nvoid openDoor() { printf(\"The door opens.\\n\"); }\nvoid closeDoor() { printf(\"The door closes.\\n\"); }\nvoid moveElevator() { printf(\"The elevator is moving.\\n\"); }\nvoid stopElevator() { printf(\"The elevator stops.\\n\"); }\n

    L'objectif est de pouvoir \u00e9mettre des transitions simplement\u2009:

    int main() {\n    State currentState = STATE_IDLE;\n\n    // Exemples de transitions\n    currentState = transition(currentState, INPUT_CALL);\n    currentState = transition(currentState, INPUT_STOP);\n    currentState = transition(currentState, INPUT_CLOSE_DOOR);\n    currentState = transition(currentState, INPUT_CALL);\n    currentState = transition(currentState, INPUT_ARRIVAL);\n}\n

    Les automates finis peuvent \u00eatre impl\u00e9ment\u00e9s de diff\u00e9rentes mani\u00e8res. Une solution courante est d'utiliser un switch-case. Voici l'exemple en C\u2009:

    State transition(State currentState, Input input) {\n    switch (currentState) {\n        case STATE_IDLE:\n            switch (input) {\n                case INPUT_OPEN_DOOR:\n                    openDoor();\n                    return STATE_DOOR_OPEN;\n                case INPUT_CALL:\n                    moveElevator();\n                    return STATE_MOVING;\n                default:\n                    return currentState;\n            }\n\n        case STATE_MOVING:\n            switch (input) {\n                case INPUT_STOP:\n                    stopElevator();\n                    openDoor();\n                    return STATE_DOOR_OPEN;\n                case INPUT_ARRIVAL:\n                    stopElevator();\n                    return STATE_IDLE;\n                default:\n                    return currentState;\n            }\n\n        case STATE_DOOR_OPEN:\n            switch (input) {\n                case INPUT_CLOSE_DOOR:\n                    closeDoor();\n                    return STATE_IDLE;\n                case INPUT_CALL:\n                    closeDoor();\n                    moveElevator();\n                    return STATE_MOVING;\n                default:\n                    return currentState;\n            }\n\n        default:\n            return currentState;\n    }\n}\n

    Une autre approche est d'utiliser un table de transition. Cela permet de s\u00e9parer la logique de transition de l'impl\u00e9mentation. Voici un exemple\u2009:

    typedef void (*ActionFunction)();\n\ntypedef struct transition {\n    State nextState;\n    ActionFunction action;\n} Transition;\n\nTransition stateTransitionTable[_STATE_COUNT][_INPUT_COUNT] = {\n    [STATE_IDLE][INPUT_OPEN_DOOR] = {STATE_DOOR_OPEN, openDoor},\n    [STATE_IDLE][INPUT_CALL] = {STATE_MOVING, moveElevator},\n    [STATE_IDLE][INPUT_STOP] = {STATE_IDLE, NULL},\n    [STATE_IDLE][INPUT_ARRIVAL] = {STATE_IDLE, NULL},\n    [STATE_IDLE][INPUT_CLOSE_DOOR] = {STATE_IDLE, NULL},\n\n    [STATE_MOVING][INPUT_OPEN_DOOR] = {STATE_MOVING, NULL},\n    [STATE_MOVING][INPUT_CALL] = {STATE_MOVING, NULL},\n    [STATE_MOVING][INPUT_STOP] = {STATE_DOOR_OPEN, openDoor},\n    [STATE_MOVING][INPUT_ARRIVAL] = {STATE_IDLE, stopElevator},\n    [STATE_MOVING][INPUT_CLOSE_DOOR] = {STATE_MOVING, NULL},\n\n    [STATE_DOOR_OPEN][INPUT_OPEN_DOOR] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_CALL] = {STATE_MOVING, moveElevator},\n    [STATE_DOOR_OPEN][INPUT_STOP] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_ARRIVAL] = {STATE_DOOR_OPEN, NULL},\n    [STATE_DOOR_OPEN][INPUT_CLOSE_DOOR] = {STATE_IDLE, closeDoor},\n};\n\nState transition(State currentState, Input input) {\n    Transition transition = stateTransitionTable[currentState][input];\n    if (transition.action != NULL) {\n        transition.action();\n    }\n    return transition.nextState;\n}\n
    ", "tags": ["COUNT"]}, {"location": "course-c/40-algorithms/automata/#dfa-et-nfa", "title": "DFA et NFA", "text": "

    Il existe deux types d'automates finis\u2009: les automates finis d\u00e9terministes (DFA) et les automates finis non d\u00e9terministes (NFA). Les DFA sont des automates finis dont les transitions sont d\u00e9termin\u00e9es par l'\u00e9tat actuel et l'entr\u00e9e. Les NFA sont des automates finis dont les transitions peuvent \u00eatre non d\u00e9terministes, c'est-\u00e0-dire qu'il peut y avoir plusieurs transitions possibles pour un \u00e9tat et une entr\u00e9e donn\u00e9s.

    Pour un ordinateur, il est plus facile de traiter un automate fini d\u00e9terministe qu'un automate fini non d\u00e9terministe. Cependant, les NFA sont plus puissants que les DFA, car ils peuvent repr\u00e9senter des langages plus complexes. Heureusement il existe des algorithmes pour convertir un NFA en un DFA \u00e9quivalent, mais cela peut entra\u00eener une explosion de l'espace d'\u00e9tat.

    Un cas typique d'utilisation de ces diagrammes d'\u00e9tats sont les expressions r\u00e9guli\u00e8res. Pour rappel, une expression r\u00e9guli\u00e8re est une cha\u00eene de caract\u00e8re qui d\u00e9crit un ensemble de cha\u00eenes de caract\u00e8res. On les utilises pour rechercher des motifs complexes.

    L'objectif n'est pas de rentrer dans le d\u00e9tail mais de vous donner un aper\u00e7u de ce qu'il est possible de faire avec les automates finis. Admettons que l'on souhaite rechercher des motifs de texte contenant les lettres A et B. On peut avoir plusieurs combinaisons par exemple on recherche soit un A ou un B /A|B/ ou bien un A suivi d'un B /AB/. En utilisant l'\u00e9toile de Kleene * on peut \u00e9galement rechercher z\u00e9ro ou plusieurs occurences de A: /A*/.

    Dans ces exemples, \u00e0 chaque \u00e9tat un caract\u00e8re est captur\u00e9 sur la cha\u00eene de recherche (le curseur avance d'un caract\u00e8re) :

    DFA simples

    \u00c0 partir de ces \u00e9l\u00e9ments simples, il est possible de construire une expression plus complexe comme /Z|X(X|Y)*/ : la lettre Z seule ou bien X suivi de A ou B z\u00e9ro ou plusieurs fois. Pour le cas de figure de l'\u00e9toile de Kleene, il n'est pas \u00e9vident de constuire cette expression. Pour r\u00e9soudre ce probl\u00e8me on introduit la notion de transition epsilon \u03b5 qui permet de passer d'un \u00e9tat \u00e0 un autre sans consommer de caract\u00e8re. On peut en mettre autant que l'on veut\u2009:

    NFA avec epsilon

    Ce diagramme d'\u00e9tat peut \u00eatre repr\u00e9sent\u00e9 sous forme de tableau de transition\u2009:

    Table de transition X Y Z \u03b5 >1 2 - 5 1 2 - - - 2,3,5 3 4 4 - 3 4 - - - 4,5 (5) - - - 5

    Dans le but de simplifier on peut utiliser l'algorithme de Thompson-McNaughton-Yamada pour convertir l'automate non d\u00e9terministe en un automate d\u00e9terministe. Pour ce faire on utilise la table de transition suivante qui utilise les epsilon-closures :

    Table de transition simplifi\u00e9e X\u03b5* Y\u03b5* Z\u03b5* >1 2,3,(5) - (5) 2,3,(5) 3,4,(5) 3,4,(5) - (5) - - - 3,4,(5) 3,4,(5) 3,4,(5) -

    Ceci nous donne un automate fini d\u00e9terministe (DFA):

    DFA

    "}, {"location": "course-c/40-algorithms/automata/#implementation_1", "title": "Impl\u00e9mentation", "text": "

    Si nous souhaitons impl\u00e9menter un moteur d'expression r\u00e9guli\u00e8res simple qui prend en compte les \u00e9l\u00e9ments suivants\u2009:

    • . : n'importe quel caract\u00e8re
    • | : ou
    • ( ) : groupe
    • * : z\u00e9ro ou plusieurs occurences
    • + : une ou plusieurs occurences
    • Les autres caract\u00e8res sont litt\u00e9raux

    Les \u00e9tapes de l'algorithmes sont les suivants\u2009:

    1. Convertir l'expression d'entr\u00e9e en un NFA
    2. Appliquer l'algorithme de Thompson pour convertir le NFA en un DFA
    3. Utiliser le DFA pour rechercher les motifs dans le texte
    "}, {"location": "course-c/40-algorithms/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/40-algorithms/introduction/#introduction", "title": "Introduction", "text": "... conduire par ordre mes pens\u00e9es, en commen\u00e7ant par les objets les plus simples et les plus ais\u00e9s \u00e0 conna\u00eetre, pour monter peu \u00e0 peu, comme par degr\u00e9s, jusques \u00e0 la connaissance des plus compos\u00e9s; et supposant m\u00eame de l'ordre entre ceux qui ne se pr\u00e9c\u00e8dent point naturellement les uns les autres.Ren\u00e9 Descartes, Discours de la m\u00e9thode

    L'algorithmique est le domaine scientifique qui \u00e9tudie les algorithmes, une suite finie et non ambigu\u00eb d'op\u00e9rations ou d'instructions permettant de r\u00e9soudre un probl\u00e8me ou de traiter des donn\u00e9es.

    Un algorithme peut \u00eatre \u00e9galement consid\u00e9r\u00e9 comme \u00e9tant n'importe quelle s\u00e9quence d'op\u00e9rations pouvant \u00eatre simul\u00e9es par un syst\u00e8me Turing-complet. Un syst\u00e8me est d\u00e9clar\u00e9 Turing-complet s'il peut simuler n'importe quelle machine de Turing. For heureusement, le langage C est Turing-complet puisqu'il poss\u00e8de tous les ingr\u00e9dients n\u00e9cessaires \u00e0 la simulation de ces machines, soit compter, comparer, lire, \u00e9crire...

    Dans le cas qui concerne cet ouvrage, un algorithme est une recette exprim\u00e9e en une liste d'instructions et permettant de r\u00e9soudre un probl\u00e8me informatique. Cette recette contient \u00e0 peu de choses pr\u00e8s les \u00e9l\u00e9ments programmatiques que nous avons d\u00e9j\u00e0 entre aper\u00e7us\u2009: des structures de contr\u00f4le, des variables, etc.

    G\u00e9n\u00e9ralement un algorithme peut \u00eatre exprim\u00e9 graphiquement en utilisant un organigramme (flowchart) ou un structogramme (Nassi-Shneiderman diagram) afin de s'affranchir du langage de programmation cible.

    La conception aussi appel\u00e9e Architecture logicielle est l'art de penser un programme avant son impl\u00e9mentation. La phase de conception fait bien souvent appel \u00e0 des algorithmes.

    Pour \u00eatre qualifi\u00e9es d'algorithmes, certaines propri\u00e9t\u00e9s doivent \u00eatre respect\u00e9es\u2009:

    1. Entr\u00e9es, un algorithme doit poss\u00e9der 0 ou plus d'entr\u00e9es en provenance de l'ext\u00e9rieur de l'algorithme.
    2. Sorties, un algorithme doit poss\u00e9der au moins une sortie.
    3. Rigueur, chaque \u00e9tape d'un algorithme doit \u00eatre claire et bien d\u00e9finie.
    4. Finitude, un algorithme doit comporter un nombre fini d'\u00e9tapes.
    5. R\u00e9p\u00e9table, un algorithme doit fournir un r\u00e9sultat r\u00e9p\u00e9table.
    "}, {"location": "course-c/40-algorithms/introduction/#complexite-algorithmique", "title": "Complexit\u00e9 Algorithmique", "text": "

    Il est souvent utile de savoir quelle est la performance d'un algorithme afin de le comparer \u00e0 un autre algorithme \u00e9quivalent. On peut s'int\u00e9resser \u00e0 deux indicateurs\u2009:

    • La complexit\u00e9 en temps : combien de temps CPU consomme un algorithme pour s'ex\u00e9cuter.
    • La complexit\u00e9 en m\u00e9moire : combien de m\u00e9moire tampon consomme un algorithme pour s'ex\u00e9cuter.

    Bien \u00e9videmment, la complexit\u00e9 d'un algorithme d\u00e9pend des donn\u00e9es en entr\u00e9e. Par exemple si on vous donne \u00e0 corriger un examen de 100 copies, et le protocol de correction associ\u00e9, votre temps de travail d\u00e9pendra du nombre de copies \u00e0 corriger, je sais de quoi je parle...

    La complexit\u00e9 en temps et en m\u00e9moire d'un algorithme est souvent exprim\u00e9e en utilisant la notation en O (Big O notation). Cette notation a \u00e9t\u00e9 introduite par le math\u00e9maticien et informaticien allemand Paul Bachmann en 1984 dans son ouvrage Analytische Zahlentheorie. Cependant, c'est le math\u00e9maticien austro-hongrois Edmund Landau qui a popularis\u00e9 cette notation dans le contexte de la th\u00e9orie des nombres.

    En substance, la complexit\u00e9 en temps d'un algorithme qui demanderait 10 \u00e9tapes pour \u00eatre r\u00e9solu s'\u00e9crirait\u2009:

    \\[ O(10) \\]

    Un algorithme qui ferait une recherche dichotomique sur un tableau de \\(n\\) \u00e9l\u00e9ments \u00e0 une complexit\u00e9 \\(O(log(n))\\). La recherche dichotomique c'est comme chercher un mot dans un dictionnaire. Vous ouvrez le dictionnaire \u00e0 la moiti\u00e9, si le mot est avant, vous r\u00e9p\u00e9tez l'op\u00e9ration sur la premi\u00e8re moiti\u00e9, sinon sur la seconde. Vous r\u00e9p\u00e9tez l'op\u00e9ration jusqu'\u00e0 trouver le mot. \u00c0 chaque \u00e9tape vous \u00e9liminez la moiti\u00e9 restante des mots du dictionnaire. Si le dictionnaire contient 100'000 mots, vous aurez trouv\u00e9 le mot en un nombre d'\u00e9tapes \u00e9quivalent \u00e0\u2009:

    \\[ \\log_2(100'000) = 16.6 \\]

    C'est bien plus rapide que de parcourir le dictionnaire de mani\u00e8re lin\u00e9aire, en tournant les pages une \u00e0 une.

    Prenons un algoritme qui prend un certain temps pour s'ex\u00e9cuter. L'algorithme \\(A\\) prend \\(f(n)\\) unit\u00e9s de temps pour une entr\u00e9e de taille \\(n\\). On peut dire que \\(A\\) est en \\(O(g(n))\\) si \\(f(n) \\leq c \\cdot g(n)\\) pour tout \\(n \\geq n_0\\), o\u00f9 \\(c\\) est une constante et \\(n_0\\) est un entier.

    On pourrait faire le raccourcis que la complexit\u00e9 algorithmique mesure le nombre d'op\u00e9rations \u00e9l\u00e9mentaires n\u00e9cessaires pour r\u00e9soudre un probl\u00e8me. Cepender, la complexit\u00e9 algorithmique ne mesure ni le temps en secondes ni le nombre d'op\u00e9rations \u00e9l\u00e9mentaires. Elle mesure la croissance du nombre d'op\u00e9rations \u00e9l\u00e9mentaires en fonction de la taille de l'entr\u00e9e. C'est tr\u00e8s diff\u00e9rent. Prenons l'exemple suivant\u2009:

    for (int i, i < n, i++) {\n    printf(\"%d \", a[i]);\n}\n\nfor (int i, i < m, i++) {\n    printf(\"%d \", b[i]);\n}\n

    On it\u00e8re sur deux tableaux a et b respectivement de taille n et m. La complexit\u00e9 de cet algorithme est \\(O(n + m)\\). Maintenant consid\u00e9rons l'exemple suivant\u2009:

    for (int i, i < n, i++) {\n    for (int j, j < m, j++) {\n        printf(\"%d \", a[i] + b[j]);\n    }\n}\n

    Cette fois-ci on observe une imbrication de deux boucles for. La complexit\u00e9 de cet algorithme est \\(O(n \\cdot m)\\). On peut dire que la complexit\u00e9 de cet algorithme est quadratique.

    Enfin, imaginons que nous devons appliquer 10 op\u00e9rations sur chaque \u00e9l\u00e9ment avant l'affichage\u2009:

    for (int i, i < n, i++) {\n    for (int j, j < m, j++) {\n        for (int k, k < 10, k++) {\n            printf(\"%d \", a[i] + b[j]);\n        }\n    }\n}\n

    Na\u00efvement on pourrait penser que la complexit\u00e9 de cet algorithme est \\(O(10 \\cdot n \\cdot m) = O(n \\cdot m)\\). Cependant, la complexit\u00e9 de cet algorithme est \\(O(10 \\cdot n \\cdot m) = O(n \\cdot m)\\). En effet, la constante 10 n'a pas d'impact sur la croissance de la complexit\u00e9 de l'algorithme. On peut dire que la complexit\u00e9 de cet algorithme est quadratique.

    ", "tags": ["for"]}, {"location": "course-c/40-algorithms/introduction/#suppression-des-constantes", "title": "Suppression des constantes", "text": "

    Dans la notation Big O, les constantes sont ignor\u00e9es. Par exemple, si un algorithme prend \\(5n^2 + 3n + 2\\) unit\u00e9s de temps pour une entr\u00e9e de taille \\(n\\), on dira que cet algorithme est en \\(O(n^2)\\). En effet, pour de grandes valeurs de \\(n\\), la croissance de \\(5n^2 + 3n + 2\\) est domin\u00e9e par \\(n^2\\).

    Suppression des constantes Complexit\u00e9 Complexit\u00e9 sans les constantes \\(O(5n^2)\\) \\(O(n^2)\\) \\(O(3n)\\) \\(O(n)\\) \\(O(2)\\) \\(O(1)\\) \\(O(n^2 + n)\\) \\(O(n^2)\\) \\(O(5n^2 + 3n + 2)\\) \\(O(n^2)\\) \\(O(n + \\log(n))\\) \\(O(n)\\)

    Cette r\u00e8gle est valable our l'addition, mais pour la multiplication, les constantes ne sont pas ignor\u00e9es. Dans le cas de \\(O(n\\cdot\\log(n))\\) la valeur de \\(\\log(n)\\) est importante.

    "}, {"location": "course-c/40-algorithms/introduction/#indicateurs-de-landau", "title": "Indicateurs de Landau", "text": "

    Il existe diff\u00e9rents indicateurs de Landau\u2009:

    Notation Big O (O)

    Utilis\u00e9e pour d\u00e9crire une borne sup\u00e9rieure asymptotique. Cela signifie qu'une fonction \\( f(n) \\) est en \\( O(g(n)) \\) s'il existe des constantes \\( c > 0 \\) et \\( n_0 \\) telles que \\( f(n) \\leq c \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une limite sup\u00e9rieure sur le comportement de \\( f(n) \\) pour de grandes valeurs de \\( n \\).

    Big O est souvent utilis\u00e9e pour d\u00e9crire le pire cas.

    Notation Big Omega (\u03a9)

    Utilis\u00e9e pour d\u00e9crire une borne inf\u00e9rieure asymptotique. Cela signifie qu'une fonction \\( f(n) \\) est en \\( \\Omega(g(n)) \\) s'il existe des constantes \\( c > 0 \\) et \\( n_0 \\) telles que \\( f(n) \\geq c \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une limite inf\u00e9rieure sur le comportement de \\( f(n) \\) pour de grandes valeurs de \\( n \\).

    Big Omega est souvent utilis\u00e9e pour d\u00e9crire le meilleur cas.

    Notation Big Theta (\u0398)

    Utilis\u00e9e pour d\u00e9crire une borne asymptotique stricte. Cela signifie qu'une fonction \\( f(n) \\) est en \\( \\Theta(g(n)) \\) s'il existe des constantes \\( c_1, c_2 > 0 \\) et \\( n_0 \\) telles que \\( c_1 \\cdot g(n) \\leq f(n) \\leq c_2 \\cdot g(n) \\) pour tout \\( n \\geq n_0 \\). En d'autres termes, \\( g(n) \\) est une approximation asymptotique exacte de \\( f(n) \\).

    Big Theta est utilis\u00e9e pour d\u00e9crire un comportement asymptotique pr\u00e9cis, souvent interpr\u00e9t\u00e9 comme le cas moyen.

    Exercice 1\u2009: Quelle Complexit\u00e9\u2009?

    Quelle est la complexit\u00e9 en temps de cet algorithme\u2009?

    void foo(int a[], int n) {\n    int sum = 0, product = 1;\n    for (int i = 0; i < n; i++) {\n        sum += a[i];\n    }\n    for (int i = 0; i < n; i++) {\n        product *= a[i];\n    }\n    printf(\"Sum: %d, Product: %d\\n\", sum, product);\n}\n
    • \\(O(2n)\\)
    • \\(O(n)\\)
    • \\(O(n^2)\\)
    • \\(O(n \\log(n))\\)

    Identifier les valeurs paires et impaires

    \u00c0 titre d'exemple, le programme suivant qui se charge de remplacer les valeurs paires d'un tableau par un \\(0\\) et les valeurs impaires par un \\(1\\) \u00e0 une complexit\u00e9 en temps de \\(O(n)\\) o\u00f9 n est le nombre d'\u00e9l\u00e9ments du tableau.

    void discriminate(int* array, size_t length)\n{\n    for (size_t i = 0; i < length; i++)\n    {\n        array[i] %= 2;\n    }\n}\n

    Somme des entiers

    Si on vous demande d'\u00e9crire un algorithme permettant de conna\u00eetre la somme des entiers de \\(1\\) \u00e0 \\(n\\), vous pourriez \u00e9crire un algorithme en \\(O(n)\\) :

    CPython
    int sum(int n) {\n    int sum = 0;\n    for (int i = 1; i <= n; i++) {\n        sum += i;\n    }\n    return sum;\n}\n
    def sum(n):\n    return [i for i in range(1, n + 1)]\n

    Ensuite vous vous posez la question de savoir si vous pouvez faire mieux. En effet, il existe une formule math\u00e9matique permettant de calculer la somme des entiers de \\(1\\) \u00e0 \\(n\\) :

    \\[ \\sum_{i=1}^{n} i = \\frac{n \\cdot (n + 1)}{2} \\]

    Cette formule est en \\(O(1)\\).

    CPython
    int sum(int n) {\n    return n * (n + 1) / 2;\n}\n
    def sum(n):\n    return n * (n + 1) // 2\n

    D'une mani\u00e8re g\u00e9n\u00e9rale, la plupart des algorithmes que l'ing\u00e9nieur \u00e9crira appartiendront \u00e0 ces cat\u00e9gories exprim\u00e9es du meilleur au plus mauvais\u2009:

    Temps pour diff\u00e9rentes complexit\u00e9s d'algorithmes Complexit\u00e9 \\(n = 100000\\) i7 (100'000 MIPS) \\(O(log(n))\\) 11 0.11 ns \\(O(n)\\) 100'000 1 us \\(O(n log(n))\\) 1'100'000 11 us \\(O(n^2)\\) 10'000'000'000 100 ms (un battement de cil) \\(O(2^n)\\) tr\u00e8s tr\u00e8s grand Le soleil devenu g\u00e9ante rouge aura ingurgit\u00e9 la terre $O(n\u2009!)` trop trop grand La galaxie ne sera plus que poussi\u00e8re

    Les diff\u00e9rentes complexit\u00e9s peuvent \u00eatre r\u00e9sum\u00e9es sur la figure suivante\u2009:

    Diff\u00e9rentes complexit\u00e9s d'algorithmes

    Un algorithme en \\(O(n^2)\\), doit \u00e9veiller chez le d\u00e9veloppeur la volont\u00e9 de voir s'il n'y a pas moyen d'optimiser l'algorithme en r\u00e9duisant sa complexit\u00e9, souvent on s'aper\u00e7oit qu'un algorithme peut \u00eatre optimis\u00e9 et s'int\u00e9resser \u00e0 sa complexit\u00e9 est un excellent point d'entr\u00e9e.

    Attention toutefois \u00e0 ne pas mal \u00e9valuer la complexit\u00e9 d'un algorithme. Voyons par exemple les deux algorithmes suivants\u2009:

    int min = MAX_INT;\nint max = MIN_INT;\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {\n    if (array[i] < min) {\n        min = array[i];\n    }\n    if (array[i] > min) {\n        max = array[i];\n    }\n}\n
    int min = MAX_INT;\nint max = MIN_INT;\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++)\n{\n    if (array[i] < min) {\n        min = array[i];\n    }\n}\n\nfor (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++)\n{\n    if (array[i] > min) {\n        max = array[i];\n    }\n}\n

    Exercice 2\u2009: Triangle \u00e9vanescent

    Quel serait l'algorithme permettant d'afficher\u2009:

    *****\n****\n***\n**\n*\n

    et dont la taille peut varier\u2009?

    Exercice 3\u2009: L'entier manquant

    On vous donne un gros fichier de 3'000'000'000 entiers positifs 32-bits, il vous faut g\u00e9n\u00e9rer un entier qui n'est pas dans la liste. Le hic, c'est que vous n'avez que 500 MiB de m\u00e9moire de travail. Quel algorithme proposez-vous\u2009?

    Une fois le travail termin\u00e9, votre manager vient vous voir pour vous annoncer que le cahier des charges a \u00e9t\u00e9 modifi\u00e9. Le client dit qu'il n'a que 10 MiB. Pensez-vous pouvoir r\u00e9soudre le probl\u00e8me quand m\u00eame\u2009?

    "}, {"location": "course-c/40-algorithms/introduction/#diagrammes-visuels", "title": "Diagrammes visuels", "text": "
    • Diagrammes en flux
    • Structogrammes
    • Diagramme d'activit\u00e9s
    • Machines d'\u00e9tats (UML state machine)
    • BPMN (Business Process Model and Notation)
    "}, {"location": "course-c/40-algorithms/introduction/#type-dalgorithmes", "title": "Type d'algorithmes", "text": ""}, {"location": "course-c/40-algorithms/introduction/#algorithmes-en-ligne-incremental", "title": "Algorithmes en ligne (incr\u00e9mental)", "text": "

    Un algorithme incr\u00e9mental ou online est un algorithme qui peut s'ex\u00e9cuter sur un flux de donn\u00e9es continu en entr\u00e9e. C'est-\u00e0-dire qu'il est en mesure de prendre des d\u00e9cisions sans avoir besoin d'une visibilit\u00e9 compl\u00e8te sur le set de donn\u00e9es.

    Un exemple typique est le probl\u00e8me de la secr\u00e9taire. On souhaite recruter une nouvelle secr\u00e9taire et le recruteur voit d\u00e9filer les candidats. Il doit d\u00e9cider \u00e0 chaque entretien s'il engage ou non le candidat et ne peut pas attendre la fin du processus d'entretiens pour obtenir le score attribu\u00e9 \u00e0 chaque candidat. Il ne peut comparer la performance de l'un qu'\u00e0 celle de deux d\u00e9j\u00e0 entrevus. L'objectif est de trouver la meilleure strat\u00e9gie.

    La solution \u00e0 ce probl\u00e8me est de laisser passer 37% des candidats sans les engager. Ceci correspond \u00e0 une proportion de \\(1/e\\). Ensuite il suffit d'attendre un ou une candidate meilleure que tous ceux/celles du premier \u00e9chantillon.

    "}, {"location": "course-c/40-algorithms/introduction/#methodes-de-resolution", "title": "M\u00e9thodes de r\u00e9solution", "text": "

    Il existe deux m\u00e9thodes quasiment infaillibles pour r\u00e9soudre un probl\u00e8me complexe\u2009:

    1. La r\u00e9duction du probl\u00e8me en sous-probl\u00e8mes plus simples.
    2. Le raisonnement par l'inverse.

    La r\u00e9diction aussi appel\u00e9e Divide and Conquer consiste \u00e0 diviser un probl\u00e8me en sous-probl\u00e8mes plus simples, les r\u00e9soudre et combiner les solutions pour obtenir la solution du probl\u00e8me initial. C'est une m\u00e9thode tr\u00e8s utilis\u00e9e en informatique pour r\u00e9soudre des probl\u00e8mes complexes. Par exemple, le tri fusion, le tri rapide, la recherche dichotomique sont des m\u00e9thodes de r\u00e9solution de probl\u00e8mes bas\u00e9es sur la r\u00e9duction.

    Le raisonnement par l'inverse consiste \u00e0 partir de la solution pour remonter au probl\u00e8me en posant des hypoth\u00e8ses. Par exemple, si vous avez un probl\u00e8me de recherche de chemin, vous pouvez partir de la destination pour remonter au point de d\u00e9part. C'est une m\u00e9thode tr\u00e8s utilis\u00e9e en math\u00e9matiques pour r\u00e9soudre des probl\u00e8mes complexes. Par exemple, la m\u00e9thode de Newton pour trouver les racines d'une fonction est bas\u00e9e sur le raisonnement par l'inverse.

    "}, {"location": "course-c/40-algorithms/introduction/#les-problemes-np-et-np-complet", "title": "Les Probl\u00e8mes NP et NP-Complet", "text": "

    La th\u00e9orie de la complexit\u00e9 computationnelle est une branche fascinante de l'informatique th\u00e9orique qui s'int\u00e9resse \u00e0 la classification des probl\u00e8mes en fonction de la difficult\u00e9 \u00e0 les r\u00e9soudre. Au c\u0153ur de cette th\u00e9orie se trouvent les classes de probl\u00e8mes NP et NP-complet, concepts essentiels pour comprendre pourquoi certains probl\u00e8mes sont si difficiles \u00e0 r\u00e9soudre. Pour les n\u00e9ophytes, ces termes peuvent sembler abstraits, mais leur compr\u00e9hension r\u00e9v\u00e8le des enjeux fondamentaux pour la science et l'ing\u00e9nierie modernes.

    "}, {"location": "course-c/40-algorithms/introduction/#quest-ce-quun-probleme-np", "title": "Qu'est-ce qu'un probl\u00e8me NP\u2009?", "text": "

    Pour saisir ce qu'est un probl\u00e8me NP, il faut d'abord comprendre la notion de temps de calcul. Le temps de calcul d'un algorithme correspond au nombre d'\u00e9tapes n\u00e9cessaires pour r\u00e9soudre un probl\u00e8me, en fonction de la taille de l'entr\u00e9e. Par exemple, trier une liste de mille \u00e9l\u00e9ments prend g\u00e9n\u00e9ralement plus de temps que trier une liste de dix \u00e9l\u00e9ments.

    Un probl\u00e8me est dit \u00ab\u2009en P\u2009\u00bb (pour \u00ab\u2009Polynomial\u2009\u00bb) si une solution peut \u00eatre trouv\u00e9e par un algorithme en un temps raisonnable, c'est-\u00e0-dire en un temps qui cro\u00eet de mani\u00e8re polynomiale avec la taille de l'entr\u00e9e. Par exemple, l'algorithme qui permet de trier une liste en utilisant un tri par insertion appartient \u00e0 la classe P car son temps de calcul est proportionnel au carr\u00e9 de la taille de la liste.

    Cependant, certains probl\u00e8mes semblent beaucoup plus complexes \u00e0 r\u00e9soudre. Un probl\u00e8me est dit \u00ab\u2009en NP\u2009\u00bb (pour \u00ab\u2009Nondeterministic Polynomial time\u2009\u00bb) si, bien qu'il puisse \u00eatre difficile de trouver une solution, il est relativement facile de v\u00e9rifier la validit\u00e9 d'une solution donn\u00e9e. Autrement dit, si on vous fournit une solution suppos\u00e9e correcte \u00e0 un probl\u00e8me en NP, vous pouvez v\u00e9rifier cette solution en temps polynomial. Le terme \u00ab\u2009nondeterministic\u2009\u00bb fait r\u00e9f\u00e9rence \u00e0 l'id\u00e9e th\u00e9orique d'une machine qui pourrait essayer simultan\u00e9ment toutes les solutions possibles et choisir la bonne.

    Un exemple classique de probl\u00e8me en NP est le probl\u00e8me du Knapsack (ou probl\u00e8me du sac \u00e0 dos). Ce probl\u00e8me consiste \u00e0 d\u00e9terminer quels objets, parmi une collection donn\u00e9e, doivent \u00eatre plac\u00e9s dans un sac \u00e0 dos de capacit\u00e9 limit\u00e9e de mani\u00e8re \u00e0 maximiser la valeur totale des objets. Trouver la meilleure combinaison d'objets \u00e0 mettre dans le sac peut \u00eatre tr\u00e8s difficile car le nombre de combinaisons possibles cro\u00eet de mani\u00e8re exponentielle avec le nombre d'objets. En revanche, si quelqu'un vous donne une combinaison pr\u00e9tendument optimale, vous pouvez rapidement v\u00e9rifier si elle respecte la capacit\u00e9 du sac et si sa valeur est maximale.

    "}, {"location": "course-c/40-algorithms/introduction/#les-problemes-np-complet", "title": "Les Probl\u00e8mes NP-Complet", "text": "

    Parmi les probl\u00e8mes en NP, certains sont particuli\u00e8rement redoutables\u2009: ce sont les probl\u00e8mes NP-complet. Un probl\u00e8me est dit NP-complet s'il est \u00e0 la fois en NP et au moins aussi difficile que tous les autres probl\u00e8mes en NP. En d'autres termes, si vous pouviez trouver une m\u00e9thode efficace pour r\u00e9soudre un probl\u00e8me NP-complet, alors vous pourriez utiliser cette m\u00e9thode pour r\u00e9soudre tous les autres probl\u00e8mes en NP.

    Le probl\u00e8me du Knapsack que nous avons mentionn\u00e9 plus t\u00f4t est un exemple de probl\u00e8me NP-complet. D'autres exemples bien connus incluent le probl\u00e8me du voyageur de commerce (TSP), o\u00f9 il s'agit de trouver le chemin le plus court pour visiter un ensemble de villes une fois et revenir au point de d\u00e9part, ou encore le probl\u00e8me de la coloration de graphe, qui consiste \u00e0 d\u00e9terminer le nombre minimum de couleurs n\u00e9cessaires pour colorier les sommets d'un graphe de mani\u00e8re que deux sommets adjacents n'aient pas la m\u00eame couleur.

    "}, {"location": "course-c/40-algorithms/introduction/#le-probleme-p-np", "title": "Le Probl\u00e8me P = NP", "text": "

    Le probl\u00e8me mill\u00e9naire P = NP, formul\u00e9 par Stephen Cook en 1971, est l'une des questions les plus c\u00e9l\u00e8bres et les plus importantes de la science informatique. Il se demande si tous les probl\u00e8mes qui peuvent \u00eatre v\u00e9rifi\u00e9s rapidement (c'est-\u00e0-dire en temps polynomial) peuvent \u00e9galement \u00eatre r\u00e9solus rapidement. En d'autres termes, P est-il \u00e9gal \u00e0 NP\u2009?

    Si P = NP, cela signifierait qu'il existe un algorithme rapide pour r\u00e9soudre chaque probl\u00e8me dont la solution peut \u00eatre rapidement v\u00e9rifi\u00e9e. Cela bouleverserait notre compr\u00e9hension de la complexit\u00e9 computationnelle et aurait des implications \u00e9normes dans de nombreux domaines, de la cryptographie \u00e0 la logistique. \u00c0 ce jour, personne n'a r\u00e9ussi \u00e0 prouver ou \u00e0 r\u00e9futer cette conjecture, et elle reste l'un des grands myst\u00e8res non r\u00e9solus de la science.

    Les probl\u00e8mes NP-complet sont essentiels car ils nous montrent les limites de ce que nous pouvons r\u00e9soudre efficacement avec des ordinateurs. Ils nous forcent \u00e0 accepter qu'il existe des probl\u00e8mes pour lesquels, en l'\u00e9tat actuel de nos connaissances, il n'existe pas de solution rapide, ce qui a des cons\u00e9quences pratiques. Par exemple, en cryptographie, la s\u00e9curit\u00e9 des syst\u00e8mes repose souvent sur l'hypoth\u00e8se que certains probl\u00e8mes sont difficiles \u00e0 r\u00e9soudre, ce qui les rend r\u00e9sistants aux attaques.

    En outre, comprendre ces probl\u00e8mes nous pousse \u00e0 d\u00e9velopper de nouvelles techniques pour les r\u00e9soudre ou les contourner. Parfois, cela signifie trouver des algorithmes approximatifs, qui fournissent des solutions proches de l'optimum en un temps raisonnable. D'autres fois, cela peut conduire \u00e0 l'innovation en mati\u00e8re de mat\u00e9riel informatique, comme l'exploration des ordinateurs quantiques, qui pourraient potentiellement r\u00e9soudre certains de ces probl\u00e8mes beaucoup plus rapidement que les ordinateurs classiques.

    "}, {"location": "course-c/40-algorithms/introduction/#conclusion", "title": "Conclusion", "text": "

    La classification des probl\u00e8mes en P, NP, et NP-complet est une pierre angulaire de l'informatique th\u00e9orique. Les probl\u00e8mes NP-complet, en particulier, repr\u00e9sentent certains des d\u00e9fis les plus redoutables auxquels nous sommes confront\u00e9s dans le domaine. Ils ne sont pas seulement des \u00e9nigmes abstraites pour les th\u00e9oriciens\u2009; ils ont des implications profondes et tr\u00e8s concr\u00e8tes pour la science, la technologie, et m\u00eame notre vie quotidienne. Comprendre ces concepts, c'est plonger au c\u0153ur de ce que signifie la complexit\u00e9 et la difficult\u00e9 dans le monde des algorithmes, et reconna\u00eetre les limites actuelles de ce que nous pouvons accomplir avec des machines de calcul.

    "}, {"location": "course-c/40-algorithms/introduction/#exercices-de-revision", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 4\u2009: Int\u00e9grateur de Kahan

    L'int\u00e9grateur de Kahan (Kahan summation algorithm) est une solution \u00e9l\u00e9gante pour pallier \u00e0 la limite de r\u00e9solution des types de donn\u00e9es.

    L'algorithme pseudo-code peut \u00eatre exprim\u00e9 comme\u2009:

    function kahan_sum(input)\n    var sum = 0.0\n    var c = 0.0\n    for i = 1 to input.length do\n        var y = input[i] - c\n        var t = sum + y\n        c = (t - sum) - y\n        sum = t\n    next i\n    return sum\n
    1. Impl\u00e9menter cet algorithme en C compte tenu du prototype\u2009:

      float kahan_sum(float value, float sum, float c);\n
    2. Expliquer comment fonctionne cet algorithme.

    3. Donner un exemple montrant l'avantage de cet algorithme sur une simple somme.

    Exercice 5\u2009: Robot aspirateur affam\u00e9

    Un robot aspirateur souhaite se rassasier et cherche le frigo, le probl\u00e8me c'est qu'il ne sait pas o\u00f9 il est. Elle serait la strat\u00e9gie de recherche du robot pour se rendre \u00e0 la cuisine\u2009?

    Le robot dispose de plusieurs fonctionnalit\u00e9s\u2009:

    • Avancer
    • Tourner \u00e0 droite de 90\u00b0
    • D\u00e9tection de sa position absolue p. ex. P5

    \u00c9laborer un algorithme de recherche.

        \u2502 A \u2502 B \u2502 C \u2502 D \u2502 E \u2502 F \u2502 G \u2502 H \u2502 I \u2502 J \u2502 K \u2502 L \u2502 M \u2502 O \u2502 P \u2502 Q \u2502\n\u2500\u2500\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n1 \u2503                     x \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503             F1: Frigo \u2503       \u2503               \u2503               \u2503\n2 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n3 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n4 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n5 \u2503       \u2503               \u2503       \u2503               \u2503      <--o     \u2503\n\u2500\u2500\u2503       \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501   \u2501\u2501\u2501\u2501\u2501\u252b       \u2503               \u2503     P5: Robot \u2503\n6 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n7 \u2503                       \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2503                       \u2503       \u2503               \u2503               \u2503\n8 \u2503       \u2503               \u2503       \u2503               \u2503               \u2503\n\u2500\u2500\u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501    \u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b   \u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b\n9 \u2503                                                       \u2503       \u2503\n\u2500\u2500\u2503                                                       \u2503       \u2503\n10\u2503                                                               \u2503\n\u2500\u2500\u2503                                                               \u2503\n11\u2503                                                       \u2503       \u2503\n\u2500\u2500\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
    "}, {"location": "course-c/40-algorithms/recursion/", "title": "R\u00e9cursivit\u00e9", "text": "To understand what recursion is, you must first understand recursion.Internet

    La r\u00e9cursivit\u00e9 est une technique de programmation dans laquelle une fonction s'appelle elle-m\u00eame pour r\u00e9soudre un probl\u00e8me. Cela signifie que la fonction r\u00e9sout une partie du probl\u00e8me et appelle ensuite la fonction elle-m\u00eame pour r\u00e9soudre le reste du probl\u00e8me.

    La r\u00e9cursivit\u00e9 est utilis\u00e9e pour r\u00e9soudre des probl\u00e8mes qui peuvent \u00eatre d\u00e9compos\u00e9s en probl\u00e8mes plus petits de la m\u00eame nature. Par exemple, la factorielle d'un nombre est le produit de tous les entiers positifs inf\u00e9rieurs ou \u00e9gaux \u00e0 ce nombre. La factorielle d'un nombre n est n! = n * (n-1)!.

    Au chapitre sur les fonctions, nous avions donn\u00e9 l'exemple du calcul de la somme de la suite de Fibonacci jusqu'\u00e0 n :

    int fib(int n)\n{\n    int sum = 0;\n    int t1 = 0, t2 = 1;\n    int next_term;\n    for (int i = 1; i <= n; i++)\n    {\n        sum += t1;\n        next_term = t1 + t2;\n        t1 = t2;\n        t2 = next_term;\n    }\n    return sum;\n}\n

    Il peut sembler plus logique de raisonner de fa\u00e7on r\u00e9cursive. Quelle que soit l'it\u00e9ration \u00e0 laquelle l'on soit, l'assertion suivante est valable\u2009:

    \\[fib(n) == fib(n - 1) + fib(n - 2)\\]

    Donc pourquoi ne pas r\u00e9\u00e9crire cette fonction en employant ce caract\u00e8re r\u00e9cursif\u2009?

    int fib(int n)\n{\n    if (n < 2) return 1;\n    return fib(n - 1) + fib(n - 2);\n}\n

    Le code est beaucoup plus simple \u00e0 \u00e9crire, et m\u00eame \u00e0 lire. N\u00e9anmoins cet algorithme est notoirement connu pour \u00eatre tr\u00e8s mauvais en termes de performance. Calculer fib(5) revient \u00e0 la cha\u00eene d'appel suivant.

    Cette cha\u00eene d'appel repr\u00e9sente le nombre de fois que fib est appel\u00e9 et \u00e0 quel niveau elle est appel\u00e9e. Par exemple fib(4) est appel\u00e9 dans fib(5) :

    %% Arbre d'appel de Fibonacci\ngraph TD\n\n5((\"fib(5)\")) --> 41((\"fib(4)\"))\n5 --> 31((\"fib(3)\"))\n\n41((\"fib(4)\")) --> 32((\"fib(3)\"))\n41 --> 21((\"fib(2)\"))\n\n21 --> 11((\"fib(1)\"))\n\n31 --> 22((\"fib(2)\"))\n31 --> 12((\"fib(1)\"))\n\n22 --> 13((\"fib(1)\"))\n32((\"fib(3)\")) --> 23((\"fib(2)\"))\n32 --> 14((\"fib(1)\"))\n\n23 --> 15((\"fib(1)\"))
    Arbre d'appel de Fibonacci

    Au final, fib(1) est appel\u00e9 5 fois, fib(2) 3 fois, fib(3) 2 fois, fib(4) et fib(5) 1 fois. Ce sont donc 12 appels \u00e0 la fonction fib pour calculer fib(5).

    Calcul Appels fib(1) 1 fib(2) 2 fib(3) 4 fib(4) 7 fib(5) 12 fib(6) 20 fib(7) 33 fib(8) 54 fib(9) 88 fib(10) 143 ... ... fib(30) 2'178'308 fib(40) 267'914'295 fib(50) 32'951'280'098 fib(100) 927'372'692'193'078'999'175

    Il s'agit de la suite A000071 de l'OEIS. On constate que le nombre d'appels est exponentiel. Pour fib(100) il faudra neuf cent vingt-sept quintillions trois cent soixante-douze quadrillions six cent quatre-vingt-douze trillions cent quatre-vingt-treize milliards soixante-dix-huit millions neuf cent quatre-vingt-dix-neuf mille cent soixante-quinze appels \u00e0 la fonction fib. Pour un processeur capable de calculer 100 GFLOPS (milliards d'op\u00e9rations par seconde), il faudra tout de m\u00eame 294 ans. C'est un peu long...

    La complexit\u00e9 algorithmique de cette fonction est dite \\(O(2^n)\\). C'est-\u00e0-dire que le nombre d'appels suit une relation exponentielle. La r\u00e9elle complexit\u00e9 est donn\u00e9e par la relation\u2009:

    En revanche, dans l'approche it\u00e9rative, on constate qu'une seule boucle for. C'est-\u00e0-dire qu'il faudra seulement 100 it\u00e9rations pour calculer la somme.

    G\u00e9n\u00e9ralement les algorithmes r\u00e9cursifs (s'appelant eux-m\u00eames) sont moins performants que les algorithmes it\u00e9ratifs (utilisant des boucles). N\u00e9anmoins il est parfois plus facile d'\u00e9crire un algorithme r\u00e9cursif.

    Notons que tout algorithme r\u00e9cursif peut \u00eatre \u00e9crit en un algorithme it\u00e9ratif, mais ce n'est pas toujours facile.

    ", "tags": ["for", "fib"]}, {"location": "course-c/40-algorithms/recursion/#les-tours-de-hanoi", "title": "Les tours de Hano\u00ef", "text": "

    Les tours de Hano\u00ef est un jeu de r\u00e9flexion invent\u00e9 par le math\u00e9maticien fran\u00e7ais \u00c9douard Lucas en 1889 et publi\u00e9 dans le tome 3 de ses R\u00e9cr\u00e9ations math\u00e9matiques. Le jeu est compos\u00e9 de trois tiges et d'un certain nombre de disques de diam\u00e8tres diff\u00e9rents qui peuvent \u00eatre empil\u00e9s sur une tige. Le but du jeu est de d\u00e9placer tous les disques d'une tige \u00e0 une autre, en respectant les r\u00e8gles suivantes\u2009:

    1. On ne peut d\u00e9placer qu'un seul disque \u00e0 la fois.
    2. Un disque ne peut \u00eatre plac\u00e9 que sur un disque plus grand que lui ou sur une tige vide.

    Tours de Hano\u00ef

    Ce probl\u00e8me se pr\u00eate tr\u00e8s bien \u00e0 une r\u00e9solution r\u00e9cursive. En effet, pour d\u00e9placer n disques de la tige A \u00e0 la tige C, il suffit de d\u00e9placer n-1 disques de la tige A \u00e0 la tige B, puis de d\u00e9placer le disque restant de la tige A \u00e0 la tige C, et enfin de d\u00e9placer les n-1 disques de la tige B \u00e0 la tige C.

    Algorithme R\u00e9cursifAlgorithme It\u00e9ratif
    #include <stdio.h>\n\nvoid hanoi(int n, char from, char to, char aux) {\n    if (n == 1) {\n        printf(\"D\u00e9placer le disque 1 de %c \u00e0 %c\\n\", from, to);\n        return;\n    }\n    hanoi(n - 1, from, aux, to);\n    printf(\"D\u00e9placer le disque %d de %c \u00e0 %c\\n\", n, from, to);\n    hanoi(n - 1, aux, to, from);\n}\n\nint main() {\n    int n = 3;\n    hanoi(n, 'A', 'C', 'B');\n}\n
    #include <stdio.h>\n#include <stdlib.h>\n\ntypedef struct {\n    int n;\n    char from;\n    char to;\n    char aux;\n    int stage;\n} Frame;\n\ntypedef struct {\n    Frame *frames;\n    int top;\n    int max_size;\n} Stack;\n\nvoid init_stack(Stack *stack, int max_size) {\n    stack->frames = (Frame *)malloc(max_size * sizeof(Frame));\n    stack->top = -1;\n    stack->max_size = max_size;\n}\n\nvoid push(Stack *stack, Frame frame) {\n    if (stack->top < stack->max_size - 1) {\n        stack->frames[++stack->top] = frame;\n    }\n}\n\nFrame pop(Stack *stack) {\n    if (stack->top >= 0) {\n        return stack->frames[stack->top--];\n    } else {\n        Frame empty = {0, '\\0', '\\0', '\\0', 0};\n        return empty;\n    }\n}\n\nint is_empty(Stack *stack) {\n    return stack->top == -1;\n}\n\nvoid hanoi_iterative(int n, char from, char to, char aux) {\n    Stack stack;\n    init_stack(&stack, 100);  // Assuming the stack size to be 100, adjust if needed\n\n    Frame initial_frame = {n, from, to, aux, 0};\n    push(&stack, initial_frame);\n\n    while (!is_empty(&stack)) {\n        Frame current_frame = pop(&stack);\n\n        switch (current_frame.stage) {\n            case 0:\n                if (current_frame.n == 1) {\n                    printf(\"D\u00e9placer le disque 1 de %c \u00e0 %c\\n\", current_frame.from, current_frame.to);\n                } else {\n                    current_frame.stage = 1;\n                    push(&stack, current_frame);\n\n                    Frame new_frame = {current_frame.n - 1, current_frame.from, current_frame.aux, current_frame.to, 0};\n                    push(&stack, new_frame);\n                }\n                break;\n\n            case 1:\n                printf(\"D\u00e9placer le disque %d de %c \u00e0 %c\\n\", current_frame.n, current_frame.from, current_frame.to);\n\n                current_frame.stage = 2;\n                push(&stack, current_frame);\n\n                Frame new_frame = {current_frame.n - 1, current_frame.aux, current_frame.to, current_frame.from, 0};\n                push(&stack, new_frame);\n                break;\n        }\n    }\n    free(stack.frames);\n}\n\nint main() {\n    int n = 3;\n    hanoi_iterative(n, 'A', 'C', 'B');\n}\n

    Ce qui donne le r\u00e9sultat suivant\u2009:

    D\u00e9placer le disque 1 de A \u00e0 C\nD\u00e9placer le disque 2 de A \u00e0 B\nD\u00e9placer le disque 1 de C \u00e0 B\nD\u00e9placer le disque 3 de A \u00e0 C\nD\u00e9placer le disque 1 de B \u00e0 A\nD\u00e9placer le disque 2 de B \u00e0 C\nD\u00e9placer le disque 1 de A \u00e0 C\n

    On voit que l'impl\u00e9mentation it\u00e9rative est bien plus complexe que l'impl\u00e9mentation r\u00e9cursive. C'est pourquoi il est souvent plus simple d'\u00e9crire un algorithme r\u00e9cursif, mais pas n\u00e9cessairement plus performant.

    "}, {"location": "course-c/40-algorithms/recursion/#utilisation-du-stack", "title": "Utilisation du stack", "text": "

    En C, la r\u00e9cursivit\u00e9 est g\u00e9r\u00e9e par le stack. Chaque appel de fonction est empil\u00e9 sur le stack. Lorsque la fonction retourne, elle est d\u00e9pil\u00e9e du stack. Il est important de noter que le stack a une taille limit\u00e9e. Par d\u00e9faut, sous Linux la taille du stack est de 8 Mio (donn\u00e9 par la commande ulimit -s), sous Windows c'est 1 Mio. Si la r\u00e9cursivit\u00e9 est trop profonde, il y a un risque de stack overflow.

    D'autre part, une fonction r\u00e9cursive qui utilise beaucoup de variables locales et beaucoup de param\u00e8tres seront tous empil\u00e9s sur le stack. Cela peut rapidement saturer la m\u00e9moire.

    Prenons l'exemple suivant d'une fonction r\u00e9cursive qui d\u00e9clare un tableau de 1Mio de caract\u00e8res\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint recurse(int n, int stack_size)\n{\n    char array[1024*1024] = {0};\n    printf(\"used stack: %d kiB\\n\", stack_size / 1024);\n    if (n == 0) return 0;\n    return recurse(n - 1, stack_size + sizeof(array));\n}\n\nint main(int argc, char* argv[])\n{\n    recurse(atoi(argv[1]), 0);\n}\n

    \u00c0 l'ex\u00e9cution, on obtient\u2009:

    used stack: 0 kiB\nused stack: 1024 kiB\nused stack: 2048 kiB\nused stack: 3072 kiB\nused stack: 4096 kiB\nused stack: 5120 kiB\nused stack: 6144 kiB\nSegmentation fault (stack overflow)\n

    Avertissement

    Avant d'impl\u00e9menter une fonction r\u00e9cursive, il est important de v\u00e9rifier que la profondeur de la r\u00e9cursivit\u00e9 ne d\u00e9passe pas la taille du stack.

    Limitez l'utilisation du stack en utilisant soit des variables globales, soit des variables statiques, soit des allocations dynamiques.

    "}, {"location": "course-c/40-algorithms/recursion/#memoisation", "title": "M\u00e9mo\u00efsation", "text": "

    En informatique la m\u00e9mo\u00efsation est une technique d'optimisation du code souvent utilis\u00e9e conjointement avec des algorithmes r\u00e9cursifs. Cette technique est largement utilis\u00e9e en programmation dynamique.

    Nous l'avons vu pr\u00e9c\u00e9demment, l'algorithme r\u00e9cursif du calcul de la somme de la suite de Fibonacci n'est pas efficace du fait que les m\u00eames appels sont r\u00e9p\u00e9t\u00e9s un nombre inutile de fois. La parade est de m\u00e9moriser pour chaque appel de fib, la sortie correspondante \u00e0 l'entr\u00e9e.

    Dans cet exemple nous utiliserons un m\u00e9canisme compos\u00e9 de trois fonctions\u2009:

    • int memoize(Cache *cache, int input, int output)
    • bool memoize_has(Cache *cache, int input)
    • int memoize_get(Cache *cache, int input)

    La premi\u00e8re fonction m\u00e9morise la valeur de sortie output li\u00e9e \u00e0 la valeur d'entr\u00e9e input. Pour des raisons de simplicit\u00e9 d'utilisation, la fonction retourne la valeur de sortie output.

    La seconde fonction memoize_has v\u00e9rifie si une valeur de correspondance existe pour l'entr\u00e9e input. Elle retourne true en cas de correspondance et false sinon.

    La troisi\u00e8me fonction memoize_get retourne la valeur de sortie correspondante \u00e0 la valeur d'entr\u00e9e input.

    Notre fonction r\u00e9cursive sera ainsi modifi\u00e9e comme suit\u2009:

    int fib(int n)\n{\n    if (memoize_has(n)) return memoize_get(n);\n    if (n < 2) return 1;\n    return memoize(n, fib(n - 1) + fib(n - 2));\n}\n

    Quant aux trois fonctions utilitaires, voici une proposition d'impl\u00e9mentation. Notons que cette impl\u00e9mentation est tr\u00e8s \u00e9l\u00e9mentaire et n'est valable que pour des entr\u00e9es inf\u00e9rieures \u00e0 1000. Il sera possible ult\u00e9rieurement de perfectionner ces fonctions, mais nous aurons pour cela besoin de concepts qui n'ont pas encore \u00e9t\u00e9 abord\u00e9s, tels que les structures de donn\u00e9es complexes.

    #define SIZE 1000\n\nbool cache_input[SIZE] = { false };\nint cache_output[SIZE];\n\nint memoize(int input, int output) {\n    cache_input[input % SIZE] = true;\n    cache_output[input % SIZE] = output;\n    return output;\n}\n\nbool memoize_has(int input) {\n    return cache_input[input % SIZE];\n}\n\nint memoize_get(int input) {\n    return cache_output[input % SIZE];\n}\n

    Exercice 1\u2009: La plus petite diff\u00e9rence

    Soit deux tableaux d'entiers, trouver la paire de valeurs (une dans chaque tableau) ayant la plus petite diff\u00e9rence (positive).

    Exemple\u2009:

    int a[] = {5, 3, 14, 11, 2};\nint b[] = {24, 128, 236, 20, 8};\n\nint diff = 3 // pair 11, 8\n
    1. Proposer une impl\u00e9mentation
    2. Quelle est la complexit\u00e9 de votre algorithme\u2009?
    ", "tags": ["output", "fib", "memoize_has", "true", "memoize_get", "input", "false"]}, {"location": "course-c/40-algorithms/recursion/#programmation-dynamique", "title": "Programmation dynamique", "text": "

    La programmation dynamique est une m\u00e9thode algorithmique datant des ann\u00e9es 1950, mais devenue populaire ces derni\u00e8res ann\u00e9es. Elle permet de coupler des algorithmes r\u00e9cursifs avec le concept de m\u00e9mo\u00efsation.

    Prenons par exemple l'algorithme de Fibonacci r\u00e9cursif\u2009:

    int fibonacci(int n) {\n    if (n <= 1) return n;\n    return fibonacci(n - 1) + fibonacci(n - 2);\n}\n

    Le probl\u00e8me de cet algorithme est sa performance. Appeler fibonacci(50) demandera de calculer fibonacci(49) et fibonacci(48) mais pour calculer fibonacci(49) il faudra recalculer fibonacci(48). On voit qu'on effectue du travail \u00e0 double. En r\u00e9alit\u00e9 c'est bien pire que \u00e7a. La complexit\u00e9 est de \\(O(2^n)\\). Donc pour calculer la valeur 50 il faudra effectuer \\(1 125 899 906 842 624\\) op\u00e9rations. Avec un ordinateur capable de calculer 1 milliard d'op\u00e9rations par seconde, il faudra tout de m\u00eame plus d'un million de secondes. Cet algorithme est donc tr\u00e8s mauvais\u2009!

    En revanche, si l'on est capable de m\u00e9moriser dans une table les r\u00e9sultats pr\u00e9c\u00e9dents des appels de Fibonacci, les performances seront bien meilleures.

    Voici l'algorithme modifi\u00e9\u2009:

    int fibonacci(int n) {\n    static int memo[1000] = {0};\n    if (memo[n]) return memo[n];\n    if (n <= 1) return n;\n    return memo[n] = fibonacci(n - 1) + fibonacci(n - 2);\n}\n

    Sa complexit\u00e9 est ainsi r\u00e9duite \u00e0 \\(O(2\\cdot n)\\) et donc \\(O(n)\\). En revanche, l'approche dynamique demande un espace m\u00e9moire suppl\u00e9mentaire. On n'a rien sans rien et l'\u00e9ternel dilemme m\u00e9moire versus performance s'applique toujours.

    "}, {"location": "course-c/40-algorithms/recursion/#backtracking", "title": "Backtracking", "text": "

    Le backtracking est une technique algorithmique qui consiste \u00e0 explorer toutes les solutions possibles d'un probl\u00e8me en testant chaque solution partielle. Lorsqu'une solution partielle ne peut pas \u00eatre compl\u00e9t\u00e9e pour former une solution valide, on revient en arri\u00e8re (backtrack) pour explorer une autre branche de l'arbre de recherche. Cette approche est souvent utilis\u00e9e pour rechercher une solution optimale \u00e0 un probl\u00e8me combinatoire.

    Par exemple, le probl\u00e8me des huit dames consiste \u00e0 placer huit dames sur un \u00e9chiquier de mani\u00e8re \u00e0 ce qu'aucune dame ne puisse attaquer une autre dame. Le backtracking est une m\u00e9thode efficace pour r\u00e9soudre ce type de probl\u00e8me.

    "}, {"location": "course-c/40-algorithms/recursion/#les-huit-dames", "title": "Les huit dames", "text": "

    Le probl\u00e8me des huit dames est un probl\u00e8me classique de placement de huit dames sur un \u00e9chiquier de 8x8 cases de mani\u00e8re \u00e0 ce qu'aucune dame ne puisse attaquer une autre dame. Une dame peut attaquer une autre dame si elles se trouvent sur la m\u00eame ligne, la m\u00eame colonne ou la m\u00eame diagonale.

    Les huit dames

    La solution na\u00efve est de tester toutes les combinaisons possibles de placement des huit dames et de v\u00e9rifier si elles sont valides. Cependant, cette approche est inefficace car le nombre de combinaisons possibles est tr\u00e8s \u00e9lev\u00e9. Si nous consid\u00e9rons simplement toutes les mani\u00e8res de placer 8 dames sur un \u00e9chiquier 8x8 sans tenir compte des contraintes d'attaque (c'est-\u00e0-dire sans tenir compte des lignes, colonnes ou diagonales), le nombre de configurations possibles est donn\u00e9 par le nombre de combinaisons de 64 cases (l'\u00e9chiquier) prises 8 \u00e0 la fois soit\u2009:

    \\[\\binom{64}{8} = \\frac{64\u2009!}{8\u2009!(64-8)!} = 4'426'165'368\\]

    N\u00e9anmoins ce probl\u00e8me qui est connu admet 92 solutions. C'est un probl\u00e8me de recherche exhaustive. On peut le r\u00e9soudre en utilisant une approche de backtracking.

    #include <stdbool.h>\n#include <stdio.h>\n\n#define N 8\n\n#define EMPTY false\n#define QUEEN (!EMPTY)\n\nvoid printSolution(bool board[N][N]) {\n   static int k = 0;\n   printf(\"Solution %d:\\n\", ++k);\n   for (int i = 0; i < N; ++i) {\n      for (int j = 0; j < N; ++j) printf(\"%s \", board[i][j] ? \"\u265b\" : \".\");\n      printf(\"\\n\");\n   }\n   printf(\"\\n\");\n}\n\nbool is_safe(bool board[N][N], int row, int col) {\n   // Column check\n   // Line check can be omitted because we are filling\n   // the board row by row\n   for (int i = 0; i < row; i++)\n      if (board[i][col] || board[row][i]) return false;\n\n   // Diagonal upper left\n   for (int i = row, j = col; i >= 0 && j >= 0; --i, --j)\n      if (board[i][j]) return false;\n\n   // Diagonal upper right\n   for (int i = row, j = col; i >= 0 && j < N; --i, ++j)\n      if (board[i][j]) return false;\n\n   return true;\n}\n\nbool solve(bool board[N][N], int row) {\n   // We have reached the end, this is a solution\n   if (row >= N) {\n      printSolution(board);\n      return true;\n   }\n\n   bool is_solution = false;\n   for (int col = 0; col < N; col++) {\n      if (is_safe(board, row, col)) {\n         board[row][col] = QUEEN;\n         is_solution = solve(board, row + 1) || is_solution;\n         board[row][col] = EMPTY;  // BACKTRACK\n      }\n   }\n\n   return is_solution;\n}\n\nint main() {\n   // False means empty, true means queen\n   bool board[N][N] = {0};\n\n   if (!solve(board, 0)) printf(\"Solution does not exist\");\n}\n

    On commence par d\u00e9finir un \u00e9chiquier de NxN cases sous la forme d'une tableau bidimensionnel bool board[N][N]. Chaque case de l'\u00e9chiquier peut contenir une dame (true) ou \u00eatre vide (false).

    La fonction is_safe prend en param\u00e8tre l'\u00e9chiquier actuel et les coordonn\u00e9es (row, col) d'une nouvelle dame \u00e0 placer. Elle v\u00e9rifie si la nouvelle dame peut \u00eatre plac\u00e9e sans \u00eatre attaqu\u00e9e par une autre dame d\u00e9j\u00e0 plac\u00e9e sur l'\u00e9chiquier. Pour cela, elle v\u00e9rifie les colonnes et les diagonales de la nouvelle dame pour s'assurer qu'aucune autre dame ne peut l'attaquer. Il n'est pas n\u00e9cessaire de v\u00e9rifier les lignes car chaque dame est plac\u00e9e sur une ligne diff\u00e9rente.

    Le coeur de l'algorithme est la fonction solve qui utilise une approche de backtracking pour placer les huit dames sur l'\u00e9chiquier. C'est une fonction r\u00e9cursive. La fonction prend en param\u00e8tre l'\u00e9chiquier actuel et la ligne courante row \u00e0 explorer. Comme la fonction s'appelle elle-m\u00eame il faut imp\u00e9rativement une condition de sortie. Dans notre cas si row est \u00e9gal \u00e0 N alors toutes les dames ont \u00e9t\u00e9 plac\u00e9es et on peut afficher la solution.

    Pour chaque ligne, on explore chaque colonne en commencant par la premi\u00e8re. Si la case est s\u00fbre, on place une dame et on appelle r\u00e9cursivement la fonction solve pour explorer la ligne suivante. A chaque appel successif de solve on place une dame suppl\u00e9mentaire. Si d'avenure on se rend compte que la configuration n'est pas valide, on retire la dame et on explore une autre colonne.

    Pour mieux comprendre, r\u00e9duisons le probl\u00e8me \u00e0 un \u00e9chiquier 4x4 et ajoutons un affichage dans solve. Pour conna\u00eetre la profondeur de la r\u00e9cursion, on passe un param\u00e8tre depth \u00e0 la fonction solve qui sera incr\u00e9mente \u00e0 chaque appel.

    #define PAD(depth)                                   \\\n   {                                                 \\\n      for (int i = 0; i < depth; i++) printf(\"   \"); \\\n   }\n\nvoid solve(bool board[N][N], int row, int depth) {\n   if (row >= N) {\n      PAD(depth);\n      printf(\"END\\n\");\n      return;\n   }\n\n   PAD(depth);\n   for (int col = 0; col < N; col++) {\n      printf(\"%c%d \", 'A' + col, row);\n      if (is_safe(board, row, col)) {\n         board[row][col] = QUEEN;\n         printf(\"\\n\");\n         solve(board, row + 1, depth + 1);\n         board[row][col] = EMPTY;  // BACKTRACK\n      }\n   }\n   printf(\"\\n\");\n   PAD(depth - 1);\n}\n

    L'ex\u00e9cution comment\u00e9e donne ceci\u2009:

    ------------------------> Niveau de r\u00e9cursion\nA0                    Une dame est plac\u00e9e puis on explore la ligne suivante\n   A1 B1 C1           Les positions A1, B1 sont invalides mais C1 est valide\n      A2 B2 C2 D2     Explore la ligne 2 mais aucune position n'est valide\n   D1                 On backtrack et explore la position suivante D1\n      A2 B2           Qui est valide, donc on explore la ligne 2\n         A3 B3 C3 D3  Et la ligne 3 car B2 \u00e9tait valide\n      C2 D2\n... Jusqu'ici aucune solution valide alors on backtrack\n\nB0\n   A1 B1 C1 D1\n      A2\n         A3 B3 C3\n            END       Une solution trouv\u00e9e avec B0, D1, A2, C3\nD3\n      B2 C2 D2\nC0\n   A1\n      A2 B2 C2 D2\n         A3 B3\n            END       Une autre solution trouv\u00e9e avec C0, A1, D2, B3\nC3 D3\n   B1 C1 D1\nD0\n   A1\n      A2 B2 C2\n         A3 B3 C3 D3\n      D2\n   B1\n      A2 B2 C2 D2\n   C1 D1\n

    Le probl\u00e8me des 8 dames peut \u00eatre aussi impl\u00e9ment\u00e9 sous forme it\u00e9rative. La technique est toujours la m\u00eame, on utilise une pile pour stocker les positions des dames. On notera que le code est bien plus complexe que la version r\u00e9cursive.

    void solve(bool board[N][N]) {\n   int row = 0, col = 0;\n   int stack[N] = {0};  // Stack to store column positions\n\n   while (row >= 0) {\n      bool placed = false;\n      while (col < N) {\n         if (is_safe(board, row, col)) {\n            board[row][col] = QUEEN;\n            stack[row] = col;\n            placed = true;\n            break;\n         }\n         col++;\n      }\n\n      if (placed) {\n         if (row == N - 1) {\n            printSolution(board);\n            board[row][col] = EMPTY;  // Backtrack\n            col = stack[row] + 1;     // Try next column in the same row\n         } else {\n            row++;\n            col = 0;\n         }\n      } else {\n         row--;\n         if (row >= 0) {\n            col = stack[row] + 1;  // Backtrack\n            board[row][stack[row]] = EMPTY;\n         }\n      }\n   }\n}\n
    ", "tags": ["row", "depth", "is_safe", "true", "solve", "false"]}, {"location": "course-c/40-algorithms/searching/", "title": "Recherche", "text": "

    La recherche est une op\u00e9ration courante en informatique. Il existe plusieurs algorithmes de recherche, chacun ayant ses avantages et inconv\u00e9nients. Les algorithmes de recherche les plus courants sont la recherche lin\u00e9aire et la recherche binaire.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-lineaire", "title": "Recherche lin\u00e9aire", "text": "

    La recherche lin\u00e9aire est une m\u00e9thode simple pour trouver un \u00e9l\u00e9ment dans un tableau. Elle consiste \u00e0 parcourir na\u00efvement tous les \u00e9l\u00e9ments du tableau, un par un, jusqu'\u00e0 trouver l'\u00e9l\u00e9ment recherch\u00e9. Si l'\u00e9l\u00e9ment est trouv\u00e9, la recherche s'arr\u00eate et renvoie la position de l'\u00e9l\u00e9ment dans le tableau. Si l'\u00e9l\u00e9ment n'est pas trouv\u00e9, la recherche renvoie une valeur sp\u00e9ciale pour indiquer que l'\u00e9l\u00e9ment est absent.

    Une impl\u00e9mentation pour une recherche d'entier serait la suivante\u2009:

    int linear_search(int *array, int size, int search) {\n    for (int i = 0; i < size; ++i)\n        if (array[i] == search)\n            return i;\n    return -1;\n}\n

    Une impl\u00e9mentation plus g\u00e9n\u00e9rique pour une recherche d'\u00e9l\u00e9ment de type variable peut \u00eatre r\u00e9alis\u00e9e en utilisant une fonction de comparaison laquelle retourne 0 si les deux \u00e9l\u00e9ments sont \u00e9gaux, 1 si le premier est plus grand et -1 si le premier est plus petit.

    int cmp_int(void *a, void *b) {\n    return *(int *)a - *(int *)b;\n}\n\nint linear_search(void *array, int size,\n    int element_size, void *search, int (*cmp)(void *, void *)) {\n    for (int i = 0; i < size; ++i)\n        if (cmp(array + i * element_size, search) == 0)\n            return i;\n    return -1;\n}\n

    La complexit\u00e9 de la recherche lin\u00e9aire est en \\(O(n)\\), o\u00f9 \\(n\\) est la taille du tableau. Si les recherches sont fr\u00e9quentes, et que la fonction de comparaison est complexe, cette m\u00e9thode n'est pas la plus efficace.

    L'utilisation d'une fonction de comparaison permet de r\u00e9aliser des recherches sur des tableaux qui ne se limites pas \u00e0 des entiers. On peut ainsi rechercher des cha\u00eenes de caract\u00e8res, des structures ou des objets plus complexes. Voici par exemple une fonction de comparaison pour des cha\u00eenes de caract\u00e8res\u2009:

    int cmp_str(void *a, void *b) {\n    return strcmp((char *)a, (char *)b);\n}\n
    "}, {"location": "course-c/40-algorithms/searching/#recherche-dichotomique", "title": "Recherche dichotomique", "text": "

    La recherche dichotomique est une m\u00e9thode plus efficace pour trouver un \u00e9l\u00e9ment dans un tableau mais elle impose que ce dernier soit tri\u00e9. Elle consiste \u00e0 diviser le tableau en deux parties \u00e9gales et \u00e0 comparer l'\u00e9l\u00e9ment recherch\u00e9 avec l'\u00e9l\u00e9ment au milieu du tableau. Si l'\u00e9l\u00e9ment est \u00e9gal \u00e0 l'\u00e9l\u00e9ment au milieu, la recherche s'arr\u00eate. Sinon, si l'\u00e9l\u00e9ment est plus petit, la recherche continue dans la premi\u00e8re moiti\u00e9 du tableau, sinon dans la seconde moiti\u00e9.

    Cette m\u00e9thode est celle que vous appliquez quand on demande de devinez un nombre choisi par un tier entre 1 et 100. Plut\u00f4t que choisir une valeur al\u00e9atoire \u00e0 chaque essai, vous annoncez en premier 50, puis 25 ou 75, puis 12 ou 37 ou 62 ou 87, etc. \u00c0 chaque essai vous \u00e9liminez la moiti\u00e9 des possibilit\u00e9s. On dit que la progression est logarithmique en base 2.

    Une impl\u00e9mentation pour une recherche d'entier serait la suivante\u2009:

    int binary_search(int *array, int size, int search) {\n    int left = 0, right = size - 1;\n    while (left <= right) {\n        int mid = left + (right - left) / 2;\n        if (array[mid] == search)\n            return mid;\n        if (array[mid] < search)\n            left = mid + 1;\n        else\n            right = mid - 1;\n    }\n    return -1;\n}\n

    Elle peut \u00e9galement \u00eatre g\u00e9n\u00e9ralis\u00e9e avec une fonction de comparaison ou sp\u00e9cialis\u00e9e pour un type particulier. La complexit\u00e9 de la recherche dichotomique est en \\(O(\\log n)\\), o\u00f9 \\(n\\) est la taille du tableau. Cette m\u00e9thode est donc beaucoup plus efficace que la recherche lin\u00e9aire pour des tableaux de grande taille mais elle impose un tri pr\u00e9alable en \\(O(n \\log n)\\).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-par-hashage", "title": "Recherche par hashage", "text": "

    L'utilisation d'un tableau de hachage permet de r\u00e9aliser des recherches en temps constant, en moyenne (\\(O(1)\\)). C'est donc une m\u00e9thode encore plus performante que la recherche dichotomique. Elle n\u00e9cessite \u00e9galement un pr\u00e9traitement pour construire la table de hachage, en \\(O(n)\\), mais elle est particuli\u00e8rement adapt\u00e9e pour des recherches fr\u00e9quentes dans des ensembles de donn\u00e9es volumineux.

    La performance de la recherche d\u00e9pend grandement de la fonction de hachage utilis\u00e9e et du facteur de charge de la table de hachage. Si le facteur de charge est trop \u00e9lev\u00e9, la recherche peut devenir lin\u00e9aire.

    Il est possible de s'affranchir du risque de collision en utilisant une table de hachage parfaite, mais cela implique une complexit\u00e9 bien plus grande pour la construction de la table. Une fonction de hashage parfaite est une fonction injective, c'est-\u00e0-dire qu'elle associe un \u00e9l\u00e9ment unique \u00e0 chaque cl\u00e9 sans risque de collision. Cette approche est particuli\u00e8rement adapt\u00e9s pour des ensemble de donn\u00e9es statiques (connus \u00e0 l'avance) et de petite taille.

    "}, {"location": "course-c/40-algorithms/searching/#localite-des-donnees", "title": "Localit\u00e9 des donn\u00e9es", "text": "

    La complexit\u00e9 de recherche n'est pas le seul crit\u00e8re \u00e0 prendre en compte pour choisir un algorithme de recherche. Le fonctionnement de l'ordinateur impose des contraintes mat\u00e9rielles qui peuvent influencer les performances. Par exemple, la recherche lin\u00e9aire peut \u00eatre plus rapide que la recherche dichotomique pour des tableaux de petite taille, car elle exploite mieux la localit\u00e9 des donn\u00e9es en m\u00e9moire.

    Nous l'avons abord\u00e9 lors de l'explication du fonctionnement de la m\u00e9moire que l'acc\u00e8s \u00e0 la RAM est lent par rapport au processeur. Pour ce faire le processeur fait appel \u00e0 une m\u00e9moire cache tr\u00e8s rapide mais beaucoup plus petite. Lorsque vous parcourez un tableau, les \u00e9l\u00e9ments sont charg\u00e9s en m\u00e9moire cache et la recherche lin\u00e9aire exploite mieux cette localit\u00e9 des donn\u00e9es. Si le tableau est tr\u00e8s grand, \u00e0 chaque saut de la recherche dichotomique vous ne profitez pas de la m\u00e9moire cache et le temps d'acc\u00e8s \u00e0 la RAM devient pr\u00e9pond\u00e9rant. Aussi dans l'\u00e9laboration d'un algorithme on cherche \u00e0 optimiser la localit\u00e9 des donn\u00e9es, \u00e0 la foi spatiale (les \u00e9l\u00e9ments sont proches en m\u00e9moire) et temporelle (les \u00e9l\u00e9ments sont utilis\u00e9s dans un court laps de temps) afin de minimiser les temps d'acc\u00e8s \u00e0 la RAM.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-sur-de-gros-volumes-de-donnees", "title": "Recherche sur de gros volumes de donn\u00e9es", "text": "

    Si on prend l'exemple d'un moteur de recherche sur internet, la recherche dichotomique n'est pas adapt\u00e9e. En effet, les donn\u00e9es sont stock\u00e9es sur des serveurs distants et le temps d'acc\u00e8s \u00e0 ces donn\u00e9es est bien plus long que le temps de calcul de l'algorithme. Dans ce cas, la recherche par hashage est plus adapt\u00e9e car elle permet de r\u00e9duire le temps d'acc\u00e8s aux donn\u00e9es en les regroupant localement. D'autre part, ce type de probl\u00e8me n'est pas impl\u00e9ment\u00e9 avec un tableau d'entiers programm\u00e9 en C. Lorsque la collection et le traitement des donn\u00e9es deviennent trop volumineux, on utilise des bases de donn\u00e9es car d'une part elles offrent les outils n\u00e9cessaire \u00e0 la gestion des donn\u00e9es et d'autre part elles permettent le stockage des donn\u00e9es sur un disque dur.

    Dans les probl\u00e8mes courant d'ing\u00e9nierie, une base de donn\u00e9e SqlLite peut \u00eatre un excellent choix pour stocker des donn\u00e9es structur\u00e9es et effectuer des recherches complexes. Voici un exemple de recherche sur une base de donn\u00e9e SqlLite\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <sqlite3.h>\n\nint create_and_insert(sqlite3 *db) {\n    // Cr\u00e9er la table Personnes\n    const char *sql_create_table =\n        \"CREATE TABLE IF NOT EXISTS people(\"\n        \"id INTEGER PRIMARY KEY AUTOINCREMENT,\"\n        \"age INT,\"\n        \"firstname TEXT,\"\n        \"lastname TEXT,\"\n        \"salary REAL);\";\n\n    rc = sqlite3_exec(db, sql_create_table, 0, 0, &err_msg);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"SQL error: %s\\n\", err_msg);\n        sqlite3_free(err_msg);\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Ins\u00e9rer des donn\u00e9es dans la table\n    const char *sql_insert =\n        \"INSERT INTO people (age, lastname, firstname, salary) VALUES \"\n        \"(25, 'Anderson', 'Alexandre', 4500),\"\n        \"(35, 'Dupont',   'Fran\u00e7ois',  3200),\"\n        \"(28, 'Martin',   'Annabelle', 4800),\"\n        \"(42, 'Favre',    'Fabrice',   5200),\"\n        \"(30, 'Armand',   'Aurelie',   4100);\";\n\n    rc = sqlite3_exec(db, sql_insert, 0, 0, &err_msg);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"SQL error: %s\\n\", err_msg);\n        sqlite3_free(err_msg);\n        sqlite3_close(db);\n        return 1;\n    }\n    return 0;\n}\n\nint main(void) {\n    sqlite3 *db;\n    char *err_msg = 0;\n    int rc;\n\n    // Ouvrir ou cr\u00e9er la base de donn\u00e9es\n    rc = sqlite3_open(\"people.db\", &db);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"Cannot open database: %s\\n\", sqlite3_errmsg(db));\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Cr\u00e9er et ins\u00e9rer des donn\u00e9es dans la base de donn\u00e9es\n    if (create_and_insert(db)) {\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Requ\u00eate pour trouver les personnes correspondant aux crit\u00e8res\n    const char *sql_query =\n        \"SELECT * FROM people WHERE \"\n        \"age BETWEEN 23 AND 42 AND \"\n        \"(lastname LIKE 'A%' OR nom LIKE 'F%') AND \"\n        \"LENGTH(firstname) = 8 AND \"\n        \"salary > 4000;\";\n\n    sqlite3_stmt *stmt;\n    rc = sqlite3_prepare_v2(db, sql_query, -1, &stmt, 0);\n\n    if (rc != SQLITE_OK) {\n        fprintf(stderr, \"Failed to execute query: %s\\n\", sqlite3_errmsg(db));\n        sqlite3_close(db);\n        return 1;\n    }\n\n    // Ex\u00e9cuter la requ\u00eate et afficher les r\u00e9sultats\n    printf(\"R\u00e9sultats de la recherche:\\n\");\n    while (sqlite3_step(stmt) == SQLITE_ROW) {\n        int id = sqlite3_column_int(stmt, 0);\n        int age = sqlite3_column_int(stmt, 1);\n        const unsigned char *nom = sqlite3_column_text(stmt, 2);\n        const unsigned char *prenom = sqlite3_column_text(stmt, 3);\n        double salaire = sqlite3_column_double(stmt, 4);\n\n        printf(\"ID: %d, Age: %d, Nom: %s, Pr\u00e9nom: %s, Salaire: %.2f\\n\",\n               id, age, lastname, firstname, salary);\n    }\n\n    sqlite3_finalize(stmt);\n    sqlite3_close(db);\n}\n
    "}, {"location": "course-c/40-algorithms/searching/#la-malediction-de-la-dimensionnalite", "title": "La Mal\u00e9diction de la Dimensionnalit\u00e9", "text": "

    L'un des principaux probl\u00e8mes rencontr\u00e9s lorsqu'on travaille avec des donn\u00e9es en plusieurs dimensions est ce que l'on appelle aussi le \u00ab\u2009fl\u00e9au de la dimension\u2009\u00bb. Ce terme invent\u00e9 par Richard Bellman en 1961 d\u00e9crit plusieurs ph\u00e9nom\u00e8nes qui rendent la manipulation des donn\u00e9es multidimensionnelles plus complexe que celle des donn\u00e9es unidimensionnelles. En particulier, les points de donn\u00e9es deviennent de plus en plus dispers\u00e9s dans l'espace au fur et \u00e0 mesure que la dimension augmente, ce qui rend plus difficile la localisation rapide d'un voisin proche.

    La difficult\u00e9 r\u00e9side dans l'imposibilit\u00e9 d'inf\u00e9rer des relations entre les donn\u00e9es pour optimiser la recherche. Par exemple, si vous avez un tableau de 1000 \u00e9l\u00e9ments, vous pouvez diviser le tableau en 10 blocs de 100 \u00e9l\u00e9ments et effectuer une recherche dichotomique sur chaque bloc. Si vous avez un tableau de 1000 \u00e9l\u00e9ments en 2 dimensions, vous ne pouvez pas diviser le tableau en 10 blocs de 100 \u00e9l\u00e9ments dans chaque dimension. Vous devez diviser le tableau en 100 blocs de 10 \u00e9l\u00e9ments dans chaque dimension, soit 1000 blocs au total. La recherche dichotomique n'est plus efficace.

    Le probl\u00e8me est encore plus complexe lorsque la recherche porte sur une combinaison de plusieurs dimensions. Par exemple, vous stocker des coordonn\u00e9es g\u00e9ographiques (latitude et longitude) et vous cherchez \u00e0 trouver les points les plus proches d'un point donn\u00e9. La recherche dichotomique ne fonctionne pas dans ce cas. Une approche courante pour ce type de recherche est d'utiliser des arbres de recherche multidimensionnels, tels que les arbres KD (K-dimensional trees) ou les arbres R (R-trees).

    "}, {"location": "course-c/40-algorithms/searching/#k-d-tree", "title": "K-D Tree", "text": "

    Le K-D Tree est une structure de donn\u00e9es arborescente qui permet de stocker des points en plusieurs dimensions. Il est utilis\u00e9 pour effectuer des recherches spatiales efficaces, telles que la recherche des points les plus proches d'un point donn\u00e9. Le principe de l'arbre KD est de diviser l'espace en deux \u00e0 chaque niveau de l'arbre, en alternant les dimensions.

    Repr\u00e9sentation du K-D Tree

    Lors de l'insertion de chaque point, le plan est divis\u00e9 en deux parties \u00e9gales (gauche/droite) ou (haut/bas) selon la parit\u00e9 du niveau de l'arbre. Au niveau stockage, on utilise un arbre binaire\u2009:

    Arbre binaire du K-D Tree

    On sait qu'un arbre binaire peut \u00eatre repr\u00e9sent\u00e9 sous forme d'une liste, la repr\u00e9sentation ci-dessus peut-\u00eatre ainsi repr\u00e9sent\u00e9e avec avec\u2009:

    int nodes[] = {1, 2, 3, 5, -1, 4, -1, 6, 8, -1, -1, -1, -1, -1, 7};\n

    La valeur -1 indique un noeud vide. La racine de l'arbre est le premier \u00e9l\u00e9ment de la liste, ici 1. Les enfants d'un noeud i sont 2*i et 2*i+1. Par exemple, les enfants de 1 sont 2 et 3. Si l'arbre n'est pas complet et fortement d\u00e9s\u00e9quilibr\u00e9, le stockage ne sera pas optimal car la plupart des noeuds seront vides. Dans ce cas il est plus \u00e9l\u00e9gant de repr\u00e9senter l'arbre avec une liste cha\u00een\u00e9e.

    Bien entendu, les noeuds seront plus complexes que des entiers, un noeud peut par exemple \u00eatre associ\u00e9 \u00e0 un identifiant, et ses coordonn\u00e9es X et Y\u2009:

    typedef struct node {\n    int id;\n    double x, y;\n} Node;\n

    Dans notre exemple pour \u00e9conomiser de l'espace, nous pouvons utiliser une liste pour les noeuds, et un tableau pour l'arbre\u2009:

    Node nodes[] = {\n    {1, 10.6, 4.6},\n    {2,  7.1, 3.9},\n    {3, 12.6, 6.7},\n    {4, 14.6, 1.6},\n    {5,  3.6, 2.6},\n    {6,  2.6, 0.6},\n    {7,  2.4, 1.6},\n    {8,  4.6, 1.4}\n};\n\nint tree[] = {0, 1, 2, 4, -1, 3, -1, 5, 7, -1, -1, -1, -1, -1, 6};\n

    L'acc\u00e8s \u00e0 un \u00e9l\u00e9ment s'\u00e9crirait alors\u2009: nodes[tree[1]] pour acc\u00e9der au noeud 2, \u00e0 condition que le noeud ne soit pas vide.

    "}, {"location": "course-c/40-algorithms/searching/#recherche-du-voisin-le-plus-proche", "title": "Recherche du voisin le plus proche", "text": "

    Pour rechercher le voisin le plus proche d'un point donn\u00e9 dans un K-D Tree, on utilise une m\u00e9thode de recherche r\u00e9cursive qui exploite la structure de l'arbre pour r\u00e9duire efficacement l'espace de recherche. Le processus commence par la recherche du point cible en parcourant l'arbre, en suivant les divisions du plan \u00e0 chaque niveau. Lorsque le point cible est trouv\u00e9 ou que l'on atteint une feuille, on remonte l'arbre en v\u00e9rifiant \u00e0 chaque n\u0153ud si celui-ci est plus proche du point cible que les \\(k\\) points actuellement dans la liste des plus proches voisins. Pour maintenir cette liste, on utilise g\u00e9n\u00e9ralement une file de priorit\u00e9 (ou un max-heap) qui garde toujours les \\(k\\) points les plus proches trouv\u00e9s jusqu'\u00e0 pr\u00e9sent.

    Lors de la remont\u00e9e, on compare \u00e9galement la distance entre le plan de s\u00e9paration du n\u0153ud courant et le point cible. Si cette distance est inf\u00e9rieure \u00e0 la distance du plus \u00e9loign\u00e9 des \\(k\\) voisins actuels, cela signifie qu'il pourrait y avoir un voisin plus proche de l'autre c\u00f4t\u00e9 du plan. Dans ce cas, la recherche est \u00e9galement effectu\u00e9e dans la sous-arborescence oppos\u00e9e.

    Prenons un exemple concret. Sur la figure suivante, on prend le point P \\((3.2, 2.3)\\) comme point cible comme pr\u00e9sent\u00e9 sur la figure suivante\u2009:

    Recherche d'une zone dans un K-D Tree

    Pour trouver le voisin le plus proche voici les op\u00e9rations\u2009:

    1. Descente dans l'arbre (recherche initiale):
    2. On commence par comparer le point \\( P \\) avec le n\u0153ud de la racine en fonction de la dimension actuelle (par exemple, si l'arbre est divis\u00e9 selon les coordonn\u00e9es \\( x \\) et \\( y \\), on commence par comparer les \\( x \\)).
    3. On continue de descendre dans l'arbre en suivant les enfants gauche ou droit en fonction de la position du point \\( P \\) par rapport au plan de s\u00e9paration de chaque n\u0153ud. \u00c0 chaque \u00e9tape, on r\u00e9duit l'espace de recherche en alternant les dimensions, jusqu'\u00e0 atteindre une feuille. Cette feuille correspond au point de l'arbre qui est le plus proche dans l'une des dimensions de \\( P \\).

    4. Remont\u00e9e dans l'arbre (recherche du point le plus proche):

    5. Une fois la feuille atteinte, on enregistre ce point comme le point le plus proche trouv\u00e9 jusqu'\u00e0 pr\u00e9sent. Cependant, il est possible que le point le plus proche ne soit pas dans la sous-arborescence de cette feuille, mais plut\u00f4t dans une autre branche de l'arbre.
    6. En remontant l'arbre, on \u00e9value chaque n\u0153ud parent pour v\u00e9rifier s'il pourrait contenir un point plus proche que le point le plus proche actuel. Pour cela, on calcule la distance entre \\( P \\) et le point stock\u00e9 dans le n\u0153ud courant, ainsi que la distance entre \\( P \\) et le plan de s\u00e9paration de ce n\u0153ud.

    7. Si la distance du plan de s\u00e9paration est inf\u00e9rieure \u00e0 la distance au point le plus proche actuel, cela signifie que l'autre sous-arborescence (c\u00f4t\u00e9 oppos\u00e9 du plan) pourrait contenir un point plus proche de \\( P \\). On doit donc explorer cette autre sous-arborescence, ce qui implique de descendre dans cette sous-arborescence comme on l'a fait initialement.

    8. Exploration de la sous-arborescence oppos\u00e9e:

    9. Lors de l'exploration de cette sous-arborescence oppos\u00e9e, on suit un processus similaire en descendant jusqu'\u00e0 une feuille et en comparant chaque n\u0153ud avec le point le plus proche actuellement connu.
    10. Si un point plus proche est trouv\u00e9 dans cette sous-arborescence, il remplace le point le plus proche actuel.

    11. Terminaison:

    12. Le processus se poursuit jusqu'\u00e0 ce que l'on ait remont\u00e9 jusqu'\u00e0 la racine, en explorant potentiellement plusieurs sous-arborescences oppos\u00e9es en fonction de la distance au plan de s\u00e9paration.
    13. \u00c0 la fin de ce processus, le point le plus proche trouv\u00e9 sera le plus proche de \\( P \\) dans l'ensemble du K-D Tree.

    En pratique, suivant notre exemple, on part de (1). Au premier niveau de l'arbre on sait que les enfants correspondent \u00e0 l'axe des abscisses et comme \\(3.2 < 10.6\\) on prend le fils gauche. On descend donc jusqu'\u00e0 (2). Cette fois-ci c'es selon l'axe des ordonn\u00e9es que l'on compare ce qui nous emm\u00e8ne \u00e0 (5), puis (6), puis (7). La descente s'arr\u00eate l\u00e0 car on \u00e0 atteint une feuille. \u00c0 partir de maintenant on entre dans la seconde phase. On remonte alors l'arbre en v\u00e9rifiant si le sous-arbre oppos\u00e9 pourrait contenir des points plus proches.Comme le plan de s\u00e9paration de (6) est plus \u00e9loign\u00e9 que (7) on explore pas la r\u00e9gion au dessus de (6). On continue la remont\u00e9e jusqu'\u00e0 (5). Cette fois-ci la distance de s\u00e9paration est plus proche (abs(P.x - 7.x) = 0.8 et abs(P.x - 5,x) = 0.4) donc, il se peut que la r\u00e9gion \u00e0 droite de (5) contienne un point plus proche. On entre alors dans a troisi\u00e8me phase\u2009: l'exploration de la partie droite de (5). On descend alors jusqu'\u00e0 (8) qui est une feuille, et qui de surcro\u00eet est plus \u00e9loign\u00e9 que (7).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-des-k-voisins-les-plus-proches", "title": "Recherche des k-voisins les plus proches", "text": "

    Pour rechercher les \\(k\\) voisins les plus proches d'un point donn\u00e9 dans un K-D Tree, on utilise une m\u00e9thode similaire \u00e0 la recherche du voisin le plus proche, mais en maintenant une liste des \\(k\\) points les plus proches trouv\u00e9s jusqu'\u00e0 pr\u00e9sent. Lors de la remont\u00e9e dans l'arbre, on compare chaque n\u0153ud parent avec les points de la liste des \\(k\\) voisins les plus proches. Si le n\u0153ud parent est plus proche que le point le plus \u00e9loign\u00e9 de la liste, on l'ajoute \u00e0 la liste et on retire le point le plus \u00e9loign\u00e9. On continue ce processus jusqu'\u00e0 ce que l'arbre soit enti\u00e8rement explor\u00e9. Cela correspond \u00e0 utiliser une structure de donn\u00e9es de file de priorit\u00e9 (ou max-heap) pour maintenir les \\(k\\) points les plus proches. La m\u00e9thode est donc tr\u00e8s similaire \u00e0 la recherche du voisin le plus proche, mais avec une gestion plus complexe de la liste des voisins.

    La recherche des \\(k\\) voisins est donc une op\u00e9ration en \\(O(k \\log n)\\).

    "}, {"location": "course-c/40-algorithms/searching/#recherche-des-voisins-dans-un-cercle", "title": "Recherche des voisins dans un cercle", "text": "

    Reconsid\u00e9rons notre figure pr\u00e9c\u00e9dente. On souhaite maintenant chercher les points \u00e0 l'int\u00e9rieur du cercle jaune de diam\u00e8tre \\(3.75\\). Pour cela on commence par chercher le point le plus proche de \\(P\\) comme pr\u00e9c\u00e9demment. On remonte l'arbre en v\u00e9rifiant si le plan de s\u00e9paration est \u00e0 une distance inf\u00e9rieure \u00e0 \\(3.75\\) du point \\(P\\). Dit autrement, est-ce que le cercle coupe le plan de s\u00e9paration. Si c'est le cas on explore l'autre sous-arbre. On continue ce processus jusqu'\u00e0 ce que l'on ait explor\u00e9 tous les sous-arbres dont le plan de s\u00e9paration est \u00e0 une distance inf\u00e9rieure \u00e0 \\(3.75\\) de \\(P\\).

    "}, {"location": "course-c/40-algorithms/searching/#implementation", "title": "Impl\u00e9mentation", "text": "

    L'impl\u00e9mentation passe par la d\u00e9finition d'un noeud\u2009:

    typedef struct Node {\n   int id;\n   double x, y;\n   struct Node *left, *right;\n} Node;\n\nNode* insert(Node* root, Node* new_node, int depth) {\n   if (root == NULL) return new_node;\n\n   int cd = depth % 2;\n\n   if ((cd == 0 && new_node->x < root->x) || (cd == 1 && new_node->y < root->y))\n      root->left = insert(root->left, new_node, depth + 1);\n   else\n      root->right = insert(root->right, new_node, depth + 1);\n\n   return root;\n}\n\nvoid search_closest(Node* root, double x, double y, int depth,\n                    ClosestNode* closest) {\n   if (root == NULL) return;\n\n   double d = squared_distance(root->x, root->y, x, y);\n   if (d < closest->distance) {\n      closest->node = root;\n      closest->distance = d;\n   }\n\n   int cd = depth % 2;\n\n   // Quelle sous-arborescence est la plus proche ?\n   Node* nearer_subtree = NULL;\n   Node* farther_subtree = NULL;\n   if ((cd == 0 && x < root->x) || (cd == 1 && y < root->y)) {\n      nearer_subtree = root->left;\n      farther_subtree = root->right;\n   } else {\n      nearer_subtree = root->right;\n      farther_subtree = root->left;\n   }\n\n   // Rechercher d'abord dans la sous-arborescence la plus proche\n   search_closest(nearer_subtree, x, y, depth + 1, closest);\n\n   // V\u00e9rifier si nous devons explorer la sous-arborescence plus \u00e9loign\u00e9e\n   double dist_to_plane = (cd == 0) ? (x - root->x) : (y - root->y);\n   if (dist_to_plane * dist_to_plane < closest->distance)\n      search_closest(farther_subtree, x, y, depth + 1, closest);\n}\n

    Pour ins\u00e9rer un noeud \u00e0 la position x, y, on utilisera\u2009:

    Node* new_node = (Node*)malloc(sizeof(Node));\nnew_node->x = x;\nnew_node->y = y;\nnew_node->left = new_node->right = NULL;\n*root = insert(*root, new_node, 0);\n

    Et pour rechercher l'\u00e9l\u00e9ment le plus proche de x, y on utilisera\u2009:

    ClosestNode closest;\nclosest.node = NULL;\nclosest.distance = DBL_MAX;\n\nsearch_closest(root, x, y, 0, &closest);\n
    "}, {"location": "course-c/40-algorithms/utilities/", "title": "Algorithmes sur cha\u00eenes", "text": ""}, {"location": "course-c/40-algorithms/utilities/#slurp", "title": "Slurp", "text": "

    Il est souvent n\u00e9cessaire de lire l'int\u00e9gralit\u00e9 de l'entr\u00e9e standard dans une cha\u00eene de caract\u00e8res. Cependant, comme l'entr\u00e9e standard (stdin) n'est pas seekable, c'est-\u00e0-dire qu'il est impossible de se d\u00e9placer librement dans le flux ou d'en d\u00e9terminer la taille \u00e0 l'avance, il devient impossible d'allouer pr\u00e9cis\u00e9ment la m\u00e9moire n\u00e9cessaire \u00e0 l'avance. Une strat\u00e9gie commune consiste \u00e0 lire le flux par fragments et \u00e0 utiliser un tableau dynamique, redimensionn\u00e9 de fa\u00e7on progressive (via un facteur de croissance), pour stocker l'int\u00e9gralit\u00e9 du contenu. C'est pr\u00e9cis\u00e9ment l'objectif de la fonction slurp pr\u00e9sent\u00e9e ci-dessous. Slurp est un terme argotique qui signifie \u00ab\u2009aspirer\u2009\u00bb ou \u00ab\u2009engloutir\u2009\u00bb en anglais, c'est \u00e9galement un terme utilis\u00e9 en informatique pour d\u00e9signer le fait de lire un fichier en entier, notamment en Perl.

    slurp.h
    #pragma once\n\n#include <stdio.h>\nchar *slurp(FILE *file);\n
    slurp.c
    #include <stdio.h>\n#include <stdlib.h>\n\nchar *slurp(FILE *file) {\n   size_t size = 256;\n   size_t len = 0;\n   char *input = (char *)malloc(size);\n\n   if (input == NULL) {\n      fprintf(stderr, \"Memory allocation failed\\n\");\n      exit(1);\n   }\n\n   size_t bytesRead;\n   while ((bytesRead = fread(input + len, 1, size - len - 1, file)) > 0) {\n      len += bytesRead;\n      if (len + 1 >= size) {\n         size *= 2;\n         char *newInput = (char *)realloc(input, size);\n         if (newInput == NULL) {\n            free(input);\n            fprintf(stderr, \"Memory reallocation failed\\n\");\n            exit(1);\n         }\n         input = newInput;\n      }\n   }\n\n   if (ferror(file)) {\n      free(input);\n      fprintf(stderr, \"Error reading file\\n\");\n      exit(1);\n   }\n\n   input[len] = '\\0';\n   return input;\n}\n
    ", "tags": ["slurp", "stdin"]}, {"location": "course-c/40-algorithms/utilities/#analyse-et-alternatives-possibles", "title": "Analyse et alternatives possibles", "text": ""}, {"location": "course-c/40-algorithms/utilities/#taille-predeterminee", "title": "Taille pr\u00e9d\u00e9termin\u00e9e", "text": "

    Une premi\u00e8re alternative consisterait \u00e0 allouer un espace m\u00e9moire suffisamment grand pour que la probabilit\u00e9 que le fichier exc\u00e8de cette taille soit tr\u00e8s faible. Cependant, cette approche est loin d'\u00eatre robuste. Elle se base sur des hypoth\u00e8ses qui pourraient \u00e9chouer sur des syst\u00e8mes avec des contraintes m\u00e9moire strictes, ou lorsque la taille du fichier d\u00e9passe largement les pr\u00e9visions. De plus, elle introduit un risque de gaspillage de m\u00e9moire si l'estimation d\u00e9passe largement la taille r\u00e9elle du fichier.

    "}, {"location": "course-c/40-algorithms/utilities/#utilisation-dun-fichier-temporaire", "title": "Utilisation d'un fichier temporaire", "text": "

    Une autre approche consisterait \u00e0 rediriger le flux non seekable vers un fichier temporaire. Une fois que le flux a \u00e9t\u00e9 enti\u00e8rement consomm\u00e9, il serait alors possible de rouvrir ce fichier temporaire et d'en d\u00e9terminer la taille exacte avec fseek. On pourrait ensuite allouer pr\u00e9cis\u00e9ment la m\u00e9moire requise en une seule op\u00e9ration, charger le fichier en m\u00e9moire, puis supprimer le fichier temporaire. Toutefois, cette solution, bien qu'ing\u00e9nieuse, pr\u00e9sente \u00e9galement des inconv\u00e9nients\u2009: elle repose sur le syst\u00e8me de fichiers, ce qui ajoute une complexit\u00e9 inutile et la rend sensiblement plus lente que la solution initiale.

    ", "tags": ["fseek"]}, {"location": "course-c/40-algorithms/utilities/#analyse-de-la-solution-proposee", "title": "Analyse de la solution propos\u00e9e", "text": "

    La solution actuelle a n\u00e9anmoins des aspects qui m\u00e9ritent d'\u00eatre discut\u00e9s. Tout d'abord, la taille du tampon initial est fix\u00e9e arbitrairement \u00e0 256 caract\u00e8res. Pour de grands fichiers, cette taille modeste entra\u00eenera un nombre \u00e9lev\u00e9 d'appels syst\u00e8mes \u00e0 read, ce qui peut affecter les performances globales. Une meilleure approche serait de permettre \u00e0 l'utilisateur de sp\u00e9cifier la taille initiale du tampon en tant qu'argument optionnel de la fonction, avec une valeur par d\u00e9faut appropri\u00e9e si un argument nul ou z\u00e9ro est fourni.

    Par ailleurs, la fonction pourrait \u00e9chouer dans le cas o\u00f9 le fichier \u00e0 lire est trop volumineux pour tenir en m\u00e9moire. Une am\u00e9lioration serait de permettre la d\u00e9finition d'une taille maximale \u00e0 charger en m\u00e9moire, transmise en param\u00e8tre. Si le fichier d\u00e9passe cette limite, la fonction pourrait retourner un pointeur NULL, signalant ainsi au programme appelant que le fichier ne peut pas \u00eatre trait\u00e9 enti\u00e8rement en m\u00e9moire.

    Un autre aspect discutable de l'impl\u00e9mentation actuelle r\u00e9side dans la gestion des erreurs. En cas d'\u00e9chec d'allocation m\u00e9moire (malloc ou realloc), la fonction appelle exit(), ce qui interrompt brutalement l'ex\u00e9cution du programme. Il serait pr\u00e9f\u00e9rable d'adopter une approche plus flexible en retournant un code d'erreur ou un pointeur NULL, laissant ainsi le programme appelant d\u00e9cider de la mani\u00e8re de g\u00e9rer l'erreur. Cela permettrait une meilleure gestion des erreurs au niveau applicatif, notamment dans les cas o\u00f9 une terminaison imm\u00e9diate du programme n'est pas souhaitable.

    Enfin, on notera que l'espace allou\u00e9 n'est pas r\u00e9duit \u00e0 la taille exacte du fichier apr\u00e8s lecture. Cela peut entra\u00eener un gaspillage de m\u00e9moire si l'algorithme vient de r\u00e9allouer la m\u00e9moire. Une am\u00e9lioration possible serait de r\u00e9duire la taille du tampon \u00e0 la taille exacte du fichier apr\u00e8s lecture, en utilisant realloc pour lib\u00e9rer l'espace exc\u00e9dentaire.

    La solution propos\u00e9e est fonctionnelle mais perfectible, comme tout code informatique. Gardons \u00e0 l'esprit ici que l'objectif est de comprendre le concept de l'algorithme et de saisir les principes de base de sa mise en \u0153uvre.

    ", "tags": ["read", "NULL", "malloc", "realloc"]}, {"location": "course-c/40-algorithms/utilities/#split", "title": "Split", "text": "

    Un besoin assez courant concernant les cha\u00eenes de caract\u00e8res est de les diviser en sous-cha\u00eenes en fonction d'un d\u00e9limiteur. Prenons l'exemple d'un fichier CSV (Comma-Separated Values) o\u00f9 les champs sont s\u00e9par\u00e9s par des virgules ou point virgules. Pour chaque ligne, l'objectif est de parcourir la cha\u00eene \u00e0 la recherche du d\u00e9limiteur et de copier le champ dans un tableau. Pour conserver un code g\u00e9n\u00e9rique, l'op\u00e9ration de stockage des champs peut \u00eatre confi\u00e9e \u00e0 une fonction de type callback, qui sera appel\u00e9e pour chaque champ trouv\u00e9.

    Pour l'impl\u00e9mentation il est possible de profiter de la fonction strtok de la biblioth\u00e8que standard C, qui permet de d\u00e9couper une cha\u00eene en fonction d'un d\u00e9limiteur. Cependant, strtok est une fonction stateful, c'est-\u00e0-dire qu'elle conserve l'\u00e9tat interne entre les appels. On pr\u00e9f\u00e8rera donc utiliser une version s\u00e9curis\u00e9e thread-safe de cette fonction, strtok_s, qui prend en param\u00e8tre un pointeur vers un pointeur de cha\u00eene de caract\u00e8res. Cela permet de d\u00e9couper une cha\u00eene en plusieurs sous-cha\u00eenes sans interf\u00e9rer avec d'autres appels \u00e0 la fonction. Voici une impl\u00e9mentation possible de la fonction split :

    #include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n\ntypedef void (*FieldCallback)(const char *field);\n\nvoid split(const char *str, const char *delim, FieldCallback callback) {\n    if (str == NULL || delim == NULL || callback == NULL) {\n        fprintf(stderr, \"Invalid argument(s) passed to split.\\n\");\n        return;\n    }\n\n    // Copies str to avoid modifying the original. strtok is destructive, as\n    // it adds null terminators to the original string.\n    char *strCopy = strdup(str);\n    if (strCopy == NULL) {\n        fprintf(stderr, \"Memory allocation failed.\\n\");\n        return;\n    }\n\n    char *saveptr;\n    char *token = strtok_r(strCopy, delim, &saveptr);\n    while (token != NULL) {\n        callback(token);\n        token = strtok_r(NULL, delim, &saveptr);\n    }\n    free(strCopy);\n}\n\nvoid printField(const char *field) { printf(\"%s\\n\", field); }\n\nint main() {\n    const char *csvLine = \"apple,banana,orange,grape\";\n    split(csvLine, \",\", printField);\n}\n

    Dans le cas ou la cha\u00eene originale est non constante, et qu'elle peut \u00eatre modifi\u00e9e sans risque, il n'est pas n\u00e9cessaire de copier la cha\u00eene avant de l'envoyer \u00e0 strtok_r et le code de split peut \u00eatre simplifi\u00e9.

    Notez \u00e9galement que si la fonction callback utilise un m\u00e9canisme de longjmp pour sortir de la fonction, le free(strCopy) pourrait ne jamais \u00eatre appel\u00e9, ce qui entra\u00eenerait une fuite de m\u00e9moire.

    ", "tags": ["strtok_r", "split", "callback", "CSV", "strtok_s", "strtok", "longjmp"]}, {"location": "course-c/40-algorithms/utilities/#join", "title": "Join", "text": "

    L'op\u00e9ration de jointure est l'op\u00e9ration inverse de split. Elle consiste \u00e0 concat\u00e9ner plusieurs cha\u00eenes de caract\u00e8res en une seule, en les s\u00e9parant par un d\u00e9limiteur. Cette op\u00e9ration est couramment utilis\u00e9e pour g\u00e9n\u00e9rer des cha\u00eenes de requ\u00eates SQL, des URL, des cha\u00eenes de formatage, etc. Voici une impl\u00e9mentation possible de la fonction join en utilisant la biblioth\u00e8que standard C\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstatic size_t get_total_length(const char **strings, size_t count,\n                               const char *delimiter) {\n   size_t total_length = 0;\n   size_t delimiter_length = strlen(delimiter);\n\n   for (size_t i = 0; i < count; i++) {\n      total_length += strlen(strings[i]);\n      if (i < count - 1) total_length += delimiter_length;\n   }\n   return total_length + sizeof('\\0');\n}\n\nchar *join(const char **strings, size_t count, const char *delimiter) {\n   if (strings == NULL || count == 0) return NULL;\n\n   size_t total_length = get_total_length(strings, count, delimiter);\n\n   char *result = (char *)malloc(total_length);\n   if (result == NULL) {\n      fprintf(stderr, \"Memory allocation failed in %s\\n\", __func__);\n      return NULL;\n   }\n\n   result[0] = '\\0';\n   for (size_t i = 0; i < count; i++) {\n      strcat(result, strings[i]);\n      if (i < count - 1) strcat(result, delimiter);\n   }\n   return result;\n}\n\n// Exemple d'utilisation de la fonction `join`\nint main() {\n   const char *strings[] = {\"apple\", \"banana\", \"orange\", \"grape\"};\n   char *result = join(strings, 4, \", \");\n\n   if (result == NULL) fprintf(stderr, \"Error joining strings\\n\");\n\n   printf(\"%s\\n\", result);\n   free(result);  // Because `join` allocates memory,\n                  // it should be freed after use\n}\n
    ", "tags": ["join", "split"]}, {"location": "course-c/40-algorithms/utilities/#trim", "title": "Trim", "text": "

    La fonction trim permet de supprimer les espaces en d\u00e9but et en fin de cha\u00eene. Cette op\u00e9ration est couramment utilis\u00e9e pour nettoyer les cha\u00eenes de caract\u00e8res avant de les traiter. Voici une impl\u00e9mentation possible de la fonction trim.

    La fonction modifie la cha\u00eene en place en d\u00e9placant les caract\u00e8res non blancs vers le d\u00e9but de la cha\u00eene, puis en ajoutant un caract\u00e8re nul \u00e0 la fin de la cha\u00eene.

    #include <stdio.h>\n#include <ctype.h>\n\nvoid trim(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to trim.\\n\");\n        return;\n    }\n\n    // Identify the start of the string\n    char *start = str;\n    while (*start != '\\0' && isspace(*start)) start++;\n\n    // Shift the string to the left\n    char *end = start;\n    while (*end != '\\0') *(str++) = *(end++);\n    *str = '\\0';\n\n    // Remove trailing whitespaces\n    if (str > start) {\n        str--;\n        while (str >= start && isspace(*str)) *(str--) = '\\0';\n    }\n}\n

    Notons que cette impl\u00e9mentation n'est pas n\u00e9cessairement optimale car elle parcours la cha\u00eene de caract\u00e8res. Dans le cas o\u00f9 il est possible de modifier le pointeur de la cha\u00eene, il est possible de grandement simplifier l'algorithme en d\u00e9pla\u00e7ant directement le pointeur de d\u00e9but et en rajoutant une sentinelle \u00e0 la fin de la cha\u00eene.

    ", "tags": ["trim"]}, {"location": "course-c/40-algorithms/utilities/#chomp", "title": "Chomp", "text": "

    La fonction chomp permet de supprimer le caract\u00e8re de fin de ligne d'une cha\u00eene de caract\u00e8res. Ce caract\u00e8re est g\u00e9n\u00e9ralement un retour \u00e0 la ligne (\\n) ou un retour chariot (\\r). Le terme vient de l'action de \u00ab\u2009manger\u2009\u00bb le caract\u00e8re de fin de ligne, il a \u00e9t\u00e9 popularis\u00e9 par le langage de programmation Perl qui dispose d'une fonction chomp pour effectuer cette op\u00e9ration.

    Voici une impl\u00e9mentation possible de la fonction chomp :

    #include <stdio.h>\n#include <string.h>\n\nvoid chomp(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to chomp.\\n\");\n        return;\n    }\n\n    size_t len = strlen(str);\n    if (len > 0 && (str[len - 1] == '\\n' || str[len - 1] == '\\r')) {\n        str[len - 1] = '\\0';\n    }\n}\n
    ", "tags": ["chomp"]}, {"location": "course-c/40-algorithms/utilities/#reverse", "title": "Reverse", "text": "

    La fonction reverse permet d'inverser une cha\u00eene de caract\u00e8res. Cette op\u00e9ration est couramment utilis\u00e9e pour inverser le contenu d'une cha\u00eene, par exemple pour afficher un texte \u00e0 l'envers. N\u00e9anmoins notons que ce n'est pas une op\u00e9ration triviale en C.

    Sans informations suppl\u00e9mentaires sur la cha\u00eene de caract\u00e8re, il n'est pas possible de savoir si elle contient des caract\u00e8res multioctets (UTF-8, UTF-16, etc.) ou des caract\u00e8res de contr\u00f4le. Dans le cas de caract\u00e8res multioctets, il est n\u00e9cessaire de traiter la cha\u00eene de caract\u00e8res en tant que s\u00e9quence de caract\u00e8res UTF-32 et non pas en tant que s\u00e9quence d'octets. D'autre pas si la cha\u00eene de caract\u00e8res contient des caract\u00e8res de contr\u00f4le comme \\r\\n il ne suffit pas d'inverser les deux caract\u00e8res car \\n\\r n'est pas n\u00e9cessairement reconnu par votre syst\u00e8me.

    ", "tags": ["reverse"]}, {"location": "course-c/40-algorithms/utilities/#implementation-ascii", "title": "Impl\u00e9mentation ASCII", "text": "

    Voyons tout d'abord le cas trivial o\u00f9 la cha\u00eene de caract\u00e8res ne contient que des caract\u00e8res ASCII :

    #include <stdio.h>\n\nvoid swap(char *a, char *b) {\n    char tmp = *a;\n    *a = *b;\n    *b = tmp;\n}\n\nvoid reverse(char *str) {\n    if (str == NULL) {\n        fprintf(stderr, \"Invalid argument passed to reverse.\\n\");\n        return;\n    }\n\n    char *start = str, *end = str;\n    while (*end != '\\0') end++;\n    end--;\n\n    while (start < end) swap(start++, end--);\n}\n

    "}, {"location": "course-c/40-algorithms/utilities/#implementation-utf-8", "title": "Impl\u00e9mentation UTF-8", "text": "

    Dans cette impl\u00e9mentation plus compliqu\u00e9e, il est n\u00e9cessaire de convertir les caract\u00e8res UTF-8 multioctets en caract\u00e8res UTF-32 pour pouvoir les inverser correctement. Voici un exemple d'impl\u00e9mentation de la fonction reverse pour les cha\u00eenes de caract\u00e8res UTF-8. Elle utilise les fonctions mbrtoc32 et c32rtomb de la biblioth\u00e8que standard C pour la conversion entre UTF-8 et UTF-32\u2009:

    #include <locale.h>\n#include <stdio.h>\n#include <stdlib.h>  // Pour MB_CUR_MAX\n#include <string.h>\n#include <uchar.h>\n\nint main() {\n   setlocale(LC_ALL, \"\");  // Initialiser la locale pour UTF-8\n\n   char utf8_str[] = \"Salut \u0393\u03b9\u03ce\u03c1\u03b3\u03bf\u03c2, comment \u00e7a va ? As-tu re\u00e7u mon \ud83d\udce7 ?\";\n   size_t utf8_len = strlen(utf8_str);\n\n   // Convertir UTF-8 en UTF-32\n   char32_t utf32_str[utf8_len];\n   size_t utf32_len = 0;\n   {\n      mbstate_t state = {0};\n      size_t ret;\n      const char *p = utf8_str;\n      while (*p != '\\0') {\n         size_t ret = mbrtoc32(&utf32_str[utf32_len], p, MB_CUR_MAX, &state);\n         if (ret == (size_t)-1) {\n            perror(\"Erreur de conversion UTF-8 vers UTF-32\");\n            return 1;\n         } else if (ret == (size_t)-2) {\n            // S\u00e9quence multioctet incompl\u00e8te, passer \u00e0 l'octet suivant\n            break;\n         } else if (ret == 0) {\n            // Fin de la cha\u00eene UTF-8 atteinte\n            break;\n         }\n         p += ret;\n         utf32_len++;\n      }\n   }\n\n   // Inverser la cha\u00eene UTF-32\n   for (size_t i = 0, j = utf32_len - 1; i < j; i++, j--) {\n      char32_t tmp = utf32_str[i];\n      utf32_str[i] = utf32_str[j];\n      utf32_str[j] = tmp;\n   }\n\n   // Conversion inverse UTF-32 vers UTF-8\n   {\n      mbstate_t state = {0};\n      char *utf8_ptr = utf8_str;\n      const char32_t *utf32_ptr = utf32_str;\n      size_t utf8_total_len = 0;\n      size_t ret;\n      while (utf32_len--) {\n         ret = c32rtomb(utf8_ptr, *utf32_ptr++, &state);\n         if (ret == (size_t)-1) {\n            perror(\"Erreur de conversion UTF-32 vers UTF-8\");\n            return 1;\n         }\n         utf8_ptr += ret;  // Avancer dans le buffer UTF-8\n         utf8_total_len += ret;\n      }\n      utf8_str[utf8_total_len] = '\\0';\n   }\n\n   printf(\"%s\\n\", utf8_str);\n}\n
    ", "tags": ["mbrtoc32", "reverse", "c32rtomb"]}, {"location": "course-c/40-algorithms/popular-algorithms/a-star/", "title": "A-Star", "text": "

    L'algorithme A* (A-Star) est un algorithme de recherche de chemin qui permet de trouver le chemin le plus court entre un point de d\u00e9part et un point d'arriv\u00e9e. Il est tr\u00e8s utilis\u00e9 en intelligence artificielle, en robotique, en jeux vid\u00e9o, etc.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/conway/", "title": "Jeu de la vie de Conway", "text": "

    Le jeu de la vie de Conway est un automate cellulaire invent\u00e9 par le math\u00e9maticien John Horton Conway en 1970. C'est une simulation qui se d\u00e9roule sur une grille bidimensionnelle infinie. Chaque cellule de la grille peut \u00eatre dans un \u00e9tat mort ou vivant. L'\u00e9tat des cellules \u00e9volue en fonction de r\u00e8gles simples.

    Un automate cellulaire est un mod\u00e8le math\u00e9matique qui consiste en une grille de cellules qui peuvent \u00eatre dans un \u00e9tat donn\u00e9. Chaque cellule interagit avec ses voisines en fonction de r\u00e8gles pr\u00e9d\u00e9finies. Les automates cellulaires sont utilis\u00e9s pour mod\u00e9liser des ph\u00e9nom\u00e8nes naturels, des syst\u00e8mes biologiques, des simulations, etc.

    Chaque cellule peut \u00eatre dans un \u00e9tat 0 (morte) ou 1 (vivante) en fonction de r\u00e8gles de transition. Les r\u00e8gles de transition d\u00e9finissent comment l'\u00e9tat d'une cellule \u00e9volue en fonction de l'\u00e9tat de ses voisines. Dans un automate bidimentionnel, donc une grille bidimensionnelle, chaque cellule a 8 voisines. On nomme ces voisines le voisinage de Moore o\u00f9 chaque voisin est num\u00e9rot\u00e9 ainsi\u2009:

    Voisinage de Moore

    Le format B/S est utilis\u00e9 pour d\u00e9finir les r\u00e8gles de transition. B signifie birth (naissance) et S signifie survival (survie). Les r\u00e8gles sont d\u00e9finies par une liste de chiffres qui indiquent le nombre de voisins n\u00e9cessaires pour qu'une cellule naisse ou survive. Par exemple, la r\u00e8gle B3/S23 signifie qu'une cellule na\u00eet si elle a exactement 3 voisins et survit si elle a 2 ou 3 voisins. Certaines r\u00e8gles ont des noms sp\u00e9cifiques\u2009:

    Nom R\u00e8gle Description Game of Life B3/S23 La r\u00e8gle classique de Conway Mazes B3/S12345 Dessine une sorte de labirynthe Mazectric B3/S1234 Une autre variante HighLife B36/S23 Une variante de Conway Day & Night B3678/S34678 Une autre variante"}, {"location": "course-c/40-algorithms/popular-algorithms/conway/#implementation", "title": "Impl\u00e9mentation", "text": "

    Pour impl\u00e9menter le jeu de la vie de Conway, il faut une grille et un pas temporel pour faire \u00e9voluer les cellules. On peut utiliser un tableau \u00e0 deux dimensions pour repr\u00e9senter la grille. Chaque cellule est repr\u00e9sent\u00e9e par un 0 (mort) ou un 1 (vivant). On peut bien entendu utiliser un tableau de taille fixe ou un tableau dynamique pour repr\u00e9senter la grille.

    A chaque pas de temps, on applique les r\u00e8gles de transition pour chaque cellule. On peut utiliser un tableau temporaire pour stocker les nouvelles valeurs des cellules. On peut aussi utiliser un seul tableau pour stocker les valeurs actuelles et futures des cellules. Il suffit de basculer entre les deux tableaux \u00e0 chaque pas de temps.

    La complexit\u00e9 de l'algorithme est en \\(O(n^2)\\) o\u00f9 \\(n\\) est le nombre de cellules dans la grille. On sait que ce type d'algorithme est tr\u00e8s gourmand en ressources et on est en droit de se demander s'il est possible de faire mieux.

    L'algorithme de Hashlife est une optimisation de l'algorithme de Conway qui permet de r\u00e9duire la complexit\u00e9 de l'algorithme \u00e0 \\(O(n \\log n)\\). Il est bas\u00e9 sur une structure de donn\u00e9es appel\u00e9e quadtree qui permet de stocker les cellules vivantes de mani\u00e8re compacte. L'algorithme de Hashlife est plus complexe \u00e0 impl\u00e9menter mais il permet de g\u00e9rer des grilles de taille importante de mani\u00e8re plus efficace au d\u00e9triments de la complexit\u00e9 de l'algorithme et d'un espace de stockage plus important.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-exp/", "title": "Exponentiation rapide", "text": "

    Cet algorithme permet de calculer rapidement des puissances enti\u00e8res (\\(a^n\\)). La m\u00e9thode na\u00efve consiste \u00e0 calculer les puissances avec une boucle\u2009:

    long long pow(long long a, long long n) {\n    for (int i = 0; i < n - 1; i++) {\n        a *= a;\n    }\n}\n

    La complexit\u00e9 de cet algorithme est \\(O(n)\\). Il est possible de faire mieux en \\(O(n log n)\\).

    long long bin_pow(long long a, long long b) {\n    if (b == 0) return 1;\n    long long res = bin_pow(a, b / 2);\n    return res * res * (b % 2 ? a : 1);\n}\n

    Comme \u00e9voqu\u00e9 plus haut, un algorithme r\u00e9cursif est souvent moins performant que sa variante it\u00e9rative. Voici l'impl\u00e9mentation it\u00e9rative\u2009:

    long long bin_pow(long long a, long long b) {\n    long long res = 1;\n    while (b > 0) {\n        if (b & 1) res = res * a;\n        a *= a;\n        b /= 2;\n    }\n    return res;\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-inverse-square-root/", "title": "Racine carr\u00e9e inverse rapide", "text": "

    Quake III Arena

    Cet algorithme a \u00e9t\u00e9 d\u00e9velopp\u00e9 chez Silicon Graphics au d\u00e9but des ann\u00e9es 90. Il a \u00e9t\u00e9 utilis\u00e9 dans des jeux vid\u00e9os comme Quake III Arena pour am\u00e9liorer la performance du calcul des angles d'incidence dans la r\u00e9flexion des lumi\u00e8res et est attribu\u00e9 \u00e0 John Carmack, un des fondateurs de id Software, qui a publi\u00e9 le code source de Quake III Arena en 2005.

    Il est utilis\u00e9 pour les vecteurs normaux dans les calculs de r\u00e9flexion de la lumi\u00e8re.

    R\u00e9flexion de la lumi\u00e8re

    float Q_rsqrt(float number)\n{\n    const float threehalfs = 1.5F;\n\n    float x2 = number * 0.5F;\n    float y = number;\n    long i = *(long *) &y; // Evil floating point bit level hacking\n    i = 0x5f3759df - (i >> 1); // What the fuck?\n    y = *(float *) &i;\n    y = y * (threehalfs - (x2 * y * y)); // 1st iteration\n#if BETTER\n    y = y * (threehalfs - (x2 * y * y)); // 2nd iteration\n#endif\n    return y;\n}\n

    Cet algorithme de racine carr\u00e9e inverse rapide utilise une constante magique 0x5f3759df. L'impl\u00e9mentation propos\u00e9e ci-dessus est extraite du code source du jeu Quake III arena (q_math.c) disponible sur GitHub.

    Ce n'est pas un algorithme tr\u00e8s acad\u00e9mique, il s'agit d'un kludge, une solution irrespectueuse des r\u00e8gles de l'art de la programmation, car la valeur y est transtyp\u00e9e en un long (i = *(long *)&y. C'est cette astuce qui permet de tirer avantage que les valeurs en virgule flottantes sont exprim\u00e9es en puissances de 2.

    ", "tags": ["long"]}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/", "title": "Fast sin", "text": ""}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#sinus-rapide", "title": "Sinus rapide", "text": "

    Dans des architectures sans support pour les nombres r\u00e9els (comme les processeurs ne supportant pas les op\u00e9rations en virgule flottante IEEE 754), il est courant d'utiliser des approximations polynomiales pour calculer des fonctions math\u00e9matiques comme le sinus. Calculer un sinus n'est pas une simple op\u00e9ration, comme l'addition ou la multiplication, et il n'existe pas d'algorithme trivial pour obtenir une valeur exacte.

    Les processeurs modernes, lorsqu'ils calculent le sinus, utilisent souvent des tables de valeurs pr\u00e9d\u00e9finies (appel\u00e9es tables de sinus) stock\u00e9es en m\u00e9moire. Ces tables contiennent des valeurs pr\u00e9-calcul\u00e9es du sinus pour diff\u00e9rents angles, g\u00e9n\u00e9ralement entre 0 et \\(\\pi/2\\). Lorsqu'un sinus doit \u00eatre calcul\u00e9, le processeur se base sur la valeur dans la table la plus proche de l'angle donn\u00e9, puis utilise une interpolation souvent lin\u00e9aire pour obtenir un r\u00e9sultat plus pr\u00e9cis. Cette m\u00e9thode permet d'\u00e9conomiser du temps de calcul au prix d'une petite perte de pr\u00e9cision. Voici une mani\u00e8re de faire\u2009:

    \\[ \\sin(\\theta) \\approx \\sin(\\theta_1) + \\frac{\\theta - \\theta_1}{\\theta_2 - \\theta_1} \\times (\\sin(\\theta_2) - \\sin(\\theta_1)) \\]

    o\u00f9 \\(\\sin(\\theta_1)\\) et \\(\\sin(\\theta_2)\\) sont les valeurs de sinus les plus proches de \\(\\theta\\) dans la table, \\(\\theta\\) est l'angle pour lequel le sinus doit \u00eatre calcul\u00e9. La recherche dans la table peut \u00eatre simplement une table de hachage pour un acc\u00e8s en \\(O(1)\\). La table serait pr\u00e9-calcul\u00e9e et stock\u00e9e sous forme d'un tableau.

    #include <stdio.h>\n\n#define TABLE_SIZE 1024\n\ndouble sin_table[TABLE_SIZE];\n\n// Computed on a powerful machine (with floating point support)\nvoid init_sin_table() {\n    for (int i = 0; i < TABLE_SIZE; ++i)\n        sin_table[i] = sin((double)i / TABLE_SIZE * M_PI / 2);\n}\n\ndouble sin_fast(double angle) {\n    double x = angle / (M_PI / 2) * TABLE_SIZE;\n    int i = (int)x;\n    double frac = x - i;\n    return sin_table[i] + frac * (sin_table[i + 1] - sin_table[i]);\n}\n

    Dans une architecture l\u00e9g\u00e8re qui ne dispose pas de support pour les nombres en virgule flottante, on utilise plut\u00f4t des approximations en virgule fixe. Ces approximations polynomiales, comme le d\u00e9veloppement de Taylor, permettent de calculer des sinus avec une pr\u00e9cision acceptable, tout en restant dans le domaine des entiers.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#approximation-du-cinquieme-ordre", "title": "Approximation du cinqui\u00e8me ordre", "text": "

    L'objectif p\u00e9dagogique est de montrer que les math\u00e9matiques peuvent \u00eatre tr\u00e8s utiles dans l'\u00e9laboration d'un algorithme. Nous allons voir comment approximer un sinus en utilisant un polyn\u00f4me de degr\u00e9 5 en utilisant les technologies suivantes\u2009:

    • Syst\u00e8mes d'\u00e9quations lin\u00e9aires pour r\u00e9soudre les coefficients du polyn\u00f4me\u2009;
    • Calcul int\u00e9gral pour minimiser l'erreur moyenne (moindre carr\u00e9s);
    • Virgule fixe pour repr\u00e9senter les nombres en m\u00e9moire.

    Le domaine du sinus est infini mais il est possible de le r\u00e9duire \u00e0 l'intervalle ci-dessous car toutes les autres sorties peuvent \u00eatre obtenues en utilisant les propri\u00e9t\u00e9s de sym\u00e9trie du sinus. En effet il suffit du dessin du quart de sinus pour obtenir le sinus complet.

    \\[ x \\in [0, \\frac{\\pi}{2}] \\]

    D'autre part, le sinus est une fonction impaire, c'est-\u00e0-dire que \\(\\sin(-x) = -\\sin(x)\\). Cette propri\u00e9t\u00e9 induit, par exemple lors d'une d\u00e9composition en s\u00e9rie de Fourier, que les coefficients de la s\u00e9rie pour les termes pairs sont nuls. Cela signifie aussi que le sinus peut \u00eatre approxim\u00e9 par un polyn\u00f4me de degr\u00e9 impair. On peut donc tenter d'approximer un sinus par un polyn\u00f4me de degr\u00e9 5 en ayant seulement 3 coefficients \u00e0 calculer. D'autre part, afin de faciliter les calculs et la repr\u00e9sentation en virgule fixe on peut r\u00e9duire l'intervalle de calcul \u00e0 \\([0, 1]\\) en posant que\u2009:

    \\[ z = \\frac{2x}{\\pi} \\]

    Le polyn\u00f4me de degr\u00e9 5 peut \u00eatre exprim\u00e9 de la mani\u00e8re suivante\u2009:

    \\[ \\sin(z) \\approx S_5(z) = cz - bz^**3** + az^5 \\]

    Pour trouver les coefficients de ce polyn\u00f4me, on peut utiliser les propri\u00e9t\u00e9s du sinus et de ses d\u00e9riv\u00e9es. Nous allons donc raisonner sur ces deux fonctions\u2009:

    \\[ \\begin{cases} S_5(z) = az^5 - bz^3 + cz \\\\ S_5'(z) = 5az^4 - 3bz^2 + c \\end{cases} \\]

    On peut noter quelques conditions aux bords de l'intervalle \\([0, 1]\\) :

    • \\(S_5(1) = 1\\) qui est \u00e9quivalent \u00e0 \\(\\sin(\\frac{\\pi}{2}) = 1\\);
    • \\(S_5'(1) = 0\\) car la pente est nulle en son sommet\u2009;
    • \\(S_5'(0) = \\frac{\\pi}{2}\\) car la pente est maximale en 0.

    Ces conditions nous permettent d'obtenir trois \u00e9quations lin\u00e9aires qu'il est trivial de r\u00e9soudre\u2009:

    \\[ \\begin{cases} 1 = a - b + c \\\\ 0 = 5a - 3b + 5c \\\\ \\frac{\\pi}{2} = a \\end{cases} \\]

    Ce qui nous donne les coefficients suivants\u2009:

    \\[ a = \\frac{\\pi}{2},\\quad b = \\pi-\\frac{5}{2},\\quad c = \\frac{\\pi}{2}-\\frac{3}{2} \\]

    Que l'on peut simplifier en fonction de \\(a\\) :

    \\[ a = \\frac{\\pi}{2},\\quad b = 2a-\\frac{5}{2},\\quad c = a-\\frac{3}{2} \\]

    Cette solution triviale n'est g\u00e9n\u00e9ralement pas optimale car un crit\u00e8re fondamental n'a pas \u00e9t\u00e9 respect\u00e9, celui de minimiser l'erreur moyenne avec le sacrifice potentiel de la pr\u00e9cision des valeurs extr\u00eames. Une approche plus rigoureuse consiste donc minimiser l'erreur moyenne sur l'intervalle \\([0, 1]\\). On utilise pour ce faire la m\u00e9thode des moindre carr\u00e9s qui consiste \u00e0 minimiser l'erreur quadratique est la somme des carr\u00e9s des diff\u00e9rences entre la fonction cible \\( \\sin\\left(\\frac{\\pi}{2}x\\right) \\) et l'approximation polynomiale \\( p(x) \\). Pour ce faire, on calcule l'int\u00e9grale suivante\u2009:

    \\[ E(a, b, c) = \\int_0^1 \\left( \\sin\\left(\\frac{\\pi}{2}x\\right) - (az^5 - bz^3 + cz) \\right)^2 dx \\]

    Pour minimiser l'erreur nous avons besoin de trouver les coefficients \\(a\\), \\(b\\) et \\(c\\) qui minimisent cette int\u00e9grale. Cela revient \u00e0 r\u00e9soudre en prenant les d\u00e9riv\u00e9es partielles de l'erreur quadratique \\(E(a, b, c)\\) par rapport \u00e0 \\(a\\), \\(b\\) et \\(c\\). Cela nous donne trois \u00e9quations\u2009:

    \\[ \\begin{cases} \\frac{\\partial E}{\\partial a} = 0 \\\\ \\frac{\\partial E}{\\partial b} = 0 \\\\ \\frac{\\partial E}{\\partial c} = 0 \\end{cases} \\]

    Ces trois \u00e9quations sont ind\u00e9pendantes, ce qui signifie qu'il est possible de r\u00e9soudre ce syst\u00e8me.

    Pratiquement on aura plut\u00f4t recours \u00e0 une r\u00e9solution num\u00e9rique par exemple avec Python\u2009:

    import numpy as np\nfrom scipy.integrate import quad\nfrom scipy.linalg import solve\n\n\ndef sin_pi_over_2(x):\n    return np.sin(np.pi * x / 2)\n\n\ndef z_power_n(n, x):\n    return x**n\n\n\nintegrals_matrix = np.zeros((3, 3))\nfor i, n1 in enumerate([5, 3, 1]):\n    for j, n2 in enumerate([5, 3, 1]):\n        integrals_matrix[i, j] = quad(lambda x: z_power_n(n1 + n2, x), 0, 1)[0]\n\nintegrals_rhs = np.zeros(3)\nfor i, n in enumerate([5, 3, 1]):\n    integrals_rhs[i] = quad(lambda x: sin_pi_over_2(x) * z_power_n(n, x), 0, 1)[0]\n\na, b, c = solve(integrals_matrix, integrals_rhs)\nprint(a, b, c)\n

    Ce qui nous donnes les coefficients suivants\u2009:

    \\[ a = 1.5704372550337553, \\quad b = 0.6427098943803898, \\quad c = 0.07243339903924052 \\]

    Une autre approche (j'ai toujours pas compris) est la suivante\u2009:

    \\[ a = 4(\\frac{3}{\\pi}-\\frac{9}{16}), \\quad b = 2a-\\frac{5}{2}, \\quad c = a-\\frac{3}{2} \\]

    Sur une architecture en virgule fixe comme un microcontr\u00f4leur MSP430 16-bit, on peut par exemple utiliser un entier de 16-bit pour repr\u00e9senter un angle. Ce dernier peut \u00eatre exprim\u00e9 en radians en compl\u00e9ment \u00e0 deux et en format Q15.1. Cela signifie que la partie enti\u00e8re repr\u00e9sente les radians et la partie fractionnaire repr\u00e9sente les d\u00e9cimales. Par exemple, 1.0 en Q15.1 est repr\u00e9sent\u00e9 par 0x8000. En termes binaire notre \u00e9quation devra \u00eatre multipli\u00e9e par \\(2^{15}\\) pour obtenir un r\u00e9sultat correct. On peut rendre g\u00e9n\u00e9rique cette d\u00e9finition en d\u00e9clarant \\(o\\) l'exponent de la puissance de 2 \u00e0 appliquer. Notre \u00e9quation compl\u00e8te est donc\u2009:

    \\[ fpsin_5(x) = \\left. 2^o\\left(az - bz^3 + cz^5\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Or, l'\u00e9quation doit pouvoir \u00eatre r\u00e9\u00e9crite seulement en terme de multiplications d'entiers, d'additions et de d\u00e9calages. En outre, il est essentiel de factoriser au maximum l'\u00e9quation pour \u00e9viter tout calcul redondant. On commence par factoriser l'\u00e9quation\u2009:

    \\[ fpsin_5(x) = \\left. z2^o\\left(a - z^2\\left(b + cz^2\\right)\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Comme \\(z\\in[0, 1]\\) n'est pas repr\u00e9sentable par un entier, on peut r\u00e9\u00e9crire \\(z\\) comme \\(y/2^o\\) avec \\(y\\in[0, 2^o]\\) et o\u00f9 \\(o\\) est le nombre de bits de la partie fractionnaire du domaine d'entr\u00e9e. On obtient alors\u2009:

    \\[ fpsin_5(x) = \\left. \\left[\\frac{y}{2^n}\\right]2^o\\left(a - \\left[\\frac{y}{2^n}\\right]^2\\left(b + c\\left[\\frac{y}{2^n}\\right]^2\\right)\\right)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Apr\u00e8s simplification on obtient\u2009:

    \\[ fpsin_5(x) = \\left. y2^{o-n}\\Big(a-y2^{-n}y2^{-n}\\left(b-y2^{-2n}cy\\right)\\Big)\\right|_{z = \\frac{2x}{\\pi}} \\]

    Lors d'une multiplication en virgule fixe, on s'int\u00e9resse \u00e0 la partie haute de la multiplication. En effet, pour un produit standard de deux entiers 8-bits, le r\u00e9sultat est un entier 16-bits. C'est d'ailleurs la raison pour laquelle les ALU offrent un registre de r\u00e9sultat deux fois plus grand que les registres d'entr\u00e9e. N\u00e9anmoins, ce sont les 8-bits de poids faible qui sont conserv\u00e9s car si la multiplication n'a pas de d\u00e9passement, le r\u00e9sultat tiendra dans les 8-bits de poids faible. On supprime donc les 8-bit de pods fort. Or, dans un calcul en virgule fixe, la logique est diff\u00e9rente. Un nombre en Q1.7 peut exprimer des grandeurs entre -1 et 1 avec une pr\u00e9cision de 1/128. En multipliant deux nombre en Q1.7, on obtient un r\u00e9sultat en Q2.14. Or, dans ce r\u00e9sultat 16-bit, l'octet de poid faible ne contient que les chiffres apr\u00e8s la virgule et none la partie int\u00e9ressante du calcul. On peut donc supprimer cet octet de poids faible et ne conserver que les 8-bits de poids fort formant un nombre en Q2.6. En C standard, il n'est pas possible d'ordonner au compilateur de choisir le mot de poids fort ou faible. En assembleur en revanche de nombreux processeurs offrent cette possibilit\u00e9s. Un ADSP-218x par exemple offre des instructions de multiplication en virgule fixe avec un mot de r\u00e9sultat de 32-bits. On peut alors choisir de conserver le mot de poids fort ou faible.

    Une m\u00e9thode pour maximiser la pr\u00e9cision des calculs est d'ajouter des facteurs d'\u00e9chelle. Par exemple, le coefficient \\(a\\) vaut environ \\(1.57\\) ce qui repr\u00e9sente pour la partie enti\u00e8re 1 bit. Avec une ALU d'une profondeur \\(m\\): 32-bits, on peut se permettre de d\u00e9caler \u00e0 gauche ce nombre tel que l'\u00e9quation suivante est satisfaite\u2009:

    \\[ \\text{sign}(a)\\cdot\\lceil |\\log_2(a)| \\rceil + k <= m \\]

    o\u00f9 \\(k\\) est le d\u00e9calage en bits.

    Si l'on ajoute un facteur d'\u00e9chelle, il doit n\u00e9cessairement \u00eatre compens\u00e9 dans l'\u00e9quation. Commencon\u00e7

    en virgule fixe on a int\u00e9r\u00eat \u00e0 le multiplier

    Afin de maximiser la pr\u00e9cision, on a besoin que nos multiplications occupent le plus de bits possibles pour le type choisi. \u00c0 cette fin, on peut introduire des facteurs d'\u00e9chelle \\(2^p\\), \\(2^q\\) et \\(2^r\\) sachant qu'ils s'annuleront et n'affecterons pas le r\u00e9sultat final. On peut alors r\u00e9\u00e9crire l'\u00e9quation en introduisant ces facteurs d'\u00e9chelle\u2009:

    \\[ \\begin{aligned} = y2^{p-n}\\left(a-y2^{-n}y{2^-n}\\left[b-2^{-r}y2^{-2n}2^rcy\\right]\\right)2^{a}\\\\ = y2^{p-n}\\left(a-2^{-p}y2^{-n}y{2^-n}\\left[2^Pb-2^{-r}y2^{-2n}2^{r+p}cy\\right]\\right)2^{a}\\\\ = y2^{-n}\\left(2^qa-2^{q-p}y2^{-n}\\left[2^Pb-2^{-r}y2^{-n}2^{r+p-n}cy\\right]\\right)2^{a-q}\\\\ \\end{aligned} \\]

    En red\u00e9finissant les constantes \\(A\\), \\(B\\) et \\(C\\) comme suit\u2009:

    \\[ A = 2^qa B = 2^Pb C = 2^{r+p-n}c \\]

    On obtient l'\u00e9quation finale\u2009:

    \\[ = y2^{-n}\\left(A-2^{q-p}y2^{-n}y2^{-n}\\left[B-2^{-r}y2^{-n}Cy\\right]\\right)2^{a-q} \\]

    On peut maintenant essayer de maximiser chaque multiplication en travaillant depuis l'int\u00e9rieur de l'\u00e9quation en direction de l'ext\u00e9rieur de fa\u00e7on \u00e0 ce que le produit soit exactement 32-bit. On peut \u00e9galement ajuster l'\u00e9chelle du r\u00e9sultat en virgule fixe pour obtenir une pr\u00e9cision maximale. Le r\u00e9sultat final est\u2009:

    a = 12;\nn = 13;\np = 32;\nq = 31;\nr = 3;\n\nA = 3370945099;\nB = 2746362156;\nC = 292421;\n

    Enfin, on peut impl\u00e9menter cette approximation en C\u2009:

    #include <stdio.h>\n#include <stdint.h>\n#include <math.h>\n\nint16_t fpsin(int16_t i) {\n   i <<= 1;\n   uint8_t c = i < 0;\n\n   if (i == (i | 0x4000)) i = (1 << 15) - i;\n   i = (i & 0x7FFF) >> 1;\n\n   enum { A1 = 3370945099UL, B1 = 2746362156UL, C1 = 292421UL };\n   enum { n = 13, p = 32, q = 31, r = 3, a = 12 };\n\n   uint32_t y = (C1 * ((uint32_t)i)) >> n;\n   y = B1 - (((uint32_t)i * y) >> r);\n   y = (uint32_t)i * (y >> n);\n   y = (uint32_t)i * (y >> n);\n   y = A1 - (y >> (p - q));\n   y = (uint32_t)i * (y >> n);\n   y = (y + (1UL << (q - a - 1))) >> (q - a);\n\n   return c ? -y : y;\n}\n\nint main() {\n    // Quelques angles : (0, pi/2, pi, 3pi/2, 2pi)\n    const int16_t angles[] = {0, 16384, 32768, 49152, 65536};\n\n    printf(\"Angle (deg)\\tFixed-Point Sin\\tStandard Sin\\tError\\n\");\n    for (int i = 0; i < 5; ++i) {\n        int16_t angle = angles[i];\n        double radians = (double)angle / 65536.0 * M_PI;\n\n        int16_t fpsin_result = fpsin(angle);\n        double sin_result = sin(radians);\n        double sin_fixed_point = sin_result * 4096.0;\n        double error = fpsin_result - sin_fixed_point;\n\n        printf(\"%d\\t\\t%d\\t\\t%.2f\\t\\t%.2f\\n\", (int)(radians * 180.0 / M_PI),\n            fpsin_result, sin_fixed_point, error);\n    }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/fast-sin/#demonstration-graphique-avec-sdl", "title": "D\u00e9monstration graphique avec SDL", "text": "

    Voici un exemple qui utilise SDL pour afficher un graphe montrant le sinus en virgule fixe et l'erreur par rapport \u00e0 la fonction sinus standard.

    #include <SDL2/SDL.h>\n#include <math.h>\n\n#define SCREEN_WIDTH 640\n#define SCREEN_HEIGHT 480\n\nvoid draw_graph(SDL_Renderer *renderer) {\n    // Dessine le sinus en virgule fixe et la version standard\n    for (int x = 0; x < SCREEN_WIDTH; x++) {\n        // Mappe x \u00e0 un angle (entre 0 et 2pi)\n        double angle = (double)x / SCREEN_WIDTH * 2.0 * M_PI;\n        int y_fixed = (int)((fpsin((int16_t)(angle * 65536.0 / (2.0 * M_PI))) / 4096.0) * SCREEN_HEIGHT / 2 + SCREEN_HEIGHT / 2);\n        int y_standard = (int)((sin(angle) * SCREEN_HEIGHT / 2) + SCREEN_HEIGHT / 2);\n\n        // Sinus standard en rouge\n        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);\n        SDL_RenderDrawPoint(renderer, x, y_standard);\n\n        // Sinus en virgule fixe en vert\n        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);\n        SDL_RenderDrawPoint(renderer, x, y_fixed);\n\n        // Erreur en bleu\n        int error = y_fixed - y_standard;\n        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);\n        SDL_RenderDrawPoint(renderer, x, SCREEN_HEIGHT / 2 - error);\n    }\n}\n\nint main(int argc, char *argv[]) {\n    SDL_Window *window = NULL;\n    SDL_Renderer *renderer = NULL;\n\n    if (SDL_Init(SDL_INIT_VIDEO) < 0) {\n        printf(\"SDL could not initialize! SDL_Error: %s\\n\", SDL_GetError());\n        return 1;\n    }\n\n    window = SDL_CreateWindow(\"Sinus Approximation\", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);\n    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n\n    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);\n    SDL_RenderClear(renderer);\n\n    draw_graph(renderer);\n    SDL_RenderPresent(renderer);\n\n    SDL_Event e;\n    int quit = 0;\n    while (!quit) {\n        while (SDL_PollEvent(&e) != 0) {\n            if (e.type == SDL_QUIT) {\n                quit = 1;\n            }\n        }\n    }\n\n    SDL_DestroyRenderer(renderer);\n    SDL_DestroyWindow(window);\n    SDL_Quit();\n\n    return 0;\n}\n

    from sympy import symbols, Eq, solve, pi

    a, b, c = symbols('a b c')

    solution = solve([ Eq(a - b + c, 1), Eq(a - 3*b + 5*c, 0), Eq(a/2 - b/4 + c/6, 2/pi) ], (a, b, c))

    solution

    "}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/", "title": "Codage de Huffman", "text": "

    Le codage de Huffman est un algorithme de compression sans perte qui permet de r\u00e9duire la taille des fichiers en utilisant des codes de longueur variable pour repr\u00e9senter les caract\u00e8res. L'algorithme repose sur l'id\u00e9e que les caract\u00e8res les plus fr\u00e9quents dans un texte peuvent \u00eatre repr\u00e9sent\u00e9s par des codes plus courts, tandis que les caract\u00e8res les moins fr\u00e9quents sont repr\u00e9sent\u00e9s par des codes plus longs.

    Il est utilis\u00e9 dans de nombreux formats de fichiers comme le format PNG, JPEG et MP3.

    Prenons le texte ABRACADABRA. Il y a des lettres qui reviennent plus souvent que d'autres et des lettres de l'alphabet qui sont absentes. Pourquoi donc repr\u00e9senter chaque caract\u00e8re sur 1 octet\u2009? On pourrait utiliser un code de longueur variable. Par exemple, la lettre A pourrait \u00eatre repr\u00e9sent\u00e9e par 0, la lettre B par 10 et la lettre R par 11. Il faudrait \u00e9galement d\u00e9finir une table de correspondance pour d\u00e9coder le texte. C'est le principe de l'abre de Huffman.

    ", "tags": ["ABRACADABRA"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#comment-ca-marche", "title": "Comment \u00e7a marche\u2009?", "text": "

    Pour notre entr\u00e9e ABRACADABRA, nous allons suivre les \u00e9tapes suivantes\u2009:

    ", "tags": ["ABRACADABRA"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#nombre-doccurences", "title": "Nombre d'occurences", "text": "

    On commence par compter la fr\u00e9quence de chaque caract\u00e8re. On obtient\u2009:

    Fr\u00e9quence de Huffman Caract\u00e8re Fr\u00e9quence A 5 B 2 R 2 C 1 D 1

    Chaque \u00e9l\u00e9ment est un noeud qui est plac\u00e9 dans une file de priorit\u00e9 (min-heap) o\u00f9 la priorit\u00e9 est la fr\u00e9quence du caract\u00e8re. Voici le pseudo code du tas minimum\u2009:

    Min-Heap : [(1, 'C'), (1, 'D'), (2, 'B'), (2, 'R'), (5, 'A')]\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#fusion", "title": "Fusion", "text": "

    On va fusionner les deux noeuds de plus faible fr\u00e9quence, c'est facile parce qu'un min-heap nous permet de r\u00e9cup\u00e9rer les deux \u00e9l\u00e9ments de plus faible fr\u00e9quence en temps constant. Apr\u00e8s fusion, on obtient une cha\u00eene de caract\u00e8re qui repr\u00e9sente les deux noeuds fusionn\u00e9s. On ajoute ce nouveau noeud \u00e0 la file de priorit\u00e9 en prenant en compte la fr\u00e9quence totale des deux noeuds fusionn\u00e9s.

    Notons que qu'en cas de priorit\u00e9 \u00e9gale, on peut choisir arbitrairement l'ordre des noeuds.

    Les noeuds C et D sont les deux noeuds de plus faible fr\u00e9quence. On les fusionne pour obtenir CD qui a une priorit\u00e9 de 2.

    Min-Heap : [(2, 'CD'), (2, 'B'), (2, 'R'), (5, 'A')]\n
    graph TD\nCD(\"CD (2)\") --> C(\"C (1)\")\nCD --> D(\"D (1)\")

    Il nous reste des noeuuds \u00e0 fusionner. On fusionne les noeuds CD et B pour obtenir CDB qui a une priorit\u00e9 de 4.

    Min-Heap : [(2, 'R'), (4, 'CDB'), (5, 'A')]\n
    graph TD\n    CDB(\"CDB (4)\") --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")

    On continue car il nous reste des noeuds \u00e0 fusionner. On fusionne les noeuds CDB et R pour obtenir CDBR qui a une priorit\u00e9 de 6.

    Min-Heap : [(5, 'A'), (6, 'RCDB')]\n
    graph TD\n    RCDB(\"RCDB (6)\") --> R(\"R (2)\")\n    RCDB --> CDB(\"CDB (4)\")\n    CDB --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")

    Enfin, on fusionne les noeuds RCDB et A pour obtenir ACDBR qui a une priorit\u00e9 de 11.

    Min-Heap : [(11, 'ARCDB')]\n
    graph TD\n    ARCDB(\"ARCDB (11)\") --> A(\"A (5)\")\n    ARCDB --> RCDB(\"RCDB (6)\")\n    RCDB --> R(\"R (2)\")\n    RCDB --> CDB(\"CDB (4)\")\n    CDB --> CD(\"CD (2)\")\n    CDB --> B(\"B (2)\")\n    CD --> C(\"C (1)\")\n    CD --> D(\"D (1)\")
    ", "tags": ["ACDBR", "CDBR", "RCDB", "CDB"]}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#generation-des-codes", "title": "G\u00e9n\u00e9ration des codes", "text": "

    Pour g\u00e9n\u00e9rer les codes, on parcourt l'arbre de Huffman en partant de la racine. On ajoute un 0 \u00e0 chaque fois qu'on descend \u00e0 gauche et un 1 \u00e0 chaque fois qu'on descend \u00e0 droite. Les noeuds fusionn\u00e9s sont des noeuds internes, on ne les prend pas en compte.

    Caract\u00e8re Code A 0 R 10 B 111 C 1100 D 1101"}, {"location": "course-c/40-algorithms/popular-algorithms/huffman/#encodage-du-texte", "title": "Encodage du texte", "text": "

    Une fois les codes g\u00e9n\u00e9r\u00e9s, on peut encoder le texte en rempla\u00e7ant chaque caract\u00e8re par son code.

    A B   R  A C    A C    A B   R  A\n0 111 10 0 1100 0 1100 0 111 10 0\n

    Comme les donn\u00e9es sont n\u00e9cessairement align\u00e9es sur des octets en m\u00e9moire, il est possible que le dernier octet contienne des bits inutilis\u00e9s, on les remplace par des z\u00e9ros.

    01111001'10001100'01111000\n                         - Remplissage (padding)\n

    Bien entendu il est n\u00e9cessaire d'encoder \u00e9galement la table de Huffmann pour pouvoir d\u00e9coder le texte. Une m\u00e9thode courante est d'encoder directement la table de Huffmann dans le fichier compress\u00e9.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/", "title": "Algorithmes d'infographie", "text": "

    Les algorithmes d'infographie sont des algorithmes utilis\u00e9s pour g\u00e9n\u00e9rer des images num\u00e9riques. Ils sont utilis\u00e9s dans de nombreux domaines, tels que les jeux vid\u00e9o, les films d'animation, la r\u00e9alit\u00e9 virtuelle, etc.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#bressenham", "title": "Bressenham", "text": "

    L'algorithme de Bresenham est une m\u00e9thode efficace pour tracer des lignes droites sur une grille de pixels en utilisant uniquement des op\u00e9rations enti\u00e8res. Il est particuli\u00e8rement adapt\u00e9 aux \u00e9crans d'ordinateur o\u00f9 les positions des pixels sont discr\u00e8tes. L'algorithme d\u00e9termine quels pixels doivent \u00eatre allum\u00e9s pour former la meilleure approximation possible d'une ligne droite entre deux points.

    Algorithme de Bressenham

    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#principe-de-lalgorithme", "title": "Principe de l'algorithme", "text": "

    Consid\u00e9rons le trac\u00e9 d'une ligne entre deux points\u2009:

    $\\(p=(x_0, y_0), q=(x_1, y_1)\\)

    Tel que pour chaque position \\(x\\), la valeur de \\(y\\) qui correspond le mieux \u00e0 la ligne id\u00e9ale.

    L'algorithme utilise une variable d'erreur pour d\u00e9cider quand incr\u00e9menter \\(y\\). Cette variable repr\u00e9sente la distance entre la position r\u00e9elle de la ligne id\u00e9ale et la position actuelle sur la grille de pixels. L'\u00e9quation de la variable d'erreur est mise \u00e0 jour \u00e0 chaque it\u00e9ration pour refl\u00e9ter cette distance. Cet algorithme est un outil puissant pour le trac\u00e9 efficace de lignes sur une grille de pixels. En utilisant uniquement des op\u00e9rations enti\u00e8res, il optimise les performances, ce qui est crucial pour les applications graphiques en temps r\u00e9el.

    void bresenhamLine(Point p, Point q, SDL_Renderer *renderer) {\n   int dx = abs(q.x - p.x), dy = abs(q.y - p.y);\n   int sx = p.x < q.x ? 1 : -1, sy = p.y < q.y ? 1 : -1;\n   int err = dx - dy;\n\n   for (;;) {\n      setPixel(renderer, p.x, p.y, 1.0f);\n      if (p.x == q.x && p.y == q.y) break;\n      int e2 = 2 * err;\n      if (e2 > -dy) {\n         err -= dy;\n         p.x += sx;\n      }\n      if (e2 < dx) {\n         err += dx;\n         p.y += sy;\n      }\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#xiaolin-wu", "title": "Xiaolin Wu", "text": "

    L'algorithme de Xiaolin Wu est une m\u00e9thode am\u00e9lior\u00e9e pour le trac\u00e9 d'antialiasing de lignes. Il utilise des techniques de subpixel pour rendre les lignes plus lisses et plus pr\u00e9cises. L'algorithme de Xiaolin Wu est bas\u00e9 sur l'algorithme de Bresenham, mais il ajoute des \u00e9tapes suppl\u00e9mentaires pour g\u00e9rer les valeurs de couleurs partielles des pixels. Il fut publi\u00e9 en 1991 dans le journal Computer Graphics (An Efficient Antialiasing Technique).

    Algorithme de Xiaolin Wu

    void wuLine(Point p0, Point p1, SDL_Renderer *renderer) {\n   bool steep = abs(p1.y - p0.y) > abs(p1.x - p0.x);\n\n   if (steep) {\n      swapXY(&p0);\n      swapXY(&p1);\n   }\n\n   if (p0.x > p1.x) swapPoints(&p0, &p1);\n\n   int dx = p1.x - p0.x;\n   int dy = p1.y - p0.y;\n   float gradient = dx == 0 ? 1 : (float)dy / (float)dx;\n\n   // Premi\u00e8re extr\u00e9mit\u00e9\n   float xEnd = p0.x;\n   float yEnd = p0.y + gradient * (xEnd - p0.x);\n   float xGap = 1.0f;\n   int xPixel1 = (int)xEnd;\n   int yPixel1 = (int)yEnd;\n\n   if (steep) {\n      setPixel(renderer, yPixel1, xPixel1, (1 - (yEnd - yPixel1)) * xGap);\n      setPixel(renderer, yPixel1 + 1, xPixel1, (yEnd - yPixel1) * xGap);\n   } else {\n      setPixel(renderer, xPixel1, yPixel1, (1 - (yEnd - yPixel1)) * xGap);\n      setPixel(renderer, xPixel1, yPixel1 + 1, (yEnd - yPixel1) * xGap);\n   }\n\n   float intery = yEnd + gradient;\n\n   // Deuxi\u00e8me extr\u00e9mit\u00e9\n   xEnd = p1.x;\n   yEnd = p1.y + gradient * (xEnd - p1.x);\n   xGap = 1.0f;\n   int xPixel2 = (int)xEnd;\n   int yPixel2 = (int)yEnd;\n\n   if (steep) {\n      setPixel(renderer, yPixel2, xPixel2, (1 - (yEnd - yPixel2)) * xGap);\n      setPixel(renderer, yPixel2 + 1, xPixel2, (yEnd - yPixel2) * xGap);\n   } else {\n      setPixel(renderer, xPixel2, yPixel2, (1 - (yEnd - yPixel2)) * xGap);\n      setPixel(renderer, xPixel2, yPixel2 + 1, (yEnd - yPixel2) * xGap);\n   }\n\n   // Tracer les pixels entre les deux extr\u00e9mit\u00e9s\n   if (steep) {\n      for (int x = xPixel1 + 1; x < xPixel2; ++x) {\n         setPixel(renderer, (int)intery, x, (1 - (intery - (int)intery)));\n         setPixel(renderer, (int)intery + 1, x, intery - (int)intery);\n         intery += gradient;\n      }\n   } else {\n      for (int x = xPixel1 + 1; x < xPixel2; ++x) {\n         setPixel(renderer, x, (int)intery, (1 - (intery - (int)intery)));\n         setPixel(renderer, x, (int)intery + 1, intery - (int)intery);\n         intery += gradient;\n      }\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#courbes-de-bezier", "title": "Courbes de B\u00e9zier", "text": "

    Les courbes de B\u00e9zier sont des courbes math\u00e9matiques utilis\u00e9es pour repr\u00e9senter des formes lisses et r\u00e9guli\u00e8res. Elles sont largement utilis\u00e9es en infographie pour le trac\u00e9 de courbes.

    Courbes de B\u00e9zier

    Le calcul de B\u00e9zier est donn\u00e9 par l'algorithme suivant\u2009:

    Point bezier(Point p, Point c, Point q, float t) {\n   const float it = 1.f - t;\n   return (Point){.x = it * it * p.x + 2 * it * t * c.x + t * t * q.x,\n                  .y = it * it * p.y + 2 * it * t * c.y + t * t * q.y};\n}\n

    Le point \\(p\\) est le point de d\u00e9part de la courbe, le point \\(q\\) est le point d'arriv\u00e9e et le point \\(c\\) est le point de contr\u00f4le. La valeur de \\(t\\) varie de 0 \u00e0 1 pour d\u00e9terminer la position le long de la courbe.

    En pratique, la valeur \\(t\\) est incr\u00e9ment\u00e9e \u00e0 chaque it\u00e9ration pour tracer des tron\u00e7ons de la courbes par des segments de droite.

    void drawBezierCurve(SDL_Renderer *renderer, Point p, Point q, Point c) {\n   const float precision = 0.01;\n   Point prev = p;\n   for (float t = 0.0; t <= 1.0; t += precision) {\n      Point current = bezier(p, c, q, t);\n      draw_line(prev, current);\n      prev = current;\n   }\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/infography/#floyd-stenberg", "title": "Floyd-Stenberg", "text": ""}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/", "title": "L-Syst\u00e8me (Lindenmayer System)", "text": "

    Un L-Syst\u00e8me est un syst\u00e8me de r\u00e9\u00e9criture de cha\u00eenes de caract\u00e8res invent\u00e9 en 1968 par le biologiste hongrois Astrid Lindenmayer. Il est utilis\u00e9 pour mod\u00e9liser la croissance des plantes, la morphogen\u00e8se, la fractale, etc. On peut d\u00e9finir ce syt\u00e8me comme une forme de grammaire g\u00e9n\u00e9rative.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#definition", "title": "D\u00e9finition", "text": "

    Un L-Syst\u00e8me est d\u00e9fini par un alphabet, un axiome et un ensemble de r\u00e8gles de r\u00e9\u00e9criture. L'axiome est la cha\u00eene de d\u00e9part. Les r\u00e8gles de r\u00e9\u00e9criture d\u00e9finissent comment les symboles de l'axiome sont remplac\u00e9s par d'autres symboles.

    Nous avons donc\u2009:

    Axiome

    C'est le point de d\u00e9part de la r\u00e9\u00e9criture. C'est une cha\u00eene de caract\u00e8res.

    R\u00e8gles de production

    Chaque r\u00e8gle s\u00e9par\u00e9e par des virgules d\u00e9finissent comment un symbole est remplac\u00e9 par une autre cha\u00eene de caract\u00e8res.

    Angle

    C'est l'angle de rotation pour les symboles + et -. \u00c0 chaque fois que l'on rencontre un +, on tourne \u00e0 droite de l'angle. \u00c0 chaque fois que l'on rencontre un -, on tourne \u00e0 gauche de l'angle. Les angles peuvent \u00eatre cumulatifs\u2009: ++ signifie tourner deux fois \u00e0 droite.

    It\u00e9rations

    C'est le nombre de fois que l'on applique les r\u00e8gles de r\u00e9\u00e9criture \u00e0 l'axiome.

    Longueur

    C'est la longueur de chaque segment de la courbe pour chaque symbole.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#grammaire", "title": "Grammaire", "text": "

    En 3 dimensions, on dispose des coordonn\u00e9es \\(x\\), \\(y\\) et \\(z\\) ainsi que des rotations \\(\\alpha\\), \\(\\beta\\) et \\(\\gamma\\).

    • + : tourner \u00e0 droite autour de l'axe \\(z\\) de l'angle \\(\\alpha\\)
    • - : tourner \u00e0 gauche autour de l'axe \\(z\\) de l'angle \\(\\alpha\\)
    • & : tourner en bas autour de l'axe \\(y\\) de l'angle \\(\\beta\\)
    • ^ : tourner en haut autour de l'axe \\(y\\) de l'angle \\(\\beta\\)
    • \\ : tourner \u00e0 droite autour de l'axe \\(x\\) de l'angle \\(\\gamma\\)
    • / : tourner \u00e0 gauche autour de l'axe \\(x\\) de l'angle \\(\\gamma\\)
    • | : tourner de 180\u00b0 autour de l'axe \\(z\\), correspond \u00e0 ++ ou --.
    • [ : sauvegarder la position et l'angle
    • ] : restaurer la position et l'angle
    • Les lettres sont des symboles de dessin
    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#exemple", "title": "Exemple", "text": "

    Prenons cette d\u00e9finition\u2009:

    name: Koch snowflake\naxiom: F\nangle: 90\niterations: 3\nlength: 20\nrules:\n  F: F+F-F-F+F\n

    L'axiome est F. On a une r\u00e8gle de r\u00e9\u00e9criture F -> F+F-F-F+F. On applique cette r\u00e8gle 3 fois.

    1. It\u00e9ration 0\u2009: F
    2. It\u00e9ration 1\u2009: F+F-F-F+F
    3. It\u00e9ration 2\u2009: F+F-F-F+F + F+F-F-F+F - F+F-F-F+F - F+F-F-F+F + F+F-F-F+F
    4. Et ainsi de suite...

    Imaginez que vous tenez un crayon et que vous suivez les instructions pour dessiner la courbe. \u00c0 chaque F, vous avancez de 20 pixels. \u00c0 chaque +, vous tournez de 90\u00b0 \u00e0 droite. \u00c0 chaque -, vous tournez de 90\u00b0 \u00e0 gauche.

    "}, {"location": "course-c/40-algorithms/popular-algorithms/lindenmayer/#evolution", "title": "\u00c9volution", "text": "

    On peut imaginer de g\u00e9n\u00e9raliser le L-Syst\u00e8me en ajoutant des variables, des fonctions, des conditions, etc. On peut \u00e9galement ajouter des r\u00e8gles de r\u00e9\u00e9criture conditionnelles, des r\u00e8gles de r\u00e9\u00e9criture probabilistes.

    Imaginons une d\u00e9finition formelle en YAML :

    name: Nom du L-Syst\u00e8me\naxiom: Cha\u00eene de d\u00e9part\nangle: Angle de rotation par d\u00e9faut\niterations: Nombre d'it\u00e9rations\nlength: Longueur de chaque segment par d\u00e9faut\nrules:\n    A: A+B\n    B: A-B\nactions:\n

    Imaginons les actions suivantes\u2009:

    actions:\n    forward(length): Avancer de length, length est optionnel\n    rotate(alpha, beta, gamma): Tourner de angle\n    color(r, g, b): Changer la couleur du crayon\n    color(name): Changer la couleur du crayon par une couleur nomm\u00e9e\n    save(): Sauvegarder la position et l'angle\n    restore(): Restaurer la position et l'angle\n    width(size): Changer la taille du crayon\nvariables:\n    level: Niveau de r\u00e9cursion\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/rabin-karp/", "title": "Algorithme de Rabin-Karp", "text": "

    Cet algorithme Rabin-Karp permet la recherche d'une sous-cha\u00eene de caract\u00e8re dans une cha\u00eene plus grande. Sa complexit\u00e9 moyenne est \\(O(n + m)\\).

    L'algorithme se base sur le principe de la fonction de hachage. Il consiste \u00e0 calculer le hash de la cha\u00eene \u00e0 rechercher et de la cha\u00eene dans laquelle on recherche. Si les deux hash sont \u00e9gales, on compare les deux cha\u00eenes caract\u00e8re par caract\u00e8re.

    L'algorithme fait glisser une fen\u00eatre de la taille de la cha\u00eene \u00e0 rechercher sur la cha\u00eene dans laquelle on recherche. \u00c0 chaque it\u00e9ration, on calcule le hash de la fen\u00eatre et on le compare au hash de la cha\u00eene \u00e0 rechercher. Si les deux hash sont \u00e9gaux, on compare les deux cha\u00eenes caract\u00e8re par caract\u00e8re.

    La performance de l'algorithme d\u00e9pend de la fonction de hachage. Si la fonction de hachage est bien choisie, l'algorithme est tr\u00e8s performant. Si la fonction de hachage est mal choisie, l'algorithme peut \u00eatre lent.

    Ici la fonction de hachage est tr\u00e8s simple, on utilise un nombre premier.

    rabin-karp.c
    #include <stdio.h>\n#include <string.h>\n#include <assert.h>\n\n#define CHARS_IN_ALPHABET 256\n\n/**\n * Rabin-Karp algorithm\n * @param needle Motif \u00e0 rechercher\n * @param haystack Texte d'entr\u00e9e\n * @param matches La liste des occurences trouv\u00e9es\n * @param size La taille du tableau matches\n * @return Le nombre d'occurences trouv\u00e9es\n */\nint search(char needle[], char haystack[], int matches[], size_t size)\n{\n    const int q = 101; // A prime number\n    const int M = strlen(needle);\n    const int N = strlen(haystack);\n\n    int h = 1;\n    for (int i = 0; i < M - 1; i++)\n        h = (h * CHARS_IN_ALPHABET) % q;\n\n    // Compute the hash value of pattern and first\n    // window of text\n    int p = 0; // Hash value for pattern\n    int t = 0; // Hash value for haystack\n    for (int i = 0; i < M; i++) {\n        p = (CHARS_IN_ALPHABET * p + needle[i]) % q;\n        t = (CHARS_IN_ALPHABET * t + haystack[i]) % q;\n    }\n\n    // Slide the pattern over text one by one\n    size_t k = 0;\n    for (int i = 0; i <= N - M; i++) {\n        // Check the hash values of current window of text\n        // and pattern. If the hash values match then only\n        // check for characters on by one\n        if (p == t) {\n            // Check for characters one by one\n            int j = 0;\n            while (haystack[i + j] == needle[j] && j < M)\n                j++;\n\n            // Save the position found\n            if (j == M)\n                if (k < size)\n                    matches[k++] = i;\n                else\n                    return k;\n        }\n\n        // Calculate hash value for next window of text.\n        // Remove leading digit and add trailing digit.\n        if (i < (N - M)) {\n            t = (CHARS_IN_ALPHABET *\n                (t - haystack[i] * h) + haystack[i + M]) % q;\n            t += t < 0 ? q : 0;\n        }\n    }\n    return k;\n}\n\nint test_search()\n{\n    char text[] =\n        \"Le courage n'est pas l'absence de peur, \"\n        \"mais la capacit\u00e9 de vaincre ce qui fait peur.\"\n        \"On ne peut vaincre sa destin\u00e9e.\"\n        \"A vaincre sans barils, on triomphe sans boire.\";\n\n    int matches[10];\n    int k = search(\"vaincre\", text, matches, sizeof(matches)/sizeof(matches[0]));\n    assert(k == 3);\n    assert(matches[0] == 61);\n    assert(matches[1] == 97);\n    assert(matches[2] == 120);\n}\n\nint main() {\n    test_search();\n}\n
    "}, {"location": "course-c/40-algorithms/popular-algorithms/random/", "title": "G\u00e9n\u00e9rateur congruentiel lin\u00e9aire", "text": "

    Le g\u00e9n\u00e9rateur congruentiel lin\u00e9aire (GCL) est un algorithme simple pour g\u00e9n\u00e9rer des nombres pseudo-al\u00e9atoires. Il est d\u00e9fini par la relation de r\u00e9currence suivante\u2009:

    \\[ X_{n+1} = (a \\cdot X_n + c) \\mod m \\]

    O\u00f9\u2009:

    \\(X_n\\)

    est la s\u00e9quence de nombres pseudo-al\u00e9atoires

    \\(a\\)

    est le multiplicateur

    \\(c\\)

    est l'incr\u00e9ment

    \\(m\\)

    est le modulo

    Les informaticiens ont remarqu\u00e9 que ce g\u00e9n\u00e9rateur fonctionne bien pour certaines valeurs de \\(a\\), \\(c\\) et \\(m\\). Par exemple, si \\(m = 2^k\\), le g\u00e9n\u00e9rateur est dit \u00e0 \u00ab\u2009congruence binaire\u2009\u00bb. Si \\(c = 0\\), le g\u00e9n\u00e9rateur est dit \u00ab\u2009multiplicatif\u2009\u00bb.

    En C c'est un g\u00e9n\u00e9rateur congruentiel lin\u00e9aire qui est utilis\u00e9 pour la fonction rand(). Voici une impl\u00e9mentation de ce g\u00e9n\u00e9rateur\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nstatic uint32_t seed = 0;\n\nvoid srand(uint32_t s) {\n    seed = s;\n}\n\nuint32_t rand() {\n    const uint32_t a = 1103515245;\n    const uint32_t c = 12345;\n    const uint32_t m = 1ULL << 31;\n    seed = (a * seed + c) % m;\n    return seed;\n}\n

    La valeur statique seed permet de conserver l'\u00e9tat du g\u00e9n\u00e9rateur entre les appels \u00e0 la fonction rand(). Si elle n'est pas initialis\u00e9e, le g\u00e9n\u00e9rateur retournera toujours la m\u00eame s\u00e9quence de nombres pseudo-al\u00e9atoires. La fonction srand() permet donc d'initialiser la graine du g\u00e9n\u00e9rateur.

    En pratique, on utilise srand() avec comme valeur d'initialisation le temps courant en secondes pour obtenir une s\u00e9quence de nombres pseudo-al\u00e9atoires diff\u00e9rente \u00e0 chaque ex\u00e9cution du programme\u2009:

    #include <time.h>\n\nint main() {\n    srand(time(NULL));\n    for (int i = 0; i < 10; ++i) {\n        printf(\"%d\\n\", rand());\n    }\n}\n

    N\u00e9anmoins si vous rappelez votre programme durant la m\u00eame seconde, vous obtiendrez la m\u00eame s\u00e9quence de nombres pseudo-al\u00e9atoires. Pour obtenir une s\u00e9quence diff\u00e9rente \u00e0 chaque ex\u00e9cution, vous pouvez utiliser inclure le PID du processus\u2009:

    #include <unistd.h>\n#include <time.h>\n\nint main() {\n    srand(time(NULL) ^ getpid());\n    for (int i = 0; i < 10; ++i) {\n        printf(\"%d\\n\", rand());\n    }\n}\n
    ", "tags": ["seed"]}, {"location": "course-c/40-algorithms/popular-algorithms/random/#valeurs-remarquables", "title": "Valeurs remarquables", "text": "

    Voici quelques valeurs de \\(a\\), \\(c\\) et \\(m\\) qui donnent de bons r\u00e9sultats\u2009:

    Source \\(m\\) \\(a\\) \\(c\\) ANSI C \\(2^{31}\\) \\(1103515245\\) \\(12345\\) Borland C \\(2^{32}\\) \\(22695477\\) \\(1\\) MMIX de Donald Knuth \\(2^{64}\\) \\(6364136223846793005\\) \\(1442695040888963407\\) Java \\(2^{48}\\) \\(25214903917\\) \\(11\\)"}, {"location": "course-c/40-algorithms/popular-algorithms/shunting-yard/", "title": "Algorithme de Shunting Yard", "text": "

    L'algorithme de Shunting Yard est un algorithme de parsing d'expression math\u00e9matique. Il permet de transformer une expression math\u00e9matique en notation infix\u00e9e en une expression postfix\u00e9e. L'algorithme a \u00e9t\u00e9 invent\u00e9 par Edsger Dijkstra en 1961.

    Imaginez que vous ayez une expression math\u00e9matique sous forme d'une cha\u00eene de caract\u00e8res\u2009:

    2 + 3 * 8 - 2 * ( 2 - 4 / ( 3 * 8 ) )\n

    Comment calculer cette expression\u2009? Si vous proc\u00e9dez de gauche \u00e0 droite, vous allez rencontrer des parenth\u00e8ses et des priorit\u00e9s d'op\u00e9rations.

    L'algorithme se compose de deux files d'attente (FIFO) et d'une pile (LIFO). La file d'attente de sortie contiendra l'expression postfix\u00e9e. La pile contiendra les op\u00e9rateurs.

    Algorithme de shunting yard

    Commen\u00e7ons par quelques d\u00e9finitions\u2009:

    TOKEN

    Un token est un \u00e9l\u00e9ment de l'expression math\u00e9matique. Il peut s'agir d'un nombre, d'un op\u00e9rateur ou d'une parenth\u00e8se.

    INPUT

    Une file d'attente contenant les TOKENS de l'expression math\u00e9matique \u00e0 traiter.

    OUTPUT

    Une file d'attente qui contiendra les TOKENS de l'expression postfix\u00e9e.

    STACK

    Une pile qui contiendra les op\u00e9rateurs.

    Voici le pseudo code de l'algorithme\u2009:

    • Tant qu'il y a des TOKEN \u00e0 lire\u2009:
    • Lire le TOKEN Si le TOKEN est\u2009:
      • Un nombre\u2009:
      • Le d\u00e9placer sur OUTPUT.
      • Un op\u00e9rateur \\(O_1\\)
      • Tant que:
        • Il y a un op\u00e9rateur \\(O_2\\) sur le dessus STACK,
        • et que ce n'est pas une parenth\u00e8se gauche,
        • et que \\(O_2\\) a une plus grande priorit\u00e9 que \\(O_1\\),
        • ou que \\(O_2\\) a la m\u00eame priorit\u00e9 que \\(O_1\\),
        • et que \\(O_1\\) est associatif \u00e0 gauche.
        • Alors:
        • D\u00e9placer \\(O_2\\) de STACK sur OUTPUT.
        • D\u00e9placer \\(O_1\\) sur le STACK.
      • Une parenth\u00e8se gauche (():
      • D\u00e9placer sur le STACK.
      • Une parenth\u00e8se droite ()):
      • Tant que l'op\u00e9rateur sur le dessus du STACK n'est pas une parenth\u00e8se gauche.
        • Alors, d\u00e9placer le dernier op\u00e9rateur du STACK sur OUTPUT.
        • Supprimer la parenth\u00e8se gauche du STACK.
    • Tant qu'il y a des TOKEN sur le STACK:
    • D\u00e9placer le TOKEN de STACK sur OUTPUT

    On observe que si on dispose de fonctions pour ajouter/supprimer des \u00e9l\u00e9ments d'une file d'attente et d'une pile, l'algorithme est relativement simple \u00e0 impl\u00e9menter. Voici l'impl\u00e9mentation en C\u2009:

    main.cqueue.hstack.hqueue.cstack.c
    #include \"queue.h\"\n#include \"stack.h\"\n\n#include <stdio.h>\n#include <ctype.h>\n\nint main() {\n    Stack stack = stack_create();\n    Queue output = queue_create();\n    Queue input = queue_create();\n\n    // Load expression in input queue\n    while (!feof(stdin)) {\n        char token;\n        scanf(\"%c\", &token);\n        if (isspace(token))\n            continue;\n        queue_push(&input, token);\n    }\n\n    // Shunting yard algorithm\n    while (!queue_empty(&input)) {\n        char token = queue_pop(&input);\n        if (isdigit(token))\n            queue_push(&output, token);\n        else if (token == '(')\n            stack_push(&stack, token);\n        else if (token == ')') {\n            while (stack_top(&stack) != '(')\n                queue_push(&output, stack_pop(&stack));\n            stack_pop(&stack);\n        } else {\n            while (!stack_empty(&stack) && stack_top(&stack) != '(')\n                queue_push(&output, stack_pop(&stack));\n            stack_push(&stack, token);\n        }\n    }\n    while (!stack_empty(&stack))\n        queue_push(&output, stack_pop(&stack));\n\n    // Display output queue\n    while (!queue_empty(&output))\n        printf(\"%c\", queue_pop(&output));\n}\n
    #pragma once\n\ntypedef struct QueueNode {\n    char data;\n    struct QueueNode *next;\n} QueueNode;\n\ntypedef struct {\n    QueueNode *front;\n    QueueNode *rear;\n} Queue;\n\nQueue queue_create();\nvoid queue_push(Queue *queue, char value);\nchar queue_pop(Queue *queue);\nint queue_empty(Queue *queue);\n
    #pragma once\n\ntypedef struct StackNode {\n    char data;\n    struct StackNode *next;\n} StackNode;\n\ntypedef struct {\n    StackNode *top;\n} Stack;\n\nStack stack_create();\nvoid stack_push(Stack *stack, char value);\nchar stack_pop(Stack *stack);\nchar stack_top(Stack *stack);\nint stack_empty(Stack *stack);\n
    #include \"queue.h\"\n\n#include <stdlib.h>\n\nQueue queue_create() {\n    Queue queue;\n    queue.front = NULL;\n    queue.rear = NULL;\n    return queue;\n}\n\nvoid queue_push(Queue *queue, char value) {\n    QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));\n    newNode->data = value;\n    newNode->next = NULL;\n    if (queue->rear)\n        queue->rear->next = newNode;\n    else\n        queue->front = newNode;\n    queue->rear = newNode;\n}\n\nchar queue_pop(Queue *queue) {\n    if (queue->front == NULL)\n        return '\\0';\n    QueueNode *node = queue->front;\n    char value = node->data;\n    queue->front = node->next;\n    if (queue->front == NULL)\n        queue->rear = NULL;\n    free(node);\n    return value;\n}\n\nint queue_empty(Queue *queue) {\n    return queue->front == NULL;\n}\n
    #include \"stack.h\"\n\n#include <stdlib.h>\n\nStack stack_create() {\n    Stack stack;\n    stack.top = NULL;\n    return stack;\n}\n\nvoid stack_push(Stack *stack, char value) {\n    StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));\n    newNode->data = value;\n    newNode->next = stack->top;\n    stack->top = newNode;\n}\n\nchar stack_pop(Stack *stack) {\n    if (stack->top == NULL)\n        return '\\0';\n    StackNode *node = stack->top;\n    char value = node->data;\n    stack->top = node->next;\n    free(node);\n    return value;\n}\n\nchar stack_top(Stack *stack) {\n    if (stack->top == NULL)\n        return '\\0';\n    return stack->top->data;\n}\n\nint stack_empty(Stack *stack) {\n    return stack->top == NULL;\n}\n

    Notation polonaise inverse

    La notation polonaise inverse (RPN) est une notation math\u00e9matique dans laquelle chaque op\u00e9rateur suit ses op\u00e9randes. Par exemple, l'expression 3 + 4 s'\u00e9crira 3 4 +. Cette notation a \u00e9t\u00e9 invent\u00e9e par le math\u00e9maticien polonais Jan \u0141ukasiewicz.

    Elle a \u00e9t\u00e9 longtemps utilis\u00e9e par les calculatrices Hewlett-Packard et permet de s'affranchir des parenth\u00e8ses. En effet, l'expression 3 + 4 * 5 s'\u00e9crira 3 4 5 * +.

    La calculatrice mythique HP-42s apparue en 1988 \u00e9tait utilis\u00e9e par beaucoup d'ing\u00e9nieurs et de scientifiques. On y remarque l'absence de touche \u00e9gale =. Pour calculer une expression, il suffisait de taper les op\u00e9randes et les op\u00e9rateurs dans l'ordre. La calculatrice se chargeait de calculer le r\u00e9sultat.

    HP-42s

    La notation polonaise inverse est \u00e9galement tr\u00e8s pratique pour les ordinateurs. En effet, il est plus facile de traiter une expression postfix\u00e9e qu'une expression infix\u00e9e. L'algorithme de Shunting Yard permet de transformer une expression infix\u00e9e en une expression postfix\u00e9e.

    "}, {"location": "course-c/40-algorithms/sorting/count-sort/", "title": "Counting Sort", "text": "

    Le tri par d\u00e9nombrement est un algorithme de tri particulier. Il est utilis\u00e9 pour trier des \u00e9l\u00e9ments dont la valeur est connue \u00e0 l'avance. Il est donc inutile pour trier des \u00e9l\u00e9ments dont la valeur est inconnue ou al\u00e9atoire.

    Nous l'avons vu pr\u00e9c\u00e9demment, il n'est pas possible de trier un tableau mieux qu'en O(nlogn). En revanche cette assertion n'est pas tout \u00e0 fait juste dans le cas ou les donn\u00e9es brutes poss\u00e8dent des propri\u00e9t\u00e9s remarquables.

    Pour que cet algorithme soit utilisable, imaginons un contexte o\u00f9 les donn\u00e9es d'entr\u00e9es poss\u00e8dent des propri\u00e9t\u00e9s remarquables.

    Ici, les donn\u00e9es d'entr\u00e9es seront g\u00e9n\u00e9r\u00e9es entre 0 et 51\u2009; chaque valeur repr\u00e9sentera une carte \u00e0 jouer selon la r\u00e8gle suivante\u2009:

    cards

    Cette s\u00e9rie de valeurs dispose de plusieurs propri\u00e9t\u00e9s int\u00e9ressantes\u2009:

    1. La valeur d'une carte est identifi\u00e9e par n % 13.
    2. La couleur est identifi\u00e9e par n / 13.
    3. Il n'y a donc que 13 valeurs par couleur et 4 couleurs.
    4. Bien qu'un entier soit stock\u00e9 sur 4 bytes, il suffit en r\u00e9alit\u00e9 de 6 bits pour encoder la valeur d'une carte.

    L'algorithme counting-sort est int\u00e9ressant, car il ne fait pas appel \u00e0 des comparaisons de valeurs.

    L'algorithme it\u00e9ratif est le suivant\u2009:

    1. On cr\u00e9e un tableau statique de 13 positions correspondant au nombre de valeurs possibles dans notre set de donn\u00e9e. Ce tableau peut \u00eatre nomm\u00e9 counts
    2. On parcourt le tableau \u00e0 trier lin\u00e9airement et on compte le nombre d'occurrences de chaque valeur dans counts. On aura donc \u00e0 la position 0 le nombre d'as contenus dans le tableau \u00e0 trier et \u00e0 la positon 10 le nombre de valets dans le jeu de cartes fourni.
    3. Une fois ces comptes termin\u00e9s, une op\u00e9ration de somme cumul\u00e9e est faite sur ce tableau (p.ex. le tableau {1, 2, 2, 0, 8, 1} sera somm\u00e9 comme suit {1, 3, 5, 5, 13, 14}).
    4. Le tableau \u00e0 trier est parcouru lin\u00e9airement de la fin vers le d\u00e9but et on copie la valeur \u00e0 la bonne position dans le tableau des valeurs tri\u00e9es. Si votre tableau d'entr\u00e9e non tri\u00e9 est nomm\u00e9 a et votre tableau de sortie tri\u00e9 b vous aurez pour chaque \u00e9l\u00e9ment i : b[c[a[i]--] - 1] = a[i]

    \u00c0 l'issue de cet algorithme, vous aurez dans b un tableau tri\u00e9 par valeurs.

    Notez qu'ici les couleurs ne sont pas tri\u00e9es.

    Voici un exemple de tri sur des entiers entre 0 et 9\u2009:

    1 0 9 3 8 1 4 8 7 5  Tableau d'entr\u00e9e `a` non tri\u00e9 et contenant que des chiffres\n                     Il y a 10 valeurs possibles par chiffre, donc le tableau\n                     `counts` aura 10 positions:\n\n1 2 0 1 1 1 0 1 2 1  Tableau `counts` calcul\u00e9, il faut lire : une occurrence\n                     de 0, deux occurrences de 2, zero occurrences de 3...\n\n1 3 3 4 5 6 6 1 9 B  Somme cumul\u00e9e (affich\u00e9 en hexad\u00e9cimal)\n\nPour trouver la position tri\u00e9e d'une valeur, par exemple le 3 :\n\n1 0 9 3 8 1 4 8 7 5  a[i] == 3 (on part du tableau a)\n      \u21a7\n1 3 3 4 5 6 6 1 9 B  counts[a[i]] == 4 (on consulte la somme cumul\u00e9e)\n      \u21a7\n_ _ _ 3 _ _ _ _ _ _  b[counts[a[i]]-- - 1] == 3 (on insert la valeur tri\u00e9e)\n\n0 1 1 3 4 5 7 8 8 9  Et ainsi de suite jusqu'\u00e0 tri complet du tableau\n

    On constate en effet que 3 est \u00e0 la bonne position dans b et qu'il y aura 0 1 1 devant.

    Tel quel, cet algorithme ne permet pas de modifier directement le tableau d'origine puisqu'il ne fait pas intervenir de permutations (swap). Il requiert donc un buffer suppl\u00e9mentaire et donc \u00e0 une complexit\u00e9 en espace de O(n).

    Concernant les cartes \u00e0 jouer, voici un exemple de tri\u2009:

    41 23 00 15 26 39 13 02 28  Tableau d'entr\u00e9e\n00 26 39 13 41 15 02 28 23  Tableau tri\u00e9\n\u00c0\u2663 \u00c0\u2665 A\u2660 A\u2666 3\u2660 3\u2666 3\u2663 3\u2665 V\u2666  Valeurs interpr\u00e9t\u00e9es des cartes\n

    Un avantage de cet algorithme est qu'il est stable, c'est-\u00e0-dire que l'ordre des \u00e9l\u00e9ments \u00e9gaux est conserv\u00e9. Donc on peut ensuite retrier les cartes par couleur.

    ", "tags": ["swap", "counts"]}, {"location": "course-c/40-algorithms/sorting/heap-sort/", "title": "Heap Sort", "text": "

    L'algorithme Heap Sort aussi appel\u00e9 \u00ab\u2009Tri par tas\u2009\u00bb est l'un des algorithmes de tri les plus performants offrant une complexit\u00e9 en temps de \\(O(n\\cdot log(n))\\) et une complexit\u00e9 en espace de \\(O(1)\\). Il s'appuie sur le concept d'arbre binaire.

    Prenons l'exemple du tableau ci-dessous et deux r\u00e8gles suivantes\u2009:

    • l'enfant de gauche est donn\u00e9 par 2 * k + 1 ;
    • l'enfant de droite est donn\u00e9 par 2 * k + 2.
      1   2       3                  4\n\u251e\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2526\n\u250208\u250204\u250212\u250220\u250206\u250242\u250214\u250211\u250203\u250235\u250207\u250209\u250211\u250250\u250216\u2502\n\u2514\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2518\n  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  (indice)\n

    La premi\u00e8re valeur du tableau est appel\u00e9e la racine root. C'est le premier \u00e9l\u00e9ment de l'arbre. Puisqu'il s'agit d'un arbre binaire, chaque n\u0153ud peut comporter jusqu'\u00e0 2 enfants. L'enfant de gauche est calcul\u00e9 \u00e0 partir de l'indice k de l'\u00e9l\u00e9ment courant. Ainsi les deux enfants de l'\u00e9l\u00e9ment 4 seront 2 * 4 + 1 = 9 et 2 * 4 + 2 == a.

    Ce tableau lin\u00e9aire en m\u00e9moire pourra \u00eatre repr\u00e9sent\u00e9 visuellement comme un arbre binaire\u2009:

                 8\n             |\n         ----+----\n       /           \\\n      4            12\n   /    \\        /    \\\n  20     6      42    14\n / \\    / \\    / \\   /  \\\n11  3  35  7  9  11 50  16\n

    Le c\u0153ur de cet algorithme est le sous-algorithme nomm\u00e9 heapify. Ce dernier \u00e0 pour objectif de satisfaire une exigence suppl\u00e9mentaire de notre arbre\u2009: chaque enfant doit \u00eatre plus petit que son parent. Le principe est donc simple. On part du dernier \u00e9l\u00e9ment de l'arbre qui poss\u00e8de au moins un enfant\u2009: la valeur 14 (indice 6). Le plus grand des enfants est \u00e9chang\u00e9 avec la valeur du parent. Ici 50 sera \u00e9chang\u00e9 avec 14. Ensuite on applique r\u00e9cursivement ce m\u00eame algorithme pour tous les enfants qui ont \u00e9t\u00e9 \u00e9chang\u00e9s. Comme 14 (anciennement 50) n'a pas d'enfant, on s'arr\u00eate l\u00e0.

    L'algorithme continue en remontant jusqu'\u00e0 la racine de l'arbre. La valeur suivante analys\u00e9e est donc 42, comme les deux enfants sont petits on continue avec la valeur 6. Cette fois-ci 35 qui est plus grand est alors \u00e9chang\u00e9. Comme 6 n'a plus d'enfant, on continue avec 20, puis 12. \u00c0 cette \u00e9tape, notre arbre ressemble \u00e0 ceci\u2009:

                 8\n             |\n         ----+----\n       /           \\\n      4            12\n   /    \\        /    \\\n  20    35      42    50\n / \\    / \\    / \\   /  \\\n11  3  6   7  9  11 14  16\n

    La valeur 12 est plus petite que 50 et est donc \u00e9chang\u00e9e. Mais puisque 12 contient deux enfants (14 et 16), l'algorithme continue. 16 est \u00e9chang\u00e9 avec 12. L'algorithme se poursuit avec 4 et se terminera avec la racine 8. Finalement l'arbre ressemblera \u00e0 ceci\u2009:

                35\n             |\n         ----+----\n       /           \\\n     20            50\n   /    \\        /    \\\n  11     7      42    16\n / \\    / \\    / \\   /  \\\n8   3  6   4  9  11 14  12\n

    On peut observer que chaque n\u0153ud de l'arbre satisfait \u00e0 l'exigence susmentionn\u00e9e\u2009: tous les enfants sont inf\u00e9rieurs \u00e0 leurs parents.

    Une fois que cette propri\u00e9t\u00e9 est respect\u00e9e, on a l'assurance que la racine de l'arbre est maintenant le plus grand \u00e9l\u00e9ment du tableau. Il est alors \u00e9chang\u00e9 avec le dernier \u00e9l\u00e9ment du tableau 12, qui devient \u00e0 son tour la racine.

    Le dernier \u00e9l\u00e9ment est sorti du tableau et notre arbre ressemble maintenant \u00e0 ceci\u2009:

    1   2       3                  4\n\u251e\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2540\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u252c\u2500\u2500\u2526\u2500\u2500\u2526\n\u250212\u250220\u250250\u250211\u2502 7\u250242\u250216\u2502 8\u2502 3\u2502 6\u2502 4\u2502 9\u250211\u250214\u250235\u2502\n\u2514\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2534\u2500\u2500\u2518\n  0  1  2  3  4  5  6  7  8  9  a  b  c  d     (indice)\n\n            12\n             |\n         ----+----\n       /           \\\n     20            50\n   /    \\        /    \\\n  11     7      42    16\n / \\    / \\    / \\   /\n8   3  6   4  9  11 14\n

    \u00c0 ce moment on recommence\u2009:

    1. heapify
    2. \u00c9change du premier \u00e9l\u00e9ment avec le dernier.
    3. Sortie du dernier \u00e9l\u00e9ment de l'arbre.
    4. Retour \u00e0 (1) jusqu'\u00e0 ce que tous les \u00e9l\u00e9ments soient sortis de l'arbre.
    ", "tags": ["heapify"]}, {"location": "course-c/40-algorithms/sorting/quick-sort/", "title": "Quick Sort", "text": "

    Le tri rapide est l'algorithme de tri par r\u00e9f\u00e9rence dans la plupart des langage de programmation. Selon le compilateur C que vous utilisez, la fonction qsort impl\u00e9mente cette m\u00e9thode de tri en \\(O(n log n)\\).

    Quick sort est th\u00e9oriquement plus lent que le Heap sort avec dans le pire des cas en \\(O(n^2)\\). N\u00e9anmoins, en s'appuyant que cette r\u00e9ponse StackOverflow, quick sort reste meilleur pour de grands tableaux car les embranchements sont moins fr\u00e9quents, et le cache processeur est donc mieux utilis\u00e9.

    Cet algorithme utilise la notion de pivot. Le pivot est un \u00e9l\u00e9ment qui est choisi pour \u00eatre le point autour duquel sont agenc\u00e9 les \u00e9l\u00e9ments. La m\u00e9thode de tri est la suivante\u2009:

    1. Choix d'un pivot
    2. Paritionnement\u2009: tous les \u00e9l\u00e9ments plus petit que le pivot sont d\u00e9plac\u00e9 \u00e0 gauche et tous les \u00e9l\u00e9ments plus grands sont \u00e0 droite. L'\u00e9l\u00e9ment pivot est ainsi positionn\u00e9 \u00e0 sa bonne place dans le tableau.
    3. Appel r\u00e9cursif pour la partie gauche et droite.

    Consid\u00e9rons le tableau suivant. Les valeurs ne sont pas tri\u00e9es. La premi\u00e8re \u00e9tape consiste \u00e0 choisir un pivot. Il existe plusieurs technique\u2009:

    • Choisir le premier \u00e9l\u00e9ment comme pivot
    • Choisir le dernier \u00e9l\u00e9ment comme pivot
    • Choisir l'\u00e9l\u00e9ment m\u00e9dian comme pivot

    Dans cet exemple, le dernier \u00e9l\u00e9ment 6 sera arbitrairement choisi comme pivot.

    Repr\u00e9sentation du tableau \u00e0 trier avec son pivot.

    L'\u00e9tape de paritionnement utilise l'algorithme suivant\u2009:

    int partition (int a[], int low, int high, int pivot)\n{\n    int i = low;\n    for (int j = low; j < high; j++)\n        if (a[j] < a[pivot])\n            swap(&a[i++], &a[j]);\n    swap(&a[i], &a[pivot]);\n    return i;\n}\n

    Voici comment partition(a, 0, 10, 10) modifie le tableau (voir code source) :

    2 9 4 1 b 5 a 7 3 8 6\n2 4 9 1 b 5 a 7 3 8 6\n2 4 1 9 b 5 a 7 3 8 6\n2 4 1 5 b 9 a 7 3 8 6\n2 4 1 5 3 9 a 7 b 8 6\n2 4 1 5 3 6 a 7 b 8 9\n

    On constate que la valeur 6 choisie comme pivot est maintenant \u00e0 sa bonne place. L'algorithme est donc appel\u00e9 r\u00e9cursivement pour les \u00e9l\u00e9ments 0 \u00e0 4 et `` 6`` \u00e0 a.

    Tri rapide apr\u00e8s le premier partitionnement.

    Voici une autre repr\u00e9sentation (voir code source) :

    1  9  5  2  b  4  a  7  3  8 [6]\n1  5  2  4  3 [6] a  7  b  8  9\n1  5  2  4 [3]\n1  2 [3] 4  5\n1 [2]\n1 [2]\n        4 [5]\n        4 [5]\n                a  7  b  8 [9]\n                7  8 [9] a  b\n                7 [8]\n                7 [8]\n                        a [b]\n                        a [b]\n
    ", "tags": ["qsort"]}, {"location": "course-c/45-software-design/", "title": "Introduction", "text": "

    Dans cette partie, nous allons aborder une partie fondamentale et souvent n\u00e9glig\u00e9e dans les cours d'informatique\u2009: la conception logicielle ou en anglais Software Design.

    Nous aborderons \u00e9galement de fa\u00e7on plus g\u00e9n\u00e9rale la gestion de projets informatiques. Travailler seul ou en \u00e9quipe sur un projet informatique n\u00e9cessite une organisation rigoureuse pour garantir la qualit\u00e9, le respect des d\u00e9lais et le succ\u00e8s du projet. La gestion de projets informatiques est une discipline \u00e0 part enti\u00e8re, qui repose sur des m\u00e9thodes, des outils et des bonnes pratiques sp\u00e9cifiques.

    Plus un projet \u00e9volue plus il devient complexe et difficile \u00e0 appr\u00e9hender. Sans un effort constant de la part de l'\u00e9quipe, de la dette de code s'accumule, des bugs apparaissent, des fonctionnalit\u00e9s sont oubli\u00e9es, des d\u00e9lais sont d\u00e9pass\u00e9s, etc. Pour \u00e9viter ces \u00e9cueils, nous allons voir comment organiser un projet informatique de mani\u00e8re efficace et structur\u00e9e.

    "}, {"location": "course-c/45-software-design/contribute/", "title": "Projet open-source", "text": "

    Le langage C est encore beaucoup utilis\u00e9 dans de gros projets qui demande une grande performance. On peut notament citer les projets suivants\u2009:

    Projet Watch Fork Stars Commits Linux 7940 53k 177k 1.3M Git 2402 25.4k 51.6k 74k Vim 676 5.4k 35.9k 20k SQLite 119 944k 6.2k 30k Gimp - 434 310k 53k FFmpeg 1438 12k 44.5k 117k Wireshark 300 1.8k 7k 93k Nginx 994 6.7k 21k 8.2k Apache 238 1.1k 3.5k 34k Le noyau Linux

    \u00c9tant donn\u00e9 qu'il est le noyau de la plupart des distributions Linux et que Linux est pr\u00e9sent sur des millions de serveurs, smartphones (via Android), et appareils IoT, on peut parler de milliards d'utilisateurs indirects.

    Git

    Comme le syst\u00e8me de contr\u00f4le de version le plus populaire, utilis\u00e9 par GitHub, GitLab, Bitbucket, et d'autres plateformes, il est utilis\u00e9 par des dizaines de millions de d\u00e9veloppeurs dans le monde.

    Vim

    En tant qu'\u00e9diteur de texte, il est tr\u00e8s populaire parmi les d\u00e9veloppeurs et les administrateurs syst\u00e8me. Il est difficile de quantifier pr\u00e9cis\u00e9ment son nombre d'utilisateurs, mais il est inclus par d\u00e9faut dans de nombreuses distributions Linux et est couramment utilis\u00e9 par des millions de d\u00e9veloppeurs.

    SQLite

    Utilis\u00e9 dans des milliards de dispositifs, que ce soit dans les navigateurs web, les t\u00e9l\u00e9phones mobiles, ou les syst\u00e8mes embarqu\u00e9s. C\u2019est probablement le SGBD le plus d\u00e9ploy\u00e9 au monde.

    Gimp

    C'est une alternative open-source populaire \u00e0 des logiciels comme Adobe Photoshop. Son nombre d'utilisateurs est dans l'ordre des millions, surtout parmi les amateurs de design graphique open-source.

    FFmpeg

    Tr\u00e8s utilis\u00e9 dans l'industrie pour le traitement de vid\u00e9os, souvent int\u00e9gr\u00e9 dans d'autres logiciels, ce qui rend difficile le comptage. Toutefois, son usage est tr\u00e8s r\u00e9pandu dans les syst\u00e8mes multim\u00e9dia, donc potentiellement des centaines de millions d'utilisateurs finaux.

    Wireshark

    Cet outil de capture et d'analyse de paquets r\u00e9seau est indispensable pour les ing\u00e9nieurs r\u00e9seau et les chercheurs en s\u00e9curit\u00e9, comptant probablement des millions d'utilisateurs dans ces domaines sp\u00e9cialis\u00e9s.

    Nginx

    Utilis\u00e9 comme serveur web ou reverse proxy, il alimente une grande partie des sites web dans le monde. Selon certaines estimations, il g\u00e8re plus de 30 % des sites web actifs, ce qui se traduit par des centaines de millions d'utilisateurs finaux.

    Apache HTTP Server

    Autre serveur web tr\u00e8s populaire, il est utilis\u00e9 sur une part significative des sites web. Bien que Nginx l'ait surpass\u00e9 en popularit\u00e9 dans certains cas, Apache reste extr\u00eamement r\u00e9pandu.

    "}, {"location": "course-c/45-software-design/contribute/#cas-de-figure-git", "title": "Cas de figure Git", "text": "

    Vous souhaitez contribuer au projet Git. La premi\u00e8re \u00e9tape est de trouver le code source. Ce n'est pas une grosse difficult\u00e9. Google donne rapidement l'information que Git est h\u00e9berg\u00e9 sur GitHub\u2009:

    Il suffit alors de cloner le projet Git sur votre machine locale sans historique. Cela permet de gagner du temps et de l'espace disque.

    git clone --depth=1 https://github.com/git/git.git\ncd git\n

    Un bref per\u00e7u du contenu du r\u00e9pertoire clon\u00e9 montre des fichiers sources C, un Makefile et un fichier INSTALL. C'est tout ce qu'il nous faut.

    $ ls\nCODE_OF_CONDUCT.md  editor.h            merge-ort.c        reset.h\nCOPYING             entry.c             merge-ort.h        resolve-undo.c\nDocumentation       entry.h             merge-recursive.c  resolve-undo.h\nGIT-VERSION-GEN     environment.c       merge-recursive.h  revision.c\nINSTALL             environment.h       merge.c            revision.h\nLGPL-2.1            ewah                merge.h            run-command.c\nMakefile            exec-cmd.c          mergesort.h        run-command.h\nREADME.md           exec-cmd.h          mergetools         sane-ctype.h\nRelNotes            fetch-negotiator.c  midx-write.c       scalar.c\nSECURITY.md         fetch-negotiator.h  midx.c             send-pack.c\nabspath.c           fetch-pack.c        midx.h             send-pack.h\n...\n

    On notera que le projet contient les fichiers bien connus\u2009:

    • .editorconfig pour d\u00e9finir les r\u00e8gles de formatage du code
    • .gitattributes pour d\u00e9finir les attributs des fichiers
    • .clang-format pour d\u00e9finir les r\u00e8gles de formatage du code C
    • .gitignore pour ignorer les fichiers inutiles
    • .gitmodules pour d\u00e9finir les sous-modules Git
    • LICENSE pour la licence du projet
    • COPYING pour la licence du projet
    • Makefile pour la compilation du projet
    • README.md pour la documentation du projet

    Avec less INSTALL, on peut prendre connaissance des instructions d'installation.

    less INSTALL\n                Git installation\n\nNormally you can just do \"make\" followed by \"make install\", and that\nwill install the git programs in your own ~/bin/ directory. ...\n\nAlternatively you can use autoconf generated ./configure script to\nset up install paths (via config.mak.autogen), so you can write instead\n\n        $ make configure ;# as yourself\n        $ ./configure --prefix=/usr ;# as yourself\n        $ make all doc ;# as yourself\n        # make install install-doc install-html;# as root\n...\n

    Nous allons donc cr\u00e9er un pr\u00e9fixe local pour ne pas polluer notre syst\u00e8me.

    mkdir _install\nmake configure prefix=$(pwd)/_install\n

    Le script configure est maintenant g\u00e9n\u00e9r\u00e9, il nous permet de configurer le projet, c'est \u00e0 dire de choisir les options de compilation. Si l'on ex\u00e9cute configure avec l'option --help, on obtient la liste des options disponibles.

    ./configure --help\n...\n  --with-libpcre          synonym for --with-libpcre2\n  --with-libpcre2         support Perl-compatible regexes via libpcre2\n                          (default is NO)\n...\n

    On peut voir par exemple que Git est compil\u00e9 par d\u00e9faut sans libpcre. Cette biblioth\u00e8que permet le support avanc\u00e9 des expressions r\u00e9guli\u00e8res Perl. Souvent les connaisseurs de Perl pr\u00e9f\u00e8re cette senteur aux expressions r\u00e9guli\u00e8res POSIX. Pour pouvoir l'utiliser il faut donc activer cette option, et n\u00e9cessairement disposer de la biblioth\u00e8que libpcre2 sur le syst\u00e8me.

    On commence donc par installer cette biblioth\u00e8que\u2009:

    sudo apt install libpcre2-dev\n

    Puis configurer le projet\u2009:

    $ ./configure --with-libpcre2 --prefix=$(pwd)/_install\nconfigure: Setting lib to 'lib' (the default)\nconfigure: Will try -pthread then -lpthread to enable POSIX Threads.\nconfigure: CHECKS for site configuration\nchecking for gcc... gcc\nchecking whether the C compiler works... yes\nchecking for C compiler default output file name... a.out\nchecking for suffix of executables...\nchecking whether we are cross compiling... no\nchecking for suffix of object files... o\nchecking whether the compiler supports GNU C... yes\nchecking whether gcc accepts -g... yes\nchecking for gcc option to enable C11 features... none needed\nchecking for stdio.h... yes\nchecking for stdlib.h... yes\n...\nchecking for pcre2_config_8 in -lpcre2-8... yes\n...\nconfigure: creating ./config.status\nconfig.status: creating config.mak.autogen\nconfig.status: executing config.mak.autogen commands\n

    A pr\u00e9sent, on peut compiler le projet\u2009:

    make -j16 V=1\n

    L'option -j16 permet de compiler en parall\u00e8le sur 16 threads. Cela acc\u00e9l\u00e8re la compilation en utilisant mieux les ressources de la machine. V=1 active le mode verbeux pour afficher les commandes ex\u00e9cut\u00e9es ce qui peut \u00eatre plus int\u00e9ressant pour comprendre ce qui se passe.

    Analysons un peu la sortie de la commande make:

    $ make -j16 V=1\nGIT_VERSION = 2.46.GIT\n    * new build flags\n    * new link flags\n    * new prefix flags\n/bin/sh ./generate-cmdlist.sh \\\n         \\\n        command-list.txt >command-list.h\ngcc -o hex.o -c -MF ./.depend/hex.o.d -MQ hex.o -MMD -MP    -g -O2  -I. \\\n-DHAVE_SYSINFO -DGIT_HOST_CPU=\"\\\"x86_64\\\"\" -DUSE_LIBPCRE2 -DHAVE_ALLOCA_H \\\n-DUSE_CURL_FOR_IMAP_SEND -DSUPPORTS_SIMPLE_IPC -DSHA1_DC \\\n-DSHA1DC_NO_STANDARD_INCLUDES -DSHA1DC_INIT_SAFE_HASH_DEFAULT=0 \\\n-DSHA1DC_CUSTOM_INCLUDE_SHA1_C=\"\\\"git-compat-util.h\\\"\" -DSHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C=\"\\\"git-compat-util.h\\\"\" -DSHA256_BLK  \\\n-DHAVE_PATHS_H -DHAVE_STRINGS_H -DHAVE_DEV_TTY -DHAVE_CLOCK_GETTIME \\\n-DHAVE_CLOCK_MONOTONIC -DHAVE_SYNC_FILE_RANGE -DHAVE_GETDELIM \\\n'-DPROCFS_EXECUTABLE_PATH=\"/proc/self/exe\"' -DFREAD_READS_DIRECTORIES \\\n-DSHELL_PATH='\"/bin/sh\"'  hex.c\n...\n

    On observe que Make compile tous les fichiers en appelant gcc avec des options sp\u00e9cifiques. On en connait d\u00e9j\u00e0 quelques unes.

    gcc\n   -o hex.o         # Nom du fichier objet de sortie\n                    # (g\u00e9n\u00e9ralement le nom du fichier source avec .o)\n   -c               # Compile sans lier, juste pour g\u00e9n\u00e9rer l'objet\n\n   # Gestion des d\u00e9pendances (pour la recompilation automatique)\n   -MF ./.depend/hex.o.d -MQ hex.o -MMD -MP\n\n   -g   # G\u00e9n\u00e8re des informations de d\u00e9bogage\n   -O2  # Optimisation de niveau 2\n   -I.  # Ajoute le r\u00e9pertoire courant pour les #include\n\n   -DUSE_LIBPCRE2  # D\u00e9finit le symbole USE_LIBPCRE2, puisque nous\n                   # avons activ\u00e9 l'option --with-libpcre2\n\n    hex.c          # Fichier source \u00e0 compiler\n

    Les options -D peuvent \u00eatre assez nombreuses selon le projet, elles permettent de \u00ab\u2009communiquer\u2009\u00bb avec le code source via des directives de pr\u00e9processeur. Dans le code source pour d\u00e9tecter si le support libpcre2 est activ\u00e9 on peut trouver des lignes comme\u2009:

    #ifdef USE_LIBPCRE2\n#  include <pcre2.h>\n#endif\n

    \u00c0 la fin de la compilation, le Makefile appellera le linker pour g\u00e9n\u00e9rer l'ex\u00e9cutable final. On reconnait facilement la ligne car c'est celle qui contient tous les fichiers objets g\u00e9n\u00e9r\u00e9s.

    gcc -g -O2 -I.\n    -o git \\\n    git.o builtin/add.o builtin/am.o builtin/annotate.o builtin/apply.o \\\n    builtin/archive.o builtin/bisect.o builtin/blame.o builtin/branch.o \\\n    builtin/bugreport.o builtin/bundle.o builtin/cat-file.o builtin/check-attr.o\n    builtin/check-ignore.o builtin/check-mailmap.o builtin/check-ref-format.o \\\n    builtin/checkout--worker.o builtin/checkout-index.o builtin/checkout.o \\\n    builtin/clean.o builtin/clone.o builtin/column.o builtin/commit-graph.o \\\n    ...\n    libgit.a xdiff/lib.a reftable/libreftable.a libgit.a \\\n    -lpcre2-8 -lz  -lrt\n

    On voit que le projet est li\u00e9 avec libpcre2-8, z et rt. La biblioth\u00e8que z est la biblioth\u00e8que standard de compression zlib. La biblioth\u00e8que rt est la biblioth\u00e8que de temps r\u00e9el. Les biblioth\u00e8ques statiques libgit.a, xdiff/lib.a, reftable/libreftable.a sont des biblioth\u00e8ques internes au projet qui ont \u00e9t\u00e9 compil\u00e9es en m\u00eame temps que le projet.

    Enfin, on peut installer le projet dans le r\u00e9pertoire _install et ex\u00e9cuter Git\u2009:

    $ make install\n$ ./_install/bin/git --version\ngit version 2.46.GIT\n

    Comme pour la plupart des projets, ce dernier dispose d'une panoplie de tests unitaires et fonctionnels. Pour les ex\u00e9cuter, il suffit de lancer la commande make test:

    make test\n    SUBDIR git-gui\n    SUBDIR gitk-git\n    SUBDIR templates\nmake -C t/ all\nmake[1]: Entering directory '/home/ycr/git/t'\nrm -f -r 'test-results'\nGIT_TEST_EXT_CHAIN_LINT=0 && export GIT_TEST_EXT_CHAIN_LINT && make aggregate-results-and-cleanup\nmake[2]: Entering directory '/home/ycr/git/t'\n*** t0000-basic.sh ***\nok 1 - verify that the running shell supports \"local\"\nok 2 - .git/objects should be empty after git init in an empty repo\nok 3 - .git/objects should have 3 subdirectories\n...\nok 2598 - checkout attr= ident aeol=crlf core.autocrlf=true core.eol= file=LF_mix_CR\nok 2599 - checkout attr= ident aeol=crlf core.autocrlf=true core.eol= file=LF_nul\nok 2600 - ls-files --eol -d -z\n# passed all 2600 test(s)\n

    Ce projet est assez simple \u00e0 compiler et \u00e0 tester. Il est bien document\u00e9 et les tests sont clairs. N\u00e9anmoins il ne d\u00e9pend pas de beaucoup de biblioth\u00e8ques externes. Pour des projets plus complexes, la gestion des d\u00e9pendances peut \u00eatre un vrai casse-t\u00eate. Prenons un autre exemple, celui de Gimp.

    ", "tags": ["libpcre2", "make", "README.md", "gcc", "libpcre", "zlib", "_install", "LICENSE", "Makefile", "configure", "libgit.a", "COPYING"]}, {"location": "course-c/45-software-design/contribute/#cas-de-figure-gimp", "title": "Cas de figure Gimp", "text": "

    Gimp est un logiciel de retouche d'image tr\u00e8s populaire. C'est la version libre de Adobe Photoshop. Il est \u00e9crit en C et utilise de nombreuses biblioth\u00e8ques externes. Pour compiler Gimp, il faut donc installer toutes ces biblioth\u00e8ques. La liste des d\u00e9pendances est longue et varie selon les distributions.

    Gimp

    En cherchant sous Google \u00ab\u2009gimp source code\u2009\u00bb, on tombe sur le site officiel\u2009:

    https://www.gimp.org/source/

    Il est indiqu\u00e9 que le code source est h\u00e9berg\u00e9 sur le r\u00e9f\u00e9rentiel de GNOME, un environnement de bureau libre pour les syst\u00e8mes Unix. Ce site redirige sur https://developer.gimp.org/ o\u00f9 l'on trouve les instructions pour les d\u00e9veloppeurs. De liens en liens on arrive sur la page Building GIMP qui donne les instructions pour compiler Gimp.

    Il est indiqu\u00e9 que Gimp utilise l'environnement Meson pour la compilation. Meson est un syst\u00e8me de build open-source qui permet de g\u00e9n\u00e9rer des fichiers de configuration pour les projets C/C++. Il est \u00e9crit en Python et est tr\u00e8s rapide.

    Les instructions indiquent certaines d\u00e9pendences\u2009:

    pkg-config

    Pkg-config est un outil qui permet de r\u00e9cup\u00e9rer les options de compilation et de liens pour les biblioth\u00e8ques install\u00e9es sur le syst\u00e8me. Il sera utilis\u00e9 par Meson pour trouver les biblioth\u00e8ques n\u00e9cessaires \u00e0 la compilation.

    gettext

    Gettext est une biblioth\u00e8que qui permet de g\u00e9rer les cha\u00eenes de caract\u00e8res multilingues. Elle est utilis\u00e9e par Gimp pour les traductions. C'est la biblioth\u00e8que de facto utilis\u00e9e pour l'internationalisation des logiciels libres.

    gegl et babl

    GEGL (Generic Graphics Library) est une biblioth\u00e8que de traitement d'image non-destructif. Elle est utilis\u00e9e par Gimp pour les op\u00e9rations de traitement d'image. BABL est une biblioth\u00e8que de conversion de couleurs. Elle est utilis\u00e9e par GEGL pour les conversions de couleurs. Ces biblioth\u00e8ques sont intrins\u00e8quement li\u00e9es \u00e0 Gimp.

    gtk3

    GTK3 est la biblioth\u00e8que graphique utilis\u00e9e par Gimp pour l'interface utilisateur. Elle est bas\u00e9e sur le langage de programmation C et est tr\u00e8s populaire pour les applications graphiques sous Linux. GTK3 d\u00e9pend \u00e9galement de glib et Pango.

    Comme il s'agit d'un gros projet, nous allons devoir installer un certain nombre de d\u00e9pendances qui ne seront pas n\u00e9cessairement utiles ensuite sur notre ordinateur. La bonne approche est de s'isoler dans un environnement virtuel. Docker est une bonne alternative. Comme Gimp est d\u00e9velopp\u00e9 sous Debian, nous allons utiliser une image Docker Debian pour compiler le projet.

    $ docker run -it debian:latest\nUnable to find image 'debian:latest' locally\nlatest: Pulling from library/debian\n903681d87777: Pull complete\nDigest: sha256:aadf411dc9ed5199bc7dab48b3e6ce18f8bbee4f170127f5ff1b75cd8035eb36\nStatus: Downloaded newer image for debian:latest\nroot@8c6a00ea1cf7:/#\n

    La premi\u00e8re \u00e9tape Preparing for Building propose d'installer le projet dans un r\u00e9pertoire local. Nous allons commencer par cr\u00e9er un dossier gimp et fixer quelques variables d'environnement\u2009:

    apt update\napt install build-essential cmake make git meson ninja-build bison flex pkg-config gettext\n
    mkdir gimp && cd gimp\nexport GIMP_DIR=$(pwd)\nexport GIMP_PREFIX=$GIMP_DIR/_install\n\n# Assuming that you have toolchain installed\n# If this don't work, so manually set the dirs\nLIB_DIR=$(cc -print-multi-os-directory | sed 's/\\.\\.\\///g')\ncc -print-multiarch | grep . && LIB_SUBDIR=$(echo $(cc -print-multiarch)'/')\n\n# Used to detect the build dependencies\nexport PKG_CONFIG_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}\npkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}\"\n\n# Used to find the libraries at runtime\nexport LD_LIBRARY_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}$\n{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\"\n\n# Used to find the glib-introspection dependencies\nexport XDG_DATA_DIRS=\"${GIMP_PREFIX}/share:/usr/share\n${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}\"\n\n# Used to find introspection files\nexport GI_TYPELIB_PATH=\"${GIMP_PREFIX}/${LIB_DIR}/${LIB_SUBDIR}\ngirepository-1.0${GI_TYPELIB_PATH:+:$GI_TYPELIB_PATH}\"\n\n# Used by Autotools to find its tools\nexport ACLOCAL_FLAGS=\"-I $GIMP_PREFIX/share/aclocal $ACLOCAL_FLAGS\"\n\nexport XDG_DATA_DIRS=\"${GIMP_PREFIX}/share:/usr/local/share:/usr/share\"\nexport PATH=\"${GIMP_PREFIX}/bin:$PATH\"\n

    En suivant la documentation, on installe quelques d\u00e9pendences, sachant qu'il y en aura probablement d'autres \u00e0 installer au fur et \u00e0 mesure\u2009:

    apt install graphviz libswscale-dev libsuitesparse-dev libpng-dev\nlibtiff-dev librsvg2-dev \\\nliblcms2-dev libmypaint-dev mypaint-brushes  libmng-dev libwmf-dev \\\nlibaa1-dev libgs-dev libheif-dev gobject-introspection libgirepository1.0-dev\n

    500 MB plus tard, nous avons install\u00e9 les d\u00e9pendances de base. Nous allons maintenant cloner le projet Gimp\u2009:

    ", "tags": ["gimp", "Pango", "glib"]}, {"location": "course-c/45-software-design/contribute/#installation-de-babl", "title": "Installation de BABL", "text": "
    git clone --depth=1 https://gitlab.gnome.org/GNOME/babl.git\ncd babl\nmeson setup --prefix $GIMP_PREFIX -Denable-gir=true _build\nninja -C _build install\n
    "}, {"location": "course-c/45-software-design/contribute/#installation-de-gegl", "title": "Installation de GEGL", "text": "
    git clone --depth=1 https://gitlab.gnome.org/GNOME/gegl.git\ncd gegl\nmeson setup --prefix $GIMP_PREFIX -Dintrospection=true -Dcairo=enabled -Dumfpack=enabled -Dworkshop=true _build\nninja -C _build install\n
    "}, {"location": "course-c/45-software-design/contribute/#installation-de-gimp", "title": "Installation de GIMP", "text": "
    git clone --depth=1 --shallow-submodules  https://gitlab.gnome.org/GNOME/gimp.git\ncd gimp\ngit submodule update --init\nmeson setup --prefix $GIMP_PREFIX _build\nRun-time dependency atk found: NO (tried pkgconfig and cmake)\n\nmeson.build:364:0: ERROR: Dependency \"atk\" not found, tried pkgconfig and cmake\n

    Nous avons un probl\u00e8me. La d\u00e9pendance atk n'est pas trouv\u00e9e. Un rapide coup d'oeil \u00e0 la documentation nous indique que atk est une biblioth\u00e8que graphique et la version 2.4.0 est n\u00e9cessaire. Or la version que l'on trouve sous Debian est libatk1.0-dev qui est la 2.30.0. Comme seule la version mineure est diff\u00e9rente il ne devrait pas y avoir de probl\u00e8me de compatibilit\u00e9. Nous allons donc installer la version 2.30.0 et voir si cela fonctionne.

    apt install libatk1.0-dev\n

    Notons que la biblioth\u00e8que se nomme libatk1.0-dev le 1.0 est la version de l'API mais cela ne signifie pas que la version de la biblioth\u00e8que est 1.0. En fait tant que l'API est compatible, la version install\u00e9e peut \u00eatre plus r\u00e9cente. On peut noter toutefois que normalement si une version majeur change, cela signifie une rupture de compatibilit\u00e9 dans l'API. Ici on voit que les d\u00e9veloppeurs ont d\u00e9cid\u00e9 de ne pas rompre la compatibilit\u00e9.

    Rebolote, on relance la configuration de Gimp\u2009:

    meson setup --prefix $GIMP_PREFIX 'python-fu-eval' is n_build\nRun-time dependency exiv2 found: NO (tried pkgconfig and cmake)\n

    On va donc installer libexiv2-dev et libgexiv2-dev. Au prochain probl\u00e8me, on installera la d\u00e9pendance manquante et ainsi de suite jusqu'\u00e0 ce que la configuration se passe sans erreur\u2009:

    apt install libgirepository1.0-dev libgtk-3-dev glib-networking appstream \\\nlibappstream-glib-dev libxmu-dev libbz2-dev libpoppler-glib-dev python3-gi \\\nxsltproc xvfb  libc6-dev gi-docgen gjs luajit libxml2-utils \\\ndesktop-file-utils iso-codes libopenjp2-7-dev libjxl-dev libasound2-dev \\\nlibgudev-1.0-dev libcfitsio-dev xdg-utils libunwind-dev lua5.1 libxpm-dev \\\nlibopenexr-dev libvala-0.56-dev valac qoi python3-gi python3-gi-cairo gir1.2-gtk-3.0\n

    Une fois configur\u00e9 il suffit de compiler le projet\u2009:

    ninja -C _build install\n

    Et voil\u00e0, Gimp est install\u00e9 dans le r\u00e9pertoire _install.

    ", "tags": ["_install", "atk"]}, {"location": "course-c/45-software-design/contribute/#reproduction", "title": "Reproduction", "text": "

    Si vous essayez de reproduire cet exemple vous aurez probablement d'autres erreurs car vous n'avez pas les m\u00eames version. Dans cet exemple, nous avons utilis\u00e9 Debian Bookworm (12)

    gimp

    e89bb33408c5ed006b7b92fbb7c793dde169b02a

    gegl

    72d86816289c11fdd871b701827f8bf0016a7f4f

    babl

    c5f97c86224a473cc3fea231f17adef84580f2cc

    "}, {"location": "course-c/45-software-design/contribute/#dependances", "title": "D\u00e9pendances", "text": "

    On voit apr\u00e8s cet exemple que certains projets peuvent \u00eatre complexes \u00e0 compiler. Les probl\u00e8mes r\u00e9currents sont\u2009:

    • Vous ne lisez pas la documentation
    • Vous \u00eates trop press\u00e9 et vous ne lisez pas les messages d'erreur
    • Vous pensez avoir lu la documentation mais vous n'avez pas vraiment lu la documentation

    En dehors de ces quelques probl\u00e8mes, il y a certaines d\u00e9pendances qui d\u00e9pendent de la distribution Linux/Unix que vous avez, les noms de biblioth\u00e8ques ne sont pas n\u00e9cessairement les m\u00eames, ou les version g\u00e9n\u00e8rent des conflits.

    Dans cet exemple, nous avons \u00e9t\u00e9 contraint de compiler GEGL et BABL depuis les sources avec des options sp\u00e9cifiques pour \u00eatre compatible avec la derni\u00e8re version de Gimp sous Git

    Ce que nous avons vu ici c'est que les d\u00e9pendances dans un projet logiciel peuvent \u00eatre nombreuses.

    Vous pouvez vous demander pourquoi utiliser autant de biblioth\u00e8ques externes\u2009? La r\u00e9ponse est simple\u2009: pour ne pas r\u00e9inventer la roue. Les biblioth\u00e8ques externes sont souvent des projets open-source tr\u00e8s bien maintenus et test\u00e9s. Elles permettent de gagner du temps et de l'argent en r\u00e9utilisant du code existant. Par exemple dans le cas de Gimp, la biblioth\u00e8que aa est utilis\u00e9e pour convertir des images en ASCII art. La biblioth\u00e8que iso-codes permet de conna\u00eetre les noms des pays dans diff\u00e9rentes langues. La biblioth\u00e8que heif permet le support des images en format natif des iPhones r\u00e9cents. On peut citer lcms2 pour la gestion des couleurs via le standard ICC, ou la biblioth\u00e8que mypaint pour la gestion des pinceaux et des calques de peinture.

    ", "tags": ["heif", "lcms2", "mypaint"]}, {"location": "course-c/45-software-design/dependencies/", "title": "Gestion des d\u00e9pendances", "text": "

    Les d\u00e9pendances sont des biblioth\u00e8ques ou des modules qui sont utilis\u00e9s dans un projet. Elles peuvent \u00eatre des biblioth\u00e8ques tierces, des modules internes ou des fichiers de configuration. La gestion des d\u00e9pendances est un aspect important du d\u00e9veloppement logiciel, car elle permet de g\u00e9rer les d\u00e9pendances entre les diff\u00e9rents composants d'un projet.

    Lorsque vous travaillez sur un projet en C ou d'ailleurs dans un autre langage, vous aurez souvent besoin d'utiliser des biblioth\u00e8ques tierces pour \u00e9tendre les fonctionnalit\u00e9s de votre programme et ne nous le cachons pas c'est toujours un enfer de g\u00e9rer les d\u00e9pendances. Nous allons voir quelques outils qui peuvent vous aider \u00e0 g\u00e9rer les d\u00e9pendances de votre projet.

    "}, {"location": "course-c/45-software-design/dependencies/#gestionnaire-de-dependances", "title": "Gestionnaire de d\u00e9pendances", "text": "

    Imagions le projet suivant. C'est un fichier unique qui utilise les biblioth\u00e8ques Curl, SQLite et PCRE. Voici le code source\u2009:

    #include <stdio.h>\n#include <curl/curl.h>\n#include <sqlite3.h>\n#include <pcre.h>\n\nint main() {\n    CURL *curl = curl_easy_init();\n    if(curl) {\n        curl_easy_setopt(curl, CURLOPT_URL, \"http://example.com\");\n        curl_easy_perform(curl);\n        curl_easy_cleanup(curl);\n    }\n\n    sqlite3 *db;\n    if (sqlite3_open(\":memory:\", &db) == SQLITE_OK) {\n        printf(\"SQLite database opened successfully.\\n\");\n        sqlite3_close(db);\n    }\n\n    const char *error;\n    int erroffset;\n    pcre *re = pcre_compile(\"hello\", 0, &error, &erroffset, NULL);\n    if (re) {\n        printf(\"PCRE compiled successfully.\\n\");\n        pcre_free(re);\n    }\n}\n

    Pour g\u00e9rer les d\u00e9pendances de ce projet il est possible d'utiliser diff\u00e9rents outils tels que CMake, Meson, Conan, etc.

    "}, {"location": "course-c/45-software-design/dependencies/#meson", "title": "Meson", "text": "

    Meson est un syst\u00e8me de build open-source con\u00e7u pour \u00eatre rapide, facile \u00e0 utiliser et extensible. Il est \u00e9crit en Python et est compatible avec de nombreux langages de programmation, y compris C, C++, Rust, Java, etc. Meson est particuli\u00e8rement utile pour les projets C/C++ car il prend en charge la compilation crois\u00e9e, la g\u00e9n\u00e9ration de fichiers de configuration et la gestion des d\u00e9pendances. Il se base sur Ninja pour la compilation. Il s'agit d'une alternative \u00e0 Make plus moderne et plus rapide.

    Commencez par installer Meson sur votre syst\u00e8me\u2009:

    sudo apt-get install meson ninja-build \\\n     libcurl4-openssl-dev libsqlite3-dev libpcre3-dev\n

    Puis cr\u00e9er un fichier meson.build pour configurer le projet\u2009:

    meson.build
    project('my_project', 'c', version: '0.1.0')\n\n# D\u00e9pendances\nlibcurl = dependency('libcurl', version: '>=8.5')\nsqlite = dependency('sqlite3', version: '>=3.1')\npcre = dependency('libpcre', version: '>=8.0')\n\n# Sources\nexecutable('my_project',\n  sources: files('main.c'),\n  dependencies: [libcurl, sqlite, pcre]\n)\n

    Enfin, configurez et compilez le projet avec Meson\u2009:

    $ meson setup build\n$ ninja -C build\n

    Meson ne g\u00e8re pas les d\u00e9pendances tierces, il faut donc les installer manuellement. Dans notre cas, nous avons install\u00e9 les d\u00e9pendances avec apt-get avant de configurer le projet.

    ", "tags": ["meson.build"]}, {"location": "course-c/45-software-design/dependencies/#conan", "title": "Conan", "text": "

    Conan est un gestionnaire de d\u00e9pendances C/C++ open-source, d\u00e9centralis\u00e9 et multiplateforme. Il permet de g\u00e9rer les d\u00e9pendances de vos projets C/C++ en automatisant le processus de t\u00e9l\u00e9chargement, de compilation et d'installation des biblioth\u00e8ques tierces. Il est particuli\u00e8rement utile sous Windows o\u00f9 la gestion des d\u00e9pendances est souvent plus complexe.

    Conan est \u00e9crit en Python et est compatible avec de nombreux syst\u00e8mes de build, tels que CMake, Make, Visual Studio, Xcode, etc. Pour l'installer sur votre syst\u00e8me utilisez la commande suivante\u2009:

    pip install conan\n

    Imaginons que nous voulons cr\u00e9er un projet qui d\u00e9pend des biblioth\u00e8ques SQlite, Curl et PCRE. Le projet aura la structure suivante\u2009:

    project/\n\u251c\u2500\u2500 CMakeLists.txt\n\u251c\u2500\u2500 conanfile.txt\n\u251c\u2500\u2500 main.c\n\u2514\u2500\u2500 build/  (cr\u00e9\u00e9 apr\u00e8s la configuration)\n

    Le fichier conanfile.txt contiendra les d\u00e9pendances du projet\u2009:

    [requires]\nlibcurl/8.9.1\nsqlite3/3.43.2\npcre/8.45\n\n[generators]\nCMakeToolchain\nCMakeDeps\n\n[options]\nlibcurl/*:shared=True\nsqlite3/*:shared=True\npcre/*:shared=True\n

    Enfin le fichier CMakeLists.txt contiendra les instructions pour inclure les biblioth\u00e8ques dans le projet\u2009:

    cmake_minimum_required(VERSION 3.15)\nproject(MyProject C)\n\ninclude(${CMAKE_BINARY_DIR}/conan_toolchain.cmake)\n\nadd_executable(my_project main.c)\n\nfind_package(CURL REQUIRED)\nfind_package(sqlite3 REQUIRED)\nfind_package(PCRE REQUIRED)\n\ntarget_link_libraries(my_project CURL::libcurl sqlite3::sqlite3 PCRE::PCRE)\n

    Commencez par compiler les d\u00e9pendances avec Conan\u2009:

    conan profile detect --force\ndetect_api: Found cc=gcc- 13.2.0\ndetect_api: gcc>=5, using the major as version\ndetect_api: gcc C++ standard library: libstdc++11\n\nDetected profile:\n[settings]\narch=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.cppstd=gnu17\ncompiler.libcxx=libstdc++11\ncompiler.version=13\nos=Linux\n
    $ conan install . --output-folder=build --build=missing\n...\n======== Computing dependency graph ========\nsqlite3/3.43.2: Not found in local cache, looking in remotes...\nsqlite3/3.43.2: Checking remote: conancenter\nsqlite3/3.43.2: Downloaded recipe revision c850734dc9724ee9aa9e5d95efd0b100\npcre/8.45: Not found in local cache, looking in remotes...\npcre/8.45: Checking remote: conancenter\npcre/8.45: Downloaded recipe revision 64cdfd792761c32817cd31d7967c3709\nbzip2/1.0.8: Not found in local cache, looking in remotes...\nbzip2/1.0.8: Checking remote: conancenter\nbzip2/1.0.8: Downloaded recipe revision 457c272f7da34cb9c67456dd217d36c4\n...\nconanfile.txt: Generating aggregated env files\nconanfile.txt: Generated aggregated env files: ['conanbuild.sh', 'conanrun.sh']\nInstall finished successfully\n

    Ensuite, configurez le projet avec CMake\u2009:

    $ cmake -B build\n
    ", "tags": ["conanfile.txt", "CMakeLists.txt"]}, {"location": "course-c/45-software-design/licenses/", "title": "Les Licences", "text": ""}, {"location": "course-c/45-software-design/licenses/#introduction", "title": "Introduction", "text": "

    Une licence logicielle est un contrat l\u00e9gal qui d\u00e9termine comment un logiciel peut \u00eatre utilis\u00e9, modifi\u00e9, et redistribu\u00e9 par les utilisateurs. Elle d\u00e9finit les droits et les obligations des utilisateurs vis-\u00e0-vis du cr\u00e9ateur du logiciel. Les licences logicielles sont cruciales parce qu'elles \u00e9tablissent les r\u00e8gles de propri\u00e9t\u00e9 intellectuelle, garantissent la protection des int\u00e9r\u00eats des auteurs tout en r\u00e9gulant l'acc\u00e8s \u00e0 la technologie, et favorisent l'innovation et la collaboration dans le cadre de projets open-source ou commerciaux.

    Les licences peuvent couvrir une large gamme d'autorisations, allant de licences strictes o\u00f9 l'utilisateur doit payer pour acc\u00e9der et utiliser le logiciel, \u00e0 des licences plus ouvertes qui permettent une libre utilisation et modification du code. La diversit\u00e9 de ces licences refl\u00e8te les diff\u00e9rentes philosophies et besoins du monde du d\u00e9veloppement logiciel.

    "}, {"location": "course-c/45-software-design/licenses/#historique", "title": "Historique", "text": ""}, {"location": "course-c/45-software-design/licenses/#logiciel-proprietaire", "title": "Logiciel Propri\u00e9taire", "text": "

    Dans les ann\u00e9es 1960 et 1970, les logiciels \u00e9taient souvent vendus avec le mat\u00e9riel informatique, et les codes sources \u00e9taient parfois fournis sans restrictions significatives. Cependant, avec l'\u00e9volution du march\u00e9 du logiciel, des entreprises ont commenc\u00e9 \u00e0 comprendre la valeur commerciale des logiciels. Ainsi, les premi\u00e8res licences logicielles restrictives ont \u00e9t\u00e9 d\u00e9velopp\u00e9es pour prot\u00e9ger les int\u00e9r\u00eats commerciaux des entreprises, restreignant l'acc\u00e8s au code source et limitant les droits des utilisateurs \u00e0 simplement utiliser le logiciel sans en voir les coulisses.

    "}, {"location": "course-c/45-software-design/licenses/#emergence-de-lopen-source", "title": "\u00c9mergence de l'Open Source", "text": "

    Avec l'av\u00e8nement de l'Internet et le besoin croissant de collaboration entre d\u00e9veloppeurs, une nouvelle approche a \u00e9merg\u00e9\u2009: le logiciel libre. Le projet GNU, lanc\u00e9 par Richard Stallman en 1983, est le fer de lance de cette r\u00e9volution. La licence GNU General Public License (GPL) est l'une des premi\u00e8res licences \u00e0 promouvoir l'id\u00e9e que les logiciels devraient \u00eatre libres d'utilisation, de modification, et de redistribution, \u00e0 condition que toutes les versions modifi\u00e9es soient \u00e9galement distribu\u00e9es sous la m\u00eame licence.

    Cependant, dans les ann\u00e9es 1990, une s\u00e9rie de conflits bien connus sous le nom de \u00ab\u2009guerres de licences\u2009\u00bb ont eu lieu entre diff\u00e9rents camps du mouvement open-source. La licence BSD, originaire du projet Berkeley Software Distribution, offrait une approche plus permissive que la GPL, permettant l'incorporation du code BSD dans des projets propri\u00e9taires sans exiger la lib\u00e9ration du code source. Cela contrastait fortement avec la philosophie de la GPL, qui impose un partage en retour (copyleft), obligeant toute d\u00e9rivation du code \u00e0 rester libre et open-source. Ce diff\u00e9rend a mis en lumi\u00e8re les diff\u00e9rentes visions de ce que devrait \u00eatre l'open-source\u2009: la libert\u00e9 pour les utilisateurs de faire ce qu'ils veulent avec le code, y compris le rendre propri\u00e9taire (BSD), versus la garantie que le code reste libre \u00e0 perp\u00e9tuit\u00e9 (GPL).

    Ce d\u00e9bat est \u00e0 la g\u00e9n\u00e8se de Linux, le noyau du syst\u00e8me d'exploitation GNU/Linux, qui a adopt\u00e9 la GPL pour garantir que le code source reste libre et ouvert. Aujourd'hui, la GPL reste l'une des licences open-source les plus populaires, mais d'autres licences plus permissives, comme la licence MIT et la licence Apache, ont \u00e9galement gagn\u00e9 en popularit\u00e9.

    La licence MIT, extr\u00eamement populaire dans la communaut\u00e9 open-source, adopte une approche encore plus simplifi\u00e9e et permissive que la licence BSD. Elle permet \u00e0 quiconque d'utiliser, copier, modifier, fusionner, publier, distribuer, sous-licencier et/ou vendre des copies du logiciel, avec comme seule obligation de mentionner l\u2019auteur original. Ce type de licence est tr\u00e8s appr\u00e9ci\u00e9 pour sa simplicit\u00e9 et sa flexibilit\u00e9.

    Les licences Creative Commons (CC), bien qu'initialement con\u00e7ues pour les \u0153uvres cr\u00e9atives non logicielles (comme Wikipedia), ont \u00e9galement influenc\u00e9 le domaine du logiciel en fournissant des cadres l\u00e9gaux pour la diffusion de contenu, incluant parfois du code source, avec diff\u00e9rents niveaux de protection des droits d'auteur.

    "}, {"location": "course-c/45-software-design/licenses/#licences-populaires", "title": "Licences Populaires", "text": "

    Voici un aper\u00e7u des licences logicielles les plus couramment utilis\u00e9es\u2009:

    GNU General Public License (GPL)

    Garantit que le logiciel reste libre \u00e0 jamais, encourage la collaboration, mais incompatibilit\u00e9 avec les licences plus permissives, contraignante pour les projets commerciaux.

    GNU Lesser General Public License (LGPL)

    Permet l'utilisation de biblioth\u00e8ques open-source dans des logiciels propri\u00e9taires mais moins contraignante que la GPL, mais impose des restrictions sur les modifications du code source.

    MIT License

    Simplicit\u00e9, grande permissivit\u00e9, utilis\u00e9e dans une multitude de projets, mais permet la r\u00e9utilisation du code dans des logiciels propri\u00e9taires sans obligation de partage.

    Apache License 2.0

    Clauses explicites sur les brevets, permissive mais prot\u00e8ge les contributeurs, mais moins simple que la licence MIT, moins contraignante que la GPL pour garantir la libert\u00e9 du logiciel.

    Berkeley Software Distribution (BSD) License

    Tr\u00e8s permissive, flexible pour les int\u00e9grations dans des logiciels propri\u00e9taires mais ne garantit pas que les versions d\u00e9riv\u00e9es resteront libres.

    Creative Commons (CC0, BY, BY-SA, etc.)

    Id\u00e9al pour la diffusion de contenus cr\u00e9atifs, permet une large diffusion avec un choix de niveaux de protection, mais moins couramment utilis\u00e9e pour les logiciels purs, difficult\u00e9 d\u2019adaptation pour certains projets techniques.

    Le choix d'une licence d\u00e9pend donc des objectifs du projet. On peut citer par exemple les cas de figures suivant\u2009:

    D\u00e9veloppement Commercial

    Pour les entreprises souhaitant conserver le contr\u00f4le total sur leur code, une licence propri\u00e9taire ou une licence permissive comme MIT ou BSD est souvent conseill\u00e9e.

    Protection de la Propri\u00e9t\u00e9 Intellectuelle

    La GPL est id\u00e9ale si l'objectif est de garantir que le logiciel reste libre et open-source, prot\u00e9geant ainsi les droits des utilisateurs \u00e0 modifier et redistribuer le code.

    Collaboration Ouverte

    La licence Apache ou MIT est souvent choisie pour des projets collaboratifs open-source, o\u00f9 la flexibilit\u00e9 et l\u2019ouverture sont primordiales, tout en assurant une protection contre les litiges de brevets (dans le cas d\u2019Apache).

    Projets Acad\u00e9miques et Exp\u00e9rimentaux

    Pour favoriser la diffusion rapide et libre des innovations, une licence tr\u00e8s permissive comme la licence MIT ou CC0 peut \u00eatre privil\u00e9gi\u00e9e.

    Contenu Cr\u00e9atif ou Documentation

    Les licences Creative Commons sont id\u00e9ales pour prot\u00e9ger et partager des documentations, des illustrations, ou des contenus audiovisuels associ\u00e9s aux projets logiciels.

    "}, {"location": "course-c/45-software-design/licenses/#brevets-logiciels", "title": "Brevets Logiciels", "text": "

    Un brevet logiciel est une protection juridique accord\u00e9e \u00e0 une invention logicielle, permettant \u00e0 son d\u00e9tenteur de contr\u00f4ler l'utilisation, la production, et la vente de cette invention pendant une p\u00e9riode donn\u00e9e, g\u00e9n\u00e9ralement 20 ans. Contrairement aux licences logicielles, qui r\u00e9gissent l'utilisation d'un logiciel, un brevet prot\u00e8ge une id\u00e9e ou une m\u00e9thode sp\u00e9cifique impl\u00e9ment\u00e9e dans le logiciel. Les brevets logiciels sont particuli\u00e8rement controvers\u00e9s en raison de leur complexit\u00e9, de leur co\u00fbt, et de leurs implications sur l'innovation technologique.

    Les brevets logiciels conf\u00e8rent \u00e0 leurs d\u00e9tenteurs un monopole sur une certaine technologie ou m\u00e9thode, leur permettant d'emp\u00eacher d'autres entreprises ou individus d'utiliser cette technologie sans autorisation. Cela peut \u00eatre avantageux pour les entreprises qui investissent massivement en R&D, car un brevet peut fournir un retour sur investissement en prot\u00e9geant leur innovation des concurrents.

    Cependant, ces brevets peuvent \u00e9galement freiner l'innovation en emp\u00eachant d'autres d\u00e9veloppeurs de construire sur des id\u00e9es existantes. Ils peuvent cr\u00e9er un environnement juridique complexe o\u00f9 les entreprises doivent naviguer prudemment pour \u00e9viter les poursuites en contrefa\u00e7on de brevet. Cela est particuli\u00e8rement probl\u00e9matique dans le d\u00e9veloppement de logiciels, o\u00f9 de nombreuses innovations sont incr\u00e9mentales et construites \u00e0 partir d'id\u00e9es pr\u00e9existantes.

    Le d\u00e9p\u00f4t d'un brevet logiciel est un processus co\u00fbteux, tant en termes de temps que d'argent. Les co\u00fbts incluent\u2009:

    Frais de d\u00e9p\u00f4t

    Les frais de d\u00e9p\u00f4t peuvent varier de quelques milliers \u00e0 plusieurs dizaines de milliers d'euros/dollars, selon le pays et la complexit\u00e9 du brevet.

    Frais juridiques

    L'assistance d'avocats sp\u00e9cialis\u00e9s est souvent n\u00e9cessaire pour pr\u00e9parer et d\u00e9poser un brevet, ce qui peut co\u00fbter plusieurs milliers d'euros suppl\u00e9mentaires.

    Frais de maintenance

    Une fois accord\u00e9, le brevet doit \u00eatre maintenu en vigueur, ce qui implique des frais p\u00e9riodiques (annuaires) qui peuvent \u00e9galement \u00eatre \u00e9lev\u00e9s.

    En raison de ces co\u00fbts, le d\u00e9p\u00f4t de brevets logiciels est souvent hors de port\u00e9e des petites entreprises et des d\u00e9veloppeurs individuels. M\u00eame pour les grandes entreprises, le co\u00fbt et la gestion des portefeuilles de brevets peuvent devenir un fardeau.

    D'autre part un brevet n'est pas universellement reconnu, et leur statut juridique varie d'un pays \u00e0 l'autre. Aux \u00c9tats-Unis par exemple, les brevets logiciels sont largement reconnus et accord\u00e9s par l'Office des brevets et des marques de commerce des \u00c9tats-Unis (USPTO). Cependant, une s\u00e9rie de d\u00e9cisions judiciaires r\u00e9centes, comme l'affaire Alice Corp. v. CLS Bank International en 2014, a restreint la port\u00e9e des brevets logiciels en exigeant que les inventions brevetables ne soient pas de simples \u00ab\u2009id\u00e9es abstraites\u2009\u00bb. En Europe, les brevets logiciels sont plus restrictifs. L'Office europ\u00e9en des brevets (OEB) n'accorde des brevets logiciels que si l'invention apporte une \u00ab\u2009contribution technique\u2009\u00bb au-del\u00e0 de l'algorithme ou du code lui-m\u00eame.

    Ces diff\u00e9rences de traitement rendent complexe la protection globale d'une innovation logicielle par un brevet, obligeant les entreprises \u00e0 adapter leurs strat\u00e9gies en fonction des juridictions.

    Pour ces raisons, beaucoup d'entreprises choisissent d'\u00e9viter les brevets logiciels. V\u00e9rifier la violation d'un brevet logiciel est notoirement difficile. Les logiciels peuvent \u00eatre \u00e9crits de plusieurs fa\u00e7ons pour atteindre un m\u00eame objectif fonctionnel, ce qui permet aux d\u00e9veloppeurs de contourner un brevet en modifiant l\u00e9g\u00e8rement leur code. Cela limite l'efficacit\u00e9 des brevets logiciels en tant que m\u00e9canisme de protection.

    De nombreuses entreprises pr\u00e9f\u00e8rent recourir \u00e0 d'autres strat\u00e9gies de protection telles que le secret commercial, qui est souvent moins co\u00fbteux et ne n\u00e9cessite pas de divulgation publique de l'invention, contrairement aux brevets.

    En conclusion, les brevets logiciels repr\u00e9sentent une arme \u00e0 double tranchant dans le domaine du g\u00e9nie logiciel. D'un c\u00f4t\u00e9, ils offrent une protection potentielle des innovations, permettant aux entreprises de capitaliser sur leurs investissements en R&D. De l'autre c\u00f4t\u00e9, leur co\u00fbt \u00e9lev\u00e9, la complexit\u00e9 juridique, les restrictions internationales, et la difficult\u00e9 \u00e0 v\u00e9rifier et \u00e0 faire respecter les brevets r\u00e9duisent leur attrait pour de nombreuses entreprises, en particulier dans un secteur en rapide \u00e9volution comme celui du d\u00e9veloppement logiciel.

    Ainsi, de nombreuses entreprises choisissent de ne pas breveter leurs logiciels, pr\u00e9f\u00e9rant d'autres formes de protection, ou simplement adopter une approche plus ouverte et collaborative. Les brevets logiciels demeurent un sujet controvers\u00e9, avec des implications profondes pour l'innovation, la concurrence, et la croissance \u00e9conomique dans le domaine des technologies de l'information.

    "}, {"location": "course-c/45-software-design/licenses/#conclusion", "title": "Conclusion", "text": "

    Les licences logicielles jouent un r\u00f4le central dans le d\u00e9veloppement et la distribution des logiciels. Elles influencent non seulement les droits et obligations des utilisateurs, mais aussi la mani\u00e8re dont les logiciels peuvent \u00e9voluer, \u00eatre partag\u00e9s, ou int\u00e9gr\u00e9s dans d'autres projets. Le choix d'une licence doit toujours \u00eatre guid\u00e9 par une compr\u00e9hension claire des objectifs du projet, des valeurs du cr\u00e9ateur, et des besoins des utilisateurs finaux. En fin de compte, la licence choisie peut d\u00e9terminer la port\u00e9e, l'impact et la durabilit\u00e9 d'un logiciel.

    "}, {"location": "course-c/45-software-design/software-project/", "title": "Organisation d'un projet", "text": ""}, {"location": "course-c/45-software-design/software-project/#introduction", "title": "Introduction", "text": "

    Dans ce chapitre, nous allons voir comment organiser un projet logiciel. Nous allons voir comment structurer un projet, comment g\u00e9rer les d\u00e9pendances, comment g\u00e9rer les tests unitaires et comment g\u00e9rer les tests fonctionnels.

    "}, {"location": "course-c/45-software-design/software-project/#structure-dun-projet", "title": "Structure d'un projet", "text": "

    La structure d'un projet logiciel est un \u00e9l\u00e9ment important pour sa maintenabilit\u00e9. Une bonne structure permet de retrouver facilement les fichiers sources, les fichiers d'en-t\u00eate, les tests, etc.

    Si votre projet est accessible par d'autres d\u00e9veloppeurs, il y a certaines conventions \u00e0 respecter pour que tout le monde puisse s'y retrouver.

    Tout commence avec un dossier racine. Ce dossier racine contient tous les fichiers sources, les fichiers d'en-t\u00eate, les tests, les d\u00e9pendances, etc. Encore faut-il bien nommer ce projet. Un nom de projet doit \u00eatre court, explicite et unique. Il est recommand\u00e9 de ne pas utiliser d'espaces, de caract\u00e8res sp\u00e9ciaux ou de majuscules.

    Les Majuscules

    Les noms de fichiers et de dossiers sont sensibles \u00e0 la casse sur les syst\u00e8mes POSIX mais pas sous Windows. Cela cr\u00e9e certains probl\u00e8mes de compatibilit\u00e9 entre les syst\u00e8mes d'exploitation.

    D'autre part, l'usage des majuscules peut cr\u00e9er des ambigu\u00eft\u00e9s. Par exemple, NomDuFichier et nomdufichier sont deux noms diff\u00e9rents. Comment allez-vous expliquer par t\u00e9l\u00e9phone \u00e0 un coll\u00e8gue que le fichier s'\u00e9crit de cette mani\u00e8re\u2009? L'ennui c'est les acronymes. Par exemple, XMLParser, XmlParser ou XMLparser\u2009? Vous aurez tendance \u00e0 choisir la troisi\u00e8me solution pour que XML ressort bien mais vous \u00eates incoh\u00e9rent puisque vous avez pas utilis\u00e9 de majuscul pour Parser.

    Le probl\u00e8me est bien r\u00e9solu avec l'utilisation d'underscores ou de tirets (notation kekbab-case ou snake_case). Par exemple, xml_parser est plus lisible et plus facile \u00e0 expliquer.

    Une convention en voque est de nommer les fichiers en minuscules et d'utiliser des tirets pour s\u00e9parer les mots. Par exemple, xml-parser. C'est la convention utilis\u00e9e sur GitHub pour le nom des d\u00e9p\u00f4ts.

    Voici une structure de projet classique\u2009:

    projet/\n\u251c\u2500\u2500 src/\n\u2502   \u251c\u2500\u2500 main.c\n\u2502   \u251c\u2500\u2500 foo.c\n\u2502   \u2514\u2500\u2500 bar.c\n\u251c\u2500\u2500 include/\n\u2502   \u251c\u2500\u2500 foo.h\n\u2502   \u2514\u2500\u2500 bar.h\n\u251c\u2500\u2500 tests/\n\u2502   \u251c\u2500\u2500 test_foo.c\n\u2502   \u2514\u2500\u2500 test_bar.c\n\u251c\u2500\u2500 Makefile\n\u2514\u2500\u2500 README.md\n

    Le point d'entr\u00e9e pour le d\u00e9veloppeur c'est le fichier README.md. Ce fichier contient une description du projet, des instructions pour l'installation, des instructions pour la compilation, des instructions pour les tests, etc.

    ", "tags": ["nomdufichier", "xml_parser", "README.md", "Parser", "XML", "NomDuFichier"]}, {"location": "course-c/45-software-design/software-project/#readmemd", "title": "README.md", "text": "

    Jadis, le fichier README \u00e9tait un fichier texte simple. Aujourd'hui, c'est un fichier Markdown. Le Markdown est un langage de balisage l\u00e9ger cr\u00e9\u00e9 en 2004 par John Gruber et Aaron Swartz. Il est facile \u00e0 lire et \u00e0 \u00e9crire. Il est utilis\u00e9 sur de tr\u00e8s nombreux supports\u2009: GitHub, GitLab, Bitbucket, Reddit, Stack Overflow, etc. C'est d'ailleurs le format utilis\u00e9 pour r\u00e9diger cet ouvrage.

    Voici un exemple de fichier README.md :

    # Nom du Projet\n\nDescription du projet.\n\n## Installation\n\nComment installer le projet :\n\n```bash\ngit clone http://...\ncd projet\nmake\n
    ", "tags": ["README", "Markdown", "README.md"]}, {"location": "course-c/45-software-design/software-project/#utilisation", "title": "Utilisation", "text": "

    Comment utiliser le projet\u2009:

    ./projet\n

    Le fichier README.md est un fichier important. Il doit \u00eatre \u00e0 jour et bien r\u00e9dig\u00e9 car c'est la premi\u00e8re chose que l'on voit lorsqu'on arrive sur le d\u00e9p\u00f4t du projet. Il doit permettre \u00e0 l'utilisateur rapidement\u2009:

    1. Que fait le projet et quelle est son utilit\u00e9\u2009?
    2. Est-ce que ce projet est fait pour moi\u2009?
    3. Comment l'installer\u2009?
    4. Comment l'utiliser\u2009?
    5. Comment contribuer\u2009?
    ", "tags": ["README.md"]}, {"location": "course-c/45-software-design/software-project/#dotfiles", "title": "Dotfiles", "text": "

    Dans un syst\u00e8me POSIX, les fichiers commen\u00e7ant par un point sont des fichiers cach\u00e9s. C'est une convention utilis\u00e9e pour les fichiers de configuration. Ces fichiers sont appel\u00e9s dotfiles.

    On va trouver toute une panoplie de fichiers de configuration dans un projet logiciel tel que\u2009:

    • .gitignore : liste des fichiers \u00e0 ignorer par Git.
    • .editorconfig : conventions de codage pour les \u00e9diteurs de texte.
    • .github/ : dossier contenant l'int\u00e9gration continue, les actions GitHub, etc.
    • .env : variables d'environnement pour le projet.
    ", "tags": ["dotfiles"]}, {"location": "course-c/45-software-design/software-project/#makefile", "title": "Makefile", "text": "

    Le Makefile est un fichier de configuration pour le programme make. C'est g\u00e9n\u00e9ralement le point d'entr\u00e9e pour construire un projet. Si vous trouvez un Makefile, vous savez que vous pourrez tr\u00e8s probablement simplement utiliser la commande make pour construire le projet.

    N\u00e9anmoins, parfois le Makefile n'est pas suffisant car votre projet est plus complexe et d\u00e9pend de plusieurs biblioth\u00e8ques tierces qui peuvent \u00eatre fastidieuses \u00e0 installer. C'est l\u00e0 qu'interviennent les gestionnaires de d\u00e9pendances que nous verrons plus tard.

    Alternativement, on peut trouver simplement un script build.sh ou build.bat pour construire le projet que vous appelerer avec ./build.sh ou .\\build.bat.

    ", "tags": ["build.bat", "Makefile", "make", "build.sh"]}, {"location": "course-c/45-software-design/software-project/#organisation-des-fichiers-sources", "title": "Organisation des fichiers sources", "text": "

    Lorsque le projet d\u00e9passe 5 \u00e0 10 fichiers, il est habituel de les d\u00e9placer dans un dossier pour ne pas encombrer la racine du projet. On cr\u00e9e un dossier src pour les fichiers sources qui va contenir tous les fichiers .c.

    Faut-il s\u00e9parer les fichiers sources des fichiers d'en-t\u00eate\u2009? C'est une question de go\u00fbt. Certains d\u00e9veloppeurs pr\u00e9f\u00e8rent tout mettre dans un seul dossier src pour simplifier la navigation. D'autres pr\u00e9f\u00e8rent s\u00e9parer les fichiers sources des fichiers d'en-t\u00eate. Cela permet de mieux organiser le projet et de mieux g\u00e9rer les d\u00e9pendances. Il n'y a pas de consensus \u00e9tabli mais on peut noter plusieurs points.

    Lorsqu'un projet est destin\u00e9 \u00e0 devenir une biblioth\u00e8que partag\u00e9e, vous devez fournir les fichiers d'en-t\u00eate pour que les autres d\u00e9veloppeurs puissent utiliser votre biblioth\u00e8que. S'ils sont s\u00e9par\u00e9s, c'est plus facile de les extraire et les distribuer.

    N\u00e9anmoins, s\u00e9placer les fichiers d'en-t\u00eate dans un dossier include n\u00e9cessite d'informer le compilateur de l'emplacement des fichiers d'en-t\u00eate. C'est g\u00e9n\u00e9ralement fait avec l'option -I de GCC\u2009:

    gcc -Iinclude -c src/*.c\n
    ", "tags": ["include", "src"]}, {"location": "course-c/45-software-design/software-project/#repertoire-de-construction", "title": "R\u00e9pertoire de construction", "text": "

    Lorsque vous compilez un projet, vous allez g\u00e9n\u00e9rer des fichiers interm\u00e9diaires (fichiers objets) et des fichiers finaux (ex\u00e9cutables, biblioth\u00e8ques). Selon le projet il peut y avoir beaucoup de fichiers. Afin d'\u00e9viter de ne voir ces fichiers dans votre explorateur de fichiers, on cr\u00e9e un dossier build pour les stocker.

    Ceci est une convention. Vous pouvez choisir un autre nom pour ce dossier. Par exemple, bin, out, dist, etc.

    N\u00e9anmoins, il faut d'une part penser \u00e0 ajouter ce dossier dans le .gitignore pour ne pas le versionner, mais \u00e9galement modifier la configuration de votre compilateur pour qu'il place les fichiers objets dans ce dossier\u2009:

    mkdir -p build\ngcc -Iinclude -c src/*.c -o build/*.o\n
    ", "tags": ["out", "build", "bin", "dist"]}, {"location": "course-c/45-software-design/software-project/#en-tetes-de-fichiers", "title": "En-t\u00eates de fichiers", "text": "

    Historiquement, les d\u00e9veloppeurs C utilisait l'outil Doxygen pour g\u00e9n\u00e9rer de la documentation \u00e0 partir des commentaires dans le code source. Cela a donn\u00e9 naissance \u00e0 une convention pour les commentaires de documentation qui est souvent utilis\u00e9e de mani\u00e8re abusive.

    Aujourd'hui, avec les \u00e9diteurs modernes, il est plus facile de naviguer dans le code source et de trouver des informations sur les fonctions et les structures. Les commentaires de documentation sont souvent redondants et inutiles, ils polluent plus qu'ils n'aident. N\u00e9anmoins, je peux vous donner un exemple de commentaires de documentation pour un fichier d'en-t\u00eate\u2009:

    /**\n * @brief D\u00e9finition des fonctions pour manipuler des nombres\n * @file numbers.h\n * @date 2024-09-01\n * @author John Doe\n * @version 1.0\n * @copyright Copyright (c) 2021\n *\n * Description d\u00e9taill\u00e9e.\n */\n\n/*********************************************************/\n/*                       INCLUDES                        */\n/*********************************************************/\n#include <stdio.h>\n\n/*********************************************************/\n/*                       STRUCTURES                      */\n/*********************************************************/\ntypedef struct point {\n    int x; //!< Abscisse du point.\n    int y; //!< Ordonn\u00e9e du point.\n} Point; //!< Structure point.\n\n/*********************************************************/\n/*                       FONCTIONS                       */\n/*********************************************************/\n\n/**\n * @brief Fonction d'addition de deux entiers.\n * @param a Premier entier.\n * @param b Deuxi\u00e8me entier.\n * @return La somme des deux entiers.\n *\n * Cette fonction permet d'additionner deux entiers.\n */\nint add(int a, int b) {\n    return a + b;\n}\n\n/*********************************************************/\n/*                       FIN                             */\n/*********************************************************/\n

    Nous pouvons maintenant discuter de la pertinence de ces commentaires. Int\u00e9ressons-nous tout d'abord \u00e0 l'en-t\u00eate du fichier\u2009:

    @file

    Le nom du fichier est en redondance avec le vrai nom du fichier. Si vous renommez le fichier, vous devrez \u00e9galement changer ce commentaire. C'est d'une part une source d'erreur mais surtout pour Git, cela implique que le fichier n'est pas simplement renomm\u00e9 mais supprim\u00e9 et recr\u00e9\u00e9. Cela peut poser des probl\u00e8mes de fusion et d'acc\u00e8s \u00e0 l'historique. Aussi, si vous utilisez Git, cette information est parfaitement inutile.

    @date

    La date est inutile car si vous avez besoin de la date, vous pouvez utiliser Git pour voir la date de la derni\u00e8re modification du fichier. D'autre part, il est courant qu'un fichier soit modifi\u00e9 sans que la date soit mise \u00e0 jour. Cela peut \u00eatre une source de confusion.

    @author

    Si vous utilisez Git, vous poss\u00e9dez d\u00e9j\u00e0 cette information et de mani\u00e8re beaucoup plus fine. Vous pouvez voir qui a modifi\u00e9 chaque ligne du fichier. G\u00e9rer cette information dans le fichier c'est \u00e9galement s'astreindre \u00e0 ajouter son nom, m\u00eame si vous n'avez ajout\u00e9 qu'une ligne. Et puis, si vous aimez tant le fichier que vous venez tant \u00e0 le modifier que vous avez chang\u00e9 toutes les lignes, devez-vous supprimer le nom du vrai auteur pour mettre le v\u00f4tre\u2009? C'est une source de conflit, est c'est beaucoup plus simple de laisser Git g\u00e9rer cette information.

    @version

    M\u00eame rengaine et m\u00eame punition. Si vous utilisez Git, vous avez d\u00e9j\u00e0 cette information. Git g\u00e8re les version avec les tags et les branches. Vous pouvez voir l'historique des modifications et les diff\u00e9rences entre les versions. D'autre part, pour les m\u00eame raisons qu'\u00e9voqu\u00e9es, \u00e0 chaque nouvelle version de votre projet, vous devez modifier ce commentaire dans tous les fichiers. Cela implique que tous ces fichiers vont changer et vous allez vous perdre dans l'historique.

    @brief

    Enfin une information utile. C'est une excellente pratique que d'avoir une description courte du fichier. N\u00e9anmoins avec Doxygen il existe l'option JAVADOC_AUTOBRIEF qui permet de g\u00e9n\u00e9rer automatiquement le @brief \u00e0 partir de la premi\u00e8re phrase du commentaire si elle se termine par un point. C'est une option \u00e0 activer pour \u00e9viter de r\u00e9p\u00e9ter la m\u00eame chose.

    @copyright

    C'est une information importante. Elle permet de savoir comment le fichier peut \u00eatre utilis\u00e9. N\u00e9anmoins, il est rare que pour un projet donn\u00e9, le copyright et la licence changent d'un fichier \u00e0 l'autre, il est donc plus judicieux de mettre cette information dans le README ou mieux dans un fichier LICENSE.

    Maintenant que nous avons balay\u00e9 l'en-t\u00eate, concentrons-nous sur la fonction add. Cette fonction fait 3 lignes et le commentaire 8 lignes. Est-ce r\u00e9ellement n\u00e9cessaire de documenter une fonction que tout le monde peut comprendre\u2009?

    La r\u00e9ponse n'est pas si simple. Si vous publier le code source avec votre biblioth\u00e8que, la r\u00e9ponse est non car n'importe qui pourra la comprendre. N\u00e9anmoins votre biblioth\u00e8que est publi\u00e9e sans le code source, la seule chose que votre utlisateur aura c'est le fichier d'en-t\u00eate (.h) :

    int add(int a, int a);\n

    C'est peut-\u00eatre un peu court et un compl\u00e9ment d'information pourrait \u00eatre utile. Mais faut-il documenter tous les param\u00e8tres\u2009? Si le brief dit que la fonction additionne deux entiers, et que vous avez acc\u00e8s aux types des param\u00e8tres, est-ce vraiment n\u00e9cessaire de documenter les param\u00e8tres\u2009? La r\u00e9ponse est non. Je le r\u00e9p\u00e8te souvent, un commentaire est fait pour expliquer le pourquoi, pas le comment. Si vous avez besoin de documenter le comment, c'est que votre code n'est pas assez clair. On pourrait par exemple r\u00e9\u00e9crire la fonction de la mani\u00e8re suivante\u2009:

    int add_two_integers_together(int first, int second) {\n    return first + second;\n}\n

    Je vous l'accorde, c'est un peu long, mais \u00e7a \u00e0 le m\u00e9rire d'\u00eatre parfaitement clair.

    Questionnons maintenant les s\u00e9parateurs de contenu. Lorsque vous d\u00e9butez en programmation, vous avez tendance \u00e0 vous faire mousser un peu en \u00e9crivant plus que le n\u00e9cessaire, \u00e9crire des commentaires c'est sans risque et cela rallonge votre beau programme. Alors pour bien structurer votre code, vous avez mis des s\u00e9parateurs de contenu. N\u00e9anmoins ils n'apportent pas grand chose. En Python par exemple, la norme propose d'ajouter deux retour \u00e0 la ligne entre chaque fonction. C'est une convention qui est largement suffisante pour s\u00e9parer les diff\u00e9rents \u00e9l\u00e9ments du code.

    Enfin, la derni\u00e8re question est de savoir si le commentaire de la structure Point est utile. Pour cette structure qui est tr\u00e8s simple, on pourrait se passer de commentaires, mais dans le cas ou les \u00e9l\u00e9ments n\u00e9cessitent d'\u00eatre expliqu\u00e9s, c'est la notation Doxygen qui peut \u00eatre discut\u00e9e.

    Doxygen utilise la notation //! pour les commentaires de documentation. C'est une notation sp\u00e9ciale qui a la facheuse tendance s'afficher en rouge vif dans certains \u00e9diteurs de texte. Si vous n'utilisez pas Doxygen, il est pr\u00e9f\u00e9rable de ne pas utiliser cette notation. Il est pr\u00e9f\u00e9rable de rester sur la notation // qui est plus universelle.

    Apr\u00e8s ces longues explications, je vous propose de vous redonner l'exemple simplifi\u00e9\u2009:

    /**\n * D\u00e9finition des fonctions pour manipuler des nombres\n *\n * Description d\u00e9taill\u00e9e.\n */\n#include <stdio.h>\n\ntypedef struct point {\n    int x; //!< Abscisse du point.\n    int y; //!< Ordonn\u00e9e du point.\n} Point; //!< Structure point.\n\nint add_two_integers(int a, int b) {\n    return a + b;\n}\n

    Le commentaire de trop

    /*********************************************************/\n/*                    FIN DU FICHIER                     */\n/*********************************************************/\n

    Diable\u2009! Pourquoi trouve-t-on toujours un commentaire FIN \u00e0 la fin des fichiers\u2009? C'est une pratique qui remonte \u00e0 l'\u00e9poque des cartes perfor\u00e9es. Les cartes perfor\u00e9es \u00e9taient utilis\u00e9es pour stocker les programmes. Chaque carte avait un num\u00e9ro de ligne et un num\u00e9ro de colonne. Pour \u00e9viter de m\u00e9langer les cartes, on mettait un commentaire FIN \u00e0 la fin de chaque carte. C'est une pratique ancestrale qui a perdur\u00e9 jusqu'\u00e0 aujourd'hui. Les d\u00e9veloppeurs r\u00e9p\u00e8tent inlassablement ces habitudes sans toutefois les remettre en question.

    Aujourd'hui, un fichier est fini au sens que lorsqu'on en atteint la fin, on le sait. Il n'est alors pas pertinent de le pr\u00e9ciser.

    C'est un peu la m\u00eame chose que ces pages dans les livres laiss\u00e9es blanches, ou presque, avec un commentaire \u00ab\u2009Cette page a \u00e9t\u00e9 intentionnellement laiss\u00e9e blanche\u2009\u00bb qui remontait \u00e0 l'\u00e9poque de l'impression en offset. Lorsqu'on imprimait un livre, on imprimait les pages par feuille de 16 pages. Si le livre faisait 200 pages, il fallait ajouter 4 pages blanches \u00e0 la fin pour que l'impression soit correcte. Aujourd'hui, les livres sont imprim\u00e9s en num\u00e9rique et il n'y a plus besoin de ces pages blanches.

    ", "tags": ["Point", "LICENSE", "add", "README", "FIN"]}, {"location": "course-c/45-software-design/software-project/#gestion-de-configuration", "title": "Gestion de configuration", "text": "

    La gestion de configuration est un \u00e9l\u00e9ment important pour un projet logiciel. Elle permet de stocker des informations qui peuvent varier d'un environnement \u00e0 un autre. Par exemple, les informations de connexion \u00e0 une base de donn\u00e9es, les cl\u00e9s d'API, les adresses IP, etc.

    Il existe plusieurs solutions pour g\u00e9rer la configuration d'un projet\u2009:

    • Les variables d'environnement.
    • Les fichiers de configuration.
    "}, {"location": "course-c/45-software-design/software-project/#variables-denvironnement", "title": "Variables d'environnement", "text": "

    Les variables d'environnement sont des variables stock\u00e9es dans l'environnement d'ex\u00e9cution d'un programme. Elles sont accessibles par le programme et peuvent \u00eatre modifi\u00e9es par l'utilisateur. Ces variables sont propag\u00e9es \u00e0 tous les processus enfants pour autant qu'elles ait \u00e9t\u00e9 export\u00e9es. Elles peuvent \u00eatre utile dans un projet pour stocker des informations sensibles qui ne devraient pas \u00eatre versionn\u00e9es.

    Prenons l'exemple d'une cl\u00e9 d'API. Cette cl\u00e9 vous permet d'acc\u00e9der \u00e0 un service tiers depuis votre programme. Chaque utilisateur de votre programme doit avoir sa propre cl\u00e9 d'API. Si vous stockez cette cl\u00e9 dans le code source, vous risquez de la versionner et de la rendre publique. Si vous stockez cette cl\u00e9 dans un fichier de configuration, vous risquez de le versionner et de le rendre public \u00e9galement. Une bonne solution est de stocker cette cl\u00e9 dans une variable d'environnement.

    Il se peut d'ailleurs que votre projet n\u00e9cessite plusieurs variables d'environnement. On utilise couramment un fichier .env.example pour lister les variables d'environnement n\u00e9cessaires au projet. Ce fichier est versionn\u00e9 et contient des valeurs par d\u00e9faut. L'utilisateur doit copier ce fichier en .env et renseigner les valeurs.

    .env.example
    API_KEY=your_api_key\nDATABASE_URL=your_database_url\n
    .env
    API_KEY=1234567890\nDATABASE_URL=postgres://user:password@localhost:5432/database\n

    Pour d\u00e9ployer votre environnement de d\u00e9veloppement, vous devez exporter ces variables d'environnement. Vous pouvez le faire dans votre terminal ou dans un fichier de configuration de votre terminal (.bashrc, .zshrc, etc.).

    export $(cat .env | xargs)\n

    Sous Windows vous pouvez utiliser la commande set :

    for /f \"delims== tokens=1,2\" %i in (.env) do set %i=%j\n

    Ces commandes sont g\u00e9n\u00e9ralement int\u00e9gr\u00e9es dans un script start.sh ou start.bat pour d\u00e9marrer votre projet, ou alors directement dans le Makefile.

    ", "tags": ["Makefile", "start.sh", "start.bat", "set"]}, {"location": "course-c/45-software-design/software-project/#fichier-den-tete", "title": "Fichier d'en-t\u00eate", "text": "

    On trouve tr\u00e8s souvent dans un projet C un fichier d'en-t\u00eate .h qui contient des d\u00e9finitions de configuration. Par exemple, un fichier config.h qui contient des constantes, des macros, des structures, etc.

    #ifndef CONFIG_H\n#define CONFIG_H\n\n#define VERSION \"1.0.0\"\n#define AUTHOR \"John Doe\"\n\n#define USE_FEATURE_A\n#define USE_FEATURE_B\n\n#define API_KEY \"your_api_key\"\n#define DATABASE_URL \"your_database_url\"\n

    Ce fichier est inclus dans les fichiers sources qui ont besoin de ces informations. Il est versionn\u00e9 et donc ne devrait pas contenir d'informations sensibles. Si vous avez des informations sensibles, vous pouvez les stocker dans un fichier .env et les exporter dans les variables d'environnement. Ces variables peuvent \u00eatre utilis\u00e9e dans votre Makefile pour d\u00e9clarer des variables de compilation.

    include .env\n\nCFLAGS += -DAPI_KEY=\\\"$(API_KEY)\\\"\nCFLAGS += -DDATABASE_URL=\\\"$(DATABASE_URL)\\\"\n
    ", "tags": ["config.h"]}, {"location": "course-c/45-software-design/teamwork/", "title": "Travail en \u00e9quipe", "text": ""}, {"location": "course-c/45-software-design/teamwork/#introduction", "title": "Introduction", "text": "

    Dans le monde du d\u00e9veloppement logiciel, le travail en \u00e9quipe est incontournable. Tr\u00e8s rarement vous travaillerez seul sur un projet, surtout dans un contexte professionnel. La collaboration avec d'autres d\u00e9veloppeurs, testeurs, chefs de projet, graphistes, commerciaux, et autres acteurs est essentielle pour r\u00e9ussir. Savoir travailler en \u00e9quipe est donc une comp\u00e9tence cruciale \u00e0 d\u00e9velopper pour tout professionnel du secteur.

    "}, {"location": "course-c/45-software-design/teamwork/#travail-en-entreprise", "title": "Travail en entreprise", "text": "

    Le travail en entreprise, qu'il s'agisse d'une petite ou d'une grande structure, pr\u00e9sente des dynamiques et des d\u00e9fis uniques. Selon la taille de l'entreprise, les m\u00e9thodes de travail, la communication et la gestion des responsabilit\u00e9s peuvent varier consid\u00e9rablement. Il est important de comprendre ces diff\u00e9rences pour mieux s'adapter aux environnements professionnels vari\u00e9s et pour anticiper les d\u00e9fis potentiels. Une fois sorti de l'\u00e9cole, vous aurez le choix entre travailler dans une start-up, une PME, une grande entreprise ou m\u00eame en freelance. Chacun de ces environnements a ses avantages et ses inconv\u00e9nients.

    "}, {"location": "course-c/45-software-design/teamwork/#petite-entreprise", "title": "Petite entreprise", "text": "

    Dans une petite entreprise, les \u00e9quipes sont souvent r\u00e9duites (souvent 2 \u00e0 5 personnes), ce qui favorise une communication directe et une grande flexibilit\u00e9. Les r\u00f4les peuvent \u00eatre moins d\u00e9finis et plus polyvalents, chaque membre de l'\u00e9quipe \u00e9tant susceptible de porter plusieurs casquettes. Cette polyvalence peut \u00eatre stimulante, car elle offre l'opportunit\u00e9 d'acqu\u00e9rir une vari\u00e9t\u00e9 de comp\u00e9tences et de participer \u00e0 divers aspects du projet. En qualit\u00e9 d'\u00e9lectronicien vous serez amen\u00e9 \u00e0 g\u00e9rer des projets de A \u00e0 Z, et vous investir aussi dans le d\u00e9veloppement logiciel. Vous pourriez \u00eatre amen\u00e9 \u00e0 faire vous m\u00eame les choix technologiques selon vos crit\u00e8res de s\u00e9lection.

    La prise de d\u00e9cision dans une petite entreprise est aussi g\u00e9n\u00e9ralement plus rapide. Les hi\u00e9rarchies sont souvent plates, et il est plus facile de collaborer directement avec les dirigeants ou les fondateurs. Cette proximit\u00e9 permet de r\u00e9agir rapidement aux changements, d'innover plus facilement, et de mettre en \u0153uvre des id\u00e9es sans passer par de longs processus de validation. En outre, les relations de confiance peuvent permettre d'avoir carte blanche pour explorer de nouvelles id\u00e9es et de prendre des initiatives.

    Cependant, cette flexibilit\u00e9 a aussi ses limites. Le manque de ressources peut parfois conduire \u00e0 une surcharge de travail, o\u00f9 les employ\u00e9s doivent jongler entre plusieurs responsabilit\u00e9s. Cela peut cr\u00e9er des tensions si les priorit\u00e9s ne sont pas bien g\u00e9r\u00e9es. De plus, l'absence de processus formalis\u00e9s peut entra\u00eener des conflits de responsabilit\u00e9, surtout lorsque les t\u00e2ches ne sont pas clairement assign\u00e9es.

    Une petite entreprise ou start/up vit souvent au jour le jour, et les priorit\u00e9s peuvent changer rapidement. L'attitude est \u00e0 la survie, la recherche de financement, et la croissance. Les projets sont souvent courts, et les \u00e9quipes sont amen\u00e9es \u00e0 pivoter rapidement en fonction des retours clients.

    En termes de d\u00e9veloppement logiciel, cela se traduit par des projets agiles, des it\u00e9rations rapides, et une forte collaboration entre les membres de l'\u00e9quipe. La communication est essentielle pour s'assurer que tout le monde est sur la m\u00eame longueur d'onde et que les objectifs sont clairs. Exploiter au mieux la communit\u00e9 open-source est souvent une strat\u00e9gie gagnante pour les petites entreprises, qui peuvent ainsi b\u00e9n\u00e9ficier de logiciels et de biblioth\u00e8ques de qualit\u00e9 sans avoir \u00e0 les d\u00e9velopper elles-m\u00eames ce qui se traduit par un consid\u00e9rable gain de temps. N\u00e9anmoins, il est fondamental de bien comprendre les licences open-source pour \u00e9viter tout probl\u00e8me de conformit\u00e9.

    Le processus d'engagement de nouveaux employ\u00e9s est souvent rapide et bas\u00e9 sur le feeling humain davantage que par le niveau de qualification. Les comp\u00e9tences techniques sont importantes, mais la personnalit\u00e9 et la capacit\u00e9 \u00e0 s'int\u00e9grer dans une \u00e9quipe sont souvent des crit\u00e8res de s\u00e9lection tout aussi importants. D'autre part, les petites entreprises n'ont pas toujours les moyens de proposer des salaires comp\u00e9titifs, mais elles peuvent offrir des avantages en nature, des opportunit\u00e9s de croissance rapide, et une ambiance de travail conviviale.

    "}, {"location": "course-c/45-software-design/teamwork/#grande-entreprise", "title": "Grande entreprise", "text": "

    Dans une grande entreprise (300..50'000 employ\u00e9s), les processus sont beaucoup plus formalis\u00e9s et structur\u00e9s. Bien que cela puisse assurer une certaine coh\u00e9rence et standardisation dans les pratiques de travail, cela condut in\u00e9vitablement \u00e0 une inertie organisationnelle. Les d\u00e9cisions transitent par plusieurs niveaux hi\u00e9rarchiques, ce qui peut ralentir l'innovation et rendre l'adaptation aux changements plus difficile.

    Par ailleurs de nombreuses grandes entreprises adh\u00e8rent \u00e0 des normes de qualit\u00e9 et de s\u00e9curit\u00e9 strictes, ce qui peut \u00eatre un avantage pour les projets critiques. Les processus de validation et de test sont g\u00e9n\u00e9ralement plus rigoureux, ce qui garantit un niveau de qualit\u00e9 \u00e9lev\u00e9. Cependant, cela peut aussi ralentir le cycle de d\u00e9veloppement et rendre les projets moins flexibles. La compliance avec la norme ISO 9001 est souvent un pr\u00e9requis pour travailler avec des grandes entreprises. Elle impose une nomenclature stricte, des processus de validation et de test, et une documentation compl\u00e8te des projets qui sont on un impact non n\u00e9gligeable sur le temps de d\u00e9veloppement et la motivation des \u00e9quipes qui peuvent se sentir brid\u00e9es dans leur cr\u00e9ativit\u00e9 et peuvent sembler faire trop de paperasse administrative sans valeur ajout\u00e9e.

    Le cloisonnement (ou \u00ab\u2009silos\u2009\u00bb) est un ph\u00e9nom\u00e8ne courant dans les grandes entreprises. Les diff\u00e9rents d\u00e9partements ou \u00e9quipes peuvent fonctionner de mani\u00e8re relativement autonome, avec peu d'interactions entre eux. Ce cloisonnement est souvent la source de tensions lorsqu'il y a un manque de communication ou de coordination entre les \u00e9quipes. Par exemple, une \u00e9quipe de d\u00e9veloppement peut se retrouver en conflit avec une \u00e9quipe de marketing si les objectifs ne sont pas align\u00e9s ou si les informations ne sont pas partag\u00e9es efficacement. Il n'est pas rare que le d\u00e9partement des ventes promette des fonctionnalit\u00e9s qui ne sont pas encore d\u00e9velopp\u00e9es, ou que le d\u00e9partement marketing communique sur des dates de sortie qui ne sont pas encore fix\u00e9es ou qui ne sont pas r\u00e9alistes. Ces d\u00e9calages peuvent entra\u00eener des frustrations et des malentendus, et nuire \u00e0 la qualit\u00e9 du produit final, notament en coupant les coins ronds pour respecter des d\u00e9lais irr\u00e9alistes. Cela se traduit dans le d\u00e9veloppement par des dettes techniques croissantes et des retours de flamme parfois catastrophiques.

    Le cloisonnement peut \u00e9galement entra\u00eener des conflits de responsabilit\u00e9. Dans une grande entreprise, il peut \u00eatre difficile de savoir qui est responsable de quoi, surtout lorsque plusieurs \u00e9quipes travaillent sur un m\u00eame projet. Les probl\u00e8mes de coordination peuvent mener \u00e0 des malentendus, des retards, et m\u00eame \u00e0 des conflits internes lorsque des t\u00e2ches importantes sont n\u00e9glig\u00e9es ou mal ex\u00e9cut\u00e9es. Les d\u00e9cisions se prennent souvent en comit\u00e9 de direction sans l'avis des \u00e9quipes techniques qui sont souvent les plus \u00e0 m\u00eame de comprendre les implications techniques des d\u00e9cisions prises. Cela m\u00e8ne souvent \u00e0 des d\u00e9cisions non optimales.

    La complexit\u00e9 des structures hi\u00e9rarchiques dans les grandes entreprises peut aussi engendrer des frustrations. Les employ\u00e9s peuvent se sentir \u00e9loign\u00e9s des d\u00e9cideurs et avoir l'impression que leurs contributions individuelles passent inaper\u00e7ues. Par ailleurs, le besoin de se conformer \u00e0 des processus rigides peut parfois \u00e9touffer la cr\u00e9ativit\u00e9 et l'innovation, rendant les employ\u00e9s moins enclins \u00e0 proposer de nouvelles id\u00e9es. Les employ\u00e9s peuvent se sentir n'\u00eatre que des num\u00e9ros. N\u00e9anmoins c'est une ligne d\u00e9cisionnelle de s'assurer qu'un employ\u00e9 ne devienne pas indispensable pour l'entreprise, et que le savoir soit partag\u00e9 entre les membres de l'\u00e9quipe afin d'\u00e9viter le \u00ab\u2009bus factor\u2009\u00bb (si un employ\u00e9 se fait renverser par un bus, l'entreprise ne doit pas s'arr\u00eater de fonctionner).

    Cependant, travailler dans une grande entreprise offre aussi de nombreux avantages. Les ressources disponibles sont souvent plus importantes, permettant un meilleur acc\u00e8s \u00e0 des formations, des technologies avanc\u00e9es, et des opportunit\u00e9s de d\u00e9veloppement de carri\u00e8re. La sp\u00e9cialisation des r\u00f4les peut aussi permettre aux employ\u00e9s de devenir experts dans un domaine particulier, avec un soutien organisationnel plus structur\u00e9. L'acc\u00e8s \u00e0 des logiciels de pointe et \u00e0 des ressources de formation est souvent plus facile dans une grande entreprise, qui peut se permettre d'investir.

    Le processus d'engagement dans une grande entreprise est souvent plus formel et structur\u00e9, avec des entretiens multiples, des tests techniques, et des \u00e9valuations approfondies. Les comp\u00e9tences techniques, notament en g\u00e9nie logiciel sont \u00e9valu\u00e9es par un code interview qui n\u00e9cessite de r\u00e9soudre des probl\u00e8mes algorithmiques en temps limit\u00e9.

    "}, {"location": "course-c/45-software-design/teamwork/#comparaison-des-dynamiques", "title": "Comparaison des dynamiques", "text": "

    La diff\u00e9rence fondamentale entre une petite et une grande entreprise r\u00e9side dans la mani\u00e8re dont les processus et la communication sont g\u00e9r\u00e9s. Dans une petite entreprise, la rapidit\u00e9 et la flexibilit\u00e9 sont souvent privil\u00e9gi\u00e9es, mais cela peut se faire au d\u00e9triment de la clart\u00e9 des r\u00f4les et des responsabilit\u00e9s. \u00c0 l'inverse, dans une grande entreprise, la structure et les processus sont plus \u00e9tablis, mais cela peut engendrer de l'inertie et des d\u00e9fis en termes de collaboration inter-\u00e9quipes.

    "}, {"location": "course-c/45-software-design/teamwork/#equipe-de-developpement", "title": "\u00c9quipe de d\u00e9veloppement", "text": "

    Dans le domaine du d\u00e9veloppement logiciel, une \u00e9quipe de d\u00e9veloppement r\u00e9unit plusieurs personnes ayant des comp\u00e9tences diverses et compl\u00e9mentaires. Dans les grandes entreprises ces r\u00f4les sont tr\u00e8s souvent cloisonn\u00e9s, alors que dans les petites entreprises, les membres de l'\u00e9quipe peuvent \u00eatre amen\u00e9s \u00e0 porter plusieurs casquettes. N\u00e9anmoins on retrouve g\u00e9n\u00e9ralement les r\u00f4les suivants\u2009:

    Chef de projet

    Responsable de la gestion globale du projet, il assure la coordination des \u00e9quipes et veille \u00e0 ce que le projet soit livr\u00e9 dans les d\u00e9lais, dans le respect du budget et avec le niveau de qualit\u00e9 attendu. Il est le point de contact principal entre l'\u00e9quipe technique et les parties prenantes.

    D\u00e9veloppeur

    Il a pour mission de concevoir, \u00e9crire, tester et documenter le code du logiciel. Les d\u00e9veloppeurs peuvent \u00eatre sp\u00e9cialis\u00e9s dans diff\u00e9rents domaines (front-end, back-end, full-stack, etc.), chacun apportant une expertise particuli\u00e8re au projet.

    Testeur

    Sp\u00e9cialis\u00e9 dans l'assurance qualit\u00e9, le testeur v\u00e9rifie que le logiciel fonctionne comme pr\u00e9vu, en identifiant les bugs et en s'assurant que toutes les fonctionnalit\u00e9s respectent les exigences d\u00e9finies. Le r\u00f4le du testeur est essentiel pour garantir un produit final fiable et performant.

    DevOps

    Le DevOps est charg\u00e9 de la gestion de l'infrastructure n\u00e9cessaire au d\u00e9ploiement du logiciel. Il travaille \u00e0 l'int\u00e9gration continue, \u00e0 la livraison continue, et \u00e0 l'automatisation des processus de d\u00e9ploiement, garantissant ainsi que le code peut \u00eatre d\u00e9ploy\u00e9 rapidement et en toute s\u00e9curit\u00e9.

    Designer UX/UI

    Bien que non mentionn\u00e9 initialement, il est important de noter que le designer UX/UI joue un r\u00f4le cl\u00e9 dans la conception de l'interface utilisateur et dans l'exp\u00e9rience utilisateur globale. Son travail est crucial pour s'assurer que le logiciel est non seulement fonctionnel mais aussi agr\u00e9able et intuitif \u00e0 utiliser.

    Lorsque vous \u00eates mendat\u00e9 pour \u00e9valuer l'effort de d\u00e9veloppement sur un projet, vous vous concentrez sur l'aspect technique du projet tout en restant tr\u00e8s optimiste sur les d\u00e9lais. Vous avez tendance \u00e0 sous-estimer le temps n\u00e9cessaire pour r\u00e9soudre les probl\u00e8mes techniques et \u00e0 surestimer votre capacit\u00e9 \u00e0 les r\u00e9soudre rapidement. Vous avez tendance \u00e0 ignorer les aspects non techniques du projet, comme la documentation, les tests, et la communication avec les parties prenantes. En r\u00e9alit\u00e9, chacun des r\u00f4les pr\u00e9c\u00e9demment mentionn\u00e9s \u00e0 une implication dans le projet. Il n'y a pas de r\u00e8gle g\u00e9n\u00e9rale mais une bonne approximation du temps entre les r\u00f4les pour le d\u00e9veloppement d'un logiciel est \u2153 pour le d\u00e9veloppement, \u2153 pour les tests, et \u2153 pour la documentation et la communication, d'o\u00f9 le facteur \\(\\pi\\) fr\u00e9quemment utilis\u00e9 dans le calcul de l'effort de d\u00e9veloppement.

    "}, {"location": "course-c/45-software-design/teamwork/#methodes-de-travail", "title": "M\u00e9thodes de travail", "text": "

    Le choix de la m\u00e9thode de travail est d\u00e9terminant pour la r\u00e9ussite d'un projet. Voici un aper\u00e7u des m\u00e9thodes les plus couramment utilis\u00e9es en d\u00e9veloppement logiciel\u2009:

    M\u00e9thode en cascade

    Traditionnellement, la m\u00e9thode en cascade segmente le projet en plusieurs phases lin\u00e9aires (analyse, conception, d\u00e9veloppement, test, d\u00e9ploiement). Chaque phase doit \u00eatre compl\u00e9t\u00e9e avant de passer \u00e0 la suivante. Cette approche convient bien aux projets o\u00f9 les exigences sont claires et peu susceptibles de changer.

    M\u00e9thode agile

    Les m\u00e9thodes agiles sont plus flexibles et permettent de s'adapter aux changements fr\u00e9quents des exigences. Le projet est divis\u00e9 en plusieurs cycles courts appel\u00e9s \u00ab\u2009sprints\u2009\u00bb (en Scrum) ou est g\u00e9r\u00e9 en flux continu (en Kanban). Les it\u00e9rations successives permettent de livrer rapidement des fonctionnalit\u00e9s et de recevoir des retours en continu, facilitant ainsi l'am\u00e9lioration progressive du produit.

    Scrum

    Un cadre agile populaire qui repose sur des it\u00e9rations r\u00e9guli\u00e8res (sprints) de 2 \u00e0 4 semaines, au cours desquelles une version du produit potentiellement livrable est d\u00e9velopp\u00e9e. Scrum met l'accent sur la collaboration, l'adaptation et l'am\u00e9lioration continue.

    Kanban

    Une autre m\u00e9thode agile qui se concentre sur la visualisation du flux de travail et la gestion continue des t\u00e2ches. Le Kanban est particuli\u00e8rement utile pour les \u00e9quipes cherchant \u00e0 am\u00e9liorer l'efficacit\u00e9 et \u00e0 r\u00e9duire les goulots d'\u00e9tranglement.

    "}, {"location": "course-c/45-software-design/teamwork/#outils-de-travail", "title": "Outils de travail", "text": "

    Pour une collaboration efficace en \u00e9quipe, l'utilisation d'outils adapt\u00e9s est indispensable. Voici quelques-uns des outils les plus couramment utilis\u00e9s\u2009:

    Gestion de version avec Git

    Git est un syst\u00e8me de gestion de versions distribu\u00e9, permettant \u00e0 plusieurs d\u00e9veloppeurs de travailler simultan\u00e9ment sur le m\u00eame projet sans risquer de perdre des modifications ou d'\u00e9craser le travail des autres. Il est essentiel pour la collaboration en \u00e9quipe, facilitant le suivi de l'\u00e9volution du code, la cr\u00e9ation de branches pour des fonctionnalit\u00e9s sp\u00e9cifiques, et la gestion des contributions gr\u00e2ce aux pull requests.

    Gestion de projet avec Trello

    Trello est un outil visuel de gestion de projet bas\u00e9 sur des tableaux, des listes et des cartes. Chaque membre de l'\u00e9quipe peut suivre l'avancement des t\u00e2ches, ajouter des commentaires, des pi\u00e8ces jointes, et organiser le travail de mani\u00e8re claire et intuitive. Trello est particuli\u00e8rement utile pour les \u00e9quipes qui utilisent une m\u00e9thode agile comme Kanban.

    Communication avec Slack/Teams/Discord

    Ces outils de communication sont essentiels pour maintenir une communication fluide au sein de l'\u00e9quipe, surtout dans un contexte de t\u00e9l\u00e9travail. Ils permettent non seulement de discuter en temps r\u00e9el, mais aussi de partager des fichiers, de coordonner des r\u00e9unions et de centraliser les \u00e9changes sur des canaux d\u00e9di\u00e9s \u00e0 diff\u00e9rents aspects du projet.

    Int\u00e9gration continue et d\u00e9ploiement continu (CI/CD) avec Jenkins/GitLab CI

    Pour automatiser le processus de construction, de test, et de d\u00e9ploiement du logiciel, des outils de CI/CD comme Jenkins ou GitLab CI sont souvent utilis\u00e9s. Ils permettent de r\u00e9duire les erreurs humaines, d'acc\u00e9l\u00e9rer le cycle de d\u00e9veloppement, et de s'assurer que le code reste toujours dans un \u00e9tat d\u00e9ployable.

    "}, {"location": "course-c/45-software-design/teamwork/#gestion-des-conflits", "title": "Gestion des conflits", "text": "

    Le d\u00e9veloppement logiciel, nous ne cessons de le r\u00e9p\u00e9ter, est un travail collaboratif, lequel peut parfois donner lieu \u00e0 des conflits. Ces conflits peuvent d\u00e9couler de diff\u00e9rences de vision, de priorit\u00e9s ou de m\u00e9thodes de travail entre les membres de l'\u00e9quipe. Ils apparaissent lorsque le niveau d'implication ou d'engagement transgresse la barri\u00e8re de l'\u00e9motionnel. Une grande implication dans un projet peut entra\u00eener des jalousies. La frustration d'avoir l'impression d'en faire davantage que les autres est une source courante de conflits car elle peut \u00eatre per\u00e7ue comme une injustice. Elle peut pousser un employ\u00e9 \u00e0 d'imisser dans les affaires des autres, \u00e0 critiquer leur travail, ou \u00e0 les d\u00e9nigrer. Apprendre \u00e0 identifier, g\u00e9rer et r\u00e9soudre ces conflits de mani\u00e8re efficace est crucial pour maintenir une dynamique de travail productive et harmonieuse.

    "}, {"location": "course-c/45-software-design/teamwork/#identifier-les-sources-de-conflits", "title": "Identifier les sources de conflits", "text": "

    Les sources de conflits peuvent varier selon le contexte de l'entreprise, mais certaines causes sont r\u00e9currentes dans le d\u00e9veloppement logiciel\u2009:

    Diff\u00e9rences de vision ou d'objectifs

    Dans les grandes entreprises, les diff\u00e9rents d\u00e9partements (d\u00e9veloppement, marketing, ventes) peuvent avoir des objectifs qui ne s'alignent pas toujours, cr\u00e9ant des tensions sur la direction \u00e0 prendre pour le projet. Dans une petite entreprise, ces conflits peuvent survenir entre les membres de l'\u00e9quipe qui ont des id\u00e9es divergentes sur les priorit\u00e9s du projet.

    Probl\u00e8mes de communication

    Le manque de communication est une source fr\u00e9quente de conflits. Dans une petite entreprise, la communication directe peut parfois manquer de formalisme, ce qui peut entra\u00eener des malentendus. Dans une grande entreprise, le cloisonnement entre les \u00e9quipes peut emp\u00eacher la circulation d'informations cruciales, exacerbant les tensions. C'est pourquoi il est essentiel de documenter les d\u00e9cisions prises et de les communiquer clairement \u00e0 l'ensemble de l'\u00e9quipe par des communiqu\u00e9s concis.

    Conflits de responsabilit\u00e9s

    Les zones grises concernant les responsabilit\u00e9s peuvent provoquer des conflits, surtout dans les grandes entreprises o\u00f9 les r\u00f4les sont tr\u00e8s sp\u00e9cialis\u00e9s. Dans les petites entreprises, o\u00f9 les membres de l'\u00e9quipe doivent souvent assumer plusieurs r\u00f4les, la r\u00e9partition floue des t\u00e2ches peut \u00e9galement \u00eatre source de d\u00e9saccords.

    Pression des d\u00e9lais

    La pression pour respecter les d\u00e9lais peut intensifier les conflits, particuli\u00e8rement lorsque les attentes sont mal g\u00e9r\u00e9es. Cela est particuli\u00e8rement vrai dans les grandes entreprises o\u00f9 les processus rigides peuvent ajouter une pression suppl\u00e9mentaire, mais c'est aussi un d\u00e9fi dans les petites entreprises o\u00f9 les ressources sont souvent limit\u00e9es.

    "}, {"location": "course-c/45-software-design/teamwork/#techniques-de-resolution-de-conflits", "title": "Techniques de r\u00e9solution de conflits", "text": "

    Une fois les sources de conflits identifi\u00e9es, il est essentiel d'avoir des techniques efficaces pour les r\u00e9soudre. Voici quelques approches courantes\u2009:

    Communication ouverte

    Encourager un dialogue ouvert et honn\u00eate est la premi\u00e8re \u00e9tape pour r\u00e9soudre un conflit. Dans une petite entreprise, cela peut signifier organiser des r\u00e9unions r\u00e9guli\u00e8res o\u00f9 chaque membre peut exprimer ses pr\u00e9occupations. Dans une grande entreprise, cela peut n\u00e9cessiter la mise en place de canaux de communication formels pour s'assurer que les pr\u00e9occupations sont entendues et trait\u00e9es.

    Mise en place de processus clairs

    \u00c9tablir des processus clairs pour la gestion des responsabilit\u00e9s et des t\u00e2ches peut pr\u00e9venir de nombreux conflits. Dans les grandes entreprises, cela implique souvent l'utilisation d'outils de gestion de projet et de documentation pr\u00e9cise. Dans les petites entreprises, il peut \u00eatre utile de formaliser certains aspects de la collaboration pour \u00e9viter les malentendus.

    Compromis et n\u00e9gociation

    Souvent, la r\u00e9solution des conflits n\u00e9cessite un compromis. Apprendre \u00e0 n\u00e9gocier des solutions qui satisfont les diff\u00e9rentes parties est une comp\u00e9tence cl\u00e9. Dans les grandes entreprises, cela peut impliquer des discussions entre les d\u00e9partements pour aligner les objectifs. Dans les petites entreprises, cela peut signifier que les membres de l'\u00e9quipe doivent \u00eatre pr\u00eats \u00e0 ajuster leurs attentes et priorit\u00e9s.

    Feedback constructif

    Donner et recevoir du feedback de mani\u00e8re constructive peut aider \u00e0 d\u00e9samorcer les conflits avant qu'ils ne s'intensifient. Dans un environnement de petite entreprise, o\u00f9 les relations sont souvent plus personnelles, le feedback doit \u00eatre donn\u00e9 avec tact pour maintenir une bonne ambiance de travail. Dans les grandes entreprises, des syst\u00e8mes formels de feedback peuvent \u00eatre mis en place pour encourager une communication r\u00e9guli\u00e8re.

    "}, {"location": "course-c/45-software-design/teamwork/#importance-de-lempathie", "title": "Importance de l'empathie", "text": "

    L'empathie joue un r\u00f4le crucial dans la gestion des conflits, surtout dans un cadre collaboratif comme le d\u00e9veloppement logiciel. \u00catre capable de se mettre \u00e0 la place de ses coll\u00e8gues et de comprendre leurs perspectives permet de mieux aborder les conflits de mani\u00e8re constructive.

    L'\u00e9coute active est une comp\u00e9tence essentielle pour manifester de l'empathie. Cela signifie \u00e9couter non seulement les mots de l'autre personne, mais aussi essayer de comprendre les \u00e9motions et les motivations sous-jacentes. Dans une petite entreprise, o\u00f9 les relations sont souvent plus proches, cela peut contribuer \u00e0 renforcer la coh\u00e9sion d'\u00e9quipe. Dans une grande entreprise, o\u00f9 les interactions peuvent \u00eatre plus formelles, l'\u00e9coute active peut aider \u00e0 d\u00e9passer les barri\u00e8res organisationnelles.

    La reconnaissance des besoins des autres est aussi fondamentale. Reconna\u00eetre que chaque membre de l'\u00e9quipe a des besoins et des contraintes diff\u00e9rentes est essentiel. Par exemple, un d\u00e9veloppeur peut \u00eatre sous pression pour livrer du code rapidement, tandis qu'un testeur peut avoir besoin de plus de temps pour assurer la qualit\u00e9. Dans une grande entreprise, la reconnaissance de ces diff\u00e9rences peut aider \u00e0 aligner les \u00e9quipes sur des objectifs communs. Dans une petite entreprise, cela peut \u00e9viter les frustrations li\u00e9es \u00e0 des attentes irr\u00e9alistes.

    "}, {"location": "course-c/45-software-design/teamwork/#gerer-un-projet-logiciel", "title": "G\u00e9rer un projet logiciel", "text": "

    Comme nous l'avons vu pr\u00e9c\u00e9demment, la gestion d'un projet logiciel implique de nombreuses \u00e9tapes qu'il est important de ne pas courtcircuiter. Cela passe souvent par une analyse fonctionnelle et l'\u00e9laboration d'un cahier des charges fonctionnel.

    Le processus est it\u00e9ratif et r\u00e9cursif. Bien qu'il y ait un ordre d'\u00e9tapes \u00e0 suivre, il est souvent n\u00e9cessaire de revenir en arri\u00e8re pour ajuster les sp\u00e9cifications en fonction des retours utilisateurs ou des contraintes techniques.

    "}, {"location": "course-c/45-software-design/teamwork/#cahier-des-charges-fonctionnel", "title": "Cahier des charges fonctionnel", "text": ""}, {"location": "course-c/45-software-design/teamwork/#analyse-des-cas-dutilisation", "title": "Analyse des cas d'utilisation", "text": "

    La premi\u00e8re \u00e9tape est l'analyse des cas d'utilisation, qui consiste d'une part \u00e0 identifier les utilisateurs du syst\u00e8me et d'autre part les interactions qu'ils auront avec le syst\u00e8me. Cette analyse permet in fine de d\u00e9marrer l'analyse des besoins.

    On peut repr\u00e9senter les cas d'utilisation sous forme de diagrammes de cas d'utilisation qui fait partie du standard UML (Unified Modeling Language). Un diagramme de cas d'utilisation est un diagramme qui montre les acteurs, les cas d'utilisation et les interactions entre eux.

    Imagions que nous devons d\u00e9velopper un syst\u00e8me qui prendra la forme d'une machine \u00e0 caf\u00e9 connect\u00e9e. Identifier les acteurs doit \u00eatre la premi\u00e8re \u00e9tape.

    Il ne faut pas h\u00e9siter faire preuve d'imagination et d'\u00eatre exausitif. Dans notre exemple, les acteurs pourraient \u00eatre\u2009:

    • l'utilisateur qui souhaite boire un caf\u00e9\u2009;
    • le technicien qui doit entretenir la machine\u2009;
    • le service client qui doit r\u00e9pondre aux questions des utilisateurs\u2009;
    • l'entreprise qui veut encourager la productivit\u00e9 au travail\u2009;
    • l'op\u00e9rateur de service.

    Le diagramme suivant montre la repr\u00e9sentation de ces cas d'utilisation.

    Diagramme de cas d'utilisation

    \u00c0 partir de ce diagramme, on peut d\u00e9duire les besoins fonctionnels du syst\u00e8me. Par exemple, le cas d'utilisation \u00ab\u2009R\u00e8glage de la mouture\u2009\u00bb implique que le syst\u00e8me doit \u00eatre capable d'offir une interface utilisateur permettant de r\u00e9gler la mouture du caf\u00e9. Le cas d'utilisation \u00ab\u2009Maintenance\u2009\u00bb implique que le syst\u00e8me doit \u00eatre capable de d\u00e9tecter les pannes et de les signaler \u00e0 l'utilisateur.

    Il se peut que des utilisateurs ou des actions inutiles ait \u00e9t\u00e9 ajout\u00e9es ou qu'il en manque. Ce n'est pas un probl\u00e8me \u00e0 ce stade de l'exercice car le processus de r\u00e9flexion est it\u00e9ratifs. On peut revenir plus tard sur le diagramme pour l'ajuster et consolider les hypoth\u00e8ses initiales.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-du-besoin", "title": "Analyse du besoin", "text": "

    L'analyse du besoin est la seconde \u00e9tape dans la gestion d'un projet d'ing\u00e9nierie ou de g\u00e9nie logiciel. Elle consiste \u00e0 Elle consiste \u00e0 identifier les besoins des utilisateurs finaux qui ont \u00e9t\u00e9 identifi\u00e9s plus haut. Ceci permettant par la suite d'identifier les fonctionnalit\u00e9s du logiciel en cons\u00e9quence. Cette \u00e9tape permet de s'assurer que le logiciel r\u00e9pondra aux attentes des utilisateurs et qu'il sera adapt\u00e9 \u00e0 leur contexte d'utilisation.

    L'analyse du besoin utilise deux armes redoutables, la question \u00ab\u2009Comment\u2009\u00bb et la question \u00ab\u2009Pourquoi\u2009\u00bb. Hi\u00e9rarchiquement un \u00ab\u2009Comment\u2009\u00bb permet de rentrer dans le d\u00e9tail, on descend dans le niveau de granularit\u00e9. Un \u00ab\u2009Pourquoi\u2009\u00bb permet de remonter dans la hi\u00e9rarchie, de comprendre les motivations et les objectifs des utilisateurs. L'exercice doit pouvoir se r\u00e9p\u00e9ter jusqu'\u00e0 ce que la r\u00e9ponse sorte du cadre du projet. Cela permet de fixer le cadre g\u00e9n\u00e9ral du projet.

    1. Pourquoi l'utilisateur veut-il boire un caf\u00e9\u2009? Parce qu'il aime le caf\u00e9\u2009!
    2. Pourquoi il aime le caf\u00e9\u2009? Parce qu'il a besoin de se r\u00e9veiller le matin\u2009!
    3. Pourquoi il a besoin de se r\u00e9veiller le matin\u2009? Parce qu'il doit aller travailler\u2009!
    4. Pourquoi il doit aller travailler\u2009? Parce qu'il a besoin d'argent\u2009!
    5. Pourquoi il a besoin d'argent\u2009? Parce qu'il a besoin de payer son loyer\u2009!
    6. Pourquoi il a besoin de payer son loyer\u2009? Parce qu'il a besoin de survivre\u2009!

    Dans le cadre du syst\u00e8me auquel on s'int\u00e9resse, il n'est pas essentiel de r\u00e9pondre \u00e0 toutes les questions. Certaines sortent du cadre, mais cela permet de comprendre les motivations des utilisateurs et de s'assurer que le syst\u00e8me r\u00e9pondra \u00e0 leurs besoins. Cela permet de faire des parall\u00e8les avec d'autres acteurs du syst\u00e8me. Car un utilisateur r\u00e9veill\u00e9 est plus productif, et une entreprise qui a des employ\u00e9s productifs est plus rentable. L'exercice peut se r\u00e9p\u00e9ter pour chaque acteur du syst\u00e8me.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-fast", "title": "Analyse FAST", "text": "

    En pratique une bonne analyse est l'analyse FAST pour Function Analysis System Technique. Elle permet de d\u00e9composer les fonctions du syst\u00e8me en sous-fonctions, en sous-sous-fonctions, etc. jusqu'\u00e0 obtenir des fonctions \u00e9l\u00e9mentaires. Cela permet de d\u00e9finir les besoins fonctionnels du syst\u00e8me de mani\u00e8re exhaustive.

    Un diagramme FAST est bidimensionnel. Horizontalement sont repr\u00e9sent\u00e9s des fonctions du syst\u00e8mes. En se dirigeant \u00e0 droite, on r\u00e9pond \u00e0 la question \u00ab\u2009Comment\u2009\u00bb, en se dirigeant \u00e0 gauche on r\u00e9pond \u00e0 la question \u00ab\u2009Pourquoi\u2009\u00bb. Verticalement on peut repr\u00e9senter le \u00ab\u2009Quand\u2009\u00bb ou l'ordre de priorit\u00e9 des fonctions.

    Diagramme FAST

    Une fonction s'\u00e9crit sous la forme d'un verbe \u00e0 l'infinitif suivi d'un compl\u00e9ment d'objet. Par exemple \u00ab\u2009Pr\u00e9parer du caf\u00e9\u2009\u00bb ou \u00ab\u2009D\u00e9tecter une panne\u2009\u00bb.

    Cette analyse se r\u00e9alise en \u00e9quipe. La discussion doit \u00eatre ouverte et constructive sans contraintes hi\u00e9rarchiques. Chaque membre de l'\u00e9quipe doit pouvoir s'exprimer librement et proposer des id\u00e9es. L'objectif est d'\u00eatre exaustif et de ne rien laisser de c\u00f4t\u00e9.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-du-besoin_1", "title": "Analyse du besoin", "text": "

    Une fois l'analyse FAST r\u00e9alis\u00e9e, le besoin de chaque utilisateur peut \u00eatre identifi\u00e9. Un besoin d'exprime sous la forme \"J'ai besoin de fonction\". Les besoins des utilisateurs. Par soucis de redondance, le libell\u00e9 du besoin sera simplement la fonction.

    On exprime les besoins sous forme d'une table, o\u00f9 chaque ligne est num\u00e9rot\u00e9e. Le num\u00e9ro peut \u00eatre hi\u00e9rarchique mais il est pr\u00e9fix\u00e9 par un identifiant unique. Par exemple le besoin \u00ab\u2009N3.2\u2009\u00bb, avec N pour Need en anglais.

    Besoin de l'utilisateur ID Besoin N1.1 \u00catre r\u00e9veill\u00e9 au travail N1.2 \u00catre productif au travail N1.3 Partager un moment avec ses coll\u00e8gues N1.4 Appr\u00e9cier l'exp\u00e9rience

    \u00c0 chaque instant, chaque besoin peut \u00eatre questionn\u00e9. On doit pouvoir r\u00e9pondre \u00e0 aux deux questions \u00ab\u2009Comment\u2009\u00bb et \u00ab\u2009Pourquoi\u2009\u00bb. Si un besoin ne peut pas \u00eatre justifi\u00e9, il n'a pas sa place dans la liste.

    L'op\u00e9ration est r\u00e9p\u00e9t\u00e9e pour chaque acteur du syst\u00e8me en veillant \u00e0 ce que les besoins soient coh\u00e9rents entre eux.

    Il n'est pas toujours \u00e9vident de positionner le besoin. Par exemple la question \u00ab\u2009Pourquoi dois-je \u00eatre productif au travail\u2009\u00bb peut mener \u00e0 des questions philosophiques ou existentielle sur le sens de la vie, la place de la soci\u00e9t\u00e9 capitaliste, etc. Bien que ces questions soient pertinentes dans un cadre plus large, il est primordial d'identifier le cadre du projet et de ne pas s'en \u00e9carter.

    Un aspect important est \u00e9galement les contraintes ext\u00e9rieures. Souvent un client contacte une entreprise avec une id\u00e9e de projet en t\u00eate. Le client pense savoir ce qu'il veut, il a d\u00e9j\u00e0 r\u00e9alis\u00e9 des \u00e9tudes de march\u00e9, des \u00e9tude de design ou d'ergonomie. Malheuresement l'exp\u00e9rience montre que le client n'a pas toujours raison. Il n'est pas rare qu'il ait courtcircuit\u00e9 l'analyse fonctionnelle de son produit et mal identifi\u00e9 ses besoins. M\u00eame si ce n'est pas toujours possible, ni diplomatiquement \u00e9vident, il est essentiel de remettre en question les besoins du client pour les consolider.

    Une fois les besoins des utilisateurs identifi\u00e9s, l'\u00e9tape suivante est d'identifier les besoins du syst\u00e8me \u00e0 concevoir. Chaque besoin doit pouvoir \u00eatre reli\u00e9 \u00e0 un besoin utilisateur. Si on identifie \u00e0 posteriori un besoin du syst\u00e8me qui n'est pas reli\u00e9 \u00e0 un besoin utilisateur, il est probable que ce besoin du syst\u00e8me n'a pas sa place dans le projet ou alors que l'on ait oubli\u00e9 un besoin utilisateur ce qui peut conduire \u00e0 une nouvelle it\u00e9ration du processus d'analyse.

    Les besoins du syst\u00e8me pourraient \u00eatre\u2009:

    Besoin du syst\u00e8me ID Besoin Reli\u00e9 \u00e0 N6.1 Pr\u00e9parer du caf\u00e9 N1.1, N3.4 N6.2 Diagnostique automatique de panne N1.1, N2.2 N6.3 Offir une boisson de qualit\u00e9 N1.4 N6.4 Proposer des boissons vari\u00e9es N1.4 N6.5 Disposer d'une interface utilisateur intuitive N1.4 N6.6 Produire un caf\u00e9 rapidement N1.3, N5.4 N6.7 Produire du caf\u00e9 \u00e0 la cha\u00eene ...

    Il est important de noter qu'un syst\u00e8me m\u00eame complexe peut \u00eatre r\u00e9duit \u00e0 une liste de besoins \u00e9l\u00e9mentaires qui tiennent sur les doigts d'une ou de deux mains. \u00c0 ce stade de l'analyse il n'y a pas de place pour des d\u00e9tails techniques ou les grandeurs physiques.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-fonctionnelle", "title": "Analyse fonctionnelle", "text": "

    L'analyse fonctionnelle est la troisi\u00e8me \u00e9tape. Elle consiste \u00e0 d\u00e9finir les fonctions du syst\u00e8me \u00e0 concevoir pour r\u00e9pondre aux besoins du syst\u00e8me et donc des utilisateurs. Cette \u00e9tape permet de d\u00e9terminer les diff\u00e9rentes t\u00e2ches que le syst\u00e8me devra r\u00e9aliser pour satisfaire les besoins identifi\u00e9s. Une fonction est exprim\u00e9e sous forme d'une exigence fonctionnelle. Elle doit \u00eatre claire, pr\u00e9cise et non ambigu\u00eb. Elle comportera une dizaine de mots maximum et sera r\u00e9dig\u00e9e sous la forme d'un verbe \u00e0 l'infinitif suivi d'un compl\u00e9ment d'objet. Les verbes tels que d\u00e9finis par les Directives ISO/IEC Partie 2\u2009: Principes er r\u00e8gles de structure et de r\u00e9daction des documents ISO et IEC (ISO/IEC DIR 2) est une excellente base, \u00e0 la fois pour la r\u00e9daction du cahier des charges et pour la r\u00e9daction des exigences fonctionnelles. Le chapitre 7.2 d\u00e9fini comment une exigence doit \u00eatre r\u00e9dig\u00e9e. On notera\u2009:

    Exigences fonctionnelles Forme verbale Description Doit Obligation de disposer de la fonctionnalit\u00e9 Peut, Devrait Exigence optionnelle, facultative Ne doit pas Interdiction de disposer de la fonctionnalit\u00e9 Ne devrait pas Recommendation de ne pas disposer de la fonctionnalit\u00e9

    Les exigences fonctionnelles sont \u00e9galement num\u00e9rot\u00e9e par exemple avec le pr\u00e9fixe F pour Function. Une exigence fonctionnelle doit pouvoir \u00eatre reli\u00e9e par la question \u00ab\u2009Pourquoi\u2009\u00bb \u00e0 un besoin du syst\u00e8me, lequel par la m\u00eame question \u00eatre amen\u00e9 aux besoins des utilisateurs. Cela permet de s'assurer que chaque exigence fonctionnelle est justifi\u00e9e par un besoin utilisateur.

    Dans le cadre de notre exemple, les exigences fonctionnelles pourraient \u00eatre\u2009:

    Exigence fonctionnelle du syst\u00e8me ID Exigence fonctionnelle Reli\u00e9 \u00e0 F1.1 Doit pouvoir moudre des grains de caf\u00e9 N6.3 F1.2 Doit disposer d'une r\u00e9serve suffisante de grains N6.6 F1.3 Doit pouvoir \u00eatre reli\u00e9 \u00e0 une source d'eau N6.7

    Dans une analyse fonctionnelle on d\u00e9coupe g\u00e9n\u00e9ralement le syst\u00e8me en diff\u00e9rentes cat\u00e9gories\u2009:

    Fonctions de service

    Les fonctions de service sont les fonctions qui permettent au syst\u00e8me de fonctionner correctement. Elles sont souvent invisibles pour l'utilisateur final mais essentielles pour le bon fonctionnement du syst\u00e8me. Par exemple, la fonction \u00ab\u2009F1.3\u2009\u00bb qui permet au syst\u00e8me d'\u00eatre reli\u00e9 \u00e0 une source d'eau.

    Fonctions de contrainte

    Les fonctions de contrainte sont les fonctions qui imposent des contraintes sur le syst\u00e8me. Elles peuvent \u00eatre li\u00e9es \u00e0 des normes, des r\u00e9glementations ou des contraintes techniques, comme l'assurance qualit\u00e9 ou la s\u00e9curit\u00e9, ou de l'hygi\u00e8ne.

    Fonctions de support

    Les fonctions de support sont les fonctions qui permettent au syst\u00e8me de s'adapter \u00e0 son environnement. Elles peuvent \u00eatre li\u00e9es \u00e0 l'ergonomie, \u00e0 l'interface utilisateur, ou \u00e0 la maintenance du syst\u00e8me.

    Selon le projet et la complexit\u00e9 du syst\u00e8me, il peut \u00eatre n\u00e9cessaire de d\u00e9finir d'autres cat\u00e9gories de fonctions.

    "}, {"location": "course-c/45-software-design/teamwork/#analyse-organique", "title": "Analyse organique", "text": "

    L'analyse organique est la quatri\u00e8me \u00e9tape. Elle consiste \u00e0 d\u00e9finir les organes du syst\u00e8me \u00e0 concevoir pour r\u00e9aliser les fonctions identifi\u00e9es. Cette \u00e9tape permet de d\u00e9terminer les diff\u00e9rentes parties du syst\u00e8me qui devront \u00eatre con\u00e7ues pour r\u00e9aliser les fonctions identifi\u00e9es. Un organe est une partie du syst\u00e8me qui r\u00e9alise une ou plusieurs fonctions. Il peut \u00eatre un composant mat\u00e9riel, un logiciel, un sous-syst\u00e8me, ou une combinaison de ces \u00e9l\u00e9ments. Chaque organe doit \u00eatre clairement identifi\u00e9 et d\u00e9crit dans le cahier des charges.

    Chaque organe identifi\u00e9 peut lui-m\u00eame disposer de ses propres besoins, ses propres fonctions et ses propres exigences. Il est important de s'assurer que chaque organe est coh\u00e9rent avec les fonctions qu'il doit r\u00e9aliser et qu'il est en mesure de satisfaire les exigences qui lui sont associ\u00e9es. De surcro\u00eet les questions \u00ab\u2009Comment\u2009\u00bb et \u00ab\u2009Pourquoi\u2009\u00bb doivent permettre de suivre chaque organe dans sa hi\u00e9rarchie.

    "}, {"location": "course-c/45-software-design/teamwork/#specification-technique", "title": "Sp\u00e9cification technique", "text": "

    La cinqui\u00e8me \u00e9tape est l'identification des sp\u00e9cifications techniques attendues pour le syst\u00e8me, et pour chaque organe du syst\u00e8me. Cette \u00e9tape permet de d\u00e9terminer les caract\u00e9ristiques techniques du syst\u00e8me, les contraintes techniques \u00e0 respecter, et les normes \u00e0 suivre.

    Une sp\u00e9cification doit \u00eatre v\u00e9rifiable, c'est \u00e0 dire qu'\u00e0 la fin du projet, on doit pouvoir v\u00e9rifier que chaque \u00e9l\u00e9ment de la sp\u00e9cification a \u00e9t\u00e9 respect\u00e9 et est dans les normes acceptables.

    Table\u2009: Sp\u00e9cifications techniques

    ID Sp\u00e9cification technique Min Nom Max Unit\u00e9 Reli\u00e9 \u00e0 S1.1 Dur\u00e9e de mouture pour un caf\u00e9 20 30 s F... S1.2 Capacit\u00e9 de la r\u00e9serve 1 2 3 kg F... S1.3 Pression de l'eau 1 2 5 bar F...

    On aura g\u00e9n\u00e9ralement diff\u00e9rentes cat\u00e9gories de sp\u00e9cifications techniques\u2009:

    • Sp\u00e9cifications fonctionnelles
    • Sp\u00e9cifications de performance
    • Sp\u00e9cification \u00e9lectriques
    • Sp\u00e9cifications m\u00e9caniques
    • Sp\u00e9cifications logicielles

    \u00c0 partir de cette sp\u00e9cification pr\u00e9liminaire il est possible, enfin, de rentrer dans la technique et de d\u00e9marrer l'\u00e9tude de d\u00e9veloppement en proposant une solution technique. Cela peut \u00eatre un prototype, un plan, un sch\u00e9ma, un diagramme, etc. qui permettra de valider la faisabilit\u00e9 du projet.

    "}, {"location": "course-c/45-software-design/teamwork/#etude-des-solutions", "title": "\u00c9tude des solutions", "text": "

    Une fois le cahier des charges fonctionnel r\u00e9alis\u00e9, on conna\u00eet les utilisateurs, les besoins, les fonctions du syst\u00e8me, ses organes internes et les sp\u00e9cifications techniques attendues. Il est alors possible de proposer une solution technique pour le syst\u00e8me. Cette solution technique doit permettre de r\u00e9aliser les fonctions identifi\u00e9es, de satisfaire les besoins des utilisateurs, et de respecter les sp\u00e9cifications techniques d\u00e9finies.

    En termes logiciels cela peut \u00eatre la proposition d'une architecture logicielle, d'une base de donn\u00e9es, d'une interface utilisateur, des choix technologiques (langage de programmation, framework, etc.), des outils de d\u00e9veloppement, etc.

    En termes mat\u00e9riels cela peut \u00eatre la proposition d'un sch\u00e9ma \u00e9lectrique, d'un plan m\u00e9canique, d'un choix de composants, d'une architecture mat\u00e9rielle, etc.

    "}, {"location": "course-c/45-software-design/teamwork/#developpement", "title": "D\u00e9veloppement", "text": "

    Une fois la solution technique valid\u00e9e, il est possible de passer \u00e0 l'\u00e9tape de d\u00e9veloppement du syst\u00e8me. Cette \u00e9tape consiste \u00e0 r\u00e9aliser les organes du syst\u00e8me, \u00e0 les assembler, \u00e0 les tester, et \u00e0 les valider. C'est \u00e0 ce moment que l'on passe de la th\u00e9orie \u00e0 la pratique, de la sp\u00e9cification \u00e0 la r\u00e9alisation.

    "}, {"location": "course-c/45-software-design/teamwork/#modeles-de-developpement", "title": "Mod\u00e8les de d\u00e9veloppement", "text": "

    Il existe de nombreux mod\u00e8les de d\u00e9veloppement logiciel, chacun ayant ses avantages et ses inconv\u00e9nients. Voici les deux mod\u00e8les les plus couramment utilis\u00e9s\u2009:

    "}, {"location": "course-c/45-software-design/teamwork/#modele-en-cascade", "title": "Mod\u00e8le en cascade", "text": "

    Le mod\u00e8le en cascade est un mod\u00e8le lin\u00e9aire qui divise le projet en plusieurs phases distinctes (analyse, conception, d\u00e9veloppement, test, d\u00e9ploiement). Chaque phase doit \u00eatre compl\u00e9t\u00e9e avant de passer \u00e0 la suivante. Ce mod\u00e8le convient bien aux projets o\u00f9 les exigences sont claires et peu susceptibles de changer.

    Le mod\u00e8le en cascade suivant r\u00e9sume le cycle de d\u00e9veloppement d'un programme. Il s'agit d'un mod\u00e8le simple, mais qu'il faut garder \u00e0 l'esprit que ce soit pour le d\u00e9veloppement d'un produit logiciel que durant les travaux pratiques li\u00e9s \u00e0 ce cours.

    Mod\u00e8le en cascade

    "}, {"location": "course-c/45-software-design/teamwork/#modele-en-v", "title": "Mod\u00e8le en V", "text": "

    Le mod\u00e8le en V est une extension du mod\u00e8le en cascade qui met l'accent sur la validation et la v\u00e9rification \u00e0 chaque \u00e9tape du processus. Chaque phase de d\u00e9veloppement est associ\u00e9e \u00e0 une phase de test correspondante

    "}, {"location": "course-c/45-software-design/testing/", "title": "Qualit\u00e9 et Testabilit\u00e9", "text": "

    Bogue de l'an 2000

    Surveiller et assurer la qualit\u00e9 d'un code est primordial dans toute institution et quelques soit le produit. Dans l'industrie automobile par exemple, un bogue qui serait d\u00e9couvert plusieurs ann\u00e9es apr\u00e8s la commercialisation d'un mod\u00e8le d'automobile aurait des cons\u00e9quences catastrophiques.

    Voici quelques exemples c\u00e9l\u00e8bres de rat\u00e9s logiciels\u2009:

    La sonde martienne Mariner

    En 1962, un bogue logiciel a caus\u00e9 l'\u00e9chec de la mission avec la destruction de la fus\u00e9e apr\u00e8s qu'elle ait diverg\u00e9 de sa trajectoire. Une formule a mal \u00e9t\u00e9 retranscrite depuis le papier en code ex\u00e9cutable. Des tests suffisants auraient \u00e9vit\u00e9 cet \u00e9chec.

    Un pipeline sovi\u00e9tique de gaz

    En 1982, un bogue a \u00e9t\u00e9 introduit dans un ordinateur canadien achet\u00e9 pour le contr\u00f4le d'un pipeline de gaz transsib\u00e9rien. L'erreur est report\u00e9e comme la plus large explosion jamais enregistr\u00e9e d'origine non nucl\u00e9aire.

    Le g\u00e9n\u00e9rateur de nombre pseudo-al\u00e9atoire Kerberos

    Kerberos est un syst\u00e8me de s\u00e9curit\u00e9 utilis\u00e9 par Microsoft pour chiffrer les mots de passe des comptes Windows. Une erreur de code lors de la g\u00e9n\u00e9ration d'une graine al\u00e9atoire a permis de fa\u00e7on triviale pendant 8 ans de p\u00e9n\u00e9trer n'importe quel ordinateur utilisant une authentification Kerberos.

    La division enti\u00e8re sur Pentium

    En 1993, une erreur sur le silicium des processeurs Pentium, fleuron technologique de l'\u00e9poque, menait \u00e0 des erreurs de calcul en virgule flottante. Par exemple la division \\(4195835.0/3145727.0\\) menait \u00e0 \\(1.33374\\) au lieu de \\(1.33382\\)

    "}, {"location": "course-c/45-software-design/testing/#square", "title": "SQuaRE", "text": "

    La norme ISO/IEC 25010 (qui remplace ISO/IEC 9126-1) d\u00e9crit les caract\u00e9ristiques d\u00e9finissant la qualit\u00e9 d'un logiciel. L'acronyme SQuaRE (Software product Quality Requirements and Evaluation) d\u00e9finit le standard international. Voici quelques crit\u00e8res d'un code de qualit\u00e9\u2009:

    • Maintenabili\u00e9
    • Modifiabilit\u00e9
    • Testabilit\u00e9
    • Analisabilit\u00e9
    • Stabilit\u00e9
    • Changeabilit\u00e9
    • R\u00e9utilisabilit\u00e9
    • Compr\u00e9hensibilit\u00e9
    "}, {"location": "course-c/45-software-design/testing/#hacking", "title": "Hacking", "text": ""}, {"location": "course-c/45-software-design/testing/#buffer-overflow", "title": "Buffer overflow", "text": "

    L'attaque par buffer overflow est un type d'attaque typique permettant de modifier le comportement d'un programme en exploitant \u00ab\u2009le jardinage m\u00e9moire\u2009\u00bb. Lorsqu'un programme a mal \u00e9t\u00e9 con\u00e7u et que les tests de d\u00e9passement n'ont pas \u00e9t\u00e9 correctement impl\u00e9ment\u00e9s, il est souvent possible d'acc\u00e9der \u00e0 des comportements de programmes impr\u00e9vus.

    Consid\u00e9rons le programme suivant\u2009:

    #include <stdio.h>\n#include <string.h>\n\nint check_password(char *str) {\n    if(strcmp(str, \"starwars\"))\n    {\n        printf (\"Wrong Password \\n\");\n        return 0;\n    }\n\n    printf (\"Correct Password \\n\");\n    return 1;\n}\n\nint main(void)\n{\n    char buffer[15];\n    int is_authorized = 0;\n\n    printf(\"Password: \");\n    gets(buffer);\n    is_authorized = check_password(buffer);\n\n    if(is_authorized)\n    {\n        printf (\"Now, you have the root access! \\n\");\n    }\n}\n

    \u00c0 priori, c'est un programme tout \u00e0 fait correct. Si l'utilisateur entre le bon mot de passe, il se voit octroyer des privil\u00e8ges administrateurs. Testons ce programme\u2009:

    $ gcc u.c -fno-stack-protector\n$ ./a.out\nPassword: starwars\nCorrect Password\nNow, you have the root access!\n

    Tr\u00e8s bien, maintenant testons avec un mauvais mot de passe\u2009:

    $ ./a.out\nPassword: startrek\nWrong Password\n

    Et maintenant, essayons avec un mot de passe magique...

    "}, {"location": "course-c/45-software-design/testing/#tests-unitaires", "title": "Tests unitaires", "text": "

    Un test unitaire est une proc\u00e9dure permettant de v\u00e9rifier le bon fonctionnement d'une unit\u00e9 de code. Une unit\u00e9 de code est la plus petite partie d'un programme qui peut \u00eatre test\u00e9e de mani\u00e8re isol\u00e9e. En C, une unit\u00e9 de code est souvent une fonction.

    Lorsque l'on travaille selon la philosophie du TDD (Test Driven Development), on commence par \u00e9crire les tests unitaires avant d'\u00e9crire le code. Voici par exemple un test unitaire pour la r\u00e9solution d'une \u00e9quation du second degr\u00e9\u2009:

    #include <stdio.h>\n\nbool quadratic_solver(double a, double b, double c, double *x1, double *x2)\n{\n    double delta = b * b - 4 * a * c;\n\n    if (delta < 0)\n        return false;\n\n    *x1 = (-b + sqrt(delta)) / (2 * a);\n    *x2 = (-b - sqrt(delta)) / (2 * a);\n\n    return true;\n}\n\nvoid test_quadratic_solver(void)\n{\n    double x1, x2;\n\n    // Cas 1 : Deux racines r\u00e9elles et distinctes\n    assert(quadratic_solver(1, -3, 2, &x1, &x2) == true);\n    assert(fabs(x1 - 2.0) < 1e-6);\n    assert(fabs(x2 - 1.0) < 1e-6);\n\n    // Cas 2 : Deux racines r\u00e9elles et \u00e9gales\n    assert(quadratic_solver(1, -2, 1, &x1, &x2) == true);\n    assert(fabs(x1 - 1.0) < 1e-6);\n    assert(fabs(x2 - 1.0) < 1e-6);\n\n    // Cas 3 : Racines complexes (pas de solution r\u00e9elle)\n    assert(quadratic_solver(1, 0, 1, &x1, &x2) == false);\n\n    // Cas 4 : a = 0 (ce n'est pas une \u00e9quation quadratique)\n    assert(quadratic_solver(0, 2, 1, &x1, &x2) == false);\n\n    // Cas 5 : Equation triviale (b = 0 et c = 0)\n    assert(quadratic_solver(1, 0, 0, &x1, &x2) == true);\n    assert(fabs(x1 - 0.0) < 1e-6);\n    assert(fabs(x2 - 0.0) < 1e-6);\n}\n

    En pratique on aimerait lancer les tests unitaires automatiquement \u00e0 chaque modification du code source. Pour cela, on utilisera des outils d'int\u00e9gration continue comme Travis CI ou GitHub Actions.

    Souvent on utilise des frameworks de tests unitaires pour automatiser les tests. En C, on peut citer Unity.

    Reprenons l'exemple pr\u00e9c\u00e9dent avec Unity\u2009:

    #include \"unity.h\"\n#include \"quadratic_solver.h\"\n\n#include <stdio.h>\n\nvoid setUp(void) {}\n\nvoid tearDown(void) {}\n\nvoid test_quadratic_solver(void)\n{\n    double x1, x2;\n\n    // Cas 1 : Deux racines r\u00e9elles et distinctes\n    TEST_ASSERT_TRUE(quadratic_solver(1, -3, 2, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 2.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 1.0);\n\n    // Cas 2 : Deux racines r\u00e9elles et \u00e9gales\n    TEST_ASSERT_TRUE(quadratic_solver(1, -2, 1, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 1.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 1.0);\n\n    // Cas 3 : Racines complexes (pas de solution r\u00e9elle)\n    TEST_ASSERT_FALSE(quadratic_solver(1, 0, 1, &x1, &x2));\n\n    // Cas 4 : a = 0 (ce n'est pas une \u00e9quation quadratique)\n    TEST_ASSERT_FALSE(quadratic_solver(0, 2, 1, &x1, &x2));\n\n    // Cas 5 : Equation triviale (b = 0 et c = 0)\n    TEST_ASSERT_TRUE(quadratic_solver(1, 0, 0, &x1, &x2));\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x1, 0.0);\n    TEST_ASSERT_FLOAT_WITHIN(1e-6, x2, 0.0);\n}\n
    "}, {"location": "course-c/45-software-design/testing/#tests-fonctionnels", "title": "Tests fonctionnels", "text": "

    Les tests fonctionnels permettent de v\u00e9rifier le bon fonctionnement d'une application dans son ensemble. Ils sont souvent utilis\u00e9s pour tester des applications web ou des applications mobiles. Les tests fonctionnels sont souvent automatis\u00e9s et peuvent \u00eatre lanc\u00e9s \u00e0 chaque modification du code source.

    Dans le cadre de ce cours on utilise le framework Baygon pour r\u00e9aliser des tests fonctionnels.

    Pour l'utiliser il suffit de cr\u00e9er un fichier tests.yml \u00e0 la racine de votre projet\u2009:

    version: 1\ntests:\n  - name: Arguments check\n    tests:\n      - name: No errors if two arguments\n        args: [1, 2]\n        exit: 0\n      - name: Error if less than two arguments\n        args: [1]\n        exit: 1\n  - name: Stdout is the sum of arguments\n    args: [1, 2]\n    stdout: []\n  - name: Version on stderr\n    args: ['--version']\n    stderr:\n      - regex: '\\b\\d\\.\\d\\.\\d\\b'\n      - contains: 'Version'\n

    Il faut ensuite installer Baygon\u2009:

    $ pip install baygon\n

    Et lancer les tests\u2009:

    $ baygon program\n
    ", "tags": ["tests.yml"]}, {"location": "course-c/47-gui/", "title": "Interfaces graphiques", "text": "

    Sans doute la premi\u00e8re frustration des \u00e9tudiants est de ne pas r\u00e9aliser d'interface graphique d\u00e8s les premiers cours d'informatique. C'est pourtant un domaine indispensable mais ce n'est pas le sujet le plus simple \u00e0 aborder. La programmation d'interfaces graphiques est un domaine complexe qui n\u00e9cessite avant tout une bonne connaissance de la programmation, des patrons de conceptions, des biblioth\u00e8ques graphiques et des syst\u00e8mes d'exploitation.

    Les interfaces graphiques sont le plus souvent r\u00e9alis\u00e9es avec des biblioth\u00e8ques tierces qui permettent de cr\u00e9er des fen\u00eatres, des boutons, des champs de texte, etc. Ces biblioth\u00e8ques sont g\u00e9n\u00e9ralement sp\u00e9cifiques \u00e0 un syst\u00e8me d'exploitation ou \u00e0 un langage de programmation et ne sont pas toujours portables. C'est \u00e0 dire qu'un programme \u00e9crit avec une biblioth\u00e8que graphique sp\u00e9cifique \u00e0 Windows ne pourra pas \u00eatre ex\u00e9cut\u00e9 sur un syst\u00e8me d'exploitation Linux ou MacOS sans modification.

    Les d\u00e9veloppeurs d'interfaces graphiques pr\u00e9f\u00e8reront souvent utiliser des langages plus adapt\u00e9s \u00e0 la cr\u00e9ation d'interfaces graphiques, comme Java, C#, ou m\u00eame de la programmation web. Cependant, il est tout \u00e0 fait possible de cr\u00e9er des interfaces graphiques en C, mais cela n\u00e9cessite un peu plus de travail.

    Maintenant que nous avons vu comment g\u00e9rer un projet complexe ainsi que ses d\u00e9pendances, nous allons aborder la programmation d'interfaces graphiques en C.

    "}, {"location": "course-c/47-gui/gtk/", "title": "GTK", "text": "

    GTK (Gimp ToolKit) est une biblioth\u00e8que logicielle libre qui permet de cr\u00e9er des interfaces graphiques. Elle est utilis\u00e9e par de nombreux logiciels, dont le bureau GNOME, GIMP, Inkscape, etc. C'est la biblioth\u00e8que graphique la plus utilis\u00e9e sous Linux.

    Architecture GTK

    Elle repose grandement sur GLib, la biblioth\u00e8que de base de GNOME, qui fournit des types de donn\u00e9es, des macros, des structures et des fonctions de base pour la programmation en C. Avec Glib on peut par exemple simplifier consid\u00e9rablement le d\u00e9veloppement en C, en fournissant des fonctions pour la gestion de la m\u00e9moire, des cha\u00eenes de caract\u00e8res, des listes, des tableaux, des arbres, des files d'attente, des piles, des tables de hachage etc. Voici un exemple simple d'utilisation de GLib\u2009:

    #include <glib.h>\n\nint main(int argc, char **argv) {\n    // Cr\u00e9er et parcourir une liste\n    GList *list = NULL;\n    list = g_list_append(list, \"Hello\");\n    list = g_list_append(list, \"World\");\n    g_list_foreach(list, (GFunc)g_print, NULL);\n    g_list_free(list);\n\n    // G\u00e9n\u00e9rer un nombre al\u00e9atoire\n    g_random_int();\n\n    // Simplifier la gestion des cha\u00eenes de caract\u00e8res\n    GString *string = g_string_new(\"Hello\");\n    g_string_append(string, \" World\");\n\n    // etc.\n}\n

    GDK (Gimp Drawing Kit) est une biblioth\u00e8que qui fournit des fonctions pour la gestion des fen\u00eatres, des \u00e9v\u00e9nements, des images, des polices de caract\u00e8res, etc. Elle est utilis\u00e9e par GTK pour dessiner les \u00e9l\u00e9ments graphiques.

    GSK (Gimp Scene Kit) est une biblioth\u00e8que qui fournit des fonctions pour la gestion des animations, des transitions, des effets graphiques, etc. Elle est utilis\u00e9e par GTK pour animer les \u00e9l\u00e9ments graphiques.

    Enfin PANGO est une biblioth\u00e8que qui fournit des fonctions pour la gestion des polices de caract\u00e8res, des textes, des langues, etc. Elle est utilis\u00e9e par GTK pour afficher du texte.

    Ces 4 biblioth\u00e8ques sont les composants principaux de GTK et elles se reposent sur diff\u00e9rentes librairies comme CAIRO pour le rendu graphique.

    Cairo est une biblioth\u00e8que de dessin 2D qui fournit des fonctions pour dessiner des lignes, des courbes, des formes, des images, des textes, etc. Elle est utilis\u00e9e par GDK pour dessiner les \u00e9l\u00e9ments graphiques.

    OpenGL et Wayland sont utilis\u00e9es pour le rendu graphique et l'int\u00e9gration avec le gestionnaire de fen\u00eatres.

    "}, {"location": "course-c/47-gui/gtk/#glade", "title": "Glade", "text": "

    Glade est un logiciel graphique qui permet de cr\u00e9er des interfaces graphiques pour GTK de mani\u00e8re interactive. Il permet de dessiner les fen\u00eatres, les boutons, les champs de texte, etc. et de g\u00e9n\u00e9rer le code source correspondant.

    Il est tr\u00e8s utile pour les d\u00e9butants qui ne connaissent pas encore bien GTK, car il permet de voir en temps r\u00e9el le rendu de l'interface graphique et de g\u00e9n\u00e9rer le code source correspondant. Il est \u00e9galement tr\u00e8s utile pour les d\u00e9veloppeurs exp\u00e9riment\u00e9s, car il permet de gagner du temps en \u00e9vitant de devoir \u00e9crire le code source \u00e0 la main.

    "}, {"location": "course-c/47-gui/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/47-gui/introduction/#quest-ce-quune-interface-graphique", "title": "Qu'est-ce qu'une interface graphique\u2009?", "text": "

    L'interface graphique, couramment d\u00e9sign\u00e9e sous l'acronyme GUI (Graphical User Interface), est la couche visuelle par laquelle un utilisateur interagit avec un syst\u00e8me informatique. Contrairement aux interfaces en ligne de commande (CLI Client Line Interface), o\u00f9 l'utilisateur doit saisir des commandes textuelles, une GUI propose une interaction plus intuitive, fond\u00e9e sur des \u00e9l\u00e9ments visuels tels que des fen\u00eatres, des ic\u00f4nes, des menus et des boutons.

    \u00c0 la naissance de l'informatique, les interactions \u00e9taient principalement r\u00e9serv\u00e9es \u00e0 des sp\u00e9cialistes capables de comprendre et de manipuler des commandes complexes. L'av\u00e8nement des interfaces graphiques, au d\u00e9but des ann\u00e9es 1980, a marqu\u00e9 un tournant fondamental dans l'accessibilit\u00e9 des syst\u00e8mes informatiques, ouvrant la voie \u00e0 une d\u00e9mocratisation rapide de ces technologies.

    Le Smaky, d\u00e9velopp\u00e9 par le professeur Jean-Daniel Nicoud au LAMI \u00e0 l'EPFL, est un des premiers ordinateurs grand public \u00e0 proposer une interface graphique. Il a \u00e9t\u00e9 commercialis\u00e9 d\u00e8s 1978. C'est le premier ordinateur personnel \u00e0 disposer d'une souris en standard laquelle a \u00e9t\u00e9 d\u00e9velopp\u00e9e en 1962 par Douglas Engelbart. Nicoud ayant \u00e9t\u00e9 s\u00e9duit par l'id\u00e9e l'a perfectionn\u00e9e au sein de son laboratoire. C'est Andr\u00e9 Guignard aussi l'origine du robot Khepera qui d\u00e9veloppa pour Nicoud la premi\u00e8re souris optom\u00e9canique fabriqu\u00e9e par Depraz et commercialis\u00e9e par Logitech.

    Souris Depraz

    Depuis lors, les interfaces graphiques ont non seulement rendu les ordinateurs plus accessibles \u00e0 un public non technique, mais elles ont \u00e9galement transform\u00e9 l'exp\u00e9rience utilisateur. L'interface graphique permet de manipuler les objets num\u00e9riques comme s'ils \u00e9taient des objets physiques. Cette analogie, que l'on appelle m\u00e9taphore d'interface, est l'un des piliers de la conception des GUI. Par exemple, la corbeille, o\u00f9 l'on d\u00e9pose les fichiers pour les supprimer, est une m\u00e9taphore simple, mais efficace qui transforme un concept abstrait en une action que tout utilisateur peut comprendre.

    En outre, les GUI facilitent les interactions complexes en cachant la complexit\u00e9 du code sous une couche de simplicit\u00e9 visuelle. Chaque clic sur un bouton, chaque interaction avec un menu d\u00e9clenche des op\u00e9rations en arri\u00e8re-plan, rendant l'utilisation du syst\u00e8me plus fluide et plus intuitive.

    Scarab\u00e9 Julodimorpha bakewelli

    Donald Hoffmann dans son excellent TED Talk intitul\u00e9 Do we see reality as it is\u2009? aborde la question de la perception de la r\u00e9alit\u00e9. Il explique que notre cerveau ne per\u00e7oit pas la r\u00e9alit\u00e9 telle qu'elle est, mais qu'il la mod\u00e9lise pour nous permettre de survivre. Les interfaces graphiques sont une forme de mod\u00e9lisation de la r\u00e9alit\u00e9 num\u00e9rique qui nous permet de manipuler des objets virtuels de mani\u00e8re intuitive.

    En effet, lorsque l'on observe nos interfaces modernes, nous y voyons des boutons, des curseurs, des fen\u00eatres. Est-ce la r\u00e9alit\u00e9 de notre perception\u2009? Si l'on prend une loupe et que l'on regarde de plus pr\u00e8s, on se rend compte que ces \u00e9l\u00e9ments ne sont que des pixels color\u00e9s sur un \u00e9cran\u2009: c'est la r\u00e9alit\u00e9 de l'interface graphique, mais qui, \u00e0 cette \u00e9chelle de perception ne nous permet pas de comprendre ce que nous manipulons.

    Le scarab\u00e9e Julodimorpha bakewelli est un exemple de la complexit\u00e9 de la r\u00e9alit\u00e9. Il est massif, brillant et brun. La femelle est incapable de voler. Le m\u00e2le vole, \u00e0 sa recherche. Il convoite une femelle s\u00e9duisante. Homo sapiens, dans un but similaire, se r\u00e9unit en groupe dans l'Outback australien accompagn\u00e9 de boissons ferment\u00e9es qui une fois vite sont laiss\u00e9es \u00e0 l'abandon. Or, il se trouve que ces bouteilles en verre sont massives, brillantes et brunes\u2009: une femelle s\u00e9duisante. Les m\u00e2les scarab\u00e9es se ruent sur les bouteilles pour tenter de s'accoupler comme illustr\u00e9 sur la figure ci-dessus. Ils perdent tout int\u00e9r\u00eat pour les femelles r\u00e9elles et l'esp\u00e8ce a failli dispara\u00eetre. L'Australie a d\u00fb modifier ses bouteilles pour sauver ses scarab\u00e9es.

    Pourrait-on consid\u00e9rer une seconde que ces scarab\u00e9es per\u00e7oivent la r\u00e9alit\u00e9 telle qu'elle est\u2009? Du point de vue du scarab\u00e9e il n'y a aucun doute, mais de notre perception la situation nous apparait comme risible et d\u00e9nu\u00e9e de sens. L'\u00e9volution leur a fourni un raccourci\u2009; une femelle, c'est tout ce qui est massif, brillant et brun, et plus c'est gros, mieux c'est. Le point de vue peut naturellement \u00eatre g\u00e9n\u00e9ralis\u00e9 et des \u00eatres venus d'ailleurs qui nous observeraient nous humains \u00e0 cliquer sur des boutons pour faire clignoter de petites lumi\u00e8res color\u00e9es agenc\u00e9es en grille pourraient se demander si nous percevons la r\u00e9alit\u00e9 telle qu'elle est. Vous voyez que la question est bien plus philosophique et profonde.

    Toutes ces consid\u00e9rations doivent nous amener \u00e0 nous interroger sur la nature m\u00eame de l'utilit\u00e9 d'une interface graphique. Dois \u00eatre elle s\u00e9duisante et donner l'illusion qu'elle se substitue au r\u00e9el ou doit \u00eatre fonctionnelle et servir un besoin pr\u00e9cis d'\u00eatre plus performante qu'une interface plus rudimentaire dans l'interaction entre l'homme et la machine\u2009?

    La technologie et l'av\u00e8nement de la souris ont craft\u00e9 une nouvelle mani\u00e8re de penser l'interaction entre l'homme et la machine. L'histoire des interfaces graphiques remonte \u00e0 1963, avec la conception du \u00ab\u2009Sketchpad\u2009\u00bb par Ivan Sutherland, un des premiers syst\u00e8mes \u00e0 exploiter des concepts graphiques.

    sketchpad

    Cependant, c'est Xerox qui, dans les ann\u00e9es 1970, avec son Xerox Alto, pose les bases des GUI modernes. La v\u00e9ritable r\u00e9volution survient avec l'introduction des interfaces graphiques par Apple en 1984, via le Macintosh, suivi de pr\u00e8s par Microsoft avec Windows. Ces syst\u00e8mes d'exploitation grand public int\u00e8grent les concepts de fen\u00eatres, d'ic\u00f4nes et de menus, offrant une alternative conviviale \u00e0 l'interface en ligne de commande.

    Xerox Alto

    Depuis, les interfaces graphiques n'ont cess\u00e9 d'\u00e9voluer, s'adaptant aux nouvelles technologies (touches tactiles, interfaces vocales) et aux besoins des utilisateurs. Aujourd'hui, les GUI se retrouvent non seulement sur les ordinateurs de bureau, mais aussi sur les appareils mobiles, les objets connect\u00e9s, et m\u00eame les syst\u00e8mes embarqu\u00e9s.

    Que seront-elles demain\u2009? La r\u00e9alit\u00e9 augment\u00e9e, l'intelligence artificielle, le neurofeedback avec des interfaces cerveau-ordinateur qui commencent \u00e0 \u00e9merger notamment avec le Neuralink d'Elon Musk.

    "}, {"location": "course-c/47-gui/introduction/#la-technologie-actuelle", "title": "La technologie actuelle", "text": "

    Nous voyons que d\u00e9finir une interface graphique est un probl\u00e8me bien plus vaste qui conteste notre perception du r\u00e9el, les objectifs pragmatiques d'un probl\u00e8me d'ing\u00e9nierie, les innovations technologiques et les perspectives d'avenir. N\u00e9anmoins s'il est important de comprendre ces enjeux, il est tout aussi important de savoir comment construire une interface graphique avec les technologies actuelles.

    "}, {"location": "course-c/47-gui/introduction/#types-dinterfaces-graphiques", "title": "Types d'interfaces graphiques", "text": "

    Le d\u00e9fi technique de la construction d'une interface graphique outre les crit\u00e8res d'efficacit\u00e9 et d'ergonomie est la portabilit\u00e9 de l'interface. Tout au long de se cours, nous abordons les probl\u00e8mes de compatibilit\u00e9 entre les syst\u00e8mes d'exploitation et les diff\u00e9rentes technologies d'interfaces. On peut citer aujourd'hui plusieurs cat\u00e9gories fondamentales d'interfaces graphiques\u2009:

    "}, {"location": "course-c/47-gui/introduction/#interfaces-specialisees-dequipements", "title": "Interfaces sp\u00e9cialis\u00e9es d'\u00e9quipements", "text": "

    Les interfaces graphiques de commandes d'\u00e9quipements professionnels se substituent aujourd'hui aux tableaux de bord et \u00e0 leurs boutons physiques par des \u00e9crans tactiles et des interfaces graphiques configurables et \u00e9volutives. Ces interfaces permettent de contr\u00f4ler des machines complexes, de visualiser des donn\u00e9es en temps r\u00e9el et de surveiller des processus industriels. On observe sur les deux figures ci-dessous une table de mixage traditionnelle et une table de mixage num\u00e9rique ou la transition technologique est clairement visible.

    Table de mixage traditionnelle

    Table de mixage num\u00e9rique Lawo

    Ces interfaces propri\u00e9taires n'ont pas vocation \u00e0 \u00eatre portable, elles sont con\u00e7ues pour un \u00e9quipement sp\u00e9cifique et sont souvent d\u00e9velopp\u00e9es dans des langages de haut niveau comme le C++ le Java ou le C#. Les composants graphiques sont souvent d\u00e9velopp\u00e9s sur mesure pour r\u00e9pondre aux besoins sp\u00e9cifiques de l'\u00e9quipement.

    "}, {"location": "course-c/47-gui/introduction/#interfaces-portables-de-commande", "title": "Interfaces portables de commande", "text": "

    De la m\u00eame mani\u00e8re, dans la commande de robots industriels, les t\u00e9l\u00e9commandes physiques ont \u00e9t\u00e9 remplac\u00e9es par des interfaces graphiques sur tablettes. Les besoins sont plus g\u00e9n\u00e9riques et les interfaces sont souvent d\u00e9velopp\u00e9es en utilisant des technologies web (HTML5, CSS, JavaScript) ou des technologies multiplateformes comme Flutter ou React Native.

    Interface d'un robot Kuka

    "}, {"location": "course-c/47-gui/introduction/#interfaces-de-controle-de-systemes-embarques", "title": "Interfaces de contr\u00f4le de syst\u00e8mes embarqu\u00e9s", "text": "

    L\u00e0 o\u00f9 le co\u00fbt de d\u00e9veloppement et la puissance de calcul limit\u00e9e des architectures embarqu\u00e9e est un enjeu, les interfaces graphiques sont r\u00e9duites \u00e0 un simple \u00e9cran tactile. La soci\u00e9t\u00e9 Decent Espresso a d\u00e9velopp\u00e9 par exemple une machine \u00e0 caf\u00e9 avec une interface graphique brisant les codes des machines \u00e0 caf\u00e9 traditionnelles ou de gros boutons et cadrans physiques \u00e9taient gage de qualit\u00e9 et de prestige.

    Interface d'une machine \u00e0 caf\u00e9 Decent

    Le prestige \u00e9tant une part importante du besoin de l'\u00e9quipement, l'interface graphique est un \u00e9l\u00e9ment cl\u00e9 qui doit \u00eatre soign\u00e9 tant au niveau de l'ergonomie que de l'esth\u00e9tisme et de la fluidit\u00e9 de l'interaction. Ces interfaces sont majoritairement d\u00e9velopp\u00e9es en utilisant des technologies web ou des technologies multiplateformes (QT, HTML, JavaScript).

    "}, {"location": "course-c/47-gui/introduction/#applications-embarquees", "title": "Applications embarqu\u00e9es", "text": "

    Embedded Wizard

    "}, {"location": "course-c/47-gui/introduction/#applications-mobiles", "title": "Applications mobiles", "text": "

    Interface de commande du robot Spot de Boston Dynamics

    "}, {"location": "course-c/47-gui/introduction/#applications-pc-multiplateformes", "title": "Applications PC multiplateformes", "text": "

    AutoCAD 2025

    "}, {"location": "course-c/47-gui/introduction/#ergonomie", "title": "Ergonomie", "text": "

    L'ergonomie des interfaces graphiques est au c\u0153ur de la conception des logiciels modernes. Il ne s'agit pas simplement d'esth\u00e9tique, mais bien de la mani\u00e8re dont une interface facilite ou entrave l'utilisation par l'utilisateur. Une bonne interface doit \u00eatre intuitive, efficace et accessible, permettant \u00e0 l'utilisateur d'atteindre ses objectifs rapidement et sans frustration.

    L'ergonomie, dans le cadre des interfaces graphiques, vise \u00e0 optimiser l'interaction entre l'utilisateur et le logiciel. Une interface bien pens\u00e9e doit respecter certains principes cl\u00e9s\u2009:

    Clart\u00e9

    Les \u00e9l\u00e9ments visuels doivent \u00eatre organis\u00e9s de mani\u00e8re claire et lisible. Chaque bouton, chaque menu, doit \u00eatre facilement identifiable et ses fonctions, explicites.

    Coh\u00e9rence

    Les interactions et les comportements des composants doivent \u00eatre uniformes \u00e0 travers l'interface. Par exemple, si un bouton annule une action dans une partie de l'application, il doit le faire dans toutes les autres. Un manque de coh\u00e9rence tr\u00e8s connu est les param\u00e8tres de Windows. Sous Windows 11 on retrouve parfois en cliquant sur \u00ab\u2009Param\u00e8tres avanc\u00e9s\u2009\u00bb une fen\u00eatre de param\u00e8tres tr\u00e8s diff\u00e9rents h\u00e9rit\u00e9e des anciennes versions de Windows.

    Simplicit\u00e9

    L'interface doit \u00e9viter de surcharger l'utilisateur avec trop d'options ou d'informations. Un design \u00e9pur\u00e9 permet \u00e0 l'utilisateur de se concentrer sur les t\u00e2ches essentielles. Apple est un exemple de cette philosophie avec ses interfaces minimalistes.

    Accessibilit\u00e9

    L'interface doit \u00eatre accessible \u00e0 tous les utilisateurs, y compris ceux ayant des limitations physiques ou cognitives. Cela inclut l'utilisation de tailles de police ad\u00e9quates, de couleurs contrast\u00e9es et la possibilit\u00e9 de naviguer \u00e0 l'aide du clavier ou de dispositifs d'assistance.

    "}, {"location": "course-c/47-gui/introduction/#le-c-et-les-interfaces-graphiques", "title": "Le C et les interfaces graphiques", "text": "

    Je dois l'admettre, le C n'est pas le langage le plus adapt\u00e9 pour le d\u00e9veloppement d'interfaces graphiques. Les GUI modernes sont souvent construits en utilisant des langages de haut niveau comme C++, Java, C#, Python ou JavaScript, qui offrent des biblioth\u00e8ques et des frameworks d\u00e9di\u00e9s \u00e0 la cr\u00e9ation d'interfaces graphiques bas\u00e9e sur des paradigmes objet, asynchrone et \u00e9v\u00e9nementiel.

    Je n'ai pas beaucoup d'exemples d'applications graphiques compl\u00e8tes en C \u00e0 vous montrer mise \u00e0 part peut \u00eatre Gimp, un logiciel de retouche d'image open source qui utilise GTK (GIMP Toolkit). En outre, la plupart des biblioth\u00e8ques graphiques sont \u00e9crites en C++. L'offre pour le C est donc limit\u00e9e, mais elle n'est pas inexistante.

    La biblioth\u00e8que la plus connue pour le d\u00e9veloppement d'interfaces graphiques en C est GTK, qui est utilis\u00e9e par de nombreuses applications open source, telles que GIMP, Inkscape, ou encore GNOME. GTK est une biblioth\u00e8que multiplateforme qui offre des composants graphiques, des outils de dessin, et une gestion des \u00e9v\u00e9nements. Elle est \u00e9crite en C, mais elle propose des bindings pour de nombreux autres langages, comme Python ou JavaScript. GTK est un excellent choix si vous souhaitez d\u00e9velopper des applications graphiques en C, de plus la biblioth\u00e8que est portable et bien document\u00e9e.

    Une autre biblioth\u00e8que populaire est SDL, Simple DirectMedia Layer, qui est une biblioth\u00e8que multim\u00e9dia utilis\u00e9e pour le d\u00e9veloppement de jeux vid\u00e9o, mais qui peut \u00e9galement \u00eatre utilis\u00e9e pour cr\u00e9er des interfaces graphiques. SDL est \u00e9crite en C, mais elle propose des bindings pour de nombreux autres langages, comme C++, Python, ou Lua. SDL est une biblioth\u00e8que l\u00e9g\u00e8re et portable, qui offre des fonctionnalit\u00e9s de base pour la cr\u00e9ation d'interfaces graphiques.

    Un autre choix un peu moins populaire, mais qui m\u00e9rite d'\u00eatre cit\u00e9 pour sa longue histoire est la biblioth\u00e8que Allero. Elle a \u00e9t\u00e9 originellement cr\u00e9\u00e9e pour les ordinateurs Atari dans les ann\u00e9es 90. Allero est une biblioth\u00e8que multim\u00e9dia qui offre des fonctionnalit\u00e9s de dessin 2D, de son, de gestion d'entr\u00e9es utilisateur.

    "}, {"location": "course-c/47-gui/opengl/", "title": "OpenGL", "text": ""}, {"location": "course-c/47-gui/opengl/#versions", "title": "Versions", "text": "

    On distingues plusieurs versions d'OpenGL\u2009:

    OpenGL 1.0\u2009: Premi\u00e8re version d'OpenGL sortie en 1992. OpenGL 2.0\u2009: Version sortie en 2004 qui introduit les shaders. OpenGL 3.0\u2009: Version sortie en 2008 qui introduit les shaders de g\u00e9om\u00e9trie. OpenGL 4.0\u2009: Version sortie en 2010 qui introduit les shaders de tessellation. OpenGL 4.3\u2009: Version sortie en 2012 qui introduit les shaders de calcul. OpenGL 4.6\u2009: Version sortie en 2017 qui introduit les shaders de t\u00e2ches.

    OpenGL ES 2.0\u2009: Version d'OpenGL pour les syst\u00e8mes embarqu\u00e9s. Vulkan\u2009: API graphique bas niveau qui succ\u00e8de \u00e0 OpenGL.

    Wayland\u2009: Protocole de communication entre le serveur et les clients. X11\u2009: Protocole de communication entre le serveur et les clients.

    "}, {"location": "course-c/47-gui/opengl/#fonctionnement", "title": "Fonctionnement", "text": ""}, {"location": "course-c/47-gui/opengl/#vertex", "title": "Vertex", "text": "

    La premi\u00e8re \u00e9tape du pipeline graphique est la transformation des coordonn\u00e9es des sommets des objets g\u00e9om\u00e9triques en coordonn\u00e9es de l'\u00e9cran. Cette \u00e9tape est appel\u00e9e vertex processing et consiste \u00e0 appliquer des transformations g\u00e9om\u00e9triques (comme la translation, la rotation, l'\u00e9chelle) aux sommets des objets. Les coordonn\u00e9es des sommets sont g\u00e9n\u00e9ralement d\u00e9finies dans un espace 3D, mais elles doivent \u00eatre transform\u00e9es en coordonn\u00e9es 2D pour l'affichage \u00e0 l'\u00e9cran.

    Imaginons que l'on souhaite dessiner une pyramide en 3D. Chaque sommet de la pyramide est d\u00e9fini par ses coordonn\u00e9es (x, y, z) dans l'espace 3D. Les donn\u00e9es sont \u00e9crites dans un buffer de sommets comme dans l'exemple suivant\u2009:

    struct Vertex { float x, y, z; }[] = {\n   {0.0f, 1.0f, 0.0f},   // Sommet 0\n   {-1.0f, -1.0f, 1.0f}, // Sommet 1\n   {1.0f, -1.0f, 1.0f},  // Sommet 2\n   {1.0f, -1.0f, -1.0f}, // Sommet 3\n   {-1.0f, -1.0f, -1.0f} // Sommet 4\n};\n

    Un vertex (sommet) peut alternativement contenir des informations suppl\u00e9mentaires comme la couleur, la texture ou la normale. Cette derni\u00e8re est une information importante pour le calcul de l'\u00e9clairage.

    Dans une carte graphique toute forme g\u00e9om\u00e9trique est d\u00e9finie par des sommets, lesquels forment des triangles. Les triangles sont les formes les plus simples \u00e0 dessiner et sont utilis\u00e9s pour repr\u00e9senter des surfaces planes. Il faut deux triangles pour dessiner un rectangle, trois pour un quadrilat\u00e8re, et un certain nombre pour dessiner un cercle. Il est int\u00e9ressant de noter qu'une carte graphique n'est pas capable de dessiner des cercles \u00e0 partir de coordonn\u00e9es simples, ces derniers seront toujours apprixim\u00e9s par des triangles.

    "}, {"location": "course-c/47-gui/opengl/#assemblage-des-primitives", "title": "Assemblage des primitives", "text": "

    Les sommets seuls ne forment pas encore une g\u00e9om\u00e9trie compl\u00e8te. Ils doivent \u00eatre assembl\u00e9s en primitives (triangles, lignes, points). L'assemblage des primitives consiste \u00e0 prendre des groupes de sommets et \u00e0 les combiner pour former des triangles ou d'autres formes g\u00e9om\u00e9triques. La primitive la plus courante est le triangle, c'est d'ailleurs celle que nous avons utilis\u00e9e pour d\u00e9finir la pyramide pr\u00e9c\u00e9dente. N\u00e9anmoins il existe d'autres primitives comme le point, la ligne, le triangle strip, le triangle fan, etc. Les deux derni\u00e8res primitives sont utilis\u00e9es pour optimiser le nombre de sommets \u00e0 envoyer \u00e0 la carte graphique, certains sommets peuvent \u00eatre partag\u00e9s entre plusieurs triangles.

    "}, {"location": "course-c/47-gui/opengl/#traitement-des-sommets", "title": "Traitement des sommets", "text": "

    Une fois les primitives assembl\u00e9es, les sommets sont trait\u00e9s par le vertex shader. Le vertex shader est un petit programme \u00e9crit en langage de shader (comme GLSL pour OpenGL ou HLSL pour DirectX sous Windows) qui s'ex\u00e9cute sur chaque sommet de la primitive. Le GLSL est un langage tr\u00e8s proche du C mais beaucoup plus limit\u00e9. En revanche il est capable de tirer parti des capacit\u00e9s de calcul parall\u00e8le des cartes graphiques et donc d'\u00eatre tr\u00e8s performant pour certaines op\u00e9rations.

    Le vertex shader est responsable de la transformation des coordonn\u00e9es des sommets, de l'application des textures, de l'\u00e9clairage, et d'autres op\u00e9rations g\u00e9om\u00e9triques.

    Par exemple, notre pyramide devra \u00eatre orient\u00e9e dans l'espace 3D. Selon la position de la cam\u00e9ra, la pyramide devra \u00eatre tourn\u00e9e, d\u00e9plac\u00e9e, et \u00e9ventuellement \u00e9clair\u00e9e. Par l'application de matrices de transformation (translation, rotation, mise \u00e0 l'\u00e9chelle) le vertex shader permet de passer de coordonn\u00e9es locales aux coordonn\u00e9es de l'espace de la cam\u00e9ra, puis aux coordonn\u00e9es de l'\u00e9cran.

    Selon l'\u00e9clairage de la sc\u00e8ne, le vertex shader peut \u00e9galement calculer la couleur de chaque sommet en fonction de la position de la lumi\u00e8re. Cette couleur est ensuite interpol\u00e9e entre les sommets pour obtenir une couleur lisse sur toute la surface du triangle.

    Enfin le vertex shader peut \u00e9galement calculer les coordonn\u00e9es de texture pour chaque sommet. Une texture est une image (comme une photo ou une illustration) qui est appliqu\u00e9e \u00e0 la surface d'un objet pour lui donner un aspect r\u00e9aliste.

    Dans notre exemple, imagions que notre pyramide est celle de Kheops, de Kh\u00e9phren ou de Myk\u00e9rinos et que notre cam\u00e9ra est un drone qui survole le Caire. Il est 17h, le soleil est bas sur l'horizon et \u00e9claire la pyramide.

    Pyramide depuis le Caire

    (donner un exemple concret avec les donn\u00e9es de la cam\u00e9ra, de la lumi\u00e8re et une texture de brique, une transformation de perspective de la lentille de la cam\u00e9ra, let normales des faces pour le calcul de l'\u00e9clairage. Donner un exemple en GLSL du vertex shader pour la pyramide.)

    "}, {"location": "course-c/47-gui/opengl/#tessellation", "title": "Tessellation", "text": "

    La tessellation est une \u00e9tape optionnelle du pipeline graphique qui permet de subdiviser les primitives en triangles plus petits. Cette technique est utilis\u00e9e pour augmenter la densit\u00e9 de triangles dans les zones de la sc\u00e8ne qui n\u00e9cessitent plus de d\u00e9tails, comme les courbes ou les surfaces complexes. La tessellation est particuli\u00e8rement utile pour le rendu de surfaces lisses et organiques, comme les visages humains ou les paysages naturels.

    "}, {"location": "course-c/47-gui/opengl/#geometrie", "title": "G\u00e9om\u00e9trie", "text": "

    L'\u00e9tape de g\u00e9om\u00e9trie est une autre \u00e9tape optionnelle du pipeline graphique qui permet de g\u00e9n\u00e9rer de nouveaux sommets \u00e0 partir des sommets d'entr\u00e9e. Cette \u00e9tape est utile pour ajouter des d\u00e9tails suppl\u00e9mentaires \u00e0 la g\u00e9om\u00e9trie, comme des ar\u00eates suppl\u00e9mentaires, des plis ou des d\u00e9formations. La g\u00e9om\u00e9trie est souvent utilis\u00e9e pour g\u00e9n\u00e9rer des ombres, des reflets ou des effets sp\u00e9ciaux dans les jeux vid\u00e9o et les applications graphiques.

    Par exemple si vous souhaitez dessiner 1000 triangles \u00e0 l'\u00e9cran, chacun avec une orientation diff\u00e9rente, il n'est pas n\u00e9cessaire de sp\u00e9cifier les 3000 sommets correspondants. Il suffit de donner les centres des triangles et les orientations. Le geometry shader se chargera de g\u00e9n\u00e9rer les sommets correspondants. Voici un exemple ci-dessous de code GLSL pour un geometry shader qui g\u00e9n\u00e8re des triangles \u00e0 partir de points. Le programme main sera appel\u00e9 par la carte graphique pour chaque point du buffer d'entr\u00e9e, il s'executera donc en parall\u00e8le pour chaque point. Un vec4 est un vecteur de 4 composantes, ici les coordonn\u00e9es \\(x\\), \\(y\\), \\(z\\) et \\(w\\). Le \\(w\\) est une composante suppl\u00e9mentaire dont nous n'avons pas besoin ici.

    #version 330 core\n\nlayout (points) in;\nlayout (triangle_strip, max_vertices = 3) out;\n\nvoid main() {\n   gl_Position = gl_in[0].gl_Position + vec4(-0.1, -0.1, 0.0, 0.0);\n   EmitVertex();\n\n   gl_Position = gl_in[0].gl_Position + vec4(0.1, -0.1, 0.0, 0.0);\n   EmitVertex();\n\n   gl_Position = gl_in[0].gl_Position + vec4(0.0, 0.1, 0.0, 0.0);\n   EmitVertex();\n\n   EndPrimitive();\n}\n
    ", "tags": ["main", "vec4"]}, {"location": "course-c/47-gui/opengl/#clipping", "title": "Clipping", "text": "

    L'\u00e9tape de clipping consiste \u00e0 \u00e9liminer les parties de la g\u00e9om\u00e9trie qui ne sont pas visibles \u00e0 l'\u00e9cran. Cette \u00e9tape est n\u00e9cessaire pour \u00e9viter de dessiner des objets qui sont en dehors du champ de vision de la cam\u00e9ra. Le clipping est g\u00e9n\u00e9ralement effectu\u00e9 en coordonn\u00e9es d'\u00e9cran, apr\u00e8s la projection des sommets en 2D. Le frustrum est une forme tronqu\u00e9e qui repr\u00e9sente le champ de vision de la cam\u00e9ra. Les parties de la g\u00e9om\u00e9trie qui se trouvent en dehors du frustrum sont \u00e9limin\u00e9es.

    Cette \u00e9tape n'a pas besoin d'\u00eatre g\u00e9r\u00e9e manuellement par le d\u00e9veloppeur, elle est g\u00e9n\u00e9ralement effectu\u00e9e par le mat\u00e9riel graphique de mani\u00e8re transparente.

    "}, {"location": "course-c/47-gui/opengl/#rasterisation", "title": "Rasterisation", "text": "

    La rasterisation est l'\u00e9tape du pipeline graphique qui transforme les primitives g\u00e9om\u00e9triques en pixels \u00e0 afficher \u00e0 l'\u00e9cran. Cette \u00e9tape consiste \u00e0 d\u00e9terminer quels pixels sont couverts par les primitives et \u00e0 calculer la couleur de chaque pixel en fonction de la couleur des sommets. La rasterisation est une op\u00e9ration complexe qui n\u00e9cessite de calculer l'intersection des primitives avec les pixels de l'\u00e9cran et d'appliquer des algorithmes de remplissage pour d\u00e9terminer la couleur de chaque pixel.

    Par exemple \u00e0 partir des sommets de notre pyramide, la rasterisation va calculer les pixels couverts par les triangles form\u00e9s par les sommets de fa\u00e7on \u00e0 remplir les faces de la pyramide. Les pixels couverts par les triangles sont ensuite color\u00e9s en fonction de la couleur des sommets et de l'\u00e9clairage de la sc\u00e8ne.

    On dit que les primitives sont rasteris\u00e9es lorsqu'elles sont transform\u00e9es en fragments. Un fragment est un pixel potentiel qui doit \u00eatre color\u00e9.

    "}, {"location": "course-c/47-gui/opengl/#fragment", "title": "Fragment", "text": "

    \u00c0 cette \u00e9tape du pipeline graphique, nos sommets ont \u00e9t\u00e9 convertis en des milliers de fragments. Chaque fragment correspond \u00e0 un pixel de l'\u00e9cran et doit \u00eatre color\u00e9. Le fragment shader est un autre programme \u00e9crit en langage de shader (GLSL) qui s'ex\u00e9cute sur chaque fragment de la primitive.

    Le fragment shader est responsable de calculer la couleur finale de chaque pixel en fonction de la couleur des sommets, de la texture, de l'\u00e9clairage, et d'autres param\u00e8tres. Le programme retourne une couleur pour chaque fragment.

    (donner un exemple concret avec les donn\u00e9es de la cam\u00e9ra, de la lumi\u00e8re, une texture de brique, les normales des faces pour le calcul de l'\u00e9clairage. Donner un exemple en GLSL du fragment shader pour la pyramide.)

    "}, {"location": "course-c/47-gui/opengl/#test-de-profondeur", "title": "Test de profondeur", "text": "

    Une fois les fragments calcul\u00e9s, ils subissent une s\u00e9rie de tests pour d\u00e9terminer s'ils doivent \u00eatre affich\u00e9s \u00e0 l'\u00e9cran. Le test de profondeur est un test qui compare la profondeur de chaque fragment avec la profondeur des fragments d\u00e9j\u00e0 affich\u00e9s \u00e0 l'\u00e9cran. Si le fragment est plus proche de la cam\u00e9ra que les fragments d\u00e9j\u00e0 affich\u00e9s, il est affich\u00e9 \u00e0 l'\u00e9cran. Sinon, il est rejet\u00e9. Ce test est appel\u00e9 le Z-Test.

    Il existe \u00e9galement le Stencil Test qui permet de d\u00e9finir une zone de l'\u00e9cran o\u00f9 les fragments peuvent \u00eatre affich\u00e9s. Cela permet de cr\u00e9er des effets sp\u00e9ciaux comme des ombres, des reflets, ou des effets de transparence.

    Enfin le Blending permet de m\u00e9langer le fragment avec les fragments d\u00e9j\u00e0 affich\u00e9s \u00e0 l'\u00e9cran. Cela permet de cr\u00e9er des effets de transparence, de luminosit\u00e9, ou de flou.

    "}, {"location": "course-c/47-gui/opengl/#ecriture-dans-le-framebuffer", "title": "\u00c9criture dans le framebuffer", "text": "

    Une fois que tous les tests et calculs ont \u00e9t\u00e9 effectu\u00e9s, les fragments restants sont convertis en pixels et \u00e9crits dans le framebuffer (la m\u00e9moire vid\u00e9o). Le framebuffer contient les pixels qui seront envoy\u00e9s \u00e0 l\u2019\u00e9cran pour \u00eatre affich\u00e9s.

    "}, {"location": "course-c/47-gui/opengl/#opengl-et-vulkan", "title": "OpenGL et Vulkan", "text": "

    OpenGL, ou Open Graphics Library, est une API graphique multiplateforme utilis\u00e9e principalement pour le rendu 2D et 3D dans des applications interactives. Elle est tr\u00e8s r\u00e9pandue dans l'industrie des jeux vid\u00e9o, la visualisation scientifique, la mod\u00e9lisation 3D, et les simulations interactives. Con\u00e7ue \u00e0 l'origine pour permettre l'acc\u00e9l\u00e9ration graphique en temps r\u00e9el via des cartes graphiques, OpenGL a marqu\u00e9 une r\u00e9volution en facilitant le d\u00e9veloppement d'applications graphiques de haute performance tout en masquant les d\u00e9tails sp\u00e9cifiques au mat\u00e9riel.

    OpenGL a \u00e9t\u00e9 initialement d\u00e9velopp\u00e9 par Silicon Graphics, Inc. (SGI) en 1992. \u00c0 l'\u00e9poque, SGI dominait le march\u00e9 des stations de travail graphiques de haute performance, utilis\u00e9es dans des domaines comme la mod\u00e9lisation 3D et la simulation scientifique. SGI voulait une API standardis\u00e9e qui permettrait aux d\u00e9veloppeurs de concevoir des applications ind\u00e9pendantes des sp\u00e9cificit\u00e9s mat\u00e9rielles des diff\u00e9rentes cartes graphiques, tout en tirant parti de l'acc\u00e9l\u00e9ration mat\u00e9rielle.

    Le but d'OpenGL \u00e9tait de fournir une interface simple et uniforme qui fonctionne sur divers syst\u00e8mes d'exploitation (Windows, Linux, macOS) et plateformes mat\u00e9rielles, permettant ainsi une portabilit\u00e9 accrue des applications graphiques. Depuis, OpenGL a \u00e9volu\u00e9 au fil des ann\u00e9es, en int\u00e9grant de nombreuses fonctionnalit\u00e9s graphiques modernes, comme les shaders et les buffers de trames.

    Bien que d'autres API graphiques comme DirectX (pour Windows) ou Direct3D (pour les jeux vid\u00e9o) soient \u00e9galement tr\u00e8s populaires, OpenGL reste une API de choix pour de nombreux d\u00e9veloppeurs en raison de sa portabilit\u00e9, de sa flexibilit\u00e9 et de sa compatibilit\u00e9 avec un large \u00e9ventail de mat\u00e9riels.

    N\u00e9anmoins, certaines limitations ont conduit le Khronos Group qui g\u00e8re OpenGL a d\u00e9velopp\u00e9 Vulkan, une nouvelle API graphique et de calcul, publi\u00e9e en 2016. Vulkan est consid\u00e9r\u00e9 comme le successeur d'OpenGL et offre de nombreuses am\u00e9liorations qui r\u00e9pondent aux besoins modernes des d\u00e9veloppeurs de jeux et d\u2019applications graphiques.

    Les jeux vid\u00e9os modernes comme FarCry, The Witcher 3, Red Dead Redemption 2, ou encore Cyberpunk 2077 utilisent des moteurs graphiques bas\u00e9s sur les API Vulkan ou DirectX 12 pour tirer parti des performances des cartes graphiques r\u00e9centes.

    En 2024, sur Windows, DirectX est l'API dominante. N\u00e9anmoins avec la technologie DXVK qui est une couche de traduction permettant \u00e0 des applications Vulkan de fonctionner avec DirectX 12.

    "}, {"location": "course-c/47-gui/opengl/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    OpenGL et Vulkan fonctionnent sur le principe de la programmation par \u00e9tats. Cela signifie que l'application configure l'\u00e9tat de l'API graphique en d\u00e9finissant des param\u00e8tres comme la couleur, la texture, la lumi\u00e8re, la perspective, etc. Une fois l'\u00e9tat configur\u00e9, l'application envoie des commandes graphiques \u00e0 l'API pour dessiner des objets g\u00e9om\u00e9triques, des textures, des effets visuels, etc.

    Ces API offrent une abstraction du mat\u00e9riel graphique sous-jacent, permettant aux d\u00e9veloppeurs de concevoir des applications graphiques sans se soucier des d\u00e9tails sp\u00e9cifiques \u00e0 la carte graphique. En effet, une carte graphique contient des milliers de c\u0153urs de calcul qui peuvent \u00eatre programm\u00e9s pour effectuer des calculs parall\u00e8les. Heureusement pour les d\u00e9veloppeurs, OpenGL et Vulkan fournissent des interfaces de haut niveau pour exploiter ces capacit\u00e9s de calcul sans avoir \u00e0 g\u00e9rer les d\u00e9tails complexes du mat\u00e9riel.

    L'abstraction consiste principalement \u00e0 ce que l'on nomme le pipeline graphique. Le pipeline graphique est une s\u00e9quence d'\u00e9tapes qui transforme les donn\u00e9es g\u00e9om\u00e9triques en pixels affich\u00e9s \u00e0 l'\u00e9cran. Ces \u00e9tapes incluent la transformation des coordonn\u00e9es, l'application des textures, l'\u00e9clairage, la perspective, et bien d'autres. Chaque \u00e9tape du pipeline est configurable par l'application, permettant ainsi de personnaliser le rendu graphique en fonction des besoins.

    "}, {"location": "course-c/47-gui/opengl/#carte-graphique", "title": "Carte graphique", "text": ""}, {"location": "course-c/47-gui/opengl/#pipeline", "title": "Pipeline", "text": "

    Le pipeline graphique d'OpenGL et de Vulkan est compos\u00e9 de plusieurs \u00e9tapes, chacune effectuant une transformation sp\u00e9cifique sur les donn\u00e9es graphiques. Voici les \u00e9tapes principales du pipeline graphique\u2009:

    "}, {"location": "course-c/47-gui/opengl/#double-buffer", "title": "Double Buffer", "text": "

    OpenGL utilise un double buffer pour afficher les images. Le double buffer est compos\u00e9 de deux buffers\u2009: un buffer de dessin et un buffer d'affichage. Le buffer de dessin est utilis\u00e9 pour dessiner les images et le buffer d'affichage est utilis\u00e9 pour afficher les images. Lorsque l'image est dessin\u00e9e dans le buffer de dessin, elle est ensuite copi\u00e9e dans le buffer d'affichage. Cela permet d'\u00e9viter les probl\u00e8mes de scintillement. C'est une pratique tr\u00e8s courante dans les applications graphiques.

    "}, {"location": "course-c/47-gui/opengl/#vsync", "title": "Vsync", "text": "

    La synchronisation verticale (Vsync) est une technique qui permet de synchroniser le taux de rafra\u00eechissement de l'\u00e9cran avec le taux de rafra\u00eechissement de l'application. Cela permet d'\u00e9viter les probl\u00e8mes de d\u00e9chirure d'\u00e9cran. La synchronisation verticale est g\u00e9n\u00e9ralement activ\u00e9e par d\u00e9faut dans les applications graphiques.

    "}, {"location": "course-c/47-gui/opengl/#gflw", "title": "GFLW", "text": "

    La biblioth\u00e8que GLFW est une biblioth\u00e8que C qui permet de cr\u00e9er des fen\u00eatres avec OpenGL. Elle est compatible avec OpenGL ES et Vulkan. Elle est utilis\u00e9e pour cr\u00e9er des fen\u00eatres et g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre. On pourrait tr\u00e8s bien utiliser GTK n\u00e9anmoins GLFW est plus simple et plus adapt\u00e9 \u00e0 OpenGL.

    Pour installer GLFW sur Ubuntu, il suffit d'installer la bibloth\u00e8que avec les fichiers d'en-t\u00eate\u2009:

    sudo apt install libglfw3-dev\n

    Pour compiler un programme avec GLFW, il est n\u00e9cessaire de lier la biblioth\u00e8que avec le programme. Pour cela, il suffit d'ajouter l'option -lglfw \u00e0 la commande de compilation.

    Un programme simple qui cr\u00e9e une fen\u00eatre avec GLFW\u2009:

    #include <GLFW/glfw3.h>\n\nint main() {\n   if (!glfwInit()) return -1;\n   GLFWwindow* window = glfwCreateWindow(800, 400, \"Window\", NULL, NULL);\n   if (!window) {\n      glfwTerminate();\n      return -2;\n   }\n   glfwMakeContextCurrent(window);\n   while (!glfwWindowShouldClose(window)) {\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   }\n}\n

    On observe que la biblioth\u00e8que est tout d'abord initialis\u00e9e avec la fonction glfwInit(). Ensuite, une fen\u00eatre est cr\u00e9\u00e9e avec un titre. Les deux derniers param\u00e8tres laiss\u00e9s \u00e0 NULL sont des pointeurs sur le moniteur sur laquelle la fen\u00eatre est affich\u00e9e GLFWmonitor et la fen\u00eatre du parent GLFWwindow utilis\u00e9e dans le cas d'une application multi-fen\u00eatres. Dans notre cas on laisse ces param\u00e8tres \u00e0 NULL car nous n'avons pas besoin de ces fonctionnalit\u00e9s.

    Si la fen\u00eatre n'a pas pu \u00eatre cr\u00e9\u00e9e, le programme se termine. Sinon, la fen\u00eatre est affich\u00e9e avec la fonction glfwMakeContextCurrent(window).

    Enfin, une boucle est cr\u00e9\u00e9e pour afficher la fen\u00eatre tant que l'utilisateur ne la ferme pas. La fonction glfwSwapBuffers(window) permet de copier le contenu du buffer de dessin dans le buffer d'affichage. La fonction glfwPollEvents() permet de g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre.

    Le grand avantage de ce programme est qu'il est portable. Il fonctionne sur Windows, Linux et MacOS.

    GLFW permet \u00e9galement de g\u00e9rer les joystick et gamepads, les \u00e9v\u00e8nements du clavier ou de la souris ainsi que le curseurs de la souris. Cela permet de cr\u00e9er des applications graphiques interactives sans n\u00e9cessit\u00e9 d'avoir recours \u00e0 d'autres biblioth\u00e8ques.

    Nous n'approfonfirons pas plus GLFW dans ce cours n\u00e9anmoins, vous avez toujours la possibilit\u00e9 de consulter la documentation officielle de GLFW qui est tr\u00e8s compl\u00e8te.

    ", "tags": ["NULL", "GLFWmonitor", "GLFWwindow"]}, {"location": "course-c/47-gui/opengl/#glew", "title": "GLEW", "text": "

    La biblioth\u00e8que GLEW (OpenGL Extension Wrangler Library) est une biblioth\u00e8que qui facilite le chargement des extensions OpenGL. OpenGL a un ensemble de fonctionnalit\u00e9s qui peut varier selon le mat\u00e9riel graphique et le syst\u00e8me d'exploitation, et GLEW est utilis\u00e9 pour acc\u00e9der \u00e0 ces fonctionnalit\u00e9s de mani\u00e8re portable. En sommes, la biblioth\u00e8que permet de charger dynamiquement des fonctions OpenGL qui ne sont pas directement accessibles par le syst\u00e8me, surtout pour les versions modernes d'OpenGL.

    Pour disposer d'un contexte OpenGL utilsable nous devons compl\u00e9ter notre programme pr\u00e9c\u00e9dent avec GLEW. D'abord des messages d'erreurs plus explicites sont affich\u00e9s en cas d'erreur. Certains param\u00e8tres sont ajout\u00e9s pour activer l'anticr\u00e9nelage et sp\u00e9cifier la version d'OpenGL que nous voulons utiliser.

    Dans la boucle principale, la touche echape permet maintenant de quitter le programme.

    #include <GL/glew.h>\n#include <GLFW/glfw3.h>\n#include <stdbool.h>\n#include <stdio.h>\n\nint main() {\n   if (!glfwInit()) {\n      fprintf(stderr, \"Failed to initialize GLFW\\n\");\n      return -1;\n   }\n   glfwWindowHint(GLFW_SAMPLES, 4);                // 4x antialiasing\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // We want OpenGL 3.3\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n   glfwWindowHint(GLFW_OPENGL_PROFILE,\n                  GLFW_OPENGL_CORE_PROFILE);  // We don't want the old OpenGL\n\n   // Open a window and create its OpenGL context\n   GLFWwindow* window = glfwCreateWindow(800, 400, \"OpenGL\", NULL, NULL);\n   if (window == NULL) {\n      fprintf(stderr, \"Error: Failed to open GLFW window.\\n\");\n      glfwTerminate();\n      return -1;\n   }\n   glfwMakeContextCurrent(window);\n   if (glewInit() != GLEW_OK) {\n      fprintf(stderr, \"Error: Failed to initialize GLEW\\n\");\n      return -1;\n   }\n   glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);\n   do {\n      glClear(GL_COLOR_BUFFER_BIT);\n\n      // ...\n\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   } while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&\n            glfwWindowShouldClose(window) == 0);\n}\n
    "}, {"location": "course-c/47-gui/opengl/#glut", "title": "GLUT", "text": "

    GLUT (OpenGL Utility Toolkit) est une biblioth\u00e8que qui facilite la cr\u00e9ation de fen\u00eatres OpenGL. Elle est plus ancienne que GLFW et elle est moins utilis\u00e9e. GLUT est une biblioth\u00e8que portable qui permet de cr\u00e9er des fen\u00eatres OpenGL sur Windows, Linux et MacOS. Elle permet \u00e9galement de g\u00e9rer les \u00e9v\u00e9nements de fen\u00eatre, les \u00e9v\u00e9nements de clavier et de souris, et les \u00e9v\u00e9nements de redimensionnement de fen\u00eatre. Pr\u00e9f\u00e9rez GLFW \u00e0 GLUT pour vos projets OpenGL.

    "}, {"location": "course-c/47-gui/opengl/#coordonnees", "title": "Coordonn\u00e9es", "text": "

    Le syst\u00e8me de coordonn\u00e9es d'OpenGL est un peu particulier. L'origine du syst\u00e8me de coordonn\u00e9es est au centre de la fen\u00eatre. Les coordonn\u00e9es x et y vont de -1 \u00e0 1. Les coordonn\u00e9es z vont de -1 \u00e0 1. Les coordonn\u00e9es x et y sont en 2D et la coordonn\u00e9e z est en 3D.

    On notera qu'il n'y a pas de notion de pixels dans le syst\u00e8me de coordonn\u00e9es d'OpenGL car les coordonn\u00e9es sont normalis\u00e9es et repr\u00e9sent\u00e9e par un flottant 32-bit. Nous verrons que pour dessiner des formes g\u00e9om\u00e9triques, la projection doit \u00eatre ajust\u00e9e car si la fen\u00eatre n'est pas carr\u00e9e, les formes g\u00e9om\u00e9triques seront d\u00e9form\u00e9es.

    Le \\(x\\) positif est \u00e0 droite, le \\(y\\) positif est en haut et le \\(z\\) positif est vers l'observateur. C'est la r\u00e8gle de la main droite issue de la physique o\u00f9 l'axe \\(z\\) est la direction du pouce, l'axe \\(x\\) est l'index et l'axe \\(y\\) est le majeur.

    Si nous souhaitons dessiner un triangle isoc\u00e8le centr\u00e9 dans l'espace, nous devons d\u00e9finir les coordonn\u00e9es des sommets du triangle.

    static const GLfloat verticles[] = {\n   -1.0f, -1.0f, 0.0f, // Bottom left\n   1.0f,  -1.0f, 0.0f, // Bottom right\n   0.0f,  1.0f,  0.0f, // Top\n};\n
    "}, {"location": "course-c/47-gui/opengl/#vertex_1", "title": "Vertex", "text": "

    Un vertex est un sommet dans l'espace 3D. Un vertex est d\u00e9fini par ses coordonn\u00e9es \\(x\\), \\(y\\) et \\(z\\) mais il peut \u00e9galement avoir d'autres attributs comme la couleur, la normale, la texture, etc. La normale est un vecteur perpendiculaire \u00e0 la surface du sommet, c'est une information importante pour les calculs d'\u00e9clairage. En effet, une surface va refl\u00e9ter la lumi\u00e8re diff\u00e9remment selon l'angle d'incidence de la lumi\u00e8re. OpenGL est assez flexible pour d\u00e9finir les attributs d'un vertex.

    D\u00e9finissons les primitives de base d'un vertex. Il peut contenir un point\u2009:

    typedef union Point2D {\n   struct {\n      GLfloat x;\n      GLfloat y;\n   };\n   GLfloat data[2];\n} Point2D;\n\ntypedef union Point3D {\n   struct {\n      GLfloat x;\n      GLfloat y;\n      GLfloat z;\n   };\n   GLfloat data[3];\n} Point3D;\n

    Il peut contenir une normale qui est un vecteur. Formellement un vecteur n'est pas un point mais il peut \u00eatre repr\u00e9sent\u00e9 par un point\u2009:

    typedef Point3D Vector3D;\n

    Il peut contenir une couleur. En OpenGL une couleur peut \u00eatre repr\u00e9sent\u00e9e par un vecteur de trois composantes rouge, vert et bleu, ou par un vecteur de quatre composantes rouge, vert, bleu et alpha pour g\u00e9rer la transparence\u2009:

    typedef union ColorRGB {\n   struct {\n      GLfloat r;\n      GLfloat g;\n      GLfloat b;\n   };\n   GLfloat data[3];\n} ColorRGB;\n\ntypedef union ColorRGBA {\n   struct {\n      GLfloat r;\n      GLfloat g;\n      GLfloat b;\n      GLfloat a;\n   };\n   GLfloat data[4];\n} ColorRGBA;\n

    Enfin, on peut imaginer diff\u00e9rents types de vertex\u2009:

    typedef union VertexA {\n   struct {\n      Point3D position;\n      ColorRGB color;\n      Vector3D normal;\n      Point2D texture_coordinates;\n   };\n   GLfloat data[11];\n} VertexA;\n\ntypedef union VertexB {\n   struct {\n      Point2D position;\n   };\n   GLfloat data[2];\n} VertexB;\n\ntypedef union VertexC {\n   struct {\n      ColorRGBA color;\n      Vector3D normal;\n      Point3D position;\n   };\n   GLfloat data[10];\n} VertexC;\n

    Une chose est s\u00fbre, un vertex est un tableau de flottants 32-bits et il est rarement seul. Il est souvent regroup\u00e9 dans un tableau de vertex pour former un objet g\u00e9om\u00e9trique.

    Comme l'ordre des attributs d'un vertex configurable, il est n\u00e9cessaire d'informer OpenGL de la mani\u00e8re dont les attributs sont structur\u00e9s. Pour configurer notre VertexC nous utiliserons par exemple\u2009:

    // Position\nglEnableVertexAttribArray(0);\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(6 * sizeof(GLfloat)));\n\n// Color\nglEnableVertexAttribArray(1);\nglVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(0 * sizeof(GLfloat)));\n\n// Normal\nglEnableVertexAttribArray(2);\nglVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexC),\n    (GLvoid*)(3 * sizeof(GLfloat)));\n

    Chaque vertex \u00e0 donc 3 attributs\u2009: la position, la couleur et la normale. L'attribut 0 est la couleur, l'attribut 1 est la normale et l'attribut 2 est la position. J'ai volontairement invers\u00e9 l'ordre des attributs pour montrer qu'il n'a pas d'importance, vous faites comme vous voulez.

    Le dernier argument de glVertexAttribPointer peut paraitre \u00e9trange. Pourquoi ne s'agit-il pas d'un uintptr_t mais d'un GLvoid* ? C'est une question de compatibilit\u00e9 avec les anciennes versions d'OpenGL. GLvoid* est un pointeur g\u00e9n\u00e9rique qui peut pointer sur n'importe quel type de donn\u00e9es. C'est en r\u00e9alit\u00e9 un void* en C. N\u00e9anmoins, ce n'est pas vraiment un pointeur non plus car il ne peut pas \u00eatre d\u00e9r\u00e9f\u00e9renc\u00e9. Il y a parfois des questions d'h\u00e9ritage dans les API ou cette derni\u00e8re \u00e0 \u00e9volu\u00e9e mais que pour des raisons de compatibilit\u00e9, certaines d\u00e9cisions historiques sont conserv\u00e9es.

    ", "tags": ["VertexC", "uintptr_t"]}, {"location": "course-c/47-gui/opengl/#vbo", "title": "VBO", "text": "

    Un VBO (Vertex Buffer Object) est un espace m\u00e9moire qui peut \u00eatre utilis\u00e9 pour stocker des donn\u00e9es de sommets. C'est en g\u00e9n\u00e9ral cet objet qui est partag\u00e9 entre le CPU et le GPU. \u00c9videmment plus le buffer est grand plus le temps de transfert peut \u00eatre long, surtout si l'op\u00e9ration est r\u00e9p\u00e9t\u00e9e \u00e0 chaque image.

    Avant de pouvoir utiliser un tel buffer il faut en faire la requ\u00eate \u00e0 OpenGL. Dans l'exemple suivant on demande \u00e0 OpenGL de nous allouer un seul buffer\u2009:

    Gluint vbo = 0;\nglGenBuffers(1, &vbo);\n

    La variable vbo contiendra l'identifiant du buffer allou\u00e9 par OpenGL. Une fois allou\u00e9 il sera important plus tard d'utiliser glDeleteBuffers(1, &vbo) pour lib\u00e9rer la m\u00e9moire allou\u00e9e dynamiquement.

    Afin d'\u00e9viter de passer l'identifiant du vbo \u00e0 toutes les fonctions OpenGL, qui traitent les buffers, la strat\u00e9gie adopt\u00e9e est de lier le buffer \u00e0 un contexte OpenGL. Cela se fait avec la fonction glBindBuffer :

    glBindBuffer(GL_ARRAY_BUFFER, vbo);\n

    Ici on indique que le vbo cr\u00e9\u00e9 sera utilis\u00e9 pour stocker des GL_ARRAY_BUFFER, c'est-\u00e0-dire des donn\u00e9es de sommets, et que ce vbo sera d\u00e9sormais le buffer actif pour toutes les fonctions du type glBufferData, glBufferSubData, glMapBuffer, etc. Et ce jusqu'\u00e0 ce qu'un autre buffer soit li\u00e9 ou que le buffer soit supprim\u00e9.

    Plus haut, nous avions d\u00e9fini les coordonn\u00e9es d'un triangle en 3 dimensions. Il est maintenant temps de les envoyer \u00e0 la carte graphique. Pour cela nous utilisons la fonction glBufferData apr\u00e8s que le buffer ait \u00e9t\u00e9 li\u00e9 au contexte\u2009:

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n

    La constante GL_STATIC_DRAW indique \u00e0 OpenGL que les donn\u00e9es ne changeront pas souvent. Il existe d'autres constantes pour indiquer \u00e0 OpenGL que les donn\u00e9es seront modifi\u00e9es fr\u00e9quemment ou rarement. Cela permet \u00e0 OpenGL d'optimiser le stockage des donn\u00e9es en m\u00e9moire.

    ", "tags": ["glMapBuffer", "glBufferData", "vbo", "glBindBuffer", "GL_STATIC_DRAW", "glBufferSubData", "GL_ARRAY_BUFFER"]}, {"location": "course-c/47-gui/opengl/#vao", "title": "VAO", "text": "

    Nous avons vu plus haut qu'un vertex peut \u00eatre plus ou moins complexe en fonction des attributs qu'il contient. Un VAO (Vertex Array Object) est un tableau qui stocke l'\u00e9tat des attributs de vertex. Nous avons vu \u00e9galement que ces attributs doivent \u00eatre configur\u00e9s pour \u00eatre utilis\u00e9s par OpenGL. C'est le r\u00f4le du VAO de stocker cette configuration.

    De la m\u00eame mani\u00e8re que pour le VBO, on demande \u00e0 OpenGL de nous allouer un VAO, puis on le lie au contexte actif. Ensuite on peut configurer les attributs du vertex et les lier au VBO.

    Gluint vao = 0;\nglGenVertexArrays(1, &vao);\nglBindVertexArray(vao);\n

    Comme notre triangle ne contient qu'une position en 3D, nous devons configurer l'attribut 0 pour qu'il pointe vers la position du vertex.

    glEnableVertexAttribArray(0);\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,\n    3 * sizeof(GLfloat), (GLvoid*)0);\n

    Ici on indique \u00e0 OpenGL que l'attribut 0 est un vecteur de 3 flottants, que les donn\u00e9es sont de type GL_FLOAT, que les donn\u00e9es ne sont pas normalis\u00e9es, que chaque vertex est s\u00e9par\u00e9 de 3 flottants et que le premier vertex commence \u00e0 l'indice 0.

    ", "tags": ["GL_FLOAT"]}, {"location": "course-c/47-gui/opengl/#shader", "title": "Shader", "text": "

    Un shader est un programme qui s'ex\u00e9cute sur la carte graphique. Il est \u00e9crit en langage GLSL (OpenGL Shading Language) pour OpenGL ou en HLSL (High-Level Shading Language) pour DirectX. Comme pour un programme en C, un shader doit \u00eatre compil\u00e9 avant d'\u00eatre ex\u00e9cut\u00e9 mais contrairement \u00e0 un programme en C il sera compil\u00e9 \u00e0 chaque ex\u00e9cution du programme. Cela permet de s'adapter \u00e0 la configuration mat\u00e9rielle de la carte graphique car l'architecture des cartes graphiques peut varier d'un PC \u00e0 l'autre.

    Le GLSL est tr\u00e8s similaire au C du point de vue de la syntaxe mais il est beaucoup plus limit\u00e9. Il n'y a pas de pointeurs, pas de structures, pas de fonctions r\u00e9cursives, pas de boucles infinies, pas de gestion de la m\u00e9moire, etc. En effet, les processeurs graphiques sont des processeurs sp\u00e9cialis\u00e9s qui n'ont pas besoin de ces fonctionnalit\u00e9s. Ils sont tr\u00e8s simples mais tr\u00e8s nombreux.

    Dans une carte graphique de type GTX 3090 sortie en 2020 et capable de faire tourner un jeu comme Cyberpunk 2077 en 4K, il y a 82 streaming multiprocessors ou SMs. Chaque SM contient 128 CUDA cores et donc la carte graphique contient 10496 CUDA cores. Chaque CUDA core peut ex\u00e9cuter diff\u00e9rent type de shaders\u2009: vertex, tessellation, geometry, fragment, etc. En outre, cette carte graphique tourne \u00e0 1.7 GHz, c'est \u00e0 dire que chaque CUDA core peut ex\u00e9cuter 1.7 milliard d'instructions par seconde.

    En d'autres termes, un shader GLSL de 10 instructions qui sera ex\u00e9cut\u00e9 pour chaque pixel d'un \u00e9cran de 3840x2160 pixels sera ex\u00e9cut\u00e9 en\u2009:

    \\[ \\frac{x \\times y}{\\text{cores}} \\times \\text{instructions} \\times \\frac{1}{\\text{freq}} \\frac{3840 \\times 2160}{10496} \\times 10 \\times \\frac{1}{1.7 \\times 10^9} = 4\\mu s \\]

    Le premier shader du pipeline graphique est le vertex shader. Il est ex\u00e9cut\u00e9 pour chaque vertex d'un objet g\u00e9om\u00e9trique. Il sera utilis\u00e9 pour transformer les coordonn\u00e9es des sommets du mod\u00e8le en coordonn\u00e9es de l'\u00e9cran car rappelez-vous que les coordonn\u00e9es des sommets sont normalis\u00e9es.

    Un shader de mani\u00e8re g\u00e9n\u00e9rique poss\u00e8de des entr\u00e9es et des sorties. Les entr\u00e9es sont les donn\u00e9es que le shader re\u00e7oit et les sorties sont les donn\u00e9es que le shader transmera \u00e0 l'\u00e9tape suivante du pipeline graphique. En plus des entr\u00e9es et des sorties, un shader peut avoir des uniformes. Une uniforme est une variable du programme principal qui est constante pour tous les vertex ou tous les fragments au m\u00eame instant de rendu. Une uniforme est typiquement utilis\u00e9e pour passer des matrices de transformation (rotation, translation, \u00e9chelle).

    Il faut \u00e9galement noter que le langage GLSL poss\u00e8de des types un peu diff\u00e9rents de C. Il existe des types de base comme int, float, vec2, vec3, vec4, mat2, mat3, mat4, etc. Il existe \u00e9galement des types de structures et des types de tableaux. Un vec3 est un vecteur de 3 flottants, un mat4 est une matrice de 4x4 flottants.

    Enfin, le langage GLSL \u00e0 beaucoup \u00e9volu\u00e9 depuis sa premi\u00e8re version en 2004. La mani\u00e8re de transmettre les entr\u00e9es et les sorties d'un shader a beaucoup chang\u00e9. Il est maintenant possible de d\u00e9clarer les entr\u00e9es et les sorties d'un shader avec des mots-cl\u00e9s comme in, out, uniform, layout, etc.

    Le vertex shader le plus typique que l'on puisse \u00e9crire est le suivant\u2009:

    #version 330 core\n\nlayout(location = 0) in vec3 position;\n\nvoid main() {\n   gl_Position = vec4(position, 1.0);\n}\n

    Ce shader prend en entr\u00e9e un vecteur de 3 flottants position issue de l'attribut 0 du vertex. Il transforme ce vecteur en un vecteur de 4 flottants gl_Position qui est la position du vertex dans l'espace de l'\u00e9cran. La quatri\u00e8me coordonn\u00e9e est li\u00e9e \u00e0 l'utlisation de coordonn\u00e9es homog\u00e8nes dans les transformations grpahiques en 3D. Cette quatri\u00e8me coordonn\u00e9e nomm\u00e9e w a un r\u00f4le crucial dans les transformations g\u00e9om\u00e9triques. En math\u00e9matiques et en infographie, les coordonn\u00e9es homog\u00e8nes permettent d'utiliser des transformations affines (comme la translation, la rotation, l'\u00e9chelle) et des transformations projectives (comme la projection en perspective) de mani\u00e8re plus simple et uniforme. Lors d'une transformation projective, la quatri\u00e8me coordonn\u00e9e est utilis\u00e9e pour d\u00e9terminer la profondeur du vertex dans l'espace 3D. Elle permet de simuler l'effet de rapetissement des objets \u00e9loign\u00e9s dans une sc\u00e8ne 3D. Pour une sc\u00e8ne en 2D ou une sc\u00e8ne en 3D sans perspective, la quatri\u00e8me coordonn\u00e9e est \u00e9gale \u00e0 1.0f.

    Le deuxi\u00e8me type de shader dont nous aurons besoin est nomm\u00e9 fragment shader. Un fragment est un \u00e9l\u00e9ment de base d'un objet g\u00e9om\u00e9trique qui contient une information de couleur mais aussi de la position sur l'\u00e9cran, la coordonn\u00e9e d'une texture, des valeurs d'interpolation ou m\u00eame une information de profondeur en \\(z\\). Chaque fragment repr\u00e9sente une peitite portion d'une primitive (triangle, carr\u00e9, cercle, etc.) souvent associ\u00e9e \u00e0 un pixel de l'\u00e9cran. Le fragment shader le plus simple que l'on puisse \u00e9crire est le suivant\u2009:

    #version 330 core\n\nout vec4 FragColor;\n\nvoid main() {\n   FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n}\n

    Pour chaque fragment, on retourne la couleur \\([1.0, 1.0, 1.0, 1.0]\\) qui est le blanc. Donc chaque pixel contenu dans la primitive dessin\u00e9e sera blanc. Il conviendra d'avoir du noir pour la couleur de fond de la fen\u00eatre pour voir notre triangle.

    Maintenant que nous avons nos deux shaders, il est temps de les compiler et de les lier \u00e0 un programme OpenGL. Un programme OpenGL est un ensemble de shaders qui sont li\u00e9s ensemble pour former un programme graphique. Un programme graphique est un ensemble de shaders qui sont ex\u00e9cut\u00e9s \u00e0 chaque image pour dessiner des primitives.

    Si nous reprenons notre triangle pr\u00e9c\u00e9dent, le vertex shader transformera les coordonn\u00e9es du triangle en coordonn\u00e9es de l'\u00e9cran sans les d\u00e9former et le fragment shader colorera chaque pixel du triangle en blanc.

    Tout d'abord chaque shader est stock\u00e9 sous forme d'une cha\u00eene de caract\u00e8res, puis chaque shader est compil\u00e9 et enfin les shaders sont li\u00e9s ensemble pour former un programme graphique\u2009:

    const char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = vec4(position, 1.0);\\n\"\n    \"}\\n\";\n\nconst char* fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"out vec4 FragColor;\\n\"\n    \"void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); }\\n\";\n\nGLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);\nglShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\nglCompileShader(vertexShader);\n\nGLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\nglShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\nglCompileShader(fragmentShader);\n\nGLuint shaderProgram = glCreateProgram();\nglAttachShader(shaderProgram, vertexShader);\nglAttachShader(shaderProgram, fragmentShader);\nglLinkProgram(shaderProgram);\nglUseProgram(shaderProgram);\n\nglDeleteShader(vertexShader);\nglDeleteShader(fragmentShader);\n

    Notez que cette m\u00e9thode est tr\u00e8s basique et ne g\u00e8re pas les erreurs de compilation ou de liaison des shaders, nous verrons plus tard un exemple plus complet.

    ", "tags": ["position", "gl_Position", "vec2", "mat4", "mat3", "out", "uniform", "vec4", "mat2", "float", "vec3", "int", "layout"]}, {"location": "course-c/47-gui/opengl/#rendu", "title": "Rendu", "text": "

    Si vous \u00eates arriv\u00e9 jusqu'ici, vous avez compris que nous utilisons GLFW pour cr\u00e9er une fen\u00eatre graphique et GLEW pour charger les extensions OpenGL. Nous avons compris la notion de double buffer et de coordonn\u00e9es normalis\u00e9es avec la r\u00e8gle de la main droite. Les notions de VBO et de VAO n'ont plus de secret pour vous et vous avez compris l'utilit\u00e9 d'un vertex shader et d'un fragment shader.

    Il est temps de dessiner notre triangle.

    #include <GL/glew.h>\n#include <GLFW/glfw3.h>\n#include <stdbool.h>\n#include <stdio.h>\n\nstatic const GLfloat vertices[] = {\n    -1.0f, -1.0f, 0.0f,  // Bottom left\n    1.0f,  -1.0f, 0.0f,  // Bottom right\n    0.0f,  1.0f,  0.0f,  // Top\n};\n\nconst char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = vec4(position, 1.0);\\n\"\n    \"}\\n\";\n\nconst char* fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"out vec4 FragColor;\\n\"\n    \"void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); }\\n\";\n\nint main() {\n   // Initialise GLFW\n   if (!glfwInit()) {\n      fprintf(stderr, \"Failed to initialize GLFW\\n\");\n      return -1;\n   }\n   glfwWindowHint(GLFW_SAMPLES, 4);                // 4x antialiasing\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // We want OpenGL 3.3\n   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n   glfwWindowHint(GLFW_OPENGL_PROFILE,\n                  GLFW_OPENGL_CORE_PROFILE);  // We don't want the old OpenGL\n\n   // Open a window and create its OpenGL context\n   GLFWwindow* window =\n       glfwCreateWindow(800, 400, \"White Triangle\", NULL, NULL);\n   if (window == NULL) {\n      fprintf(stderr, \"Error: Failed to open GLFW window.\\n\");\n      glfwTerminate();\n      return -1;\n   }\n   glfwMakeContextCurrent(window);\n   if (glewInit() != GLEW_OK) {\n      fprintf(stderr, \"Error: Failed to initialize GLEW\\n\");\n      return -1;\n   }\n   glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);\n\n   // Compile and link shaders\n   unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);\n   glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\n   glCompileShader(vertexShader);\n\n   unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\n   glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\n   glCompileShader(fragmentShader);\n\n   unsigned int shaderProgram = glCreateProgram();\n   glAttachShader(shaderProgram, vertexShader);\n   glAttachShader(shaderProgram, fragmentShader);\n   glLinkProgram(shaderProgram);\n   glUseProgram(shaderProgram);\n\n   glDeleteShader(vertexShader);\n   glDeleteShader(fragmentShader);\n\n   // Setup VAO/VBO\n   unsigned int VBO = 0, VAO = 0;\n   glGenVertexArrays(1, &VAO);\n   glGenBuffers(1, &VBO);\n   glBindVertexArray(VAO);\n\n   glBindBuffer(GL_ARRAY_BUFFER, VBO);\n   glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\n   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\n   glEnableVertexAttribArray(0);\n\n   //  Main loop\n   do {\n      glClear(GL_COLOR_BUFFER_BIT);\n      glDrawArrays(GL_TRIANGLES, 0, 3);\n      glfwSwapBuffers(window);\n      glfwPollEvents();\n   } while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&\n            glfwWindowShouldClose(window) == 0);\n}\n

    Apr\u00e8s compilation avec la commande suivante, on obtient un triangle blanc comme illustr\u00e9 ci-dessous

    gcc -o triangle triangle.c -lglfw -lGLEW -lGL -lm\n

    Triangle Blanc

    ", "tags": ["GLEW", "GLFW"]}, {"location": "course-c/47-gui/opengl/#matrices", "title": "Matrices", "text": "

    Le risque avec l'OpenGL bas niveau c'est qu'\u00e0 un moment donn\u00e9 on se retrouve \u00e0 devoir g\u00e9rer des matrices de transformation et donc de faire des maths. Si nous voulons rendre notre exemple plus interfactif, nous pourrions par exemple ajouter une rotation au triangle, d'abord dans le plan \\(xy\\) puis plus tard en 3D.

    Pour m\u00e9moire, en math\u00e9mathique, un vecteur est une matrice de \\(n \\times 1\\) soit avec une seule colonne, tandiqu'un point est une matrice de \\(1 \\times n\\) soit avec une seule ligne. La matrice qui n'a pas d'effet sur un vecteur est la matrice identit\u00e9. La matrice identit\u00e9 est une matrice carr\u00e9e de taille \\(n \\times n\\) avec des \\(1\\) sur la diagonale principale et des \\(0\\) ailleurs. La matrice identit\u00e9 est not\u00e9e \\(I_n\\). En 3D nous avons donc\u2009:

    \\[ I_3 = \\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 1 \\end{bmatrix} \\]

    Une matrice de rotation en 3D est une matrice qui permet de faire tourner un vecteur autour d'un axe. Il existe trois types de rotation en 3D\u2009: la rotation autour de l'axe \\(x\\), la rotation autour de l'axe \\(y\\) et la rotation autour de l'axe \\(z\\). La rotation autour de l'axe \\(x\\) est donn\u00e9e par la matrice\u2009:

    \\[ R_x(\\theta) = \\begin{bmatrix} 1 & 0 & 0 \\\\ 0 & \\cos(\\theta) & -\\sin(\\theta) \\\\ 0 & \\sin(\\theta) & \\cos(\\theta) \\end{bmatrix} \\]

    Ces deux matrices sont des matrices 3x3. Dans OpenGL on utilise des coordonn\u00e9es homog\u00e8nes pour les transformations g\u00e9om\u00e9triques. Une matrice de transformation homog\u00e8ne est une matrice 4x4 qui permet de faire des transformations affines et projectives en 3D. La matrice de rotation peut \u00eatre r\u00e9\u00e9crite de la forme\u2009:

    \\[ R_x(\\theta) = \\begin{bmatrix} 1 & 0 & 0 & 0 \\\\ 0 & \\cos(\\theta) & -\\sin(\\theta) & 0 \\\\ 0 & \\sin(\\theta) & \\cos(\\theta) & 0 \\\\ 0 & 0 & 0 & 1 \\end{bmatrix} \\]

    On peut facilement calculer une matrice de rotation en impl\u00e9mentant une fonction en C\u2009:

    void identityMatrix(GLfloat matrix[16]) {\n   for (int i = 0; i < 16; i++) matrix[i] = 0;\n   matrix[0] = 1;\n   matrix[5] = 1;\n   matrix[10] = 1;\n   matrix[15] = 1;\n}\n\nvoid rotationMatrix(GLfloat matrix[16], GLfloat angle, GLfloat x, GLfloat y, GLfloat z) {\n   GLfloat c = cos(angle);\n   GLfloat s = sin(angle);\n   GLfloat t = 1 - c;\n   matrix[0] = x * x * t + c;\n   matrix[1] = x * y * t - z * s;\n   matrix[2] = x * z * t + y * s;\n   matrix[3] = 0;\n   matrix[4] = y * x * t + z * s;\n   matrix[5] = y * y * t + c;\n   matrix[6] = y * z * t - x * s;\n   matrix[7] = 0;\n   matrix[8] = x * z * t - y * s;\n   matrix[9] = y * z * t + x * s;\n   matrix[10] = z * z * t + c;\n   matrix[11] = 0;\n   matrix[12] = 0;\n   matrix[13] = 0;\n   matrix[14] = 0;\n   matrix[15] = 1;\n}\n

    Une mani\u00e8re plus simple est de faire appel \u00e0 une biblioth\u00e8que de math\u00e9matiques comme CGLM que vous pouvez installer avec la commande suivante\u2009:

    sudo apt-get install libcglm-dev\n
    ", "tags": ["CGLM"]}, {"location": "course-c/47-gui/opengl/#model-view-projection", "title": "Model View Projection", "text": "

    La matrice de transformation homog\u00e8ne la plus courante est la matrice de transformation Model View Projection ou MVP. La matrice Model est la matrice qui transforme les coordonn\u00e9es du mod\u00e8le en coordonn\u00e9es du monde. La matrice View est la matrice qui transforme les coordonn\u00e9es du monde en coordonn\u00e9es de la cam\u00e9ra. La matrice Projection est la matrice qui transforme les coordonn\u00e9es de la cam\u00e9ra en coordonn\u00e9es de l'\u00e9cran.

    Souvenez-vous dans Futurama lorsque Cubert Farnsworth r\u00e9alise comment fonctionne les moteurs du vaisseau spatial de son p\u00e8re. Il dit\u2009:

    I understand how the engines work now. It came to me in a dream. The engines don't move the ship at all. The ship stays where it is and the engines move the universe around it.

    En fran\u00e7ais

    J'ai compris comment fonctionnent les moteurs maintenant. C'est venu \u00e0 moi dans un r\u00eave. Les moteurs ne d\u00e9placent pas le vaisseau du tout. Le vaisseau reste o\u00f9 il est et les moteurs d\u00e9placent l'univers autour de lui.

    En OpenGL il n'y pas de cam\u00e9ra qui se d\u00e9place, de longueur focale ou de distance de vue. C'est l'univers qui se d\u00e9place autour de la cam\u00e9ra. Cette derni\u00e8re reste fixe avec ses coordonn\u00e9es \\((0, 0, 0)\\) et son orientation \\((0, 0, -1)\\).

    "}, {"location": "course-c/47-gui/opengl/#transformation", "title": "Transformation", "text": "

    Ce n'est pas tr\u00e8s joli d'avoir chaque sommet du triangle coll\u00e9 au bord de la fen\u00eatre. Nous allons donc d\u00e9placer le triangle au centre de la fen\u00eatre. Pour cela nous allons utiliser une matrice de transformation. On peut d\u00e9finir par exemple que le triangle fera 70% de la hauteur de la fen\u00eatre et son rapport largeur/hauteur sera de 1.0. Vous l'avez compris on utilisera une matrice de transformation homog\u00e8ne qui sera appliqu\u00e9e par le vertex shader \u00e0 chaque sommet du triangle.

    Un autre probl\u00e8me est que les dimensions de la fen\u00eatre viewport peuvent changer si l'utilisateur d\u00e9cide de la redimensionner. Or, comme les coordonn\u00e9es des sommets du triangle sont normalis\u00e9es, dans le cas d'une fen\u00eatre deux fois plus large que haute. \\(1.0\\) en \\(x\\) sera deux fois plus grand que \\(1.0\\) en \\(y\\).

    Pour \u00e9viter cela, une mani\u00e8re est d'utiliser une matrice de projection orthographique homog\u00e8ne d\u00e9finie comme\u2009:

    \\[ \\begin{bmatrix} \\frac{2}{width} & 0 & 0 & 0 \\\\ 0 & \\frac{2}{height} & 0 & 0 \\\\ 0 & 0 & -1 & 0 \\\\ -1 & -1 & 0 & 1 \\end{bmatrix} \\]

    Pour se faire on aura besoin de modifier le vertex shader pour qu'il prenne en compte une matrice de transformation\u2009:

    const char* vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 position;\\n\"\n    \"uniform mat4 model;\\n\"\n    \"void main() {\\n\"\n    \"    gl_Position = model * vec4(position, 1.0);\\n\"\n    \"}\\n\";\n
    "}, {"location": "course-c/47-gui/sdl/", "title": "SDL", "text": "

    SDL (Simple DirectMedia Layer) est une biblioth\u00e8que multiplateforme qui permet de cr\u00e9er des applications graphiques. Elle est utilis\u00e9e dans de nombreux jeux vid\u00e9o et applications multim\u00e9dia. Elle est \u00e9crite en C, mais des bindings existent pour de nombreux langages, dont Python ou C++.

    La biblith\u00e8que a \u00e9t\u00e9 cr\u00e9\u00e9e en 1998 par Sam Lantinga, un d\u00e9veloppeur alors employ\u00e9 par Loki Software, une soci\u00e9t\u00e9 sp\u00e9cialis\u00e9e dans le portage de jeux vid\u00e9o sur Linux. \u00c0 l\u2019\u00e9poque, il \u00e9tait difficile pour les d\u00e9veloppeurs de cr\u00e9er des jeux multiplateformes, car chaque syst\u00e8me d'exploitation (Windows, Linux, macOS) utilisait des biblioth\u00e8ques et des API diff\u00e9rentes pour g\u00e9rer les graphismes, le son et les entr\u00e9es. SDL a \u00e9t\u00e9 d\u00e9velopp\u00e9e pour offrir une interface unique et simple permettant d'acc\u00e9der \u00e0 ces fonctionnalit\u00e9s sur plusieurs plateformes, facilitant ainsi le d\u00e9veloppement de jeux et d'applications multim\u00e9dias multiplateformes.

    On peut citer quelques projets qui utilisent SDL.

    • The Battle for Wesnoth
    • Faster Than Light
    • Doom 3
    • ScummVM

    La plupart de ces projets sont d\u00e9velopp\u00e9s en C++ car le C n'est pas un langage tr\u00e8s adapt\u00e9 pour le d\u00e9veloppement d'applications complexes et graphiques.

    La biblioth\u00e8que est plus qu'une simple abstraction des fonctionnalit\u00e9s graphiques des syst\u00e8mes d'exploitation. Elle offre \u00e9galement des fonctionnalit\u00e9s notament\u2009:

    • Gestion des fen\u00eatres
    • Gestion des \u00e9v\u00e9nements (clavier, souris, joystick)
    • Gestion du son
    • Multi-threading
    • Timers
    • Acc\u00e9l\u00e9ration 2D
    • Support 3D (Vulkan, Metal, OpenGL)

    La biblioth\u00e8que est plus compl\u00e8te que GLFW et est bien adapt\u00e9e \u00e0 des projets complexes en C.

    "}, {"location": "course-c/47-gui/sdl/#premiers-pas", "title": "Premiers pas", "text": "

    Pour installer SDL sur votre syst\u00e8me, vous pouvez utiliser le gestionnaire de paquets de votre distribution. Sous Ubuntu\u2009:

    sudo apt install libsdl2-dev \"libsdl2-*\"\n

    Une extension de SDL nomm\u00e9e GFX est \u00e9galement disponible. Elle ajoute des fonctionnalit\u00e9s graphiques suppl\u00e9mentaires comme des fonctions de dessin de primitives (lignes, rectangles, cercles, etc.).

    #include <SDL2/SDL.h>\n#include <SDL2/SDL2_gfxPrimitives.h>\n#include <stdbool.h>\n#include <stdio.h>\n\n#define S(i) (sin(2 * M_PI / 3 * i))\n#define C(i) (cos(2 * M_PI / 3 * i))\n\nint main(int argc, char *argv[]) {\n   if (SDL_Init(SDL_INIT_VIDEO) < 0) {\n      fprintf(stderr, \"Error: SDL Initialization: %s\\n\", SDL_GetError());\n      exit(1);\n   }\n   SDL_Window *window =\n       SDL_CreateWindow(\"Circles\", SDL_WINDOWPOS_CENTERED,\n                        SDL_WINDOWPOS_CENTERED, 800, 400, SDL_WINDOW_SHOWN);\n   if (!window) {\n      fprintf(stderr, \"Error: CreateWindow: %s\\n\", SDL_GetError());\n      exit(2);\n   }\n   SDL_Renderer *r = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n   if (!r) {\n      fprintf(stderr, \"Error: Renderer: %s\\n\", SDL_GetError());\n      exit(3);\n   }\n   SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_ADD);\n   bool running = true;\n   while (running) {\n      SDL_Event event;\n      while (SDL_PollEvent(&event))\n         if (event.type == SDL_QUIT) running = false;\n      SDL_SetRenderDrawColor(r, 255, 255, 255, 255);\n      SDL_RenderClear(r);\n      Sint16 cx = 400, cy = 200, d = 50;\n      filledCircleRGBA(r, cx + d * C(1), cy + d * S(1), 100, 255, 0, 0, 100);\n      filledCircleRGBA(r, cx + d * C(2), cy + d * S(2), 100, 0, 255, 0, 100);\n      filledCircleRGBA(r, cx + d * C(3), cy + d * S(3), 100, 0, 0, 255, 100);\n      SDL_RenderPresent(r);\n   }\n   SDL_DestroyRenderer(r);\n   SDL_DestroyWindow(window);\n   SDL_Quit();\n}\n

    Intersections de cercles

    "}, {"location": "course-c/47-gui/sdl/#polygones", "title": "Polygones", "text": "

    Voici un exemple d'un programme de dessin de polygones en utilisant SDL.

    Programme de dessin de polygones

    #include <SDL2/SDL.h>\n#include <SDL2/SDL2_gfxPrimitives.h>  // Inclure SDL2_gfx\n#include <math.h>\n#include <stdbool.h>\n#include <stdio.h>\n\n#define GRIDSTEP 10\n#define MAX_POINTS 100\nconst int WINDOW_WIDTH = 800, WINDOW_HEIGHT = 400;\n\nconst SDL_Color GRID_COLOR = {80, 80, 80, 200};\nconst SDL_Color BACKGROUND_COLOR = {30, 30, 30, 255};\nconst SDL_Color LINE_COLOR = {255, 0, 128, 255};\nconst SDL_Color ANCHOR_COLOR = {0, 255, 255, 150};\n\ntypedef struct Point {\n   float x, y;\n} Point;\n\ntypedef struct Segment {\n   Point p, q;\n} Segment;\n\ntypedef struct State {\n   bool drawing;\n   Point start, last, current;\n   size_t segment_count;\n   Segment segments[MAX_POINTS];\n} State;\n\n#define COLORARGS(color) color.r, color.g, color.b, color.a\n\nvoid clear(SDL_Renderer *renderer) {\n   SDL_SetRenderDrawColor(renderer, COLORARGS(BACKGROUND_COLOR));\n   SDL_RenderClear(renderer);\n}\n\nvoid drawGrid(SDL_Renderer *renderer) {\n   for (int x = 0; x < WINDOW_WIDTH; x += GRIDSTEP) {\n      SDL_Color color = GRID_COLOR;\n      color.a = x % 100 ? 100 : 200;\n      SDL_SetRenderDrawColor(renderer, COLORARGS(color));\n      SDL_RenderDrawLine(renderer, x, 0, x, WINDOW_HEIGHT);\n   }\n   for (int y = 0; y < WINDOW_HEIGHT; y += GRIDSTEP) {\n      SDL_Color color = GRID_COLOR;\n      color.a = y % 100 ? 100 : 200;\n      SDL_SetRenderDrawColor(renderer, COLORARGS(color));\n      SDL_RenderDrawLine(renderer, 0, y, WINDOW_WIDTH, y);\n   }\n}\n\nvoid drawAnchor(SDL_Renderer *r, Point p, SDL_Color color) {\n   const int size = 6, half = size / 2;\n   SDL_SetRenderDrawColor(r, COLORARGS(color));\n   SDL_RenderFillRect(r, &(SDL_Rect){p.x - half, p.y - half, size, size});\n}\n\nvoid drawSegment(SDL_Renderer *r, Segment s) {\n   SDL_SetRenderDrawColor(r, COLORARGS(LINE_COLOR));\n   SDL_RenderDrawLine(r, s.p.x, s.p.y, s.q.x, s.q.y);\n   drawAnchor(r, s.p, ANCHOR_COLOR);\n   drawAnchor(r, s.q, ANCHOR_COLOR);\n}\n\nvoid drawSegments(SDL_Renderer *renderer, Segment *segments, size_t count) {\n   for (int i = 0; i < count; i++) drawSegment(renderer, segments[i]);\n}\n\nvoid addSegment(State *state) {\n   if (state->segment_count >= MAX_POINTS) {\n      fprintf(stderr, \"Reached maximum number of points\\n\");\n      return;\n   }\n   state->segments[state->segment_count++] =\n       (Segment){.p = state->last, .q = state->current};\n}\n\nvoid endSegment(State *state) {\n   state->drawing = false;\n   addSegment(state);\n}\n\nbool isClose(Point p, Point q, int threshold) {\n   return (fabs(p.x - q.x) < threshold) && (fabs(p.y - q.y) < threshold);\n}\n\nbool onStartAnchor(State state) {\n   return isClose(state.start, state.current, 10);\n}\n\nint main(int argc, char *argv[]) {\n   SDL_Init(SDL_INIT_VIDEO);\n   SDL_Window *window = SDL_CreateWindow(\"Polygons\", SDL_WINDOWPOS_CENTERED,\n                                         SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH,\n                                         WINDOW_HEIGHT, SDL_WINDOW_SHOWN);\n   SDL_Renderer *renderer =\n       SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);\n\n   SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);\n   State state = {0};\n   bool running = true;\n   while (running) {\n      SDL_Event event;\n      while (SDL_PollEvent(&event)) {\n         if (event.type == SDL_QUIT) {\n            running = false;\n            continue;\n         }\n         if (event.type == SDL_MOUSEMOTION) {\n            state.current = (Point){event.button.x, event.button.y};\n            continue;\n         }\n         if (event.type == SDL_MOUSEBUTTONDOWN &&\n             event.button.button == SDL_BUTTON_LEFT) {\n            if (!state.drawing) {\n               state.drawing = true;\n               state.start = state.current;\n            } else if (onStartAnchor(state)) {\n               state.current = state.start;\n               endSegment(&state);\n            } else {\n               addSegment(&state);\n            }\n            state.last = state.current;\n         }\n      }\n      clear(renderer);\n      drawGrid(renderer);\n      drawSegments(renderer, state.segments, state.segment_count);\n      if (state.start.x >= 0 && state.start.y >= 0)\n         drawAnchor(renderer, state.start, ANCHOR_COLOR);\n      if (state.drawing)\n         drawSegment(renderer, (Segment){.p = state.last, .q = state.current});\n      SDL_RenderPresent(renderer);\n   }\n   SDL_DestroyRenderer(renderer);\n   SDL_DestroyWindow(window);\n   SDL_Quit();\n}\n
    "}, {"location": "course-c/47-gui/window/", "title": "Fen\u00eatre graphique", "text": "

    Comme nous l'avons vu, un programme informatique standard dispose de plusieurs flux\u2009: un flux d'entr\u00e9e (stdin) et deux flux de sortie (stdout et stderr). Ces flux sont g\u00e9n\u00e9ralement associ\u00e9s \u00e0 la console texte, mais ne sont pas directement reli\u00e9s \u00e0 une fen\u00eatre graphique permettant des interactions visuelles. Pourtant, de nombreuses applications modernes proposent une interface graphique pour permettre une meilleure interaction avec l'utilisateur.

    Sous Windows comme sous Linux, l'interface utilisateur graphique, compos\u00e9e de fen\u00eatres, de menus et d'autres \u00e9l\u00e9ments interactifs, est g\u00e9r\u00e9e par ce que l'on appelle un window manager. Ce gestionnaire de fen\u00eatres est responsable de la cr\u00e9ation, de la gestion et de la disposition des diff\u00e9rentes fen\u00eatres \u00e0 l'\u00e9cran. Il agit comme une couche interm\u00e9diaire entre le noyau du syst\u00e8me d'exploitation et les applications graphiques.

    Le window manager est un processus distinct qui fonctionne en parall\u00e8le de votre programme. Il est donc essentiel, d'une part, qu'il soit d\u00e9j\u00e0 lanc\u00e9 avant l'ex\u00e9cution de votre application (ce qui est g\u00e9n\u00e9ralement le cas dans un environnement de bureau standard) et, d'autre part, que votre programme soit capable de communiquer avec ce gestionnaire de mani\u00e8re appropri\u00e9e. Sous Linux, cette communication se fait le plus souvent par l'interm\u00e9diaire le protocole X11 ou plus r\u00e9emment Wayland, \u00e9galement connu sous le nom de \u00ab\u2009X Window System\u2009\u00bb. Sous Windows c'est l'API Win32 qui est utilis\u00e9e mais des alternatives comme GTK ou Qt sont \u00e9galement disponibles via MinGW ou MSYS.

    ", "tags": ["stdin", "stdout", "stderr"]}, {"location": "course-c/47-gui/window/#le-protocole-x11", "title": "Le protocole X11", "text": "

    X11 est un protocole r\u00e9seau qui permet \u00e0 un programme de dessiner des fen\u00eatres sur l'\u00e9cran, qu'il soit local ou distant. Il a la particularit\u00e9 d'\u00eatre ind\u00e9pendant de la machine o\u00f9 l'application s'ex\u00e9cute, permettant ainsi de contr\u00f4ler graphiquement des applications sur d'autres ordinateurs. Cependant, interagir directement avec X11 est un processus complexe, car cela implique de g\u00e9rer de nombreux param\u00e8tres relatifs aux fen\u00eatres\u2009: dimensions, position, gestion des \u00e9v\u00e9nements, et autres aspects li\u00e9s \u00e0 l'interaction utilisateur.

    Le protocole X11 est bas\u00e9 sur un mod\u00e8le client-serveur. Un programme se connecte donc au serveur en utilisant des sockets, typiquement des sockets Unix (Unix Domain Socket). Ce socket est un canal de communication avec le serveur dans lequel des ordre du type \u00ab\u2009dessine-moi un rectangle \u00e0 telle position\u2009\u00bb ou \u00ab\u2009affiche ce texte\u2009\u00bb sont envoy\u00e9s. Le serveur X11 est responsable de la gestion des fen\u00eatres, des \u00e9v\u00e9nements, des polices, des couleurs, etc.

    Comme la communication passe par les sockets, il est possible de profiter de l'encapsulation TCP/IP et de dialoguer avec un server distant via internet ou un r\u00e9seau local. Avant l'av\u00e8nement WSLg (Windows Subsystem for Linux GUI), il \u00e9tait possible d'afficher des fen\u00eatres graphiques Linux en utilisant un serveur X11 install\u00e9 directement sous Windows comme VcXsrv. D'ailleurs si vous utilisez Docker, ou que vous vous connectez \u00e0 un Raspberry Pi, vous pouvez \u00e9galement profiter d'une interface graphique depuis une connection SSH par exemple (via l'option -X).

    Pour illustrer la complexit\u00e9 de l'usage direct de X11, voici un exemple de programme en C qui ouvre une simple fen\u00eatre de 100 pixels par 100 pixels\u2009:

    #include <X11/Xlib.h>\n#include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n   Display *d = XOpenDisplay(NULL);\n   if (d == NULL) {\n      fprintf(stderr, \"Impossible d'ouvrir le display\\n\");\n      exit(1);\n   }\n\n   // Create window\n   int s = DefaultScreen(d);\n   Window w = XCreateSimpleWindow(d, RootWindow(d, s), 10, 10,  // x, y\n                                  100, 100,                     // width, height\n                                  1,                            // border width\n                                  BlackPixel(d, s), WhitePixel(d, s));\n\n   XSelectInput(d, w, ExposureMask | KeyPressMask);  // Select event to watch\n   XMapWindow(d, w);                                 // Display the window\n\n   // Event loop\n   XEvent e;\n   do {\n      XNextEvent(d, &e);\n      if (e.type == Expose)\n         XFillRectangle(d, w, DefaultGC(d, s), 20, 20, 10, 10);\n   } while (e.type != KeyPress);\n\n   XCloseDisplay(d);\n}\n

    Pour compiler et ex\u00e9cuter ce programme vous aurez besoin de la biblioth\u00e8que X11. Sous Ubuntu, vous pouvez l'installer avec la commande suivante\u2009:

    sudo apt-get install libx11-dev\n

    Ensuite, vous pouvez compiler le programme avec la commande suivante\u2009:

    gcc window.c -lX11\n

    Le programme ainsi g\u00e9n\u00e9r\u00e9 ouvre une fen\u00eatre minimaliste, mais il est \u00e9vident qu'impl\u00e9menter des interfaces graphiques sophistiqu\u00e9es en utilisant uniquement X11 est un travail consid\u00e9rable. La gestion manuelle des \u00e9v\u00e9nements, des composants graphiques et des interactions utilisateurs devient rapidement fastidieuse. D'autre part ce protocol n'est pas portable, il est sp\u00e9cifique \u00e0 Linux et n'est pas disponible sur d'autres syst\u00e8mes d'exploitation.

    "}, {"location": "course-c/47-gui/window/#wayland", "title": "Wayland", "text": "

    Wayland est un protocole graphique plus r\u00e9cent que X11, con\u00e7u pour remplacer ce dernier en tant que gestionnaire de fen\u00eatres par d\u00e9faut sous Linux. Wayland est plus moderne, plus s\u00e9curis\u00e9 et plus performant que X11, mais il est \u00e9galement plus restrictif en termes de fonctionnalit\u00e9s. Wayland est con\u00e7u pour \u00eatre plus simple en limitant l'acc\u00e8s des applications aux ressources syst\u00e8me et en isolant les processus les uns des autres.

    "}, {"location": "course-c/47-gui/window/#lapi-win32", "title": "L'API Win32", "text": "

    Sous Windows, l'\u00e9quivalent de X11 est l'API Win32, qui permet de cr\u00e9er des applications graphiques pour le syst\u00e8me d'exploitation de Microsoft. L'API Win32 est une interface de programmation d'applications (API) bas\u00e9e sur des messages, qui permet de cr\u00e9er des fen\u00eatres, des contr\u00f4les, des menus, et d'autres \u00e9l\u00e9ments d'interface utilisateur. L'API Win32 est plus complexe que X11, mais elle offre des fonctionnalit\u00e9s plus avanc\u00e9es et une meilleure int\u00e9gration avec le syst\u00e8me d'exploitation. Voici \u00e0 titre d'exemple le m\u00eame programme que pr\u00e9c\u00e9demment, mais cette fois-ci en utilisant l'API Win32\u2009:

    #include <windows.h>\n\nLRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wParam,\n                                 LPARAM lParam) {\n   switch (msg) {\n      case WM_DESTROY:\n         PostQuitMessage(0);\n         break;\n      default:\n         return DefWindowProc(hwnd, msg, wParam, lParam);\n   }\n   return 0;\n}\n\nint WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,\n                   LPSTR lpCmdLine, int nCmdShow) {\n   // D\u00e9finition de la classe de fen\u00eatre\n   WNDCLASS wc = {0};\n   wc.lpfnWndProc = WindowProcedure;  // Fonction callback pour les messages\n   wc.hInstance = hInstance;\n   wc.lpszClassName = \"MaFenetreClass\";\n\n   if (!RegisterClass(&wc)) {\n      return -1;\n   }\n\n   // Cr\u00e9ation de la fen\u00eatre\n   HWND hwnd = CreateWindowEx(0, \"MaFenetreClass\", \"Ma Fenetre\",\n                              WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,\n                              100, 100, NULL, NULL, hInstance, NULL);\n\n   if (hwnd == NULL) {\n      return -1;\n   }\n\n   ShowWindow(hwnd, nCmdShow);\n   UpdateWindow(hwnd);\n\n   // Event loop\n   MSG msg = {0};\n   while (GetMessage(&msg, NULL, 0, 0)) {\n      TranslateMessage(&msg);\n      DispatchMessage(&msg);\n   }\n\n   return msg.wParam;\n}\n

    Pour compiler ce programme, vous pouvez utiliser le compilateur MinGW (Minimalist GNU for Windows) qui fournit les outils n\u00e9cessaires pour compiler des programmes C sous Windows. Vous pouvez installer MinGW via le gestionnaire de paquets MSYS2, qui fournit un environnement de d\u00e9veloppement similaire \u00e0 celui de Linux.

    gcc window.c -lgdi32\n

    Alternativement, vous pouvez utiliser l'environnement officiel de d\u00e9veloppement de Microsoft Visual Studio, qui fournit un ensemble complet d'outils pour le d\u00e9veloppement d'applications Windows mais son installation est plus lourde et d\u00e9passe le cadre de ce cours.

    "}, {"location": "course-c/47-gui/window/#gtk", "title": "GTK", "text": "

    GUI avec GTK

    "}, {"location": "course-c/47-gui/window/#rendu-logiciel-ou-materiel", "title": "Rendu logiciel ou mat\u00e9riel", "text": "

    Les premiers ordinateurs personnels, comme le Macintosh d'Apple ou l'Amiga d'Atari, utilisaient un rendu graphique purement logiciel. Cela signifie que le processeur central (CPU) \u00e9tait responsable de dessiner les pixels \u00e0 l'\u00e9cran, en calculant les couleurs, les textures, les ombres, et autres effets visuels. Historiquement avant les syst\u00e8mes d'exploitation multit\u00e2ches, pour afficher un pixel \u00e0 l'\u00e9cran, il suffisait ou presque d'\u00e9crire une valeur dans la m\u00e9moire vid\u00e9o.

    Depuis ces ages imm\u00e9moriaux, les syst\u00e8mes d'exploitations sont devenus beaucoup plus restrictifs et n'autorisent plus l'acc\u00e8s direct \u00e0 la m\u00e9moire vid\u00e9o. D'ailleurs les cartes graphiques modernes fonctionnent tr\u00e8s diff\u00e9remment. N\u00e9anmoins beaucoup de biblioth\u00e8ques graphiques de haut niveau comme GTK, Qt, SDL ou Allegro peuvent encore utiliser un rendu logiciel pour dessiner certains \u00e9l\u00e9ments graphiques \u00e0 l'\u00e9cran. Plut\u00f4t qu'acc\u00e9der \u00e0 la m\u00e9moire vid\u00e9o ces biblioth\u00e8ques utilisent un framebuffer. Il s'agit d'une zone de m\u00e9moire correspondant \u00e0 l'int\u00e9rieur de la fen\u00eatre graphique o\u00f9 les pixels sont dessin\u00e9s. Une fois que le dessin est termin\u00e9, le contenu du framebuffer est copi\u00e9 dans la m\u00e9moire vid\u00e9o par le window manager.

    Le rendu mat\u00e9riel par opposition n'est pas g\u00e9r\u00e9 par le CPU mais par la carte graphique. Si vous \u00e9tiez paysan au moyen-\u00e2ge pour planter une carottes, vous retroussiez vos manches, preniez votre b\u00eache et en avant la besogne. Vous auriez \u00e9t\u00e9 dans le champ (m\u00e9moire vid\u00e9o) pour y planter (allumer) une carotte (pixel). Une carte graphique c'est des milliers de processeurs, c'est une entreprise colossale plus proche des 12 travaux d'Asterix que de la culture ancestrale de la carotte. Coordonner les bonnes personne pour qu'elles aille pour vous planter une carotte \u00e0 l'endroit souhait\u00e9 c'est forc\u00e9ment plus compliqu\u00e9. Le rendu mat\u00e9riel c'est ordonner \u00e0 la carte graphique de planter les carottes \u00e0 votre place. Pendant ce temps vous pouvez faire autre chose. C'est plus rapide, plus efficace, mais forc\u00e9ment plus complexe.

    Le rendu mat\u00e9riel est particuli\u00e8rement plus efficace lorsque des effets graphiques sont utilis\u00e9s (animations 3D, effets d'ombres ou de flous, etc.). L'exemple suivant utlise le rendu mat\u00e9riel pour animer une th\u00e9i\u00e8re en 3D\u2009:

    #include <GL/glut.h>\n#include <math.h>\n\nfloat angle = 0.0f;\n\nvoid display() {\n   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n   glLoadIdentity();\n   gluLookAt(1.0f, 2.0f, 5.0f,   // Position de la cam\u00e9ra\n             0.0f, 0.0f, 0.0f,   // Point de mire\n             0.0f, 1.0f, 0.0f);  // Vecteur \"up\"\n\n   glLightfv(GL_LIGHT0, GL_POSITION, (GLfloat[]){1.0f, 1.0f, 1.0f, 0.0f});\n   glRotatef(angle, 0.0f, 1.0f, 0.0f);  // Rotation autour de l'axe Y\n   glColor3f(0.8f, 0.2f, 0.2f);\n   glutSolidTeapot(1.0);\n   glutSwapBuffers();\n}\n\nvoid reshape(int width, int height) {\n   if (height == 0) height = 1;\n   glViewport(0, 0, width, height);\n   glMatrixMode(GL_PROJECTION);\n   glLoadIdentity();\n   gluPerspective(45.0f, (float)width / (float)height, 1.0f, 100.0f);\n   glMatrixMode(GL_MODELVIEW);\n}\n\nvoid initOpenGL() {\n   glEnable(GL_DEPTH_TEST);\n   glEnable(GL_LIGHTING);\n   glEnable(GL_LIGHT0);\n   glShadeModel(GL_SMOOTH);\n   glClearColor(0.0f, 0.0f, 0.0f, 1.0f);\n}\n\nvoid update(int value) {\n   if ((angle += 2.0f) > 360) angle -= 360;\n   glutPostRedisplay();\n   glutTimerFunc(16 /* ms */, update, 0);\n}\n\nint main(int argc, char** argv) {\n   glutInit(&argc, argv);\n   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);\n   glutInitWindowSize(800, 400);\n   glutCreateWindow(\"Animated Teapot with Shadows\");\n   initOpenGL();\n   glutDisplayFunc(display);\n   glutReshapeFunc(reshape);\n   glutTimerFunc(16, update, 0);\n   glutMainLoop();\n}\n

    Th\u00e9i\u00e8re en 3D

    La biblioth\u00e8que Cairo est un exemple de biblioth\u00e8que de rendu 2D qui peut fonctionner en mode logiciel ou mat\u00e9riel. Cairo est utilis\u00e9 par GTK pour le rendu graphique de ses composants. Il est \u00e9galement utilis\u00e9 par d'autres applications comme Inkscape, Firefox, et WebKit. Voici l'exemple d'un programme qui trace une ligne noir sous la souris\u2009:

    #include <gtk/gtk.h>\n\n#define WIDTH 800\n#define HEIGHT 400\n\ngboolean on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data) {\n   cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);  // White\n   cairo_paint(cr);\n   return FALSE;\n}\n\ngboolean on_mouse_move(GtkWidget *widget, GdkEventMotion *event, gpointer d) {\n   static int last_x = -1, last_y = -1;\n   cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(widget));\n   int x = (int)event->x, y = (int)event->y;\n   if (last_x != -1 && last_y != -1) {\n      cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);\n      cairo_move_to(cr, last_x, last_y);\n      cairo_line_to(cr, x, y);\n      cairo_stroke(cr);\n   }\n   last_x = x;\n   last_y = y;\n   cairo_destroy(cr);\n   return TRUE;  // Stop event propagation\n}\n\nint main(int argc, char *argv[]) {\n   gtk_init(&argc, &argv);\n   GtkWidget *w = gtk_window_new(GTK_WINDOW_TOPLEVEL);\n\n   gtk_window_set_title(GTK_WINDOW(w), \"Frame Buffer Example\");\n   gtk_window_set_default_size(GTK_WINDOW(w), WIDTH, HEIGHT);\n\n   GdkWindow *gdk_window = gtk_widget_get_window(w);\n   GdkDisplay *display = gdk_window_get_display(gdk_window);\n   GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_ARROW);\n   gdk_window_set_cursor(gdk_window, cursor);\n   g_object_unref(cursor);\n\n   g_signal_connect(w, \"destroy\", G_CALLBACK(gtk_main_quit), NULL);\n   g_signal_connect(w, \"draw\", G_CALLBACK(on_draw_event), NULL);\n   g_signal_connect(w, \"motion-notify-event\", G_CALLBACK(on_mouse_move), NULL);\n   gtk_widget_add_events(w, GDK_POINTER_MOTION_MASK);\n   gtk_widget_show_all(w);\n   gtk_main();\n}\n

    Pour compiler ce programme vous aurez besoin de la biblioth\u00e8que GTK. Or cette bibloth\u00e8que a de nombreuses d\u00e9pendances (pango, glib, harfbuzz, freetype, libpng, webp, at...). Pkg-config se r\u00e9v\u00e8le tr\u00e8s utile pour r\u00e9cup\u00e9rer les flags de compilation et de liens. Voici comment compiler ce programme\u2009:

    gcc `pkg-config --cflags gtk+-3.0` x.c `pkg-config --libs gtk+-3.0`\n

    Rien de sorcier, les backticks permettent d'ex\u00e9cuter des sous commandes. On ex\u00e9cute donc pkg-config deux fois. Une pour r\u00e9cup\u00e9rer les drapeaux de compilation et l'autre pour r\u00e9cup\u00e9rer les biblioth\u00e8ques \u00e0 lier. On aurait bien entendu pu le faire \u00e0 la main, mais vous conviendrez que c'est plus simple avec pkg-config.

    gcc -I/usr/include/gtk-3.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0\n-I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/harfbuzz\n-I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount\n-I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo\n-I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0\n-I/usr/include/x86_64-linux-gnu -I/usr/include/webp -I/usr/include/gio-unix-2.0\n-I/usr/include/atk-1.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0\n-I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -pthread\nx.c -lgtk-3 -lgdk-3 -lz -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0\n-lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0\n

    Hello trac\u00e9 avec la souris

    "}, {"location": "course-c/47-gui/window/#bibliotheques-graphiques-de-haut-niveau", "title": "Biblioth\u00e8ques graphiques de haut niveau", "text": "

    Pour faciliter le d\u00e9veloppement d'interfaces graphiques, des biblioth\u00e8ques de plus haut niveau ont \u00e9t\u00e9 cr\u00e9\u00e9es. Celles-ci encapsulent les d\u00e9tails complexes du protocole X11 et fournissent des composants graphiques pr\u00eats \u00e0 l'emploi, comme des boutons, des champs de texte, des bo\u00eetes de dialogue, et bien plus.

    Sous Linux, l'une des biblioth\u00e8ques graphiques les plus populaires est GTK (GIMP Toolkit). GTK est une biblioth\u00e8que open source \u00e9crite en C, largement utilis\u00e9e pour d\u00e9velopper des interfaces graphiques multiplateformes. Elle est notamment \u00e0 la base de l'environnement de bureau GNOME et est utilis\u00e9e dans des logiciels libres majeurs tels que GIMP et Inkscape.

    GTK simplifie \u00e9norm\u00e9ment la cr\u00e9ation d'interfaces graphiques gr\u00e2ce \u00e0 une large gamme de widgets pr\u00eats \u00e0 l'emploi et \u00e0 une gestion int\u00e9gr\u00e9e des \u00e9v\u00e9nements utilisateurs (comme les clics de souris ou les frappes clavier). Par exemple, cr\u00e9er une fen\u00eatre avec des boutons, des listes d\u00e9roulantes, ou des champs de texte est beaucoup plus simple et rapide qu'avec X11 pur.

    #include <gtk/gtk.h>\n\nstatic void on_activate(GtkApplication *app, gpointer user_data) {\n   GtkWidget *window = gtk_application_window_new(app);\n   gtk_window_set_title(GTK_WINDOW(window), \"Exemple GTK4\");\n   gtk_window_set_default_size(GTK_WINDOW(window), 100, 100);\n   gtk_window_present(GTK_WINDOW(window));\n}\n\nint main(int argc, char **argv) {\n   GtkApplication *app =\n       gtk_application_new(\"org.gtk.example\", G_APPLICATION_DEFAULT_FLAGS);\n   g_signal_connect(app, \"activate\", G_CALLBACK(on_activate), NULL);\n   int status = g_application_run(G_APPLICATION(app), argc, argv);\n   g_object_unref(app);\n\n   return status;\n}\n

    Cet exemple montre \u00e0 quel point GTK rend les choses plus simples\u2009: une seule fonction pour ouvrir une fen\u00eatre avec des param\u00e8tres de base.

    "}, {"location": "course-c/47-gui/window/#autres-bibliotheques-graphiques", "title": "Autres biblioth\u00e8ques graphiques", "text": "

    Selon les besoins de votre application, d'autres biblioth\u00e8ques graphiques peuvent \u00eatre plus adapt\u00e9es. Par exemple, Qt est une biblioth\u00e8que graphique populaire pour le d\u00e9veloppement d'applications multiplateformes, notamment en C++. Il s'agit davantage d'un framework complet et des outils tiers pour le design interactif d'interfaces graphiques. De base QT (prononc\u00e9 cute) est un framework objet, il n'est donc pas tr\u00e8s adapt\u00e9 \u00e0 la programmation en C.

    Les biblioth\u00e8ques les plus utilis\u00e9es pour la programmation graphique en C sont les suivantes\u2009:

    • SDL (Simple DirectMedia Layer)
    • Allegro
    "}, {"location": "course-c/47-gui/window/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    La plupart des biblioth\u00e8ques graphiques de haut niveau reposent sur les m\u00eames principes de base pour cr\u00e9er des interfaces graphiques interactives\u2009:

    1. Initialisation\u2009: la biblioth\u00e8que est initialis\u00e9e et les ressources n\u00e9cessaires sont allou\u00e9es.
    2. Cr\u00e9ation de la fen\u00eatre\u2009: une fen\u00eatre graphique est cr\u00e9\u00e9e avec les dimensions et les param\u00e8tres souhait\u00e9s.
    3. Cr\u00e9ation des composants graphiques\u2009: des widgets (boutons, champs de texte, etc.) sont ajout\u00e9s \u00e0 la fen\u00eatre.
    4. Une boucle principale est lanc\u00e9e pour g\u00e9rer les \u00e9v\u00e9nements utilisateur (clics de souris, frappes clavier, etc.) et mettre \u00e0 jour l'affichage en cons\u00e9quence. Les \u00e9v\u00e8nements arrivent dans une file d'attente et sont trait\u00e9s un par un. L'\u00e9cran est rafra\u00eechi \u00e0 chaque it\u00e9ration de la boucle pour afficher les changements.
    5. Lib\u00e9ration des ressources\u2009: une fois l'application termin\u00e9e, les ressources allou\u00e9es sont lib\u00e9r\u00e9es et la fen\u00eatre est ferm\u00e9e.

    Lorsque diff\u00e9rentes fen\u00eatres sont n\u00e9cessaires, le programme peut cr\u00e9er des contextes isol\u00e9s comme des threads pour g\u00e9rer les \u00e9v\u00e9nements de chaque fen\u00eatre. Cela permet de garder l'interface utilisateur r\u00e9active m\u00eame si une fen\u00eatre est bloqu\u00e9e par une op\u00e9ration longue.

    "}, {"location": "course-c/47-gui/window/#allegro", "title": "Allegro", "text": "

    Allegro est une biblioth\u00e8que graphique multiplateforme qui fournit des fonctionnalit\u00e9s pour la cr\u00e9ation de jeux vid\u00e9o et d'applications multim\u00e9dia. Elle est plus orient\u00e9e vers les jeux vid\u00e9o que les interfaces graphiques traditionnelles, mais elle peut \u00eatre utilis\u00e9e pour des applications plus g\u00e9n\u00e9rales. Allegro fournit des fonctionnalit\u00e9s pour la cr\u00e9ation de fen\u00eatres, la gestion des \u00e9v\u00e9nements utilisateur, le rendu graphique, la lecture de sons et de musiques, et bien plus encore. Elle est \u00e9crite en C et est compatible avec de nombreux syst\u00e8mes d'exploitation, y compris Windows, Linux, macOS, iOS et Android et elle est utilisable sur de nombreux langages de programmation, dont le C.

    Voici un exemple simple d'utilisation d'Allegro pour cr\u00e9er une fen\u00eatre graphique avec un cercle rouge qui rebondit sur les bords de la fen\u00eatre\u2009:

    Balle qui rebondit

    #include <allegro5/allegro.h>\n#include <allegro5/allegro_image.h>\n#include <allegro5/allegro_primitives.h>\n#include <stdio.h>\n\nconst float FPS = 60.0;\nconst int SCREEN_W = 640;\nconst int SCREEN_H = 480;\nconst int BALL_SIZE = 20;\n\nint main(int argc, char **argv) {\n   float ball_x = SCREEN_W / 2.0 - BALL_SIZE / 2.0;\n   float ball_y = SCREEN_H / 2.0 - BALL_SIZE / 2.0;\n   float ball_dx = -4.0, ball_dy = 4.0;\n\n   if (!al_init()) {\n      fprintf(stderr, \"Erreur d'initialisation d'Allegro.\\n\");\n      return -1;\n   }\n\n   al_init_primitives_addon();\n   al_install_keyboard();\n\n   ALLEGRO_TIMER *timer = al_create_timer(1.0 / FPS);\n   if (!timer) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation du timer.\\n\");\n      return -1;\n   }\n\n   ALLEGRO_DISPLAY *display = al_create_display(SCREEN_W, SCREEN_H);\n   if (!display) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation de la fen\u00eatre.\\n\");\n      al_destroy_timer(timer);\n      return -1;\n   }\n\n   ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();\n   if (!event_queue) {\n      fprintf(stderr, \"Erreur de cr\u00e9ation de la file d'\u00e9v\u00e9nements.\\n\");\n      al_destroy_display(display);\n      al_destroy_timer(timer);\n      return -1;\n   }\n\n   al_register_event_source(event_queue, al_get_display_event_source(display));\n   al_register_event_source(event_queue, al_get_timer_event_source(timer));\n   al_register_event_source(event_queue, al_get_keyboard_event_source());\n\n   al_start_timer(timer);\n\n   bool redraw = true;\n   for (;;) {\n      ALLEGRO_EVENT ev;\n      al_wait_for_event(event_queue, &ev);\n\n      if (ev.type == ALLEGRO_EVENT_TIMER) {\n         // Move\n         ball_x += ball_dx;\n         ball_y += ball_dy;\n         // Bounce\n         if (ball_x <= 0 || ball_x >= SCREEN_W - BALL_SIZE) ball_dx = -ball_dx;\n         if (ball_y <= 0 || ball_y >= SCREEN_H - BALL_SIZE) ball_dy = -ball_dy;\n         redraw = true;\n      } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE)\n         break;\n\n      if (redraw && al_is_event_queue_empty(event_queue)) {\n         redraw = false;\n         al_clear_to_color(al_map_rgb(0, 0, 0));\n         al_draw_filled_circle(ball_x + BALL_SIZE / 2, ball_y + BALL_SIZE / 2,\n                               BALL_SIZE / 2, al_map_rgb(255, 0, 0));\n         al_flip_display();  // Refresh\n      }\n   }\n\n   // Free resources\n   al_destroy_timer(timer);\n   al_destroy_display(display);\n   al_destroy_event_queue(event_queue);\n}\n
    "}, {"location": "course-c/47-gui/window/#conclusion", "title": "Conclusion", "text": "

    En r\u00e9sum\u00e9, bien que X11 soit le fondement de l'affichage graphique sous Linux, son utilisation directe est rarement n\u00e9cessaire pour les d\u00e9veloppeurs modernes. Des biblioth\u00e8ques comme GTK permettent de se concentrer sur la logique de l'application et l'exp\u00e9rience utilisateur, tout en masquant les d\u00e9tails techniques sous-jacents. Il est recommand\u00e9 d'utiliser ces outils de plus haut niveau pour cr\u00e9er des interfaces graphiques compl\u00e8tes, flexibles et ergonomiques.

    "}, {"location": "course-c/48-network/", "title": "Introduction", "text": "

    Le r\u00e9seau est une sacr\u00e9e gal\u00e8re. C'est un domaine de l'ing\u00e9nierie en soi, et il n'est pas possible de tout couvrir en un seul chapitre. N\u00e9anmoins, je pense qu'il est essentiel \u00e0 tout ing\u00e9nieur qu'il soit \u00e9lectronicien ou informaticien d'avoir quelques notions de base sur le sujet. Cette partie se concentre sur le mod\u00e8le OSI, comprendre la pile de protocoles r\u00e9seau et se familiariser avec quelques protocoles. Ensuite, quelques exemples d'utilisations de sockets en C sont donn\u00e9s.

    "}, {"location": "course-c/48-network/applications/", "title": "Applications r\u00e9seau", "text": "

    Ce chapitre donne quelques exemples tr\u00e8s simples d'applications r\u00e9seau en C.

    "}, {"location": "course-c/48-network/applications/#serveur-web", "title": "Serveur Web", "text": "

    Le plus simple en C est d'utiliser la biblioth\u00e8que Mongoose qui est tr\u00e8s l\u00e9g\u00e8re et utilisable dans des applications embarqu\u00e9es. Elle fonctionne sous Windows, Linux, macOS, Windows, Android et sur diff\u00e9rents microcontr\u00f4leurs (STM32, NXP, ESP32, NRF52, ...).

    Le plus magique c'est que cette biblioth\u00e8que est monolithique, c'est-\u00e0-dire qu'elle ne d\u00e9pend d'aucune autre biblioth\u00e8que. Il suffit de rajouter les fichiers mongoose.c et mongoose.h dans votre projet et de les compiler avec votre programme.

    L'utilisation est tr\u00e8s simple. Le programme d'exemple ci-dessous cr\u00e9e un serveur web qui \u00e9coute sur le port 8090. Il r\u00e9pond \u00e0 quatre requ\u00eates diff\u00e9rentes\u2009:

    • / : Retourne Hello, world!, c'est la page d'accueil.
    • /source : Retourne le contenu du dossier www situ\u00e9 dans le r\u00e9pertoire courant.
    • /api/time/get : Retourne l'heure actuelle en JSON, il simule une API.
    • /teapot : Retourne I'm a teapot, c'est une blague.
    • Tout autre URI retourne une erreur 500.
    #include \"mongoose.h\"\n\nvoid ev_handler(struct mg_connection *c, int ev, void *ev_data) {\n   if (ev == MG_EV_HTTP_MSG) {\n      struct mg_http_message *hm = (struct mg_http_message *)ev_data;\n      if (mg_match(hm->uri, mg_str(\"/\"), NULL)) {\n         mg_http_reply(c, 200, \"Content-Type: text/plain\\r\\n\",\n            \"Hello, world!\\n\");\n      } else if (mg_match(hm->uri, mg_str(\"/source\"), NULL)) {\n         struct mg_http_serve_opts opts = {.root_dir = \"./www\"};\n         mg_http_serve_dir(c, hm, &opts);\n      } else if (mg_match(hm->uri, mg_str(\"/api/time/get\"), NULL)) {\n         mg_http_reply(c, 200, \"Content-Type: application/json\\r\\n\",\n            \"{%m:%lu}\\n\", MG_ESC(\"time\"), time(NULL));\n      } else if (mg_match(hm->uri, mg_str(\"/teapot\"), NULL)) {\n         mg_http_reply(c, 418, \"\", \"I'm a teapot\\n\");\n      } else {\n         mg_http_reply(c, 500, \"Content-Type: application/json\\r\\n\",\n            \"{%m:%m}\\n\", MG_ESC(\"error\"), MG_ESC(\"Unsupported URI\"));\n      }\n   }\n}\n\nint main(void) {\n   struct mg_mgr mgr;\n   mg_mgr_init(&mgr);\n   mg_http_listen(&mgr, \"http://0.0.0.0:8090\", ev_handler, NULL);\n   for (;;) mg_mgr_poll(&mgr, 1000);\n}\n
    ", "tags": ["mongoose.c", "mongoose.h", "www"]}, {"location": "course-c/48-network/applications/#requete-http", "title": "Requ\u00eate HTTP", "text": "

    Il est parfois utile pour un programme de r\u00e9cup\u00e9rer des informations sur un serveur web. Le d\u00e9veloppeur recherchera une solution impliquant une API simple retournant un contenu JSON. Le JSON est un format de s\u00e9rialisation de donn\u00e9es tr\u00e8s populaire, et il est facile \u00e0 lire et \u00e0 \u00e9crire pour les humains.

    Imaginons que vous souhaitez r\u00e9aliser un programme qui affiche un fait incroyable sur les chats. Heureusement, vous avez \u00e0 votre disposition l'API Cat Facts. Une requ\u00eate HTTP GET sera envoy\u00e9e \u00e0 l'URL https://catfact.ninja/fact pour obtenir un fait al\u00e9atoire en JSON.

    Voici un exemple de requ\u00eate\u2009:

    GET /fact HTTP/1.1\nHost: catfact.ninja\n

    Et voici un exemple de r\u00e9ponse\u2009:

    {\n    \"fact\": \"The cat who holds the record for the longest non-fatal fall is\n    Andy. He fell from the 16th floor of an apartment building\n    (about 200 ft/.06 km) and survived.\",\n    \"length\": 157\n}\n

    Il y a plusieurs mani\u00e8res de r\u00e9aliser ce programme en C tout d'abord sans aucune d\u00e9pendance. Le protocole HTTP sera \u00e9mul\u00e9 en utilisant des sockets et la r\u00e9ponse JSON sera d\u00e9coup\u00e9e \u00e0 la main. Notez que ce code n'est pas du tout un bon exemple. Tout d'abord aucune erreur n'est v\u00e9rifi\u00e9e, et le code est tr\u00e8s fragile. Si la r\u00e9ponse du serveur change, le programme ne fonctionnera plus. N\u00e9anmoins l'exercice ici est de comprendre que HTTP n'est qu'un simple protocole texte.

    #define BUFFER_SIZE 4096\n\nint main() {\n   char *request =\n       \"GET /fact HTTP/1.1\\r\\n\"\n       \"Host: catfact.ninja\\r\\n\"\n       \"Connection: close\\r\\n\"\n       \"\\r\\n\";\n\n   struct addrinfo hints = {0}, *res;\n   hints.ai_family = AF_UNSPEC;  // Allow IPv4 or IPv6\n   hints.ai_socktype = SOCK_STREAM;\n   getaddrinfo(\"catfact.ninja\", \"80\", &hints, &res);\n\n   int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);\n   connect(sockfd, res->ai_addr, res->ai_addrlen);\n   freeaddrinfo(res);\n   send(sockfd, request, strlen(request), 0);\n\n   char buffer[BUFFER_SIZE];\n   int bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);\n   if (bytes_received < 0) perror(\"Erreur lors de la r\u00e9ception de la r\u00e9ponse\");\n   close(sockfd);\n\n   printf(\"R\u00e9ponse re\u00e7ue : %.*s\\n\", bytes_received, buffer);\n\n   char *json_start = strstr(buffer, \"\\r\\n\\r\\n\");\n   if (json_start) {\n      json_start += 4;\n      char *fact_start = strstr(json_start, \"\\\"fact\\\":\\\"\");\n      if (fact_start) {\n         fact_start += 8;\n         char *fact_end = strchr(fact_start, '\"');\n         if (fact_end) {\n            *fact_end = '\\0';\n            printf(\"%s\\n\", fact_start);\n         }\n      }\n   } else {\n      printf(\"R\u00e9ponse HTTP invalide ou absence de JSON.\\n\");\n   }\n}\n

    H\u00e9las, si ce code fonctionnait bien il y a quelques ann\u00e9es, la r\u00e9ponse que l'on obtient est la suivante\u2009:

    R\u00e9ponse re\u00e7ue : HTTP/1.1 301 Moved Permanently\nServer: nginx/1.24.0\nDate: Mon, 16 Sep 2024 12:58:04 GMT\nContent-Type: text/html\nContent-Length: 169\nConnection: close\nLocation: https://catfact.ninja/fact\n\n<html>\n<head><title>301 Moved Permanently</title></head>\n<body>\n<center><h1>301 Moved Permanently</h1></center>\n<hr><center>nginx/1.24.0</center>\n</body>\n</html>\n

    Le code de r\u00e9ponse est 301 parce que le site a \u00e9t\u00e9 d\u00e9plac\u00e9 vers un nouvel emplacement et cet emplacement est https://catfact.ninja/fact. Notez la pr\u00e9sence du s \u00e0 http. Cela signifie que le serveur utilise le protocole HTTPS et non HTTP. Sans biblioth\u00e8que suppl\u00e9mentaire, il est impossible de r\u00e9aliser une requ\u00eate HTTPS car cela n\u00e9cessite une couche de chiffrement et des certificats.

    Voyons voir comment faire la m\u00eame chose convenablement, en utilisant les bonnes d\u00e9pendances. Nous aurons besoin de quelques biblioth\u00e8ques\u2009:

    sudo apt install libcurl4-openssl-dev libjson-c-dev\n

    Voici le code C. La biblioth\u00e8que libcurl est utilis\u00e9e pour effectuer la requ\u00eate HTTP(S) et la biblioth\u00e8que libjson-c est utilis\u00e9e pour analyser la r\u00e9ponse JSON\u2009:

    #include <curl/curl.h>\n#include <json-c/json.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nstruct response { char *memory; size_t size; };\n\nstatic size_t write_callback(void *data, size_t size, size_t nmemb, void *userp) {\n   size_t realsize = size * nmemb;\n   struct response *mem = (struct response *)userp;\n   char *ptr = realloc(mem->memory, mem->size + realsize + 1);\n   if (ptr == NULL) {\n      printf(\"Not enough memory (realloc failed)\\n\");\n      return 0;\n   }\n   mem->memory = ptr;\n   memcpy(&(mem->memory[mem->size]), data, realsize);\n   mem->size += realsize;\n   mem->memory[mem->size] = '\\0';\n   return realsize;\n}\n\nint main() {\n   struct response chunk = {.memory = malloc(1), .size = 0};\n\n   curl_global_init(CURL_GLOBAL_DEFAULT);\n   CURL *curl = curl_easy_init();\n   if (!curl) {\n      fprintf(stderr, \"Erreur cURL: impossible d'initialiser\\n\");\n      return 1;\n   }\n\n   curl_easy_setopt(curl, CURLOPT_URL, \"https://catfact.ninja/fact\");\n   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);\n   curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);\n   CURLcode res = curl_easy_perform(curl);\n\n   if (res != CURLE_OK) {\n      fprintf(stderr, \"Erreur cURL: %s\\n\", curl_easy_strerror(res));\n      return 1;\n   }\n\n   struct json_object *parsed_json = json_tokener_parse(chunk.memory);\n   struct json_object *fact;\n   json_object_object_get_ex(parsed_json, \"fact\", &fact);\n   printf(\"%s\\n\", json_object_get_string(fact));\n   json_object_put(parsed_json);\n\n   curl_easy_cleanup(curl);\n   free(chunk.memory);\n   curl_global_cleanup();\n}\n

    N'oubliez pas lors de la compilation de rajouter les biblioth\u00e8ques\u2009:

    gcc catfact.c -lcurl -ljson-c\n
    ", "tags": ["libcurl"]}, {"location": "course-c/48-network/osi/", "title": "Mod\u00e8le OSI", "text": ""}, {"location": "course-c/48-network/osi/#introduction", "title": "Introduction", "text": "

    Le mod\u00e8le OSI (Open Systems Interconnection) est un mod\u00e8le de r\u00e9f\u00e9rence pour les communications entre ordinateurs. Il a \u00e9t\u00e9 cr\u00e9\u00e9 par l'ISO (International Organization for Standardization) en 1984.

    Le mod\u00e8le OSI est divis\u00e9 en 7 couches, chacune ayant un r\u00f4le sp\u00e9cifique dans le processus de communication. Chaque couche communique avec les couches adjacentes pour transmettre les donn\u00e9es.

    "}, {"location": "course-c/48-network/osi/#les-couches", "title": "Les couches", "text": "

    La figure suivante illustre les 7 couches du mod\u00e8le OSI en comparaison du PDU (Protocol Data Unit) associ\u00e9 \u00e0 chaque couche et du mod\u00e8le internet TCP/IP qui est apparu avant la standardisation du mod\u00e8le OSI. En effet ce mod\u00e8le est plus ancien et ne comporte que 4 couches et ses couches ne sont pas conformes au mod\u00e8le OSI. En effet, comme nous le verrons plus tard, une pile de protocoles ne doit pas d\u00e9pendre des protocoles des autres couches, or dans TCP/IP la somme de contr\u00f4le de la couche de transport fait intervenir une partie de l'en-t\u00eate IP.

    Mod\u00e8le OSI

    1. Couche physique (Physical Layer): Cette couche est responsable de la transmission des donn\u00e9es brutes sur le support de communication. Elle d\u00e9finit les caract\u00e9ristiques \u00e9lectriques, m\u00e9caniques et fonctionnelles du mat\u00e9riel de communication.

    2. Couche liaison de donn\u00e9es (Data Link Layer): Cette couche est responsable de la transmission des donn\u00e9es entre les n\u0153uds voisins sur le r\u00e9seau local. Elle g\u00e8re les erreurs de transmission, le contr\u00f4le de flux et l'acc\u00e8s au support.

    3. Couche r\u00e9seau (Network Layer): Cette couche est responsable de la transmission des donn\u00e9es entre les n\u0153uds distants sur le r\u00e9seau. Elle g\u00e8re le routage des donn\u00e9es \u00e0 travers le r\u00e9seau.

    4. Couche transport (Transport Layer): Cette couche est responsable de la transmission des donn\u00e9es de bout en bout entre les applications. Elle g\u00e8re le contr\u00f4le de flux, la segmentation et le r\u00e9assemblage des donn\u00e9es.

    5. Couche session (Session Layer): Cette couche est responsable de l'\u00e9tablissement, de la gestion et de la fin des sessions de communication entre les applications.

    6. Couche pr\u00e9sentation (Presentation Layer): Cette couche est responsable de la traduction, de la compression et du chiffrement des donn\u00e9es pour la communication entre les applications.

    7. Couche application (Application Layer): Cette couche est responsable de la communication entre les applications. Elle fournit des services de haut niveau tels que l'authentification, la messagerie et le partage de fichiers.

    "}, {"location": "course-c/48-network/osi/#encapsulation", "title": "Encapsulation", "text": "

    L'interop\u00e9rabilit\u00e9 entre diff\u00e9rents syst\u00e8mes est souvent rendue possible gr\u00e2ce \u00e0 un concept nomm\u00e9 encapsulation. C'est un concept \u00e9galement cl\u00e9 en programmation orient\u00e9e objet. L'encapsulation consiste \u00e0 dissimuler de la complexit\u00e9 non n\u00e9cessaire \u00e0 comprendre la partie int\u00e9ressante d'un probl\u00e8me dans un ensemble isol\u00e9 et qui peut \u00eatre consid\u00e9r\u00e9 comme une bo\u00eete noire.

    En communication r\u00e9seau, par exemple lorsque vous \u00eates au t\u00e9l\u00e9phone, vous n'avez pas besoin de savoir quelle est la nature de la ligne de transport qui v\u00e9hicule votre voix. Est-ce une ligne de cuivre, une fibre optique, une liaison satellite, cela n'a pas d'importance. N\u00e9anmoins vous n'\u00eates que l'utilisateur final de cette communication, il y a d'autres \u00e9tapes interm\u00e9diaires. Celle qui v\u00e9hicule r\u00e9ellement votre voix accompagn\u00e9e \u00e9ventuellement de votre vid\u00e9o et des touches que vous appuyez sur votre t\u00e9l\u00e9phone. Cette \u00e9tape est la couche application, son besoin c'est de pouvoir communiquer d'un t\u00e9l\u00e9phone \u00e0 l'autre, le reste d'a pas d'importance. En dessous de cette couche, il y a la couche transport qui va s'occuper de d\u00e9couper les diff\u00e9rentes donn\u00e9es en paquets qui pourront \u00eatre achemin\u00e9s par diff\u00e9rentes routes (via le Wi-Fi, le r\u00e9seau cellulaire, etc.). Cette couche ne sait pas ce qu'elle transporte comme donn\u00e9es et ne sait pas non plus sur quoi sont transport\u00e9es ces donn\u00e9es. Tout en bas de la pile, il y a la couche physique, c'est pr\u00e9cis\u00e9ment le type de m\u00e9dia qui est utilis\u00e9 pour transporter les donn\u00e9es (c\u00e2ble, fibre optique ...).

    Vous imaginez probablement que les donn\u00e9es sont transmises sous forme de bits, ce qui est vrai en partie. En effet, en transmission par c\u00e2ble ou sans fil, les donn\u00e9es ne sont pas simplement des \u00e9tats logiques 0 ou 1, souvent elles sont modul\u00e9es pour \u00eatre transport\u00e9es plus efficacement. Cela donne d'ailleurs le nom aux dispositifs qui s'occupent de moduler et d\u00e9moduler les signaux, les modems.

    L'encapsulation ici sert donc \u00e0 d\u00e9couper les responsabilit\u00e9s. Si vous souhaitez v\u00e9hiculer des bits d'un point \u00e0 un autre, vous n'avez pas besoin de savoir comment cela se fait, vous devez juste vous reposer sur la couche physique.

    "}, {"location": "course-c/48-network/osi/#protocoles-internet", "title": "Protocoles Internet", "text": "

    La pile de protocoles Internet est assez complexe, mais elle est tr\u00e8s bien document\u00e9e. Les RFC (Request For Comments) sont des documents qui d\u00e9crivent les protocoles Internet. Ils sont publi\u00e9s par l'IETF (Internet Engineering Task Force) et sont des standards de facto. Chaque protocole que l'on utilise pour communiquer sur un r\u00e9seau informatique est d\u00e9crit dans un RFC. Par exemple, le protocole HTTP (HyperText Transfer Protocol) responsable de communiquer les pages web visibles dans votre navigateur est d\u00e9crit dans le RFC 2616. Ce protocole est encapsul\u00e9 dans le protocole TCP normalis\u00e9 par le RFC 793 (Transmission Control Protocol) qui lui-m\u00eame est encapsul\u00e9 dans le protocole IP (RFC 791). Ce dernier est \u00e9galement encapsul\u00e9 dans le protocole MAC RFC 3422 (Media Access Control) qui est lui-m\u00eame encapsul\u00e9 dans le protocole Ethernet (RFC 894).

    RFC

    Un fait notable avec les RFC est le format de r\u00e9daction. Ces documents sont \u00e9crits en pur texte ASCII sans outils de mise en page. Ce n'est pas du Markdown, du Word, du LaTeX, c'est du texte brut mais tr\u00e8s bien structur\u00e9 avec des figures, des tables des mati\u00e8res, des r\u00e9f\u00e9rences crois\u00e9es, etc.

    La raison \u00e0 cela est que les RFC sont des documents de r\u00e9f\u00e9rence et doivent \u00eatre accessibles par tous et \u00e0 toute \u00e9poque. Les informaticiens savaient d\u00e9j\u00e0 que les formats de fichiers sont \u00e9ph\u00e9m\u00e8res et que les outils de traitement de texte ne sont pas toujours compatibles entre eux. Le texte brut est donc le format le plus s\u00fbr pour garantir la p\u00e9rennit\u00e9 des documents. Et ils ont raison car depuis 50 ans, les RFC sont toujours lisibles et utilisables.

    En termes informatiques, et surtout en programmation, chaque protocole dispose de ses propres champs de donn\u00e9es. Par exemple voici le la figure 3 du RFC 793 telle que publi\u00e9e en 1981\u2009:

     0                   1                   2                   3\n 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|          Source Port          |       Destination Port        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                        Sequence Number                        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Acknowledgment Number                      |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|  Data |           |U|A|P|R|S|F|                               |\n| Offset| Reserved  |R|C|S|S|Y|I|            Window             |\n|       |           |G|K|H|T|N|N|                               |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|           Checksum            |         Urgent Pointer        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Options                    |    Padding    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                             data                              |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n

    Ce sch\u00e9ma d\u00e9crit l'en-t\u00eate du protocole TCP. Il est compos\u00e9 de plusieurs champs qui sont utilis\u00e9s pour la communication entre les machines. On voit par exemple que ce protocole utilise la notion de port pour identifier les applications qui communiquent. Les ports sont des num\u00e9ros qui sont attribu\u00e9s \u00e0 chaque application. Par exemple, le port 80 est utilis\u00e9 pour le protocole HTTP, le port 443 pour le protocole HTTPS, le 21 est utilis\u00e9 pour le protocole FTP tandis que le port 22 est utilis\u00e9 pour le protocole SSH. Outre les ports, les donn\u00e9es peuvent \u00eatre fragment\u00e9es en plusieurs paquets c'est pourquoi chaque paquet contient un num\u00e9ro de s\u00e9quence et un num\u00e9ro d'acquittement. \u00c0 la fin de l'en-t\u00eate se trouvent les donn\u00e9es des couches sup\u00e9rieures.

    Le protocole de transport TCP est bas\u00e9 sur le protocole IP. On trouve \u00e9galement dans son standard (RFC 791, figure 4) un en-t\u00eate similaire qui va contenir l'adresse IP (p. ex. 192.168.45.20) source et destination, le protocole utilis\u00e9 en dessus (TCP, UDP, ICMP, etc.), la taille du paquet, etc.

     0                   1                   2                   3\n 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|Version|  IHL  |Type of Service|          Total Length         |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|         Identification        |Flags|      Fragment Offset    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|  Time to Live |    Protocol   |         Header Checksum       |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                       Source Address                          |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Destination Address                        |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n|                    Options                    |    Padding    |\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n

    En C, si l'on devait r\u00e9impl\u00e9menter ces protocoles, on pourrait utiliser des structures. Voici par exemple comment on pourrait d\u00e9finir les en-t\u00eates des protocoles MAC et IP\u2009:

    struct mac_header {\n    uint8_t destination_mac[6];  // Adresse MAC de destination\n    uint8_t source_mac[6];       // Adresse MAC source\n    uint16_t ethertype;  // Protocole encapsul\u00e9 (p. ex. 0x0800 pour IPv4)\n};\n\nstruct ip_header {\n    uint8_t version_ihl;\n    uint8_t dscp_ecn;\n    uint16_t total_length;\n    uint16_t identification;\n    uint16_t flags_fragment_offset;\n    uint8_t ttl;                 // Time To Live (dur\u00e9e de vie)\n    uint8_t protocol;            // Protocole de couche sup\u00e9rieure (ex. TCP = 6)\n    uint16_t checksum;\n    uint32_t source_ip;          // Adresse IP source\n    uint32_t destination_ip;     // Adresse IP de destination\n};\n

    En pratique c'est plus complexe que cela, d'une part par que les donn\u00e9es sont v\u00e9hicul\u00e9es en big-endian (octets de poids fort en premier) et que les champs de donn\u00e9es sont de taille variable. Par exemple, l'adresse IP est cod\u00e9e sur 32 bits, mais elle peut \u00eatre repr\u00e9sent\u00e9e en IPv4 (4 octets) ou en IPv6 (16 octets).

    L'id\u00e9e ici est juste de montrer l'int\u00e9r\u00eat des structures dans ce type de situation.

    "}, {"location": "course-c/48-network/protocoles/", "title": "Protocoles", "text": "

    Cet ouvrage n'\u00e9tant pas un cours consacr\u00e9 au r\u00e9seau, il n'est pas raisonnable d'y consacrer plus d'un chapitre. N\u00e9anmoins, l'ing\u00e9nieur d\u00e9veloppeur en C devra tout de m\u00eame comprendre, du moins conceptuellement les diff\u00e9rences entre les diff\u00e9rents protocoles de communication, comment ils sont imbriqu\u00e9s et \u00e0 quoi ils servent. in fine en programmation vous aurez probablement simplement besoin de savoir comment ouvrir une connexion TCP ou UDP, comment envoyer des donn\u00e9es et comment les recevoir.

    "}, {"location": "course-c/48-network/protocoles/#mac", "title": "MAC", "text": "

    Chaque carte r\u00e9seau install\u00e9e dans un ordinateur, un t\u00e9l\u00e9phone portable ou une tablette poss\u00e8de une adresse physique unique appel\u00e9e adresse MAC (Media Access Control). Cette adresse est cod\u00e9e sur 48 bits (6 octets) et est attribu\u00e9e par le fabricant de la carte. Ce dernier obtient un OUI (Organizationally Unique Identifier) qui est un pr\u00e9fixe de 24 bits attribu\u00e9 par l'IEEE (Institute of Electrical and Electronics Engineers) \u00e0 une organisation. Avec un OUI, le fabricant peut g\u00e9rer jusqu'\u00e0 16 millions d'adresses MAC. Les 24 bits restants sont attribu\u00e9s par le fabricant lui-m\u00eame. Obtenir un OUI co\u00fbte environ 2 700 dollars. Ceci explique pourquoi les Raspberry PI ont des adresses MAC qui commencent par b8:27:eb qui est l'OUI de la fondation Raspberry PI.

    \u00c0 moins de vouloir communiquer directement avec une carte r\u00e9seau, l'adresse MAC n'est pas tr\u00e8s utile pour un d\u00e9veloppeur. En effet, c'est le syst\u00e8me d'exploitation qui se charge de g\u00e9rer les communications r\u00e9seau et qui va utiliser l'adresse IP pour identifier les machines. L'adresse MAC c'est un peu comme la coordonn\u00e9e GPS d'une maison. Elle est utile au cadastraliste pour identifier un terrain et y attribuer une adresse postale, mais pour le facteur, c'est l'adresse postale qui est utile.

    "}, {"location": "course-c/48-network/protocoles/#ip", "title": "IP", "text": "

    Un r\u00e9seau informatique c'est un groupement de machines qui communiquent entre elles. Mettez 8 milliards de personnes dans la m\u00eame salle, donnez-leur un porte-voix assez puissant et vous aurez un r\u00e9seau. Vous admettrez n\u00e9anmoins qu'arbitrer les \u00e9changes pour que chacun puisse parler \u00e0 son tour est irr\u00e9alisable. En outre, cela ne vous int\u00e9resse probablement gu\u00e8re de savoir que Madame Michu a gagn\u00e9 5 francs au Tribolo. C'est pourquoi les r\u00e9seaux informatiques sont organis\u00e9s en sous-r\u00e9seau.

    Les ing\u00e9nieurs syst\u00e8me utilisent le protocole IP pour associer \u00e0 une adresse MAC donn\u00e9e une adresse IP. L'adresse IP est une adresse logique attribu\u00e9e \u00e0 chaque machine sur un r\u00e9seau. Elle est historiquement cod\u00e9e sur 32-bits et permettant ainsi d'identifier environ 4 milliards de machines. Avec l'explosion de l'Internet, cette taille est devenue tr\u00e8s largement insuffisante. C'est pourquoi un nouveau protocole, IPv6 (version 6) a \u00e9t\u00e9 cr\u00e9\u00e9. Ce protocole utilise des adresses cod\u00e9es sur 128 bits et permet d'identifier 340 sextillions de machines. C'est largement suffisant pour attribuer une adresse IP \u00e0 chaque grain de sable sur Terre. N\u00e9anmoins les adresses IPv6 sont moins facilement m\u00e9morisables que les adresses IPv4. C'est pourquoi ces derni\u00e8res sont encore largement utilis\u00e9es dans des r\u00e9seaux priv\u00e9s ou d'entreprise.

    Un p\u00e9riph\u00e9rique IP peut \u00eatre cloisonn\u00e9 facilement en plusieurs sous-r\u00e9seaux gr\u00e2ce \u00e0 un masque de sous-r\u00e9seau. Par exemple, le p\u00e9riph\u00e9rique \u00e0 l'adresse IP 192.168.1.42 avec un masque de sous-r\u00e9seau 255.255.255.0 ne va s'int\u00e9resser qu'aux p\u00e9riph\u00e9riques de la plage 192.168.1.0 \u00e0 192.168.1.255. Cela permet \u00e0 la carte r\u00e9seau de filtrer les paquets qui lui sont destin\u00e9s et de ne pas s'int\u00e9resser aux autres. Pour reprendre notre analogie, c'est comme si vous aviez sur vos oreilles un casque antibruit qui ne laisse passer que certaines voix.

    Il y a deux types d'adresses IP, celles dites publiques et routables sur Internet et celles dites priv\u00e9es qui sont utilis\u00e9es dans les r\u00e9seaux locaux. Les adresses priv\u00e9es sont d\u00e9finies par la RFC 1918 et sont les suivantes\u2009:

    Plage d'adresses Masque de sous-r\u00e9seau Classe 10.0.0.0 10.255.255.255 (10/8) A 172.16.0.0 172.31.255.255 (172.16/12) B 192.168.0.0 192.168.255.255 (192.168/16) C

    Il est tr\u00e8s probable que l'adresse IP de votre ordinateur ou de votre t\u00e9l\u00e9phone sur le r\u00e9seau de votre domicile ou celui de votre travail soit l'une de ces adresses. Elles ne sont pas routables c'est-\u00e0-dire qu'elles ne peuvent pas \u00eatre transport\u00e9es en dehors du r\u00e9seau local.

    Pour communiquer publiquement, il faut utiliser une adresse IP publique. Ces adresses sont attribu\u00e9es par les fournisseurs d'acc\u00e8s \u00e0 Internet (FAI) et sont g\u00e9r\u00e9es par l'IANA (Internet Assigned Numbers Authority). Chez vous, vous avez probablement une adresse IP publique attribu\u00e9e par votre FAI (Salt, Swisscom, Sunrise, Orange...) c'est l'adresse attribu\u00e9e \u00e0 votre routeur ou box.

    \u00c0 votre travail ou dans votre \u00e9cole, c'est pareil. Une \u00e9cole n'a pas les moyens ni l'int\u00e9r\u00eat d'avoir une adresse IP publique pour chaque ordinateur. Elle dispose d'une certaine quantit\u00e9 d'adresses IP publiques utilis\u00e9es pour communiquer vers l'ext\u00e9rieur. Si par exemple vous allez sur le site showmyip.com, vous pouvez voir l'adresse IP publique utilis\u00e9e pour communiquer sur internet.

    "}, {"location": "course-c/48-network/protocoles/#nat-pat", "title": "NAT / PAT", "text": "

    On peut se demander comment les donn\u00e9es en provenance d'internet peuvent arriver jusqu'\u00e0 votre ordinateur si tout le monde dans une m\u00eame institution \u00e0 la m\u00eame adresse. C'est l\u00e0 qu'intervient le NAT (Network Address Translation), plus pr\u00e9cis\u00e9ment dans notre cas le PAT (Port Address Translation). Il s'agit d'un dispositif qui va traduire les adresses IP priv\u00e9es en adresses IP publiques. Admettons que j'aimerais envoyer un message \u00e0 Monsieur Google \u00e0 l'adresse 1600 Amphi-theatre Parkway, Mountain View, CA 94043, USA. Pour simplifier disons simplement que son adresse est\u2009: 142.250.203.110:80443. Le 80443 est le num\u00e9ro du port utilis\u00e9. C'est un peu comme le num\u00e9ro de l'appartement dans un immeuble. Comme je crois dur comme fer \u00e0 l'encapsulation, je vais indiquer sur l'enveloppe au dos de l'enveloppe l'adresse de l'exp\u00e9diteur, c'est-\u00e0-dire mon adresse. Je vais donc \u00e9crire quelque chose comme\u2009: 192.168.1.42:7878. Or, vous l'aurez compris, cette adresse n'est pas routable sur internet et Monsieur Google ne saura pas o\u00f9 me r\u00e9pondre. C'est pareil que dire que le tr\u00e9sor se cache sous la troisi\u00e8me pierre \u00e0 droite du vieux ch\u00eane. Sans savoir dans quelle for\u00eat, dans quelle contr\u00e9e et dans quel pays chercher, vous ne deviendrez pas riche.

    N\u00e9anmoins, rappelez-vous du NAT. Vous avez un gentil facteur qui, avant que votre lettre ne soit post\u00e9e en direction des Am\u00e9riques, va r\u00e9\u00e9crire l'adresse de l'exp\u00e9diteur. Il va mettre par exemple\u2009: 193.134.219.72:57898. Il s'agit de l'adresse IP publique de votre institution. Quant au port, il a \u00e9t\u00e9 choisit al\u00e9atoirement par le facteur. C'est un peu comme si le facteur avait mis un num\u00e9ro de bo\u00eete postale disponible. Bien entendu il notera dans un cahier que cette bo\u00eete postale (ce port) est r\u00e9serv\u00e9e \u00e0 la r\u00e9ponse de Monsieur Google et qu'il vous est destin\u00e9.

    Vous l'aurez compris, il n'existe pas de facteur. C'est un dispositif informatique nomm\u00e9 routeur qui se charge de faire cette traduction. C'est le routeur qui se charge de cette t\u00e2che. Il va traduire l'adresse IP priv\u00e9e en adresse IP publique et va conserver dans une table de traduction l'adresse IP priv\u00e9e et le port utilis\u00e9. Lorsque la r\u00e9ponse de Monsieur Google arrivera, le routeur va regarder dans sa table de traduction et va savoir \u00e0 qui renvoyer la r\u00e9ponse. C'est un peu comme si le facteur avait un cahier avec les noms des destinataires et les num\u00e9ros de bo\u00eetes postales.

    Sur votre machine Linux, vous pouvez voir les connexions en cours avec conntrack, on y voir le type de socket (TCP, UDP), l'adresse IP source et destination, le port source et destination, et la traduction. Pour la premi\u00e8re connexion, on voit une adresse source priv\u00e9e, mais une adresse de destination publique. C'est une connexion sortante. Le port de destination 34230 est probablement attribu\u00e9 par le m\u00e9canisme de PAT du routeur du destinataire.

    \u279c sudo conntrack -L\ntcp      6 95 SYN_SENT src=172.24.167.101 dst=169.254.169.254 sport=34230\n         dport=80 [UNREPLIED] src=169.254.169.254 dst=172.24.167.101\n         sport=80 dport=34230 mark=0 use=1\nudp      17 23 src=10.255.255.254 dst=10.255.255.254 sport=41319\n         dport=53 src=10.255.255.254 dst=10.255.255.254\n         sport=53 dport=41319 mark=0 use=1\ntcp      6 99 SYN_SENT src=172.24.167.101 dst=169.254.169.254 sport=57376\n         dport=80 [UNREPLIED] src=169.254.169.254 dst=172.24.167.101\n         sport=80 dport=57376 mark=0 use=1\n...\n

    Ce m\u00e9canisme de NAT/PAT intervient \u00e0 de multiples strates. Tout d'abord au niveau d'un programme complexe comme Docker qui va cr\u00e9er des r\u00e9seaux virtuels pour isoler les conteneurs. Ensuite au niveau de votre syst\u00e8me d'exploitation pour g\u00e9rer les connexions entre les diff\u00e9rents programmes. En effet votre PC ou votre Mac \u00e0 une seule carte r\u00e9seau partag\u00e9e par tous les programmes. Enfin, au niveau de votre routeur qui va g\u00e9rer les connexions entre votre r\u00e9seau local et internet. La situation se r\u00e9p\u00e8te de l'autre c\u00f4t\u00e9 de l'atlantique chez Monsieur Google (du routeur \u00e0 la machine, de la machine au programme...).

    ", "tags": ["conntrack"]}, {"location": "course-c/48-network/protocoles/#dhcp", "title": "DHCP", "text": "

    Lorsque vous connectez un p\u00e9riph\u00e9rique \u00e0 un r\u00e9seau local, il a besoin d'une adresse IP. Dans de rares cas, c'est de votre responsabilit\u00e9 de choisir une adresse, mais comme vous ne connaissez probablement pas la configuration du r\u00e9seau, vous ne saurez pas choisir une adresse valide qui n'est pas utilis\u00e9e par quelqu'un d'autre. C'est le r\u00f4le du protocole DHCP (Dynamic Host Configuration Protocol) qui permet \u00e0 un p\u00e9riph\u00e9rique de demander une adresse IP. Le serveur DHCP va attribuer une adresse IP \u00e0 ce p\u00e9riph\u00e9rique pour une dur\u00e9e d\u00e9termin\u00e9e. Une fois allou\u00e9e, vous pouvez communiquer sur le r\u00e9seau.

    Le serveur DHCP va stocker dans une table la correspondance entre l'adresse MAC du p\u00e9riph\u00e9rique et l'adresse IP attribu\u00e9e. Ces informations sont compl\u00e9t\u00e9es par un bail DHCP (DHCP Lease) qui est la dur\u00e9e pendant laquelle l'adresse IP est attribu\u00e9e. Une fois le bail expir\u00e9, le p\u00e9riph\u00e9rique doit renouveler son bail pour continuer \u00e0 communiquer. Selon la configuration du serveur DHCP, il peut attribuer la m\u00eame adresse IP ou une nouvelle adresse. C'est pour cette raison que parfois votre adresse IP change toute seule.

    Ce que vous devez savoir en tant que d\u00e9veloppeur, c'est que vous pouvez soit disposer d'une adresse IP statique, c'est-\u00e0-dire que votre adresse est stock\u00e9e en dur dans la configuration de votre machine, soit d'une adresse IP dynamique, c'est-\u00e0-dire que votre adresse est attribu\u00e9e par un serveur DHCP. Dans la tr\u00e8s grande majorit\u00e9 des cas, c'est la deuxi\u00e8me option qui est utilis\u00e9e.

    La plupart du temps un serveur DHCP r\u00e9cup\u00e8re \u00e9galement le nom de l'h\u00f4te (hostname), c'est-\u00e0-dire le nom de la machine.

    "}, {"location": "course-c/48-network/protocoles/#arp", "title": "ARP", "text": "

    Nous avons vu qu'une fois dans un r\u00e9seau local et que chacun a une adresse IP, il est possible de communiquer. On profite de l'encapsulation des protocoles pour faire transiter les donn\u00e9es d'une machine \u00e0 une autre sans se soucier des couches inf\u00e9rieures. N\u00e9anmoins, il reste un probl\u00e8me \u00e0 r\u00e9soudre.

    IP est bas\u00e9 sur le protocole Ethernet. Chaque paquet IP est encapsul\u00e9 dans un paquet Ethernet lequel contient l'adresse MAC source et destination. Vous devez donc conna\u00eetre l'adresse MAC de la machine \u00e0 qui vous souhaitez envoyer des donn\u00e9es.

    C'est le protocole ARP (Address Resolution Protocol) qui permet de faire la correspondance entre une adresse IP et une adresse MAC. Lorsque vous souhaitez communiquer avec une machine, vous envoyez un message ARP en broadcast sur le r\u00e9seau (\u00e0 tout le monde). Toutes les machines du r\u00e9seau re\u00e7oivent ce message et celle qui poss\u00e8de l'adresse IP demand\u00e9e r\u00e9pond avec son adresse MAC.

    \u00c9videmment si dix-mille ordinateurs sont dans le m\u00eame sous r\u00e9seau, ils ne vont pas envoyer un message broadcast avant chaque transmission. Chaque ordinateur conserve sa propre table ARP qui contient les correspondances entre les adresses IP et les adresses MAC qu'il conna\u00eet. C'est seulement lorsque l'adresse MAC n'est pas dans la table ou lorsque l'adresse IP change qu'une requ\u00eate ARP sera envoy\u00e9e.

    ARP Flood

    Il existe une attaque informatique nomm\u00e9e ARP Flood qui consiste \u00e0 envoyer un grand nombre de requ\u00eates ARP sur un r\u00e9seau pour saturer les tables ARP des machines. Cela peut \u00eatre utilis\u00e9 pour emp\u00eacher les machines de communiquer entre elles.

    Puisque les requ\u00eates ARP sont diffus\u00e9es en broadcast, tous les appareils du r\u00e9seau les re\u00e7oivent.

    Heureusement les routeurs et les switchs modernes sont capables de d\u00e9tecter et de bloquer ce type d'attaque en limitant le nombre de requ\u00eates ARP par seconde ou en blocant les adresses MAC suspectes.

    Sous Linux vous pouvez consulter la table ARP avec la commande ip neigh show et sous Windows vous pouvez utiliser arp -a.

    "}, {"location": "course-c/48-network/protocoles/#icmp", "title": "ICMP", "text": "

    Le protocole ICMP (Internet Control Message Protocol) est un protocole qui permet de communiquer des messages de contr\u00f4le et d'erreur entre les machines. Par exemple, si vous essayez de communiquer avec une machine qui n'existe pas, vous allez recevoir un message ICMP de type Destination Unreachable.

    C'est ce protocole qui permet de faire des pings pour tester la connectivit\u00e9 entre deux machines. Le ping est un message ICMP de type Echo Request qui est envoy\u00e9 \u00e0 une machine. Si la machine est connect\u00e9e et qu'elle est configur\u00e9e pour r\u00e9pondre aux pings, elle va renvoyer un message ICMP de type Echo Reply.

    Dans la majorit\u00e9 des cas en ing\u00e9nierie et durant le d\u00e9veloppement, les machines sont configur\u00e9es pour r\u00e9pondre aux pings. Si vous connectez un Raspberry PI sur votre r\u00e9seau local, vous pourrez le pinger avec la commande ping 192.168.1.142 (o\u00f9 l'adresse IP est celle de votre Raspberry PI).

    Vous pouvez \u00e9galement envoyer une requ\u00eate ICMP sur un serveur public comme Google. On voit ci-dessous que le serveur r\u00e9pond aux pings et que le temps de r\u00e9ponse est d'environ 3 ms. On d\u00e9couvre \u00e9galement que l'adresse IP est 142.250.203.110

    \u279c ping google.com\nPING google.com (142.250.203.110) 56(84) bytes of data.\n64 bytes from zrh04s16.net (142.250.203.110): icmp_seq=1 ttl=111 time=3.38 ms\n64 bytes from zrh04s16.net (142.250.203.110): icmp_seq=2 ttl=111 time=3.35 ms\n^C\n--- google.com ping statistics ---\n2 packets transmitted, 2 received, 0% packet loss, time 2203ms\n

    Des services de localisation permettent de savoir o\u00f9 se trouve une adresse IP. Par exemple, l'adresse IP 142.250.203.110 est attribu\u00e9e \u00e0 Google et est situ\u00e9e \u00e0 Zurich en Suisse. Il s'agit de l'adresse d'un datacenter.

    Lorsque vous mettez un serveur priv\u00e9 sur internet. Vous voulez probablement qu'il ne r\u00e9ponde pas aux pings. En effet, un attaquant pourrait envoyer des pings \u00e0 des adresses IP al\u00e9atoires pour d\u00e9couvrir les serveurs actifs. Une fois qu'il sait que votre machine r\u00e9pond, il peut essayer de le pirater. D\u00e9sactiver le ping sous Linux se fait en modifiant le fichier /etc/sysctl.conf et en ajoutant la ligne net.ipv4.icmp_echo_ignore_all = 1.

    Traceroute

    Le protocole ICMP est \u00e9galement utilis\u00e9 par la commande traceroute qui permet de suivre le chemin emprunt\u00e9 par un paquet entre deux machines. La commande envoie des paquets ICMP avec un TTL (Time To Live) de 1, 2, 3, ... jusqu'\u00e0 ce que le paquet atteigne sa destination. Chaque routeur sur le chemin va d\u00e9cr\u00e9menter le TTL et lorsqu'il atteint 0, il va renvoyer un message ICMP de type Time Exceeded \u00e0 l'exp\u00e9diteur. C'est ainsi que l'on peut voir le chemin emprunt\u00e9 par un paquet entre deux machines.

    traceroute to 142.250.203.110, 30 hops max, 60 byte packets\n1  172.24.160.1  0.246 ms  0.216 ms  0.207 ms\n2  10.192.77.1  0.444 ms  0.535 ms  0.425 ms\n3  10.193.255.34  0.815 ms  0.806 ms  0.799 ms\n4  10.193.255.102 0.989 ms  0.977 ms  0.966 ms\n5  185.144.39.4  1.176 ms  1.164 ms  1.155 ms\n6  rchon01-te-0-0-2_81.as203130.ch  1.046 ms  1.253 ms  1.224 ms\n7  swiyv2-ge-0-0-0-0.hessoadm.ch  2.078 ms  1.937 ms  1.822 ms\n8  swiNE1-10GE-0-0-2-3.switch.ch  2.401 ms  2.179 ms  2.285 ms\n9  swiNE2-10GE-0-0-1-1.switch.ch  4.419 ms  4.409 ms  4.403 ms\n10  swiBI1-10GE-0-0-0-18.switch.ch  3.170 ms  2.996 ms  3.333 ms\n11  swiZH3-100GE-0-0-0-1.switch.ch  3.776 ms  3.660 ms  3.626 ms\n12  72.14.242.82  3.825 ms  3.817 ms  3.814 ms\n13  * * *\n14  172.253.50.20  4.565 ms 172.253.50.22 5.579 ms\n    zrh04s16-in-f14.1e100.net (142.250.203.110)  3.852 ms\n

    Le * * * indique que le routeur ne r\u00e9pond pas aux requ\u00eates ICMP. C'est une configuration courante pour les routeurs de gros fournisseurs de services internet. Ci-dessus on voit que de 1 \u00e0 4, les routeurs sont dans le r\u00e9seau local. 185.144.39.4 appartient \u00e0 la HEIG-VD puis elle transite par la HES-SO en 7 pour arriver chez Switch \u00e0 Zurich en 8. En 12 on voit que le paquet est transmis \u00e0 Google.

    ", "tags": ["traceroute"]}, {"location": "course-c/48-network/protocoles/#udp", "title": "UDP", "text": "

    Le protocole UDP (User Datagram Protocol) est un protocole de transport qui permet de transmettre des donn\u00e9es sans garantie de livraison. C'est un protocole simple qui ne g\u00e8re pas la retransmission des paquets perdus. Il est utilis\u00e9 pour les applications qui n'ont pas besoin d'une communication fiable. Par exemple, le protocole DNS utilise UDP pour transmettre les requ\u00eates de r\u00e9solution de noms de domaines. Les flux vid\u00e9os (streaming) utilisent \u00e9galement UDP pour transmettre les donn\u00e9es, car il n'est pas grave de perdre quelques images si cela permet de r\u00e9duire la latence et le trafic r\u00e9seau.

    Le protocole est relativement simple. Il est d\u00e9fini par la RFC 768 son en-t\u00eate est tr\u00e8s simple\u2009:

                      0      7 8     15 16    23 24    31\n                 +--------+--------+--------+--------+\n                 |          source address           |\n                 +--------+--------+--------+--------+\n                 |        destination address        |\n                 +--------+--------+--------+--------+\n                 |  zero  |   17   |   UDP length    |\n                 +--------+--------+--------+--------+\n                               Trame IP\n\n                  0      7 8     15 16    23 24    31\n                 +--------+--------+--------+--------+\n                 |   Source Port   |   Dest. Port    |\n                 +--------+--------+--------+--------+\n                 |     Length      |    Checksum     |\n                 +--------+--------+--------+--------+\n                 |                                   |\n                 |          Data (payload)           |\n                 |                                   |\n                 +-----------------------------------+\n                              Trame UDP\n

    On y trouve l'adresse IP source et destination, un champ r\u00e9serv\u00e9 \u00e0 z\u00e9ro, le protocole utilis\u00e9 (17 pour UDP), et la longueur du paquet sur 16 bits. Un paquet UDP \u00e0 donc une taille maximale de 65'535 octets (ou 65 Kio). Avec un en-t\u00eate de 8 octets, cela laisse 65'527 octets pour les donn\u00e9es. En pratique le MTU (Maximum Transmission Unit) des r\u00e9seaux Ethernet est de 1500 octets, c'est-\u00e0-dire qu'un paquet UDP plus grand sera fragment\u00e9 au niveau de la couche IP. Comme UDP n'a pas de m\u00e9canisme pour garantir la livraison des paquets, dans le cas o\u00f9 un fragment est perdu, l'int\u00e9gralit\u00e9 du paquet l'est \u00e9galement.

    L'en-t\u00eate UDP contient le port de source et le port de destination. On peut noter \u00e9galement que le paquet UDP contient un champ length correspondant \u00e0 la taille des donn\u00e9es et de l'en-t\u00eate. N\u00e9anmoins, on constate qu'il y a d\u00e9j\u00e0 cette information dans la trame IP. Cette redondance est le r\u00e9sultat de l'encapsulation des protocoles et de la s\u00e9paration des responsabilit\u00e9s. Cela cr\u00e9e de l'overhead, c'est-\u00e0-dire que la quantit\u00e9 d'information utile doit \u00eatre augment\u00e9e pour contenir les informations de routage et de contr\u00f4le. Au final c'est la m\u00eame chose avec les lettres papier. Le poids de l'encre par rapport au support (le papier, l'enveloppe, le timbre) et au camion du postier est tr\u00e8s faible.

    ", "tags": ["length"]}, {"location": "course-c/48-network/protocoles/#tcp", "title": "TCP", "text": "

    Le protocole TCP (Transmission Control Protocol) est un protocole de transport qui permet de transmettre des donn\u00e9es de mani\u00e8re fiable. On dit que le protocole est connect\u00e9 (connection-oriented), car il \u00e9tablit une connexion entre les deux machines avant de transmettre les donn\u00e9es. C'est un protocole complexe qui g\u00e8re la retransmission des paquets perdus, le contr\u00f4le de flux, le contr\u00f4le de congestion, etc.

    L'\u00e9tablissement d'une connexion TCP se fait avec un 3-way handshake (une poign\u00e9e de main \u00e0 trois). Le client envoie un paquet SYN (synchronize) \u00e0 la machine distante. La machine distante r\u00e9pond avec un paquet SYN-ACK (synchronize-acknowledge). Enfin, le client r\u00e9pond avec un paquet ACK (acknowledge) et la connexion est \u00e9tablie. Une fois la connexion \u00e9tablie, du point de vue d'un programme c'est comme si un tuyau avait \u00e9t\u00e9 pos\u00e9 entre les deux machines. Pour envoyer des donn\u00e9es, il suffit de les \u00e9crire dans le tuyau et elles sortiront de l'autre c\u00f4t\u00e9. Bien entendu les donn\u00e9es pourraient \u00eatre perdues, fragment\u00e9es en plusieurs paquets, \u00e9mises par c\u00e2ble sous-marin ou par satellite, mais pour le programme c'est transparent. Lui, il a la garantie que les donn\u00e9es arriveront dans l'ordre et sans erreur.

    C'est pourquoi le protocole TCP est le plus utilis\u00e9. De nombreux protocoles l'utilisent\u2009:

    • HTTP (port 80) pour les pages web\u2009;
    • HTTPS (port 443) pour les pages web s\u00e9curis\u00e9es\u2009;
    • FTP (port 21) pour le transfert de fichiers\u2009;
    • SSH (port 22) pour les connexions s\u00e9curis\u00e9e \u00e0 distance\u2009;
    • SMTP (port 25) pour l'envoi de courriels\u2009;
    • MQTT (port 1883) pour l'IoT, etc.
    "}, {"location": "course-c/48-network/protocoles/#dns", "title": "DNS", "text": "

    Le protocole DNS (Domain Name System) est un protocole bas\u00e9 sur UDP qui permet de faire la correspondance entre un nom de domaine et une adresse IP. Par exemple, lorsque vous tapez google.com dans votre navigateur, votre ordinateur va envoyer une requ\u00eate DNS pour obtenir l'adresse IP de Google. Une fois l'adresse IP obtenue, votre ordinateur va pouvoir communiquer avec le serveur de Google pour obtenir la page d'accueil.

    Les noms de domaines sont arbitr\u00e9s par l'ICANN (Internet Corporation for Assigned Names and Numbers) qui attribue les noms de domaines de premier niveau (TLD) comme .com, .org, .net, etc. Les noms de domaines de deuxi\u00e8me niveau (SLD) sont attribu\u00e9s par des registres de noms de domaines. Par exemple, le registre de noms de domaines pour la Suisse est SWITCH qui attribue les noms de domaines en .ch.

    Le protocole DNS est bas\u00e9 sur le protocole UDP et utilise le port 53. Lorsque vous voulez disposer de votre propre serveur, vous allez d\u00e9poser une requ\u00eate aupr\u00e8s votre NIC (Network Information Center) pour obtenir un nom de domaine. Vous allez ensuite configurer votre serveur DNS pour faire la correspondance entre votre nom de domaine et une adresse IP.

    On parle de r\u00e9solution de nom lorsque l'on veut obtenir l'adresse IP d'un nom de domaine et de r\u00e9solution inverse lorsque l'on veut obtenir le nom de domaine \u00e0 partir d'une adresse IP. Chaque institution dispose de son propre serveur DNS qui va g\u00e9rer les requ\u00eates pour les noms de domaines qu'il conna\u00eet. Depuis votre ordinateur assis \u00e0 votre bureau, vous pouvez envoyer une requ\u00eate DNS \u00e0 votre serveur DNS local, lequel va se charger de faire la r\u00e9solution pour vous. Il pourrait r\u00e9pondre directement ou bien il pourrait envoyer une requ\u00eate \u00e0 un serveur DNS de niveau sup\u00e9rieur. Il y a donc une hi\u00e9rarchie de serveurs DNS qui permet de r\u00e9soudre les noms de domaines. Le probl\u00e8me c'est combien de temps est-ce qu'une entr\u00e9e DNS est conserv\u00e9e en cache. Si vous changez l'adresse IP de votre serveur, vous allez mettre \u00e0 jour les informations du domaine, mais il faudra attendre que les caches DNS soient invalid\u00e9s pour que tout le monde puisse acc\u00e9der \u00e0 votre site. Ce param\u00e8tre est appel\u00e9 le TTL (Time To Live) et est configur\u00e9 par le propri\u00e9taire du domaine. Il est souvent de 24 heures pour les sites web, mais il peut \u00eatre plus court pour les services critiques.

    Une entr\u00e9e DNS peut \u00eatre de la forme suivante ou le nom de domaine HEIG-VD est associ\u00e9 \u00e0 l'adresse IP 193.134.219.72 avec un temps de vie de 86400 secondes (24 heures) :

    www.heig-vd.ch.  86400  IN  A  193.134.219.72\n

    Un serveur DNS peut \u00e9galement fonctionner au sein d'une institution pour g\u00e9rer les noms des serveurs internes. Par exemple, si vous avez un serveur de base de donn\u00e9es nomm\u00e9 eistore0 dans votre entreprise, vous pouvez configurer votre serveur DNS pour que ce nom soit associ\u00e9 \u00e0 l'adresse IP de votre serveur.

    ", "tags": ["google.com", "eistore0"]}, {"location": "course-c/48-network/protocoles/#http", "title": "HTTP", "text": "

    Le protocole HTTP (HyperText Transfer Protocol) est un protocole de la couche application qui permet de transf\u00e9rer des donn\u00e9es sur le web. Il est bas\u00e9 sur le protocole TCP et utilise le port 80. Le protocole est d\u00e9fini par la RFC 2616 et est un protocole texte. C'est-\u00e0-dire que les donn\u00e9es sont transmises en clair et que les en-t\u00eates sont des cha\u00eenes de caract\u00e8res.

    Lorsque vous demandez la page d'accueil de Google, votre navigateur va envoyer une requ\u00eate HTTP de type GET \u00e0 l'adresse http://www.google.com/. Cette requ\u00eate aura possiblement la forme suivante\u2009:

    GET / HTTP/1.1\nHost: www.google.com\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\nAccept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nUpgrade-Insecure-Requests: 1\n

    La r\u00e9ponse du serveur sera un message HTTP de type 200 OK avec le contenu de la page d'accueil.

    HTTP/1.1 200 OK\nDate: Tue, 15 Jun 2021 08:00:00 GMT\nServer: Apache/2.4.46 (Unix)\nLast-Modified: Tue, 15 Jun 2021 07:00:00 GMT\nContent-Length: 1234\nContent-Type: text/html\n\n<!DOCTYPE html>\n...\n

    Le code de retour 200 indique que la requ\u00eate a \u00e9t\u00e9 trait\u00e9e avec succ\u00e8s. Il existe de nombreux autres codes organis\u00e9s en cinq cat\u00e9gories\u2009:

    • 1xx\u2009: Information
    • 2xx\u2009: Succ\u00e8s
    • 3xx\u2009: Redirection
    • 4xx\u2009: Erreur client
    • 5xx\u2009: Erreur serveur

    On retrouve l'erreur 404 pour indiquer que la page n'a pas \u00e9t\u00e9 trouv\u00e9e, 403 pour indiquer que l'acc\u00e8s est interdit, 500 pour indiquer une erreur interne du serveur, etc. Plus amusant, on trouve le code 418 pour indiquer que le serveur est une th\u00e9i\u00e8re. C'est un code d'erreur qui n'est pas vraiment utilis\u00e9, mais qui est d\u00e9fini par la RFC 2324 (Hyper Text Coffee Pot Control Protocol).

    "}, {"location": "course-c/48-network/protocoles/#mqtt", "title": "MQTT", "text": "

    Le protocole MQTT (Message Queuing Telemetry Transport) est un protocole de messagerie bas\u00e9 sur le protocole TCP. Il est utilis\u00e9 pour les applications IoT (Internet of Things) pour transmettre des messages entre les capteurs et les serveurs. Le protocole est bas\u00e9 sur le principe de publication/abonnement. Un client peut publier un message sur un topic et un autre client peut s'abonner \u00e0 ce topic pour recevoir les messages. Le protocole est tr\u00e8s l\u00e9ger et est utilis\u00e9 pour les applications qui n\u00e9cessitent une faible consommation d'\u00e9nergie et une faible bande passante. La sp\u00e9cification du protocole est d\u00e9finie par la norme OASIS MQTT et le protocole utilise le port 1883.

    Par exemple un capteur de temp\u00e9rature peut publier un message sur le topic :

    /switzerland/vaud/weather/temperature/42\n

    avec la valeur 25.5 et un client peut s'abonner \u00e0 ce topic pour recevoir les valeurs de temp\u00e9rature en temps r\u00e9el. MQTT est beaucoup utilis\u00e9 pour les applications de l'internet des objets, car il permet de transmettre des messages de mani\u00e8re asynchrone et de mani\u00e8re fiable via un serveur interm\u00e9diaire nomm\u00e9 broker (courtier).

    "}, {"location": "course-c/48-network/sockets/", "title": "Sockets", "text": "

    Un socket (prise en fran\u00e7ais) est un point de communication entre deux processus sur un r\u00e9seau. Il permet l'\u00e9change de donn\u00e9es entre les processus en utilisant majoritairement les protocoles de communication TCP ou UDP.

    H\u00e9las, les sockets ne sont pas disponibles dans la biblioth\u00e8que standard de C, ils ne sont donc pas standardis\u00e9s et donc chaque syst\u00e8me d'exploitation a sa propre impl\u00e9mentation. Cependant, la plupart des syst\u00e8mes d'exploitation modernes supportent (avec de l\u00e9g\u00e8res variations) les sockets BSD (Berkeley Software Distribution).

    Un serveur web est un programme qui utilise les sockets TCP pour \u00e9couter les connexions entrantes et r\u00e9pondre aux requ\u00eates des clients. Un client web est un programme qui utilise les sockets TCP pour se connecter \u00e0 un serveur web et envoyer des requ\u00eates. Une base de donn\u00e9es MySQL est un programme qui expose un socket TCP pour permettre aux clients de se connecter et d'envoyer des requ\u00eates SQL. De m\u00eames, Docker expose un socket Unix pour permettre aux clients de se connecter et de contr\u00f4ler les conteneurs. Les sockets sont partout.

    "}, {"location": "course-c/48-network/sockets/#fonctionnement-des-sockets", "title": "Fonctionnement des sockets", "text": "

    Les sockets sont une couche d'abstraction de la communication entre deux processus (programmes), que ce soit sur le m\u00eame appareil ou sur des machines distantes. Le principe de base d'un socket est d'agir comme un canal de communication pour \u00e9changer des donn\u00e9es binaires. On qualifie souvent un programme de serveur s'il \u00e9coute les connexions entrantes et de client s'il initie une connexion. Une fois la connexion \u00e9tablie, les deux processus peuvent envoyer et recevoir des donn\u00e9es de mani\u00e8re bidirectionnelle.

    Les sockets sont identifi\u00e9s par une adresse IP et un num\u00e9ro de port. L'adresse IP identifie l'appareil sur le r\u00e9seau et le num\u00e9ro de port identifie le processus sur l'appareil. L'organisme IANA (Internet Assigned Numbers Authority) est responsable du maintien des affectations des num\u00e9ros de port pour les communications TCP et UDP. On notera que les ports de 0 \u00e0 1023 sont r\u00e9serv\u00e9s pour les services syst\u00e8me, les ports de 1024 \u00e0 49151 sont r\u00e9serv\u00e9s pour les applications utilisateur et les ports de 49152 \u00e0 65535 sont r\u00e9serv\u00e9s pour les connexions dynamiques (NAT / PAT / UPnP...).

    Le port de loin le plus utilis\u00e9 est le port 80 pour les connexions HTTP, suivi du port 443 pour les connexions HTTPS. Les ports 20 et 21 \u00e9taient utilis\u00e9s jadis pour les connexions FTP. Les services de messagerie (e-mail) utilisent les ports 25, 465 et 587 etc.

    Pour \u00e9tablir un socket, un programme commence par cr\u00e9er un socket avec la fonction socket(). C'est l\u00e0 qu'est d\u00e9fini la famille d'adresse (IPv4 ou IPv6), le type de socket (TCP/UDP) et le protocole. Le socket est ensuite li\u00e9 \u00e0 une adresse et \u00e0 un port sp\u00e9cifique. Une fois reli\u00e9, la fonction listen() permet d'\u00e9couter sur le socket. Un serveur \u00e9coute un socket lorsqu'il attend des connexions entrantes. Une fois une tentative de connexion d\u00e9tect\u00e9e, la fonction accept() est utilis\u00e9e pour \u00e9tablie la connexion et permettre l'\u00e9change de donn\u00e9es. Enfin, les fonctions read(), write() sont utilis\u00e9es pour \u00e9changer des donn\u00e9es. Lorsque la communication est termin\u00e9e, le socket est ferm\u00e9 avec close().

    "}, {"location": "course-c/48-network/sockets/#types-de-sockets", "title": "Types de sockets", "text": "

    Il existe principalement trois cat\u00e9gories de sockets encore utilis\u00e9es\u2009: AF_INET utilis\u00e9 pour les connexions IPv4, AF_INET6 utilis\u00e9 pour les connexions IPv6 et AF_UNIX Utilis\u00e9 pour les connexions Unix. Le pr\u00e9fixe AF signifie Address Family. Si l'on j\u00e8te un oeil \u00e0 <socket.h> on peut constater d'autres familles peu int\u00e9ressantes comme AF_AX25 utilis\u00e9 par les radio amateurs, AF_APPLETALK utilis\u00e9 par les r\u00e9seaux Macintosh entre 1985 et 1995 ou encore AF_AAL5 utilis\u00e9 par les r\u00e9seaux ATM de 1990 \u00e0 2000. On constate l'h\u00e9ritage de l'histoire des r\u00e9seaux dans les sockets.

    Une fois la famille choisie, il faut choisir le type de socket. Il existe trois types de sockets principaux\u2009: SOCK_STREAM pour une connexion TCP, c'est un socket de type flux orient\u00e9 connexion. Il garantit la livraison des donn\u00e9es dans l'ordre et sans perte. SOCK_DGRAM pour une connexion UDP. Il ne garantit pas la livraison des donn\u00e9es ni l'ordre. Il n'y a pas besoin de connexion pr\u00e9alable. Le type SOCK_RAW permet d'acc\u00e9der directement aux trames r\u00e9seau au niveau IP, au-dessus de la couche liaison. Utilis\u00e9 pour impl\u00e9menter des protocoles r\u00e9seau au niveau utilisateur ou pour des outils de diagnostic r\u00e9seau (comme ping, traceroute). Il est g\u00e9n\u00e9ralement utilis\u00e9 par des programmes ayant des privil\u00e8ges \u00e9lev\u00e9s (root, sudo). Les autres types de sockets sont moins utilis\u00e9s et ne m\u00e9ritent pas d'\u00eatre mentionn\u00e9s ici.

    ", "tags": ["AF_APPLETALK", "AF_AX25", "AF_AAL5", "sudo"]}, {"location": "course-c/48-network/sockets/#portabilite", "title": "Portabilit\u00e9", "text": "

    Comme il a \u00e9t\u00e9 \u00e9voqu\u00e9, les sockets sont une API de bas niveau et ne sont pas standardis\u00e9s. Chaque syst\u00e8me d'exploitation a sa propre impl\u00e9mentation des sockets. Heureusement, pour simplifier la portabilit\u00e9 entre syst\u00e8mes, il existe des biblioth\u00e8ques externes qui offrent une abstraction (oui, encore une...) des sockets. La biblioth\u00e8que la plus populaire est libuv qui a \u00e9t\u00e9 d\u00e9velopp\u00e9e initialement pour Node.js. Elle est aujourd'hui utilis\u00e9e par de nombreux projets open-source et est disponible pour la plupart des syst\u00e8mes d'exploitation.

    Lorsque vous d\u00e9veloppez un programme utilisant des sockets, il n'est pas recommand\u00e9 d'utiliser directement les appels syst\u00e8me, ou les fonctions sp\u00e9cifiques \u00e0 votre syst\u00e8me d'exploitation. Pr\u00e9f\u00e9rez utiliser une biblioth\u00e8que externe qui vous simplifiera la vie.

    Notons que si vous d\u00e9veloppez une application graphique avec SDL, GTK, Qt, etc. vous n'aurez pas besoin de g\u00e9rer les sockets directement. Ces biblioth\u00e8ques offrent des fonctions pour g\u00e9rer les connexions r\u00e9seau de mani\u00e8re plus simple et \u00e9galement portable.

    "}, {"location": "course-c/48-network/sockets/#creation-dun-socket", "title": "Cr\u00e9ation d'un socket", "text": "

    L'exercice du jour est la cr\u00e9ation de deux programmes\u2009: un serveur et un client, pour illustrer l'utilisation des sockets en C. Bien entendu cet exemple sera r\u00e9alis\u00e9 sur Linux, sans biblioth\u00e8que externe.

    Le serveur va \u00e9couter les connexions entrantes et le client va se connecter au serveur pour envoyer et recevoir des donn\u00e9es. Pour les besoins de l'exemple, le serveur va simplement r\u00e9pondre \u00e0 ping par pong.

    Voici le serveur\u2009:

    #include <netinet/in.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <unistd.h>\n\n#define PORT 8080\n\nint main() {\n   int server_fd;\n   if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {\n      perror(\"socket failed\");\n      exit(EXIT_FAILURE);\n   }\n\n   struct sockaddr_in address = {.sin_family = AF_INET,\n                                 .sin_addr.s_addr = INADDR_ANY,\n                                 .sin_port = htons(PORT)};\n   const int addrlen = sizeof(address);\n\n   if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {\n      perror(\"bind failed\");\n      exit(EXIT_FAILURE);\n   }\n\n   if (listen(server_fd, 3) < 0) {\n      perror(\"listen\");\n      exit(EXIT_FAILURE);\n   }\n\n   while (1) {\n      int new_socket;\n      if ((new_socket = accept(server_fd, (struct sockaddr *)&address,\n                               (socklen_t *)&addrlen)) < 0) {\n         perror(\"accept\");\n         exit(EXIT_FAILURE);\n      }\n\n      char buffer[1024] = {0};\n      char *response = \"pong\";\n      read(new_socket, buffer, 1024);\n      printf(\"Received: %s\\n\", buffer);\n      send(new_socket, response, strlen(response), 0);\n      printf(\"Response sent\\n\");\n\n      close(new_socket);\n   }\n}\n

    et voici le client\u2009:

    #include <arpa/inet.h>\n#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\n\n#define PORT 8080\nint main() {\n   int sock = 0;\n   if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {\n      printf(\"Socket creation error\\n\");\n      return -1;\n   }\n\n   struct sockaddr_in serv_addr;\n   serv_addr.sin_family = AF_INET;\n   serv_addr.sin_port = htons(PORT);\n\n   if (inet_pton(AF_INET, \"127.0.0.1\", &serv_addr.sin_addr) <= 0) {\n      printf(\"Invalid address / Address not supported\\n\");\n      return -1;\n   }\n\n   if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {\n      printf(\"Connection failed\\n\");\n      return -1;\n   }\n\n   const char *ping_msg = \"ping\";\n\n   char buffer[1024] = {0};\n   send(sock, ping_msg, strlen(ping_msg), 0);\n   printf(\"Ping sent\\n\");\n   int valread = read(sock, buffer, sizeof(buffer));\n   if (valread > 0) printf(\"Server: %s\\n\", buffer);\n\n   close(sock);\n}\n

    Un descripteur de fichier server_fd est cr\u00e9\u00e9 pour le serveur. Il s'agit d'un vrai file descriptor selon le principe de Unix\u2009:everything is a file. Le socket est cr\u00e9\u00e9 sur la famille IPv4 en mode TCP (SOCK_STREAM). Si le socket ne peut pas \u00eatre cr\u00e9\u00e9, la valeur -1 est retourn\u00e9e et le programme se termine.

    Ensuite une structure sockaddr_in est cr\u00e9\u00e9e pour d\u00e9finir l'adresse et le port du serveur. L'adresse est d\u00e9finie \u00e0 INADDR_ANY pour \u00e9couter sur toutes les interfaces r\u00e9seau. Le port est d\u00e9fini \u00e0 8080. La fonction bind() est utilis\u00e9e pour lier le socket \u00e0 l'adresse et au port. Si la fonction \u00e9choue, le programme se termine. Pour le type et la famille de socket choisie, nous utilisons ici la structure sockaddr_in qui est sp\u00e9cifique \u00e0 IPv4. Elle contiendra l'adresse IP et le port du serveur. le port 8080 utilis\u00e9 ici sera converti en big-endian avec la fonction htons() puisque le r\u00e9seau utilise le big-endian, et l'adresse IP est d\u00e9finie \u00e0 INADDR_ANY pour \u00e9couter sur toutes les interfaces r\u00e9seau.

    L'\u00e9tape suivante est de lier le socket \u00e0 l'adresse avec la fonction bind(). Si la fonction \u00e9choue, le programme se termine. Si une erreur doit se produire, c'est tr\u00e8s souvent le bind qui \u00e9choue. En effet, il ne peut y avoir qu'un seul programme \u00e9coutant sur un port donn\u00e9. Si un autre programme \u00e9coute d\u00e9j\u00e0 sur le port 8080, le bind \u00e9chouera avec l'erreur\u2009: Address already in use. C'est une erreur aga\u00e7ante parce qu'il faut chercher quel programme \u00e9coute sur ce port. La commande ss -tulnp permet de lister les programmes \u00e9coutant sur les ports TCP.

    La fonction listen() est ensuite utilis\u00e9e pour d\u00e9marrer l'\u00e9coute. Cette fonction prend en param\u00e8tre le descripteur de fichier du socket et la taille souhait\u00e9e de la file d'attente des connexions entrantes. La taille de la file d'attente est fix\u00e9e \u00e0 3 ici, donc trois clients pourraient \u00eatre en attente de connexion. Si un quatri\u00e8me client tente de se connecter, il recevra un message d'erreur.

    Lorsqu'une demande de connexion est d\u00e9tect\u00e9e, la fonction accept() est utilis\u00e9e pour accepter la connexion. Cette fonction retourne un nouveau descripteur de fichier new_socket qui est utilis\u00e9 pour envoyer et recevoir des donn\u00e9es. La fonction accept() est bloquante, c'est-\u00e0-dire qu'elle attend qu'une connexion soit \u00e9tablie. Si aucune connexion n'est en attente, le programme est mis en pause jusqu'\u00e0 ce qu'une connexion soit \u00e9tablie.

    La fonction read est similaire \u00e0 celle utilis\u00e9e pour lire un fichier. Elle prend en param\u00e8tre le descripteur de fichier, un tampon pour stocker les donn\u00e9es lues et la taille du tampon. Comme pour les fichiers et l'entr\u00e9e standard, la fonction est bloquante. Elle attend que des donn\u00e9es soient disponibles pour les lire. Si la connexion est ferm\u00e9e par le client, la fonction read retourne 0.

    Pourquoi avoir besoin d'un deuxi\u00e8me descripteur de fichier new_socket ? Parce que le socket server_fd est utilis\u00e9 pour \u00e9couter les connexions entrantes, mais une fois une connexion \u00e9tablie, il faut un nouveau socket pour \u00e9changer des donn\u00e9es avec le client. C'est le socket new_socket qui est utilis\u00e9 pour envoyer et recevoir des donn\u00e9es. Comme analogie server_fd est le bureau de r\u00e9ception o\u00f9 les clients arrivent pour se connecter. Il reste ouvert pour accepter de nouveaux clients et new_socket est comme la cabine t\u00e9l\u00e9phonique o\u00f9 le client et le serveur peuvent \u00e9changer des donn\u00e9es.

    ", "tags": ["ping", "read", "bind", "pong", "sockaddr_in", "server_fd", "new_socket", "INADDR_ANY"]}, {"location": "course-c/48-network/sockets/#exemple-portable", "title": "Exemple portable", "text": "

    Pour un exemple portable, nous allons utiliser la biblioth\u00e8que libuv. Cette biblioth\u00e8que est bas\u00e9e sur un mod\u00e8le de programmation asynchrone et \u00e9v\u00e9nementielle. C'est-\u00e0-dire que le programme ne bloque pas lorsqu'il attend une connexion ou des donn\u00e9es, mais qu'il fonctionne avec des callback. C'est un mod\u00e8le de programmation tr\u00e8s populaire pour les applications r\u00e9seau et les serveurs web.

    N\u00e9cessairement l'exemple donn\u00e9 est un peu plus complexe que l'exemple pr\u00e9c\u00e9dent. Il est cependant plus robuste et portable. Voici le serveur\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <uv.h>\n\n#define PORT 8080\n\nvoid alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {\n   buf->base = (char *)malloc(suggested_size);\n   buf->len = suggested_size;\n}\n\nvoid on_write(uv_write_t *req, int status) {\n   if (status) fprintf(stderr, \"Write error: %s\\n\", uv_strerror(status));\n   printf(\"Response sent\\n\");\n   uv_close((uv_handle_t *)req->handle, NULL);\n   free(req);\n}\n\nvoid on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {\n   if (nread > 0) {\n      printf(\"Received: %s\\n\", buf->base);\n      uv_buf_t wrbuf = uv_buf_init(\"pong\", 4);\n      uv_write_t *req = (uv_write_t *)malloc(sizeof(uv_write_t));\n      uv_write(req, client, &wrbuf, 1, on_write);\n   } else if (nread < 0) {\n      if (nread != UV_EOF)\n         fprintf(stderr, \"Read error: %s\\n\", uv_err_name(nread));\n      uv_close((uv_handle_t *)client, NULL);\n   }\n   free(buf->base);\n}\n\nvoid on_new_connection(uv_stream_t *server, int status) {\n   if (status < 0) {\n      fprintf(stderr, \"New connection error: %s\\n\", uv_strerror(status));\n      return;\n   }\n   uv_tcp_t *client = (uv_tcp_t *)malloc(sizeof(uv_tcp_t));\n   uv_tcp_init(uv_default_loop(), client);\n   if (uv_accept(server, (uv_stream_t *)client) == 0)\n      uv_read_start((uv_stream_t *)client, alloc_buffer, on_read);\n   else\n      uv_close((uv_handle_t *)client, NULL);\n}\n\nint main() {\n   uv_loop_t *loop = uv_default_loop();\n   uv_tcp_t server;\n   uv_tcp_init(loop, &server);\n   struct sockaddr_in addr;\n   uv_ip4_addr(\"0.0.0.0\", PORT, &addr);\n   uv_tcp_bind(&server, (const struct sockaddr *)&addr, 0);\n   int r = uv_listen((uv_stream_t *)&server, 128, on_new_connection);\n   if (r) {\n      fprintf(stderr, \"Error listening: %s\\n\", uv_strerror(r));\n      return 1;\n   }\n   printf(\"Server listening on port %d\\n\", PORT);\n   return uv_run(loop, UV_RUN_DEFAULT);\n}\n

    Et voici le client

    #include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <uv.h>\n\n#define PORT 8080\n\nuv_loop_t *loop;\nuv_tcp_t socket_client;\nuv_connect_t connect_req;\n\nvoid alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {\n   buf->base = (char *)malloc(suggested_size);\n   buf->len = suggested_size;\n}\n\nvoid on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {\n   if (nread > 0) {\n      printf(\"Server: %s\\n\", buf->base);\n   } else if (nread < 0) {\n      fprintf(stderr, \"Read error: %s\\n\", uv_err_name(nread));\n      uv_close((uv_handle_t *)client, NULL);\n   }\n\n   free(buf->base);\n}\n\nvoid on_write(uv_write_t *req, int status) {\n   if (status) fprintf(stderr, \"Write error: %s\\n\", uv_strerror(status));\n   printf(\"Ping sent to server\\n\");\n   uv_read_start((uv_stream_t *)req->handle, alloc_buffer, on_read);\n   free(req);\n}\n\nvoid on_connect(uv_connect_t *req, int status) {\n   if (status < 0) {\n      fprintf(stderr, \"Connection error: %s\\n\", uv_strerror(status));\n      return;\n   }\n   printf(\"Connected to server\\n\");\n\n   uv_write_t *write_req = (uv_write_t *)malloc(sizeof(uv_write_t));\n   const char *ping_msg = \"ping\";\n   uv_buf_t buffer = uv_buf_init((char *)ping_msg, strlen(ping_msg));\n   uv_write(write_req, req->handle, &buffer, 1, on_write);\n}\n\nint main() {\n   loop = uv_default_loop();\n   uv_tcp_init(loop, &socket_client);\n   struct sockaddr_in dest;\n   uv_ip4_addr(\"127.0.0.1\", PORT, &dest);\n   uv_tcp_connect(&connect_req, &socket_client, (const struct sockaddr *)&dest,\n                  on_connect);\n   return uv_run(loop, UV_RUN_DEFAULT);  // Boucle d'\u00e9v\u00e9nements\n}\n
    "}, {"location": "course-c/48-network/sockets/#erreurs-courantes", "title": "Erreurs courantes", "text": "

    Les erreurs les plus courantes lors de la cr\u00e9ation d'un socket sont\u2009:

    Address already in use

    Un autre programme \u00e9coute d\u00e9j\u00e0 sur le port sp\u00e9cifi\u00e9. Utilisez la commande ss -tulnp pour lister les programmes \u00e9coutant sur les ports TCP ou sous Windows netstat -aon. Essayez de changer de port.

    Permission denied

    Vous n'avez pas les droits pour \u00e9couter ou sur le port sp\u00e9cifi\u00e9. Essayez de lancer le programme avec les droits root ou sudo. Sous Windows, ex\u00e9cutez le programme en tant qu'administrateur et sous Linux, utilisez sudo. Il faut savoir que les ports de 0 \u00e0 1023 sont r\u00e9serv\u00e9s pour les services syst\u00e8me et n\u00e9cessitent des privil\u00e8ges \u00e9lev\u00e9s pour \u00eatre utilis\u00e9s. Si votre objectif est d'\u00e9crire un programme de test, utilisez un port sup\u00e9rieur \u00e0 1024. Dans le cas d'un socket Unix, v\u00e9rifiez que le fichier de socket n'existe pas d\u00e9j\u00e0 ou si vous vous connectez dessus, v\u00e9rifiez que vous avez les droits n\u00e9cessaires.

    Connection refused

    Le serveur n'accepte pas la connexion. Cela peut \u00eatre d\u00fb \u00e0 un pare-feu, \u00e0 une erreur dans le code du serveur ou \u00e0 une erreur dans l'adresse IP ou le port du client. Il faut investiguer pour trouver la cause.

    Le probl\u00e8me de l'adresse d\u00e9j\u00e0 utilis\u00e9e peut appara\u00eetre m\u00eame si le programme a \u00e9t\u00e9 arr\u00eat\u00e9. Cela est d\u00fb au fait que le syst\u00e8me d'exploitation conserve les sockets en \u00e9tat TIME_WAIT pendant un certain temps (entre 30 secondes et 2 minutes) apr\u00e8s la fermeture du programme. Cela permet de s'assurer que tous les paquets ont \u00e9t\u00e9 re\u00e7us et envoy\u00e9s. Si vous avez besoin de r\u00e9utiliser un port imm\u00e9diatement apr\u00e8s la fermeture du programme, vous pouvez utiliser l'option SO_REUSEADDR avec la fonction setsockopt() :

    int opt = 1;\nif (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {\n    perror(\"setsockopt failed\");\n    exit(EXIT_FAILURE);\n}\n

    Une autre solution est de forcer la fermeture du socket imm\u00e9diatement apr\u00e8s la fermeture du programme avec l'option SO_LINGER :

    struct linger so_linger;\nso_linger.l_onoff = 1;   // Activer SO_LINGER\nso_linger.l_linger = 0;  // Fermer imm\u00e9diatement sans d\u00e9lai\n\nif (setsockopt(server_fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)) < 0) {\n    perror(\"setsockopt failed\");\n    exit(EXIT_FAILURE);\n}\n

    Le d\u00e9lai d'attente sous Linux est d\u00e9termin\u00e9 par le param\u00e8tre net.ipv4.tcp_fin_timeout du noyau. Vous pouvez le modifier avec la commande sysctl -w net.ipv4.tcp_fin_timeout=30 pour le mettre \u00e0 30 secondes par exemple. Vous pouvez \u00e9galement le lire avec la commande sysctl net.ipv4.tcp_fin_timeout. Sous Ubuntu 24.04, le d\u00e9lai est de 60 secondes par d\u00e9faut.

    La derni\u00e8re solution est de lister les connexions en \u00e9tat TIME_WAIT avec la commande suivante et tuer les connexions qui posent probl\u00e8me\u2009:

    ss -o state time-wait '( sport = :8080 )'\n
    ", "tags": ["SO_REUSEADDR", "SO_LINGER", "root", "TIME_WAIT", "sudo"]}, {"location": "course-c/60-safety/introduction/", "title": "Introduction", "text": ""}, {"location": "course-c/60-safety/introduction/#securite-du-langage-c", "title": "S\u00e9curit\u00e9 du langage C", "text": "

    Le langage C est un langage de programmation qui permet de manipuler directement la m\u00e9moire de l'ordinateur. Cela permet de r\u00e9aliser des programmes tr\u00e8s performants, mais cela peut aussi \u00eatre dangereux. En effet, si un programme \u00e9crit en C contient une erreur, il peut provoquer des bugs, des plantages, voire des failles de s\u00e9curit\u00e9.

    Le langage n'est pas \u00e9quip\u00e9 de m\u00e9canismes de s\u00e9curit\u00e9 avanc\u00e9s comme le Java ou le C#, qui sont des langages de programmation plus modernes. Il n'y a pas de gestion de d\u00e9passerment de tampon, de v\u00e9rification de la m\u00e9moire, de v\u00e9rification des types, etc. Il est facile de jardiner la m\u00e9moire, c'est-\u00e0-dire d'\u00e9crire dans des zones m\u00e9moires qui ne nous appartiennent pas.

    Des langages plus modernes comme le Zig ou le Rust ont \u00e9t\u00e9 cr\u00e9\u00e9s pour pallier \u00e0 ces probl\u00e8mes. Ils sont plus s\u00e9curis\u00e9s, mais aussi plus complexes \u00e0 apprendre.

    Par ailleurs le C souffre de son anciennet\u00e9 et de l'imp\u00e9rieuse n\u00e9cessit\u00e9 de conserver une compatibilit\u00e9 ascendante. Cela signifie que des fonctionnalit\u00e9s obsol\u00e8tes ou dangereuses sont toujours pr\u00e9sentes dans le langage. Des correctifs ont \u00e9t\u00e9 apport\u00e9s principalement par l'ajout de nouvelles biblioth\u00e8ques ou fonctions.

    "}, {"location": "course-c/60-safety/introduction/#fonctions-dangereuses", "title": "Fonctions dangereuses", "text": "

    Vous avez peut \u00eatre vu des fonctions en C se terminant par le suffixe _s comme strcpy_s, strcat_s, sprintf_s, etc. Ces fonctions sont des versions s\u00e9curis\u00e9es des fonctions classiques strcpy, strcat, sprintf, etc. Elles v\u00e9rifient que la taille des buffers est suffisante pour contenir les donn\u00e9es \u00e0 copier. Si ce n'est pas le cas, elles ne copient pas les donn\u00e9es et retournent une erreur. Ces fonctions s\u00e9curis\u00e9es sont plus lentes que les fonctions classiques, mais elles sont plus s\u00fbres.

    Voici la liste des fonctions s\u00e9curis\u00e9es offertes par le langage, jusqu'\u00e0 C23\u2009:

    Fonctions s\u00e9curis\u00e9es du langage C Fonction Description strcpy_s Copie une cha\u00eene de caract\u00e8res dans un buffer, avec v\u00e9rification de la taille du buffer strcat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res avec v\u00e9rification de la taille du buffer sprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec v\u00e9rification de la taille du buffer strncpy_s Copie une cha\u00eene de caract\u00e8res dans un buffer avec une taille maximale strncat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res avec une taille maximale et v\u00e9rification de la taille du buffer snprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec une taille maximale et v\u00e9rification memcpy_s Copie une zone m\u00e9moire dans une autre zone m\u00e9moire, avec v\u00e9rification de la taille du buffer memmove_s Copie une zone m\u00e9moire dans une autre zone m\u00e9moire, m\u00eame si les zones se chevauchent, avec v\u00e9rification memset_s Remplit une zone m\u00e9moire avec une valeur donn\u00e9e, avec v\u00e9rification du tampon fopen_s Ouvre un fichier de mani\u00e8re s\u00e9curis\u00e9e freopen_s Rouvre un fichier existant de mani\u00e8re s\u00e9curis\u00e9e tmpfile_s Cr\u00e9e un fichier temporaire s\u00e9curis\u00e9 getc_s Lit un caract\u00e8re d'un fichier de mani\u00e8re s\u00e9curis\u00e9e fgets_s Lit une ligne de texte d'un fichier de mani\u00e8re s\u00e9curis\u00e9e, avec gestion de la taille du buffer fread_s Lit des blocs de donn\u00e9es d'un fichier dans un buffer s\u00e9curis\u00e9, avec v\u00e9rification des tailles strerror_s Renvoie un message d'erreur de mani\u00e8re s\u00e9curis\u00e9e bsearch_s Recherche un \u00e9l\u00e9ment dans un tableau tri\u00e9 de mani\u00e8re s\u00e9curis\u00e9e qsort_s Trie un tableau de mani\u00e8re s\u00e9curis\u00e9e fscanf_s Lit des donn\u00e9es format\u00e9es depuis un fichier de mani\u00e8re s\u00e9curis\u00e9e sscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e vsnprintf_s \u00c9crit une cha\u00eene de caract\u00e8res format\u00e9e dans un buffer avec une taille maximale et v\u00e9rification vsscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e vswscanf_s Lit des donn\u00e9es format\u00e9es depuis une cha\u00eene de caract\u00e8res de mani\u00e8re s\u00e9curis\u00e9e wcscpy_s Copie une cha\u00eene de caract\u00e8res larges dans un buffer, avec v\u00e9rification de la taille du buffer wcscat_s Concat\u00e8ne deux cha\u00eenes de caract\u00e8res larges avec v\u00e9rification de la taille du buffer wcsncpy_s Copie une cha\u00eene de caract\u00e8res larges dans un buffer avec une taille maximale strtok_s D\u00e9coupe une cha\u00eene de caract\u00e8re en tokens de mani\u00e8re s\u00e9curis\u00e9e", "tags": ["tmpfile_s", "memmove_s", "fopen_s", "strcat_s", "wcsncpy_s", "fgets_s", "memset_s", "strerror_s", "strtok_s", "getc_s", "strcpy", "vswscanf_s", "freopen_s", "fscanf_s", "strncat_s", "vsscanf_s", "sprintf_s", "vsnprintf_s", "strcpy_s", "memcpy_s", "bsearch_s", "sscanf_s", "sprintf", "fread_s", "snprintf_s", "wcscpy_s", "strncpy_s", "qsort_s", "wcscat_s", "strcat"]}, {"location": "course-c/60-safety/introduction/#gets", "title": "gets", "text": "

    La fonction gets est une fonction dangereuse qui a \u00e9t\u00e9 retir\u00e9e du langage avec C11 et marqu\u00e9e obsol\u00e8te en C99. Elle lit une ligne de texte depuis l'entr\u00e9e standard et la stocke dans un buffer. Le probl\u00e8me, entre autre partag\u00e9 par d'autres fonctions, est que la fonction ne v\u00e9rifie pas la taille du buffer, ce qui peut provoquer un d\u00e9passement de tampon. Il est recommand\u00e9 d'utiliser la fonction fgets \u00e0 la place, qui prend en param\u00e8tre la taille du buffer. Pour comprendre le probl\u00e8me, voici un exemple de code vuln\u00e9rable\u2009:

    #include <stdio.h>\n\nint main() {\n   char buffer[10];\n   char password[10];\n   gets(buffer);\n\n   printf(\"Buffer :   '%s'\\n\", buffer);\n   printf(\"Password : '%s'\\n\", password);\n}\n

    Dans cet exemple, l'utilisateur peut entrer une cha\u00eene de caract\u00e8res de plus de 10 caract\u00e8res, ce qui provoquera un d\u00e9passement de tampon. La fonction gets ne v\u00e9rifie pas la taille du buffer et \u00e9crira dans la m\u00e9moire adjacente, en l'occurrence la variable password. Cela peut \u00eatre exploit\u00e9 par un attaquant pour \u00e9crire du code malveillant dans la m\u00e9moire et ex\u00e9cuter ce code.

    ", "tags": ["fgets", "password", "gets"]}, {"location": "course-c/60-safety/introduction/#scanf", "title": "scanf", "text": "

    La fonction scanf est une autre fonction dangereuse comme gets ne v\u00e9rifie pas la taille du buffer. L'erreur classique se produit avec le format %s qui lit une cha\u00eene de caract\u00e8res sans v\u00e9rifier la taille du buffer. Une solution est d'utiliser %10s pour limiter la taille de la cha\u00eene \u00e0 10 caract\u00e8res plus le caract\u00e8re nul de fin de cha\u00eene.

    ", "tags": ["scanf", "gets"]}, {"location": "course-c/60-safety/introduction/#mauvaises-pratiques", "title": "Mauvaises pratiques", "text": "

    Cette section regroupe les mauvaises pratiques constat\u00e9es lors mon exp\u00e9rience professionnelle de d\u00e9veloppeurs ainsi que les pi\u00e8ges dans lesquels mes \u00e9tudiants tombent r\u00e9guli\u00e8rement.

    "}, {"location": "course-c/60-safety/introduction/#retour-de-scanf", "title": "Retour de scanf", "text": "

    Bon sang, c'est probablement ce que je r\u00e9p\u00e8te le plus souvent\u2009: Toujours v\u00e9rifier la valeur de retour de scanf. Cette fonction retroune le nombre d'\u00e9l\u00e9ments qui a \u00e9t\u00e9 lus, ou EOF si la fin du fichier est atteinte. Si vous attendez de lire 3 variables, vous devez v\u00e9rifier que le retour est bien \u00e9gal \u00e0 3. Sinon, vous avez un probl\u00e8me.

    int a, b, c;\nif (scanf(\"%d %d %d\", &a, &b, &c) != 3) {\n    fprintf(stderr, \"Erreur de lecture\\n\");\n    return 1;\n}\n
    ", "tags": ["EOF", "scanf"]}, {"location": "course-c/60-safety/introduction/#realloc", "title": "Realloc", "text": "

    La fonction realloc est une fonction dangereuse car elle peut \u00e9chouer et retourner NULL. Si vous ne stocker pas le retour de realloc dans une variable temporaire, vous allez perdre le pointeur sur la m\u00e9moire allou\u00e9e pr\u00e9c\u00e9demment et vous ne pourrez plus jamais la lib\u00e9rer. C'est pourquoi la valeur de retour de realloc doit toujours \u00eatre stock\u00e9es dans un pointeur temporaire et v\u00e9rifi\u00e9e.

    int *tab = malloc(10 * sizeof(int));\nif (tab == NULL) {\n    fprintf(stderr, \"Erreur d'allocation\\n\");\n    return 1;\n}\n\n{\n    int *tmp = realloc(tab, 20 * sizeof(int));\n    if (tmp == NULL) {\n        fprintf(stderr, \"Erreur de r\u00e9allocation\\n\");\n        free(tab);\n        return 1;\n    }\n    tab = tmp;\n}\n
    ", "tags": ["NULL", "realloc"]}, {"location": "course-c/60-safety/introduction/#erreur-et-free", "title": "Erreur et free", "text": "

    Lorsqu'une erreur survient dans une fonction, il est important de lib\u00e9rer les ressources allou\u00e9es avant de quitter la fonction. Si vous oubliez de lib\u00e9rer la m\u00e9moire allou\u00e9e, vous aurez une fuite de m\u00e9moire. Cela peut \u00eatre critique dans un programme qui tourne en continu, car la m\u00e9moire allou\u00e9e ne sera jamais lib\u00e9r\u00e9e et le programme finira par planter. Lorsque vous avez plusieurs \u00e9l\u00e9ments \u00e0 nettoyer, c'est l'une des seule fois o\u00f9 il est acceptable d'utiliser un goto:

    void function() {\n    if (test_error1) goto error;\n    // ...\n    if (test_error2) goto error;\n    // ...\n    if (test_error3) goto error;\n    // ...\n\n  error:\n    free(ptr1);\n    free(ptr2);\n    free(ptr3);\n    return;\n}\n
    ", "tags": ["goto"]}, {"location": "course-c/60-safety/introduction/#vulnerabilites", "title": "Vuln\u00e9rabilit\u00e9s", "text": "

    Ce chapitre aborde les vuln\u00e9rabilit\u00e9s classiques en C tel que le buffer overflow, le remote code execution, l'attaque par format de cha\u00eene, l'attaque de type, etc.

    "}, {"location": "course-c/60-safety/introduction/#buffer-overflow", "title": "Buffer overflow", "text": "

    Les attaquants peuvent exploiter les d\u00e9passements de tampon pour \u00e9craser les adresses de retour sur la pile, permettant ainsi l'ex\u00e9cution de code arbitraire. Il s'agit de l'une des failles de s\u00e9curit\u00e9 les plus critiques dans les programmes C. Par exemple, dans une fonction vuln\u00e9rable, l'attaquant peut injecter un shellcode dans le tampon et modifier l'adresse de retour pour pointer vers ce shellcode.

    "}, {"location": "course-c/60-safety/introduction/#remote-code-execution", "title": "Remote code execution", "text": "

    Les d\u00e9passements de tampon, mal g\u00e9r\u00e9s, peuvent permettre \u00e0 un attaquant de contr\u00f4ler \u00e0 distance le flux d'ex\u00e9cution d'un programme C, menant \u00e0 une ex\u00e9cution de code arbitraire \u00e0 distance.

    "}, {"location": "course-c/60-safety/introduction/#attaque-par-format-de-chaine", "title": "Attaque par format de cha\u00eene", "text": "

    En utilisant des cha\u00eenes de format mal s\u00e9curis\u00e9es dans des fonctions comme printf, un attaquant peut acc\u00e9der \u00e0 des zones de m\u00e9moire sensibles, voire ex\u00e9cuter du code arbitraire.

    char userInput[100];\nscanf(\"%s\", userInput);\nprintf(userInput);  // Vuln\u00e9rabilit\u00e9 de format\n
    ", "tags": ["printf"]}, {"location": "course-c/60-safety/introduction/#attaque-de-type", "title": "Attaque de type", "text": "

    L'attaque use-after-free consiste \u00e0 exploiter un pointeur qui pointe vers une zone m\u00e9moire qui a \u00e9t\u00e9 lib\u00e9r\u00e9e. L'attaquant peut alors r\u00e9allouer cette zone m\u00e9moire et \u00e9crire du code malveillant dedans.

    "}, {"location": "course-c/60-safety/introduction/#thread-safety", "title": "Thread Safety", "text": "

    Un programme ou une fonction est dit thread-safe lorsqu'il peut \u00eatre utilis\u00e9 simultan\u00e9ment par plusieurs threads sans risque de corruption de donn\u00e9es, d'interblocage ou de conditions de concurrence. En C, la gestion de la concurrence est laiss\u00e9e \u00e0 la charge du programmeur. Il est donc de la responsabilit\u00e9 du d\u00e9veloppeur de garantir la thread-safety de son code. N\u00e9anmoins le d\u00e9veloppeur n'a pas une ma\u00eetrise compl\u00e8te de son code code, certaines fonctions de la biblioth\u00e8que standard sont pr\u00e9compil\u00e9es et l'impl\u00e9mentation est g\u00e9n\u00e9ralement opaque \u00e0 l'utilisateur. Un certain nombre de ces fonctions ne sont pas thread-safe.

    En effet, certaines fonctions sont dites stateful, c'est-\u00e0-dire qu'elles utilisent des variables globales pour conserver un \u00e9tat entre les appels. Cela peut poser probl\u00e8me si plusieurs threads utilisent la m\u00eame fonction en m\u00eame temps. L'exemple ls plus \u00e9vident est la fonction rand qui utilise une variable globale pour conserver l'\u00e9tat du g\u00e9n\u00e9rateur de nombres pseudo-al\u00e9atoires. Chaque appel \u00e0 rand modifie cette variable globale et donc influence le r\u00e9sultat des appels suivants. Une autre fonction bien connue est strtok qui utilise \u00e9galement un \u00e9tat interne et qui n'est par cons\u00e9quent pas thread-safe. Pour cette derni\u00e8re, il est recommand\u00e9 d'utiliser la fonction strtok_r \u00e0 la place, qui est la version s\u00e9curis\u00e9e.

    Certains langages plus modernes comme le Rust ou le Go int\u00e8grent nativement la gestion de la concurrence dans le langage. En Rust, par exemple, le compilateur v\u00e9rifie \u00e0 la compilation que le code est thread-safe et ne contient pas de conditions de concurrence. En Go, la gestion de la concurrence est simplifi\u00e9e par l'utilisation de goroutines et de canaux, mais en C, il faut g\u00e9rer la concurrence manuellement.

    Ce qu'il faut retenir c'est que la majorit\u00e9 des fonctions de la biblioth\u00e8que standard du C ne sont pas thread-safe. Il est donc important de faire les recherches pr\u00e9alables avant de les utiliser dans un contexte multithread\u00e9.

    ", "tags": ["strtok_r", "rand", "strtok"]}, {"location": "course-c/60-safety/introduction/#normes", "title": "Normes", "text": "

    Dans le domaine du d\u00e9veloppement en C, la s\u00e9curit\u00e9 et la qualit\u00e9 du code sont r\u00e9gies par des normes rigoureuses qui visent \u00e0 assurer la fiabilit\u00e9 et la robustesse des syst\u00e8mes, notamment dans les secteurs critiques tels que l\u2019automobile, le m\u00e9dical et l\u2019a\u00e9rospatial. Ces normes permettent de pr\u00e9venir les erreurs fatales et de garantir un code de haute qualit\u00e9, en particulier dans des environnements o\u00f9 les cons\u00e9quences d\u2019une d\u00e9faillance peuvent \u00eatre graves. Elles d\u00e9finissent un ensemble de bonnes pratiques et de contraintes visant \u00e0 minimiser les comportements ind\u00e9finis, les vuln\u00e9rabilit\u00e9s, et \u00e0 maximiser la s\u00fbret\u00e9 fonctionnelle.

    "}, {"location": "course-c/60-safety/introduction/#misra-c", "title": "MISRA C", "text": "

    MISRA (Motor Industry Software Reliability Association) est un ensemble de r\u00e8gles de codage destin\u00e9es \u00e0 am\u00e9liorer la s\u00e9curit\u00e9 et la qualit\u00e9 du code en langage C, initialement d\u00e9velopp\u00e9 pour l'industrie automobile. Au fil des ann\u00e9es, MISRA C est devenu une r\u00e9f\u00e9rence non seulement dans le domaine de l\u2019automobile, mais aussi dans d\u2019autres industries o\u00f9 la s\u00e9curit\u00e9 est primordiale.

    Les r\u00e8gles de MISRA C ont pour but d\u2019\u00e9viter des constructions de code dangereuses, de limiter l\u2019utilisation de certaines fonctionnalit\u00e9s du langage C susceptibles de provoquer des comportements ind\u00e9finis, et d\u2019am\u00e9liorer la lisibilit\u00e9 et la maintenabilit\u00e9 du code. Il existe plusieurs versions de la norme (p. ex. : MISRA C:1998, MISRA C:2004, MISRA C:2012), chacune apportant des \u00e9volutions pour prendre en compte les nouvelles r\u00e9alit\u00e9s du d\u00e9veloppement logiciel.

    Les r\u00e8gles de MISRA C sont class\u00e9es en trois cat\u00e9gories\u2009:

    R\u00e8gles obligatoires (Mandatory)

    Elles doivent imp\u00e9rativement \u00eatre respect\u00e9es sans exception. Leur violation peut conduire \u00e0 des comportements ind\u00e9termin\u00e9s, des bugs critiques, voire des failles de s\u00e9curit\u00e9. Voici un exemple de r\u00e8gle obligatoire\u2009:

    Ne jamais utiliser la fonction dangereuse malloc() dans un contexte temps r\u00e9el sans v\u00e9rifier si la m\u00e9moire a bien \u00e9t\u00e9 allou\u00e9e.

    R\u00e8gles n\u00e9cessaires (Required)

    Ces r\u00e8gles doivent \u00eatre suivies, mais elles permettent une certaine flexibilit\u00e9 si des justifications solides sont fournies. Si une r\u00e8gle n\u2019est pas respect\u00e9e, il est imp\u00e9ratif de documenter la raison de l'\u00e9cart et de prouver que la s\u00e9curit\u00e9 n'en est pas compromise. Voici un exemple\u2009:

    Limiter l\u2019usage des conversions implicites de types pour \u00e9viter des erreurs de pr\u00e9cision.

    R\u00e8gles recommand\u00e9es (Advisory)

    Elles sont fortement encourag\u00e9es pour am\u00e9liorer la qualit\u00e9 g\u00e9n\u00e9rale du code, mais leur non-application n\u2019est pas forc\u00e9ment dangereuse, \u00e0 condition qu\u2019elle soit justifi\u00e9e, par exemple\u2009:

    Pr\u00e9f\u00e9rer l\u2019utilisation de macros \u00e0 la place des constantes magiques dans le code pour am\u00e9liorer la lisibilit\u00e9.

    Un concept cl\u00e9 de la norme MISRA C est la matrice de justification. Lorsqu\u2019une r\u00e8gle Required ou Advisory n\u2019est pas respect\u00e9e, il est indispensable d\u2019expliquer pourquoi et de justifier la d\u00e9cision. Cette documentation pr\u00e9cise pourquoi l\u2019\u00e9cart est acceptable et quelles pr\u00e9cautions ont \u00e9t\u00e9 prises pour att\u00e9nuer les risques associ\u00e9s.

    Par exemple, si une \u00e9quipe de d\u00e9veloppement choisit de ne pas respecter une r\u00e8gle concernant l'utilisation d'une fonction sp\u00e9cifique dans un syst\u00e8me embarqu\u00e9 critique, une analyse approfondie doit \u00eatre effectu\u00e9e pour d\u00e9montrer que cette d\u00e9cision n\u2019aura pas d\u2019impact n\u00e9gatif sur la s\u00e9curit\u00e9 globale du syst\u00e8me.

    MISRA C favorise donc une approche pragmatique o\u00f9 l\u2019objectif n\u2019est pas de suivre aveugl\u00e9ment les r\u00e8gles, mais de les appliquer intelligemment, en justifiant tout \u00e9cart d\u2019une mani\u00e8re rigoureuse et tra\u00e7able.

    Certaines r\u00e8gles MISRA peuvent sembler restrictives ou contraignantes et ne servir ni la lisibilit\u00e9 du code, ni la s\u00e9curit\u00e9. Par exemple, la r\u00e8gle recommand\u00e9e MISRA-C:2012 15.5 d\u00e9cr\u00e8te que\u2009: A function should have a single point of exit at the end. Dit autremement, une fonction ne peut pas avoir plusieurs return.

    Le point de retour unique est requis par la norme IEC 61508, qui concerne les syst\u00e8mes embarqu\u00e9s critiques ainsi que l'ISO 26262 pour l'industrie automobile. L'argumentaire est qu'un retour pr\u00e9matur\u00e9 peut mener \u00e0 des omission involontaires de nettoyage de la m\u00e9moire ou de lib\u00e9ration de ressources.

    Ces r\u00e8gles, nombreuses, ont des rationels toujours fond\u00e9s mais qui peuvent parfois \u00eatre en contradiction avec les bonnes pratiques de d\u00e9veloppement moderne. Prenons l'exemple suivant de cette fonction qui n'a qu'un point de retour. Elle implique un sch\u00e9ma plus compliqu\u00e9 et une variable suppl\u00e9mentaire pour stocker le r\u00e9sultat de retour.

    int foo(FILE *fp, char *buffer, size_t size) {\n    int error = 0;\n    if (fp != NULL) {\n        if (buffer != NULL) {\n            if (size > 0) {\n                fread(buffer, 1, size, fp);\n            } else {\n                error = 4;\n            }\n        } else {\n            error = 3;\n        }\n    } else {\n        error = 2;\n    }\n    return error;\n}\n

    En s'autorisant plusieurs points de retour, le code est plus lisible et plus concis\u2009:

    int foo(FILE *fp, char *buffer, size_t size) {\n    if (fp == NULL) return 2;\n    if (buffer == NULL) return 3;\n    if (size == 0) return 4;\n    fread(buffer, 1, size, fp);\n    return 0;\n}\n

    L'objectif n'est pas ici de vous monter contre MISRA-C qui est une excellente norme, mais de vous montrer qu'il est important d'avoir un esprit critque et de ne pas suivre aveugl\u00e9ment les r\u00e8gles. Cette r\u00e8gle en question n'est que recommand\u00e9e, la norme vous pousse \u00e0 vous interroger sur la pertinence de l'appliquer ou non, de s'assurer que vous comprenez les risques et les b\u00e9n\u00e9fices.

    Voici pour information quelques r\u00e8gles de la norme MISRA C:2012\u2009:

    Exemples de r\u00e8gles MISRA C:2012 R\u00e8gle Description Cat\u00e9gorie Rule 1.1 Les fichiers source ne doivent pas contenir de code non standard Mandatory Rule 1.2 Les fichiers source doivent \u00eatre conformes \u00e0 la norme ISO 9899:1999 Mandatory Rule 2.1 Tout code inutile doit \u00eatre supprim\u00e9 Required Rule 8.7 Les objets non utilis\u00e9s doivent \u00eatre supprim\u00e9s Required Rule 10.1 Les types de donn\u00e9es ne doivent pas \u00eatre m\u00e9lang\u00e9s dans les expressions Required Rule 11.3 Un cast entre des pointeurs de types diff\u00e9rents ne doit pas \u00eatre effectu\u00e9 Required Rule 14.3 Les contr\u00f4les de boucles doivent \u00eatre constants et d\u00e9finis Required Rule 15.5 Il doit y avoir une clause default dans chaque instruction switch Required Rule 16.7 Les arguments de fonction ne doivent pas \u00eatre ignor\u00e9s Required Rule 17.2 Les index des tableaux doivent \u00eatre dans les limites du tableau Required Rule 18.1 La m\u00e9moire dynamique (malloc, free) ne doit pas \u00eatre utilis\u00e9e Required Rule 8.13 Les variables automatiques doivent \u00eatre initialis\u00e9es avant utilisation Advisory Rule 17.5 Ne pas acc\u00e9der directement \u00e0 un tableau avec des pointeurs Advisory Rule 20.4 Ne pas utiliser printf, scanf, sprintf de la biblioth\u00e8que standard Advisory", "tags": ["malloc", "return", "free", "printf", "default", "switch", "scanf", "sprintf"]}, {"location": "course-c/60-safety/introduction/#iso-26262", "title": "ISO 26262", "text": "

    La norme ISO 26262 est une norme internationale pour la s\u00e9curit\u00e9 fonctionnelle (functional safety) dans l'industrie automobile. Elle couvre le cycle de vie entier du d\u00e9veloppement des syst\u00e8mes embarqu\u00e9s dans les v\u00e9hicules, de la conception initiale \u00e0 la production et la maintenance. Cette norme est cruciale dans les v\u00e9hicules modernes o\u00f9 l\u2019\u00e9lectronique et le logiciel jouent un r\u00f4le fondamental dans la s\u00e9curit\u00e9 (syst\u00e8mes d\u2019assistance \u00e0 la conduite, gestion moteur, etc.).

    Les exigences cl\u00e9s de cette norme sont\u2009:

    Classification ASIL (Automotive Safety Integrity Level)

    Elle indique le niveau de criticit\u00e9 du syst\u00e8me. Il est class\u00e9 de A (le plus faible) \u00e0 D (le plus critique), et les exigences de d\u00e9veloppement augmentent avec le niveau de criticit\u00e9. Par exemple, un syst\u00e8me de freinage ABS est class\u00e9 ASIL D, car une d\u00e9faillance de ce syst\u00e8me peut entra\u00eener des accidents graves.

    Tests et couverture du code

    La norme impose la r\u00e9alisation de tests rigoureux pour garantir que le code est exempt de bugs critiques. Un niveau de couverture du code \u00e9lev\u00e9 est exig\u00e9, souvent plus de 95 pour cent, avec des tests unitaires, des tests de stress et des tests d\u2019int\u00e9gration qui couvrent aussi bien les chemins normaux que les cas extr\u00eames.

    V\u00e9rification et validation

    Un processus rigoureux de v\u00e9rification et de validation doit \u00eatre mis en place pour s\u2019assurer que toutes les exigences de s\u00e9curit\u00e9 sont respect\u00e9es. Cela inclut des revues de code, des tests de s\u00e9curit\u00e9, des analyses de risques, etc.

    Rappelez-vous, d\u00e9velopper du code pour l'automobile c'est lent, c'est cher, c'est compliqu\u00e9. C'est lent car chaque changement doit \u00eatre valid\u00e9, chaque ligne de code doit \u00eatre test\u00e9e. C'est cher car les tests sont co\u00fbteux et les outils sont chers (parce qu'ils sont certifi\u00e9s). C'est compliqu\u00e9 car il faut respecter des normes, des standards, des processus.

    "}, {"location": "course-c/60-safety/introduction/#iec-62304", "title": "IEC 62304", "text": "

    La norme IEC 62304 est une norme internationale d\u00e9di\u00e9e aux logiciels de dispositifs m\u00e9dicaux. Elle sp\u00e9cifie les processus de d\u00e9veloppement, de maintenance et de gestion des risques associ\u00e9s aux logiciels critiques utilis\u00e9s dans les appareils m\u00e9dicaux, tels que les syst\u00e8mes de monitoring, les appareils d\u2019imagerie m\u00e9dicale ou encore les pacemakers.

    Elle est tr\u00e8s similaires \u00e0 l'ISO 26262, mais adapt\u00e9e aux dispositifs m\u00e9dicaux. Les exigences de s\u00e9curit\u00e9 sont tout aussi strictes, car une d\u00e9faillance d\u2019un logiciel m\u00e9dical peut avoir des cons\u00e9quences dramatiques pour les patients. La classification m\u00e9dicale est similaire \u00e0 l'ASIL de l'ISO 26262, allant de A (le moins critique) \u00e0 C (le plus critique).

    "}, {"location": "course-c/60-safety/introduction/#compilateur-certifie", "title": "Compilateur certifi\u00e9", "text": "

    Dans le contexte de l\u2019application des normes comme l\u2019ISO 26262 et l\u2019IEC 62304, le choix du compilateur est critique. Un compilateur non certifi\u00e9 peut introduire des comportements non d\u00e9terministes ou des optimisations dangereuses qui ne respectent pas les contraintes de s\u00fbret\u00e9.

    Contrairement \u00e0 ce que l\u2019on pourrait penser, des compilateurs populaires comme GCC ou Clang ne sont pas certifi\u00e9s pour une utilisation dans des syst\u00e8mes critiques n\u00e9cessitant une conformit\u00e9 aux normes de s\u00e9curit\u00e9. Cela signifie que, bien qu\u2019ils soient extr\u00eamement performants et largement utilis\u00e9s dans l\u2019industrie g\u00e9n\u00e9rale, ils ne peuvent pas \u00eatre utilis\u00e9s tels quels dans des syst\u00e8mes r\u00e9pondant \u00e0 des normes comme l\u2019ISO 26262 ou l\u2019IEC 62304.

    Pour les projets critiques, des compilateurs certifi\u00e9s tels que Green Hills, IAR Systems, ou Tasking sont souvent utilis\u00e9s. Ces compilateurs sont conformes aux exigences de s\u00e9curit\u00e9, et leur comportement est rigoureusement v\u00e9rifi\u00e9 pour garantir qu\u2019ils ne produisent pas de code incorrect ou dangereux dans des environnements critiques. Ces compilateurs offrent \u00e9galement des fonctionnalit\u00e9s de suivi et d\u2019audit qui facilitent la conformit\u00e9 avec les normes de s\u00e9curit\u00e9. Ils int\u00e8grent g\u00e9n\u00e9ralement MISRA C et d\u2019autres r\u00e8gles de codage directement.

    "}, {"location": "course-c/70-philosophy/funny/", "title": "Humour d'informaticien", "text": "There are only two kinds of programming languages: those people always bitch about and those nobody uses.Bjarne Stroustrup

    L'humour des informaticiens, souvent empreint de subtilit\u00e9 et d'ironie, s'est peu \u00e0 peu impos\u00e9 comme un langage \u00e0 part enti\u00e8re au sein de la communaut\u00e9 des d\u00e9veloppeurs. Popularis\u00e9 par des personnalit\u00e9s et des \u0153uvres embl\u00e9matiques telles que XKCD, le c\u00e9l\u00e8bre webcomic de Randall Munroe, cet humour n'est pas simplement un moyen de divertissement, mais un outil pr\u00e9cieux pour les professionnels du code. D'autres figures marquantes, comme The Oatmeal, Dilbert ou encore CommitStrip, ont \u00e9galement su capturer l'essence de l'exp\u00e9rience informatique, transformant les d\u00e9fis techniques et les absurdit\u00e9s bureaucratiques en sources de rires partag\u00e9s.

    Ce type d'humour, bienveillant et souvent auto-d\u00e9risoire, joue un r\u00f4le fondamental dans la vie d'un d\u00e9veloppeur. Il permet de prendre du recul face \u00e0 la complexit\u00e9 et \u00e0 la frustration inh\u00e9rentes au m\u00e9tier. Les blagues sur les bugs r\u00e9calcitrants, les interminables r\u00e9unions ou les fonctions improbables deviennent des catalyseurs de coh\u00e9sion au sein des \u00e9quipes. En riant ensemble de leurs d\u00e9boires quotidiens, les d\u00e9veloppeurs cr\u00e9ent un environnement de travail plus l\u00e9ger et plus humain, o\u00f9 l'erreur n'est pas un \u00e9chec, mais une \u00e9tape normale du processus d'apprentissage.

    Cet humour est \u00e9galement une forme d'\u00e9vasion, un moyen de d\u00e9dramatiser les situations stressantes et de rappeler \u00e0 chacun que, malgr\u00e9 la rigueur technique n\u00e9cessaire \u00e0 leur travail, il est important de ne pas se prendre trop au s\u00e9rieux. Il favorise la cr\u00e9ativit\u00e9, l'innovation et un \u00e9tat d'esprit ouvert, essentiels \u00e0 la r\u00e9solution de probl\u00e8mes complexes. En somme, l'humour des informaticiens est bien plus qu'une simple plaisanterie\u2009: c'est un levier de bien-\u00eatre et de productivit\u00e9, au c\u0153ur m\u00eame de la culture du d\u00e9veloppement logiciel.

    Voici quelques exemples de blagues et de comics qui illustrent ces propos.

    "}, {"location": "course-c/70-philosophy/funny/#une-nouvelle-norme", "title": "Une nouvelle norme\u2009?", "text": "

    Parfois un d\u00e9veloppeur a une id\u00e9e brillante pour une nouvelle technologie, un nouveau protocol de communication, un nouveau format de fichier, etc. Il est persuad\u00e9 que ce nouveau standard plus simple, plus efficace, plus rapide, plus s\u00e9curis\u00e9, etc. va r\u00e9volutionner le monde de l'informatique et remplacer de sucro\u00eet plusieurs standards existants.

    H\u00e9las, le constat est souvent le m\u00eame\u2009: le nouveau standard ne sera qu'un \u00e9ni\u00e8me standard \u00e0 ajouter \u00e0 la liste des standards existants.

    XKCD Standards

    "}, {"location": "course-c/70-philosophy/funny/#unicode_1", "title": "Unicode", "text": "

    Unicode est dirig\u00e9 par un consortium de grandes entreprises technologiques et de parties prenantes. Les fondateurs d'Unicode comprennent Joe Becker, qui travaillait pour Xerox dans les ann\u00e9es 80. Il avait une barbe et il se pourrait bien qu'il est le personnage figurant dans le premier et le troisi\u00e8me panneaux.

    Les emoji ont \u00e9t\u00e9 initialement ajout\u00e9s pour \u00eatre compatibles avec les encodages de messages texte au Japon. Mais ce n'est plus le cas. L'emoji homard (\ud83e\udd9e) a \u00e9t\u00e9 approuv\u00e9 en 2018, date de sortie de ce comics.

    XKCD Unicode

    "}, {"location": "course-c/70-philosophy/funny/#vrais-programmeurs", "title": "Vrais programmeurs", "text": "

    Un rappel \u00e0 aux guerres d'\u00e9diteurs de texte, en particulier Vim et Emacs... De toute mani\u00e8re Vim \u00e0 gagn\u00e9 la guerre, non\u2009?

    XKCD Real Programmers

    "}, {"location": "course-c/70-philosophy/funny/#compilation", "title": "Compilation", "text": "

    Compiler un programme est une t\u00e2che qui peut prendre du temps. Parfois, il est n\u00e9cessaire de compiler un programme plusieurs fois pour obtenir un programme fonctionnel. Selon la taille du programme, le d\u00e9veloppeur peut avoir le temps de faire une pause ou plusieurs, de prendre un caf\u00e9 ou plusieurs, de lire un livre, ou de justifier n'importe quelle autre activit\u00e9...

    XKCD Compiling

    "}, {"location": "course-c/70-philosophy/funny/#le-code-des-autres", "title": "Le code des autres", "text": "

    Le code des autres est souvent difficile \u00e0 comprendre. Parfois, il est m\u00eame difficile de comprendre son propre code. C'est pourquoi il est important de commenter son code, de le documenter, de le tester, de le relire, de le refactoriser, etc.

    Le bon code

    "}, {"location": "course-c/70-philosophy/funny/#la-faute-a-personne", "title": "La faute \u00e0 personne", "text": "

    La collaboration au sein d'une \u00e9quipe de d\u00e9veloppement est cruciale pour le succ\u00e8s d'un projet. Lorsque chaque membre se contente de respecter les sp\u00e9cifications des API sans communication r\u00e9elle, les composants logiciels risquent de ne pas fonctionner ensemble. Chacun pense avoir bien fait, et le bl\u00e2me se d\u00e9place de l'un \u00e0 l'autre, tandis que personne ne veut corriger le probl\u00e8me. Ce dilemme, souvent li\u00e9 aux co\u00fbts du travail, rappelle que la solution ne r\u00e9side pas dans la recherche d'un coupable, mais dans un compromis et une coop\u00e9ration pour le bien commun.

    La faute \u00e0 personne

    "}, {"location": "course-c/70-philosophy/funny/#commentaire-de-commit", "title": "Commentaire de commit", "text": "

    La tentation est grande de miniser le temps pass\u00e9 \u00e0 r\u00e9diger des commentaires de commit. Pourtant, ces messages sont essentiels pour comprendre l'\u00e9volution du code, pour suivre les modifications apport\u00e9es, pour identifier les erreurs, etc. Un bon commentaire de commit est clair, concis, informatif et utile. Il permet de retracer l'historique du code, de faciliter la collaboration entre les d\u00e9veloppeurs, de documenter les changements, etc.

    Commentaire de commit

    "}, {"location": "course-c/70-philosophy/philosophy/", "title": "Philosophie", "text": "

    La philosophie d'un bon d\u00e9veloppeur repose sur plusieurs principes de programmation relevant majoritairement du bon sens de l'ing\u00e9nieur. Les vaudois l'appelant parfois\u2009: le bon sens paysan comme l'aurait sans doute confirm\u00e9 Jean Villard dit Gilles.

    "}, {"location": "course-c/70-philosophy/philosophy/#rasoir-dockham", "title": "Rasoir d'Ockham", "text": "

    Illustration humoristique du rasoir d'Ockham

    Le rasoir d'Ockham expose en substance que les multiples ne doivent pas \u00eatre utilis\u00e9s sans n\u00e9cessit\u00e9. C'est un principe d'\u00e9conomie, de simplicit\u00e9 et de parcimonie. Il peut \u00eatre r\u00e9sum\u00e9 par la devise Shadok, non sans une pointe d'ironie\u2009: \u00ab\u2009Pourquoi faire simple quand on peut faire compliqu\u00e9\u2009?\u2009\u00bb

    En philosophie, un rasoir est une m\u00e9thode heuristique visant \u00e0 \u00e9liminer les explications invraisemblables d'un ph\u00e9nom\u00e8ne donn\u00e9. Ce principe tire son nom de Guillaume d'Ockham, un penseur du XIVe si\u00e8cle, bien que son origine remonte probablement \u00e0 Emp\u00e9docle (\u1f18\u03bc\u03c0\u03b5\u03b4\u03bf\u03ba\u03bb\u1fc6\u03c2), aux environs de 450 avant J.-C.

    Ce principe trouve une r\u00e9sonance particuli\u00e8re en programmation, domaine o\u00f9 le d\u00e9veloppeur ne peut appr\u00e9hender la totalit\u00e9 d'un logiciel, intrins\u00e8quement insaisissable \u00e0 l'\u0153il humain. Seuls la simplicit\u00e9 et l'art de la conception logicielle peuvent le pr\u00e9server du chaos, car un programme, quel qu'en soit l'envergure, peut demeurer limpide pour peu que chaque strate de son architecture reste claire et intelligible pour quiconque souhaiterait contribuer \u00e0 l'\u0153uvre d'autrui.

    "}, {"location": "course-c/70-philosophy/philosophy/#leffet-dunning-kruger", "title": "L'effet Dunning-Kruger", "text": "

    L' est un biais cognitif qui se manifeste par une surestimation des comp\u00e9tences d'une personne. Les personnes les moins comp\u00e9tentes dans un domaine ont tendance \u00e0 surestimer leurs comp\u00e9tences, tandis que les personnes les plus comp\u00e9tentes ont tendance \u00e0 les sous-estimer.

    L'effet Dunning-Kruger est un biais cognitif o\u00f9 les individus tendent \u00e0 surestimer leurs comp\u00e9tences. Paradoxalement, ce sont les moins exp\u00e9riment\u00e9s dans un domaine qui s'illusionnent le plus sur leurs capacit\u00e9s, tandis que les experts, eux, ont tendance \u00e0 sous-\u00e9valuer leur ma\u00eetrise.

    Illustration satirique de l'effet Dunning-Kruger

    J'ai souvent observ\u00e9 ce biais de surconfiance chez mes \u00e9tudiants et coll\u00e8gues, et je n'en ai pas \u00e9t\u00e9 exempt moi-m\u00eame. Il est en effet difficile de jauger avec pr\u00e9cision son propre niveau de comp\u00e9tence, et ce n'est qu'en se confrontant au regard critique de ses pairs que l'on prend v\u00e9ritablement conscience de ses lacunes. Soumettre son code \u00e0 l'examen d'autrui peut \u00eatre une d\u00e9marche intimidante, mais elle s'av\u00e8re \u00eatre une source d'enrichissement inestimable.

    Note

    L'effet Dunning-Kruger ne fait pas consensus au sein de la communaut\u00e9 scientifique, mais il est souvent cit\u00e9 en psychologie populaire.

    "}, {"location": "course-c/70-philosophy/philosophy/#ultracrepidarianisme", "title": "Ultracr\u00e9pidarianisme", "text": "

    L'ultracr\u00e9pidarianisme, terme rare mais puissant, d\u00e9signe l'art de s'exprimer avec assurance sur des sujets que l'on ne ma\u00eetrise gu\u00e8re. Ce ph\u00e9nom\u00e8ne, aussi ancien que la parole elle-m\u00eame, se manifeste lorsque des individus, ignorants des nuances et des subtilit\u00e9s d'un domaine, s'\u00e9rigent en experts. \u00c9tienne Klein, physicien et philosophe des sciences, a popularis\u00e9 ce mot en France, mettant en garde contre les dangers de cette posture, notamment \u00e0 l'\u00e9poque du Covid-19, o\u00f9 les voix des v\u00e9ritables sp\u00e9cialistes furent souvent noy\u00e9es par le vacarme de ceux qui, en sachant moins, parlaient davantage.

    Dans le domaine de l'informatique, ce travers est particuli\u00e8rement pr\u00e9gnant. Les d\u00e9veloppeurs, en premi\u00e8re ligne de la complexit\u00e9 technique, se voient fr\u00e9quemment dict\u00e9s leur conduite par ceux qui ne partagent ni leur expertise ni leur compr\u00e9hension des enjeux. Directeurs, managers, clients \u2013 tous s'autorisent \u00e0 \u00e9mettre des avis, \u00e0 imposer des choix, souvent au m\u00e9pris des r\u00e9alit\u00e9s techniques. Cette immixtion, loin d'\u00eatre anodine, est une source constante de frustration. Pire encore, elle peut mener \u00e0 des d\u00e9sastres techniques, notamment lorsque des d\u00e9cisions mal avis\u00e9es entra\u00eenent une accumulation de dette technique, cette gangr\u00e8ne silencieuse du code que l'on reporte de corriger, jusqu'au jour o\u00f9 l'effort n\u00e9cessaire pour la r\u00e9sorber devient titanesque, voire impossible.

    Ainsi, l'ultracr\u00e9pidarianisme, en s'infiltrant dans les rouages du d\u00e9veloppement logiciel, menace l'\u00e9quilibre d\u00e9licat entre cr\u00e9ation et rigueur, innovation et solidit\u00e9. Il est un rappel de l'importance de la modestie, de la n\u00e9cessit\u00e9 d'\u00e9couter ceux qui savent, et de la sagesse qu'il y a \u00e0 reconna\u00eetre ses propres limites.

    "}, {"location": "course-c/70-philosophy/philosophy/#philosophie-de-conception", "title": "Philosophie de conception", "text": "

    Ces principes constituent des lignes directrices essentielles pour aider le d\u00e9veloppeur \u00e0 structurer son code de mani\u00e8re \u00e0 le rendre plus lisible, plus maintenable et moins susceptible de contenir des erreurs humaines.

    Il ne suffit pas qu'un programme fonctionne correctement ou qu'il satisfasse les attentes d'un sup\u00e9rieur hi\u00e9rarchique\u2009; l'attitude du programmeur va bien au-del\u00e0 du simple acte de coder. Cet \u00e9tat d'esprit ne s'enseigne pas, il s'acquiert avec l'exp\u00e9rience.

    Voici les quatre principes les plus embl\u00e9matiques\u2009:

    DRY

    Ne vous r\u00e9p\u00e9tez pas. (Do not repeat yourself.)

    KISS

    Restez simple, stupide. (Keep it simple, stupid.)

    SSOT

    Une seule source de v\u00e9rit\u00e9. (Single source of truth.)

    YAGNI

    Vous n'en aurez pas besoin. (You ain't gonna need it.)

    "}, {"location": "course-c/70-philosophy/philosophy/#dry_1", "title": "DRY", "text": "

    Ne vous r\u00e9p\u00e9tez pas (Don't Repeat Yourself) ! Je le r\u00e9p\u00e8te\u2009: ne vous r\u00e9p\u00e9tez pas ! Ce principe fondamental du d\u00e9veloppement logiciel vise \u00e0 \u00e9viter la redondance de code. Dans leur ouvrage incontournable, The Pragmatic Programmer, Andrew Hunt et David Thomas le formulent ainsi\u2009:

    Dans un syst\u00e8me, toute connaissance doit avoir une repr\u00e9sentation unique, non ambigu\u00eb et faisant autorit\u00e9.

    En d'autres termes, le programmeur doit rester constamment vigilant, pr\u00eat \u00e0 entendre une alarme mentale, rouge, vive et bruyante d\u00e8s qu'il s'appr\u00eate \u00e0 utiliser la combinaison Ctrl+C (ou Cmd+C), suivie de Ctrl+V (ou Cmd+V). Dupliquer du code, quelle que soit la quantit\u00e9, est toujours une mauvaise pratique, car cela trahit souvent un code smell, signal \u00e9vident que le code pourrait \u00eatre simplifi\u00e9 et optimis\u00e9.

    L'exemple de code suivant pr\u00e9sente une violation du principe DRY : la fonction display est appel\u00e9e deux fois. Dans les deux cas, elle re\u00e7oit un pointeur sur un fichier, ce qui indique qu'une simplification est possible.

    FILE *fp = NULL;\nif (argc > 1) {\n    fp = fopen(argv[1], \"r\");\n    display(fp);\n}\nelse {\n    display(stdin);\n}\n

    Voici la version corrig\u00e9e\u2009:

    FILE *fp = argc > 1 ? fopen(argv[1], \"r\") : stdin;\ndisplay(fp);\n

    ", "tags": ["display"]}, {"location": "course-c/70-philosophy/philosophy/#kiss_1", "title": "KISS", "text": "

    Keep it simple, stupid est une ligne directrice de conception qui encourage la simplicit\u00e9 d'un d\u00e9veloppement. Elle est similaire au rasoir d'Ockham, mais plus commune en informatique. \u00c9nonc\u00e9 par Eric Steven Raymond puis par le Zen de Python un programme ne doit faire qu'une chose, et une chose simple. C'est une philosophie grandement respect\u00e9e dans l'univers Unix/Linux. Chaque programme de base du shell (ls, cat, echo, grep...) ne fait qu'une t\u00e2che simple, le nom est court et simple \u00e0 retenir.

    La fonction suivante n'est pas KISS car elle est responsable de plusieurs t\u00e2ches\u2009: v\u00e9rifier les valeurs d'un set de donn\u00e9e et les afficher\u2009:

    int process(Data *data, size_t size) {\n    // Check consistency and display\n    for (int i = 0; i < size; i++) {\n        if (data[i].value <= 0)\n            data[i].value = 1;\n\n        printf(\"%lf\\n\", 20 * log10(data[i].value));\n    }\n}\n

    Il serait pr\u00e9f\u00e9rable de la d\u00e9couper en deux sous-fonctions\u2009:

    #define TO_LOG(a) (20 * log10(a))\n\nint fix_data(Data *data, const size_t size) {\n    for (int i = 0; i < size; i++) {\n        if (data[i].value <= 0)\n            data[i].value = 1;\n    }\n}\n\nint display(const Data *data, const size_t size) {\n    for (int i = 0; i < size; i++)\n        printf(\"%lf\\n\", TO_LOG(data[i].value));\n}\n

    ", "tags": ["echo", "cat", "grep"]}, {"location": "course-c/70-philosophy/philosophy/#yagni_1", "title": "YAGNI", "text": "

    YAGNI est un anglicisme de you ain't gonna need it qui peut \u00eatre traduit par\u2009: vous n'en aurez pas besoin. C'est un principe tr\u00e8s connu en d\u00e9veloppent Agile XP (Extreme Programming) qui stipule qu'un d\u00e9veloppeur logiciel ne devrait pas impl\u00e9menter une fonctionnalit\u00e9 \u00e0 un logiciel tant que celle-ci n'est pas absolument n\u00e9cessaire.

    Ce principe combat le biais du d\u00e9veloppeur \u00e0 vouloir sans cesse d\u00e9marrer de nombreux chantiers sans se focaliser sur l'essentiel strictement n\u00e9cessaire d'un programme et permettant de satisfaire au cahier des charges convenu avec le partenaire/client.

    "}, {"location": "course-c/70-philosophy/philosophy/#ssot_1", "title": "SSOT", "text": "

    Ce principe tient son acronyme de single source of truth. Il adresse principalement un d\u00e9faut de conception relatif aux m\u00e9tadonn\u00e9es que peuvent \u00eatre les param\u00e8tres d'un algorithme, le mod\u00e8le d'une base de donn\u00e9es ou la m\u00e9thode usit\u00e9e d'un programme \u00e0 collecter des donn\u00e9es.

    Un programme qui respecte ce principe \u00e9vite la duplication des donn\u00e9es. Des d\u00e9fauts courants de conception sont\u2009:

    • indiquer le nom d'un fichier source dans le fichier source\u2009;

      /**\n * @file main.c\n */\n
    • stocker la m\u00eame image, le m\u00eame document dans diff\u00e9rents formats\u2009;

      convert input.jpg -resize 800x800 image-small.jpg\nconvert input.jpg -resize 400x400 image-smaller.jpg\nconvert input.jpg -resize 10x10 image-tiny.jpg\ngit add image-small.jpg image-smaller.jpg image-tiny.jpg\n
    • stocker dans une base de donn\u00e9es le nom Doe, pr\u00e9nom John ainsi que le nom complet\u2009;

      INSERT INTO users (first_name, last_name, full_name)\nVALUES ('John', 'Doe', 'John Doe');\n
    • avoir un commentaire C ayant deux v\u00e9rit\u00e9s contradictoires\u2009;

      int height = 206; // Size of Haf\u00fe\u00f3r J\u00fal\u00edus Bj\u00f6rnsson which is 205 cm\n
    • conserver une copie des m\u00eames donn\u00e9es sous des formats diff\u00e9rents (un tableau de donn\u00e9es brutes et un tableau des m\u00eames donn\u00e9es, mais tri\u00e9es).

      ssconvert data.csv data.xlsx\nlibreoffice --headless --convert-to pdf fichier.csv\ngit add data.csv data.xlsx data.pdf # Beurk !\ngit commit -m \"Add all data formats\"\n
    "}, {"location": "course-c/70-philosophy/philosophy/#zen-de-python", "title": "Zen de Python", "text": "

    Le Zen de Python est un ensemble de 19 principes publi\u00e9s en 1999 par Tim Peters. Largement accept\u00e9 par la communaut\u00e9 de d\u00e9veloppeurs et il est connu sous le nom de PEP 20.

    Voici le texte original anglais\u2009:

    Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one\u2014and preferably only one\u2014obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than right now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea\u2014let's do more of those\u2009!

    Un code est meilleur s'il est beau, esth\u00e9tique, que les noms des variables, l'alignement et la mise en forme sont coh\u00e9rents et forment une unit\u00e9.

    Un code se doit \u00eatre explicite, et r\u00e9ellement traduire l'intention du d\u00e9veloppeur. Il est ainsi pr\u00e9f\u00e9rable d'\u00e9crire u = v / 4 plut\u00f4t que u >>= 2. De la m\u00eame mani\u00e8re, d\u00e9tecter si un nombre est pair est plus explicite avec if (n % 2 == 0) que if (n & 1).

    "}, {"location": "course-c/70-philosophy/philosophy/#the-code-taste", "title": "The code taste", "text": "

    Voici une version am\u00e9lior\u00e9e de votre texte, en tenant compte du style que vous appr\u00e9ciez\u2009:

    Lors d'une conf\u00e9rence TED en 2016, le cr\u00e9ateur de Linux, Linus Torvalds, introduisit un concept qu'il nomma code taste, que l'on pourrait traduire par le go\u00fbt du code.

    Il pr\u00e9senta l'exemple de code C suivant, interrogeant son auditoire sur le fait de savoir si ce code \u00e9tait de bon go\u00fbt\u2009:

    void remove_list_entry(List* list, Entry* entry)\n{\n    Entry* prev = NULL;\n    Entry* walk = list->head;\n\n    while (walk != entry) {\n        prev = walk;\n        walk = walk->next;\n    }\n\n    if (!prev)\n        list->head = entry->next;\n    else\n        prev->next = entry->next;\n}\n

    Torvalds r\u00e9pondit sans d\u00e9tour que ce code \u00e9tait de mauvais go\u00fbt, le qualifiant de vilain et moche. En effet, ce test plac\u00e9 apr\u00e8s la boucle while d\u00e9note par rapport au reste du code. Cette dissonance esth\u00e9tique sugg\u00e8re qu'il existe une impl\u00e9mentation plus \u00e9l\u00e9gante, car lorsque le code para\u00eet laid, il y a fort \u00e0 parier qu'une solution de meilleur go\u00fbt peut \u00eatre trouv\u00e9e. On dit dans ces cas-l\u00e0 que le code sent : ce test est superflu, et il doit exister une mani\u00e8re d'\u00e9viter de traiter un cas particulier en choisissant un algorithme mieux con\u00e7u.

    En r\u00e9alit\u00e9, retirer un \u00e9l\u00e9ment d'une liste cha\u00een\u00e9e requiert de g\u00e9rer deux cas distincts\u2009:

    • Si l'\u00e9l\u00e9ment se trouve au d\u00e9but de la liste, il faut ajuster le pointeur head.
    • Dans le cas contraire, il convient de modifier prev->next.

    Apr\u00e8s avoir questionn\u00e9 l'auditoire, Torvalds d\u00e9voila une nouvelle impl\u00e9mentation\u2009:

    void remove_list_entry(List* list, Entry* entry)\n{\n    Entry** indirect = &head;\n\n    while ((*indirect) != entry)\n        indirect = &(*indirect)->next;\n\n    *indirect = entry->next;\n}\n

    La fonction originellement \u00e9tal\u00e9e sur dix lignes est d\u00e9sormais r\u00e9duite \u00e0 quatre. Bien que le nombre de lignes importe moins que la lisibilit\u00e9, cette nouvelle version \u00e9limine la gestion des cas particuliers gr\u00e2ce \u00e0 un adressage indirect beaucoup plus raffin\u00e9.

    Cependant, un programmeur novice en C pourrait \u00eatre d\u00e9concert\u00e9 par l'emploi des doubles pointeurs et juger la premi\u00e8re version plus lisible. Cela illustre \u00e0 quel point la connaissance des structures de donn\u00e9es et des algorithmes est cruciale pour \u00e9crire du code de qualit\u00e9\u2009: on ne s'improvise pas d\u00e9veloppeur, c'est un art qui demande patience et apprentissage.

    Un exemple similaire, plus accessible, est pr\u00e9sent\u00e9 par Brian Barto dans un article publi\u00e9 sur Medium. Il y discute de l'initialisation \u00e0 z\u00e9ro de la bordure d'un tableau bidimensionnel\u2009:

    for (size_t row = 0; row < GRID_SIZE; ++row)\n{\n    for (size_t col = 0; col < GRID_SIZE; ++col)\n    {\n        if (row == 0)\n            grid[row][col] = 0; // Top Edge\n\n        if (col == 0)\n            grid[row][col] = 0; // Left Edge\n\n        if (col == GRID_SIZE - 1)\n            grid[row][col] = 0; // Right Edge\n\n        if (row == GRID_SIZE - 1)\n            grid[row][col] = 0; // Bottom Edge\n    }\n}\n

    On constate plusieurs fautes de go\u00fbt\u2009:

    1. GRID_SIZE pourrait \u00eatre diff\u00e9rent de la r\u00e9elle taille de grid
    2. Les valeurs d'initialisation sont dupliqu\u00e9es
    3. La complexit\u00e9 de l'algorithme est de \\(O(n^2)\\) alors que l'on ne s'int\u00e9resse qu'\u00e0 la bordure du tableau.

    Voici une solution plus \u00e9l\u00e9gante\u2009:

    const size_t length = sizeof(grid[0]) / sizeof(grid[0][0]);\nconst int init = 0;\n\nfor (size_t i = 0; i < length; i++)\n{\n    grid[i][0] = grid[0][i] = init; // Top and Left\n    grid[length - 1][i] = grid[i][length - 1] = init; // Bottom and Right\n}\n
    ", "tags": ["while", "grid", "GRID_SIZE", "head"]}, {"location": "course-c/70-philosophy/philosophy/#lodeur-du-code-code-smell", "title": "L'odeur du code (code smell)", "text": "

    Un code sent si certains indicateurs sont au rouge. On appelle ces indicateurs des antipatterns. Voici quelques indicateurs les plus courants\u2009:

    Mastodonte

    Une fonction est plus longue qu'un \u00e9cran de haut (~50 lignes)

    Titan

    Un fichier est plus long que 1000 lignes.

    Ligne Dieu

    Une ligne beaucoup trop longue et de facto illisible.

    Usine \u00e0 gaz

    Une fonction \u00e0 plus de trois param\u00e8tres

    void make_coffee(int size, int mode, int mouture, int cup_size,\n    bool with_milk, bool cow_milk, int number_of_sugars);\n
    Miroir magique

    Du code est dupliqu\u00e9. Du code est dupliqu\u00e9.

    Pamphlets touristiques

    Les commentaires expliquent le comment du code et non le pourquoi

    // Additionne une constante avec une autre pour ensuite l'utiliser\ndouble u = (a + cst);\nu /= 1.11123445143; // division par une constante inf\u00e9rieure \u00e0 2\n
    Arbre de No\u00ebl

    plus de deux structures de contr\u00f4les sont impliqu\u00e9es

    if (a > 2) {\n    if (b < 8) {\n        if (c ==12) {\n            if (d == 0) {\n                exception(a, b, c, d);\n            }\n        }\n    }\n}\n
    T\u00e9l\u00e9portation sauvage

    Usage de goto, o\u00f9 quand le code saute d'un endroit \u00e0 l'autre sans logique apparente.

    loop:\n    i +=1;\n    if (i > 100)\n        goto end;\nhappy:\n    happy();\n    if (j > 10):\n        goto sad;\nsad:\n    sad();\n    if (k < 50):\n        goto happy;\nend:\n
    Jumeaux diaboliques

    Plusieurs variables avec des noms tr\u00e8s similaires

    int advice = 11;\nint advise = 12;\n
    Action \u00e0 distance

    Une fonction qui tire les ficelles \u00e0 distance gr\u00e2ce aux variables globales.

    void make_coffee() {\n    powerup_nuclear_reactor = true;\n    number_of_coffee_beans_to_harvest = 62;\n    ...\n}\n
    Ancre de bateau

    Un composant inutilis\u00e9, mais gard\u00e9 dans le logiciel pour des raisons politiques (YAGNI)

    Cyclomatisme aigu

    Quand trop de structures de contr\u00f4les sont n\u00e9cessaires pour traiter un probl\u00e8me apparemment simple

    Attente active

    Une boucle qui ne contient qu'une instruction de test, attendant la condition

    while (true) {\n    if (finished) break;\n}\n
    Valeur Bulgare

    Popularis\u00e9 par Jacques-Andr\u00e9 Porchet, une grandeur Bulgare est une valeur magique qui n'a pas de sens pour un non-initi\u00e9 et qui semble \u00eatre sortie de nulle part.

    double it_works_with_this_value = 1.11123445143;\n
    Objet divin

    Quand un composant logiciel assure trop de fonctions essentielles (KISS)

    Coul\u00e9e de lave

    Lorsqu'un code immature est mis en production

    En novembre 1990 \u00e0 14h25, la soci\u00e9t\u00e9 AT&T effecue une mise \u00e0 jour de son r\u00e9seau t\u00e9l\u00e9phonique. Un bug dans le code de mise \u00e0 jour provoque un crash du r\u00e9seau entra\u00eenant une interruption de service de 9 heures affectant quelque 50 millions d'appels et co\u00fbtant \u00e0 l'entreprise 60 millions de dollars. (Source\u2009: The 1990 AT&T Long Distance Network Collapse)

    Chirurgie au fusil de chasse

    Quand l'ajout d'une fonctionnalit\u00e9 logicielle demande des changements multiples et disparates dans le code (Shotgun surgery).

    ", "tags": ["goto"]}, {"location": "course-c/70-philosophy/philosophy/#conclusion", "title": "Conclusion", "text": "

    La qu\u00eate d'un code de qualit\u00e9 repose sur des principes philosophiques et m\u00e9thodologiques essentiels. Le rasoir d'Ockham encourage la simplicit\u00e9 en \u00e9liminant les \u00e9l\u00e9ments superflus, tandis que l'effet Dunning-Kruger met en garde contre la surestimation de ses comp\u00e9tences, rappelant l'importance de la remise en question et du retour critique. Des doctrines telles que DRY (ne vous r\u00e9p\u00e9tez pas), KISS (restez simple), SSOT (une seule source de v\u00e9rit\u00e9) et YAGNI (vous n'en aurez pas besoin) guident le d\u00e9veloppeur vers une conception \u00e9pur\u00e9e et efficace.

    Le Zen de Python illustre cette philosophie par dix-neuf aphorismes valorisant la beaut\u00e9, la lisibilit\u00e9 et la simplicit\u00e9 du code. Linus Torvalds, cr\u00e9ateur de Linux et de Git, a soulign\u00e9 l'importance du code taste ou \u00ab\u2009go\u00fbt du code\u2009\u00bb, d\u00e9montrant qu'une impl\u00e9mentation \u00e9l\u00e9gante r\u00e9sulte souvent d'une r\u00e9flexion profonde sur les structures de donn\u00e9es et les algorithmes, plut\u00f4t que d'une complexit\u00e9 inutile.

    Enfin, reconna\u00eetre et \u00e9viter les odeurs de code (code smells), ces indicateurs de mauvaise conception comme les fonctions tentaculaires, les duplications ou les commentaires mal pens\u00e9s, est crucial. En identifiant ces antipatterns \u2014 qu'ils se manifestent sous la forme d'un \u00ab\u2009Arbre de No\u00ebl\u2009\u00bb de structures conditionnelles imbriqu\u00e9es ou d'une \u00ab\u2009T\u00e9l\u00e9portation sauvage\u2009\u00bb via des goto intempestifs\u2014le d\u00e9veloppeur s'assure de produire un code maintenable, clair et performant.

    ", "tags": ["goto"]}, {"location": "course-c/90-exercises/", "title": "Exercices de r\u00e9vision", "text": "

    Exercice 1\u2009: Mot du jour

    \u00c9crire un programme qui retourne un mot parmi une liste de mot, de fa\u00e7on al\u00e9atoire.

    #include <time.h>\n#include <stdlib.h>\n\nchar *words[] = {\"Alb\u00e9do\", \"Bigre\", \"Maringouin\", \"Pluripotent\", \"Entrechat\",\n    \"Caracoler\" \"Palinodie\", \"S\u00e9millante\", \"Atavisme\", \"Cyclothymie\",\n    \"Idiosyncratique\", \"Ent\u00e9l\u00e9chie\"};\n\n#if 0\n    srand(time(NULL));   // Initialization, should only be called once.\n    size_t r = rand() % sizeof(words) / sizeof(char*); // Generate random value\n#endif\n
    Solution
    #include <time.h>\n#include <stdlib.h>\n\nchar *words[] = {\n    \"Alb\u00e9do\", \"Bigre\", \"Maringouin\", \"Pluripotent\", \"Entrechat\",\n    \"Caracoler\" \"Palinodie\", \"S\u00e9millante\", \"Atavisme\", \"Cyclothymie\",\n    \"Idiosyncratique\", \"Ent\u00e9l\u00e9chie\"};\n\nint main(void)\n{\n    srand(time(NULL));\n    puts(words[rand() % (sizeof(words) / sizeof(char*))]);\n}\n
    "}, {"location": "course-concurrent/arch/", "title": "Architecture processeur", "text": ""}, {"location": "course-concurrent/arch/#introduction", "title": "Introduction", "text": "

    L'architecture d'un processeur est l'ensemble des \u00e9l\u00e9ments qui le compose et qui lui permettent de fonctionner. C'est un peu comme le corps humain, il y a des organes qui ont chacun un r\u00f4le bien pr\u00e9cis.

    "}, {"location": "course-concurrent/arch/#historique", "title": "Historique", "text": "

    Les premiers processeurs \u00e9taient tr\u00e8s simples, ils \u00e9taient compos\u00e9s de quelques milliers de transistors et avaient une fr\u00e9quence de quelques MHz. Aujourd'hui, les processeurs sont compos\u00e9s de plusieurs milliards de transistors et ont une fr\u00e9quence de plusieurs GHz.

    Alain Turing a \u00e9t\u00e9 l'un des premiers \u00e0 imaginer un ordinateur, il a con\u00e7u un mod\u00e8le th\u00e9orique d'ordinateur appel\u00e9 \u00ab\u2009Machine de Turing\u2009\u00bb. C'est un mod\u00e8le abstrait qui a permis de poser les bases de l'informatique moderne.

    La machine de Turing permet de comprendre la notion de programme, de m\u00e9moire, de calcul, etc. Son fonctionnement est tr\u00e8s simple\u2009:

    • Elle poss\u00e8de une bande de papier infinie sur laquelle sont stock\u00e9es des donn\u00e9es.
    • Une t\u00eate de lecture/\u00e9criture qui peut lire et \u00e9crire des donn\u00e9es sur la bande.
    • Un \u00e9tat interne qui permet de savoir ce que la machine est en train de faire.
    • Un ensemble de r\u00e8gles qui permettent de changer l'\u00e9tat interne en fonction des donn\u00e9es lues.

    Avec ces quelques \u00e9l\u00e9ments simples, on peut montrer que l'on peut r\u00e9soudre n'importe quel probl\u00e8me algorithmique. C'est ce qu'on appelle la \u00ab\u2009th\u00e8se de Church-Turing\u2009\u00bb. La difficult\u00e9 principale de ce mod\u00e8le est de comment \u00e9crire les r\u00e8gles pour r\u00e9soudre un probl\u00e8me donn\u00e9. C'est l\u00e0 qu'intervient la programmation.

    Lorsque les premiers ordinateurs ont vus le jours, il \u00e9taient compos\u00e9s d'\u00e9l\u00e9ments simples\u2009:

    • Une m\u00e9moire volatile pour stocker les donn\u00e9es et des \u00e9tats interm\u00e9diaires.
    • Une m\u00e9moire non-volatile pour stocker des instructions a ex\u00e9cuter.
    • Un processeur pour ex\u00e9cuter les instructions.

    Ce processeur \u00e9tait \u00e9galement tr\u00e8s simple. Il se composait d'une unit\u00e9 de calcul, d'une unit\u00e9 de contr\u00f4le et d'une unit\u00e9 de gestion de la m\u00e9moire.

    L'unit\u00e9 de calcul nomm\u00e9 ALU (Arithmetic Logic Unit) permet de faire des op\u00e9rations arithm\u00e9tiques et logiques (addition, soustraction, multiplication, division, ET, OU, etc). L'unit\u00e9 de contr\u00f4le permet de lire les instructions en m\u00e9moire et de les ex\u00e9cuter. L'unit\u00e9 de gestion de la m\u00e9moire permet de lire et \u00e9crire des donn\u00e9es en m\u00e9moire. Le jeu d'instruction \u00e9tait tr\u00e8s simple, il se composait de quelques instructions seulement\u2009:

    1. D\u00e9placer une donn\u00e9e de la m\u00e9moire vers l'ALU.
    2. D\u00e9placer une donn\u00e9e de l'ALU vers la m\u00e9moire.
    3. Faire une op\u00e9ration arithm\u00e9tique ou logique sur les donn\u00e9es de l'ALU.
    4. Aller chercher une instruction \u00e0 une adresse donn\u00e9e en m\u00e9moire et la charger dans l'unit\u00e9 de contr\u00f4le.
    5. Tester si une valeur est nulle et sauter \u00e0 une adresse donn\u00e9e si c'est le cas.

    Avec les \u00e9volutions technologiques, de nombreux syst\u00e8mes complexes ont \u00e9t\u00e9 ajout\u00e9s.

    Le premier constat de ces premiers ordinateurs est que le processeur \u00e9tait souvent plus rapide que la m\u00e9moire et qu'aller chercher une valeur en m\u00e9moire n\u00e9cessitait plusieurs \u00e9tapes. Durant ces \u00e9tapes, le processeur devait attendre et n'utilisait pas son plein potentiel de calcul. On a alors ajout\u00e9 un composant nomm\u00e9 pipeline qui permet d'ex\u00e9cuter les diff\u00e9rentes \u00e9tapes de la lecture d'une instruction en parall\u00e8le. Cela permet de r\u00e9duire le temps d'attente du processeur. En effet, puisque le programme est ex\u00e9cut\u00e9 s\u00e9quentiellement, on sait que la prochaine instruction \u00e0 ex\u00e9cuter est la suivante de celle en cours d'ex\u00e9cution. Si l'on dispose d'un pointeur int *p sur cett instruction, on sait que l'instruction suivante est localis\u00e9e \u00e0 p + 1. On peut alors anticiper la lecture de cette instruction en avance et la charger dans l'unit\u00e9 de contr\u00f4le. C'est ce qu'on appelle le pr\u00e9chargement. Typiquement un processeur dispose de 3 \u00e0 5 \u00e9tages de pipeline dont chaque \u00e9tage correspond \u00e0 une \u00e9tape de la lecture d'une instruction\u2009:

    1. Pr\u00e9chargement de l'instruction.
    2. D\u00e9codage de l'instruction.
    3. Ex\u00e9cution de l'instruction.
    4. Acc\u00e8s \u00e0 la m\u00e9moire.
    5. \u00c9criture du r\u00e9sultat.

    H\u00e9las, le pipeline n'est pas sans inconv\u00e9nient. En effet, si une instruction d\u00e9pend du r\u00e9sultat d'une autre instruction, il faut attendre que cette derni\u00e8re soit termin\u00e9e pour pouvoir ex\u00e9cuter la premi\u00e8re. C'est ce qu'on appelle un hazard. Il existe plusieurs types de hazard\u2009:

    • Data hazard : lorsque deux instructions d\u00e9pendent du m\u00eame registre.
    • Control hazard : lorsque le r\u00e9sultat d'une instruction conditionnelle n'est pas encore connu.

    Le control hazard appara\u00eet typiquement lorsque l'on fait un saut conditionnel (if, for, while...). En effet, le processeur ne sait pas si le saut doit \u00eatre effectu\u00e9 ou non avant d'avoir ex\u00e9cut\u00e9 l'instruction conditionnelle. Sur des microcontrolleurs simples, une des deux branches de la condition est privil\u00e9gi\u00e9e. On pr\u00e9charge cette branche dans le pipeline et si la condition est fausse, on annule les r\u00e9sultats de l'ex\u00e9cution ce qui \u00e9quivaut \u00e0 vider int\u00e9gralement le pipeline. Le co\u00fbt est important car le processeur doit alors attendre que le pipeline se remplisse \u00e0 nouveau avant de pouvoir ex\u00e9cuter la prochaine instruction.

    Avec la complexification des processeurs modernes, le pipeline est devenu de plus en plus long. Aujourd'hui, un processeur moderne dispose de 15 \u00e0 20 \u00e9tages de pipeline. Le probl\u00e8me des hazards est devenu de plus en plus important.

    Un processeur moderne s'est donc \u00e9quip\u00e9 d'un autre m\u00e9canisme pour pallier ce probl\u00e8me que l'on nomme le predicteur d'embranchement. En effet, si le processeur peut pr\u00e9dire si un saut doit \u00eatre effectu\u00e9 ou non, il peut pr\u00e9charger la bonne instruction dans le pipeline. Si la pr\u00e9diction est bonne, on gagne du temps, sinon on perd du temps. Il existe plusieurs strat\u00e9gies de pr\u00e9diction de branchement\u2009:

    • Pr\u00e9diction statique : on pr\u00e9dit que le saut est toujours effectu\u00e9 ou non.
    • Pr\u00e9diction dynamique : on pr\u00e9dit le saut en fonction de l'historique des sauts pr\u00e9c\u00e9dents.

    Les processeurs modernes conservent donc une table de pr\u00e9diction de branchement qui permet de stocker l'historique des sauts pr\u00e9c\u00e9dents pour chaque instruction de conditions. Si un saut est souvent effectu\u00e9, on pr\u00e9dit qu'il le sera \u00e0 nouveau.

    N\u00e9anmoins, il reste une difficult\u00e9 majeure \u00e0 r\u00e9soudre, les latences m\u00e9moires. En effet, plus la m\u00e9moire est grosse et \u00e9loign\u00e9e du processeur, plus le temps d'acc\u00e8s est long.

    A titre d'exemple, une m\u00e9moire DDR4 a une latence de 15 ns alors qu'un processeur moderne a une fr\u00e9quence de 3 GHz soit une latence de 0.33 ns. Cela signifie que le processeur doit attendre 45 cycles pour acc\u00e9der \u00e0 la m\u00e9moire. En pratique, c'est encore pire car la m\u00e9moire est souvent partag\u00e9e entre plusieurs processeurs et doit \u00eatre synchronis\u00e9e. D'autre part, les m\u00e9moires sont organis\u00e9es en pages m\u00e9moires, changer de page demande un temps suppl\u00e9mentaire. Le d\u00e9lai peut \u00eatre de plusieurs centaines de cycles pour lire une donn\u00e9e isol\u00e9e en RAM.

    Pour palier \u00e0 ce probl\u00e8me, les processeurs se sont \u00e9quip\u00e9s de m\u00e9moires interm\u00e9diaires nomm\u00e9es m\u00e9moires caches. On distingue ajourd'hui 3 niveaux de caches\u2009:

    • L1\u2009: cache de niveau 1, tr\u00e8s rapide, souvent int\u00e9gr\u00e9 dans le coeur du processeur et \u00e0 proximit\u00e9 directe de l'ALU.
    • L2\u2009: cache de niveau 2, plus lent que le L1 mais plus gros.
    • L3\u2009: cache de niveau 3, plus lent que le L2 mais plus gros et partag\u00e9 entre plusieurs coeurs processeur.

    Ceci nous am\u00e8ne aux architectures modernes multicoeurs. Un processeur Intel ou AMD moderne comporte tr\u00e8s souvent 6 ou 8 processeurs ind\u00e9pendants reli\u00e9s entre eux par une m\u00e9moire cache de niveau L3. Chaque coeur dispose de sa propre m\u00e9moire cache de niveau L1 et L2.

    ", "tags": ["while", "for", "pipeline"]}, {"location": "course-concurrent/arch/#processeur-moderne", "title": "Processeur moderne", "text": "

    La figure suivante repr\u00e9sente la vue a\u00e9rienne d'un processeur moderne. Le die ou substrat en silicium fait environ 1 \u00e0 2 cm de c\u00f4t\u00e9 et comporte plusieurs milliards de transistors. Le savoir faire des ing\u00e9nieurs est tr\u00e8s gard\u00e9 mais en observant la structure du die, on peut deviner les diff\u00e9rents composants qui le compose.

    cpu

    On peut voir sur cette figure la m\u00e9moire cache de niveau L3 facilement identifiable \u00e0 son pattern de grille. On peut \u00e9galement voir les diff\u00e9rents coeurs qui l'entourent, ils se ressemblent tous et on voit qu'ils sont \u00e9galement compos\u00e9s d'un motif r\u00e9p\u00e9titif qui est la m\u00e9moire cache de niveau L1 et L2. Souvent ces processeurs int\u00e8grent \u00e9galement une partie GPU qui est utilis\u00e9e pour les calculs graphiques lorsqu'il n'y a pas de carte graphique int\u00e9gr\u00e9e.

    Si l'on s'int\u00e9resse \u00e0 un coeur en particulier, on peut voir qu'il est compos\u00e9 de plusieurs \u00e9l\u00e9ments. Tout d'abord \u00e0 droite on trouve la m\u00e9moire cache L2 qui repr\u00e9sente environ 20% de la surface du coeur. Ensuite le pr\u00e9dicteur d'embranchement tr\u00e8s proche du cache L1 contenant les prochaines instructions \u00e0 ex\u00e9cuter. Le d\u00e9codeur d'instructions est \u00e0 proximit\u00e9 du I-Cache et du pr\u00e9dicteur d'embranchement. Il est coupl\u00e9 \u00e0 un ordonnanceur de micro-op\u00e9rations qui adresse chaque calcul soit sur l'ALU pour de la virgule fixe, soit sur la FPU pour les calculs en virgule flottante. Dans la partie inf\u00e9rieure, on trouve l'ALU 64-bits, le cache de donn\u00e9es L1 et le gestionnaire de m\u00e9moire permettant de lire/\u00e9crire des donn\u00e9es en m\u00e9moire.

    core

    "}, {"location": "course-concurrent/async/", "title": "Programmation Asynchrone", "text": "

    Un paradigme de programmation asynchrone est un style de programmation concurrente qui permet de g\u00e9rer des \u00e9v\u00e9nements de mani\u00e8re non bloquante. C'est une mani\u00e8re de g\u00e9rer des \u00e9v\u00e9nements qui ne sont pas n\u00e9cessairement li\u00e9s \u00e0 l'ex\u00e9cution du programme et une am\u00e9lioation de la programmation concurrente classique utilisant explicitement des threads.

    Ce paradigme est devenu populaire avec l'arriv\u00e9e des applications web et des interfaces graphiques. En effet, ces applications ont besoin de r\u00e9agir \u00e0 des \u00e9v\u00e9nements de mani\u00e8re asynchrone pour ne pas bloquer l'interface utilisateur. Le langage JavaScript est un exemple de langage qui utilise ce paradigme.

    Prenons l'exemple d'une page web qui affiche des donn\u00e9es provenant d'un serveur. Lorsque la page est charg\u00e9e, une requ\u00eate est envoy\u00e9e au serveur pour r\u00e9cup\u00e9rer les donn\u00e9es. Si la requ\u00eate \u00e9tait bloquante, l'interface utilisateur serait fig\u00e9e jusqu'\u00e0 ce que les donn\u00e9es soient re\u00e7ues. Avec la programmation asynchrone, la requ\u00eate est envoy\u00e9e et le programme continue de s'ex\u00e9cuter. Lorsque les donn\u00e9es sont re\u00e7ues, un \u00e9v\u00e9nement est d\u00e9clench\u00e9 et le programme r\u00e9agit \u00e0 cet \u00e9v\u00e9nement en affichant les donn\u00e9es. Voici un exemple web simplifi\u00e9\u2009:

    <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"fr\" lang=\"fr\">\n<head>\n    <title>Exemple de programmation asynchrone</title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n</head>\n<body>\n    <h1>Donn\u00e9es</h1>\n    <div id=\"data\">Chargement des donn\u00e9es en cours...</div>\n    <script>\n        // Envoi de la requ\u00eate au serveur\n        fetch('https://jsonplaceholder.typicode.com/posts')\n            .then(response => response.json())\n            .then(data => {\n                // Affichage des donn\u00e9es\n                document.getElementById('data').innerText = JSON.stringify(data, null, 2);\n            });\n    </script>\n</body>\n</html>\n

    Vous pouvez ex\u00e9cuter cet exemple en enregistrant le code dans un fichier HTML et en l'ouvrant dans un navigateur. Vous devriez voir les donn\u00e9es affich\u00e9es dans la page.

    Dans cet exemple, la requ\u00eate est envoy\u00e9e au serveur avec la fonction fetch, qui renvoie une promesse. Lorsque la promesse est r\u00e9solue (c'est-\u00e0-dire lorsque les donn\u00e9es sont re\u00e7ues), la fonction then est appel\u00e9e pour afficher les donn\u00e9es dans la page.

    JavaScript utilise une notation tr\u00e8s particuli\u00e8re qui permet de cha\u00eener les actions. C'est impl\u00e9ment\u00e9 en retournant l'objet courant (this en C++).

    La m\u00e9thode fetch retourne donc une instance de Promise qui est un objet repr\u00e9sentant la r\u00e9solution ou le rejet d'une valeur asynchrone. Une promesse peut \u00eatre dans l'un des trois \u00e9tats suivants\u2009:

    1. en attente,
    2. r\u00e9solue ou
    3. rejet\u00e9e.

    Lorsqu'une promesse est r\u00e9solue, la m\u00e9thode then est appel\u00e9e avec la valeur de la promesse. Si la promesse est rejet\u00e9e, la m\u00e9thode catch est appel\u00e9e avec l'erreur.

    En JavaScript les fonctions lambda aussi appel\u00e9es fonctions fl\u00e9ch\u00e9es (=>) sont tr\u00e8s utilis\u00e9es. Elles permettent de d\u00e9finir des fonctions de mani\u00e8re plus concise. Par exemple le code traduit en C++ pourrait ressembler \u00e0 ceci\u2009:

    fetch(\"https://jsonplaceholder.typicode.com/posts\")\n    .then([](Response response) {\n        return response.json();\n    })\n    .then([](Data data) {\n        document.getElementById(\"data\").innerText = JSON.stringify(data, nullptr, 2);\n    });\n

    On note donc que la fonction fetch n'est pas bloquante, une action est associ\u00e9e lorsque la promesse sera r\u00e9solue. On voit ici une cha\u00eene d'actions qui se d\u00e9clenchent les unes apr\u00e8s les autres.

    1. On fait la promesse qu'une r\u00e9ponse sera re\u00e7ue
    2. Une fois les donn\u00e9es re\u00e7u, on promet de les transformer en JSON
    3. Une fois les donn\u00e9es transform\u00e9es, on promet de les afficher dans la page

    C'est une mani\u00e8re de g\u00e9rer des \u00e9v\u00e9nements de mani\u00e8re asynchrone sans bloquer le programme.

    ", "tags": ["then", "fetch", "Promise", "this", "catch"]}, {"location": "course-concurrent/async/#c-et-la-programmation-asynchrone", "title": "C++ et la programmation asynchrone", "text": "

    En C++ la notion de promesse et de future ont \u00e9t\u00e9 introduites en C++11. Leur fonctionnement est similaire \u00e0 celui des promesses en JavaScript.

    Une future est un objet qui contient une valeur qui sera disponible dans le futur. Elle est associ\u00e9e \u00e0 une promesse qui est l'objet qui promet de fournir la valeur.

    L'exemple donn\u00e9 en JavaScript n'est pas directement transposable en C++ car le langage ne poss\u00e8de pas de fonction fetch qui permet de faire des requ\u00eates HTTP de mani\u00e8re asynchrone. N\u00e9anmoins l'utilisation de la biblioth\u00e8que CURL et nlohmann/json peut nous aider.

    sudo apt install libcurl4 libcurl4-openssl-dev nlohmann-json3-dev\n
    #include <iostream>\n#include <string>\n#include <curl/curl.h>\n#include <nlohmann/json.hpp>\n#include <future>\n#include <thread>\n\nusing json = nlohmann::json;\n\nstatic size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {\n    ((std::string*)userp)->append((char*)contents, size * nmemb);\n    return size * nmemb;\n}\n\nstd::string fetchUrl(const std::string& url) {\n    CURL *curl = curl_easy_init();\n    if(!curl) return \"\";\n\n    std::string readBuffer;\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);\n    CURLcode res = curl_easy_perform(curl);\n    curl_easy_cleanup(curl);\n\n    if(res != CURLE_OK)\n        throw std::runtime_error(\"CURL failed: \" + std::string(curl_easy_strerror(res)));\n    return readBuffer;\n}\n\nint main() {\n    std::string url = \"https://jsonplaceholder.typicode.com/posts\";\n\n    // Cr\u00e9ation de la promesse et du futur\n    std::promise<std::string> promise;\n    std::future<std::string> future = promise.get_future();\n\n    // Ex\u00e9cution dans un thread s\u00e9par\u00e9\n    std::jthread([url, &promise]() {\n        try {\n            std::string result = fetchUrl(url);\n            promise.set_value(result);\n        } catch(...) {\n            promise.set_exception(std::current_exception());\n        }\n    });\n\n    try {\n        // Attendre et obtenir la valeur future\n        std::string result = future.get();\n        json j = json::parse(result);\n        std::cout << j.dump(4) << std::endl;\n    } catch(const std::exception& e) {\n        std::cerr << \"Error: \" << e.what() << std::endl;\n    }\n}\n

    On peut observer que le code est moins concis que le code JavaScript. L'asynchronisme est plus explicite et n\u00e9cessite la cr\u00e9ation d'un thread pour effectuer la requ\u00eate. La promesse est utilis\u00e9e pour transmettre le r\u00e9sultat de la requ\u00eate au thread principal.

    ", "tags": ["fetch"]}, {"location": "course-concurrent/async/#stdasync", "title": "std\u2009::async", "text": "

    Async est une fonction qui permet de lancer une fonction de mani\u00e8re asynchrone, autrement dit dans un thread s\u00e9par\u00e9 mais sans avoir \u00e0 g\u00e9rer la cr\u00e9ation du thread.

    Elle retourne une future qui contient le r\u00e9sultat de la fonction. C'est une mani\u00e8re plus simple de lancer une fonction de mani\u00e8re asynchrone sans avoir \u00e0 g\u00e9rer la cr\u00e9ation d'un thread.

    #include <iostream>\n#include <string>\n#include <curl/curl.h>\n#include <nlohmann/json.hpp>\n#include <future>\n\nusing json = nlohmann::json;\n\nstatic size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {\n    ((std::string*)userp)->append((char*)contents, size * nmemb);\n    return size * nmemb;\n}\n\nstd::string fetchUrl(const std::string& url) {\n    CURL *curl = curl_easy_init();\n    if(!curl) return \"\";\n\n    std::string readBuffer;\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);\n    CURLcode res = curl_easy_perform(curl);\n    curl_easy_cleanup(curl);\n\n    if(res != CURLE_OK)\n        throw std::runtime_error(\"CURL failed: \" + std::string(curl_easy_strerror(res)));\n    return readBuffer;\n}\n\nint main() {\n    // Lancement asynchrone de la requ\u00eate HTTP\n    std::future<std::string> future = std::async(\n        std::launch::async, fetchUrl, \"https://jsonplaceholder.typicode.com/posts\");\n\n    try {\n        // Attendre et obtenir la valeur future\n        std::string result = future.get();\n        json j = json::parse(result);\n        std::cout << j.dump(4) << std::endl;\n    } catch(const std::exception& e) {\n        std::cerr << \"Error: \" << e.what() << std::endl;\n    }\n}\n
    "}, {"location": "course-concurrent/memory/", "title": "M\u00e9moire cache", "text": "

    Chaque processeur poss\u00e8de une m\u00e9moire cache qui permet de stocker des donn\u00e9es et des instructions pour les rendre plus rapidement accessibles. La m\u00e9moire cache est plus rapide que la m\u00e9moire principale (RAM) mais elle est aussi plus petite. Il existe plusieurs niveaux de cache (L1, L2, L3) qui sont de plus en plus grands et de plus en plus lents. Le dernier rampart avant la RAM est aussi appel\u00e9 le cache de dernier niveau (LLC).

    Le principal probl\u00e8me des ordinateurs modernes est que l'acc\u00e8s \u00e0 la m\u00e9moire RAM est extr\u00eamement lent (50 \u00e0 200 cycles) par rapport \u00e0 la vitesse du processeur. Il y a plusieurs raison \u00e0 cela, la premi\u00e8re est que la m\u00e9moire RAM est souvent d\u00e9port\u00e9e du processeur (sur une carte m\u00e8re) et que la vitesse de la lumi\u00e8re est limit\u00e9e\u2009: avec une distance de 20 cm (40 cm all\u00e9 retour) il faut 1.33 nanosecond soit d\u00e9j\u00e0 entre 5 et 10 cycles processeur. La seconde raison est que la m\u00e9moire RAM est compos\u00e9e de condensateurs qui se d\u00e9chargent et qui doivent \u00eatre recharg\u00e9s \u00e0 chaque lecture, on appelle cela le rafraichissement\u2009: toutes les 7.8us un ordinateur \u00e0 un hoquet et les acc\u00e8s RAM sont ralentis. Ces deux raisons ajout\u00e9 \u00e0 tous les circuits logiques qui doivent \u00eatre configur\u00e9s pour acheminer l'information d'un point \u00e0 l'autre font que l'acc\u00e8s \u00e0 la m\u00e9moire RAM est tr\u00e8s lente.

    Pour palier \u00e0 ce probl\u00e8me la notion de m\u00e9moire cache a \u00e9t\u00e9 introduite. Le premier processeur dot\u00e9 d'une m\u00e9moire cache est le Motorola 68000 en 1979.

    Un ennui majeur est que plus une m\u00e9moire est rapide plus elle est volumineuse et plus elle est volumineuse plus elle est ch\u00e8re. Une solution \u00e0 ce probl\u00e8me est d'utiliser plusieurs niveaux de cache. Le cache est tr\u00e8s rapide mais aussi tr\u00e8s volumineux en surface de silicium, il est donc tr\u00e8s cher. Il repr\u00e9sente environ 20% de la surface d'un processeur.

    "}, {"location": "course-concurrent/memory/#organisation-de-la-memoire-cache", "title": "Organisation de la m\u00e9moire cache", "text": "

    La m\u00e9moire cache est organis\u00e9e en lignes de cache. Chaque ligne de cache identifi\u00e9e par un index contient un bloc de donn\u00e9es et un tag. Le tag est un identifiant unique pour chaque bloc de donn\u00e9es. Lorsqu'un processeur veut acc\u00e9der \u00e0 une donn\u00e9e, il va d'abord chercher le tag dans le cache, si le tag est trouv\u00e9 alors le processeur a trouv\u00e9 la donn\u00e9e et il peut l'utiliser. Si le tag n'est pas trouv\u00e9 alors le processeur doit aller chercher la donn\u00e9e dans la m\u00e9moire RAM, ou dans un autre cache.

    Typiquement un cache L1 sur un processeur x86 contient 32 Ko de donn\u00e9es et 32 Ko d'instructions. Une ligne de cache contient g\u00e9n\u00e9ralement 64 octets.

    Lorsqu'une donn\u00e9e est charg\u00e9e en m\u00e9moire, par exemple l'acc\u00e8s \u00e0 un \u00e9l\u00e9ment d'un tableau, le processeur profite de la localit\u00e9 spatiale pour charger en m\u00e9moire les donn\u00e9es voisines. La localit\u00e9 spatiale est le fait que les donn\u00e9es voisines d'une donn\u00e9e sont souvent utilis\u00e9es en m\u00eame temps. Parall\u00e8lement, on appelle localit\u00e9 temporelle le fait que les donn\u00e9es sont souvent r\u00e9utilis\u00e9es plusieurs fois.

    Prenons l'exemple d'un tableau de 128 \u00e9l\u00e9ments de 4 bytes (int). Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 0, le processeur va automatiquement charger en m\u00e9moire les \u00e9l\u00e9ments 0 \u00e0 15 dans le cache. Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 16, le processeur doit aller chercher les \u00e9l\u00e9ments 16 \u00e0 31 dans la m\u00e9moire RAM. Si j'acc\u00e8de \u00e0 l'\u00e9l\u00e9ment 0 une seconde fois, le processeur n'a pas besoin d'aller chercher les donn\u00e9es dans la m\u00e9moire RAM car elles sont d\u00e9j\u00e0 dans le cache, ceci jusqu'\u00e0 concurrence de la taille du cache.

    "}, {"location": "course-concurrent/memory/#cache-miss-et-cache-hit", "title": "Cache miss et cache hit", "text": "

    Lorsqu'un processeur cherche une donn\u00e9e dans le cache, il peut y avoir deux cas de figure\u2009: le cache hit et le cache miss.

    Un cache hit est le cas o\u00f9 le processeur trouve la donn\u00e9e dans le cache. C'est le cas le plus rapide car le processeur n'a pas besoin d'aller chercher la donn\u00e9e dans la m\u00e9moire RAM.

    Un cache miss est le cas o\u00f9 le processeur ne trouve pas la donn\u00e9e dans le cache. C'est le cas le plus lent car le processeur doit aller chercher la donn\u00e9e dans la m\u00e9moire RAM. Il existe plusieurs types de cache miss, citons-en quelques uns\u2009:

    1. Compulsory miss: C'est le premier acc\u00e8s \u00e0 une donn\u00e9e. Le processeur ne peut pas savoir \u00e0 l'avance si la donn\u00e9e est dans le cache ou non. Il doit donc aller chercher la donn\u00e9e dans la m\u00e9moire RAM. Ce type de cache miss est in\u00e9vitable.
    2. Capacity miss: Le cache est plein et le processeur doit remplacer une ligne de cache pour pouvoir stocker une nouvelle donn\u00e9e. Ce type de cache miss est in\u00e9vitable.
    3. Conflict miss: Deux donn\u00e9es diff\u00e9rentes ont le m\u00eame tag. Ce type de cache miss est \u00e9vitable en choisissant judicieusement les tags.
    4. Coherence miss: Un autre processeur a modifi\u00e9 la donn\u00e9e. Ce type de cache miss est \u00e9vitable en utilisant des protocoles de coh\u00e9rence de cache.

    Plusieurs raisons peuvent expliquer un cache miss.

    "}, {"location": "course-concurrent/memory/#true-sharing", "title": "True sharing", "text": "

    Le true sharing est le cas o\u00f9 deux processeurs partagent la m\u00eame donn\u00e9e. Cela peut causer des ralentissements importants dans un programme concurrent car les deux processeurs doivent se synchroniser pour \u00e9viter les probl\u00e8mes de coh\u00e9rence de cache \u00e0 chaque acc\u00e8s \u00e0 la donn\u00e9e partag\u00e9e.

    "}, {"location": "course-concurrent/memory/#false-sharing", "title": "False sharing", "text": "

    Le false sharing est un probl\u00e8me qui survient lorsqu'un processeur modifie une donn\u00e9e non partag\u00e9e mais sur la m\u00eame ligne de cache qu'une autre donn\u00e9e. Cela survient typiquement dans le cas de la programmation concurrente. Lorsqu'un processeur modifie une donn\u00e9e, il doit invalider la ligne de cache de l'autre processeur. Si les deux processeurs modifient des donn\u00e9es qui sont sur la m\u00eame ligne de cache, alors les deux processeurs doivent se synchroniser.

    On appelle cela le false sharing car les deux processeurs ne partagent pas la m\u00eame donn\u00e9e mais ils partagent la m\u00eame ligne de cache.

    En C++17 le concept de std::hardware_destructive_interference_size permet de conna\u00eetre la taille d'une ligne de cache. Il n'est n\u00e9anmoins pas disponible dans tous les compilateurs. On peut utiliser la valeur 64 octets pour la plupart des processeurs.

    "}, {"location": "course-concurrent/memory/#compteur-de-performance", "title": "Compteur de performance", "text": "

    Un processeur moderne poss\u00e8de des compteurs de performance qui permettent de mesurer le comportement du processeeur. A chaque mauvaise pr\u00e9diction d'embranchement, \u00e0 chaque ralentissement d'un calcul FPU, \u00e0 chaque \u00e9v\u00e8nement du cache (miss, hit, etc) un compteur est incr\u00e9ment\u00e9. Ces compteurs sont accessibles via l'outil perf sur Linux et VTune sur Windows. Ce sont des outils indispensables pour mesurer les performances d'un programme.

    ", "tags": ["VTune", "perf"]}, {"location": "course-concurrent/memory/#exemples", "title": "Exemples", "text": "

    Dans ce chapitre, l'objectif est de sensibilier le lecteur aux ralentissements caus\u00e9s par le mat\u00e9riel. Il ne suffit pas de faire de la programmation concurrente en cherchant \u00e0 paralleliser un maximum de t\u00e2ches pour obtenir un programme plus rapide. Il est n\u00e9cessaire de prendre en compte les probl\u00e8mes de coh\u00e9rence de cache et de localit\u00e9 spatiale.

    Plusieurs exemples vont \u00eatre donn\u00e9s\u2009:

    • Rafraichissement de la m\u00e9moire
    • Localit\u00e9 spatiale
    • True sharing
    • False sharing
    • Pr\u00e9diction de branchement

    Pour chaque exemple, il est important de d\u00e9sactiver certaines fonctionnalit\u00e9s du processeur pour \u00e9viter des comportement inattendus.

    1. D\u00e9sactiver la randomisation de l'espace d'adressage, c'est \u00e0 dire que les adresses m\u00e9moires sont toujours les m\u00eames \u00e0 chaque ex\u00e9cution du programme.
    $ sudo bash -c \"echo 0 > /proc/sys/kernel/randomize_va_space\"\n
    1. D\u00e9sactiver le turbo mode, c'est \u00e0 dire que le processeur ne peut pas augmenter sa fr\u00e9quence pour acc\u00e9l\u00e9rer les calculs.
    $ sudo bash -c \"echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo\"\n
    1. D\u00e9sactiver l'hyper-threading, c'est \u00e0 dire que chaque coeur du processeur est utilis\u00e9 par un seul thread. Cela doit \u00eatre fait pour chaque coeur du processeur.

    bash $ sudo bash -c \"echo 0 > /sys/devices/system/cpu/cpuX/online\"

    1. D\u00e9sactiver le C-state, c'est \u00e0 dire que le processeur ne peut pas passer en mode veille pour \u00e9conomiser de l'\u00e9nergie.

      $ sudo cpupower frequency-set --governor performance\n
    "}, {"location": "course-concurrent/memory/#localite-spatiale", "title": "Localit\u00e9 spatiale", "text": "

    Prenons l'exemple d'une matrice de NxN \u00e9l\u00e9ments. Si on parcourt la matrice par ligne, on profite de la localit\u00e9 spatiale car les \u00e9l\u00e9ments d'une ligne sont souvent utilis\u00e9s en m\u00eame temps.

    En revanche si on parcourt la matrice par colonne, on ne profite pas de la localit\u00e9 spatiale car les \u00e9l\u00e9ments d'une colonne ne sont pas utilis\u00e9s en m\u00eame temps. Le cache est rapidement plein et le processeur doit aller chercher les donn\u00e9es dans la m\u00e9moire RAM.

    Le code suivant met en \u00e9vidence la diff\u00e9rence de performance entre un parcours par ligne et un parcours par colonne\u2009:

    #include <iostream>\n#include <vector>\n#include <chrono>\n\nconst int N = 10'000;\n\nint main() {\n    std::vector<std::vector<int>> matrix(N, std::vector<int>(N, 0));\n\n    for (size_t i = 0; i < N; i++)\n        for (size_t j = 0; j < N; j++)\n            matrix[i][j] = i * N + j;\n\n    auto start = std::chrono::high_resolution_clock::now();\n    long long sum = 0;\n    for (size_t i = 0; i < N; i++)\n        for (size_t j = 0; j < N; j++)\n#ifdef LINE\n            sum += matrix[i][j];\n#else\n            sum += matrix[j][i];\n#endif\n    auto end = std::chrono::high_resolution_clock::now();\n\n    std::chrono::duration<double> time = end - start;\n    std::cout << \"Somme en ligne : \" << sum << \"\\n\";\n    std::cout << \"Temps d'acc\u00e8s: \" << time.count() << \" s\" << std::endl;\n}\n

    Pour le cas d'une matrice de 10'000x10'000 entiers 32-bits, on obtient les r\u00e9sultats suivants\u2009:

    $ g++ -DCOLUMN -O3 locality-line.cpp && ./a.out\nSomme en ligne : 4999999950000000\nTemps d'acc\u00e8s: 0.772923 s\n\n$ g++ -DLINE -O3 locality-line.cpp && ./a.out\nSomme en ligne : 4999999950000000\nTemps d'acc\u00e8s: 0.0275642 s\n

    On observe que le parcours par ligne est 28 fois plus rapide que le parcours par colonne.

    "}, {"location": "course-concurrent/memory/#references", "title": "R\u00e9f\u00e9rences", "text": "
    • https://medium.com/@techhara/speed-up-c-false-sharing-44b56fffe02b
    • https://github.com/Kobzol/hardware-effects
    • https://blog.cloudflare.com/every-7-8us-your-computers-memory-has-a-hiccup/
    "}, {"location": "course-concurrent/mutex/", "title": "Exclusion Mutuelle", "text": "

    Les exclusions mutuelles sont un concept fondamental en programmation concurrente et parall\u00e8le. L'id\u00e9e est de garantir que certaines parties du code ne sont pas ex\u00e9cut\u00e9es simultan\u00e9ment par plusieurs threads, afin d'\u00e9viter des conditions de concurrence et des r\u00e9sultats impr\u00e9visibles.

    Les race conditions ou en fran\u00e7ais conditions de course sont des situations o\u00f9 le r\u00e9sultat d'une op\u00e9ration d\u00e9pend de l'ordre d'ex\u00e9cution des threads, ce qui peut conduire \u00e0 des r\u00e9sultats incorrects ou incoh\u00e9rents. Les exclusions mutuelles permettent de prot\u00e9ger les sections critiques du code pour \u00e9viter ces probl\u00e8mes.

    En C++, les exclusions mutuelles sont souvent mises en \u0153uvre \u00e0 l'aide de verrous (locks) fournis par la biblioth\u00e8que standard. Ces verrous sont des objets qui peuvent \u00eatre verrouill\u00e9s (locked) par un thread pour emp\u00eacher d'autres threads d'acc\u00e9der \u00e0 une section critique, puis d\u00e9verrouill\u00e9s (unlocked) pour permettre \u00e0 d'autres threads d'acc\u00e9der \u00e0 cette section.

    En principe les v\u00e9rrous sont utilis\u00e9s pour\u2009:

    • Prot\u00e9ger les donn\u00e9es partag\u00e9es entre plusieurs threads
    • Prot\u00e9ger les ressources partag\u00e9es entre plusieurs threads (fichiers, sockets, etc.)
    • Synchroniser l'acc\u00e8s \u00e0 des sections critiques du code
    • Synchroniser la communication entre threads
    • Synchroniser la terminaison de threads
    • etc.
    "}, {"location": "course-concurrent/mutex/#histoire", "title": "Histoire", "text": "

    Le concept d'exclusion mutuelle a \u00e9t\u00e9 introduit par Edsger Dijkstra en 1965. Il a invent\u00e9 le terme \u00ab\u2009exclusion mutuelle\u2009\u00bb pour d\u00e9crire une propri\u00e9t\u00e9 souhaitable des syst\u00e8mes informatiques, \u00e0 savoir que deux processus ne peuvent pas \u00eatre simultan\u00e9ment dans une section critique. Dijkstra a \u00e9galement invent\u00e9 l'algorithme du \u00ab\u2009banquier\u2009\u00bb pour \u00e9viter les interblocages dans les syst\u00e8mes informatiques.

    Dans son article (Solution of a Problem in Concurrent Programming Control)[https://www.cs.utexas.edu/users/EWD/ewd01xx/EWD123.PDF], Dijkstra a introduit le concept de s\u00e9maphores, qui sont des variables enti\u00e8res utilis\u00e9es pour la synchronisation entre processus ou threads. Les s\u00e9maphores peuvent \u00eatre utilis\u00e9s pour impl\u00e9menter l'exclusion mutuelle, ainsi que d'autres formes de synchronisation.

    Dekker a introduit un autre algorithme pour l'exclusion mutuelle entre deux processus en 1968. Cet algorithme est similaire \u00e0 l'algorithme de Peterson, mais utilise des variables suppl\u00e9mentaires pour \u00e9viter les probl\u00e8mes de synchronisation.

    Peterson a introduit un algorithme pour l'exclusion mutuelle entre deux processus en 1981. Cet algorithme est souvent utilis\u00e9 pour illustrer les concepts d'exclusion mutuelle et de synchronisation dans les cours d'informatique.

    \u00c0 cette \u00e9poque les processeurs multi-c\u0153urs n'existaient pas, et les syst\u00e8mes informatiques \u00e9taient g\u00e9n\u00e9ralement des syst\u00e8mes \u00e0 un seul processeur. Les probl\u00e8mes de synchronisation et d'exclusion mutuelle \u00e9taient donc moins complexes que dans les syst\u00e8mes modernes. Un processeur moderne dispose de plusieurs c\u0153urs, et chaque c\u0153ur peut ex\u00e9cuter plusieurs threads simultan\u00e9ment. La synchronisation et l'exclusion mutuelle sont donc des probl\u00e8mes plus complexes dans les syst\u00e8mes modernes.

    "}, {"location": "course-concurrent/mutex/#exemple-du-compteur-partage", "title": "Exemple du compteur partag\u00e9", "text": "

    Consid\u00e9rons un exemple simple o\u00f9 deux threads partagent un compteur. Chaque thread incr\u00e9mente le compteur 1000000 fois. Si les threads ne sont pas synchronis\u00e9s, le r\u00e9sultat final d\u00e9pendra de l'ordre d'ex\u00e9cution des threads.

    En effet, si les deux threads tentent d'incr\u00e9menter le compteur simultan\u00e9ment, le r\u00e9sultat final sera impr\u00e9visible car l'ex\u00e9cution des instructions n'est pas atomique. Le processeur va devoir d\u00e9composer l'op\u00e9ration d'incr\u00e9mentation en plusieurs \u00e9tapes\u2009:

    1. Charger la valeur actuelle du compteur depuis la m\u00e9moire dans un registre
    2. Incr\u00e9menter la valeur dans le registre
    3. \u00c9crire la nouvelle valeur du registre dans la m\u00e9moire

    Si un autre thread modifie la valeur du compteur entre ces \u00e9tapes, le r\u00e9sultat final sera incorrect.

    Pour \u00e9viter cela, nous devons synchroniser l'acc\u00e8s au compteur pour que les threads ne puissent pas l'incr\u00e9menter simultan\u00e9ment. La partie d'incr\u00e9mentation est nomm\u00e9e section critique.

    // counter.cpp\n#include <iostream>\n#include <thread>\n#include <vector>\n\nsize_t counter = 0;\n\nclass Worker {\npublic:\n    Worker(size_t max_iterations = 1000000) : max_iterations(max_iterations) {}\n    const size_t max_iterations;\n    void operator() () {\n        for (size_t i = 0; i < max_iterations; ++i)\n        { // section critique\n            ++counter;\n        }\n    }\n};\n\nint main() {\n    const int num_threads = std::thread::hardware_concurrency();\n\n    std::vector<std::jthread> threads;\n    Worker worker;\n    for (int i = 0; i < num_threads; ++i)\n        threads.emplace_back(std::jthread(worker));\n\n    for (auto& t : threads) t.join();\n\n    std::cout << \"Number of threads: \" << num_threads << std::endl;\n    std::cout << \"Expected Counter: \" << worker.max_iterations * num_threads << std::endl;\n    std::cout << \"Actual Counter: \" << counter << std::endl;\n}\n
    "}, {"location": "course-concurrent/mutex/#verrous-mutex", "title": "Verrous Mutex", "text": "

    En C++, les verrous sont g\u00e9n\u00e9ralement impl\u00e9ment\u00e9s \u00e0 l'aide de la classe std::mutex de la biblioth\u00e8que standard. Un verrou de type std::mutex peut \u00eatre verrouill\u00e9 \u00e0 l'aide de la m\u00e9thode lock() et d\u00e9verrouill\u00e9 \u00e0 l'aide de la m\u00e9thode unlock().

    "}, {"location": "course-concurrent/mutex/#exemple-simple", "title": "Exemple simple", "text": "
    #include <iostream>\n#include <thread>\n\nstatic int x = 0;\n\nint a() { for(;;) { x = 5; std::cout << x; } }\nint b() { for(;;) x = 7; }\n\nint main() {\n    std::thread ta(a);\n    std::thread tb(b);\n    ta.join();\n    tb.join();\n}\n
    $ g++ sync.cpp\n$ timeout 10s ./a.out\n$ grep -o 7 data | wc -l\n$ wc -c < data\n

    On peut voir qu'il y a environ 1 change sur 10000 d'avoir un 7.

    "}, {"location": "course-concurrent/mutex/#autre-exemple", "title": "Autre Exemple", "text": "

    Dans cet exemple, nous avons deux threads a et b qui partagent une variable globale x. Le thread a affecte la valeur 5 \u00e0 x et l'affiche, tandis que le thread b affecte la valeur 7 \u00e0 x. Les threads s'ex\u00e9cutent ind\u00e9pendamment et peuvent modifier x simultan\u00e9ment, ce qui peut entra\u00eener des r\u00e9sultats impr\u00e9visibles.

    #include <iostream>\n#include <thread>\n\nstatic int x = 0;\n\nint a() { for(;;) { x = 5; std::cout << x; } }\nint b() { for(;;) x = 7; }\n\nint main() {\n    std::thread ta(a);\n    std::thread tb(b);\n    ta.join();\n    tb.join();\n}\n

    Exercice\u2009: Testez quelle est la probablilit\u00e9 d'avoir un 7 dans la sortie.

    Pour cela faite tourner le programme en redirigeant la sortie standard vers un fichier, puis comptez le nombre de 7 dans le fichier. Ex\u00e9cutez le programme pendant 5 secondes avec la commande timeout.

    Attention, le fichier peut \u00eatre tr\u00e8s volumineux, la commande grep pour chercher les 7 peut \u00eatre utilis\u00e9e comme suit\u2009:

    $ timeout 5s ./a.out > data\n$ grep -o 5 data | wc -l\n$ grep -o 7 data | wc -l\n
    ", "tags": ["timeout"]}, {"location": "course-concurrent/os/", "title": "Syst\u00e8me d'exploitation (POSIX)", "text": ""}, {"location": "course-concurrent/os/#processus", "title": "Processus", "text": "

    Un processus est un programme en cours d'ex\u00e9cution. Il est compos\u00e9 d'un espace d'adressage, d'un ensemble de ressources (fichiers, sockets, etc.) et d'un ou plusieurs threads. Un processus est identifi\u00e9 par un PID (Process IDentifier).

    "}, {"location": "course-concurrent/os/#etats-dun-processus", "title": "\u00c9tats d'un processus", "text": ""}, {"location": "course-concurrent/os/#etat-du-processus", "title": "Etat du processus", "text": "

    Nouveau (New) : Un processus vient d'\u00eatre cr\u00e9\u00e9 mais n'a pas encore \u00e9t\u00e9 admis par l'ordonnanceur du syst\u00e8me d'exploitation pour ex\u00e9cution.

    Pr\u00eat (Ready) : Le processus est charg\u00e9 en m\u00e9moire et attend qu'un processeur lui soit attribu\u00e9 par l'ordonnanceur pour ex\u00e9cuter ses instructions.

    Ex\u00e9cution (Running) : Le processus est en cours d'ex\u00e9cution sur un processeur. Dans cet \u00e9tat, le processus effectue les op\u00e9rations pour lesquelles il a \u00e9t\u00e9 con\u00e7u.

    En attente (Waiting ou Blocked) : Le processus ne peut pas ex\u00e9cuter tant qu'un certain \u00e9v\u00e9nement n'a pas lieu, comme la fin d'une op\u00e9ration d'entr\u00e9e/sortie. Dans cet \u00e9tat, le processus est suspendu et attend que la condition bloquante soit lev\u00e9e.

    Termin\u00e9 (Terminated ou Zombie) : Un processus entre dans cet \u00e9tat une fois qu'il a fini de s'ex\u00e9cuter. Cependant, il reste dans la table des processus jusqu'\u00e0 ce que son processus parent r\u00e9cup\u00e8re son code de sortie, permettant au syst\u00e8me de lib\u00e9rer les ressources associ\u00e9es. Un processus en \u00e9tat \u00ab\u2009zombie\u2009\u00bb a termin\u00e9 son ex\u00e9cution mais attend que son processus parent appelle wait() pour r\u00e9cup\u00e9rer son statut de sortie.

    Orphelin (Orphan) : Un processus devient orphelin si son processus parent se termine avant lui. Les processus orphelins sont adopt\u00e9s par le processus init (PID 1), qui r\u00e9cup\u00e8re automatiquement leur statut de sortie, \u00e9vitant ainsi la cr\u00e9ation de zombies.

    Interrompu (Stopped) : Un processus peut \u00eatre mis en pause (ou arr\u00eat\u00e9 temporairement) par un signal (par exemple, SIGSTOP). Il peut \u00eatre repris plus tard (par exemple, avec le signal SIGCONT).

    Zombie (Zombie) : Comme mentionn\u00e9 pr\u00e9c\u00e9demment, un processus \u00ab\u2009zombie\u2009\u00bb ou \u00ab\u2009processus d\u00e9funt\u2009\u00bb est un processus qui a termin\u00e9 son ex\u00e9cution mais reste dans la table des processus parce que son processus parent n'a pas encore r\u00e9cup\u00e9r\u00e9 son statut de sortie. Cela permet au parent de r\u00e9cup\u00e9rer les informations sur le statut de fin de son enfant. Un processus zombie ne consomme pas de ressources \u00e0 part une entr\u00e9e dans la table des processus.

    "}, {"location": "course-concurrent/os/#creation-dun-processus", "title": "Cr\u00e9ation d'un processus", "text": "

    Sous les syst\u00e8mes d'exploitation POSIX, il existe principalement deux fa\u00e7ons de cr\u00e9er un processus\u2009:

    "}, {"location": "course-concurrent/os/#fork", "title": "fork()", "text": "

    La fonction fork() est utilis\u00e9e pour cr\u00e9er un nouveau processus, appel\u00e9 processus enfant, qui est une copie du processus appelant (processus parent). L'enfant re\u00e7oit une copie des donn\u00e9es du parent, mais les deux processus ont des espaces d'adressage s\u00e9par\u00e9s. Apr\u00e8s le fork(), les deux processus (parent et enfant) continuent leur ex\u00e9cution \u00e0 partir de l'instruction suivante apr\u00e8s l'appel fork(). La principale diff\u00e9rence entre eux est la valeur de retour de fork(): 0 pour l'enfant et l'ID du processus enfant (PID) pour le parent, ou -1 en cas d'\u00e9chec.

    "}, {"location": "course-concurrent/os/#exec", "title": "exec()", "text": "

    La famille de fonctions exec() est utilis\u00e9e pour ex\u00e9cuter un nouveau programme dans l'espace d'adressage d'un processus. Cela remplace l'image du processus actuel par une nouvelle image de programme. Les diff\u00e9rentes variantes de exec() (comme execl(), execp(), execv(), etc.) permettent de sp\u00e9cifier le programme \u00e0 ex\u00e9cuter et de passer des arguments de diff\u00e9rentes mani\u00e8res. Habituellement, exec() est appel\u00e9 par un processus enfant apr\u00e8s un fork() pour remplacer son image par celle du programme \u00e0 ex\u00e9cuter, permettant ainsi au processus parent de continuer \u00e0 ex\u00e9cuter son code original tout en lan\u00e7ant un nouveau programme dans le processus enfant. Ces deux fonctions sont souvent utilis\u00e9es ensemble pour cr\u00e9er un nouveau processus et ex\u00e9cuter un nouveau programme au sein de ce processus. Le mod\u00e8le \u00ab\u2009fork-exec\u2009\u00bb est un motif commun dans les syst\u00e8mes POSIX pour la cr\u00e9ation de processus et l'ex\u00e9cution de programmes. Ce m\u00e9canisme permet une grande flexibilit\u00e9 dans la gestion des processus, en permettant \u00e0 un processus parent de contr\u00f4ler l'ex\u00e9cution de processus enfants et de r\u00e9cup\u00e9rer leur statut de sortie une fois qu'ils ont termin\u00e9.

    "}, {"location": "course-concurrent/os/#exemple-de-zombie", "title": "Exemple de zombie", "text": "
    #include <iostream>\n#include <unistd.h> // Pour fork(), getpid(), sleep()\n#include <sys/wait.h> // Pour wait()\n\nint main() {\n    pid_t pid = fork(); // Cr\u00e9e un nouveau processus\n\n    if (pid == -1) {\n        // En cas d'\u00e9chec du fork()\n        std::cerr << \"\u00c9chec du fork()\" << std::endl;\n        return 1;\n    } else if (pid > 0) {\n        // Code ex\u00e9cut\u00e9 par le processus parent\n        std::cout << \"Je suis le processus parent (PID: \" << getpid() << \"), et mon enfant a le PID \" << pid << std::endl;\n        std::cout << \"Je dors 20 secondes. V\u00e9rifiez l'\u00e9tat du processus enfant avec 'ps -l'.\" << std::endl;\n        sleep(20); // Le parent dort, laissant l'enfant devenir un zombie\n        std::cout << \"Le parent se r\u00e9veille et se termine. Le processus enfant devrait \u00eatre nettoy\u00e9.\" << std::endl;\n    } else {\n        // Code ex\u00e9cut\u00e9 par le processus enfant\n        std::cout << \"Je suis le processus enfant (PID: \" << getpid() << \") et je me termine.\" << std::endl;\n        // L'enfant se termine imm\u00e9diatement, devenant un zombie jusqu'\u00e0 ce\n        // que le parent termine son sommeil\n    }\n}\n
    "}, {"location": "course-concurrent/os/#status-des-processus-avec-ps", "title": "Status des processus avec ps", "text": "

    Lorsque vous utilisez la commande ps -l sur un syst\u00e8me UNIX ou Linux pour lister les processus avec des informations d\u00e9taill\u00e9es, vous pouvez observer diff\u00e9rents \u00e9tats de processus repr\u00e9sent\u00e9s par des lettres. Voici les principaux \u00e9tats que vous pourriez voir\u2009:

    D: Non interrompible (uninterruptible sleep) - Le processus est en attente d'une op\u00e9ration d'entr\u00e9e/sortie et ne peut pas \u00eatre interrompu.

    R: Ex\u00e9cutable (running or runnable) - Le processus est en cours d'ex\u00e9cution sur un processeur ou en attente d'\u00eatre ex\u00e9cut\u00e9.

    S : Endormi (interruptible sleep) - Le processus attend un \u00e9v\u00e9nement pour se poursuivre.

    T : Arr\u00eat\u00e9 (stopped) - Le processus a \u00e9t\u00e9 arr\u00eat\u00e9, g\u00e9n\u00e9ralement par un signal de contr\u00f4le comme SIGSTOP.

    Z : Zombie (defunct) - Le processus s'est termin\u00e9, mais des informations sont toujours conserv\u00e9es dans la table des processus car le processus parent n'a pas encore r\u00e9cup\u00e9r\u00e9 son statut de sortie.

    I : Processus inactif (idle) - Utilis\u00e9 pour les t\u00e2ches du noyau.

    W : Pagin\u00e9 - Un \u00e9tat obsol\u00e8te non utilis\u00e9 dans les versions modernes de Unix/Linux, indiquant qu'un processus \u00e9tait \u00e9chang\u00e9 (swapped out). L'\u00e9tat du processus est une information cl\u00e9 pour comprendre ce que fait un processus \u00e0 un moment donn\u00e9. Les \u00e9tats comme \u00ab\u2009D\u2009\u00bb et \u00ab\u2009S\u2009\u00bb indiquent qu'un processus attend quelque chose pour continuer, tandis que \u00ab\u2009R\u2009\u00bb signifie que le processus est soit en cours d'ex\u00e9cution, soit pr\u00eat \u00e0 \u00eatre ex\u00e9cut\u00e9 d\u00e8s qu'un processeur est disponible. Les \u00e9tats \u00ab\u2009T\u2009\u00bb et \u00ab\u2009Z\u2009\u00bb sont des cas sp\u00e9ciaux, indiquant respectivement un processus arr\u00eat\u00e9 et un processus qui s'est termin\u00e9 mais dont les ressources ne sont pas encore totalement lib\u00e9r\u00e9es par le syst\u00e8me.

    "}, {"location": "course-concurrent/os/#communication-inter-processus-ipc", "title": "Communication inter-processus (IPC)", "text": "

    Les processus dans les syst\u00e8mes d'exploitation POSIX peuvent communiquer entre eux par plusieurs m\u00e9canismes de communication interprocessus (IPC - Inter-Process Communication). Ces m\u00e9canismes permettent aux processus de partager des donn\u00e9es et de coordonner leurs actions. Voici les principaux moyens de communication interprocessus\u2009:

    Pipes (tuyaux) : Un pipe permet \u00e0 un flux de donn\u00e9es de passer d'un processus \u00e0 l'autre. Les pipes anonymes sont utilis\u00e9s pour la communication entre un processus parent et son enfant ou entre enfants d'un m\u00eame parent. Les pipes nomm\u00e9s (FIFOs) peuvent \u00eatre utilis\u00e9s entre n'importe quels processus sur le syst\u00e8me.

    Signaux : Les signaux sont des messages simples envoy\u00e9s \u00e0 un processus pour lui indiquer qu'un \u00e9v\u00e9nement particulier s'est produit. Bien qu'ils ne transportent pas de grandes quantit\u00e9s de donn\u00e9es, ils sont utiles pour contr\u00f4ler les op\u00e9rations des processus et pour la gestion d'\u00e9v\u00e9nements asynchrones.

    M\u00e9moire partag\u00e9e : La m\u00e9moire partag\u00e9e est un segment de m\u00e9moire qui peut \u00eatre acc\u00e9d\u00e9 par plusieurs processus. C'est un moyen efficace pour \u00e9changer de grandes quantit\u00e9s de donn\u00e9es entre processus, car elle \u00e9vite la copie de donn\u00e9es d'un espace d'adressage \u00e0 l'autre.

    S\u00e9maphores : Les s\u00e9maphores sont utilis\u00e9s pour synchroniser l'acc\u00e8s \u00e0 des ressources partag\u00e9es par plusieurs processus. Ils peuvent \u00eatre utilis\u00e9s pour \u00e9viter les conditions de course en contr\u00f4lant l'acc\u00e8s \u00e0 des ressources telles que les fichiers ou les segments de m\u00e9moire partag\u00e9e.

    Files de messages : Les files de messages permettent aux processus d'envoyer et de recevoir des messages sous forme de files d'attente. Chaque message est une structure de donn\u00e9es qui peut contenir des informations vari\u00e9es. Les files de messages sont utiles pour \u00e9changer de petites quantit\u00e9s de donn\u00e9es de mani\u00e8re asynchrone.

    Sockets : Les sockets permettent la communication entre processus sur le m\u00eame ordinateur ou entre processus sur des ordinateurs diff\u00e9rents dans un r\u00e9seau. Ils supportent la communication en mode connect\u00e9 (TCP) et non connect\u00e9 (UDP) et sont la base de nombreuses communications r\u00e9seau, y compris le Web.

    Ces m\u00e9canismes offrent divers degr\u00e9s d'abstraction et peuvent \u00eatre choisis en fonction des besoins sp\u00e9cifiques en mati\u00e8re de communication interprocessus, comme la quantit\u00e9 de donn\u00e9es \u00e0 transf\u00e9rer, la n\u00e9cessit\u00e9 de synchronisation ou la pr\u00e9f\u00e9rence entre la communication locale et la communication en r\u00e9seau.

    "}, {"location": "course-concurrent/os/#exemple", "title": "Exemple", "text": "
    #include <iostream>\n#include <unistd.h>\n#include <sys/wait.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n#include <signal.h>\n#include <vector>\n\nconstexpr int SHM_SIZE = 1024; // Taille du segment de m\u00e9moire partag\u00e9e\nconstexpr int NUM_CHILDREN = 10; // Nombre de processus enfants \u00e0 cr\u00e9er\n\n// Handler de signal pour les processus enfants\nvoid signalHandler(int sig) {\n    std::cout << \"Processus \" << getpid() << \" lit la valeur.\" << std::endl;\n    // Ici, vous acc\u00e9deriez \u00e0 la m\u00e9moire partag\u00e9e pour lire la valeur\n    // C'est une simplification; l'acc\u00e8s r\u00e9el n\u00e9cessiterait des pointeurs et une synchronisation\n}\n\nint main() {\n    int shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);\n    if (shm_id < 0) {\n        std::cerr << \"\u00c9chec de la cr\u00e9ation de la m\u00e9moire partag\u00e9e.\" << std::endl;\n        return 1;\n    }\n    int* shared_memory = (int*)shmat(shm_id, nullptr, 0);\n    *shared_memory = 0; // Initialisation de la m\u00e9moire partag\u00e9e\n\n    signal(SIGUSR1, signalHandler); // Configuration du handler de signal pour tous les processus\n\n    std::vector<pid_t> children;\n\n    // Cr\u00e9ation de 10 processus enfants\n    for (int i = 0; i < NUM_CHILDREN; ++i) {\n        pid_t pid = fork();\n        if (pid == 0) { // Enfant\n            // Boucle infinie pour que l'enfant reste actif\n            while (true) pause(); // Attend le signal\n            exit(0);\n        } else if (pid > 0) { // Parent\n            children.push_back(pid);\n        } else {\n            std::cerr << \"\u00c9chec du fork().\" << std::endl;\n            return 1;\n        }\n    }\n\n    // Code du processus parent pour modifier la m\u00e9moire partag\u00e9e et envoyer des signaux\n    while (true) {\n        int val;\n        std::cout << \"Entrez une valeur \u00e0 \u00e9crire dans la m\u00e9moire partag\u00e9e: \";\n        std::cin >> val;\n        *shared_memory = val; // \u00c9crit dans la m\u00e9moire partag\u00e9e\n        for (pid_t child : children) {\n            kill(child, SIGUSR1); // Envoie un signal \u00e0 chaque enfant\n        }\n    }\n\n    // Nettoyage (pas atteint dans cet exemple simplifi\u00e9)\n    for (pid_t child : children) {\n        int status;\n        waitpid(child, &status, 0);\n    }\n    shmdt(shared_memory);\n    shmctl(shm_id, IPC_RMID, nullptr);\n}\n

    Avec htop, vous pouvez observer les processus enfants en attente de signal. Utilisez la fonction t pour afficher le mode arbre. Vous pouvez \u00e9galement observer la m\u00e9moire partag\u00e9e avec ipcs -m.

    Dans cet exemple, un segment de m\u00e9moire partag\u00e9e est cr\u00e9\u00e9 pour stocker une valeur enti\u00e8re. Le processus parent \u00e9crit une valeur dans la m\u00e9moire partag\u00e9e et envoie un signal \u00e0 chaque processus enfant. Chaque processus enfant est configur\u00e9 pour ex\u00e9cuter un gestionnaire de signal qui lit la valeur de la m\u00e9moire partag\u00e9e. Cela permet de communiquer des donn\u00e9es entre le processus parent et les processus enfants de mani\u00e8re asynchrone.

    N\u00e9anmoins il faut d'avantage pour g\u00e9rer la synchronisation et la communication entre les processus. Les exemples ci-dessus sont des simplifications pour illustrer les concepts de base.

    ", "tags": ["htop"]}, {"location": "course-concurrent/os/#synchronisation-de-la-memoire-partagee", "title": "Synchronisation de la m\u00e9moire partag\u00e9e", "text": "

    Dans cet exemple, on se contente d'informer l'utilisateur qu'une valeur a \u00e9t\u00e9 d\u00e9pos\u00e9e dans la m\u00e9moire partag\u00e9e mais on ne peut pas la lire directement. On a besoin d'un m\u00e9canisme de synchronisation.

    #include <iostream>\n#include <unistd.h>\n#include <sys/wait.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n#include <signal.h>\n#include <vector>\n#include <semaphore.h>\n\nconstexpr int SHM_SIZE = sizeof(int); // Taille du segment de m\u00e9moire partag\u00e9e pour stocker un entier\nconstexpr int NUM_CHILDREN = 10; // Nombre de processus enfants \u00e0 cr\u00e9er\n\nint* shared_memory; // Pointeur vers la m\u00e9moire partag\u00e9e\nsem_t* sem; // Pointeur vers le s\u00e9maphore\n\nvoid signalHandler(int sig) {\n    sem_wait(sem); // Attendre pour acc\u00e9der \u00e0 la section critique\n    std::cout << \"Processus \" << getpid() << \" lit la valeur \" << *shared_memory << \".\" << std::endl;\n    sem_post(sem); // Lib\u00e9rer l'acc\u00e8s \u00e0 la section critique\n}\n\nint main() {\n    // Cr\u00e9ation de la m\u00e9moire partag\u00e9e\n    int shm_id = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);\n    if (shm_id < 0) {\n        std::cerr << \"\u00c9chec de la cr\u00e9ation de la m\u00e9moire partag\u00e9e.\" << std::endl;\n        return 1;\n    }\n    shared_memory = (int*)shmat(shm_id, nullptr, 0);\n\n    // Initialisation du s\u00e9maphore dans la m\u00e9moire partag\u00e9e\n    sem = new sem_t;\n    if (sem_init(sem, 1, 1) == -1) { // 1 pour partage entre processus\n        std::cerr << \"Erreur d'initialisation du s\u00e9maphore.\" << std::endl;\n        return 1;\n    }\n\n    signal(SIGUSR1, signalHandler); // Configuration du gestionnaire de signal\n\n    std::vector<pid_t> children;\n\n    // Cr\u00e9ation des processus enfants\n    for (int i = 0; i < NUM_CHILDREN; ++i) {\n        pid_t pid = fork();\n        if (pid == 0) { // Enfant\n            while (true) pause(); // Attend le signal\n            exit(0);\n        } else if (pid > 0) { // Parent\n            children.push_back(pid);\n        } else {\n            std::cerr << \"\u00c9chec du fork().\" << std::endl;\n            return 1;\n        }\n    }\n\n    // Code du processus parent pour lire et mettre \u00e0 jour la m\u00e9moire partag\u00e9e\n    while (true) {\n        int val;\n        std::cout << \"Entrez une valeur \u00e0 \u00e9crire dans la m\u00e9moire partag\u00e9e: \";\n        std::cin >> val;\n        sem_wait(sem); // Acc\u00e9der \u00e0 la section critique\n        *shared_memory = val; // \u00c9crire dans la m\u00e9moire partag\u00e9e\n        sem_post(sem); // Lib\u00e9rer l'acc\u00e8s\n        for (pid_t child : children) {\n            kill(child, SIGUSR1); // Signaler aux enfants de lire la valeur\n        }\n    }\n\n    // Nettoyage (non atteint dans cet exemple)\n    for (pid_t child : children) {\n        int status;\n        waitpid(child, &status, 0);\n    }\n    shmdt(shared_memory);\n    shmctl(shm_id, IPC_RMID, nullptr);\n    sem_destroy(sem);\n    delete sem;\n}\n
    "}, {"location": "course-concurrent/os/#processus-leger-versus-processus-clonefork", "title": "Processus l\u00e9ger versus processus (clone/fork)", "text": "

    Sous Linux, l'appel syst\u00e8me utilis\u00e9 pour cr\u00e9er des threads est clone() ou, dans certains cas, des variantes comme fork() ou vfork(). Cependant, clone() est plus flexible et permet une plus grande personnalisation du partage d'espace d'adressage, des fichiers, des signaux, etc., ce qui le rend particuli\u00e8rement adapt\u00e9 pour la cr\u00e9ation de threads.

    clone() : Cet appel syst\u00e8me est utilis\u00e9 pour cr\u00e9er un processus l\u00e9ger (un thread, dans ce contexte). Contrairement \u00e0 fork(), qui duplique le processus appelant dans un nouveau processus avec un espace d'adressage s\u00e9par\u00e9, clone() permet de sp\u00e9cifier exactement quels \u00e9l\u00e9ments (espace d'adressage, table des fichiers ouverts, espace des signaux, etc.) doivent \u00eatre partag\u00e9s entre le thread appelant et le nouveau thread. Cela permet une cr\u00e9ation de thread plus efficace et un partage de ressources entre les threads.

    "}, {"location": "course-concurrent/os/#processus_1", "title": "Processus", "text": "

    Un processus est une instance d'un programme en cours d'ex\u00e9cution. Chaque processus poss\u00e8de son propre espace d'adressage virtuel, ses propres ressources (comme des descripteurs de fichiers) et son propre contexte d'ex\u00e9cution (registres, pile, etc.). Les processus sont isol\u00e9s les uns des autres, ce qui signifie que sans m\u00e9canismes de communication inter-processus (IPC), ils ne peuvent pas directement partager de donn\u00e9es. L'isolation entre les processus assure la s\u00e9curit\u00e9 et la stabilit\u00e9 du syst\u00e8me, car l'erreur dans un processus ne peut pas corrompre directement l'espace m\u00e9moire d'un autre processus.

    "}, {"location": "course-concurrent/os/#thread-processus-leger", "title": "Thread (Processus l\u00e9ger)", "text": "

    Un thread, souvent appel\u00e9 un processus l\u00e9ger, est une unit\u00e9 d'ex\u00e9cution qui peut \u00eatre cr\u00e9\u00e9e au sein d'un processus. Contrairement aux processus, les threads d'un m\u00eame processus partagent le m\u00eame espace d'adressage et peuvent acc\u00e9der aux m\u00eames donn\u00e9es en m\u00e9moire. Cela facilite le partage d'informations et la communication entre les threads, mais requiert \u00e9galement une synchronisation soign\u00e9e pour \u00e9viter les conditions de course et les incoh\u00e9rences de donn\u00e9es. Les threads ont leur propre pile d'ex\u00e9cution et leur propre contexte de registres, mais partagent d'autres ressources avec les threads du m\u00eame processus, comme les descripteurs de fichiers et les segments de m\u00e9moire allou\u00e9s.

    "}, {"location": "course-concurrent/os/#differences-cles", "title": "Diff\u00e9rences cl\u00e9s", "text": "

    Isolation\u2009: Les processus sont isol\u00e9s les uns des autres, tandis que les threads partagent l'espace d'adressage du processus qui les a cr\u00e9\u00e9s. Cr\u00e9ation et terminaison\u2009: Cr\u00e9er un nouveau processus est g\u00e9n\u00e9ralement plus co\u00fbteux en termes de ressources et de temps que cr\u00e9er un thread. De m\u00eame, terminer un processus et nettoyer ses ressources est plus lourd que terminer un thread.

    Communication\u2009: La communication entre processus n\u00e9cessite l'utilisation de m\u00e9canismes IPC, tels que les files de messages, les tubes (pipes), la m\u00e9moire partag\u00e9e, etc. En revanche, les threads peuvent communiquer directement en lisant et \u00e9crivant dans la m\u00e9moire partag\u00e9e du processus, bien que cela n\u00e9cessite une synchronisation, comme l'utilisation de verrous ou de s\u00e9maphores. Utilisation\u2009: Les processus sont utiles pour ex\u00e9cuter des t\u00e2ches isol\u00e9es et s\u00e9curis\u00e9es, sans risque d'interf\u00e9rence directe entre elles. Les threads sont pr\u00e9f\u00e9r\u00e9s pour les t\u00e2ches qui n\u00e9cessitent un partage intensif de donn\u00e9es ou une communication rapide entre les unit\u00e9s d'ex\u00e9cution au sein d'une m\u00eame application.

    En r\u00e9sum\u00e9, bien que les threads (ou processus l\u00e9gers) et les processus servent tous deux \u00e0 ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente, ils diff\u00e8rent par leur niveau d'isolation, leurs co\u00fbts de cr\u00e9ation et de gestion, ainsi que par leur mani\u00e8re de communiquer et de partager des donn\u00e9es.

    "}, {"location": "course-concurrent/os/#appel-systeme-clone", "title": "Appel syst\u00e8me clone()", "text": "
    #include <sched.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <sys/wait.h>\n#include <unistd.h>\n\n// Taille de la pile allou\u00e9e pour le thread cr\u00e9\u00e9 par clone\n#define STACK_SIZE (1024 * 1024)\n\n// La fonction \u00e0 ex\u00e9cuter dans le thread\nint threadFunction(void *arg) {\n    printf(\"Bonjour du thread!\\n\");\n    return 0;\n}\n\nint main() {\n    // Alloue de l'espace pour la pile du thread\n    char *stack = (char *)malloc(STACK_SIZE);\n    if (stack == NULL) {\n        perror(\"malloc\");\n        exit(1);\n    }\n\n    // Pr\u00e9pare le sommet de la pile pour le nouveau thread (clone grandit vers le bas)\n    char *stackTop = stack + STACK_SIZE;\n\n    // Cr\u00e9e le thread. Les flags d\u00e9terminent quels \u00e9l\u00e9ments sont partag\u00e9s.\n    // CLONE_VM partage l'espace d'adressage, et SIGCHLD est utilis\u00e9 pour que waitpid() puisse \u00eatre utilis\u00e9.\n    pid_t pid = clone(threadFunction, stackTop, CLONE_VM | SIGCHLD, NULL);\n\n    if (pid == -1) {\n        perror(\"clone\");\n        exit(1);\n    }\n\n    // Attend que le thread (processus) cr\u00e9\u00e9 se termine\n    if (waitpid(pid, NULL, 0) == -1) {\n        perror(\"waitpid\");\n        exit(1);\n    }\n\n    // Nettoie\n    free(stack);\n\n    printf(\"Thread termin\u00e9\\n\");\n}\n
    "}, {"location": "course-concurrent/os/#signaux", "title": "Signaux", "text": "

    Sous les syst\u00e8mes d'exploitation POSIX, les signaux sont des messages logiciels envoy\u00e9s \u00e0 un processus pour lui indiquer qu'un \u00e9v\u00e9nement particulier s'est produit. Les signaux sont utilis\u00e9s pour g\u00e9rer les interruptions, les erreurs et les \u00e9v\u00e9nements asynchrones, et peuvent \u00eatre envoy\u00e9s par le noyau, par d'autres processus ou par le processus lui-m\u00eame.

    Voici les signaux POSIX standard et leurs descriptions\u2009:

    Signal Description SIGHUP(1) Terminaison du terminal ou du processus contr\u00f4lant SIGINT(2) Interruption depuis le clavier SIGQUIT(3) Interruption depuis le clavier avec un core dump SIGILL(4) Instruction ill\u00e9gale SIGTRAP(5) Trace ou point d'arr\u00eat SIGABRT(6) Signal d'abandon SIGBUS(7) Erreur de bus SIGFPE(8) Erreur arithm\u00e9tique SIGKILL(9) Terminaison forc\u00e9e SIGUSR1(10) Signal utilisateur 1 SIGSEGV(11) Violation de la segmentation SIGUSR2(12) Signal utilisateur 2 SIGPIPE(13) \u00c9criture sur un tube sans lecteur SIGALRM(14) Alarme horloge SIGTERM(15) Terminaison SIGSTKFLT(16) Erreur de pile SIGCHLD(17) Enfant termin\u00e9 ou arr\u00eat\u00e9 SIGCONT(18) Continuer l'ex\u00e9cution, si arr\u00eat\u00e9 SIGSTOP(19) Arr\u00eat de l'ex\u00e9cution du processus SIGTSTP(20) Arr\u00eat de l'ex\u00e9cution du processus depuis le clavier SIGTTIN(21) Lecture depuis le terminal en arri\u00e8re-plan SIGTTOU(22) \u00c9criture sur le terminal en arri\u00e8re-plan SIGURG(23) Donn\u00e9es urgentes sur le socket SIGXCPU(24) Temps CPU \u00e9coul\u00e9 SIGXFSZ(25) Taille de fichier maximale d\u00e9pass\u00e9e SIGVTALRM(26) Alarme virtuelle SIGPROF(27) Profilage du signal SIGWINCH(28) Changement de taille de fen\u00eatre SIGIO(29) \u00c9v\u00e9nement d'entr\u00e9e/sortie asynchrone SIGPWR(30) \u00c9v\u00e9nement d'alimentation SIGSYS(31) Erreur syst\u00e8me

    Les signaux les plus couramment utilis\u00e9s sont SIGINT (interruption depuis le clavier), SIGTERM (terminaison) et SIGKILL (terminaison forc\u00e9e). Ces signaux sont souvent utilis\u00e9s pour demander \u00e0 un processus de se terminer ou de r\u00e9agir \u00e0 des \u00e9v\u00e9nements utilisateur.

    ", "tags": ["SIGINT", "SIGTERM", "SIGKILL"]}, {"location": "course-concurrent/os/#information-sur-un-processus", "title": "Information sur un processus", "text": ""}, {"location": "course-concurrent/os/#pstree", "title": "pstree", "text": "

    Pour afficher l'arborescence des processus, vous pouvez utiliser la commande pstree. Par exemple, pour afficher l'arborescence des processus pour l'utilisateur actuel, vous pouvez ex\u00e9cuter\u2009:

    Exercice\u2009:

    Essayez de lancer un programme en arri\u00e8re-plan (par exemple, ./myprogram &) et utilisez pstree pour voir comment il est li\u00e9 \u00e0 d'autres processus.

    $ pstree\n
    ", "tags": ["pstree"]}, {"location": "course-concurrent/os/#proc", "title": "proc", "text": "

    Ex\u00e9cutez le programme suivant\u2009:

    // Read string and print it\n#include <iostream>\n\nint main()\n{\n    std::string s;\n    std::cin >> s;\n    std::cout << s << std::endl;\n    return 0;\n}\n
    $ g++ -o read read.cpp\n$ ./read &\n[1] 12345\n$ cat /proc/12345/status\n$ cat /proc/12345/maps\n$ cat /proc/12345/smaps\n
    ", "tags": ["proc"]}, {"location": "course-concurrent/os/#appels-systemes-utiles", "title": "Appels syst\u00e8mes utiles", "text": "Appel syst\u00e8me Description fork() Cr\u00e9e un nouveau processus en dupliquant le processus appelant. exec() Ex\u00e9cute un nouveau programme dans le contexte du processus appelant. wait() Attend la terminaison d'un processus enfant. waitpid() Attend la terminaison d'un processus enfant sp\u00e9cifique. clone() Cr\u00e9e un nouveau processus l\u00e9ger (thread) avec des options de partage personnalis\u00e9es. kill() Envoie un signal \u00e0 un processus. signal() Configure un gestionnaire de signal pour un signal donn\u00e9. pipe() Cr\u00e9e un tube (pipe) pour la communication entre processus. shmget() Alloue un segment de m\u00e9moire partag\u00e9e. shmat() Attache un segment de m\u00e9moire partag\u00e9e \u00e0 l'espace d'adressage d'un processus. sem_init() Initialise un s\u00e9maphore pour la synchronisation entre processus. mmap() Mappe un fichier ou un p\u00e9riph\u00e9rique dans l'espace d'adressage d'un processus. mprotect() Modifie les protections d'acc\u00e8s pour une r\u00e9gion de m\u00e9moire. munmap() Supprime un mappage de m\u00e9moire. nice() Modifie la priorit\u00e9 de planification d'un processus. sched_setaffinity() Modifie l'affinit\u00e9 du processeur pour un processus. getpriority() Obtient la priorit\u00e9 de planification d'un processus. setpriority() Modifie la priorit\u00e9 de planification d'un processus."}, {"location": "course-concurrent/semaphores/", "title": "Semaphore", "text": "

    Le semaphore a \u00e9t\u00e9 introduit par Edsger Dijkstra en 1965. Il s'agit d'une variable enti\u00e8re non n\u00e9gative qui peut \u00eatre utilis\u00e9e pour synchroniser l'acc\u00e8s \u00e0 une ressource partag\u00e9e. Un s\u00e9maphore est un objet de synchronisation qui permet \u00e0 plusieurs threads d'acc\u00e9der \u00e0 une ressource partag\u00e9e en m\u00eame temps.

    Historiquement, puisque Dijkstra \u00e9tait N\u00e9erlandais, il a choisi comme noms de signaux\u2009:

    • P pour \u00ab\u2009Proberen\u2009\u00bb (essayer en n\u00e9erlandais)
    • V pour \u00ab\u2009Verhogen\u2009\u00bb (augmenter en n\u00e9erlandais)

    Un s\u00e9maphore \u00e9tait vu comme la brique de base pour la synchronisation de threads. Il est toujours utilis\u00e9 dans les syst\u00e8mes d'exploitation modernes pour impl\u00e9menter des m\u00e9canismes de synchronisation tels que les mutex, les moniteurs, les barri\u00e8res, etc.

    N\u00e9anmoins, on peut \u00e9muler un s\u00e9maphore avec un mutex et une variable condition. C'est ce que fait la classe std::counting_semaphore de la biblioth\u00e8que standard C++20. Voici comment on peut \u00e9muler un s\u00e9maphore avec un mutex et une variable condition\u2009:

    #include <mutex>\n#include <condition_variable>\n\nclass Semaphore {\n    std::mutex mtx;\n    std::condition_variable cv;\n    int count;\n\npublic:\n    Semaphore(int count = 0) : count(count) {}\n\n    // Proberen\n    void notify() {\n        std::unique_lock<std::mutex> lock(mtx);\n        ++count;\n        cv.notify_one();\n    }\n\n    // Verhogen\n    void wait() {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this] { return count > 0; });\n        --count;\n    }\n};\n

    Dans cet exemple, la m\u00e9thode notify incr\u00e9mente le compteur du s\u00e9maphore et notifie un thread en attente. La m\u00e9thode wait attend que le compteur soit sup\u00e9rieur \u00e0 z\u00e9ro, puis le d\u00e9cr\u00e9mente.

    Il est parfois utile pour compter des ressources de fournir aux m\u00e9thodes notify et wait un argument n pour incr\u00e9menter ou d\u00e9cr\u00e9menter le compteur de n unit\u00e9s. Cela permet de lib\u00e9rer ou d'acqu\u00e9rir plusieurs ressources en une seule op\u00e9ration. Par exemple, si n est \u00e9gal \u00e0 3, cela signifie que trois ressources sont lib\u00e9r\u00e9es ou acquises en une seule op\u00e9ration.

    #include <mutex>\n#include <condition_variable>\n\nclass Semaphore {\n    std::mutex mtx;\n    std::condition_variable cv;\n    int count;\n\npublic:\n    Semaphore(int count = 0) : count(count) {}\n\n    void notify(int n = 1) {\n        std::unique_lock<std::mutex> lock(mtx);\n        count += n;\n        cv.notify_all();\n    }\n\n    void wait(int n = 1) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this, n] { return count >= n; });\n        count -= n;\n    }\n};\n
    ", "tags": ["wait", "notify"]}, {"location": "course-concurrent/semaphores/#probleme-du-producteur-et-du-consommateur", "title": "Probl\u00e8me du producteur et du consommateur", "text": "

    Le probl\u00e8me du producteur et du consommateur est un probl\u00e8me classique de synchronisation de threads. Il s'agit de synchroniser un ou plusieurs threads producteurs qui produisent des donn\u00e9es et un ou plusieurs threads consommateurs qui consomment ces donn\u00e9es.

    Le probl\u00e8me du producteur et du consommateur peut \u00eatre r\u00e9solu \u00e0 l'aide de deux s\u00e9maphores\u2009:

    • Un s\u00e9maphore empty pour compter le nombre d'emplacements vides dans le tampon
    • Un s\u00e9maphore full pour compter le nombre d'emplacements pleins dans le tampon
    ", "tags": ["full", "empty"]}, {"location": "course-concurrent/summary/", "title": "R\u00e9sum\u00e9", "text": "

    POSIX (Portable Operating System Interface) est une norme qui d\u00e9finit une interface syst\u00e8me standard pour les syst\u00e8mes d'exploitation de type UNIX. Cette norme a \u00e9t\u00e9 d\u00e9finie par l'IEEE (Institute of Electrical and Electronics Engineers) et l'ISO (International Organization for Standardization). Elle a \u00e9t\u00e9 invent\u00e9e en 1988 pour permettre la portabilit\u00e9 des applications entre les syst\u00e8mes UNIX.

    "}, {"location": "course-concurrent/summary/#os", "title": "OS", "text": ""}, {"location": "course-concurrent/summary/#appels-systemes", "title": "Appels Syst\u00e8mes", "text": "
    • fork : Cr\u00e9e un nouveau processus en copiant le processus appelant.
    • clone : Cr\u00e9e un nouveau processus l\u00e9ger en copiant le processus appelant.
    • exec : Remplace l'image m\u00e9moire du processus appelant par un nouveau programme.
    • wait : Attend la fin d'un processus.
    • exit : Termine le processus appelant.
    • open : Ouvre un fichier.
    • close : Ferme un fichier.
    • read : Lit des donn\u00e9es depuis un fichier.
    • write : Ecrit des donn\u00e9es dans un fichier.
    • mmap : Mappe un fichier en m\u00e9moire (new, malloc).
    • ...
    ", "tags": ["exec", "fork", "open", "read", "write", "wait", "exit", "clone", "close", "mmap"]}, {"location": "course-concurrent/summary/#processus", "title": "Processus", "text": "

    Un processus c'est un programme en cours d'ex\u00e9cution. Il poss\u00e8de un espace d'adressage, un identifiant unique (PID), un \u00e9tat (running, waiting, sleeping, zombie), un parent (PPID), des ressources (fichiers ouverts, m\u00e9moire allou\u00e9e, ...).

    Pour cr\u00e9er un processus on a vu que l'appel syst\u00e8me fork permet de dupliquer le processus appelant. Le processus fils h\u00e9rite de l'espace d'adressage du processus parent. A ce moment l\u00e0 la m\u00e9moire est copi\u00e9e en mode \u00ab\u2009copy-on-write\u2009\u00bb. Cela signifie que la m\u00e9moire n'est pas copi\u00e9e imm\u00e9diatement mais seulement lorsqu'elle est modifi\u00e9e. Et les deux processus sont ind\u00e9pendants. Donc c'est lourd en m\u00e9moire.

    ", "tags": ["fork"]}, {"location": "course-concurrent/summary/#processus-legers-ou-threads", "title": "Processus l\u00e9gers ou threads", "text": "

    Un processus l\u00e9ger se cr\u00e9e avec l'appel syst\u00e8me clone. Il est plus l\u00e9ger qu'un processus car il partage le m\u00eame espace d'adressage que le processus parent. Cela signifie que les threads partagent les m\u00eames variables globales, les m\u00eames fichiers ouverts, les m\u00eames signaux, ... Mais cela peut poser des probl\u00e8mes de synchronisation\u2009: des acc\u00e8s concurrents \u00e0 des ressources partag\u00e9es.

    ", "tags": ["clone"]}, {"location": "course-concurrent/summary/#mecanismes-de-synchronisation", "title": "M\u00e9canismes de synchronisation", "text": "

    Pour synchroniser les threads on utilise des primitives de synchronisation. Les plus courantes sont les mutex, les s\u00e9maphores et les moniteurs.

    La notion de concurrence a \u00e9t\u00e9 imagin\u00e9e par Edsger Dijkstra en 1965 pour r\u00e9soudre les probl\u00e8mes de synchronisation dans les syst\u00e8mes d'exploitation. Il \u00e9tait d'origine n\u00e9erlandaise et a travaill\u00e9 pour la compagnie Philips.

    Il a invent\u00e9 le concept de s\u00e9maphore. Un s\u00e9maphore est un objet de synchronisation qui poss\u00e8de un compteur. Il poss\u00e8de deux op\u00e9rations atomiques\u2009: wait et post. wait d\u00e9cr\u00e9mente le compteur et bloque si le compteur est n\u00e9gatif. post incr\u00e9mente le compteur et r\u00e9veille un thread en attente si le compteur est n\u00e9gatif. Historiquement les noms donn\u00e9s par Dijkstra \u00e9taient P et V pour Proberen et Verhogen en n\u00e9erlandais.

    Le s\u00e9maphore compte des ressources disponibles. Si le s\u00e9maphore est \u00e0 z\u00e9ro, il n'y a plus de ressources disponibles. Si le s\u00e9maphore est \u00e0 un, il y a une ressource disponible. Si le s\u00e9maphore est \u00e0 deux...

    Plus tard sont venu des op\u00e9rations atomiques dans les processeurs comme le test-and-set ou le compare-and-swap. Ces op\u00e9rations permettent de r\u00e9aliser des primitives de synchronisation plus complexes comme les mutex. Le gros avantage c'est l'absence d'attente active. L'attente active c'est une boucle qui teste une condition en permanence. C'est tr\u00e8s gourmand en CPU.

    Une op\u00e9ration atomique dans le cadre de l'informatique c'est une op\u00e9ration qui s'ex\u00e9cute en une seule instruction machine. Cela signifie que l'op\u00e9ration est indivisible. C'est soit tout ou rien. C'est soit l'op\u00e9ration s'ex\u00e9cute compl\u00e8tement, soit elle ne s'ex\u00e9cute pas du tout.

    ", "tags": ["post", "Proberen", "wait", "Verhogen"]}, {"location": "course-concurrent/summary/#mutex", "title": "Mutex", "text": "

    En C++ le mutex est une classe qui permet de prot\u00e9ger des ressources partag\u00e9es entre plusieurs threads. Il poss\u00e8de deux m\u00e9thodes\u2009: lock et unlock. lock bloque le mutex si il est d\u00e9j\u00e0 verrouill\u00e9. unlock d\u00e9verrouille le mutex.

    #include <iostream>\n#include <thread>\n#include <mutex>\n\nstd::mutex mtx;\n\nvoid func() {\n    mtx.lock();\n    // Section critique\n    std::cout << \"Hello\" << std::endl;\n    mtx.unlock();\n}\n\nint main() {\n    std::thread t(func);\n    t.join();\n}\n

    On appelle section critique une portion de code qui acc\u00e8de \u00e0 des ressources partag\u00e9es. Il est important de prot\u00e9ger cette section critique avec un mutex pour \u00e9viter les acc\u00e8s concurrents.

    En g\u00e9n\u00e9ral on utilise pas le lock/unlock directement mais plut\u00f4t un std::lock_guard qui est un RAII (Resource Acquisition Is Initialization). C'est une classe qui s'occupe de lib\u00e9rer les ressources automatiquement \u00e0 la fin du bloc.

    void func() {\n    {\n        std::lock_guard<std::mutex> lock(mtx);\n        // Section critique\n        std::cout << \"Hello\" << std::endl;\n    }\n}\n

    On peut impl\u00e9menter tr\u00e8s facilement un lock_guard de la mani\u00e8re suivante\u2009:

    struct LockGuard {\n    LockGuard(std::mutex &mtx) : mtx(mtx) { mtx.lock(); }\n    ~LockGuard() { mtx.unlock(); }\nprivate:\n    std::mutex &mtx;\n};\n

    Aternativement au lock_guard on peut utiliser un std::unique_lock. La diff\u00e9rence c'est qu'un unique_lock est un verrou de port\u00e9e manuelle. Il peut \u00eatre d\u00e9verrouill\u00e9 et verrouill\u00e9 \u00e0 nouveau.

    void func() {\n    std::unique_lock<std::mutex> lock(mtx);\n    // Section critique\n    std::cout << \"Hello\" << std::endl;\n}\n

    Le unique_lock sera utilis\u00e9 par exemple dans les variables conditions.

    ", "tags": ["unlock", "lock", "unique_lock", "lock_guard"]}, {"location": "course-concurrent/summary/#variable-condition", "title": "Variable condition", "text": "

    Une variable condition est un objet de synchronisation qui permet de mettre un thread en attente tant qu'une condition n'est pas remplie. Elle utilise un mutex pour prot\u00e9ger la condition et permet de notifier des threads.

    Les m\u00e9thodes les plus courantes sont wait, wait_for, wait_until, notify_one et notify_all.

    int beers_in_fridge = 0;\nstd::mutex mtx;\nstd::variable_condition cv;\n\nvoid drinker() {\n    while(true) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, []{ return beers_in_fridge > 0; });\n        beers_in_fridge--;\n    }\n}\n\nvoid gf() {\n    while(true) {\n        std::unique_lock<std::mutex> lock(mtx);\n        beers_in_fridge++;\n        cv.notify_one();\n    }\n}\n\nint main() {\n    std::thread t1(drinker);\n    std::thread t2(gf);\n    t1.join();\n    t2.join();\n}\n

    Dans cet exemple, il y a un probl\u00e8me c'est que la copine (gf) peut mettre une bi\u00e8re dans le frigo alors qu'il est d\u00e9j\u00e0 plein. Il faudrait ajouter un s\u00e9maphore pour g\u00e9rer le nombre de bi\u00e8res dans le frigo. Puisqu'un s\u00e9maphore est un compteur de ressources.

    Mais ici, on est dans un cas de producteur-consommateur. Un producteur met des bi\u00e8res dans le frigo et un consommateur les boit.

    ", "tags": ["wait_for", "notify_all", "wait_until", "wait", "notify_one"]}, {"location": "course-concurrent/summary/#producteur-consommateur", "title": "Producteur-Consommateur", "text": "

    Le probl\u00e8me du producteur-consommateur est un probl\u00e8me classique de synchronisation.

    On peut le r\u00e9soudre de deux mani\u00e8res\u2009:

    1. Avec deux s\u00e9maphores\u2009: un pour le producteur et un pour le consommateur.
    2. Avec une variable condition.
    "}, {"location": "course-concurrent/summary/#moniteur", "title": "Moniteur", "text": "

    Un moniteur est un objet de synchronisation qui encapsule des donn\u00e9es et des op\u00e9rations sur ces donn\u00e9es. Il permet de prot\u00e9ger les donn\u00e9es et de synchroniser les threads qui acc\u00e8dent \u00e0 ces donn\u00e9es.

    Concr\u00e8tement c'est un classe qui contient des donn\u00e9es et des m\u00e9thodes pour acc\u00e9der \u00e0 ces donn\u00e9es. Les m\u00e9thodes sont prot\u00e9g\u00e9es par un mutex pour \u00e9viter les acc\u00e8s concurrents.

    class Fridge {\npublic:\n    void put(int beer) {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this]{ return beers.size() < MAX; });\n        beers.push_back(beer);\n        cv.notify_one();\n    }\n\n    int get() {\n        std::unique_lock<std::mutex> lock(mtx);\n        cv.wait(lock, [this]{ return beers.size() > 0; });\n        int beer = beers.back();\n        beers.pop_back();\n        cv.notify_one();\n        return beer;\n    }\nprivate:\n    std::vector<int> beers;\n    std::mutex mtx;\n    std::variable_condition cv;\n};\n

    Si on veut l'utiliser dans l'exemple pr\u00e9c\u00e9dent on peut faire\u2009:

    void drinker(Fridge &fridge) {\n    while(true) { int beer = fridge.get(); }\n}\n\nvoid gf(Fridge &fridge) {\n    while(true) { fridge.put(1); }\n}\n\nint main() {\n    Fridge fridge;\n    std::thread t1(drinker, std::ref(fridge));\n    std::thread t2(gf, std::ref(fridge));\n    t1.join();\n    t2.join();\n}\n
    "}, {"location": "course-concurrent/summary/#deadlock-et-starvation", "title": "Deadlock et starvation", "text": "

    Le deadlock est une situation o\u00f9 deux threads se bloquent mutuellement en attendant une ressource que l'autre thread poss\u00e8de. C'est un probl\u00e8me classique de synchronisation.

    Typiquement si la copine (gf) attend que le frigo soit vide pour mettre une bi\u00e8re et que le buveur (drinker) attend que le frigo soit plein pour boire une bi\u00e8re. C'est un deadlock.

    C'est aussi le cas dans le dilemme des philosophes. Chaque philosophe a besoin de deux fourchettes pour manger. Si chaque philosophe prend une fourchette et attend que l'autre fourchette soit libre, il y a un deadlock\u2009: y'a cinq philosophes, une fouchette \u00e0 gauche et une fourchette \u00e0 droite, la table est ronde. Donc on a 5 fourchettes et 5 philosophes ce qui ne permet pas que tout le monde mange en m\u00eame temps.

    En th\u00e9orie de programmation concurrente ce probl\u00e8me des philosphophe peut \u00eatre d\u00e9crit comme ayant un cycle dans le graphe des ressources. Chaque philosophe est un noeud et chaque fourchette est une ar\u00eate. Si on a un cycle dans le graphe, il y a un deadlock.

    La starvation (famine) est une situation o\u00f9 un thread n'arrive pas \u00e0 acc\u00e9der \u00e0 une ressource partag\u00e9e \u00e0 cause de l'ordonnancement du syst\u00e8me. C'est un probl\u00e8me de synchronisation.

    Un exmple de famine serait si le buveur (drinker) boit toutes les bi\u00e8res et que la copine (gf) ne peut jamais mettre de bi\u00e8re dans le frigo. C'est une famine.

    "}, {"location": "course-concurrent/summary/#termes-de-programmation-concurrente", "title": "Termes de programmation concurrente", "text": "
    • Section Critique: Une portion de code qui acc\u00e8de \u00e0 des ressources partag\u00e9es, dans laquelle un seul thread doit s'ex\u00e9cuter \u00e0 la fois sinon il y a risque de corruption des donn\u00e9es.
    • Deadlock: Situation o\u00f9 deux threads se bloquent mutuellement en attendant une ressource que l'autre thread poss\u00e8de.
    • Starvation: Situation o\u00f9 un thread n'arrive pas \u00e0 acc\u00e9der \u00e0 une ressource partag\u00e9e \u00e0 cause de l'ordonnancement du syst\u00e8me
    • Condition de course: Situation o\u00f9 le r\u00e9sultat d'une op\u00e9ration d\u00e9pend de l'ordre d'ex\u00e9cution des threads.
    "}, {"location": "course-concurrent/summary/#memoire-cache", "title": "M\u00e9moire Cache", "text": "

    La m\u00e9moire cache est une m\u00e9moire rapide situ\u00e9e entre le processeur et la m\u00e9moire principale. Elle permet d'acc\u00e9l\u00e9rer l'acc\u00e8s aux donn\u00e9es en stockant les donn\u00e9es les plus fr\u00e9quemment utilis\u00e9es par le processeur.

    Les probl\u00e8mes typiques que l'on rencontre en programmation concurrente sont\u2009:

    • Cache Miss: Situation o\u00f9 une donn\u00e9e n'est pas pr\u00e9sente dans le cache et doit \u00eatre charg\u00e9e depuis la m\u00e9moire principale.
    • False Sharing: Situation o\u00f9 deux threads acc\u00e8dent \u00e0 des donn\u00e9es qui sont stock\u00e9es dans la m\u00eame ligne de cache, ce qui entra\u00eene des acc\u00e8s m\u00e9moire inutiles.
    • True Sharing: Situation o\u00f9 deux threads acc\u00e8dent \u00e0 des donn\u00e9es qui sont stock\u00e9es dans la m\u00eame ligne de cache, mais o\u00f9 les donn\u00e9es sont r\u00e9ellement partag\u00e9es entre les threads.

    Pour r\u00e9soudre ces probl\u00e8mes on va s'assurer que l'alignement m\u00e9moire que l'on utilise dans nos thread ne cr\u00e9e pas de situation de false sharing. On peut aussi utiliser des attributs de compilation pour forcer l'alignement m\u00e9moire.

    "}, {"location": "course-concurrent/summary/#programmation-asynchrone", "title": "Programmation Asynchrone", "text": "

    La programmation asynchrone est un style de programmation qui permet d'ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente sans bloquer le thread principal. Cela permet d'am\u00e9liorer les performances en \u00e9vitant les attentes inutiles.

    En C++ on peut utiliser les threads, les promesses et les t\u00e2ches pour r\u00e9aliser de la programmation asynchrone.

    • std::async : Ex\u00e9cute une fonction de mani\u00e8re asynchrone et renvoie un std::future qui permet de r\u00e9cup\u00e9rer le r\u00e9sultat de la fonction.
    • std::promise : Permet de communiquer entre deux threads en envoyant une valeur d'un thread \u00e0 un autre.
    • std::future : Permet de r\u00e9cup\u00e9rer le r\u00e9sultat d'une fonction asynchrone.
    "}, {"location": "course-concurrent/summary/#programmation-parallele", "title": "Programmation Parall\u00e8le", "text": "

    La programmation parall\u00e8le est un style de programmation qui permet d'ex\u00e9cuter des t\u00e2ches de mani\u00e8re concurrente sur plusieurs processeurs ou c\u0153urs de processeur. Cela permet d'am\u00e9liorer les performances en r\u00e9partissant la charge de calcul sur plusieurs unit\u00e9s de calcul.

    En C++ on va souvent utiliser std::hardware_concurrency pour conna\u00eetre le nombre de coeurs disponibles sur la machine. Pourquoi on va cr\u00e9er uniquement le nombre de thread qui correspond au nombre de coeurs\u2009? Parce que si on cr\u00e9e plus de threads que de coeurs, on va cr\u00e9er des threads qui vont se partager le temps de calcul des coeurs. On va perdre beaucoup de temps dans les changements de contexte.

    En pratique il est rare d'utiliser directement les thread pour parall\u00e9liser des t\u00e2ches. On va plut\u00f4t utiliser des librairies comme OpenMP ou des frameworks comme Qt.

    Avec OpenMP on peut parall\u00e9liser des boucles for, des sections de code ou des t\u00e2ches. C'est tr\u00e8s simple \u00e0 utiliser et tr\u00e8s efficace.

    #include <iostream>\n#include <omp.h>\n\nint main() {\n    #pragma omp parallel for\n    for(int i = 0; i < 10; i++) {\n        std::cout << i << std::endl;\n    }\n}\n

    Alternativement, on peut confier l'ex\u00e9cution de certain probl\u00e8mes paralleles \u00e0 des GPU. Les GPU sont des unit\u00e9s de calcul massivement parall\u00e8les qui permettent d'acc\u00e9l\u00e9rer les calculs en parall\u00e9lisant les t\u00e2ches.

    "}, {"location": "course-concurrent/thread-pool/", "title": "Thread Pool", "text": "

    Un thread pool est un module logiciel qui permet de g\u00e9rer un ensemble de threads. Il permet de limiter le nombre de threads actifs et de r\u00e9utiliser les threads existants. Un thread pool est compos\u00e9 de deux \u00e9l\u00e9ments principaux\u2009:

    • une file d'attente de t\u00e2ches\u2009;
    • un ensemble de threads.

    Les t\u00e2ches sont ajout\u00e9es \u00e0 la file d'attente et les threads les r\u00e9cup\u00e8rent et les ex\u00e9cutent. Lorsqu'un thread a termin\u00e9 une t\u00e2che, il en r\u00e9cup\u00e8re une autre dans la file d'attente. Cela permet de r\u00e9duire le temps de cr\u00e9ation et de destruction des threads, et d'\u00e9viter les probl\u00e8mes de concurrence li\u00e9s \u00e0 la gestion des threads.

    "}, {"location": "course-concurrent/thread-pool/#cas-dutilisation", "title": "Cas d'utilisation", "text": "

    Les thread pools sont utilis\u00e9s dans de nombreux domaines, notamment dans les serveurs web, les serveurs d'applications, les bases de donn\u00e9es, les syst\u00e8mes d'exploitation, etc. Ils permettent de g\u00e9rer efficacement un grand nombre de connexions simultan\u00e9es, en limitant le nombre de threads actifs et en r\u00e9utilisant les threads existants.

    Par exemple le projet OpenCV utilise Intel TBB (Threading Building Blocks) pour g\u00e9rer les threads. TBB est une biblioth\u00e8que C++ qui fournit des primitives de parall\u00e9lisme de haut niveau, telles que les thread pools, les t\u00e2ches parall\u00e8les, les boucles parall\u00e8les, etc. Elle permet de tirer parti des processeurs multi-c\u0153urs et de r\u00e9duire le temps d'ex\u00e9cution des applications parall\u00e8les.

    Le logiciel Blender permettant la cr\u00e9ation de contenu 3D utilise \u00e9galement un thread pool pour g\u00e9rer les t\u00e2ches de rendu. Cela permet de r\u00e9partir les t\u00e2ches de rendu sur plusieurs threads et d'acc\u00e9l\u00e9rer le processus de rendu.

    "}, {"location": "course-concurrent/threads/", "title": "Threads", "text": ""}, {"location": "course-concurrent/threads/#introduction", "title": "Introduction", "text": "

    La gestion de l'ordonnancement des processus et des threads (processus l\u00e9gers) par le noyau d'un syst\u00e8me d'exploitation a \u00e9volu\u00e9 avec le temps, notamment sous les syst\u00e8mes UNIX et Linux. Historiquement, en effet, les threads \u00e9taient souvent g\u00e9r\u00e9s au niveau utilisateur par des biblioth\u00e8ques de threads, comme la biblioth\u00e8que POSIX Threads (pthreads) sous UNIX. Cette approche est connue sous le nom de threads au niveau utilisateur (User-Level Threads). Cependant, les syst\u00e8mes modernes, y compris Linux, utilisent une approche diff\u00e9rente qui permet une meilleure int\u00e9gration avec l'ordonnanceur du noyau.

    "}, {"location": "course-concurrent/threads/#threads-au-niveau-utilisateur", "title": "Threads au Niveau Utilisateur", "text": "

    Dans les mod\u00e8les de threads au niveau utilisateur, l'ordonnancement des threads est g\u00e9r\u00e9 enti\u00e8rement par la biblioth\u00e8que de threads dans l'espace utilisateur. Le noyau n'est pas conscient de la pr\u00e9sence de threads au sein des processus\u2009; il ne voit et n'ordonnance que des processus. Cette approche permet une grande flexibilit\u00e9 et une portabilit\u00e9 entre diff\u00e9rents syst\u00e8mes d'exploitation, mais pr\u00e9sente plusieurs inconv\u00e9nients\u2009:

    • Manque de Connaissance du Noyau : comme le noyau n'est pas conscient des threads, il ne peut pas prendre de d\u00e9cisions d'ordonnancement bas\u00e9es sur l'\u00e9tat de tous les threads dans le syst\u00e8me. Cela peut conduire \u00e0 une utilisation sous-optimale des ressources, notamment sur les syst\u00e8mes multiprocesseurs.
    • Blocage au Niveau du Processus : Si un thread effectue un appel syst\u00e8me bloquant, tout le processus (et donc tous ses threads) peut \u00eatre bloqu\u00e9, ce qui n'est pas id\u00e9al pour la concurrence.
    "}, {"location": "course-concurrent/threads/#threads-au-niveau-noyau", "title": "Threads au Niveau Noyau", "text": "

    Pour surmonter ces limitations, la plupart des syst\u00e8mes d'exploitation modernes, y compris Linux, g\u00e8rent d\u00e9sormais les threads au niveau du noyau (Kernel-Level Threads). Dans ce mod\u00e8le, le noyau est pleinement conscient de chaque thread et peut les ordonnancer ind\u00e9pendamment. Cela permet une meilleure utilisation des ressources sur les syst\u00e8mes multic\u0153urs, car le noyau peut r\u00e9partir les threads sur diff\u00e9rents processeurs.

    • Meilleure Concurrency : Chaque thread peut \u00eatre ordonnanc\u00e9 ind\u00e9pendamment, permettant \u00e0 un processus de continuer \u00e0 s'ex\u00e9cuter m\u00eame si l'un de ses threads est bloqu\u00e9.
    • Gestion des Priorit\u00e9s : Le noyau peut ajuster les priorit\u00e9s des threads individuellement, offrant une granularit\u00e9 fine dans la gestion de l'ordonnancement.
    • Support Multiprocesseur : Le noyau peut r\u00e9partir les threads d'un m\u00eame processus sur plusieurs c\u0153urs, exploitant pleinement le mat\u00e9riel multiprocesseur.
    "}, {"location": "course-concurrent/threads/#modele-nm", "title": "Mod\u00e8le N:M", "text": "

    Il existe \u00e9galement un mod\u00e8le hybride, le mod\u00e8le N:M, qui tente de combiner les avantages des threads au niveau utilisateur et au niveau noyau. Dans ce mod\u00e8le, N threads au niveau utilisateur sont mapp\u00e9s sur M threads au niveau noyau. Cela permet une certaine flexibilit\u00e9 dans la gestion des threads et peut am\u00e9liorer la performance en r\u00e9duisant le nombre de changements de contexte n\u00e9cessaires. Cependant, ce mod\u00e8le est complexe \u00e0 impl\u00e9menter et n'est pas largement utilis\u00e9 dans les syst\u00e8mes d'exploitation modernes.

    "}, {"location": "course-concurrent/threads/#conclusion", "title": "Conclusion", "text": "

    L'approche moderne de la gestion de l'ordonnancement des processus et des threads par le noyau offre une flexibilit\u00e9, une efficacit\u00e9 et une scalabilit\u00e9 significativement meilleures par rapport aux m\u00e9thodes historiques. Les threads au niveau noyau permettent une utilisation optimale du mat\u00e9riel multiprocesseur et multic\u0153ur, tout en offrant une gestion fine de la concurrence et de la parall\u00e9lisation au sein des applications.

    C++20 lui-m\u00eame n'introduit pas de nouvelles fonctionnalit\u00e9s sp\u00e9cifiquement con\u00e7ues pour les threads au niveau utilisateur, mais il continue de supporter le multithreading \u00e0 travers sa biblioth\u00e8que standard, notamment avec <thread>, <mutex>, <future>, <atomic>, et d'autres facilit\u00e9s de synchronisation et de communication entre threads.

    Les threads g\u00e9r\u00e9s par la biblioth\u00e8que standard C++ sont des threads au niveau syst\u00e8me (ou noyau), ce qui signifie que leur cr\u00e9ation, destruction, et ordonnancement sont g\u00e9r\u00e9s par le syst\u00e8me d'exploitation. Cela offre des avantages en termes de performances et d'utilisation sur des syst\u00e8mes multic\u0153urs, mais peut ne pas \u00eatre id\u00e9al pour tous les cas d'utilisation, particuli\u00e8rement ceux n\u00e9cessitant un grand nombre de threads avec des changements de contexte fr\u00e9quents.

    "}, {"location": "course-cpp/cpp/", "title": "C++", "text": "C vous permet de vous tirer une balle dans le pied facilement ; C++ rend cela plus difficile, mais quand vous y arrivez, \u00e7a vous arrache toute la jambe.Bjarne Stroustrup"}, {"location": "course-cpp/cpp/#introduction", "title": "Introduction", "text": "

    Dans le vaste univers des langages de programmation, il est des \u00e9toiles qui brillent d\u2019un \u00e9clat singulier, non par leur simplicit\u00e9, mais par la puissance et la profondeur qu\u2019elles conf\u00e8rent \u00e0 ceux qui osent s\u2019y aventurer. C++ est sans doute l\u2019une de ces constellations. Langage aux multiples facettes, oscillant entre l\u2019h\u00e9ritage brut de C et les abstractions sophistiqu\u00e9es de la programmation orient\u00e9e objet, C++ trouve son origine dans les ann\u00e9es 1980, sous la plume inspir\u00e9e de Bjarne Stroustrup, un informaticien danois dont l\u2019ambition \u00e9tait aussi pragmatique que visionnaire.

    Dans les ann\u00e9es 1970 et 1980, les langages de programmation n\u2019\u00e9taient pas encore aussi vari\u00e9s et sp\u00e9cialis\u00e9s qu\u2019aujourd\u2019hui. C, un langage imp\u00e9ratif et structur\u00e9, dominait le paysage, notamment dans les domaines des syst\u00e8mes d\u2019exploitation et du d\u00e9veloppement de logiciels \u00e0 hautes performances. Toutefois, malgr\u00e9 sa rapidit\u00e9 et son efficacit\u00e9, C poss\u00e9dait des limitations flagrantes, en particulier dans la gestion des concepts abstraits et complexes propres aux grands syst\u00e8mes logiciels. Stroustrup, alors chercheur aux laboratoires Bell, se trouvait confront\u00e9 \u00e0 une question existentielle\u2009: comment allier la performance brute d\u2019un langage bas niveau, capable de manipuler le mat\u00e9riel avec une pr\u00e9cision chirurgicale, \u00e0 l\u2019\u00e9l\u00e9gance et \u00e0 la modularit\u00e9 d\u2019un langage orient\u00e9 objet, tel que Simula, qui \u00e9tait d\u00e9j\u00e0 pris\u00e9 pour ses capacit\u00e9s \u00e0 mod\u00e9liser des syst\u00e8mes complexes\u2009?

    L\u2019id\u00e9e de C with Classes, qui deviendra par la suite C++, \u00e9tait de conserver la robustesse de C tout en y introduisant les concepts de la programmation orient\u00e9e objet (POO), un paradigme qui, \u00e0 l\u2019\u00e9poque, commen\u00e7ait \u00e0 d\u00e9montrer son efficacit\u00e9 pour organiser le code, le rendre plus r\u00e9utilisable, et g\u00e9rer la complexit\u00e9 croissante des syst\u00e8mes logiciels modernes.

    Bjarne Stroupstrup

    Le paradigme objet qui a fait sa grande entr\u00e9e avec Simula en 1967, puis Smalltalk en 1972, \u00e9tait une r\u00e9volution dans la mani\u00e8re de concevoir les programmes. Il permettait de regrouper les donn\u00e9es et les fonctions qui les manipulent dans des entit\u00e9s coh\u00e9rentes appel\u00e9es objets, et de d\u00e9finir des relations entre ces objets, en termes d\u2019h\u00e9ritage, de polymorphisme, et d\u2019encapsulation. C++ allait donc s\u2019inscrire dans cette lign\u00e9e, en apportant sa propre vision de la POO, plus proche de C que de Simula, mais tout aussi puissante.

    "}, {"location": "course-cpp/cpp/#equilibre-entre-pouvoir-et-simplicite", "title": "\u00c9quilibre entre pouvoir et simplicit\u00e9", "text": "

    Le dessein de Stroustrup n\u2019\u00e9tait pas de cr\u00e9er un langage compl\u00e8tement nouveau, mais plut\u00f4t d\u2019\u00e9tendre un outil d\u00e9j\u00e0 existant, C, pour en faire un instrument plus flexible, plus puissant, capable de s\u2019adapter \u00e0 des applications vari\u00e9es, allant des syst\u00e8mes d\u2019exploitation aux simulations scientifiques. Cependant, il \u00e9tait clair d\u00e8s le d\u00e9but que cet \u00e9quilibre ne serait pas facile \u00e0 atteindre. \u00c0 la mani\u00e8re d\u2019un architecte concevant une cath\u00e9drale o\u00f9 chaque vo\u00fbte porte la promesse d\u2019un effondrement potentiel, Stroustrup devait naviguer entre les contraintes de performances de bas niveau et la promesse d\u2019une abstraction \u00e9lev\u00e9e.

    C++ introduisit donc des classes, l\u2019h\u00e9ritage, l\u2019encapsulation, et bient\u00f4t, des concepts comme les mod\u00e8les (templates), qui allaient permettre d\u2019abstraire le code tout en conservant une efficacit\u00e9 optimale. Toutefois, cette flexibilit\u00e9 et cette ambition eurent aussi leurs revers.

    Toute naissance est imparfaite. Et C++, bien que prometteur, n\u2019\u00e9chappa pas aux vicissitudes de l\u2019\u00e9volution. Parmi les premiers d\u00e9fis que les d\u00e9veloppeurs rencontr\u00e8rent en adoptant ce langage se trouvait ce qui allait \u00eatre nomm\u00e9 avec un certain humour noir, le \u201cMost Vexing Parse\u201d \u2014 une des nombreuses subtilit\u00e9s de la syntaxe de C++ qui pouvait amener m\u00eame les plus exp\u00e9riment\u00e9s \u00e0 se gratter la t\u00eate. En raison de l\u2019ambigu\u00eft\u00e9 entre la d\u00e9claration de variables et la d\u00e9finition de fonctions, certaines constructions valides en apparence pouvaient se r\u00e9v\u00e9ler incompr\u00e9hensibles ou sources d\u2019erreurs.

    De m\u00eame, la gestion de la m\u00e9moire, h\u00e9rit\u00e9e de C, restait un terrain min\u00e9. Alors que C++ proposait des m\u00e9canismes comme les destructeurs pour automatiser la lib\u00e9ration des ressources, l\u2019absence de garbage collector (collecteur de d\u00e9chets) natif, \u00e0 l\u2019instar de Java ou C#, signifiait que l\u2019erreur humaine demeurait un facteur crucial dans la gestion des ressources\u2009: n'avez-vous jamais oubli\u00e9 un free apr\u00e8s un malloc en C\u2009?

    Ces \u00ab\u2009erreurs de jeunesse\u2009\u00bb ne faisaient pas qu'alourdir la t\u00e2che du programmeur\u2009; elles rappelaient aussi que C++ n'\u00e9tait pas un langage de novices. Il exigeait discipline, rigueur, et une compr\u00e9hension intime de ses m\u00e9canismes internes. Il y avait une beaut\u00e9 dans cette complexit\u00e9, un plaisir intellectuel pour ceux qui ma\u00eetrisaient ses arcanes.

    Malgr\u00e9 ses d\u00e9buts tumultueux, C++ gagna rapidement en popularit\u00e9. Sa capacit\u00e9 \u00e0 \u00eatre \u00e0 la fois bas niveau et abstrait en faisait l\u2019outil privil\u00e9gi\u00e9 pour de nombreux domaines, des moteurs de jeux vid\u00e9o aux simulations financi\u00e8res. Mais, comme tout langage en \u00e9volution rapide, il n\u00e9cessitait un cadre rigoureux pour \u00e9viter le chaos. C\u2019est ainsi qu\u2019en 1998, l'ISO standardisa officiellement C++, posant les bases de ce qui deviendrait une longue lign\u00e9e de versions standardis\u00e9es.

    Voici les principales \u00e9tapes de cette \u00e9volution\u2009:

    C++98 (ISO/IEC 14882:1998)

    La premi\u00e8re norme officielle de C++, qui formalise les ajouts du langage, notamment les templates et les exceptions.

    C++03 (ISO/IEC 14882:2003)

    Une r\u00e9vision mineure visant \u00e0 corriger des erreurs et clarifier certaines ambigu\u00eft\u00e9s dans C++98.

    C++11 (ISO/IEC 14882:2011)

    Une r\u00e9volution pour le langage, avec l\u2019ajout des expressions lambda, des smart pointers, des threads natifs, et de nombreuses am\u00e9liorations en termes de performance et de simplicit\u00e9 d\u2019utilisation.

    C++14 (ISO/IEC 14882:2014)

    Une r\u00e9vision de C++11, qui affine certains concepts et introduit quelques nouvelles fonctionnalit\u00e9s mineures.

    C++17 (ISO/IEC 14882:2017)

    Un standard qui introduit des fonctionnalit\u00e9s plus avanc\u00e9es comme std::optional, std::variant, et des am\u00e9liorations pour le travail avec les cha\u00eenes de caract\u00e8res.

    C++20 (ISO/IEC 14882:2020)

    Une mise \u00e0 jour majeure, avec des concepts, des coroutines, et des ranges, renfor\u00e7ant encore davantage les capacit\u00e9s de programmation g\u00e9n\u00e9rique et moderne de C++.

    C++23

    Dernier en date au moment de cet \u00e9crit, qui continue d\u2019affiner et de simplifier l\u2019exp\u00e9rience des d\u00e9veloppeurs, en int\u00e9grant des fonctionnalit\u00e9s comme des modules et des am\u00e9liorations pour la concurrence.

    \u00c0 travers chaque standardisation, C++ a su \u00e9voluer sans jamais renier ses principes fondateurs\u2009: performance, flexibilit\u00e9 et un contr\u00f4le pr\u00e9cis des ressources.

    "}, {"location": "course-cpp/cpp/#un-heritage-a-double-tranchant", "title": "Un h\u00e9ritage \u00e0 double tranchant", "text": "

    En conclusion, C++ n\u2019est pas un langage pour ceux qui cherchent la facilit\u00e9. Il est le produit d\u2019une vision grandiose, o\u00f9 chaque ligne de code peut atteindre des sommets de performance, mais o\u00f9 chaque erreur peut avoir des cons\u00e9quences drastiques. Ses subtilit\u00e9s syntaxiques, ses m\u00e9canismes de m\u00e9moire et sa philosophie de contr\u00f4le total sont autant de d\u00e9fis que de promesses pour le programmeur aguerri.

    Mais c\u2019est aussi un langage vivant, \u00e9voluant constamment avec ses utilisateurs, capable d'adapter ses structures aux paradigmes modernes tout en conservant sa puissance brute. C++, en somme, est \u00e0 l\u2019image de son cr\u00e9ateur, Bjarne Stroustrup\u2009: une \u0153uvre \u00e0 la fois rationnelle et passionn\u00e9e, o\u00f9 le code devient plus qu\u2019un outil \u2014 il devient un art.

    "}, {"location": "course-cpp/garbage-collection/", "title": "Collecteur de d\u00e9chets (garbage collector)", "text": "

    Le C est un langage primitif qui ne g\u00e8re pas automatiquement la lib\u00e9ration des ressources allou\u00e9es dynamiquement. L'exemple suivant est \u00e9vocateur\u2009:

    int* get_number() {\n    int *num = malloc(sizeof(int));\n    *num = rand();\n}\n\nint main() {\n    for (int i = 0; i < 100; i++) {\n        printf(\"%d\\n\", *get_number());\n    }\n}\n

    La fonction get_number alloue dynamiquement un espace de la taille d'un entier et lui assigne une valeur al\u00e9atoire. Dans le programme principal, l'adresse retourn\u00e9e est d\u00e9r\u00e9f\u00e9renc\u00e9e pour \u00eatre affich\u00e9e sur la sortie standard.

    A la fin de l'ex\u00e9cution de la boucle for, une centaine d'espaces m\u00e9moire sont maintenant dans les limbes. Comme le pointeur retourn\u00e9 n'a jamais \u00e9t\u00e9 m\u00e9moris\u00e9, il n'est plus possible de lib\u00e9rer cet espace m\u00e9moire avec free.

    On dit que le programme \u00e0 une fuite m\u00e9moire. En admettant que ce programme reste r\u00e9sidant en m\u00e9moire, il peut arriver un moment o\u00f9 le programme peut aller jusqu'\u00e0 utiliser toute la RAM disponible. Dans ce cas, il est probable que malloc retourne NULL et qu'une erreur de segmentation apparaisse lors du printf.

    Allons plus loin dans notre exemple et consid\u00e9rons le code suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint foo(int *new_value) {\n    static int *values[10] = { NULL };\n    static int count = 0;\n\n    if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {\n        values[count++] = new_value;\n    }\n\n    if (count > 0)\n        printf(\"Foo aime %d\\n\", *values[rand() % count]);\n}\n\nint bar(int *new_value) {\n    static int *values[10] = { NULL };\n    static int count = 0;\n\n    if (rand() % 5 && count < sizeof(values) / sizeof(*values) - 1) {\n        values[count++] = new_value;\n    }\n\n    if (count > 0)\n        printf(\"Bar aime %d\\n\", *values[rand() % count]);\n}\n\nint* get_number() {\n    int *number = malloc(sizeof(int));\n    *number = rand() % 1000;\n    return number;\n}\n\nint main() {\n    int experiment_iterations = 10;\n    for (int i = 0; i < experiment_iterations; i++) {\n        int *num = get_number();\n        foo(num);\n        bar(num);\n        #if 0 // ...\n            free(num) ??\n        #endif\n    };\n}\n

    La fonction get_number alloue dynamiquement un espace m\u00e9moire et assigne un nombre al\u00e9atoire. Les fonctions foo et bar re\u00e7oivent en param\u00e8tre un pointeur sur un entier. Chacune \u00e0 le choix de m\u00e9moriser ce pointeur et de clamer sur stdout qu'elle aime un des nombres m\u00e9moris\u00e9s.

    Au niveau du #if 0 dans la fonction main, il est impossible de savoir si l'adresse point\u00e9e par num est encore utilis\u00e9e ou non. Il se peut que foo et bar utilisent cet espace m\u00e9moire, comme il se peut qu'aucun des deux ne l'utilise.

    Comment peut-on savoir si il est possible de lib\u00e9rer ou non num ?

    Une solution couramment utilis\u00e9e 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\u00e9f\u00e9rences utilis\u00e9es. De cette mani\u00e8re il est possible en tout temps de savoir si le pointeur est r\u00e9f\u00e9renc\u00e9 quelque part. Dans le cas o\u00f9 le nombre de r\u00e9f\u00e9rence tombe \u00e0 z\u00e9ro, il est possible de lib\u00e9rer la ressource.

    Dans un certain nombre de langage de programmation comme Python ou Java, il existe un m\u00e9canisme automatique nomm\u00e9 Garbage Collector et qui, p\u00e9riodiquement, fait un tour de toutes les allocations dynamique pour savoir si elle sont encore r\u00e9f\u00e9renc\u00e9es ou non. Le cas \u00e9ch\u00e9ant, le gc d\u00e9cide lib\u00e9rer la ressource m\u00e9moire. De cette mani\u00e8re il n'est plus n\u00e9cessaire de faire la chasse aux ressources allou\u00e9es.

    En revanche, en C, il n'existe aucun m\u00e9canisme aussi sophistiqu\u00e9 alors prenez garde \u00e0 bien lib\u00e9rer les ressources utilis\u00e9es et \u00e0 \u00e9viter d'\u00e9crire des fonctions qui allouent du contenu m\u00e9moire dynamiquement.

    ", "tags": ["malloc", "bar", "free", "get_number", "num", "printf", "NULL", "foo", "stdout", "main"]}, {"location": "course-cpp/object-oriented/", "title": "Orient\u00e9 objet", "text": ""}, {"location": "course-cpp/object-oriented/#un-voyage-vers-labstraction-structuree", "title": "Un voyage vers l'abstraction structur\u00e9e", "text": "

    \u00c0 l'aube de la programmation informatique, le code n'\u00e9tait qu'une suite d'instructions lin\u00e9aires, un simple flux s\u00e9quentiel qui, dans les meilleurs des cas, reproduisait des sch\u00e9mas logiques simples. Mais au fur et \u00e0 mesure que les syst\u00e8mes devenaient plus complexes, l'organisation de ces instructions demandait plus de structure, plus de modularit\u00e9. C'est l\u00e0 que les paradigmes de programmation commenc\u00e8rent \u00e0 \u00e9merger.

    "}, {"location": "course-cpp/object-oriented/#une-maniere-de-penser-la-programmation", "title": "Une mani\u00e8re de penser la programmation", "text": "

    Un paradigme en informatique peut \u00eatre d\u00e9fini comme un mod\u00e8le ou un style de programmation. Il impose une mani\u00e8re sp\u00e9cifique de penser et d\u2019organiser le code, de r\u00e9soudre des probl\u00e8mes en suivant des r\u00e8gles bien d\u00e9finies. Le paradigme imp\u00e9ratif, par exemple, ordonne les instructions, pas \u00e0 pas, tandis que le paradigme fonctionnel privil\u00e9gie les expressions math\u00e9matiques pures. Chaque paradigme propose un point de vue diff\u00e9rent, un cadre conceptuel particulier pour aborder la r\u00e9solution des probl\u00e8mes.

    Parmi ces paradigmes, un se distingue par sa capacit\u00e9 \u00e0 mod\u00e9liser des syst\u00e8mes complexes en s\u2019inspirant du monde r\u00e9el\u2009: le paradigme orient\u00e9 objet.

    Le paradigme orient\u00e9 objet (OO) propose une approche o\u00f9 le programme est con\u00e7u comme un ensemble d\u2019entit\u00e9s autonomes appel\u00e9es objets. Ces objets interagissent entre eux, chacun repr\u00e9sentant une part sp\u00e9cifique du monde ou du probl\u00e8me \u00e0 mod\u00e9liser. L'id\u00e9e sous-jacente est de rendre le code plus modulaire, r\u00e9utilisable, et compr\u00e9hensible en refl\u00e9tant la structure des entit\u00e9s du monde r\u00e9el ou de l'application simul\u00e9e.

    L'OO permet de repr\u00e9senter des concepts abstraits tout en conservant une organisation claire et hi\u00e9rarchis\u00e9e des donn\u00e9es et des comportements. Cette approche est apparue en r\u00e9ponse \u00e0 la complexit\u00e9 croissante des syst\u00e8mes logiciels et des besoins d'une meilleure gestion de cette complexit\u00e9.

    Historiquement, l\u2019OO trouve ses racines dans des langages pionniers comme Simula (ann\u00e9es 1960), qui introduit les concepts d\u2019objets et de classes, et Smalltalk (ann\u00e9es 1970), qui formalise cette approche. Ces langages marqu\u00e8rent un tournant dans la fa\u00e7on de concevoir les programmes. Par la suite, des langages comme Java, Python, et C#, ainsi que C++, port\u00e8rent ce paradigme \u00e0 des niveaux plus \u00e9lev\u00e9s de popularit\u00e9 et de sophistication.

    L'OO repose sur quelques concepts cl\u00e9s qui constituent son fondement et qui permettent d\u2019organiser le code de mani\u00e8re robuste, modulaire et extensible\u2009:

    Objet

    Un objet est une entit\u00e9 ind\u00e9pendante qui poss\u00e8de un \u00e9tat (sous forme de donn\u00e9es ou attributs) et des comportements (sous forme de m\u00e9thodes ou fonctions). Il est l\u2019\u00e9l\u00e9ment fondamental du paradigme objet, l\u2019unit\u00e9 de base avec laquelle on construit tout le syst\u00e8me.

    Classe

    Une classe est un mod\u00e8le, ou un plan, qui d\u00e9finit les attributs et les comportements des objets. Elle permet de cr\u00e9er plusieurs objets similaires partageant la m\u00eame structure et les m\u00eames fonctionnalit\u00e9s. Les objets sont ainsi des instances d\u2019une classe.

    Encapsulation

    Ce principe consiste \u00e0 cacher les d\u00e9tails internes d'un objet et \u00e0 n'exposer que ce qui est n\u00e9cessaire pour son utilisation. Cela se traduit par la s\u00e9paration des donn\u00e9es priv\u00e9es, que l\u2019on prot\u00e8ge, et des m\u00e9thodes publiques qui permettent d\u2019interagir avec l\u2019objet. L\u2019encapsulation r\u00e9duit les interf\u00e9rences non souhait\u00e9es avec l\u2019\u00e9tat interne de l\u2019objet et am\u00e9liore la robustesse du code.

    Abstraction

    L\u2019abstraction consiste \u00e0 repr\u00e9senter uniquement les aspects essentiels d\u2019un objet tout en ignorant les d\u00e9tails superflus. C\u2019est une forme de simplification du code, permettant de manipuler les objets \u00e0 un niveau \u00e9lev\u00e9 sans se soucier de leur impl\u00e9mentation d\u00e9taill\u00e9e.

    H\u00e9ritage

    L\u2019h\u00e9ritage permet de cr\u00e9er de nouvelles classes \u00e0 partir de classes existantes, en r\u00e9utilisant et en sp\u00e9cialisant leurs propri\u00e9t\u00e9s et m\u00e9thodes. Une classe d\u00e9riv\u00e9e (ou sous-classe) h\u00e9rite ainsi des attributs et comportements d\u2019une classe parente tout en pouvant les \u00e9tendre ou les modifier. Cela favorise la r\u00e9utilisation du code et la cr\u00e9ation de hi\u00e9rarchies d'objets.

    Polymorphisme

    Le polymorphisme permet \u00e0 des objets de diff\u00e9rentes classes de r\u00e9pondre \u00e0 un m\u00eame message ou appel de m\u00e9thode de mani\u00e8res distinctes. Il peut \u00eatre de deux types\u2009: polymorphisme statique (ou surcharge) comme plusieurs m\u00e9thodes portant le m\u00eame nom mais avec des signatures diff\u00e9rentes ou le polymorphisme dynamique (ou substitutivit\u00e9) lorsqu'une m\u00e9thode peut \u00eatre red\u00e9finie dans une sous-classe pour se comporter diff\u00e9remment tout en conservant le m\u00eame nom.

    Ces concepts, bien que simples en apparence, offrent une immense flexibilit\u00e9 pour mod\u00e9liser des syst\u00e8mes logiciels complexes tout en assurant une gestion claire de la structure et du comportement des objets.

    "}, {"location": "course-cpp/object-oriented/#les-limites-dune-approche-manuelle", "title": "Les limites d\u2019une approche manuelle", "text": "

    Avant l'av\u00e8nement des langages orient\u00e9s objet, le langage C, bien qu\u2019efficace pour la programmation syst\u00e8me, ne poss\u00e9dait pas ces abstractions de haut niveau. Cependant, certains programmeurs cr\u00e9atifs r\u00e9ussirent \u00e0 simuler certains concepts de l\u2019OO dans C, bien que d\u2019une mani\u00e8re moins naturelle et plus complexe.

    Prenons l'exemple des structures en C. Une structure (ou struct) est un regroupement de donn\u00e9es sous un m\u00eame type. On peut y voir une \u00e9bauche d\u2019objet\u2009: elle permet d\u2019associer plusieurs donn\u00e9es li\u00e9es sous un seul type composite. Par exemple\u2009:

    struct Point {\n    int x;\n    int y;\n};\n

    Pour ajouter du comportement \u00e0 cette structure, des pointeurs de fonctions peuvent \u00eatre utilis\u00e9s afin d'associer des fonctions sp\u00e9cifiques \u00e0 des structures. Voici comment un embryon de m\u00e9thode pourrait \u00eatre impl\u00e9ment\u00e9 en C\u2009:

    struct Point {\n    int x;\n    int y;\n    void (*move)(struct Point* self, int dx, int dy);\n};\n\nvoid move_point(struct Point* self, int dx, int dy) {\n    self->x += dx;\n    self->y += dy;\n}\n\nint main() {\n    struct Point p = {0, 0, move_point};\n    p.move(&p, 5, 10); // D\u00e9place le point\n}\n

    Ici, on simule une m\u00e9thode move \u00e0 l\u2019aide d\u2019un pointeur de fonction, ce qui permet \u00e0 un objet (la structure Point) d\u2019avoir un comportement, \u00e0 la mani\u00e8re d\u2019un objet dans un langage OO. Toutefois, cette approche pr\u00e9sente des limites consid\u00e9rables\u2009:

    Manque de s\u00e9curit\u00e9

    Rien ne garantit que les fonctions seront correctement associ\u00e9es aux structures. Le typage est plus l\u00e2che, et le programmeur doit manuellement g\u00e9rer ces associations, augmentant le risque d'erreurs.

    Absence d\u2019h\u00e9ritage et de polymorphisme

    En C, il n\u2019y a pas de moyen natif de cr\u00e9er des hi\u00e9rarchies de types ou de surcharger des fonctions. Toute tentative d'imiter cela implique des bricolages fastidieux.

    Ainsi, bien que C permette une certaine approximation de l'OO, il ne fournit pas les abstractions n\u00e9cessaires pour exploiter pleinement ce paradigme.

    ", "tags": ["struct", "move", "Point"]}, {"location": "course-cpp/object-oriented/#les-classes-une-abstraction-naturelle", "title": "Les classes, une abstraction naturelle", "text": "

    Avec l\u2019introduction de C++, ces approximations manuelles deviennent superflues. Le langage fournit directement des classes, qui englobent non seulement des donn\u00e9es, mais aussi les m\u00e9thodes qui leur sont associ\u00e9es. Une classe en C++ remplace la structure et le pointeur de fonction, tout en introduisant l\u2019encapsulation, l\u2019h\u00e9ritage, et le polymorphisme.

    Voici un exemple \u00e9quivalent en C++ :

    struct Point {\n    int x, y;\n\n    Point(int x = 0, int y = 0) {\n        this->x = x;\n        this->y = y;\n    }\n\n    void move(int dx, int dy) {\n        x += dx;\n        y += dy;\n    }\n};\n\nint main() {\n    Point p(0, 0);\n    p.move(5, 10);  // D\u00e9place le point\n}\n

    Dans cet exemple, la classe Point encapsule les donn\u00e9es (x, y) et les comportements (move) en une seule entit\u00e9 coh\u00e9rente. L'utilisation des constructeurs permet d'initialiser directement l'objet, et la gestion du comportement devient plus intuitive et s\u00e9curis\u00e9e.

    En r\u00e9sum\u00e9, le passage de C \u00e0 C++ repr\u00e9sente un saut qualitatif immense dans la gestion des abstractions. Tandis que C permettait de bricoler des objets avec des structures et des pointeurs de fonction, C++ offre un cadre natif pour la programmation orient\u00e9e objet, avec toutes les garanties et la souplesse que cela implique. Ce qui \u00e9tait laborieux et sujet \u00e0 erreurs dans C devient naturel, \u00e9l\u00e9gant et puissant dans C++.

    ", "tags": ["move", "Point"]}, {"location": "course-cpp/object-oriented/#le-vocabulaire-oriente-objet", "title": "Le vocabulaire orient\u00e9 objet", "text": "

    La programmation orient\u00e9e objet (OO) repose sur un ensemble de concepts cl\u00e9s qui forment un langage commun, essentiel pour comprendre et manipuler les syst\u00e8mes logiciels modernes. Ce vocabulaire constitue la pierre angulaire de la conception OO et permet d'aborder la complexit\u00e9 de mani\u00e8re structur\u00e9e et modulaire. Voici un aper\u00e7u des termes les plus importants de ce paradigme.

    "}, {"location": "course-cpp/object-oriented/#classe", "title": "Classe", "text": "

    Une classe est un mod\u00e8le ou un plan qui d\u00e9finit la structure et le comportement des objets. Elle d\u00e9crit les attributs (ou propri\u00e9t\u00e9s) et les m\u00e9thodes (ou fonctions) que ses instances, appel\u00e9es objets, poss\u00e9deront. Voici un exemple de classe en C++:

    class Animal {\npublic:\n    string nom;\n    int age;\n    void eat() { cout << \"L'animal mange\" << endl; }\n};\n

    Ici, la classe Animal d\u00e9finit deux attributs, nom et age, ainsi qu'une m\u00e9thode eat.

    ", "tags": ["nom", "eat", "Animal", "age"]}, {"location": "course-cpp/object-oriented/#instance", "title": "Instance", "text": "

    Une instance est cr\u00e9\u00e9 \u00e0 partir d'une classe. Chaque instance poss\u00e8de ses propres valeurs pour les attributs de la classe et peut ex\u00e9cuter les m\u00e9thodes d\u00e9finies par celle-ci. Si la classe est le plan d'une maison, une instance est une maison r\u00e9elle construite selon ce plan.

    "}, {"location": "course-cpp/object-oriented/#objet", "title": "Objet", "text": "

    Un objet est une instance d'une classe. Il repr\u00e9sente une entit\u00e9 r\u00e9elle ou abstraite avec laquelle le programme peut interagir. Chaque objet poss\u00e8de son propre \u00e9tat (valeurs des attributs) et peut ex\u00e9cuter les comportements d\u00e9finis par sa classe.

    Animal chat;\nchat.nom = \"Mimi\";\nchat.eat();\n

    Dans cet exemple, chat est un objet de la classe Animal qui poss\u00e8de ses propres valeurs pour nom et age.

    ", "tags": ["nom", "Animal", "chat", "age"]}, {"location": "course-cpp/object-oriented/#attribut", "title": "Attribut", "text": "

    Les attributs (ou champs, propri\u00e9t\u00e9s) sont les variables d\u00e9finies dans une classe qui repr\u00e9sentent l'\u00e9tat ou les caract\u00e9ristiques de l'objet. Chaque objet a ses propres copies de ces attributs.

    int age;   // attribut qui stocke l'\u00e2ge d'un objet de la classe Animal\n
    "}, {"location": "course-cpp/object-oriented/#methode", "title": "M\u00e9thode", "text": "

    Une m\u00e9thode est une fonction d\u00e9finie \u00e0 l'int\u00e9rieur d'une classe, qui repr\u00e9sente un comportement ou une action que les objets de cette classe peuvent accomplir. Les m\u00e9thodes peuvent manipuler les attributs de l'objet ou interagir avec d'autres objets.

    void manger() {\n    cout << \"L'animal mange\" << endl;\n}\n
    "}, {"location": "course-cpp/object-oriented/#encapsulation", "title": "Encapsulation", "text": "

    L'encapsulation consiste \u00e0 restreindre l'acc\u00e8s direct aux attributs d'un objet et \u00e0 contr\u00f4ler cet acc\u00e8s via des m\u00e9thodes. Cela permet de prot\u00e9ger l'\u00e9tat interne de l'objet et de d\u00e9finir clairement les points d'interaction avec celui-ci. En C++, cela se traduit par l'utilisation de modificateurs d'acc\u00e8s comme public, private, et protected. Exemple\u2009:

    class Animal {\nprivate:\n    string nom;  // Attribut priv\u00e9\n\npublic:\n    void setNom(string n) { nom = n; }  // M\u00e9thode publique pour modifier l'attribut\n    string getNom() { return nom; }     // M\u00e9thode publique pour acc\u00e9der \u00e0 l'attribut\n};\n

    Dans cet exemple, l'attribut nom est priv\u00e9, et ne peut \u00eatre directement modifi\u00e9 ou lu que via les m\u00e9thodes publiques setNom et getNom.

    ", "tags": ["nom", "setNom", "protected", "public", "getNom", "private"]}, {"location": "course-cpp/object-oriented/#abstraction", "title": "Abstraction", "text": "

    L'abstraction est le concept de simplification en se concentrant sur les caract\u00e9ristiques essentielles d'un objet tout en cachant les d\u00e9tails non pertinents. Cela permet de manipuler des objets \u00e0 un niveau conceptuel, sans se soucier de leur impl\u00e9mentation interne. En C++, les classes abstraites et les interfaces sont utilis\u00e9es pour fournir des mod\u00e8les conceptuels sans impl\u00e9mentation concr\u00e8te imm\u00e9diate.

    class Forme {\npublic:\n    virtual double aire() = 0;  // M\u00e9thode virtuelle pure, d\u00e9finissant un comportement sans impl\u00e9mentation\n};\n

    Cette classe Forme repr\u00e9sente une abstraction de toute forme g\u00e9om\u00e9trique sans se pr\u00e9occuper de sa nature exacte. Les sous-classes devront fournir leur propre impl\u00e9mentation de la m\u00e9thode aire.

    ", "tags": ["aire", "Forme"]}, {"location": "course-cpp/object-oriented/#heritage", "title": "H\u00e9ritage", "text": "

    L'h\u00e9ritage est un m\u00e9canisme qui permet \u00e0 une classe de d\u00e9river d'une autre classe. La classe d\u00e9riv\u00e9e h\u00e9rite des attributs et m\u00e9thodes de la classe parent, tout en pouvant ajouter ou red\u00e9finir ses propres fonctionnalit\u00e9s. Cela favorise la r\u00e9utilisation du code et permet de cr\u00e9er des hi\u00e9rarchies de classes.

    class Chien : public Animal {  // Chien h\u00e9rite d'Animal\npublic:\n    void aboyer() {\n        cout << \"Le chien aboie\" << endl;\n    }\n};\n

    Ici, Chien h\u00e9rite des attributs et m\u00e9thodes de Animal, tout en ajoutant son propre comportement aboyer.

    ", "tags": ["Animal", "Chien", "aboyer"]}, {"location": "course-cpp/object-oriented/#polymorphisme", "title": "Polymorphisme", "text": "

    Le polymorphisme est la capacit\u00e9 d'une m\u00e9thode ou d'un objet \u00e0 se comporter de mani\u00e8re diff\u00e9rente en fonction du contexte. Il permet \u00e0 une classe d\u00e9riv\u00e9e de red\u00e9finir des m\u00e9thodes de la classe parent. Le polymorphisme peut \u00eatre statique (surcharge de m\u00e9thodes) ou dynamique (via l'utilisation de m\u00e9thodes virtuelles).

    class Animal {\npublic:\n    virtual void faireDuBruit() { cout << \"L'animal fait du bruit\" << endl; }\n};\n\nclass Chien : public Animal {\npublic:\n    void faireDuBruit() override { cout << \"Le chien aboie\" << endl; }\n};\n\nAnimal* a = new Chien();\na->faireDuBruit();  // Appelle la m\u00e9thode faireDuBruit() de la classe Chien\n

    Gr\u00e2ce au polymorphisme dynamique, m\u00eame si a est de type Animal, l'appel de la m\u00e9thode faireDuBruit() ex\u00e9cutera celle de Chien.

    ", "tags": ["Animal", "Chien"]}, {"location": "course-cpp/object-oriented/#constructeur-et-destructeur", "title": "Constructeur et Destructeur", "text": "

    Un constructeur est une m\u00e9thode sp\u00e9ciale appel\u00e9e lors de la cr\u00e9ation d'un objet. Il initialise les attributs de l'objet. Un destructeur est une m\u00e9thode appel\u00e9e lors de la destruction de l'objet, qui permet de lib\u00e9rer des ressources ou de nettoyer l'\u00e9tat.

    class Animal {\npublic:\n    Animal() { cout << \"L'animal est cr\u00e9\u00e9\" << endl; }  // Constructeur\n    ~Animal() { cout << \"L'animal est d\u00e9truit\" << endl; }  // Destructeur\n};\n
    "}, {"location": "course-cpp/object-oriented/#interface-et-classe-abstraite", "title": "Interface et classe abstraite", "text": "

    Une interface est une classe qui ne contient que des m\u00e9thodes virtuelles pures, c\u2019est-\u00e0-dire des m\u00e9thodes sans impl\u00e9mentation, que les classes d\u00e9riv\u00e9es doivent impl\u00e9menter. Une classe abstraite est une classe qui peut contenir \u00e0 la fois des m\u00e9thodes impl\u00e9ment\u00e9es et des m\u00e9thodes virtuelles pures.

    class IAnimal {\npublic:\n    virtual void faireDuBruit() = 0;  // Interface : m\u00e9thode pure\n};\n

    Toute classe qui impl\u00e9mente cette interface doit fournir une impl\u00e9mentation de faireDuBruit().

    "}, {"location": "course-cpp/object-oriented/#surcharge", "title": "Surcharge", "text": "

    La surcharge est une forme de polymorphisme statique o\u00f9 plusieurs m\u00e9thodes peuvent partager le m\u00eame nom mais avec des signatures diff\u00e9rentes (param\u00e8tres distincts).

    class Math {\npublic:\n    int additionner(int a, int b) { return a + b; }\n    double additionner(double a, double b) { return a + b; }  // Surcharge de la m\u00e9thode\n};\n
    "}, {"location": "course-cpp/object-oriented/#redefinition", "title": "Red\u00e9finition", "text": "

    La red\u00e9finition est le fait qu\u2019une classe d\u00e9riv\u00e9e puisse fournir sa propre version d'une m\u00e9thode d\u00e9finie dans une classe parent. Cela se fait g\u00e9n\u00e9ralement via les m\u00e9thodes virtuelles.

    class Animal {\npublic:\n    virtual void faireDuBruit() { cout << \"L'animal fait du bruit\" << endl; }\n};\n\nclass Chat : public Animal {\npublic:\n    void faireDuBruit() override { cout << \"Le chat miaule\" << endl; }  // Red\u00e9finition\n};\n
    "}, {"location": "course-cpp/shared-memory/", "title": "Shared memory", "text": "", "tags": ["mmap"]}, {"location": "course-cpp/shared-memory/#memoire-partagee", "title": "M\u00e9moire partag\u00e9e", "text": "

    Nous le verrons plus loin au chapitre sur la MMU, mais la m\u00e9moire d'un processus m\u00e9moire (programme) ne peut pas \u00eatre acc\u00e9d\u00e9e par un autre programme. Le syst\u00e8me d'exploitation l'en emp\u00eache.

    Lorsque l'on souhaite communiquer entre plusieurs programmes, il est possible d'utiliser diff\u00e9rentes m\u00e9thodes\u2009:

    • les flux (fichiers, stdin, stdout...)
    • la m\u00e9moire partag\u00e9e
    • les sockets

    Vous avez d\u00e9j\u00e0 vu les flux au chapitre pr\u00e9c\u00e9dent, et les sockets ne font pas partie de ce cours d'introduction.

    Notons que la m\u00e9moire partag\u00e9e est un m\u00e9canisme propre \u00e0 chaque syst\u00e8me d'exploitation. Sous POSIX elle est normalis\u00e9e et donc un programme compatible POSIX et utilisant la m\u00e9moire partag\u00e9e pourra fonctionner sous Linux, WSL ou macOS, mais pas sous Windows.

    C'est principalement l'appel syst\u00e8me mmap qui est utilis\u00e9. Il permet de mapper ou d\u00e9mapper des fichiers ou des p\u00e9riph\u00e9riques dans la m\u00e9moire.

    void *mmap(\n    void *addr,\n    size_t length, // Size in bytes\n    int prot,      // Access protection (read/write/execute)\n    int flags,     // Attributs (shared/private/anonymous...)\n    int fd,\n    int offset\n);\n

    Voici un exemple permettant de r\u00e9server un espace partag\u00e9 en \u00e9criture et en lecture entre deux processus\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n#include <sys/mman.h>\n\nvoid* create_shared_memory(size_t size) {\n    // Accessible en lecture et \u00e9criture\n    int protection = PROT_READ | PROT_WRITE;\n\n    // D'autres processus peuvent acc\u00e9der \u00e0 cet espace\n    // lequel est anonyme\n    // so only this process and its children will be able to use it:\n    int visibility = MAP_SHARED | MAP_ANONYMOUS;\n\n    // The remaining parameters to `mmap()` are not important for this use case,\n    // but the manpage for `mmap` explains their purpose.\n    return mmap(NULL, size, protection, visibility, -1, 0);\n}\n
    "}, {"location": "course-cpp/shared-memory/#file-memory-mapping", "title": "File memory mapping", "text": "

    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\u00e9cessaire, ce fichier est copi\u00e9 en m\u00e9moire\u2009:

    FILE *fp = fopen(\"foo\", \"r\");\nfseek(fp, 0, SEEK_END);\nint filesize = ftell(fp);\nfseek(fp, 0, SEEK_SET);\nchar *file = malloc(filesize);\nfread(file, filesize, sizeof(char), fp);\nfclose(fp);\n

    Cette copie n'est pas n\u00e9cessairement n\u00e9cessaire. Une approche POSIX, qui n'est donc pas couverte par le standard C99 consiste \u00e0 lier le fichier dans un espace m\u00e9moire partag\u00e9.

    Ceci n\u00e9cessite l'utilisation de fonctions bas niveau.

    #include <stdio.h>\n#include <stdlib.h>\n#include <sys/mman.h>\n\nint main() {\n    int fd = open(\"foo.txt\", O_RDWR, 0600);\n    char *addr = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n    printf(\"Espace mapp\u00e9 \u00e0 %p\\n\", addr);\n    printf(\"Premiers caract\u00e8res du fichiers : %.*s...\\n\", 20, addr);\n}\n

    Les avantages de cette m\u00e9thode sont\u2009:

    • pas n\u00e9cessaire de copier l'int\u00e9gralit\u00e9 du fichier en m\u00e9moire\u2009;
    • possibilit\u00e9 de partager le m\u00eame fichier ouvert entre plusieurs processus\u2009;
    • possibilit\u00e9 laiss\u00e9e au syst\u00e8me d'exploitation d'utiliser la RAM ou non si les ressources m\u00e9moires deviennent tendues.
    ", "tags": ["fopen"]}, {"location": "summary/summary/", "title": "R\u00e9sum\u00e9", "text": ""}, {"location": "summary/summary/#introduction", "title": "Introduction", "text": "

    Le langage C a cr\u00e9\u00e9 en 1972 par Brian Kernighan et Dennis Ritchie est s'est d\u00e8s lors impos\u00e9 comme le standard industriel pour la programmation embarqu\u00e9e et pour tout d\u00e9veloppement n\u00e9cessitant de hautes performances.

    Le langage est standardis\u00e9 par l'ISO (standardisation internationale) et le standard le plus couramment utilis\u00e9 en 2019 est encore C99.

    Il faut retenir que le C est un langage dit\u2009:

    • Imp\u00e9ratif: programmation en s\u00e9quences de commandes
    • Structur\u00e9: programmation imp\u00e9rative avec des structures de contr\u00f4le imbriqu\u00e9es
    • Proc\u00e9dural: programmation imp\u00e9rative avec appels de proc\u00e9dures

    Ce sont ses paradigmes de programmation

    "}, {"location": "summary/summary/#cycle-de-developpement", "title": "Cycle de d\u00e9veloppement", "text": "

    Le cycle de d\u00e9veloppement se compose toujours des phases\u2009: \u00e9tude, \u00e9criture du cahier des charges, de l'\u00e9criture des tests, de la conception du logiciel, du codage \u00e0 proprement parler et des validations finales. Le mod\u00e8le en cascade est un bon r\u00e9sum\u00e9 tr\u00e8s utilis\u00e9 dans l'industrie.

    "}, {"location": "summary/summary/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    Faire \u00e9voluer un logiciel est aussi un processus it\u00e9ratif\u2009:

    • Editer le code avec un \u00e9diteur comme vi ou vscode
    • Compilation et pr\u00e9traitement console $ gcc -std=c99 -O2 -Wall -c foo.c -o foo.o $ gcc -std=c99 -O2 -Wall -c bar.c -o bar.o
    • Edition des liens console $ gcc -o foobar foo.o bar.o -lm
    • Tests
    ", "tags": ["vscode"]}, {"location": "summary/summary/#make", "title": "Make", "text": "

    Souvent, pour s'\u00e9viter de r\u00e9p\u00e9ter les m\u00eames commandes les d\u00e9veloppeurs utilisent un outil comme make qui tire des r\u00e8gles de compilations d'un fichier nomm\u00e9 Makefile. Cet outil permet d'automatiquement recompiler les fichiers qui ne sont plus \u00e0 jour et r\u00e9g\u00e9n\u00e9rer automatiquement l'ex\u00e9cutable. Certaines recettes de make sont souvent utilis\u00e9es comme\u2009:

    • make all Pour compiler tout le projet
    • make clean Pour supprimer tous les fichiers interm\u00e9diaires g\u00e9n\u00e9r\u00e9s
    • make mrproper Pour supprimer tous les fichiers interm\u00e9diaires ainsi que les ex\u00e9cutables produits.
    • make test Pour ex\u00e9cuter les tests de validation

    D'autres recettes peuvent \u00eatre \u00e9crites dans le fichier Makefile, mais la courbe d'apprentissage du langage de make est raide.

    ", "tags": ["Makefile", "make"]}, {"location": "summary/summary/#linuxposix", "title": "Linux/POSIX", "text": "

    Un certain nombre de commandes sont utilis\u00e9es durant ce cours et voici un r\u00e9sum\u00e9 de ces derni\u00e8res

    Commande Description cat Affiche sur stdout le contenu d'un fichier ls Liste le contenu du r\u00e9pertoire courant ls -al Liste le contenu du r\u00e9pertoire courant avec plus de d\u00e9tails echo Affiche sur stdout les \u00e9l\u00e9ments pass\u00e9s par argument au programme make Outil d'aide \u00e0 la compilation utilisant le fichier Makefile gcc Compilateur open source largement utilis\u00e9 dans l'industrie vi \u00c9diteur de texte ultra puissant, mais difficile \u00e0 apprendre", "tags": ["make", "gcc", "cat", "echo", "Makefile", "stdout"]}, {"location": "summary/summary/#programmation", "title": "Programmation", "text": ""}, {"location": "summary/summary/#diagramme-de-flux", "title": "Diagramme de flux", "text": "

    Le diagramme de flux est beaucoup utilis\u00e9 pour exprimer un algorithme comme celui d'Euclide pour chercher le plus grand diviseur commun.

    :::{figure} {assets}/figures/dist/algorithm/euclide-gcd.* Algorithme de calcul du PGCD d'Euclide. :::

    "}, {"location": "summary/summary/#langage-c", "title": "Langage C", "text": ""}, {"location": "summary/summary/#caracteres-non-imprimables", "title": "Caract\u00e8res non imprimables", "text": "Expression Nom Nom anglais Description \\n LF Line feed Retour \u00e0 la ligne \\v VT Vertical tab Tabulation verticale (entre les paragraphes) \\f FF New page Saut de page \\t TAB Horizontal tab Tabulation horizontale \\r CR Carriage return Retour charriot \\b BS Backspace Retour en arri\u00e8re, effacement d'un caract\u00e8re", "tags": ["TAB"]}, {"location": "summary/summary/#fin-de-lignes", "title": "Fin de lignes", "text": "

    Les caract\u00e8res de fin de ligne d\u00e9pendent du syst\u00e8me d'exploitation et sont appel\u00e9s EOL: End Of Line.

    Expression Nom Syst\u00e8me d'exploitation \\r\\n CRLF Windows \\r CR Anciens Macintoshs (\\< 2000) \\n LF Linux/Unix/POSIX", "tags": ["CRLF"]}, {"location": "summary/summary/#identificateurs", "title": "Identificateurs", "text": "

    :::{figure} {assets}/figures/dist/grammar/identifier.* Grammaire d'un identificateur C :::

    Le format des identificateurs peut \u00e9galement \u00eatre exprim\u00e9 par une expression r\u00e9guli\u00e8re\u2009:

    ^[a-zA-Z_][a-zA-Z0-9_]*$\n
    "}, {"location": "summary/summary/#variable", "title": "Variable", "text": "

    Une variable poss\u00e8de 6 param\u00e8tres\u2009: nom, type, valeur, adresse, port\u00e9e, visibilit\u00e9.

    Elle peut \u00eatre\u2009: globale et dans ce cas elle est automatiquement initialis\u00e9e \u00e0 0\u2009:

    int foo;\n\nint main(void) {\n    return foo;\n}\n

    Ou elle peut \u00eatre locale et dans ce cas il est n\u00e9cessaire de l'initialiser \u00e0 une valeur\u2009:

    int main(void) {\n    int foo = 0;\n    return foo;\n}\n

    Il est possible de d\u00e9clarer plusieurs variables d'un m\u00eame type sur la m\u00eame ligne\u2009:

    int i, j, k;\nint m = 32, n = 22;\n

    Les conventions de nommage pour une variable sont\u2009: camelCase et snake_case, certains utilisent la notation PascalCase.

    Les termes toto, tata, foo, bar sont souvent utilis\u00e9s comme noms g\u00e9n\u00e9riques et sont appel\u00e9s termes m\u00e9tasyntaxiques.

    ", "tags": ["bar", "snake_case", "camelCase", "foo", "toto", "tata", "PascalCase"]}, {"location": "summary/summary/#constantes-litterales", "title": "Constantes litt\u00e9rales", "text": "

    Une constante litt\u00e9rale est une grandeur exprimant une valeur donn\u00e9e qui n'est pas calcul\u00e9e \u00e0 l'ex\u00e9cution\u2009:

    Expression Type Description 6 int Valeur d\u00e9cimale 12u unsigned int Valeur non sign\u00e9e en notation d\u00e9cimale 6l long Valeur longue en notation d\u00e9cimale 010 int Valeur octale 0xa int Valeur hexad\u00e9cimale 0b111 int Valeur binaire (uniquement gcc, pas standard C99) 12. double Nombre r\u00e9el 'a' char Caract\u00e8re \"salut\" char* Cha\u00eene de caract\u00e8re", "tags": ["double", "gcc", "char", "long", "int"]}, {"location": "summary/summary/#commentaires", "title": "Commentaires", "text": "

    Il existe deux types de commentaires\u2009:

    • Les commentaires de lignes (depuis C99)
    // This is a single line comment.\n
    • Les commentaires de blocs
    /* This is a\n   Multi-line comment */\n
    "}, {"location": "summary/summary/#fonction-main", "title": "Fonction main", "text": "

    La fonction main peut s'\u00e9rire sous deux formes\u2009:

    int main(void) {\n    return 0;\n}\n
    int main(int argc, char *argv[]) {\n    return 0;\n}\n
    "}, {"location": "summary/summary/#numeration", "title": "Num\u00e9ration", "text": "

    Les donn\u00e9es dans l'ordinateur sont stock\u00e9es sous forme binaire et le type d'une variable permet de d\u00e9finir son interpr\u00e9tation.

    • Une valeur enti\u00e8re et non sign\u00e9e est exprim\u00e9e sous la forme binaire pure\u2009: text \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u25020\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = 0b1010011 = 83 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    • Une valeur enti\u00e8re et sign\u00e9e est exprim\u00e9e en compl\u00e9ment \u00e0 deux\u2009: text \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510 \u25021\u25021\u25020\u25021\u25020\u25020\u25021\u25021\u2502 = ! \u25020\u25020\u25021\u25020\u25021\u25021\u25020\u25020\u2502 = (-1) * (0b101100 + 1) = -45 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    • Une valeur r\u00e9elle ou flottante est exprim\u00e9e selon le standard IEEE-754 et comporte un bit de signe, un exposant et une mantisse. \u250c Signe 1 bit \u2502 \u250c Exposant 8 bits \u2502 \u2502 \u250c Mantisse 23 bits \u2534 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u251e\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u2540\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2510\u250c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u252c\u2500\u2526 \u25020\u25020\u25020\u25021\u25020\u25020\u25020\u25020\u2502\u25020\u25021\u25020\u25020\u25021\u25020\u25020\u25020\u2502\u25021\u25021\u25020\u25021\u25021\u25021\u25021\u25021\u2502\u25020\u25021\u25020\u25020\u25020\u25020\u25020\u25021\u2502 \u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518\u2514\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2534\u2500\u2518
    "}, {"location": "summary/summary/#operateurs", "title": "Op\u00e9rateurs", "text": "

    Les op\u00e9rateurs appliquent une op\u00e9ration entre une ou plusieurs valeurs\u2009:

    • Les op\u00e9rateurs unaire s'appliquent \u00e0 un seul op\u00e9rande (!12, ~23)
    • Les op\u00e9rateurs standards s'appliquent \u00e0 deux op\u00e9randes (12 ^ 32)

    Les op\u00e9rateurs ont une priorit\u00e9 et une direction d'associativit\u00e9\u2009:

    u = ++a + b * c++ >> 3 ^ 2\n\nRang  Op\u00e9rateur  Associativit\u00e9\n----  ---------  -------------\n 1    ()++       -->\n 2    ++()       <--\n 2    +          <--\n 2    *          <--\n 5    >>         -->\n 9    ^          -->\n14    =          -->\n

    Donc la priorit\u00e9 de ces op\u00e9rations sera\u2009:

    (u = ((((++a) + (b * (c++))) >> 3) ^ 2))\n

    Dans le cas des op\u00e9rateurs de pr\u00e9 et post incr\u00e9mentation, ils sont en effet les plus prioritaires mais leur action est d\u00e9cal\u00e9e dans le temps au pr\u00e9c\u00e9dent/suivant point de s\u00e9quence. C'est-\u00e0-dire\u2009:

    a += 1;\n(u = (((a + (b * c)) >> 3) ^ 2));\nc += 1;\n
    "}, {"location": "summary/summary/#valeur-gauche", "title": "Valeur gauche", "text": "

    Une valeur gauche lvalue d\u00e9fini ce qui peut se trouver \u00e0 gauche d'une affectation. C'est un terme qui appara\u00eet souvent dans les erreurs de compilation. L'exemple suivant retourne l'erreur\u2009: lvalue required as increment operand car le r\u00e9sultat de a + b n'a pas d'emplacement m\u00e9moire et il n'est pas possible de l'assigner \u00e0 quelque chose pour effectuer l'op\u00e9ration de pr\u00e9-incr\u00e9mentation.

    c = ++(a + b);\n

    Dans cet exemple c est une valeur gauche

    "}, {"location": "summary/summary/#types-de-donnees", "title": "Types de donn\u00e9es", "text": "

    Dans 90% des cas, voici les types qu'un d\u00e9veloppeur utilisera en C et sur le mod\u00e8le de donn\u00e9e LP64

    Type Profondeur Description char 8-bit Caract\u00e8re ou valeur d\u00e9cimale int 32-bit Entier sign\u00e9 unsigned int 32-bit Entier non sign\u00e9 long long 64-bit Entier sign\u00e9 float 32-bit Nombre r\u00e9el (23 bit de mantisse) double 64-bit Nombre r\u00e9el (54 bit de mantisse)

    Pour s'assurer d'une taille donn\u00e9e on peut utiliser les types standard C99 en incluant la biblioth\u00e8que <stdint.h>

    #include <stdint.h>\n\nint main(void) {\n    int8_t foo = 0;  // Valeur sign\u00e9e sur 8-bit\n    uint32_t bar = 0;  // Valeur non sign\u00e9e sur 32-bit\n\n    uint_least16_t = 0;  // Valeur non sign\u00e9e d'au moins 16-bit\n}\n

    Les valeurs sign\u00e9es sont exprim\u00e9es en compl\u00e9ment \u00e0 deux c'est-\u00e0-dire que les valeurs maximales et minimales sont pour un entier 8-bit de -128 \u00e0 +128.

    La construction des types standards\u2009:

    :::{figure} {assets}/figures/dist/datatype/ansi-integers.* :alt\u2009: \"Entiers standardis\\xE9s C89\" :width\u2009: 100 % :::

    La construction des types portables\u2009:

    :::{figure} {assets}/figures/dist/datatype/c99-integers.* :alt\u2009: \"Entiers standardis\\xE9s C99\" :width\u2009: 100 % :::

    ", "tags": ["char", "float", "int", "double"]}, {"location": "summary/summary/#caracteres", "title": "Caract\u00e8res", "text": "

    Un caract\u00e8re est une valeur binaire cod\u00e9e sur 8-bit et dont l'interpr\u00e9tation est confi\u00e9e \u00e0 une table de correspondance nomm\u00e9e ASCII :

    :::{figure} {assets}/figures/dist/encoding/ascii.* Table ANSI INCITS 4-1986 (standard actuel) :::

    Seul ces valeurs sont garanties d'\u00eatre stock\u00e9es sur 8-bit. Pour les caract\u00e8res accentu\u00e9s ou les \u00e9motic\u00f4nes, la mani\u00e8re dont ils sont cod\u00e9s en m\u00e9moire d\u00e9pend de l'encodage des caract\u00e8res. Souvent on utilise le type d'encodage utf-8.

    Les \u00e9critures suivantes sont donc strictement identiques\u2009:

    char a;\n\na = 'a';\na = 0x61;\na = 97;\na = 0141;\n
    "}, {"location": "summary/summary/#chaine-de-caractere", "title": "Cha\u00eene de caract\u00e8re", "text": "

    Une cha\u00eene de caract\u00e8re est exprim\u00e9e avec des guillemets double. Une cha\u00eene de caract\u00e8re comporte toujours un caract\u00e8re terminal \\0.

    char str[] = \"Hello\";\n

    La taille en m\u00e9moire de cette cha\u00eene de caract\u00e8re est de 6 bytes, 5 caract\u00e8res et un caract\u00e8re de terminaison.

    "}, {"location": "summary/summary/#booleens", "title": "Bool\u00e9ens", "text": "

    En C la valeur 0 est consid\u00e9r\u00e9e comme fausse (false) et une valeur diff\u00e9rente de 0 est consid\u00e9r\u00e9e comme vraie (true). Toutes les assertions suivantes sont vraies\u2009:

    if (42) { /* ... */ }\nif (!0) { /* ... */ }\nif (true && true || false) { /* ... */ }\n

    Pour utiliser les mots cl\u00e9s true et false il faut utiliser la biblioth\u00e8que <stdbool.h>

    ", "tags": ["true", "false"]}, {"location": "summary/summary/#promotion-implicite", "title": "Promotion implicite", "text": "

    Un type est automatiquement et tacitement promu dans le type le plus g\u00e9n\u00e9ral\u2009:

    char a;\nint b;\nlong long c;\nunsigned int d;\n\na + b // R\u00e9sultat promu en `int`\na + c // R\u00e9sultat promu en `long long`\nb + d // R\u00e9sultat promu en `int`\n

    Attention aux valeurs en virgule flottante\u2009:

    int a = 9, b = 2;\ndouble b;\n\na / b;  // R\u00e9sultat de type entier, donc 4 et non 4.5\n(float)a / b;  // R\u00e9sultat de type float donc 4.5\nb / a;  // R\u00e9sultat en type double (promotion)\n
    "}, {"location": "summary/summary/#transtypage", "title": "Transtypage", "text": "

    Pr\u00e9fixer une variable ou une valeur avec (int) comme dans\u2009: (int)a permet de convertir explicitement cette variable dans le type donn\u00e9.

    Le transtypage peut \u00eatre implicite par exemple dans int a = 4.5

    Ou plus sp\u00e9cifiquement dans\u2009:

    float u = 0.0;\nprintf(\"%f\", b); // Promotion implicite de `float` en `double`\n
    "}, {"location": "summary/summary/#structure-de-controle", "title": "Structure de contr\u00f4le", "text": ""}, {"location": "summary/summary/#sequence", "title": "S\u00e9quence", "text": "

    Une s\u00e9quence est d\u00e9termin\u00e9e par un bloc de code entre accolades\u2009:

    {\n    int a = 12;\n    b += a;\n}\n
    "}, {"location": "summary/summary/#si-sinon", "title": "Si, sinon", "text": "
    if (condition)\n{\n    // Si vrai\n}\nelse\n{\n    // Sinon\n}\n
    "}, {"location": "summary/summary/#si-sinon-si-sinon", "title": "Si, sinon si, sinon", "text": "
    if (condition)\n{\n    // Si vrai\n}\nelse if (autre_condition)\n{\n    // Sinon si autre condition valide\n}\nelse\n{\n    // Sinon\n}\n
    "}, {"location": "summary/summary/#boucle-for", "title": "Boucle For", "text": "
    for (int i = 0; i < 10; i++)\n{\n    // Block ex\u00e9cut\u00e9 10 fois\n}\n\nk = i; // Erreur car `i` n'est plus accessible ici...\n
    "}, {"location": "summary/summary/#boucle-while", "title": "Boucle While", "text": "
    int i = 10;\n\nwhile (i > 0) {\n    i--;\n}\n
    "}, {"location": "summary/summary/#programmes-et-processus", "title": "Programmes et Processus", "text": "\u00c9l\u00e9ment Description stdin Entr\u00e9e standard stdout Sortie standard stderr Sortie d'erreur standard argc Nombre d'arguments argv Valeurs des arguments exit-status Status de sortie d'un programme $? signaux Interaction avec le syst\u00e8me d'exploitation

    :::{figure} {assets}/figures/dist/process/program.* R\u00e9sum\u00e9 des interactions avec un programme :::

    ", "tags": ["stdin", "argc", "stderr", "signaux", "stdout", "argv"]}, {"location": "summary/summary/#entrees-sorties", "title": "Entr\u00e9es Sorties", "text": ""}, {"location": "summary/summary/#printf", "title": "printf", "text": "

    Les sorties format\u00e9es utilisent printf dont le format est\u2009:

    %[parameter][flags][width][.precision][length]type\n
    parameter (optionnel)

    Num\u00e9ro de param\u00e8tre \u00e0 utiliser

    flags (optionnel)

    Modificateurs\u2009: pr\u00e9fixe, signe plus, alignement \u00e0 gauche ...

    width (optionnel)

    Nombre minimum de caract\u00e8res \u00e0 utiliser pour l'affichage de la sortie.

    .precision (optionnel)

    Nombre minimum de caract\u00e8res affich\u00e9s \u00e0 droite de la virgule. Essentiellement, valide pour les nombres \u00e0 virgule flottante.

    length (optionnel)

    Longueur en m\u00e9moire. Indique la longueur de la repr\u00e9sentation binaire.

    type

    Type de formatage souhait\u00e9

    :::{figure} {assets}/figures/dist/string/formats.* Formatage d'un marqueur :::

    ", "tags": ["parameter", "printf", "flags", "length", "width", "type"]}, {"location": "summary/summary/#techniques-de-programmation", "title": "Techniques de programmation", "text": ""}, {"location": "summary/summary/#masque-binaire", "title": "Masque binaire", "text": "

    Pour tester si un bit est \u00e0 un\u2009:

    if (c & 0x040)\n

    Pour forcer un bit \u00e0 z\u00e9ro\u2009:

    c &= ~0x02;\n

    Pour forcer un bit \u00e0 un\u2009:

    c |= 0x02;\n
    "}, {"location": "summary/summary/#permuter-deux-variables-sans-valeur-intermediaire", "title": "Permuter deux variables sans valeur interm\u00e9diaire", "text": "
    a = b ^ c;\nb = a ^ c;\na = b ^ c;\n
    "}, {"location": "tools/analysis/analysis/", "title": "Diagnostiques", "text": "

    Les outils de diagnostiques permettent de comprendre le comportement d'un programme en cours d'ex\u00e9cution. Ils permettent de voir les appels syst\u00e8mes, les appels de fonctions, les appels de biblioth\u00e8ques, les appels de fonctions syst\u00e8me, les appels de fonctions de biblioth\u00e8ques.

    "}, {"location": "tools/analysis/analysis/#time", "title": "time", "text": "

    time est une commande Unix qui permet de mesurer le temps d'ex\u00e9cution d'un programme. Elle est utilis\u00e9e pour mesurer le temps d'ex\u00e9cution d'un programme et les ressources utilis\u00e9es par ce programme. L'utilisation typique est la suivante\u2009:

    time ./a.out\n./a.out 1.23s user 0.10s system 78% cpu 1.23 total\n

    On note plusieurs m\u00e9triques\u2009:

    • user est le temps pass\u00e9 dans l'espace utilisateur, c'est le temps r\u00e9el pass\u00e9 par le programme \u00e0 ex\u00e9cuter des instructions.
    • system est le temps pass\u00e9 dans l'espace noyau, c'est le temps pass\u00e9 par le programme \u00e0 ex\u00e9cuter des appels syst\u00e8mes.
    • wall ou real est le temps r\u00e9el pass\u00e9 par le programme \u00e0 s'ex\u00e9cuter. C'est le temps qui s'\u00e9coule entre le lancement du programme et sa fin.
    • cpu est le pourcentage de temps CPU utilis\u00e9 par le programme.

    Un programme multi-thread\u00e9 peut utiliser plus de 100% de CPU si plusieurs threads sont ex\u00e9cut\u00e9s en parall\u00e8le. Par exemple un programme qui s'ex\u00e9cuterait de mani\u00e8re parall\u00e8le sur 5 processeurs et dont le temps mur serait de 1 secondes, aurait un temps CPU de 500% et un temps user de 5 seconde.

    ", "tags": ["real", "user", "time", "wall", "system", "cpu"]}, {"location": "tools/analysis/analysis/#strace", "title": "strace", "text": "

    Strace a \u00e9t\u00e9 initialement \u00e9crit pour SunOS par Paul Kranenburg en 1991. Il a \u00e9t\u00e9 port\u00e9 sur Linux par Branko Lankester en 1992. Strace est un outil de diagnostique qui permet de suivre les appels syst\u00e8mes et les signaux re\u00e7us par un processus. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution.

    Son code source est bien entendu disponible sur GitHub et il est \u00e9crit en C.

    L'utilisation typique est la suivante, o\u00f9 PID est le num\u00e9ro de processus du programme \u00e0 tracer.

    strace -o /tmp/strace.log -f -e trace=all -p PID\n

    Il est \u00e9galement possible d'ex\u00e9cuter un programme avec strace directement.

    strace ./a.out\n
    ", "tags": ["PID", "strace"]}, {"location": "tools/analysis/analysis/#cas-dutilisation", "title": "Cas d'utilisation", "text": ""}, {"location": "tools/analysis/analysis/#analyse-de-performance", "title": "Analyse de performance", "text": "

    Les performances d'un programmes peuvent \u00eatre impact\u00e9es par des appels syst\u00e8mes trop nombreux. Lorsqu'un programme communique avec le syst\u00e8me d'exploitation (lecture disque, r\u00e9seau, etc.), il doit passer par des appels syst\u00e8mes. Ces appels peuvent \u00eatre co\u00fbteux en temps et en ressources car ils n\u00e9cessitent un changement de contexte entre le mode utilisateur et le mode noyau. C'est \u00e0 dire que le programme doit se mettre en pause pour laisser le noyau effectuer l'op\u00e9ration demand\u00e9e. Lors de la mesure du temps d'ex\u00e9cution d'un programme, par exemple avec time, on peut noter le temps system qui correspond au temps pass\u00e9 dans le noyau. Si ce temps est trop \u00e9lev\u00e9, cela peut indiquer que le programme n'est pas efficace car il passe son temps \u00e0 attendre des op\u00e9rations d'entr\u00e9e/sortie. strace permet dans ce cas de mesurer le nombre d'appels syst\u00e8mes et de les analyser pour comprendre pourquoi le programme est lent.

    \u00c0 titre d'exemple, prenons la sortie standard. Dans le standard POSIX, un programme n'est pas directement connect\u00e9 \u00e0 la sortie standard. La biblioth\u00e8que standard C utiliser un tampon pour stocker les donn\u00e9es avant de les envoyer \u00e0 la sortie standard. Ce tampon est automatiquement vid\u00e9 selon certaines conditions notamment\u2009:

    • lorsqu'un caract\u00e8re de fin de ligne est \u00e9crit,
    • lorsque le programme se termine,
    • lorsque le tampon est plein,
    • lorsque le programme appelle fflush ou fclose.

    Si vous \u00e9crivez par exemple le code suivant, ce n'est que lorsque le caract\u00e8re \\n est \u00e9crit que le tampon est vid\u00e9 et que l'appel syst\u00e8me write est effectu\u00e9. On peut le v\u00e9rifier avec strace

    printf(\"foo\");\nprintf(\"bar\");\nprintf(\"\\n\");\n

    Cette ex\u00e9cution est report\u00e9e avec la ligne suivante. On observe que les trois op\u00e9rations printf sont regroup\u00e9es dans un seul appel syst\u00e8me.

    write(1, \"foobar\\n\", 7)                 = 7\n

    En revanche, si le programme est modifi\u00e9 pour vider le tampon apr\u00e8s chaque caract\u00e8re, on observe davantage d'appels syst\u00e8mes\u2009:

    int main() {\n   char *str = \"foobar\\n\";\n   while (*str) {\n      putchar(*(str++));\n      fflush(stdout);\n   }\n}\n
    write(1, \"f\", 1f)     = 1\nwrite(1, \"o\", 1o)     = 1\nwrite(1, \"o\", 1o)     = 1\nwrite(1, \"b\", 1b)     = 1\nwrite(1, \"a\", 1a)     = 1\nwrite(1, \"r\", 1r)     = 1\nwrite(1, \"\\n\", 1)     = 1\n
    ", "tags": ["fclose", "write", "printf", "time", "strace", "fflush"]}, {"location": "tools/analysis/analysis/#suivi-des-operations-dentreesortie", "title": "Suivi des op\u00e9rations d'entr\u00e9e/sortie", "text": "

    strace permet de suivre les op\u00e9rations d'entr\u00e9e/sortie d'un programme. Cela peut \u00eatre utile pour comprendre pourquoi un programme ne fonctionne pas correctement. Par exemple, si un programme lit un fichier et que le fichier n'est pas trouv\u00e9, strace permet de voir l'appel syst\u00e8me open et le code d'erreur ENOENT qui indique que le fichier n'existe pas.

    Dans ce cas on ne sera sensible qu'aux appels syst\u00e8mes open et write:

    strace -e trace=open,read,write ./a.out\n
    ", "tags": ["ENOENT", "open", "write", "strace"]}, {"location": "tools/analysis/analysis/#resolution-des-dependances", "title": "R\u00e9solution des d\u00e9pendances", "text": "

    Au d\u00e9but de l'ex\u00e9cution du programme, avant que le main ne soit ex\u00e9cut\u00e9, la biblioth\u00e8que standard C doit \u00eatre charg\u00e9e. strace permet de voir les d\u00e9pendances du programme et les biblioth\u00e8ques qui sont charg\u00e9es. Cela peut \u00eatre utile pour comprendre pourquoi un programme ne fonctionne pas correctement ou pourquoi il ne trouve pas une biblioth\u00e8que. On observera typiquement au d\u00e9but de la sortie de strace quelque chose de similaire \u00e0 ceci\u2009:

    execve(\"./a.out\", [\"./a.out\"], 0x7ffec3bd4ef0 /* 50 vars */) = 0\n\nbrk(NULL) = 0x56475a63a000\nmmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2143f6f000\naccess(\"/etc/ld.so.preload\", R_OK) = -1 ENOENT (No such file or directory)\n\nopenat(AT_FDCWD, \"/etc/ld.so.cache\", O_RDONLY|O_CLOEXEC) = 3\nfstat(3, {st_mode=S_IFREG|0644, st_size=87775, ...}) = 0\nmmap(NULL, 87775, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2143f59000\nclose(3) = 0\n\nopenat(AT_FDCWD, \"/lib/x86_64-linux-gnu/libc.so.6\", O_RDONLY|O_CLOEXEC) = 3\nread(3, \"\\177ELF\\2\\(...)\"..., 832) = 832\npread64(3, \"\\6\\0\\0\\0\\4\\0\\0\\0@\\0(...)\"..., 784, 64) = 784\nfstat(3, {st_mode=S_IFREG|0755, st_size=2125328, ...}) = 0\npread64(3, \"\\6\\0\\0\\0\\4\\0\\0\\0@(...)\"..., 784, 64) = 784\nmmap(NULL, 2170256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2143d47000\nmmap(0x7f2143d6f000, ...)\nmmap(0x7f2143ef7000, ...)\nmmap(0x7f2143f46000, ...)\nmmap(0x7f2143f4c000, ...)\nclose(3)\n

    Analysons en d\u00e9tail les premi\u00e8res lignes de la sortie de strace:

    1. execve d\u00e9marre l'ex\u00e9cution du programme ./a.out. C'est le tout premier appel syst\u00e8me qui est effectu\u00e9.
    2. brk ajuste la taille du heap du programme. Appel\u00e9 avec NULL, le noyau retourne la limite actuelle du heap.
    3. mmap alloue de la m\u00e9moire, ici 8 kio pour des besoins internes.
    4. access(\"/etc/ld.so.preload\") permet d'injecter des biblioth\u00e8ques avant le chargement des biblioth\u00e8ques standard. Ici, le fichier n'existe pas. C'est notament utilis\u00e9 pour les outils de profiling. On observe l'erreur ENOENT qui signifie que le fichier n'existe pas.
    5. openat ouvre le fichier /etc/ld.so.cache qui contient les chemins des biblioth\u00e8ques partag\u00e9es pr\u00e9compil\u00e9es. Le syst\u00e8me y acc\u00e8de pour localiser rapidement les biblioth\u00e8ques dynamiques n\u00e9cessaires plut\u00f4t que de les charger depuis le disque
    6. mmap alloue de l'espace m\u00e9moire pour le fichier /etc/ld.so.cache.
    7. close ferme le fichier /etc/ld.so.cache une fois charg\u00e9.
    8. openat permet de charger la biblioth\u00e8que standard C libc.so.6 qui est situ\u00e9e dans /lib/x86_64-linux-gnu/.
    9. Les autres mmap permettent de charger la biblioth\u00e8que standard C en m\u00e9moire afin de pouvoir \u00eatre utilis\u00e9es par le programme.

    On observe donc des \u00e9l\u00e9ments importants de l'ex\u00e9cution d'un programme. Les biblioth\u00e8ques dynamiques sont charg\u00e9es en m\u00e9moire au d\u00e9but de l'ex\u00e9cution du programme. Si ces biblioth\u00e8ques sont absentes du syst\u00e8me, il ne pourra pas \u00eatre ex\u00e9cut\u00e9.

    ", "tags": ["brk", "heap", "execve", "NULL", "ENOENT", "strace", "mmap", "close", "openat", "libc.so.6", "main"]}, {"location": "tools/analysis/analysis/#suivi-des-threads", "title": "Suivi des threads", "text": "

    Lors d'un programme concurrent, il est possible de suivre la cr\u00e9ation et la terminaison des threads avec strace, notament au moyen des appels syst\u00e8mes fork, clone ou exec. Les appels \u00e0 futex permettent de voir les op\u00e9rations de synchronisation entre les threads utilis\u00e9s par les mutex et les variables de condition.

    ", "tags": ["exec", "futex", "fork", "clone", "strace"]}, {"location": "tools/analysis/analysis/#fonctionnement-interne", "title": "Fonctionnement interne", "text": "

    Le fonctionnement de cet outil repose sur ptrace qui est une fonction du noyau Linux qui permet de suivre l'ex\u00e9cution d'un processus. strace utilise cette fonction pour intercepter les appels syst\u00e8mes et les signaux. ptrace est notament utilis\u00e9 par les d\u00e9bogueurs comme gdb. \u00c0 chaque fois qu'un processus enfant execute un appel syst\u00e8me, le processus traceur est notifi\u00e9 et peut inspecter les registres du processus enfant.

    Pour mieux comprendre comment fonctionn strace, voici un exemple simple d'un programme C qui utilise ptrace pour tracer l'appel syst\u00e8me write notament utilis\u00e9 par printf.

    #include <errno.h>\n#include <stdio.h>\n#include <sys/ptrace.h>\n#include <sys/syscall.h>\n#include <sys/types.h>\n#include <sys/user.h>\n#include <sys/wait.h>\n#include <unistd.h>\n\nint main() {\n   pid_t child;\n   child = fork();\n\n   if (child == 0) {  // Child (executed under trace)\n      // Allow parent to trace this process\n      ptrace(PTRACE_TRACEME, 0, NULL, NULL);\n      raise(SIGSTOP);  // Stop child to let parent catch up\n      printf(\"Hello from the child process!\\n\");\n\n   } else {  // Parent\n      int status;\n      waitpid(child, &status, 0);\n\n      ptrace(PTRACE_SYSCALL, child, NULL, NULL);\n\n      while (1) {\n         struct user_regs_struct regs;\n\n         waitpid(child, &status, 0);\n         if (WIFEXITED(status)) break;\n\n         ptrace(PTRACE_GETREGS, child, NULL, &regs);\n\n         if (regs.orig_rax == SYS_write) {\n            printf(\"Intercepted write syscall!\\n\");\n            printf(\"File descriptor: %lld\\n\", regs.rdi);\n            printf(\"Buffer address: %lld\\n\", regs.rsi);\n            printf(\"Buffer size: %lld\\n\", regs.rdx);\n         }\n         ptrace(PTRACE_SYSCALL, child, NULL, NULL);  // Let child continue\n         waitpid(child, &status, 0);                 // Wait for syscall exit\n         if (WIFEXITED(status)) break;               // Child exited?\n\n         ptrace(PTRACE_SYSCALL, child, NULL, NULL);\n      }\n   }\n}\n

    La sortie de ce programme est la suivante\u2009:

    Intercepted write syscall!\nFile descriptor: 1\nBuffer address: 93866916508320\nBuffer size: 30\nHello from the child process!\n

    Ce programme utilise deux processus l\u00e9gers (threads), l'enfant simule le processus trac\u00e9 et le parent est le traceur. Au moment du fork le programme est dupliqu\u00e9 et s'ex\u00e9cute en parall\u00e8le. Pour distinguer le parent de l'enfant, on utilise la valeur de retour de fork. Si la valeur est 0, c'est que le processus est l'enfant, sinon c'est le parent. Il y a donc deux chemins possibles dans le programme.

    L'enfant autorise le parent \u00e0 le tracer avec PTRACE_TRACEME et se met en pause avec raise(SIGSTOP);. Le parent attend que l'enfant soit en pause avec waitpid(child, &status, 0); et commence \u00e0 tracer les appels syst\u00e8mes avec PTRACE_SYSCALL, child. Cette instruction demande au noyau de laisser l'enfant continuer son ex\u00e9cution, mais avec une interception \u00e0 chaque entr\u00e9e et sortie d'un appel syst\u00e8me. Cela signifie que chaque fois que l'enfant effectue un appel syst\u00e8me, il sera suspendu, et le parent sera notifi\u00e9. Le point de sortie de la boucle while est le test de WIFEXITED qui v\u00e9rifie si le processus enfant s'est termin\u00e9, dans ce cas, le parent sort de la boucle. L'appel PTRACE_GETREGS permet au parent de lire les registres du processus enfant. Ces registres contiennent des informations cruciales comme le num\u00e9ro de l'appel syst\u00e8me dans le registre orig_rax pour les syst\u00e8mes x86_64. Si l'appel syst\u00e8me intercept\u00e9 est SYS_write, le parent affiche les informations sur l'appel syst\u00e8me. Enfin, le parent laisse l'enfant continuer son ex\u00e9cution avec PTRACE_SYSCALL et attend le suivant.

    ", "tags": ["SYS_write", "fork", "PTRACE_TRACEME", "write", "ptrace", "PTRACE_SYSCALL", "printf", "WIFEXITED", "PTRACE_GETREGS", "orig_rax", "strace", "gdb", "x86_64"]}, {"location": "tools/analysis/analysis/#ltrace", "title": "ltrace", "text": "

    ltrace (pour library trace) est un outil de diagnostique qui permet de suivre les appels aux fonctions des biblioth\u00e8ques partag\u00e9es. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution. ltrace est un outil plus simple que strace car il ne suit que les appels de fonctions et non les appels syst\u00e8mes. Prenons l'exemple du programme suivant\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\nint main() {\n    char *buffer = (char *)malloc(100);\n    printf(\"Hello, World!\\n\");\n    free(buffer);\n}\n

    L'appel de ltrace sur ce programme donne la sortie suivante\u2009:

    ltrace ./a.out\nmalloc(100)  = 0x55af7c6d42a0\nputs(\"Hello, World!\"Hello, World!)  = 14\nfree(0x55af7c6d42a0) = <void>\n+++ exited (status 0) +++\n

    On peut voir notament que le compilateur \u00e0 remplac\u00e9 printf par puts car aucun format n'est utilis\u00e9.

    ", "tags": ["puts", "strace", "ltrace", "printf"]}, {"location": "tools/analysis/analysis/#perf", "title": "perf", "text": "

    perf est un outil tr\u00e8s puissant de diagnostique qui permet de suivre les performances d'un programme en analysant les compteurs mat\u00e9riels du processeur.

    Le processeur est un organe tr\u00e8s complexe qui contient de nombreux compteurs mat\u00e9riels qui permettent de mesurer des m\u00e9triques comme le nombre de cycles d'horloge, l'utilisation de la m\u00e9moire cache, le predicteur d'embranchement, les changements de contexte, etc. Ces compteurs sont tr\u00e8s utiles pour comprendre le comportement d'un programme et identifier les goulots d'\u00e9tranglement.

    Les PMU (Performance Monitoring Units) sont des composants mat\u00e9riels qui permettent de mesurer ces m\u00e9triques. Les PMU sont sp\u00e9cifiques \u00e0 chaque processeur et sont g\u00e9n\u00e9ralement accessibles via des instructions sp\u00e9cifiques. Ils d\u00e9pendent grandement du processeur utilis\u00e9. Par exemple, un processeur Intel Core i7 de 10e g\u00e9n\u00e9ration ne dispose pas des m\u00eames compteurs qu'un processeur AMD Ryzen 9. L'installation de perf n'est donc pas triviale, surtout sur des syst\u00e8mes hypervis\u00e9s ou virtualis\u00e9s.

    Par exemple sur WSL2, l'installation de perf n\u00e9cessite de compiler un noyau Linux avec les options de d\u00e9bogage activ\u00e9es. Sur des syst\u00e8mes comme macOS, perf n'est pas disponible car le noyau XNU ne fournit pas les m\u00eames fonctionnalit\u00e9s que le noyau Linux.

    ", "tags": ["perf"]}, {"location": "tools/analysis/analysis/#utilisation", "title": "Utilisation", "text": "

    Une utilisation typique de perf est d'analyser les pertes de performances li\u00e9 \u00e0 la m\u00e9moire cache. Prenons l'exemple d'une matrice 1000x1000 ou chaque \u00e9l\u00e9ment est multipli\u00e9 par 2. Le programme suivant effectue cette op\u00e9ration\u2009:

    #include <stdio.h>\n#include <stdlib.h>\n\n#define SIZE 10000\n\nint main() {\n   // Allocate\n   int **matrix = (int **)malloc(SIZE * sizeof(int *));\n   for (int i = 0; i < SIZE; i++) matrix[i] = (int *)malloc(SIZE * sizeof(int));\n\n   // Init\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[i][j] = i + j;\n\n         // Multiply\n#if DIRECTION == 0\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[i][j] *= 2;\n#else\n   for (int i = 0; i < SIZE; i++)\n      for (int j = 0; j < SIZE; j++) matrix[j][i] *= 2;\n#endif\n\n   // Cleanup\n   for (int i = 0; i < SIZE; i++) free(matrix[i]);\n   free(matrix);\n}\n

    En compilant chaque programme avec la variable DIRECTION \u00e9gale \u00e0 0 ou 1, on peut observer les diff\u00e9rences de performances. En effet, la m\u00e9moire est stock\u00e9e de mani\u00e8re lin\u00e9aire en m\u00e9moire, et en parcourant la matrice ligne par ligne ou colonne par colonne, on peut observer des diff\u00e9rences de performances.

    $ gcc -DDIRECTION=0 x.c && perf stat -e cache-misses,cycles,instructions ./a.out\n\n       11048991      cache-misses:u\n     1502929728      cycles:u\n     4304906724      instructions:u\n\n    0.384328204 seconds time elapsed\n\n    0.314080000 seconds user\n    0.070921000 seconds sys\n\n$ gcc -DDIRECTION=1 x.c && perf stat -e cache-misses,cycles,instructions ./a.out\n\n       23466037      cache-misses:u\n     4320403836      cycles:u\n     4304906756      instructions:u\n\n    0.966821928 seconds time elapsed\n\n    0.886317000 seconds user\n    0.080574000 seconds sys\n

    Dans un cas, le compteur cache-misses est bien plus \u00e9lev\u00e9 que dans l'autre. Ce la arrive lors de probl\u00e8mes de localit\u00e9s spatiales ou temporelles. Dans le premier cas, les donn\u00e9es sont charg\u00e9es dans le cache et restent en cache, alors que dans le second cas, le cache ne peut pas \u00eatre utilis\u00e9 efficacement. Le programme est 3x plus lent dans le second cas, pour une simple inversion dans la boucle.

    ", "tags": ["DIRECTION"]}, {"location": "tools/analysis/analysis/#wsl2", "title": "WSL2", "text": "

    Malheureusement perf ne fonctionne pas nativement sous WSL2. WSL2 ex\u00e9cute Linux dans une machine virtuelle l\u00e9g\u00e8re (Hyper-V), mais n'a pas d'acc\u00e8s direct aux compteurs mat\u00e9riels, essentiels pour perf. Cela signifie que les \u00e9v\u00e9nements li\u00e9s aux performances mat\u00e9rielles, tels que les cycles d'horloge du processeur, les instructions, et les caches, ne sont pas accessibles.

    La premi\u00e8re \u00e9tape est d'installer les paquets n\u00e9cessaires\u2009:

    sudo apt install build-essential flex bison libssl-dev \\\nlibelf-dev libpfm4-dev libtraceevent-dev asciidoc xmlto\n

    On clone ensuite le noyau Linux de WSL2 en n'oubliant pas le --depth=1 pour ne r\u00e9cup\u00e9rer que le dernier commit.

    git clone --depth=1 https://github.com/microsoft/WSL2-Linux-Kernel.git\n

    L'outil perf est inclus dans le noyau, et l'objectif est de le compiler. Pour ce faire, on se place dans le r\u00e9pertoire du noyau et on copie la configuration du noyau actuel.

    cd WSL2-Linux-Kernel/tools/perf\nmake\nsudo make prefix=/usr install\n

    Vous pouvez d\u00e9sormais utiliser perf pour diagnostiquer les performances de vos programmes sous WSL2. Il est possible de v\u00e9rifier les \u00e9v\u00e9nements disponibles avec perf list. Il se peut que sur votre syst\u00e8me, certains \u00e9v\u00e9nements ne soient pas disponibles.

    $ perf list\n  branch-instructions OR branches      [Hardware event]\n  branch-misses                        [Hardware event]\n  bus-cycles                           [Hardware event]\n  cache-misses                         [Hardware event]\n  cache-references                     [Hardware event]\n  cpu-cycles OR cycles                 [Hardware event]\n  instructions                         [Hardware event]\n  ref-cycles                           [Hardware event]\n  alignment-faults                     [Software event]\n  bpf-output                           [Software event]\n  cgroup-switches                      [Software event]\n  context-switches OR cs               [Software event]\n  cpu-clock                            [Software event]\n  cpu-migrations OR migrations         [Software event]\n  dummy                                [Software event]\n  emulation-faults                     [Software event]\n  major-faults                         [Software event]\n  minor-faults                         [Software event]\n  page-faults OR faults                [Software event]\n  task-clock                           [Software event]\n  duration_time                        [Tool event]\n  user_time                            [Tool event]\n  system_time                          [Tool event]\n
    ", "tags": ["perf"]}, {"location": "tools/analysis/analysis/#valgrind", "title": "valgrind", "text": "

    valgrind est un outil de diagnostique qui permet de suivre les erreurs de m\u00e9moire dans un programme. Il est tr\u00e8s utile pour comprendre le comportement d'un programme en cours d'ex\u00e9cution. valgrind est un outil plus simple que strace car il ne suit que les erreurs de m\u00e9moire et non les appels syst\u00e8mes.

    L'outil est disponible sur Ubuntu et d\u00e9riv\u00e9s avec la commande suivante\u2009:

    sudo apt install valgrind\n

    L'utilisation typique est de lancer un programme avec valgrind pour d\u00e9tecter les erreurs de m\u00e9moire. Par exemple, pour un programme a.out:

    valgrind ./a.out\n

    Voici une liste des fonctionnalit\u00e9s typiques de valgrind:

    • D\u00e9tection des fuites de m\u00e9moire : Valgrind permet de trouver les allocations de m\u00e9moire qui ne sont pas lib\u00e9r\u00e9es, aidant \u00e0 identifier les fuites de m\u00e9moire dans les programmes.

    • D\u00e9tection des erreurs de m\u00e9moire : Il d\u00e9tecte les acc\u00e8s ill\u00e9gaux \u00e0 la m\u00e9moire, tels que l'acc\u00e8s \u00e0 des zones m\u00e9moire non allou\u00e9es, l'utilisation de m\u00e9moire apr\u00e8s qu'elle a \u00e9t\u00e9 lib\u00e9r\u00e9e (use-after-free), ou encore les buffer overflows.

    • D\u00e9tection des erreurs d'initialisation de m\u00e9moire : Valgrind rep\u00e8re l'utilisation de variables non initialis\u00e9es en m\u00e9moire, ce qui peut provoquer des comportements impr\u00e9visibles.

    • Profilage de la gestion de la m\u00e9moire : Il vous montre comment la m\u00e9moire est allou\u00e9e et lib\u00e9r\u00e9e pendant l'ex\u00e9cution, permettant d'optimiser l'utilisation de la m\u00e9moire et de d\u00e9tecter les probl\u00e8mes de fragmentation.

    • D\u00e9bogage multithread : Valgrind aide \u00e0 d\u00e9tecter les erreurs de synchronisation dans les programmes multithread, comme les data races (acc\u00e8s concurrents non prot\u00e9g\u00e9s \u00e0 des variables partag\u00e9es) ou les verrous non lib\u00e9r\u00e9s.

    • V\u00e9rification des appels syst\u00e8me incorrects : Valgrind d\u00e9tecte les mauvais usages des appels syst\u00e8mes, comme la fermeture de descripteurs de fichiers qui ne sont pas ouverts.

    • Analyse des performances : Avec l'outil Callgrind (inclus dans Valgrind), il permet de profiler les programmes en comptant les instructions ex\u00e9cut\u00e9es et en g\u00e9n\u00e9rant des informations sur l'utilisation du CPU et des fonctions, utile pour l'optimisation des performances.

    • Simulation de cache CPU : Avec Cachegrind, Valgrind peut simuler le comportement des caches CPU et montrer le nombre de cache misses (d\u00e9fauts de cache) pour aider \u00e0 am\u00e9liorer l'efficacit\u00e9 des programmes en mati\u00e8re de gestion de cache.

    • D\u00e9tection des erreurs de pile : Il v\u00e9rifie l'utilisation correcte de la pile (stack), d\u00e9tectant les d\u00e9bordements et erreurs de gestion dans la pile d'appels.

    ", "tags": ["valgrind", "a.out", "strace"]}, {"location": "tools/analysis/binutils/", "title": "Binutils", "text": "

    Les outils binaires (binutils) sont une collection de programmes install\u00e9s avec un compilateur et permettant d'aider au d\u00e9veloppement et au d\u00e9bogage. Certains de ces outils sont tr\u00e8s pratiques, mais nombreux sont les d\u00e9veloppeurs qui ne les connaissent pas.

    nm

    Liste tous les symboles dans un fichier objet (binaire). Ce programme appliqu\u00e9 sur le programme hello world de l'introduction donne\u2009:

    $ nm a.out\n0000000000200dc8 d _DYNAMIC\n0000000000200fb8 d _GLOBAL_OFFSET_TABLE_\n00000000000006f0 R _IO_stdin_used\n                w _ITM_deregisterTMCloneTable\n                w _ITM_registerTMCloneTable\n\n...\n\n                U __libc_start_main@@GLIBC_2.2.5\n0000000000201010 D _edata\n0000000000201018 B _end\n00000000000006e4 T _fini\n00000000000004f0 T _init\n0000000000000540 T _start\n\n...\n\n000000000000064a T main\n                 U printf@@GLIBC_2.2.5\n00000000000005b0 t register_tm_clones\n

    On observe notamment que la fonction printf est en provenance de la biblioth\u00e8que GLIBC 2.2.5, et qu'il y a une fonction main.

    strings

    Liste toutes les cha\u00eenes de caract\u00e8res imprimables dans un fichier binaire. On observe tous les symboles de d\u00e9bogue qui sont par d\u00e9faut int\u00e9gr\u00e9s au fichier ex\u00e9cutable. On lit \u00e9galement la cha\u00eene de caract\u00e8re hello, world. Attention donc \u00e0 ne pas laisser les \u00e9ventuels mots de passes ou num\u00e9ro de licence en clair dans un fichier binaire.

    $ strings a.out\n/lib64/ld-linux-x86-64.so.2\nlibc.so.6\nprintf\n\n...\n\nAUATL\n[]A\\A]A^A_\nhello, world\n;*3$\"\nGCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0\n\n...\n\n_IO_stdin_used\n__libc_csu_init\n__bss_start\nmain\n__TMC_END__\n_ITM_registerTMCloneTable\n__cxa_finalize@@GLIBC_2.2.5\n.symtab\n.strtab\n\n...\n\n.data\n.bss\n.comment\n
    size

    Lister la taille des segments m\u00e9moires utilis\u00e9s. Ici le programme repr\u00e9sente 1517 bytes, les donn\u00e9es initialis\u00e9es 8 bytes, les donn\u00e9es variables 600 bytes, soit une somme d\u00e9cimale de 2125 bytes ou 84d bytes.

    $ size a.out\ntext    data     bss     dec     hex filename\n1517     600       8    2125     84d a.out\n
    ", "tags": ["size", "strings", "main", "printf"]}, {"location": "tools/arch/filesystem/", "title": "Syst\u00e8me de fichier", "text": "

    Fourmi portant des donn\u00e9es

    Un syst\u00e8me de fichier est une structure de donn\u00e9es qui permet de stocker des fichiers et des r\u00e9pertoires sur un support de stockage. Les syst\u00e8mes de fichiers sont utilis\u00e9s pour organiser les donn\u00e9es sur les disques durs, les cl\u00e9s USB, les cartes m\u00e9moire, etc.

    Nous savons maintenant que les donn\u00e9es informatiques sont stock\u00e9es sous forme binaire. Sur une cl\u00e9 USB ou un disque dur, c'est pareil. On aura des 0 et des 1 stock\u00e9s \u00e0 perte de vue sur le silicium. Si votre disque dur de 1 Tio est la surface de la terre, sans les oc\u00e9ans et d'environ \\(148.9\\) millions de \\(\\text{km}^2\\). Notre disque contient \\(2^{40}\\) octets, soit \\(2^{43}\\) bits\u2009:

    \\[ N_{bits} = 8'796'093'022'208 \\]

    Cela revient \u00e0 stocker environ 16 bits par millim\u00e8tre carr\u00e9. Une fourmi aurait sur son dos 2 \u00e0 3 octets de donn\u00e9es. Imaginez vous un instant prendre l'avion, le train, rev\u00eatir votre tenue d'explorateur pour chercher les fourmis qui portent les donn\u00e9es que vous cherchez\u2009?

    Oui ce n'est pas tr\u00e8s r\u00e9aliste, et le calcul que nous avons fourni est tr\u00e8s simplifi\u00e9. Mais cela permet de comprendre que les donn\u00e9es stock\u00e9es doivent \u00eatre organis\u00e9es car elles ne sont pas accessibles imm\u00e9diatement. Pour un disque dur, la t\u00eate de lecture doit parcourir le disque pour atteindre les donn\u00e9es. Sur une cl\u00e9 USB, la m\u00e9moire doit \u00eatre configur\u00e9e pour acc\u00e9der une r\u00e9gion pr\u00e9cise.

    Nous avons donc besoin d'une carte pour savoir o\u00f9 sont stock\u00e9es les donn\u00e9es. C'est le r\u00f4le du syst\u00e8me de fichier. Il permet de stocker les fichiers, les organiser en dossiers. N'importe qui n'aura pas acc\u00e8s \u00e0 toutes les donn\u00e9es, il faut les droits d'acc\u00e8s. Et les donn\u00e9es peuvent \u00eatre corrompues, d\u00e9truites par des rayonnements cosmiques. Il faut aussi g\u00e9rer les erreurs.

    Le premier syst\u00e8me de fichier a \u00e9t\u00e9 invent\u00e9 par IBM en 1956 pour le disque dur IBM 305 RAMAC. Il s'appelait le syst\u00e8me de fichier \u00e0 index. Il a \u00e9t\u00e9 invent\u00e9 par Hans Peter Luhn. Il permettait de stocker 5 Mo de donn\u00e9es sur un disque de 24 pouces. Depuis

    "}, {"location": "tools/arch/filesystem/#fat-et-les-autres", "title": "FAT et les autres", "text": "

    Le syst\u00e8me de fichier FAT (File Allocation Table) est un syst\u00e8me de fichier simple et robuste. Il a \u00e9t\u00e9 invent\u00e9 par Microsoft en 1977 pour le syst\u00e8me d'exploitation MS-DOS. Il est toujours utilis\u00e9 aujourd'hui pour les cl\u00e9s USB, les cartes m\u00e9moire, etc. C'est tr\u00e8s certainement celui-ci qu'un ing\u00e9nieur embarqu\u00e9 utiliserait sur une carte SD interfac\u00e9e avec un microcontr\u00f4leur.

    L'API de FAT, c'est \u00e0 dire les fonctions primitives pour contr\u00f4ler le syst\u00e8me de fichier contient par exemple les fonctions suivantes\u2009:

    • fopen : Ouvrir un fichier \u00e0 partir d'un chemin
    • fread : Lire de donn\u00e9es depuis un fichier ouvert
    • fseek : Se d\u00e9placer dans un fichier ouvert
    • fsize : Obtenir la taille d'un fichier
    • fopendir : Ouvrir un r\u00e9pertoire
    • frename : Renommer un fichier
    • fmkdir : Cr\u00e9er un r\u00e9pertoire
    • ...

    Les syst\u00e8mes de fichiers les plus utilis\u00e9s aujourd'hui sont\u2009:

    Quelques syst\u00e8mes de fichiers Syst\u00e8me de fichier Ann\u00e9e Utilisation FAT 1977 Cl\u00e9s USB, cartes m\u00e9moire FAT32 1996 Disques durs NTFS 1993 Windows ext4 2006 Linux APFS 2017 macOS Btrfs 2009 Linux ZFS 2005 Solaris, FreeBSD", "tags": ["fsize", "fopendir", "fopen", "fseek", "fread", "frename", "fmkdir"]}, {"location": "tools/arch/filesystem/#organisation", "title": "Organisation", "text": "

    Un syst\u00e8me de fichier comporte en g\u00e9n\u00e9ral\u2009:

    • Des r\u00e9pertoires (des dossiers) qui contiennent des fichiers ou d'autres r\u00e9pertoires.
    • Des fichiers qui contiennent des donn\u00e9es.

    Les r\u00e9pertoires sont organis\u00e9s en arborescence de mani\u00e8re hi\u00e9rarchique. Si vous avez dans votre maison, dans votre cuisine, dans votre frigo, au second rayonnage, sur la droite, une pomme. Vous pourriez \u00e9crire\u2009:

    maison -> cuisine -> frigo -> rayonnage2 -> droite -> pomme\n

    Mais la pomme est un fruit, comment le savoir avec seulement un nom. C'est peut-\u00eatre une pomme de terre, une pomme de pin ou une pomme de douche. G\u00e9n\u00e9ralement, on ajoute une extension, c'est un suffixe qui permet de savoir de quel type de fichier il s'agit. Par exemple, .txt pour un fichier texte, .jpg pour une image, .c pour un fichier source en langage C, etc.

    Enfin, utiliser -> pour indiquer la hi\u00e9rarchie n'est pas pratique. Tous les syst\u00e8mes de fichiers du monde utilisent le slash / pour s\u00e9parer les r\u00e9pertoires. Tous... sauf un qui r\u00e9siste encore et toujours \u00e0 l'envahisseur. Sur Windows, la convention est diff\u00e9rente, et c'est le backslash qui est utilis\u00e9 \\. C'est une source de confusion pour les d\u00e9veloppeurs qui doivent \u00e9crire des programmes compatibles avec les deux syst\u00e8mes d'exploitation.

    Reprenons. Notre pomme serait un fruit, une image de fruit. On indiquerait alors son chemin complet\u2009:

    /maison/cuisine/frigo/rayonnage2/droite/pomme.jpg\n

    Tient\u2009? Pourquoi un slash au d\u00e9but\u2009? C'est pour indiquer le r\u00e9pertoire racine\u2009: le r\u00e9pertoire d'origine, le point de d\u00e9part de l'arborescence. Evidemment si vous vous trouvez d\u00e9j\u00e0 dans la cuisine, vous n'avez pas besoin de pr\u00e9ciser /maison/cuisine. Vous pouvez simplement \u00e9crire\u2009: frigo/rayonnage2/droite/pomme.jpg. Donc un chemin peut, ou non avoir un slash au d\u00e9but. On dit qu'il est absolu ou relatif.

    Windows

    Sur Windows, le r\u00e9pertoire racine est C:\\ pour le disque dur principal. Donc un chemin absolu sur Windows ressemblerait \u00e0\u2009:

    C:\\maison\\cuisine\\frigo\\rayonnage2\\droite\\pomme.jpg\n

    L'usage de C est historique. \u00c0 l'origine il n'y avait pas de disques durs mais des disquettes. Ceux qui avaient la chance d'avoir un lecteur de disquette avaient un unique lecteur A. Ceux qui voulaient faire des copies de disquettes devaient en avoir un deuxi\u00e8me, c'\u00e9tait le lecteur B. En g\u00e9n\u00e9ral, le disque dur \u00e9tait le troisi\u00e8me lecteur, le lecteur C. Et depuis Windows 95, c'est rest\u00e9 comme \u00e7a.

    ", "tags": ["slash"]}, {"location": "tools/arch/filesystem/#navigation", "title": "Navigation", "text": "

    Nous savons qu'un chemin est une suite de r\u00e9pertoires s\u00e9par\u00e9s par des slashes mais ce que nous ne savons pas c'est o\u00f9 on se trouve. Lorsque vous ouvrez un terminal, ou une application vous \u00eates dans un r\u00e9pertoire, c'est le r\u00e9pertoire courant, ou le r\u00e9pertoire de travail (working directory). Pour savoir o\u00f9 vous trouvez vous pouvez ouvrir un terminal et taper\u2009:

    POSIXWindows
    $ pwd\n/home/username\n
    > cd\nC:\\Users\\username\n

    Mais si je suis dans la maison et que je veux aller dans le jardin\u2009? Je ne vais pas indiquer le chemin complet\u2009:

    /universe/\n    galaxy/\n        solar-system/\n            earth/\n                europe/\n                    switzerland/\n                        vaud/\n                            yverdon-les-bains/\n                                maison/\n                                    jardin\n

    Je ne peux pas non plus utiliser le chemin relatif jardin car le jardin n'est pas dans la maison. Ni d'ailleurs /jardin car celui-ci n'est pas \u00e0 la racine de l'univers. On voit qu'il est essentiel d'introduire une notion de parent\u00e9\u2009; ce que je souhaite faire c'est remonter d'un niveau pour aller dans le jardin.

    Il existe un fichier sp\u00e9cial qui permet de remonter d'un niveau c'est le fichier ...

    Et si je suis dans le jardin et que j'aimerais tondre la pelouse. J'aimerais appeler le programme tondre en lui donnant le chemin jusque l\u00e0 o\u00f9 je me trouve. Je pourrais \u00e9crire\u2009: ../jardin mais c'est redondant, et cela implique de conna\u00eetre le nom de l\u00e0 ou je me trouve. Il existe pour cela un deuxi\u00e8me fichier sp\u00e9cial qui permet de d\u00e9signer le r\u00e9pertoire courant, c'est le fichier ..

    Enfin, si je souhaite me d\u00e9placer dans l'arborescence, je peux utiliser la commande cd pour change directory. Ce programme prend en argument un chemin absolu ou relatif.

    ", "tags": ["jardin", "tondre"]}, {"location": "tools/arch/filesystem/#commandes-utiles", "title": "Commandes utiles", "text": "POSIXWindows CMDWindows PowerShell Commande Description Exemple pwd Affiche le r\u00e9pertoire courant /home/username cd Change de r\u00e9pertoire cd /home/username ls Liste les fichiers et r\u00e9pertoires ls mkdir Cr\u00e9e un r\u00e9pertoire mkdir -p /home/username rmdir Supprime un r\u00e9pertoire rmdir /home/username touch Cr\u00e9e un fichier vide touch /home/username/file.txt rm Supprime un fichier rm /home/username/file.txt mv D\u00e9place un fichier mv /home/username/file.txt /home/username/backup/ cp Copie un fichier cp /home/username/file.txt /home/username/backup/ Commande Description Exemple cd Affiche le r\u00e9pertoire courant cd cd Change de r\u00e9pertoire cd C:\\Users\\username dir Liste les fichiers et r\u00e9pertoires dir mkdir Cr\u00e9e un r\u00e9pertoire mkdir C:\\Users\\username\\backup rmdir Supprime un r\u00e9pertoire rmdir C:\\Users\\username\\backup echo Cr\u00e9e un fichier vide echo. > C:\\Users\\username\\file.txt del Supprime un fichier del C:\\Users\\username\\file.txt move D\u00e9place un fichier move C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ copy Copie un fichier copy C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ Commande Description Exemple pwd Affiche le r\u00e9pertoire courant pwd cd Change de r\u00e9pertoire cd C:\\Users\\username ls Liste les fichiers et r\u00e9pertoires ls mkdir Cr\u00e9e un r\u00e9pertoire mkdir C:\\Users\\username\\backup rmdir Supprime un r\u00e9pertoire rmdir C:\\Users\\username\\backup echo Cr\u00e9e un fichier vide echo. > C:\\Users\\username\\file.txt rm Supprime un fichier rm C:\\Users\\username\\file.txt mv D\u00e9place un fichier mv C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\ cp Copie un fichier cp C:\\Users\\username\\file.txt C:\\Users\\username\\backup\\

    Windows

    On voit que Microsoft a appris de ses erreurs et a introduit pwd et ls dans PowerShell. C'est une bonne chose car ces commandes sont tr\u00e8s utiles. PowerShell est un shell plus moderne que CMD et plus puissant. Il est bas\u00e9 sur le framework .NET et permet d'interagir avec des objets .NET.

    Avertissement

    Les commandes rm, mv, cp sont tr\u00e8s dangereuses. Elles peuvent d\u00e9truire des donn\u00e9es. Il est important de faire attention \u00e0 ce que vous faites. Il est recommand\u00e9 de faire des sauvegardes r\u00e9guli\u00e8res de vos donn\u00e9es.

    ", "tags": ["pwd", "copy", "touch", "rmdir", "echo", "del", "mkdir", "dir", "move"]}, {"location": "tools/arch/filesystem/#permissions", "title": "Permissions", "text": "

    Les syst\u00e8mes de fichiers modernes permettent de d\u00e9finir des permissions sur les fichiers et les r\u00e9pertoires. Ces permissions permettent de contr\u00f4ler qui peut lire, \u00e9crire ou ex\u00e9cuter un fichier. Les permissions sont d\u00e9finies pour trois cat\u00e9gories d'utilisateurs\u2009:

    • Le propri\u00e9taire du fichier
    • Le groupe auquel appartient le fichier
    • Les autres utilisateurs

    Les permissions sont d\u00e9finies pour trois actions\u2009:

    • Lire le fichier
    • \u00c9crire dans le fichier
    • Ex\u00e9cuter le fichier

    Les permissions sont repr\u00e9sent\u00e9es par des lettres\u2009:

    • r pour lire
    • w pour \u00e9crire
    • x pour ex\u00e9cuter

    Les permissions sont affich\u00e9es par la commande ls -l dans POSIX. Sous Windows ce n'est pas aussi simple. Les permissions sont affich\u00e9es par la commande icacls mais le format est diff\u00e9rent.

    NTFS

    Le syst\u00e8me de fichier NTFS (New Technology File System) de Microsoft permet de d\u00e9finir des permissions tr\u00e8s fines sur les fichiers et les r\u00e9pertoires. Il permet de d\u00e9finir des permissions pour des utilisateurs individuels ou des groupes d'utilisateurs. Il permet aussi de chiffrer les donn\u00e9es pour les prot\u00e9ger contre les acc\u00e8s non autoris\u00e9s.

    Les commandes sont donc beaucoup plus complexes et ne se r\u00e9duisent pas aux permissions que l'on explique ici.

    Cette granularit\u00e9 est tr\u00e8s utile dans un environnement professionnel o\u00f9 les donn\u00e9es sont sensibles mais cela peut \u00eatre un cauchemar pour les administrateurs syst\u00e8me et surtout cela rend le syst\u00e8me plus lent car il doit v\u00e9rifier toutes les permissions \u00e0 chaque acc\u00e8s disque.

    Pour changer une permission, on utilise la commande chmod dans POSIX. Sous Windows il est pr\u00e9f\u00e9rable de passer par l'interface graphique.

    Commandes utiles pour les permissions Description Exemple Ajouter l'ex\u00e9cution sur un fichier $ chmod +x program Retirer l'ex\u00e9cution sur un fichier $ chmod -x program Ajouter l'\u00e9criture pour le groupe $ chmod g+w file Retirer l'\u00e9criture pour les autres $ chmod o-w file Ajouter la lecture pour tous $ chmod a+r file Retirer tous les droits pour les autres $ chmod o-rwx file Changer les permissions pour tous $ chmod 777 file

    Repr\u00e9sentation octale

    Dans un environnement POSIX, on peut repr\u00e9senter les permissions avec des chiffres. Chaque permission est repr\u00e9sent\u00e9e par un bit\u2009:

    • r : 4
    • w : 2
    • x : 1

    On additionne les bits pour obtenir la permission\u2009:

    • rwx : 7
    • rw- : 6
    • r-x : 5

    Chaque chiffre repr\u00e9sente dans l'ordre le propri\u00e9taire, le groupe et les autres. Ainsi la permission du fichier ~/.ssh/id_rsa est 400 ce qui signifie que le propri\u00e9taire a le droit de lire le fichier mais ni d'\u00e9crire ni d'ex\u00e9cuter. Le groupe et les autres n'ont aucun droit. C'est normal c'est la cl\u00e9 priv\u00e9e SSH, elle ne doit \u00eatre lue que par le propri\u00e9taire.

    ", "tags": ["chmod", "rwx", "icacls"]}, {"location": "tools/arch/filesystem/#proprietaire-et-groupe", "title": "Propri\u00e9taire et groupe", "text": "

    Chaque fichier a un propri\u00e9taire et un groupe. Le propri\u00e9taire est l'utilisateur qui a cr\u00e9\u00e9 le fichier. Le groupe est le groupe auquel appartient le fichier. Les utilisateurs peuvent appartenir \u00e0 plusieurs groupes. Les groupes permettent de d\u00e9finir des permissions pour plusieurs utilisateurs. Par exemple, un groupe admin pourrait avoir des droits d'\u00e9criture sur un r\u00e9pertoire.

    Pour changer le propri\u00e9taire d'un fichier, on utilise la commande chown dans POSIX. Sous Windows, il est pr\u00e9f\u00e9rable de passer par l'interface graphique.

    Commandes utiles pour les propri\u00e9taires et les groupes Description Exemple Changer le propri\u00e9taire d'un fichier $ chown username file Changer le groupe d'un fichier $ chgrp group file Ajouter un utilisateur \u00e0 un groupe $ usermod -aG group username Retirer un utilisateur d'un groupe $ gpasswd -d username group Cr\u00e9er un groupe $ groupadd group Supprimer un groupe $ groupdel group Lister les groupes d'un utilisateur $ groups username", "tags": ["chown", "admin"]}, {"location": "tools/arch/filesystem/#acl-access-control-list", "title": "ACL (Access Control List)", "text": "

    Les syst\u00e8mes de fichiers modernes permettent de d\u00e9finir des ACL (Access Control List) sur les fichiers et les r\u00e9pertoires. Les ACL permettent de d\u00e9finir des permissions plus fines que les permissions POSIX. Les ACL permettent de d\u00e9finir des permissions pour des utilisateurs individuels ou des groupes d'utilisateurs. Les ACL sont plus complexes \u00e0 g\u00e9rer que les permissions POSIX mais elles permettent de d\u00e9finir des permissions plus pr\u00e9cises.

    En g\u00e9n\u00e9ral, les ACL sont g\u00e9r\u00e9es par des outils graphiques ou des commandes sp\u00e9cifiques. Les ACL sont stock\u00e9es dans les m\u00e9tadonn\u00e9es des fichiers et des r\u00e9pertoires. Les ACL sont utilis\u00e9es dans les environnements professionnels o\u00f9 les donn\u00e9es sont sensibles et o\u00f9 il est n\u00e9cessaire de d\u00e9finir des permissions tr\u00e8s pr\u00e9cises. Cela permet de se rapprocher de syst\u00e8mes de fichiers comme NTFS de Microsoft.

    Sur votre ordinateur ou dans votre carri\u00e8re professionnelle vous n'aurez tr\u00e8s certainement jamais besoin de g\u00e9rer des ACL. C'est un sujet complexe et r\u00e9serv\u00e9 aux administrateurs syst\u00e8me. Mais pour les plus curieux voici quelques exemples\u2009:

    Description Exemple Afficher les ACL d'un fichier $ getfacl file Modifier les ACL d'un fichier $ setfacl -m u:username:rwx file Supprimer les ACL d'un fichier $ setfacl -b file Copier les ACL d'un fichier $ getfacl file1 | setfacl --set-file=- file2 Sauvegarder les ACL d'un fichier $ getfacl file > file.acl Restaurer les ACL d'un fichier $ setfacl --restore=file.acl"}, {"location": "tools/arch/filesystem/#manipulation-bas-niveau", "title": "Manipulation bas niveau", "text": "

    Une question que l'on peut se poser est s'il est possible d'acc\u00e9der aux donn\u00e9es brutes d'un disque dur (les 1 et les 0) : la r\u00e9ponse est oui. Il est possible d'acc\u00e9der aux donn\u00e9es brutes d'un disque dur en utilisant des outils sp\u00e9cifiques. Ces outils permettent de lire et d'\u00e9crire des donn\u00e9es directement sur le disque dur sans passer par le syst\u00e8me de fichiers.

    La commande dd dans POSIX permet de lire et d'\u00e9crire des donn\u00e9es brutes sur un disque dur. Cette commande est tr\u00e8s puissante et tr\u00e8s dangereuse. Elle permet de lire et d'\u00e9crire des donn\u00e9es directement sur le disque dur. Il est tr\u00e8s facile de d\u00e9truire des donn\u00e9es avec cette commande.

    On peut utiliser des fichiers sp\u00e9ciaux comme /dev/zero ou /dev/random pour g\u00e9n\u00e9rer des fichiers de donn\u00e9es.

    Par exemple cr\u00e9er un fichier de 1 Mio rempli de z\u00e9ros\u2009:

    $ dd if=/dev/zero of=file bs=1M count=1\n

    Ou cr\u00e9er un fichier de 100 bytes rempli de donn\u00e9es al\u00e9atoires\u2009:

    $ dd if=/dev/random of=file bs=100 count=1\n

    On peut aussi lire des donn\u00e9es brutes sur un disque dur. Par exemple lire les 100 premiers bytes d'un disque dur\u2009:

    $ dd if=/dev/sda of=file bs=100 count=1\n
    "}, {"location": "tools/arch/filesystem/#arborescence-posix", "title": "Arborescence POSIX", "text": "

    L'arborescence POSIX est une convention pour organiser les fichiers et les r\u00e9pertoires sur un syst\u00e8me de fichiers. L'arborescence POSIX est utilis\u00e9e par la plupart des syst\u00e8mes d'exploitation bas\u00e9s sur UNIX. L'arborescence POSIX est organis\u00e9e de la mani\u00e8re suivante\u2009:

    • / : Le r\u00e9pertoire racine
    • /bin : Les programmes de base
    • /boot : Les fichiers de d\u00e9marrage
    • /dev : Les fichiers de p\u00e9riph\u00e9riques
    • /etc : Les fichiers de configuration
    • /home : Les r\u00e9pertoires des utilisateurs
    • /lib : Les biblioth\u00e8ques partag\u00e9es
    • /media : Les points de montage des p\u00e9riph\u00e9riques amovibles
    • /mnt : Les points de montage des p\u00e9riph\u00e9riques
    • /opt : Les logiciels optionnels
    • /proc : Les informations sur les processus
    • /root : Le r\u00e9pertoire de l'administrateur
    • /tmp : Les fichiers temporaires
    • /usr : Les programmes et les fichiers partag\u00e9s
    • /var : Les fichiers variables
    • /srv : Les donn\u00e9es des services
    • /sys : Les informations sur le noyau

    Windows

    Malheureusement Windows n'a pas vraiment de convention rigide pour l'organisation des fichiers et des r\u00e9pertoires. Chaque programme peut d\u00e9cider o\u00f9 il veut stocker ses fichiers de configuration, ses fichiers de donn\u00e9es, etc. Cela rend la maintenance et la sauvegarde des donn\u00e9es plus difficile. On peut n\u00e9anmoins retrouver quelques r\u00e9pertoires communs\u2009:

    • C:\\Program Files : Les programmes install\u00e9s
    • C:\\Program Files (x86) : Les programmes 32 bits install\u00e9s sur un syst\u00e8me 64 bits
    • C:\\Users : Les r\u00e9pertoires des utilisateurs
    • C:\\Windows : Les fichiers du syst\u00e8me d'exploitation
    • C:\\Windows\\System32 : Les fichiers du syst\u00e8me d'exploitation 64 bits
    • C:\\Windows\\SysWOW64 : Les fichiers du syst\u00e8me d'exploitation 32 bits sur un syst\u00e8me 64 bits
    • C:\\Temp : Les fichiers temporaires
    • C:\\Users\\username\\AppData : Les fichiers de configuration des programmes
    • C:\\Users\\username\\Documents : Les documents de l'utilisateur
    • C:\\Users\\username\\Downloads : Les fichiers t\u00e9l\u00e9charg\u00e9s
    "}, {"location": "tools/build-system/cmake/", "title": "CMake", "text": ""}, {"location": "tools/build-system/cmake/#introduction", "title": "Introduction", "text": "

    CMake, cr\u00e9ature singuli\u00e8re dans l\u2019univers du d\u00e9veloppement logiciel, est un outil de compilation destin\u00e9 \u00e0 automatiser et simplifier la g\u00e9n\u00e9ration des fichiers de configuration n\u00e9cessaires \u00e0 la compilation d\u2019un projet. N\u00e9 \u00e0 la fin des ann\u00e9es 1990, \u00e0 l\u2019\u00e9poque o\u00f9 la prolif\u00e9ration des syst\u00e8mes d\u2019exploitation et des plateformes mat\u00e9rielles complexifiait le processus de construction des logiciels, CMake s\u2019inscrit dans une qu\u00eate pragmatique\u2009: rendre le d\u00e9veloppement multi-plateformes plus fluide et plus unifi\u00e9.

    \u00c0 une \u00e9poque o\u00f9 les syst\u00e8mes de build comme Make \u00e9taient omnipr\u00e9sents, ces outils \u00e9taient souvent tr\u00e8s sp\u00e9cifiques \u00e0 un syst\u00e8me particulier, et les d\u00e9veloppeurs devaient \u00e9crire des fichiers de configuration diff\u00e9rents pour chaque environnement. CMake a donc \u00e9t\u00e9 con\u00e7u par Kitware, avec pour objectif de g\u00e9n\u00e9rer des fichiers de build qui pourraient s\u2019adapter \u00e0 diff\u00e9rents compilateurs et plateformes sans avoir \u00e0 r\u00e9\u00e9crire manuellement de nombreuses configurations. Ce besoin est apparu principalement dans le cadre de projets de grande envergure, tels que les logiciels scientifiques ou industriels, qui devaient tourner aussi bien sur des syst\u00e8mes Unix que sur Windows ou encore macOS.

    D\u00e8s ses d\u00e9buts, CMake s\u2019est impos\u00e9 comme une solution de configuration multi-plateforme flexible. Son avantage par rapport \u00e0 ses concurrents r\u00e9side dans sa capacit\u00e9 \u00e0 g\u00e9n\u00e9rer des fichiers de construction adapt\u00e9s aux environnements vari\u00e9s, que ce soit pour Make, Ninja, ou encore des outils sp\u00e9cifiques \u00e0 Visual Studio. Cette abstraction permet de s\u2019affranchir de l'\u00e9criture manuelle de fichiers de build complexes pour chaque plateforme.

    Compar\u00e9 \u00e0 ses concurrents directs comme Autotools, CMake brille par sa relative simplicit\u00e9 \u00e0 prendre en main et \u00e0 configurer des projets multi-syst\u00e8mes, tout en restant assez puissant pour s\u2019adapter \u00e0 des projets de grande envergure. Contrairement \u00e0 des outils plus anciens, il se concentre sur l'abstraction du processus de construction et permet de g\u00e9rer plus facilement les d\u00e9pendances externes et internes d'un projet. En cela, CMake surpasse souvent les syst\u00e8mes de build traditionnels par sa flexibilit\u00e9.

    Cependant, s\u2019il poss\u00e8de un net avantage pour un projet complexe et multi-plateforme, il souffre aussi de certains travers. Et c\u2019est ici que les d\u00e9veloppeurs grimacent souvent.

    "}, {"location": "tools/build-system/cmake/#une-syntaxe-austere-et-rebutante", "title": "Une syntaxe aust\u00e8re et rebutante", "text": "

    L\u2019un des reproches les plus fr\u00e9quents adress\u00e9s \u00e0 CMake est sa syntaxe. Elle est souvent d\u00e9crite comme aust\u00e8re, voire \u00ab\u2009immonde\u2009\u00bb, pour reprendre l\u2019expression que beaucoup lui pr\u00eatent dans des cercles de d\u00e9veloppeurs chevronn\u00e9s. La langue de CMake est une sorte de dialecte d\u00e9riv\u00e9 d\u2019une forme rudimentaire de script, mais dont la lisibilit\u00e9 et la clart\u00e9 laissent \u00e0 d\u00e9sirer.

    Prenons par exemple un fichier typique de configuration, appel\u00e9 CMakeLists.txt. Ce fichier est une s\u00e9quence de directives qui sont interpr\u00e9t\u00e9es pour g\u00e9n\u00e9rer les fichiers de build. Voici un exemple minimaliste\u2009:

    cmake_minimum_required(VERSION 3.15)\nproject(MonProjet LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 17)\nset(SOURCES main.cpp util.cpp)\n\nadd_executable(MonExecutable ${SOURCES})\n

    \u00c0 premi\u00e8re vue, il peut sembler relativement simple. Mais lorsqu\u2019on plonge dans un projet plus cons\u00e9quent, la syntaxe devient rapidement verbeuse et d\u00e9nu\u00e9e de la concision ou de l\u2019\u00e9l\u00e9gance syntaxique qu\u2019on pourrait esp\u00e9rer dans un langage de configuration moderne. Les fonctions sont souvent longues et les macros omnipr\u00e9sentes, rendant difficile le d\u00e9bogage et la maintenance d\u2019un script CMake complexe.

    Compar\u00e9 \u00e0 des outils modernes comme Meson, qui se veut plus concis et plus explicite, CMake trahit son origine dans les ann\u00e9es 1990. Il conserve un certain bagage historique, une sorte de legacy syntaxique qui, \u00e0 d\u00e9faut d\u2019\u00eatre intuitive, a tout de m\u00eame l\u2019avantage d\u2019\u00eatre \u00e9prouv\u00e9e et tr\u00e8s bien document\u00e9e.

    ", "tags": ["CMakeLists.txt"]}, {"location": "tools/build-system/cmake/#exemple", "title": "Exemple", "text": "

    L'utilisation de CMake, bien que l\u00e9g\u00e8rement d\u00e9routante au d\u00e9part, suit des principes relativement simples. Voici comment se d\u00e9roule typiquement le processus\u2009:

    1. \u00c9crire le fichier CMakeLists.txt : Ce fichier contient les instructions qui d\u00e9crivent les sources, les d\u00e9pendances, les biblioth\u00e8ques, et d\u2019autres options de configuration.

    2. G\u00e9n\u00e9rer les fichiers de build :

      Depuis la ligne de commande, vous ex\u00e9cutez une commande comme\u2009:

      cmake -S . -B build\n

      Cela cr\u00e9e un r\u00e9pertoire build o\u00f9 sont plac\u00e9s les fichiers de configuration pour l\u2019outil de construction choisi (Make, Ninja, etc.).

    3. Compiler le projet :

      Vous pouvez alors ex\u00e9cuter\u2009:

      cmake --build build\n

      Et CMake utilisera les fichiers g\u00e9n\u00e9r\u00e9s pour compiler votre projet.

    4. Facilit\u00e9 de gestion des d\u00e9pendances :

      L\u2019un des atouts majeurs de CMake r\u00e9side dans sa gestion des d\u00e9pendances via find_package(), qui permet de d\u00e9tecter et lier automatiquement les biblioth\u00e8ques externes.

    Par exemple, si vous voulez lier votre projet \u00e0 Boost, vous pouvez simplement ajouter ceci dans votre CMakeLists.txt :

    find_package(Boost 1.75 REQUIRED)\ntarget_link_libraries(MonExecutable Boost::Boost)\n

    C\u2019est cette capacit\u00e9 \u00e0 manipuler ais\u00e9ment des d\u00e9pendances complexes qui a permis \u00e0 CMake de supplanter d\u2019autres syst\u00e8mes.

    ", "tags": ["build", "CMakeLists.txt"]}, {"location": "tools/build-system/cmake/#conclusion", "title": "Conclusion", "text": "

    En d\u00e9finitive, CMake incarne un compromis d\u00e9licat entre flexibilit\u00e9 et simplicit\u00e9. N\u00e9 d\u2019un besoin imp\u00e9rieux de rationaliser la compilation multi-plateforme, il a su s\u2019imposer comme une r\u00e9f\u00e9rence malgr\u00e9 ses imperfections syntaxiques. Si sa syntaxe d\u00e9plait \u00e0 beaucoup et peut rebuter au premier abord, il n\u2019en reste pas moins que ses capacit\u00e9s \u00e0 g\u00e9rer des projets d\u2019envergure, \u00e0 s\u2019adapter \u00e0 une multitude de syst\u00e8mes et \u00e0 orchestrer la compilation de mani\u00e8re automatis\u00e9e en font un outil indispensable dans le paysage actuel du d\u00e9veloppement logiciel.

    Un outil imparfait, certes, mais ind\u00e9niablement efficace dans sa mission.

    "}, {"location": "tools/build-system/make/", "title": "Make", "text": ""}, {"location": "tools/build-system/make/#introduction", "title": "Introduction", "text": "

    make est un outil de gestion de projet qui permet de compiler des programmes C/C++ de mani\u00e8re efficace. make utilise un fichier Makefile qui contient les r\u00e8gles de compilation.

    Le langage Make est tr\u00e8s ancien (1976) et n'est pas tr\u00e8s lisible aux yeux des d\u00e9butants. Cependant, il est tr\u00e8s puissant et permet de g\u00e9rer des projets de grande envergure. Voici un exemple de Makefile g\u00e9n\u00e9rique pour compiler un programme en C\u2009:

    CC=gcc\nCFLAGS=-std=c17 -O3 -Wall -Werror -pedantic\nLDFLAGS=-lm\nEXEC=main\nSRCS=$(wildcard *.c)\nOBJS=$(SRCS:.c=.o)\n\nall: $(EXEC)\n\n-include $(OBJS:.o=.d)\n\n$(EXEC): $(OBJS)\n    $(CC) -o $@ $^ $(LDFLAGS)\n\n%.o: %.c\n    $(CC) -o $@ -c $< $(CFLAGS) -MMD -MP\n\nclean:\n    $(RM) -f $(OBJS) $(EXEC) $(OBJS:.o=.d)\n\n.PHONY: all clean\n

    Make utilise des variables sp\u00e9ciales et l'appel de macros. Les variables sont d\u00e9finies avec VAR=valeur et appel\u00e9es avec $(VAR).

    Variables Make Variable Description CC Compilateur (convention) CFLAGS Options de compilation (convention) LDFLAGS Options d'\u00e9dition de liens (convention) EXEC Nom du fichier ex\u00e9cutable (convention) SRCS Liste des fichiers sources OBJS Liste des fichiers objets $@ Nom de la cible $^ Liste des d\u00e9pendances $< Premi\u00e8re d\u00e9pendance %.o Jalon g\u00e9n\u00e9rique pour tous les fichiers en .o $(wildcard *.foo) Liste des fichiers .foo dans le r\u00e9pertoire courant $(RM) Commande de suppression de fichiers sur le syst\u00e8me courant

    Le fonctionnement de Make est en soi assez simple. Des r\u00e8gles sont d\u00e9finies avec cible: d\u00e9pendances et les commandes \u00e0 ex\u00e9cuter pour g\u00e9n\u00e9rer la cible.

    Dans l'exemple ci-dessus, la r\u00e8gle all d\u00e9pend de $(EXEC), autrement dit le fichier main. Pour g\u00e9n\u00e9rer main, Make recherche une autre r\u00e8gle du m\u00eame nom. Il trouve main: $(OBJS) qui d\u00e9pend de tous les fichiers objets. Pour g\u00e9n\u00e9rer un fichier objet, Make recherche une autre r\u00e8gle permettant de g\u00e9n\u00e9rer les fichiers objets n\u00e9cessaires. Il trouve %.o: %.c qui d\u00e9pend de tous les fichiers sources. Une fois les fichiers objets g\u00e9n\u00e9r\u00e9s, Make peut g\u00e9n\u00e9rer l'ex\u00e9cutable main.

    Make fonctionne de mani\u00e8re incr\u00e9mentale. Si un fichier source est modifi\u00e9, Make ne recompile que les fichiers objets n\u00e9cessaires. Cela permet de gagner du temps lors du d\u00e9veloppement. Il se base sur les dates de modification des fichiers pour d\u00e9terminer si un fichier doit \u00eatre recompil\u00e9 ou non.

    Ce mode de fonctionnement peut cr\u00e9er des probl\u00e8mes avec les fichiers d'en-t\u00eate. En effet, si un fichier d'en-t\u00eate est modifi\u00e9, Make ne recompile pas les fichiers sources qui incluent ce fichier d'en-t\u00eate puisque les fichiers d'en-t\u00eate n'apparaissent dans aucune r\u00e8gles. Pour r\u00e9soudre ce probl\u00e8me, il est possible de g\u00e9n\u00e9rer des fichiers de d\u00e9pendances avec l'option -MMD -MP du compilateur GCC. Ces fichiers de d\u00e9pendances sont inclus dans le Makefile avec l'option !include $(OBJS:.o=.d). Ils contiennent la liste des fichiers d'en-t\u00eate inclus par chaque fichier source qui indique par exemple que l'objet main.o d\u00e9pend du fichier add.h. Ainsi, si add.h est modifi\u00e9, Make recompile main.o et reg\u00e9n\u00e8re l'ex\u00e9cutable main.

    ", "tags": ["SRCS", "main.o", "make", "LDFLAGS", "EXEC", "main", "CFLAGS", "OBJS", "add.h", "Makefile", "all"]}, {"location": "tools/build-system/make/#utilisation", "title": "Utilisation", "text": "

    Pour utiliser Make, il suffit de cr\u00e9er un fichier Makefile dans le r\u00e9pertoire du projet. Ensuite, il suffit de taper la commande make dans un terminal pour compiler le projet. Pour nettoyer les fichiers temporaires, il suffit de taper la commande make clean.

    Voici l'exemple de cr\u00e9ation d'un programme en C avec deux fichiers C et un fichier d'en-t\u00eate\u2009:

    main.cadd.hadd.c
    #include \"add.h\"\n#include <stdio.h>\n\nint main() {\n    const int a = 2, b = 3;\n    printf(\"Somme de %d+%d = %d\\n\", a, b, add(a, b));\n}\n
    #pragma once\nint add(int a, int b);\n
    int add(int a, int b) {\n    return a + b;\n}\n

    Votre Makefile devrait ressembler \u00e0 ceci\u2009:

    CC=gcc\nCFLAGS=-std=c17 -O3 -Wall -Werror -pedantic\nLDFLAGS=-lm # Si vous utilisez la librairie math\u00e9matique\nEXEC=main\n\nall: $(EXEC)\n\n$(EXEC): main.o add.o\n    $(CC) -o $@ $^ $(LDFLAGS)\n\n%.o: %.c | add.h\n    $(CC) -o $@ -c $< $(CFLAGS)\n\nclean:\n    $(RM) -f *.o $(EXEC)\n
    ", "tags": ["Makefile", "make"]}, {"location": "tools/build-system/meson/", "title": "Meson", "text": ""}, {"location": "tools/build-system/meson/#quest-ce-que-meson", "title": "Qu'est-ce que Meson\u2009?", "text": "

    Meson est un syst\u00e8me de build open-source con\u00e7u pour \u00eatre simple, rapide, et multiplateforme. Il est utilis\u00e9 principalement pour g\u00e9rer des projets en C, C++, mais il supporte \u00e9galement d'autres langages comme Python, Java, Rust, et plus encore. Meson a \u00e9t\u00e9 cr\u00e9\u00e9 avec un objectif clair\u2009: acc\u00e9l\u00e9rer le processus de compilation, tout en simplifiant la gestion des projets et en r\u00e9duisant les probl\u00e8mes li\u00e9s \u00e0 la configuration.

    Il g\u00e9n\u00e8re des fichiers de build pour diff\u00e9rents syst\u00e8mes de build, comme Ninja (qui est le plus couramment utilis\u00e9 avec Meson pour sa vitesse). Contrairement \u00e0 des outils comme CMake ou Autotools, Meson offre une syntaxe de configuration plus simple et plus lisible, et il g\u00e8re mieux les projets complexes gr\u00e2ce \u00e0 sa gestion automatique des d\u00e9pendances.

    "}, {"location": "tools/build-system/meson/#histoire-et-motivation-derriere-meson", "title": "Histoire et motivation derri\u00e8re Meson", "text": "

    Meson a \u00e9t\u00e9 cr\u00e9\u00e9 par Jussi Pakkanen en 2012, avec pour objectif de r\u00e9pondre \u00e0 plusieurs limitations des outils de build traditionnels comme Make, Autotools ou m\u00eame CMake. Il visait \u00e0 r\u00e9soudre des probl\u00e8mes de complexit\u00e9 excessive, de vitesse de compilation, et \u00e0 offrir une meilleure exp\u00e9rience utilisateur avec\u2009:

    1. Une syntaxe plus claire et concise pour les fichiers de configuration.
    2. Une meilleure int\u00e9gration avec Ninja pour des builds plus rapides.
    3. Un support am\u00e9lior\u00e9 pour la compilation incr\u00e9mentale.
    4. Une portabilit\u00e9 facile entre Linux, Windows, et macOS.
    5. Une gestion simplifi\u00e9e des d\u00e9pendances et des sous-projets.

    Meson a \u00e9t\u00e9 rapidement adopt\u00e9 par de grands projets open-source, tels que GNOME, X.org, GStreamer, ou encore systemd, en raison de sa simplicit\u00e9 et de ses performances.

    "}, {"location": "tools/build-system/meson/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    Meson fonctionne en deux \u00e9tapes principales\u2009:

    1. Configuration : Meson configure le projet, g\u00e9n\u00e8re un ensemble de fichiers de build pour Ninja (ou d'autres syst\u00e8mes), et v\u00e9rifie les d\u00e9pendances et les param\u00e8tres du compilateur.
    2. Build : Ninja (ou un autre backend) est ensuite utilis\u00e9 pour compiler et lier les fichiers sources.

    Voici quelques caract\u00e9ristiques cl\u00e9s\u2009:

    • Syntaxe simple : Les fichiers meson.build sont simples \u00e0 \u00e9crire et \u00e0 comprendre.
    • Multiplateforme : Supporte les syst\u00e8mes Linux, macOS, Windows, etc.
    • Optimisation du parall\u00e9lisme : Utilise Ninja pour tirer parti des syst\u00e8mes multi-c\u0153urs.
    • Configuration rapide : Par rapport \u00e0 des outils comme CMake ou Autotools, Meson est con\u00e7u pour des configurations plus rapides.
    • Test et Benchmark : Meson int\u00e8gre directement des outils pour les tests unitaires et les benchmarks.
    ", "tags": ["meson.build"]}, {"location": "tools/build-system/meson/#installation", "title": "Installation", "text": "

    Meson est \u00e9crit en Python, donc son installation est simple avec pip :

    pip install meson\n

    Ou sur certaines distributions Linux comme Ubuntu\u2009:

    sudo apt-get install meson\n

    Sur macOS avec Homebrew\u2009:

    brew install meson\n
    ", "tags": ["pip"]}, {"location": "tools/build-system/meson/#utilisation", "title": "Utilisation", "text": "

    Meson utilise des fichiers appel\u00e9s meson.build pour d\u00e9finir la configuration de build. Prenons l'exemple d'un projet typique ayant la structure suivante\u2009:

    project/\n  src/\n    main.c\n    util.c\n    util.h\n  meson.build\n  meson_options.txt\n
    ", "tags": ["meson.build"]}, {"location": "tools/build-system/meson/#exemple", "title": "Exemple", "text": "

    Le fichier meson.build utilise la syntaxe Meson bas\u00e9e sur Python pour configurer le projet. Voici un exemple simple\u2009:

    project('super-rocket', 'c',\n  version: '1.0',\n  default_options: ['warning_level=3', 'c_std=gnu11'])\n\nexecutable('super-rocket',\n  sources: ['src/main.c', 'src/server.c'],\n  dependencies: [dependency('libuv')]\n)\n

    Frustrations

    Les d\u00e9veloppeurs de meson ont fait quelques choix que vous pourriez trouver frustrants. Par exemple, les fichiers de configuration sont \u00e9crits en Python, mais ils ne sont pas des scripts Python valides. Cela signifie que vous ne pouvez pas utiliser de variables, de boucles ou de conditions Python dans les fichiers de configuration.

    Les param\u00e8tres de configuration sont des cha\u00eenes de caract\u00e8res dont aucune aide possible n'est fournie par l'IDE. Cela peut rendre la configuration plus difficile pour les nouveaux utilisateurs.

    La liste des fichiers source doit \u00eatre \u00e9crite manuellement, ce qui peut \u00eatre fastidieux pour les projets de grande taille. Il n'y a pas de moyen d'utiliser des expressions r\u00e9guli\u00e8res ou un glob (comme *.c) pour inclure automatiquement tous les fichiers source. C'est un choix d\u00e9lib\u00e9r\u00e9 pour \u00e9viter les probl\u00e8mes de r\u00e9p\u00e9tabilit\u00e9, mais cela peut \u00eatre un inconv\u00e9nient pour certains projets.

    Ce fichier fait plusieurs choses\u2009: il d\u00e9finit le nom du projet (my_program) et le langage utilis\u00e9 (C), ainsi que quelques options par d\u00e9faut, il sp\u00e9cifie que l'ex\u00e9cutable my_program sera construit \u00e0 partir des fichiers main.c et server.c enfin, il sp\u00e9cifie que le programme d\u00e9pend de la biblioth\u00e8que libuv, en utilisant dependency('libuv') pour lier automatiquement cette biblioth\u00e8que au projet.

    Nous prendrons pour l'exemple les fichiers suivants\u2009:

    • Le fichier main.c est le point d'entr\u00e9e du programme.

      #include <stdio.h>\n#include \"server.h\"\n\nint main() { start_server(); }\n
    • Le fichier server.h d\u00e9clare la fonction start_server.

      #pragma once\nvoid start_server();\n
    • Le fichier server.c impl\u00e9mente la logique du serveur, en utilisant libuv pour cr\u00e9er un serveur TCP.

      #include <stdio.h>\n#include <stdlib.h>\n#include <uv.h> // Requires -std=gnu11\n#include \"server.h\"\n\nstatic void on_new_connection(uv_stream_t *server, int status) {\n    if (status < 0) {\n        fprintf(stderr, \"New connection error %s\\n\", uv_strerror(status));\n        return;\n    }\n    uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));\n    uv_tcp_init(uv_default_loop(), client);\n    if (uv_accept(server, (uv_stream_t*) client) == 0)\n        printf(\"Accepted new connection\\n\");\n    else\n        uv_close((uv_handle_t*) client, NULL);\n}\n\nvoid start_server() {\n    uv_tcp_t server;\n    uv_tcp_init(uv_default_loop(), &server);\n    struct sockaddr_in addr;\n    uv_ip4_addr(\"0.0.0.0\", 7000, &addr);\n    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);\n    int r = uv_listen((uv_stream_t*) &server, 128, on_new_connection);\n    if (r) {\n        fprintf(stderr, \"Listen error %s\\n\", uv_strerror(r));\n        return;\n    }\n    printf(\"Listening on port 7000\\n\");\n    uv_run(uv_default_loop(), UV_RUN_DEFAULT);\n}\n

    Si aucun meson.build n'existe, vous pouvez en cr\u00e9er un en utilisant la commande meson init :

    meson init\n

    Pour compiler ce projet on appelle la commande meson avec l'option setup pour configurer le projet, puis compile pour compiler les fichiers sources\u2009:

    $ meson setup builddir\nThe Meson build system\nVersion: 1.3.2\nSource dir: /home/ycr/meson-test\nBuild dir: /home/ycr/meson-test/builddir\nBuild type: native build\nProject name: super-rocket\nProject version: 1.0\nC compiler for the host machine: cc (gcc 13.2.0\n    \"cc (Ubuntu 13.2.0-23ubuntu4) 13.2.0\")\nC linker for the host machine: cc ld.bfd 2.42\nHost machine cpu family: x86_64\nHost machine cpu: x86_64\nFound pkg-config: YES (/usr/bin/pkg-config) 1.8.1\nRun-time dependency libuv found: YES 1.48.0\nRun-time dependency threads found: YES\nBuild targets in project: 1\n\nFound ninja-1.11.1 at /usr/bin/ninja\n

    Cette commande configure le projet dans un r\u00e9pertoire de build (builddir) et g\u00e9n\u00e8re les fichiers n\u00e9cessaires pour Ninja qui est le backend par d\u00e9faut de Meson. Ensuite, on peut compiler le projet avec\u2009:

    $ meson compile -C builddir\nINFO: autodetecting backend as ninja\nINFO: calculating backend command to run: /usr/bin/ninja\n    -C /home/ycr/meson-test/builddir\nninja: Entering directory `/home/ycr/meson-test/builddir'\n[3/3] Linking target super-rocket\n

    De la m\u00eame mani\u00e8re que CMake, meson utilise un r\u00e9pertoire de build s\u00e9par\u00e9 pour isoler les fichiers g\u00e9n\u00e9r\u00e9s du code source. Cela permet de nettoyer facilement les fichiers de build en supprimant simplement le r\u00e9pertoire de build, mais cela demande une \u00e9tape suppl\u00e9mentaire lors du build.

    Une fois compil\u00e9, le programme peut \u00eatre ex\u00e9cut\u00e9\u2009:

    ./builddir/super-rocket\n

    Cela d\u00e9marre le serveur, et il \u00e9coutera les connexions TCP sur le port 7000.

    Extension vscode

    Une extension Meson existe pour Visual Studio Code, qui fournit une coloration syntaxique pour les fichiers meson.build et des fonctionnalit\u00e9s de compl\u00e9tion automatique pour les options Meson.

    ", "tags": ["setup", "builddir", "meson.build", "start_server", "my_program", "main.c", "compile", "server.h", "meson", "server.c"]}, {"location": "tools/build-system/meson/#cheatsheet", "title": "Cheatsheet", "text": "

    Voici une liste de commandes Meson couramment utilis\u00e9es\u2009:

    Commande Meson Description meson setup builddir Configure le projet dans le r\u00e9pertoire de build builddir. meson compile -C builddir Compile le projet dans le r\u00e9pertoire de build builddir. meson test -C builddir Ex\u00e9cute les tests unitaires du projet. meson install -C builddir Installe les fichiers du projet sur le syst\u00e8me. meson configure -Doption=value Configure le projet avec une option sp\u00e9cifique. meson introspect Affiche des informations sur le projet.", "tags": ["builddir"]}, {"location": "tools/build-system/ninja/", "title": "Ninja", "text": "

    Ninja est un outil de build syst\u00e8me con\u00e7u pour \u00eatre rapide. Il est souvent utilis\u00e9 pour compiler de gros projets C/C++ comme LLVM, Chromium ou Android. Ninja est \u00e9crit en C++ et est distribu\u00e9 sous licence Apache 2.0. Il a \u00e9t\u00e9 con\u00e7u pour g\u00e9rer efficacement de tr\u00e8s gros projets, tels que Chromium, LLVM ou Android, o\u00f9 des milliers de fichiers peuvent \u00eatre compil\u00e9s et li\u00e9s. Il a \u00e9t\u00e9 cr\u00e9\u00e9 en tant qu'alternative \u00e0 Make, pour r\u00e9soudre des probl\u00e8mes de performance dans des sc\u00e9narios de compilation \u00e0 grande \u00e9chelle. Ninja est particuli\u00e8rement optimis\u00e9 pour la vitesse d'ex\u00e9cution, en \u00e9vitant de faire plus de travail que n\u00e9cessaire.

    L'objectif de Ninja est simple\u2009: minimiser le temps n\u00e9cessaire \u00e0 la compilation incr\u00e9mentale, c'est-\u00e0-dire le temps requis pour recompiler uniquement les fichiers qui ont \u00e9t\u00e9 modifi\u00e9s ou leurs d\u00e9pendances, sans avoir \u00e0 recompiler tout le projet.

    "}, {"location": "tools/build-system/ninja/#principe-de-fonctionnement", "title": "Principe de fonctionnement", "text": "

    Ninja fonctionne avec des fichiers de build appel\u00e9s build.ninja, qui contiennent les r\u00e8gles de compilation. Ces fichiers sont semblables aux Makefile, mais beaucoup plus simples et compacts. Voici comment fonctionne Ninja\u2009:

    1. Fichiers d'entr\u00e9e simplifi\u00e9s : Ninja ne g\u00e9n\u00e8re pas ses fichiers de configuration de mani\u00e8re manuelle, contrairement \u00e0 Make, o\u00f9 les d\u00e9veloppeurs doivent souvent \u00e9crire de longs et complexes Makefile. Les fichiers build.ninja sont g\u00e9n\u00e9ralement g\u00e9n\u00e9r\u00e9s par un outil de configuration externe (comme CMake, GN ou Meson).

    2. Graphes de d\u00e9pendance : Ninja cr\u00e9e un graphe des d\u00e9pendances pour d\u00e9terminer quelles parties du projet doivent \u00eatre reconstruites en fonction des changements dans les fichiers source.

    3. Compilation incr\u00e9mentale rapide : Ninja d\u00e9tecte les fichiers modifi\u00e9s et ne recompile que ce qui est n\u00e9cessaire. Il optimise \u00e9galement le parall\u00e9lisme en construisant plusieurs fichiers simultan\u00e9ment, en fonction des c\u0153urs CPU disponibles.

    4. Pas de fonctionnalit\u00e9s inutiles : Ninja se concentre uniquement sur le build. Il n\u2019inclut pas de fonctionnalit\u00e9s additionnelles comme l'installation, le nettoyage (clean), etc., qui sont souvent pr\u00e9sentes dans d'autres syst\u00e8mes comme Make. Il d\u00e9l\u00e8gue ces t\u00e2ches \u00e0 des outils externes.

    ", "tags": ["Makefile", "build.ninja"]}, {"location": "tools/build-system/ninja/#avantages-par-rapport-a-make", "title": "Avantages par rapport \u00e0 Make", "text": "
    1. Vitesse : Ninja est con\u00e7u pour \u00eatre plus rapide que Make, particuli\u00e8rement pour les builds incr\u00e9mentaux. Il minimise le temps de reconfiguration et de relance des builds en \u00e9vitant des v\u00e9rifications inutiles.

    2. Optimisation du parall\u00e9lisme : Ninja est plus efficace que Make pour tirer parti des syst\u00e8mes multi-c\u0153urs. Il calcule mieux les d\u00e9pendances et s'assure que le maximum de t\u00e2ches est lanc\u00e9 en parall\u00e8le.

    3. Simplicit\u00e9 des fichiers de build : Les fichiers build.ninja sont simples, \u00e9pur\u00e9s et plus rapides \u00e0 lire pour l'outil. Cela contraste avec les Makefile, souvent longs et difficiles \u00e0 maintenir.

    4. Moins d'ambigu\u00eft\u00e9 : Ninja \u00e9vite certains pi\u00e8ges de Make, comme la n\u00e9cessit\u00e9 de suivre des conventions implicites. Ninja est explicite et ne laisse pas de place \u00e0 des comportements ambigus.

    5. Aucun overhead de fonctionnalit\u00e9s inutiles : Ninja se concentre exclusivement sur la compilation et ne se charge pas d'autres t\u00e2ches comme l'installation ou le nettoyage. Cela r\u00e9duit la complexit\u00e9 du processus de build.

    6. Build d\u00e9terministe : Ninja est con\u00e7u pour \u00eatre d\u00e9terministe, c'est-\u00e0-dire que si l'\u00e9tat du syst\u00e8me est identique, le build produit sera identique, ce qui n'est pas toujours le cas avec Make.

    Ci-dessous un tableau comparatif entre Make et Ninja.

    Comparaison entre Make et Ninja Caract\u00e9ristiques Make Ninja Performance Plus lent pour les gros projets Con\u00e7u pour \u00eatre extr\u00eamement rapide Configuration Fichiers Makefile manuellement \u00e9crits Fichiers build.ninja souvent meta-g\u00e9n\u00e9r\u00e9s Parall\u00e9lisme Support, mais moins optimis\u00e9 Tr\u00e8s performant avec des builds parall\u00e8les Complexit\u00e9 Peut \u00eatre tr\u00e8s complexe et verbeux Fichiers simples et explicites Objectif Outil g\u00e9n\u00e9raliste Con\u00e7u uniquement pour la compilation rapide D\u00e9pendances Manuelle, souvent avec des outils externes D\u00e9tection automatique des d\u00e9pendances Fonctionnalit\u00e9s Supporte des t\u00e2ches additionnelles Focus uniquement sur la compilation", "tags": ["Makefile", "build.ninja"]}, {"location": "tools/build-system/ninja/#installation", "title": "Installation", "text": "

    Pour installer Ninja, il suffit de t\u00e9l\u00e9charger l'ex\u00e9cutable ou d'utiliser un gestionnaire de paquets. Par exemple\u2009:

    UbuntumacOSWindows
    sudo apt-get install ninja-build\n
    brew install ninja\n

    Aucune id\u00e9e... \u00c0 vous de chercher.

    "}, {"location": "tools/build-system/ninja/#exemple-de-fichier-buildninja", "title": "Exemple de fichier build.ninja", "text": "

    Un fichier simple build.ninja pourrait ressembler \u00e0 ceci\u2009:

    rule cc\n  command = gcc -c $in -o $out\n  description = Compiling $in\n\nrule link\n  command = gcc $in -o $out\n  description = Linking $out\n\nbuild foo.o: cc foo.c\nbuild bar.o: cc bar.c\nbuild my_program: link foo.o bar.o\n

    Dans cet exemple, Ninja ex\u00e9cute deux \u00e9tapes principales\u2009:

    • Compilation (cc), qui compile les fichiers .c en .o.
    • Linkage (link), qui prend les fichiers objets .o et les lie pour produire l'ex\u00e9cutable final my_program.

    Une fois que le fichier build.ninja est g\u00e9n\u00e9r\u00e9, vous pouvez compiler votre projet en ex\u00e9cutant simplement la commande\u2009:

    ninja\n

    Cela lancera la compilation en fonction des r\u00e8gles d\u00e9finies dans le fichier build.ninja, avec des optimisations pour la compilation incr\u00e9mentale et le parall\u00e9lisme.

    ", "tags": ["link", "build.ninja", "my_program"]}, {"location": "tools/build-system/ninja/#discussion", "title": "Discussion", "text": "

    Contrairement \u00e0 make, ninja ne permet pas de d\u00e9finir des r\u00e8gles de mani\u00e8re dynamique. Cela signifie que vous ne pouvez pas g\u00e9n\u00e9rer des r\u00e8gles de compilation en fonction de l'\u00e9tat du syst\u00e8me ou d'autres param\u00e8tres. Cela peut \u00eatre un inconv\u00e9nient pour certains projets, mais cela permet \u00e9galement de garantir une certaine d\u00e9terminisme dans le processus de build.

    Par exemple avec Make, on peut d\u00e9finir la r\u00e8gle suivante qui est une r\u00e8gle g\u00e9n\u00e9rique pour compiler tous les fichiers .c en fichiers .o.

    %.o: %.c\n    $(CC) -o $@ -c $< $(CFLAGS) -MMD -MP\n

    Cette r\u00e8gle est souvent associ\u00e9e \u00e0 une autre r\u00e8gle pour r\u00e9cup\u00e9rer les fichiers sources automatiquement\u2009:

    SRCS := $(wildcard *.c)\nOBJS := $(SRCS:.c=.o)\n

    Avec Ninja, vous devez d\u00e9finir explicitement chaque r\u00e8gle de compilation, ce qui peut \u00eatre fastidieux pour les projets de grande taille. C'est pour cette raison que l'outil est souvent utilis\u00e9 en conjonction avec des outils de configuration comme CMake ou Meson qui g\u00e9n\u00e8rent les fichiers build.ninja pour vous.

    Ninja n'est donc pas un outil destin\u00e9 \u00e0 \u00eatre utilis\u00e9 manuellement pour chaque projet, mais plut\u00f4t comme un outil de build syst\u00e8me pour des projets plus importants o\u00f9 la vitesse et l'efficacit\u00e9 sont des priorit\u00e9s.

    ", "tags": ["build.ninja"]}, {"location": "tools/c-cpp/conan/", "title": "Conan", "text": "

    Conan est un gestionnaire de paquets C/C++ multiplateforme. Il permet de g\u00e9rer les d\u00e9pendances de vos projets C/C++ de mani\u00e8re simple et efficace. Il est multiplateforme, open-source et supporte de nombreux gestionnaires de build (CMake, Visual Studio, Meson, etc.).

    Il est souvent utilis\u00e9 conjointement avec Meson, un autre outil de build C/C++ moderne.

    Conan est d\u00e9velopp\u00e9 avec Python et est disponible sur PyPI. Pour l'installer, vous pouvez utiliser pip :

    pipx install conan\n

    Il utilise un fichier de configuration conanfile.txt ou conanfile.py pour d\u00e9clarer les d\u00e9pendances de votre projet. Voici un exemple de fichier conanfile.txt :

    [requires]\npoco/1.10.1\nsdl2/2.0.10\n\n[generators]\ncmake\n

    Alternativement vous pouvez ajouter une d\u00e9pendance \u00e0 un projet existant avec la commande conan install :

    conan install conan install Poco/1.13.3@pocoproject/stable --build missing\n
    ", "tags": ["conanfile.txt", "pip", "conanfile.py"]}, {"location": "tools/c-cpp/gcc/", "title": "Compilateur C/C++", "text": "

    Compiler un programme en C ou en C++ requiert un compilateur. Il s'agit d'un programme qui permet de traduire le code source en langage C ou C++ en un fichier objet (extension .o ou .obj), ou un fichier ex\u00e9cutable (extension .exe sous Windows ou sans extension dans un syst\u00e8me POSIX).

    Le compilateur le plus utilis\u00e9 pour le C est gcc (GNU Compiler Collection) et pour le C++ est g++. Ces deux compilateurs sont inclus dans la suite logicielle gcc. Pour installer gcc sous Ubuntu, il suffit de taper la commande suivante dans un terminal\u2009:

    sudo apt-get install build-essential\n

    Sous Windows, c'est plus compliqu\u00e9. GCC \u00e9tant un logiciel pr\u00e9vu pour un syst\u00e8me POSIX, il n'est pas directement compatible avec Windows. Il existe cependant des ports de GCC pour Windows, comme MinGW, Cygwin ou MSYS2. Ces ports permettent d'installer GCC sur Windows, mais il est plus \u00e9l\u00e9gant et plus pratique d'utiliser WSL si les applications que vous d\u00e9veloppez sont destin\u00e9es \u00e0 vos \u00e9tudes et ne seront pas distribu\u00e9es \u00e0 des utilisateurs Windows.

    Si vous souhaitez compiler sous Windows avec un compilateur Windows, vous pouvez utiliser Visual Studio qu'il ne faut pas confondre avec Visual Studio Code. Visual Studio est un IDE complet qui inclut un compilateur C/C++. Ce compilateur nomm\u00e9 cl est un compilateur propri\u00e9taire de Microsoft. Il est inclus dans Visual Studio et n'est pas disponible s\u00e9par\u00e9ment. Ce compilateur ne respecte pas toujours le standard C (ISO/IEC 9899) mais couvre presque en totalit\u00e9 la norme C++ (ISO/IEC 14882).

    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#cycle-de-compilation", "title": "Cycle de compilation", "text": "

    La compilation requiert plusieurs \u00e9tapes\u2009:

    1. Pr\u00e9traitement : Le pr\u00e9processeur remplace les macros par leur d\u00e9finition, inclut les fichiers d'en-t\u00eate, etc.
    2. Compilation : La compilation converti le code source en assembleur.
    3. Assemblage : L'assembleur converti le code assembleur en code objet, il lie les biblioth\u00e8ques statiques et r\u00e9soud les adresses des fonctions externes.
    4. \u00c9dition de liens : Le lien \u00e9dite les fichiers objets pour cr\u00e9er un fichier ex\u00e9cutable.

    Chacune de ces \u00e9tapes peut \u00eatre r\u00e9alis\u00e9e s\u00e9par\u00e9ment. Par exemple, pour compiler un programme en C, il est possible de g\u00e9n\u00e9rer le fichier objet sans l'\u00e9diter de liens. Cela permet de gagner du temps lors du d\u00e9veloppement, car seule la partie modifi\u00e9e du code est recompil\u00e9e\u2009:

    # G\u00e9n\u00e8re un fichier C\n$ cat > main.c <<EOF\n#include <stdio.h>\nint main() {\n    printf(\"hello, world!\\n\");\n}\nEOF\n\n# \u00c9tape de pr\u00e9processeur\n$ gcc -E main.c -o main.i\n\n# \u00c9tape de compilation\n$ gcc -S main.i -o main.s\n\n# \u00c9tape d'assemblage\n$ gcc -c main.s -o main.o\n\n# \u00c9tape d'\u00e9dition de liens\n$ gcc main.o -o main\n
    "}, {"location": "tools/c-cpp/gcc/#compilation-separee", "title": "Compilation s\u00e9par\u00e9e", "text": "

    La compilation s\u00e9par\u00e9e est une technique de d\u00e9veloppement qui consiste \u00e0 compiler chaque fichier source s\u00e9par\u00e9ment. Cela permet de r\u00e9duire le temps de compilation lors du d\u00e9veloppement, car seuls les fichiers modifi\u00e9s sont recompil\u00e9s. Cela permet \u00e9galement de r\u00e9duire la taille des fichiers objets, car les fonctions non utilis\u00e9es ne sont pas incluses dans le fichier objet.

    # G\u00e9n\u00e8re un fichier C\n$ cat > main.c <<EOF\n#include \"add.h\"\n#include <stdio.h>\nint main() {\n    const int a = 2, b = 3;\n    printf(\"Somme de %d+%d = %d\\n\", a, b, add(a, b));\n}\nEOF\n\n$ cat > add.h <<EOF\nint add(int a, int b);\nEOF\n\n$ cat > add.c <<EOF\nint add(int a, int b) {\n    return a + b;\n}\nEOF\n\n# Compilation s\u00e9par\u00e9e des objets\n$ gcc -c main.c -o main.o\n$ gcc -c add.c -o add.o\n\n# \u00c9dition de liens\n$ gcc main.o add.o -o main\n

    Notez que si vous avez deux fichiers C mais que vous ne souhaitez pas les compiler s\u00e9par\u00e9ment, vous pouvez les compiler en une seule commande\u2009:

    $ gcc main.c add.c -o main\n
    "}, {"location": "tools/c-cpp/gcc/#options-de-compilation", "title": "Options de compilation", "text": "

    gcc et g++ acceptent de nombreuses options de compilation. Les plus courantes sont\u2009:

    Options de compilation de GCC Option Description -c Compile le code source en un fichier objet sans l'\u00e9diter de liens. -o Sp\u00e9cifie le nom du fichier de sortie. -I Sp\u00e9cifie un r\u00e9pertoire o\u00f9 chercher les fichiers d'en-t\u00eate. -L Sp\u00e9cifie un r\u00e9pertoire o\u00f9 chercher les biblioth\u00e8ques. -l Sp\u00e9cifie une biblioth\u00e8que \u00e0 lier. -Wall Active tous les avertissements. -Werror Traite les avertissements comme des erreurs. -g Inclut des informations de d\u00e9bogage dans le fichier objet. -O Optimise le code. -std Sp\u00e9cifie la norme du langage. -pedantic Respecte strictement la norme. -D D\u00e9finit une macro. -U Undefine une macro. -E Arr\u00eate apr\u00e8s l'\u00e9tape de pr\u00e9processeur. -S Arr\u00eate apr\u00e8s l'\u00e9tape de compilation. -v Affiche les commandes ex\u00e9cut\u00e9es par le compilateur.

    Pour compiler un programme avec les optimisations maximales, dans la norme C17, avec tous les avertissements activ\u00e9s et trait\u00e9s comme des erreurs, vous pouvez utiliser la commande suivante\u2009:

    $ gcc -std=c17 -O3 -Wall -Werror -pedantic main.c -o main\n
    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#optimisation", "title": "Optimisation", "text": "

    L'optimisation est une technique qui vise \u00e0 am\u00e9liorer les performances d'un programme en r\u00e9duisant le temps d'ex\u00e9cution et/ou la consommation de m\u00e9moire. Les compilateurs gcc et g++ offrent plusieurs niveaux d'optimisation, chacun ayant des caract\u00e9ristiques propres.

    -O0

    Aucune optimisation. C'est le niveau par d\u00e9faut. Le code C est traduit en langage assembleur sans tentative d'am\u00e9lioration des performances. Cela facilite le d\u00e9bogage, car le code g\u00e9n\u00e9r\u00e9 reste tr\u00e8s proche du code source. Toutefois, les performances peuvent \u00eatre deux \u00e0 trois fois inf\u00e9rieures \u00e0 celles du code optimis\u00e9.

    -O1

    Optimisation l\u00e9g\u00e8re. Le compilateur applique des optimisations locales comme la suppression des instructions inutiles, la propagation des constantes, et la simplification des expressions constantes. Ce niveau d'optimisation \u00e9quilibre le gain de performances avec la vitesse de compilation.

    -O2

    Optimisation standard. \u00c0 ce niveau, le compilateur applique des optimisations plus agressives, telles que la r\u00e9duction des boucles, la suppression des appels de fonctions inutiles et l'am\u00e9lioration de la gestion des expressions. Cela peut allonger le temps de compilation et augmenter la taille du code, mais les performances g\u00e9n\u00e9r\u00e9es sont significativement am\u00e9lior\u00e9es.

    -O3

    Optimisation maximale. Le compilateur applique toutes les optimisations du niveau -O2 ainsi que des optimisations suppl\u00e9mentaires comme l'optimisation des boucles (loop unrolling), l'inlining de fonctions plus importantes, et l'am\u00e9lioration de la gestion des appels de fonctions. Ce niveau maximise les performances, mais peut \u00e9galement accro\u00eetre la taille du code et le temps de compilation.

    -Os

    Optimisation pour la taille. Le compilateur se concentre sur la r\u00e9duction de la taille du fichier ex\u00e9cutable. Cela peut r\u00e9duire les performances, mais est souvent utile pour les environnements \u00e0 ressources limit\u00e9es, comme les syst\u00e8mes embarqu\u00e9s, ou pour les applications devant \u00eatre distribu\u00e9es via des r\u00e9seaux \u00e0 faible bande passante.

    -Ofast

    Optimisation rapide. Le compilateur applique les optimisations du niveau -O3, mais avec des assouplissements sur le respect des normes du langage C. Certaines v\u00e9rifications sont d\u00e9sactiv\u00e9es pour am\u00e9liorer encore les performances, ce qui peut entra\u00eener des comportements non conformes \u00e0 la norme dans certains cas. Ce niveau peut \u00eatre tr\u00e8s performant, mais il doit \u00eatre utilis\u00e9 avec pr\u00e9caution.

    -ffast-math

    Optimisation agressive des calculs math\u00e9matiques. Le compilateur effectue des optimisations pouss\u00e9es sur les op\u00e9rations math\u00e9matiques en ignorant certaines pr\u00e9cautions sur la pr\u00e9cision et les exceptions. Cela peut consid\u00e9rablement acc\u00e9l\u00e9rer les calculs, mais augmente \u00e9galement le risque de r\u00e9sultats incorrects ou inattendus. Cette option est \u00e0 utiliser uniquement si la pr\u00e9cision des calculs n'est pas critique.

    -flto

    Optimisation intermodulaire (Link-Time Optimization). Le compilateur fusionne les fichiers objets avant l'\u00e9dition de liens, permettant ainsi des optimisations globales \u00e0 l'\u00e9chelle du programme. Cela inclut l'inlining entre fichiers, la suppression des fonctions inutilis\u00e9es, la propagation des constantes et la fusion des boucles. Cette optimisation est particuli\u00e8rement efficace sur les gros projets avec plusieurs fichiers source, permettant d'am\u00e9liorer les performances globales du programme.

    ", "tags": ["gcc"]}, {"location": "tools/c-cpp/gcc/#bibliotheques", "title": "Biblioth\u00e8ques", "text": "

    Les biblioth\u00e8ques sont des fichiers contenant des fonctions et des variables pr\u00e9d\u00e9finies. Il existe deux types de biblioth\u00e8ques\u2009: les biblioth\u00e8ques statiques et les biblioth\u00e8ques partag\u00e9es. Les biblioth\u00e8ques statiques ont l'extension .a sous POSIX et .lib sous Windows. Les biblioth\u00e8ques partag\u00e9es ont l'extension .so sous POSIX et .dll sous Windows.

    La biblioth\u00e8que standard du langage C est libc. Elle est incluse par d\u00e9faut dans tous les programmes C. Pour inclure une biblioth\u00e8que, il suffit de sp\u00e9cifier son nom avec l'option -l. Par exmple la biblioth\u00e8que m contient des fonctions math\u00e9matiques. Elle s'appelle libm sous Unix/Linux.

    Aussi si vous souhaitez calculer le sinus de 3.14, vous pouvez utiliser la fonction sin de la biblioth\u00e8que m, et par cons\u00e9quent vous devez lier cette biblioth\u00e8que \u00e0 votre programme\u2009:

    $ gcc main.c -o main -lm\n

    Notez que l'option -lm doit \u00eatre plac\u00e9e apr\u00e8s le nom du fichier source. En effet, gcc traite les options dans l'ordre o\u00f9 elles apparaissent sur la ligne de commande. Si l'option -lm est plac\u00e9e avant le nom du fichier source, gcc ne trouvera pas la fonction sin et \u00e9chouera. Dans la table ci-dessous vous trouverez quelques biblioth\u00e8ques couramment utilis\u00e9es.

    Biblioth\u00e8ques couramment utilis\u00e9es Biblioth\u00e8que Description libc Biblioth\u00e8que standard du langage C. libm Fonctions math\u00e9matiques. libpthread Fonctions de threads POSIX. libcurl Client HTTP. libssl Biblioth\u00e8que de chiffrement SSL. libcrypto Biblioth\u00e8que de chiffrement. libz Compression de donn\u00e9es. libpng Traitement d'images PNG. libsqlite3 Base de donn\u00e9es SQLite.", "tags": ["gcc", "libc", "libpthread", "sin", "libcrypto", "libm", "libssl", "libcurl", "libz", "libsqlite3", "libpng"]}, {"location": "tools/c-cpp/gdb/", "title": "GDB (GNU Debugger)", "text": "

    GDB est un d\u00e9bogueur en ligne de commande. Il permet de suivre l'ex\u00e9cution d'un programme pas \u00e0 pas, de mettre des points d'arr\u00eat, d'inspecter la m\u00e9moire, de modifier le contenu des variables, etc.

    "}, {"location": "tools/c-cpp/vcpkg/", "title": "VcPKG", "text": "

    VcPKG est un gestionnaire de paquets C++ multiplateforme qui permet d'installer et de g\u00e9rer des biblioth\u00e8ques tierces. Il est utilis\u00e9 pour simplifier le processus d'installation de biblioth\u00e8ques C++ sur Windows, Linux et macOS.

    Il est similaire \u00e0 Conan, mais il est plus orient\u00e9 vers les biblioth\u00e8ques C++ et est souvent utilis\u00e9 avec Visual Studio.

    "}, {"location": "tools/dev/bash/", "title": "Bash (Ligne de commande)", "text": "

    La ma\u00eetrise de la ligne de commande n'est pas indispensable pour ce cours, mais la compr\u00e9hension de quelques commandes est utile pour bien comprendre les exemples donn\u00e9s.

    Dans un environnement POSIX l'interaction avec le syst\u00e8me s'effectue pour la plupart du temps via un terminal. Le programme utilis\u00e9 pour interagir avec le syst\u00e8me d'exploitation est appel\u00e9 un interpr\u00e9teur de commande. Sous Windows vous utilisez le programme cmd.exe ou PowerShell.exe. Sous Linux vous utilisez tr\u00e8s souvent un d\u00e9riv\u00e9 de Bourne shell nom \u00e9ponyme de Stephen Bourne et apparu en 1979. La compatibilit\u00e9 est toujours maintenue aujourd'hui via son successeur Bash dont le nom est un acronyme de Bourne-again shell.

    Bash est \u00e9crit en C et les sources sont naturellement disponibles sur internet. Lorsque vous lancez Bash, vous aurez un simple prompt\u2009:

    $\n

    Ce dernier vous invite \u00e0 taper une commande laquelle est le plus souvent le nom d'un programme. Voici un exemple\u2009:

    $ cat foo.txt | hexdump -C -n100\n
    ", "tags": ["PowerShell.exe", "cmd.exe"]}, {"location": "tools/dev/bash/#navigation", "title": "Navigation", "text": "

    Pour naviguer dans l'arborescence, le programme cd est utilis\u00e9. Il est l'acronyme de change directory. Ce programme prend en argument un chemin relatif ou absolu.

    $ cd /usr\n/usr$ cd bin\n/usr/bin$ cd .\n/usr/bin$ cd ..\n/usr/$ cd /var/tmp\n/var/tmp$ cd\n~$\n

    La derni\u00e8re commande est singuli\u00e8re\u2009: si cd est appel\u00e9 sans argument, il nous ram\u00e8ne dans notre r\u00e9pertoire personnel nomm\u00e9 home et abbr\u00e9g\u00e9 ~.

    "}, {"location": "tools/dev/bash/#affichage", "title": "Affichage", "text": "

    L'affichage du contenu courant de l'arborescence est possible avec le programme ls pour list structure.

    $ ls /usr/bin/as*\n/usr/bin/as                 /usr/bin/asciitopgm     /usr/bin/assistant\n/usr/bin/asan_symbolize     /usr/bin/aspell         /usr/bin/asy\n/usr/bin/asan_symbolize-10  /usr/bin/aspell-import\n$ ls -al /usr/bin/as*\n-rwxr-xr-x 1 root root  38K 2020-04-20 07:12 /usr/bin/asan_symbolize-10\n-rwxr-xr-x 1 root root 9.9K 2016-04-23 13:53 /usr/bin/asciitopgm\n-rwxr-xr-x 1 root root 167K 2020-03-22 16:33 /usr/bin/aspell\n-rwxr-xr-x 1 root root 2.0K 2020-03-22 16:33 /usr/bin/aspell-import\nlrwxrwxrwx 1 root root    9 2020-03-22 16:55 /usr/bin/assistant -> qtchooser\n-rwxr-xr-x 1 root root 4.3M 2020-02-10 15:52 /usr/bin/asy\n

    On utilise souvent les options a (pour all) et l (pour long) permettant d'afficher les r\u00e9sultats avec plus de d\u00e9tails. Dans l'ordre on peut lire les permissions du fichier, le propri\u00e9taire, le groupe, la taille du fichier, sa date de derni\u00e8re modification et enfin son nom.

    "}, {"location": "tools/dev/bash/#pipe", "title": "Pipe", "text": "

    Le signe pipe | permet de rediriger le flux de sortie d'un programme vers le flux d'entr\u00e9e d'un autre programme et ainsi les ex\u00e9cuter \u00e0 la cha\u00eene.

    % code-block\u2009::text % % $ echo \u00ab\u2009Bonjour\u2009\u00bb | cowsay

    Il se peut que vous souhaitiez rediriger la sortie d'erreur vers la sortie standard et ainsi concat\u00e9ner les deux flux sur l'entr\u00e9e standard d'un autre programme.

    % code-block\u2009::text % % $ echo \u00ab\u2009Bonjour\u2009\u00bb 2>&1 | cowsay

    "}, {"location": "tools/dev/config/", "title": "Fichiers de configuration", "text": ""}, {"location": "tools/dev/config/#introduction", "title": "Introduction", "text": "

    Dans un projet, vous aurez tr\u00e8s souvent un tas de fichiers de configuration. Ils commencent g\u00e9n\u00e9ralement par un point (.) pour les cacher dans le r\u00e9pertoire. C'est la mani\u00e8re dont les fichiers sont cach\u00e9s dans les syst\u00e8mes de fichiers Unix.

    "}, {"location": "tools/dev/config/#fichiers-populaires", "title": "Fichiers populaires", "text": ""}, {"location": "tools/dev/config/#clang-format", "title": ".clang-format", "text": "

    Ce fichier est au format YAML et contient des directives pour formater votre code automatiquement soit \u00e0 partir de VsCode si vous avez install\u00e9 l'extension Clang-Format et l'ex\u00e9cutable clang-format (sudo apt install -y clang-format). Clang-format est un utilitaire de la suite LLVM, proposant Clang un compilateur alternatif \u00e0 GCC.

    On voit que le texte pass\u00e9 sur stdin (jusqu'\u00e0 EOF) est ensuite format\u00e9 proprement\u2009:

    $ clang-format --style=mozilla <<EOF\n#include <stdio.h>\nint\nmain\n()\n{printf(\"hello, world\\n\");}\nEOF\n#include <stdio.h>\nint\nmain()\n{\nprintf(\"hello, world\\n\");\n}\n

    Par d\u00e9faut clang-format utilise le fichier de configuration nomm\u00e9 .clang-format qu'il trouve.

    Vous pouvez g\u00e9n\u00e9rer votre propre configuration facilement depuis un configurateur tel que clang-format configurator.

    ", "tags": ["stdin"]}, {"location": "tools/dev/config/#editor_config", "title": ".editor_config", "text": "

    Ce fichier au format YAML permet de sp\u00e9cifier des recommandations pour l'\u00e9dition de fichiers sources. Vous pouvez y sp\u00e9cifier le type de fin de lignes CR ou CRLF, le type d'indentation (espaces ou tabulations) et le type d'encodage (ASCII ou UTF-8) pour chaque type de fichiers. EditorConfig est aujourd'hui support\u00e9 par la plupart des \u00e9diteurs de textes qui cherchent automatiquement un fichier de configuration nomm\u00e9 .editor_config.

    Dans Visual Studio Code, il faut installer l'extension EditorConfig for VS Code pour b\u00e9n\u00e9ficier de ce fichier.

    Pour les travaux pratiques, on se contente de sp\u00e9cifier les directives suivantes\u2009:

    root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ncharset = utf-8\n\n[*.{json,yaml}]\nindent_style = space\nindent_size = 2\n\n[Makefile]\nindent_style = tab\n\n[*.{cmd,bat}]\nend_of_line = crlf\n
    "}, {"location": "tools/dev/config/#gitattributes", "title": ".gitattributes", "text": "

    Ce fichier permet \u00e0 Git de r\u00e9soudre certains probl\u00e8mes dans l'\u00e9dition de fichiers sous Windows ou POSIX lorsque le type de fichiers n'a pas le bon format. On se contente de d\u00e9finir quelle sera la fin de ligne standard pour certains types de fichiers\u2009:

    * text=auto eol=lf\n*.{cmd,[cC][mM][dD]} text eol=crlf\n*.{bat,[bB][aA][tT]} text eol=crlf\n
    "}, {"location": "tools/dev/config/#gitignore", "title": ".gitignore", "text": "

    Ce fichier de configuration permet \u00e0 Git d'ignorer par d\u00e9faut certains fichiers et ainsi \u00e9viter qu'ils ne soient ajout\u00e9s par erreur au r\u00e9f\u00e9rentiel. Ici, on souhaite \u00e9viter d'ajouter les fichiers objets .o et les ex\u00e9cutables *.out :

    *.out\n*.o\n*.d\n*.so\n*.lib\n
    "}, {"location": "tools/dev/config/#vscodelaunchjson", "title": ".vscode/launch.json", "text": "

    Ce fichier permet \u00e0 Visual Studio Code de savoir comment ex\u00e9cuter le programme en mode d\u00e9bogue. Il est au format JSON. Les lignes importantes sont program qui contient le nom de l'ex\u00e9cutable \u00e0 lancer args qui sp\u00e9cifie les arguments pass\u00e9s \u00e0 ce programme et MiMode qui est le nom du d\u00e9bogueur que vous utiliserez. Par d\u00e9faut nous utilisons GDB.

    {\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Launch Main\",\n            \"type\": \"cppdbg\",\n            \"request\": \"launch\",\n            \"program\": \"${workspaceFolder}/a.out\",\n            \"args\": [\"--foobar\", \"filename\", \"<<<\", \"hello, world\"],\n            \"stopAtEntry\": true,\n            \"cwd\": \"${workspaceFolder}\",\n            \"environment\": [],\n            \"externalConsole\": false,\n            \"MIMode\": \"gdb\",\n            \"setupCommands\": [\n                {\n                    \"description\": \"Enable pretty-printing for gdb\",\n                    \"text\": \"-enable-pretty-printing\",\n                    \"ignoreFailures\": true\n                }\n            ],\n            \"preLaunchTask\": \"Build Main\"\n        }\n    ]\n}\n
    ", "tags": ["program", "MiMode", "args"]}, {"location": "tools/dev/config/#vscodetasksjson", "title": ".vscode/tasks.json", "text": "

    Ce fichier contient les directives de compilation utilis\u00e9es par Visual Studio Code lors de l'ex\u00e9cution de la t\u00e2che build accessible par la touche <F5>. On y voit que la commande ex\u00e9cut\u00e9e est make. Donc la mani\u00e8re dont l'ex\u00e9cutable est g\u00e9n\u00e9r\u00e9 d\u00e9pend d'un Makefile.

    {\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Build Main\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            }\n        },\n        {\n            \"label\": \"Clean\",\n            \"type\": \"shell\",\n            \"command\": \"make clean\"\n        }\n    ]\n}\n
    ", "tags": ["Makefile", "make"]}, {"location": "tools/dev/docker/", "title": "Docker", "text": ""}, {"location": "tools/dev/docker/#introduction", "title": "Introduction", "text": "

    Docker

    Docker est une plateforme logicielle qui permet de cr\u00e9er, de tester et de d\u00e9ployer des applications dans des conteneurs logiciels. Un conteneur est une unit\u00e9 logicielle qui contient une application et toutes ses d\u00e9pendances. Les conteneurs sont l\u00e9gers, portables et auto-suffisants. Ils sont ex\u00e9cut\u00e9s dans un environnement isol\u00e9 de l'h\u00f4te.

    C'est une alternative \u00e0 la virtualisation (comme VirtualBox ou VMware) vue comme beaucoup plus l\u00e9g\u00e8re et plus rapide. Docker est devenu un outil incontournable pour les d\u00e9veloppeurs et les administrateurs syst\u00e8me.

    Sous Windows, Docker d\u00e9pend de la technologie Hyper-V qui permet de cr\u00e9er des machines virtuelles. Sous Linux, Docker utilise les fonctionnalit\u00e9s de virtualisation du noyau Linux. Dans les versions r\u00e9centes, Docker est bas\u00e9 sur WSL 2 (Windows Subsystem for Linux 2) qui permet d'ex\u00e9cuter un noyau Linux complet dans Windows.

    Pour utiliser docker il y a deux notions \u00e0 comprendre\u2009:

    • Images : une image est un mod\u00e8le de conteneur. C'est un fichier binaire qui contient toutes les d\u00e9pendances n\u00e9cessaires pour ex\u00e9cuter une application. Les images sont stock\u00e9es dans un registre (comme Docker Hub) et peuvent \u00eatre partag\u00e9es.
    • Conteneurs : un conteneur est une instance d'une image. C'est une application en cours d'ex\u00e9cution avec son propre environnement isol\u00e9. Les conteneurs peuvent \u00eatre cr\u00e9\u00e9s, d\u00e9marr\u00e9s, arr\u00eat\u00e9s, d\u00e9plac\u00e9s et supprim\u00e9s.

    Imaginons que nous souhaitions ex\u00e9cuter une application Python qui utilise numpy sur une machine qui n'a pas Python d'install\u00e9. Nous pourrions cr\u00e9er une image Docker qui contient Python et numpy, puis ex\u00e9cuter un conteneur bas\u00e9 sur cette image. L'application fonctionnera sans probl\u00e8me, m\u00eame si Python n'est pas install\u00e9 sur la machine h\u00f4te.

    docker run python:3.8-slim python -c \"import numpy; print(numpy.__version__)\"\n
    "}, {"location": "tools/dev/environment/", "title": "L'environnement de travail", "text": "

    Vous \u00eates \u00e9tudiante ou \u00e9tudiant et vous \u00eates perdus avec l'utilisation de Python, LaTeX, Git, etc., sous Windows, Linux ou encore Docker Ce document est fait pour vous. Il vous guidera dans l'installation et l'utilisation de ces outils. L'objectif est de comprendre les avantages et les inconv\u00e9nients de chaque outil et de vous permettre de les utiliser de mani\u00e8re efficace.

    "}, {"location": "tools/dev/environment/#preambule", "title": "Pr\u00e9ambule", "text": "

    Les applications utilis\u00e9es typiquement par un ing\u00e9nieur aujourd'hui sont Python, Git, GCC, LaTeX ou m\u00eame Docker. Ces applications ont un point commun, c'est qu'elles ont d'abord \u00e9t\u00e9 \u00e9crites pour un environnement POSIX (Unix).

    POSIX est une norme internationale (IEEE 1003) qui d\u00e9finit l'interface de programmation d'un syst\u00e8me d'exploitation. Elle est bas\u00e9e sur UNIX. Elle est utilis\u00e9e pour les syst\u00e8mes d'exploitation de type UNIX. Windows n'est pas un syst\u00e8me qui respecte cette norme, ce qui complique l'utilisation de ces applications.

    Afin de pouvoir porter Python ou Git sous Windows, il a fallu ajouter une couche d'abstraction pour rendre compatible le monde Linux avec le monde Windows. Historiquement le projet Cygwin n\u00e9 en 1995 a \u00e9t\u00e9 le premier \u00e0 proposer une telle couche. Il s'agissait d'un environnement POSIX pour Windows muni d'un terminal, d'un gestionnaire de paquets et d'une biblioth\u00e8que d'\u00e9mulation POSIX. Les outils en ligne de commande type ls, cat ou m\u00eame grep \u00e9taient propos\u00e9s. N\u00e9anmoins, Cygwin n'\u00e9tait pas parfait, il n\u00e9cessitait son propre environnement de travail et n'\u00e9tait pas bien int\u00e9gr\u00e9 \u00e0 Windows. Le projet MSYS a \u00e9t\u00e9 cr\u00e9\u00e9 en 2003 pour pallier \u00e0 ces probl\u00e8mes. Il s'agissait d'une couche d'abstraction POSIX pour Windows qui se voulait plus l\u00e9g\u00e8re. Au lieu de compiler des applications Linux qui devaient imp\u00e9rativement \u00eatre lanc\u00e9es sous Cygwin, MSYS int\u00e9grait la couche d'abstraction dans les applications elles-m\u00eames, ces derni\u00e8res \u00e9taient compil\u00e9es en .exe et pouvaient \u00eatre lanc\u00e9es directement depuis l'explorateur Windows. MSYS a \u00e9t\u00e9 un franc succ\u00e8s et a \u00e9t\u00e9 int\u00e9gr\u00e9 dans le projet MinGW (Minimalist GNU for Windows) qui est un portage de GCC pour Windows.

    Lorsque vous installez Git sous Windows et que vous visitez l'emplacement d'installation (C:\\Program Files\\Git), vous verrez des dossiers aux noms compatibles POSIX comme bin, etc, lib, usr, etc. Le dossier bin qui contient les ex\u00e9cutables contient bash qui n'est rien d'autre que le terminal utilis\u00e9 sous Linux. La raison est que Git ou m\u00eame Python sont des outils avant tout d\u00e9velopp\u00e9s pour les environnements Unix/Linux.

    Le choix de l'environnement de travail est donc compliqu\u00e9. Faut-il travailler sous Windows avec les limitations que le portage des applications Linux implique ou faut-il travailler sous Linux directement\u2009? Un ing\u00e9nieur reste aujourd'hui attach\u00e9 au monde Windows car il d\u00e9pend de logiciels comme SolidWorks ou Altium Designer qui ne sont pas disponibles sous Linux. Il est donc n\u00e9cessaire de trouver un compromis.

    En 2016, Microsoft a annonc\u00e9 le support de Linux dans Windows 10. Il s'agissait d'une couche d'abstraction qui permettait de faire tourner des applications Linux directement sous Windows. Cette couche d'abstraction s'appelle Windows Subsystem for Linux (WSL). Elle est bas\u00e9e sur une technologie de virtualisation de conteneurs. WSL a \u00e9t\u00e9 un franc succ\u00e8s et il a tr\u00e8s vite \u00e9t\u00e9 adopt\u00e9 par les d\u00e9veloppeurs Web. Il fut de surcro\u00eet propos\u00e9 comme une alternative \u00e0 Cygwin et MSYS.

    WSL a \u00e9t\u00e9 am\u00e9lior\u00e9 au fil des ann\u00e9es et en 2019, Microsoft a annonc\u00e9 WSL 2. WSL 2 est bas\u00e9 sur une technologie de virtualisation de type 2 (hyperviseur) et non plus de type 1 (noyau Linux). WSL 2 est donc plus performant que WSL 1. Il est possible de faire tourner un serveur web ou m\u00eame un serveur de base de donn\u00e9es directement sous WSL 2. WSL 2 est maintenant une alternative cr\u00e9dible \u00e0 Linux.

    Il rend possible de travailler sous Windows et de faire tourner des applications Linux directement sous Windows.

    Le choix donn\u00e9 aux ing\u00e9nieurs est donc\u2009:

    1. Choix facile, mais source d'incoh\u00e9rences: Travailler exclusivement sous Windows et installer Python, Git, LaTeX sous Windows. L'inconv\u00e9nient est que chacune de ses applications ne profite pas d'une unit\u00e9 de travail commune. \u00c0 force d'installer des applications, vous aurez dans votre syst\u00e8me plusieurs installations de Python, plusieurs ex\u00e9cutables Git ce qui peut vite devenir compliqu\u00e9 \u00e0 g\u00e9rer.
    2. Choix plus difficile, mais offrant davantage de flexibilit\u00e9: Travailler sous WSL 2 et de faire tourner Python, Git, LaTeX sous Linux. L'avantage est que vous aurez une unit\u00e9 de travail commune. Vous pourrez installer des applications Linux directement depuis le gestionnaire de paquets de votre distribution Linux. N\u00e9anmoins vous devrez vous familiariser avec la ligne de commande Linux.
    ", "tags": ["etc", "Python", "usr", "cat", "LaTeX", "SolidWorks", "lib", "grep", "Git", "bin", "bash", "POSIX"]}, {"location": "tools/dev/environment/#le-terminal", "title": "Le terminal", "text": "

    Historiquement sous Windows, le terminal \u00e9tait une application graphique appel\u00e9e cmd. Elle n'a pas \u00e9volu\u00e9 depuis Windows NT. Son interface est limit\u00e9e \u00e0 un nombre fini de caract\u00e8res par ligne et ne supportait que quelques couleurs. Elle ne supportait pas les caract\u00e8res Unicode et ne supportait pas les raccourcis clavier comme Ctrl+C ou Ctrl+V.

    Heureusement Windows a \u00e9volu\u00e9 et propose PowerShell. PowerShell est un terminal plus moderne qui supporte les couleurs, les raccourcis clavier, les caract\u00e8res Unicode, etc. PowerShell est un terminal plus puissant que cmd.

    L'interface du terminal \u00e9tait \u00e9galement rudimentaire (pas d'onglets, pas de s\u00e9parateurs, etc.). Heureusement Windows propose depuis 2019 Windows Terminal. Windows Terminal est un terminal moderne multionglets qui supporte plusieurs terminaux (cmd, PowerShell, WSL, etc.). S'il n'est pas install\u00e9, vous pouvez le faire via le Microsoft Store.

    Interface de cmd.exe dans Windows Terminal

    Interface de PowerShell dans Windows Terminal

    Interface de Ubuntu dans Windows Terminal

    ", "tags": ["cmd"]}, {"location": "tools/dev/environment/#variables-denvironnement", "title": "Variables d'environnement", "text": "

    Que vous soyez sous POSIX ou Windows, votre syst\u00e8me d'exploitation dispose de variables d'environnement. Il s'agit de variables qui sont accessibles par toutes les applications. Elles sont utilis\u00e9es pour stocker des informations comme le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable, le nom de l'utilisateur, le r\u00e9pertoire de travail, etc.

    La variable la plus importante est PATH. Elle contient une liste de chemins d'acc\u00e8s aux ex\u00e9cutables. Lorsque vous tapez une commande dans un terminal, le syst\u00e8me d'exploitation parcourt les chemins d'acc\u00e8s de la variable PATH pour trouver l'ex\u00e9cutable correspondant \u00e0 la commande. Si vous avez install\u00e9 Python, Git, LaTeX, etc., dans des r\u00e9pertoires diff\u00e9rents, il est n\u00e9cessaire de les ajouter \u00e0 la variable PATH. Parfois les installateurs le font automatiquement, parfois non. Il est donc n\u00e9cessaire de v\u00e9rifier manuellement.

    Une variable d'environnement n'est propag\u00e9e \u00e0 un processus que si ce dernier est lanc\u00e9 apr\u00e8s la modification de la variable. Si vous modifiez la variable, PATH les processus d\u00e9j\u00e0 lanc\u00e9s ne verront pas la modification. Il est n\u00e9cessaire de fermer le terminal et de le rouvrir (relancer Visual Studio Code, votre terminal, etc.).

    Parfois si vous installez plusieurs versions d'un m\u00eame logiciel comme Python vous pourriez avoir plusieurs variables d'environnement qui pointent vers des versions diff\u00e9rentes de Python. C'est une source de confusion et c'est un probl\u00e8me fr\u00e9quent sous Windows. Vous pouvez v\u00e9rifier quel est le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable avec la commande where sous Windows et which sous Linux.

    Linux/WSL/MacOSWindows CMDPowerShell
    $ which python\n/usr/bin/python\n
    PS C:\\> where python\nC:\\Python39\\python.exe\n
    PS C:\\> Get-Command python\n
    ", "tags": ["Python", "where", "which", "PATH"]}, {"location": "tools/dev/environment/#latex", "title": "LaTeX", "text": "

    Sous Linux/WSL, le plus simple est d'installer LaTeX avec le gestionnaire de paquets de votre distribution Linux. Ouvrez un terminal et tapez la commande suivante\u2009:

    sudo apt install texlive-full latexmk\n

    Sous Windows c'est plus compliqu\u00e9. Il existe plusieurs distributions LaTeX pour Windows. La plus courante est MiKTeX. T\u00e9l\u00e9chargez l'installateur et suivez les instructions.

    "}, {"location": "tools/dev/environment/#commandes-principales", "title": "Commandes principales", "text": ""}, {"location": "tools/dev/environment/#gcc", "title": "GCC", "text": "Commande Description gcc Compilateur C g++ Compilateur C++ make Gestionnaire de compilation

    Pour compiler un programme\u2009:

    CC++Plusieurs fichiers CCompilation s\u00e9par\u00e9e
    gcc -o hello hello.c\n
    g++ -o hello hello.cpp\n
    gcc -o hello main.c functions.c\n
    gcc -c functions.c\ngcc -c main.c\ngcc -o hello main.o functions.o\n
    ", "tags": ["make", "gcc"]}, {"location": "tools/dev/environment/#linuxwsl", "title": "Linux/WSL", "text": "Commande Description ls Liste les fichiers du r\u00e9pertoire courant cd Change de r\u00e9pertoire pwd Affiche le r\u00e9pertoire courant cat Affiche le contenu d'un fichier less Affiche le contenu d'un fichier page par page grep Recherche une cha\u00eene de caract\u00e8res dans un fichier find Recherche un fichier dans un r\u00e9pertoire man Affiche le manuel d'une commande which Affiche le chemin d'acc\u00e8s \u00e0 un ex\u00e9cutable", "tags": ["pwd", "cat", "man", "which", "grep", "find", "less"]}, {"location": "tools/dev/environment/#afficher-les-fichiers-du-repertoire-courant", "title": "Afficher les fichiers du r\u00e9pertoire courant", "text": "
    ls -al # Par noms\nls -lt # Par date de modification\nls -lh # En format lisible\n
    "}, {"location": "tools/dev/environment/#python", "title": "Python", "text": "Commande Description python Lance l'interpr\u00e9teur Python pip Gestionnaire de paquets Python ipython Lance l'interpr\u00e9teur IPython jupyter lab Lance l'environnement Jupyter Lab", "tags": ["ipython", "python", "pip"]}, {"location": "tools/dev/environment/#latex_1", "title": "LaTeX", "text": "Commande Description latexmk -xelatex Compile un document LaTeX"}, {"location": "tools/dev/git/", "title": "Git", "text": "

    Fanart Git

    "}, {"location": "tools/dev/git/#introduction", "title": "Introduction", "text": "

    Git est un outil de gestion de versions. Il a \u00e9t\u00e9 invent\u00e9 par Linus Torvalds en 2005 pour g\u00e9rer le d\u00e9veloppement du noyau Linux qui contient des millions de lignes de code devant \u00eatre modifi\u00e9es par des centaines de milliers de d\u00e9veloppeurs. Git a \u00e9t\u00e9 con\u00e7u pour \u00eatre rapide, efficace et pour g\u00e9rer des projets de toutes tailles. Il est aujourd'hui le syst\u00e8me de gestion de version le plus utilis\u00e9 au monde.

    Git est la suite logique des outils comme CVS ou Subversion qui \u00e9taient utilis\u00e9s pour g\u00e9rer des projets de d\u00e9veloppement de logiciels.

    Git est avant tout un outil en ligne de commande. Son utilisation passe par la commande git suivi d'une sous-commande (clone, pull, push, commit, etc.). Il est possible de l'utiliser avec une interface graphique mais l'interface en ligne de commande est plus puissante, plus rapide et plus flexible.

    Un d\u00e9p\u00f4t (r\u00e9f\u00e9rentiel) Git est un dossier qui contient un dossier cach\u00e9 .git. Ce dossier contient l'historique des modifications du projet. Ne supprimez pas ce dossier, vous perdriez l'historique de votre projet. Lorsque vous faites des commit (sauvegarde incr\u00e9mentationnelle), Git enregistre les modifications dans ce dossier cach\u00e9 et lorsque vous faites un push (envoi des modifications sur un serveur distant), Git envoie les modifications \u00e0 un serveur distant (GitHub, GitLab, Bitbucket, etc.).

    Par cons\u00e9quent, l'utilisation de Git est intrinc\u00e8quement li\u00e9e \u00e0 l'utilisation d'un serveur distant. Il est possible de travailler en local mais l'int\u00e9r\u00eat de Git est de pouvoir travailler en \u00e9quipe. Il est possible de travailler sur une branche (version parall\u00e8le du projet) et de fusionner les modifications avec la branche principale (master).

    Ceci implique de comprendre comment Git communique avec un serveur distant. Notons qu'il est possible de travailler avec plusieurs serveurs distants. Ils se configure avec la commande git remote. Deux protocoles de communication existent\u2009: SSH et HTTPS. Le protocole SSH est plus s\u00e9curis\u00e9 que HTTPS mais n\u00e9cessite l'\u00e9change de cl\u00e9s cryptographiques. Le protocole HTTPS est plus simple \u00e0 mettre en place mais demande un mot de passe ou un jeton d'authentification \u00e0 chaque communication avec le serveur distant. La solution recommand\u00e9e est d'utiliser SSH.

    ", "tags": ["HTTPS", "pull", "commit", "CVS", "clone", "git", "push", "SSH"]}, {"location": "tools/dev/git/#installation-de-git", "title": "Installation de Git", "text": "LinuxWindowsMacOS

    Sous Linux, et particuli\u00e8rement Ubuntu, Git est probablement d\u00e9j\u00e0 install\u00e9 par d\u00e9faut. Si ce n'est pas le cas, avec Ubuntu ouvrez un terminal Bash et tapez la commande suivante\u2009:

    sudo apt install git\n

    [!NOTE] Selon la distribution Linux que vous utilisez, les gestionnaires de paquets n'ont pas le m\u00eame nom. Sous Fedora, le gestionnaire de paquets est dnf et sous Arch Linux, le gestionnaire de paquets est pacman. N\u00e9anmoins la commande reste tr\u00e8s similaire.

    Pour installer Git sous Windows, le plus simple est d'utiliser le nouveau gestionnaire de paquet de Windows appel\u00e9 winget. Ouvrez un terminal PowerShell et tapez la commande suivante issue de Winget Git/Git

    winget install -e --id Git.Git\n

    Sous MacOS, Git est probablement d\u00e9j\u00e0 install\u00e9. Si ce n'est pas le cas, vous pouvez installer Git avec Homebrew en tapant la commande suivante dans un terminal\u2009:

    brew install git\n
    ", "tags": ["winget", "pacman", "dnf", "PowerShell"]}, {"location": "tools/dev/git/#configuration-de-git", "title": "Configuration de Git", "text": ""}, {"location": "tools/dev/git/#utilisateur-et-adresse-e-mail", "title": "Utilisateur et adresse e-mail", "text": "

    La premi\u00e8re chose \u00e0 faire apr\u00e8s avoir install\u00e9 Git est de le configurer. Ouvrez un terminal et tapez les commandes suivantes en veillant bien \u00e0 remplacer John Doe par votre nom et john.doe@acme.com par votre adresse e-mail.

    git config --global user.name \"John Doe\"\ngit config --global user.email john.doe@acme.com\n

    Ces informations sont utilis\u00e9es pour chaque commit que vous ferez. Elles sont importantes car elles permettent de savoir qui a fait une modification dans le projet. Ne vous trompez pas dans votre nom ou votre adresse e-mail, il est difficile de changer ces informations une fois qu'elles ont \u00e9t\u00e9 enregistr\u00e9es dans un commit, et surtout si elle ont \u00e9t\u00e9 envoy\u00e9es sur un serveur distant.

    "}, {"location": "tools/dev/git/#methode-de-fusion-merge", "title": "M\u00e9thode de fusion (merge)", "text": "

    Lorsque vous utilisez la commande git pull pour r\u00e9cup\u00e9rer les modifications d'un serveur distant, Git peut \u00eatre amen\u00e9 \u00e0 fusionner des modifications. Il est possible de choisir la m\u00e9thode de fusion. Les deux m\u00e9thodes les plus courantes sont merge et rebase. La m\u00e9thode merge est la m\u00e9thode par d\u00e9faut. La m\u00e9thode rebase est plus avanc\u00e9e et est utilis\u00e9e pour r\u00e9\u00e9crire l'historique du projet. Elle est plus propre mais elle peut \u00eatre source de probl\u00e8mes si elle est mal utilis\u00e9e. Git s'attend \u00e0 que vous configuriez laquelle des deux m\u00e9thodes vous pr\u00e9f\u00e9rez. Vous pouvez le faire avec la commande suivante\u2009:

    Pour le rebase\u2009:

    git config --global pull.rebase true\n

    Pour le merge\u2009:

    git config --global pull.rebase false\n

    Note

    La m\u00e9thode rebase est plus propre mais elle peut \u00eatre source de probl\u00e8mes si elle est mal utilis\u00e9e. Elle est recommand\u00e9e pour les projets personnels mais pas pour les projets en \u00e9quipe. En cas de doute privil\u00e9giez la m\u00e9thode merge.

    ", "tags": ["rebase", "merge"]}, {"location": "tools/dev/git/#affichage-de-lhistorique-des-commits-log", "title": "Affichage de l'historique des commits (log)", "text": "

    Lorsque vous utilisez la commande git log pour afficher l'historique des commits, Git affiche les commits dans un format compact. Il est possible de personnaliser l'affichage de l'historique des commits. Vous pouvez le faire avec la commande suivante\u2009:

    git config --global alias.lg \"log -n30 --boundary --graph --pretty=format:'%C(bold blue)%h%C(bold green)%<|(20)% ar%C(reset)%C(white)% s %C(dim white)-% an%C(reset)%C(auto)% d%C(bold red)% N' --abbrev-commit --date=relative\"\n

    D\u00e8s lors vous utiliserez le raccourcis lg pour afficher l'historique des commits (git lg).

    Pour afficher la date en format ISO 8601, vous pouvez utiliser la commande suivante\u2009:

    git config --global alias.lga \"log -n30 --boundary --graph --pretty=format:'%C(bold blue)%h%C(bold green)%<|(20)% ai%C(reset)%C(white)% s %C(dim white)-% an%C(reset)%C(auto)% d%C(bold red)' --abbrev-commit --date=iso\"\n
    "}, {"location": "tools/dev/git/#configuration-ssh", "title": "Configuration SSH", "text": "

    Comme nous l'avons vu, Git peut communiquer avec un serveur distant en utilisant le protocole SSH. Pour cela, il est n\u00e9cessaire de configurer une cl\u00e9 cryptographique.

    La premi\u00e8re chose \u00e0 faire est de v\u00e9rifier si vous avez d\u00e9j\u00e0 une cl\u00e9 SSH. Ouvrez un terminal et tapez la commande suivante\u2009:

    $ ls -al ~/.ssh/*.pub\n-rw-r--r-- 1 ycr ycr 393 2023-09-06 08:38 /home/ycr/.ssh/id_rsa.pub\n

    Le fichier id_rsa.pub est votre cl\u00e9 publique. Si vous ne voyez pas ce fichier, c'est que vous n'avez pas de cl\u00e9 SSH. Vous pouvez en g\u00e9n\u00e9rer une avec la commande suivante\u2009:

    ssh-keygen\n

    Sous Windows, la commande est la m\u00eame mais vous devez lancer Git Bash pour l'ex\u00e9cuter. Vous pouvez lancer Git Bash en tapant Git Bash dans le menu de recherche de Windows.

    ", "tags": ["id_rsa.pub", "SSH"]}, {"location": "tools/dev/git/#configuration-de-github", "title": "Configuration de GitHub", "text": "

    Si vous utilisez GitHub, il est n\u00e9cessaire de configurer votre cl\u00e9 SSH dans votre compte GitHub. Pour cela, ouvrez un terminal et tapez la commande suivante\u2009:

    $ cat ~/.ssh/id_rsa.pub\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+yNp7af6zI8NINIFX1aRj+nzKksZ6XzBSkgA/\niuPpYIGz5SSZOkwkvN0DnX8J42DcuEK/mnu3+f9Wh746823gxhXqt+7Wv9z9DJ9O9qrsYlnxIMip\noqepE/Xt+jE5Yv8ullIdsvZdzY611R5DFwrVswslz9OdmpH6nWCmnY/cGZva79ngdcvJLKFk++fl\n+Be1xshWt24svawRH7Fdxn8VyUKmP2Twy6iMo3MT9xGe5leV1CiTXfkzLYntNV50/dtzQN+pwcwR\nBdXBP9FdO9+IzieY6bUGttT6t2VcWoK6jFF+i94Chl/FeGvRU1X/QzSP3SYT2biNRNmznSIa2VD\nycr@heig-vd\n

    Copiez la cl\u00e9 publique et collez-la dans votre compte GitHub. Pour cela, ouvrez votre navigateur et allez sur GitHub. Connectez-vous \u00e0 votre compte et cliquez sur votre photo de profil en haut \u00e0 droite. Cliquez sur Settings puis sur SSH and GPG keys. Cliquez sur New SSH key et collez votre cl\u00e9 publique dans le champ Key. Donnez un nom \u00e0 votre cl\u00e9 et cliquez sur Add SSH key.

    ", "tags": ["Key", "Settings"]}, {"location": "tools/dev/git/#commandes-utiles", "title": "Commandes utiles", "text": "Commandes Git les plus courantes Commande Description git init Initialise un d\u00e9p\u00f4t Git dans le r\u00e9pertoire courant git clone <address> Clone un d\u00e9p\u00f4t distant dans le r\u00e9pertoire courant git status Affiche l'\u00e9tat du d\u00e9p\u00f4t git add <file> Ajoute des fichiers \u00e0 l'index git commit -am \"message\" Enregistre les modifications dans l'historique du d\u00e9p\u00f4t git pull R\u00e9cup\u00e8re les modifications du serveur distant git push Envoie les modifications sur le serveur distant git log Affiche l'historique des commits git lg Affiche l'historique des commits de mani\u00e8re plus lisible"}, {"location": "tools/dev/windows/", "title": "Windows", "text": ""}, {"location": "tools/dev/windows/#msvc-microsoft-visual-c", "title": "MSVC (Microsoft Visual C++)", "text": "

    MSVC est le compilateur C++ de Microsoft. Il est inclus dans Visual Studio et est souvent utilis\u00e9 pour compiler des programmes Windows. Historiquement, MSVC a toujours \u00e9t\u00e9 un peu en retrait par rapport au support des standards modernes du langage C, notamment C99, C11, C18 et maintenant C23. Alors que le compilateur suit de tr\u00e8s pr\u00e8s les standards C++, il ne supporte pas aussi bien les \u00e9volutions du langage C.

    Pour installer MSVC, vous devez t\u00e9l\u00e9charger Visual Studio depuis le site web de Microsoft. L'installation de Visual Studio est assez lourde, car elle installe de nombreux composants qui ne sont pas n\u00e9cessaires pour la compilation de programmes C/C++.

    "}, {"location": "tools/dev/windows/#msys2", "title": "MSYS2", "text": "

    MSYS2 est un environnement de d\u00e9veloppement qui fournit un shell Unix-like et des outils GNU pour Windows. Il est bas\u00e9 sur Cygwin et utilise le gestionnaire de paquets Pacman de Arch Linux.

    MSYS2 est un excellent choix pour compiler des programmes en C/C++ qui peuvent \u00eatre ex\u00e9cut\u00e9s nativement sous Windows. Le compilateur MinGW-w64 est une variante de GCC qui permet de compiler des binaires au format PE (Portable Executable) pour Windows.

    L'installation de MSYS2 est assez simple. Une approche consiste \u00e0 t\u00e9l\u00e9charger l'ex\u00e9cutables d'installation depuis le site web de MSYS2 et \u00e0 l'ex\u00e9cuter. Ma pr\u00e9f\u00e9rence va pour winget disponible nativement sur Windows 10/11.

    winget install -e --id MSYS2.MSYS2\n

    Une fois install\u00e9, vous pouvez ouvrir le programme MSYS2 MINGW64 qui tourne dans un terminal mintty un peu archa\u00efque. Il est pr\u00e9f\u00e9rable d'utiliser Windows Terminal. Pour ce faire il faut configurer un profil pour MSYS2.

    1. Ouvrir Windows Terminal
    2. Utiliser ++Ctrl+Shift+,++ pour ouvrir le fichier de configuration JSON
    3. Allez \u00e0 la section \"profiles\" et ajouter un nouvel \u00e9l\u00e9ment pour MSYS2\u2009:

      {\n    \"colorScheme\": \"Campbell\",\n    \"commandline\": \"\\\"C:\\\\\\\\msys64\\\\\\\\usr\\\\\\\\bin\\\\\\\\bash.exe\\\" --login -i -c \\\"cd ~ && /usr/bin/env MSYSTEM=MINGW64 bash\\\"\\r\",\n    \"guid\": \"{cffe7ad8-1ccd-43ea-99d9-d3dff62b8a02}\",\n    \"hidden\": false,\n    \"icon\": \"C:\\\\msys64\\\\mingw64.ico\",\n    \"name\": \"MinGW\",\n    \"startingDirectory\": \"~\"\n}\n
    4. Attention \u00e0 bien adapter le chemin et veillez \u00e0 ce que le fichier JSON soit bien form\u00e9, c'est \u00e0 dire que les virgules soient bien plac\u00e9es.

    5. Sauvegarder le fichier JSON et fermer Windows Terminal
    6. Ouvrir Windows Terminal et s\u00e9lectionner le profil MinGW dans le menu d\u00e9roulant.

    Le terminal que vous avez est tr\u00e8s similaire \u00e0 celui de WSL. L'installation de paquets est sensiblement diff\u00e9rente de celle de apt ou yum. Pour installer un paquet, il suffit de lancer la commande pacman -S nom-du-paquet. Par exemple, pour installer git, il suffit de lancer la commande suivante\u2009:

    pacman -S git\n

    Pour rechercher un paquet, vous pouvez utiliser la commande pacman -Ss nom-du-paquet.

    Afin de pouvoir r\u00e9cup\u00e9rer des projets GitHub avec votre cl\u00e9 SSH, vous devez soit\u2009:

    1. G\u00e9n\u00e9rer une nouvelle cl\u00e9 SSH pour MSYS2 et l'ajouter \u00e0 votre compte GitHub.
    2. Utiliser la cl\u00e9 SSH existante en la copiant dans le dossier ~/.ssh de MSYS2.

    La premi\u00e8re option est pr\u00e9f\u00e9rable. Pour g\u00e9n\u00e9rer une nouvelle cl\u00e9 SSH, vous pouvez utiliser les commande suivantes.

    ssh-keygen\ncat ~/.ssh/id_rsa.pub\n
    ", "tags": ["winget", "apt", "git", "mintty", "yum"]}, {"location": "tools/dev/wsl/", "title": "Windows Subsystem for Linux", "text": "

    Windows Subsystem for Linux est un outil qui permet d'ex\u00e9cuter des applications Linux sur Windows. Il est disponible sur Windows 10 et Windows Server 2019.

    Face \u00e0 l'augmentation de la popularit\u00e9 de Linux, Microsoft a d\u00e9cid\u00e9 de cr\u00e9er un outil permettant d'ex\u00e9cuter des applications Linux sur Windows. Cela permet aux d\u00e9veloppeurs de travailler sur des projets Linux sans avoir \u00e0 quitter Windows.

    Cela tombe bien, la plupart des outils de d\u00e9veloppement sont disponibles sur Linux. Par exemple, vous pouvez utiliser des outils comme Git, Node.js, Python, Ruby, etc.

    "}, {"location": "tools/dev/wsl/#resolution-des-problemes", "title": "R\u00e9solution des probl\u00e8mes", "text": ""}, {"location": "tools/dev/wsl/#jai-oublie-mon-mot-de-passe", "title": "J'ai oubli\u00e9 mon mot de passe", "text": "

    Si vous \u00eates sur un vrai linux et que vous avez oubli\u00e9 votre mot de passe utilisateur, vous pouvez utiliser le compte root, et si vous avez oubli\u00e9 le mot de passe de root vous \u00eates dans la mouise.

    Sur Windows, il n'y a pas de mot de passe root, et pour entrer dans ce mode voici les \u00e9tapes\u2009:

    1. Ex\u00e9cutez une fen\u00eatre cmd.exe en tant qu'administrateur
    2. Ex\u00e9cutez wsl -l pour lister les distributions WSL install\u00e9es
    3. Ex\u00e9cutez la commande wsl -d nom_distribution -u root (remplacez nom_distribution par le nom de votre distribution). Si vous avez install\u00e9 Ubuntu-24.04, ce sera wsl -d Ubuntu-24.04 -u root
    4. Vous \u00eates maintenant dans le mode root et vous pouvez changer le mot de passe de n'importe quel utilisateur avec passwd nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur)
    5. Si ne vous rappellez pas du nom de l'utilisateur, vous pouvez lister les utilisateurs avec cat /etc/passwd | cut -d: -f1 | tail -n5, votre utilisateur devrait \u00eatre un des cinq derniers.

    Une fois le mot de passe chang\u00e9, vous pouvez vous reconnecter avec votre utilisateur.

    ", "tags": ["cmd.exe", "nom_utilisateur", "root"]}, {"location": "tools/dev/wsl/#en-root-par-defaut", "title": "En root par d\u00e9faut", "text": "

    Si, lorsque vous lancer WSL, vous \u00eates en mode root, c'est que vous avez \u00e9chou\u00e9 \u00e0 cr\u00e9er un utilisateur lors de l'installation de WSL, probablement en annulant la saisie du mot de passe.

    Pour r\u00e9soudre ce probl\u00e8me\u2009:

    1. Ex\u00e9cutez Ubuntu (vous serez en mode super-utilisateur)
    2. Cr\u00e9ez un utilisateur avec adduser nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur), choisissez un nom d'utilisateur simple, en minusucule, sans espace ni caract\u00e8res sp\u00e9ciaux
    3. Suivez les instructions pour d\u00e9finir un mot de passe
    4. Ajoutez l'utilisateur au groupe sudo avec usermod -aG sudo nom_utilisateur
    5. Pour configurer cet utilisateur comme utilisateur par d\u00e9faut, ex\u00e9cutez ubuntu config --default-user nom_utilisateur (remplacez nom_utilisateur par le nom de l'utilisateur)
    ", "tags": ["nom_utilisateur", "root", "sudo"]}, {"location": "tools/editor/vim/", "title": "Vim", "text": "

    Vi IMproved est un \u00e9diteur de texte tr\u00e8s populaire parmi les geek et les d\u00e9veloppeurs. Il est une am\u00e9lioration de Vi, un \u00e9diteur de texte en mode texte tr\u00e8s populaire sur les syst\u00e8mes Unix. Vim est un \u00e9diteur de texte en mode texte, c'est-\u00e0-dire qu'il fonctionne dans un terminal. Il est tr\u00e8s puissant et tr\u00e8s rapide. Il est tr\u00e8s populaire parmi les d\u00e9veloppeurs pour sa rapidit\u00e9 et sa puissance.

    Le fonctionnement de Vi est d\u00e9routant pour les nouveaux utilisateurs, mais une fois que vous avez compris les bases c'est un incontournable depuis un terminal.

    Vim est un \u00e9diteur modal, c'est \u00e0 dire que le clavier peut \u00eatre configur\u00e9 pour avoir des comportements diff\u00e9rents en fonction du mode dans lequel il se trouve. Il y a plusieurs modes dans Vim\u2009:

    • Normal : c'est le mode par d\u00e9faut. Il permet de naviguer dans le fichier, de copier, coller, supprimer, etc.
    • Insertion : dans ce mode, vous pouvez taper du texte.
    • Visual : dans ce mode, vous pouvez s\u00e9lectionner du texte.
    • Commande : dans ce mode, vous pouvez taper des commandes.
    • S\u00e9lection : dans ce mode, vous pouvez s\u00e9lectionner du texte avec la souris.
    "}, {"location": "tools/editor/vscode/", "title": "Visual Studio Code", "text": "

    Visual Studio Code est un \u00e9diteur de code source d\u00e9velopp\u00e9 par Microsoft. Il est gratuit, open-source et multiplateforme. Il est tr\u00e8s populaire pour le d\u00e9veloppement de logiciels, notamment pour les langages de programmation tels que C, C++, Python, Java, etc.

    Il s'inscrit dans une tr\u00e8s longue liste d'\u00e9diteurs de code source. La table suivante pr\u00e9sente quelques \u00e9diteurs de code source populaires.

    \u00c9diteurs de code source populaires \u00c9diteur Ann\u00e9e Commentaire Inspir\u00e9 de ed 1971 \u00c9diteur primitif en mode texte d\u00e9fini dans la norme POSIX - Vi 1976 \u00c9diteur en mode texte d\u00e9fini dans la norme POSIX ed Emacs 1976 \u00c9diteur extensible et personnalisable. TECO Vim 1991 Am\u00e9lioration de Vi, tr\u00e8s populaire des geek et devloppeurs Vi Nano 1999 \u00c9diteur simple et convivial en ligne de commande - Sublime Text 2008 \u00c9diteur propri\u00e9taire avec une version gratuite Vim Atom 2014 \u00c9diteur open-source d\u00e9velopp\u00e9 par GitHub Sublime Text Visual Studio Code 2015 \u00c9diteur open-source d\u00e9velopp\u00e9 par Microsoft Atom

    Outre ces \u00e9diteurs on peut citer d'autres \u00e9diteurs de texte tels que Notepad++, TextPad, UltraEdit, etc. Si vous les utilisez, demandez-vous pourquoi...

    "}, {"location": "tools/editor/vscode/#raccourcis-clavier", "title": "Raccourcis clavier", "text": "

    Parmis les tr\u00e8s nombreux raccourcis clavier de Visual Studio Code, vous trouverez ci-dessous une liste des raccourcis les plus utiles.

    Raccourcis clavier de Visual Studio Code Raccourci Description Ctrl+P Ouvrir un fichier (fuzzy search) Ctrl+Shift+P Ouvrir la palette de commandes (fuzzy search) Ctrl+Shift+N Nouvelle fen\u00eatre Ctrl+Shift+F Rechercher dans tous les fichiers Ctrl+Shift+G Ouvrir le contr\u00f4le de code source (pour faire un Git commit) Ctrl+Shift+D Ouvrir le contr\u00f4le de d\u00e9bogage Ctrl+Shift+X Ouvrir le gestionnaire d'extensions Ctrl+Shift+V Ouvrir un aper\u00e7u du fichier Markdown Ctrl+K V Ouvrir un aper\u00e7u c\u00f4te \u00e0 c\u00f4te du fichier Markdown Ctrl+K Z Activer/d\u00e9sactiver le mode Zen (plein \u00e9cran) Ctrl+K S Enregistrer sous... Ctrl+K R Ouvrir le dossier du fichier actuel Ctrl+K Ctrl+O Ouvre un dossier Ctrl+D S\u00e9lectionner le mot suivant (multicurseur) (r\u00e9p\u00e9ter) Ctrl+U Annuler la derni\u00e8re s\u00e9lection Ctrl+J Ouvrir un terminal int\u00e9gr\u00e9 Ctrl+L S\u00e9lectionner la ligne enti\u00e8re (r\u00e9p\u00e9ter) Ctrl+Shift+L S\u00e9lectionner toutes les occurrences du mot s\u00e9lectionn\u00e9 (multicurseur) Alt+Click Ins\u00e9rer un curseur Ctrl+Alt+Up Ins\u00e9rer un curseur au-dessus"}, {"location": "tools/editor/vscode/#installation", "title": "Installation", "text": "

    Pour installer Visual Studio Code, rendez-vous sur la page https://code.visualstudio.com/ et t\u00e9l\u00e9chargez la version correspondant \u00e0 votre syst\u00e8me d'exploitation. Une fois t\u00e9l\u00e9charg\u00e9, installez-le en suivant les instructions.

    Alternativement, utilisez winget depuis PowerShell\u2009:

    winget install -e --id Microsoft.VisualStudioCode\n
    ", "tags": ["winget"]}, {"location": "tools/editor/vscode/#extensions", "title": "Extensions", "text": ""}, {"location": "tools/editor/vscode/#wsl", "title": "WSL", "text": "

    Si vous utilisez WSL vous devez installer l'extension Remote - WSL pour Visual Studio Code. Cette extension permet d'ouvrir un terminal int\u00e9gr\u00e9 dans WSL, d'ex\u00e9cuter des commandes dans WSL, de d\u00e9boguer des programmes dans WSL, etc.

    "}, {"location": "tools/editor/vscode/#remote-ssh", "title": "Remote - SSH", "text": "

    Si vous utilisez SSH pour vous connecter \u00e0 un serveur distant par exemple vous connecter directement sur votre RaspberryPI, vous devez installer l'extension Remote - SSH pour Visual Studio Code. Cette extension permet d'ouvrir un terminal int\u00e9gr\u00e9 sur un serveur distant, d'ex\u00e9cuter des commandes sur le serveur distant, de d\u00e9boguer des programmes sur le serveur distant, etc.

    "}, {"location": "tools/editor/vscode/#cc", "title": "C/C++", "text": "

    Si vous d\u00e9veloppez en C ou en C++, vous devez installer l'extension C/C++ pour Visual Studio Code. Cette extension permet d'ajouter des fonctionnalit\u00e9s pour le d\u00e9veloppement en C et en C++ telles que la coloration syntaxique, l'autocompl\u00e9tion, la compilation, le d\u00e9bogage, etc.

    "}, {"location": "tools/editor/vscode/#python", "title": "Python", "text": "

    Si vous d\u00e9veloppez en Python, vous devez installer l'extension Python pour Visual Studio Code. Cette extension permet d'ajouter des fonctionnalit\u00e9s pour le d\u00e9veloppement en Python telles que la coloration syntaxique, l'autocompl\u00e9tion, la compilation, le d\u00e9bogage, etc.

    ", "tags": ["Python"]}, {"location": "tools/editor/vscode/#configuration-pour-le-debogueur", "title": "Configuration pour le debogueur", "text": "

    Visual Studio Code n'a pas la notion de projet mais d'espace de travail workspace. Un espace de travail est simplement un r\u00e9pertoire. \u00c0 l'int\u00e9rieur de ce r\u00e9pertoire, on y trouvera\u2009:

    .\n\u251c\u2500\u2500 .vscode\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 launch.json\n\u2514\u2500\u2500 main.c\n

    Visual Studio Code peut en g\u00e9n\u00e9ral g\u00e9n\u00e9rer automatiquement le fichier .vscode/launch.json qui contient tout ce qu'il faut pour compiler et ex\u00e9cuter le programme\u2009:

    {\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"gcc\",\n      \"type\": \"cppdbg\",\n      \"request\": \"launch\",\n      \"program\": \"${fileDirname}\\\\${fileBasenameNoExtension}.exe\",\n      \"args\": [],\n      \"stopAtEntry\": false,\n      \"cwd\": \"${workspaceFolder}\",\n      \"environment\": [],\n      \"externalConsole\": false,\n      \"MIMode\": \"gdb\",\n      \"miDebuggerPath\":\n        \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\\\\gdb.exe\",\n      \"setupCommands\": [\n        {\n          \"description\": \"Enable pretty-printing for gdb\",\n          \"text\": \"-enable-pretty-printing\",\n          \"ignoreFailures\": true\n        }\n      ],\n      \"preLaunchTask\": \"gcc.exe build active file\"\n    }\n  ]\n}\n
    {\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"shell\",\n      \"label\": \"gcc.exe build active file\",\n      \"command\":\n        \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\\\\gcc.exe\",\n      \"args\": [\n        \"-g\",\n        \"${file}\",\n        \"-o\",\n        \"${fileDirname}\\\\${fileBasenameNoExtension}.exe\"\n      ],\n      \"options\": {\n        \"cwd\":\n          \"C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\mingw\\\\tools\\\\install\\\\mingw64\\\\bin\"\n      },\n      \"problemMatcher\": [\n        \"$gcc\"\n      ]\n    }\n  ]\n}\n
    "}, {"location": "tools/python/python/", "title": "Python", "text": "

    Un python informaticien

    "}, {"location": "tools/python/python/#introduction", "title": "Introduction", "text": "

    Python est un langage de programmation comme le C mais il est plus haut niveau. Cela signifie d'une que Python est plus facile \u00e0 apprendre et \u00e0 utiliser que le C. Python est un langage interpr\u00e9t\u00e9, car le code source est ex\u00e9cut\u00e9 directement par un interpr\u00e9teur sans n\u00e9cessairement passer par une \u00e9tape de compilation.

    C'est un langage tr\u00e8s populaire pour l'enseignement de la programmation et pour la science des donn\u00e9es. Python est utilis\u00e9 dans de nombreux domaines, tels que l'intelligence artificielle, l'apprentissage automatique, l'analyse de donn\u00e9es, la programmation web, la programmation syst\u00e8me, etc.

    Guido van Rossum a cr\u00e9\u00e9 Python en 1991. Il en a \u00e9t\u00e9 le BDFL (dictateur bienveillant \u00e0 vie) jusqu'en 2018 o\u00f9 il a d\u00e9cid\u00e9 de se retirer de la direction du projet en raison de d\u00e9saccords avec la communaut\u00e9 Python. Cette date marque un tournant dans l'histoire de Python qui devient un langage communautaire avec ses avantages et ses inconv\u00e9nients. L'avantage est que Python est beaucoup plus dynamique et innovant. L'inconv\u00e9nient est qu'il change rapidement et que les versions ne sont pas toujours compatibles entre elles.

    "}, {"location": "tools/python/python/#installation-de-python", "title": "Installation de Python", "text": "

    Sous Linux/WSL, l'installation de Python est tr\u00e8s simple. Ouvrez un terminal et tapez la commande suivante\u2009:

    UbuntuMacOSWindows
    sudo apt install python3\n
    brew install python3\n
    winget install -e --id Python.Python.3.12\n

    Sous Windows, vous devez choisir la version de Python que vous souhaitez installer. Il est recommand\u00e9 d'installer la version 3.12 de Python. Utilisez le gestionnaire de paquets winget pour installer Python.

    ", "tags": ["winget"]}, {"location": "tools/python/python/#configuration-des-variables-denvironnement", "title": "Configuration des variables d'environnement", "text": "

    Selon la m\u00e9thode utilis\u00e9e pour installer Python, il est possible que les variables d'environnement ne soient pas configur\u00e9es automatiquement. Il y a deux entr\u00e9es \u00e0 configurer dans la variable PATH :

    1. Le chemin vers l'ex\u00e9cutable Python. Sous Linux/WSL, le chemin sera d\u00e9j\u00e0 configur\u00e9 (/usr/bin). Sous Windows, le chemin diff\u00e8re selon les \u00e9poques, les installateurs et les versions.
    2. Le chemin vers les paquets locaux install\u00e9s avec pip.

    Pour configurer sous Linux/WSL le chemin vers les paquets locaux install\u00e9s avec pip, ouvrez un terminal et tapez la commande suivante\u2009:

    Linux/WSLWindows
    echo \"export PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"\" >> ~/.bashrc\nsource ~/.bashrc\n

    Sous Windows, c'est plus compliqu\u00e9. Selon l'installation de Python vous devez identifier le chemin vers le dossier Scripts qui contient les ex\u00e9cutables Python install\u00e9s avec pip.

    Les conventions \u00e9voluent avec le temps. Voici un chemin possible\u2009:

    C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python3x\\Scripts\n
    ", "tags": ["Scripts", "pip", "PATH"]}, {"location": "tools/python/python/#installation-des-paquets-principaux", "title": "Installation des paquets principaux", "text": "

    Installons les paquets les plus couramment utilis\u00e9s avec Python. Ouvrez un terminal et tapez les commandes suivantes\u2009:

    pip install ipython numpy matplotlib pandas jupyterlab black flake8\n
    "}, {"location": "tools/python/python/#environnement-virtuel", "title": "Environnement virtuel", "text": "

    Lorsque vous installez des paquets avec PIP, un syst\u00e8me complexe de gestion de d\u00e9pendances est mis en place. Par exemple, si vous installez numpy, matplotlib sera install\u00e9 automatiquement. Chaque paquet d\u00e9pend d'autres paquets.

    Il n'est pas rare d'avoir des conflits entre les versions des paquets. Par exemple, si vous avez un projet qui utilise numpy en version 1.20 et un autre projet qui utilise numpy en version 1.21, vous aurez des probl\u00e8mes.

    Pour \u00e9viter ces probl\u00e8mes, il est recommand\u00e9 d'utiliser un environnement virtuel.

    Un environnement virtuel est un dossier local au projet sur lequel vous travaillez qui contient une installation de Python, un gestionnaire de paquets PIP et un ensemble de paquets. Chaque environnement virtuel est ind\u00e9pendant des autres. Vous pouvez avoir autant d'environnements virtuels que vous le souhaitez.

    La gestion d'environnement virtuel a beaucoup \u00e9volu\u00e9 avec les versions de Python. Il existe plusieurs outils que l'on peut confondre\u2009:

    • venv est un module de la biblioth\u00e8que standard de Python depuis la version 3.3 qui permet de cr\u00e9er des environnements virtuels. Il est recommand\u00e9 d'utiliser venv pour les projets personnels.
    • virtualenv est un outil plus ancien disponible pour Python 2 et 3. Il n'est pas recommand\u00e9 de l'utiliser.
    • poetry est un outil externe. Utile pour tester des projets sur diff\u00e9rentes versions de Python\u2009; facilite la migration entre les versions.

    Pour cr\u00e9er un environnement virtuel avec venv, ouvrez un terminal et tapez les commandes suivantes\u2009:

    Linux/WSLWindows
    python3 -m venv venv\nsource venv/bin/activate\n
    python -m venv venv\n.\\venv\\Scripts\\Activate\n
    ", "tags": ["numpy", "matplotlib", "virtualenv", "venv", "poetry"]}, {"location": "tools/python/python/#poetry", "title": "Poetry", "text": "

    Poetry est un outil de gestion de d\u00e9pendances pour Python. Il permet de g\u00e9rer les d\u00e9pendances de votre projet, de cr\u00e9er des environnements virtuels, de g\u00e9rer les versions de Python, de publier des paquets sur PyPI, etc.

    Il est une excellente alternative \u00e0 venv et pip car en plus de g\u00e9rer les d\u00e9pandances, il peut g\u00e9rer diff\u00e9rentes versions de Python.

    Si vous voulez cr\u00e9er un projet Python avec Poetry, ouvrez un terminal et tapez les commandes suivantes\u2009:

    mkdir myproject\ncd myproject\npoetry init\n

    Ensuite pour ajouter des d\u00e9pendances \u00e0 votre projet, utilisez poetry add :

    poetry add numpy matplotlib pandas\n

    Il est \u00e9galement possible d'ajouter des paquets de d\u00e9veloppement avec poetry add --dev :

    poetry add --group dev black flake8\n

    Ces commandes vont cr\u00e9er un fichier de configuration nomm\u00e9 pyproject.toml qui contient toutes les informations sur votre projet et ses d\u00e9pendances. L'avantage est qu'en mettant ce fichier sous Git, lorsque vous clonez votre projet sur une autre machine, il suffit de taper poetry install pour installer toutes les d\u00e9pendances.

    Par d\u00e9faut poetry ne cr\u00e9e pas d'enviroonement virtuel. Pour activer l'environnement virtuel, tapez la commande suivante\u2009:

    poetry shell\n

    Alternativement si l'objectif est simplement d'ex\u00e9cuter une commande dans l'environnement virtuel, utilisez poetry run :

    poetry run python main.py\n
    ", "tags": ["pyproject.toml", "venv", "pip"]}, {"location": "tools/python/python/#pipx", "title": "Pipx", "text": "

    pipx est un outil qui permet d'installer des paquets Python en dehors de l'environnement virtuel. Cela permet d'installer des paquets Python comme des ex\u00e9cutables. Par exemple, si vous voulez installer black ou flake8 pour formater votre code, vous pouvez les installer avec pipx :

    pipx install black\n

    Par d\u00e9faut \u00e0 partir de Ubuntu 24.04, pip n'est plus conseill\u00e9 pour installer des paquets Python. Le PEP 668 ayant \u00e9t\u00e9 accep\u00e9, la notion de Externally Managed Environment a \u00e9t\u00e9 introduite.

    Il y a plusieurs m\u00e9thodes pour installer des paquets dans une distributions Linux\u2009:

    1. pip install ...
    2. apt install python3-...

    La premi\u00e8re m\u00e9thode est la plus courante mais elle peut cr\u00e9er des probl\u00e8mes de compatibilit\u00e9 entre les paquets. Comme ces derniers sont install\u00e9s pour l'utilisateur courant, une version plus r\u00e9cente de numpy ne serait possiblement pas compatible avec un paquet Ubuntu qui d\u00e9pendrait d'une version plus ancienne de numpy.

    Il est pr\u00e9f\u00e9rable alors d'utiliser la version apt autant que possible.

    Alternativement, pour les outils en ligne de commande, il est possible d'utiliser pipx qui installe les paquets dans un environnement virtuel et cr\u00e9e des liens symboliques vers les ex\u00e9cutables dans le r\u00e9pertoire ~/.local/bin.

    ", "tags": ["numpy", "flake8", "pip", "apt", "pipx", "black"]}, {"location": "tools/python/python/#analyse-syntaxique-et-formatage", "title": "Analyse syntaxique et formatage", "text": "

    Il y a pl\u00e9tore d'outils pour analyser la syntaxe et formater le code Python. Les paquets flake8, pylint, isort, bandit ont \u00e9t\u00e9 tr\u00e8s populaires mais il est aujourd'hui recommand\u00e9 d'utiliser\u2009:

    1. black pour le formatage du code
    2. ruff pour l'analyse syntaxique
    3. mypy pour le typage statique
    ", "tags": ["flake8", "pylint", "mypy", "bandit", "isort", "ruff", "black"]}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index b7569609..1c722218 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -128,6 +128,10 @@ https://heig-tin-info.github.io/handbook/course-c/20-composite-types/unions/ 2024-09-27 + + https://heig-tin-info.github.io/handbook/course-c/25-architecture-and-systems/abi/ + 2024-09-27 + https://heig-tin-info.github.io/handbook/course-c/25-architecture-and-systems/computer/ 2024-09-27 diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 540f5acf..5763d91a 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/summary/summary/index.html b/summary/summary/index.html index eb8bedfa..4721210e 100644 --- a/summary/summary/index.html +++ b/summary/summary/index.html @@ -1582,6 +1582,8 @@ + + @@ -1761,6 +1763,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/analysis/analysis/index.html b/tools/analysis/analysis/index.html index 140c74b6..7ec06c2d 100644 --- a/tools/analysis/analysis/index.html +++ b/tools/analysis/analysis/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/analysis/binutils/index.html b/tools/analysis/binutils/index.html index bc061596..9a8fd321 100644 --- a/tools/analysis/binutils/index.html +++ b/tools/analysis/binutils/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/arch/filesystem/index.html b/tools/arch/filesystem/index.html index 5b18f100..723cea96 100644 --- a/tools/arch/filesystem/index.html +++ b/tools/arch/filesystem/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/build-system/cmake/index.html b/tools/build-system/cmake/index.html index 96bc2491..2facca89 100644 --- a/tools/build-system/cmake/index.html +++ b/tools/build-system/cmake/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/build-system/make/index.html b/tools/build-system/make/index.html index 75bb04d0..f7df5c35 100644 --- a/tools/build-system/make/index.html +++ b/tools/build-system/make/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/build-system/meson/index.html b/tools/build-system/meson/index.html index 19a2373e..85ca4669 100644 --- a/tools/build-system/meson/index.html +++ b/tools/build-system/meson/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/build-system/ninja/index.html b/tools/build-system/ninja/index.html index 19936580..08b0d062 100644 --- a/tools/build-system/ninja/index.html +++ b/tools/build-system/ninja/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/c-cpp/conan/index.html b/tools/c-cpp/conan/index.html index bb3e5946..b8045095 100644 --- a/tools/c-cpp/conan/index.html +++ b/tools/c-cpp/conan/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/c-cpp/gcc/index.html b/tools/c-cpp/gcc/index.html index 7de0ceff..1a4b058c 100644 --- a/tools/c-cpp/gcc/index.html +++ b/tools/c-cpp/gcc/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/c-cpp/gdb/index.html b/tools/c-cpp/gdb/index.html index 1168115f..8cc00826 100644 --- a/tools/c-cpp/gdb/index.html +++ b/tools/c-cpp/gdb/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/c-cpp/vcpkg/index.html b/tools/c-cpp/vcpkg/index.html index f0cc371d..38e1de7d 100644 --- a/tools/c-cpp/vcpkg/index.html +++ b/tools/c-cpp/vcpkg/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/bash/index.html b/tools/dev/bash/index.html index 796fa9e1..d3ae49c1 100644 --- a/tools/dev/bash/index.html +++ b/tools/dev/bash/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/config/index.html b/tools/dev/config/index.html index 256309fe..cea6987e 100644 --- a/tools/dev/config/index.html +++ b/tools/dev/config/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/docker/index.html b/tools/dev/docker/index.html index e948af84..c2c0d225 100644 --- a/tools/dev/docker/index.html +++ b/tools/dev/docker/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/environment/index.html b/tools/dev/environment/index.html index 4c247c4d..54bc886b 100644 --- a/tools/dev/environment/index.html +++ b/tools/dev/environment/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/git/index.html b/tools/dev/git/index.html index c3004b77..cfd51acc 100644 --- a/tools/dev/git/index.html +++ b/tools/dev/git/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/windows/index.html b/tools/dev/windows/index.html index 59157d36..d63ea656 100644 --- a/tools/dev/windows/index.html +++ b/tools/dev/windows/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/dev/wsl/index.html b/tools/dev/wsl/index.html index ecee28f5..34989346 100644 --- a/tools/dev/wsl/index.html +++ b/tools/dev/wsl/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/editor/vim/index.html b/tools/editor/vim/index.html index ce88f411..9b724d93 100644 --- a/tools/editor/vim/index.html +++ b/tools/editor/vim/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/editor/vscode/index.html b/tools/editor/vscode/index.html index 036148b6..2344f73d 100644 --- a/tools/editor/vscode/index.html +++ b/tools/editor/vscode/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + + diff --git a/tools/python/python/index.html b/tools/python/python/index.html index 0920055f..fe16b7b2 100644 --- a/tools/python/python/index.html +++ b/tools/python/python/index.html @@ -1586,6 +1586,8 @@ + + @@ -1765,6 +1767,33 @@ + + + + + + +
  • + + + + + + + ABI + + + + + + + + +
  • + + + +