Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdf
С++ для начинающих |
362 |
int calc( int, int );
int (*pfi2s)( const string &, const 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() {
cout << "Прямой вызов: min: "
<< min( ia, iaSize ) << endl;
cout << "Косвенный вызов: min: " << pf( ia, iaSize ) << endl;
return 0;
}
int min( int* ia, int sz ) { int minVal = ia[ 0 ];
for ( int ix = 1; ix < sz; ++ix ) if ( minVal > ia[ ix ] )
minVal = ia[ ix ]; return minVal;
косвенный вызов по указателю записываются одинаково:
}
Вызов
pf( ia, iaSize );
может быть записан также и с использованием явного синтаксиса указателя:
(*pf)( ia, iaSize );
С++ для начинающих |
363 |
Результат в обоих случаях одинаковый, но вторая форма говорит читателю, что вызов осуществляется через указатель на функцию.
Конечно, если такой указатель имеет нулевое значение, то любая форма вызова приведет к ошибке во время выполнения. Использовать можно только те указатели, которые адресуют какую-либо функцию или были проинициализированы таким значением.
7.9.4. Массивы указателей на функции
Можно объявить массив указателей на функции. Например:
int (*testCases[10])();
testCases – это массив из десяти элементов, каждый из которых является указателем на функцию, возвращающую значение типа int и не имеющую параметров.
Подобные объявления трудно читать, поскольку не сразу видно, с какой частью ассоциируется тип функции.
В этом случае помогает использование имен, определенных с помощью директивы
// typedef делает объявление более понятным
typedef int (*PFV)(); // typedef для указателя на функцию
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 ]();
образом:
}
Массив указателей на функции может быть инициализирован списком, каждый элемент которого является функцией. Например:
С++ для начинающих |
364 |
int lexicoCompare( const string &, const string & ); int sizeCompare( const string &, const string & );
typedef int ( *PFI2S )( const string &, const string & ); PFI2S compareFuncs[2] =
{
lexicoCompare, sizeCompare
};
Можно объявить и указатель на compareFuncs, его типом будет “указатель на массив указателей на функции”:
PFI2S (*pfCompare)[2] = compareFuncs;
Это объявление раскладывается на составные части следующим образом:
(*pfCompare)
Оператор разыменования говорит, что pfCompare является указателем. [2] сообщает о количестве элементов массива:
(*pfCompare) [2]
PFI2S – имя, определенное с помощью директивы typedef, называет тип элементов. Это “указатель на функцию, возвращающую int и имеющую два параметра типа const string &”. Тип элемента массива тот же, что и выражения &lexicoCompare.
Такой тип имеет и первый элемент массива compareFuncs, который может быть получен
compareFunc[ 0 ];
с помощью любого из выражений:
(*pfCompare)[ 0 ];
Чтобы вызвать функцию lexicoCompare через pfCompare, нужно написать одну из
// эквивалентные |
вызовы |
// сокращенная форма |
pfCompare [ 0 ]( |
string1, string2 ); |
следующих инструкций:
((*pfCompare)[ 0 ])( string1, string2 ); // явная форма
7.9.5. Параметры и тип возврата
Вернемся к задаче, сформулированной в начале данного раздела. Как использовать указатели на функции для сортировки элементов? Мы можем передать в алгоритм сортировки указатель на функцию, которая выполняет сравнение:
С++ для начинающих |
365 |
int sort( string*, string*,
int (*)( const string &, const string & ) );
И в этом случае директива typedef помогает сделать объявление sort() более
//Использование директивы typedef делает
//объявление sort() более понятным
typedef int ( *PFI2S )( const string &, const string & );
понятным:
int sort( string*, string*, PFI2S );
Поскольку в большинстве случаев употребляется функция lexicoCompare, можно
// значение по умолчанию для третьего параметра
int lexicoCompare( const string &, const string & );
использовать значение параметра по умолчанию:
int sort( string*, string*, PFI2S = lexicoCompare );
1 void sort( string *sl, string *s2,
2PFI2S compare = lexicoCompare )
3{
4// условие окончания рекурсии
5if ( si < s2 ) {
6string elem = *s1;
7string *1ow = s1;
8string *high = s2 + 1;
9
10for (;;) {
11while ( compare ( *++1ow, elem ) < 0 && low < s2) ;
12while ( compare( elem, *--high ) < 0 && high > s1)
14if ( low < high )
151ow->swap(*high);
16else break;
17} // end, for(;;)
18
19s1->swap(*high);
20sort( s1, high - 1 );
21sort( high +1, s2 );
22} // end, if ( si < s2 )
Определение sort() выглядит следующим образом:
23 }
sort() реализует алгоритм быстрой сортировки Хоара (C.A.R.Hoare). Рассмотрим ее определение детально. Она сортирует элементы массива от s1 до s2. Это рекурсивная функция, которая вызывает сама себя для последовательно уменьшающихся подмассивов. Рекурсия окончится тогда, когда s1 и s2 укажут на один и тот же элемент или s1 будет располагаться после s2 (строка 5).
С++ для начинающих |
366 |
elem (строка 6) является разделяющим элементом. Все элементы, меньшие чем elem, перемещаются влево от него, а большие – вправо. Теперь массив разбит на две части. sort() рекурсивно вызывается для каждой из них (строки 20-21).
Цикл for(;;) проводит разделение (строки 10-17). На каждой итерации цикла индекс low увеличивается до первого элемента, большего или равного elem (строка 11). Аналогично high уменьшается до последнего элемента, меньшего или равного elem (строка 12). Когда low становится равным или большим high, мы выходим из цикла, в
противном случае нужно поменять местами значения элементов и начать новую итерацию (строки 14-16). Хотя элементы разделены, elem все еще остается первым в массиве. swap() в строке 19 ставит его на место до рекурсивного вызова sort() для двух частей массива.
Сравнение производится вызовом функции, на которую указывает compare (строки 1112). Чтобы поменять элементы массива местами, используется операция swap() с аргументами типа string, представленная в разделе 6.11.
#include <iostream> #include <string>
// это должно бы находиться в заголовочном файле int lexicoCompare( const string &, const string & ); int sizeCompare( const string &, const string & );
typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI=lexicoCompare );
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 ) cout << as[ i ].c_str() << "\n\t";
Вот как выглядит main(), в которой применяется наша функция сортировки:
}
Результат работы программы:
"a"
"drizzle"
"falling"
"left"
"light"
"museum"
"the"
"they"
"was"
"when"
Параметр функции автоматически приводится к типу указателя на функцию:
С++ для начинающих |
367 |
|
|
// typedef представляет собой тип функции |
|
|
|
|
|
typedef int functype( const string &, const string & ); |
|
|
void sort( string *, string *, functype ); |
|
|
|
|
|
void sort( string *, string *, |
|
|
|
|
|
|
|
sort() рассматривается компилятором как объявленная в виде |
|
|
|
int (*)( const string &, const string & ) ); |
|
|
|
|
Два этих объявления sort() эквивалентны. |
|
|
Заметим, что, помимо использования в качестве параметра, указатель на функцию может |
|
|
быть еще и типом возвращаемого значения. Например: |
|
|
|
int (*ff( int ))( int*, int ); |
|
|
|
|
|
|
|
ff() объявляется как функция, имеющая один параметр типа int и возвращающая |
|
|
указатель на функцию типа |
|
|
|
int (*)( int*, int ); |
|
|
|
|
Издесь использование директивы typedef делает объявление понятнее. Объявив PF с
//Использование директивы typedef делает
//объявления более понятными
typedef int (*PF)( int*, int );
помощью typedef, мы видим, что ff() возвращает указатель на функцию:
PF ff( int );
Типом возвращаемого значения функции не может быть тип функции. В этом случае
// typedef представляет собой тип функции typedef int func( int*, int );
выдается ошибка компиляции. Например, нельзя объявить ff() таким образом: func ff( int ); // ошибка: тип возврата ff() - функция
7.9.6. Указатели на функции, объявленные как extern "C"
Можно объявлять указатели на функции, написанные на других языках программирования. Это делается с помощью директивы связывания. Например, указатель pf ссылается на С-функцию:
extern "C" void (*pf)(int);
С++ для начинающих |
368 |
extern "C" void exit(int);
// pf ссылается на C-функцию exit() extern "C" void (*pf)(int) = exit; int main() {
//...
//вызов С-функции, а именно exit() (*pf)(99);
Через pf вызывается функция, написанная на языке С.
}
Вспомним, что присваивание и инициализация указателя на функцию возможны лишь тогда, когда тип в левой части оператора присваивания в точности соответствует типу в правой его части. Следовательно, указатель на С-функцию не может адресовать функцию С++ (и инициализация его таким адресом не допускается), и наоборот. Подобная попытка
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. Например:
С++ для начинающих |
369 |
//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)(vector<int>&);
(b)void (*apf[20])(doub1e);
Каковы типы следующих объявлений:
(c) void (*(*papf)[2])(int);
Как сделать эти объявления более понятными, используя директивы typedef?
Упражнение 7.23
double abs(double); double sin(double); double cos(double);
Вот функции из библиотеки С, определенные в заголовочном файле <cmath>: double sqrt(double);
Как бы вы объявили массив указателей на С-функции и инициализировали его этими четырьмя функциями? Напишите main(), которая вызывает sqrt() с аргументом 97.9 через элемент массива.
Упражнение 7.24
Вернемся к примеру sort(). Напишите определение функции
int sizeCompare( const string &, const string & );
Если передаваемые в качестве параметров строки имеют одинаковую длину, то sizeCompare() возвращает 0; если первая строка короче второй, то отрицательное число, а если длиннее, то положительное. Напоминаем, что длина строки возвращается операцией size() класса string. Измените main() для вызова sort(), передав в качестве третьего аргумента указатель на sizeCompare().
С++ для начинающих |
370 |
8.Область видимости и время жизни
Вэтой главе обсуждаются два важных вопроса, касающиеся объявлений в С++. Где употребляется объявленное имя? Когда можно безопасно использовать объект или вызывать функцию, т.е. каково время жизни сущности в программе? Для ответа на первый вопрос мы введем понятие областей видимости и покажем, как они ограничивают применение имен в исходном файле программы. Мы рассмотрим разные типы таких областей: глобальную и локальную, а также более сложное понятие областей видимости пространств имен, которое появится в конце главы. Отвечая на второй вопрос, мы опишем, как объявления вводят глобальные объекты и функции (сущности, “живущие” в течение всего времени работы программы), локальные (“живущие” на определенном отрезке выполнения) и динамически размещаемые объекты (временем жизни которых управляет программист). Мы также исследуем свойства времени выполнения, характерные для этих объектов и функций.
8.1.Область видимости
Каждое имя в С++ программе должно относиться к уникальной сущности (объекту, функции, типу или шаблону). Это не значит, что оно встречается только один раз во всей программе: его можно повторно использовать для обозначения другой сущности, если только есть некоторый контекст, помогающий различить разные значения одного и того же имени. Контекстом, служащим для такого различения, служит область видимости. В С++ поддерживается три их типа: локальная область видимости, область видимости
пространства имен и область видимости класса.
Локальная область – это часть определении функции (или в блоке). каждая составная инструкция (или отдельную локальную область.
исходного текста программы, содержащаяся в Любая функция имеет собственную такую часть, и блок) внутри функции также представляет собой
Область видимости пространства имен – часть исходного текста программы, не содержащаяся внутри объявления или определения функции или определения класса.
Самая внешняя часть называется глобальной областью видимости или глобальной областью видимости пространства имен.
Объекты, функции, типы и шаблоны могут быть определены в глобальной области видимости. Программисту разрешено задать пользовательские пространства имен, заключенные внутри глобальной области с помощью определения пространства имен. Каждое такое пространство является отдельной областью видимости. Пользовательское пространство, как и глобальное, может содержать объявления и определения объектов, функций, типов и шаблонов, а также вложенные пользовательские пространства имен. (Они рассматриваются в разделах 8.5 и 8.6.)
Каждое определение класса представляет собой отдельную область видимости класса. (О таких областях мы расскажем в главе 13.)
Имя может обозначать различные сущности в зависимости от области видимости. В следующем фрагменте программы имя s1 относится к четырем разным сущностям:
С++ для начинающих |
371 |
||
|
|
#include <iostream> |
|
|
|
|
|
|
|
#include <string> |
|
|
|
// сравниваем s1 и s2 лексикографически |
|
|
|
int lexicoCompare( const string &sl, const string &s2 ) { ... } |
|
|
|
// сравниваем длины s1 и s2 |
|
|
|
int sizeCompare( const string &sl, const string &s2 ) { ... } |
|
|
|
typedef int ( PFI)( const string &, const string & ); |
|
|
|
// сортируем массив строк |
|
|
|
void sort( string *s1, string *s2, PFI compare =lexicoCompare ) |
|
|
|
{ ... } |
|
|
|
string sl[10] = { "a", "light", "drizzle", "was", "falling", |
|
|
|
"when", "they", "left", "the", "school" }; |
|
|
|
int main() |
|
|
|
{ |
|
|
|
// вызов sort() со значением по умолчанию параметра compare |
|
|
|
// s1 - глобальный массив |
|
|
|
sort( s1, s1 + sizeof(s1)/sizeof(s1[0]) - 1 ); |
|
|
|
// выводим результат сортировки |
|
|
|
for ( int i = 0; i < sizeof(s1) / sizeof(s1[0]); ++i ) |
|
|
|
cout << s1[ i ].c_str() << "\n\t"; |
|
|
|
} |
|
|
|
||
|
|
|
|
Поскольку определения функций lexicoCompare(), sizeCompare() и sort()
представляют собой различные области видимости и все они отличны от глобальной, в каждой из этих областей можно завести переменную с именем s1.
Имя, введенное с помощью объявления, можно использовать от точки объявления до конца области видимости (включая вложенные области). Так, имя s1 параметра функции lexicoCompare() разрешается употреблять до конца ее области видимости, то есть до конца ее определения.
Имя глобального массива s1 видимо с точки его объявления до конца исходного файла, включая вложенные области, такие, как определение функции main().
В общем случае имя должно обозначать одну сущность внутри одной области видимости. Если в предыдущем примере после объявления массива s1 добавить следующую строку, компилятор выдаст сообщение об ошибке:
void s1(); // ошибка: повторное объявление s1
Перегруженные функции являются исключением из правила: можно завести несколько одноименных функций в одной области видимости, если они отличаются списком параметров. (Перегруженные функции рассматриваются в главе 9.)
ВС++ имя должно быть объявлено до момента его первого использования в выражении.
Впротивном случае компилятор выдаст сообщение об ошибке. Процесс сопоставления имени, используемого в выражении, с его объявлением называется разрешением. С помощью этого процесса имя получает конкретный смысл. Разрешение имени зависит от способа его употребления и от его области видимости. Мы рассмотрим этот процесс в различных контекстах. (В следующем подразделе описывается разрешение имен в
локальной области видимости; в разделе 10.9 – разрешение в шаблонах функций; в конце главы 13 – в области видимости классов, а в разделе 16.12 – в шаблонах классов.)
