Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
47
Добавлен:
16.02.2016
Размер:
455.17 Кб
Скачать

Глава 14

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

Почему следует использовать перегрузку

Перегрузка функций - это один из типов полиморфизма, обеспечиваемого C++. В C++ несколько функций могут иметь одно и то же имя. В этом случае функция, идентифицируемая этим именем, называется перегруженной. Перегрузить можно только функции, которые отличаются либо типом, либо числом своих аргументов. Перегрузить функции, которые отличаются только типом возвращаемого значения, нельзя. Возникает вопрос: зачем нужно использовать перегрузку? Ответ прост: перегружае­мые функции дают возможность упростить программы, допуская обращение к одному имени для выполнения близких по смыслу действий.

Чтобы перегрузить некоторую функцию, нужно просто объя­вить, а затем определить все требуемые варианты ее вызова. Компилятор автоматически выберет правильный вариант вызова на основании числа и типа используемых аргументов. Пример:

#include <iostream.h>

//Перегрузка функции abs()

int abs(int n);

long abs(long 1);

double abs(double d);

main()

{

int n=255; long l=-25L; double d=-15.0L;

cout « "Абсолютная величина " « n «" равна: " « abs(n) « "\n";

cout « "Абсолютная величина " « l «" равна: " « abs(l) « "\n";

cout « "Абсолютная величина” « d «" равна: " « abs(d) « "\n";

return 0;

}

int abs(int n)

{

return n<0? -n:n;

}

long abs(long l)

{

return l<0? -l:l;

}

double abs(double d)

{

return d<0? -d:d;

}

Как известно, библиотека времени выполнения C++ использу­ет три разных функции abs (), labs () и fabs () для вычисле­ния абсолютного значения аргумента. Использование той или другой из них зависит от типа аргумента. Так, в данном примере осуществляется перегрузка функции abs (), что упрощает про­грамму. В зависимости от переданного аргумента вызывается нужный вариант функции.

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

Чтобы реализовать концепцию перегрузки, разработчикам компиляторов C++ пришлось ввести декорирование имен. По­следнее означает, что все функции в коде программы получают от компилятора имена, основываясь на имени, заданном про­граммистом, и количестве и типах аргументов. Различные компи­ляторы делают это несколько отличным друг от друга образом. Здесь мы опишем, как это делает компилятор фирмы Inprise (ра­нее - фирма Borland): вначале идет имя класса (если речь идет о функции-члене класса), предваряемое символом "@", затем имя функции, снова предваряемое символом "@". У всех идентифи­каторов различается регистр букв. Затем следует последователь­ность символов "@q", начиная с которой идут кодированные обо­значения параметров функции. Для обозначения указателей и ссылок к кодам встроенных типов добавляются буквы "р" и "r", соответственно. Например, если дано такое определение класса:

class AnyClass

{

public:

void SetVal() ;

void SetVal(int);

void SetVal(int, int);

void SetVal(int, int, int);

void SetVal(int&, int, int);

};

компилятор Borland C++ сгенерирует такие имена функций:

@AnyClass@SetVal@qv

@AnyClass@SetVal@qi

@AnyClass@SetVal@qii

@AnyClass@SetVal@qiii

@AnyClass@SetVal@qriii

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

Отсюда видно, что в декорировании имен возвращаемое функцией значение не участвует. Именно по этой причине нельзя перегружать функции, которые отличаются только типом воз­вращаемого значения. Разобраться с тем, как декорирует имена конкретный компилятор достаточно легко. Достаточно потребо­вать от него выдать ассемблерный эквивалент текста программы.

Замечание. Декорирование имен порождает и определенные проблемы. Если нужно написать код, который будет вызываться из программы, написанной на другом языке или даже на другом компиляторе C++, нужно отключить декорирование имен, иначе нужная функция не будет найдена. Это делается с помощью клю­чевого слова extern в форме extern "С" {} Буква "С" здесь напоминает о языке С, в котором декорирования имен не было.

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

