Система начисления заработной платы,



План занятия 24

 

Техническая сторона

Полиморфизма, виртуальных функций

И динамического связывания

C++ делает программирование полиморфизма несложным. Конечно, вполне возможно программирование полиморфизма и в языках, не объектно-ориентированных, таких, как С, но это связано со сложными и потенциально опасными манипуляциями с указателями. В этом разделе мы обсудим возможную внутреннюю реализацию полиморфизма, виртуальных функций и позднего связывания в C++. Это даст вам твердое понимание того, как в действительности работают эти механизмы. И что более важно, вы сможете оценить издержки полиморфизма в плане дополнительного расхода памяти и процессорного времени. Это поможет вам в принятии решений относительно того, когда целесообразно использовать полиморфизм и когда следует этого избегать. Заметьте, что компоненты библиотеки стандартных шаблонов C++ (STL) реализованы без полиморфизма и виртуальных функций; это было сделано ради исключения связанных с ними расходов времени исполнения и для достижения максимальной 

эффективности, к которой в STL предъявляются исключительные требования.

    Полиморфизм достигается благодаря трем уровням указателей (т.е. путем «тройной косвенной адресации»).

Пример 1

    Когда C++ компилирует класс с одной или несколькими виртуальными функциями, он строит для него таблицу виртуальных функций, или vtable. При посредстве vtable выполняющаяся программа выбирает нужную реализацию функции всякий раз, когда вызывается виртуальная функция данного класса. Крайний левый столбец на рис. иллюстрирует vtable для классов Employee, SalariedEmployee, Hourly Employee, CommissionEmployee и BasePlusCommissionEmployee.

    В vtable для класса Employee первый указатель на функцию устанавливается равным 0 (т.е. нулевому указателю). Это делается потому, что earnings — чисто виртуальная функция и, следовательно, реализация ее отсутствует. Второй указатель ссылается на функцию print, которая выводит полное имя и номер социальной страховки служащего. [Замечание. Для экономии места мы показываем вывод каждой из функций print в сокращенном виде.] Любой класс, имеющий в своей vtable один или несколько нулевых указателей, является абстрактным. Классы без нулевых указателей в vtable (такие, как SalariedEmployee, Hourly Employee, CommissionEmployee и BasePlusCommis-

sionEmployee) являются конкретными


 

 

Как работает вызов виртуальных функций


 

    Класс SalariedEmployee заменяет функцию earnings, чтобы она возвращала недельный оклад, поэтому соответствующий указатель ссылается на функцию earnings класса SalariedEmployee. Этот класс заменяет и функцию print, поэтому соответствующий указатель ссылается на элемент-функцию класса SalariedEmployee, которая печатает "salaried employee: " и затем полное имя, номер страховки и недельный оклад.

    Указатель функции earnings в vtable для класса HourlyEmployee ссылается на функцию earnings из HourlyEmployee, возвращающую почасовую ставку служащего, умноженную на число отработанных часов. Здесь также для экономии места мы упоминаем, что служащие с почасовой оплатой получают полуторную плату за сверхурочную работу. Указатель функции print ссылается на версию функции из класса HourlyEmployee, которая печатает "hourly employee:", полное имя, номер страховки, почасовую ставку и отработанные часы. Обе функции заменяют соответствующие функции в классе Employee.

    Указатель функции earnings в vtable для класса CommissionEmployee ссылается на функцию earnings из CommissionEmployee, возвращающую общую сумму продаж, умноженную на комиссионный процент. Указатель функции print ссылается на версию функции из класса CommissionEmployee, которая печатает тип служащего, имя, номер страховки, комиссионный процент и сумму продаж. Как и в классе HourlyEmployee, обе функции заменяют соответствующие функции в классе Employee.

    Указатель функции earnings в vtable для класса BasePlusCommissionEmployee ссылается на функцию earnings из CommissionEmployee, возвращающую базовый оклад плюс объем продаж, умноженный на комиссионный процент. Указатель функции print ссылается на версию функции из класса BasePlusCommissionEmployee, которая печатает базовый оклад в дополнение к типу, имени, номеру страховки, комиссионному проценту и сумме продаж. Обе функции заменяют соответствующие функции в классе Employee.

Заметьте, что в нашем примере каждый конкретный класс предусматривает свои собственные реализации для виртуальных функций earnings и print.

    Вы уже знаете, что каждый класс, непосредственно наследующий абстрактному базовому классу Employee, должен, чтобы быть конкретным классом, реализовывать функцию earnings, поскольку это — чисто виртуальная функция. Однако этим классам, чтобы быть конкретными, не обязательно реализовывать функцию print — это не чисто виртуальная функция и производные классы могут наследовать ее реализацию в классе Employee. Более того, классу BasePlusCommissionEmployee не обязательно реализовывать ни print, ни earnings — обе реализации могут наследоваться от класса CommissionEmployee. Если бы класс в нашей иерархии подобным образом наследовал реализации функций, их указатели в vtable просто ссылались бы на наследуемую реализацию функции. Например, если бы класс BasePlusCommissionEmployee не заменял earnings, указатель функции earnings в vtable для класса BasePlusCommissionEmployee ссылался бы на ту же самую функцию, на которую указывает vtable для класса CommissionEmployee.

    Полиморфизм реализуется посредством изящной структуры данных с тремя уровнями указателей. Пока мы обсудили только один уровень — указатели на функции в vtable. Они указывают на действительные функции, исполняемые при активации виртуальной функции.

    Рассмотрим теперь второй уровень указателей. Всякий раз, когда создается объект класса с одной или несколькими виртуальными функциями, компилятор прикрепляет к объекту указатель на vtable для данного класса. Этот указатель располагается обычно в начале объекта, но он не обязательно должен быть реализован именно таким образом. На рис. эти указатели ассоциированы с объектами, создаваемыми программой примере 5 урок 23 (по одному объекту для каждого из классов SalariedEmployec, Hourly Employee, CommissionEmployee и BasePlusCommissionEmployee). На диаграмме показаны также значения элементов данных этих объектов. Например, объект salariedEmployee содержит

указатель на vtable класса SalariedEmployee; объект содержит также значения John Smith, 111-11-1111 и $800.0.

    Третий уровень указателей представлен просто дескрипторами объектов(указателями на объекты), получающих вызовы виртуальных функций. Дескрипторы на этом уровне могут быть и ссылками. Обратите внимание, что на рис справа показан вектор employees, содержащий указатели на Employee.

    Давайте теперь проследим, как исполняется типичный вызов виртуальной функции. Рассмотрим вызов employees[i]->print() (строка 85 пример 5). Предположим, мы обрабатываем employees[ 1 ] (т.е. адрес объекта hourlyEmployee в employees). Когда компилятор обрабатывает этот вызов, он определяет, что вызов действительно производится через указатель базового класса и что print является виртуальной функцией.

              Компилятор определяет, что print идет вторым пунктом в каждой из виртуальных таблиц. Чтобы найти эту функцию, компилятор замечает, что ему нужно пропустить первый пункт. Таким образом, компилятор вычисляет смещение в машинном представлении таблицы указателей, равное четырем байтам (на современных 32-битных машинах указатель занимает четыре байта, и нужно пропустить всего один указатель), позволяющее найти код исполняемой виртуальной функции.

Компилятор генерирует код, выполняющий следующие операции. [Замечание.

Нумерация в списке соответствует обведенным кружком цифрам на рис ]

 

    1. Выбрать i-й элемент employees (в данном случае адрес объекта hourlyEmployee).

 

    2. Разыменовать этот указатель, чтобы перейти к самому объекту hourlyEmployee, который, как вы помните, начинается с указателя на vtable класса HourlyEmployee.

 

    3. Разыменовать указатель на vtable в объекте hourlyEmployee, чтобы перейти к vtable класса HourlyEmployee.

 

    4. Пропустить смещение, равное четырем байтам, чтобы выбрать указатель на функцию print.

 

    5. Разыменовать указатель на функцию print, чтобы получить «имя» действительной функции, которая должна исполняться, и применить операцию вызова () для исполнения соответствующей функции print, которая в данном случае печатает тип служащего, имя, номер страховки, почасовую ставку и отработанные часы.

 

    Структуры данных на рис.  могут показаться сложными, но компилятор скрывает от вас эту сложность, делая полиморфное программирование довольно прямолинейным. Операции разыменования указателей и обращения к памяти, происходящие при каждом вызове виртуальной функции, требуют некоторых дополнительных затрат времени. Виртуальные таблицы и прикрепляемые к объектам указатели на них требуют некоторой дополнительной памяти. Вы теперь располагаете достаточной информацией, чтобы решить, подходят ли для ваших программ виртуальные функции.

    Полиморфизм в том виде, как он реализуется в C++ посредством виртуальных функций и динамического связывания, весьма эффективен. Использование его возможностей наносит эффективности лишь номинальный ущерб.

    Виртуальные функции и динамическое связывание делают полиморфное программирование реальной альтернативой программированию логики switch. Оптимизирующие компиляторы обычно генерируют полиморфный код, работающий столь же эффективно, как и закодированная вручную логика с операторами switch. Издержки полиморфизма приемлемы для большинства приложений. Но в некоторых ситуациях — например, в приложениях реального времени с их жесткими требованиями к производительности — «накладные расходы» полиморфизма могут оказаться слишком велики.

    Динамическое связывание позволяет независимым производителям программного обеспечения (ISV) распространять программное обеспечение, не раскрывая своих секретов. Дистрибутивы могут состоять только из заголовочных и объектных файлов — исходный код раскры вать не требуется. Разработчики программ могут затемиспользовать наследование для создания новых классов из тех, что поставляют ISV. Программное обеспечение, работавшее с поставляемыми ISV классами, будет работать и с производными классами, вызывая (посредством динамического связывания) замещенные виртуальные функции, предусмотренные в этих классах.

Система начисления заработной платы,


Дата добавления: 2018-08-06; просмотров: 307; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!