Les opérateurs
Terminologie
Un opérateur (operator) est une fonction dont l’écriture utilise un ou deux caractères spéciaux comme par exemple les opérateurs mathématiques : +, -, ×, ÷. Ainsi, pour le compilateur, l’écriture 4+7 correspond en fait à l’appel d’une fonction d’addition équivalente à l’écriture : Op+(4,7).
Note
Contrairement aux fonctions, les opérateurs n’utilisent pas une paire de parenthèses. En l’absence de délimiteurs, vous aurez parfois du mal à décrypter certaines écritures comme par exemple a+++b ou encore TBL[i]()[u](v).
Il existe trois catégories d’opérateurs établies suivant le nombre de paramètres utilisés par l’opérateur :
Les opérateurs binaires (binary operator) : ce sont les plus courants, ils prennent deux arguments et on trouve par exemple tous les opérateurs arithmétiques.
Les opérateurs unaires (unary operator) : plus rares, on trouve ici les opérateurs acceptant un seul paramètre. Le plus connu est l’opérateur ++ servant à incrémenter une variable ou encore l’opérateur ! servant à la négation dans une condition.
les opérateurs ternaires : il n’en existe qu’un et nous le considérons hors programme.
Le terme opérande (operand) désigne tout élément pouvant être utilisé par un opérateur. Les opérandes ne correspondent pas forcément à une valeur. Par exemple, dans l’écriture a+4; l’opérande de gauche correspond à un nom de variable.
Une expresssion (expression) est un terme désignant une séquence d’opérateurs et d’opérandes. Cette définition semble assez intuitive pourtant elle cache de nombreuses réalités bien plus vastes que les expressions arithmétiques. De plus, le résultat d’une expression n’est pas forcément un numérique et peut correspondre à n’importe quel type existant dans le langage. Voici quelques exemples :
L’opérateur () permet un appel de fonction. L’écriture toto() est ainsi une expression. Son type de retour peut être une chaîne de caractères et ainsi l’expression toto()+toto() correspond à la concaténation de deux chaînes.
L’opérateur [] permet l’accès à un tableau. L’écriture T[i] est ainsi une expression. Son type de retour correspond au type des éléments du tableaux. Ainsi, l’écriture T[T[i]] est une expression au même titre que sin(cos(t)).
L’opérateur = est utilisé pour effectuer une affectation. Cela peut paraître étrange car aucune opération de calcul n’est réellement effectuée. Cet opérateur retourne une valeur, au même titre que l’opérateur +, et cette valeur correspond à la valeur transmise durant l’affectation. Quel intérêt ? Tout simplement pour permettre l’écriture a = b = c où la variable a prend comme valeur le résultat de l’expression b=c.
Une expression peut regrouper des littéraux, des variables, des opérateurs et des appels de fonctions que le langage de programmation interprète pour produire un résultat. Ce processus, comme pour les expressions mathématiques, est appelé évaluation (evaluation). Qu’est-ce qui n’est pas une expression alors ? On peut citer par exemple les structures de contrôle if/for qui ne sont pas des expressions mais qui par contre utilisent des expressions pour fonctionner.
Note
On peut parler du type d’une expression, ce type correspond au type du résultat de son évaluation.
Indiquez si les affirmations suivantes sont vraies ou fausses :
Un opérateur unaire possède un seul paramètre.
Un opérateur se comporte comme une fonction.
Un opérateur binaire retourne un résultat binaire true/false.
Les opérations mathématiques sont traitées comme des opérateurs.
Une opérande désigne un appel de fonction.
Un opérateur binaire s’applique uniquement sur des valeurs binaires.
Dans une expression, on peut trouver des variables, des littéraux, des appels de fonctions.
L’instruction if (…) est une expression conditionnelle.
L’écriture a=toto() est une expression.
L’évaluation consiste à déterminer le résultat d’une expression.
Principe de fonctionnement
Dans le langage C++, les opérateurs sont omniprésents à tel point que l’on peut dire que le langage s’articule autours d’eux. Comprendre leur mécanique n’est pas chose facile. Cependant, leur logique interne est très proche des opérateurs arithmétiques que vous avez l’habitude d’utiliser. Ainsi nous allons tout d’abord rappeler les règles d’utilisation des opérateurs arithmétiques pour les généraliser ensuite à tous les opérateurs du C++. En assimilant cette mécanique, vous serez en mesure de prévoir le comportement de toute ligne de code écrite en C++, ce qui représente un atout majeur pour votre vie de développeur.
Opérateurs arithmétiques
Nous citons les opérateurs arithmétiques :
Nom anglais |
Nom français |
Notation |
---|---|---|
unary minus |
moins unaire |
-a |
addition |
– |
a + b |
subtraction |
soustraction |
a - b |
multiplication |
– |
a * b |
division |
– |
a / b |
modulo |
– |
a % b |
Note
L’opérateur modulo désigne le reste de la division de a par b, par exemple 8%5 = 3 ou encore 16%5=1.
La priorité des opérateurs
Lorsque l’on écrit 3*6+7, en l’absence de parenthèses, on pourrait interpréter cette écriture de deux manière différentes (3*6)+7 ou 3*(6+7) ce qui n’est pas une bonne chose car les résultats associés sont complètement différents. On utilise alors la règle suivante : les opérateurs avec une priorité plus forte sont exécutés en premier. Ainsi, par convention, la multiplication étant prioritaire sur l’addition, l’écriture 3*6+7 est interprétée comme (3*6)+7 ce qui donne au final Op+(Op*(3,6),7).
L’utilisation des parenthèses
Il est possible d’utiliser une paire de parenthèses dans une expression. Ces parenthèses ne correspondent pas à un opérateur du C++ à proprement parler. Elles ont pour rôle de définir une sous-expression à l’intérieur de l’expression courante. Dans la logique de la priorité des opérateurs, on pourrait considérer que la priorité des parenthèses lors de l’évaluation d’une expression est alors plus haute que tous les opérateurs existants. En résumé : priorité op() > priorité op* > priorité op+. Ainsi, l’expression entre parenthèses est d’abord évaluée et son résultat est ensuite transmis au reste du calcul.
Le sens d’évaluation
Lorsque l’on écrit 3+6+7+4, le résultat n’est pas très difficile à calculer de tête. Certains effectuerons les calculs dans un ordre différent en faisant d’abord 3+7 et ensuite 6+4 car l’addition est commutative. Cependant lorsque l’on trouve une expression contenant des + et des - comme dans l’exemple 3-6+7-4, il faut savoir dans quel ordre exactement sont effectués les calculs. En effet, le résultat de 3-((6+7)-4) n’a rien à voir avec celui de (3-6)+(7-4). Dans cet exemple, la règle sur les priorités ne s’appliquent pas car les opérateurs + et - ont des priorités équivalentes. Dans ce cas précis, nous appliquons la règle suivante : lorsque deux opérateurs ont des priorités équivalentes, l’évaluation se fait de gauche à droite. Ainsi, c’est le sens de lecture qui guide les calculs et dans l’expression 3-6+7-4 le calcul effectué est : (((3-6)+7)-4).
Note
Il existe quelques opérateurs dont l’évaluation se fait de droite à gauche. On peut citer par exemple l’opérateur d’affectation =. Ce choix peut sembler étrange, mais il vient d’une syntaxe que l’on peut parfois trouver dans les programmes : a=b=c=3. Une évaluation de gauche à droite ferait que la variable a deviendra égale à la variable b puis la variable b deviendrait égale à la variable c et la variable c prendrait finalement la valeur 3. Ce n’est pas ce que nous attendions ! L’évaluation de droite à gauche fait que la variable c devient égale à 3, la variable b prend la même valeur que c et vaut maintenant 3 et de la même manière la variable a prend la valeur 3.
Pour chacune des expressions, sa valeur :
Expression |
Valeur |
---|---|
2 + 3 * 2 |
|
2 + ( 5 % 2 ) |
|
(4+5)*7 |
|
(1+3)*(4+5) |
|
5+2-3+6-8 |
|
(4*3+2)-(3*2+2) |
L’ordre d’évaluation des opérandes
Cette section peut être sautée par le lecteur débutant.
Si nous savons dans quel ordre chaque opération est effectuée, l’ordre d’évaluation des opérandes n’est pas connu . Cette affirmation peut en première lecture vous sembler un peu floue. Prenons un exemple :
#include <iostream>
int a() { std::cout << "a"; return 1;}
int b() { std::cout << "b"; return 2;}
int c() { std::cout << "c"; return 3;}
int main()
{
std::cout << a() + b() + c() ; // les 6 combinaisons d'affichage sont possibles
}
L’appel des fonctions a(), b() et c() retourne respectivement 1, 2 et 3. Ainsi, dans tous les cas, le résultat sera toujours 6. Cependant, l’ordre d’évaluation des opérandes est quelconque. Ainsi le programme peut choisir d’appeler d’abord la fonction c() et ensuite la fonction b() et finalement la fonction a(). Il n’y aucune règle et les 6 combinaisons d’appels peuvent se produire. Il n’est même pas garanti que durant toute l’exécution du programme, l’ordre choisi soit toujours le même… Ainsi l’exemple précédent peut aussi bien afficher « abc » comme « bca » ou encore « acb ».
La plupart du temps, vous ne rencontrerez pas de problèmes. Cependant, il faut savoir que certaines écritures sont très risquées. Par exemple l’utilisation abusive des opérateurs ++ et -- peut amener des situations inextricables :
int b = 5;
int a = (b++) + (b--)
En effet, si l’opérande de gauche est évaluée en premier, son retour vaut 5 et la variable b passe à 6. Ensuite, l’opérande de droite retourne 6 et la variable b repasse à 5. Le résultat final vaut ainsi 11. Cependant, si le programme évalue l’opérande de droite avant, cette opérande retourne la valeur 5 et la variable b passe à 4 et l’opérande de gauche retourne 4 et la variable b repasse à la valeur 5. Le résultat final est alors 9. Vous venez de découvrir le coté sombre des opérateurs !
Note
Si l’ordre d’évaluation des opérandes était imposé, il deviendrait alors plus difficile d’effectuer des optimisations lors des calculs et lors des appels de fonctions. La quête d’optimisation à outrance des compilateurs C++ est donc à l’origine de cette réalité.
Note
Il existe des exceptions, notamment pour les opérateurs logiques ou cette fois l’ordre d’évaluation des opérandes est respecté. En effet, il s’agit d’une raison historique où lorsqu’on accède à un tableau, on pouvait écrire : if (i>=0) && (i<n) && (T[i]…). Le but est de vérifier que l’indice i est valide par rapport à la taille du tableau. Si le premier test est faux, un mécanisme de coupe-circuit évite alors l’évaluation des autres opérandes. Ainsi l’évaluation de T[i] est évitée car l’indice est incorrect. L’opérande de droite est évaluée seulement si les deux tests précédents sont valides.
Les différents opérateurs du C++
Nous généralisons le cas des opérateurs arithmétiques à l’ensemble des opérateurs du langage C++.
Opérateurs d’affectation
Nous présentons les différents opérateurs d’affectation. Les syntaxes +=, -=… ne sont pas plus rapides à l’exécution. Cependant, elles sont plus lisibles car on comprend plus facilement en lisant a += … que l’on augmente la valeur de a de la quantité qui suit le signe =.
Nom anglais |
Nom français |
Notation |
Équivalent |
---|---|---|---|
simple assignment |
affectation standard |
a = b |
|
addition assignment |
affectation après addition |
a += b |
a = a + b |
subtraction assignment |
affectation après soustraction |
a -= b |
a = a - b |
multiplication assignment |
affectation après multiplication |
a *= b |
a = a * b |
division assignment |
affectation après division |
a /= b |
a = a / b |
Pour chacune des affectations, sachant que la variable a vaut 5 au départ, donnez sa valeur après l’exécution de chaque affectation :
Expression |
Valeur |
---|---|
a += 3 |
|
a -= 2 |
|
a *= 2+1 |
Opérateurs de pré/post incrémentation
Nous présentons les opérateurs de pré et post incrémentation et décrémentation. Incrémenter une variable signifie l’augmenter de 1 et inversement décrémenter une variable signifie la diminuer de 1. Le terme pré ou post signifie que l’incrémentation ou la décrémentation auront lieu avant ou après avoir retourné la valeur courante de la variable. Lorsque l’opérateur ++ est à gauche de la variable, il s’agit de l’opérateur de pré-incrémentation : la variable est incrémenté puis cette variable est retournée comme résultat. Lorsque l’opérateur ++ est à droite, c’est l’inverse, la valeur est lue, puis ensuite la variable est incrémentée et finalement ce sera la valeur de départ qui est retournée et non la variable.
Note
Il y a une petite subtilité sur le type de retour des opérateurs de pré/post incrémentation. Tous les opérateurs s’appliquent sur une variable et non sur une valeur. Ainsi l’écriture 4++ n’a aucun sens. Pour l’opérateur de pré-incrémentation, le type de retour est un référence sur la variable courante. Ceci permet ainsi de chaîner cet opérateur, l’écriture ++++a est donc possible. Pour l’opérateur de post-incrémentation, le type de retour est une valeur ne permettant pas de chaîner cet opérateur : l’écriture a++++ est incorrecte. Respirez et relisez ce passage :)
Nom anglais |
Nom français |
Notation |
Exemple |
Résultat pour a = 5 |
---|---|---|---|---|
post-increment |
post-incrémentation |
a++ |
b = a++; |
b: 5 et a: 6 |
post-decrement |
post-décrémentation |
a-- |
b = a--; |
b: 5 et a: 4 |
pre-increment |
pré-incrémentation |
++a |
b = ++a; |
b: 6 et a: 6 |
pre-decrement |
pré-décrémentation |
--a |
b = --a; |
b: 4 et a: 4 |
Dans la pratique, on utilisera 99% du temps les opérateurs de post-incrémentation/décrémentation.
Sachant que la variable a vaut 5 avant l’exécution de chaque ligne, donnez l’affichage ainsi que la valeur de a après l’exécution :
Code |
Affichage |
Valeur de a après l’exécution |
---|---|---|
std::cout << a++; |
||
std::cout << a--; |
||
std::cout << --a; |
||
std::cout << ++a; |
Note
Pour ces différents opérateurs, un sens d’évaluation gauche/droite ou droite/gauche est donné, ce qui peut paraître assez étrange pour un opérateur unaire. En fait, la direction indiquée permet de situer la variable par rapport à l’opérateur. Ainsi comme l’opérateur de post-incrémentation est indiqué comme s’évaluant de gauche à droite, il faut donc comprendre que son paramètre se trouve à gauche comme dans l’écriture de a++. Inversement, l’opérateur de pré-incrémentation est indiqué comme s’évaluant de droite à gauche, il faut donc comprendre que son paramètre se trouve à droite comme dans l’écriture de ++a.
Opérateurs logiques
Nous présentons les opérateurs logiques généralement utilisés à l’intérieur des conditions :
Nom anglais |
Nom français |
Notation |
---|---|---|
equal to |
égalité |
a == b |
not equal to |
différent |
!= b |
negation |
non logique |
!a |
a and b |
et logique |
a && b |
a or b |
ou logique |
a || b |
less than |
inférieur strict |
a < b |
greater than |
supérieur strict |
a > b |
less than or equal to |
inférieur ou égal |
a <= b |
greater than or equal to |
supérieur ou égal |
a >= b |
Note
Comme dans le cas de l’opérateur de pré-incrémentation, l’opérateur non logique ! est décrit comme ayant une lecture de droite à gauche. Comme il s’agit d’un opérateur unaire, il faut ainsi comprendre qu’il s’applique sur l’élément à sa droite : !a.
Donnez les valeurs true ou false des expressions suivantes, sachant que t désigne une variable valant true, f une variable valant false et v une variable entière valant 5:
Expression |
Valeur |
---|---|
(t && t)|| (t||f) |
|
(v<10)&&(v>0) |
|
(v<=5) || (t||f) |
|
!(v>0) |
Opérateurs spéciaux
Comme nous en avons déjà parlé, les opérateurs s’étendent au delà des expression arithmétiques ou conditionnelles. Nous listons les principaux opérateurs ci-dessous :
Nom anglais |
Nom français |
Notation |
Exemple |
---|---|---|---|
Scope resolution operator |
Résolution de portée |
:: |
std::cout |
Function call operator |
Opérateur d’appel de fonctions |
( ) |
Fnt() |
Subscript operator |
Opérateur d’indexation |
[ ] |
T[4] |
Member access operator |
opérateur d’accès aux membres |
. |
object.position |
Priorité des opérateurs
La norme du C++ définit la priorité de tous les opérateurs ainsi que leur sens d’évaluation gauche droite ou droite gauche. Nous résumons ci-dessous la priorité des principaux opérateurs.:
Priorité |
Opérateur |
Nom |
Sens |
---|---|---|---|
1 |
:: |
Résolution de portée |
🠚 |
2 |
a++ a-- |
Opérateurs de post incrémentation/décrémentation |
🠚 |
a() |
Opérateur d’appel de fonctions |
🠚 |
|
a[] |
Opérateur d'indexation |
🠚 |
|
3 |
++a --a |
Opérateurs de pré incrémentation/décrémentation |
⇦ |
-a |
Opérateur moins unaire |
⇦ |
|
! |
Opérateur NON logique |
⇦ |
|
5 |
a*b a/b |
a%b Opérateurs arithmétiques : multiplication division modulo🠚 |
|
6 |
a+b a-b |
Opérateurs arithmétiques : addition et soustraction |
🠚 |
9 |
< <= > >= |
Opérateurs de comparaison |
🠚 |
10 |
== != |
Opérateurs d’égalité et de différence |
🠚 |
14 |
&& |
Opérateur ET logique |
🠚 |
15 |
|| |
Opérateur OU logique |
🠚 |
16 |
= += -= *= /= |
Opérateurs d’affectations |
⇦ |
Note
Vous remarquez la présence d’un petit nouveau dans la liste : l’opérateur moins unaire ! Mais où peut-on trouver un tel opérateur ? Le signe - n’est en effet pas toujours utilisé comme un opérateur binaire. Par exemple, il peut servir d’inversion de signe comme dans l’écriture : a = -b. Comme il s’agit d’un opérateur unaire, son sens de lecture de droite à gauche, indique en fait que l’élément sur lequel il s’applique se trouve à sa droite.
Donnez les valeurs des expressions suivantes, sachant que t désigne une variable valant true, f une variable valant false et v une variable entière valant 5:
Expression |
Valeur |
---|---|
(v++> 5) |
|
(v=v) |
|
(v<5==v<3) |
|
(–v <= 4) |
|
(!t==false) |