Archives du mot-clé langage C

Quelques liens utiles pour programmer sereinement

www

A la demande de quelqu’un, voici quelques uns des liens que j’utilise fréquemment pour programmer.

1) Sites généraux:

  • Google code: très utile pour trouver un petit bout de code, un fichier.h mystérieux, etc..
  • Développez.com: Sur ce site vraiment très utile, vous trouverez tout un tas de tutoriels très bien fait, et le forum est actif est très utile également. Ce site m’a bien dépanné pendant mon stage de deuxième année :).
  • Programmez.com: Site un peu similaire sur lequel on peut également trouver pas mal de tutos très intéressants.
  • Le site du zéro: On ne le présente plus, ce site est très adapté pour apprendre à partir du niveau débutant.
  • Et bien sûr, quoiqu’on n’y pense pas toujours, quelques articles de wikipédia dont celui sur la programmation ;).
  • Enfin, wikibooks sera sans doute l’un de vos amis les plus chers, ce site est vraiment génial :).

2) Sites spécifiques au langage C:

  • C++ Référence: très sympa pour retrouver rapidement le prototype d’une fonction et à quoi elle sert.
  • cplusplus.com: bon complément du précédent.

Notez que ce ne sont que les sites que je connais et que j’ai référencés dans les marque pages. Mais je ne prétend pas les avoir tous trouvés, ni avoir trouvé les meilleurs, donc pour des sites complémentaires sur le C, je vous laisser chercher ;).

3) Sites spécifiques au langage java:

  • Javadoc online: Comme son nom l’indique, ce site permet d’accéder online à la javadoc commune, vraiment très très utile. Je vous conseille de le mettre en anglais, car parfois on tombe sur des pages d’explication en français, mais qui ne sont pas de la vraie javadoc.

Et une fois encore je me limiterai à ça, même si j’ai tout plein de liens divers dans mes favoris sur le sujet. De toute manière beaucoup pointent vers l’un des sites généraux du premier paragraphe. Si vous avez besoin d’un site précis et que vous ne trouvez vraiment pas, laissez toujours un com, je regarderai si j’ai ça quelque part ;).

Autres langages:
Concernant les autres langages, je n’ai pas de site particulier les concernant, à part quelques pages spécifiques par ci par là. Pareil, jetez un coup d’oeuil au sites généraux, vous devriez pouvoir y trouver votre bonheur :o).

Compilation séparée (4) – Le Makefile

compilation_separee-41Bon nous voici enfin arrivés à la partie la plus attendue sans doute, celle du makefile. Comme d’habitude, je rappelle que cet article vient à la suite de plusieurs autres articles traitant de la compilation séparée, à savoir:

… et qu’il peut être utile de les lire si ce n’est déjà fait pour comprendre le présent article, car ils contiennent les notions pré requises sur lesquelles je ne reviendrai donc pas. Toujours comme d’habitude, je ne vais pas chercher à vous embrouiller avec des notions compliqués, ni à faire des makefiles de tueurs, ça pourra toujours faire l’objet d’un prochain article si le besoin s’en fait sentir. Non, aujourd’hui, nous nous contenterons du cas le plus simple et le plus compréhensible.

9) Le Makefile:

Nous nous intéressons toujours au même projet que dans les articles cités ci dessus. La structure en est rappelée pour mémoire dans l’illustration d’en tête. Nous avons vu au cours de l’article précédent, que pour compiler ce projet, il était nécessaire d’exécuter 4 commandes. C’est pas encore trop fastidieux comme s’il y avait une dizaine de fichiers objet à générer, mais ça commence tout de même à être un petit peu lourd, d’autant que chaque fois que vous allez faire une modification, il va falloir exécuter deux commandes au moins, une pour re-générer le fichier objet, et une pour re-générer le fichier exécutable. D’où l’idée de se simplifier la tâche en simplifiant tout ça.

Je ne sais pas si vous connaissez les scripts shell? Ce sont en gros des fichiers textes dans lesquels on place les suites de commandes répétitives que l’on taperai sinon les unes après les autres dans un terminal. Eh bien le principe du makefile est un peu similaire. Mais trêve de blabla, regardons le makefile que je viens de concocter pour le projet, et les commentaires viendront après 😉

makefile-2

Comme vous pouvez le voir, il y a un certain nombre de commentaires, reconnaissables au caractère ‘#’ qui débute les lignes, puis quatre groupes de deux lignes. Les sauts de lignes, comme les commentaires comptent pour rien, mais sont vivement conseillés dans un souci de clarification. Au niveau contraintes, la principale est que le fichier doit impérativement se nommer makefile. La tabulation que vous pouvez observer au début de chaque deuxième ligne est elle aussi impérative. Ne vous souciez pas des couleurs si chez vous elles sont différentes, en effet elles sont ajoutées par l’éditeur à partir du moment où il reconnait que vous faites un makefile (c’est à dire en général la première fois que vous enregistrez le fichier) dans un souci d’améliorer la lisibilité. Mais les conventions de couleurs peuvent varier selon la manière dont votre éditeur est configuré, et en tout état de cause, ce n’est pas à vous de les ajouter ou de vous en préoccupper.

