Accueil ⇒ Informatique ⇒ Langage C ⇒ Ancienne initiation au C

Ancienne initiation au C

Nous allons ici apprendre le célèbre langage C, encore très utilisé aujourd'hui notamment pour ses performances. Ce cours constitue une introduction au langage dans le but de familiariser le débutant avec les concepts essentiels, communs à la plupart des langages de programmation actuels.

Introduction

Présentation

Au tout début, un petit génie eut l'idée de créer le langage de programmation révolutionnaire BCPL, plus proche du langage parlé, qui fut suivit du langage B et du Pascal. Le B fut vite abandonné, surpassé par le langage C qui fut créé par Dennis Ritchie et Brian Kernighan. Celui-ci offrait bien plus de possibilités, était plus simple, et se révélait un langage de programmation puissant tout en étant assez proche du langage parlé. Il connut un essor fulgurant et est à la base des plus grand OS, notamment Unix et ses dérivés. Le C est un langage très puissant, et même s'il prend de l'âge il reste polyvalent et a été reprit, notamment par Bjarne Stroustrup et son excellent C++, ou par Microsoft avec le C#, sans parler de l'Objective C. Il n'en reste pas moins l'un des langages les plus pratiqués aujourd'hui et fait preuve d'incontestables performances. Cette série de cours va vous apprendre à maniers les concepts intéressants du langage C pour coder vos propres applications, en mode console pour commencer. Il vous faudra cependant un bon compilateur : nous vous conseillons d'utiliser Dev-C++ sous win32 ou gcc sous GNU/Linux et Unix pour profiter des API ou SDK tels que le DirectX ou OpenGL.

Un petit Hello World.

Vous allez ici coder un premier programme qui consistera à afficher le message "Hello World !". Il s'agit d'un grand classique en programmation, et pour le fun la plupart des développeurs apprenant un nouveau langage commencent par écrire un "Hello World !", ce qui permet de découvrir les fonctions "de base" du langage Ce programme est un mythe, la plupart des grands programmeurs ont commencé par là, vous ne pouvez donc pas y échapper. Examinons le code sources suivant :

#include <stdio.h>

int main(void)
{
printf("Hello World !\n");
printf("Appuyez sur [ENTREE] pour continuer.\n");
getchar();
return 0;
}

La première ligne du programme est indispensable : elle sert à "inclure" des fonctions standards pour les utiliser dans votre programme. Une fonction, dans ce langage, est assez proche d'une fonction mathématique : c'est une suite d'acions finies sur les données passées en argument. int main(void) est la fonction principale, le point de départ de votre programme. En C, on indique le début et la fin d'une fonction par des accolades '{' et '}'. La fonction main (principale en anglais) se termine donc après return 0; par une accolade fermante.

Syntaxe et fonctions

A la fin de chaque ligne, vous voyez des points-virgules : ils sont indispensables. En effet, le C interprête les points-virgules comme la fin d'une instruction et il ne tient pas compte des espaces ou des retours à la ligne. printf est une fonction qui sert à écrire du texte : elle prend en argument une chaîne de caractère, soit du texte entre guillemets, suivie d'un nombre indéfini de variables. Pour écrire du texte à l'écran, on entre donc "printf", on ouvre des parenthèses et des guillemets, puis on entre le texte, après quoi on referme les guillemets et les parenthèses pour terminer la ligne par un point-virgule. Il existe aussi des caractères spéciaux pour utiliser des commandes spécifiques. Par exemple :

'\n' = Retour à la ligne (entrée).
'\t' = Tabulation

Vous verrez tous ces caractères spéciaux plus loin. Un programme se termine quand toutes les instructions de la fonction principale ont été éffectuées. Donc, pour l'instant, le programme se ferme dès que le texte est affiché, et on a pas le temps de voir. Il font donc faire une "pause" une fois le texte affichée. On écrit donc une phrase indiquant qu'il faut appuyer sur une touche pour terminer le programme, puis on entre l'instruction getchar(). Cette instruction attend que l'utilisateur appuie sur une touche : plus tard, nous récupèrerons la valeur de cette touche. Pour l'instant, elle sert juste à faire une pause jusqu'à ce qu'une touche soit pressée. On termine toujours la fonction principale par return 0;, qui sert à indiquer au système que tout s'est déroulé sans problème.

L'instruction return indique la valeur de retour d'une fonction, c'est à dire les données qu'elle associe aux arguments passés, comme en mathématiques où f(x) = 1/x a pour valeur de retour l'inverse de x. On ferme l'accolade pour terminer notre fonction main. Allez-y, compilez et admirez votre premier programme. C'est un début qui vous mènera loin :) Sachez cependant que le C ne supporte aucun accent en mode console (il faut pour cela effectuer un petit détournement que nous ne verrons pas ici).

Commentaires

Dans vos fichiers sources, tout caractère est considéré comme du code C, à moins que vous ne précisiez au compilateur le contraire. En fait, il est possible de "commenter" certaines parties du fichier source, ce qui veut dire que le texte choisi sera ignoré par le compilateur. Cela sert à donner des indications aux autres développeurs, mais aussi à se rappeler rapidemment l'utilité de telle ou telle portion de code. Par exemple, les sources fournis dans ce cours sont tous commentés afin de faciliter la compréhension du lecteur ;)

Pour commenter du code en C, il existe deux moyens. Le premier est celui recommandé pour rester compatible avec les anciennes versions du langage : tout code situé entre /* et */ dans le code est ignoré. Attention : le début d'un commentaire ne peut être donné qu'en-dehors de guillemets. Ce type de commentaire est "multi-ligne", car tout un paragraphe peut être commenté.

/*
* Exemple de commentaire
* multi-ligne en C
*/

Par ailleurs, il existe une seconde manière de commenter du code : plus simple, elle a été définie avec le langage C++, et depuis ajoutée aux compilateurs C. Tout texte situé après les caractères // sera ignoré pour toute la ligne, ce qui signifie que le commentaire s'étendra du double-slash jusqu'à la fin de la ligne. Idem, le double-slash doit être placé en-dehors de guillemets.

// Exemple de commentaire sur une ligne 

Notion de blocs

