4.2.1e Puntero constante/ a constante
§1 Sinopsis
Un puntero puede ser declarado con el modificador const; recuérdese que cualquier entidad declarada con esta propiedad no puede modificar su valor ( 3.2.1c). En el caso de la declaración de punteros, la posición del modificador es determinante, de forma que pueden darse dos situaciones cuyos resultados son bastante distintos.
§2 Puntero-a-constante
Las dos formas que siguen son análogas; en ambos casos representan puntero-a-tipoX-constante (el objeto al que se apunta es constante), abreviadamente: puntero-a-constante. Se utilizan para avisar al compilador que el objeto referenciado no puede ser modificado, ni aún a través de su puntero.
tipoX const * puntero ... ;
const tipoX * puntero ... ;
Ejemplo:
int const * xPtr = &x;
const int * yPtr = &y;
xPtr e yPtr son dos objetos del mismo tipo (puntero-a-int constante). Ambos señalan a sendos enteros cuyo Rvalue no pueden ser modificado, sin embargo, los punteros si pueden ser modificados. Por ejemplo, señalando a un nuevo objeto (siempre naturalmente que este sea un int constante).
Los punteros-a-constante son de utilización más frecuente que los punteros constantes (ver a continuación). Se utilizan
principalmente cuando se pasan punteros a funciones, porque se desea aligerar la secuencia de llamada
( 4.4.6b). En especial si los
punteros señalan a objetos muy grandes y se desea evitar que la función invocada pueda modificar el objeto en cuestión.
Ejemplo:
class CMG {...}; // clase muy grande
void fun(const CMG*); // función aceptando un puntero a la clase
int main() { // ===========
CMG* aptr = new CMG;
...
fun(aptr); // fun no puede modificar el objeto
}
§3 Puntero constante
La expresión que sigue significa puntero-constante-a-tipoX (el puntero es constante), abreviadamente: puntero constante. Se utiliza para informar al compilador que el valor del puntero no puede cambiar, aunque el objeto al que apunta si pueda cambiar (si no es constante).
tipoX * const puntero ... ;
Puesto que en este tipo de sentencias el puntero es declarado constante y su valor no puede ser modificado con posterioridad, hay que establecer su valor en el mismo momento de la declaración. Es decir, debe ser una definición del tipo:
tipoX * const puntero = dirección-de-objeto ;
Ejemplo:
int* const xPtr = &x;
En este caso xPtr es un puntero constante-a-int. El puntero es constante, lo que significa que su Rvalue no puede ser cambiado; por tanto debe señalar al entero x durante toda su vida.
§4 No confundir puntero constante con puntero-a-constante. Como puede verse en la última línea del ejemplo que
sigue, ambos casos pueden darse simultáneamente (puntero constante a constante):
int nN = 7;
// entero iniciado a 7
const int nC = 7; // entero constante iniciado a 7
int *const pt1 = &nN; // puntero constante a int
const int * pt2 = &nC; // puntero a constante tipo int
const int * const pt3 = &nC // puntero constante a constante tipo int
Como resumen podemos afirmar que:
- Un puntero constante no puede ser modificado
- Un puntero-a-constante apunta a un objeto cuyo Rvalue no puede cambiar
Observe que, desde la óptica del C++, puntero-a-tipoX y puntero-a-tipoX-constante son tipos distintos,
del mismo modo que tipoX y tipoX-constante son también tipos distintos. Ejemplos:
const int kte = 75;
int x = 75;
int* ptr; // puntero-a-int
ptr = &kte; // Error. La asignación no es posible
const int* ptk; // puntero-a-int-constante
ptk = &kte; // Ok: Asignación correcta
§5 Es ilegal crear un puntero que pudiera violar la no asignabilidad de un objeto constante. Considere los
ejemplos siguientes:
int i;
// i es int (no iniciado)
const int ci = 7; // ci es int constante (iniciado a 7)
i = ci; //
Ok: Asigna constante-int a int (i == 7)
ci = 0;
// ERROR: asignar valor a constante
ci--;
// ERROR: modificar valor de constante
int * pi;
// pi es puntero-a-int (no iniciado)
int * const cp = &i; // cp es puntero-constante-a-int
(iniciado), apunta a un int no constante (i)
*cp = ci;
// Ok: Asigna constante-int al objeto apuntado por un puntero-constante-a-int (el objeto apuntado i, no es
constante)
cp = &ci;
// ERROR: asignar nuevo valor a puntero constante
const int * pci = &ci; // pci es puntero-a-constante-int
*pci = 3;
// ERROR: asignar valor al objeto apuntado por pci (es puntero-a-constante)
++pci;
// Ok: Incrementa puntero-a-constante-int
const int * const cpc = &ci; // cpc es puntero-constante-a-constante-int
pci = cpc;
// Ok: Asigna puntero-constante-a-constante-int a un puntero-a-constante-int
cpc++;
// ERROR: modificar valor de puntero-constante
pi = pci; // ERROR:
si se permitiese la asignación, sería posible asignar al valor apuntado por pci (una constante) mediante asignación a la
dirección apuntada por pi.
Reglas similares pueden aplicarse al modificador volatile. Observe que ambos: const
( 3.2.1c ) y volatile
( 4.1.9) pueden aparecer
como modificadores del mismo identificador.
§5.1 A pesar a pesar de lo indicado anteriormente, es posible modificar una
constante, de forma indirecta, a través de un puntero. Considere el siguiente ejemplo [2]:
int const edad = 40; // L1: constante tipo int iniciada a 40
printf("%3i\n", edad); // L2: -> 40
*(int *)&edad = 35; // L3: Se acepta la instrucción sin error
printf("%3i\n", edad); // L4: -> 40
printf("%3i\n", *(int *)&edad); // L5: -> 35
Comentario:
El ejemplo, un tanto sorprendente, merece una explicación:
L1: La variable edad se define como constante-tipo-int. Se inicia al valor 40
L2: Se muestra su valor: 40 como cabría esperar.
L3: Esta sentencia se ejecuta sin error. Modifica el valor de la constante de forma indirecta, a través de un puntero, asignándole un valor de 35.
La explicación es que la dirección &edad, Lvalue(edad), que es la dirección de una constante-tipo-int, es modificada por el operador de modelado (int *) que la promueve a puntero-a-int (aquí se pierde el carácter de constante); a este puntero lo denominaremos provisionalmente iptr. Finalmente, la expresión *iptr = 35 asigna 35 a la "variable" señalada por iptr, lo que es perfectamente legal.
L4: Esta sorprendente sentencia indica que edad sigue teniendo el valor 40 (su valor inicial).
L5: Acto seguido, pedimos que nos muestre el valor guardado en la dirección iptr (la dirección de edad), y nos muestra el valor modificado, 35.
La explicación de esta aparente contradicción es que (por razones de eficacia en la ejecución del código), en la línea 4 el compilador utiliza el valor de la constante resuelta en tiempo de compilación (se le había dicho -Línea 1- que era constante). El compilador supone que será realmente constante y en el segmento de código correspondiente a la sentencia de impresión pone directamente el valor 40 obtenido en tiempo de compilación. Este es el valor que nos muestra en la línea 4. Si embargo, cuando en la línea 5 se le pide que muestre el verdadero valor (el que hay en la zona de almacenamiento de variables [1]) muestra el valor modificado.
[1] Recuérdese lo indicado al respecto ( 3.2.3); no está garantizado que el compilador asigne un Lvalue a los objetos declarados inicialmente como constantes.
[2] Naturalmente que todos estos "trucos" están totalmente desaconsejados, y no está garantizado que funcionen en todos los compiladores. El C++ ofrece alternativas adecuadas y portables para hacer las cosas.