vez sumar concatenar concatena c compilation c-preprocessor

sumar - ¿Por qué no concatenar archivos fuente C antes de la compilación?



sumar en javascript sin concatenar (10)

Vengo de un entorno de secuencias de comandos y el preprocesador en C siempre me ha parecido feo. Sin embargo, lo he aceptado mientras aprendo a escribir pequeños programas en C. Realmente solo estoy usando el preprocesador para incluir las bibliotecas estándar y los archivos de encabezado que he escrito para mis propias funciones.

Mi pregunta es ¿por qué los programadores de C simplemente no omiten todas las inclusiones y simplemente concatenan sus archivos fuente de C y luego los compilan? Si coloca todas sus inclusiones en un solo lugar, solo tendría que definir lo que necesita una vez, en lugar de en todos sus archivos de origen.

Aquí hay un ejemplo de lo que estoy describiendo. Aquí tengo tres archivos:

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

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

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

Al hacer algo como cat *.c > to_compile.c && gcc -o myprogram to_compile.c en mi Makefile, puedo reducir la cantidad de código que escribo.

Esto significa que no tengo que escribir un archivo de encabezado para cada función que creo (porque ya están en el archivo fuente principal) y también significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo. ¡Esto me parece una gran idea!

Sin embargo, me doy cuenta de que C es un lenguaje de programación muy maduro y me imagino que alguien más inteligente que yo ya ha tenido esta idea y decidió no usarla. Por qué no?


Esto significa que no tengo que escribir un archivo de encabezado para cada función que creo (porque ya están en el archivo fuente principal) y también significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo. ¡Esto me parece una gran idea!

Los profesionales que notó son en realidad una razón por la cual esto a veces se hace en una escala más pequeña.

Para programas grandes, no es práctico. Al igual que otras buenas respuestas mencionadas, esto puede aumentar sustancialmente los tiempos de construcción.

Sin embargo, se puede utilizar para dividir una unidad de traducción en bits más pequeños, que comparten el acceso a las funciones de una manera que recuerda la accesibilidad del paquete de Java.

La forma en que se logra lo anterior implica cierta disciplina y ayuda del preprocesador.

Por ejemplo, puede dividir su unidad de traducción en dos archivos:

// a.c static void utility() { } static void a_func() { utility(); } // b.c static void b_func() { utility(); }

Ahora agrega un archivo para su unidad de traducción:

// ab.c static void utility(); #include "a.c" #include "b.c"

Y su sistema de compilación no construye ni ac ni bc , sino que solo construye ab.o partir de ab.c

¿Qué logra ab.c ?

Incluye ambos archivos para generar una sola unidad de traducción y proporciona un prototipo para la utilidad. Para que el código en ac y bc pueda verlo, independientemente del orden en que se incluyan, y sin requerir que la función sea extern .


Si coloca todas sus inclusiones en un solo lugar, solo tendría que definir lo que necesita una vez, en lugar de en todos sus archivos de origen.

Ese es el propósito de los archivos .h , para que pueda definir lo que necesita una vez e incluirlo en todas partes. Algunos proyectos incluso tienen un encabezado everything.h que incluye cada archivo .h individual. Por lo tanto, su profesional también se puede lograr con archivos .c separados.

Esto significa que no tengo que escribir un archivo de encabezado para cada función que creo [...]

No se supone que deba escribir un archivo de encabezado para cada función de todos modos. Se supone que debe tener un archivo de encabezado para un conjunto de funciones relacionadas. Entonces su estafa tampoco es válida.


Algunos software se construyen de esa manera.

Un ejemplo típico es SQLite . A veces se compila como una amalgamation (realizada en tiempo de compilación a partir de muchos archivos fuente).

Pero ese enfoque tiene pros y contras.

Obviamente, el tiempo de compilación aumentará bastante. Por lo tanto, es práctico solo si compila esas cosas raramente.

