Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

423

9

9. Перегруженные функции

Итак, мы уже знаем, как объявлять, определять и использовать функции в программах. В этой главе речь пойдет об их специальном виде перегруженных функциях. Две функции называются перегруженными, если они имеют одинаковое имя, объявлены в одной и той же области видимости, но имеют разные списки формальных параметров. Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим вопрос об их разрешении, т.е. о том, какая

именно из нескольких перегруженных функций вызывается во время выполнения программы. Эта проблема является одной из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно прочитать два раздела в конце главы,

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

9.1. Объявления перегруженных функций

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

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

вычисления выражения

1 + 3

вызывается операция целочисленного сложения, тогда как вычисление выражения

1.0 + 3.0

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

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

9.1.1. Зачем нужно перегружать имя функции

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

С++ для начинающих

424

 

int i_max( int, int );

 

 

 

 

int vi_max( const vector<int> & );

 

 

int matrix_max( const matrix & );

 

 

 

 

 

 

Однако все они делают одно и то же: возвращают наибольшее из значений параметров. С

 

точки зрения пользователя, здесь лишь одна операция вычисление максимума, а детали

 

ее реализации большого интереса не представляют.

 

Отмеченная лексическая сложность отражает ограничение программной среды: всякое

 

имя, встречающееся в одной и той же области видимости, должно относиться к

 

уникальной сущности (объекту, функции, классу и т.д.). Такое ограничение на практике

 

создает определенные неудобства, поскольку программист должен помнить или каким-то

 

образом отыскивать все имена. Перегрузка функций помогает справиться с этой

 

проблемой.

 

Применяя перегрузку, программист может написать примерно так:

 

 

vector<int> vec;

 

 

 

 

//...

 

 

int ix = max( j, k );

 

 

 

 

int iy = max( vec );

 

 

 

 

Этот подход оказывается чрезвычайно полезным во многих ситуациях.

 

9.1.2. Как перегрузить имя функции

В C++ двум или более функциям может быть дано одно и то же имя при условии, что их списки параметров различаются либо числом параметров, либо их типами. В данном

int max ( int, int );

int max( const vector<int> & );

примере мы объявляем перегруженную функцию max(): int max( const matrix & );

Для каждого перегруженного объявления требуется отдельное определение функции max() с соответствующим списком параметров.

Если в некоторой области видимости имя функции объявлено более одного раза, то второе (и последующие) объявление интерпретируется компилятором так:

∙ если списки параметров двух функций отличаются числом или типами

// перегруженные функции void print( const string & );

параметров, то функции считаются перегруженными: void print( vector<int> & );

С++ для начинающих

425

если тип возвращаемого значения и списки параметров в объявлениях двух

// объявления одной и той же функции void print( const string &str );

функций одинаковы, то второе объявление считается повторным: void print( const string & );

Имена параметров при сравнении объявлений во внимание не принимаются;

если списки параметров двух функций одинаковы, но типы возвращаемых значений различны, то второе объявление считается неправильным (несогласованным с первым) и

unsigned int max( int i1, int i2 );

int max( int i1, int i2 ); // ошибка: отличаются только типы

помечается компилятором как ошибка:

// возвращаемых значений

Перегруженные функции не могут различаться лишь типами возвращаемого значения;

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

// объявления одной и той же функции int max ( int *ia, int sz );

умолчанию значениями аргументов, то второе объявление считается повторным: int max ( int *ia, int = 10 );

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

//typedef не вводит нового типа typedef double DOLLAR;

//ошибка: одинаковые списки параметров, но разные типы

//возвращаемых значений

extern DOLLAR calc( DOLLAR );

раньше:

extern int calc( double );

Спецификаторы const или volatile при подобном сравнении не принимаются во внимание. Так, следующие два объявления считаются одинаковыми:

С++ для начинающих

426

// объявляют одну и ту же функцию void f( int );

void f( const int );

Спецификатор const важен только внутри определения функции: он показывает, что в теле функции запрещено изменять значение параметра. Однако аргумент, передаваемый по значению, можно использовать в теле функции как обычную инициированную переменную: вне функции изменения не видны. (Способы передачи аргументов, в частности передача по значению, обсуждаются в разделе 7.3.) Добавление спецификатора const к параметру, передаваемому по значению, не влияет на его интерпретацию. Функции, объявленной как f(int), может быть передано любое значение типа int, равно как и функции f(const int). Поскольку они обе принимают одно и то же множество значений аргумента, то приведенные объявления не считаются перегруженными. f() можно определить как

void f( int i ) { }

или как

void f( const int i ) { }

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

Однако, если спецификатор const или volatile применяется к параметру указательного

//объявляются разные функции void f( int* );

void f( const int* );

//и здесь объявляются разные функции void f( int& );

или ссылочного типа, то при сравнении объявлений он учитывается. void f( const int& );

9.1.3. Когда не надо перегружать имя функции

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

void setDate( Date&, int, int, int ); Date &convertDate( const string & );

первый взгляд, они являются подходящими кандидатами для перегрузки: void printDate( const Date& );

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

С++ для начинающих

427

употреблением различных имен, проистекает из принятого программистом соглашения

об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но

#include <string> class Date { public:

set( int, int, int );

Date& convert( const string & ); void print();

// ...

при этом оставить разные имена, отражающие смысл операции:

};

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

Screen& moveHome();

Screen& moveAbs( int, int );

Screen& moveRel( int, int, char *direction );

Screen& moveX( int );

move():

Screen& moveY( int );

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

// функция, объединяющая moveX() и moveY()

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

Screen& move( int, char xy );

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

// какой вызов понятнее?

// мы считаем, что этот!

myScreen.home();

ниже вызовов более понятен пользователю и легче запоминается? myScreen.move();

В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена:

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