Operators, expressions and input · about 50 min

Conversions, promotions and casts

Why this matters

C converts values between types all the time without telling you: when you add a char to an int, when you store a double into an int, when you compare a signed with an unsigned number. Most of these conversions are exactly what you want. A few silently change the value: fractions vanish, large numbers wrap, -1 becomes four billion. Knowing the rules lets you read an expression and know what type it is computed in, which is the difference between a correct average and an average of zero.

Every expression has a type

7 is an int. 7.0 is a double. x + y has a type too, decided by the types of x and y according to fixed rules, and the operation is carried out in that type. The destination of the result plays no part. In

double avg = (a + b + c) / 3;

with int variables, the right side is entirely int arithmetic; only the finished int result is converted to double for storing.

There are three situations where conversions happen implicitly: promotions of small types, the usual arithmetic conversions when an operator has two operands of different types, and assignment (including passing arguments and returning values).

Integer promotion

Types narrower than int (char, signed char, unsigned char, short, unsigned short, and _Bool) are promoted to int whenever they are used in arithmetic. The CPU's integer registers are at least int-sized anyway, so this costs nothing.

#include <stdio.h>

int main(void)
{
    unsigned char a = 200;
    unsigned char b = 100;
    int sum = a + b;
    unsigned char small = a + b;
    printf("sum = %d, small = %d\n", sum, small);

    char c = 'x';
    printf("sizeof c = %zu, sizeof(c + 0) = %zu\n", sizeof c, sizeof(c + 0));
    return 0;
}
sum = 300, small = 44
sizeof c = 1, sizeof(c + 0) = 4

a + b is computed as an int (300) and does not overflow. Storing it back into an unsigned char reduces it modulo 256 (300 - 256 = 44). And c + 0 is an int, which sizeof confirms.

The usual arithmetic conversions

When a binary operator (+ - * / % < > == ...) gets two operands of different types, the "smaller" one is converted to the type of the "larger" one, and the operation is done in that type. Simplified:

  1. If either is long double  -> both become long double
  2. else if either is double  -> both become double
  3. else if either is float   -> both become float
  4. otherwise both are integers: promote both, then convert to the
     higher-ranked type:  int < long < long long
     and if the ranks are equal but one is unsigned, unsigned wins.

Examples, with int i, long l, unsigned u, double d:

expressioncomputed as
i + ddouble
i * llong
i + uunsigned int
'a' + 1int
l + 1.0ffloat

Rule 4's last clause is the dangerous one: mixing int and unsigned int converts the int to unsigned.

int neg = -1;
unsigned int zero = 0;
printf("%d\n", neg < zero);
main.c:7:24: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
0

-1 becomes 4294967295, which is not less than 0. This bites constantly with size_t, the unsigned type of sizes and lengths: comparing an int index with a size_t length does exactly this. Take the -Wsign-compare warning seriously.

Conversion on assignment

When you store a value into a variable of another type, it is converted to the variable's type:

fromtowhat happens
any integerwider integervalue preserved
any integerunsigned type too narrowreduced modulo 2^n (defined)
any integersigned type too narrowimplementation-defined; gcc keeps the low bits
floatingintegerfraction discarded (truncated toward zero); out-of-range is undefined
integerfloatingnearest representable value (big integers may lose precision)
doublefloatrounded; may lose precision
int i = 3.99;              /* 3 */
int j = -3.99;             /* -3 */
long long big = 3000000000LL;
int truncated = big;       /* -1294967296 on gcc */

Note that double-to-int conversion truncates, it does not round. 3.99 becomes 3. If you want rounding, use round() from <math.h>, or for non-negative values add 0.5 before converting.

None of these produce a warning with -Wall -Wextra. The -Wconversion flag reports them:

warning: conversion from 'double' to 'int' changes value from '3.9900000000000002e+0' to '3' [-Wfloat-conversion]
warning: conversion from 'long long int' to 'int' may change value [-Wconversion]

It is noisy on large codebases but very educational on small programs. Try it.

Casts: explicit conversions

A cast converts a value to a type you name, written as the type in parentheses before an expression:

