Lenguaje C

¿Qué signfica que un lenguaje sea, como C, compilado?

Significa que para ejecutar nuestro programa se lo debe primero traducir a código binario que la CPU de la computadora pueda entender. Una vez traducido, el resultado (las instrucciones binarias) se pueden enviar a la CPU para su ejecución.

A ese paso de traducción le llamamos compilación .

Es por esto que las instrucciones del TP0 consisten en dos pasos. Primero, la compilación:

gcc -std=c99 -Wall -pedantic -Werror -o pruebas *.c

(El compilador que empleamos se llama GCC.)

Esto produce en nuestro directorio de trabajo un archivo nuevo llamado pruebas (pero podríamos llamarle cualquier otro nombre que quisiéramos).

A continuación, le podemos decir al sistema operativo que corra ese código binario que se encuentra en el archivo nuevo:

./pruebas

Solo en este segundo paso veríamos si nuestro código compilado hace lo que se espera.

En el primer paso, el compilador solamente detecta errores de sintaxis, o errores simples (advertencias) que el compilador piensa que pueden conducir a errores de comportamiento.

¿Cómo hago para compilar varios archivos?

Para compilar solo un archivo (por ejemplo, pruebas.c) se utiliza:

gcc -std=c99 -Wall -pedantic -Werror -o pruebas pruebas.c

Acá, pruebas.c debe contener la función main, que es la que ejecutará.

Por otro lado, para compilar varios archivos en un solo programa se utiliza:

gcc -std=c99 -Wall -pedantic -Werror -o pruebas pruebas1.c pruebas2.c

Para no especificar cada archivo del proyecto, puedo simplemente llamar a todo archivo .c del directorio, con el wildcard (comodín) *.c

gcc -std=c99 -Wall -pedantic -Werror -o pruebas *.c

¿Cómo hago para tener una función que usa otra, pero está más adelante en el código?

Se puede incluir el prototipo de la función para 'avisarle' al compilador que se va a encontrar con la definición de la función más adelante:

void f(int x);

int main(){ f(3); return 0; }

void f(int x){ ... }

De esta forma, el compilador puede hacer el chequeo de tipos sin conocer el cuerpo de la función.

Esto es lo que se llama una declaración de una función de C.

¿Qué son los archivos .h?

Un archivo header es un archivo que contiene declaraciones de funciones que utilizaremos en el programa.

Es con estas cabeceras que relacionamos dos archivos .c. Ya que un archivo siempre debe conocer el prototipo de una función que utiliza, un archivo debe incluir la cabecera de otro para poder hacer uso de sus funciones.

Por ejemplo, teniendo un archivo principal que imprime hola y un archivo que imprime mundo, desde nuestro archivo principal hola.c debemos incluir la función de nuestro segundo archivo. Esto se hace con el comando al preprocesador de C, #include "mundo.h" en las primeras lineas del programa. Al compilar, tendremos que compilar al mismo programa tanto el archivo hola como el archivo mundo. No deben incluirse los archivos .h en la compilación.

También el mismo archivo mundo.c debe incluir a su propia cabecera, para poder ahorrarse el pensar en el orden que tiene que definir las funciones que va a utilizar. Ya que al incluir la cabecera el compilador sabrá todas las declaraciones de sus funciones, luego todas pueden ser escritas en el orden que sea, sin restricciones de declarar una función antes de utilizarla.

Hacemos una distinción del comando de inclusión:

  • #include <archivo.h>: Incluye cabeceras del lenguaje C, para poder hacer uso de funciones como printf, puts y otras.

  • #include "archivo.h": Incluye cabeceras en nuestro mismo directorio.

Estos archivos también funcionan como documentación del programa. ¿Qué puede usar el usuario de mi programa? ¿Qué tiene a su disposición? ¿Qué hace cada función?

Es decir, este archivo es la interfaz que se proporciona junto al programa. Por ejemplo, al hacer un juego tendremos varias funciones. Muchas de estas serán públicas (por ejemplo, jugar, con su respectiva documentación), ya que son las que le daremos al usuario, pero también algunas son privadas (por ejemplo, calcular_puntaje). Son las funciones públicas las que se declaran en el archivo header.

¿Cómo hago para que gcc escupa los mensajes de error en inglés?

Cuando gcc (o cualquier otro programa) imprime un mensaje de error en castellano e intentamos buscarlo en internet porque no lo entendemos, es probable que encontremos poca información. Lamentablemente hay muchísima más información en inglés que en castellano.

Por lo tanto, es una buena idea obligar a un programa a imprimir los mensajes en el idioma original antes de buscar en internet:

$ LANG=C programa

Por ejemplo:

$ gcc noexiste.c
gcc: error: noexiste.c: No existe el fichero ó directorio
gcc: error fatal: no hay ficheros de entrada
$ LANG=C gcc noexiste.c
gcc: error: noexiste.c: No such file or directory
gcc: fatal error: no input files

¿Qué significa el error "assignment discards qualifiers from pointer target type"?

Significa que con la asignación se está descartando un calificador de un puntero. Particularmente se suele dar cuando el dato recibido es un const y se lo asigna a una variable (o miembro de una estructura) que no es const, por lo que se le estaría dando luz verde para que se cambie, ya que a través de un puntero se puede cambiar el dato.

¿Para qué se usa el tipo size_t?

size_t es un tipo entero sin signo devuelto por el operador sizeof y es usado para representar el tamaño de construcciones en bytes. Este tipo está definido de manera tal de garantizar que siempre va a poder almacenar el tamaño del tipo más grande posible, por lo que también garantiza que va a poder almacenar cualquier índice de cualquier arreglo.

Estas características lo convierten en el tipo adecuado para manejar tamaños e índices.

¿Cómo comparo si dos cadenas son iguales?