Le langage C emploie la notion de "bloc" pour organniser le code. Celle-ci est relativement simple : un bloc est une paire composée d'une accolade ouvrante '{' et d'une accolade fermante '}'. Tout code entre ces deux caractères fait partie du bloc. Ainsi, les instructions d'une fonction comme la fonction principale sont comprises dans un bloc, limité par les accolades ouvrante et fermante. Mais la notion de bloc ne se limite pas à ces accolades, car les blocs peuvent être "imbriqués" : ainsi, vous pouvez insérer un bloc dans un autre, obtenant un code du style :

int main(void)
{
{
{
// instructions
}

{
// instructions
}
}
{
// instructions
}
}

Attention : les blocs sont ouverts et fermés dans le même ordre, c'est-à-dire qu'un bloc ouvert dans un bloc sera égalemment fermé dans celui-ci. On peut considérer un bloc comme "descendant" d'un autre s'il est ouvert (et donc fermé) dans ce dernier, qui est alors son "parent". Cette notion de parentée est importante, entre autre, afin de disposer de variables, comme vous le verrez plus loin. Les fonctions et certaines autres instructions du langage nécessitent égalemment un bloc contenant le code associé.

Variables

Description

Nous allons poursuivre notre exploration du langage par l'étude des variables. Là encore, le concept est assez proche des variables mathématiques. En C cependant, les variables sont "typées", c'est à dire qu'une variable ne peut contenir qu'un même type de données, les types étant les nombres entiers, flottants, décimaux, les caractères ou encore les variables booléennes (oui ou non). Plus précisément, les variables sont des entitées nommées auxquelles vous associez des valeurs diverses, dépendant de leur type. Ces valeurs peuvent évoluer, d'où le nom "variable", à l'opposé des "constantes" qui sont des valeurs invariables dans votre code. Par exemple, 1 et 8.42 sont des constantes. Voici un bref aperçu des types de variables standards :

Type Description Bornes
bool Variable booléenne, "vraie" ou "fausse", 1 ou 0. true ou false
char Désigne un caractère, peut être utilisé comme entier. (8 bits) [-128; 127]
unsigned char Désigne un caractère, peut être utilisé comme entier positif. (8 bits) [0; 255]
short int Nombre entier. (16 bits) [-32 768 ; 32 767]
unsigned short int Nombre entier positif. (16 bits) [0 ; 65 535]
int Nombre entier. (integer value = nombre entier) (32 bits) [-2^31 ; (2^31-1)]
unsigned int Nombre entier. (32 bits) [0 ; (2^32-1)]
long Entier long, peut prendre des valeurs plus importantes
qu'un int. Dépend du compilateur. (32 bits)
[-2^31 ; (2^31-1)]
unsigned long Entier long, peut prendre des valeurs plus importantes
qu'un int. Dépend du compilateur. (32 bits)
[0 ; (2^32-1)]
float Réel à virgule flottante. (32 bits) [3.4e-38 ; 3.4e+38]
double Réel à virgule flottante (64 bits). [1.7e-308 ; 1.7e+308]
long double Réel à virgule flottante (80 bits). [3.4e-4932 ; 1.1e+4932]

Initialisation

Vous remarquerez que les types du C ont des bornes, c'est à dire que les variables d'un type précis ne peuvent prendre que les valeurs comprisent entre la borne inférieure et la borne supérieure du type. A noter aussi que vous pouvez ajouter signed ou unsigned avant le type de votre variable : signed est l'option par défaut, et indique que celle-ci sera signée, c'est-à-dire qu'elle pourra prendre des valeurs négatives et positives. A l'inverse, une variable non-signée est toujours positive, ce qui lui permet d'accepter des valeurs extrèmes plus élevées. Par exemple, une variable de type unsigned int pourra prendre des valeurs de [0; 10^32]. La syntaxe pour déclarer une variable est la suivante : type nom_de_variable;. Ainsi, voici comment créer une variable 'dix' :

int dix;

La variable dix ainsi déclarée, on peut lui affecter n'importe quelles valeurs entières comprises entre les bornes du type 'int'. Il est possible de déclarer plusieures variables d'un même type sur une même ligne de code, en séparant leurs noms par des virgules. Toutes les variables disposent de leurs propres opérateurs, c'est-à-dire des symboles permettant de modifier la variable ; ainsi, '=' est appelé "opérateur d'égalité", et permet d'affecter une valeur à une variable. Vous pouvez affecter une valeur à une variable n'importe quand, y compris dès sa déclaration :

int dix = 10, onze;
dix = 11;
onze = dix;
dix = 10;

Opérations sur les variables

Vous disposez désormais de variables, encore faut-il savoir les manipuler. Pour ce faire, le C propose des opérations primaires, correspondants aux principales opérations mathématiques (addition, soustraction...) jusqu'à des théories plus complexes (voir <math.h>). Plus tard, vous serez à même d'écrire vos propres fonctions pour manipuler ces variables, mais pour l'instant l'essentiel est d'apprendre à utiliser les opérateurs dits binaires. Ceux-ci se nomment ainsi car ils nécessitent deux paramètres, l'un étant votre variable, l'autre la valeur à manipuler. Par exemple, variable = 2; utilise l'opérateur binaire '=', qui affecte la valeur 2 à votre variable. Rien de bien difficile à ce niveau, les principales opérations arithmétiques s'utilisant de la même manière que dans la vie courrante. Voici la liste des principaux opérateurs binaires, suivie d'un code source d'exemple :

Opérateur Résultat
= Affecte une valeur à une variable.
Celle-ci doit être à gauche de l'opérateur.
+ Retourne la somme des deux paramètres.
- Retourne la différence des deux paramètres.
* Retourne le produit des deux paramètres.
/ Retourne le quotient des deux paramètres.
% Retourne le modulo des deux paramètres.
(Reste de la division du 1er par le 2nd)
unsigned int x;

/* Affecte la valeur 2 à x */
x = 2;

x = (2 + 2);
/* Ici, x = 4 */

x = ((2+1)*(4/2));
/* Ici, x = 6 */

x = (x - 2);
/* Ici, x = 4 */

x = (8/x);
/* Ici, x = 2 */

Ces opérations s'effectuent exactement de la même manière qu'en mathématiques, et ne présentent donc pas de réelle difficulté. Idem pour les parenthèses. Comme vous pouvez le remarquer, nous avons également utilisé x à droite de l'opérateur '=' : dans ce cas, c'est la valeur de x qui est évaluée, et la dernière opération de l'exemple ci-dessus revient au même que d'écrire x = (8/4); puisque la valeur de x avant l'opération était 4. Il peut être fastidieux de toujours avoir à utiliser l'opérateur d'égalité pour modifier une variable, aussi existe-t-il d'autres opérateurs permettant de clarifier et d'alléger la rédaction du code.

Incrémentations et décrémentations

Le sens de ces deux termes est simple : incrémenter une variable de x signifie ajouter la valeur x à la variable. Par conséquent, décrémenter une variable de x signifie soustraire la valeur x à cette variable. Il existe des opérateurs unaires, c'est-à-dire qu'ils ne prennent qu'un paramètre. L'exemple le plus fréquent est l'opérateur '++' : écrire le code variable++; entraîne une incrémentation de 1 de la variable variable. Cet opérateur est dit unaire car il ne prend qu'un seul paramètre : la variable à incrémenter. A l'opposée, il existe l'opérateur '--'. Ce sont les seuls opérateurs unaires qui seront abordés dans ce cours, mais ils sont relativement important pour pouvoir utiliser des boucles.

/* Variable i = 12 */
int i = 12;

/* Affichage de i */
printf("i = %d\n" , i);

i++;
/* Maintenant, i = 13 */

/* Affichage de i */
printf("i = %d" , i);

Voici le résultat à l'écran de ce morceau de code :

i = 12
i = 13

i++; est un exemple de Post incrémentation, i--; de Post décrémentation. Cette syntaxe vous évite d'écrire à chaque fois i = (i + 1);, ce qui est déjà un gain de temps considérable. Il existe aussi des opérateurs de pré-décrémentation et de pré-incrémentation. Leur intérêt intervient au niveau de l'évaluation : nous avons vu plus haut que la valeur d'une variable est évaluée si, par exemple, elle se situe à droite de l'opérateur d'égalité. Utiliser une post-incrémentation dans une portion de code où la variable est évaluée ajoutera 1 à celle-ci une fois qu'elle aura été évaluée, tandis qu'utiliser une pré-incrémentation ajoutera 1 à la variable avant l'évaluation. Le code suivant illustre ce cas de figure :

unsigned int i = 1, x;

x = (i++);
/* Ici, x = 1 et i = 2 */

x = (++i);
/* Ici, x = 3 et i = 3 */

Il existe d'autres opérateurs d'incrémentation et de décrémentation, binaire ceux-ci : il s'agit de +=, -=, *= et /=, correspondants aux quatre opérations arithémtiques de base. La syntaxe à utiliser est la même que pour l'opérateur =, c'est-à-dire la variable à gauche et la valeur à droite.

i += 5; /* iden que i = (i+5); */
i -= 5; /* idem que i = (i-5); */

i = 2;
i *= (++i);
/* Ici, i = 9 */

Maîtriser ces principaux opérateurs est essentiel, d'une part pour se familiariser avec la syntaxe du langage, mais surtout parceque vous les utiliserez à outrance.

Affichage de variables

Vous pouvez avoir besoin de consulter le contenu de vos variables durant le programme. Plus tard, vous serez à même de les manipuler à l'écran ou dans un fichier, mais pour l'instant le moyen le plus simple et d'utiliser la fonction printf() :

printf("La variable dix est égale à %d", dix);

"%d" est un "flag" utilisé pour appeler une variable entière dans printf() : vous pouvez taper ceci à l'intérieur des guillemets, et devez indiquer par la suite (après la virgule) les variables associées, dans l'ordre d'apparition des flags dans le texte. Voici les différents "flags" disponnibles, par type de variable :

%d int
%c char
%f double
%e float
%ld long
%s (char*) : chaîne de caractères

Exemple :

/* Affichage de variables */

unsigned int var1 = 8, var2 = 2;
char lettre = 'x';
double var3 = 10.0;

printf("%c = (%d + %d) = %f", lettre, var1, var2, var3);

Résultat :

x = (8 + 2) = 10.000000

Vous remarquerez l'affichage de la variable de type 'double'. Les variables double contiennent plusieurs chiffres après la virgule. En C, vous n'avez pas d'autres choix, pour le moment, que de laisser ceci tel quel. Les variables de ce type ne peuvent en effet s'écrire avec printf qu'en décimal (six chiffres après la virgule) ou en exponentiel (puissance de 10). Laissons donc cela de côté pour le moment, nous y reviendrons un peu plus tard.

Chaînes de caractères

Les chaînes de caractères constituent une suite de caractères accessible via une seule variable. Elles reposent sur les concepts liés des tableaux et des pointeurs, que nous n'aborderons pas encore. Néanmoins, nous avons jugé qu'il est plus intéressant pour un débutant de manipuler du texte rapidement, aussi seront-elles introduites dès maintenant et expliquées en profondeur plus loin. Une chaîne de caractère se définit comme une variable de type char dont, à la déclaration, on suffixe le nom par une paire de crochets contenant le nombre de caractères à stocker. Ainsi, char nom[20+1]; est une variable tableau de caractères dont le nom est nom et qui peut contenir 21 caractères.

Sur ces 21 caractères, seuls 20 seront accessibles car le dernier caractère d'une chaîne est toujours le caractère '\0', soit l'entier 0. Cela indique au langage la fin de chaîne. Ceci fait, on peut utiliser la variable via son nom (sans les crochets) dans le code, mais les opérateurs sur les variables vus plus haut, comme '=', ne fonctionnent pas : ainsi, nom = "Robert";, avec nom tableau de caractères, ne fonctionnera absolument pas. Pour ce faire, on utilisera des fonctions standards qui seront vues plus loin, le but de cet encart étant simplement de vous familiariser avec cette syntaxe que vous verrez plus amplement avec les tableaux.

Second programme

Le code suivant permet d'écrire en sortie le nom, l'age et la taille d'une personne stockés dans des variables. Plus tard, nous utiliserons les fonctions d'interaction avec l'utilisateur pour qu'il entre ses propres paramètres. Ne pas oublier le getchar(); avant le return 0; de la fonction principale.

#include <stdio.h>

/* Fonction principale */
int main (void)
{
/* Variables utilisees */
int age;
double taille;
char nom[20+1];

/* Affectation des valeurs*/
age = 41;
taille = 1.75;

/* La fonction strcpy(variable, "texte");
affecte la valeur "texte" à une chaîne
de caractères. Elle ajoute le 0 de fin
de chaîne automatiquement */

strcpy(nom, "Billou");

/* Affichage du resultat */
printf("Billou a %d ans et mesure %f metres.", nom, age, taille);

/* Fin d'execution */
getchar();
return 0;
}

Ce programme donne ceci :

Billou a 41 ans et mesure 1.750000 metres._

Nous n'avons pas utilisé de variable pour stocker le nom "Billou" car il s'agit de "Chaînes de caractères", et que ce concept recquiert la maîtrise des tableaux et des pointeurs. Néanmoins, vous en savez désormais assez sur les variables pour pouvoir les utiliser dans vos premiers programmes.

Contrôle du programme

Introduction

Un des points forts du langage C est qu'il permet une gestion puissante des boucles et des conditions, et les variables vont ici prendre une place essentielle. De façon schématique, le langage comporte des instructions du type :

si (condition)
faire ceci;
sinon, et si (condition)
faire cela;
sinon
faire ça;

pendant que (condition)
faire ceci;

Ce concept de programmation, désormais commun à tous les langages, a constitué une révolution à une certaine époque. Vous devrez maîtriser les diverses instructions qui suivent sur le bout des doigts, car elles constituent la base même de l'interactivité de tout logiciel :

L'Instruction if..else if..else

L'instruction if est la plus commune des instructions de branchement, c'est à dire les instructions qui n'exécutent une portion de code que si une ou plusieures conditions sont vérifiées. Son utilisation se fait de la façon suivante : if (condition) { instructions; } La condition est une valeur dite booléenne, c'est-à-dire OUI ou NON, 1 ou 0. Cette valeur peut être celle d'une variable ou la valeur de retour d'une fonction, mais la plupart du temps elle est le résultat de l'évaluation d'une condition, utilisant les opérateurs de comparaison et les opérateurs logiques. Voici un exemple d'utilisation de cette instruction :

if (taille == 2.20)
{
printf("Vous etes grand !");
}

On fournit la condition de l'instruction entre les parenthèses. Ici, si la variable 'taille' est égale à 2.20, on affichera le texte : "Vous etes grand !". Sinon, cette portion de code ne sera pas exécutée. A noter le double-égal : vous n'affectez pas de valeur à taille, vous testez sa valeur, ce qui explique l'utilisation d'un symbole différent, l'opérateur logique d'égalité '=='. Une instruction if sera exécuté si les valeurs passé en conditions prennent la valeur booléenne vraie. Voici les opérateurs de comparaison existants :

Opérateurs de comparaison

Opérateur Condition Exemple
== Egalité de deux valeurs (variable == 1)
!= Inégalité entre deux valeurs (variable != 1)
< Infériorité d'une valeur par rapport à une autre (variable < 8)
< Infériorité ou égalité d'une valeur par / à une autre (variable <= 8)
> Supériorité d'une valeur par / à une autre (variable1 > variable2)
> Supériorité ou égalité d'une valeur par / à une autre (variable >= 6)

En utilisant ceux-ci, il est possible de comparer des valeurs entre elles (le terme valeur comprend aussi bien les constantes que les variables). Il est bien entendu possible de tester plusieures conditions en même temps : pour ce faire, il est préférable de mettre chaque expression (condition) entre parenthèses, et d'associer les expressions entre elles en utilisant les opérateurs logiques que voici :

Opérateurs logiques

&& Et logique
|| Ou logique
! Non logique
^ Ou exclusif

Le Et logique '&&' permet d'associer deux conditions, rendant le test vrai si et seulement si les deux conditions sont vraies. Le Ou logiaue '||' permet de fournir plus d'une condition, mais fait que si une seule est remplie, le test sera validé. A l'inverse, le Ou logique '^', aussi appelé XOR pour eXclusive OR, ne valide le test que si l'une des conditions est vraie, mais pas plusieures. Le Non logique '!' sert à inverser la valeur d'une condition, une valeur vraie devenant fausse et réciproquement.

else if..else

Il est possible d'ajouter d'autres ensembles d'instructions à la suite d'une instruction if, de telle manière qui ne seront exécutés que si les instructions if précédentes ne l'ont pas étées. Pour ce faire, on peut utiliser else if(condition), qui consiste en une seconde instruction if qui ne sera évaluée que si l'instruction if précédente n'a pas été exécutée. Cette syntaxe permet de tester certaines possibilités uniquement si les précédentes sont fausses, ce qui optimise le code car dans certains cas ce n'est pas la peine de tester ces valeurs en fonction des tests précédents. On peut aussi utiliser else, dont les instructions ne seront exécutées que si toutes les instructions if..else if précédentes ne l'ont pas étées. On obtient ainsi des tests type si..ou si..sinon, syntaxe pratique et performante. Il est possible d'omettre les accolades si une instructions if, else if ou else ne comporte qu'une seule instruction, comme suit : if (x == 1) x = 0;. Ainsi, considérons l'exemple suivant :

if (taille > 2.00)
printf("Vous etes un geant.\n");
else if (taille > 1.80)
printf("Vous mesurez entre 1.60m et 1.80m.\n");
else if (taille > 1.60)
printf("Vous mesurez entre 1.60m et 1.80m.\n");
else if (taille > 1.40)
printf("Vous mesurez entre 1.40m et 1.60m.\n");
else
printf("Vous etes un nain.\n");

Cet exemple illustre l'intérêt de tels constructions : il n'est pas besoin de tester si la taille de l'utilisateur est supérieure à 1.60m si l'on sait déjà qu'elle est supérieure à 1.80 et qu'on a déjà effectué les instructions associées. Ainsi, dès que la taille de l'utilisateur aura étée située, les conditions suivantes ne seront pas testées, ce qui rend le code plus clair, car sachant que les conditions précédentes sont fausses on a pas à retester les deux bornes de taille, mais aussi plus performant puisqu'on effectue moins de tests. Afin de clarifier un peu les choses, voici un autre programme d'exemple plus évolué utilisant des constructions if..else if..else :

#include <stdio.h>

/* Fonction principale */
int main (void)
{
/* Variables utilisees */
unsigned int age = 21;
double taille = 1.28;
bool adulte = false;

/* Test de l'age */
if (age >= 20)
{
printf("Vous etes adulte ");
adulte = true;
}
else
printf("Vous etes un enfant ");

/* Evaluation de la taille */
if (taille > 2.00)
printf("et vous etes un geant.\n");
else if (taille > 1.80)
printf("et vous mesurez entre 1.60m et 1.80m.\n");
else if (taille > 1.60)
printf("et vous mesurez entre 1.60m et 1.80m.\n");
else if (taille > 1.40)
printf("et vous mesurez entre 1.40m et 1.60m.\n");
else if (adulte)
printf("et vous etes un nain :P\n");
else
printf("mais vous etes encore petit ;)\n");

/* Fin d'exécution */
getchar ();
return 0;
}

Après compilation, ce programme donne :

Vous etes adulte et vous etes un nain :P_

A noter l'utilisation d'une variable booléenne adulte, qui ne peut prendre que les valeurs vrai ou faux : si l'age est supérieur à 18 ans, on met cette variable sur true (vrai) ; plus tard, lors de l'évaluation de la taille, on ne se permettra de souligner la petite taille de la personne que si elle est adulte, la condition testée étant la valeur de la variable. On aurait aussi pu écrire else if (adulte == true), mais la variable étant déjà booléenne ce n'est pas nécessaire. Nous ne pouvons pas encore acquérir les valeurs de age et taille par l'utilisateur du programme, mais il ne saurait tarder.

L'instruction switch

L'instruction switch est une autre instruction de branchement, plus utilisée pour tester les valeurs possibles d'une variable. Sa syntaxe est différente, mais relativement simple. Elle s'utilise de la façon suivante : switch (variable) { case valeur: { instructions; break; } default: { instructions; break; }}. switch peut aussi servir à tester n'importe quel valeur, comme un retour de fonction ou une valeur booléenne (variable, condition...) Chaque test de valeur se rédige sous la forme case valeur: {instructions; break;}, et il est possible de prévoir un cas par défaut, comportant les instructions exécutées si aucun autre test n'a été vrai. Les accolades après les tests ne sont pas nécessaires. Voici un exemple d'utilisation :

switch (variable)
{
case 1:
printf("OK");
break;

case 2:
printf("FALSE");
break;

default:
printf("?");
break;
}

Ici, si la variable variable est égale à 1 on affiche "OK", et "FALSE" si elle est égale à 2. Si elle n'est égale ni à 1 ni à 2, c'est-à-dire à aucune des possibilitées prévues, on affiche "?". A noter l'obligation du mot clef "break" après une condition remplie : si ce mot-clef était ommis, les instructions des tests suivants seraient aussi exécutées.

L'opérateur ternaire

Plus complexe mais très utile, l'opérateur ternaire permet d'écrire de façon concise ce qui aurait put être une longue suite d'instructions if. Il se définit ainsi : (condition) ? oui : non; Si la condition condition est remplie, le code oui après le ? est évalué. Sinon, ce sera le cas du code non après les deux points ':'. Cet opérateur peut s'employer avec des variables comme avec des instructions. Voici quelques exemples concrets illustrant son utilisation :

/* Instructions : */
(age >= 20)
? puts("Vous etes adulte.")
: puts("Vous n'etes pas adulte");

/* Valeurs : */
puts ((age >= 20)
? "Vous etes adulte."
: "Vous n'etes pas adulte.");

Dans le premier cas, la première instruction puts (équivalente à printf) est exécutée si l'age est supérieur ou égal à 20, faute de quoi la seconde sera exécutée. Dans le second cas, ce ne sont plus les instructions qui sont soumies à condition mais les valeurs à afficher : idem, en fonction de l'age, une chaîne de caractères constante sera affichée plutôt qu'une autre. L'avantage de cet opérateur est qu'il peut être inclus dans n'importe quelle portion de code, même au milieu d'un appel de fonction (comme ci-dessus). Il vous sera sans doute utile, mais attention car la relecture du code n'en devient que plus ardue, ce qui est source de bugs.

Les boucles while

Nous allons maintenant voir le cas des boucles. Il s'agit d'un concept essentiel en programmation. Une boucle est un ensemble d'instructions qui s'ont exécutées répétitivement jusqu'à ce qu'une condition soit remplie, ou qu'on quitte la boucle via une instruction spécifique. Le premier cas de boucle étudé sera celui de while, qui peut être utilisée seule ou avec le mot-clef do. Nous verrons les deux possibilités ; examinons les deux syntaxes :

Utilisation de do..while

int cpt = 0;
do
{
cpt++;
printf("cpt = %d\n", cpt);
}
while (cpt < 5);

Utilisation de while

int cpt = 0;
while (cpt < 5)
{
cpt++;
printf("cpt = %d\n", cpt);
}

Résultat

cpt = 1
cpt = 2
cpt = 3
cpt = 4
cpt = 5

Le principe d'une boucle est d'exécuter un ensemble d'instructions jusqu'à ce qu'une ou plusieures conditions soient validées. Le test de ces conditions est appelé "test de sortie", et selon la syntaxe utilisée il est effectué avant ou après avoir exécuté les instructions. Dans le premier cas, comme le laisse suggérer la syntaxe du code, les instructions sont exécutées avant le test de sortie, ce qui signifie au moins une fois, à l'inverse du second où le test sera effectué en premier. Il existe un mot-clef permettant de sortir d'une boucle à partir de n'importe laquelle de ses instructions : break; La plupart du temps, il est utilisé au sein des instructions de la boucle pour quitter celle-ci si une ou plusieures conditions sont validées.

Par ailleurs, il existe un autre mot-clef, continue;, qui permet de "revenir" au début de la boucle après avoir exécuté le test de sortie. Les boucles while et do..while sont les plus accessibles du langage. Elles sont souvent utilisées comme "boucles principales" d'un programme, c'est à dire une boucle dans la fonction principale qui se répète tant que l'utilisateur n'a pas demandé à quitter. Il existe cependant un autre type de boucles, plus complet : les boucles for.

Les boucles for

Les boucles for sont un type de boucle un peu plus complexe que les boucles while, car elles ne se contentent pas de répéter une suite d'instructions soumises à condition. On peut les rapprocher de l'utilisation du symbole mathématique Sigma : en effet, la principale utilisation de telles boucles est le parcours d'une suite d'éléments. Pour ce faire, on leur fournit trois paramètres, qui peuvent tous trois être omis en fonction des besoins. En premier lieu, on peut déclarer des variables spécifiques à la boucle, c'est-à-dire qu'elles ne seront disponnible que dans le bloc d'instructions de la boucle. La plupart du temps, ces variables sont des compteurs sur le nombre d'éléments de la liste. Le second paramètre est la condition de poursuite. Le troisième paramètre constitue une suite d'instructions à exécuter en fin de boucle, qui représente la plupart du temps une mise-à-jour de l'élément courrant de la liste. Voici le schéma syntaxique d'utilisation des boucles for :

for (déclaration de variables; condition(s); instructions de fin de boucle)
{
instructions
}

Sans les premiers et troisièmes paramètres, une telle boucle est similaire à une boucle while. Cette syntaxe particulière est néanmoins très pratique, surtout dans le cas où l'on parcours une liste d'éléments, car elle permet de séparer les instructions pour chaque élément du code de parcours. Ainsi, si l'on veut reproduire l'exemple présenté ci-avant pour les boucles while, il suffit de déclarer une variable compteur cpt spécifique à la boucle, et d'incrémenter cpt en fin de boucle. Le code obtenu est bien plus clair et concis que le précédent, et le code d'affichage est distinct du code de "parcours" qui compte le nombre d'éléments.

for (int cpt = 1; cpt <= 5; cpt++)
{
printf("cpt = %d\n", cpt);
}

Le résultat est strictement le même que précédemment, mais l'écriture en est plus concise. Quelques détails diffèrent : on donne la valeur 1 à cpt plutôt que 0 car l'action cpt++; n'est plus effectuée au début mais à la fin de la boucle. Aussi, on utilise <= plutôt que < car while vérifiait la condition de poursuite en fin de boucle, tandis que for le fait au début. L'utilité de la boucle de l'exemple ci-dessus est donc d'effectuer 5 fois les instructions entre accolade, la variable cpt servant de compteur. Il vous sera impossible d'utiliser cpt en-dehors de la boucle, mais vous pouvez toujours la déclarer dans un bloc parent.

Interactions avec l'utilisateur

Vous connaissez désormais les bases pour faire un programme autonome, suivant une liste d'instructions données, mais vous ne savez pas interagir avec l'utilisateur, c'est-à-dire lui demander des informations afin d'agir en conséquence. Nous allons apprendre ci-dessous les principales fonctions et macros d'E/S (Entrée / Sortie).

La macro getchar()

Comme son nom l'indique, cette "macro", qui s'utilise comme une fonction, sert à demander à l'utilisateur d'entrer un caractère quelconque, lettre ou chiffre. getchar() retourne ce caractère, de la même manière que votre fonction main() retourne la valeur 0. Ainsi, évaluer getchar() revient à évaluer sa valeur de retour, soit le caractère tapé par l'utilisateur. On peut donc utiliser l'opérateur '=' pour affecter ce caractère à une variable :

caractere = getchar();

La valeur de la variable caractere de type char sera celle du caractère tapé par l'utilisateur. Attention : afin de valider son caractère, l'utilisateur appuyera sur [Entrée]. Or, entrée est considéré comme un autre caractère, '\n', que vous avez déjà utilisé. Ainsi, les prochaines fois que vous utiliserez getchar(), il vous faudra effectuer deux appels : un appel seul, pour passer cette entrée, et un appel affectant la valeur du caractère à votre variable. Cette macro peut servir à toutes sortes de traitements de caractères, mais n'est souvent utilisée que pour demander à l'utilisateur de taper [Entrée].

La macro gets()

getchar() ne permet d'acquérir qu'un caractère à la fois à la fois. Afin d'y pallier, nous utiliserons une autre macro, gets(), qui sert à acquérir non pas un mais une chaîne de caractères (on pourra la stocker dans un tableau comme précédemment). Imaginons que nous voulions acquérir le nom de l'utilisateur : il suffit de stocker celui-ci dans une variable char nom[20+1]; Voici un programme qui vous permettra d'obtenir celui-ci.

#include <stdio.h>

/* Fonction principale */
int main (void)
{
/* Tableau de caractère */
char nom[20+1];

/* Message de demande : */
printf("Entrez votre nom :\n");

/* Lecture du nom */
gets(nom);

/* Affichage du résultat avec
printf() et le flag %s */

printf("Votre nom est %s.\n", nom);

/* Fin d'exécution */
getchar();
return 0;
}

Mettons que vous entriez Robert, le binaire exécuté donne :

Entrez votre nom :
Robert
Votre nom est Robert._

En-dehors des chaînes de caractères, gets() permet aussi de lire des entiers ou des réels à virgule flottante. Néanmoins, l'utilisation de scanf() lui est préférable (voir plus loin) car gets() n'effectue aucun test de sécurité quant aux débordements en mémoire, ce qui peut arriver si l'utilisateur entre d'avantage de caractères que la variable nom ne peut en stocker. Dans ce cas, le programme risque de se terminer brutalement sur une erreur d'exécution, car gets() tentera d'écrire en mémoire dans une zone en-dehors de la variable. A l'opposée, scanf() dispose d'une panoplie de tests pour éviter ce genre de désagréments.

La macro puts()

Rien de bien compliqué ici, cette macro se content d'écrire le contenu d'une variable en sortie. Son temps d'exécution est meilleur que celui de printf(), mais elle n'autorise aucune mise-en-forme. Elle peut être bénéfique en termes de performances si elle est utilisée dans une portion de code appelée très fréquemment. Dans le cas contraire, on lui préfèrera printf().

puts(variable);

scanf()

De même que puts() va de paire avec gets(), le fonctionnement de scanf() est similaire à celui de printf(), mais son rôle est de lire des informations en entrée. Pour ce faire, cette fonction prend en paramètres une chaîne de caractères comportant les flags représentant les variables à lire, identiques à ceux de printf(). Ceci fait, les variables doivent être spécifiées après les guillemets, mais d'une manière différente que pour printf() : en effet, scanf() doit connaître non pas le nom de la variable mais son adresse, c'est à dire la zone en mémoire où est stockée la variable. On obtient l'adresse d'une variable en préfixant son nom d'un Et Commercial '&', mais vous verrez ce concept plus en détail en abordant les poinbteurs. Les adresses des variables doivent être séparées par des virgules et dans le même ordre d'apparition que les flags.

Modèle d'utilisation de scanf()

scanf(flags, &variable...)

Ainsi, pour lire en entrée une variable entière, on peut employer le code suivant :

unsigned int entier;

/* Lecture de la variable en entrée */
scanf ("%d", &entier);

Précision : pour lire une chaîne de caractères, il ne faut pas utiliser le & préfixant le nom de la variable, car une telle variable est en réalité un pointeur sur la zone mémoire comportant les caractères. Vous verrez ce point plus en détail en abordant tableaux et pointeurs.

Lecture d'une chaîne de caractères

char nom[40+1];
scanf ("%s", nom);

Voici donc pour les principales fonctions d'Entrée/Sortie utiles aux débutants en C. Il s'agit d'un petit encart au parcours plus théorique d'apprentissage du langage pour permettre aux néophytes de coder rapidement de petites applications interactives, et ce avant d'aborder des concepts plus complexes.

Conclusion

Conclusion

Ceci n'est pas une fin mais un début : arrivés ici, vous commencez à maîtriser le langage C et les premiers concepts à connaître. Vous pourrez poursuivre et approfondir votre apprentissage avec les cours suivants, qui vous enseignerons d'autres points importants nécessaires à une bonne maîtrise du langage. Sur ce, nous ne pouvons que vous recommander de vous entraîner encore en codant quelques programmes simples et interactifs. Pour terminer cet article, nous vous fournissons le code source d'un petit programme d'exemple, ainsi que celui d'une calculatrice simpliste en C (lien en bas de page).

personne.c

#include <stdio.h>

/* Fonction principale */
int main (void)
{
/* Variables locales */
int age;
double taille;
char nom[20+1];

/* Acquisition du nom */
printf("Entrez votre nom :\n");
gets(nom);

/* Acquisition de l'age */
printf("Entrez votre age :\n");
scanf("%d", &age);

/* Acquisition de la taille */
printf("Entrez votre taile :\n");
scanf("%lf", &taille);

/* Messages en sortie */
printf("Bonjour %s !!!\n\n");
printf("Vous avez %d ans et vous mesurez %f metres.", nom, age, taille);
printf("J'en sais des choses non ?\n");

/* Fin d'exécution */
getchar();
return 0;
}

Tableaux

Les tableaux sont bien utiles en C. Il ne s'agit pas d'un tableau graphique où l'on entre des données comme dans un tableur mais d'un tableau virtuel que vous remplissez de données. Voici un tableau :

int table_multiplication_1[10];

Ce tableau table_multiplication_1 contiendra dix entiers (int) que vous pourrez utiliser à loisir. L'avantage, c'est qu'en une seule variable (la variable tablea_multiplication_1), vous pouvez entrer dix chiffres que vous appellerez à loisir. Voici comment nous allons définir la table_multiplication_1 :

int table_multiplication_1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Dans un tableau, le premier élément n'est pas 1 mais 0. Donc, si vous voulez afficher le '3' du tableau, vous ferez :
printf("%d", table_multiplication_1[2]);

Les tableaux peuvent contenir pas mal d'éléments et vous pourrez faire des tableaux de int, de double, de float, de long, de char et de n'importe quel objet que vous créerez (structures, classes...). Nous verrons plus loin les tableaux de caractères, mais en attendant, voici un programme qui sert à afficher la table de multiplication de 4 :

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
int i = 1;
int mult4[10] = {4, 8, 12, 16, 20, 24, 28, 32, 36, 40};

printf("Appuyez sur une touche pour afficher la table"\
"de multiplication de 4.\n\n"
);
getchar();

for (;i <= 10;i++)
{
printf ("4 fois %d = %d\n", i, mult4[i-1]);
}

getchar();
return 0;
}

Petite explication ? Tout d'abord, on déclare un tableau "mult4" de dix entiers : la table de multiplication de quatre. Puis, un petit getchar () (ici, c'est permis) pour faire pause. Ensuite, on déclare une variable "i" qui servira à désigner les éléments successifs du tableau. Ensuite, on créé une boucle for qui se termine une fois que l'on a atteint les dix nombres du tableau. Dans le printf, on appelle "i" pour savoir où on en est de la table, puis, on appelle la donnée du tableau représentée par "i". Manque de bol, un tableau sous C ne commence pas à 1 mais à 0. Donc, au lieu de faire "i", on fait "i-1" ou "i--". Une fois que les dix nombres ont étés affichés, on ferme le programme par un system("PAUSE") final.

Vous pouvez faire le même type de tableau avec toutes les variables numériques, mais il reste un type de tableau qu'il vous faudra découvrir : les tableaux de caractères.

Fonctions

Les Fonctions en C sont utiles pour plusieurs raisons : elles vous permettent d'alléger votre fonction principale et elles effectuent des actions qui peuvent être effectuées à volonté (pour un texte d'intro, c'est pratique.) Il est donc nécessaire des les apprendre. Nous allons commencer ce cours par les fonctions situées dans le fichier principal, puis nous les placerons dans des fichiers ".h". Vous êtes toujours là ? Alors allons-y !

Une fonction se présente ainsi :

valeurDeRetour nomFonction(arguments) /* comme : int main (void) */ 

Qui renvoie un int, se nomme main et ne prend aucun argument. Les fonctions des langages de programmation sont proches des fonctions mathématiques : on leur passe des variables en argument pour qu'elles agissent dessus et renvoient un résultat (valeur de retour). La fonction suivante renvoie un double, se nomme fonct et on lui passe un double en argument :

double fonct(double a);

La valeur de retour (ce que renvoie une fonction) est utile quand il s'agit d'opérations mathémathiques. Si j'écris :

double variable = fonct(variable);

On va passer la variable "variable" à la fonction qui la modifira. Ensuite, "variable" sera égale à la valeur de retour de la fonction. "void" signifie vide en anglais, c'est pourquoi int main (void) signifie que l'on ne passe rien (il serai difficile de passer quelque chose qui n'existe pas encore). Une fonction n'a pas forcément de valeur de retour et peut n'avoir aucun rapport avec le programme principal. On écrit alors :

void fonction_a_part(void);

Une fonction peut avoir ses propres variables mais peut accéder aux variables globales. Une variable globale est une variable qui n'est pas définie dans une fonction (une variable à part-entière). Une variable définie dans une fonction cesse d'exister à la fin de la fonction. C'est pourquoi il existe des variables static, qui continuent d'exister après la fonction. Voici un programme d'exemple :

#include <stdio.h>
int variable_globale = 2;

int fonction (void)
{
static int variable_statique = 8;
int variable_locale = 2;
}

int main (void)
{
printf("%d marche, %d aussi mais %d n'existe plus.",
variable_globale, variable_statique, variable_locale);
}

Vous ne devriez même pas pouvoir compiler ce programme car on tente d'accéder à une variable non-statique depuis une autre fonction. Donc, la variable n'existe plus. Pour contrer ce problème, définissez des variables statiques. "variable_globale" existe toujours. Maintenant, voici un programme d'exemple qui passe le nombre entré au carré :

#include <stdio.h>

int fonction_carre (int a)
{
a = a * a;
return a;
}

int main (void)
{
int chiffre;
scanf("%d", &chiffre);
chiffre = fonction_carre(chiffre);
}

Dans la fonction "fonction_carre", on dit que la valeur passée se nommera "a". "a" est donc la valeur passée. On passe "a" au carré, puis on renvoie "a". "fonction_carre" est alors égale à "a". Donc, chiffre est égal à la valeur de retour de la fonction, et cette valeur de retour est son carré. On obtient donc ce que l'on veut. Vous pouvez également placer vos fonctions dans des fichiers séparés. Créez un fichier ".h" est coupez-collez la fonction "fonction_carré" dans ce fichier. Sauvegardez-le en l'appellant "carre.h". Puis, au début du programme, ajoutez : #include "carre.h". Le résultat est le même mais le code est plus propre, plus court et plus précis. Si vous définissez des variables globales dans un fichier ".h", elles seront variables pour toutes les fonctions.

Les classes de mémorisation

La classe de mémorisation d'une variable détermine sa portée, sa classe d'allocation et d'autres paramètres que vous verrez plus tard. La portée d'une variable dépend de l'utilisation que vous allez en faire : par exemple, une variable déclarée dans une fonction "fnct()" ne sera pas accessible depuis la fonction "main()", à moins que vous ne spécifiez le contraire. Par contre, une variable déclarée en global sera disponnible pour toutes les fonctions du programme. Cette disponnibilité ou non de la variable dans le programme se nomme sa portée. La classe d'allocation d'une variable définit le type de création voulut : par exemple, on la déclare une seule fois dans une fonction, et elle n'est pas recréée à chaque fois (allocation statique), ou alors on la déclare sans plus de précision, auquel cas la variable sera effacée une fois la fonction terminée (allocation automatique). Pour modifier la classe de mémorisation d'une variable, on ajoute un mot-clef avant son type :

int fonction1()
{
static int variableStatique = 0;
printf("%d",variableStatique);
variableStatique++;
}

Appelée plusieures fois, cette fonction affichera tour à tour 0, puis 1, puis 2... car la variable "variableStatique" est statique, et donc sa valeure reste en mémoire une fois l'appel à la fonction terminé. Si on enlève le mot-clef static, la fonction affichera toujours 0. Voici un tableau récapitulatif des différents mot-clefs disponnibles :

MOT-CLEF EFFET
auto Correspond à une classe d'allocation automatique. Il n'est valable que dans une fonction. Le mot-clef auto n'est pas nécessaire car il est appliqué... automatiquement (^^) à toute variable déclarée au sein d'une fonction.
static A l'inverse de "auto", le mot-clef statique correspond à une classe d'allocation statique : la variable demeure, même après la fin de l'exécution de la fonction. Valable également uniquement dans une fonction.
register Permet de demander au compilateur de placer la variable dans un registre de l'UC (Unité Centrale). L'avantage : les temps de traitement de la variable sont infiniement plus rapide. Cependant, on ne peut pas être sûr qu'un registre sera disponnible, et l'opérateur & pour accèder à l'adresse mémoire de la variable ne sera pas utilisable. A utiliser avec précaution.
extern Permet de déclarée une variable statique au sein d'une fonction ou d'un fichier qui sera utilisable pour toutes les autres fonctions ou tous les autres fichiers du programme.

Surcharge des noms de fonctions :

C'est une autre particularité des fonctions assez utilisée. Considérons l'exemple :

int fnt(void);
int fnt(int a);

Sont deux fonctions bien distinctes. Le fait de donner le même nom mais pas les mêmes valeurs/arguments à des fonctions s'appelle la "surcharge des noms de fonctions". Ainsi, si vous appellez fnt() sans lui passer d'argument, la première fonction sera utilisée. Au contraire si vous passez un int en argument à fnt(), la seconde sera appellée. Cette utilisation de plusieures fonctions pour un même rôle et en fonction des arguments passés peut se révéler dangereuse si vous en abusez.

Voilà, vous en savez assez sur les fonctions. Vous pouvez dès maintenant faire des programmes à base de fonctions. Elles vous seront très utiles. Par exemple, dans la Quête des Mondes - Version C, une fonction est utilisée pour afficher les scores. Ainsi, on peut les afficher n'importe où et autant de fois qu'on le veut. Je vous conseille d'utiliser beaucoup de fonctions plutôt que des instructions "if".

Voilà. Vous savez l'essentiel à savoir sur les fonctions désormais. Bientôt, nous verrons la gestion de la mémoire pour rendre vos programmes plus efficaces.

Pointeurs

Un pointeur est une sorte de variable qui désigne une autre variable par son adresse et, avant que vous ne vous disiez : "aucun intérêt !" (je suis sûr que vous venez de vous le dire), sachez qu'un pointeur est un outil primordial. Le plus gros avantage d'un pointeur, c'est qu'il permet de modifier une variable directement, en donnant son adresse mémoire, mais aussi de rediriger sa valeur tout au cours du programme. Voici un exemple :

int f = 0;
int* t = &f;
*t=10;

printf("%d, f, est egal a %d, t.\n\n", f, *t);

Vous ne le saviez peut être pas, mais quand vous utilisez scanf, vous utilisez un pointeur. Explication du code ci-dessus : on déclare une variable f = 0 (de type int). Puis, on déclare un pointeur t qui pointe vers f. Une variable pointeur est désignée par une étoile après le type, et le fichier pointé est précédé d'un &. Et oui, comme dans scanf : ce caractère sert à donner l'adresse de la variable en mémoire, et non la variable elle-même. Ensuite, on déclare que le pointeur t (*t) est égal à 10 et on affiche les variables. Alors, à votre avis, quel texte va s'afficher ?

10, f, est egal a 10, t.<

Vous pouvez appeller la variable et le pointeur. Les pointeurs sont également intéressants dans les fonctions : une fonction séparée de la fonction principale n'a normalement pas accès à ses variables. Les pointeurs sont la clef du problème. Un pointeur n'est en fait qu'une adresse qu'on utilise pour accèder aux variables, et si l'on passe en argument à une fonction l'adresse d'un variable, même si celle-ci lui est normalement inaccessible, on pourra l'éditer :

int a=0; // Variable a
int * pointeur; // Déclaration du pointeur
*pointeur = a; // On affecte l'adresse de a au pointeur

/* Maintenant, le pointeur pointe sur a
* *pointeur = valeur de a
* &pointeur = adresse mémoire de a
* pointeur = le pointeur lui-même
*/

Un pointeur aide : par exemple, il peut s'agir d"un objet en cours :toutes les fonctions modifient l'objet de même façon, mais le pointeur peut pointer vers la variable a comme vers n'importe quelle variable de même type (le traitement sera le même). Mais il existe d'autres fonctions où les pointeurs sont utiles : dans les tableaux et dans les échanges. Nous allons voire cela ci-dessous.

Pointeurs de tableaux :

L'avantage des pointeurs de tableaux, c'est qu'ils peuvent aussi servir pour les tableaux (et sans le savoir vous les avez déjà utilisés) : un pointeur pointe vers un élément d'un tableau, et que l'on peut décaler ce pointeur pour passer à l'élément suivant. Cela permettra d'agir en conséquence dans nos programmes, mais plutôt qu'un long discours, voici un exemple :

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
int tableau [3] = {1, 2, 3};
int * ptr = &tableau[0];
int i = 1;

for (; i <= 3 ; i++)
{
printf ("ptr, le pointeur, pointe sur %d\n", *ptr);
*ptr = *(ptr + i);
}

getchar();
return 0;
}

