6 Declaración de variables

Una característica de C++, es la necesidad de declarar las variables que se usarán en un programa. Esto resulta chocante para los que se aproximan al C++ desde otros lenguajes de programación en los que las variables de crean automáticamente la primera vez que se usan. Se trata, es cierto, de una característica de bajo nivel, más cercana al ensamblador que a lenguajes de alto nivel, pero en realidad una característica muy importante y útil de C++, ya que ayuda a conseguir códigos más compactos y eficaces, y contribuye a facilitar la depuración y la detección y corrección de errores y a mantener un estilo de programación elegante.

Uno de los errores más comunes en lenguajes en los que las variables se crean de forma automática se produce al cometer errores ortográficos. Por ejemplo, en un programa usamos una variable llamada prueba, y en un punto determinado le asignamos un nuevo valor, pero nos equivocamos y escribimos prubea. El compilador o interprete no detecta el error, simplemente crea una nueva variable, y continúa como si todo estuviese bien.

En C++ esto no puede pasar, ya que antes de usar cualquier variable es necesario declararla, y si por error usamos una variable que no ha sido declarada, se producirá un error de compilación.

Cómo se declaran las variables

Ya hemos visto la mecánica de la declaración de variables, al mostrar la sintaxis de cada tipo en el capítulo 2.

El sistema es siempre el mismo, primero se especifica el tipo y a continuación una lista de variables y finalmente un punto y coma.

La declaración de variables es uno de los tipos de sentencia de C++. La prueba más clara de esto es que la declaración terminará con un ";". Sintaxis:

<tipo> <lista de variables>;

También es posible inicializar las variables dentro de la misma declaración. Por ejemplo:

int a = 1234;
bool seguir = true, encontrado;

Declararía las variables a, seguir y encontrado; y además iniciaría los valores de a y seguir con los valores 1234 y true, respectivamente.

En C++, contrariamente a lo que sucede con otros lenguajes de programación, las variables no inicializadas tienen un valor indeterminado (con algunas excepciones que veremos más tarde), y contienen lo que normalmente se denomina "basura". Cuando se declara una variable se reserva un espacio de memoria para almacenarla, pero no se hace nada con el contenido de esa memoria, se deja el valor que tuviera previamente, y ese valor puede interpretarse de distinto modo, dependiendo del tipo.

Ámbitos

Llamamos ámbito a la zona desde que cierto objeto es accesible.

En C++ solemos referirnos a dos tipos de ámbitos: temporal y de acceso. Así, el ámbito temporal indica el intervalo de tiempo en el que un objeto existe o es accesible. El ámbito de acceso nos dice desde donde es accesible.

En este capítulo hablaremos un poco sobre el ámbito de las variables, pero no entraremos en muchos detalles todavía, ya que es un tema largo.

Por otra parte, las funciones (y otros objetos de los que aún no hemos hablado nada), también tienen distintos ámbitos.

Ámbito de las variables

Dependiendo de dónde se declaren las variables, podrán o no ser accesibles desde distintas partes del programa. Es decir, su ámbito de acceso y temporal dependerá del lugar en que se declaren.

Las variables declaradas dentro de un bucle, serán accesibles sólo desde el propio bucle, esto es, tendrán un ámbito local para el bucle. Esto es porque las variables se crean al inciar el bucle y se destruyen cuando termina. Evidentemente, una variable que ha sido destruida no puede ser accedida, por lo tanto, el ámbito de acceso está limitado por el ámbito temporal.

Nota:

En compiladores de C++ antiguos, (y en algunos modernos y mal implementados), no existe este ámbito, que sin embargo está descrito en la norma ANSI.

En estos compiladores, las variables declaradas dentro de un bucle tienen el mismo ámbito temporal y de acceso que las variables locales. Es decir, existen y son accesibles desde el punto en que se declaren hasta el final de la función.

Si usamos uno de esos compiladores no será posible, por ejemplo, usar varios bucles con declaraciones de variables locales de bucle con el mismo nombre.

for(int i=0; i < 100; i++) HacerAlgo(i);
for(int i=0; i > -100; i--) DeshacerAlgo(i);


Este código daría un error al intentar redefinir la variable local i.

Las variables declaradas dentro de una función, y recuerda que main también es una función, sólo serán accesibles para esa función, desde el punto en que se declaran hasta el final. Esas variables son variables locales o de ámbito local de esa función.

