Section courante

A propos

Section administrative du site

Types, opérateurs et expressions

Les variables et les constantes sont les objets de données de base manipulés dans un programme de langage de programmation C. La déclaration répertorie les variables à utiliser et indique leur type et peut-être leurs valeurs initiales. Les opérateurs précisent ce qu'ils doivent leur faire. Les expressions combinent des variables et des constantes pour produire de nouvelles valeurs. Le type d'un objet détermine l'ensemble de valeurs qu'il peut avoir et quelles opérations peuvent être effectuées sur celui-ci. La norme ANSI a apporté de nombreux petits changements et ajouts aux types et expressions de base. Il existe désormais des formes signées et non signées de tous les types d'entiers, ainsi que des notations pour les constantes non signées et les constantes de caractères hexadécimaux. Les opérations à virgule flottante peuvent être effectuées en simple précision; il existe également un type de données double long pour une précision étendue. Les constantes de chaîne de caractères peuvent être concaténées au moment de la compilation. Les énumérations font désormais partie du langage, officialisant une caractéristique de longue date. Les objets peuvent être déclarés const, ce qui les empêche d'être modifiés. Les règles pour les coercitions automatiques entre les types arithmétiques ont été augmentées pour gérer l'ensemble de types plus riche.

Noms de variables

Il est important de noter, qu'il existe des restrictions sur les noms des variables et des constantes symboliques. Les noms sont composés de lettres et de chiffres; Le premier caractère doit être une lettre. Le trait de soulignement «_» compte comme une lettre; il est parfois utile pour améliorer la lisibilité des noms de variables longs. Cependant, ne commencez pas les noms de variables par un trait de soulignement, car les routines de bibliothèque utilisent souvent ces noms. Les majuscules et les minuscules sont distinctes, donc x et X sont deux noms différents. La pratique traditionnelle du C consiste à utiliser des minuscules pour les noms de variables et toutes les majuscules pour les constantes symboliques. Au moins les 31 premiers caractères d'un nom interne sont significatifs. Pour les noms de fonction et les variables externes, le nombre peut être inférieur à 31, car les noms externes peuvent être utilisés par les assembleurs et les chargeurs sur lesquels le langage de programmation n'a aucun contrôle. Pour les noms externes, la norme garantit l'unicité uniquement pour 6 caractères et une seule casse. Les mots clefs comme if, else, int, float,..., sont réservés : vous ne pouvez pas les utiliser comme noms de variables. Ils doivent être en minuscules.

Il est sage de choisir des noms de variables étant liés à l'objectif de la variable et ne risquant pas d'être mélangés typographiquement. Nous avons tendance à utiliser des noms courts pour les variables locales, en particulier les indices de boucle, et des noms plus longs pour les variables externes.

Types de données et tailles

Il n'y a que des types de données de base en C :

Nom Description
char Ce type de données permet d'indiquer un seul octet, capable de contenir un caractère dans l'ensemble de caractères local.
int Ce type de données permet d'indiquer un entier, reflétant généralement la taille naturelle des entiers sur la machine hôte.
float Ce type de données permet d'indiquer une virgule flottante de simple précision.
double Ce type de données permet d'indiquer une virgule flottante de double précision.

De plus, il existe un nombre de qualificatifs pouvant être appliqués à ces types de base. Les types de données short et long s'appliquent aux entiers :

  1. short int sh;
  2. long int counter;

Le mot int peut être omis dans de telles déclarations, et l'est généralement. L'intention est que short et long devraient fournir des longueurs d'entiers différentes lorsque cela est possible; int sera normalement la taille naturelle d'une machine particulière. Le short est souvent 16 bits, long est 32 bits et le int est 16 ou 32 bits. Chaque compilateur est libre de choisir les tailles appropriées pour son propre matériel, sous réserve uniquement de la restriction que short et int sont au moins 16 bits, les long sont au moins 32 bits et short ne dépasse pas int, ce qui n'est pas plus long que long.

Le qualificatif signed ou unsigned peut être appliqué à char ou à n'importe quel entier. Les nombres unsigned sont toujours positifs ou nuls, et obéissent aux lois de l'arithmétique modulo 2n, où n est le nombre de bits dans le type. Ainsi, par exemple, si les char valent 8 bits, les variables unsigned char ont des valeurs entre 0 et 255, tandis que signed char ont des valeurs entre -128 et 127 (dans une machine à complément à deux). Le fait que les caractères simples soient signés ou non signés dépend de la machine, mais les caractères affichables sont toujours positifs.

Le type long double spécifie une virgule flottante à précision étendue. Comme pour les entiers, les tailles des objets à virgule flottante sont définies par la mise en oeuvre; float, double et long double pourraient représenter une, deux ou trois tailles distinctes.

Les entêtes standard <limits.h> et <float.h> contiennent des constantes symboliques pour toutes ces tailles, ainsi que d'autres propriétés de la machine et du compilateur.

Constantes

Une constante entière comme 1234 est un int. Une constante long est écrite avec une borne l ou L, comme dans 123456789L; un entier trop grand pour tenir dans un int sera également considéré comme un long. Les constantes non signées sont écrites un terminal u ou U, et le suffixe u1 ou UL indique unsigned long.

Les constantes à virgule flottante contiennent un point décimal (123.4) ou un exposant (1e-2) ou les deux; leur type de données est double, sauf s'il est suffixé. Les suffixes f ou F indiquent une constante flottante; l ou L indiquent un long double.

