Pull to refresh

Comments 204

Ой, какие-то притянутые за уши рассуждения о системах счисления и расположении в памяти. Просто при любых действиях с интервалами смещения возникают чуть чаще чем на каждом шагу. Поскольку си делали умные люди для них было естественным что индекс массива - это смещение от начала блока до начала элемента, вот и всё.

А потом начались приколы с расположением мета-информации типа счётчиков ссылок или записи размера по отрицательным смещениям.

Да, но на момент разработки С до потребности в этих приколах ещё четверть века оставалось. Писалось это на (и для) весьма недешёвой мини-ЭВМ, которая с периферией и накопителями занимала несколько 19" шкафов, и имела в те годы аж 64 килобайта ОЗУ в топовой комплектации. Поэтому да, в этой ситуации естественно упрощать в языке всё, что только можно, чтобы добиться максимальной эффективности.

Мета-информация может варьировать в размерах, так что ее место именно на отрицательных смещениях. Так избегается перемещение данных.

смещения возникают при адресной арифметике с указателями и собственно реализации массивов как указателя на положение первого элемента в памяти.

в языках где указатели как таковые явно не очень присутсвуют: mathematica, matlab, lua, ... массивы нормально индексируются с 1.

Смещения возникают везде где есть какие-нибудь координаты. Например, на линейке "3" находится по смещению 1см от "2", а год 2022 - по смещению 22 от 2000. Но, внезапно, по смещению 21 от начала 2-го тысячелетия. И 19-й век это почему-то 18хх. А матлаб прсото сжечь хочется когда нужно отмасштабировать, сместить, наложить диапазоны и т.п.

когда что-нибудь сочитать надо, загибая пальцы, тоже с 0 начинаете?

не надо путать размер набора и смещение элемента

я не путаю, я лишь спросил как обычно считаете в повседневной жизни.

с различными типами индексации при подсчёте элементов и при адресации:

первое яблоко, второе яблоко, третье яблоко, ... всего десять.

возьму пожалуй то, что со смещением два от начала массива. или всё-таки третье?

Одно, два, ..., десять яблок. В жизни никто яблокам номера не присваивает при их подсчёте. Просто сколько яблок переложил столько и пальцев загнул.
Если бы участок решил яблоками разметить разложив их через метр то были бы 0-е 1-е 2-е как деления на линейке

Одно, два, ..., десять яблок. В жизни никто яблокам номера не присваивает при их подсчёте.

мне кажется что в первом предложении произошло ровно то, что как раз отрицается по втором.

мне кажется

Ну в этом вы не ошиблись: "десять яблок" - это не то же самое, что "десятое яблоко".

Вы инкрементирующий десятичный счётчик яблок с массивом путаете :)

Так исторически сложилось.

Да. Ладонь, на которой ни один палец не загнут — это 0.

представьте что вы токарь с 4 пальцами на одной руке. Вам нужно подавать сигналы напарнику. Типа передай инструмент N.(говорить не вариант-в цехе шумно)
Если нумерацию инструментов начинать с нуля, то вы можете адресовать только 15 инструментов. А если с нуля, то 16.

представьте что вы токарь с 4 пальцами на одной руке

Это фрезеровщик :)

А на экране первый пиксель начинается со смещения -0.5.

смещение/расстояние от левого края растра до начала "первого" пикселя = 0

Мейби, не помню деталей. Но чтобы его полностью закрасить приходится добавлять пол пикселя к смещению, иначе будут на половину закрашены два соседних пикселя.

Где как, это у вас графический движок такой. У него логические пиксели, видимо, могут иметь произвольную координату, а затем это всё пытаемся нарисовать с помощью физических пикселей с целочисленными координатами, да ещё антиалиасинг задействован.

Именно это я и имел в виду. OpenGL, DirectX, антиалиасинг.

На экране конкретного физического монитора — возможно.
А вот если вспомнить что как минимум в некоторых ОС — координаты окон считаются по отношению к primary monitor и при этом расположение secondary мониторов не фиксированно в общем случае...

Мне так всю жизнь, с самого детства казалось нелогичным и неудобным что именование времени «пол-четвёртого» это 3:30. Всегда подвисал на этом «так, нужно взять четыре, отнять единицу, прибавить полчаса». Только раздражало это. Как и именование времени в старинном 12-часовом формате. «Три часа пополудни». «Так, нужно взять полдень, прибавить три часа, ага, значит сейчас 15:00»

Не, я понимаю что тут конфликт поколений. Любители такого странного именования выросли в советское время, для них «часы» это синоним механических с циферблатом, отсюда все эти странно звучащие отнимания четвертей и половинок.

Я, как человек рождённый уже на закате Союза, тоже конечно застал эти часы, но всё-таки вокруг уже повсюду окружали электронные 24-часовые, все мои наручные были такими.

И когда меня спрашивают время, всегда принципиально говорю «пятнадцать часов тридцать минут» Хотя, к сожалению, это заставляет подвиснуть уже вопрошающего, если он представитель старшего поколения, увы.

Мне так всю жизнь, с самого детства казалось нелогичным и неудобным что именование времени «пол-четвёртого

Так всё правильно: "пол-четвёртого" = "через полчаса будет четыре", "без десяти пять" = "через десять минут будет пять". Меня не волнует время, которое уже прошло; меня волнует время, которое скоро наступит — и мне надо знать, КАК скоро. Например, у меня в пять важная встреча, и мне надо понять — успею я на метро, или надо хватать такси и нестись пулей. А когда мне говорят "четыре двадцать две", я зависаю, пытаясь понять, сколько до пяти осталось-то.

А если говорят "без четверти пять", а встреча в 18:00 (так в письме написано), не надо делать пересчёты в голове?

Так там всего 6 значений — 13, 14, 15, 16, 17 и 18, маппинг "6->18" заучивается после третьего раза. А в 19-00 нормальные люди уже давно не на работе ;)

Вы так говорите, будто все работают строго 9-18. Бывают предприятия работающие в несколько смен, дежурства. Да и кроме работы есть жизнь: личные встречи, свидания и т.д.

Интересно как выглядел мир, если бы победила двенадцатиричная система счисления, а не десятичная.

"четыре двадцать две", я зависаю, пытаясь понять, сколько до пяти осталось-то

двадцать две минуты пятого

Полчетвёртого ещё ладно. В детстве меня подвешивали определения времени вроде "четверть пятого" или "двадцать минут шестого". С половинами я как-то справлялся на лету (не знаю почему, они по сути не отличаются), а вот такие определения приходилось реально раскручивать в голове. Ага, идёт шестой час, значит полных часов пять и ещё двадцать минут. На мой взгляд, часы со стрелками не объясняют зачем переключаться между количественными и порядковыми числительными. Со стрелочных часов вполне легко считывается "пять двадцать". Ещё можно понять зачем говорить "без пяти пять" или "без четверти пять" - стрелка часовая ближе к пяти (и при этом нет перехода к порядковым числительным), но "двадцать минут шестого" зачем?

"Время второй час ночи"

А ещё б-гомерзкое "четвёртый час", что значит "3 часа"

И 19-й век..

При этом нет нулевого часа в сутках, но есть нулевой километр на дорогах. Считаем это аксиомами и работаем с этим

