Attention: Pour activer le langage C, executer le code suivant avec Shift-Entrée
où avec le bouton "Run".
!pip install git+git://github.com/frehseg/gcc4jupyter
%load_ext gcc_plugin
# Marche pas sur Google Colab: %load_ext add_cpp_magic
Pour tester si le plugin C marche, executez la cellule suivante: selectionnez-là et tapez Shift-Entrée
. Si vous voyez le texte "Ca marche." apparaître en bas de la cellule, tout va bien.
%%c
#include <stdio.h>
int main () {
printf("Ca marche.\n");
}
Attention: La première ligne est "%%c" pour dire au Notebook que c'est un programme C. Cette ligne ne fait pas partie du programme!
Hello World: petit programme introduit par Dennis Ritchie dans son livre sur C, en 1978
Voici le fameux programme "Hello World" en C:
%%c
#include <stdio.h>
int main ()
{
printf("Hello World !!\n");
return 0;
}
Executer le code suivant avec Shift-Entrée
où avec le bouton "Run":
%%c
/* Mon premier programme en C */
#include <stdio.h>
int main ()
{
printf("Hello World !!\n");
return 0;
}
Nous allons présenter la syntaxe (les réglès de forme) du langage C à l'exemple du programme "Hello World".
;
printf ( "Hello" ) ;
printf("Hello");
printf( "Hello"
)
;
{
et }
{
printf("Hello ");
printf("World.");
}
/* un commentaire...
... sur plusieurs lignes ...
... voilà. */
// commentaire sur une seule ligne
Quand on écrit un programme, on commence avec les commentaires!
Format d'une fonction :
...type de retour... nom_de_fonction
(
... arguments ... )
{
... instructions ...
}
Ici :
int
main
(
)
{
... instructions ...
}
Quand un programme est lancé, c'est la fonction main
qui est appelé.
Une fonction peut donner une valeur de retour :
...type de tour... nom_de_fonction
(
... arguments ... )
{
... instructions ...
return
... valeur de retour ...;
}
Dans le main, toujours:
int
(entier)0
si tout va bien; autre valeur indique un problèmeExemple d'utilisation dans le shell: Afficher "ok" si tout va bien.
./test1 && echo "ok"
#include <stdio.h>
Pour voir ce que ça fait, on l'enlève du programme en le passant en commentaire :
// #include <stdio.h>
test1.c: In function ‘main’:
test1.c:4:1: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
printf("Hello World !!\n");
^~~~~~
test1.c:4:1: warning: incompatible implicit declaration of built-in function ‘printf’
test1.c:4:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
stdio.h déclare printf
. On peut regarder le contenu de stdio.h, qui se trouve dans /usr/include/stdio.h
...
Un programme s’écrit sous forme d’un texte ≪ source ≫. On utilise donc un éditeur de texte.
Une fois écrit, il faut transformer ce texte en exécutable, c'est à dire dans une suite d'instructions connus au processeur. On utilise un compilateur C, dans ce cours ça sera le "GNU C Compiler" gcc
. Dans un terminal :
gcc -Wall -Wfatal-errors monprogramme.c
Le fichier exécutable créée par défaut se nomme a.out
.
Vous pouvez alors exécuter votre programme en tapant dans un terminal :
./a.out
Des gros projets contiennent des milliers de fonctions et de millions de lignes de code. Tout ça c'est écrit par des équipes de plusieurs personnes.
Pour mieux organiser le travail, les fonctions sont distribués sur plusieurs fichiers.
Compiler le programme en entier peut durer plusieurs minutes, voir heures. On compile donc un fichier de code à la fois. Mais le compilateur doit connaître les fonctions données dans les autres fichiers...
La solution c'est de linker le code à la fin (édition de lien).
Regardons ce qui se passe si on n'a pas de fonction qui s'appelle main
:
%%c
#include <stdio.h>
int main2 ()
{
printf("Hello World !!\n");
return 0;
}
ld
est le nom du linker. Il râle parce qu'il n'a pas trouvé de fonction "main". Pourtant, il en a besoin parce que c'est la fonction qui sera appelé quand on démarre le programme!
On peut basculer entre terminal et editeur avec Super-Tab.
mkdir cours1
cd cours1
On supposera que vous savez editer et sauvegarder des fichiers... En revanche, prenez l'habitude de sauvegarder votre travail (backup). C'est embêtant de perdre un programme sur lequel vous avez travaillé pendant deux heures, juste à la fin de l'examen (voir exemple en bas).
Une solution rapide est de faire un zip de votre répertoire de temps en temps:
zip -r cours1_debut.zip cours1
puis changez le nom de temps en temps:
zip -r cours1_moitie.zip cours1
et
zip -r cours1_presque_fini.zip cours1
Attention: Sans l'option -r
votre zip contiendra un dossier vide!
gcc test.c
La commande ls
montre que le fichier a.out
a été créé.
On l'execute avec
./a.out
parce que juste a.out
ne cherche pas dans le répertoire courant (pour éviter toute ambiguité).
Pour changer le nom du fichier produit avec l'option -o
.
On peut récuperer la ligne précédente du terminal avec la flêche vers le haut, puis l'éditer.
gcc -o test1 test1.c
ls
./test1
Attention: Une erreur courante est de taper trop vite et faire
gcc -o test1.c test1
C'est quoi le problème? Eh oui... le texte du programme qui était dans test1.c est perdu.
Si le compilateur voit un problème avec votre programme, il produit des warnings (avertissements) et des messages d'erreurs. Pour les warnings, le compilateur ne s'arrête pas. Il est quand même fortement recommandé de les regarder et de les résoudre! Pour les erreurs, il faut savoir que seulement la première erreur compte. Tous les autres peuvent être un effet secondaire de la première erreur.
Options du compilateur gcc
essentielles:
-Wall
: activer tous les avertissements -Werror
: traîter les warnings comme des erreurs-Wfatal-errors
: s'arrêter à la première erreurImperatif pour ce cours: Toujours utiliser les trois options essentielles!
gcc -Wall -Werror -Wfatal-errors test1.c
Si vous lancez la compilation depuis un terminal, vous n'êtes pas obligés de retaper les commandes à chaque fois. Il y a deux façons de réutiliser vos commandes précédentes:
Ctrl-r
+ texte: cherche la dernière commande qui commence avec ce texte. Encore une fois Ctrl-r
cherche l'avant-dernière etc.Dans la cellule ci-dessous, écrivez un programme C qui affiche "Bonjour". Rappel: Pour executer le programme dans ce notebook, taper Shift-Entré
.
/* Ecrivez le programme ici ...*/
Les informations que l’on souhaite traiter peuvent être de natures variées : entiers naturels, rationnels, valeurs de vérité (vrai / faux), lettres, texte ...
Les langages de programmation fournissent des types de données variés à cet effet. Le type est nécessaire pour :
+
,-
,/
, etc. qui sont différent au niveau materiel si entiers ou flottantsEn C, il y a 3 types de base pour représenter des nombres:
int
),float
et double
),char
) - pour l'ordi ce n'est qu'un nombre.On appelle booléen le type des valeurs de vérité (≪ vrai ≫ et ≪ faux ≫). C ne fournit pas de vrais booléens. En C, ce sont des entiers avec pour convention 0 ≡ ≪ faux ≫ et (tout sauf 0) ≡ ≪ vrai ≫.
Les types de nombres entiers existent en version avec signe (signed
) ou sans signe (unsigned
):
unsigned char
: caractère sur 1 octet, valeurs 0...255signed char
: caractère sur 1 octet, valeurs -128...127char
: peut être signed ou unsigned (voir limit.h)int
: entiers sur au moins 2 octets, mais très souvont 4 octets = 32 bit float
: flottants de basse précision, sur 4 octetsdouble
: flottants de haute précision, sur 8 octetsint
: 0
La taille en octets peut varier selon la machine et le compilateur; si besoin consulter limit.h
(cours ultérieur).
Un programme manipule des données stockées en mémoire. Il faut donc pouvoir manipuler des ≪ réceptacles ≫ d’information : des variables. En C, on déclare une variable en donnant son type suivi de son nom :
int compteur;
On peut initialiser une variable directement au moment de sa déclaration en lui affectant une valeur par la construction =
:
int compteur = 0;
printf
¶Afficher une variable (entier):
%%c
#include <stdio.h>
int main() {
int i = 23;
printf("hello %d bye bye",i);
}
%d
est remplaçé par la variable après le virgule.
Afficher plusieurs variables (entier):
%%c
#include <stdio.h>
int main() {
int i = 23;
int j = 17;
int k = 3;
printf("d'abord %d, après %d, ensuite %d",i,j,k);
return 0;
}
Les %d
sont remplaçés dans l'ordre d'apparition par les variables après le virgule.
Afficher sur plusieurs lignes avec \n
:
%%c
#include <stdio.h>
int main() {
int i = 23;
int j = 17;
int k = 3;
printf("d'abord %d,\naprès %d,\nensuite %d",i,j,k);
return 0;
}
Codes utiles:
%c
: caractère (ASCII)%d
: entier%f
: flottant (float ou double)%g
: flottant en format compact%%c
#include <stdio.h>
int main() {
double x = 12345;
printf("%f\n",x);
printf("%g\n",x);
}
Précision:
%.3f
: avec 3 décimales %.3g
: avec 3 chiffres significatives %%c
#include <stdio.h>
int main() {
double x = 12345;
printf("%.3f\n",x);
printf("%.3g\n",x);
printf("%.4g\n",x);
printf("%.5g\n",x);
}
Alerte Bug: Ce que donne le programme suivant?
%%c
#include <stdio.h>
int main() {
double x = 1.23;
printf("%d",x);
}
Les octets de x
sont traités comme si c'était un entier -- puisque le codage est complètement différent ça donne n'importe quoi.
Pour tester la précision, affichez les valeurs
avec précision maximale en utilisant le format %.17g
.
%%c
#include <stdio.h>
int main() {
/* ... a compléter ... */
}
%%c
#include <stdio.h>
int main() {
printf("%.17g\n", 1.0 );
printf("%.17g\n", 1.0 + 1e-16 );
printf("%.17g\n", 1.0 + 2e-16 );
}
Une variable dans un bloc {
... }
est reconnue seulement dans ce bloc:
{
int i = 123;
}
On l'appele une variable locale.
Les variables qui ne sont dans aucun bloc sont des variables globales, disponibles partout dans le programme. Les variables globales entraînent facilement des bugs, donc on préfère des variables locales.
Elle est distinct des autres variables en dehors du bloc, même des celles qui portent le même nom.
int i = -7;
{
int i = 123;
printf("%d ",i);
}
printf("%d ",i);
affiche: 123 -7
La variable est 'détruite' à la fin du bloc : la place réservée pour la mémoire est libérée et peut désormais être utilisée pour d'autres variables.
Les blocs peuvent être imbriqués :
int i = 4;
{
int i = -7;
{
int i = 123;
printf("%d ",i);
}
printf("%d ",i);
}
printf("%d ",i);
affiche: 123 -7 4
En simplifiant, on différencie 2 types de constructions de C : — Les expressions, qui ≪ ont une valeur ≫ (elles ≪ valent ≫) et n’ont pas d’effet. — Les instructions, qui ≪ ont un effet ≫ (elles ≪ font ≫) sans forcément valoir quelque chose. La différence réelle entre instruction et expression est plus complexe et plus floue en C.
Expressions de base : -Variables : toute variable d ́eclar ́ee et de port ́ee accessible.
x + y
, x - y
, x * y
x / y
x,y
sont int
float
ou double
x % y
%%c
#include <stdio.h>
int main() {
int x = 7;
int y = 3;
printf("%d", x / y );
}
%%c
#include <stdio.h>
int main() {
double x = 7;
double y = 3;
printf("%f", x / y );
}
Affichez le résultat du calcul (x+y)/z
avec x=1, y=2, z=5
int
,double
.
Est-ce que ça fait une différence lequel est double
?%%c
#include <stdio.h>
int main() {
/** A compléter **/
}
x = y
donne comme valeur de retour la nouvelle valeur de x
x = y = z = 3
(déconseillé)attention: facile à confondre avec le test d'égalité x == y
%%c
#include <stdio.h>
int main() {
int x = 2;
int y = 3;
printf("%d\n", x==y );
printf("%d\n", x );
printf("%d\n", x=y );
printf("%d\n", x );
}
Pour tester si x
et y
sont différentes, on peut utiliser x != y
.
x && y
x || y
!x
%%c
#include <stdio.h>
int main() {
int x = 3; // vrai, car pas 0
int y = 0; // faux, car 0
printf("%d\n", x && y);
}
%%c
#include <stdio.h>
int main() {
int x = 3; // vrai, car pas 0
int y = 1; // vrai, car pas 0
printf("%d\n", x && y);
printf("%d\n", !0 );
}
attention:
x & y
x | y
~x
%%c
#include <stdio.h>
int main() {
int x = 2; // vrai, car pas 0
int y = 1; // vrai, car pas 0
printf("%d\n", x && y);
printf("%d\n", x & y);
}
2
en binaire = 10
, 1
en binaire = 01
2 && 1
en binaire = 00
x ^ y
= opération logique "ou exclusif"!%%c
#include <stdio.h>
int main() {
double x = 2;
double y = 3;
printf("%f", x ^ y);
}
pow
de math.h#include <stdio.h>
#include <math.h> // pour pow
int main() {
double x = 2;
double y = 3;
printf("%f", pow(x,y) );
}
gcc test3.c
/tmp/cceMWz9n.o : Dans la fonction « main » :
test3.c:(.text+0x39) : référence indéfinie vers « pow »
test3.c:(.text+0x65) : référence indéfinie vers « pow »
collect2: error: ld returned 1 exit status
C'est quoi le problème?
C'est le linker (ld) qui rale. Les fonctions dans math.h
sont déjà précompilées dans un fichier m
.
Il faut dire au linker de les retrouver dans m
:
gcc test3.c -lm
Attention: L'option -lm
doit venir à la fin, sinon le linker ne la voit pas!
%%c
#include <stdio.h>
#include <math.h>
int main() {
double x = 2;
double y = 3;
printf("%f", pow(x,y));
}
L’instruction conditionnelle permet effectuer un traitement si une condition est vraie:
if (
expression) {
instruction(s) `;`
}
... ou (optionnellement) un autre traitement sinon :
if (
expression) {
instruction1(s) `;`
} else {
instruction2(s) `;`
}
%%c
#include <stdio.h>
int main() {
int n = -1;
if (n<0) {
return 0;
} else {
// On continue...
}
}
Attention:
if
!{
et }
très fortement conseillé !L’instruction while permet effectuer une boucle tant qu’une condition est vraie:
while (
expression ) {
instruction(s) `;`
}
while (<test>) {
... corps de la boucle ...
}
<test>
est executé:
La précision d'un type de variable est la plus petite valeur qu'on peut toujours distinguer de 0 lors d'une opération. Pour l'addition, on appelera la précision la valeur tel que mais pour tous les .
Ecrivez un programme qui définit une variable x
de type double
qui vaut d'abord 1.0
. Ensuite, on divise x
par 2.0
tant que 1.0 + x
est différent de 1.0
.
Finalement, on multiplie x
par 2.0
et affiche sa valeur.
Ca donne la structure suivante :
// On déclare x et lui donne la valeur 1.0.
while (<si 1.0+x est différent de 1.0>) {
// diviser x par 2.0
}
// multiplier x par 2.0
// afficher x
%%c
#include <stdio.h>
int main() {
// à vous de jouer...
}
%%c
#include <stdio.h>
int main() {
double x = 1.0;
while (1.0+x != 1.0) {
x = x/2.0;
}
x = x*2.0;
printf("précision de double: %g",x);
}
La structure suivante est extrèmement courrante :
int i = 1;
while (i<=n) {
... calcul ...
i = i+1;
}
plus généralement :
<instruction1>
while (<test>) {
... calcul ...
<instruction2>
}
100% équivalent:
for (<instruction1>; <test>; <instruction2>) {
... calcul ...
}
Après chaque <instruction2>
, <test>
est executé pour voir si on arrête la boucle.
Ecrivez un programme pour afficher les multiples de 3 jusqu'à pour un nombre n
donné.
%%c
#include <stdio.h>
int main() {
// à vous de jouer...
}
%%c
#include <stdio.h>
int main() {
int n = 9;
for (int i = 1; i<=n; ++i) {
printf("%d ",3*i);
}
}
Affichez les nombres carrés de jusqu'à , séparés par des virgules (sans virgule à la fin, ni au début). On supposera .
%%c
#include <stdio.h>
int main() {
// à vous de jouer...
}
%%c
#include <stdio.h>
int main(){
int n = 10;
for (int i=1; i<=n; ++i) {
if (i>1) {
printf(",");
}
printf("%d",i*i);
}
}
Souvent il faut répéter une tâche plusieurs fois, et la tâche est elle-même une répétition d'actions. On fait alors une boucle dans une boucle.
Ecrivez un programme pour afficher la table de multiplication jusqu'à n
xn
pour un nombre n
donné.
On commence à écrire un programme en le décrivant avec des commentaires :
%%c
#include <stdio.h>
int main() {
// Définir le nombre de tours n
// Boucle: parcourir toutes les valeurs de i=1 à i=n
// Boucle: parcourir toutes les valeurs de j=1 à j=n
// afficher i x j = i*j
// à vous de jouer...
}
%%c
#include <stdio.h>
int main() {
// Définir le nombre de tours n
int n = 4;
// Boucle: parcourir toutes les valeurs de i=1 à i=n
for (int i = 1;i<=n;++i) {
// Boucle: parcourir toutes les valeurs de j=1 à j=n
for (int j = 1; j <= n; ++j) {
// afficher i x j = i*j
printf("%d x %d = %3d ",i,j,i*j);
}
printf("\n");
}
}
La boucle while est parfois pas très élégante :
#include <stdio.h>
int main() {
int reponse;
printf("Entrez 4 pour quitter: ");
scanf("%d",&reponse);
while (reponse!=4) {
scanf("%d",&reponse);
}
}
scanf
est écrit deux fois : deux fois plus de chance d'un bug
variante : boucle do
-while
execute son corps au moins une fois
#include <stdio.h>
int main() {
int reponse;
printf("Entrez 4 pour quitter: ");
do {
scanf("%d",&reponse);
} while (reponse!=4)
}
Chaque variable est stocké dans la mémoire à une adresse réservée pour elle toute seule.
Cette adresse est un nombre entier positif, sur 32 ou 64 bit (selon la machine).
int i = 123;
double x = 0.1;
variable | adresse | contenu |
---|---|---|
x | 1028 | 0.1 |
i | 1024 | 123 |
On peut faire des calculs très malin si on stocke cette adresse aussi dans une variable : un pointeur.
Un pointeur est une variable qui contient l'adresse de mémoire d'une autre variable.
Un pointeur a le type de la variable cible, suivi par *
: int* px
On obtient l'adresse du cible avec &
: px = &x
int i = 123;
double x = 0.1;
int* pi = &i; // pointer vers i
double* px = &x; // pointer vers x
variable | adresse | contenu |
---|---|---|
px | 1044 | 1028 |
pi | 1036 | 1024 |
x | 1028 | 0.1 |
i | 1024 | 123 |
Un pointeur peut pointer vers un autre pointeur:
int i = 123;
double x = 0.1;
int* pi = &i; // pointeur vers i
double* px = &x; // pointeur vers x
double** ppx = &px; // pointeur vers px
variable | adresse | contenu |
---|---|---|
ppx | 1052 | 1044 |
px | 1044 | 1028 |
pi | 1036 | 1024 |
x | 1028 | 0.1 |
i | 1024 | 123 |
%%c
int main() {
int i = 123;
double x = 0.1;
int* pi = &i; // pointeur vers i
double* px = &x; // pointeur vers x
double** ppx = &px; // pointeur vers px
}
Avec un pointeur on peut modifier la variable cible ajoutant un &
devant le pointeur :
int i = 123;
double x = 0.1;
int* pi = &i; // pointeur vers i
variable | adresse | contenu |
---|---|---|
pi | 1036 | 1024 |
x | 1028 | 0.1 |
i | 1024 | 123 |
*pi = -7;
variable | adresse | contenu |
---|---|---|
pi | 1036 | 1024 |
x | 1028 | 0.1 |
i | 1024 | -7 |
a retenir :
pointeur à partir d'une variable : &x
variable à partir d'un pointeur : *px
Les pointeurs seront essentiel pour les fonctions...
Format d'une fonction :
...type de retour... nom_de_fonction
(
... arguments ... )
{
... instructions ...
}
Les arguments sont des variables locales dont la portée est le bloc de la fonction:
int carre (int x) {
return x * x;
}
%%c
#include <stdio.h> /* Pour acceder a printf. */
int main () {
double x = 80; // temperature en Fahrenheit
double y = 5.0 / 9 * x - 160.0 / 9;
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
%%c
#include <stdio.h> /* Pour acceder a printf. */
/* Convertir un température x de Fahrenheit en Celsius */
double F2C(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
int main () {
double x = 80; // temperature en Fahrenheit
double y = F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
double F2C(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
int main () {
<@> double x = 80; // x est alloué 8 octets et 80 y est copiée
double y = F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu |
---|---|---|
x (main) | 1028 | 80 |
double F2C(double x) {
<@> // la variable locale x est crée,
// et la valeur de l'argument effectif y est copié
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
int main () {
double x = 80;
double y = <@>F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu |
---|---|---|
x (F2C) | 1044 | 80 |
y (main) | 1036 | ??? |
x (main) | 1028 | 80 |
double F2C(double x) {
<@> double y = 5.0 / 9 * x - 160.0 / 9; // y est créé et affecté sa valeur
return y;
}
int main () {
double x = 80;
double y = <@>F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu (4 octets) |
---|---|---|
y (F2C) | 1052 | 26.66667 |
x (F2C) | 1044 | 80 |
y (main) | 1036 | ??? |
x (main) | 1028 | 80 |
double F2C(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
<@> return y; // la valeur de retour est copié dans un tampon (registre)
}
int main () {
double x = 80;
double y = <@>F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu |
---|---|---|
val. retour | registre | 26.66667 |
y (F2C) | 1052 | 26.66667 |
x (F2C) | 1044 | 80 |
y (main) | 1036 | ??? |
x (main) | 1028 | 80 |
double F2C(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
<@> } // fin du bloc : variables locales libérées
int main () {
double x = 80;
double y = <@>F2C(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu |
---|---|---|
val. retour | registre | 26.66667 |
y (main) | 1036 | ??? |
x (main) | 1028 | 80 |
double F2C(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
int main () {
double x = 80;
<@> double y = F2C(x); // val. de retour copié dans y
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
variable | adresse | contenu (4 octets) |
---|---|---|
y (main) | 1036 | 26.66667 |
x (main) | 1028 | 80 |
Une fonction qui ne donne pas de valeur de retour est déclaré avec :
void
nom-de-fonction(
...)
%%c
#include <stdio.h> /* Pour acceder a printf. */
/* Convertir un température x de Fahrenheit en Celsius */
double Fahrenheit2Celsius(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
int main ()
{
double x = 80; // temperature en Fahrenheit
double y = Fahrenheit2Celsius(x);
printf ("%.2f F -> %.2f C\n", x, y) ;
return 0;
}
%%c
#include <stdio.h> /* Pour acceder a printf. */
/* Convertir un température x de Fahrenheit en Celsius */
double Fahrenheit2Celsius(double x) {
double y = 5.0 / 9 * x - 160.0 / 9;
return y;
}
/* Afficher le résultat */
void afficher_resultat(double x, double y) {
printf ("%.2f F -> %.2f C\n", x, y) ;
}
int main ()
{
double x = 80; // temperature en Fahrenheit
double y = Fahrenheit2Celsius(x);
afficher_resultat(x,y);
return 0;
}
Les arguments sont des variables:
int carre_par_valeur(int z) {
return z*z;
}
int main() {
int x = 3;
x = carre_par_valeur(x);
printf("%d",x);
}
x
dans z
.L'argument est un pointeur:
void carre_par_adresse(int* px) {
(*px) = (*px) * (*px); // rappel: (*px) identique à x
}
int main() {
int x = 3;
carre_par_adresse(&x);
printf("%d",x);
}
x
.x
via un pointeur.On essaie d'écrire une fonction qui incrémente la valeur de son argument par 1. Observez ce que fait la version avec passage par valeur:
%%c
#include <stdio.h>
void incrementer(int x) {
x = x + 1;
}
int main() {
int x = 7;
incrementer(x);
printf("%d",x);
}
x
dans main
ne change pas. Il est impossible de modifier x
avec une fonction avec passage par valeur, sans passer par la valeur de retour.
Voici une version avec passage par adresse:
%%c
#include <stdio.h>
void incrementer(int* px) {
*px = *px + 1;
}
int main() {
int x = 7;
incrementer(&x);
printf("%d",x);
}
Cette fois, la variable x
de main
change de valeur. Avec passage par adresse, la fonction a accès à la variable x
du main
, en passant par le pointeur px
.
%%c
#include <stdio.h>
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int x = 7; int y = 3; // <@>
swap(x,y);
printf("%d,%d",x,y);
}
%%c
#include <stdio.h>
void swap(int* px, int* py) {
int temp = *px;
*px = *py;
*py = temp;
}
int main() {
int x = 7; int y = 3; // <@>
swap(&x,&y);
printf("%d,%d",x,y);
}
Le prototype, ou déclaration, d'une fonction est
valeur-de-retour nom_de_fonction(
...arguments...);
Exemples:
double Fahrenheit2Celsius(double x);
void afficher_resultat(double x, double y);
void swap(int* px, int* py);
Pour utiliser la fonction dans autre fichier, il suffit de connaître son prototype.
Rappel: la définition d'une fonction inclut aussi son corps (les instructios entre accolades):
valeur-de-retour nom_de_fonction(
...arguments...) {
...instructions...
};
Pour afficher une variable :
`printf("message: %d",x)`
Pour lire une valeur au clavier :
`scanf("%d", &x)`
Attention: Pas de message dans scanf
! Donner seulement le format (%d,%f
,...)
&x
: l'adresse de x
(pourquoi on utilise l'adresse?)
valeur de retour : nombre de valeurs lus
Pour afficher deux valeurs :
`printf("message: %d,%d",x,y)`
Pour lire deux valeurs au clavier :
`scanf("%d,%d", &x, &y)`
valeur de retour : nombre de valeurs lus
#include <stdio.h>
int main() {
int x,y;
printf("Entrer deux valeurs: ");
int n = scanf("%d,%d",&x,&y);
printf("x=%d,y=%d,n=%d\n",x,y,n);
}
$ ./a.out
Entrer deux valeurs: 3,4
x=3,y=4,n=2
$ ./a.out
Entrer deux valeurs: 3 4
x=3,y=1759981600,n=1
$ ./a.out
Entrer deux valeurs: 3 a
x=3,y=1604196336,n=1
Utiliser les codes suivants selon le type de variable:
%d
: int
(avec signe)%u
: unsigned int
(sans signe)%f
: float
(mais pas double
)%lf
: double
(mais pas float
)Attention:
printf("%f",x);
marche si x
est float
et aussi si x
est double
.scanf("%f",&x);
marche seulement si x
est float
!Les structures répondent au besoin d’aggréger des données de types différents. Une structure est un groupement de données par champs nommés. Déclaration d’un type structure :
struct nom-type {
nom-champ1 type-champ1 ;
... ;
nom-champn type-champn ;
} ;
Déclaration d’une variable de type struct
:
struct nom-type nom-variable
Comme pour les tableaux, il est possible d’initialiser directement les valeurs des champs d’une variable de type structure lors de sa déclaration, mais impossible d’affecter d’un seul coup une telle variable. L’accès à un champ de la structure se fait par notation pointée :
nom-variable.nom-champ
%%c
#include <stdio.h>
struct circle {
int x ;
int y ;
unsigned int radius ;
};
int main() {
struct circle c = { 100,50,7 };
printf("Un cercle à (%d,%d) avec radius %d.\n",
c.x,c.y,c.radius);
struct circle d = c;
printf("Une copie du cercle à (%d,%d) avec radius %d.\n",
d.x,d.y,d.radius);
}
Attention: Pour définir un cercle après sa première
déclaration, on ne peut pas utiliser la notation
{ valeur, valeur, ... }
.
%%c
#include <stdio.h>
struct circle {
int x ;
int y ;
unsigned int radius ;
};
int main() {
struct circle c;
c = { 100,50,7 }; // ne marche pas, car est trop tard
printf("Un cercle à (%d,%d) avec radius %d.\n",
c.x,c.y,c.radius);
}
Une fois la structure est crée, il ne peut que modifier les champs un à la fois:
%%c
#include <stdio.h>
struct circle {
int x ;
int y ;
unsigned int radius ;
};
int main() {
struct circle c;
c.x = 100;
c.y = 50;
c.radius = 7;
printf("Un cercle à (%d,%d) avec radius %d.\n",
c.x,c.y,c.radius);
}
Si p
est un pointeur vers une structure avec un champ x
, p.x
ne marche pas. Il faut utiliser p->x
.
Par exemple, on crée une fonction pour déplacer un cercle:
%%c
#include <stdio.h>
struct circle {
int x ;
int y ;
unsigned int radius ;
};
void deplacer(struct circle* p, int dx, int dy) {
p->x = p->x+dx;
p->y = p->y+dy;
}
int main() {
struct circle c = { 100,50,7 };
deplacer(&c,3,4);
printf("Un cercle à (%d,%d) avec radius %d.\n",
c.x,c.y,c.radius);
}
Ajoutez une fonction elargir(struct circle* p, int dr)
pour augmenter le radius d'un cercle par une quantité dr
. Testez avec dr=4
.
%%c
#include <stdio.h>
struct circle {
int x ;
int y ;
unsigned int radius ;
};
int main() {
struct circle c = { 100,50,7 };
/** A vous de jouer */
printf("Un cercle à (%d,%d) avec radius %d.\n",
c.x,c.y,c.radius);
}
caractère : entier de 0 à 255
chaque numéro correspond à une lettre:
chaîne : suite de caractères terminé par le numéro 0
(pas besoin de stocker la longueur)
chaîne modifiable avec []
:
char ma_chaine[] = "ENSTA";
chaîne non modifiable avec *
:
char* ma_chaine = "ENSTA";
[]
: tableau (voir cours 4)ma_chaine
est un pointeur char*
vers E
%%c
#include <stdio.h>
int main() {
char ma_chaine[] = "IN102";
printf("%s\n", ma_chaine );
}
Rappel : une chaîne déclaré avec char*
n'est pas modifiable si elle est initialisée avec une chaîne entre guillemets ("..blabla..."
).
%%c
#include <stdio.h>
int main() {
char* chaine = "ENSTA";
*chaine = 'I'; // erreur : non modifiable
printf("%s\n", chaine );
}
La cellule ci-dessous n'affiche rien parce que le programme se plante.
Dans le programme suivant tout va bien, parce qu'on déclare la chaîne
avec []
:
%%c
#include <stdio.h>
int main() {
char chaine[] = "ENSTA";
chaine[0] = 'I'; // ok
printf("%s\n", chaine );
}
Attention: chaine2 = chaine1;
ne fait pas de copie! On fait juste pointer chaine2
vers la même adresse que chaine1
.
Voici un exemple pour montrer que les deux pointent vers les mêmes lettres. Dans l'exemple suivant, une modification de chaine1
change également chaine2
.
%%c
#include <stdio.h>
int main() {
char chaine1[] = "ENSTA";
char* chaine2;
chaine2 = chaine1;
chaine1[0] = 'I';
printf("%s\n", chaine1 );
printf("%s\n", chaine2 );
}
Pour faire une copie d'une chaîne, il y la commande strcpy
, fournie par la bilbiothèque string.h
.
char* strcpy (dest, source)
copie à partir de l'adresse source
lettre par lettre toute la chaîne vers l'adresse dest
. La valeur de retour est ici inutile, c'est simplement l'adresse dest
. Attention: il faut prévoir assez de place à la destination, sinon il peut y avoir des erreurs graves.
%%c
#include <stdio.h>
#include <string.h>
int main() {
char chaine1[] = "ENSTA";
char chaine2[100];
strcpy(chaine2,chaine1); // on fait une copie
*chaine1 = 'I'; // une modification ne touche pas l'original
printf("%s\n", chaine1 );
printf("%s\n", chaine2 );
}
Pour comparer deux chaines, on ne peut pas utiliser un comparaison de la forme chaine1 == chaine2
. Cela compare les adresses de chaine1
et chaine2
au lieu des lettre de la chaîne.
%%c
#include <stdio.h>
int main() {
char chaine1[] = "ENSTA";
char chaine2[] = "ENSTA";
printf("%d\n", chaine1 == chaine2 );
printf("%p\n", chaine1 );
printf("%p\n", chaine2 );
}
On compare ici les adresses au lieux des lettres et ces adresses ne sont pas les mêmes!
int strcmp(chaine1,chaine2)
donne la différence entre les chaînes
%%c
#include <stdio.h>
#include <string.h>
int main() {
char chaine1[] = "ENSTA";
char chaine2[] = "ENSTA";
printf("%d\n", strcmp(chaine1,chaine2));
}
Ci-dessus, les chaînes sont égaux, donc strcmp
donne 0
(pas de différence).
%%c
#include <stdio.h>
#include <string.h>
int main() {
char chaine1[] = "ENSTA";
char chaine2[] = "FNSTA";
char chaine3[] = "GNSTA";
printf("%d\n", strcmp(chaine1,chaine2));
printf("%d\n", strcmp(chaine1,chaine3));
}
Ci-dessus, strcmp
donne -1
car la première lettre de chaine1
qui est différente de celles de chaine2
est E
, ce qui dans l'alphabet est 1 place avant F
.
Ensuite strcmp
donne -2
car la première lettre de chaine1
qui est différente de celles de chaine3
est E
, ce qui dans l'alphabet est 2 places avant G
.
char* strstr (botte_de_foin, aiguille)
si trouvé, donne le pointeur où aiguille
commence dans botte_de_foin
si pas trouvé, donne 0.
%%c
#include <string.h>
#include <stdio.h>
int main() {
char botte_de_foin[] = "J'adore l'ENSTA, c'est top.";
char aiguille[] = "ENSTA";
char* trouve = strstr(botte_de_foin,aiguille);
printf("%p\n",trouve);
printf("%s\n",trouve);
trouve = strstr(botte_de_foin,"toto");
printf("%p\n",trouve); // nil = 0
}
Un tableau est un ensemble de ≪ cases ≫ mémoire consécutives. Toutes les ≪ cases ≫ ont le même type. On accède immédiatement à une case particulière (≪ élément ≫) par indexation. Un tableau répond au besoin de stocker plusieurs données de même type et d’accéder rapidement (en temps constant) à n’importe quel élément.
Par statique, on entend ≪ dont la taille est connue à la compilation ≫. La taille des tableaux statiques est un nombre fixe au lieu d'une variable.
Comme les variables, les tableaux doivent être déclarés :
type-élément nom [
taille-constant ];
Voici un programme :
int main () {
float tf [10] ; // Tableau de 10 flottants.
int ti [5] ; // Tableau de 5 entiers signés.
return (0) ;
}
Si la taille d'un tableau est spécifiée par une variable, on parle d'un tableaux à longueur variable. Ceci est permis seulement depuis le standard C99, introduit en 1999. Attention: ça ne veut pas dire qu'on peut changer la taille du tableau après l'avoir défini.
Déclaration d'un tableau à longueur variable :
type-élément nom [
variable-entier ];
Voici un programme :
int main () {
int n =17;
float tf [n] ; // Tableau de 17 flottants.
int ti [n+1] ; // Tableau de 18 entiers signés.
return (0) ;
}
L’élément d’indice i
d’un tableau t
est dénoté par t[i]
.
Les indices de tableaux (≪ numéros de cases ≫) commencent à 0! Donc un tableau de taille n
a
des ≪numéros de cases ≫ de 0 à n−1
.
%%c
#include <stdio.h>
int main() {
int T[3];
T[0]=17;
printf("%d\n",T[0]);
}
Si on declare un tableau T
avec
int T[3]
la variable T
est un pointeur qui pointe vers la première case du tableau. On peut accéder au premier élément du tableau avec *T
, comme avec tout autre pointeur.
%%c
#include <stdio.h>
int main() {
int T[3];
T[0]=17;
printf("%d\n",*T); // on peut utiliser T comme pointeur
}
Pour accéder à l'élément d'index i
, on peut calculer son adresse avec la formule:
adresse de
T[i]
= adresse stocké dansT
+i
*(taille des éléments deT
)
Dans C, ceci s'écrit simplement avec
adresse = T+i
car le compilateur replace automatiquement le +i
avec
i
*(taille des éléments deT
.
Ce calcul s'appele l'arithmétique des pointeurs.
%%c
#include <stdio.h>
int main() {
int T[3];
int* adresse = T+2; // calcul de l'adresse de T[2]
printf("%p\n",&T[2]); // l'adresse de T[2]
printf("%p\n",adresse); // l'adresse calculé est la même
}
On peut accéder à un élément du tableau en utilisant un pointeur dont l'adresse était calculé.
%%c
#include <stdio.h>
int main() {
int T[3];
T[2]=3;
int* adresse = T+2; // calcul de l'adresse de T[2]
printf("%d\n",T[2]);
printf("%d\n",*adresse); // accès à T[2] via le pointeur
}
Voici un exemple pour illustrer que les cases du tableau se suivent. On peut le voir en affichant leur adresses:
%%c
#include <stdio.h>
int main() {
int T[3];
T[0]=17;
T[1]=31;
T[2]=22;
int* adr = T+2; // calcul de l'adresse de T[2]
printf("%p\n",&T[0]); // l'adresse de T[0]
printf("%p\n",&T[1]); // l'adresse de T[1]
printf("%p\n",&T[2]); // l'adresse de T[2]
printf("%p\n",adr); // le contenu du pointer adr
printf("%d\n",*adr); // accès à T[2] via le pointeur
printf("%p\n",&adr); // l'adresse où est stocké le pointer adr
// (peut être avant ou après T)
}
variable | adresse | contenu |
---|---|---|
adr | 1036 | 1032 |
T[2] | 1032 | 22 |
T[1] | 1028 | 31 |
T[0] | 1024 | 17 |
Pour être le plus rapide possible, C ne véfifie pas si l'indice dépasse la taille du tableau. Si on accède au tableau avec un indice trop grand (ou négatif), le programme peut s'arrêter brutalement quand on accède à une zone de mémoire interdite ("segmentation fault"). Mais tant qu'on reste à l'intérieur de la zone mémoire du programme, on n'a pas d'erreur. Il se peut alors qu'un bug du programme reste indétecté.
%%c
#include <stdio.h>
int main() {
int T[3];
T[0]=5;
T[1]=11;
T[2]=17;
printf("%d\n",T[2]);
printf("%d\n",T[3]); // pas d'erreur, mais résultat aléatoire
printf("%d\n",T[-1]); // pas d'erreur, mais résultat aléatoire
}
Si on dépasse un tableau, on peut (sans se rendre compte) accéder à d'autres variables du même programme.
%%c
#include <stdio.h>
int main() {
int T[2];
T[0]=5;
T[1]=11;
int X[2];
X[0]=23;
X[1]=47;
printf("%d\n",T[1]); // le dernier élément de T
printf("%d\n",T[2]); // en dépassant T, on accède X, qui suit
printf("%d\n",T[3]); // en dépassant T, on accède X, qui suit
printf("%d\n",X[0]);
printf("%d\n",X[1]);
}
Pour accéder à T[2]
, le compilateur calcule l'adresse avec la formule T+2 * sizeof(int)
, puisque T est un tableau de int
. Typiquement, sizeof(int)
vaut 4. Dans l'exemple ci-dessous, c'est l'adresse 1024+2*4=1032, ci qui est l'adresse de X[0]
.
variable | adresse | contenu |
---|---|---|
X[1] | 1036 | 47 |
X[0] | 1032 | 23 |
T[1] | 1028 | 11 |
T[0] | 1024 | 5 |
Pour passer un tableau T
à une fonction il y a deux façons équivalents : comme un tableau, suivi par []
, ou comme un pointeur, précédé par *
. Par exemple :
void afficher(int T[], int n)
void afficher(int* T, int n)
Dans les deux cas, le corps de la fonction est le même. Voici un exemple pour afficher un tableau d'entiers de longueur n
:
%%c
#include <stdio.h>
void afficher(int T[], int n) {
for (int i = 0; i<n; ++i) {
printf("%d\n",T[i]);
}
}
int main() {
int T[3];
T[0]=5;
T[1]=11;
T[2]=17;
afficher(T,3);
}
Si on passe T
comme pointeur, ça ne change rien :
%%c
#include <stdio.h>
void afficher(int* T, int n) {
for (int i = 0; i<n; ++i) {
printf("%d\n",T[i]);
}
}
int main() {
int T[3];
T[0]=5;
T[1]=11;
T[2]=17;
afficher(T,3);
}
Un tableau de struct se déclare de façon analogue aux tableau des types de base.
%%c
#include <stdio.h>
struct point {
int x;
int y;
};
int main(){
struct point T[3];
T[0].x=1;
T[0].y=2;
printf("%d,%d\n",T[0].x,T[0].y);
}
Voici un exemple où on passe un tableau de struct à une fonction :
%%c
#include <stdio.h>
struct point {
int x,y;
};
void afficher_point(struct point P) {
printf("(%d,%d)",P.x,P.y);
}
void afficher_tableau(struct point T[], int n) {
for (int i=0;i<n;++i) {
afficher_point(T[i]);
}
}
int main(){
struct point T[3];
T[0].x=1;
T[0].y=2;
T[1].x=3;
T[1].y=4;
T[2].x=5;
T[2].y=6;
afficher_tableau(T,3);
}
Vous avez déjà vu comment on lance votre programme dans un terminal en utilisant la ligne de commande:
> ./monprogramme
Dans cette leçon, on regardera comment passer des arguments par la ligne de commande. Lorsqu'on lance on programme dans un terminal, le shell (gestionnaire du terminal) identifie les arguments passés selon des règles fixes:
> ./monprogramme argument1 argument2 argument3
Notemment, les arguments sont séparés par des espaces. Pour donner un argument qui comporte des espaces, on peut le mettre entre guillemets doubles (les guillemets seront enlevés par le shell):
> ./monprogramme "un long argument1" argument2
Pour utiliser les arguments passés en ligne de commande en C, il faut déclarer le main
du programme dans la forme
int main (int argc, char *argv[])
La variable argc
contient le nombre d'arguments passés, mais attention : le nom du programme compte aussi comme argument.
La variable argv
est un tableau ([]
) de char*
. Rappelez-vous la l'utilisation qu'on avait fait de char*
; c'étaut pour stocker des chaînes de caractères. Alors, argv
est un tableau de chaînes.
Si on lance le programme avec
> ./monprogramme "un long argument1" argument2
on a les valeurs suivantes
argc
= 3 (on compte monprogramme
)argv[0]
= monprogramme
argv[1]
= un long argument1
(le shell a enlevé les guillemets)argv[2]
= argument2
Dans un Jupyter Notebook, il n'y a pas de terminal et donc pas de ligne de commande. Par contre, un peut passer une commande en utilisant le shell: il suffit de commencer la commande avec un point d'exclamation (!
). Pour afficher le contenu du dossier courant avec ls
, on utilise:
!ls
!ls
Afin de lancer un programme avec le shell dans un Jupyter Notebook, il faut stocker le code dans un fichier .c
et le compiler avec gcc
.
Pour stocker, on utilse une cellule avec le mot magique
%%writefile monprogramme.c
#include <stdio.h>
... et ensuite le code ...
et pour compiler avec gcc
, c'est
!gcc -Wall -Wfatal-error monprogramme.c
%%writefile monprogramme.c
#include <stdio.h>
int main() {
printf("Bonjour!\n");
}
!gcc -Wall -Wfatal-errors monprogramme.c
!./a.out
On ecrit un programme pour afficher tous les arguments passés par la ligne de commande:
%%writefile monprogramme.c
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i<argc; ++i) {
printf("Argument %d: %s\n",i,argv[i]);
}
}
!gcc -Wall -Wfatal-errors monprogramme.c
!./a.out
!./a.out "un argument long"
!./a.out 1 23 4 -7 "8 9" '10 11'
Les arguments passés par la ligne de commande sont forcement des chaînes de caractères. Pour passer un nombre, il faut convertir la chaîne en nombre. Pour cela, il existent les fonctions suivantes en C (requièrent #include <stdlib.h>
):
int atoi(char* c)
: produit un entier de type int
double atof(char* c)
: produit un flottant de type double
Voici un exemple qui prend en argument deux entiers et affiche leur somme:
%%writefile monprogramme.c
#include <stdio.h>
#include <stdlib.h> // necessaire pour atoi
int main(int argc, char *argv[]) {
int x = atoi(argv[1]); // premier argument
int y = atoi(argv[2]); // deuxième argument
printf("%d + %d = %d",x,y,x+y);
}
!gcc -Wall -Wfatal-errors monprogramme.c
!./a.out 3 4
Ecrivez un programme qui calcule le quotient de deux nombres flottants.
%%writefile monprogramme.c
#include <stdio.h>
int main(int argc, char *argv[]) {
// à compléter
}
!gcc -Wall -Wfatal-errors monprogramme.c
!./a.out 3.14 2
Les variables locales sont stockés dans une zone mémoire appelée la pile (stack).
Sa fin est indiqué par le pointeur de pile.
On peut regarder la taille de la pile avec la commande shell:
ulimit -a
Cela affiche:
stack size (kbytes, -s) 8192
La pile peut stocker 8192 kilo-octets, donc 8192*1024 octets. Si on essaie de faire un tableau statique de cette taille, le programme se plante, parce qu'il n'y aura pas assez de place (la pile est déjà un peu rempli avec quelques d'autres données).
%%c
#include <stdio.h>
int main() {
char grandtableau[1024*8192]; // aussi grand que la pile
// le programme s'arrête ici parce que le tableau est trop grand
printf("%p",grandtableau);
}
En diminuant un peu la taille du tableau, ça passe:
%%c
#include <stdio.h>
int main() {
char grandtableau[1024*8172]; // plus petit que la pile
printf("%p",grandtableau);
}
Des zones de mémoire arbitrairement grandes peuvent être réservés sur le tas (heap).
réservation "manuelle" avec
malloc
: réserver X octets et obtenir l'adresse d'une zonefree
: libérer la zone*
mon_pointeur = malloc(
taille);
free(
mon_pointeur);
void* malloc(size_t nombre_d_octets)
nombre_d_octets
octets dans le tas (plus un peu de place pour une en-tête)size_t
: type entier non-signé assez grand
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
char* grandtableau = malloc(1024*8182);
grandtableau[0] = 13;
printf("%d\n",grandtableau[0]);
printf("%p\n",grandtableau);
}
Si accès à une adresse libérée (ou autrement interdite): segmentation fault
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
char* grandtableau = malloc(1024*8182);
grandtableau[0] = 13;
printf("%p\n",grandtableau);
printf("%d\n",grandtableau[0]);
free(grandtableau);
printf("%p",grandtableau);
printf("%d\n",grandtableau[0]);
}
Pour réutiliser la mémoire après free
: de nouveau un malloc
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
int* grandtableau = malloc(sizeof(int)*1024);
grandtableau[0] = 13;
printf("%p\n",grandtableau);
printf("%d\n",grandtableau[0]);
free(grandtableau);
int* autretableau = malloc(sizeof(int)*2048);
printf("%p\n",autretableau);
printf("%d\n",autretableau[0]);
free(autretableau);
}
Ecrire une fonction crypter
qui prend en argument une chaîne et une clé c
, et qui donne
en valeur de retour la chaîne crypté (sans détruire l'original). Les caractères sont cryptés en additionnant c
.
%%c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* crypter(char* chaine,int cle){
// 1. réserver la mémoire pour la chaine cryptée
// 2. pour chaque lettre x dans chaine,
// écrire x+cle dans la chaine cryptée
// 3. retourner le pointeur vers la chaine cryptée
}
int main() {
char* orig = "ENSTA";
char* cryp = crypter(orig,3);
printf("%s\n",cryp);
free(cryp); // libérer la mémoire!
}
Solution:
%%c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* crypter(char* chaine,int cle){
int longueur = strlen(chaine);
char* chaine_cryptee = malloc(sizeof(char)*longueur);
for (int i=0; chaine[i]!=0; ++i) {
chaine_cryptee[i] = chaine[i]+cle;
}
return chaine_cryptee;
}
int main() {
char* orig = "ENSTA";
char* cryp = crypter(orig,3);
printf("%s\n",cryp);
free(cryp); // libérer la mémoire!
}
Utiliser la même fonction pour décrypter:
%%c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* crypter(char* chaine,int cle){
int longueur = strlen(chaine);
char* chaine_cryptee = malloc(sizeof(char)*longueur);
for (int i=0; chaine[i]!=0; ++i) {
chaine_cryptee[i] = chaine[i]+cle;
}
return chaine_cryptee;
}
int main() {
char* orig = "ENSTA";
char* cryp = crypter(orig,3);
printf("%s\n",cryp);
char* decryp = crypter(cryp,-3);
printf("%s\n",decryp);
free(cryp); // libérer la mémoire!
free(decryp); // libérer la mémoire!
}
Les types enumérés représentent des valeurs choisies parmis un (petit) ensemble, par exemple :
Afin de représenter ces valeurs dans un programme, il faut associer chaque valeur à un nombre. On pourrait choisir des valeurs entières :
Ensuite on pourrait les traiter comme des entiers dans le programme:
int d = 2; // on commence avec le sud
...
if (d == 3) { // vers l'ouest
printf("Ce n'est pas par là.");
}
En revanche, il est pénible et sujette à erreurs de se souvenir des différentes nombres, surtout dans un grand programme qui est écrit par plusieurs personnes. En C, peut demander au compilateur de faire ce travail pour nous, en déclarant un type enum
:
enum
nom-type {
nom-valeur1 ,
nom-valeur2 ,
... };
Par défaut, le compilateur va associer nom-valeur1 avec 0, nom-valeur2 avec 1, etc. Le code devient beaucoup plus lisible et plus facile à modifier :
enum direction { NORD, EST, SUD, OUEST };
enum direction d = SUD; // on commence avec le sud
...
if (d == OUEST) { // vers l'ouest
printf("Ce n'est pas par là.");
}
Voici un petit exemple:
%%c
#include <stdio.h>
enum direction { NORD, EST, SUD, OUEST };
int main(void) {
enum direction d = SUD; // on commence avec le sud
if (d == OUEST) { // vers l'ouest
printf("Ce n'est pas par là.\n");
} else if (d == OUEST) { // vers l'ouest
printf("Ce n'est pas par là.\n");
} else {
printf("Par ici c'est bon.\n");
}
printf("entier associé à NORD: %d\n",NORD);
printf("entier associé à EST: %d\n",EST);
printf("entier associé à SUD: %d\n",SUD);
printf("entier associé à OUEST: %d\n",OUEST);
return 0;
}
On peut utiliser les types enum
commes les autres types, par exemple dans un tableau ou dans une fonction:
%%c
#include <stdio.h>
enum direction { NORD, EST, SUD, OUEST };
enum direction opposee(enum direction d) {
if (d == NORD) {
return SUD;
} else if (d == EST) {
return EST;
} else if (d == SUD) {
return NORD;
} else {
return OUEST;
}
}
int main(void) {
enum direction d1 = SUD; // on commence avec le sud
// changer de sens
enum direction d2 = opposee(d1);
printf("l'opposée de SUD: %d\n",d2);
printf("entier associé à NORD: %d\n",NORD);
printf("entier associé à EST: %d\n",EST);
printf("entier associé à SUD: %d\n",SUD);
printf("entier associé à OUEST: %d\n",OUEST);
return 0;
}
Pour afficher un type enum
de façon plus lisible, on peut les associer avec un tableau de chaînes de caractères :
%%c
#include <stdio.h>
enum direction { NORD, EST, SUD, OUEST };
char* direction_chaine[] = {
"Nord",
"Est",
"Sud",
"Ouest"
};
enum direction opposee(enum direction d) {
if (d == NORD) {
return SUD;
} else if (d == EST) {
return EST;
} else if (d == SUD) {
return NORD;
} else {
return OUEST;
}
}
int main(void) {
enum direction d1 = SUD; // on commence avec le sud
// changer de sens
enum direction d2 = opposee(d1);
printf("l'opposée de %s est %s\n",
direction_chaine[d1],
direction_chaine[d2]
);
return 0;
}
Si on utilise un nombre constant partout dans le programme, il est préférable de la remplacer par un macro qui l'associe à un nom.
Un exemple d'un programme qui utilise un paramètre partout qui pour l'instant vaut 10
:
%%c
#include <stdio.h>
void ligne() {
for (int i=0;i<10;++i) {
printf("*");
}
}
int main() {
for (int i=0;i<10;++i) {
ligne();
printf("\n");
}
}
Si on veut remplacer 10 par 20, il est facile de faire une erreur. Mieux utiliser une constante globale:
%%c
#include <stdio.h>
#define DIMENSION 10
void ligne() {
for (int i=0;i<DIMENSION;++i) {
printf("*");
}
}
int main() {
for (int i=0;i<DIMENSION;++i) {
ligne();
printf("\n");
}
}
Les macros sont remplacés textuellement avant compilation par le préprocesseur C.
Qu'est-ce que se passe si on n'itialise pas une variable? Les instructions
int i;
printf("%d",i);
peuvent afficer la valeur 0, mais aussi tout autre valeur de int
(-98765
,1234567
,...).
C réserve la place dans la mémoire pour i
, mais ne modifie pas le contenu des octets!
Compiler avec -Wall
permet d'attraper des fautes d'initialisation :
$ gcc -Wall test1.c
test1.c: In function ‘main’:
test1.c:3:4: warning: ‘i’ is used uninitialized in this function [-Wuninitialized]
printf("%d",i);
^~~~~~~~~~~~~~
Mieux initialiser tout de suite :
int i = 0;
%%c
#include <stdio.h>
int main() {
int i; // declaration sans initialisation
printf("%d",i);
}
Chaque variable a une certain taille, donnée en nombre d'octets. Cela impose forcément des limites sur la plage de valeurs.
En conséquence, chaque type de variable a une valeur maximale et une valeur minimale qui peuvent être représentées.
Les limites sont disponibles dans limits.h
:
%%c
#include <stdio.h>
#include <limits.h>
int main() {
printf("The number of bits in a byte = %d\n", CHAR_BIT);
printf("The minimum value of INT = %d\n", INT_MIN);
printf("The maximum value of INT = %d\n", INT_MAX);
return(0);
}
Quand on dépasse la valeur maximale ou la valeur minimale d'une variable on parle de débordement. Le débordement peut entraîner des bugs difficiles à détecter. Pour un nombre nonsigné, le CPU calcule alors la valeur modulo (maximum + 1).
Exemples avec type nonsigné:
%%c
#include <stdio.h>
#include <limits.h>
int main() {
unsigned char c = 255;
printf("%u\n", c);
c = c + 1; // 255 + 1 modulo 256 = 0
printf("%u\n", c);
}
%%c
#include <stdio.h>
#include <limits.h>
int main() {
unsigned char c = 0;
printf("%u\n", c);
c = c - 1; // 0 - 1 modulo 256 = 255
printf("%u\n", c);
}
%%c
#include <stdio.h>
#include <limits.h>
int main() {
unsigned char c = 26;
printf("%u\n", c);
c = 10*c; // 26*10 modulo 256 = 4
printf("%u\n", c);
}
Si le type est signé, le débordement passe de la valeur maximale à la valeur minimale et vice versa:
%%c
#include <stdio.h>
#include <limits.h>
int main() {
signed char c = 127;
printf("%d\n", c);
c = c + 1;
printf("%d\n", c);
}
%%c
#include <stdio.h>
#include <limits.h>
int main() {
int i = 2147483647;
printf("%d\n", i);
i = i + 1;
printf("%d\n", i);
}
%%c
#include <stdio.h>
#include <limits.h>
int main() {
int i = -2147483648;
printf("%d\n", i);
i = i - 1;
printf("%d\n", i);
}
C'est quoi le problême avec le programmme suivant ?
#include <stdio.h>
int main() {
unsigned int i;
for (i = 3; i >= 0; i=i-1) {
printf("i = %u\n", i);
}
}
i = 3
i = 2
i = 1
i = 0
i = 4294967295
i = 4294967294
...
La boucle était censé compter de 3 à 0 dans l'ordre décroissante, mais elle tourne sans fin! Puisque i
est une variable nonsigné, le résultat de i-1
quand i
vaut 0
est le nombre 4294967295
, qui est le plus grand nombre représenté par le type unsigned int
. Du coup, le test i >= 0
est toujours vrai et la boucle ne s'arrête jamais.
Version plus méchante :
#include <stdio.h>
int main() {
unsigned int i;
for (i = 3; i >= 0; i=i-1) {
printf("i = %d\n", i);
}
}
i = 3
i = 2
i = 1
i = 0
i = -1
i = -2
...
Ci-dessus, la boucle est exactement la même. Par contre, l'affichage est trompeur : On affiche avec le format %d
qui interprête i
comme un nombre signé. Au lieu d'afficher le nombre 4294967295
, il affiche alors -1
. Du coup on ne voit pas pourquoi la boucle ne s'arrête pas. Le seul moyen de trouver le bug c'est de regarder le type de la variable i
.
Si on affecte une variable à une variable d'un autre type, C fait une conversion automatique. Si l'autre type ne peut pas représenter toutes les valeurs du type d'origine, ceci peut entraîner...(suspens)... des bugs.
Exemple:
unsigned char c;
int i = 123;
c = i; // ok pour entiers entre 0 et 255
%%c
#include <stdio.h>
int main() {
unsigned char c;
int i = 123;
c = i; // ok pour entiers entre 0 et 255
printf("%d",c);
}
%%c
#include <stdio.h>
int main() {
unsigned char c;
int i = 256+123;
c = i; // ok pour entiers entre 0 et 255
printf("%d",c);
}
En allant d'un flottant vers un entier, on perd la fraction:
%%c
#include <stdio.h>
int main() {
int i;
double d = 3.4142;
i = d; // ok entre -2147483648 et 2147483647
printf("%d",i);
}
%%c
#include <stdio.h>
int main() {
int i;
double d = 3.99999;
i = d; // ok entre -2147483648 et 2147483647
printf("%d",i);
}
Si on affect un nombre plus grand que les limites du type, on se retrouve avec une sitation de dépassement :
%%c
#include <stdio.h>
int main() {
int i;
double d = 121474836480;
i = d; // ok entre -2147483648 et 2147483647
printf("%d",i);
}
Conversion correcte s'il n'y a pas de perte d'information:
Attention aux autres cas: le résultat peut être complètement faux!
printf("%d",i)
: %d
s'applique à un entier de type int
.
Si on l'appelle avec un char
, celui est automatiquement converti en int
.
%%c
#include <stdio.h>
int main() {
char c = 127;
printf("%d",c);
}
printf("%f",d)
: %f
s'applique à un flottant de type double
.
Si on l'appelle avec un float
, celui est automatiquement converti en double
.
%%c
#include <stdio.h>
int main() {
float f = 1.23;
printf("%f",f);
}
%%c
#include <stdio.h>
void afficher(int x) {
printf("%d",x);
}
int main() {
double f = 2.34;
afficher(f);
}
%%c
#include <stdio.h>
int main() {
int i = -1234;
char c = i;
printf("%d -> %d",i,c);
}
%%c
#include <stdio.h>
int main() {
double z = 1e10;
int i = z;
printf("%g -> %d",z,i);
}
Pour chercher des bugs il y a deux techniques principales :
printf
,gdb
.Avec printf
, on peut facilement attraper les erreurs les plus courantes.
Attention: L'affichage n'a lieu qu'après un retour à la ligne. Toujours ajouter \n
si printf
est pour déboger.
Voici quelques exemples.
Afficher les entrées et les sorties de la fonction!
Voici un bug:
%%c
#include <stdio.h>
double div(int x, int y) {
double z = x/y;
return z;
}
int main() {
double a = div(1,3);
printf("%g\n",a); // on veut 0.3333 mais ça donne 0
}
On ajoute des printf pour afficher entrées et sorties:
%%c
#include <stdio.h>
double div(int x, int y) {
printf("%g,%g",x,y);
double z = x/y;
printf("%g",z);
return z;
}
int main() {
double a = div(1,3);
printf("%g\n",a); // on veut 0.3333 mais ça donne 0
}
Ici, l'avertissement du compilateur nous pointe vers la source du problème : x
et y
devraient être déclarés comme int
!
while
n'arrête pas comme prévu¶Afficher tout les parties de la condition de while
.
S'il y a un compteur de boucle, l'afficher également.
Le programme suivant ne calcule pas le bon résultat:
%%c
#include <stdio.h>
int main() {
int iter = 0;
int iter_max = 10;
double x = 2;
while (x*x<4 && iter < iter_max) {
x = -0.5*x - 1;
iter = iter + 1;
}
printf("%g ",x);
}
On ajoute un printf avant et à la fin de la boucle while
pour afficher les valeurs des variables et les résultats des tests:
%%c
#include <stdio.h>
int main() {
int iter = 0;
int iter_max = 10;
double x = 2;
printf("x*x: %g, test1: %d, ",x*x,x*x<4);
printf("iter: %d, test2: %d\n",iter,iter<iter_max);
while (x*x<4 && iter < iter_max) {
x = -0.5*x - 1;
iter = iter + 1;
printf("x*x: %g, test1: %d, ",x*x,x*x<4);
printf("iter: %d, test2: %d\n",iter,iter<iter_max);
}
printf("%g ",x);
}
Grace au printf on se rend compte qu'on n'entre jamais dans la boucle car le test x*x<4
échoue. La solution était d'utiliser x*x<=4
.
%%c
#include <stdio.h>
int main() {
int iter = 0;
int iter_max = 10;
double x = 2;
printf("x*x: %g, test1: %d, ",x*x,x*x<4);
printf("iter: %d, test2: %d\n",iter,iter<iter_max);
while (x*x<=4 && iter < iter_max) {
x = -0.5*x - 1;
iter = iter + 1;
printf("x*x: %g, test1: %d, ",x*x,x*x<4);
printf("iter: %d, test2: %d\n",iter,iter<iter_max);
}
printf("%g ",x);
}
Parfois le programme se plante subitement. Voici quelques causes potentielles :
x/y
avec y=0
T[i]
avec i
plus grand que la taille de T
le permet*p
où p
ne pointe pas vers la bonne adressefree(p)
sur un mauvais pointeur (oublié p=malloc(...)
ou déjà fait free
avant)On peut localiser l'instruction responsable pour l'erreur en l'imbriquant entre deux printf
(principe de bisection).
Ajouter des printf("test A\n")
avant et printf("test B\n")
après l'arrêt soupçonné, puis déplacer les deux jusqu'ils imbriquent l'instruction fautive.
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
int i = 0;
int N = 10;
double* T = 0;
// oublié: T = malloc(sizeof(double)*N);
double x = 2;
while (x*x<=4 && i <= N) {
x = -0.5*x - 1;
T[i] = x;
i = i + 1;
}
printf("test A\n"); // ----> erreur
printf("%g ",T[0]); // instruction soupçonnée
printf("test B\n"); // erreur <----
free(T);
}
... affiche ni test A
ni test B
(si lancé dans un terminal; dans un Jupyter Notebook un programme qui se plante n'affiche rien du tout). L'arrêt doit alors être plus tôt que test A
.
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
int i = 0;
int N = 10;
double* T = 0;
// oublié: T = malloc(sizeof(double)*N);
double x = 2;
while (x*x<=4 && i <= N) {
printf("test A\n"); // ----> erreur
x = -0.5*x - 1;
T[i] = x;
i = i + 1;
printf("test B\n"); // erreur <----
}
printf("%g ",T[0]);
free(T);
}
... affiche test A
mais pas test B
(si lancé dans un terminal; dans un Jupyter Notebook un programme qui se plante n'affiche rien du tout). L'arrêt doit alors être plus tard que test A
.
%%c
#include <stdio.h>
#include <stdlib.h>
int main() {
int i = 0;
int N = 10;
double* T = 0;
// oublié: T = malloc(sizeof(double)*N);
double x = 2;
while (x*x<=4 && i <= N) {
x = -0.5*x - 1;
printf("test A\n"); // ----> erreur
T[i] = x;
printf("test B\n"); // erreur <----
i = i + 1;
}
printf("%g ",T[0]);
free(T);
}
... affiche test A
mais pas test B
(si lancé dans un terminal; dans un Jupyter Notebook un programme qui se plante n'affiche rien du tout). On encerclé une seule instruction qui doit être responsable de l'arrêt. On se souvient que le tableau T
n'a jamais été alloué (malloc
oublié) et on corrige l'erreur...
Attention: Eviter de faire plusieurs opérations dans une seule ligne d'instruction. Cela rend le débogage plus difficile. Au besoin, on sépare les opérations en introduisant des variables supplémentaires, qu'on peut afficher.
Exemple: Le programme suivant ne donne pas le résultat attendu. Comme la ligne de calcul est longue, ce n'est pas évident de trouver l'erreur.
%%c
#include <stdio.h>
int main() {
int x = 3;
double z = x/9+0.5*x;
printf("%g\n",z); // devrait donner 1.8333
}
En séparant les opérations et affichant les résultats intermédiaires, on trouve rapidement la faute:
%%c
#include <stdio.h>
int main() {
int x = 3;
double temp1 = x/9;
printf("%g\n",temp1);
double temp2 = 0.5*x;
printf("%g\n",temp2);
double z = temp1+temp2;
printf("%g\n",z); // devrait donner 1.8333
}
L'instruction x/9
utilise la division entière, ce qui donne la faux résultat.
Le mieux c'est d'attrapper les bugs le plus tôt possible. Voici quelques astuces :
gcc -Wall -Werror -Wfatal-errors monpgramme.c
Si une partie de votre programme ne marche pas, il faut la désactiver pour pouvoir compiler. De plus, il faut la remplacer par une substitution qui vous permet de continuer à développer le reste du programme:
Une telle fonction temporaire qui se substitue pour d'autre code s'appelle un stub (bouchon en français).
Dans l'exemple suivant, l'entrée du nombre ne marche pas:
#include <stdio.h>
int main() {
int x;
do {
scanf("Donner un nombre: %d",&x);
printf("Le carré de %d est %d.\n",x,x*x);
} while (x>0);
}
On met l'instruction en commentaires /*
... */
pour la désactiver, mais du coup le reste du programme ne marche plus parce que x
n'a pas la bonne valeur:
#include <stdio.h>
int main() {
int x;
do {
/*
scanf("Donner un nombre: %d",&x);
*/
printf("Le carré de %d est %d.\n",x,x*x);
} while (x>0);
}
Mieux: On déplace les instructions fautives dans une fonction, qui donne une valeur de retour "utile" pour pouvoir tester le reste du programme. Ici ont choisit 0
parce que sinon le programme ne s'arrête jamais:
%%c
#include <stdio.h>
int entree() {
int x;
/* Je ne trouve pas la faute ici:
scanf("Donner un nombre: %d",&x);
*/
// pour continuer, je donne une valeur par défaut
x = 0;
return x;
}
int main() {
int x;
do {
x = entree();
printf("Le carré de %d est %d.\n",x,x*x);
} while (x>0);
}
Vous pouvez même ajouter du code pour simuler plusieurs entrées :
%%c
#include <stdio.h>
/**********************************************
Code pour simuler des entrées parce que
je n'arrive pas à faire marcher scanf.
*/
int entree_compteur = 0;
int entrees_fixes[4] = {2,7,1,0};
int entree() {
int x;
/* Je ne trouve pas la faute ici:
scanf("Donner un nombre: %d",&x);
*/
// pour continuer, je donne 4 valeurs par défaut
x = entrees_fixes[entree_compteur];
++entree_compteur;
return x;
}
/**********************************************/
int main() {
int x;
do {
x = entree();
printf("Le carré de %d est %d.\n",x,x*x);
} while (x>0);
}
Si plus tard vous trouvez l'erreur, il suffit de corriger la fonction, sans toucher au reste du programme:
%%c
#include <stdio.h>
int entree() {
int x;
printf("Donner un nombre: ");
scanf("%d",&x);
return x;
}
int main() {
int x;
do {
x = entree();
printf("Le carré de %d est %d.\n",x,x*x);
} while (x>0);
}
La fonction principale d'un stub est de servir d'emplacement pour du code qui reste encore à écrire. Ca marche très bien pour structurer son programme au fur et à mesure, surtout si on ajoute des commentaires qui expliquent la fonctionalité qui reste à développer:
%%c
#include <stdio.h>
/* STUB: Demander à l'utilisateur de taper un entier */
int entree() {
/* à faire */
return 3;
}
/* STUB: Faire le calcul compliqué */
double calcul(int a) {
/* à faire */
return 3.1415;
}
/* STUB: Afficher le résultat */
void affichage(double x) {
/* à faire proprement */
printf("%g\n",x);
return;
}
int main() {
// grace aux fonctions, le programme principal
// est simple et lisible:
int a = entree();
double x = calcul(a);
affichage(x);
}