В чем разница раннего и позднего связывания
Перейти к содержимому

В чем разница раннего и позднего связывания

  • автор:

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

Имеются два термина, часто используемых, когда речь заходит об объектно-ориентированных языках программирования: раннее и позднее связывание. По отношению к С++ эти термины соответствуют событиям, которые возникают на этапе компиляции и на этапе исполнения про­граммы соответственно.

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

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

Какое именно связывание должна использовать программа, зависит от предназначения про­граммы. Фактически достаточно сложные программы используют оба вида связывания. Позднее связывание является одним из самых мощных добавлений языка С++ к возможностям языка С. Платой за такое увеличение мощи программы служит некоторое уменьшение ее скорости испол­нения. Поэтому использование позднего связывания оправдано только тогда, когда оно улучша­ет структурированность и управляемость программы. Надо иметь в виду, что проигрыш в произ­водительности невелик, поэтому когда ситуация требует позднего связывания, можно использовать его без всякого сомнения.

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

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

Когда программа на C++ выполняется, она выполняется последовательно, начиная с начала main() . Когда встречается вызов функции, точка выполнения переходит к началу вызываемой функции. Как CPU узнает об этом?

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

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

Раннее связывание

Большинство вызовов функций, с которыми сталкивается компилятор, будут прямыми вызовами функций. Прямой вызов функции – это инструкция, которая вызывает функцию напрямую. Например:

#include void printValue(int value) < std::cout int main() < printValue(5); // это прямой вызов функции return 0; >

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

Давайте посмотрим на простую программу калькулятора, в которой используется раннее связывание:

#include int add(int x, int y) < return x + y; >int subtract(int x, int y) < return x - y; >int multiply(int x, int y) < return x * y; >int main() < int x; std::cout > x; int y; std::cout > y; int op; do < std::cout > op; > while (op < 0 || op >2); int result = 0; switch (op) < // вызываем целевую функцию напрямую, используя раннее связывание case 0: result = add(x, y); break; case 1: result = subtract(x, y); break; case 2: result = multiply(x, y); break; >std::cout

Поскольку add() , subtract() и multiply() являются прямыми вызовами функций, компилятор будет использовать раннее связывание для разрешения вызовов функций add() , subtract() и multiply() . Компилятор заменит вызов функции add() инструкцией, которая сообщает процессору перейти к адресу функции add() . То же самое верно и для subtract() и multiply() .

Позднее связывание

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

Например, следующий код вызывает функцию add() :

#include int add(int x, int y) < return x + y; >int main() < // Создаем указатель на функцию и // заставляем его указывать на функцию add int (*pFcn)(int, int) = add; std::cout 

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

#include int add(int x, int y) < return x + y; >int subtract(int x, int y) < return x - y; >int multiply(int x, int y) < return x * y; >int main() < int x; std::cout > x; int y; std::cout > y; int op; do < std::cout > op; > while (op < 0 || op >2); // Создаем указатель на функцию с именем pFcn (да, синтаксис уродлив) int (*pFcn)(int, int) = nullptr; // Устанавливаем pFcn, чтобы указывать на функцию, которую выбрал пользователь switch (op) < case 0: pFcn = add; break; case 1: pFcn = subtract; break; case 2: pFcn = multiply; break; >// Вызов функции, на которую указывает pFcn, с параметрами x и y // Это использует позднее связывание std::cout

В этом примере вместо прямого вызова функции add() , subtract() или multiply() мы установили pFcn так, чтобы он указывал на функцию, которую мы хотим вызвать. Затем мы вызываем функцию через этот указатель. Компилятор не может использовать раннее связывание для разрешения вызова функции pFcn(x, y) , потому что во время компиляции он не может сказать, на какую функцию будет указывать pFcn !

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

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

В чем разница раннего и позднего связывания

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

Приведенный пример в определенной мере отражает смысл применения раннего и позднего связывания, соответственно. Очевидно, что для данного примера первый вариант оптимален. Во втором случае мы слишком много всего предусмотрели, но нам это не понадобилось. С другой стороны, если по дороге на рынок мы решим, что апельсины нам не нужны и решим купить 10 кг. яблок, то в первом случае мы уже не сможем этого сделать. Во втором же случае - легко.

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

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

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

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

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

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

Приведем определение виртуальных методов:

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

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

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

Для иллюстрации применения виртуальных методов приведу пример на языке С++, который я позаимствовал из одного C++ Tutorial. Даже если вы не очень разбираетесь в этом языке, надеюсь, что мои пояснения хоть как-то объяснят его смысл.