Это не нулевой час. Это ноль часов.
После рождения Иисуса прошло 0 лет, но шёл первый год нашей эры.

Нулевого километра нет. Есть отметка с некорректным названием "нулевой километр", но сразу от неё начинается первый километр. Участка дороги протяжённостью километр с нулевым номером не существует.

Нулевой километр, это точка.
А первый час это отрезок.
Т.е. нулевой километр это не первый километр -1. Это отдельное понятие.

Тогда элементы одного массива не будут обладать одинаковыми свойствами: один будет 'точка', другой 'отрезок', ... , последний 'остановка'. То есть будут противоречить фундаментальному определению массива как индексированному набору ОДНОРОДНЫХ данных. Хотя для любителей вести счёт (нумерацию) с 0 все как надо, а тех кому привычнее с 1 могут нулевой элемент просто не использовать.

вы не поняли.
Нулевой километр, это не нулевой элемент массива километры. Это отдельное понятие.

пол-четвёртого

Ну есть считать, что полностью это выглядит как "прошла половина четвертого часа", то выглядит вполне логично. Четыре часа еще не прошло, только четвертый идет.

Четыре часа еще не прошло, только четвертый идет.

Даже эта фраза-объяснение звучит немного неуклюже. При беглом чтении она воспринимается нелогичной, мозг на ней невольно спотыкается и заставляет перечитать ещё раз.

Насколько бы она лучше звучала в формате "четыре часа ещё не прошло, только третий идёт"

Или «Час ещё не прошёл, только нулевой идёт»?

"Мама сказала, что придет с работы через четыре часа, а четыре часа еще не прошло, только третий идет, еще больше часа можем мультики смотреть".


Когда время 3:30, третий час после полудня уже прошел, неправильно говорить, что он еще идет. Если днем в 12:42 еще можно сказать, что идет двенадцатый час, а не первый, то ночью в 0:42 как говорить, "время нулевой час ночи"? Просто надо привыкнуть к мысли, что первое число показывает, сколько часов уже прошло, а не какой час идет.
Аналогично с расстоянием, если мы прошли 900 метров, мы идем первый километр, а если 1700, то второй.

Тогда удобнее было бы, чтоб часовая цифра указывала не на час, который прошёл, а на час, который идёт. Чтобы они совпадали.
То есть после полуночи сразу идёт 1:01, никаких "ноль часов".

Если не секрет, а где вы живёте, что вас "повсюду окружают электронные 24-часовые"? Спрашиваю, потому что в каждом, в каждом доме, где я бывал на стене висят именно стрелочные часы.

Настенные часы, как мне кажется, это уже больше элемент интерьера, символ домашнего уюта, дань традиции, если угодно.

Да, я себе их тоже повесил, но я на них почти не смотрю (да и через пару лет после покупки надоело в них менять батарейки, теперь просто висят)

Время считываю из операционной системы компьютера, планшета или телефона - в зависимости от того, с каким устройством работаю в данный момент.

у меня часы висят только в углу экрана. И они там 24 часовые.

В современном русском полтора (="пол-втора") остаётся последним рудиментом системы "получислительных", о которой писал Даль: "счет этот идет до десяти (полтретья, полчетверта, полсема и пр.), затем в круглых десятках и сотнях: полтретьядцатьполчетвертадцать и полтораста, полтретьяста, полосьмаста, полдевятаста и пр." Система эта настолько вышла из употребления, что смысл таких числительных загадывают в ЧГК, и то с неправильным ударением.

В книжке "Russische Sprachlehre für Deutsche" (Рига, 1804) этим русским числительным сопоставляются немецкие, ныне столь же архаичные: "Полтора, anderthalb, полтретья, dritthalb, полчетверта, vierthalb, полпята, полшеста u.s.w." В современном немецком все эти числительные называются на единицу ниже: zweieinhalb вместо dritthalbdreieinhalb вместо vierthalb, и т.д.

Аналогично, и в "Svenskt och Ryskt Lexikon" (Гельсингфорс, 1847) приводятся пары: "Halfannan, halftannat, полтора, полторы; halftredje, полтретья; halffjerde, -femte, -tionde, полчетверта; полпята; полдесята;" -- все эти шведские числительные вышли из употребления, уступив место аналитическим конструкциям en och en halv, två och en halv, и т.д.

Как так получилось, что принятая во многих языках (находятся примеры и из голландского с датским) система числительных -- во всех этих языках распалась без остатка? У нас, у голландцев и у датчан сохранилось лишь "полтора"; у прочих германцев не сохранилось даже этого.

Слово "полтораста" пока ещё употребимо.

в языках где указатели как таковые явно не очень присутсвуют: mathematica, matlab, lua, ... массивы нормально индексируются с 1.

А потом Вы пытаетесь выделить подстроку из строки - или подматрицу из матрицы - и начинаются танцы с бубном...

Вообще же, "ноль" - является естественно выделяемым значением в области чисел, хоть натуральных, хоть целых, хоть каких.

Танцы с бубном будут в любом случае при перетаскивании данных (оссобенно многомерных, просто упакованных в последовательный кусок памяти за неимением человеческих массивов) между двумя разными способами индексации. А внутри каждого определённого способа с 0 или 1 всё нормально.

Во многих 1-based языках есть ещё синтактический сахар для адресации с конца массива отрицательными индексами, там уже с бубном придётся танцевать если индексация начинается с 0, да и "опечаток" с записью в последний элемент как a[N] вместо a[N-1] тоже хватает. Так что куда эти грабли с +-1 перекладывать в начало массива или в конец - разница не велика.

Может просто привык уже пользуясь и вольфрамовской математикой и C+Lua что способы индексации бывают разные, и не считаю что какой-то из них единственно верный. Если нужны указатели и прямая работа с памятью - удобнее смещениями с 0, а ворочать какие-нибудь многомерные массивы в математике, не задумываясь как они там в памяти расположены, иногда удобнее с 1.

Во многих 1-based языках есть ещё синтактический сахар для адресации с конца массива отрицательными индексами, там уже с бубном придётся танцевать если индексация начинается с 0,

В 0-based совершенно логично получается, что нумерация с конца начинается с [-1], потому что [0] уже занят. Фактически, можно представить массив как кольцевой буфер, и индекс натуральным образом будет переходить с головы в хвост при инкременте/декременте, потому что теперь это просто модулярная арифметика натянутая на Z.


Если в 1-based реверсивные индексы начинаются с [-1], то во множестве индексов получается странная сингулярность, ломающая арифметику индексов, и эту сингулярность в каких-то случаях придётся специально обрабатывать. Для чисел одна арифметика, для индексов — совершенно другая, а если они где-то пересекутся, то жди беды.

это опять же всё верно при представлении (реализации) массива как последовательного куска памяти и манипуляциями с указателями.

а когда в более высокоуровневых, да и местами не особо типизируемых языках в массиве вообще содержатся элементы разных типов адресная арифметика указателей не особо работает.

если мне надо убрать с начала и конца по несколько элементов, то меня запись [[5;;-5]] в той же математике вполне устраивает без лишних размышлений куда там +-1 от нулевого элемента надо добавить в начало или в конец.

и реализовывать кольцевой буфер из этих массивов никто в здравом уме не будет, для кольцевого буфера будет будет какая-нибудь отдельная обёртка, которая уже внутри при реализации будет использовать рассововерные смещения от нулевого элемента.