On a créé un tableau d'entiers "tableau" qui contient les nombres 1, 2 et 3. Puis, on a initialisé un pointeur "ptr" pointant vers le premier élément du tableau (je rappelle qu'un tableau commence à 0 et non à 1). Ensuite, on déclare une variable i. Une fois cela fait, on lance une boucle for qui s'achève quand "i" atteint trois et qui incrémente "i" à la fin de chaque boucle. Dans cette boucle, on affiche la valeur du pointeur, puis on le décale. Je vous explique : le pointeur renvoie à l'élément 0 du tableau, seulement ici, on ajoute "i" au pointeur. Donc, quand i=0, ptr renvoie au premier élément du tableau. Si i=1, ptr renvoie au deuxième élément (+1). Si i=2, ptr renvoie au troisième élément et ainsi de suite. Essayez ce programme, il affiche :

ptr, le pointeur, pointe sur 1
ptr, le pointeur, pointe sur 2
ptr, le pointeur, pointe sur 3
Appuyez sur une touche pour continuer. _

Décaler un pointeur dans un tableau marche pour tous les types de tableaux, y compris les tableaux de caractères. Je tiens tout de même à vous donner un autre exemple, plus concret, qui consiste à agir sur une variable locale à l'aide d'une fonction externe :

#include <stdio.h>
#include <stdlib.h>

