Когда вызывается перегруженная f(), компилятор должен понять, к какой из функций с именем f следует обратиться. Это делается путем сравнения типов фактических параметров с типами формальных параметров всех функций с именем f. Поиск функции, которую надо вызвать, осуществляется за три отдельных шага:
Искать функцию соответствующую точно, и использовать ее, если она найдена;
Искать соответствующую функцию используя встроенные преобразования и использовать любую найденную функцию; и
Искать соответствующую функцию используя преобразования, определенные пользователем, и если множество преобразований единственно, использовать найденную функцию.
Например:
overload print(double), print(int);
void f();
{
print(1);
print(1.0);
}
Правило точного соответствия гарантирует, что f напечатает 1 как целое и 1.0 как число с плавающей точкой. Ноль, char или short точно соответствуют параметру int. Аналогично, float точно соответствует double.
К параметрам функций с перегруженными именами стандартные C++ правила преобразования применяются не полностью. Преобразования, могущие уничтожить информацию, не выполняются. Остаются int в long, int в double, ноль в long, ноль в double и преобразования указателей: ноль в указатель, ноль в void*, и указатель на производный класс в указатель на базовый класс .
Вот пример, в котором преобразование необходимо:
overload print(double), print(long);
void f(int a);
{
print(a);
}
Здесь a может быть напечатано или как double, или как long. Неоднозначность разрешается явным преобразованием типа (или print(long(a)) или print(double(a))).
При этих правилах можно гарантировать, что когда эффективность или точность вычислений для используемых типов существенно различаются, будет использоваться простейший алгоритм (функция).
Например:
overload pow;
int pow(int, int);
double pow(double, double); // из
complex pow(double, complex); // из
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);
Процесспоискаподходящейфункцииигнорирует unsigned и const.
Незаданное Число Параметров
Для некоторых функций невозможно задать число и тип всех параметров, которые можно ожидать в вызове. Такую функцию описывают завершая список описаний параметров многоточием (...), что означает "и может быть, еще какие-то параметры".
Например:
int printf(char* ...);
Это задает, что в вызове printf должен быть по меньшей мере один параметр, char*, а остальные могут быть, а могут и не быть.
Например:
printf("Hello, world\n");
printf("Моеимя %s %s\n", first_name, second_name);
printf("%d + %d = %d\n",2,3,5);
Такая функция полагается на информацию, которая недоступна компилятору при интерпретации ее списка параметров. В случае printf() первым параметром является строка формата, содержащая специальные последовательности символов, позволяющие printf() правильно обрабатывать остальные параметры. %s означает "жди параметра char*", а %d означает "жди параметра int". Однако, компилятор этого не знает, поэтому он не может убедиться в том, что ожидаемые параметры имеют соответствующий тип.
Например:
printf("Мое имя %s %s\n",2);
откомпилируется и в лучшем случае приведет к какой-нибудь странного вида выдаче.
Очевидно, если параметр не был описан, то у компилятора нет информации, необходимой для выполнения над ним проверки типа и преобразования типа. В этом случае char или short передаются как int, а float передается как double. Это не обязательно то, чего ждет пользователь.
Чрезмерное использование многоточий, вроде wild(...), полностью выключает проверку типов параметров, оставляя программиста открытым перед множеством неприятностей, которые хорошо знакомы программистам на C. В хорошо продуманной программе требуется самое большее несколько функций, для которых типы параметров не определены полностью. Для того, чтобы позаботиться о проверке типов, можно использовать перегруженные функции и функции с параметрами по умолчанию в большинстве тех случаев, когда иначе пришлось бы оставить типы параметров незаданными. Многоточие необходимо только если изменяются и число параметров, и тип параметров. Наиболее обычное применение многоточия в задании интерфейса с функциями C библиотек, которые были определены в то время, когда альтернативы не было:
extern int fprintf(FILE*, char* ...); // из
extern int execl(char* ...); // из
extern int abort(...); // из
Разберем случай написания функции ошибок, которая получает один целый параметр, указывающий серьезность ошибки, после которого идет произвольное число строк. Идея состоит в том, чтобы составлять сообщение об ошибке с помощью передачи каждого слова как отдельного строкового параметра:
void error(int ...);
main(int argc, char* argv[])
{
switch(argc) {
case 1:
error(0,argv[0],0);
break;
case 2:
error(0,argv[0],argv[1],0);
default:
error(1,argv[0],"с",dec(argc-1),"параметрами",0);
}
}
Функцию ошибок можно определить так:
#include
void error(int n ...)
/*
"n" с последующим списком char*, оканчивающихся нулем
*/
{
va_list ap;
va_start(ap,n); // раскрутка arg
for (;;) {
char* p = va_arg(ap,char*);
if(p == 0) break;
cerr << p << " ";
}
va_end(ap); // очистка arg
cerr << "\n";
if (n) exit(n);
}
Первый из va_list определяется и инициализируется вызовом va_start(). Макрос va_start получает имя va_list"а и имя последнего формального параметра как параметры. Макрос va_arg используется для выбора неименованных параметров по порядку. При каждом обращении программист должен задать тип; va_arg() предполагает, что был передан фактический параметр, но обычно способа убедиться в этом нет. Перед возвратом из функции, в которой был использован va_start(), должен быть вызван va_end(). Причина в том, что va_start() может изменить стек так, что нельзя будет успешно осуществить возврат; va_end() аннулирует все эти изменения.
Указатель на Функцию
С функцией можно делать только две вещи: вызывать ее и брать ее адрес. Указатель, полученный взятием адреса функции, можно затем использовать для вызова этой функции.
Например:
void error(char* p) { /* ... */ }
void (*efct)(char*); // указатель на функцию
void f()
{
efct = &error; // efct указывает на error
(*efct)("error"); // вызов error через efct
}
Чтобы вызвать функцию через указатель, например, efct, надо сначала этот указатель разыменовать, *efct. Поскольку операция вызова функции () имеет более высокий приоритет, чем операция разыменования *, то нельзя писать просто *efct("error"). Это означает *efct("error"), а это ошибка в типе. То же относится и к синтаксису описаний .
Заметьте, что у указателей на функции типы параметров описываются точно также, как и в самих функциях. В присваиваниях указателя должно соблюдаться точное соответствие полного типа функции.
Например:
void (*pf)(char*); // указатель на void(char*)
void f1(char*); // void(char*)
int f2(char*); // int(char*)
void f3(int*); // void(int*)
void f()
{
pf = &f1; // ok
pf = &f2; // ошибка: не подходит возвращаемый тип
pf = &f3; // ошибка: не подходит тип параметра
(*pf)("asdf"); // ok
(*pf)(1); // ошибка: не подходит тип параметра
int i = (*pf)("qwer"); // ошибка: void присваивается int"у
}
Правила передачи параметров для непосредственных вызовов функции и для вызовов функции через указатель одни и те же.
Часто, чтобы избежать использования какого-либо неочевидного синтаксиса, бывает удобно определить имя типа указатель-на-функцию.
Например:
typedef int (*SIG_TYP)(); // из
typedef void (*SIG_ARG_TYP);
SIG_TYP signal(int,SIG_ARG_TYP);
Бывает часто полезен вектор указателей на функцию. Например, система меню для моего редактора с мышью*4 реализована с помощью векторов указателей на функции для представления действий. Подробно эту систему здесь описать не получится, но вот общая идея:
typedef void (*PF)();
PF edit_ops[] = { // операции редактирования
cut, paste, snarf, search
};
PF file_ops[] = { // управлениефайлом
open, reshape, close, write
};
Затем определяем и инициализируем указатели, определяющие действия, выбранные в меню, которое связано с кнопками (button) мыши:
PF* button2 = edit_ops;
PF* button3 = file_ops;
В полной реализации для определения каждого пункта меню требуется больше информации. Например, где-то должна храниться строка, задающая текст, который высвечивается. При использовании системы значение кнопок мыши часто меняется в зависимости от ситуации. Эти изменения осуществляются (частично) посредством смены значений указателей кнопок. Когда пользователь выбирает пункт меню, например пункт 3 для кнопки 2, выполняется связанное с ним действие:
(button2[3])();
Один из способов оценить огромную мощь указателей на функции - это попробовать написать такую систему не используя их. Меню можно менять в ходе использования программы, внося новые функции в таблицу действий. Во время выполнения можно также легко сконструировать новое меню.
Указатели на функции можно использовать для задания полиморфных подпрограмм, то есть подпрограмм, которые могут применяться к объектам многих различных типов:
typedef int (*CFT)(char*,char*);
int sort(char* base, unsigned n, int sz, CFT cmp)
/*
Сортирует "n" элементов вектора "base"
в возрастающем порядке
с помощью функции сравнения, указываемой "cmp".
Размер элементов "sz".
Очень неэффективный алгоритм: пузырьковая сортировка
*/
{
for (int i=0; iname, Puser(q)->name);
}
int cmp2(char*p, char* q) // Сравниваетчисла dept
{
return Puser(p)->dept-Puser(q)->dept;
}
Эта программа сортирует и печатает:
main ()
{
sort((char*)heads,6,sizeof(user),cmp1);
print_id(heads,6); // в алфавитном порядке
cout << "\n";
sort((char*)heads,6,sizeof(user),cmp2);
print_id(heads,6); // по порядку подразделений
}
Можно взять адрес inline-функции, как, впрочем, и адрес перегруженной функции.