const y punteros: leer declaraciones sin equivocarse
La regla de derecha a izquierda que descifra cualquier declaración, la diferencia exacta entre puntero a constante y puntero constante, y qué promete de verdad const en un lenguaje sin garantías de inmutabilidad.
Pocas cosas producen más inseguridad que encontrarse una declaración con dos const y un asterisco y no saber cuál de las dos partes es la inmutable. La buena noticia es que no hay que memorizar casos: existe una regla mecánica que descifra cualquier declaración, y una vez la aplicas, la distinción entre puntero a constante y puntero constante deja de ser trivia sintáctica para convertirse en una herramienta de diseño de interfaces.
- Aplicar la lectura de derecha a izquierda a cualquier declaración con punteros.
- Distinguir puntero a constante, puntero constante y la combinación de ambos.
- Entender qué garantiza
constal compilador y qué no garantiza en absoluto. - Diseñar firmas de función que documenten con
constquién puede modificar qué.
Leer declaraciones de derecha a izquierda
El algoritmo es simple: parte del identificador y avanza hacia la derecha mientras puedas; cuando ya no puedas, gira hacia la izquierda. Traduce cada símbolo a palabras. El asterisco se lee “puntero a”, los corchetes “array de” y los paréntesis “función que devuelve”.
Aplicado a los cuatro casos que importan en este nivel:
int *p; // p es puntero a int
const int *p; // p es puntero a int constante
int *const p; // p es puntero constante a int
const int *const p; // p es puntero constante a int constante
La clave está en de qué lado del asterisco cae el const. Si aparece a la izquierda del asterisco, califica al tipo apuntado. Si aparece a la derecha, califica al puntero en sí. Una forma de recordarlo sin ambigüedad: el const que está pegado al identificador, después del asterisco, es el que congela al puntero.
Conviene además saber que const int *p y int const *p son exactamente lo mismo. La segunda forma, aunque menos común, es más regular: si escribes siempre el calificador a la derecha del tipo, la lectura de derecha a izquierda se vuelve puramente mecánica y no hay excepciones que recordar.
Puntero a constante frente a puntero constante
La diferencia práctica se ve mejor con lo que cada declaración prohíbe.
Puntero a constante
Con const int *p, no puedes escribir *p, pero sí puedes reasignar p para que apunte a otro sitio. Lo protegido es el dato.
Puntero constante
Con int *const p, puedes escribir *p libremente, pero p debe inicializarse y ya no puede apuntar a otra cosa. Lo protegido es la dirección.
Ambos constantes
Con const int *const p no puedes ni cambiar el dato ni redirigir el puntero. Es el caso más restrictivo y el que mejor documenta la intención.
El caso frecuente
En la práctica, más del noventa por ciento de los const que escribirás son punteros a constante en parámetros de función. Los otros casos son raros.
Traducido a código, el compilador rechaza exactamente lo que cabría esperar:
int x = 1, y = 2;
const int *pa = &x;
// *pa = 99; // error: el dato es constante a traves de pa
pa = &y; // valido: el puntero no lo es
int *const pb = &x;
*pb = 99; // valido: el dato no es constante
// pb = &y; // error: el puntero es constante
const int *const pc = &x;
// *pc = 99; // error
// pc = &y; // error
flowchart TD
D[Declaracion con const y asterisco] --> Q{Donde esta el const}
Q -->|a la izquierda del asterisco| A[Puntero a constante: el dato no se toca]
Q -->|a la derecha del asterisco| B[Puntero constante: la direccion no cambia]
Q -->|a ambos lados| C[Ni el dato ni la direccion]
style A fill:#a6e3a1,color:#11111b
style B fill:#89b4fa,color:#11111b
style C fill:#f9e2af,color:#11111bQué garantiza const y qué no
Aquí es donde muchos programadores construyen una intuición equivocada. const no significa “este dato es inmutable”. Significa “a través de este nombre, prometo no modificarlo”. Es una restricción sobre la vía de acceso, no una propiedad del objeto.
La consecuencia inmediata es que un mismo objeto puede ser accesible simultáneamente por una vía calificada y otra no calificada, y modificarlo por la segunda es perfectamente legal:
int x = 10;
const int *lectura = &x; // vista de solo lectura
int *escritura = &x; // vista de escritura
*escritura = 20;
// A traves de lectura veremos 20: el objeto cambio legalmente.
Hay, en cambio, una línea que sí es infranqueable. Si el objeto se definió como const, quitarle el calificador con un cast y escribir a través del resultado es comportamiento indefinido, no una picardía tolerada. El compilador puede haber colocado ese objeto en memoria de solo lectura, o haber propagado su valor como constante en tiempo de compilación.
const int fijo = 5;
int *tramposo = (int *)&fijo; // el cast compila...
// *tramposo = 7; // ...pero escribir aqui es UB puro
Merece un aviso el caso de las cadenas literales. En C un literal como "hola" tiene tipo array de char sin calificar, de modo que asignarlo a un char * compila sin queja; pero modificar su contenido es comportamiento indefinido y en la práctica suele provocar una violación de segmento, porque vive en una sección de solo lectura del binario. La disciplina correcta es declarar siempre const char * para apuntar a literales.
const correctness en las APIs
El valor real de const no está en que impida errores tontos dentro de una función, sino en que convierte la firma en un contrato legible. Compara estas dos declaraciones:
size_t longitud(char *s);
size_t longitud(const char *s);
La segunda te dice, sin leer una línea de implementación, que la función no tocará tu cadena. Esa es la razón por la que toda la biblioteca estándar está escrita así: en strcpy, el destino es char * y el origen es const char *, y la firma sola te explica la dirección del flujo de datos.
Hay además una razón mecánica para adoptar el hábito desde el principio: la conversión solo funciona en un sentido. Un int * se convierte implícitamente a const int *, pero no al revés. Por eso una función que olvida el const en un parámetro de solo lectura resulta inutilizable para cualquier llamante que tenga datos constantes, y el problema se propaga hacia arriba obligando a añadir casts o a duplicar funciones.
Esa regla tiene una excepción que sorprende a casi todo el mundo la primera vez: la conversión implícita no se propaga a través de dos niveles de indirección. Un int ** no se convierte a const int **, y el rechazo del compilador es correcto, no una molestia:
int *p;
int **pp = &p;
// const int **cpp = pp; // error, y con razon
const int fijo = 5;
// Si la linea anterior se permitiera, esto seria legal:
// *cpp = &fijo; // pone en p la direccion de un const
// *p = 99; // y escribe en un objeto constante
Si esa conversión fuera legal, se podría depositar la dirección de un objeto constante en un puntero no calificado sin un solo cast, y escribir después a través de él. El compilador la prohíbe para cerrar exactamente ese agujero. La forma correcta para el segundo nivel es const int *const *, que sí es aceptada.
Añadir const a una base de código existente es una tarea ingrata: cada parámetro que calificas obliga a calificar las funciones que llama, y la propagación no termina hasta llegar a las hojas. Por eso el orden importa. Empieza por las funciones más internas —las que no llaman a nadie— y sube. Si arrancas por la interfaz pública, cada paso te dejará el proyecto sin compilar hasta el final. Y si trabajas sobre código ajeno sin const, resiste la tentación de usar un cast para silenciar el error: ese cast es exactamente la mentira que el calificador existía para impedir.
La lectura superficial de const es “esto no se puede cambiar”, y esa lectura te fallará antes o después porque el lenguaje la desmiente: el mismo objeto puede tener a la vez una vía constante y una que no lo es, y escribir por la segunda es legal. Lo que const califica no es el objeto, es el camino hacia él. Por eso la pregunta correcta nunca es “¿es este dato inmutable?” sino “¿quién tiene permiso para escribirlo, y por qué puerta?”. Con esa lente, const se revela como lo que realmente es: una herramienta de diseño de interfaces disfrazada de calificador de tipo. Cuando escribes const char * en un parámetro no estás pidiendo una comprobación al compilador, estás publicando una promesa a todo futuro llamante —incluido tú dentro de seis meses— sobre qué hace tu función y qué no. Y aquí aparece la asimetría que decide el estilo: const se propaga hacia dentro con toda naturalidad y se resiste ferozmente a ser añadido a posteriori, porque un solo parámetro sin calificar en una función profunda contamina hacia arriba toda la cadena de llamadas. De ahí la única regla de estilo que necesitas: pon const por defecto en todo parámetro que no modifiques y quítalo solo cuando el compilador te obligue. No es pedantería; es la diferencia entre una base de código donde puedes razonar localmente sobre quién muta qué y una donde cada llamada es una caja negra que podría estar reescribiendo tus datos.
- Traduce a español, aplicando la lectura de derecha a izquierda, estas cuatro declaraciones antes de compilarlas:
const int *a,int *const b,const int *const c,char *d[8]. - Escribe un programa que intente las cuatro modificaciones prohibidas y anota el mensaje exacto de error que da tu compilador en cada caso.
- Convierte un
int *aconst int *implícitamente y comprueba que la conversión inversa exige un cast explícito. - Reescribe una función tuya que reciba un array de solo lectura para que use
consten el parámetro, y verifica que sigue compilando desde todos los puntos de llamada. - Declara un literal de cadena con
char *y conconst char *; razona por escrito por qué la segunda forma debe ser siempre tu opción por defecto.