Перегрузка - это практика предоставления более чем одного определения для данного имени функции в одной и той же облас­ти видимости. Возможность выбрать соответствующую версию функции, основываясь на типах и числе аргументов, с которыми она вызывается, предоставляется компилятору. При этом два типа данных считаются различными, если для них используются различные инициализаторы. Поэтому аргумент данного типа и ссылка на этот тип рассматриваются как одно и то же с точки зрения перегрузки. Например, объявление двух таких функций

int func(int, int)

и

int func(int&, int&)

приведет к ошибке, так как с точки зрения перегрузки они счита­ются одинаковыми. По той же причине аргументы функции, от­носящиеся к некоторому типу, модифицированные const или volatile, не рассматриваются как отличные от базового типа с точки зрения перегрузки. Указатели на const- и volatile-объекты также не рассматриваются как отличные от указателей на базовый тип с точки зрения перегрузки. Однако механизм пе­регрузки может различать ссылки, которые имеют модификаторы const и volatile, и ссылки на базовый тип. Например, сле­дующий код вполне допустим:

#include <iostream.h>

class AnyClass

{

public:

AnyClass()

{cout «"Конструктор по умолчанию "«"для AnyClass" « "\n";} AnyClass(AnyClassS ob)

{cout «"Конструктор копирования для " « "AnyClass" « "\n";}

AnyClass(const AnyClassS ob)

{cout «"Конструктор копирования для" « "AnyClass" « "\n";}

};

main()

{

AnyClass ob1;

AnyClass оb2(ob1);

const AnyClass оb3;

AnyClass оb4(оb3);

}

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

• любые две перегруженные функции должны иметь раз­личные списки параметров;

• перегрузка функций с совпадающими списками аргумен­тов на основе лишь типа возвращаемых ими значений не­допустима;

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

• typedef-определения не влияют на механизм перегруз­ки, так как они не вводят новых типов данных, а опреде­ляют лишь синонимы для существующих типов. Напри­мер, следующее определение

typedef char* PSTR;

не позволит компилятору рассматривать две приведенные ниже функции –

void SetVal(char* sz) ;

и

void SetVal(PSTR sz);

как различные. Поэтому их одновременное объявление в классе вызовет ошибку;

• все enum-типы данных рассматриваются как различные и могут использоваться для различения перегруженных функций;

• типы "массив (чего-то)" и "указатель (на что-то)" рас­сматриваются как идентичные с точки зрения перегрузки. Это верно только для одномерных массивов. Поэтому в случае перегрузки следующих функций будет выдана ошибка:

void SetVal(char sx);

void SetVal(char* psz);

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

void SetVal(char szStr[]);

void SetVal(char szStr[][4]);

При объявлении перегруженных функций компилятор отсле­живает объявления, данные в пределах одной и той же области видимости. Поэтому, если в производном классе объявлена функция с тем же именем, что и функция в базовом классе, функ­ция из производного класса скрывает функцию базового класса вместо того, чтобы осуществлять перегрузку (так как их области видимости различны). Короче говоря, нельзя путать переопреде­ление функций с перегрузкой. Точно также компилятор отслежи­вает и другие области видимости. Поэтому, так как функция, объявленная в области видимости файла, находится не в той же области видимости, что и функция, объявленная в блоке, даже если они имеют одинаковые имена, компилятор не рассматривает их как перегруженные. Локально объявленная функция просто скрывает глобально объявленную. Например:

#include <iostream.h>

void func(int i)

{

cout « "Вызов глобально объявленной "«"функции:"« i « endl;

}

void func(char* str)

{

cout «"Вызов локально объявленной " « "функции:" « str « endl;

}

main ()

{

extern void func(char*);

//Следующий вызов функции вызовет ошибку

func(100);

//А это - верно

func("переопределение!");

return 0;

}

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

class AnyClass

{

public:

AnyClass();

double MembFunc(double, char*);

private:

int MembFunc(int);

};

double

AnyClass:: MembFunc(double, char*)

{

//Тело функции

}

int

AnyClass:: MembFunc(int)

{

//Тело функции

}

main()

{

AnyClass* pCl = new AnyClass;

//Следующая инструкция вызовет ошибку, т.к. она содержит вызов private-функции

рСl-> MembFunc(104);

//А здесь - все верно, т.к. вызывается publiс-функция

рСl-> MembFunc(104.22, "строковый аргумент");

delete pCl;

return 0;

}

