2 Tipos de variables I

Conceptualmente, desde el punto de vista de un programador, una variable es una entidad cuyo valor puede cambiar a lo largo de la ejecución de un programa.

En el nivel más bajo, una variable se almacena en la memoria del ordenador. Esa memoria puede ser un conjunto de semiconductores dentro de un circuito integrado, ciertos campos magnéticos sobre una superficie de un disco, ciertas polarizaciones en una memoria de ferrita, o cualquier cosa que aún no se haya inventado. Afortunadamente, no deberemos preocuparnos por esos detalles.

Teclado binario

Teclado para programación en binario.

En un nivel más lógico, una variable ocupa un espacio de memoria reservado en el ordenador para contener sus valores durante la ejecución de un programa. Cada variable debe pertenecer a un tipo determinado, y ese tipo determina, por una parte, el tamaño del espacio de memoria ocupado por la variable, y por otra, el modo en que se manipulará esa memoria por el ordenador.

No olvides, si es que ya lo sabías, que la información en el interior de la memoria del ordenador se almacena siempre de forma binaria, al menos a bajo nivel. El modo en que se interpreta la información almacenada en la memoria de un ordenador es, en cierto modo, arbitraria; es decir, el mismo valor puede codificar una letra, un número, una instrucción de programa, etc. No hay nada diferente en una posición de memoria que contenga una instrucción de programa o una letra de un texto; si observamos una posición de memoria cualquiera, no habrá modo de saber qué significa el valor que contiene. Es mediante el tipo como le decimos al compilador el modo en que debe interpretarse y manipularse cierta información binaria almacenada en la memoria de un ordenador.

De momento sólo veremos los tipos fundamentales, que son: void, char, int, float y double, en C++ se incluye también el tipo bool. También existen ciertos modificadores, que permiten ajustar ligeramente ciertas propiedades de cada tipo; los modificadores pueden ser: short, long, signed y unsigned, y pueden combinarse algunos de ellos. También veremos en este capítulo los tipos enumerados, enum.

Sobre la sintaxis

A partir de ahora mostraremos definiciones de la sintaxis para las diferentes sentencias en C++.

Estas definiciones nos permiten conocer las diferentes opciones para cada tipo de sentencia, las partes obligatorias, las opcionales, dónde colocar los identificadores, etc.

En este curso las definiciones de sintaxis se escribirán usando un rectángulo verde. Las partes opcionales se colocan entre corchetes [ ], los valores separados con | indican que sólo puede escogerse uno de esos valores. Los valores entre <> indican que debe escribirse obligatoriamente un texto que se usará como el concepto que se escribe en su interior.

Por ejemplo, veamos la siguiente sintaxis, que define una sentencia de declaración de variables de tipo char:

[signed|unsigned] char <identificador>[,<identificador2>[,<identificador3>]...];

Significa que se puede usar signed o unsigned, o ninguna de las dos, ya que ambas están entre [ ], y separadas con un |.

El subrayado de signed indica que se trata de la opción por defecto. En este caso, si no se usa signed ni unsigned, el compilador elige la opción signed.

A continuación de char, que debe aparecer de forma obligatoria, debe escribirse un texto, que tiene ser una única palabra que actuará como identificador o nombre de la variable. Este identificador es el que usaremos para referirnos a la variable en el programa. En un programa C++ siempre llamaremos a las cosas por su nombre.

Opcionalmente, podemos declarar más variables del mismo tipo, añadiendo más identificadores separados con comas. Podemos añadir tantos identificadores como queramos.

Para crear un identificador hay que tener en cuenta algunas reglas, ya que no es posible usar cualquier cosa como identificador.

  • Sólo se pueden usar letras (mayúsculas o minúsculas), números y ciertos caracteres no alfanuméricos, como el '_', pero nunca un punto, coma, guión, comillas o símbolos matemáticos o interrogaciones.
  • El primer carácter no puede ser un número.
  • C++ distingue entre mayúsculas y minúsculas, de modo que los identificadores numero y Numero son diferentes.

Finalmente, la declaración termina con un punto y coma.

Las palabras en negrita son palabras reservadas de C++. Eso significa que son palabras que no se pueden usar para otras cosas, concretamente, no se pueden usar como identificadores en un programa C++. Es decir, están reservadas para usarse del modo en que se describe en la sintaxis, y no se pueden usar de otro modo.

Serán válidos estos ejemplos:

signed char cuenta, cuenta2, total;
unsigned char letras;
char caracter, inicial, respuesta;
signed char _letra;

Tipos fundamentales

En C sólo existen cinco tipos fundamentales y los tipos enumerados, C++ añade un séptimo tipo, el bool, y el resto de los tipos son derivados de ellos. Los veremos uno por uno, y veremos cómo les afectan cada uno de los modificadores.

