Type Conversion & Casting
C constantly converts values between types behind the scenes — sometimes safely, sometimes not — and knowing when a conversion is implicit versus when you must ask for one explicitly avoids a whole category of silent, hard-to-spot bugs.
Every value C works with has a type, and C is far more willing than most languages to move a value from one type into another — sometimes silently, on its own initiative, and sometimes only because you told it to with an explicit cast. Almost every mysterious C bug that involves a number 'coming out wrong' — an average that's always a whole number, a comparison that goes the wrong way, a character that prints as gibberish — traces back to a conversion nobody was watching for. This page works through both kinds of conversion end to end: what implicit conversion does on its own, what an explicit cast lets you force, the specific rules for moving between integer types, floating types, and signed/unsigned pairs, and the handful of places — division, assignment, function calls, return statements — where the exact same conversion rules keep showing up in slightly different clothes.
What implicit type conversion is
Implicit conversion is C changing a value's type on its own, without you writing anything that requests it, because an expression or an assignment needs both sides to agree on a type before it can proceed. You've almost certainly triggered this already without naming it.
x is a plain int holding 10. Assigning it into y, a double, doesn't require any special syntax — C notices the mismatch and converts the value for you before storing it.
x = 10 stores an ordinary integer value.
double y = x; needs an int's value stored into a double variable — C automatically converts 10 into 10.0 as part of the assignment.
Printing x still shows 10 (it was never touched), while y prints as 10.000000 — a genuinely different, wider representation of the same number.
int x = 10;
double y = x;
printf("x: %d\n", x);
printf("y: %f\n", y);What an explicit cast is
An explicit cast is you, the programmer, writing the target type in parentheses directly in front of a value, telling C exactly which conversion to perform right there — rather than waiting for C to decide one is needed on its own.
(int)price is the cast — it produces the value 19 from price's 19.99, and that 19 is what actually gets stored into dollars.
The parentheses-and-typename syntax, (type)expression, is called a cast expression. It can be applied to almost any scalar value, and unlike implicit conversion it can move in directions C would never take on its own, including ones that deliberately lose information.
double price = 19.99;
int dollars = (int)price;
printf("price: %.2f\n", price);
printf("dollars: %d\n", dollars);How casting and conversion relate to each other
"Conversion" is the umbrella term for any change of a value's type, however it happens. "Casting" is just the name for one specific way of causing a conversion: writing it out yourself, by hand, with a cast expression. Every cast is a conversion, but plenty of conversions — like the int-to-double example above — happen with no cast in sight, because C's own rules already required them.
Keeping the two words straight matters mostly because 'conversion' covers a much wider net: it includes things that happen automatically during ordinary arithmetic, during assignment, when passing arguments, and when returning values — none of which involve you writing (type) anywhere.
Why C needs to convert values at all
Different types store the same kind of number completely differently in memory — an int and a double representing '3' don't share a bit pattern. Whenever an operator like + is asked to combine two operands of different types, C has to pick a common type to actually perform the arithmetic in, and it does that by converting one operand up to match the other before the operation runs.
quantity is an int and unitPrice is a double — mixing them in + forces a conversion before the addition can even happen.
quantity holds 3 (int) and unitPrice holds 2.5 (double) — two different representations meeting inside one expression.
Before + runs, C converts the lower-ranked operand, quantity, up to double, producing 3.0 — unitPrice itself is left alone.
3.0 + 2.5 is now genuine floating-point addition, giving 5.5.
total, already declared double, stores that result exactly, and %.2f prints 5.50.
int quantity = 3;
double unitPrice = 2.5;
double total = quantity + unitPrice;
printf("%.2f\n", total);Implicit conversion at work: two everyday shapes
The int-to-double case above is one common shape implicit conversion takes; converting a character to an int is another, and it comes up constantly because a char in C is, underneath, just a small integer.
Storing a char into an int doesn't need a cast — grade's underlying numeric value (66 for 'B' on an ASCII system) is what gets copied into numericCode.
char grade = 'B';
int numericCode = grade;
printf("grade: %c\n", grade);
printf("numeric code: %d\n", numericCode);Widening conversions
A widening conversion moves a value into a type that can represent a broader range of values than the original — int to double is the standard example, since a double can represent every value an int can and then a great deal more.
largest and asDouble don't print the same number, even though this looks like an ordinary, safe widening conversion.
largest holds the biggest value a 64-bit long long can represent.
Converting it to double doesn't lose range — double's range is far larger — but double can only represent integers exactly up to about 2^53 before it starts rounding to the nearest value it can actually store.
largest is well past that point, so asDouble ends up holding a nearby double value, not the exact original integer — the printed numbers differ.
"Widening never loses information" is a useful rule of thumb, not a guarantee the C standard actually makes. It holds reliably for the common cases (int to double, float to double, short to int), but converting a very large integer type into a floating type is a real counterexample — the destination type is wider in range, yet the exact value can still be rounded away.
long long largest = 9223372036854775807LL;
double asDouble = largest;
printf("original: %lld\n", largest);
printf("as double: %.0f\n", asDouble);Narrowing conversions
A narrowing conversion moves a value into a type that cannot represent everything the original type could — double to int is the classic case, since an int has no way to store a fractional part at all.
The .95 doesn't round anywhere — it's simply cut off, because int has no fractional part to put it in.
double gpa = 3.95;
int whole = (int)gpa;
printf("gpa: %.2f\n", gpa);
printf("whole: %d\n", whole);Converting an integer to a floating-point type
Converting an ordinary integer to float or double is usually exact — most int values a program deals with day to day fit into a double's roughly 15-to-17 significant decimal digits without any rounding at all.
7 becomes 7.0 exactly — small, everyday integers convert to floating types without any surprises.
The precision caveat from the widening section above still applies here: it's specifically very large integer values (ones needing more significant digits than the floating type's mantissa can hold) where int-to-float or long-to-double conversion stops being exact.
int itemCount = 7;
float asFloat = itemCount;
printf("count: %d\n", itemCount);
printf("float: %.1f\n", asFloat);Converting a floating-point value to an integer
Converting float or double to an integer type truncates toward zero — it does not round to the nearest whole number. The fractional part is simply cut off, regardless of whether it was closer to the number above or below.
25.9 becomes 25, and -25.9 becomes -25 — both times, truncation moves the value toward zero, not downward. If this were rounding, -25.9 would become -26; if it were 'always round down' (floor), it would also become -26. Truncation gives -25 instead, because -25 is the value closer to zero.
If actual rounding is what you want, reach for round() (or floor()/ceil() for always-down or always-up behavior) from <math.h> and apply the cast to its result — a bare cast to int never rounds, no matter which direction the fractional part was closer to.
printf("%d\n", (int)25.9);
printf("%d\n", (int)-25.9);When float-to-integer conversion becomes dangerous
Truncation toward zero is well-defined C behavior as long as the truncated value actually fits inside the destination integer type. When it doesn't — because the floating-point value is far larger (or far more negative) than the integer type can represent — the conversion isn't defined at all.
1e20 is vastly larger than INT_MAX — there is no meaningful int value this could sensibly become.
Converting a floating-point value that's outside the destination integer type's range is undefined behavior in C — not simply 'produces a wrong number.' The result isn't guaranteed to be any particular value, a saturated maximum, zero, or anything predictable at all, and different compilers or optimization settings can legitimately produce different outcomes. Range-check a value before casting it down whenever it might plausibly be out of bounds, rather than trusting the cast to fail gracefully.
double huge = 1e20;
int bad = (int)huge; /* huge is nowhere close to fitting in an int */Converting between different integer types
Moving a value between two integer types — int to long, long long to int, and so on — follows the same widening/narrowing split as floating types: moving to a type with a larger range is safe, and moving to a smaller one can lose data if the value doesn't fit.
small widens into bigger with no cast needed and no risk. hugeValue, at five billion, doesn't fit in a typical 32-bit int at all, so truncatedValue ends up holding some other, unrelated-looking number.
Don't assume 'int is 4 bytes' or 'long is 8 bytes' as universal facts — the C standard only guarantees minimum ranges (int at least 16 bits, long at least 32), and the actual sizes genuinely differ across platforms and compilers. Code that depends on an exact width should use the fixed-width types from <stdint.h> — int32_t, uint32_t, int64_t, and so on — rather than assuming plain int or long always means the same number of bytes everywhere.
int small = 42;
long bigger = small; /* widening: always safe */
long long hugeValue = 5000000000LL;
int truncatedValue = (int)hugeValue; /* narrowing: may lose data */
printf("bigger: %ld\n", bigger);
printf("truncatedValue: %d\n", truncatedValue);Converting into a smaller integer type
When an integer value doesn't fit into a smaller unsigned destination type, C doesn't clamp it or error out — it reduces the value modulo 2^N, where N is the number of bits the destination type has.
unsigned char is a single byte (8 bits) on virtually every system in practical use, so it can only hold values 0 through 255. 300 reduced modulo 256 is 44, which is exactly what small ends up holding.
It's tempting to describe this as 'just keeps the lower 8 bits,' and that description happens to match on almost every real system today — but it's only literally guaranteed when CHAR_BIT (the number of bits in a byte on that platform) is exactly 8, which the C standard doesn't strictly require. The rule that's actually portable to remember is the modulo-2^N one, not a specific bit count.
int value = 300;
unsigned char small = (unsigned char)value;
printf("value: %d\n", value);
printf("small: %d\n", small);Converting between signed and unsigned integers
A negative signed value converted to an unsigned type of the same width follows the exact same modulo rule as the previous section, just scaled up to a wider type — the result is the value that's congruent to the original modulo 2^N.
For a typical 32-bit unsigned int, -1 reduced modulo 2^32 is 4294967295 — the largest value that type can hold — which is exactly what prints.
int negative = -1;
unsigned int asUnsigned = (unsigned int)negative;
printf("asUnsigned: %u\n", asUnsigned);The classic signed-versus-unsigned comparison surprise
This is one of the most reliable interview and code-review traps in C, precisely because the code looks completely ordinary. When a signed value and an unsigned value of the same rank meet in a comparison, C's rules convert the signed operand to unsigned before comparing — not the other way around — and a negative number reinterpreted as unsigned turns into a huge positive one.
It looks obvious that -1 should be less than 1 — and mathematically it is — but that's not the comparison C actually performs here.
a holds -1 (signed int); b holds 1 (unsigned int) — a signed and an unsigned operand of the same rank meeting in one comparison.
The usual arithmetic conversions require the signed operand to be converted to unsigned before < can run — so a's -1 is reinterpreted as an unsigned int, becoming the largest value that type can hold (about 4.29 billion on a typical system), not -1.
That huge reinterpreted value is nowhere close to being less than 1, so a < b evaluates to false.
The else branch runs, printing that a is NOT less than b — the exact opposite of what the two numbers seem to say mathematically.
Whenever a signed value might realistically be negative, don't compare it directly against an unsigned value (including size_t, which is what sizeof and most array-length functions return) — cast the signed value to a wide enough signed type first, or check it against zero as a completely separate condition, before letting an unsigned comparison silently reinterpret it.
int a = -1;
unsigned int b = 1;
if (a < b) {
printf("a is less than b\n");
} else {
printf("a is NOT less than b\n"); /* this branch actually runs */
}Character conversion and implementation-defined values
A char holds a small integer value that's interpreted as a character using whatever character encoding the platform uses (ASCII, in practice, almost everywhere you'll run into). Converting a char to int simply reveals that underlying numeric code.
code ends up holding 65 on an ASCII-based system — the numeric value that was already sitting inside letter, just viewed through an int instead of a char.
Whether plain char behaves like a signed or an unsigned type is implementation-defined by the C standard — different compilers (and even the same compiler on different platforms) can choose differently. Code that needs a char to reliably hold negative values, or that needs the full 0-255 range, should say so explicitly with signed char or unsigned char rather than relying on plain char's default.
char letter = 'A';
int code = letter;
printf("letter: %c\n", letter);
printf("code: %d\n", code);Integer promotions
Before a char or a short takes part in most expressions, C promotes it to int (or occasionally unsigned int) first. That means arithmetic that looks like it's happening between two chars is actually being carried out between two ints under the hood.
a and b are both promoted to int before + ever runs, so this addition is genuinely int + int, not char + char — result correctly holds 30.
char a = 10;
char b = 20;
int result = a + b;Why integer promotions exist
Promotions exist so that small integer types behave predictably and consistently once they're actually used — in arithmetic, in comparisons, in bitwise operations, and when passed to functions that accept a variable number of arguments. Without a fixed promotion rule, the exact behavior of arithmetic on chars and shorts would depend on how narrow those types happened to be on a given platform.
Variadic functions rely on this directly: printf's %d format specifier expects an argument that's already int-sized. Passing a char or a short argument still works correctly only because integer promotions convert it to int automatically as part of the function call, before printf ever sees it.
The usual arithmetic conversions
Once integer promotions have run, if the two operands of an arithmetic operator still have different types, C applies a further, fixed set of rules — the usual arithmetic conversions — to bring them to a common type before the operation happens. Roughly, the ranking runs from int and unsigned int, up through long and unsigned long, up to float, then double, then long double, with the lower-ranked operand always converted up to match the higher one.
count is int and 2.0f is float; the usual arithmetic conversions convert count up to float before the division runs, giving a genuine floating-point result of 2.00, not integer division.
int count = 4;
float average = count / 2.0f;
printf("%.2f\n", average);Type conversion during a plain assignment
Assignment is its own conversion context, separate from arithmetic — the value on the right side of = is converted to match the type of the variable on the left, whether that means widening or narrowing.
truncated ends up holding 99 — the .9 is discarded exactly as a cast would discard it, just without any (int) written anywhere. asDouble ends up holding 5.0, widened without incident.
double preciseValue = 99.9;
int truncated = preciseValue; /* narrows on assignment, typically warns */
int wholeNumber = 5;
double asDouble = wholeNumber; /* widens on assignment, no warning */
printf("truncated: %d\n", truncated);
printf("asDouble: %.1f\n", asDouble);Writing an explicit cast
A cast is written as the destination type in parentheses, placed directly before the value being converted: (type)expression. The single most common place you'll reach for one is forcing a division to happen in floating-point instead of as integer division.
(double)a converts a's value to 7.0 before the division runs at all — that's the entire trick.
int a = 7;
int b = 2;
double result = (double)a / b;
printf("%.2f\n", result);Why casting matters specifically for division
If both operands of / are integers, C performs integer division — computing the exact mathematical quotient and then throwing away anything after the decimal point, with no warning at all. Storing that already-truncated result into a double afterward doesn't bring the lost fraction back.
a / b runs first, entirely as integer division, giving 3 — only afterward does that 3 get converted to 3.0 to fit into wrong.
This is the single most common type-conversion mistake in everyday C code: assigning the result of int / int into a double variable does not retroactively recover the decimal part, because the division already happened — and already truncated — before the assignment's own conversion ever gets a turn to run. The fix has to happen before the division, not after it.
int a = 7;
int b = 2;
double wrong = a / b;
printf("%.2f\n", wrong); /* prints 3.00, not 3.50 */Getting real floating-point division
The fix is to cast one of the operands to a floating type before / runs, not the finished result afterward. Because the cast operator binds more tightly than /, (double)a / b applies the cast to a alone first.
(double)a produces 7.0 before / ever runs. With one operand now a double, the usual arithmetic conversions automatically promote b to 2.0 as well, so the division that follows is genuine floating-point division from the start.
int a = 7;
int b = 2;
double result = (double)a / b;
printf("%.2f\n", result); /* 3.50 */A cast never changes the variable it's applied to
A cast produces a new value — it does not reach back and modify the storage of the variable it was applied to. (double)a evaluates to a double-typed value derived from a; a itself is left completely untouched.
a still holds 5, exactly as before — (double)a only ever produced a separate, temporary value that got copied into d.
int a = 5;
double d = (double)a;
printf("a: %d\n", a);
printf("d: %.1f\n", d);Where you place the cast changes what it applies to
A cast applies to whatever expression comes immediately after it — parentheses decide exactly how much of a larger expression that is. (double)(a / b) and (double)a / b look almost identical but tell C to do genuinely different work.
In the first line, a / b is computed completely (as integer division, if both are int) before the cast ever touches it. In the second, the cast lands on a by itself, so the division that comes after it is already floating-point.
(double)(a / b) /* casts the finished result of a / b */
(double)a / b /* casts a alone, before the division runs */A worked example: same numbers, two different answers
Putting real numbers through both versions makes the difference concrete rather than abstract.
Same a, same b, same-looking cast — two different results, purely from where the parentheses sit.
a / b runs first inside wrong's expression: 10 / 4 as integer division gives 2, discarding the .5. Casting that finished 2 to double just produces 2.0.
right's expression casts a alone first: (double)a is 10.0. With one operand now floating-point, b is promoted to 4.0 automatically, and 10.0 / 4.0 gives the full 2.5.
wrong prints 2.00; right prints 2.50 — the only difference between the two lines is which sub-expression the cast actually reached.
int a = 10;
int b = 4;
double wrong = (double)(a / b);
double right = (double)a / b;
printf("wrong: %.2f\n", wrong);
printf("right: %.2f\n", right);Explicit casts and implicit conversions, side by side
The same short program can show both kinds of conversion happening right next to each other — one that C performs on its own, and one that had to be requested by hand.
implicit needs no cast at all, because assigning an int into a double variable is a case C already handles automatically. Getting floating-point division out of two int variables, by contrast, is not something C will do on its own — that one genuinely needs an explicit cast written by hand.
int a = 10;
int b = 4;
double implicit = a; /* C converts on its own: assignment context */
double explicitCast = (double)a / b; /* you request the conversion: expression context */
printf("implicit: %.1f\n", implicit);
printf("explicitCast: %.2f\n", explicitCast);How a cast can hide a real bug
A cast doesn't just perform a conversion — it also tells the compiler, and anyone reading the code, 'yes, I meant this, stop warning me about it.' That's genuinely useful when the loss really is intentional, and genuinely dangerous when a cast gets added purely to make an annoying warning disappear.
The cast doesn't round — it truncates, same as always — so rounded silently becomes 99 even though 99.99 is a hair away from 100.
Adding a cast purely to quiet a narrowing-conversion warning, without first understanding what data the conversion actually throws away, doesn't fix anything — it just silences the one signal that was telling you data loss might be happening. Only cast once you've actually decided the loss is fine; if it isn't, the real fix is rounding (round() from <math.h>) or restructuring the calculation, not a cast.
double price = 99.99;
int rounded = (int)price;
printf("price: %.2f\n", price);
printf("rounded: %d\n", rounded); /* 99, not 100 */Constants get converted too
A literal written directly in code has a type of its own, just like a variable does, and it's just as subject to conversion when it's assigned or used somewhere that expects a different type.
10.5 is itself a double constant. Assigning it into x, an int, triggers the exact same narrowing conversion a variable would go through — the .5 is discarded, and x ends up holding 10.
int x = 10.5;
printf("%d\n", x);Integer constants have types too
A plain integer literal like 10 defaults to type int, but a suffix on the literal can request a different type outright, which matters once a value is too large for int or needs to be treated as unsigned from the start.
The letter suffixes are read as U for unsigned and L (or LL) for long (or long long), and they can be combined, as in 10ULL.
Writing a very large literal with no suffix at all, like 10000000000, doesn't automatically fail — C picks the smallest type from a standard list (int, then long, then long long, and their unsigned variants) that can actually hold the value. Relying on that fallback instead of writing the suffix you actually intend can make code behave differently on a platform where long happens to be a different size.
10 int
10U unsigned int
10L long
10UL unsigned long
10LL long long
10ULL unsigned long longFloating-point literal types
A floating-point literal also has a default type — double — unless a suffix says otherwise.
f (or F) requests float, and l (or L) requests long double; with no suffix at all, a floating literal is always double.
Mixing a float literal into an expression with a double variable still just triggers an ordinary widening conversion (float up to double) via the usual arithmetic conversions — the suffix changes the literal's own type, not how it behaves once it meets another type in an expression.
3.14 double
3.14f float
3.14L long doubleConversion in function arguments
When an argument's type doesn't match the parameter's declared type, C converts the argument to match the parameter before the function runs — the same implicit-conversion rules that apply to assignment apply here too.
5 is an int literal, but showValue expects a double — C converts it to 5.0 automatically as part of making the call, exactly as if 5 had been assigned into a double variable first.
void showValue(double d) {
printf("%f\n", d);
}
int main(void) {
showValue(5);
return 0;
}Conversion in return values
A return statement's expression is converted to match the function's declared return type, the same way an assignment converts its right-hand side — including the same trap where a computation that already truncated can't be fixed by the conversion that happens afterward.
total is an int, but the function is declared to return double — the value 9 is widened to 9.0 as part of returning it, exactly like assigning it into a double variable would.
As with assignment, this conversion happens to the value the return expression already produced — if that expression itself was int / int, it has already truncated before the return type gets any say, so a function declared to return double doesn't retroactively fix an integer division performed inside it.
double getAverage(void) {
int total = 9;
return total; /* total is converted to 9.0 to match the double return type */
}Boolean context: what counts as true or false
C treats the integer 0 (or, for any type, a value equal to zero) as false in any condition — an if, a while, a for's test — and treats any nonzero value at all as true, regardless of what type that value actually is.
flag is 5, not literally 1 — but any nonzero value satisfies an if condition, so this branch runs.
C99 added a real Boolean type, _Bool, along with the friendlier bool, true, and false names available by including <stdbool.h> — but underneath, a bool is still just a small integer restricted to the values 0 and 1, following the exact same zero-is-false rule as everything else.
int flag = 5;
if (flag) {
printf("treated as true\n");
}#include <stdbool.h>
bool isValid = true;
if (isValid) {
printf("valid\n");
}A brief word on casts and pointers
Pointers can be cast too — (int*)somePointer, for instance — but pointer casts follow a genuinely different, stricter set of rules than the numeric casts covered on this page, and dereferencing a pointer through the wrong type is a far more serious hazard than an ordinary narrowing cast between numbers.
This page deliberately doesn't go deep on pointer casts — they get their proper, careful treatment in the Pointers topic, where the memory model needed to reason about them safely is already built up. For now, it's enough to know that pointer casts are their own separate topic, not just "casting, but for pointers."
A cast doesn't always mean "convert the bits"
Casting a double to an int is a value conversion: C looks at the double's mathematical value and computes a brand-new integer bit pattern that represents that same value as closely as an int can, truncating the fraction along the way. It is not simply taking the double's raw 8 bytes and relabeling them as an int.
i ends up holding 3 — a small, sensible integer. If (int)d instead reinterpreted d's raw bit pattern as an int (the way a pointer cast can reinterpret memory), the result would be some enormous, meaningless number derived from IEEE-754's bit layout for 3.9 — not 3 at all.
double d = 3.9;
int i = (int)d;
printf("%d\n", i);Conversion versus reinterpretation as two mental models
It's worth keeping two genuinely different models apart, since C uses the word 'cast' for both. An ordinary numeric cast — int to double, double to int, int to char — is a value conversion: C computes a new representation of the same underlying number in the destination type. A pointer cast, by contrast, is reinterpretation: it tells the compiler to treat the exact same bytes already sitting in memory as a different type, without computing anything at all.
Keeping these separate now avoids a specific confusion later: once bit-manipulation tricks and pointer-based type punning show up, it can look like just another cast doing 'the same thing' this page covers — it isn't, and the difference between converting a value and reinterpreting its bits is exactly what makes the pointer version worth its own careful treatment.
A complete example putting it together
One short program showing plain integer division and cast-forced floating-point division side by side, on identical inputs.
Running this program prints "integer division: 3" followed by "floating-point division: 3.40" — the same total and count, the same-looking division, and two different answers depending entirely on whether a cast was involved.
total / count computes as integer division because both operands are int, giving 3 with the .4 discarded — that's truncated.
(double)total / count casts total to 17.0 first, promotes count to 5.0 automatically, and divides as floating-point, giving the exact 3.4 — that's precise.
Both variables came from the exact same total and count; the only difference in the whole program is where the cast was placed.
#include <stdio.h>
int main(void) {
int total = 17;
int count = 5;
int truncated = total / count;
double precise = (double)total / count;
printf("integer division: %d\n", truncated);
printf("floating-point division: %.2f\n", precise);
return 0;
}A checklist for reading a mixed-type expression
When an expression mixes several types and it isn't obvious what the result will be, working through the same fixed sequence of questions every time removes the guesswork.
Running the (double)a / b example from earlier through this list is exactly why it works: a is cast to double explicitly (step 3 happens early, by hand), b is then promoted to double automatically (step 3 again, this time automatic), the division runs in floating-point (step 4), the result is double (step 5), and it's stored straight into a double variable with no further conversion needed (step 6).
1. Identify every operand's actual type.
2. Check whether integer promotions apply (char/short -> int).
3. Apply the usual arithmetic conversions to line up any remaining
mismatched operand types.
4. Perform the operation using those already-converted types.
5. Determine the resulting expression's own type.
6. Check whether one more conversion happens on top of that result
-- at assignment, at a return, or when passed as a function argument.Best practices for converting types deliberately
Nearly every conversion-related bug in C traces back to a conversion that happened without anyone deciding it should — either C did something automatic that wasn't expected, or a cast got added out of habit rather than intent. Treating every conversion as a small decision, rather than background noise, is the actual skill this whole page has been building toward.
Cast on purpose, for a reason you could state out loud, not because the compiler complained. Never add a cast solely to make a warning go away without first understanding what it was warning about. Take real care whenever a signed value and an unsigned value meet, especially in a comparison. Know exactly which sub-expression a cast is attached to — (double)a / b and (double)(a / b) are not the same line. And remember that any conversion, implicit or explicit, is capable of discarding information; the only question is whether that was the intended outcome or an accident nobody noticed.
Quick reference: conversion types at a glance
Type conversion is changing a value from one data type to another. C does many conversions implicitly (automatically) when types are mixed in an expression, and lets you force an explicit conversion using a cast, written as (type)value.
Different types store data completely differently in memory — converting between them safely (or intentionally choosing to lose precision) requires understanding exactly what the conversion does, rather than hoping it 'just works'.
Mixing int and float in a calculation, converting user input text into a number, or truncating a double down to an int to discard its decimal part are all everyday examples across virtually any C program.
Converting types is like pouring liquid from a large jug into a small cup — it might fit fine (int to double, no data lost), or it might overflow and spill (double to int, the decimal part is simply cut off and lost forever).
Forcing floating-point division with a cast Without the explicit cast, integer division would silently truncate the result to 3 — the cast forces the division to happen in floating-point.
(double)total explicitly converts the int value 7 into the double value 7.0 — this is an explicit cast, requested directly by the programmer.
Because one operand of the / is now a double, C's implicit conversion rules promote the other operand (count) to double as well before the division happens.
7.0 / 2.0 is now floating-point division, giving the precise result 3.5, instead of integer division's truncated 3.
average, itself declared as double, stores the full 3.5 without any further loss.
%.2f in printf formats the double to exactly two decimal places, printing 3.50.
Narrowing loses data silently Two narrowing conversions in a row — double to int truncates the decimal, and an out-of-range int to char wraps around unpredictably.
(int)price truncates 19.99 toward zero, discarding the .99 entirely — whole becomes 19, not 20 (no rounding happens here).
(char)321 tries to fit 321 into a single byte (a char's typical range is roughly -128 to 127 or 0 to 255); 321 doesn't fit, so the extra high bits are simply dropped, producing an implementation-defined, easy-to-misread value.
Printing letter back out as an int reveals what actually got stored — usually not 321, demonstrating exactly how a narrowing cast can silently corrupt a value instead of erroring.
double avg = (double)(total / count); // total=7, count=2 double avg = (double)total / count;
Casting the result AFTER the division is too late — total / count has already truncated to 3 as integer division, and (double)3 is just 3.0. The cast must be applied to an operand BEFORE the division happens.
double d = 3.99; int rounded = (int)d; // expecting 4 A cast to int truncates toward zero — (int)3.99 gives 3, not 4. Actual rounding requires round() from <math.h> before the cast.
Implicit conversion (also called 'automatic' or 'coercion') happens whenever an expression mixes types — C converts the 'smaller' or 'lower-ranked' type up to match the 'larger' one (int to double, char to int) following a fixed usual arithmetic conversions rule set.
Explicit conversion (casting) is when you write (type)expression yourself, overriding what would otherwise happen, or converting in a direction C would never do automatically (like double to int, which loses information).
Narrowing conversions (double to int, int to char) can silently lose data — a double like 3.99 cast to int becomes 3 (truncated, not rounded), and an int too large for a char wraps around unpredictably.
Converting a numeric character digit like '7' to its actual integer value 7 requires subtracting '0' (since '7' - '0' relies on ASCII's digits being sequential), not just casting it directly.
Expecting (double)total / count to round instead of just converting one operand — the division itself is still exact floating-point math; rounding a final result to the nearest whole number requires functions like round() from <math.h>, a separate step.
Casting away a compiler warning without understanding why it appeared — a narrowing-conversion warning is the compiler telling you data loss is likely, and silencing it with a cast doesn't fix the underlying issue, it just tells the compiler you 'meant it'.
Forgetting that casting a pointer type doesn't convert the underlying data, only how it's interpreted — this becomes especially important once you reach Pointers and Structures later in this track.
Assuming (int)someDouble rounds to the nearest integer — it truncates toward zero instead, so (int)3.99 is 3 and (int)-3.99 is -3, not -4.
Casting at least one operand to double before dividing whenever you need a precise, non-truncated result from two integer values.
Using explicit casts as a form of documentation — (int)someDouble in code makes it unmistakably clear to a reader that truncation is intentional, not a bug.
Reaching for round(), floor() or ceil() from <math.h> when you specifically need rounding behavior rather than truncation.
The 'usual arithmetic conversions' hierarchy When two operands of different types meet in an arithmetic expression, C converts the 'lower-ranked' one up to match the 'higher-ranked' one, following a strict hierarchy: roughly char/short → int → unsigned int → long → unsigned long → float → double → long double. This hierarchy is exactly why mixing signed and unsigned integers of the same rank can produce surprising results — the signed value gets silently reinterpreted as unsigned.
Casting pointers reinterprets memory, it doesn't convert values
Casting an int* to a float* doesn't convert the integer bit pattern into an equivalent float — it just tells the compiler 'treat these same 4 bytes as a float from now on', producing a nonsensical value unless you specifically intend that kind of raw reinterpretation (a technique sometimes used deliberately in low-level bit-manipulation code, covered later in this track).
What's the difference between implicit and explicit type conversion?
Implicit conversion happens automatically whenever C's rules require it (mixing types in an expression). Explicit conversion is a cast the programmer writes deliberately, like (int)someDouble, overriding or clarifying what conversion should happen.
Does casting a double to an int round or truncate?
It truncates toward zero — the decimal part is simply discarded, so 3.99 becomes 3 and -3.99 becomes -3, not -4.
Why does (double)total / count differ from (double)(total / count)?
The first casts total alone before the division happens, so the whole division is done in floating-point. The second computes total / count as integer division first (truncating), and only converts that already-truncated integer result to double afterward — the precision is already lost by the time the cast happens.
What's the actual difference between type conversion and type casting?
Conversion is the general term for any change of a value's type, whether or not anyone asked for it — it covers automatic cases like mixed arithmetic and assignment. Casting is specifically writing (type)expression yourself to request a conversion by hand. Every cast is a conversion, but most conversions in a typical program happen without any cast at all.
Why doesn't (double)a change what's stored in the variable a?
A cast produces a new, separate value derived from a — it doesn't reach back into a's own storage and rewrite it. a keeps whatever value it already held; only the temporary result of the cast expression is different, and that result only matters if something (like an assignment) actually captures it.
How can adding a cast actually hide a bug instead of fixing one?
A cast tells the compiler 'I meant this, stop warning me' — if it's added just to silence a narrowing-conversion warning without first understanding what data the conversion throws away, the underlying loss still happens, it just no longer produces any visible signal that something might be wrong.
What's the difference between a widening and a narrowing conversion?
A widening conversion moves a value into a type that can represent a broader range (int to double), and is usually safe. A narrowing conversion moves it into a type that can represent less (double to int, or a large int into a smaller integer type), and can silently discard part of the value. Widening usually preserves the value exactly, though very large integers converted to floating types are a real exception to that.
Why are pointer casts treated as a separate topic instead of just another kind of numeric cast?
A numeric cast (int to double, double to int) is a value conversion — C computes a new representation of the same number. A pointer cast is reinterpretation — it tells the compiler to treat the same bytes already in memory as a different type, without computing anything at all. Those are two different mental models, and pointer casts come with their own stricter rules that depend on the memory model covered properly in the Pointers topic.
What does this print?
3.50 — (double)a converts a to 7.0 before the division runs, so one operand is already floating-point; b is then automatically promoted to 2.0, and the whole division happens as real floating-point math.
What do wrong and right each print, and why do they differ?
wrong prints 2.00; right prints 2.50. In wrong, a / b runs first as integer division (10 / 4 = 2, discarding the .5), and only that already-truncated 2 gets cast to double. In right, the cast reaches a alone before the division happens, so b is promoted too and the division is genuine floating-point from the start, giving the full 2.5.
What does this print, and why might it be surprising?
It prints "a is NOT less". Mixing a signed int with an unsigned int in a comparison converts the signed operand to unsigned first — so a's -1 becomes the largest value an unsigned int can hold (around 4.29 billion on a typical system), which is certainly not less than 1. The else branch runs, contradicting what -1 < 1 seems to say mathematically.
What do these two lines each evaluate to?
25, then -25. Casting a floating-point value to int always truncates toward zero rather than rounding — 25.9 loses its .9 and becomes 25, and -25.9 loses its .9 and becomes -25 (not -26, which is what rounding or floor() would give).
What value does small end up holding, and why?
44. unsigned char can hold only 0 through 255 (8 bits, on essentially every real platform), so a value that doesn't fit is reduced modulo 256 rather than clamped or flagged as an error: 300 % 256 is 44, which is exactly what small stores.
Is this addition performed as char + char, or as something else, and what does result hold?
As int + int, and result holds 30. Integer promotions convert small integer types like char to int before they take part in an expression, so the addition itself is carried out on two ints, not on two single-byte chars.
10 — 10.5 is itself a double literal, and assigning it into x, an int, triggers the same narrowing/truncation rule as any other double-to-int conversion: the .5 is simply discarded, not rounded.
Write a function that safely converts a double to an int, but only if the value actually fits within int's range — otherwise it should report an error instead of producing a meaningless result.
The function first compares value against INT_MAX and INT_MIN (cast to double for the comparison, so the comparison itself happens in floating-point) before ever attempting the cast. If value is outside that range, it returns false and leaves *out untouched, avoiding the undefined behavior that a direct (int)value cast would risk on an out-of-range value. Only once the range check passes does it perform the actual (int)value truncation and report success through the boolean return value and the out-parameter.
Write an expression that correctly computes the average of three int variables a, b, and c as a precise floating-point value, not a truncated integer.
double average = (double)(a + b + c) / 3; — a + b + c is computed first, entirely as integer addition (which loses nothing, since addition alone doesn't truncate), and only the finished sum is cast to double before dividing by 3. Casting just one of a, b, or c individually (like (double)a + b + c) would also work, since it forces the whole sum to be computed in floating-point from that point on, but casting the completed sum is the clearest and least error-prone way to write it. Writing (double)(a + b + c) / 3 also avoids the common mistake of writing (double)((a + b + c) / 3), which would perform the addition and division as integers first and only cast the already-truncated result afterward.
Write a function that converts a single digit character ('0' through '9') into its integer value, and returns -1 if the character passed in isn't actually a digit.
The function first checks that c falls within the '0'..'9' range using ordinary char comparisons (chars are promoted to int for the comparison, but that doesn't change the logic). '0' through '9' are guaranteed by the C standard to have consecutive, increasing numeric codes, so once c is confirmed to be a digit, c - '0' measures exactly how far c's code is from '0', which is precisely the digit's numeric value — 0 through 9. Any character outside that range returns -1 as a sentinel instead of producing a meaningless subtraction result.
int -> double widening, implicit, no data lost for ordinary values
double -> int narrowing, truncates toward zero, needs a cast
int -> smaller int narrows modulo 2^N (e.g. int -> unsigned char)
signed -> unsigned reinterpreted modulo 2^N; negative becomes huge
char -> int reveals the char's underlying numeric code
implicit conversion C performs it automatically (assignment, mixed arithmetic)
explicit conversion you request it by hand: (type)expression
expression conversion applies to one operand before an operator runs
assignment conversion applies to the whole right-hand side, once, at the end#include <stdio.h>
int main() {
int total = 7;
int count = 2;
double average = (double)total / count;
printf("%.2f\n", average); // 3.50, not 3.00
return 0;
}#include <stdio.h>
int main() {
double price = 19.99;
int whole = (int)price;
char letter = (char)321;
printf("whole: %d\n", whole);
printf("letter as int: %d\n", (int)letter);
return 0;
}int a = 7;
int b = 2;
double result = (double)a / b;
printf("%.2f\n", result);int a = 10;
int b = 4;
double wrong = (double)(a / b);
double right = (double)a / b;
printf("%.2f\n", wrong);
printf("%.2f\n", right);int a = -1;
unsigned int b = 1;
if (a < b) {
printf("a is less\n");
} else {
printf("a is NOT less\n");
}printf("%d\n", (int)25.9);
printf("%d\n", (int)-25.9);int value = 300;
unsigned char small = (unsigned char)value;
printf("%d\n", small);char a = 10;
char b = 20;
int result = a + b;int x = 10.5;
printf("%d\n", x);