En C las cadenas de caracteres son vectores que tienen caracteres como elementos. C no sabe comparar vectores, de ningún tipo. Al hacer una comparación del tipo: cadena1 == cadena2, lo que se compara es que las direcciones de memoria de ambas variables sean iguales, es decir que sólo van a ser iguales cuando realmente sean el mismo puntero.

Para poder comparar el contenido de dos cadenas, es necesario usar la función strcmp(cadena1, cadena2) de la biblioteca string.h, que devuelve 0 si son iguales, menor que 0 si la primera es menor y mayor que 0 si la primera es mayor. En este caso no importa que las cadenas ocupen o no la misma porción de memoria.

¿Cómo copio dos cadenas?

La sentencia:

char* cad_1 = cad_2;

No crea una copia de una cadena, sino una copia de la referencia a la cadena. Para hacer una copia de una cadena es necesario hacer:

strcpy(buf_destino, cad_origen);

Siendo buf_destino una posición de memoria tal que pueda albergar la cadena a copiar, típicamente reservada con malloc y strlen (teniendo en cuenta el espacio necesario para alojar el fin de cadena) o asegurando que a tiempo de compilación se encuentre el espacio necesario para copiar la cadena en un arreglo estático de tamaño fijo.

¿Por qué mis printf no se imprimen?

La convención en Unix es que la entrada estándar y la salida estándar tengan un búfer asociado. Esto significa que al utilizar printf de la manera normal, no se imprime inmediatamente a la consola: sólo sucede cuando suficientes mensajes están esperando ser impresos. Por esto, si el programa se queda en un ciclo infinito o se termina por algún error entre que llamamos a printf y que el búfer se vacía, nuestro mensaje no se muestra.

La solución es utilizar un canal específico para los errores, que no tiene un búfer asociado: stderr. Se puede utilizar como cualquier archivo llamando a fprintf:

fprintf(stderr, ...);

¿Qué es typedef?

Typedef es una característica de C que nos permite darle un alias a cualquier tipo de C. Por ejemplo, si quisiéramos representar la edad de una persona podríamos querer abstraernos de si representamos esa edad como un entero, un entero sin signo, un entero corto (short), etc. y además tener consistencia en todo nuestro código de usar siempre el mismo tipo para todas las variables que representen una edad. Para esto podemos 'crear' un tipo nuevo en C que se refiera a la edad de las personas, supongamos que como base quisiéramos hacer uso del tipo unsigned int para crear nuestro tipo edad, pero a su vez queremos dejarle explícito al lector que éste es un tipo definido por el programador, por lo que por convención le agregamos el sufijo _t (que se lee como "tipo") siendo nuestro nuevo tipo edad_t.

Para esto la sintaxis correspondiente es:

typedef unsigned int edad_t;

Siempre la sintaxis para definir un nuevo tipo en C es como si se tratara de declarar una variable con determinado nombre, pero anteponiendo la palabra reservada typedef. Entonces, en vez de declarar una nueva variable lo que se "declara" es un nuevo tipo, es decir: typedef tipo_existente mi_nuevo_tipo;.

Y luego podemos, como si fuese cualquier otro tipo de datos de C, declarar variables, hacer uso de ellas y demás como:

edad_t luis = 14;
// Equivale a unsigned int luis = 14

La ventaja de utilizar typedef es que se agrega una capa de abstracción sobre el tipo: en vez de preguntarme qué representa determinada variable de tipo unsigned int, el mismo tipo edad_t me aclara exactamente para qué sirve ese tipo. Sintácticamente escribir unsigned int y edad_t en mi código van a ser exactamente lo mismo, pero semánticamente son dos tipos totalmente diferentes y no son intercambiables. Además de la abstracción simplifica la mantenibilidad del código, por ejemplo, si el día de mañana hiciera la asunción de que la edad de las cosas no puede superar los 255 años, tranquilamente podría reemplazar la definición del typedef por un unsigned char y toda mi implementación se actualizaría consistentemente a esa nueva representación apenas modificando una línea en mi código.

Por otro lado la desventaja de utilizar typedef es que se enmascaran los tipos básicos. Entonces cuando vemos una variable de tipo edad_t tal vez en vez de abstraernos tenemos que ir a buscar su definición para saber si es un tipo entero, una estructura, etc. y cómo definirla o utilizarla. A veces menos es más.

typedef puede servirnos para otras cosas, como para omitir la palabra struct cada vez que hacemos una estructura en C, dándole ahora a nuestras estructuras el rango de tipo:

struct persona {
    edad_t edad;
    char *nombre;
};

typedef struct persona persona_t;

Cuando se utilizan estructuras para encapsular tipos abstractos de datos suele mantenerse de forma privada la definición de la estructura. En estos casos la definición de la estructura queda en el archivo .c mientras que la definición del tipo se publica en una cabecera .h, permitiendo que la interfaz pública del dato use el nombre del tipo.

Si se trata de un struct que no se expone públicamente (por ejemplo, por estar limitado a un único .c y declararse ahí), se puede combinar declaración y typedef en una sola instrucción:

typedef struct {
    edad_t edad;
    char *nombre;
} persona_t;

Como ejemplo también podemos hacer uso de typedef para simplificar la declaración de punteros a función:

// recibe_puntero_a_funcion es una funcion que recibe un puntero
// funcion_recibida es un puntero a funcion que recibe void* y devuelve bool

// Versión A, sin typedef.
int recibe_puntero_a_funcion(bool (*funcion_recibida)(void*));

// Version B, con typedef
// Hago un tipo de funciones, funcionvoidp_t que se refiere a las funciones que
// reciben void* y devuelven bool

typedef bool (*funcionvoidp_t)(void*);

int recibe_puntero_a_funcion(funcionvoidp_t funcion_recibida);