← Programación en C para Sistemas Operativos Bloque 3 · Funciones y organización · Tema 13 de 42

Tema 13

Programas en varios archivos

Ningún programa real cabe en un solo archivo. El núcleo Linux tiene decenas de miles; bash, más de cien. En el tema 4 vimos que el compilador permite compilar cada archivo por separado y unirlos al final; en este tema aprendemos a organizarlos. Un programa se divide en módulos, cada uno con una interfaz pública en un encabezado .h y su implementación en un .c. Veremos qué va en cada archivo, cómo se comparte una variable entre ellos con extern, cómo se esconde lo privado con static, para qué sirven las guardas de inclusión, y los errores típicos, algunos de los cuales el compilador no puede detectar porque nunca ve más de un archivo a la vez.

1. Por qué dividir un programa

Un archivo de varios miles de líneas es difícil de leer, de navegar y de modificar sin romper algo. Dividirlo tiene ventajas que van más allá del orden:

2. Un módulo: interfaz e implementación

En C, la unidad de organización es el módulo: un par de archivos con el mismo nombre.

ArchivoPapelContieneLo leen
formato.hInterfaz: qué ofrece el módulo.Declaraciones: prototipos, tipos, constantes, extern.Todos los archivos que usan el módulo, con #include.
formato.cImplementación: cómo lo hace.Definiciones: el código de las funciones, las variables, y todo lo privado.Solo el compilador, una vez.

Es la misma división que ya conocemos de la biblioteca de C: stdio.h es la interfaz, que incluimos en cada programa, y su implementación está compilada dentro de libc.so.6, como vimos en el tema 3. Quien usa printf lee su prototipo y su manual, nunca su código. La meta de un buen módulo propio es la misma: que quien lo usa solo necesite leer el .h.

3. El proyecto de ejemplo

Para todo el tema usaremos una pequeña herramienta, tamano, que muestra el tamaño de los archivos que recibe, en un formato legible. Reúne piezas de temas anteriores en tres módulos:

terminal · la estructura del proyecto
$ find . -type f | sort
./include/avisos.h
./include/formato.h
./src/avisos.c
./src/formato.c
./src/main.c

El módulo formato convierte bytes en texto, como tamano.c del tema 11, pero escribiendo en un buffer del llamador, la solución recomendada en el tema 12:

include/formato.h
/* formato.h — convertir cantidades en texto legible */
#ifndef FORMATO_H
#define FORMATO_H

#include <stddef.h>            /* size_t */

/* Escribe en destino (de tam bytes) un tamaño como "15.6 KiB".
   Devuelve destino. */
char *formato_tamano(double bytes, char *destino, size_t tam);

#endif
src/formato.c
/* formato.c — implementación de formato.h */
#include <stdio.h>
#include "formato.h"

static const char *unidades[] = {"B", "KiB", "MiB", "GiB", "TiB"};

char *formato_tamano(double bytes, char *destino, size_t tam)
{
    int i = 0;

    while (bytes >= 1024 && i < 4) {
        bytes /= 1024;
        i++;
    }
    snprintf(destino, tam, "%.1f %s", bytes, unidades[i]);
    return destino;
}

El módulo avisos es la función de mensajes del tema 11, convertida en módulo, con una variable compartida que decide si se muestran los mensajes informativos:

include/avisos.h
/* avisos.h — mensajes de la herramienta por stderr */
#ifndef AVISOS_H
#define AVISOS_H

extern int avisos_detalle;     /* 0: solo errores; 1: también información */

void avisos_iniciar(const char *nombre_programa);

__attribute__((format(printf, 1, 2)))
void avisos_error(const char *formato, ...);

__attribute__((format(printf, 1, 2)))
void avisos_info(const char *formato, ...);

#endif
src/avisos.c
/* avisos.c — implementación de avisos.h */
#include <stdio.h>
#include <stdarg.h>
#include "avisos.h"

int avisos_detalle = 0;                    /* la definición: aquí vive */

static const char *programa = "?";        /* privada de este archivo */

static void emitir(const char *tipo, const char *formato, va_list args)
{
    fprintf(stderr, "%s: %s", programa, tipo);
    vfprintf(stderr, formato, args);
    fputc('\n', stderr);
}

void avisos_iniciar(const char *nombre_programa)
{
    programa = nombre_programa;
}

