
Урок 9.
План занятия.
Встраивание.
Перегрузка функций.
Шаблоны функций.
Встраивание.
Ключевое слово inline.
В прошлом уроке мы познакомились с понятием функции. И выяснили, что как только программа встречает вызов функции, она сразу же обращается к телу данной функции и выполняет его. Этот процесс существенно сокращает код программы, но при этом увеличивает время ее выполнения за счет постоянных обращений к описанию конкретной вызванной функции. Но, бывает и не так. Некоторые функции в языке C можно определить с использованием специального служебного слова inline.
Данный спецификатор позволяет определить функцию как встраиваемую, то есть подставляемую в текст программы в местах обращения к этой функции. Например, следующая функция определена как подставляемая:
inline float module(float x = 0, float у = 0) { return sqrt(x * x + у * у); } |
Обрабатывая каждый вызов встраиваемой функции module, компилятор подставляет на место ее вызова - в текст программы - код операторов тела функции. Тем самым при многократных вызовах подставляемой функции, размер программы может увеличиться, однако исключаются временные затраты на обращение к вызываемой функции и возврат из нее в основную функцию программы.
Примечание: Наиболее эффективно использовать подставляемые функции в тех случаях, когда тело функции состоит всего из нескольких операторов.
Случается так, что компилятор не может определить функцию, как встраиваемую и просто игнорирует ключевое слово inline. Перечислим причины, которые приводят к такому результату:
1. Слишком большой размер функции.
2. Функция является рекурсивной. (с этим понятием вы познакомитесь в следующих уроках)
3. Функция повторяется в одном и том же выражении несколько раз
4. Функция содержит цикл, switch или if.
Как видите - всё просто, но inline-функции не единственный способ встраивания. Об этом расскажет следующая тема урока.
Раскрытие макро.
Помимо вызова функции, для встраивания в программу повторяющегося фрагмента используют, так называемое, раскрытие макро. Для этих целей применяется директива препроцессора #define, со следующим синтаксисом:
#define Имя_макроса(Параметры) (Выражение) |
Пример.
#include <iostream>
#define SQR(X) ((X) * (X)) #define CUBE(X) (SQR(X)*(X)) #define ABS(X) (((X) < 0)? -(X) : X)
using namespace std; void main() { у = SQR(t + 8) - CUBE(t - 8) ; cout <<sqrt(ABS(y)) ; } |
1. C помощью директивы #define объявляются три макроса sqr(x), cube(x) и abs(x).
2. В функции main происходит вызов вышеописанных макросов по имени.
3. Препроцессор раскрывает макро (т. е. подставляет на место вызова выражение из директивы #define) и передает получившийся текст компилятору.
4. После встраивания выражение в main выглядит для программы таким образом:
у = ((t+8) * (t+8)) - ((((t-8)) * (t-8)) * (t-8)); cout << sqrt(((y < 0)? -(y) : y)); |
Примечание: Следует обратить внимание на использование скобок при объявлении макроса. С помощью них мы избегаем ошибок в последовательности вычислений. Например:
#define SQR(X) X * X у = SQR(t + 8); //раскроет макро t+8*t+8 |
В примере при вызове макроса SQR сначала выполнится умножение 8 на t, а потом к результату прибавится значение переменной t и восьмерка, хотя очевидно, что нашей целью было получение квадрата суммы t+8.
Теперь вы полностью знакомы с понятием встраивания и можете использовать его в своих программах.
Встроенные функции являются усовершенствованием языка C++, предназначенным для ускорения работы программ. Основное различие между встроенными и обычными функциями связано не с написанием кода, а с тем, каким образом компилятор внедряет функцию в программу.
Конечным продуктом процесса компиляции является исполняемая программа, которая состоит из набора машинных команд. При запуске программы операционная система загружает эти команды в оперативную память так, что каждая команда обладает собственным адресом в памяти. Затем команды поочередно выполняются. Когда в программе встречается, например, оператор цикла или условного перехода, выполнение "перепрыгивает" вперед или назад через несколько команд, осуществляя переход по определенному адресу. При вызове обычной функции также осуществляется переход к определенному адресу (адресу функции) с последующим возвратом после завершения ее работы. Рассмотрим типичную реализацию этого процесса немного подробнее. Когда в программе встречается команда вызова функции, программа сохраняет адрес команды, следующей сразу после вызова функции, копирует аргументы функции в стек (зарезервированный для этой цели блок памяти), переходит к ячейке памяти, обозначающей начало функции, выполняет код функции (возможно, помещая возвращаемое значение в регистр), а затем переходит к команде, адрес которой сохранен 1. Переходы и запоминание соответствующих адресов влекут за собой дополнительные затраты времени, связанные с использованием функций.
Скомпилированный код встроенной функции непосредственно встраивается в код программы. Иначе говоря, компилятор подставляет вместо вызова функции ее код. В результате программе не нужно выполнять переход к другому адресу и возвращаться назад. Таким образом, встраиваемые функции выполняются немного быстрее, чем обычные, однако за это нужно платить дополнительным расходом памяти. Если в десяти различных местах программа выполняет вызов одной и той же встроенной функции, ее код будет содержать десять копий этой функции.
Решение об использовании встроенной функции должно быть взвешенным. Если затраты времени на выполнение функции значительно превышают длительность реализации механизма ее вызова, экономия времени на фоне общего процесса окажется незаметной. Если же время выполнения кода невелико, то разница во времени при использовании встроенной функции по сравнению с обычной может оказаться значительной. С другой стороны, в этом случае ускоряется и без того сравнительно быстрый процесс, поэтому при нечастом вызове функции общая экономия времени может быть невелика.
1 - Это можно сравнить с процессом чтения некоторого текста, когда приходится отвлекаться на ознакомление с содержанием сноски, а затем возвращаться к фрагменту, где чтение было прервано.
При вызове обычной функции управление программой передается отдельному блоку кода.
Вызов встроенной функции заменяется ее кодом. (в функии main непосредственно располагаются эти функции, и могут неоднократно!!)
Чтобы воспользоваться встроенной функцией, нужно выполнить хотя бы одно из следующих действий.
• Предварить объявление функции ключевым словом inline.
• Предварить определение функции ключевым словом inline.
Общепринято опускать прототип и помещать полное описание (заголовок и весь код функции) туда, где обычно находится прототип.
Компилятор не обязательно удовлетворит запрос пользователя на то, чтобы сделать функцию встроенной. Он может прийти к заключению, что функция слишком велика, или обнаружит, что она обращается сама к себе (рекурсия для встроенных функций не допускается и невозможна сама по себе). Кроме того, возможен случай, когда опция реализации встроенных функций у компилятора отключена либо он не поддерживает эту опцию вообще.
В листинге иллюстрируется метод встраивания на примере функции square (), которая возводит в квадрат переданный ей аргумент. Обратите внимание, что все определение функции уместилось в одну строку. Хотя это не обязательно, но если определение не помещается в одной или двух строках (предполагается, что длинные идентификаторы не используются), то такая функция, скорее всего, является плохим кандидатом на то, чтобы быть встроенной.
Задача. Использование встроенной функции вычисляющей квадрат числа.
#include <iostream>
using namespace std;
// Определение встроенной функции
inline double square (double x) { return x * x; }
void main()
{
setlocale (LC_CTYPE, "russian");
double x, y;
cout << "Введите число для возведения в квадрат: ";
cin >> y;
x = square(y);
cout << "rezult = " << x << "\n\n";
cout << "next rezult = " << square (++y) << "\n";
cout << "Now y = " << y << "\n";
cin.get();
cin.get();
}
Вывод:
Введите число для возведения в квадрат: 4
rezult = 16
next rezult = 25
Now y = 5
Для продолжения нажмите любую клавишу . . .
Полное определение функции, которое дается перед тем, как она будет выполнена первый раз, служит прототипом. Это означает, что можно использовать функцию square () с аргументом типа int или long, и программа автоматически выполнит приведение аргумента к типу double, прежде чем передавать его значение функции