В проектах мы периодически говорим, что компьютер почти всё начинает считать с нуля, а не с единицы. Это значит, что первый элемент массива вызывается командой 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:
for (var i = 0; i++, i<arr.length) { console.log(arr[key]) }
В Python:
for i in range(len(arr)):
print(arr[i])
В C++:
for(int i = 0; i < length; i++)
{
std::cout << arr[i];
}
Обход массивов вообще без счётчиков (и связанных с ними нулей)
В некоторых языках есть более компактная версия этой конструкции, которая буквально означает «Обойди этот объект». Вот примеры в JavaScript:
for (var i in arr) { console.log(arr[i]) }
В Python:
for i in arr:
print(i)
Здесь нужно смотреть на слово 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.
Но это если уже совсем угореть. Всё, хорош, и так понятно: компьютеры считают с нуля.