Why this matters
C converts values between types all the time without telling you: when you add a char to an int, when you store a double into an int, when you compare a signed with an unsigned number. Most of these conversions are exactly what you want. A few silently change the value: fractions vanish, large numbers wrap, -1 becomes four billion. Knowing the rules lets you read an expression and know what type it is computed in, which is the difference between a correct average and an average of zero.
Every expression has a type
7 is an int. 7.0 is a double. x + y has a type too, decided by the types of x and y according to fixed rules, and the operation is carried out in that type. The destination of the result plays no part. In
double avg = (a + b + c) / 3;
with int variables, the right side is entirely int arithmetic; only the finished int result is converted to double for storing.
There are three situations where conversions happen implicitly: promotions of small types, the usual arithmetic conversions when an operator has two operands of different types, and assignment (including passing arguments and returning values).
Integer promotion
Types narrower than int (char, signed char, unsigned char, short, unsigned short, and _Bool) are promoted to int whenever they are used in arithmetic. The CPU's integer registers are at least int-sized anyway, so this costs nothing.
#include <stdio.h>
int main(void)
{
unsigned char a = 200;
unsigned char b = 100;
int sum = a + b;
unsigned char small = a + b;
printf("sum = %d, small = %d\n", sum, small);
char c = 'x';
printf("sizeof c = %zu, sizeof(c + 0) = %zu\n", sizeof c, sizeof(c + 0));
return 0;
}
sum = 300, small = 44
sizeof c = 1, sizeof(c + 0) = 4
a + b is computed as an int (300) and does not overflow. Storing it back into an unsigned char reduces it modulo 256 (300 - 256 = 44). And c + 0 is an int, which sizeof confirms.
The usual arithmetic conversions
When a binary operator (+ - * / % < > == ...) gets two operands of different types, the "smaller" one is converted to the type of the "larger" one, and the operation is done in that type. Simplified:
1. If either is long double -> both become long double
2. else if either is double -> both become double
3. else if either is float -> both become float
4. otherwise both are integers: promote both, then convert to the
higher-ranked type: int < long < long long
and if the ranks are equal but one is unsigned, unsigned wins.
Examples, with int i, long l, unsigned u, double d:
| expression | computed as |
|---|---|
i + d | double |
i * l | long |
i + u | unsigned int |
'a' + 1 | int |
l + 1.0f | float |
Rule 4's last clause is the dangerous one: mixing int and unsigned int converts the int to unsigned.
int neg = -1;
unsigned int zero = 0;
printf("%d\n", neg < zero);
main.c:7:24: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
0
-1 becomes 4294967295, which is not less than 0. This bites constantly with size_t, the unsigned type of sizes and lengths: comparing an int index with a size_t length does exactly this. Take the -Wsign-compare warning seriously.
Conversion on assignment
When you store a value into a variable of another type, it is converted to the variable's type:
| from | to | what happens |
|---|---|---|
| any integer | wider integer | value preserved |
| any integer | unsigned type too narrow | reduced modulo 2^n (defined) |
| any integer | signed type too narrow | implementation-defined; gcc keeps the low bits |
| floating | integer | fraction discarded (truncated toward zero); out-of-range is undefined |
| integer | floating | nearest representable value (big integers may lose precision) |
| double | float | rounded; may lose precision |
int i = 3.99; /* 3 */
int j = -3.99; /* -3 */
long long big = 3000000000LL;
int truncated = big; /* -1294967296 on gcc */
Note that double-to-int conversion truncates, it does not round. 3.99 becomes 3. If you want rounding, use round() from <math.h>, or for non-negative values add 0.5 before converting.
None of these produce a warning with -Wall -Wextra. The -Wconversion flag reports them:
warning: conversion from 'double' to 'int' changes value from '3.9900000000000002e+0' to '3' [-Wfloat-conversion]
warning: conversion from 'long long int' to 'int' may change value [-Wconversion]
It is noisy on large codebases but very educational on small programs. Try it.
Casts: explicit conversions
A cast converts a value to a type you name, written as the type in parentheses before an expression:
int total = 7, count = 2;
printf("%f\n", (double)total / count); /* 3.500000 */
printf("%f\n", (double)(total / count)); /* 3.000000 */
printf("%f\n", total / (double)count); /* 3.500000 */
A cast has higher precedence than the arithmetic operators, so (double)total / count means ((double)total) / count: convert total, then divide in double because one operand is double. In the second line the parentheses force the integer division first, and the cast converts the already-truncated 3.
What a cast does underneath depends on the types: int to double is a real conversion instruction (cvtsi2sd on x86-64); int to unsigned int changes nothing in the bits, only how the compiler treats them from then on; long to int simply uses the low 32 bits.
When to cast:
- To get floating-point division from integers, as above.
- To make an intended narrowing explicit, which also silences
-Wconversion:unsigned char low = (unsigned char)value; - To pass the right type to a function such as printf:
printf("%d\n", (int)sizeof x);(better: use%zu).
When not to cast: to make a warning go away without understanding it. A cast tells the compiler "I know what I am doing", and it will believe you.
Common mistakes
- Casting the result instead of an operand:
(double)(a / b)still does integer division. - Expecting conversion to int to round.
(int)2.9is 2.(int)-2.9is -2. - Floating-point error plus truncation.
19.99 * 100is 1998.9999999999998, so(int)(19.99 * 100)is 1998. Round instead of truncating. - Comparing signed and unsigned.
if (i < len)withint iandsize_t lenmisbehaves wheniis negative. Use matching types (usuallysize_tfor indices). - Out-of-range float-to-int conversion, such as
(int)1e10. It is undefined behavior; on x86-64 it typically yields -2147483648, but do not rely on anything.
Recap
- Every expression has a type determined by its operands, not by where the result goes.
- Small integer types are promoted to int in arithmetic.
- Mixed operands are converted to the "larger" type; floating beats integer, and between equal ranks unsigned beats signed, so -1 can compare greater than 0u.
- Assignment converts to the destination type: doubles truncate toward zero, narrow integers lose high bits.
- A cast
(type)exprconverts explicitly and binds tighter than*and/. -Wconversionand-Wsign-comparepoint out conversions that may change values.
Going further: compile some of your earlier exercises with -Wconversion added and read each warning. Decide for each whether the conversion is intentional (add a cast) or a bug (fix the types).