Algorithmique et C de base

Objectifs
Dans ce cours, nous allons définir
  • Ce qu'est une variable
  • Ce qu'est un type, les types de base
  • Ce qu'est une constante, la déclarer et la définir
  • Les expressions et affectations
  • Les tests
  • Les boucles

Identificateurs

Un identificateur correspond à des mots-clés utilisés pour spécifier ou donner un sens (i.e pouvoir comprendre le rôle uniquement via l’identificateur pour lire un programme plus rapidement) à des variables, des fonctions ou actions, des types ou des structures. Les identificateurs sont construits de la manière suivante:

  • il s’agit d’une suite de caractères, chiffres et du caractère spécial _
  • ils ne commencent pas par un chiffre
  • en ce qui concerne les caractères, la casse (majuscule/minuscule) est importante

Types de base

Rappel (cours de logique)
Toutes les données représentées et stockées dans un ordinateur sont en binaire (représentation en base 2). Même si, pour des raisons de simplicité, vous pouvez entrer des données sous forme décimale, octale, hexadécimale... elles sont stockés en interne en binaire.

Variables

Le principe d’une variable part de la volonté de stocker en mémoire des données, données que l’on peut retrouver facilement via leur identificateur. Étant donné que ces données peuvent être modifiées par le programme, elles ne sont pas constantes au cours du temps d’où le terme de variable. Bien entendu, les données peuvent être de différente nature: nombres, suite de nombres, texte, images… Ainsi sont définis les types qui permettent de catégoriser les différentes variables. Et une variable d’un type spécifique ne peut contenir que des données du même type. De même, certaines opérations sont valides sur un type donné mais pas sur d’autres. Au final une variable est un élément qui:

  • occupe une place en mémoire (et la place occupée est définie par le type),
  • a un identificateur ce qui permet au programmeur de ne pas avoir à se préoccuper de l’endroit en mémoire où se trouve la variable
  • a un type qui permet de définir quelles opérations sont valides (i.e. les opérations valides ne sont pas les mêmes que l’on manipule du texte, des nombres, des images…).

Avant d’utiliser une variable il faut la déclarer, c’est à dire spécifier son type et son identificateur.

Convention d'écriture (voir ...)
les identificateurs des variables sont en minuscules avec le caractère _ comme séparateur de mots.

Type entier

Le type entier permet de représenter un nombre entier positif ou négatif. En C, le type int code des entiers signés sur 32 bits (4 octets). Par conséquent les entiers que l’on peut coder avec ce type int appartiennent à l’intervalle [-2^31, 2^31^-1].

int x;        // déclaration d'une variable x de type entier
int a = 1024; // idem avec affectation d'une valeur initiale décimale
int b = 012;  // idem avec affectation d'une valeur initiale octale
int c = 0x4f; // idem avec affectation d'une valeur initiale hexadécimale

D’autres types existent pour représenter les entiers:

  • unsigned int sur 4 octets qui représente des entiers positifs ou nuls [0, 2^32-1],
  • short sur 2 octets qui représente des entiers signés [-2^15^, 2^15^-1],
  • unsigned short sur 2 octets qui représente des entiers positifs ou nuls [0, 2^16-1],
  • long sur 8 octets qui représente des entiers signés [-2^63^, 2^63^-1].
  • unsigned long sur 8 octets qui représente des entiers positifs ou nuls [0, 2^64-1].

Ceci implique par exemple que:

short          x = 40000; // ERREUR ! (la valeur d'un short est au max 32767)
unsigned short y = 40000; // OK !
int            z = 40000; // OK !

Sur ces types entiers, un certain nombre d’opérations existent:

int a = 5;
int b = 3;
int c = 10;

int d = a + b;  // d = 8
int e = a - b;  // e = 3
int f = a * b;  // f = 15
int g = c / b;  // g = 3 (quotient de la division entière)
int h = c % b;  // h = 1 (reste de la division entière)
int i = a << 2; // i = 20 (décalage binaire de deux bits vers la gauche)
int j = a >> 2; // j = 1 (décalage binaire de deux bits vers la droite)
int k = a & b;  // k = 1 (ET-logique bit à bit)
int l = a | b;  // l = 7 (OU-logique bit à bit)

Et bien entendu il est possible de comparer deux entiers ensemble avec les opérateurs de comparaison == (égalité), != (différence), <, >, <=, >=

Type booléen

Le type bool permet de représenter un booléen (deux états VRAI ou FAUX). En C ce type n’existe pas réellement (à savoir un type qui prend 1 bit de mémoire). Il existe un type bool défini depuis 1999 mais il s’agit en fait d’un entier codé sur 1 octet qui correspond à false lorsqu’il vaut 0 et true lorsqu’il vaut 1 (en fait toute valeur différente de 0 est considérée comme étant true).

#include <stdbool.h>
// bool n'est pas un type standard, une inclusion de sa déclaration est
// nécessaire avant de pouvoir utiliser des variables de types bool

bool a;
bool b = false;
bool c = 1; // équivalent à true

Pour les booléens, il existe bien évidemment les opérateurs logiques: && pour le ET-logique, || pour le OU-logique et ! pour le complément.

#include <stdbool.h>

bool a = true;
bool b = false;
bool c = a && b; // c = false
bool d = a || b; // d = true
bool e = !a;     // e = false

Type réel

Le type réel permet de représenter un nombre réel quelque soit sa représentation. En C la représentation interne est forcément scientifique (i.e. comme 0.5e^-4^) et se base sur la norme IEEE754 que nous ne détaillerons pas ici. Deux types permettent de représenter les réels: les réels simple précision float codés sur 4 octets et les réels double précision double codés sur 8 octets.

float  a = 10.495;
double b = 756436.2357;
float  c = 1.0f;
Remarque
le suffixe f est parfois ajouté car par défaut les nombres sont interprétés comme de type double. On ajoute le f lorsque l'on veut être sûr que le nombre soit interprété comme un float (pour des raisons d'optimisations). Vous rencontrerez certainement cette notation au cours de cette année mais son utilisation restera marginale.

Les mêmes opérateurs arithmétiques et de comparaisons que pour les entiers existent sauf %, <<, >> qui n’ont pas de sens sur des réels.

Remarque
bien entendu l'opérateur / effectue une division réelle lorsque vous manipulez des variables réelles.

Type caractère

Le type réel permet de représenter n’importe quel caractère que l’on peut afficher (lettres, chiffres, signes de ponctuation…). En C, un caractère est codé sur un octet. Chaque caractère est en fait représenté par un entier (signé ou non, cela dépend de l’architecture) en se basant sur la table ASCII étendue. Pour représenter la lettre a, on utilise la syntaxe suivante: 'a'. Vous utiliserez tout au long de l’année un certain nombre de caractère spéciaux: '\t' (tabulation), '\n' retour à la ligne…

char c;
char d = 'a';

L’utilisation de la table ASCII implique que ça n’est pas le symbole du caractère qui est stocké en mémoire mais son code ASCII qui est un entier. Du coup, il est possible de faire les opérations suivantes:

int i = 'a'; // un code ASCII est codé sur 1 octet et donc
// est également stockable dans un int (4 octets)
char c = 65;    // 65 est le code ASCII de 'A'
char d = c + 2; // d vaut 67 qui est le code ASCII de 'C'

Autres types

Tous les autres types dérivent de ces types de bases, ils seront vus plus tard (pour les tableaux) et au semestre 6 (pour les structures cartésiennes).

Expressions

Après le stockage des données (section précédente), leur traitement se fait via des expressions comme par exemple effectuer la somme de variables ou tester si une variable entière est plus petite qu’un autre. On définit ainsi des expressions numériques et des expressions booléennes.

Par exemple en C:

1 + x *y - 3(x<7 && y> 3) || b

Ainsi, on remarque qu’une expression est constituée d’opérateurs, de sous-expressions et de sous expressions de base (variable ou constante).

En C, une expression peut être (entre autres):

  • un identificateur
  • une constante
  • une chaîne littérale (“bonjour !”)
  • une expression numérique
  • une expression booléenne
  • une expression-affectation. Il s’agit de l’expression qui permet de stocker des valeurs dans des variables (affectation).

En C, une affectation s’écrit de la manière suivante:

