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++ ofrece un gran control sobre la creación y visibilidad de nombres, el lugar donde se almacenan y el enlazado de nombres. La palabra reservada static estaba sobrecargada en C incluso antes de que la mayoría de la gente supiera qué 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 una 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 el acceso a los nombres mediante el uso de la palabra reservada de C++ namespace. También descubrirá cómo utilizar funciones que fueron escritas y compiladas en C.
Tanto en C como en C++ la palabra reservada 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 un á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 de 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 array 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 marco 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 y se devuelve el primer carácter del array. Cada llamada posterior a oneChar() sin argumentos produce el valor por defecto cero para charArray, que indica a la función que todavía se están extrayendo caracteres del valor de s inicializado previamente. La función continuará devolviendo caracteres hasta que alcance el terminador nulo del array de caracteres, momento en el que deja de incrementar el puntero evitando que éste sobrepase la última posición del array.
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 predefinido, 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, incluido el hecho de 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 al llamar a exit()) utilizando la función de librería estándar de C atexit(). En este caso, las funciones registradas con atexit() pueden ser 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 se ejecuta 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, que corresponde al fichero de traza, 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 que su definición (a diferencia de una declaración tipo extern) aparezca al principio del fichero, antes de cualquier posible uso de out. De lo contrario estaría 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 en el destructor 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 para el enlazador 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, se produzcan conflictos 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 enlazado interno. Esto significa que puede usar el mismo nombre en otras unidades de traducción sin conflicto de nombres.
Una ventaja del enlazado interno es que el nombre puede situarse en un fichero de cabecera sin tener que preocuparse de si habrá o no un conflicto de nombres durante el enlazado. Los nombres que aparecen usualmente en los ficheros 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 extiende 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 enlazado interno. El tipo de almacenamiento no se altera: el objeto reside en el área de datos estática tanto si su visibilidad es static como si es extern.
En el caso de las 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 declaración sin adornos
void f();
y si utiliza
static void f();
significa que f() es visible sólo para esta unidad de traducción (esto suele llamarse estático a fichero o 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 ésa 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.