La valeur d'un entier peut être spécifiée en octal ou hexadécimal au lieu de décimal. Un 0 (zéro) en tête sur une constante entière signifie octal; un 0x ou 0X en tête signifie hexadécimal. Par exemple, le nombre décimal 31 peut être écrit sous la forme 037 en octal et 0x1f ou 0X1F en hexadécimal. Les constantes octales et hexadécimales peuvent également être suivies de L pour les rendre long et U pour les rendre unsigned: 0XFUL est une constante unsigned long avec la valeur 15 décimal.

Une constante de caractère est un entier, écrit comme un caractère entre guillemets simples, tel que «x». La valeur d'une constante de caractère est la valeur numérique du caractère dans l'ensemble de caractères de la machine. Par exemple, dans l'ensemble de caractères ASCII, la constante de caractère «0» a la valeur 48, n'étant pas liée au ensemble de caractères numériques, la constante de caractère «0» a la valeur 48, n'étant pas liée à la valeur numérique 0. Si nous écrivons «0» au lieu d'une valeur numérique comme 48 qui dépend de l'ensemble de caractères, le programme est indépendant de la valeur particulière et plus facile à lire. Les constantes de caractères participent aux opérations numériques comme tout autre entier, bien qu'elles soient le plus souvent utilisées dans les comparaisons avec d'autres caractères. Certains caractères peuvent être représentés dans des constantes de caractères et de chaînes de caractères par des séquences d'échappement comme \n (nouvelle ligne); ces séquences ressemblent à deux caractères, mais n'en représentent qu'un. De plus, un motif de bits arbitraire de la taille d'un octet peut être spécifié par :

'\ooo'

ooo est un trois chiffres octaux (0...7) ou par :

'\xhh'

hh est un ou plusieurs chiffres hexadécimaux (0...9, a...f, A...F). Alors on pourrait écrire :

  1. #define VERTICAL_TAB '\013'
  2. #define BELLC_CHARACTER '\007'

ou, en hexadécimal :

  1. #define VERTICAL_TAB '\xb'
  2. #define BELLC_CHARACTER '\x7'

L'ensemble complet des séquences d'échappement est :

Échappement Description
\a Cette séquence d'échappement permet d'indiquer un caractère d'alerte (cloche).
\b Cette séquence d'échappement permet d'indiquer un retour en arrière.
\f Cette séquence d'échappement permet d'indiquer un saut de page.
\n Cette séquence d'échappement permet d'indiquer une nouvelle ligne.
\r Cette séquence d'échappement permet d'indiquer une retour de chariot.
\t Cette séquence d'échappement permet d'indiquer une tabulation horizontale.
\v Cette séquence d'échappement permet d'indiquer une tabulation verticale.
\\ Cette séquence d'échappement permet d'indiquer une barre oblique inversé.
\? Cette séquence d'échappement permet d'indiquer un point d'interrogation.
\' Cette séquence d'échappement permet d'indiquer un simple guillemet.
\" Cette séquence d'échappement permet d'indiquer un double guillemet.
\ooo Cette séquence d'échappement permet d'indiquer un nombre octal.
\xhh Cette séquence d'échappement permet d'indiquer un nombre hexadécimal.

La constante de caractère '\0' représente le caractère de valeur zéro, le caractère nul. Le caractère '\0' est souvent écrit au lieu de 0 pour souligner la nature de caractère d'une expression, mais la valeur numérique est juste 0. Une expression constante est une expression n'impliquant que des constantes. De telles expressions peuvent être évaluées pendant la compilation plutôt que lors de l'exécution, et peuvent donc être utilisées à tout endroit où une constante peut apparaître, comme dans :

  1. #define MAXLIGNE 1000
  2. char ligne[MAXLIGNE+1];

ou

  1. #define BISSEXTILE
  2. int jours[31+28+BISSEXTILE+31+30+31+30+31+31+30+31+30+31];

Une constante de chaîne de caractères, ou chaîne de caractères littérale, est une séquence de zéro ou plusieurs caractères entourés de guillemets, comme dans

  1. "J'ai une chaîne de caractères"

ou

  1. "Gladir.com!"

ou

  1. "" /* la chaîne de caractères est vide */

Les guillemets ne font pas partie de la chaîne de caractères, mais servent uniquement à la délimiter. Les mêmes séquences d'échappement utilisées dans les constantes de caractères s'appliquent aux chaînes de caractères; «\"» représente le caractère guillemet double. Les constantes de chaîne de caractères peuvent être concaténées au moment de la compilation :

  1. "Bonjour," " le monde"

est équivalent de

  1. "Bonjour, le monde"

Il est utile pour diviser de longues chaînes de caractères sur plusieurs lignes source. Techniquement, une constante de chaîne de caractères est un tableau de caractères. La représentation interne d'une chaîne a un caractère nul '\0' à la fin, donc l'entreposage physique requis est un de plus que le nombre de caractères écrits entre les guillemets. Cette représentation signifie qu'il n'y a pas de limite à la longueur d'une chaîne de caractères, mais les programmes doivent analyser une chaîne de caractères complètement pour déterminer sa longueur. La fonction de bibliothèque standard strlen(s) renvoie la longueur de son paramètre de chaîne de caractères s, à l'exclusion du terminal '\0'. Voici une version du strlen :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int strlen(char string[]) {
  5.     int i = 0;
  6.     while(string[i] != '\0') ++i;
  7.     return i;
  8.  
  9. }
  10.  
  11. int main()
  12. {
  13.     printf("Longueur = %i\n",strlen("Gladir.com"));
  14.     return 0;
  15. }