#include // подключение стандартной библиотек С++, в // которой описаны некоторый функции, применяемые в программе class vehicle // класс "транспортное средство" < int wheels; float weight; public: // начало публичного(открытого) раздела класса virtual void message(void) // описание виртуальной функции message класса vehicle и реализация этой // функции. При вызове функции message класса vehicle на экран монитора // будет выведена строка "Транспортное средство" >; class car : public vehicle // класс "легковая машина", унаследованный из // класса "транспортное средство" < int passenger_load; public: // начало публичного(открытого) раздела класса void message(void) // описание виртуальной функции message класса car и реализация этой // функции. При вызове функции message класса car на экран монитора // будет выведена строка " Легковая машина " >; class truck : public vehicle // класс "грузовая машина", унаследованный из // класса "транспортное средство" < int passenger_load; float payload; public: // начало публичного(открытого) раздела класса int passengers(void) >; class boat : public vehicle // класс "лодка", унаследованный из // класса "транспортное средство" < int passenger_load; public: // начало публичного(открытого) раздела класса int passengers(void) void message(void) // описание виртуальной функции message класса boat и реализация этой // функции. При вызове функции message класса boat на экран монитора // будет выведена строка "Лодка" >; void main() // основной исполняемый блок программы < vehicle *unicycle; // описываем переменной unicycle как указатель на // объект класса vehicle unicycle = new vehicle; // Создаем объект класса vehicle, // указатель unicycle указывает на этот объект unicycle->message(); // вызываем метод message объекта delete unicycle; // удаляем объект unicycle // Все последующие блоки по 3 строки абсолютно идентичны первому // блоку с той лишь разницей, что изменяется класс создаваемого объекта // на car, truck, boat unicycle = new car; unicycle->message(); delete unicycle; unicycle = new truck; unicycle->message(); delete unicycle; unicycle = new boat; unicycle->message(); delete unicycle; >

Результаты работы программы(вывод на экран):

Транспортное средство Легковая машина Транспортное средство Лодка

Рассмотрим приведенный пример. У нас есть три класса car, truck и boat, которые являются производными от базового класса vehicle. В базовом классе vehicle описана виртуальная функция message. В двух из трех классов(car, boat) также описаны свои функции message, а в классе truck нет описания своей функции message. Все строки, к которым я не приводил комментарии, не имеют принципиального для данного примера значения. Теперь пробежимся по основному блоку программы - функции main(). Описываем переменную unicycle, как указатель на объект типа vehicle. Не буду вдаваться в подробности, почему именно указатель на объект. Так надо. В данном случае воспринимайте работу с указателем, как с самим объектом. Подробности работы с указателями можно найти в описаниях конкретного языка ООП. Затем, создаем объект класса vehicle, переменная unicycle указывает на этот объект. После этого вызываем метод message объекта unicycle, а в следующей строке удаляем этот объект. В следующих трех блоках по 3 строки проводим аналогичные операции, с той лишь разницей, что работаем с объектами классов car, truck, boat. Применение указателя позволяет нам использовать один и этот же указатель для всех производных классов. Нас интересует вызов функции message для каждого из объектов. Если бы мы не указали, что функция message класса vehicle является виртуальной(virtual), то компилятор статически(жестко) связал бы любой вызов метода объекта указателя unicycle с методом message класса vehicle, т.к. при описании мы сказали, что переменная unicycle указывает на объект класса vehicle. Т.е. произвели бы раннее связывание. Результатом работы такой программы был бы вывод четырех строк "Транспортное средство". Но за счет применения виртуальной функции в классе мы получили несколько другие результаты.

При работе с объектами классов car и boat вызываются их собственные методы message, что и подтверждается выводом на экран соответствующих сообщений. У класса truck нет своего метода message, по этой причине производится вызов соответствующего метода базового класса vehicle.

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

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

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

Объясните наглядно разницу раннего и позднего связывания методов.

Позднее связывание методов это когда имеется ссылочная переменная, и в зависимости от того экземпляр какого класса будет создан, и будет вызван соответствующий метод. А как насчет раннего связывания в чем отличие?

Отслеживать
задан 28 июн 2013 в 15:06
1,986 6 6 золотых знаков 29 29 серебряных знаков 48 48 бронзовых знаков

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

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

Кстати, то что вы называете поздним связываением, есть скорее dynamic dispatch.

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

Отслеживать
ответ дан 28 июн 2013 в 15:14
misha-nesterenko misha-nesterenko
956 5 5 серебряных знаков 8 8 бронзовых знаков
Можно пример позднего связывания
28 июн 2013 в 15:36

Вот пример: Object a = . // какое-то присваивание a.toString(); На этапе компиляции мы не знаем, какого типа объект a . Он может быть как собственно Object , так и любым его наследником, в котором метод toString() переопределён. Именно на этапе выполнения определяется тип a и вызывается toString() из того класса, какого типа объект a . Это и есть позднее связывание.

28 июн 2013 в 15:54
@JAVAVladuxa, посмотри en.wikipedia.org/wiki/Late_binding#Late_binding_in_Java
28 июн 2013 в 18:11

Раннее связывание, как было отмечено выше, происходит на этапе компиляции. Оно применяется при вызове обычных методов (не виртуальных).

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

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

По поводу Java могу сказать, что там, если не ошибаюсь, ко всем методам по умолчанию применяется позднее связывание (если они не помечены модификатором final) в отличие от, скажем, С++, где по умолчанию применяется раннее связывание. Еще для большего понимания вопроса почитайте про таблицу виртуальных методов

З.Ы. с Java близко не знаком, поэтому точно не могу сказать, насколько там применим термин "функция"

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *