11. Otras características avanzadas de C
11.1 Operaciones con bits
Podemos hacer desde C operaciones entre bits de dos números (AND, OR, XOR, etc). Vamos primero a ver qué significa cada una de esas operaciones.
Operación |
Qué hace |
En C |
Ejemplo |
Complemento (not) |
Cambiar 0 por 1 y viceversa |
~ |
~1100 = 0011 |
Producto lógico (and) |
1 sólo si los 2 bits son 1 |
& |
1101 & 1011 = 1001 |
Suma lógica (or) |
1 si uno de los bits es 1 |
| |
1101 | 1011 = 1111 |
Suma exclusiva (xor) |
1 sólo si los 2 bits son distintos |
^ |
1101 ^ 1011 = 0110 |
Desplazamiento a la izquierda |
Desplaza y rellena con ceros |
<< |
1101 << 2 = 110100 |
Desplazamiento a la derecha |
Desplaza y rellena con ceros |
>> |
1101 >> 2 = 0011 |
Ahora vamos a aplicarlo a un ejemplo completo en C:
/*---------------------------*/ /* Ejemplo en C nº 93: */ /* C093.C */ /* */ /* Operaciones de bits en */ /* números enteros */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> int main() { int a = 67; int b = 33; printf("La variable a vale %d\n", a); printf("y b vale %d\n", b); printf(" El complemento de a es: %d\n", ~a); printf(" El producto lógico de a y b es: %d\n", a&b); printf(" Su suma lógica es: %d\n", a|b); printf(" Su suma lógica exclusiva es: %d\n", a^b); printf(" Desplacemos a a la izquierda: %d\n", a << 1); printf(" Desplacemos a a la derecha: %d\n", a >> 1); return 0; }
La respuesta que nos da Dev-C++ 4.9.9.2 es la siguiente:
La variable a vale 67
y b vale 33
El complemento de a es: -68
El producto lógico de a y b es: 1
Su suma lógica es: 99
Su suma lógica exclusiva es: 98
Desplacemos a a la izquierda: 134
Desplacemos a a la derecha: 33
Para comprobar que es correcto, podemos convertir al sistema binario esos dos números y seguir las operaciones paso a paso:
67 = 0100 0011
33 = 0010 0001
En primer lugar complementamos "a", cambiando los ceros por unos:
1011 1100 = -68
Después hacemos el producto lógico de A y B, multiplicando cada bit, de modo que 1*1 = 1, 1*0 = 0, 0*0 = 0
0000 0001 = 1
Después hacemos su suma lógica, sumando cada bit, de modo que 1+1 = 1, 1+0 = 1, 0+0 = 0
0110 0011 = 99
La suma lógica exclusiva devuelve un 1 cuando los dos bits son distintos: 1^1 = 0, 1^0 = 1, 0^0 = 0
0110 0010 = 98
Desplazar los bits una posición a la izquierda es como multiplicar por dos:
1000 0110 = 134
Desplazar los bits una posición a la derecha es como dividir entre dos:
0010 0001 = 33
¿Qué utilidades puede tener todo esto? Posiblemente, más de las que parece a primera vista. Por ejemplo: desplazar a la izquierda es una forma muy rápida de multiplicar por potencias de dos; desplazar a la derecha es dividir por potencias de dos; la suma lógica exclusiva (xor) es un método rápido y sencillo de cifrar mensajes; el producto lógico nos permite obligar a que ciertos bits sean 0 (algo que se puede usar para comprobar máscaras de red); la suma lógica, por el contrario, puede servir para obligar a que ciertos bits sean 1...
Un último comentario: igual que hacíamos operaciones abreviadas como
x += 2;
también podremos hacer cosas como
x <<= 2;
x &= 2;
x |= 2;
...
11.2 Directivas del preprocesador
Desde el principio hemos estado manejando cosas como
#include <stdio.h>
Y aquí hay que comentar bastante más de lo que parece. Ese “include” no es una orden del lenguaje C, sino una orden directa al compilador (una “directiva”). Realmente es una orden a una cierta parte del compilador que se llama “preprocesador”. Estas directivas indican una serie de pasos que se deben dar antes de empezar realmente a traducir nuestro programa fuente.
Aunque “include” es la directiva que ya conocemos, vamos a comenzar por otra más sencilla, y que nos resultará útil cuando lleguemos a ésta.
11.2.1. Constantes simbólicas: #define
La directiva “define” permite crear “constantes simbólicas”. Podemos crear una constante haciendo
#define MAXINTENTOS 10
y en nuestro programa lo usaríamos como si se tratara de cualquier variable o de cualquier valor numérico:
if (intentoActual >= MAXINTENTOS) ...
El primer paso que hace nuestro compilador es reemplazar esa “falsa constante” por su valor, de modo que la orden que realmente va a analizar es
if (intentoActual >= 10) ...
pero a cambio nosotros tenemos el valor numérico sólo al principio del programa, por lo que es muy fácil de modificar, mucho más que si tuviéramos que revisar el programa entero buscando dónde aparece ese 10.
Comparado con las constantes “de verdad”, que ya habíamos manejado (const int MAXINTENTOS=10;), las constantes simbólicas tienen la ventaja de que no son variables, por lo que no se les reserva memoria adicional y las comparaciones y demás operaciones suelen ser más rápidas que en el caso de un variable.
Vamos a ver un ejemplo completo, que pida varios números y muestre su suma y su media:
/*---------------------------*/ /* Ejemplo en C nº 94: */ /* C094.C */ /* */ /* Ejemplo de #define */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #define CANTIDADNUMEROS 5 int main() { int numero[CANTIDADNUMEROS]; int suma=0; int i; for (i=0; i<CANTIDADNUMEROS; i++) { printf("Introduzca el dato número %d: ", i+1); scanf("%d", &numero[i]); } for (i=0; i<CANTIDADNUMEROS; i++) suma += numero[i]; printf("Su suma es %d\n", suma); printf("Su media es %4.2f\n", (float) suma/CANTIDADNUMEROS); return 0; }
Las constantes simbólicas se suelen escribir en mayúsculas por convenio, para que sean más fáciles de localizar. De hecho, hemos manejado hasta ahora muchas constantes simbólicas sin saberlo. La que más hemos empleado ha sido NULL, que es lo mismo que un 0, está declarada así:
#define NULL 0
Pero también en casos como la pantalla en modo texto con Turbo C aparecían otras constantes simbólicas, como éstas
#define BLUE 1
#define YELLOW 14
Y al acceder a ficheros teníamos otras constantes simbólicas como SEEK_SET (0), SEEK_CUR (1), SEEK_END (2).
A “define” también se le puede dar también un uso más avanzado: se puede crear “macros”, que en vez de limitarse a dar un valor a una variable pueden comportarse como pequeñas órdenes, más rápidas que una función. Un ejemplo podría ser:
#define SUMA(x,y) x+y
Vamos a aplicarlo en un fuente completo:
/*---------------------------*/ /* Ejemplo en C nº 95: */ /* C095.C */ /* */ /* Ejemplo de #define (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #define SUMA(x,y) x+y int main() { int n1, n2; printf("Introduzca el primer dato: "); scanf("%d", &n1); printf("Introduzca el segundo dato: "); scanf("%d", &n2); printf("Su suma es %d\n", SUMA(n1,n2)); return 0; }
11.2.2 Inclusión de ficheros: #include
Ya nos habíamos encontrado con esta directiva. Lo que hace es que cuando llega el momento de que nuestro compilador compruebe la sintaxis de nuestro fuente en C, ya no existe ese “include”, sino que en su lugar el compilador ya ha insertado los ficheros que le hemos indicado.
¿Y eso de por qué se escribe <stdio.h>, entre < y >? No es la única forma de usar #include. Podemos encontrar líneas como
#include <stdlib.h>
y como
#include "misdatos.h"
El primer caso es un fichero de cabecera estándar del compilador. Lo indicamos entre < y > y así el compilador sabe que tiene que buscarlo en su directorio (carpeta) de “includes”. El segundo caso es un fichero de cabecera que hemos creado nosotros, por lo que lo indicamos entre comillas, y así el compilador sabe que no debe buscarlo entre sus directorios, sino en el mismo directorio en el que está nuestro programa.
Vamos a ver un ejemplo: declararemos una función “suma” dentro de un fichero “.h” y lo incluiremos en nuestro fuente para poder utilizar esa función “suma” sin volver a definirla. Wl fichero de cabecera sería así:
/*---------------------------*/ /* Ejemplo en C nº 96 (a): */ /* C096.H */ /* */ /* Ejemplo de #include */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ int suma(int x,int y) { return x+y; }
(Nota: si somos puristas, esto no es correcto del todo. Un fichero de cabecera no debería contener los detalles de las funciones, sólo su “cabecera”, lo que habíamos llamado el “prototipo”, y la implementación de la función debería estar en otro fichero, pero eso lo haremos dentro de poco).
Un fuente que utilizara este fichero de cabecera podría ser
/*---------------------------*/ /* Ejemplo en C nº 96 (b): */ /* C096.C */ /* */ /* Ejemplo de #include (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include "c096.h" int main() { int n1, n2; printf("Introduzca el primer dato: "); scanf("%d", &n1); printf("Introduzca el segundo dato: "); scanf("%d", &n2); printf("Su suma es %d\n", suma(n1,n2)); return 0; }
11.2.3. Compilación condicional: #ifdef, #endif
Hemos utilizado #define para crear constantes simbólicas. Desde dentro de nuestro fuente, podemos comprobar si está definida una constante, tanto si ha sido creada por nosotros como si la ha creado el sistema.
Cuando queramos que nuestro fuente funcione en varios sistemas distintos, podemos hacer que se ejecuten unas órdenes u otras según de qué compilador se trate, empleando #ifdef (o #if) al principio del bloque y #endif al final. Por ejemplo,
#ifdef _GCC_
instanteFinal = clock () + segundos * CLOCKS_PER_SEC ;
#endif
#ifdef _TURBOC_
instanteFinal = clock () + segundos * CLK_TCK ;
#endif
Los programas que utilicen esta idea podrían compilar sin ningún cambio en distintos sistemas operativos y/ distintos compiladores, y así nosotros esquivaríamos las incompatibilidades que pudieran existir entre ellos (a cambio, necesitamos saber qué constantes simbólicas define cada sistema).
Esta misma idea se puede aplicar a nuestros programas. Uno de los usos más frecuentes es hacer que ciertas partes del programa se pongan en funcionamiento durante la fase de depuración, pero no cuando el programa esté terminado.
Vamos a mejorar el ejemplo 94, para que nos muestre el valor temporal de la suma y nos ayude a descubrir errores:
(-- Fichero no encontrado --)
Este fuente tiene intencionadamente un error: no hemos dado un valor inicial a la suma, con lo que contendrá basura, y obtendremos un resultado incorrecto.
El resultado de nuestro programa sería
Introduzca el dato numero 1: 2
Introduzca el dato numero 2: 3
Introduzca el dato numero 3: 4
Introduzca el dato numero 4: 5
Introduzca el dato numero 5: 7
Valor actual de la suma: 2009055971
Valor actual de la suma: 2009055973
Valor actual de la suma: 2009055976
Valor actual de la suma: 2009055980
Valor actual de la suma: 2009055985
Su suma es 2009055992
Su media es 401811198.40
Vemos que ya en la primera pasada, el valor de la suma no es 2, sino algo que parece absurdo, así que falta el valor inicial de la suma, que debería ser “int suma=0;”. Cuando acaba la fase de depuración, basta con eliminar la frase #define DEPURANDO 1 (no es necesario borrarla, podemos dejarla comentada para que no haga efecto), de modo que el fuente corregido sería:
/*---------------------------*/ /* Ejemplo en C nº 97b: */ /* C097b.C */ /* */ /* Compilacion condicional */ /* con #define (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #define CANTIDADNUMEROS 5 /*#define DEPURANDO 1*/ int main() { int numero[CANTIDADNUMEROS]; int suma=0; /* Error corregido */ int i; for (i=0; i<CANTIDADNUMEROS; i++) { printf("Introduzca el dato número %d: ", i+1); scanf("%d", &numero[i]); } for (i=0; i<CANTIDADNUMEROS; i++) { #ifdef DEPURANDO printf("Valor actual de la suma: %d\n", suma); #endif suma += numero[i]; } printf("Su suma es %d\n", suma); printf("Su media es %4.2f\n", (float) suma/CANTIDADNUMEROS); return 0; }
También tenemos otra alternativa: en vez de comentar la línea de #define, podemos anular la definición con la directiva #undef:
#undef DEPURANDO
Por otra parte, también tenemos una directiva #ifndef para indicar qué hacer si no está definida una contante simbólico, y un #else, al igual que en los “if” normales, para indicar qué hacer si no se cumple la condición, y una directiva #elif (abreviatura de “else if”), por si queremos encadenar varias condiciones.
11.2.4. Otras directivas
Las que hemos comentado son las directivas más habituales, pero también existen otras. Una de las que son frecuentes (pero menos estándar que las anteriores) es #pragma, que permite indicar opciones avanzadas específicas de cada compilador.
No veremos detalles de su uso en ningún compilador concreto, pero sí un ejemplo de las cosas que podemos encontrar su manejamos fuentes creados por otros programadores:
#if __SC__ || __RCC__
#pragma once
#endif
#ifndef RC_INVOKED
#pragma pack(__DEFALIGN)
#endif
11.3. Programas a partir de varios fuentes
11.3.1. Creación desde la línea de comandos
Es bastante frecuente que un programa no esté formado por un único fuente, sino por varios. Puede ser por hacer el programa más modular debido a su complejidad, por reparto de trabajo entre varias personas, etc.
En cualquier caso, la gran mayoría de los compiladores de C serán capaces de juntar varios fuentes independientes y crear un único ejecutable a partir de todos ellos. Vamos a ver cómo conseguirlo.
Empezaremos por el caso más sencillo: supondremos que tenemos un programa principal, y otros dos módulos auxiliares en los que hay algunas funciones que se usarán desde el programa principal.
Por ejemplo, el primer módulo (uno.c) podría ser simplemente:
/*---------------------------*/ /* Ejemplo en C nº 98a: */ /* uno.c */ /* */ /* Programa a partir de */ /* varios fuentes (1) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ void uno() { printf("Función uno\n"); }y el segundo módulo (dos.c):
/*---------------------------*/ /* Ejemplo en C nº 98b: */ /* dos.c */ /* */ /* Programa a partir de */ /* varios fuentes (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ void dos(int numero) { printf("Función dos, con el parámetro %d\n", numero); }Un programa principal simple, que los utilizase, sería (TRES.C):
/*---------------------------*/ /* Ejemplo en C nº 98c: */ /* tres.c */ /* */ /* Programa a partir de */ /* varios fuentes (3) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> int main() { printf("Estamos en el cuerpo del programa.\n"); uno(); dos(3); return 0; }
Para compilar los tres fuentes y crear un único ejecutable, desde la mayoría de los compiladores de Dos o Windows bastaría con acceder a la línea de comandos, y teclear el nombre del compilador seguido por los de los tres fuentes:
TCC UNO DOS TRES
(TCC es el nombre del compilador, en el caso de Turbo C y de Turbo C++; sería BCC para el caso de Borland C++, SCC para Symantec C++, etc.).
Entonces el compilador convierte los tres ficheros fuente (.C) a ficheros objeto (.OBJ), los enlaza y crea un único ejecutable, que se llamaría UNO.EXE (porque UNO es el nombre del primer fuente que hemos indicado al compilador), y cuya salida en pantalla sería:
Estamos en el cuerpo del programa.
Función uno
Función dos, con el parámetro 3
En el caso de GCC para Linux (o de alguna de sus versiones para Windows, como MinGW, o DevC++, que se basa en él), tendremos que indicarle el nombre de los ficheros de entrada (con extensión) y el nombre del fichero de salida, con la opción “-o”:
gcc uno.c dos.c tres.c -o resultado
Pero puede haber compiladores en los que la situación no sea tan sencilla. Puede ocurrir que al compilar el programa principal, que era:
int main()
{
printf("Estamos en el cuerpo del programa.\n");
uno();
dos(3);
return 0;
}
el compilador nos dé un mensaje de error, diciendo que no conoce las funciones "uno()" y "dos()". No debería ser nuestro caso, si al compilar le hemos indicado los fuentes en el orden correcto (TCC UNO DOS TRES), pero puede ocurrir si se los indicamos en otro orden, o bien si tenemos muchos fuentes, que dependan los unos de los otros.
La forma de evitarlo sería indicándole que esas funciones existen, y que ya le llegarán más tarde los detalles en concreto sobre cómo funcionan.
Para decirle que existen, lo que haríamos sería incluir en el programa principal los prototipos de las funciones (las cabeceras, sin el desarrollo) que se encuentran en otros módulos, así:
/*---------------------------*/ /* Ejemplo en C nº 98d: */ /* tres.c */ /* */ /* Programa a partir de */ /* varios fuentes (3b) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> void uno(); /* Prototipos de las funciones externas */ void dos(int numero); int main() /* Cuerpo del programa */ { printf("Estamos en el cuerpo del programa.\n"); uno(); dos(3); return 0; }Esta misma solución de poner los prototipos al principio del programa nos puede servir para casos en los que, teniendo un único fuente, queramos declarar el cuerpo del programa antes que las funciones auxiliares:
/*---------------------------*/ /* Ejemplo en C nº 99: */ /* c099.c */ /* */ /* Funciones después de */ /* "main",usando prototipos */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> void uno(); /* Prototipos de las funciones */ void dos(int numero); int main() /* Cuerpo del programa */ { printf("Estamos en el cuerpo del programa.\n"); uno(); dos(3); return 0; } void uno() { printf("Función uno\n"); } void dos(int numero) { printf("Función dos, con el parámetro %d\n", numero); }
En ciertos compiladores puede que tengamos problemas con este programa si no incluimos los prototipos al principio, porque en "main()" se encuentra la llamada a "uno()", que no hemos declarado. Al poner los prototipos antes, el compilador ya sabe qué tipo de función es "uno()" (sin parámetros, no devuelve ningún valor, etc.), y que los datos concretos los encontrará más adelante.
De hecho, si quitamos esas dos líneas, este programa no compila en Turbo C++ 1.01 ni en Symantec C++ 6.0, porque cuando se encuentra en "main()" la llamada a "uno()", da por supuesto que va a ser una función de tipo "int". Como después le decimos que es "void", protesta. (En cambio, GCC, que suele ser más exigente, en este caso se limita a avisarnos, pero compila el programa sin problemas).
La solución habitual en estos casos en que hay que declarar prototipos de funciones (especialmente cuando se trata de funciones compartidas por varios fuentes) suele ser agrupar estos fuentes en "ficheros de cabecera". Por ejemplo, podríamos crear un fichero llamado EJEMPLO.H que contuviese:
/* EJEMPLO.H */
void uno(); /* Prototipos de las funciones */
void dos(int numero);
y en el fuente principal escribiríamos:
#include <stdio.h>
#include "ejemplo.h"
int main()
{
printf("Estamos en el cuerpo del programa.\n");
uno();
dos(3);
return 0;
}
Aquí es importante recordar la diferencia en la forma de indicar los dos ficheros de cabecera:
<stdio.h> Se indica entre corchetes angulares porque el fichero de cabecera es propio del compilador (el ordenador lo buscará en los directorios del compilador).
"ejemplo.h" Se indica entre comillas porque el fichero H es nuestro (el ordenador lo buscará en el mismo directorio que están nuestros fuentes).
Finalmente, conviene hacer una consideración: si varios fuentes distintos necesitaran acceder a EJEMPLO.H, deberíamos evitar que este fichero se incluyese varias veces. Esto se suele conseguir definiendo una variable simbólica la primera vez que se enlaza, de modo que podamos comprobar a partir de entonces si dicha variable está definida, con #ifdef, así:
/* EJEMPLO.H mejorado */
#ifndef _EJEMPLO_H
#define _EJEMPLO_H
void uno(); /* Prototipos de las funciones */
void dos(int numero);
#endif
11.3.2. Introducción al uso de la herramienta Make
Make es una herramienta que muchos compiladores incorporan y que nos puede ser de utilidad cuando se trata de proyectos de un cierto tamaño, o al menos creados a partir de bastantes fuentes.
Su uso normal consiste simplemente en teclear MAKE. Entonces esta herramienta buscará su fichero de configuración, un fichero de texto que deberá llamarse "makefile" (podemos darle otro nombre; ya veremos cómo). Este fichero de configuración le indica las dependencias entre ficheros, es decir, qué ficheros es necesario utilizar para crear un determinado "objetivo". Esto permite que no se recompile todos y cada uno de los fuentes si no es estrictamente necesario, sino sólo aquellos que se han modificado desde la última compilación.
En general, el contenido del fichero "makefile" será algo parecido a esto:
objetivo: dependencias
órdenes
(En la primera línea se escribe el objetivo, seguido por dos puntos y por la lista de dependencias; en la segunda línea se escribe la orden que hay que dar en caso de que sea necesario recompilar, precedida por un tabulador). Si queremos añadir algún comentario, basta con precederlos con el símbolo #.
Vamos a crear un ejecutable llamado PRUEBA.EXE a partir de cuatro ficheros fuente llamados UNO.C, DOS.C, TRES.C, CUATRO.C, usando Turbo C++.
Así, nuestro primer “makefile” podría ser un fichero que contuviese el siguiente texto:
PRUEBA.EXE: UNO.C DOS.C TRES.C CUATRO.C
TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C
Es decir: nuestro objetivo es conseguir un fichero llamado PRUEBA.EXE, que queremos crear a partir de varios ficheros llamados UNO.C, DOS.C, TRES.C y CUATRO.C. La orden que queremos dar es la que aparece en la segunda línea, y que permite, mediante el compilador TCC, crear un ejecutable llamado PRUEBA.EXE a partir de cuatro fuentes con los nombres anteriores. (La opción "-e" de Turbo C++ permite indicar el nombre que queremos que tenga el ejecutable; si no, se llamaría UNO.EXE, porque tomaría su nombre del primero de los fuentes).
¿Para qué nos sirve esto? De momento, nos permite ahorrar tiempo: cada vez que tecleamos MAKE, se lee el fichero MAKEFILE y se compara la fecha (y hora) del objetivo con la de las dependencias; si el fichero objetivo no existe o si es más antiguo que alguna de las dependencias, se realiza la orden que aparece en la segunda línea (de modo que evitamos escribirla completa cada vez).
En nuestro caso, cada vez que tecleemos MAKE, ocurrirá una de estas tres posibilidades
- Si no existe el fichero PRUEBA.EXE, se crea uno nuevo utilizando la orden de la segunda línea.
- Si ya existe y es más reciente que los cuatro fuentes, no se recompila ni se hace nada, todo queda como está.
- Si ya existe, pero se ha modificado alguno de los fuentes, se recompilará de nuevo para obtener un fichero PRUEBA.EXE actualizado.
Eso sí, estamos dando por supuesto varias cosas “casi evidentes”:
- Que tenemos la herramienta MAKE y está accesible (en el directorio actual o en el PATH).
- Que hemos creado el fichero MAKEFILE.
- Que existen los cuatro ficheros fuente UNO.C, DOS.C, TRES.C y CUATRO.C.
- Que existe el compilador TCC y está accesible (en el directorio actual o en el PATH).
Vayamos mejorando este MAKEFILE rudimentario. La primera mejora es que si la lista de dependencias no cabe en una única linea, podemos partirla en dos, empleando la barra invertida \
PRUEBA.EXE: UNO.C DOS.C \ #Objetivo y dependencias
TRES.C CUATRO.C # Mas dependencias
TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C #Orden a dar
Al crear el MAKEFILE habíamos ganado en “velocidad de tecleo” y en que no se recompilase todo nuevamente si no se ha modificado nada. Pero en cuanto un fuente se modifica, nuestro MAKEFILE recompila todos otra vez, aunque los demás no hayan cambiado. Esto podemos mejorarlo añadiendo un paso intermedio (la creación cada fichero objeto OBJ) y más objetivos (cada fichero OBJ, a partir de cada fichero fuente), así:
# Creacion del fichero ejecutable
prueba.exe: uno.obj dos.obj tres.obj
tcc -eprueba.exe uno.obj dos.obj tres.obj
# Creacion de los ficheros objeto
uno.obj: uno.c
tcc -c uno.c
dos.obj: dos.c
tcc -c dos.c
tres.obj: tres.c
tcc -c tres.c
Estamos detallando los pasos que normalmente se dan al compilar, y que muchos compiladores realizan en una única etapa, sin que nosotros nos demos cuenta: primero se convierten los ficheros fuente (ficheros con extensión C) a código máquina (código objeto, ficheros con extensión OBJ) y finalmente los ficheros objeto se enlazan entre sí (y con las bibliotecas propias del lenguaje) para dar lugar al programa ejecutable (en MsDos y Windows normalmente serán ficheros con extensión EXE).
Así conseguimos que cuando modifiquemos un único fuente, se recompile sólo este (y no todos los demás, que pueden ser muchos) y después se pase directamente al proceso de enlazado, con lo que se puede ganar mucho en velocidad si los cambios que hemos hecho al fuente son pequeños.
(Nota: la opción "-c" de Turbo C++ es la que le indica que sólo compile los ficheros de C a OBJ, pero sin enlazarlos después).
Si tenemos varios MAKEFILE distintos (por ejemplo, cada uno para un compilador diferente, o para versiones ligeramente distintas de nuestro programa), nos interesará poder utilizar nombres distintos.
Esto se consigue con la opción "-f" de la orden MAKE, por ejemplo si tecleamos
MAKE -fPRUEBA
la herramienta MAKE buscaría un fichero de configuración llamado PRUEBA o bien PRUEBA.MAK.
Podemos mejorar más aún estos ficheros de configuración. Por ejemplo, si precedemos la orden por @, dicha orden no aparecerá escrita en pantalla
PRUEBA.EXE: UNO.C DOS.C TRES.C CUATRO.C
@TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C
Y si precedemos la orden por & , se repetirá para los ficheros indicados como "dependencias". Hay que usarlo en conjunción con la macro $**, que hace referencia a todos los ficheros dependientes, o $?, que se refiere a los ficheros que se hayan modificado después que el objetivo.
copiaSeguridad: uno.c dos.c tres.c
© $** a:\fuentes
Una última consideración: podemos crear nuestras propias macros, con la intención de que nuestro MAKEFILE resulte más fácil de leer y de mantener, de modo que una versión más legible de nuestro primer fichero sería:
FUENTES = uno.c dos.c tres.c
COMPIL = tcc
prueba.exe: $(FUENTES)
$(COMPIL) -eprueba.exe $(FUENTES)
Es decir, las macros se definen poniendo su nombre, el signo igual y su definición, y se emplean precediéndolas de $ y encerrándolas entre paréntesis.
Pero todavía hay más que no hemos visto. Las herramientas MAKE suelen permitir otras posibilidades, como la comprobación de condiciones (con las directivas "!if", "!else" y similares) o la realización de operaciones (con los operadores estándar de C: +, *, %, >>, etc). Quien quiera profundizar en estos y otros detalles, puede recurrir al manual de la herramienta MAKE que incorpore su compilador.
¿Alguna diferencia en Linux? Pocas. Sólo hay que recordar que en los sistemas Unix se distingue entra mayúsculas y minúsculas, por lo que la herramienta se llama “make”, y el fichero de datos “Makefile” o “makefile” (preferible la primera nomenclatura, con la primera letra en mayúsculas). De igual modo, el nombre del compilador y los de los fuentes se deben escribir dentro del “Makefile” exactamente como se hayan creado (habitualmente en minúsculas).
11.3.3. Introducción a los “proyectos”
En la gran mayoría de los compiladores que incorporan un “entorno de desarrollo”, existe la posibilidad de conseguir algo parecido a lo que hace la herramienta MAKE, pero desde el propio entorno. Es lo que se conoce como “crear un proyecto”.
Se suele poder hacer desde desde un menú llamado “Proyecto”, donde existirá una opción “Nuevo proyecto” (en inglés Project / New Project), o a veces desde el menú Archivo.
En muchas ocasiones, tendremos varios tipos de proyectos disponibles, gracias a que algún asistente deje el esqueleto del programa preparado para nosotros.
Desde esta primera ventana también le daremos ya un nombre al proyecto (será el nombre que tendrá el fichero ejecutable), y también desde aquí podemos añadir los fuentes que formarán parte del proyecto, si los tuviéramos creados desde antes (suele ser algo como “Añadir fichero”, o en inglés “Add Item”).
En una cierta ventana de la pantalla tendremos información sobre los fuentes que componen nuestro proyecto (en el ejemplo, tomado de Turbo C++ Second Edition´, esta ventana aparece en la parte inferior de la pantalla).
En otros entornos, como Anjuta o KDevelop, esta información aparece en la parte izquierda de la pantalla:
Las ventajas que obtenemos al usar “proyectos” son:
- La posibilidad de manipular varios fuentes a la vez y de recompilar un programa que esté formado por todos ellos, sin necesidad de salir del entorno de desarrollo.
- La ventaja añadida de que el gestor de proyectos sólo recompilará aquellos fuentes que realmente se han modificado, como comentábamos que hacía la herramienta MAKE.
11.4 Uniones y campos de bits
Conocemos lo que es un struct: un dato formado por varios “trozos” de información de distinto tipo. Pero C también tiene dos tipos especiales de “struct”, de manejo más avanzado. Son las uniones y los campos de bits.
Una unión recuerda a un “struct” normal, con la diferencia de que sus “campos” comparten el mismo espacio de memoria:
union {
char letra; /* 1 byte */
int numero; /* 4 bytes */
} ejemplo;
En este caso, la variable “ejemplo” ocupa 4 bytes en memoria (suponiendo que estemos trabajando en un compilador de 32 bits, como lo son la mayoría de los de Windows y Linux). El primer byte está compartido por “letra” y por “numero”, y los tres últimos bytes sólo pertenecen a “numero”.
Si hacemos
ejemplo.numero = 25;
ejemplo.letra = 50;
printf("%d", ejemplo.numero);
Veremos que “ejemplo.numero” ya no vale 25, puesto que al modificar “ejemplo.letra” estamos cambiando su primer byte. Ahora “ejemplo.numero” valdría 50 o un número mucho más grande, según si el ordenador que estamos utilizando almacena en primer lugar el byte más significativo o el menos significativo.
Un campo de bits es un elemento de un registro (struct), que se define basándose en su tamaño en bits. Se define de forma muy parecida (pero no igual) a un "struct" normal, indicando el número de bits que se debe reservar a cada elemento:
struct campo_de_bits {
int bit_1 : 1;
int bits_2_a_5 : 4;
int bit_6 : 1;
int bits_7_a_16 : 10;
} variableDeBits;
Esta variable ocuparía 1+4+1+10 = 16 bits (2 bytes). Los campos de bits pueden ser interesantes cuando queramos optimizar al máximo el espacio ocupado por nuestro datos.
11.5. El operador coma
Cuando vimos la orden “for”, siempre usábamos una única variable como contador, pero esto no tiene por qué ser siempre así. Vamos a verlo con un ejemplo:
/*---------------------------*/ /* Ejemplo en C nº 100: */ /* c100.c */ /* */ /* Operador coma (1) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main(){ int i, j; for (i=0,j=1; i<=5, j<=30; i++, j+=2) printf("i vale %d y j vale %d.\n", i, j); }
Vamos a ver qué hace este "for":
* Los valores iniciales son i=0, j=1.
* Se repetirá mientras que i <= 5, j <= 30.
* Al final de cada paso, i aumenta en una unidad, y j en dos unidades.
El único problema está en saber cúando terminará el bucle: si se parará en cuanto se cumpla una de las dos condiciones o si tendrán que cumplirse las dos.
El resultado de este programa servirá como respuesta:
i vale 0 y j vale 1.
i vale 1 y j vale 3.
i vale 2 y j vale 5.
i vale 3 y j vale 7.
i vale 4 y j vale 9.
i vale 5 y j vale 11.
i vale 6 y j vale 13.
i vale 7 y j vale 15.
i vale 8 y j vale 17.
i vale 9 y j vale 19.
i vale 10 y j vale 21.
i vale 11 y j vale 23.
i vale 12 y j vale 25.
i vale 13 y j vale 27.
i vale 14 y j vale 29.
Como podemos observar, llega un momento en que deja de cumplise que i<=5, pero el programa sigue avanzando: no se sale del bucle “for” hasta que se cumplen las dos condiciones (realmente, hasta que se cumple la segunda).
La idea es que, en general, si se usa el operador coma para separar dos expresiones, nuestro compilador evalúa la primera expresión, luego la segunda, y devuelve como valor el resultado de la segunda. Veámoslo con un segundo ejemplo algo más rebuscado
/*---------------------------*/ /* Ejemplo en C nº 101: */ /* c101.c */ /* */ /* Operador coma (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main(){ int i, j, k; k = ((i=5), (j=6)); printf("i=%d, j=%d, k=%d", i, j, k); }
Aun así, en la práctica, el único uso habitual del operador coma es el primero que hemos visto: utilizar dos condiciones simultáneas para controlar un bucle “for”.
11.6. Enumeraciones
Cuando tenemos varias constantes, cuyos valores son números enteros, y especialmente si son números enteros consecutivos, tenemos una forma abreviada de definirlos. Se trata de enumerarlos:
enum diasSemana { LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO,
DOMINGO };
(Al igual que las constantes de cualquier otro tipo, se suele escribir en mayúsculas para recordar en cualquier parte que se sepa "de un vistazo" que son constantes, no variables)
La primera constante valdrá 0, y las demás irán aumentando de una en una, de modo que en nuestro caso valen:
LUNES = 0, MARTES = 1, MIERCOLES = 2, JUEVES = 3, VIERNES = 4,
SABADO = 5, DOMINGO = 6
Si queremos que los valores no sean exactamente estos, podemos dar valor a cualquiera de las contantes, y las siguientes irán aumentando de uno en uno. Por ejemplo, si escribimos
enum diasSemana { LUNES=1, MARTES, MIERCOLES, JUEVES=6, VIERNES,
SABADO=10, DOMINGO };
Ahora sus valores son:
LUNES = 1, MARTES = 2, MIERCOLES = 3, JUEVES = 6, VIERNES = 7,
SABADO = 10, DOMINGO = 11
11.7. Definición de tipos
El tipo de una variable nos indica el rango de valores que puede tomar. Tenemos creados para nosotros los tipos básicos, pero puede que nos interese crear nuestros propios tipos de variables, para lo que usamos “typedef”. El formato es
typedef tipo nombre;
Lo que hacemos es darle un nuevo nombre a un tipo de datos. Puede ser cómodo para hacer más legible nuestros programas. Por ejemplo, alguien que conozca Pascal o Java, puede echar en falta un tipo de datos “boolean”, que permita hacer comparaciones un poco más legibles, siguiendo esta estructura:
if (encontrado == FALSE) ...
o bien como
if (datosCorrectos == TRUE) ...
/*---------------------------*/ /* Ejemplo en C nº 102: */ /* c102.c */ /* */ /* Definición de tipos (1) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #define VALIDA 711 /* Clave correcta */ #define TRUE 1 /* Nostalgia de los boolean */ #define FALSE 0 typedef int boolean; /* Definimos un par de tipos */ typedef int integer; integer clave; /* Y dos variables */ boolean acertado; int main() { do { printf("Introduzca su clave numérica: "); scanf("%d", &clave); acertado = (clave == VALIDA); if (acertado == FALSE) printf("No válida!\n"); } while (acertado != TRUE); printf("Aceptada.\n"); return 0; }
También podemos usar "typedef" para dar un nombre corto a todo un struct:
/*---------------------------*/ /* Ejemplo en C nº 103: */ /* c103.c */ /* */ /* Definición de tipos (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> typedef struct { /* Defino el tipo "datos" */ int valor; float coste; char ref; } datos; datos ejemplo; /* Y creo una variable de ese tipo */ int main() { ejemplo.valor = 34; ejemplo.coste = 1200.5; ejemplo.ref = 'A'; printf("El valor es %d", ejemplo.valor); return 0; }