¿Por qué los miembros de datos estáticos deben definirse fuera de la clase por separado en C ++ (a diferencia de Java)?

39
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

No veo la necesidad de tener A::x definido por separado en un archivo .cpp (o el mismo archivo para las plantillas). ¿Por qué no se puede declarar y definir A::x al mismo tiempo?

¿Ha sido prohibido por razones históricas?

Mi pregunta principal es, ¿afectará a cualquier funcionalidad si se declararon / definieron static datos miembros al mismo tiempo (igual que Java )?

    
pregunta iammilind 20.04.2012 - 07:12

7 respuestas

15

Creo que la limitación que ha considerado no está relacionada con la semántica (¿por qué debería cambiar algo si se definiera la inicialización en el mismo archivo?) sino más bien con el modelo de compilación de C ++ que, por razones de compatibilidad con versiones anteriores, no se puede cambiar fácilmente porque se volvería demasiado complejo (admitir un nuevo modelo de compilación y el existente al mismo tiempo) o no permitiría compilar el código existente (introduciendo un nuevo modelo de compilación y eliminando el existente).

El modelo de compilación de C ++ se deriva de C, en el que se importan las declaraciones a un archivo de origen al incluir los archivos (encabezado). De esta manera, el compilador ve exactamente un archivo fuente grande, que contiene todos los archivos incluidos, y todos los archivos incluidos de esos archivos, recursivamente. Esto tiene una gran ventaja de IMO, a saber, que hace que el compilador sea más fácil de implementar. Por supuesto, puede escribir cualquier cosa en los archivos incluidos, es decir, tanto declaraciones como definiciones. Solo es una buena práctica colocar declaraciones en archivos de encabezado y definiciones en archivos .c o .cpp.

Por otro lado, es posible tener un modelo de compilación en el que el compilador sabe muy bien si está importando la declaración de un símbolo global que está definido en otro módulo , o si está compilando la definición de un símbolo global proporcionado por el módulo actual . Solo en este último caso, el compilador debe poner este símbolo (por ejemplo, una variable) en el actual archivo de objeto.

Por ejemplo, en GNU Pascal puede escribir una unidad a en un archivo a.pas gusta esto:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

donde la variable global se declara e inicializa en el mismo archivo fuente.

Luego puedes tener diferentes unidades que importan y usan la variable global MyStaticVariable , por ej. una unidad b ( b.pas ):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

y una unidad c ( c.pas ):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Finalmente, puedes usar las unidades byc en un programa principal m.pas :

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Puedes compilar estos archivos por separado:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

y luego genere un ejecutable con:

$ gpc -o m m.o a.o b.o c.o

y ejecútalo:

$ ./m
1
2
3

El truco aquí es que cuando el compilador ve una directiva usa en un módulo de programa (por ejemplo, usa a en b.pas), no incluye el archivo .pas correspondiente, pero busca un archivo .gpi, es decir, un archivo de interfaz precompilado (Consulte la documentación ). Estos archivos .gpi son generados por el compilador junto con los archivos .o cuando se compila cada módulo. Por lo tanto, el símbolo global MyStaticVariable solo se define una vez en el archivo de objeto a.o .

Java funciona de una manera similar: cuando el compilador importa una clase A a la clase B, busca el archivo de clase para A y no necesita el archivo A.java . Por lo tanto, todas las definiciones e inicializaciones para la clase A se pueden colocar en un archivo fuente.

Volviendo a C ++, la razón por la que en C ++ tiene que definir miembros de datos estáticos en un archivo separado está más relacionada con el modelo de compilación de C ++ que con las limitaciones impuestas por el vinculador u otras herramientas utilizadas por el compilador. En C ++, importar algunos símbolos significa construir su declaración como parte de La unidad de compilación actual. Esto es muy importante, entre otras cosas, Por la forma en que se compilan las plantillas. Pero esto implica que no puede / no debe definir ningún símbolo global (funciones, variables, métodos, miembros de datos estáticos) en un archivo incluido, de lo contrario estos símbolos podrían ser Se define de forma múltiple en los archivos de objetos compilados.

    
respondido por el Giorgio 21.04.2012 - 10:58
40

Dado que los miembros estáticos se comparten entre TODAS las instancias de una clase, deben definirse en un solo lugar. En realidad, son variables globales con algunas restricciones de acceso.

Si intenta definirlos en el encabezado, se definirán en cada módulo que incluya ese encabezado y obtendrá errores durante el enlace al encontrar todas las definiciones duplicadas.

Sí, esto es al menos en parte un problema histórico que data de cfront; se podría escribir un compilador que crearía una especie de "static_members_of_everything.cpp" oculto y un enlace a eso. Sin embargo, rompería la compatibilidad hacia atrás y no habría ningún beneficio real al hacerlo.

    
respondido por el mjfgates 20.04.2012 - 07:24
6

La razón probable de esto es que mantiene el lenguaje C ++ implementable en entornos donde el archivo de objetos y el modelo de vinculación no admiten la fusión de varias definiciones de varios archivos de objetos.

Una declaración de clase (llamada declaración por buenas razones) se obtiene en varias unidades de traducción. Si la declaración contenía definiciones para variables estáticas, entonces terminaría con múltiples definiciones en múltiples unidades de traducción (y recuerde, estos nombres tienen vínculos externos).

Esa situación es posible, pero requiere que el vinculador maneje múltiples definiciones sin quejarse.

(Y tenga en cuenta que esto entra en conflicto con la Regla de definición única, a menos que se pueda hacer de acuerdo con el tipo de símbolo o el tipo de sección en que se encuentra).

    
respondido por el Kaz 20.04.2012 - 08:00
6

Hay una gran diferencia entre C ++ y Java.

Java opera en su propia máquina virtual que crea todo en su propio entorno de tiempo de ejecución. Si se ve una definición más de una vez, simplemente actuará sobre el mismo objeto que el entorno de ejecución conoce en última instancia.

En C ++ no hay un "propietario de conocimiento definitivo": C ++, C, Fortran Pascal, etc. son todos "traductores" de un código fuente (archivo CPP) a un formato intermedio (el archivo OBJ o el archivo ".o" , dependiendo del sistema operativo) donde las declaraciones se convierten en instrucciones de la máquina y los nombres se convierten en direcciones indirectas mediadas por una tabla de símbolos.

El compilador no crea un programa, sino otro programa (el "vinculador") que une todos los OBJ-s (sin importar el idioma del que provengan) al volver a apuntar todas las direcciones hacia los símbolos. , hacia su definición efectiva.

Por la forma en que funciona el enlazador, una definición (lo que crea el espacio físico para una variable) debe ser única.

Tenga en cuenta que C ++ no se enlaza por sí mismo y que el vinculador no se emite según las especificaciones de C ++: el vinculador existe debido a la forma en que se construyen los módulos del sistema operativo (generalmente en C y ASM). C ++ tiene que usarlo como es.

Ahora: un archivo de encabezado es algo que se "pega" en varios archivos CPP. Cada archivo CPP se traduce independientemente de cada otro. Un compilador que traduce diferentes archivos CPP, todos los que reciban en una definición misma colocarán el " código de creación " para el objeto definido en todos los OBJs resultantes.

El compilador no sabe (y nunca sabrá) si todos esos OBJ se usarán juntos para formar un solo programa o por separado para formar diferentes programas independientes.

El enlazador no sabe cómo y por qué existen las definiciones y de dónde provienen (ni siquiera sabe acerca de C ++: cada "lenguaje estático" puede producir definiciones y referencias para vincular). Simplemente sabe que hay referencias a un "símbolo" dado que está "definido" en una dirección resultante.

Si hay varias definiciones (no confunda las definiciones con las referencias) para un símbolo dado, el vinculador no tiene conocimiento (por lo que se refiere al lenguaje) sobre qué hacer con ellas.

Es como unir una cantidad de ciudades para formar una gran ciudad: si se te encuentra con dos " Time square " y varias personas que vienen de afuera piden ir a " Time square ", no puede decidir sobre una base técnica pura (sin ningún conocimiento sobre las políticas que asignaron esos nombres y estará a cargo de administrarlos) en qué lugar exacto enviar. ellos.

    
respondido por el Emilio Garavaglia 21.04.2012 - 09:10
5

Se requiere porque, de lo contrario, el compilador no sabe dónde colocar la variable. Cada archivo cpp se compila individualmente y no se conoce el otro. El enlazador resuelve variables, funciones, etc. Personalmente, no veo cuál es la diferencia entre los miembros de vtable y static (no tenemos que elegir en qué archivo se definen los vtable).

En general, asumo que es más fácil para los escritores de compiladores implementarlo de esa manera. Existen vars estáticos fuera de clase / estructura y quizás por razones de coherencia o porque sería más fácil de implementar para los escritores de compiladores que definieron esa restricción en los estándares.

    
respondido por el user2528 21.04.2012 - 05:57
2

Creo que encontré la razón. La definición de la variable static en un espacio separado permite inicializarla a cualquier valor. Si no se inicializa, el valor predeterminado será 0.

Antes de C ++ 11, la inicialización en clase no estaba permitida en C ++. Así que un no puede escribir como:

struct X
{
  static int i = 4;
};

Por lo tanto, ahora, para inicializar la variable, se debe escribir fuera de la clase como:

struct X
{
  static int i;
};
int X::i = 4;

Como se analiza en otras respuestas también, int X::i ahora es global y declarar global en muchos archivos provoca un error de enlace de varios símbolos.

Por lo tanto, uno tiene que declarar una variable de clase static dentro de una unidad de traducción separada. Sin embargo, aún se puede argumentar que de la siguiente manera se debe indicar al compilador que no cree múltiples símbolos

static int X::i = 4;
^^^^^^
    
respondido por el iammilind 18.11.2012 - 09:00
0

A :: x es solo una variable global pero con espacio de nombres a A, y con restricciones de acceso.

Alguien todavía tiene que declararlo, como cualquier otra variable global, y eso incluso puede hacerse en un proyecto que está estáticamente vinculado al proyecto que contiene el resto del código A.

Llamaría a estos todo mal diseño, pero hay algunas características que puedes explotar de esta manera:

  1. el orden de llamada del constructor ... No es importante para un int, pero para un miembro más complejo que quizás acceda a otras variables estáticas o globales, puede ser crítico.

  2. el inicializador estático: puedes dejar que un cliente decida a qué debe inicializarse A :: x.

  3. en c ++ yc, ya que tiene acceso total a la memoria a través de los punteros, la ubicación física de las variables es significativa. Hay cosas muy malas que puedes explotar en función de la ubicación de una variable en un objeto de enlace.

Dudo que estos sean "por qué" ha surgido esta situación. Es probable que solo sea una evolución de C que se convierta en C ++ y un problema de compatibilidad con versiones anteriores que le impida cambiar el idioma ahora.

    
respondido por el James Podesta 23.02.2017 - 07:21

Lea otras preguntas en las etiquetas