Теоретический минимум по Computer Science — страница 19 из 21

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

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

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

Полезные материалы

• Таненбаум Э., Остин Т. Архитектура компьютера. — СПб.: «Питер», 2017.

• Современная реализация компилятора на C (Modern Compiler Implementation in C, Appel, https://code.energy/appel).

Глава 8. Программирование

Когда кто-то скажет: «Мне нужен язык программирования, в котором достаточно только сказать, что мне нужно сделать», — дайте ему леденец на палочке.

Алан Перлис

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

определять лингвистику, которая управляет программным кодом;

хранить вашу драгоценную информацию внутри переменных;

обдумывать решения в условиях разных парадигм.

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

8.1. Лингвистика

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

Значения

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

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

Выражения

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

3

Бум! Мы буквально только что создали значение 3, написав: «3». Довольно прямолинейно. Как литералы можно создавать и другие типы значений. Большинство языков программирования позволит вам создать строковое значение Привет мир, набрав на клавиатуре «Привет мир». Функции же генерируют значение согласно методу или процедуре, которые запрограммированы в каком-то другом месте. Например:

getPacificTime()

Это выражение создало значение, равное текущему времени в Лос-Анджелесе. Если сейчас 4 часа утра, то метод вернет 4.

Еще одним базовым элементом любого языка программирования является оператор. Оператор может объединять простые выражения для формирования более сложных. Например, оператор + позволяет создать значение, равное времени в Нью-Йорке:



Когда в Лос-Анджелесе 4 часа утра, наше выражение сведется к 7. В действительности выражение — это любая запись, которую компьютер сможет свести к единственному значению. Большие выражения могут сочетаться с другими выражениями посредством операторов, формируя еще более крупные выражения. В конечном счете даже самое сложное выражение всегда будет вычислено и сведено к единственному значению.

Наряду с литералами, операторами и функциями выражения могут также содержать круглые скобки. Они позволяют управлять порядком выполнения операторов: (2 + 4)2 сводится к 62, которое, в свою очередь, сводится к 36. Выражение 2 + 42 сводится к 2 + 16, а затем к 18.

Инструкции

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

print("привет мир").


Рис. 8.1.[84]


Более сложные примеры включают условная инструкция if, инструкции циклов while и for. Разные языки программирования поддерживают разные типы инструкций.

Определения. Некоторые языки программирования имеют специальные инструкции, именуемые определениями. Они изменяют состояние программы, добавляя не существовавшие ранее объекты, такие как новые значения или функции[85]. Чтобы обратиться к объекту, который мы определили, мы должны назвать его. Этот процесс называется привязкой имен. Например, имя getPacificTime должно быть привязано к определению функции, заданному где-то в другом месте.

8.2. Переменные

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

pi ← 3.142

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

var pi

pi = 3.142

Эта инструкция резервирует блок памяти, записывает в него значение 3,142 и привязывает имя "pi" к адресу блока памяти.

Типизация переменных

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

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

float pi;

pi = 3.142;

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

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

Область видимости переменных

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

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

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

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

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

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

8.3. Парадигмы