Introduction aux opérateurs de bits
Last updated on Wednesday, October 30, 2013

Free online content available in this course.

Got it!

Introduction du cours

Vous souhaitez créer un système de droits d'accès pour votre site, vous utilisez des objets aux multiples propriétés, vous voulez passer de nombreux paramètres de type oui / non à une seule fonction ?
Les opérateurs de bits peuvent vous aider !

Vous avez dit opérateur ?

Quel que soit le programme que vous créerez, vous allez utiliser des opérateurs. Un opérateur, c'est ce qui vous permet d'obtenir une valeur à partir d'une ou de plusieurs autres valeurs. Il en existe différentes catégories :

- certaines que vous connaissez probablement :

  • opérateurs arithmétiques (+ , - , / , * ,...),

  • opérateurs de comparaison (== , > , < ,...),

  • opérateurs logiques (&& , || , ! ,...),

  • opérateurs d'assignation (= , .= , += ,...).

- d'autres semblent plus obscurs, dont les opérateurs de bits. Nous allons utiliser une partie de ces derniers.

Gérer les droits des utilisateurs

Supposons que vous vouliez instaurer un système de droits pour vos pages. Certains des membres auront le droit de publier un article, d'autres seulement de le proposer, d'autres seront restreints à la lecture. D'autres pages du site seront réservées aux administrateurs ou au webmaster... Supposons un site complexe dans lequel une dizaine de droits puissent être attribués (ou non) aux membres...

Une première possibilité serait de créer, dans la table membres, un champ par droit attribuable. Lors de la connexion du membre, les droits sont placés dans des variables de session.

<?php
        // Connexion à la base de données [...]
        $Resultat = mysql_query("SELECT * FROM membres WHERE Pseudo='$Pseudo'");
        if ($Donnee = mysql_fetch_array($Resultat)) {
                //... Vérifier le mot de passe,  etc. ...
                if ($Donnee['lire_article'])   $_SESSION['droits']['lire_article'] = TRUE;
                if ($Donnee['ecrire_article']) $_SESSION['droits']['ecrire_article'] = TRUE;
                if ($Donnee['publier_article']) $_SESSION['droits']['publier_article'] = TRUE;
                if ($Donnee['proposer_new'])   $_SESSION['droits']['proposer_new'] = TRUE;
                if ($Donnee['valider_new'])    $_SESSION['droits']['valider_new'] = TRUE;
                if ($Donnee['gerer_membres'])  $_SESSION['droits']['gerer_membres'] = TRUE;
                if ($Donnee['gerer_base'])     $_SESSION['droits']['gerer_base'] = TRUE;
                // etc.
        }
?>

Lorsqu'une page doit être restreinte, on teste :

<?php
        /* Le membre a-t-il droit à une page donnée ? */
        if (empty ($_SESSION['droits']['proposer_new'])) {
                echo 'Tu n\'as rien à faire ici !!';
                exit;
        }
?>

Cette première solution, que je vous déconseille vivement, fonctionnerait avec un petit nombre de droits. Avec dix, c'est vraiment lourd — aussi bien pour le programmeur que pour le serveur, qui devra garder des tas de variables en mémoire, et faire de nombreuses opérations... :'(

Nous allons donc placer l'ensemble des droits du membre dans une seule variable. Là encore, plusieurs possibilités s'offrent à nous.

Une première idée pourrait être de symboliser chaque droit par une lettre et de tester si la lettre requise pour autoriser la page se trouve bien dans la variable Droits du membre. Ce qui donnerait :

<?php
        /* les droits (la correspondance sera conservée dans une table par exemple)
                A : lire un article
                B : écrire un article
                C : publier un article
                etc.          */
 
        /* Pour attribuer les droits, on utilisera l'opérateur de concaténation */
        $DroitsDuMembre = '';
        if ($Droit_lecture)     $DroitsDuMembre .= 'A';
        if ($Droit_Publication) $DroitsDuMembre .= 'C';  
        // etc.
 
        /* Lors du login de l'utilisateur */
        // Connexion à la base de données [...]
 
        $Resultat = mysql_query("SELECT Nom, Pass, Droits FROM membres WHERE Pseudo='$Pseudo'");
        if ($Donnee = mysql_fetch_array($Resultat)) {
                //... Vérifier le mot de passe,  etc ...
                $_SESSION['droits'] = $Donnee['Droits'];
        }
 
 
        /* Le membre a-t-il droit à une page donnée ? */
        if (strpos($_SESSION['droits'], 'C') === FALSE) {
                echo 'Désolé, mais cette page t\'est interdite...';
                exit;
        }
?>

C'est déjà nettement mieux ! o_O

Penser binaire

Pour l'ordinateur, un nombre est une suite de 0 et de 1, chacun correspondant à un bit. La plupart de nos ordinateurs travaillent par séries de 32 bits.

Sur un nombre, nous pouvons donc avoir 32 bits pouvant prendre chacun la valeur 1 (qui signifiera pour nous : « vrai » ou « oui ») ou 0 (qui signifiera bien sûr « faux » ou « non »). C'est donc largement suffisant pour stocker notre table de droits !

Nous pouvons décider que (les nombres sont ici sur 16 bits) :
0000000000000001 signifie "lire un article"
0000000000000010 signifie "écrire un article"
0000000000000100 signifie "publier un article"
0000000000001000 signifie "proposer une new"
0000000000010000 signifie "valider une new"
0000000000100000 signifie "gérer les membres"
0000000001000000 signifie "gérer la base de données"

Pour dire que Untel a le droit de lire, écrire, publier des articles, et gérer les membres, il suffira d'attribuer la valeur suivante à sa variable $Droit :
0000000000100111

Pour savoir si Untel a le droit d'écrire un article, il suffit de regarder si le deuxième bit à partir de la droite est bien un 1.

Pour lui ajouter le droit de sauvegarder la base de données du site et de télécharger la sauvegarde, nous modifierons simplement le septième bit à partir de la droite en 1.

Pour lui retirer ce droit (il vaut mieux ne pas laisser la base de données entre les mains de n'importe qui...), nous donnerons à nouveau à ce même septième bit la valeur 0.

Mais comment effectuer ces opérations ? Nous n'allons quand même pas avoir à écrire en base 2 (0000000000100111) dans le programme ?!

Les opérateurs de bits sont là pour nous faciliter la tâche. Il est donc temps de les découvrir. Nous en verrons quatre dans ce tuto :
& : ET binaire
| : OU binaire (au sens de ET / OU)
^ : XOR binaire (OU exclusif)
~ : NON binaire

Puis nous verrons comment faire en pratique pour ne pas avoir à écrire en binaire.

[Edit] Finalement, nous introduirons aussi les opérateurs de décalage << et >> .

Les opérateurs de bits & et |

Untel a-t-il le droit d'écrire un article ? Opérateur &

Le ET binaire compare deux nombres binaires et donne un résultat dans lequel seuls les bits 1 en même position dans les deux nombres gardent une valeur 1. Tous les autres sont mis à zéro.

0000000000100111 Droits de Untel
& opérateur ET
0000000000000010 écrire un article
=
0000000000000010 Résultat qui est différent de 0000000000000000 donc VRAI

Ajouter le droit de sauvegarder la base de données : Opérateur |

Le OU binaire compare deux nombres binaires et donne un résultat dans lequel les bits 1 de chacun des deux nombres gardent une valeur 1.

0000000000100111 Droits de Untel
| opérateur OU
0000000001000000 gérer la base
=
0000000001100111 qui est le nouveau droit de Untel

Observation : le résultat aurait été le même si Untel disposait déjà de ce droit.
0000000001100111
|
0000000001000000
=
0000000001100111

Les opérateurs de bits ^ et ~

Retirer le droit de sauvegarder la base de données : ~ et ^

Selon que nous connaissons ou non les droits actuels de Untel, nous pourrons utiliser un opérateur (^) ou une combinaison de deux opérateurs (& et ~).

Une méthode risquée pour retirer un droit : le OU Exclusif

Le OU Exclusif binaire ne retient que les 1 présents dans UN SEUL des deux nombres examinés.

0000000001100111 Droits de Untel
^ opérateur OU Exclusif (XOR)
0000000001000000 gérer la base
=
0000000000100111 qui est le nouveau droit de Untel

Meilleure méthode : utiliser un masque NON ~

L'opérateur NON binaire inverse tous les bits du nombre auquel il est appliqué.

Cela nous permet de créer un masque que nous appliquerons aux droits de Untel pour supprimer uniquement le droit de gestion de la base.
~0000000001000000 gérer la base
=1111111110111111 masque du droit de gestion de la base
&0000000001100111 droits de Untel
=0000000000100111 nouveaux droits de Untel

On peut observer qu'appliquer le même masque si Untel ne dispose pas du droit à supprimer ne l'ajoutera pas :
=1111111110111111 masque du droit de gestion de la base
&0000000000100111 droits de Untel
=0000000000100111 nouveaux droits de Untel

Comparer les droits de deux utilisateurs : encore l'opérateur ^

On peut utiliser l'opérateur logique ==, mais l'opérateur de bits ^ est un autre moyen. En fait, le résultat peut être interprété comme « est différent de », ce qui permet de déduire l'égalité bit à bit.
0000000000100111 Droits de Untel
^ opérateur XOR (OU exclusif)
0000000000100111 Droits de User2
=
0000000000000000 Résultat qui est = à 0 donc FAUX : il n'y a pas de différence entre les deux utilisateurs.

Au contraire :
0000000000100111 Droits de Untel
^ opérateur XOR (OU exclusif)
0000000000000011 Droits de User2
=
0000000000100100Résultat qui est différent de 0 donc VRAI : les droits des deux utilisateurs sont différents !

Les droits avec les opérateurs de bits

L'hexadécimal à notre rescousse

Nous ne pouvons pas vraiment écrire directement en base 2 en PHP. Ce serait d'ailleurs trop fatigant. ;) Nous pouvons bien sûr écrire en base 10, comme d'habitude. Mais observons le tableau suivant :

base 2

base 10 (décimal)

base 16 (hexadécimal)

0000000000000001

1

0x01

0000000000000010

2

0x02

0000000000000100

4

0x04

0000000000001000

8

0x08

0000000000010000

16

0x10

0000000000100000

32

0x20

0000000001000000

64

0x40

0000000010000000

128

0x80

0000000100000000

256

0x100

0000001000000000

512

0x200

0000010000000000

1024

0x400

0000100000000000

2048

0x800

0001000000000000

4096

0x1000

0010000000000000

8192

0x2000

0100000000000000

16384

0x4000

1000000000000000

32768

0x8000

En base 2, les puissances de 2 s'écrivent avec un seul 1, tout le reste étant des 0. Ce sont donc les puissances de 2 qui nous intéressent.

En pratique... et en PHP

Voici un petit exemple de ce que pourrait être la gestion des droits avec les opérateurs de bits.

<?php
    /* Définition des droits dans un fichier PHP * 
    *********************************************
    Les droits pourraient aussi être définis dans une table, ce qui permettrait facilement d'accroître la liste et de créer des checkbox dans les formulaires d'inscription. */
 
    define ('LIRE_ARTICLE',    0x01);
    define ('ECRIRE_ARTICLE',  0x02);
    define ('PUBLIER_ARTICLE', 0x04);
    define ('PROPOSER_NEW',    0x08);
    define ('VALIDER_NEW',     0x10);
    define ('GERER_MEMBRES',   0x20);
 
 
    /* Attribution des droits *
    ************************** 
    On suppose que les variables $Droit_lecture, $Droit_Publication... sont récupérées d'un formulaire ; on utilise donc l'opérateur OU Binaire combiné à l'opérateur d'assignation = . */
 
    $DroitsDuMembre = 0;
    if ($Droit_lecture)     $DroitsDuMembre |= LIRE_ARTICLE;
    if ($Droit_Publication) $DroitsDuMembre |= ECRIRE_ARTICLE;  
    // etc ...
 
    // Le membre devient newser... On lui donne plusieurs droits d'un coup.
    $DroitsDuMembre |= PROPOSER_NEW | VALIDER_NEW;
 
 
    /* Suppression de droits *
    ***********************/
    // Les news n'étaient pas bonnes ; retrait des droits correspondants.
    $DroitsDuMembre &= ~(PROPOSER_NEW | VALIDER_NEW);
    // Interprétation de droite à gauche :
    // On a commencé par créer le droit réunissant les deux à supprimer par | .
    // Ensuite, on a créé le masque de cet ensemble de droits par ~ .
    // Enfin, on utilise & pour ne garder que les autres droits du membre.
 
 
    /* Login de l'utilisateur *
    *************************/
    // Connexion à la base de données [...]
 
    $Resultat = mysql_query("SELECT Nom, Pass, Droits FROM membres WHERE Pseudo='$Pseudo' ");
    if ($Donnee = mysql_fetch_array($Resultat)) {
        //... Vérifier le mot de passe,  etc. ...
        $_SESSION['droits'] = (int)$Donnee['Droits'];
    }
 
 
    /* Examen des droits avant d'autoriser l'accès à une page *
    *********************************************************/
 
    /* ATTENTION, les variables de PHP n'ont pas vraiment un type défini, il est donc utile de signaler au programme que l'on veut comparer deux nombres et non deux chaînes de caractères. */
    // Le membre a-t-il le droit de publier les articles ?
 
    // On utilise l'opérateur & pour tester le droit.
    if (!((int)$_SESSION['droits'] & PUBLIER_ARTICLE)) {
        echo 'Désolé, mais cette page t\'est interdite...';
        exit;
    }
?>

Appendice : les opérateurs de décalage

Initialement, je ne pensais pas vous en parler, car je ne les avais pas utilisés en PHP. Mais DHKold a suggéré de leur consacrer un petit paragraphe, ce qui me convient tout à fait puisqu'en avançant dans mon projet, j'ai découvert qu'ils étaient très utiles ! :p

Votre site s'est bien développé, et vous proposez à vos membres toute une série d'options (recevoir un mail lorsque l'on a un message personnel, afficher ou masquer tel ou tel menu...). Pour ne pas encombrer votre table « membres », vous décidez d'utiliser un champ (entier non signé) pour stocker l'ensemble des options. Toutes les options sont rassemblées dans une table de votre base de données, et vous voulez créer un script pour gérer cette table directement en ligne.

Lister les puissances de 2 : opérateur << (décalage à gauche)

À chaque option doit correspondre un ID qui soit une puissance de 2 (un nombre avec un seul bit à 1). Mais comment vérifier que l'entrée saisie dans le champ ID en soit une ?

Puisqu'il n'y a que trente-et-une puissances de 2 utilisables, nous allons les tester toutes ! :o

L'algorithme pourrait s'écrire :

Pour la suite des puissances de 2 $p2 à partir de 1 (= 2 puissance 0), et inférieur à 2 puissance 31 :
 
         si $p2 est égal au nombre testé : le nombre testé est une puissance de 2;
 
         sinon je multiplie $p2 par 2 et je recommence le test....

Ce sera peut-être plus clair en PHP :

function EstPuissanceDe2 ($Nombre_a_tester) {
        for ($i = 1 ; $i < 0x80000000 ; $i *= 2) {
                if ($i == $Nombre_a_tester) {
                        // Le nombre est une puissance de 2
                        return TRUE;
                }
        }
        // Le nombre n'est pas une puissance de 2 < 2 puissance 31
        return FALSE;
}

Nous avons vu que pour multiplier un nombre écrit en binaire par 2, il suffisait de rajouter un 0 à droite. C'est exactement ce que fait l'opérateur de décalage vers la gauche, que l'on note <<. Il rajoute de un à trente-deux zéro(s) à droite (et supprime les un à trente-deux bits les plus à gauches).

0000000000001010<< 1 est égal à 0000000000010100 (multiplication par 2)
0000000000001010<< 4 est égal à 0000000010100000 (multiplication par 2 puissance 4 = 16)

Nous pouvons donc remplacer l'instruction $i * 2 par $i << 1

Reste un petit problème :
2 puissance 31 s'écrit en hexadécimal 0x80000000, ce qui est un nombre positif.
En binaire, ce nombre donne : 10000000 00000000 00000000 00000000).
Pour les systèmes 32 bits (la majorité des ordinateurs personnels actuels), ceci peut désigner aussi bien un entier non signé positif, qu'un entier signé négatif.
Or, comme nous n'avons rien spécifié de particulier, la variable $i sera supposée être un entier signé... Lorsqu'elle prendra la valeur 2 puissance 31, elle sera en fait un nombre négatif, donc inférieure à 0x80000000 => La boucle ne s'interrompt pas !
La prochaine itération donnera donc :
0x80000000 << 1 = 0x00000000 = 0; (Le bit le plus à gauche qui était à 1 est supprimé et on ajoute un 0 à droite : il ne reste que 32 0 !).
Les itérations suivantes donneront toutes le même 0 (appliquer un décalage sur 0 donne toujours 0 par définition). Nous entrerons donc dans une boucle infinie !

La solution sera d'utiliser un des opérateurs de bits que nous avons déjà vu, et de remplacer l'instruction "$i < 0x80000000"; par "$i ^ 0x80000000"; que l'on pourra traduire « tant que $iest différent de 0x80000000 ».

Nous obtenons donc le code suivant :

function EstPuissanceDe2 ($Nombre_a_tester) {
        for ($i = 1 ; $i ^ 0x80000000 ; $i <<= 1) {
                if (! ($i ^ (int) $Nombre_a_tester)) {
                        // s'ils ne sont pas différents, c'est qu'ils sont égaux !
                        return TRUE;
                }
        }
        // Le nombre n'est pas une puissance de 2 inférieure à 2 puissance 31
        return FALSE;
}

Opérateur de décalage vers la droite : >>

C'est, bien sûr, exactement le contraire du décalage vers la gauche : on efface n bit(s) le(s) plus à droite et on ajoute n 0 à gauche, n pouvant aller de 1 à 31 (un décalage à droite ou à gauche de 32 bits ne servirait à rien : le résultat serait toujours égal à 0 sur un système 32 bits !).

Vous voulez afficher un tableau comportant pour chaque membre sa situation par rapport à toutes les options possibles.

Membre

Option 1

Option 2

Option 3

Option 4

Option 5

Titi

oui

non

oui

oui

non

GrosMinet

oui

oui

oui

non

non

Pour remplir les titres du tableau, il suffit de faire une requête SELECT à partir de la table des options. Mais comment afficher les options de chaque membre, puisque ces options sont stockées dans un entier ?
Une façon simple est d'examiner un par un les bits du champ Options des membres, et une petite boucle nous permettra de le faire sans difficulté :

<?php
 
        // Connexion à la base de données [etc ...]
 
        // On compte les options.
        $Resultat = mysql_query("SELECT COUNT(*) FROM Options");
        if ($Donnee = mysql_fetch_row($Resultat)) {
                $Nbre_Options = $Donnee[0];
        }
 
        // On récupère le nom des options et on les liste dans l'en-tête du tableau. 
        // [...] vous savez très bien faire ce code...
 
        // Afficher les options pour chaque membre :
        $Resultat = mysql_query("SELECT Nom, Options FROM membres");
        while ($Donnee = mysql_fetch_array($Resultat)) {
                // Pour chaque membre, on affiche le nom…
                echo '<tr><td>', $Donnee['Nom'], '</td>';
                // … et on récupère les options.
                $Options = (int) $Donnee['Options'];
/*******************************
 * Partie intéressante du code *
 *******************************/
                for ($i = 0 ; $i < $Nbre_Options ; $i ++) {
                        // On examine le bit le plus à droite (1 est le masque correspondant au bit le plus à droite).
                        if ($Options & 1) {
                                echo '<td>OUI</td>';
                        }
                        else {
                                echo '<td>non</td>';
                        }
                        // On passe au bit suivant des Options par un décalage à droite d'un bit.
                        $Options >>= 1;
                }
/*********************************
 * Fin de la partie intéressante *
 *********************************/
                // On ferme la ligne.
                echo '</tr>';
        }
        // On ferme la table.
        echo '</table>';
?>

J'espère vous avoir donné envie d'explorer le monde des opérateurs de bits.

Nous ne sommes pas obligés de ne stocker que des options de type oui / non. Nous pouvons aussi utiliser des valeurs sur plusieurs bits (4 choix utilisent 2 bits, 8 choix utilisent 3 bits, etc.). Le tout est d'utiliser les masques correspondants lors des opérations.

Vous trouverez d'autres exemples d'applications des opérateurs de bits dans ce cours de javascript, au paragraphe sur les opérateurs binaires.

Personnellement, j'ai mis assez longtemps avant de comprendre comment cela fonctionnait, mais je n'avais que des explications succinctes en anglais (ce n'était pas pour du PHP).

J'ai eu l'idée de ce « petit » tuto lorsque je me suis aperçu, en travaillant sur un intranet, que j'avais fait des erreurs dans ma gestion des droits...

How courses work

  • 1

    You have now access to the course contents and exercises.

  • 2

    You will advance in the course week by week. Each week, you will work on one part of the course.

  • !

    Exercises must be completed within one week. The completion deadline will be announced at the start of each new part in the course. You must complete the exercises to get your certificate of achievement.

  • 3

    At the end of the course, you will get an email with your results. You will also get a certificate of achievement if you are a <a href="/premium>Premium</a> Member and if you got at least 70% of your answers right.

Example of certificate of achievement
Example of certificate of achievement