стэк в той же lua [1] - первый элемент, [-1] - последний. вместо "переполнения" через границу стэка чтобы из последнего элемента инкрементом внезапно попадать в первый, или наоборот, уж лучше получить ошибку при наступании на 0.

поэтому там где память, регистры и указатели - смещения от 0, а в вольфрамовской математике и матлабе массивы с 1.

почему у некоторых так от этого подгорает не понимаю.

"Набор из разных сущностей" - массивом не является просто по-определению, не надо плодить лишних сущностей и портить терминологию.

"Сверхвысокоуровневые языки" - штука крайне специфическая, серьёзно изолирующая программиста от "железа" - и далеко не факт, что обеспечивающая обработку возникающих ошибок в случае их возникновения.

Доступ к "последнему элементу стека" - штука уже несколько сомнительная, ибо стек - это сущность с односторонним добавлением/удалением элементов.

Не "Сверхвысокоуровневые", а в любом динамически типизированном языке в массиве лежит что угодно и быть одинаковым по размеру не обязано. а в некоторых и индексу так-то не обязательно быть вообще числом.

Ну так про то и речь что как только абстрагировались от реализации массива в виде прямого доступа к куску памяти по указателю, и перестали компилировать a[b] напрямую в mov eax, [ecx], адресация по смещению от 0 какой-то особой дополнительной радости больше не приносит.

Иногда удобно подсматривать содежимое стэка по порядку снизу, без того чтобы извлекать всё задом наперёд, а потом положить обратно как было.

В той же луа при взаимодействии с С, адресация стэка с обеих сторон жизнь упрощает.

в любом динамически типизированном языке в массиве лежит что угодно и быть одинаковым по размеру не обязано

Это что угодно и не лежит в массиве. В массиве лежат структуры фиксированного размера (4 или 8 байт), которые уже ссылаются на данные в куче, которые эта структура не может вместить.

Не "Сверхвысокоуровневые", а в любом динамически типизированном языке в массиве лежит что угодно и быть одинаковым по размеру не обязано. а в некоторых и индексу так-то не обязательно быть вообще числом.

Праститя, апачиму у Вас "это" - называется "массивом", ане "кортежом"?
Вам глубоко начхать на терминология, да-аа?

Ну так про то и речь что как только абстрагировались от реализации массива в виде прямого доступа к куску памяти по указателю,

...мы абстрагировались от самого понятия массива...

Иногда удобно подсматривать содежимое стэка по порядку снизу, без того чтобы извлекать всё задом наперёд, а потом положить обратно как было.

Праститя, ещё раз - апачиму Вы "стеком" - называете "кольцевой буфер"?
Патамущта Вам так нравиццо - штоп люди Вас не понимали, и штоп Вы демонстративно людей путали?
Апачиму тогда Вы кошку - не называете "собакой", а рыбку - "насекомым"?
Ну так, чиста для удобства?

P.S. При счёте массива "с нуля" для "отрицательные индексы" связаны с положительными гладко, однородно и без исключительных случаев:

index = length + reverse_index

А при счёте с единицы - будет разрыв в нуле и формула будет:

index = length + reverse_index + 1

P.P.S. Ни Матлаб, ни Вольфрам Математика, ни Маткад - не являются языками программирования, ибо это - "системы для решения математических задач". И вообще, судя по дискуссии - сторонники "счёта с единицы" - хотят "красиво выглядящих текстов", а сторонникам "счёта с нуля" - нужна возможность "удобно писать программы".

При этом, когда на Матлабе или Математике - начинают писать программы для эмбедда - неизбежно приходится обратно возвращать ограниченные типы, строгую типизацию - и немедленно начинаются самые разнообразные танцы с бубном вокруг этого.

В математике принято нумеровать, например, члены ряда, с единицы. Я так помню. Например, в формуле суммы ряда, индекс от 1:

Hidden text

Не везде, например:


  • полиномы: a0 + a1x1 + a2x2 + a3x3 + ...
  • ряды, например ряд Тейлора, дискретное преобразование Фурье, и др.
  • 4-вектора в СТО: X = (x0, x1, x2, x4)

Как раз получается, что нулевой член ряда особенный, а массив должен быть однородным?

Нет, нулевой член абсолютно такой же как все остальные: a0 — это, на самом деле, a0x0

Он не зависит от переменной, и в этом его отличие

зависит, просто переменную в 0 степень возвели, а коэффициент не виноват в том что множитель переменная от этого меняться перестала, превратившись в 1.

ровно как и косинус нулевой частоты в интегралах Фурье.

Ой, какие-то притянутые за уши рассуждения о системах счисления и расположении в памяти.

Почему "притянутое"?
Любые типы в каждом конкретном компьютере - занимают вполне конкретное число бит.
И массив из 256 элементов - может быть проиндексирован значением типа uint8.(И, соответственно - аналогично и для других типов.)

Это всё хорошо, но основная причина того что адресовать элементы смещением удобно и правильно лежит не в диапазонах переменных и не в экономии тактов. Это лишь приятные бонусы (хотя и неизбежные, если смотреть глобальную картину).

Гораздо проще, имхо.

Потому что так больше влазит в байтовое кодирование и лишним перекодирование имён не нужно заниматься.

Ну и в качестве примера неэлегантности нумерации с нуля: если элемента в массиве нет, то поиск его индекса возвращает, внезапно, -1, а не 0. А это:

  • Требует использования знакового типа большей точности, вместо беззнакового той же точности.

  • Требуется хитрых манипуляций в условных конструкциях, так как -1 является истиной, а 0 - ложью.

Кроме того, получается неэлегантная асимметрия: первый элемент имеет номер 0, но для обращения к последнему приходится использовать уже -1 или в некоторых языках $-1.

если элемента в массиве нет, то поиск его индекса возвращает, внезапно, -1

-1 действительно выглядит произвольным непонятно откуда взятым значением. N или UINT_MAX хоть какая-то логика была бы.

Требуется хитрых манипуляций в условных конструкциях, так как -1 является истиной, а 0 - ложью.

Вообще, в си функции поиска не возвращали -1. Это в новодельных языках так. И это к авторам этих языков вопрос, почему при наличии у них булевого типа вообще разрешено проверять int на истину/ложь без хитрых манипуляций предварительного сравнения.
Действительно "хитрых" манипуляций требует нумерация с единицы. Во всех формулах приходится вычитать единицу чтобы получить нормальное смещение, с которым можно работать и потом прибавлять единицу обратно чтобы получить номер элемента. Вот это невероятно бесит в !@#$%ом матлабе.

Кроме того, получается неэлегантная асимметрия: первый элемент имеет номер 0, но для обращения к последнему приходится использовать уже -1 или в некоторых языках $-1.

и что же тут неэлегантного? -1 - смещение от конца блока до начала последнего элемента. Это как раз соверешнно логично и естественно.

А потом начались приколы с расположением мета-информации типа счётчиков ссылок или записи размера по отрицательным смещениям.

И что плохого в отрицательных смещениях? Смещение - это просто расстояние от точки до точки. Если в качестве точки отсчёта взято начало блока элементов, то что расположено до блока будет по отрицательным смещениям. Да, "очень элегантно", обращаться к данным, расположенным до блока, по неотрицательным смещениям, только ради того чтобы смещение было неотрицательным. Если же мета-информация и счётчики ссылок расположены внутри блока то и смещение будет неотрицательным при нормальной "индексации с нуля"

