IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Le langage C


précédentsommairesuivant

II. Opérateurs et expressions

II-A. Généralités

Dans cette section nous étudions les opérateurs et les expressions du langage C. Les expressions simples sont les constantes littérales (0, 'A', 0.31416e1, etc.), les constantes symboliques (lundi, false, etc.) et les noms de variables (x, nombre, etc.). Les opérateurs servent à construire des expressions complexes, comme 2 * x + 3 ou sin(0.31416e1). Les propriétés d'une expression complexe découlent essentiellement de la nature de l'opérateur qui chapeaute l'expression.

On peut distinguer les expressions pures des expressions avec effet de bord 11. Dans tous les cas une expression représente une valeur. Une expression pure ne fait que cela : l'état du système est le même avant et après son évaluation. Au contraire, une expression à effet de bord modifie le contenu d'une ou plusieurs variables. Par exemple, l'expression y + 1 est pure, tandis que l'affectation x = y + 1 (qui en C est une expression) est à effet de bord, car elle modifie la valeur de la variable x. Comme nous le verrons, en C un grand nombre d'expressions ont de tels effets.

Remarque 1. Les opérateurs dont il sera question ici peuvent aussi apparaitre dans les déclarations, pour la construction des types dérivés (tableaux, pointeurs et fonctions) comme dans la déclaration complexe :

 
Sélectionnez
char (*t[20])();

La signification des expressions ainsi écrites est alors très différente de celle des expressions figurant dans la partie exécutable des programmes, mais on peut signaler d'ores et déjà que toutes ces constructions obéissent aux mêmes règles de syntaxe, notamment pour ce qui concerne la priorité des opérateurs et l'usage des parenthèses. La question des déclarations complexes sera vue à la section 5.4.

Remarque 2. C'est une originalité de C que de considérer les « désignateurs » complexes (les objets pointés, les éléments des tableaux, les champs des structures, etc.) comme des expressions construites avec des opérateurs et obéissant à la loi commune, notamment pour ce qui est des priorités. On se souvient qu'en Pascal les signes qui permettent d'écrire de tels désignateurs (c'est-à-dire les sélecteurs [], ^ et .) n'ont pas statut d'opérateur. Il ne viendrait pas à l'idée d'un programmeur Pascal d'écrire 2 + (t[i]) afin de lever une quelconque ambigüité sur la priorité de + par rapport à celle de [], alors qu'en C de telles expressions sont habituelles. Bien sûr, dans 2 + (t[i]) les parenthèses sont superflues, car la priorité de l'opérateur [] est supérieure à celle de +, mais ce n'est pas le cas dans (2 + t)[i], qui est une expression également légitime.

11Effet de bord est un barbarisme ayant pour origine l'expression anglaise side effect qu'on peut traduire par effet secondaire souvent un peu caché, parfois dangereux.

II-A-1. Lvalue et rvalue

Toute expression possède au moins deux attributs : un type et une valeur. Par exemple, si i est une variable entière valant 10, l'expression 2 * i + 3 possède le type entier et la valeur 23. Dans la partie exécutable des programmes, on trouve deux sortes d'expressions :

  • Lvalue (expressions signifiant « le contenu de… »). Certaines expressions sont représentées par une formule qui détermine un emplacement dans la mémoire ; la valeur de l'expression est alors définie comme le contenu de cet emplacement. C'est le cas des noms des variables, des composantes des enregistrements et des tableaux, etc. Une lvalue possède trois attributs : une adresse, un type et une valeur. Exemples : x,
 
Sélectionnez
table[i], fiche.numero.
  • Rvalue (expressions signifiant « la valeur de… »). D'autres expressions ne sont pas associées à un emplacement de la mémoire : elles ont un type et une valeur, mais pas d'adresse. C'est le cas des constantes et des expressions définies comme le résultat d'une opération arithmétique. Une rvalue ne possède que deux attributs : type, valeur. Exemples : 12, 2 * i + 3.

Toute lvalue peut être vue comme une rvalue ; il suffit d'ignorer le contenant (adresse) pour ne voir que le contenu (type, valeur). La raison principale de la distinction entre lvalue et rvalue est la suivante : seule une lvalue peut figurer à gauche du signe = dans une affectation ; n'importe quelle rvalue peut apparaitre à droite. Cela justifie les appellations lvalue (« left value ») et rvalue (« right value »).

Les sections suivantes préciseront, pour chaque sorte d'expression complexe, s'il s'agit ou non d'une lvalue.

Pour les expressions simples, c'est-à-dire les constantes littérales et les identificateurs, la situation est la suivante :

  • sont des lvalue :

    • les noms des variables simples (nombres et pointeurs),
    • les noms des variables d'un type struct ou union
  • ne sont pas des lvalue :

    • les constantes,
    • les noms des variables de type tableau,
    • les noms des fonctions.

II-A-2. Priorité des opérateurs

C comporte de très nombreux opérateurs. La plupart des caractères spéciaux désignent des opérations et subissent les mêmes règles syntaxiques, notamment pour ce qui concerne le jeu des priorités et la possibilité de parenthésage. La table 2 montre l'ensemble de tous les opérateurs, classés par ordre de priorité.

Image non disponible

Remarque. Dans certains cas un même signe, comme -, désigne deux opérateurs, l'un unaire (à un argument), l'autre binaire (à deux arguments). Dans le tableau 2, les suffixes un et bin précisent de quel opérateur il s'agit.

Le signe -> indique l'associativité « de gauche à droite » ; par exemple, l'expression x - y - z signifie (x - y) - z. Le signe <- indique l'associativité de « droite à gauche » ; par exemple, l'expression x = y = z signifie x = (y = z).

Notez que le sens de l'associativité des opérateurs précise la signification d'une expression, mais en aucun cas la chronologie de l'évaluation des sous-expressions.

II-B. Présentation détaillée des opérateurs

II-B-1. Appel de fonction ()

Opération : application d'une fonction à une liste de valeurs. Format :

 
Sélectionnez
expn ( exp1 , ... expn )

