Initiation au C

Cette page est destinée aux débutants en programmation et en calcul numérique: étudiants en sciences, élèves de classes prépa. et tous ceux qui veulent s'initier aux joies de la physique numérique ou plus simplement du calcul. Elle est orientée C. Ce langage et son frère le C++ prennent de plus en plus d'essor en calcul scientifique et sont déjà très répandus dans l'industrie. Pour ceux qui préfèrent le FORTRAN, je les invite dans la page "Introduction au FORTRAN".
Le langage C est d'un abord assez rébarbatif, surtout lorsqu'on aborde certains aspects comme les pointeurs ou les structures de données. Dans cette page, je présente un résumé des principales caractéristiques du C indispensables pour écrire des programmes de calcul. Comme pour le FORTRAN, je me limiterai au stricte nécessaire à un physicien. Nous ne sommes pas informaticiens, quoique...

Un peu d'histoire

Le langage C a été mis au point par Brian Kernighan et Dennis Ritchie (les célèbres K&R) des Bell Laboratories en 1972, après qu'ils se soient fait la main sur les langages A et B. Le langage C est le langage de prédilection pour le développement des systèmes. La quasi-totalité des systèmes d'exploitation comme Unix et Linux (par nature) mais aussi Windows sont écrits en C, avec quelques parties intimes en assembleur.
La définition du langage C est du domaine public. L'institut américain de normalisation, l'ANSI, a normalisé ce langage en 1989, que l'on connaît maintenant sous le vocable C-ANSI.
Il existe un compilateur C pour presque toutes les plateformes et ordinateurs. Un programme écrit en C standard (conforme à la norme ANSI) pourra être compilé sur un PC ordinaire, une station de travail SUN ou un gros IBM... Toutefois, attention! Chaque machine possède sa propre bibliothèque spécialisée et optimisée pour sa CPU. Donc, votre code sera portable que s'il reste strictement ANSI. Cependant, pour optimiser le code, on aura tendance à utiliser la bibliothèque "top" du constructeur, et là, on perd l'avantage de la portabilité: c'est un choix difficile. Pour tout dire, on sacrifie la plupart du temps la portabilité à l'efficacité.
On ne peut parler de C sans évoquer le C++. Ce dernier découle directement d'une extension fondamentale (le concept objet) du C, introduite par Bjarne Stroustrup en 1982. L'ISO a normalisé le C++ en 1998, ce qui a permis son essor dans l'industrie. Le C++ tend à supplanter le C dans tous les domaines d'applications, sauf peut être les systèmes embarqués où le besoin d'un code compact est primordial. Les programmeurs intéressés devront absolument lire le "The C++ programming language" (dernière édition de 2000) publié en 1985 par Bjarne, qui est la bible du C++ (son K&R...)
L'apprentissage du C++ est assez difficile. J'estime pour ma part que l'apport du concept objet en calcul numérique n'est pas primordial, et qu'en tous les cas, il ne compense pas le travail et l'expérience requise pour maîtriser le C++. Je n'aborderai donc pas ce langage ici.

Les bases du C

Organisation d'un programme C

Un programme C est construit autour d'une architecture assez typique. Elle ressemble à cela:

#include <stdio.h>

#include <stdlib.h>

<déclaration des constantes symboliques>

