лекции (2003) (Глазкова) (1160821), страница 22
Текст из файла (страница 22)
Если записать это выражение в префиксной форме, то оно будет очень сложным.
PLUS(A,
MINUS(
MINUS(
MULT(B,C),D),Y));
Понятно, что для удобной работы возможность переопределения стандартных операций должна быть соединена с возможностью непосредственной видимости этих операций.
В частности, поэтому в языке Ада есть специальная конструкция:
use список_имен_моделей;
Эта конструкция делает непосредственно видимыми все имена, определенные в спецификации модулей, входящих в список_имен_модулей.
Как только в приведенном примере сделать use Math; имена иVector и «+» становится непосредственно видимыми, и мы можем записывать:
A,B,C: Vector
A:=B+C;
Проблемы могут возникнуть, если много имен, и они конфликтуют между собой.
Правила разрешения имен достаточно сложные.
Кроме этого ситуация усугубляется тем, что имена функций могут перекрываться.
Если имена функций конфликтуют, и их профили совпадают, то имена закрывают друг друга, а если профили не совпадают- то нет.
Т.о., правила видимости в языке Ада с использованием конструкции use достаточно сложные. Однако конструкция use необходима, если мы хотим использовать перекрытие стандартных знаков операций.
Во многих языках (таких как Ада), где сложные правила перекрытия имен и сложные правила взаимодействия областей видимости, есть специальная директива renames, которая позволяет переименовывать имена. Она имеет следующий синтаксис:
новое_имя renames старое_имя,
где старое_имя уже определено.
Конструкция типа i renames j; считается плохим стилем программирования и ничего кроме путаницы не приносит.
Если переименовать i renames P1.P11.a; то конструкция renames позволяет в некоторых случаях избегать конфликта имен.
Пример.
Пусть имя Х определено и в модуле М1, и в модуле М2.
Далее use M1,M2;
Удобно переименовать XM1 renames M1.X;
XM2 renames M2.X;
Итак, что же нам дает понятие модуля в контексте определения новых типов данных.
Пример (Ада)
package M is
TYPE T is ...;
...P(...X:T,...) или function ... return T;
...
end M;
В языке Ада пакет М называется определяющим для типа данных Т.
Т.е. определяющий пакет для ТД – это пакет, в котором появилось определение этого ТД.
При этом все операции, у которых объект типа Т выступает в качестве формального параметра или в качестве возвращаемого значения, - это операции над типом данных Т.
Пусть в одном модуле (пакете) определен и ТД Т и Т1. Операция f(a: T; b: T1) – это, по определению, операция одновременно и над ТД Т, и над ТД Т1. Т.е. возможны операции, которые не привязаны к единому ТД.
Вообще, пакет – это более общее понятие, но, в частности, он служит и для определения типов данных.
Заметим, что не любой интерфейс должен иметь реализацию.
Например, если модуль служит для определения допустимого множества символов.
Пункт 3. Классы.
Особенности класса:
-
Любой класс – это некоторый ТД (но не любой ТД является классом – например, в С++ любой примитивный ТД не является классом). В языке C# существуют примитивные ТД. Существует CTS (Common Type System), которая является частью CLR (Common Language Runtime). Каждому примитивному ТД в C# соответствует ТД, который можно отождествить с классом в CTS(например, в модуле System есть класс Int32).
-
Класс определяется явным заданием
-
объектов данных, которые принадлежат переменным этого класса (члены данные);
-
операций над классом (члены функции).
Т.е. класс отличается от стандартных типов данных тем, что мы явным образом перечисляем структуру (т.е. объекты данных, принадлежащие переменным этого класса) и явно перечисляем операции над этим классом.
-
Но главное отличие класса от других ТД в том, что члены класса образуют среду ссылок. Т.е. каждый класс имеет свою среду ссылок (с этой точки зрения класс похож на модуль).
-
операции над классом имеют неявный аргумент – ссылку на ОД этого класса, который образует соответствующую среду ссылок. (в эту среду ссылок входят члены-данные и члены-функции).
Во всех языках с классами на этот неявный аргумент можно ссылаться.
В С++ этот аргумент является указателем на объект соответствующего класса и называется this: X* this.
В C# он тоже называется this, но в C# (в отличие от С++) все классы имеют ссылочную структуру: X this.
В Delphi он называется Self (и имеет тип: ссылка на класс): Self:X.
В Java: X this.
Итак, основное назначение класса – быть средством для определения новых типов данных.
class имя_класса
{
объявления членов-данных
объявления членов-функций
}
Абстрагируемся сейчас от понятия наследования и динамического связывания и поговорим о классе как таковом.
Каждый класс образует среду ссылок.
Как следствие, в телах функций-членов класса другие члены класса видимы нам непосредственно (поскольку они входят в среду ссылок данного класса).
Пример1.
class X
{
int x;
void f(){ ... x... // x – видима непосредственно}
};
Хотя можно писать – в С++: this->x; в C#: this.x.
Пример 2.
class X
{
T1 a;
T2 b;
X(T1 a;T2 b) {...this.a=a; this.b=b;..}
…….
};
В языке Delphi формальные параметры операций входят в среду ссылок класса. Поэтому имена формальных параметров операций внутри класса не могут совпадать с именами членов этого класса.
В большинстве других ЯП (во всех остальных) считается, что блок открывает локальную СС для данной функции, и формальные параметры функции принадлежат локальной СС этой функции.
И, следовательно, они могут переименовывать имена из более глобальной СС.
На языке Delphi пишут так:
X(aName1:X; aName2:Y)
Name1=aName1;
Name2=aName2;
В языке С++ синтаксис объявления класса следующий:
class X <информация о наследовании>
{
объявления
};
В С++ вместо class можно использовать struct: единственное отличие между ними – права доступа по умолчанию (в struct – public, в class – privаte)
Синтаксис класса в языке Delphi:
type T=class <информация о наследовании>
объявления
end;
Синтаксис объявления класса в Delphi очень похож на синтаксис объявления записи. Класс служит обобщением понятия записи- отличия: наследование, наличие функций-членов.
Объявления членов-данных имеют вид:
в C++: тип имя;
в Delphi: имя: тип;
Т.е. объявления членов-данных аналогичны объявлению переменных.
В языке Delphi может быть только заголовок операции; в языке С++ может быть прототип операции, либо полное определение операции.
Вернемся к принципу разделения определения, реализации и использования.
В Delphi определение и реализация разнесены.
Когда мы имеем определение класса, мы можем в нем указывать только заголовок операции.
Тело операции должно быть отдельно указано в реализации.
Пример.
Type=class
...
procedure P;
...
end;
implementation
procedure T.P
begin
операторы
end
Имя функции обязательно надо квалифицировать (имя_класса.имя_функции),
потому что каждый класс – это своя область видимости (среда ссылок), и поэтому в разных классах могут быть одни и те же имена.
Аналогичная ситуация в языке С++.
В качестве объявления члена-функции может быть задан прототип
class T
{
void P( );
...
};
...
// где-то после определения Т
void T::P( )
{...}
Квалификация в С++
<имя_класса>::<имя_операции>.
Т.о., в языках С++ и Delphi реализован принцип разделения определения и реализации.
Однако в С++ функцию можно определить прямо в определении соответствующего класса.
Это аналог того, если бы мы определили Р извне, но с помощью спецификатора inline:
inline void T::P( ){...}
В более современных ЯП Java, C# принцип РОРИ нарушен: как только мы написали объявление функции, мы должны написать ее тело. Определение и реализация в этих языках слиты воедино.
И в C#, и в Java есть специальные утилиты в составе сред программирования, которые «выстригают» интерфейс из определения класса.
В этих ЯП принцип РОРИ реализован на уровне сред программирования.
Использование
Все классовые языки характеризуются тем, что операции всегда «привязаны» к одному типу данных (к одному объекту), ссылка на этот объект передается через this или self.
Пусть есть объект какого-то класса: X a;
Тогда обращаться к членам этого класса мы можем через операцию:
a.f() или a.i.
При этом у нас должны быть права доступа к соответствующему члену.
Статические и нестатические члены класса.
Нестатические члены (члены-данные и члены-функции) образуют среду ссылок, которая принадлежит каждому члену класса.
Статические члены
В языке SmallTalk есть понятие экземпляр класса – это объект данных какого-то класса; и есть понятие переменные экземпляра и класса.
Переменные, с точки зрения терминологии языка С++, - это члены-данные.
Переменные экземпляра класса являются частью каждого объекта класса.
Переменные экземпляра – это аналог нестатических членов.
Пусть класс Х содержит нестатический член int i;
Есть два экземпляра класса Х: Х а, b; то каждый из этих экземпляров содержит свой экземпляр переменной i.
Переменные класса – это переменные, которые принадлежат классу в целом. Переменные класса –это аналог статических членов – данных в С++.
Пусть static int j;
Тогда j принадлежит одновременно и объекту а, и объекту b.
Т.о., память для статических членов отводится отдельно. Известно, что статические члены класса существуют, даже если мы не создали ни одного объекта этого класса.
Статические члены класса – это аналог локальных переменных, описанных внутри пакета, т.е. они являются как бы внешними (глобальными) по отношению к экземплярам класса, но, с точки зрения видимости, они локализованы внутри экземпляров этого класса. Не случайно в языке С++ использование статических членов не очень распространено, а в C# и Java использование статических членов необходимо, потому что в этих языках отсутствует как понятие глобальной функции, так и глобальной переменной (все переменные и функции являются членами какого-либо класса). При этом функции, которые не зависят от класса, естественно объявить статическими.
Пример: (C#, Java)
public class X{
рublic static void main (String args[]){...}
}
С выполнения функции main() и начинается выполнение всего приложения. Она обязана быть статической, потому что, когда программа начинает работать, ни один экземпляр класса ещё не создан.
Итак, статический член-данное - это аналог глобальной переменной, т.е. переменной, которая размещается отдельно от экземпляров класса, но, в то же время, она локализована внутри класса.
Статическая функция – это аналог глобальной функции, имя которой локализовано внутри класса. Т.е. от обычных функций статическая функция отличается тем, что у неё отсутствует параметр this(self).
Заметим, что из рассматриваемых языков понятие статического члена отсутствует в языке Delphi, потому что аналогом статических членов в нём являются переменные, описанные в unit, но за пределами класса.
В С++, с этой точки зрения, тоже можно было бы отказаться от статических функций. В чём преимущество статических функций и статических членов-данных? В языке Delphi переменные и функции, описанные внутри unit, но за пределами класса, имеют доступ к членам этого класса. Просто же глобальные функции таких прав не имеют. Т.е. статические функции члены - класса обладают теми же правами доступа к классу, что и остальные. Единственное ограничение: в статических функциях нельзя обращаться к нестатическим членам класса и к нестатическим функциям, потому что у нас отсутствует доступ к среде ссылок (в нестатических функциях мы получаем доступ через this(self)).