Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
837 } Рассмотрим шаблон функции try_array(). На первом шаге печатается исходный объект Array , что подтверждает успешную конкретизацию оператора вывода шаблона, а заодно дает начальную картину, с которой можно будет сверяться при последующих модификациях. В переменной find_val хранится значение, которое мы впоследствии передадим find(). Если бы try_array() была обычной функцией, роль такого значения сыграла бы константа. Но поскольку никакая константа не может обслужить все типы, которыми допустимо конкретизировать шаблон, то приходится выбирать другой путь. Далее одним элементам Array случайным образом присваиваются значения других элементов, чтобы протестировать min(), max(), size() и, конечно, оператор взятия индекса. Затем объект iA2 почленно инициализируется объектом iA, что приводит к вызову копирующего конструктора. После этого тестируется оператор взятия индекса с объектом ia2 : производится присваивание элементу с индексом mid/2. (Эти две строки представляют интерес в случае, когда iA – производный подтип Array, а оператор взятия индекса объявлен виртуальной функцией. Мы вернемся к этому в главе 18 при обсуждении наследования.) Далее в iA почленно копируется модифицированный объект iA2 , что приводит к вызову копирующего оператора присваивания класса Array. Затем проверяются функции-члены grow() и find(). Напомним, что find() возвращает значение –1, если искомый элемент не найден. Попытка выбрать из “массива” Array #include "Array.h" template { cout << "try_array: начальные значения массива\n"; cout << iA << endl; elemType find_val = iA [ iA.size()-1 ]; iA[ iA.size()-1 ] = iA.min(); int mid = iA.size()/2; iA[0] = iA.max(); iA[mid] = iA[0]; cout << "try_array: после присваиваний\n"; cout << iA << endl; Array искомое значение: " << find_val; cout << "\t возвращенный индекс: " << index << endl; elemType value = iA[index]; cout << " значение элемента с этим индексом: "; cout << value << endl; С++ для начинающих 838 элемент с индексом –1 приведет к выходу за левую границу. (В главе 18 для перехвата этой ошибки мы построим производный от Array класс, который будет проверять выход за границы массива.) Убедиться, что наша реализация шаблона работает для различных типов данных, например целых чисел, чисел с плавающей точкой и строк, поможет программа main(), которая вызывает try_array() с каждым из указанных типов: } Вот что программа выводит при конкретизации шаблона Array типом double: try_array: начальные значения массива ( 5 )< 12.3, 7.9, 14.6, 9.8, 128 > try_array: после присваиваний ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 > try_array: почленная инициализация ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 > try_array: после почленного копирования ( 5 )< 14.6, 14.6, 14.6, 9.8, 7.9 > try_array: после вызова grow ( 8 )< 14.6, 14.6, 14.6, 9.8, 7.9, 0, 0, 0 > искомое значение: 128 возвращенный индекс: -1 значение элемента с этим индексом: 3.35965e-322 Выход индекса за границу массива приводит к тому, что последнее напечатанное программой значение неверно. Конкретизация шаблона Array типом string заканчивается крахом программы: #include "Array.C" #include "try_array.C" #include { static int ia[] = { 12,7,14,9,128,17,6,3,27,5 }; static double da[] = { 12.3,7.9,14.6,9.8,128.0 }; static string sa[] = { "Eeyore", "Pooh", "Tigger", "Piglet", "Owl", "Gopher", "Heffalump" }; Array Array Array С++ для начинающих 839 template Array ( 7 )< Eeyore, Pooh, Tigger, Piglet, Owl, Gopher, Heffalump > try_array: после присваиваний ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: почленная инициализация ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: после почленного копирования ( 7 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: после вызова grow ( 11 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore, < пусто>, <пусто>, < пусто>, <пусто> > искомое значение: Heffalump возвращенный индекс: -1 Memory fault (coredump) Упражнение 16.11 Измените шаблон класса Array, убрав из него функции-члены sort(), find(), max(), min() и swap(), и модифицируйте шаблон try_array() так, чтобы она вместо них пользовалась обобщенными алгоритмами (см. главу 12). С++ для начинающих 840 Часть V Объектно-ориентированное программирование Объектно-ориентированное программирование расширяет объектное программирование, вводя отношения тип-подтип с помощью механизма, именуемого наследованием. Вместо того чтобы заново реализовывать общие свойства, класс наследует данные-члены и функции-члены родительского класса. В языке C++ наследование осуществляется посредством так называемого порождения производных классов. Класс, свойства которого наследуются, называется базовым, а новый класс – производным. Все множество базовых и производных классов образует иерархию наследования. Например, в трехмерной компьютерной графике классы OrthographicCamera и PerspectiveCamera обычно являются производными от базового Camera. Множество операций и данных, общее для всех камер, определено в абстрактном классе Camera. Каждый производный от него класс реализует лишь отличия от абстрактной камеры, предоставляя альтернативный код для унаследованных функций-членов либо вводя дополнительные члены. Если базовый и производный классы имеют общий открытый интерфейс, то производный называется подтипом базового. Так, PerspectiveCamera является подтипом класса Camera. В C++ существует специальное отношение между типом и подтипом, позволяющее указателю или ссылке на базовый класс адресовать любой из производных от него подтипов без вмешательства программиста. (Такая возможность манипулировать несколькими типами с помощью указателя или ссылки на базовый класс называется полиморфизмом.) Если дана функция: void lookAt( const Camera *pCamera ); то мы реализуем lookAt(), программируя интерфейс базового класса Camera и не заботясь о том, на что указывает pCamera: на объект класса PerspectiveCamera, на объект класса OrthographicCamera или на объект, описывающий еще какой-то вид камеры, который мы пока не определили. При каждом вызове lookAt() ей передается адрес объекта, принадлежащего к одному из подтипов Camera. Компилятор автоматически преобразует его в указатель на подходящий базовый класс: lookAt( pcam ); Наша реализация lookAt() не зависит от набора подтипов класса Camera, реально существующих в приложении. Если впоследствии потребуется добавить новый подтип или исключить существующий, то изменять реализацию lookAt() не придется. // правильно: автоматически преобразуется в Camera* OrthographicCamera ocam; lookAt( &ocam ); // ... // правильно: автоматически преобразуется в Camera* PerspectiveCamera *pcam = new PerspectiveCamera; С++ для начинающих 841 Полиморфизм подтипов позволяет написать ядро приложения так, что оно не будет зависеть от конкретных типов, которыми мы манипулируем. Мы программируем открытый интерфейс базового класса придуманной нами абстракции, пользуясь только ссылками и указателями на него. При работе программы будет определен фактический тип адресуемого объекта и вызвана подходящая реализация открытого интерфейса. Нахождение (или разрешение) нужной функции во время выполнения называется динамическим связыванием (dynamic binding) (по умолчанию функции разрешаются статически во время компиляции). В C++ динамическое связывание поддерживается с помощью механизма виртуальных функций класса. Полиморфизм подтипов и динамическое связывание формируют основу объектно-ориентированного программирования, которому посвящены следующие главы. В главе 17 рассматриваются имеющиеся в C++ средства поддержки объектно- ориентированного программирования и изучается влияние наследование на такие механизмы, как конструкторы, деструкторы, почленная инициализация и присваивание; для примера разрабатывается иерархия классов Query, поддерживающая систему текстового поиска, введенную в главе 6. Темой главы 18 является изучение более сложных иерархий, возможных за счет использования множественного и виртуального наследования. С его помощью мы развернем шаблон класса из главы 16 в трехуровневую иерархию. В главе 19 обсуждается идентификация типов во время выполнения (RTTI), а также изучается вопрос о влиянии наследования на разрешение перегруженных функций. Здесь мы снова обратимся к средствам обработки исключений, чтобы разобраться в иерархии классов исключений, которую предлагает стандартная библиотека. Мы покажем также, как написать собственные такие классы. Глава 20 посвящена углубленному рассмотрению библиотеки потокового ввода/вывода iostream . Эта библиотека представляет собой иерархию классов, поддерживающую как виртуальное, так и множественное наследование. 17 17. Наследование и подтипизация классов В главе 6 для иллюстрации обсуждения абстрактных контейнерных типов мы частично реализовали систему текстового поиска и инкапсулировали ее в класс TextQuery . Однако мы не написали к ней никакой вызывающей программы, отложив реализацию поддержки формулирования запросов со стороны пользователя до рассмотрения объектно-ориентированного программирования. В этой главе язык запросов будет реализован в виде иерархии классов Query с одиночным наследованием. Кроме того, мы модифицируем и расширим класс TextQuery из главы 6 для получения полностью интегрированной системы текстового поиска. Программа для запуска нашей системы текстового поиска будет выглядеть следующим образом: С++ для начинающих 842 } build_text_map() – это слегка видоизмененная функция-член doit() из главы 6. Ее основная задача – построить отображение для хранения позиций всех значимых слов текста. (Если помните, мы не храним семантически нейтральные слова типа союзов if, and , but и т.д. Кроме того, мы заменяем заглавные буквы на строчные и устраняем суффиксы, обозначающие множественное число: например, testifies преобразуется в testify , а marches в march.) С каждым словом ассоциируется вектор позиций, в котором хранятся номера строки и колонки каждого вхождения слова в текст. query_text() принимает запросы пользователя и преобразует их во внутреннюю форму на основе иерархии классов Query с одиночным наследованием и динамическим связыванием. Внутреннее представление запроса применяется к отображению слов на вектор позиций, построенному в build_text_map(). Ответом на запрос будет множество строк текстового файла, удовлетворяющих заданному критерию: Enter a query - please separate each item by a space. Terminate query (or session) with a dot( . ). ==> fiery && ( bird || shyly ) fiery ( 1 ) lines match bird ( 1 ) lines match shyly ( 1 ) lines match ( bird || shyly ) ( 2 ) lines match fiery && ( bird || shyly ) ( 1 ) lines match Requested query: fiery && ( bird || shyly ) ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her. В нашей системе мы выбрали следующий язык запросов: • одиночное слово, например Alice или untamed. Выводятся все строки, в которых оно встречается, причем каждой строке предшествует ее номер, заключенный в скобки. (Строки печатаются в порядке возрастания номеров). Например: ==> daddy daddy ( 3 ) lines match Requested query: daddy ( 1 ) Alice Emma has long flowing red hair. Her Daddy says ( 4 ) magical but untamed. "Daddy, shush, there is no such thing," ( 6 ) Shyly, she asks, "I mean, Daddy, is there?" #include "TextQuery.h" int main() { TextQuery tq; tq.build_up_text(); tq.query_text(); С++ для начинающих 843 • запрос “НЕ”, формулируемый с помощью оператора !. Выводятся все строки, где не встречается указанное слово. Например, так формулируется отрицание запроса 1: ==> ! daddy daddy ( 3 ) lines match ! daddy ( 3 ) lines match Requested query: ! daddy ( 2 ) when the wind blows through her hair, it looks almost alive, ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 5 ) she tells him, at the same time wanting him to tell her more. запрос “ИЛИ”, формулируемый с помощью оператора ||. Выводятся все строки, в которых встречается хотя бы одно из двух указанных слов: ==> fiery || untamed fiery ( 1 ) lines match untamed ( 1 ) lines match fiery || untamed ( 2 ) lines match Requested query: fiery || untamed ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 4 ) magical but untamed. "Daddy, shush, there is no such thing," запрос “И”, формулируемый с помощью оператора &&. Выводятся все строки, где оба указанных слова встречаются, причем располагаются рядом. Сюда входит и случай, когда одно слово является последним в строке, а другое – первым в следующей: ==> untamed && Daddy untamed ( 1 ) lines match daddy ( 3 ) lines match untamed && daddy ( 1 ) lines match Requested query: untamed && daddy ( 4 ) magical but untamed. "Daddy, shush, there is no such thing," Эти элементы можно комбинировать: fiery && bird || shyly Однако обработка производится слева направо, и все элементы имеют одинаковые приоритеты. Поэтому наш составной запрос интерпретируется как fiery bird ИЛИ shyly , а не как fiery bird ИЛИ fiery shyly: ==> fiery && bird || shyly fiery ( 1 ) lines match bird ( 1 ) lines match fiery && bird ( 1 ) lines match shyly ( 1 ) lines match fiery && bird || shyly ( 2 ) lines match С++ для начинающих 844 Requested query: fiery && bird || shyly ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 6 ) Shyly, she asks, "I mean, Daddy, is there?" Чтобы можно было группировать части запроса, наша система должна поддерживать скобки. Например: fiery && (bird || shyly) выдает все вхождения fiery bird или fiery shyly1. Результат исполнения этого запроса приведен в начале данного раздела. Кроме того, система не должна многократно отображать одну и ту же строку. 17.1. Определение иерархии классов В этой главе мы построим иерархию классов для представления запроса пользователя. Сначала реализуем каждую операцию в виде отдельного класса: AndQuery // William && Shakespeare В каждом классе определим функцию-член eval(), которая выполняет соответствующую операцию. К примеру, для NameQuery она возвращает вектор позиций, содержащий координаты (номера строки и колонки) начала каждого вхождения слова (см. раздел 6.8); для OrQuery строит объединение векторов позиций обоих своих операндов и т.д. Таким образом, запрос untamed || fiery состоит из объекта класса OrQuery, который содержит два объекта NameQuery в качестве операндов. Для простых запросов этого достаточно, но при обработке составных запросов типа Alice || Emma && Weeks 1 Напомним, что для упрощения реализации необходимо, чтобы между любыми двумя словами, включая скобки и операторы запроса, был пробел. В реальной системе такое требование вряд ли разумно, но мы полагаем, что для вводного курсе, каковым является наша книга, это вполне приемлемо. NameQuery // Shakespeare NotQuery // ! Shakespeare OrQuery // Shakespeare || Marlowe Примечание [O.A.5]: Нумера ция сносок сбита. С++ для начинающих 845 возникает проблема. Данный запрос состоит из двух подзапросов: объекта OrQuery, содержащего объекты NameQuery для представления слов Alice и Emma, и объекта AndQuery . Правым операндом AndQuery является объект NameQuery для слова Weeks. NameQuery ("Weeks") Но левый операнд – это объект OrQuery, предшествующий оператору &&. На его месте мог бы быть объект NotQuery или другой объект AndQuery. Как же следует представить операнд, если он может принадлежать к типу любого из четырех классов? Эта проблема имеет две стороны: • необходимо уметь объявлять тип операнда в классах OrQuery, AndQuery и NotQuery так, чтобы с его помощью можно было представить тип любого из четырех классов запросов; • какое бы решение мы ни выбрали в предыдущем случае, мы должны иметь возможность вызывать соответствующий классу каждого операнда вариант функции-члена eval(). Решение, не согласующееся с объектной ориентированностью, состоит в том, чтобы определить тип операнда как объединение и включить дискриминант, показывающий текущий тип операнда: }; Хранить указатели на объекты можно и с помощью типа void*: AndQuery OrQuery NameQuery ("Alice") NameQuery ("Emma") // не объектно-ориентированное решение union op_type { // объединение не может содержать объекты классов с // ассоциированными конструкторами NotQuery *nq; OrQuery *oq; AndQuery *aq; string *word; }; enum opTypes { Not_query=1, O_query, And_query, Name_query }; class AndQuery { public: // ... private: /* * opTypes хранит информацию о фактических типах операндов запроса * op_type - это сами операнды */ op_type _lop, _rop; opTypes _lop_type, _rop_type; С++ для начинающих 846 }; Нам все равно нужен дискриминант, поскольку напрямую использовать объект, адресуемый указателем типа void*, нельзя, равно как невозможно определить тип такого объекта по указателю. (Мы не рекомендуем применять описанное решение в C++, хотя в языке C это весьма распространенный подход.) Основной недостаток рассмотренных решений состоит в том, что ответственность за определение типа возлагается на программиста. Например, в случае решения, основанного на void*-указателях, операцию eval() для объекта AndQuery можно реализовать так: } В результате явного управления разрешением типов увеличивается размер и сложность кода и добавление нового типа или исключение существующего при сохранении работоспособности программы затрудняется. Объектно-ориентированное программирование предлагает альтернативное решение, в котором работа по разрешению типов перекладывается с программиста на компилятор. Например, так выглядит код операции eval() для класса AndQuery в случае применения объектно-ориентированного подхода (eval() объявлена виртуальной): class AndQuery { public: // ... private: void * _lop, _rop; opTypes _lop_type, _rop_type; void AndQuery:: eval() { // не объектно-ориентированный подход // ответственность за разрешение типа ложится на программиста // определить фактический тип левого операнда switch( _lop_type ) { case And_query: AndQuery *paq = static_cast OrQuery *pqq = static_cast NotQuery *pnotq = static_cast AndQuery *pnmq = static_cast } // то же для правого операнда |