web-dev-qa-db-fra.com

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

Je viens d'un environnement de script et le préprocesseur en C m'a toujours semblé moche. Néanmoins, je l'ai adopté en apprenant à écrire de petits programmes en C. J'utilise uniquement le préprocesseur 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 inclus, concaténent simplement leurs fichiers source C et les compilent ensuite? Si vous placez toutes vos inclus dans un seul emplacement, il vous suffira de définir ce dont vous avez besoin une 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 besoin d'é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 que je n'ai pas à inclure les bibliothèques standard dans chaque fichier que je crée. Cela me semble une bonne idée!

Cependant, je réalise que 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?

74
user3420382

Vous pouvez le faire, mais nous aimons séparer les programmes C en nités de traduction, principalement parce que:

  1. Cela accélère les constructions. Il vous suffit de reconstruire les fichiers qui ont été modifiés et ceux-ci peuvent être liés 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. Voudriez-vous vraiment avoir à recompiler tout ça?

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

26
Bathsheba

Votre approche de la concaténation des fichiers .c est complètement brisée:

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

    Autrement dit, vous avez des dépendances entre vos fichiers .c qui forcent un certain ordre. Si votre commande de concaténation ne parvient pas à respecter cet ordre, vous ne pourrez pas compiler le résultat.

    En outre, si vous avez deux fonctions qui s’utilisent de manière récursive, il n’est absolument pas possible d’écrire une déclaration directe pour au moins l’une des deux. Vous pouvez également placer ces déclarations dans un fichier d’en-tête où les utilisateurs s’attendent à les trouver.

  • Lorsque vous concaténez le tout dans un seul fichier, vous forcez une reconstruction complète chaque fois qu'une seule ligne de votre projet est modifiée.

    Avec l'approche classique de la compilation divisé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 une recompilation des fichiers qui incluent réellement 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 lorsque vous concaténez le tout dans un seul fichier.

    Avez-vous un gros processeur 12 core avec hyper-threading activé? Dommage, votre fichier source concaténé est compilé par un seul thread. Vous venez de perdre un facteur d'accélération supérieur à 20 ... Ok, c'est un exemple extrême, mais j'ai construit un logiciel avec make -j16 déjà, et je vous le dis, cela peut faire une énorme différence.

  • Les temps de compilation sont généralement et non linéaires.

    Les compilateurs contiennent généralement au moins certains algorithmes ayant un comportement d'exécution quadratique. Par conséquent, il existe généralement un seuil à partir duquel la compilation agrégée est en réalité plus lente que la compilation des parties indépendantes.

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