float fonction1(<liste d'arguments>);

void fonctionN(<liste d'arguments>);

<déclaration des variables globales>

int main(<liste d'arguments>)

{

< déclaration des variables locales>

<instructions>

return <code retour>

}

float fonction1(<liste d'arguments>)

{

<déclaration des variables locales>

<instructions>

return <code retour>

}

void fonctionN(<liste d'arguments>)

{

<déclaration des variables locales>

<instructions>

return

}

Un peu d'explications!
Un programme C est un assemblage de blocs de programmation que l'on appelle fonctions, dont l'une est un peu particulière. On la nomme main et c'est la fonction principale du programme, celle que l'on exécute en premier et qui appelle toutes les autres.
Toutes les fonctions sont organisées de la même façon:

Une fonction est identifiée par son prototype, qui contient:

Le corps de la fonction est borné par une parenthèse ouvrante et une parenthèse fermante.

Dans le corps d'une fonction, on trouve:

Avant de rencontrer la description des fonctions, on observe:

Les types de données

Les types de données sont assez similaires à ceux du FORTRAN. Les principaux utilisés en calcul sont:

NOTA : les int et les long peuvent être non signés (unsigned)variant donc resp. entre 0 et 216-1 et 0 et 232-1

Citons encore le char, qui stocke un caractère sur un octet. Il en existe d'autres dont nous parlerons plus loin et encore d'autres dont nous ne parlerons pas.

Les constantes

Il existe deux manières de déclarer une constante:
1 - Utiliser la directive #define, par exemple:

#define PI 3.141592

Cette directive impose au compilateur de remplacer dans tout le texte du programme la chaîne PI par la valeur 3.141592. Il est possible de rassembler tous les #define dans un même fichier header et de l'inclure dans le programme par une directive #include. Une constante symbolique n'est pas typée et prend le type de la variable d'affectation. Avec cette directive, la portée de la constante couvre tout le programme.
2 - Utiliser le mot clé const, par exemple:

const int nbmax = 1000;

const double PI = 3.141592654

Dans ce cas, la portée de la constante dépend de l'endroit de sa déclaration: globale si elle est déclarée dans la zone globale, locale lorsqu'elle est déclarée dans une fonction.

Les variables

Dans un programme C, on doit déclarer toutes les variables en précisant le type de chaque variable. Une variable est donc caractérisée par son nom et son type.
Son nom peut contenir des chiffres et des lettres. Sa longueur ne doit pas dépasser 256 caractères mais seuls les 31 premiers caractères sont significatifs. Le nom doit débuter par une lettre.

Voici quelques déclarations de variables:

int toto; // Correcte

float Rayon; // Correcte

double double; // Incorrecte, le mot double est un mot clé du C

float Rayon, Volume; // il est possible de définir plusieurs variables sur la même ligne...

int 9neuf; // Incorrecte, le nom débute par un chiffre.

Les règles de portée

Les opérateurs et les fonctions

Le C possède les opérateurs arithmétiques binaires classiques:

Notez qu'il ne possède pas d'opérateur d'exponentiation. Il faut utiliser la fonction mathématique pow().

C possède deux opérateurs unaires assez pratiques:

ATTENTION : la place de l'opérateur unaire est importante, selon qu'il soit disposé avant ou après la variable. S'il est disposé:

et cela a une grande importance. Par exemple, dans le code suivant:
x = i++; x prend la valeur de i puis i st incrémenté, i.e si i= 10, après l'exécution de l'instruction, x=10 et i =11
x = ++i; i est incrémenté puis x prend la valeur de i, et donc dans ce cas, après exécution, x=11 et i=11, ce qui n'est pas tout à fait pareil!

Le C permet quelques fantaisies du style:

Les règles de position de l'opérateur énoncées ci-dessus sont aussi valables dans ces cas.

Ces subtilités peuvent être pratiques pour écrire un code concis. Ne pas en abuser sous peine de rendre votre code illisible....

La libraire qui correspond au header math.h contient toutes les fonctions mathématiques courantes. Lorsque vous utilisez un sin ou un cos ou encore un pow, n'oubliez pas:

Les pointeurs

Avant d'aborder la suite, je suis obligé de faire une petite digression pour parler des pointeurs. Il n'est guère envisageable de faire du C sans évoquer les pointeurs et leur gestion. On en aura besoin pour manipuler certaines fonctions d'entrées/sorties et dans certains cas pour les tableaux. Alors allons-y!
Dans une mémoire d'ordinateur, une information, ar exemple un entier I, occupe un espace, un certain nombre de mots mémoire (2 pour I si I est de type int). Cet espace est identifié par le système d'exploitation par son adresse. I est stocké à l'adresse X et occupe donc l'espace mémoire X et X+1.

Un pointeur est une variable dans laquelle on stocke l'adresse d'une autre variable, adresse qui a été attribuée par le compilateur. Reprenons notre exemple.
Créons la variable p_I pour stocker l'adresse de I (adresse qui vaut X dans notre exemple). En C, on créé cette variable par l'instruction:

int I;

int *p_I;

Le * (nommé opérateur d'indirection) rappelle que la variable p_I désigne un pointeur sur une variable de type int.
Il nous reste à récupérer l'adresse de la variable I, ce que nous faisons en utilisant un autre opérateur, qui est l'opérateur d'adresse noté & ce qui nous donne: p_I = &I;
Cette instruction indique que l'on stocke dans le pointeur p_I l'adresse de la variable I (qui vaut X dans notre exemple). Attention ici, premier piège des pointeurs: la règle est de charger l'adresse d'une variable dans un pointeur de même type que cette variable. Pas question de charger l'adresse d'une variable float dans un pointeur d'int!

Voyons maintenant l'usage de l'opérateur d'indirection *. Il vous permet d'écrire indifféremment I ou *p_I pour désigner le contenu de la variable I. *p_I signifie "le contenu de la zone mémoire pointée par p_I". Et là, on comprend l'intérêt du typage du pointeur, si l'on se rappelle qu'un int occupe 2 octets et un float 4!

Les pointeurs étant des variables comme les autres, on peut leur appliquer tous les opérateurs arithmétiques, bien que très généralement on se limite à l'addition et la soustraction. Nous verrons un exemple de cela lorsqu'on abordera les tableaux.

Pour l'instant, on va en rester là avec les pointeurs. Sachez qu'il existe aussi des pointeurs de pointeurs, notés **, sources de pleins de pièges mais aussi de pas mal de subtilités du C. Voir votre cours de C habituel!

Les entrées/sorties

Voyons les instructions qui nous permettent d'afficher un résultat sur l'écran ou de saisir un paramètre depuis notre clavier. A ce stade nous en resterons à ce genre d'entrée/sortie. Nous parlerons des fichiers plus loin.
Sachez néanmoins que le C renferme un bon nombre de fonctions permettant de lire un caractère ou une chaîne de caractères depuis le clavier ou un autre périphérique d'entrée (fichier, port série, etc..) et d'écrire des caractères vers ces mêmes périphériques (on dit flux en C...). Je vous renvoie vers votre cours de C pour toutes ces subtilités inutiles dans le cadre de cette introduction.
Une autre chose importante : le C ne connaît pas les entrées/sorties "non formatées", analogues au WRITE(*,*) du FORTRAN. Il faut toujours préciser le format de ce que l'on lit ou écrit.

Lire

Dans le cadre de cette initiation, nous n'utiliserons qu'une seule fonction C pour lire nos paramètres depuis le clavier. Il s'agit de la fonction scanf(). Sa syntaxe est:

int scanf( const char *format [,argument]... );

La fonction scanf()lit la suite de caractères introduits au clavier jusqu'au caractère RETURN (ou ENTREE) puis écrit cette suite de caractères (sans le RETURN) dans la variable argument, au format fixé par format. Par exemple, si format est "%d" alors les caractères saisis seront considérés comme constituant un entier et écrit comme tel dans une variable qui devra être de type int.

La fonction scanf() retourne une valeur entière qui est nulle en cas de problème et non nulle si la lecture a été correctement réalisée. Bien souvent (ce sera le cas dans cette introduction), on ne tient pas compte du code retour de scanf().

Prenons un exemple. Il s'agit de lire un réel (au format double) depuis le clavier. Le code correspondant sera:

scanf("%lf",&Rayon);

Le format "%lf" indique qu'il s'agit d'un flottant long, ce qui correspond à un double.

L'écriture &Rayon désigne comme on l'a vu plus haut l'adresse de la variable Rayon, l'endroit où le programme doit écrire la suite de caractères saisis.

On peut également lire plusieurs données, même de type différent, comme par exemple:

scanf("%lf %d",&Rayon,&Nbpas);

La chaîne de caractères format indique le type de variable lue. Citons parmis tous les formats possibles, ceux qui nous servirons les plus:

Il en existe bien d'autres. Je vous invite à consulter votre cours de C ou bien l'aide de votre compilateur.

Pour utiliser scanf(), n'oubliez pas de faire l'include de <stdio.h>.

Ecrire

Une des fonctions du C les plus utilisées, pour debugger! Il s'agit même souvent de la première instruction C connue: la célèbre printf(). Cette fonction écrit ce que l'on veut sur notre écran.

Sa syntaxe est identique à celle de scanf():

int printf( const char *format [, argument]... );

La fonction printf() écrit la suite de caractères contenus dans la variable argument, au format fixé par format. Par exemple, si format est "%d"alors les caractères seront considérés comme constituant un entier et écrit comme tel à l'écran.

La fonction printf() retourne une valeur entière qui est négative en cas de problème et le nombre de caractères écrits en cas de succès. Bien souvent (ce sera le cas dans cette introduction), on ne tient pas compte du code retour de printf().

La chaîne format prend les mêmes valeurs que pour scanf(), auquel on peut ajouter:

"\a" pour écrire (déclencher plutôt) un ring (la cloche...)

"\n" pour passer au début de la ligne suivante. Très employé!

entre autres. L'instruction printf() peut être assez sophistiquée. Pour mieux la connaître, je vous renvoie à l'aide de votre compilateur. Avec ce que j'ai mentionné ci-dessus, on devrait couvrir tous nos besoins.

Un exemple d'utilisation de printf():

printf("\nLe volume de la sphere est de: %f cm3 \n", Volume);

Cette instruction revient à la ligne suivante sur l'écran (par la présence du \n) et écrit sur l'écran le contenu de la variable Volume en flottant. Notez l'usage des constantes chaînes (le texte dont vous souhaitez agrémenter vos résultats), entourés par des guillemets. Dans cet exemple, la valeur de Volume s'affiche à l'endroit du texte où se situe le %f.

Un exemple d'usage très courant de printf(),accompagné de scanf(), pour saisir un paramètre:

printf("Rayon de la sphere (en cm) : ");

scanf("%lf", &Rayon);

Ici, le texte d'invite à la saisie est affiché à l'écran sans saut de ligne. L'utilisateur peut saisir le rayon, qui sera récupéré par le programme comme un double.

Les particularités d'un code source C

Notre premier programme

Le programme VolumeSphere

Pour notre premier programme C, nous calculerons le volume d'une sphère en fonction de son rayon que nous indiquerons au programme.

Le code source de ce programme:

/* Declaration des headers des librairies utilisés */

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

/* Declaration des constantes */

#define PI 3.141592654

/* Programme de calcul du volume d'une sphere

Dominique LEFEBVRE TangenteX.com avril 2006

*/

int main(int argc, char *argv[])

{

// declaration des variables du programme

double Rayon, Volume;

// Saisie du rayon

printf("Rayon de la sphere (en cm) : ");

scanf("%lf", &Rayon);

// Calcul du volume

Volume = (4*PI/3)*pow(Rayon,3);

// Affichage du resultat

printf("\nLe volume de la sphere est de: %f cm3 \n", Volume);

// Fin du programme

system("PAUSE");

return EXIT_SUCCESS;

}

Il commence par une ligne de commentaire. Bonne habitude à prendre que de commenter son code. Notez qu'en C moderne on peut utiliser deux types de commentaires:

Puis viennent les déclarations des fichiers d'en-têtes (header, avec une extension .h) des librairies qui seront utilisées dans le programme. Vous notez que le nom du fichier est encadré par <>. Ce n'est pas toujours le cas. Vous pourrez voir des noms de headers encadrés par "". C'est une histoire de localisation des fichiers, sur laquelle je n'insisterai pas ici. Notez la directive include précédé d'un #.

Puis la déclaration de la constante PI à l'aide d'un #define, assez classique.

Et le corps du programme... Là application bête et méchante du cours...

On déclare les variables "double". En effet, la librairie mathématique de C ne manipule que des double! Si un jour dans un programme, vous avez des résultats surprenants, il y a fort à parier que vous avez utilisez des float avec la librairie math. La conversion implicite float <-> double est parfois déroutante.

Puis vient la saisie du rayon. Le printf() est sans piège (ici...) mais attention au scanf() et à son opérateur d'adresse (le & devant Rayon). Si vous l'oubliez, le compilateur ne dira rien. Et à l'exécution, plantage garanti! Cette instruction est un vraie piège. Rien à voir avec un bête READ de FORTRAN!

Dans le printf(), le \n qui débute le commentaire permet d'effectuer un retour à la ligne suivante.

Puis fin du programme, avec un system("PAUSE") pour vous permettre de lire votre résultat. La commande system permet de faire exécuter par le programme C une commande du système d'exploitation. Cela peut être utile, mais pas vraiment en calcul!
Dans le return, notez l'usage de EXIT_SUCCESS, une constante de la librairie standard, qui vaut 0.

Installer un environnement de développement

Là commencent les discussions et les choix... Partons du principe, comme je l'ai fait pour FORTRAN, que vous travaillez sous Windows (pour les linuxiens, si vous utilisez KDE comme moi, utilisez KDE Development avec le compilateur standard gcc, ça fonctionne impec!).

Pour développer en C sous Windows, il faut un environnement de développement, intégré de préférence, ce que l'on nomme un IDE. Il vous permet d'éditer vos codes sources, d'organiser vos projets, de compiler, de debugger, d'exécuter vos programmes et plein d'autres choses!

Si vous êtes riche, pas de problème. Procurez vous Microsoft Visual Studio, qui contient un IDE C/C++ (Visual C++ 6.0 ou supérieur) qui est très bon. C'est un outil professionnel qui vous conviendra parfaitement. Seul petit ennui, il est affreusement cher! Les physiciens, c'est bien connu, sont fauchés, surtout s'ils sont étudiants! Et même les autres...

Bref, j'ai donc cherché un IDE gratuit sur le net. Et je suis tombé sur Dev-C++. Ce n'est pas le seul et ce n'est peut être pas le meilleur. Mais ça fait quelques mois que je l'utilise avec de gros programmes bien compliqués et je n'ai pas eu le moindre souci!

Vous pouvez le télécharger sur www.bloodshed.net/dev/devcpp.html. Il vous permet de produire des applications windows ou console (en mode ligne dans une fenêtre de commandes, la forme que nous utilisons le plus pour les calculs...), et de compiler en C ou C++. Téléchargez le et installez le en suivant les consignes, c'est quasi enfantin. Il faut un peu de place sur le disque quand même: il occupe 61 Mo environ sur mon disque C, mais rien à voir avec Visual Studio qui occupe lui 400 Mo environ!

Pendant que vous faites chauffer votre ligne ADSL, téléchargez donc GnuPlot pour tracer les courbes et autres graphiques sur www.gnuplot.info.

Saisir le programme

Commençons par lancer notre IDE en cliquant sur l'icone Dev-C++ (sur le bureau ou dans la barre de lancement rapide). Un espace de travail s'ouvre avec un menu au standard Windows en haut de la fenêtre.

Nous allons créer le projet VolumeSphere dans le répertoire de votre choix (le mien est NumLab\LabPhysique\ProjetsC). Pour ce faire, allez dans le menu Fichier\Nouveau\Projet et cliquez. Une fenêtre s'ouvre pour saisir le nouveau projet. Choisissez l'option "Console Application", indiquez le nom du projet (VolumeSphere) et indiquez aussi qu'il s'agit d'un projet C puis cliquez sur OK. Dev-C++ ouvre une fenêtre pour vous permettre de choisir votre répertoire. Sélectionnez-le et enregistrez.

Puis une fenêtre de saisie s'ouvre en présentant un squelette de programme, qui ressemble à ça:

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char *argv[])

{

system("PAUSE");

return 0;

}

Nous allons l'utiliser en le complétant pour saisir notre programme d'après le code source ci-dessus.
Lorsque vous avez saisi ce code, il convient de sauvegarder le fichier en faisant Fichier\Sauvegarder. Dev-C++ ouvre une fenêtre pour vous permettre de choisir le nom du fichier source (VolumeSphere) et le répertoire de sauvegarde. Par défaut, c'est le répertoire du projet, conservez-le. Dev-C++ attribut automatiquement l'extension .c au fichier, donc ne vous en préoccupez pas.
Faites attention à la saisie du code! Comme indiqué plus haut, le C est "case sensitive": Rayon et rayon ne désignent pas la même variable! C'est un piège classique. Attention aussi à ne pas oublier le ; en fin d'instruction.

Le compiler - linker - exécuter

Rien de plus simple avec Dev-C++. Allez dans Executer\Compiler et Executer. Cliquez. Une fenêtre s'ouvre qui vous permet de suivre la compilation. Si vous avez respecté la syntaxe, il ne doit pas y avoir de problème. Lorsque le programme est compilé avec succès, une fenêtre de commande s'ouvre et le programme débute son exécution.
Il vous demande un rayon. Entrez une valeur quelconque, 1.7 par exemple. Puis il affiche le volume (20,579526 en l'occurence). Et voilà...

Nous allons pouvoir aller un peu plus loin maintenant...

Un peu plus compliqué

Les conditions

Pour aborder les conditions, il faut d'abord parler d'expression logique.

Il existe en C un type de variable boolean. Une variable de type boolean peut prend deux valeurs: true ou false Il faut savoir que true vaut 1 et false vaut 0.

C définit les expressions logiques suivantes, dans lesquelles x et y sont deux variables :

Il définit aussi les opérateurs logiques binaires ou unaires:

Munis de ces expressions logiques, on peut définir les instructions conditionnelles suivantes:

if (expression) <instruction>;

ou, s'il y a plusieurs instructions à exécuter si expression est vraie:

if (expression) then

{

<liste d'instructions>

}

La signification de ce jeu d'instructions est simple: si expression est vraie, par exemple x > 0, alors la liste d'instructions contenue dans le bloc entre parenthèses est exécutée. Sinon, le programme continu et exécute l'instruction immédiatement suivante le bloc.

Il existe plusieurs variantes d'instructions conditionnelles. Par exemple, la condition alternative:

if (expression)

{

<liste d'instructions 1>

}

else

{

<liste d'instructions 2>

}

Si expression est vraie, alors la liste d'instructions 1 est exécutée (et pas la liste d'instructions 2). Si expression est fausse, c'est la liste d'instruction 2 qui est exécutée. Il s'agit en fait de la forme complète de l'instruction conditionnelle if.
Le langage C possède une instruction très sympathique, le 'switch', qui permet de faire des traitements différents en fonction de la valeur d'une variable. En voici le principe:

switch(expression)

{

case cond1:

{

liste d'instructions 1;

}

case cond2:

{

liste d'instructions 2;

}

case cond3:

{

liste d'instructions 3;

}

default:

{

liste d'instructions en cas de défaut

}

}

Le fonctionnement est simple. Si la valeur expression est égale à une des valeurs condi déterminées, alors le bloc d'instructions correspondant (et seulement celui-ci) est exécuté. Si aucune valeur déterminée ne correspond à la valeur expression, c'est le bloc d'instructions default qui est exécuté. Pratique non!

Attention, expression doit être un long, un char ou un int. Sinon, il faut utiliser l'instruction conditionnelle :

if (expression1)

{

<liste d'instructions 1>

}

else if

(expression2)

{

<liste d'instructions 2>

}

else if (expression3)

{

<liste d'instructions 3>

}

Les boucles

La boucle for

Sa syntaxe est:

for (initial; condition; incrément)

{

liste d'instructions

}

Cette boucle est exécutée tant que condition est vraie. Par défaut, incrément vaut 1. Il peut être négatif ou positif.

Exemple de boucle for

int i;

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

{

printf("%d\n", i);

}

Dans ce cas, les parenthèses sont inutiles: je les mets par habitude, au cas où il me prendrait envie d'ajouter du code dans la boucle...

La boucle while

Sa syntaxe est:

while (condition)

{

liste d'instructions

}

Cette boucle est exécutée tant que condition est vraie. Si j'écris le code précédent en utilisant un boucle while, cela donne:

int i;

i = 0;

while (i<= 10)

{

printf("%d\n", i);

i++;

}

Dans ce cas, pas grand intérêt! Pourtant la boucle while est souvent utilisée lorsque la valeur de condition dépend d'une action extérieure (opérateur, environnement).

Notez que si condition est fausse avant d'entrer dans la boucle, la liste d'instructions n'est jamais exécutée.

La boucle do..while

Sa syntaxe est:

do

{

liste d'instructions

}

while (condition)

La boucle do while est très similaire à la boucle while. La seule différence est algorithmique. Dans la boucle do while , le bloc d'instruction est toujours exécuté au moins une fois, même si condition est fausse avant d'entrer dans la boucle. Cela peut servir dans certains traitements.

Bien sur, toutes ces boucles peuvent être imbriquées entre elles.

Les tableaux

Comme en FORTRAN, il est bien sur possible de déclarer et d'utiliser des tableaux de une ou plusieurs dimensions (vecteurs ou matrices). Mais en C, les choses ne sont pas aussi simples qu'en FORTRAN...

Voyons d'abord la déclaration et l'usage de tableaux dont on connait les dimensions et que l'on ne veut pas passer en paramètre d'une fonction.
Pour déclarer un tableau de type donné et de dimension donnée, rien de plus simple. Considérons le cas d'un vecteur de double de 10 éléments. Je le déclare en écrivant:

double tableau1[10];

Une chose très importante: en C la première position d'un tableau est la position 0 (en FORTRAN c'est 1). Les éléments de mon tableau1 sont donc numérotés de 0 à 9.

Autre chose : si vous essayez d'adresser le10eme élément du tableau, gare à vous...

Si vous voulez déclarer une matrice de float à 2 dimensions vous écrirez:

float matrice1[10][10];

ce qui vous donnera une matrice 10x10 de float.

Pour désigner un élément d'un tableau, par exemple pour l'assigner à une variable, vous écrirez:

int i,j;

float v,w;

float matrice1[10][10];

v = 0.112;

matrice1[i][j] = v;

w = matrice1[i][j];

Pour initialiser un tableau, lors de sa déclaration:

int tableau2[4] = {1,2,3,4};

int tableau3[4][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}};

Notez l'ordre: on commence par la première ligne en faisant varier le dernier indice. Les valeurs pour chaque ligne sont regroupées entre parenthèses.

Une chose importante lors de la déclaration d'un tableau. Vous devez utiliser une constante numérique pour dimensionner votre tableau. Il est par exemple interdit d'écrire:

const int imax = 10;

float tableau1[imax];

mais vous pouvez, et c'est même une très bonne habitude à prendre, écrire:

#define IMAX 10

float tableau1[IMAX]; // par convention, j'écris les constantes symboliques en majuscules.

Mais il parfois est indispensable d'utiliser les pointeurs. Voyons cela.

Il faut d'abord retenir que le nom d'un tableau désigne en fait un pointeur sur la zone mémoire qui va contenir les données stockées dans le tableau. Lorsque vous utilisez le nom du tableau comme variable sans les crochets vous utilisez en fait le pointeur sur le premier élement du tableau.

Par exemple, si je déclare le tableau tableau1[], la variable tableau1 désigne l'adresse de tableau1[0] et donc tableau1 est strictement équivalente à &tableau1[0].

Ceci étant dit, voyons comment déclarer et manipuler un tableau avec des pointeurs. Considérons d'abord le cas où l'on alloue la mémoire nécessaire au tableau à sa déclaration, ce qui suppose que l'on connaisse sa taille.

Soit

int tableau[5] = {0,1,2,3,4};

int *p_tableau;

J'initialise le pointeur p_tableau pour désigner le premier élément de tableau:

p_tableau = tableau;

Je peux accéder aux éléments du tableau en faisant un peu d'arithmétique des pointeurs. Par exemple, pour afficher les éléments du tableau je peux écrire:

int i;

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

printf("%d ", *p_tableau++);

Je peux ainsi parcourir le tableau en incrémentant ou en décrémentant le pointeur p_tableau. C'est très pratique mais attention! Si vous incrémentez le pointeur un peu trop, par exemple au delà de la taille du tableau, vous tapez dans une zone mémoire qui contient des choses inconnues... C'est le plantage assuré! Combien de bugs en C sont du à des défauts de maîtrise des pointeurs? Presque tous...

Cependant, l'intérêt d'utiliser des pointeurs pour travailler avec des tableaux ne réside pas dans ces subtilités. C'est indispensable quand on ne connaît pas la taille des tableaux lorsqu'on rédige le programme.

Imaginons que l'on veuille créer et afficher le contenu d'un tableau dont la taille varie selon un paramètre saisie par l'utilisateur (cas fréquent en stat.) ou encore selon la quantité de données lues dans un fichier (très classique en calcul). On peut déclarer le tableau avec une taille maximum. Mais alors bonjour le gaspillage de mémoire! Et dans le cas d'une lecture fichier, c'est le plantage assuré.

Il existe une autre solution très pratique en C. C'est l'allocation dynamique de la mémoire à un tableau. Je m'explique:

je déclare un pointeur sur un tableau de float par exemple

float *tableau;

au cours de mon programme, je saisis sa taille imax dans un int

int i,imax;

scanf("%d", &imax);

j'alloue la zone mémoire qui va bien pour que mon tableau puisse exister et recevoir des données

tableau = (float *)malloc((unsigned)(imax)*sizeof(float));

Cette instruction mérite explication. La fonction malloc()alloue une certaine quantité de mémoire et assigne l'adresse de début de cette zone à la variable tableau (qui est un pointeur, je vous rappelle...). La taille allouée est un entier non signé, calculé à partir de la taille d'un float (4 octets) multiplié par le nombre de float que l'on veut stocker, ici imax. Le (float *) qui figure en tête d'instruction est un cast, qui opére la transformation du pointeur sur int retourné par malloc() en pointeur sur float. Ce n'est pas clair? Pas de panique, vous trouverez un exemple d'utilisation de cette fonction dans le programme Euler qui suit!

Pour accéder à un élément de mon tableau:

float x;

i = 10; // attention, il faut que i soit < imax, sinon plantage!

x = tableau[i];

tableau[i] = 10.12;

Je peux aussi faire ce genre de boucle:

for (i=0;i<imax;i++)

printf("%f ", *tableau++);

Important: avant de sortir de mon programme, je libère la mémoire allouée à mon tableau (c'est à ces détails que l'on reconnait les bons programmeurs...):

free(tableau);

Vous trouverez dans le programme Euler des exemples de manipulations des tableaux et leur passage en paramètre d'une fonction.
ATTENTION : l'usage des fonctions malloc() et free() nécessitent un include du header <malloc.h>

Les fichiers

C'est un très vaste sujet! Comme pour le FORTRAN je me limiterai ici aux instructions nécessaires pour lire et écrire sur des fichiers disques des données de calcul.

Ouvrir un fichier séquentiel

Un fichier est désigné dans un programme C par une variable pointeur de type FILE. On le déclare par un:

FILE *fp;

où ici fp désigne le pointeur qui désignera le fichier dans toutes les instructions.

Pour l'ouvrir, on utilise la fonction fopen() dont la syntaxe est:

FILE *fopen(const char *filename, const char *mode);

où filename est une chaîne de caractères contenant le nom du fichier et mode une chaine de caractères décrivant le mode d'ouverture du fichier qui peut être :

La fonction fopen() retourne le pointeur sur le fichier (plus exactement sur la structure de description du fichier, mais peu importe...)

Lire un fichier

Pour lire des données dans un fichier disque, on utilisera une fonction qui doit vous être familière:

int fscanf( FILE *stream, const char *format [, argument ]... );

Elle s'utilise strictement de la même façon que la fonction scanf(). Le premier argument est bien le pointeur fp obtenu à l'ouverture du fichier.

Ecrire dans un fichier

Idem pour l'écriture pour laquelle nous utiliserons la fonction fprintf(), dont la syntaxe est:

int fprintf( FILE *stream, const char *format [, argument ]...);

Elle s'utilise strictement de la même façon que la fonction printf(). Le premier argument est bien le pointeur fp obtenu à l'ouverture du fichier.

Fermer un fichier

Encore plus simple puisqu'on ferme le fichier en appelant fclose(), dont la syntaxe est:

int fclose( FILE *stream);

Ne pas oublier de fermer les fichiers dans ses programmes...

Exemple d'utilisation

Supposons que nous ayons stocké les résultats d'un calcul dans deux vecteurs x et y de dimension n=50. Nous voulons sauvegarder ces valeurs dans un fichier pour les plotter avec un logiciel comme Matlab, Scilab ou GnuPlot.

Voilà le bout de code correspondant. On supposera que le fichier data.txt n'existe pas.

int i;

float t[50], x[50], y[50];

FILE *fp;

fp=fopen("data.txt","w+");

for(i=0;i<50; i++)

fprintf(fp,"%f %f %f\n", t[i], x[i], y[i]);

fclose(fp);

OUF ! Nous voilà au bout du chemin, avec ce qu'il faut pour faire enfin autre chose que de l'informatique. Passons maintenant à un petit programme exemple qui reprend toutes ces notions....

Le premier programme de calcul - La méthode d'EULER

Résolution d'une équation différentielle par la méthode d'Euler

Pour illustrer cette présentation, j'ai choisi la méthode d'Euler. C'est la méthode la plus simple, à défaut d'être la plus efficace, de résolution d'une EDO (équation différentielle ordinaire ou ODE comme disent les anglophones). Cette méthode est présentée dans tous les cours d'analyse numérique. Rappelons là brièvement.

Soit à résoudre une EDO avec une condition initiale de type:

y' = f(x, y)

y(x0) = y0

La méthode d'Euler consiste à estimer l'accroissement de y entre les abscisses xi et xi+1 par un développement de Taylor d'ordre 1 (on dit que c'est une méthode à un pas):

yi+1 = yi + h*f(xi, yi) où h = xi+1 - xi

Cette méthode n'est pas très précise. De plus, elle génère et amplifie les erreurs. Mais dans les cas simples, si f n'est pas trop variable et h très petit, elle sera suffisante en première approximation.
Pour traiter des EDO d'ordre supérieur, on procède à un changement de variables qui aboutit à un système d'équations différentielles. Par exemple, si l'on considère l'EDO:

y'' = f(x,y,y')

avec y(x0) = y0 et y'(x0) = y'0

On peut la décomposer en un système de 2 équations du premier ordre en posant y' = u(x), d'où:

y' = u(x)

u'(x) = f(x,y,u)

et résoudre ce système par la méthode d'Euler.

Un exemple

Nous allons illustrer l'utilisation de la méthode d'Euler par l'étude de la dynamique du pendule simple. Comme vous le savez sans doute, l'équation différentielle de son mouvement est :

d2(θ)/dt2 = -(g/l)sin(θ)

où l = longueur du fil de suspension. Je ne présente pas g...
Pour appliquer la méthode d'Euler, je vais décomposer cette EDO en un système à 2 équations en posant:

d(θ)/dt = ω la vitesse angulaire

a(θ) = d(ω)/dt l'accelération angulaire, qui vaut -(g/l)*sin(θ)

L'application du schéma d'Euler me donne les équations:

θi+1 = θi + h*ωi             (1)

ωi+1 = ωi + h*ai = ωi + h*(-(g/l)sin(θi))            (2)

Nous retrouverons tout ce petit monde dans le programme ci-dessus:

Le programme

//*******************************************************************************

//*

//* Programme d'essai de la méthode d'Euler sur l'équation différentielle

//* du pendule simple.

//*

//* Rappel : le système différentiel traité est d(theta)2/dt2 = -(g/l)*sin(theta)

//* Dominique Lefebvre - TangenteX.com - Avril 2006

//*

//*******************************************************************************


//*******************************************************************************

//* Inclusion des headers standards


//*******************************************************************************

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <malloc.h>


//*******************************************************************************

//* Declaration des constantes

//*******************************************************************************

#define N 10 // Nombre de cycles de calcul

#define P 1000 // Nombre de pas de calcul par cycle

#define G 9.81 // Acceleration de la pesanteur

#define PI 3.141592654


//*******************************************************************************

//* Déclaration des variables globales.

//* La variable l est donc partagée par toutes les routines du programme

//*******************************************************************************

double l; // Longueur du pendule (en m)


//*******************************************************************************

//* Routine de description du système différentiel a integrer

//* Ici, en l'occurence, il s'agit du système du pendule simple

//*******************************************************************************

void Derivee(double X[], double DX[])

{

DX[0] = X[1];

DX[1] = -G*sin(X[0])/l;

}

//*******************************************************************************

//* Routine d'implémentation de la méthode d'Euler du premier ordre

//*******************************************************************************

void Euler(double x[], double y[], double DX[], double dt, int i)

{

x[i+1] = x[i] + DX[0]*dt;

y[i+1] = y[i] + DX[1]*dt;

}


//*******************************************************************************

//* Corps du programme principal

//*******************************************************************************

int main(int argc, char *argv[])

{


//* Declaration des variables

int i;

double theta,h, X[2], DX[2];

double *x, *y, *t;

FILE *fp;


//* Allocation de la mémoire pour les tableaux de calcul

x = (double *)malloc((unsigned)(N*P+1)*sizeof(double));

y = (double *)malloc((unsigned)(N*P+1)*sizeof(double));

t = (double *)malloc((unsigned)(N*P+1)*sizeof(double));


//* Initialisation des constantes

l = G/(4*PI*PI); // j'utilise une longueur qui m'arrange

theta = 2*PI*sqrt(l/G); // periode du pendule

h = theta/P; // pas temporel pour le calcul


//* Determination des conditions initiales

x[0] = 60*PI/180; // angle initial du pendule de 60° converti en radians

y[0] = 0.0; // vitesse initiale du pendule nulle


//* Calcul

for (i=0; i<N*P; i++)

{

t[i] = i*h; // incrementation du temps


// Calcul des derivees, qui sont retournees dans DX

X[0] = x[i];

X[1] = y[i];

Derivee(X,DX);


// Application de la méthode d'Euler, qui calcule x(i+1) et y(i+1) en fonction

// de x(i), y(i),dx(i) et dy(i)

Euler(x, y, DX,h, i);

}


// Sauvegarde des résultats dans un fichier texte pour tracer par GnuPlot

// Ce fichier se nomme EulerC.dat. Il est cree dans le repertoire courant (celui dans lequel

// est lance le programme.

// S'il n'existe pas, il est cree. S'il existe, son contenu est ecrase.

fp=fopen("EulerC.dat","w+");

for(i=0;i<N*P; i++)

fprintf(fp,"%f %f %f\n", t[i], x[i]*180./PI, y[i]);

fclose(fp);


//* Liberation de la memoire et sortie

free(x);

free(y);

free(t);

system("PAUSE");

return EXIT_SUCCESS;

}

Pour télécharger le code source Euler.cpp. Ce fichier porte une extension cpp, mais vous pouvez le compiler en C en la modifiant en .c

Pour aller plus loin

Si vous souhaitez plus de renseignements, voici quelques références:

La programmation C

La physique numérique en C++

Il y a un bon bouquin d'introduction à la simulation numérique en C++ chez Dunod, de Pironneau, Hecht et Danaila, qui permettra aux accros du C++ d'aborder la physique numérique avec leur langage favori...
Signalons aussi le "Numerical Methods for Physics" de A. Garcia chez Prentice Hall, pour le C++ et Matlab. Très bon bouquin d'introduction à la physique numérique.


Contenu et design par Dominique Lefebvre - www.tangenteX.com avril 2006   Licence Creative Commons    Contact : PhysiqueX ou

Cette œuvre est mise à disposition selon les termes de la Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Pas de Modification 3.0 France.