👉 Pointeurs et références 🌙

I/ Exemple

1) Machine de Post

Dans un article de 1936, Emil Post décrivait l'ancêtre des machines de Turing, sous la forme suivante :

☝️

Les instructions possibles sont

2) Étymologie

index est un mot latin, à l'origine des mots français

3) Tableaux

On peut représenter l'état initial de la machine de Post ci-dessus par un tableau :

0001 0110 0100

En effaçant l'essentiel des bords haut et bas de ce tableau, il ressemble à [0|0|0|1|0|1|1|0|0|1|0|0]. En Python ce sera [0,0,0,1,0,1,1,0,0,1,0,0]. En C on écrit {0,0,0,1,0,1,1,0,0,1,0,0} et en Ocaml, [|0;0;0;1;0;1;1;0;0;1;0;0|].

Dans tous ces langages, la position initiale de l'index est 0 et si l'index i pointe sur le troisième élément (i = 2) on accède à cet élément avec la notation t[2] en Python et en C, avec la notation t.(2) en Ocaml. Par écrit ce sera plutôt t2. De manière générale, on gagne à considérer que tout ce qui est entre crochets est indice. La première lettre du mot index étant un i, les variables de boucles sont souvent notées i.

II/ Tableaux

1) En Ocaml

Le programme

let tab = Array.make 11 0;;
for i = 0 to Array.length tab - 1 do
    tab.(i) <- i*i
done;;
donne le tableau
0149162536496481100

2) En C

Le programme C

    int tab[10];
    int i;
    for (i=0; i<10; tab[i++]=i*i);

donne le tableau

149162536496481100

3) Variable de boucle

Que ce soit en C ou en Ocaml, i est une variable entière. On peut s'en apercevoir avec ces modifications des programmes de création de tableaux :

a) Ocaml

let tab = Array.make 11 0;;
for i = 0 to Array.length tab - 1 do
    print_int i;
    print_string ","
done;;

b) C

#include <stdio.h>
int main(){
    int tab[10];
    int i;
    for (i=0; i<10; printf("%d,",i++));
    return 0;
}

III/ Pointeurs

1) Tableau en mémoire

On considère toujours le tableau tab des carrés, construit avec

    int tab[10];
    int i;
    for (i=0; i<10; tab[i++]=i*i);

Pour accéder à une variable en C, on écrit juste le nom de la variable. C remplacera automatiquement ce nom par la valeur de la variable. Mais si on précède le nom de la variable par le caractère &, C ira chercher l'adresse mémoire de la variable (un entier). Par exemple, on a les termes d'une suite arithmétique avec

    for (i=0; i<10; i++) {
        printf("%u\n", &tab[i]);
    }

2) Pointeur

En fait on a exactement la même suite arithmétique de nombres entiers avec

    int *pointeur = tab;
    for (i=0; i<10; i++) {
        printf("%u\n", pointeur);
        pointeur++;
    }

La variable pointeur est un entier spécial (un pointeur). En l'initialisant à tab, on lui donne comme valeur initiale, l'adresse &tab de tab. Et chaque fois qu'on l'« incrémente » avec pointeur++, il augmente non pas d'une unité, mais de la valeur qui convient pour qu'il aille directement à l'élément suivant du tableau. Pour ce faire, on l'a déclaré de type int * ce qui veut dire « pointeur sur entier ».

3) Vers la lune

Une variante du programme précédent affiche les carrés des entiers :

    int *pointeur = tab;
    for (i=0; i<10; i++) {
        printf("%u\n", *pointeur);
        pointeur++;
    }

L'opération unaire * veut donc dire suivez le doigt du regard : *pointeur désigne, non la valeur du pointeur, mais la valeur pointée par le pointeur.

Ainsi, au lieu d'écrire tab[2] on peut écrire *(tab+2).

4) Pointeurs et arbres

Les arbres binaires (et plus spécifiquement les listes) peuvent être implémentés par des pointeurs. Par exemple un arbre binaire peut être représenté par :

typedef struct {
    int label;
    struct binarytree *gauche;
    struct binarytree *droite;
} binarytree;

Pour implémenter les feuilles de l'arbre binaire, on utilise la valeur spéciale NULL pour les pointeurs (une feuille n'ayant ni enfant gauche, ni enfant droit). L'idée est à la base du langage de programmation Lisp (McCarthy 1958).

5) Graphes

a) Un pointeur

Un pointeur est un graphe orienté :

%3 i i 2 4 i->2 3 9 2->3 0 0 1 1 0->1 1->2 4 16 3->4 5 25 4->5 6 36 5->6 7 49 6->7 8 64 7->8 9 81 8->9 10 100 9->10

b) Pointeurs de pointeurs

Comme un pointeur peut pointer vers n'importe quel type de variable, un pointeur (comme p ci-dessous) peut même pointer vers un pointeur ( i ci-dessous) :

%3 i i 2 4 i->2 3 9 2->3 p p p->i 0 0 1 1 0->1 1->2 4 16 3->4 5 25 4->5 6 36 5->6 7 49 6->7 8 64 7->8 9 81 8->9 10 100 9->10

Un exemple classique est la liste des arguments d'une fonction, par exemple la fonction principale

int main(int argc, char **argv)

Après le doigt, voici donc la main 😂

IV/ Références

1) Déférencement en C

On a vu que si p est un pointeur, *p donne non pas le contenu de la variable p, mais celui de la variable pointée par p. L'opérateur * est appelé opérateur de déréférencement.

a) Opérateurs et fonctions

