Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

hkjCJgcQqF

.pdf
Скачиваний:
2
Добавлен:
15.04.2023
Размер:
810.94 Кб
Скачать

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

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

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

Вприведенном примере для подходящей функции myfunc(int) должно быть применено приведение фактического аргумента типа double к типу

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

Процесс разрешения используется также при вызовах перегруженной функции-члена класса и перегруженного оператора.

Преобразования типов аргументов

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

точное соответствие. Тип фактического аргумента точно соответствует типу формального параметра;

соответствие с преобразованием типа. Тип фактического аргумента не соответствует типу формального параметра, но может быть преобразован в него;

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

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

точное соответствие лучше расширения типа ;

расширение типа лучше стандартного преобразования;

стандартное преобразование лучше преобразования, определенного

пользователем.

Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К

43

аргументу могут быть применены некоторые тривиальные преобразования,

а именно:

 

• преобразование l-значения в r-значение;

 

• преобразование массива в указатель;

 

• преобразование функции в указатель;

 

• преобразования спецификаторов.

 

Категория соответствия с преобразованием типа

включает несколько

видов:

 

повышение (расширение ) типов ;

стандартные преобразования;

определенные пользователем преобразования.

Преобразование с расширением типа имеет ранг преобразований выше чем стандартное преобразование.

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

фактический аргумент типа char, unsigned char или short расширяется до типа int;

фактический аргумент типа unsigned short расширяется до типа int, если машинный размер int больше, чем размер short, и до типа unsigned int в противном случае;

аргумент типа float расширяется до типа double;

аргумент перечислимого типа расширяется до первого из следующих типов, который способен представить все значения элементов перечисления: int, unsigned int, long, unsigned long;

аргумент типа bool расширяется до типа int.

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

#include <cstdlib> #include <iostream> using namespace std; void func(int a)

{cout<<"Это формальный тип int "<<a<<endl;

}

void func (double d)

{

cout<<" Это формальный тип double "<< d << endl;

}

int main(int argc, char *argv[])

{

func('a');

system("PAUSE");

44

return EXIT_SUCCESS;

}

Символьный литерал имеет тип char. Он расширяется до int.

Стандартное преобразование

Имеется пять видов стандартных преобразований, а именно:

Целочисленные преобразования: приведение целочисленного типа или перечисления к любому другому целому типу (исключая преобразования , которые выше были отнесены к категории повышения типов);

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

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

Преобразования указателей: приведение целочисленного значения 0 к типу указателя или преобразование указателя любого типа в тип void*;

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

Вот несколько примеров: #include <cstdlib> #include <iostream> using namespace std; void func(void * pf)

{

cout<<"Это тип void * "<< pf <<endl;

}

void func (double d)

{

cout<<"Это тип double "<< d << endl;

}

int main(int argc, char *argv[])

{

int k=5;

 

 

 

func( k );

//соответствует func (double )

 

// аргумент k подвергается стандартному преобразованию

 

func (& k);

//соответствует

func(void * )

&k

подвергается стандартному

//преобразованию

 

из int* в void*

 

 

 

system("PAUSE");

 

 

45

return EXIT_SUCCESS;

}

Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как назначенный тип может и не обеспечивать представления всех значений исходного. Например, с помощью float нельзя адекватно представить все значения типа int. Именно по этой причине преобразования, входящие в эти группы, отнесены к категории стандартных преобразований, а не повышения типов.

#include <cstdlib> #include <iostream> using namespace std; void func(float f)

{

cout<<"Это тип float "<< f <<endl;

}

void func (char* c)

{

cout<<" Это тип char* "<< c << endl;

}

int main(int argc, char *argv[])

{

int k=65255; func( k );

system("PAUSE");

 

return EXIT_SUCCESS; }

 

При вызове функции

func() применяется стандартное

преобразование из целого типа int в тип с плавающей точкой float. В зависимости от значения переменной i может оказаться, что его нельзя сохранить в типе float без потери точности.

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

Например, если даны две перегруженные функции: #include <cstdlib>

#include <iostream> using namespace std; void func(float f) {

cout<<"Это тип float "<< f <<endl;

}

46

void func (long lg)

