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

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 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();

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

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

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

428

moveAbs(int, int);

moveAbs(int, int, char*);

различаются наличием третьего параметра типа char*. Если их реализации похожи и для третьего аргумента можно найти разумное значение по умолчанию, то обе функции можно заменить одной. В данном случае на роль значения по умолчанию подойдет указатель со значением 0:

move( int, int, char* = 0 );

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

9.1.4. Перегрузка и область видимости A

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

#include <string>

 

void print( const string & );

// перегружает print()

void print( double );

void fooBar( int ival )

{

//отдельная область видимости: скрывает обе реализации print() extern void print( int );

//ошибка: print( const string & ) не видна в этой области print( "Value: ");

print( ival );

// правильно: print( int ) видна

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

}

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

Объявлять такие функции разрешается и внутри пространства имен. С каждым из них также связана отдельная область видимости, так что функции, объявленные в разных

#include <string> namespace IBM {

extern void print( const string & );

extern void print( double ); // перегружает print()

}

namespace Disney {

//отдельная область видимости:

//не перегружает функцию print() из пространства имен IBM extern void print( int );

пространствах, не перегружают друг друга. Например:

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

429

}

Использование using-объявлений и using-директив помогает сделать члены пространства имен доступными в других областях видимости. Эти механизмы оказывают определенное влияние на объявления перегруженных функций. (Using-объявления и using-директивы рассматривались в разделе 8.6.)

Каким образом using-объявление сказывается на перегрузке функций? Напомним, что оно вводит псевдоним для члена пространства имен в ту область видимости, в которой это

namespace libs_R_us { int max( int, int );

int max( double, double );

extern void print( int ); extern void print( double );

}

// using-объявления using libs_R_us::max;

using libs_R_us::print( double ); // ошибка

void func()

 

{

// вызывает libs_R_us::max( int, int )

max( 87, 65 );

объявление встречается. Что делают такие объявления в следующей программе? max( 35.5, 76.6 ); // вызывает libs_R_us::max( double, double )

Первое using-объявление вводит обе функции libs_R_us::max в глобальную область видимости. Теперь любую из функций max() можно вызвать внутри func(). По типам аргументов определяется, какую именно функцию вызывать. Второе using-объявление это ошибка: в нем нельзя задавать список параметров. Функция libs_R_us::print() объявляется только так:

using libs_R_us::print;

Using-объявление всегда делает доступными все перегруженные функции с указанным именем. Такое ограничение гарантирует, что интерфейс пространства имен libs_R_us не будет нарушен. Ясно, что в случае вызова

print( 88 );

автор пространства имен ожидает, что будет вызвана функция libs_R_us::print(int).

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

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

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

430

#include <string> namespace libs_R_us {

extern void print( int ); extern void print( double );

}

extern void print( const string & );

//libs_R_us::print( int ) и libs_R_us::print( double )

//перегружают print( const string & )

using libs_R_us::print;

void fooBar( int ival )

{

// вызывает глобальную функцию

print( "Value: ");

print( ival );

// print( const string & )

// вызывает libs_R_us::print( int )

}

 

Using-объявление добавляет в глобальную область видимости два объявления: для print(int) и для print(double). Они являются псевдонимами в пространстве libs_R_us и включаются в множество перегруженных функций с именем print, где уже находится глобальная print(const string &). При разрешении перегрузки print в fooBar рассматриваются все три функции.

Если using-объявление вводит некоторую функцию в область видимости, в которой уже имеется функция с таким же именем и таким же списком параметров, это считается ошибкой. С помощью using-объявления нельзя задать псевдоним для функции print(int) в пространстве имен libs_R_us, если в глобальной области видимости уже

namespace libs_R_us { void print( int ); void print( double );

}

void print( int );

using libs_R_us::print; // ошибка: повторное объявление print(int)

void fooBar( int ival )

{

print( ival );

// какая print? ::print или libs_R_us::print

есть print(int). Например:

}

Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим особенности применения using-директивы. Using-директива приводит к тому, что члены пространства имен выглядят объявленными вне этого пространства, добавляя их в новую область видимости. Если в этой области уже есть функция с тем же именем, то происходит перегрузка. Например:

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

431

#include <string> namespace libs_R_us {

extern void print( int ); extern void print( double );

}

extern void print( const string & );

//using-директива

//print(int), print(double) и print(const string &) - элементы

//одного и того же множества перегруженных функций

using namespace libs_R_us;

void fooBar( int ival )

{

// вызывает глобальную функцию

print( "Value: ");

print( ival );

// print( const string & )

// вызывает libs_R_us::print( int )

}

 

Это верно и в том случае, когда есть несколько using-директив. Одноименные функции,

namespace IBM { int print( int );

}

namespace Disney { double print( double );

являющиеся членами разных пространств, включаются в одно и то множество:

//using-директива

//формируется множество перегруженных функций из различных

//пространств имен

using namespace IBM; using namespace Disney;

long double print(long double);

int main() {

// вызывается IBM::print(int)

print(1);

print(3.1);

// вызывается Disney::print(double)

return 0;

 

}

 

}

 

Множество перегруженных функций с именем print в глобальной области видимости включает функции print(int), print(double) и print(long double). Все они рассматриваются в main() при разрешении перегрузки, хотя первоначально были определены в разных пространствах имен.

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