10: Control de nombres

Tabla de contenidos

10.1. Los elementos estáticos de C
10.2. Espacios de nombres
10.3. Miembros estáticos en C++
10.4. Dependencia en la inicialización de variables estáticas
10.5. Especificaciones de enlazado alternativo
10.6. Resumen
10.7. Ejercicios

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.

10.1. Los elementos estáticos de 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.

10.1.1. Variables estáticas dentro de funciones

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.

Objetos estáticos dentro de funciones

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.

Destructores de objetos estáticos

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.

10.1.2. Control del enlazado

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.

Confusión

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).

10.1.3. Otros especificadores para almacenamiento de clases

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.