Interprétation du makefile par le système:

Lors du lancement de la compilation par makefile via la commande ‘make‘, le système parcourt le makefile jusqu’à la première cible (une cible est le mot en vert suivit d’un ‘:’ qui débute chaque groupe de deux lignes). Donc dans le cas présent, il va aller examiner la cible ‘Programme:’, puisque c’est elle la plus haute dans le fichier. Il lit ensuite les noms de fichiers sur la même ligne que la cible, qui indiquent les dépendances à satisfaire pour procéder à la compilation. Ensuite il regarde parmis ces noms de fichier si certains correspondent à d’autres cibles. Si tel est le cas, il va sur toutes ces cibles l’une après l’autre, avant de poursuivre. Donc dans le cas présent, il va d’abord aller sur la cible ‘humain.o‘.

Au niveau de la cible ‘humain.o‘, il va lire la suite de la ligne, et constater que les dépendances du fichier ‘humain.o’ sont ‘humain.c‘ et ‘humain.h‘. Il va alors procéder à des vérifications sur les dates de dernière modification des fichiers. Si l’un des deux fichiers de dépendance s’avère être plus récent que le fichier ‘humain.o’, il va en déduire que le fichier ‘humain.o’ a besoin d’être recompilé, et exécutera donc la commande de compilation située à la ligne du dessous. Dans le cas contraire, il en concluera que cette cible est à jour et retournera vers la cible dont il vient. Donc dans le cas présent il va retourner vers la cible ‘Programme‘, continuer de vérifier les fichiers listés qui sont aussi des cibles, et vérifier de même voiture.o et conduire.o

Ceci fait, deux cas peuvent se présenter: s’il existe au moins une dépendance qui est plus récente que l’exécutable ou si l’exécutable n’existe pas ou plus, il sera régénéré grâce à la commande de compilation de la ligne du dessous. Sinon, il ne se passera rien car l’exécutable sera considéré comme étant à jour.

Généralisation:

Le cas traité ici est simple, mais vous pouvez tomber sur des make plus compliqués. Mais l’algorithme utilisé est toujours le même:

  1. Le système lit le makefile de haut en bas à la recherche de la première cible.
  2. Il regarde les dépendances de la cible, qui sont explicitées sur la même ligne.
  3. Si certaines de ces dépendances s’avèrent être elles même des cibles, il va les traiter l’une après l’autre avant de poursuivre le traitement de la cible en cours.
  4. Notez qu’il est tout à fait possible que parmi ces dépendances, il y en ait qui aient elles même des dépendances qui sont aussi des cibles, auquel cas, le système fera de même jusqu’à ce qu’il tombe sur une cible dont aucune dépendance n’est une cible.
  5. Il vérifie que si fichier.o concerné est à jour en comparant les dates de dernière modification des fichiers.
  6. Si ce n’est pas le cas, il procède à une recompilation suivant les instructions données à la ligne du dessous (après une tabulation à ne pas oublier surtout).
  7. Ensuite il remonte sur la cible d’où il vient, et continue de parcourir les cibles jusqu’à ce que tous les fichiers objets nécessaires pour la compilation finale de l’exécutables aient été mis à jour dans le cas où c’était nécessaire.
  8. Enfin, il procède si nécessaire à la (re)compilation du programme exécutable.

Conclusion:

En procédant de cette manière, il est certain de perdre le moins de temps système possible. Et pour l’utilisateur c’est beaucoup plus simple, vu qu’à chaque fois qu’il change quelque chose dans l’un de ses fichiers C, il a juste à taper ‘make’ dans le terminal (en se plaçant quand même dans le répertoire où sont placés les fichiers C et le makefile), et tout ce qu’il y a à recompiler est recompilé automatiquement.

10) Conclusion générale:

Vous disposez donc dorénavant de toutes les connaissances nécessaires pour profiter des joies de la compilation séparée. Vous verrez qu’au début, c’est toujours un petit peu pénible, parce qu’il faut faire l’effort d’apprendre, mais une fois qu’on a maitrisé l’outil, on utiliserai un makefile pour se faire des oeufs sur le plat si c’était possible ;).

Je vais maintenant m’arrêter quelques jours de parler du C (ça fait quand même quatre longs articles quatre jours de suite tout de même!), même si je pense que d’autres articles pourraient s’avérer utiles éventuellement, peut être un complément sur le makefile pour des usages plus avancés, ou encore un article sur l’utilisation de gdb pour débugger un programme, on verra ça :).

Compilation séparée (3) – Fichiers objets et compilation manuelle.

