Second, even though memory is now quite cheap, that doesn't mean we should use a large number of bits on integers. In particular, if you use too many bits, then it's cumbersome to store large numbers of integers. Imagine if you have a million integers (a very large database, for example). If each integer requires 1000 bytes of memory, that's one billion bytes of memory.
You want the number of bits per integer to allow you to represent a wide range of usuable numbers, but in a few number of bytes.
Historically, memory was a big consideration. Thus, integers used to be stored in 16 bits. 16 bits allows for 65,536 different bit patterns (usually called 64K), thus up to 65,536 different representable values.
64K isn't a lot of values, but memory was a consideration. It wasn't until about the 1990s when 32 bit CPUs, and therefore 32 bit int values, were much more commonly seen. This allows values up to about 4 billion bitstring patterns. That's still not very large, but is more reasonable.
The number of bytes used in an int has often been a function of memory and CPU. The more bits you need, the more pins need to appear on a CPU, and the more memory you need to store a large number of ints.
Let's assume that a typical unsigned int uses 32 bits of memory. We define the following terms, to be used throughout the discussion of signed and unsigned ints.
This is the number of possible values (as opposed to represented values) that a given representation allows. In most cases, the number of possible values is the same as the number of possible representations. It's always the case that the number of values is less than or equal to the number of possible representations.
With signed ints, you'll discover that the number of possible values is less than the number of representations.
The minimum value allowed by the representation. All other values are greater than this value.
The maximum value allowed by the representation. All other values are less than this value.
Number of Bits | Number of Values | Min Value | Max Value |
16 | 2^{16} | 0 | 2^{16} - 1 |
32 | 2^{32} | 0 | 2^{32} - 1 |
N | 2^{N} | 0 | 2^{N} - 1 |
You'll notice that the minimum value for unsigned int is always 0. Thus, the max value is 2^{N} - 1 for N bits.
There's two ways to convince yourself 2^{N} - 1 is the correct maximum value.
It may be easier to answer this question in base 10, first. Imagine you have an odometer that reads 1000. That's 1 followed by 3 zeroes. Subtract 1 from 1000, and you get 999, which is three 9's. Now, Suppose you have 1,000,000 (1 followed by 6 zeroes). Subtract 1 from that and you get 999,999 (six 9's).
What's the general pattern for subtracting 1 from 1 followed by N zeroes? It is N 9's.
Similarly, subtract 1 from 1 followed by 2^{N} is N 1's, Thus 2^{N} - 1 is the formula for N 1's.
Even though these quantities are never negative, sometimes you want to have negative numbers for error conditions. For example, suppose you want to write a function that determines where a certain character appears in a character array. It returns the index of that character.
Suppose the character isn't there. What value should you return? You can't return -1 because you can't represent negative values for variables with type, unsigned int.
One possibility is to return the array size, since the array size is larger than all valid indices of an array. However, the problem with that approach is that this number varies from array to array. -1 is nice because you can use it regardless of the size of an array.
A possible solution is to use a second variable that is set to 0 if not found and 1, if found.
This value can sometimes be used as an error value. However, I'm not sure how standard it is on C compilers.