1. Тип возврата функции, на которую он указывает.
2. Имя указателя, предваренное звездочкой, говорящей о том, что это – указатель.
3. Типы параметров функции, на которую он указывает.
Если попытаться присвоить адрес функции указателю, который не соответствует типам в его объявлении, компилятор выдаст сообщение об ошибке.
Можно инициализировать указатель на функцию именем функции непосредственно в операторе объявления этого указателя. Ниже приведены примеры.
long sum(long numl, long num2); // Прототип функции
long (*pfun)(long, long) = sum; // Указатель указывает на функцию sum()
В общем случае возможно установить объявленный здесь указатель pfun так, чтобы он указывал на любую функцию, принимающую два аргумента типа long и возвращающую значение типа long. В данном случае он инициализирован адресом функции sum(), которая имеет прототип, представленный в первом операторе.
Конечно, также можно инициализировать указатель на функцию с помощью оператора присваивания. Предполагая, что указатель pfun объявлен, как показано выше, то можно установить значение указателя равным адресу другой функции в следующих операторах:
long product (long, long); // Прототип функции
. . .
pfun = product; // Установить указатель на product()
Как и с указателями на переменные, программист должен обеспечить инициализацию указателей на функции перед тем, как использовать их для вызова функций. Без инициализации такой вызов гарантирует катастрофический сбой вашей программы.
Чтобы прочувствовать использование этих указатели и увидеть их в действии, рассмотрим следующий пример программы.
// Упражнение с указателями на функции
#include <iostream>
using namespace std;
long sum(long a, long b) ; // Прототип функции
long product(long a, long b); // Прототип функции
int main(void)
{
long (*pdo_it)(long, long); //Объявление указателя на функцию
pdo_it = product;
cout <<endl<< "3*5 = " <<pdo_it(3, 5); // Вызов product через указатель
pdo_it = sum; // Переназначение указателя на sum()
cout << endl<< "3*(4 + 5) + 6 = "<<pdo_it (product (3, pdo_it(4, 5)), 6);
//Дважды вызвать через указатель
cout<<endl;
return 0;
}
// Функция для умножения двух значений
long product(long a, long b)
{
return a*b;
}
// Функция для сложения двух значений
long sum(long a, long b)
{
return a + b;
}
Эта программа генерирует следующий вывод (рис. 58):
Рис. 58. Результат работы программы с использованием указателя на функцию
Описание полученных результатов
Это не особенно полезная программа, однако на очень простом примере она показывает, как объявить указатель на функцию, как ему присвоить значение и затем использовать для вызова функции.
После обычной преамбулы объявляется указатель на функцию pdo_it, который может указывать на любую из двух функций, которые были определены – sum() или product(). Указателю присваивается адрес функции product () в следующем операторе:
pdo_it = product;
В качестве присваиваемого значения используется просто имя функции – без скобок и каких-либо других украшений. Имя функции автоматически преобразуется компилятором в ее адрес, который и сохраняется в указателе (рис. 59).
pdo_it ( product ( 3 , pdo_it ( 4 , 5 ) ) , 6) |
эквивалентно |
sum ( 4 , 5 ) |
product ( 3 , 9 ) |
результат в |
pdo_it ( 27 , 6 ) |
sum ( 27 , 6 ) |
результат в |
дает в результате |
33 |
эквивалентно |
Рис. 59. Схема работы программы
Функция product() вызывается неявно, через указатель pdo_it в операторе вывода:
cout <<endl<< "3*5 = " <<pdo_it(3, 5); // Вызов product через указатель
Здесь используется имя указателя, как если бы он был именем функции, с последующими аргументами в скобках – точно так же, как они задавались бы при непосредственном вызове функции по ее имени.
Далее, исключительно для того, чтобы показать, что так можно сделать, указателю присваивается адрес функции sum ():
pdo_it = sum; // Переназначение указателя на sum()
После этого он используется в следующем, хитро закрученном операторе, выполняющем несложную арифметику:
cout << endl<< "3*(4 + 5) + 6 = "<<pdo_it (product (3, pdo_it(4, 5)), 6);
Этот код демонстрирует, что указатель на функцию может использоваться точно таким же образом, как и функция, на которую он указывает.
Поскольку указатель на функцию – вполне законный тип, любая функция может иметь параметр типа указателя на функцию. Такая функция затем может вызывать функцию, указатель на которую ей передан в аргументе. Поскольку указатель может быть переустановлен на другую функцию в других обстоятельствах, это позволяет определять вызывающей программе определять, какая функция должна быть вызвана из данной функции. В этом случае можно передать функцию явно в виде аргумента.
Пример такой функции высчитывает определенный интеграл на отрезке [a,b] за n шагов (рис. 60). Интеграл функции на отрезке [a,b] примерно равен сумме площадей прямоугольников, полученных разбиением площади, полученной между графиком функции и осью 0x на n частей. Площадь каждого прямоугольна равно f(x)*(b-a)/n.
x |
y |
F(a) |
F(b) |
n |
F(x) |
F(xi) |
a |
b |
Рис. 60. Расчет определенного интеграла на отрезке
#include <iostream>
#include <math.h>
using namespace std;
float f1(float x); // функция для интеграла
float f2(float x); // функция для интеграла
float Integral(float a,float b, int n, float (*pfun)(float x)); // функция расчета интеграла
/* a,b —отрезок, n— число шагов, pfun— указатель на
Функцию, интеграл которой будет расчитан*/
float f1(float x)
{
float y=x;
return y;
}
float f2(float x)
{
float y=2*x;
return y;
}
float Integral(float a,float b, int n, float (*pfun)(float x))
{
float h=(b-a)/n; //расчет шага
float x,In=0.f;
for(x=a;x<b;x+=h) // цикл для n шагов
In+=pfun(x)*h; //сумма площадей прямоугольников
return In;
}
int main(void)
{
float a,b,In;
int n;
setlocale(NULL,"Russian");
cout<<"ВВедите левый и правый концы отрезка для определения определенного интеграла\n";
cin>>a>>b;
cout<<"Введите количество шагов N>5\n";
cin>>n;
// расчет интеграла для стандартной функции sin(x)
In=Integral(a,b,n,sin);
cout<<"Интеграл равен "<<In<<endl;
// расчет интеграла для функции f1(x)
In=Integral(a,b,n,f1);
cout<<"Интеграл равен "<<In<<endl;
// расчет интеграла для функции f2(x)
In=Integral(a,b,n,f2);
cout<<"Интеграл равен "<<In<<endl;
return 0;
}
Результат работы программы выгладит так (рис. 61):
Рис. 61. Результат работы программы по расчету определенного интеграла
Со всеми функциями, которые использовались до сих пор, программист должен был позаботиться о передаче аргументов, соответствующих каждому из параметров вызываемой функции. Было бы довольно удобно, если бы при вызове функции можно было пропускать один или более аргументов, имеющих значения по умолчанию, чтобы эти значения передавались автоматически. Этого можно добиться, инициализируя параметры функции в ее прототипе.
О такой функции говорят, что она имеет значения по умолчанию. Пропускать параметры можно строго справа налево, то есть, если функция имеет следующий прототип:
float sum (float a=1.f, float b=2.f, float c=0.f, float d=-1.f);
то пропускать надо последовательно, начиная с d и заканчивая a. Нельзя объяснить функции, что она будет выполняться со значениями по умолчанию для переменных только a и c, а остальные будет ей переданы по значению. пусть sum() считает сумму своих аргументов. Тогда можно проанализировать результат работы функции для следующих вызовов в одном файле исходного кода. Код такой программы будет иметь вид:
#include<iostream>
#include<windows.h>// заголовочный файл для CharToOemA
using namespace std;
char text[80]; //глобальная переменная для работы руссификатора
char* Rus(char* str); // прототип русификатора
float sum (float a=1.f, float b=2.f, float c=0.f, float d=-1.f);
//прототип сумматора
char* Rus(char* str)
{
CharToOemA(str,text);
// функция переводит str из Windows-кодировки в text DOS- кодировки.
return text;// возвращает перекодированную строку
}
float sum (float a, float b, float c, float d) // заголовок сумматора
{
return (a+b+c+d);//
}
int main(void)
{
cout<<Rus("здесь значения параметров соответствуют значения по порядку следования\n a==8, b==10, c==12,d==35.6 их сумма=")<<sum (8, 10, 12, 35.6)<<endl;
cout<<Rus("— a==8, b==10, c==12,значение по умолчанию d==-1 их сумма= ") << sum(8, 10, 12) <<endl;
cout<<Rus("— a==8, b==10, значения по умолчанию c==0, d==-1 их сумма= ") << sum(8, 10) << endl;
cout<<Rus("— a==8, значения по умолчанию b==2, c==0, d==-1 их сумма= ") << sum(8) << endl;
cout<<Rus("—значения по умолчанию a==1, b==2, c==0, d==-1 их сумма= ") << sum() << endl;
return 0;
}
Функция из приведённого примера может быть вызвана со следующим наборам параметров:
sum (8, 10, 12, 35.6); здесь значения параметров соответствуют значения по порядку следования a==8, b==10, c==12,d==35.6
sum (8, 10, 12);— a==8, b==10, c==12,значение по умолчанию d==-1
sum (8, 10);— a==8, b==10, значения по умолчанию c==0, d==-1
sum (8);— a==8, значения по умолчанию b==2, c==0, d==-1
sum (););—значения по умолчанию a==1, b==2, c==0, d==-1
Все эти способы вызова функции абсолютно правильны и законны. Результат работы этих вызовов имеет вид (рис. 62):
Рис. 62. Результат работы программы с функцией sum()
В данном примере используется функция Rus() которая является решением проблемы с использованием в консольном приложении русских букв. Если эту функцию применить к любой символьной константе или переменной, то она будет перекодирована для консольного приложения.
Результат работы данной функции char* – символьная строка, более подробно о строках будет рассказано в главе 7.
В процессе решения какой-либо задачи может возникнуть необходимость или желание написать функции, выполняющие похожие действия с различными типами переменных. Можно использовать уникальные имена для каждой новой функции, а можно воспользоваться специальным механизмом языка С называемым перегрузкой функции.
Перегрузка функций позволяет использовать одно и то же имя для определения нескольких функций – до тех пор, пока они принимают различные списки параметров. При вызове функции, компилятор находит подходящую для конкретного случая версию на основе списка аргументов, который применяется. Очевидно, что компилятор должен всегда быть в состоянии недвусмысленно решить, какая именно функция должна быть выбрана в каждом конкретном случае вызова, поэтому список параметров для каждой из множества перегруженных функций должен быть уникальным. Эти функции разделяют одно общее имя, но перегруженные функции можно различать по наличию параметров разного типа либо по различному количеству параметров. Стоит обратить внимание, что разные типы возврата не могут адекватно различать функции. Причина в том, что такие функции для компилятора неотличимы от функции с прототипом, содержащим такой же список аргументов, но имеющих различные возвращаемые значения. В этом случае компилятор выдаст ошибку и программа не скомпилируется. Пример с функциями sum (), в котором создаются перегруженные функции со следующими прототипами:
#include<iostream>
#include<windows.h>
using namespace std;
//#include "Rus.h"
char text[80];
char* Rus(char* str);
float sum (float a=1.f, float b=2.f, float c=0.f, float d=-1.f);
int sum (int a=1, int b=2, int c=0, int d=-1);
char sum (char a='a', char b='b', char c='c', char d='d');
char* Rus(char* str)
{
CharToOemA(str,text);
return text;
}
float sum (float a, float b, float c, float d)
{
return (a+b+c+d);
}
int sum (int a, int b, int c, int d)
{
return (a*b*c*d);
}
char sum (char a, char b, char c, char d)
{
return abs(a-b-c-d)%256;
}
int main(void)
{
cout<<Rus("здесь значения параметров соответствуют значения по порядку следования\n a==8, b==10, c==12,d==35 их сумма=")<<sum (8, 10, 12, 35)<<endl;
cout<<Rus("— a==8, b==10, c==12,значение по умолчанию d==-1 их произведение= ") << sum (8.f, 10.f, 12.f)<<endl;
cout<<Rus("— a==8, b==10, значения по умолчанию c==0, d==-1 их сумма= ")<<sum (8, 10) <<endl;
cout<<Rus("—значения по умолчанию a==1, b==2, c==0, d==-1 их abs(a-b-c-d)%256= ") <<sum ('z')<<endl;
return 0;
}
Результат работы программы выгладит, как показано на рис. 63.
Рис. 63. Результат работы программы с использованием перегруженных функций
Первый вызов это функция с параметрами int, по алгоритму считается сумма параметров, второй вызов – параметры типа float и считается произведение парметров, последний вызов с параметрами char считается выражение abs(a-b-c-d)%256. Вызов без параметров не возможен так как в этом случае компилятор не может определить какую из перегруженных функций необходимо запускать. Если в перегруженных функциях не определять все значения по умолчанию, а оставить их только в одной, то вызов без параметров снова станет возможет.
Фактически о каждой функции (не только перегруженной) говорят, что она имеет сигнатуру, определяемую ее именем и списком параметров. Все функции в программе должны иметь уникальные сигнатуры, иначе программа не компилируется.
Следует учитывать, что если функция оформляется как имеющая значения по умолчанию, то нельзя её перегружать с различным количеством параметров, совпадающих с количеством параметров возможных при использовании по умолчанию.
Перегрузка функций позволяет имени перегруженной функции не нести информацию о типах используемых параметров. Это сродни тому, как работают базовые операции C++. Чтобы сложить два числа, программист использует одну и ту же операцию, независимо от типа ее операндов. Перегруженные функции имеют одно и то же имя, независимо от типа обрабатываемых данных. Это помогает сделать код более читабельным и облегчает использование функций.
Назначение перегрузки функций понятно: разрешишь выполнять одну и ту же операцию с разными операндами, используя единственное имя функции. Поэтому всякий раз, когда возникает необходимость написать функции, которые, по сути, делают одно и то же, но с разными типами аргументов, необходимо перегрузить их и использовать одно и то же имя.
Перегрузка функций эффективна, если алгоритм работы функции зависит от обрабатываемых параметров. Если же от типов параметров алгоритм не меняется эффективнее использовать другой механизм: шаблоны функций
Чтобы не пришлось дублировать, один и тот же код в каждой функции, но с разными переменными и типами параметров, имеется возможность выписать рецепт, по которому компилятор автоматически сгенерирует похожие функции с различными типами параметров. Код, описывающий такой "рецепт" для генерации определенной группы функций называется шаблоном функции (function template).
Шаблон функции имеет один или более параметров типа, и программист генерирует определенную функцию, подставляя аргумент – конкретный тип для каждого из параметров шаблона. Таким образом, все функции, сгенерированные по шаблону, имеют один и тот же базовый код, но настраиваются типом аргумента, который применяется в вызове такой функции. Как это работает на практике, можно увидеть, определив шаблон функции sum (i) для предыдущего примера. Шаблон для функции sum (i) можно определить следующим образом:
#include<iostream>
#include<windows.h>
using namespace std;
//#include "Rus.h"
char text[80];
char* Rus(char* str);
template<typename T> T sum(T a,T b, T c, T d);
char* Rus(char* str)
{
CharToOemA(str,text);
return text;
}
template<typename T>
T sum(T a,T b, T c, T d)
{
return (a+b+c+d);
}
int main(void)
{
cout<<Rus(" a==8.1, b==10.4, c==12.2,d==35.6 (double) их сумма=")<<sum (8.1, 10.4, 12.2, 35.6)<<endl;
cout<<Rus("— a==8, b==10, c==12,d==-1 (int) их сумма= ")<<sum (8, 10, 12,-1)<<endl;
cout<<Rus("— a=='a', b=='b', c=='c', d=='d' (char) их сумма= ")<<sum ('a','b','c','d' )<<endl;
return 0;
}
Результат работы программы представлен на рис. 64.
Рис. 64. Результат работы программы с использованием шаблона функции
Ключевое слово template указывает на то, что это – определение шаблона. Угловые скобки, следующие за ключевым словом template, заключают в себе параметры типа, используемые для создания определенного экземпляра функции и разделенные запятыми; в данном случае имеется только один параметр типа – Т. Ключевое слово class перед Т указывает на то, что Т – параметр типа для шаблона; здесь class – обобщенный термин, означающий "тип". Шаблоны функций есть объектно-ориентированное расширение для С++, поэтому и необходимо использовать термин class. Если поэкспериментировать и отказаться от использования служебного слова class, то оказывается можно использовать другое ключевое слово typename для идентификации параметров шаблона функции. В этом случае определение шаблона выглядит так:
template<typename T> T sum(T a,T b, T c, T d);
Некоторые программисты предпочитают применять ключевое слово typename, поскольку слово class ассоциируется с типом, определенным пользователем, в то время как typename – более нейтральное, а потому легче воспринимается и подразумевает фундаментальные типы наряду с типами, определяемыми пользователем. На практике оба ключевых слова используются одинаково широко.
Всякий раз, когда Т появляется в определении шаблона функции, оно заменяется специфическим аргументом типа, который применяется, при создании экземпляра функции из шаблона. Создание конкретного экземпляра функции называется реализацией (instantiation).
К сожалению особенность использования шаблонов вынуждает отказаться от возможности реализовать значения по умолчанию.
Контрольные вопросы и задания
1. Что такое функция, для чего используются функции?
2. Какие преимущества дает использование функций при написании программ?
3. Каким образом записывается общая форма заголовка функции? Приведите пример.
4. Для чего используется оператор return? Каковы особенности использования оператора return в случае когда тип возврата функции специфицирован как void?
5. С какими переменными можно работать в функции?
6. Каким образом можно обратиться к глобальной переменной, если имена локальных и глобальных переменных одинаковы?
7. Что такое прототип функции, для чего он нужен?
8. Каким образом можно вернуть из функции несколько значений?
9. Какими способами можно передавать данные в функцию?
10. Какие функции называются рекурсивными? Приведите пример рекурсивной функции.
11. Какие элементы могут выступать в качестве параметров функции?
12. Каким образом происходит объявление указателя на функцию, приведите общую форму объявления указателя на функцию.
13. Из каких компонентов состоит объявление указателя на функцию?
14. Для чего нужна перегрузка функций?
15. Что такое шаблон функции, каково его назначение?
16. Каким образом создаются шаблоны функций? Приведите пример определения шаблона функции.
ГЛАВА 8
РАБОТА С МАССИВАМИ
До этой главы использовались, в основном, так называемые простые типы данных.