- Aprender
- Informática
- Programación
- Programación en C
- Funciones y estructura de programas en C
Funciones y estructura de programas en C
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
- 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 resultadoEscribe definiciones de funciones con un tipo de retorno, parámetros y un cuerpo, utiliza
returnyvoidcorrectamente y llama a funciones dentro de expresiones. - Declarar funciones mediante prototiposDeclara 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 resultadoExplica 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ónSigue 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.
- Definir funciones que reciban datos de entrada y devuelvan un resultado
- 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 almacenamientoDistingue 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 cuidadosaDefine 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.
- Determinar dónde puede usarse un nombre (ámbito)
- 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 macrosExplica 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 condicionalUtiliza #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.
- Incluir archivos y definir macros
- 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 fuenteDescribe 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 cabeceraColoca 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 externUtiliza 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.
- Compilar y enlazar varios archivos fuente
- 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 makefileEscribe 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ónUtiliza variables como
CCyCFLAGS, las variables automáticas$@,$<y$^, reglas de patrón, reglas predefinidas y objetivos ficticios para mantener un archivo makefile breve y correcto.
- Escribir reglas y dependencias en archivos makefile
Guía de estudio de Funciones y estructura de programas en C
1Escribir, 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.
returntermina la llamada; mostrar un valor no proporciona su resultado.- Usa
voidpara 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ón | Significado en C17 | Significado en C23 |
|---|---|---|
int read_sensor(void); | Sin parámetros | Sin parámetros |
int read_sensor(); | Información sobre los parámetros no especificada | Sin 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 llamadora | Efecto 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 activa | Operació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.
2Controlar 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
| Especificador | Significado en este código C17 |
|---|---|
auto | Almacenamiento automático explícito para una variable de bloque; normalmente se omite |
register | Una sugerencia para acceso rápido; está prohibido obtener su dirección |
static dentro de una función | Almacenamiento conservado con un nombre local |
static en el ámbito de archivo | Vinculación interna, que mantiene el nombre dentro de su unidad de traducción |
extern | Una 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
staticdetermina 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.
3Utilizar 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
| Nombre | Informació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
#ifdefcomprueba la existencia;#ifcomprueba un valor.- Los identificadores no definidos en
#ifse evalúan como cero. -DNAME=valueproporciona 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.
4Dividir 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.
| Etapa | Resultado | Opción de GCC que se detiene ahí |
|---|---|---|
| Preprocesamiento | Texto C expandido | -E |
| Compilación | Texto ensamblador | -S |
| Ensamblado | Archivo objeto | -c |
| Enlazado | Ejecutable | Construcció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.
-cproduce 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 cabecera | Mantener en un archivo fuente |
|---|---|
| Prototipos de funciones | Cuerpos de funciones con vinculación externa |
Declaraciones de variables extern | Definiciones de objetos compartidos |
| Macros y tipos necesarios | Funciones 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ón | Significado |
|---|---|
| Externa | Las declaraciones pueden referirse a una entidad en distintas unidades de traducción |
| Interna | Las declaraciones se refieren a una entidad dentro de una unidad de traducción |
| Ninguna | Un 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
externy una sola definición. staticen 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
externde ámbito de archivo en una definición. - La conservación con
staticlocal y la vinculación constaticde ámbito de archivo cumplen funciones distintas.
5Construir 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 reutilizamain.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
| Variable | Valor 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
.dgenerados 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
1Construir y seguir una llamada a función
19 pasos
- 1Introducció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.
- 2EjemploDar 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; }intantes del nombre describe el resultado.int readingdeclara 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. - 3EjemploUsar una llamada como expresión
En
int result = corrected(9);, el argumento9inicializa el parámetroreading. La expresión de retorno produce13y la asignación de la función llamadora guarda ese valor enresult.La llamada también puede estar dentro de otra expresión:
2 * corrected(9)da26. Escribir solocorrected(9);también ejecuta la llamada, pero descarta el valor devuelto. - 4ConsejoElegir 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
voidpuede 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 unreturnadecuado; mostrar un número no lo proporciona a la función llamadora. - 5Desafí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 da13, la división entera da6y la conversión al tipo de retorno da6.0. Usa2.0para que la división sea de coma flotante si necesitas6.5. El tipo de retorno no puede recuperar una fracción ya descartada. - 6Explicació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. - 7Idea 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. - 8DesafíoSeguir una conversión permitida
El compilador ha visto
int corrected(int reading);. ¿Qué devuelvecorrected(6.8)y debe rechazarse la llamada?---reveal---
La llamada puede compilarse. El prototipo convierte
6.8en el entero6, después la función suma4y devuelve10. 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. - 9EjemploRepresentar 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,sampley el parámetro contienen cada uno9en objetos separados. El cuerpo cambia su parámetro a13. Cuando la llamada retorna,samplesigue conteniendo9: no hubo ninguna asignación a esa variable. - 10Conexió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 actualizasample. - 11Desafí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
sampledespués?---reveal---
La primera llamada calcula
11y lo descarta, dejandosampleen7. La segunda también recibe7, devuelve11y esta vez la función llamadora lo guarda. El valor final es11, no15; una llamada no actualiza su variable de argumento solo por devolver un valor modificado. - 12Explicació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.
- 13EjemploEscribir las llamadas en la ida
Sigue
total_to(3). La primera llamada tienen == 3y espera para sumar3atotal_to(2). Esa llamada tiene su propian == 2y espera atotal_to(1).La llamada con
n == 1espera atotal_to(0), que devuelve0de inmediato. En ese punto más profundo hay cuatro llamadas activas. No sustituyas los valores anteriores denpor cero; pertenecen a llamadas distintas. - 14EjemploAnotar los resultados en la vuelta
Una vez que
total_to(0)devuelve0, el trabajo pendiente puede terminar:- La llamada con
n == 1devuelve1 + 0, es decir,1. - La llamada con
n == 2devuelve2 + 1, es decir,3. - La llamada con
n == 3devuelve3 + 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ó.
- La llamada con
- 15DesafíoComprobar si un caso base es alcanzable
Supón que la comprobación del caso base en
total_tose cambia an == 0y que el paso recursivo sigue usandon - 1. ¿Qué falla cuando el argumento inicial es-2?---reveal---
Las entradas pasan a ser
-2,-3,-4y 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 conn <= 0detiene esta entrada de inmediato. - 16ConsejoComprobar también el tamaño de la respuesta
Un cálculo recursivo que termina también puede superar su tipo de retorno. Con un
intde 32 bits,factorial(12)cabe, mientras que el valor matemático defactorial(13)supera el máximo2147483647.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.
- 17ConsejoSeparar 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 modificaisin la secuenciación necesaria. El paso por valor no convierte en segura una expresión de argumento insegura. - 18DesafíoCombinar declaración, conversión y llamadas
Con
total_todeclarada antes de su uso, una función llamadora escribeint x = 2; total_to(x); x = total_to(3.9);. ¿Cuál es el valor final dex?---reveal---
El primer resultado se descarta, por lo que
xsigue siendo2. El segundo argumento se convierte en3; la traza recursiva devuelve6, que la asignación guarda enx. La declaración controla la conversión, la recursión calcula el valor y la función llamadora decide si lo conserva. - 19RepasoUsar 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.
2Seguir nombres y valores conservados
13 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónComenzar con una tarea pequeña
- 2EjemploLeer la declaración visible más cercana
- 3ExplicaciónMantener el ámbito donde se escribió la función
- 4DesafíoCorregir un valor necesario después de un bloque
- 5EjemploObservar cómo se reinicia un contador automático
- 6EjemploConservar el contador con static
- 7DesafíoDistinguir inicialización y asignación
- 8ConsejoInterpretar la palabra clave según su ubicación
- 9EjemploSeguir una actualización global
- 10DesafíoEncontrar la variable local que oculta la global
- 11ConexiónFacilitar la prueba de un cálculo
- 12DesafíoCombinar ámbito y estado conservado
- 13RepasoAnotar nombres y tiempos de vida por separado
3Examinar el código antes de la compilación
13 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónComenzar con una tarea pequeña
- 2ExplicaciónVer qué proporciona realmente la inclusión
- 3EjemploSustituir tokens antes de aplicar las reglas de operadores
- 4EjemploProteger la agrupación interior y exterior
- 5DesafíoEncontrar lo que los paréntesis no pueden corregir
- 6ConsejoMantener separadas las reglas de macros y de ámbito
- 7EjemploComprobar una configuración por existencia y por valor
- 8DesafíoSeleccionar una rama mediante una opción del compilador
- 9EjemploSeguir la primera inclusión de una cabecera
- 10Idea claveComenzar con un estado nuevo de la guarda en otra unidad
- 11ConexiónLocalizar un diagnóstico en el código fuente
- 12DesafíoCombinar expansión y selección de ramas
- 13RepasoExaminar la transformación en orden
4Construir un programa a partir de dos archivos fuente
14 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónComenzar con una tarea pequeña
- 2ExplicaciónSeguir una unidad de traducción hasta un archivo objeto
- 3EjemploEscribir la declaración compartida
- 4EjemploPoner la implementación en su propio archivo fuente
- 5DesafíoColocar correctamente una variable compartida
- 6EjemploCompilar por separado y después enlazar
- 7DesafíoCorregir una referencia no definida
- 8ConsejoNo copiar la implementación en la función llamadora
- 9EjemploMantener un contador dentro del módulo
- 10ExplicaciónSeparar tres propiedades del contador
- 11DesafíoExplicar dos contadores independientes
- 12ConsejoHacer que una cabecera funcione por sí sola
- 13DesafíoCorregir la privacidad y la interfaz conjuntamente
- 14RepasoRelacionar cada necesidad con su etapa de construcción
5Construir solo lo que ha cambiado
15 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónComenzar con una tarea pequeña
- 2EjemploRegistrar las entradas del ejecutable y los archivos objeto
- 3EjemploDar una receta a un archivo objeto
- 4EjemploRealizar la primera construcción y reconstruir tras un cambio de fuente
- 5DesafíoSeguir una modificación de cabecera compartida
- 6ConsejoExaminar el plan antes de ejecutarlo
- 7ExplicaciónSustituir la configuración repetida por variables
- 8EjemploExpandir la receta de enlazado
- 9EjemploUsar un patrón de compilación para ambos archivos objeto
- 10DesafíoElegir un archivo fuente sin compilar la cabecera
- 11ConsejoHacer de la limpieza una acción
- 12ConexiónDejar que el compilador registre las dependencias de cabeceras
- 13DesafíoMantener correcta una reconstrucción paralela
- 14DesafíoAplicar las opciones modificadas a todo el programa
- 15RepasoComprobar el grafo antes de confiar en el resultado
Ejercicios de Funciones y estructura de programas en C
Set 1: Seguir los valores a través de llamadas a funciones10 Preguntas de Práctica
Pregunta 1 de 10
int triple(int value) { return value * 3; }?Mostrar respuestaOcultar respuesta
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
void report(int code)?Mostrar respuestaOcultar respuesta
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
double portion(int total) { return total / 4; } para portion(11)?Mostrar respuestaOcultar respuesta
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
int capacity(void) aparece después de main. ¿Qué declaración antes de main describe por completo una llamada sin entradas en C17?Mostrar respuestaOcultar respuesta
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
int fifth(int n) { return n / 5; } declarada antes de su uso, ¿cuánto vale fifth(-14.8)?Mostrar respuestaOcultar respuesta
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
double stretch(int n);. ¿Qué definición es compatible con esa declaración?Mostrar respuestaOcultar respuesta
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);
}
Mostrar respuestaOcultar respuesta
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);
Mostrar respuestaOcultar respuesta
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);
}
Mostrar respuestaOcultar respuesta
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);
}
Mostrar respuestaOcultar respuesta
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
i de for (int i = 0; i < 6; i++) { }?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);
}
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);
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
int amount = amount + 2; dentro de un bloque no se inicializa a partir de una amount exterior?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
static int calls = 0; durante una ejecución del programa?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;
}
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;
}
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
int permitted(int n) lee una variable global limit. ¿Por qué podría producir el mismo argumento un resultado diferente en una llamada posterior?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);
}
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
read_sensor() en tiempo de ejecución. ¿Por qué static int first = read_sensor(); no es válido en C17?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
#define LIMIT 18 en el código fuente posterior?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
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
#define LIMIT 12; hace que int count = LIMIT * 2; no sea válido?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);
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))
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
#define TRACE 0, ¿qué hace #ifdef TRACE?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
#if VERBOSE podría omitir una rama sin avisar cuando la construcción solo define VERB0SE?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
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));
}
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
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?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
gcc -std=c17 -c driver.c en un sistema tipo Unix?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
reading, pero al enlazar solo su archivo objeto se informa de una referencia no definida a reading. ¿Qué debe añadirse?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
static int cached = 3; cuando se declara fuera de todas las funciones?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
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é?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
int failures definida en un único archivo fuente?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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
size_t packet_count(void);. ¿Qué hace que la cabecera sea autosuficiente?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
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?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
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
tool: main.o engine.o, ¿en qué se expanden $@ y $^?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 $@
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
clean puede impedir que una regla clean: sin marcar y sin prerrequisitos elimine archivos objeto?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
.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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
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?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?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
¿Te atascas con una pregunta tuya? Ayuda con Tareas
Términos Clave
A
Archivo de cabecera
Archivo de dependencias generado
-MMD puede escribir sensor.d, que hace que una modificación posterior de una cabecera reconstruya sensor.o.Archivo objeto
sensor.c, la construcción puede sustituir sensor.o y reutilizar un main.o sin cambios.Argumento
cube(5 + 1), el argumento se evalúa como 6 antes de que comience el cuerpo de cube.auto
auto indica explícitamente almacenamiento automático en C17; C23 también la usa para inferir un tipo a partir de un inicializador.auto int level; tiene el mismo comportamiento de almacenamiento que una variable local ordinaria int level;.Á
Ámbito (alcance)
Ámbito de archivo
Ámbito de bloque
if no puede nombrarse en una impresión después de que ese bloque se cierre.Ámbito de prototipo
value a su parámetro mientras que la definición llama n al mismo parámetro.C
Cabecera autosuficiente
size_t incluye <stddef.h> antes de esa declaración.Caso base
1, lo que permite que terminen las multiplicaciones pendientes de las llamadas anteriores.Caso recursivo
Compilación condicional
Concatenación de tokens
## en una lista de sustitución de macro para combinar tokens vecinos en un único token de preprocesamiento.temp y 2 puede formar el identificador temp2 en el código fuente expandido.Conversión de argumentos
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
# 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.3 + 4 en una cadena para mostrar la expresión junto con su valor calculado.D
Declaración
main permite comprobar una llamada incluso cuando el cuerpo de la función aparece más adelante.Definición de función
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)
main causan un error de definición múltiple o de símbolo duplicado.Definición tentativa
extern que puede proporcionar una definición.int count; en una cabecera compartida no es un sustituto portable de una declaración extern.Dependencia oculta
Directiva de preprocesamiento
# y controla la transformación del texto fuente.#include "sensor.h" inserta el texto de la cabecera en el preprocesamiento actual.Duración automática del almacenamiento
Duración del almacenamiento
Duración estática del almacenamiento
E
Efecto secundario
Ejecución simulada (dry run)
make -n muestra las recetas ordinarias de construcción sin ejecutarlas, revelando los comandos previstos del compilador y del enlazador.Enlazador
Estado compartido
extern
extern puede declarar una entidad con vinculación sin definir un objeto, siempre que ningún inicializador la convierta en definición.extern int errors;, mientras que un archivo fuente proporciona la definición del objeto inicializado.F
Función
Función llamadora
main llama a read_sensor, main es la función llamadora que espera recibir la lectura devuelta.G
Guarda de inclusión
M
Macro de tipo función
Macro de tipo objeto
#define LIMIT 16 sustituye el token LIMIT, pero deja intacto "LIMIT".Macro predefinida
__FILE__ y __LINE__ puede identificar dónde se origina su mensaje.Marco de pila
factorial(4), cuatro llamadas a factorial tienen marcos activos separados.Módulo
O
Objetivo de make
Objetivo ficticio
.PHONY, de modo que un archivo existente con ese nombre no pueda impedir la ejecución de su receta.clean como ficticio garantiza que la limpieza siga ejecutándose si aparece un archivo llamado clean en el directorio.Objetivo predeterminado
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
total en un bucle puede dejar sin cambios el acumulador exterior previsto.Operador defined
defined del preprocesador da uno si su operando, un nombre de macro, está definido y cero si no lo está.#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
P
Parámetro
int cube(int n), el parámetro n recibe un valor de entrada separado para cada llamada.Paso por valor
n dentro de una función deja sin cambios la variable entera ordinaria de la función llamadora.Pila de llamadas
main.Preprocesador
gcc -E sample.c para examinar el código expandido antes de que el compilador compruebe las expresiones de C.Prerrequisito
Prototipo de función
double scale(double factor); indica al compilador que convierta su argumento a double.R
Receta de make
Recursión
Referencia no definida (undefined reference)
sensor.o del enlazado puede dejar una referencia no definida a una función pública de lectura que solo sensor.o define.register
register marca una variable local con una sugerencia de optimización que los compiladores pueden ignorar y prohíbe obtener su dirección.register a un contador de bucle no obliga a un compilador moderno a usar un registro del procesador.Regla de make
main.o indica tanto main.c como las cabeceras del proyecto incluidas durante su compilación.Regla de patrón
% 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.%.o: %.c relaciona sensor.o con sensor.c usando sensor como raíz compartida.S
Sin vinculación
U
Unidad de traducción
V
Valor de retorno
double result sin mostrarlo.Variable automática de make
$@ nombra sensor.o mientras se ejecuta su receta de archivo objeto.Variable de make
$(CFLAGS).CC cambia el texto del compilador que usan las recetas que hacen referencia a $(CC).Variable global
main.Variable local estática
static que conserva un único valor almacenado entre llamadas repetidas.Vinculación
static limita su vinculación a una unidad de traducción.Vinculación externa
Vinculación interna
check sin conflicto durante el enlazado.void
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.void de impresión realiza su tarea de salida, pero no puede proporcionar el lado derecho de una asignación.#
#error
#error requiere un diagnóstico cuando se alcanza durante el preprocesamiento, lo que permite rechazar una construcción no admitida.#error cuando el estándar de C elegido es anterior al que requiere el programa.Sobre esta unidad
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.