La fonction strlen et d'autres fonctions de chaîne de caractères sont déclarées dans l'entête standard <string.h>. Veuillez à faire la distinction entre une constante de caractère et une chaîne de caractères contenant un seul caractère : 'x' n'est pas la même chose que "x". Le premier est un entier, utilisé pour produire la valeur numérique de la lettre x dans l'ensemble de caractères de la machine. Ce dernier est un tableau de caractères contenant un caractère (la lettre x) et un '\0'.

Il existe un autre type de constante, la constante d'énumération. Une énumération est une liste de valeurs entières constantes, comme dans :

  1. enum booleen { FAUX, VRAI };

Le premier nom est une énumération a la valeur 0, le suivant 1, et ainsi de suite, sauf si des valeurs explicites sont spécifiées. Si toutes les valeurs ne sont pas spécifiées, les valeurs non spécifiées continuent la progression à partir de la dernière valeur spécifiée, comme dans le deuxième de ces exemples :

  1. enum echappement { BELL = '\a', RETOUR_ARRIERE = '\b', TABULATION = '\t', NOUVELLELIGNE = '\n', TABULATION_VERTICAL = '\v', RETOUR = '\r' };
  2. enum mois { JANV = 1, FEVR = 2, MARS, AVRI, MAI, JUIN, JUIL, AOUT, SEPT, OCTO, NOVE, DECE }; /* FEVR est 2, MARS est 3,... */

Les noms sont des énumérations différentes et doivent être distinctes. Les valeurs n'ont pas besoin d'être distinctes dans la même énumération. Les énumérations offrent un moyen pratique d'associer des valeurs constantes à des noms, une alternative à la directive #define avec l'avantage que les valeurs peuvent être générées par vous. Bien que des variables de types enum puissent être déclarées, les compilateurs n'ont pas besoin de vérifier que ce que vous entreposez dans une telle variable et qu'une valeur soit valide pour l'énumération. Néanmoins, les variables d'énumération offrent une possibilité de vérification et sont donc souvent meilleures que #define. De plus, un débogueur peut être en mesure d'afficher les valeurs des variables d'énumération sous leur format symbolique.

Déclarations

Toutes les variables doivent être déclarées avant utilisation, bien que certaines déclarations puissent être implicitement contextuelles. Une déclaration spécifie un type et contient une liste d'une ou plusieurs variables de ce type, comme dans :

  1. int minuscule, majuscule, saut;
  2. char c, ligne[1000];

Les variables peuvent être réparties entre les déclarations de n'importe quelle manière; les listes ci-dessus pourraient également être écrites comme :

  1. int minuscule;
  2. int majuscule;
  3. int saut;
  4. char c;
  5. char ligne[1000];

Cette dernière forme prend plus d'espace, mais est pratique pour ajouter un commentaire à chaque déclaration ou pour des modifications ultérieures. Une variable peut également être initialisée dans sa déclaration. Si le nom est suivi d'un signe égal et d'une expression, l'expression sert d'initialisateur, comme dans :

  1. char echappement = '\\';
  2. int i = 0;
  3. int limite = MAXLIGNE+1;
  4. float eps = 1.0e-5;

