dmitgu

Category:

2. Условности языков программирования высокого уровня и факты «низкого» уровня

. К оглавлению . Показать весь текст .

Тут можно углубляться в нюансы – например, арифметические действия нужно всё же определить как подпрограммы через строковые операции, а не считать, что они готовые функции для строк типа «1678». И можно построить предыдущую программу, не обращаясь к арифметическим операциям, а используя: 

i = Comp(«Мама мыла раму», x);

Для получения меток так:

mark = «[next_1_» ⋅ i ⋅ «]»;

И метки [next_1_1], [next_1_2] ставить рядом в программе – чтоб они приводили к одному и тому же переходу. А для случая:

i = Comp(«Папа строил баню», x);

генерировать уже метки [next_2_0], [next_2_1], [next_2_2].

Перепишем программу уже без использования арифметических операций:

i = Comp(«Мама мыла раму», x);

mark = «[next_1_» ⋅ i ⋅ «]»;

goto mark;

[next_1_1];

[next_1_2];

i = Comp(«Папа строил баню», x);

mark = «[next_2_» ⋅ i ⋅ «]»;

goto mark;

[next_1_0];

Return = «Маме выдать печеньки»;

goto «[end]»;

[next_2_0];

Return = «Папе выдать пива»;

goto «[end]»;

[next_2_1];

[next_2_2];

Return = «Что происходит?»;

[end].

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

mark = «[next_1_» ⋅ i ⋅ «]»;

то мы сводим их к более примитивным – где используется одна операция с двумя аргументами. Просто мы для удобства пишем такой «программный текст» на языке «среднего уровня», а на самом деле подразумевать под этим будем «откомпилированную» программу. Например, программная строка на языке «среднего уровня»:

n = m_1 + m_2 × m_3;

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

m_ multiplication1 = m_2;

m_ multiplication2 = m_3;

m_ multiplication_back = «[mark_1]»;

goto «[multiplication]»;

[mark_1];

m_ sum1 = m_1;

m_ sum2 = m_ multiplication_return;

m_ sum_back = «[mark_2]»;

goto «[sum]»;

[mark_2];

n = m_ sum_return;

Тот, у кого есть опыт программирования, засомневается, правильно ли, что оператор goto используется для обращения к подпрограммам (запрограммированным заранее функциям). Но в реальности на уровне «железа» компьютера все переходы происходят по аналогу goto. Дело лишь в синтаксисе и соглашениях в языках высокого уровня, которые и создают отличия между использованием goto для обращения к подпрограммам и «просто» переходам при написании программ.

А именно, в «прикладном» программировании принято, что на языке высокого уровня переход к подпрограммам (к любым их частям) «снаружи» никогда не происходит при помощи «простого» goto. «Снаружи» к подпрограмме можно обращаться только с использованием стека вызова подпрограмм, внося туда адрес для возврата – вместо чего мы использовали чуть выше присваивание:

m_ sum_back = «[mark_2]»;

goto «[sum]»;

[mark_2];

Принято (в практических соглашениях о программировании вызовов подпрограмм), что переход происходит только в начало подпрограммы. И выход из подпрограммы – только к адресу возврата, сохраненному в стеке. В приведённом примере (без стека) это

[mark_2];

И при этом стек при возврате сокращается на отработанный вызов подпрограммы.

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

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

Я так подробно рассказал о практических соглашения в отношении организации вызова подпрограмм и возврата из них, чтобы пояснить на данном примере, что «внутренняя кухня» работы программ на уровне «железа» противоречит некоторым распространённым представлениям о «правильном» программировании, хотя именно к «примитивным» и «грубым» командам вроде goto сводится работа программ на уровне «железа», после компиляции программного текста с языков высокого и среднего уровня в машинный код. 

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

Аналогичная ситуация – с «невидимыми снаружи» именами «локальных» переменных подпрограмм, делением подпрограмм по уровням их «видимости» для вызовов и т.п. На «машинном» уровне всё равно получаются переменные и подпрограммы с глобальной видимостью (адреса в памяти) и к переменным в стеке вызова подпрограмм можно на «машинном уровне» обращаться так же свободно и «глобально», как и к остальным переменным с хитрыми «правилами видимости». Потому что ограничения этих правил действуют только на условном уровне языков высокого и среднего уровней – но исчезают после компиляции. 

То, как появляются «неожиданные» переменные при «компиляции» мы рассмотрели на примере раскрытия строки программы

n = m_1 + m_2 × m_3;

И разобрали тот код, который этой строке реально должен соответствовать.

Я не буду углубляться здесь в нюансы программирования стеков, арифметических операций, правил компиляции и т.п. – это давно выполнено системными программистами и может быть аккуратно «переведено» в математических учебниках в соответствии с принятыми для математики стандартами. Я – всего лишь любитель математики, не получающий за это денег и не имеющий поэтому достаточно времени для решения таких огромных по трудозатратам (но не по принципиальным трудностям) задач. А в одиночку подобные объемы работ не мог выполнять (для логики и арифметики) даже такой великий профессионал, как Гильберт. Задача по реализации программы Гильберта в отношении теории алгоритмов является задачей для всего математического сообщества, и должна быть решена совместными усилиями этого сообщества. 

Кратко изложу, всё же, как можно написать программы в данной модели программирования для сложения и умножения «в столбик»:

Сначала надо инвертировать цифры в строке. То есть – переписывать цифры «наоборот» - когда последняя будет 1-й, предпоследняя – 2-й и т.д. Это можно делать в цикле, наращивая «контрольную» строку control на 1 символ конкатенациями типа: 

control = control ⋅ Chr(0);

Используя её нарастающую длину для доступа к символам строки x, например, методом:

Simb = Str(x, len(control), 1);

А полученные символы соединять конкатенацией в обратном порядке, пока не будет получен пустой символ, который обнаружим операцией Comp(Simb, ⊖) – тогда выход из цикла.

А затем для операции сложения вопрос сведём к прибавлению числа с первой цифрой, не равной 0, и остальными нулями, вот так, примерно:

945678 + 230400 = 945678 + 400 + 30000 + 200000

Умножение сводим к сложению простых произведений:

(945678 + 230400) = 945678 × 400 + 945678 × 30000 + 945678 × 200000

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

Заметим, что стандартное (для практики) оформление подпрограмм с использованием стека не требуется для простых задач – кроме того, можно вообще обойтись без использования подпрограмм. Потому, что легко доказать (программисты давно доказали), что любые «рекурсивные» вызовы подпрограмм всегда можно заменить на обычные вызовы немного других подпрограмм. 

Где слово «рекурсивные» тут имеет программистский смысл – когда одна и та же подпрограмма вызывается снова и снова, до того, как произошёл возврат после исполнения предыдущих вызовов данной подпрограммы. А если рекурсивных вызовов нет, то нет никакой разницы – переходить к подпрограмме для её исполнения и продолжения затем (после исполнения данной подпрограммы и возврата из неё) исполнения основной программы, либо же никуда не переходить, а вместо оператора перехода goto иметь текст всей этой подпрограммы «на месте» и исполнять его без перехода куда-либо. 

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

Error

default userpic

Your reply will be screened

Your IP address will be recorded 

When you submit the form an invisible reCAPTCHA check will be performed.
You must follow the Privacy Policy and Google Terms of use.