Chapter 1. Процедурно-ориентированное программирование

В части II были представлены базовые компоненты языка С++: встроенные типы данных (int и double), типы классов (std::string и std::vector) и операции, которые можно совершать над данными. В части III мы увидим, как из этих компонентов строятся функции, служащие для реализации алгоритмов.

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

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

Для облегчения использования функций С++ предлагает множество средств, рассматриваемых нами в части III. Первым из них является перегрузка. Функции, которые выполняют семантически одну и ту же операцию, но работают с разными типами данных и потому имеют несколько отличающиеся реализации, могут иметь общее имя. Например, все функции для печати значений разных типов, таких, как int, std::string и т.д., называются print(). Поскольку программисту не приходится запоминать много разных имен для одной и той же операции, пользоваться ими становится проще. Компилятор сам подставляет нужное в зависимости от типов фактических аргументов. В главе 9 объясняется, как объявлять и использовать перегруженные функции и как компилятор выбирает подходящую из набора перегруженных.

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

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

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

Мы рассмотрели, как объявлять переменные, как писать выражения и инструкции. Здесь мы покажем, как группировать эти компоненты в определения функций, чтобы облегчить их многократное использование внутри программы. Мы увидим, как объявлять и определять функции и как вызывать их, рассмотрим различные виды передаваемых параметров и обсудим особенности использования каждого вида. Мы расскажем также о различных видах значений, которые может вернуть функция. Будут представлены четыре специальных случая применения функций: встроенные (inline), рекурсивные, написанные на других языках и объявленные директивами связывания, а также функция main(). В завершение главы мы разберем более сложное понятие – указатель на функцию.

Content

§ 1.1. Введение

§ 1.2. Прототип функции

§ 1.3. Передача аргументов

§ 1.4. Возврат значения

§ 1.5. Рекурсия

§ 1.6. Встроенные функции

§ 1.7. Директива связывания extern "C"

§ 1.8. Функция main(): разбор параметров командной строки

§ 1.9. Указатели на функции

§ 1.1. Введение

Функцию можно рассматривать как операцию, определенную пользователем. В общем случае она задается своим именем. Операнды функции, или формальные параметры, задаются в списке параметров, через запятую. Такой список заключается в круглые скобки. Результатом функции может быть значение, которое называют возвращаемым. Об отсутствии возвращаемого значения сообщают ключевым словом void. Действия, которые производит функция, составляют ее тело; оно заключено в фигурные скобки. Тип возвращаемого значения, ее имя, список параметров и тело составляют определение функции. Вот несколько примеров:

inline int abs(int obj) {
    // возвращает абсолютное значение iobj
    return(iobj < 0 ? -iobj : iobj);
}
inline int min(int p1, int p2) {
    // возвращает меньшую из двух величин
    return(pi < p2 ? pi : p2);
}

int gcd(int vl, int v2) {
    // возвращает наибольший общий делитель
    while (v2) {
        int temp = v2;
        v2 = vl % v2;
        vl = temp;
    }
    return vl;
}

Выполнение функции происходит тогда, когда в тексте программы встречается оператор вызова. Если функция принимает параметры, при ее вызове должны быть указаны фактические параметры, аргументы. Их перечисляют внутри скобок, через запятую. В следующем примере main() дважды вызывает abs() и по одному разу min() и gcd(). Функция main() определяется в файле main.cpp.

#include <iostream>

int main() {
    // прочитать значения из стандартного ввода
    std::cout << "Введите первое значение: ";
    int i;
    std::cin >> i;
    if (!std::cin) {
        std::cerr << "!? Ошибка ввода - аварийный выход!\n";
        return -1;
    }

    std::cout << "Введите второе значение: ";
    int j;
    std::cin >> j;
    if (!std::cin) {
        std::cerr << "!? Ошибка ввода - аварийный выход!\n";
        return -2;
    }

    std::cout << "\nmin: " << min(i, j) << std::endl;
    i = abs(i);
    j = abs(j);
    std::cout << "НОД: " << gcd(i, j) << std::endl;
    return 0;
}

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

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

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

В нашем примере файл main.cpp не содержит определений abs(), min() и gcd(), поэтому вызов любой из них приводит к ошибке компиляции. Чтобы компиляция была успешной, их необязательно определять, достаточно только объявить:

int abs(int);
int min(int, int);
int gcd(int, int);

(В таком объявлении можно не указывать имя параметра, ограничиваясь названием типа.)

Объявления (а равно определения встроенных функций) лучше всего помещать в заголовочные файлы, которые могут включаться всюду, где необходимо вызвать функцию. Таким образом, все файлы используют одно общее объявление. Если его необходимо модифицировать, изменения будут локализованы. Вот так выглядит заголовочный файл для нашего примера. Назовем его localMath.h:

// определение функции находится в файле gcd.С
int gcd(int, int);

inline int abs(int i) {
  return(i<0 ? -i : i);
}
inline int min(int vl, int v2) {
  return(vl<v2 ? vl : v2);
}

В объявлении функции описывается ее интерфейс. Он содержит все данные о том, какую информацию должна получать функция (список параметров) и какую информацию она возвращает. Для пользователей важны только эти данные, поскольку лишь они фигурируют в точке вызова. Интерфейс помещается в заголовочный файл, как мы поступили с функциями min(), abs() и gcd().

При выполнении наша программа main.cpp, получив от пользователя значения:

Введите первое значение: 15
Введите второе значение: 123

выдаст следующий результат:

mm: 15
НОД: 3

§ 1.2. Прототип функции

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

7.2.1. Тип возвращаемого функцией значения

Тип возвращаемого функцией значения бывает встроенным, как int или double, составным, как int& или double*, или определенным пользователем – перечислением или классом. Можно также использовать специальное ключевое слово void, которое говорит о том, что функция не возвращает никакого значения:

#include <string>
#include <vector>

class Date { /* определение */ };

bool look_up(int *, int);
double calc(double);
int count(const std::string&, char);
Date& calendar(const char);
void sum(std::vector<int>&, int);

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

// массив не может быть типом возвращаемого значения
int[10] foo_bar();

Но можно вернуть указатель на первый элемент массива:

// правильно: указатель на первый элемент массива
int *foo_bar();

(Размер массива должен быть известен вызывающей программе.)

Функция может возвращать типы классов, в частности контейнеры. Например:

// правильно: возвращается список символов
std::list<char> foo_bar();

(Этот подход не очень эффективен. Обсуждение типа возвращаемого значения см. в разделе 7.4.)

Тип возвращаемого функцией значения должен быть явно указан. Приведенный ниже код вызывает ошибку компиляции:

// ошибка: пропущен тип возвращаемого значения
const is_equa1(std::vector<int> vl, std::vector<int> v2);

В предыдущих версиях С++ в подобных случаях считалось, что функция возвращает значение типа int. Стандарт С++ отменил это соглашение. Правильное объявление is_equal() выглядит так:

// правильно: тип возвращаемого значения указан
const bool is_equa1(std::vector<int> vl, std::vector<int> v2);

7.2.2. Список параметров функции

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

int fork();

int fork(void);

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

int manip(int vl, v2); // ошибка

int manip(int vl, int v2); // правильно

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

void print(int *array, int size);

Имена параметров в объявлении и в определении одной и той же функции не обязаны совпадать. Однако употребление разных имен может запутать пользователя.

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

7.2.3. Проверка типов формальных параметров

Функция gcd() объявлена следующим образом:

int gcd(int, int);

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

Что будет, если попытаться вызвать функцию gcd() с аргументами типа char*?

cd("hello", "world");

А если передать этой функции не два аргумента, а только один? Или больше двух? Что случится, если потеряется запятая между числами 24 и 312?

gcd(24312);

Единственное разумное поведение компилятора – сообщение об ошибке, поскольку попытка выполнить такую программу чревата весьма серьезными последствиями. С++ действительно не пропустит подобные вызовы. Текст сообщения будет выглядеть примерно так:

// gcd("hello", "world")
error: invalid argument types (const char *, const char *) --
       expecting (int, int)
ошибка: неверные типы аргументов (const char *, const char *) --
       ожидается (int, int)

// gcd(24312)
error: missing value for second argument
ошибка: пропущено значение второго аргумента

А если вызвать эту функцию с аргументами типа double? Должен ли этот вызов расцениваться как ошибочный?

gcd(3.14, 6.29);

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

Вызов превращается в

gcd(3, 6);

что дает в результате 3.

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

Пропуск аргумента при вызове или передача аргумента неуказанного типа часто служили источником ошибок в языке С. Теперь такие погрешности обнаруживаются на этапе компиляции.

Упражнение 7.1

Какие из следующих прототипов функций содержат ошибки? Объясните.

(a) set(int *, int);
(b) void func();
(c) std::string error(int);
(d) arr[10] sum(int *, int);

Упражнение 7.2: Напишите прототипы для следующих функций:

  1. Функция с именем compare, имеющая два параметра типа ссылки на класс matrix и возвращающая значение типа bool.
  2. Функция с именем extract без параметров, возвращающая контейнер set для хранения значений типа int.

Упражнение 7.3

Имеются объявления функций:

double calc(double);
int count(const std::string &, char);
void sum(std::vector<int> &, int);
std::vector<int> vec(10);

Какие из следующих вызовов содержат ошибки и почему?

(a) calc(23.4, 55.1);
(b) count("abcda", 'a');
(c) sum(vec, 43.8);
(d) calc(66);

§ 1.3. Передача аргументов

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

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

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

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

Однако такой способ передачи аргументов может не устраивать нас в следующих случаях:

swap() обменивает значения локальных копий своих аргументов. Те же переменные, что были использованы в качестве аргументов при вызове, остаются неизменными. Это можно проиллюстрировать, написав небольшую программу:

#include <iostream>

void swap(int, int);
int main() {
  int i = 10;
  int j = 20;
  std::cout << "Перед swap():\ti: " << i << "\tj: " << j << std::endl;

  swap(i, j);
  std::cout << "После swap():\ti: " << i << "\tj: " << j << std::endl;

  return 0;
}

Результат выполнения программы:

Перед swap(): i: 10 j: 20
После swap(): i: 10 j: 20

Достичь желаемого можно двумя способами. Первый – объявление параметров указателями. Вот как будет выглядеть реализация swap() в этом случае:

// pswap() обменивает значения объектов,
// адресуемых указателями vl и v2
void pswap(int *vl, int *v2) {
   int tmp = *v2;
   *v2 = *vl;
   *vl = tmp;
}

Функция main() тоже нуждается в модификации. Вместо передачи самих объектов необходимо передавать их адреса:

pswap(&i, &j);

Теперь программа работает правильно:

Перед swap(): i: 10 j: 20
После swap(): i: 20 j: 10

Альтернативой может стать объявление параметров ссылками. В данном случае реализация swap() выглядит так:

// rswap() обменивает значения объектов,
// на которые ссылаются vl и v2
void rswap(int& vl, int& v2) {
   int tmp = v2;
   v2 = vl;
   vl = tmp;
}

Вызов этой функции из main() аналогичен вызову первоначальной функции swap():

rswap(i, j);

Выполнив программу main(), мы снова получим верный результат.

7.3.1. Параметры-ссылки

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

В каких случаях применение параметров-ссылок оправданно? Во-первых, тогда, когда без использования ссылок пришлось бы менять типы параметров на указатели (см. приведенную выше функцию swap()). Во-вторых, при необходимости вернуть из функции несколько значений. В-третьих, для передачи большого объекта типа класса. Рассмотрим два последних случая подробнее.

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

#include <vector>

// параметр-ссылка 'occurs'
// содержит второе возвращаемое значение

std::vector<int>::const_iterator look_up(const std::vector<int>& vec,
    int value,    // искомое значение
    int& occurs)  // количество вхождений
{
    // res_iter инициализируется значением
    // следующего за конечным элемента
    std::vector<int>::const_iterator res_iter = vec.end();
    occurs = 0;

    for (std::vector<int>::const_iterator iter = vec.begin();
             iter != vec.end();
             ++iter)
        if (*iter == value) {
            if (res_iter == vec.end())
                res_iter = iter;
            ++occurs;
        }

    return res_iter;
}

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

class Huge { public: double stuff[1000]; };
extern int calc(const Huge&);

int main() {
    Huge table[1000];
    // ... инициализация table

    int sum = 0;
    for (int ix=0; ix < 1000; ++ix)
        // calc() ссылается на элемент массива
        // типа Huge
        sum += calc(tab1e[ix]);
    // ...
}

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

В следующем примере нарушается константность параметра xx функции foo(). Поскольку параметр функции foo_bar() не является ссылкой на константу, то нет гарантии, что вызов foo_bar() не изменит значения аргумента. Компилятор сигнализирует об ошибке:

class X;
extern int foo_bar(X&);
int foo(const X& xx) {
  // ошибка: константа передается
  // функции с параметром неконстантного типа
  return foo_bar(xx);
}

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

extern int foo_bar(const X&);
extern int foo_bar(X); // передача по значению

Вместо этого можно передать копию xx, которую позволено менять:

int foo(const X& xx) {
  // ...
  X x2 = xx; // создать копию значения

  // foo_bar() может поменять x2,
  // xx останется нетронутым
  return foo_bar(x2); // правильно
}

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

void ptrswap(int*& vl, int*& v2) {
   int *trnp = v2;
   v2 = vl;
   vl = tmp;
}

Объявление

int*& v1;

должно читаться справа налево: v1 является ссылкой на указатель на объект типа int. Модифицируем функцию main(), которая вызывала rswap(), для проверки работы ptrswap():

#include <iostream>
void ptrswap(int*& vl, int*& v2);
int main() {
  int i = 10;
  int j = 20;
  int *pi = &i;
  int *pj = &j;

  std::cout << "Перед ptrswap():\tpi: " << *pi << "\tpj: " << *pj << std::endl;
  ptrswap(pi, pj);
  std::cout << "После ptrswap():\tpi: " << *pi << "\tpj: " << pj << std::endl;

return 0;

}

Вот результат работы программы:

Перед ptrswap(): pi: 10 pj: 20
После ptrswap(): pi: 20 pj: 10

7.3.2. Параметры-ссылки и параметры-указатели

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

Ссылка может быть один раз инициализирована значением объекта, и впоследствии изменить ее нельзя. Указатель же в течение своей жизни способен адресовать разные объекты или не адресовать вообще.

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

class X;
void manip(X *px) {
   // проверим на 0 перед использованием
   if (px != 0)
     // обратимся к объекту по адресу...
}

Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею объект. Например:

class Type { };
void operate(const Type& p1, const Type& p2);
int main() {
  Type obj1;
  // присвоим objl некоторое значение
  // ошибка: ссылка не может быть равной 0
  Type obj2 = operate(objl, 0);
}

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

Одна из важнейших сфер применения параметров-ссылок – эффективная реализация перегруженных операций. При этом использование операций остается простым и интуитивно понятным. (Подробнее данный вопрос рассматривается в главе 15.) Разберем маленький пример. Представим себе класс Matrix (матрица). Хорошо бы реализовать операции сложения и присваивания привычным способом:

Matrix a, b, c;
c = a + b;

Эти операции реализуются с помощью перегруженных операторов – функций с немного необычным именем. Для оператора сложения такая функция будет называться operator+. Посмотрим, как ее определить:

Matrix // тип возврата - Matrix
operator+(// имя перегруженного оператора
Matrix m1, // тип левого операнда
Matrix m2) // тип правого операнда
{
   Matrix result;
   // необходимые действия
   return result;
}

При такой реализации сложение двух объектов типа Matrix выглядит вполне привычно:

a + b;

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

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

// реализация с параметрами-указателями
operator+(Matrix *ml, Matrix *m2)
{
   Matrix result;
   // необходимые действия
   return result;
}

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

&a + &b; // допустимо, хотя и плохо

Хотя такая форма не может не вызвать критику, но все-таки два объекта сложить еще удается. А вот три уже крайне затруднительно:

// а вот это не работает
// &a + &b возвращает объект типа Matrix
&a + &b + &c;

Для того чтобы сложить три объекта, при подобной реализации нужно написать так:

// правильно: работает, однако ...
&(&a + &b) + &c;

Трудно ожидать, что кто-нибудь согласится писать такие выражения. К счастью, параметры-ссылки дают именно то решение, которое требуется. Если параметр объявлен как ссылка, функция получает его l-значение, а не копию. Лишнее копирование исключается. И тип фактического аргумента может быть Matrix – это упрощает операцию сложения, как и для встроенных типов. Вот схема перегруженного оператора сложения для класса Matrix:

// реализация с параметрами-ссылками
operator+(const Matrix& m1, const Matrix& m2)
{
   Matrix result;
   // необходимые действия
   return result;
}

При такой реализации сложение трех объектов Matrix выглядит вполне привычно:

a + b + c;

Ссылки были введены в С++ именно для того, чтобы удовлетворить двум требованиям: эффективная реализация и интуитивно понятное применение.

7.3.3. Параметры-массивы

Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление

void putValues(int[10]);

рассматривается компилятором так, как будто оно имеет вид

void putValues(int*);

Размер массива неважен при объявлении параметра. Все три приведенные записи эквивалентны:

// три эквивалентных объявления putValues()
void putValues(int*);
void putValues(int[]);
void putValues(int[10]);

Передача массивов как указателей имеет следующие особенности:

При проверке типов параметров компилятор способен распознать, что в обоих случаях тип аргумента int* соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.

По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный параметр функции. Например:

void putValues(int[], int size);
int main() {
   int i, j[2];
   putValues(&i, 1);
   putValues(j, 2);
   return 0;
}

putValues() печатает элементы массива в следующем формате:

 (10)< 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >

где 10 – это размер массива. Вот как выглядит реализация putValues(), в которой используется дополнительный параметр:

#include <iostream>

const lineLength =12; // количество элементов в строке
void putValues(int *ia, int sz) {
    std::cout << "(" << sz << ")< ";
    for (int i=0;i < sz; ++i)
    {
        if (i % lineLength == 0 && i)
            std::cout << "\n\t"; // строка заполнена

        std::cout << ia[i];

        // разделитель, печатаемый после каждого элемента,
        // кроме последнего
        if (i % lineLength != lineLength-1 &&
                 i != sz-1)
            std::cout << ", ";
    }
    std::cout << " >\n";
}

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

// параметр - ссылка на массив из 10 целых
void putValues(int (&arr)[10]);
int main() {
   int i, j [2];
   putValues(i); // ошибка:
                 // аргумент не является массивом из 10 целых
   putValues(j); // ошибка:
                 // аргумент не является массивом из 10 целых
   return 0;
}

Поскольку размер массива теперь является частью типа параметра, новая версия putValues() способна работать только с массивами из 10 элементов. Конечно, это ограничивает ее область применения, зато реализация значительно проще:

#include <iostream>

void putValues(int (&ia)[10])
{
    std::cout << "(10)< ";
    for (int i = 0; i < 10; ++i) { std::cout << ia[i];

    // разделитель, печатаемый после каждого элемента,
    // кроме последнего
    if (i != 9)
        std::cout << ", ";
    }
    std::cout << " >\n";
}

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

Хотя две предыдущих реализации putValues() правильны, они обладают серьезными недостатками. Так, первый вариант работает только с массивами типа int. Для типа double* нужно писать другую функцию, для long* – еще одну и т.д. Второй вариант производит операции только над массивом из 10 элементов типа int. Для обработки массивов разного размера нужны дополнительные функции. Лучшим решением было бы использовать шаблон – функцию, или, скорее, обобщенную реализацию кода целого семейства функций, которые отличаются только типами обрабатываемых данных. Вот как можно сделать из первого варианта putValues() шаблон, способный работать с массивами разных типов и размеров:

template <class Type>
void putValues(Type *ia, int sz) {
   // так же, как и раньше
}

Параметры шаблона заключаются в угловые скобки. Ключевое слово class означает, что идентификатор Type служит именем параметра, при конкретизации шаблона функции putValues() он заменяется на реальный тип – int, double, std::string и т.д. (В главе 10 мы продолжим разговор о шаблонах функций.)

Параметр может быть многомерным массивом. Для такого параметра должны быть заданы правые границы всех измерений, кроме первого. Например:

putValues(int matrix[][10], int rowSize);

Здесь matrix объявляется как двумерный массив, который содержит десять столбцов и неизвестное число строк. Эквивалентным объявлением для matrix будет:

int (*matrix)[10]

Многомерный массив передается как указатель на его нулевой элемент. В нашем случае тип matrix – указатель на массив из десяти элементов типа int. Как и для одномерного массива, граница первого измерения не учитывается при проверке типов. Если параметры являются многомерными массивами, то контролируются все измерения, кроме первого.

Заметим, что скобки вокруг *matrix необходимы из-за более высокого приоритета операции взятия индекса. Инструкция

int *matrix[10];

объявляет matrix как массив из десяти указателей на int.

7.3.4. Абстрактные контейнерные типы в качестве параметров

Абстрактные контейнерные типы, представленные в главе 6, также используются для объявления параметров функции. Например, можно определить putValues() как имеющую параметр типа std::vector<int> вместо встроенного типа массива.

Контейнерный тип является классом и обеспечивает значительно большую функциональность, чем встроенные массивы. Так, std::vector<int> знает собственный размер. В предыдущем подразделе мы видели, что размер параметра-массива неизвестен функции и для его передачи приходится задавать дополнительный параметр. Использование std::vector<int> позволяет обойти это ограничение. Например, можно изменить определение нашей putValues() на такое:

#include <iostream>
#include <vector>

const lineLength = 12; // количество элементов в строке
void putValues(std::vector<int> vec) {
    std::cout << "(" << vec.size() << ")< ";
    for (int i = 0; i < vec.size(); ++1) {
        if (i % lineLength == 0 && i)
            std::cout << "\n\t"; // строка заполнена

    std::cout << vec[i];

    // разделитель, печатаемый после каждого элемента,
    // кроме последнего
    if (1 % lineLength != lineLength-1 && i != vec.size()-1)
            std::cout << ", ";
    }
    std::cout << " >\n";
}

Функция main(), вызывающая нашу новую функцию putValues(), выглядит так:

void putValues(std::vector<int>);
int main() {
    int i, j[2];
    // присвоить i и j некоторые значения
    std::vector<int> vec1(1); // создадим вектор из 1 элемента
    vecl[0] = i;
    putValues(vecl);

    std::vector<int> vec2;    // создадим пустой вектор
    // добавим элементы к vec2
    for (int ix = 0; ix < sizeof(j) / sizeof(j[0]); ++ix)
      // vec2[ix] == j [ix]
      vec2.push_back(j[ix]);
    putValues(vec2);

    return 0;
}

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

Как бы вы изменили объявление putValues()?

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

void putValues(const std::vector<int>&) { ... }

7.3.5. Значения параметров по умолчанию

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

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

char *screenInit(int height = 24, int width = 80, char background = ' ');

Функция, для которой задано значение параметра по умолчанию, может вызываться по-разному. Если аргумент опущен, используется значение по умолчанию, в противном случае – значение переданного аргумента. Все следующие вызовы screenInit() корректны:

char *cursor;
// эквивалентно screenInit(24,80,' ')
cursor = screenInit();

// эквивалентно screenInit(66,80,' ')
cursor = screenlnit(66);

// эквивалентно screenInit(66,256,' ')
cursor = screenlnit(66, 256);
cursor = screenlnit(66, 256, '#');

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

background, не задавая его для height и width.

// эквивалентно screenInit('?',80,' ')
cursor = screenInit('?');

// ошибка, неэквивалентно screenInit(24,80,'?')
cursor = screenInit(, ,'?');

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

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

// ошибка: width должна иметь значение по умолчанию,
// если такое значение имеет height
char *screenlnit(int height = 24, int width,
char background = ' ');

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

// tf.h
int ff(int = 0);

