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
- Convert 300 to char, unsigned char and short, and print each result with the matching format.
- Write a function that takes a double and returns the rounded int, including negative values.
- 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.
-
What happens to
charoperands before arithmetic?Answer
They are promoted to
intTypes smaller than
intare promoted before arithmetic, so twocharvalues add asintvalues. -
How does converting a negative
doublesuch as-2.9tointbehave?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.
-
What does
(unsigned int) -1produce?Answer
UINT_MAXA negative value converted to unsigned has the modulus added until it fits, so -1 becomes
UINT_MAX. -
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
-Wconversionwarning for that conversion. -
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
charoperands toint, so the expression has the size of anint. -
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.
-
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.
-
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. -
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.
-
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