Cours 3 : Structures de données en C, Types VS Classes de stockage, Arithmétique des pointeurs
Structures de données
SdD qu’on peut fabriquer dans un langage (exs) :
- tableaux / records
- en C : un zone où ont stockés des données d’un même type.
- pointeurs
- très élémentaire
- envoie le contenu d’une adresse $A$ vers l’adresse que constitue ce contenu
- en C :
char * s;
{.C} : pointeur vers une adresse contenant un “caractère” (interprété comme tel) - NB1:
char * s;
{.C} ⟶ pointe vers une zone où sont stockés des caractères
struct
{.C}
Pq
movl $1, %eax
movl %eax, autre_dest
pcque de toute façon, on doit lire la valeur $1
depuis le bus.
Dans la mémoire :
i, c , ebp sauvegardé, ret, 3 = argc, argv
Syntaxe : char * argv[]
$\sim$ char * * argv
Pq ?
En C, les déclarations de type sont plus difficiles qu’en Pascal :
char * s;
char c;
se lit s
est une variable tq si j’écris *s
, ça donne un char
Ex:
char *s[3];
tableau de taille 3 tq qui on suit son adresse, on arrive dans une zone de caractères.
NB :
- le type de
s
est :char *[3];
{.C} &toto
: retrouve l’adresse o est stockétoto
f= (int(*)(char *, int))&toto
{.C} : type des fonctionschar *, int ⟶ int
Pourquoi “à peu près pareil” et pas “exactement pareil” ?
Quand on fait une déclaration de variables en C (ex: int i
) : fait en même temps :
- type
- classe de stockage : compilateur : taille de
i
= celle d’unint
+ comment on accède à l’objet
char * argv[]
et char **argv
: mm type, mais pas mm classe de stockage
1)
argv = malloc(3*sizeof(char*))
: alloue 3 entrées, chacune de la taille d’un pointeur de char
PUIS : char **argv
⟶ argv
: est stocké à l’adresse &argv
VS
2) char * argv[5]
: argv
: c’est l’adresse du tableau, directement.
⟹ normalement : *x = x[0]
:
- dans le premier cas, aucun problème : récupère
&argv
, y va et récupèreargv
, puis va dans*argv
- dans le deuxième cas :
*argv=argv[0]
: aucun sens, carargv
EST l’endroit où est le tableau (pas stocké dans une case, comme dans le cas précédent).
Petite astuce : quand on compile
while (i <= argc)
A
⟶ naïf :
compl i, argc
jle #vers A
jmp #tout à la fin
A
jmp #vers compl i, argc
mais MIEUX :
jmp #vers cmpl i, argc
A
cmpl i, argc
jle #vers jmp
$.main_3
: adresse où est main_3
NB : &
: pas de perte de performance
=x
⟶ movl _, _
=&x
⟶ leal _, _
Arithmétique des pointeurs en C
Si x
est un pointeur, i
un entier :
Valeur de
x+i
= Valeur dex
+ Valeur dei
$\times$ Sizeof(type des élémentsx[i]
)
Pour accéder à la i
-ème case du tableau pointé par x
.
Valeur de
x-i
= Valeur dex
- Valeur dei
$\times$ Sizeof(type des élémentsx[i]
)
⟹ x[i] = *(x+i)
Les Struct
struct point {
double x;
double y;
} ;
struct point p;
p.x;
p.y;
(double
: flottants en 64bits)
⟶ produit cartésien en maths
NB:
struct point {} ;
{.C} est de type struct point
{.C}, pas point
- Si on veut que ce soit le cas :
typedef struct point point;
{.C}
type 'a list = nil
| cons 'a * 'a list
nil
= un pointeur (NULL)
[1;2;3] = cons(1, cons(2, cons(3, nil)))
pointeur vers 1 ⟶ pointeur vers 2, …
En C :
struct list {
int val;
struct list * next;
};
MAIS : en C, on a le droit d’utiliser un type que s’il a été défini avant.
MAIS : marche quand même : car en C : struct toto
est accepté, pour tout toto
(met un pointeur).
Type enum
Si on a un interrupteur :
type interrupteur = arriere | avant | arret
en C :
enum interrupteur {
// ...
};
MAIS : pas une bonne idée, car en fait, C les implémente comme des entiers (embêtant pour caster dessus (ex: dans les switch
), après)
(*q).x
= q -> x
Type union
union {int n; double x;} z;
z.n, z.x
⟶ sont stockés au même endroit (/!\ : pas terrible, on peut écrire dans z.n
depuis z.x
)
Pointeurs dans les autres langages de programmation
Pour certains langages :
- pointeurs ∈ syntaxe ⟹ dans la sémantique (ex: C)
- pointeurs ∈ sémantique, mais pas syntaxe (ex: Java, Python)
NB :
a)
En Pascal : ressemble un peu à C, mais différent : opérations pas les mêmes :
- pas d’arithmétique des pointeurs
MAIS : en Pascal : pointeurs ≠ pointeurs
b)
CamL : ref
{.ocaml} ⟶ style impératif
En plus : récursivité terminale optimisée (sans pile : comme une boucle while
)
ref let p = ref 25 in
...
p : int ref
⟶ alloue une case
%% Example code
graph TB
p-->25;
CamL : alloue le pointeur à sa création, et nous force à spécifier la valeur à laquelle il est associé.
VS
C : on peut ne pas allouer le pointeur, et ne pas spécifier la valeur de la case sur laquelle il pointe.
!p
: lit la valeur le la case à laquelle est alloué p
p:=14
(expression de type unit
) on n’écrit pas dans la variable p
, on écrit dans la case sur laquelle p
pointe.
%% Example code
graph LR
p-->14;
NB : officiellement, on n’est pas censé pouvoir obtenir
&p
- MAIS :
- module
obj
: méthodemagic
⟹obj.magic : 'a -> 'b
si
a
est uneref
, retourne l’adresse où est le pointeur.
MAIS : Dangeureux, à cause du
Garbage Collector : Algorithme : Gère les adresses en libérant celles des variables plus utilisées. Libère la mémoire pour nous. (Garbage Collector : en canadien : Glaneur de Cellules)
Version simple : Mark and Sweep : Quand le programme n’a plus de mémoire : libère les variables plus utilisées.
Algo M&S :
Dans le graphe des adresses utilisées par le programme : il existe des racines, depuis lesquels on peut accéder à tous les sommets accessibles (algo linéaire (en nb de sommets) par marquage)
Puis : parcours de toutes les cases mémoires :
Si non allouées :
on les libère
Sinon :
on remet le marquage à 0
VS en CamL : Stop and Copy :
Algo S&C :
On divise la mémoire en deux demi-espaces : seul un sera utilisé pour allouer
Dans premier demi-espace :
étape de "copy" : on copie les adresses normalement marquées dans le deuxième demi-espace
Puis : on change de demi-espace de travail ⟶ les données non accessibles n'y sont pas (car pas recopiées)
($N$ objets, $n$ accessibles)
M&S :
allocation/désallocation en $O(n+N)$
S&C
- désallocation en temps constant.
- MAIS : recopie en $O(n×\text{taille moyenne des objets})$
- allocation très simple : l’indice du top de la pile n’est pas trop modifié
PUIS : analyse amortie pour le S&C ⟶ meilleur, si les données stockées n’ont pas une taille d’objet trop grande.
SUR le papier : M&S << S&C
, mais en pratique :
- accès à une case mémoire ⟶ pas en temps constant, à cause des caches ⟹ en pratique :
M&S
quasiment aussi bon.
CamL : langage bootstrapé (⟵ “to pull oneself from one’s bootstrap” ≃ auto-référence) : le compilateur CamL est en CamL.
CamL : écrit en CamL depuis el début.
CompCert : compilateur optimisant qui prend du code C ayant une sémantique en entrée et renvoie de l’assembleur ⟶ entièrement prouvé, et formellement : renvoie un code en assembleur ayant la même sémantique que le code C.
type 'a ref =
{ mutable contents : 'a};
type 'a list = nil (*[]*)
| cons of 'a * 'a list
cons(1, cons(2,nil))
%% Subgraph example
graph TB
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
c1-->a2
Th en CamL : Well Typed Programs Do Not Go Wrong
hypothèses : primitives du langage ont les types qui correspondent à leur spécification.
-
Pointeurs $\sim$ tableaux ↩
Leave a comment