Rappels de C : l'ABC du C selon Pascal

De Wiki du LAMA (UMR 5127)
Aller à la navigation Aller à la recherche

Voici une petite introduction/résumé de ce qu'il vous faut savoir à propos du langage C pour réaliser les projets de simulation que vous avez a entreprendre. Il ne s'agit pas d'un cours, plutôt un pense-bête, basé sur les connaissances que vous êtes supposé avoir après les cours de Pascal que vous avez eu.

L'ensemble de cet abécédaire ne prétend absolument pas être exhaustif. Il vous donnera l'information juste nécessaire. Pour avoir plus d'infos, reportez-vous a un manuel de C.

Introduction

Les variables, leurs types et leur usage

Les variables

Les types de variables définis en C sont les suivants :

Type Description Longueur (octets)
char caractère 1
int entier 2 ou 4
short entier non signé 2
long entier 8
float réel 4
double réel 8

ne supposez jamais que vous connaissez la longueur d'un type. Elle peut varier d'une machine a l'autre (en particulier pour le type int)

Déclarations

Comme en Pascal, une variable est déclarée avant d'être utilisée : Exemples :

  int x, y, z;
  float r;

notez le point-virgule à la fin des instructions (comme en Pascal)

Types personnalisés

Constantes

Affectations

En C, une affectation se fait avec l'opérateur d'affectation = :

  x = 3;
  r=4.567;

Notez le fait que les espaces ne comptent pas. Vous pouvez en mettre autant que nécessaire pour rendre votre programme lisible. D'ailleurs, comme la fin d'une instruction est signifiée par ";", les retours-chariots (ou retours à la ligne) ne comptent pas non plus. On peut écrire tout son programme sur une seule ligne si on le veut !

Conversion automatique

Lorsque vous effectuez une affectation d'une variable avec une valeur d'un autre type, il y a une conversion automatique. Par exemple :

  int i;
  i = 3.14;  // Conversion automatique 3.14 -> 3

Notez les commentaires, un // indique que ce qui suit jusqu'à la fin de la ligne est un commentaire. C'est une manière "C++" de le signifier, mais on peut l'employer dans l'environnement de travail ROOT que vous allez utiliser.

Opérateurs

Opérateurs binaires

Les opérateurs binaires sont ceux qui prennent deux variables et donnent un résultat. Ceux dont vous pourrez avoir besoin sont les suivants :

* multiplication
/ division
% modulo
= affectation
== égalité
- soustraction
addition
> supérieur
< inférieur
>= supérieur ou égal
<= inférieur ou égal
!= différent de
&& et logique
| | ou logique

Quelques exemples :

  i == 12     // la valeur de i est-elle égale à 12 ?
  (compt >0) && (document!=12)  /* deux conditions
              simultanées : compt est supérieur à 0 et document est
              différent de 12  */

Notez un nouveau type de commentaire : tout ce qui est compris entre /* et */ est considéré comme commentaire. Utile pour des commentaires longs de plusieurs lignes.

Opérateurs unaires

Ce sont des opérateurs qui n'agissent que sur une variable pour la transformer :

! NON logique
++ incrément
-- décrément
* déréférence de pointeur
& adresse

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

Ces opérateurs sont prévus pour utiliser efficacement certains registres des processeurs. L'opérateur ++ ou -- signifie qu'une variable est affectée à elle-même en lui ajoutant ou retranchant 1 :

  i++    est équivalent à    i=i+1
  i--    est équivalent à    i=i-1

On utilise le plus souvent ces opérateurs en suffixe mais il est aussi possible de les utiliser en préfixes. Par exemple :

  x = 5;
  y = x++;  // y est égal à 5, x devient égal à 6

ou bien :

  x = 5;
  y = ++x;  // y et x sont tous deux égaux à 6

Nous verrons l'utilisation très usuelle de l'opérateur ++ dans les boucles for.

Priorités entre opérateurs

Les priorités entre opérateurs sont les mêmes qu'en Pascal

Transtypage

On peut forcer une variable a devenir d'un autre type pour la durée d'une instruction. Ceci s'appelle le transtypage. On met entre parenthèses le nouveau type temporaire de la variable devant l'appel de la variable

double x = 3.1415,y;
y = (int) x;  /* pour l'opérateur d'affectation, x sera considéré comme un entier
                     -> il y aura un arrondi, y prendra la valeur 3.0 */

Ici, le transtypage a été utilisé pour effectuer un arrondi.

Classes de stockage et portée

Lorsque vous définissez une variable dans une fonction, cette variable ne sera visible que dans cette fonction. Des que vous serez retourné dans la partie de programme qui a appelé la fonction, vous ne pourrez plus utiliser la variable, ou du moins ce ne sera pas la même. On appelle cette propriété la portée d'une variable.

Si vous déclarez une variable a l'extérieur de toutes les fonctions, cette variable sera considérée comme globale, c'est à dire que toutes les fonctions la verront.

 int varGlobale;
 maFonction()
 {
   float x;
    // fonction qui fait quelque chose
    // La variable varGlobale, définie à l'extérieur, est visible ici
    // et partout ailleurs.
    // La variable x est visible uniquement à l'intérieur de cette fonction.
    // elle est locale
 }

Tableaux

On déclare un tableau avec le nombre d'éléments entre crochets comme dans

 char nom_de_personne[100];
 double x[10], y[3];

Les tableaux a plusieurs dimensions se déclarent comme suit :

 float image[100][100];

représente un tableau de réels de dimensions 100 par 100.

Lorsque vous voulez accéder à un élément d'un tableau, les indices commencent toujours à 0, ce qui signifie que le dernier indice valide est le nombre d'éléments du tableau -1. Par exemple si le tableau est déclaré comme suit :

 char a[10];

il possède 10 éléments et les noms de ces éléments sont a[0], a[1], a[2],..., a[9] (soit 10 éléments indicés de 0 à 9). Pour ce tableau, a[10] n'est pas un élément valide.

ATTENTION, si vous lisez une valeur en dehors des limites d'un tableau, cela ne porte pas particulièrement à conséquence. Vous n'obtiendrez qu'une valeur quelconque. Par contre, le fait d'écrire à l'extérieur des limites du tableau peut vous apporter une "erreur de segmentation" qui est toujours fatale. Il n'y a pas de test de la validité de ce que vous écrivez lors de l'exécution du programme. Vous voilà prévenus !!


Enfin, une remarque : les tableaux sont passés à des fonctions sous forme de pointeurs. Pour en savoir plus, jetez un oeil à ce paragraphe.

Structures

Contrôle de flux d'un programme

Structure du code

Tout groupe d'instructions en C est regroupé entre des accolades { et }. On parle de bloc d'instructions ou d'instruction composée. Ceci est strictement équivalent au bloc begin ... end en Pascal. Nous allons voir un exemple d'utilisation.

Branchements : if ... else ...

Le plus simple des éléments de contrôle du flux d'un programme est le test et le branchement, effectué par l'instruction if. La syntaxe pour une instruction if est la suivante :

  if (expression)
     instruction
  else
     autre_instruction

Si expression est vraie, instruction est exécutée, sinon c'est autre_instruction. Par exemple :

  if (k <= 5)
     a = 3 + k;
  else
     a = 1;

si k est inférieur ou égal à 5, a sera positionné à k+3, sinon a prendra la valeur 1.

Une instruction peut très bien être composée. Par exemple :

  if (k <= 5)
     a = 3 + k;
  else 
  {
     int Err = 3;
     a = 1;
     printf("erreur %d\n",Err);
     erreur = 0;
  }

La partie else contient une instruction composée qui pourrait très bien avoir des dizaines de lignes de code.

Si vous déclarez une variable à l'intérieur d'un bloc de code, cette variable sera locale au bloc, invisible à l'extérieur. C'est le cas de la variable Err de notre exemple. Sa portée est locale, elle cesse d'exister à l'extérieur du bloc de l'instruction "else".

Boucles

Boucles while

Même utilisation qu'en Pascal, lorsqu'on veut tester l'expression avant de d'entrer dans la boucle. La syntaxe pour une instruction de ce type est  :

  while (expression)
     instruction

"instruction" s'exécute tant que "expression" est vraie. Exemple :

  while (k>25) {
     printf("boucle exécutée : %f\n",k);
     k--;
  }

