Потому что так удобно компьютеру
В проектах мы периодически говорим, что компьютер почти всё начинает считать с нуля, а не с единицы. Это значит, что первый элемент массива вызывается командой arr[0], второй — arr[1], а шестой — arr[5]. Объясняем, почему так.
Память, переменные и первый байт
Чтобы что-то посчитать, компьютеру нужно место, куда он будет записывать результаты подсчёта. Это место — какие-то ячейки памяти.
Физически ячейка памяти — это транзистор, у которого может быть два состояния: открытый или закрытый (как краны с водой). Эти состояния мы называем «логический ноль» и «логическая единица».
Минимальная ячейка, в которой может лежать единица или ноль, называется бит. Но в бите можно закодировать только 1 или 0 или «да/нет». Чтобы кодировать что-то более сложное, нужно взять не один бит, а группу. В компьютере принято объединять биты в группы по 8, такая группа называется «байт».
Получается, что байт — это 8 транзисторов памяти, которые компьютер интерпретирует как единое целое (например, число). Внутри байта находятся логические нули или единицы, стоящие в любом порядке.
Кодирование происходит в двоичной системе: в зависимости от расположения логических единиц и нулей подразумеваются разные числа в привычной нам десятичной системе счисления. Например:
00000000 — ноль, минимальное значение байта
00000001 — один
00000010 — два
00000011 — три
00000100 — четыре
00000101 — пять
…
11111110 — двести пятьдесят четыре
11111111 — двести пятьдесят пять, максимальное значение байта
Обратите внимание, что минимальное значение, которое можно хранить в одном байте — не единица, а именно ноль.
Логика компьютера
Теперь представьте: компьютер исполняет программу, где ему нужно обойти какой-нибудь массив и что-то там подсчитать. Для обхода массива ему нужна область памяти для подсчёта. Что происходит дальше:
- Процессор находит свободное место в памяти и запоминает: «Вот тут у меня будет лежать счётчик для этого массива».
- Место в памяти обнуляется, чтобы там не было никакого мусора. Мало ли там какие данные лежали от прошлой программы?
- Обнулённый счётчик — это 00000000, то есть ноль.
- Раз счётчик уже есть и у него есть валидное значение «ноль», то компьютер начинает считать именно с нуля.
Благодаря тому что компьютер начинает считать с нуля, в 8 бит он может поместить 256 значений: от 0 до 255. Если бы компьютер считал от 1 до 255, в 8 бит поместилось бы 255 значений — то есть на одно меньше.
Можно ли всё-таки считать с 1?
Можно, но это необычно. Чтобы компьютер начал считать всё с единицы, нам нужно объяснить ему, как это делать. Например, подход может быть таким:
- Мы создаём массив, в котором элементов на один больше, чем нам нужно. Например, если мы собираемся там хранить 100 чисел, делаем массив на 101 элемент.
- Когда начинаем его заполнять, то первым элементом мы указываем единицу: arr[1] = 15, вторым — двойку и так до конца.
- Так мы заполним 100 элементов, которые можно начинать считать с единицы, а нулевой элемент останется незаполненным.
При этом с точки зрения компьютера в массиве всё равно будет 101 элемент, которые он будет начинать считать с нуля. Но человеку так может быть удобнее, когда номера элементов совпадают с его порядковым номером.
Как ошибаются из-за счётчиков с нуля
Самая частая ошибка при начале счёта с нуля — путать между собой длину массива и индекс последнего элемента. Следите за руками:
- Мы сделали массив на 100 элементов.
- Первый элемент массива — это arr[0], а последний — arr[99].
- Когда мы запросим длину массива, то в ответ получим число 100.
- А если мы обратимся к arr[100], то получим ошибку, потому что элемента с индексом 100 в массиве нет.
Эта же ошибка встречается и в циклах, когда нам нужно перебрать все индексы с первого до последнего. Чаще всего начинают считать с единицы и пропускают нулевой элемент.
Как грамотно обойти массив с помощью счётчиков на разных языках (и не запутаться в единицах и нулях)
Представим, что у нас есть массив arr[], в котором хранится 100 чисел, и нам нужно вывести их на экран.
Классический алгоритм такой:
- Заводим переменную для счётчика, обычно её обозначают буквой i.
- В i записывается ноль. Начинается цикл.
- Внутри цикла берётся массив. Из него достаётся элемент под номером i, то есть соответствующий текущему номеру прохода цикла. Так как в начале алгоритма в счётчике ноль, то мы получим нулевой элемент цикла (то есть по-человечески — первый).
- Когда шаг цикла выполнен, в переменную i добавляется единица.
- Теперь цикл повторяется, но из массива достаётся не нулевое, а первое значение (то есть по-человечески — второе).
- Цикл повторяется до тех пор, пока i меньше, чем длина массива.
Есть хитрость в выходе из цикла: значение счётчика i должно быть меньше, чем длина массива (а не «меньше или равно»).
Дело в том, что длина цикла измеряется по-человечески — 1, 2, 3 и далее. А счётчик работает по-компьютерному — 0, 1, 2… Это значит, что в массиве из 100 чисел длина массива будет 100, а максимальное значение счётчика — 99.
Получается, что если повторять цикл вплоть до i < arr.length, то он завершится корректно при i == 99. Это стандартная практика. А если крутить цикл до i <= arr.length, то цикл исполнится 101 раз, и при i == 100 будет ошибка: этого элемента в массиве нет.
Пример такого кода в JavaScript:
В Python:
В C++:
Обход массивов вообще без счётчиков (и связанных с ними нулей)
В некоторых языках есть более компактная версия этой конструкции, которая буквально означает «Обойди этот объект». Вот примеры в JavaScript:
В Python:
Здесь нужно смотреть на слово in, что буквально означает «по всем составляющим этого объекта». В разных языках у него разное значение
- В JavaScript в переменную i положат номера элементов массива arr[], то есть эта переменная будет работать как счётчик. Фраза i in arr означает «для каждого номера элемента массива arr: положи этот номер в переменную i».
- В Python фраза i in arr означает «возьми сами элементы массива arr и положи их по очереди в переменную i»
То есть JavaScript использует for…in для простого управления счётчиками, а Python вообще избавляется от понятия счётчика и сразу отдаёт в переменную то значение, которое он сейчас перебирает.
Если совсем просто:
- Допустим, у нас есть массив arr[], который состоит из чисел 2, 12, 85, 6.
- В конструкции на JavaScript в переменной i будут числа 0, 1, 2, 3.
- А в конструкции на Python в переменной i будут 2, 12, 85, 6.
Но это если уже совсем угореть. Всё, хорош, и так понятно: компьютеры считают с нуля.