Usar múltiples archivos
Los módulos suelen tener bastante código, por lo que dejar todo en un único main.c
nos puede resultar difícil de mantener.
Para evitar esto, podemos utilizar múltiples archivos.
Ejemplo inicial
Supongamos que tenemos un main.c
con un tipo t_persona
:
#include <stdlib.h>
#include <stdio.h>
#include <commons/string.h>
typedef struct persona {
char *nombre;
char *apellido;
int edad;
} t_persona;
int main(void) {
t_persona *messi = malloc(sizeof(t_persona));
messi->nombre = string_duplicate("Lionel");
messi->apellido = string_duplicate("Messi");
messi->edad = 34;
printf("{ nombre: %s, apellido: %s, edad: %d }\n",
messi->nombre,
messi->apellido,
messi->edad
);
free(messi->nombre);
free(messi->apellido);
free(messi);
return 0;
}
Sería más organizado convertir el tipo t_persona
en un Tipo Abstracto de Dato (TAD) con las funciones asociadas:
persona_new
: Que cree una instancia det_persona
.persona_to_string
: Que convierta una instancia det_persona
a string para ser impresa por pantalla conprintf
,puts
o cualquier función de logging.persona_destroy
: Que libere toda la memoria ocupada por una instancia det_persona
.
De esta forma, si en otra parte del código queremos crear a cr7
, podemos reutilizar la misma lógica que usamos para crear a messi
.
Crear un header
Para esto, primero crearemos un header en la carpeta src/
con nuestro editor favorito o desde la consola ejecutando:
touch src/persona.h
En él vamos a guardar los prototipos de las funciones del nuevo TAD t_persona
:
#ifndef PROJECT_PERSONA_H_
#define PROJECT_PERSONA_H_
typedef struct persona t_persona;
// También podemos agregar Docstrings como incluyen las Commons.
/**
* @NAME: persona_new
* @DESC: Crea una instancia de "t_persona".
*/
t_persona *persona_new(char *nombre, char *apellido, int edad);
/**
* @NAME: persona_to_string
* @DESC: Convierte una instancia de "t_persona" a string.
*/
char *persona_to_string(t_persona *persona);
/**
* @NAME: persona_free
* @DESC: Libera la memoria ocupada por una instancia de "t_persona".
*/
void persona_free(t_persona *persona);
#endif
TIP
Nótese que definimos al tipo t_persona
como un struct persona
pero no expusimos los atributos que lo conforman.[1]
Si bien esto es completamente opcional, si en Paradigmas vieron el concepto de encapsulamiento, esta es la forma más parecida de lograrlo en C, ya que impedimos que quienes incluyan este header manipulen los atributos del struct
directamente.
Crear un archivo fuente
Luego, crearemos un archivo fuente:
touch src/persona.c
Y moveremos ahí la implementación de ese TAD, junto con todas las bibliotecas que necesita.
#include <persona.h>
#include <stdlib.h>
#include <stdio.h>
#include <commons/string.h>
struct persona {
char *nombre;
char *apellido;
int edad;
};
t_persona *persona_new(char *nombre, char *apellido, int edad) {
t_persona *persona = malloc(sizeof(t_persona));
persona->nombre = string_duplicate(nombre);
persona->apellido = string_duplicate(apellido);
persona->edad = edad;
return persona;
}
char *persona_to_string(t_persona *persona) {
return string_from_format(
"{ nombre: %s, apellido: %s, edad: %d }",
persona->nombre,
persona->apellido,
persona->edad
);
}
void persona_free(t_persona *persona) {
free(persona->nombre);
free(persona->apellido);
free(persona);
}
También es una buena práctica incluir al inicio su header, como se puede ver al principio.
TIP
Los makefiles, por defecto, incluyen el flag -I./src
a la hora de compilar el proyecto con gcc
. De esta forma, se puede incluir cualquier header que se encuentre en esa carpeta y sus subcarpetas utilizando corchetes angulares < >
como en el ejemplo de arriba.
Sin este flag, también se podría lograr el mismo resultado utilizando rutas relativas entre comillas:
#include "../src/persona.h"
¿Cómo funciona el "#include"?
Por defecto, el compilador solamente va a buscar headers en las siguientes dos rutas:
/usr/include
/usr/local/include
Se pueden agregar más rutas pasándole a gcc
el flag -I{path}
o editando la variable de entorno C_INCLUDE_PATH
.
Incluir el archivo fuente en el main.c
Por último, vamos a hacer que nuestra función main()
utilice las funciones del nuevo TAD t_persona
para crear a messi
:
#include <stdlib.h>
#include <stdio.h>
#include <persona.h>
int main(void) {
t_persona *messi = persona_new("Lionel", "Messi", 34);
char* messi_str = persona_to_string(messi);
puts(messi_str);
free(messi_str);
persona_free(messi);
return 0;
}
En conclusión, nos quedó un main()
mucho más simple y con un TAD que nos permite crear nuevas instancias de t_persona
en el proyecto de forma más sencilla.
.
└── src
├── main.c
├── persona.c
└── persona.h
Esta forma de declarar tipos de conoce como Opaque Types, si quieren entrar más en detalle les recomiendo este video (en inglés). ↩︎