Al igual que ocurre con las variables locales de bucle, en las de función, las variables se crean al inciar la función y se destruyen al terminar.

Las variables declaradas fuera de las funciones, serán accesibles desde todas las funciones definidas después de la declaración. Diremos que esas variables son globales o de ámbito global.

El ámbito temporal de estas variables es también global: se crean junto con el programa, y se destruyen cuando el programa concluye.

Las variables globales son las únicas que son inicializadas automáticamente con valor cero cuando se declaran. Esto no sucede con ninguna variable local.

En todos los casos descritos, el ámbito temporal coincide con el de acceso: las variables que no pueden ser accedidas es porque no existen todavía o porque han sido destruídas. Más adelante veremos casos en que estos ámbitos no coinciden.

Una variable global declarada después de la definición de una función no será accesible desde esa función, por eso, normalmente se declaran las variables globales antes de definir las funciones.

Pero esto es hablando de forma general, en realidad, en C++ está mal visto usar variables globales, ya que se consideran poco seguras.

Ejemplo:

int EnteroGlobal; // Declaración de una variable global

int Funcion1(int a); // Declaración de un prototipo

int main() {
   // Declaración de una variable local de main:
   int EnteroLocal;

   // Acceso a una variable local:
   EnteroLocal = Funcion1(10);
   // Acceso a una valiable global:
   EnteroGlobal = Funcion1(EnteroLocal);

   return 0;
}

int Funcion1(int a)
{
   char CaracterLocal; // Variable local de funcion1
   // Desde aquí podemos acceder a EnteroGlobal,
   // y también a CaracterLocal
   // pero no a EnteroLocal
   if(EnteroGlobal != 0)
      return a/EnteroGlobal;
   return 0;
}

De modo que en cuanto a los ámbitos locales tenemos varios niveles:

<tipo> funcion(parámetros) // (1)
{
   <tipo> var1;             // (2)
   for(<tipo> var2;...)     // (3)
   ...
   <tipo> var3;             // (4)
   ...
   return var;
}

(1) Los parámetros se comportan del mismo modo que variables locales, tienen ámbito local a la función.

(2) Las variables declaradas aquí, también.

(3) Las declaradas en bucles, son de ámbito local en el bucle.

(4) Las variables locales sólo son accesibles a partir del lugar en que se declaren. Esta variable: var3, es de ámbito local para la función, pero no es accesible en el código previo a su declaración.

Es una buena costumbre inicializar las variables locales.

Los ámbitos pueden ser alterados mediante ciertos modificadores que veremos en otros capítulos.

Enmascaramiento de variables

Generalmente no es posible, y no suele ser necesario, declarar dos variables con el mismo nombre, pero hay condiciones bajo las cuales es posible hacerlo.

Por ejemplo, podemos declarar una variable global con un nombre determinado, y declarar otra variable (del mismo tipo o de otro diferente) de forma local en una función, usando el mismo nombre.

En ese caso decimos que la segunda declaración (la local), enmascara a la primera (la global). Con eso queremos decir que el acceso a la variable global está bloqueado o enmascarado por la local, que es a la única que podemos acceder directamente.

Por ejemplo:

int x;

int main() {
   int x;

   x = 10;
   return 0;
}

En este programa, cuando asignamos 10 a x estamos accediendo a la versión local de la variable x. En la función main, la variable global x está enmascarada, y no puede accederse a ella directamente.

Del mismo modo, una variable de ámbito de bucle o de ámbito de bloque puede enmascarar a una variable global o local, o a una de un bloque o bucle más externo:

   int x = 10;
   {
      int x = 0;
      for(int x = 0; x < 10; x++) HacerAlgoCon(x);
   }

En este caso la declaración de x dentro del bloque enmascara la declaración anterior, y a su vez, la declaración dentro del bucle for enmascara a la declaración del bloque.

Otra cuestión sería qué utilidad pueda tener esto.

Operador de ámbito

Existe un método para acceder a una variable global enmascarada por una variable local. Se trata del operador de ámbito, que consiste en dos caracteres de dos puntos seguidos (::).

Veremos este operador con más detalle en el capítulo dedicado a los espacios con nombre, pero veamos ahora cómo lo podemos usar para acceder a una variable global enmascarada:

int x; // Variable global