Notez ici que, comme les blancs et retours chariots ne comptent pas, la position de l'accolade ouvrante définissant le début du bloc qui va être exécuté dans l'instruction "while" est juste derrière celui ci. Cette notation est appelée "style Kernighan et Ritchie". C'est celui que l'auteur de ce petit ABC a l'habitude d'utiliser. Vous pouvez faire ce qui vous plaît, bien sûr, mais restez cohérent et "accro" au style que vous avez choisi.

Boucles do-while

Cette boucle est similaire à la boucle while, sauf que le test de expression est réalisé en fin de boucle. Le corps de la boucle est donc exécuté au moins une fois. La syntaxe d'une boucle do-while est la suivante :

 do
    instruction
 while (expression)

Boucles for

Utilisez une boucle for lorsque vous connaissez le nombre d'itérations. La syntaxe en est :

 for (expression_initiale ; test ; incrément)
    instruction

Il y a trois expressions à l'intérieur des parenthèses d'une boucle for. La première expression initialise le compteur de boucle. La seconde est l'expression de test. On boucle tant que l'expression est vraie. La troisième incrémente le compteur de boucle. L'exemple suivant illustre l'initialisation d'un tableau :

 short i, c[MAX];
 for ( i=0; i<MAX; i++)
    c[i] = 0;

Il s'agit d'une boucle dans laquelle i prend toutes les valeurs de 0 à MAX-1. Rappelez vous que les valeurs de l'indice d'un tableau commencent toujours à 0 pour finir à MAX-1 ou MAX est la taille du tableau. Rien ne vous empêche d'incrémenter i de plus de 1, par exemple pour aller de 5 en 5 :

for ( i=0; i<MAX; i = i+5)
   instruction(i);

Break

L'instruction break provoque la sortie d'une boucle. A utiliser avec parcimonie :

for ( i=0; i<MAX; i = i+5)
{
   erreur = Fonction(i);
   if (erreur = 0) break; // sort de la boucle si erreur
}

Fonctions

S'il n'est pas recommandé d'utiliser les instructions break à tout va, il en va autrement des fonctions. Comme en Pascal, elles permettent une lisibilité du programme beaucoup plus grande.

En C, il n'est pas fait de distinction entre procédure et fonction. Il n'y a que des fonctions ! Eventuellement, ces fonctions ne renvoient aucune valeur. Quand votre programme appelle une fonction, le contrôle est transmis à celle-ci, l'exécution se poursuivant jusqu'à la fin de la fonction ou bien jusqu'à ce qu'une instruction "return" soit rencontrée.

Une fonction est appelée par son nom avec une liste d'arguments entre parenthèses :

 MaFonction(a,b,c);

Si il n'y a pas besoin d'arguments, on appelle la fonction avec des parenthèses vides :

  MonAutreFonction();

Une fonction peut renvoyer une valeur :

 CetteFenetre = MaTroisiemeFonnction();

La variable "CetteFenetre" reçoit le résultat de la fonction MaTroisiemeFonnction() lorsque celle-ci finit de s'exécuter. Vous pouvez définir vous-même des fonctions. Voici la syntaxe à employer :

type-retourné  nom (liste_d_arguments)
{
   déclarations

   instructions
}

La valeur retournée par la fonction sera de type "type-retourné" et la variable contenant la valeur retournée apparaît dans l'instruction return :

/* type retourné : float
   fonction ayant deux paramètres
*/

float  MaFonction(int a, float phi)
{
   float f;                     // Déclare f comme float
   f = sin((float)a/10 + phi);  // Calcul de f
   return f;                    // Renvoie f comme résultat
}

Une fonction qui ne renvoie rien est déclarée de type "void", l'instruction return n'est alors pas suivie d'un paramètre.


Prototypes de fonctions

On appelle "prototype" d'une fonction l'ensemble des types de variables qu'elle prend en entrée, ainsi que le type de la variable retournée. En C++, une fonction peut avoir plusieurs prototypes. Par exemple

float  MaFonction(int a, int b)
float  MaFonction(int a, float phi)
float  MaFonction(double g)

Lorsque vous appellerez l'une de ces fonctions, par exemple avec

f = MaFonction(i, phi);