Tipo "char" o carácter:

[signed|unsigned] char <identificador>[,<identificador2>[,<identificador3>]...];

Es el tipo básico alfanumérico, es decir que puede contener un carácter, un dígito numérico o un signo de puntuación. Desde el punto de vista del ordenador, todos esos valores son caracteres. En C++ este tipo siempre contiene un único carácter del código ASCII. El tamaño de memoria es de 1 byte u octeto. Hay que notar que en C un carácter es tratado en todo como un número, de hecho, habrás observado que puede ser declarado con y sin signo. Si no se especifica el modificador de signo, se asume que es con signo.

Nota: sé que esto sorprende, inquieta y despista a muchos lectores, así que probablemente necesite alguna explicación más detallada. De modo que he añadido un pequeño apéndice que explica cómo es posible que un número y una letra se puedan representar con el mismo tipo: Apéndice A.

Este tipo de variables es apto para almacenar números pequeños, como los dedos que tiene una persona, o letras, como la inicial de mi nombre de pila.

El tipo char es, además, el único que tiene un tamaño conocido y constante. Para el resto de los tipos fundamentales que veremos, el tamaño depende de la implementación del compilador, que a su vez suele depender de la arquitectura del procesador o del sistema operativo. Sin embargo el tipo char siempre ocupa un byte, y por lo tanto, podemos acotar sus valores máximo y mínimo.

Así, el tipo char con el modificador signed, puede tomar valores numéricos entre -128 y 127. Con el modifiador unsigned, el rango está entre 0 y 255.

El hecho de que se trate de un tipo numérico entero nos permite usar variables de char para trabajar con valores pequeños, siempre que lo consideremos necesario.

El motivo por el que este tipo también puede contener un caracter es porque existe una correspondencia entre números y caracteres. Esa correspondencia recibe el nombre de código ASCII.

Según este código, por ejemplo, al número 65 le corresponde el carácter 'A' o al número 49 el '1'.

El código ASCII primitivo usaba sólo 7 bits, es decir, podía codificar 128 caracteres. Esto era más que suficiente para sus inventores, que no usaban acentos, eñes, cedillas, etc. El octavo bit se usaba como bit de paridad en transmisiones de datos, para la detección de errores.

Después, para internacionalizar el código, se usó el octavo bit en una tabla ASCII extendida, que puede codificar 256 caracteres.

Pero como esto también es insuficiente, se han creado otras codificaciones de caracteres multibyte, aunque esto queda fuera de las posibilidades de char.

Tipo "int" o entero:

[signed|unsigned] [short|long] int <identificador>[,<identificador2>[,<identificador3>]...];
[signed|unsigned] long [int] <identificador>[,<identificador2>[,<identificador3>]...];
[signed|unsigned] short [int] <identificador>[,<identificador2>[,<identificador3>]...];

Las variables enteras almacenan números enteros dentro de los límites de cada uno de sus tamaños. A su vez, esos tamaños dependen de la plataforma, del compilador, y del número de bits que use por palabra de memoria: 8, 16, 32... No hay reglas fijas para saber el tamaño, y por lo tanto, el mayor número que podemos almacenar en cada tipo entero: short int, int o long int; depende en gran medida del compilador y del sistema operativo. Sólo podemos estar seguros de que el tamaño de un short int es menor o igual que el de un int, y éste a su vez es menor o igual que el de un long int. Veremos cómo averiguar estos valores cuando estudiemos los operadores.

A cierto nivel, podemos considerar los tipos char, short int, int y long int como tipos enteros diferentes. Pero esa diferencia consiste sólo en el tamaño del valor máximo que pueden contener, y en el tamaño que ocupan en memoria, claro.

Este tipo de variables es útil para almacenar números relativamente grandes, pero sin decimales, por ejemplo el dinero que tienes en el banco, (salvo que seas Bill Gates), o el número de lentejas que hay en un kilo de lentejas.

Tipo "long long":

[signed|unsigned] long long [int] <identificador>[,<identificador2>[,<identificador3>]...];

Este tipo no pertenece al estandar ANSI, sin embargo, está disponible en compiladores GNU, como el que se usa en Linux o el que usa el propio Dev-C++ (y otros entornos de desarrollo para Windows).

Este tipo ocupa el siguiente puesto en cuanto a tamaño, después de long int. Como en los otros casos, su tamaño no está definido, pero sí sabemos que será mayor o igual que el de long int.

Tipo "float" o coma flotante:

float <identificador>[,<identificador2>[,<identificador3>]...];

Las variables de este tipo almacenan números en formato de coma flotante, esto es, contienen un valor de mantisa y otro de exponente, que, para entendernos, codifican números con decimales.

