Expressions

En C, une expression consiste en un ou plusieurs opérandes et son ou ses opérateurs associés.

Une expression peut être évaluée conditionellement , être séquencée ou non ou produire un effet visible sur le programme ou non (on parle aussi d’effet de bord).

Une opération peut faire intervenir des valeurs littérales, des objets et des fonctions.

Expressions arithmétiques

Lorsque deux opérandes sont des nombres entiers, 5 opérateurs arithmétiques peuvent leur être appliqués : +, -, *, /, % qui signifient respectivement l’addition, la soustraction, la multiplication, la division et le reste de la division, ou module.

Les opérandes peuvent être de valeurs intégrales littérales, ou des objets de ce type, ou des pointeurs vers des objets de ce type.

int main(void) {
    int toto = 5;
    int * ptoto = &toto;

    1+1;  // 2
    toto+1;  // 6
    *ptoto + 2; // 7
    1-2;  // -1
    4*3;  // 12
    5/2;  // 2
    5%2;  // 1
}

Si une opération arithmétique produit une valeur trop grande pour être contenue dans la variable qui lui est associée il y’à « perte de retenue », ou encore « erreur d’opérande » ou encore « dépassement d’entier » (de l’anglais integer overflow). C’est à dire que les bits qui dèpassent sont tronqués.

🧨 Dans ce cas d’un entier signé, vu qu’il existe trois manières de les repésenter en C, il n’y a pas de garantie quand à la valeur obtenu en cas de dépassement, donc le comportement est indéfini dans ce cas.

int main()
{
    signed char val = 127; // 0b01111111
    val = val + 1; // 0b10000000

    printf("%d\n", val);
    // signe et magnitude : -0
    // complément à un : -127
    // complément à deux: -128
}

(depuis le C23 ) Depuis le C23, la seule représentation acceptée par la norme des nombres signés est le complément à deux, donc le comportement de ce programme devient défini pour afficher « -128 ».

Pour les nombres non signés, le comportement sera le même quelle que soit la représentation la valeur zero, c’est à dire la troncature des bits qui débordent.

Arithmétique à nombre flottants

Si un des deux opérandes est un nombre flottant, l’autre sera converti en nombre flottant si besoin et les 4 opérateurs suivants peuvent leur être appliques : +, -, * et /, noter l’absence du module.

int main(void) {
    int toto = 5;

    1+1.5;  // 2.50000
    toto+1.0;  // 6.00000
    1-2.;  // -1.00000
    4*3.;  // 12.00000
    5./2;  // 2.500000
    0.0/0.0;    // Nan
    1.0/0.0;    // Inf
    -1.0/0.0;   // -Inf
    // 4.0 % 2.0;  // interdit
}

En cas de division par zero, l’unité de calcul en virgule flottante produit la valeur « not a number » (NaN).

En cas de dépassement de capacité soit vers les nombres positifs, la valeur « plus infini » (+Inf) est produite.

En cas de dépassement de capacité négative, la valeur « mois infini » (-Inf) est produite.

On peut obtenir la valeur infinie en divisant par le zero positif ou négatif.

float plus_inf = 42.0f / 0.f;
float moins_inf = 42.0f / -0.f;

Arithmétique de pointeurs

Il est possible de soustraire deux pointeurs entre eux, d’additionner et soustraire une valeur entière à un pointeur. Avec un pointeur l’addition est commutative. Dans le cas de la soustraction l’operande pointeur doit être forcément à gauche.

Il est interdit d’utiliser les autres operateurs artihmétiques dans l’arithmétique utilisant des pointeurs en opérande (on l’appelle également arithmétique de pointeurs), c’est à dire multiplier, diviser ou chercher le reste de la division d’un pointeur. Seuls les valeurs entières sont acceptées comme opérandes en vis à vis de pointeurs.

int main(void) {

    int toto, tata;

    &toto + 1;  // ok
    // &toto + 1.0;  // interdit
    // &toto + &tata;  // interdit
    1 + &toto;  // ok
    &toto - 1;  // ok
    // 1 - &toto;  // interdit
    &toto - &tata;  // ok
    &tata - &toto;  // ok
    // &toto * 2;  // interdit
    // &toto / 2;  // interdit
    // &toto % 2;  // interdit

}

Opérateurs unaires

Il existe deux opérateurs arithmétiques unaires, le plus (depuis le C ansi ) et le moins, notés respectivement + et -. Tous deux appliquent la promotion d’entier à leur opérande. Comme toute opération arithmétique, le résultat est une non l-value.