exp1 … expn sont appelés les arguments effectifs de l'appel de la fonction. exp0 doit être de type fonction rendant une valeur de type T ou bien 12 adresse d'une fonction rendant une valeur de type T. Alors exp0 ( exp1 … expn ) possède le type T. La valeur de cette expression découle de la définition de la fonction (à l'intérieur de la fonction, cette valeur est précisée par une ou plusieurs instructions « return exp ; »).

L'expression exp0 ( exp1 … expn ) n'est pas une lvalue.

Exemple :

 
Sélectionnez
y = carre(2 * x) + 3;

sachant que, carre est le nom d'une fonction rendant un double, l'expression carre(2 * x) a le type double. Un coup d'œil au corps de la fonction (cf. section I.F.2) pourrait nous apprendre que la valeur de cette expression n'est autre que le carré de son argument, soit ici la valeur 4x2.

Les contraintes supportées par les arguments effectifs ne sont pas les mêmes dans le C ANSI et dans le C original (ceci est certainement la plus grande différence entre les deux versions du langage) :

A. En C ANSI, si la fonction a été définie ou déclarée avec prototype (cf. section IV.A) :

  • le nombre des arguments effectifs doit correspondre à celui des arguments formels 13 ;
  • chaque argument effectif doit être compatible, au sens de l'affectation, avec l'argument formel correspondant ;
  • la valeur de chaque argument effectif subit éventuellement les mêmes conversions qu'elle subirait dans l'affectation :
 
Sélectionnez
argument formel = argument effectif

B. En C original, ou en C ANSI si la fonction n'a pas été définie ou déclarée avec prototype :

  • aucune contrainte (de nombre ou de type) n'est imposée aux arguments effectifs ;
  • tout argument effectif de type char ou short est converti en int ;
  • tout argument effectif de type float est converti en double ;
  • les autres arguments effectifs ne subissent aucune conversion.

Par exemple, avec la fonction carre définie à la section 1.6.2, l'appel

 
Sélectionnez
    y = carre(2);

est erroné en C original (la fonction reçoit la représentation interne de la valeur entière 2 en « croyant » que c'est la représentation interne d'un double), mais il est correct en C ANSI (l'entier 2 est converti en double au moment de l'appel).

Toutes ces questions sont reprises plus en détail à la section 4.

Remarque 1. Bien noter que les parenthèses doivent apparaitre, même lorsque la liste des arguments est vide. Leur absence ne provoque pas d'erreur, mais change complètement le sens de l'expression. Cela constitue un piège assez vicieux tendu aux programmeurs dont la langue maternelle est Pascal.

Remarque 2. Il faut savoir que lorsqu'une fonction a été déclarée comme ayant des arguments en nombre variable (cf. section IV.C.4) les arguments correspondant à la partie variable sont traités comme les arguments des fonctions sans prototype, c'est-à-dire selon les règles de la section B ci-dessus.

Cette remarque est loin d'être marginale, car elle concerne, excusez du peu, les deux fonctions les plus utilisées : printf et scanf. Ainsi, quand le compilateur rencontre l'instruction

 
Sélectionnez
printf(expr0, expr1, ... exprk);

il vérifie que expr0 est bien de type char *, mais les autres paramètres effectifs expr1… exprk sont traités selon les règles de la section B.

12 Cette double possibilité est commentée à la section 6.3.4.

13 Il existe néanmoins un moyen pour écrire des fonctions avec un nombre variable d'arguments (cf. section IV.C.4)

II-B-2. Indexation []

Définition restreinte (élément d'un tableau). Opération : accès au ième élément d'un tableau. Format :

 
Sélectionnez
    exp0 [ exp1 ]

exp0 doit être de type « tableau d'objets de type T », exp1 doit être d'un type entier. Alors exp0[exp1] est de type T ; cette expression désigne l'élément du tableau dont l'indice est donné par la valeur de exp1. Deux détails auxquels on tient beaucoup en C :

  • le premier élément d'un tableau a toujours l'indice 0 ;
  • il n'est jamais vérifié que la valeur de l'indice dans une référence à un tableau appartient à l'intervalle 0 … N-1 déterminé par le nombre N d'éléments alloués par la déclaration du tableau. Autrement dit, il n'y a jamais de « test de débordement ».

Exemple. L'expression t[0] désigne le premier élément du tableau t, t[1] le second, etc.

En C, les tableaux sont toujours à un seul indice ; mais leurs composantes peuvent être à leur tour des tableaux. Par exemple, un élément d'une matrice rectangulaire sera noté :

 
Sélectionnez
m[i][j]

Une telle expression suppose que m[i] est de type « tableau d'objets de type T » et donc que m est de type « tableau de tableaux d'objets de type T ». C'est le cas, par exemple, si m a été déclarée par une expression de la forme (NL et NC sont des constantes) :

 
Sélectionnez
double m[NL][NC];

Définition complète (indexation au sens large). Opération : accès à un objet dont l'adresse est donnée par une adresse de base et un déplacement. Format :

 
Sélectionnez
exp0 [ exp1 ]

exp0 doit être de type « adresse d'un objet de type T », exp1 doit être de type entier. Alors exp0[exp1] désigne l'objet de type T ayant pour adresse (voir la figure 2) :

 
Sélectionnez
valeur(exp0) + valeur(exp1) * taille(T)
Fig. 2 - L'indexation
Fig. 2 - L'indexation

Il est clair que, si exp0 est de type tableau, les deux définitions de l'indexation données coïncident. Exemple : si t est un tableau d'entiers et p un « pointeur vers entier » auquel on a affecté l'adresse de t[0], alors les expressions suivantes désignent le même objet :

 
Sélectionnez
t[i] p[i] *(p + i) *(t + i)

Dans un cas comme dans l'autre, l'expression exp0[exp1] est une lvalue, sauf si elle est de type tableau.

II-B-3. Sélection .

Opération : accès à un champ d'une structure ou d'une union. Format :

 
Sélectionnez
exp . identif

exp doit posséder un type struct ou union, et identif doit être le nom d'un des champs de la structure ou de l'union en question. En outre, exp doit être une lvalue. Alors, exp.identif désigne le champ identif de l'objet désigne par exp.

Cette expression est une lvalue, sauf si elle est de type tableau.

Exemple. Avec la déclaration

 
Sélectionnez
struct personne {
    long int num;
    struct {
        char rue[32];
        char *ville;
    } adresse;
} fiche;

les expressions

 
Sélectionnez
fiche.num     fiche.adresse     fiche.adresse.rue     etc.

désignent les divers champs de la variable fiche. Seules les deux premières sont des lvalue.

II-B-4. Sélection dans un objet pointé ->

Opération : accès au champ d'une structure ou d'une union pointée. Format :

 
Sélectionnez
    exp->identif

exp doit posséder le type « adresse d'une structure ou d'une union » et identif doit être le nom d'un des champs de la structure ou de l'union en question. Dans ces conditions, exp->identif désigne le champ identif de la structure ou de l'union dont l'adresse est indiquée par la valeur de exp. Cette expression est une lvalue, sauf si elle est de type tableau.

Ainsi, exp->identif est strictement synonyme de (*exp).identif (remarquez que les parenthèses sont indispensables).

Par exemple, avec la déclaration :

 
Sélectionnez
struct noeud {
    int info;
    struct noeud *fils, *frere;
} *ptr;

les expressions suivantes sont correctes :

 
Sélectionnez
ptr->info ptr->fils->frere ptr->fils->frere->frere

II-B-5. Négation !

Opération : négation logique. Format :

 
Sélectionnez
!exp

Aucune contrainte. Cette expression désigne une valeur de l'ensemble {0,1}, définie par :

[ALT-PASTOUCHE]

Cette expression n'est pas une lvalue.

Remarque. Bien que cela ne soit pas exigé par le langage C, on évitera de « nier » (et plus généralement de comparer à zéro) des expressions d'un type flottant (float, double). À cause de l'imprécision inhérente à la plupart des calculs avec de tels nombres, l'égalité à zéro d'un flottant n'est souvent que le fruit du hasard.

II-B-6. Complément à 1 ~

Opération : négation bit à bit. Format :

 
Sélectionnez
        ~exp

exp doit être d'un type entier. Cette expression désigne l'objet de même type que exp qui a pour codage interne la configuration de bits obtenue en inversant chaque bit du codage interne de la valeur de exp : 1 devient 0, 0 devient 1.

Cette expression n'est pas une lvalue.

Remarque. Le complément à un n'est pas une opération abstraite (cf. section II.C.3). La portabilité d'un programme ou cet opérateur figure n'est donc pas assurée.

II-B-7. Les célèbres ++ et --

Il existe deux opérateurs unaires ++ différents : l'un est postfixé (écrit derrière l'opérande), l'autre préfixé (écrit devant).

1. Opération : postincrémentation. Format :

 
Sélectionnez
exp++

exp doit être de type numérique (entier ou flottant) ou pointeur. Ce doit être une lvalue. Cette expression est caractérisée par :

  • un type : celui de exp ;
  • une valeur : la même que exp avant l'évaluation de exp++ ;
  • un effet de bord : le même que celui de l'affectation exp = exp + 1.

2. Opération : préincrémentation. Format :

 
Sélectionnez
++exp

exp doit être de type numérique (entier ou flottant) ou pointeur. Ce doit être une lvalue. Cette expression est caractérisée par :

  • un type : celui de exp ;
  • une valeur : la même que exp après l'évaluation de exp++ ;
  • un effet de bord : le même que celui de l'affectation exp = exp + 1.

Les expressions exp++ et ++exp ne sont pas des lvalue.

Exemple.

L'affectation équivaut à

 
Sélectionnez
    y = x++ ;     y = x ; x = x + 1 ;
    y = ++x ;     x = x + 1 ; y = x ;

L'opérateur ++ bénéficie de l'arithmétique des adresses au même titre que +. Ainsi, si exp est de type « pointeur vers un objet de type T », la quantité effectivement ajoutée à exp par l'expression exp++ dépend de la taille de T.

Il existe de même deux opérateurs unaires -- donnant lieu à des expressions exp-- et --exp. L'explication est la même, en remplaçant +1 par ¡1.

Application : réalisation d'une pile. Il est très agréable de constater que les opérateurs ++ et -- et la manière d'indexer les tableaux en C se combinent harmonieusement et permettent par exemple la réalisation simple et efficace de piles par des tableaux, selon le schéma suivant :

  • déclaration et initialisation d'une pile (OBJET est un type prédéfini, dépendant du problème particulier considéré ; MAXPILE est une constante représentant un majorant du nombre d'éléments dans la pile) :
 
