<< Пред.           стр. 41 (из 121)           След. >>

Список литературы по разделу

 
  using blip::bj; // скрывает глобальную bj
  // bj в функции manip()ссылается на blip::bj
  ++bj; // blip::bj == 16
 
  int bk; // объявление локальной bk
  using blip::bk; // ошибка: повторное определение bk в manip()
 }
 
 int wrongInit = bk; // ошибка: bk невидима
  // надо использовать blip::bk
 Using-объявления в функции manip() позволяют ссылаться на членов пространства blib с помощью неквалифицированных имен. Такие объявления не видны вне manip(), и неквалифицированные имена могут применяться только внутри этой функции. Вне ее необходимо употреблять квалифицированные имена.
 Using-объявление упрощает использование членов пространства имен. Оно вводит только одно имя. Using-объявление может находиться в определенной области видимости, и, значит, мы способны точно указать, в каком месте программы те или иные члены разрешается употреблять без дополнительной квалификации.
 В следующем подразделе мы расскажем, как ввести в определенную область видимости все члены некоторого пространства имен.
 8.6.3. Using-директивы
 Пространства имен появились в стандартном С++. Предыдущие версии С++ их не поддерживали, и, следовательно, поставляемые библиотеки не помещали глобальные объявления в пространства имен. Множество программ на С++ было написано еще до того, как компиляторы стали поддерживать такую опцию. Заключая содержимое библиотеки в пространство имен, мы можем испортить старое приложение, использующее ее предыдущие версии: все имена из этой библиотеки становятся квалифицированными, т.е. должны включать имя пространства вместе с оператором разрешения области видимости. Те приложения, в которых эти имена употребляются в неквалифицированной форме, перестают компилироваться.
 Сделать видимыми имена из библиотеки, используемой в нашей программе, можно с помощью using-объявления. Предположим, что файл primer.h содержит интерфейс новой версии библиотеки, в котором глобальные объявления помещены в пространство имен cplusplus_primer. Нужно заставить нашу программу работать с новой библиотекой. Два using-объявления сделают видимыми имена класса matrix и функции inverse() из пространства cplusplus_primer:
 #include "primer.h"
 using cplusplus_primer::matrix;
 using cplusplus_primer::inverse;
 
 // using-объявления позволяют использовать
 // имена matrix и inverse без спецификации
 void func( matrix &m ) {
  // ...
  inverse( m );
  return m;
 }
 Но если библиотека достаточно велика и приложение часто использует имена из нее, то для подгонки имеющегося кода к новой библиотеке может потребоваться много using-объявлений. Добавлять их все только для того, чтобы старый код скомпилировался и заработал, утомительно и чревато ошибками. Решить эту проблему помогают using-директивы, облегчающие переход на новую версию библиотеки, где впервые стали применяться пространства имен.
 Using-директива начинается ключевым словом using, за которым следует ключевое слово namespace, а затем имя некоторого пространства имен. Это имя должно ссылаться на определенное ранее пространство, иначе компилятор выдаст ошибку. Using-директива позволяет сделать все имена из этого пространства видимыми в неквалифицированной форме.
 Например, предыдущий фрагмент кода может быть переписан так:
 #include "pnmer.h"
 
 // using-директива: все члены cplusplus_primer
 // становятся видимыми
 using namespace cplusplus_primer;
 
 // имена matrix и inverse можно использовать без спецификации
 void func( matrix &m ) {
  // ...
  inverse( m );
  return m;
 }
 Using-директива делает имена членов пространства имен видимыми за его пределами, в том месте, где она использована. Например, приведенная using-директива создает иллюзию того, что все члены cplusplus_primer объявлены в глобальной области видимости перед определением func(). При этом члены пространства имен не получают локальных псевдонимов, а как бы перемещаются в новую область видимости. Код
 namespace A {
  int i, j;
 }
 выглядит как
 int i, j;
 для фрагмента программы, содержащего в области видимости следующую using-директиву:
 using namespace A;
 Рассмотрим пример, позволяющий подчеркнуть разницу между using-объявлением (которое сохраняет пространство имен, но создает ассоциированные с его членами локальные синонимы) и using-директивой (которая полностью удаляет границы пространства имен).
 namespace blip {
  int bi = 16, bj = 15, bk = 23;
  // прочие объявления
 }
 int bj = 0;
 
 void manip() {
  using namespace blip; // using-директива -
  // коллизия имен ::bj and blip::bj
  // обнаруживается только при
  // использовании bj
  ++bi; // blip::bi == 17
  ++bj; // ошибка: неоднозначность
  // глобальная bj или blip::bj?
  ++::bj; // правильно: глобальная bj == 1
  ++blip::bj; // правильно: blip::bj == 16
 
  int bk = 97; // локальная bk скрывает blip::bk
  ++bk; // локальная bk == 98
 }
 Во-первых, using-директивы имеют область видимости. Такая директива в функции manip() относится только к блоку этой функции. Для manip() члены пространства имен blip выглядят так, как будто они объявлены в глобальной области видимости, а следовательно, можно использовать их неквалифицированные имена. Вне этой функции необходимо употреблять квалифицированные.
 Во-вторых, ошибки неоднозначности, вызванные применением using-директивы, обнаруживают себя при реальном обращении к такому имени, а не при встрече в тексте самой этой директивы. Например, переменная bj, член пространства blib, выглядит для manip() как объявленная в глобальной области видимости, вне blip. Однако в глобальной области уже есть такая переменная. Возникает неоднозначность имени bj в функции manip(): оно относится и к глобальной переменной, и к члену пространства blip. Ошибка проявляется только при упоминании bj в функции manip(). Если бы это имя вообще не использовалось в manip(), коллизия не проявилась бы.
 В-третьих, using-директива не затрагивает употребление квалифицированных имен. Когда в manip() упоминается ::bj, имеется в виду переменная из глобальной области видимости, а blip::bj обозначает переменную из пространства имен blip.
 И наконец члены пространства blip выглядят для функции manip() так, как будто они объявлены в глобальной области видимости. Это означает, что локальные объявления внутри manip() могут скрывать имена членов пространства blip. Локальная переменная bk скрывает blip::bk. Ссылка на bk внутри manip() не является неоднозначной – речь идет о локальной переменной.
 Using-директивы использовать очень просто: стоит написать одну такую директиву, и все члены пространства имен сразу становятся видимыми. Однако чрезмерное увлечение ими возвращает нас к старой проблеме засорения глобального пространства имен:
 namespace cplusplus_primer {
  class matrix { };
  // прочие вещи ...
 }
 namespace DisneyFeatureAnimation {
  class matrix { };
  // здесь тоже ...
 
 using namespace cplusplus_primer;
 using namespace DisneyFeatureAnimation;
 
 matrix m; //ошибка, неоднозначность:
 // cplusplus_primer::matrix или DisneyFeatureAnimation::matrix?
 Ошибки неоднозначности, вызываемые using-директивой, обнаруживаются только в момент использования. В данном случае – при употреблении имени matrix. Такая ошибка, найденная не сразу, может стать сюрпризом: заголовочные файлы не менялись и никаких новых объявлений в программу добавлено не было. Ошибка появилась после того, как мы решили воспользоваться новыми средствами из библиотеки.
 Using-директивы очень полезны при переводе приложений на новые версии библиотек, использующие пространства имен. Однако употребление большого числа using-директив возвращает нас к проблеме засорения глобального пространства имен. Эту проблему можно свести к минимуму, если заменить using-директивы более селективными using-объявлениями. Ошибки неоднозначности, вызываемые ими, обнаруживаются в момент объявления. Мы рекомендуем пользоваться using-объявлениями, а не using-директивами, чтобы избежать засорения глобального пространства имен в своей программе.
 8.6.4. Стандартное пространство имен std
 Все компоненты стандартной библиотеки С++ находятся в пространстве имен std. Каждая функция, объект и шаблон класса, объявленные в стандартном заголовочном файле, таком, как или , принадлежат к этому пространству.
 Если все компоненты библиотеки объявлены в std, то какая ошибка допущена в данном примере:
 #include
 #include
 #include
 
 int main()
 {
  // привязка istream_iterator к стандартному вводу
  istream_iterator infile( cin );
 
  // istream_iterator, отмечающий end-of-stream
  istream_iterator eos;
 
  // инициализация svec элементами, считываемыми из cin
  vector svec( infile, eos );
  // ...
 }
 Правильно, этот фрагмент кода не компилируется, потому что члены пространства имен std должны использоваться с указанием их специфицированных имен. Для того чтобы исправить положение, мы можем выбрать один из следующих способов:
 заменить имена членов пространства std в этом примере соответствующими специфицированными именами;
 применить using-объявления, чтобы сделать видимыми используемые члены пространства std;
 употребить using-директиву, сделав видимыми все члены пространства std.
 Членами пространства имен std в этом примере являются: шаблон класса istream_iterator, стандартный входной поток cin, класс string и шаблон класса vector.
 Простейшее решение – добавить using-директиву после директивы препроцессора #include:
 using namespace std;
 В данном примере using-директива делает все члены пространства std видимыми. Однако не все они нам нужны. Предпочтительнее пользоваться using-объявлениями, чтобы уменьшить вероятность коллизии имен при последующем добавлении в программу глобальных объявлений.
 Using-объявления, необходимые для компиляции этого примера, таковы:
 using std::istream_iterator;
 using std::string;
 using std::cin;
 using std::vector;
 Но куда их поместить? Если программа состоит из большого количества файлов, можно для удобства создать заголовочный файл, содержащий все эти using-объявления, и включать его в исходные файлы вслед за заголовочными файлами стандартной библиотеки.
 В нашей книге мы не употребляли using-объявлений. Это сделано, во-первых, для того, чтобы сократить размер кода, а во-вторых, потому, что большинство примеров компилировались в реализации С++, не поддерживающей пространства имен. Подразумевается, что using-объявления указаны для всех членов пространства имен std, используемых в примерах.
 Упражнение 8.14
 Поясните разницу между using-объявлениями и using-директивами.
 Упражнение 8.15
 Напишите все необходимые using-объявления для примера из раздела 6.14.
 Упражнение 8.16
 Возьмем следующий фрагмент кода:
 namespace Exercise {
  int ivar = 0;
  double dvar = 0;
  const int limit = 1000;
 }
 int ivar = 0;
 
 //1
 void manip() {
  //2
  double dvar = 3.1416;
  int iobj = limit + 1;
  ++ivar;
  ++::ivar;
 }
 Каковы будут значения объявлений и выражений, если поместить using-объявления для всех членов пространства имен Exercise в точку //1? В точку //2? А если вместо using-объявлений использовать using-директиву?
 9
 9. Перегруженные функции
 Итак, мы уже знаем, как объявлять, определять и использовать функции в программах. В этой главе речь пойдет об их специальном виде – перегруженных функциях. Две функции называются перегруженными, если они имеют одинаковое имя, объявлены в одной и той же области видимости, но имеют разные списки формальных параметров. Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим вопрос об их разрешении, т.е. о том, какая именно из нескольких перегруженных функций вызывается во время выполнения программы. Эта проблема является одной из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно прочитать два раздела в конце главы, где тема преобразования типов аргументов и разрешения перегруженных функций раскрывается более подробно.
 9.1. Объявления перегруженных функций
 Теперь, научившись объявлять, определять и использовать функции в программах, познакомимся с перегрузкой – еще одним аспектом в C++. Перегрузка позволяет иметь несколько одноименных функций, выполняющих схожие операции над аргументами разных типов.
 Вы уже воспользовались предопределенной перегруженной функцией. Например, для вычисления выражения
 1 + 3
 вызывается операция целочисленного сложения, тогда как вычисление выражения
 1.0 + 3.0
 осуществляет сложение с плавающей точкой. Выбор той или иной операции производится незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить работу с операндами разных типов. Ответственность за распознавание контекста и применение операции, соответствующей типам операндов, возлагается на компилятор, а не на программиста.
 В этой главе мы покажем, как определять собственные перегруженные функции.
 9.1.1. Зачем нужно перегружать имя функции
 Как и в случае со встроенной операцией сложения, нам может понадобиться набор функций, выполняющих одно и то же действие, но над параметрами различных типов. Предположим, что мы хотим определить функции, возвращающие наибольшее из переданных значений параметров. Если бы не было перегрузки, пришлось бы каждой такой функции присвоить уникальное имя. Например, семейство функций max() могло бы выглядеть следующим образом:
 int i_max( int, int );
 int vi_max( const vector & );
 int matrix_max( const matrix & );
 Однако все они делают одно и то же: возвращают наибольшее из значений параметров. С точки зрения пользователя, здесь лишь одна операция – вычисление максимума, а детали ее реализации большого интереса не представляют.
 Отмеченная лексическая сложность отражает ограничение программной среды: всякое имя, встречающееся в одной и той же области видимости, должно относиться к уникальной сущности (объекту, функции, классу и т.д.). Такое ограничение на практике создает определенные неудобства, поскольку программист должен помнить или каким-то образом отыскивать все имена. Перегрузка функций помогает справиться с этой проблемой.
 Применяя перегрузку, программист может написать примерно так:
 int ix = max( j, k );
 vector vec;
 //...
 int iy = max( vec );
 Этот подход оказывается чрезвычайно полезным во многих ситуациях.
 9.1.2. Как перегрузить имя функции
 В C++ двум или более функциям может быть дано одно и то же имя при условии, что их списки параметров различаются либо числом параметров, либо их типами. В данном примере мы объявляем перегруженную функцию max():
 int max ( int, int );
 int max( const vector & );
 int max( const matrix & );
 Для каждого перегруженного объявления требуется отдельное определение функции max() с соответствующим списком параметров.
 Если в некоторой области видимости имя функции объявлено более одного раза, то второе (и последующие) объявление интерпретируется компилятором так:
 если списки параметров двух функций отличаются числом или типами параметров, то функции считаются перегруженными:
 // перегруженные функции
 void print( const string & );
 void print( vector & );
 если тип возвращаемого значения и списки параметров в объявлениях двух функций одинаковы, то второе объявление считается повторным:
 // объявления одной и той же функции
 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, но выполняют семантически различные действия. В этом случае лексическая сложность, связанная с употреблением различных имен, проистекает из принятого программистом соглашения об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но при этом оставить разные имена, отражающие смысл операции:
 #include
 class Date {
 public:
  set( int, int, int );
  Date& convert( const string & );
  void print();
 
  // ...
 };
 Приведем еще один пример. Следующие пять функций-членов Screen выполняют различные операции над экранным курсором, являющимся принадлежностью того же класса. Может показаться, что разумно перегрузить эти функции под общим названием move():
 Screen& moveHome();
 Screen& moveAbs( int, int );
 Screen& moveRel( int, int, char *direction );
 Screen& moveX( int );
 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
 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 ) видна
 }
 Поскольку каждый класс определяет собственную область видимости, функции, являющиеся членами двух разных классов, не перегружают друг друга. (Функции-члены класса описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается в главе 15.)
 Объявлять такие функции разрешается и внутри пространства имен. С каждым из них также связана отдельная область видимости, так что функции, объявленные в разных пространствах, не перегружают друг друга. Например:
 #include
 namespace IBM {
  extern void print( const string & );
  extern void print( double ); // перегружает print()
 }
 namespace Disney {
  // отдельная область видимости:
  // не перегружает функцию print() из пространства имен IBM
  extern void print( int );
 }
 Использование 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()
 {
  max( 87, 65 ); // вызывает libs_R_us::max( int, int )
  max( 35.5, 76.6 ); // вызывает libs_R_us::max( double, double )

<< Пред.           стр. 41 (из 121)           След. >>

Список литературы по разделу