i = 5;
c = 'a';

L’affectation est toujours construite de la manière suivante:

  • à gauche: une variable ou expression que l’on peut affecter (on parle de l-value)
  • à droite: une variable ou une expression ou une constante (on parle de r-value)

Ainsi la syntaxe 13 = i n’est pas possible en C car 13 n’est pas une l-value (c’est une constante). Au niveau du fonctionnement, la valeur de droite est calculée (évaluée) et affectée à la variable de gauche. En C, la valeur de l’expression-affectation est la valeur calculée à droite. Par exemple x = (y = 8) + 1 fait que x vaut 9 et que la valeur de toute l’expression est également 9.

Il existe également des affectations qui utilisent un opérateur que l’on rencontre très fréquemment (on parle d’assignements composés):

j = i++; // j = i; i = i + 1 ;
j = ++i; // i = i + 1; j = i;
j = i--; // j = i; i = i - 1 ;
j = --i; // i = i - 1; j = i;
i += 4;  // i = i + 4; idem avec -= /= *= ...

Constantes

Une constante est, comme son nom l’indique, une valeur qui ne change pas au cours de l’exécution d’un programme. C’est une fonctionnalité intéressante puisqu’elle permet d’écrire du code évolutif que l’on peut paramétrer.

En C il y a plusieurs façons de définir une constante, nous en verrons deux.

define

La première syntaxe pour définir une constante est la suivante:

// Déclaration d'une constante
#define CONSTANTE valeur

// Syntaxe à utiliser quand la déclaration
// ne tient pas sur une seule ligne
#define AUTRE_CONSTANTE (autre_ leur)

Cette syntaxe permet de faire un alias (i.e. raccourci) que le compilateur va utiliser pour remplacer dans la suite du programme toutes les occurrences de CONSTANTE par valeur. Il est important de remarquer qu’il n’y a pas de ; final. Cette syntaxe comporte deux inconvénients:

  • valeur n’a pas de type, ce qui peut poser problème dans certains cas où des opérateurs différents suivant le type manipulé.
  • il ne s’agit pas d’une affectation de variable: il n’y a pas d’évaluation mais uniquement une substitution littérale, ce qui peut entraîner des erreurs.

Voici un exemple qui montre les limites de #define :

#define X 2
#define Y 4
#define N X + Y
int z = 3 * N; // z = 10, ERREUR !

Ici, le compilateur en faisant la substitution génère l’expression int z = 3 * X + Y;. On peut corriger cette erreur en écrivant #define N ( (x) + (y) ).

const

Cette syntaxe permet d’éviter les inconvénients mentionnés précédemment: l’absence de type et l’absence d’affectation. La syntaxe est proche de la déclaration d’une variable:

const int X = 2;
const int Y = 4;
const int N = X + Y;
int       z = 3 * N; // z = 18, OK !

L’inconvénient principal de cette approche est que la constante est en mémoire et peut donc être involontairement modifiée (via des débordements mémoire par exemple).

En conclusion

Les deux approches sont valables, chacune est utilisable. Il faut juste être conscient de leurs limites qui peuvent créer des bogues dans vos programmes.

Instructions

Une instruction est une ligne élémentaire d’un programme C. Les instructions peuvent réaliser un calcul simple, affecter des variables… Les instructions vues dans ce cours seront:

  • les instructions simples
  • les instructions composées
  • les instructions conditionnelles
  • les instructions d’itération

Instruction simple

En C, une instruction est également une expression suivie de ; (point-virgule). Par exemple:

int x = 2;
int y = x + 4;
printf ("Bonjour !");
int z = pow (x, y);

Instruction composée

L’instruction composée ou bloc est définie en C par un groupe d’instructions qui sont encadrées par des accolades {}. Un bloc présente plusieurs intérêts:

  • grouper plusieurs instructions sous la forme syntaxique d’une seule instruction (voir le cours d’Informatique Fondamentale au semestre 7).
  • de déclarer des variables accessibles uniquement dans le bloc. Une fois sorti du bloc, les variables ne sont plus visibles.