Sélectionnez
    OBJET espace[MAXPILE];
    int nombreElements = 0;
  • opération « empiler la valeur de x » :
 
Sélectionnez
    if (nombreElements >= MAXPILE)
        erreur("tentative d'empilement dans une pile pleine");
    espace[nombreElements++] = x;
  • opération « dépiler une valeur et la ranger dans x » :
 
Sélectionnez
    if (nombreElements <= 0)
        erreur("tentative de depilement d'une pile vide");
    x = espace[--nombreElements];

On notera que, si on procède comme indiqué ci-dessus, la variable nombreElements possède constamment la valeur que son nom suggère : le nombre d'éléments effectivement présents dans la pile.

II-B-8. Moins unaire -

Opération : changement de signe. Format :

 
Sélectionnez
                        -exp

exp doit être une expression numérique (entière ou réelle). Cette expression représente l'objet de même type que exp dont la valeur est l'opposée de celle de exp. Ce n'est pas une lvalue.

II-B-9. Indirection *

Opération : accès à un objet pointé. On dit aussi « déréference ». Format :

 
Sélectionnez
        *exp

exp doit être une expression de type « adresse d'un objet de type T ». *exp représente alors l'objet de type T ayant pour adresse la valeur de exp.

L'expression *exp est une lvalue.

Remarque 1. On prendra garde au fait qu'il existe un bon nombre d'opérateurs ayant une priorité supérieure à celle de *, ce qui oblige souvent à utiliser des parenthèses. Par exemple les expressions Pascal e"[i]" et e"[i]" doivent respectivement s'écrire, en C, (*e)[i] et *(e[i]), la deuxième pouvant aussi s'écrire *e[i].

Remarque 2. L'expression *p signifie accès à la mémoire dont p contient l'adresse. C'est une opération « sans filet ». Quel que soit le contenu de p, la machine pourra toujours le considérer comme une adresse et accéder à la mémoire correspondante. Il appartient au programmeur de prouver que cette mémoire a été effectivement allouée au programme en question. Si c'est le cas, on dit que la valeur de p est une adresse valide ; dans le cas contraire, les pires erreurs sont à craindre à plus ou moins court terme.

II-B-10. Obtention de l'adresse &

Opération : obtention de l'adresse d'un objet occupant un emplacement de la mémoire. Format :

 
Sélectionnez
&exp

exp doit être une expression d'un type quelconque T. Ce doit être une lvalue.

L'expression &exp a pour type « adresse d'un objet de type T » et pour valeur l'adresse de l'objet représenté par exp. L'expression &exp n'est pas une lvalue.

Ainsi, si i est une variable de type int et p une variable de type « pointeur vers un int », alors à la suite de l'instruction

 
Sélectionnez
p = &i;

i et *p désignent le même objet.

Exemple 1. Une utilisation fréquente de cet opérateur est l'obtention de l'adresse d'une variable en vue de la passer à une fonction pour qu'elle modifie la variable :

 
Sélectionnez
scanf("%d%lf%s", &i, &t[j], &p->nom);

Exemple 2. Une autre utilisation élégante de cet opérateur est la création de composantes « fixes » dans les structures chainées. Le programme suivant déclare une liste chainée circulaire représentée par le pointeur entree et réduite pour commencer à un unique maillon qui est son propre successeur (voir figure 3) ; d'autres maillons seront créés dynamiquement durant l'exécution :

Fig. 3 - Maillon fixe en tête d'une liste chainée
Fig. 3 - Maillon fixe en tête d'une liste chainée
 
Sélectionnez
struct en_tete {
    long taille;
    struct en_tete *suivant;
} en_tete_fixe = { 0, &en_tete_fixe };
 
struct en_tete *entree = &en_tete_fixe;

Remarque. Une expression réduite à un nom de tableau, à un nom de fonction ou à une constante chaine de caractères est considérée comme une constante de type adresse ; l'opérateur & appliqué à de telles expressions est donc sans objet. Un tel emploi de & devrait être considéré comme erroné, mais beaucoup de compilateurs se contentent de l'ignorer.

II-B-11. Opérateur sizeof

Opération : calcul de la taille correspondant à un type. Première forme :

 
Sélectionnez
    sizeof ( descripteur-de-type )

Cette expression représente un nombre entier qui exprime la taille qu'occuperait en mémoire un objet possédant le type indiqué (les descripteurs de types sont expliqués à la section 5.4).

Deuxième forme :

 
Sélectionnez
    sizeof exp

