Виртуальная функция

Из Википедии, бесплатной энциклопедии

В объектно-ориентированном программировании, например, которое часто используется в C++ и Object Pascal , виртуальная функция или виртуальный метод — это наследуемая и переопределяемая функция или метод , которые отправляются динамически . Виртуальные функции являются важной частью полиморфизма (время выполнения) в объектно-ориентированном программировании (ООП). Они позволяют выполнять целевые функции, которые не были точно определены во время компиляции.

Большинство языков программирования, таких как JavaScript , PHP и Python , по умолчанию рассматривают все методы как виртуальные. [1] [2] и не предоставляйте модификатор для изменения этого поведения. Однако некоторые языки предоставляют модификаторы, предотвращающие переопределение методов производными классами (например, ключевые слова Final и Private в Java). [3] и PHP [4] ).

Цель [ править ]

Концепция виртуальной функции решает следующую задачу:

В объектно-ориентированном программировании , когда производный класс наследуется от базового класса, на объект производного класса можно ссылаться через указатель или ссылку типа базового класса вместо типа производного класса. Если существуют методы базового класса, переопределенные производным классом, метод, фактически вызываемый такой ссылкой или указателем, может быть связан (связан) либо «рано» (компилятором), в соответствии с объявленным типом указателя или ссылки, либо «поздно» (т. е. системой времени выполнения языка) в соответствии с фактическим типом объекта.

Виртуальные функции разрешаются «поздно». Если рассматриваемая функция является «виртуальной» в базовом классе, реализация функции в наиболее производном классе вызывается в соответствии с фактическим типом объекта, на который делается ссылка, независимо от объявленного типа указателя или ссылки. Если он не «виртуальный», метод разрешается «рано» и выбирается в соответствии с объявленным типом указателя или ссылки.

Виртуальные функции позволяют программе вызывать методы, которые даже не обязательно существуют на момент компиляции кода. [ нужна цитата ]

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

Пример [ править ]

С++ [ править ]

Диаграмма классов животных

Например, базовый класс Animal может иметь виртуальную функцию Eat. Подкласс Llama реализовал бы Eat иначе, чем подкласс Wolf, но можно вызвать Eat в любом экземпляре класса, называемом Animal, и получите Eat поведение конкретного подкласса.

class   Animal   { 
  public  : 
   // Намеренно не виртуальный: 
   void   Move  ()   { 
     std  ::  cout   <<   "Это животное каким-то образом движется"   <<   std  ::  endl  ; 
    } 
   виртуальная   пустота   Eat  ()   =   0  ; 
  }; 

  // При желании класс Animal может содержать определение Eat. 
  class   Llama   :   public   Animal   { 
  public  : 
   // Невиртуальная функция Move наследуется, но не переопределяется. 
    void   Eat  ()   override   { 
     std  ::  cout   <<   "Ламы едят траву!"    <<   std  ::  endl  ; 
    } 
 }; 

Это позволяет программисту обрабатывать список объектов класса Animal, приказывая каждому по очереди поесть (вызвав Eat), без необходимости знать, какие животные могут быть в списке, как каждое животное ест или каким может быть полный набор возможных типов животных.

В C механизм виртуальных функций может быть реализован следующим образом:

#include   <stdio.h> 

 /* объект указывает на свой класс... */ 
 struct   Animal   { 
     const   struct   AnimalVTable   *  vtable  ; 
  }; 

  /* который содержит виртуальную функцию Animal.Eat */ 
 struct   AnimalVTable   { 
     void   (  *  Eat  )(  struct   Animal   *  self  );    // "виртуальная" функция 
 }; 

  /* 
 Поскольку Animal.Move не является виртуальной функцией, 
 ее нет в приведенной выше структуре. 
  */ 
 void   Move  (  const   struct   Animal   *  self  )   { 
     printf  (  "<Животное в %p> каким-то образом переместилось  \n  "  ,   (   void  *   )(  self  )); 
  } 

 /* 
 в отличие от Move, который выполняет Animal.Move напрямую, 
 Eat не может знать, какую функцию (если таковая имеется) вызывать во время компиляции. 
  Animal.Eat можно разрешить только во время выполнения, когда вызывается Eat. 
  */ 
 void   Eat  (  struct   Animal   *  self  )   { 
     const   struct   AnimalVTable   *  vtable   =   *  (   const   void  **   )(  self  ); 
      if   (  vtable  ->  Eat   !=   NULL  )   {  
         (  *  vtable  ->  Eat  )(  self  );    // выполняем Animal.Eat 
     }   else   { 
         fprintf  (  stderr  ,   "Виртуальный метод 'Eat' не реализован  \n  "  ); 
      } 
 } 

 /* 
 реализация Llama.Eat — это целевая функция, 
 вызываемая 'void Eat(struct Animal *self).' 
  */ 
 static   void   _Llama_eat  (  struct   Animal   *  self  )   { 
     printf  (  "<Ллама в %p> Лама ест траву!  \n  "  ,   (  void  *   )(  self  ));     
  } 

 /* инициализировать класс */ 
 const   struct   AnimalVTable   Animal   =   {   NULL   };    // базовый класс не реализует Animal.Eat 
 const   struct   AnimalVTable   Llama   =   {   _Llama_eat   };     // но производный класс выполняет 

 int   main  (  void  )   { 
    /* инициализирует объекты как экземпляр своего класса */ 
    struct   Animal   Animal   =   {   &  Animal   }; 
     struct   Animal   llama   =   {   &  Llama   }; 
     Переместить  (  &  животное  );    // Animal.Move 
    Move  (  &  лама  );     // Llama.Move 
    Eat  (  &  Animal  );     // невозможно разрешить Animal.Eat, поэтому выведите «Not Implemented» в stderr 
    Eat  (  &  llama  );      // разрешает Llama.Eat и выполняет 
 } 