Концептуально можно перегружать и статические функции-члены класса, хотя это не имеет большого смысла. Ниже приве­ден пример перегруженной статической функции-члена.

class AnyClass

{

public:

static void SetVal(int);

static void SetVal(double);

private:

static int x;

static double y;

};

int AnyClass::x = 0;

double AnyClass::у = 0;

void AnyClass::SetVal(int _x){x = _x;}

void AnyClass::SetVal(double _y){y = _y;}

main ()

{

AnyClass::SetVal(10);

AnyClass::SetVal(10.4567);

return 0;

}

Перегрузка конструкторов

Чаще всего перегрузка применяется при создании перегру­женных конструкторов (перегружать деструктор нельзя!). Целью этой перегрузки является желание предоставить пользователю как можно больше вариантов создания представителей класса. На самом деле мы уже неоднократно встречались с перегрузкой кон­структоров, хотя и не говорили об этом. Если класс предоставля­ет конструктор с параметрами и конструктор по умолчанию, мы уже имеем дело с перегрузкой конструкторов. Как мы уже знаем, конструктор по умолчанию необходим при выделении динамиче­ской памяти массиву объектов (ибо динамический массив объек­тов не может быть инициализирован).

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

void func(AnyClass& rOb);

Любая попытка вызвать эту функцию с аргументом другого типа приведет к ошибке компиляции, потому что компилятор не знает, как осуществить нужное преобразование типа. Чтобы ука­зать компилятору, как осуществить преобразование какого-то типа Т в тип данных AnyClass, нужно просто задать в соответ­ствующем классе конструктор, принимающий единственный па­раметр типа т. При этом будет использовано уже отмечавшееся нами ранее свойство компилятора трактовать конструкцию

AnyClass ob(t);

как

AnyClass ob = AnyClass(t);

если в классе определен конструктор

AnyClass(T t) ;

В рассматриваемом случае это означает, что вызов функции с аргументом t типа Т, то есть

func(t);

трактуется как

func(AnyClass(t));

При этом осуществляется неявное преобразование типа дан­ных. Неявное потому, что исходный код не вызывает явно конст­руктор для преобразования. С этой целью компилятор каждый раз будет создавать временный объект класса AnyClass, кото­рому и будет передавать параметр, переданный в функцию.

Следующий пример это демонстрирует.

class AnyClass

{

int x;

public:

//Конструктор по умолчанию

AnyClass () ;

//Конструктор преобразования типа int

AnyClass(int);

//Конструктор преобразования типа long

AnyClass(long);

//Конструктор преобразования типа double

AnyClass(double);

};

AnyClass::AnyClass() {x = 0;}

AnyClass::AnyClass(int _x) {x = _x;}

AnyClass::AnyClass(long _x) {x = int(_x);}

AnyClass::AnyClass(double _x) {x = _x;}

void func(AnyClass& rOb)

{

//Тело функции

}

main()

{

func(10); //Преобразование int в AnyClass

func(10L); //Преобразование long в AnyClass

//Преобразование double в AnyClass

func(10.0);

AnyClass ob;

func(ob); //Преобразование не нужно

return 0;

}

Этот же способ преобразования типа данных может быть ис­пользован и для преобразования из одного класса в другой. При­веденный ниже пример демонстрирует конструктор преобразова­ния из одного класса в другой.

class AnyClass

{

int x;

public:

AnyClass(){x =0;}

int GetX(){return x;}

};

class OtherClass

{

int z ;

public:

//Конструктор по умолчанию

OtherClass(){z =0;}

//Конструктор преобразования

OtherClass(AnyClass& cl) ;

};

//Определение конструктора преобразования

OtherClass::OtherClass(AnyClass& cl)

{

z = cl.GetX() ;

}

void func(OtherClass& Ob)

{

//Тело функции

}

main ()

{

AnyClass ob;

//Преобразование AnyClass в Othlass

func(ob);

return 0;

}

Несмотря на то, что данный пример работает, преобразование типа, которое он осуществляет, не имеет большого смысла. Ответственность за осуществляемое преобразование типа, как все­гда, лежит на программисте.

