Why Programmers Count Starting from Zero?

Share

The reason why programmers count starting from zero, in many cases, is because there are some low-level cases in which starting from zero is useful, and because high-level applications are built upon these low-level programs, they tend to use the same 0-based index concept, which means that now everything starts from zero instead of starting from one.

To understand this better we need to understand what are these low-level scenarios where zero-based indexing is useful.

Let's consider, for example, a list of N items. In some cases, the way this is programmed in a computer is a data structure called an array, which, in some languages, like C, is a structure tightly packed in memory, such that if one item occupies 4 bytes of memory, then an array of 10 items in C will occupy exactly 40 bytes in memory.

Memory is byte-addressable, which means each byte in memory has an unique address. With 4 bytes (32 bits), we can address 4 gigabytes of memory. This is why old 32-bit CPUs could only use 4 gigabytes of RAM.

In order to use an array in C, we need to know the memory address (which byte) the array starts. Since an address is 4 bytes in a 32-bit CPU, this address needs to be stored in a data structure of 4 bytes. This data structure that stores the memory address of something is called a pointer. If we had a 64-bit CPU instead, the pointer would require 8 bytes instead of 4, but the idea is the same.

So we have a pointer that points to the start of the array, and the array contains only its items, tightly packed in memory. Let's think about what this means for a moment.

A 16-bit unsigned integer can store a number from 0 to 65535 using 2 bytes of memory. These are called uint_16 for short. If we have a single uint_16 that starts at memory address 0x00 00 00 03, and it has 2 bytes, then 0x00 00 00 04 is the address of its second byte. If we have another uint_16 immediately after it, the address of its first byte will be 0x00 00 00 05, and s on.

For example, if the first uint_16 had the value of 42 (101010 in binary) and the second had the value of 9999 (10011100001111 in binary), we would have the following situation in memory:

0x03      0x04      0x05      0x06
0000 0000 0010 1010 0010 0111 0000 1111

If we have 10 of these packed together in an array, that's 20 bytes.

To access the first value (and this is important), we need a pointer that points to 0x03, and each value is 2 bytes long, we take 0x03 and 0x04 and interpret it as a uint_16.

How do we access the second value, which starts at 0x05?

We take the pointer to the start of the array, 0x03, and we add to it the size of a uint_16 in bytes. Since it's 2 bytes:

0x03 + 0x02 = 0x05

How do we access the Nth value? We follow the same process, we just need to multiply the size in bytes by N, except that, when N is 1 (the first value), we must add ZERO to the starting address of the array. So our math is going to be "start + size times (N - 1)."

0x03 + 0x02 * (N - 1)
0x03 + 0x02 * (1 - 1) = 0x03 + 0x00 = 0x03
0x03 + 0x02 * (2 - 1) = 0x03 + 0x02 = 0x05
0x03 + 0x02 * (3 - 1) = 0x03 + 0x04 = 0x07

Low-level programming languages like C provide syntax to conveniently do this sort of math.

// allocates in memory an array for 10 elements
uint_16 my_array[10];

// sets the first value to 42
my_array[0] = 42

// sets the second value to 9999
my_array[1] = 9999

// sets the 10th (last) value to 100
my_array[9] = 100

// declares a pointer
uint_16 *pointer_to_one_item;

// sets the pointer address to the 2nd address of the array
pointer_to_one_item = my_array + 1;

// sets the second value of the array to 33
*pointer_to_one_item = 33

// sets the third value to 34
my_array[2] = my_array[1] + 1

Note: C doesn't actually have a type called uint_16. There's uint16_t in the <stdint.h> header. The idea is the same, however.

As we can see above, my_array[0] uses the address of the first element, which is the same address as the address at the start of the array. As this can become extremely confusing very quickly, we normally just count everything from zero. So this is called the zeroth element instead.

Note that in C, all pointers have a type, and every type has a known size, so C lets us do things like pointer + 1 or array + 1 to add the size of the type to the address stored in a variable, i.e. pointer + 1 is one element after the pointer. Generally we use the term offset to avoid confusion as well.

Also note that my_array[0] + 1 is different from my_array + 1. In C, my_array is of type uint_16* (a pointer), while my_array[0] is of type uint_16 (a number), so what happens when you use + varies from one to another. This confusing is mainly fault of the language, though. It is a terrible language to learn and use. If you're planning to learn low-level programming, I recommend learning Zig. Nobody uses Zig yet because the language is still in beta, but it's way easier to learn than C will ever be.

The number between [ and ] is called index. The square brackets themselves are sometimes called an index operator.

Exceptions

There are a few cases where arrays do not start at zero in programming.

Notably, in Lua (a Brazilian-made programming language), indexes start at 1.

Comments

Leave a Reply

Leave your thoughts! Required fields are marked *