Este documento reúne un conjunto de reglas de estilo diseñadas para hacer que el código en C sea más claro, fácil de leer y menos propenso a errores. Programar en C puede ser bastante flexible, pero también es fácil caer en malas prácticas que pueden llevar a errores difíciles de detectar. Por eso, tener un conjunto de reglas claras ayuda a mantener el código ordenado y seguro.
La idea detrás de estas reglas es que un buen código no solo funcione, sino que también sea comprensible para cualquier persona que tenga que leerlo, ya sea el mismo programador en el futuro o alguien más que se sume al proyecto. Un código limpio y bien organizado facilita mucho el trabajo en equipo, ahorra tiempo en correcciones y evita dolores de cabeza cuando llega el momento de depurarlo o actualizarlo.
Estas reglas cubren todo, desde cómo nombrar variables y funciones hasta cómo estructurar los condicionales y lazos. Seguirlas no solo ayuda a mantener la coherencia en el proyecto, sino que también hace que el código sea más robusto y fácil de mantener a largo plazo.
Comenzar con reglas rígidas en un lenguaje flexible, nos da un respaldo adicional y cuando nuestro entendimiento del lenguaje mejore, podemos comenzar a doblar las reglas y llegar a un estilo propio.
Estamos abiertos a conversar todas las reglas, solo tienen que abrir un hilo en Discussions (o un Ticket en el Issue Tracker) asi como nuevas reglas, clasificaciones, explicacioes y potenciales excepciones.
(En algún momento dejaremos)
El código debe ser claro y fácil de entender para cualquiera que lo lea, no solo para quien lo escribe. Un código limpio y prolijo evita errores, facilita el mantenimiento y mejora la colaboración en equipo. La claridad es preferible a trucos de programación o técnicas avanzadas que solo complican el entendimiento.
- for (int i = 0, j = 10; i < j; i++, j--) { printf("%d", i+j); }
+ for (int i = 0; i < 10; i++)
+ {
+ int suma = i + (10 - i);
+ printf("%d", suma);
+ }
Los nombres de variables, funciones y demás identificadores deben reflejar claramente su propósito. Esto ayuda a que el código sea autodescriptivo, sin necesidad de comentarios adicionales. Usar nombres significativos facilita la lectura y comprensión.
// Malos identificadores
- int a, b;
- a = obtener_precio();
- b = calcular_descuento(a);
// Identificadores descriptivos
+ int precio, descuento;
+ precio = obtener_precio();
+ descuento = calcular_descuento(precio);
-int a, b, c;
+int a;
+int b;
+int c;
Es importante que una variable utilizada como R-Value tenga un valor conocido antes de tomar lo que tenga.
-uno=dos+tres;
+uno = dos + tres;
if (condicion)
{
//camino true
}
else
{
//camino false
}
El uso de break y continue dentro de los lazos puede generar un flujo de control inesperado, lo que dificulta el seguimiento del programa. Es preferible utilizar una bandera (variable de control) para salir de los lazos de forma explícita y ordenada, lo que hace el código más predecible y fácil de mantener.
El lazo while es más flexible y adecuado cuando no se conoce de antemano el número de iteraciones. Además, el while es generalmente más fácil de leer cuando la condición de parada no está claramente relacionada con un contador. Si se utiliza un lazo para repetir indefinidamente o hasta que una condición específica sea verdadera, while es preferible a for.
Limitar una función a un único punto de retorno mejora la legibilidad y facilita el seguimiento del flujo de control. Además, ayuda a evitar errores relacionados con la liberación de recursos o la ejecución de código después de múltiples retornos.
Las funciones deben estar separadas de la entrada y salida (I/O) para que sean útiles en otros contextos y se probar.
Si el propósito de la función no es realizar I/O, estos llamados deben evitarse, delegando la entrada y salida a otras funciones.
/**
* Descripción de la función.
* @param parametro rol
* PRE:
* @returns caracteristicas del valor de retorno.
* POST:
*/
Las variables globales pueden ser modificadas desde cualquier parte del programa, lo que puede causar efectos secundarios inesperados y dificultar el rastreo de errores.
Cada función debe encargarse de una sola tarea o responsabilidad. Esto mejora la legibilidad y facilita la reutilización y el mantenimiento del código. Las funciones pequeñas y especializadas son más fáciles de probar y depurar.
Si una condición contiene múltiples operadores lógicos, divídela en partes más pequeñas o agrega comentarios explicativos.
if (condicion1 && (condicion2 || !condicion3)) {
// Explicar qué hace cada condición
}
Los arreglos ALV no estan permitidos por los problemas que pueden ocasionar, por lo que deben ser definidos con un tamaño fijo que se determina en tiempo de compilación.
- int n = 10;
- int numeros[n]; // ALV
+ int numeros[10];
Pueden lograr esto creando una función que reciba los argumentos y el resultado esperado para comparar, o hacer una funcion para cada caso.
Por ejemplo, si una variable numérica se usa como condición, siempre se debe ser explícito:
- if (x) {
+ if (x != 0) {
El usar nombres descriptivos para los valores facilita la comprensión del propósito del retorno al darle un nombre explicito.
-return -1;
+return NO_FUNCIONO;
#define NO_FUNCIONO -1
Incluso para bloques de una sola línea.
- if (condicion) accion;
+ if (condicion) {
+ accion;
+ }
//nivel 0
{
//nivel 1
{
//nivel 2
{
//nivel 3
}
}
}
El uso de goto
rompe el flujo natural del programa y dificulta la lectura y depuración del código, ya que salta entre diferentes partes del programa de manera impredecible. En lugar de usar esta instrucción, emplea estructuras de control como if-else
, for
, while
y switch
, que permiten un flujo claro y estructurado.
El operador condicional (ternario) es compacto, pero puede hacer que el código sea difícil de leer, especialmente si se usa de manera excesiva o anidada.
Esto fomenta la modularización del código, facilita la prueba de unidades, y promueve la reutilización del código. Dividir la lógica en funciones permite que el código sea más organizado y comprensible.
El uso de snake_case (nombres en minúsculas con guiones bajos entre palabras) para los nombres de funciones y procedimientos es una convención de estilo que mejora la consistencia y legibilidad del código. De esta forma y siguiendo las otras reglas de este estilo, podemos saber inmediatamente que es una funcion, una variable, una constante y las demás piezas del programa.
Para facilitar la identificación visual de la variable como un puntero y mejora la claridad.
-int* ptr;
+int *ptr;
Cualquier asignación dinámica de memoria con malloc
, calloc
o realloc
debe ser seguida por una comprobación de éxito:
ptr = malloc(tamaño);
if (!ptr)
{
// Manejo de error
}
Cada vez que se usa malloc
/calloc
/realloc
, debe asegurarse que la memoria sea liberada correctamente usando free.
free(ptr);
ptr = NULL; // Evitar punteros colgantes
Mantener las asignaciones y comparaciones en líneas separadas reduce la posibilidad de errores sutiles.
- if ((ptr = malloc(tamaño)) == NULL) {
+ ptr = malloc(tamaño);
+ if (!ptr) {
fgets es más seguro que gets
y scanf
porque evita desbordamientos de buffer.
fgets(buffer, sizeof(buffer), archivo);
Siempre verificar si el archivo se abre correctamente y cerrarlo después de su uso:
FILE *archivo = fopen("archivo.txt", "r");
if (!archivo)
{
// Manejo de error
}
fclose(archivo);
struct Ejemplo ejemplo = {0}; // Inicializa todos los campos a cero o NULL
Facilita el manejo y mejora la legibilidad del código al declarar tipos complejos:
typedef struct {
int campo1;
char *campo2;
} MiEstructura;
Cuando se accede a los campos de una estructura mediante un puntero, siempre usar ->
en lugar de .
:
ptr->campo = valor;
El uso de punteros genéricos debe ser evitado a menos que sea estrictamente necesario, ya que puede ocultar errores de tipo.
Esto complica la lectura y el manejo, especialmente cuando se trata de asignación o liberación de memoria.
Cuando una función recibe o devuelve un puntero a memoria dinámica, es importante documentar quién es responsable de liberar la memoria:
/**
* @param ptr Puntero que será liberado por el llamador.
*/
Recuerden que no es posible que el programa diferencie la memoria dinámica de la automática.
El uso de const proporciona garantías adicionales y ayuda a evitar modificaciones accidentales:
void funcion(const int *ptr);
Usa NULL
para inicializar y verificar punteros, no 0, para mayor claridad y coherencia semántica.
int *ptr = NULL;
if (ptr == NULL) {
// ...
}
Las conversiones de tipos deben ser claras y explícitas para evitar errores:
void *mem = malloc(sizeof(int));
int *ptr = (int *) mem; // Cast explícito
Regla 0x75h: Usar macros de tamaño (sizeof) siempre que sea posible en asignaciones de memoria dinámica.
Facilita la modificación y reduce errores al manejar estructuras y tipos dinámicos:
ptr = malloc(sizeof(*ptr)); // Asigna la cantidad correcta de memoria para el tipo de ptr
Evita accesos fuera de los límites del arreglo, esto es una de las fuentes más comunes de errores en C:
if (indice >= 0 && indice < tamaño_arreglo) {
arreglo[indice] = valor;
}
Los punteros a funciones pueden introducir complejidad innecesaria. Prefiere mantener las funciones independientes si es posible.
Mejora la legibilidad y reduce errores al manejar múltiples constantes:
enum Estado { INACTIVO, ACTIVO, PAUSADO };
Estado estado = ACTIVO;
Regla 0x79h: Documentar explícitamente el comportamiento de las funciones al manejar punteros nulos.
Cuando una función acepta o devuelve un puntero nulo, el comportamiento debe estar claramente documentado:
/**
* @param ptr Puntero que puede ser NULL.
* @returns NULL si ocurre un error.
* @returns ERROR_POR_NULO no se pudió seguir.
*/
Esto no implica un cambio en la estructura de la función, es una cuestión de documentar la situación en la estructura que tenga la función.
Esto es especialmente importante en programas complejos donde varias porciones de memoria son asignadas en secuencia, como con matrices.
free(ptr2);
free(ptr1);