Quizás, el compilador podría optimizar un poco más. Pero con las optimizaciones de tiempo de enlace (por ejemplo, si usa un CCG reciente , compile y enlace con gcc -flto -O2 ) puede obtener el mismo efecto (por supuesto, a expensas de un mayor tiempo de construcción).

No tengo que escribir un archivo de encabezado para cada función

Ese es un enfoque incorrecto (de tener un archivo de encabezado por función). Para un proyecto de una sola persona (de menos de cien mil líneas de código, también conocido como KLOC = kilo línea de code ), es bastante razonable, al menos para proyectos pequeños, tener un único archivo de encabezado común (que podría pre-compile si usa GCC ), que contendrá declaraciones de todas las funciones y tipos públicos, y quizás definiciones de funciones en static inline (aquellas lo suficientemente pequeñas y llamadas con suficiente frecuencia como para beneficiarse de la SQLite ). Por ejemplo, el shell de sash se organiza de esa manera (y también lo es el formateador lout , con 52 KLOC).

También puede tener algunos archivos de encabezado, y quizás tener un solo encabezado de "agrupación" que #include -s todos (y que podría precompilar). Vea, por ejemplo, jansson (que en realidad tiene un solo archivo de encabezado público ) y GTK (que tiene muchos encabezados internos, pero la mayoría de las aplicaciones que lo usan have solo un #include <gtk/gtk.h> que a su vez incluye todos los encabezados internos) . En el lado opuesto, POSIX tiene una gran cantidad de archivos de encabezado, y documenta cuáles deben incluirse y en qué orden.

Algunas personas prefieren tener muchos archivos de encabezado (y algunos incluso prefieren poner una declaración de función única en su propio encabezado). No lo hago (para proyectos personales o pequeños proyectos en los que solo dos o tres personas cometerían código), pero es cuestión de gustos . Por cierto, cuando un proyecto crece mucho, sucede con bastante frecuencia que el conjunto de archivos de encabezado (y de unidades de traducción) cambia significativamente. Busque también en REDIS (tiene 139 archivos de encabezado .h y 214 archivos .c , es decir, unidades de traducción que totalizan 126 KLOC).

Tener una o varias unidades de traducción también es una cuestión de gustos (y de conveniencia, hábitos y convenciones). Mi preferencia es tener archivos de origen (es decir, unidades de traducción) que no sean demasiado pequeños, generalmente de varios miles de líneas cada uno, y que a menudo tengan (para un proyecto pequeño de menos de 60 KLOC) un archivo de encabezado único común. No olvide utilizar alguna herramienta de automatización de compilación como GNU make (a menudo con una compilación parallel través de make -j ; luego tendrá varios procesos de compilación ejecutándose simultáneamente). La ventaja de tener una organización de archivos de este tipo es que la compilación es razonablemente rápida. Por cierto, en algunos casos vale la pena un enfoque de metaprogramming : algunos de sus archivos de "fuente" C (encabezado interno o unidades de traducción) podrían ser generados por otra cosa (por ejemplo, algún script en AWK , algún programa especializado en C como bison o tu propia cosa )

Recuerde que C fue diseñado en la década de 1970, para computadoras mucho más pequeñas y lentas que su computadora portátil favorita actual (por lo general, la memoria era en ese momento un megabyte como máximo, o incluso unos pocos cientos de kilobytes, y la computadora era al menos mil veces más lenta que tu teléfono móvil hoy).

Le sugiero que estudie el código fuente y cree algunos proyectos de software libre existentes (por ejemplo, aquellos en GitHub o SourceForge o su distribución de Linux favorita). Aprenderás que son enfoques diferentes. Recuerde que en C las convenciones y los hábitos son muy importantes en la práctica , por lo que hay diferentes maneras de organizar su proyecto en archivos .c y .h . Lea sobre el preprocesador C.

También significa que no tengo que incluir las bibliotecas estándar en cada archivo que creo

