C / TYPES AND REPRESENTATION
Explicit casts and when narrowing loses data
Convert values between C types with explicit casts, predict exactly what a narrowing conversion discards, and check whether a value survives the trip.
What you will learn
- Write (type)expr to convert a value; the variable you read from is unchanged.
- Narrowing to an unsigned type keeps the value modulo 2^N; the high bits are gone.
- double to int truncates toward zero, and is undefined if the whole part doesn't fit.
- Prove a narrowing cast is safe with a round trip: (int)(short)v == v.
Understanding Explicit casts and when narrowing loses data
A cast is a parenthesized type name in front of an expression, and it produces a new value of that type; the object you read from is not modified. C already performs these conversions for you at assignments, function arguments and returns, so writing the cast mostly means stating in the source what would otherwise happen quietly. The mental model that keeps you out of trouble is that a cast asks an arithmetic question, what is this value in that type, rather than telling the compiler to relabel bytes in memory.
That question has no honest answer when the destination cannot represent the value, and such a conversion is called narrowing. For an unsigned destination of N bits the result is defined as the value reduced modulo 2^N, which in practice keeps the low N bits and drops everything above them, so (unsigned char)1000 is 232. For a signed destination the same low bits usually survive, but the top one is now the sign bit, so 200 arrives as -56. Converting a floating value cuts the fraction toward zero, and if the integral part is outside the destination range the behavior is undefined rather than wrapping.
The real hazard of a cast is not technical but social: it tells the compiler you meant it, so the diagnostic about the narrowing disappears while the truncation stays in the program. Write the cast only after you have established that the value fits, by checking against the bounds in limits.h or stdint.h, by clamping, or by converting back and comparing. If the round trip does not reproduce the original value, the cast lost data, and that check works even on the cases where the exact narrowed result is implementation-defined.
<stdio.h>
int main(void)
{
int n = 200;
unsigned char uc = (unsigned char)n; /* 200 is inside 0..255 */
signed char sc = (signed char)n; /* 200 is outside -128..127 */
printf("n = %d, uc = %d, sc = %d\n", n, uc, sc);
int m = 1000;
unsigned char low = (unsigned char)m; /* 1000 mod 256 */
printf("m = %d -> unsigned char %d, widened back %d\n", m, low, (int)low);
double x = -7.9;
printf("(int)%.1f = %d\n", x, (int)x); /* fraction cut toward zero */
long long big = 4294967296LL + 5; /* 2^32 + 5 */
printf("(int)(2^32+5) = %d\n", (int)big); /* only the low 32 bits survive */
return 0;
}
A cast converts a value to another type, and when that type cannot represent the value the conversion discards information silently — the cast records that you accepted the loss, it does not prevent it.
Worked examples
Truncation is not rounding
Shows that a cast to int always cuts toward zero, and how to round on purpose instead.
<stdio.h>
static int round_to_int(double v)
{
return (int)(v >= 0.0 ? v + 0.5 : v - 0.5);
}
int main(void)
{
double v[4] = { 2.7, -2.7, 0.5, -0.5 };
for (int i = 0; i < 4; i++)
printf("%5.1f cast=%3d rounded=%3d\n",
v[i], (int)v[i], round_to_int(v[i]));
return 0;
}
Example explained
Line 1(int)v[i] discards the fractional part, so 2.7 becomes 2 and -2.7 becomes -2; the result never moves away from zero.
Line 2round_to_int adds or subtracts 0.5 before the cast, pushing the value across the next integer boundary so that truncation lands on the nearest integer.
Line 30.5 and -0.5 both cast to 0, which shows the cast has no tie-breaking rule at all; the fraction is simply gone.
Round-trip check before you narrow
A conversion that refuses to store the value when narrowing would lose it.
<stdio.h>
<limits.h>
static int to_short(int value, short *out)
{
short narrowed = (short)value;
if ((int)narrowed != value)
return 0; /* the trip changed the value */
*out = narrowed;
return 1;
}
int main(void)
{
int tests[3] = { 1000, 40000, -40000 };
for (int i = 0; i < 3; i++) {
short s;
if (to_short(tests[i], &s))
printf("%d fits: short = %d\n", tests[i], s);
else
printf("%d does not fit in short (limit %d..%d)\n",
tests[i], SHRT_MIN, SHRT_MAX);
}
return 0;
}
Example explained
Line 1short narrowed = (short)value; performs the conversion, and (int)narrowed != value asks whether widening it back reproduces what went in.
Line 240000 and -40000 fail because they lie outside SHRT_MIN..SHRT_MAX, so the function reports failure and never writes through out.
Line 3The check never inspects what the out-of-range conversion produced, so it stays correct regardless of what a given compiler does with it.
A cast converts the value, not the bytes
Contrasts (unsigned)f with the float's actual bit pattern, obtained by copying its storage.
<stdio.h>
<string.h>
int main(void)
{
float f = 1.5f;
unsigned int bits = 0;
printf("(unsigned)f = %u\n", (unsigned)f);
memcpy(&bits, &f, sizeof bits); /* both are 4 bytes here */
printf("bit pattern of f = 0x%08X\n", bits);
return 0;
}
Example explained
Line 1(unsigned)f asks for the number 1.5 expressed as an unsigned integer, so the fraction is dropped and the result is 1.
Line 2memcpy copies the four bytes the float occupies, so bits holds 0x3FC00000, the encoding people often expect a cast to hand them.
Line 3The two lines disagree because a cast is arithmetic on values; if you want the encoding you must copy the object.
Important notes
The numbers above assume 32-bit int and 16-bit short; the conversion rules are fixed, but the point where a value stops fitting moves with the platform, so test against limits.h rather than memorising 32767.
Converting an out-of-range value to a signed type was implementation-defined before C23, and an implementation was even permitted to raise a signal; C23 requires two's-complement wrapping. Unsigned destinations have always been defined as modulo 2^N.
Common mistakes
Adding a cast purely to silence a conversion warning: (unsigned char)count compiles quietly and turns a count of 1000 into 232 at run time.
Treating (int)x as rounding: (int)-7.9 is -7, not -8, so every negative result comes out one step too large.
Casting too late, as in double avg = (double)(sum / count); the integer division already dropped the fraction, so the cast only turns 3 into 3.0. Write (double)sum / count.
Try it yourself
Change, predict, then run
Write int fits_uchar(int v) that returns 1 only when (int)(unsigned char)v == v, then print fits_uchar(v) next to (int)(unsigned char)v for -1, 0, 200, 255, 256 and 1000 and note which inputs change.
Open the C workspaceCheck your understanding
int n = 130; signed char c = (signed char)n; printf("%d", c); prints -126 on a typical machine. What explains that result?
- The cast clamps the value into signed char's range, and the nearest reachable value is -126.
- 130 is rounded to the nearest multiple of 128, which prints as -126.
- Only the low 8 bits survive, and read as an 8-bit signed type that pattern means 130 - 256.
- The cast reinterprets the int's bytes, so the answer depends on the machine's endianness.
Show answer
Narrowing keeps the value modulo 2^8, and 130 is congruent to -126 once the top of those eight bits acts as the sign, which is why a positive value comes back negative. Clamping is the tempting wrong answer because other systems do saturate; if C clamped, the result would be SCHAR_MAX, 127. Endianness is irrelevant here since the cast converts a value rather than reading storage.