SCO-UNIXSupport de Formation
TABLE DES MATIÈRES
1. Introduction à la Programmation sous Unix ..2
2. L’éditeur ‘vi’ 2
3. La Programmation du Shell .4
3.1. Les Scripts .5
3.1.1. Les Caractères Spéciaux .5
3.1.2. Passage de Paramètres ..5
3.1.3. Utilisation de Variables Locales ..6
3.1.4. Exemples de Programmation Shell 6
3.2. Le Sous-Shell ..9
3.3. La programmation du Korn-Shell 10
3.3.1. Démarrer le Korn Shell .11
3.3.2. La Commande test .12
3.3.3. Les Alternatives ..13
3.3.4. Les Opérateurs && et || 13
3.3.5. Les Boucles ..14
4. Les Utilitaires awk et sed 14
4.1. sed ..15
4.2. awk .16
5. Le Langage ‘C’ 18
5.1. Introduction ..18
5.2. Création d’un programme en langage ‘C’ .19
5.3. Structure d’un Programme ‘C’ ..20
5.4. Les Variables .21
5.5. Les Fonctions .23
5.6. Gestion de la Mémoire .24
5.7. Les Opérateurs 25
5.8. Les Boucles et les tests .26
5.8.1. La boucle for : 26
5.8.2. La boucle while : ..26
5.8.3. Les tests " si, sinon.. " .27
5.8.4. Le choix multiple avec " switch. " ..27
5.9. Les Pointeurs .28
5.9.1. Caractéristiques 28
5.9.2. Pointeurs et Chaînes de caractères 30
5.9.3. Les pointeurs de Fonctions .31
5.10. Les Entrées/Sorties 31
5.11. Programmation Système sous UNIX .32
5.12. Développement d'Applications en Langage C .33
5.13. Le "Déverminage" 33
5.14. Exemple de Programmation en Langage C ..34
Programmer sous UNIX signifie dialoguer avec le système d’exploitation grâce à des outils adaptés. Nous étudierons deux types principaux de langage de programmation permettant de communiquer avec le système.
Il s’agit du shell qui nous permet d’utiliser des commandes directement tapées au clavier ou de grouper ces commandes dans des fichiers avec l’avantage de pouvoir utiliser des boucles et des tests.
Le deuxième type de langage nous permettant d’utiliser le système est le langage ‘C’ qui est particulièrement adapté à UNIX puisque, rappelons le, le système UNIX lui-même est écrit à 90 % en langage ‘C’.
Références :
SCO?UNIX Operating System
User’s Reference
Chapitre : Commands(C).
Comme avec tout système d’exploitation nous avons besoin d’un éditeur de texte pour placer et modifier du texte dans un fichier. Unix met à notre disposition un éditeur standard appelé vi (visual). D’autres éditeurs plus souples à manier existent sur le marché (tel emacs) mais vi est celui que l’on pourra retrouver sur toutes les machines Unix. C’est pourquoi, malgré ses nombreuses imperfections, c’est celui dont nous apprendrons l’usage.
$ vi NomFichier
Pour travailler avec vi, nous utiliserons trois modes de travail distincts :
les commandes
la saisie
des commandes spéciales
Un certain nombre de commandes permettent de passer en mode saisie, chacune de ces commandes doivent être précédées de la touche ESC (Echap) pour devenir actives, exemple :
ESC a :
Nom | Signification | |
a | add | Ajout après le dernier caractère actif |
A | add | Ajout en bout de ligne |
i | insert | Insertion devant le caractère actif |
I | insert | Insertion en début de ligne |
o | open | Ajout d’une nouvelle ligne sous la ligne active |
O | open | Ajout d’une nouvelle ligne sur la ligne active |
Pour quitter l’éditeur il existe trois possibilités :
Nom | Signification | |
ZZ | Quitter et sauvegarder | |
:wq | write,quit | Quitter et sauvegarder |
:q! | quit | Quitter sans sauvegarder |
Pour se déplacer avec la plus part des terminaux (vi est lié aux types de terminaux)
on peut utiliser les flèches de déplacement.
Pour supprimer :
Nom | Signification | |
x | Supprime un caractère | |
dw | delete word | Supprime un mot |
dd | Supprime une ligne | |
3dd | Supprime trois lignes |
Pour copier :
Nom | Signification | |
y | yank | Marque la ligne à copier |
3yy | Marque trois lignes | |
p | | Copie |
La commande set permet de modifier l’affichage dans vi.
: set nu
affiche les numéros de lignes.
? Les " : " font partie intégrante de la commande. Ce sont eux qui permettent à vi de passer en ligne de commande.
:set nonum
supprime l’option d’affichage des numéros de lignes.
Pour rechercher un texte :
:/Texte à chercher
La recherche se fait à partir de la ligne courante.
Pour remplacer un texte :
:g/Texte à remplacer/s//texte de remplacement/[gc]
Le premier g (pour recherche globale) s (pour substitute).
Option g : Toutes les occurrences seront remplacées.
Option c : Chaque occurrence devra être confirmée pour le remplacement.
? Les options de vi sont extrêmement nombreuses et certaines peu utilisées, notre but n’est pas de les parcourir toutes, il suffit pour cela d’éditer le " man " de vi pour connaître toutes les possibilités de l’éditeur.
Comme nous l’avons vu dans notre module d’introduction à Unix, on peut utiliser le shell par des commandes tapées au clavier de manière interactive ou par l’intermédiaire de fichiers (scripts) ce qui permet d’utiliser avec plus de souplesse toutes les possibilités du shell (boucles, tests, etc.).
Nous rappelons que l’ensemble de la description des commandes se trouve dans deux manuels :
Références :
SCO?Open Systems Software
Programmer’s Reference Manuel Volume 1
et
Programmer’s Reference Manuel Volume 2
Nous allons nous attacher à l’apprentissage de la création de nouvelles commandes ou de programmes shell.
Ecrire un fichier script, c'est d'abord utiliser un éditeur de texte (tel que vi). Ce fichier comportera un certain nombre de commandes Unix qui peuvent être incluses dans des boucles et des tests. Enfin pour que ce fichier puisse être exécuté, il faudra soit le rendre "exécutable" par la commande chmod +x (ou chmod 777 si nécéssaire) soit utiliser la commande sh suivie du nom du script..
Les scripts utilisent les mêmes caractères spéciaux, parfois appelés métacaractères que ceux que l'on peut utiliser sur une ligne de commande, voici un rappel de ces signes particuliers et de leur signification :
Symbole | Signification |
; | Séparateur de commandes |
( ) | Groupement de commandes; leur exécution se fait dans un sous-shell |
{ } | Groupement de commandes; leur exécution se fait dans le shell courant |
[ ] | Equivalent à la commande test |
[A-F] | Impose une fouchette (ici de A à F) |
! | Non |
? | Caractère de substitution (un caractère) |
* | Caractère de substitution (plusieurs caractères) |
`cde` | Renvoie le résultat de la commande cde |
| | Le pipe qui permet le passage des arguments de sortie d'une commande en arguments d'entrée d'une autre commande. |
# | Débute une ligne de commentaires |
A ces signes il faut ajouter tous les opérateurs mathématiques (+, -, =, *, /).
Les scripts peuvent recevoir des arguments en paramètres qui sont récupérés à l'aide des variables $1, $2$9 ainsi que nous l'avons vu dans le chapitre d'introduction.
Ainsi le sript suivant que nous appelerons "affiche" :
# "affiche" permet de visualiser le fonctionement des variables
echo $0
echo $1
echo $*
echo $#
Exécutons le
affiche Bonjour les Amis
affiche
Bonjour
les
Bonjour les Amis
3
Le passage de plus de neuf arguments nécéssiterait l'utilisation de la commande de shift qui déplace les arguments ($3 devient $2, $2 devient $1..).
Nous avons vu qu'Unix possédait un certain nombre de variables d'environnement telles que $PATH, $TERM etc., l'utilisateur a la possibilité de créer ses propres variables.
MAVARIABLE="Ce que je veux"
echo $MAVARIABLE
Ce que je veux
Mais cette variable n'est visible que dans le shell courant, pour qu'elle soit accessible dans un sous-shell (un paragraphe est consacré au sous-shell), il faut utiliser la commande export.
Dans le module d'introduction au système Unix nous avons abordé un certain nombre de commandes simples ainsi que l'utilisation des boucles et des tests. Nous allons également aborder l'usage de commandes telles que awk qui sont à elles seules de véritables utilitaires.
Voyons maintenant comment utiliser ces possibilités du shell en partant d'un exemple. Nous allons décortiquer ensemble un programme simple qui affiche les différents sous répertoires à partir d’un répertoire passé en paramètre. Les sous répertoires sont affichés de manière décalée pour mettre en valeur l’arborescence des fichiers sous Unix. Le résultat obtenu par ce petit programme n’est certes pas des plus raffinés, mais nous avons tenu à rester simple pour être plus compréhensible. Si le coeur vous en dit, vous pouvez l’améliorer de façon par exemple à obtenir un affichage d’arborescence digne du gestionnaire de fichiers de Windows.
UTILISATION=" Utilisation : $0 [répertoire] "
case $# in
0);;
1) if [ ! -d [email protected] ] ; then
echo "$1 : n’est pas un répertoire " >&2
echo $UTILISATION >&2
exit 2
fi ;;
esac
cd $1
cdir =`pwd`
find . -type d -print | sort -b |
awk -F/ ‘
{
if ( NF > 1)
for (i = 0; i < NF; i++)
printf " "
printf "‘/’ %s \n", $NF
} ‘ | pg
La variable UTILISATION est initialisée avec un texte incluant la variable $0 du shell qui contient le nom de la commande (le nom du fichier script exécutable).
La première partie du script est constituée d’un test de type choix-multiple ‘case ..esac). Si le premier paramètre (contenu dans $1) n’est pas un fichier de type répertoire (-d), alors (then) le programme affiche le contenu de la variable UTILISATION sur la sortie des erreurs ( >&2).
Sinon ,la deuxième partie du script s’exécute.
La commande cd$1 effectue un positionnement dans le répertoire passé en paramètre au script. La commandefind.-typed-print, recherche à partir du répertoire courant tous les fichiers de type répertoire. Le résultat de cette commande est passé à la commande de tri (sort -b) par l’intermédiaire du canal (|) qui elle retransmet à son tour à la commande awk.
La commande awk -F/ est là pour manipuler les données, l’option -F/ redéfini le séparateur d’arguments comme étant le "/ ".
Si le nom de répertoire ainsi isolé n’est pas à sa première occurrence (cf. l’usage de la boucle for) alors la fonction d’affichage formaté printf imprime des espaces blancs, sinon elle imprime le nom du répertoire contenu dans la variable d’awk $NF précédé d’un "/ ". Le tout ainsi re-formaté est envoyé vers la commande pg qui permet un affichage page par page.
? La syntaxe de la boucle (for i = 0; i < NR; i++) est celle utilisée par awk, la syntaxe d’une boucle for du Korn Shell s’écrit : for i in variable.
Nous reviendrons sur l’utilisation des commandes awk et printf.
On s’aperçoit par cet exemple que le shell nous permet d’écrire des programmes tout à fait complet nous permettant de pallier à l'absence de telle ou telle commande particulière sous Unix. Si l’on souhaite créer de véritables application, il est sans doute souhaitable de se familiariser avec le langage C qui reste très certainement plus souple d’emploi.
Nous allons prendre un deuxième exemple pour illustrer les possibilités du shell. Nous allons utiliser une nouvelle commande appelée grep qui permet de rechercher une chaîne de caractère dans un ou plusieurs fichiers. Malheureusement cette commande ne permet pas d'ausculter un ensemble de sous répertoires, c'est pourquoi une fois encore nous allons utiliser la commande find pour parcourir l'arborescence d'Unix puis utiliser l'option -exec de cette commande pour exécuter la commande grep elle même. Cette commande étant assez longue à taper, on pourra la stocker dans un fichier script. Les variables $1 et $2 contenant les arguments 1 et 2 passés au fichier (à savoir le chemin d'accès du répertoire et la chaîne de caractères à trouver)..
find $1 -type f -exec grep -l $2 {} \;
Les Scripts peuvent être maniés de manière récurcive, c'est ce que nous pourrons constater avec l'exemple suivant, mais cela doit être fait avec une grande prudence si l’on veut être sur que le programme puisse s’arrêter de lui-même.
UNIX possède un ensemble de commande qui permettent d’accèder à une partition DOS et d’y récuperer des fichiers mais ces commandes ne permettent pas par exemple de rappatrier un ensemble de répertoires ou de sous-répertoires. Nous allons essayer de contourner cette difficulté par le programme shell suivant que nous appellerons par exemple recupdos.:
Nous lancerons cette commande de la manière suivante :
$ recupdos c:/repertoire
Le contenu du programme :
var=$1
for i in `dosls $var`
do
doscp $var/$i . 2>/dev/null
if [ $? != 0 ]
then
mkdir $i
cd $i
pwd
/usr/tools/recupdos $var/$i
cd ..
fi
done
La boucle foriin récupère la liste des fichiers et des sous répertoire sous c:/repertoire.
A l’intèrieur de la boucle la commande doscp copie les fichiers dans le répertoire courant (.). Si doscp essaie de copier un répertoire il renvoie dans $? un code d’erreur (différent de 0), alors le programme crée un répertoire unix dans le répertoire courant, on se déplace dans le répertoire et la commande est appellée à nouveau avec pour paramètre la concaténation de la variable de départ et du nouveau répertoire ($var/$i) etc puis on remonte d’un cran dans l’arborescence (cd ..)..
Chaque commande script que nous lançons donne naissance à un processus fils. La commande est alors exécutée dans le shell-fils ou sous-shell. La commande " . " (dot) permet d’exécuter la commande dans le shell courant. Prenons par exemple le script suivant que nous appelerons sousshell lancé à partir de notre répertoire courant /usr/pat :
pwd
cd /usr/bin
pwd
Si nous l’exécutons normalement nous obtiendrons (en supposant que l’on aie bien modifié la variable PS1 pour afficher le répertoire courant) :
/usr/pat> sousshell
/usr/pat
/usr/bin
/usr/pat>
Si nous l’exécutons maintenant précédée d’un point " . "
/usr/pat> . sousshell
/usr/pat
/usr/bin
/usr/bin>
Dans les deux cas le changement de répertoire s’est bien effectué mais dans le premier cas il s’est fait dans le sous-shell, dans le deuxième dans le shellcourant.
Ce type de commande (dot) sera fort utile pour pouvoir modifier les variables d’envirronement du shell courant. Un autre usage interressant de cette commande réside dans la possibilité qu'elle donne d'exécuter le .profile sans obliger un déconnexion préalable.
Depuis quelques années le Korn Shell a peu à peu pris le pas sur le Bourne Shell dont il reconnaît toutes les commandes. Le Korn Shell se présente comme une amélioration du Bourne Shell avec l’apport notamment :
$ alias dir=‘l -F’
On utilisera dir au lieu de l -F. C'est donc un outil de gain de temps très précieux que l'on peut utiliser pour toutes les commandes complexes employées fréquemment.
Ces alias ne sont pas conservés par le système à la sortie du shell, il est donc conseillé de les stocker dans un fichier ou de les intégrer par exemple dans son .profile qui les chargera à chaque nouvelle session..
<ESC> k
k
j
k et j permettent de naviguer d'avant en arrière dans le fichier historique.
$ PS1=‘$PWD >‘ ; export PS1
/usr/bin >
/usr/bin > cd /etc/default
/etc/default >
A partir d'un autre shell, la commande ksh ouvre une session du Korn Shell dont on peut sortir par la commande exit.
La configuration la plus souhaitable est sans aucun doute de pouvoir accèder au Korn Shell dès la connexion. Le rôle en revient alors à l'administrateur du système qui associera au compte utilisateur l'usage du Korn Shell comme shell de travail.
Le système va alors créer un fichier .profile spécifique qui sera exécuté lors de la connexion. L'utilisateur peut modifier ce fichier, il devra naturellement utiliser la syntaxe du Korn Shell pour ajouter les instructions qu'il juge nécéssaires (par exemple il peut y mettre les lignes qui modifierons l'affichage du prompt). L'administrateur peut à tout moment modifier le shell choisit par défault (notament par l'utilitaire sysadmsh).
Cette commande (qui n'est pas spécifique au Korn Shell) permet de tester :
Condition | Résultat : VraiSi |
Fichiers | |
-b | Fichier Spécial (bloc) |
-c | Fichier Spécial (caractère) |
-d | Répertoire |
-f | Normal |
-g | SGID (permissions du groupe) positionné |
-k | Sticky Bit positionné |
-s | Non vide |
-u | SUID (permission utilisateur) positionné |
-r | Accessible en lecture |
-w | Accessible en écriture |
-x | Exécutable |
Chaînes | |
< >=< > | Chaines égales |
< >!=< > | Chaines différentes |
< > | Non vide |
-n | Nulle |
-z | Non Nulle |
Nombres | |
-eqou= | Egal |
-neou!= | Non égal |
-geou>= | Plus grand ou égal |
-leou<= | Plus petit ou égal |
-gtou> | Plus grand |
-ltou< | Plus petit |
Rappellons que la commande test peut s'écrire de deux manières différentes
test num1 -ne num2
équivaut à :
[ num1 != num2 ]
if [ -d $1 ]
then
echo $1 "est un répertoire"
else
echo $1 "n'est pas un répertoire"
fi
Ces commandes peuvent être imbriquées.
La syntaxe du choix multiple se décline ainsi :
echo "Répondez par (O/N) : \c"
read var
case $var in
[oO]) echo "Donc c'est Oui !" ;;
[nN]) echo "Donc c'est Non !" ;;
*) echo "Vous n'avez le choix qu'entre O et N !" ;;
esac
La dernière instruction de chaque élément de choix (il peut y en avoir plusieurs, elles sont alors séparées par un ";") doit se terminer par ";;".
La commande read attend une frappe du clavier.
L'équivalent de cette commande en C-Shell est la commande switch.
Ces deux opérateurs permetent de chaîner des commandes. L' opérateur && (ET) exécutera la deuxième commande si la première s'est exécutée correctement (valeur de retour 0), l'opérateur || (OU) fait exactement le contraire.
$ lp CeFichier && rm CeFichier
Le fichier CeFichier ne sera détruit que si il a pu être imprimé au paravent.
Le Korn Shell utilise trois boucles :
for do done
while do ..done
until do done
var=$2
for i in `find -type f -print`
do
grep -l "$var" $i
done
Ce script recherche une chaîne de caractères passée en argument par $2 dans une liste de fichiers standards recherchés à partir de $1 par la commande find. Cette commande s'exécute (utilisation des `` (touche 7)) et les arguments sont récupérés par la boucle for.et la variable $i.
Références :
SCO?UNIX Operating System
User’s Guide
Chapitre : Simple Programming with awk
Chapitre : Manipulating Text with sed.
La manipulation de grandes chaînes de caractères n’est pas toujours facile avec les commandes du Shell, les utilitaires sed et awk sont là pour y pallier. Ils sont en soit de véritables langages de programmation.
Ces utilitaires lisent le contenu d’un fichier ou d’un canal d’entrée standard, les données peuvent être modifiées puis redirigées vers la sortie standard où vers un autre canal. Les utilitaires sed et ed ont une syntaxe d’exécution comparable :
$ awk ‘commandes awk’ fichier
$ sed ‘commandes sed’ fichier
Les commandes sed et awk peuvent être groupées dans un fichier de commandes. Ces même commandes appartenant à awk et sed peuvent être combinées aux commandes du shell dans un même script.
who | awk ‘
{
printf("%10s travaille sur %s depuis %s Heures \n", $1, $2, $5)
}
‘ | more
Affichera par exemple :
root travaille sur tty01 depuis 08:58 Heures
patrick travaille sur tty02 depuis 10:12 Heures
Le résultat de la commande who est passé à awk par le canal pipe et awk le reformate par l’appel à la commande printf, le résultat final est à nouveau canalisé par pipe vers la commande more pour un affichage ligne à ligne (au cas ou plus d’une page de réponses s’afficherait).
La commande awk utilise entre autres la fonction printf (issue de la bibliothèque standard du langage C) et qui permet l’affichage formaté des variables qui lui sont passées en argument. Le premier signe %s reçoit la valeur du premier argument etc , la partie entre guillemets peut recevoir du texte, des tabulations, des retours chariot. Les caractères spéciaux doivent être précédés d’un " \ ".Le format d’affichage est décrit dans le tableau ci-dessous :
Symbole | Signification |
%s | Chaîne de caractères |
%10s | Chaîne de dix caractères |
%d | Décimal |
%x | Hexadécimal |
%f | Chiffres à virgule flottante |
%2.3f | 2 Chiffres avant la virgule, 3 après |
La commande print équivaut à printf sans argument.
Voyons quelques fonctions de sed.
Cdes | Signification | |
b | Branchement à l'étiquette | |
:étiq | Définition de l' Etiquette | |
d | delete | Supprime la ligne et lit la suivante |
i\ | insert | Insère une ligne |
p | | Affiche le contenu du buffer |
q | quit | Sort du programme |
s | substitute | Remplacement de caractères (s/texte/nouveau/option) |
s/ | ||
{ } | Traite les commandes incluses dans les { } |
sed lit les lignes de fichiers d’entrée une à une et leur applique les commandes de la liste:
$ who | sed ‘s/ .*//’
root
patrick
$
sed récupère le résultat de la commande who et par la commande s (substitute) remplace la fin de la ligne par un espace.
Sans vouloir aborder toutes les possibilités de awk qui sort du cadre de l’approche générale que nous voulons en avoir, nous allons répertorier la liste des variables pré-définies par awk.
Variable | Signification |
ARGC | Le nombre d’arguments sur la ligne de commandes |
ARGV[] | La liste des arguments dans un tableau |
FILENAME | Nom du Fichier courant d’entrée |
NR | Numéro de l’enregistrement courant |
FS | Séparateur de champ |
NF | Compteur de Champs |
FNR | Nombre de lignes du fichier actif |
OFS | Séparateur de Champ de Sortie |
ORS | Séparateur d’Enregistrements en Sortie |
RS | Séparateur de Lignes de Saisie |
Un exemple inutile et amusant :
awk ‘ { ligne[NR] = $0 }
END { for (i = NR; i > 0; i--) print ligne[i] }
‘ $*
Ce programme lit un fichier passé en paramètre et l’imprime à l’envers.
L'utilitaire awk permet de provoquer des actions avant et aprés le corps de la fonction awk par l'utilisation des mots-clefs BEGIN et END. Attention, de même que le "`" doit être sur la même ligne que le mot awk, l' "{" qui suit les mots-clefs BEGIN et END doit être également sur la même ligne que ces mots-clefs.
awk -F: `
BEGIN {
print " Impression de l' Entête"
}
{
print "Corps de la fonction "
}
END {
print "Fin de la commande"
} `
Le script reçoit en paramètre le nom du fichier par l’intermédiaire de la variable $*
Enfin on remarquera l’utilisation de la boucle for() dans la ligne de commandes de awk.
La commande awk respecte la syntaxe suivante :
awk options ' { programme } ' fichiers.( ou canal d'entrée).
La commande awk possède également des fonctions intégrées pouvant manier des chaînes de caractères telles printf(" ", ), sprintf("", ), length(s) ou des fonctions mathématiques telles cos(x), sin(x), rand() etc. La fonction getline charge successivement chaque ligne à partir d'une variable, d'un fichier ou d'un pipe..
Références :
SCO?Open Systems Software
Programmer’s Reference Manuel Volume 1
et
Programmer’s Reference Manuel Volume 2
Notre but n’est pas ici de faire l’apprentissage du langage ‘C’, ce qui est un long processus, mais de comprendre son usage et ses particularités essentielles.
Le développement du langage C est intimement lié à l’histoire d’UNIX pour lequel il a été écrit. Dennis Ritchie développa le langage de programmation C (à la suite du B) en 1972 dans le but de rendre UNIX flexible et portable. Il travailla ensuite avec Brian Kernighan qui préparèrent en 1978 une première standardisation du langage C.
Par ce langage les auteurs ont cherché à créer un outil qui puisse travailler au plus près de la machine sans avoir à faire face à la complexité de l'assembleur.
Grâce à l’introduction de la normalisation ANSI (American Standard Insitut) en 1984, les fonctions de la bibliothèque permettant la gestion du système d’exploitation ont été standardisées.
Le langage C possède entre autres spécificités, celle de pouvoir accéder à des adresses mémoire par l’intermédiaire de pointeurs et la possibilité de faire du calcul d'adresses.
Le langage C autorise également les opérations sur les bits.
Toutes les fonctions peuvent être appellées de façon récursive et les variables locales sont recrées à chaque appel ce qui les protèges de toute modification abusive.
Si le langage C est un langage procédural, son évolution avec le C++ lui permet d’enter dans le monde de la programmation objet avec l'introduction des notions de classes et de types de données.
? L’ouvrage de référence sur le langage C est celui écrit par ses auteurs. le langage C par Kernighan & Ritchie.
La création d’un programme en C passe par trois étapes :
La commande cc appelle le compilateur.
La commande link appelle le linker.
cc peut être invoquée pour lancer conjointement les options de création de liens
$ cc -o test test.c
Cette commande va successivement passer le pré-processeur sur le fichier source test.c puis créer (en appellant indirectement le link) un exécutable (nommé a.out en Bourne Shell et test en Korn-Shell).
cc possède une très grande panoplie d’options, c’est pourquoi il est souvent utile de grouper les commandes de compilations dans un fichier make qui permettra de créer un fichier exécutable personnalisé.
? Un mot sur la commande lint.
L’utilitaire lint, à l’image de l’analyseur syntaxique du compilateur, réalise un certain nombre de vérifications poussées d’un fichier de sources C. Il détecte entre autres les instructions qui peuvent poser des problèmes de portabilité. Il détectera également des instructions sans effet ou des variables déclarées et non utilisées.
Un programme en langage C se compose d’une ou plusieurs fonctions qui peuvent être réparties dans un ou plusieurs fichiers sources.
Tout programme C comporte une fonction (et une seule) portant le doux nom de main.(principal), c’est le point de départ d’exécution de tout programme C.
Une fonction se décrit de la manière suivante :
Sa déclaration :
valeur de retour NomFct(type paramètre 1, type paramètre 2, etc ..);
par exemple :
void FonctionF(int, char *);
Toute fonction C doit être au paravent déclarée pour que le compilateur puisse vérifier la validité des passages de paramètres. Cette déclaration s'appelle prototypage.
Lorsqu'une fonction est susceptible d'être utilisée par plusieurs modules, il est recommendée d'inclure son prototype dans un fichier d'entête.
Une fonction elle même se présente de la manière suivante :
void FonctionF (int param_un, char *param_deux)
{
déclaration des variables locales;
instructions;
appels à d’autres fonctions();
instructions;
}
Nous verrons les différents types de variables qu’utilise le langage C.
Un fichier de sources C peut se présenter ainsi :
/* Des lignes de commentaires */
#include<stdio.h> /* Insertion de fichiers d'en-tête */
#define MAX 2555 /* Déclaration de constantes */
#define PI 3.1416 /* */
/* Première Fonction */
int FirstFonct(void)
{
corps de la fonction;
}
/* Deuxième Fonction */
char SecondFonct(char *param)
{
corps de la fonction;
}
Les variables numériques :
Nom | Signification |
short int | Un entier court |
long int | Un entier long |
long double | Un entier double |
float * | Nombre à virgule flottante |
Chacun de ces types peut être précédé de signed ou unsigned (signé, non signé).
Les variables de type caractère :
Nom | Signification |
char | Un caractère |
char[n] | Un tableau de n caractères |
char * | Un pointeur sur une chaîne de caractères (Le sujet des pointeurs fera l’objet d’un chapitre particulier) |
Les variables en langage C appartiennent à des classes différentes :
Nom | Signification |
auto | Variables accessibles dans la partie où elles sont déclarées |
static | Ces variables gardent leur valeur au delà de la fonction dans laquelle elles sont déclarées |
extern | Variables déclarées dans d’autres fichiers et que l’on rend ainsi accessibles |
constant | Valeur constante comme son nom l’indique |
register | Si une variable est déclarée dans cette classe le système essaiera de la placer dans un des registres du processeur (gain en temps d’accès) |
Les structures permettent regrouper des données de type différents dans une même variable :
struct NomStruct
{
int VarNum;
char VarChar;
char *VarChaine;
};
Les champs d’une structure sont accessibles par deux procédés différents,
par valeur :
NomStruct.VarChaine;
par pointeur :
NomStruct?VarChaine;
L’utilisation des pointeurs fera l’objet d’une étude spécifique dans notre survol du langage C.
En parcourant l’architecture d’un programme UNIX, nous avons donc déjà rencontré la notion de fonction.
Une fonction pourrait être définie comme une séquence d’instructions pouvant être réutilisée à partir de différents points (et même à partir de différents programme s’il a été mise en bibliothèque).
La norme ANSI a introduit la notion de prototypage des fonctions définissant le type et le nombre de paramètres passés à la fonction ainsi que le type de la valeur retournée.
void FonctionA(int , char *);
La fonction FonctionA reçoit un integer comme premier paramètre, un pointeur surune chaîne de caractères en deuxième paramètre et ne renvoi rien (void).
Ce prototypage servira au compilateur pour contrôler la validité des paramètres passés à la fonction lors de son appel.
Vous rencontrerez certainement des fonctions ou les paramètres sont déclarés en dehors des parenthèses : il s’agit là d’une ancienne forme (prés ANSI).
Le langage C permet d’utiliser des pointeurs sur des fonctions, ce qui permet :
Les fonctions écrites en C peuvent être récursives , ce qui signifie qu'une fonction peut s'appeller elle même, soit directement, soit indirectement. Les variables locales étant récrées à chaque nouvel appel de la fonction.
? La récursivité doit toujours être maniée avec une grande prudence par le programmeur.
Nous avons vu dans le chapitre sur les variables, les classes mémoires auxquelles elles peuvent appartenir. Ces classes mémoires sont caractérisées par une durée de vie et une zone de visibilité.
Classe | Durée de Vie | Zone de Visibilité |
Auto | Bloc | Bloc |
Extern | Programme | Programme |
Register | Bloc | Bloc |
Static | Programme | Bloc |
Un programme C dispose de quatre sortes de mémoires pour son exécution :
La bibliothèque standard C contient trois fonctions d’allocation déclarées dans <stdlib.h> :
Nom | Signification |
malloc() | Alloue un objet |
calloc() | Alloue un tableau d’objets |
realloc() | Modifie la taille d’une zone préalablement allouée |
free() | Libère la place mémoire préalablement allouée |
malloc et calloc renvoient un pointeur sur le début de l’objet à allouer.
#define TAILLE 21
char *chaine;
chaine = (char *) malloc(TAILLE);
memset(chaine, '\0', TAILLE);
.
free(chaine);
La commande free() sera utilisée pour libérer la place mémoire résultante de l'allocation dynamique. Attention allouer une variable ne signifie pas l'initialiser, d'où l'usage de la fonction memset() pour initialiser chaine à '\0'.
Opérateur | Fonction | Ordre d’exécution |
+ | Addition | Gauche ? Droite |
- | Soustraction | |
< | Inférieur | |
<= | Inférieur ou Égal | |
> | Supérieur | |
>= | Supérieur ou Égal | |
= = | Égal | |
!= | Non Égal | |
= | Affectation (! Ne pas confondre avec = =) | Droite? Gauche |
+= | Affectation Numérique | |
-= | ||
*= | ||
/= | ||
? : | Condition (Si vrai .. alors ..) | |
&& | ET Logique | Gauche ?Droite |
|| | OU Logique |
La notation i++ équivaut à i = i + 1
? i = i + 1;Attention
if ( VarA = VarB)
est toujours vrai. Il ne faut pas confondre avec :
if ( VarA = = VarB)
Les opérateurs binaires permettent de condenser des informations sur un très petit espace mémoire. Ils appellent quelques précisions complémentaires que nous allons illustrer par les exemples suivants à partir de deux valeurs x et y :
x | 0 0 0 0 1 0 1 1 | 0x0B | 11 en décimale |
y | 0 0 1 1 0 1 1 1 | 0x35 | 55 en décimale |
Opérations | Résultat en Binaire | Hexa | Signification |
x & y | 0 0 0 0 0 0 1 1 | 0x03 | Et Bit à Bit |
x | y | 0 0 1 1 1 1 1 1 | 0x3F | OU Bit à Bit |
x ^ y | 0 0 1 1 1 1 0 0 | 0x3C | OU Exclusif Bit à Bit |
x << 4 | 1 0 1 1 0 0 0 0 | 0xB0 | Décalage à Droite |
y >>4 | 0 0 0 0 0 0 1 1 | 0x03 | Décalage à Gauche |
~x | 1 1 1 1 0 1 0 0 | 0xF4 | Complément à 1 |
for (i = 0; i < 100; i++)
{
action();
}
La variable i initialisée à zéro est incrémentée jusqu’à 100 et à chaque itération la fonction action() est appelée.
while (température > 10)
{
printf(" Il fait bon ");
température--;
}
Traduction : Tant que la température est supérieure à 10°, on affiche " il fait bon ".
et l’on décrémente la température d’un degré.
La boucle while peut s’écrire de la manière suivante
do
{
action();
} while( var > 10);
Dans ce cas la boucle sera effectuée au moins une fois quelque soit le résultat du test de la boucle while.
Le mot-clef break permet de sortir d’une boucle.
Le mot-clef continue ramène le déroulement du programme au test de la boucle.
Très simples :
if (condition)
{
action(1);
}
else
{
action(2);
}
switch(expression)
{
case Valeur_1 : action();
break;
case Valeur_2 : action();
break;
default : break;
}
L’expression évaluée dans switch est comparée à chaque valeur placée derrière le mot-clef case si les valeurs sont identiques les instructions qui suivent sont exécutées, sinon ce sont les instructions placées derrière le mot-clef default qui sont exécutées.
Un pointeur est une variable qui contient l’adresse d’un objet donné. Cet objet est d’un type particulier.
On déclare ainsi
int *pt_var_num; un pointeur sur une variable numérique (int).
char *pt_var_char; un pointeur sur une variable de type caractère.
L’opérateur & permet d’affecter l’adresse de la variable au pointeur.
Utilisons un petit programme pour mieux comprendre le fonctionnement des pointeurs :
int main(void)
{
int tab[5]; /* Une tableau de variables numériques */
int *pt_int; /* Un pointeur vers une variable numérique */
int i;
for (i = 0; i < 5; i++)
tab[i] = i + 10; /* Le tableau est initialisé */
pt_int = &tab[0]; /* Le pointeur prend l’adresse du 1er élément du
tableau */
for (i = 0; i < 5; i++)
{
printf("\n *pt_int = %d &tab[%d] = %X, pt_int = %X, &pt_int = %X",
*pt_int, i, &tab[i], pt_int, &pt_int);
pt_int++;
}
? printf(); est une fonction assez complexe de la bibliothèque standard qui permet un affichage formaté des valeurs qui lui sont passées.
Le résultat de la fonction ci-dessus donne par exemple :
*pt_int = 10 &tab[0] = 7FFFD90 pt_int = 7FFFD90 &pt_int = 7FFFD48
*pt_int = 11 &tab[1] = 7FFFD94 pt_int = 7FFFD94 &pt_int = 7FFFD48
*pt_int = 12 &tab[2] = 7FFFD98 pt_int = 7FFFD98 &pt_int = 7FFFD48
*pt_int = 13 &tab[3] = 7FFFD9C pt_int = 7FFFD9C &pt_int = 7FFFD48
*pt_int = 14 &tab[4] = 7FFFDA0 pt_int = 7FFFDA0 &pt_int = 7FFFD48
L'adresse du pointeur (&pt_int) ne change pas (7FFFD48) mais son contenu prend successivement l'adresse de l'élement suivant et sa valeur affiche la valeur de l'élément correspondant du tableau. Un integer étant cadré sur quatre octets, la progression se fait donc par tranche de quatre octets.
7FFFD90 | 7FFFD94 | 7FFFD98 | 7FFFD9C | 7FFFDA0 | |
? | ? | ? | ? | ? | |
10 | 11 | 12 | 13 | 14 | |
7FFFD48 | |||||||||
? | |||||||||
7FFFD90 | Contenu | de pt_int | |||||||
pt_int++;
7FFFD48 | |||||||||
? | |||||||||
7FFFD94 | Contenu | de pt_int | |||||||
? Un pointeur doit toujours être initialisé.
Les pointeurs pouvant faire référence à n'importe quel type d'objet, ils peuvent être utilisés pour transmettre une fonction en paramètre à une autre fonction. Un tel procédé donne une grande souplesse à la programmation en langage C.
En toute logique rien n'interdit à un pointeur de pointer vers un autre pointeur (qui lui même peut pointer sur un autre pointeur etc .Attention néanmoins à garder les déclarations déchiffrables.
Les pointeurs reconnaissent trois types d’opérateurs : +, - et =.
Si un pointeur est incrémenté d’une unité, il pointe sur l’adresse suivante:
alors que la même opération sur un pointeur vers une variable de type char (caractère) n’augmenterai l’adresse que d’un octet.
En C, le type chaîne de caractères n’existe pas en tant que tel. Néanmoins nous ne sommes pas démunis pour autant.
Nous avons vu précédemment qu’il était possible de déclarer des tableaux de caractères.
char chaîne[10];
Une deuxième manière de déclarer une chaîne de caractères est de déclarer un pointeur sur le premier caractère de cette chaîne.
{
char *pt_chaine; /* Déclaration */
chaîne = (char *) malloc(20); /* Allocation */
strcpy(chaîne, " Hello world "); /* Affectation */
strcat(chaîne, " \0 "); /* Fin de Chaîne */
.
free(chaîne); /* Libération */
}
Toute chaîne de caractères doit se terminer par un '\0'. Dans l'exemple ci_dessus, le programme réserve une zone mémoire assez grande pour recevoir la chaîne de caractères suivie du caractère final ('\0') par l'intermédiaire de la fonction malloc().
5EEEEA0 | |||||||||||||||||||||||
? | |||||||||||||||||||||||
H | e | l | l | o | W | o | r | l | d | \0 | |||||||||||||
7FFFD48 | |||||||||
? | |||||||||
5EEEEA0 | Contenu | de chaine | |||||||
chaine++;
7FFFD48 | |||||||||
? | |||||||||
5EEEA1 | Contenu | de chaine | |||||||
Les pointeurs de fonctions offrent un mécanisme particulierement performant. Ils permettent d'appeller telle ou telle fonction suivant les conditions d'exécution d'un programme avec une grande souplesse.
int fonct(void); est un prototype de fonction
int (*pt_fonct)(void); est un pointeur de fonction renvoyant un int
Cependant les pointeurs de fonctions souffrent de certaines restrictions par rapport aux pointeurs des autres types. D'une part il est impossible de les incrémenter ou de les décrémenter, d'autre part ils ne peuvent être initialisés que par parasitage et non par allocation dynamique.
void main(void)
{
int (*pt_fct) (void);
pt_fct = fonct; /* parasitage : pt_fct pointe vers fonct */
}
Si le C comprend un nombre limité d’instructions, la bibliothèque standard fournit de nombreuses fonctions qui permettent de gérer les entrées sorties.
Les fonctions de la bibliothèque standard qui permettent d’effectuer des traitements sur des fichiers sont définies dans le fichier d’en-têtes <stdio.h>.
Nom | Signification |
fopen, fclose | Ouverture /fermeture |
fgetc, fgets, fread, fscanf | Lecture |
fputc, fputs, fwrite, fprintf | Ecriture |
fseek | Déplacement |
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp;
if ( fp = fopen(argv[1], "r")) == NULL)
{
fprintf(stderr, "\n %s ne peut ouvrir le fichier %s \n",
argv[0], argv[1]);
exit(1);
}
else
{
traitement();
}
fclose(fp);
exit(0);
}
Les fonctions effectuant des manipulations sur les fichiers telles fopen(), fclose(), fread() etc.. utilisent un pointeur de type FILE .Dans l'exemple ci-dessus, fopen() essaie de lire (mode "r", lecture seule) un fichier passé en paramètre au programme.
Comme nous l’avons vu précédemment, la gestion des processus est une des taches principales d’UNIX. Un programme écrit en C sous UNIX doit pouvoir être capable de créer des processus, ceci se produit grâce à l’appel de la primitive fork.
Sous UNIX, le langage C possède un certain nombre de primitives qui permettent le dialogue avec le système d’exploitation. Nous pouvons les regrouper en trois volets :
Nom | Signification |
fork, exec, wait | Exécutions paralléles |
semget, semctl, semop | Partage des ressources |
pipe, shmget, shmat, shmdt, msgget, msgsnd, msgrcv | Communications entre processus. |
Le langage C trouve sa véritable dimension dans le développement d'applications. Le problème qui se pose est alors de savoir découper les programmes en une multitude de fichiers sources. Ce découpage est très important, nous pensons qu'il doit tendre à créer des modules aussi réduits que possible et cela pour deux raisons essentielles :
La difficulté réside maintenant à faire le lien entre ces modules une fois ceux-ci compilés. Le programme principale peut comporter des programmes en assembleur parallèlement aux programmes écrits en C Tout cela est gèré par l'utilitaire make.
Comprendre l'utilisation de cet utilitaire signifie maîtriser toutes les options de compilations et d'édition de liens ce qui nécessite une expérience du langage C qui dépasse les limites d'une première approche aussi nous contenterons nous d'en reconnaître l'existence.
Références :
SCO?Development System
Debuggind Tools Guide
Lors de la création de programmes (quelque soit le langage !) le programmeur doit faire face à toutes sortes d'erreur dont il n'est pas toujours facile de déceler la cause.
Cette erreur cachée dans un programme s'appelle un bug en anglais (punaise, vermine) c'est pourquoi nous préferrons le terme de déverminage (rencontré dans une revue informatique) à celui sans signification de debogage (calqué sur debugger) comme il est traduit dans les ouvragers français..
Au delà des querelles de vocabulaire c'est bien évidemment les techniques de déverminage qui retiendrons toute notre attention.
L'utilitaire sdb (symbolic debugger) est un des outils d'Unix mis à notre disposition pour faciliter la recherche d'erreurs dans un programme, il nécessite la compilation des fichiers sources avec l'option -g (pour la reconnaîssance des numéros de ligne).
La commande cflow analyse les fichiers sources et nous informe sur l'imbrications des fonctions.
Le plus interressant et le plus complet des outils de déverminages se nomme CodeView, il se lance par la commande cv.
# cv executable
Le programme s'exécute au travers d'une fenêtre d'exécution modulable qui permet d'afficher le code source, le contenu des variables, le contenu des registres etc
On peut ainsi exécuter le programme pas à pas (touche de fonction F10) ou utiliser des points d'arrêt et suivre ainsi son déroulement dans les moindres détails, c'est là un très grand progrès par rapport aux versions précédentes.
La fenêtre d'affichage du code source permet d'afficher (Touche F3) pour un même fichier source (.c) le code c ou le code assembleur. et même d'avoir les deux ensemble ligne par ligne.
/* Calcul des MAX Nombres Premiers */
#include <stdio.h>
#define VRAI 0
#define FAUX 1
#define MAX 150
void main(void)
{
register int i, j, k;
int tab[MAX+1] = {VRAI};
for (i = 2; i <= MAX; i++)
{
j = i - 1;
while (j++)
{
k = i * j;
if ( k <= MAX)
tab[k] = FAUX;
else
break;
}
}
for ( i = 3; i <= MAX; i++)
{
if (tab[i] = = VRAI)
printf("%d est un Nombre Premier \n", i);
}
}
Le fichier d'en-tête stdio.h est inclus car il contient le prototypage de la fonction printf.
Les constantes VRAI, FAUX et MAX sont déclarées grâce au mot-clef #define
Ce cours programme qui affiche les MAX premiers nombres premiers n'est constitué que d'une fonction principale main qui ne retourne aucune valeur (void).
Le principe de ce programme est de stocker dans un tableau de MAX éléments une valeur (0 ou 1) suivant que l'indice de chaque élément est à VRAI ou à FAUX.
Le tableau est initialisé à VRAI lors de sa déclaration, puis à chaque fois qu'un nombre compris entre les bornes 2 et MAX, est le résultat d'une multiplication (donc non premier) on met l' élément du tableau dont il est l'indice à FAUX. Pour raccourcir le temps d'exécution du programme, on sort de la boucle while (tant que j s'incrémente) grâce au mot-clef break, lorsque le résultat de la multiplication dépasse la borne supérieure.
Il n'y a plus qu'à afficher à l'écran (fonction printf) tous les nombres (parcourus par la boucle for) dont l' élément correspondant du tableau est à VRAI.
Un autre exemple va nous permettre d’aborder la manipulation des bits. Ce programme permet de convertir une valeur décimale en binaire et de la’afficher sous ce format.
#include <stdio.h>
#include <string.h>
#define MASK 0x80
char tab[9] = {'\0'};
char *conversion(int);
void main(void)
{
int nbre;
printf("\n Entrez le nombre à convertir : ");
scanf("%d", &nbre);
printf("Valeur %d = %s \n", nbre, conversion(nbre));
}
char *conversion(int valeur)
{
int i;
for (i = 0; i < 8; i++)
{
if (valeur & MASK)
tab[i] = ‘1’;
else
tab[i] = ‘0’;
valeur <<= 1;
}
return(&tab[0]);
}
La fonction scanf récupère la saisie de l'entrée standard (le clavier). Il est à noter que scanf à la différence de la fonction printf utilise le passage par adresse (&nbre).
La fonction conversion a pour paramètre un entier sur 8 bits, dans une boucle on compare chaque bit au masque de référence (0x80 soit 10000000 en binaire) puis on décale l’entier vers la gauche d’un bit. Lorsque le contenu de la variable " valeur " correspond au masque de référence un 1 est chargé dans le tableau " tab " (variable globale) qui sera passé à la fonction printf par l'intermédiaire du pointeur sur la fonction conversion(). Un bon exemple valant mieux qu’un long discours, il suffit de faire tourner ce programme (éventuellement en affichant les valeurs intermédiaires de la variable " valeur ") pour comprendre ces opérations sur les bits.
afpa | Auteur | Formation | Module | Date | Page N°1 |
Nanterre | P.Gobé | Unix | Program. | 1/4/2018 |