INF145 : 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 pouvoir ainsi
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 garder une adresse.
Pour un type quelconque T, 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, 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 -- 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. À 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.
4. 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é.
5. 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.
6. 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.
7. 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.
L’indirection, comment atteindre le contenu d’une adresse
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) ou
libérer (free) de la mémoire en 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 :
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.
On peut aussi obtenir des blocs de mémoire grâce aux fonctions calloc et realloc de stdlib que je ne
décris même pas ici vu leur moindre importance.
Plusieurs exemples de la méthode pour un gros tableau
La fonction malloc s’utilise normalement sous la forme :
malloc(nombre d’éléments * sizeof(element))
La fonction free s’utilise normalement sous la forme :
free( adresse obtenue précédemment d’un malloc)
/*=====================================================*/
Imaginons maintenant que vous avez besoin d’une très grande matrice de réels,
par exemple une de (3500 X 1200).
/*=====================================================*/
D’abord la forme statique (sans allocation dynamique) que vous connaissez, le compilateur gère autant
l’apparition que la disparition du tableau, sans aucune capacité de modification d’espace sans recompiler
le projet.
/* ce sera un bloc unique, un monstre de 33 600 000 octets */
double tab [3500][1200];
/*pour atteindre un réel de cette matrice, aucune difficulté */
tab[217][38] = 34.89 ;
//sa durée de vie terminée, le compilateur se charge seul d’utiliser cette mémoire à d’autres fins.
/*=====================================================*/
Première forme d’allocation programmée, qu’on peut qualifier de semi-dynamique. Le premier tableau de
3500 adresses est statique (obtenu en compile time).
/* un tableau de double * , pour (3500 x 4) bytes */
double * tab [3500];
int i;
/* puis les blocs obtenus dynamiquement sont indépendants et contrôlés*/
for(i=0;i<3500;++i) {
/* ce typecast n’est pas obligatoire*/
tab[i] = (double *) malloc(1200* sizeof(double));
assert(tab[i]); // contrôle de base ultra-simple
}
/* mais pour atteindre un réel , rien n’est changé */
tab[2419][807] = 34.89 ;
/* usage terminé, les 3500 tableaux doivent être libérés mais tab sera géré par le compilateur */
for(i=0;i<3500;++i) { free(tab[i]);}
/*=====================================================*/
Deuxième forme, tous les tableaux sont dynamiques, le compilateur ne prend en charge que 4 octets.
int i;
double ** tab ;
tab = (double **) malloc( 3500 * sizeof(double*));
if ( tab != NULL) {
for(i=0;i<3500;++i) {
assert(tab[i] = malloc(1200* sizeof(double)) );
}
}
/*pour atteindre un réel de la matrice, aucune différence */
tab[2215][378] = 34.89;
/*après usage, les 3500 tableaux de réels doivent être libérés*/
for(i=0;i<3500;++i) { free(tab[i]);}
/* et aussi le tableau de pointeurs, ici l’ordre est important */
free(tab);
/*=====================================================*/
En conclusion, 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.
1 / 8 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 !