TP C1 2324v2 (obligatoirement dans un Terminal Linux, ou MacOS)

1.   Le langage C

Il est apparu en 1972 bien avant le langage Python (1991) ou Java (1995) - lire (en dehors du TP encadré) cet historique (uniquement parties I et II) - et a été initialement conçu pour remplacer l'assembleur dans l'écriture d'un nouveau système d'exploitation : Unix (ancêtre de Linux). C'est encore de nos jours un des langages les plus utilisés dans l'industrie, avec le C++, notamment sur les kits de développement pour cartes à micro-processeurs.

Ce langage (lire uniquement les parties 1, 2, 3 en dehors du TP encadré) est non orienté objet et est très proche du matériel (pas de public/private, pas de classes/objets, pas d'héritage/d'abstraction, pas de surcharge, pas de ramasse-miettes, ...).

La bonne nouvelle pour ceux qui connaissent Java est que pour tout ce qui n'est pas orienté objet, Java avait conservé la syntaxe du C (voir quelques comparaisons - lire uniquement les 3 premiers triples-cadres Java et C -) ; la syntaxe leur sera donc globalement familière.
De toute façon, vous pratiquerez tous le C dans les unités Algorithmique et Systèmes d'Exploitation, et c'est encore le meilleur moyen d'apprendre que de l'utiliser en situation.
Les documents Learning C from Java et C for Python Programmers (sur la page de l'unité) peuvent servir de référence, en cas de besoin.

Cette première expérience d'apprentissage en autonomie (assistée) d'un nouveau langage de programmation vous sera utile plusieurs fois dans votre scolarité puis dans votre carrière.
Et quel que soit le langage, l'ennemi public numéro un en programmation reste la duplication de code !

2.   Le cycle de développement

Cette partie rassemble toutes les commandes linux nécessaires pour créer/modifier/compiler/exécuter des programmes en C.
Lisez-la une première fois, puis reportez-vous y dans la partie 3 qui vous demande d'écrire votre premier programme en C.
Il est bien évident que vous ne devez pas utiliser d'environnement de développement (IDE) puisqu'un des objectifs ici est de vous apprendre à développer à la ligne de commande dans un terminal pour bien comprendre et différencier toutes les étapes.

La première chose essentielle à savoir est que le C est un langage dit "compilé". Le programme "compilé" s'exécutera directement sur le processeur (matériel) de l'ordinateur et non sur un processeur virtuel (la machine virtuelle Java) ni ne sera interprété (comme en Python). Le code compilé ne sera donc pas portable entre différentes machines ou systèmes d'exploitation : il faudra recompiler le source C sur chaque plateforme où l'on voudra exécuter le programme.

D'autre part, le cycle de développement classique est
2.A. d'utiliser un éditeur de texte pour créer le source C, puis
2.B. de taper une commande de compilation dans une fenêtre Terminal, puis
2.C. de lancer l'exécution du programme compilé dans ce même terminal (voir aide pour les commandes Linux).

Ne rien taper pendant cette partie 2 : s'y reporter pour faire l'exercice 3 et les suivants.  

2.A. Pour créer ou modifier le source C du programme, il faudra utiliser un éditeur de texte (kwrite ? emacs ? vi ? notepadqq ?) — mais pas un traitement de texte (comme Word) ! — ; nous supposerons que le fichier s'appelle exercice.c par exemple.

Ne pas oublier de sauvegarder le fichier avant de passer à l'étape suivante.

Bon à savoir : sous notepadqq, en enregistrant dès le début son fichier sous un nom se terminant par .c, on obtient la coloration syntaxique qui est une aide agréable.  

2.B. Pour compiler le fichier source en vue de produire un fichier exécutable, il faut ouvrir une fenêtre Terminal, aller dans le bon répertoire, puis taper une commande qui lance le compilateur Gnu : gcc, avec les bons paramètres. Comme cette commande nécessite des options et pour ne pas avoir à les retaper à chaque fois, il est souhaitable de se créer sa propre commande de compilation mycc une seule fois au début du TP, en tapant (par copier/coller) la commande suivante dans la fenêtre Terminal :
- version bash :   mycc() { echo gcc: ; gcc "$@" -std=c99 -Wall -Wextra -lm ; echo Done. ;}
- version csh ou tcsh :   alias mycc  'echo -\> gcc : ; gcc \!* -std=c99 -Wall -Wextra -lm ; echo Done.'

Pour connaître votre shell, il suffit de taper : echo $SHELL

Dans les exercices suivants, lorsqu'on voudra compiler et créer l'exécutable souhaité, il suffira de taper à chaque fois (en adaptant le nom du fichier) :
mycc exercice.c -o exercice.exe
L'option -o permet de spécifier le nom du programme exécutable que l'on veut créer.

S'il y a des erreurs de compilation, l'exécutable n'est pas créé et il faut recommencer à l'étape 2.A. Comme en java, il est important d'essayer de comprendre les messages en anglais ; si ce n'est pas le cas, demandez !
Quand il n'y a plus d'erreur de compilation :

2.C. Pour exécuter ce programme, il suffira de taper dans le terminal la commande (en adaptant le nom du fichier) :
./exercice.exe
ce qui lance automatiquement la fonction main du programme (voir 3.A. ci-dessous).
Le ./ est nécessaire car, par défaut, Linux ne va pas chercher les commandes dans le répertoire courant.  
IMPORTANT !   Si votre programme ne s'arrête jamais, il est possible de l'interrompre en tapant CTRL-C.
Lors de l'exécution, vous n'aurez quasiment qu'un seul message d' erreur : Segmentation fault. (ou Erreur de segmentation.)
Cela signifie généralement que vous avez accédé à une case en dehors d'un tableau (indice < 0 ou >= à la taille), ou bien que vous avez provoqué une récursion infinie.

2.D. Partie facultative, mais fortement conseillée avant la fin du TP :

Si vous vous lassez de retaper l'alias de commande mycc au début de chaque TP (ou de chaque session de travail, ou si vous fermez malencontreusement la fenêtre Terminal), vous pouvez l'enregistrer dans un fichier (de script) qui se lance automatiquement à chaque fois que vous vous connectez. Il suffit d'ajouter avec un éditeur de texte la commande alias indiquée au 2.B. dans le fichier « invisible » qui porte exactement le nom .cshrc et qui se trouve à la racine de votre compte.
Attention si vous êtes en bash : le fichier s'appelle .bashrc
Vous pouvez donc taper notepadqq ~/.cshrc & puis y copier/coller la commande d'alias, puis sauvegarder et ouvrir une nouvelle fenêtre Terminal.

3.   Premier programme en C

Pour faire cet exercice, vous aurez besoin des commandes expliquées dans la partie 2 ; il faudra donc s'y reporter régulièrement.

Ouvrez un éditeur de texte (voir les exemples ci-dessus) ; n'oubliez pas d'ajouter le caractère & à la fin de la ligne de commande, sinon le terminal restera bloqué tant que vous ne serez pas sorti de l'éditeur. Dans l'éditeur, tapez simplement la ligne :
// mon premier programme en C
puis enregistrez le fichier sous le nom premier.c dans un répertoire créé pour ce TP.
La couleur de la ligne change-t-elle ?

Information (sur les commentaires) : Il n'y a pas de commentaires de documentation (pas de Javadoc), mais les formes // et /* */ fonctionnent comme en java.

Bon à savoir : Si ce n'est pas déjà le cas, il est utile de demander l'affichage des numéros de ligne pour faciliter le repérage des erreurs que le compilateur pourrait signaler ...

La deuxième chose essentielle à savoir est qu'il faut toujours un programme principal pour pouvoir exécuter un programme écrit en C : il s'agit de la fonction int main( void ).
main est son nom obligatoire, int signifie qu'elle retourne un entier au système d'exploitation (en fait, un code d'erreur valant 0 si tout s'est bien passé), et void pour signifier qu'on ne veut passer aucun paramètre (même si ça compile sans le mettre, cela permet au compilateur d'effectuer plus de vérifications et donc de diminuer les erreurs à l'exécution).  

3.A. Complétez premier.c avec les lignes suivantes :
int main( void )
{
  printf( "Mon premier programme C !" );
  return 0;
} // main()
La fonction printf qui accepte un paramètre de type chaîne de caractères est dans ce cas l'équivalent de System.out.print. Nous verrons à l'exercice 4. d'autres possibilités de printf.

Sauvegardez et compilez. L'erreur suivante doit apparaître à propos de printf :
warning: incompatible implicit declaration of built-in function 'printf'
Explications :
premier.c:4 signifie que l'erreur est signalée à la ligne 4 du fichier premier.c. Il y a donc un problème avec la fonction prédéfinie (built-in) printf car on ne l'a pas déclarée.
premier.c:5:10 signifierait que l'erreur serait détectée à la colonne 10 de la ligne 5 du fichier premier.c.  

3.B. Pour corriger cela, ajoutez cette ligne au début du fichier :
#include <stdio.h>

Explication : De même qu'il faut importer des classes en Java, il faut inclure des déclarations en C. En effet, la fonction printf est déclarée (avec beaucoup d'autres fonctions) dans le fichier stdio.h (STD veut dire STandarD, IO veut dire Input/Output, et H veut dire Header ou entête).
L'ensemble des fichiers d'entête de la C Standard Library est consultable ici.
Sauvegardez et compilez. Il ne devrait plus y avoir d'erreur. Exécutez. Un défaut d'affichage ?  

