Вступление.

Приветствую всех, кто обратил внимание на эту статью. В ней я расскажу Вам о полиморфизме. Как Вы помните это один из трёх слонов, на которых держится CPP. Смысл этого термина заключается в том, что для похожих действий можно определить один интерфейс. Выполнение конкретного действия в программе будет определяться типом данных. Полиморфизм позволяет существенно упростить программу, избавив Вас от необходимости создавать множество функций с разными названиями, даже если они выполняют, по сути, схожие действия. Так в С, который не поддерживал полиморфизм, существовало несколько функций для нахождения модуля числа: abs(), labs(), fabs(), которые возвращают абсолютные значения целого, длинного целого и числа с плавающей точкой. В CPP же существует возможность переложить заботу о том, где какую функцию вызывать на плечи компилятора. Если мне удалось Вас убедить, что полиморфизм есть штука стоящая, то давайте уже начнем!

Перегрузка функций.

Разберем предыдущий пример с функциями семейства abs():
long	abs( long l ){return(labs(l));}
float abs( float f ){return(fabs(f));}

Вот и всё! Просто, да? Теперь поговорим об одной очень распространённой вещи: если Вы будете активно использовать перегрузку функций, то рано или поздно наткнётесь на “доброжелателя”, который скажет Вам, что перегрузка очень сильно снижает производительность программы. Не слушайте этого человека. Все эти “страшные” рассказы о полиморфизме относятся к той категории слухов, которые все с удовольствием смакуют и рассказывают друг другу, но когда спросишь такого горе-программиста, откуда он набрался всей этой ереси, в ответ слышишь только нечленораздельное мычание. Сейчас я развенчаю весь этот бред (один человек на полном серьёзе говорил мне, что создается таблица перегруженных функций, по которой при вызове ищется нужный вариант!) а заодно расскажу, что происходит на низком уровне, когда мы балуемся перегрузкой. Допустим, у нас есть такой код:
#include 
#include

void f( float ){}

void f( int ){}

void f( float , double ){}

void main(void){
f( ( float )1.0 );
}

Теперь давайте взглянем на фрагмент ассемблерного листинга (он был сгенерирован с помощью Borland C++ 3.1) этой программы:
; ; void	f( float ){
;
?debug L 4
assume cs:CLASSES_TEXT
@f$qf proc far
?debug B
push bp
mov bp,sp
push ds
mov ax,CLASSES_DATA
mov ds,ax
?debug C E6000E0A060000
?debug B
;
; }
;
?debug L 5
pop ds
pop bp
ret
?debug E
?debug E
@f$qf endp
;
; void f( int ){
;
?debug L 7
assume cs:CLASSES_TEXT
@f$qi proc far
?debug B
push bp
mov bp,sp
push ds
mov ax,CLASSES_DATA
mov ds,ax
?debug C E600040A060000
?debug B
;
; }
;
?debug L 8
pop ds
pop bp
ret
?debug E
?debug E
@f$qi endp
;
; void f( float , double ){
;
?debug L 10
assume cs:CLASSES_TEXT
@f$qfd proc far
?debug B
push bp
mov bp,sp
push ds
mov ax,CLASSES_DATA
mov ds,ax
?debug C E6000F0A0A0000000E0A060000
?debug B
;
; }
;
?debug L 11
pop ds
pop bp
ret
?debug E
?debug E
@f$qfd endp
;
; void main(void){
;
?debug L 13
assume cs:CLASSES_TEXT
_main proc far
?debug B
push bp
mov bp,sp
push ds
mov ax,CLASSES_DATA
mov ds,ax
?debug B
;
;
; f( ( float )1.0 );
;
?debug L 15
fld1
sub sp,4
fstp dword ptr [bp-6]
fwait
push cs
call near ptr @f$qf
add sp,4
;
;
; }

Ассемблер, как Вы наверное догадываетесь, ничего не знает о перегруженных функциях, поэтому на низком уровне генерируется несколько функций с разными названиями. Если вы приглядитесь повнимательнее, то обнаружите что общей части названия @f$q прибавляется суффикс, состоящий из аббревиатур типов формальных параметров функции. Так функция void f( int ) имеет название @f$qi, функция void f(float,double) имеет название @f$qfd. Таким образом, у типа int аббревиатура i, double – d, float – f, void – v и так далее. А вот о типе возвращаемого значения информация не сохраняется. Итак, мы готовы сформулировать критерий возможности существования перегруженной функции –
  • перегруженные функции должны различаться списком формальных параметров.
  • функции с одинаковыми списками формальных параметров, но разными типами возвращаемых значений с точки зрения компилятора неразличимы. Рассмотрим несколько примеров:
  • void	f( int ){}
    void f( float ){}

    void main( void ){ float _f; int _i; f( 1.0 );//1 f( ( int ) 1.0 );//2 f( ( long ) 1 );//3 f( _f );//4 f( _i );//5 }
    Первый вызов ошибочный, дело в том, что если типы являются совместимыми, то компилятор при необходимости может преобразовать значение от одного типа к другому. В первом же примере он не знает, к какому типу приводить (float или int). Поэтому возникает неоднозначность. В остальных примерах неоднозначности нет, т.к. мы явно указали какого типа параметры. Следующий пример так же не будет работать:
    int	f( void ){return(0);}
    float f( void ){return(0);}

    Т.к. у функций одинаковые списки формальных параметров. Осталось только рассмотреть варианты, когда какие-то значения передаются по ссылке:
    void	f( int & ){}
    void f( int ){}
    Для компилятора эти две функции эквивалентны. Поэтому при попытке воспользоваться ими, он разразится сообщениями об ошибках.

    Параметры по умолчанию.

    Иногда возникает необходимость написать функцию, у которой набор формальных параметров мог бы изменяться. Что нам делать создавать всевозможные перегруженные версии одной функции? Нет. В CPP существует механизм, называемый “параметрами по умолчанию”. Действует он следующим образом: в определении функции прописывается некоторое значение. Которое будет присвоено аргументу, в случае если он не определён при вызове функции. Например:
    void	f( int i , int j=0){
    	// “j=0” – определение значения параметра
    	cout<<j<<endl;
    }
    void	main( void ){
    f( 1 );
    f( 1 , 1 );
    }

    При выполнении этой программы вы увидите:
    0
    1

    Т.е. при первом вызове функции (когда мы не указали значение второго параметра) второму параметру было присвоено значение по умолчанию. Во втором же случае, последний параметр был указан явно. Стоит отметить, что все параметры по умолчанию должны передаваться последними.
    class banana{};
    void f( int , float f = 0 , banana ){}

    Компилятор вежливо предложит Вам указать значение по умолчанию и для третьего параметра. Вот вроде бы и все что я хотел рассказать про перегрузку функций. Осталась правда большая тема по перегрузке операторов, но про это я расскажу как-нибудь попозже. Замечания и предложения можете отправлять мне на электронную почту. Адрес прежний dodonov_a_a(___AT)inbox.ru.
    Смежные вопросы:
    Урок 1. Основы классов.
    Урок 2. Конструкторы копий, оператор присваивания.
    © 2004-2005 Savardge.ru
    Hosted by uCoz