Module 4 · Operators and Type Conversion
Type Conversion: Promotion, Casting and Silent Loss
In this lesson
- Predict which type an operator will work in when its two operands differ.
- Write a cast with
(type)and say what it is for in each of its two honest uses. - Name the three silent losses: truncation, narrowing and signed turning unsigned.
Maria has a stock count of -1 after a bad import, and a check that the count is below a limit of 1u.
She writes count < limit. It is false. Minus one is not less than one, says the program.
Nothing was corrupted. The u on the limit changed which type the comparison happened in, and -1 does not survive that change.
C converts before it operates, always
Almost every C operator needs both operands to be the same type. When they are not, C converts one of them first.
This happens without you asking and without any message. It is called implicit conversion, and it is the subject of this lesson.
Two sets of rules do the work: integer promotion, which happens to small types, and the usual arithmetic conversions, which settle the rest.
Integer promotion: small types are not operated on at all
A char or a short is never the type an arithmetic operator works in. Both are promoted to int first.
#include <stdio.h>
int main(void)
{
char small = 'A';
short shorter = 300;
printf("a char is %zu byte\n", sizeof(small));
printf("a short is %zu bytes\n", sizeof(shorter));
printf("char + char is %zu bytes\n", sizeof(small + small));
printf("short + short is %zu bytes\n", sizeof(shorter + shorter));
return 0;
}
a char is 1 byte
a short is 2 bytes
char + char is 4 bytes
short + short is 4 bytes
Two one-byte values were added and the answer is four bytes wide. Neither operand was a char by the time the + saw them.
This is good news. It is why 'A' + 1 cannot overflow, and why Module 2's character arithmetic behaved sensibly.
The usual arithmetic conversions, as a ladder
Once both sides are at least int, C picks the higher of the two on this ladder and converts the other one up to it.
The first rung is easy to accept: mix an int with a double and you get a double. Lesson 1's 3.0 was this rule.
The rung between int and unsigned int is the one that costs people money.
Signed meets unsigned, and the sign loses
int and unsigned int are the same width. When they meet, C converts the signed one to unsigned.
Converting -1 to unsigned int does not produce an error or a zero. It produces 4294967295, the largest value that type can hold.
#include <stdio.h>
int main(void)
{
int count = -1;
printf("as unsigned %u\n", (unsigned int)count);
printf("count < 1u %d\n", count < 1u);
printf("count < 1 %d\n", count < 1);
return 0;
}
as unsigned 4294967295
count < 1u 0
count < 1 1
The middle line is Maria's bug. The comparison is correct arithmetic on the wrong pair of values.
The Playground says nothing at all. A local gcc -Wall reports warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int'.
So the fix is to keep both sides signed, or to cast the unsigned side down on purpose once you know it is small enough.
The cast, and its two honest uses
A cast is you doing the conversion yourself, in writing. It is row 2 of lesson 5's table, so it binds tighter than any arithmetic.
The cast operator
(type)value
(double)total / count turn one operand into a double, then divide
(int)price keep the whole part of a double, on purpose
(unsigned int)n reinterpret a signed value as unsigned
- It applies to the single value on its right, not to the whole expression.
(double)total / countconvertstotalonly. - That is enough: once one side is a
double, the ladder raises the other side too. (double)(total / count)is a different thing, and it is the bug, not the fix.
Two uses are honest. The first is forcing real division, which lesson 1 did with a 3.0 and could not do when the divisor was a variable.
The second is a narrowing you want and are willing to document. (int)price says "I know this loses the fraction, and I mean it".
#include <stdio.h>
int main(void)
{
int total = 239;
int count = 3;
double price = 99.87;
printf("no cast %d\n", total / count);
printf("cast left %.2f\n", (double)total / count);
printf("cast outside %.2f\n", (double)(total / count));
printf("to int %d\n", (int)price);
return 0;
}
no cast 79
cast left 79.67
cast outside 79.00
to int 99
The third line is the mistake that looks like the fix. The division finished as integer division before the cast was reached.
A third use, casting to silence a compiler warning you have not understood, is not honest and this track does not do it.
Three silent losses, and where each one happens
None of the three below produces a message on the Playground's command line. Each one is a value leaving through a hole in a type.
- Truncation. A
doublestored in anintloses its fraction, and it cuts toward zero rather than rounding. 99.87 becomes 99. - Narrowing. A wide integer stored in a narrow one keeps only the bits that fit. For signed types the result when the value does not fit is implementation defined, and GCC wraps: 300 in a
charbecomes 44. - Sign loss. A negative value converted to unsigned becomes a large positive one. This one is fully defined by the standard, which makes it worse: nothing is wrong, so nothing can complain.
A local gcc -Wconversion catches the first two: warning: conversion from 'double' to 'int' may change value. That flag is not in -Wall, and most projects never switch it on.
So the defence is not a compiler flag. It is choosing the type at the point where the value is born, which was Module 2's whole argument.
Two integers in, one real average out.
#include <stdio.h>
int main(void)
{
int total = 0;
int count = 0;
scanf("%d %d", &total, &count);
printf("%.2f\n", (double)total / count);
return 0;
}
79.67
That output is for the input 239 3. One cast, on the left operand, and the ladder does the rest.
The same value compared against a signed limit and an unsigned one.
#include <stdio.h>
int main(void)
{
int count = 0;
scanf("%d", &count);
printf("against 1 %d\n", count < 1);
printf("against 1u %d\n", count < 1u);
printf("cast fixes %d\n", count < (int)1u);
return 0;
}
against 1 1
against 1u 0
cast fixes 1
That output is for the input -1. Feed it 0 and all three lines agree, which is why this survives a careless test.
A price in taka and paisa, split into the two whole numbers a receipt needs.
#include <stdio.h>
int main(void)
{
double price = 0.0;
scanf("%lf", &price);
int taka = (int)price;
int paisa = (int)(price * 100) - taka * 100;
printf("%d taka %d paisa\n", taka, paisa);
return 0;
}
99 taka 87 paisa
That output is for the input 99.87. Both casts are deliberate and both are written down.
It is also fragile, and Module 2 lesson 3 said why: some prices multiply to a value just under the whole number you expected. A real till counts in paisa from the start and never stores a decimal price at all.
Run in CompilerWhere this is used
- Every average in every report. A dashboard that shows 79 where the answer was 79.67 has one missing cast, and it is the single most common numeric bug in new code.
- Indexing with
size_t.sizeofand every array length in the C library are unsigned. A loop that counts down past zero with a signed index and compares against one of those is Maria's bug, in Module 9. - Byte-level code. A network or file parser reads
unsigned charand works inint, because of integer promotion. Getting that pair wrong is how a parser reads negative lengths. - Ariane 5, 1996. Module 2 lesson 2 told this one: a 64-bit floating value converted into a 16-bit signed integer that could not hold it. The rocket was destroyed 37 seconds after launch.
Common mistakes
1. Comparing a signed value with an unsigned one.
int count = -1;
printf("%d\n", count < 1u);
Silent on the Playground; a local gcc -Wall says warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int'. It prints 0. Keep both sides signed.
2. Casting the answer instead of an operand.
int total = 239;
printf("%.2f\n", (double)(total / 3));
No message at either command line, and it prints 79.00. The cast arrived after the division was over. Write (double)total / 3.
3. Storing a double in an int and expecting rounding.
double price = 99.87;
int whole = price;
printf("%d\n", whole);
Silent at both command lines; a local gcc -Wconversion says warning: conversion from 'double' to 'int' may change value. It prints 99, not 100. For rounding, use round from <math.h>.
4. Putting a value that does not fit into a narrow type.
int big = 300;
char narrow = (char)big;
printf("%d\n", narrow);
No message at either command line, because the cast says you meant it. It prints 44 on the Playground, and that result is implementation defined rather than a rule. Check the range before you narrow.
David wants the average of five whole marks, printed as a real number.
Input. One line with five integers.
Output. One line with their average, to exactly two decimal places.
Constraints. Each mark is between 0 and 100.
Sample. Input 75 82 82 90 61 gives 78.00.
#include <stdio.h>
int main(void)
{
int a = 0;
int b = 0;
int c = 0;
int d = 0;
int e = 0;
scanf("%d %d %d %d %d", &a, &b, &c, &d, &e);
/* Add as integers. Then one cast, on one operand of the division. */
return 0;
}
Not graded in this module. Put the cast outside the division once, on purpose, to see the wrong answer.
Run in CompilerAmara is checking that she can name the type of an expression before she prints it.
Input. One line with two integers n and m.
Output. Three lines: n / m printed with %d, then n / (double)m printed with %.2f, then n * 1.0 / m printed with %.2f.
Constraints. -1000 <= n <= 1000, and 1 <= m <= 1000.
Sample. Input 239 3 gives 79, then 79.67, then 79.67.
#include <stdio.h>
int main(void)
{
int n = 0;
int m = 0;
scanf("%d %d", &n, &m);
/* Three lines. The last two reach the same answer by different routes. */
return 0;
}
Not graded in this module. Say out loud which type each of the three divisions happened in before you run it.
Run in CompilerKenji is writing a receipt line and needs a decimal price as two whole numbers.
Input. One line with one decimal value, with at most two digits after the point.
Output. One line with the whole part, one space, then the two digit fraction as a whole number.
Constraints. 0 <= price <= 100000, with at most two decimal places.
Sample. Input 99.87 gives 99 87.
#include <stdio.h>
int main(void)
{
double price = 0.0;
scanf("%lf", &price);
/* Two deliberate casts, and one subtraction between them. */
return 0;
}
Not graded in this module. Add 0.5 before the second cast and say which inputs that changes.
Run in CompilerMaria's import left some counts negative, and the limit check has to survive that.
Input. One line with two integers: a signed count and a limit given as an unsigned value.
Output. Two lines: the value of the comparison written as count < limit with the limit left unsigned, then the value of the comparison done correctly in signed arithmetic.
Constraints. -1000000 <= count <= 1000000, and 0 <= limit <= 1000000.
Sample. Input -1 1 gives 0, then 1.
#include <stdio.h>
int main(void)
{
int count = 0;
unsigned int limit = 0;
scanf("%d %u", &count, &limit);
/* First line: no cast. Second line: one cast on the limit. */
return 0;
}
Graded as unsigned-fix. The two lines agree for every non-negative count, so the hidden tests are where the sign changes.
Common doubts
Does a cast change the variable?
No. It produces a converted value for that one expression. The variable still holds what it held.
Why does
(double)total / countneed only one cast?Because the ladder does the rest. Once one operand is a
double, C raises the other one to match before dividing.Is
unsigneda bad idea then?No, it is the right type for bits and for counts that cannot be negative. The danger is only at the boundary where it meets a signed value.
How do I round instead of truncate?
Use
roundfrom<math.h>, then cast. Adding 0.5 before the cast works only for non-negative values, and that half-answer is a common bug.Should I turn
-Wconversionon?On a new project, try it. It reports a great many lines that are perfectly fine, which is why it is not in
-Walland why most teams leave it off.
Key takeaways
- C converts operands to a common type before nearly every operator.
charandshortare promoted toint, so arithmetic never happens in them.- The ladder raises the lower operand;
intbesideunsigned intbecomes unsigned. - A negative value converted to unsigned becomes a very large positive one, silently and legally.
- A cast applies to the single value on its right, so it goes on an operand, never on the answer.
- Truncation, narrowing and sign loss are all silent without
-Wconversion.
Next comes the module's graded set, where these six lessons meet ten problems and a judge.
End of lesson 6
Mark it done, and your progress moves with you.
Next: Problems: Expressions That Bite