N или UINT_MAX хоть какая-то логика была бы.

Без разницы, через какое имя вы будете записывать -1.

почему при наличии у них булевого типа вообще разрешено проверять int на истину/ложь без хитрых манипуляций предварительного сравнения

Чтобы не делать очевидной проверки на пустоту вручную.

Во всех формулах приходится вычитать единицу чтобы получить нормальное смещение

И зачем вам "нормальное смещение"? Обращайтесь по индексам.

и что же тут неэлегантного?

Асимметрия.

И что плохого в отрицательных смещениях?

При работе с памятью нужно очень аккуратно с ними работать, иначе всё сломается. И такие случаи не редки. Программист не всегда в курсе сколько надо "отступить назад", чтобы корректно скопировать или освободить память.

У использования нуля и адресации с единицы появляется противоположная сторона, заключающаяся в том, что INT_SIZE_MAX - 1 перестают быть представимыми в рамках заданного "бинарного" типа. В результате абстракции все равно текут.

Странный аргумент, против -1, для 64 битных чисел, если ssize_t должно хватить всем. А вот то,что из-за граничных случаев, компилятор не может лучше оптимизировать код, такое вполне себе случается.

При работе с памятью нужно очень аккуратно с ними работать, иначе всё сломается

Просто не надо с ними работать. Никогда. Это внутренняя кухня компилятора, куда посторонним вход запрещен.

В нормальных языках это реализуется в библиотеках, а не хардкодится в компиляторе.

В нормальных языках это хардкодится в компиляторе , а не реализуется в библиотеках.

Обоснования заявления выше будут?

-1 и UINT_MAX идентичны.

Например для 16-битного числа -1 кодируется как 1111_1111_1111_1111 (0xFFFF).

А с точки зрения машинного кода нет разницы для знаковых и беззнаковых чисел на части основных операций. Сложение, вычитание, умножение и большинство битовых операций выполнены таким образом, что знак не важен. Для сравнения, деления и смещения вправо знак важен и эти операции обычно имеют два варианта.

https://ru.wikipedia.org/wiki/Дополнительный_код

Кроме того использование валидного числового значения для возврата ошибки является рудиментом ранних языков программирования. Во всех более современных ошибка с более абстрактной стороны строго типизированна, а с низкоуровневой стороны хранится как отдельное значение.

Элегантное обращение к внутренним метаданным языка программирования не требуется. Это слишком редкая и слишком специфичная штука.

Отсутствие элемента в массиве ошибкой не является.

Это метаданные не языка, а структуры данных. А структур разных миллионы.

Отсутствие элемента в массиве ошибкой не является.

Если вы про выход за границы массива, то является ошибкой. В зависимости от конкретного языка программирования и/или флагов компиляции эта ситуация может по разному обрабатываться (возвращение ошибки или бросок исключения) или даже игнорироваться полностью (как в чистом няшном Си в релизной сборке без специальных флагов).

Кстати, судя по обсуждению выше следует уточнить, что вы вообще подразумеваете под массивом. Следует разделять массив как тип (1), массив как данные на стеке (2), ссылку на массив в куче, слайс (обычно ссылка+длинна) и вектор. Обычно под просто массивом подразумевают (1) и (2). И в любом случае речь идёт о данных в оперативной памяти. Метаинформация о размере массива существует на этапе компиляции и не хранится в рантайме. Если мы конечно говорим о компилируемых языках.

В случае если у вас ссылка на массив в куче, то метаданные о размере этого массива хранятся в аллокаторе. В таком случае не очень понятно, зачем вам туда лезть. Очень сильное колдунство.

Если мы говорим о векторе, то вектор может давать доступ к своим метаданным тем или иным образом. Как минимум ссылку на начало массива (или слайс), а также отдельно длину массива и иногда общий объём занятой памяти. Но детали могут меняться в зависимости от реализации и конкретного языка программирования.

-1 и UINT_MAX идентичны.

Полагаться на это не стоит:

long long a = -1;
long long b = UINT_MAX;

Извините, но если элемента в массиве нет, то поиск его индекса возвращает по-хорошему не -1 и не 0, а Nothing/null/nil (либо в крайнем случае кидает исключение).

Может это и по-хорошему, но использовать ради такой операции дополнительный тип слишком накладно.

Только потому что кулхацкеры на строгость типов кладут с прибором, и смешивают тип значений с типом хранения. Тип — это набор допустимых значений и операций. Все эти int и size_t в качестве типов индекса — это хаки, по сути. В 0-based, строгий тип индекса — это [0..size). В 1-based — [1..size+1). Не больше и не меньше. Значение [-1] не является допустимым значением для типа "индекс", это значение совершенно другого типа NotFound (хоть и имеет такой же storage type). Но так как строгой системы типов в язык не завезли, то в ход идут ухищрения.


Если уж по-хорошему, то операция поиска в массиве должна возвращать либо option, либо кортеж (bool, index), либо специальный тип SearchResult, а никак не null/nil/npos/MAX_INT/-1.

Вообще использование -1 как кода особой ситуации неэлегантно. Для наких случаев придуманы элегантные Алгебраические Типы Данных - Option/Maybe.

0 для отсутствия индекса вполне элегантно, быстро и компактно. В отличие от ADT, который превращается в discriminated union.

А в чём конкретно проблема discriminated union, если он нормально реализован и, тем более, хорошо подлежит оптимизациям (TL;DR, если у вас есть дырка из невозможного битового представления, то это невозможное значение и будет представлять собой пустой вариант)?

Option<NonZero<Num>> ничем не отличается от просто Num.

А с чего вдруг NonZero<Num>, если речь, как раз, о том, чтобы оставить 0 как индекс начинающего элемента?

И? То, что оптимизация, на которую я сослался, есть, не значит, что надо изобретать бессмысленные структуры, как Option<NonZero<Num>> (на самом деле, вполне себе осмысленные, но не для данной задачи).

Суть моего комментария -- противопоставление Вашему

0 для отсутствия индекса вполне элегантно, быстро и компактно. В отличие от ADT, который превращается в discriminated union.

Контрпример, демонстрирующий, что сум-тип -- очень даже удобно.

Оптимизация были упомянуты дополнительно как что-то, что, в целом, хорошо дружит с последними, но не как инструкция к написанию нелогичного -- для задачи инексирования -- типа.

В дополнение к тому, что индексация с нуля удобна, она ещё и естественна для многих прикладных вещей.

Можно ещё исключение кидать, как в Питоне.

Если по логике программы искомый элемент должен присутствовать в массиве, а его отсутствие - это ошибка, то можно. Если же отрицательный результат поиска вполне нормальная ситуация, то не стоит - это как затрудняет понимание логики программы, так и вносит ненужные накладные расходы на выброс и обработку исключения.

Ну там накладные расходы небольшие, а если в большинстве случаев его кидать не надо, то получится даже быстрее, чем с дополнительной проверкой результата на -1, потому что она будет выполняться каждую итерацию, а обработчик исключения - гораздо реже.

Это конкретно про Питон. Там в целом исключения используются достаточно часто, например, цикл в конце кидает StopIteration под капотом. Ну и вообще там подход "It's easier to ask for forgiveness than for permission".