int z;
z = 10;
{
    int x;
    x = 4; // OK x a été déclaré dans le block
    z = 3;
    z = z + x;
}
x = 0; // ERREUR ! x n'est pas accessible

Instruction conditionnelle

L’instruction conditionnelle est une instruction composée dont certaines instructions ne sont exécutées que dans certaines conditions. La syntaxe en C est la suivante:

if ( condition )
{
  // instructions1
}
else
{
  // instructions2
}
Remarque
condition est une expression booléenne. Si son évaluation donne true alors le premier jeu d'instructions est exécutée, sinon c'est l'ensemble instructions2 qui est exécuté. À noter que le bloc else{} est optionnel.

Voici quelques exemples en C:

if ( a > b )
{
  max = a;
}
else
{
  max = b;
}

if ( 2 + a <= b )
{
  printf("Inférieur !");
}

if ( a > b )
{
  if (c < d)
  {
    u = v;
  }
  else
  {
    i = j;
  }
}

// une version compacte peut être utilisée (voir conventions de codage)
if ( a ) printf("a différent de 0");
Attention
On rappelle que l'opérateur de comparaison est == est que celui d'affectation est =. Mais comme l'utilisation de ces deux opérateurs produit des expressions qui sont évaluables, elles sont utilisables dans des instructions conditionnelles... sauf que pour des raisons de lisibilité / maintenance on évitera autant que possible de faire des affectations dans des conditions.

Soient les deux fragments de programme ci-dessous, quelles sont les valeurs de t et de x ?

int x = 3, t;
if ( x == 4 )
{
  t = 3;
}
else
{
  t = 2;
}
int x = 3, t;
if ( x = 4 )
{
  t = 3;
}
else
{
  t = 2;
}

Cette deuxième instruction est fortement déconseillée (très peu d’avantage et source de nombres de bogues). Pour éviter les erreurs liés à la confusion entre les deux opérateurs, il est possible d’utiliser les Yoda-conditions, à savoir (valeur == variable). Si par inadvertance vous écrivez, if (5 = count), le compilateur génère une erreur puisque 5 n’est pas une l-value et ne peut donc pas recevoir d’affectation.

Instruction d’itération

L’objectif de cette instruction est simple: automatiser l’écriture de certaines instructions répétitives plutôt que de les écrire manuellement. On parler souvent de boucle puisqu’il s’agit d’une partie du programme qui est exécutée plusieurs fois.

Boucle for

La syntaxe classique est:

int cpt;
for ( cpt = inf; cpt <= sup; cpt = cpt + inc )
{
  // instructions
}

// Si votre compteur ne doit être accessible que
// dans la boucle, il est possible d'écrire:
for ( int cpt = inf; cpt <= sup; cpt = cpt + inc )
{
  // instructions
}

Le déroulement d’une boucle for est le suivant:

  • cpt est initialisé avec la valeur inf
  • l’expression cpt <= sup est évaluée. Si elle vaut false, on sort de la boucle (.i.e du bloc entre accolades)
  • Sinon (i.e. si elle est true) les instructions du bloc sont exécutées.
  • L’expression cpt = cpt + inc est ensuite exécutée…
  • …puis l’expression cpt <= sup est de nouveau évaluée et ainsi de suite.
Remarque
la boucle for est à utiliser en priorité lorsque le nombre d'itérations est connu.

Boucle while

La syntaxe classique en C est:

while (expression)
{
  // instructions
}

Le déroulement d’une boucle while est le suivant: tant que expression est évalué à true, le bloc d’instructions est exécuté. Ceci signifie, entre autres, que si la condition est initialement fausse, le bloc d’instructions n’est jamais exécuté.

Voici quelques exemples en C:

while ( x > 0 )
{
  x = x - 1;
  z = z + x;
}

while ( x > 0 )  x--;
// une seule instruction, les accolades sont optionnelles

Boucle do-while

La syntaxe classique est:

do
{
  // instructions
} while ( expression );

Le déroulement d’une boucle do-while est le suivant: le bloc d’instructions est exécuté une fois, puis l’expression est évaluée. Si elle vraie, on boucle, sinon on sort de la boucle. Contrairement à la boucle while même si expression est à false, les instructions du bloc sont exécutées au moins une fois.