73 votes

Pourquoi ne pas concaténer les fichiers sources C avant la compilation ?

Je viens d'un milieu de script et le préprocesseur en C m'a toujours semblé laid. Néanmoins, je l'ai adopté pour apprendre à écrire de petits programmes en C. Je n'utilise vraiment le préprocesseur que pour inclure les bibliothèques standard et les fichiers d'en-tête que j'ai écrits pour mes propres fonctions.

Ma question est la suivante : pourquoi les programmeurs C ne sautent-ils pas tous les includes et ne concatènent-ils pas simplement leurs fichiers source C pour ensuite les compiler ? Si vous mettez toutes vos inclusions à un seul endroit, vous n'aurez à définir ce dont vous avez besoin qu'une seule fois, plutôt que dans tous vos fichiers source.

Voici un exemple de ce que je décris. Ici, j'ai trois fichiers :

// includes.c
#include <stdio.h>

// main.c
int main() {
    foo();
    printf("world\n");
    return 0;
}

// foo.c
void foo() {
    printf("Hello ");
}

En faisant quelque chose comme cat *.c > to_compile.c && gcc -o myprogram to_compile.c dans mon Makefile, je peux réduire la quantité de code que j'écris.

Cela signifie que je n'ai pas à écrire un fichier d'en-tête pour chaque fonction que je crée (car ils sont déjà dans le fichier source principal) et cela signifie également que je n'ai pas à inclure les bibliothèques standard dans chaque fichier que je crée. Cela me semble être une excellente idée !

Cependant, je me rends compte que le C est un langage de programmation très mature et j'imagine que quelqu'un d'autre, beaucoup plus intelligent que moi, a déjà eu cette idée et a décidé de ne pas l'utiliser. Pourquoi pas ?

103voto

Basile Starynkevitch Points 67055

Certains logiciels sont construits de cette façon.

Un exemple typique est SQLite . Il est parfois compilé comme un amalgamation (fait au moment de la construction à partir de nombreux fichiers sources).

Mais cette approche a des avantages et des inconvénients.

Évidemment, le temps de compilation augmentera considérablement. Ce n'est donc pratique que si vous compilez rarement ce genre de choses.

Peut-être que le compilateur pourrait optimiser un peu plus. Mais avec les optimisations au moment de la liaison (par exemple, si l'on utilise un fichier récent GCC, compiler et lier avec gcc -flto -O2 ), vous pouvez obtenir le même effet (bien sûr, au prix d'une augmentation du temps de construction).

Je n'ai pas besoin d'écrire un fichier d'en-tête pour chaque fonction.

C'est une mauvaise approche (avoir un fichier d'en-tête par fonction). Pour un projet d'une seule personne (de moins de cent mille lignes de code, alias KLOC = kilo line of code ), il est tout à fait raisonnable - au moins pour les petits projets - d'avoir une simple fichier d'en-tête commun (que vous pourriez précompiler si vous utilisez CCG ), qui contiendra les déclarations de toutes les fonctions et de tous les types publics, et éventuellement définitions de static inline fonctions (celles qui sont suffisamment petites et appelées assez fréquemment pour bénéficier de inlining ). Par exemple, le sash coquille est organisé de cette manière (tout comme le lout formateur avec 52 KLOC).

Vous pouvez également disposer de plusieurs fichiers d'en-tête, et peut-être d'un seul en-tête de "regroupement" qui #include -s tous (et que vous pourriez précompiler). Voir par exemple jansson (qui a en fait un seul public ) et GTK (qui a lots d'en-têtes internes, mais la plupart des applications l'utilisant ont juste un #include <gtk/gtk.h> qui comprennent à leur tour tous les en-têtes internes). De l'autre côté, POSIX a un grand nombre de fichiers d'en-tête, et il documente ceux qui doivent être inclus et dans quel ordre.

Certaines personnes préfèrent avoir beaucoup de fichiers d'en-tête (et d'autres préfèrent même mettre une seule déclaration de fonction dans son propre en-tête). Je ne le fais pas (pour les projets personnels, ou les petits projets sur lesquels seulement deux ou trois personnes commettent du code), mais il s'agit de goût . BTW, quand un projet se développe beaucoup, il arrive assez souvent que l'ensemble des fichiers d'en-tête (et des unités de traduction) change de manière significative. Regardez aussi dans REDIS (il a 139 .h fichiers d'en-tête et 214 .c c'est-à-dire des unités de traduction totalisant 126 KLOC).