Dans le cas du plus unaire, le résultat après promotion est retourné, dans le cas du moins, l’inverse du résultat est retourné. Dans une architecture représentant les nombres signés en signe et magnitude ou en complément à un, -0 peut avoir une représentation différente de 0, à savoir pour un char, 0b10000000 et 0b11111111 respectivement, 0 valant 0b00000000 dans tous les cas.

int main(void) {
    bool toto = true;
    int tata = 2;

    +1;     // 1
    -1;     // -1
    +-1;    // -1
    -+-1;   // 1
    +toto;  // 1
    -toto;  // -1

    tata = tata + +toto;   // tata == 3
    // +tata = 5;   // illegal, +tata n'est pas une lvalue.

    char c = 1;
    int s1 = sizeof(c); // 1
    int s2 = sizeof(+c); // 4
}

Conversions implicites

Lorsque deux opérandes ne sont pas du même type, celui qui possède le type le plus petit est converti en celui du type le plus grand. Si l’un des opérandes est de type à virgule flottante, l’autre sera converti en nombre à virgule flottante, l’opération se fera dans l’unité de calcul en vigule flottante et le résultat sera un nombre à virgule flottante.


int main(void) {

    5/2;  // 2, l'unité de calcul intgeral intervient
    5/2.0;  // 2.50000, le processeur de nombres à virgule flottant intervient
}

Opérateurs de test

Les opérateur de test sont ==, !=, <, >, <=, >=, &&, || et !. Tous ces opérateurs ne peuvent produire que deux valeurs : 0 ou 1, il y a donc perte d’information avec ceux-ci, mais on peut faire de l’arithmétique avec :

#include <stdio.h>

int main()
{
    printf("%d\n", (1 == 1) + (4 > 2) + (0 || 10) + !0); // 4
}

Avant que le test soit effectué, les deux opérandes sont promus , l’opérande est converti en 0 si sa valeur est zero, en 1 si valeur est n’importe quelle valeur différente de zero.

Dans le cas d’un numbre flottant , il existe deux manières de représenter zero, l’une avec le bit de signe positif représenté +0.f et l’autre avec un bit de signe négatif représenté -0.f qui correspond à la valeur 0.f auquel on applique l’opérateur -, qui dans le cas d’un nombre flottant revient à changer son bit de signe.

Dans 0.f == -0.f, comme les deux zero sont considérés valoir zero, ils sont chacun convertis en 0, d’où 0 == 0, le résultat est 1.

Les opérateurs de test s’associent de gauche à droite : l’expression 4 == 2 + 2 == 2 * 2 sera évaluée ainsi 4 == (2 + 2) == (2 * 2), soit 4 == 4 == 4, soit (4 == 4) == 4, soit 1 == 4 (on se rapelle qu’il ya perte d’information et une conversion implicite vers 1), soit 0. Si on veut réelement chainer les opérateurs logiques plutot que décrire a == b == c, il faut écrire (a == b) && (b == c).

🐍 A noter que le python lève cette lourdeur de syntaxe et il est tout à fait possible d’écrire en python 1 < 2 == 2 < 4 == 4 qui évalue à True car 1 est inférieur à 2 qui est égal à 2, qui est inférieur 4, etc.

Dans une machine représentant les nombres négatifs en signe et magnitude ou bien en complément à un, il y a deux manières de représenter zero et de la même façon que pour un nombre flottant, chacune est considérée comme valant zero dans un test booléen, malgré que leur représentation binaire ne soit pas entièrement composée de zeros.

L’écriture -0 ne correspond pas à un syntagme du langage C, mais au nombre octal zero auquel on applique l’ opérateur unaire -. Il n’est pas garanti que l’application de cet opérateur produise la représentation binaire du zero négatif. Toutefois l’intéret des représentations signées en signe et magnitude et en complément à un est que le changement de signe d’un nombre soit peu coûteux, il est donc probable que -0 produise l’effet escompté d’avoir un zero négatif.

-0 sur une machine en complément à deux change 0b00000000 en 0b11111111, puis lui ajoute 1, ce qui avec la perte de retenue produit 0b00000000, soit la représentation binaire initiale.

(depuis le C23 ) La seule représentation signée des nombres comprise dans la norme est en complément à deux, la question du zero négatif ne se pose plus que dans le cas des nombres flottants.

int main(){
        volatile union u{
            float fv;
            int iv;
        } uv;
        uv.fv = 0;
        if(!uv.fv) {
            puts("zero positif flottant");
        }
        if(!uv.iv) {
            puts("zero positif entier");
        }
        printf("%x\n", uv.iv);
        uv.fv = -0.0f; // attention bien écrire -0.0f : uv.fv = -0; n'aura pas le même effet
        if(!uv.fv) {
            puts("zero negatif flottant");
        }
        if(!uv.iv) {
            puts("zero negatif entier");
        }
        printf("%x\n", uv.iv);

        printf("%hhd\n", 0.f == -0.f);
}

/* affiche
zero positif flottant
zero positif entier
0
zero negatif flottant
80000000
1
*/

Egalité : ==

== test l’égalité des deux opérandes. Au niveau du processeur, il est généralement implémenté en soustrayant les deux opérandes puis en vérifiant que le résultat est égal à zero.

int main(void) {

    int a = (5 == 5);  // 1
    int b = (a == 0);  // 0
}

La table de vérité qui lui correspond est la suivante

a b a == b
0 0 1
0 1 0
1 0 0
1 1 1

Différence : !=

!= teste l’inálité des deux opérandes, a != b est une manière raccourcie d’écrire (a == b) == 0.

a b a != b
0 0 0
0 1 1
1 0 1
1 1 0

A proscrire avec des nombres flottants

🧨 Il est tout à fait déconseillé d’utiliser les opérateurs == et != avec des nombres flottants car le résultat peut être aléatoire : selon la machine et le comportement du programme n’est pas garanti. De même chaque opération arithmétique sur un nombre flottant peut accumuler des écarts infinitésimaux. Pour vérifier l’égalité de deux nombres flottants on utilise l’idiome (fabs(x - 1.0) < FLT_EPSILON) :

#include <math.h>

int main() {
    float x = 0.0;
    while (x != 1.0) {  // ne sera jamais validée à cause des erreurs d'arrondi
        // ...
        x += 0.1; // accumulation d'erreurs infinitésimales
        if (fabs(x - 1.0) < FLT_EPSILON) {
            // idiome à utiliser pour verifier que deux nombres flottants sont "egaux"
            break; 
        }
    }
}

Infériorité : <

< teste si l’opérande de gauche est inférieur à l’opérande de droite. Au niveau du processeur ilest géneralement implémenté en soustrayant l’opérande à gauche à celui de droite puis en regardant le bit de signe.

a b a < b
0 0 0
0 1 1
1 0 0
1 1 0

Supériorité : >

> teste si l’opérande de gauche est supérieur à l’opérande de droite. Il revient à écrire (a < b) == 0.

a b a > b
0 0 0
0 1 0
1 0 1
1 1 0

Et logique : &&

&& teste que les deux opérandes soient différents de zero. a && b revient à tester a != 0, le cas échéant, on teste b != 0, sinon l’opérateur renvoye zero (0) et on ne teste pas b. Si b != 0 renvoye 1 alors le résultat est 1, sinon 0.

a b a && b
0 * 0
1 0 0
1 1 1

*signifie que l’opérande n’est pas évalué

Évaluation court-circuit

Le fait que le deuxième opérande ne soit pas évalué siginifie que dans le code suivant

# include <stdio.h>

int toto() {

    puts("tata");
    return 1;
}

int main(void) {

    0 && toto(); 
}

Le texte “tata” ne sera jamais affiché car toto() ne sera pas évaluée pendant l’exécution du programme.

Ou logique ||

|| teste que l’un ou chacun des deux opérandes soient différents de zero. a || b revient à tester a != 0, le cas échéant, le résultat est 1 et on ne teste pas b. Sinon on teste b != 0. Si b != 0 vaut 1 alors le résultat est 1, sinon 0.

L’opérateur || correspond à la table de vérité suivante

a b a || b
0 0 0
0 1 1
1 * 1

*signifie que l’opérande n’est pas évalué

Absence de « ou exclusif »

Il n’existe pas de notation raccourcie pour exprimer le « ou exclusif », il faut donc le noter entièrement

(a && !b) || (!a && b) — ou si l’on ne souhaite pas évaluer deux fois chaque opérande !!a ^ !!b.

Inférieur ou égal : <=

L’opérateur <= teste que l’opérande de gauche soit inférieur OU égal ‘a l’opérande de droite.

a <= b revient à écrire (a < b || a == b) à la différence que les opérandes a et b ne sont évalués qu’une seule fois. C’est à dire que

# include <stdio.h>

int toto1() {

    puts("tata1");
    return 1;
}

int toto2() {

    puts("tata2");
    return 1;
}


int main(void) {

    int a = (0 <= toto1()); // affiche "tata1"
    int b = (0 == toto2() || 0 < toto2()); // affiche "tata2 tata2"
}
a b a <= b
0 0 1
0 1 1
1 0 0
1 1 1

