Learn C Programming

Lesson 6 of 8 · Operators and Type Conversion

Module 4 · Operators and Type Conversion

Type Conversion: Promotion, Casting and Silent Loss

FreeReading

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 conversion ladder: the lower type is raised to the higher one The lower operand is raised; the higher one never moves up long double double float unsigned long long long long unsigned int int char and short are promoted to int before they arrive 1 + 2.5 happens in double -1 < 1u happens in unsigned int and that one loses the minus sign

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 / count converts total only.
  • 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 double stored in an int loses 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 char becomes 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.

Example 1: the smallest cast that matters

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.

Run in Compiler
Example 2: the comparison that was true and then was not

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.

Run in Compiler
Example 3: a narrowing done on purpose, and reported

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 Compiler

Where 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. sizeof and 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 char and works in int, 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.

Brain teaser

Zara stores a byte value from a sensor and prints it as a number.

#include <stdio.h>

int main(void)
{
    char c = 200;

    printf("%d\n", c);
    return 0;
}

It prints -56 on the Playground. Explain the two separate steps that produced that number, and say which type each step worked in. Then answer the harder half: the honest phrase for this outcome is "implementation defined", not "undefined". Say what the difference is, and what one line of the program you would change to make the value 200 on every machine.

Module 2 lesson 4 said plain char may be signed or unsigned. Work out 200 minus 256, then ask why that subtraction is the one that happened.

Exercise 1Easy

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 Compiler
Exercise 2Medium

Amara 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 Compiler
Exercise 3Medium

Kenji 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 Compiler
Exercise 4Hard

Maria'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.

Run in Compiler

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 / count need 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 unsigned a 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 round from <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 -Wconversion on?

    On a new project, try it. It reports a great many lines that are perfectly fine, which is why it is not in -Wall and why most teams leave it off.

Key takeaways

  • C converts operands to a common type before nearly every operator.
  • char and short are promoted to int, so arithmetic never happens in them.
  • The ladder raises the lower operand; int beside unsigned int becomes 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