{

cout<<" Это тип

long * "<< lg << endl; }

int main(int argc, char *argv[])

{

func( 3.14 ); // ошибка: неоднозначность //func (float) не лучше, чем func(long)

system("PAUSE");

return EXIT_SUCCESS;

}

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

func ( static_cast<long>( 3.14 ) ); // func( long )

либо используя суффикс, обозначающий, что константа принадлежит к типу float:

func(3.14F);

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

typedef int (*PFV) ( );

PFV PfvArray[5];//массив указателей на функции

void reset ( void *);

 

int main ( )

 

 

{ ....

 

 

reset (PfvArray[0]

);

//ошибка: нет стандартного преобразования

между

 

 

return 0;

// int(*) ( ) и void*

}

 

 

Ссылки

Фактический параметр или формальный параметр функции могут быть ссылками. Когда ссылкой является фактический аргумент, тогда аргумент-ссылка трактуется как l-значение, тип которого совпадает с типом соответствующего объекта. Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове (формальный параметр -ссылка) не влияет на сам тип аргумента.

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

int i; int& ri = i;

47

void func( double ); int main() {

func( i ); //стандартное преобразование между целым типом // и типом с плавающей точкой

func( ri ); // то же самое return 0;}

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

• Фактический аргумент подходит в качестве инициализатора параметра-ссылки, т.е. между ними точное соответствие, например: void swap( int &, int &);

void myfunc (int ivar1 , int ivar2 )

{

// .....

swap ( ivar1, ivar2 ) ; //правильно: вызывается swap (int & , int & )

...}

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

int obj;

void myfunc( double & ); int main() {

myfunc( obj );

//ошибка: параметр должен иметь тип const double & return 0; }

Вызов функции myfunc() является ошибкой. Фактический аргумент имеет тип int и должен быть преобразован в тип double, чтобы соответствовать формальному параметру-ссылке. Результатом такого преобразования является временная переменная. Поскольку ссылка не имеет спецификатора const, то для ее инициализации такие переменные использовать нельзя.

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

class MyClass;

void setMyClass( MyClass & ); MyClass getMyClass ();

int main() {

setMyClass (getMyClass () );

// ошибка: параметр должен быть типа const MyClass & return 0; }

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

48

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

void myfunc ( int ); void myfunc( int& );

int iobj;

 

int &ri = iobj;

 

int main() {

 

myfunc( iobj );

// ошибка: неоднозначность

myfunc ( ri );

// ошибка: неоднозначность

myfunc( 57 );

// правильно: вызывается myfunc( int )

return 0;

 

Литерал, константу и параметр, требующий преобразования, можно передавать как параметр типа const&, но без спецификации const передавать нельзя. Допуская преобразования для параметра типа const T&, гарантируется, что он может принимать значения из того же множества, что и параметр типа T, значение которого передается при необходимости с помощью временной переменной.

float fsqrt(const float&); void g(double d) { float r;

r = fsqrt(2.0f);

// передача ссылки на временную переменную, содержащую 2.0f r = fsqrt(r); // передача ссылки на r

r = fsqrt(d);

// передача ссылки на временную переменную, содержащую float(d)

}

Запрет на преобразования типа для параметров-ссылок без спецификации const введен для того, чтобы избежать нелепых ошибок, связанных с использованием при передаче параметров временных переменных:

void update(float& i); void g(double d) {

float r;

 

update(2.0f);

// ошибка: параметр-константа

update(r);

// нормально: передается ссылка на r

update(d); }

// ошибка: здесь нужно преобразовывать тип

Аргументы со значениями по умолчанию

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

49

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

void func(int a){ cout<<”one parameter ”<< a;} void func(long a, int b=0) { cout<< “long ”<<a;}

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

аргументами:

 

int main()

{

//

 

func(2L);

//соответствует func(long, 0);

func(2.25);

//ошибка : неоднозначность

return 0; }

Лабораторная работа № 2 Тема: Перегруженные функции

Цель: Научиться использовать перегрузку функций, указатели на функции в рекурсивных функциях

Краткие теоретические сведения

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

void error(char* p) { /* ... */ }

void (*pfch)(char*); // указатель на функцию void f()

{

pfch = &error;

//pfch настроен на функцию error

(*pfch)("error");

// вызов error через указатель pfch

}

 

Для вызова функции с помощью указателя (pfch в нашем примере) надо вначале применить операцию косвенности к указателю - *pfch. Поскольку приоритет операции вызова () выше, чем приоритет косвенности *, нельзя писать просто *pfch("error"). Это будет означать *(pfch("error")), что является ошибкой. По той же причине скобки нужны и при описании указателя на функцию. Однако, писать просто pfch("error") можно, т.к. транслятор понимает, что pfch является указателем на функцию, и создает команды, делающие вызов нужной функции.

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

void (*pf)(char*);

// указатель на void(char*)

void f1(char*);

// void(char*);

int f2(char*);

// int(char*);

void f3(int*);

// void(int*);

50

void f()

 

 

 

{

 

 

 

pf = &f1;

// нормально

pf = &f2;

// ошибка: не тот тип возвращаемого значения

pf = &f3;

// ошибка: не тот тип параметра

(*pf)("asdf");

// нормально

(*pf)(1);

// ошибка: не тот тип параметра

int i = (*pf)("qwer");

// ошибка: void присваивается int

}

 

 

 

Функция,

которая

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

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

Одного или нескольких условий останова, которые могут быть вычислены для определенных параметров.

Шага рекурсии, в котором текущее значение в алгоритме может быть определено в терминах предыдущего значения, в конечном итоге

шаг рекурсии должен приводить к условиям останова.

Например, рекурсивное определение степенной функции имеет

единственное условие останова для случая n=0 (x0 = 1). an = x* x(n-1) , при n>0

Примеры построения рекурсивных функций:

1. Факториал неотрицательных целых чисел, Factorial(N), определяется как произведение всех положительных целых чисел, меньших или равных N. Число, обозначаемое N!, представляется следующим образом:

N!= N * (N-1)*(N-2)*...*2*1

 

Рекурсивная форма факториала может быть

представлена

следующей функцией:

 

long Factorial(long n)

 

{

 

// if n == 0, then 0! = 1; условие останова, в противном случае n! =

n*(n-1)!

if (n == 0) return 1;

else

//шаг рекурсии

return n * Factorial(n - 1);

}

2. Степенная функция (рекурсивная форма). Конструкция if .. else различает условие останова и шаг рекурсии при вычислении степенной функции :

51

//вычислить x в степени n, используя рекурсию float power ( float x, int n ) {

//условием останова является n ==0 if ( n == 0 )

//0 в степени 0 не определен

if (x ==0)

{

cerr << “power (0, 0 ) не определено ” << endl;

exit ( 1) ;

}

else

 

// x в степени 0 равен 1

return 1;

 

else

//шаг рекурсии

power (x, n ) = x * power (x, n-1 ) return x * power (x, n-1 ) ; }

Функция суммирования ( рекурсивная форма ) //вычислить 1+2 + .... + n рекурсивно

int S ( int n ) {

//условие останова является n ==1 if ( n == 1)

return 1;

else

// шаг рекурсии: S (n ) = n + S ( n- 1) return (n + S ( n- 1)); }

3. Комбинаторика: задача о комитетах. К комбинаторным относятся алгоритмы подсчета числа способов наступления того или иного события. В классической задаче о комитетах требуется определить число C (N, K), где N и K –неотрицательные целые, равное количеству способов формирования комитетов по K членов в каждом из общего списка N людей. Общее число комитетов складывается из числа комитетов по K членов, выбранных из N-1 человек и числа комитетов по K-1 членов, выбранных из N-1 человек. Первая группа насчитывает C (N-1, K ), а вторая C(N-1, K-1) вариантов.

C (N, K ) = C (N-1, K-1 ) + C (N-1, K ) //шаг рекурсии

Условия останова состоят из нескольких экстремальных случаев, которые можно проанализировать непосредственно. Если K>N, то нет достаточного числа людей для формирования комитетов. Число возможных комитетов по K членов, сформированных из N человек, равно 0. Если K=0, то формируется пустой комитет и это можно сделать лишь одним способом. Если K=N, все должны оказаться в одном комитете. Это может произойти только выбором всех членов списка в этот комитет. Объединяя условия останова и шаг рекурсии реализовать рекурсивную

52

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]