Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
В этой лекции рассматриваются фундаментальные семантические проблемы, связанные с использованием переменных. Её главная тема: природа имен и специальных слов в языках программирования. Затем рассматриваются атрибуты переменных, в том числе их тип, адрес и значение. Обсуждаются также альтернативные имена. Далее вводятся важные понятия связывания и времени связывания. Различные варианты времени связывания атрибутов с переменными определяют четыре различные категории переменных. За их описанием следует разбор проверки типов, строгой типизации и правил совместимости типов. Затем описываются два способа обзора данных: динамический и статический, одновременно рассматривается концепция среды ссылок оператора. В заключение описываются именованные константы и способы инициализации переменных.
4.1. Введение
Императивные языки программирования это (в различной степени) абстракция компьютерной архитектуры фон Неймана, лежащей в их основе. Двумя основными компонентами этой архитектуры являются память, в которой хранятся данные и команды, и процессор, позволяющий изменять содержимое памяти. В языке программирования абстракциями ячеек памяти машины являются переменные. В некоторых случаях характеристики абстракций и ячеек очень близки; примером этому служит переменная целого типа, которая обычно точно представляется в виде отдельного слова аппаратной памяти. В других случаях абстракции довольно далеки от ячеек памяти. Так, для поддержания трехмерного массива требуется программная функция отображения.
Охарактеризовать переменные можно с помощью набора свойств, или атрибутов, важнейшим из которых является тип основное понятие в языках программирования. Изучение структуры типов данных в языке требует рассмотрения разнообразных вопросов. К важнейшим из них относятся область видимости и время жизни переменных. С этими вопросами, в свою очередь, связаны проверка типов и инициализация. Еще одной важной составляющей понятия типов данных языка является совместимость типов.
Далее, в процессе обсуждения приводятся ссылки на семейства языков как на один язык. Например, ссылка на язык FORTRAN, подразумевает все его версии. Сказанное относится и к семействам языков Pascal и Ada. Упоминания о языке С включают исходную версию этого языка и язык ANSI С. Конкретная версия языка упоминается только в случаях, когда она отличается от остальных представителей семейства. Кроме того, ссылки на главы подразумевают книгу Роберта У. Себесты «Основные концепции языков программирования», -М+СП+Киев, «Вильямс»-2001
4.2. Имена
Прежде чем начать обсуждение переменных, рассмотрим такой фундаментальный атрибут переменных, как имена, которые используются не только для именования переменных. Имена связаны с понятием меток, подпрограмм, формальных параметров и другими программными конструкциями. Синонимом термина имя часто является термин идентификатор (identificator).
4.2.1. Структура имен
Ниже приводятся основные вопросы, связанные с именами.
Эти вопросы обсуждаются в следующих двух разделах, в которые также включены примеры некоторых соглашений по конструированию имен.
4.2.2. Виды имен
Имя (name) это строка символов, используемая для идентификации некоторой сущности в программе. В первых языках программирования использовались имена, состоящие только из одного символа. Это было естественно, поскольку ранние языки программирования были в основном математическими, а математики давно использовали имена, состоящие из одного символа, для формального обозначения неизвестных параметров.
Эта традиция была нарушена с появлением языка FORTRAN I, в котором в именах разрешалось использовать до шести символов. Это ограничение длины имени шестью символами сохранилось и в языке FORTRAN 77, но в языках FORTRAN 90 и С разрешенное количество символов в именах увеличилось до 31; в языке Ada вообще нет ограничения на длину имени, и все имена являются значащими. В некоторых языках, например C++, также нет ограничения на длину имени, хотя иногда такое ограничение вводится создателями конкретных систем реализаций этих языков, чтобы таблица имен, в которой во время компиляции хранятся идентификаторы, не была слишком большой и сложной.
Общепринятым видом имени является строка с разумным ограничением длины (или без него), содержащая такие соединительные символы, как символ подчеркивания (_). Символ подчеркивания используется с той же целью, что и пробел в русских текстах, но при этом он не разрывает строку имени, содержащую его. Большинство современных языков программирования позволяют использовать в именах соединительные символы.
В некоторых языках, особенно в языках С, C++ и Java, различаются прописные и строчные буквы; т.е. имена в этих языках зависят от регистра (case sensitive). Например, в языке C++ три следующих имени различны: rose, ROSE и Rose. В определенном смысле это значительно ухудшает читабельность, поскольку имена, внешне выглядящие очень похоже, на самом деле означают различные объекты. В этом отношении зависимость от регистра нарушает принцип соответствия, гласящий, что языковые конструкции, имеющие одинаковый вид, должны иметь одинаковый смысл.
Разумеется, не все согласятся с утверждением, что зависимость от регистра это неудачное свойство имен. В языке С, например, зависимости от регистра можно избежать, используя исключительно имена, состоящие из строчных букв. Однако в языке Java подобным образом проблемы избежать не удается, поскольку многие предопределенные имена содержат и прописные, и строчные буквы. Например, используемый в языке Java метод преобразования строки в целочисленное значение называется parselnt, а если написать его в виде Parselnt или parseint, то команда не будет распознана. Это проблема связана с легкостью создания программы, а не с ее читабельностью, поскольку необходимость запоминать необычные правила написания слов затрудняет создание корректных программ. Она представляет собой разновидность нетерпимости части разработчиков языка, навязываемой компилятором.
В версиях языка FORTRAN вплоть до версии FORTRAN 90 в именах могли использоваться только прописные буквы, что было излишним ограничением. Многие реализации языка FORTRAN 77 (подобно языку FORTRAN 90) позволяют использовать строчные буквы просто они переводятся в прописные для внутреннего использования во время компиляции.
4.2.3. Специальные слова
В языках программирования специальные слова улучшают читабельность программ, называя выполняемые действия. Они также используются для обособления синтаксических сущностей программ. В большинстве языков программирования эти слова классифицируются как зарезервированные, но в некоторых они являются всего лишь ключевыми.
Ключевое слово (keyword) имеет особое значение только в определенном контексте. Примером языка, в котором специальные слова являются ключевыми, служит язык FORTRAN. Если слово REAL языка FORTRAN встречается в начале предложения, а за ним следует имя, то это слово рассматривается как ключевое, указывающее на то, что данное выражение является оператором объявления переменной. В то же время, если за словом REAL следует оператор присваивания, то это слово рассматривается как имя переменной. Ниже проиллюстрированы эти варианты использования слова REAL:
REAL APPLE REAL = 3.4
Компиляторы языка FORTRAN и читатели его программ должны различать имена и специальные слова по контексту.
Зарезервированное слово (reserved word) не может использоваться в качестве имени. При разработке языка лучше использовать зарезервированные слова, поскольку возможность переопределения ключевых слов может ухудшить читабельность. Например, в языке FORTRAN могут встретиться следующие операторы:
INTEGER REAL REAL INTEGER
Эти операторы объявляют, что переменная программы REAL имеет тип INTEGER, а переменная INTEGER тип REAL. Помимо странного вида этих операторов объявления переменных, появление слов REAL и INTEGER в качестве имен переменных может ввести читателя такой программы в заблуждение.
В данной книге в примерах фрагментов программ зарезервированные слова выделяются жирным шрифтом.
Многие языки содержат предопределенные имена, которые в некотором смысле являются чем-то средним между зарезервированными словами и именами, определяемыми пользователем. Они имеют предопределенный смысл, но могут переопределяться пользователем. Например, такие встроенные в язык Ada имена типов данных, как INTEGER и FLOAT, предопределены. Эти имена не зарезервированы: они могут переопределяться любой программой на языке Ada. В языке Pascal предопределенные имена иногда называются стандартными идентификаторами. В этом языке обычные имена подпрограмм ввода-вывода, в том числе и названия подпрограмм readln и writeln, предопределены.
Определения предопределенных имен в языках Pascal и Ada должны быть видимы компиляторами этих языков, поскольку проверка типов в таких языках производится во время компиляции. В обоих языках приведенные выше примеры предопределенных имен видимы компилятором неявно. Другие предопределенные имена языка Ada, например, стандартные подпрограммы ввода и вывода GET и PUT, сделаны видимыми явно с помощью оператора with, указанного пользователем.
В языках С и C++ многие имена предопределены в библиотеках, используемых пользовательскими программами. Например, имена функций ввода и вывода языка С (printf и scanf) определяются в библиотеке stdio. Доступ компилятора к именам, предопределенным в библиотеках, возможен через соответствующие заголовочные файлы.
4.3. Переменные
Переменная в программе представляет собой абстракцию ячейки памяти компьютера или совокупности таких ячеек. Программисты часто думают о переменных как об именах ячеек памяти, но переменная это не только имя. Переход с машинных языков на языки ассемблера происходил в основном путем замены абсолютных числовых адресов ячеек памяти именами, при этом значительно повысилась читабельность программ, а значит, они стали более легкими для создания и эксплуатации. Этот шаг также позволил избежать возникновения проблем, связанных с абсолютной адресацией, поскольку транслятор преобразовывает имена в фактические адреса, которые сам выбирает.
Переменные можно охарактеризовать шестеркой атрибутов (имя, адрес, значение, тип, время жизни, область видимости). Несмотря на то, что такое описание может показаться слишком сложным для внешне простой концепции, оно позволяет ясно объяснить различные свойства переменных.
Обсуждение атрибутов переменных приводит к исследованию важных концепций, связанных с этим вопросом: альтернативных имен, связывания, времени связывания, объявлений, проверки типов, строгой проверки типов, правил обзора данных и сред ссылок.
Имя, адрес, тип и значение переменных рассматриваются в следующих подразделах. Время жизни и область видимости атрибутов описаны в разделах 4.4.3 и 4.8, соответственно.
Имена переменных самые распространенные имена в программах. Они детально рассматривались в разделе 4.2 в общем контексте имен сущностей, из которых состоят программы. Большинство переменных имеют имена. Переменные, у которых нет имен, рассматриваются в разделе 4.4.3.3. Часто вместо слова "имя" используется слово "идентификатор".
4.3.2. Адрес
Адрес (address) переменной это ячейка памяти, с которой связана данная переменная. Эта связь не так проста, как может показаться на первый взгляд, Во многих языках одно и то же имя можно связать с разными адресами в разных местах программы и в разное время. Например, программа может иметь две подпрограммы subl и sub2, в каждой из которых определяется переменная с одним и тем же именем, скажем, переменная sum. Поскольку эти переменные не зависят друг от друга, то обращение к переменной sum в подпрограмме subl не связано с обращением к переменной sum подпрограммы sub2. Подобным образом большинство языков позволяют связать одно имя с разными адресами в разные моменты времени выполнения программы. Например, рекурсивно вызываемая подпрограмма содержит многочисленные версии каждой локально объявленной переменной, по одной на каждую активацию подпрограммы. Связывание переменных с адресами будет обсуждаться в разделе 4.4.3. Модель реализации подпрограмм и их активаций для языков, подобных языку ALGOL, рассматривается в главе 9.
Адрес переменной иногда называется ее левым значением (L-value), поскольку именно он нужен, если в левой части оператора присваивания указана переменная.
4.3.2. Т. Альтернативные имена
Иногда несколько идентификаторов одновременно ссылаются на один и тот же адрес. Если для доступа к отдельной ячейке памяти можно использовать несколько имен переменных, то эти имена называются альтернативными (aliases). Совмещение имен ухудшает читабельность программы, поскольку оно позволяет переменным изменять свои значения при присвоении их другим переменным. Например, если имена переменных А и В являются альтернативными, то любое изменение переменной А приводит к изменению переменной В, и наоборот. Человек, читающий программу, всегда должен помнить, что А и В разные названия одной и той же ячейки памяти. Поскольку в программе может быть любое количество альтернативных имен, то на практике это создает серьезные трудности. Кроме того, совмещение имен затрудняет верификацию программы.
Альтернативные имена могут создаваться в программе несколькими различными способами. В языке FORTRAN для этого используется оператор EQUIVALENCE. Они также могут создаваться с помощью вариантных записей в таких языках, как Pascal и Ada, или с помощью объединений в языках С и C++. Альтернативные имена, создаваемые этими типами данных, экономят память, позволяя переменным разнога типа использовать в разное время одни и те же ячейки памяти. Они также могут применяться для нарушения правил работы с типами в некоторых языках, допускающих эту возможность. Подробно вариантные записи и объединения рассмотрены в главе 5.
Две переменные указателя являются альтернативными, если они указывают на одну и ту же ячейку памяти. Это справедливо и для ссылок. Такая разновидность совмещения имен не экономит место, а просто является побочным эффектом природы указателей и ссылок. Если указатель в языке C++ адресует именованную переменную, то при разыменовании указатель и имя переменной являются альтернативными именами. Эти и другие свойства указателей и ссылок будут уточняться позднее.
Во многих языках программирования имена можно совмещать, используя параметры подпрограмм. Эта разновидность альтернативных имен рассмотрена в главе 8.
Причин, оправдывавших использование альтернативных имен, уже не существует. Когда языковая конструкция создает альтернативные имена для повторного использования памяти, их можно заменить схемой управления динамической памятью, позволяющей повторное использование памяти, но не обязательно создающей при этом альтернативные имена. Более того, современная память компьютера значительно больше, чем была в те времена, когда разрабатывались языки, подобные языку FORTRAN, так что сейчас память уже не такой дефицитный товар.
Момент времени, когда переменная связывается с адресом, очень важен для понимания языков программирования. Подробнее этот вопрос рассмотрен в разделе 4.4.3.
4.3.3. Тип
Тип (type) переменной определяет диапазон значений, которые может иметь переменная, и набор операций, предусмотренных для переменных этого типа. Например, для типа INTEGER в некоторых реализациях языка FORTRAN установлен диапазон значений от -32768 до 32767 и определены арифметические операции сложения, вычитания, умножения, деления и возведения в степень, а также некоторые библиотечные функции для выполнения других операций, например, вычисления абсолютной величины числа.
4.3.4. Значение
Значение переменной это содержимое ячейки или ячеек памяти, связанных с данной переменной. Память компьютера удобно представлять себе в терминах абстрактных ячеек, а не физических. Ячейки, или отдельно адресуемые единицы, большинства современных компьютеров имеют размер, равный байту, как правило, содержащему восемь битов. Этот размер слишком мал для большинства программных переменных. Мы будем считать, что абстрактная ячейка памяти имеет размер, достаточный для хранения связанной с ней переменной. Несмотря на то что числа с плавающей точкой в отдельной реализации конкретного языка могут занимать четыре физических байта, мы считаем, что число с плавающей точкой занимает одну абстрактную ячейку памяти. Мы полагаем, что величина любого элементарного неструктурированного типа занимает отдельную абстрактную ячейку. С этого момента под ячейкой памяти мы будем подразумевать абстрактную ячейку памяти.
Значение переменной иногда называется ее правым значением (r-value), поскольку именно оно необходимо при использовании переменной, указанной в правой части оператора присваивания. Для того чтобы получить доступ к правому значению переменной, вначале следует определить ее левое значение. Значительно усложнить этот процесс могут, например, правила обзора данных, как это показано в разделе 4.8.
4.4. Концепция связывания
В общем смысле, связывание (binding) представляет собой процесс установления связи, аналогичной существующей между атрибутом и объектом или между операцией и символом. Момент времени, когда эта связь устанавливается, называется временем связывания (binding time). Связывание и время связывания важные понятия семантики языков программирования. Связывание может происходить во время разработки или реализации языка; при компиляции, загрузке или выполнении программы. Звездочка (*). например, обычно связывается с операцией умножения во время разработки языка. Тип данных, например тип INTEGER в языке FORTRAN, связывается с диапазоном возможных значений во время реализации языка. В языках С и Pascal переменная связывается с конкретным типом данных во время компиляции программы. Вызов библиотечной подпрограммы связывается с командами подпрограммы при компиляции. Переменная может связываться с ячейкой памяти при загрузке программы в память. Аналогичное связывание в некоторых случаях не происходит вплоть до времени выполнения программы. например, для переменных, объявленных в подпрограммах языка Pascal, или функций языка С (если объявления не содержат спецификатор static).
Рассмотрим следующий оператор присваивания языка С с указанным определением переменной count:
int count;
сount = count + 5;
Ниже приведены некоторые виды связывания и времен связывания для частей оператора присваивания.
Для того чтобы разобраться в семантике языка программирования, необходимо понять, что такое время связывания атрибутов с сущностями программы. Например, необходимо знать, как при вызове подпрограммы фактические параметры связываются с формальными параметрами в ее определении. Для того чтобы определить текущее значение переменной, нужно узнать, когда данная переменная была связана с ячейкой памяти.
4.4.1. Связывание атрибутов с переменными
Связывание называется статическим (static), если оно выполняется до выполнения программы и не меняется во время ее выполнения. Если связывание происходит во время выполнения программы или может меняться в ходе ее выполнения, то оно называется динамическим (dynamic). Физическое связывание переменной с ячейкой в среде виртуальной памяти сложный процесс, поскольку страница или сегмент адресного пространства, в котором находится ячейка, во время выполнения программы может многократно загружаться и выгружаться из памяти. В некотором смысле такие переменные многократно связываются и открепляются. Такие связи, однако, поддерживаются аппаратным обеспечением компьютера, а программе и пользователю эти изменения не видны. Поскольку это не представляет интереса для обсуждения, мы не фокусируем внимание на аппаратном связывании. Главным моментом для нас являются различия между статическим и динамическим связываниями.
4.4.2. Связывание типов
Прежде чем к переменной программы можно будет обращаться, она должна быть связана с типом данных. При этом необходимо рассмотреть два важных аспекта этого связывания: каким образом указывается этот тип и когда происходит связывание. Типы могут определяться статически с помощью некоторой формы явного или неявного объявления.
4.4.2. Т. Объявление переменных
Явное объявление (explicit declaration) это оператор программы, перечисляющий имена переменных и устанавливающий, что они имеют определенный тип. Неявное объявление (impicit declaration) это средство связывания переменных с типами посредством принятых по умолчанию соглашений, а не операторов объявления. В этом случае первое появление имени переменной в программе является ее неявным объявлением. И явное, и неявное объявления создают статические связи с типами.
Большинство языков программирования, созданных с середины 1960-х годов, требуют явного объявления всех переменных (двумя исключениями являются языки Perl и ML). Некоторые широко используемые языки, первоначальные работы по созданию которых были проведены в конце 1960-х годов, допускают неявные объявления переменных, особенно это касается языков FORTRAN. PL/I и BASIC. Например, в программах на языке FORTRAN идентификатор, для которого не было выполнено явного объявления, объявляется неявно в соответствии со следующим соглашением: если идентификатор начинается с одной из букв I, J, К, L, М или N, то неявно объявлено, что он имеет тип INTEGER; в противном случае неявно объявлено, что он имеет тип REAL.
Неявные объявления могут нанести серьезный ущерб надежности программы, поскольку они препятствуют выявлению на этапе компиляции различных опечаток или программистских ошибок. Переменные, которые программистом были случайно оставлены необъявленными, получат типы по умолчанию и будут иметь неожиданные атрибуты, что может вызвать неявные ошибки, которые трудно обнаружить.
Некоторых проблем, связанных с неявными объявлениями, можно избежать, требуя, чтобы имена отдельных типов начинались с конкретных специальных символов. Например, в языке Perl все имена, начинающиеся с символа 5, являются скалярными величинами, которые могут быть строкой или числом. Если имя начинается с символа @, то именуемая им сущность является массивом; если имя начинается с символа %, то оно обозначает хешированную структуру (hash structure). Подобный подход создает различные пространства имен для различных типов переменных. При таком сценарии имена dapple и dapple не связаны между собой, поскольку принадлежат различным пространствам имен. Более того, тип переменной определяется ее именем.
В языках С и C++ необходимо различать объявления и определения. Объявления устанавливают типы и другие атрибуты, но не приводят к распределению памяти. Определения устанавливают атрибуты и вызывают распределение памяти. Для конкретного имени в программе на языке С может содержаться любое количество согласованных объявлений, но только одно определение. В языке С можно объявлять переменные, внешние по отношению к функции. Объявление указывает компилятору тип переменной и то, что она где-то была определена. Эта идея переносится на функции языков С и C++, у которых прототипы объявляют имена и интерфейсы, но не команды функций. С другой стороны, определения функций являются полными.
4.4.2.2, Динамическое связывание типов
При динамическом связывании в операторе объявления тип не указывается. Вместо :-того переменная связывается с типом при присвоении ей значения оператором присваивания. При выполнении оператора присваивания переменная, которой присваивается значение, связывается с типом переменной, выражения или значения, находящегося в правой части оператора присваивания.
Языки, в которых типы связываются динамически, значительно отличаются от языков со статическим связыванием типов. Основным преимуществом динамического связывания переменных с типом является то, что оно обеспечивает значительную гибкость программирования. Например, программу обработки таблицы данных в языке, использующем динамическое связывание типов, можно написать в виде настраиваемой программы. Это означает, что программа сможет работать с данными любого типа. Любой тип входных данных будет приемлемым, поскольку переменные, предназначенные для их хранения, после ввода этих данных будут связываться с соответствующим типом во время присваивания. В отличие от динамического статическое связывание типов не позволяет написать на языках С или Pascal программу обработки таблицы данных без уточнения типа данных.
В языках APL и SNOBOL4 связывание переменных с типом происходит динамически. Например, в программе на языке APL может содержаться следующий оператор:
LIST <- 10.2 5.1 0.0
Независимо от предыдущего типа переменной LIST в результате этого присваивания она станет обозначать одномерный массив длины 3, содержащий числа с плавающей точкой. Если оператор
LIST <- 4.7
будет выполнен после написанного выше присваивания, то переменная LIST станет целочисленной скалярной переменной.
У динамического связывания типов есть два недостатка. Во-первых, поскольку по обе стороны оператора присваивания могут находиться величины двух любых типов, то возможность обнаружения компилятором ошибок снижается по сравнению с языками со статическим связыванием типов. Неверные типы в правой стороне оператора присваивания не будут расценены как ошибки: вместо этого просто произойдет изменение типа левой стороны оператора присваивания на этот неверный тип. Предположим, что в конкретной программе i и х целочисленные переменные, а у массив, содержащий числа с плавающей точкой. Предположим также, что в программе необходим оператор присваивания
1 := x
Однако, при наборе оператор был записан в виде
1:= у
В языке с динамическим связыванием типов ни система компиляции, ни система поддержки выполнения программ не обнаружат ошибку. Тип переменной i просто будет изменен на тип массива, содержащего числа с плавающей точкой. Поскольку вместо правильной перемененной х была использована переменная у, результаты программы будут ошибочными. В языке со статическим связыванием типов компилятор обнаружит ошибку, и программа не будет выполнена.
Отметим, что этот недостаток до некоторой степени присутствует и в таких языках, использующих статическое связывание типов, как FORTRAN, С и C++, которые во многих случаях автоматически преобразовывают тип правой части оператора присваивания в тип его левой части.
Другим недостатком динамического связывания типов является его иена, которая весьма значительна, особенно во время выполнения. Именно в это время должна производиться проверка типов. Более того, каждая переменная должна содержать дескриптор, связанный с ней, для запоминания текущего типа. Память, используемая для хранения переменной, должна быть переменного размера, поскольку значения различных типов требуют различных объемов памяти.
Языки, имеющие динамическое связывание типов переменных, часто реализуются с помощью интерпретаторов, а не компиляторов. Это происходит отчасти из-за сложности динамического изменения типов переменных в машинных кодах. Более того, время, необходимое для динамического связывания типов, перекрывается общим временем интерпретации, так что в этой среде динамическое связывание кажется более дешевым. С другой стороны, языки со статическим связыванием типов редко реализуются с помощью интерпретаторов, поскольку программы, написанные на этих языках, легко могут транслироваться в эффективные версии в машинных кодах.
4.4.2.3. Логический вывод типа
Относительно недавно был разработан язык ML, поддерживающий как функциональное, так и императивное программирование (Miller et al., 1990). Этот язык использует интересный механизм логического вывода типа, в котором типы большинства выражений могут определяться без участия программиста. Например, объявление функции
fun circumf(r) = 3.14159 * r * r;
определяет функцию, аргумент и результат которой имеют дейстэительный тип. Их тип логически выводится из типа константы, входящей в выражение. Подобным образом в функции
fun timeslO(x) = 10 * х;
логически выводится целый тип аргумента и значения функции. Система языка ML отвергнет функцию
fun square(х) = х * х;
Это происходит потому, что тил оператора * определить невозможно. В подобных случаях программист может дать системе подсказку, подобную указанной ниже, в которой функция устанавливается как имеющая тип int.
fun square (х): int = х * х;
Факта, что значение функции указано целочисленным, достаточно для логического вывода, что аргумент также является целым. Вполне дозволены также следующие определения:
fun square(х: int] = х * х;
fun square (x) = {х : int) * х;
fun square(x) = x * (х : int);
Логический вывод типа также используется в чисто функциональных языках про-гаммирования Miranda и Haskell.
4.4.3. Связывание переменных с ячейками памяти и время их жизни
Основные свойства языка программирования в значительной степени определяются разработкой способов связывания ячеек памяти с переменными, которые в них хранятся. Из этого следует важность четкого понимания этих связей.
Ячейки памяти, с которыми связываются переменные, каким-то образом должны извлекаться из пула доступной памяти. Этот процесс называется размещением в памяти (allocation). Удаление из памяти (deallocation) это процесс помещения ячейки памяти, открепленной от переменной, обратно в пул доступной памяти.
(Следует обратить внимание на то, что с ячейкой и переменной, хранящейся в этой ячейке, производятся прямо противоположные действия. Для связывания с некоей переменной свободная ячейка извлекается из пула свободной памяти, а переменная помещается в эту ячейку, т.е. размещается в памяти.
При этом можно отметить, что при разрыве связи между переменной и ячейкой переменная удаляется из памяти, а ячейка возвращается обратно в пул свободной памяти. По этой причине обычно термины размещение и удаление относятся к переменной, а не к ячейке.
Время жизни переменной это время, в течение которого переменная связана с определенной ячейкой памяти. Таким образом, время жизни переменной начинается при ее связывании с определенной ячейкой памяти и заканчивается при ее откреплении от этой ячейки. Для изучения связывания памяти с переменными удобно разделить скалярные (неструктурированные) переменные на четыре категории согласно их временам жизни. Мы назовем эти категории статическими (static), автоматическими (stack-dynamic), явными динамическими (explicit heap-dynamic) и неявными динамическими переменными (implicit heap-dynamic). В следующих разделах рассматриваются эти четыре категории, в том числе их цели, достоинства и недостатки.
4.4.3.1. Статические переменные
Статическими называются переменные, которые связываются с ячейкой памяти до начала выполнения программы и остаются связанными с той же самой ячейкой памяти вплоть до прекращения выполнения программы. Переменные, которые статически связываются с памятью, имеют несколько полезных применений в программировании. Очевидно, что глобальные переменные часто используются на всем протяжении программы, что делает необходимым их привязку к одному месту памяти в течение всего времени выполнения программы. Иногда бывает удобно, чтобы переменные, объявляемые в подпрограммах, зависели от предыстории (history sensitive), т.е. сохраняли свое значение между отдельными выполнениями подпрограммы. Это как раз и является характеристикой переменной, статически связанной с памятью.
Другим плюсом статических переменных является их эффективность. Вся адресация статических переменных может быть прямой, тогда как другие типы переменных часто требуют более медленной косвенной адресации. Более того, на размещение статических переменных в памяти и удаление их из памяти в процессе выполнения программы не затрачивается дополнительное время.
Одним из недостатков статического связывания с памятью является уменьшение гибкости; в частности, в языках, имеющих только статические переменные, не поддерживаются рекурсивные подпрограммы. Еще одним недостатком является невозможность совместного использования памяти несколькими переменными. Предположим, что в программе есть две подпрограммы, причем обеим нужны большие, не связанные между собой массивы. Если они являются статическими, то память, в которой они хранятся, нельзя использовать совместно.
В языках FORTRAN I, II и IV все переменные были статическими. Языки С, C++ и Java позволяют программисту включать в определение локальных переменных спецификатор static, делая их статическими. В языке Pascal статические переменные не предусмотрены.
4.4.3.2. Автоматические переменные
Автоматическими называются переменные, связывание памяти с которыми выполняется при обработке их операторов объявления, но типы которых связываются статически. Обработка (elaboration) такого объявления означает распределение памяти и выполнение процессов связывания, указанных в объявлении. Эти действия происходят при достижении фрагмента кода, с которым связано объявление, в процессе выполнения программы. Следовательно, обработка происходит во время выполнения программы. Например, процедура языка Pascal состоят из раздела объявлений и раздела операторов. Раздел объявлений обрабатывается непосредственно перед началом выполнения раздела операторов, происходящего при вызове процедуры. Память для переменных, находящихся в разделе объявления, выделяется во время обработки объявлений и освобождается после возврата процедурой управления вызывающему оператору. Как показывает их название, память автоматическим переменным выделяется из стека выполняемой программы (run-time stack).
Структура языка ALGOL 60 и последующих языков позволяет использовать рекурсивные подпрограммы. Для того чтобы быть полезными, по крайней мере, в большинстве случаев, рекурсивным подпрограммам требуется некоторая динамическая локальная память для того, чтобы каждая активная копия рекурсивной подпрограммы имела свою собственную версию локальных переменных. Для удовлетворения этих требований используются автоматические переменные. Даже при отсутствии рекурсии наличие доступной для подпрограмм локальной памяти с динамическим стеком имеет свои положительные стороны, поскольку для хранения локальных переменных все подпрограммы совместно используют одну область памяти. Недостатками автоматических переменных являются затраты времени на размещение в памяти и удаление из нее, а также то, что локальные переменные не могут зависеть от предыстории.
Языки FORTRAN 77 и FORTRAN 90 позволяют разработчикам систем их реализации использовать для локальных вычислений автоматические переменные, но содержат оператор
SAVE list
Этот оператор позволяет программисту указать, что некоторые или все перечисленные в списке list переменные в подпрограмме, содержащей данный оператор SAVE, будут статическими.
В языках С и C++ локальные переменные по умолчанию являются автоматическими. В языках Pascal и Ada все определенные в подпрограммах нединамические переменные являются автоматическими.
Все атрибуты, кроме памяти, статически связываются с автоматическими переменными, включения (для некоторых структурированных типов) рассматриваются в главе 5. Размещение в памяти и удаление из нее автоматических переменных описаны в главе 9.
4.4.3.3. Явные динамические переменные
Явные динамические переменные это безымянные (абстрактные) ячейки памяти, размещаемые и удаляемые с помощью явных команд периода выполнения, определяемых программистом. Обращаться к этим переменным можно только с помощью указателей и ссылок. Куча (heap) представляет собой набор ячеек памяти с крайне неорганизованной структурой, вызванной непредсказуемостью их использования. Явные динамические переменные создаются либо оператором (например, в языках Ada и C++), либо вызовом предусмотренной для этого системной подпрограммы (например в языке Сосуществующий в языке C++ оператор распределения памяти new в качестве операнда использует имя типа. При выполнении этого оператора создается явная динамическая переменная, имеющая тип операнда, и возвращается указатель на нее. Поскольку явная динамическая переменная связывается с типом во время компиляции, то это связывание является статическим. Тем не менее, подобные переменные связываются с некоторой ячейкой памяти во время их создания, т.е. при выполнении программы.
Помимо подпрограмм или операторов для создания явных динамических переменных, в некоторых языках есть средства их уничтожения.
В качестве примера явных динамических переменных рассмотрим следующий фрагмент программы на языке C++:
int *intnode;
intnode = new int /*связывает ячейку int */
delete intnode; /*освобождает ячейку, на которую указывает указатель intnode*/
В этом примере явная динамическая переменная, имеющая тип int, создается оператором new. Обращаться к этой переменной можно с помощью указателя intnode. Затем переменная удаляется из памяти оператором delete.
В объектно-ориентированном языке Java все данные, за исключением основных скалярных величин, являются объектами. Объекты языка Java представляют собой явные динамические объекты, и доступ к ним открывают ссылки. В языке Java нет способа явного уничтожения динамических переменных; вместо этого используется неявная "сборка мусора".
Явные динамические переменные часто используются в таких динамических структурах, как связные списки и деревья, которым необходимо расти и/или сокращаться во время выполнения программы. Подобные структуры удобнее создавать с помощью указателей или ссылок, а также явных динамических переменных.
Недостатком явных динамических переменных является сложность корректного использования указателей и ссылок, а также стоимость ссылок на переменные, размещения в памяти и удаления из нее. Эти соображения, указатели и ссылки, а также методы реализации явных динамических переменных подробно рассмотрены в главе 5.
4.4.3.4. Неявные динамические переменные
Неявные динамические переменные связываются с ячейкой динамической памяти только при присваивании им значений. Все их атрибуты фактически связываются каждый раз при присвоении переменным некоторого значения. Эти переменные, в некотором смысле, просто имена, приспособленные для любого использования, которое от них потребуется. Плюсом таких переменных является их высокая степень гибкости, позволяющая писать крайне общие программы. Минусом являются затраты на поддержку во время выполнения программы всех динамических атрибутов, в число которых, помимо прочих, могут входить типы и диапазоны индексов массивов. Другим недостатком является потеря возможности обнаружения компилятором некоторых ошибок (см. раздел 4.4.2.2). В том же разделе приводятся примеры неявных динамических переменных в языке APL. Примеры этих переменных в языке ALGOL 68 даны в главе 2, где они называются массивами flex.
4.5. Проверка типов
Для обсуждения проверки типов обобщим понятия операндов и операторов, чтобы включить в эти понятия подпрограммы и операторы присваивания. Мы будем считать подпрограммы операторами, операндами которых являются их параметры. Символ присваивания мы будем рассматривать как бинарный оператор, а операндами будут его целевая переменная и выражение.
Проверка типов (type checking) обеспечивает совместимость типов операндов оператора. Совместимым (compatible) типом называется тип. который либо изначально допускается для данного оператора, либо правила языка позволяют с помощью команд компилятора неявно преобразовать его в тип. допускаемый для данного оператора. Это автоматическое преобразование называется приведением (coercion). Применение оператора к операнду неприемлемого типа называется ошибкой определения типа (type error).
Если в языке все связывания переменных с типами являются статическими, то проверка типов практически всегда может выполняться статически. Динамическое связывание типов требует проверки типов во время выполнения программы, называющейся динамической проверкой типов.
Некоторые языки, например APL и SNOBOL4, имеющие динамическое связывание типов, допускают только динамическую проверку чипов. Намного легче обнаружить ошибки во время компиляции, чем во время выполнения, поскольку исправление на ранних стадиях, как правило, обходится дешевле. Платой за статическую проверку типов является снижение гибкости программирования. При этом разрешается использовать значительно меньше сокращений и уловок. Правда, подобные технические приемы ценятся сейчас не очень высоко.
Проверка типов осложняется, если язык позволяет хранить в ячейке памяти в разнос время выполнения программы величины разных типов. Это можно сделать, например, с помощью вариантных записей языков Ada и Pascal, оператора EQUIVALENCE языка FORTRAN и объединений языков С и C++. В этих случаях проверка типов, если она производится, должна быть динамической, кроме того, она требует, чтобы система поддержки выполнения программ хранила типы текущих значений, записанных в таких ячейках памяти. Таким образом, даже если в языках, подобных С и Pascal, все переменные статически связаны с типами, не все ошибки определения типа будут выявлены при статической проверке типов.
4.6. Строгая типизация
Одной из новых идей в области структуры языков, проявившейся во время так называемой структурной революции в программировании 1970-х годов, является строгая типизация (strong typing). Строгая типизация общепризнанна как крайне полезная концепция. К сожалению, довольно часто она определяется неточно, а иногда в компьютерной литературе используется вообще без определения.
Ниже следует простое, но неполное определение строго типизированного языка. В программах, написанных на таком языке, каждое имя имеет отдельный связанный с ним тип, причем этот тип известен во время компиляции. Сутью этого определения является то, что все типы связываются статически. Слабостью этого определения является игнорирование в нем следующей возможности: хотя тип переменной и может быть известен, ячейка памяти, с которой связана эта переменная, в разное время может содержать величины разных типов. Для того чтобы учесть эту возможность, мы определим строго типизированный язык как такой, в котором всегда обнаруживаются ошибки типов. Для этого требуется, чтобы типы всех операндов могли определяться либо во время компиляции, либо во время выполнения. Важность строгой типизации заключается в возможности выявления всех неправильных употреблений переменных, приводящих к ошибкам определения типов. Строго типизированные языки позволяют также обнаружить (в процессе выполнения) использование величин некорректных типов в переменных, которые могут содержать величины нескольких типов.
Язык FORTRAN не принадлежит к строго типизированным языкам, поскольку отношения между фактическими и формальными параметрами не подвергаются проверке соответствия типов. Кроме того, использование оператора EQUIVALENCE между переменными различных типов позволяет переменной одного типа ссылаться на переменную другого типа, причем, если на одну из переменных, входящих в оператор EQUIVALENCE, ссылаются или присваивают ей значение, то система не может проверить тип этой величины. Проверка типов переменных, входящих в оператор EQUIVALENCE, фактически сводит па нет большинство достоинств этих переменных.
Язык Pascal относится к почти строго типизированным языкам, причем этим "почти" он обязан своей структуре вариантных записей, поскольку последние позволяют опускать метку, содержащую текущий тип переменной и являющуюся средством проверки корректного типа величины. Вариантные записи и потенциальные проблемы, возникающие при их использовании, рассматриваются в главе 5.
Язык Ada также является почти строго типизированным языком. Ссылки на переменные, входящие в вариантные записи, динамически проверяются на наличие величин корректных типов. Это значительное усовершенствование по сравнению с языками Pascal и Modula-2, в которых проверка вообще невозможна и намного менее необходима. Однако язык Ada позволяет программистам нарушать правила проверки типов, вводя отдельное требование о временной отсрочке проверки для конкретного преобразования типов. Такая отсрочка возможна только при использовании библиотечной функции UNCHECKED_CONVERSION. Эта функция, версия которой может существовать для любого типа данных, принимает переменную ее типа в качестве параметра и возвращает сроку битов, представляющую собой текущее значение этой переменной. Действительного преобразования при этом не происходит; это просто средство извлечения значения переменной одного типа и использования его в качестве значения переменной другого типа. Это может оказаться полезным в определяемых пользователем операциях по размещению переменных в памяти и освобождению памяти, при которых с адресами обращаются как с целыми числами, в то время как они должны использоваться как указатели. Поскольку в функции UNCHECKED_CONVERSIQN нет проверки типов, то за осмысленное использование переменных, полученных из этой функции, ответственность несет программист.
Те же функции, что и функция UNCHECKED_CONVERSION языка Ada, выполняет имеющаяся в языке Modula-З встроенная процедура LOOPHOLE.
Языки С и C++ не относятся к строго типизированным языкам, поскольку в них допускается существование функций, тип параметров которых не проверяется. Более того, типы объединений этих языков также не проверяются.
Строго типизированным языком является язык ML, правда с некоторыми отличиями от императивных языков. В языке ML есть переменные, все типы которых известны статически или из объявлений, или из правил логического вывода типов, описанных в разделе 4.4.2.3.
Язык Java, хотя он в значительной степени создан на основе языка С++, является строго типизированным в том же смысле, что и язык Ada. Типы могут приводиться явно, что может вызвать ошибку определения типа. Тем не менее, не существует неявных путей, позволяющих ошибкам определения типа остаться незамеченными.
Правила приведения типов языка значительно влияют на результат проверки типов. Например, выражения в языке Pascal являются строго типизированными. Несмотря на это, допускается использование арифметического оператора с одним операндом, являющимся числом с плавающей точкой (в языке Pascal называемым real) и одним целым операндом. Значение целого операнда приводится к виду числа с плавающей точкой, и в результате получается операция над числами с плавающей точкой. Обычно это именно то, что задумывал программист. Тем не менее, это также приводит к потере одной ил причин строгой типизации обнаружения ошибок. Таким образом, приведение типов снижает результат строгой типизации. Языки, в которых широко используется приведение типов, например FORTRAN, С и C++, значительно менее надежны, чем языки, в которых приведение типов применяется нечасто, например Ada. В языке Java содержится в два раза меньше разновидностей приведений типов, чем в языке C++. Подробнее вопрос приведения типов освещается в главе 6.
4.7. Совместимость типов
Понятие совместимости типов было определено при освещении вопроса проверки типов. В данном разделе мы рассмотрим различные правила совместимости типов. Структура существующих в языке правил совместимости типов важна, поскольку она влияет на структуру типов данных и операции, производимые над величинами этих типов. Вероятно, важнейшим следствием того, что две переменные имеют совместимые типы, является то, что любой из них может быть присвоено значение другой.
Существуют два различных вида совместимости типов: совместимость имен типов и совместимость структур типов. Совместимость имен типов (name type compatibility) означает, что две переменные имеют совместимые типы только в том случае, если они были объявлены в одном объявлении или в объявлении, использующем одно и то же имя типа. Совместимость структур типов (structure type compatibility) означает, что две переменные имеют совместимые типы в том случае, если у их типов одинаковые структуры. Существуют некоторые разновидности этих двух методов, и в большинстве языков используются комбинации различных способов.
Совместимость имен типов легко реализуется, но крайне ограничивает программиста. При строгой интерпретации переменная, принадлежащая к ограниченному типу целых чисел, не будет совместимой с переменной, имеющей целый тип. Предположим, что в языке Pascal используется строгая совместимость типов имен, и рассмотрим следующий фрагмент программы:
type indextype = 1..100; (ограниченный тип)
var
count : integer;
index : indextype;
Переменные count и index не будут совместимы; значение переменной count не сможет присваиваться переменной index, и наоборот.
Другая проблема, связанная с совместимостью имен типов, возникает при передаче структурированного типа между подпрограммами через параметры. Такой тип должен определяться только один раз, глобально. Подпрограмма не может устанавливать тип подобных формальных параметров локально, как было в исходной версии языка Pascal.
Совместимость структур типов значительно более гибкая, чем совместимость имен типов, но гораздо сложнее реализуется. При определении совместимости имен типов должны сравниваться только имена двух типов, а при использовании совместимости структур типов целые структуры двух типов. Выполнить это сравнение не всегда легко. (Рассмотрите, например, такую структуру данных, ссылающуюся на собственный тип, как связный список.) При этом может возникнуть еще один вопрос. Являются ли, например, два комбинированных или структурных типа совместимыми, если они имеют одинаковую структуру, но разные имена полей? Совместимы ли два одномерных массива в программе на языке Pascal или Ada, если они содержат элементы одного типа, но различаются областью значений индекса: 0. .10 и 1.. 11? Совместимы ли два перечислимых типа, если они содержат одинаковое число компонентов, но по-разному образовывают литеральные константы?
Еще одной трудностью, связанной с совместимостью структур типов, является то, что она не признает различий между типами, имеющими одинаковую структуру. Рассмотрим следующее объявление, которое могло бы появиться в программе на языке Pascal:
type
Celsius = real;
Fahrenheit = real;
Переменные указанных типов считаются совместимыми при проверке совместимости структур типов. Это позволяет им смешиваться в выражениях, что, очевидно, в данном случае нежелательно. Вообще, типы с различными именами, вероятнее всего, являются абстракциями различных категорий сущностей задачи, и не должны рассматриваться как эквивалентные.
В исходном определении языка Pascal (Wirth, 1971) явно не устанавливается, когда должна использоваться совместимость структур типов, а когда совместимость их имен. Это крайне вредно для мобильности программ, поскольку программа, корректная в одной системе реализации языка, не должна быть некорректной в другой. Стандарт языка Pascal, созданный Международной организацией по стандартизации (ISO, 1982), явно устанавливает правила совместимости типов для данного языка, частично по имени, частично по структуре. В большинстве случаев используется структура, а имя применяется для формальных параметров и в некоторых других ситуациях. Рассмотрим, например, следующие объявления.
type
type1 = array [1:.10] of integer;
type2 = array [1..10] of integer;
type3 = type2;
В этом примере типы typel и type2 несовместимы, что свидетельствует об использовании совместимости структур типов. Более того, тип type2 совместим с типом type3, из чего можно заключить, что эквивалентность имен также не используется строго. Такая форма совместимости иногда называется эквивалентностью объявлений (declaration equivalence), поскольку при определении типа с помощью имени другого типа, оба они являются совместимыми, даже если они несовместимы по именам типов.
В языке Ada используется совместимость имен типов, но при этом имеются две конструкции подтипы и производные типы, позволяющие устранить проблемы, возникающие при этом виде совместимости. Производным (derived) называется новый тип. основанный на некотором ранее определенном типе, с которым он несовместим, несмотря на то, что они имеют идентичную структуру. Производные типы наследуют все свойства родительских типов. Рассмотрим следующий пример:
type Celsius is new FLOAT;
type fahrenheit is new FLOAT;
Переменные данных типов несовместимы, хотя и имеют идентичную структуру. Более того, переменные этих типов несовместимы ни с каким другим типом чисел с плавающей точкой. Исключением из правила являются только литеральные константы. Литеральная константа, например 3. 0, имеет тип универсальных действительных чисел и совместима с любым типом чисел с плавающей точкой. Производные типы также могут содержать ограничения диапазона родительского типа, наследуя при этом все его операции.
Подтип (subtype) в языке Ada версия существующего типа с возможно ограниченным диапазоном. Подтип совместим с породившим его типом. Рассмотрим следующее объявление:
subtype SMALL JTYPE is INTEGER range 0..99;
Переменные, имеющие тип SMALL_TYPE, совместимы с переменными, имеющими тип INTEGER.
Правила совместимости типов в языке Ada более важны, чем соответствующие правила языков, в которых широко используется приведение типов. Например, два операнда, входящие в оператор сложения в языке С, могут иметь практически любую комбинацию числовых типов этого языка. Один из операндов при этом просто приводится к типу другого. Однако в языке Ada нет приведения типов операндов арифметического оператора.
В языке С используется структурная эквивалентность для всех типов, за исключением структур (записи языка С) и объединений, для которых используется эквивалентность объявлений. Правда, если две структуры или объединения определяются в двух различных файлах, используется эквивалентность типов структур.
В языке C++ используется эквивалентность имен. Отметим, что оператор typadaf языков С и C++ не вводит новый тип. Он просто определяет новое имя для уже существующего типа.
Во многих языках переменные могут объявляться без использования имен типа, создавая безымянные типы. Рассмотрим следующий пример из языка Ada:
А: array (1..10) of INTEGER;
В этом случае переменная А имеет безымянный, но неоднозначно определенный тип. В объявлении
В: array (1..10) of INTEGER;
переменные А и В будут принадлежать к безымянным, но различным и несовместимым типам, хотя они имеют идентичную структуру. Множественное объявление
С, D: array (1..10) of INTEGER;
создаст два безымянных типа: один для переменной С, другой для переменной D, несовместимых между собой. Фактически эти объявления можно рассматривать как следующие два объявления:
С: array (1..10) of INTEGER;
D: array (1..10) of INTEGER;
Результатом этого будет несовместимость переменных С и D. Однако в объявлении
type LIST_10 is array (1..10) of INTEGER;
C, D: LIST_10;
переменные С и D будут совместимыми.
Очевидно, что в языках, не позволяющих пользователям определять и называть типы, например FORTRAN и COBOL, эквивалентность имен использоваться не может.
Возникновение таких объектно-ориентированных языков, как Java и C++, подняло вопрос о другом типе совместимости типов. Это вопрос о совместимости объектов и его связи с иерархией наследования. Подробнее об этом рассказывается в главе 11.
Совместимость типов в выражениях рассмотрена в главе 6; совместимость типов параметров подпрограмм описана в главе 8.
4.8. Область видимости
Одной из важнейших характеристик переменных является область видимости. Область видимости (scope) переменных программы это ряд операторов, в которых переменная видима. Переменная является видимой (visible) в операторе, если к переменной, входящей в оператор, можно обратиться.
Правила обзора данных в языке определяют, как именно конкретное появление имени связано с переменной. В частности, правила обзора данных определяют, каким образом ссылки на переменные, объявленные вне выполняющейся, в данный момент подпрограммы или блока, связаны с их объявлениями и, вследствие этого, с их атрибутами (блоки рассматриваются в разделе 4.8.2). Следовательно, для написания или чтения программ на данном языке необходимо полное знание этих правил.
Как определялось в разделе 4.4.3.2, переменная является локальной в программной единице или блоке, если она там объявлена. (В данной главе мы считаем программными единицами главный программный модуль или подпрограммы. Единицы, подобные классам языков C++ или Java, рассматриваются в главе 10.) Нелокальными переменными (nonlocal variables) программной единицы или блока называются переменные, которые видимы в этой программной единице или блоке, но не объявляются в них.
4.8.1. Статическая область видимости
В языке ALGOL 60 был введен метод связывания имен с нелокальными переменными, названный статическим обзором данных (static scoping). Позже этот метод был позаимствован большинством императивных, а также многими не императивным к языками. Использование статического обзора данных получило свое название из-за возможности статического (т.е. до периода выполнения) определения области видимости любой переменной.
Большинство отдельных статических областей видимости в императивных языках ;вязаны с определениями программных единиц. Предположим, что все области видимости связаны с программными единицами. В данной главе мы также будем полагать, что для обращения к нелокальным переменным в обсуждаемых языках используются только области видимости. Последнее не совсем справедливо даже для языков со статическим .хлором данных, но такое предположение упрощает обсуждение. Дополнительные мето-:ы обращения к нелокальным переменным рассматриваются в главе 8.
Во многих языках подпрограммы создают собственные области видимости. Во всех распространенных языках со статическим обзором данных, за исключением языков С, С++, Java и FORTRAN, подпрограммы могут вкладываться в другие подпрограммы, что создает в программе иерархию областей видимости.
Когда в языке, использующем статический обзор данных, компилятор обнаруживает переменную, ее атрибуты определяются путем поиска объявившего ее оператора. В языках, использующих статический обзор данных, при наличии вложенных подпрограмм этот процесс протекает следующим образом. Предположим, что сделано обращение к переменной х подпрограммы sub1. Соответствующее объявление вначале разыскивает х в объявлениях подпрограммы sub1. Если для данной переменной объявления не найдено, то поиск продолжается в объявлениях подпрограммы, объявившей подпрограмму sub1, называемой статическим родителем (static parent) подпрограммы sub1. Если объявление переменной х не найдено и в этой подпрограмме, то поиск продолжается в следующей внешней единице (в модуле, объявившем родителя подпрограммы sub1) и так далее, пока не будет найдено объявление переменной х, или поиск в самом внешнем блоке не увенчается успехом. В последнем случае будет зафиксирована ошибка необъявленной переменной. Статический родитель подпрограммы sub1, его статический родитель и так далее вплоть до основной программы называются статическими предками (.ic ancestors) подпрограммы sub1. Отметим, что методы реализации статического обзора данных, рассматриваемые в главе 9, значительно эффективнее, чем только что описанный процесс.
Рассмотрим следующую процедуру языка Pascal:
procedure big;
var
x,i integer;
procedure sub1;
begin { sub1 }
end; { sub1 }
procedure sub2;
var
x: integer;
begin { sub2 }
end; { sub2 }
begin ( big }
end; { big }
При использовании статического обзора данных ссылка на переменную х полпрограммы sub1 относится к переменной х, объявленной в процедуре big. Это действительно так, поскольку поиск объявления переменной х начался в той процедуре, в которой встречается ссылка, но объявления этой переменной в ней найдено не было. Далее поиск продолжился в статическом родителе подпрограммы sub1 (подпрограмме big), в котором и было найдено объявление переменной х.
Наличие предопределенных имен, рассмотренных в разделе 4.2.3, несколько затрудняет описанный процесс. Иногда предопределенное имя подобно ключевому слову и может переопределяться пользователем. В таких случаях предопределенное имя используется, только если пользовательская программа не содержит переопределения. В других случаях предопределенное имя может быть зарезервировано, что означает качало поиска значения данного имени в списке предопределенных имен, который выполняется даже до проверки объявлений локальной области видимости.
В языках со статическим обзором данных объявления некоторых переменных могут быть скрыты от некоторых подпрограмм. Рассмотрим следующую скелетную программу на языке Pascal:
program main;
var x : integer;
procedure subl;
var x : integer;
begin ( sub1 }
...x...
end; ( sub1 }
begin (main}
end. {main}
Ссылка на переменную х в процедуре sub1 относится к объявившей переменную х процедуре subl. В этом случае переменная х программы main скрыта от команд процедуры subl. Вообще, объявление переменной эффективно скрывает любое объявление одноименной переменной, содержащейся во внешней области видимости.
В языке Ada к переменным, скрытым от областей видимостей предков, можно получить доступ с помощью селективных ссылок, содержащих имя области видимости предка. В предыдущей программе, например, к переменной х процедуры sub1 можно обратиться с помощью ссылки main. х.
Несмотря на то что в языках С и C++ не разрешено использование подпрограмм, вложенных в определения других подпрограмм, глобальные переменные в этих языках есть. Эти переменные объявляются вне определения любой подпрограммы. Как и в языке Pascal, локальные переменные могут скрывать эти глобальные переменные. В языке C++ к таким скрытым глобальным переменным можно обращаться с помощью оператора доступа (::). Например, если переменная х является глобальной переменной, скрытой в подпрограмме локальной переменной х, то обратиться к глобальной переменной можно в форме ::х.
4.8.2. Блоки
Многие языки позволяют создавать новые статические области видимости во время выполнения программы. Эта мощная концепция, впервые появившаяся в языке ALGOL 60, позволяет фрагменту программы иметь собственные локальные переменные с минимизированной областью видимости. Подобные переменные, как правило, являются автоматическими, так что память выделяется им в начале выполнения фрагмента программы, а освобождается по окончании его выполнения. Подобный фрагмент программного кода получил название блока (block).
Как показано ниже, в языке Ada блоки задаются оператором declare:
declare TEMP: integer;
begin
TEMP:= FIRST;
FIRS?:= SECOND;
SECOND:= TEMP;
end;
От понятия "блок" произошло выражение язык с блочной структурой (block structured language). Хотя языки Pascal и Modula-2 и называются языками с блочное структурой, они не имеют непроцедурных блоков.
В языках С, C++ и Java любой составной оператор (последовательности операторов, заключенной в фигурные скобки) может содержать объявления и таким образом определять новую область видимости. Такие составные операторы являются блоками. Например, если list массив целых чисел, то можно написать следующий код:
if (list[i] < list[j]) {
int temp;
temp = list[i];
list [i] = list [ j];
list [j] = temp;
}
Области видимости, создаваемые блоками, трактуются точно так же, как области видимости, создаваемые подпрограммами. Обращения к переменным блока, не объявленным в этом блоке, связываются с их объявлениями путем поиска по возрастающей во вешних областях.
В языках C++ и Java определять переменные можно в любом месте функции. Если определение появляется не в начале функции, то область видимости данной переменной начинается с оператора определения и заканчивается концом функции.
Оператор for языков C++ и Java позволяет определять переменные в выражениях. инициализирующих счетчики цикла. В ранних версиях языка C++ область видимости такой переменной начиналась с ее определения и заканчивалась в конце наименьшего блока, содержащего данную переменную. Впрочем, в предварительной стандартной версии область видимости таких переменных была ограничена телом цикла for, так же поступили и в языке Java.
Определения класса и метода в объектно-ориентированных языках программирования также порождают вложенные статические области видимости. Этот вопрос рассматривается в главе 11.
4.8.3. Оценка статического обзора данных
Использование статических областей видимости представляет собой метод нелокального доступа, хорошо работающий во многих ситуациях. Впрочем, у этого метода есть и недостатки. Рассмотрим программу, скелетная структура которой показана на рис. 4.1. При этом будем считать, что все области видимости создаются определениями основной программы и процедур.
Программа содержит общую область видимости для блока main, помимо этого существуют две процедуры А и В, определяющие области видимости внутри блока main. Внутри процедуры А расположены области видимости процедур С и D. Внутри процедуры в находится область видимости процедуры Е. Мы предполагаем, что необходимое обращение к данным и процедурам определяется структурой рассматриваемой программы. Требуемое обращение к процедуре имеет следующий вид: программа main может вызывать блоки А и В, блок А блоки С и D, а блок В блоки А и Е.
Структуру программы удобно представить в виде дерева, каждый узел которого представляет процедуру и, следовательно, область видимости. Представление программы, показанной на рис. 4.а, изображено на рис. 4.б. Структура этой программы может показаться весьма естественной организацией программы, отчетливо отражающей структурные требования.
На рис. 4.в показаны требуемые вызовы рассматриваемой программы. Различия между рис. 4.б и 4.в показывают число возможных, а не необходимых вызовов.
Программист может по ошибке вызвать подпрограмму, вызов которой не должен допускаться, причем это действие не будет расценено компилятором как ошибка. В результате ошибка будет обнаружена только при выполнении программы, что может повысить стоимость ее исправления. Следовательно, доступ к процедурам должен быть ограничен только необходимыми процедурами.
С этой проблемой связано слишком интенсивное обращение к данным. Например, все переменные, объявленные в основной программе, видимы для всех процедур, причем избежать этого невозможно.
Следующий сценарий описывает другие проблемы, возникающие при использовании статического обзора данных. Предположим, что после разработки и тестирования программы потребовалось изменить ее спецификацию. В частности, необходимо открыть процедуре Е доступ к некоторым переменным из области видимости процедуры D. Для того чтобы решить возникшую проблему, можно переместить процедуру Е внутрь области видимости процедуры D. Однако в этом случае процедура Е уже не сможет обращаться к области видимости процедуры В, что, по-видимому, требуется (иначе зачем она там находится?). Другим решением является перемещение переменных, определяемых в процедуре D и необходимых в процедуре Е, в блок main. Это позволит обращаться к ним переменным из любой процедуры, которых может оказаться больше, чем надо, что может спровоцировать некорректный доступ. Например, неверно написанный идентификатор процедуры может быть воспринят не как ошибка, а как ссылка на идентификатор в некоторой внешней области видимости. Предположим, что переменная, перемещенная в блок main, называется х и необходима в процедурах D и Е. Допустим, что переменная с этим же именем объявляется в процедуре А. Это приведет к сокрытию требуемой переменной х от ее изначального владельца процедуры D.
Последней проблемой, возникающей при перемещении объявления переменной х в блок main, является пагубное влияние этого действия на читабельность, выражающееся в том, что объявления переменных находятся слишком далеко от места их использования.
Проблемы, связанные с видимостью переменных при использовании статических областей видимости, характерны и для обращений к подпрограммам. Предположим, что в программе, изображенной на рис. 4.б, вследствие некоторых изменений спецификации возникла потребность вызова процедурой Е процедуры D. Чтобы это стало возможным, следует вложить процедуру D непосредственно в блок main, предполагая, что эта процедура также требуется процедурам А или С. При этом перемещении процедура D теряет доступ к переменным, определенным в процедуре А. Этот способ решения проблемы, если использовать его часто, приведет к возникновению программ с длинными списками служебных процедур низкого уровня.
Таким образом, нарушение правил статического обзора данных может привести к тому, что структуры программ будут лишь отдаленно походить на оригинал, причем даже в тех областях программы, в которых изменения не проводились. Разработчиков поощряют использовать излишнее количество глобальных переменных. Все процедуры в конце концов окажутся вложенными в основную программу на одном и том же уровне. При этом будут использоваться не более глубокие уровни вложения, а глобальные переменные. Кроме того, окончательная структура может быть неуклюжей, запутанной и не соответствовать основной структурной концепции. У статического обзора данных есть и другие недостатки, подробно рассмотренные в книге Clarke, Wileden and Wolf (1980). Для решения проблем, связанных со статическим обзором данных, во многих новейших языках программирования используется конструкция инкапсуляции, подробно рассмотренная в главе 8.
4.8.4. Динамические области видимости
Область видимости переменных в таких языках, как APL, SNOBOL4 и ранние версии языка LISP, является динамической. Динамический обзор данных (dynamic scoping) опирается на последовательность вызова подпрограмм, а не на их пространственную взаимосвязь. Следовательно, область видимости можно определить только во время выполнения программы.
Рассмотрим повторно процедуру big из раздела 4.8.1:
procedure big;
var x : integer;
procedure subl;
begin { subl }
...x...
end; { subl }
procedure sub2;
vаr
x : integer;
begin { sub2 }
еnd; { sub2 )
begin { big }
end; { big }
Предположим, что правила динамического обзора данных применимы к нелокальным ссылкам. Значение идентификатора х, к которому обращаются в подпрограмме sub1, динамическое, оно не может определяться во время компиляции. В зависимости от последовательности вызова это значение может относиться к переменной из предыдущего объявления переменной х.
Для того чтобы определить корректное значение переменной х во время выполнения программы, можно начать его поиск среди локальных объявлений. Этим же способом начинается поиск и при статическом обзоре данных, правда, на этом их сходство заканчивается. При неудачном завершении поиска среди локальных объявлений рассматриваются объявления динамического родителя, или вызывающей процедуры. Если объявление переменной х не будет найдено и там, то поиск продолжается в динамическом родителе этой процедуры, и так далее, пока не будет найдено объявление переменной х. Если оно не будет найдено ни в одном из динамических предков, то это будет ошибкой периода выполнения программы (run-time error).
Рассмотрим две различные последовательности вызовов процедуры sub1 в приведенном выше примере. В первом случае процедура big обращается к процедуре sub2, которая вызывает процедуру sub1. При этом поиск перейдет от локальной процедуры sub1 к вызывающей ее процедуре sub2, в которой находится объявление переменной х. Таким образом, в данном случае обращение к переменной к процедуры sub1 является обращением к переменной х, объявленной в процедуре sub2. Во втором случае процедура sub1 вызывается непосредственно из процедуры big. При этом динамическим родителем процедуры sub1 является процедура big, и обращение будет направлено к переменной х, объявленной в процедуре big.
4.8.5. Оценка динамического обзора данных
Воздействие динамического обзора данных на программирование значительно. Корректные атрибуты нелокальных переменных, видимые операторам программы, невозможно определить статически. Более того, такие переменные не всегда одинаковы. Оператор подпрограммы, содержащей ссылки на нелокальные переменные, во время различных вызовов этой подпрограммы может обращаться к различным нелокальным переменным. Некоторые проблемы программирования связаны непосредственно с динамическим обзором данных.
Во-первых, в период между вызовом подпрограммы и ее завершением все локальные переменные этой подпрограммы видимы для всех выполняющихся подпрограмм, вне зависимости от их буквальной близости. От такой общедоступности локальных переменных нет защиты. Подпрограммы всегда выполняются в непосредственной среде вызывающей программы; следовательно, использование динамического обзора данных порождает менее надежные программы, чем использование статического обзора.
Второй проблемой, связанной с динамическим обзором данных, является невозможность статической проверки типов при обращении к нелокальным переменным. Это происходит из-за невозможности статически определить объявление переменной, на которую ссылаются как на нелокальную.
Использование динамического обзора данных также затрудняет чтение программ, поскольку для определения нелокальных переменных, на которые имеются ссылки, должна 5ыть известна последовательность вызова подпрограмм. Если программу читает человек, то определить последнее практически невозможно.
В заключение отметим, что обращение к нелокальным переменным в языках с динамическим обзором данных происходит значительно медленнее, чем обращение в языках со статическим обзором. Причины этого подробно описаны в главе 9.
С другой стороны, у динамического обзора данных есть и достоинства. В некоторых случаях параметры, передаваемые от одной подпрограммы к другой, являются простыми переменными, определяемыми в вызывающем модуле. В языках с динамическим обзором данных не требуется передачи ни одной из этих переменных, поскольку все они неявно видимы в вызываемой подпрограмме.
Нетрудно понять причины более широкого распространения статического, а не динамического обзора данных. Программы на языках со статическим обзором данных легче читать, они надежнее и выполняются быстрее, чем аналогичные программы на языках, использующих динамический обзор. Именно по этим причинам в большинстве современных диалектов языка LISP динамический обзор данных был заменен статическим. Далее в главе 9 рассматриваются методы реализации статического и динамического обзора данных.
4.9. Область видимости переменных и время их жизни
Иногда область видимости и время жизни переменных оказываются связанными между собой. Рассмотрим, например, переменную, объявленную в процедуре языка Pascal, не содержащей вызовов подпрограмм. Область видимости такой переменной от ее объявления до зарезервированного слова еnd в процедуре. Время жизни такой переменной начинается при входе в процедуру и заканчивается при достижении команды end (в языке Pascal нет оператора возврата). Хотя и очевидно, что область видимости и время жизни переменной не совпадают, поскольку статическая область видимости представляет собой буквальную (или пространственную) концепцию, а время жизни концепция временная, в данном случае эти две концепции связаны между собой, или, по крайней мере, кажутся такими.
В других случаях связь между областью видимости и временем жизни переменной не настолько очевидна. В языках С и C++, например, переменная, объявленная в функции с использованием спецификатора static, статически связана с областью видимости этой функции и с памятью. Таким образом, ее область видимости статична и локальна по отношению к функции, но ее время жизни распространяется на все выполнение программы, частью которой она является.
Область видимости и время жизни не связаны также при вызовах подпрограмм. Рассмотрим следующую функцию языка C++:
void printheader() {
} /* конец функции printheader */
void compute() {
int sum;
printheader();
} /* конец функции compute */
Область видимости переменной sum полностью находится внутри функции compute. Она не распространяется на тело функции printheader, хотя эта функция выполняется в середине функции compute. Тем не менее, время жизни переменной sum распространяется на время выполнения функции printheader. Какая бы ячейка памяти не была выделена переменной sum до вызова функции printheader, эта связь сохраняется во время и после выполнения этой функции.
4.10. Среды ссылок
Средой ссылок (referencing environment) оператора называется совокупность всех имен, видимых в данном операторе. Среда ссылок оператора в языках со статическим обзором данных состоит из переменных, объявленных в его локальной области видимости, и совокупности всех видимых переменных из областей видимости его предков. В таком языке среда ссылок оператора нужна во время компиляции оператора, поэтому можно создавать команды и структуры данных, позволяющие доступ к переменным из других областей видимости во время выполнения программы. Методы реализации сред ссылок на нелокальные переменные в языках, использующих статический и динамический обзор данных, рассматриваются в главе 9.
В языке Pascal, в котором области видимости создаются исключительно определениями процедур, среда ссылок оператора содержит локальные переменные, все переменные, объявленные в процедурах, содержащих данный оператор, а также переменные, объявленные в основной программе (за исключением переменных в нелокальных областях видимости, скрытых объявлениями ближних процедур). Каждое определение процедуры создает новую область видимости, а следовательно, и новую среду. Рассмотрим следующую скелетную программу на языке Pascal:
program example;
var
a,b: integer;
procedure sub1;
var
x,у: integer;
begin { sub1 }
. . . <------------------------- 1
end; { sub1 }
procedure sub2;
var
x: integer;
. . .
procedure sub3;
var
x: integer;
begin { sub3 }
. . . <------------------------ 2
end; { sub3 }
begin { sub2 }
. . . <------------------------- 3
end; { sub2 }
begin { example }
. . . <-------------------------- 4
end. { example }
В указанных точках данная программа имеет следующие среды ссылок:
Точка |
Среда ссылок |
1 |
переменные x и у процедуры sub1, переменные а и Ь программы example |
2 |
переменная х процедуры sub3, (переменная а процедуры sub2 скрыта), переменные а и Ь программы example |
3 |
переменная х процедуры sub2, переменные а и b программы example |
4 |
переменные а и Ь программы example |
Рассмотрим теперь объявления переменных в скелетной программе. Отметим, во-первых, что, хотя область видимости процедуры sub1 и выше уровнем (она менее глубоко вложена), чем область видимости процедуры sub3, область видимости процедуры sub1 не является статическим предком процедуры sub3, поэтому процедура sub3 не имеет доступа к переменным, объявленным в процедуре sub1. Для этого существует важная причина. Переменные, объявленные в процедуре sub1, являются автоматическими, поэтому они не связаны с памятью в то время, когда процедура sub1 не выполняется. Поскольку процедура sub3 может выполняться в то время, когда процедура sub1 не выполняется, она не должна иметь доступа к переменным процедуры sub1, которые не обязательно должны связываться с ячейками памяти во время выполнения процедуры sub3.
Подпрограмма называется активной, если ее выполнение началось, но еще не завершилось. Среда ссылок оператора в языке с динамическим обзором данных состоит из локально объявленных переменных и переменных всех других активных на данный момент подпрограмм. Повторимся, некоторые переменные активных подпрограмм могут быть скрыты от среды ссылок. Новые активации подпрограммы могут содержать объявления переменных, скрывающих переменные с теми же именами в предыдущих активациях подпрограммы.
Рассмотрим следующую программу. Предположим, что единственными возможными вызовами функций являются следующие: функция main вызывает функцию sub2, которая вызывает функцию sub1.
void subl() {
int a, b;
...<-------------------------------------
} /* конец функции subl */
void sub2 () {
int b, c;
... <-------------------------- 2
subl ;
} /* конец функции sub2 */
void main() {
int c, d;, ,
...<------------------------------------ 3
sub2(); } /* конец функции main */
В указанных точках данная программа имеет следующие среды ссылок:
Точка |
Среда ссылок |
переменные а и b процедуры sub1, переменная с процедуры sub2, переменная d функции main (переменная с функции main и переменная b процедуры sub2 скрыты) |
|
переменные b и с процедуры sub2, переменная d блока main (переменная с функции main скрыта) |
|
переменные end функции main |
4.11. Именованные константы
Именованной константой (named constant) называется переменная, связываемая со своим значением только во время связывания ее с ячейкой памяти; значение именованной константы невозможно изменить оператором присваивания или ввода. Данный объект помогает улучшить читабельность программы: значительно удобнее, например, использовать имя pi, а не константу 3.14159.
Другим полезным применением именованных констант являются программы, обрабатывающее заданное количество данных, например, 100. В подобных программах во многих местах, как правило, используется константа 100 для объявления диапазонов значений индексов массивов, пределов изменения счетчиков цикла и т.п. Рассмотрим следующую скелетную программу на языке Pascal:
program example; type
intarray = array [1..100J of integer; realarray = array [1..100] of real;
begin { example }
for index := 1 to 100 do begin
end;
for count := 1 to 100 do begin
end;
average := sum div 100;
end. {example}
Для того чтобы эту программу модифицировать для работы с другим количеством значений, следует обнаружить все места, в которых написана цифра 100, и все числа заменить новыми. В большой программе это может оказаться трудоемким и подверженным ошибкам процессом. Более легким и надежным методом является использование ■:ме нов энной константы:
program example;
count listlen = 100; type
intarray = array [1..listlen] of integer;
realarray = array [1..listlen] of real;
begin { example }
for index := 1 to listlen do
end;
for count := 1 to listler. do begin
end;
average := sum div listlen;
end. {example}
Если в такой программе потребуется изменить количество данных, то изменять придется только одну строку, вне зависимости от количества упоминаний константы 100 в программе. Вот еще один пример преимущества абстракции: имя listlen является абстракцией для количества элементов некоторых массивов и количества повторений некоторых циклов. Приведенный пример иллюстрирует, как именованные константы могут облегчить модификацию программы.
Объявление именованных констант в языке Pascal требует просто наличия некоторого значения справа от оператора =. Вместе с тем, языки Modula-2 и FORTRAN 90 позволяют использовать константные выражения, которые могут содержать предварительно объявленные именованные константы, постоянные величины и операторы. Язык Pascal ограничивается только константами, а язык Modula-2 константными выражениями, поскольку в этих языках используется статическое связывание величин с именованными константами. Именованные константы в языках со статическим связыванием величин иногда еще называются манифестными константами (manifest constants),
Языки Ada, C++ и Java допускают динамическое связывание величин с именованными константами. Это позволяет в объявлениях присваивать константам выражения, содержащие переменные. Например, оператор
МАХ: constant integer:= 2 * WIDTH + 1
языка Ada объявляет переменную МАХ именованной константой целого типа, значение которой соответствует значению выражения 2 * WIDTH + 1, причем значение переменной WIDTH должно быть видимым при выделении памяти константе МАХ и связывании этой константы со своим значением. Помимо этого, язык Ada допускает существование именованных констант перечислимого и структурированного типов, рассматриваемых В главе 5.
4.12. Инициализация переменных
Обсуждение связывания значений с именованными константами естественным образом приводит к теме инициализации переменных, поскольку связывание величины с именованной константой является таким же процессом, только постоянным.
В большинстве случаев удобно, чтобы переменные имели значения до начала выполнения программы или подпрограммы, в которой они объявляются. Связывание переменной со значением во время связывания ее с ячейкой памяти называется инициализацией (initialization). Если переменная статически связана с ячейкой памяти, то связывание и инициализация происходят до выполнения программы. При динамическом же связывании с памятью инициализация также является динамической.
В языке FORTRAN начальные значения переменных могут устанавливаться в операторе DATA:
REAL PT
INTEGER SUM
DATA SUM /0/, PI /3.14159/
В этом операторе переменная SUM инициализируется значением 0, а переменная PI значением 3.14159. В данном случае фактическая инициализация происходит во время компиляции. Как только начнется выполнение программы, переменные SUM и PI не будут ничем отличаться от других переменных.
Во многих языках исходные значения переменных могут задаваться в операторах объявления, как в объявлении языка Ada:
SUM : INTEGER := 0;
Ни язык Pascal, ни язык Modula-2 не содержат иного способа инициализации переменных, кроме инициализации во время выполнения с помощью операторов присваивания.
Вообще, инициализация статических переменных происходит только один раз, но для таких динамических переменных, как локальные переменные в процедуре языка Ada, она происходит каждый раз при распределении памяти.
Заключение
Форма имен в языке может воздействовать как на читабельность этого языка, так и и на удобство его использования. Другим значительным конструкторским решением являете*, взаимосвязь имен и специальных слов, которыми могут быть зарезервированные или ключевые слова.
Охарактеризовать переменные можно шестеркой атрибутов: именем, адресом, значением, типом, временем жизни и областью видимости.
Альтернативными именами называются имена, связанные с одним адресом памяти С точки зрения надежности они расцениваются как вредные, но полностью исключить их из языка трудно.
Связывание это установление связи между программными объектами и их атрибутами. Знание момента времени, когда происходит связывание атрибутов с объектами необходимо для понимания семантики языков программирования. Связывание может быть статическим или динамическим. Объявления, явные или неявные, обеспечивают средства статического связывания переменной с типом. Динамическое связывание, в общем случае, придает языку большую гибкость за счет ухудшения читабельности, эффективности и надежности программ, написанных на нем.
Скалярные переменные можно разделить на четыре категории по времени их жизни статические, автоматические, явные динамические и неявные динамические переменные.
Строгая типизация означает необходимость обнаружения всех ошибок определения типа. Результатом строгой типизации является повышенная надежность.
Правила совместимости типов в языке оказывают значительное влияние на выполняемые операции над величинами. Совместимость типов определяется, как правило, в терминах совместимости имен или структур типов.
Статический обзор данных является основным свойством языка ALGOL 60 и большинства его потомков. Он предлагает эффективный метод разрешения видимости нелокальных переменных подпрограмм. Динамический обзор данных обеспечивает большую по сравнению со статическим обзором гибкость, но, опять-таки, за счет ухудшения читабельности, эффективности и надежности программ.
Среда ссылок оператора является совокупностью всех его переменных, видимых данным оператором.
Именованные константы это простые переменные, связываемые со своим значением только в момент ич связывания с ячейкой памяти. Инициализация это связывание переменной со своим значением во время ее связывания с ячейкой памяти.
Вопросы
Какие вопросы разработки языков программирования связаны с именами?
В чем состоит потенциальная опасность имен, зависящих от регистра?
Чем зарезервированные слова лучше ключевых?
Что такое альтернативное имя?
Какие категории ссылок в языке C++ всегда имеют альтернативные имена?
Что такое левое значение переменной? Что такое правое значение переменной?
Дайте определение связывания и времени связывания.
Какие четыре варианта времени связывания есть в программе после разработки
языка и его реализации?
Дайте определение статического и динамического связывания.
Назовите достоинства и недостатки неявных объявлений.
Назовите достоинства и недостатки динамического связывания типов.
Дайте определение статических переменных, автоматических переменных,
явных и неявных динамических переменных.
Дайте определение приведения типов, ошибки определения типов, проверки
типов и строгой типизации.
Дайте определение совместимости имен типов и совместимости структур
типов. Какое полезное свойство объединяет эти два понятия?
Чем различаются производные типы языка Ada и подтипы того же языка?
Дайте определение времени жизни, области видимости, статического и дина
мического обзора данных.
Каким образом обращение к нелокальной переменной в программе со статическим
обзором данных связано с ее определением?
Назовите основную проблему, возникающую при статическом обзоре данных.
Что такое "среда ссылок оператора"?
Что является статическим предком подпрограммы? Что является динамическим
предком подпрограммы?
Что такое "блок"?