Détails supplémentaires sur les rapports traditionnels
entre octets et valeurs du type char

  1. Interprétation des octets non-ASCII.
  2. Décodage des valeurs négatives (exercice)
    1. De manière méthodique,
    2. D'une manière plus mystérieuse,
    3. Enfin, d'une manière encore plus mystérieuse,
  1. Interprétation des octets non-ASCII.

    Par définition, les valeurs du type char sont dans l'intervalle [-128, +127].
    Ce qui signifie que
    Les 128 octets "positifs ou nuls" sont interprétés automatiquement comme les caractères du code ASCII 7 bits,
    ce qui se voit par exemple en exécutant l'instruction suivante, où la mention "%c" dans la chaîne de format
    dit bien imprimez comme caractère :
    printf("Valeur de ch : %c \n ", ch);

    L'interprétation des octets "négatifs", en revanche, pose problème !
    En pratique, elle dépend du jeu de caractères employé par le système d'exploitation lors de l'exécution du programme.

    Application : voyez la troisième question de l'examen 2007, ainsi que le programme de visualisation des octets.

  2. Décodage des valeurs négatives (exercice)

    Si on a fait l'erreur d'employer une variable de type char, pour loger la valeur lue par fgetc,
    et qu'on veuille (c'est notre cas) interpréter les octets "négatifs"
    pour désigner des caractères non-ASCII, soit dans une table à 8bits, soit en composant un code utf-8,
    la première chose à faire est de "décoder" la valeur négative que l'interprétation du type char nous impose,
    pour retrouver la valeur numérique normale de l'octet.

    Il faut alors confier cette tâche à une fonction nommée valoct, déclarée comme
    int valoct(char c).
    dont voici une réalisation :
    int valoct(char c) {
        if( c<0 ){
            return 128 + (127 & c);
        }else{
            return c;
        }
    }

    Il s'agit ni plus ni moins que de défaire ce qu'a fait la conversion implicite de int en char
    opérée par l'affectation ch=fgetc(fp);
    La formule employée demande une justification !
    On trouvera ci-après une discussion approfondie du pourquoi de la chose et de ses modalités.
    Le lecteur intéressé en déduira d'autres réalisations de cette fonction.

    On peut procéder de plusieurs façons :
    1. De manière méthodique,

      en considérant que la cause du problème se trouve dans le bit initial,
      et que par conséquent on obtiendra la valeur numérique correcte

      1. en supprimant ce bit et en ne gardant que les 7 bits de poids faible,
        dont la valeur numérique sera obtenue sans ambiguïté, puisqu'elle est positive

      2. en transférant  cette valeur dans une variable de type entier (int ou short) et non plus char,

      3. et en lui ajoutant la valeur 128 qui correspond au bit de poids fort.
        Soulignons l'importance du changement de type pour la variable en question :
        de lui dépend l'interprétation qui sera faite de l'octet obtenu en ajoutant 128,
        • si ce type est char, cette interprétation donne un entier négatif, obtenu par complémentà 2,
        • si ce type est short ou int, l'interprétation donne l'entier positif qui nous intéresse.

      La suppression du premier bit peut se faire d'au moins deux manières :

      • En filtrant les 7 derniers bits par l'opération bit à bit "AND" avec l'octet 0111 111 = 127 ;
        le calcul précédent est alors réalisé par :
        short val;
        val = 127 & ch;
        val = 128 + val;

        abrégé en val = 128 + (127 & ch);

      • En effaçant le premier bit par l'opération "XOR" (ou exclusif) avec l'octet 1000 0000 = 128 ;
        les deux opérations (127 & ch) et (128 ^ ch) donnent le même résultat.
        L'expression précédente devient  val = 128 + (128 ^ ch);
    2. D'une manière plus mystérieuse,

      en ajoutant 256 à la valeur de ch.
      L'instruction se réduit alors à
      val = 256 + ch; (*)

      Sa validité repose sur une propriété remarquable du complément à 2 :

      Étant donné un octet dont le bit de poids fort est à 1, appelons v7 la valeur des 7 bits de poids faible,
      et vc la valeur (négative) de l'octet tout entier en tant que char.
      Alors, en donnant au signe "=" son sens mathématique de "égale"
      (et non plus celui de l'affectation en C),
      on a l'égalité
      v7 = 128 + vc (**).

      Comme la valeur de l'octet tout entier est 128 + v7,  l'égalité (**) implique le résultat souhaité (*).
      Voyons ce que ça donne sur notre exemple de l'octet  1100 0010 :
      • les 7 bits de poids faible sont 100 0010, donc v7 = 64+2 = 66.
      • vc = -62 (nous allons voir pourquoi)
      • 66 = 128 - 62, conformément à (*)
      • val = 128 + 66 = 194 comme annoncé.

      Pour comprendre l'égalité (**), il nous faut retracer les étapes du calcul de vc par complément à 2.
      Appelons oct0 l'octet de départ, dont le bit de poids fort est à 1, dans notre exemple c'est 1100 0010.
      1. On inverse tous les bits de l'octet oct0 (opération dite complément à 1) ;
        appelons oct1 l'octet résultat, pour notre exemple oct1 est 0011 1101.
      2. On ajoute 1, ce qui dans notre exemple fait  0011 1110 = 2+4+8+16+32 = 62 ;
        appelons oct2 cet octet.
      3. La valeur de oct2 affectée du signe moins est renvoyée comme résultat,
        d'où vc = -62 comme prédit.

      Venons-en à (**), que nous pouvons réécrire (puisque nous faisons des mathématiques, pas de la programmation)
      v7 - vc = 128 (***).
      En supprimant le signe "-" ajouté à l'étape 3, c'est à dire en considérant la valeur vc' = -vc, elle se réécrit
      v7 + vc' = 128 (****).
      vc' est la valeur de l'octet oct2 obtenu à l'étape 2, dans notre exemple 0011 1110,
      c'est à dire 1 + la valeur de l'octet oct1 obtenu à l'étape 1 (par complément à 1), dans notre exemple 0011 1101.
      Appelons vc" la valeur de oct1, donc vc"= vc'-1, et réécrivons une dernière fois notre égalité :
      v7 + vc"+1 = 128 (*****).

      Sachant que oct1 est obtenu par complément à 1 à partir de oct0, et que le bit de poids fort de oct0 est à 1,
      on voit que :
      • le bit de poids fort de oct1 est à 0, et que par conséquent la valeur de l'octet (notre vc") est celle de ses
        7 bits de poids faible ;
      • les 7 bits en question alternent par rapport à ceux de oct0 (dont la valeur est notre v7),
        c'est-à-dire que l'un d'entre eux est à 1 si et seulement si son homologue de oct0 est à 0.
      Il s'ensuit que pour calculer la somme v7 + vc", l'addition en binaire sur 7 bits donnera nécessairement
      111 1111 = 127.
      Constatons-le sur notre exemple, en superposant les deux octets : 
      oct0 : 1100 0010
      oct1 : 0011 1101
      et l'addition sur 7 bits :
        100 0010
       +
      011 1101
      ----------
        111 1111

      En revenant à (*****), on obtient donc v7 + vc"+1 = 127+1 = 128 comme annoncé.
      Et en remontant la chaîne des réécritures, on  a bien démontré l'égalité  (**) qui fonde la validité
      de l'instruction C (*).

    3. Enfin, d'une manière encore plus mystérieuse,

      en utilisant le type unsigned char : si on écrit

      unsigned char val;
      val = ch;


      la valeur de val sera celle que nous cherchons !

      En effet, le type unsigned char a pour valeurs l'intervalle [0, 255], et la conversion de type qui s'effectue
      lors de l'affectation de la valeur de ch (qui est de type char) à la variable val (unsigned char)
      envoie les octets "négatifs" de char sur le segment [128, 255].