2. Tipos de datos básicos
NOTA: Este apartado
está sin
terminar de revisar.
Falta incluir imágenes, añadir colores... incluso
releer ;-)
2.1. Tipo de datos entero
2.1.1. Tipos de enteros: signed/unsigned, short/long
Hemos hablado de números enteros, de cómo realizar operaciones sencillas y de cómo usar variables para reservar espacio y poder trabajar con datos cuyo valor no sabemos de antemano.
Empieza a ser el momento de refinar, de dar más
detalles. El primer “matiz” importante es el signo de los
números: hemos hablado de números enteros (sin
decimales), pero no hemos detallado si esos números son
positivos, negativos o si podemos elegirlo nosotros.
Pues es sencillo: si no decimos nada, se da por sentado que el número puede ser negativo o positivo. Si queremos dejarlo más claro, podemos añadir la palabra “signed” (con signo) antes de “int”. Este es uno de los “modificadores” que podemos emplear. Otro modificador es “unsigned” (sin signo), que nos sirve para indicar al compilador que no vamos a querer guardar números negativos, sólo positivos. Vamos a verlo con un ejemplo:
/*-------------------------*/
/* Ejemplo en C nº 6: */
/* C006.C */
/* */
/* Numeros enteros con y */
/* sin signo */
/*-------------------------*/
#include <stdio.h>
int primerNumero;
signed int segundoNumero;
unsigned int tercerNumero;
main() /* Cuerpo del programa */
{
primerNumero = -1;
segundoNumero = -2;
tercerNumero = 3;
printf("El primer numero es %d, ", primerNumero);
printf("el segundo es %d, ", segundoNumero);
printf("el tercer numero es %d.", tercerNumero);
}
El resultado de este programa es el que podíamos esperar:
El primer numero es -1, el segundo es -2, el tercer numero es 3
¿Y sí hubiéramos escrito
“tercerNumero=-3” después de decir que va a ser un
entero sin signo, pasaría algo? No, el programa mostraría
un –3 en la pantalla. El lenguaje C nos deja ser tan descuidados
como queramos ser, así que generalmente deberemos trabajar con
un cierto cuidado.
La pregunta que puede surgir ahora es: ¿resulta
útil eso de no usar números negativos? Sí, porque
entonces podremos usar números positivos de mayor tamaño
(dentro de poco veremos por qué ocurre esto).
De igual modo que detallamos si queremos que un número pueda ser negativo o no, tenemos disponible otro modificador que nos permite decir que queremos más espacio, para poder almacenar números más grandes. Un “int” normalmente nos permite guardar números inferiores al 2.147.483.647, pero si usamos el modificador “long”, ciertos sistemas nos permitirán usar números mucho mayores, o bien con el modificador “short” podremos usar números menores (sólo hasta 32.767, en caso de que necesitemos optimizar la cantidad de memoria que utilizamos).
Ejercicio propuesto: Multiplicar dos números de 4 cifras que teclee el usuario, usando el modificador “long”.
2.1.2. Problemática: asignaciones y tamaño de
los números; distintos espacios ocupados según el sistema
El primer problema a tener en cuenta es que si asignamos a una
variable “demasiado pequeña” un valor más
grande del que podría almacenar, podemos obtener valores
incorrectos. Un caso típico es intentar asignar un valor
“long” a una variable “short”:
/*-------------------------*/
/* Ejemplo en C nº 7: */
/* C007.C */
/* */
/* Numeros enteros */
/* demasiado grandes */
/*-------------------------*/
#include <stdio.h>
int primerNumero;
signed int segundoNumero;
unsigned int tercerNumero;
main() /* Cuerpo del programa */
{
primerNumero = -1;
segundoNumero = 33000;
tercerNumero = 123456;
printf("El primer numero es %d, ", primerNumero);
printf("el segundo es %d, ", segundoNumero);
printf("el tercer numero es %d.", tercerNumero);
}
El resultado en pantalla de este programa, si usamos el compilador Turbo C 2.01 no sería lo que esperamos:
El primer numero es -1, el segundo es -32536, el tercer numero es -7616
Y un problema similar lo podríamos tener si asignamos
valores de un número sin signo a uno con signo (o viceversa).
Pero el problema llega más allá: el espacio
ocupado por un “int” depende del sistema operativo que
usemos, a veces incluso del compilador. Por ejemplo, hemos comentado
que con un “int” podemos almacenar números cuyo
valor sea inferior a 2.147.483.647, pero el ejemplo anterior usaba
números pequeños y aun así daba problemas.
¿Por qué? Por que este último ejemplo lo
hemos probado con un compilador para MsDos. Se trata de un sistema
operativo más antiguo, de 16 bits, capaz de manejar
números de menor tamaño. En estos sistemas, los
“int” llegaban hasta 32.767 (lo que equivale a un short en
los sistemas modernos de 32 bits) y los “short” llegaban
sólo hasta 127. En los sistemas de 64 bits (poco frecuentes
todavía) existen “int” de mayor tamaño.
Para entender por qué ocurre esto, vamos a hablar un
poco sobre unidades de medida utilizadas en informática y sobre
sistemas de numeración.
2.1.3. Unidades de medida empleadas en informática (1): bytes, kilobytes, megabytes...
En informática, la unidad básica de
información es el byte. En la práctica, podemos pensar
que un byte es el equivalente a una letra. Si un cierto texto
está formado por 2000 letras, podemos esperar que ocupe unos
2000 bytes de espacio en nuestro disco.
Eso sí, suele ocurrir que realmente un texto de 2000
letras que se guarde en el ordenador ocupe más de 2000 bytes,
porque se suele incluir información adicional sobre los tipos de
letra que se han utilizado, cursivas, negritas, márgenes y
formato de página, etc.
Un byte se queda corto a la hora de manejar textos o datos
algo más largos, con lo que se recurre a un múltiplo
suyo, el kilobyte, que se suele abreviar Kb o K. En teoría, el
prefijo kilo querría decir “mil”, luego un kilobyte
debería ser 1000 bytes, pero en los ordenadores conviene buscar
por comodidad una potencia de 2 (pronto veremos por qué), por lo
que se usa 2 10 =1024. Así, la equivalencia exacta es 1 K = 1024
bytes. Los K eran unidades típicas para medir la memoria de
ordenadores: 640 K ha sido mucho tiempo la memoria habitual en los IBM
PC y similares. Por otra parte, una página mecanografiada suele
ocupar entre 2 K (cerca de 2000 letras) y 4 K.
Cuando se manejan datos realmente extensos, se pasa a otro
múltiplo, el megabyte o Mb, que es 1000 K (en realidad 1024 K) o
algo más de un millón de bytes. Por ejemplo, en un
diskette “normal” caben 1.44 Mb, y en un Compact Disc para
ordenador (Cd-Rom) se pueden almacenar
hasta 700 Mb. La memoria principal (RAM) de un ordenador actual suele
andar por encima de los 512 Mb, y un disco duro actual puede tener una
capacidad superior a los 80.000 Mb.
Para estas unidades de gran capacidad, su tamaño no se
suele medir en megabytes, sino en el múltiplo siguiente: en
gigabytes, con la correspondencia 1 Gb = 1024 Mb. Así, son cada
vez más frecuentes los discos duros con una capacidad de 120,
200 o más gigabytes.
Y todavía hay unidades mayores, pero que aún se utilizan muy poco. Por ejemplo, un terabyte son 1024 gigabytes.
Todo esto se puede resumir así:
Unidad Equivalencia Valores posibles
Byte - 0 a 255 (para guardar 1 letra)
Kilobyte (K o Kb) 1024 bytes Aprox. media página mecanografiada
Megabyte (Mb) 1024 Kb -
Gigabyte (Gb) 1024 Mb -
Terabyte (Tb) 1024 Gb -
Pero por debajo de los bytes también hay unidades más pequeñas...
2.1.4. Unidades de medida empleadas en informática (2): los bits
Dentro del ordenador, la información se debe almacenar realmente de alguna forma que a él le resulte "cómoda" de manejar. Como la memoria del ordenador se basa en componentes electrónicos, la unidad básica de información será que una posición de memoria esté usada o no (totalmente llena o totalmente vacía), lo que se representa como un 1 o un 0. Esta unidad recibe el nombre de bit.
Un bit es demasiado pequeño para un uso normal (recordemos: sólo puede tener dos valores: 0 ó 1), por lo que se usa un conjunto de ellos, 8 bits, que forman un byte. Las matemáticas elementales (combinatoria) nos dicen que si agrupamos los bits de 8 en 8, tenemos 256 posibilidades distintas (variaciones con repetición de 2 elementos tomados de 8 en 8: VR2,8):
00000000
00000001
00000010
00000011
00000100
...
11111110
11111111
Por tanto, si en vez de tomar los bits de 1 en 1 (que resulta
cómodo para el ordenador, pero no para nosotros) los utilizamos
en grupos de 8 (lo que se conoce como un byte), nos encontramos con 256
posibilidades distintas, que ya son más que suficientes para
almacenar una letra, o un signo de puntuación, o una cifra
numérica o algún otro símbolo. Por ejemplo, se
podría decir que cada vez que encontremos la secuencia 00000010
la interpretaremos como una letra A, y la combinación 00000011
como una letra B, y así sucesivamente.
También existe una correspondencia entre cada grupo de bits y un número del 0 al 255: si usamos el sistema binario de numeración (que aprenderemos dentro de muy poco), en vez del sistema decimal, tenemos que:
0000 0000 (binario) = 0 (decimal)
0000 0001 (binario) = 1 (decimal)
0000 0010 (binario) = 2 (decimal)
0000 0011 (binario) = 3 (decimal)
...
1111 1110 (binario) = 254 (decimal)
1111 1111 (binario) = 255 (decimal)
En la práctica, existe un código
estándar, el código ASCII (American Standard Code for
Information Interchange, código estándar americano para
intercambio de información), que relaciona cada letra,
número o símbolo con una cifra del 0 al 255 (realmente,
con una secuencia de 8 bits): la "a" es el número 97, la "b" el
98, la "A" el 65, la "B", el 32, el "0" el 48, el "1" el 49, etc.
Así se tiene una forma muy cómoda de almacenar la
información en ordenadores, ya que cada letra ocupará
exactamente un byte (8 bits: 8 posiciones elementales de memoria).
Aun así, hay un inconveniente con el código ASCII: sólo los primeros 127 números son estándar. Eso quiere decir que si escribimos un texto en un ordenador y lo llevamos a otro, las letras básicas (A a la Z, 0 al 9 y algunos símbolos) no cambiarán, pero las letras internacionales (como la Ñ o las vocales con acentos) puede que no aparezcan correctamente, porque se les asignan números que no son estándar para todos los ordenadores.
Nota: Eso de que realmente el ordenador trabaja con ceros y unos, por lo que le resulta más fácil manejar los números que son potencia de 2 que los que no lo son, es lo que explica que el prefijo kilo no quiera decir “exactamente mil”, sino que se usa la potencia de 2 más cercana: 210 =1024. Por eso, la equivalencia exacta es 1 K = 1024 bytes.
2.1.5. Sistemas de numeración: 1- Sistema binario
Nosotros normalmente utilizamos el sistema decimal de numeración: todos los números se
expresan a partir de potencias de 10, pero normalmente lo hacemos sin pensar.
Por ejemplo, el número 3.254 se podría desglosar como:
254 = 3 · 1000 + 2 · 100 + 5 · 10 + 4 · 1
o más detallado todavía:
254 = 3 · 103 + 2 · 102 + 5 · 101 + 4 · 100
(aunque realmente nosotros lo hacemos automáticamente:
no nos paramos a pensar este tipo de cosas cuando sumamos o
multiplicamos dos números).
Para los ordenadores no es cómodo contar hasta 10. Como
partimos de “casillas de memoria” que están
completamente vacías (0) o completamente llenas (1), sólo
les es realmente cómodo contar con 2 cifras: 0 y 1.
Por eso, dentro del ordenador cualquier número se
deberá almacenar como ceros y unos, y entonces los
números se deberán desglosar en potencias de 2 (el
llamado “sistema binario”):
13 = 1 · 8 + 1 · 4 + 0 · 2 + 1 · 1
o más detallado todavía:
13 = 1 · 2 3 + 1 · 2 2 + 0 · 2 1 + 1 · 2 0
de modo que el número decimal 13 se escribirá en binario como 1101.
En general, convertir un número binario al sistema
decimal es fácil: lo expresamos como suma de potencias de 2 y
sumamos:
0110 1101 (binario) = 0 · 2 7 + 1 · 2 6 + 1
· 2 5 + 0 · 2 4 + 1 · 2 3 + 1 · 2 2 + 0
· 2 1 + 1 · 2 0 =
= 0 · 128 + 1 · 64 + 1 · 32 + 0 · 16 + 1
· 8 + 1· 4 + 0 · 2 + 1 · 1 = 109 (decimal)
Convertir un número de decimal a binario resulta algo menos intuitivo. Una forma sencilla es ir
dividiendo entre las potencias de 2, y coger todos los cocientes de las divisiones:
109 / 128 = 0 (resto: 109)
109 / 64 = 1 (resto: 45)
45 / 32 = 1 (resto: 13)
13 / 16 = 0 (resto: 13)
13 / 8 = 1 (resto: 5)
5 / 4 = 1 (resto: 1)
1 / 2 = 0 (resto: 1)
1 / 1 = 1 (se terminó).
Si “juntamos” los cocientes que hemos obtenido,
aparece el número binario que buscábamos: 109 decimal =
0110 1101 binario
(Nota: es frecuente separar los números binarios en
grupos de 4 cifras -medio byte- para mayor legibilidad, como yo he
hecho en el ejemplo anterior; a un grupo de 4 bits se le llama nibble).
Otra forma sencilla de convertir de decimal a binario es dividir consecutivamente entre 2 y coger los restos que hemos obtenido, pero en orden inverso:
109 / 2 = 54, resto 1
54 / 2 = 27, resto 0
27 / 2 = 13, resto 1
13 /2 = 6, resto 1
6 / 2 = 3, resto 0
3 / 2 = 1, resto 1
1 / 2 = 0, resto 1
(y ya hemos terminado)
Si leemos esos restos de abajo a arriba, obtenemos el
número binario: 1101101 (7 cifras, si queremos completarlo a 8
cifras rellenamos con ceros por la izquierda: 01101101).
¿Y se pueden hacer operaciones con números binarios? Sí, casi igual que en decimal:
0·0 = 0 0·1 = 0 1·0 = 0 1·1 = 1
0+0 = 0 0+1 = 1 1+0 = 1 1+1 = 10 (en decimal: 2)
Ejercicios propuestos:
1. Expresar en sistema binario los números decimales 17, 101, 83, 45.
2. Expresar en sistema decimal los números binarios de 8 bits: 01100110, 10110010,
11111111, 00101101
3. Sumar los números 01100110+10110010, 11111111+00101101. Comprobar el
resultado sumando los números decimales obtenidos en el ejercicio anterior.
4. Multiplicar los números binarios de 4 bits 0100·1011, 1001·0011. Comprobar el
resultado convirtiéndolos a decimal.
2.1.6. Sistemas de numeración: 2- Sistema octal
Hemos visto que el sistema de numeración más
cercano a como se guarda la información dentro del ordenador es
el sistema binario. Pero los números expresados en este sistema
de numeración "ocupan mucho". Por ejemplo, el número 254
se expresa en binario como 11111110 (8 cifras en vez de 3).
Por eso, se han buscado otros sistemas de numeración
que resulten más "compactos" que el sistema binario cuando haya
que expresar cifras medianamente grandes, pero que a la vez mantengan
con éste una correspondencia algo más sencilla que el
sistema decimal. Los más
usados son el sistema octal y, sobre todo, el hexadecimal.
El sistema octal de numeración trabaja en base 8. La
forma de convertir de decimal a binario será, como siempre
dividir entre las potencias de la base. Por ejemplo:
254 (decimal) ->
254 / 64 = 3 (resto: 62)
62 / 8 = 7 (resto: 6)
6 / 1 = 6 (se terminó)
de modo que
254 = 3 · 8 2 + 7 · 8 1 + 6 · 8 0
o bien
254 (decimal) = 376 (octal)
Hemos conseguido otra correspondencia que, si bien nos resulta
a nosotros más incómoda que usar el sistema decimal, al
menos es más compacta: el número 254 ocupa 3 cifras en
decimal, y también 3 cifras en octal, frente a las 8 cifras que
necesitaba en sistema binario.
Pero además existe una correspondencia muy sencilla
entre el sistema octal y el sistema binario: si agrupamos los bits de 3
en 3, el paso de binario a octal es rapidísimo
254 (decimal) = 011 111 110 (binario)
011 (binario ) = 3 (decimal y octal)
111 (binario ) = 7 (decimal y octal)
110 (binario ) = 6 (decimal y octal)
de modo que
254 (decimal) = 011 111 110 (binario) = 376 (octal)
El paso desde el octal al binario y al decimal también es sencillo. Por ejemplo, el número 423
(octal) sería 423 (octal) = 100 010 011 (binario)
o bien
423 (octal) = 4 · 64 + 2 · 8 + 3 · 1 = 275 (decimal)
De cualquier modo, el sistema octal no es el que más se utiliza en la práctica, sino el
hexadecimal...
Ejercicios propuestos:
1. Expresar en sistema octal los números decimales 17, 101, 83, 45.
2. Expresar en sistema octal los números binarios de 8 bits: 01100110, 10110010,
11111111, 00101101
3. Expresar en el sistema binario los números octales 171, 243, 105, 45.
4. Expresar en el sistema decimal los números octales 162, 76, 241, 102.
2.1.7. Sistemas de numeración: 3- Sistema hexadecimal
El sistema octal tiene un inconveniente: se agrupan los bits de 3 en 3, por lo que convertir de
binario a octal y viceversa es muy sencillo, pero un byte está formado por 8 bits, que no es
múltiplo de 3.
Sería más cómodo poder agrupar de 4 en 4 bits, de modo que cada byte se representaría por 2
cifras. Este sistema de numeración trabajará en base 16 (2 4 =16), y es lo que se conoce como
sistema hexadecimal.
Pero hay una dificultad: estamos acostumbrados al sistema decimal, con números del 0 al 9, de
modo que no tenemos cifras de un solo dígito para los números 10, 11, 12, 13, 14 y 15, que
utilizaremos en el sistema hexadecimal. Para representar estas cifras usaremos las letras de la
A a la F, así:
0 (decimal) = 0 (hexadecimal)
1 (decimal) = 1 (hexadecimal)
2 (decimal) = 2 (hexadecimal)
3 (decimal) = 3 (hexadecimal)
4 (decimal) = 4 (hexadecimal)
5 (decimal) = 5 (hexadecimal)
6 (decimal) = 6 (hexadecimal)
7 (decimal) = 7 (hexadecimal)
8 (decimal) = 8 (hexadecimal)
9 (decimal) = 9 (hexadecimal)
10 (decimal) = A (hexadecimal)
11 (decimal) = B (hexadecimal)
12 (decimal) = C (hexadecimal)
13 (decimal) = D (hexadecimal)
14 (decimal) = E (hexadecimal)
15 (decimal) = F (hexadecimal)
Con estas consideraciones, expresar números en el sistema hexadecimal ya no es difícil:
254 (decimal) ->
254 / 16 = 15 (resto: 14)
14 / 1 = 14 (se terminó)
de modo que
254 = 15 · 16 1 + 14 · 16 0
o bien
254 (decimal) = FE (hexadecimal)
Vamos a repetirlo para un convertir de decimal a hexadecimal número más grande:
54331 (decimal) ->
54331 / 4096 = 13 (resto: 1083)
1083 / 256 = 4 (resto: 59)
59 / 16 = 3 (resto: 11)
11 / 1 = 11 (se terminó)
de modo que
54331 = 13 · 4096 + 4 · 256 + 3 · 16 + 11 · 1
o bien
254 = 13 · 16 3 + 4 · 16 2 + 3 · 16 1 + 11 · 16 0
es decir
54331 (decimal) = D43B (hexadecimal)
Ahora vamos a dar el paso inverso: convertir de hexadecimal a decimal, por ejemplo el número A2B5
A2B5 (hexadecimal) = 10 · 16 3 + 2 · 16 2 + 11 · 16 1 + 5 · 16 0 = 41653
El paso de hexadecimal a binario también es
(relativamente) rápido, porque cada dígito hexadecimal
equivale a una secuencia de 4 bits:
0 (hexadecimal) = 0 (decimal) = 0000 (binario)
1 (hexadecimal) = 1 (decimal) = 0001 (binario)
2 (hexadecimal) = 2 (decimal) = 0010 (binario)
3 (hexadecimal) = 3 (decimal) = 0011 (binario)
4 (hexadecimal) = 4 (decimal) = 0100 (binario)
5 (hexadecimal) = 5 (decimal) = 0101 (binario)
6 (hexadecimal) = 6 (decimal) = 0110 (binario)
Fundamentos de programación en C, por Nacho Cabanes
Revisión 0.05 – Página 33
7 (hexadecimal) = 7 (decimal) = 0111 (binario)
8 (hexadecimal) = 8 (decimal) = 1000 (binario)
9 (hexadecimal) = 9 (decimal) = 1001 (binario)
A (hexadecimal) = 10 (decimal) = 1010 (binario)
B (hexadecimal) = 11 (decimal) = 1011 (binario)
C (hexadecimal) = 12 (decimal) = 1100 (binario)
D (hexadecimal) = 13 (decimal) = 1101 (binario)
E (hexadecimal) = 14 (decimal) = 1110 (binario)
F (hexadecimal) = 15 (decimal) = 1111 (binario)
de modo que A2B5 (hexadecimal) = 1010 0010 1011 0101 (binario)
y de igual modo, de binario a hexadecimal es dividir en grupos de 4 bits y hallar el valor de
cada uno de ellos:
110010100100100101010100111 =>
0110 0101 0010 0100 1010 1010 0111 = 6524AA7
2.1.8. Formato de constantes enteras: oct, hex
En C tenemos la posibilidad de dar un valor a una variable usando el sistema decimal, como hemos hecho hasta ahora, pero también podemos usar el sistema octal si ponemos un 0 a la izquierda del número, o el sistema hexadecimal, si usamos 0x (pero no existe una forma directa de trabajar con números en binario):
/*-------------------------*/
/* Ejemplo en C nº 8: */
/* C008.C */
/* */
/* Numeros enteros en */
/* decimal, octal y */
/* hexadecimal */
/*-------------------------*/
#include <stdio.h>
int primerNumero;
int segundoNumero;
int tercerNumero;
main() /* Cuerpo del programa */
{
primerNumero = 15; /* Decimal */
segundoNumero = 015; /* Octal: 8+5=13 */
tercerNumero = 0x15; /* Hexadecimal: 16+5=21 */
printf("El primer numero es %d, ", primerNumero);
printf("el segundo es %d, ", segundoNumero);
printf("el tercer numero es %d.", tercerNumero);
}
El resultado de este programa sería
El primer numero es 15, el segundo es 13, el tercer numero es 21.
2.1.9. Representación interna de los enteros
Ahora que ya sabemos cómo se representa un número en sistema binario, podemos detallar un
poco más cómo se almacenan los números enteros en la memoria del ordenador, lo que nos
ayudará a entender por qué podemos tener problemas al asignar valores entre variables que no
sean exactamente del mismo tipo.
En principio, los números positivos se almacenan como hemos visto cuando hemos hablado del
sistema binario. El único matiz que falta es indicar cuantos bits hay disponibles para cada
número. Lo habitual es usar 16 bits para un “int” si el sistema operativo es de 16 bits (como
MsDos) y 32 bits para los sistemas operativos de 32 bits (como la mayoría de las versiones de
Windows y de Linux –o sistemas Unix en general-).
En cuanto a los “short” y los “long”, depende del sistema. Vamos a verlo con un ejemplo:
Turbo C 2.01 (MsDos) GCC 3.4.2 (Windows 32b) GCC 3.4.2 (Linux 64b)
int: bits 16 32 32
int: valor máximo 32.767 2.147.483.647 2.147.483.647
short: bits 16 16 16
short: valor máximo 32.767 32.767 32.767
long: bits 32 32 64
long: valor máximo 2.147.483.647 2.147.483.647 9·1018
Para los números enteros negativos, existen varios formas posibles de representarlos. Las más habituales son:
Ø Signo y magnitud: el primer bit (el de más a
la izquierda) se pone a 1 si el número es negativo y se deja a 0
si es positivo. Los demás bits se calculan como ya hemos visto.
Por ejemplo, si usamos 4 bits, tendríamos
6 (decimal) = 0110 -6 = 1110
Es un método muy sencillo, pero que tiene el inconveniente de que las operaciones en las que aparecen números negativos no se comportan correctamente. Vamos a ver un ejemplo, con números de 8 bits:
13 (decimal) = 0000 1101 - 13 (decimal) = 1000 1101
34 (decimal) = 0010 0010 - 34 (decimal) = 1010 0010
13 + 34 = 0000 1101 + 0010 0010 = 0010 1111 = 47 (correcto)
(-13) + (-34) = 1000 1101 + 1010 0010 = 0010 1111 = 47 (INCORRECTO)
13 + (-34) = 0000 1101 + 1010 0010 = 1010 1111 = -47 (INCORRECTO)
Ø Complemento a 1: se cambian los ceros por unos para expresar los números negativos.
Por ejemplo, con 4 bits
3 (decimal) = 0011 -3 = 1100
6 (decimal) = 0110 -6 = 1001
También es un método sencillo, en el que las
operaciones con números negativos salen bien, y que sólo
tiene como inconveniente que hay dos formas de expresar el
número 0 (0000 0000 o 1111 1111), lo que complica algunos
trabajos internos del ordenador.
Ejercicio propuesto: convertir los números decimales 13, 34, -13, -34 a sistema binario, usando complemento a uno para expresar los números negativos. Calcular (en binario) el resultado de las operaciones 13+34, (-13)+(-34), 13+(-34) y comprobar que los resultados que se obtienen son los correctos.
Ø Complemento a 2: para los negativos, se cambian los ceros por unos y se suma uno al resultado.
Por ejemplo, con 4 bits
3 (decimal) = 0011 -3 = 1101
6 (decimal) = 0110 -6 = 1010
Es un método que parece algo más complicado,
pero que no es difícil de seguir, con el que las operaciones con
números negativos salen bien, y no tiene problemas para expresar
el número 0 (00000000).
Ejercicio propuesto: convertir los números decimales 13, 34, -13, -34 a sistema binario, usando complemento a dos para expresar los números negativos. Calcular (en binario) el resultado de las operaciones 13+34, (-13)+(-34), 13+(-34) y comprobar que los resultados que se obtienen son los correctos.
En general, todos los formatos que permiten guardar
números negativos usan el primer bit para el signo. Por eso, si
declaramos una variable como “unsigned”, ese primer bit se
puede utilizar como parte de los datos, y podemos almacenar
números más grandes. Por ejemplo, un “unsigned
int” en MsDos podría tomar valores entre 0 y 65.535.
2.1.10. Incremento y decremento
Hay una operación que es muy frecuente cuando se crean programas, pero que no tiene un
símbolo específico para representarla en matemáticas. Es incrementar el valor de una variable
en una unidad:
a = a+1;
Pues bien, en C, existe una notación más compacta para esta operación, y para la opuesta (el decremento):
a++; es lo mismo que a = a+1;
a--; es lo mismo que a = a-1;
Pero esto tiene más misterio todavía del que
puede parecer en un primer vistazo: podemos distinguir entre
"preincremento" y "postincremento". En C es posible hacer asignaciones
como
b = a++;
Así, si "a" valía 2, lo que esta
instrucción hace es dar a "b" el valor de "a" y aumentar el
valor de "a". Por tanto, al final tenemos que b=2 y a=3
(postincremento: se incrementa "a" tras asignar su valor). En cambio,
si escribimos
b = ++a;
y "a" valía 2, primero aumentamos "a" y luego los asignamos a "b" (preincremento), de modo que a=3 y b=3.
Por supuesto, también podemos distinguir postdecremento (a--) y predecremento (--a).
Ejercicio propuesto: Crear un programa que use tres variables
x,y,z. Sus valores iniciales serán 15, -10, 2.147.483.647. Se
deberá incrementar el valor de estas variables.
¿Qué valores esperas que se obtengan? Contrástalo
con el resultado obtenido por el programa.
Y ya que estamos hablando de las asignaciones, hay que comentar que en C es posible hacer asignaciones múltiples:
a = b = c = 1;
2.1.11. Operaciones abreviadas: +=
Pero aún hay más. Tenemos incluso formas reducidas de escribir cosas como "a = a+5". Allá
van
a += b ; es lo mismo que a = a+b;
a -= b ; es lo mismo que a = a-b;
a *= b ; es lo mismo que a = a*b;
a /= b ; es lo mismo que a = a/b;
a %= b ; es lo mismo que a = a%b;
Ejercicio propuesto: Crear un programa que use tres variables x,y,z. Sus valores iniciales
serán 15, -10, 214. Se deberá incrementar el valor de estas variables en 12, usando el formato
abreviado. ¿Qué valores esperas que se obtengan? Contrástalo con el resultado obtenido por el
programa.
2.1.12. Modificadores de acceso: const, volatile
Podemos encontrarnos con variables cuyo valor realmente no
varíe durante el programa. Entonces podemos usar el modificador
“const” para indicárselo a nuestro compilador, y
entonces ya no nos dejará modificarlas por error.
const int MAXIMO = 10;
si luego intentamos
MAXIMO = 100;
obtendríamos un mensaje de error que nos diría que no podemos modificar una constante.
También podemos encontrarnos (aunque es poco frecuente)
con el caso contrario: una variable que pueda cambiar de valor sin que
nosotros modifiquemos (porque accedamos a una valor que cambie
“solo”, como el reloj interno del ordenador, o porque los
datos sean compartidos con otro programa que también pueda
modicarlos, por ejemplo). En ese caso, usaremos el modificador
“volatile”, que hace que el compilador siempre compruebe el
valor más reciente de la variable antes de usarlo, por si
hubiera cambiado:
volatile int numeroDeUsuarios = 1;
2.2. Tipo de datos real
Cuando queremos almacenar datos con decimales, no nos sirve el
tipo de datos “int”. Necesitamos otro tipo de datos que
sí esté preparado para guardar números
“reales” (con decimales). En el mundo de la
informática hay dos formas de trabajar con números reales:
Ø Coma fija: el número máximo de cifras
decimales está fijado de antemano, y el número de cifras
enteras también. Por ejemplo, con un formato de 3 cifras enteras
y 4 cifras decimales, el número 3,75 se almacenaría
correctamente, el número 970,4361 también, pero el
5,678642 se guardaría como 5,6786 (se perdería a partir
de la cuarta cifra decimal) y el 1010 no se podría guardar
(tiene más de 3 cifras enteras).
Ø Coma flotante: el número de decimales y de cifras enteras permitido es variable, lo que importa es el número de cifras significativas (a partir del último 0). Por ejemplo, con 5 cifras significativas se podrían almacenar números como el 13405000000 o como el 0,0000007349 pero no se guardaría correctamente el 12,0000034, que se redondearía a un número cercano.
2.2.1. Simple y doble precisión
Tenemos dos tamaños para elegir, según si queremos guardar números con mayor cantidad de
cifras o con menos. Para números con pocas cifras significativas (un máximo de 6) existe el tipo
“float” y para números que necesiten más
precisión (unas 10) tenemos el tipo “double”:
float double
Tamaño en bits 32 64
Valor máximo -3,4·10-38 -1,7·10-308
Valor mínimo 3,4·1038 1,7·10308
Cifras significativas 6 o más 10 o más
En algunos sistemas existe un tipo “long double”, con mayor precisión todavía (40 bits o incluso
128 bits).
Para definirlos, se hace igual que en el caso de los números enteros:
float x;
o bien, si queremos dar un valor inicial en el momento de
definirlos (recordando que para las cifras decimales no debemos usar
una coma, sino un punto):
float x = 12.56;
2.2.2. Mostrar en pantalla números reales
En principio es sencillo: usaremos “printf”, al que le indicaremos “%f” como código de formato:
printf("El valor de x es %f", x); /* Escribiría 12.5600 */
Pero también podemos detallar la anchura, indicando el
número de cifras totales y el número de cifras decimales:
printf("El valor de x es %5.2f", x); /* Escribiría 12.56 */
Si indicamos una anchura mayor que la necesaria, se rellena con espacios al principio (queda alineado a la derecha)
printf("El valor de x es %7.2f", x); /* Escribiría “ 12.56” */
Si quisiéramos que quede alineado a la izquierda (con
los espacios de sobra al final), debemos escribir la anchura como un
número negativo
printf("El valor de x es %-7.2f", x); /* Escribiría “12.56 ” */
Si indicamos menos decimales que los necesarios, se redondeará el número
printf("El valor de x es %4.1f", x); /* Escribiría 12.6 */
Y si indicamos menos cifras enteras que las necesarias, no se
nos hará caso y el número se escribirá con la
cantidad de cifras que sea necesario usar
printf("El valor de x es %1.0f", x); /* Escribiría 13 */
Vamos a juntar todo esto en un ejemplo:
/*---------------------------*/
/* Ejemplo en C nº 9: */
/* C009.C */
/* */
/* Numeros en coma flotante */
/*---------------------------*/
#include <stdio.h>
float x = 12.56;
main() {
printf("El valor de x es %f", x);
printf(" pero lo podemos escribir con 2 decimales %5.2f", x);
printf(" o solo con uno %5.1f", x);
printf(" o con 7 cifras %7.1f", x);
printf(" o alineado a la izquierda %-7.1f", x);
printf(" o sin decimales %2.0f", x);
printf(" o solo con una cifra %1.0f", x);
}
El resultado sería
El valor de f es 12.560000 pero lo podemos escribir con 2 decimales 12.56 o solo
con uno 12.6 o con 7 cifras 12.6 o alineado a la izquierda 12.6 o sin de
cimales 13 o solo con una cifra 13
Ejercicio propuesto: El usuario de nuestro programa podrá teclear dos números de hasta 8
cifras significativas. El programa deberá mostrar el resultado de dividir el primer número entre
el segundo, utilizando tres cifras decimales.
2.3. Operador de tamaño: sizeof
Hemos comentado lo que habitualmente ocupa una variable de
tipo int, de tipo long int, de tipo float... Pero tenemos una forma de
saber exactamente lo que ocupa: un operador llamado
“sizeof” (tamaño de). Veamos un ejemplo de su uso
/*---------------------------*/
/* Ejemplo en C nº 10: */
/* C010.C */
/* */
/* Tamaño de una variable o */
/* de un tipo */
/*---------------------------*/
#include <stdio.h>
float f;
short int i;
main() {
printf("El tamaño de mi float es %d", sizeof f);
printf(" y lo normal para un float es %d", sizeof(float) );
printf(" pero un entero corto ocupa %d", sizeof i);
}
que nos diría lo siguiente:
El tamaño de mi float es 4 y lo normal para un float es 4 pero un entero corto ocupa 2
Como se puede ver, hay una peculiaridad: si quiero saber lo
que ocupa un tipo de datos, tengo que indicarlo entre
paréntesis: sizeof(float), pero si se trata de una variable,
puedo no usar paréntesis: sizeof i. Eso sí, el compilador
no dará ningún mensaje de error si uso un
paréntesis cuando sea una variable sizeof(i), así que
puede resultar cómodo poner siempre el paréntesis, sin
pararse a pensar si nos lo podríamos haber ahorrado.
2.4. Operador de molde: (tipo) operando
Si tenemos dos números enteros y hacemos su
división, el resultado que obtenemos es otro número
entero, sin decimales:
float f = 5/2; /* f valdrá 2.000000 */
Esto se debe a que la operación se realiza entre números enteros, se obtiene un resultado que es un número entero, y ese valor obtenido se asigna a la variable “float”... pero ya es demasiado tarde.
Para evitar ese tipo de problemas, podemos indicar que queremos convertir esos valores a numeros reales. Cuando son números, basta con que indiquemos algún decimal:
float f = 5.0/2.0; /* ahora f valdrá 2.500000 */
y si son variables, añadiremos antes de ellas
“(float)” para que las considere como números reales
antes de trabajar con ellas:
float f = (float) x / (float) y;
Vamos a verlo mejor en un programa completo:
/*---------------------------*/
/* Ejemplo en C nº 11: */
/* C011.C */
/* */
/* Conversión de int a */
/* float */
/*---------------------------*/
#include <stdio.h>
int n1 = 5, n2 = 2;
float division1, division2;
main() {
printf("Mis números son %d y %d", n1, n2);
division1 = n1/n2;
printf(" y su division es %f", division1 );
division2 = (float)n1 / (float)n2;
printf(" pero si convierto antes a float: %f", division2 );
}
que tendría como resultado
Mis números son 5 y 2 y su division es 2.000000 pero si convierto antes a float: 2.500000
De igual modo, podemos convertir un “float” a
“int” para despreciar sus decimales y quedarnos con la
parte entera:
/*---------------------------*/
/* Ejemplo en C nº 12: */
/* C012.C */
/* */
/* Conversión de float a */
/* int */
/*---------------------------*/
#include <stdio.h>
float x = 5, y = 3.5;
float producto;
main() {
printf("Mis números son %3.1f y %3.1f", x, y);
producto = x*y;
printf(" y su producto es %3.1f", producto);
printf(", sin decimales sería %d", (int) producto);
}
que daría
Mis números son 5.0 y 3.5 y su producto es 17.5, sin decimales sería 17
2.5. Tipo de datos carácter
También tenemos un tipo de datos que nos permite
almacenar una única letra (ya veremos que manipular una cadena
de texto completa es relativamente complicado). Es el tipo
“char”:
char letra;
Asignar valores es sencillo:
letra = 'a';
(hay que destacar que se usa una comilla simple en vez de comillas dobles). Mostrarlos en pantalla también es fácil:
printf("%c", letra);
Así, un programa que leyera una letra tecleada por el usuario, fijara otra y mostrara ambas podría ser:
/*---------------------------*/
/* Ejemplo en C nº 13: */
/* C013.C */
/* */
/* Tipo de datos char */
/*---------------------------*/
#include <stdio.h>
char letra1, letra2;
main() {
printf("Teclea una letra ");
scanf("%c", &letra1);
letra2 = 'a';
printf("La letra que has tecleado es %c y la prefijada es %c",
letra1, letra2);
}
2.5.1. Secuencias de escape: \n y otras.
Al igual que ocurría con expresiones como %d, que
tenían un significado especial, ocurre lo mismo con ciertos
caracteres, que nos permiten hacer cosas como bajar a la línea
siguiente o mostrar las comillas en pantalla.
Son las siguientes:
Secuencia Significado
\a Emite un pitido
\b Retroceso (permite borrar el último carácter)
\f Avance de página (expulsa una hoja en la impresora)
\n Avanza de línea (salta a la línea siguiente)
\r Retorno de carro (va al principio de la línea)
\t Salto de tabulación horizontal
\v Salto de tabulación vertical
\' Muestra una comilla simple
\" Muestra una comilla doble
\\ Muestra una barra invertida
\0 Carácter nulo (NULL)
\7 Emite un pitido (igual que \a)
\ddd Un valor en octal
\xddd Un valor en hexadecimal
Ejercicio propuesto: Crear un programa que pida al usuario que
teclee cuatro letras y las muestre en pantalla juntas, pero en orden
inverso, y entre comillas dobles. Por ejemplo si las letras que se
teclean son a, l, o, h, esribiría "hola".
2.5.2. Introducción a las dificultades de las cadenas de texto
En el lenguaje C, no existe un tipo de datos para representar
una cadena de texto. Eso supone que su manejo no sea tan sencillo como
el de los números enteros, numeros reales y las letras.
Deberemos tratarla como un bloque de varias letras. Por eso lo veremos
más adelante.