Ce qu'il faut surtout savoir, c'est que le C est un langage très proche du fonctionnement réel du micro-processeur et, quand on connaît celui-ci, les caractéristiques et les limitations du C deviennent naturelles.
Le micro-processeur est indépendant, en lui-même, de la machine dans laquelle il est installé et on peut ainsi retrouver le même CPU dans des ordinateurs très différents. Mais dans tous les cas, il ne communique avec l'environnement qu'avec un bus d'adresse, un bus de données et une ligne R/W (Read or Write). Les « bus » sont un ensemble de lignes électriques parallèles distribuées simultanément à tous les périphériques d'un ordinateur. Le bus d'adresse sert donc à coder un nombre binaire indiquant le numéro de l'octet en mémoire auquel on cherche à accéder, le bus de données à véhiculer la donnée, soit pour lire son contenu, soit pour écrire dedans et la ligne R/W à savoir justement laquelle des deux opérations on cherche à faire.
Ça veut dire que la seule chose que voit le micro-processeur, c'est une longue plage continue d'octets, eux-mêmes numérotés de 00000000 à FFFFFFFF sur les architectures 32 bits. Ce plan mémoire est principalement occupé par des plages de RAM, quelques plages de ROM, quelques zones vides éventuellement (il y a quelques années, on aurait dit « pratiquement toujours » mais aujourd'hui, les machines équipées de 4 Gio de RAM au moins deviennent courantes) et quelques mini-plages réservées aux ports d'entrées-sorties servant à piloter les différents périphériques et composants de la machine. Ça veut dire également que la seule chose, en fin de compte, que sait faire un micro-processeur, c'est lire des données à un certain endroit, appliquer une opération logique ou arithmétique dessus, et la ré-écrire à un autre. Il est donc possible, en théorie, de piloter un ordinateur entier uniquement avec l'équivalent de « PEEK » et « POKE » en Basic.
Donc, lorsque tu fais « x=1 » dans n'importe quel langage pour affecter la valeur « 1 » à la variable « x », en particulier avec un langage interprété, tu fais appel aux routines du logiciel que tu utilises qui, elles, vont aller déposer cette valeur quelque part en mémoire, dans une zone réputée décrire le contenu de la variable « x ». Le processus est généralement invisible au programmeur mais si tu te débrouilles pour savoir quel est l'adresse de cet emplacement, alors tu peux le modifier en écrivant dedans. Évidemment, c'est de la bidouille et tu fais ta manip' dans le dos du langage, mais c'est marrant de voir sa variable « muter » comme par magie.
En C, en revanche, c'est totalement normal : on manipule directement les adresses mémoire sans utiliser de système sous-jacent, ne serait-ce que parce que le langage C sert justement à écrire ces systèmes. En fait, c'est pour cela qu'il a été conçu : compiler des programmes en un code autonome et restant au plus proche de la machine tout en s'affranchissant de l'assembleur et en écrivant donc des programmes en principe portables partout. Il faut aussi souligner que le C est apparu en 1972 et qu'à cette époque, les ressources systèmes étaient autrement plus limitées qu'elles le sont aujourd'hui.
Maintenant, un pointeur est, comme son nom l'indique, quelque chose qui « pointe » une autre chose en mémoire, donc qui indique l'emplacement. Un pointeur est donc l'adresse mémoire de quelque chose et, par extension, la variable qui contient cette adresse. Un pointeur est donc une variable de type « adresse mémoire » et a généralement la largeur du bus d'adresse, donc quatre octets sur une machine 32 bits. En ce sens, un pointeur est donc en fait un entier non signé mais, sémantiquement parlant, il a été décidé d'en faire un type à part entière. D'abord pour informer le compilateur de ce dont il s'agit, ensuite parce que le format minimum des entiers imposé par la norme C ne correspond pas forcément avec celui du bus d'adresse de la machine cible, et enfin parce qu'il existe une arithmétique propre aux pointeurs, alors qu'en revanche, toutes les opérations applicables aux entiers numériques n'ont pas forcément de sens avec un pointeur, par exemple, le multiplier. Pas plus qu'additionner deux pointeurs d'ailleurs : ce n'est pas parce que toi, tu habites au № 5 et un de tes amis au № 8 qu'à vous deux, vous habitez au № 13. :-)
Si j'utilise « & », je peux connaître l'emplacement en mémoire de mes variables et du code de mes fonctions (plus précisément, leur point d'entrée). Par exemple, en écrivant ceci :
#include <stdio.h>
int main (void)
{
int x;
int y;
printf ("Emplacement de x : %p\n",&x);
printf ("Emplacement de y : %p\n",&y);
return 0;
}
… j'obtiens :
Emplacement de x : 0x7fff93cc99dc
Emplacement de y : 0x7fff93cc99d8
On voit donc clairement où mes variables sont matérialisées, et on voit également que « x » se trouve quatre octets après « y » parce mes entiers tiennent sur 32 bits eux aussi.
Par contre, le seul service que te garantit un pointeur est conserver une adresse mémoire, à laquelle est censé se trouver un objet de type connu. Mais il n'y a aucune garantie que cette adresse soit valide si tu ne l'as pas initialisé correctement. Par exemple, dans l'exemple précédent, si j'affecte à mon pointeur l'adresse de « y » et que je me débrouille pour le faire avancer d'exactement deux octets, je vais me retrouver « à cheval » entre deux variables.
#include <stdio.h>
int main (void)
{
int x = 0;
int y = 0;
unsigned int * ptr = NULL;
printf ("Avant : x=%d ; y=%d\n",x,y);
ptr = (unsigned int *)(((char *)&y)+2);
*ptr = 0xffffffff;
printf ("Après : x=%d ; y=%d\n",x,y);
printf ("%p %p %p\n",&x,&y,ptr);
return 0;
}
… ce qui donne :
Avant : x=0 ; y=0
Après : x=65535 ; y=-65536
0x7fff720800b4 0x7fff720800b0 0x7fff720800b2
On voit que les deux variables sont affectés. Le C me laisse le faire. Il me laisse même écrire n'importe où en mémoire si j'en ai envie, écrasant éventuellement les autres programmes ou le système d'exploitation, comme on pourrait le faire en assembleur. Heureusement, le mode protégé est désormais sur toutes les machines et c'est le micro-processeur lui-même qui refusera de continuer si on lui demande de lire ou d'écrire dans une zone non explicitement déclarée par ton O.S., déclenchant la célibrissime « segfault ».
[^] # Re: Structure
Posté par Obsidian . En réponse au message retourner un pointeur vers un tableau de pointeurs. Évalué à 10.
Ce qu'il faut surtout savoir, c'est que le C est un langage très proche du fonctionnement réel du micro-processeur et, quand on connaît celui-ci, les caractéristiques et les limitations du C deviennent naturelles.
Le micro-processeur est indépendant, en lui-même, de la machine dans laquelle il est installé et on peut ainsi retrouver le même CPU dans des ordinateurs très différents. Mais dans tous les cas, il ne communique avec l'environnement qu'avec un bus d'adresse, un bus de données et une ligne R/W (Read or Write). Les « bus » sont un ensemble de lignes électriques parallèles distribuées simultanément à tous les périphériques d'un ordinateur. Le bus d'adresse sert donc à coder un nombre binaire indiquant le numéro de l'octet en mémoire auquel on cherche à accéder, le bus de données à véhiculer la donnée, soit pour lire son contenu, soit pour écrire dedans et la ligne R/W à savoir justement laquelle des deux opérations on cherche à faire.
Ça veut dire que la seule chose que voit le micro-processeur, c'est une longue plage continue d'octets, eux-mêmes numérotés de 00000000 à FFFFFFFF sur les architectures 32 bits. Ce plan mémoire est principalement occupé par des plages de RAM, quelques plages de ROM, quelques zones vides éventuellement (il y a quelques années, on aurait dit « pratiquement toujours » mais aujourd'hui, les machines équipées de 4 Gio de RAM au moins deviennent courantes) et quelques mini-plages réservées aux ports d'entrées-sorties servant à piloter les différents périphériques et composants de la machine. Ça veut dire également que la seule chose, en fin de compte, que sait faire un micro-processeur, c'est lire des données à un certain endroit, appliquer une opération logique ou arithmétique dessus, et la ré-écrire à un autre. Il est donc possible, en théorie, de piloter un ordinateur entier uniquement avec l'équivalent de « PEEK » et « POKE » en Basic.
Donc, lorsque tu fais « x=1 » dans n'importe quel langage pour affecter la valeur « 1 » à la variable « x », en particulier avec un langage interprété, tu fais appel aux routines du logiciel que tu utilises qui, elles, vont aller déposer cette valeur quelque part en mémoire, dans une zone réputée décrire le contenu de la variable « x ». Le processus est généralement invisible au programmeur mais si tu te débrouilles pour savoir quel est l'adresse de cet emplacement, alors tu peux le modifier en écrivant dedans. Évidemment, c'est de la bidouille et tu fais ta manip' dans le dos du langage, mais c'est marrant de voir sa variable « muter » comme par magie.
En C, en revanche, c'est totalement normal : on manipule directement les adresses mémoire sans utiliser de système sous-jacent, ne serait-ce que parce que le langage C sert justement à écrire ces systèmes. En fait, c'est pour cela qu'il a été conçu : compiler des programmes en un code autonome et restant au plus proche de la machine tout en s'affranchissant de l'assembleur et en écrivant donc des programmes en principe portables partout. Il faut aussi souligner que le C est apparu en 1972 et qu'à cette époque, les ressources systèmes étaient autrement plus limitées qu'elles le sont aujourd'hui.
Maintenant, un pointeur est, comme son nom l'indique, quelque chose qui « pointe » une autre chose en mémoire, donc qui indique l'emplacement. Un pointeur est donc l'adresse mémoire de quelque chose et, par extension, la variable qui contient cette adresse. Un pointeur est donc une variable de type « adresse mémoire » et a généralement la largeur du bus d'adresse, donc quatre octets sur une machine 32 bits. En ce sens, un pointeur est donc en fait un entier non signé mais, sémantiquement parlant, il a été décidé d'en faire un type à part entière. D'abord pour informer le compilateur de ce dont il s'agit, ensuite parce que le format minimum des entiers imposé par la norme C ne correspond pas forcément avec celui du bus d'adresse de la machine cible, et enfin parce qu'il existe une arithmétique propre aux pointeurs, alors qu'en revanche, toutes les opérations applicables aux entiers numériques n'ont pas forcément de sens avec un pointeur, par exemple, le multiplier. Pas plus qu'additionner deux pointeurs d'ailleurs : ce n'est pas parce que toi, tu habites au № 5 et un de tes amis au № 8 qu'à vous deux, vous habitez au № 13. :-)
Si j'utilise « & », je peux connaître l'emplacement en mémoire de mes variables et du code de mes fonctions (plus précisément, leur point d'entrée). Par exemple, en écrivant ceci :
… j'obtiens :
On voit donc clairement où mes variables sont matérialisées, et on voit également que « x » se trouve quatre octets après « y » parce mes entiers tiennent sur 32 bits eux aussi.
Par contre, le seul service que te garantit un pointeur est conserver une adresse mémoire, à laquelle est censé se trouver un objet de type connu. Mais il n'y a aucune garantie que cette adresse soit valide si tu ne l'as pas initialisé correctement. Par exemple, dans l'exemple précédent, si j'affecte à mon pointeur l'adresse de « y » et que je me débrouille pour le faire avancer d'exactement deux octets, je vais me retrouver « à cheval » entre deux variables.
… ce qui donne :
On voit que les deux variables sont affectés. Le C me laisse le faire. Il me laisse même écrire n'importe où en mémoire si j'en ai envie, écrasant éventuellement les autres programmes ou le système d'exploitation, comme on pourrait le faire en assembleur. Heureusement, le mode protégé est désormais sur toutes les machines et c'est le micro-processeur lui-même qui refusera de continuer si on lui demande de lire ou d'écrire dans une zone non explicitement déclarée par ton O.S., déclenchant la célibrissime « segfault ».