3.C. Pour corriger cela, ajoutez après le dernier caractère de la chaîne à afficher le caractère spécial \n (signifiant New line) qui provoque comme en Java le passage à la ligne suivante.
Sauvegardez, compilez et exécutez. Tout est OK ?


4.   Affichage d'un tableau

4.A. Recopiez le programme précédent dans un nouveau fichier afftab.c , modifiez les commentaires correspondants, et sauvegardez.

4.B. En première ligne de la fonction main, ajoutez la déclaration :
double vTab[] = { 1.1, -2.2, 3.0, -4.44, -5.0, 6.6, 7.77 };
qui déclare/crée/initialise un tableau constant (un peu comme en Java, mais remarquez la place des crochets).

En C, il n'y a pas d'attribut length comme en Java ; on doit donc presque toujours passer la taille du tableau en paramètre supplémentaire de la fonction que l'on est en train d'écrire.

Seul cas où l'on peut calculer la taille d'un tableau : dans le même bloc, juste après la déclaration ci-dessus, on peut utiliser la formule :
int vNbEle = sizeof(monTableau)/sizeof(double );
pour un tableau de réels, car sizeof retourne le nombre d'octets qu'occupe son paramètre.
Si on ajoute une valeur lors de la déclaration/initialisation, vNbEle sera automatiquement mis à jour.
Attention ! La formule ci-dessus n'est pas valable si vous recevez un tableau en paramètre, puisque le tableau aura été créé ailleurs.