int main()
{
   int x; // Variable local que enmascara a la global

   x = 10; // Accedemos a la variable local
   ::x = 100; // Mediante el operador de ámbito accedemos a la global
   return 0;
}

El operador de ámbito, usado de este modo, permite acceder al espacio de variables global. Pero este no es más que un uso restringido del operador, que tiene muchas más aplicaciones.

Problemas resueltos de capítulos 1 a 6

Pensar
Pensar

Veamos ahora algunos ejemplos que utilicen los conocimientos que ya tenemos sobre C++.

Pero antes introduciremos, sin explicarlo en profundidad, dos elementos que nos permitirán que nuestros programas se comuniquen con nosotros. Se trata de la salida estándar, cout y de la entrada estándar cin. Estos objetos nos permiten enviar a la pantalla o leer desde el teclado cualquier variable o constante, incluidos literales. Lo veremos más detalladamente en un capítulo dedicado a ellos, de momento sólo nos interesa cómo usarlos para mostrar o leer cadenas de caracteres y variables.

Nota:

en realidad cout es un objeto de la clase ostream, y cin un objeto de la clase istream pero los conceptos de clase y objeto quedarán mucho más claros en capítulos posteriores.

El uso es muy simple:

#include <iostream>
using namespace std;

cout << <variable|constante> [<< <variable|constante>...];
cin >> <variable> [>> <variable>...];

Veamos un ejemplo:

#include <iostream>
using namespace std;

int main()
{
   int a;

   cin >> a;
   cout << "la variable a vale " << a;
   return 0;
}

Un método muy útil para cout es endl, que hará que la siguiente salida se imprima en una nueva línea.

cout << "hola" << endl;

Otro método, este para cin es get(), que sirve para leer un carácter, pero que nos puede servir para detener la ejecución de un programa.

Esto es especialmente útil cuando trabajamos con compiladores como Dev-C++, que crea programas de consola. Cuando se ejecutan los programas desde el compilador, al terminar se cierra la ventana automáticamente, impidiendo ver los resultados. Usando get() podemos detener la ejecución del programa hasta que se pulse una tecla.

A veces, sobre todo después de una lectura mediante cin, pueden quedar caracteres pendientes de leer. En ese caso hay que usar más de una línea cin.get().

#include <iostream>
using namespace std;

int main()
{
   int a;

   cin >> a;
   cout << "la variable a vale " << a;
   cin.get();
   cin.get();
   return 0;
}

Las líneas #include <iostream> y using namespace std; son necesarias porque las declaraciones que permiten el uso de cout y cin están en una biblioteca externa. Con estos elementos ya podemos incluir algunos ejemplos.

Generalmente no está muy bien visto usar sentencias using namespace, y se prefiere usar los identificadores completos, por ejemplo, std::cout o std::endl, sin embargo, en la mayor parte de los ejemplos de este curso las usaremos por claridad. De momento no te preocupes demasiado por este tema, todo quedará más claro en capítulos posteriores.

Te aconsejo que intentes resolver los ejemplos antes de ver la solución, o al menos piensa unos minutos sobre ellos.

Ejemplo 6.1

Primero haremos uno fácil. Escribir un programa que muestre una lista de números del 1 al 20, indicando a la derecha de cada uno si es divisible por 3 o no.

// Este programa muestra una lista de números,
// indicando para cada uno si es o no múltiplo de 3.
// 11/09/2000 Salvador Pozo

#include <iostream> // biblioteca para uso de cout
using namespace std;

int main() // función principal
{
   int i; // variable para bucle

   for(i = 1; i <= 20; i++) // bucle for de 1 a 20
   {
      cout << i; // muestra el número
      if(i % 3 == 0) cout << " es múltiplo de 3"; // resto==0
      else cout << " no es múltiplo de 3"; // resto != 0
      cout << endl; // cambio de línea
   }

   return 0;
}

Ejecutar este código en OnlineGDB.

El enunciado es el típico de un problema que puede ser solucionado con un bucle for. Observa el uso de los comentarios, y acostúmbrate a incluirlos en todos tus programas. Acostúmbrate también a escribir el código al mismo tiempo que los comentarios. Si lo dejas para cuando has terminado el programa, probablemente sea demasiado tarde, y la mayoría de las veces no lo harás. ;-)