Абстрактные классы и чистые виртуальные функции [ править ]

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

Например, абстрактный базовый класс MathSymbol может предоставить чисто виртуальную функцию doOperation()и производные классы Plus и Minus осуществлять doOperation()предоставить конкретные реализации. Реализация doOperation() не имело бы смысла в MathSymbol класс, как MathSymbol — абстрактное понятие, поведение которого определяется исключительно для каждого данного вида (подкласса) MathSymbol. Аналогично, данный подкласс MathSymbol не будет полным без реализации doOperation().

Хотя чисто виртуальные методы обычно не имеют реализации в классе, который их объявляет, чисто виртуальные методы в некоторых языках (например, C++ и Python) могут содержать реализацию в объявляющем классе, обеспечивая запасное поведение или поведение по умолчанию, которому производный класс может делегировать. , в случае необходимости. [5] [6]

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

во время строительства и разрушения Поведение

Языки различаются по своему поведению во время работы конструктора или деструктора объекта. По этой причине вызов виртуальных функций в конструкторах обычно не рекомендуется.

В C++ вызывается «базовая» функция. В частности, вызывается наиболее производная функция, которая не является более производной, чем текущий класс конструктора или деструктора. [7] : §15.7.3  [8] [9] Если эта функция является чисто виртуальной функцией, возникает неопределенное поведение . [7] : §13.4.6  [8] Это верно, даже если класс содержит реализацию этой чисто виртуальной функции, поскольку вызов чисто виртуальной функции должен быть явно определен. [10] Соответствующая реализация C++ не требуется (и, как правило, не способна) обнаруживать косвенные вызовы чисто виртуальных функций во время компиляции или компоновки . Некоторые системы выполнения выдают ошибку вызова чисто виртуальной функции при вызове чисто виртуальной функции во время выполнения .

В Java и C# вызывается производная реализация, но некоторые поля еще не инициализируются производным конструктором (хотя они инициализируются нулевыми значениями по умолчанию). [11] Некоторые шаблоны проектирования , такие как паттерн «Абстрактная фабрика» , активно продвигают такое использование в языках, поддерживающих эту возможность.

Виртуальные деструкторы [ править ]

Объектно-ориентированные языки обычно управляют выделением и освобождением памяти автоматически при создании и уничтожении объектов. Однако некоторые объектно-ориентированные языки позволяют при желании реализовать собственный метод деструктора. Если рассматриваемый язык использует автоматическое управление памятью, вызываемый пользовательский деструктор (обычно называемый в этом контексте финализатором) обязательно будет подходящим для рассматриваемого объекта. Например, если создается объект типа Wolf, наследующий Animal, и оба имеют собственные деструкторы, вызванный будет тот, который объявлен в Wolf.

В контексте ручного управления памятью ситуация может быть более сложной, особенно в отношении статической диспетчеризации . Если создается объект типа Wolf, но на него указывает указатель Animal, и именно этот тип указателя Animal удаляется, вызванный деструктор может фактически быть тем, который определен для Animal, а не деструктором для Wolf, если только деструктор не является виртуальным. . Это особенно актуально для C++, где такое поведение является частым источником ошибок программирования, если деструкторы не являются виртуальными.

См. также [ править ]

Ссылки [ править ]

  1. ^ «Полиморфизм (Учебные пособия по Java™ > Изучение языка Java > Интерфейсы и наследование)» . docs.oracle.com . Проверено 11 июля 2020 г.
  2. ^ «9. Классы — документация Python 3.9.2» . docs.python.org . Проверено 23 февраля 2021 г.
  3. ^ «Написание окончательных классов и методов (Учебные пособия по Java™ > Изучение языка Java > Интерфейсы и наследование)» . docs.oracle.com . Проверено 11 июля 2020 г.
  4. ^ «PHP: последнее ключевое слово — руководство» . www.php.net . Проверено 11 июля 2020 г.
  5. ^ Чистые виртуальные деструкторы - cppreference.com
  6. ^ "abc — Абстрактные базовые классы: @abc.abstractmethod"
  7. ^ Перейти обратно: а б «N4659: Рабочий проект стандарта языка программирования C++» (PDF) .
  8. ^ Перейти обратно: а б Чен, Раймонд (28 апреля 2004 г.). «Что такое __purecall?» .
  9. ^ Мейерс, Скотт (6 июня 2005 г.). «Никогда не вызывайте виртуальные функции во время создания или разрушения» .
  10. ^ Чен, Раймонд (11 октября 2013 г.). «Угловой случай C++: в базовом классе можно реализовать чисто виртуальные функции» .
  11. ^ Ганеш, С.Г. (1 августа 2011 г.). «Радость программирования: вызов виртуальных функций из конструкторов» .