Funciones y estructura de programas en C

Aprende a organizar programas en C como se construyen los proyectos reales. Escribe funciones con parámetros, valores de retorno y prototipos, descubre por qué C pasa los argumentos por valor y sigue la ejecución de llamadas y de la recursión. Controla dónde son visibles las variables y cuánto tiempo existen mediante el ámbito y las clases de almacenamiento, incluidas static y extern. Utiliza el preprocesador para las cabeceras, las macros y la compilación condicional, divide un programa en archivos fuente y de cabecera, y constrúyelo con make para que solo se recompilen los archivos modificados.

PrincipianteUnidad 2 de 8 del curso Curso de Programación en C

Lo que aprenderás

  1. Divide un programa en funciones que reciban parámetros y devuelvan resultados, decláralas con prototipos, comprende que C pasa los argumentos por valor y sigue cómo se ejecutan y retornan las llamadas, incluidas las recursivas.
    • Definir funciones que reciban datos de entrada y devuelvan un resultado
      Escribe definiciones de funciones con un tipo de retorno, parámetros y un cuerpo, utiliza return y void correctamente y llama a funciones dentro de expresiones.
    • Declarar funciones mediante prototipos
      Declara una función con un prototipo antes de su primera llamada, conoce qué comprueba y convierte el compilador, y evita los paréntesis vacíos y las declaraciones implícitas.
    • Pasar argumentos por valor y devolver el resultado
      Explica que cada argumento de C se copia en su parámetro, predice qué puede y qué no puede cambiar una función y utiliza valores de retorno para devolver resultados.
    • Seguir la ejecución de llamadas a funciones y de la recursión
      Sigue cómo cada llamada obtiene sus propios parámetros y variables locales y retorna a la función llamadora, y escribe y sigue la ejecución de funciones recursivas con un caso base.
  2. Distingue el ámbito de un nombre del tiempo de vida de una variable, utiliza variables locales estáticas para mantener el estado entre llamadas y evalúa los costes de las variables globales.
    • Determinar dónde puede usarse un nombre (ámbito)
      Aplica el ámbito de bloque, de archivo y de prototipo, predice a qué variable se refiere un nombre cuando hay ocultación de nombres y evita errores de ámbito.
    • Elegir cuánto tiempo existe una variable mediante las clases de almacenamiento
      Distingue la duración automática de la estática, utiliza variables locales estáticas para recordar valores entre llamadas y conoce el significado de auto, register, static y extern.
    • Compartir datos con variables globales de forma cuidadosa
      Define y utiliza variables de ámbito de archivo (globales), explica su visibilidad e inicialización, y prefiere parámetros y valores de retorno cuando una variable global añadiría un acoplamiento oculto.
  3. Utiliza #include, #define y macros de forma segura, y selecciona código en tiempo de compilación mediante directivas condicionales, macros predefinidas y opciones del compilador.
    • Incluir archivos y definir macros
      Explica qué hace el preprocesador antes de la compilación, incluye cabeceras, define macros de constantes y de tipo función, y evita los problemas clásicos de las macros.
    • Compilar código de forma condicional
      Utiliza #if, #ifdef, #ifndef, #elif, #else y #endif con opciones -D y macros predefinidas para incluir o excluir código en tiempo de compilación, incluidas las guardas de inclusión.
  4. Compila varios archivos fuente por separado y enlázalos, escribe archivos de cabecera que compartan declaraciones y controla qué nombres pueden ver otros archivos mediante static y extern.
    • Compilar y enlazar varios archivos fuente
      Describe las etapas desde el archivo fuente hasta el ejecutable, compila los archivos por separado en archivos objeto, enlázalos y distingue los errores de compilación de los errores de enlazado.
    • Escribir archivos de cabecera
      Coloca las declaraciones compartidas en una cabecera con guarda, inclúyela donde sea necesaria, incluso en su propio archivo fuente, y mantén las definiciones fuera de ella.
    • Controlar la vinculación con static y extern
      Utiliza extern para compartir una variable entre archivos y static para mantener funciones y variables privadas de su archivo, y corrige los errores de enlazado que resultan de hacerlo incorrectamente.
  5. Escribe archivos makefile cuyas reglas y dependencias reconstruyan solo lo que ha cambiado, y mantenlos breves mediante variables, variables automáticas, reglas de patrón y objetivos ficticios.
    • Escribir reglas y dependencias en archivos makefile
      Escribe reglas con objetivos, prerrequisitos y recetas con sangría de tabulación, predice qué comandos ejecuta make después de un cambio y enumera correctamente las dependencias de cabeceras.
    • Simplificar archivos makefile con variables y reglas de patrón
      Utiliza variables como CC y CFLAGS, las variables automáticas $@, $< y $^, reglas de patrón, reglas predefinidas y objetivos ficticios para mantener un archivo makefile breve y correcto.

Guía de estudio de Funciones y estructura de programas en C

5 capítulos

1

Escribir, declarar y llamar a funciones

Una función da a una tarea un nombre, un conjunto de datos de entrada y una forma clara de producir un resultado. Construirás definiciones y prototipos, y después seguirás los valores al entrar en las llamadas y al regresar a las funciones llamadoras. Las trazas pequeñas muestran las reglas: un parámetro recibe una copia, un valor de retorno necesita un destino y una llamada recursiva debe terminar en algún momento. Los ejemplos usan C17; las listas explícitas de parámetros también aclaran el código al compilarlo como C23.

1.1 Definir funciones que reciban datos de entrada y devuelvan un resultado

Una función agrupa una tarea para que otra parte del programa pueda llamarla. Dale un nombre que describa esa tarea y elige sus datos de entrada y su resultado antes de escribir su cuerpo. En un programa C ordinario ejecutado en un entorno anfitrión, la ejecución comienza en main.

Construir una definición
int square(int n)
{
    return n * n;
}

La definición tiene un tipo de retorno (int), un nombre (square), una lista de parámetros (int n) y un cuerpo entre llaves. El parámetro es la variable local que recibe un dato de entrada. Un argumento es el valor proporcionado por una llamada: en square(6), ese valor es 6. La llamada produce 36. Define cada función a nivel de archivo, fuera de las demás funciones; el C estándar no permite definiciones de funciones anidadas.

Cada parámetro necesita su propio tipo. Escribe int max(int a, int b), con una coma que separe las declaraciones. En una llamada, proporciona los valores sin sus tipos: max(8, 3).

Devolver un valor a la función llamadora
int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

Un return termina de inmediato la llamada actual. Para max(8, 3), se ejecuta el primer retorno, por lo que nunca se llega al segundo. Una expresión devuelta se convierte al tipo de retorno de la función. Elige ese tipo para conservar la clase de resultado que necesitas.

Para obtener un resultado fraccionario, usa aritmética de coma flotante en todo el cálculo:

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

La llamada c_to_f(37.0) devuelve 98.6. Una versión entera que use c * 9 / 5 + 32 devuelve 98, porque la división descarta la fracción. Cambiar el orden a 9 / 5 * c + 32 hace que 9 / 5 se evalúe primero como 1, lo que da 69 para 37. Un tipo de retorno de coma flotante por sí solo no puede deshacer un cálculo entero ya realizado dentro del cuerpo.

Usar el resultado

Una llamada que devuelve un valor es una expresión. Con la definición anterior, square(3) + square(4) da 25. En square(square(2)), la llamada interna devuelve 4, que se convierte en el argumento de la llamada externa y da 16. Una sentencia como square(5); es válida, pero descarta el valor devuelto.

Mostrar y devolver cumplen funciones distintas. printf muestra texto; return proporciona un resultado que la función llamadora puede guardar o usar en otra expresión. Si una función ordinaria que no es void llega a su llave de cierre sin devolver un valor y la función llamadora usa su resultado, el comportamiento es indefinido. Compila con -Wall -Wextra para ayudar a detectar retornos ausentes.

Funciones que no producen un valor

Una función void realiza una tarea sin devolver un valor. Puede salir antes con return;. Por ejemplo, void print_line(int width) puede mostrar una línea de guiones, pero int x = print_line(5); no es válido porque la llamada no proporciona ningún valor. Para no recibir datos de entrada, escribe void en la lista de parámetros, como en int read_sensor(void).

int main(void) devuelve un estado al entorno anfitrión. Usa return 0; para indicar éxito; <stdlib.h> también proporciona EXIT_SUCCESS y EXIT_FAILURE. Llegar al final de main devuelve cero automáticamente. Las demás funciones que devuelven valores no tienen esa excepción.

Puntos clave

  • Una definición indica un tipo de retorno, un nombre, parámetros con sus tipos y un cuerpo.
  • Un parámetro recibe un argumento proporcionado por una llamada.
  • return termina la llamada; mostrar un valor no proporciona su resultado.
  • Usa void para no devolver ningún valor y (void) para no recibir parámetros.
  • Elige operaciones aritméticas y tipos de retorno que conserven el resultado necesario.

1.2 Declarar funciones mediante prototipos

Una llamada a una función necesita una declaración que el compilador ya haya visto. Si la definición aparece más adelante, coloca un prototipo antes de la primera llamada. El prototipo indica al compilador cómo comprobar la llamada sin proporcionar el cuerpo de la función.

Separar la promesa del cuerpo
#include <stdio.h>

double c_to_f(double c);

int main(void)
{
    printf("%.1f\n", c_to_f(25.0));
    return 0;
}

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

El prototipo termina con un punto y coma. La definición tiene un cuerpo y no lleva punto y coma entre su lista de parámetros y la llave de apertura. Una organización útil es: cabeceras, prototipos, main y después las demás definiciones. La salida aquí es 77.0.

Los nombres de los parámetros son opcionales en los prototipos: double c_to_f(double); declara la misma función. Nombres como c pueden documentar un dato de entrada, pero no tienen que coincidir con los nombres de la definición. Todas las declaraciones deben coincidir en el tipo de retorno y los tipos de los parámetros. Una función utilizada necesita una definición; repetir prototipos compatibles no crea más funciones.

Qué comprueba el compilador

Con un prototipo, el compilador comprueba el número de argumentos y convierte cada valor proporcionado al tipo declarado del parámetro. Dado double c_to_f(double c);, tanto c_to_f() como c_to_f(20.0, 5.0) tienen un número incorrecto de argumentos y requieren un diagnóstico.

En cambio, una diferencia de tipo numérico puede ser una conversión permitida:

int half(int n)
{
    return n / 2;
}

Para half(7.9), la conversión da primero al parámetro el valor 7. La división entera produce entonces 3. Para half(-7.9), la conversión da -7 y la división produce -3. En ambos casos se descarta la parte fraccionaria hacia cero. La llamada no conserva el valor double original solo porque el argumento se haya escrito con un punto decimal. Añade -Wconversion cuando quieras recibir advertencias sobre estas conversiones.

Indicar explícitamente que no hay parámetros
DeclaraciónSignificado en C17Significado en C23
int read_sensor(void);Sin parámetrosSin parámetros
int read_sensor();Información sobre los parámetros no especificadaSin parámetros

En C17, una lista de parámetros vacía en una declaración impide la comprobación completa de los argumentos; no autoriza argumentos arbitrarios para la función que se defina finalmente. Usa (void) de forma coherente. GCC 15 cambió su dialecto de C predeterminado a gnu23, por lo que -std=c17 hace explícito el estándar previsto al seguir estos ejemplos.

Corregir un problema de declaración

Llamar a una función no declarada infringe las reglas del C moderno. Incluye la cabecera estándar que la declara, como <stdio.h> para printf, o añade tu propio prototipo compatible. Las versiones actuales de GCC y Clang normalmente rechazan las declaraciones implícitas.

Un prototipo como double area(double r); no puede coincidir con una definición que devuelve float y recibe float. Incluye la cabecera de un módulo en el archivo que define sus funciones para que el compilador pueda compararlos. No intentes corregir una discrepancia de tipos añadiendo una conversión explícita en la llamada: corrige la declaración y la definición para que describan la misma función.

Puntos clave

  • Coloca una declaración antes de la primera llamada.
  • Un prototipo termina con punto y coma; una definición proporciona el cuerpo.
  • Los prototipos comprueban el número de argumentos y especifican sus conversiones.
  • Usa (void) para indicar que no hay parámetros tanto en C17 como en C23.
  • Incluye la cabecera que contiene la declaración y mantén todas las declaraciones compatibles.
  • Una conversión numérica permitida también puede perder una fracción.

1.3 Pasar argumentos por valor y devolver el resultado

C pasa todos los argumentos por valor: cada parámetro comienza con una copia del valor de su argumento. Asignar un valor a un parámetro entero ordinario cambia esa copia, incluso si comparte nombre con una variable de la función llamadora.

Seguir la copia

Con <stdio.h> incluido y esta definición antes de su llamada:

void double_it(int n)
{
    n *= 2;
    printf("inside: %d\n", n);
}

La función llamadora ejecuta:

int score = 21;
double_it(score);
printf("after: %d\n", score);

La salida es inside: 42 y después after: 21. El parámetro cambia de 21 a 42; ninguna sentencia asigna un valor a score en la función llamadora.

