18.5 – Раннее и позднее связывание
В этом и следующем уроках мы подробнее рассмотрим, как реализуются виртуальные функции. Хотя эта информация не является строго необходимой для эффективного использования виртуальных функций, но она интересна. Тем не менее, вы можете считать оба этих раздела необязательными для прочтения.
Когда программа на C++ выполняется, она выполняется последовательно, начиная с начала main()
. Когда встречается вызов функции, точка выполнения переходит к началу вызываемой функции. Как CPU узнает об этом?
Когда программа компилируется, компилятор преобразует каждую инструкцию в вашей программе на C++ в одну или несколько строк машинного кода. Каждой строке машинного кода дается собственный уникальный адрес в последовательности адресов. То же самое и с функциями – когда встречается функция, она преобразуется в машинный код и получает следующий доступный адрес. Таким образом, каждая функция получает уникальный адрес.
Связывание относится к процессу, который используется для преобразования идентификаторов (например, имен переменных и функций) в адреса. Хотя связывание используется как для переменных, так и для функций, в этом уроке мы сосредоточимся на связывании функций.
Раннее связывание
Большинство вызовов функций, с которыми сталкивается компилятор, будут прямыми вызовами функций. Прямой вызов функции – это инструкция, которая вызывает функцию напрямую. Например:
#include <iostream>
void printValue(int value)
{
std::cout << value;
}
int main()
{
printValue(5); // это прямой вызов функции
return 0;
}
Прямые вызовы функций могут быть разрешены с помощью процесса, известного как раннее связывание. Раннее связывание (также называемое статическим связыванием) означает, что компилятор (или компоновщик) может напрямую связать имя идентификатора (например, имя функции или переменной) с машинным адресом. Помните, что каждая функция имеет уникальный адрес. Поэтому, когда компилятор (или компоновщик) сталкивается с вызовом функции, он заменяет вызов функции инструкцией машинного кода, которая сообщает процессору перейти к адресу функции.
Давайте посмотрим на простую программу калькулятора, в которой используется раннее связывание:
#include <iostream>
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 << "Enter a number: ";
std::cin >> x;
int y;
std::cout << "Enter another number: ";
std::cin >> y;
int op;
do
{
std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
std::cin >> 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 << "The answer is: " << result << '\n';
return 0;
}
Поскольку add()
, subtract()
и multiply()
являются прямыми вызовами функций, компилятор будет использовать раннее связывание для разрешения вызовов функций add()
, subtract()
и multiply()
. Компилятор заменит вызов функции add()
инструкцией, которая сообщает процессору перейти к адресу функции add()
. То же самое верно и для subtract()
и multiply()
.
Позднее связывание
В некоторых программах до момента выполнения (когда программа запущена) невозможно узнать, какая функция будет вызвана. Это называется поздним связыванием (или динамическим связыванием). В C++ один из способов получить позднее связывание – использовать указатели на функции. Вкратце, указатель на функцию – это тип указателя, который указывает на функцию, а не на переменную. Функция, на которую указывает указатель, может быть вызвана с помощью применения оператора вызова функции (()
) к указателю.
Например, следующий код вызывает функцию add()
:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
int main()
{
// Создаем указатель на функцию и
// заставляем его указывать на функцию add
int (*pFcn)(int, int) = add;
std::cout << pFcn(5, 3) << '\n'; // складываем 5 + 3
return 0;
}
Вызов функции через указатель на функцию также известен как косвенный вызов функции. Следующая программа калькулятора функционально идентична примеру калькулятора, приведенному выше, за исключением того, что в ней вместо прямого вызова функций используется указатель на функцию:
#include <iostream>
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 << "Enter a number: ";
std::cin >> x;
int y;
std::cout << "Enter another number: ";
std::cin >> y;
int op;
do
{
std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
std::cin >> 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 << "The answer is: " << pFcn(x, y) << '\n';
return 0;
}
В этом примере вместо прямого вызова функции add()
, subtract()
или multiply()
мы установили pFcn
так, чтобы он указывал на функцию, которую мы хотим вызвать. Затем мы вызываем функцию через этот указатель. Компилятор не может использовать раннее связывание для разрешения вызова функции pFcn(x, y)
, потому что во время компиляции он не может сказать, на какую функцию будет указывать pFcn
!
Позднее связывание немного менее эффективно, поскольку предполагает дополнительный уровень косвенного обращения. При раннем связывании CPU может напрямую перейти к адресу функции. При позднем связывании программа должна сначала прочитать адрес, содержащийся в указателе, а затем перейти к этому адресу. Это включает в себя один дополнительный шаг, что делает этот тип связывания немного медленнее. Однако преимущество позднего связывания состоит в том, что оно гибче, чем раннее связывание, поскольку решения о том, какую функцию вызывать, не нужно принимать до времени выполнения.
В следующем уроке мы рассмотрим, как позднее связывание используется для реализации виртуальных функций.