compilation_separee-3
Bon apparemment c’est un thème qui intéresse pas mal de visiteurs, et c’est tant mieux. Je rappelle pour ceux qui tomberaient précisément sur cette page que cet article s’inscrit dans une suite de quatre articles traitant de la compilation séparé, entre les articles Généralités sur la compilation séparée en C et Compilation séparée (2) – Dépendances qui le précèdent, et l’article Compilation séparée (4) – Le Makefile, qui le suit. Aujourd’hui nous allons nous intéresser aux fichiers objet, et étudier la compilation manuelle du projet en ligne de commande.

6) Les fichiers objet.

Je suis loin de maîtriser parfaitement ce concept, donc j’ai effectué quelques recherches sur Google. Il s’est très vite avéré que la documentation en français disponible à ce sujet était plus que limitée, donc comme d’habitude je suis allé voir sur des sites en anglais. Je vais essayer de parler simplement.

On se souvient que le compilateur a pour rôle de traduire un fichier source .c écrit dans un langage intelligible par l’être humain en un fichier contenant du code machine compréhensible par un ordinateur. Or il se trouve que la compilation passe par plusieurs étapes intermédiaires, et que pour chacune de ces étapes, on peut demander au compilateur de générer le fichier correspondant à cette étape.

Par exemple il va y avoir une étape où les #include et les #define vont prendre effet et où les commentaires vont être supprimés. Il va aussi y avoir une étape où le code C est transformé en langage machine, et il y a aussi une dernière étape appelée « édition des liens » durant laquelle toutes les dépendances entre fichiers différents sont résolues par inclusion du code correspondant dans un seul fichier (ce n’est pas tout à fait exact, mais ça suffit pour comprendre).

Et en gros le fichier objet correspond au moment au moment situé entre la traduction du C en langage machine et l’étape d’édition des liens. C’est une étape intéressante, car c’est la dernière où les fichiers peuvent rester séparés, vu qu’après tous deviennent nécessaires pour générer l’exécutable final.

7) Compilation des fichiers objet.

Par conséquent en compilation séparée, et dans les buts d’optimisation expliqués plus haut, on va compiler tous les fichiers en fichiers objets et s’arrêter là. Ensuite, on génèrera l’exécutable final à partir d’un programme de test, par exemple test.conduite.c (non représenté sur le dessin). Occupons nous tout d’abord de humain.o:

On va générer ce fichier à l’aide de la commande suivante: gcc -c humain.c

Cette commande est destinée à être exécutée dans un terminal. Bien sûr, il faut naviguer dans celui-ci jusqu’à votre répertoire de travail. Pour ceux qui utilisent un logiciel pour développer, ils ne le voient généralement pas, mais c’est ce que fait le système lorsqu’ils cliquent sur le bouton compiler. Dans cet exemple, je me place dans le cas le plus simple et pratique, pratique, celui du développement à l’aide d’un terminal et de fichiers textes sous Linux.

Notons que la commande donnée si dessus est celle utilisée lorsque l’on est sûr de son coup. En général lorsque l’on compile la première fois, on utilisera plutôt une commande du type suivant, dont je vais détailler la signification: gcc -g -Wall -c humain.c

  • gcc est le nom du compilateur C.
  • humain.c est le nom du fichier source
  • le reste, ce sont des options du compilateur, on le redonnait au tiret par lequel elles commencent.
  • -g demande au compilateur d’ajouter dans le code des balises qui vont permettre d’utiliser un debugger pour le cas échéant pouvoir analyser ce qui ne va pas dans le programme et où ça plante. Très pratique pour détecter la source des erreurs de segmentation autrement qu’en mettant des puts(« coucou ») partout dans le code.
  • -Wall demande au compilateur de signaler tout ce qui n’empêche pas la compilation, mais qu’il trouve bizarre. Et en général on essaiera d’éliminer ces avertissements, car lorsque le programme ne fait pas ce qu’on attend de lui, les problèmes viennent souvent de là.
  • enfin l’option -c demande au compilateur d’arrêter la compilation au fichier objet.

Notons qu’il n’est pas fait mention du fichier .h. Pourtant celui-ci est utilisé lors de la compilation par le biais du #include « humain.h » que vous n’aurez pas manqué de mettre dans le fichier humain.c

8.) Fin de la compilation:

Alors pour générer un exécutable qui marche à partir du projet décrit par l’image, il faut encore

  1. obtenir le fichier voiture.o via la commande gcc -g -Wall -c voiture.c,
  2. obtenir le fichier conduire.o via la commande gcc -g -Wall -c conduire.c,
  3. générer l’exécutable final via la commande suivante:

gcc -g -Wall -o Programme test.conduite.c humain.o voiture.o conduire.o

« Programme » est le nom de l’exécutable que vous allez générer. Vous remarquerez que l’option -o est utilisée. Elle indique au compilateur qu’il y a des fichiers .o à utiliser dans la compilation de l’exécutable final à partir du fichier test.conduite.c (qui sera le seul à inclure une méthode main). Concernant les options -g et -Wall, elles sont bien évidemment optionnelles :).

Voila pour aujourd’hui, j’espère avoir été assez clair (n’hésitez pas à poser des questions). Demain, si j’ai le temps, je concluerai sur la compilation séparée avec un article traitant de la rédaction d’un makefile.

Compilation séparée (2) – Dépendances.

compilation_separee-2

Cet article est la suite directe de celui d’hier, qui s’intitulait Généralités sur la compilation séparée en C. Je rappelle pour ceux qui tomberaient précisément sur cette page que cet article s’inscrit dans une suite de quatre articles traitant de la compilation séparé, et qu’il est suivit des articles  Compilation séparée (3) – Fichiers objets et compilation manuelle et   Compilation séparée (4) – Le Makefile. Aujourd’hui, nous allons étudier la mise en oeuvre de la compilation séparée, et je vous expliquer la raison d’être des jolies flèches sur mon schéma.

3) Un petit exemple pour mieux comprendre:

Comme vous le voyez, j’ai pris un cas facile à imaginer: on veut faire un programme dans lequel un humain va conduire une voiture. Pour cela, il faut:

  1. Le code C (par exemple une structure) qui va caractériser un humain, et toutes fonctions utilitaires qui vont avec,
  2. De même, le code C qui caractérise une voiture,
  3. Et enfin le code C qui va leur permettre d’interagir.

On notera tout l’intérêt de la compilation séparée dans cet exemple. En effet on pourrait par exemple imaginer:

  • qu’un collègue a déjà développé tout le code qui caractérise l’humain. Auquel cas il suffira de recompiler le fichier une seule fois puis de l’utiliser tel quel.
  • que plus tard on veuille créer un programme dans lequel un humain conduit un camion. Auquel cas il suffirait de créer le code C caractérisant le camion, et de modifier à peine le code d’interaction, mais sans avoir besoin de toucher à celui de l’humain
  • etc. Notons encore que l’humain et la voiture étant deux choses totalement différentes, il aurait de toute manière été illogique et peu clair de les mettre dans un unique fichier.

4) Rappel: utilité du .h

Comme vous le savez sans doute, il est de coutume d’utiliser conjointement un fichier .c et un fichier .h. Le fichier.h contient les prototypes des fonctions (c’est à dire leur déclaration avec juste un point virgule à la fin et non des accolades comme lorsque l’on les implémente), des commentaires qui les décrivent, et les déclarations des structures utilisés. L’utilité est double:

  • D’abord ça permet d’aérer le fichier .c contenant tout le code, et de l’organiser comme on le désire. On n’est plus astreint par exemple, de déclarer toutes les fonctions avant le main, car le #include « fichier.h » que l’on place au début aura pour effet de copier-coller à l’endroit où il est placé (c’est à dire généralement en tête de fichier), l’intégralité du fichier .h correspondant.
  • Ensuite ça permet dans le cadre de la compilation séparée d’inclure le fichier .h d’un programme simple dans le fichier .h d’un programme plus compliqué, afin de pouvoir utiliser ses structures et ses fonctions. Dans le cadre de notre exemple, voiture.h et humain.h sont par exemple inclus dans conduire.h, ce qui permet à conduire.c de manipuler des humains, des voitures, et leurs fonctions respectives dans le programme conduire.c

Le .h est également très utiles lorsque l’on veut savoir où l’on va. Ainsi il est fortement conseillé lorsque l’on est confronté à un problème, de ne pas commencer à faire le code bille en tête, et puis d’extraire à la fin les commentaires, les déclarations et structures dans un fichier .h juste pour faire plaisir au prof de TP, mais au contraire de commencer par réaliser le fichier .h avant de commencer à coder le fichier .c. En effet de même qu’avant de partir en voyage, on prépare le chemin à l’aide d’une carte, car avant de partir, il faut déjà savoir où l’on va, de même on gagnera énormément de temps au final en prévoyant à l’avance les fonctions qui vont être nécessaires ce qui permettra de plus d’avoir une vision globale du résultat attendu à tout moment du développement.

5) Explication du graphe

Alors pour le graphe, je n’ai pas cherché à reprendre celui que les profs utilisent, j’ai fait à mon idée en suivant la logique, avant de me rendre compte que ça ressemblait quand même énormément. En fait en général les profs utilisent les flèches pour schématiser les inclusions de code, tandis que moi je les utilise pour schématiser les dépendances. Ainsi les flèches qui partent de chaque objet pointent vers celui qui en est directement dépendant. Quelques exemples:

  1. humain.c a besoin que humain.h existe.
  2. voiture.c a besoin que voiture.h existe.
  3. conduire.h a besoin que humain.h et voiture.h existent.
  4. conduire.c a besoin que conduire.h (et ses dépendances), voiture.c et humain.c existent.
  5. Chaque fichier objet .o a besoin que le fichier.c qui lui correspond existe,
  6. et le programme exécutable a besoin de la totalité du code, puisqu’il dépend des trois fichiers.o

