TP C1 2526 v2 (obligatoirement dans un Terminal Linux, ou MacOS)
Durée : 2h+2h+2h (= TP C1.1 + C1.2 + C1.3)
Linux est censé avoir été installé au préalable, sinon utiliser les PC fournis par l'école en les démarrant sous Debian.
Il est bien sûr possible d'installer un double-boot aussi sur son ordinateur personnel,
mais si cela vous paraît aventureux, il existe une
solution plus simple.
1ère partie
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 a 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 !
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),
par exemple VS-Code, 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 l'aide pour les commandes Linux
que vous avez apprises dans le TP IN-CMI).
Prenez l'habitude de taper la commande ls quasiment après chaque commande,
pour vérifier ses effets.
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 ?
voire nedit, gedit, geany,...?)
— mais pas un traitement de texte (comme Word) ! — .
Pour expliquer les commandes, nous supposerons que le fichier s'appelle
exercice.c,
mais le vrai nom du fichier changera dans chaque exercice.
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 bonnes options.
Ces options changent pour chaque environnement de développement, chaque unité d'enseignement ou chaque entreprise.
Pour cette unité, nous allons volontairement toutes et tous utiliser le standard ANSI-C,
demander l'affichage d'un maximum de warnings (puisque c'est une période d'apprentissage),
et prévoir d'utiliser la librairie mathématique.
Comme cela implique de nombreuses 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 (selon votre shell)
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.'
Aide : 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
(comme output)
permet de spécifier le nom du programme exécutable que l'on veut créer.
A chaque fois que vous fermez le Terminal, la commande mycc sera inconnue
(voir le 2.D ci-dessous pour une solution sur votre compte
ou sur votre ordinateur personnel).
Dans le cadre d'un autre enseignement ou pendant un stage en entreprise, la commande mycc
sera évidemment inconnue, mais on vous donnera d'autres consignes de compilation,
et pourrez les incorporer dans une nouvelle commande mycc adaptée.
Il n'y a donc aucun intérêt pendant cette unité à retaper à chaque fois
toutes les options de gcc ; par contre, ne vous attendez pas à retrouver la commande
mycc dans un autre contexte ;-)
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 !
Si vous obtenez un message d'erreur commençant par
/usr/bin/ld:, il s'agit d'un message de l'éditeur de liens pas du compilateur,
donc la compilation proprement dite s'est bien passée : pas d'erreur de syntaxe.
Il se peut que vous ayez oublié d'écrire la fonction main par exemple...
Quand il n'y a plus ni erreur ni warning :
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.
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.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).
Comme en Java, on utilise le type double pour les variables réelles (plutôt que
float puisque toutes les fonctions mathématiques utilisent des double.
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.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 avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
6.C. On veut maintenant utiliser la fonction puissance dans le
main :
pour chaque nombre du tableau, affichez sur la même ligne les puissances 0, 1, 2, 3 de ce nombre
(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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2ème partie
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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.
Aucune nouvelle procédure n'est nécessaire.
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à).
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.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.
Modifiez ce qu'il faut et compilez pour corriger les premières erreurs.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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),
en séparant par des tabulations les puissances d'un même nombre sur la même ligne.
Modifiez ce qu'il faut, compilez, testez.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
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.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.
Si vous avez des difficultés,
des indices
figurent à la fin de l'énoncé.
- 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.A. Bien que vous ne les ayez pas étudiés, les opérateurs de manipulation de bits existaient bien en Java, et sont les mêmes en C (sauf pour >>>). En C, les 6 opérateurs suivants sont plus utiles qu'en Java, car le C est plus "proche" de la machine, et est souvent utilisé pour interagir avec du matériel :
12.B.
Les opérateurs & et | ne doivent pas être confondus avec
les opérateurs logiques && et ||, qui manipulent des valeurs booléennes.
- À partir du C99 (*), le type bool existe, et stocke les valeurs false (0) et
true (1) dans un int.
Une conversion de int en bool transforme toute valeur non nulle en 1 ;
cette propriété est souvent utilisée dans les tests et les boucles.
- Autrement dit, quels que soient les nombres non nuls (considérés comme vrais)
ou nuls (considérés comme faux) que vous fournissez en entrée, le résultat de && et ||
sera systématiquement 0 ou 1.
Exemples : 93 || 240 → 1 alors que 93 | 240 → 243
- D'autre part, les opérateurs logiques && et || sont dits « paresseux »
(lazy evaluation), car ils n'évaluent leur 2ème opérande que
si c'est indispensable pour déterminer le résultat ; donc la
2ème opérande
ne sera jamais évaluée dans les deux cas suivants :
(faux && ∀)
et (vrai || ∀),
et la fonction f
ne sera jamais appelée, par exemple, dans les deux cas suivants :
(0 && f(x)) et (-3 || f(x)).
- L'opérateur ~ ne doit pas non plus être confondu avec l'opérateur logique/booléen
!.
Exemples : ~0 → -1 et ~16 → -15
alors que !0 → 1 et !16 → 0
(*) Pour pouvoir utiliser les mots bool, false et true, il faut avoir inclus stdbool.h . Et bien que les programmeurs ayant appris le C avant 1999 continuent d'utiliser int/0/1, il est recommandé d'utiliser bool/false/true pour une meilleure lisibilité.
12.C. Affichage en binaire
Pour pouvoir faire des exercices de manipulations de bits, il faut être capable d'afficher
des nombres en binaire. Malheureusement, la fonction printf ne fournit pas de format
%b pour cela :-(
Nous allons donc utiliser la procédure affBinaire définie dans le fichier
affbits.c.
Ce fichier contient également un main pour montrer différents cas d'affichage selon
la taille de l'entier à afficher.
Compilez et exécutez ce programme.
Remarquez-vous que les deux premiers nombres en binaire semblent «identiques» ?
Comprenez-vous pourquoi ?
13.A. Recopiez le programme précédent dans un nouveau fichier moyenneE.c, videz le main,
mais laissez la procédure affBinaire.
13.B. Écrivez maintenant une fonction moyenne qui calcule et
retourne la partie entière de la moyenne des deux «entiers» passés en paramètres, mais en respectant les contraintes suivantes :
- toutes les variables et tous les paramètres devront être des unsigned char
- stocker d'abord la somme dans une variable, puis la moitié dans une autre variable,
puis retourner ce dernier résultat.
1. le main devra créer deux variables vC1 et vC2 initialisées
avec 100 et 140, puis afficher leur moyenne.
Le résultat est-il bien 120 ?
2. ajoutez une variable vC3 initialisée à 200 puis afficher sa moyenne
avec vC2.
Les résultats sont-ils bien 120 et 170 ?
Comprenez-vous pourquoi ?
Le type unsigned char (sur 8 bits) permet de stocker toutes les valeurs comprises
entre 0 et 255.
200+140 → 340%256 = 84, d'où le résultat surprenant que vous avez obtenu, même si c'est la
réponse ultime à toutes les questions existentielles...
Vous pouvez le vérifier en ajoutant avant le return de la fonction moyenne
les affichages binaires de p1, p2, vSomme, et vMoy
suivis respectivement par '+', '=', '>' et '\n'.
Cela devrait donner [01100100]+[10001100]=[11110000]>[01111000] pour 120.
13.C. Il existe une astuce pour calculer la moyenne entière de deux valeurs
pour ne pas avoir de problème quand la somme dépasse la valeur maximale du type entier :
additionner les bits communs (quel opérateur ?)
aux bits différents (quel opérateur ?) décalés d'un cran vers la droite
(ce qui revient à ajouter la moitié de l'écart entre les deux valeurs).
Dupliquez maintenant la fonction moyenne en fonction moyenneB, puis la modifier
pour qu'elle calcule et retourne toujours la moyenne entière de ses deux paramètres,
mais en utilisant la formule «binaire» ci-dessus, et en décomposant bien
en 4 étapes pour pouvoir afficher en binaire les résultats intermédiaires.
Cela devrait donner [00000100]+[11101000]>[01110100]=[01111000] pour 120.
14.A. Recopiez le programme précédent dans un nouveau fichier rotations.c, supprimez les deux fonctions
moyenne et videz le main,
mais laissez la procédure affBinaire.
14.B. Le C (comme Java) ne fournit pas d'opérateur permettant d'effectuer
non pas un décalage à gauche mais une rotation à gauche, c'est-à-dire que les bits sortant
par la gauche doivent être réinjectés par la droite au lieu d'être remplacés par des 0.
Écrivez maintenant une fonction rotateLeft qui prend en paramètres l'entier à faire tourner
(qu'on choisira unsigned char, donc sur 8 bits) et un entier représentant le nombre de crans
de rotation, et qui retourne évidemment le résultat.
Par exemple pour 150, left([10010110],2) → [01011010],
alors qu'un simple décalage aurait donné [01011000]. Testez en comparant les deux.
14.C. Dupliquez maintenant la fonction rotateLeft en fonction
rotateRight, en adaptant ce qu'il faut pour changer le sens de rotation.
Par exemple pour 150, right([10010110],2) → [10100101],
alors qu'un simple décalage aurait donné [00100101]. Testez en comparant les deux.
S'il reste du temps avant la fin du TP, faites les exercices suivants :.
15.A. Compter le nb de bits à 1 (la boucle peut ne tourner que N fois,
N étant le nombre de bits à 1 et non le nombre total de bits).
15.B. Tester si un nombre est une puissance de 2 (sans aucune boucle,
ni division ni multiplication).
15.C. Échanger deux variables entières (sans variable intermédiaire,
ni addition ni soustraction).