Conversions and casts

Every arithmetic operation in C happens in a type. When the operands differ, the compiler converts one of them, sometimes without a word in the source. Most conversions are harmless and a few change the value, so the skill is to know which is which and to ask the compiler to report the rest.

Integer promotion

Types smaller than int are promoted to int before arithmetic. Two char values add as two ints, and a character constant such as 'A' has type int in C, not char. The promotion is why a sum of two characters can exceed the range of a character.

#include <stdio.h> int main(void) { char a = 100; char b = 100; int sum = a + b; printf("sizeof(a + b): %zu\n", sizeof(a + b)); printf("sum: %d\n", sum); printf("sizeof 'A': %zu\n", sizeof 'A'); return 0; }

gcc -Wall -Wextra promote.c -o promote
./promote

sizeof(a + b): 4
sum: 200
sizeof 'A': 4

The addition produces an int, and the assignment to sum keeps it. Assigning the result back to a char would narrow it again, which is the next section.

Narrowing conversions

Converting to a narrower type keeps the low bits. For an unsigned target the result is defined modulo 2n; for a signed target whose range cannot hold the value, the result is implementation-defined. GCC on x86_64 produces the same wrapped number, which is what the casts below show.

#include <stdio.h> int main(void) { int big = 300; char small = (char) big; unsigned char byte = (unsigned char) big; printf("(char) 300: %d\n", small); printf("(unsigned char) 300: %d\n", byte); return 0; }

gcc -Wall -Wextra narrow.c -o narrow
./narrow

(char) 300: 44
(unsigned char) 300: 44

300 is 256 + 44, so the low byte is 44 in both cases. The two types differ above 127: a plain char is signed on x86_64 and can hold a negative value, an unsigned char cannot.

Signed to unsigned

Converting a negative signed value to an unsigned type adds or subtracts the modulus until the value fits, so -1 becomes UINT_MAX. The same conversion happens silently in a comparison against an unsigned operand, which is why -1 < 1u is false.

#include <stdio.h> int main(void) { int minus_one = -1; unsigned int converted = (unsigned int) minus_one; printf("as unsigned: %u\n", converted); printf("equal to UINT_MAX: %d\n", converted == 4294967295u); printf("converted < 1u: %d\n", converted < 1u); return 0; }

gcc -Wall -Wextra signedunsigned.c -o signedunsigned
./signedunsigned

as unsigned: 4294967295
equal to UINT_MAX: 1
converted < 1u: 0

The comparison is false because both sides are unsigned and the left one is the largest possible value. Keep counters signed, or keep them consistently unsigned; mixing the two in one comparison is where bugs start.

Floating point and integers

Converting a floating value to an integer truncates toward zero, so 2.9 becomes 2 and -2.9 becomes -2. Converting the other way is exact only while the integer fits in the mantissa; after that the low digits are lost. The cast in the last line applies to the 7 only, so the division happens in double precision.

#include <stdio.h> int main(void) { double positive = 2.9; double negative = -2.9; printf("(int) 2.9: %d\n", (int) positive); printf("(int) -2.9: %d\n", (int) negative); printf("(double) 7 / 2: %.2f\n", (double) 7 / 2); return 0; }

gcc -Wall -Wextra floatint.c -o floatint
./floatint

(int) 2.9: 2
(int) -2.9: -2
(double) 7 / 2: 3.50

A value outside the range of the target type makes the conversion undefined, so check before casting when the input comes from a file or a user.

Explicit casts

A cast states the conversion in the source, and it is the place to put the rounding you actually want. Prices are the usual example: the truncated conversion loses a cent, so add half a unit before the cast and the result rounds to the nearest cent.

#include <stdio.h> int main(void) { double price = 19.99; int cents = (int) (price * 100.0 + 0.5); printf("%d\n", cents); return 0; }

gcc -Wall -Wextra -Wconversion cast.c -o cast
./cast

1999

This file builds without a warning because the conversion is explicit. -Wconversion reports implicit conversions that may change a value, and an explicit cast is the way to say that the change is intended.

Silent conversions

Without the cast, the same rounding is gone and the compiler says so only when asked. GCC keeps this check behind -Wconversion. The first program loses the fraction of a cent:

#include <stdio.h> int main(void) { double price = 19.99; int cents = price * 100; printf("%d\n", cents); return 0; }

gcc -Wall -Wextra -Wconversion conv.c -o conv

conv.c: In function 'main':
conv.c:6:17: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
6 | int cents = price * 100;
| ^~~~~

The second one overflows a short. The literal fits in an int, but not in the target:

#include <stdio.h> int main(void) { short value = 100000; printf("%d\n", value); return 0; }

gcc -Wall -Wextra -Wconversion narrowc.c -o narrowc

narrowc.c: In function 'main':
narrowc.c:5:19: warning: overflow in conversion from 'int' to 'short int' changes value from '100000' to '-31072' [-Woverflow]
5 | short value = 100000;
| ^~~~~~

Both programs build and run, and both produce a number that is not the one in the source. A warning is cheaper than the bug report that follows the wrong price.

Exercises

  1. Convert 300 to char, unsigned char and short, and print each result with the matching format.
  2. Write a function that takes a double and returns the rounded int, including negative values.
  3. Compile the integer-division program from lesson 5 with -Wconversion and list every warning it produces.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What happens to char operands before arithmetic?

    Answer

    They are promoted to int

    Types smaller than int are promoted before arithmetic, so two char values add as int values.

  2. How does converting a negative double such as -2.9 to int behave?

    Answer

    It truncates toward zero and gives -2

    A floating-to-integer conversion discards the fractional part and keeps the sign, so -2.9 becomes -2.

  3. What does (unsigned int) -1 produce?

    Answer

    UINT_MAX

    A negative value converted to unsigned has the modulus added until it fits, so -1 becomes UINT_MAX.

  4. What is the purpose of an explicit cast in a narrowing conversion?

    Answer

    It tells the compiler the change is intended

    The cast does not change the bits, but it documents the intent and silences the -Wconversion warning for that conversion.

  5. What does this program print? #include <stdio.h> int main(void) { char a = 100, b = 100; printf("%zu\n", sizeof(a + b)); return 0; }

    Answer

    4

    The addition promotes both char operands to int, so the expression has the size of an int.

  6. What does this program print? #include <stdio.h> int main(void) { int big = 300; printf("%d\n", (char) big); return 0; }

    Answer

    44

    300 is 256 plus 44, and the conversion keeps the low byte, which is 44.

  7. What does this program print? #include <stdio.h> int main(void) { printf("%d\n", (int) -2.9); return 0; }

    Answer

    -2

    The conversion truncates toward zero, so the fraction is dropped and the value stays negative.

  8. What does this program print? #include <stdio.h> int main(void) { printf("%.2f\n", (double) 7 / 2); return 0; }

    Answer

    3.50

    The cast makes the left operand a double, so the division is done in double precision and gives 3.5.

  9. What does this program print? #include <stdio.h> int main(void) { double price = 19.99; int cents = price * 100; printf("%d\n", cents); return 0; }

    Answer

    1998

    The product is slightly below 1999 in binary, and the conversion truncates it to 1998.

  10. What does this program print? #include <stdio.h> int main(void) { int minus_one = -1; printf("%d\n", minus_one < 1u); return 0; }

    Answer

    0

    The signed operand is converted to unsigned, so it becomes the largest value and is not smaller than one.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev