Tabla de contenidos
La creación de nombres es una actividad fundamental en la programación, y cuando un proyecto empieza a crecer, el número de nombres puede llegar a ser inmanejable con facilidad.
C++ permite
gran control sobre la creación y visibilidad de
nombres, el lugar donde se almacenan y el enlazado de nombres. La
palabra clave static
estaba sobrecargada en C incluso
antes de que la mayoría de la gente supiera que significaba el
término «sobrecargar». C++ ha añadido además otro
significado. El concepto subyacente bajo todos los usos de
static
parece ser «algo que mantiene su
posición» (como la electricidad estática), sea manteniendo
un ubicación física en la memoria o su visibilidad en un fichero.
En este capítulo aprenderá cómo static
controla el
almacenamiento y la visibilidad, así como una forma mejorada para
controlar los nombres mediante el uso de la palabra clave de C++
namespace
. También descubrirá como utilizar funciones que
fueron escritas y compiladas en C.
Tanto en C como en C++ la palabra clave static
tiene dos
significados básicos que, desafortunadamente, a menudo se confunden:
Almacenado una sola vez en una dirección de memoria fija. Es decir, el objeto se crea en una área de datos estática especial en lugar de en la pila cada vez que se llama a una función. Éste es el concepto de almacenamiento estático.
Local a una unidad de traducción particular (y también local
para el ámbito de una clase en C++, tal como se verá
después). Aquí, static
controla la visibilidad de
un nombre, de forma que dicho nombre no puede ser visto
fuera del la unidad de traducción o la clase. Esto también
corresponde al concepto de enlazado, que determina qué
nombres verá el enlazador.
En esta sección se van a analizar los significados anteriores de
static
tal y como se heredaron de C.
Cuando se crea una variable local dentro de una función, el compilador reserva espacio para esa variable cada vez que se llama a la función moviendo hacia abajo el puntero de pila tanto como sea preciso. Si existe un inicializador para la variable, la inicialización se realiza cada vez que se pasa por ese punto de la secuencia.
No obstante, a veces es deseable retener un valor entre llamadas
a función. Esto se puede lograr creando una variable global,
pero entonces esta variable no estará únicamente bajo control de
la función. C y C++ permiten crear un objeto static
dentro de
una función. El almacenamiento de este objeto no se lleva a cabo
en la pila sino en el área de datos estáticos del
programa. Dicho objeto sólo se inicializa una vez, la primera
vez que se llama a la función, y retiene su valor entre
diferentes invocaciones. Por ejemplo, la siguiente función
devuelve el siguiente carácter del vector cada vez que se la
llama:
//: C10:StaticVariablesInfunctions.cpp #include "../require.h" #include <iostream> using namespace std; char oneChar(const char* charArray = 0) { static const char* s; if(charArray) { s = charArray; return *s; } else require(s, "un-initialized s"); if(*s == '\0') return 0; return *s++; } char* a = "abcdefghijklmnopqrstuvwxyz"; int main() { // oneChar(); // require() fails oneChar(a); // Initializes s to a char c; while((c = oneChar()) != 0) cout << c << endl; } ///:~
Listado 10.1. C10/StaticVariablesInfunctions.cpp
La variable static char* s
mantiene su valor
entre llamadas a oneChar()
porque no está
almacenada en el segmento de pila de la función, sino que está
en el área de almacenamiento estático del programa. Cuando se
llama a oneChar()
con char*
como argumento, s
se asigna a ese argumento
de forma que se devuelve el primer carácter del array. Cada
llamada posterior a oneChar()
sin argumentos devuelve el valor por
defecto cero para charArray
, que indica a
la función que todavía se están extrayendo caracteres del
valor previo de s
. La función continuará
devolviendo caracteres hasta que alcance el valor de final del
vector, momento en el que para de incrementar el puntero
evitando que éste sobrepase la última posición del vector.
Pero ¿qué pasa si se llama a oneChar()
sin argumentos y sin haber inicializado previamente el valor
de s
? En la definición para
s
, se podía haber utilizado la
inicialización,
static char* s = 0;
pero si no se incluye un valor inicial para una variable
estática de un tipo definido, el compilador garantiza que la
variable se inicializará a cero (convertido al tipo adecuado)
al comenzar el programa. Así pues, en
oneChar()
, la primera vez que se llama a
la función, s
vale cero. En este caso, se
cumplirá la condición if(!s)
.
La inicialización anterior para s
es muy
simple, pero la inicialización para objetos estáticos (como la
de cualquier otro objeto) puede ser una expresión arbitraria,
que involucre constantes, variables o funciones previamente
declaradas.
Fíjese que la función de arriba es muy vulnerable a problemas de concurrencia. Siempre que diseñe funciones que contengan variables estáticas, deberá tener en mente este tipo de problemas.
Las reglas son las mismas para objetos estáticos de tipos definidos por el usuario, añadiendo el hecho que el objeto requiere ser inicializado. Sin embargo, la asignación del valor cero sólo tiene sentido para tipos predefinidos. Los tipos definidos por el usuario deben ser inicializados llamando a sus respectivos constructores. Por tanto, si no especifica argumentos en los constructores cuando defina un objeto estático, la clase deberá tener un constructor por defecto. Por ejemplo:
//: C10:StaticObjectsInFunctions.cpp #include <iostream> using namespace std; class X { int i; public: X(int ii = 0) : i(ii) {} // Default ~X() { cout << "X::~X()" << endl; } }; void f() { static X x1(47); static X x2; // Default constructor required } int main() { f(); } ///:~
Listado 10.2. C10/StaticObjectsInFunctions.cpp
Los objetos estáticos de tipo X
dentro de
f()
pueden ser inicializados tanto con
la lista de argumentos del constructor como con el
constructor por defecto. Esta construcción ocurre únicamente
la primera vez que el control llega a la definición.
Los destructores para objetos estáticos (es decir, cualquier
objeto con almacenamiento estático, no sólo objetos
estáticos locales como en el ejemplo anterior) son invocados
cuando main()
finaliza o cuando la
función de librería estándar de C
exit()
se llama explícitamente. En la
mayoría de implementaciones, main()
simplemente llama a exit()
cuando
termina. Esto significa que puede ser peligroso llamar a
exit()
dentro de un destructor porque
podría producirse una invocación recursiva infinita. Los
destructores de objetos estáticos no se invocan si se sale
del programa utilizando la función de librería estándar de C
abort()
.
Es posible especificar acciones que se lleven a cabo tras
finalizar la ejecución de main()
(o
llamando a exit()
) utilizando la
función de librería estándar de C
atexit()
. En este caso, las funciones
registradas en atexit()
serán invocadas
antes de los destructores para cualquier objeto construido
antes de abandonar main()
(o de llamar
a exit()
).
Como la destrucción ordinaria, la destrucción de objetos
estáticos se lleva a cabo en orden inverso al de la
inicialización. Hay que tener en cuenta que sólo los objetos
que han sido construidos serán destruidos. Afortunadamente,
las herramientas de desarrollo de C++ mantienen un registro
del orden de inicialización y de los objetos que han sido
construidos. Los objetos globales siempre se construyen
antes de entrar en main()
y se
destruyen una vez se sale, pero si existe una función que
contiene un objeto local estático a la que nunca se llama,
el constructor de dicho objeto nunca fue ejecutado y, por
tanto, nunca se invocará su destructor. Por ejemplo:
//: C10:StaticDestructors.cpp // Static object destructors #include <fstream> using namespace std; ofstream out("statdest.out"); // Trace file class Obj { char c; // Identifier public: Obj(char cc) : c(cc) { out << "Obj::Obj() for " << c << endl; } ~Obj() { out << "Obj::~Obj() for " << c << endl; } }; Obj a('a'); // Global (static storage) // Constructor & destructor always called void f() { static Obj b('b'); } void g() { static Obj c('c'); } int main() { out << "inside main()" << endl; f(); // Calls static constructor for b // g() not called out << "leaving main()" << endl; } ///:~
Listado 10.3. C10/StaticDestructors.cpp
En Obj
, char c
actúa
como un identificador de forma que el constructor y el
destructor pueden imprimir la información acerca del objeto
sobre el que actúan. Obj a
es un objeto global
y por tanto su constructor siempre se llama antes de que el
control pase a main()
, pero el
constructor para static Obj b
dentro de
f()
, y el de static Obj c
dentro de g()
sólo serán invocados si
se llama a esas funciones.
Para mostrar qué constructores y qué destructores serán
llamados, sólo se invoca a f()
. La
salida del programa será la siguiente:
Obj::Obj() for a inside main() Obj::Obj() for b leaving main() Obj::~Obj() for b Obj::~Obj() for a
El constructor para a
se invoca antes de
entrar en main()
y el constructor de
b
se invoca sólo porque existe una
llamada a f()
. Cuando se sale de
main()
, se invoca a los destructores de
los objetos que han sido construidos en orden inverso al de
su construcción. Esto significa que si llama a
g()
, el orden en el que los
destructores para b
y
c
son invocados depende de si se llamó
primero a f()
o a
g()
.
Nótese que el objeto out
de tipo
ofstream
, utilizado en la gestión de
ficheros, también es un objeto estático (puesto que está
definido fuera de cualquier función, reside en el área de
almacenamiento estático). Es importante remarcar que su
definición (a diferencia de una declaración tipo
extern
) aparece al principio del fichero, antes de
cualquier posible uso de out
. De lo
contrario estaríamos utilizando un objeto antes de que
estuviese adecuadamente inicializado.
En C++, el constructor de un objeto estático global se
invoca antes de entrar en main()
, de
forma que ya dispone de una forma simple y portable de
ejecutar código antes de entrar en
main()
, así como ejecutar código
después de salir de main()
. En C, eso
siempre implicaba revolver el código ensamblador de arranque
del compilador utilizado.
Generalmente, cualquier nombre dentro del ámbito del fichero (es decir, no incluido dentro de una clase o de una función) es visible para todas las unidades de traducción del programa. Esto suele llamarse enlazado externo porque durante el enlazado ese nombre es visible desde cualquier sitio, desde el exterior de esa unidad de traducción. Las variables globales y las funciones ordinarias tienen enlazado externo.
Hay veces en las que conviene limitar la visibilidad de un nombre. Puede que desee tener una variable con visibilidad a nivel de fichero de forma que todas las funciones de ese fichero puedan utilizarla, pero quizá no desee que funciones externas a ese fichero tengan acceso a esa variable, o que de forma inadvertida, cause solapes de nombres con identificadores externos a ese fichero.
Un objeto o nombre de función, con visibilidad dentro del
fichero en que se encuentra, que es explícitamente declarado
como static
es local a su unidad de traducción (en
términos de este libro, el fichero cpp
donde se lleva a cabo la declaración). Este nombre tiene
enlace interno. Esto significa que puede
usar el mismo nombre en otras unidades de traducción sin
confusión entre ellos.
Una ventaja del enlace interno es que el nombre puede situarse
en un fichero de cabecera sin tener que preocuparse de si
habrá o no un choque de nombres durante el enlazado. Los
nombres que aparecen usualmente en los archivos de cabecera,
como definiciones const
y funciones inline
,
tienen por defecto enlazado interno. (De todas formas,
const
tiene por defecto enlazado interno sólo en C++;
en C tiene enlazado externo). Nótese que el enlazado se
refiere sólo a elementos que tienen direcciones en tiempo de
enlazado / carga. Por tanto, las declaraciones de clases y de
variables locales no tienen enlazado.
He aquí un ejemplo de cómo los dos significados de
static
pueden confundirse. Todos los objetos
globales tienen implícitamente almacenamiento de tipo
estático, o sea que si usted dice (en ámbito de fichero)
int a = 0;
el almacenamiento para a
se llevará a
cabo en el área para datos estáticos del programa y la
inicialización para a
sólo se realizará
una vez, antes de entrar en
main()
. Además, la visibilidad de
a
es global para todas las unidades de
traducción. En términos de visibilidad, lo opuesto a
static
(visible tan sólo en su :unidad de
traducción) es extern
que establece explícitamente
que la visibilidad del nombre se extienda a todas las
unidades de traducción. Es decir, la definición de arriba
equivale a
extern int a = 0;
Pero si utilizase
static int a = 0;
todo lo que habría hecho es cambiar la visibilidad, de forma
que a
tiene enlace interno. El tipo de
almacenamiento no se altera, el objeto reside en el área de
datos estática aunque en este caso su visibilidad es
static
y en el otro es extern
.
Cuando pasamos a hablar de variables locales,
static
deja de alterar la visibilidad y pasa a
alterar el tipo de almacenamiento.
Si declara lo que parece ser una variable local como
extern
, significa que el almacenamiento existe en
alguna otra parte (y por tanto la variable realmente es
global a la función). Por ejemplo:
//: C10:LocalExtern.cpp //{L} LocalExtern2 #include <iostream> int main() { extern int i; std::cout << i; } ///:~
Listado 10.4. C10/LocalExtern.cpp
Para nombres de funciones (sin tener en cuenta las funciones
miembro), static
y extern
sólo pueden
alterar la visibilidad, de forma que si escribe
extern void f();
es lo mismo que la menos adornada declaración
void f();
y si utiliza
static void f();
significa que f()
es visible sólo para
la unidad de traducción, (esto suele llamarse estático a
fichero (file static).
El uso de static
y extern
está muy
extendido. Existen otros dos especificadores de tipo de
almacenamiento bastante menos conocidos. El especificador
auto
no se utiliza prácticamente nunca porque le dice
al compilador que esa es una variable local. auto
es
la abreviatura de automático«» y se refiere a la
forma en la que el compilador reserva espacio automáticamente
para la variable. El compilador siempre puede determinar ese
hecho por el contexto en que la variable se define por lo que
auto
es redundante.
El especificador register
aplicado a una variable
indica que es una variable local (auto
), junto con la
pista para el compilador de que esa variable en concreto va a
ser ampliamente utilizada por lo que debería ser almacenada en
un registro si es posible. Por tanto, es una ayuda para la
optimización. Diferentes compiladores responden de diferente
manera ante dicha pista; incluso tienen la opción de
ignorarla. Si toma la dirección de la variable, el
especificador register
va a ser ignorado casi con
total seguridad. Se recomienda evitar el uso de
register
porque, generalmente, el compilador suele
realizar las labores de optimización mejor que el usuario.