Si la variable en question n'est pas automatique, l'initialisation est effectuée une seule fois, conceptuellement avant que le programme ne commence à s'exécuter, et l'initialiseur doit être une expression constante. Une variable automatique explicitement initialisée est initialisée chaque fois que la fonction ou le bloc dans lequel elle se trouve est entré; l'initialiseur peut être n'importe quelle expression. Les variables externes et statiques sont initialisées à zéro par défaut. Les variables automatiques pour lesquelles il n'y a pas d'initialisation explicite ont des valeurs non définies (c'est-à-dire des déchets).

Le qualificatif const peut être appliqué à la déclaration de n'importe quelle variable pour spécifier que sa valeur ne sera pas modifiée. Pour un tableau, le qualificatif const indique que les éléments ne seront pas modifiés.

  1. const double pi = 3.14159;
  2. const char message[] = "attention: ";

La déclaration const peut également être utilisée avec des paramètres de tableau, pour indiquer que la fonction ne change pas le tableau :

  1. int strlen(const char[]);    

Le résultat est défini par sa mise en oeuvre si une tentative est faite pour modifier un const.

Opérateurs arithmétiques

Les opérateurs arithmétiques binaires sont «+», «-», «*», «/» et l'opérateur de modulo «%». La division entière tronque toute partie fractionnaire. L'expression :

  1. x % y

produit le reste lorsque x est divisé par y, et est donc égal à zéro lorsque y divise exactement x. Par exemple, une année est une année bissextile si elle est divisible par 4 mais pas par 100, sauf que les années divisibles par 400 sont des années bissextiles. Donc :

  1. if((annee & 4 == 0 && annee % 100 != 0) || annee % 400 == 0) printf("%d est une année bissextile"); else printf("%d n'est pas une année bissextile");

L'opérateur «%» ne peut pas être appliqué à float ou double. Le sens de la troncature pour «/» et le signe du résultat pour «%» dépendent de la machine pour les opérandes négatifs, tout comme l'action entreprise en cas de dépassement de capacité ou de dépassement inférieur. Les opérateurs binaires «+» et «-» ont la même priorité, étant inférieure à la priorité de «*», «/» et «%», étant à son tour inférieure au binaire «+» et «-». Les opérateurs arithmétiques s'associent de gauche à droite.

Opérateurs relationnels et logiques

Les opérateurs relationnels sont :

C Mathématique
> Supérieur (>)
>= Supérieur ou égale (≥)
< Inférieur (<)
<= Inférieur ou égale (≤)

Ils ont tous la même priorité. Juste en dessous d'eux, en priorité se trouvent les opérateurs d'égalité :

C Mathématique
== Égalité (=)
!= Différent de (≠)

Les opérateurs relationnels ont une priorité plus faible que les opérateurs arithmétiques, donc une expression comme i < limites-1 est considérée comme i < (limites-1), comme on pouvait s'y attendre. Les opérateurs logiques && et || sont plus intéressants. Les expressions reliées par && ou || sont évaluées de gauche à droite, et l'évaluation s'arrête dès que la vérité ou le mensonge du résultat est connu. La plupart des programmes C reposent sur ces propriétés. Par exemple, voici une boucle de la fonction d'entrée décrivant cet aspect :

  1. for(i=0; i < limites-1 && (c=getchar())!= '\n' && c != EOF; ++i) s[i] = c; 

Avant de lire un nouveau caractère il faut vérifier qu'il y a de la place pour l'entreposer dans le tableau s, donc le test i < limites-1 doit être effectué en premier. De plus, si ce test échoue, nous ne devons pas continuer et lire un autre caractère. De même, il serait malheureux si c était testé contre EOF avant que getchar ne soit appelé; par conséquent, l'appel et l'affectation doivent avoir lieu avant que le caractère c ne soit testé. La priorité de && est plus élevée que celle de ||, et les deux sont inférieurs aux opérateurs relationnels et d'égalité, donc des expressions comme :

  1. i < limites-1 && (c=getchar()) != '\n' && c != EOF 

n'ont pas besoin de parenthèses supplémentaires. Mais comme la priorité de «!=» est supérieure à l'affectation, les parenthèses sont nécessaires dans :

  1. (c = getchar()) != '\n'

pour obtenir le résultat souhaité de l'affectation à c, puis comparaison avec '\n'. Par définition, la valeur numérique d'une expression relationnelle ou logique est 1 si la relation est vraie et 0 si la relation est fausse. L'opérateur de négation binaire «!» convertit un opérande non nul en 0 et un opérande nul en 1. Une utilisation courante de «!» est dans des constructions comme :

  1. if(!valide)

plutôt que

  1. if(valide == 0)    

Il est difficile de généraliser sur quelle forme est la meilleure. Les constructions comme !valide se lisent bien («si non valides»), mais les plus compliquées peuvent être difficiles à comprendre.

Conversions de type

Lorsqu'un opérateur a des opérandes de types différents, ils sont convertis en un type commun selon un petit nombre de règles. En général, les seules conversions automatiques sont celles qui convertissent un opérande "plus étroit" en un opérande "plus large" sans perdre d'informations, comme la conversion d'un entier en virgule flottante dans une expression comme f + i. Les expressions n'ayant pas de sens, comme l'utilisation d'un float comme indice, sont interdites. Les expressions susceptibles de perdre des informations, comme l'affectation d'un type entier plus long à un type plus court ou à virgule flottante à un entier, peuvent générer un avertissement, mais elles ne sont pas illégales.

Un char est juste un petit entier, donc les caractères peuvent être librement utilisés dans les expressions arithmétiques. Il permet une flexibilité considérable dans certains types de transformations de caractères. L'un est illustré par cette mise en oeuvre native de la fonction atoi, convertissant une chaîne de caractères de chiffres en son équivalent numérique.

  1. #include <stdio.h>
  2.  
  3. int atoi(char string[]) {
  4.     int i, n = 0;
  5.     for(i = 0; string[i] >= '0' && string[i] <= '9'; ++i) n = 10 * n + (string[i] - '0');
  6.     return n;
  7. }
  8.  
  9. int main()
  10. {
  11.     printf("Résultat = %i\n",atoi("987"));
  12.     return 0;
  13. }

L'expression string[i] - '0' donne la valeur numérique du caractère entreposé dans la string[i], car les valeurs de '0', '1', ..., forment une séquence croissante continue. Un autre exemple de conversion char en int est la fonction tolower, cartographiant un seul caractère en minuscules pour l'ensemble de caractères ASCII. Si le caractère n'est pas une lettre majuscule, tolower le renvoie inchangé.

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int tolower(int caractere) {
  5.     if(caractere >= 'A' && caractere <= 'Z') return caractere + 'a'-'A';
  6.     return caractere;
  7. }
  8.  
  9. int main()
  10. {
  11.     printf("Minuscule de G=%c\n",tolower('G'));
  12.     printf("Minuscule de l=%c\n",tolower('l'));
  13.     printf("Minuscule de a=%c\n",tolower('a'));
  14.     return 0;
  15. }

Il fonctionne pour ASCII parce que les lettres majuscules et minuscules correspondantes sont séparées par une distance fixe en tant que valeurs numériques et que chaque alphabet est contigu - il n'y a que des lettres entre A et Z. Cette dernière observation n'est pas vraie pour l'ensemble de caractères EBCDIC, cependant, ce code convertirait plus que de simples lettres en EBCDIC. L'entête standard <ctype.h>, définit une famille de fonctions fournissant des tests et des conversions indépendants de l'ensemble de caractères. Par exemple, la fonction tolower(caractere) renvoie la valeur minuscule de caractere si caractere est une majuscule, donc tolower est un remplacement portable pour la fonction inférieure illustrée ci-dessus. De même, le test :

  1. caractere >= '0' && caractere <= '9' 

peut être remplacé par

  1. isdigit(caractere)

Nous utiliserons désormais les fonctions <ctype.h>. Il y a un point subtil concernant la conversion des caractères en nombres entiers. Le langage de programmation ne spécifie pas si les variables de type char sont des quantités signed ou unsigned. Lorsqu'un char est converti en int, peut-il jamais produire un entier négatif ? La réponse varie d'une machine à l'autre, reflétant les différences d'architecture. Sur certaines machines, char dont le bit le plus à gauche est 1 sera converti en un entier négatif ("extension de signe"). Sur d'autres, un char est promu à un int en ajoutant des zéros à l'extrémité gauche, et est donc toujours positif. La définition de C garantit que tout caractère de l'ensemble de caractères d'affichage standard de la machine ne sera jamais négatif, donc ces caractères seront toujours des quantités positives dans les expressions. Mais les modèles de bits arbitraires entreposés dans les variables de caractères peuvent sembler négatifs sur certaines machines, mais positifs sur d'autres. Pour la portabilité, spécifiez signed ou unsigned si les données sans caractère doivent être entreposées dans des variables char. Les expressions relationnelles comme i > j et les expressions logiques reliées par && et || sont définis pour avoir la valeur 1 si vrai et 0 si faux. Ainsi l'affectation :

  1. d = caractere >= '0' && caractere <= '9' 

met d à 1 si caractere est un chiffre, et à 0 sinon. Cependant, des fonctions telles que isdigit peuvent renvoyer n'importe quelle valeur non nulle pour vrai. Dans la partie test de if, while, for,..., vrai signifie simplement non zéro, donc cela ne fait aucune différence.

Les conversions arithmétiques implicites fonctionnent comme il le devrait. En général, si un opérateur comme «+» ou «*» prenant deux opérandes (un opérateur binaire) a des opérandes de types différents, le type inférieur est promu au type supérieur avant que l'opération ne se poursuive. Le résultat est du type supérieur. S'il n'y a pas d'opérandes unsigned, cependant, l'ensemble de règles informel suivant suffira :

Notez que float dans une expression n'est pas automatiquement converti en double; il s'agit d'un changement par rapport à la définition originale. En général, les fonctions mathématiques comme celles de math.h utiliseront la double précision. La principale raison d'utiliser float est d'économiser de l'entreposage dans de grandes tableaux, ou, moins souvent, de gagner du temps sur des machines où l'arithmétique à double précision est particulièrement coûteuse. Les règles de conversion sont plus compliquées lorsque des opérandes unsigned sont impliqués. Le problème est que les comparaisons entre les valeurs signed et unsigned dépendent de la machine, car elles dépendent de la taille des différents types d'entiers. Par exemple, supposons que int est 16 bits et long est 32 bits. Ensuite, -1L < 1U, car 1U, étant un int, est promu à un long signed. Mais -1L > 1UL, car -1L est promu à unsigned long et semble donc être un grand nombre positif. Les conversions ont lieu à travers les affectations; la valeur du côté droit est convertie en type du côté gauche, étant le type du résultat. Un caractère est converti en entier, par extension de signe ou non, comme décrit ci-dessus. Les entiers longs sont convertis en nombres plus courts ou en caractères en supprimant les bits de poids fort en excès. Ainsi dans :

  1. int i;
  2. char caractere;
  3. i = caractere;
  4. caractere = i;

la valeur de caractere est inchangée. Cette situation est vrai que l'extension de signe soit impliquée ou non. L'inversion de l'ordre des affectations peut toutefois perdre des informations. Si x est float et i est int, alors x = i et i = x provoquent tous deux des conversions; float à int casse la troncature de toute partie fractionnaire. Lorsque double est converti en float, le fait que la valeur soit arrondie ou tronquée dépend de la mise en oeuvre.

Étant donné qu'un paramètre d'un appel de fonction est une expression, les conversions de type ont également lieu lorsque des paramètres sont passés aux fonctions. En l'absence de prototype de fonction, char et short deviennent int, et float devient double. C'est pourquoi nous avons déclaré que les paramètres de fonction sont int et double même lorsque la fonction est appelée avec char et float. Enfin, les conversions de type explicite peuvent être forcées dans n'importe quelle expression, avec un opérateur binaire appelé castre. Dans la construction :

(type-name) expression

l'expression est convertie en type nommé par les règles de conversion ci-dessus. La signification précise d'un castre est comme si l'expression était affectée à une variable du type spécifié, étant ensuite utilisée à la place de la construction entière. Par exemple, la routine de bibliothèque sqrt attend un paramètre double, et produira si elle lui est remise par inadvertance autre chose. La fonction sqrt est déclaré dans math.h. Donc, si n est un entier, nous pouvons utiliser :

  1. sqrt((double) n)

pour convertir la valeur de n en double avant de la passer à sqrt. Notez que la conversion produit la valeur de n dans le type approprié; n lui-même n'est pas modifié. L'opérateur de castre a la même priorité élevée que les autres opérateurs binaires. Si des paramètres sont déclarés par un prototype de fonction, comme ils devraient normalement l'être, la déclaration provoque la coercition automatique de tous les paramètres lorsque la fonction est appelée. Ainsi, étant donné un prototype de fonction pour sqrt :

  1. double sqrt(double);

l'appel avec :

  1. racine2 = sqrt(2);

contraint l'entier 2 à la valeur double 2.0 sans avoir besoin de transtyper. La bibliothèque standard C comprend une mise en oeuvre portable d'un générateur de nombres pseudo-aléatoires et une fonction pour initialiser la graine de départ; l'exemple suivant illustre un castre :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. unsigned long int next = 1;
  5.  
  6. int rand(void) {
  7.     next = next * 1103515245 + 12345;
  8.     return (unsigned int) (next/65536) % 32768;
  9. }
  10.  
  11. void srand(unsigned int seed) {
  12.     next = seed;
  13. }
  14.  
  15. int main()
  16. {
  17.     printf("Premier nombre : %i\n", rand());
  18.     printf("Deuxième nombre : %i\n", rand());
  19.     printf("Troisième nombre : %i\n", rand());
  20.     return 0;
  21. }

Opérateurs d'incrémentation et de décrémentation

Le C fournit deux opérateurs inhabituels pour incrémenter et décrémenter des variables. L'opérateur d'incrémentation «++» additionne 1 à son opérande, tandis que l'opérateur de décrémentation «--» soustrait 1. En C on utilise fréquemment «++» pour incrémenter des variables, comme dans :

  1. if(caractere == '\n') ++n1;    

L'aspect inhabituel est que «++» et «--» peuvent être utilisés soit comme opérateurs de préfixe (avant la variable, comme dans «++n»), soit comme suffixe (après la variable : n++). Dans les deux cas, l'effet est d'incrémenter n. Mais l'expression «++n» incrémente n avant que sa valeur ne soit utilisée, tandis que n++ incrémente n après que sa valeur a été utilisée. Cette situation signifie que dans un contexte où la valeur est utilisée, pas seulement l'effet, ++n et n++ sont différents. Si n vaut 6, alors :

  1. x = n++;

mettre x à 6, mais

  1. x = ++n;

réglez x sur 7. Dans les deux cas, n devient 7. Les opérateurs d'incrémentation et de décrémentation ne peuvent être appliqués qu'aux variables; une expression comme (i+j)++ est illégale. Dans un contexte où aucune valeur n'est souhaitée, juste l'effet incrémentiel, comme dans :

  1. if(caractere == '\n') n1++;

préfixe et suffixe sont les mêmes. Mais il y a des situations où l'un ou l'autre est spécifiquement requis. Par exemple, considérons la fonction squeeze(string,caractere), supprimant toutes les occurrences du caractère caractere de la chaîne de caractères string :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. void squeeze(char string[], int caractere) {
  5.     int i, j;
  6.     for(i = j = 0; string[i] != '\0'; i++) if(string[i] != caractere) string[j++] = string[i];
  7.     string[j] = '\0';
  8. }
  9.  
  10. int main(){
  11.     char string[100] = "Sylvain Maltais";
  12.     squeeze(string,'l');
  13.     printf("Sylvain Maltais = %s!\n",string);
  14.     return 0;
  15. }

Chaque fois qu'un non-caractere se produit, il est copié dans la position j courante, et alors seulement j est incrémenté pour être prêt pour le caractère suivant. C'est exactement équivalent à :

  1. if(string[j] != caractere) {
  2.    string[j] = string[i];
  3.    j++;
  4. }

Un autre exemple, considérons la fonction standard strcat(source,target), concaténant la chaîne de caractères target à la fin de la chaîne de caractères source. La fonction strcat suppose qu'il y a suffisamment d'espace dans source pour contenir la combinaison. Comme nous l'avons écrit, strcat ne renvoie aucune valeur; la version standard de la bibliothèque renvoie un pointeur vers la chaîne de caractères résultante.

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. void strcat(char source[], char target[]) {
  5.     int i,j;
  6.     i = j = 0;
  7.     while(source[i] != '\0') i++;
  8.     while((source[i++] = target[j++]) != '\0');
  9. }
  10.  
  11. int main() {
  12.     char Temp[255] = "";
  13.     strcat(&Temp,"Gladir");
  14.     strcat(&Temp,".com");
  15.     printf("%s\n",Temp);
  16.     return 0;
  17. }

Comme chaque caractère est copié de target vers source, le suffixe ++ est appliqué i et j pour s'assurer qu'ils sont en position pour le prochain passage dans la boucle.

Opérateurs au niveau du bit

Le C fournit six opérateurs pour la manipulation de bits; ceux-ci ne peuvent être appliqués qu'aux opérandes intégraux, c'est-à-dire char, short, int et long, qu'ils soient signed ou unsigned.

Opérateur Description
& Au niveau du bit ET
| OU inclusif au niveau du bit
^ OU exclusif au niveau du bit
<< Décalage vers la gauche
>> Décalage vers la droite
~ Complément à un (binaire)

L'opérateur ET au niveau du bit & est souvent utilisé pour masquer un ensemble de bits; par exemple :

  1. n = n & 0177;

met à zéro tous les bits sauf les 7 bits de poids faible de n. L'opérateur OU au niveau du bit | est utilisé pour activer les bits :

  1. x = x | SET_ON;

met à un dans x les bits étant mis à un dans SET_ON. L'opérateur OU exclusif au niveau du bit ^ définit un dans chaque position de bit où ses opérandes ont des bits différents, et zéro là où ils sont identiques. Il faut distinguer les opérateurs binaires & et | à partir des opérateurs logiques && et ||, impliquant une évaluation de gauche à droite d'une valeur de vérité. Par exemple, si x vaut 1 et y vaut 2, alors x & y vaut zéro tandis que x && y vaut un. Les opérateurs de décalage << et >> effectuent des décalages vers la gauche et vers la droite de leur opérande gauche du nombre de positions de bit donné par l'opérande droit, devant être positif. Ainsi x << 2 décale la valeur de x vers la gauche de deux positions, remplissant les bits vacants par zéro; cela équivaut à une multiplication par 4. Le décalage à droite d'une quantité unsigned remplit toujours les bits vacants avec zéro. Le décalage à droite d'une quantité signée se remplira de bits de signe (décalage arithmétique) sur certaines machines et de 0 bits (décalage logique) sur d'autres. L'opérateur binaire ~ donne le complément à un d'un entier; c'est-à-dire qu'il convertit chaque 1 bit en 0 bit et vice versa. Par exemple :

  1. x = x & ~077;

définit les six derniers bits de x à zéro. Notez que x & ~077 est indépendant de la longueur du mot, et est donc préférable à, par exemple, x & 0177700, supposant que x est une quantité de 16 bits. Le formulaire portable n'implique aucun castre supplémentaire, puisque ~077 est une expression constante pouvant être évaluée au moment de la compilation. Pour illustrer certains des opérateurs de bits, considérons la fonction getbits(x, p, n) renvoyant le champ de n bits (ajusté à droite) de x commençant à la position p. Nous supposons que la position de bit 0 est à l'extrémité droite et que n et p sont des valeurs positives sensibles. Par exemple, getbits(x,4,3) renvoie les trois bits en position de bit 4, 3 et 2, ajustés à droite.

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. unsigned getbits(unsigned x, int p, int n) {
  5.     return (x >> (p+1-n)) & ~(~0 << n);
  6. }
  7.  
  8. int main()
  9. {
  10.     printf("%i\n",getbits((unsigned)0xFFFF,4,3));
  11.     return 0;
  12. }

L'expression x >> (p + 1-n) déplace le champ souhaité à l'extrémité droite du mot. ~0 correspond à 1 bits; le décaler vers la gauche de n positions de bits avec ~0 << n place des zéros dans les n bits les plus à droite; en complétant cela avec ~ crée un masque avec des uns dans les n bits les plus à droite.

Opérateurs d'affectation et expressions

Une expression telles que :

  1. i = i + 2;

dans lequel la variable sur le côté gauche est répétée immédiatement sur la droite, peut être écrite sous le format compressée :

  1. i += 2; 

L'opérateur += est appelé un opérateur d'affectation. La plupart des opérateurs binaires (opérateurs comme «+» ayant un opérande gauche et droit) ont un opérateur d'affectation correspondant op =, où op est l'un des opérateurs «+», «-», «*», «/», «%», «<<», «>>», «&», «^», «|». Si expr1 et expr2 sont expressions, alors :

expr1op = expr2

est équivalent à

expr1 = (expr1) op (expr2)

sauf que expr1 n'est calculé qu'une seule fois. Notez les parenthèses autour de expr2 :

  1. x *= y + 1

signifie

  1. x = x * (y + 1)

plutôt que

  1. x = x * y + 1 

Par exemple, la fonction bitcount compte le nombre de bit à 1 dans son paramètre entier.

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int bitcount(unsigned x) {
  5.     int b;
  6.     for(b = 0; x != 0; x >>= 1) if(x & 01) b++;
  7.     return b;
  8. }
  9.  
  10. int main()
  11. {
  12.     printf("Nombre de bits 1 dans 0xFF00 = %i\n",bitcount(0xFF00));
  13.     printf("Nombre de bits 1 dans 0x00FF = %i\n",bitcount(0x00FF));
  14.     return 0;
  15. }

Déclarer le paramètre x comme étant unsigned garantit que lorsqu'il est décalé vers la droite, les bits vides seront remplis de zéros et non de bits de signe, quelle que soit la machine sur laquelle le programme est exécuté. Outre la concision, les opérateurs d'affectation ont l'avantage de mieux correspondre à la façon dont les gens pensent. Nous disons «ajouter 2 à i» ou «incrémenter i de 2», et non «prendre i, ajouter 2, puis remettre le résultat dans i». Ainsi, l'expression i += 2 est préférable à i=i + 2. De plus, pour une expression compliquée comme :

  1. gladvalue[gladpv[p3+p4]+gladpv[p1+p2]]+=2; 

L'opérateur d'affectation facilite la compréhension du code, car le lecteur n'a pas à vérifier minutieusement que deux expressions longues sont bien identiques, ni à se demander pourquoi elles ne le sont pas. Et un opérateur d'affectation peut même aider un compilateur à produire du code efficace. Nous avons déjà vu que l'instruction d'affectation a une valeur et peut apparaître dans des expressions; l'exemple le plus courant est :

  1. while((c = getchar()) != EOF)    

Les autres opérateurs d'affectation (+ =, - =, ...) peuvent également apparaître dans les expressions, bien que ce soit moins fréquent. Dans toutes ces expressions, le type d'une expression d'affectation est le type de son opérande de gauche et la valeur est la valeur après l'affectation.

Expressions conditionnelles

Prenons pour exemple la déclaration suivante&mbsp;:

  1. if(a > b) g = a; else g = b; 

calculer en g le maximum de a et b. L'expression conditionnelle, écrite avec l'opérateur ternaire «?:», fournit une autre manière d'écrire ceci et des constructions similaires. Dans l'expression :

expr1 ? expr2 : expr3

l'expression expr1 est évaluée en premier. S'il est différent de zéro (vrai), alors l'expression expr2 est évaluée, et c'est la valeur de l'expression conditionnelle. Sinon, expr3 est évalué, et c'est la valeur. Un seul parmi expr2 et expr3 est évalué. Ainsi, pour mettre g au maximum de a et b :

  1. g = (a > b) ? a : b;

Il convient de noter que l'expression conditionnelle est en effet une expression et qu'elle peut être utilisée partout où une autre expression peut être. Si expr2 et expr3 sont de types différents, le type du résultat est déterminé par les règles de conversion. Par exemple, si f est un float et n est un int, alors l'expression suivante :

  1. (n > 0) ? f : n 

est un type de données float indépendamment du fait que n soit positif ou non. Les parenthèses ne sont pas nécessaires autour de la première expression d'une expression conditionnelle, puisque la priorité de «?:» est très faible, juste au-dessus de l'affectation. Cependant, ils sont néanmoins recommandés, car ils rendent la condition partie de l'expression plus facile à voir. L'expression conditionnelle conduit souvent à un code succinct. Par exemple, cette boucle affiche n éléments d'un tableau, 12 par ligne, avec chaque colonne séparée par un blanc et avec chaque ligne (y compris la dernière) terminée par une nouvelle ligne :

  1. for(i = 0; i < n; i++) printf("%6d%c",a[i],(i%12==11 || i==n-1) ? '\n' : ' '); 

Une nouvelle ligne est affichée après chaque dixième élément et après le n-ième. Tous les autres éléments sont suivis d'un blanc. Elle peut sembler délicat, mais c'est plus compact que l'équivalent if-else. Un autre bon exemple est lorsqu'on veut rajouter un «s» pour un mot aux pluriels lorsqu'il y a plus d'un élément :

  1. printf("Vous avez %d élément%s.\n",n,n==1?"":"s"); 

Préséance et ordre d'évaluation

Le tableau suivant résume les règles de priorité et d'associativité de tous les opérateurs. Les opérateurs sur la même ligne ont la même priorité; les lignes sont par ordre de priorité décroissante, donc, par exemple, «*», «/» et «%» ont tous la même priorité, étant supérieure à celle des binaires «+» et «-». Les opérateurs «(» et «)» font référence à l'appel de fonction. Les opérateurs «->» et «.» sont utilisés pour accéder aux membres des structures.

Opérateurs Description
( ) [ ] -> . Gauche à droite
! - ++ -- + - * & (type) sizeof Droite à gauche
* / % Gauche à droite
+ - Gauche à droite
<< >> Gauche à droite
< <= > >= Gauche à droite
== != Gauche à droite
& Gauche à droite
^ Gauche à droite
| Gauche à droite
&& Gauche à droite
|| Gauche à droite
?: Droite à gauche
= += -= *= /= %= &= ^= != <<= >>= Droite à gauche
, Gauche à droite

Notez que la priorité des opérateurs binaires &, ^ et | tombe en dessous de == et !=. Cela implique que des expressions de test de bits comme :

  1. if((x & MASQUE) == 0) /* ... */ 

doit être entièrement entre parenthèses pour donner des résultats corrects. Le langage de programmation C, comme la plupart des langages de programmation, ne spécifie pas l'ordre dans lequel l'opérande d'un opérateur est évalué. (Les exceptions sont &&, ||, ?: et ','). Par exemple, dans une déclaration comme :

  1. x = g() + l(); 

g peut être évalué avant l ou vice versa; ainsi si g ou l modifie une variable dont dépend l'autre, x peut dépendre de l'ordre d'évaluation. Les résultats intermédiaires peuvent être entreposés dans des variables temporaires pour garantir une séquence particulière. De même, l'ordre dans lequel les paramètres de fonction sont évalués n'est pas spécifié, donc l'instruction :

printf("%d %d\n",++g,pow(3,g));

peut produire différents résultats avec différents compilateurs, selon que g est incrémenté avant l'appel de pow. La solution, bien sûr, est d'écrire :

  1. ++n;
  2. printf("%d %d\n",g,pow(3,g));

Les appels de fonction, les instructions d'assignation imbriquées et les opérateurs d'incrémentation et de décrémentation provoquent des effets secondaires - certaines variables sont modifiées en tant que sous-produit de l'évaluation d'une expression. Dans toute expression impliquant des effets secondaires, il peut y avoir des dépendances subtiles sur l'ordre dans lequel les variables participant à l'expression sont mises à jour. Une situation malheureuse est caractérisée par la déclaration :

  1. g[i] = i++; 

La question est de savoir si l'indice est l'ancienne valeur de i ou la nouvelle. Les compilateurs peuvent interpréter cette situation de différentes manières et générer des réponses différentes en fonction de leur interprétation. La norme laisse intentionnellement la plupart de ces questions sans précision. Lorsque des effets de bord (affectation à des variables) se produisent dans une expression est laissé à la discrétion du compilateur, puisque le meilleur ordre dépend fortement de l'architecture de la machine. (La norme spécifie que tous les effets secondaires sur les paramètres prennent effet avant l'appel d'une fonction, mais cela n'aidera pas dans l'appel à printf ci-dessus.) La vérité c'est que l'écriture de code dépendant de l'ordre d'évaluation est une mauvaise pratique de programmation dans n'importe quel langage. Naturellement, il faut savoir ce qu'il faut éviter, mais si vous ne savez pas comment cela se fait sur différentes machines, vous ne serez pas tenté de profiter d'une mise en oeuvre particulière. Bien que la syntaxe soit plus longue et qu'il générera plus de code machine, il serait donc plus prudent, d'écrire le code de la manière suivante :

  1. g[i] = i, i++;

Si vous êtes confrontés à ce genre de situation, vous devrez envisager d'utiliser des jeux d'essais en fonction des différentes machines et plateformes et de vous assez qu'il réagisse correctement dans la bonne situation.



Dernière mise à jour : Dimanche, le 8 novembre 2020