Aunque el formato en que se almacenan estos números en un ordenador es binario, podemos ver cómo es posible almacenar números muy grandes o muy pequeños mediante dos enteros relativamente pequeños, usando potencias en base 10. Por ejemplo, tenemos para la mantisa un valor entero, m, entre -0.99 y 0.99, y para el exponente un valor, e entre -9 y 9.

Los números se interpretan como m x 10e.

Este formato nos permite almacenar números entre -0.99 x 109 y 0.99 x 109. Es decir, entre -990000000 y 99000000.

Y también números tan pequeños como 0.01 x 10-9 ó -0.01 x 10-9. Es decir, como 0,00000000001 ó -0,00000000001.

Esto sólo con tres dígitos decimales, más los signos. Pero en un ordenador se usa aritmética binaria. Por ejemplo, para un tipo float típico de 32 bits, se usa un bit de signo para la mantisa y otro para el exponente, siete bits para el exponente y 23 para la mantisa.

Para más detalles se puede consultar el siguiente enlace: representación de los números en punto flotante.

Estas variables son aptas para variables de tipo real, como por ejemplo el cambio entre euros y dólares. O para números muy grandes, como la producción mundial de trigo, contada en granos.

Pero el fuerte de estos números no es la precisión, sino el orden de magnitud, es decir lo grande o pequeño que es el número que codifica. Por ejemplo, la siguiente cadena de operaciones no dará el resultado correcto:

float a = 12335545621232154;
a = a + 1;
a = a - 12335545621232154;

Finalmente, "a" valdrá 0 y no 1, como sería de esperar.

Los formatos en coma flotante sacrifican precisión en favor de tamaño. Sin embargo el ejemplo si funcionaría con números más pequeños. Esto hace que las variables de tipo float no sean muy adecuadas para todos los casos, como veremos más adelante.

Puede que te preguntes (alguien me lo ha preguntado), qué utilidad tiene algo tan impreciso. La respuesta es: aquella que tú, como programador, le encuentres. Te aseguro que float se usa muy a menudo. Por ejemplo, para trabajar con temperaturas, la precisión es suficiente para el margen de temperaturas que normalmente manejamos y para almacenar al menos tres decimales. Pero hay millones de otras situaciones en que resultan muy útiles.

Tipo "bool" o Booleano:

bool <identificador>[,<identificador2>[,<identificador3>]...];

Las variables de este tipo sólo pueden tomar dos valores true (verdadero) o false (falso). Sirven para evaluar expresiones lógicas. Este tipo de variables se puede usar para almacenar respuestas, por ejemplo: ¿Posees carné de conducir?. O para almacenar informaciones que sólo pueden tomar dos valores, por ejemplo: qué mano usas para escribir. En estos casos debemos acuñar una regla, en este ejemplo, podría ser diestro->true, zurdo->false.

bool respuesta;
bool continuar;

Nota: En algunos compiladores de C++ antiguos no existe el tipo bool. Lo lógico sería no usar esos compiladores, y conseguir uno más actual. Pero si esto no es posible, se puede simular este tipo a partir de un enumerado.

enum bool {false=0, true};

Tipo "double" o coma flotante de doble precisión:

[long] double <identificador>[,<identificador2>[,<identificador3>]...];

Las variables de este tipo almacenan números en formato de coma flotante, mantisa y exponente, al igual que float, pero usan una precisión mayor, a costa de usar más memoria, claro. Son aptos para variables de tipo real. Usaremos estas variables cuando trabajemos con números grandes, pero también necesitemos gran precisión. El mayor espacio para almacenar el número se usa tanto para ampliar el rango de la mantisa como el del exponente, de modo que no sólo se gana en precisión, sino también en tamaño.

Al igual que pasaba con los números enteros, no existe un tamaño predefinido para cada tipo en coma flotante. Lo que sí sabemos es que el tamaño de double es mayor o igual que el de float y el de long double mayor o igual que el de double.

Lo siento, pero no se me ocurre ahora ningún ejemplo en el que sea útil usar uno de estos tipos.

Bueno, también me han preguntado por qué no usar siempre double o long double y olvidarnos de float. La respuesta es que C++ siempre ha estado orientado a la economía de recursos, tanto en cuanto al uso de memoria como al uso de procesador. Si tu problema no requiere la precisión de un double o long double, ¿por qué derrochar recursos? Por ejemplo, en el compilador Dev-C++ float requiere 4 bytes, double 8 y long double 12, por lo tanto, para manejar un número en formato de long double se requiere el triple de memoria y el triple o más tiempo de procesador que para manejar un float.