void modif(int * e)
{
*e = 5;
return;
}

int main (void)
{
int i;
i = modif(&i);

printf("i = %d",i);
return 0;
}

Cet exemple est plus parlant : à partir d'une fonction externe il permet de modifier une variable locale. Je précise aussi que le nom d'un tableau, quel qu'il soit, est en fait un pointeur vers son premier élément, et quand vous utiliser tableau[4] c'est l'opérateur [] qui se charge de gérer le pointeur pour vous.

Echanges :

Ce n'est pas réelement une fonction des pointeurs mais je me dois de vous la décrire car je la trouve intéressante pour un début : Il s'agit ici d'utiliser des pointeurs pour échanger deux nombres. On aura besoin de deux variables, deux pointeurs et une variable tampon. Voici :

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
int fab = 2;
int tas = 4;
int * ptr1 = &fab;
int * ptr2 = &tas;
int tmp;

printf("%d et %d\n", fab, tas);

tmp = *ptr1;
*ptr1 = *ptr2;
*ptr2 = tmp;

printf("%d et %d\n", fab, tas);
getchar();
return 0;
}

Voyons voir, on déclare deux variables aux valeurs différentes, puis deux pointeurs vers ces variables. Tmp est un tampon où on stockera une ou deux données. Dans un premier temps, on affiche les deux variables. Puis, on envoie la valeur de fab (par son pointeur) sur le tampon. On envoie, toujours par les pointeurs, la valeur de fab sur tas, puis on envoie le tampon sur tas. En gros, on a échangé deux pointeurs. L'avantage, c'est qu'ainsi on a pas touché aux variables et que les pointeurs peuvent passer partout. Dans un programme, vous pourrez aiguiller des pointeurs où vous voulez et le résultat sera simple et terriblement. Pour le moment, je ne peux que vous donner des exemples sur des petits programmes sans intérêt, mais bientôt nous attaquerons des choses plus sérieuses. Le résultat est le suivant :

