Tipos de Variables VII: tipos de almacenamiento
Existen ciertos modificadores de variables que se nos estaban quedando en el tintero y que no habíamos visto todavía. Estos modificadores afectan al modo en que se almacenan las variables y a su ámbito temporal, es decir, la zona de programa desde donde las variables son accesibles.
En el capítulo 6 ya comentamos algo sobre el ámbito de las variables. Los tipos de almacenamiento son los que definen el ámbito de las variables u objetos. Los modificadores, por su parte, permiten alterar el comportamiento de esas variables.
Se distinguen dos aspectos en cuanto al ámbito: el temporal, o duración y el de acceso o alcance o visibilidad.
El ámbito de acceso es la parte de código desde el que un objeto es accesible.
El ámbito temporal se refiere al periodo de ejecución en que existe un objeto.
Ambos quedan definidos por el tipo de almacenamiento, y este tipo, a su vez, queda definido por los modificadores.
C++ dispone de los siguientes especificadores de tipo de almacenamiento: auto, extern, static y register.
Hay además algunos modificadores: const y volatile.
Por último, hay otra palabra reservada: mutable, que se suele considerar un especificador, aunque en mi opinión, es más bien un modificador.
Almacenamento automático
Para especificar el tipo de almacenamiento automático se usa el especificador auto.
Sintaxis:
[auto] <tipo> <nombre_variable>; |
Sirve para declarar variables automáticas o locales. Es el modificador por defecto cuando se declaran variables u objetos locales, es decir, si no se especifica ningún modificador, se creará una variable automática.
Estas variables se crean durante la ejecución, y se elige el tipo de memoria a utilizar en función del ámbito temporal de la variable. Una vez cumplido el ámbito, la variable es destruida. Es decir, una variable automática local de una función se creará cuando sea declarada, y se destruirá al terminar la función. Una variable local automática de un bucle será destruida cuando el bucle termine.
Debido a que estos objetos serán creados y destruidos cada vez que sea necesario, usándose, en general, diferentes posiciones de memoria, su valor se perderá cada vez que sean creadas, perdiéndose el valor previo en cada caso.
Por supuesto, no es posible crear variables automáticas globales, ya que son conceptos contradictorios.
Almacenamiento estático
Para especificar este tipo de almacenamiento se usa el especificador static.
Sintaxis:
static <tipo> <nombre_variable>; static <tipo> <nombre_de_función>(<lista_parámetros>); |
Cuando se usa en la declaración de objetos, este especificador hace que se asigne una dirección de memoria fija para elobjeto mientras el programa se esté ejecutando. Es decir, su ámbito temporal es total. En cuanto al ámbito de acceso conserva el que le corresponde según el punto del código en que aparezca la declaración.
Debido a que el objeto tiene una posición de memoria fija, su valor permanece, aunque se trate de un objeto declarado de forma local, entre distintas reentradas en el ámbito del objeto. Por ejemplo si se trata de un objeto local a una función, el valor del objeto se mantiene entre distintas llamadas a la función.
Hay que tener en cuenta que los objetos estáticos no inicializados toman un valor nulo.
Por el contrario, si se le da un valor inicial a una variable estática, la asignación sólo afecta a la primera vez que es declarada.
#include <iostream> using namespace std; int funcion(); int main() { for(int i = 0; i < 10; i++) cout << "Llamada " << i+1 << ": " << funcion() << endl; return 0; } int funcion() { static int x=10; x++; return x; } |
La salida de este programa será:
Llamada 1: 11 Llamada 2: 12 Llamada 3: 13 Llamada 4: 14 Llamada 5: 15 Llamada 6: 16 Llamada 7: 17 Llamada 8: 18 Llamada 9: 19 Llamada 10: 20 |
Nota: en realidad, el compilador analiza el código fuente y crea el código ejecutable necesario para crear todas las variables estáticas antes de empezar la ejecución el programa, asignando sus valores iniciales, si se indican o cero en caso contrario. De modo que cuando se llama a la funcion no se crea el objeto x, sino que se usa directamente. En este caso, desde el punto de vista del ámbito temporal, x se comporta como un objeto global, pero para el ámbito de acceso se trata de un objeto local de funcion. |
Este tipo de almacenamiento se usa con el fin de que las variables locales de una función conserven su valor entre distintas llamadas sucesivas a la misma. Las variables estáticas tienen un ámbito local con respecto a su accesibilidad, pero temporalmente son como las variables externas.
Parecería lógico que, análogamente a lo que sucede con el especificador auto, no tenga sentido declarar objetos globales como estáticos, ya que lo son por defecto. Sin embargo, el especificador static tiene un significado distinto cuando se aplica a objetos globales. En ese caso indica que el objeto no es accesible desde otros ficheros fuente del programa.
En el caso de las funciones, el significado es el mismo, las funciones estáticas sólo son accesibles desde el fichero en que están declaradas.
Nota: Veremos en el capítulo siguiente que esto se puede conseguir en C++ de una forma más clara usando espacios con nombre. |
Almacenamiento externo
Para especificar este tipo de almacenamiento se usa la palabra extern.
Sintaxis:
extern <tipo> <nombre_variable>; [extern] <tipo> <nombre_de_función>(<lista_parámetros>); |
De nuevo tenemos un especificador que se puede aplicar a funciones y a objetos. O más precisamente, a prototipos de funciones y a declaraciones de objetos.
Este especificador se usa para indicar que el almacenamiento y valor de una variable o la definición de una función están definidos en otro módulo o fichero fuente. Las funciones declaradas con extern son visibles por todos los ficheros fuente del programa, salvo que (como vimos más arriba) se defina la función como static.
El especificador extern sólo puede usarse con objetos y funciones globales.
En el caso de las funciones prototipo, el especificador extern es opcional. Las declaraciones de prototipos son externas por defecto.
Este especificador lo usaremos con programas que usen varios ficheros fuente, que será lo más normal con aplicaciones que no sean ejemplos o aplicaciones simples.
Veamos un ejemplo de un programa con dos ficheros fuente:
// principal.cpp #include <iostream> using namespace std; int x=100; // Declaración e inicialización int funcion(); // extern por defecto int main() { cout << funcion() << endl; return 0; } |
Fichero fuente de "modulo.cpp":
// modulo.cpp extern int x; // x es externa int funcion() { // definición de la función return x; } |
En este ejemplo vemos que no es necesario declarar el prototipo como extern. En el fichero "modulo.cpp" declaramos x como externa, ya que está declarada y definida en el fichero "principal.cpp", y porque necesitamos usar su varlor.
Se puede usar extern "c" con el fin de prevenir que algún nombre de función escrita en C pueda ser ocultado por funciones de programas C++. Este especificador no se refiere al tipo de almacenamiento, ya que sabemos que en el caso de prototipos de funciones es el especificador por defecto. En realidad es una directiva que está destinada al enlazador, y le instruye para que haga un enlazado "C", distinto del que se usa para funciones en C++.
Comentarios