Programmation C/Opérateurs

Un livre de Wikilivres.


Les opérateurs du C[modifier | modifier le wikicode]

Les opérateurs du C permettent de former des expressions, expressions qui diront quoi faire à votre programme. On peut voir un programme C comme étant composé de trois catégories d'instructions :

  • Les déclarations et définitions (variables, fonctions, types) : déclarent et définissent les objets que le programme pourra manipuler.
  • Les expressions : manipulent les déclarations, via les opérateurs.
  • Les instructions : manipulent les expressions pour leur donner une signification particulière (test, boucle, saut, ...).

Les déclarations de variables ont en partie été décrites dans les chapitres précédents. Les expressions seront en partie décrites dans celui-ci (les opérateurs liés aux pointeurs, tableaux, structures et fonctions seront décrits dans des chapitres dédiés), et les instructions seront décrites au cours des chapitres suivants.

Les expressions en C sont en fait très génériques, elles peuvent inclure des opérations arithmétiques classiques (addition, soustraction, division, modulo, ...), des expressions booléennes (OU logique, ET logique, OU exclusif, ...), des comparaisons (égalité, inégalité, différence, ...) et même des affectations (copie, auto-incrément, ...). Toutes ces opérations peuvent s'effectuer en une seule expression, il suffit d'appliquer les bons opérateurs sur les bons opérandes.