void avisos_error(const char *formato, ...)
{
    va_list args;

    va_start(args, formato);
    emitir("error: ", formato, args);
    va_end(args);
}

void avisos_info(const char *formato, ...)
{
    va_list args;

    if (!avisos_detalle)
        return;
    va_start(args, formato);
    emitir("", formato, args);
    va_end(args);
}

Y main.c usa los dos módulos sin saber cómo están hechos:

src/main.c
/* main.c — tamano: muestra el tamaño de los archivos indicados */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "avisos.h"
#include "formato.h"

int main(int argc, char *argv[])
{
    int errores = 0;
    int primero = 1;
    char texto[32];

    avisos_iniciar("tamano");
    if (argc > 1 && strcmp(argv[1], "-v") == 0) {
        avisos_detalle = 1;
        primero = 2;
    }
    for (int i = primero; i < argc; i++) {
        struct stat info;

        avisos_info("consultando %s", argv[i]);
        if (stat(argv[i], &info) != 0) {
            avisos_error("no se puede consultar '%s'", argv[i]);
            errores++;
            continue;
        }
        printf("%10s  %s\n", formato_tamano((double) info.st_size, texto, sizeof texto), argv[i]);
    }
    return errores > 0;
}

4. Compilar el proyecto

Cada .c se compila por separado hasta su objeto, y al final se enlazan, como en el tema 4. La novedad es -I include, que agrega el directorio include a la lista donde el preprocesador busca los encabezados:

terminal · compilar cada módulo, enlazar y probar
$ gcc -std=c17 -Wall -Wextra -Iinclude -c src/formato.c -o formato.o
$ gcc -std=c17 -Wall -Wextra -Iinclude -c src/avisos.c -o avisos.o
$ gcc -std=c17 -Wall -Wextra -Iinclude -c src/main.c -o main.o
$ gcc -o tamano main.o formato.o avisos.o
$ ./tamano tamano /etc/passwd /usr/lib/x86_64-linux-gnu/libc.so.6 noexiste; echo $?
  16.3 KiB  tamano
   1.3 KiB  /etc/passwd
   2.1 MiB  /usr/lib/x86_64-linux-gnu/libc.so.6
tamano: error: no se puede consultar 'noexiste'
1
$ ./tamano -v tamano noexiste
tamano: consultando tamano
  16.3 KiB  tamano
tamano: consultando noexiste
tamano: error: no se puede consultar 'noexiste'
$ gcc -std=c17 -Wall -Wextra -c src/main.c     # sin -I
src/main.c:6:10: fatal error: avisos.h: No such file or directory
    6 | #include "avisos.h"
      |          ^~~~~~~~~~

Recordemos del tema 3 que #include "…" busca primero en el directorio del archivo que incluye, que aquí es src, y después en la lista de directorios; los encabezados están en include, y sin -I no se encuentran. Para un proyecto pequeño también se puede compilar todo de una vez, gcc -Iinclude -o tamano src/*.c, pero entonces se recompila todo cada vez. El camino del código fuente al ejecutable es este:

La estructura del proyecto: formato.h lo incluyen formato.c y main.c; avisos.h lo incluyen avisos.c y main.c. Cada archivo .c se compila con gcc -c en su objeto: formato.o, avisos.o y main.o, y los tres se enlazan en el ejecutable tamano formato.h avisos.h formato.c avisos.c main.c formato.o avisos.o main.o tamano #include gcc -c enlazar
Los encabezados no se compilan solos: se copian, por #include, dentro de cada .c que los usa. Cada .c produce un objeto, y el enlazador los une.

5. Qué va en un encabezado

Un encabezado se copia, entero, en cada archivo que lo incluye. De ahí sale la regla de qué puede ir en él: declaraciones sí, definiciones no. Una declaración puede repetirse en muchos archivos sin problema; una definición crea algo, una función o una variable, y si se repite en dos archivos, el programa termina con dos cosas del mismo nombre.

En el .hEn el .c
Prototipos de las funciones públicas.El código de esas funciones.
Declaraciones extern de las variables públicas.La definición de esas variables, con su valor inicial.
Definiciones de tipos: struct, enum, typedef.Los tipos que solo usa el módulo.
Constantes con #define o enum que el usuario necesita.Las constantes internas.
Funciones static inline pequeñas (sección 12).Las funciones y variables auxiliares, todas static.
Los #include que la propia interfaz necesita, como stddef.h para size_t.Los #include que necesita la implementación, como stdio.h.
Comentarios que documentan cómo se usa cada función.Comentarios sobre cómo está hecha.

Las definiciones de tipos son la excepción aparente: una struct se define en el encabezado y se repite en cada archivo que lo incluye. No es un problema, porque un tipo no ocupa memoria ni genera símbolos para el enlazador; solo le dice al compilador la forma de los datos. Fíjate también en que formato.h incluye stddef.h: un encabezado debe poder incluirse solo, sin depender de que quien lo incluye haya incluido antes lo que él necesita.

La prueba de que una definición en un encabezado es un error es fácil de hacer. Este config.h define una variable, y dos archivos lo incluyen:

terminal · config.h contiene int nivel = 1; y lo incluyen uno.c y dos.c
$ gcc -std=c17 -Wall -Wextra -o config uno.c dos.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cclOvpWM.o:(.data+0x0): multiple definition of `nivel'; /tmp/ccaW75HA.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

Cada archivo compiló bien por separado, y cada uno creó su propia variable nivel. El error apareció recién al enlazar, cuando el enlazador encontró dos definiciones del mismo símbolo: el multiple definition que vimos en el tema 4.

6. Cada .c incluye su propio encabezado

formato.c incluye formato.h, aunque no lo necesita para compilar: sus funciones ya están definidas en él mismo. Lo incluye para que el compilador compare la declaración pública con la definición. Sin esa inclusión, nada garantiza que coincidan, y el resultado puede ser desastroso. En este caso, el encabezado dice que doble recibe y devuelve int, pero la implementación, en otro archivo que no incluye el encabezado, usa double:

terminal · doble.h dice int doble(int); doble.c define double doble(double) sin incluir doble.h
$ gcc -std=c17 -Wall -Wextra -o usa_doble usa_doble.c doble.c && ./usa_doble
doble(21) = -1992188440
$ gcc -std=c17 -Wall -Wextra -c doble_bien.c     # la misma definición, pero incluyendo doble.h
doble_bien.c:4:8: error: conflicting types for ‘doble’; have ‘double(double)’
    4 | double doble(double x)
      |        ^~~~~
In file included from doble_bien.c:2:
doble.h:1:5: note: previous declaration of ‘doble’ with type ‘int(int)’
    1 | int doble(int x);
      |     ^~~~~

La primera compilación no dio ninguna advertencia, y el programa mostró un número absurdo. Cada archivo era correcto en sí mismo: usa_doble.c pasó un int en el registro %edi, según el prototipo que conocía, y esperó el resultado en %eax; doble.c, compilado por separado, buscó su argumento double en %xmm0 y dejó el resultado en otro registro de punto flotante, por la convención del tema 12. El compilador nunca vio los dos archivos a la vez, y el enlazador solo compara nombres, no tipos. Con el encabezado incluido en la implementación, el mismo error se convierte en un conflicting types al compilar.

Es una regla que no tiene excepciones: cada .c incluye su propio .h, y conviene que sea su primer #include, lo que además comprueba que el encabezado se puede incluir solo. Como red de seguridad adicional, la optimización en tiempo de enlace, -flto, ve todo el programa a la vez y detecta estos desacuerdos: con el mismo par de archivos, advierte type of ‘doble’ does not match original declaration [-Wlto-type-mismatch].

7. Variables compartidas: extern

Una variable global se comparte entre archivos con la misma lógica que una función: una sola definición, en un .c, y una declaración en el encabezado. Para una variable, la declaración sin definición se escribe con extern:

EscritoEsEfectoDónde
extern int avisos_detalle;Declaración«Existe una variable int con este nombre, definida en algún lugar.» No reserva memoria.En el .h.
int avisos_detalle = 0;DefiniciónCrea la variable y le da su valor inicial.En un solo .c.
int avisos_detalle; fuera de funcionesDefinición tentativaUna definición con valor 0, salvo que aparezca otra con valor en el mismo archivo.Mejor evitarla: escribir el = 0.

Con este esquema, main.c escribe avisos_detalle = 1 y avisos.c lo lee, y los dos hablan de la misma variable, porque el enlazador resuelve el nombre en la única definición. Para las funciones, extern es implícito: un prototipo ya es una declaración, y escribir extern delante no cambia nada.

Las definiciones tentativas tienen una historia que todavía se ve. Hasta gcc 10, dos archivos con int contador; sin valor, sin extern, se fusionaban silenciosamente en una sola variable, un comportamiento llamado common. Desde gcc 10, el comportamiento por omisión es el del estándar, y es un error:

terminal · c1.c y c2.c, cada uno con int contador;
$ gcc -std=c17 -Wall -Wextra -o cc c1.c c2.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccEhu3cM.o:(.bss+0x0): multiple definition of `contador'; /tmp/ccZS8uOx.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
$ gcc -std=c17 -Wall -Wextra -fcommon -o cc c1.c c2.c; echo $?
0

Muchos programas antiguos dejaron de compilar con el cambio, y la opción -fcommon recupera el comportamiento viejo. Es mejor no necesitarla: una definición, en un solo .c, y extern en el encabezado.

Que se pueda compartir una variable no significa que convenga, como discutimos en el tema 6. avisos_detalle es un caso razonable, estado de todo el programa que se fija una vez; aun así, muchos módulos prefieren no exponer la variable y ofrecer una función, como avisos_detallar(1), que deja al módulo el control de su estado.

8. Lo privado: static

En avisos.c, la variable programa y la función emitir son static: tienen enlace interno, y no existen para los demás archivos. Son detalles de la implementación, y declararlas static lo garantiza. La ventaja se ve cuando dos archivos usan el mismo nombre para cosas distintas:

terminal · a.c y b.c definen cada uno su propia función ayudante
$ gcc -std=c17 -Wall -Wextra -o ayud a.c b.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cci0iYil.o: in function `ayudante':
b.c:(.text+0x0): multiple definition of `ayudante'; /tmp/cc7kRgJt.o:a.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
$ sed -i 's/^int ayudante/static int ayudante/' a.c b.c
$ gcc -std=c17 -Wall -Wextra -o ayud a.c b.c && ./ayud
1 2

Sin static, los dos ayudante chocan. Con static, cada archivo tiene la suya, invisible para el otro, y el programa funciona: desde_a(), en a.c, llama a la de a.c, que devuelve 1, y main, en b.c, a la de b.c, que devuelve 2. En un proyecto con muchos archivos, y con bibliotecas de terceros, los choques de nombres son una amenaza real. La regla del tema 6 se vuelve imprescindible: todo lo que no está en el .h es static.

9. Las guardas de inclusión

Un encabezado puede incluir a otros, y un archivo puede terminar incluyendo el mismo encabezado dos veces sin darse cuenta: directamente y a través de otro. Con un encabezado que solo tiene prototipos no pasa nada, porque las declaraciones se pueden repetir; pero las definiciones de tipos no:

terminal · dibujo.c incluye punto.h y figura.h, y figura.h también incluye punto.h
$ gcc -std=c17 -Wall -Wextra -c dibujo.c
In file included from figura.h:1,
                 from dibujo.c:2:
punto.h:1:8: error: redefinition of ‘struct punto’
    1 | struct punto {
      |        ^~~~~
In file included from dibujo.c:1:
punto.h:1:8: note: originally defined here
    1 | struct punto {
      |        ^~~~~

La solución es una guarda de inclusión, las tres líneas que rodean formato.h y avisos.h:

la forma de una guarda
#ifndef FORMATO_H          /* si todavía no se definió FORMATO_H... */
#define FORMATO_H          /* ...se define ahora */

/* todo el contenido del encabezado */

#endif                     /* fin del bloque condicional */

La primera vez que el preprocesador procesa el archivo, FORMATO_H no existe, de modo que entra al bloque y lo define. La segunda vez, ya existe, y el preprocesador salta todo hasta el #endif: el contenido aparece una sola vez. Es la compilación condicional que se estudia en el tema 14. El nombre de la macro tiene que ser único en todo el proyecto, y la convención es derivarlo del nombre del archivo. La biblioteca de C hace exactamente lo mismo:

terminal · la guarda de stdio.h
$ grep -n '_STDIO_H' /usr/include/stdio.h | head -n 2
24:#ifndef _STDIO_H
25:#define _STDIO_H	1

La biblioteca usa nombres que empiezan con guion bajo y mayúscula, reservados para la implementación como vimos en el tema 3; los encabezados propios no deben imitarla en eso. La alternativa #pragma once, una sola línea al principio del archivo, hace lo mismo y la entienden gcc, clang y casi todos los compiladores, pero no es parte del estándar de C. Las dos formas son aceptables; lo que no es aceptable es un encabezado sin ninguna de las dos.

10. El programa visto por el enlazador

nm muestra cómo quedó repartido el programa entre los objetos. Las letras son las del tema 6: mayúscula para lo visible desde otros archivos, minúscula para lo static, U para lo que se usa pero está definido en otra parte:

terminal · los símbolos de cada objeto
$ nm formato.o
0000000000000000 T formato_tamano
                 U snprintf
0000000000000000 d unidades
$ nm avisos.o
                 U __stack_chk_fail
0000000000000000 B avisos_detalle
0000000000000089 T avisos_error
000000000000015f T avisos_info
000000000000006f T avisos_iniciar
0000000000000000 t emitir
                 U fprintf
                 U fputc
0000000000000000 d programa
                 U stderr
                 U vfprintf
$ nm main.o
                 U __stack_chk_fail
                 U avisos_detalle
                 U avisos_error
                 U avisos_info
                 U avisos_iniciar
                 U formato_tamano
0000000000000000 T main
                 U printf
                 U stat
                 U strcmp

La salida es la interfaz del proyecto, vista desde abajo. Cada símbolo que main.o necesita (U) lo define en mayúscula otro objeto: formato_tamano está en formato.o, y avisos_detalle, la variable compartida, en avisos.o, en .bss porque vale 0. Lo privado, unidades, programa y emitir, aparece en minúscula: el enlazador lo ve, pero no deja que otro objeto lo use. Y los U que no define ningún objeto propio, como printf o stat, los resolverá la biblioteca de C. Este es el trabajo del enlazador que el tema 31 estudia a fondo.

11. Las dependencias entre archivos

Un objeto depende de su .c y de todos los encabezados que este incluye. El compilador puede calcular esa lista con -MM:

terminal · las dependencias de cada objeto
$ gcc -Iinclude -MM src/*.c
avisos.o: src/avisos.c include/avisos.h
formato.o: src/formato.c include/formato.h
main.o: src/main.c include/avisos.h include/formato.h

Si cambia formato.h, hay que recompilar formato.o y main.o, aunque no haya cambiado ningún .c. Olvidarlo no siempre produce un error: puede producir un programa que funciona mal. Este ejemplo tiene un módulo con una estructura en su encabezado, y un programa que la usa:

registro.h, en su versión original
struct registro {
    int id;
    int valor;
};

void mostrar(struct registro r);
terminal · se agrega un campo a la estructura y se recompila solo registro.c
$ gcc -std=c17 -Wall -Wextra -c registro.c usa_registro.c
$ gcc -o registro registro.o usa_registro.o && ./registro
id = 7, valor = 42
$ sed -i 's/    int id;/    long marca;\n    int id;/' registro.h     # nuevo primer campo
$ gcc -std=c17 -Wall -Wextra -c registro.c                    # solo este
$ gcc -o registro registro.o usa_registro.o && ./registro
id = -628326488, valor = 32766

Ni una advertencia, y datos absurdos. registro.o se compiló con la estructura nueva, que tiene un long al principio, y usa_registro.o sigue con la vieja: los dos objetos tienen ideas distintas de dónde está cada campo, y se pasan la estructura sin saberlo. Es el mismo problema de la sección 6, ahora causado por una compilación incompleta. La única defensa es recompilar todo lo que depende del encabezado modificado, y como hacerlo a mano no escala, es exactamente lo que automatiza make con la información de -MM, en los temas 35 y 36. Ante una duda, recompilar todo.

Al recompilar los dos archivos, apareció además una advertencia que antes no estaba: missing initializer for field ‘valor’. El programa inicializaba la estructura con {7, 42}, y con el campo nuevo al principio, el 7 fue a marca y el 42 a id. Cambiar una estructura que se inicializa por posición tiene sus propios riesgos; el tema 24 muestra la forma de inicializar por nombre de campo, {.id = 7, .valor = 42}, que es inmune a este cambio.

12. Funciones en los encabezados

Hay un caso en que el código de una función sí va en un encabezado: funciones muy pequeñas, que conviene expandir en línea en cada lugar donde se usan, como las del tema 11. Se definen static inline:

una función pequeña en un encabezado
#ifndef UNIDADES_H
#define UNIDADES_H

#include <stddef.h>

static inline size_t kib_a_bytes(size_t kib)
{
    return kib * 1024;
}

#endif

El static es lo que lo hace correcto: cada archivo que incluye el encabezado obtiene su propia copia privada, de modo que no hay multiple definition, y el compilador la expande en línea y no deja ninguna copia si no hace falta. El núcleo Linux usa este patrón en miles de funciones. Para funciones de más de unas pocas líneas, lo correcto sigue siendo un prototipo en el .h y el código en el .c.

13. Organizar y nombrar

Algunas convenciones hacen que un proyecto con muchos archivos se mantenga legible:

ConvenciónPor qué
Un prefijo por módulo en todo lo público: avisos_error, formato_tamano.C no tiene espacios de nombres; el prefijo evita choques y dice de qué módulo viene cada función.
Encabezados en include/, fuentes en src/.Separa la interfaz de la implementación; es la estructura que esperan muchas herramientas.
El .h propio primero, luego los del sistema, luego los de otros módulos.Incluir primero el propio comprueba que el encabezado es autosuficiente.
Cada archivo incluye lo que usa, y nada más.Menos dependencias, menos recompilaciones, y ningún archivo que funciona «de casualidad» por un #include ajeno.
El .h documenta cada función: qué hace, qué devuelve, cómo informa errores.Quien usa el módulo solo debería necesitar leer el encabezado.
Interfaces mínimas: exponer lo indispensable.Todo lo que se expone se vuelve difícil de cambiar, porque otros dependen de ello.

La última idea se puede llevar más lejos con los tipos opacos: un encabezado que declara struct conexion; sin definir sus campos, y funciones que reciben un puntero a ella. Quien usa el módulo no puede ver ni tocar el interior, solo usar las funciones. Es lo que hace la biblioteca de C con FILE, y lo veremos cuando lleguemos a las estructuras y los punteros.

14. Errores frecuentes

ErrorQué pasaCorrección
Definir una variable o una función en un .hmultiple definition al enlazar, si dos archivos lo incluyen.extern o prototipo en el .h; la definición en un .c.
Un .c que no incluye su propio .hDeclaración y definición pueden diferir sin advertencia: resultados absurdos.Incluirlo siempre, primero.
Declarar una función a mano en lugar de incluir su .hLa copia puede quedar desactualizada.Un solo prototipo, en el encabezado.
Encabezado sin guardaredefinition of ‘struct …’ al incluirlo dos veces.#ifndef/#define/#endif o #pragma once.
Olvidar -Ifatal error: x.h: No such file or directory.-Iinclude.
Olvidar un .o o un .c al enlazarundefined reference.Enlazar todos los objetos.
Funciones auxiliares sin staticChoques de nombres con otros archivos o bibliotecas.static en todo lo que no está en el .h.
int x; en dos archivosmultiple definition desde gcc 10.Una definición y extern en el encabezado.
Cambiar un .h y recompilar solo algunos archivosObjetos con ideas distintas de los tipos: datos corruptos.Recompilar todo lo que depende de él; make con dependencias.
Un encabezado que depende de inclusiones previasCompila en un archivo y falla en otro.Que cada .h incluya lo que necesita.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué un encabezado puede contener declaraciones pero no definiciones de funciones o variables?

Porque el encabezado se copia en cada archivo que lo incluye. Una declaración puede repetirse sin problema, porque solo describe algo que existe en otra parte. Una definición crea la función o la variable, y si dos archivos la incluyen, el programa tiene dos definiciones del mismo nombre: el enlazador lo rechaza con multiple definition.

2. ¿Por qué usa_doble mostró -1992188440 sin ninguna advertencia?

Porque usa_doble.c conocía la función como int doble(int) y le pasó un entero en %edi, esperando el resultado en %eax, mientras que doble.c, que no incluía el encabezado, la definía como double doble(double), con argumento y resultado en registros de punto flotante. El compilador nunca vio los dos archivos juntos, y el enlazador solo compara nombres, de modo que nadie detectó el desacuerdo.

3. ¿Qué gana un .c al incluir su propio encabezado, si no lo necesita para compilar?

Que el compilador vea la declaración pública y la definición en el mismo archivo, y las compare. Cualquier diferencia entre ellas se convierte en un error de compilación, conflicting types, en lugar de un error silencioso en tiempo de ejecución. Si además es el primer #include, comprueba que el encabezado se puede incluir solo.

4. ¿Qué diferencia hay entre extern int x; e int x = 0;?

La primera es una declaración: anuncia que existe una variable int llamada x, definida en algún lugar, y no reserva memoria; va en el encabezado. La segunda es una definición: crea la variable con su valor inicial; va en un solo .c.

5. ¿Por qué int contador; en dos archivos funciona con -fcommon y falla sin él?

Porque cada int contador; fuera de una función es una definición tentativa. Con el viejo comportamiento common, que -fcommon recupera, el enlazador fusionaba las definiciones tentativas del mismo nombre en una sola variable. Desde gcc 10, el comportamiento por omisión sigue el estándar, que no lo permite, y son dos definiciones del mismo símbolo.

6. ¿Cómo pueden a.c y b.c tener cada uno su función ayudante?

Declarándolas static. Con enlace interno, cada ayudante solo existe dentro de su archivo, y el enlazador no las considera el mismo símbolo. Cada archivo llama a la suya.

7. ¿Cómo funciona una guarda de inclusión?

El encabezado empieza con #ifndef NOMBRE y #define NOMBRE, y termina con #endif. La primera vez que se procesa, NOMBRE no está definido, el preprocesador entra, lo define y procesa el contenido. Las siguientes veces, NOMBRE ya existe y el preprocesador salta todo hasta el #endif, de modo que el contenido aparece una sola vez.

8. En la salida de nm main.o, ¿qué significa que formato_tamano aparezca con U?

Que main.o usa formato_tamano pero no la define: es una referencia pendiente. El enlazador la resolverá con la definición pública que encuentre en otro objeto, que aquí es formato.o, donde aparece con T.

9. Después de agregar un campo a struct registro y recompilar solo registro.c, ¿por qué los datos salen corruptos?

Porque usa_registro.o se compiló con la versión vieja de la estructura y registro.o con la nueva, y cada uno cree que los campos están en posiciones distintas. Al pasarse la estructura, uno escribe id donde el otro lee parte de marca. Hay que recompilar todo lo que incluye el encabezado modificado; gcc -MM dice qué es, y make lo automatiza.

10. ¿Por qué una función definida en un encabezado debe ser static inline?

Porque el encabezado se copia en varios archivos, y cada copia sería una definición. Con static, cada archivo tiene su propia copia privada, sin choques al enlazar; con inline, y con optimización, el compilador la expande en el lugar de cada llamada. Solo tiene sentido para funciones muy pequeñas.

11. ¿Por qué se usan prefijos como avisos_ en los nombres públicos?

Porque C tiene un único espacio de nombres para todos los símbolos públicos del programa y de las bibliotecas que usa. Un prefijo por módulo evita que dos módulos, o un módulo y una biblioteca, definan el mismo nombre, y además indica de qué módulo viene cada función al leer el código.

Para practicar

  1. Reproduce el proyecto tamano con la misma estructura de directorios, compílalo por partes y comprueba con nm qué símbolos define y usa cada objeto.
  2. Agrega al módulo avisos una función pública avisos_cantidad_errores(void) que devuelva cuántos errores se informaron, usando un contador static. Úsala en main para el código de salida, y elimina la variable errores de main.
  3. Convierte avisos_detalle en una variable static de avisos.c y expón en su lugar void avisos_detallar(int activo). ¿Qué cambia en nm avisos.o?
  4. Agrega un módulo permisos, con permisos.h y permisos.c, que contenga la función texto_permisos del tema 8, y haz que tamano muestre también los permisos de cada archivo.
  5. Quita la guarda de formato.h e inclúyelo dos veces en main.c. ¿Hay error? ¿Por qué no, si en dibujo.c sí lo había? Agrega una struct al encabezado y vuelve a probar.
  6. Cambia en formato.c el tipo del primer parámetro de formato_tamano a long, sin tocar el encabezado. Compila y explica el mensaje. Luego quita el #include "formato.h" de formato.c, compila todo y ejecuta: ¿qué pasa ahora?
  7. Usa gcc -Iinclude -MM src/*.c y escribe a mano, a partir de su salida, la lista de órdenes que habría que ejecutar si cambiara solo avisos.h.