2 et 4
4 et 2
Appuyez sur une touche pour continuer. _

Voilà. Vous en savez déjà assez sur le C pour créer vos premiers programmes réellement utiles. Reste un point à maîtriser, un peu plus ardu, pour améliorer l'éfficacité et diminuer le poids de vos applications en mémoire.

Gestion mémoire

Pour pouvoir profiter de la gestion dynamique de la mémoire, vous devrez inclure <stdlib.h> et "malloc.h". La gestion mémoire permet, entre autre, de préparer de la mémoire pour des variables, puis de l'effacer afin de garantir des performances optimales. Considérons l'exemple suivant :

char * mem;
mem = malloc (20);

Ici, on alloue vingt octets de mémoire et on y donne l'adresse de "mem". C'est à dire que l'on obtient 20 octets, et qu'on y accède depuis "mem". Voici un autre exemple :

long mem;
mem = malloc (100 * sizeof(mem));

On a alors alloué 100 fois plus de mémoire dans mem (soir assez de place pour 100 fois la taille en mémoire de la variable "mem"). Mem est alors considéré comme une suite de 100 entiers longs. Mais le principal avantage de la gestion dynamique de la mémoire, c'est que l'on alloue de la mémoire et que l'on peut la libérer pour la remplir autrement à n'importe quel moment. Ainsi, les programmes sont plus légers et plus performants. Voici un exemple où vous pourrez voir l'intérêt de la gestion mémoire. %p indique l'adresse mémoire des objets dans printf.

char * mem1, * mem2, * mem3;
mem1=malloc(100);
printf("allocation de 100 octets en %p\n", mem1);

mem2=malloc(50);
printf("allocation de 50 octets en %p\n", mem2);

free (mem1);
printf("libération de 100 octets en %p\n", mem1);

mem3=malloc(60);
printf("allocation de 60 octets en %p\n", mem3);

Le résultat est le suivant :

allocation de 100 octets en 06AC
allocation de 50 octets en 0714
libération de 100 octets en 06AC
allocation de 60 octets en 06E8

Ne vous tracassez pas à essayer de comprendre les adresses mémoire, mais vous remarquez que, quand on a libéré la mémoire de "mem1" en 06AC, la mémoire de "mem3" est venu se "placer" juste à côté. L'avantage : vous y gagnez en performances et diminuez l'utilisation de la mémoire en effaceant les octets inutiles, ce qui peut être très utile plus tard. Pour libérer la mémoire après utilisation, vous pouvez utiliser la fonction free ainsi.

char * mem1;

mem1 = malloc(100);
printf("allocation de 100 octets en %p\n", mem1);

free(mem1);
printf("libération de 100 octets en %p\n", mem1);

Vous pourrez ainsi consommer le moins de ressources système possiblet.

Structures et unions

Structures

Les structures sont un outil bien pratique qui vous permet de logez plusieurs types d'informations sous un même nom identificateur. Mais plutôt qu'un trop long discours, voici un exemple concret :

struct personne {
char nom[20];
short age;
} jean;

Nous venons de définir une structure personne qui contient un tableau de caractères nom de vingt caractères et un entier court age. Après la définition de la structure personne on initialise une personne jean. Pour accéder aux variables de la structures il suffit d'entrer "struct.variable", ou variable est le nom de la variable à accéder dans la structure struct. Ainsi, pour accéder à l'age de jean on tape "jean.age", et "jean.nom[0]" pour la première lettre du nom. On aurrait aussi bien pu rajouter "jean, charles;" à la place de "jean;" pour déclarer plusieures personnes. Pour déclarer une nouvelle personne vous pouvez toujours faire : personne charles;.

Grâce aux structures vous pouvez déclarer et libérer un ensemble de variables la fois, ce qui peut être très utile. Les structures doivent être déclarées globalement. Les structures vous permettent d'"encapsuler" des variables pour les utiliser par la suite, mais aussi pour les créer et les libérer ensemble. Vous pouvez déclarer une structure en "statique", comme ci-dessus : dans ce cas, l'objet "jean" de type "personne" est initialisé au lancement du programme et libérer à la fin. Il sera utilisable durant tout le programme. Voici un exemple :

/* Struct.c :: Utilisation des structures */

#include <stdio.h>
#include <malloc.h>

struct personne
{
char nom[20+1];
int age;
} jean;

int main(int argc, char* argv[])
{
strcpy(jean.nom,"Jean Dupont");
jean.age=12;

printf("%s a %d ans.\n",jean.nom, jean.age);
system("PAUSE");
return 0;
}

Cependant, afin de gérer la mémoire de manière efficace et de faire des programmes plus légers, vous pouvez déclarer une structure dynamiquement. Ainsi, vous, et vous seul, déciderez de la création ou de la libération de ce cher jean. Pour ce faire, on ne créé pas un objet personne mais un pointeur sur un objet de type personne, ainsi : personne *jean. Vous ne pouvez pas encore y accèder car le pointeur est nul. Pour créer jean, vous allouerez las mémoire nécessaire, comme vu précedemment avec malloc() : jean = (personne*)malloc(sizeof(personne)); . Une fois jean créé, on peut accèder à ces données, non plus avec un point comme vu plus haut mais avec une flèche ->. Par exemple, jean->age pour accèder à la variable age de jean. Une fois que vous aurez fini de l'utiliser, vous pourrez libérer jean avec free(), comme vu précédemment. Voici un peu de code :

/* DynamicStruct.c :: Utilisation des structures en dynamique */

#include <stdio.h>
#include <malloc.h>

struct personne
{
char nom[20+1];
int age;
};

personne *jean;

int main(int argc, char* argv[])
{
jean = (personne*)malloc(sizeof(personne));

strcpy(jean->nom,"Jean Dupont");
jean->age=12;

printf("%s a %d ans.\n",jean->nom, jean->age);
system("PAUSE");
free(jean);
return 0;
}

Les structures sont une première avance dans la notion d'objet : ici, jean est un objet de type personne, que vous pouvez manipuler aussi bien qu'un int ou qu'un char. En C++, la notion d'objet est bien plus importante.

Unions

Les unions sont un dérivé des structures un peu spécial. En fait, une union est une structure où tous les membres emploient la même adresse mémoire, c'est à dire que chaque variable membres occupe le même espace mémoire que les autres. Ainsi, on ne peut utiliser qu'un seul champ d'une union à la fois (utiliser un champe efface les autres). Exemple :

union int_ou_double
{
int nombre1;
double nombre2;
} x;

/* x est un objet qui peut prendre
l'aspect d'un entier ou d'un double */


x.nombre1=2; /* x est un entier de valeur 2, le double est détruit */
x.nombre2=1.16; /* x est un double de valeur 1.16, l'entier est détruit */

Les unions sont assez peu utilisées, sauf dans des cas où l'on a besoin d'interprêter des données de différentes façons selon le contexte, ce qui relève déjà d'un stade assez avancé.

Entrées-sorties

[system] Nous allons ici nous pencher sur des points plus avancés des fonctions d'entrée sortie, principalement printf() et scanf(). Vous verrez plus bas toutes les fonctions existantes d'I/O (Input/Output, Entrée/Sortie).

Périphériques

Printf()

Exemple : printf("%+10.2f", sin(1.2));

On voit ici un exemple plus compliqué que d'habitude avec printf(). J'explique : ici, on obtient l'affichage de la valeur signée de sin(1.2) avec deux chiffres après la virgule et justifiée à droite dans un gabarit de 10 caractères (je préfère choisir un exemple très complet dès le début). Que de noms compliqués ! Pas tant que ça. Regardons de plus près comment afficher une variable avec mise en forme :

%[drapeaux][gabarit][.précision]spécificateur

(Les paramètres entre crochets ne sont pas nécessaires.) Normalement, vous utilisiez juste %spécificateur pour afficher une variable (ex : '%d'), mais vous pouvez donner à la fonction plus de précisions : les drapeaux, le gabarit et la précision. La précision est le nombre de chiffres à afficher après la virgule (dans le cas de nombres). Le gabarit est la longeur de la chaîne attribuée pour afficher la variable. Par exemple, la valeur '1' affichée avec un gabarit de dix (%10d) écrira          1, soit 9 espaces et '1'. Seulement, si on veut afficher '1' en premier et les espaces ensuite ? On utilise les drapeaux (et oui, tout est prévu). Voici différents drapeaux :

Ainsi, vous pourrez formater les données que vous afficherez en sortie. Ces valeurs ne sont valables que pour la fonction printf(). Vous verrez plus bas de nouveaux spécificateurs à utiliser (en plus de ceux que vous avez déjà vu dans la partie 1 de ces cours).

Scanf()

Nous allons ici voir comment utiliser de manière plus avancée la fonction scanf(). Voici un exemple d'utilisation de cette fonction :

Exemple : scanf("%2d",&chaine)

