С.Б. Липпман, Ж. Лажойе - Язык программирования С++ Вводный курс (1114944), страница 71
Текст из файла (страница 71)
Выбор той или иной операциипроизводится незаметно для пользователя. Операция сложения перегружена, чтобыобеспечить работу с операндами разных типов. Ответственность за распознаваниеконтекста и применение операции, соответствующей типам операндов, возлагается накомпилятор, а не на программиста.В этой главе мы покажем, как определять собственные перегруженные функции.9.1.1. Зачем нужно перегружать имя функцииКак и в случае со встроенной операцией сложения, нам может понадобиться наборфункций, выполняющих одно и то же действие, но над параметрами различных типов.Предположим, что мы хотим определить функции, возвращающие наибольшее изпереданных значений параметров.
Если бы не было перегрузки, пришлось бы каждойтакой функции присвоить уникальное имя. Например, семейство функций max() моглобы выглядеть следующим образом:С++ для начинающих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> & );424С++ для начинающих•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 при подобном сравнении не принимаются вовнимание. Так, следующие два объявления считаются одинаковыми:С++ для начинающих// объявляют одну и ту же функцию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, но выполняютсемантически различные действия. В этом случае лексическая сложность, связанная с426С++ для начинающих427употреблением различных имен, проистекает из принятого программистом соглашенияоб обеспечении набора операций над типом данных и именования функций всоответствии с семантикой этих операций. Правда, механизм классов C++ делает такоесоглашение излишним. Следовало бы сделать такие функции членами класса Date, но#include <string>class Date {public:set( int, int, int );Date& convert( const string & );void print();// ...при этом оставить разные имена, отражающие смысл операции:};Приведем еще один пример.
Следующие пять функций-членов Screen выполняютразличные операции над экранным курсором, являющимся принадлежностью того жекласса. Может показаться, что разумно перегрузить эти функции под общим названиемScreen&Screen&Screen&Screen&moveHome();moveAbs( int, int );moveRel( int, int, char *direction );moveX( int );move():Screen& moveY( int );Впрочем, последние две функции перегрузить нельзя, так как у них одинаковые списки// функция, объединяющая moveX() и moveY()параметров. Чтобы сделать сигнатуру уникальной, объединим их в одну функцию:Screen& move( int, char xy );Теперь у всех функций разные списки параметров, так что их можно перегрузить подименем move(). Однако этого делать не следует: разные имена несут информацию, безкоторой программу будет труднее понять.
Так, выполняемые данными функциямиоперации перемещения курсора различны. Например, moveHome() осуществляетспециальный вид перемещения в левый верхний угол экрана. Какой из двух приведенных// какой вызов понятнее?myScreen.home();// мы считаем, что этот!ниже вызовов более понятен пользователю и легче запоминается?myScreen.move();В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена:применение подразумеваемых по умолчанию значений аргументов позволяет объединитьнесколько функций в одну.
Например, функции управления курсоромС++ для начинающихmoveAbs(int, int);moveAbs(int, int, char*);различаются наличием третьего параметра типа char*. Если их реализации похожи и длятретьего аргумента можно найти разумное значение по умолчанию, то обе функцииможно заменить одной.
В данном случае на роль значения по умолчанию подойдетуказатель со значением 0:move( int, int, char* = 0 );Применять те или иные возможности следует тогда, когда этого требует логикаприложения. Вовсе не обязательно включать перегруженные функции в программутолько потому, что они существуют.9.1.4. Перегрузка и область видимости AВсе перегруженные функции объявляются в одной и той же области видимости. К#include <string>void print( const string & );void print( double );// перегружает print()void fooBar( int ival ){// отдельная область видимости: скрывает обе реализации print()extern void print( int );// ошибка: print( const string & ) не видна в этой областиprint( "Value: ");print( ival );// правильно: print( int ) виднапримеру, локально объявленная функция не перегружает, а просто скрывает глобальную:}Поскольку каждый класс определяет собственную область видимости, функции,являющиеся членами двух разных классов, не перегружают друг друга.