0.2 – Введение в языки программирования

Добавлено 17 марта 2021 в 12:26

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

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

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

Машинный язык

Центральный процессор (CPU) компьютера не понимает язык C++. Ограниченный набор инструкций, которые CPU может понимать напрямую, называется машинным кодом (или машинным языком, или набором инструкций).

Вот пример инструкции машинного языка: 10110000 01100001.

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

Как организованы эти инструкции, выходит за рамки данного введения, но интересно отметить две вещи.

  • Во-первых, каждая инструкция состоит из последовательности нулей и единиц. Каждый отдельный 0 или 1 называется битом (от «binary digit», или «двоичная цифра»). Количество битов, составляющих одну команду, может различаться – например, некоторые процессоры обрабатывают инструкции, которые всегда имеют длину 32 бита, тогда как некоторые другие процессоры (например, семейство x86, которое вы, вероятно, используете) имеют инструкции, которые могут быть переменной длины.
  • Во-вторых, каждый набор битов интерпретируется центральным процессором в команду для выполнения очень конкретной работы, например, сравнения этих двух чисел или помещения этого числа в эту ячейку памяти. Однако из-за того, что разные CPU имеют разные наборы инструкций, инструкции, написанные для одного типа процессоров, не могут использоваться процессором, который не использует такой же набор команд. Это означало, что программы, как правило, нельзя было портировать (переносить, использовать без серьезной доработки) между различными типами систем, и их приходилось переписывать заново.

Язык ассемблера

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

Вот та же инструкция, что и выше, но на языке ассемблера: mov al, 061h.

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

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

Высокоуровневые языки

Чтобы решить проблемы удобочитаемости и переносимости, были разработаны новые языки программирования, такие как C, C++, Pascal (и более поздние языки, такие как Java, Javascript и Perl). Эти языки называются языками высокого уровня, поскольку они разработаны, чтобы позволить программисту писать программы, не беспокоясь о том, на каком компьютере будет выполняться программа.

Вот всё та же инструкция, что и выше, на C/C++: a = 97;

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

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

Ниже показано упрощенное представление процесса компиляции:

Рисунок 1 – Пример сборки приложения
Рисунок 1 – Пример сборки приложения

Поскольку программы на C++ обычно компилируются, мы вскоре рассмотрим компиляторы более подробно.

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

Ниже показано упрощенное представление процесса интерпретации:

Рисунок 2 – Пример интерпретации
Рисунок 2 – Пример интерпретации

Дополнительная информация


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

В целом, компиляторы имеют следующие преимущества:

  1. Поскольку они могут видеть весь исходный код заранее, при генерации кода они могут выполнять ряд анализов и оптимизаций, благодаря чему окончательная версия кода выполняется быстрее, чем простая интерпретация каждой строки по отдельности.
  2. Компиляторы часто могут генерировать низкоуровневый код, который выполняет эквивалент высокоуровневых идей, таких как «динамическое связывание» или «наследование» с точки зрения поиска в памяти внутри таблиц. Это означает, что итоговые программы должны помнить меньше информации об исходном коде, что снижает использование памяти сгенерированной программой.
  3. Скомпилированный код обычно быстрее, чем интерпретируемый код, потому, что выполняемые инструкции обычно предназначены только для самой программы, а не для самой программы плюс накладные расходы интерпретатора.

В целом, у компиляторов есть следующие недостатки:

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

В целом, у интерпретаторов есть следующие преимущества:

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

В целом, у интерпретаторов есть следующие недостатки:

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

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

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

И последняя деталь: языки не компилируются и не интерпретируются. Обычно код C компилируется, но доступны и интерпретаторы C, которые упрощают отладку или визуализацию выполняемого кода (они часто используются при вводном изучении программирования – или, по крайней мере, они использовались раньше). JavaScript считался интерпретируемым языком, пока некоторые JS-движки не начали его компилировать. Некоторые реализации Python являются чисто интерпретаторами, но вы можете найти компиляторы Python, которые генерируют нативный код. Некоторые языки легче компилировать или интерпретировать, чем другие, но ничто не мешает вам создать компилятор или интерпретатор для любого конкретного языка программирования. Теоретический результат, называемый проекциями Футамуры, показывает, что всё, что можно интерпретировать, можно и скомпилировать.

Большинство языков можно компилировать или интерпретировать, однако традиционно языки, такие как C, C++ и Pascal, компилируются, тогда как «скриптовые» языки, такие как Perl и Javascript, обычно интерпретируются. В некоторых языках, например в Java, используется сочетание этих двух методов.

Высокоуровневые языки обладают многими необходимыми свойствами.

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

Во-вторых, высокоуровневые языки требуют меньшего количества инструкций, чем низкоуровневые языки, для выполнения одной и той же задачи, что делает программы более краткими и легкими для понимания. В C++ вы можете сделать что-то вроде a = b * 2 + 5; в одну строку. На ассемблере для этого потребуется 5 или 6 разных инструкций.

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

Рисунок 3 – Пример портируемости приложения
Рисунок 3 – Пример портируемости приложения

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

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

Правила, рекомендации и предупреждения

По мере изучения этих руководств мы будем выделять важные моменты в следующих трех категориях:

Правило


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

Лучшая практика


Лучшие практики – это то, что вам следует делать, потому что такой способ обычно считается стандартным или настоятельно рекомендуется. То есть либо так поступают все (а если вы поступите иначе, то вы будете делать не то, что ожидают), либо это лучше других альтернатив.

Предупреждение


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

Теги

C++ / CppВысокоуровневые языки программированияДля начинающихИнтерпретаторКомпиляторОбучениеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.