Como programadores estamos en la obligación de no desperdiciar nuestros recursos, y mucho menos los recursos de nuestros clientes, para los que crearemos nuestros programas. C++ nos dan un gran control sobre estas características, es nuestra responsabilidad aprender a usarlo como es debido.

Tipo "void" o sin tipo:

void <identificador>[,<identificador2>[,<identificador3>]...];

En realidad esta sintaxis es errónea: no se pueden declarar variables de tipo void, ya que tal cosa no tiene sentido.

void es un tipo especial que indica la ausencia de tipo. Se usa para indicar el tipo del valor de retorno en funciones que no devuelven ningún valor, y también para indicar la ausencia de parámetros en funciones que no los requieren, (aunque este uso sólo es obligatorio en C, y opcional en C++), también se usará en la declaración de punteros genéricos, aunque esto lo veremos más adelante.

Las funciones que no devuelven valores parecen una contradicción. En lenguajes como Pascal, estas funciones se llaman procedimientos. Simplemente hacen su trabajo, y no revuelven valores. Por ejemplo, una función que se encargue de borrar la pantalla, no tienen nada que devolver, hace su trabajo y regresa. Lo mismo se aplica a las funciones sin parámetros de entrada, el mismo ejemplo de la función para borrar la pantalla no requiere ninguna entrada para poder realizar su cometido.

Tipo "enum" o enumerado:

enum [<identificador_de_enum>] {
   <nombre> [= <valor>], ...} <identificador>[,<identificador2>[,<identificador3>]...];
enum <identificador_de_enum> {
   <nombre> [= <valor>], ...} [<identificador>[,<identificador2>[,<identificador3>]...]];

Se trata de una sintaxis más elaborada que las que hemos visto hasta ahora, pero no te asustes, (si es que te ha asustado esto) cuando te acostumbres a ver este tipo de cosas comprobarás que son fáciles de comprender.

Este tipo nos permite definir conjuntos de constantes enteras, llamados datos de tipo enumerado. Las variables declaradas de este tipo sólo podrán tomar valores dentro del dominio definido en la declaración.

Vemos que hay dos sintaxis. En la primera, el identificador de tipo es opcional, y si lo usamos podremos declarar más variables del tipo enumerado en otras partes del programa:

[enum] <identificador_de_enum> <identificador>[,<identificador2>[,<identificador3>]...];

La segunda sintaxis nos permite añadir una lista de variables, también opcional.

De este modo podemos separar la definición del tipo enumerado de la declaración de variables de ese tipo:

enum orden {primero=1, segundo, tercero};
...
enum orden  id1, id2, id3;

O podemos hacer ambas cosas en la misma sentencia: definición y declaración:

enum orden {primero=1, segundo, tercero} id1, id2, id3;

Si decidimos no usar un identificador para el enumerado sólo podremos declarar variables en ese momento, y no en otros lugares del programa, ya que no será posible referenciarlo:

enum {primero=1, segundo, tercero} uno, dos;

Varios identificadores pueden tomar el mismo valor, pero cada identificador sólo puede usarse en un tipo enumerado. Por ejemplo:

enum tipohoras { una=1, dos, tres, cuatro, cinco,
  seis, siete, ocho, nueve, diez, once,
  doce, trece=1, catorce, quince,
  dieciseis, diecisiete, dieciocho,
  diecinueve, veinte, ventiuna,
  ventidos, ventitres, venticuatro = 0};

En este caso, una y trece valen 1, dos y catorce valen 2, etc. Y veinticuatro vale 0. Como se ve en el ejemplo, una vez se asigna un valor a un elemento de la lista, los siguientes toman valores correlativos. Si no se asigna ningún valor, el primer elemento tomará el valor 0.

Los nombres de las constantes pueden utilizarse en el programa, pero no pueden ser leídos ni escritos. Por ejemplo, si el programa en un momento determinado nos pregunta la hora, no podremos responder doce y esperar que se almacene su valor correspondiente. Del mismo modo, si tenemos una variable enumerada con el valor doce y la mostramos por pantalla, se mostrará 12, no doce. Deben considerarse como "etiquetas" que sustituyen a enteros, y que hacen más comprensibles los programas. Insisto en que internamente, para el compilador, sólo son enteros, en el rango de valores válidos definidos en cada enum.

La lista de valores entre las llaves definen lo que se denomina el "dominio" del tipo enumerado. Un dominio es un conjunto de valores posibles para un dato. Una variable del tipo enumerado no podrá tomar jamás un valor fuera del dominio.

Palabras reservadas usadas en este capítulo

Las palabras reservadas son palabras propias del lenguaje de programación. Están reservadas en el sentido de que no podemos usarlas como identificadores de variables o de funciones.

char, int, float, double, bool, void, enum, unsigned, signed, long, short, true y false.