Позволяет корпоративным клиентам до 31 августа этого года приобрести оборудование для подключения к Интернету по мобильной сети оператора всего за 1 рубль. Читайте подробности>>
Cодержит самые полные данные об угрозах, исходящих из Интернета, авторитетный анализ и комментарии. Выводы отчета помогут эффективно защитить компьютеры от вирусов, фишинга и спама в будущем.
Рассматриваются три типичных метода хищения данных: добронамеренные сотрудники, нацеленные атаки извне и мстительные сотрудники. Наряду с обзором способов противодействия даны конкретные советы по предотвращению взлома.
Мир ПК :: Студия программирования
Критика Си++. Виртуальные функции*
Язык программирования работает на многих уровнях и выполняет различные функции, а потому должен критически рассматриваться по отношению именно к этим уровням и функциям. Именно виртуальные функции — основной объект критики языка Cи++.
Ян Джойнер
Язык программирования работает на многих уровнях и выполняет различные функции, а потому должен критически рассматриваться по отношению именно к этим уровням и функциям. Именно виртуальные функции — основной объект критики языка Cи++.
Cи++ представляет собой интересный эксперимент по адаптации возможностей объектной технологии к традиционному языку программирования. Бьерн Страуструп вполне достоин аплодисментов за то, что ему в голову пришла мысль слить обе технологии воедино. В то же время в Cи++ сохранились проблемы старого поколения средств программного производства. Язык Cи++ обладает тем преимуществом перед Cи, что поддерживает некоторые аспекты объектной технологии, которые могут быть использованы для ограниченного проведения анализа требований и проектирования. Однако процессы анализа, проектирования и реализации проекта все еще в значительной степени остаются внешними по отношению к Cи++. Таким образом, в Cи++ не реализованы важные преимущества объектной технологии, которые прямо бы привели к экономичному производству программной продукции.
Виртуальные функции
Полиморфизм — основополагающая концепция объектно-ориентированного программирования. В языке Си++ ключевое слово virtual предоставляет функции возможность стать полиморфической, если она будет переписана (переопределена) в одном классе-потомке или более. Однако слово virtual отнюдь не является необходимым, так как любая функция, переопределенная (overriden) в классе-потомке, может быть полиморфической. Компилятору только требуется генерировать коммутирующий код для истинно полиморфических процедур.
Если автор родительского класса в языке Си++ не предвидит, что класс-потомок захочет переопределить функцию, то он не сможет сделать ее и полиморфической. В этом заключается наиболее серьезный порок Си++, поскольку снижается гибкость программных компонентов, а следовательно, и способность создавать адаптируемые и расширяемые библиотеки.
Си++ также позволяет функциям быть перегруженными (overloaded); в такой ситуации вызов нужной функции зависит от аргументов. Различие между перегруженными и полиморфическими (переопределенными) функциями состоит в том, что в перегруженных функциях нужная определяется при компиляции, а в случае полиморфических определяется при выполнении.
Когда родительский класс проектируется программистом, то приходится только догадываться, может ли быть функция переопределена или перегружена. Класс-потомок способен перегружать функцию в любое время, но это не соответствует более важному механизму полиморфизма, когда автор родительского класса должен точно задавать, что процедура будет виртуальной. Тогда компилятор установит коммутирующую запись для данной функции в таблице переключения классов. Значит, на программиста ложится забота обо всем том, что автоматически должно выполняться самим компилятором и что совершает компилятор в других языках. Такой пережиток унаследован Си++ из-за того, что первоначально он был реализован с помощью инструментария UNIX, а не с помощью специальной поддержки компилятора и компоновщика.
Виртуальные функции предоставляют один из возможных путей реализации полиморфизма. Разработчик языка может сделать выбор в пользу определения полиморфизма либо в родительском, либо в наследующем классе. Так что же из них имеет смысл выбрать разработчику языка? Здесь можно выделить несколько вариантов для родительских классов и классов-потомков, которые не станут взаимоисключающими и смогут достаточно легко найти себе место в любом объектно-ориентированном языке.
Существуют три варианта, связанные с переопределением, которые описываются словами «не должно», «может» и «должно».
Переопределение процедуры запрещено. Классы-потомки должны использовать процедуру в том виде, как она есть
Процедура может быть переопределена. Классы-потомки могут применять процедуру такой, как она есть, или же обеспечить свою собственную реализацию в строгом соответствии с первоначальным описанием интерфейса
Процедура является абстрактной. Реализация не предоставляется, и каждый неабстрактный класс-потомок должен обеспечивать свою собственную реализацию. Это и есть полиморфизм.
Разработчик базового класса должен принять варианты 1 и 3, а классов-потомков — 2. Для всех вариантов язык обязан предоставлять соответствующий синтаксис.
Вариант 1
Язык Cи++ не запрещает переопределение процедуры в классе-потомке. Даже приватные виртуальные процедуры могут быть переопределены. Саккинен [1] указывает на то, что класс-потомок может переопределять приватную виртуальную функцию и тогда, когда к ней нет никакого доступа.
С ним тесно связан отказ от использования виртуальной процедуры. Здесь процедура может быть полностью заменена, что вызывает две проблемы. Первая заключается в том, что процедуру можно ненароком заменить в классе-потомке, и тогда компилятор будет генерировать синтаксическую ошибку из-за дублирования описаний. Это логично, так как классы-потомки являются частью того же самого пространства имен, что и классы, от которых они наследуются. Переопределение имени внутри одной и той же области видимости будет вызывать конфликт имен, т. е. если там две сущности будут иметь одинаковое имя, то это приведет к неоднозначности и другим схожим проблемам.
Вторую проблему можно рассмотреть на следующем примере (Листинг 1).
В этом примере класс B имеет расширенные или взятые из класса A замененные процедуры. Для объектов типа B должна быть вызвана процедура B::nonvirt. Программисту-пользователю класса Си++ придает гибкость, т. е. можно вызывать либо A::nonvirt, либо B::nonvirt. Но того же можно было добиться гораздо проще и более прямым путем. A::nonvirt и B::nonvirt следует дать разные имена. В этом случае программист вызывает нужную ему процедуру явно, а не за счет каких-то темных махинаций языка, приводящих к возможности ошибок (Листинг 2).
Теперь разработчик класса B имеет прямой контроль над интерфейсом этого класса. Приложение требует, чтобы клиенты класса B могли вызывать и A::nonvirt, и B::nonvirt. Разработчик класса B обеспечивает вызов явным образом, что можно считать хорошим объектно-ориентированным проектированием, при котором предоставляются четко определенные интерфейсы. Си++ позволяет программистам-пользователям класса совершать различные трюки с интерфейсами, внешними по отношению к данному классу, и разработчик класса B не в силах предотвратить вызов A::nonvirt. Объекты класса B содержат свои собственные специализированные процедуры nonvirt, но разработчик класса B не имеет достаточного контроля над интерфейсом этого класса B, чтобы гарантировать вызов корректной версии этой процедуры.
Си++ также не защищает класс B от других изменений, вносимых в систему. Предположим, что нам требуется создать такой класс C, у которого процедура nonvirt должна быть виртуальной. Для этого nonvirt в классе A также должна быть виртуальной, что сводит на нет трюк с B::nonvirt. Требование класса C иметь виртуальную процедуру заставляет вносить изменения в базовый класс, затрагивающие всех остальных потомков данного базового класса. Это делается вместо локализации нового специфического требования в новом классе. Подобные действия идут вразрез с идеей объектно-ориентированного программирования иметь слабосвязанные классы, для того чтобы новые требования и изменения обладали локальным характером и не заставляли вносить коррективы повсюду. Потенциально это может привести к конфликту с другими существующими частями системы.
Еще один аргумент состоит в том, что любой оператор должен постоянно иметь одну и ту же семантику. Полиморфическая интерпретация оператора вида a->f() заключается в том, что для объекта, на который ссылается a, вызывается наиболее подходящая реализация f(), независимо от того, принадлежит ли этот объект типу A или же потомку класса A. Однако в языке Си++ для того чтобы четко представлять себе, что же вызывает a->f(), программист должен знать, определена ли функция f() как виртуальная или как невиртуальная. Следовательно, про оператор a->f() нельзя сказать, что он не зависит от реализации, и принцип сокрытия деталей реализации нарушается. Изменение в описании f() будет влиять на семантику вызова. А вот независимость от реализации означает, что ее изменения не затрагивают семантику исполняемых операторов.
Если правка в описании изменяет семантику, то будет генерироваться ошибка компиляции. Программист должен создавать оператор семантически целостным с измененным описанием, что отражает динамическую природу разработки программного обеспечения, когда текст программы постоянно изменяется.
Чтобы познакомиться еще с одним примером нарушения целостности семантики оператора a->f(), посмотрите раздел 10.9c в руководстве [2, с. 232].
Ни в Eiffel, ни в Java подобных проблем не возникает. Их механизмы проще и понятнее, они не приводят к сюрпризам, которых хватает у Си++. В языке Java все является виртуальным, и это приводит к тому, что метод должен быть не переопределен или определен с квалификатором final. Язык Eiffel позволяет специфицировать процедуру как замороженную, и тогда она не может быть переопределена в классах-потомках.
Вариант 2
Этот вариант переопределения открыт для разработчиков классов-потомков, однако в языке Си++ решение должно приниматься в базовом классе. Если говорить об объектно-ориентированном проектировании, то решения от чего-либо отказаться столь же важны, как и на что-то согласиться. Окончательные же решения нужно оттягивать до самого последнего момента. Такая стратегия предотвращает от появления ошибок на самых ранних стадиях программирования. При принятии скоропалительных решений вы зачастую будете попадать в тупик, опираясь на то, что впоследствии окажется некорректным. Си++ требует, чтобы родительский класс специфицировал потенциальный полиморфизм через виртуальные методы (хотя промежуточный класс в цепочке наследования тоже способен вводить виртуализацию). Из этого следует, что процедура может быть переопределена в классах-потомках. Такой подход весьма проблематичен, поскольку процедуры, которые фактически не являются полиморфическими, доступны не через прямой процедурный вызов, а через несколько менее эффективную схему виртуальных таблиц. (Это не столь уж значительные накладные расходы, однако объектно-ориентированные программы имеют тенденцию использовать большое количество маленьких процедур, что в сумме ведет к повышению накладных расходов.) Политика языка Си++ состоит в том, что процедуры, которые могут быть переопределены, должны быть объявлены как виртуальные.
Платформа DIRECTUM стала центральным звеном в создаваемой информационной системе, направленной на повышение эффективности и открытости местных органов власти.
Комментарии:
Для того, чтобы оставить комментарий авторизуйтесь или зарегистрируйтесь.