Но я согласен, что всё должно быть в разумных рамках и не влиять отрицательно на читабельность кода.

Это убийство для производительности.

Ну для Питона производительность падает всего в разы, а не на порядки, и то только в случае выброса исключения; да и числодробилок на чистом Питоне никто не пишет. Первый же сетевой запрос нивелирует разницу в производительности.

Вроде речь идет не только о Питоне, а об общем случае?

Да, вы правы. Я привёл его как пример языка, где реализована альтернатива возврату -1 или 0 при поиске.

Нет, внезапно, если вы иногда профилируете, то оказывается, что сетевые запросы часто ничего не нивелируют, особенно если ресурсы в одном ДЦ. Да, сложно быть медленнее чем сеть, но питон периодически справляется.

UFO just landed and posted this here

В нормальных языках программирования (к коим Javascript никак не относится) поиск индекса элемента в массиве возвращает опциональный индекс, а не спец-значение из области значений индекса.

Требуется хитрых манипуляций в условных конструкциях, так как -1 является истиной, а 0 - ложью

Хитрая манипуляция - это != -1 ?

Если нужно проверить, не является ли результат нулем, то тоже лучше будет написать != 0, так читабильней

вообще, альтернативы всего 3: с 0, с 1, или позволить выбирать. У всех вариантов есть достоинства и недостатки. Нужно один раз определиться (хотя бы для конкретного языка), и все. index==-1 не такая страшная штука (signed int не критично больше, чем unsigned)

Ну да, всего в 2 раза больше.

Насколько помню, таки обычно не больше (имеется ввиду размер в памяти). Просто диапазон сдвинут "в минусовую" сторону до середины

Например в C#:

short: хранит целое число от -32768 до 32767 и занимает 2 байта

ushort: хранит целое число от 0 до 65535 и занимает 2 байта

Индексы в массиве не могут быть отрицательными, так что половина диапазона теряется.

Увы, чудес не бывает, и если в массиве может быть N элементов, а функция поиска должна дополнительно обрабатывать ситуацию когда элемент не найден, то спектр значений по-любому расширяется, и в исходный диапазон уже не входит. Принцип Дирихле никто не отменял. Например, при нумерации элементов с 1, массив из 65536 ячеек вполне укладывается в 16-битную адресацию, но вот номер последнего элемента в 16-битный int уже не влезет. Проблема никуда не исчезает, а лишь смещается в редко используемый диапазон значений, увеличивая риск быть пропущенной при тестировании и остаться лежать граблями замедленного действия.

В Си пошли на сознательный шаг для «особых» значений явно требовать особый (более мощный) тип. Если следовать этой философии с первого дня изучения нового языка программирования, то она намертво закрепляется в памяти и потом выглядит естественно привычно. Ключевой момент — с самого начала явно оговорить эту ситуацию и объяснить причины.
Например, у классиков K&R уже в первой главе чётко объясняется, почему функция getchar возвращает не char, а int.

В C++ возвращается npos, эквивалентный UINT_MAX:

npos is a static member constant value with the greatest possible value for an element of type size_t.

This value, when used as the value for a len (or sublen) parameter in string's member functions, means "until the end of the string".

As a return value, it is usually used to indicate no matches.

Так что можно и обойтись без знакового типа вдвое большего размера.

Максимальное беззнаковое и -1 это одно и тоже. По крайней мере в рамках C/C++ с их слабоватой типизацией.

Не совсем: для максимального беззнакового диапазон индексов расширяется вдвое по сравнению со знаковым (например, для 16 бит беззнаковый индекс может быть до 65535 (или, если учесть специальное значение UINT_MAX как "индекс не найден", до 65534), а знаковый индекс только до 32767.

Сложно говорить о строгости этих значений при слабой типизации. UINT_MAX для signed int будет побитово идентичен -1. INT_MAX для unsigned int будет равен половине от UINT_MAX. Проблема в неявном преобразовании между signed int и unsigned int.

Это понятно. Но если функция поиска в строке возвращает 16-битное знаковое, вы из неё не сможете вернуть позицию больше 32767. А если беззнаковое - то вернёте до 65534.

Давайте по другому. В языках со слабой типизацией разница между знаковыми и беззнаковыми на самом деле лишь косметическая. Разница только в выборе инструкций для некоторых операций. Пара деталей: https://habr.com/ru/post/696666/comments/#comment_24872228

Сначала начал писать длинный пост, а потом проверил. Вы правы, прошу прощения. Что для знаковой переменной, что для беззнаковой, выражение &str[i] генерирует одну и ту же ассемблерную инструкцию add.

С другой стороны, объявлять функцию поиска по строке как возвращающую знаковое значение означает либо отказаться от половины возможных значений (и уменьшить в два раза допустимую длину строки), либо возвращать (на примере 16-битных целых) значения вроде 40000 как -25536, что очень удивит читающих этот код впоследствии.

Это всё в предположении, что битность арифметики указателей совпадает с битностью используемых целых. Если она больше, то там ещё разные инструкции будут (zero-extension vs sign-extension).

Лучше уж пусть эта функция возвращает беззнаковое, а в случае неудачного поиска - UINT_MAX.

Нет, использование простого int в подобных контекстах это просто признак yolo-кода. Ну или этот код старше, чем большинство из нас. Это если мы говорим о Си и его производных.

В современных языках есть возможность использовать всякие Option, Result. Ну или в языке принято использовать исключения. Чисто технически первое будет означать, что требуется задействовать не один регистр для передачи результата, а два. Вариант с исключениями разумеется будет гораздо дороже. Раскрутка стека и вот это всё.

Если она больше, то там ещё разные инструкции будут (zero-extension vs sign-extension).

Это уже зависит от архитектуры. Варианта два: отдельные операции для дополнения (то, что вы говорите), либо специальные операции для каждой размерности. Первое будет в ARMv6 и новее, второе в amd64. Что-то более экзотичное я не проверял. Ну или даже оба варианта, я все случаи с разными компиляторами и инструкциями не проверял.

До сих пор возмущен, что «паскалевские» индексы по любым перечисляемым типам не реализуют в других языках. И ещё больше возмущен случаями, когда вместо такого массива предлагается использовать словари.

В тех же плюсах сделать массивы с произвольными диапазонами(аналог array), совсем не проблема, просто это особо не кому не нужно.

Сделать то можно что угодно, и действительно, это особо никому не нужно, так как на исполняемую программу не влияет – только на читабельность. Но вот код с SO по подсчету количества разных символов в строке на Си:
int counts[26] = { 0 };
for (i = 0; i < len; i++) {
    counts[(int)(str[i] - 97)]++;
}
а на Паскале можно было бы:
counts: array ['a'..'z'] of integer;
...
counts[str[i]] := counts[str[i]] + 1;
Как по мне, то польза тут от «паскалевских» индексов очевидна. Только об этом я и хотел сказать.

Тоже самое на C здорового человека:

int counts['z' - 'a' + 1] = { 0 };
for (i = 0; i < len; i++)
    counts[str[i] - 'a']++;        

Как по мне, все равно хуже ) Но я чаще вижу варианты с магической «97».
int counts['z' — 'a' + 1] = { 0 };

Ну да, ну да...

А сколько надо? От a до z включительно именно столько элементов.

Столько и надо. По-моему, это был подкол в сторону тех, у кого нумерация с нуля исключает возню с прибавлением-вычитанием единицы.

Не согласен, что не нужно. Типичный С/С++ программист просто не знает насколько это удобно. Во-первых, код более читабельный. Во-вторых, более безопасный (размер массива всегда равен размеру энама). В-третьих, добавляются проверки на этапе компиляции. Вот я набросал пример: https://rextester.com/VQDRP17163

Если в этом примере в энам добавить meFour четвертым значением, то во-первых, код не скомпилируется, т.к. скажет, что значение cMyEnumText должно содержать больше значений. А во-вторых, цикл автоматически будет итерироваться по всем значениям, включая новое добавленное. В С++ при добавленнии значения в энам нам надо будет: 1. не забыть поправить размеры массивов, иначе упадет в рантайме 2. не забыть поравить руками циклы, иначе не проитерируемся.

Зато в плюсах я видел бесчетное количество раз, когда пишут:

enum class { Some1, Some2, Last }

Где Last - буквально костыль чтобы сделать так, как оно в паскале. При этом все равно приходится руками лепить ущербные тайпкасты.

p.s. Вообще в паскале есть еще одна крутая вещь, это встроенные эффективные множества. Но не будем о грустном.

Энумы Паскаля - одна из самых крутых фич языка, наряду с безгеморными строками и динмассивами (очень было кайфово с ними после голых сей)

В PHP можно реализовать через произвольный класс с Iterator, ArrayAccess, Countable

Нумерация с нуля: LISP 1.5APL (допускает выбор при запуске программы).

Нумерация с единицы: APL, Бейсик, ранний Фортран.

В лиспе нет массивов. А в бейсике зависит от версии.

Кстати, бейсик для Apple ][ – единственный известный мне язык, где необъявленный массив получал размер по умолчанию. В таком случае его длина равнялась 11 элементам с индексами от 0 до 10.

› длина равнялась 11 элементам с индексами от 0 до 10.

Хм. Выглядит как попытка угодить и нашим и вашим (и тем кто с нуля начинает и тем кто с 1). Чтоб ненароком не выйти за пределы массива.
Интересно, а на объявленный массив выделялось ровно сколько объявлено? Или тоже +1 элемент "на всякий случай"?

Это же вопрос семантики объявления. Оператор DIM A (N) декларировал массив с N+1 элементами от 0 до N. То же самое с многомерными массивами. Так что вообще-то на практике обычно пропадало много нулевых элементов, что курьёзно для компьютера с 48 килобайтами оперативной памяти.

Лично Билл Гейтс, кстати, писал этот интерпретатор.

Также упрощается и становится более интуитивной реализация многомерных массивов:

array[row * num_cols + col]

Эта формула что конкретно символизирует? Если на уровне самого языка программирования высокого уровня, то индексироваться одним индексом вместо нескольких вам в большинстве языков не позволят. А если на уровне реализации в машинном коде, то там не будет таких арифметических вычислений индекса, так как, во-первых, участвует начальный адрес, не равный нулю, а во-вторых, могут применяться всякие индексные регистры и т.п.

Это с точки зрения программиста, у которого в распоряжении одномерные массивы, но необходима структура данных, имеющая схожий с многомерными массивами интерфейс.

Весьма гипотетическая картина.

Можно привести практический пример, в котором вы ограничены одномерными массивами?

Если мы говорим про C lang, то разницы, на самом деле, нет. Но в других языках, вроде Python мы получаем лишние объекты, которых могли бы избежать (практический пример приводить, если честно, не хочу, просто всегда одномерные использую)

Ну это не ответ :) Вы, по существу, написали, что любите использовать одномерные массивы вместо многомерных. Я уважаю Ваши привычки, но согласитесь, что это не повод для проектирования языков программирования. Во всяком случае, в питоне многомерные массивы точно есть.

Не из коробки. В питоне у вас из коробки может быть только список списков

Если мы говорим про проектирование, то тут стоит использовать одномерные массивы с намеком, что все хранится в одном блоке памяти (что может быть не верно для некоторых языков). Для многомерных можно использовать специальные структуры данных и inline функции

P.S. Но по сути согласен, тут мы мнениями делимся

Не совсем. В питоне в качестве элемента массива можно задать любой другой объект, в том числе и еще один массив. Таким образом получается массив массивов (или не массивов, объекты могут быть любыми, не всегда однородными). Это имеет несколько последствий, не всегда понятных тем, кто ожидает простую матрицу n*m.

Справедливое замечание, но тогда надо начинать с того, что и одномерный – не массив и был.

Питон и не врет - он сразу называет это списком объектов. Использование в качестве массива чисел - всего лишь частный случай, потому что число в питоне тоже объект.

А потом через полтора года ваш код дадут джуниору добавить какую нибудь ерунду и он полысеет, пока разберется, что значат все эти страшные оптимизированные конструкции. Или просто перепишет все с нуля. И это на python, от которого никто не ждет высокой производительности и экономности, но ожидается читаемость и легкая поддерживаемость.

Можно порадоваться, что не на перле пишите, там логику работы "оптимизированных" программ бывает не может объяснить сам автор на следующий день.

В моем коде многомерные массивы только в numly увидеть и можно

В C нет из коробки многомерных динамических массивов, те же матрицы произвольной размерности надо эмулировать через одномерные массивы.

Мешает. Этот синтаксис работает, только для массивов с известной размерностью на момент компиляции. Каким образом это должно работать с динамической размерностью? (хинт: на этапе компиляции вы не знаете размерности, чтобы вычислить индекс элемента)

Писал в институте Гауссово преобразование матрицы. На линейном массиве работает гораздо быстрее, чем на двухмерном

Вы же не путаете двумерный массив с массивом массивов?

В С перебор строки или столбца на линейном массиве можно сделать одной операцией - прибавлением смещения. В двумерном - умножение и смещение.

UFO just landed and posted this here
Например, на 8086 память выделялась системой кратно 16 байтам (сегменту). Из выделенной памяти четыре байта расходовались на организацию списка выделенных фрагментов. Соответственно, создавая массив символов 16х16 в виде char** мы расходуем 48+16*32 = 560 байт, из которых 44 будут служебными. Формируя же линейный массив char* из 256 символов и используя вычисленную адресацию получим расход 272 байт и 4 байта накладных расходов. То есть, хоть формально мы и не ограничены в создании многомерных массивов, но в реальности для экономии памяти выгоднее использовать однимерные с вычислением адреса.
Собственно, такая структура, как куча, в языке появилясь как раз для снижения накладных расходов при выделении небольших блоков памяти. Программа запрашивала у системы большой фрагмент памяти и распределяла её сама с меньшей дискретностью.

Ну Вы сами, собственно, и ответили на своё возражение в нём самом. Конечно, никто не говорит о том, чтобы выделять область памяти с дескриптором на каждую строку двумерного массива по отдельности. Выделяется обычно целиком куча или стек.

Формула символизирует то что многомерный массив - всего лишь частный случай вложенных интервалов. С которыми, кстати, приходится работать не только через операторы [] или [][] (реализующие для удобства простейшие случаи). Где есть интервалы, диапазоны и т.п. - там и смещения вылазят на каждом шагу. Вот когда принуждают обращаться к элементам не по смещению а от с потолка взятого начального индекса (1, 42 и т.п.) - то и хочется такой язык сжечь.

