summaryrefslogtreecommitdiff
path: root/articles
diff options
context:
space:
mode:
Diffstat (limited to 'articles')
-rw-r--r--articles/bizarreries-du-langage-c.md (renamed from articles/bizarrerie-du-langage-c.md)151
1 files changed, 71 insertions, 80 deletions
diff --git a/articles/bizarrerie-du-langage-c.md b/articles/bizarreries-du-langage-c.md
index e90829f..7ad6aae 100644
--- a/articles/bizarrerie-du-langage-c.md
+++ b/articles/bizarreries-du-langage-c.md
@@ -5,8 +5,9 @@ Les bizarreries du langage C
Le C est un langage à la syntaxe simple. Les seules complexités de ce langage viennent du fait qu'il agit de manière proche de la machine.
Pourtant, une partie des syntaxes autorisées par le C n'est pratiquement jamais enseignée. Attaquons-nous à ces cas mystérieux ! 🧞
->!attention
-> Pour comprendre ce billet, il est nécessaire d'avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C.
+:::attention
+Pour comprendre ce billet, il est nécessaire d'avoir des bases dans un langage ayant une syntaxe et un fonctionnement proche du C.
+:::
Sommaire
@@ -23,7 +24,7 @@ Mais pas seulement ! C'est également un opérateur.
### L'opérateur virgule
-L'instruction suivante, bien qu'inutile est tout à fait valide :
+L'instruction suivante, bien qu'inutile est tout à fait valide :
~~~c
printf("%d", (5,3) );
@@ -33,14 +34,14 @@ Elle affiche 3. L'opérateur `,` sert à juxtaposer des expressions.
La valeur de l'expression complète est égale à la valeur de la dernière expression.
Cet opérateur se révèle très utile dans une boucle `for` pour multiplier les itérations.
-Par exemple pour incrémenter `i` et décrémenter `j` dans la même itération d'une boucle `for` on peut faire :
+Par exemple pour incrémenter `i` et décrémenter `j` dans la même itération d'une boucle `for` on peut faire :
~~~c
for( ; i < j ; i++, j-- ) {
// [...]
}
~~~
-Ou encore, dans de petits `if` pour les simplifier :
+Ou encore, dans de petits `if` pour les simplifier :
~~~c
if( argc > 2 && argv[2][0] == '0' )
@@ -55,7 +56,7 @@ On peut également s'en servir pour retirer des parenthèses.
while( c = getchar(), c != EOF && c != '\n' ) {
// [...]
}
-// Est strictement équivalent à :
+// Est strictement équivalent à :
while( (c = getchar()) != EOF && c != '\n' ) {
// [...]
}
@@ -69,7 +70,7 @@ Cette remarque est d'ailleurs également valide pour le prochain opérateur !
Le ternaire pour les intimes. Le seul opérateur du langage C qui prenne 3 opérandes.
Il sert à simplifier des expressions conditionnelles.
-Par exemple pour afficher le minimum de deux nombres, sans le ternaire, on ferait :
+Par exemple pour afficher le minimum de deux nombres, sans le ternaire, on ferait :
~~~c
if (a < b)
@@ -78,7 +79,7 @@ else
printf("%d", b);
~~~
-Ou avec une variable temporaire :
+Ou avec une variable temporaire :
~~~c
int min = a;
@@ -87,38 +88,38 @@ if( b < a)
printf("%d", min);
~~~
-Alors qu'avec les ternaires on fait simplement :
+Alors qu'avec les ternaires on fait simplement :
~~~c
-printf("%d", a<b ? a : b);
+printf("%d", a<b ? a : b);
~~~
Grâce au ternaire, on a économisé une répétition ainsi que quelques lignes.
Mais surtout on a gagné en lisibilité. Quand on sait en lire une...
-Pour lire une expression ternaire, il faut la découper en 3 parties :
+Pour lire une expression ternaire, il faut la découper en 3 parties :
~~~c
-expression_1 ? expression_2 : expression_3
+expression_1 ? expression_2 : expression_3
~~~
La valeur d'une expression ternaire est `expression_2` si la valeur de `expression_1` est évaluée à vrai et `expression_3` sinon.
En somme, cela simplifie un peu la lecture pour de courtes expressions.
-L'expression `a<b ? a : b` se lit « Si `a` est inférieur à `b` alors `a` sinon `b` ».
+L'expression `a<b ? a : b` se lit « Si `a` est inférieur à `b` alors `a` sinon `b` ».
J'insiste encore sur le fait que cet opérateur, s'il est mal utilisé, peut nuire à la lisibilité du code.
-Notez que l'on peut très bien utiliser une expression ternaire comme opérande d'une autre expression ternaire :
+Notez que l'on peut très bien utiliser une expression ternaire comme opérande d'une autre expression ternaire :
~~~c
-printf("%d", a<b ? a<c ? a : b<c ? b : c : b < c ? b : c);
+printf("%d", a<b ? a<c ? a : b<c ? b : c : b < c ? b : c);
~~~
Désormais, on prend le minimum de trois nombres. C'est aéré, mais impossible à suivre.
-Le plus lisible est d'utiliser une macro :
+Le plus lisible est d'utiliser une macro :
~~~c
-#define MIN(a,b) ((a) < (b) ? (a) : (b))
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
printf("%d", MIN(a,MIN(b,c)));
~~~
@@ -130,7 +131,7 @@ Puis d'ailleurs, même les opérateurs que l'on connait déjà, on n'en maîtris
L'accès à un tableau
------------------
-On nous a toujours appris que pour afficher le 3ème élément d'un tableau, on faisait :
+On nous a toujours appris que pour afficher le 3ème élément d'un tableau, on faisait :
~~~c
int tab[5] = {0, 1, 2, 3, 4, 5};
@@ -139,25 +140,25 @@ printf("%d", tab[2]);
2 et non 3, car un tableau en C commence à 0. Si un tableau commence à 0, c'est une histoire d'adresse[^adresse]. L'adresse du tableau est en fait l'adresse du premier élément du tableau. Et par arithmétique des pointeurs, l'adresse du 3ème élément est `tab+2`.
-Donc on aurait très bien pu écrire :
+Donc on aurait très bien pu écrire :
~~~c
printf("%d", *(tab+2));
~~~
Puis comme l'addition est commutative, `tab+2` ou `2+tab` sont équivalents.
-En fait, on aurait même pu faire :
+En fait, on aurait même pu faire :
~~~c
printf("%d", 2[tab]);
~~~
-C'est tout à fait valide. Et pour cause : la syntaxe `E[F]` est strictement équivalente à `*((E)+(F))`.
+C'est tout à fait valide. Et pour cause : la syntaxe `E[F]` est strictement équivalente à `*((E)+(F))`.
Du coup, le titre de cette section est un peu trompeur. Cet opérateur n'a pas grand-chose à voir avec les tableaux en fait.
C'est du sucre syntaxique pour cacher l'arithmétique des pointeurs.[^sucre]
Par exemple pour afficher le caractère `=` pour vrai, `!` pour faux et `~` pour aucun des deux.
-On pourrait faire :
+On pourrait faire :
~~~c
if( is_good == 1 )
printf("%c", '=');
@@ -167,11 +168,11 @@ else
printf("%c", '~');
~~~
-Mais il existe plus simple :
+Mais il existe plus simple :
~~~c
printf("%c", "!=~"[is_good]);
-// Ou comme on l'a vu :
+// Ou comme on l'a vu :
printf("%c", is_good["!=~"] ); // Affiche '!' si is_good vaut 0
// '=' si is_good vaut 1
// '~' si is_good vaut 2
@@ -190,7 +191,7 @@ L'initialisation
L'initialisation, c'est quelque chose que l'on maîtrise en C. C'est le fait de donner une valeur à une variable lors de sa déclaration.
En gros, on définit sa valeur.
-Pour un tableau[^tableau] :
+Pour un tableau[^tableau] :
~~~c
int tab[10] = {0};
tab[2] = 5;
@@ -200,13 +201,13 @@ tab[2] = 5;
À la première ligne, on initialise le tableau avec des 0 car, lorsqu'on initialise un tableau toute valeur non spécifiée est par défaut 0.
La ligne suivante est une affectation et non une initialisation.
-Si on veut seulement initialiser le troisième élément, puisque l'initialisation d'un tableau se fait suivant l'ordre de ses valeurs, on devrait écrire :
+Si on veut seulement initialiser le troisième élément, puisque l'initialisation d'un tableau se fait suivant l'ordre de ses valeurs, on devrait écrire :
~~~c
int tab[10] = {0, 0, 5};
~~~
-Mais en réalité, il existe une autre syntaxe qui permet de faire plus simple :
+Mais en réalité, il existe une autre syntaxe qui permet de faire plus simple :
~~~c
int tab[10] = {[2] = 5};
@@ -215,8 +216,9 @@ int tab[10] = {[2] = 5};
On dit simplement que la troisième case vaut 5. Le reste est par défaut 0.
Une syntaxe équivalente existe d'ailleurs pour les structures et les unions.
->!information
-> Pour l'exemple, on va prendre une structure `point` que je vais utiliser plusieurs fois dans ce billet, idem pour la structure `message`.
+:::information
+ Pour l'exemple, on va prendre une structure `point` que je vais utiliser plusieurs fois dans ce billet, idem pour la structure `message`.
+:::
On peut ainsi initialiser un point en utilisant ses composantes.
@@ -232,7 +234,7 @@ point A = {.x = 1, .y = 2};
Ici, il n'y avait pas d'ambigüité.
Mais pour une structure plus complexe, cette syntaxe est vraiment avantageuse.
-Tenez :
+Tenez :
~~~c
typedef struct message {
@@ -243,7 +245,7 @@ typedef struct message {
message to_send = {.src="", .dst="23:12:23", .msg="Code 10"};
-// Est bien plus clair que :
+// Est bien plus clair que :
message to_send = {"", "23:12:23", "Code 10"};
@@ -261,7 +263,7 @@ message to_send = { .dst="23:12:23", .msg="Code 10"};
Avec ces syntaxes on peut également alourdir le code, mais généralement, on gagne en lisibilité.
Parfois, ces syntaxes sont très judicieusement utilisées !
-Comme ici, dans ce décodeur de base64 :
+Comme ici, dans ce décodeur de base64 :
>~~~c
>static int b64_d[] = {
> ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4,
@@ -283,22 +285,22 @@ Source: [Taurre](https://openclassrooms.com/forum/sujet/defis-8-tout-en-base64-1
[^tableau]:
- `{0}` peut également être utilisé pour un nombre. Toute valeur initialisant une variable simple (pointeur, nombre, ...) peut optionnellement prendre des accolades.
+ `{0}` peut également être utilisé pour un nombre. Toute valeur initialisant une variable simple (pointeur, nombre, ...) peut optionnellement prendre des accolades.
- ~~~c
- int a = {11};
- float pi = {3.1415926};
- char* s = {"unicorn"};
- ~~~
+ ~~~c
+ int a = {11};
+ float pi = {3.1415926};
+ char* s = {"unicorn"};
+ ~~~
- Ce qui caractérise le tableau du coup est surtout le fait d'utiliser des virgules entre les accolades.
+ Ce qui caractérise le tableau du coup est surtout le fait d'utiliser des virgules entre les accolades.
Les expressions littéralement composées
--------------------------------------
Puisque nous parlons des tableaux. Il existe une syntaxe simple pour utiliser des tableaux à usage unique.
-Je voudrais utiliser ce tableau :
+Je voudrais utiliser ce tableau :
~~~c
int tab[5] = {5, 4, 5, 2, 1};
printf("%d", tab[i]); // Avec i égale à quelque chose >=0 et <5
@@ -307,16 +309,16 @@ printf("%d", tab[i]); // Avec i égale à quelque chose >=0 et <5
Cependant, je ne l'utilise qu'une seule fois ce tableau ...
C'est un peu dérangeant d'avoir à utiliser un identificateur juste pour ça.
-*Eh* bien je peux faire ceci :
+*Eh* bien je peux faire ceci :
~~~c
printf("%d", ((int[]){5,4,5,2,1}) [i] ); // Avec i égale à quelque chose >=0 et <5
~~~
-Ce n'est pas super lisible pour le coup. Mais il existe plein de cas où cette syntaxe est très utile. Par exemple avec une structure :
+Ce n'est pas super lisible pour le coup. Mais il existe plein de cas où cette syntaxe est très utile. Par exemple avec une structure :
~~~c
-// Pour envoyer notre message :
+// Pour envoyer notre message :
send_msg( (message){ .dst="192.168.11.1", .msg="Code 11"} );
// Pour afficher la distance entre deux points
@@ -331,7 +333,7 @@ On appelle ces expressions des *compound literals* (ou littéraux agrégats si l
Introduction aux VLAs
--------
-Les tableaux à taille variable (ou *Variable Length Arrays* en anglais, soit VLAs) sont des tableaux dont la taille n'est connue qu'à l'exécution. Si vous n'avez jamais entendu parler des VLAs, ceci devrait vous choquer :
+Les tableaux à taille variable (ou *Variable Length Arrays* en anglais, soit VLAs) sont des tableaux dont la taille n'est connue qu'à l'exécution. Si vous n'avez jamais entendu parler des VLAs, ceci devrait vous choquer :
~~~c
int n = 11;
@@ -356,7 +358,7 @@ double tab2[2*n];
unsigned int tab[foo()]; // avec foo une fonction définie ailleurs
~~~
-Un VLA ne peut pas être initialisé, de plus, il ne peut être déclaré `static`. C'est-à-dire que ces deux déclarations sont incorrectes :
+Un VLA ne peut pas être initialisé, de plus, il ne peut être déclaré `static`. C'est-à-dire que ces deux déclarations sont incorrectes :
~~~c
int n = 30;
@@ -365,7 +367,7 @@ int tab[n] = {0};
static tab2[n];
~~~
-Dans une fonction, on pourrait utiliser :
+Dans une fonction, on pourrait utiliser :
~~~c
void bar(int n, int tab[n]) {
@@ -373,7 +375,7 @@ void bar(int n, int tab[n]) {
}
~~~
-Cependant, la taille de la première dimension d'un tableau n'a pas beaucoup d'importance, un tableau étant implicitement convertit en un pointeur vers son premier élément. En revanche, pour un tableau à deux dimensions, la taille de la deuxième dimension *doit* être spécifiée :
+Cependant, la taille de la première dimension d'un tableau n'a pas beaucoup d'importance, un tableau étant implicitement convertit en un pointeur vers son premier élément. En revanche, pour un tableau à deux dimensions, la taille de la deuxième dimension *doit* être spécifiée :
~~~c
void foo( int n, int m, int tab[][m]) {
@@ -389,7 +391,7 @@ void foo(int, int, int[][*]);
Bien, après cette courte introduction aux VLAs, passons aux cas qui nous intéressent. Pour être précis, les bizarreries et excentricités que les VLAs ont introduits.
-[^raisons]: Je peux néanmoins vous donnez deux références [“Is it safe to use variable-length arrays?”](https://stackoverflow.com/questions/7326547/is-it-safe-to-use-variable-length-arrays) de stack overflow et cet article : [“The Linux Kernel Is Now VLA-Free”](https://www.phoronix.com/scan.php?page=news_item&px=Linux-Kills-The-VLA).
+[^raisons]: Je peux néanmoins vous donnez deux références [“Is it safe to use variable-length arrays?”](https://stackoverflow.com/questions/7326547/is-it-safe-to-use-variable-length-arrays) de stack overflow et cet article : [“The Linux Kernel Is Now VLA-Free”](https://www.phoronix.com/scan.php?page=news_item&px=Linux-Kills-The-VLA).
L'exception des VLAs
---------------------------
@@ -424,7 +426,7 @@ printf("%zu", sizeof(char[++n])); // Affiche 6
Ce n'est que peu intuitif puisque les VLAs ont ici introduit une *exception à la règle* qui est de ne pas exécuter l'expression passée à `sizeof`.
-Un autre comportement bizarre introduit par les VLAs est l'exécution des expressions liées à la taille d'un VLA dans la définition d'une fonction. Ainsi :
+Un autre comportement bizarre introduit par les VLAs est l'exécution des expressions liées à la taille d'un VLA dans la définition d'une fonction. Ainsi :
~~~c
int foo( char tab[printf("bar")] ) {
@@ -447,7 +449,7 @@ Vous n'avez peut-être jamais entendu parler des « *flexible arrays member* ».
L'objectif est d'allouer une structure mais dont un champ (un tableau) est de taille inconnue à la compilation et le tout sur un espace contiguë[^pourquoi].
Ici, pas de VLAs, car comme on l'a vu, ceux-ci sont interdits en tant que champs de structure. L'allocation dynamique s'impose.
-On pourrait vouloir faire :
+On pourrait vouloir faire :
~~~c
struct foo {
@@ -455,7 +457,7 @@ struct foo {
};
~~~
-Et l'utiliser comme ceci :
+Et l'utiliser comme ceci :
~~~c
struct foo* contigue = malloc( sizeof(struct foo) );
@@ -476,7 +478,7 @@ struct foo {
};
~~~
-Ici, le champ `flexiTab` est un tableau membre flexible. Un tel tableau doit être le dernier élément de la structure et ne pas spécifier de taille[^zero]. On l'utilise comme ceci :
+Ici, le champ `flexiTab` est un tableau membre flexible. Un tel tableau doit être le dernier élément de la structure et ne pas spécifier de taille[^zero]. On l'utilise comme ceci :
~~~c
struct foo* contigue = malloc( sizeof(struct foo) + N * sizeof *flexiTab );
@@ -503,7 +505,7 @@ De manière à ne jamais avoir à utiliser `goto`.
Du coup, on n'apprend jamais ce qu'est une étiquette...
-Voici comment on utilise `goto` et une étiquette :
+Voici comment on utilise `goto` et une étiquette :
~~~c
goto end;
@@ -537,15 +539,16 @@ Ici, les `case` et le `default` sont en fait des étiquettes ! Comme pour les `g
Sauf qu'elles sont pour les `switch`, et inutilisables par les `goto`...
->!question
-> Pourquoi tu nous parles de ça ?
+:::question
+Pourquoi tu nous parles de ça ?
+:::
Déjà, c'est bien de savoir que ça s'appelle une étiquette. Ensuite, parce que je vais vous parlez d'un classique.
Le dispositif de *Duff*.
C'est une sorte de boucle déroulée optimisée. Le but est de réduire le nombre de vérifications de fin de boucle (ainsi que le nombre de décrémentations).
-Voici la version historique écrite par Tom Duff :
+Voici la version historique écrite par Tom Duff :
~~~c
{
@@ -573,7 +576,7 @@ Le test que l'on cherche à effectuer le moins possible est `--n > 0`.
En temps normal, `n` serait en fait `count`. Et on devrait faire le test `count` fois.
De même pour sa décrémentation.
-C'est-à-dire :
+C'est-à-dire :
~~~c
while( count-- > 0 )
@@ -601,7 +604,7 @@ Les nombres complexes
Encore une fois nous allons voir une syntaxe introduite en C99. Plus exactement, ce sont 3 types qui ont été introduits, ceux-ci correspondent aux nombres complexes. Le type d'un nombre complexe est `double _Complex` (les deux autres types suivent le même schéma, afin de ne pas me répéter, je vais me concentrer sur le type `double`).
-Ainsi en C, il est possible de déclarer un nombre complexe comme ceci :
+Ainsi en C, il est possible de déclarer un nombre complexe comme ceci :
~~~c
double complex point = 2 + 3 * I;
@@ -609,7 +612,7 @@ double complex point = 2 + 3 * I;
Ici, on retrouve les macros spéciales `complex` et `I` (définie dans l'en-tête `<complex.h>`). Le premier sert à créer un type complexe alors que le second sert à définir la partie imaginaire d'un nombre complexe.
-En mémoire une variable complexe occupe autant d'espace que 2 fois le type réel sur lequel elle est basée. On se sert d'une variable complexe comme d'une variable normale. L'arithmétique y est intuitive puisque basées sur celle des réels. Il est à noter qu'il est recommandé d'utiliser le macro `CMPLX` pour initialiser un nombre complexe :
+En mémoire une variable complexe occupe autant d'espace que 2 fois le type réel sur lequel elle est basée. On se sert d'une variable complexe comme d'une variable normale. L'arithmétique y est intuitive puisque basées sur celle des réels. Il est à noter qu'il est recommandé d'utiliser le macro `CMPLX` pour initialiser un nombre complexe :
~~~c
double complex cplx = CMPLX(2, 3);
@@ -626,7 +629,7 @@ Il existe un moyen en C d'avoir des macros qui soient définies différemment en
Cette généricité s'obtient avec les sélections génériques basées sur la syntaxe ` _Generic ( /* ... */ )`
-Pour comprendre la syntaxe, voyons un exemple simpliste :
+Pour comprendre la syntaxe, voyons un exemple simpliste :
~~~c
#include <stdio.h>
@@ -651,10 +654,10 @@ int main(int argc, char* argv[]) {
~~~
Ici, on affiche le maximum que peut stocker chacun des types que nous utilisons. C'est quelque chose qui n'aurait pas été possible sans l'utilisation de ce nouveau mot-clé `_Generic`.
-Pour utiliser cette syntaxe, on utilise le mot clé `_Generic` auquel on passe 2 paramètres. Le premier est une expression dont le type va influencer l'expression finalement exécutée. Le deuxième est une suite d'association de type et d'expression (type : expression) dont les associations sont séparées par des virgules. Au final, seule l'expression désignée par le type de la première expression est finalement évaluée.
+Pour utiliser cette syntaxe, on utilise le mot clé `_Generic` auquel on passe 2 paramètres. Le premier est une expression dont le type va influencer l'expression finalement exécutée. Le deuxième est une suite d'association de type et d'expression (type : expression) dont les associations sont séparées par des virgules. Au final, seule l'expression désignée par le type de la première expression est finalement évaluée.
-Un exemple réel d'utilisation pourrait être :
+Un exemple réel d'utilisation pourrait être :
~~~c
int powInt(int,int);
@@ -662,7 +665,7 @@ int powInt(int,int);
~~~
-Il n'y a plus grand-chose à dire si ce n'est qu'il est possible d'avoir dans la liste des types le mot `default` qui correspondra alors à tous les types non-cités. Ainsi une définition plus propre de la macro `POW` de tout à l'heure pourrait être :
+Il n'y a plus grand-chose à dire si ce n'est qu'il est possible d'avoir dans la liste des types le mot `default` qui correspondra alors à tous les types non-cités. Ainsi une définition plus propre de la macro `POW` de tout à l'heure pourrait être :
~~~c
int powIntuInt(int a, unsigned int b);
@@ -673,7 +676,7 @@ double powlFltInt(long double a,int b) { return powl(a,b); }
#define POWINT(x) _Generic((x), double: powFltInt, \
- float : powfFltInt, \
+ float : powfFltInt, \
long double: powlFltInt, \
unsigned int: powIntuInt, \
default: powIntInt)
@@ -695,33 +698,22 @@ Et pour chaque caractère n'étant pas sur le clavier et utiliser en langage C,
Voici un tableau récapitulatif des séquences de trigraphes, de digraphes et de leur représentation en caractère.
-+-----------+----------+-----------+
| Caractère | Digraphe | Trigraphe |
-+===========+==========+===========+
+| --------- | -------- | --------- |
| `#` | `%:` | `??=` |
-+-----------+----------+-----------+
| `[` | `<:` | `??(` |
-+-----------+----------+-----------+
| `]` | `:>` | `??)` |
-+-----------+----------+-----------+
| `{` | `<%` | `??<` |
-+-----------+----------+-----------+
| `}` | `%>` | `??>` |
-+-----------+----------+-----------+
| `\` | | `??/` |
-+-----------+ +-----------+
| `^` | | `??'` |
-+-----------+ +-----------+
-| `|` | | `??!` |
-+-----------+ +-----------+
+| `\|` | | `??!` |
| `~` | | `??-` |
-+-----------+----------+-----------+
| `##` | `%:%:` | |
-+-----------+----------+-----------+
-La différence **principale** entre les digraphes et les trigraphes est dans une chaîne de caractère :
+La différence **principale** entre les digraphes et les trigraphes est dans une chaîne de caractère :
~~~c
puts("??= est un croisillon");
@@ -729,8 +721,7 @@ puts("%:");
~~~
Ces mécanismes du Moyen Âge sont encore valides de nos jours en C.
-Du coup, cette ligne de code est tout à fait valide :
-
+Du coup, cette ligne de code est tout à fait valide :
~~~c
??=define FIRST tab<:0]
~~~