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 1
avant 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 |
- A noter que si l’opérateur binaire tronquait vers le bas
111 | 1
tronquerait le premier opérande1 | 1
et le résultat serait001
, or il n’en est rien, c’est le deuxieme opérande qui est étendu111 | 001
et le résultat est111
.
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.