Programando correctamente en C

C es un lenguaje muy peculiar, tiene un bajo nivel de abstracción, es poderoso y mueve gran parte de los sistemas modernos. Sistemas como Linux y MacOS, proyectos como Nginx y Apache, y lenguajes como C++, PHP y Ruby están hechos en base a este lenguaje que, de hecho, es mi favorito.

Lamentablemente, debido a su bajo nivel de abstracción y altísima libertad, el lenguaje nos permite dispararnos en el pie... con una escopeta... con balas incendiarias.

C es un lenguaje extremadamente sencillo y podemos sacar provecho de eso para muchas cosas, incluyendo el evitar mutilarse sangrientamente mientras se programa en él, los tips que explicaré a continuación han sido redactados en base a mi experiencia con este lenguaje.

Manejo de cadenas

Lo primero a tomar en cuenta es cuidar las funciones relacionadas a las cadenas de texto, como muchos saben, en C una cadena de texto no es más que un arreglo de caracteres cuyo último carácter es null o '\0' el cual indica el final de la cadena.

char str[5] = {'H', 'o', 'l', 'a', '\0'};
//str es 'Hola'

El problema recae en lo que pasa si se maneja una cadena que no termine en null, lo que pasa aquí es que básicamente la computadora seguirá leyendo memoria pasada la cadena y leerá secciones de memoria inválidas, algo extremadamente peligroso y poco predecible.

La solución a esto es evitar las funciones como strcpy o strcat, ya que estas funciones no toman en cuenta el tamaño del arreglo destino, pudiendo causar un desbordamiento del buffer, muchos dicen que una solución es usar el equivalente de estas funciones que toman el número de caracteres a leer como strncpy o strncat, pero esto si bien es más seguro, sigue siendo peligroso ya que si a la función se le pasa un arreglo de caracteres más corto que la cantidad de caracteres que se especifica manejar, la función no asegurara que el arreglo destino termine en null.

¿Solución definitiva? Usar funciones como snprintf las cuales aseguran que el arreglo destino siempre termine en null, además, funciones como esta se pueden aprovechar para dar formato al texto.

strcpy(str1, str2); //Peligroso si str1 no termina en null

strncpy(str1, str2, 9); //Peligroso si str1 no tiene tamaño suficiente para esos 9 caracteres

snprintf(str1, 9, "%s", str2); //Mucho mas seguro.

Memoria

Otro consejo muy importante es liberar toda la memoria almacenada de forma dinámica (o sea almacenada con las funciones de malloc, realloc, calloc…), esto tiene que ver más con un principio ético que otra cosa, sobre todo en estos tiempos (a menos que vayas a crear un programa que esté en ejecución constante claro), si no liberas memoria en tu programa no hay mucho problema, es casi seguro que el mismo sistema operativo la libere cuando termine la ejecución del programa o el proceso, pero igual, por principio muy ético libera toda la memoria.

char *str = malloc(6);

//...

free(str); //Bien hecho

Comportamiento indefinido

Ahora hemos llegado a la parte del comportamiento indefinido, ¿qué pasa cuando lees un puntero que no se ha inicializado?, no lo sé, pregúntame cual será el próximo número ganador de la lotería y posiblemente me sea más fácil responderte a eso.

En C todo es posible. :)

El comportamiento indefinido es imposible de predecir, y lo peor es que en muchos casos es silencioso, tu programa puede funcionar perfectamente bien hasta que un día, sencillamente se rompió en mil pedazos, o peor, corrompió información valiosa que estaba manejando.

Entonces ¿Cómo evitar el comportamiento indefinido? La respuesta es fácil, no hagas cosas como acceder a un puntero no inicializado, acceder a secciones de memoria invalidas como la posición 10 de un arreglo de longitud 9, modificar una cadena literal, o incrementar el valor de un int más allá del valor máximo que puede almacenar.

En lo que respecta a punteros, un puntero no inicializado no es lo mismo que null (aunque parezca que por lógica debería ser así) y si llegas a hacer algo como eso, todo puede pasar, así que asegúrate de SIEMPRE inicializar un puntero a null.

Todas estas declaraciones dan comportamiento indefinido:

char *ptr;
if (ptr == NULL) { }

int arr[6];
print(arr[10]);

char *s = "hola";
s[0] = 'a';

Aquí hay ejemplos excelentes de comportamientos indefinidos que debes evitar, cabe resaltar que está en inglés, pero el código se entiende igual.

Portable vs Independiente de la plataforma

Por último, está el tema del código portable y el código independiente de la plataforma, ambos términos se confunden mucho o tienen interpretaciones diferentes, pero para simplificar las cosas, tratare de explicar su diferencia.

Un código independiente de la plataforma es aquel que se ejecuta y da los mismos resultados en diferentes plataformas, mientras que un código portable se puede ejecutar o compilar en diferentes plataformas más no asegura que dará los mismos resultados.

Vamos con un ejemplo, el tipo de dato size_t presente en C es portable, ya que se puede ejecutar sin problemas en sistemas como Linux y Windows y en arquitecturas tanto de 32 como 64 bits, PERO no es independiente de la plataforma, porque en un sistema de 32 bits tendrá mínimo 32 bits y en un sistema de 64 bits tendrá mínimo 64 bits, pudiendo almacenar cantidades diferentes de información en cada sistema y, por ende, dando resultados diferentes.

size_t len; //Funcionara en 32 y 64 bits pero seguramente no tendrá el mismo tamaño

uint32_t len; //Funcionara en 32 y 64 bits y asegurara tener el mismo tamaño (32 bits)

Así que el hecho de que algo sea portable lamentablemente no significa que será independiente de la plataforma, algo muy importante a tomar en cuenta a la hora de escribir código destinado a correr en múltiples equipos.

Así que ya sabes, el que tu código funcione en Linux y Windows no necesariamente significa que en todos los casos funcione igual en ambos sistemas, así que vale la pena documentarse sobre los tipos de datos y las funciones que usas.

Como excelente tip, recomiendo usar Valgrind si vas a programar en C, Valgrind es un programa para depurar memoria y puedes usarlo para ver que memoria en un programa no has liberado o a qué secciones de memoria estas accediendo de forma invalida, este programa te puede simplificar el solucionar la mitad de los problemas que mencione previamente en este post así que definitivamente vale la pena usarlo.