Repaso de C: librerías estándar

Las librerías estándar de C son las librerías que forman parte del estándar del lenguaje. Son un conjunto de funciones, tipos y macros que están disponibles en cualquier implementación de C (siempre y cuando sea compatible con el estándar de C, claro).

Las librerías estándar de C han cambiado a lo largo del tiempo, al igual que el propio lenguaje, pero las librerías principales siguen siendo las mismas:

  • stdio.h: Proporciona funciones para la entrada y salida de datos, como printf(), scanf(), fopen() y fclose().
  • ctype.h: Proporciona funciones para clasificar y manipular caracteres, como isalpha(), isdigit(), islower(), isupper(), tolower() y toupper().
  • string.h: Proporciona funciones para la manipulación de cadenas, como strlen(), strcpy() y strcat().
  • math.h: Proporciona funciones matemáticas, como pow(), sqrt(), sin() y cos().
  • stdlib.h: Proporciona funciones para la gestión de memoria, como malloc() y free(), y funciones de conversión de cadenas y números, como atoi() y strtol().
  • time.h: Proporciona funciones para la gestión del tiempo, como time(), localtime() y strftime().
  • limits.h: Proporciona constantes para conocer los valores límite de los diferentes tipos de datos, como INT_MIN, INT_MAX, LONG_MIN, LONG_MAX, etc.

Hay muchas más librerías estándar (¡¡y no estándar!!), pero estas son las principales. La buena noticia es que casi todas ellas están incluidas en cc65, como se puede comprobar en el directorio cc65\include, siendo la ausencia más reseñable la de math.h.

Está fuera del alcance de este repaso el revisar en profundidad todas las librerías estándar de C o, siquiera, la selección anterior. Sí interesa destacar las librerías más importantes, quedando para el lector interesado una revisión en profundidad.


Código de ejemplo: librerías

Repaso de C: estructuras, uniones y typedef

Estructuras:

Las estructuras son conjuntos de variables relacionadas que se manejan de forma conjunta. Esas variables relacionadas se llaman “miembros” y pueden ser de tipos distintos (no como en un array). Un ejemplo típico sería:

Como se puede ver, los tipos de los miembros pueden ser simples (ej. int, double, float, etc.) o complejos (ej. arrays, punteros, etc.). Es más, podrían ser incluso otras estructuras.

Una cuestión importante, y que suele ser fuente de confusión, es que “struct cliente” es en realidad el tipo de datos. Luego se pueden definir variables de este tipo, por ejemplo, con:

struct cliente mi_cliente;

Con la variable mi_cliente podemos referirnos a todo el conjunto de variables de la estructura, lo cual es práctico. También es posible acceder a miembros concretos con el operador “.”, es decir:

edad = mi_cliente.edad;

Con frecuencia, las estructuras se manejan por referencia, es decir, mediante un puntero. Por ejemplo, pcliente sería un puntero a una estructura de tipo struct cliente:

struct cliente *pcliente;

Este uso conjunto de punteros y estructuras es tan habitual que, incluso, C define una sintaxis específica para el acceso a los miembros de las estructuras apuntadas por punteros. Así, (*pcliente).nombre, que es el miembro nombre de la estructura apuntada por el puntero pcliente, también se puede escribir:

pcliente->nombre;

Las variables de tipo estructura se pueden inicializar, asignar, pasar a funciones (por valor y por referencia), retornar de funciones, etc. También es posible definir arrays de estructuras y, por supuesto, estructuras cuyos miembros son arrays o cadenas.

sizeof y sizeof():

El operador sizeof permite conocer el tamaño de cualquier variable y, en particular, de una estructura.

En general, el tamaño de una estructura será la suma de los tamaños de sus miembros, pero no siempre tiene que ser así. Hay procesadores o compiladores que exigen que las palabras (palabra = 2 bytes) empiecen en posición par o impar de la memoria, lo que significa que, si una estructura incluye miembros que ocupan un byte (ej. char) y miembros que ocupan palabras (ej. int), estos últimos tendrán que ser “alineados” para empezar en la posición de memoria correcta, utilizándose para ello bytes de relleno. Por ello, lo mejor es no confiar en la suma de tamaños y usar siempre el operador size.

Por otro lado, sizeof() es similar a size, pero opera sobre tipos, no variables.

Uniones:

Las uniones son parecidas a las estructuras, en el sentido de que son una forma de agrupar un conjunto de variables relacionadas (los miembros):

La principal diferencia es que, mientras que en una estructura todos los miembros tienen sentido y existencia en todo momento, en el caso de una unión, sólo uno de los miembros existe en cada momento.

Por ejemplo, en el caso anterior, la fecha indicada estará almacenada en formato ddmmaa o en formato aammdd, pero no en ambos. Y en ambos casos se reutilizan las mismas posiciones de memoria. De hecho, es el programador el que tiene que saber qué dato está almacenado y actuar en consecuencia, porque si no lo controla puede recuperar información sin sentido.

Otro buen ejemplo lo tenemos en el header file _vic2.h de cc65. En este header file se definen los tipos de datos que dan acceso a los registros del chip de vídeo del C64 (VIC o, mejor dicho, VIC-II). Por ejemplo, para acceder y/o modificar las posiciones de los sprites se usan las posiciones:

SP0X, SP0Y$d000 y $d001
SP1X, SP1Y$d002 y $d003
SP2X, SP2Y$d004 y $d005
SP3X, SP3Y$d006 y $d007
SP4X, SP4Y$d008 y $d009
SP5X, SP5Y$d00a y $d00b
SP6X, SP6Y$d00c y $d00d
SP7X, SP7Y$d00e y $d00f

Hay dos formas de ver o entender estos 16 bytes:

  • Como 16 posiciones independientes, cada una con su nombre y dirección.
  • O como un array de 8 parejas de posiciones (x, y).

Y esto, precisamente, es lo que hace _vic2.h mediante una unión:

  • Como 16 posiciones independientes, cada una con su nombre y dirección:
  • O como un array de 8 parejas de posiciones (x, y):

Es decir, el primer miembro de la unión es la estructura de 16 posiciones independientes, y el segundo miembro de la unión es el array de 8 parejas (x, y).

Se puede acceder a las posiciones de los sprites de la primera forma, mediante sus nombres (spr0_x, spr0_y, etc.), o de la segunda forma, mediante el array (spr_pos[i].x, spr_pos[i].x, etc.), pero en ambos casos nos estamos refiriendo a las mismas posiciones de memoria.

En definitiva, las uniones son formas alternativas de utilizar unas mismas posiciones de memoria. Por lo demás, las uniones tienen usos muy parecidos a los de las estructuras, con “.” y “->” para acceder a los miembros, permitiéndose inicialización, asignación, paso por valor y referencia a funciones, etc.

typedef:

En C hay tipos de datos simples (char, int, float y double) y tipos de datos compuestos o estructurados (estructuras y uniones). En ambos casos es posible usar typedef para definir sinónimos para los tipos de datos.

Por ejemplo, con estos usos de typedef:

conseguimos que “byte” sea equivalente a “unsigned char” y que “Fecha_ddmmaa” sea equivalente a “struct ddmmaa”.

Esto es especialmente útil en el caso de estructuras y uniones porque, como se ha visto en los apartados anteriores, al definir estructuras y uniones en realidad estamos definiendo tipos, y los nombres de esos tipos son “struct ddmmaa” y “union fecha”. Con typedef es posible tener nombres de tipos más compactos.


Código de ejemplo: estructuras

Repaso de C: punteros, arrays y cadenas

Punteros:

Los punteros son variables que contienen la dirección de una posición de memoria, que típicamente almacenará otra variable, pero que también puede ser la dirección de memoria de un registro, por ejemplo, un registro del VIC, la dirección del comienzo de la pantalla ($0400), o lo que sea.

El operador & sirve para obtener la dirección de memoria de una variable. Por ejemplo:

int variable1;

int *puntero = &variable1;

En el caso particular del C64, como las direcciones son de 16 bits (desde $0000 hasta $ffff), y como los int de cc65 también ocupan 16 bits, una dirección o puntero viene ser básicamente lo mismo que un int.

Pero, más allá de lo anterior (que un puntero es o almacena una dirección de memoria), al nivel de C, un “puntero” no es una dirección arbitraria sin más, sino que es una dirección que apunta a un double, o a un char, o a un int, etc. Adonde quiero llegar, los punteros en C tienen un tipo asociado, son punteros que apuntan a un tipo de variable concreto. Esto es lo que luego va a permitir la “aritmética de punteros”, es decir, va a permitir hacer cosas como:

puntero = puntero + 1;

sabiendo que ese +1 no es la dirección o el byte siguiente en sentido literal, sino la posición siguiente tiendo en cuenta el tamaño de las variables apuntadas, que en el caso de un char es 1 byte, pero que en el caso de otros tipos serán tamaños mayores.

Por otro lado, el operador * es el operador de indirección, es decir, aplicado sobre un puntero nos da el contenido de la variable o posición de memoria apuntada por él:

int variable2 = *puntero;

También es posible hacer a la inversa, es decir, almacenar cierto contenido en la posición o variable apuntada por un puntero:

*puntero = variable2;

E, incluso, es posible asignar valores entre punteros, es decir, conseguir que varios punteros apunten a la misma dirección:

puntero1 = puntero2;

Por último, en ocasiones es necesario o conveniente indicar que un puntero no apunta a nada, o no apunta a nada válido. En tal caso, puede usarse la dirección 0 o, lo que es lo mismo, la constante simbólica NULL definida en <stdio.h>.

Punteros y funciones:

Ya dijimos en su momento que los argumentos o parámetros de las funciones en C se pasan “por valor”, es decir, en una función del tipo int doble(int x), cuando se llama con doble(x), si x vale 7, la función tiene una variable local también llamada x, que inicialmente vale 7, pero que es independiente de la variable x del programa o función llamante. Este “paso por valor” es la regla general.

Ahora bien, el caso de los punteros es especial. Los punteros también pueden ser parámetros de funciones y, como tales punteros, también se pasan “por valor”, es decir, la función tendrá otro puntero local con la misma dirección.

Pero como este segundo puntero, el local a la función, apunta a la misma dirección que el argumento pasado, en el fondo, a través de esa dirección, es posible modificar el valor de la variable apuntada. Esto es lo que se llama “paso por referencia”, porque lo que sea pasa en realidad es una referencia o puntero a la variable original.

En definitiva, cuando en C se pretende que una función pueda modificar una variable pasada a una función, el paso tiene que ser por puntero o referencia.

Otra cuestión interesante, relativa a punteros y funciones, es que, al ser un puntero en esencia una dirección, esa dirección no sólo puede apuntar a una variable, sino a una posición de memoria arbitraria (ej. un registro del VIC o del SID). En particular, también puede apuntar a una dirección de memoria que tenga código ejecutable, no datos. Es decir, que un puntero pueda apuntar al comienzo de una función. Sabiendo ensamblador esto se ve muy claro, ya que una posición de memoria (o una etiqueta de un macroensamblador) puede contener datos o código ejecutable.

Esto permite en C hacer cosas bastante abstractas, como algoritmos de ordenación (ordenar básicamente es comparar elementos e intercambiarlos si es necesario) en los que es posible cambiar, mediante punteros a diferentes funciones, la función de comparación y/o la función de intercambio.

Arrays:

Los arrays son posiciones consecutivas de memoria que almacenan una secuencia de valores del mismo tipo. El array se maneja o referencia mediante el puntero a su primera posición. De ahí la relación estrecha entre punteros y arrays en C.

Por tanto, los punteros y los arrays en C son construcciones muy parecidas, siendo la principal diferencia que los arrays admiten una sintaxis específica, por ejemplo:

unsigned char buffer[10];

Esto declara un array llamado “buffer” que tiene 10 posiciones, y cada una de esas 10 posiciones almacena un unsigned char, es decir, un byte.

Además, si se quiere acceder a la posición i-ésima del array esto se puede hacer con un índice:

unsigned char mi_char;

mi_char = buffer[i];

Pero la relación entre arrays y punteros no se limita a la primera posición del array (ej. puntero = &buffer[0]). Un puntero del tipo adecuado puede apuntar a cualquier posición intermedia del array (ej. puntero = &buffer[i]) e, incluso, mediante aritmética de punteros (ej. puntero = puntero+1), es posible recorrer el array y acceder a los valores de las diferentes posiciones (ej. *(puntero+1)). Y todo esto, además, teniendo en cuenta que ese +1 no se refiere literalmente al byte siguiente, sino a la posición siguiente teniendo en cuenta el tamaño (1 byte, 2 bytes, 3 bytes, etc.) de las posiciones del array.

Los arrays, de hecho, cuando son parámetros de funciones se pasan por referencia, puesto que en el fondo el nombre del array equivale a un puntero a su primera posición.

Cadenas de caracteres:

Las cadenas de caracteres en C son arrays de caracteres que terminan con el carácter cero (‘\0’). Por tanto, la cadena “Hola mundo” equivale al array de caracteres:

Hola mundo\0

La principal novedad, como ya se ha visto, es que las cadenas de caracteres tienen una sintaxis especial para poder ser definidas:

char cadena1[] = “Hola mundo”;

De hecho, como el tamaño del array está implícito (10 caracteres + 1 carácter por el 0 final), ni siquiera hace falta especificarlo. Esto viene a ser equivalente a la sintaxis para inicializar arrays basada en llaves:

char cadena2[] = {‘H’, ‘o’, ‘l’, ‘a’, ‘ ’, ‘m’, ‘u’, ‘n’, ‘d’, ‘o’, ‘\0’};

Las principales diferencias son que se usan comillas en vez de llaves y que, con comillas, no hace falta especificar el ‘\0’ final. Se da por hecho que es una cadena de caracteres y, por tanto, que termina con el carácter cero.

En definitiva, los punteros y los arrays son construcciones muy parecidas en C, y las cadenas de caracteres no dejan de ser arrays (de caracteres) que terminan con el carácter cero.

Paso de parámetros a un programa en C:

Ahora que sabemos cómo funcionan los arrays y las cadenas, la forma de hacer llegar uno o varios parámetros a un programa en C, es decir, a su función main(), es mediante los parámetros opcionales argc y argv[]:

void main(int argc, char *argv[]) {…}

El entero argc nos dice el número de parámetros o argumentos efectivamente pasados, y el array argv[] nos da acceso a esos argumentos, que en el fondo son cadenas de texto, es decir, punteros a caracteres (char *).

Por último, recordemos que la forma de ejecutar un programa cc65 y pasarle parámetros es con la sintaxis:

RUN : REM ARG1 “ARG 2” ARG3 “ARG 4” …


Código de ejemplo: punteros

Repaso de C: ficheros de cabecera y preprocesador

Ficheros de cabecera:

Los ficheros de cabecera o “header files” ya se han introducido en entradas anteriores, concretamente al revisar las librerías de cc65 para hacer entrada / salida, manejar un joystick, un ratón, gráficos bitmap, sprites o sonido.

Estos ficheros sirven para recoger definiciones de constantes simbólicas, prototipos de funciones, tipos de datos, macros, etc. Son útiles especialmente en el contexto de programas grandes que necesitan ser troceados en varios ficheros, o que utilizan librerías propias o ajenas.

De este modo, es posible concentrar esas definiciones comunes en uno o varios ficheros *.h, y luego referenciar esos ficheros y sus definiciones desde los ficheros *.c que las utilizan, evitando así los errores que podrían derivarse de tener que replicarlas en varios sitios.

Los ficheros de cabecera guardan mucha relación con el preprocesador de C. En primer lugar, porque muchas de las definiciones que incluyen, por ejemplo, una constante simbólica o una macro, se definen mediante directivas del preprocesador (ej. #define SPRITES_NUM 8). Y, en segundo lugar, porque para referenciar o incluir un fichero de cabecera en un fichero de implementación también se utilizan las directivas del preprocesador #include <fichero.h> o #include “fichero.h”.

La directiva #include, como su nombre indica, lo que hace es incluir las definiciones del fichero de cabecera en el fichero de implementación, justo antes de que éste se compile.

Preprocesador:

El preprocesador también lo hemos introducido en entradas anteriores. Como hemos adelantado, “es un paso previo a la compilación”.

Las dos directivas más habituales del preprocesador son #define, para definir constantes simbólicas y macros, e #include, para incluir el contenido de un fichero en otro. Ambas han sido presentadas también.

#define no sólo vale para definir constantes simbólicas, es decir, literales que se sustituyen por un valor (ej. #define SPRITES_NUM 8). También vale para definir macros.

Las macros son básicamente lo mismo que una constante, pero con algunos parámetros. Por ejemplo, si definimos #define MULTI(A, B) ((A)*(B)), cada vez que el preprocesador se encuentre MULTI(A, B) en el código fuente, lo sustituirá por ((A)*(B)), donde A y B lógicamente son parámetros. Es decir, MULTI(2, 4) se sustituiría por ((2)*(4)).

No es obligatorio abusar tanto de los paréntesis, pero es habitual hacerlo para garantizar la correcta asociación en caso de que los valores de A o B no sean simples (ej. si A = 1+1).

Una macro se parece a una función, porque tiene parámetros, pero no es lo mismo. Cada aparición de la macro se sustituye por su definición en tiempo de preprocesamiento, justo antes de compilar. Por tanto, el concepto es más similar al de una macro de un macroensamblador (defm – endm en CBM prg Studio).

Por otro lado, #include <fichero.h> sirve para buscar fichero.h en la ruta donde el compilador guarda por defecto las librerías, e incluirlo en el fichero que se está procesando. En el caso de cc65 esa ruta por defecto es cc65\include.

La variante #include “fichero.h” hace lo mismo, pero busca el fichero de cabecera primero en la ruta actual o de trabajo, es decir, donde se encuentra el fichero que se está procesando o compilando en ese momento. Si no lo encuentra ahí, busca en la ruta por defecto.

El preprocesador vale para más cosas, por ejemplo, para hacer inclusión condicional. Con una estructura como esta en un fichero de cabecera:

es posible evaluar una condición, concretamente si está definida la constante CONST, y actuar en consecuencia. En este ejemplo, si CONST no está definida se define y, además, se definen las constantes, macros, prototipos y demás construcciones que fueran de interés.

Estos controles son bastante habituales en los ficheros de cabecera. Ha de tenerse en cuenta que algunos ficheros de cabecera incluyen a otros, y estos a otros, por ejemplo, cabeceras de librerías estándar, y al final el galimatías de referencias cruzadas acaba siendo muy complejo. Por ello, para evitar que el preprocesador incluya varias veces las mismas definiciones, se verifica si una bandera (CONST) ya está definida y, caso de ya estarlo, no se hace nada; si no está definida, se define y se acompañan el resto de definiciones.

#ifdef CONST e #ifndef CONST, sin paréntesis, son versiones abreviadas de #if defined(CONST) e #if !defined(CONST) respectivamente.


Código de ejemplo: macros

Repaso de C: reglas de alcance y variables estáticas

Reglas de alcance:

Las reglas de alcance son las reglas que determinan en qué parte de un programa en C se ve o se puede usar un nombre de variable o función. Recordemos que un programa puede constar de varios ficheros fuente, no sólo uno.

Como regla general, en C no se puede referenciar nada que no haya sido declarado antes. Esto es así porque el compilador de C sólo hace una pasada. Por tanto, cuando el compilador de C está a medias de procesar un fichero fuente, sólo conoce lo declarado desde el comienzo del fichero hasta ese punto.

Si se trata de un parámetro de entrada o de una variable local declarada al comienzo de una función, el alcance es la propia función. Si se trata de una variable local declarada a mitad de la función, el alcance es desde ahí hasta el final de la función.

Si se trata de una variable externa o una función, el alcance es desde el punto en que están declaradas hasta el final del fichero en que se encuentran.

Si se quiere usar una variable externa declarada en otro fichero, entonces hay que usar extern para indicar que se trata de una variable global, y por tanto accesible, pero declarada en otro sitio.

Variables y funciones estáticas:

Las reglas de alcance anteriores se pueden modificar con la palabra reservada static, que se puede aplicar a variables y a funciones.

Cuando una variable global / externa se declara como static, entonces esa variable sólo está disponible desde las funciones del archivo en que está declarada. Es una forma de limitar el alcance de las variables globales.

Igualmente, cuando una función se declara como static, ésta sólo se puede llamar desde el fichero en que está implementada. Nuevamente, es una forma de limitar el alcance de las funciones.

Por último, cuando la palabra reservada static se aplica a una variable local a una función, lo que ocurre es que esa variable mantiene su valor entre llamadas.


Código de ejemplo: alcance

Repaso de C: funciones, variables externas y estructura de un programa

Funciones:

Una función es un conjunto de instrucciones relacionadas entre sí y que implementan una funcionalidad concreta y autocontenida.

Las funciones constan de:

  • Un prototipo, que es opcional.
  • Una definición o implementación.

El prototipo sólo es necesario si la función se utiliza, es decir, se llama, antes de ser definida. Consiste en el nombre de la función, su valor de retorno y sus parámetros. Los parámetros se suelen documentar con tipo y nombre, aunque para el prototipo es suficiente con el tipo.

Un ejemplo de prototipo de función sería así:

Y luego está la definición o implementación de la función, que es la función propiamente dicha. La definición consta de nombre, valor de retorno, parámetros (tipo y nombre), y cuerpo, que constará de un conjunto de instrucciones.

No todos estos elementos son obligatorios. En realidad, sólo el nombre lo es. El retorno, los parámetros, e incluso el cuerpo son opcionales (aunque no tiene mucho sentido una función vacía, es posible hacerla).

Un ejemplo de implementación de función sería así:

No es lo mismo el tipo void, que significa que no hay parámetros o retorno, que no poner tipo, en cuyo caso se sobreentiende que es int. No se considera buena práctica apoyarse en estos tipos implícitos.

Lógicamente, la implementación tiene que concordar con el prototipo, en el sentido de usar el mismo nombre de función y los mismos tipos de parámetros y retorno. Si no fuera así, el compilador se quejaría.

La comunicación entre la función llamante y la función llamada es mediante los parámetros y el retorno. Al llamar, los parámetros se convierten en variables locales a la función, a las que se copia el valor de los parámetros (paso por valor, con la excepción de los arrays y las cadenas que se pasan por referencia o puntero).

La comunicación en sentido contrario es mediante el retorno, que se produce cuando la función ejecuta la instrucción return. La función llamante puede usar ese retorno o prescindir de él.

Variables externas:

Las variables externas ya las hemos introducido en alguna entrada anterior. Son variables que no se definen dentro de una función y, por tanto, son globales al programa. En principio, son visibles en todas las funciones del programa, salvo que se declaren como static, en cuyo caso sólo se ven desde el fichero en que están declaradas.

Las funciones son, en cierto modo, siempre externas, ya que C no permite definir funciones dentro de otras funciones.

Otra variante del main() anterior usando una variable externa sería así:

Como se puede observar, la variable externa o global a es visible desde main().

Por tanto, otra forma de comunicación entre un programa o función llamante y una función llamada es mediante las variables externas que, al ser globales al programa, son visibles desde todas las funciones (en principio).

Estructura de un programa en C:

Las funciones son la abstracción principal de un programa en C. Permiten dividir un problema complejo en varios problemas más sencillos, reutilizar código y crear niveles de abstracción.

Por tanto, un programa en C básicamente consiste en un conjunto de variables externas y funciones que se organizan en uno o varios ficheros, que se compilan por separado.

Una de esas funciones debe ser la función main(), que es donde empieza la ejecución del programa.


Código de ejemplo: funciones

Repaso de C: control de flujo

En C las instrucciones o proposiciones se terminan con ; . Las instrucciones se pueden agrupar con { … } para dar lugar a proposiciones compuestas o bloques.

Las instrucciones de control de flujo son las que permiten tomar decisiones y controlar por dónde fluye la ejecución del programa. En el caso de C son:

  • if – else if – else.
  • switch.
  • while.
  • for.
  • do – while.
  • break y continue.
  • goto y etiquetas.

Lo más simple es ver un ejemplo de cada tipo:

if – else if – else:

La instrucción if evalúa una condición y, si se cumple, ejecuta el bloque que sigue. Este bloque puede ser simple, en cuyo caso no hacen falta llaves, o compuesto:

En caso de que no se cumpla la condición, tenemos la rama else opcional, que también puede tener un bloque simple o compuesto:

Y si se quieren evaluar varias condiciones seguidas, tenemos else if:

switch:

La instrucción switch es similar al if – else if – else, es decir, también vale para decisiones múltiples. La principal diferencia es que, en vez de evaluar una expresión arbitraria y decidir si es cierta (!= 0) o falsa (== 0), como el if, compara la expresión, normalmente una variable, con un conjunto limitado de valores constantes.

Por ejemplo, la instrucción switch:

es básicamente equivalente a if (a == 0) {…} else if (a == 1) {…} else if (a == 2) {…} else {…}.

Sí hay un matiz importante, y es que en un if – else if – else sólo se ejecuta una de las ramas. Sin embargo, en un switch se evalúan y ejecutan todas las ramas case a partir de la coincidencia, salvo que de forma explícita se decida salir del switch con un break. La instrucción break también se utiliza para salir de bucles while, for y do – while.

while:

La instrucción while sirve para hacer bucles. La condición de terminación del bucle se evalúa primero, y el bucle se ejecuta hasta que deja de cumplirse:

Cuando el cuerpo del while es una proposición simple también se pueden obviar las llaves. Esto es una norma general de todas las instrucciones de control de flujo.

for:

La instrucción for también vale para implementar bucles. Es un poco más compleja que while, porque tiene una inicialización, una condición de terminación del bucle, y una expresión que se ejecuta al final de cada iteración, y que típicamente se utiliza para incrementar o decrementar un contador.

Lo bueno del for es que todo lo que controla el bucle, la inicialización, la condición de terminación / continuación, y el cambio de una iteración a la siguiente, están claros y en el mismo sitio.

do – while:

El do – while es el tercer tipo de bucle en C. A diferencia del while, primero se ejecuta el cuerpo el mismo, y luego se comprueba la condición. Por tanto, un do – while tiene sentido cuando el bucle tiene que ejecutarse al menos una vez:

Hay bucles, sin embargo, que tienen que ejecutarse 0 o más veces, en función del valor de una variable. Para estos es mejor un while.

break y continue:

En todos los tipos de bucles cabe la opción de salir del mismo. Esto se hace con la instrucción break, que ya se presentó con el switch.

Igualmente, también cabe la opción de saltar a la siguiente iteración, lo cual se hace con la instrucción continue. Esto no tiene sentido en un switch:

goto y etiquetas:

Por último, C soporta etiquetas y goto. Las etiquetas son como las etiquetas de ensamblador: sirven para marcar posiciones en el código. Se nombran como las variables (letras, números y algunos símbolos), pero tienen que terminar con : .

Por su parte, el goto es una instrucción que hace saltar la ejecución a una etiqueta. El goto y la etiqueta tienen que estar en la misma función:

En general, el goto está desaconsejado, porque todo lo que se puede escribir con goto se puede también escribir sin él, resultando más fácil de entender y mantener.


Código de ejemplo: control_flujo

Repaso de C: operadores y expresiones

Operadores y operandos:

Como en el caso de las variables, todo programador tiene una idea intuitiva de qué es un operador: es una operación que relaciona unos operandos (variables o constantes) para devolver un valor.

En C, como en otros lenguajes, hay muchos tipos de operadores:

  • Operadores aritméticos: Sirven para realizar operaciones aritméticas. Son +, -, *, / y % (resto de la división entera). No hay exponenciación.
  • Operadores de relación: Sirven para comparar. Son >, >=, <, <=, == y !=. No se debe confundir la igualdad (==) con la asignación (=).
  • Operadores lógicos: Sirven para realizar operaciones lógicas o booleanas, es decir, && (AND), || (OR) y ! (NOT). En C no existe el tipo boolean, así que se toma 1 como true (realmente distinto de 0) y 0 como false.
  • Operadores de incremento y decremento: Son ++ y –, y sirven para incrementar o decrementar una variable. Lo curioso es que pueden usarse como prefijo o sufijo, hablándose de preincremento, postincremento, etc. En la versión prefijo primero se incrementa o decrementa y luego se devuelve el valor; en la versión sufijo primero se devuelve el valor y luego se incrementa o decrementa.
  • Operadores de manejo de bits: Son & (AND), | (OR), ^ (OR exclusivo), << (desplazamiento a la izquierda), >> (desplazamiento a la derecha) y ~ (complemento a uno o negación). Como se puede ver, C es un lenguaje de alto nivel, pero no tanto, porque se puede operar directamente con bits.
  • Operadores de asignación: El operador de asignación por excelencia es el =, que asigna un valor a una variable. Pero hay más, porque la mayoría de operadores binarios (+, -, *, /, %, …) tienen también un operador de asignación asociado (+=, -=, *=, /=, %=, …). Por ejemplo, la asignación i = i + 2 también puede expresarse como i += 2.

Hay más operadores, como los que tienen que ver con estructuras (. y ->), con arrays ([]), con direcciones y punteros (* y &), con funciones (paréntesis), con conversión de tipos o “casting”, sizeof para obtener el tamaño de una variable, etc. Todos estos operadores se verán más adelante.

Lo que sí es bastante complejo son las reglas de precedencia y asociación de los operadores de C (extracto del libro de Kernighan & Ritchie):

Expresiones:

Las expresiones son un tipo especial de instrucción que devuelven un valor. Por ejemplo, todas las operaciones vistas anteriormente (aritméticas, de relación, booleanas, de manejo de bits, etc.) conforman expresiones, porque devuelven un valor.

En particular, las asignaciones no sólo almacenan un valor en una variable; además, devuelven ese valor para hacer más cosas con él, si se quiere. Por este motivo se pueden hacer cosas como i = j = 0. Esto no sólo asigna 0 a la variable j, sino que además devuelve el valor 0, que se asigna a la variable i. Es decir, la asignación j = 0 es, además, una expresión.

Hay un tipo especial de expresión que es la expresión condicional. Ésta verifica una condición y, en función del resultado, devuelve un valor u otro. Se expresa con el operador ternario ? : . Por ejemplo, (a > b) ? a : b devuelve a si a > b y b en caso contrario.


Código de ejemplo: operadores

Repaso de C: variables, tipos, constantes y declaraciones

Variables:

Todo programador tiene una idea intuitiva de qué es una variable: un trocito de memoria que guarda un valor. Las variables tienen:

  • Una dirección de memoria.
  • Un nombre.
  • Un valor, que puede cambiar, por eso se llama variable.
  • Un tipo.

En C, los nombres de las variables tienen que empezar por una letra, y pueden contener letras, algunos símbolos y números. Por supuesto, el nombre no puede ser una palabra reservada del lenguaje. Las mayúsculas y las minúsculas se consideran diferentes. Las variables se suelen nombrar con minúsculas y las constantes simbólicas con mayúsculas.

Tipos de datos:

Las variables tienen un tipo, que define el conjunto de valores que pueden tomar. Los tipos de datos pueden ser básicos o estructurados.

Los tipos básicos son los más sencillos, los que aporta el lenguaje de forma nativa. En el caso de C son:

  • char.
  • int.
  • float (no soportado en cc65).
  • double (no soportado en cc65).

Estos tipos básicos se pueden modificar con los calificadores:

  • short y long.
  • signed y unsigned.

El primero hace referencia al tamaño en bytes, y el segundo al carácter con signo / sin signo.

Una curiosidad de C es que no fija el tamaño en bytes de sus tipos básicos o primitivos. Sólo fija que un short tiene que ser menor que un int, que tiene que ser menor que un long. Esto ha dificultado su portabilidad entre máquinas.

Constantes y constantes simbólicas:

Las constantes –o valores constantes– son valores fijos que se utilizan en inicializaciones, expresiones, llamadas a funciones, etc. Las constantes también tienen un tipo, que puede ser el natural a partir de su sintaxis o se puede forzar mediante sufijos. Lo mejor es ver ejemplos:

  • 1234 es una constante de tipo int.
  • 123456789L es una constante de tipo long.
  • 123.4 es una constante de tipo double.
  • ‘A’ es una constante de tipo char.
  • “programa” es una constante de tipo array de char (cadena).
  • Etc.

Las constantes de tipo int se pueden expresar en decimal, en octal empezando por 0, o en hexadecimal empezando por 0x.

Los caracteres especiales se pueden representar mediante secuencias de escape. Por ejemplo, la nueva línea se puede representar con \n, la barra invertida con \\, etc. También se puede usar \xHH, donde HH es el valor ASCII en hexadecimal.

Con la instrucción del preprocesador #define es posible definir constantes simbólicas, es decir, literales que equivalen a un valor fijo. Por ejemplo, #define SPRITES_NUM 8 hace que el literal SPRITES_NUM equivalga a 8.

También es posible definir enumeraciones, por ejemplo:

enum meses {ENE=1, FEB, MAR, ABR, MAY, JUN, JUL, AGO, SEP, OCT, NOV, DIC}

lo cual equivale a definir ENE = 1, FEB = 2, MAR = 3, etc.

Declaraciones:

Para poder usar una variable, antes hay que declararla. Al declararla se indica:

  • Su tipo.
  • Su nombre.
  • Y una inicialización opcional.

Por ejemplo:

  • int contador = 0;
  • char car = ‘a’;

En función de donde se declaren, las variables pueden ser locales a una función o globales al programa, que en C se llaman variables “externas”.

Las variables locales, si no se inicializan, contienen basura. Y si están inicializadas, se inicializan en cada ejecución de la función (salvo que se declaren static, como veremos más adelante).

Las variables globales se inicializan sólo una vez antes de iniciarse la ejecución del programa. Se inicializan a 0 salvo que tengan una inicialización explícita.

En una declaración se puede usar el calificador const para indicar que una variable no cambia su valor. Sería una variable constante, si vale la paradoja. También se puede usar const en los parámetros de las funciones.


Código de ejemplo: variables

Repaso rápido del lenguaje de programación C

Llegados a este punto, y de cara a desarrollar un posible proyecto en C para el C64, lo suyo sería repasar de forma breve, aunque sistemática, el lenguaje de programación C.

Y la biblia de C ya sabéis cual es, el libro escrito hace décadas por los diseñadores del lenguaje (Brian Kernighan y Dennis Ritchie). Este libro ha tenido varias ediciones según ha ido evolucionando el lenguaje y, en particular, por su proceso de estandarización en ANSI:

Siguiendo un poco la estructura de este libro, los aspectos principales a revisar serían:

  • Variables.
  • Tipos de datos.
  • Constantes.
  • Declaraciones.
  • Operadores.
  • Expresiones.
  • Control de flujo.
  • Funciones.
  • Variables externas.
  • Estructura de un programa C.
  • Reglas de alcance.
  • Variables estáticas.
  • Ficheros de cabecera.
  • Preprocesador.
  • Punteros.
  • Arrays.
  • Cadenas de caracteres.
  • Estructuras.
  • Uniones.
  • typedef.
  • Librerías estándar.

Por supuesto, el lenguaje C tiene muchos más aspectos, pero lo anterior es lo fundamental. Sólo eso ya dará para varias entradas.