Что такое раннее и позднее связывание. Различие переопределения и сокрытия методов. Конструкторы. Зарезервированные слова super и this. Блоки инициализации

10.11.2019 Программы

Чтобы выяснить, в чем состоит различие между ранним (статическим) и поздним (динамическим) связыванием в Java, нужно сначала понять, что такое это самое связывание . Связывание означает наличие связи между ссылкой и кодом. Например, переменная, на которую вы ссылаетесь, привязана к коду, в котором она определена. Аналогично, вызываемый метод привязан к месту в коде, где он определен.

Существует два типа связывания методов в языке Java: ранее связывание (его ещё называют статическим) и позднее (соответственно, динамическое) связывание . Вызов метода в Java означает, что этот метод привязывается к конкретному коду или в момент компиляции, или во время выполнения, при запуске программы и создании объектов. Можно понять из названия, статическое связывание носит более статический характер, так как происходит во время компиляции, то есть код «знает», какой метод вызывать после компиляции исходного кода на Java в файлы классов. А поскольку это относится к ранней стадии жизненного цикла программы, то называется также ранним связыванием (early binding). С другой стороны, динамическое связывание происходит во время выполнения, после запуска программы виртуальной машиной Java. В этом случае то, какой метод вызвать, определяется конкретным объектом, так что в момент компиляции информация недоступна, ведь объекты создаются во время выполнения. А поскольку это происходит на поздней стадии жизненного цикла программы, то называется в языке Java поздним связыванием (late binding). Давайте рассмотрим еще несколько отличий, чтобы лучше разобраться с этим, а, кроме того, мочь ответить на этот очень популярный вопрос, который задают на собеседованиях по Java.

Раннее и позднее связывание в Java

Существует множество различий статического и динамического связывания в языке Java, но важнейшее – то, как их использует JVM. Задумывались ли вы когда-нибудь, каким образом JVM решает, какой метод вызвать, если в области видимости содержится более одного метода с одним именем? Если вы когда-либо использовали перегрузку или переопределение методов, то знаете, что в Java может быть несколько методов с одним именем. В случае с Java виртуальная машина JVM использует как статическое, так и динамическое связывание для выбора нужного метода.

Пример статического и динамического связывания в Java

В этой программе вы увидите, что привязка виртуальных методов не происходит во время компиляции при помощи статического связывания, поскольку в этом случае вызывался бы метод из суперкласса, как происходит со статическими методами, которые связываются рано. Если будет вызван метод из подкласса, то для связывания функции использовался конкретный объект во время выполнения, а, следовательно, для связывания виртуальных функций используется динамическое связывание. public class Main { public static void main (String args) { // Пример статического и динамического связывания в Java Insurance current = new CarInsurance () ; // Динамическое связывание на основе объекта int premium = current. premium () ; // Статическое связывание на основе класса String category = current. category () ; System. out. println ("premium: " + premium) ; System. out. println ("category: " + category) ; } } class Insurance { public static final int LOW = 100 ; public int premium () { return LOW; } public static String category () { return "Insurance" ; } } class CarInsurance extends Insurance { public static final int HIGH = 200 ; public int premium () { return HIGH; } public static String category () { return "Car Insurance" ; } } Результаты выполнения: premium : 200 category : Insurance Как вы видите, вызов метода premium() привел к выполнению метода из подкласса, в то время как вызов метода category() привел к выполнению метода суперкласса. Это происходит из-за того, что premium() – виртуальный метод, который разрешается при помощи позднего связывания, в то время как category() – статический метод, который разрешается при помощи статического связывания во время компиляции по имени класса.
Интересно читать о Java? Вступайте в группу !

Различия между ранним и поздним связыванием в языке Java

Теперь, когда вы разобрались и понимаете, как в языке Java связываются вызовы методов и как функционирует статическое и динамическое связывание, давайте еще раз перечислим ключевые различия между ранним и поздним связыванием в языке Java:
  1. Статическое связывание происходит во время компиляции, а динамическое – во время выполнения.

  2. Поскольку статическое связывание происходит на ранней стадии жизненного цикла программы, его называют ранним связыванием. Аналогично, динамическое связывание называют также поздним связыванием, поскольку оно происходит позже, во время работы программы.

  3. Статическое связывание используется в языке Java для разрешения перегруженных методов, в то время как динамическое связывание используется в языке Java для разрешения переопределенных методов.

  4. Аналогично, приватные, статические и терминальные методы разрешаются при помощи статического связывания, поскольку их нельзя переопределять, а все виртуальные методы разрешаются при помощи динамического связывания.

  5. В случае статического связывания используются не конкретные объекты, а информация о типе, то есть для обнаружения нужного метода используется тип ссылочной переменной. С другой стороны, при динамическом связывании для нахождения нужного метода в Java используется конкретный объект.
Вот неплохое упражнение, основанное на понятиях статического и динамического связывания в языке Java. Сможете ли вы ответить на вопрос: "Что будет выведено при выполнении следующей программы?" Что выведет эта программа? Collection , Set или HashSet ? Вот и все, что мы хотели рассказать вам о различиях между ранним (статическим) и поздним (динамическим) связыванием в языке Java. Это один из лучших вопросов для телефонного собеседования по языку Java, поскольку оно предоставляет немало возможностей проверки глубины знаний кандидата. Всегда помните, что приватные , статические и final-методы связываются при помощи статического связывания , а виртуальные – динамического . Аналогично, лучший пример статического связывания – перегрузка методов, а переопределение – динамического.

2

Say не было никакой функции Здравствуйте, и мы просто называемся ob.display в основном, то она вызывает функцию отображения класса B, а не класс А.

Вызов функции display() устанавливается один раз компилятором как версия, определенная в базовом классе. Это называется статическим разрешением вызова функции или статической привязкой - вызов функции фиксируется перед выполнением программы. Это также иногда называют ранним связыванием, потому что функция display() задается во время компиляции программы.

Теперь, как он может вызвать функцию отображения производного класса без использования виртуального ключевого слова (позднего связывания) перед функцией отображения в базовом классе?

Теперь в этой программе, передавая объект как вызов по значению, вызов по указателю и вызов по ссылке на функцию Hello отлично работают. Теперь, если мы используем Полиморфизм и хотим отобразить функцию-член производного класса, если он вызывается, мы должны добавить ключевое слово virtual перед функцией отображения в базе. Если передать значение объекта при вызове с помощью указателя и вызова по ссылке это вызов функции в производном классе, но если передать объект по значению он не почему это так?>

Class A { public: void display(); // virtual void display() { cout << "Hey from A" <display() } int main() { B obj; Hello(obj); // obj //&ob return 0; }

  • 2 ответа
  • Сортировка:

    Активность

4

сейчас как он может вызвать функцию отображения производного класса без использования ключевого слова virtual (поздняя привязка) перед функцией отображения в базовом классе?

Не виртуальная функция просто разрешается компилятором в соответствии с статическим типом объекта (или ссылки или указателя), который он вызывает. Таким образом, данный объект производного типа, а также ссылку на его подъобекта:

B b; A & a = b;

вы получите разные результаты от вызова невиртуальный функции:

B.display(); // called as B a.display(); // called as A

Если вы знаете, вещественный тип, то вы можете указать, что вы хотите назвать эту версию:

Static_cast(a).display(); // called as B

но что бы ужасно неправильно, если объект a относится к не имеет типа B .

Теперь, если мы используем Полиморфизм и хотим отобразить функцию-член производного класса, если он вызывается, мы должны добавить виртуальное ключевое слово перед функцией отображения в базе.

Исправить. Если вы сделаете функцию виртуальной, то она будет разрешена во время выполнения в соответствии с динамическим типом объекта, даже если вы используете другой тип ссылки или указатель для доступа к ней. Таким образом, оба вышеуказанных примера назвали бы его B .

Если мы передаем значение объекта по вызову указателем и вызываем по ссылке, он вызывает функцию в производном классе, но если мы передаем объект по значению, это не значит, почему это так?

Если передать его по значению, то вы нарезки его: копирование только A части объекта, чтобы сделать новый объект типа A . Итак, независимо от того, является ли эта функция виртуальной, вызов ее на этом объекте будет выбирать версию A , так как это A и ничего, кроме A .

0

wikipedia говорит, что нарезка объектов происходит потому, что нет места для хранения дополнительных членов производного класса в суперклассе, поэтому он срезан. Почему нарезка объектов не происходит, если мы передаем ее ссылкой или указателем? Почему суперкласс получает дополнительное пространство для его хранения? -

ВИРТУАЛЬНЫЕ ФУНКЦИИ______________________________________________________________ 1

Раннее и позднее связывание. Динамический полиморфизм ___________________________________ 1

Виртуальные функции___________________________________________________________________ 1 Виртуальные деструкторы _______________________________________________________________ 4 Абстрактные классы и чисто виртуальные функции___________________________________________ 5

ВИРТУАЛЬНЫЕ ФУНКЦИИ

Раннее и позднее связывание. Динамический полиморфизм

В C++ полиморфизм поддерживается двумя способами.

Во-первых, при компиляции он поддерживается посредством перегрузки функций и операторов. Такой вид полиморфизма называетсястатическим полиморфизмом , поскольку он реализуется еще до выпол-

нения программы, путем раннего связывания идентификаторов функций с физическими адресамина стадии компиляции и компоновки.

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

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

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

Для каждого полиморфного типа данных компилятор создает таблицу виртуальных функций и встраивает в каждый объект такого класса скрытый указатель на эту таблицу. Она содержит адреса виртуальных функций соответствующего объекта. Имя указателя на таблицу виртуальных функций и название таблицы зависят от реализации в конкретном компиляторе. Например, в Visual C++ 6.0 этот указатель имеет имя vfptr , а таблица называетсяvftable (от английского Virtual Function Table). Компилятор автоматически встраивает в начало конструктора полиморфного класса фрагмент кода, который инициализирует указатель на таблицу виртуальных функций. Если вызывается виртуальная функция, код, сгенерированный компилятором, находит указатель на таблицу виртуальных функций, затем просматривает эту таблицу и извлекает из нее адрес соответствующей функции. После этого производится переход на указанный адрес и вызов функции.

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

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

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

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

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

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

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

class Coord

Базовый класс координат

// базовый класс координат

protected:

// защищённые члены класса

double x , y ;

// координаты

public:

// открытые члены класса

Coord () { x = 0 ; y = 0 ; }

// конструктор базового класса

void Input () ;

// объявляет невиртуальную функцию

virtual void Print () ;

// объявляет виртуальную функцию

void Coord:: Input ()

// позволяет вводить координаты с клавиатуры

cout<<"\tx=";

// вводит значение x с клавиатуры

cout<<"\ty=";

// вводит значение y с клавиатуры

void Coord:: Print ()

// выводит значения координат на экран

cout<<"\tx="<

Производный класс точки

class Dot: publicCoord

// наследник класса координат

char name ;

// имя точки

public:

// открытые члены класса

Dot (ch ar N) : Coord () { name = N ; }

// вызывает конструктор базового класса

void Input () ;

void Print () ;

void Dot:: Input ()

// позволяет вводить координаты точки с клавиатуры

char S ="Введите координаты точки ";

CharToOem (S , S) ;

cout<

Coord:: Input () ;

void Dot:: Print()

// выводит значения координат точки на экран

char S ="Координаты точки ";

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя точки

Coord:: Print () ;

// вызывает функцию базового класса

class Vec: publicCoord

Производный класс вектора

// наследник класса координат

char name [ 3 ] ;

// имя вектора

public:

// открытые члены класса

Vec (char * pName) : Coord () { strncpy (name , pName , 3) ; name [ 2 ] = "\0" ; }

void Input () ;

// переопределяет невиртуальную функцию

void Print () ;

// переопределяет виртуальную функцию

void Vec:: Input()

// позволяет вводить проекции вектора с клавиатуры

Лекция 9 Виртуальные функции 3

char S ="Введите проекции вектора "; // объявляет и инициализирует строку приглашения

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран приглашение и имя вектора

Coord:: Input () ;

// вызывает функцию базового класса

void Vec:: Print ()

// выводит значения проекций вектора на экран

char S = "Проекции вектора ";

// объявляет и инициализирует строку заголовка

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя вектора

Coord:: Print () ;

// вызывает функцию базового класса

В приведённом примере объявлен базовый класс Coord и два производных классаDot иVec . ФункцияPrint () в производных классах является виртуальной, так как она объявлена виртуальной в базовом классеCoord . ФункцияPrint () в производных классахDot иVec переопределяет функцию базового класса. Если производный класс не предоставляет переопределенной реализации функцииPrint () , используется реализация по умолчанию из базового класса.

Функция Input () объявлена невиртуальной в базовом классеCoord и переопределена в производных классахDot иVec .

void main ()

Coord* pC = new Coord () ;

// объявляет указатель на координаты и выделяет память

Dot* pD = new Dot ("D") ;

// объявляет указатель на точку и выделяет память

Vec* pV = new Vec ("V") ;

// объявляет указатель на вектор и выделяет память

pC->Input () ;

pC->Print () ;

// вызывает виртуальную функцию Coord:: Print ()

// указатель на координаты получает адрес объекта типа точки

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Dot:: Print ()

// указатель на координаты получает адрес объекта типа вектора

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Vec:: Print ()

В приведённом примере указатель на координаты pC поочерёдно принимает значения адреса объектов координат, точки и вектора. Несмотря на то, что тип указателяpC не изменяется, он вызывает различныевиртуальные функции в зависимости от своего значения.

При использовании указателя на базовый класс, который реально указывает на объект производного класса, вызывается невиртуальная функция базового класса.

Необходимо отметить, что операция присвоения pC = pD , в которая использует операнды различных типов (Coord* иDot* ) без преобразования, возможна только для указателя на базовый класс в левой части. Обратная операция присвоенияpD = pC недопустима и вызывает ошибку синтаксиса.

При выполнении программа выводит на экран:

Координаты точки D:

Проекции вектора V:

При вызове функции с помощью указателей и ссылок, применяются следующее правила:

вызов виртуальной функции разрешается в соответствии с типом объекта, адрес которого хранит указатель или ссылка;

вызов невиртуальной функции разрешается в соответствии с типом указателя или ссылки.

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

нельзя объявить глобальную или статическую функцию виртуальной. Ключевое слово virtual может

Позднее связывание с компонентами COM

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

Программы с ранним связыванием узнают адреса на ранней стадии процесса компиляции/выполнения - во время компиляции. Когда программа (ранним связыванием компилируется, компилятор использует библиотеку типов компонента для включения адресов методов и свойств компонента в клиентскую исполнимую программу, чтобы к адресам можно было очень быстро и безошибочно обращаться. Технологии взаимодействия COM которые были рассмотрены до сих пор, используют раннее связывание.

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

Но сначала проверим, как позднее связывание выполняется с помощью отражения в C# (Отражение, является способом, который используется кодом во время выполнения для определения информации об интерфейсах серверных классов; см. главу 5.)

При позднем связывании с объектом COM в программе C# не нужно создавать RCW для компонента COM. Вместо этого вызывается метод класса GetTypeFromProgID класса Type для создания экземпляра объекта, представляющего тип объекта COM. Класс Type является членом пространства имен System.Runtime.InteropServices и в коде ниже мы конфигурируем объект Type для того же компонента COM доступа к данным, который использовался в предыдущих примерах:


Type objCustomerTableType;

Когда имеется объект Type , инкапсулирующий информацию о типе объекта COM, он используется для создания экземпляра самого объекта COM. Это реализуется передачей объекта Type в метод класса CreateInstance класса Activator.CreateInstance создает экземпляр объекта COM и возвращает на него ссылку позднего связывания, которую можно сохранить в ссылке типа object.

object objCustomerTable;
objCustomerTable = Activator.CreateInstance(objCustomerTableType);

К сожалению, невозможно вызывать методы непосредственно на ссылке типа object . Чтобы можно было обратиться к объекту COM, необходимо использовать метод InvokeMember объекта Type , который был создан вначале. При вызове метода InvokeMember ему передается ссылка на объект COM вместе с именем вызываемого метода COM, а также массив типа object всех входящих аргументов метода.

ObjCustomerTableType.InvokeMember("Delete", BindingFlags.InvokeMethod, null, objCustomerTable, aryInputArgs);

Напомним еще раз последовательность действий:

1. Создать объект Type для типа объекта COM с помощью метода класса Type.GetTypeFromProgID() .

2. Использовать этот объект Type для создания объекта COM с помощью Activator.CreateInstance() .

3. Методы вызываются на объекте COM, вызывая метод InvokeMember на объекте Type и передавая в него ссылку object в качестве входящего аргумента. Ниже приведен пример кода, объединяющий все это в один блок:

using System.Runtime.InteropServices;
Type objCustomerTableType;
object objCustomerTable;
objCustomerTableType=Type.GetTypeFromProgID("DataAccess.CustomerTable");
objCustomerTable=Activator.CreateInstance(ObjCustomerTableType);
objCustomerTableType.InvokeMember("Delete", BindingFlags, InvokeMethod, null, objCustomerTable, aryInputArgs);
objCustomerTableType = Type.GetTypeFromProgID("DataAccess.CustomerTable");

Хотя средства позднего связывания C# позволяют избежать трудностей RCW, необходимо знать о некоторых, связанных с этим, недостатках.

Первый: позднее связывание может быть опасным. При использовании раннего связывания компилятор может запросить библиотеку типов компонента COM, чтобы убедиться, что все вызываемые на объектах COM методы в действительности существуют. При позднем связывании ничто не препятствует опечатке в вызове метода InvokeMember() , что может породить ошибку во время выполнения.

Последнее обновление: 04.02.2019

Ранее было рассмотрена два способа изменения функциональности методов, унаследованных от базового класса - сокрытие и переопределение. В чем разница между двумя этими способами?

Переопределение

Возьмем пример с переопределением методов:

Class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public virtual void Display() { Console.WriteLine($"{FirstName} {LastName}"); } } class Employee: Person { public string Company { get; set; } public Employee(string firstName, string lastName, string company) : base(firstName, lastName) { Company = company; } public override void Display() { Console.WriteLine($"{FirstName} {LastName} работает в {Company}"); } }

Также создадим объект Employee и передадим его переменной типа Person:

Person tom = new Employee("Tom", "Smith", "Microsoft"); tom.Display(); // Tom Smith работает в Microsoft

Теперь мы получаем иной результат, нежели при сокрытии. А при вызове tom.Display() выполняется реализация метода Display из класса Employee.

Для работы с виртуальными методами компилятор формирует таблицу виртуальных методов (Virtual Method Table или VMT). В нее записывается адреса виртуальных методов. Для каждого класса создается своя таблица.

Когда создается объект класса, то компилятор передает в конструктор объекта специальный код, который связывает объект и таблицу VMT.

А при вызове виртуального метода из объекта берется адрес его таблицы VMT. Затем из VMT извлекается адрес метода и ему передается управление. То есть процесс выбора реализации метода производится во время выполнения программы. Собственно так и выполняется виртуальный метод. Следует учитывать, что так как среде выполнения вначале необходимо получить из таблицы VMT адрес нужного метода, то это немного замедляет выполнение программы.

Сокрытие

Теперь возьмем те же классы Person и Employee, но вместо переопределения используем сокрытие:

Class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public void Display() { Console.WriteLine($"{FirstName} {LastName}"); } } class Employee: Person { public string Company { get; set; } public Employee(string firstName, string lastName, string company) : base(firstName, lastName) { Company = company; } public new void Display() { Console.WriteLine($"{FirstName} {LastName} работает в {Company}"); } }

И посмотрим, что будет в следующем случае:

Person tom = new Employee("Tom", "Smith", "Microsoft"); tom.Display(); // Tom Smith

Переменная tom представляет тип Person, но хранит ссылку на объект Employee. Однако при вызове метода Display будет выполняться та версия метода, которая определена именно в классе Person, а не в классе Employee. Почему? Класс Employee никак не переопределяет метод Display, унаследованный от базового класса, а фактически определяет новый метод. Поэтому при вызове tom.Display() вызывается метод Display из класса Person.