K&R C Exercise 2-1: Determine Ranges of Integer Types

Exercise 2-1. Write a program to determine the ranges of char, short, int, and long variables, both signed and unsigned, by printing appropriate values from standard headers and by direct computation. Harder if you compute them: determine the ranges of the various floating-point types.

Approach

The exercise splits neatly into two halves. The straightforward half reads the pre-computed constants from <limits.h> and <float.h> — the headers that exist precisely for this purpose and that every real C program should use. The harder half derives those same limits from scratch using only bit manipulation, without consulting the headers at all.

The core trick is that ~0 (bitwise NOT of zero) sets every bit to 1, producing the maximum value of any unsigned type. Right-shifting that all-ones value by 1 clears the sign bit, giving the maximum signed value. The signed minimum then follows from two’s complement: there is always one more negative value than positive, so min = -(max + 1). All the arithmetic is deliberately kept in unsigned types to avoid undefined behaviour; the result is cast to signed only at the point of printing.

Solution

/* K&R Exercise 2-1: ranges of integer and floating-point types
 * Compile: gcc -ansi -Wall exercise2-1.c -o exercise2-1
 */
#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void)
{
    /* ---- From standard headers ---- */
    printf("=== From <limits.h> ===\n");
    printf("char:           %d to %d\n",   CHAR_MIN,  CHAR_MAX);
    printf("unsigned char:  0 to %u\n",               UCHAR_MAX);
    printf("short:          %d to %d\n",   SHRT_MIN,  SHRT_MAX);
    printf("unsigned short: 0 to %u\n",               USHRT_MAX);
    printf("int:            %d to %d\n",   INT_MIN,   INT_MAX);
    printf("unsigned int:   0 to %u\n",               UINT_MAX);
    printf("long:           %ld to %ld\n", LONG_MIN,  LONG_MAX);
    printf("unsigned long:  0 to %lu\n",              ULONG_MAX);

    printf("\n=== By direct computation ===\n");
    /* ~0 sets all bits; >> 1 on unsigned clears the sign bit -> signed max */
    {
        unsigned char uc = ~0;
        signed char sc = uc >> 1;
        printf("char:           %d to %d\n", -(sc + 1), (int)sc);
        printf("unsigned char:  0 to %u\n",  (unsigned)uc);
    }
    {
        unsigned short us = ~0;
        short ss = us >> 1;
        printf("short:          %d to %d\n", -(ss + 1), (int)ss);
        printf("unsigned short: 0 to %u\n",  (unsigned)us);
    }
    {
        unsigned int ui = ~0;
        int si = ui >> 1;
        printf("int:            %d to %d\n", -(si + 1), si);
        printf("unsigned int:   0 to %u\n",  ui);
    }
    {
        unsigned long ul = ~0;
        long sl = ul >> 1;
        printf("long:           %ld to %ld\n", -(sl + 1), sl);
        printf("unsigned long:  0 to %lu\n",   ul);
    }

    printf("\n=== Floating-point from <float.h> ===\n");
    printf("float:  %e to %e  (precision: %d decimal digits)\n",
           FLT_MIN, FLT_MAX, FLT_DIG);
    printf("double: %e to %e  (precision: %d decimal digits)\n",
           DBL_MIN, DBL_MAX, DBL_DIG);

    return 0;
}

Understanding the Bit Tricks

Here is what the direct-computation steps look like in detail for a 32-bit int. The same logic applies to every other integer type — just substitute the width.

Step 1 — set all bits with ~0:

unsigned int ui = ~0;

0   in binary: 00000000 00000000 00000000 00000000
~0  in binary: 11111111 11111111 11111111 11111111
                                           = 4,294,967,295  (UINT_MAX)

Every bit is 1. As an unsigned int this is 232 − 1 = 4,294,967,295, the largest value the type can hold.

Step 2 — clear the sign bit with >> 1:

int si = ui >> 1;

11111111 11111111 11111111 11111111   (ui, before shift)
01111111 11111111 11111111 11111111   (after shift right by 1)
                                           = 2,147,483,647  (INT_MAX)

Right-shifting by 1 moves every bit one position toward the LSB and fills the vacated top bit with 0. The result is the largest positive value a 32-bit signed integer can hold.

Step 3 — derive the signed minimum:

INT_MIN = -(si + 1) = -(2,147,483,647 + 1) = -2,147,483,648

In two’s complement there are 232 = 4,294,967,296 distinct bit patterns. Half (231) represent non-negative values (0 through INT_MAX) and the other half represent negative values (INT_MIN through −1). That means there is always one extra negative value: INT_MIN is −2,147,483,648 while INT_MAX is only 2,147,483,647. Never write -INT_MAX when you mean INT_MIN — they differ by 1.

Why unsigned arithmetic? Unsigned types wrap on overflow in a well-defined way in C. Signed integer overflow is undefined behaviour, so the computation must stay unsigned until a safe cast. The char and short cases are safe because C’s integer promotions silently widen them to int before any arithmetic, but the int and long cases technically rely on two’s complement behaviour. All modern hardware (and C23 by specification) guarantees two’s complement, so in practice this is fine — but in portable production code you would use the header constants.

Compile and Run

gcc -ansi -Wall exercise2-1.c -o exercise2-1
./exercise2-1

Sample Output

On a typical 64-bit Linux system (int is 32-bit, long is 64-bit). Windows differs: long stays 32-bit there, so its row matches the int row.

=== From <limits.h> ===
char:           -128 to 127
unsigned char:  0 to 255
short:          -32768 to 32767
unsigned short: 0 to 65535
int:            -2147483648 to 2147483647
unsigned int:   0 to 4294967295
long:           -9223372036854775808 to 9223372036854775807
unsigned long:  0 to 18446744073709551615

=== By direct computation ===
char:           -128 to 127
unsigned char:  0 to 255
short:          -32768 to 32767
unsigned short: 0 to 65535
int:            -2147483648 to 2147483647
unsigned int:   0 to 4294967295
long:           -9223372036854775808 to 9223372036854775807
unsigned long:  0 to 18446744073709551615

=== Floating-point from <float.h> ===
float:  1.175494e-38 to 3.402823e+38  (precision: 6 decimal digits)
double: 2.225074e-308 to 1.797693e+308  (precision: 15 decimal digits)

Both sections print identical ranges. That agreement is the whole point of the exercise: the bit tricks and the header constants must match, confirming you have computed the limits correctly. Note that FLT_MIN / DBL_MIN are the smallest positive normalised values — the negative end of the float range is -FLT_MAX.

What This Exercise Teaches

  • Standard limit macros<limits.h> and <float.h> are the correct, portable way to query type ranges in production C. Hard-coding 32768 or 2147483647 is a portability bug waiting to happen.
  • Bitwise NOT ~0 — sets every bit in any integer type. Assigned to an unsigned variable, it gives that type’s maximum value, regardless of width.
  • Right-shift to extract the signed maximum — shifting an all-ones unsigned value right by 1 clears the sign bit. The assignment to a signed variable then gives INT_MAX (or the equivalent for other types).
  • Two’s complement asymmetry — signed types always have one more negative value than positive. INT_MIN = -(INT_MAX + 1), not -INT_MAX. Forgetting this causes off-by-one bugs in boundary checks.
  • Platform variance — type sizes vary between architectures and operating systems. Always check the standard headers rather than assuming sizes.

Set Up Your C Environment

To compile and run this solution you need GCC installed:

← Exercise 1-24  | 
Chapter 2 Solutions  | 
Exercise 2-2 →

Book:

The C Programming Language, 2nd Ed — Kernighan & Ritchie

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>