Avoir un ou plusieurs unités de traduction est aussi une question de goût (et de commodité, d'habitudes et de conventions). Ma préférence va aux fichiers sources (c'est-à-dire aux unités de traduction) qui ne sont pas trop petits, typiquement plusieurs milliers de lignes chacun, et qui ont souvent (pour un petit projet de moins de 60 KLOC) un seul fichier d'en-tête commun. N'oubliez pas d'utiliser des construire l'automatisation outil comme GNU make (souvent avec un parallèle construire par make -j ; alors vous aurez plusieurs processus de compilation fonctionnant simultanément). L'avantage d'avoir une telle organisation du fichier source est que la compilation est raisonnablement rapide. D'ailleurs, dans certains cas, un métaprogrammation L'approche est intéressante : certains de vos fichiers "source" C (en-tête interne, ou unités de traduction) pourraient être généré par quelque chose d'autre (par exemple, un certain script en AWK ou un programme C spécialisé comme bison ou votre propre truc).

N'oubliez pas que le langage C a été conçu dans les années 1970, pour des ordinateurs beaucoup plus petits et plus lents que votre ordinateur portable préféré d'aujourd'hui (à l'époque, la mémoire était généralement d'un mégaoctet au maximum, voire de quelques centaines de kilooctets, et l'ordinateur était au moins mille fois plus lent que votre téléphone portable actuel).

Je vous conseille vivement de étudier le code source et construire quelques existant logiciel gratuit projets (par exemple, celles sur GitHub ou SourceForge ou votre distribution Linux préférée). Vous apprendrez qu'il s'agit d'approches différentes. N'oubliez pas que en C conventions et habitudes comptent beaucoup dans la pratique donc il y a différents façons d'organiser votre projet en .c et .h fichiers . Lisez à propos de la Préprocesseur C .

Cela signifie également que je ne dois pas inclure les bibliothèques standard dans chaque fichier que je crée.