On voit une utilisation plus complexe de scanf() que d'habitude. Un peu comme pour printf(), on peut utiliser des gabarits avant le spécificateur. Ici, on emploie un gabarit de 2 pour un entier. Mais on peut faire encore pire :

Exemple : scanf("%20[a-z]s",&chaine)

Dans le cas de chaînes de caractères (%s), on peut utiliser des drapeaux qui indiquent quant à eux la liste des caractères acceptables (ici de 'a' à 'z'). L'instruction scanf() s'arrêtera alors dès que l'utilisateur entrera un caractère qui n'est pas dans la liste. Par exemple, scanf("%[aber]s", &chaine) reçoit tous les caractères 'a', 'b', 'e' et 'r' entrés par l' utilisateur : si celui-ci entre 'f', le scanf() prendra fin.

On peut aussi indiquer à scanf() de s'arrêter au premier caractère qui fait partie de la liste des caractères acceptables, par exemple un retour à la ligne entré par l'utilisateur. On utilise alors un accent circonflexe avant la liste des caractères acceptables.

Exemple : scanf("%[^'\n']s",&chaine)

Iici la lecture de l'entrée (clavier) prendra fin au premier caractère '\n' (retour à la ligne) rencontré, soit dès que l'utilisateur appuiera sur Entrée. Autre détail : si une étoile * est placée après le signe %, l'élément est lu mais n'est affecté à aucune variable, ce qui est utilisé pour "sauter" des données (principalement utilisé dans les fichiers). Ainsi, la fonction scanf() se révèle plus complexe et plus recherchée qu'elle n'y parraît, et c'est dans des utilisations comme celle-ci qu'elle se différencie de gets() ou getchar() habituels.

Spécificateurs

Voici différents spécificateurs qui vous seront utiles pour afficher des variables en sortie ou les acquérir en entrée (en plus de ceux que vous connaissez déjà). Vous n'êtes pas obligé de comprendre dès maintenant l'utilité de chacun, vous la comprendrez sûrement quand vous en aurez besoin.

Voilà, désormais vous êtes parfaitement capable d'utiliser les fonctions d'E/S sur les fichiers stdin (le clavier) et stdout (l'écran). Mais il reste un flux d'E/S qui va particulièrement vous intéresser à découvrir...

Fichiers

Ah, on passe à des choses plus intéressantes (enfin), et que vous utiliserez pas mal pour vos jeux et autres programmes. Nous allons donc voir comment créer des fichiers avec les fonctions de base du C. Plus tard, nous verrons comment les faire en C++ pour que vous puissiez comparer les deux.

Pour gérer les fichiers, vous utiliserez les objets FILE et différentes fonctions des bibliothèques stdlib et stdio.h. Voyons comment créer un fichier "config.txt" que l'on remplira avec une chaîne de caractères :

FILE *fichier;

fichier = fopen("config.txt","w");
fwrite("Hello Files ;)\n",sizeof(char),strlen("Hello Files ;)\n"),fichier);
fclose(fichier);

Explications : après la déclaration d'un objet de fichier fichier, on ouvre un fichier "config.txt" dans le dossier courrant avec la fonction fopen(). Cette fonction prend comme arguments l'adresse du fichier (ex: "C:\mon_fichier.txt", ou "config.txt") ainsi que le mode d'ouverture. Ce mode peut être "w" (écriture), "r" (lecture seule) ou "a" (ajout à la suite du contenu du fichier). Il existe aussi "r+" (lecutre et écriture) "w+" (écriture et lecture) et "a+" (aurevoir... heu... pardon... ajout et lecture ^^).

Les modes "r" et "a" nécessitent que le fichier existe déjà (logique), par contre, le mode "w" effacera le fichier s'il existe et en créera un nouveau. Une fois le fichier ouvert, on utilise la fonction fwrite() pour ajouter la chaîne "Hello Files ;)" au fichier. La fonction se définit ainsi : fwrite(const void *tmp, size_t taille, size_t n, FILE *pf). Une déclaration bien compliquée, mais au final ce n'est pas si dur : elle écrit n objet(s) de taille taille "pointés" par tmp dans le fichier pf. Seul détail à respecter : le premier argument est un pointeur, c'est à dire que si vous voulez passer une variable autre qu'une chaîne de caractère vous devrez donner son adresse mémoire (avec le & devant son nom) et non son nom seul. fwrite() permet d'écrire sans problème et de manière complète les differentes variables de votre programme, et ce même si elles sont de type int (ce qui n'est pas le cas des fonctions suivantes).

Pour écrire des caractères dans un fichier, vous pouvez utiliser les fonctions de sortie classiques (puts(), putchar(), printf()...) en ajoutant un f au début de leur nom et en passant un argument supplémentaire : le pointeur sur l'objet FILE (la place de l'argument est variable). Ainsi, vous pourrez utiliser les fonctions fputs(), fputchar(), fprintf() et autres pour écrire des caractères dans un fichier.

Voilà, nous avons écrit le fichier, reste à le lire :

FILE *fichier;
char *sChaine = (char*)malloc(strlen("Hello Files ;)\n")*sizeof(char));

fichier = fopen("config.txt","r");
fread(sChaine,sizeof(char),strlen("Hello Files ;)\n"),fichier);
fclose(fichier);

Rien de trop compliqué : on déclare une chaîne de caractères sChaine et on lui alloue assez de mémoire pour recevoir la chaîne entrée dans le fichier (le nombre de caractères de la chaîne * la taille d'un char = la taille de la chaine en mémoire). Après quoi on ouvre le fichier en lecture seule et on récupère la variable entrée avec la fonction fread(), qui est l'exacte réplique de fwrite()... mais pour lire les données au lieu de les écrire. Encore une fois, pour lire les données vous pourrez utiliser fgets(), fgetc() (l'équivalent de getchar() pour les fichiers) et d'autres fonctions identiques aux fonctions que vous avez déjà utilisé.

Et pour résumer, voici un petit programme qui utilise les fonctions les plus communes pour la gestion des fichiers, dont la fonction rename() pour les renommer, ou la fonction remove() pour les supprimer.

/* fichiers.c :: lecture et écriture de fichiers */

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>

FILE *fichier;
char *sChaine;
unsigned int i = 2;

int main(int argc, char* argv[])
{
sChaine = (char*)malloc(20*sizeof(char));
strcpy(sChaine,"Hello World !!!\n");
int iChaineSize = strlen(sChaine);

fichier = fopen("config.txt","w");
fwrite(sChaine,sizeof(char),iChaineSize,fichier);

// Similaire à :
// fprintf(fichier, "%s", sChaine);
// fputs(sChaine,fichier);

fprintf(fichier,"%d", i);
fclose(fichier);

printf("Ecriture du fichier terminee.\n");
system("PAUSE");

free(sChaine); i=0;
sChaine = (char*)malloc(20*sizeof(char));

fichier = fopen("config.txt","r");
fgets(sChaine,iChaineSize,fichier);

// Ou encore :
// fread(sChaine,sizeof(char),iChaineSize,fichier);
// fscanf(fichier,"%[^\n]",sChaine);
// fscanf(fichier,"%s",sChaine);
// Afficherai "Hello" car scanf s'arrête au premier
// caractère "non-acceptable" (espace...)
// Nous verrons cela plus tard.

fscanf(fichier,"%d",&i);
fclose(fichier);

printf("\nsChaine = \"%s\"\ni = %d\n\n",sChaine, i);
system("PAUSE");

// Allez, tant qu'à faire :
rename("config.txt","prefs.cfg"); // On renomme le fichier
remove("prefs.cfg"); // Suppression dudit fichier
return 0;
}

Préprocesseur

Le preprocesseur constitue une sorte de langage à part entière traité par le compilateur au moment de la compilation, avant de retranscrire le programme en langage machine. Très utile, il vous permet d'effectuer beaucoup d'actions utiles dans votre code (mise en forme, constantes...) A noter deux choses : 1/ une instruction préprocesseur commence par un dièze (#) 2/ Il n'y a pas de point-virgule à la fin d'une instruction préprocesseur. Voici les principales instructions :

#include

Et oui, vous avez déjà utilisez le préprocesseur pour inclure des librairies, et ce sans même le savoir. Voici quelques précsions tout de même sur #include : cette directive vous permet d'inclure dans votre code des fichiers .h ou .c, soit en utilisant < et > (ex : #include <stdio.h>) pour inclure des fichiers standrads du compilateur, soit en utilisant des guillemets pour inclure vos fichiers persos (ex : #include "persos.h").

#define

Utilisation : #define nom texte_substitutif

Ici, nom est comme une variable... invariable (si si) de valeur texte_substitutif : à chaque fois que vous utiliserez nom dans votre code, le résultat sera le même que si vous utilisiez une variable dont la valeur serait texte_substitutif. On appele cela des constantes, des variables de valeur constantes. L'intérêt ? Bien, voyons quelques exemples :

#define TAILLE_PERSO 160
printf("Taille du personnage de base : %d cm.",TAILLE_PERSO);

Ainsi, à chaque fois que vous utiliserez TAILLE_PERSO dans votre code, le résultat sera le même que si vous utilisiez 160. L'avantage ? Et bien, si vous utilisiez 160 comme taille du personnage tout au