La palabra clave const en C no es una mera sugerencia para el programador; es una restricción de tipo que le dice al compilador cómo debe tratar la memoria. Cuando trabajamos con punteros, la posición de const determina qué es exactamente lo que queda “congelado”: el valor al que apuntamos o la dirección que almacena el puntero.
Para descifrar cualquier declaración de este tipo sin cometer errores, aplica la regla de la derecha a la izquierda (o spiral rule). Empieza por el nombre de la variable y avanza hacia la izquierda para entender el contrato:
int *p: Un puntero a unint. Tienes control total: puedes cambiar la dirección y puedes cambiar el valor.int * const p: Un puntero constante a unint. La dirección almacenada es fija; no puedes hacer que apunte a otro sitio, pero sí puedes modificar el valor en esa dirección.const int *p(oint const *p): Un puntero a un entero constante. Puedes mover el puntero a otras direcciones, pero no puedes modificar el contenido de la dirección a la que apunta a través de este puntero.const int * const p: Un puntero constante a un entero constante. El puntero es inmutable y el dato también.
Si intentas modificar algo marcado como const, el compilador lanzará un error de compilación. Sin embargo, si intentas usar un cast para forzar la escritura en un objeto que fue definido originalmente como const, caerás en comportamiento indefinido (Undefined Behavior), lo cual es un error crítico que el compilador no siempre podrá detectar en tiempo de ejecución.
#include <stdio.h>
/*
* El contrato de la API: este puntero le dice al llamador que la
* función no alterará la cadena de texto original.
*/
void imprimir_mensaje(const char *mensaje) {
// mensaje[0] = 'X'; // ERROR: el tipo es 'const char'
printf("Mensaje: %s\n", mensaje);
}
int main(void) {
int valor_a = 10;
int valor_b = 20;
// 1. Puntero normal: mutabilidad total.
int *p1 = &valor_a;
*p1 = 15; // OK: el dato cambia.
p1 = &valor_b; // OK: la dirección cambia.
// 2. Puntero a dato constante: el contenido es inmutable.
const int *p2 = &valor_a;
// *p2 = 25; // ERROR: el contenido es constante.
p2 = &valor_b; // OK: la dirección puede cambiar.
// 3. Puntero constante: la dirección es inmutable.
int * const p3 = &valor_a;
*p3 = 35; // OK: el dato puede cambiar.
// p3 = &valor_b; // ERROR: el puntero es constante.
// 4. Puntero constante a dato constante: mutabilidad nula.
const int * const p4 = &valor_a;
// *p4 = 40; // ERROR: contenido constante.
// p4 = &valor_b; // ERROR: dirección constante.
/*
* Nota: 'const char *' y 'char const *' son identicos.
* Lo que importa es si el 'const' está a la izquierda o derecha del '*'.
*/
const char *msg = "Sistema operativo";
imprimir_mensaje(msg);
printf("Final - A: %d, B: %d\n", valor_a, valor_b);
return 0;
}
Desglose del ejemplo
En el código anterior, observa cómo p1 permite cambiar tanto valor_a como la dirección hacia valor_b. Sin embargo, p2 nos impone una restricción sobre el contenido; aunque podemos redirigir p2 para que apunte a valor_b, no podemos usar p2 para cambiar el valor de valor_a a 25 porque el tipo es “puntero a int constante”.
La variable p3 es un caso distinto: su dirección de memoria es fija. Una vez que le asignas &valor_a, no hay forma de que p3 apunte a valor_b sin recurrir a un cast peligroso. No obstante, como el puntero no es a un dato constante, *p3 = 35 es perfectamente válido. p4 es la restricción máxima: es un bloque de solo lectura tanto en dirección como en contenido.
En la función imprimir_mensaje, el uso de const char *mensaje es una buena práctica de diseño de APIs. Garantiza al programador que el mensaje no será alterado. Es importante notar que si pasamos un char * (no constante) a esta función, el compilador lo acepta mediante una conversión implícita segura, pero no permite que la función modifique el original.
El error frecuente
Un error sutil ocurre cuando intentas “quitar” la constancia con un cast para modificar un objeto que es realmente constante en memoria.
const int VALOR_REAL = 100; int *ptr_malicioso = (int *)&VALOR_REAL; // Forzamos el cast *ptr_malicioso = 200; // UNDEFINED BEHAVIOR
En este caso, ptr_malicioso es un puntero normal, por lo que el compilador no se queja. Sin embargo, como VALOR_REAL fue declarado con const, el compilador y el sistema operativo pueden haberla colocado en una sección de memoria de solo lectura (como .rodata en sistemas POSIX). Intentar escribir en ella provocará, como mínimo, una violación de acceso (segmentation fault) o, de lo contrario, un comportamiento impredecible que corromperá el programa. Herramientas como AddressSanitizer detectarán este acceso ilegal.
N° 47