Disponible la versión 6 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.1.2 Declaraciones y definiciones

§1 Sinopsis

Utilizando un léxico formalista, podemos decir que una declaración es una sentencia que introduce un nombre en una unidad de compilación ( 1.4.2) dándole existencia semántica. Esto de la "existencia semántica" es una forma elegante de decir que a partir de ahí el compilador sabe que "cosa" es (representa) ese nombre. La forma de darle existencia semántica a las entidades es declararlos (algo así como "presentarlos" formalmente en el código). Por ejemplo, si declaramos una variable x o una función func, a partir de ahí el compilador sabe que x es una variable de tal tipo, y que func es una función de características cuales.

El punto importante a resaltar aquí es que cada declaración asocia un nombre con un tipo de dato, lo que en C++ (una sociedad muy clasista 2.2) es importante, ya que el conocimiento del "tipo" que corresponde a un identificador, proporciona al compilador mucha información sobre la entidad representada por este (en muchas ocasiones "casi" toda la información necesaria ). En especial el compilador dispone de una amplia información acerca del uso de los tipos básicos; que operaciones son permitidas, y que significado tienen estas operaciones.

No olvidar que una declaración no hace nada más que esto. Es decir, no añade ninguna otra información distinta de relacionar una etiqueta con un tipo. Posteriormente, cuando esta etiqueta esté asociada con una entidad concreta (con una zona de memoria), las operaciones permitidas se realizarán sobre esta entidad. Recordemos que la entidad puede ser un objeto-dato o un algoritmo (una función).


§1.1  La declaración se completa con la definición. En esta fase se concreta la creación de la entidad (donde y cuando). Si es un objeto-dato se le asigna memoria física y posiblemente se inicializa. Ejemplo: int x = 3;. Si es un algoritmo (función) se establece su código. En muchos casos la declaración y definición se realiza en la misma sentencia. En otros casos la declaración mantiene su sentido original de ser una simple exposición de un tipo de entidad con un nombre y posiblemente algún atributo adicional ( 4.1.1); en este caso la declaración se denomina también referencia.

Como puede verse, las declaraciones pueden definir y/o referenciar. Cualquier declaración que además reserve almacenamiento a un objeto o función es una definición. Así pues, el concepto definición implica una iniciación del objeto (en el sentido de que empieza a tener existencia física al asignársele espacio en memoria). En lo sucesivo, para evitar ambigüedades, utilizaremos declaración en el sentido referenciar, y definición cuando se trata de asignar memoria física y posiblemente inicializar esta con valores determinados.

En estas cuestiones es muy importante mantener claras las diferencias conceptuales y semánticas, en especial cuando los objetos son instancias de clases:


§1.2 Declaración
: Simplemente asocia un identificador con un tipo (existencia semántica). La declaración de una función se denomina prototipo ( 4.4.1). La gramática C++ exige que la declaración de una entidad se indique primero su tipo y después el identificador con el que se la conocerá en adelante.

Ejemplos:

extern int x;

class C;

int func(int x, char c);   // prototipo

Observe que la gramática C++ permite realizar varias declaraciones en una sola sentencia separando con comas los identificadores:

int x, y, z;

C c1, c2, c3;

Pero hay que prestar atención a este tipo de sentencias porque pueden deparar sorpresas:

int x, y, z;    // Ok! x, y, z son tipo int.

C* c1, c2, c3;  // Atención: c1 es tipo C*, mientras que c2 y c3 son tipo C.


Después de la declaración es poco lo que puede hacer el compilador con una etiqueta, ya que solo conoce el "tipo" de objeto que representa. Sin embargo, son posibles aquellos usos para los que basta con esta información. Ejemplo:

struct E1;        // declara que E1 es tipo struct
struct E2* pe2;   // declara que pe2 es tipo E2* (puntero-a-struct-E2)
E1* ep1;          // declara que pe1 es tipo E1* (puntero-a-struct-E1)


Este tipo de declaraciones se denominan adelantadas (en el sentido que no están acompañadas por una definición adecuada). Las entidades en esta situación (en la que el compilador solo tiene conocimiento del tipo), se denominan tipos incompletos ("Incompletely defined object type"); son las clases declaradas y no definidas, y las matrices de tipos incompletos o de tamaño indefinido. Ejemplo:

