лекции (2010) (by Ульянов Алексей_ Лихогруд Николай_ Сергеев Николай) (1160852), страница 13
Текст из файла (страница 13)
Очевидно, что эта черта присуща популярным языкам.Ключевые черты(требовния) ООП хорошо известны:1. Первая — инкапсуляция — это определение классов — пользовательских типов данных,объединяющих своё содержимое в единый тип и реализующих некоторые операции или методы надним.
Классы обычно являются основой модульности, инкапсуляции и абстракции данных в языкахООП.2. Вторая ключевая черта, — наследование — есть способ определения нового типа, наследуя элементы(содержание и методы) существующего и модифицируя или расширяя их. Это способствуетвыражению специализации и генерализации.3. Третья черта, известная как полиморфизм, позволяет единообразно ссылаться на объекты различныхклассов (обычно внутри некоторой иерархии).
Это делает классы ещё более удобными и делаетпрограммы, основанные на них, легче для расширения и поддержки.Инкапсуляция, наследование и полиморфизм — фундаментальные свойства, требуемые от языка,претендующего называться объектно-ориентированным (языки, не имеющие наследования и полиморфизма,но имеющие только классы, обычно называются объектными языками). Таким образом мы остановимсяболее подробно на последних двух свойствах: наследование и полиморфизм.Головин вел понятие наследования, как отношения между классами: при наследовании всегда естьбазовый класс и класс, который наследуется от базового, то есть вы определяете новый тип, расширяя илимодифицируя существующий, другими словами, производный класс обладает всеми данными и методамибазового класса, новыми данными и методами и, возможно, модифицирует некоторые из существующихметодов. Различные ООП языки используют различные жаргоны для описания этого механизма (derivation,inheritance, sub-classing), для класса, от которого вы наследуете (базовый класс, родительский класс,суперкласс) и для нового класса (производный класс, дочерний класс, подкласс).При наследовании дополнительно к тому что было сказано выше должны выполняться следующиесвойства: совместимость унаследованного класса с базовым при передаче параметра и присваивании, то естьунаследованный класс должен приводиться к базовому.
Тоже самое должно выполняться и для ссылок иуказателей: ссылке (указателю) на базовый класс должно быть возможно присвоить ссылку(указатель) напроизводный класс. Эта возможность работы со ссылками приводит нас к понятию динамического типа:ссылка или указатель одного класса не обязательно указывает(ссылается) на объект того же класса, она можетссылаться на объект производного класса, таким образом динамический тип – это тип объекта, на которыйссылается (указывает) ссылка(указатель)Рассмотрим синтаксис наследования в различных языках программирования, и убедимся, что в нихмного общего.С++:class Derived : [модификатор] Base {// обьявление новых членов};[модификатор] ::= {private, public, protected}C++ использует слова public, protected, и private для определения типа наследования и чтобы спрятатьнаследуемые методы или данные, делая их приватными или защищёнными. Хотя публичное наследованиенаиболее часто используется, по умолчанию берётся приватное.
Чаще всего приватное наследованиеиспользуется в написании интерфейсов. Public – не меняет модификатор доступа свойств наследуемогокласса в производном, protected – делает все публичные свойства наследуемого класса защищенными, аprivate – все свойства наследуемого класса делаем закрытыми(модификатор доступа private). Для болееподробного осознания материала автор рекомендует обратиться к известной книге Страуструпа.Не будем отходить далеко и рассмотрим синтаксисС#:Java:class Derived : Base {//определение новых членов}class Derived extends Base {// определение новых членов}Java использует слово extends для выражения единственного типа наследования, соответствующегопубличному наследованию в C++.
Java не поддерживает множественное наследование. Классы Java тожепроисходят от общего базового класса.Oberon, Object Pascal, Turbo Pascal:TYPE Derived = RECORD (Base)// определение новых членовEND;Delphi:TYPE Derived = class (Base)// определение новых членовEND;В этих языках при наследовании используется не ключевое слово, а специальный синтаксис добавление в скобках имени базового класса. Эти языки поддерживают только один тип наследования,который в C++ называется публичным.Ada:type Base is tagged record// членыend;Type Derived is new Base with record// определение новых членовend;// новые члены не определяются, используется для добавления новых методов к // базовомуклассуType Derived is new Base with null record;Замечания:1.В некоторых ООП языках(Java, C#, Delphi) каждый класс происходит по крайней мере от некоторогобазового класса по умолчанию.
Этот класс, часто называемый Object, или подобно этому, обладаетнекоторыми основными способностями, доступными всем классам. Фактически, все другие классы вобязательном порядке его наследуют. Этот подход является общим ещё и потому, что так первоначальноделалось в Smalltalk. Хотя язык C++ и не поддерживает такое свойство, многие структуры приложенийбазируются на нём, вводя идею общего базового класса. Пример тому — MFC с его классом COobject. Такжелюбой класс может стать первым в иерархии классов в таких языках, как Оберон, Ада - 952.Единственный язык, поддерживающий множественное наследование из языков, проходимых вкурсе ООП – язык С++.3. Единственный язык, поддерживающий модификацию прав доступа свойств базового класса впроизводном – язык С++.Теперь давайте поговорим о реализации наследования.
Во многих языках (всех, рассматриваемых вкурсе кроме Smaltalk) свойства объектов распределены линейно, то есть если у нас есть классclass Base{int a; // 2 байтаb; // 1 байтcharint * c; // 4 байта} Object ;то адрес памяти для Object.b равен (*Object + 2), а для Object.a - (*Object).Замечание:Методы класса в этом распределении не участвуют, так как для всех экземпляров класса методыабсолютно одинаковые, в отличии от свойств класса.В производном классе, смещения адресов свойств базового класса относительно начала сохраняется, а новыесвойства как бы дописываются в конец класса. Такая реализация позволяет относительно просто реализоватьприсваивание обьектам базового класса обьектов производных, (программисты в таком случае говорят, чтопроисходит «срезка») и также просто реализовать при передаче параметров обьекта производного классавместо базового.
Но обратное присваивание: производному классу присвоить базовый не допускается, таккак в этом случае останутся не инициализированные поля(такое ограничение действует также и для ссылок иуказателей).Рассмотрим пример:class Base {void f();};class Derived: public Base {int f;};Как мы видим и класс Base, и производный от него класс имеют общее имя f(в такой ситуации говорят, что f впроизводном классе скрывает f в базовом, и если бы в производном классе вместо int f; было бы void f(int); –то здесь происходило бы тоже скрытие(не перегрузка!)) Такая ситуация вполне возможна, поэтому давайтепоговорим о:1) перегрузке(overloading) (в одной области действия)2) скрытии(hiding)3) переопределении(overriding) (динамическое связывание - рассмотрим чуть – чуть позднее)Все эти понятия вы должны были изучить на примере С++ в 4 семестре, поэтому обратиися кматериалам 4 семестра:Перегрузка функций(! Именно функций, так как не существует перегрузки свойств)Статический полиморфизм(мы не работаем с указателями, где есть базовый класс и производный) позволяетдавать одно имя нескольким функциям.
Как правило, эти функции имеют схожую семантику, но отличаютсясписком формальных параметров. Какая функция будет вызвана, определяется на этапе трансляции. Оперегрузке функций можно говорить только в пределах одной области видимости. Кстати, когда мыоб’являем несколько конструкторов одного класса – это тоже перегрузка функций.Проблема поиска подходящей перегруженной функции (best matching) – нетривиальная задача. Для началаопишем этот алгоритм для функции одного аргумента.1.
Поиск функции, точно совпадающей по типу параметра (точное отождествление). Если функциявызывается от параметра типа T, то может быть вызвано описание с прототипом от T, T&, constT, const T&, переопределения этих типов с помощью typedef, T[] эквивалентно T*,функция эквивалентна указателю на функцию.2. Если не найдено точное соответствие, то пробуем применить стандартные преобразования. На второмшаге могут сработать безопасные преобразования – целочисленное или вещественное расширение(integral/floating promotion).
Тут bool, char, short, enum (знаковые или беззнаковые)преобразуются к int(если возможно) или unsigned, float преобразуется к double.3. Если не получилось выполнить шаг 2, пробуем все остальные стандартные преобразования:оставшиеся арифметические преобразования и преобразования указателей и ссылок (указатель напроизводный класс приводится к указателю на однозначный доступный базовый класс, любойуказатель приводится к void*, 0 приводится к NULL).4. Пользовательские преобразования - рассматриваются конструкторы, которые могут быть вызваны содним параметром.
Также рассматриваются специальные функции преобразования типов. Привыполнении пользовательского преобразования можно сделать еще одно (!) преобразование, нотолько с шага 2 или 3.5. Если ничего не помогло, придётся вызывать функцию с ‘…’.Если функция имеет один параметр, то алгоритм действует следующим образом: если на некотором шагенайдена одна функция – отлично, ее и будем вызывать. Если две и более – ошибка (неоднозначный вызов). Кследующим шагам переходим тогда и только тогда, когда ни одного соответствия нет.Рассмотрим ряд примеров.Пример на 2-й шаг:void f(int);void f(double);void g() {short a=1;float ff=1.0;f(a);// f(int)// 2-й шагf(ff); // f(double) // 2-й шаг}Пример на 3-й шаг:void f(char);void f(double);void g() {f(1); // ошибка: неоднозначность(две возможности на 3-м шаге)}Пример на 4-й шаг:struct S{S(long);// long -> Soperator int(); // S -> int};void f(long);void f(char *);void g(S);void g(char *);void ex(S &a){f(a);// f((long)(a.operator int()))g(1);// g(S((long)(1))g(0); // g((char *)0) – 3-й шаг!}Особенности четвёртого шага:1.
Отсутствие транзитивности пользовательских преобразований. То есть, за один раз не может выполнитьсяболее одного преобразования типа.class X { public: operator int(); ... };class Y { public: operator X(); ... };void f(){Y a;b = a;int b; ...// нельзя}Можно явно указать b = a.operator X().operator int();.2. Пользовательские преобразования могут применяться неявно, только если они однозначны.class B {public: B (int i);operator int();B operator+ (int B);};void f(){B l(1);... l+1 ...}Возникает неоднозначность: то ли l стоит преобразовать к int с помощью определённого преобразованияи складывать числа, то ли вызвать конструктор от int и складывать об’екты типа B.3.
Конструктор должен быть описан так, чтобы он допускал неявный вызов. То есть, конструктор не можетбыть описан как explicit.class X { public: X(int); };X a(1);X b= 2;// так можноТеперь изменим об’явление:class X { public: explicit X(int); };X a(1);// так можноX b = 2;// так нельзя!X с = X(2);// так можноЗачем же нужна такая конструкция? Вспомним наш класс String.class String { String (int n); ... };String s1 = 10;// выделится память под строку из 10 символовString s2 = ‘a’;// неужели мы хотим выделить память// под строку из код('a') символов?!Никто нам не запрещает так делать. Но, если мы допишем explicit к конструктору, то такая нелогичнаязапись не прокатит и придётся вызывать через скобочки.Алгоритм поиска наилучшего соответствия для вызова функции с произвольным числом параметров N:I. По каждому из параметров ищется best matching по пятишаговому алгоритму за тем исключением, чтоесли на каком-то шаге несколько кандидатов, способных обслужить вызов, запоминаем все их.