Salut le forum !
Lorsque l'on apprend à programmer, en général on nous dit de commencer par le langage C.
L'avantage de ce langage c'est qu'il est très simple, j'entends par là qu'il y a peu de feature à connaître. On a assez vite le sentiment de le maîtriser et fait vite plein de chose avec. La majorité des développeurs connaissent donc le C, et vous êtes des développeurs! (ça tombe bien non ?)
(Si ce n'est pas votre cas, je suis désolé mais cela va être complexe pour vous de lire la suite, alors aller apprendre le C <3 !)
Extra: Pourquoi apprendre le C ? (Et pourquoi il existe toujours ?)
Vous le savez, un ordinateur n’exécute pas le code que vous écrivez directement. Il a besoin que cela soit 'traduit' dans un langage qu'il comprend, qu'on appel langage machine, et ce langage ne se compose que de nombre les uns à la suite des autres. C'est absolument illisible. On a alors crée un langage (le langage assembleur) qui donne des noms et un sens (pour nous humain) à tous ces nombres, on appel ces noms des mnémoniques. Ce langage est 'lisible' mais pas du tout adapté à l’écriture d'algorithme comme nous avons l’habitude de les penser. On utilise donc des langages dit de haut niveau (d'abstraction), dans lesquels on écrit des algorithmes, en suivant une syntaxe bien définit et proche du langage humain (à part certain langage :p). Nous utilisons ensuite un compilateur qui génère le code assembleur puis le code pour la machine qui correspond au programme que l'on a écrit dans le langage plus haut niveau.
L’avantage de monter le niveau d'abstraction, c'est de ne pas avoir à se préoccuper des mécanismes réels du processeur.
Le désavantage, c'est que l'on maîtrise moins ces mêmes mécanismes, je parle de la gestion de la mémoire (call stack/heap/registres) et du flow of control (branch/...) principalement. Ce n'est pas un problème lorsque l'on développe une grosse application sur un système d'exploitation qui nous fournit déjà une bonne base d'abstraction (avec les syscalls et les librairies spécifiques à l'OS). En fait c'est même plus pratique, on ne se prend pas la tête avec des mécanismes de bas niveau et on construit de jolis algos de haut niveau d'abstraction.
Mais en informatique il y a beaucoup de domaine d’étude, il n'y a pas que le développement d'application. Il nous faudra toujours savoir garder une main mise sur les fonctionnements bas niveau (même au niveau matériel).
Et bien justement ! Le langage C est parfait comme premier niveau d'abstraction, au-dessus de l'assembleur, on peut écrire des algos lisibles humainement, et il reste pourtant très proche de l'assembleur, on peut même traduire simplement à la main du C en Assembleur avec un peu de patience. Mais un compilateur le fait mieux et avec des optimisations assez dingue !
Pourtant, on apprend souvent le C avant tout pour ça simplicité. Et pas toujours pour ça proximité à la machine. Mais je tenais à expliquer pourquoi le langage C, qui semble largement en retard sur les langages moderne, et pourtant encore très utilisé. Il est parfois le meilleur choix, cela dépend du niveau d'abstraction et de la proximité à la machine nécessaire à la réalisation voulu.
Quand on commence à apprendre le C, on a aussi assez vite la sensation que ce langage est très 'rigide'. J'entends par là qu'à la moindre erreur de syntaxe on se fait hurler dessus par le compilateur. Il faut toujours corriger le code pour répondre aux exigences du compilateur. Je pense que vous serez d'accord avec moi pour dire qu'on n’écrit pas n’importe quoi en C sinon on se fait jeter par le compilo !
Pour résumer, Le langage C serait donc un langage simple (grâce à son typage faible et son dictionnaire réduit), et un langage rigide (avec ça syntaxe peu permissive).
Mais ... Connaissez-vous vraiment le C ?
Cette question peut sembler étrange après ce que je viens de dire...
Alors voilà, je vous expose le code suivant :
// main.c
#include <stdio.h>
void __(){}int ma\
in (_,p)
int _,p;
#define go_to_(a) #a
{
_ = (__(), 0xC0DE ?_:p, 202<<8)^666 / 3;
__(0 ,p?"Success":"Fail", _);
printf("3.14159""%s !\b\b\b\b\b\b" // \
"Hello google %d" // | print
"rni\r" go_to_(Hell) "o %4x\n"); // /
}
Extra: Je compile mon code avec gcc sous linux.
Je ne garantit pas que la compilation passe avec le compilateur de Microsoft car ... j'ai la flemme de tester et je ne connais pas du tout ce compilateur.
Le code suit les standards qui définissent le langage C (Peut-être à un détail près sur la manipulation de la pile peut-être non defini... Je n'ai pas épluché les normes C89(ANSI), C90, C95 et C99 pour vérifier).
Mais je ne sais pas si le compilateur de Microsoft suit vraiment les standards du C... J'ai même lut qu'il était très mauvais pour cela (et c'est de toute façon pas dans les habitudes de Microsoft que de suivre les standards), alors je ne pense pas que cela fonctionnera.
Voici la ligne de commande sur les systèmes unix-like avec gcc (il faut gcc d'installé bien sûr):
gcc -o exec main.c -std=c99
./exec
PS: J'utilise le standard C99 pour avoir droit d’écrire des commentaires avec "//" (dit single-line), car ils sont souvent plus claire que les commentaires multi-line.
(et aussi pour cacher un petit truc ... mais on verra cela plus tard)
Si vous trouvez ce code absolument immonde et scandaleux ... Pour commencer vous avez raison ! Mais sachez que l'on peut écrire des codes bien plus troublant que celui-ci, j'ai été assez gentil.
Et si vous êtes joueur, vous pouvez essayer de deviner ce que va afficher ce programme !
Si vous voulez le résultat de l’exécution du programme :
Cliquez pour révéler
Cliquez pour masquer
Si vous êtes familier avec le C, il y a 2 ou 3 choses qui devraient vous choquer dans ce code.
- Pourquoi le printf n'as pas de paramètre, alors que la string de format indique des paramètres ?
- Pourquoi la colorisation syntaxique indique que la 2eme partie de la chaîne de caractère est en commentaire ???
- Pourquoi je définis des variables entre main(...) et le début de son code !?
- Où sont les includes ???
- que fait la ligne "_ = (f(), 0xC0DE ?_ : p, 202<<8)^666 / 3;" ?
- ou tout simplement : Pourquoi ce code compile-il ! je pensais que le C était un langage très rigide pour la syntaxe..
Si vous vous posez ces questions, vous connaissez alors sûrement bien les base du C et vous êtes normalement constitué, je vous rassure.
Ce que je vous propose ici c'est donc de lister quelque petit comportement drôle en C !
Opérateur [] ('Array subscripting') commutatif
char c = 4["Hello World"];
Cela semble étrange comme écriture, mais elle est en faite équivalente à "Hello World"[4]. Et on peut expliquer cela très simplement en décortiquant l’opérateur avec des pointeurs:
char str[] = "Hello World";
int index = 4;
char a = str [ index ]; // (A) //
char b = *( str + index ); // (B) // (A) <=> (B)
char c = *( index + str ); // (C) // (B) <=> (C)
char d = index [ str ]; // (D) // (C) <=> (D) // (A) <=> (D)
On doit donc cette propriété à la commutativité de l'addition et au typage faible du langage C.
Commentaire single-line (mais en multi-line !)
Celui-la je l'ai utilisé dans mon exemple, il y a peu de chance que vous connaissiez, car c'est tordu...
Mais en C, on a le droit de continuer une ligne à la ligne suivante en terminant la première ligne par le caractère '\'. Alors si on fini un commentaire single-line par ce caractère, on a une ligne de commentaire supplémentaire !
#include <stdio.h>
int main () {
printf("Hello friend...\n"); // commentaire \
... suite du commentaire ... \
encore la suite !
return 0;
}
Attention, c'est vraiment une très mauvaise pratique d'utiliser ce caractère a la fin d'un commentaire !
Avec la colorisation syntaxique on peut ce rendre compte de cette effet, mais sans colorisation syntaxique, ou avec une colorisation syntaxique mal conçu, elle est presque indétectable, et encore plus si ce commentaire est noyé à travers d'autre.
Un exemple de code tout bête, qui peut nous faire chercher très longtemps si on ne sais pas ce qui ce passe.
switch ( c=getchar() ) {
case '\r': case '\n': valid(); break; // Validate - Enter
case '\\': EscChar(); break; // Escape char - \
case 0x1b: exit(0); break; // Exit process - Esc
default: doSomeStuff(c); break; // Use char - Other key
}
Chaîne de caractère multi-line
Si vous ne connaissez pas, c'est dommage car c'est super utile !
En C, on a le droit d’arrêter une chaîne de caractère (en fermant les doubles guillemets) et de la reprendre plus loin (en rouvrant les guillemets). Entre les 2, il ne faut pas de code, donc on a droit au espace, tabulation, saut de ligne, commentaire, préprocesseur...
C'est toujours pratique pour éviter les printf multi-ligne trop long et illisible.
printf( "Premiere ligne\n"
"ligne suivante\n" );
Pas assez de variable local dans le main ? Utilisez les paramètres !
Les paramètres argc et argv du main ont souvent une image bien particulière dans la tête des développeurs, et on a naturellement peur d'y toucher.. Pourtant se sont des paramètres comme les autres, et les paramètres sont des variables locals comme les autres (avec une valeur par défaut qui est fournit par l'appelant). Rien ne vous empêche de les utiliser pour d'autre chose, et puis vous pouvez très bien utiliser plus de 3 paramètres) ! et vous pouvez utiliser d'autre type que prévu (rappelez vous le C est à typage faible).
Attention : C'est une mauvaise pratique ! C'est utilisé seulement pour l’obfuscation en C.
int main (int a, int b, int c) {
a = 1; b = 2; c = 3;
return c - b - a;
}
Type par défaut
En C, lorsque l'on doit définir un type, mais que l'on en précise pas, le type int est choisi par défaut ! C'est pour cette raison que je peux écrire "main()" au lieu de "int main()", ou même "main(a, b)".
main () {
const a = 0;
return a;
}
À noter qu'on ne peut tout de même pas écrire juste "a = 0;" car la ligne serait prise pour une affectation classique. Or dans une affectation, la left value doit être déjà définit et ce n'est pas le cas ici. Mais en ajoutant const la ligne ne peut etre qu'une définition de variable, le type int est alors choisit par défaut. Pour éviter cela on peut : placer a dans les paramètres du main, ou le placer en dehors du main (car il est interdit d’écrire du code en dehors d'une fonction, la ligne sera donc évaluée comme une définition de variable global).
Court-circuitage d’opérateur logique
Très connu et indispensable pour comprendre certain langage C !
Le principe est simple :
Lorsque l'on utilise un opérateur logique a && b si l’expression a est évalué fausse, il est inutile d’évaluer b car le résultat de l’opération sera forcement faux. De ce fait, l’expression b
ne sera pas évalué pour optimiser.
De même pour l’opérateur a || b, si a est évalué vrai, b ne sera pas évalué.
Et si b contient une fonction, elle ne sera donc pas exécutée.
#include "stdio.h"
int f () {
printf("[Exec] f\n");
return 0;
}
int main () {
printf ("\nif ( 0 && f() )\n");
if ( 0 && f() )
/* ... code */;
printf ("\nif ( 1 && f() )\n");
if ( 1 && f() )
/* ... code */;
printf ("\nif ( 0 || f() )\n");
if ( 0 || f() )
/* ... code */;
printf ("\nif ( 1 || f() )\n");
if ( 1 || f() )
/* ... code */;
return 0;
}
Ceci est valable en JS, C++, ... en faite dans la plupart des langages de programmation.
Opérateur virgule
L’opérateur virgule est l’opérateur le moins connu des développeurs, pourtant il existe en C, en java, en C++, en C#, en JS, ... (les langages inspiré du C)
Il se présente comme cela : "expr1, expr2"
Les 2 expressions sont évaluées et l’opérateur retourne le résultat de expr2.
(à ne pas confondre avec d'autre utilisation de la virgule dans le langage)
On l'utilise d’ailleurs souvent dans les boucles for de la manière suivante.
int i, j; // Attention, ceci n'est pas l’opérateur virgule ( c'est un piège ;) )
for ( i=0, j=1 ; j<N ; i++, j*=10 ) // <= Exemple ici !
printf("10^%d = %d\n", i, j); // Ici non plus, ce n'est pas l’opérateur virgule
Mais on peut l'utiliser pour faire des trucs drôles.
// programme C classique
if (a < b) {
min = a;
max = b;
} else {
min = b;
max = a;
}
// Même programme (mais pas du tout classique !)
// avec opérateur virgule et court-circuitage d’opérateur logique
(a<b) && ( // if a < b
(min = a),
(max = b),
1) || ( // else
(min = b),
(max = a)
);
Ceci est valable en JS, C++, ... en faite dans la plupart des langages de programmation.
Include automatique
Lorsque l'on utilise les fonctions standards les plus basiques mais que l'on ne a pas inclus la librairie pour l'utilise, le compilateur C voit une déclaration implicite. Mais pour certaine fonction il considère que son prototype est connu.
Rien d'extra ordinaire, mais il faut le savoir.
int main () {
printf("sans include !\n");
}
Passage de paramètre fantôme (exploitation de la pile)
Vous l'aurez remarqué, j'ai pu utiliser la fonction printf(), en précisant qu'un int était à afficher (en hexa et de 4 caractères "%4x") ainsi qu'une chaîne de caractère ("%s"). Or dans mon printf, il n'y a pas de paramètres !
Mais comment ça marche ? (désolé si les explications ne sont pas claire, le mieux et de tester par sois même pour bien le comprendre)
Pour cela il faut comprendre ce qu'est la pile (sous entendu la pile d’exécution ou call stack en anglais). Une pile est un objet informatique dans lequel on peut placer des élément à ressortir plus tard, dans le cas de la pile d’exécution, le dernier élément entrée est le premier à sortir (on appel cela une pile LIFO). Cette pile est indispensable au fonctionnement d'un programme, et c'est un comportement intégré au processeur, le processeur à un registre (ou 2) dédie à la gestion de la pile : SP pour Stack Pointer (mais aussi FP pour Frame Pointer, mais on ne s’intéressera qu'au SP pour simplifier). Ce pointeur indique où se trouve en mémoire le haut de la pile, et il suffit de l’incrémenter et de le décrémenter pour entrer et sortir des valeurs de la pile (attention la pile est souvent orienté en sens décroissant des adresses de la mémoire, mais cela n'as pas d'importance ici).
Dans le stack, on stock des tas d'informations indispensable au fonctionnement du programme et plus précisément des appels de fonctions. Par exemple on y stock : l'adresse de retour à la fonction appelante, une sauvegarde du FP de l'appelant, mais aussi tous les paramètres, et toutes les variables local d'une fonction ! (parfois les paramètre sont plutôt dans des registres du proc, mais l'idée reste la même)
Et ici on s’intéresse au paramètres envoyé à la fonction.
Si on fait 2 appels de fonction consécutif avec le même nombres de paramètre, il faut savoir que les paramètres vont en réalité prendre les même emplacement mémoire. (le premier appel utilise des cases mémoires nécessaire dans la pile puis les libèrent une fois la fonction fini, puis le 2eme appel fait de même et utilise donc l'emplacement mémoire qui vient d’être libère par le premier appel).
Il faut aussi savoir, que lorsque la mémoire de la pile est libéré, elle n'est pas effacée, alors les paramétrés envoyé à la fonction reste en mémoire.
Alors si l'on se débrouille pour ne pas remplir tous les paramètres du 2eme appel, il va prendre les paramètres qui ont été écrit par le premier appel.
Et justement, les fonctions qui utilise des va_arg (nombre d'argument variable) comme printf, ne vérifient pas le nombre d’argument passé. donc je peux écrire :
#include <stdio.h>
int f (char* a, int b, int c) {}
int main () {
printf("param1 : %d, param2 : %d \n", 10, 20);
// équivalant à :
f(NULL, 10, 20)
printf("param1 : %d, param2 : %d \n");
return 0;
}
Si on est vraiment méchant, on peut même utiliser un mauvais nombre d'argument dans f..
Il faut savoir que si l'on fait cela, nos valeur (ici 10 et 20) ne sont pas protégé. Ce qui, dans certain cas, est dangereux (sur un microcontrôleur en fonctionnement par interruption, nos valeurs pourrait être écrase par un appel par interruption par exemple).
J'ajouterais que si l'on connaît bien le fonctionnement de la pile, on peut construire des choses plus complexe, c'est ici juste un exemple.
Cacher une affectation
Ici la faille est humaine:
int isAdmin = 0;
checkAdmin(&isAdmin);
if(isAdmin=1)
doAdminStuff();
else
doOtherStuff();
Vous l’aurez peut-être vu au premier coup d’œil, dans la condition du if, il s'agit d'une affectation (=) et non d'un test d’égalité (==). C'est classique de faire cette erreur, mais dans le cas où c'est réalisé volontairement cela peut constituer une véritable backdoor.
Un petite technique pour éviter cela.
// écrire :
if ( 0==a ); // (0=a) ne compile pas !
// plutôt que :
if ( a==0 ); // (a=0) compile et a un comportement très différent.
Pour être encore moins visible on peut écrire:
if(isAdmin =! 0)
Qui n'est pas un test d’inégalité, mais une affectation à la valeur !0 qui est la valeur 1.
Operateur -->
Merci à @numaru (sur Discord, je ne connais pas ton pseudo sur le forum) pour m'avoir signalé ce trick.
Je parie que vous ne connaissez pas l’opérateur --> (ou son alternative <--)
int i = 20;
while (i --> 0) { // i --> 0 // i from 19 to 0
// 0 <-- i // i from 19 to 1
printf("i = %d\n", i);
// do some stuff ...
}
Ce code compile bien, et si vous ne connaissez pas ce fameux operateur... c'est simplement parce qu'il n'existe pas !
C'est simplement une utilisation un peu mesquine et caché de l’opérateur de post-incrémentation(i--) suivi d'une comparaison (>). On peut le voir comme cela :
// code équivalent
int i = 20;
while ((i--) > 0) { // (i--) > 0 // i from 19 to 0
// 0 < (--i) // i from 19 to 1
printf("i = %d\n", i);
// do some stuff ...
}
Ce code peut être vu comme une drôle de coïncidence qui fait coller une utilisation un peu abusive de la syntaxe à une syntaxe qui fait exactement se quelle explicite. Mais il faut bien-sur éviter de l'utiliser dans un code qui se veut claire.
Bonus ! (C++ vs C et lvalue vs rvalue)
En C++ on peut aller plus loin:
int i = 20;
while (0 <---- i) { // compile
// while (i ----> 0) { // compile pas
// do some stuff ...
}
Il peut semblé étrange que i ----> 0 ne passe pas alors que 0 <---- i.
Pour expliquer cela je doit aborder la notion de lvalue et de rvalue.
La notion de lvalue & rvalue
Dans tout langage qui utilise des affectations de valeurs on distingue le côté gauche et le côté droit d'une affectation de la manière suivante:
[lvalue] = [rvalue]
left = right
A = B
Les 2 côtés de l’affectation ont des propriétaires bien différentes. En effet l'expression de gauche doit être accessible en écriture, et le coder droit en lecture.
Dit autrement, la lvalue doit représenter un objet qui a un emplacement en mémoire. Les rvalues, à l'inverse, n'en ont pas.
Et on peut ajouter la régle suivante, toutes lvalue peut être convertie en rvalue. l’opération inverse est impossible. (enfin on peut dire que si mais elle est explicite, c'est l'affectation de valeur)
Si on prend un exemple, on peut donner le type de chaque expression (-> représente ici une conversion):
int i = 20; // (i lvalue) = (20 rvalue)
i = 5 + i; // (i lvalue) = (((5 rvalue) + (i lvalue->rvalue)) rvalue)
On respect bien nos règles :) ! C'est super non ?
Mais alors, pourquoi i---- ou ((i--)--) ne compile pas et ----i ou (--(--i)) compile !?
Et bien en C++ i-- retourne une rvalue, alors que --i retourne une lvalue. (ce qui est logique puisque --i retourne la valeur réelle de l'objet alors que i-- ne peut pas)
Et l’opérateur de post ou pré-incrémentation ne peut prendre que une lvalue en entrée (car c'est une affectation de valeur !).
Donc dans i-- -- on utilise ()-- sur une rvalue, hors c'est illégale.
"En C, la post et pré incrémentation / décrémentation retourne toujours une rvalue, donc tout ceci ne fonctionne pas. Mais la notion est importante.
"
printf Success !
Petite particularité du printf. Sous les système unix-like, le format %m ne prend pas de valeur et affiche "Success". Utile pour rendre du texte invisible dans le code !
printf("%m"); // Affiche: Success
Afficher du texte avec des nombres (hexa)
Une autre méthode pour rendre du texte invisible: utiliser des nombres en hexadécimal (a, b, c, d, e, f) pour afficher du texte. Bien-sur on est limité aux caractères entre A et F (en majuscule et minuscule) mais on peut aussi profiter du nombre zero pour imiter la lettre O majuscule.
Avec printf:
On peu utiliser %x pour afficher un nombre hexa en caractère minuscule (a - f).
On peu utiliser %X pour afficher un nombre hexa en caractère minuscule (A - F).
On peu utiliser %2x ou %2X pour afficher un nombre hexa en limitant l'affichage à 2 caractère.
printf("%4x-%4X\n", 0xDEAD, 0xC0DE); // affiche: dead-C0DE
Pour vraiment cacher le texte, il suffit de convertir les nombre hexa en decimal ou octal
(hexa) 0xDEAD = (decimal) 57005 = (octal) 0157255
(hexa) 0xC0DE = (decimal) 49374 = (octal) 0140336
printf("%4x-%4X\n", 0xDEAD, 0xC0DE); // affiche: dead-C0DE via hexa
printf("%4x-%4X\n", 57005, 49374); // affiche: dead-C0DE via decimal
printf("%4x-%4X\n", 0157255, 0140336); // affiche: dead-C0DE via octal
Duff's Device
Le Duff's Device est une optimisation qui exploite une boucle do while et un switch case imbriqué qu'une manière particulièrement deroutante.
Voici le code (ce n'est pas l'original, mais l'idée est là, et le code suppose que count > 0):
void copy(char* to, char* from, size_t count) {
size_t n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
void copy_no_opti (char* to, char* from, size_t count) {
do {
*to++ = *from++;
} while (--count > 0);
}
Le code non-optimisé devra realiser un saut entre la fin de la boucle et le debut pour chaque octet à copier.
Dans le code optimisé, on réduit le nombre de saut en effectuant plusieurs copie par passage de boucle. Mais si on utiliser une simple boucle contenant plusieurs copies, il faudrait que count soit toujours un multiple de 8 ( c'est à dire count = 8*k ).
Pour vous aidez a bien comprendre, le do while a pour but d’arrêter la copie des donnée quand le compteur n arrive à 0, c'est à dire quand plus aucune donnée sont à écrire.
Et le switch à pour but de de sauter un certain nombre de copie de donnée dans le cas ou le count n'est pas divisible par 8. Si count est de la forme 8*k + 1 le modulo par 8 retourne 1, et il faudra faire une seule copie, on saute donc 7 copie pour n'en effectuer qu'une seule au premier cycle.
pour la suite, chaque cycle effectue 8 copies car nous ne repassons pas par le switch.
Je précise que la structure du code est tout à fait valable en C.
Tableau statique
Ceci s’éloigne un peu de l'obfuscation, et ce n'est pas un comportement étrange, mais c'est quelque chose de souvent très mal compris et pourtant très important à bien comprendre, je vais donc tenter de le ré-expliquer.
On vous a peut-etre dit qu'un tableau statique est un pointeur vers une zone de mémoire statique qui contient les valeurs du tableau.
char array[] = "123456789ABCDE"; // tableau statique
Si c'est le cas, la case mémoire de array devrait contenir une adresse (c'est la définition d'un pointeur). Et bien observons cela en effectuant un dump du morceau de mémoire !
#include "stdio.h"
/// Affiche le contenu de la mémoire entre 2 adresses.
void dump_mem (char* from, char* to) {
printf("[Dump] from %p to %p\n", from, to);
for(char* at=from ; at <= to ; at++) // parcourir la mémoire entre 'from' et 'to'
printf("%02X ", *at & 0xFF); // Afficher l'octet
printf("\n");
}
int main () {
char a= 0xAA; // identifier la fin
char array[] = "123456789ABCDE"; // donnée à etudier
char b= 0xBB; // identifier le debut
printf( "array addr : %p\n", array ); // <= Adresse du tableau
printf( "array ascii: \"%s\"\n", array ); // <= Contenu (en ascii) de la memoire à cette adresse
dump_mem(&b, &a); // <= Dump de la memoire à cette adresse
return 0;
}
array addr : 0x7ffdfd760c00
array ascii: "123456789ABCDE"
[Dump] from 0x7ffdfd760bff to 0x7ffdfd760c0f
BB 31 32 33 34 35 36 37 38 39 41 42 43 44 45 00 AA
^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^
b array a
Vous noterez que la variable b ce trouve avant a, alors que a est déclaré avant b. C'est dut au fait que le stack (la pile) est souvent construite de manière à remonter les adresses. La pile étant placé après le code, si aucune vérification de dépassement de pile est effectué elle écrase le code en cas de dépassement. Cela arrive sur les microcontrôleurs.
On peut voir que array contient <31 32 33 34 35 36 37 38 39 41 42 43 44 45 00>, ce qui correspond en ascii à "123456789ABCDE". La case mémoire array ne contient donc pas d'adresse !
Et pourtant printf("%p", array) à afficher une adresse :o !
Et oui car en réalité, le mot clef array correspond à l'adresse de la zone de mémoire, mais cette adresse n'est pas dans une zone de mémoire comme les variables, ce n'est donc pas un pointeur mais simplement une adresse !
Il est donc impossible d’écrire des choses comme :
char str[] = "ABC";
array = str; // pas bien
// ou
array++; // pas mieux !
Et ce n'est pas non plus une constante. Car en C une constante à une zone de mémoire (sur laquelle il est interdit d’écrire). C'est une adresse/un nombre, au même titre que dans printf("%d", 8), 8 est un nombre.
et il ne vous viendrez pas à l'esprit d’écrire:
int i = 42;
8 = i;
// ou
8++;
Et bien vous devez vous mettre en tête que le mot clef array (le nom d'un tableau statique) se manipule comme 8, comme un nombre. :)
Suite à venir...
Pour vous teaser si cela vous plaît voici une idée de la suite:
- Tableau statique VS pointeur de tableau statique
- do while(0) ?
- le retour du scanf
- Tableau statique VS pointeur de tableau statique
- printf / scanf format avancé
- ...peut-etre d'autre si cela me revient ...
- trigraphs & bigraphs (merci à @Numaru)
Et si vous connaissez des choses du genre, dite toujours, je suis friand de ça ;) !
Et vous pouvez visiter le site de IOCCC pour voir des exemples de code complètement dingue.