Langage C : notes du cours
Hanna Klaudel, Université d'Evry
Plan du cours :
1. Introduction (notion d’algorithme, programme, fonctionnement interne de l’ordinateur, interpréteur de commandes shell, quelques commandes de base).
2. Préliminaires (notion de type simple, variable, constante, affectation, expression d’algorithme en pseudo-langage et codage, fonctions de base d’I/O).
3. Expressions et instructions (expressions arithmétiques, Booléennes, opérateurs associés ; instructions itératives, conditionnelles, autres).
4. Types de données (tableaux, chaînes de caractères, structures, unions).
5. Structure d’un programme complexe (notion de bloc, portée des variables).
6. Fonctions (passage de paramètres, valeur de retour, variables locales, variables globales).
7. Récursivité.
• B. Kernighan et D. Ritchie. Le langage C. Dunod, 2002.
• C. Delannoy. Programmer en Langage C. Eyrolles. 2002.
• L'informatique est une science qui s'occupe du traitement automatisé de l'information (à l'aide des logiciels ou programmes).
• Un programme peut être assimilé à une recette ou à une procédure à appliquer pour réaliser quelque chose.
1. (cuisine) Pour faire un kir on a besoin de rassembler les ingrédients (du vin blanc et de la crème de cassis) et de les mélanger dans un verre en respectant les proportions ;
2. (calcul) Pour savoir quels élèves avaient la moyenne du bac supérieure à la moyenne de la classe, on a besoin de connaître la moyenne de chaque élève (éventuellement, on a besoin de la calculer), la moyenne de la classe, le nombre d'élèves dans la classe, et enfin, on doit comparer la moyenne de chacun à la moyenne de la classe et choisir comment on va présenter le résultat (sous forme d'une liste, d'un tableau etc).
Deux phases :
• Algorithmique : la modélisation et abstraction du problème qui mène à l'élaboration d'une solution automatisée :
o Choix des structures de données (tableau, liste, etc) o Conception des algorithmes (suite d'opérations à faire) Al Khowarizmi, Bagdad IXième siècle.
Encyclopedia Universalis : « Spécification d’un schéma de calcul sous forme d’une suite finie d’opérations élémentaires obéissant à un enchaînement déterminé. »
• Programmation o Traduction dans un langage compréhensible par l'ordinateur des structures de données et algorithmes
ordinateur
CPU : Central Processing Unit (unité centrale de traitement) processeur.
ALU : Unité Arithmétique et Logique : le composant qui sait faire les calculs.
Registres : mémoires internes du processeur. Un registre est capable de stocker N 0 ou 1, par exemple le résultat de la dernière opération de l'ALU. Dans un processeur, le nombre de registres est très limité (3 à 8). L'un de ces registres est plus important que les autres accumulateur (accu).
PC (Partie Commande) : la PC commande le processeur.
Comment commande-t-on la PC ?
• Chaque action que PC sait faire est numérotée (en binaire) code opération ou code-op.
• Le choix de la codification est fait par le fabriquant du processeur langage machine.
• On commande l'ordinateur à l'aide d'un programme suite de code-ops (stocké en mémoire).
• C'est le registre d'adresse de programme (RAP) qui sait à tout moment où on en est dans le programme.
• Le CPU va y lire l'instruction suivante, la mémorise dans le registre d'instruction (RI) et l'effectue, ajoute 1 au RAP (sauf si c'était un goto) et recommence, indéfiniment.
• Le registre d'adresse de données (RAD) sert à stoker l'adresse de la prochaine donnée à lire ou écrire en mémoire.
Programmation : les différents niveaux d'abstraction.
• Matériel (Hardware) : logique numérique, microprogrammes
• Logiciel (Software) :
o Langage machine : ajouter 2 entiers, déplacer une donnée,
o Langage assembleur : représentation symbolique des instructions et données
o Langage de programmation de haut niveau : C, C++,
Fortran, Java, Lisp, o Générateurs d'application
La partie logicielle (software) de l'ordinateur est subdivisée en deux parties :
entrée sortie
\/ /\
\/ /\
Logiciels d'application |
Logiciel de base |
Matériel |
• Les logiciels d'application sont les logiciels pour lesquels on emploie l'ordinateur comme le traitement de texte, la comptabilité, les bases de données, le tableur, le logiciel de dessin, etc..
• Les autres programmes définissent ce qu'on appelle le logiciel de base. On y trouve des programmes gérant les fonctions du système.
La couche appelée logiciel de base peut être décomposée en deux sous-niveaux :
entrée | sortie | ||
\/ | /\ | ||
\/ | /\ | ||
Logiciels d'application | |||
Outils et services | |||
Système d'exploitation | |||
Matériel | |||
• outils et services : composés d'utilitaires, d'éditeurs (Emacs ), de compilateurs (gcc, );
• système d'exploitation (operating system - OS : MS-DOS, WINDOWS, OS/2, Unix, etc) : un ensemble de programmes qui servent d'interface entre les logiciels d'application et la machine physique (environnement), et contrôlent le fonctionnement général de l'ordinateur.
§ la structuration des supports informatiques (disques, disquettes…).
Les fichiers sont rangés dans une structure arborescente dont la racine est /.
… comme une adresse postale lue à l'envers: Pays, Ville, Rue, Numéro de rue
Exemple d'arborescence | Exemples de chemins |
/ | / |
`--France | /France |
|--Evry | /France/Evry |
| |--Université | /France/Evry/toto |
| `--INT | |
`--Paris | /France/Paris |
Un chemin peut-être absolu ou relatif. …si vous êtes déjà dans la bonne rue, le numéro vous suffit.
Lorsque vous ouvrez une session sous UNIX, vous avez en général une console ou une fenêtre dans laquelle vous pouvez tapez des commandes.
Le programme avec lequel vous interagissez est le shell. Il va interpréter vos instructions au fur et à mesure que vous terminez de les taper. Vous lui indiquez qu'il peut commencer à interpréter à chaque fois que vous validez une introduction ou une série d'instructions en appuyant sur la touche enter.
Voir :
Le shell vous indique qu'il est prêt en affichant un prompt, en général le caractère dollar $ ou supérieur >, en début de ligne. Comme ceci:
bash$ |
csh> |
Les commandes, celles que vous demandez à l'ordinateur d'exécuter sont indiquées comme ceci:
bash$ commande
Ce que celui-ci vous répond est indiqué
comme ceci
Qui suis-je?
Commençons par une commande simple qui vous renvoie votre login.
bash$ whoami |
tru |
Sur quelle machine suis-je?
hostname vous renvoie le nom de la machine sur lequel votre shell s'exécute.
bash$ hostname |
nemo07 |
Stop!
Ctrl-c c'est-à-dire: [ctrl] enfoncée puis lettre c ne signifie pas copier! Mais plutôt interrompre la commande actuelle.
exit permet de quitter le shell.
bash$ exit
bash$ commande option1 optionN argument1 | argumentM |
Les options sont souvent de la forme -une_seule_lettre.
Les options modifient le comportement de votre commande alors que les arguments sont les objets sur lesquels la commande s'applique.
Par exemple, la commande ls :
ls : affiche le contenu du répertoire courant ls -l : affiche le contenu du répertoire courant avec détails
ls –la : affiche en plus les fichiers « cachés » commençant par le caractère .
(point).
Dans un shell, les expressions régulières (REgular EXpression) forment un moyen condensé de décrire une chaîne de caractères. Pratiquement cela donne:
• * signifie n'importe quelle chaîne de caractères dont le premier terme est alphanumérique (majuscules/minuscules comprises).
• A* signifie n'importe quelle chaîne de caractères commençant par la lettre A.
• ? signifie exactement un seul caractère alphanumérique
• [Aa9] signifie soit la lettre "A majuscule", soit la lettre "a minuscule" soit le chiffre 9.
bash$ ls *.html *.txt |
bash$ ls -l /tmp/*A* |
bash$ ls .???* |
du pour diskusage donne la taille de tous les arguments qui lui sont passés et de façon récursive.
Les options les plus fréquemment utilisées sont -s pour n'afficher que la somme pour chacun des arguments, -k pour l'exprimer en kilo octets et -h pour adapter le coefficient à la taille (kilo, méga, giga octets).
bash$ du $HOME |
bash$ du -k $HOME |
bash$ du -s $HOME |
bash$ du -sk $HOME |
bash$ du -h $HOME |
Où suis-je ?
pwd PrintWorking Directory
bash$ pwd |
/home/Bis/tru/perso/cours/cea |
. et ..
Le . (point) représente le répertoire courant : c'est le répertoire dans lequel vous êtes actuellement. Le .. (point point) représente le répertoire parent : c'est le répertoire juste au dessus.
bash$ pwd |
/home/Bis/tru/perso/cours/cea |
. est ici /home/Bis/tru/perso/cours/cea
.. est ici /home/Bis/tru/perso/cours
cd ChangeDirectory
bash$ cd
cd sans argument, vous ramène chez vous (dans $HOME). cd peut prendre un argument, c'est le répertoire de destination.
bash$ cd .. |
bash$ cd /usr/local/bin |
bash$ cd / |
bash$ cd ../usr |
ls pour LiSt est une commande que nous avons vu précédemment.
bash$ ls [-la] fichier(s) ou répertoire(s)
mv pour MoVe
bash$ mv fichier1 fichier2 repertoire1 destination |
bash$ mv ancien_nom nouveau_nom |
bash$ mv ../fichier1 /tmp/fichier2 . |
S'il y a plusieurs objets à déplacer, la destination est nécessairement un répertoire.
S'il n'y a que deux arguments, mv ne fait que renommer les deux objets de même type.
cp pour CoPy
bash$ cp fichier1 fichier1.copie |
bash$ cp fichier1 fichier2 *.txt repertoire |
bash$ cp -r repertoire1 repertoire2 |
rm pour RemoVe permet d'effacer
bash$ rm *.bak |
bash$ rm -r repertoire |
bash$ rm -fr repertoire fichiers |
Il N'existe AUCUN moyen d'annuler un rm -rf (rm récursif sans confirmation). Une fois effacé, votre fichier/répertoire est complètement PERDU.
rmdir ReMove DIRectory
bash$ rmdir repertoire_vide
mkdir pour MaKe DIRirectory
bash$ mkdir nouveau_rep autre_rep
cat permet d'afficher le contenu d'un fichier sur STDOUT (en général l'écran).
bash$ cat |
nameserver 132.166.192.6 |
nameserver 132.166.192.7 |
Si le fichier est très long, ce n'est pas pratique du tout! Mieux vaut faire défiler page par page.
more est ce que l'on nomme un PAGER. Il permet d'afficher un fichier page par page.
• SPACE pour faire défiler en avant d'une page
• b pour faire défiler en arrière d'une page.
bash$ more
ps pour ProcessStatus permet de lister les processus en cours.
Selon les UNIX, ce sera la première ou la seconde:
bash$ ps -ax |
bash$ ps -ef |
bash$ kill 3456
kill termine/tue le processus dont le numéro est passé en argument.
Historique : créé par Dennis Ritchie en 1972 aux labos Bell
Caractéristiques :
• langage de haut niveau, impératif, orienté bloc
• programmation système Unix
• modulaire : nombreuses librairies standard (string, math) Premier exemple
Imprimer les mots : Hello World, programme hello.c en C
#include <stdio.h> |
main() |
{ |
printf("Hello World\n"); |
} |
• le texte du programme est écrit à l'aide d'un éditeur (Emacs) dans un fichier dont le nom doit finir par .c
• #include permet d'inclure des fichiers/bibliothèques
• main() est une fonction sans arguments qui indique l'endroit par lequel l'exécution du programme va commencer
• les accolades { } contiennent les instructions qui composent la fonction
• printf("Hello World\n"); correspond à l'appel de la fonction standard printf avec l'argument "Hello World\n"
• les guillemets " " délimitent une chaîne de caractères
• \n est le caractère d'interligne ( \t - tabulation, \b - backspace,
\" - ", \\ - \)
Compilation et exécution sous Unix Commandes Unix :
> gcc hello.c -o hello | > gcc hello.c |
> hello | > a.out (ou bien ) |
Hello World | Hello World |
> | > |
Compilation en 2 étapes :
1. Compilation : source.c -------> objet.o
2. Edition de liens : objet.o --------> executable
hello.c ------> hello.o -------> hello
/* permet de construire des mots et des phrases */ |
#include <stdio.h> |
void f1() |
{ |
printf("a"); |
} |
void f2() |
{ |
printf("un"); |
} |
void f3() |
{ |
printf("on"); |
} |
void f4() |
{ |
printf("chat"); |
} |
void f5() |
{ |
printf(" "); |
} |
main() |
{ f2(); |
f5(); |
f4(); |
f5(); |
f3(); |
} |
inclusion des fichiers d'entête |
#include <stdio.h> |
définitions de fonctions |
void f2(){ } |
int somme( ) { } |
double moyenne( ) { } |
programme principal |
int main() { } |
• un programme est une liste de fonctions
• lors de son appel, les instructions d'une fonction sont exécutées séquentiellement.
• chaque fonction est indépendante des autres (variables utilisées)
• les fonctions peuvent s'appeler entre elles : dans main() appel de f1()
• la fonction main() est le point d'entrée du programme
Une variable est identifiée par son nom et correspond à une zone en mémoire, repérée par son adresse.
Le langage C est typé. Chaque type correspond à une taille de zone mémoire (nombre d’octets).
Types simples : | Exemples de valeurs valides : | |
int /* | entier */ | 2 4 -8 'a' 'P' '\n' 3.2 -1.685E24 RIEN |
char /* | caractère */ | |
float double | /* virgule | |
flottante */ | ||
void /* | vide */ |
Déclarations de variables :
Type NomVariable [= Expression] ;
int i; |
int ratio = 6; |
int j, k; |
double p, p=3.0+18; |
double r = somme(18, 7.0) + ratio; |
char a ='a', caractere; |
int i,j; | /* declaration de variables |
entières i et j */ |
Toute variable doit être déclarée avant d'être utilisée.
Le nom de la variable est intimement lié à sa valeur.
La valeur de la variable varie généralement pendant l'exécution du programme.
Pour "donner une valeur" à une variable, on réalise une instruction d'affectation :
NomVariable = Expression ;
i = 8; /* affectation à i de la valeur 8 */ |
j = i + 3; /* affectation à j de la valeur |
de i (qui est actuellement 8) |
augmentée de 3 */ |
j = j + 1; /* affectation à j de la valeur |
de j d'avant l'opération |
(c'est-à-dire 11) augmentée |
de 1 */ |
Dans la pratique, une variable est un couple (adresse, valeur), où adresse désigne la zone mémoire où est stockée la variable.
Le nombre d'octets utilisés pour le stockage dépend du type de la variable : ex. sizeof(int) vaut 4 octets.
Programme qui imprime la table de conversion des euros en devises avec le cours = 2.11, pour 0, 10, 20, , 200. Donc, la table :
0 0.0
10 21.1
20 42.2
200 422.0
variables : euro, devise : réels euro <- 0 tant que euro <= 200 faire début devise <- euro * 2.11, imprimer(euro, devise), euro <- euro + 10 fin |
La valeur de la variable euro est initialement 0 et augmente de 10 à chaque exécution du corps de la boucle.
Attention, l'indentation est importante pour mieux voir la structure de l'algorithme.
/* tab-conversions1.c, imprime la table de |
conversion des euros en devises, |
version avec valeurs en dur */ |
#include <stdio.h> |
main(){ |
float euro, devise; |
euro=0; |
while (euro <= 200) { |
devise = euro * 2.11; |
printf("%4.0f %6.1f\n", euro, devise); |
euro = euro + 10; |
} |
} |
mais, c'est très mauvais de projeter dans un programme des nombres comme 0, 200, 2.11 etc sans préciser ce qu'ils représentent. Il vaut mieux définir des constantes :
#define NomConstante ChaîneDeCharactères
Le compilateur remplacera chaque occurrence du nom de la constante par la chaîne correspondante (preprocessing).
/* tab-conversions2.c, imprime la table de |
conversion des euros en devises, |
version paramétrable avec constantes */ |
#include <stdio.h> |
#define DEBUT 0 |
/* limite inférieure de la table */ |
#define FIN 200 |
/* limite supérieure de la table */ |
#define STEP 10 |
/* la taille du pas */ |
#define COURS 2.11 |
main(){ |
float euro, devise; |
euro=DEBUT; |
while (euro <= FIN) { |
devise = euro * COURS; |
printf("%4.0f %6.1f\n", euro, devise); |
euro = euro + STEP; |
} |
} |
Encore une version, avec une autre sorte de boucle (for) :
/* tab-conversions3.c, imprime la table de |
conversion des euros en devises, version avec |
constantes et boucle for */ |
#include <stdio.h> |
#define DEBUT 0 |
/* limite inférieure de la table */ |
#define FIN 200 |
/* limite supérieure de la table */ |
#define STEP 10 |
/* la taille du pas */ |
#define COURS 2.11 |
main(){ |
float euro; |
for (euro=DEBUT; euro <= FIN; euro = euro + STEP) |
printf("%4.0f %6.1f\n", euro, euro * COURS); |
} |
Dernière version, avec opérateurs spécifiques d'affectation :
/* tab-conversions4.c, imprime la table de |
conversion des euros en devises, |
version avec constantes et boucle for */ |
#include <stdio.h> |
#define DEBUT 0 |
/* limite inférieure de la table */ |
#define FIN 200 |
/* limite supérieure de la table */ |
#define STEP 10 |
/* la taille du pas */ |
#define COURS 2.11 |
main(){ |
float euro; |
for (euro=DEBUT; euro <= FIN; euro += STEP) |
/* euro = euro + STEP */ |
printf("%4.0f %6.1f\n", euro, euro * COURS); |
} |
D'autres opérateurs se rapportant aux affectations sont possibles, par exemple :
a *= 5;
b -= a + 4; /* qui équivaut à b = b-(a+4); */
Les constituants d'un programme sont :
• Les fichiers. Un programme peut être constitué de plusieurs fichiers, liés à la compilation. L'un d'entre eux contient le main().
• Les fonctions. Une fonction est définie dans un fichier (qui peut contenir une ou plusieurs fonctions). Elle est constituée :
o d'une en-tête qui définit les arguments et la valeur (le type de valeur) retournée par la fonction
o du corps de la fonction, représenté par un bloc.
• Les blocs. Un bloc est une suite d'instructions à exécuter séquentiellement. Il est entouré par des accolades { }.
Un bloc a la forme suivante :
{ |
déclarations de variables |
utilisables dans tout le bloc. |
instructions |
qui correspondent à l'exécution du bloc et exploitent les variables. |
} |
• un bloc peut contenir d'autres blocs.
• un bloc sans définition de variables, et à une seule instruction n'a pas besoin d'accolades.
{ /* commentaire : debut du bloc */ |
int a = 4; /* definitions */ |
int b, c; |
b = a * 2; /* une instruction */ |
if (b > 7) /* UNE instruction */ |
{ /* conditionnelle */ |
int d; |
d = a + 3; |
c = 9 * d; |
} |
else |
c = 1; /* qui finit ici */ |
} /* fin du bloc */ |
L'exécution d'un bloc correspond à un contexte d'exécution :
• Quelles sont les variables définies lorsque le bloc s'exécute ?
(les variables définies au début du bloc)
• Les instructions sont exécutées séquentiellement, en partant de la première après les définitions.
• A la fin de l'exécution du bloc, son contexte est effacé : les variables n'existent plus !!! --> elles ne sont plus visibles de l'extérieur, et la place mémoire qu'elles occupaient est libérée.
Corollaire : un nom de variable correspond à une zone mémoire (une adresse) définie dans un contexte d'exécution.
Deux variables de même nom définies dans 2 blocs différents n'ont rien à voir entre elles !!!
En gros : une fonction équivaut à un bloc,
mais c'est plus que cela : une fonction a un nom --> elle peut être appelée (exécutée) plusieurs fois dans un même programme.
void f1() | void f2() | main() |
{ | { | { |
int a; | int b; | f2(); |
f1(); | ||
a = 3; | b = 8; | f2(); |
} | } | } |
Une fonction peut être appelée de n'importe quelle autre fonction (y compris d’elle-même). Elle est alors évaluée.
(exécution d'un programme C = évaluation de main())
Une fonction est un bloc nommé qui peut retourner une valeur.
La valeur retournée est typée.
L'instruction qui permet de retourner une valeur est :
return expression ;
(expression doit être du type prévu !!!)
int f1() | void f2() | |
{ | { | |
int a; | int c = 4, toto; | |
a = 7; | toto = f1()+c*f1(); | |
if (a < 12) | } | |
return 4; | ||
a += 3; | ||
return a + 8; | ||
} | ||
float convert() | ||
{ | ||
float montant; | ||
float cours; | ||
montant=200; | ||
cours=2.11; | ||
return montant * cours; | ||
} |
Remarque : dans f1(), a vaut toujours 7 au départ , dans convert(), il serait utile de pouvoir changer de montant et de cours
Une fonction peut avoir des paramètres. Le résultat de son exécution dépend alors de la valeur de ses paramètres.
#include <stdio.h> |
float convert(float montant, float cours) |
{ |
return montant * cours; |
} |
main() |
{ |
float euro, cours_dollar, cours_livre; |
euro=100.0; |
cours_dollar=1.1; |
cours_livre=1.6; |
printf("%6.1f euro vaut %6.1f dollars\n", euro, |
convert(euro,cours_dollar)); |
printf("%6.1f euro vaut %6.1f livres\n", euro, |
convert(euro,cours_livre)); |
} |
On peut passer la valeur de plusieurs variables à une fonction :
void plusieurs(arg1, arg2, arg3, arg4) int arg1; char arg2; int arg3, arg4;
{ . }
main()
{
int truc = 18;
plusieurs(12, 'a', 2 - 89, truc);
}
avec des types différents.
Les arguments arg1, , arg4 dans la déclaration de la fonction sont appelés les paramètres formels, alors que ceux avec lesquels la fonction est appelée sont ses paramètres effectifs.
Attention, différences de notation des en-têtes en K&R ou C ANSI
void KR(a, b) | void C_ANSI(int a, char b) |
int a; | {…} |
char b; | |
{…} |
Les arguments des fonctions sont toujours passés par valeur en C : l'exécution du bloc de la fonction est faite sur des copies locales des variables (venant du bloc appelant la fonction). Toute modification d'un argument (c'est-à-dire de sa copie) dans le bloc de la fonction n'a aucun effet sur l'original après le retour de la fonction.
void inc(int i) |
{ |
i = i + 1; |
} |
main() |
{ |
int x = 5; |
inc(x); |
/* x vaut toujours 5 !!! */ |
} |
La variable x dans l'appel inc(x) sert à initialiser la valeur du paramètre formel de la fonction inc.
La fonction inc travaille sur une variable locale i !
Mais comment faire si on a besoin de modifier des arguments d'une fonction ?
Par exemple, un programme de tri peut échanger les valeurs de deux éléments qui ne sont pas rangés dans le bon ordre, à l'aide de la fonction nommée swap. A cause du passage des paramètres par valeur, l'appel swap(a,b); n'est pas suffisant car swap ne peut modifier les arguments a et b dans le programme appelant. Le moyen de s'en sortir consiste à passer à la fonction swap les pointeurs (adresses mémoire, notées &a et &b) qui indiquent où sont rangées les valeurs de a et de b.
MEMOIRE
|______________________|
|______________________|
adresse de a, &a ->|_____valeur de a______|
|______________________|
|______________________|
|______________________| adresse de b, &b ->|_____valeur de b______| |______________________|
| |
main() /* programme appelant swap */ |
{ |
int a,b; |
swap(&a,&b); |
/* les adresses de a et de b |
sont passées à swap */ |
/* les contenus de a et b sont échangés */ |
} |
La fonction swap aura donc deux paramètres formels x et y qui sont deux adresses mémoire et échangera leurs contenus, c'est-à-dire, *x et
*y.
La syntaxe est la suivante :
void swap(int * x, int * y) | ||
/* x et y sont des pointeurs (adresses), | ||
swap échange les valeurs pointées par x | ||
et y, c'est-à-dire *x et *y */ | ||
{ | ||
int temp; | ||
temp = *x; | ||
*x = *y; | ||
*y = temp; | ||
} | ||
MEMOIRE |______________________| |______________________| x, &a ->|__valeur de a, *x_____| | ||
|______________________| |______________________| | ||
|______________________| y, &b ->|__valeur de b, *y_____| |______________________| | | |
• l'opérateur d'indirection * signifie «ce qui est pointé par» ; • l'opérateur d'adresse & signifie «adresse de» ;
int i, j, * pi, * pj; |
/* pi et pj sont déclarés |
comme pointeurs sur des entiers, |
cela veut dire que si on applique |
l'opérateur * sur pi ou sur pj, |
on obtiendra une valeur du type int */ |
pi = &i; /* pi pointe sur i, pi a pour |
valeur l'adresse de i */ |
j = *pi + 1; /* équivaut à j = i + 1; */ |
*pi = 0; /* équivaut à i = 0; */ |
pj = pi; /* veut dire que pj pointe |
aussi sur i */ |
Attention : dans une déclaration de pointeur comme
int * pi;
l’étoile * n'est pas l'opérateur d'indirection.
{ |
int i; |
float x; |
scanf("%d %f",&i,&x); |
printf("%3d %6.1f",i,x); |
} |
avec en entrée la ligne
23456 12.123 scanf affectera 23456 à i et 12.123 à x et printf imprimera
234 12.1
Instructions
Les instructions terminent toujours par des points-virgules ;
• instruction vide : ;
• expression : a*5; a<=1 && b>0;
• appel de fonction : convert(200,2.11);
atoi("la chaine");
• affectation variable = expression :
a = 4*6 + sqrt(8.7); a += 5;
• expressions arithmétiques (sur les nombres)
a*5 2.11/4.6+b*44
o +, -, *, /, % (modulo), <, <=, ==
(égalité), != (inégalité) , >=, > o les opérations peuvent faire intervenir différents types o les calculs se font avec une précision limitée
• expressions booléennes ou conditions
a == 3 && b < a a <= 7 || a > 10
o && ("et" logique), || ("ou" logique), ! ("non"), ==
(équivaut) o utilisation des tables de vérité pour évaluer les expressions booléennes
o convention : une expression (quelconque) est fausse si elle s'évalue à 0, sinon elle est interprétée comme vraie
• caractères 'a' 'A' 'Z' (littéral entre quotes) o code ASCII du caractère, …
o <, <=, ==, !=, >=, >
Pour exécuter des instructions si une condition est vérifiée:
if ( expression ) |
bloc1 |
[ |
else |
bloc2 |
] |
• else est optionnel
• expression est une expression booléenne (interprétée comme telle)
Rappel : un bloc est une instruction simple ou { }
main() |
{ |
int a = 8, b = 5; |
/* expr */ |
if ( a+b > 16 ) { /* bloc 1 */ |
a = a - 5; |
b = b + 4; |
} /* fin bloc 1 */ |
else a = 1; /* bloc 2 -> a = 1; */ |
a = a * 7; |
printf("%d %d\n",a,b); |
} |
Qu'affiche-t-on ?
Attention : si plusieurs if's imbriqués, chaque else est associé au if qui le précède.
/* attention piège */ | /* version corrigée */ | |||
int moins(int a,b) | int moins(int a,b) | |||
{ | { | |||
if ( a-b <= 0 | ) | if ( a-b <= | 0 | ) { |
a = -1; | a = -1; | |||
if ( a - b | == 0 ) | if ( a - | b | == 0 ) |
a = 0; | a = 0; | |||
else | } | |||
a = 1; | else | |||
return a; | a = 1; | |||
} | return a; | |||
} |
Combien valent dans les deux cas :
moins(2,5) ? moins(2,2) ? moins(5,2) ?
Elles servent à répéter plusieurs fois un bloc. La syntaxe est la suivante :
/* tant que expression | /* faire bloc tant que |
faire bloc */ | expression */ |
while ( expression ) | do |
bloc | bloc |
while ( expression ) ; |
• expression Booléenne
• Intérêt du do … while : pas d'initialisation des variables utilisées dans la condition d'arrêt.
/* n! | /* ATTENTION ! | (expression | |||
(factorielle) */ | invariante) */ | ||||
int n = res = 10; | int i | = 1, j = | 3, t = 4; | ||
while (n > | 1) { | while | (i | != 0) | { |
n-=1; | t | += | 4; | ||
res *= | n; | j | -= | 1; | |
} | } |
{ | |
int a = 0, b; | |
do | |
scanf("%d", &b); | |
a += b; | |
printf("a vaut maintenant %d\n",a); | |
while (b > 0); | |
} | |
{ | |
char c; | |
do | |
c = getchar(); | |
while (c != EOF); | |
} |
• rappel : scanf permet de lire un nombre au clavier ;
• getchar() renvoie un caractère lu au clavier ;
• EOF (pour End Of File) est une constante qui indique la fin du fichier.
Pour répéter plusieurs fois un bloc en utilisant un (des) compteur(s).
for (expr initiale; expr booléenne; expr fin de bloc) bloc
• expr initiale est évaluée une seule fois, avant l'exécution du bloc bloc.
• expr booléenne est évaluée avant chaque exécution du bloc bloc. Si elle est fausse, la boucle est terminée et on n'exécute plus le bloc.
• expr fin de bloc est évaluée après chaque exécution du bloc bloc.
Généralement, expr initiale et expr fin de bloc sont des affectations d'une variable appelée compteur.
Produit des n premiers entiers
cumul = 1; |
for (i=n; i>0; i-=1) |
cumul *= i; |
ou :
for (cumul=1, i=n; i>0; i--) |
cumul*=i; |
ou :
i = cumul = 1; |
for ( ; i<=n; ) |
cumul *= i++; |
for ( ; ; ) | while (1) |
; | ; |
Exercice : Que vaut c après la boucle ?
for (i=1, j=1, c=0; i>=j; i++, j*=2) c += i - j;
Il est possible d'interrompre l'exécution d'une boucle ( while, do … while et for) avant la fin en plaçant l'instruction break.
int cumul=0, i, j; |
for (i=0; i<10; i++) { |
scanf("%d", &j); |
if (j<0) |
break; |
cumul += j; |
} |
Il est possible de ne pas finir l'exécution du bloc de la boucle ( while, do … while et for) si une condition est vérifiée en plaçant l'instruction continue.
int cumul=0, i, j; | int cumul=0, i, j; |
for (i=0; i<20; i++) { | for (i=0; i<20; i++) { |
scanf("%d", &j); | scanf("%d", &j); |
if (j==7 || j==8) | if ( (j%3) == 0 ) |
continue; | /* modulo 3 */ |
cumul += j; | continue; |
} | cumul += j; |
} |
Dans le cas du for, le rebouclage est précédé de la ré-évaluation de la condition d'arrêt.
Un swich permet d'effectuer différentes instructions suivant la valeur d'une expression
switch (expression) { |
case etiquette1 : instructions |
case etiquette2 : instructions |
default: instructions /* optionnel */ |
} |
Les différents choix sont étiquetés par des expressions constantes qui doivent être toutes différentes.
La valeur de expression est comparée successivement aux étiquettes. Pour tous les cas où elle correspond à une étiquette, les instructions correspondantes sont exécutées.
Si aucun cas ne correspond, les instructions qui suivent le cas default (s'il existe) sont exécutées, sinon il ne se passe rien.
Exemples
int n, a=8; |
switch (n) { |
case 1: a++; |
case 2: a--; |
case 3: a*=2; |
} |
Quels sont les différents cas ? Que vaut a après le switch ?
char c; int chiffreHexa; |
… |
switch (c) { |
case '0': case '1': case '2': case '3': |
case '4': case '5': case '6': case '7': |
case '8': case '9': |
chiffreHexa = c - '0'; |
break; |
case 'A': case 'B': case 'C': |
case 'D': case 'E': case 'F': |
chiffreHexa = c - 'A' + 10; |
break; |
default: printf("Erreur !"); |
} |
A ne PAS utiliser !
Permet d'aller à une étiquette (instruction étiquetée).
goto etiquette; /* avant l’étiquette */ |
… |
etiquette : instructions |
… |
goto etiquette; /* après l’étiquette */ |
… |
Exemple :
for ( ) |
for ( ) { |
if (catastrophe) |
goto erreur; /* saut */ |
} |
erreur : printf("gros problème!!!"); |
/* étiquette */ |
Une déclaration de variable permet au compilateur de réserver (allouer) la place nécessaire au contenu de la variable. La place dépend du type de la variable.
L'opérateur sizeof( unType ) permet de connaître le nombre d'octets prévu pour une variable de type unType.
L'endroit où est situé le premier octet alloué à une variable est l'adresse de la variable.
MEMOIRE
|______________________|
|______________________|
adresse de a ->|_XXXXXXXXXXXXXXXXXXXX_| |_XXXX_valeur de a_XXX_|
|_XXXXXXXXXXXXXXXXXXXX_|
|_XXXXXXXXXXXXXXXXXXXX_|
|______________________|
| |
La définition int a; réserve 4 octets pour stocker la valeur de a (sizeof(int) vaut 4).
Les pointeurs sont des variables ayant comme valeurs des adresses.
Ils pointent vers des variables d'un type donné, indiqué à la définition du pointeur type * variable; .
int * ptr1; |
char * ptr2; |
int * *ptr3; |
Soit une variable variable, de type type :
• L'expression &variable renvoie l'adresse de la variable, qui est donc de type type *, c'est à dire pointeur de type.
• L'expression *variable renvoie la valeur de l'adresse que contient variable. variable est donc nécessairement un pointeur. L'expression est du type pointé par type.
int a; |
int * ptr; /* déclaration de ptr qui est une |
variable qui contient |
l'adresse d'un entier */ |
a = 10; |
ptr = & a; /* ptr vaut l'adresse (&) de a */ |
*ptr = a + 2; |
Que vaut a ? Que vaut *ptr ?
int i = 249; |
int *ptr = &i; |
Que valent les expressions :
1) i + 5; | 5) *ptr + 5; |
2) *(&i); | 6) &(*i); |
3) &ptr; | 7) *(ptr + 5); |
4) **ptr; | 8) i**ptr; |
De quel type sont ces expressions ?
Comment est réservée la mémoire ?
• Si la taille est connue à la compilation, une définition de variable suffit ;
• Sinon, réservation explicite via des pointeurs.
Objets complexes :
• Tableaux : ensemble d'objets identiques ;
• Structures : assemblage d'objets de plusieurs types différents.
La syntaxe de la déclaration est la suivante :
type nomVariable[taille];
float b[20]; |
char toto[N+1]; |
int *tutu[5]; |
La place mémoire allouée à la variable nomVariable est de taille * sizeof(type).
L'accès à un élément : nomVariable[index].L'index est une expression entière.
Attention : Les index débutent à 0.
{ |
int t[6]; |
int i; |
for (i=0; i<6; i++) t[i] = 0; |
for (i=0; i<5; i++) t[i] = t[i]+t[i+1]+i; |
} |
Après la déclaration, il n'y a plus aucun contrôle sur la taille du tableau alloué : c'est au programmeur de savoir si le ième élément de son tableau existe.
Initialisation :
{ |
int a[3] = { 1, 2, 3 }; |
int a[3] = { 1, 2 /* , 0 */ }; |
int a[] = { 1, 2, 3 }; |
} |
Cas des chaînes de caractères. Syntaxe : "Une Chaine" .
{ |
char chaine[] = "Une chaine"; |
chaine[4] = 'C'; |
} |
Une chaîne est un tableau de caractères plus le caractère NULL (code ASCII )
U | n | e | C | h | a | i | n | e | \0 |
Déclaration :
{ |
int a[3][6]; |
char tableau[15][9][56]; |
int a[2][3] = { { 1, 2, 3 }, { 4, 5, 6}}; |
} |
Accès :
{ |
int i, j; |
int N=5, M=8; |
int tableau[N][M]; |
/* initialisation */ |
/* affichage */ |
for (i=0; i<N; i++) { |
for (j=0; j<M; j++) |
printf("%d ", tableau[i][j]); |
printf("\n"); |
} |
Si tab[10] est un tableau, tab est en fait un pointeur constant (une adresse fixe en mémoire), dont la valeur est &tab[0], l'adresse du premier élément du tableau.
int tab[10]; |
int *ptr = &tab[0]; /* == tab */ |
D'où :
tab[0] == *tab tab[1] == *(tab + 1) tab[i] == *(tab + i) == tab[i]
Décalage de i * sizeof(type des éléments) pour calculer l'adresse de la valeur tab[i].
Une structure regroupe un ensemble d'objets de types différents ou identiques. Chaque structure est associée à un type.
Il faut d'abord définir le type associé à la structure pour pouvoir ensuite définir des variables de ce nouveau type.
struct nomType{ | struct point{ | struct personne{ |
type1 nomChamp1; | int x; | char * nom; |
type2 nomChamp2; | int y; | int age; |
double norme; | }; | |
}; | }; |
Déclaration de variables :
struct nomStructure variable;
Exemples :
/* les definitions de types sont faites en DEHORS des |
blocs */ |
struct point { |
int x, y; |
double norme; |
}; |
struct personne { |
char * nom; |
int age; |
}; |
main() |
{ |
int i, j; |
struct point unPoint; |
struct personne l, m; |
struct point p1 = { 3, 4, 5.0 }; |
struct personne toto = { "Mr Toto", 10 }; |
} |
La notation . (point) est utilisée : pour accéder à la valeur du champ age d'une variable toto de type struct personne :
{ |
int sonAge; |
char *sonNom; |
struct personne toto = { "Mr Toto", 10 }; |
sonAge = ; = 24; |
sonNom = ; |
} |
La notation -> (flèche) est utilisée si la variable est un pointeur de structure :
{ |
int sonAge; |
struct personne toto = {"Mr Toto",10}; |
struct personne *pointeurToto = &toto; |
sonAge = pointeurToto->age; /* */ |
/* ou (*pointeurToto).age */ |
} |
Réservation implicite :
int i; /* réserve la place pour un entier, statique */ |
int tab[10] /* réserve la place pour 10 entiers, statique */ |
Comment réserver de la place mémoire de manière explicite ? Par exemple :
Combien de notes ? (lecture de nbNotes) Pour i de 1 à nbNotes notes[i] lecture d'une note moyenne(notes, nbNotes) |
Problème : comment garantir que le tableau notes est assez grand ?
1. Allouer statiquement de la place mémoire :
int notes[MAX] ;
o si plus de MAX entiers ? o si nbNotes << MAX, mémoire gachée
2. Allouer dynamiquement de la place en mémoire une fois que l'on connaît la taille nécessaire.
L'allocation dynamique de mémoire utilise les pointeurs. Principe :
• Le programme demande de la place en mémoire (en octets),
• Le système recherche une zone mémoire libre assez grande, et renvoie l'adresse du premier octet de la zone, c'est-à-dire, un pointeur sur la zone allouée.
Utilisation de la fonction malloc() : void * malloc(int)
• la fonction a un argument de type entier (nb octets demandés),
• elle renvoie un pointeur de caractères (adresse zone allouée).
int *ptr1; |
ptr1 = malloc(10 * sizeof(int)); |
ptr1 = (int *) malloc(10 * sizeof(int)); |
/* avec cast OK */ |
ptr1[9] = 1234; |
main(){ |
int nom, nbNoms = 0; |
char *tabMinus[] = { "toto", "mimi", "lulu", 0 }; |
char **tabMajus; |
while (tabMinus[nbNoms] != 0) |
nbNoms++; |
tabMajus = (char **)malloc(nbNoms*sizeof(char*)); |
for (nom=0; nom<nbNoms; nom++) { |
int car; |
int taille = strlen(tabMinus[nom]); |
tabMajus[nom] = (char *)malloc(taille+1); |
for (car=0; car<taille; car++) |
tabMajus[nom][car] = tabMinus[nom][car]; |
*tabMajus[nom] = *tabMinus[nom] +'A' - 'a'; |
tabMajus[nom][taille] = 0; |
} |
for (nom=0; nom<nbNoms; nom++) |
printf("%s\n",tabMajus[nom]); |
} |
Que fait le programme ?
L'allocation de la mémoire est explicite et la libération l'est aussi.
La fonction free(char *) déalloue la mémoire allouée avec malloc().
allocation implicite (statique) :
{ |
int tab[8]; |
tab[3] = 4; |
/* liberation automatique */ |
} |
allocation explicite (dynamique) :
{ |
int *tab=(int *)malloc(8*sizeof(int)); |
tab[3] = 4; |
free(tab); |
} |
Une fonction et son appel :
double carre(double x) |
{ |
int res; |
res = x * x; |
return res; |
} |
main() |
{ |
double res = 56; |
double truc; |
truc = carre(res); |
res = res * truc; |
} |
Déroulez le programme
Comment écrire carreEtCube(double x) qui renvoie à la fois le carré et le cube de x ?
Première solution
Une fonction C renvoie au plus une valeur : on peut renvoyer une structure contenant les deux valeurs x2 et x3 :
struct cEtc { /* definition du type */ |
double carre; |
double cube; |
}; |
/* fonction */ |
struct cEtc carreEtcube(double x) |
{ |
struct cEtc res; |
res.carre = x * x; |
= res.carre * x; |
return res; |
} |
main() |
{ |
double res = 56; |
struct cEtc truc; |
truc = carreEtcube(res); |
} |
Il faut définir un type spécialement
Deuxième solution
Faire du passage par adresse : la fonction renvoie x3 et stocke x2 dans un argument :
double carreEtcube(double x, double carre) |
/* FAUX !!! */ |
{ |
carre = x * x; |
return carre * x; |
} |
main() |
{ |
double truc, res; |
truc = carreEtcube(56, res) |
} |
Ça ne marche pas : res ne contient pas la bonne valeur après l'appel dans le main()
double carreEtcube(double x, double * carre) |
{ |
*carre = x * x; |
return (*carre) * x; |
} |
main() |
{ |
double truc, res; |
truc = carreEtcube(56, &res) |
} |