L’opérateur >= teste que l’opérande de gauche soit supérieur OU égal à l’opérande de droite, avec la même remarque que ci-dessus.

a b a >= b
0 0 1
0 1 0
1 0 1
1 1 1

L’opérateur ! teste que l’opérateur qui lui est associé soit égal à zero, il renvoye 1 dans ce cas, 0 sinon. !a revient à écrire a == 0.

L’opérateur ! correspond à la table de vérité suivante

a !a
0 1
1 0

Opérateurs binaires

Les opérateurs &, |, ^, ~, << et >> permettent de manipuler directement la représentation binaire de valeurs.

Là où les opérateurs logiques convertissent chaque valeur en 0 ou 1avant d’effectuer les calculs, les opérateurs binaires conservent leurs opérandes intacts et effectuent chaque opération bit à bit sans retenue de valeur d’un rang à l’autre.

Si l’un des opérandes n’a pas le même nombre de bits que l’autre, des zero 0 sont ajoutés à celui qui est le plus court jusqu’à ce que les deux opérateurs soient commensurables : c’est à dire qu’il n’y a pas de troncation : la nivelation se fait vers le haut.

Pour chaque couplet de bits, voici la table de vérité correspondant à chaque opérateur

Et : &

a b a & b
0 0 0
0 1 0
1 0 0
1 1 1
000 000 000
111 000 000
111 111 111
111 101 101
000 0 (000) 000
111 0 (000) 000
110 0 (000) 000
101 0 (000) 000
000 1 (001) 000
111 1 (001) 001
110 1 (001) 000
101 1 (001) 001

Il est utile pour « éteindre » un bit d’une suite de bits quelconque.

Imaginons qu’on ait la valeur 0b11000110 et qu’on veuille éteindre le troisème bit en partant de la droite, la manière la plus simple est de lui appliquer un « et » avec un champ où tous les bits sont à 1 sauf celui qu’on veut éteindre, c’est à dire 0b11000110 & 0b11111011

Cela est plus simple que par une soustraction car si le bit qu’on voulait éteindre était à zero , la soustraction se reporterait sur la rangée à droite, il faut donc d’abord lire le bit et si il vaut 1, soustraire 1, sinon 0. De la même manière passer par un « ou exclusif ^ » suppose de d’abord lire la valeur qui s’y trouve. Un « et &0 » quand à lui ne suppose pas de connaitre la valeur qui se trouve à son rang et supprimera le bit quoi qu’il arrive. La seule difficulé est de mettre tous les bits du masque qu’on applique qu’on ne veut pas toucher à 1.

Pour obtenir 0b11111011, la manière la plus simple est d’utiliser l’opérateur « non ~ » de la sorte ~ 0b00000100. Pour obtenir 0b00000100, on peut faire 1 << 2. En somme la fonction qui supprime une bit d’un rang donné peut s’écrire

int eteind_bit(int val, int pos)
{
    return val & ~(1 << pos);
}

Ou : |

a b a | b
0 0 0
0 1 1
1 0 1
1 1 1
000 000 000
111 000 111
111 111 111
111 101 111
000 0 (000) 000
111 0 (000) 111
110 0 (000) 110
101 0 (000) 101
000 1 (001) 001
111 1 (001) 111 *
110 1 (001) 111
101 1 (001) 101

Cet opérateur est utile pour allumer n’importe quel bit d’un nombre.

Pour allumer le 4ème bit en poartant de la droite de 0b11000110, il suffit de lui appliquer 0b00001000, soit 0b11000110 | 0b00001000. Pour obtenir b00001000 il suffit d’écrire 1 << 3.

Une fonction qui allume un bit d’un nombre peut s’écrire

int allume_bit(int val, int pos)
{
    return val | (1 << pos);
}

Ou exclusif : ^

a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0

Cet opérateur est utile en cryptographie car appliquer un masque sur un nombre avec cet opérateur deux fois de suite retourne la valeur initiale. Par exemple soit le masque 0b1010, si on l’applique à 0b1100, on a 0b1100 ^ 0b1010 soit 0b0110. Si on applique 0b1010 à 0b0110, on a 0b0110 ^ 0b1010, soit 0b1100, soit la valeur initiale. Tant que la clé de chiffrement est gardée secrète, on peut “cacher” la valeur initiale en lui appliquant un « ou exclusif ».

Inverse : ~

a ~a
0 1
1 0
0b000 0b111
0b001 0b110
0b100 0b011
0b111 0b000

De la même maniêre que l’opérateur « ou exclusif », cet opérateur appliqué deux fois à un nombre retourne le nombre initial, il a donc une utilité en cryptographie non destructive.