
Функции
используются для наведения порядка в хаосе алгоритмов.
Б. Страуструп
Функция - это именованная часть программы, к которой можно обращаться из других частей программы столько раз, сколько потребуется. Рассмотрим программу, печатающую степени числа 2:
extern float pow(float,int); //pow() определена в другом месте
main() {
for (int i=0; i<10
; i++)cout << pow( 2,i )<<”\n”; }
Первая строка функции - описание, указывающее, что pow - функция, получающая параметры типа float и int и возвращающая float. Описание функции используется для того, чтобы сделать определенными обращения к функции в других местах. При вызове тип каждого параметра функции сопоставляется с ожидаемым типом точно так же, как если бы инициализировалась переменная описанного типа. Это гарантирует надлежащую проверку и преобразование типов. Например, обращение pow(12.3,"abcd") вызовет недовольство компилятора, поскольку "abcd" является строкой, а не int. При вызове pow(2,i) компилятор преобразует 2 к типу float, как того требует функция. Функция pow может быть определена например так:
float pow(float x, int n)
{
if (n <0) error("извините, отрицательный показатель для pow()"); switch (n) { case 0: return 1; case 1: return x; default: return x*pow(x,n-1); } }
Первая часть определения функции задает имя функции, тип возвращаемого ею значения (если таковое имеется) и типы и имена ее параметров (если они есть). Значение возвращается из функции с помощью оператора return. Разные функции обычно имеют разные имена, но функциям, выполняющим сходные действия над объектами различных типов, иногда лучше дать возможность иметь одинаковые имена. Если типы их параметров различны, то компилятор всегда может различить их и выбрать для вызова нужную функцию. Может, например, иметься одна функция возведения в степень для целых переменных и другая для переменных с плавающей точкой:
overload pow;int pow(int, int);double pow(double, double);//...x=pow(2,10);y=pow(2.0,10.0); Описание
overload pow; сообщает компилятору, что использование имени pow более чем для
одной функции является умышленным.
Если функция не возвращает значения, то ее следует описать как void:
void swap(int* p, int* q) // поменять местами{
int t = *p; *p = *q; *q = t;}
void - Ключевое слово C++, используемое для объявления чего-либо, не обладающего типом. Имеет специальные значения, например, объявляет функцию с пустым списком параметров.
void*- Указатель на тип void. Как правило, в Си и С++ используется при приведении указателей, как тип низшего уровня.
éЕсли функция не должна возвращать значение ,указывается тип void.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Обычный способ сделать что-либо в C++ программе - это вызвать функцию, которая это делает. Определение функции является способом задать то, как должно делаться некоторое действие. Функция не может быть вызвана, пока она не описана.
Описание
функции задает имя функции, тип возвращаемого функцией значения (если таковое есть)
и число и типы параметров, которые должны быть в вызове функции. Например:
extern double sqrt(double); extern elem* next_elem(); extern char* strcpy(char* to, const char* from); extern void exit(int);
Семантика передачи параметров идентична семантике инициализации. Проверяются
типы параметров, и когда нужно производится неявное преобразование типа.
Например, если были заданы предыдущие определения, то
double sr2 = sqrt(2);
Каждая функция, вызываемая в программе, должна быть где-то определена (только один раз). Определение функции - это описание функции, в котором приводится тело функции. Например:
extern void swap(int*, int*); // описание
void swap(int*, int*) // определение{
int t = *p; *p =*q; *q = t;}
Чтобы избежать расходов на вызов функции, функцию можно описать как inline ,
а чтобы обеспечить более быстрый доступ к параметрам, их можно описать как
register . Оба средства могут использоваться неправильно, и их следует избегать
везде где есть какие-либо сомнения в их полезности.
Когда вызывается функция, дополнительно выделяется память под ее
формальные параметры, и каждый формальный параметр инициализируется
соответствующим ему фактическим параметром. Семантика передачи параметров
идентична семантике инициализации. В частности, тип фактического параметра
сопоставляется с типом формального параметра, и выполняются все стандартные и
определенные пользователем преобразования типов. Есть особые правила для
передачи векторов , средство передавать параметр без проверки и средство для задания параметров по
умолчанию . Рассмотрим
void f(int val, int& ref){
val++; ref++; }
Когда вызывается f(), val++ увеличивает локальную копию первого фактического
параметра, тогда как ref++ увеличивает второй фактический параметр. Например:
int i = 1;int j = 1;
f(i,j);
увеличивает j, но не i. Первый параметр, i, передается по значению,
второй параметр, j, передается по ссылке. Использование функций, которые
изменяют переданные по ссылке параметры, могут сделать программу трудно
читаемой, и их следует избегать. Однако передача большого объекта по ссылке
может быть гораздо эффективнее, чем передача его по значению. В этом случае
параметр можно описать как const, чтобы указать, что ссылка применяется по
соображениям эффективности, а также чтобы не позволить вызываемой функции
изменять значение объекта:
void f(const large& arg)
{
// значение "arg" не может быть изменено }
Аналогично, описание параметра указателя как const сообщает читателю,
что значение объекта, указываемого указателем, функцией не изменяется. Например:
extern int strlen(const char*); // из
extern char* strcpy(char* to, const char* from); extern int strcmp(const char*, const char*);
Важность такой практики растет с размером программы.
Заметьте, что семантика передачи параметров отлична от семантики присваивания.
Это важно для const параметров, ссылочных параметров и параметров некоторых
типов, определяемых пользователем .
Как
правило, давать разным функциям разные имена - мысль хорошая, но когда
некоторые функции выполняют одинаковую работу над объектами разных типов, может
быть более удобно дать им одно и то же имя. Использование одного имени для
различных действий над различными типами называется перегрузкой (overloading).
Метод уже используется для основных операций C++: у сложения существует только
одно имя, +, но его можно применять для сложения значений целых, плавающих и
указательных типов. Эта идея легко расширяется на обработку операций,
определенных пользователем, то есть, функций. Чтобы уберечь программиста от
случайного повторного использования имени, имя может использоваться более чем
для одной функции только если оно сперва описано как перегруженное. Например:
overload print; void print(int); void print(char*); Что касается компилятора, единственное общее, что имеют функции содинаковым именем, это имя. Предположительно, они в каком-то смысле
похожи, но в этом язык ни стесняет программиста, ни помогает ему.
Таким образом, перегруженные имена функций - это главным образом
удобство записи. Это удобство значительно в случае функций с
общепринятыми именами вроде sqrt, print и open. Когда имя
семантически значимо, как это имеет место для операций вроде +, *
и << и в случае конструкторов , это
удобство становится существенным. Когда вызывается перегруженная
f(), компилятор должен понять, к какой из функций с именем f
следует обратиться. Это делается путем сравнения типов фактическихпараметров с типами формальных параметров всех функций с именем f.Поиск функции, которую надо вызвать, осуществляется за три
отдельных шага:
[1] Искать функцию соответствующую точно, и использовать ее, если она найдена;[2] Искать соответствующую функцию используя встроенные
преобразования и использовать любую найденную функцию; и[3] Искать соответствующую функцию используя преобразования,
определенные пользователем , и если множество
преобразований единственно, использовать найденную функцию.Например:
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.С
функцией можно делать только две вещи: вызывать ее и брать ее адрес. Указатель,
полученный взятием адреса функции, можно затем использовать для вызова этой
функции. Например:
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 // okpf = &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-функции, как, впрочем, и адрес перегруженной функции.