Глава 24
Языки высокого и низкого уровня
Писать программы в машинных кодах — все равно что есть с помощью зубочистки. Кусочки еды настолько малы, а процесс столь трудоемок, что обед может длиться вечно. Точно так же фрагменты машинного кода выполняют элементарные вычислительные задачи: загрузку числа из памяти в процессор, прибавление к нему другого числа, сохранение результата. На самом деле сложно понять, какую роль они играют.
По крайней мере, мы значительно продвинулись относительно описанного в начале главы 22 примитивного использования переключателей пульта управления для ввода в память двоичных данных. Тогда мы разобрались с написанием простых программ, позволяющих использовать клавиатуру и дисплей для ввода и просмотра шестнадцатеричных байтов машинного кода. Разумеется, это далеко не последнее из возможных улучшений.
Как вы знаете, байты машинного кода соответствуют некоторым коротким мнемоническим кодам, таким как MOV, ADD, CALL и HLT, благодаря чему компьютерные команды отдаленно напоминают английские слова. Эти мнемонические коды часто сопровождаются операндами, уточняющими действие машинной инструкции. Например, машинная инструкция для микропроцессора 8080 перемещает в регистр B содержимое ячейки памяти, 16-битный адрес которой хранится в паре регистров HL. Вот ее краткая запись.
MOV B, [HL]
Конечно, писать программы на языке ассемблера проще, чем в машинных кодах, однако микропроцессор этот язык не понимает. Я уже объяснял, как писать ассемблерные программы на бумаге. Когда вы будете готовы запустить подобную программу, вы вручную преобразуете инструкции на языке ассемблера в машинный код и введете их в память.
Прекрасно, если компьютер мог бы выполнить это преобразование вместо вас. Компьютер с процессором 8080, работающий под управлением операционной системы CP/M, предусматривает для этого все необходимые инструменты. Это работает следующим образом.
Сначала вы создаете текстовый файл, который будет содержать вашу программу, написанную на языке ассемблера. Для этого можете обратиться к программе CP/M ED.COM или к текстовому редактору, позволяющему создавать и изменять текстовые файлы. Предположим, вы создали текстовый файл с именем PROGRAM1.ASM. Тип ASM говорит, что файл содержит программу на языке ассемблера. Сам файл может выглядеть примерно так.
В этом файле встречается пара новых для нас команд. Первая — ORG, которая не является частью системы команд процессора 8080; она указывает, что адрес следующей команды должен начинаться с ячейки 0100h. Как вы помните, именно с этого адреса CP/M загружает программы в память.
Следующая команда — LXI (Load Extended Immediate — непосредственная загрузка регистровой пары), загружающая 16-битное значение в пару регистров DE. В данном случае это 16-битное значение указывается в качестве метки Text в нижней части программы перед оператором DB (Data Byte — байт данных), который мы также встречаем впервые. За этим оператором могут следовать несколько байтов, разделенных запятыми (как в приведенном выше примере), или некоторый текст в одинарных кавычках.
Команда MVI (Move Immediate — передача непосредственного операнда) перемещает значение 9 в регистр C. Команда CALL 5 вызывает функции CP/M. Функция 9 отображает на экране строку символов, начинающуюся по адресу, который находится в паре регистров DE, и заканчивающуюся значком доллара. (То, что текст в последней строке программы завершается значком доллара, может показаться странным, однако именно так работает операционная система CP/M). Последняя команда, RET, завершает программу и возвращает управление системе CP/M. (На самом деле это лишь один из способов завершения программы CP/M.) Оператор END обозначает окончание файла на языке ассемблера.
Итак, у нас есть текстовый файл, содержащий семь строк текста. Теперь его нужно ассемблировать, то есть преобразовать в машинный код. Раньше мы делали это вручную. Однако теперь можем использовать предусмотренную в CP/M специально для этой цели программу-ассемблер ASM.COM. Она запускается из командной строки CP/M следующим образом.
ASM PROGRAM1.ASM
Программа ASM просматривает файл PROGRAM1.ASM и создает новый файл с именем PROGRAM1.COM, который содержит машинный код, соответствующий написанным нами на языке ассемблера командам. (Этот процесс включает еще один этап, однако он не имеет особого значения в описываемом примере.) Теперь вы можете запустить файл PROGRAM1.COM из командной строки CP/M. На экране отобразится текст Hello! — и все закончится.
Файл PROGRAM1.COM содержит следующие 16 байт.
11 09 01 OE09 CD05 00 C9 48 65 6C6C6F 21 24
Первые три байта — инструкция LXI, следующие два байта — инструкция MVI, следующие три байта — инструкция CALL, следующий байт — инструкция RET. Последние семь байт — это ASCII-коды, соответствующие пяти буквам слова Hello, восклицательному знаку и значку доллара.
Действия ассемблера, такого как ASM.COM, сводятся к чтению ассемблерной программы, часто называемой файлом с исходным кодом, и написанию исполняемого файла, содержащего машинный код. По большому счету ассемблеры довольно простые программы, поскольку между мнемоническими ассемблерными обозначениями команд и машинным кодом существует взаимно-однозначное соответствие. Ассемблер разделяет каждую текстовую строку на мнемокоды команд и аргументы, а затем сравнивает их с содержащимся в нем списком всех возможных мнемокодов и аргументов. Это сравнение показывает, какие машинные инструкции соответствуют каждой команде.
Обратите внимание на то, как ассемблер «понимает», что инструкция LXI должна сохранить в паре регистров DE адрес 0109h. Если сама инструкция LXI находится по адресу 0100h (а она там и находится, когда система CP/M загружает программу в память для последующего запуска), адрес 0109h соответствует началу текстовой строки. Обычно программисту, использующему ассемблер, не нужно беспокоиться о конкретных адресах, связанных с различными частями программы.
Создателю первого ассемблера, разумеется, пришлось вручную преобразовывать программу в машинный код. Человек, пишущий новый (возможно, улучшенный) ассемблер для того же компьютера, может воспользоваться языком ассемблера, чтобы затем преобразовать первый ассемблер. Как только новый ассемблер будет превращен в машинный код, он сможет ассемблироваться.
С выходом нового микропроцессора возникает необходимость в новом ассемблере. Однако новый ассемблер можно написать на существующем компьютере, используя его же транслятор. Когда ассемблер, работающий на компьютере А, создает код, который выполняется на компьютере Б, он называется кросс-ассемблером.
Несмотря на то что ассемблер избавляет от необходимости решать наименее творческие задачи программирования (вручную преобразовывать программу в машинный код), он не решает двух основных проблем, связанных с этим языком. Возможно, вы уже догадались, что первая проблема в том, что все действия с ассемблером могут быть крайне утомительными, поскольку вам приходится работать на уровне микропроцессора и беспокоиться о каждой мелочи.
Вторая проблема: написанные на языке ассемблера программы не являются переносимыми. Если вы пишете ассемблерную программу для микропроцессора Intel 8080, она не подойдет для микропроцессора Motorola 6800. Вам придется переписать ее на языке ассемблера 6800. Вероятно, это будет не так сложно по сравнению с написанием исходной программы, поскольку вы уже решили основные организационные и алгоритмические задачи. Однако это все равно потребует серьезных действий.
В предыдущей главе я говорил, что в современные микропроцессоры встроены машинные инструкции, выполняющие арифметические операции над числами с плавающей точкой. Это, безусловно, удобно, но это мало что дает. Предпочтительнее было бы полностью отказаться от машинно-зависимых инструкций, которые производят отдельные элементарные арифметические операции, а вместо этого выражать множество математических операций, используя проверенную временем алгебраическую форму записи. Например:
A × sin (2 × π + B) / C,
где A, B и C — числа, а число π равно 3,14159.
Почему бы и нет? Если такое выражение записано в текстовом файле, у вас должна быть возможность написать ассемблерную программу, которая читает этот текстовый файл и преобразует алгебраическое выражение в машинный код.
Если бы вам требовалось вычислить значение этого алгебраического выражения только один раз, вы могли бы сделать это вручную или с помощью калькулятора. Вероятно, вы собираетесь использовать компьютер, поскольку вам необходимо вычислить значение этого выражения при многих различных значениях A, B и C. По этой причине вы также должны предусмотреть для данного алгебраического выражения некоторый контекст, позволяющий вычислять его значение при разных коэффициентах.
То, к созданию чего вы приблизились, называется высокоуровневым языком программирования. Язык ассемблера считается низкоуровневым, поскольку взаимодействует непосредственно с аппаратным обеспечением. Несмотря на то что термин «язык высокого уровня» используется для описания любого языка программирования, отличного от языка ассемблера, некоторые языки считаются более высокоуровневыми по сравнению с другими. Если бы вы, будучи президентом компании, могли бы сесть за компьютер и ввести команду (еще лучше просто положить ноги на стол и продиктовать): «Рассчитать все прибыли и убытки за этот год, написать годовой отчет, распечатать несколько тысяч копий и разослать всем нашим акционерам», это бы означало, что вы работаете с языком очень высокого уровня! В реальном мире языки программирования даже не приближаются к идеалу.
Человеческие языки — это сотни и тысячи лет сложных взаимодействий, случайных изменений и приспособлений. Даже в основе таких искусственных языков, как эсперанто, лежит реальный язык. Однако компьютерные языки высокого уровня — результат более целенаправленной работы. Задача изобретения языка программирования интересна для некоторых людей, поскольку язык определяет то, как человек передает инструкции компьютеру. По оценкам, сделанным в 1993 году, с начала 1950-х были изобретены и внедрены более тысячи языков высокого уровня.
Конечно, недостаточно просто создать высокоуровневый язык (что подразумевает разработку синтаксиса, то есть набора правил для составления выражений). Вы обязательно должны написать компилятор — программу, которая преобразует инструкции вашего высокоуровневого языка в машинный код. Подобно ассемблеру, компилятор должен прочитать файл с исходным кодом символ за символом и разбить его на короткие слова, символы и цифры. Тем не менее компилятор намного сложнее, чем ассемблер. Относительная простота последнего обусловлена взаимно-однозначным соответствием между инструкциями на языке ассемблера и машинным кодом. Компилятору обычно приходится преобразовывать одну инструкцию, написанную на языке высокого уровня, во множество машинных. Компиляторы писать нелегко, о чем свидетельствуют множество книг, посвященных их разработке.
Языки высокого уровня имеют свои преимущества и недостатки. Основная ценность в том, что их обычно легче изучать и использовать, чем языки ассемблера. Программы, написанные на языках высокого уровня, часто получаются более понятными и краткими. Кроме того, такие языки в основном являются переносимыми, то есть не зависят от конкретного процессора, что позволяет программисту не учитывать базовую структуру компьютера, где будет работать программа. Разумеется, если хотите запустить программу более чем на одном процессоре, вам потребуются компиляторы, которые генерируют машинный код для этих процессоров. Исполняемые файлы по-прежнему будут специфическими для каждого из них.
Однако код, написанный хорошим ассемблерным программистом, почти всегда превосходит код, созданный компилятором. Это означает, что исполняемый файл, получившийся из программы, написанной на языке высокого уровня, будет более объемным и медленным по сравнению с функционально идентичной программой, написанной на языке ассемблера. (В последние годы это стало менее очевидным в связи с усложнением микропроцессоров и усовершенствованием компиляторов в плане оптимизации кода.)
Несмотря на то что язык высокого уровня может упростить работу с процессором, он не сделает его более мощным. С помощью ассемблера вы можете получить доступ ко всем функциям процессора. Поскольку высокоуровневый язык необходимо преобразовывать в машинный код, он может только сократить возможности процессора. Действительно, если высокоуровневый язык является переносимым, он не может использовать функции, характерные для определенных процессоров.
Например, многие процессоры предусматривают команды побитового сдвига. Как вы помните, эти команды сдвигают содержащиеся в аккумуляторе биты вправо или влево. Однако таких команд практически не существует ни в одном высокоуровневом языке программирования. Если перед вами стоит задача, требующая использования побитового сдвига, придется имитировать его путем умножения или деления на 2. (Нельзя сказать, что это плохо: многие современные компиляторы используют команды побитового сдвига процессора для умножения или деления на степень двойки.) Кроме того, во многих языках не предусмотрены и булевы операции над битами.
На заре эры домашних компьютеров большинство прикладных программ были написаны на ассемблере. Однако в наши дни этот язык используется редко и только для решения особых задач. По мере добавления в процессоры аппаратного обеспечения для конвейеризации — одновременного прогрессивного выполнения нескольких команд — язык ассемблера постоянно усложнялся. В то же время компиляторы становились все более интеллектуальными. Увеличение емкости диска и оперативной памяти современных компьютеров также сыграло свою роль: программистам больше не нужно создавать код, требующий небольшого объема памяти и умещающийся на небольшой дискете.
Несмотря на то что разработчики многих ранних компьютеров пытались формулировать для них задачи, используя алгебраическую форму записи, первым настоящим рабочим компилятором считается A-0 для компьютера UNIVAC, созданный в 1952 году Грейс Мюррей Хоппер (1906–1992) в корпорации Remington Rand. Доктор Хоппер пришла в компьютерную индустрию в 1944 году, когда присоединилась к команде Говарда Эйкена для работы над компьютером «Марк I». В свои восемьдесят с лишним лет она продолжала работать в этой сфере, занимаясь связями с общественностью в корпорации Digital Equipment Corporation (DEC).
Самый старый из используемых сегодня языков высокого уровня — ФОРТРАН (FORTRAN) (хотя за прошедшие годы он был многократно пересмотрен). Названия многих языков программирования пишутся прописными буквами, потому что являются акронимами. Название FORTRAN образовано от слов FORmula и TRANslation («трансляция формул»). Он был разработан в IBM для компьютеров серии 704 в середине 1950-х годов. На протяжении многих лет именно ФОРТРАНом пользовались ученые и инженеры. Он предусматривает обширную поддержку операций с плавающей точкой и работу с комплексными числами (которые, как я объяснил в предыдущей главе, представляют собой комбинации действительных и мнимых чисел).
У каждого языка программирования существуют сторонники и противники, которые могут горячо отстаивать свои предпочтения. В попытке занять нейтральную позицию при описании следующих концепций программирования я выбрал язык, который почти никто уже не использует, — АЛГОЛ (ALGOL — ALGOrithmic Language, алгоритмический язык; так же называется вторая по яркости звезда в созвездии Персея).
АЛГОЛ подходит для исследования природы высокоуровневых языков программирования, поскольку во многих отношениях это прямой предок многих популярных языков общего назначения, разработанных за последние 40 лет. Даже сегодня люди называют некоторые языки программирования языками типа АЛГОЛ.
Первая версия этого языка, АЛГОЛ 58, была разработана международным комитетом программистов в 1957 и 1958 годах. Два года спустя, в 1960-м, язык был доработан, а его пересмотренная версия получила название АЛГОЛ 60. Со временем появился АЛГОЛ 68, однако в этой главе я буду обращаться к версии, описанной в документе «Переработанное описание алгоритмического языка АЛГОЛ 60», который был завершен в 1962 году и впервые опубликован в 1963-м.
Давайте напишем краткий код на языке АЛГОЛ. Предположим, у нас есть компилятор под названием ALGOL.COM, который работает под управлением операционной системы CP/M или MS-DOS. Наша первая программа, написанная на языке АЛГОЛ, — текстовый файл с именем FIRST.ALG. Обратите внимание на тип файла ALG.
Программа на этом языке должна быть заключена между словами begin и end. Отобразим следующую текстовую строку.
Вы можете запустить компилятор, указав на программу FIRST.ALG, следующим образом.
ALGOL FIRST.ALG
Скорее всего, в ответ компилятор выведет что-то вроде этого.
Line 3: Unrecognized keyword 'ende'
К орфографическим ошибкам компилятор относится строже, чем придирчивый преподаватель. Я допустил ошибку в слове end, когда писал код, поэтому компилятор сообщил мне о наличии синтаксической ошибки. Вместо ende он ожидал встретить ключевое слово, которое способен распознать.
После исправления ошибки вы можете снова запустить компилятор. Иногда он может создать исполняемый файл напрямую (с именем FIRST.COM или FIRST.EXE в MS-DOS); иногда вам нужно выполнить еще одно действие. В любом случае, вскоре вы сможете запустить программу FIRST из командной строки.
FIRST
Программа FIRST выведет на экран следующую строку.
This is my fist ALGOL program!
Ой! Еще одна орфографическая ошибка (в слове first («первая») пропущена буква r). Компилятор не в состоянии обнаружить эту ошибку, поэтому она называется ошибкой времени выполнения — проявляется только при запуске программы.
Вероятно, вы уже поняли, что оператор print в нашей первой программе на языке АЛГОЛ отображает что-то на экране, в данном случае строку текста (эквивалент программы, написанной на языке ассемблера CP/M). На самом деле оператор print — это не часть официальной спецификации языка АЛГОЛ, но я предполагаю, что конкретный используемый нами компилятор предусматривает такую функцию, иногда называемую встроенной. Большинство операторов АЛГОЛ (не считая begin и end) должны сопровождаться точкой с запятой. Отступ для оператора print не обязателен, однако он часто применяется, чтобы сделать структуру программы более четкой.
Предположим, что вы хотите написать программу, которая перемножает два числа. В каждом языке программирования существует понятие переменной. Именем переменной в программе может быть буква, короткая последовательность букв или даже короткое слово. Переменная соответствует области памяти, однако в программе для обращения к ней используется имя, а не адрес памяти в числовом выражении. В этой программе задействованы три переменных с именами a, b и c.
Оператор real необходим для объявления переменных в программе. В данном случае переменные называются a, b и c и представляют собой действительные числа или числа с плавающей точкой. (Для объявления целочисленных переменных в языке АЛГОЛ есть ключевое слово integer.) Как правило, языки программирования требуют того, чтобы имена переменных начинались с буквы. Еще они могут содержать числа, но в переменных не должны употребляться пробелы и большая часть других символов. Часто компиляторы ограничивают длину имени переменной. В приведенном в этой главе примере я использую просто буквы.
Если наш компилятор АЛГОЛ поддерживает стандарт IEEE для представления чисел с плавающей точкой, то для хранения каждой из трех переменных нужны четыре байта памяти (для чисел одинарной точности) или восемь байт памяти (для чисел двойной точности).
Следующие три выражения — операторы присваивания. В языке АЛГОЛ такой оператор легко узнается по двоеточию и знаку равенства. (В большинстве компьютерных языков в качестве оператора присваивания используется только знак равенства.) В левой части находится переменная, в правой — выражение. Переменной присваивается число, полученное в результате вычисления. Первые два оператора присваивания указывают, что переменным a и b назначаются конкретные значения. Третий оператор присваивает переменной с значение произведения переменных a и b.
В настоящее время использование всем известного символа умножения «×» в языках программирования обычно не допускается, поскольку он отсутствует в наборах символов ASCII и EBCDIC. В большинстве языков операция умножения обозначается звездочкой «*». Хотя в АЛГОЛе для деления используется косая черта «/», этот язык также предполагает употребление символа «÷» для операции целочисленного деления, результат которого показывает, сколько раз делитель умещается в делимом. Кроме того, для возведения в степень в языке АЛГОЛ используется символ «↑», который также не входит в набор ASCII.
Для отображения данных на экране применяется оператор print. Выводимые текст и переменные разделяются запятыми. Отображение символов ASCII, вероятно, не является для оператора print трудной задачей, однако в данном случае функция также должна преобразовать в символы ASCII числа с плавающей точкой.
Произведение 535.43 и 289.771 равно 155152.08653
Затем программа завершает работу и возвращает управление операционной системе.
Если хотите перемножить еще несколько чисел, нужно отредактировать программу, изменить числа, перекомпилировать и снова запустить ее. Вы можете избежать этой многократной перекомпиляции, воспользовавшись другой встроенной функцией под названием read.
Оператор read считывает символы ASCII, которые вы вводите с клавиатуры, и преобразует их в числа с плавающей точкой.
В языках высокого уровня важной конструкцией является цикл, позволяющий написать программу, выполняющую одни и те же действия с разными значениями переменной. Предположим, вы хотите, чтобы программа вычисляла кубы чисел 3, 5, 7 и 9. Это можно сделать таким образом.
Оператор for сначала присваивает переменной a значение 3, а затем выполняет команду, следующую за ключевым словом do. Если команд, которые требуется выполнить, несколько (как в приведенном примере), то между ключевыми словами begin и end необходимо задействовать несколько операторов. Эти два ключевых слова определяют блок операторов. Затем оператор for выполняет те же команды для переменной a, которой присвоены значения 5, 7 и 9.
Вот еще одна версия инструкции for, которая вычисляет кубы нечетных чисел от 3 до 99.
Сначала оператор for присваивает переменной a значение 3 и выполняет блок операторов, следующий за инструкцией for. Затем значение переменной a увеличивается на величину, следующую за ключевым словом step, то есть на 2. Новое значение переменной a (5) используется для выполнения блока операторов. Значение переменной a будет и дальше увеличиваться на 2. Когда оно превысит 99, цикл for завершится.
Как правило, языки программирования имеют строгий синтаксис. Например, в АЛГОЛе 60 за ключевым словом for может следовать только имя переменной. Однако в английском после слова for могут располагаться всевозможные слова. Несмотря на сложность написания компиляторов, создать их намного проще, чем программы для интерпретации человеческой речи.
Еще одна важная особенность большинства языков программирования — условные структуры, позволяющие выполнить некоторое действие только в случае истинности конкретного условия. Вот пример использования встроенной функции языка АЛГОЛ sqrt, которая вычисляет квадратный корень. Функция sqrt не работает с отрицательными числами, и данная программа это учитывает.
Левая угловая скобка (<) — знак «меньше». Если пользователь введет отрицательное число, то будет выполнен первый оператор print. При вводе положительного числа будет запущен блок операторов, содержащий другой print.
До сих пор каждая из переменных в приведенных выше программах хранила лишь одно значение. Часто бывает удобно хранить в одной переменной несколько значений. В этом случае переменная называется массивом. В программе на языке АЛГОЛ массив объявляется следующим образом.
real array a[1:100];
В данном случае мы указали, что хотим использовать эту переменную для хранения 100 различных чисел с плавающей точкой, называемых элементами массива. Для обращения к первому элементу массива используется выражение a[1], ко второму — a[2], к последнему — a[100]. Число в квадратных скобках называется индексом элемента массива.
Эта программа вычисляет квадратные корни всех чисел от 1 до 100 и сохраняет их в массиве. Затем она выводит их на экран.
Кроме того, программа показывает целочисленную переменную с именем i, которое является традиционным, поскольку это первая буква в слове integer («целое»). В первом цикле for каждому элементу массива присваивается значение квадратного корня из его индекса, во втором элементы массива выводятся на экран.
Помимо типов real и integer, АЛГОЛ предусматривает тип переменных Boolean. (Помните Джорджа Буля из главы 10?) Такая переменная может иметь только два возможных значения: true и false. Буду использовать массив булевых переменных (и почти все, что мы изучили до сих пор) в последней программе этой главы, в которой реализуется известный алгоритм нахождения простых чисел под названием «Решето Эратосфена». Эратосфен (около 276–196 до н. э.) служил главным библиотекарем в легендарной Александрийской библиотеке и сегодня широко известен благодаря вычислению точной длины окружности Земли.
Простыми являются целые числа, которые без остатка делятся только сами на себя и на 1. Первое простое число — 2 (единственное четное простое число), затем следуют 3, 5, 7, 11, 13, 17 и т. д.
Первый шаг в алгоритме Эратосфена — составление списка положительных целых чисел начиная с 2. Поскольку 2 — простое число, необходимо удалить все числа, кратные двум (все четные числа, кроме 2), а так как 3 — простое число, следует исключить все числа, кратные 3. Мы уже знаем, что 4 не является простым числом, потому что оно было вычеркнуто. Следующее простое число — 5, значит, нужно удалить все числа, кратные 5. Продолжая действовать таким способом, вы будете находить новые простые числа.
В программе для нахождения простых чисел от 2 до 10 000, написанной на языке АЛГОЛ, этот алгоритм можно реализовать, объявив булев массив с индексами от 2 до 10 000.
Первый цикл for присваивает всем элементам булева массива значение true. Таким образом, предполагается, что все числа, находящиеся в начале программы, простые. Второй цикл проверяет числа от 1 до 100 (квадратный корень из 10 000). Если число простое (a[i] имеет значение true), то вложенный цикл for задает значение false всем числам, кратным ему. Последний цикл for выводит на экран все простые числа, то есть значения i, при которых a[i] — true.
Иногда люди спорят, является ли программирование искусством или наукой. С одной стороны, существуют учебные программы в области компьютерных наук, а с другой — есть такие знаменитые книги, как «Искусство программирования» Дональда Кнута. Как писал физик Ричард Фейнман: «Информатика скорее подобна инженерному делу: нужно просто заставить что-то делать что-то».
Если попросите 100 разных людей написать программу, которая выводит на экран простые числа, получите 100 различных решений. Даже те программисты, которые используют алгоритм Эратосфена, реализуют его не так, как это сделал я. Если бы программирование действительно было наукой, не существовало бы такого множества возможных решений, а неправильные заключения были бы более очевидными. Иногда стоящая перед программистом проблема вызывает у него озарения и творческие вспышки: в этом и заключается «искусство». И все же программирование в основном сводится к проектированию и строительству, подобно процессу возведения моста.
Многие из первых программистов были учеными и инженерами, способными формулировать задачи в виде математических алгоритмов, что было необходимо при использовании языков ФОРТРАН и АЛГОЛ. Тем не менее на протяжении истории развития этой сферы осуществлялись попытки создания языков для более широкого круга пользователей.
Одним из первых удачных языков, предназначенных для бизнеса, был КОБОЛ (COBOL, COmmon Business Oriented Language, «универсальный язык, ориентированный на коммерческие задачи»), который широко распространен и сегодня. Разработка языка КОБОЛ была начата в 1959 году комитетом, состоявшим из представителей промышленности и министерства обороны США, и большое влияние на него оказали ранние компиляторы Грейс Хоппер. Язык КОБОЛ был создан так, чтобы менеджеры, не занимавшиеся написанием кода, могли по крайней мере прочитать его и проверить, что он делает именно то, что от него ожидается. (Однако в реальной жизни так бывает нечасто.)
В языке КОБОЛ предусмотрены обширные возможности для чтения записей и создания отчетов. Запись — это набор взаимосвязанных данных. Например, страховая компания может хранить большие файлы с информацией обо всех проданных ею полисах. Каждому полису будет соответствовать отдельная пометка, включающая имя клиента, дату рождения и прочие данные. Многие ранние программы на языке КОБОЛ были написаны для работы с 80-столбцовыми записями, хранящимися на перфокартах IBM. Для экономии места на этих картах годы часто кодировались с помощью двух цифр вместо четырех, что в дальнейшем послужило причиной широко распространенной «проблемы 2000 года».
В середине 1960-х IBM в связи со своим проектом System/360 разработала язык под названием ПЛ/1 (PL/I, Programming Language I, «язык программирования номер один»). Цель его создания — объединить блочную структуру АЛГОЛа, математические функции ФОРТРАНа и средства КОБОЛа для работы с записями. Однако этот язык так никогда и не достиг популярности ФОРТРАНа и КОБОЛа.
Несмотря на существование версий ФОРТРАНа, АЛГОЛа, КОБОЛа и ПЛ/1 для домашних компьютеров, главным языком для них стал БЕЙСИК.
Язык БЕЙСИК (BASIC, Beginner’s All-purpose Symbolic Instruction Code, «универсальный код символических инструкций для начинающих») был придуман в 1964 году профессорами математического факультета Дартмутского университета Джоном Кемени и Томасом Курцем для работы с университетской системой распределения времени. Большинство студентов Дартмута не были математиками или инженерами, поэтому никто не ожидал, что они будут возиться с перфокартами и сложным синтаксисом. Вместо этого студент, сидя перед терминалом, мог написать простую программу, просто набрав команды, которым предшествовали числа, обозначающие их порядок. Команды, не сопровождающиеся числами, предназначались для системы: SAVE (сохранить на диске), LIST (отобразить строки по порядку), RUN (скомпилировать и запустить). Вот первая программа на языке БЕЙСИК, опубликованная в первом печатном руководстве.
10 LET X = (7 + 8) / 3
20 PRINT X
30 END
В отличие от АЛГОЛа язык БЕЙСИК не требовал указывать тип переменной. Большинство чисел сохранялись в виде значений с плавающей точкой.
Многие последующие реализации языка БЕЙСИК имели форму интерпретаторов, а не компиляторов. Как я объяснял, компилятор читает файл с исходным кодом и создает исполняемый файл. Интерпретатор читает исходный код и сразу выполняет его, не создавая исполняемого файла. По сравнению с компиляторами интерпретаторы легче писать, однако интерпретируемая программа выполняется медленнее, чем скомпилированная. На домашних компьютерах язык БЕЙСИК начал использоваться в 1975 году, когда два приятеля, Билл Гейтс (род. 1955) и Пол Аллен (род. 1953), написали интерпретатор языка БЕЙСИК для микрокомпьютера Altair 8800 и основали корпорацию Microsoft.
Язык программирования Паскаль (Pascal), который унаследовал большую часть своей структуры от АЛГОЛа, но включал функции обработки записей КОБОЛа, был разработан в конце 1960-х годов швейцарским профессором информатики Никлаусом Виртом (род. 1934). Язык Паскаль был довольно популярен среди программистов, работавших на компьютере IBM PC, но лишь в специфической реализации — Turbo Pascal, выпущенной компанией Borland International в 1983 году. Реализация Turbo Pascal, написанная датским студентом Андерсом Хейлсбергом (род. 1960), представляла версию, дополненную интегрированной средой разработки (Integrated Development Environment, IDE). Текстовый редактор и компилятор были объединены в одну программу, что значительно ускорило процесс программирования. Интегрированные среды разработки широко использовались на мейнфреймах, а среда Turbo Pascal ознаменовала начало их применения на небольших компьютерах.
Паскаль также серьезно повлиял на язык Ада (Ada), созданный для министерства обороны США. Язык был назван в честь Августы Ады Байрон, о которой я упоминал в главе 18, когда описывал аналитическую машину Чарльза Бэббиджа.
Затем появился очень популярный язык C (Си), созданием которого в период c 1969 по 1973 год в основном занимался Деннис Ритчи из Bell Telephone Laboratories. Часто спрашивают, почему язык называется С. Все просто: он был написан на основе более раннего языка B — упрощенной версии BCPL (Basic CPL), предшественником которого являлся CPL (Combined Programming Language, «комбинированный язык программирования»).
В главе 22 я упоминал, что операционная система UNIX задумывалась как переносимая. Большинство операционных систем в то время были разработаны на языке ассемблера для конкретного процессора. В 1973 году UNIX была написана (вернее, переписана) на языке C, и с тех пор эта система и этот язык тесно связаны.
Как правило, в языке C операторы записываются весьма кратко. Например, вместо ключевых слов begin и end, используемых в АЛГОЛе и Паскале для разграничения блоков, в C применяются фигурные скобки { и }. Вот еще один пример. Программисту часто требуется увеличить значение переменной на некую постоянную величину.
i = i + 5;
В C вы можете сократить этот оператор.
i + = 5;
Если вам нужно увеличить значение переменной на 1 (инкрементировать ее), данный оператор можно записать еще короче.
i++;
Такой оператор на 16- или 32-разрядном микропроцессоре может выполняться с помощью одной машинной инструкции.
Ранее я упоминал, что большая часть языков высокого уровня не предполагает операций побитового сдвига или булевых операций над битами, которые являются частью функционала многих процессоров. Язык C — исключение из правила. Кроме того, важная особенность этого языка — поддержка указателей, которые, по сути, являются числовыми представлениями адресов памяти. Поскольку язык C поддерживает операции, реализующие многие инструкции процессора, он иногда классифицируется как язык ассемблера высокого уровня. По сравнению с любым другим языком типа АЛГОЛ язык C точнее всего имитирует общие наборы команд процессора.
Тем не менее все языки типа АЛГОЛ, то есть наиболее распространенные языки программирования, разрабатывались для компьютеров с архитектурой фон Неймана. Выйти за рамки соответствующего образа мышления непросто, а заставить других людей использовать такой язык еще сложнее. Один из подобных языков — LISP (List Processing, «обработка списков»), созданный Джоном Маккарти в конце 1950-х годов и пригодный для работы в области искусственного интеллекта. Другой язык, столь же необычный, как LISP, но совершенно на него не похожий, — APL (A Programming Language, «язык программирования») — был придуман в конце 1950-х Кеннетом Айверсоном. В нем используется набор специальных символов, которые выполняют операции одновременно над целыми массивами чисел.
Несмотря на то что языки типа АЛГОЛ продолжают доминировать, в последние годы они были несколько усовершенствованы, что привело к появлению так называемых объектно-ориентированных языков, удобных при работе с графическими операционными системами, о которых я расскажу в следующей главе.