Fundamentos de programación en C - Tema 2

Por Nacho Cabanes
Material para la asignatura "Fundamentos de Programación",
Ciclo Formativo de Administración de Sistemas Informáticos,
I.E.S. San Vicente (San Vicente, Alicante), curso 2005/2006


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

3 (decimal) = 0011 -3 = 1011
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.