¿Cómo puedo dividir mis progtwigs monolíticos en archivos más pequeños y separados?

En todo el código que veo en línea, los progtwigs siempre se dividen en muchos archivos más pequeños. Sin embargo, para todos mis proyectos para la escuela, me las arreglé solo por tener un archivo fuente C gigantesco que contiene todas las estructuras y funciones que uso.

Lo que quiero aprender a hacer es dividir mi progtwig en archivos más pequeños, lo que parece ser el estándar profesional. (Por cierto, ¿por qué esto es así? ¿Es solo para facilitar la lectura?)

He buscado por todas partes, y todo lo que puedo encontrar información sobre la construcción de bibliotecas, que no es lo que quiero hacer, no lo creo. Desearía poder ser más útil, pero no estoy totalmente seguro de cómo implementar esto, solo estoy seguro del producto final que quiero.

Bueno, eso es exactamente lo que quieres: ¡divide tu código en varias bibliotecas!

Tomemos un ejemplo, en un archivo tienes:

#include  int something() { return 42; } int bar() { return something(); } void foo(int i) { printf("do something with %d\n", i); } int main() { foo(bar()); return 0; } 

puede dividir esto hasta

mylib.h:

 #ifndef __MYLIB_H__ #define __MYLIB_H__ #include  int bar(); void foo(); #endif 

NB: el código del preprocesador anterior se denomina “guarda” y se usa para no ejecutar dos veces este archivo de encabezado, por lo que puede llamar a la misma inclusión en varios lugares y no tener ningún error de comstackción

mylib.c:

 #include  int something() { return 42; } int bar() { return something(); } void foo(int i) { printf("do something with %d\n", i); } 

myprog.c:

 #include  int main() { foo(bar()); return 0; } 

para comstackrlo usted hace:

 gcc -c mylib.c -I./ gcc -o myprog myprog.c -I./ mylib.o 

Ahora las ventajas?

  1. le permite dividir lógicamente su código, y luego encontrar la unidad de código más rápido
  2. le permite dividir su comstackción y recomstackr solo lo que necesita cuando modificó algo (que es lo que Makefile hace por usted)
  3. le permite exponer algunas funciones y ocultar otras (como el “algo ()” en el ejemplo anterior), y ayuda a documentar sus API para las personas que leerán su código (como su profesor);)

¿Es solo para facilitar la lectura?

Las razones principales son

  • Mantenimiento: en progtwigs grandes y monolíticos como lo que usted describe, existe el riesgo de que el cambio de código en una parte del archivo pueda tener efectos no deseados en otra parte. De vuelta en mi primer trabajo, tuvimos la tarea de acelerar el código que conducía una pantalla gráfica 3D. Era una función main , monolítica, de más de 5000 líneas (no tan grande en el gran esquema de las cosas, pero lo suficientemente grande como para ser un dolor de cabeza), y cada cambio que hicimos rompió un camino de ejecución en otro lugar. Este fue un código mal escrito en todos los sentidos ( goto s en abundancia, literalmente cientos de variables separadas con nombres increíblemente informativos como nv001x , estructura del progtwig que se lee como BÁSICA de la vieja escuela, micro-optimizaciones que no hacen más que hacer el código que mucho más difícil de leer, frágil como el infierno) pero mantenerlo todo en un archivo empeoró la mala situación. Finalmente, nos dimos por vencidos y le dijimos al cliente que tendríamos que reescribir todo desde cero o que tendrían que comprar hardware más rápido. Terminaron comprando hardware más rápido.

  • Reutilización: No tiene sentido escribir el mismo código una y otra vez. Si obtiene un bit de código generalmente útil (como, por ejemplo, una biblioteca de análisis XML o un contenedor genérico), manténgalo en sus propios archivos de origen comstackdos por separado, y simplemente vincúlelos cuando sea necesario.

  • Probabilidad: dividir las funciones en sus propios módulos separados le permite probar esas funciones de forma aislada del rest del código; Puedes verificar cada función individual más fácilmente.

  • Capacidad de construcción: está bien, así que ” capacidad de construcción ” no es una palabra real, pero reconstruir todo un sistema desde cero cada vez que cambies una o dos líneas puede llevar mucho tiempo. He trabajado en sistemas muy grandes donde las construcciones completas pueden tardar más de varias horas. Al dividir su código, limita la cantidad de código que debe reconstruirse. Sin mencionar que cualquier comstackdor tendrá algunos límites en el tamaño del archivo que puede manejar. Ese controlador gráfico que mencioné anteriormente? Lo primero que intentamos hacer para acelerarlo fue comstackrlo con las optimizaciones activadas (comenzando con O1). El comstackdor consumió toda la memoria disponible, luego consumió todo el intercambio disponible hasta que el kernel entró en pánico y derribó todo el sistema. Literalmente no pudimos construir ese código con ninguna optimización activada (esto fue en los días en que 128 MB era una memoria muy costosa). Si ese código se hubiera dividido en varios archivos (demonios, solo varias funciones dentro del mismo archivo), no habríamos tenido ese problema.

  • Desarrollo paralelo: No hay una palabra de “capacidad” para esto, pero al dividir la fuente en varios archivos y módulos, puede paralizar el desarrollo. Trabajo en un archivo, tú trabajas en otro, alguien más trabaja en un tercero, etc. No nos arriesgamos a pisar el código del otro de esa manera.

¿Es solo para facilitar la lectura?

No, también te puede ahorrar mucho tiempo comstackndo; cuando cambia un archivo de origen, solo recomstack ese archivo, luego vuelve a vincularlo, en lugar de recomstackrlo todo. Pero el punto principal es dividir un progtwig en un conjunto de módulos bien separados que son más fáciles de entender y mantener que un solo “blob” monolítico.

Para empezar, intente adherirse a la regla de Rob Pike de que “los datos dominan” : diseñe su progtwig alrededor de un montón de estructuras de datos ( struct , generalmente) con operaciones en ellas. Coloque todas las operaciones que pertenecen a una única estructura de datos en un módulo separado. Haga todas las funciones static que no necesitan ser llamadas por funciones fuera del módulo.

La facilidad para leer es un punto para dividir los archivos, pero otro es que al crear un proyecto que contiene varios archivos (encabezado y fuente), un buen sistema de comstackción solo reconstruirá los archivos que se han modificado, acortando así los tiempos de comstackción.

En cuanto a cómo dividir un archivo monolítico en varios archivos, hay muchas maneras de hacerlo. Hablando por mí, trataría de agrupar la funcionalidad, por ejemplo, todo el manejo de entrada se coloca en un archivo de origen, la salida en otro y las funciones que son utilizadas por muchas funciones diferentes en un tercer archivo de origen. Haría lo mismo con estructuras / constantes / macros, estructuras relacionadas con grupos / etc. en archivos de encabezado separados. También marcaría las funciones usadas solo en un archivo fuente único como static , por lo que no se pueden usar de otros archivos fuente por error.

Solo para darte una idea.

cree un archivo llamado print.c, ponga esto dentro:

 #include  #include  #include  void print_on_stdout(const char *msg) { if (msg) fprintf(stdout, "%s\n", msg); } void print_on_stderr(const char *msg) { if (msg) fprintf(stderr, "%s\n", msg); } 

cree un archivo llamado print.h, ponga esto dentro:

 void print_on_stdout(const char *msg); void print_on_stderr(const char *msg); 

Crea un archivo llamado main.c, pon esto dentro:

 #include  #include  #include  #include "print.h" int main() { print_on_stdout("test on stdout"); print_on_stderr("test on stderr"); return 0; } 

Ahora, para cada archivo C, compile con:

 gcc -Wall -O2 -o print.o -c print.c gcc -Wall -O2 -o main.o -c main.c 

Luego vincula los archivos comstackdos para generar un ejecutable:

 gcc -Wall -O2 -o test print.o main.o 

Ejecutar ./prueba y disfruta.

Bueno, no soy un experto, pero siempre trato de pensar en entidades más grandes que una función. Si tengo un grupo de funciones que lógicamente pertenecen juntas, lo pongo en un archivo separado. Por lo general, si la funcionalidad es similar y alguien quiere una de esas funciones, probablemente también necesite otras funciones de este grupo.

La necesidad de dividir el archivo único se debe a la misma razón por la que está utilizando carpetas diferentes para sus archivos: la gente quiere tener alguna organización lógica en las numerosas funciones, de modo que no necesiten grep el enorme archivo de origen único para encontrando la necesaria. De esta manera, puede olvidarse de las partes irrelevantes del progtwig cuando esté pensando / desarrollando alguna parte fija de él.

Una razón más para la división podría ser que puede ocultar alguna función interna del rest del código al no mencionarlo en el encabezado. De esta manera, separe explícitamente las funciones internas (que solo se necesitan dentro del archivo .c ) de las funciones interesantes para el “universo” externo de su progtwig.

Algunos lenguajes de nivel más alto incluso han extendido la noción de “función que pertenece en conjunto” a “funciones que trabajan en la misma cosa, presentada como una entidad”, y llamaron a eso una clase .

Otra razón histórica para la división es la función de comstackción separada. Si su comstackdor es lento (como suele ser el caso con C ++, por ejemplo), dividir el código en varios archivos significa que si modifica solo una ubicación, hay muchas posibilidades de que solo se deba volver a comstackr un archivo para recoger los cambios. . Como los comstackdores de C modernos no son tan lentos en comparación con la velocidad típica del procesador, esto puede no ser un problema para usted.