int total = 7, count = 2;
printf("%f\n", (double)total / count);     /* 3.500000 */
printf("%f\n", (double)(total / count));   /* 3.000000 */
printf("%f\n", total / (double)count);     /* 3.500000 */

A cast has higher precedence than the arithmetic operators, so (double)total / count means ((double)total) / count: convert total, then divide in double because one operand is double. In the second line the parentheses force the integer division first, and the cast converts the already-truncated 3.

What a cast does underneath depends on the types: int to double is a real conversion instruction (cvtsi2sd on x86-64); int to unsigned int changes nothing in the bits, only how the compiler treats them from then on; long to int simply uses the low 32 bits.

When to cast:

When not to cast: to make a warning go away without understanding it. A cast tells the compiler "I know what I am doing", and it will believe you.

Common mistakes

Recap

Going further: compile some of your earlier exercises with -Wconversion added and read each warning. Decide for each whether the conversion is intentional (add a cast) or a bug (fix the types).

Exercises

Exercise 1 · quiz

Small types in arithmetic

What does this print?

unsigned char a = 200;
unsigned char b = 100;
printf("%d\n", a + b);
Answer
  • 300

Before arithmetic, values of types smaller than int (char, short and their unsigned versions) are promoted to int. So a + b is computed as int 200 + int 100 = 300, and printed as 300. Only if you stored the result back into an unsigned char would it be reduced modulo 256 to 44.

Exercise 2 · quiz

Where the cast goes

With int total = 7; and int count = 2;, which expressions evaluate to 3.5? Select all that apply.

Answer
  • (double)total / count

  • total / (double)count

  • 1.0 * total / count

A cast binds tighter than /, so (double)total / count converts total, and then the division is done in double. (double)(total / count) converts the result of an integer division, which is already 3. In total / count * 1.0 the integer division happens first (left to right), giving 3, then 3.0. In 1.0 * total / count the multiplication comes first and turns everything into double.

Exercise 3 · write code

A percentage with a cast

A student answered 17 of 23 questions correctly. The starter stores both counts as int. Print the score as a percentage with one decimal, exactly:

score: 73.9%

Use a cast ((double)) to get floating-point division; keep both variables as int.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int correct = 17;
    int total = 23;
    double percent = correct / total * 100;
    printf("score: %.1f%%\n", percent);
    return 0;
}

Example

expected output

score: 73.9%
Hint 1

Right now 17 / 23 is integer division: 0. Then 0 * 100 is 0.

Hint 2

Cast one operand of the division, not the result: (double)correct / total * 100.

Reference solution
#include <stdio.h>

int main(void)
{
    int correct = 17;
    int total = 23;
    double percent = (double)correct / total * 100;
    printf("score: %.1f%%\n", percent);
    return 0;
}

Exercise 4 · write code

Dollars to cents

A shop stores prices in whole cents to avoid floating-point trouble, but a price list arrives in dollars as double values. Write a function

int to_cents(double dollars)

that converts a non-negative dollar amount with at most two decimals to whole cents, so that to_cents(19.99) returns 1999, to_cents(0.29) returns 29 and to_cents(4.35) returns 435.

The obvious (int)(dollars * 100) gives 1998 for 19.99. Run the starter to see it, work out why (think about how 19.99 is stored and what converting to int does), and fix it. Do not use any functions from <math.h>: casts and arithmetic are enough.

File: main.c

Starting point

#include <stdio.h>

int to_cents(double dollars)
{
    return (int)(dollars * 100);
}

int main(void)
{
    printf("%d %d %d\n", to_cents(19.99), to_cents(0.29), to_cents(4.35));
    return 0;
}
Hint 1

Print dollars * 100 with %.17g: 19.99 * 100 is 1998.9999999999998, because 19.99 has no exact binary representation.

Hint 2

Converting a double to int does not round; it chops off the fraction (truncates toward zero).

Hint 3

For non-negative values, adding 0.5 before truncating rounds to the nearest integer: (int)(dollars * 100 + 0.5).

Reference solution
#include <stdio.h>

int to_cents(double dollars)
{
    return (int)(dollars * 100 + 0.5);
}

int main(void)
{
    printf("%d %d %d\n", to_cents(19.99), to_cents(0.29), to_cents(4.35));
    return 0;
}