Type Conversions
When operands of different types meet, C converts them before doing anything. The rules are fixed, silent, and applied to every expression you write. This is the week that explains bugs which compile without a warning and behave perfectly until the day they do not.
- Apply integer promotion and the usual arithmetic conversions to predict an expression's type.
- Explain why comparing a signed and an unsigned value can produce a mathematically wrong answer.
- Use a cast deliberately, and recognize when a cast is hiding a bug rather than fixing one.
- Describe what happens when a value does not fit the type it is assigned to.
- Compare two floating-point numbers correctly, and detect
NaNand infinity.
1Conversions happen whether you ask or not
C is a statically typed language with a permissive attitude: rather than rejecting an expression whose operands differ, it converts them to a common type and proceeds. These implicit conversions occur in four places:
- In an arithmetic or comparison expression, to bring both operands to one type.
- On assignment, to convert the right-hand value to the left-hand type.
- When passing an argument to a function, to match the parameter type.
- On
return, to match the function's declared return type.
Most of the time this is convenient and correct. The remainder of this week is about the cases where it is not.
2Integer promotion
Before any arithmetic, operands smaller than int are converted to int. This applies to char, signed char, unsigned char, short, unsigned short, and _Bool.
The reason is historical and practical: early processors had no arithmetic on sub-word operands, so the language defined arithmetic to happen at word width.
char a = 100;
char b = 100;
char c = a + b; /* a and b promoted to int: 200. Stored back
into a char, which cannot hold 200. */The addition itself is fine — it happens in int and yields 200. The damage is done on assignment, where 200 is squeezed back into a signed char.
Promotion also explains something that puzzles beginners:
printf("%zu\n", sizeof(char)); /* 1 */
printf("%zu\n", sizeof('A')); /* 4 — in C, 'A' is an int */
printf("%zu\n", sizeof(char) + 1); /* 8? no: size_t arithmetic */A character constant such as 'A' has type int in C, not char. This differs from C++, and it occasionally matters.
3The usual arithmetic conversions
Once both operands are at least int, C brings them to a common type by walking down this list and stopping at the first case that applies:
| Step | Rule |
|---|---|
| 1 | If either is long double, convert the other to long double. |
| 2 | Otherwise if either is double, convert the other to double. |
| 3 | Otherwise if either is float, convert the other to float. |
| 4 | Otherwise both are integers: apply integer promotion, then convert toward the type of greater rank. |
| 5 | If the two have equal rank but differ in signedness, the signed one is converted to unsigned. |
Step 5 is the one that draws blood. The rest are intuitive: mixing an int with a double gives a double, which is what anyone would expect.
Worked through:
int i = 3;
double d = 2.0;
i / 2 /* int / int -> int -> 1 */
i / 2.0 /* int / double -> double -> 1.5 */
i / d /* int / double -> double -> 1.5 */
(i + 1) / 2 /* 4 / 2 -> int -> 2 */4The signed-unsigned trap
Here is the single most notorious conversion bug in C:
int a = -1;
unsigned b = 1;
if (a < b) {
puts("-1 is less than 1"); /* never printed */
} else {
puts("-1 is NOT less than 1"); /* this one runs */
}Both operands have rank int, and one is unsigned, so rule 5 applies: a is converted to unsigned int. In two's complement, −1 becomes 4 294 967 295. That number is not less than 1, so the comparison is false — and it is correct according to the language. The bug is in the program, not the compiler.
The same mechanism turns loops into infinite loops:
size_t n = 0;
for (size_t i = 0; i < n - 1; i++) {
/* n - 1 is unsigned. 0 - 1 wraps to SIZE_MAX.
This loop runs about 18 quintillion times. */
}And it is why strlen returning size_t deserves care:
if (strlen(s) - 1 >= 0) /* always true: unsigned is never negative */Defences
- Compile with
-Wsign-compare, which-Wextraenables. It flags exactly this. - Do not mix signedness in one expression. Pick a convention per function and convert at the boundary.
- Rearrange rather than subtract: write
i + 1 < ninstead ofi < n - 1. - Use unsigned types for quantities that genuinely cannot be negative — sizes, indices, bit patterns — and signed types for everything else.
Unsigned arithmetic never overflows; it wraps. That is defined behavior, computed modulo 2N. It is not an error, nothing is reported, and the program continues with a wrong value. Signed overflow, by contrast, is undefined behavior — see week 36. Neither is a safety net.
5Explicit conversion: the cast
A cast forces a conversion:
double average = (double)total / count;Placement matters entirely. (double)total / count converts total first, so the division is double / int → double. (double)(total / count) performs integer division first and then widens the already-truncated result. The parentheses are the whole difference between a correct average and a wrong one.
When a cast is right
- Forcing floating-point division between integers, as above.
- Silencing a legitimate narrowing where you have already checked the range.
- Converting
void *to a typed pointer — week 30. - Selecting an overload of a standard function, for example
(double)xbefore aprintf("%f").
When a cast is wrong
A cast tells the compiler "I know what I am doing." If you do not, the cast merely deletes the warning that would have told you. A cast added to make a warning go away, without a reason you could state out loud, is a bug with the alarm disconnected.
/* Wrong: hides a real narrowing problem */
short s = (short)some_large_int;
/* Right: the check makes the cast honest */
if (some_large_int >= SHRT_MIN && some_large_int <= SHRT_MAX) {
short s = (short)some_large_int;
}6When the value does not fit
| Conversion | What happens |
|---|---|
| Larger integer → smaller integer | The high-order bits are discarded. For unsigned targets this is defined modulo arithmetic; for signed targets it is implementation-defined. |
| Negative → unsigned | Defined: add 2N. −1 becomes the maximum value. |
double → integer | The fractional part is discarded — truncated toward zero, not rounded. If the value is out of range, the behavior is undefined. |
Integer → float | May lose precision. A 32-bit int has more significant digits than a float can hold. |
double → float | Rounded to the nearest representable float; may become infinity. |
Note the truncation rule for floating point to integer: (int)3.99 is 3 and (int)-3.99 is −3. To round, use round, floor, or ceil from <math.h> — and remember to link with -lm.
7Comparing floating-point values
Week 4 established that most decimal fractions have no exact binary representation. The consequence for comparison is direct: never use == on floating-point values unless you are comparing against a value you know to be exactly representable, such as 0.0 assigned literally.
The naive fix, and why it is not enough
if (fabs(a - b) < 1e-9) { … } /* absolute tolerance */This works when the numbers are near 1. It fails at both extremes: for values around 10⁻¹⁵ every pair compares equal, and for values around 10¹⁵ no pair ever does, because the gap between adjacent representable doubles is already larger than 10⁻⁹.
A tolerance that scales
#include <math.h>
#include <float.h>
#include <stdbool.h>
static bool nearly_equal(double a, double b, double relative)
{
double diff = fabs(a - b);
if (diff <= DBL_EPSILON) { /* handles a == b and both near zero */
return true;
}
double scale = fmax(fabs(a), fabs(b));
return diff <= relative * scale;
}Choosing relative is a judgement about your problem, not a property of the language. For values derived from a handful of arithmetic steps, something around 1e-9 for double is reasonable. For a long accumulation, error grows and the tolerance must grow with it.
Accumulated error
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
/* sum is 0.99999999999999988898, not 1.0 */Each addition rounds, and the errors do not cancel. Where exactness matters — money, above all — do not use floating point. Store amounts as integer minor units (kuruş, cents) and format for display.
NaN and infinity
#include <math.h>
double x = 0.0 / 0.0; /* NaN */
double y = 1.0 / 0.0; /* infinity */
isnan(x) /* true */
isinf(y) /* true */
isfinite(y) /* false */
(x == x) /* false — NaN is not equal to itself */That last line is the defining property of NaN and a legitimate test for it, though isnan says what you mean. It also means a NaN quietly poisons every comparison: a < b, a > b, and a == b are all false when either is NaN, so a sort can behave bizarrely if one slips into the data.
8Worked example: two bugs that compile cleanly
Save as convert.c.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <float.h>
#include <limits.h>
static bool nearly_equal(double a, double b, double relative)
{
double diff = fabs(a - b);
if (diff <= DBL_EPSILON) {
return true;
}
double scale = fmax(fabs(a), fabs(b));
return diff <= relative * scale;
}
int main(void)
{
puts("== bug 1: a comparison that is wrong but not incorrect ==");
int signed_value = -1;
unsigned unsigned_value = 1;
printf(" -1 < 1u evaluates to %d\n", signed_value < unsigned_value);
printf(" because -1 converted to unsigned is %u\n",
(unsigned)signed_value);
printf(" fix: compare in a signed type -> %d\n",
signed_value < (int)unsigned_value);
puts("\n== the loop that would never end ==");
const char *empty = "";
size_t length = strlen(empty);
printf(" strlen(\"\") = %zu\n", length);
printf(" length - 1 = %zu <-- wrapped\n", length - 1);
puts(" so for (i = 0; i < length - 1; i++) would run ~1.8e19 times");
puts(" fix: write i + 1 < length instead");
puts("\n== bug 2: floating point equality ==");
double a = 0.1, b = 0.2;
printf(" 0.1 + 0.2 = %.20f\n", a + b);
printf(" == 0.3 -> %s\n", (a + b == 0.3) ? "true" : "false");
printf(" nearly_equal -> %s\n",
nearly_equal(a + b, 0.3, 1e-9) ? "true" : "false");
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += 0.1;
}
printf(" 0.1 added ten times = %.20f\n", sum);
printf(" == 1.0 -> %s\n", (sum == 1.0) ? "true" : "false");
printf(" nearly_equal -> %s\n",
nearly_equal(sum, 1.0, 1e-9) ? "true" : "false");
puts("\n== promotion and narrowing ==");
char small_a = 100, small_b = 100;
printf(" 100 + 100 as int = %d\n", small_a + small_b);
printf(" stored in a char = %d <-- did not fit\n",
(char)(small_a + small_b));
printf("\n (int)3.99 = %d (truncates, never rounds)\n", (int)3.99);
printf(" (int)-3.99 = %d\n", (int)-3.99);
printf(" lround(3.99) = %ld\n", lround(3.99));
puts("\n== the average, done wrong and right ==");
int total = 7, count = 2;
printf(" total / count = %d\n", total / count);
printf(" (double)(total / count) = %g <-- cast too late\n",
(double)(total / count));
printf(" (double)total / count = %g <-- correct\n",
(double)total / count);
puts("\n== NaN and infinity ==");
double nan_value = 0.0 / 0.0;
double inf_value = 1.0 / 0.0;
printf(" 0.0/0.0 = %f, isnan -> %d, equals itself -> %d\n",
nan_value, isnan(nan_value), nan_value == nan_value);
printf(" 1.0/0.0 = %f, isinf -> %d\n", inf_value, isinf(inf_value));
return EXIT_SUCCESS;
}gcc -std=c17 -Wall -Wextra -g -o convert convert.c -lm
./convertThe -lm is required: <math.h> declares fabs and isnan, but their implementations live in the math library, which the linker does not search unless told. Omit it and you get the undefined reference error from week 2.
Read the warnings first
convert.c:24:44: warning: comparison of integer expressions of different
signedness: 'int' and 'unsigned int' [-Wsign-compare]That one warning is the entire subject of section 4, delivered by the compiler for free. A project that compiles with warnings switched on and then ignores them has paid the cost of the tooling without collecting the benefit.
9Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Comparing int with size_t | Negative values become huge; comparison inverts | Keep one signedness per expression; heed -Wsign-compare. |
i < length - 1 when length is 0 | Wraps to SIZE_MAX; loop never ends | Write i + 1 < length. |
(double)(a / b) | Integer division already truncated | (double)a / b. |
if (x == 0.1) | Almost always false | Compare with a scaled tolerance. |
| Adding a cast to silence a warning | The bug stays, the alarm is gone | Understand the warning; cast only when you can state why. |
Using double for money | Accumulated rounding error | Store integer minor units. |
Forgetting -lm | undefined reference to 'fabs' | Link the math library. |
Assuming (int)x rounds | 3.99 becomes 3 | Use lround, floor, or ceil. |
10Check yourself
Why is -1 < 1u false?
Both operands have the rank of int and one is unsigned, so the usual arithmetic conversions convert the signed operand to unsigned. In two's complement −1 becomes 4 294 967 295, which is not less than 1. The comparison is behaving exactly as specified; the defect is mixing signedness in the expression.
What is the difference between (double)total / count and (double)(total / count)?
The first converts total before dividing, so the division is performed in double and keeps the fraction. The second divides two integers — truncating immediately — and then widens the already-wrong result. Only the first computes an average correctly.
Is char c = a + b; with a and b both 100 an error?
Not a compile error. The operands are promoted to int, the sum 200 is computed correctly, and then it is converted back to char on assignment. If plain char is signed and 8 bits, 200 does not fit and the result is implementation-defined. The arithmetic was never the problem; the storage was.
Why is an absolute tolerance such as fabs(a-b) < 1e-9 not a general solution?
Because the spacing between representable doubles grows with magnitude. Near 10¹⁵ that spacing already exceeds 10⁻⁹, so no two distinct values ever compare equal; near 10⁻¹⁵ almost every pair does. A tolerance must scale with the size of the values being compared.
A sort of your double array produces nonsense. One element is NaN. Why does that break it?
Every comparison involving NaN is false — <, >, and == alike. A comparison function therefore reports that the NaN is neither before nor after nor equal to anything, which violates the ordering the sorting algorithm assumes and can corrupt the whole result. Filter or reject NaN before sorting.
11Where this leads
You can now predict the type and value of any expression. Week 8 uses that directly: printf and scanf take a format string whose specifiers must match the argument types exactly, and the conversions from this week are precisely what decides whether they do. It is also where you meet the first function that can corrupt memory if you get an argument wrong.