// ft.С
#include "ff.h"
int ff(int i = 0) { ... } // ошибка

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

Можно объявить функцию повторно и таким образом задать дополнительные параметры по умолчанию. Это удобно при настройке универсальной функции для конкретного приложения. Скажем, в системной библиотеке UNIX есть функция chmod(), изменяющая режим доступа к файлу. Ее объявление содержится в системном заголовочном файле <cstdlib>:

int chmod(char *filePath, int protMode);

protMode представляет собой режим доступа, а filePath – имя и каталог файла. Если в некотором приложении файл только читается, можно переобъявить функцию chmod(), задав для соответствующего параметра значение по умолчанию, чтобы не указывать его при каждом вызове:

#include <cstdlib>
int chmod(char *filePath, int protMode=0444);

Если функция объявлена в заголовочном файле так:

file int ff(int a, int b, int с = 0); // ff.h

то как переобъявить ее, чтобы присвоить значение по умолчанию для параметра b? Следующая строка ошибочна, поскольку она повторно задает значение для с:

#include "ff.h"
int ff(int a, int b = 0, int с = 0); // ошибка

Так выглядит правильное объявление:

#include "ff.h"
int ff(int a, int b = 0, int с); // правильно

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

#include "ff.h"
int ff(int a, int b = 0, int с); // правильно
int ff(int a = 0, int b, int с); // правильно

Значение по умолчанию не обязано быть константным выражением, можно использовать любое:

int aDefault();
int bDefault(int);
int cDefault(double = 7.8);
int glob;

int ff(int a = aDefault() ,
     int b = bDefau1t(glob) ,
     int с = cDefault());

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

7.3.6. Многоточие

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

void foo(parm_list, ...);
void foo(...);

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

Примером вынужденного использования многоточия служит функция printf() стандартной библиотеки С. Ее первый параметр является C-строкой:

int printf(const char* ...);

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

printf("hello, world\n");

имеет один строковый аргумент. Но

printf("hello, %s\n", userName);

имеет два аргумента. Символ % говорит о наличии второго аргумента, а буква s, следующая за ним, определяет его тип – в данном случае символьную строку.

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

Отметим, что следующие объявления неэквивалентны:

void f();
void f(...);

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

f(someValue);
f(cnt, a, b, с);

корректны только для второго объявления. Вызов

f();

применим к любой из двух функций.

Упражнение 7.4

Какие из следующих объявлений содержат ошибки? Объясните.

(a) void print(int arr[][], int size);

(b) int ff(int a, int b = 0, int с = 0);

(c) void operate(int *matrix[]);

(d) char *screenInit(int height = 24, int width,

char background);

(e) void putValues(int (&ia)[]);

Упражнение 7.5

Повторные объявления всех приведенных ниже функций содержат ошибки. Найдите их.

(a) char *screenInit(int height, int width,

char background = ' ');

char *screenInit(int height = 24, int width,

char background);

(b) void print(int (*arr)[6], int size);

void print(int (*arr)[5], int size);

(c) void manip(int *pi, int first, int end = 0);

void manip(int *pi, int first = 0, int end = 0);

Упражнение 7.6

Даны объявления функций.

void print(int arr[][5], int size);

void operate(int *matrix[7]);

char *screenInit(int height = 24, int width = 80,

char background = ' ');

Вызовы этих функций содержат ошибки. Найдите их и объясните.

(a) screenInit();

(b) int *matrix[5];

operate(matrix);

(c) int arr[5][5];

print(arr, 5);

Упражнение 7.7

Перепишите функцию putValues(std::vector<int>), приведенную в подразделе 7.3.4, так, чтобы она работала с контейнером std::list<std::string>. Печатайте по одному значению на строке. Вот пример вывода для списка из двух строк:

(2)

<

"first string"

"second string"

>

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

"put function declarations in header files"
"use abstract container types instead of built-in arrays"
"declare class parameters as references"
"use reference to const types for invariant parameters"
"use less than eight parameters"

Упражнение 7.8

В каком случае вы применили бы параметр-указатель? А в каком – параметр-ссылку? Опишите достоинства и недостатки каждого способа.

§ 1.4. Возврат значения

В теле функции может встретиться инструкция return. Она завершает выполнение функции. После этого управление возвращается той функции, из которой была вызвана данная. Инструкция return может употребляться в двух формах:

return;
return expression;

Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return напоминает инструкцию break) После конечной инструкции функции подразумевается наличие return. Например:

void d_copy(double *src, double *dst, int sz) {
    /* копируем массив "src" в "dst"
    * для простоты предполагаем, что они одного размера
    */

    // завершение, если хотя бы один из указателей равен 0
    if (!src || !dst)
        return;

    // завершение,
    // если указатели адресуют один и тот же массив
    if (src == dst)
        return;

    // копировать нечего
    if (sz == 0)
        return;

    // все еще не закончили?
    // тогда самое время что-то сделать
    for (int ix = 0; ix < sz; ++ix)
        dst[ix] = src[ix];

    // явного завершения не требуется
}

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

return val * factorial(val-1);

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

// определение интерфейса класса Matrix
#include "Matrix.h"

bool is_equa1(const Matrix& ml, const Matrix& m2) {
    /* Если содержимое двух объектов Matrix одинаково,
    *  возвращаем true;
    *  в противном случае - false
    */

    // сравним количество столбцов
    if (ml.colSize() != m2.co1Size())
        // ошибка: нет возвращаемого значения
        return;

    // сравним количество строк
    if (ml.rowSize() != m2.rowSize())
        // ошибка: нет возвращаемого значения
        return;

    // пробежимся по обеим матрицам, пока
    // не найдем неравные элементы
    for (int row = 0; row < ml.rowSize(); ++row)
        for (int col = 0; co1 < ml.colSize(); ++co1)
           if (ml[row][col] != m2[row][col])
               return false;
    // ошибка: нет возвращаемого значения
    // для случая равенства
}

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

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

Matrix grow(Matrix* p) {
   Matrix val;
   // ...
   return val;
}

grow() возвращает вызывающей функции копию значения, хранящегося в переменной val.

Такое поведение можно изменить, если объявить, что возвращается указатель или ссылка. При возврате ссылки вызывающая функция получает l-значение для val и потому может модифицировать val или взять ее адрес. Вот как можно объявить, что grow() возвращает ссылку:

Matrix& grow(Matrix* p) {
   Matrix *res;
   // выделим память для объекта Matrix
   // большого размера
   // res адресует этот новый объект
   // скопируем содержимое *p в *res
   return *res;
}

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

Объявляя функцию как возвращающую ссылку, программист должен помнить о двух возможных ошибках:

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

// ошибка: возврат ссылки на локальный объект
Matrix& add(Matrix& m1, Matrix& m2) {
   Matrix result:
   if (m1.isZero())
     return m2;
   if (m2.isZero())
     return m1;
// сложим содержимое двух матриц
// ошибка: ссылка на сомнительную область памяти
// после возврата
   return result;
}

В таком случае тип возврата не должен быть ссылкой. Тогда локальная переменная может быть скопирована до окончания времени своей жизни:

Matrix add(...)

функция возвращает l-значение. Любая его модификация затрагивает сам объект. Например:

#include <vector>
int& get_val(std::vector<int> &vi, int ix) {
  return vi [ix];
  }
  // ...
  int ai[4] = { 0, 1, 2, 3 };
  std::vector<int> vec(ai, ai+4); // копируем 4 элемента ai в vec

  int main() {
  // увеличивает vec[0] на 1
  get_val(vec.0)++;
  // ...
  }

Для предотвращения нечаянной модификации возвращенного объекта нужно объявить тип возврата как const:

const int &get_val(...)

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

7.4.1. Передача данных через параметры и через глобальные объекты

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

Глобальный объект определен вне функции. Например:

int glob;
int main() {
   // что угодно
}

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

Можно сделать вывод, что для передачи информации между функциями предпочтительнее пользоваться параметрами и возвращаемыми значениями.

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

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

Упражнение 7.9

Каковы две формы инструкции return? Объясните, в каких случаях следует использовать первую, а в каких вторую форму.