…Et ce sera tout pour aujoud’hui (c’est déjà pas mal). Demain nous étudierons la mise en oeuvre de tout ça, à quoi servent les fichiers .o et quelques exemples de compilation. Je ferais peut être un article séparé sur le makefile, je ne sais pas encore…

Généralités sur la compilation séparée en C.

compilation_separee-1

On m’a demandé de faire quelques articles pour parler des makefiles et de la compilation séparée. Donc celui ci sera le premier. Comme d’habitude, je vais tenter d’être simple et clair, même si on rentre ici dans un domaine un peu plus technique. Cet article s’adresse à tous, mais plus spécifiquement au public qui a déjà commencé à s’initier au C, et qui jusqu’à présent a toujours mis l’intégralité de son code dans un unique fichier .c à compiler en un coup. Nous allons étudier ce qu’est la compilation séparée, à quoi elle sert, comment la mettre en œuvre, et l’explication du joli dessin que vous voyez au dessus. Nous étudierons aussi la réalisation d’un makefile. Par contre je ne peux pas tout traiter en un seul article, donc pour la suite, voir les liens en fin de billet ;).

1) Rappel sur la compilation:

La compilation d’un programme consiste se fait au moyen d’un logiciel appelé compilateur. Elle est nécessaire parce que l’ordinateur ne comprend que le langage machine, soit en gros des lignes de zéros et de uns, tandis que l’être humain préfère codé dans un langage qu’il comprend. Le rôle du compilateur est donc de traduire le langage compréhensible par l’humain (le C) en langage compréhensible par la machine (les lignes de bits).

2) Explication de la compilation séparée

La compilation séparée permet comme son nom l’indique de compiler séparément plusieurs morceaux du programme, au lieu de tout compiler d’un seul bloc comme dans le cas de la compilation simple. Bien sûr, il faut pour cela que le programme ne soit pas écrit d’un bloc, mais qu’il ait été séparé en plusieurs fichiers. Les intérets sont multiples:

  • Gain en lisibilité. On regroupe dans des fichiers séparés les bouts de code qui correspondent à des thèmes/objectifs différents.
  • Gain d’efficacité. Dans les gros projets, il faut parfois 15 min pour tout compiler, donc s’il faut le faire chaque fois qu’on déplace une virgule, on perd vite son temps. Tandis que si on ne recompile que la partie dans laquelle se trouve la modification, le gain de temps est important.
  • Les bouts de code étant séparés par thèmes, leur réutilisation pour des objectifs nouveaux en ne prenant que certains est facilitée, et il n’est pas nécessaire dans un nouveau projet de revenir sur ce qui a déjà été fait.

… Et ce sera tout pour aujourd’hui, car j’ai encore énormément de travail à faire ce soir. La suite? Demain et dans le jours à venir 😉

Edit: liens vers les trois autres articles:

Les pointeurs en C

Après les deux articles précédents, la compréhension de ce qu’est un pointeur devrait être plus facile. En effet, qu’avons nous vu? Un ordinateur comprend un microprocesseur, de la mémoire et des cartes d’entrée/sorties. Lorsque l’on effectue une opération, il va chercher les informations dans la mémoire, il les traite et les restocke dans la mémoire. Ceci implique qu’il sache s’y retrouver dans la mémoire. En gros on peut dire que ce qui se passe revient à ceci : tous les octets de la mémoire sont numérotés. Chacun de ces numéros est appelé l’adresse de l’octet. Les adresses sont stockées sur 32 bits, soit sur 8 chiffres en hexadécimal. Chaque case mémoire a donc une adresse propre, et le microprocesseur a sous la main un fichier dans lequel sont indiquées les adresses de toutes les variables qu’il a besoin de manipuler. Par ailleurs le microprocesseur à sous le coude un autre fichier qui contient toutes les instructions qu’il doit exécuter. Encore une fois je précise que ce n’est pas tout à fait ça. Mais du point de vue du programmeur C qui découvre les pointeurs, ça suffira.

a) Les adresses :

Donc lorsque l’on déclare une nouvelle valeur, par exemple un tableau T de 15 entiers, le microprocesseur sait que la mémoire requise va être 15x « le nombre d’octets nécessaire pour le stockage d’un entier ». Soit 60 octets si on est sur un ordinateur où chaque int est stocké sur 4 octets. Le microprocesseur va donc reserver quelque part dans la mémoire (où il veut, c’est son problème du moment que la place est libre) 60 octets consécutifs. Puis il va noter quelques part que ces octets ne sont plus libres, et il va noter dans son fichier de variables l’adrese du premier octet du tableau. Vous avez peut être déjà essayé de faire printf(« %d »,T); T étant un tableau, et obtenu un curieux résultat du style 0x8ab45fe5, eh bien c’est précisément ça : vous avez affiché l’adresse du premier octet du tableau T. De même si vous tapez printf(« %d »,&a); dans un programme, a étant un entier quelconque, vous allez afficher l’adresse du premier des octets dans lesquels a est stocké. Et quand vous écrivez scanf(« %d »,&a); vous demandez au microprocesseur de stocker la valeur saisie au clavier à l’adresse de a, tout simplement. Attention tout de même, car si dans le cadre du tableau, vous tapez printf(« %d »,T+1); vous n’allez pas obtenir 0x8ab45fe6, adresse du deuxième octet sur lequel est stocké le premier entier de T, mais plutôt 0x8ab45fea9 correspondant au premier octet sur lequel est stocké l’entier suivant. En effet le compilateur sachant que T est un tableau d’entiers sait qu’il faut sauter 4 octets pour se placer sur l’entier suivant. C’est pour ça que &T[i] et (T+i) reviennent au même, tous deux désignant l’adresse du ième entier de T. A partir de maintenant nous allons donc pouvoir distinguer deux types de variables : celles qui contiennent des données et celles qui contiennent des adresses. Vous l’aurez deviné, les variables qui contiennent des adresses sont appelées des pointeurs. En effet depuis la case mémoire où elles sont stockées, elles pointent vers les cases mémoires dont elles contiennent l’adresse.