4.C. Toujours dans le main, on demande maintenant d'afficher, à raison d'un nombre par ligne, le contenu du tableau, sous la forme vTab[indice] = valeur (exemple pour le premier élément : vTab[0] = 1.1
La boucle for (pas de for each en C !) s'écrit comme en Java (notamment en déclarant le compteur de boucle dans la parenthèse du for), mais le fonctionnement de printf mérite d'être détaillé.
En effet, pour afficher autre chose que du texte, il faut spécifier dans son premier paramètre (qui est forcément une chaîne de caractères) un format d'affichage, par exemple %d pour un nombre entier. Exemple :
printf( "rationnel=%d/%d\n", vN, vD ); pour afficher rationnel=2/11 si vN vaut 2 et vD vaut 11.
Pour afficher un nombre réel (double), le format est %lf.
On peut inclure autant de formats %qqch dans la chaîne de caractères en premier paramètre de printf à condition qu'il y ait ensuite le bon nombre de variables (séparées par des virgules) et dans le bon ordre !
Donc, printf a un nombre variable de paramètres, le premier étant toujours une chaîne de caractères (n+1 paramètres s'il y a n caractères % dans le format en premier paramètre).

Bon à savoir : Il est même possible de spécifier pour chaque variable réelle combien de chiffres on veut afficher après la virgule ; il suffit d'ajouter .2 entre % et lf pour imposer seulement 2 chiffres après la virgule par exemple.

Ajoutez ce qu'il faut pour respecter la forme d'affichage imposée (avec un seul chiffre après la virgule), compilez, testez.

5.   Comptage des positifs, négatifs, ou nuls

Si vous avez des difficultés, des indices figurent à la fin de l'énoncé.

5.A. Recopiez le programme précédent dans un nouveau fichier comptepnn.c, modifiez les commentaires correspondants, supprimez ce qui ne sert plus, et sauvegardez.

5.B. Ce programme devra compter puis afficher le nombre de valeurs positives, négatives, et nulles dans le tableau. Pour cela, il utilisera évidemment des tests : la syntaxe est la même qu'en Java.
Modifiez ce qu'il faut, compilez, testez (en n'oubliant pas d'ajouter des valeurs nulles dans le tableau).
Si vous êtes bloqué, vous pouvez consulter les indices donnés à la fin de cet énoncé.

6.   Fonction puissance

6.A. Recopiez le programme afftab.c dans un nouveau fichier puissance.c , modifiez les commentaires correspondants, et sauvegardez. Lisez l'intégralité de l'énoncé de cet exercice avant de commencer.

6.B. Ce programme devra définir (avant le main) et utiliser (dans le main) une fonction puissance (en supposant évidemment que cette fonction n'existe pas déjà) qui calcule xN, x (réel) et N (entier positif) étant les 2 paramètres de cette fonction. Pour éviter le cas litigieux, la fonction retournera toujours 1.0 si N est nul, même si x vaut 0.0. Ceux qui veulent savoir pourquoi peuvent lire ces explications.
Pour le calcul, les deux possibilités (récursive ou itérative) sont acceptées pour l'instant, mais pas un mélange des deux !

Attention !
En C, il ne faut pas mettre de droit d'accès (public/private) ni de final avant la déclaration de chaque paramètre. A part ça, la syntaxe est la même qu'en Java, sauf que la fonction doit forcément être définie AVANT le main pour pouvoir être appelée dans/par le main.

Écrivez la fonction puissance et compilez pour corriger les premières erreurs.

6.C. Pour utiliser la fonction puissance dans le main, affichez sur une ligne les puissances 0, 1, 2, 3 de chaque nombre du tableau (sans duplication de code). Pour présenter correctement les résultats, il est souhaitable d'utiliser le caractère spécial \t qui affiche une tabulation.
Modifiez ce qu'il faut, compilez, testez.

7.   Procédure d'affichage

7.A. Recopiez afftab.c dans un nouveau fichier procafftab.c , modifiez les commentaires correspondants, et sauvegardez.

7.B. Modifiez le programme pour qu'il définisse et appelle dans le main une procédure afftab pour afficher le tableau ; celle-ci aura 2 paramètres : le tableau et le nombre de cases à prendre en compte (rappel : on ne peut pas calculer le nombre de cases du tableau dans la procédure).
Attention !  En C, il faut écrire double pTab[] et non double[] pTab
Modifiez ce qu'il faut, compilez, testez.

8.   Valeur absolue

8.A. Recopiez le programme précédent dans un nouveau fichier fabstab.c , modifiez les commentaires correspondants, et sauvegardez.

8.B. Ce programme devra afficher le tableau avant ET après avoir remplacé chaque valeur par sa valeur absolue dans le tableau.
Attention !  En C, l'ordre de déclaration des fonctions a de l'importance si elles s'appellent entre elles.
Pour éviter de réinventer la roue, il faut utiliser la fonction mathématique fabs qui retourne la valeur absolue de son paramètre réel.
Modifiez ce qu'il faut et compilez. Une erreur se produit (mais vous la connaissez déjà).

8.C. C'est donc que la fonction fabs n'est pas déclarée dans stdio.h. Effectivement, elle l'est dans math.h, comme l'ensemble des fonctions mathématiques usuelles.
Modifiez ce qu'il faut, compilez, testez.

9.   Amélioration du 6

9.A. Améliorez le programme puissance.c pour que la fonction puissance fonctionne aussi avec des N négatifs.
À cette occasion, si ce n'est pas déjà le cas, écrivez la fonction puissance forcément de façon récursive.
Si vous ne voyez pas comment, demandez-vous comment calculer xN à partir de x-N quand N est négatif, et xN à partir de xN-1 quand N est positif.
Si vous avez toujours du mal, allez voir les indices ci-dessous.
Modifiez ce qu'il faut et compilez pour corriger les premières erreurs.

9.B. Pour utiliser cette nouvelle version de la fonction puissance, affichez les puissances -2, -1, 0, 1, 2, de tous les nombres du tableau (sans duplication de code et sans boucle while).
Modifiez ce qu'il faut, compilez, testez.

10.  Le pré-processeur

10.A. Au 3.B ci-dessus, on vous a fait ajouter une ligne bizarre, commençant par # et ne finissant pas par un point-virgule (#include <stdio.h>). Cette ligne n'est pas une instruction C, mais une directive à l'intention du pré-processeur.

Le pré-processeur est un programme qui est lancé par la commande gcc juste avant de lancer le compilateur C proprement dit, c'est-à-dire que le compilateur ne compile pas directement notre fichier prog.c, mais un fichier prog.i, résultat de la transformation de prog.c par le pré-processeur.

En quoi consistent principalement ces transformations :
1) inclure des fichiers .h (qui contiennent des déclarations) tels que stdio.h, math.h, ou bien d'autres
2) remplacer toutes les occurrences d'un mot par une suite de caractères que l'on choisit :
    - #define TAILLE 10   permet de définir une constante
    Attention !  #define TAILLE 10;  (voyez-vous la différence ?) provoquerait une erreur de compilation dans une instruction telle que int max = TAILLE / 2;
    - #define TAB { 11, -22, 33 }   permet de définir un tableau dont on aurait besoin plusieurs fois dans la suite du programme
    Attention !  Cette ligne ne déclare aucun tableau pour le compilateur ...

Le pré-processeur sert à bien d'autres choses, comme l'utilisation de macros, la compilation conditionnelle, l'inclusion d'assembleur, etc...

10.B. Améliorez le 9.B pour qu'il affiche tous les xN avec N entre -LIMITE et +LIMITE. Essayez en définissant une LIMITE valant 3.
Modifiez ce qu'il faut, compilez, testez.

10.C. Modifiez le 8.C pour qu'il affiche le tableau non modifié APRÈS le tableau modifié. Pour cela, définir un nouveau mot TAB qui contiendra les valeurs du tableau entre accolades. TAB sera utilisé pour initialiser la variable tableau vTab avant les modifications, puis pour initialiser une nouvelle variable tableau vTab2 après les modifications, dans le but de pouvoir afficher les valeurs non modifiées.
Modifiez ce qu'il faut, compilez, testez.

10.D. On ne peut pas programmer en C sans connaître le pré-processeur, mais comme il ne fait que de la substitution de texte (sans tenir compte de la syntaxe du C), on préférera utiliser :
- les fonctions au lieu des macros (voir sur internet pour ceux que ça intéresse)
- les const au lieu des #define pour les constantes, par exemple : const int TAILLE=10;

11.  La compilation séparée (important !)

11.A. Lorsque les programmes grossissent, il devient déraisonnable de tout mettre dans le même fichier. Il nous faut donc répartir le code dans plusieurs fichiers : il y aura plusieurs fichiers .c (contenant les instructions) et plusieurs fichiers .h (contenant les déclarations). De plus, nous souhaitons comme en java pouvoir compiler chaque fichier .c séparément.

11.B. Reprenons le programme 10.B et découpons-le en deux : puiss.c qui contiendra la fonction puiss, et main.c qui contiendra la fonction main (le programme principal) ; où doit-on mettre le #include ?

- Compiler puiss.c. Des erreurs ? Il ne trouve pas l'indispensable fonction main et ne peut donc générer un programme exécutable.
Mais c'est normal : ici, on veut juste compiler la fonction puiss pour s'en servir plus tard, lorsqu'on créera le programme principal.
Pour indiquer cela au compilateur, utiliser la commande mycc -c puiss.c qui génèrera uniquement le fichier compilé puiss.o et non pas le programme exécutable.

- Compiler main.c. des erreurs ? Il ne trouve pas notre fonction puiss. Contrairement à Java, il ne cherche pas automatiquement dans le répertoire courant, il faut lui indiquer où la trouver !
On pourrait penser inclure puiss.c dans main.c, mais dans ce cas, il n'y aurait plus de compilation séparée, puisque compiler main.c reviendrait à compiler 100% des instructions du programme complet.
Il nous faut donc extraire de puiss.c juste la déclaration de la fonction puiss (mais pas les instructions !) pour que main.c ait les renseignements minimaux pour compiler. Il s'agit donc juste du prototype de la fonction (l'équivalent en C de la signature d'une méthode en java) terminé par un point-virgule, et on le met dans un fichier puiss.h (les fichiers .h sont destinés à contenir des déclarations, et on n'inclut jamais de fichiers .c).
Attention ! Pour inclure un fichier .h qui est dans le répertoire courant, on doit écrire #include "fichier.h" et non <fichier.h> qui irait chercher ce fichier dans les bibliothèques standard du C.

- Compiler main.c ; si vous avez une erreur, vous avez probablement oublié l'option -c qui permet de compiler séparément une partie du programme complet.

11.C. Nous avons maintenant compilé les 2 parties de notre programme, et disposons donc des fichiers puiss.o et main.o. Il faut maintenant procéder à ce qu'on appelle l'édition de liens (entre les 2 parties de programme et avec les bibliothèques standard du C) par la commande mycc puiss.o main.o -o puiss.exe

11.D. Refaire la même démarche pour compiler séparément les 2 parties de fabstab.c .


12.  Indices pour ce TP