Compléments

Table des matières

Constantes
0, 010, 0x0, 0U, 0L, '0', 0.0, 0e0, "0", const, enum
Types de base
char, short, int, long, float, double, signed, unsigned
Opérateurs
associativité, précédence
Bugs courants
void main, conseil d'achat de livre

Constantes

Littéraux

Les littéraux constants, comme les variables, ont tous un type de base. Des notations particulières permettent de préciser le type d'un littéral.

#define A                 'A'             /* entier (le code du caractère A) */
#define APOSTROPHE        '\''            /* entier (le code du caractère ') */
#define A_LA_LIGNE        '\n'            /* entier (le code du caractère \n) */
#define UN                1               /* entier */
#define DIX               10              /* entier */
#define DIXo              012             /* entier octal */
#define DIXh              0xA             /* entier hexadécimal */
#define MOINS_UN          (-1)            /* entier */
#define UN_NON_SIGNE      1U              /* entier non signé */
#define GRAND_NON_SIGNE   65535U          /* entier non signé */
#define UN_LONG           1L              /* entier long */
#define PETIT_LONG        (-2147483647L-1)/* entier long */
#define UN_LONG_NON_SIGNE 1UL             /* entier long non signé */
#define PI                3.1415927       /* flottant double précision */
#define c                 3e8             /* flottant double précision */
#define PI_FLOAT          3.1415927F      /* flottant simple précision */
#define PI_LONG_DOUBLE    3.1415927L      /* flottant long double précision */
#define CHAINE_VIDE       ""              /* chaîne de caractères */
#define HELLO_WORLD       "hello, world"  /* chaîne de caractères */

Caratères littéraux

En C les littéraux caractères (comme 'A') sont de type int ! En C++ ils sont de type char. Mais dans les deux cas ce sont des nombres entiers qui valent le code du caractère entre guillemets.

Certains caractères peuvent ou doivent être représenté dans un source C sous la forme d'une séquence d'échappement :

séquencecaractère
\aalert (sonnerie)
\bbackspace (effacement)
\fformfeed
\nnewline (nouvelle ligne)
\rcarriage return
\thorizontal tab
\vvertical tab
séquencecaractère
\\\
\??
\'' (utile dans '\'')
\"" (utile dans "\"")
\ooocaractère de code octal ooo
\xhhcaractère de code hexadécimal hh

Énumération

Si on a besoin d'une collection de constantes entières de même type, on peut utiliser une énumération :

enum couleurs { noir, blanc, rouge=3, bleu, vert };
int go(enum couleurs feu) { return feu == vert; }

Les identificateurs de l'énumération sont des constantes de type int. Par défaut elles sont numérotées à partir de 0. Ici on a noir=0, blanc=1, rouge=3, bleu=4, vert=5.

Subtilités

const

const int un = 1;
int main(void) { return un; }

ANSI C a introduit le mot-clé const. Il ne sert cependant pas à déclarer de véritables constantes. Il indique simplement qu'une variable ne sera pas modifiée. En particulier, une variable const ne peut pas être utilisée là où une expression constante est attendue (taille de tableau en ANSI C, label de case).

À la place on utilise des macros ou des énumérés (pour les constantes entières).

En C++ les variables constantes sont devenues utilisables comme de vraies constantes. Elles restent cependant toujours des variables ; elles se trouvent notamment à une adresse en mémoire vive.

-2147483648

La notation (-2147483647L-1) est nécessaire pour obtenir -2147483648 car en 32 bits on ne peut pas écrire -2147483648L. En effet, le compilateur verrait l'opérateur unaire - appliqué à 2147483648L, or le plus grand long 32 bits possible est 2147483647L.

Types de base

Les types de base sont des types de données directement manipulables par les processeurs.

Types de base
nom completnom usuelbits (typique)bits (minimum)limites formelles
charchar880 à 255 ou -127 à 127
signed charsigned char88-127 à 127
unsigned charunsigned char880 à 255
signed short intshort1616-32767 à 32767
unsigned short intunsigned short16160 à 65535
signed intint3216-32767 à 32767
unsigned intunsigned32160 à 65535
signed long intlong3232-2147483647 à 2147483647
unsigned long intunsigned long32320 à 4294967295
floatfloat32¹6 décimales ²
doubledouble64¹10 décimales ²
long doublelong double80¹10 décimales ²

¹ N'a pas vraiment de sens.
² Et d'autres exigences.

Voici un programme utilisant tous les types de base :

#include <stdio.h>

int main(void)
{
  char c = 'é';                    /* 11101001 */
  signed char sc = -23;            /* 11101001 */
  unsigned char uc = 233;          /* 11101001 */
  short si = -12345;               /* 1100111111000111 */
  unsigned short usi = 53191U;     /* 1100111111000111 */
  int i = -12345;                  /* 11111111111111111100111111000111 */
  unsigned int ui = 53191U;        /* 00000000000000001100111111000111 */
  long li = 1073741824L;           /* 01000000000000000000000000000000 */
  unsigned long uli = 1073741824UL;/* 01000000000000000000000000000000 */
  float f = 3.14159265358979323846264;
  double d = 3.14159265358979323846264;
  long double ld = 3.14159265358979323846264L;

  printf("%c %d %u\n", c, sc, uc);
  printf("%d %u %d %u %ld %lu\n", si, usi, i, ui, li, uli);
  printf("%.24g\n%.24g\n%.24Lg\n", f, d, ld);

  return 0;
}

