Herramientas de usuario

Herramientas del sitio


tipos

El sistema de tipos de C

El sistema de tipos de un lenguaje de programación incluye:

  • Sus tipos de datos primitivos
  • Expresiones y operadores
  • Las reglas de inferencia del tipo de una expresión
  • Como se definen nuevos tipos de datos

C ofrece los siguientes tipos primitivos:

  • Tipos numéricos: enteros y reales

C no define tipos especiales para valores booleanos (como en Java: boolean) o para los strings (como en Java: String).

Tipos numéricos enteros

Los enteros se almacenan internamente en binario. Por ejemplo:

(2001)10 = (111 1101 0001)2 = 210+29+28+27+26+24+20 = 1024+512+256+128+64+16+1

Aunque la representación interna sea en binario, el lenguaje C permite escribir los número en notación decimal, octal y hexadecimal.

Un número octal siempre comienza con un 0 y cada cifra representa 3 bits:

(2001)10 = (11 111 010 001)2 = (3721)8 = 03721

¡Cuidado! Un cero a la izquierda en C sí tiene valor, porque cambia la notación decimal por octal. Es decir:

int i= 010;

es equivalente a

int i= 8; 

Un número en hexadecimal siempre comienza con el prefijo 0x y cada cifra representa 4 bits:

(2001)10 = (111 1101 0001)2 = (7d1)16 = 0x7d1

Tipos enteros con signo

C ofrece en forma estándar 4 tipos enteros: char, short, int y long. El estándar no define exactamente cuanto espacio ocupan estos tipos en memoria y por lo tanto pueden variar de una plataforma a otra, especialmente en plataformas antiguas. En general char y short ocupan 1 y 2 bytes respectivamente pero históricamente int y long han ocupado un espacio diferente desde que se fabricaban máquinas de 16 bits (los primeros PCs), luego máquinas de 32 bits (procesadores 386 hasta los Pentium III) y finalmente las actuales máquinas de 64 bits.

La siguiente tabla describe el espacio ocupado en bytes y el rango de valores representables para máquinas de 16, 32 y 64 bits.

