И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 24
Текст из файла (страница 24)
В случае когда модификаторprivate, то все унаследованные члены становятся закрытыми в производном классе. Закрытое наследование полезно, например дляреализации интерфейсов.Рассмотрим следующий пример:struct А {int х,у;Instruct В: А {int z ;};A al;В Ы;Ы .х = 1;Ы . у = 2;bl.z = 3;al = bl;Объект производного типа в содержит внутри себя члены базовоготипа А:При наследовании наследуются не только члены-данные, нои методы. Однако существуют исключения.
В языке C++ не наследуются:• конструкторы;• деструктор;• операция присваивания.118Из подразд. 7.2 известно, что конструкторы и деструкторы содержат системную часть (генерируемую компилятором), в которойпроисходит, в частности, вызов конструктора (деструктора) базовогокласса.При создании объекта производного типа В сначала вызываетсяконструктор базового типа А (в системной части). При этом есликонструктору базового типа требуются параметры, то его необходимовызывать явно в списке инициализации конструктора. Затем вызывается пользовательская часть — тело конструктора производноготипа В.Деструкторы вызываются в обратном порядке. При разрушенииобъекта производного типа сначала вызывается деструктор этоготипа, а затем деструктор базового типа.Совместимость производного типа с базовым.
Наследование существенно изменяет взгляд на систему типов языка. В чисто императивных языках типы данных не пересекаются: каждый объект данныхпринадлежит одному и только одному типу. Это один из главных недостатков императивных языков. В языках с наследованием объектыпроизводного типа принадлежат одновременно и своему, и базовомутипу. С точки зрения системы типов базовый тип включает в себя всеобъекты производных типов. Все операции, применимые к базовомутипу, применимы и к производному. Поэтому везде в программе, гдемогут стоять объекты данных базового типа, на их месте могут появляться объекты производного типа.Таким образом, существует неявное преобразование из объектовпроизводного типа в объекты базового типа, которое рассматриваетсячисто формально, ведь никаких действий (при одиночном наследовании) при таком преобразовании не происходит.Пусть X — базовый класс, a Y — производный.Рассмотрим следующий фрагмент:X х; Y у;х = у; // неявное преобразование из Y в X: х = (X) у;// объект у рассматривается как объект типа X// работает операция присваивания из базового// класса Xvoid f (X х) ;f (У) ; // ~ f { (X) у) ;// неявное преобразование из производного типа// в базовый работает конструктор копирования// из базового класса Xvoid g (Х& х) ;Я (У) ;Заметим, что тип объекта базового класса при присваивании емуобъекта производного класса не меняется.
Это же касается и формального параметра функции f при передаче параметра по значению.119Копируется часть объекта производного типа в объект базового типа,а часть объекта, специфичная для производного класса, просто игнорируется.Совершенно меняется ситуация при рассмотрении ссылок илиуказателей на объекты базового и производного классов. Тип ссылки (указателя) на производный класс неявно преобразуется к типуссылки (указателя) на базовый класс (т. е. к указателям и ссылкам применимо то же правило преобразования, что и к объектамсобственно классов). Однако при присваивании указателю адресаобъекта производного класса тип объекта, на который ссылаетсяуказатель, меняется:X х; Y у;X *рХ = &х; //////рХ = &у;//////////типы левой и правой частейприсваивания совпадают,рХ указывает на объект типа Xтипы левой и правой частейприсваивания не совпадаюттип левой части (Y*) неявнопреобразуется в X * .
Сейчас рХуказывает на объект типа YТо же самое относится и к ссылкам: если инициализировать ссылку на базовый класс объектом производного класса, то тип объекта,обозначаемого ссылкой, — это производный класс:X х; Y у;Х& хх - х ; // хх обозначает объект класса XХ& ххх = у; // ххх обозначает объект класса YТаким образом, указатели и ссылки на объекты классов обладаютстатическим и динамическим типом. Статический тип указателя(ссылки) — это тип из объявления указателя (ссылки). Динамическийтип — это тип объекта, адрес которого в настоящий момент содержится в указателе (ссылке).
Динамический тип указателя может меняться при присваивании. Динамический тип ссылки определяетсяпри инициализации ссылки и не меняется в течение жизни ссылки.Динамический тип либо совпадает со статическим, либо являетсяпроизводным (прямо или косвенно через цепочку наследования).Объекты классов не обладают динамическим типом, их тип либозадается в объявлении, либо указывается при выполнении операцииnew (для динамических объектов).Понятие динамического типа используется при динамическомполиморфизме.Классы как области видимости.
Добавленные и переопределенные при наследовании члены производного класса образуютотдельную область видимости, вложенную в область видимости базового класса.120Вложенность областей видимости определяет алгоритм поиска,определяющего вхождения имени члена класса. Рассмотрим такойалгоритм для обращения к члену класса извне методов класса, т. е.через объект или указатель:Y а;а .f () ;Разберемся, как происходит поиск определяющего вхожденияимени при обработке обращения а . f. Компилятор по имени объекта находит его тип из объявления Y а ;. Далее анализируетсяобъявление класса Y. Если в классе Y нет объявления имени f, топоиск продолжается во вложенной области видимости, т. е.
в базовом классе и далее по цепочке наследования до тех пор, покаобъявление имени f не будет найдено либо цепочка наследованияне закончится (в этом случае выдается ошибка). Учтите, что припоиске игнорируются права доступа (закрытые объявления такжепросматриваются) и контекст использования имени (т. е. факт, что fвызывается как функция, игнорируется). Права доступа и корректность использования имени анализируются только после того, какнайдено объявление имени. При этом поиск заканчивается и невозобновляется даже в случае ошибки (например, имя f недоступноили f не является функцией).Рассмотрим следующий пример:class X{public: int а;};class Y : public X{public:void f ();void g ();void g (int i);private:int a;};class Z : public Y{public:int f,i;void g(int i) ;};Z z; Y y;z.f(); // ошибка: имя Z::f не является функциейy.i = 0; // ошибка: имени i нет ни в Y, ни в X121y.f(); // правильно: Y::f();z.a = -1; // ошибка: имя Y::a недоступноz.g(); // ошибка: вызов Z::g(int) без параметраy.g(); // правильно: Y::g();y.g(O); // правильно: Y::g(int);z.g(O); // правильно: Z::g(int);Заметим, что если подобная иерархия встречается в реальной программе, то это свидетельствует о плохом проектировании.
Проблемаздесь в том, что имеет место скрытие имен. Скрытие имени происходит, если во вложенной области видимости имя переопределяется.В нашем примере имеет место сразу несколько скрытий:• объявление Y : : а скрывает X ::а;• объявление z : : f скрывает Y: : f ;• объявление z : : g скрывает Y : : g (оба объявления).Скрытие имен провоцирует появление ошибок в программах,поэтому его следует избегать.Заметим, что в примере есть два определяющих вхождения одногои того же имени (Y: : д) в одной области видимости (класс Y).
Такаяситуация называется перегрузкой и, как уже отмечалось, она допустима только для имен функций. Если хотя бы одно из перегруженныхимен не является именем функции, то возникает ситуация конфликтаимен. Заметим, что при описании имен во вложенных областях конфликта имен нет, а есть скрытие одного объявления другим.
Такжезаметим, что объявления перегруженных имен методов должны находиться в одной и той же области видимости, иначе имеет место скрытие, даже если прототипы методов разные (поэтому метод Z ::g (int)скрывает оба перегруженных метода Y : : g () и Y : : g (int)).Если обращение к имени происходит внутри тела метода класса,то алгоритм поиска определяющего вхождения работает аналогично,но следует иметь в виду, что здесь возникает еще одна вложеннаяобласть видимости, а именно блок-тело метода. В теле метода могутобъявляться локальные переменные, к которым относятся такжеи формальные параметры метода. Поэтому поиск начинается с наиболее вложенной области — блока, и только потом он достигает области объявления членов класса.
Кроме того, если имя не найденони в теле метода, ни в классе (в том числе базовом), тогда поискпродолжается в области видимости, в которой находится объявлениекласса (например, в глобальной области видимости).Например:void f ();void g () ;class X{ ...protected:void f () ;122};class Y : X{int i;void h(int k){int i = 0; // скрывает член if () ; // X ::f (); - допустимоg(); // глобальная g ( ) ;Y::i = k; // допустимо}};Одной из причин того, что скрытие все-таки допустимо (хотя егоследует избегать), является то, что до скрытых имен можно добраться,используя уточнение именем класса. Например, внутри метода Y ::hскрытый член i становится доступен через уточнение: Y : : i.В приведенном примере (с иерархией классов Х-Y-z) ошибок,связанных со скрытием, можно избежать, используя уточнения:z .Y ::f (); // допустимоz.X::a = -1; // допустимоz.Y::g(); // допустимоАналогично к именам из глобальной области видимости можнообратиться, используя синтаксис:: :f () ;Однако к именам локальных переменных в блоке правило уточнения не относится.
Так, в следующем примере формальный параметрп недоступен внутри вложенного блока:void f(int n){ --if (a < b) {double n; // плохой стиль! ! !// нет никакой возможности использовать// здесь формальный параметр п}}Особенности реализации наследования в языкахJava и C#Многое из того, что говорилось о наследовании в языке C++, относится и к языкам Java и С#.
Сосредоточимся на их отличиях.123Синтаксис объявления производного класса имеет видclass имя extends имя-базового-класса { // Javaобъявления-новых-и-переопределенных-членов}class имя : имя-базового-класса { // C#объявления-новых-и-переопределенных-членов}Модификация доступа при наследовании не разрешена, все унаследованные члены сохраняют свои модификаторы доступа.Фундаментальное отличие Java и C# от C++ состоит в том, что всеклассы имеют общего предка — класс Object. Если класс объявляется без базового, то по умолчанию он наследуется непосредственноот Object.Класс Object играет особую роль в этих языках.
Он не обладает членами-данными, зато имеет большой набор методов (в томчисле и статических). Ранее уже упоминались методы toString (),clone () в языке Java, которым соответствуют методы ToString ()и MemberwiseClone () в языке С#. Далее будем учитывать, чтопо неформальному соглашению имена методов в Java начинаютсясо строчной буквы, а в C# — с прописной. Программисты не обязаны следовать этому соглашению, но авторы стандартной библиотеки неукоснительно его соблюдают. Поэтому далее иногда не будемуточнять, к объекту какого языка относится метод, поскольку этоможно понять по имени.Наличие единого предка имеет ряд важных следствий.Во-первых, все объекты располагают единым набором операций,отвечающим, в частности, за копирование, сравнение, финализацию,перевод в текстовое представление.