le système saura quelle fonction appeler parmi les trois possibles ayant le même nom parce que "i" étant un entier et "phi" un float, il n'y a qu'une seule possibilité. Par contre, les trois fonctions doivent renvoyer le même type de variable (ici un float).


Mais bon, tout ceci n'est que pour votre culture personnelle, ne vous embarrassez pas trop avec ces histoires. Si une fonction a besoin de paramètres différents, donnez-lui un autre nom ! Ces histoires de prototypes ne fonctionnent qu'avec le C++, pas avec le C.

Pointeurs

C'est probablement le sujet qui donne le plus de maux de tête aux programmeurs en C du monde entier !

C'est quoi un pointeur ? Dans un ordinateur, tout est physiquement écrit sous forme de nombres binaires. Ces données sont stockées dans la mémoire de l'ordinateur. On regroupe les éléments binaires (ensemble de 0 et de 1, appelés bits) sous forme de mots. Certaines machines comprennent des mots de 32 bits, certaines autres de 64, voire 128 bits. Ainsi, les mots sont stockés les uns derrière les autres dans la mémoire. Pour savoir leur emplacement, chaque élément mémoire, qui contient un mot de 32 ou 64 bits, est référencé par une adresse. La mémoire ressemble un peu à ceci :

Adresse Contenu
adresse : 1 1010011101110011
2 1110100011110110
3 0010100010110010
4 1111111111111111
5 1011001000101010
6 0110010001000100
7 1011010101000011

Les nombres binaires représentés peuvent avoir diverses significations pour le processeur, par exemple 1111111111111111 peut représenter l'entier relatif -1. Nous n'écrirons plus les nombres binaires sous forme de 0 et de 1. Le tableau précédent peut s'écrire en décimal :

Adresse Contenu
adresse : 1 -22669
2 -5898
3 10418
4 -1
5 -19926
6 25668
7 -19133

Un pointeur, c'est simplement une variable qui contient l'adresse d'une autre variable. Ainsi, une variable égale à 25668 et qui serait à l'adresse 6, aurait un pointeur de valeur 6, comme montré ci-dessus. Bon, mais ça nous sert à quoi, à nous ? L'utilisation la plus courante, dans votre cas sera le passage de tableau à des fonctions. Un tableau est une suite de variables toutes de la même taille, qui se suivent en mémoire. Supposons un tableau a[0] à a[99]. La première variable a[0] sera à l'adresse x (qui est donc un pointeur sur cette variable). Supposons que chaque variable fasse 2 mots (cas d'un réel long pour une machine 32 bits). la variable a[1] est alors à l'adresse x+2. a[99] sera à l'adresse x+2*99. Imaginez que vous ayez un tableau de 100 valeurs à passer à une fonction qui calcule la moyenne de ces 100 valeurs. Si vous essayez de passer l'ensemble du tableau, la machine va perdre du temps à recopier ces 100 valeurs pour les utiliser dans cette fonction. Ce qui est fait est que, puisque les adresses mémoires de ces 100 valeurs sont contigües, seule l'adresse (pointeur) de la première variable a[0] est passée à la fonction. Cette fonction saura que les variables suivantes sont les variables du tableau. Ceci se fait en désignant ce pointeur par le nom du tableau. Ainsi a[0] représente le premier élément du tableau, alors que a représente le pointeur sur cet élément. Bien sûr, il faut aussi envoyer à la fonction la taille du tableau car elle n'a aucun moyen de la connaître par ailleurs. Exemple :

float a[10];       // déclaration du tableau

a[0] = 12; a[1]=3;     // remplissage
a[2] = 1; a[3]=455;    // remplissage
a[4] = 45; a[5]=21;    // remplissage
a[5] = 889; a[7]=77;   // remplissage
a[6] = 1; a[9]=37;     // remplissage

moyenne(a,10);            // appel de la fonction moyenne en
                      // lui envoyant le pointeur sur le tableau a
                      // et la taille (ici 10) dudit tableau

Un dernier point. Il n'y a pas que les tableaux qui sont passés par pointeur. Il existe des pointeurs sur fonction. Ils représentent l'adresse de la première instruction d'une fonction. Vous en verrez l'utilisation lorsque vous voudrez passer l'adresse d'une fonction à un programme calculant par exemple l'intégrale de cette fonction.