Sentencias de declaración y definición

La declaración de una variable, array, función o cualquier otro elemento con nombre de un programa (incluidas las estructuras y clases, que se tratarán en la Parte 3) es una sentencia.

La declaración debe contener el tipo y el identificador del elemento (véase Declaración y definición de variables), así como un valor inicial opcional para inicialización. Además, al realizar una declaración, se pueden especificar modificadores adicionales que cambian ciertas características del elemento. En concreto, ya conocemos los modificadores static y const , y pronto se añadirán más. Los arrays requieren una especificación adicional de la dimensión y el número de elementos (véase Descripción de arrays), mientras que las funciones requieren una lista de parámetros (para obtener más detalles, véase Funciones).

La sentencia de declaración de variables puede resumirse de la siguiente manera:

[modifiers] identifier type
  [= initialization expressions] ;

Para un array, tiene este aspecto:

[modifiers] identifier type [ [size_1]ᵒᵖᵗ ] [ [size_N] ]ᵒᵖᵗ(3)
  [ = { initialization_list } ]ᵒᵖᵗ ;

La principal diferencia es la presencia obligatoria de al menos un par de corchetes (el tamaño dentro de ellos puede indicarse o no; dependiendo de eso, obtendremos un array fijo o distribuido dinámicamente). En total, se permiten hasta 4 pares de corchetes (4 es el número máximo de medidas admitidas).

En muchos casos, una declaración puede actuar simultáneamente como definición, es decir, reserva memoria para el elemento, determina su comportamiento y permite utilizarlo en el programa. En concreto, la declaración de una variable o array es también una definición. Desde este punto de vista, una sentencia de declaración puede denominarse igualmente sentencia de definición, pero esto no se ha convertido en una práctica habitual.

Nuestro conocimiento básico de las funciones es suficiente para suponer de forma fiable cómo debe ser su definición:

type identifier ( [list_of_arguments] )
{
  [statements]
}

El tipo, el identificador y la lista de argumentos conforman el encabezado de la función.

Tenga en cuenta que se trata de una definición, ya que esta descripción contiene tanto los atributos externos de la función (interfaz) como las sentencias que definen su esencia interna (implementación). Esto último se hace con un bloque de código formado por un par de llaves e inmediatamente después del encabezado de la función. Como puede adivinar, este es un ejemplo de la sentencia compuesta que mencionamos en la sección anterior. En este caso se hace indispensable una tautología terminológica, pues está perfectamente justificada: la sentencia compuesta forma parte de la sentencia de definición de la función.

Un poco más adelante descubriremos por qué y cómo separar la descripción de la interfaz de la implementación y conseguir así la declaración de función sin definirla. También demostraremos la diferencia entre una declaración y una definición utilizando la clase a modo de ejemplo.

La sentencia de declaración hace que el nuevo elemento esté disponible por su nombre en el contexto del bloque de código (véase Contexto, ámbito y vida útil de las variables) en el que se encuentra la sentencia. Recordemos que los bloques forman el ámbito local de los objetos (variables, arrays). En la primera parte del libro hablamos de ello al describir la función de saludo.

Además de los ámbitos locales, existe siempre un ámbito global en el que también se pueden utilizar sentencias de declaración para crear elementos accesibles desde cualquier parte del programa.

Si no hay ningún modificador static en la sentencia de declaración y ésta se encuentra en algún bloque local, entonces el elemento correspondiente se crea e inicializa en el momento en que se ejecuta la sentencia (en sentido estricto, la memoria para todas las variables locales dentro de la función se asigna, en aras de la eficiencia, nada más entrar en la función, pero aún no están formadas en ese momento).

Por ejemplo, la siguiente declaración de la variable i al principio de la función OnStart garantiza que dicha variable se creará con el valor inicial especificado (0) en cuanto la función reciba el control (es decir, el terminal la invocará porque es la función principal del script).

void OnStart()
{
   int i = 0;
   Print(i);
   
   // error: 'j' - undeclared identifier
   // Print(j); 
   int j = 1;
}

Gracias a la declaración de la primera sentencia, la variable i es conocida y está disponible en las líneas posteriores de la función; en concreto, en la segunda línea con la llamada a la función Print, que muestra el contenido de la variable en el registro.

La variable j descrita en la última línea de la función se creará justo antes del final de la función (esto, por supuesto, es insignificante, pero claro). Por lo tanto, esta variable no se conoce en todas las cadenas anteriores de esta función. Si se intenta enviar j al registro mediante una llamada a Print comentada se producirá un error de compilación por «identificador no declarado».

Los elementos declarados de esta forma (dentro de bloques de código y sin el modificador static ) se denominan automáticos, ya que el programa en sí les asigna memoria al entrar en el bloque y los destruye al salir del bloque (en nuestro caso, después de salir de la función). Por ello, la zona de memoria en la que esto ocurre se denomina pila («último en entrar, primero en salir»).

