C Part I · Module 3 · Expressions and Operators
Type Conversion, Casts and Integer Promotion
This module has handed you five results that were correct and looked wrong, and each time the explanation was deferred to the same place. Dividing two whole numbers discarded a fraction. A comparison reported that minus one is not less than one. Inverting four bits produced thirty-two. A shift happened in a type nobody declared, with a count larger than the variable was wide. In every case the operator behaved perfectly and the operands were not what the source code appeared to say they were, because C had converted them first. This chapter is that mechanism, built in stages rather than dumped as rules, and it ends by settling all five. It also covers casts — what they genuinely do, and the four things people believe they do that they do not.
Intermediate16 min readCPart IFundamentalsTypes
Part I · Module 3 · Chapter 3.12 · Type Conversion, Casts and Integer Promotion
1. The Debt This Module Has Been Running
Five times now, something has been deferred to this chapter:
3.3 7 / 2 gives 3, but 7.0 / 2.0 gives 3.5
3.5 -1 < 1 gives 1, but the same comparison against an unsigned gives 0
3.8 ~0x0Fu is 0xFFFFFFF0, not 0xF0
3.9 an unsigned char shifted left by 9 gives 512
3.2 "the type of the result comes from the operands"They look like five unrelated quirks. They are one mechanism seen from five angles, and the mechanism is this:
Before an operator is applied, C may convert its operands. The operator then works on the converted values, not on the types you wrote.
Chapter 3.1 asked you to get into the habit of asking what type an expression produces, not just what value. This is the chapter where that habit pays for itself five times over.
2. Stage One: Integer Promotions
The first stage affects narrow integer types — those of lower rank than int, which for our purposes means char, signed char, unsigned char, short and unsigned short.
The rule:
An operand of a type below
intin rank is converted tointif anintcan represent all its values, and tounsigned intotherwise.
Both halves matter. You will frequently see this stated as small integer types become int, which is the common case and is not the rule. On a machine where int is 32 bits, an unsigned short holds values up to 65535 and an int can represent all of them, so it promotes to int. On a machine where int and short are the same width, it cannot, and the promotion goes to unsigned int instead.
Chapter 3.9 already showed you this happening, and it is worth re-reading now that it has a name:
#include <stdio.h>
int main(void)
{
unsigned char small = 0x01u;
printf("sizeof small = %zu\n", sizeof small);
printf("sizeof (small << 1) = %zu\n", sizeof (small << 1));
printf("small << 9 = %u\n", small << 9);
return 0;
}Output:
sizeof small = 1
sizeof (small << 1) = 4
small << 9 = 512The object is one byte. The expression is four. The shift happened in int, which is why a count of 9 is legal and why the single set bit survived to position 9 instead of falling off an 8-bit edge.
That also settles Chapter 3.8's ~ surprise. ~0x0Fu inverted thirty-two positions because the operand's type was 32 bits wide — the operator inverted every bit of the type it was actually given.
3. Stage Two: The Usual Arithmetic Conversions
If the two operands still have different types after promotion, C brings them to one common type. The process, in the order C applies it:
1. Is either operand a floating type?
→ convert to the larger floating type. Done.
2. Otherwise apply the integer promotions to both.
3. Same signedness?
→ convert to the higher rank. Done.
4. Different signedness. Does the UNSIGNED operand have rank >= the signed one?
→ convert the signed operand to the unsigned type.
5. Otherwise, can the SIGNED type represent every value of the unsigned type?
→ convert the unsigned operand to the signed type.
6. Otherwise → convert both to the unsigned version of the signed type.Step 1 settles Chapter 3.3 immediately. In 7 / 2 both operands are int, so the common type is int and integer division applies. In 7.0 / 2.0 both are double, so the division is floating-point. Nothing about / changed; its operands did.
Steps 4 and 5 are where the interesting case lives, and they are the reason for the next section.
4. "Unsigned Always Wins" Is False
The folk rule is that when signed meets unsigned, the unsigned type takes over. Chapter 3.5's reversed comparison appears to confirm it. Here is the evidence that it is wrong:
#include <stdio.h>
int main(void)
{
int si = -1;
long sl = -1;
unsigned int ui = 1;
printf("sizeof(int) %zu, sizeof(long) %zu, sizeof(unsigned int) %zu\n",
sizeof(int), sizeof(long), sizeof(unsigned int));
printf("int -1 < unsigned 1 -> %d\n", si < ui);
printf("long -1 < unsigned 1 -> %d\n", sl < ui);
return 0;
}Output on the machine this chapter was written on:
sizeof(int) 4, sizeof(long) 8, sizeof(unsigned int) 4
int -1 < unsigned 1 -> 0
long -1 < unsigned 1 -> 1Two comparisons of the same two numbers. Opposite answers.
Walk them through §3's steps.
int against unsigned int. Same rank. Step 4 applies: the unsigned operand's rank is not lower, so the signed operand converts to unsigned. The −1 becomes the largest unsigned int value — Chapter 2.7's all-ones pattern — and that is not less than 1. Result 0.
long against unsigned int. long has higher rank, and on this machine it is 64 bits while unsigned int is 32. Step 4 does not apply. Step 5 does: a long can represent every value an unsigned int can hold, so the unsigned operand converts to long. The comparison is then −1 against 1 with both signed, and the answer is 1.
So the signed type won. Not because signed is privileged, but because it was wide enough to hold every value of the unsigned type without loss — which is exactly what step 5 is checking for.
This also explains a detail you can observe: the compiler warned about the first comparison and said nothing about the second.
warning: comparison of integers of different signs: 'int' and 'unsigned int' [-Wsign-compare]Only one warning, for the one that actually loses information. The diagnostic is tracking the same rule.
5. Casts: Asking for a Conversion Explicitly
Everything so far has been implicit — conversions C performs whether or not you asked. A cast is how you ask for one deliberately:
(type) expressionThe classic use is forcing a division to be done in floating point:
#include <stdio.h>
int main(void)
{
int total = 7;
int count = 2;
double good = (double) total / count;
double bad = (double) (total / count);
printf("(double) total / count = %f\n", good);
printf("(double) (total / count) = %f\n", bad);
return 0;
}Output:
(double) total / count = 3.500000
(double) (total / count) = 3.000000The difference is entirely about when the conversion happens, and Chapter 3.10's grouping rules decide that.
(double) total / count — a cast binds more tightly than /, so this is ((double) total) / count. One operand is now double, so §3's step 1 fires and converts the other one too. The division is floating-point. Result 3.5.
(double) (total / count) — the parentheses make the division happen first, in int, so Chapter 3.3's truncation applies and produces 3. Converting 3 to double afterwards gives 3.0. The fraction was gone before the cast ran.
This is the best example in the module of why these chapters belong together. Getting it right needs grouping from 3.10, integer division from 3.3, and conversions from here — and getting it wrong produces a plausible number with no warning at all.
6. Four Things a Cast Does Not Do
Casts attract wrong mental models. Each of these is worth rejecting explicitly.
A cast does not change the object's type. (double) total produces a new double value. total is still an int holding 7 afterwards, exactly as Chapter 3.1 said every operand-reading expression leaves its operand alone.
A cast does not reinterpret the bits. This one matters for the rest of the curriculum. An arithmetic conversion produces the same value in a different type, computed according to C's rules — converting the int 7 to double produces the double value 7.0, whose bit pattern has nothing in common with the int's. Module 2 drew the line between a value and its representation; conversion operates on the value. Reinterpreting a representation is a different thing entirely and belongs to Module 10's unions and Module 7's pointers.
A cast does not make unsafe arithmetic safe. Casting the result of an overflowing signed addition does not repair it — the overflow already happened, and Chapter 2.6 established there is nothing to repair. The cast converts whatever it was given.
A cast is not a way to silence a warning. If the compiler warns about mixing signedness and you add a cast, the warning disappears and the behaviour does not change. You have told the compiler you meant it. If you did mean it, the cast is honest documentation; if you did not, you have hidden the only notice you were going to get.
The useful test: a cast should make a deliberate conversion visible, not make a type problem invisible. (double) total / count passes — it says I want this division done in floating point. A cast added because a warning appeared usually fails it.
7. The Five Surprises, Settled
Back to §1's list, each now a consequence rather than a quirk:
| Surprise | Explanation |
|---|---|
7 / 2 is 3; 7.0 / 2.0 is 3.5 | Step 1. Two int operands give an int common type, so integer division. Two double operands give floating division. |
-1 < 1u is 0 | Step 4. Equal rank, so the signed operand converts to unsigned and −1 becomes the largest value. |
long -1 < 1u is 1 | Step 5. long outranks and can represent every unsigned int value, so the unsigned converts to long. |
~0x0Fu is 0xFFFFFFF0 | The operand's type is 32 bits wide, so 32 positions were inverted. |
small << 9 is 512 | Integer promotion. The operand became int before the shift, so the operation had 32 bits to work in. |
One mechanism. Five consequences. That is what Chapter 3.1 was setting up when it asked you to ask about types as well as values.
8. What You Should Be Able to Say Now
- C converts operands before applying an operator. The operator works on the converted values.
- Integer promotions convert operands of rank below
int— tointif it can represent all their values, otherwise tounsigned int. - That is why
small << 9works and why~inverted 32 positions. - The usual arithmetic conversions then bring both operands to a common type: floating first, else promote, else rank and signedness.
- "Unsigned always wins" is false. When the signed type outranks the unsigned type and can represent all its values, the unsigned operand converts to signed.
- Which happens depends on your implementation's widths, so the safe habit is not to mix signedness.
- The result's type is the common type — not necessarily either operand's original type.
- A cast requests a conversion explicitly, and binds more tightly than the arithmetic operators.
(double) a / band(double) (a / b)differ because of when the conversion happens.- A cast does not change the object, reinterpret bits, repair unsafe arithmetic, or make a warning's cause go away.
9. Exercise
Exercise 1 — name the common type. For each, give the common type and the result's type, assuming int is 32 bits and long is 64: int + int; int + double; short + short; unsigned int + int; long + unsigned int.
Exercise 2 — predict, then run. What does each print, and why?
printf("%f\n", 5 / 2);
printf("%f\n", 5 / 2.0);
printf("%f\n", (double) 5 / 2);
printf("%f\n", (double) (5 / 2));One of those four lines is a mistake of a different kind than the others — identify it.
Exercise 3 — apply the ladder. Using §3's steps, explain in your own words why si < ui gave 0 and sl < ui gave 1 in §4. Name the step number that decided each.
Exercise 4 — predict the warning. Of the two comparisons in §4, only one produced -Wsign-compare. Explain which, and why the compiler was right not to warn about the other.
Exercise 5 — repair the average. This is meant to report an average and prints 3.000000. Say exactly where the fraction was lost and give two different correct versions:
int points = 7;
int games = 2;
printf("%f\n", (double) (points / games));Exercise 6 — judge each cast. Say whether each is honest documentation or a hidden problem:
(a) (double) total / count to force floating division
(b) (unsigned int) signed_value added because -Wsign-compare complained
(c) (int) some_double to deliberately discard the fractional part
(d) (int) (a * b) added because the multiplication overflowed
Exercise 7 — close the module. For the expression (unsigned char) 200 + (unsigned char) 200, say what type the addition happens in, what value it produces, and why the answer is not 144.
10. Summary
- Operands are converted before the operator runs. This is invisible in the source and explains every surprise in this module.
- Integer promotions widen operands below
int— tointwhere possible, otherwiseunsigned int. - The usual arithmetic conversions then find a common type: floating first, then rank and signedness.
- Unsigned does not always win. A wider signed type that can represent every unsigned value takes over instead — and the same comparison flips.
- Which applies depends on your implementation's widths, so do not mix signedness.
- A cast asks for a conversion explicitly and binds tightly, which is why
(double) a / band(double) (a / b)differ. - A cast does not change the object, reinterpret bits, fix unsafe arithmetic, or resolve the problem a warning was reporting.
- Five surprises, one mechanism.
Module 3 in one sentence
An expression is not syntax that computes a number. It has operands with types, conversions applied before anything runs, grouping fixed by precedence, an evaluation order that is mostly not fixed, possibly side effects, and a result that has both a value and a type — and asking those questions in order is how you reason about any C expression you have never seen before.
That reasoning is what the next module needs. Until now every program has run straight through from the first statement to the last, because C has offered no way to do anything else. Module 4 adds the ability to choose a path and to repeat work — and both of those are built on expressions, because what C examines to decide which way to go is exactly the value-and-type you have spent this module learning to predict.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