Les conversions entre types de base sont automatiques. On peut écrire float f = 'a'; ou char c = 2.23;. Ce n'est pas du meilleur style, mais le compilateur fait simplement les conversions qui s'imposent. Même dans une conversion perdant des bits ou de la précision le compilateur n'émet généralement aucun avertissement.

Pour C un char n'est qu'un petit nombre entier. Écrire char c='A'; ou char c=65; revient au même si 'A' est codé en ASCII.

Le type int correspond au fameux mot machine, c'est celui sur lequel les calculs entiers sont les plus efficaces. Les calculs sont faits au moins dans la précision de int. Autrement dit, si on additionne par exemple deux short, alors ils sont d'abord promus (automatiquement convertis) en int avant que l'addition ait lieu en int et produise un int.

Bien sûr si un type est plus grand qu'un int alors la promotion se fait vers le type le plus grand.

Les types int et long sont souvent équivalents (32 bits) sur les processeurs actuels. Sur les anciens processeurs, int et short étaient souvent équivalents (16 bits).

C99 a ajouté un type long long d'au moins 64 bits.

Le type char est parfois signé, parfois pas, ça dépend des systèmes. C'est pour cela que signed char n'est pas forcément équivalent à char.

Opérateurs

Opérateurs par précédence décroissante
OpérateurOpération
objet . membre
pointeur -> membre
pointeur [ expr ]
expr ( expr,... )
lvalue ++
lvalue --
sélection de membre
sélection de membre
indexation
appel de fonction
post-incrémentation
post-décrémentation
sizeof expr
sizeof ( type )
++ lvalue
-- lvalue
~ expr
! expr
- expr
+ expr
& lvalue
* expr
( type ) expr
taille d'objet
taille de type
pré-incrémentation
pré-décrémentation
complément de bits
négation
moins unaire
plus unaire
adresse de
déréférence
conversion de type (cast)
expr * expr
expr / expr
expr % expr
multiplication
division
modulo
expr + expr
expr - expr
addition
soustraction
expr << expr
expr >> expr
décalage de bits à gauche
décalage de bits à droite
expr < expr
expr <= expr
expr > expr
expr >= expr
plus petit
plus petit ou égal
plus grand
plus grand ou égal
expr == expr
expr != expr
égal
différent
expr & expr
et de bits
expr ^ expr
ou exclusif de bits
expr | expr
ou de bits
expr && expr
et logique
expr || expr
ou logique
lvalue = expr
lvalue *= expr
lvalue /= expr
lvalue %= expr
lvalue += expr
lvalue -= expr
lvalue <<= expr
lvalue >>= expr
lvalue &= expr
lvalue |= expr
lvalue ^= expr
assignation
multiplication et assignation
division et assignation
modulo et assignation
addition et assignation
soustraction et assignation
décalage à gauche et assignation
décalage à droite et assignation
et de bits et assignation
ou de bits et assignation
ou exclusif de bits et assignation
expr ? expr : expr
expresion conditionelle
expr , expr
virgule de séquence

Ce tableau inspiré du livre The C++ programming language de Stroustrup.

expr signifie toute expression au résultat d'un type acceptable. lvalue signifie toute expression retournant un objet modifiable d'un type acceptable.

Les opérateurs unaires, d'assignation et l'expression conditionelle s'associent de droite à gauche, les autres de gauche à droite. Cela signifie que a=b=c équivaut à a=(b=c) alors que a+b+c équivaut à (a+b)+c.

Les opérateurs sur des bits et le modulo ne s'appliquent que sur des opérandes de type entier.

Particularités
OpérateursCommentaires
pointeur -> membre

L'expression pointeur->membre équivaut à (*pointeur).membre.

pointeur [ expr ]

Par définition E1[E2] est équivalent à *((E1)+(E2)). Ceci rend cet opérateur commutatif, donc on peut aussi écrire expr [ pointeur ]. En pratique ce n'est utile que dans un obsfucated C contest :

if (i["hello"]) 3[t] = 123;
lvalue ++
lvalue --

La précédence supérieure de ces opérateurs à l'opérateur de déréférencement * est utilisée dans la fameuse boucle de copie de chaîne :

while (*s++ = *t++);
lvalue ++
lvalue --
++ lvalue
-- lvalue

Il faut être prudent avec ces opérateurs car modifier deux fois la même lvalue sans point de séquencement intermédiaire cause un résultat indéfini, comme dans i = ++i. Le point-virgule à la fin d'une instruction est un point de séquencement. En outre les opérateurs suivants introduisent un point de séquencement : &&, ||, ?: et ,.