Los elementos automáticos se crean en el orden en que se ejecutan las sentencias de declaración (primero i, luego j). La destrucción se realiza en orden inverso (primero j, luego i).

Si se declara una variable sin inicializarla y se empieza a utilizar en sentencias posteriores (por ejemplo, a la derecha del signo '=') sin escribir primero en ella un valor significativo, el compilador emite un aviso: «posible uso de variable no inicializada».

void OnStart()
{
   int ip;
   i = p// warning: possible use of uninitialized variable 'p'
}

Si una sentencia de declaración tiene el modificador static, el elemento correspondiente se crea una sola vez cuando la sentencia se ejecuta por primera vez y permanece en memoria, independientemente de la salida y de posibles entradas y salidas posteriores en el mismo bloque de código. Todos estos miembros estáticos se eliminan sólo cuando se descarga el programa.

A pesar del aumento de la vida útil, el alcance de dichas variables sigue estando limitado al contexto local en el que se definen, y sólo se puede acceder a ellas desde sentencias posteriores (situadas más abajo en el código).

Por el contrario, las sentencias de declaración en el contexto global crean sus elementos en el mismo orden en el que aparecen en el código fuente, inmediatamente después de que se cargue el programa (antes de que se llame a cualquier función de inicio estándar, como OnStart para scripts). Los objetos globales se eliminan en orden inverso cuando se descarga el programa.

Para demostrar lo anterior vamos a crear un ejemplo más «ingenioso» (StmtDeclaration.mq5). Recordando los conocimientos adquiridos en la primera parte, además de OnStart escribiremos una función sencilla Init que se utilizará en expresiones de inicialización de variables y registrará una secuencia de llamadas.

int Init(const int v)
{
   Print("Init: "v);
   return v;
}

La función Init acepta un único parámetro v de tipo entero int, cuyo valor se devuelve al código de llamada (sentencia return).

Esto permite utilizarlo como una envoltura para establecer el valor inicial de una variable; por ejemplo, para dos variables globales:

int k = Init(-1);
int m = Init(-2);

El valor del argumento pasado se introduce en las variables k y m al llamar a la función y volver de ella. Sin embargo, dentro de Init sacamos de forma adicional el valor con Print, y así podemos hacer un seguimiento de cómo se crean las variables.

Tenga en cuenta que no podemos utilizar la función Init en la inicialización de variables globales por encima de su definición. Si intentamos mover la declaración de la variable k por encima de la declaración Init obtendremos el error «'Init' es un identificador desconocido». Esta limitación sólo funciona para la inicialización de variables globales, ya que las funciones también se definen globalmente, y el compilador construye una lista de dichos identificadores de una sola vez. En todos los demás casos, el orden de definición de las funciones en el código no es importante, ya que el compilador primero las registra todas en la lista interna y, a continuación, vincula una a otra sus llamadas desde los bloques. En particular, puede mover toda la función Init y la declaración de las variables globales k y m debajo de la función OnStart: ello no romperá nada.

Dentro de la función OnStart describiremos varias variables más utilizando Init: i y j locales, así como n estática. Para simplificar, todas las variables reciben valores únicos a fin de poder distinguirlas.

void OnStart()
{
   Print(k);
   
   int i = Init(1);
   Print(i);
   // error: 'n' - undeclared identifier
   // Print(n);
   static int n = Init(0);
   // error: 'j' - undeclared identifier
   // Print(j);
   int j = Init(2);
   Print(j);
   Print(n);
}

Los comentarios muestran intentos erróneos de llamar a las variables relevantes antes de que estén definidas.

Ejecute el script y obtenga el siguiente registro:

Init: -1
Init: -2
-1
Init: 1
1
Init: 0
Init: 2
2
0

Como podemos ver, las variables globales se inicializaron antes de llamar a la función OnStart , y exactamente en el orden en que se encontraban en el código. Las variables internas se crearon en la misma secuencia en que se escribieron sus sentencias de declaración.

Si una variable está definida pero no se utiliza en ninguna parte, el compilador emitirá un aviso de «variable 'nombre' no utilizada». Esto es señal de un posible error del programador.

De cara al futuro, digamos que con la ayuda de las sentencias de declaración o definición se pueden introducir en el programa no sólo elementos de datos (variables, arrays) o funciones, sino también nuevos tipos definidos por el usuario (estructuras, clases, plantillas, espacios de nombres) que aún no conocemos. Estas sentencias sólo pueden hacerse a nivel global, es decir, fuera de todas las funciones.

Tampoco es posible definir una función dentro de otra función. El siguiente código no se compilará:

void OnStart()
{
   int Init(const int v)
   {
      Print("Init: "v);
      return v;
   }
   int i = 0;
}

El compilador generará un error: «las declaraciones de función sólo se permiten en el ámbito global, de espacio de nombres o de clase».