Un opérateur comme * vu ci-dessus est dit unaire. En fait il existe un opérateur binaire en C, également noté * : c'est la multiplication.

Les opérateurs unaires sont en fait des fonctions. Par exemple l'opérateur unaire * de C est la fonction qui, à tout pointeur, associe la valeur pointée par le pointeur.

Cette fonction est notée $ en bash. On verra plus bas comment elle est notée en Ocaml, mais avant cela, une remarque qui est à la base de la programmation fonctionnelle : tous les opérateurs sont en réalité des fonctions. L'idée remonte à Haskell Curry (1926).

b) Curryfication

La multiplication des entiers par exemple, est notée de façon infixe (c'est une opération binaire). Le produit de x par y est noté x*y. Mais on peut aussi considérer la multiplication notée préfixe : à deux entiers x et y, elle associe un entier qui est le produit de x par y.

La multiplication est alors une fonction qui, à un couple d'entiers, associe le produit de ces entiers :

# let mul1 (x,y) = x*y;;
val mul1 : int * int -> int = <fun>

%3 x x xy x*y x->xy y y y->xy

Le graphe ci-dessus décrit correctement la multiplication mais certaines opérations binaires ne sont pas commutatives et l'ordre dans lequel on regarde les variables a de l'importance.

%3 xy1 (x,y) xy2 x*y xy1->xy2

Un couple (comme (x,y)x et y sont des entiers) est un élément du carré cartésien de N. Ocaml lui donne un type produit :

# let couple = (8,5);;
val couple : int * int = (8, 5)

Dans un couple, on distingue le premier élément du couple, du second élément :

# fst couple;;
- : int = 8
# snd couple;;
- : int = 5

Un graphe n'est autre qu'un sous-ensemble d'un carré cartésien. Il possède donc un type produit. En fait, une fonction est aussi un sous-ensemble d'un carré (ou plus généralement, d'un produit) cartésien :

# let graphe x y = (x,y);;
val graphe : 'a -> 'b -> 'a * 'b = <fun>

a pour effet de transformer 'a -> 'b en 'a * 'b.

Un couple est lui-même un graphe (l'arc allant du premier élément au second). La signature de la multiplication des entiers est donc

%3 xy1 %3 x int y int x->y xy2 int xy1->xy2

La curryfication consiste à enlever le cercle extérieur :

%3 x int y int x->y xy int y->xy

L'approche de Curry consiste à voir la multiplication des entiers (fonction qui, à deux variables entières, associe leur produit), comme une fonctionnelle qui à un entier, associe une fonction linéaire sur les entiers :

# let mul2 x y = x*y;;
val mul2 : int -> int -> int = <fun>

On convient que ce graphe revient au même que

%3 xy1 int xy2 %3 x int y int x->y xy1->xy2

Noter que le verbe curryfier est représentable en Ocaml par une fonction :

# let curry f = fun (x,y) -> f x y;;
val curry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun>

c) Décurryfication

La curryfication permet de programmer en Ocaml des fonctions de plusieurs variables. Son inverse (appelée décurryfication, en anglais uncurry) permet de comprendre la signature d'une fonction Ocaml en la voyant comme une fonction de plusieurs variables. La version curryfiée de la multiplication des entiers est

%3 x int y int x->y xy int y->xy

donc la décurryfication fait passer du graphe précédent au graphe de départ qui est :

%3 x int xy int x->xy y int y->xy

Le verbe décurryfier peut lui aussi être traduit par une fonction Ocaml :

# let uncurry f = fun x y -> f(x,y);;
val uncurry : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun>

2) Références en Ocaml

a) Une fonction Ocaml

La fonction ref associe, à toute valeur de type 'a, une référence à cette valeur 

# ref;;
- : 'a -> 'a ref = <fun>

b) Création d'une référence

# let i = ref 0;;
val i : int ref = {contents = 0}

i est maintenant une référence vers l'entier 0 :

%3 i i 0 0 i->0

Les références modélisent la notion de variable : il est possible de modifier le contenu d'une référence mais pas la référence elle-même. Par exemple si on essaye

# let j = ref 0;;
val j : int ref = {contents = 0}

on a créé une nouvelle référence :

%3 i i 0 0 i->0 j j j->0

Si on modifie le contenu de i, cela ne modifie pas celui de j :

# i := 1;;
- : unit = ()
# i;;
- : int ref = {contents = 1}
# j;;
- : int ref = {contents = 0}
%3 i i 1 1 i->1 j j 0 0 j->0

c) Une autre fonction Ocaml

# (!);;
- : 'a ref -> 'a = <fun>

La fonction notée par un point d'exclamation en Ocaml associe, à une référence, son contenu (le point d'exclamation se lit à voix haute « le contenu de »). Il n'est pas possible d'additionner un nombre à une référence, mais si le contenu de cette référence est un entier, l'addition est possible :

# i+1;;
Error: This expression has type int ref
       but an expression was expected of type int
# !i+1;;
- : int = 2

Pour incrémenter le contenu d'une référence on fait donc

# !i;;
- : int = 1
# i := !i+1;;
- : unit = ()
# !i;;
- : int = 2

On peut bien entendu incrémenter en boucle :

# for n = 1 to 8 do i:=!i+1 done;;
- : unit = ()
# i;;
- : int ref = {contents = 10}
s

d) Incrémentations successives

On peut créer des références vers tous les types, y compris vers des références :

# let k = ref j;;
val k : int ref ref = {contents = {contents = 0}}
# !k;;
- : int ref = {contents = 0}
# ! ! k;;
- : int = 0
%3 i i 1 1 i->1 j j 0 0 j->0 k k k->j