Conservar un resultado devuelto
int doubled(int n)
{
    return n * 2;
}
Sentencia de la función llamadoraEfecto en una score inicialmente igual a 21
doubled(score);Descarta 42; score sigue siendo 21
score = doubled(score);Guarda 42 en score

La función calcula un valor; la asignación de la función llamadora actualiza su variable. Devolver el valor de una variable local es seguro: int total = a + b; return total; copia el valor antes de que la variable local automática deje de existir. Los cálculos que usan solo parámetros y resultados devueltos pueden probarse sin preparar un estado compartido oculto.

Entender por qué falla un intercambio solo por valor
void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
}

Después de int p = 3, q = 7; swap(p, q);, la función llamadora sigue teniendo p == 3 y q == 7. El cuerpo intercambió sus copias. Para modificar objetos de la función llamadora mediante parámetros se necesitan sus direcciones, pasadas como valores de puntero.

Secuenciar los argumentos cuando importa el orden

Los argumentos pueden ser expresiones como a + 1 o square(b). Todos terminan de evaluarse antes de que comience el cuerpo de la función llamada, pero su orden de evaluación no está especificado. Dos llamadas a next_ticket(), que modifica el estado, en una misma impresión pueden proporcionar los números de turno en cualquiera de los dos órdenes. Usa sentencias separadas:

int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);

Evita f(i, i++): su lectura y modificación no secuenciadas de i producen comportamiento indefinido.

Puntos clave

  • Los parámetros reciben copias de los valores de los argumentos.
  • Guarda un resultado devuelto mediante una asignación cuando la función llamadora necesite una actualización.
  • Un intercambio solo por valor modifica sus parámetros, no las variables de la función llamadora.
  • Usa sentencias separadas cuando importe el orden de evaluación de los argumentos.

1.4 Seguir la ejecución de llamadas a funciones y de la recursión

Una llamada pausa la función llamadora hasta que hay un resultado disponible. Seguir las llamadas activas explica cómo un programa retorna al lugar correcto y por qué la recursión necesita una regla de parada.

Mantener un registro por cada llamada activa

La mayoría de las implementaciones usan una pila de llamadas. Cada llamada activa tiene un marco que registra los valores de sus parámetros, sus variables locales automáticas y dónde debe continuar la función llamadora. El estándar especifica el comportamiento sin exigir esta distribución concreta de la memoria.

Si main llama a print_table y print_table llama a c_to_f, la función de conversión retorna primero. La ejecución se reanuda dentro de print_table, que después retorna a main. Las variables locales automáticas ordinarias pertenecen a una llamada. Una variable local estática es diferente: todas las llamadas comparten su único valor almacenado.

Seguir un cálculo recursivo en la ida

Una función recursiva se llama a sí misma con un problema más pequeño. Un caso base retorna directamente; un caso recursivo avanza hacia él.

int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

Para factorial(4), la primera llamada no puede terminar su multiplicación hasta que retorne factorial(3). Esa llamada espera a factorial(2), que espera a factorial(1).

Llamada activaOperación que espera un resultado
factorial(4)4 * factorial(3)
factorial(3)3 * factorial(2)
factorial(2)2 * factorial(1)
factorial(1)Caso base: devuelve 1

En el punto más profundo hay cuatro llamadas activas a factorial. Cada una tiene su propia n; llegar a n == 1 no cambia los valores de las llamadas anteriores.

Seguir los retornos en la vuelta

La llamada del caso base devuelve 1. La llamada con n == 2 calcula ahora 2 * 1 y devuelve 2. La siguiente devuelve 3 * 2, es decir, 6; la primera devuelve 4 * 6, es decir, 24. Anota los valores de retorno en una traza durante la vuelta, después de conocer el resultado de la función llamada.

La condición de parada debe ser alcanzable para las entradas admitidas. Si la comprobación fuera n == 1, comenzar en cero seguiría produciendo llamadas con -1, -2 y valores menores en lugar de llegar a uno. Continuar la recursión agota los recursos y puede provocar un fallo. La comprobación más amplia n <= 1 detiene estas entradas, aunque el factorial matemático se usa para enteros no negativos.

Comprobar tanto la profundidad como el tipo del resultado

Detenerse correctamente no garantiza que el resultado quepa en el tipo. En una plataforma con un int de 32 bits, el valor máximo es 2147483647. factorial(12) da 479001600, que cabe; factorial(13) necesitaría 6227020800, que no cabe. El desbordamiento de enteros con signo tiene comportamiento indefinido.

Un bucle puede calcular el factorial manteniendo un producto acumulado en una sola llamada a función. La recursión es útil cuando el siguiente problema tiene la misma estructura, pero un tamaño menor. Para enteros positivos, sumar los dígitos es otro ejemplo: sum_digits(472) necesita 2 + sum_digits(47), después 2 + 7 + sum_digits(4), lo que da 13. Una traza debe mostrar tanto la reducción de la entrada como la combinación posterior de los valores devueltos.

Puntos clave

  • Las llamadas terminan en orden inverso al de su anidamiento.
  • Cada llamada tiene sus propios parámetros y variables locales automáticas.
  • La recursión necesita un caso base alcanzable y avanzar hacia él.
  • Sigue las llamadas en la ida y los valores devueltos en la vuelta.
  • Comprueba que los resultados recursivos quepan en su tipo numérico.
2

Controlar dónde son visibles las variables y cuánto tiempo existen

Un nombre puede ser visible en una pequeña parte del código fuente mientras su variable existe durante todo el programa. Mantener separadas esas dos propiedades facilita mucho la comprensión de las variables locales, las locales estáticas y las globales. Seguirás los nombres a través de bloques anidados, compararás valores nuevos con valores conservados y elegirás cómo obtiene una función los datos que necesita. Esa elección afecta a la facilidad con la que puedes rastrear un resultado incorrecto y probar una función por separado.

2.1 Determinar dónde puede usarse un nombre (ámbito)

El ámbito es la parte del código fuente donde puede usarse un nombre. Lee las llaves y las declaraciones para determinar a qué variable se refiere una expresión; el orden de las llamadas no cambia estas reglas.

Encontrar el límite del ámbito

Una variable declarada dentro de un bloque tiene ámbito de bloque, que comienza justo después de su declarador y termina en la llave de cierre de ese bloque. Los parámetros son visibles dentro de los cuerpos de sus funciones. Una declaración en la cabecera de un for, como for (int i = 0; i < 10; i++), hace que i esté disponible durante ese bucle, pero no después.

Un nombre declarado fuera de todas las funciones tiene ámbito de archivo, desde su declaración hasta el final de la unidad de traducción. Las funciones situadas antes de esa declaración necesitan una declaración previa para usarlo. El nombre de un parámetro en un prototipo tiene ámbito de prototipo, que termina con el prototipo. Así, double c_to_f(double c); puede ir seguido de una definición que llame degrees al parámetro.

Seguir la ocultación de nombres de dentro hacia fuera
#include <stdio.h>

int x = 10;

void show(void)
{
    printf("%d\n", x);
}

int main(void)
{
    int x = 5;
    {
        int x = 1;
        printf("%d\n", x);
    }
    printf("%d\n", x);
    show();
    return 0;
}

La primera impresión, dentro del bloque interior, usa 1, la siguiente usa 5 y show muestra 10. Prevalece la declaración visible de x más cercana. Al salir del bloque interior, la variable local exterior vuelve a quedar visible. Llamar a show no hace que la variable local de main sea visible dentro de ella: show se definió donde la x de ámbito de archivo es visible.

Distintas funciones pueden declarar cada una una variable local i, count o result. Son variables distintas porque sus declaraciones pertenecen a bloques distintos.

Declarar un valor donde sea visible para sus usos posteriores

Este código no se compila:

if (temp > 30.0) {
    int warning = 1;
}
printf("%d", warning);

La impresión está fuera del ámbito de warning. Si el valor debe usarse después, decláralo e inicialízalo antes de la condición:

int warning = 0;
if (temp > 30.0) {
    warning = 1;
}
printf("%d", warning);

La inicialización también proporciona el valor para la ruta en la que la condición es falsa.

Evitar una nueva variable accidental

Declarar int total = 0; dentro de un bucle crea una nueva variable local cada vez que se entra en su bloque. Si querías acumular en una total exterior, usa una asignación como total += value;. Activa -Wshadow para ayudar a detectar nombres ocultos.

El ámbito comienza antes de que se evalúe el inicializador. Dentro de un bloque, int x = x + 1; lee la nueva x sin inicializar, incluso si existe una x exterior. Usa otro nombre si el inicializador necesita el valor exterior.

El ámbito describe dónde es visible un nombre en el código fuente. La duración del almacenamiento describe cuánto tiempo existe su objeto mientras se ejecuta el programa. Una variable local estática tiene ámbito de bloque mientras su valor almacenado existe durante toda la ejecución.

Puntos clave

  • El ámbito de bloque termina en la llave de cierre correspondiente.
  • Los nombres de ámbito de archivo son visibles después de sus declaraciones.
  • Una declaración interior oculta una declaración exterior visible.
  • Una función llamada no puede ver las variables locales automáticas de la función llamadora.
  • El ámbito comienza antes de que se evalúe un inicializador.
  • El ámbito y la duración del almacenamiento responden a preguntas diferentes.

2.2 Elegir cuánto tiempo existe una variable mediante las clases de almacenamiento

La duración del almacenamiento describe cuánto tiempo existe el almacenamiento de una variable. Usa variables automáticas para el trabajo que pertenece a una llamada y almacenamiento estático cuando un valor deba sobrevivir entre llamadas.

Comparar un valor nuevo con un valor conservado

Los parámetros y las variables ordinarias de ámbito de bloque tienen duración automática del almacenamiento. Cada llamada a una función obtiene sus propias instancias. Una declaración sin inicializador, como int sum;, no proporciona un valor inicial utilizable. Inicializa un acumulador antes de leerlo: int sum = 0;.

Las variables de ámbito de archivo y las variables de ámbito de bloque declaradas static tienen duración estática del almacenamiento. Existen durante toda la ejecución del programa. La inicialización ocurre una vez antes de que el programa comience; si no hay un inicializador explícito, los objetos numéricos comienzan en cero.

int next_ticket(void)
{
    static int n = 0;
    n++;
    return n;
}

Tres llamadas en sentencias separadas devuelven 1, 2 y 3. El inicializador no vuelve a ejecutarse al entrar en la función. Eliminar static crea una nueva variable automática n = 0 para cada llamada, por lo que las tres devuelven 1.

El nombre n sigue teniendo ámbito de bloque. Otras funciones no pueden nombrarlo directamente. Las llamadas recursivas también comparten la misma variable local estática; no reciben copias estáticas separadas como ocurre con las variables locales automáticas.

Inicializar una vez y asignar cuando sea necesario

En C, un inicializador numérico estático debe ser una expresión constante. static int limit = 12; es válido; static int limit = read_sensor(); no es válido. Realiza una asignación en tiempo de ejecución en código ejecutable cuando un valor deba proceder de una llamada. Una sentencia como n = 0; se ejecuta cada vez que la ejecución llega a ella, por lo que colocarla al principio de next_ticket restablecería el contador en cada llamada.

Interpretar los especificadores de clase de almacenamiento
EspecificadorSignificado en este código C17
autoAlmacenamiento automático explícito para una variable de bloque; normalmente se omite
registerUna sugerencia para acceso rápido; está prohibido obtener su dirección
static dentro de una funciónAlmacenamiento conservado con un nombre local
static en el ámbito de archivoVinculación interna, que mantiene el nombre dentro de su unidad de traducción
externUna declaración que se refiere a un objeto o función existente

Los compiladores modernos eligen por sí mismos la asignación de registros, por lo que escribir register rara vez mejora el código. auto int x; significa lo mismo que una variable local ordinaria int x; en C17. C23 también permite la inferencia de tipos con auto x = 3.5;, que declara un double.

C17 también tiene _Thread_local, escrito thread_local en C23, para el almacenamiento por hilo. La gramática agrupa typedef con los especificadores de clase de almacenamiento aunque da nombre a un tipo en lugar de elegir un almacenamiento. En C17, normalmente solo aparece un especificador de clase de almacenamiento en una declaración; _Thread_local también puede acompañar a static o extern.

Un extern int count; de ámbito de bloque no crea una copia local automática. Declara un objeto con vinculación y duración estática del almacenamiento. Ten siempre en cuenta dónde aparece la declaración antes de decidir qué hace una palabra clave de clase de almacenamiento.

Puntos clave

  • Inicializa las variables automáticas antes de leerlas.
  • El almacenamiento estático dura toda la ejecución del programa y se inicializa en cero de forma predeterminada.
  • Una variable local estática conserva su valor mientras su nombre sigue siendo local.
  • Un inicializador estático se ejecuta una vez; una asignación se ejecuta cuando se llega a ella.
  • La ubicación de static determina si su función principal es el tiempo de vida o la vinculación.

2.3 Compartir datos con variables globales de forma cuidadosa

Una variable global se declara en el ámbito de archivo, fuera de todas las funciones. Conserva un valor durante toda la ejecución y puede dar a varias funciones acceso a un estado compartido.

Seguir el objeto compartido
#include <stdio.h>

double total_km = 0.0;

void add_trip(double km)
{
    total_km += km;
}

int main(void)
{
    add_trip(12.5);
    add_trip(7.25);
    printf("%.2f\n", total_km);
    return 0;
}

Ambas llamadas actualizan el mismo objeto y producen 19.75. El parámetro km sigue siendo una copia; la función asigna por separado un valor a una variable global que puede nombrar. El paso por valor no impide esa asignación.

Las variables globales tienen duración estática del almacenamiento. Las variables globales numéricas se inicializan en cero de forma predeterminada y requieren inicializadores que sean expresiones constantes: int error_count; comienza en 0 y double limit = 2.5 * 4; comienza en 10.0.

Comprobar la visibilidad y la ocultación de nombres

Un nombre global solo es visible después de su declaración. Una función situada antes de la definición necesita una declaración previa, como extern double total_km;.

Si add_trip contiene en cambio:

double total_km = 0.0;
total_km += km;

crea y actualiza un objeto local. La variable global permanece sin cambios. Elimina la declaración adicional para actualizar el total compartido; -Wshadow ayuda a identificar la ocultación accidental.

Hacer explícitos los datos de entrada cuando no es necesario compartirlos

Un contador de errores compartido puede ser útil cuando varias funciones informan de fallos. Pero un cálculo que lee una variable global tiene un dato de entrada ausente de su lista de parámetros. Las pruebas deben preparar ese estado y, para rastrear un valor incorrecto, hay que encontrar las funciones que pueden modificarlo.

Una alternativa para el cálculo de los recorridos es:

double with_trip(double total, double km)
{
    return total + km;
}

La función llamadora usa total = with_trip(total, 12.5);. Una prueba proporciona ambos datos de entrada directamente, sin preparar ninguna variable global.

Restringir los nombres compartidos

Las variables ordinarias de ámbito de archivo tienen vinculación externa. Comparte una mediante declaraciones y una sola definición; las definiciones repetidas en distintos archivos rompen esta organización. En cambio, static en el ámbito de archivo da vinculación interna, lo que permite que otro archivo tenga su propio objeto independiente con el mismo nombre. Ambos objetos siguen existiendo durante toda la ejecución: aquí la palabra clave limita la vinculación, no el tiempo de vida.

Puntos clave

  • Una variable global conserva un valor entre llamadas y tiene duración estática del almacenamiento.
  • El paso por valor no impide que una función modifique una variable global visible.
  • El estado compartido añade entradas y efectos que la lista de parámetros no muestra.
  • Prefiere parámetros explícitos y resultados devueltos cuando no sea necesario compartir estado.
3

Utilizar el preprocesador de C

Antes de que se compile el código C, el preprocesador incluye cabeceras, expande macros y selecciona ramas condicionales. Estas operaciones transforman el código que recibe el compilador, por lo que una macro breve puede cambiar una expresión de forma sorprendente. Examinarás las expansiones, agruparás expresiones de forma segura y usarás opciones y guardas de inclusión para controlar qué texto se conserva. La distinción clave está entre elegir código durante la construcción y elegir una ruta mientras se ejecuta un programa.

3.1 Incluir archivos y definir macros

El preprocesador transforma el texto fuente antes de que el compilador compruebe las expresiones y los tipos de C. Sus directivas comienzan con #. Examina su salida con gcc -E file.c cuando resulte difícil explicar una inclusión o la expansión de una macro.

Incluir declaraciones y dar nombre a constantes

#include <stdio.h> inserta una cabecera estándar. Usa #include "convert.h" para una cabecera del proyecto: GCC busca primero en el directorio del archivo que la incluye y después en sus rutas de cabeceras configuradas. La opción -Iinclude añade un directorio de búsqueda. La inclusión copia el texto de la cabecera; no enlaza funciones compiladas.

Una macro de tipo objeto sustituye tokens:

#define MAX_STUDENTS 30

Los usos posteriores del token MAX_STUDENTS se convierten en 30. El preprocesador no modifica la cadena "MAX_STUDENTS" ni el nombre distinto MAX_STUDENTS_2. No escribas = ni un punto y coma al final. Con #define PI 3.14159;, return PI * r * r; se expande como el código incorrecto return 3.14159; * r * r;. Una directiva normalmente termina con el salto de línea; una barra invertida final la continúa.

Expandir antes de calcular

Una macro de tipo función no tiene espacio entre su nombre y el paréntesis de apertura:

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))

SQUARE_BAD(2 + 3) se convierte en 2 + 3 * 2 + 3, que se evalúa como 11. 100 / SQUARE_BAD(5) se convierte en 100 / 5 * 5, lo que da 100. La macro agrupada da 25 y 4, respectivamente. Pon paréntesis alrededor de cada parámetro y de toda la expresión de sustitución para que los operadores que la rodean no puedan cambiar su agrupación.

Escribir en cambio #define SQUARE (x) ((x) * (x)) crea una macro de tipo objeto cuya sustitución comienza con (x). El espacio cambia el significado de la definición.

Los paréntesis no impiden la evaluación repetida

SQUARE(i++) se expande como ((i++) * (i++)). Modifica i dos veces sin secuenciación, por lo que el comportamiento es indefinido. Una función real como square(i++) evalúa el argumento una vez antes de entrar en el cuerpo. Prefiere funciones para los cálculos, especialmente cuando una expresión de entrada cambia el estado.

Una macro de tipo función no tiene tipos declarados para sus parámetros ni para su retorno. El código C expandido sigue sometiéndose a la comprobación de tipos, pero no tiene un prototipo que especifique las conversiones de argumentos. Las macros siguen siendo útiles para las guardas de inclusión, la compilación condicional y las expresiones constantes enteras. En C17, una variable const int no es una expresión constante entera; una macro o una constante enum puede proporcionar un valor para case. C23 también introduce constexpr.

Convertir argumentos en tokens o texto
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b

SHOW(3 + 4); usa # para convertir el argumento en texto y muestra 3 + 4 = 7. CAT(temp, 1) usa ## para unir tokens en el nombre temp1. La conversión en literal de cadena (stringification) usa los tokens del argumento proporcionado sin expandir las macros dentro de esa aparición; una aparición ordinaria del argumento sí se expande.

Las macros no siguen el ámbito de bloque. Una definición permanece activa hasta #undef NAME o hasta el final de la unidad de traducción, incluso si la directiva se escribió entre llaves. Elimina una definición anterior con #undef antes de darle un texto de sustitución diferente.

Puntos clave

  • El preprocesamiento incluye texto y expande tokens antes de la compilación de C.
  • Una definición de macro no tiene = ni punto y coma final.
  • Pon paréntesis alrededor de los parámetros de una macro y de toda la expresión de sustitución.
  • Los argumentos repetidos de una macro pueden repetir efectos secundarios; usa una función para los cálculos.
  • # convierte un argumento en literal de cadena y ## une tokens.
  • Una macro permanece definida a través de los límites de los bloques hasta que se elimina su definición.

3.2 Compilar código de forma condicional

La compilación condicional selecciona qué líneas fuente llegan al compilador. Puede activar la salida de depuración, elegir código para una plataforma o proteger una cabecera contra inclusiones repetidas.

Comprobar la existencia o comprobar un valor
#define DEBUG 0

#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif

#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif

#ifdef DEBUG pregunta si la macro está definida. #if DEBUG evalúa su valor de sustitución como una expresión constante entera. Dentro de #if, un identificador no definido cuenta como cero; una macro mal escrita puede eliminar una rama sin avisar. Las expresiones defined DEBUG y defined(DEBUG) comprueban ambas la existencia.

Usa #ifndef NAME para la comprobación opuesta de existencia. Cierra cada condicional con #endif. #elif añade otra condición y #else proporciona la alternativa. C23 añade #elifdef y #elifndef; C17 puede expresar esas comprobaciones mediante #elif defined(NAME) o #elif !defined(NAME).

Elegir la configuración en tiempo de construcción
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif

gcc -std=c17 -DDEBUG main.c define DEBUG como 1 y conserva la impresión. Sin esa definición, la línea se elimina antes de la compilación. -DLEVEL=2 proporciona un valor como si #define LEVEL 2 apareciera antes del texto del archivo.

Esto difiere de un if ordinario, cuya condición se evalúa cuando se ejecuta el programa. Las condiciones del preprocesador no pueden leer una variable ordinaria de tiempo de ejecución. #if 0 elimina un bloque, incluido el código con comentarios /* */ existentes que harían inseguro rodearlo con otro comentario de ese tipo.

Proteger una cabecera
#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);

#endif

La primera inclusión define CONVERT_H y conserva el prototipo. Las inclusiones posteriores en esa misma unidad de traducción omiten el contenido. Usa un nombre de guarda distinto basado en el nombre de la cabecera; evita los guiones bajos iniciales reservados para la implementación. Otra unidad de traducción comienza su propio preprocesamiento e incluye las declaraciones para sí misma. Una guarda no impide que distintos archivos objeto definan el mismo símbolo externo.

Muchos compiladores admiten #pragma once, pero no es una directiva de ISO C. Las guardas funcionan con directivas condicionales estándar.

Leer la información predefinida
NombreInformación
__FILE__Nombre del archivo fuente actual como cadena
__LINE__Número de línea actual del archivo fuente
__DATE__, __TIME__Fecha y hora de traducción
__STDC_VERSION__Versión del estándar seleccionado

Los valores de versión incluyen 199901L para C99, 201112L para C11, 201710L para C17 y 202311L para C23. __func__ contiene el nombre de la función actual, pero es un identificador predefinido, no una macro.

Una rama para una plataforma puede comprobar defined(_WIN32) o defined(__linux__) y después usar #else como alternativa. Para rechazar explícitamente una construcción no admitida, escribe una condición seguida de #error "This program needs C11 or later" y #endif. En cambio, omitir un #endif produce un error de directiva como «unterminated conditional directive».

Puntos clave

  • #ifdef comprueba la existencia; #if comprueba un valor.
  • Los identificadores no definidos en #if se evalúan como cero.
  • -DNAME=value proporciona una definición de macro en tiempo de construcción.
  • Las guardas de inclusión impiden el procesamiento repetido dentro de una unidad de traducción.
  • Las macros predefinidas proporcionan información sobre el archivo, la línea, el momento de construcción y el estándar.
4

Dividir un programa en archivos fuente y de cabecera

Un programa más grande puede mantener funciones relacionadas en archivos fuente separados y compartir sus declaraciones mediante cabeceras. Cada archivo fuente se compila de forma independiente y el enlazador conecta los archivos objeto resultantes. Construirás un pequeño módulo, decidirás qué nombres necesitan los demás archivos y diagnosticarás definiciones ausentes o duplicadas. Incluir la misma cabecera en las funciones llamadoras y en el archivo que contiene las definiciones ayuda al compilador a detectar discrepancias antes de que se ejecute el programa.

4.1 Compilar y enlazar varios archivos fuente

La compilación separada convierte cada archivo fuente en un archivo objeto antes de enlazar el programa. Esto permite reutilizar los archivos objeto que no han cambiado y da a las funciones relacionadas un lugar definido.

Seguir las etapas de construcción

Una unidad de traducción es un archivo .c después del preprocesamiento, incluido el contenido de las cabeceras que incluye. Cada unidad se compila de forma independiente.

EtapaResultadoOpción de GCC que se detiene ahí
PreprocesamientoTexto C expandido-E
CompilaciónTexto ensamblador-S
EnsambladoArchivo objeto-c
EnlazadoEjecutableConstrucción final normal

Un archivo objeto contiene código máquina e información de símbolos sobre los nombres que define o necesita. En sistemas tipo Unix suele tener el sufijo .o. El enlazador conecta los usos de nombres externos con las definiciones proporcionadas por archivos objeto y bibliotecas.

Compilar dos archivos y enlazarlos

Supón que convert.h declara double c_to_f(double c);, convert.c incluye esa cabecera y define la función, y main.c incluye la cabecera y la llama.

gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp

El primer comando produce main.o sin enlazar. El segundo produce convert.o. El último crea el ejecutable temp a partir de ambos archivos objeto. También puedes compilar y enlazar con un solo comando:

gcc -Wall -Wextra -std=c17 main.c convert.c -o temp

Proporciona los archivos fuente para compilarlos y los archivos objeto para enlazarlos. Las cabeceras entran mediante #include; no las enumeres como archivos fuente del programa que se compilan por separado. Si solo cambia convert.c, reutiliza main.o, reconstruye convert.o y vuelve a enlazar. Si cambia una cabecera compartida, reconstruye cada archivo objeto cuyo archivo fuente la incluya.

Diagnosticar la etapa que falló

Un error de compilación afecta a una unidad de traducción: sintaxis no válida, número incorrecto de argumentos o tipos visibles incompatibles. El enlazador no puede corregirlos.

Un error de enlazado afecta al programa ensamblado. Si construyes solo main.c, el prototipo puede hacer que la compilación tenga éxito mientras que el enlazado informa «undefined reference to c_to_f» o «Undefined symbols». La declaración promete una función; no proporciona su código máquina. Añade convert.c a una construcción combinada o convert.o al enlazado.

Dos definiciones externas enlazadas de la misma función causan un error de símbolo duplicado. Un programa ordinario tiene una sola main; enlazar dos archivos fuente que la definen falla. Las funciones static privadas de un archivo pueden tener el mismo nombre en distintas unidades de traducción porque tienen vinculación interna.

Las bibliotecas también proporcionan definiciones. En muchos sistemas Linux, el código que usa funciones declaradas por <math.h> necesita una opción de enlazado de la biblioteca matemática: gcc main.c -o app -lm. Mantén esa opción de biblioteca después del código que la necesita.

Incluir la interfaz y compilar la implementación

Evita #include "convert.c". Eso copia los cuerpos de las funciones en otra unidad de traducción. Si convert.c también se compila y enlaza, sus definiciones externas aparecen dos veces. El archivo .h de un módulo comparte sus declaraciones; su archivo .c se compila una vez como archivo fuente.

Puntos clave

  • Una unidad de traducción es un archivo fuente preprocesado con el texto que incluye.
  • -c produce un archivo objeto sin enlazar.
  • Enlaza todos los archivos objeto necesarios en el ejecutable.
  • Las definiciones ausentes y duplicadas son problemas de enlazado.
  • Incluye cabeceras; compila los archivos fuente por separado.

4.2 Escribir archivos de cabecera

La cabecera de un módulo comparte su interfaz; su archivo fuente proporciona la implementación. Tanto las funciones llamadoras como la implementación incluyen la cabecera para que sus declaraciones coincidan.

Declarar la interfaz una vez

Un archivo convert.h con guarda contiene:

#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);
double f_to_c(double f);

#endif

La implementación en convert.c lo incluye antes de los cuerpos:

#include "convert.h"

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

double f_to_c(double f)
{
    return (f - 32.0) * 5.0 / 9.0;
}

Una función llamadora incluye la misma cabecera y <stdio.h>. La llamada printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); muestra 77.0 100.0.

Si la implementación define float c_to_f(float c), incluir su cabecera muestra al compilador los tipos incompatibles. La compilación independiente sin esa declaración puede pasar por alto la discrepancia.

Elegir el contenido adecuado
Compartir en una cabeceraMantener en un archivo fuente
Prototipos de funcionesCuerpos de funciones con vinculación externa
Declaraciones de variables externDefiniciones de objetos compartidos
Macros y tipos necesariosFunciones auxiliares y estado privados

Una cabecera que contiene int count = 0; define un objeto en cada unidad de traducción que la incluye, lo que causa definiciones externas duplicadas. Incluso int count; es una definición tentativa. La configuración predeterminada de GCC desde la versión 10 diagnostica las copias repetidas durante el enlazado, mientras que algunas cadenas de herramientas fusionan los símbolos comunes y ocultan el defecto. Usa extern int count; en la cabecera y una sola definición en un archivo fuente.

Hacer que la inclusión sea fiable

Una guarda impide el procesamiento repetido dentro de una unidad de traducción, incluida la inclusión indirecta mediante otras cabeceras. Los prototipos compatibles pueden repetirse, pero las definiciones de tipos como un enum pueden causar errores de redefinición.

Una cabecera autosuficiente incluye lo que necesitan sus declaraciones. Si un prototipo usa size_t, incluye allí <stddef.h>. Las funciones llamadoras no deberían necesitar un orden de inclusión especial.

Para las cabeceras del proyecto, las comillas hacen que GCC busque primero en el directorio del archivo que las incluye. Añade un directorio separado de cabeceras con -Iinclude, como en gcc -Iinclude -std=c17 -c src/main.c. El archivo objeto de la implementación sigue necesitando ser enlazado.

Puntos clave

  • Las cabeceras comparten prototipos, declaraciones extern, macros y tipos.
  • Mantén las definiciones de objetos compartidos y los cuerpos de funciones con vinculación externa en archivos fuente.
  • Incluye la cabecera de un módulo en su propia implementación.
  • Usa guardas e incluye las cabeceras que necesitan tus declaraciones.

4.3 Controlar la vinculación con static y extern

La vinculación determina si las declaraciones en distintos lugares se refieren a la misma entidad. Es distinta del ámbito, que describe dónde puede usarse un nombre, y de la duración del almacenamiento, que describe cuánto tiempo existe un objeto.

Conectar un nombre con un objeto
VinculaciónSignificado
ExternaLas declaraciones pueden referirse a una entidad en distintas unidades de traducción
InternaLas declaraciones se refieren a una entidad dentro de una unidad de traducción
NingunaUn nombre local, como el de un parámetro ordinario, identifica su propia entidad

Las funciones y las variables ordinarias de ámbito de archivo tienen vinculación externa de forma predeterminada. static en el ámbito de archivo da vinculación interna. Las variables locales automáticas ordinarias y los parámetros no tienen vinculación.

Para compartir un contador de errores, coloca estas declaraciones en una cabecera con guarda:

extern int error_count;
void log_error(int code);

Un archivo fuente incluye esa cabecera y proporciona:

int error_count = 0;

void log_error(int code)
{
    printf("error: %d\n", code);
    error_count++;
}

Ese archivo fuente también incluye <stdio.h> para printf. Los demás archivos fuente incluyen la cabecera compartida, no una segunda definición. Después de tres llamadas, cada función llamadora que lee error_count se refiere al mismo objeto y ve 3.

Distinguir declaración y definición

extern int error_count; declara un objeto existente y no reserva almacenamiento nuevo. int error_count = 0; lo define. Un inicializador cambia el caso: en el ámbito de archivo, extern int error_count = 0; es una definición a pesar de la palabra clave. Mantén el inicializador en el único archivo que contiene la definición.

En las declaraciones ordinarias de funciones públicas, extern es opcional: double c_to_f(double c); ya declara una función con vinculación externa. Incluye la declaración pública en el archivo que la define para que el compilador pueda comprobar su compatibilidad.

Ocultar el estado detrás de funciones
static int count = 0;

void counter_increment(void)
{
    count++;
}

int counter_value(void)
{
    return count;
}

El valor almacenado existe durante toda la ejecución, pero solo esta unidad de traducción puede referirse a él mediante el nombre count. Pon los prototipos de las funciones públicas en una cabecera. Otro archivo puede incrementar y leer el contador mediante esas funciones sin modificar directamente la variable privada. Una función auxiliar también puede ser static cuando otros archivos no la necesitan.

Diagnosticar una elección de vinculación incorrecta

Una definición static int count; en una cabecera crea un contador privado independiente en cada unidad de traducción que la incluye. Actualizar uno no actualiza los demás. Las guardas de inclusión no fusionan estos objetos: cada unidad de traducción preprocesa su propia copia de la cabecera.

Una función definida como static en una unidad de traducción no puede satisfacer una llamada externa desde otra; el enlazado falla para ese nombre externo. Da vinculación externa solo a las funciones que necesitan otros archivos y decláralas en la cabecera.

Dentro de una función, static da a una variable local almacenamiento que se conserva sin ampliar su ámbito. Fuera de las funciones, da a un objeto o función vinculación interna. Las variables de ámbito de archivo tienen duración estática del almacenamiento con o sin esa palabra clave.

Puntos clave

  • La vinculación externa conecta declaraciones entre unidades de traducción.
  • Comparte una variable mediante declaraciones extern y una sola definición.
  • static en el ámbito de archivo hace que un objeto o función sea privado de su unidad de traducción.
  • Una cabecera que contiene una variable estática da objetos separados a las unidades que la incluyen.
  • Un inicializador convierte una declaración de objeto extern de ámbito de archivo en una definición.
  • La conservación con static local y la vinculación con static de ámbito de archivo cumplen funciones distintas.
5

Construir programas de varios archivos con make

Una construcción correcta necesita más que una lista de comandos del compilador: necesita saber qué archivos dependen de otros. Un archivo makefile registra esas relaciones para que una modificación de un archivo fuente recompile los archivos objeto afectados y después vuelva a enlazar el programa. Seguirás esas decisiones de reconstrucción, incluirás dependencias de cabeceras y abreviarás comandos repetidos mediante variables y reglas de patrón. Las ejecuciones simuladas muestran el plan antes de que se ejecute cualquier receta.

5.1 Escribir reglas y dependencias en archivos makefile

make lee Makefile o makefile en el directorio del proyecto y actualiza el objetivo solicitado. Sin un argumento de objetivo, normalmente construye el primer objetivo ordinario.

Registrar salidas y entradas

Una regla comienza con target: prerequisites. Su receta está en las líneas siguientes con sangría de tabulación o después de un punto y coma en la misma línea:

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g

temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o

main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c

convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c

.PHONY: clean
clean:; rm -f temp main.o convert.o

Aquí temp es el objetivo predeterminado. make clean solicita la acción de limpieza. Si escribes una receta en la línea siguiente, usa una tabulación literal: los espacios pueden causar «missing separator». La opción del editor para mostrar los caracteres en blanco ayuda a detectar la diferencia.

Seguir las decisiones de reconstrucción

Make actualiza primero los prerrequisitos de un objetivo. Después reconstruye el objetivo si no existe o si algún prerrequisito tiene una fecha de modificación más reciente.

  • Primera construcción: compilar ambos archivos fuente y después enlazar temp: tres comandos.
  • Sin cambios: no es necesario ejecutar ninguna receta del programa.
  • Modificar solo convert.c: recompilarlo y volver a enlazar: dos comandos; se reutiliza main.o.
  • Modificar convert.h: recompilar ambos archivos fuente y volver a enlazar: tres comandos.

La cabecera afecta a ambas unidades de traducción, aunque no se haya modificado ninguno de los archivos fuente. Imagina que los archivos de entrada alimentan a los archivos objeto y que estos alimentan al ejecutable. Los cambios se propagan por esas relaciones registradas.

Incluir dependencias de cabeceras

La regla incompleta main.o: main.c no tiene en cuenta las modificaciones de convert.h. Make no lee el código fuente de C para descubrir las inclusiones; sigue los prerrequisitos declarados. Enumera las cabeceras incluidas o carga archivos de dependencias generados por el compilador para evitar enlazar archivos objeto desactualizados.

Mantener juntas las operaciones dependientes del intérprete

Las líneas separadas de una receta normalmente usan intérpretes separados. Una línea que solo contiene cd build no cambia el directorio de la línea siguiente. Usa un solo comando como cd build && gcc -c ../main.c cuando la compilación dependa de ese cambio de directorio. Previsualiza las recetas previstas con make -n.

Puntos clave

  • Las reglas indican objetivos, prerrequisitos y recetas con sangría de tabulación.
  • Make actualiza los prerrequisitos antes de decidir si debe reconstruir su objetivo.
  • Un objetivo ausente o un prerrequisito más reciente activa una reconstrucción.
  • Las modificaciones de cabeceras deben activar la reconstrucción de todos los archivos objeto afectados.
  • Cada línea de receta normalmente se ejecuta en su propio intérprete.

5.2 Simplificar archivos makefile con variables y reglas de patrón

Las variables y las reglas de patrón eliminan comandos repetidos del compilador y conservan el grafo de dependencias. Los ejemplos colocan cada receta después de un punto y coma en su línea de regla. Abrevia un archivo makefile solo después de que sus relaciones de construcción sean correctas.

Dar nombre a la configuración compartida
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o

temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^

%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:; rm -f temp $(OBJS)

$(CC) expande la variable del compilador; $(CFLAGS) expande sus opciones. Por convención, CPPFLAGS contiene opciones del preprocesador como -DDEBUG o -Iinclude. Las opciones -Wall -Wextra activan las advertencias, -g añade información de depuración y -O2 activa la optimización. Las variables de make no definidas se expanden como texto vacío. Escribe $(CC), no $CC: esto último significa la variable de una letra C, seguida de una C literal.

Interpretar las variables automáticas dentro de una regla
VariableValor en la regla actual
$@Nombre del objetivo
$<Primer prerrequisito
$^Todos los prerrequisitos, eliminando los nombres duplicados
$?Prerrequisitos más recientes que el objetivo

Para main.o, la raíz del patrón es main, por lo que %.c se convierte en main.c. La receta de compilación usa $< para pasar ese archivo fuente al compilador y $@ para dar nombre a main.o. La cabecera es una dependencia, pero no se pasa como otra entrada de compilación.

En la regla de enlazado, todos los prerrequisitos son archivos objeto. $^ se expande como main.o convert.o y $@ se convierte en temp. Usar $< ahí pasaría solo main.o y perdería las definiciones de convert.o.

GNU make tiene reglas predefinidas que pueden compilar x.c para obtener x.o usando CC, CPPFLAGS y CFLAGS. Una regla de dependencias explícita sin receta puede usar esa receta predefinida. GNU make incluso puede construir un programa sencillo con make hello a partir de hello.c sin un archivo makefile.

Marcar objetivos de acciones como ficticios

clean da nombre a una acción, no a un archivo que se va a crear. .PHONY: clean garantiza que la acción se ejecute incluso si existe un archivo llamado clean. rm -f tolera que los archivos ya no existan. Un objetivo all que agrupa varios programas también suele ser ficticio.

Generar listas de cabeceras y examinar reconstrucciones

Añade -MMD -MP a las opciones de compilación para escribir un archivo .d junto a cada archivo objeto. -MMD registra las dependencias de cabeceras de usuario; -MP añade reglas vacías que ayudan cuando se elimina una cabecera registrada. Después del objetivo predeterminado ordinario, carga las listas con:

-include $(OBJS:.o=.d)

La sustitución convierte los nombres de archivos objeto en nombres de archivos de dependencias. El signo menos inicial tolera la ausencia de archivos .d en la primera construcción. Elimina estos archivos generados en clean junto con los archivos objeto.

make -n previsualiza los comandos, make -B fuerza la reconstrucción y make -j4 permite ejecutar hasta cuatro recetas a la vez. Las dependencias correctas también importan para las construcciones paralelas. Cambiar solo el texto de CFLAGS no hace que los archivos objeto existentes sean más antiguos que un prerrequisito. Después de cambiar las opciones, usa make -B o una reconstrucción limpia para que todos los archivos objeto usen la nueva configuración.

Puntos clave

  • Usa $(NAME) para expandir una variable de make con nombre.
  • $@, $< y $^ indican el objetivo, el primer prerrequisito y todos los prerrequisitos.
  • Las reglas de patrón reutilizan una receta para archivos con raíces coincidentes.
  • Los objetivos ficticios ejecutan acciones incluso cuando existen archivos con el mismo nombre.
  • Los archivos .d generados registran las dependencias de cabeceras de usuario.
  • Fuerza una reconstrucción o realiza una reconstrucción limpia después de cambiar las opciones del compilador.

Ejemplos de Funciones y estructura de programas en C

5 Lecciones Interactivas

1

Construir y seguir una llamada a función

19 pasos
Escribe una función, declárala, conserva su resultado y sigue un cálculo recursivo.
  1. 1
    IntroducciónComenzar con una tarea pequeña

    Sigue un valor al entrar en una función y después localiza el destino de su resultado. Construiremos una definición y un prototipo, distinguiremos un parámetro copiado de la función llamadora y seguiremos las llamadas recursivas hasta un caso base y de vuelta mediante sus valores devueltos.

  2. 2
    EjemploDar a una tarea una entrada con tipo

    Queremos una función que añada un ajuste fijo a una lectura entera.

    int corrected(int reading)
    {
        return reading + 4;
    }
    

    int antes del nombre describe el resultado. int reading declara un parámetro y el cuerpo calcula el valor devuelto. Define esta función fuera de los cuerpos de todas las demás funciones; el C estándar no admite definiciones anidadas.

  3. 3
    EjemploUsar una llamada como expresión

    En int result = corrected(9);, el argumento 9 inicializa el parámetro reading. La expresión de retorno produce 13 y la asignación de la función llamadora guarda ese valor en result.

    La llamada también puede estar dentro de otra expresión: 2 * corrected(9) da 26. Escribir solo corrected(9); también ejecuta la llamada, pero descarta el valor devuelto.

  4. 4
    ConsejoElegir entre devolver y mostrar

    Un cálculo puede devolver un resultado sin mostrarlo. Esto permite que una función llamadora lo guarde, lo compare o lo muestre más tarde.

    Una función declarada void puede realizar una tarea de salida, pero no proporciona ningún valor para una asignación. Dentro de ella, return; sale sin un valor. En una función que devuelve un valor, asegúrate de que cada ruta que produce un resultado llegue a un return adecuado; mostrar un número no lo proporciona a la función llamadora.

  5. 5
    DesafíoComprobar la aritmética antes del retorno

    ¿Qué devuelve esta función para average_pair(5, 8)?

    double average_pair(int a, int b)
    {
        return (a + b) / 2;
    }
    

    ---reveal---

    Devuelve 6.0: la suma entera da 13, la división entera da 6 y la conversión al tipo de retorno da 6.0. Usa 2.0 para que la división sea de coma flotante si necesitas 6.5. El tipo de retorno no puede recuperar una fracción ya descartada.

  6. 6
    ExplicaciónInformar al compilador antes de la llamada

    Cuando el cuerpo aparece más adelante, coloca int corrected(int reading); antes de la llamada. Este prototipo permite al compilador comprobar el número de argumentos y las conversiones. Su punto y coma indica una declaración; la definición proporciona el cuerpo.

  7. 7
    Idea claveEspecificar una lista de entradas vacía

    Usa int read_sensor(void); para indicar que no hay parámetros tanto en C17 como en C23. En C17, int read_sensor(); deja sin especificar la información sobre los parámetros; C23 cambia esa forma para que signifique que no hay ninguno. Selecciona C17 explícitamente con -std=c17.

  8. 8
    DesafíoSeguir una conversión permitida

    El compilador ha visto int corrected(int reading);. ¿Qué devuelve corrected(6.8) y debe rechazarse la llamada?

    ---reveal---

    La llamada puede compilarse. El prototipo convierte 6.8 en el entero 6, después la función suma 4 y devuelve 10. Una advertencia de conversión puede ayudar a detectar la pérdida de información; usa -Wconversion. Un prototipo comprueba una llamada usando sus tipos declarados, pero no toda diferencia en el tipo de un argumento numérico es un error.

  9. 9
    EjemploRepresentar dos valores separados

    Considera otra función:

    void change(int reading)
    {
        reading += 4;
    }
    

    La función llamadora ejecuta int sample = 9; change(sample);. Al entrar, sample y el parámetro contienen cada uno 9 en objetos separados. El cuerpo cambia su parámetro a 13. Cuando la llamada retorna, sample sigue conteniendo 9: no hubo ninguna asignación a esa variable.

  10. 10
    ConexiónDejar que la función llamadora conserve un resultado

    Usa sample = corrected(sample); para conservar el resultado. La función calcula a partir de una copia; después, la función llamadora guarda el valor devuelto. La coincidencia de nombres no conectaría sus objetos: la asignación actualiza sample.

  11. 11
    DesafíoEncontrar la llamada cuyo resultado se descarta

    Partiendo de int sample = 7;, la función llamadora ejecuta:

    corrected(sample);
    sample = corrected(sample);
    

    ¿Qué valor tiene sample después?

    ---reveal---

    La primera llamada calcula 11 y lo descarta, dejando sample en 7. La segunda también recibe 7, devuelve 11 y esta vez la función llamadora lo guarda. El valor final es 11, no 15; una llamada no actualiza su variable de argumento solo por devolver un valor modificado.

  12. 12
    ExplicaciónAñadir una regla de parada para la recursión

    Una función recursiva se llama a sí misma con un problema más pequeño:

    int total_to(int n)
    {
        if (n <= 0) return 0;
        return n + total_to(n - 1);
    }
    

    Para una entrada positiva, restar uno acerca al caso base. El caso base responde directamente. El caso recursivo no puede terminar su suma hasta que retorne la llamada más pequeña.

  13. 13
    EjemploEscribir las llamadas en la ida

    Sigue total_to(3). La primera llamada tiene n == 3 y espera para sumar 3 a total_to(2). Esa llamada tiene su propia n == 2 y espera a total_to(1).

    La llamada con n == 1 espera a total_to(0), que devuelve 0 de inmediato. En ese punto más profundo hay cuatro llamadas activas. No sustituyas los valores anteriores de n por cero; pertenecen a llamadas distintas.

  14. 14
    EjemploAnotar los resultados en la vuelta

    Una vez que total_to(0) devuelve 0, el trabajo pendiente puede terminar:

    • La llamada con n == 1 devuelve 1 + 0, es decir, 1.
    • La llamada con n == 2 devuelve 2 + 1, es decir, 3.
    • La llamada con n == 3 devuelve 3 + 3, es decir, 6.

    La mayoría de las implementaciones mantienen estas llamadas activas en marcos de pila. Cada retorno reanuda la función llamadora después de su llamada y después libera el estado automático de la llamada que terminó.

  15. 15
    DesafíoComprobar si un caso base es alcanzable

    Supón que la comprobación del caso base en total_to se cambia a n == 0 y que el paso recursivo sigue usando n - 1. ¿Qué falla cuando el argumento inicial es -2?

    ---reveal---

    Las entradas pasan a ser -2, -3, -4 y siguen alejándose de cero. La comprobación de parada existe, pero es inalcanzable por esa ruta. La recursión continuada puede agotar el espacio de la pila. La versión con n <= 0 detiene esta entrada de inmediato.

  16. 16
    ConsejoComprobar también el tamaño de la respuesta

    Un cálculo recursivo que termina también puede superar su tipo de retorno. Con un int de 32 bits, factorial(12) cabe, mientras que el valor matemático de factorial(13) supera el máximo 2147483647.

    El desbordamiento con signo es comportamiento indefinido. Antes de elegir entradas recursivas, comprueba tanto que las llamadas se detengan como que las operaciones intermedias y finales quepan en los tipos que usas. Un bucle también necesita la comprobación aritmética.

  17. 17
    ConsejoSeparar las llamadas cuando importa el orden

    Todas las expresiones de los argumentos terminan antes de que comience el cuerpo de la función llamada, pero C no especifica su orden de evaluación. Si dos llamadas modifican el estado compartido, calcula sus resultados en sentencias separadas antes de pasarlos a otra función.

    Evita f(i, i++): lee y modifica i sin la secuenciación necesaria. El paso por valor no convierte en segura una expresión de argumento insegura.

  18. 18
    DesafíoCombinar declaración, conversión y llamadas

    Con total_to declarada antes de su uso, una función llamadora escribe int x = 2; total_to(x); x = total_to(3.9);. ¿Cuál es el valor final de x?

    ---reveal---

    El primer resultado se descarta, por lo que x sigue siendo 2. El segundo argumento se convierte en 3; la traza recursiva devuelve 6, que la asignación guarda en x. La declaración controla la conversión, la recursión calcula el valor y la función llamadora decide si lo conserva.

  19. 19
    RepasoUsar tres puntos de comprobación para una llamada

    Para una llamada, comprueba su declaración, sigue sus entradas copiadas y localiza el uso de su valor de retorno. Para la recursión, sigue cada llamada activa por separado y combina los resultados hacia fuera desde el caso base.

2

Seguir nombres y valores conservados

13 pasos
Sigue un bloque, un contador estático y un total compartido sin confundir sus objetos.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónComenzar con una tarea pequeña
  2. 2
    EjemploLeer la declaración visible más cercana
  3. 3
    ExplicaciónMantener el ámbito donde se escribió la función
  4. 4
    DesafíoCorregir un valor necesario después de un bloque
  5. 5
    EjemploObservar cómo se reinicia un contador automático
  6. 6
    EjemploConservar el contador con static
  7. 7
    DesafíoDistinguir inicialización y asignación
  8. 8
    ConsejoInterpretar la palabra clave según su ubicación
  9. 9
    EjemploSeguir una actualización global
  10. 10
    DesafíoEncontrar la variable local que oculta la global
  11. 11
    ConexiónFacilitar la prueba de un cálculo
  12. 12
    DesafíoCombinar ámbito y estado conservado
  13. 13
    RepasoAnotar nombres y tiempos de vida por separado
3

Examinar el código antes de la compilación

13 pasos
Expande expresiones, selecciona una rama de depuración y sigue una guarda de cabecera.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónComenzar con una tarea pequeña
  2. 2
    ExplicaciónVer qué proporciona realmente la inclusión
  3. 3
    EjemploSustituir tokens antes de aplicar las reglas de operadores
  4. 4
    EjemploProteger la agrupación interior y exterior
  5. 5
    DesafíoEncontrar lo que los paréntesis no pueden corregir
  6. 6
    ConsejoMantener separadas las reglas de macros y de ámbito
  7. 7
    EjemploComprobar una configuración por existencia y por valor
  8. 8
    DesafíoSeleccionar una rama mediante una opción del compilador
  9. 9
    EjemploSeguir la primera inclusión de una cabecera
  10. 10
    Idea claveComenzar con un estado nuevo de la guarda en otra unidad
  11. 11
    ConexiónLocalizar un diagnóstico en el código fuente
  12. 12
    DesafíoCombinar expansión y selección de ramas
  13. 13
    RepasoExaminar la transformación en orden
4

Construir un programa a partir de dos archivos fuente

14 pasos
Crea una cabecera y una implementación, enlaza sus archivos objeto y protege el estado privado.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónComenzar con una tarea pequeña
  2. 2
    ExplicaciónSeguir una unidad de traducción hasta un archivo objeto
  3. 3
    EjemploEscribir la declaración compartida
  4. 4
    EjemploPoner la implementación en su propio archivo fuente
  5. 5
    DesafíoColocar correctamente una variable compartida
  6. 6
    EjemploCompilar por separado y después enlazar
  7. 7
    DesafíoCorregir una referencia no definida
  8. 8
    ConsejoNo copiar la implementación en la función llamadora
  9. 9
    EjemploMantener un contador dentro del módulo
  10. 10
    ExplicaciónSeparar tres propiedades del contador
  11. 11
    DesafíoExplicar dos contadores independientes
  12. 12
    ConsejoHacer que una cabecera funcione por sí sola
  13. 13
    DesafíoCorregir la privacidad y la interfaz conjuntamente
  14. 14
    RepasoRelacionar cada necesidad con su etapa de construcción
5

Construir solo lo que ha cambiado

15 pasos
Escribe dependencias, sigue las reconstrucciones y abrevia un archivo makefile sin perder su significado.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónComenzar con una tarea pequeña
  2. 2
    EjemploRegistrar las entradas del ejecutable y los archivos objeto
  3. 3
    EjemploDar una receta a un archivo objeto
  4. 4
    EjemploRealizar la primera construcción y reconstruir tras un cambio de fuente
  5. 5
    DesafíoSeguir una modificación de cabecera compartida
  6. 6
    ConsejoExaminar el plan antes de ejecutarlo
  7. 7
    ExplicaciónSustituir la configuración repetida por variables
  8. 8
    EjemploExpandir la receta de enlazado
  9. 9
    EjemploUsar un patrón de compilación para ambos archivos objeto
  10. 10
    DesafíoElegir un archivo fuente sin compilar la cabecera
  11. 11
    ConsejoHacer de la limpieza una acción
  12. 12
    ConexiónDejar que el compilador registre las dependencias de cabeceras
  13. 13
    DesafíoMantener correcta una reconstrucción paralela
  14. 14
    DesafíoAplicar las opciones modificadas a todo el programa
  15. 15
    RepasoComprobar el grafo antes de confiar en el resultado

Ejercicios de Funciones y estructura de programas en C

50 Preguntas de Práctica en 5 sets

Set 1: Seguir los valores a través de llamadas a funciones10 Preguntas de Práctica

Pregunta 1 de 10

¿Cuál es el parámetro en int triple(int value) { return value * 3; }?
  1. triple
  2. int
  3. value
  4. value * 3
Mostrar respuestaOcultar respuesta
Respuesta: C
El parámetro es value, la variable inicializada a partir del argumento proporcionado. triple da nombre a la función; value * 3 es la expresión devuelta, no un parámetro.

Pregunta 2 de 10

¿Qué permite hacer a una función llamadora void report(int code)?
  1. Guardar report(4) en un int.
  2. Llamar a report(4); como sentencia.
  3. Sumar report(4) a un entero.
  4. Devolver report(4) desde una función int.
Mostrar respuestaOcultar respuesta
Respuesta: B
Una llamada void puede realizar su tarea como sentencia, pero no proporciona ningún resultado. El parámetro int code describe una entrada, por lo que no hace que la llamada pueda usarse en una asignación a un entero.

Pregunta 3 de 10

¿Qué valor devuelve double portion(int total) { return total / 4; } para portion(11)?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Mostrar respuestaOcultar respuesta
Respuesta: A
Ambos operandos de total / 4 son enteros, por lo que el cuerpo calcula 11 / 4 como 2. Ese resultado se convierte después a double, lo que da 2.0. Un tipo de retorno double no hace que la división anterior sea de coma flotante; 2.75 requeriría un operando de coma flotante.

Pregunta 4 de 10

Una definición de int capacity(void) aparece después de main. ¿Qué declaración antes de main describe por completo una llamada sin entradas en C17?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Mostrar respuestaOcultar respuesta
Respuesta: D
int capacity(void); declara explícitamente un resultado entero y ningún parámetro. En C17, int capacity(); deja sin especificar la información sobre los parámetros en lugar de describir por completo la llamada sin entradas.

Pregunta 5 de 10

Con int fifth(int n) { return n / 5; } declarada antes de su uso, ¿cuánto vale fifth(-14.8)?
  1. -3
  2. -2
  3. -2.96
  4. -14
Mostrar respuestaOcultar respuesta
Respuesta: B
La conversión al tipo del parámetro descarta primero la fracción hacia cero, lo que da n == -14. La división entera entre 5 da después -2. Elegir -3 supondría redondear hacia abajo en lugar de aplicar la regla de C hacia cero para estas conversiones y la división entera.

Pregunta 6 de 10

Una cabecera declara double stretch(int n);. ¿Qué definición es compatible con esa declaración?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
Mostrar respuestaOcultar respuesta
Respuesta: D
El tipo de retorno debe seguir siendo double y el tipo del parámetro debe seguir siendo int; el nombre del parámetro puede cambiar. Mantener el nombre n mientras se cambia su tipo a double sigue siendo incompatible con el prototipo. La compatibilidad depende de los tipos declarados y de la lista de parámetros, no de que coincidan los nombres de los parámetros.

Pregunta 7 de 10

¿Qué muestra el siguiente programa completo?

#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
    int level = 17;
    lower(level);
    printf("%d\n", level);
}
  1. 17
  2. 13
  3. 4
  4. 0
Mostrar respuestaOcultar respuesta
Respuesta: A
La llamada copia 17 en el parámetro n y la resta modifica solo ese parámetro. level en main sigue conteniendo 17 cuando se muestra. El resultado tentador 13 requeriría asignar un resultado devuelto a level u otra operación que realmente modificara el objeto de la función llamadora.

Pregunta 8 de 10

¿Cuál es el valor final de x?

int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
  1. 22
  2. 28
  3. 16
  4. 14
Mostrar respuestaOcultar respuesta
Respuesta: C
La primera llamada calcula 14, pero lo descarta y deja la x de la función llamadora en 4. La siguiente llamada recibe 5, aumenta su variable local a 8 y devuelve 16, que se asigna en la función llamadora. Un resultado de 22 conserva incorrectamente la actualización local de la primera llamada como si hubiera cambiado la variable de la función llamadora antes de la segunda.

Pregunta 9 de 10

¿Qué devuelve depth(3)?

int depth(int n) {
    if (n == 0) return 2;
    return 1 + depth(n - 1);
}
  1. 3
  2. 4
  3. 5
  4. 6
Mostrar respuestaOcultar respuesta
Respuesta: C
Las llamadas descienden de 3 a 0, donde el caso base devuelve 2. Cada una de las tres llamadas que esperan suma uno, lo que produce 3, 4 y después 5. Un resultado de 3 cuenta los pasos recursivos, pero pierde el valor proporcionado por el caso base.

Pregunta 10 de 10

¿Qué valor guarda int answer = collect(4.9);?

int collect(int n) {
    if (n <= 1) return 1;
    return n + collect(n - 2);
}
  1. 7
  2. 8
  3. 6
  4. 5
Mostrar respuestaOcultar respuesta
Respuesta: A
El parámetro int recibe 4, por lo que las llamadas activas usan 4, 2 y 0. La última devuelve 1; las llamadas anteriores devuelven 2 + 1 == 3 y 4 + 3 == 7. Obtener 6 supone incorrectamente que la llamada con entrada cero devuelve cero, aunque el caso base real devuelve uno.
Set 2: Seguir nombres locales y estado conservado10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Dónde puede usarse el nombre i de for (int i = 0; i < 6; i++) { }?
  1. En toda la función que contiene el bucle.
  2. En todo el resto del archivo fuente.
  3. En cada función llamada por el bucle.
  4. En esa sentencia for.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

¿Qué valores muestra este programa?

#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
    int size = 4;
    show();
    printf("%d", size);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

¿Qué se muestra?

int total = 0;
for (int i = 1; i <= 3; i++) {
    int total = i;
    total += 2;
}
printf("%d", total);
  1. 6
  2. 0
  3. 12
  4. 5
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

¿Por qué int amount = amount + 2; dentro de un bloque no se inicializa a partir de una amount exterior?
  1. El nombre exterior pierde su valor almacenado.
  2. Los inicializadores se ejecutan antes del ámbito.
  3. El nuevo nombre está en ámbito en su inicializador.
  4. Ambas declaraciones identifican el mismo objeto local.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

¿Cuántas veces se inicializa static int calls = 0; durante una ejecución del programa?
  1. Una vez, antes de iniciar el programa.
  2. Una vez por llamada a su función.
  3. Una vez en cada nivel de recursión.
  4. Una vez cada vez que se lee.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

¿Qué valores devuelven tres llamadas separadas a tick?

int tick(void) {
    static int n = 5;
    n += 2;
    return n;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

¿Cuál es el valor de result después de int result = visit(2);?

int visit(int n) {
    static int calls = 0;
    calls++;
    if (n == 0) return calls;
    int inner = visit(n - 1);
    return inner + calls;
}
  1. 6
  2. 9
  3. 7
  4. 3
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un cálculo int permitted(int n) lee una variable global limit. ¿Por qué podría producir el mismo argumento un resultado diferente en una llamada posterior?
  1. Pasar un argumento int cambia su variable de origen.
  2. Cada llamada restablece a cero los objetos de archivo.
  3. Un nombre local de la función llamadora sustituye al global.
  4. Otra operación puede haber modificado limit.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

¿Qué dos enteros se muestran?

#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
    int count = 2;
    count = advance(count);
    int next = advance(1);
    printf("%d %d", count, next);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Una función necesita inicializar una variable numérica conservada con read_sensor() en tiempo de ejecución. ¿Por qué static int first = read_sensor(); no es válido en C17?
  1. Una variable local estática no puede leerse tras inicializarse.
  2. Un inicializador numérico estático debe ser constante.
  3. Una función no puede devolver un valor a una variable local.
  4. Una variable local estática se inicializa en cada llamada.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 3: Expandir macros y elegir ramas en tiempo de construcción10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Qué texto sustituye #define LIMIT 18 en el código fuente posterior?
  1. Los caracteres LIMIT dentro de "LIMIT".
  2. El token independiente LIMIT.
  3. El sufijo del nombre LOCAL_LIMIT.
  4. La sentencia completa que contiene LIMIT.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

¿Cuál es el valor de 5 * INC(3)?

#define INC(x) x + 1
  1. 16
  2. 20
  3. 19
  4. 15
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

¿Por qué #define LIMIT 12; hace que int count = LIMIT * 2; no sea válido?
  1. La macro no puede sustituir un token después de =.
  2. Una macro numérica necesita un tipo de retorno declarado.
  3. El punto y coma pasa a formar parte de la expresión.
  4. La macro convierte count en un nombre de constante.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Después de este fragmento, ¿cuáles son los valores de a y b?

#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

¿Cuál es el comportamiento de CUBE(i++) con esta definición?

#define CUBE(x) ((x) * (x) * (x))
  1. Evalúa el argumento una vez y eleva ese valor al cubo.
  2. Incrementa tres veces en un orden especificado de izquierda a derecha.
  3. Se rechaza porque una macro no puede recibir expresiones.
  4. Tiene comportamiento indefinido por cambios repetidos no secuenciados.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Con #define TRACE 0, ¿qué hace #ifdef TRACE?
  1. Omite su rama porque TRACE se expande como cero.
  2. Conserva su rama porque TRACE está definido.
  3. Rechaza la definición porque cero no es un nombre.
  4. Lee una variable de ejecución llamada TRACE.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

¿Por qué #if VERBOSE podría omitir una rama sin avisar cuando la construcción solo define VERB0SE?
  1. El identificador no definido VERBOSE cuenta como cero.
  2. Los dos nombres comparten una definición porque se parecen.
  3. Todo nombre no definido del preprocesador causa un error fatal.
  4. La directiva espera un valor de VERBOSE en tiempo de ejecución.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Con -DLEVEL=2, ¿qué valor llega al compilador para result?

#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
  1. 6
  2. 7
  3. 8
  4. 2
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

¿Qué texto muestra este programa completo?

#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
    int item2 = 7;
    PRINT(JOIN(item, 2));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Una cabecera con guarda contiene int mode = 2;. Dos archivos fuente separados la incluyen, se compilan y después se enlazan. ¿Por qué la guarda no impide un error de definición duplicada?
  1. El segundo archivo reutiliza el estado de la guarda del primero.
  2. La guarda convierte ambas definiciones en declaraciones compartidas.
  3. Cada unidad de traducción incluye su propia definición externa.
  4. El enlazador elimina los nombres de guarda antes de buscar duplicados.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 4: Diagnosticar cabeceras, archivos objeto y vinculación10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Qué produce normalmente gcc -std=c17 -c driver.c en un sistema tipo Unix?
  1. Un archivo objeto llamado driver.o.
  2. Un ejecutable llamado driver.
  3. Texto ensamblador llamado driver.s.
  4. Un archivo de cabecera llamado driver.h.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Una función llamadora incluye un prototipo correcto para reading, pero al enlazar solo su archivo objeto se informa de una referencia no definida a reading. ¿Qué debe añadirse?
  1. Otro prototipo compatible en la función llamadora.
  2. Una guarda alrededor de la declaración de función existente.
  3. El archivo objeto que define reading en las entradas de enlazado.
  4. La misma cabecera como entrada compilada por separado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Una cabecera declara int classify(double x);, pero su archivo fuente define double classify(double x). ¿Qué hace visible la discrepancia durante la compilación del archivo fuente que contiene la definición?
  1. Añadir el archivo que define la función a un comando de enlazado posterior.
  2. Poner un segundo prototipo solo en el archivo que realiza la llamada.
  3. Añadir una guarda de inclusión solo alrededor del código que realiza la llamada.
  4. Incluir esa misma cabecera en el archivo que define la función.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

¿Qué vinculación tiene static int cached = 3; cuando se declara fuera de todas las funciones?
  1. Vinculación externa.
  2. Vinculación interna.
  3. Sin vinculación.
  4. Vinculación automática.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Una cabecera con guarda tiene static int errors; y void record(void);. Un archivo fuente la incluye y define record para incrementar errors; otro la incluye, llama dos veces a record y después muestra su propia errors. ¿Qué se muestra y por qué?
  1. 2, porque ambas inclusiones nombran el mismo contador compartido.
  2. 1, porque la guarda permite solo una llamada correcta.
  3. 0, porque los dos archivos tienen contadores independientes.
  4. Un error de enlazado, porque ambos contadores estáticos son externos.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

¿Qué debe ir en una cabecera para permitir que varios archivos fuente usen una sola variable int failures definida en un único archivo fuente?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

main.c incluye "calc.c"; calc.c incluye una cabecera calc.h con guarda y define la función pública int calc(int n). La construcción gcc main.c calc.c -o run informa de dos definiciones de calc. ¿Qué cambio conserva la compilación separada y comprueba las llamadas correctamente?
  1. Incluir calc.h en main.c y mantener ambos archivos fuente en la construcción.
  2. Quitar calc.c de la construcción y mantener su inclusión en main.c.
  3. Mantener ambos archivos fuente y añadir otra guarda de inclusión a calc.h.
  4. Incluir main.c dentro de calc.c para que coincidan ambas definiciones.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un archivo fuente define static int quota = 9; e int quota_value(void) { return quota; }. Otro incluye una cabecera que declara tanto extern int quota; como int quota_value(void); y después usa ambos nombres. ¿Cuál es la corrección adecuada de la interfaz para mantener privada la cuota almacenada?
  1. Quitar static de la variable y mantener todos los usos de quota.
  2. Añadir static int quota; a la cabecera y mantener las lecturas directas.
  3. Repetir extern int quota; en la función llamadora antes de cada uso.
  4. Quitar la declaración de la variable externa y usar quota_value() para leerla.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Una cabecera declara size_t packet_count(void);. ¿Qué hace que la cabecera sea autosuficiente?
  1. Pedir a cada función llamadora que incluya <stddef.h> después de la cabecera.
  2. Trasladar la declaración a cada archivo fuente con una copia del nombre del tipo.
  3. Incluir el archivo fuente de la implementación antes de la cabecera en las funciones llamadoras.
  4. Incluir <stddef.h> antes de esa declaración en la cabecera.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Dos archivos fuente incluyen cada uno una cabecera con extern int jobs = 1;. Uno incrementa jobs y el otro lo muestra. ¿Qué corrección crea un único objeto compartido y mantiene la comprobación de tipos en ambos archivos fuente?
  1. Usar extern int jobs; en la cabecera y una definición en un archivo fuente.
  2. Usar static int jobs = 1; en la cabecera y ninguna definición en archivos fuente.
  3. Mantener la cabecera sin cambios y protegerla con un nuevo nombre de macro.
  4. Eliminar la cabecera e inicializar jobs en ambos archivos fuente.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 5: Planificar construcciones incrementales fiables10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Qué hace que un objetivo ordinario de make existente necesite reconstruirse después de actualizar sus prerrequisitos?
  1. Su archivo fuente tiene más líneas que antes.
  2. Un prerrequisito tiene una fecha de modificación más reciente.
  3. Su receta contiene más de un comando.
  4. El mismo comando se ejecutó un día anterior.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

El ejecutable viewer depende de main.o y screen.o; cada archivo objeto depende de su archivo fuente correspondiente. Solo screen.c pasa a ser más reciente que su archivo objeto y todas las demás entradas permanecen sin cambios. ¿Qué recetas se necesitan?
  1. Compilar main.c y después enlazar viewer.
  2. Compilar ambos archivos fuente y después enlazar viewer.
  3. Enlazar viewer sin compilar ninguno de los archivos fuente.
  4. Compilar screen.c y después enlazar viewer.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un archivo fuente incluye settings.h, pero la regla de su archivo objeto indica solo el archivo fuente. ¿Por qué modificar la cabecera puede dejar el programa sin cambios después de ejecutar make?
  1. Make no tiene una relación declarada entre la cabecera y el archivo objeto.
  2. Las cabeceras siempre se procesan solo durante la ejecución del programa.
  3. El enlazador restaura automáticamente el contenido anterior de la cabecera.
  4. La inclusión en el archivo fuente impide que su archivo objeto dependa de cabeceras.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Para la regla de enlazado tool: main.o engine.o, ¿en qué se expanden $@ y $^?
  1. main.o y engine.o
  2. tool y main.o
  3. tool y main.o engine.o
  4. main.o engine.o y tool
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Para la siguiente regla, ¿qué entradas y salida del compilador se usan cuando make construye sensor.o? Supón que CPPFLAGS = -DTRACE=1 y CFLAGS = -std=c17.

%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
  1. Fuente sensor.c, salida sensor.o, con -DTRACE=1 -std=c17.
  2. Fuentes sensor.c shared.h, salida sensor.o, con -DTRACE=1 -std=c17.
  3. Fuente sensor.o, salida sensor.c, con -DTRACE=1 -std=c17.
  4. Fuente sensor.c, salida shared.h, con -DTRACE=1 -std=c17.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una construcción reciente tiene el orden de fechas de modificación shared.h < main.o < helper.o < app. Las reglas son app: main.o helper.o, main.o: main.c shared.h y helper.o: helper.c. Después se modifica solo shared.h. Ambos archivos fuente lo incluyen. ¿Qué ocurre y qué está mal?
  1. Se reconstruyen ambos archivos objeto antes de enlazar; el grafo de dependencias está completo.
  2. Solo se reconstruye helper.o antes de enlazar; falta una dependencia de main.o.
  3. No se reconstruye ningún archivo objeto; las cabeceras no pueden activar sus recetas.
  4. Solo se reconstruye main.o antes de enlazar; falta una dependencia de helper.o.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

¿Por qué un archivo llamado clean puede impedir que una regla clean: sin marcar y sin prerrequisitos elimine archivos objeto?
  1. Make reserva todo objetivo llamado clean para la compilación predefinida.
  2. El intérprete rechaza comandos de objetivos que tienen archivos coincidentes.
  3. Make puede considerar que ese objetivo existente ya está actualizado.
  4. El compilador elimina las recetas de limpieza cuando se activa la optimización.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un proyecto carga archivos .d generados después de su regla de ejecutable. main.d indica main.o: main.c a.h; worker.d indica worker.o: worker.c b.h a.h. Todas las salidas están actualizadas y después se modifica solo a.h. ¿Qué debería mostrar make -n?
  1. Compilar solo main.c y después enlazar; no ejecutar ninguna de esas recetas.
  2. Compilar ambos archivos fuente y después enlazar; no ejecutar ninguna de esas recetas.
  3. Compilar ambos archivos fuente y después enlazar; ejecutar las tres recetas.
  4. Solo enlazar; no ejecutar recetas porque los archivos de dependencias ya existen.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un proyecto tiene app: main.o work.o, main.o: main.c shared.h y work.o: work.c. Todas las salidas están actualizadas. CFLAGS cambia de -O0 a -O2 y después se modifica solo shared.h. Ambas recetas usan CFLAGS. ¿Qué hace make de forma ordinaria y qué garantiza que las nuevas opciones se apliquen de forma coherente?
  1. Reconstruye ambos archivos objeto y enlaza; no hace falta otra reconstrucción para las nuevas opciones.
  2. Reconstruye main.o y enlaza; fuerza una reconstrucción completa para actualizar también work.o.
  3. Solo vuelve a enlazar y conserva ambos archivos objeto; cambia otra vez la cabecera para aplicar las opciones.
  4. Reconstruye work.o y enlaza; fuerza una reconstrucción completa para actualizar también main.o.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

app depende de main.o y codec.o. Cada archivo objeto tiene una regla completa de dependencias de archivos fuente y cabeceras; solo codec.h es compartida por ambos archivos fuente. Después de modificar esa cabecera, ¿por qué make -j4 app puede compilar de forma simultánea y aun así enlazar correctamente?
  1. El enlazado puede comenzar cuando termine cualquiera de los archivos objeto y reutilizar el otro antiguo.
  2. Cada archivo objeto espera al otro y después el enlazado comienza con ambos archivos antiguos.
  3. Ambas recetas de archivos objeto pueden ejecutarse a la vez; el enlazado espera a que terminen las dos.
  4. El enlazado se ejecuta primero para reservar app y después ambas recetas de archivos objeto lo sustituyen.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

¿Te atascas con una pregunta tuya? Ayuda con Tareas

Términos Clave

68 Términos Clave, cada uno con su definición

A

Archivo de cabecera
Un archivo de cabecera comparte declaraciones, macros o tipos mediante inclusión textual.
Una cabecera de conversión declara ambos sentidos de la conversión sin proporcionar sus cuerpos compilados.
Archivo de dependencias generado
Un archivo de dependencias generado registra los archivos fuente y las cabeceras que el compilador ha detectado como prerrequisitos para que make los cargue en construcciones posteriores.
Compilar con -MMD puede escribir sensor.d, que hace que una modificación posterior de una cabecera reconstruya sensor.o.
Archivo objeto
Un archivo objeto contiene código máquina compilado e información de símbolos que el enlazador puede combinar con otros archivos objeto.
Después de modificar solo sensor.c, la construcción puede sustituir sensor.o y reutilizar un main.o sin cambios.
Argumento
Un argumento es una expresión proporcionada por una llamada para dar a un parámetro su valor inicial.
En cube(5 + 1), el argumento se evalúa como 6 antes de que comience el cuerpo de cube.
auto
La palabra clave auto indica explícitamente almacenamiento automático en C17; C23 también la usa para inferir un tipo a partir de un inicializador.
En C17, auto int level; tiene el mismo comportamiento de almacenamiento que una variable local ordinaria int level;.

Á

Ámbito (alcance)
El ámbito de un nombre es la región del código fuente donde su declaración es visible.
Una variable puede seguir existiendo mientras su nombre está oculto por otra declaración en un bloque anidado.
Ámbito de archivo
El ámbito de archivo se extiende desde una declaración fuera de las funciones hasta el final de la unidad de traducción.
Una función auxiliar definida antes de una variable global necesita una declaración previa para poder usarla.
Ámbito de bloque
El ámbito de bloque limita un nombre local a la región que va desde justo después de su declarador hasta el final del bloque que lo contiene.
Una variable declarada dentro de un bloque if no puede nombrarse en una impresión después de que ese bloque se cierre.
Ámbito de prototipo
El ámbito de prototipo limita el nombre de un parámetro al prototipo de función que lo declara.
Un prototipo puede llamar value a su parámetro mientras que la definición llama n al mismo parámetro.

C

Cabecera autosuficiente
Una cabecera autosuficiente incluye las cabeceras que necesitan sus propias declaraciones, lo que hace innecesario un orden de inclusión especial.
Una cabecera que declara una función con size_t incluye <stddef.h> antes de esa declaración.
Caso base
Un caso base responde directamente a una entrada sin realizar otra llamada recursiva.
El caso base del factorial devuelve 1, lo que permite que terminen las multiplicaciones pendientes de las llamadas anteriores.
Caso recursivo
Un caso recursivo es una rama que resuelve parte de un problema llamando a la misma función con una entrada más cercana a un caso base.
Restar uno a una entrada positiva del factorial acerca el caso recursivo a su comprobación de parada.
Compilación condicional
La compilación condicional conserva u omite ramas del código fuente según comprobaciones del preprocesamiento antes de compilar el código resultante.
Una impresión de depuración eliminada mediante compilación condicional está ausente del ejecutable resultante.
Concatenación de tokens
La concatenación de tokens usa ## en una lista de sustitución de macro para combinar tokens vecinos en un único token de preprocesamiento.
Una llamada a una macro que une temp y 2 puede formar el identificador temp2 en el código fuente expandido.
Conversión de argumentos
Una conversión de argumentos cambia el valor de un argumento proporcionado al tipo del parámetro especificado por un prototipo de función visible.
Pasar 5.8 a un parámetro int lo convierte en 5; no lo redondea al entero más cercano.
Conversión en literal de cadena
La conversión en literal de cadena usa # antes de un parámetro de macro en el texto de sustitución para convertir los tokens del argumento proporcionado en un literal de cadena.
Una macro de diagnóstico puede convertir 3 + 4 en una cadena para mostrar la expresión junto con su valor calculado.

D

Declaración
Una declaración introduce un nombre y la información de tipos necesaria para comprobar sus usos.
Una declaración antes de main permite comprobar una llamada incluso cuando el cuerpo de la función aparece más adelante.
Definición de función
Una definición de función proporciona el cuerpo ejecutable de una función junto con su nombre, tipo de retorno y lista de parámetros.
La definición de double scale(double factor) debe coincidir con el prototipo incluido tanto en la función llamadora como en la implementación.
Definición múltiple (multiple definition)
Un error de definición múltiple ocurre cuando el enlazado recibe definiciones incompatibles del mismo nombre con vinculación externa.
Dos archivos fuente enlazados que definen ambos main causan un error de definición múltiple o de símbolo duplicado.
Definición tentativa
Una definición tentativa es una declaración de objeto de ámbito de archivo sin inicializador ni extern que puede proporcionar una definición.
Poner int count; en una cabecera compartida no es un sustituto portable de una declaración extern.
Dependencia oculta
Una dependencia oculta es una entrada o un requisito de estado que afecta a una función sin aparecer en su lista de parámetros.
Un cálculo que lee un límite global necesita que ese límite se prepare antes de poder probar la función.
Directiva de preprocesamiento
Una directiva de preprocesamiento comienza con # y controla la transformación del texto fuente.
La directiva #include "sensor.h" inserta el texto de la cabecera en el preprocesamiento actual.
Duración automática del almacenamiento
La duración automática del almacenamiento proporciona a las variables locales ordinarias y a los parámetros un almacenamiento ligado a la ejecución de su bloque o a la llamada a la función.
Dos llamadas recursivas activas tienen parámetros automáticos distintos aunque sus nombres coincidan.
Duración del almacenamiento
La duración del almacenamiento describe cuánto tiempo existe el almacenamiento de un objeto durante la ejecución, independientemente de dónde sea visible su nombre.
Una variable local estática combina ámbito de bloque con una duración del almacenamiento que abarca toda la ejecución del programa.
Duración estática del almacenamiento
La duración estática del almacenamiento significa que un objeto existe durante toda la ejecución del programa y se inicializa una vez antes de iniciarlo.
Un objeto numérico de ámbito de archivo sin inicializador explícito comienza en cero y conserva las asignaciones posteriores.

E

Efecto secundario
Un efecto secundario es un cambio en el estado del programa o una interacción como la salida, más allá del simple cálculo del valor de una expresión.
Incrementar un contador compartido es un efecto secundario, por lo que el orden de llamadas separadas al contador puede importar.
Ejecución simulada (dry run)
Una ejecución simulada con make -n muestra las recetas ordinarias de construcción sin ejecutarlas, revelando los comandos previstos del compilador y del enlazador.
Examina una ejecución simulada después de modificar una cabecera para ver qué archivos objeto planea reconstruir make.
Enlazador
Un enlazador combina archivos objeto y bibliotecas y resuelve nombres externos para relacionarlos con sus definiciones.
El enlazador necesita el archivo objeto que define una función de conversión incluso cuando la función llamadora incluyó su prototipo.
Estado compartido
El estado compartido son datos conservados que varias operaciones pueden leer o modificar.
Un recuento de errores compartido hace que un informe posterior dependa de las funciones anteriores que lo actualizaron.
extern
La palabra clave extern puede declarar una entidad con vinculación sin definir un objeto, siempre que ningún inicializador la convierta en definición.
Una cabecera compartida declara extern int errors;, mientras que un archivo fuente proporciona la definición del objeto inicializado.

F

Función
Una función es un bloque de código ejecutable con nombre que realiza una tarea cuando se llama.
Una función de conversión de temperatura permite que varias partes de un programa reutilicen un cálculo probado.
Función llamadora
Una función llamadora es la función que invoca a otra función y reanuda su ejecución después de que esta retorne.
Cuando main llama a read_sensor, main es la función llamadora que espera recibir la lectura devuelta.

G

Guarda de inclusión
Una guarda de inclusión impide el procesamiento repetido de una cabecera dentro de una unidad de traducción.
Dos cabeceras pueden incluir una cabecera común con guarda sin repetir su contenido en la misma unidad.

M

Macro de tipo función
Una macro de tipo función recibe argumentos formados por tokens y los sustituye en un texto de reemplazo antes de la compilación ordinaria de C.
Los paréntesis en una macro de tipo función conservan la agrupación, pero no impiden la evaluación repetida de argumentos.
Macro de tipo objeto
Una macro de tipo objeto asocia un nombre con tokens de sustitución que se insertan cuando ese nombre aparece más adelante.
La definición #define LIMIT 16 sustituye el token LIMIT, pero deja intacto "LIMIT".
Macro predefinida
Una macro predefinida es proporcionada por la implementación y ofrece información como la línea fuente actual o la versión del lenguaje.
Un diagnóstico que usa __FILE__ y __LINE__ puede identificar dónde se origina su mensaje.
Marco de pila
Un marco de pila es un registro de una llamada activa que normalmente contiene sus parámetros, variables locales automáticas y ubicación de retorno.
En el punto más profundo de factorial(4), cuatro llamadas a factorial tienen marcos activos separados.
Módulo
Un módulo agrupa código de implementación de C relacionado en un archivo fuente con su correspondiente cabecera.
Un módulo de contador puede exponer dos funciones públicas mientras mantiene privado su recuento almacenado.

O

Objetivo de make
Un objetivo de make es el archivo de salida o la acción que se solicita a make que actualice.
El objetivo del ejecutable suele colocarse primero para que ejecutar make sin argumentos construya todo el programa.
Objetivo ficticio
Un objetivo ficticio da nombre a una acción declarada con .PHONY, de modo que un archivo existente con ese nombre no pueda impedir la ejecución de su receta.
Declarar clean como ficticio garantiza que la limpieza siga ejecutándose si aparece un archivo llamado clean en el directorio.
Objetivo predeterminado
El objetivo predeterminado es el objetivo que make elige sin un argumento de objetivo explícito, normalmente el primer objetivo ordinario del archivo.
Poner all o el ejecutable final primero impide que una ejecución ordinaria de make se detenga en un solo archivo objeto.
Ocultación de nombres
La ocultación de nombres ocurre cuando una declaración interior oculta un nombre exterior visible, haciendo que los usos en el ámbito interior seleccionen la nueva entidad.
Declarar una segunda variable total en un bucle puede dejar sin cambios el acumulador exterior previsto.
Operador defined
El operador defined del preprocesador da uno si su operando, un nombre de macro, está definido y cero si no lo está.
Usa #if defined(DEBUG) para comprobar la existencia incluso cuando DEBUG tiene el texto de sustitución 0.
Orden de evaluación de argumentos no especificado
El orden de evaluación de argumentos no especificado significa que C no exige un orden determinado para evaluar las expresiones de los argumentos de una llamada.
Dos llamadas que generan números de turno en una misma impresión pueden proporcionar sus números en cualquiera de los dos órdenes.

P

Parámetro
Un parámetro es una variable de función que se inicializa con una copia del valor de un argumento al entrar en la función.
En int cube(int n), el parámetro n recibe un valor de entrada separado para cada llamada.
Paso por valor
El paso por valor es la regla de C para pasar argumentos: cada parámetro recibe una copia del valor calculado por su expresión de argumento.
Una asignación al parámetro n dentro de una función deja sin cambios la variable entera ordinaria de la función llamadora.
Pila de llamadas
Una pila de llamadas es la estructura habitual de implementación que sigue las llamadas a funciones activas y les permite retornar en orden inverso al de su anidamiento.
Una función de conversión llamada por otra que muestra una tabla retorna antes de que esta última retorne a main.
Preprocesador
El preprocesador transforma el texto fuente mediante directivas como la inclusión, la sustitución de macros y la selección condicional.
Ejecuta gcc -E sample.c para examinar el código expandido antes de que el compilador compruebe las expresiones de C.
Prerrequisito
Un prerrequisito es una entrada o dependencia declarada que make comprueba y actualiza antes de decidir si un objetivo necesita reconstruirse.
Una cabecera compartida es un prerrequisito de cada archivo objeto cuya unidad de traducción incluye esa cabecera.
Prototipo de función
Un prototipo de función es una declaración que especifica el tipo de retorno y los tipos de los parámetros de una función para comprobar las llamadas.
El prototipo double scale(double factor); indica al compilador que convierta su argumento a double.

R

Receta de make
Una receta de make es la secuencia de comandos del intérprete asociada a una regla de make, cuyas líneas ordinarias comienzan con tabulaciones literales.
La receta de un archivo objeto compila su archivo fuente, mientras que la receta del ejecutable enlaza los archivos objeto necesarios.
Recursión
La recursión ocurre cuando una función se invoca a sí misma para resolver otra instancia de un problema.
Una función que suma dígitos puede procesar el último dígito y sumar recursivamente los que quedan.
Referencia no definida (undefined reference)
Una referencia no definida es un fallo de enlazado en el que un nombre externo necesario no tiene una definición disponible en las entradas enlazadas.
Omitir sensor.o del enlazado puede dejar una referencia no definida a una función pública de lectura que solo sensor.o define.
register
La palabra clave register marca una variable local con una sugerencia de optimización que los compiladores pueden ignorar y prohíbe obtener su dirección.
Añadir register a un contador de bucle no obliga a un compilador moderno a usar un registro del procesador.
Regla de make
Una regla de make indica un objetivo, sus prerrequisitos y los comandos de receta necesarios para actualizar ese objetivo a partir de esas entradas.
La regla de main.o indica tanto main.c como las cabeceras del proyecto incluidas durante su compilación.
Regla de patrón
Una regla de patrón usa % para hacer coincidir una raíz compartida de nombres de archivo, de modo que un patrón de dependencias y una receta puedan construir muchos objetivos.
La regla %.o: %.c relaciona sensor.o con sensor.c usando sensor como raíz compartida.

S

Sin vinculación
Sin vinculación significa que un nombre identifica su propia entidad en lugar de conectar declaraciones mediante vinculación interna o externa.
Las variables locales automáticas ordinarias y los parámetros no tienen vinculación incluso cuando otra función usa los mismos nombres.

U

Unidad de traducción
Una unidad de traducción es un archivo fuente después del preprocesamiento, junto con su texto incluido, que se traduce de forma independiente.
Incluir la misma cabecera en dos archivos fuente aporta declaraciones a dos unidades de traducción separadas.

V

Valor de retorno
Un valor de retorno es el resultado que una función proporciona a la función llamadora para usarlo en una expresión.
El valor de retorno de una conversión puede asignarse a double result sin mostrarlo.
Variable automática de make
Una variable automática de make la establece make para la regla actual y proporciona valores como los nombres de su objetivo o sus prerrequisitos.
La variable automática $@ nombra sensor.o mientras se ejecuta su receta de archivo objeto.
Variable de make
Una variable de make da nombre a texto reutilizable, como opciones del compilador o listas de archivos objeto, que se expande mediante una referencia como $(CFLAGS).
Cambiar una sola asignación de CC cambia el texto del compilador que usan las recetas que hacen referencia a $(CC).
Variable global
Una variable global es un objeto declarado en el ámbito de archivo, con duración estática del almacenamiento y visibilidad después de su declaración.
Dos funciones que añaden recorridos pueden actualizar un total global de distancia que permanece disponible para main.
Variable local estática
Una variable local estática es un objeto de ámbito de bloque declarado static que conserva un único valor almacenado entre llamadas repetidas.
Un contador local estático de números de turno avanza en cada llamada en lugar de reiniciarse con su inicializador.
Vinculación
La vinculación determina si las declaraciones de un nombre en distintos lugares se refieren al mismo objeto o función.
Una variable global puede tener duración estática del almacenamiento mientras static limita su vinculación a una unidad de traducción.
Vinculación externa
La vinculación externa permite que declaraciones en distintas unidades de traducción identifiquen el mismo objeto o función.
Una función pública de contador tiene vinculación externa para que las funciones llamadoras de otro archivo fuente puedan enlazar con su definición.
Vinculación interna
La vinculación interna limita la identidad compartida de un nombre a una unidad de traducción y mantiene su entidad privada frente a otras unidades.
Dos archivos pueden definir cada uno una función auxiliar estática llamada check sin conflicto durante el enlazado.
void
La palabra clave void indica que no se devuelve ningún valor cuando es un tipo de retorno, o que no hay parámetros cuando se usa sola en una lista de parámetros.
Una función void de impresión realiza su tarea de salida, pero no puede proporcionar el lado derecho de una asignación.

#

#error
La directiva #error requiere un diagnóstico cuando se alcanza durante el preprocesamiento, lo que permite rechazar una construcción no admitida.
Una comprobación de versión puede seleccionar #error cuando el estándar de C elegido es anterior al que requiere el programa.

Sobre esta unidad

Por StudeliInformar de un error

Crea tus propios Materiales de Estudio

Convierte tus notas, diapositivas y documentos en Guías de Estudio, Preguntas de Práctica, Flashcards y 6 herramientas más.

Comenzar