Regla 0x3005h: Minimizá el uso de múltiples niveles de indirección (punteros a punteros)
Memoria, punteros y tipos (0x30XX)
0x3005h: Minimizá el uso de múltiples niveles de indirección (punteros a punteros)¶
Enunciado normativo¶
DEBE evitarse la indirección múltiple (
**,***) salvo cuando sea estrictamente necesaria para el contrato de la función. Cuando haga falta modificar el puntero del llamador, DEBE preferirse retornar el puntero como valor de retorno en lugar de recibirT **.
¿Por qué existe esta regla?¶
El problema¶
Cada nivel de indirección agrega un salto mental: para saber qué contiene p,
el lector debe resolver *p, luego **p, y recordar en qué nivel se encuentra.
En el modelo de memoria, cada * corresponde a una dirección almacenada en
memoria; seguir la cadena exige desreferenciar dos o tres veces, y cada
desreferencia puede ser inválida si el nivel intermedio es nulo o colgante.
Los punteros a punteros aparecen por dos motivos: modificar el puntero del
llamador (parámetro de salida) o representar arreglos de cadenas (char **argv).
El primero casi siempre puede reemplazarse por un valor de retorno más claro; el
segundo es legítimo y tiene forma canónica.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Legibilidad | Firmas como int ***ptr exigen reconstruir la cadena de memoria. |
| Errores de nivel | Es fácil desreferenciar una vez de más o de menos (*p vs **p). |
| Robustez | Cada nivel puede ser nulo; las validaciones se multiplican. |
Fundamento en el estándar y en la cátedra¶
C11 no limita la cantidad de niveles, pero cada * adicional apunta a un objeto
distinto. La cátedra adopta el criterio de 0x0001h: La claridad y prolijidad son de máxima importancia: si la firma necesita
explicación para leerse, es demasiado compleja.
Alcance y excepciones¶
Aplica a parámetros y variables locales. No alcanza a los casos canónicos:
char **argv/char *argv[], la firma demain.const char **en funciones de ordenamiento o de impresión de listas de cadenas.Parámetros de salida
T **cuando la función debe informar más de un resultado y ya devuelve algo distinto (por ejemplo, un código de error).Estructuras que contienen naturalmente punteros a punteros.
Aun en los casos permitidos, la documentación debe explicar quién es dueño de cada nivel (ver 0x3006h: Documentá la propiedad de los recursos al utilizar punteros).
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Tres niveles para devolver un arreglo¶
void obtener_datos(int ***ptr_datos, size_t *cantidad);Por qué falla: el llamador debe declarar int **datos;, pasar &datos y luego
recordar que *datos es el arreglo a liberar. La firma no dice si el tercer
nivel está reservado ni quién lo libera. Un valor de retorno elimina uno de los
niveles y vuelve el contrato evidente.
❌ Contraejemplo 2 — T ** sólo para “poder asignar”¶
void inicializar(nodo_t **nodo, int valor)
{
*nodo = malloc(sizeof(**nodo));
(*nodo)->dato = valor;
}Por qué falla: se usa el parámetro de salida para devolver una única cosa. El
doble nivel complica la validación (nodo == NULL, *nodo == NULL) y cada
acceso necesita paréntesis por la precedencia de * y ->. Devolver nodo_t *
es más simple y admite la verificación de 0x3001h: Siempre verificá la asignación exitosa de memoria dinámica.
✅ Ejemplo conforme 1 — Retornar el puntero¶
int *obtener_datos(size_t *cantidad);
void usar(void)
{
size_t cantidad = 0;
int *datos = obtener_datos(&cantidad);
if (datos == NULL)
{
return;
}
free(datos);
}Se usa un solo parámetro de salida (size_t *) y el valor de retorno para el
arreglo. El cliente lee el contrato sin resolver niveles de indirección.
✅ Ejemplo conforme 2 — Caso legítimo de dos niveles¶
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}char *argv[] es un arreglo de cadenas: cada elemento es un char * y el
arreglo decae a char **. Es la forma canónica y no se reemplaza por nada. La
regla se respeta porque el doble nivel es el modelo natural del dominio.
⚠️ Casos límite¶
Matriz dinámica
T **: es un arreglo de punteros; su liberación sigue el orden inverso de 0x300Fh: Liberá la memoria en el orden inverso a su asignación.Parámetro de salida con error:
bool crear(recurso_t **out)puede justificarse cuando el valor de retorno se reserva para el éxito. Documentá quién asigna y quién libera.consten el nivel correcto:const char **ychar *const *no significan lo mismo; elegí el que refleja el contrato y documentalo.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | Declaraciones con *** o ** en parámetros no canónicos. |
gcc / clang | gcc -Wall -Wextra -std=c11 ... | No detecta complejidad, sí tipos incompatibles en las desreferencias. |
| Revisión manual | — | Parámetro T ** cuya única salida es un puntero. |
Checklist de autocontrol¶
¿Puedo devolver el puntero en lugar de usar
T **?¿Cada nivel de indirección tiene un dueño documentado?
¿Validé cada nivel antes de desreferenciarlo?
¿La firma se lee sin contar asteriscos?
¿Es un caso canónico (
argv, arreglo de cadenas) y no una comodidad?
Reglas relacionadas¶
0x3006h: Documentá la propiedad de los recursos al utilizar punteros — cada nivel de indirección exige documentar la propiedad.
0x3007h: Los argumentos de tipo puntero deben ser const siempre que la función no los modifique — el
constcorrecto aclara qué nivel es de sólo lectura.0x200Ah: Modularización: una función no debe exceder 4 parámetros de entrada — el exceso de parámetros suele acompañar al exceso de indirección.