Incluye archivos de encabezado, no bibliotecas (pero debe link bibliotecas). Pero podría incluirlos en cada archivo .c (y muchos proyectos lo están haciendo), o podría incluirlos en un solo encabezado y precompilar ese encabezado, o podría tener una docena de encabezados e incluirlos después de los encabezados del sistema en cada unidad de compilación YMMV. Tenga en cuenta que el tiempo de preprocesamiento es rápido en las computadoras actuales (al menos, cuando le pide al compilador que optimice, ya que las optimizaciones requieren más tiempo que el análisis y el preprocesamiento).

Observe que lo que se #include en algún archivo #include -d es convencional (y no está definido por la especificación C). Algunos programas tienen parte de su código en algún archivo de este tipo (que no debería llamarse "encabezado", solo un "archivo incluido"; y que no debería tener un sufijo .h , sino algo como .inc ). Busque por ejemplo en archivos XPM . En el otro extremo, es posible que, en principio, no tenga ninguno de sus propios archivos de encabezado (todavía necesita archivos de encabezado de la implementación, como <stdio.h> o <dlfcn.h> de su sistema POSIX) y copie y pegue el código duplicado en sus archivos .c -eg tiene la línea int foo(void); en cada archivo .c , pero esa es una muy mala práctica y está mal vista. Sin embargo, algunos programas están generando archivos C que comparten contenido común.

Por cierto, C o C ++ 14 no tienen módulos (como OCaml tiene). En otras palabras, en C un módulo es principalmente una convención .

(tenga en cuenta que tener muchos miles de archivos .h y .c muy pequeños de solo unas pocas docenas de líneas cada uno puede ralentizar su tiempo de construcción dramáticamente; tener cientos de archivos de unos cientos de líneas cada uno es más razonable, en términos de tiempo de construcción. )

Si comienza a trabajar en un proyecto de una sola persona en C, sugeriría primero tener un archivo de encabezado (y precompilarlo) y varias unidades de traducción .c . En la práctica, cambiará los archivos .c mucha más frecuencia que los archivos .h . Una vez que tenga más de 10 KLOC, puede refactorizarlo en varios archivos de encabezado. Tal refactorización es difícil de diseñar, pero fácil de hacer (solo una gran cantidad de copia y pegado de códigos). Otras personas tendrían sugerencias y sugerencias diferentes (¡y eso está bien!). Pero no olvide habilitar todas las advertencias e información de depuración al compilar (compile con gcc -Wall -g , quizás estableciendo CFLAGS= -Wall -g en su Makefile ). Use el depurador gdb (y valgrind ...). Solicite optimizaciones ( -O2 ) cuando -O2 un programa ya depurado. También use un sistema de control de versiones como Git .

Por el contrario, si está diseñando un proyecto más grande en el que trabajarían varias personas , podría ser mejor tener varios archivos, incluso varios archivos de encabezado, (intuitivamente, cada archivo tiene una sola persona principalmente responsable de él, con otros que hacen menores) contribuciones a ese archivo).

En un comentario, agrega:

Estoy hablando de escribir mi código en muchos archivos diferentes, pero usar un Makefile para concatenarlos

No veo por qué eso sería útil (excepto en casos muy extraños). Es mucho mejor (y una práctica muy habitual y común) compilar cada unidad de traducción (por ejemplo, cada archivo .c ) en su archivo objeto (un archivo .o ELF en Linux) y link más tarde. Esto es fácil con make (en la práctica, cuando cambie solo un archivo .c , por ejemplo, para corregir un error, solo ese archivo se compila y la compilación incremental es realmente rápida), y puede pedirle que compile archivos de objetos en parallel usando make -j (y luego su compilación va muy rápido en su procesador multi-core).


La razón principal es el tiempo de compilación. Compilar un archivo pequeño cuando lo cambia puede llevar un poco de tiempo. Sin embargo, si compila todo el proyecto cada vez que cambia una sola línea, compilará, por ejemplo, 10,000 archivos cada vez, lo que podría llevar mucho más tiempo.