class C;          // clase declarada pero no definida

char m1[ ];       // matriz de tamaño indefinido

C matriz[5];      // matriz de tipos incompletos

Nota: para justificar que tiene unas características de tipo que podríamos denominar "restringidas", el Estándar C++ (§3.9) establece que el tipo void ( 2.2.1) es también un tipo incompleto (que no podrá nunca llegar a ser completo). 


Salvo en las contadas ocasiones en que no se requiere conocer el tamaño del objeto, los tipos incompletos no pueden ser utilizados en la definición de otros tipos, aunque sí en su declaración -también incompleta-. Ejemplo:

extern C* cptr;      // puntero a tipo incompleto
typedef int UNDA[];  // matriz incompleta
UNDA* aptr;          // puntero a tipo incompleto

class D {

   UNDA** apptr;     // Error: puntero a tipo incompleto

   ...               // en definición

};


En los casos en que no es necesario conocer el tamaño del objeto incompletamente declarado, sí es posible utilizarlos en la definición de otras entidades.  Esto ocurre típicamente cuando la definición solo contiene punteros y referencias al objeto incompleto, ya que en ambos casos, el tamaño siempre es el mismo.  Por ejemplo, sería válido el siguiente trozo de código:

class A:             /* Clase declarada pero no definida aquí
          la suponemos definida en otra unidad de compilación */
class B {
   public:
   A* aptr;
   void foo (const A& a);
   ...


§1.3 Iniciación: asigna memoria física al objeto (existencia física). Si no se produce una inmediata asignación de valores determinados, la zona asignada puede contener basura. Ejemplo:

int* ptr;

int x;


§1.4  Definición
: asocia un identificador con un tipo y le asigna espacio en memoria (declaración + iniciación). Observe que después de la definición, el objeto no tiene porqué estar inicializado. Es decir, si es un objeto-dato, el espacio asignado puede contener basura.

En el caso de funciones, la definición se realiza cuando se establece el cuerpo de la función. En el caso de clases, cuando se describen cuales serán sus propiedades y métodos. Ejemplo:

int x;

int func(int x, char c) { return (x + c) }

class C { int x; char c; };


  Adelantemos aquí que en C++ existe la denominada regla de una sola definición ODR ("One Definition Rule") según la cual, cualquier nombre puede ser declarado varias veces en cada unidad de compilación (con la condición de que estas declaraciones sean idénticas), pero solo puede definirse una vez .  Al tratar de los constructores y destructores ( 4.11.2d) veremos que en C++ es muy importante la correcta creación e inicialización de los objetos, por lo que en el caso de los tipos complejos ( 2.2) la creación e inicialización están indisolublemente unidas, y el lenguaje garantiza que al crearse un objeto es inicializado adecuadamente.


§1.5 Inicialización
: asignar valores concretos al objeto (existencia utilizable); a partir de aquí, el espacio de memoria contiene datos correctos. Ejemplo:

x = 5;              // inicia x con el valor 5

int* ptr = &x;      // inicia ptr con la dirección de x

int y = y;          // inicia y con su propio valor indefinido

C c;                // Valores por defecto asignados por el constructor

C d = { 15, 'z' };  // Valores asignados de forma explícita


§1.6 Destrucción
: en adelante el identificador no es reconocido y el espacio de memoria es desasignado; puede volver a ser utilizado por otros objetos. Sus valores actuales pueden permanecer, pero serán basura para el próximo objeto que ocupe dicho espacio de memoria. Ejemplo:

delete c;


§1.7 De acuerdo con lo anterior, puede considerarse que la secuencia vital de un objeto utilizable por el programa contiene las siguientes fases:

Declarar     Relacionar un identificador (nombre) con un tipo.

     

Iniciar         Reservar almacenamiento.

     

Inicializar    Asignarle valores.

     

Destruir      Desasignación semántica y física


Aunque existen circunstancias en las que los tres primeros estadios del objeto ocurren por separado, en ocasiones el compilador los realiza simultáneamente. Sobre todo cuando tiene suficiente información para ello. En ocasiones es conveniente y necesaria esta simultaneidad (caso de las constantes); en otras trata de evitarse.


§2
  Al llegar a este punto hay que hacer una matización importante: Cuando en un programa C++ se encuentra una expresión del tipo: int n; técnicamente hablando se trata solo de una declaración. Sin embargo, el compilador ya tiene toda la información necesaria (en este caso) y le asigna un espacio de memoria (momentáneamente puede estar lleno de basura). Por esta razón, a veces se dice que una expresión como la anterior es una definición. En estos casos, la única forma de asegurarse que el compilador interpreta la sentencia en sus términos exactos (solo declaración), es añadiendo el especificador extern.  Ejemplo:

extern int n;

Esto garantiza que el compilador sepa que es solo una declaración, y que la definición (y el almacenamiento) está en algún otro sitio. Nótese que lo anterior (conocer toda la información con solo la declaración) solo ocurre con las declaraciones de variables, no así con las funciones. En una declaración de función (lo que denominamos un prototipo) de la forma:

int funcion (char letra, int cantidad, long distancia);

el compilador no puede hacer gran cosa (aparte de una verificación estática de tipos), ya que falta el cuerpo (definición) de la función. De hecho, en la declaración de cualquier función puede suponerse que está implícito el especificador extern ( 4.1.8d).

Más detalles y ejemplos: ( Declaraciones y definiciones)

§3 Regla de una sola definición

Esta regla, conocida también por su acrónimo inglés ODR, establece que en cada programa, especialmente si es multifichero, puede haber muchas referencias al mismo identificador, pero solo se permite una definición para cada identificador en cada espacio de nombres ( 4.1.11). Las entidades que se pueden declarar incluyen:

