4.11.7 Miembros estáticos
§1 Sinopsis
El especificador de tipo de almacenamiento static ( 4.1.8c) puede utilizarse en la declaración de propiedades y métodos de clases. Tales miembros se denominan estáticos y tienen distintas propiedades que el resto. En cada instancia de la clase existe una copia de los miembros no-estáticos, aunque solo una de estáticos para todas las instancias. La singularidad de estas copias conduce a algunas particularidades no permitidas al resto de los miembros. Por ejemplo, pueden ser accedidas sin referencia a ninguna instancia concreta de la clase y deben ser definidas como si fuesen variables estáticas normales; además se permiten algunas singularidades en la notación de acceso a estos miembros.
§2 Propiedades estáticas
El punto importante para comprender el porqué y el cómo de los miembros estáticos, es conocer que con ellos ocurre algo parecido que con las funciones-miembro ( 4.11.5). Aunque convencionalmente se acepta que los objetos contienen un sub-conjunto de "todas" las propiedades de la clase, esto solo es cierto para las variables no-estáticas. Las propiedades estáticas se comportan aquí de forma parecida a las variables estáticas de funciones normales. Solo existe una copia de ellas, que en realidad está en el objeto-clase ( 4.11.5). Se trata por tanto de propiedades de clase. A cada instancia le basta un puntero a los valores del objeto-clase y cuando desde una instancia cualquiera accedemos a una de estas propiedades, en realidad se accede a esta única copia.
Lo ponemos de manifiesto con un sencillo experimento:
#include <iostream.h>
class C { public: static int x; };
int C::x = 13; // L.3: definición
int main () { // ================
C c1, c2;
cout << "Valor c1.x == " << c1.x << endl;
cout << "Valor c2.x == " << c2.x << endl;
c1.x = 22;
cout << "Valor c1.x == " << c1.x << endl;
cout << "Valor c2.x == " << c2.x << endl;
}
Salida:
Valor c1.x == 13
Valor c2.x == 13
Valor c1.x == 22
Valor c2.x == 22
En el ejemplo se han instanciado dos objetos, c1 y c2. El valor inicial 13 asignado a la variable estática x en L.3 (volveremos de inmediato a esta "extraña" sentencia ), es puesto de manifiesto en las dos primeras salidas. A continuación vemos como la modificación del valor x en el objeto c1 tiene la virtualidad de cambiar dicha propiedad x en el segundo objeto.
Observe que definiciones como la de L.3 del ejemplo anterior, solo son permitidas para miembros estáticos y son posibles con independencia de que sean públicos o privados es decir:
class C {
static char* ptr1; // privado por defecto
public:
static char* ptr2; // declaración
char* ptr3;
};
char* C::ptr1 = "Adios"; // Ok. Definición
char* C::ptr2 = "mundo"; // Ok
char* C::ptr3 = "cruel"; // Error !! ptr3 no es estática
Lo anterior no significa que las propiedades estáticas, privadas o protegidas, puedan ser accedidas directamente
desde el exterior es decir:
...
func () {
...
cout << "Valor de ptr1: " << C::ptr1 << endl; // Error: no accesible!
cout << "Valor de ptr2: " << C::ptr2 << endl; // Ok: -> "mundo"
}
De todo esto se derivan algunas consecuencias teóricas y prácticas. La primera
importante es que al existir en el objeto-clase, las propiedades estáticas no dependen de ninguna instancia para su existencia.
Es decir, existen incluso antes que ninguna instancia de la clase. Esto ha sido
ya puesto de manifiesto en la línea 3 del ejemplo anterior
, donde hemos iniciado la variable estática x
antes que se haya instanciado ningún objeto de la clase.
§3 Definición de miembros estáticos
Puesto que las declaraciones de miembros estáticos existentes en la declaración de una clase no son definiciones ( 4.1.2), y estos miembros existen antes que ninguna instancia de la clase, la consecuencia es que debe proporcionarse una definición en algún sitio para proveer de espacio de almacenamiento e inicialización (en condiciones normales, esta tarea es encomendada a los constructores 4.11.2d1). Considere el siguiente ejemplo:
class C {
static y; // int por defecto en algunos compiladores
public: int x;
static int* p;
static char* c;
static int gety () { return y; }
};
Al compilar se producirán tres errores de enlazado Unresolved external...
, correspondientes a los miembros
C::y, C::p y C::c; señalando que las variables estáticas están declaradas pero no definidas (no tienen
espacio de almacenamiento). Para evitarlo, podemos hacer:
class C {
static y;
public: int x;
static int* p;
static char* c;
static int gety () { return y; }
};
...
int C::y = 1; // no es necesario poner static (pero si int!!)
int* C::p = &C::y; // ídem int*
char* C::c = "ABC"; // ídem char*
...
Las asignaciones de las tres últimas líneas proporcionan espacio de almacenamiento, a la vez que pueden servir de
inicializadores de los miembros correspondientes. Observe especialmente la notación empleada. Los especificadores de tipo:
int, int* y char* son necesarios. En cambio, no es preciso
repetir aquí la palabra static.
Nota: esta inicialización de las constantes estáticas fuera del cuerpo de la clase, es una excepción de la regla general C++ de que las propiedades de clases solo pueden inicializarse en el cuerpo del constructor o en la lista de inicializadores ( 4.11.2d3). La razón es que los miembros estáticos no son parte de los objetos de la clase sino objetos independientes [3].
§4 Iniciar miembros estáticos
§4.1 Iniciar constantes estáticas
Recordemos que excepcionalmente, las constantes estáticas pueden ser iniciadas en el cuerpo de la clase ( 4.11.2a). Es decir, se permiten expresiones del tipo [1]:
class C {
static const int k1 = 2; // Ok:
static const float f1 = 2.0; // Ok:
static Etiquetas objetos[MAXNUM];
...
};
const int C::k1;
cons float C::f1;
Etiquetas C::cargos[MAXNUM];
Observe que en este caso aún es necesario declarar el miembro fuera de la clase, aunque no es necesaria aquí su inicialización.
Nota: recordar que es posible sustituir una constante estática entera por un enumerador ( 4.11.2a).
A este respecto tenga en cuenta que los miembros
estáticos de una clase global pueden ser inicializados como objetos globales ordinarios, pero solo dentro del ámbito del fichero.
Es oportuno señalar que la inclusión de un constructor explícito en la declaración de la clase no hubiese evitado tener que incluir las tres últimas sentencias para definición de los miembros respectivos. Por ejemplo, el código que sigue daría los mismos errores de compilación que el anterior.
class C {
static y;
public: int x;
static int* p;
static char* c;
static int gety () { return y; }
C () { // constructor por defecto
y = 1;
p = &y;
c = "ABC";
};
La razón es evidente: el constructor es invocado cuando se instancia un miembro de la clase, mientras que los miembros estáticos (que en realidad "pertenecen" a la clase y no a las instancias), tienen existencia incluso antes de existir ninguna instancia concreta .
Como consecuencia directa, si se incluye una asignación a un miembro estático dentro del constructor, al ser esta asignación posterior a la que se realiza en la definición, los valores indicados en el constructor machacarán a los que existieran en la definición.
Ejemplo
#include <iostream>
using namespace std;
class A {a
public: static int x; // miembro estático
A(int i = 12) { x = i; } // constructor por defecto
};
int A::x = 13; // definición de miembro
int main() { // ==============
cout << "Valor de A.x: " << A::x << endl;
A a1; // Invoca al constructor.
cout << "Valor de A.x: " << A::x << endl;
return 0;
}
Salida:
Valor de A.x: 13
Valor de A.x: 12
Para verificar la sucesión de los hechos, construimos otro sencillo experimento, añadiendo algunas
instrucciones al ejemplo anterior:
#include <iostream.h>
class C {
static y; // int por defecto
public: int x;
static int* p;
static char* c;
static int gety () { return y; }
C () { // constructor por defecto
y = 1; // iniciadores de "instancia"
x = 3;
p = &y;
c = "ABC";
}
};
int C::y = 20; // iniciadores de "clase"
int* C::p = &C::y;
char* C::c = "abc";
int main () { // ===============
cout << "Valor .y == " << C::gety() << endl;
cout << "Valor .p == " << *(C::p) << endl;
cout << "Valor .c == " << C::c << endl;
/*cout << "Valor .x == " << C::x << endl; ERROR:
Member C::x cannot be used without an object in function main() */
C c1;
cout << "Valor c1.y == " << c1.gety() << endl;
cout << "Valor c1.p == " << *(c1.p) << endl;
cout << "Valor c1.c == " << c1.c << endl;
cout << "Valor c1.x == " << c1.x << endl;
}
Salida:
Valor .y == 20
Valor .p == 20
Valor .c == abc
Valor c1.y == 1
Valor c1.p == 1
Valor c1.c == ABC
Valor c1.x == 3
§5 Características de los miembros estáticos
En este sencillo programa comprobamos un buen montón de las características especiales de los miembros estáticos:
Las tres primeras salidas se producen antes de instanciar ningún objeto:
Los miembros estáticos tienen existencia antes que cualquier instancia de la clase.
Los valores iniciales son debidos a los iniciadores de clase.
El intento de construir una cuarta salida siguiendo la pautas de las anteriores con el miembro x no-estático conduce al previsible error de compilación (muy explícito por cierto).
§5.1 Particularidades de la notación.
Observe las tres primeras sentencias de salida:
Los miembros estáticos pueden ser accedidos mediante una declaración explícita ( 4.1.11c) utilizando el operador de resolución de ámbito :: ( 4.9.19) con la notación C::miembro, que sabemos es propia de los miembros de clases y otros subespacios de nombres.
Comprobamos que las invocaciones a miembros se han realizado sin utilizar ninguna instancia concreta de C; es decir, la invocación a miembros estáticos puede realizarse sobre la clase, sin tener ningún objeto particular "en mente".
Las cuatro últimas salidas se refieren a una instancia concreta:
Los miembros son accedidos utilizando la notación convencional mediante el selector de miembro . ( 4.9.16). Esta notación también está permitida en miembros estáticos si se utiliza una instancia concreta. Si bien el manual nos advierte que en estos casos, si c es un objeto de la clase C, y cptr es un puntero a objeto de dicha clase, puede utilizarse la notación: c.x y cptr->x, aunque las expresiones c y cptr no sean evaluadas .
Como era de esperar, los valores obtenidos corresponden a los proporcionados por el constructor de la clase, que es invocado automáticamente cuando se instancia un miembro. A partir de este momento, los valores quedan modificados para cualquier otra instancia ya existente de la clase.
La última salida se refiere a un miembro no estático (privativo de esta instancia), que ha sido inicializado por el constructor. Mientras que un miembro estático puede ser accedido con o sin la sintaxis especial de los miembros de clase, el resto de los miembros (no estáticos) tiene que ser forzosamente referenciados mediante los operadores de acceso a miembros, directo . (4.9.16) o indirecto -> (4.9.16).
§5.2 Otras propiedades de las propiedades estáticas
Las peculiaridades de las constantes estáticas enteras no se limitan a que puedan ser inicializadas en el cuerpo de la clase , también pueden ser accedidas desde el exterior con el operador de acceso a ámbito, aún cuando sean declaradas miembros privados [2]. Ejemplo:
class C {
const int static x = 13; // Privado por defecto
...
};
...
func () {
cout << "Miembro privado C.x = " << C::x; // Ok:
...
}
Las clases declaradas locales a una función no pueden tener miembros estáticos [4]. Los miembros
estáticos de clases globales tienen enlazado externo (
1.4.4) y almacenamiento estático
( 4.1.8c).
Los miembros estáticos, anidados a cualquier nivel, siguen las reglas normales de acceso a miembros, excepto que pueden ser inicializados.
class C {
static int x;
static const int size = 5;
class Interior {
// clase dentro de una clase
static float f;
// declaración anidada
void func(void);
// declaración anidada
};
public : char array[size];
};
int C::x = 1;
// inicializa x
float C::Interior::f = 3.14; // initialización de estática anidada
void C::Interior::func(void) { /* defina la función anidada */ }
Por razón de esta particularidad de ser comunes a todas las instancias de la clase, los punteros a miembros estáticos
tienen características especiales no disponibles en los punteros a miembros no estáticos
( 4.2.1g).
§6 Métodos estáticos
A tenor de lo comentado hasta ahora y de lo señalado para las funciones-miembro ( 4.11.5), debemos admitir que no tiene mucho sentido hablar de funciones-miembro estáticas, ya que por definición los métodos son "estáticos", en el sentido que se alojan en el objeto-clase (en las instancias de clase solo existen punteros a dichas funciones).
La razón última de que el Estándar admita la existencia de funciones miembro estáticas, es tanto garantizar una cierta uniformidad en la categoría, como en el hecho de que en realidad, las características especiales de estos miembros les hace constituir una especie de "club" aparte del resto de miembros no estáticos.
En efecto, considere la función gety del ejemplo anterior . Teóricamente este método podría funcionar antes de la declaración de ningún objeto de la clase sin necesidad de ser estático, ya que por sí mismo cumple con los dos requisitos: existe en el objeto-clase y solo utiliza variables estáticas (que tienen existencia igualmente en el objeto-clase). La razón de que la gramática C++ exija declararla estática explícitamente, es doble: de un lado avisar al compilador que estas funciones no necesitan puntero this ( 4.11.6), ya que pueden ser invocadas sin hacer referencia a ningún objeto particular. De otro lado, implementar un cierto mecanismo de control, sin el cual surgiría una cuestión: si gety puede ser invocada antes que exista una instancia concreta de la clase C ¿Que pasa si en el cuerpo de esta función se hace referencia a una propiedad no estática?. Por ejemplo, si en el programa del ejemplo sustituimos el método gety haciendo que devuelva una variable no estática en la siguiente forma:
static int getx () { return x; }
En este caso el compilador lanza un mensaje de error: "Member C::x cannot be used without an object in function
C::getx()
" que también es bastante explícito respecto a su causa.
La solución dada para salvar el problema es que, puesto que los métodos estáticos no tienen puntero this, para acceder a las propiedades no estáticas, es necesario especificar el objeto al que correspondan dichos miembros y para esto no es posible utilizar los selectores . o -> ( 4.11.2e). Ver ejemplo a continuación .
Nota: observe que esto no es necesario en las funciones miembro no estáticas que sí tienen dicho puntero. Cuando en el cuerpo de una de estas funciones se hace referencia a una propiedad, se sabe inmediatamente que se refiere al objeto señalado por this (el objeto que invocó la función).
Ejemplo
Una redefinición simplificada del ejemplo anterior, que permitiese cumplir con los requisitos exigidos, sería la siguiente:
#include <iostream.h>
class C {
int x;
public: static int y;
// static int getx () { return x; } Error!!
// illegal reference to data member 'C::x' in a static member function
static int getx (C* cptr) { return cptr->x; } // Ok.
C () {
// constructor por defecto
x = 3;
y = 1;
}
};
int C::y = 20; // definición
int main () { // ===============
//cout << "Valor .x == " << C::getx(...) << endl; No es posible ahora!!
//cout << "Valor c1.x == " << c.getx(...) << endl; No es posible ahora!!
cout << "Valor .y == " << C::y << endl;
// Ok.
C c1;
C* cptr = &c1; // puntero a c1
cout << "Valor c1.x == " << C::getx(cptr) << endl;
// Ok.
cout << "Valor c1.x == " << c1.getx(cptr) << endl;
// Ok.
cout << "Valor c1.y == " << c1.y << endl;
// Ok.
return 0;
}
Salida:
Valor .y == 20
Valor c1.y == 1
Valor c1.x == 3
Valor c1.x == 3
§6.1 Los métodos de clases gobales tienen enlazado externo
( 1.4.4).
§6.2 Las funciones estáticas se asocian exclusivamente con la clase en que son declaradas, por tanto no pueden ser virtuales ( 4.11.8a).
§6.3 Es ilegal tener funciones estáticas y no-estáticas del mismo nombre y con los mismos argumentos.
§7 Utilidad y empleo
La utilidad principal de los miembros estáticos es guardar información común a todos los objetos de una clase. Por ejemplo, el número de instancias creadas ( Ejemplo), o el último recurso utilizado de entre un conjunto compartido por todas las instancias. También se utilizan para:
- Reducir el número de nombres globales visibles. Los miembros estáticos ejercen el papel de miembros globales de la clase sin polucionar el espacio global.
- Agrupar y evidenciar qué objetos estáticos pertenecen a qué clase
- Permitir control de acceso a sus nombres
[1] Esta particularidad se introdujo en la revisión del Estándar de Julio 1998, de forma que el comportamiento de algunos compiladores respecto a este punto, depende de su grado de adhesión al Estándar. Por ejemplo, el compilador GNU cpp 2.95 se adapta al Estándar; Visual C++ 6.0 de MS no permite esta inicialización de constantes estáticas en el interior de la clase y Borland C++ 5.5 solo lo permite con constantes estáticas enteras.
[2] Este comportamiento anómalo es específico del compilador Borland C++ 5.5 y probablemente se trate de un error, por lo que esta característica no debería ser utilizada en ningún programa C++.
[3] Stroustrup & Ellis: ACRM §9.4
[4] La razón es que tales miembros no tienen enlazado y en consecuencia no podrían ser inicializados fuera de la declaración de la clase.