Si tiene, como en el ejemplo anterior, 10,000 archivos fuente y compilar uno toma 10 ms, entonces todo el proyecto se construye de forma incremental (después de cambiar un solo archivo) en (10 ms + tiempo de enlace) si compila solo este archivo modificado, o (10 ms * 10000 + tiempo de enlace corto) si compila todo como un solo blob concatenado.


Los archivos de encabezado deben definir interfaces; esa es una convención deseable a seguir. No están destinados a declarar todo lo que está en un archivo .c correspondiente, o un grupo de archivos .c . En cambio, declaran toda la funcionalidad en los archivos .c que están disponibles para sus usuarios. Un archivo .h bien diseñado comprende un documento básico de la interfaz expuesta por el código en el archivo .c incluso si no hay un solo comentario en él. Una forma de abordar el diseño de un módulo C es escribir primero el archivo de encabezado y luego implementarlo en uno o más archivos .c .

Corolario: las funciones y las estructuras de datos internas de la implementación de un archivo .c normalmente no pertenecen al archivo de encabezado. Es posible que necesite declaraciones directas, pero deben ser locales y todas las variables y funciones declaradas y definidas deben ser static : si no forman parte de la interfaz, el vinculador no debería verlas.


Porque dividir las cosas es un buen diseño de programa. El buen diseño del programa tiene que ver con la modularidad, los módulos de código autónomos y la reutilización del código. Como resultado, el sentido común lo llevará muy lejos al hacer el diseño del programa: las cosas que no pertenecen juntas no deben colocarse juntas.

Colocar código no relacionado en diferentes unidades de traducción significa que puede localizar el alcance de las variables y funciones tanto como sea posible.

Fusionar las cosas crea un acoplamiento estrecho , lo que significa dependencias incómodas entre los archivos de código que realmente ni siquiera deberían saber acerca de la existencia del otro. Es por eso que un "global.h" que contiene todas las inclusiones en un proyecto es algo malo, porque crea un acoplamiento estrecho entre cada archivo no relacionado en todo su proyecto.

Supongamos que está escribiendo firmware para controlar un automóvil. Un módulo en el programa controla la radio FM del automóvil. Luego, reutiliza el código de radio en otro proyecto, para controlar la radio FM en un teléfono inteligente. Y luego su código de radio no se compilará porque no puede encontrar frenos, ruedas, engranajes, etc. Cosas que no tienen el más mínimo sentido para la radio FM, y mucho menos el teléfono inteligente para saber.

Lo que es aún peor es que si tiene un acoplamiento estrecho, los errores aumentan en todo el programa, en lugar de permanecer local en el módulo donde se encuentra el error. Esto hace que las consecuencias del error sean mucho más graves. Escribe un error en su código de radio FM y de repente los frenos del automóvil dejan de funcionar. Aunque no haya tocado el código de freno con su actualización que contenía el error.

Si un error en un módulo rompe cosas completamente no relacionadas, es casi seguro debido al diseño deficiente del programa. Y una cierta manera de lograr un diseño de programa deficiente es fusionar todo en su proyecto en un gran blob.


Si bien aún puede escribir su programa de forma modular y construirlo como una sola unidad de traducción, perderá todos los mecanismos que C proporciona para hacer cumplir esa modularidad . Con múltiples unidades de traducción, tiene un control preciso sobre las interfaces de sus módulos utilizando, por ejemplo, palabras clave extern y static .

Al fusionar su código en una sola unidad de traducción, se perderá cualquier problema de modularidad que pueda tener porque el compilador no le advertirá sobre ellos. En un gran proyecto, esto eventualmente dará como resultado que se extiendan dependencias no deseadas. Al final, tendrá problemas para cambiar cualquier módulo sin crear efectos secundarios globales en otros módulos.


Su enfoque de concatenar archivos .c está completamente roto:

  • Aunque el comando cat *.c > to_compile.c colocará todas las funciones en un solo archivo, el orden es importante: debe tener cada función declarada antes de su primer uso.

    Es decir, tiene dependencias entre sus archivos .c que fuerzan un cierto orden. Si su comando de concatenación no cumple con este orden, no podrá compilar el resultado.

    Además, si tiene dos funciones que se usan recursivamente, no hay absolutamente ninguna forma de escribir una declaración de reenvío para al menos una de las dos. También puede colocar esas declaraciones en un archivo de encabezado donde la gente espera encontrarlas.

  • Cuando concatena todo en un solo archivo, fuerza una reconstrucción completa cada vez que cambia una sola línea en su proyecto.

    Con el enfoque clásico de compilación dividida .c / .h, un cambio en la implementación de una función requiere la recompilación de exactamente un archivo, mientras que un cambio en un encabezado requiere la recompilación de los archivos que realmente incluyen este encabezado. Esto puede acelerar fácilmente la reconstrucción después de un pequeño cambio en un factor de 100 o más (dependiendo del recuento de archivos .c).

  • Pierde toda la capacidad de compilación paralela cuando concatena todo en un solo archivo.

    ¿Tiene un procesador de 12 núcleos grande y gordo con hyper-threading habilitado? Lástima, su archivo fuente concatenado está compilado por un solo hilo. Acabas de perder una aceleración de un factor superior a 20 ... Ok, este es un ejemplo extremo, pero ya tengo un software de compilación con make -j16 , y te digo que puede marcar una gran diferencia.

  • Los tiempos de compilación generalmente no son lineales.

    Por lo general, los compiladores contienen al menos algunos algoritmos que tienen un comportamiento de tiempo de ejecución cuadrático. En consecuencia, generalmente hay un umbral desde el cual la compilación agregada es en realidad más lenta que la compilación de las partes independientes.

    Obviamente, la ubicación precisa de este umbral depende del compilador y de los indicadores de optimización que le pase, pero he visto que un compilador tarda más de media hora en un solo archivo fuente enorme. No desea tener ese obstáculo en su ciclo de cambio-compilación-prueba.

No se equivoque: a pesar de que viene con todos estos problemas, hay personas que usan la concatenación de archivos .c en la práctica, y algunos programadores de C ++ llegan al mismo punto moviendo todo a plantillas (para que la implementación se encuentre en el .hpp y no hay ningún archivo .cpp asociado), lo que permite que el preprocesador realice la concatenación. No veo cómo pueden ignorar estos problemas, pero lo hacen.

También tenga en cuenta que muchos de estos problemas solo se hacen evidentes con proyectos de mayor tamaño. Si su proyecto tiene menos de 5000 líneas de código, sigue siendo relativamente irrelevante cómo lo compila. Pero cuando tiene más de 50000 líneas de código, definitivamente desea un sistema de compilación que admita compilaciones incrementales y paralelas. De lo contrario, está perdiendo su tiempo de trabajo.


Podría hacerlo, pero nos gusta separar los programas C en unidades de traducción separadas, principalmente porque:

  1. Acelera las construcciones. Solo necesita reconstruir los archivos que han cambiado, y estos pueden vincularse con otros archivos compilados para formar el programa final.

  2. La biblioteca estándar de C consta de componentes precompilados. ¿Realmente querrías tener que volver a compilar todo eso?

  3. Es más fácil colaborar con otros programadores si la base del código se divide en diferentes archivos.


  • Con la modularidad, puede compartir su biblioteca sin compartir el código.
  • Para proyectos grandes, si cambia un solo archivo, terminaría compilando el proyecto completo.
  • Puede quedarse sin memoria más fácilmente cuando intenta compilar grandes proyectos.
  • Puede tener dependencias circulares en los módulos, la modularidad ayuda a mantenerlas.

Puede haber algunas mejoras en su enfoque, pero para lenguajes como C, compilar cada módulo tiene más sentido.