Vous incluez les fichiers d'en-tête, pas les bibliothèques (mais vous devriez lien bibliothèques). Mais vous pourriez les inclure dans chaque .c (et de nombreux projets le font), ou vous pouvez les inclure dans un seul en-tête et précompiler cet en-tête, ou vous pouvez avoir une douzaine d'en-têtes et les inclure après les en-têtes système dans chaque unité de compilation. YMMV. Notez que le temps de prétraitement est rapide sur les ordinateurs d'aujourd'hui (du moins, lorsque vous demandez au compilateur d'optimiser, puisque l'optimisation prend plus de temps que l'analyse syntaxique et le prétraitement).

Remarquez que ce qui va dans certains #include Le fichier -d est conventionnel (et n'est pas défini par la spécification C). Certains programmes ont une partie de leur code dans un tel fichier (qui ne devrait alors pas être appelé "en-tête", mais simplement "fichier inclus", et qui ne devrait pas avoir de nom de fichier. .h mais quelque chose d'autre comme .inc ). Regardez par exemple dans XPM dossiers. À l'autre extrême, vous pouvez en principe n'avoir aucun de vos propres fichiers d'en-tête (vous avez toujours besoin des fichiers d'en-tête de l'implémentation, par exemple <stdio.h> ou <dlfcn.h> de votre système POSIX) et copier et coller le code dupliqué dans votre .c par exemple, avoir la ligne int foo(void); dans chaque .c mais c'est une très mauvaise pratique qui est mal vue. Cependant, certains programmes générant Des fichiers C partageant un contenu commun.

BTW, C ou C++14 n'ont pas de modules (comme OCaml). En d'autres termes, en C, un module est principalement une convention .

(remarquez que le fait d'avoir plusieurs milliers de très petit .h et .c de seulement quelques dizaines de lignes chacun peut ralentir votre temps de construction de façon spectaculaire ; avoir des centaines de fichiers de quelques centaines de lignes chacun est plus raisonnable, en terme de temps de construction).

Si vous commencez à travailler sur un projet solo en C, je vous suggère d'avoir d'abord un fichier d'en-tête (et de le précompiler) et plusieurs .c unités de traduction. En pratique, vous changerez .c beaucoup plus souvent que les fichiers .h d'autres. Une fois que vous avez plus de 10 KLOC, vous pouvez les refactorer en plusieurs fichiers d'en-tête. Une telle refactorisation est délicate à concevoir, mais facile à faire (juste beaucoup de copier-coller de morceaux de codes). D'autres personnes auront des suggestions et des conseils différents (et c'est bien ainsi !). Mais n'oubliez pas d'activer tous les avertissements et les informations de débogage lors de la compilation (donc compilez avec le bouton gcc -Wall -g peut-être en fixant CFLAGS= -Wall -g dans votre Makefile ). Utilisez le gdb débogueur (et Valgrind ...). Demandez des optimisations ( -O2 ) lorsque vous évaluez un programme déjà débogué. Utilisez également un système de contrôle de version comme Git .

Au contraire, si vous êtes en train de concevoir un projet plus vaste sur lequel plusieurs personnes fonctionnerait, il pourrait être préférable d'avoir plusieurs fichiers - voire plusieurs fichiers d'en-tête - (intuitivement, chaque fichier a une seule personne qui en est principalement responsable, d'autres personnes apportant des contributions mineures à ce fichier).

Dans un commentaire, vous ajoutez :

Je parle d'écrire mon code dans plusieurs fichiers différents et d'utiliser un Makefile pour les concaténer.

Je ne vois pas pourquoi cela serait utile (sauf dans des cas très bizarres). Il est bien mieux (et très habituel et courant) de compiler chaque unité de traduction (par exemple chaque .c ) dans son fichier objet (a .o ELF sous Linux) et lien plus tard. C'est facile avec make (en pratique, lorsque vous ne modifiez qu'un seul .c par exemple pour corriger un bogue, seul ce fichier est compilé et la compilation incrémentale est très rapide), et vous pouvez lui demander de compiler les fichiers objets dans le répertoire parallèle en utilisant make -j (et alors votre construction va très vite sur votre processeur multi-core).

26voto

Bathsheba Points 23209

Vous pourrait faire cela, mais nous aimons séparer les programmes C en programmes séparés unités de traduction principalement parce que :

  1. Cela accélère les constructions. Vous n'avez besoin de reconstruire que les fichiers qui ont été modifiés, et ceux-ci peuvent être lié à avec d'autres fichiers compilés pour former le programme final.

  2. La bibliothèque standard C est constituée de composants précompilés. Voulez-vous vraiment avoir à recompiler tout cela ?

  3. Il est plus facile de collaborer avec d'autres programmeurs si la base de code est divisée en plusieurs fichiers.

16voto

Mohit Jain Points 6202
  • Grâce à la modularité, vous pouvez partager votre bibliothèque sans partager le code.
  • Pour les grands projets, si vous modifiez un seul fichier, vous finirez par compiler le projet complet.
  • Vous risquez de manquer de mémoire plus facilement lorsque vous tentez de compiler de grands projets.
  • Vous pouvez avoir des dépendances circulaires dans les modules, la modularité aide à les maintenir.

Votre approche peut présenter certains avantages, mais pour des langages comme le C, la compilation de chaque module est plus logique.

16voto

cmaster Points 7460

Votre approche consistant à concaténer les fichiers .c est complètement cassée :

  • Même si la commande cat *.c > to_compile.c mettra toutes les fonctions dans un seul fichier, L'ordre est important : Chaque fonction doit être déclarée avant sa première utilisation.

    C'est-à-dire que vous avez des dépendances entre vos fichiers .c qui imposent un certain ordre. Si votre commande de concaténation ne respecte pas cet ordre, vous ne pourrez pas compiler le résultat.

    De même, si vous avez deux fonctions qui s'utilisent récursivement l'une l'autre, il n'y a absolument aucun moyen d'éviter d'écrire une déclaration forward pour au moins l'une des deux. Vous pouvez tout aussi bien mettre ces déclarations forward dans un fichier d'en-tête où les gens s'attendent à les trouver.

  • Quand vous concaténerez tout dans un seul fichier, vous forcez une reconstruction complète chaque fois qu'une seule ligne de votre projet change.

    Avec l'approche classique de compilation séparée .c/.h, un changement dans l'implémentation d'une fonction nécessite la recompilation d'un seul fichier, tandis qu'un changement dans un en-tête nécessite la recompilation des fichiers qui incluent effectivement cet en-tête. Cela peut facilement accélérer la reconstruction après un petit changement par un facteur de 100 ou plus (en fonction du nombre de fichiers .c).

  • Vous perdez toute possibilité de compilation parallèle quand vous concaténerez tout dans un seul fichier.

    Vous avez un gros processeur à 12 cœurs avec l'hyper-threading activé ? Dommage, votre fichier source concaténé est compilé par un seul thread. Vous venez de perdre un gain de vitesse d'un facteur supérieur à 20... Ok, c'est un exemple extrême, mais j'ai construit des logiciels avec make -j16 déjà, et je vous le dis, ça peut faire une énorme différence.

  • Les temps de compilation sont généralement pas linéaire.

    En général, les compilateurs contiennent au moins quelques algorithmes qui ont un comportement quadratique à l'exécution. Par conséquent, il existe généralement un certain seuil à partir duquel la compilation agrégée est réellement plus lente que la compilation des parties indépendantes.

    Évidemment, l'emplacement précis de ce seuil dépend du compilateur et des drapeaux d'optimisation que vous lui passez, mais j'ai vu un compilateur prendre plus d'une demi-heure sur un seul énorme fichier source. Vous ne voulez pas avoir un tel obstacle dans votre boucle changement-compilation-test.

Ne vous y trompez pas : Même s'il y a tous ces problèmes, il y a des gens qui utilisent la concaténation de fichiers .c dans la pratique, et certains programmeurs C++ arrivent à peu près au même point en déplaçant tout dans des modèles (de sorte que l'implémentation se trouve dans le fichier .hpp et qu'il n'y a pas de fichier .cpp associé), laissant le préprocesseur faire la concaténation. Je ne vois pas comment ils peuvent ignorer ces problèmes, mais ils le font.

Notez également que nombre de ces problèmes ne deviennent apparents qu'avec des projets de plus grande taille. Si votre projet comporte moins de 5000 lignes de code, la façon dont vous le compilez n'a pas d'importance. Mais lorsque vous avez plus de 50000 lignes de code, vous voulez absolument un système de compilation qui supporte les compilations incrémentales et parallèles. Sinon, vous perdez votre temps de travail.

15voto

Lundin Points 21616

Parce que diviser les choses est une bonne conception de programme. Une bonne conception de programme est une question de modularité, de modules de code autonomes et de réutilisation du code. Il s'avère que le bon sens vous mènera très loin dans la conception de programmes : Les choses qui ne vont pas ensemble ne doivent pas être placées ensemble.

Le fait de placer du code non apparenté dans différentes unités de traduction permet de localiser autant que possible la portée des variables et des fonctions.

La fusion des choses crée couplage étroit Cela signifie des dépendances maladroites entre des fichiers de code qui ne devraient même pas avoir à connaître l'existence des autres. C'est pourquoi un "global.h" qui contient toutes les inclusions d'un projet est une mauvaise chose, car il crée un couplage étroit entre tous les fichiers sans rapport avec le projet.

Supposons que vous écriviez un micrologiciel pour contrôler une voiture. Un module du programme contrôle la radio FM de la voiture. Ensuite, vous réutilisez le code radio dans un autre projet, pour contrôler la radio FM d'un téléphone intelligent. Et là, votre code radio ne compile pas parce qu'il ne trouve pas de freins, de roues, de vitesses, etc. Des choses qui n'ont pas le moindre sens pour la radio FM, et encore moins pour le téléphone intelligent.

Le pire, c'est qu'en cas de couplage étroit, les bogues se propagent à l'ensemble du programme, au lieu de rester localisés dans le module où se trouve le bogue. Cela rend les conséquences du bug beaucoup plus graves. Vous écrivez un bug dans le code de votre radio FM et soudain les freins de la voiture cessent de fonctionner. Même si vous n'avez pas touché le code des freins avec votre mise à jour qui contenait le bogue.

Si un bogue dans un module brise des éléments complètement indépendants, c'est presque certainement dû à une mauvaise conception du programme. Et un moyen certain de parvenir à une mauvaise conception du programme est de fusionner tout ce qui se trouve dans votre projet en un seul gros bloc.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X