В нормальных языках это выглядит так:

array[row,col]

Если нумеровать элементы с 1, то в нулевом элементе можно хранить длину массива. Очень удобно иногда, не надо ничего вычислять каждый раз при обращении к нему или держать где-то отдельно таблицы длин.

А что, все массивы у нас имеют тип, эквивалентный условному size_t?

Почему-то мне кажется что людей, которые считают логичным хранить длину массива в перемешку с элементами и адресовать элементы не смещением, такие мелочи не волнуют

Очень удобно иногда

Забавно, что в статье как бы намекается, что "сейчас, в век терафлопсов и гигабайтов в каждом мобильнике все эти заморочки с нумерацией с нуля кажутся неудобных архаизмом". Хотя есть миллиарды устройств на всяких там ARM Cortex M0, и даже более мелких микроконтроллерах. А терафлопсы на мобильниках расходуются так коряво, что тормозят даже самые простые приложения...

Во времена, когда память исчислялась килобайтами, основной код писался руками на ассемблере и каждая операция была на счету, то в коде заниматься постоянно отниманием этой единицы, что бы получить адрес смещения - лишний головняк.
Посему и в Си не стали усложнять, эффективность была важнее.
В Паскале кстати по умолчанию индексация массивов была с единицы и редко кто ее переопределял.

В Паскале нет индексации по умолчанию, там всегда указывается нижняя граница.

Заниматься отниманием единицы в коде не нужно, потому что смещение в машинном коде относится не к началу массива, а к базовому адресу. Если, допустим, массив 32-разрядных чисел находится по адресу 0x8888, то базовый адрес в таком случае будет 0x8884. Тут единственный головняк для низкоуровневого программиста – с умножением индекса на 4 (но в 1950-е годы, когда всё это безобразие начиналось, память адресовалась словами, и в таком умножении не было нужды).

В Object Pascal были динамические массивы, где границы можно было не указывать.
arr: array of integer;
Юмор в том, что индексы в них начинались с нуля )

Массивы как дети, их возраст при рождении тоже отсчитывается не сразу с 1 года, а с 0 лет, по крайней мере в большинстве стран.

Вообще это называется “первый год жизни”, а про “0 лет” может сказать только программист :)

Именно потому у обывателей вечные проблемы понять и запомнить что 18хх - это 19-й век и второе тысячелетие начинается с 1 января 2001 а не 2000, а в Корее с рождения ребёнку сразу исполнялся 1 год но сейчас это пофиксили, и прочие прелести что возникают с масштабированием, сдвигом и наложением диапазонов при нумерации не смещением. Вот, смотря на комментарии, удивляет конечно количество желающих тащить этот ужас в программирование, считающих его удобным и рассуждающих что отцы-основатели лишь за такты боролись, просто сделав нормально. Мне кажется, что раньше было лучше.

"Первый год жизни" да, но на вопрос "сколько ребенку лет" обычно отвечают скажем "3 месяца", что является сокращением от "0 лет 3 месяца"

Это количественные и порядковые числительные. Прожитых лет 0, год жизни первый.

Статью я конечно же не читал, но я много писал когда-то на ассемблерах и в частности на Intel 8051. Подобных вопросов у меня не возникало, потому что там множество команд сравнения с нулем. Ценная команда - djnz - "декремент, переход если не ноль". Цикл организовывался в две (!) строчки. Что-то вроде:

mov r1, #8

label: <тело цикла>

djnz r1, label

Эти массивы с нуля и с единицы сейчас создают целую проблему при кодогенерации из MATLAB в C/C++
Жесть начинается когда пытаешься создать интерфейс на C к параметрам в MATLAB.
На мгновение забудешь про индексацию в MATLAB с единицы, и получаешь гейзенбаг на многие дни.

Еще интересный случай это работа с граничными значениями. Если сравнивать границы для байтов, то можно написать i <= NUM при этом NUM тоже будет байтом, если прибавить единичку это все сломается.

Да, и? Для краевого значения будет всегда истина, но обычно это константа или переменная, код для которой пишется в общем виде. Вопрос, в том какого типа должна быть NUM если это константа, и если это переменная. А то так, даже банальный цикл while (i < NUM) работать не будет, без расширения в более длинные типы, которых нативно может и не быть.

Всё хорошо сделано, оставляйте так!

..ишь, с нуля им видите ли не нравится)))

С аргументацией Дейкстры насчет наименьшего натурального числа...
"У них" наименьшее натуральное —ноль, но у нас-то — единица! :)

Неотрицательные начинаются с нуля. С единицы начинаются позитивные числа. Забудьте всё, чему вас там пытались научить. Натуральными можно считать и те и другие. Вообще Википедия напоминает, что для случаев, когда это важно, вы можете обозначить это:

\mathbb {N} _{0} \mathbb {N} _{1}

Нет никакого "у них" и "у нас", есть только образование низкого качества. У вас.

Нет, просто образование и кандидатский диплом советские, когда учили математике, а не хамству :)

Для вас - апеллирую не к умным книгам (сомневаюсь, что вы их сможете понять), а к Википедии :)

UFO just landed and posted this here
Все гораздо проще: «naturalis» — с лат. «естественный», то что можно увидеть естественным/природным образом — глазами. Ноль предметов увидеть глазами нельзя, поэтому когда придумывали определение для натуральных чисел (в эпоху антропо- и бого-центризма), ноль туда не включили. А более математические доводы за натуральный ноль подъехали позже, когда традиция уже сформировалась. Даже в XIX веке Пуанкаре и Кронекер выступали против натурального нуля. Вообщем здесь то-ли религиозное, то-ли еще какое-то легаси.

Хотя, по правде сказать, ноль действительно особенное число (среди всех вещественных)…

Не все массивы начинаются с нуля. В sql, например, с единицы. В фортране вообще индекс может быть даже отрицательным. Во многих скриптовых языках какой назначишь, такой и будет. Так что какв заголовке статьи опущено слово "в языке си". И встает вопрос, а почему таки оно опущено? А потому, что если его написать, то статья вырождается в один, ЕМНИП, абзац из стандарта на язык, где говорится, что массивы, как таковые, и постфиксный оператор "квадратные скобки" -- это всего лишь синтаксический сахар, а на самом деле компилятор всегда оперирует суммой указателя с типом, соответствующим типу элемента и индекса, приведенного к указателю этого типа. И формула даже приведена для тех, кто слаб в грамматике английской речи.

В AWK массивы начинаются с единицы... Очень "удобно". AWK не намного моложе, чем C.

У AWK аналогия с аргументами коммандной строки в bash, а они начинаются с 1

Как говорил эксперт по выбору из двух вариантов (по совместительству солист группы Бредор): обе альтернативы не лишены недостатков, поэтому категоричный выбор не приносит удовлетворения.

Недавно я писал код по транспонированию матрицы, в нём удобнее работать с массивом нумерованным от нуля. Так при расчёте индексов при перестановках исключается постоянное добавление и вычитание единицы, код становится значительно менее перегруженным и более понятным.

