Memoire, types, variables
Un programme C peut accéder à la mémoire de l’ordinateur pour conserver le résultat de calculs antérieurs. On parle de mémoire ou mémoire volatile (en anglais memory) lorsque celle ci est accessible rapidement mais se disparait à l’arrêt du programme, et de stockage ou mémoire de masse (en anglais storage) pour la mémoire à accès lent mais persistant à l’arrêt du programme (disque dur, cloud…).
Voici un programme qui accède à la mémoire temporaire de l’ordinateur :
#include <stdio.h>
int main()
{
int toto = 10;
printf("%d\n", 5 + toto);
// affiche 15
}
L’accès à la mémoire se fait par l’intermédiaire de variables et de fonctions : une fonction occupe de la place en mémoire et une variable occupe de la place en mémoire. Variables et fonctions sont considérés comme des objets en C. Un objet en C est une entité qui occupe de la place en mémoire.
Voici un programme qui affiche l’adresse des deux objets que nous avons crée
#include <stdio.h>
int main()
{
int toto = 10;
printf("%zu\n", &toto);
printf("%zu\n", &main);
// affiche 15
}
Exemple d’affichage
6165771116
4301127484
La machine virtuelle du C nous fournit une adresse représentée par un nombre entier positif.
Ce programme déclare une variable nomée
« toto
» de type « int
» et
l’initialise à la valeur 10
. Ensuite il additione 5
à la valeur en mémoire
référencée par le nom toto
. L’opérateur =
en C affecte une valeur à une variable.
L’opérateur d’égalité en C s’écrit ==
.
Noms
Le nom de la variable sert à la référencer après l’avoir déclarée. Un nom
de variable doit être unique dans sa portée, et ne pas commencer par
un chiffre. Une variable ne peut pas avoir le même nom
qu’un mot clé du langage (if
, switch
, etc.). La casse est significative :
toto
et Toto
sont deux variables différentes. Une variable hormis des
chiffres et des lettres peut contenir des underscore _
: on peut
les utiliser pour améliorer la lisibilité du nom de la variable, mais il
faut éviter de faire commencer une variable par un _
car un tel procédé
est souvent utilisé par la librairie standart. Traditionellement,
les noms de variables en C sont en minuscule et les constantes
symboliques en toutes majuscules.
Pour une variable locale, les 31 premiers caractères sont significatifs, pour les variables globales exportées par une librarie, on recommande qu’elle ne dépasse pas 6 caractères de sorte qu’un maximum de langages puissent linker notre librairie.
Types
Le C possède quatre types de base : char
qui contient un octet et qui
sert à contenir un caractère ASCII, int
qui sert à contenir un
nombre entier servant à faire des calculs avec des nombres entiers,
float
qui sert à contenir
des nombres à virgule flottante en simple précision et double
qui
contient des nombres à virgule flottante en double précision.
Il est possible de regrouper ces variables dans des tableaux, des structures et des unions. Un tableau de caractères est considéré comme une chaine de caractères et possède une notation particulière.
Il est possible de déclarer des pointeurs qui pointent sur ces variables, ainsi que sur des fonctions retournant et prenant en argument de telles variables.
Le type d’une variable spécifie sa taille en mémoire, c’est à dire l’ensemble des valeurs qu’il peut avoir, et les opérations qui sont permises dessus.
#include <stdio.h>
main() {
char variable; // variable de type char non initialiasée
char variable1 = 'A'; // variable de type char initialiasée
int variable2 = 12; // variable de int initialisée
float variable3 = 12.34f; // variable de type nombre à virgule
double variable4 = 12.34; // variable de type nombre à virgule en double précision
int 1var; // illegal
int _ma_var; // déconseillé
ma_var_implicite = 12; // par défaut le type d'une variable est 'int'
putchar(variable1); // affiche un caractère à l'écran
}
putchar()
affiche à l’écran un caractère isolé. Contrairement à printf()
qui prend en argument des
chaînes de caractère
entourées de
guillemets double « " », les caractères littéraux isolés sont entourés de guillemets
simples « ’ »
Types entiers signés
Le type par défaut d’une variable en C est int
pour integer soit
un nombre entier. Une variable déclarée sans type est par défaut de type int
.
Ce mécanisme à l’origine facilitant le portage de programmes écrits en
B
est aujourd’hui désuet.
// fonction retournant un int de manière implicite, notation désuete
ma_fonction() {
int ma_variable;
mon_autre_variable; // aussi un int, désuet
}
On peut spécifier si on veut que la variable soit signée ou non, c’est à dire qu’elle accepte ou non des valeurs négatives.
ma_fonction() {
signed int var1 = -12;
unsigned int var2 = 12; // ne pourra jamais être plus petit que zero (0)
int var3 = -12; // par défaut un int est de type signé
unsigned var4 = 12; // int est optionel si on spécifie unsigned
signed var4 = 12; // de même pour signed
}
Par défaut le type int
est signé : ainsi le mot-clé signed
est inutile,
sauf si on travaille avec le type char
car il n’est
pas spécifié
qu’il doive être signé ou non. En pratique il est généralement non signé
pour permettre plus intuitivement de contenir des caractères étendus de
l’ASCII. Si on veut travailler avec des entiers de un octet, il faut
donc spécifier explicitement signed char
et unsigned char
selon
qu’on veuille travailler sur une plage de -128/127 ou 0/255.
En dehors de ces rares cas, il est rare de rencontrer le mot clé
signed
dans un code source, auquel cas il peut être un indicateur de
problème.
Le C traditionnellement accepte 3 représentations différentes des entiers signés : une représentation en bit de signe et magnitude, une représentation en complément à un, et une représentation en complément à 2.
Dans une représentation en bit de signe, on utilise le premier bit de poids
fort pour indiquer le signe du nombr : 1
aura
la représentation binaire 0000 0001
, et -1
aura la représentation
binaire 1000 0001
. Dans cette représentation il y a deux facons
de noter zero : 0000 0000
et 1000 0000
.
La représentation en complément inverse tous les bits pour représenter
la valeur opposée : c’est à dire
que 12
sera noté 0000 1100
, et -12
sera noté 1111 0011
. Dans cette
notation il y a aussi deux facons de représenter 0 : 0000 0000
et 1111 1111
On parle parfois de zero positif et négatif : +0
et -0
.
Finalement la notation en complément à deux soustrait de un la valeur qu’on
souhaite inverser puis inverse tous les bits : pour avoir -10 dans cette notation
on part de 10
soit 0000 1010
, on soustrait un soit 0000 1001
, puis on inverse
soit 1111 0110
. Dans cette notation -1 se note 1111 1111
, et il n’y a qu’une
seule facon de représenter le chiffre 0.
La notation en complément à deux
permet de faire des opérations arithmétiques sans nécessiter de conversion,
par exemple pour calculer 10 - 1
, on additionne 10
à la valeur -1
,
soit 0000 1010
+ 1111 1111
, soit 0000 1001
.
Si on a la valeur 127
codée 0111 1111
et qu’on lui ajoute 1
,
on obtient 1000 0000
, qui dans une machine en signe et magnitude
vaut -0
, une machine en complément à 1 vaut -127
et une
machine en complément à 2 vaut -128
. Le C ne garantit
pas la valeur d’un tel entier signé en cas de dépassement
de capacité et considère qu’un tel comportement comme une
condition d’erreur.
La notation en complément à deux s’est imposée comme unique standart, mais historiquement le C considérait les trois procédés comme possible, et ne garantissait pas la représentation binaire d’un nombre négatif, et à fortiori le comportement en cas de débordement d’entier.
En complément à un et en signe et magintude, un signed char
peut être dans la plage -127
/127
, en complément à deux un signed char
peut
être dans la place -128
/127
, en cas de dépassement le C ne garantit pas la valeur
qu’aura un signed int
. Pour profiter de l’arithmétique modulaire il
faut donc spécifiquement déclarer la variable comme unsigned
.
Dans les prochaines versions du C, le codage en complément à deux sera l’unique acceptée et le comportement en cas de dépassement ne sera plus indéfini.
Types entiers extensibles
Un entier de un octet
s’appelle char
: il sert à contenir un caractère affichable dans le
jeu de caractères local. Depuis le C99 ce jeu de caractères est l’utf8.
Un entier de 16 bits s’appelle int
. Lorsque les machines
32 bits sont apparues, les mots clés short
et long
ont permis de
spécifier explicitement un entier de 16 ou 32
bits : dans ce cas il faut spécifier short int
et long int
comme type.
Si on utilise une valeur de int
nue, selon qu’on soit sur une machine
16 ou 32 bits, celui-ci vaudra 16 ou 32 bits - la taille avec laquelle
le processeur préfère effectuer des calculs avec des nombres entiers.
Lorsque les machines
64 bits sont apparues il est devenu possible de spécifier un
type long long int
pour obtenir explicitement des entiers
de 64 bits, mais si lors du passage des ordinateurs 16 à 32 bits,
le type int a suivi le mouvement, lors du passage de 32 à 64 bits,
le type int est généralement resté bloqué à 32 bits.
Sur linux (et plus généralement POSIX) on utilise
short int
pour un entier 16 bits, int
pour un entier 32 bits et long int
pour un entier 64 bits (💩).
Le type int
a une taille variable et long int
a une signifiation
différente selon qu’on soit dans le monde Linux ou non, pour
pallier cela on a des types entiers à taille explicite
de la forme uint_32
, int_16
et ainsi de suite (💩).
Voici les bornes des différents types des entiers et leur équivalent en écriture explicte.
type | signed | unsigned |
---|---|---|
char | -128 / 127 (int_8) | 0/255 (uint_8) |
short | -32768 / 32767 | 0/65536 |
long | -2 147 483 648 / 2 147 483 647 | 0/4 294 967 296 |
long long | -9 223 372 036 854 775 808 / 9 223 372 036 854 775 807 | 0/18 446 744 073 709 551 616 |
Performance
On peut également spécifier la taille maximale souhaitée pour l’entier. Avant les années 70 et l’ASCII, les ordinateurs avaient des mots de grande taille, typiquement 12, 16, 18 bits… où on stockait des nombres mais aussi des lettres. L’ASCII ayant popularisé un système a 7 bits, et l’EBCDIC étant sur 8 bits, pour stocker du texte les ordinateurs à 8 bits sont devenus la norme, et pour calculer avec des nombres de grande taille les processeurs sont devenus « extensibles », avec des registres étendus, où AX, BX, CX, DX aggrandissent les reguistres A, B, C et D et pemettent des calculs sur 16 bits là où les registres initiaux jadis allants jusque 24 bits sont confinés à 8 bits. Avec les ordinateurs 32 bits, les registres EAX, EBX, etc. doublent cette capacité. Le C permet de travailler avec des sous parties de registres pour coller aux programmes assembleur qui se prêtaient à des optimisations d’usage des registres. Le besoin d’optimiser l’usage des registres est extrêmement marginal.
Il est en règle générale dangeureux et inutile de travailler avec des entiers
plus petits que la taille maximale permise par les registres.
Le gain de stocker ses calculs dans des short
ou des char
plutot
que des int
est marginal au risque de débordement de tampon.
Faire travailler un processeur en « sous régime » a également
pour conséquence de le faire travailler en performances dégradées.
Utiliser le modificateur « unsigned
» pour gagner quelques octets d’espace
est également contre-productif, car il est peu probable qu’on ait besoin
de stocker des valeurs spécifiquement plus grandes que 2 milliard et plus petites
que 4 milliard. Le gain est ici marginal au danger de soustraire une valeur
qui produirait un nombre négatif, ou de faire une comparaison avec un type incompatible
qui enclencherait un mécanisme complèxe et hasardeux de
promotion d’entier
.
Les types int
décorés sont une source récurrente de problèmes
difficile à déceler : ainsi trouver un « short
»,
un « signed char
» ou un « unsigned
»
dans un code source et généralement un indicateur de problème
et un symptome de code non robuste. On peut également considérer que d’avoir
besoin de stocker des valeurs plus grandes que 2 milliards soit marginal
et que « long long
» soit problématique. Le simple type int
devrait
être le seul que vous utilisiiez.
Même si vous êtes certain que votre variable
ne contiendra que des valeurs de petite taille ou que des valeurs positives,
n’utilisez pas de types unsigned int
, short int
ou char
,
car vous dégraderiez les performances
du programme et ouvrez la portée à une volée de problèmes et de bugs potentiels.
Type texte
Le C fournit un type char
contenu sur un octet et qui contient
un caractère ASCII qui a vocation a être affiché comme texte. Un caractère
ASCII nécessite 7 bits, le 8ème peut être utilisé comme
bit de parité
,
ou bien comme mécanisme d’extension de l’ASCII. Selon la machine
le type char peut être signé ou non. On considère, même dans la norme,
que le type char
seul soit un type à part entière - non entier
ayant pour unique vocation de contenir des valeurs de code ASCII.
Si on veut faire des calculs mathématiques sur des entiers limités 8 bits
on doit utiliser signed char
et unsigned char
qui sont
des types de nombres entiers et dont des types texte.
Nombres à virgule fixe
Pour palliers aux débordements d’entiers, une extension au C propose des nombres à virgule fixe saturés 🌍⤴ , qui ne sont pas pour l’instant dans la norme.
Portée
Dans le C ANSI, une variable ne peut être déclarée que en début de fonction. Depuis le C99, cette restriction est enlevée et on peut déclarer une variable nímporte où au sein d’une fonction. Depuis le C99 une variable possède donc une visibilité qui n’est plus globale à la fonction mais limitée au bloc où elle se trouve.
#include<stdio.h>
main() {
char ma_var = 'A';
putchar(ma_var); // A
{ // déclare un bloc
char ma_var = 'B'; // redéclare la variable au sein du bloc
putchar(ma_var); // B
char ma_deuxieme_var = 'C';
}
// a la sortie du bloc, la variable ma_var de valeur B disparait
putchar(ma_var); // A
putchar(ma_deuxieme_var); // en outre ceci est illégal,
// ma_deuxieme_var n'existe que au sein du bloc
}
Le goto
n’ignore pas les déclarations de variables qui le précèdent
#include<stdio.h>
main() {
char ma_var = 'A';
goto ici;
{ // déclare un bloc
char ma_var = 'B'; // redéclare la variable au sein du bloc
ici:
putchar(ma_var); // indéfini, le goto a sauté l'instruction
// d'assignation, mais l'espace sur la pile existe
}
}