INF155 : quatrième cours
Tout sur les adresses
La mémoire
La mémoire symbolique d’une machine est un vecteur d’octets. Chaque octet possède un
numéro dans l’intervalle [ 0, quantité de mémoire ]. Tout programme, peu importe la langue de
programmation, se sert uniquement de cette mémoire symbolique.
La mémoire physique d’une machine dont la géographie, le maintien et le comportement sont d’une
grande complexité, est toujours atteinte avec le contrôleur d’adresse au service de tout programme dont
le système d’exploitation(OS)
Une adresse
Une adresse est une valeur qui possède une position mémoire et un type (et ainsi pouvoir
considérer son contenu). C’est la conjonction d’un numéro d’octet de départ dans la
mémoire et d’un contenu de type T.
L’adresse d’un contenu de type T possède le type T*
Une adresse permet des expressions utiles puisque de sa valeur et dépendant de T on
pourra soit
1. atteindre son contenu en lecture comme en écriture (ssi sizeof(T) est défini)
2. se déplacer de cette position vers d’autres adresses en décalage d’elle(ssi sizeof(T)
est défini, se fait de façon honnête essentiellement dans un tableau)
3. déclencher une fonction à partir d’une adresse de fonction
une adresse de type void* peut exister, elle a l’avantage de pouvoir conserver la position d’un
objet peu importe son type T, mais on perd alors la capacité d’accès au contenu.
Les pointeurs
On donne le nom générique de pointeur à toute variable faite pour conserver une adresse.
Pour un type T quelconque, la déclaration d’une variable de type T* est possible. Ce pointeur
est apte à garder une adresse dont le contenu est un T. Ainsi, avec la déclaration T * ptr
(sizeof(T) existe et T n’est ni void ni de type fonction), l’expression *ptr donnera accès au contenu
de type T à la valeur de ptr.
Remarquez dans une déclaration T*… l’apparition du troisième et dernier opérateur de type du
C , après T…[ pour le tableau et T…( pour la fonction.
Au contraire des deux opérateurs précédents, l’opérateur * ne génère pas d’incompatibilité
avec un autre opérateur.
Lorsqu’un pointeur est déclaré, celui-ci n’est pas initialisé automatiquement à une adresse
correcte. L’utilisation d’un pointeur non initialisé est une erreur grave et malheureusement
fréquente en C. Le compilateur ne prend aucune responsabilité sur l’utilisation du contenu d’un
pointeur. Pour aider le programmeur, la norme ANSI offre le symbole NULLéquivalent normalement à la valeur 0 et
assignable à tout type de pointeur pour indiquer qu’il n’adresse rien mais rien de plus ne vous aidera
On peut déclarer
1. une variable, pointeur de type T* ( apte à conserver l’adresse d’un objet de type T)
2. des tableaux T*…[ ] de pointeurs T*
3. une fonction qui pourra retourner un T* ou qui pourra avoir un T* en paramètre formel.
Précision importante
1. une adresse n’est jamais à priori l’équivalent d’une simple position dans la mémoire.
2. La taille en octets des adresses sizeof(T*) dépend du modèle de compilation choisi avec
le compilateur utilisé. Avec la taille des mémoires actuelles sur les ordinateurs
personnels, toutes les adresses sont normalement gardées sur quatre octets. En
ingénierie, la programmation sur de toutes petites (micro)plate-formes se fait encore
avec des adresses plus petites.
Comment obtenir des adresses dans un programme
1. Avec l’opérateur & : L’expression obtenue de l’opérateur & en préfixe à une variable de
type T est évaluée comme une adresse de type T*. Cet opérateur est utilisé avant
tout lors du passage de paramètres par référence à une fonction. Note très
technique…. la classe d’allocation register et l’opérateur & sont incompatibles.
2. À l’aide du nom d’un tableau : Dans une expression, le nom d’un tableau contenant des
objets de type T est automatiquement évalué comme une adresse de type T*
correspondant à l’adresse du premier élément du tableau. ce qui explique qu’un tableau
utilisé en paramètre effectif puisse être modifié à travers le paramètre formel dans la fonction
3. Par arithmétique des adresses : À l’aide de tout opérateur d’ addition ou de
soustraction des entiers (+ - ++ -- += -= ), on peut obtenir une nouvelle adresse. Une
section plus bas décrit toute l’arithmétique des adresses.
4. Par allocation programmée : En Run Time du programme, la fonction classique
malloc permet la demande au système d’exploitation d’un bloc d’octets de la taille
donnée en paramètre. La fonction malloc retourne en cas de succès l’adresse (void*) du
premier octet du bloc d’octets offert alors par l’OS dans le heap de la machine (c’est la
partie de la mémoire non-utilisée lors de la demande) ou la constante symbolique NULL en
cas d’échec. Ces blocs d’octets n’ont pas de classe d’allocation, de « storage class » qui
définit normalement la durée de vie et l’accessibilité de toute mémoire obtenue
automatiquement en compilation (Compile time) pour le programme. Ces blocs de
mémoire obtenus dynamiquement (synonyme d’allocation programmée) sont tous à la
charge du programmeur qui devra les libérer explicitement avec la fonction free une fois
leur usage complété. La perte par le programmeur d’une adresse obtenue en allocation
programmée amène une perte très réelle de mémoire « dite memory leak » de la
machine hôte pendant toute l’exécution du programme. Ces bugs sont pervers et
amènent implacablement au plantage de l’application.
5. À l’aide du nom d’une fonction : Dans une expression, le nom d’une fonction donne
l’adresse du premier octet de son code. Le type de cette adresse est associé à la
déclaration de la fonction. Le nom d’une fonction pourra donc servir de paramètre
effectif et cette fonction pourra être déclenchée avec son paramètre formel
correspondant.
6. Par transtypage : Le transtypage (typecasting) d’une adresse correcte donne une
adresse différente. La position n’est pas changée mais le contenu est différent. La forme
la plus correcte devrait se faire en transitant par void*. Peu importe le type de l’adresse
addr, l’expression (T1*)(void*) addr sera de type T1* . Aucun compilateur ne
donne de limites à ces transtypages. Tout transtypage d’adresse est potentiellement
dangereux et le programmeur en est totalement responsable, donc un transtypage
d’adresse DOIT TOUJOURS être abondamment commenté.
7. Par adressage absolu : Un entier peut être considéré comme une adresse. Par exemple,
l’expression (int*)(void *) 1600 permet d’avoir accès à un entier à l’octet numéro
1600 en mémoire. Cette pratique n’est rencontrée qu’en programmation sur de micro-
plateformes , elle est absolument NON PORTABLE et interdite dans notre cours.
L’indirection, comment atteindre un contenu
Dans une expression, l’opérateur d’indirection * en préfixe à une valeur adresse sera évaluée et
permet d’accéder au contenu de cette adresse une seule exception naturelle décrite au point 3).
Ce qui peut être par exemple :
1. Une addresse addr de type double* ou int* qui est l’adresse d’une valeur réelle ou
entière. L’expression *addr permet d’atteindre son contenu en lecture comme en
écriture. L’expression sizeof(*addr) est correcte et donne la taille en octets du
contenu.
2. Une adresse addr de type double( * )(double) par exemple est l’adresse d’une
fonction d’une variable réelle (mat 145). L’expression addr(2.56) ou (*addr)(8.65)
permet de déclencher l’exécution de la fonction et la valeur retournée sera réelle.
Remarquez que l’utilisation de l’opérateur * est alors superflue et que seules les fonctions ont ce
privilège. Dans ce cas, l’expression sizeof(*addr) ne s’applique pas puisque la
déclaration d’une fonction ne suffit pas à connaître le nombre d’octets de son code.
3. Une adresse de type void* seule la position mémoire est importante et le type du
contenu volontairement éludé. Le besoin d’adresses génériques -- peu importe alors le type
du contenu -- a fait naître ce type spécial. On les utilise pour la comparaison (memcmp)
ou la copie (memcpy) de blocs d’octets de toutes tailles ou pour obtenir (malloc, realloc)
ou libérer (free) de la mémoire obtenue par allocation programmée. Ni l’indirection, ni
l’arithmétique des adresses ne s’appliquent à une adresse void * .
L’arithmétique des adresses
Les opérations arithmétiques permises sur une adresse tiennent compte de la taille de son
contenu. Ainsi, on ne pourra pas effectuer d’opération sur une adresse dont le contenu n’admet
pas l’opérateur sizeof -- adresses de fonctions et adresses génériques (void*) --.
1. La somme d’une adresse et d’un entier donne une adresse : Avec une adresse addr de
type T* et un entier positif n, (addr + n) est une adresse de type T* et sa position est
avancée de n * sizeof(T) octets dans la mémoire par rapport à addr.
2. La différence d’une adresse et d’un entier donne une adresse : Avec une adresse addr
de type T* et un entier positif n, (addr - n) est une adresse de type T* et sa position
est reculée de n * sizeof(T) octets dans la mémoire par rapport à addr.
3. La différence de deux adresses du même type T* donne un entier : La valeur obtenue
correspond au nombre d’objets de type T entre les deux adresses. Attention, ne
confondez pas ici nombre d’objets et nombre d’octets.
L’usage portable de l’arithmétique des adresses n’a de sens que sur des adresses à
l’intérieur du même tableau.
Le passage par référence (par adresse)
La programmation moderne, structurée et modulaire, encourage la création de fonctions et
l’absence de variables globales. Cette pratique amène avec elle l’utilisation massive de la
paramétrisation.
En C, tout passage en paramètre (du paramètre effectif au paramètre formel) se fait par copie.
Cette pratique est efficace pour les types de base. Pour un tableau ou un objet structuré dont
la taille est souvent imposante, le gaspillage de temps et de mémoire rend cette méthode
inacceptable C’est l’adresse d’un tel objet plus massif qui sera toujours passée à une fonction
dans un paramètre formel pointeur (on devra alors spécifier si le contenu sera constant ou non). Cette
pratique ne nécessite la copie que de quelques octets et empêche si nécessaire toute
modification des données originales.
C’est une erreur sémantique d’oublier la présence du const dans la déclaration du paramètre
formel pointeur si le contenu à l’adresse donnée en paramètre effectif ne doit pas être modifié.
Le mot réservé const et la déclaration des pointeurs
La présence de const à la déclaration d’un pointeur est particulièrement intéressante et
amène une agréable
1. Voudrez-vous signifier par ce const que le contenu obtenu par indirection de sa valeur--
ne puisse être modifié mais que le pointeur lui-même reste sujet à l’assignation et puisse
être modifié.( adresse modifiable, contenu invariable)
2. Voudrez-vous que la valeur donnée au pointeur avec une initialisation obligatoire reste
invariante et que le contenu à cette adresse puisse être modif reste sujet à
l’assignation— ( adresse invariable, contenu modifiable)
3. Et pourquoi pas les deux : adresse invariable, contenu invariable.
Le C vous laissera choisir celui des trois dont vous aurez besoin.
Ainsi les délarations
const T * pt1 ; donne un pointeur de la forme #1
T * const pt2 = adr ; (adr est une adresse valide) donne un pointeur de la forme #2
const T * const pt3 = adr ; donne un pointeur de la forme #3
Supplément en allocation programmée
Utiliser malloc, une fonction (ANSI) qui offre au client d’obtenir en exécution du programme l’adresse du
premier octet d’un groupe de taille définie d’octets contigus (cette valeur entière sera le paramètre de la
fonction) actuellement libres dans la mémoire (heap) de l’appareil sur lequel s’exécute le programme.
Cette fonction implémente une demande directe à l’O.S. (operating system ou système d’exploitation) qui
doit essayer de la satisfaire. Si la demande est possible et le bloc d’octets réservé (plus le paramètre est
grand et moins vous avez de chance), l’O.S. vous assure que ce bloc d’octets sera réser au programme
qui en a fait la demande jusqu’à ce qu’une autre instruction directe du programme (appel de la fonction
free avec comme paramètre l’adresse obtenue précédemment ) ne les libère ou que le programme se
termine.
Ces blocs dynamiques d’octects n’ont pas de classe d’allocation ( le storage class obtenu en compile time
comme auto, register , static ou extern) comme les déclarations de variables ou de fonctions dont vous
connaissez déjà l’existence.
Le maintien d’un bloc obtenu dynamiquement est tout à la charge du programmeur.
Le résultat d’une allocation programmée sera :
NULL(symbole de stdlib) si l’OS est incapable de fournir le bloc du nombre d’octets demandé.
Une adresse -- void * -- différente de 0 en cas de succès. C’est au demandeur de transtyper
« typecast » l’adresse reçue (bien qu’ANSI accepte l’assignation directe d’une valeur void*,
certains compilateurs trop sérieux grognent encore en émettant un warning) et de la garder
précieusement puisque si vous la perdez, ce bloc devient inutilisable pour vous comme pour l’OS
beau gaspillage --
Avantages et désavantages de l’allocation programmée :
On obtient facilement des tableaux de tous types en allocation programmée, ces tableaux sont
ultra-utilisés, on leur donne même le nom de tableaux dynamiques.
La mémoire est utilisée avec efficacité, bien souvent un programme complexe a des besoins très
différents en mémoire dépendant des conditions initiales d’une exécution donnée c’est
normalement le cas des simulations numériques --.
Elle est nécessaire à la construction de structures de données permettant d’accumuler du data en
cours de traitement en utilisant toujours juste la mémoire suffisante.
Ces appels à l’OS sont lents et peu sûrs puisque l’état d’utilisation du Ram de la machine dicte
aussitôt le résultat de la demande!! Beaucoup de programmes où la fiabilité est essentielle
médical, transport aérien…. apprécient très peu ou refusent à priori l’allocation programmée.
Pour conclure en allocation programmée :
Vous ne verrez aucune différence à l’usage d’un tableau qu’il soit obtenu en allocation programmée ou
statiquement lors de la compilation avec un storage class défini.
C’est uniquement avant et après usage que se situent les différences.
En fait lorsque l’on comprend bien le sens du malloc et du free, l’allocation programmée n’a plus de secret
mais elle garde ses dangers.
NB on peut aussi allonger ou raccourcir un tableau dynamique avec la fonction realloc de stdlib. Je ne
la décris pas actuellement …. Commencez par bien utiliser malloc
Et pourquoi une fonction retounerait-elle une adresse?
Question importante dont les réponses sont traditionnelles :
1. La fonction retourne l’adresse d’un tableau dynamique dont les valeurs sont sans aucun
doute bien initialisées. La taille du nouveau tableau ne sera pas inconnue, soit obtenue
de ses paramètres effectifs ou liée en sortie à un paramètre par référence. Ces
fonctions réalisent fréquemment des filtres de tableaux existants : pour un exemple
ultra-simple, pensons à extraire dans un nouveau tableau tous les nombres positifs d’un
tableau d’entiers existant.
int* filtre_posi(const int[], int taille, int * nb_posi);
2. La fonction est faite pour chercher selon des critères précis à partir d’une adresse
fournie en paramètre par le client (dans l’analoque d’un tableau en fait). La fonction
retourne en cas de succès de la recherche une adresse déjà propriété du client. Ces
fonctions ne génèrent pas de nouveaux blocs et leur usage est moins dangereux que
celles de la forme précédente.
3. La fonction est faite pour bien construire grâce aux valeurs des paramètres effectifs un
nouvel objet dynamique. La fonction retourne NULL si la mémoire nécessaire n’a pu
être obtenue ou une adresse différente de NULL en cas de succès. Cette adresse devra
obligatoirement être récupérée et précieusement conservée par le client de la fonction
devenu propriétaire du nouvel objet (peu importe sa nature) jusqu’à sa libération explicite
dont il sera seul responsable.
On donne le nom de « handle » à ces adresses et de constructeur à ces fonctions. La
réalisation complète de leur existence de constructeurs viendra dans toutes les langues
de la programmation Objet C++, Java, C# ….
Historiquement ces fonctions d‘initiation d’objets sont importantes, ce sont elles qui ont
initié la modularité de construction et de maintien des représentations complexes de
l’informatique moderne. Toute idée d’encapsulation du data et de son traitement indirect
par des fonctions d’information ou de mutation vient d’abord des constructeurs en C.
1 / 7 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !