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