También es una buena costumbre incluir al principio del programa un comentario extenso que incluya el enunciado del problema, añadiendo también el nombre del autor y la fecha en que se escribió. Además, cuando hagas revisiones, actualizaciones o correcciones deberías incluir una explicación de cada una de ellas y la fecha en que se hicieron.

Una buena documentación te ahorrará mucho tiempo y te evitará muchos dolores de cabeza.

Ejemplo 6.2

Escribir el programa anterior, pero usando una función para verificar si el número es divisible por tres, y un bucle de tipo while.

// Este programa muestra una lista de números,
// indicando para cada uno si es o no múltiplo de 3.
// 11/09/2000 Salvador Pozo

#include <iostream> // biblioteca para uso de cout
using namespace std;

// Prototipos:
bool MultiploDeTres(int n);

int main() // función principal
{
   int i = 1; // variable para bucle

   while(i <= 20) // bucle hasta i igual a 20
   {
      cout << i; // muestra el número
      if(MultiploDeTres(i)) cout << " es múltiplo de 3";
      else cout << " no es múltiplo de 3";
      cout << endl; // cambio de línea
      i++;
   }

   return 0;
}

// Función que devuelve verdadero si el parámetro 'n' en
// múltiplo de tres y falso si no lo es
bool MultiploDeTres(int n)
{
   if(n % 3) return false; else return true;
}

Ejecutar este código en OnlineGDB.

Comprueba cómo hemos declarado el prototipo de la función MultiploDeTres. Además, al declarar la variable i le hemos dado un valor inicial 1. Observa que al incluir la función, con el nombre adecuado, el código queda mucho más legible, de hecho prácticamente sobra el comentario.

Por último, fíjate en que la definición de la función va precedida de un comentario que explica lo que hace. Esto también es muy recomendable.

Ejemplo 6.3

Escribir un programa que muestre una salida de 20 líneas de este tipo:

1
1 2
1 2 3
1 2 3 4
...
// Este programa muestra una lista de números
// de este tipo:
// 1
// 1 2
// 1 2 3
// ...

// 11/09/2000 Salvador Pozo

#include <iostream> // biblioteca para uso de cout
using namespace std;

int main() // función principal
{
   int i, j; // variables para bucles

   for(i = 1; i <= 20; i++) // bucle hasta i igual a 20
   {
      for(j = 1; j <= i; j++) // bucle desde 1 a i
         cout << j << " "; // muestra el número
      cout << endl; // cambio de línea
   }

   return 0;
}

Ejecutar este código en OnlineGDB.

Este ejemplo ilustra el uso de bucles anidados. El bucle interior, que usa j como variable toma valores entre 1 e i. El bucle exterior incluye, además del bucle interior, la orden de cambio de línea, de no ser así, la salida no tendría la forma deseada. Además, después de cada número se imprime un espacio en blanco, de otro modo los números aparecerían amontonados.

Ejemplo 6.4

Escribir un programa que muestre una salida con la siguiente secuencia numérica:

1, 5, 3, 7, 5, 9, 7, ..., 23

La secuencia debe detenerse al llegar al 23.

El enunciado es rebuscado, pero ilustra el uso de los bucles do..while.

La secuencia se obtiene partiendo de 1 y sumando y restando 4 y 2, alternativamente. Veamos cómo resolverlo:

// Programa que genera la secuencia:
// 1, 5, 3, 7, 5, 9, 7, ..., 23
// 11/09/2000 Salvador Pozo

#include <iostream> // biblioteca para uso de cout
using namespace std;

int main() // función principal
{
   int i = 1; // variable para bucles
   bool sumar = true; // Siguiente operación es suma o resta
   bool terminado = false; // Condición de fin

   do { // Hacer
      cout << i; // muestra el valor en pantalla
      terminado = (i == 23); // Actualiza condición de fin
      // Puntuación, separadores
      if(terminado) cout << "."; else cout << ", ";
      // Calcula siguiente elemento
      if(sumar) i += 4; else i -= 2;
      sumar = !sumar; // Cambia la siguiente operación
   } while(!terminado); // ... mientras no se termine
   cout << endl; // Cambio de línea

   return 0;
}

Ejecutar este código en OnlineGDB.

Ejemplo 6.5

Escribir un programa que pida varios números, hasta que el usuario quiera terminar, y los descomponga en factores primos.

No seremos especialmente espléndidos en la optimización, por ejemplo, no es probable que valga la pena probar únicamente con números primos para los divisores, podemos probar con algunos que no lo sean, al menos en este ejercicio no será una gran diferencia.

Piensa un momento en cómo resolverlo e inténtalo, después puedes continuar leyendo.

Lo primero que se nos ocurre, al menos a mi, cuando nos dicen que el programa debe ejecutarse mientras el usuario quiera, es implementar un bucle do..while, la condición de salida será que usuario responda de un modo determinado a cierta pregunta.

En cada iteración del bucle pediremos el número a descomponer y comprobaremos si es divisible entre los números entre 2 y el propio número.

No podemos empezar 1, ya que sabemos que todos los números son divisibles por 1 infinitas veces, por eso empezamos por el 2.

Pero si probamos con todos los números, estaremos intentando dividir por todos los pares entre 2 y el número, y sabremos de antemano que ninguno de ellos es un factor, ya que sólo el 2 es primo y par a la vez, por lo tanto, podemos probar con 2, 3 y a partir de ahí incrementar los factores de dos e dos.

Por otra parte, tampoco necesitamos llegar hasta el factor igual al número, en realidad sólo necesitamos alcanzar la raíz cuadrada del número, ya que ninguno de los números primos entre ese valor y número puede ser un factor de número.

Supongamos que tenemos en número 'n', y que la raíz cuadrada de 'n' es 'r'. Si existe un número 'x' mayor que 'r' que es un factor primo de 'n', por fuerza debe existir un número 'h', menor que 'r', que multiplicado por 'x' sea 'n'. Pero ya hemos probado todos los números por debajo de 'r', de modo que si existe ese número 'h' ya lo hemos extraído como factor de 'n', y si hemos llegado a 'r' sin encontrarlo, es que tampoco existe 'x'.

Por ejemplo, el número 257. Su raíz cuadrada es (aproximada), 16. Es decir, deberíamos probar con 2, 3, 5, 7, 11 y 13 (nuestro programa probará con 2, 3, 5, 7, 9, 11, 13 y 15, pero bueno). Ninguno de esos valores es un factor de 257. El siguiente valor primo a probar sería 17, pero sabemos que el resultado de dividir 257 por 17 es menor que 17, puesto que la raíz cuadrada de 257 es 16.031. Sin embargo ya hemos probado con todos los primos menores de 17, con resultado negativo, así que podemos decir que 17 no es factor de 257, ni tampoco, por la misma razón, ningún número mayor que él.

Ya tenemos dos buenas optimizaciones, veamos cómo queda el programa:

// Programa que descompone números en factores primos
// 26/07/2003 Salvador Pozo

#include <iostream> // biblioteca para uso de cout
using namespace std;

int main()
{
   int numero;
   int factor;
   char resp[12];

   do {
      cout << "Introduce un número entero: ";
      cin >> numero;
      factor = 2;
      while(numero >= factor*factor) {
         if(!(numero % factor)) {
            cout << factor << " * ";
            numero = numero / factor;
            continue;
         }
         if(factor == 2) factor++;
         else factor += 2;
      }
      cout << numero << endl;
      cout << "Descomponer otro número?: ";
      cin >> resp;
   } while(resp[0] == 's' || resp[0] == 'S');
   return 0;
}

Ejecutar este código en OnlineGDB.

Vemos claramente el bucle do..while, que termina leyendo una cadena y repitiendo el bucle si empieza por 's' o 'S'.

En cada iteración se lee un numero, y se empieza con el factor 2. Ahora entramos en otro bucle, este while, que se repite mientras el factor sea menor que la raíz cuadrada de numero (o mientras numero sea mayor o igual al factor al cuadrado).

Dentro de ese bucle, si numero es divisible entre factor, mostramos el factor, actualizamos el valor de numero, dividiéndolo por factor, y repetimos el bucle. Debemos probar de nuevo con factor, ya que puede ser factor primo varias veces. Para salir del bucle sin ejecutar el resto de las sentencias usamos la sentencia continue.

Si factor no es un factor primo de numero, calculamos el siguiente valor de factor, que será 3 si factor es 2, y factor + 2 en otro caso.

Cuando hemos acabado el bucle while, el valor de numero será el del último factor.

Puedes intentar modificar este programa para que muestre los factores repetidos en forma exponencial, en lugar de repetitiva, así, los factores de 256, en lugar de ser: "2 * 2 * 2 * 2 * 2 * 2 * 2 * 2", serían "28".