Главная страница
Навигация по странице:

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

  • 9.1.6. Указатели на перегруженные функции A

  • 9.1.7. Безопасное связывание A

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница40 из 93
    1   ...   36   37   38   39   40   41   42   43   ...   93
    9.1.3.
    Когда не надо перегружать имя функции
    В каких случаях перегрузка имени не дает преимуществ? Например, тогда, когда присвоение функциям разных имен облегчает чтение программы. Вот несколько примеров. Следующие функции оперируют одним и тем же абстрактным типом даты. На первый взгляд, они являются подходящими кандидатами для перегрузки: void printDate( const Date& );
    Эти функции работают с одним типом данных – классом Date, но выполняют семантически различные действия. В этом случае лексическая сложность, связанная с
    // объявляют одну и ту же функцию void f( int );
    // объявляются разные функции void f( int* ); void f( const int* );
    // и здесь объявляются разные функции void f( int& ); void setDate( Date&, int, int, int );
    Date &convertDate( const string & );

    С++ для начинающих
    427
    употреблением различных имен, проистекает из принятого программистом соглашения об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но при этом оставить разные имена, отражающие смысл операции:
    };
    Приведем еще один пример. Следующие пять функций-членов Screen выполняют различные операции над экранным курсором, являющимся принадлежностью того же класса. Может показаться, что разумно перегрузить эти функции под общим названием move()
    :
    Screen& moveY( int );
    Впрочем, последние две функции перегрузить нельзя, так как у них одинаковые списки параметров. Чтобы сделать сигнатуру уникальной, объединим их в одну функцию:
    Screen& move( int, char xy );
    Теперь у всех функций разные списки параметров, так что их можно перегрузить под именем move(). Однако этого делать не следует: разные имена несут информацию, без которой программу будет труднее понять. Так, выполняемые данными функциями операции перемещения курсора различны. Например, moveHome() осуществляет специальный вид перемещения в левый верхний угол экрана. Какой из двух приведенных ниже вызовов более понятен пользователю и легче запоминается? myScreen.move();
    В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена: применение подразумеваемых по умолчанию значений аргументов позволяет объединить несколько функций в одну. Например, функции управления курсором
    #include class Date { public: set( int, int, int );
    Date& convert( const string & ); void print();
    // ...
    Screen& moveHome();
    Screen& moveAbs( int, int );
    Screen& moveRel( int, int, char *direction );
    Screen& moveX( int );
    // функция, объединяющая moveX() и moveY()
    // какой вызов понятнее? myScreen.home(); // мы считаем, что этот!

    С++ для начинающих
    428
    moveAbs(int, int, char*); различаются наличием третьего параметра типа char*. Если их реализации похожи и для третьего аргумента можно найти разумное значение по умолчанию, то обе функции можно заменить одной. В данном случае на роль значения по умолчанию подойдет указатель со значением 0: move( int, int, char* = 0 );
    Применять те или иные возможности следует тогда, когда этого требует логика приложения. Вовсе не обязательно включать перегруженные функции в программу только потому, что они существуют.
    9.1.4.
    Перегрузка и область видимости A
    Все перегруженные функции объявляются в одной и той же области видимости. К примеру, локально объявленная функция не перегружает, а просто скрывает глобальную:
    }
    Поскольку каждый класс определяет собственную область видимости, функции, являющиеся членами двух разных классов, не перегружают друг друга. (Функции-члены класса описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается в главе 15.)
    Объявлять такие функции разрешается и внутри пространства имен. С каждым из них также связана отдельная область видимости, так что функции, объявленные в разных пространствах, не перегружают друг друга. Например: moveAbs(int, int);
    #include 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 ) видна
    #include 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-объявление сказывается на перегрузке функций? Напомним, что оно вводит псевдоним для члена пространства имен в ту область видимости, в которой это объявление встречается. Что делают такие объявления в следующей программе? 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-объявление. Поэтому введенные функции участвуют в процессе разрешения имен всех перегруженных функций, присутствующих в данной области видимости: 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()
    { max( 87, 65 ); // вызывает libs_R_us::max( int, int )

    С++ для начинающих
    430
    }
    Using-объявление добавляет в глобальную область видимости два объявления: для print(int)
    и для print(double). Они являются псевдонимами в пространстве libs_R_us и включаются в множество перегруженных функций с именем print, где уже находится глобальная print(const string &). При разрешении перегрузки print в fooBar рассматриваются все три функции.
    Если using-объявление вводит некоторую функцию в область видимости, в которой уже имеется функция с таким же именем и таким же списком параметров, это считается ошибкой. С помощью using-объявления нельзя задать псевдоним для функции print(int)
    в пространстве имен libs_R_us, если в глобальной области видимости уже есть print(int). Например:
    }
    Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим особенности применения using-директивы. Using-директива приводит к тому, что члены пространства имен выглядят объявленными вне этого пространства, добавляя их в новую область видимости. Если в этой области уже есть функция с тем же именем, то происходит перегрузка. Например:
    #include 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( const string & ) print( ival ); // вызывает libs_R_us::print( int ) 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

    С++ для начинающих
    431
    }
    Это верно и в том случае, когда есть несколько using-директив. Одноименные функции, являющиеся членами разных пространств, включаются в одно и то множество:
    }
    }
    Множество перегруженных функций с именем print в глобальной области видимости включает функции print(int), print(double) и print(long double). Все они рассматриваются в main() при разрешении перегрузки, хотя первоначально были определены в разных пространствах имен.
    Итак, повторим, что перегруженные функции находятся в одной и той же области видимости. В частности, они оказываются там в результате применения using-объявлений и using-директив, делающих доступными имена из других областей.
    #include 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( const string & ) print( ival ); // вызывает libs_R_us::print( int ) namespace IBM { int print( int );
    } namespace Disney { double print( double );
    // using- директива
    // формируется множество перегруженных функций из различных
    // пространств имен using namespace IBM; using namespace Disney; long double print(long double); int main() { print(1); // вызывается IBM::print(int) print(3.1); // вызывается Disney::print(double) return 0;

    С++ для начинающих
    432
    9.1.5.
    Директива extern "C" и перегруженные функции A
    В разделе 7.7 мы видели, что директиву связывания extern "C" можно использовать в программе на C++ для того, чтобы указать, что некоторый объект находится в части, написанной на языке C. Как эта директива влияет на объявления перегруженных функций? Могут ли в одном и том же множестве находиться функции, написанные как на C++, так и на C?
    В директиве связывания разрешается задать только одну из множества перегруженных функций. Например, следующая программа некорректна: extern "C" void print( int );
    Приведенный ниже пример перегруженной функции calc() иллюстрирует типичное применение директивы extern "C": extern BigNum calc( const BigNum& );
    Написанная на C функция calc() может быть вызвана как из C, так и из программы на
    C++. Остальные две функции принимают в качестве параметра класс и, следовательно, их допустимо использовать только в программе на C++. Порядок следования объявлений несуществен.
    Директива связывания не имеет значения при решении, какую функцию вызывать; важны только типы параметров. Выбирается та функция, которая лучше всего соответствует типам переданных аргументов:
    }
    9.1.6.
    Указатели на перегруженные функции A
    Можно объявить указатель на одну из множества перегруженных функций. Например:
    // ошибка: для двух перегруженных функций указана директива extern "C" extern "C" void print( const char* ); class SmallInt ( /* ... */ ); class BigNum ( /* ... */ );
    // написанная на C функция может быть вызвана как из программы,
    // написанной на C, так и из программы, написанной на C++.
    // функции C++ обрабатывают параметры, являющиеся классами extern "C" double calc( double ); extern SmallInt calc( const SmallInt& );
    Smallint si = 8; int main() { calc( 34 ); // вызывается C-функция calc( double ) calc( si ); // вызывается функция C++ calc( const SmallInt & )
    // ... return 0;

    С++ для начинающих
    433
    void ( *pf1 )( unsigned int ) = &ff;
    Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует указатель, компилятор ищет в множестве всех перегруженных функций ту, которая имеет тот же тип возвращаемого значения и список параметров, что и функция, на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned int).
    А что если не найдется функции, в точности соответствующей типу указателя? Тогда компилятор выдаст сообщение об ошибке: void ( *pf2 )( int ) = &ff; double ( *pf3 )( vector ) = &ff;
    Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке.
    Таким образом, преобразование типов между указателями на функции никогда не производится. pc2 = &calc;
    9.1.7.
    Безопасное связывание A
    При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве систем компиляции программы, обрабатывающие этот текст для получения исполняемого extern void ff( vector ); extern void ff( unsigned int );
    // на какую функцию указывает pf1? extern void ff( vector ); extern void ff( unsigned int );
    // ошибка: соответствие не найдено: неверный список параметров
    // ошибка: соответствие не найдено: неверный тип возвращаемого значения matrix calc( const matrix & ); int calc( int, int ); int ( *pc1 )( int, int ) = 0; int ( *pc2 )( int, double ) = 0;
    // ...
    // правильно: выбирается функция calc( int, int ) pc1 = &calc;
    // ошибка: нет соответствия: неверный тип второго параметра

    С++ для начинающих
    434
    кода, требуют, чтобы все имена были различны. Редакторы связей, как правило, разрешают внешние ссылки лексически. Если такой редактор встречает имя print два или более раз, он не может различить их путем анализа типов (к этому моменту информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение о повторно определенном символе print и завершает работу.
    Чтобы разрешить эту проблему, имя функции вместе с ее списком параметров
    декорируется так, чтобы получилось уникальное внутреннее имя. Вызываемые после компилятора программы видят только это внутреннее имя. Как именно производится такое преобразование имен, зависит от реализации. Общая идея заключается в том, чтобы представить число и типы параметров в виде строки символов и дописать ее к имени функции.
    Как было сказано в разделе 8.2, такое кодирование гарантирует, в частности, что два объявления одноименных функций с разными списками параметров, находящиеся в разных файлах, не воспринимаются редактором связей как объявления одной и той же функции. Поскольку этот способ помогает различить перегруженные функции на фазе редактирования связей, мы говорим о безопасном связывании.
    Декорирование имен не применяется к функциям, объявленным с помощью директивы extern "C"
    , так как лишь одна из множества перегруженных функций может быть написана на чистом С. Две функции с различными списками параметров, объявленные как extern "C", редактор связей воспринимает как один и тот же символ.
    Упражнение 9.1
    Зачем может понадобиться объявлять перегруженные функции?
    Упражнение 9.2
    Как нужно объявить перегруженные варианты функции error(), чтобы были корректны следующие вызовы: error( "Invalid selection", selectVal );
    Упражнение 9.3
    Объясните, к какому эффекту приводит второе объявление в каждом из приведенных примеров: int index; int upperBound; char selectVal;
    // ... error( "Array out of bounds: ", index, upperBound ); error( "Division by zero" );

    С++ для начинающих
    435
    extern "C" double compute( double *, double );
    Упражнение 9.4
    Какая из следующих инициализаций приводит к ошибке? Почему?
    (d) void (*pf4)( const matrix & ) = 0;
    9.2.
    Три шага разрешения перегрузки
    Разрешением перегрузки функции называется процесс выбора той функции из множества перегруженных, которую следует вызвать. Этот процесс основывается на указанных при вызове аргументах. Рассмотрим пример:
    }
    Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется, будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float)
    или зафиксируется ошибка.
    Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь разобраться во всех деталях, начинающие программисты столкнутся с серьезными трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того, как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то впечатление
    (a) int calc( int, int ); int calc( const int, const int );
    (b) int get(); double get();
    (c) int *reset( int * ); double *reset( double * ):
    (d) extern "C" int compute( int *, int );
    (a) void reset( int * ); void (*pf)( void * ) = reset;
    (b) int calc( int, int ); int (*pf1)( int, int ) = calc;
    (c) extern "C" int compute( int *, int ); int (*pf3)( int*, int ) = compute;
    T t1, t2; void f( int, int ); void f( float, float ); int main() { f( t1, t2 ); return 0;

    С++ для начинающих
    436
    об этом процессе. Для тех, кто хочет узнать больше, в следующих двух разделах приводится более подробное описание.
    Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем на следующем примере:
    }
    При разрешении перегрузки функции выполняются следующие шаги:
    1. Выделяется множество перегруженных функций для данного вызова, а также свойства списка аргументов, переданных функции.
    2. Выбираются те из перегруженных функций, которые могут быть вызваны с данными аргументами, с учетом их количества и типов.
    3. Находится функция, которая лучше всего соответствует вызову.
    Рассмотрим последовательно каждый пункт.
    На первом шаге необходимо идентифицировать множество перегруженных функций, которые будут рассматриваться при данном вызове. Вошедшие в это множество функции называются кандидатами. Функция-кандидат – это функция с тем же именем, что и вызванная, причем ее объявление видимо в точке вызова. В нашем примере есть четыре таких кандидата: f(), f(int), f(double, double) и f(char*, char*).
    После этого идентифицируются свойства списка переданных аргументов, т.е. их количество и типы. В нашем примере список состоит из двух аргументов типа double.
    На втором шаге среди множества кандидатов отбираются устоявшие (viable) – такие, которые могут быть вызваны с данными аргументами, Устоявшая функция либо имеет столько же формальных параметров, сколько фактических аргументов передано вызванной функции, либо больше, но тогда для каждого дополнительного параметра должно быть задано значение по умолчанию. Чтобы функция считалась устоявшей, для любого фактического аргумента, переданного при вызове, обязано существовать преобразование к типу формального параметра, указанного в объявлении.
    В нашем примере есть две устоявших функции, которые могут быть вызваны с приведенными аргументами:

    функция f(int) устояла, потому что у нее есть всего один параметр и существует преобразование фактического аргумента типа double к формальному параметру типа int;

    функция f(double,double) устояла, потому что для второго аргумента есть значение по умолчанию, а первый формальный параметр имеет тип double, что в точности соответствует типу фактического аргумента.
    Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным. В таких случаях мы говорим, что имеет место отсутствие соответствия. void f(); void f( int ); void f( double, double = 3.4 ); void f( char *, char * ); void main() { f( 5.6 ); return 0;

    С++ для начинающих
    1   ...   36   37   38   39   40   41   42   43   ...   93


    написать администратору сайта