Ne vous y méprenez pas: même si cela pose tous ces problèmes, certaines personnes utilisent en pratique la concaténation de fichiers .c, et certains programmeurs C++ obtiennent à peu près la même chose en déplaçant le tout dans des modèles (de sorte que la mise en œuvre se trouve dans le fichier .hpp (aucun fichier .cpp associé), laissant le préprocesseur effectuer la concaténation. Je ne vois pas comment ils peuvent ignorer ces problèmes, mais ils le font.

Notez également que bon nombre de ces problèmes ne deviennent apparents qu'avec des projets de plus grande taille. Si votre projet comporte moins de 5 000 lignes de code, la manière dont vous le compilez reste relativement peu pertinente. Mais lorsque vous avez plus de 50000 lignes de code, vous souhaitez certainement un système de construction prenant en charge les générations incrémentielles et parallèles. Sinon, vous perdez votre temps de travail.

16
cmaster
  • Avec la modularité, vous pouvez partager votre bibliothèque sans partager le code.
  • Pour les grands projets, si vous modifiez un seul fichier, vous compilerez le projet complet.
  • Vous risquez de manquer de mémoire plus facilement lorsque vous essayez de compiler des projets volumineux.
  • Vous pouvez avoir des dépendances circulaires dans les modules, la modularité aide à les maintenir.

Votre approche présente peut-être des avantages, mais pour des langages tels que C, la compilation de chaque module est plus logique.

16
Mohit Jain

Parce que séparer les choses est une bonne conception de programme. Une bonne conception de programme repose sur la modularité, les modules de code autonomes et la réutilisation de code. En fin de compte, le bon sens vous mènera très loin lors de la conception de programmes: les éléments qui ne font pas partie les uns des autres ne doivent pas être placés ensemble.

Le fait de placer du code non lié dans différentes unités de traduction signifie que vous pouvez localiser autant que possible l'étendue des variables et des fonctions.

La fusion de plusieurs éléments crée couplage étroit, ce qui signifie des dépendances délicates entre des fichiers de code qui ne devraient même pas avoir à connaître l’existence de chacun. C'est pourquoi un "global.h" qui contient tous les inclus dans un projet est une mauvaise chose, car il crée un couplage étroit entre tous les fichiers non liés de votre projet.

Supposons que vous écrivez un firmware 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 dans un smartphone. Et puis votre code radio ne sera pas compilé car il ne trouve pas les freins, les roues, les engrenages, etc. Ce qui n’a aucun sens pour la radio FM, sans parler du téléphone intelligent.

Ce qui est encore pire, c’est que si vous avez un couplage étroit, les bogues s’aggravent tout au long 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 bogue dans votre code radio FM et soudain, les freins de la voiture cessent de fonctionner. Même si vous n'avez pas touché le code de frein avec votre mise à jour qui contenait le bogue.

Si un bogue dans un module casse des choses complètement non liées, c'est probablement à cause d'une mauvaise conception du programme. Et une bonne façon d’obtenir une conception de programme médiocre consiste à fusionner tous les éléments de votre projet en une grosse tâche.

15
Lundin

Les fichiers d'en-tête doivent définir les interfaces - c'est une convention souhaitable à suivre. Ils ne sont pas censés déclarer tout ce qui se trouve dans un fichier .c Correspondant ou dans un groupe de fichiers .c. Au lieu de cela, ils déclarent toutes les fonctionnalités des fichiers .c Disponibles pour leurs utilisateurs. Un fichier .h Bien conçu comprend un document de base de l'interface exposée par le code du fichier .c Même s'il ne contient pas un seul commentaire. Une façon d'aborder la conception d'un module C consiste à écrire d'abord le fichier d'en-tête, puis à l'implémenter dans un ou plusieurs fichiers .c.

Corollaire: les fonctions et les structures de données internes à la mise en oeuvre d'un fichier .c N'appartiennent normalement pas au fichier d'en-tête. Vous pourriez avoir besoin de déclarations forward, mais celles-ci doivent être locales et toutes les variables et fonctions ainsi déclarées et définies doivent être static: si elles ne font pas partie de l'interface, l'éditeur de liens ne devrait pas les voir.

11
Kuba Ober

La raison principale est le temps de compilation. Compiler un petit fichier lorsque vous le modifiez peut prendre peu de temps. Cependant, si vous compiliez tout le projet à chaque fois que vous modifiez une seule ligne, vous compileriez, par exemple, 10 000 fichiers à chaque fois, ce qui pourrait prendre beaucoup plus de temps.

Si vous avez, comme dans l'exemple ci-dessus, 10 000 fichiers source et que la compilation prend 10 ms, l'ensemble du projet est construit de manière incrémentielle (après modification d'un fichier unique) en (10 ms + temps de liaison) si vous ne compilez que ce fichier modifié, ou (10 ms * 10000 + temps de liaison court) si vous compilez le tout en un seul blob concaténé.

8
Freddie Chopin

Bien que vous puissiez toujours écrire votre programme de manière modulaire et le construire en une seule unité de traduction, vous allez rater tout les mécanismes fournis par C pour appliquer cette modularité. Avec plusieurs unités de traduction, vous avez un contrôle précis sur les interfaces de vos modules en utilisant par exemple extern et static mots-clés.

En fusionnant votre code dans une seule unité de traduction, vous éviterez tout problème de modularité, car le compilateur ne vous en avertira pas. Dans un grand projet, cela finira par créer des dépendances inattendues. En fin de compte, vous aurez du mal à changer un module sans créer d’effets secondaires globaux dans d’autres modules.

7
Dmitry Grigoryev

Si vous placez toutes vos inclus dans un seul emplacement, il vous suffira de définir ce dont vous avez besoin une fois, plutôt que dans tous vos fichiers source.

C'est le but de .h fichiers pour que vous puissiez définir ce dont vous avez besoin une fois et l’inclure partout. Certains projets ont même un everything.h en-tête qui inclut chaque personne .h fichier. Ainsi, votre pro peut être obtenu avec .c fichiers également.

Cela signifie que je n'ai pas à écrire un fichier d'en-tête pour chaque fonction que je crée [...]

De toute façon, vous n'êtes pas censé écrire un fichier d'en-tête pour chaque fonction. Vous êtes censé avoir un fichier d'en-tête pour un ensemble de fonctions connexes. Donc, votre con n'est pas valide non plus.

4
DepressedDaniel

Cela signifie que je n'ai pas besoin d'é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 que je n'ai pas à inclure les bibliothèques standard dans chaque fichier que je crée. Cela me semble une bonne idée!

Les avantages que vous avez remarqués sont en fait une des raisons pour lesquelles cela se fait parfois à plus petite échelle.

Pour les gros programmes, ce n'est pas pratique. Comme pour les autres bonnes réponses mentionnées, cela peut augmenter considérablement les temps de construction.

Cependant, il peut être utilisé pour diviser une unité de traduction en bits plus petits, qui partagent l'accès aux fonctions d'une manière qui rappelle l'accessibilité aux packages de Java.

La réalisation de ce qui précède implique une certaine discipline et l'aide du pré-processeur.

Par exemple, vous pouvez diviser votre unité de traduction en deux fichiers:

// a.c

static void utility() {
}

static void a_func() {
  utility();
}

// b.c

static void b_func() {
  utility();
}

Maintenant, vous ajoutez un fichier pour votre unité de traduction:

// ab.c

static void utility();

#include "a.c"
#include "b.c"

Et votre système de compilation ne construit ni a.c Ni b.c, Mais ne construit que ab.o À partir de ab.c.

Qu'est-ce que ab.c Accomplit?

Il inclut les deux fichiers pour générer une seule unité de traduction et fournit un prototype pour l'utilitaire. Ainsi, le code dans a.c Et b.c Pourrait le voir, quel que soit l'ordre dans lequel ils sont inclus, et sans exiger que la fonction soit extern.

2
StoryTeller