Zero-based arrays are counter-intuitive for a while, but if you deal with a lot of data, you typically realize that it's a small price to pay to make manipulation much easier in many contexts. For instance, if you have a ring buffer of size N and an unwrapped position P, the wrapped position is:
Zero-based: P % N
One-based: ((P - 1) % N) + 1
It might seem trivial, but each +/-1 is an opportunity for confusion and a bug nest. With zero-based arrays, it's often the case that the only required +/-1's are when producing and consuming human-readable one-based text.
The next stop on the zero-based epiphany train is the realization that a convenient way to store a range is a { first, first_past } tuple. The size of the range is (first_past - first). The whole-array range is { 0, size }, while a simple empty range is { 0, 0 } (zero is often the default initialization, simplifying things further.)
Both elements are indices, so they can be similarly manipulated, compared and range-checked, making many 'if' clauses easier to think about and verify. If there is a bug, it often ends up being harmless because of the arithmetic properties of this scheme.
Once you start dealing with multiple ranges, the advantages are even more obvious. Two ranges are adjacent iff (first_a == past_b || first_b == past_a). The intersection of two ranges is { max(first_a, first_b), min(past_a, past_b) }, which is nonempty iff they overlap. An array of M adjacent ranges is stored as a uniform (M+1)-tuple.
This realization has become so second-nature for me that I'm probably overlooking four or five even better examples here.