C convierte valores entre tipos constantemente. Algunas de esas conversiones las escribes tú con un cast; la mayoría las realiza el compilador en silencio bajo reglas que no elegiste. Saber cuál es cuál es la diferencia entre "¿por qué mi promedio siempre da 3?" y código que hace lo que dice.
Conversión implícita
Siempre que un valor de un tipo llega a un contexto que espera otro, C lo convierte:
Las conversiones que no pueden perder información (int a double, char a int, short a long) son de ampliación y siempre son seguras. Las conversiones en el otro sentido son de estrechamiento y pueden perder datos: el 3.9 de arriba se volvió 3 sin ningún aviso, a menos que lo pidas con -Wconversion.
Las conversiones aritméticas habituales
Cuando un operador binario tiene operandos de tipos distintos, C los convierte a un tipo común antes de hacer el trabajo. La escalera, de abajo hacia arriba:
- Cualquier cosa más pequeña que
int(char,short,_Bool) se promociona aint. Esto es la promoción de enteros y ocurre primero, siempre. - Si cualquiera de los lados es
long double, el otro pasa along double. - Si no, si cualquiera es
double, el otro pasa adouble. - Si no, si cualquiera es
float, el otro pasa afloat. - Si no, entre tipos enteros gana el de mayor rango, y si los rangos empatan, gana el sin signo.
Esa última regla es la que causa errores de verdad. El resto son intuitivas.
La promoción de enteros es por lo que la aritmética con char no se desborda como podrías esperar, y por lo que guardar el resultado de vuelta en un char sí lo hace.
Casts explícitos
Un cast es el tipo destino entre paréntesis:
(double)x
(int)3.9
(char)65
(unsigned int)n
Se aplica a la expresión que va inmediatamente después, y se asocia muy fuerte, más que *, / o +.
La primera línea divide como enteros (dando 3) y luego convierte el 3 a 3.0: demasiado tarde. La segunda convierte total antes de la división, así que el operador / ve un double y un int, promociona el int y realiza división en punto flotante.
Convertir un operando basta. Las conversiones aritméticas habituales se ocupan del otro.
Arreglar la división entera
Esta es con diferencia la razón más común para escribir un cast en C:
La línea del porcentaje es instructiva: passed / n es 3 / 5, que como enteros es 0, y 0 * 100 es 0. Multiplicar antes de dividir (100 * passed / n) lo arregla incluso sin un cast, porque 300 / 5 es exacto, pero eso solo funciona cuando los números cooperan. El cast es el arreglo fiable.
Truncamiento, no redondeo
Convertir un valor de punto flotante a un entero descarta la fracción. Trunca hacia cero: no redondea.
Si compilas esto en tu propia máquina, recuerda que math.h necesita -lm al enlazar en Linux.
Un peligro más: convertir un valor de punto flotante demasiado grande para el tipo entero es comportamiento indefinido, no una vuelta a la carga. (int)1e20 puede producir cualquier cosa. Comprueba el rango antes de convertir cuando el valor no esté bajo tu control.
char e int
Un char en C es un entero pequeño que guarda un código de carácter. Convertir entre los dos es trabajo de todos los días:
digit - '0' es el modismo estándar para convertir un carácter de dígito en su valor, y funciona porque los diez caracteres de dígito tienen garantizado ser consecutivos. Para las letras, prefiere toupper() y tolower() de ctype.h a la aritmética con + 32: el desplazamiento es un hecho de ASCII, no una garantía de C.
Una trampa emparentada: las funciones de ctype.h como isdigit y toupper toman un int que debe ser o bien EOF o bien representable como unsigned char. Pasar un char a secas que sea negativo (posible, ya que un char simple puede tener signo) es indefinido. Conviértelo: isdigit((unsigned char)c).
La trampa de signed y unsigned
El paso 5 de la escalera de conversiones —el sin signo gana el empate— produce la comparación más sorprendente de C:
-1 se convierte a unsigned int, lo que reinterpreta su patrón de bits como 4.294.967.295. Eso es mayor que 1, así que la comparación es falsa.
La misma conversión hace que los bucles corran para siempre:
/* BUG: i es sin signo, asi que i >= 0 siempre es verdadero. Cuando i es 0, i-- da la vuelta. */
for (size_t i = n - 1; i >= 0; i--) { ... }
Y hace fallar las comprobaciones de longitud:
/* BUG: strlen devuelve size_t (sin signo). Si la cadena es mas corta
que 5, len - 5 da la vuelta a un numero enorme y la prueba pasa. */
if (strlen(s) - 5 > 0) { ... }
Reescríbelo como if (strlen(s) > 5) y la resta nunca ocurre.
Las defensas: mantén los conteos y los índices con un solo tipo de signo en todo el código, compila con -Wsign-compare (incluida en -Wextra), y cuando debas mezclar, convierte explícitamente tras establecer que el valor no puede ser negativo.
Convertir punteros
Los casts también convierten entre tipos de puntero, y aquí conllevan un riesgo real porque cambian cómo se interpreta la memoria, no los bytes en sí.
En una máquina little-endian esto imprime 01 00 00 00. Inspeccionar la representación de un objeto a través de un unsigned char * es una de las pocas conversiones de punteros que el estándar bendice explícitamente.
La mayoría de las demás conversiones de punteros no están bendecidas. Leer un int a través de un float * viola la regla de aliasing estricto y es comportamiento indefinido aunque compile; usa memcpy para reinterpretar bytes.
Dos convenciones que conviene conocer. void * se convierte desde y hacia cualquier tipo de puntero a objeto sin un cast en C, y por eso no deberías convertir el resultado de malloc:
int *arr = malloc(n * sizeof *arr); /* C correcto */
int *arr = (int *)malloc(n * sizeof *arr); /* innecesario; oculta una cabecera olvidada */
El cast es obligatorio en C++, que es por lo que tanto código lo lleva. En C puede esconder el error de olvidar <stdlib.h>.
Y printf("%p", ...) espera un void *, así que los argumentos de tipo puntero ahí sí necesitan un cast de verdad: printf("%p", (void *)p).
Cuándo un cast es la respuesta equivocada
Un cast silencia al compilador. A veces el compilador tenía razón.
long big = 5000000000L;
int small = (int)big; /* el cast esconde una perdida real de datos */
Si el valor de verdad cabe, el cast documenta que lo comprobaste. Si puede que no quepa, el cast ha convertido un aviso en una respuesta equivocada silenciosa. Antes de escribir uno, pregúntate si el arreglo es cambiar el tipo de una variable: double en vez de int, size_t en vez de int, long long en vez de long. Un cast es la herramienta correcta sobre todo cuando dos tipos correctos deben encontrarse para una operación, como en (double)sum / count.
Preguntas frecuentes
¿Cómo se hace un cast en C?
Pon el tipo destino entre paréntesis delante del valor: (double)x, (int)3.9, (char)65. El cast se aplica a la expresión que va inmediatamente después, así que (double)a / b convierte a primero y luego divide, mientras que (double)(a / b) divide como enteros y convierte el resultado.
¿Cómo convierto un int a float en C?
La asignación lo hace de forma implícita: double d = 5; guarda 5.0. Dentro de una expresión a menudo necesitas un cast explícito: (double)total / count fuerza la división en punto flotante en lugar de la división entera.
¿Qué pasa cuando conviertes un float a int en C?
La parte fraccionaria se descarta: se trunca hacia cero, nunca se redondea. (int)3.9 es 3 y (int)-3.9 es -3. Para redondear, suma 0.5 antes del cast en los positivos, o usa round(), floor() o ceil() de math.h.
¿Por qué comparar un int con signo y uno sin signo da la respuesta equivocada?
Las conversiones aritméticas habituales de C convierten el valor con signo a sin signo, así que -1 < 1u es falso: -1 se vuelve un número positivo enorme. Mantén los conteos y los tamaños con un solo tipo de signo, o convierte explícitamente tras comprobar que el valor no puede ser negativo.