С другой стороны, когда код работает с порядковыми числительными из реального мира, зачастую гораздо нагляднее выглядит нумерация с единицы. Банальный пример - словарь месяцев, которые и в человеческом календаре нумеруются с единицы, как и в большинстве языков программирования (кроме, кажется, JS). Тут же, не отходя от календаря стоит вспомнить, что дни недели зачастую нумеруются с нуля, либо и так и так.

Если язык программирования предоставляет выбор, это удобно.

Рассуждения по поводу нумерации индексов в C выглядят как попытка рационализировать постфактум то, что и так легко объясняется арифметикой указателей.

Все просто, раньше любой приличный язык программирования позволял прямо в коде в любом месте делать вставки на ассемблере и этим реально пользовались, а там реально для обращения к элементу массива необходимо было использовать смещение в памяти поэтому первый элемент естественно с нулевым смещением.

Кроме того раньше очень экономно использовали все доступные числовые значения, например тип char это всего 256 значений, вот просто так отказаться от использования значения 0? Да вы так все полимеры ...

Как там писал Пелевин, "Миром правит не тайное ложе, а явная лажа".

C++, как и прочие Java, идут корнями в Си, а Си, изначально, - это структурированный макроассемблер. Наивный, тупой и безответственный - за это он и становится первым ЯП для любой новой железки. И все аналогии в нём максимально простые и ассемблерные.

A * a;
a[i] == (*(A*)((void*)a + sizeof(A) * i));

Fortran, как и прочие MATLAB, языки глубоко специальные, и оперируют терминами предметной области. А в предметной области математических вычислений принято всё делать с единицы. Проще научить компилятор пересчитывать индексы, чем толпу математиков правильно переписать их формулы, да ещё на богохульную нумерацию с нуля! Вы бы ещё придумали, матрицы переписать в линейные массивы!!!

А задание области определения массива - это, конечно, интересная фишка, - но это синтаксический сахар над, собственно, областями. И если эти самые области больше нигде не встречаются, а индексация в языке реализуется через словарь, - требуйте возврата средств, вас обманули.

При этом няшный Си это в некоторой степени ассемблер для PDP-11. Особенно если мы говорим о версиях до стандартизации.

а что, нынче программистов не учат что в любой системе счисления первое число - 0, а не 1? для меня такого вопроса никогда не возникало - в первом семестре научили что все считается с нуля.

автору могу подкинуть идею для новой статьи - "почему все положительные координаты начинаются с нуля?".

Если 0 — это первое число, то предыдущее (-1) — нулевое?

И? Где ответ на вопрос? Где по вашей ссылке вообще упоманание о "первом числе в любой системе счисления"?

Нумерацию массивов как структур данных в программировании лучше начинать с 0 или с 1 ? А кому как будет удобнее для реализации конкретного алгоритма. В реальном мире арифметический счёт идёт с 1 и человек начал считать в десятичной системе потому что на руках у него было и остается 10 пальцев. Если структуры данных в массивах отражают объекты материального мира, то здравый смысл подсказывает что их счёт и нумерацию логично вести с 1. Если же массивом моделируется некая структура элементов, которые могут вставляться и удаляться, тогда нужно поддерживать как список занятых элементов, так и список свободных элементов, якоря которых могут быть, например, соответственно в минус первом и нулевом элементах, т.е. нумерация в массиве будет начинаться с -1. В конечном итоге все определяется конкретной задачей и личными предпочтениями программиста.

В некоторых алгоритмах фильтрации сигнала чрезвычайно удобно использовать «симметричные» массивы вида a[-N]...a[0]...a[N]. Жаль, что не все языки такое позволяют.

Кто (или что) в этом случае мешает вам использовать 2 одинаково объявленных массива и работать с ними также как если бы они были зеркальными в одном массиве ? Все пожелания в язык программирования "не запихнешь", да и делать этого не следует. Главный принцип в программировании это простота и наглядность, которые в первую очередь должны быть в самом языке и соблюдаться при разработке алгоритма и кода программы.

Никто не мешает, конечно же. Как никто не мешает выделить кусок памяти и заниматься арифметикой указателей. Но один массив с диапазоном индексов [-N, N] даёт самую элегантную реализацию алгоритма.

Главным основанием индексации от нуля все-таки являлас полнота использования размерности индекса. Для индекса, размером byte можно было адресовать массив в 256 ячеек, при адресации от 1 уже только 255.

Ну и для многомерных массивов при адресации от 1 добавляется операция вычитания на каждую доп. размерность в рантайме. Тогда это было важно.

Все остальное лирика.

Путаница возникает из-за смешения двух разных понятий - индексов и их границ. В случае адресации к массиву индексами являются целые числа - 0, 1, 2,... Но их границы - это другой тип данных. 0] - это правая граница нуля, [1 - левая граница единицы и т.д. Обращаться к значениям массива можно либо через границы индексов, либо через сами индексы.

Январь - это первый месяц года, понедельник - первый день недели. Тут мы обращаемся к индексам. Когда же мы ссылаемся якобы на нулевой элемент массива, то на самом деле адресуемся к границе между индексами. Это нулевое смещение от левой границы массива.

В идеале можно было управлять типом адресации через вид скобок. Например, квадратные - обращение к границам, круглые - к индексам. Тогда А[0] == А(1).

Проблема различия элементов (интервалов) и их границ актуальна не только для индексов, но и для других типов данных. Для дат, например. Задать строго конец (или начало) суток обычно нетривиальная задача.

Когда-то программировал на ассемблере. Вот решил ты сделать аналог массива и выделил для него память. Как найти адрес нужного элемента? Если это однобайтовые значения, то просто сложить адрес массива и номер элемента. Для 2, 4, 8 байтовых значений нужно просто индекс предварительно побитово сдвинуть влево 1, 2, 3 раза. А если начинать с единицы, то нужно учитывать нулевой элемент... Или постоянно выполнять лишнее вычитание или адрес массива хранить со смещением. А зачем заморачиваться, если с нулём можно не заморачиваться? А на счёт лишнего инкремента. Даже на относительно быстром процессоре из прошлого Z80 при частоте 3,5 мегагерца инкремент занимает 4 такта процессора.

В ассемблере в таком случае нужно будет просто адресоваться не от начала массива, а от предыдущего адреса.

Другое дело, что при ручном программировании на ассемблере более естественно проходить циклы в обратном порядке до нуля, чтобы выходить из них по флажку нулевого результата, и всё будет плясать от этого.

тут всё очень просто:

  1. Языки низкого уровня — оперируют абсолютной адресацией физической памяти, которая ВСЕГДА начинается с нулевой границы (так проще считать смещения и — дешевле изготавливать процессор используя флаги переноса разрядов). А если это не так — используется выравнивание. (Особенно у intel, с его сегментными. Ну, можете специально пихать искусственно выдуманные смещения, для усложнения своей жизни, никто не запрещает испытывать боль). Даже если это маскируют метками.

  2. Языки истинно высокого уровня — оперируют естественной нумерацией. Которая максимально приближена к естественному положению вещей. Где "0" — это отсутствие, а не начало. На то они и "высокого уровня".

  3. И — да: "от 1 до 10" — это количественно "10", а не "9". Инвалиды-токари, инвалиды-столяры и прочие ампутанты — идут дальше сокращать свой счетчик....

    И ни какого шаманства и двоемыслия.

Sign up to leave a comment.

Articles