sizeof expr

Il est très rare de voir cet opérateur utilisé sans parenthèses, même si avec une expression elles sont optionnelles.

sizeof ( type )

Par définition, sizeof(char) vaut 1, quelle que soit la taille en bits de char (certains processeurs spécialisés ont des char de 32 bits).

~ expr

unsigned u = ~0; est nettement plus propre que unsigned u = -1; pour avoir une variable avec tous les bits à 1.

expr / expr
lvalue /= expr

Entre nombres entiers / calcule le quotient. Un diviseur nul plante le programme.

expr % expr
expr %= expr

Le modulo par zéro plante le programme. Autrement le modulo est le reste de la division et on a toujours (a/b)*b + a%b égal a. N'est possible qu'avec des nombres entiers.

expr >> expr
expr << expr

Le décalage à droite d'un nombre négatif donne un résultat dépendant de l'implémentation (non portable). Un décalage à droite ou gauche négatif ou de trop de bits donne un résultat indéfini.

expr & expr
expr | expr
expr ^ expr

Attention, la précédence moindre que les opérateurs de comparaison requièrent généralement l'usage de parenthèses :

if ((val1 | FLAG) != (val2 | FLAG))

Cela est dû au fait qu'au tout début de C, les opérateurs && et || (de précédence à peine moindre) n'existaient pas.

expr & expr

Utile pour tester un bit : val & 0x40.

expr | expr

Utile pour tester plusieurs bits : val & (FLAG1 | FLAG2 | FLAG3).

expr && expr
expr || expr

Ces opérateurs introduisent un point de séquencement, ce qui permet d'écrire :

if (i-- && t[i])

La seconde opérande n'est évaluée que si nécessaire, ce qui permet d'écrire :

if (y != 0 && x / y > 100)
lvalue &= expr

Utile pour mettre un bit à zéro : val &= ~0x40.

lvalue |= expr

Utile pour mettre un bit à un : val |= 0x40.

lvalue ^= expr

Utile pour inverser un bit : val ^= 0x40.

expr ? expr : expr

Teste la première expression avec tous les effets de bord (point de séquencement). Exécute la seconde ou troisième expression selon que le résultat de la première est vrai ou faux.

#define MAX(A,B) ((A) > (B) ? (A) : (B))
expr , expr

Très rarement utile, sauf dans un contexte nécessitant une seule expression :

for (i = 0; i < 10; i++, j++)

Lorsque l'opérateur , peut être confondu avec une virgule requise par la syntaxe, il faut utiliser des parenhèses. Par exemple dans un appel de fonction :

f(a, (t = 3, t + 2), c)

Bugs courants

void main()

On tombe souvent sur des programmes déclarant main comme fonction void. Un certain nombre de compilateurs (sur Windows) le supportent. Toutefois, void main n'existe dans aucun standard.

t[i] = i++

Modifier deux fois la même variable sans point de séquencement entre deux a un effet indéfini. L'opérateur = n'introduit pas de point de séquencement.

printf(argv[3])

L'erreur est de donner à printf une chaîne de formatage contenant un nombre inconnu de %. Ce problème (de sécurité !) se pose bien sûr avec toutes les fonctions prenant une chaîne de formatage. Avec printf on le règle ainsi : printf("%s", argv[3]).

char* gets(char*)

Cette fonction standard lit une ligne de caractère et la sauve dans un tableau. Elle est inutilisable de manière sure car une longue ligne risque toujours de déborder du tableau.

int p = malloc(4);
int adr = (int)&toto;

Un int n'occupe pas forcément 4 bytes ; sizeof(int) donne la bonne taille. Les préjugés sur la taille des types et le non usage des types appropriés (size_t, ptrdiff_t, unsigned long) causent des problèmes de portage, notamment sur les systèmes 64 bits.

t[argv[1][i]]

Utiliser un char comme indice de tableau fait d'un caractère 8 bits un indice négatif sur une architecture où les caractères sont signés, d'où des accès à côté du tableau.

exit(-1)

La valeur 1 indique conventionnellement l'échec. Ou EXIT_FAILURE si on veut être absolument portable. -1 est hors des limites données par la documentation de la libc (de 0 à 255).

void transformer(char*, char*);

Le const dans le prototype de la fonction standard char* strcpy(char*, const char*) indique clairement quelle est la chaîne source non modifiée et la chaîne destination modifiée, c'est agréable. C'est aussi un des rares supports du compilateur pour éviter les bugs de pointeur. L'usage de const dans les prototypes de fonctions recevant des pointeurs est donc important.

if (ptr != NULL) free(ptr);

Ce n'est pas un bug, mais le programmeur ne lit probablement pas les manuels des fonctions standards, ce qui n'inspire pas confiance et cause des caractères inutiles. (man 3 free : If ptr is NULL, no operation is performed.)


© 2002 Marc Mongenet, pour le Groupe des Utilisateurs Linux du Léman (GULL).