Упражнение 7.10

Найдите в данной функции потенциальную ошибку времени выполнения:

std::vector<std::string> &readText() {
  std::vector<std::string> text;

  std::string word;
  while (std::cin >> word) {
    text.push_back(word);
    // ...
  }
  // ....
  return text;
}

Упражнение 7.11

Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода

§ 1.5. Рекурсия

Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например:

int rgcd(int vl, int v2) {
   if (v2 != 0)
     return rgcd(v2, vl%v2);
   return vl;
}

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

Вызов

rgcd(15, 123);

возвращает 3 (см. табл. 7.1).

Table 1.1: Трассировка вызова rgcd (15,123)
v1 v2 return
15 123 rgcd(123,15)
123 15 rgcd(15,3)
15 3 rgcd(3,0)
3 0 3

Последний вызов,

rgcd(3,0);

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

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

Приведем пример. Факториалом числа n является произведение натуральных чисел от 1 до n. Так, факториал 5 равен 120: 1 ? 2 ? 3 ? 4 ? 5 = 120.

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

unsigned long
factorial(int val) {
   if (val &gt; 1)
     return val * factorial(val-1);
   return 1;
}

Рекурсия обрывается по достижении val значения 1.

Упражнение 7.12

Перепишите factorial() как итеративную функцию.

Упражнение 7.13

Что произойдет, если условием окончания factorial() будет следующее:

if (val != 0)

§ 1.6. Встроенные функции

Рассмотрим следующую функцию min():

int min(int vl, int v2) {
   return(vl < v2 ? vl : v2);
}

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

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

int minVa12 = min(i, j);

заменяется при компиляции на

int minVal2 = i < j ? i : j;

Таким образом, не требуется тратить время на реализацию min() в виде функции.

Функция min() объявляется как встроенная с помощью ключевого слова inline перед типом возвращаемого значения в объявлении или определении:

inline int min(int vl, int v2) { /* ... */ }

Заметим, однако, что спецификация inline – это только подсказка компилятору. Компилятор может проигнорировать ее, если функция плохо подходит для встраивания по месту. Например, рекурсивная функция (такая, как rgcd()) не может быть полностью встроена в месте вызова (хотя для самого первого вызова это возможно). Функция из 1200 строк также скорее всего не подойдет. В общем случае такой механизм предназначен для оптимизации небольших, простых, часто используемых функций. Он крайне важен для поддержки концепции сокрытия информации при разработке абстрактных типов данных. Например, встроенной объявлена функция-член size() в классе IntArray.

Встроенная функция должна быть видна компилятору в месте вызова. В отличие от обычной, такая функция определяется в каждом исходном файле, где есть обращения к ней. Конечно же, определения одной и той же встроенной функции в разных файлах должны совпадать. Если программа содержит два исходных файла compute.cpp и draw.cpp, не нужно писать для них разные реализации функции min(). Если определения функции различаются, программа становится нестабильной: неизвестно, какое из них будет выбрано для каждого вызова, если компилятор не стал встраивать эту функцию.

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

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

§ 1.7. Директива связывания extern "C"

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

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

// директива связывания в форме простой инструкции
extern "C" void exit(int);
// директива связывания в форме составной инструкции
extern "C" {
  int printf(const char* ...);
  int scanf(const char* ...);
}
// директива связывания в форме составной инструкции
extern "C" {
  #include <cmath>
}

Первая форма такой директивы состоит из ключевого слова extern, за которым следует строковый литерал, а за ним – обычное объявление функции. Хотя функция написана на другом языке, проверка типов вызова выполняется полностью. Несколько объявлений функций могут быть помещены в фигурные скобки составной инструкции директивы связывания – второй формы этой директивы. Скобки отмечают те объявления, к которым она относится, не ограничивая их видимости, как в случае обычной составной инструкции. Составная инструкция extern "C" в предыдущем примере говорит только о том, что функции printf() и scanf() написаны на языке С. Во всех остальных отношениях эти объявления работают точно так же, как если бы они были расположены вне инструкции.

Если в фигурные скобки составной директивы связывания помещается директива препроцессора #include, все объявленные во включаемом заголовочном файле функции рассматриваются как написанные на языке, указанном в этой директиве. В предыдущем примере все функции из заголовочного файла cmath написаны на языке С.

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

int main() {
    // ошибка: директива связывания не может появиться
    // внутри тела функции
    extern "C" double sqrt(double);
    double getValue(); //правильно

    double result = sqrt (getValue());
    //...
    return 0;
}

Если мы переместим директиву так, чтобы она оказалась вне тела main(), программа откомпилируется правильно:

extern "C" double sqrt(double);
int main() {
   double getValue(); //правильно
   double result = sqrt (getValue());

  //...
  return 0;
}

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

Как сделать С++ функцию доступной для программы на С? Директива extern "C" поможет и в этом:

// функция calc() может быть вызвана из программы на C
extern "C" double calc(double dparm) { /* ... */ }

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