b) Les pointeurs :

Bon maintenant qu’ils ont si bien été introduits, il ne reste plus grand chose à dire sinon peut être qu’ils sont l’essence du C. Jusqu’à maintenant on ne vous en avait pas parlé car ce n’était pas nécéssaire, mais en fait ils sont omniprésents même si cela ne saute pas aux yeux. C’est notamment grâce aux pointeurs qu’une fonction peut modifier plusieurs variable : on lui passe en argument les adresses des cases mémoires qu’il va falloir lire et modifier, et elles peut ainsi modifier les variables même si elle ne retourne rien. Les pointeurs sont également très utiles dés lors que l’on veut utiliser des structures complexes, créer des listes, files, ensembles, graphes, etc, et optimiser la place mémoire requise. Un exemple typique est la liste linéaire chainée : chaque cellule contient l’adresse de la suivante. On se sert aussi de pointeurs pour tout ce qui est tableau : int *t; est typiquement la manière de déclarer un pointeur vers un ou des entiers. Et si on fait un tableau à deux dimensions, la déclaration devient int **t, qui se décompose en (int *) *t pour un pointeur vers des pointeurs vers des entiers. La structure typique est la suivante : dans la n-ièmecase du tableau de pointeurs est stocké le pointeur de la n-ième ligne du tableau.

Allocation Dynamique

Nous avons introduit le fonctionnement de base de l’ordinateur. Bien que l’allocation dynamique soit déjà un degré de complication au-dessus des premiers exercices, sa compréhension ne devrait pas poser de problèmes dés lors que le I est bien assimilé. Nous allons l’introduire au moyen de quelques exemples :

a) Allocation statique:

Supposons que l’on pose à un étudiant le sujet suivant : « Le programme doit demander à l’utilisateur de rentrer 5 nombres au clavier, les stocker dans un tableau, puis les afficher ». Un étudiant normal codera à peu près ceci :

int main(void)
{
int i;
int t[5];

./*PHASE DE SAISIE*/
puts(« entrez 5 entiers »);
for(i=0;i<5;i++)
scanf(« %d »,t+i);

.

/*PHASE D’AFFICHAGE*/
puts(« vous avez saisi : « );
for(i=0;i<5;i++)
printf(« %d, « ,t[i]);

printf(« \n »);
}

.
.
/*declaration du compteur de boucle*/
/*declaration d’un tableau statique de 5 entiers*/
.
/*equivalent de printf(« entrez 5 entiers\n »)*/
/*initialisation de i à 0 puis avant chaque boucle, vérification que*/ /*i<5, puis exécution des instructions de la boucle, enfin*/
/*incrémentation de i.*/
/*boucle : saisie d’un nombre au clavier et stockage à l’adresse*/ t+i*/

.

/*le %d indique que l’on va afficher un entier. t[i] est la valeur*/
/*de la i ème*/
/*case du tableau (on compte à partir de zéro)*/
/*pour passer à la ligne en fin d’éxécution*/

Bon les commentaires sont là pour faciliter la compréhension de tous. Mais compte tenu de leur lourdeur et du fait que ce n’est pas une partie de plaisir de mettre des tabulations et autres procédés de mise en forme en html, c’est un luxe que je ne vais pas longtemps me permettre. Voyons tout de suite le problème suivant : « Le programme doit demander à l’utilisateur de choisir un nombre n puis de rentrer n entiers au clavier, de les stocker dans un tableau, puis de les afficher ».

La première solution qui vient à l’esprit du programmeur qui n’aime pas trop réfléchir et qui préfère extrapoler à partir de l’exemple d’avant consiste à rajouter au début du code précédent « int n; printf(« Donne n> »); scanf(« %d »,&n);  » puis de remplacer les 5 par des n, puis de se demander pendant une demi heure pourquoi le int t[n]; ne passe pas à la compilation (Si vous avez des doutes, relire le précédent article ne sera pas du luxe).

Il convient alors de se rappeller que nulle mémoire n’est infinie et que quelque soit le mécanisme qui va limiter la taille du tableau, il ne peut pas ne pas en exister un qui n’impose une limite (à moins peut être de travailler sous windows ;=) ), et ça peut très bien être vous. En allocation statique il n’existe pas vraiment de manière élégante de s’en tirer. Le plus simple est alors de supposer que l’utilisateur ne choisira pas un nombre plus grand qu’un maximum que vous fixez. Vous pouvez par exemple déclarer un tableau de 1024 entiers {int t[1024];} en supposant qu’aucun utilisateur ne sera jamais assez courageux pour choisir plus de 1000 nombres. Par contre ne faites que n boucles 😉 Les puristes rajouteront un petit test pour afficher un message d’erreur dans le cas où le n choisi est supérieur à 1024. Voici un exemple de ce que vous pouvez faire :

int main(void)
{
int i,n;
int t[1024];

/*PHASE DE SAISIE*/
puts(« combien de nombres voulez vous rentrer? »);
scanf(« %d »,&n);

printf(« donnez %d nombres \n »,n);
for(i=0;i<n;i++)
scanf(« %d »,t+i);

/*PHASE D’AFFICHAGE*/
puts(« vous avez saisi : « );
for(i=0;i<n;i++)
printf(« %d, « ,t[i]);
printf(« \n »);
}

b) Allocation Dynamique:

Le problème de la solution que l’on vient d’exposer est qu’elle est un peu bancale notamment au niveau de l’optimisation de l’espace mémoire, mais ce n’est que le problème le plus évident et il y en a d’autres. La solution est simple : il faut allouer soi même la mémoire dont on va avoir besoin en spécifiant la manière dont on va l’utiliser pour que le compilateur ne soit pas perdu. Pour cela on utilise des instructions spécifiques dont malloc et calloc. Je vais expliquer pour le calloc car c’est celui qui s’applique le mieux à notre exemple : pour réserver un tableau de n entiers on va écrire :

nt *t=(int*)calloc(n,sizeof(int));

et en voici la traduction :

  • ‘int *t’ =>on déclare un tableau d’entiers.
  • ‘calloc(n, sizeof(int))’ => on réserve pour ce tableau la place nécessitée par le stockage de n entiers sachant que sizeof(int) retourne le nombre d’octets utilisés pour stocker un entier. Il en découle pour le compilateur que le nombre d’octets à réserver est donc n*sizeof(int).
  • ‘(int*)’ est un cast c’est à dire un opérateur de conversion qui va indiquer au compilateur que l’espace que l’on va alouer va correspondre à un tableau d’entiers.

Pour les curieux, la petite étoile a un rapport avec quelque chose d’assez mystérieux appelé les pointeurs, nous y viendrons bien assez tôt. Et pour les malins qui ont repéré que sizeof(int) retournait 4 sur leur ordinateur et qui se contentent donc d’écrire « int *t=(int *)calloc(n,4); », pensez que vous exécuterez peut être un jour votre programme sur un ordinateur qui utilise 8 octets pour stocker un int. Ce jour là vous aurez l’air malin (et ce d’autant plus que vous n’aurez pas de bug à la compilation, mais seulement à l’éxécution et encore pas systématiquement, un coup à s’arracher les cheveux). Bref voici donc comment traiter le problème précédent en utilisant l’allocation dynamique :

int main(void)
{
int i,n;
int *t;

/*PHASE DE SAISIE*/
puts(« combien de nombres voulez vous rentrer? »);
scanf(« %d »,&n);
t=(int *)calloc(n, sizeof(int));

printf(« donnez %d nombres \n »,n);
for(i=0;i<n;i++)
scanf(« %d »,t+i);

/*PHASE D’AFFICHAGE*/
puts(« vous avez saisi : « );
for(i=0;i<n;i++)
printf(« %d, « ,t[i]);
printf(« \n »);
}

Il y a bien sûr encore pas mal de choses à dire sur les allocations dynamiques, la différence entre le calloc et le malloc, et l’instruction realloc, mais ce n’est pas l’objet de cette page qui veut avant tout faire comprendre le principe et l’esprit de la programmation C plutôt qu’un exposé rigoureux de ses fonctionnalités. Si vous avez un doute sur une instruction, pensez à ouvrir un terminal et à taper ‘man’ suivit du nom de l’instruction pour avoir des informations sur son mode d’utilisation.

Quelques prérequis à la compréhension du C

Alala les erreurs de segmentation, quelle plaie, n’est-ce pas? Heureusement nous n’allons pas commencer par là, même si nous y viendrons bien vite, alors ne nous pressons pas trop. Voyons pour l’instant les quelques prérequis de base: Nous allons tout d’abord évoquer rapidement le fonctionnement de base d’un ordinateur, avant d’évoquer la manière dont le C fonctionne. Enfin nous expliquerons en détail le fonctionnement de certaines commandes épineuses.

a) Fonctionnement de l’ordinateur:

Comme chacun le sait, les ordinateurs ne sont pas magiques, bien qu’on puisse en avoir un peu l’impression tellement les effets que l’on obtient actuellement sont extraordinaires. Et je crois qu’il est bon de le garder à l’esprit car trop souvent on est porté à les considérer comme des artefacts au pouvoir mystérieux.

Pour faire simple, un ordinateur marche grâce à un microprocesseur, de la mémoire, et un ensemble de composants permettant la communication avec l’environnement extérieur. Il y a deux types de mémoires : les mémoires de stockage, tels les disques dur, clés USB, et CDs, et les mémoires de travail aussi appelées mémoires RAM dans lesquelles sont chargées les données dont le microprocesseur a besoin pour travailler. Le microprocesseur est une puce qui contient des millions de transistors. Il effectue les calculs et traite les requêtes. Pour cela il va chercher les instructions dans la mémoire de travail, et y stocke les résultats qu’il obtient. Diverses cartes également bourrées de transistors interprètent ces résultats pour l’utilisateur. Les mémoires de stockages sont des éléments externes au microprocesseur. Chaque fois qu’il a besoin d’y lire ou d’y stocker des données, il est donc ralenti, c’est pourquoi on s’efforce généralement de limiter ces besoins en loadant une bonne fois pour toute les données requises dans la mémoire de travail.

b) Introdution au C

Ce n’est pas transparent pour l’utilisateur lambda, mais lorsque l’on demande à un ordinateur d’additionner deux entiers a et b, le microprocesseur doit connaitre l’endroit où se trouvent les entiers a et b, sur combien d’octets ils sont stockés et où il doit écrire le résultat. C’est pourquoi en C on déclare les variables que l’on va utiliser en spécifiant quel est leur type. Ainsi lors de la déclaration d’une variable (par exemple : « int var; » ) le microprocesseur réserve quelque part dans la mémoire de travail les octets nécessaire pour le stockage d’une variable de type int, il retient l’adresse de ces octets, et prend note que chaque fois que l’utilisateur utilise ou modifie la variable var, c’est à cet endroit là qu’il devra aller voir.

Mais en général on ne parle pas de microprocesseur directement mais de compilateur. C’est tout simplement parce que le microprocesseur ne sait parler qu’en binaire ou a la rigueur en hexadécimal, tandis que l’utilisateur programme dans un langage spécifique. Il est donc nécessaire d’utiliser un programme appelé compilateur qui va traduire tout ce que l’utilisateur a demandé pour que le microprocesseur puisse le comprendre. Il arrive malheureusement que l’utilisateur ait demandé quelque chose qui n’est pas logique, ou qui n’est pas compréhensible, et le compilateur est alors très malheureux car il ne sait pas quoi dire au microprocesseur. En général il réagit en demandant des précisions à l’utilisateur (il marque « erreur de quelque chose dans le fichier truc.c en ligne n »), mais parfois il fait à son idée, ce qui peut générer des erreurs assez cocasses à l’exécution.

c) Quelle différence entre int t[5]; et int t[n]; ?

Cet exemple va vous permettre de voir si vous avez bien tout compris. Le compilateur qui voit « int t[5]; » est content. En effet il sait qu’il doit dire au microprocesseur de réserver la place nécessaire au stockage de 5 entiers. Par contre lorsque le compilateur voit « int t[n]; », il est tres malheureux. En effet il sait qu’il doit dire au microprocesseur de réserver la place nécessaire pour stocker des entiers, mais il ne sait pas combien. Donc il en fait part à l’utilisateur qui doit donc s’y prendre autrement lorsqu’il veut réserver un tableeau d’un nombre indéterminé d’entiers. C’est l’objet du prochain aticle.

Introduction au langage C

schéma de l'inclusion en C

Dans le cadre de mon dernier site, j’avais écrit quelques articles sur le langage C. Ces articles étaient très orientés débutants. Je vais donc dans le cadre du transfert de mon ancien site vers celui ci les poster dans mes prochaines publications. Ces articles sont avant tout destiné aux gens qui prendraient des cours de langage C, mais seraient un peu largués. Ainsi on s’attendra à ce que le lecteur connaisse certaines commandes de base du C. Par contre on s’attachera à réexpliquer le comment du pourquoi, et la raison pour laquelle le compilateur accepte certaines choses et pas d’autres. On fera également le lien entre ce qui se passe dans le programme et ce qui se passe au même moment au coeur de l’ordi de manière à mettre en évidence l lien entre les deux.

Voici le sommaire:

  1. Quelques prérequis à la compréhension du C.
  2. Allocation Dynamique.
  3. Les pointeurs.

Je n’ai bien sûr pas la prétention d’expliquer toute la philosophie du C en trois petits articles, mais je pense que leur contenu aurait pu être d’un grand secours à pas mal de mes condisciples moins habitués que moi à la programmation au moment où nous avons découvert les joies de la programmation en C :).