tipo espacio / rango en máquinas de 32 bits espacio / rango en máquinas de 64 bits espacio / rango en máquinas de 16 bits
char 1 / [ -27, 27[ 1 / [ -27, 27[ 1 / [ -27, 27[
short 2 / [ -215, 215[ 2 / [ -215, 215[ 2 / [ -215, 215[
int 4 / [ -231, 231[ 4 / [ -231, 231[ 2 / [ -215, 215[
long 4 / [ -231, 231[ 8 / [ -263, 263[ 4 / [ -231, 231[
  • Observe que el rango de representación no es simétrico: en 32 bits -231 es representable, ¡pero 231 no lo es!
  • Los número negativos se representan en complemento de 2.
  • En la plataforma Windows de 64 bits, el tipo long es de 32 bits (no es de 64 bits como en Unix).
  • A partir del estandar C99 existe el tipo long long. Se especifica que debe ser de al menos 8 bytes.
  • Observe que aún cuando los procesadores de PCs y smartphones son de 64 bits, la mayoría de los smartphones funcionan en modo 32 bits, a no ser que tengan al menos 4 GB de memoria RAM.
  • Tampoco se fabrican PCs de 16 bits, pero se venden muchos procesadores para sistemas embebidos que son de 16 bits, con precios insignificantes al lado de sus hermanos de 32 o 64 bits. Por razones de costos nadie colocaría un procesador de 32 o 64 bits para controlar una lavadora.

Advertencia: Cuando se opera con números enteros y se produce un desborde, el runtime de C no genera ningún tipo de error. El resultado simplemente se trunca al tamaño que debe poseer el resultado. El tamaño está especificado por las reglas de inferencia de tipos que veremos más abajo en esta misma página.

Representación de números negativos

Los números negativos se representan en complemento de 2. Esto significa que si se representa un número positivo x en 8 bits, entonces -x se representa negando todos los sus bits (lo que se denomina complemento de 1) y sumando 1. Ejemplos:

valor positivo representación en binario valor negativo complemento de 1 complemento de 2
0 00000000 -0 11111111 000000000
1 00000001 -1 11111110 111111111
2 00000010 -2 11111101 111111110
3 00000011 -3 11111100 111111101
127 01111111 -127 10000000 100000001
-128 100000000
  • Observe que todos los números positivos tienen el primer bit en 0
  • Todos los negativos tienen el primer bit en 1
  • Se puede representar el -128, pero no el 128
  • En C la expresión ~x entrega el complemento de 1 de x (la negación bit a bit)
  • la expresión -x entrega el complemento de 2 de x: ~x + 1 (es decir le cambia el signo)

¿Por qué se escogió esta representación? El número de transistores que tenían los primeros computadores era muy limitado. La operación más compleja era lejos la suma y no quedaban transistores para implementar una resta, multiplicación o división. La gracia de la representación en complemento de 2 es que:

x-y == x+(-y) == x+(~y+1)

Es decir que podemos hacer la resta sumando el complemento de 2. No se necesita un nuevo circuito para hacer la resta. Por otra parte la multiplicación se puede realizar haciendo sumas, y la división haciendo sumas y restas. ¡La economía en transistores es mayúscula!

Tipos enteros sin signo

Además C ofrece enteros sin signo anteponiendo el atributo unsigned al tipo. La siguiente tabla describe los enteros sin signo.

tipo espacio / rango en máquinas de 32 bits espacio / rango en máquinas de 64 bits espacio / rango en máquinas de 16 bits
unsigned char 1 / [ 0, 28[ 1 / [ 0, 28[ 1 / [0, 28[
unsigned short 2 / 0, 216[ 2 / [ 0, 216[ 2 / [0, 216[
unsigned int 4 / [ 0, 232[ 4 / [ 0, 232[ 2 / [0, 216[
unsigned long 4 / [ 0, 232[ 8 / [ 0, 264[ 4 / [0, 232[

Observe que si x e y son sin signo, x-y todavía se calcula como x+~y+1.

Tipos reales

  • Los números reales se representan en notación de punto flotante (en binario).
  • C ofrece en forma estándar 2 tipos reales: float de 4 bytes y double de 8 bytes.
  • El tamaño de estos tipos no varía de una plataforma a la otra.
  • Ambos tipos son con signo: no existen contrapartes sin signo.
  • Interesa el valor absoluto del número más grande que se puede representar, así como el más pequeño.

La siguiente tabla muestra estos límites:

tipo espacio nro. más pequeño nro. más grande dígitos de precisión bits mantisa bits exponente
float 4 1.18e-38 3.40e38 7 24 7
double 8 2.23e-308 1.79e308 15 52 11

Lo más importante que hay que entender al trabajar con números reales es que el número de decimales disponibles es limitado (7 para float y 15 para double) y por lo tanto los números son una aproximación del valor real. Hay que considerar un pequeño error en cada operación que se haga. El error se acumula a medida que aumenta la profundidad del árbol de operaciones con estos números.

Por lo mismo es importante no usar el operador de igualdad con números de punto flotante: siempre usar más bien |x-y|< epsilon. Para entender lo impredecible que puede ser la precisión de los números en punto flotante, cabe señalar que mientras 0.3 es un número representable en forma exacta en base 10, el mismo 0.3 resulta ser un número periódico en base 2 y por lo tanto no puede ser almacenado en forma exacta en el computador.

Observación: los primeros computadores no ofrecían números reales de forma nativa, si no que se implementaban por software. Por lo mismo usar reales era muy lento.

Cuando se opera con números reales y se produce un desborde, el resultado es un not a number (NaN). Un NaN operado con cualquier otro número siempre resulta ser un NaN.

Expresiones y operadores

Java heredó los operadores de C y C++. Para más detalles sobre los operadores de C consultar la wikipedia

Precedencia

La precedencia de los operadores es la que dicta cómo se hace la parentización en caso de ambigüedad. Por ejemplo la expresión a+b*c es ambigua porque se podría interpretar como (a+b)*c o como a+(b*c). Pero como C especifica que * tiene mayor precedencia que + entonces la expresión es equivalente a a+(b*c).

Si ha escrito una expresión y tiene dudas sobre la precedencia consulte la wikipedia o alternativamente use parentización explícita.

Asociatividad

En caso de ambigüedad con operadores de la misma precedencia se usa la regla de asociatividad para determinar la parentización implícita. Por ejemplo la expresión a-b+c es ambigua y los operadores + y - tienen la misma precedencia. La regla de asociatividad para + y - es de izquierda a derecha y por lo tanto la expresión equivalente es (a-b)+c.

Para la mayoría de los operadores la regla de asociatividad es de izquierda a derecha pero hay excepciones: el operador =. La expresión a=b=c es equivalente a a=(b=c). De hecho escribir (a=b)=c es un error.

La otra excepción son los operadores de indirección y post-incremento, ambos con la misma precedencia. La expresión *p++ es ambigua porque se puede referir a (*p)++ o bien *(p++). La regla de asociatividad para estos operadores es de derecha a izquierda de modo que la interpretación correcta es *(p++). El operador unario * es el operador de indirección y será estudiado en la sección de punteros.

Un resumen de los operadores, su precedencia y asociatividad se encuentra en: http://users.dcc.uchile.cl/~lmateu/CC3301/apuntes/LenguajeC/#9

Inferencia de tipos

Las reglas de inferencia de tipos indican cual es el tipo de la expresión: e1 operador e2, a partir de los tipos de e1 y e2. Por ejemplo en el siguiente codigo:

int a= 1;
int b= 2;
double x= a/b;

¿Cuanto vale x? Parece obvio: 0.5. ¡Pero vale 0! La razón es que el tipo de a/b se determina a partir del tipo de sus operandos, no a partir del uso que se de al resultado. Ya que el destino es una variable real, se tendería a pensar que la división debería ser con números reales, pero así no razona el compilador. La regla es que si ambos operandos son enteros, entonces la división es entera y el resultado tendrá 32 bits. Por eso 1/2 es 0.

Otra regla para los operadores binarios es que si un operando es double, el otro operando se convierte implícitamente a double también. Por lo tanto para lograr la divisón real hay que usar un cast para convertir uno de los operandos a double:

double x= (double)a / b;

Aquí (double) es un cast y tiene mayor precedencia que /. El código es equivalente a:

double x= ((double)a) / ((double)b);

Cuidado: ¡el siguiente código también entrega 0!

double x= (double) (a / b);

Consideremos otro ejemplo:

char a= 127;
char b= 1;
int c= a+b;

¿Cuanto vale c? ¿128? ¿o podría ser -128? De acuerdo al texto de más arriba, se podría pensar que la suma debería realizarse en 8 bits con signo. Pero 128 no es representable en 8 bits con signo. De hecho el resultado de la suma sería el valor binario 10000000, ¡que resulta ser -128 en 8 bits con signo! Pero el valor de c sí resulta ser 128. ¿Por qué?

Hay una regla en C que dice que todas las operaciones aritméticas deben considerar al menos el número de bits del tipo int, es decir 32 bits (casi siempre). Por lo tanto la asignación de c es equivalente a:

int c= (int)a + (int)b;

Pero cuidado, el número positivo más grande representable en 32 bits es 2147483647. Considere este código:

int a= 2147483647;
double x= a + 1;

En este caso, la asignación de x es equivalente a:

double x= (double)(a+1);

Por lo tanto a+1 se realiza en 32 bits con signo. El resultado en binario es un 1 seguido de 31 ceros, que corresponde al valor entero -2147483648. Ese es el valor incorrecto que queda almacenado finalmente en x. Y no 2147483648 como debería ser.

Ejercicio: Reescriba la instrucción de asignación cambiando todas las conversiones implícitas a conversiones explícitas.

double x;
char c;
long long ll;
...
int i= ll + c/2 + x*2;

Resumen:

  • El rango de un tipo numérico es el intervalo de números que puede representar.
  • Los tipos numéricos están estrictamente ordenados por su rango:
    • char < short <= int <= long <= long long < float < double
  • Cuando se realiza una operación numéricas entre tipos distintos, el operando de un tipo con rango menor se convierte implícitamente al tipo del otro operando.
  • No se realizan operaciones aritméticas con un tipo de rango inferior a int. Es decir cuando un operando es de tipo char o short se convierte implícitamente al menos a un int.

Mezcla de operandos con y sin signo

A partir de Ansi C se especifica que si un operando es con signo y el otro sin signo, entonces el operando sin signo se convierte implícitamente a un tipo con signo y la operación se realiza con signo. Esto significa que si un operando es unsigned int y se suma con un int, entonces el primero se convierte a int. ¡Cuidado! En esta conversión se podría producir un desborde.

El costo de las operaciones

Dado que C es un lenguaje en donde interesa la eficiencia, resulta importante conocer cual es el costo de las operaciones aritméticas en términos de su latencia. La latencia de una operación es la cantidad de ciclos del reloj del procesador que deben pasar para poder usar el resultado de esa operación.

Símbolo Tipo de datos Latencia (ciclos) Observaciones
+, - int 1
* int 3 en un procesador reciente
/ int 8, 16 o 32 1 ciclo por cada 1, 2 o 4 bits del divisor
+, -, * float/double 3 en un procesador reciente
/ float 8 a 32 ciclos 1 ciclo por cada 1, 2 o 4 bits del divisor
/ double 16 a 64 ciclos 1 ciclo por cada 1, 2 o 4 bits del divisor

Observe que la suma y la resta de enteros son las operaciones más eficientes, seguidas de la suma, resta y multiplicación de números en punto flotante. Por otro lado, la división es lejos la operación más lenta. Esto se debe a que su implementación en circuitos requiere un ciclo por cada 4 bits del divisor, en los procesadores más recientes (Haswell). En los procesadores menos recientes (Sandy Bridge) pueden ser 1 ciclo por cada 2 bits del divisor e incluso 1 ciclo por cada bit del divisor (más lento).

Definición de nuevos tipos

En C se definen nuevos tipos con struct, union, enum y typedef. Estos temas serán abordados más adelante.

tipos.txt · Última modificación: 2020/03/17 20:38 por lmateu