Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Виртуальные функции в базовом и производном классах
По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана:
} Статический тип pb – это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display() вызывается через неявный указатель this. Статическим типом указателя this также является Query*, поэтому вызвана будет функция-член класса Query. Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:
}; Если функция-член виртуальна, то при обращении к ней вызывается функция, определенная в динамическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана. Однако для самих объектов класса статический и динамический тип – это одно и то же. Механизм виртуальных функций правильно работает только для указателей и ссылок на объекты. Таким образом, полиморфизм проявляется только тогда, когда объект производного класса адресуется косвенно, через указатель или ссылку на базовый. Использование самого объекта базового класса не сохраняет идентификацию типа производного. Рассмотрим следующий фрагмент кода:
Query qobject = nq; Инициализация qobject переменной nq абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не является объектом NameQuery. Часть nq, принадлежащая NameQuery, “усечена” перед инициализацией qobject, поскольку она не помещается в область памяти, отведенную под объект Query. Для поддержки этой парадигмы приходится использовать указатели и ссылки, но не сами объекты:
} В данном примере оба обращения через указатель pointer и ссылку reference разрешаются своим динамическим типом; в обоих случаях вызывается NameQuery:: print(). Обращение же через объект object всегда приводит к вызову Query:: print(). (Пример программы, в которой используется эффект “усечения”, приведен в разделе 18.6.2.) В следующих подразделах мы продемонстрируем определение и использование виртуальных функций в разных обстоятельствах. Каждая такая функция-член будет иллюстрировать один из аспектов объектно-ориентированного проектирования. Виртуальный ввод/вывод Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл: ostream& print( ostream & os = cout ) const; Функцию print() следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса AndQuery эта функция могла бы выглядеть так:
} Необходимо объявить print() виртуальной функцией в абстрактном базовом Query, иначе мы не сможем вызвать ее для членов классов AndQury, OrQuery и NotQuery, являющихся указателями на операнды соответствующих запросов типа Query*. Однако для самого Query разумной реализации print() не существует. Поэтому мы определим ее как пустую функцию, а потом сделаем чисто виртуальной:
}; В базовом классе, где виртуальная функция появляется в первый раз, ее объявлению должно предшествовать ключевое слово virtual. Если же ее определение находится вне этого класса, повторно употреблять virtual не следует. Так, данное определение print() приведет к ошибке компиляции:
virtual ostream& Query:: print( ostream& ) const {... } Правильный вариант не должен включать слово virtual. Класс, в котором впервые появляется виртуальная функция, должен определить ее или объявить чисто виртуальной (напомним, что пока мы определили ее как пустую). В производном классе может быть либо определена собственная реализация той же функции, которая в таком случае становится активной для всех объектов этого класса, либо унаследована реализация из базового класса. Если в производном классе определена собственная реализация, то говорят, что она замещает реализацию из базового. Прежде чем приступать к рассмотрению реализаций print() для наших четырех производных классов, обратим внимание на употребление скобок в запросе. Например, с помощью
fiery & & bird || shyly
пользователь ищет вхождения пары слов
fiery bird
или одного слова
shyly
С другой стороны, запрос
fiery & & ( bird || hair )
найдет все вхождения любой из пар
fiery bird
или
fiery hair
Если наши реализации print() не будут показывать скобки в исходном запросе, то для пользователя они окажутся почти бесполезными. Чтобы сохранить эту информацию, введем в наш абстрактный базовый класс Query два нестатических члена, а также функции доступа к ним (подобное расширение класса – естественная часть эволюции иерархии):
}; _lparen – это количество левых, а _rparen – правых скобок, которое должно быть выведено при распечатке объекта. (В разделе 17.7 мы покажем, как вычисляются такие величины и как происходит присваивание обоим членам.) Вот пример обработки запроса с большим числом скобок:
==> ( untamed || ( fiery || ( shyly ) ) ) evaluate word: untamed _lparen: 1 _rparen: 0 evaluate Or _lparen: 0 _rparen: 0
evaluate word: fiery _lparen: 1 _rparen: 0
evaluate 0r _lparen: 0 _rparen: 0
evaluate word: shyly _lparen: 1 _rparen: 0
evaluate right parens: _rparen: 3
( untamed ( 1 ) lines match ( fiery ( 1 ) lines match ( shyly ( 1 ) lines match ( fiery || (shyly ( 2 ) lines match3 ( untamed || ( fiery || ( shyly ))) ( 3 ) lines match
Requested query: ( untamed || ( fiery || ( shyly ) ) ) ( 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, " ( 6 ) Shyly, she asks, " I mean, Daddy, is there? "
Реализация print() для класса NameQuery:
} А так выглядит объявление:
}; Чтобы реализация виртуальной функции в производном классе замещала реализацию из базового, прототипы функций обязаны совпадать. Например, если бы мы опустили слово const или объявили еще один параметр, то реализация print() в NameQuery не заместила бы реализацию из базового класса. Возвращаемые значения также должны быть одинаковыми за одним исключением: значение, возвращенное реализацией в производном классе, может принадлежать к типу класса, который открыто наследует классу значения, возвращаемого реализацией в базовом классе. Если бы реализация из базового класса возвращала значение типа Query*, то реализация из производного могла бы возвращать NameQuery*. (Позже при работе с функцией clone() мы покажем, зачем это нужно.) Вот объявление и реализация print() в NotQuery:
};
} Разумеется, вызов print() через _op – виртуальный. Объявления и реализации этой функции в классах AndQuery и OrQuery практически дублируют друг друга. Поэтому приведем их только для AndQuery:
};
} Такая реализация виртуальной функции print() позволяет вывести любой подтип Query в поток класса ostream или любого другого, производного от него:
pq-> print( cout ); Однако такой возможности недостаточно. Еще нужно уметь распечатывать любой производный от Query тип, который уже есть или может появиться в будущем, с помощью оператора вывода из библиотеки iostream:
< < " получены следующие результаты: \n"; Мы не можем непосредственно предоставить виртуальный оператор вывода, поскольку они являются членами класса ostream. Вместо этого мы должны написать косвенную виртуальную функцию:
} Строки
cout < < query < < endl; вызывают наш оператор вывода в ostream, который в свою очередь вызывает q.print( os ) где q привязано к объекту query класса AndQuery, а os – к cout. Если бы вместо этого мы написали:
cout < < query2 < < endl; то была бы вызвана реализация print() из класса NameQuery. Обращение
cout < < *pquery < < endl; приводит к вызову той функции print(), которая ассоциирована с объектом, адресуемым указателем pquery в данной точке выполнения программы. |
Последнее изменение этой страницы: 2019-04-09; Просмотров: 284; Нарушение авторского права страницы