Определенные пользователем преобразования типа применя­ются компилятором в следующих ситуациях:

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

• при вызове функций;

• при возврате функцией значений.

Разумеется, их можно применять и для явного преобразования типа. Например:

main ()

{

AnyClass ob;

//Явное преобразование типа

OtherClass newOb = (OtherClass) ob;

}

Рассмотрим теперь пример, который демонстрирует пользу перегрузки другого конструктора - конструктора копирования. Пусть дан класс, который предназначен для использования в про­грамме прямоугольных областей:

class Rect

{

public:

Rect() ;

Rect(int, int);

Rect(int, int, int, int);

Rect(const Rect&);

Rect(const Rect&, int =0, int =0);

private:

int x, y, w, h;

};

Rect:: Rect()

{

x=y = w = h = 0;

}

Rect:: Rect(int _x, int _y)

{

x = _x;

y = _y;

w = h = 100;

}

Rect:: Rect(int _x, int _y, int _w, int _h)

{

x = _x;

у = _y;

w = _w;

h = _h;

}

Rect:: Rect(const Rect& rc)

{

x = rc.x;

у = rc.у;

w = rc.w;

h = rc.h;

}

Rect:: Rect(const Rect& rc, int _x, int _y)

{

x = _x;

у = _у;

w = rc.w;

h = rc.h;

}

main()

{

Rect rc(5,10,10,200);

Rect newrc(rc, 15, 40);

return 0;

}

Данный класс содержит несколько конструкторов, которые должны предоставить пользователю класса достаточную гиб­кость в создании нужных ему прямоугольных областей. Среди прочих данный класс содержит перегруженный конструктор ко­пирования

Rect(const Rect&, int =0, int =0);

Приведенный код перегруженного конструктора копирования копирует некоторые из данных-членов объекта, указанного ссылкой, но какие-то данные-члены нового объекта инициализирует по-другому. Данный конструктор можно квалифицировать как конструктор копирования благодаря тому, что первый его параметр представляет собой (константную) ссылку на объект класса, а остальные - это аргументы по умолчанию. Таким обра­зом, вполне возможно создавать конструкторы копирования, со­держащие дополнительные параметры. Это предоставляет чрез­вычайную гибкость при создании разнообразных конструкторов копирования.

Создание и использование конструкторов копирования

Ранее мы уже встречались с этим понятием. Здесь мы подроб­но рассмотрим, как и зачем используются конструкторы копиро­вания. Вначале рассмотрим вопрос: зачем понадобилось вводить такое понятие, как конструктор копирования.

В случае, если в функцию в качестве аргумента передается объект (то есть происходит передача аргумента по значению), делается точная (или побитовая) копия этого объекта, которая и передается параметру функции. Однако, если объект содержит указатель на выделенную область памяти, то в копии указатель будет указывать на ту же самую область памяти, на которую указывает исходный указатель. Поэтому, если в функции будет использован указатель из копии, - будет изменена область памя­ти, связанная с исходным объектом. Кроме того, когда функция завершается, - вызывается деструктор, который уничтожает ко­пию объекта. Опять это приведет к изменению содержимого об­ласти памяти, связанной с исходным объектом.

Если объект является возвращаемым значением функции, то перед возвратом из функции компилятор генерирует временный объект для хранения, возвращаемого функцией значения. Когда происходит возврат из функции, этот временный объект выходит из области видимости. Поэтому компилятор вызывает для него деструктор. Однако если деструктор вызывает освобождение ди­намической области памяти, выделенной самому объекту или одному из содержащихся в нем объектов, это приведет к ошибке.

В основе этих проблем лежит использование сгенерированно­го компилятором конструктора копирования по умолчанию. В случае, если программист предоставляет конструктор копирова­ния, именно он будет использоваться компилятором при инициа­лизации одного объекта другим (и, в частности, при передаче объекта в функцию).

Замечание. Следует отличать конструктор копирования от оператора присваивания. Например,

AnyClass ob1;

AnyClass func(); //объявление функции

//оb2 инициализируется оb1 с помощью конструктора копирования

AnyClass ob2(ob1);

//Следующий оператор эквивалентен предыдущему

AnyClass оb2 = оb1;

//Возврат объекта из функции с помощью оператора копирования

оb1 = func();

//Напротив, следующая инструкция использует оператор присваивания оb2 = оb1;

Таким образом, несмотря на наличие в инструкции знака при­сваивания, она не всегда означает выполнение присваивания. Ин­струкция

AnyClass оb2 = оb1 ;

представляет собой просто другую форму записи конструкто­ра копирования.

Устаревшее ключевое слово overload

В старых версиях C++ для создания перегруженных функций требовалось ключевое слово overload. Синтаксис его исполь­зования был следующим:

overload <имя_функции>;

Упомянутая в overload функция должна была объявляться до использования этого оператора. Таким образом, ключевое сло­во overload сообщало компилятору о намерении перегрузить упомянутую после него функцию. Хотя ключевое слово overload в современных компиляторах больше не поддерживается, его все еще можно встретить в старых программах, по­этому полезно знать как и для чего оно использовалось.

Перегрузка и неоднозначность. Ключевое слово explicit

Перегруженные функции выбираются по принципу наи­лучшего соответствия объявлений функций в текущей области видимости аргументам, предоставленным в вызове функции. Если подходящая функция найдена, эта функция и вызывается. Слово "подходящая" в этом контексте означает одно из сле­дующего (в порядке ухудшения соответствия):

• найдено точное соответствие;

• выполнено тривиальное преобразование;

• выполнено преобразование целочисленных типов;

• существует стандартное преобразование к желаемому ти­пу аргумента;

• существует определенное программистом преобразование (оператор преобразования или конструктор) к тре­буемому типу аргумента;

• были найдены аргументы, представленные многоточием.

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

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

Функция с n аргументами по умолчанию, с точки зрения со­ответствия аргументов, рассматривается как совокупность из п+1 функций, каждая из которых отличается от предыдущей за­данием одного дополнительного аргумента. Многоточие (...) дей­ствует как произвольный символ: оно соответствует любому за­данному аргументу. Это может служить источником неоднознач­ности при выборе перегруженной функции.

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

Функции-члены класса рассматриваются различным образом в зависимости от того, объявлены ли они статическими или нет, потому что нестатические функции имеют неявный аргумент, через который передается указатель this. При определении функции, которая наилучшим образом соответствует вызову, для нестатических функций-членов рассматриваются только те пере­груженные функции-члены, у которых скрытый указатель this соответствует типу объекта, который передан функции при вызо­ве. В отличие от других аргументов при попытке установить со­ответствие аргумента с указателем this никакие преобразова­ния не производятся.

В отношении перегруженных конструкторов язык C++ пре­доставляет дополнительную возможность по управлению процес­сом поиска соответствия вызываемого конструктора. Ключевое слово explicit представляет собой спецификатор объявления, который может применяться только в объявлениях конструкторов (но не в определениях конструкторов вне класса). Например:

class Т

{

public:

explicit Т (int);

explicit T(double)

{

//тело конструктора

}

};

Т::Т (int)

{

// …

}

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

void f(T) {}; //Определение функции

void g(int i)

{

//Следующая инструкция вызовет ошибку, т.к. неявное преобразование типа int в тип Т запрещено.

f(i);

}

void h()

{

Т ob(10); // А это - верно.

}

Замечание. Нет смысла применять ключевое слово explicit к конструкторам с несколькими параметрами, так как такие конструк­торы не принимают участия в неявных преобразованиях типа.

Определение адреса перегруженной функции

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

//Первая версия функции

int Func(int i, int j);

int Func(long 1); //Вторая версия функции

//Определение адреса первой версии функции

int (*pFunc) (int, int) = Func;

//Вызов функции

pFunc(10,20);

Компилятор определяет, какую версию функции нужно вы­брать путем отыскания функции, которая в точности соответст­вует заданному указателем типу. Если точное соответствие не найдено, выражение, которое получает адрес функции, неодно­значно. В этом случае генерируется ошибка.

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

Соседние файлы в папке ЯзыкС++Глушаков