// ---- myMath.h ----
extern "C" double calc(double);
// ---- myMath.cpp ----
// объявление calc() в myMath.h
#include "myMath.h"
// определение функции extern "C" calc()
// функция calc() может быть вызвана из программы на C
double calc(double dparm) { // ... }

В данном разделе мы видели примеры директивы связывания extern "C" только для языка С. Это единственный внешний язык, поддержку которого гарантирует стандарт С++. Конкретная реализация может поддерживать связь и с другими языками. Например, extern "Ada" для функций, написанных на языке Ada; extern "FORTRAN" для языка FORTRAN и т.д. Мы описали один из случаев использования ключевого слова extern в С++. Это слово имеет и другое назначение в объявлениях функций и объектов.

Упражнение 7.14

exit(), printf(), malloc(), strcpy() и strlen() являются функциями из библиотеки С. Модифицируйте приведенную ниже С-программу так, чтобы она компилировалась и связывалась в С++.

  const char *str = "hello";
  void *malloc(int);
  char *strcpy(char *, const char *);
  int printf(const char *, ...);
  int exit(int);
  int strlen(const char *);
/* программа на языке С */
int main() {
  char* s = malloc(strlen(str)+l);
  strcpy(s, str);
  printf("%s, world\n", s);
  exit(0);
  }

§ 1.8. Функция main(): разбор параметров командной строки

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

prog -d -o of lie dataO

Фактические параметры являются аргументами функции main() и могут быть получены из массива C-строк с именем argv; мы покажем, как их использовать.

Во всех предыдущих примерах определение main() содержало пустой список:

int main() { ... }

Развернутая сигнатура main() позволяет получить доступ к параметрам, которые были заданы пользователем в командной строке:

int main(int argc, char *argv[]){...}

argc содержит их количество, а argv – C-строки, представляющие собой отдельные значения (в командной строке они разделяются пробелами). Скажем, при запуске команды

prog -d -o ofile data0

argc получает значение 5, а argv включает следующие строки:

argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "dataO";

В argv[0] всегда входит имя команды (программы). Элементы с индексами от 1 до argc-1 служат параметрами.

Посмотрим, как можно извлечь и использовать значения, помещенные в argv. Пусть программа из нашего примера вызывается таким образом:

prog [-d] [-h] [-v]
[-o output_file] [-l limit_value]
file_name
[file_name [file_name [...]]]

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

prog chap1.doc

Но можно запускать и так:

prog -l 1024 -o chap1-2.out chapl.doc chap2.doc
prog d chap3.doc
prog -l 512 -d chap4.doc

При разборе параметров командной строки выполняются следующие основные шаги:

  1. По очереди извлечь каждый параметр из argv. Мы используем для этого цикл for с начальным индексом 1 (пропуская, таким образом, имя программы):
    for (int ix = 1; ix < argc; ++ix) {
        char *pchar = argv[ix];
        // ...
    }
    
  2. Определить тип параметра. Если строка начинается с дефиса (-), это одна из опций { h, d, v, l, o}. В противном случае это может быть либо значение, ассоциированное с опцией (максимальный размер для -l, имя выходного файла для -o), либо имя входного файла. Чтобы определить, начинается ли строка с дефиса, используем инструкцию switch:
    switch (pchar[0]) {
         case '-': {
           // -h, -d, -v, -l, -o
         }
        default: {
          // обработаем максимальный размер для опции -1
          // имя выходного файла для -o
          // имена входных файлов ...
        }
    }

Реализуем обработку двух случаев пункта 2.

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

case '-': {
  switch(pchar[1])
  {
    case 'd':
      // обработка опции debug
      break;

    case 'v':
      // обработка опции version
      break;

    case 'h':
      // обработка опции help
      break;

    case 'o':
      // приготовимся обработать выходной файл
      break;

    case 'l':
      // приготовимся обработать макс.размер
      break;

    default:
      // неопознанная опция:
      // сообщить об ошибке и завершить выполнение
  }
}

Опция -d задает необходимость отладки. Ее обработка заключается в присваивании переменной с объявлением

bool debug_on = false;

значения true:

case 'd':
   debug_on = true;
   break;

В нашу программу может входить код следующего вида:

if (debug_on)
   display_state_elements(obj);

Опция -v выводит номер версии программы и завершает исполнение:

case 'v':
   std::cout << program_name << "::" << program_version << std::endl;
   return 0;

Опция -h запрашивает информацию о синтаксисе запуска и завершает исполнение. Вывод сообщения и выход из программы выполняется функцией usage():

case 'h':
   // break не нужен: usage() вызывает exit()
   usage();

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

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

// если ofi1e_on==true,
// следующий параметр - имя выходного файла
bool ofi1e_on = false;
// если ofi1e_on==true,
// следующий параметр - максимальный размер
bool limit_on = false;

Вот обработка опций -l и -o в нашей инструкции switch:

case 'l':
   limit_on = true;
   break;
case 'o':
  ofile_on = true;
  break;

Встретив строку, не начинающуюся с дефиса, мы с помощью переменных состояния можем узнать ее содержание:

// обработаем максимальный размер для опции -1
//            имя выходного файла для       -o
//            имена входных файлов ...
default: {
   // ofile_on включена, если -o встречалась
   if (ofile_on) {
     // обработаем имя выходного файла
     // выключим ofile_on
   }
   else if (limit_on) { // если -l встречалась
     // обработаем максимальный размер
     // выключим limit_on
   } else {
     // обработаем имя входного файла
   }
}

Если аргумент является именем выходного файла, сохраним это имя и выключим ofile_on:

if (ofile_on) {
   ofile_on = false;
   ofile = pchar;
}

Если аргумент задает максимальный размер, мы должны преобразовать строку встроенного типа в представляемое ею число. Сделаем это с помощью стандартной функции atoi(), которая принимает строку в качестве аргумента и возвращает int (также существует функция atof(), возвращающая double). Для использования atoi() включим заголовочный файл ctype.h. Нужно проверить, что значение максимального размера неотрицательно и выключить limit_on:

// int limit;
else if (limit_on) {
  limit_on = false;
  limit = atoi(pchar);
  if (limit < 0) {
    std::cerr << program_name << "::"
         << program_version << " : error: "
         << "negative value for limit.\n\n";
    usage(-2);
  }
}

Если обе переменных состояния равны false, у нас есть имя входного файла. Сохраним его в векторе строк:

else

file_names.push_back(std::string(pchar));

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

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

prog - d dataOl

prog -oout_file dataOl

(Оба случая мы оставим для упражнений в конце раздела.)

Вот полный текст нашей программы. (Мы добавили инструкции печати для трассировки выполнения.)

#include <iostream>
#include <string>
#include <vector>
#include <cctype>

char const program_name[] = "comline";
char const program_version[] = "version 0.01 (08/07/97)";

inline void usage(int exit_value = 0) {
  // печатает отформатированное сообщение о порядке вызова
  // и завершает программу с кодом exit_value ...

  std::cerr << "порядок вызова:\n"
       << program_name << " "
       << "[-d] [-h] [-v] \n\t"
       << "[-o output_file] [-l limit] \n\t"
       << "file_name\n\t[file_name [file_name [...]]]\n\n"
       << "где [] указывает на необязательность опции:\n\n\t"
       << "-h: справка.\n\t\t"

       << "печать этого сообщения и выход\n\n\t"
       << "-v: версия.\n\t\t"
       << "печать информации о версии программы и выход\n\n\t"
       << "-d: отладка.\n\t\t включает отладочную печать\n\n\t"
       << "-l limit\n\t\t"
       << "limit должен быть неотрицательным целым числом\n\n\t"
       << "-o ofile\n\t\t"
       << "файл, в который выводится результат\n\t\t"
       << "по умолчанию результат записывается на стандартный вывод\n\n"
       << "file_name\n\t\t"
       << "имя подлежащего обработке файла\n\t\t"
       << "должно быть задано хотя бы одно имя --\n\t\t"
       << "но максимальное число не ограничено\n\n"
       << "примеры:\n\t\t"
       << "$command chapter7.doc\n\t\t"
       << "$command -d -l 1024 -o test_7_8 "
       << "chapter7.doc chapter8.doc\n\n";

  exit(exit_value);
}

int main(int argc, char* argv[]) {
  bool debug_on = false;
  bool ofile_on = false;
  bool limit_on = false;
  int limit = -1;
  std::string ofile;
  std::vector<std::string> file_names;

  std::cout << "демонстрация обработки параметров в командной строке:\n" << "argc: " << argc << std::endl;

  for (int ix = 1; ix < argc; ++ix) {
    std::cout << "argv[" << ix << "]: "
         << argv[ix] << std::endl;

    char *pchar = argv[ix];
    switch (pchar[0]) {
      case '-': {
        std::cout << "встретился \'-\'\n";
        switch(pchar[1]) {
          case 'd':
            std::cout << "встретилась -d: отладочная печать включена\n";

            debug_on = true;
            break;

          case 'v':
            std::cout << "встретилась -v: выводится информация о версии\n";

            std::cout << program_name
                 << " ::  "
                 << program_version
                 << std::endl;

            return 0;

          case 'h':
            std::cout << "встретилась -h: справка\n";

            // break не нужен: usage() завершает программу
            usage();

          case 'o':
            std::cout << "встретилась -o: выходной файл\n";
            ofile_on = true;
            break;
          case 'l':
            std::cout << "встретилась -l: ограничение ресурса\n";

            limit_on = true;
            break;

          default:
            std::cerr << program_name << " : ошибка : неопознанная опция: - " << pchar << "\n\n";

            // break не нужен: usage() завершает программу
            usage(-1);
        }
        break;
      }
      default: // либо имя файла
        std::cout << "default: параметр без дефиса: " << pchar << std::endl;

        if (ofile_on) {
          ofile_on = false;
          ofile = pchar;
        } else if (limit_on) {
          limit_on = false;
          limit = atoi(pchar);
          if (limit < 0) {
            std::cerr << program_name << " : ошибка : отрицательное значение limit.\n\n";

            usage(-2);
          }
        }
        else file_names.push_back(std::string(pchar));
        break;
    }
  }

  if (file_names.empty()) {
    std::cerr << program_name << " : ошибка : не задан ни один входной файл.\n\n";
    usage(-3);
  }

  if (limit != -1)
    std::cout << "Заданное пользователем значение limit: " << limit << std::endl;

  if (! ofile.empty())
    std::cout << "Заданный пользователем выходной файл: " << ofile << std::endl;

  std::cout << (file_names.size() == 1 ? "Файл, " : "Файлы, ")
       << "подлежащий(е) обработке:\n";


  for (int inx = 0; inx < file_names.size(); ++inx)
    std::cout << "\t" << file_names[inx] << std::endl;
}
// a.out -d -l 1024 -o test_7_8 chapter7.doc chapters.doc

Вот трассировка обработки параметров командной строки:

демонстрация обработки параметров в командной строке:
argc: 8
argv[1]: -d
встретился '-'
встретилась -d: отладочная печать включена
argv[2]: -l
встретился '-'
встретилась -l: ограничение ресурса
argv[3]: 1024
default: параметр без дефиса: 1024
argv[4]: -o
встретился '-'
встретилась -o: выходной файл
argv[5]: test_7_8
default: параметр без дефиса: test_7_8
argv[6]: chapter7.doc
default: параметр без дефиса: chapter7.doc
argv[7]: chapter8.doc
default: параметр без дефиса: chapter8.doc
Заданное пользователем значение limit: 1024
Заданный пользователем выходной файл: test_7_8
Файлы, подлежащий(е) обработке:
chapter7.doc
chapter8.doc

7.8.1. Класс для обработки параметров командной строки

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

extern int parse_options(int arg_count, char *arg_vector);

int main(int argc, char *argv[]) {
  // ...
  int option_status;
  option_status = parse_options(argc, argv);
  // ...
}

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

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

#include <vector>
#include <string>

class CommandOpt {
public:
    CommandOpt() : _limit(-1), _debug_on(false) {}
    int parse_options(int argc, char *argv[]);

    std::string out_file() { return _out_file; }
    bool   debug_on() { return _debug_on; }
    int    files()    { return _file_names.size(); }

    std::string& operator[](int ix);

private:
    inline void usage(int exit_value = 0);

    bool _debug_on;
    int _limit;
    std::string _out_file;
    std::vector<std::string> _file_names;

    static const char *const program_name;
    static const char *const program_version;
};

Так выглядит модифицированная функция main():

#include "CommandOpt.h"

int main(int argc, char "argv[]) {
  // ...
  CommandOpt com_opt;
  int option_status;
  opttion_status = com_opt. parse_options (argc, argv);
  // ...
}

Упражнение 7.15

Добавьте обработку опций -t (включение таймера) и -b (задание размера буфера bufsize). Не забудьте обновить usage(). Например:

prog -t -b 512 dataO

Упражнение 7.16

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

Упражнение 7.17

Наша реализация не может различить лишний пробел между дефисом и опцией:

prog - d dataO

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

Упражнение 7.18

В нашей программе не предусмотрен случай, когда опции -l или -o задаются несколько раз. Реализуйте такую возможность. Какова должна быть стратегия при разрешении конфликта?

Упражнение 7.19

В нашей реализации задание неизвестной опции приводит к фатальной ошибке. Как вы думаете, это оправдано? Предложите другое поведение.

Упражнение 7.20

Добавьте поддержку опций, начинающихся со знака плюс (+), обеспечив обработку +s и +pt, а также +sp и +ps. Предположим, что +s включает строгую проверку синтаксиса, а +p допускает использование устаревших конструкций. Например:

prog +s +p -d -b 1024 dataO

§ 1.9. Указатели на функции

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

sort(start, end, compare);

где start и end являются указателями на элементы массива строк. Функция sort() сортирует элементы между start и end, а аргумент compare задает операцию сравнения двух строк этого массива.

Какую реализацию выбрать для compare? Мы можем сортировать строки лексикографически, т.е. в том порядке, в котором слова располагаются в словаре, или по длине – более короткие идут раньше более длинных. Нам нужен механизм для задания альтернативных операций сравнения. (Заметим, что в главе 12 описан алгоритм sort() и другие обобщенные алгоритмы из стандартной библиотеки С++. В этом разделе мы покажем свою собственную версию sort() как пример употребления указателей на функции. Наша функция будет упрощенным вариантом стандартного алгоритма.)

Один из способов удовлетворить наши потребности – использовать в качестве третьего аргумента compare указатель на функцию, применяемую для сравнения.

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

7.9.1. Тип указателя на функцию

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

#include <string>
int lexicoCompare(const std::string &sl, const std::string &s2) {
   return sl.compare(s2);
}

Если все символы строк s1 и s2 равны, lexicoCompare() вернет 0, в противном случае – отрицательное число, если s1 меньше чем s2, и положительное, если s1 больше s2.

Имя функции не входит в ее сигнатуру – она определяется только типом возвращаемого значения и списком параметров. Указатель на lexicoCompare() должен адресовать функцию с той же сигнатурой. Попробуем написать так:

int *pf(const std::string &, const std::string &);
// нет, не совсем так

Эта инструкция почти правильна. Проблема в том, что компилятор интерпретирует ее как объявление функции с именем pf, которая возвращает указатель типа int*. Список параметров правилен, но тип возвращаемого значения не тот. Оператор разыменования (*) ассоциируется с данным типом (int в нашем случае), а не с pf. Чтобы исправить положение, нужно использовать скобки:

int (*pf)(const std::string &, const std::string &);
// правильно

pf объявлен как указатель на функцию с двумя параметрами, возвращающую значение типа int, т.е. такую, как lexicoCompare().

pf способен адресовать и приведенную ниже функцию, поскольку ее сигнатура совпадает с типом lexicoCompare():

int sizeCompare(const std::string &sl, const std::string &s2);

Функции calc() и gcd()другого типа, поэтому pf не может указывать на них:

int calc(int , int);
int gcd(int , int);

Указатель, который адресует эти две функции, определяется так:

int (*pfi)(int, int);

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

int printf(const char*, ...);
int strlen(const char*);

int (*pfce)(const char*, ...); // может указывать на printf()
int (*pfc)(const char*); // может указывать на strlen()

Типов функций столько, сколько комбинаций типов возвращаемых значений и списков параметров.

7.9.2. Инициализация и присваивание

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

lexicoCompare;

получается указатель типа

int (*)(const std::string&, const std::string&);

Применение оператора взятия адреса к имени функции также дает указатель того же типа, например lexicoCompare и &lexicoCompare. Указатель на функцию инициализируется следующим образом:

int (*pfi)(const std::string &, const std::string &) = lexicoCompare;
int (*pfi2)(const std::string &, const std::string &) = &lexicoCompare;

Ему можно присвоить значение:

pfi = lexicoCompare;
pfi2 = pfi;

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

int calc(int, int);
int (*pfi2s)(const std::string&, const std::string &) = 0;
int (*pfi2i)(int, int) = 0;
int main() {
   pfi2i = calc; // правильно
   pri2s = calc; // ошибка: несовпадение типов
   pfi2s = pfi2i; // ошибка: несовпадение типов
   return 0;
}

Такой указатель можно инициализировать нулем или присвоить ему нулевое значение, в этом случае он не адресует функцию.

7.9.3. Вызов

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

#include <iostream>

int min(int*, int);
int (*pf)(int*, int) = min;

const int iaSize = 5;
int ia[iaSize] = { 7, 4, 9, 2, 5 };

int main() {
    std::cout << "Прямой вызов: min: " << min(ia, iaSize) << std::endl;

    std::cout << "Косвенный вызов: min: " << pf(ia, iaSize) << std::endl;

    return 0;
}

int min(int* ia, int sz) {
    int minVal = ia[0];
    for (int ix = 1; ix < sz; ++ix)
        if (minVal &gt; ia[ix])
            minVal = ia[ix];
    return minVal;
}

Вызов

pf(ia, iaSize);

может быть записан также и с использованием явного синтаксиса указателя:

(*pf)(ia, iaSize);

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

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

7.9.4. Массивы указателей на функции

Можно объявить массив указателей на функции. Например:

int (*testCases[10])();

testCases – это массив из десяти элементов, каждый из которых является указателем на функцию, возвращающую значение типа int и не имеющую параметров.

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

В этом случае помогает использование имен, определенных с помощью директивы typedef:

// typedef делает объявление более понятным
typedef int (*PFV)(); // typedef для указателя на функцию 
PFV testCases[10];

Данное объявление эквивалентно предыдущему.

Вызов функций, адресуемых элементами массива testCases, выглядит следующим образом:

const int size = 10;
PFV testCases[size];
int testResults[size];

void runtests() {
    for (int i = 0; i < size; ++i)
        // вызов через элемент массива
        testResults[i] = testCases[i]();
}

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

int lexicoCompare(const std::string &, const std::string &);
int sizeCompare(const std::string &, const std::string &);

typedef int (*PFI2S)(const std::string &, const std::string &);
PFI2S compareFuncs[2] = {
    lexicoCompare,
    sizeCompare
  };

Можно объявить и указатель на compareFuncs, его типом будет указатель на массив указателей на функции:

PFI2S (*pfCompare)[2] = compareFuncs;

Это объявление раскладывается на составные части следующим образом:

(*pfCompare)

Оператор разыменования говорит, что pfCompare является указателем. [2] сообщает о количестве элементов массива:

(*pfCompare) [2]

PFI2S – имя, определенное с помощью директивы typedef, называет тип элементов. Это указатель на функцию, возвращающую int и имеющую два параметра типа const std::string &. Тип элемента массива тот же, что и выражения &lexicoCompare.

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

compareFunc[0];
(*pfCompare)[0];

Чтобы вызвать функцию lexicoCompare через pfCompare, нужно написать одну из следующих инструкций:

// эквивалентные вызовы
pfCompare [0](string1, string2); // сокращенная форма
((*pfCompare)[0])(string1, string2); // явная форма

7.9.5. Параметры и тип возврата

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

int sort(std::string*, std::string*,
int (*)(const std::string&, const std::string&));

И в этом случае директива typedef помогает сделать объявление sort() более понятным:

// Использование директивы typedef делает
// объявление sort() более понятным
typedef int (*PFI2S)(const std::string &, const std::string &);
int sort(std::string*, std::string*, PFI2S);

Поскольку в большинстве случаев употребляется функция lexicoCompare, можно использовать значение параметра по умолчанию:

// значение по умолчанию для третьего параметра
int lexicoCompare(const std::string&, const std::string&);
int sort(std::string*, std::string*, PFI2S = lexicoCompare);

Определение sort() выглядит следующим образом:

void sort(std::string *sl, std::string *s2, PFI2S compare = lexicoCompare) {
    // условие окончания рекурсии
    if (si < s2) {
        std::string elem = *s1;
        std::string *1ow = s1;
        std::string *high = s2 + 1;

        for (;;) {
            while (compare (*++1ow, elem) < 0 && low < s2);
            while (compare(elem, *--high) < 0 && high > s1)
            if (low < high)
                low->swap(*high);
            else break;
        }

        s1->swap(*high);
        sort(s1, high - 1);
        sort(high +1, s2);
    } // end, if (si < s2)
}

sort() реализует алгоритм быстрой сортировки Хоара (C.A.R.Hoare). Рассмотрим ее определение детально. Она сортирует элементы массива от s1 до s2. Это рекурсивная функция, которая вызывает сама себя для последовательно уменьшающихся подмассивов. Рекурсия окончится тогда, когда s1 и s2 укажут на один и тот же элемент или s1 будет располагаться после s2 (строка 5).

elem (строка 6) является разделяющим элементом. Все элементы, меньшие чем elem, перемещаются влево от него, а большие – вправо. Теперь массив разбит на две части. sort() рекурсивно вызывается для каждой из них (строки 20-21).

Цикл for(;;) проводит разделение (строки 10-17). На каждой итерации цикла индекс low увеличивается до первого элемента, большего или равного elem (строка 11). Аналогично high уменьшается до последнего элемента, меньшего или равного elem (строка 12). Когда low становится равным или большим high, мы выходим из цикла, в противном случае нужно поменять местами значения элементов и начать новую итерацию (строки 14-16). Хотя элементы разделены, elem все еще остается первым в массиве. swap() в строке 19 ставит его на место до рекурсивного вызова sort() для двух частей массива.

Сравнение производится вызовом функции, на которую указывает compare (строки 11-12). Чтобы поменять элементы массива местами, используется операция swap() с аргументами типа std::string.

Вот как выглядит main(), в которой применяется наша функция сортировки:

#include <iostream>
#include <string>

// это должно бы находиться в заголовочном файле
int lexicoCompare(const std::string&, const std::string&);
int sizeCompare(const std::string&, const std::string&);
typedef int (*PFI)(const std::string&, const std::string&);
void sort(std::string *, std::string *, PFI=lexicoCompare);

std::string as[10] = { "a", "light", "drizzle", "was", "falling",
                  "when", "they", "left", "the", "museum" };

int main() {
    // вызов sort() с значением по умолчанию параметра compare
    sort(as, as + sizeof(as) / sizeof(as[0]) - 1);

   // выводим результат сортировки
   for (int i = 0; i < sizeof(as) / sizeof(as[0]); ++i)
       std::cout << as[i].c_str() << "\n\t";
}

Результат работы программы:

"a"
"drizzle"
"falling"
"left"
"light"
"museum"
"the"
"they"
"was"
"when"

Параметр функции автоматически приводится к типу указателя на функцию:

// typedef представляет собой тип функции
typedef int functype(const std::string &, const std::string &);
void sort(std::string *, std::string *, functype);

sort() рассматривается компилятором как объявленная в виде

void sort(std::string *, std::string *, int (*)(const std::string&, const std::string&));

Два этих объявления sort() эквивалентны.

Заметим, что, помимо использования в качестве параметра, указатель на функцию может быть еще и типом возвращаемого значения. Например:

int (*ff(int))(int*, int);
// ff() объявляется как функция, имеющая один параметр типа int
// и возвращающая указатель на функцию типа int (*)(int*, int);

И здесь использование директивы typedef делает объявление понятнее. Объявив PF с помощью typedef, мы видим, что ff() возвращает указатель на функцию:

// Использование директивы typedef делает объявления более понятными
typedef int (*PF)(int*, int);
PF ff(int);

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

// typedef представляет собой тип функции
typedef int func(int*, int);

func ff(int); // ошибка: тип возврата ff() - функция

7.9.6. Указатели на функции, объявленные как extern "C"

Можно объявлять указатели на функции, написанные на других языках программирования. Это делается с помощью директивы связывания. Например, указатель pf ссылается на С-функцию:

extern "C" void (*pf)(int);
Через pf вызывается функция, написанная на языке С.
extern "C" void exit(int);
// pf ссылается на C-функцию exit()
extern "C" void (*pf)(int) = exit;
int main() {
  // ...
  // вызов С-функции, а именно exit()
  (*pf)(99);
}

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

void (*pfl)(int);
extern "C" void (*pf2)(int);
int main() {
   pfl = pf2; // ошибка: pfl и pf2 имеют разные типы
   // ...
}

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

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

В следующем примере параметр pfParm также служит указателем на С-функцию. Директива связывания применяется к объявлению функции, к которой этот параметр относится:

// pfParm - указатель на С-функцию
extern "C" void f1(void(*pfParm)(int));

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

Коль скоро директива связывания относится ко всем функциям в объявлении, то как же объявить функцию С++, имеющую в качестве параметра указатель на С-функцию? С помощью директивы typedef. Например:

// FC представляет собой тип:
// С-функция с параметром типа int, не возвращающая никакого значения
extern "C" typedef void FC(int);

// f2() - C++ функция с параметром -
// указателем на С-функцию
void f2(FC *pfParm);

Упражнение 7.21

В разделе 7.5 приводится определение функции factorial(). Напишите объявление указателя на нее. Вызовите функцию через этот указатель для вычисления факториала 11.

Упражнение 7.22

Каковы типы следующих объявлений:

(a) int (*mpf)(std::vector<int>&);
(b) void (*apf[20])(doub1e);
(c) void (*(*papf)[2])(int);

Как сделать эти объявления более понятными, используя директивы typedef?

Упражнение 7.23

Вот функции из библиотеки С, определенные в заголовочном файле <cmath>:

double abs(double);
double sin(double);
double cos(double);
double sqrt(double);

Как бы вы объявили массив указателей на С-функции и инициализировали его этими четырьмя функциями? Напишите main(), которая вызывает sqrt() с аргументом 97.9 через элемент массива.

Упражнение 7.24

Вернемся к примеру sort(). Напишите определение функции

int sizeCompare(const std::string&, const std::string&);

Если передаваемые в качестве параметров строки имеют одинаковую длину, то sizeCompare() возвращает 0; если первая строка короче второй, то отрицательное число, а если длиннее, то положительное. Напоминаем, что длина строки возвращается операцией size() класса std::string. Измените main() для вызова sort(), передав в качестве третьего аргумента указатель на sizeCompare().