Almacenamiento en registro
Para especificar este tipo de almacenamiento se usa el especificador register.
Sintaxis:
register <tipo> <nombre_variable>; |
Indica al compilador una preferencia para que el objeto se almacene en un registro de la CPU, si es posible, con el fin de optimizar su acceso, consiguiendo una mayor velocidad de ejecución.
Los datos declarados con el especificador register tienen el mismo ámbito que las automáticas. De hecho, sólo se puede usar este especificador con parámetros y con objetos locales.
El compilador puede ignorar la petición de almacenamiento en registro, que se acepte o no estará basado en el análisis que realice el compilador sobre cómo se usa la variable.
Un objeto de este tipo no reside en memoria, y por lo tanto no tiene una dirección de memoria, es decir, no es posible obtener una referencia a un objeto declarado con el tipo de almacenamiento en registro.
Se puede usar un registro para almacenar objetos de tipo char, int, float, punteros. En general, objetos que quepan en un registro.
#include <iostream> using namespace std; void funcion(register int *x); int main() { int s[10] = {1, 2, 1, 5, 2, 7, 3, 1, 3, 0}; funcion(s); return 0; } void funcion(register int *x) { register char a = 'a'; for(register int i=0; i < 10; i++) { cout << *x++ << " " << a++ << endl; } } |
Modificador de almacenamiento constante
El modificador const crea nuevos tipos de objetos, e indica que el valor de tales objetos no puede ser modificado por el programa. Los tipos son nuevos en el sentido de que const int es un tipo diferente de int. Veremos que en algunos casos no son intercambiables.
Sintaxis:
const <tipo> <variable> = <inicialización>; const <tipo> <variable_agregada> = {<lista_inicialización>}; <tipo> <nombre_de_función> (const <tipo>*<nombre-de-variable> ); const <tipo> <nombre_de_función>(<lista_parámetros>); <tipo> <nombre_de_función_miembro>(<lista_parámetros>) const; |
Lo primero que llama la atención es que este modificador se puede aparecer en muchas partes diferentes de un programa C++. En cada una de las sintaxis expuestas el significado tiene matices diferentes, que explicaremos a continuación.
De las dos primeras se deduce que es necesario inicializar siempre, los objetos declarados como constantes. Puesto que el valor de tales objetos no puede ser modificado por el programa posteriormente, será imprescindible asignar un valor en la declaración. C++ no permite dejar una constante indefinida.
Cuando se trata de un objeto de un tipo agregado: array, estructura o unión, se usa la segunda forma.
En C++ es preferible usar este tipo de constantes en lugar de constantes simbólicas (macros definidas con #define). El motivo es que estas constantes tienen un tipo declarado, y el compilador puede encontrar errores por el uso inapropiado de constantes que no podría detectar si se usan constantes simbólicas.
Hay una excepción, que en realidad no es tal, y es que la declaración tenga además el especificador extern. En ese caso, como vimos antes, no estamos haciendo una definición, no se crea espacio para el objeto, y por lo tanto, no necesitamos asignar un valor. Una declaración extern indica al compilador que la definición del objeto está en otra parte del programa, así como su inicialización.
Cuando se usa con parámetros de funciones, como en el caso tercero, impide que el valor de los parámetros sea modificado por la función.
Sabemos que los parámetros son pasados por valor, y por lo tanto, aunque la función modifique sus valores, estos cambios no afectan al resto del programa fuera de la función, salvo que se trate de punteros, arrays o referencias. Es precisamente en estos tres casos cuando el modificador tiene aplicación, impidiendo que el código de la función pueda modificar el valor de los objetos referenciados por los parámetros.
Los intentos de hacer estas modificaciones se detectan en la fase de compilación, de modo que en realidad, a quien se impide que haga estas modificaciones es al programador. En ese sentido, la declaración de un parámetro constante nos compromete como programadores a no intentar modificar el valor de los objetos referenciados.
#include <iostream> using namespace std; void funcion(const int *x); int main() { int s = 100; funcion(&s); return 0; } void funcion(const int *x) { (*x)++; // ERROR: intento de modificar un valor constante } |
El compilador dará un error al intentar compilar este ejemplo.
Nota: El compilador puede referirse a estos objetos como de "sólo lectura", o "read-only". Viene a significar lo mismo: ya que podemos leer el valor de una constante, pero no escribirlo (o modificarlo). |
Con las referencias pasa algo similar:
void funcion(const int &x); |
En este caso no podremos modificar el valor del objeto referenciado por x.
Esto tiene varias utilidades prácticas:
Imaginemos que sólo disponemos del prototipo de la función. Por ejemplo, la función strlen tiene el siguiente prototipo: int strlen(const char*). Esto nos dice que podemos estar seguros de que la función no alterará el valor de la cadena que pasamos como parámetro, y por lo tanto no tendremos que tomar ninguna medida extraordinaria para preservar ese valor. Hay que tener en cuenta que no podemos pasar arrays por valor.
Otro caso es cuando queremos pasar como parámetros objetos que no queremos que sean modificados por la función. En ese caso tenemos la opción de pasarlos por valor, y de este modo protegerlos. Pero si se trata de objetos de gran tamaño, resulta muy costoso (en términos de memoria y tiempo de proceso) hacer copias de tales objetos, y es preferible usar una referencia. Si queremos dejar patente que la función no modificará el valor del objeto declararemos el parámetro como una referencia constante.
Cuando se aplica al valor de retorno de una variable, como en el cuarto caso, el significado es análogo. Evidentemente, cuando el valor de retorno no es una referencia, no tiene sentido declararlo como constante, ya que lo será siempre. Pero cuando se trate de referencias, este modificador impide que la variable referenciada sea modificada.
#include <iostream> using namespace std; int y; const int &funcion(); int main() { // funcion()++; // Ilegal (1) cout << ", " << y << endl; return 0; } const int &funcion() { return y; } |
Como vemos en (1) no nos es posible modificar el valor de la referencia devuelta por "funcion".
El último caso, cuando el modificador se añade al final de un prototipo de función de una clase o estructura, indica que tal función no modifica el valor de ningun dato miembro del objeto. Cuando veamos con detalle clases, veremos que tiene gran utilidad.
Punteros constantes y punteros a constantes
Este matiz es importante, no es lo mismo un puntero constante que un puntero a una constante.
Declaración de un puntero constante:
<tipo> *const <identificador>=<valor inicial> Declaración de un puntero a una constante: const <tipo> *<identificador>[=<valor inicial>] |
En el primero caso estaremos declarando un objeto constante de tipo puntero. Por lo tanto, deberemos proporcionar un valor inicial, y este puntero no podrá apuntar a otros objetos durante toda la vida del programa.
En el segundo caso estaremos declarando un puntero a un objeto constante. El puntero podrá apuntar a otros objetos, pero ningún objeto apuntado mediate este puntero podrá ser modificado.
A un puntero a constante se le pueden asignar tanto direcciones de objetos constantes como de objetos no constantes:
... int x = 100; // Objeto entero const int y = 200; // Objeto entero constante const int *cp; // Puntero a entero constante cp = &x; // Asignamos a cp la dirección de un objeto no constante cp = &y; // Asigmanos a cp la dirección de un objeto constante ... |
Lo que está claro es que cualquier objeto referenciado por cp nunca podrá ser modificado mediante ese puntero:
... int x = 100; // Objeto entero const int y = 200; // Objeto entero constante const int *cp; // Puntero a entero constante cp = &x; // Asignamos a cp la dirección de un objeto no constante (*cp)++; // Ilegal, cp apunta a un objeto constante x++; // Legal, x no es constante, ahora *cp contendrá el valor 101 ... |
Modificador de almacenamiento volatile
Sintaxis:
volatile <tipo> <nombre_variable>; <identificador_función> ( volatile <tipo> <nombre_variable> ); <identificador_función> volatile; |
Este modificador se usa con objetos que pueden ser modificados desde el exterior del programa, mediante procesos externos. Esta situación es común en programas multihilo o cuando el valor de ciertos objetos puede ser modificado mediante interrupciones o por hardware.
El compilador usa este modificador para omitir optimizaciones de la variable, por ejemplo, si se declara una variable sin usar el modificador volatile, el compilador o el sistema operativo puede almacenar el valor leído la primera vez que se accede a ella, bien en un registro o en la memoria caché. O incluso, si el compilador sabe que no ha modificado su valor, no actualizarla en la memoria normal. Si su valor se modifica externamente, sin que el programa sea notificado, se pueden producir errores, ya que estaremos trabajando con un valor no válido.
Usando el modificador volatile obligamos al compilador a consultar el valor de la variable en memoria cada vez que se deba acceder a ella.
Por esta misma razón es frecuente encontrar juntos los modificadores volatile y const: si la variable se modifica por un proceso externo, no tiene mucho sentido que el programa la modifique.
Las formas segunda y tercera de la sintaxis expuesta sólo se aplica a clases, y las veremos más adelante.
Modificador de almacenamiento mutable
Sintaxis:
class <identificador_clase> { ... mutable <tipo> <nombre_variable>; ... }; struct <identificador_estructura> { ... mutable <tipo> <nombre_variable>; ... }; |
Sirve para que determinados miembros de un objeto de una estructura o clase declarado como constante, puedan ser modificados.
#include <iostream> using namespace std; struct stA { int y; int x; }; struct stB { int a; mutable int b; }; int main() { const stA A = {1, 2}; // Obligatorio inicializar const stB B = {3, 4}; // Obligatorio inicializar // A.x = 0; // Ilegal (1) // A.y = 0; // B.a = 0; B.b = 0; // Legal (2) return 0; } |
Como se ve en (2), es posible modificar el miembro "b" del objeto "B", a pesar de haber sido declarado como constante. Ninguno de los otros campos, ni en "A", ni en "B", puede ser modificado.
Palabras reservadas usadas en este capítulo
auto, const, extern, mutable, register, static y volatile.
Comentarios