  • Variables
  • Funciones explícitas y genéricas
  • Clases explícitas y genéricas y sus miembros
  • Tipos
  • Etiqueta estructura, unión y enumeración
  • Miembros de Estructuras
  • Miembros de Uniones
  • Matrices de otros tipos
  • Constantes de Enumeración
  • Etiquetas
  • Macros de preproceso

Observe que si nos referimos al espacio global del programa, la regla ODR conduce a que en todo el programa solo puede existir una definición de cualquiera de las entidades antes enunciadas.

Dentro de un mismo fichero pueden existir múltiples typedef ( 3.2.1a) o macro definiciones (#define 4.9.10b), siempre que la definición resultante sea la misma, lo que es conocido como redefinición benigna ("benign redefinition").

De forma general puede afirmarse que un identificador no puede utilizarse en un programa antes del punto del código fuente en que es declarado [1].  Las excepciones a esta regla, conocidas como referencias adelantadas ( 4.11.4a), son llamadas a funciones; etiquetas de clases; estructuras, o uniones no declaradas.

A pesar de lo anterior, el lector habrá observado que es frecuente la práctica de incluir definiciones en ficheros de cabecera, digamos p.e. <cabecera.h> y que más tarde pueden aparecer sentencias del tipo #include <cabecera.h> en varios fuentes del mismo programa, lo que conduciría a pensar que se contraviene la regla [6].

La razón de que en estos casos no se produzcan errores, es que esta regla no debe tomarse al pié de la letra: En realidad el compilador acepta la aparición en el código de más de una definición de una entidad, y las toma como imágenes de una sola definición, siempre que se cumplan las siguientes condiciones:

  • Las definiciones aparezcan en distintas unidades de compilación.
  • Resulten idénticas token a token para el "parser" ( 1.4).
  • El significado de los tokens sea idéntico en todas estas unidades de compilación (no ocurra, por ejemplo, que un símbolo tenga un significado en un módulo y otro distinto en el siguiente debido a un typedef).
§4 Declaraciones

En la práctica una declaración es una lista de nombres (identificadores) que comienzan con un especificador de tipo de almacenamiento (que es opcional), seguido de especificadores de tipo y otros modificadores. Los identificadores están separados por comas y toda la lista terminada en punto y coma ;.  Una declaración de variables puede tener el siguiente aspecto:

tipo-de-dato var1 <=inic1>, var2 <=inic2>, ...;

donde var1, var2,... es cualquier secuencia de identificadores distintos con iniciadores <=inicX> opcionales. Cada una de las variables es declarada del tipo señalado por tipo-de-dato. Por ejemplo:

int x = 1, y = 2, z;

Esta declaración [2] crea tres variables de tipo int: x, y, y z, y las inicia a los valores 1 y 2, respectivamente (z queda sin definir).


En los ejemplos de declaraciones que siguen puede verse que los tipos de los identificadores (en todos los casos es el mismo: nomb y cont) se deducen de los declaradores (en negrita).

Declaración      tipo implícito de "nomb"                  Ejemplo de uso

tipoX nomb;      // tipoX                                  int cont;

tipoX nomb[];    // matriz (abierta)de tipoX               int cont[];

tipoX nomb[3];   // matriz de tres elementos tipoX         int cont[3];

                 // nomb[0], nomb[1] y nomb[2])

tipoX* nomb;     // Puntero-a-tipoX                        int *cont;

tipoX* nomb[];   // Matriz de punteros-a-tipoX             int *cont[];

tipoX *(nomb[]); // equivalente al anterior                int *(cont[]);

tipoX (*nomb)[]; // -Puntero a matriz de tipoX             int (*cont) [];

tipoX &nomb;     // Referencia a tipoX                     int &cont;

tipoX nomb();    // Función devolviendo tipoX              int cont();

tipoX* nomb();   // Función devolviendo puntero-a-tipoX    int *cont();

tipoX *(nomb()); // equivalente al anterior                int *(cont());

tipoX (*nomb)(); // -Puntero a función devolviendo tipoX   int (*cont)();


Nota
: observe la necesidad de paréntesis en (*nomb)[] y (*nomb)(). Es así porque en ambos, la precedencia ( E4.9.0a) del declarador de matrices [ ] y del declarador de funciones ( ), es mayor que el declarador de puntero *. En cambio, el paréntesis en *(nomb[]) es opcional.

§5  Definiciones

Se tiene una definición cuando el compilador tiene información suficiente para construir en memoria una imagen de la entidad. Podemos suponer que la definición es una reserva o asignación de espacio en un sitio concreto de memoria.

int x;

char ch;

long z;

int dias[7];

struct S { int a; int b; };

char* ptr;

float power2(float n) { return n*n; }


La definición de una entidad C++ puede ser un proceso bastante complejo. En especial cuando se trata de estructuras o clases. Es frecuente que en estos casos, el proceso se considere descompuesto en dos fases: una primera fase es la mera declaración, y contiene una imagen o resumen del cuerpo de la clase con las declaraciones de sus propiedades y los prototipos de sus funciones-miembro. La definición propiamente dicha, se realiza en una segunda fase, denominada de implementación, en la que se precisan todos los detalles, incluyendo las definiciones de todas las funciones-miembro, posible lista de iniciadores ( 4.11.2d3) Etc.

El subconjunto de la declaración, que contiene solo referencias a las propiedades y métodos públicos, se conoce como interfaz de la clase. Se supone que la interfaz contiene toda la información de debe conocer el usuario de la clase para poder utilizarla. El resto de miembros privados y protegidos solo conciernen al implementador de la clase.

§6 Iniciar

Se ha apuntado que iniciar es el hecho de dar valores concretos, y correctos, a la imagen en memoria de la entidad. El vocablo "iniciar" se utiliza solo con variables y constantes [5], dado que las funciones no pueden ser "iniciadas" (su proceso de creación concluye con la definición).

Cuando se inicia un objeto, debe estar ya declarado y definido (aunque todos los pasos puedan estar implícitos en la misma sentencia). Es decir, antes de iniciar una constante o variable, el compilador debe conocer el tipo a que pertenece el identificador y disponer del espacio correspondiente en memoria. Lo usual es que este espacio esté previamente ocupado por valores sin sentido (basura).

En general, la inicialización se realiza mediante asignaciones, que pueden realizarse en el momento de la declaración o después, pero existen posibilidades sintácticas especiales para algunos tipos. Por ejemplo: las matrices ( 4.3.1), estructuras ( 4.5.2), uniones ( 4.6), y las instancias de clases (objetos). En particular estas últimas disponen de funciones especiales encargadas de estos menesteres: los constructores ( 4.11.2d3). En los siguientes epígrafes se exponen algunas reglas y consideraciones sobre el asunto.

§7 Reglas de inicio

§7.1 El Estándar establece que dentro de cada unidad de compilación, la inicialización de los objetos se realizará en el mismo orden de su definición [7].


§7.2
  Si un identificador tiene ámbito de bloque y especificación de enlazado externo o interno, la declaración no puede contener ninguna inicialización (debe ser una mera referencia). Ejemplo:

...

{

   extern long peso = 1000;   // L.1 Error

   extern long peso;          // L.2 correcto

}

...

Nota: una sentencia como L.1 puede no producir error porque el compilador sencillamente ignora la cláusula extern. Recuerde que una declaración con inicializador es siempre una definición.


§7.3
  Recuerde que fuera de los bloques de funciones o clases, en él ámbito global de fichero, no puede existir ningún tipo de sentencia distinta de definiciones o declaraciones. Por ejemplo, considere el siguiente programa:

#include <iostream.h>
int a;             // L.2 Ok. declaración
a = 13;            // Error: Asignación no permitida aquí
int b = 23;        // Ok. definición

int c = b;         // Ok. definición

cout << "c = " << c << endl;    // Error: sentencia no permitida aquí


int main() {       // =====================================
  cout << "a = " << a << endl;  // Ok. sentencia permitida
  a = 13;          // Ok. Asignación permtida aquí
  cout << "a = " << a << endl;
  int d;           // M.4 Ok. declaración

  cout << "d = " << d << " (Basura)" << endl;
  d = 33;          // Ok. Inicialización
  cout << "d = " << d << endl;
}

Salida después de eliminadas las sentencias erróneas:

a = 0

a = 13
d = 5570560 (Basura)
d = 33


Tenga en cuenta que las variables declaradas (y no inicializadas) en el espacio global, o en un subespacio de nombres, son inicializadas por defecto. Por ejemplo, la sentencia L.2 del ejemplo anterior equivale a:

int a = 0;

lo que explica la primera salida. En cambio esto no ocurre con las variables locales ( 4.1.8a) o creadas en la zona de almacenamiento persistente ( 1.3.2), así que la variable d declarada en M.4 no es inicializada por defecto y contiene basura.


§7.4  Las variables escalares pueden inicializarse en el mismo punto de su declaración siguiendo el nombre de la variable con el signo igual y una expresión (en otras palabras: mediante una asignación utilizando una expresión como Rvalue). Ejemplo:

int x = 1;

char simplecomilla = '\'';

long msdia = 1000L * 60L * 60L * 24L ; // milisegundos del día


Si un objeto tiene duración automática y no es inicializado, su contenido es indeterminado (puede ser simplemente basura). En estas, y en las variables de registro, el iniciador no está restringido a una constante. Puede ser cualquier expresión que implique valores previamente definidos, incluso llamadas a funciones. Por ejemplo:

int binsearch(int x, int v[], int n) {

   iny low = 0;

   int high = n-1;

   int mid;

  ...

}


§7.6
  Para variables externas y estáticas la inicialización ocurre conceptualmente una sola vez antes que el programa comience su ejecución.

  • Si no son inicializadas explícitamente, la inicialización ocurre antes que cualquier otra en el programa, adoptándose por defecto los valores que siguientes:
    • Cero si es de tipo aritmético
    • Nulo si es un puntero
  • Si son inicializadas mediante expresiones constantes, esta inicialización ocurre antes que la de los objetos automáticos . Los inicializadores de objetos estáticos pueden ser cualquier expresión que incluya constantes y variables, o funciones que hayan sido declaradas previamente.


§7.7
  Los iniciadores de una lista de inicio para una matriz deben ser constantes o expresiones que se reduzcan a una constante. En el ejemplo que sigue, todas las expresiones son correctas, y las cinco matrices resultan de contenidos idénticos:

int  x = 97;

char ch = 'a';

char m1[] = {"Hola"};

char m2[4] = {'H','o','l','a','\0'};

char m3[4] = {'H','o','l',ch,'\0'};

char m4[4] = {'H','o','l',f(97),'\0'};

char m5[4] = {'H','o','l',f(x),'\0'};

...

char f(int x){ return (char)x; }

 

El compilador C++Builder permite declaraciones posteriores de variables externas [4], tales como matrices, estructuras y uniones, de forma que se añada información a la contenida en la declaración previa. Ejemplo:

extern int a[];      // L1: no se especifica tamaño (matriz abierta)

struct mystruct;     // L2: no especifica miembros (decl. anticipada)

...

int a[3] = {1, 2, 3}; // se especifica tamaño y se inicia

struct mystruct {

  int i, j;

};                    // se añade declaración de miembros

  Obsérvese que la expresión de L1 es una declaración de a como variable externa, la línea L2 es en cambio una declaración anticipada en el sentido indicado en 4.5.1e. Lo único que hace esta sentencia es declarar mystruct como de ámbito global al fichero. Se trata pues de matices distintos en ambas declaraciones.


§7.8  Inicio de matrices, estructuras y uniones

Las estructuras y matrices pueden inicializarse (incluso en el mismo punto de su declaración), con una lista de iniciadores entre corchetes { } separados por comas, uno para cada miembro de la matriz o estructura. Por ejemplo:

int dias[7] = { 1, 1, 1, 1, 1, 1, 1 }

Las reglas que siguen se aplican a la inicialización de matrices de caracteres normales y anchos ( 3.2.3):


§7.8.1   Puede iniciarse una matriz de caracteres con una cadena literal, opcionalmente entre corchetes. Cada carácter de la cadena, incluyendo el terminador nulo (incluido automáticamente), inicializa elementos sucesivos del array. Por ejemplo:

char nomb[] = { "Jorge" };

inicia una matriz de seis elementos, que son:

nomb[0]=='J', nomb[1]=='o', ... nomb[6]== 0.

Equivale a:

char nomb[] = {'J','o','r','g','e','\0'};


§7.8.2
   Puede iniciarse una matriz de caracteres anchos (compatible con wchar_t) utilizando una cadena de caracteres anchos, opcionalmente entre corchetes. Como en el caso de caracteres normales, los códigos de la cadena alfanumérica ancha inician elementos sucesivos de la matriz.


§7.8.3
  La inicialización de estructuras y uniones se detalla en los apartados correspondientes ( 4.5.2).

§8 Definición provisional

En contra de lo que ocurre en C, en C++ no existe el concepto de declaración provisional [3]. Una declaración de dato externo sin un especificador de tipo de almacenamiento, es tomado siempre como una definición, por lo que cualquier inicialización posterior dará lugar a un error de "Declaración múltiple". Por ejemplo:

int x;          // declara x

int x;           // Error: "Múltiple declaración de x"

int y;          // declara y

int y = 4;       // Error: "Multiple declaración de y"

int z = 5;      // Legal: z declarado e iniciado a 5

int z = 6;       // Error: "Múltiple declaración de z"

  Inicio.


[1]  Así mismo, como regla de buena programación se recomienda no declararlo antes de que sea realmente necesario

[2]  Estas son definiciones en el sentido utilizado más arriba; se asigna espacio de almacenamiento.

[3]  El estándar ANSI C incluye el concepto de definición provisional. Cualquier declaración de identificador externo que no tenga especificador de tipo de almacenamiento ni inicializador, se considera una definición provisional. En caso que el identificador aparezca en alguna definición posterior, entonces la definición provisional es considerada como si el especificador de tipo externo estuviera presente. En otras palabras, la definición provisional se transforma en una simple referencia.

Si se llega al final de la unidad de traducción y no ha aparecido ninguna inicialización para el identificador (provisional), entonces la definición provisional se transforma en una definición completa, y el objeto definido dispone de un espacio reservado no inicializado (rellenado con ceros).

[4]  Nótese que la palabra externo/a tiene varias connotaciones en C++. En este caso se refiere a declaraciones de variables cuya definición está en otro módulo ( 4.1.8d).

[5] En el caso de las constantes, la inicialización debe hacerse inexcusablemente en el mismo momento de su declaración, de forma que ambas deben realizarse en la misma sentencia ( 3.2.1c). Sin embargo, cuando estas constantes son miembros de clases, el principio anterior está en contra de otra regla de la definición de clases: Que no pueden realizarse asignaciones en el cuerpo de la declaración.  El resultado es una solución de compromiso ( 4.11.2d3).

[6] El creador del lenguaje nos informa que la intención de esta regla es que se pueda incluir la definición de una clase en distintas unidades de compilación a partir de un fichero común TC++PL §9.2.3.

[7] El compilador GNU cpp dispone de una opción especial (atributo init__priority), que permite asignar a cada clase una constante entera, de valor comprendido entre 101 y 65535 como indicativo de la prioridad relativa para la inicialización objetos de dicha clase.