Les opérateurs binaires et ternaires utilisent une notation infixe (l'opérateur se trouve entre les 2 ou 3 opérandes). Les opérateurs unaires s'écrivent de manière préfixé (avant l'opérande), à l'exception des opérateurs ++ et -- qui peuvent s'écrire de manière préfixée ou suffixée (avec une différence subtile, décrite ci-après).

La priorité (quel opérateur est appliqué avant, en l'absence de parenthèses explicites) et l'associativité (dans quel ordre sont traités les arguments des opérateurs ayant la même priorité) sont résumées dans la table suivante (par ordre décroissant de priorité - les opérateurs décrits dans un autre chapitre ont un lien dédié) :

opérateur parité associativité description
( ) gauche vers la droite (GD) parenthésage
() [] . -> GD appel de fonction, index de tableau, membre de structure, pointe sur membre de structure
! unaire droite vers la gauche (DG) négation booléenne
~ unaire DG négation binaire
++ -- unaire DG incrémentation et décrémentation
- unaire DG opposé
(type) unaire DG opérateur de transtypage (cast)
* unaire DG opérateur de déréférencement (ou indirection)
& unaire DG opérateur de référencement (ou d'adresse)
sizeof unaire DG fournit la taille en nombre de "char" de l'expression (souvent en octet mais pas toujours, mais sizeof(char) == 1 par définition, voir Caractères)
* / % binaire GD multiplication, division, modulo (reste de la division)
+ - binaire GD addition, soustraction
>> << binaire GD décalages de bits
> >= < <= binaire GD comparaisons
== != binaire GD égalité/différence
& binaire GD et binaire
^ binaire GD ou exclusif binaire
| binaire GD ou inclusif binaire
&& binaire GD et logique avec séquencement
|| binaire GD ou logique avec séquencement
? : ternaire DG si...alors...sinon
= += -= *= /= %= ^= &= |= >>= <<= binaire DG affectation
, binaire GD séquencement

Post/pré incrémentation/décrémentation[modifier | modifier le wikicode]

C'est un concept quelque peu sibyllin du C. Incrémenter ou décrémenter de un est une opération extrêmement courante. Écrire à chaque fois 'variable = variable + 1' peut-être très pénible à la longue. Le langage C a donc introduit des opérateurs raccourcis pour décrémenter ou incrémenter n'importe quel type atomique (gérable directement par le processeur : c'est à dire pas par un tableau, ni une structure ou une union). Il s'agit des opérateurs '++' et '--', qui peuvent être utilisés de manière préfixée ou suffixée (avant ou après la variable).

Utilisé de manière préfixée, l'opérateur incrémente/décrémente la variable, puis retourne la valeur de celle-ci. En fait, les expressions (++E) et (--E) sont équivalentes respectivement aux expressions (E+=1) et (E-=1).

Par contre, utilisé de manière suffixée, l'opérateur retourne la valeur originale avant de modifier la valeur de la variable.

int i = 0, j;

j = i++; /* j vaut  0 et i vaut  1 */
j = --i; /* j vaut 0 et i vaut 0 */

Il est important de noter que le langage ne fait que garantir qu'une variable post-incrémentée ou post-décrémentée acquiert sa nouvelle valeur entre le résultat de l'opérateur et le prochain point de séquencement atteint (généralement la fin d'une instruction). Mais on ne sait pas vraiment quand entre les deux. Ainsi, si l'objet sur lequel s'applique un tel opérateur apparaît plusieurs fois avant un point de séquencement, alors le résultat est imprévisible et son comportement peut changer simplement en changeant les options d'un même compilateur :

Avertissement Ce code contient plusieurs erreurs volontaires !
/* Le code qui suit est imprévisible */
int i = 0;
i = i++;
Avertissement Ce code contient une erreur volontaire !
/* Et celui-là d'après vous ? */
int i = 0;
i = ++i;

Contrairement à ce à quoi vous pouviez vous attendre, le bout de code du deuxième bloc de code n'est pas mieux défini que celui du premier bloc de code. La valeur de i y est modifiée une première fois lors de son incrémentation, puis une deuxième fois par l'opérateur d'affectation =. Or, un autre moyen de générer un comportement indéfini est de modifier la valeur d'une variable plusieurs fois entre deux points de séquencement.

Promotion entière[modifier | modifier le wikicode]

La promotion entière, à ne pas confondre avec la conversion automatique de type, fait que tous les types plus petits ou égaux à int (char, short, champs de bits, type énuméré) sont convertis (promus) en int ou unsigned avant toute opération. Ainsi, dans l'exemple suivant, a + b est calculé avec des int et le résultat est de type int :

short sum(short a, short b) {
	return a + b; /* équivaut à : return (int)a + (int)b; */
}

Le compilateur peut émettre un avertissement du fait que le résultat int est converti en short pour être retourné, et cette conversion peut causer une perte de précision (par exemple si int a une largeur de 32 bits et short de 16 bits).

La promotion se fait vers unsigned lorsqu'un int ne peut pas représenter la valeur promue.

Conversion automatique[modifier | modifier le wikicode]

La conversion est un mécanisme qui permet de convertir implicitement les nombres dans le format le plus grand utilisé dans l'expression.

L'exemple classique est le mélange des nombres réels avec des nombres entiers. Par exemple l'expression '2 / 3.' est de type double et vaudra effectivement deux tiers (0,666666...). L'expression '2 * a / 3' calculera les deux tiers de la variable 'a', et arrondira automatiquement à l'entier par défaut, les calculs ne faisant intervenir que des instructions sur les entiers (en supposant que 'a' soit un entier). À noter que l'expression '2 / 3 * a' vaudra... toujours zéro !

Plus subtile, l'expression '2 * a / 3.', en supposant toujours que a soit un entier, effectuera une multiplication entière entre 2 et a, puis promouvra le résultat en réel (double) puis effectuera la division réelle avec trois, pour obtenir un résultat réel lui-aussi.

Enfin un cas où les promotions peuvent surprendre, c'est lorsqu'on mélange des entiers signés et non-signés, plus particulièrement dans les comparaisons. Considérez le code suivant :

/* Ce code contient un effet de bord sibyllin */
unsigned long a = 23;
signed char b = -23;

printf( "a %c b\n", a < b ? '<' : (a == b ? '=' : '>') );

De toute évidence a est supérieur à b dans cet exemple. Et pourtant, si ce code est exécuté sur certaines architectures, il affichera a < b, justement à cause de cette conversion.

Tout d'abord dans la comparaison a < b, le type de a est « le plus grand », donc b est promu en unsigned long. C'est en fait ça le problème : -23 est une valeur négative, et la conversion d'une valeur négative en un type non signé se fait modulo la valeur maximale représentable par le type non signé + 1. Si la valeur maximale pour le type unsigned long est 232-1, alors -23 est converti modulo 232, et devient 4294967273 (soit 232-23). Dans ce cas là effectivement 23 < 4294967273, d'où ce résultat surprenant.

À noter qu'on aurait eu le même problème si le type de b était signed long. En fait les types signés sont promus en non-signés, s'il y a au moins une variable non-signée dans l'expression, et que le type non-signé est "plus grand" que le type signé.

Dans ce cas présent, il faut effectuer soi-même le transtypage :

printf( "a %c b\n", (long) a < b ? '<' : ((long)a == b ? '=' : '>') );

La norme définit précisément les conversions arithmétiques et les promotions entières, se référer à elle pour les détails.

Évaluation des expressions booléennes[modifier | modifier le wikicode]

Le C ne possède pas de type booléen dédié[1]. Dans ce langage, n'importe quelle valeur différente de zéro est considérée vraie, zéro étant considéré comme faux. Ce qui veut dire que n'importe quelle expression peut être utilisée à l'intérieur des tests (entier, réels, pointeurs, tableaux, etc.). Cela peut conduire à des expressions pas toujours très claires, comme :

int a;
a = une_fonction();
if (a)
{
    /* ... */
}

Ce type d'écriture simplifiée a ses adeptes, mais elle peut aussi s'écrire de la manière suivante:

int a;
a = une_fonction();
if (a != 0)
{
    /* ... */
}

Cette seconde écriture, sémantiquement équivalente à la première, rend explicite le test qui est effectué (on compare la valeur de la variable a à zéro).

On peut également tester directement la valeur de retour de la fonction :

if (une_fonction() != 0)
{
    /* ... */
}

Par ailleurs, cette absence de type booléen rend aussi valide le code suivant :

int a = 0;
int b = 2;

if (a = b)
{
        /* Le code qui suit sera toujours exécuté car b est différent de 0 ... */
}

Dans cet exemple, il y a un seul caractère = entre a et b, donc on ne teste pas si a est égal à b, mais on affecte la valeur de la variable b à la variable a. Le résultat de l'expression étant 2, elle est donc toujours considérée comme vraie (elle serait toujours considérée comme fausse si b avait la valeur 0). Ce genre de raccourci est en général à éviter. En effet, il est facile de faire une faute de frappe et d'oublier de taper un caractère =. Comme le code résultant est toujours valide au sens du C, cette erreur peut ne pas être vue immédiatement. Pour aider les développeurs à détecter ce genre d'erreurs, de nombreux compilateurs émettent un avertissement quand un tel code leur est présenté. Une manière de faire taire ces avertissements, une fois qu'on s'est assuré qu'il ne s'agit pas d'une erreur, est de mettre entre parenthèses l'affectation:

/* On veut faire une affectation ici. */
/* doublement des parenthèses pour supprimer l'avertissement du compilateur*/
if ((a = b))
{
    /* ... */
}

Comme un développeur devant maintenir un programme contenant un test if (a = b) { /* ... */ } se demandera si le développeur précédent n'a pas fait une faute de frappe, il est préférable d'éviter autant que possible ce genre de situations et, s'il est nécessaire (ce qui a très peu de chances d'arriver), de la commenter. En effet, même un code if ((a = b)) { /* ... */ } non commenté doit être lu avec attention, car le développeur précédent peut avoir ajouté les parenthèses juste pour faire taire le compilateur, sans se rendre compte d'une erreur. Dans tous les cas, la manière la plus sûre est de décomposer ainsi :

a = b;
if (a != 0)
{
    /* ... */
}

Une autre technique classique, lorsqu'une comparaison fait intervenir une constante, est de mettre la constante à gauche. De cette manière, si la comparaison se transforme par mégarde en affectation, cela provoquera une erreur à la compilation :

if (0 == b)
{
    /* Une instruction "0 = b" ne passerait pas */
}

Les opérateurs logiques de comparaisons (&& et ||, similaires sémantiquement à leur équivalent binaire & et |) évaluent leurs opérandes en circuit court. Dans le cas du ET logique (&&), si l'opérande gauche s'évalue à faux, on sait déjà que le résultat du ET sera faux et donc ce n'est pas la peine d'évaluer l'opérande droite. De la même manière si l'opérande gauche d'un OU logique (||) est évalué à vrai, le résultat sera aussi vrai et donc l'évaluation de l'opérande droite est inutile. Ceci permet d'écrire des expressions de ce genre, sans générer d'exception de l'unité arithmétique du processeur :

if (z != 0 && a / z < 10)
{
    printf("Tout va bien\n");
}

Remarquez aussi que les opérateurs logiques contrairement à ceux bit-à-bit sont non commutatifs. Dans la littérature des années 70 et 80 on les nomme parfois comme opérateurs de McCarthy.

Et voila ![modifier | modifier le wikicode]

Dans l'exemple ci-dessous, on veut affecter à la variable len la longueur de la chaîne pointée par la variable string, mais en vérifiant d'abord que celle-ci ne contient pas le pointeur NULL (dont la valeur est 0), auquel cas len prendra la valeur 0.

if (string)
 len = strlen(string);
else
 len = 0;

On peut réduire cette instruction par :

len = string ? strlen(string) : 0;

C'est-à-dire que len reçoit si string est non-NULL la longueur de string, sinon 0


Notes[modifier | modifier le wikicode]

  1. La norme C99 a ajouté le type _Bool, mais celui-ci est peu utilisé, l'usage des types entiers pour représenter les booléens en C étant très répandu (voir Booléens).