exp est une expression quelconque (qui n'est pas évaluée par cette évaluation de sizeof). L'expression sizeof

exp représente la taille qu'occuperait en mémoire un objet possédant le même type que exp. Dans un cas comme dans l'autre sizeof… est une expression constante. En particulier, ce n'est pas une lvalue.

La taille des objets est exprimée en nombre d'octets. Plus exactement, l'unité choisie est telle que la valeur de sizeof(char) soit 1 (on peut aussi voir cela comme une définition du type char).

Remarque 1. Dans le C original, le type de l'expression sizeof… est int. Dans le C ANSI, ce type peut changer d'un système à un autre (int, long, unsigned, etc.). Pour cette raison il est défini, sous l'appellation size t, dans le fichier stddef.h. Il est recommandé de déclarer de type size t toute variable (resp. toute fonction) devant contenir (resp. devant renvoyer) des nombres qui représentent des tailles.

Remarque 2. Lorsque son opérande est un tableau (ou une expression de type tableau) la valeur rendue par sizeof est l'encombrement effectif du tableau. Par exemple, avec la déclaration

 
Sélectionnez
    char tampon[80];

l'expression sizeof tampon vaut 80, même si cela parait en contradiction avec le fait que tampon peut être vu comme de type « adresse d'un char ». Il en découle une propriété bien utile : quel que soit le type du tableau t, la formule

 
Sélectionnez
    sizeof t / sizeof t[0]

exprime toujours le nombre d'éléments de t.

II-B-12. Conversion de type (\cast" operator)

Opération : conversion du type d'une expression. Format :

 
Sélectionnez
                ( type2 ) exp

type2 représente un descripteur de type ; exp est une expression quelconque. L'expression ci-dessus désigne un élément de type type2 qui est le résultat de la conversion vers ce type de l'élément représenté par exp.

L'expression (type2)exp n'est pas une lvalue.

Les conversions légitimes (et utiles) sont 14 :

  • Entier vers un entier plus long (ex. char → int). Le codage de exp est étendu de telle manière que la valeur représentée soit inchangée.
  • Entier vers un entier plus court (ex. long → short). Si la valeur de exp est assez petite pour être représentable dans le type de destination, sa valeur est la même après conversion. Sinon, la valeur de exp est purement et simplement tronquée (une telle troncation, sans signification abstraite, est rarement utile).
  • Entier signé vers entier non signé, ou le contraire. C'est une conversion sans travail : le compilateur se borne à interpréter autrement la valeur de exp, sans effectuer aucune transformation de son codage interne.
  • Flottant vers entier : la partie fractionnaire de la valeur de exp est supprimée. Par exemple, le flottant 3.14 devient l'entier 3. Attention, on peut voir cette conversion comme une réalisation de la fonction mathématique partie entière, mais uniquement pour les nombres positifs : la conversion en entier de -3.14 donne -3, non -4.
  • Entier vers flottant. Sauf cas de débordement (le résultat est alors imprévisible), le flottant obtenu est celui qui approche le mieux l'entier initial. Par exemple, l'entier 123 devient le flottant 123.0.
  • Adresse d'un objet de type T1 vers adresse d'un objet de type T2. C'est une conversion sans travail : le compilateur donne une autre interprétation de la valeur de exp, sans effectuer aucune transformation de son codage interne.

Danger ! Une telle conversion est entièrement placée sous la responsabilité du programmeur, le compilateur l'accepte toujours.

  • Entier vers adresse d'un objet de type T. C'est encore une conversion sans travail : la valeur de exp est interprétée comme un pointeur, sans transformation de son codage interne.

Danger ! Une telle conversion est entièrement placée sous la responsabilité du programmeur. De plus, si la représentation interne de exp n'a pas la taille voulue pour un pointeur, le résultat est imprévisible.

Toutes les conversions ou l'un des types en présence, ou les deux, sont des types struct ou union sont interdites.

Note 1. Si type2 et le type de exp sont numériques, alors la conversion effectuée à l'occasion de l'évaluation de l'expression ( type2 ) exp est la même que celle qui est faite lors d'une affectation.

 
Sélectionnez
    x = expr;

ou x représente une variable de type type2.

Note 2. En toute rigueur, le fait que l'expression donnée par un opérateur de conversion de type ne soit pas une lvalue interdit des expressions qui auraient pourtant été pratiques, comme

 
Sélectionnez
    ((int) x)++; /* DANGER ! */

Cependant, certains compilateurs acceptent cette expression, la traitant comme

 
Sélectionnez
    x = (le type de x)((int) x + 1) ;

Exemple 1. Si i et j sont deux variables entières, l'expression i / j représente leur division entière (ou euclidienne, ou encore leur quotient par défaut). Voici deux manières de ranger dans x (une variable de type float) leur quotient décimal :

 
Sélectionnez
    x = (float) i / j; x = i / (float) j;

Et voici deux manières de se tromper (en n'obtenant que le résultat de la conversion vers le type float de leur division entière)

 
Sélectionnez
    x = i / j; /* ERREUR */ x = (float)(i / j); /* ERREUR */

Exemple 2. Une utilisation pointue et dangereuse, mais parfois nécessaire, de l'opérateur de conversion de type entre types pointeurs consiste à s'en servir pour « voir » un espace mémoire donné par son adresse comme possédant une certaine structure alors qu'en fait il n'a pas été ainsi déclaré :

  • déclaration d'une structure :
 
Sélectionnez
struct en_tete {
    long taille;
    struct en_tete *suivant;
};
  • déclaration d'un pointeur « générique » :
 
Sélectionnez
void *ptr;
  • imaginez que {pour des raisons non détaillées ici{ ptr possède à un endroit donné une valeur qu'il est légitime de considérer comme l'adresse d'un objet de type struct en tete (alors que ptr n'a pas ce type-là). Voici un exemple de manipulation cet objet :
 
Sélectionnez
    ((struct en_tete *) ptr)->taille = n;

Bien entendu, une telle conversion de type est faite sous la responsabilité du programmeur, seul capable de garantir qu'à tel moment de l'exécution du programme ptr pointe bien un objet de type struct en tete.

N.B. L'appel d'une fonction bien écrite ne requiert jamais un « cast ». L'opérateur de change- ment de type est parfois nécessaire, mais son utilisation diminue toujours la qualité du programme qui l'emploie, pour une raison facile à comprendre : cet opérateur fait taire le compilateur. En effet, si expr est d'un type pointeur et type2 est un autre type pointeur, l'expression (type2)expr est toujours acceptée par le compilateur sans le moindre avertissement. C'est donc une manière de cacher des erreurs sans les résoudre.

Exemple typique : si on a oublié de mettre en tête du programme la directive #include <stdlib.h>, l'utilisation de la fonction malloc de la bibliothèque standard soulève des critiques :

 
Sélectionnez
    ...
    MACHIN *p;
    ...
    p = malloc(sizeof(MACHIN));
    ...

À la compilation on a des avertissements. Par exemple (avec gcc) :

 
Sélectionnez
    monProgramme.c: In function `main'
    monProgramme.c:9:warning: assignment makes pointer from integer without a cast

On croit résoudre le problème en utilisant l'opérateur de changement de type

 
Sélectionnez
    ...
    p = (MACHIN *) malloc(sizeof(MACHIN)); /* CECI NE REGLE RIEN! */
    ...

et il est vrai que la compilation a lieu maintenant en silence, mais le problème n'est pas résolu, il est seulement caché. Sur un ordinateur où les entiers et les pointeurs ont la même taille, cela peut marcher, mais ailleurs la valeur rendue par malloc sera endommagée lors de son affectation à p. À ce sujet, voyez la remarque de la page 65.

Il suffit pourtant de bien lire l'avertissement affiché par le compilateur pour trouver la solution. L'affectation p = malloc(…) lui fait dire qu'on fabrique un pointeur (p) à partir d'un entier (le résultat de malloc) sans opérateur cast. Ce qui est anormal n'est pas l'absence de cast, mais le fait que le résultat de malloc soit tenu pour un entier 15, et il n'y a aucun moyen de rendre cette affectation juste aussi longtemps que le compilateur fera une telle hypothèse fausse. La solution est donc d'informer ce dernier à propos du type de malloc, en faisant précéder l'affectation litigieuse soit d'une déclaration de cette fonction

 
Sélectionnez
    void *malloc (size_t);

soit, c'est mieux, d'une directive ad hoc :

 
Sélectionnez
    #include <stdlib.h>
    ...
    p = malloc(sizeof(MACHIN));
    ...

14 Attention, il y a quelque chose de trompeur dans la phrase « conversion de exp ». N'oubliez pas que, contrairement à ce que suggère une certaine manière de parler, l'évaluation de l'expression (type)exp ne change ni le type ni la valeur de exp.

15 Rappelez-vous que toute fonction appelée et non déclarée est supposée de type int.

II-B-13. Opérateurs arithmétiques

Ce sont les opérations arithmétiques classiques : addition, soustraction, multiplication, division et modulo (reste de la division entière). Format :

Image non disponible

Aucune de ces expressions n'est une lvalue.

Avant l'évaluation de l'expression, les opérandes subissent les conversions « usuelles » (cf. section II.C.1). Le langage C supporte l'arithmétique des adresses (cf. section VI.B.1).

À propos de la division. En C on note par le même signe la division entière, qui prend deux opérandes entiers (short, int, long, etc.) et donne un résultat entier, et la division flottante dans laquelle le résultat est flottant (float, double). D'autre part, les règles qui commandent les types des opérandes et du résultat des expressions arithmétiques (cf. section II.C.1), et notamment la règle dite « du plus fort », ont la conséquence importante suivante : dans l'expression

 
Sélectionnez
    expr1 / expr2
  • si expr1 et expr2 sont toutes deux entier alors « / » est traduit par l'opération « division entière » 16, et le résultat est entier,
  • si au moins une des expressions expr1 ou expr2 n'est pas entière, alors l'opération faite est la division flottante des valeurs de expr1 et expr2 toutes deux converties dans le type double. Le résultat est une valeur double qui approche le rationnel expr1
 
Sélectionnez
expr2

du mieux que le permet la la précision du type double. Il résulte de cette règle un piège auquel il faut faire attention : 1/2 ne vaut pas 0:5, mais 0. De même, dans

 
Sélectionnez
    int somme, nombre;
    float moyenne;
    ...
    moyenne = somme / 100;

la valeur de moyenne n'est pas ce que ce nom suggère, car somme et 100 sont tous deux entiers et l'expression somme / 100 est entière, donc tronquée (et il ne faut pas croire que l'affectation ultérieure à la variable flottante moyenne pourra retrouver les décimales perdues). Dans ce cas particulier, la solution est simple :

 
Sélectionnez
    moyenne = somme / 100.0;

Même problème si le dénominateur avait été une variable entière, comme dans

 
Sélectionnez
    moyenne = somme / nombre;

ici la solution est un peu plus compliquée :

 
Sélectionnez
    moyenne = somme / (double) nombre;

16On prendra garde au fait que si les opérandes ne sont pas tous deux positifs la division entière du langage C ne coïncide pas avec le « quotient par défaut » (quotient de la « division euclidienne » des matheux). Ici, si q est le quotient de a par b alors |q| est le quotient de |a| par |b|. Par exemple, la valeur de (-17)/5 ou de 17/(-5) est -3 alors que le quotient par défaut de -17 par 5 est plutôt -4 (-17 = 5 * (-4) + 3).

II-B-14. Décalages << >>

Opération : décalages de bits. Format :

Image non disponible

exp1 et exp2 doivent être d'un type entier (char, short, long, int…). L'expression exp1 << exp2 (resp. exp1 >> exp2) représente l'objet de même type que exp1 dont la représentation interne est obtenue en décalant les bits de la représentation interne de exp1 vers la gauche 17 (resp. vers la droite) d'un nombre de positions égal à la valeur de exp2. Autrement dit,

 
Sélectionnez
    exp1 << exp2

est la même chose (si << 1 apparait exp2 fois) que

 
Sélectionnez
((exp1 << 1) << 1) ... << 1

Remarque analogue pour le décalage à droite >>.

Les bits sortants sont perdus. Les bits entrants sont :

  • dans le cas du décalage à gauche, des zéros ;
  • dans le cas du décalage à droite : si exp1 est d'un type non signé, des zéros ; si exp1 est d'un type signé, des copies du bit de signe 18.

Par exemple, si on suppose que la valeur de exp1 est codée sur huit bits, notés

 
Sélectionnez
b7b6b5b4b3b2b1b0

(chaque bi vaut 0 ou 1), alors

 
Sélectionnez
exp1 << 1     =     b6b5b4b3b2b1b00
exp1 >> 1     =     0b7b6b5b4b3b2b1 (cas non signé)
exp1 >> 1     =     b7b7b6b5b4b3b2b1 (cas signé)

Avant l'évaluation du résultat, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces expressions ne sont pas des lvalue.

Remarque. Les opérateurs de décalage de bits ne sont pas des opérations abstraites (cf. section II.C.3). La portabilité d'un programme où ils figurent n'est donc pas assurée.

17Par convention la droite d'un nombre codé en binaire est le coté des bits de poids faible (les unités) tandis que la gauche est celui des bits de poids forts (correspondant aux puissances 2k avec k grand).

18De cette manière le décalage à gauche correspond (sauf débordement) à la multiplication par 2exp2 , tandis que le décalage à droite correspond à la division entière par 2exp2 , aussi bien si exp1 est signée que si elle est non signée

II-B-15. Comparaisons == != < <= > >=

Il s'agit des comparaisons usuelles : égal, différent, inférieur, inférieur ou égal, supérieur, supérieur ou égal. Format :

Image non disponible

exp1 et exp2 doivent être d'un type simple (nombre ou pointeur). Cette expression représente l'un des éléments (de type int) 1 ou 0 selon que la relation indiquée est ou non vérifiée par les valeurs de exp1 et exp2. Avant la prise en compte de l'opérateur, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces expressions ne sont pas des lvalue.

Notez que, contrairement à ce qui se passe en Pascal, ces opérateurs ont une priorité supérieure à celle des connecteurs logiques, ce qui évite beaucoup de parenthèses disgracieuses. Ainsi, en C, l'expression

 
Sélectionnez
        0 <= x && x < 10

est correctement écrite.

À propos de « être vrai » et « être non nul ». Comme on a dit (cf. section I.D.1), le type booléen n'existe pas en C. N'importe quelle expression peut occuper la place d'une condition ; elle sera tenue pour fausse si elle est nulle, pour vraie dans tous les autres cas. Une conséquence de ce fait est la suivante : à la place de

 
Sélectionnez
    if (i != 0) etc.
    while (*ptchar != '\0') etc.
    for (p = liste; p != NULL; p = p->suiv) etc.

on peut écrire respectivement

 
Sélectionnez
    if (i) etc.
    while (*ptchar) etc.
    for (p = liste; p; p = p->suiv) etc.

On admet généralement qu'à cause de propriétés techniques des processeurs, ces expressions raccourcies représentent une certaine optimisation des programmes. C'est pourquoi les premiers programmeurs C, qui ne disposaient que de compilateurs simples, ont pris l'habitude de les utiliser largement. On ne peut plus aujourd'hui conseiller cette pratique. En effet, les compilateurs actuels sont suffisamment perfectionnés pour effectuer spontanément cette sorte d'optimisations et il n'y a plus aucune justification de l'emploi de ces comparaisons implicites qui rendent les programmes bien moins expressifs.

Attention. La relation d'égalité se note ==, non =. Voici une faute possible chez les nouveaux venus à C en provenance de Pascal qui, pour traduire le bout de Pascal if a = 0 then etc., écrivent

 
Sélectionnez
if (a = 0) etc.

Du point de vue syntaxique cette construction est correcte, mais elle est loin d'avoir l'effet escompté. En tant qu'expression, a = 0 vaut 0 (avec, comme effet de bord, la mise à zéro de a). Bien sûr, il fallait écrire

 
Sélectionnez
if (a == 0) etc.

II-B-16. Opérateurs de bits & | ^

Ces opérateurs désignent les opérations logiques et, ou et ou exclusif sur les bits des représentations internes des valeurs des opérandes. Format :

Image non disponible

exp1 et exp2 doivent être d'un type entier. Cette expression représente un objet de type entier dont le codage interne est construit bit par bit à partir des bits correspondants des valeurs de exp1 et exp2, selon la table suivante, dans laquelle (exp)i signifie « le ième bit de exp » :

Image non disponible

Avant l'évaluation du résultat, les opérandes subissent les conversions usuelles (cf. section II.C.1). Ces ex- pressions ne sont pas des lvalue.

Exemple. Il n'y a pas beaucoup d'exemples « de haut niveau » 19 d'utilisation de ces opérateurs. Le plus utile est sans doute la réalisation d'ensembles de nombres naturels inférieurs à une certaine constante pas trop grande, c'est-à-dire des sous-ensembles de l'intervalle d'entiers [ 0 … N-1 ], N valant 8, 16 ou 32.

Par exemple, les bibliothèques graphiques comportent souvent une fonction qui effectue l'affichage d'un texte affecté d'un ensemble d'attributs (gras, italique, souligné, capitalisé, etc.). Cela ressemble à ceci :

 
Sélectionnez
void afficher(char *texte, unsigned long attributs);

Pour faire en sorte que l'argument attributs puisse représenter un ensemble d'attributs quelconque, on associe les attributs à des entiers conventionnels :

 
Sélectionnez
#define GRAS 1             /* en binaire: 00...000001 */
#define ITALIQUE 2         /* en binaire: 00...000010 */
#define SOULIGNE 4         /* en binaire: 00...000100 */
#define CAPITALISE 8         /* en binaire: 00...001000 */
/* etc. */

Les valeurs utilisées pour représenter les attributs sont des puissances de 2 distinctes, c'est-à-dire des nombres qui, écrits en binaire, comportent un seul 1 placé différemment de l'un à l'autre. L'utilisateur d'une telle fonction emploie l'opérateur | pour composer, lors de l'appel, l'ensemble d'attributs qu'il souhaite :

 
Sélectionnez
afficher("Bonjour", GRAS | SOULIGNE); /* affichage en gras et italique */

(avec les constantes de notre exemple, la valeur de l'expression GRAS | SOULIGNE est un nombre qui, en binaire, s'écrit 00…000101).

De son côté, le concepteur de la fonction utilise l'opérateur & pour savoir si un attribut appartient ou non à l'ensemble donné :

 
Sélectionnez
    ...
    if ((attributs & GRAS) != 0)
        prendre les dispositions nécessaires pour afficher en gras
    else if ((attributs & ITALIQUE) != 0)
        prendre les dispositions nécessaires pour afficher en italique
    ...

19 En effet, ces opérateurs sont principalement destinés à l'écriture de logiciel de bas niveau, comme les pilotes de périphérique (les célèbres « drivers »), c'est-à-dire des programmes qui reçoivent les informations transmises par les composants matériels ou qui commandent le fonctionnement de ces derniers. Dans de telles applications on est souvent aux pries avec des nombres dont chaque bit a une signification propre, indépendante des autres. Ces programmes sortent du cadre de ce cours.

II-B-17. Connecteurs logiques && et ||

Opérations : conjonction et disjonction. Format :

Image non disponible

exp1 et exp2 sont deux expressions de n'importe quel type. Cette expression représente un élément de type int parmi { 0, 1 } défini de la manière suivante :

Pour évaluer exp1 && exp2 : exp1 est évaluée d'abord et

  • si la valeur de exp1 est nulle, exp2 n'est pas évaluée et exp1 && exp2 vaut 0 ;
  • sinon exp2 est évaluée et exp1 && exp2 vaut 0 ou 1 selon que la valeur de exp2 est nulle ou non.

Pour évaluer exp1 || exp2 : exp1 est évaluée d'abord et

  • si la valeur de exp1 est non nulle, exp2 n'est pas évaluée et exp1 || exp2 vaut 1 ;
  • sinon exp2 est évaluée et exp1 || exp2 vaut 0 ou 1 selon que la valeur de exp2 est nulle ou non.

Ces expressions ne sont pas des lvalue.

Applications. Ainsi, C garantit que le premier opérande sera évalué d'abord et que, s'il suffit à déterminer le résultat de la conjonction ou de la disjonction, alors le second opérande ne sera même pas évalué. Pour le programmeur, cela est une bonne nouvelle. En effet, il n'est pas rare qu'on écrive des conjonctions dont le premier opérande « protège » (dans l'esprit du programmeur) le second ; si cette protection ne fait pas partie de la sémantique de l'opérateur pour le langage utilisé, le programme résultant peut être faux. Considérons l'exemple suivant : un certain tableau table est formé de nombre chaines de caractères ; soit ch une variable chaine. L'opération « recherche de ch dans table » peut s'écrire :

 
Sélectionnez
    ...
    i = 0;
    while (i < nombre && strcmp(table[i], ch) != 0)
        i++;
        ...

Ce programme est juste parce que la condition strcmp(table[i], ch) != 0 n'est évaluée qu'après avoir vérifié que la condition i < nombre est vraie (un appel de strcmp(table[i], ch) avec un premier argument table[i] invalide peut avoir des conséquences tout à fait désastreuses).

Une autre conséquence de cette manière de concevoir && et || est stylistique. En C la conjonction et la disjonction s'évaluent dans l'esprit des instructions plus que dans celui des opérations. L'application pratique de cela est la possibilité d'écrire sous une forme fonctionnelle des algorithmes qui dans d'autres langages seraient séquentiels. Voici un exemple : le prédicat qui caractérise la présence d'un élément dans une liste chainée. Version (récursive) habituelle :

 
Sélectionnez
int present(INFO x, LISTE L) { /* l'information x est-elle dans la liste L ? */
    if (L == NULL)
        return 0;
    else if (L->info == x)
        return 1;
    else
        return present(x, L->suivant);
}

Version dans un style fonctionnel, permise par la sémantique des opérateurs && et || :

 
Sélectionnez
int existe(INFO x, LISTE L) { /* l'information x est-elle dans la liste L ? */
    return L != NULL && (x == L->info || existe(x, L->suivant));
}

II-B-18. Expression conditionnelle ? :

Opération : sorte de if…then…else… présenté sous forme d'expression, c'est-à-dire renvoyant une valeur.

Format :

 
Sélectionnez
    exp0 ? exp1 : exp2

exp0 est d'un type quelconque. exp1 et exp2 doivent être de types compatibles. Cette expression est évaluée de la manière suivante :

La condition exp0 est évaluée d'abord

  • si sa valeur est non nulle, exp1 est évaluée et définit la valeur de l'expression conditionnelle. Dans ce cas, exp2 n'est pas évaluée ;
  • sinon, exp2 est évaluée et définit la valeur de l'expression conditionnelle. Dans ce cas, exp1 n'est pas évaluée.

L'expression exp0 ?exp1:exp2 n'est pas une lvalue.

Exemple. L'opérateur conditionnel n'est pas forcément plus facile à lire que l'instruction conditionnelle, mais permet quelquefois de réels allégements du code. Imaginons un programme devant afficher un des textes non ou oui selon que la valeur d'une variable reponse est nulle ou non. Solutions classiques de ce microproblème :

 
Sélectionnez
    if (reponse)
        printf("la réponse est oui");
    else
        printf("la réponse est non");

ou bien, avec une variable auxiliaire :

 
Sélectionnez
    char *texte;
    ...
    if (reponse)
        texte = "oui";
    else
        texte = "non";
    printf("la réponse est %s", texte);

Avec l'opérateur conditionnel, c'est bien plus compact :

 
Sélectionnez
    printf("la réponse est %s", reponse ? "oui" : "non");

II-B-19. Affectation =

Opération : affectation, considérée comme une expression. Format :

 
Sélectionnez
        exp1 = exp2

exp1 doit être une lvalue. Soit type1 le type de exp1 ; l'affectation ci-dessus représente le même objet que

 
Sélectionnez
        ( type1 ) exp2

(la valeur de exp2 convertie dans le type de exp1), avec pour effet de bord le rangement de cette valeur dans l'emplacement de la mémoire déterminé par exp1.

L'expression exp1 = exp2 n'est pas une lvalue.

Contrairement à ce qui se passe dans d'autres langages, une affectation est donc considérée en C comme une expression : elle « fait » quelque chose, mais aussi elle « vaut » une certaine valeur et, à ce titre, elle peut figurer comme opérande dans une surexpression. On en déduit la possibilité des affectations multiples, comme dans l'expression :

 
Sélectionnez
    a = b = c = 0;

comprise comme a = (b = (c = 0)). Elle aura donc le même effet que les trois affectations a = 0 ; b = 0 ;

c = 0 ; Autre exemple, lecture et traitement d'une suite de caractères dont la fin est indiquée par un point :

 
Sélectionnez
    ...
    while ((c = getchar()) != '.')
    exploitation de c
    ...

Des contraintes pèsent sur les types des deux opérandes d'une affectation exp1 = exp2. Lorsqu'elles sont satisfaites, on dit que exp1 et exp2 sont compatibles pour l'affectation. Essentiellement :

  • deux types numériques sont toujours compatibles pour l'affectation. La valeur de exp2 subit éventuellement une conversion avant d'être rangée dans exp1. La manière de faire cette conversion est la même que dans le cas de l'opérateur de conversion (cf. section II.B.12) ;
  • si exp1 et exp2 sont de types adresses distincts, certains compilateurs (dont ceux conformes à la norme ANSI) les considéreront comme incompatibles tandis que d'autres compilateurs se limiteront à donner un message d'avertissement lors de l'affectation de exp2 à exp1 ;
  • dans les autres cas, exp1 et exp2 sont compatibles pour l'affectation si et seulement si elles sont de même type.

D'autre part, de la signification du nom d'une variable de type tableau (cf. V.A.1) et de celle d'une variable de type structure (cf. V.B.1) on déduit que :

  • on ne peut affecter un tableau à un autre, même s'ils sont définis de manière rigoureusement identique (un nom de tableau n'est pas une lvalue) ;
  • on peut affecter le contenu d'une variable de type structure ou union à une autre, à la condition qu'elles aient été explicitement déclarées comme ayant exactement le même type.

II-B-20. Autres opérateurs d'affectation += *= etc.

Opération binaire vue comme une modification du premier opérande. Format :

Image non disponible

exp1 doit être une lvalue. Cela fonctionne de la manière suivante : si • représente l'un des opérateurs + - * / % >> << & ^ |, alors

 
Sélectionnez
    exp1 • = exp2

peut être vue comme ayant la même valeur et le même effet que

 
Sélectionnez
    exp1 = exp1 • exp2

mais avec une seule évaluation de exp1. L'expression résultante n'est pas une lvalue.

Exemple. écrivons la version itérative usuelle de la fonction qui calcule xn avec x flottant et n entier non négatif :

 
Sélectionnez
double puissance(double x, int n) {
    double p = 1;
    while (n != 0) {
        if (n % 2 != 0) /* n est-il impair ? */
            p *= x;
        x *= x;
        n /= 2;
    }
    return p;
}

Remarque. En examinant ce programme, on peut faire les mêmes commentaires qu'à l'occasion de plusieurs autres éléments du langage C :

  • l'emploi de ces opérateurs constitue une certaine optimisation du programme. En langage machine, la suite d'instructions qui traduit textuellement a += c est plus courte que celle qui correspond à a = b + c. Or, un compilateur rustique traitera a = a + c comme un cas particulier de a = b + c, sans voir l'équivalence avec la première forme ;
  • hélas, l'emploi de ces opérateurs rend les programmes plus denses et moins faciles à lire, ce qui favorise l'apparition d'erreurs de programmation :
  • de nos jours les compilateurs de C sont devenus assez perfectionnés pour déceler automatiquement la possibilité de telles optimisations. Par conséquent, l'argument de l'efficacité ne justifie plus qu'on obscurcisse un programme par l'emploi de tels opérateurs.

Il faut savoir cependant qu'il existe des situations ou l'emploi de ces opérateurs n'est pas qu'une question d'efficacité. En effet, si exp1 est une expression sans effet de bord, alors les expressions exp1 • = exp2 et exp1 = exp1 • exp2 sont réellement équivalentes. Mais ce n'est plus le cas si exp1 a un effet de bord. Il est clair, par exemple, que les deux expressions suivantes ne sont pas équivalentes (la première est tout simplement erronée) 20 :

 
Sélectionnez
nombre[rand() % 100] = nombre[rand() % 100] + 1;     /* ERRONE ! */
 
nombre[rand() % 100] += 1;                             /* CORRECT */

II-B-21. L'opérateur virgule ,

Opération : évaluation en séquence. Format :

 
Sélectionnez
    exp1 , exp2

exp1 et exp2 sont quelconques. L'évaluation de exp1 , exp2 consiste en l'évaluation de exp1 suivie de l'évaluation de exp2. L'expression exp1 , exp2 possède le type et la valeur de exp2 ; le résultat de l'évaluation de exp1 est « oublié », mais non son éventuel effet de bord (cet opérateur n'est utile que si exp1 a un effet de bord). L'expression exp1 , exp2 n'est pas une lvalue.

Exemple. Un cas fréquent d'utilisation de cet opérateur concerne la boucle for (cf. section III.B.6), dont la syntaxe requiert exactement trois expressions à trois endroits bien précis. Parfois, certaines de ces expressions doivent être doublées :

 
Sélectionnez
...
for (pr = NULL, p = liste; p != NULL; pr = p, p = p->suivant)
    if (p->valeur == x)
        break;
...

Remarque syntaxique. Dans des contextes ou des virgules apparaissent normalement, par exemple lors d'un appel de fonction, des parenthèses sont requises afin de forcer le compilateur à reconnaitre l'opérateur virgule. Par exemple, l'expression

 
Sélectionnez
    uneFonction(exp1, (exp2, exp3), exp4);

représente un appel de uneFonction avec trois arguments effectifs : les valeurs de exp1, exp3 et exp4. Au passage, l'expression exp2 aura été évaluée. Il est certain que exp2 aura été évaluée avant exp3, mais les spécifications du langage ne permettent pas de placer les évaluations de exp1 et exp4 par rapport à celles de exp2 et exp3.

20 La fonction rand() renvoie un entier aléatoire distinct chaque fois qu'elle est appelée.

II-C. Autres remarques

II-C-1. Les conversions usuelles

Les règles suivantes s'appliquent aux expressions construites à l'aide d'un des opérateurs *, /, %, +, -, <, <=, >, >=, ==, !=, &, ^, |, && et ||. Mutatis mutandis, elles s'appliquent aussi à celles construites avec les opérateurs *=, /=, %=, +=, -=, <<=, >>=, &=, ^= et |=.

Dans ces expressions, les opérandes subissent certaines conversions avant que l'expression ne soit évaluée. En C ANSI ces conversions, dans l'ordre « logique » ou elles sont faites, sont les suivantes :

  • Si un des opérandes est de type long double, convertir l'autre dans le type long double ; le type de l'expression sera long double.
  • Sinon, si un des opérandes est de type double, convertir l'autre dans le type double ; le type de l'expression sera double.
  • Sinon, si un des opérandes est de type float, convertir l'autre dans le type float ; le type de l'expression sera float 21.
  • Effectuer la promotion entière : convertir les char, les short, les énumérations et les champs de bits en des int. Si l'une des valeurs ne peut pas être représentée dans le type int, les convertir toutes dans le type unsigned int.
  • Ensuite, si un des opérandes est unsigned long, convertir l'autre en unsigned long ; le type de l'expression sera unsigned long.
  • Sinon, si un des opérandes est long et l'autre unsigned int 22 :

    • si un long peut représenter toutes les valeurs unsigned int, alors convertir l'opérande de type unsigned int en long. Le type de l'expression sera long ;
    • sinon, convertir les deux opérandes en unsigned long. Le type de l'expression sera unsigned long.
  • Sinon, si un des opérandes est de type long, convertir l'autre en long ; le type de l'expression sera long.
  • Sinon, si un des opérandes est de type unsigned int, convertir l'autre en unsigned int ; le type de l'ex- pression sera unsigned int.
  • Sinon, et si l'expression est correcte, c'est que les deux opérandes sont de type int ; le type de l'expression sera int.

21 Cette règle est apparue avec le C ANSI : le compilateur accepte de faire des calculs sur des flottant en simple précision. Dans le C original, elle s'énonce plus simplement : « sinon, si l'un des opérandes est de type float, convertir les deux opérandes dans le type double ; le type de l'expression sera double ».

22 Cette règle compliquée est apparue avec le C ANSI. En C original, le type unsigned « tire vers lui » les autres types.

II-C-2. L'ordre d'évaluation des expressions

Les seules expressions pour lesquelles l'ordre (chronologique) d'évaluation des opérandes est spécifié sont les suivantes :

  • « exp1 && exp2 » et « exp1 || exp2 » : exp1 est évaluée d'abord ; exp2 n'est évaluée que si la valeur de exp1 ne permet pas de conclure ;
  • « exp0 ? exp1 : exp2 » exp0 est évaluée d'abord. Une seule des expressions exp1 ou exp2 est évaluée ensuite ;
  • « exp0 , exp0 » : exp1 est évaluée d'abord, exp2 est évaluée ensuite.

Dans tous les autres cas, C ne garantit pas l'ordre chronologique dans lequel les opérandes intervenant dans une expression ou les arguments effectifs d'un appel de fonction sont évalués ; il ne faut donc pas faire d'hypothèse à ce sujet. La question ne se pose que lorsque ces opérandes et arguments sont à leur tour des expressions complexes ; elle est importante dans la mesure ou C favorise la programmation avec des effets de bord. Par exemple, si i vaut 1, l'expression a[i] + b[i++] peut aussi bien additionner a[1] et b[1] que a[2] et b[1].

L'ordre d'évaluation des opérandes d'une affectation n'est pas fixé non plus. Pour évaluer exp1 = exp2, on peut évaluer d'abord l'adresse de exp1 et ensuite la valeur de exp2, ou bien faire l'inverse. Ainsi, le résultat de l'affectation a[i] = b[i++] est imprévisible.

II-C-3. Les opérations non abstraites

Beaucoup d'opérateurs étudiés dans cette section (opérateurs arithmétiques, comparaisons, logiques, etc.) représentent des opérations abstraites, c'est-à-dire possédant une définition formelle qui ne fait pas intervenir les particularités de l'implantation du langage. Bien sûr, les opérandes sont représentés dans la machine par des configurations de bits, mais seule leur interprétation comme des entités de niveau supérieur (nombres entiers, flottants…) est utile pour définir l'effet de l'opération en question ou les contraintes qu'elle subit.

À l'opposé, un petit nombre d'opérateurs, le complément à un (~), les décalages (<< et >>) et les opérations bit à bit (&, ^ et |), n'ont pas forcément de signification abstraite. Les transformations qu'ils effectuent sont définies au niveau des bits constituant le codage des opérandes, non au niveau des nombres que ces opérandes représentent. De telles opérations sont réservées aux programmes qui remplissent des fonctions de très bas niveau, c'est-à-dire qui sont aux points de contact entre la composante logicielle et la composante matérielle d'un système informatique.

L'aspect de cette question qui nous intéresse le plus ici est celui-ci : la portabilité des programmes contenant des opérations non abstraites n'est pas assurée. Ce défaut, qui n'est pas rédhibitoire dans l'écriture de fonctions de bas niveau (ces fonctions ne sont pas destinées à être portées), doit rendre le programmeur très précautionneux dès qu'il s'agit d'utiliser ces opérateurs dans des programmes de niveau supérieur, et le pousser à :

  • isoler les opérations non abstraites dans des fonctions de petite taille bien repérées ;
  • documenter soigneusement ces fonctions ;
  • constituer des jeux de tests validant chacune de ces fonctions.

précédentsommairesuivant

Copyright © 1988-2005 Henri Garreta. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.