ттттт. Объектноориентированное программирование
Скачать 1.73 Mb.
|
Ромбовидное наследование – ситуация в объектно-ориентированных языках программирования с поддержкой множественного наследования, ко- гда два класса B и C наследуют от A , а класс D наследует от обоих классов B и C . При этой схеме наследования может возникнуть неоднозначность: если метод класса D вызывает метод, определенный в классе A (и этот метод не был переопределен), а классы B и C по-своему переопределили этот метод, то от какого класса его наследовать: B или C ? 57 Проблема ромба получила свое название благодаря очертаниям диа- граммы наследования классов в этой ситуации. В данной статье, класс A обо- значается в виде вершины, классы B и C по отдельности указываются ниже, а D соединяется с обоими в самом низу, образуя ромб. C++ по умолчанию не создает ромбовидного наследования: компилятор обрабатывает каждый путь наследования отдельно, в результате чего объект D будет на самом деле содержать два разных подобъекта A , и при использо- вании членов A потребуется указать путь наследования ( B::A или C::A ). Чтобы сгенерировать ромбовидную структуру наследования, необходимо воспользоваться виртуальным наследованием класса A на нескольких путях наследования: если оба наследования от A к B и от A к C помечаются специ- фикатором virtual (например, class B : virtual public A ), C++ спе- циальным образом проследит за созданием только одного подобъекта A , и использование членов A будет работать корректно. Если виртуальное и не- виртуальное наследования смешиваются, то получается один виртуальный подобъект A и по одному невиртуальному подобъекту A для каждого пути невиртуального наследования к A . При виртуальном вызове метода вирту- ального базового класса используется так называемое правило доминирова- ния: компилятор запрещает виртуальный вызов метода, который был пере- гружен на нескольких путях наследования. Виртуальное наследование в C++ – один из вариантов наследования, ко- торый нужен для решения некоторых проблем, порождаемых наличием воз- можности множественного наследования (особенно «ромбовидного насле- дования»), путем разрешения неоднозначности того, методы которого из классов-предков необходимо использовать. Оно применяется в тех случаях, когда множественное наследование вместо предполагаемой полной компо- зиции свойств классов-предков приводит к ограничению доступных насле- дуемых свойств вследствие неоднозначности. Базовый класс, наследуемый множественно, определяется виртуальным с помощью ключевого слова virtual Рассмотрим следующий пример: class Animal { public: virtual void eat(); // Метод определяется для данного класса. }; class Mammal : public Animal { 58 public: virtual Color getHairColor(); }; class WingedAnimal : public Animal { public: virtual void flap(); }; class Bat : public Mammal, public WingedAnimal {}; // Метод eat() не переопределен в Bat. В вышеприведенном коде вызов bat.eat() является неоднозначным. Он может относиться как к Bat::WingedAnimal::Animal::eat() так и к Bat::Mammal::Animal::eat() . У каждого промежуточного наследника ( WingedAnimal , Mammal ) метод eat() может быть переопределен (это не ме- няет сущность проблемы с точки зрения языка). Проблема в том, что семан- тика традиционного множественного наследования не соответствует моде- лируемой им реальности. В некотором смысле, сущность Animal един- ственна по сути; Bat – это Mammal и WingedAnimal , но свойство животности летучей мыши ( Bat ), оно же свойство животности млекопитающего ( Mammal ) и оно же свойство животности WingedAnimal – по сути это одно и то же свойство. При наследовании классы предка и наследника просто помещаются в па- мяти друг за другом. Таким образом объект класса Bat это на самом деле последовательность объектов классов ( Animal , Mammal , Animal , WingedAnimal , Bat ), размещенных последовательно в памяти, при этом Animal повторяется дважды, что и приводит к неоднозначности. Можно переопределить исходные классы следующим образом: class Animal { public: virtual void eat(); }; // Two classes virtually inheriting Animal: class Mammal : public virtual Animal // <--- ключевое слово virtual { public: virtual Color getHairColor(); }; class WingedAnimal : public virtual Animal // <--- ключевое слово 59 virtual { public: virtual void flap(); }; class Bat : public Mammal, public WingedAnimal {}; Теперь, часть Animal объекта класса Bat::WingedAnimal та же самая, что и часть Animal , которая используется в Bat::Mammal , и можно сказать, что Bat имеет в своем представлении только одну часть Animal и вызов Bat::eat() становится однозначным. Виртуальное наследование реализуется через добавление указателей на виртуальную таблицу vtable в классы Mammal и WingedAnimal , это дела- ется в частности потому, что смещение памяти между началом Mammal и его Animal части неизвестно на этапе компиляции, а выясняется только во время выполнения. Таким образом, Bat представляется, как ( vtable* , Mam- mal , vtable* , WingedAnimal , Bat , Animal ). Два указателя vtable на объект увеличивают размер объекта на величину двух указателей, но это обеспечи- вает единственность Animal и отсутствие многозначности. Нужны два ука- зателя vtables : по одному на каждого предка в иерархии, который вирту- ально наследуется от Animal : один для Mammal и один для WingedAnimal Все объекты класса Bat будут иметь одни и те же указатели vtable* , но каждый отдельный объект Bat будет содержать собственную реализацию объекта Animal . Если какой-нибудь другой класс будет наследоваться от Mammal , например Squirrel (белка), то vtable* в объекте Mammal объекта Squirrel будет отличаться от vtable* в объекте Mammal объекта Bat , хотя в особом случае они по-прежнему могут быть одинаковы по сути: когда часть Squirrel объекта имеет тот же самый размер, что и часть Bat , по- скольку, тогда расстояние от реализации Mammal до части Animal будет оди- наковым. Но сами виртуальные таблицы vtables будут все же разными, в отличие от располагаемой в них информации о смещениях. 60 8. ШАБЛОНЫ ФУНКЦИЙ 8.1 Использование шаблонов функций При создании функций иногда возникают ситуации, когда две функции выполняют одинаковую обработку, но работают с разными типами данных (например, одна использует параметры типа int , а другая типа float ). Вы уже знаете, что с помощью механизма перегрузки функций можно исполь- зовать одно и то же имя для функций, выполняющих разные действия и име- ющих разные типы параметров. Однако, если функции возвращают значе- ния разных типов, вам следует использовать для них уникальные имена. Предположим, например, что у вас есть функция с именем max , которая воз- вращает максимальное из двух целых значений. Если позже вам потребу- ется подобная функция, которая возвращает максимальное из двух значений с плавающей точкой, вам следует определить другую функцию, например fmax Шаблон определяет набор операторов, с помощью которых ваши про- граммы позже могут создать несколько функций. Программы часто используют шаблоны функций для быстрого опреде- ления нескольких функций, которые с помощью одинаковых операторов ра- ботают с параметрами разных типов или имеют разные типы возвращаемых значений. Шаблоны функций имеют специфичные имена, которые соответствуют имени функции, используемому вами в программе. После того как ваша программа определила шаблон функции, она в даль- нейшем может создать конкретную функцию, используя этот шаблон для задания прототипа, который включает имя данного шаблона, возвращаемое функцией значение и типы параметров. В процессе компиляции компилятор C++ будет создавать в вашей про- грамме функции с использованием типов, указанных в прототипах функций, которые ссылаются на имя шаблона. Шаблоны функций имеют уникальный синтаксис, который может быть на первый взгляд непонятен. Однако после создания одного или двух шаб- лонов вы обнаружите, что реально их очень легко использовать. 61 8.2 Создание простого шаблона функции С помощью такого шаблона ваши программы в дальнейшем могут опре- делить конкретные функции с требуемыми типами. Например, ниже опре- делен шаблон для функции с именем max , которая возвращает большее из двух значений: template { if (а > b) return(а); else return(b); } Буква T данном случае представляет собой общий тип шаблона. После определения шаблона внутри вашей программы вы объявляете прототипы функций для каждого требуемого вам типа. В случае шаблона max следую- щие прототипы создают функции типа float и int float max(float, float); int max(int, int); Когда компилятор C++ встретит эти прототипы, то при построении функции он заменит тип шаблона T указанным вами типом. В случае с ти- пом float функция max после замены примет следующий вид: template { if (a > b) return(а) ; else return(b); } float max(float a, float b) { if (a > b) return(a) ; else return(b); } Следующая программа МАХ_ТЕМР.СРР использует шаблон max для со- здания функции типа int и float template< typename T > // прототип: шаблон sort объявлен, но не определён void sort( T array[], int size); template< typename T > void sort( T array[], int size ) // объявление и определение { T t; for (int i = 0; i < size - 1; i++) for (int j = size - 1; j > i; j--) if (array[j] < array[j-1]) 62 { t = array[j]; array[j] = array[j-1]; array[j-1] = t; } } int i[5] = { 5, 4, 3, 2, 1 }; sort< int >( i, 5 ); char c[] = "бвгда"; sort< char >( c, strlen( c ) ); sort< int >( c, 5 ); // ошибка: у sort< int > параметр int[] а не char[] int i[5] = { 5, 4, 3, 2, 1 }; sort( i, i + 5 ); // вызывается sort< int > char c[] = "бвгда"; sort( c, c + strlen( c ) ); // вызывается sort< char > #include { if (a > b) return(a); else return(b); } float max(float, float); int max(int, int); void main(void) { cout << "Максимум 100 и 200 равен " << max(100, 200) << endl; cout << "Максимум 5.4321 и 1.2345 равен " << max(5.4321, 1.2345) << endl; } В процессе компиляции компилятор C++ автоматически создает опера- торы для построения одной функции, работающей с типом int , и второй функции, работающей с типом float . Поскольку компилятор C++ управ- ляет операторами, соответствующими функциям, которые вы создаете с по- мощью шаблонов, он позволяет вам использовать одинаковые имена для функций, которые возвращают значения разных типов. Вы не смогли бы это сделать, используя только перегрузку функций. 8.3 Использование шаблонов функций По мере того как ваши программы становятся более сложными, воз- можны ситуации, когда вам потребуются подобные функции, выполняющие одни и те же операции, но с разными типами данных. Шаблон функции поз- воляет вашим программам определять общую, или типонезависимую, функ- цию. Когда программе требуется использовать функцию для определенного типа, например int или float , она указывает прототип функции, который 63 использует имя шаблона функции и типы возвращаемого значения и пара- метров. В процессе компиляции C++ создаст соответствующую функцию. Создавая шаблоны, вы уменьшаете количество функций, которые должны кодировать самостоятельно, а ваши программы могут использовать одно и то же имя для функций, выполняющих определенную операцию, незави- симо от возвращаемого функцией значения и типов параметров. 8.4 Шаблоны, использующие несколько типов Предыдущее определение шаблона для функции max использовало единственный общий тип Т . Очень часто в шаблоне функции требуется ука- зать несколько типов. Например, следующие операторы создают шаблон для функции show_array , которая выводит элементы массива. Шаблон ис- пользует тип Т для определения типа массива и тип Т1 для указания типа параметра count : template { T1 index; for (index =0; index < count; index++) cout << array[index] << ' '; cout << endl; } Как и ранее, программа должна указать прототипы функций для требуе- мых типов: void show_array(int *, int); void show_array(float *, unsigned); Следующая программа SHOW_TEM.CPP использует шаблон для созда- ния функций, которые выводят массивы типа int и типа float #include { T1 index; for (index = 0; index < count; index++) cout << array[index] << ‘ ‘; cout << endl; } void show_array(int *, int); void show_array(float *, unsigned); void main(void) { int pages[] = { 100, 200, 300, 400, 500 }; float pricesH = { 10.05, 20.10, 30.15 }; show_array(pages, 5); show_array(prices, 3); } 64 8.5 Шаблоны и несколько типов По мере того как шаблоны функций становятся более сложными, они могут обеспечить поддержку нескольких типов. Например, ваша программа может создать шаблон для функции с именем array_sort , которая сорти- рует элементы массива. В данном случае функция может использовать два параметра: первый, соответствующий массиву, и второй, соответствующий количеству элементов массива. Если программа предполагает, что массив никогда не будет содержать более 32767 значений она может использовать тип int для параметра размера массива. Однако более универсальный шаб- лон мог бы предоставить программе возможность указать свой собственный тип этого параметра, как показано ниже: template { // операторы } С помощью шаблона array_sort программа может создать функции ко- торые сортируют маленькие массивы типа float (менее 128 элементов) и очень большие массивы типа int , используя следующие прототипы: void array_sort(float, char); void array_sort(int, long); template< class T1, // параметр-тип typename T2, // параметр-тип int I, // параметр обычного типа T1 DefaultValue, // параметр обычного типа template< class > class T3, // параметр-шаблон class Character = char // параметр по умолчанию > 65 9. ШАБЛОНЫ КЛАССОВ Шаблон класса позволяет задать класс, параметризованный типом дан- ных. Передача классу различных типов данных в качестве параметра со- здает семейство родственных классов. Наиболее широкое применение шаб- лоны находят при создании контейнерных классов. Контейнерным называ- ется класс, который предназначен для хранения каким-либо образом орга- низованных данных и работы с ними. Преимущество использования шабло- нов состоит в том, что как только алгоритм работы с данными определен и отлажен, он может применяться к любым типам данных без переписывания кода. 9.1 Создание шаблонов классов Рассмотрим процесс создания шаблона класса на примере двусвязного списка. Поскольку списки часто применяются для организации данных, удобно описать список в виде класса, а так как может потребоваться хранить данные различных типов, этот класс должен быть параметризованным. Сначала рассмотрим непараметризованную версию класса. Список со- стоит из узлов, связанных между собой с помощью указателей. Каждый узел хранит целое число, являющееся ключом списка. Опишем вспомогательный класс для представления одного узла списка: class Node { public: int d; // Данные Node *next, *prev;//Указатели на предыдущий и последующий узлы Node(int dat = 0) { d = dat; next = 0; prev = 0; } // Конструктор }; Поскольку этот класс будет описан внутри класса, представляющего список, поля для простоты доступа из внешнего класса сделаны доступными ( public ). Это позволяет обойтись без функций доступа и изменения полей. Назовем класс списка List : class List { class Node{ ... }; Node *pbeg, *pend; // Указатели на начало и конец списка public: List() { pbeg = 0; pend = 0; } // Конструктор List(); // Деструктор void add(int d); // Добавление узла в конец списка Node * find(int i); // Поиск узла по ключу Node * insert(int key, int d); //Вставка узла d после узла с ключом key 66 bool remove(int key); // Удаление узла void print(); // Печать списка в прямом направлении void print_back(); // Печать списка в обратном направлении }; Рассмотрим реализацию методов класса. Метод add выделяет память под новый объект типа Node и присоединяет его к списку, обновляя указа- тели на его начало и конец: void List::add(int d) { Node *pv = new Node(d); // Выделение памяти под новый узел if (pbeg == 0) pbeg = pend = pv; // Первый узел списка else { pv->prev = pend; // Связывание нового узла с предыдущим pend->next = pv; pend = pv; } // Обновление указателя на конец списка } Метод find выполняет поиск узла с заданным ключом и возвращает ука- затель на него в случае успешного поиска и 0 в случае отсутствия такого узла в списке: Node * List::find( int d ) { Node *pv = pbeg; while (pv) { if(pv->d == d) break; pv=pv->next; } return pv; } Метод insert вставляет в список узел после узла с ключом key и воз- вращает указатель на вставленный узел. Если такого узла в списке нет, вставка не выполняется и возвращается значение 0: Node * List::insert(int key, int d) { if(Node *pkey = find(key)) { // Поиск узла с ключом key /* Выделение памяти под новый узел и его инициализация */ Node *pv = new Node(d); /* Установление связи нового узла с последующим */ pv->next = pkey->next; // Установление связи нового узла с предыдущим pv->prev = pkey; // Установление связи предыдущего узла с новым pkey->next = pv; if( pkey != pend) (pv->next)->prev = pv; /* Установление связи последующего узла с 67 новым */ // Обновление указателя на конец списка, если узел вставляется в конец else pend = pv; return pv; } return 0; } Метод remove удаляет узел с заданным ключом из списка и возвращает значение true в случае успешного удаления и false , если узел с таким клю- чом в списке не найден: bool List::remove(int key) { if(Node *pkey = find(key)) { if (pkey == pbeg) { // Удаление из начала списка pbeg = pbeg->next; pbeg->prev = 0; } else if (pkey == pend) { // Удаление из конца списка pend = pend->prev; pend->next = 0; } else { // Удаление из середины списка (pkey->prev)->next = pkey->next; (pkey->next)->prev = pkey->prev; } delete pkey; return true; } return false; } Методы печати списка в прямом и обратном направлении поэлементно просматривают список, переходя по соответствующим ссылкам: void List::print() { Node *pv = pbeg; cout << endl << "list: "; while (pv) { cout << pv->d << ' '; pv=pv->next;} cout << endl; } void List::print_back() { Node *pv = pend; cout << endl << " list back: "; while (pv){ cout << pv->d << ' '; pv=pv->prev; } cout << endl; } Деструктор списка освобождает память из-под всех его элементов: List::List(){ if (pbeg != 0) { 68 Node *pv = pbeg; while (pv) {pv = pv->next; delete pbeg; pbeg = pv;} } } Ниже приведен пример программы, использующей класс List . Про- грамма формирует список из 5 чисел, выводит его на экран, добавляет число в список, удаляет число из списка и снова выводит его на экран: int main() { List L; for (int i = 2; i<6; i++) L.add(i); L.print(); L.print_back(); L.insert(2,200); if (!L.remove(5))cout << "not found"; L.print(); L.print_back();} Класс List предназначен для хранения целых чисел. Чтобы хранить в нем данные любого типа, требуется описать этот класс как шаблон и пере- дать тип в качестве параметра. |