El calificador const en C: semántica y peligros

En C, const no es una propiedad intrínseca del objeto en memoria, sino un calificador de tipo. Cuando declaras const int x = 10;, no estás creando necesariamente una “constante” en el sentido matemático o de tiempo de compilación (como lo hace constexpr en C23), sino que le estás diciendo al compilador: “A través del nombre x, me comprometo a no modificar este valor”.

Esta distinción es fundamental. Mientras que en C++ un const puede ser evaluado en tiempo de compilación para definir tamaños de arrays, en C tradicional (C89/C90), un const int n = 5; no es una constante simbólica; es una variable en memoria cuyo tipo prohíbe la asignación directa. Si intentas usar n para definir el tamaño de un array (int arr[n];), fallará en C89, aunque en C99 esto es posible mediante los Variable Length Arrays (VLA), lo cual introduce matices de gestión de memoria en el stack que conviene evitar si buscas portabilidad estricta.

El const funciona como un contrato semántico que el compilador utiliza para dos propósitos: primero, para emitir errores si intentas violar el contrato (por ejemplo, x = 20;), y segundo, para aplicar optimizaciones, como mantener un valor en un registro si el compilador puede asegurar que nadie lo modificará. Sin embargo, este contrato es débil. Si utilizas un puntero no-const para acceder a la dirección de un objeto const, puedes saltarte la protección. El resultado es comportamiento indefinido (Undefined Behavior), ya que el compilador asume que el calificador const es una verdad absoluta y cualquier intento de burlarlo rompe las reglas de la máquina abstracta de C.

Debes usar const siempre que el semántica de tu función no requiera modificar el objeto. Es la herramienta principal para comunicar la intención del programador. Si no lo usas, estás dejando la puerta abierta a errores accidentales y perdiendo oportunidades de optimización.

#include <stdio.h>

/* 
 * El parámetro 'msg' es un punteroc a datos constantes.
 * El contrato: esta función no modificará el contenido de la cadena.
 */
void imprimir_mensaje(const char *msg) {
    // msg[0] = 'X'; // Error de compilación: se intenta modificar un objeto const.
    printf("Mensaje: %s\n", msg);
}

int main(void) {
    // 1. Variable const: objeto en memoria con restricción de acceso.
    const int limite = 42;

    // 2. El buffer es mutable, pero su puntero puede ser const.
    char buffer[] = "Datos";
    
    // Un puntero a datos constantes: puedes mover el puntero, pero no el contenido.
    const char *ptr_dato_const = buffer;
    
    // Un puntero constante: no puedes mover el puntero, pero sí el contenido.
    char *const ptr_const_ptr = buffer;

    printf("Buffer original: %s\n", buffer);

    // Modificamos el contenido a través del puntero constante al dato.
    // Esto es legal porque el objeto original 'buffer' es mutable.
    ptr_const_ptr[0] = 'B';

    // 3. El "bypass" de const:
    // Si intentamos tratar a 'limite' como mutable mediante un cast,
    // estamos violando la semántica. En este caso, 'limite' está en el stack,
    // así que podría funcionar, pero es una mala práctica peligrosa.
    int *p_bypass = (int *)&limite;
    *p_bypass = 100; 

    // 4. El peligro real: Literales de cadena.
    // Los literales como "Mundo" residen en una sección de solo lectura (.rodata).
    const char *literal = "Mundo";

    printf("Buffer tras modificaciones: %s\n", buffer);
    printf("Límite tras bypass: %d\n", limite);
    printf("Literal: %s\n", literal);

    imprimir_mensaje(buffer);

    return 0;
}

Desglose del ejemplo

Fíjate en la diferencia de comportamiento entre ptr_dato_const y ptr_const_ptr. En ptr_dato_const, el calificador const aplica al tipo de dato al que apunta (char). El compilador te impedirá hacer *ptr_dato_const = 'X', pero te permitirá hacer ptr_dato_const++. En cambio, en ptr_const_ptr, el const aplica al propio puntero; puedes cambiar el contenido (ptr_const_ptr[0] = 'B'), pero el puntero está “anclado” a la dirección de buffer.

Cuando aplicamos un cast a &limite para obtener un int *, estamos realizando un “cast-away const”. Como limite es una variable local en el stack, el sistema operativo no lanzará un error de segmentación, pero hemos roto la promesa de inmutabilidad. El valor de limite cambiará a 100, lo que demuestra que const es una restricción del compilador, no una restricción física del hardware en este caso.

Finalmente, la función imprimir_mensaje recibe un const char *msg. Esto es fundamental para la robustez: cualquier programador que use tu librería verá que la función no toca sus datos y podrá pasarle buffers sensibles con seguridad.

El error frecuente

El error más crítico ocurre cuando intentas usar un puntero const para modificar un literal de cadena.

const char *nombre = "Usuario";
name[0] = 'u'; // CRASH / Undefined Behavior

Aquí, nombre apunta a una dirección de memoria en la sección .rodata (read-only data) del binario. El compilador te permite el cast-away const si intentas forzar la escritura mediante un puntero normal, pero en tiempo de ejecución, el procesador detectará que intentas escribir en una página de memoria protegida y lanzará un Segmentation Fault. Herramientas como AddressSanitizer detectarán esto inmediatamente como una violación de memoria.

23

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio