Course outline

C / Basic: Read and Construct Programs / Lesson 2

Types, Ranges, and Conversions

Choose representations by their contracts, and follow the conversions that occur before a value is stored.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish integer and floating arithmetic.
  • Use limits and sizeof without assuming machine widths.
  • Explain promotions, narrowing, and signedness risks.

A type limits meaningful values

A type tells the implementation how to interpret a stored representation and which operations apply. int is convenient for ordinary whole numbers, double for approximate real arithmetic, and char for character storage. Neither int nor long has a universal byte width. limits.h exposes integer bounds, while sizeof measures storage in C bytes; a byte contains CHAR_BIT bits and need not contain eight.

Choose types from the problem domain and the documented platform constraints. A count of array elements naturally uses size_t; a possibly negative temperature needs a signed representation. An exact-width type such as uint32_t exists only when the implementation provides the required exact representation.

Arithmetic happens before assignment

When both operands are int, division produces an integer quotient truncated toward zero. Assigning that quotient to double cannot recover a discarded fractional part. Make an operand floating before division when the model requires a fraction. A cast requests a conversion at its location; it does not retroactively change earlier operations.

The example uses small positive values so every intermediate integer is representable. Floating output is formatted to two decimal places; this is a presentation decision, not proof that all decimal fractions have exact binary representations. Money calculations often require a deliberately chosen integer scale or a more specialized arithmetic policy.

c Conversion before and after division
#include <stdio.h>

int main(void)
{
    int total = 7;
    int people = 2;
    double truncated = total / people;
    double share = (double)total / people;
    printf("%.2f %.2f\n", truncated, share);
    return 0;
}
Expected output
3.00 3.50

The first division yields int 3; the second uses floating arithmetic and yields 3.5.

Follow the expression type

Small integer types undergo integer promotions in many expressions. Mixed signed and unsigned operands then follow conversion rules that can make a negative operand behave like a large unsigned value. Avoid solving this with a blanket cast: first decide whether negative values are valid, validate them, and convert only after that check.

Signed integer overflow is undefined behavior in C17; unsigned arithmetic wraps modulo one more than the maximum value. Wrapping is useful only when it is the intended algorithm. Casting an overflowing signed result afterward is too late. For positive int addition, checking a <= INT_MAX - b before evaluating a + b avoids the overflow.

Check before the risky operation

The next program keeps a simple contract: both inputs are nonnegative int values and addition is allowed only within INT_MAX. The guard is evaluated before the addition, so its own arithmetic remains representable. For arbitrary signs, a complete helper needs separate positive and negative cases; a one-sided guard must not be advertised as a universal overflow checker.

Printing a yes/no result instead of a machine-specific width makes this example portable. In real programs, document whether an arithmetic failure is rejected, saturated, or reported to the caller. Quietly replacing one of these policies with another changes the meaning of the data.

c A nonnegative addition contract
#include <limits.h>
#include <stdio.h>

int main(void)
{
    int a = 20;
    int b = 22;
    if (a >= 0 && b >= 0 && a <= INT_MAX - b) {
        printf("sum=%d\n", a + b);
    } else {
        puts("out of range");
    }
    return 0;
}
Expected output
sum=42

The documented inputs satisfy the guard, so addition safely produces 42.

Separate representation from meaning

A character constant such as 'A' has type int in C, while a string literal denotes an array of char with a terminating null character. Plain char may behave as signed or unsigned, which matters when it stores arbitrary bytes. Do not assume an encoding value for a letter unless a format explicitly requires that encoding.

For floating values, compare the needs of the calculation with the available range and precision. Rounding can accumulate, and exact equality after a calculation may be inappropriate. The right tolerance depends on scale and error analysis, not a universal magic constant. This distinction between representation and meaning will recur in file formats and system interfaces.

Common errors and fixes

A double receives 3 instead of 3.5.

Why it happens
Integer division occurred before assignment.
How to fix it
Convert an operand to double before the division.
Prevent it next time
Annotate the type of each intermediate expression.

A negative value compares unexpectedly with a size.

Why it happens
Unsigned conversions changed the comparison domain.
How to fix it
Reject negative input before converting to size_t.
Prevent it next time
Keep signed validation and unsigned indexing as explicit steps.

A cast appears to fix an overflow.

Why it happens
The invalid signed operation already occurred before the cast.
How to fix it
Check operands first or choose a representation with a justified range.
Prevent it next time
Derive bounds for intermediate results, not only stored results.

Check your understanding

1. Compute the average of integer values 8, 9, and 10 without losing the fractional policy. Explain where conversion belongs.

Show worked solution

The sum is 27, safely representable as int for these fixed inputs. Use (double)sum / 3 to request floating division; the result is 9.0. For arbitrary data, also prove the sum cannot overflow, or use a checked accumulation strategy. Converting only the final integer quotient would preserve any prior truncation.

2. Explain why sizeof(int) == 4 is a platform assumption, and replace it with a useful portable requirement for accepting 30000.

Show worked solution

sizeof counts C bytes and says neither the number of value bits nor a universal representation. To require that 30000 is representable as int, compare the domain requirement with INT_MAX; ISO C already guarantees int can represent at least through 32767. For a serialized 32-bit field, document its encoding and use exact-width support only after checking availability.

Key takeaways

  • Expression types determine arithmetic before assignment.
  • Prove intermediate ranges and validate signedness changes.
  • Use documented limits; storage size alone does not define a portable data format.

Official references