ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 

Часть 8

                              Глава 6 Перегрузка Операций
           
                                                       Здесь водятся Драконы!
                                                            - старинная карта
           
             В этой  главе описывается аппарат,  предоставляемый в С++ для пе-
           регрузки операций.  Программист может определять смысл операций при
           их  применении  к  объектам  определенного класса.  Кроме арифмети-
           ческих,  можно определять еще и логические операции, операции срав-
           нения,  вызова () и индексирования [], а также можно переопределять
           присваивание и инициализацию. Можно определить явное и неявное пре-
           образование  между  определяемыми пользователем и основными типами.
           Показано, как определить класс, объект которого не может быть никак
           иначе скопирован или уничтожен кроме как специальными определенными
           пользователем функциями.
                6.1 Введение
           
             Часто программы работают с объектами, которые являются конкретны-
           ми представлениями абстрактных понятий.  Например, тип данных int в
           С++ вместе с операциями +,  -, *, / и т.д. предоставляет реализацию
           (ограниченную) математического понятия целых чисел.  Такие  понятия
           обычно включают в себя множество операций, которые кратко, удобно и
           привычно представляют основные действия над объектами. К сожалению,
           язык программирования может непосредственно поддерживать лишь очень
           малое число таких понятий. Например, такие понятия, как комплексная
           арифметика, матричная алгебра, логические сигналы и строки не полу-
           чили прямой поддержки в С++.  Классы дают средство  спецификации  в
           С++  представления  неэлементарных  объектов  вместе  с  множеством
           действий, которые могут над этими объектами выполняться. Иногда оп-
           ределение того, как действуют операции на объекты классов, позволя-
           ет программисту обеспечить более общепринятую и удобную запись  для
           манипуляции  объектами  классов,  чем  та,  которую  можно  достичь
           используя лишь основную функциональную запись. Например:
           
             class complex { double re, im;
             public:
                 complex(double r,  double i) { re=r;  im=i;  } friend complex
                 operator+(complex,       complex);       friend       complex
                 operator*(complex, complex);
             };
           
           определяет простую реализацию понятия комплексного числа, в которой
           число представляется парой чисел с плавающей  точкой  двойной  точ-
           ности,  работа с которыми осуществляется посредством операций + и *
           (и только). Программист задает смысл операций + и * с помощью опре-
           деления  функций с именами operator+ и operator*.  Если,  например,
           даны  b  и  c  типа  complex,  то  b+c  означает  (по  определению)
           operator+(b,c). Теперь есть возможность приблизить общепринятую ин-
           терпретацию комплексных выражений. Например:
           
             void f() {
                 complex a  = complex(1,  3.1);  complex b = complex(1.2,  2);
                 complex c = b;
           
                 a = b+c; b = b+c*a; c = a*b+complex(1,2);
             }
           
           Выполняются обычные  правила  приоритетов,  поэтому второй оператор
           означает b=b+(c*a), а не b=(b+c)*a.
                6.2 Функции Операции
           
             Можно описывать  функции,  определяющие значения следующих опера-
           ций:
           
             + - * / % ^ & | ~ !
             = < > += -= *= /= %= ^= &=
             |= << >> >>= <<= == != <= >= &&
             || ++ -- [] () new delete
           
             Последние четыре  -  это  индексирование  (#6.7),  вызов  функции
           (#6.8),  выделение свободной памяти и освобождение свободной памяти
           (#3.2.6).  Изменить  приоритеты  перечисленных операций невозможно,
           как невозможно изменить и синтаксис  выражений.  Нельзя,  например,
           определить унарную операцию % или бинарную !. Невозможно определить
           новые лексические символы операций,  но в тех случаях,  когда  мно-
           жество операций недостаточно,  вы можете использовать запись вызова
           функции.  Используйте например, не **, а pow(). Эти ограничения мо-
           гут  показаться драконовскими,  но более гибкие правила могут очень
           легко привести к неоднозначностям. Например, на первый взгляд опре-
           деление операции **,  означающей возведение в степень,  может пока-
           заться очевидной и простой задачей, но подумайте еще раз. Должна ли
           **  связываться  влево  (как в Фортране) или вправо (как в Алголе)?
           Выражение  a**p  должно  интерпретироваться  как  a*(*p)  или   как
           (a)**(p)?
           
             Имя функции операции есть ключевое слово operator (то есть,  опе-
           рация),  за которым следует сама  операция,  например,  operator<<.
           Функция  операция описывается и может вызываться так же,  как любая
           другая функция.  Использование операции - это лишь сокращенная  за-
           пись явного вызова функции операции. Например:
           
             void f(complex a, complex b) {
                 complex c =  a  +  b;  //  сокращенная  запись  complex  d  =
                 operator+(a,b); // явный вызов
             }
           
           При наличии предыдущего описания complex оба  инициализатора  явля-
           ются синонимами.
                6.2.1 Бинарные и Унарные Операции
           
             Бинарная операция может быть определена или как функция член, по-
           лучающая один параметр,  или как функция друг, получающая два пара-
           метра. Таким образом, для любой бинарной операции @ aa@bb может ин-
           терпретироваться     или     как    aa.operator@(bb),    или    как
           operator@(aa,bb).  Если определены обе,  то aa@bb является ошибкой.
           Унарная операция, префиксная или постфиксная, может быть определена
           или как функция член,  не получающая параметров,  или  как  функция
           друг,  получающая один параметр.  Таким образом,  для любой унарной
           операции  @  aa@  или  @aa   может   интерпретироваться   или   как
           aa.operator@(), или как operator@(aa). Если определено и то, и дру-
           гое, то и aa@, и @aa являются ошибками. Рассмотрим следующие приме-
           ры:
           
             class X {
             // друзья
           
                 friend X   operator-(X);   //   унарный   минус   friend    X
                 operator-(X,X);  //  бинарный минус friend X operator-();  //
                 ошибка:  нет операндов friend X operator-(X,X,X);  // ошибка:
                 тернарная
           
             // члены (с неявным первым параметром: this)
           
                 X* operator&();  // унарное & (взятие адреса) X operator&(X);
                 // бинарное & (операция И) X operator&(X,X);  // ошибка: тер-
                 нарное
           
             };
           
             Когда операции  ++  и -- перегружены,  префиксное использование и
           постфиксное различить невозможно.
                6.2.2 Предопределенный Смысл Операций
           
             Относительно смысла операций,  определяемых пользователем, не де-
           лается никаких предположений. В частности, поскольку не предполага-
           ется, что перегруженное = реализует присваивание ее первому операн-
           ду, не делается никакой проверки, чтобы удостовериться, является ли
           этот операнд lvalue (#с.6).
           
             Значения некоторых  встроенных операций определены как равносиль-
           ные определенным комбинациям других операций над теми же аргумента-
           ми. Например, если a является int, то ++a означает a+=1, что в свою
           очередь означает a=a+1.  Такие соотношения для определяемых пользо-
           вателем операций не выполняются,  если только не случилось так, что
           пользователь сам определил их таким образом.  Например, определение
           operator+=  () для типа complex не может быть выведено из определе-
           ний complex::operator+() и complex::operator=().
           
             По историческому совпадению операции =  и  &  имеют  определенный
           смысл для объектов классов.  Никакого элегантного способа "не опре-
           делить" эти две операции не существует.  Их можно,  однако, сделать
           недееспособными    для   класса   X.   Можно,   например,   описать
           X::operator&(),  не  задав  ее  определения.  Если  где-либо  будет
           браться адрес объекта класса X, то компоновщик обнаружит отсутствие
           определения*.  Или,  другой способ, можно определить X::operator&()
           так, чтобы она вызывала ошибку во время выполнения.
           
           ____________________
           * В некоторых системах компоновщик настолько "умен", что ругается,
           даже если  неопределена  неиспользуемая  функция.  В таких системах
           этим методом воспользоваться нельзя. (прим автора)
                6.2.3 Операции и Определяемые Пользователем Типы
           
             Функция операция должна или быть членом,  или получать в качестве
           параметра по меньшей мере один объект класса (функциям, которые пе-
           реопределяют операции new и delete,  это делать необязательно). Это
           правило гарантирует, что пользователь не может изменить смысл ника-
           кого  выражения,  не включающего в себя определенного пользователем
           типа. В частности, невозможно определить функцию, которая действует
           исключительно на указатели.
           
             Функция операция, первым параметром которой предполагается основ-
           ной встроенный тип, не может быть функцией членом. Рассмотрим, нап-
           ример, сложение комплексной переменной aa с целым 2: aa+2, при под-
           ходящим образом описанной функции члене,  может быть проинтерпрети-
           ровано  как  aa.operator+(2),  но с 2+aa это не может быть сделано,
           потому что нет такого класса int,  для которого можно было бы опре-
           делить + так,  чтобы это означало 2.operator+(aa). Даже если бы та-
           кой тип был, то для того, чтобы обработать и 2+aa и aa+2, понадоби-
           лось  бы  две различных функции члена.  Так как компилятор не знает
           смысла +,  определяемого пользователем,  то не может  предполагать,
           что он коммутативен,  и интерпретировать 2+aa как aa+2. С этим при-
           мером могут легко справиться функции друзья.
           
             Все функции операции по определению перегружены. Функция операция
           задает новый смысл операции в дополнение к встроенному определению,
           и может существовать несколько функций операций с одним  и  тем  же
           именем,  если в типах их параметров имеются отличия, различимые для
           компилятора, чтобы он мог различать их при обращении (см. #4.6.7).
                6.3 Определяемое Пользователем Преобразование Типа
           
             Приведенная во  введении реализация комплексных чисел слишком ог-
           раничена,  чтобы она могла устроить  кого-либо,  поэтому  ее  нужно
           расширить. Это будет в основном повторением описанных выше методов.
           Например:
           
             class complex { double re, im;
             public:
                 complex(double r, double i) { re=r; im=i; }
           
                 friend complex operator+(complex,  complex);  friend  complex
                 operator+(complex,  double); friend complex operator+(double,
                 complex);
           
                 friend complex operator-(complex,  complex);  friend  complex
                 operator-(complex,  double); friend complex operator-(double,
                 complex); complex operator-() // унарный -
           
                 friend complex operator*(complex,  complex);  friend  complex
                 operator*(complex,  double); friend complex operator*(double,
                 complex);
           
                 // ...
             };
           
             Теперь, имея описание complex, мы можем написать:
           
             void f() {
                 complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5); a = -b-c; b =
                 c*2.0*c; c = (d+e)*a;
             }
           
             Но писать функцию для каждого сочетания complex и double, как это
           делалось выше для operator+(),  невыносимо нудно. Кроме того, близ-
           кие к реальности средства комплексной арифметики должны  предостав-
           лять по меньшей мере дюжину таких функций. Посмотрите, например, на
           тип complex, описанный в .
                6.3.1 Конструкторы
           
             Альтернативу использованию   нескольких  функций  (перегруженных)
           составляет описание конструктора,  который по заданному double соз-
           дает complex. Например:
           
             class complex {
                 // ...
                 complex(double r) { re=r; im=0; }
             };
           
             Конструктор, требующий только один параметр,  необязательно вызы-
           вать явно:
           
             complex z1 = complex(23); complex z2 = 23;
           
           И z1, и z2 будут инициализированы вызовом complex(23).
           
             Конструктор - это предписание, как создавать значение данного ти-
           па.  Когда требуется значение типа,  и когда такое  значение  может
           быть создано конструктором,  тогда,  если такое значение дается для
           присваивания, вызывается конструктор. Например, класс complex можно
           было бы описать так:
           
             class complex { double re, im;
             public:
                 complex(double r, double i = 0) { re=r; im=i; }
           
                 friend complex  operator+(complex,  complex);  friend complex
                 operator*(complex, complex);
             };
           
           и действия,  в  которые  будут  входить  переменные complex и целые
           константы,  стали бы допустимы. Целая константа будет интерпретиро-
           ваться как complex с нулевой мнимой частью. Например, a=b*2 означа-
           ет:
           
             a=operator*( b, complex( double(2), double(0) ) )
           
             Определенное пользователем преобразование типа применяется неявно
           только тогда, когда оно является единственным.
           
             Объект, сконструированный  с  помощью  явного или неявного вызова
           конструктора,  является автоматическим и будет уничтожен при первой
           возможности, обычно сразу же после оператора, в котором он был соз-
           дан.
                6.3.2 Операции Преобразования
           
             Использование конструктора  для задания преобразования типа явля-
           ется удобным,  но имеет следствия,  которые могут оказаться нежела-
           тельными:
           
             [1] Не  может быть неявного преобразования из определенного поль-
                зователем типа в основной тип (поскольку основные типы не  яв-
                ляются классами)
           
             [2] Невозможно задать преобразование из нового типа в старый,  не
                изменяя описание старого
           
             [3] Невозможно иметь конструктор с одним параметром,  не имея при
                этом преобразования.
           
             Последнее не является серьезной проблемой, а с первыми двумя мож-
           но справиться,  определив для исходного типа операцию  преобразова-
           ния.  Функция  член X::operator T(),  где T - имя типа,  определяет
           преобразование из X в T.  Например, можно определить тип tiny (кро-
           шечный), который может иметь значение только в диапазоне 0...63, но
           все равно может свободно сочетаться в целыми в арифметических  опе-
           рациях:
           
             class tiny  {  char  v;  int assign(int i) { return v = (i&~63) ?
                 (error("ошибка диапазона"),0) : i; }
             public:
                 tiny(int i) { assign(i);  } tiny(tiny& i) { v =  t.v;  }  int
                 operator=(tiny& i) { return v = t.v; } int operator=(int i) {
                 return assign(i); } operator int() { return v; }
             }
           
             Диапазон значения проверяется всегда, когда tiny инициализируется
           int, и всегда, когда ему присваивается int. Одно tiny может присва-
           иваться  другому без проверки диапазона.  Чтобы разрешить выполнять
           над  переменными  tiny   обычные   целые   операции,   определяется
           tiny::operator int(), неявное преобразование из tiny в int. Всегда,
           когда в том месте, где требуется int, появляется tiny, используется
           соответствующее ему int. Например:
           
             void main() {
                 tiny c1 = 2; tiny c2 = 62; tiny c3 = c2 - c1; // c3 = 60 tiny
                 c4 = c3; // нет проверки диапазона (необязательна) int i = c1
                 + c2; // i = 64 c1 = c2 + 2 * c1; // ошибка диапазона: c1 = 0
                 (а не 66) c2 = c1 -i; // ошибка диапазона: c2 = 0 c3 = c2; //
                 нет проверки диапазона (необязательна)
             }
             Тип вектор из tiny может оказаться более полезным,  поскольку  он
           экономит пространство. Чтобы сделать этот тип более удобным в обра-
           щении, можно использовать операцию индексирования.
           
             Другое применение определяемых операций преобразования - это  ти-
           пы,  которые предоставляют нестандартные представления чисел (ариф-
           метика по основанию 100,  арифметика,  арифметика  с  фиксированной
           точкой,  двоично-десятичное представление и т.п.).  При этом обычно
           переопределяются такие операции, как + и *.
           
             Функции преобразования оказываются особенно полезными для  работы
           со структурами данных, когда чтение (реализованное посредством опе-
           рации преобразования) тривиально,  в то время  как  присваивание  и
           инициализация заметно более сложны.
           
             Типы istream и ostream опираются на функцию преобразования, чтобы
           сделать возможными такие операторы, как
           
             while (cin>>x) cout<>x выше возвращает istream&.  Это значение не-
           явно преобразуется к значению,  которое указывает состояние cin,  а
           уже это значение может проверяться оператором while  (см.  #8.4.2).
           Однако  определять  преобразование из оного типа в другой так,  что
           при этом теряется информация, обычно не стоит.
                6.3.3 Неоднозначности
           
             Присваивание объекту  (или  инициализация объекта) класса X явля-
           ется допустимым,  если или присваиваемое значение является  X,  или
           существует  единственное  преобразование  присваиваемого значения в
           тип X.
           
             В некоторых случаях значение нужного типа может быть построено  с
           помощью нескольких применений конструкторов или операций преобразо-
           вания. Это должно делаться явно; допустим только один уровень неяв-
           ных  преобразований,  определенных  пользователем.  Иногда значение
           нужного типа может быть построено более чем одним  способом.  Такие
           случаи являются недопустимыми. Например:
           
             class x { /* ...  */ x(int);  x(char*);  };  class y { /* ...  */
             y(int); }; class z { /* ... */ z(x); };
           
             overload f; x f(x); y f(y);
           
             z g(z);
           
             f(1); //  недопустимо:  неоднозначность   f(x(1))   или   f(y(1))
             f(x(1));  f(y(1));  g("asdf"); // недопустимо: g(z(x("asdf"))) не
             пробуется g(z("asdf"));
           
             Определяемые пользователем преобразования рассматриваются  только
           в том случае, если без них вызов разрешить нельзя. Например:
           
             class x { /* ... */ x(int); } overload h(double), h(x); h(1);
           
             Вызов мог  бы  быть проинтерпретирован или как h(double(1)),  или
           как h(x(1)), и был бы недопустим по правилу единственности. Но пер-
           вая  интерпретация  использует  только стандартное преобразование и
           она будет выбрана по правилам, приведенным в #4.6.7.
             Правила преобразования  не являются ни самыми простыми для реали-
           зации и документации, ни наиболее общими из тех, которые можно было
           бы  разработать.  Возьмем требование единственности преобразования.
           Более общий подход разрешил бы компилятору применять любое преобра-
           зование,  которое он сможет найти;  таким образом, не нужно было бы
           рассматривать все возможные преобразования перед тем,  как объявить
           выражение допустимым. К сожалению, это означало бы, что смысл прог-
           раммы зависит от того, какое преобразование было найдено. В резуль-
           тате  смысл  программы неким образом зависел бы от порядка описания
           преобразований.  Поскольку они часто находятся  в  разных  исходных
           файлах (написанных разными людьми),  смысл программы будет зависеть
           от порядка компоновки этих частей вместе.  Есть  другой  вариант  -
           запретить  все неявные преобразования.  Нет ничего проще,  но такое
           правило приведет либо к неэлегантным пользовательским  интерфейсам,
           либо к бурному росту перегруженных функций, как это было в предыду-
           щем разделе с complex.
           
             Самый общий подход учитывал бы всю имеющуюся информацию о типах и
           рассматривал   бы  все  возможные  преобразования.  Например,  если
           использовать предыдущее  описание,  то  можно  было  бы  обработать
           aa=f(1),  так как тип aa определяет единственность толкования. Если
           aa является x, то единственное, дающее в результате x, который тре-
           буется присваиванием,  - это f(x(1)),  а если aa - это y, то вместо
           этого будет использоваться f(y(1)). Самый общий подход справился бы
           и  с  g("asdf"),  поскольку единственной интерпретацией этого может
           быть g(z(x("asdf"))). Сложность этого подхода в том, что он требует
           расширенного анализа всего выражения для того, чтобы определить ин-
           терпретацию каждой операции и вызова функции. Это приведет к замед-
           лению  компиляции,  а также к вызывающим удивление интерпретациям и
           сообщениям об ошибках,  если компилятор рассмотрит  преобразования,
           определенные в библиотеках и т.п.  При таком подходе компилятор бу-
           дет принимать во внимание больше, чем, как можно ожидать, знает пи-
           шущий программу программист!
           
                6.4 Константы
           
             Константы классового  типа определить невозможно в том смысле,  в
           каком 1.2 и 12e3 являются константами типа double.  Вместо них, од-
           нако,  часто  можно использовать константы основных типов,  если их
           реализация обеспечивается с помощью функций членов.  Общий  аппарат
           для  этого  дают  конструкторы,  получающие  один  параметр.  Когда
           конструкторы просты и подставляются inline, имеет смысл рассмотреть
           в   качестве   константы  вызов  конструктора.  Если,  например,  в
              есть   описание   класса   comlpex,   то    выражение
           zz1*3+zz2*comlpex(1,2) даст два вызова функций,  а не пять.  К двум
           вызовам функций приведут две операции *,  а операция + и  конструк-
           тор,  к которому обращаются для создания comlpex(3) и comlpex(1,2),
           будут расширены inline.
                6.5 Большие Объекты
           
             При каждом  применении  для comlpex бинарных операций,  описанных
           выше,  в функцию,  которая реализует операцию, как параметр переда-
           ется копия каждого операнда.  Расходы на копирование каждого double
           заметны,  но с ними вполне можно примириться.  К сожалению,  не все
           классы имеют небольшое и удобное представление.  Чтобы избежать не-
           нужного копирования, можно описать функции таким образом, чтобы они
           получали ссылочные параметры. Например:
           
             class matrix { double m[4][4];
             public:
                 matrix(); friend matrix operator+(matrix&,  matrix&);  friend
                 matrix operator*(matrix&, matrix&);
             };
           
             Ссылки позволяют использовать выражения, содержащие обычные ариф-
           метические операции над большими объектами, без ненужного копирова-
           ния. Указатели применять нельзя, потому что невозможно для примене-
           ния к указателю смысл операции переопределить невозможно.  Операцию
           плюс можно определить так:
           
             matrix operator+(matrix&, matrix&); {
                 matrix sum; for (int i=0; i<4; i++)
                     for (int  j=0;  j<4;  j++)  sum.m[i][j]  = arg1.m[i][j] +
                         arg2.m[i][j];
                 return sum;
             }
           
             Эта operator+() обращается к операндам + через ссылки, но возвра-
           щает значение объекта.  Возврат ссылки может оказаться более эффек-
           тивным:
           
             class matrix {
                 // ...
                 friend matrix& operator+(matrix&,  matrix&);  friend  matrix&
                 operator*(matrix&, matrix&);
             };
           
             Это является допустимым, но приводит к сложности с выделением па-
           мяти.  Поскольку  ссылка на результат будет передаваться из функции
           как ссылка на возвращаемое значение,  оно не может  быть  автомати-
           ческой переменной.  Поскольку часто операция используется в выраже-
           нии больше одного раза, результат не может быть и статической пере-
           менной.  Как правило, его размещают в свободной памяти. Часто копи-
           рование возвращаемого значения оказывается дешевле (по времени  вы-
           полнения, объему кода и объему данных) и проще программируется.
                6.6 Присваивание и Инициализация
           
             Рассмотрим очень простой класс строк string:
           
             struct string { char* p;  int size; // размер вектора, на который
                 указывает p
           
                 string(int sz) { p = new char[size=sz]; }
                 ~string() { delete p; }
             };
           
             Строка -  это  структура данных,  состоящая из вектора символов и
           длины этого вектора.  Вектор создается конструктором и уничтожается
           деструктором.  Однако,  как показано в #5.10,  это может привести к
           неприятностям. Например:
           
             void f() {
                 string s1(10); string s2(20); s1 = s2;
             }
           
           будет размещать два вектора символов,  а присваивание  s1=s2  будет
           портить указатель на один из них и дублировать другой. На выходе из
           f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот
           же  вектор с непредсказуемо разрушительными последствиями.  Решение
           этой проблемы состоит в том,  чтобы соответствующим образом опреде-
           лить присваивание объектов типа string:
           
             struct string { char* p;  int size; // размер вектора, на который
                 указывает p
           
                 string(int sz) { p = new char[size=sz]; }
                 ~string() { delete p; }
                 void operator=(string&)
             };
           
             void string::operator=(string& a) {
                 if (this == &a) return;  // остерегаться s=s; delete p; p=new
                 char[size=a.size]; strcpy(p,a.p);
             }
           
             Это определение string гарантирует,и что предыдущий пример  будет
           работать как предполагалось.  Однако небольшое изменение f() приве-
           дет к появлению той же проблемы в новом облике:
           
             void f() {
                 string s1(10); s2 = s1;
             }
             Теперь создается только одна строка, а уничтожается две. К неини-
           циализированному  объекту   определяемая   пользователем   операция
           присваивания  не применяется.  Беглый взгляд на string::operator=()
           объясняет,  почему было бы неразумно так делать:  указатель p будет
           содержать  неопределенное  и  совершенно случайное значение.  Часто
           операция присваивания полагается на то,  что ее аргументы инициали-
           зированы. Для такой инициализации, как здесь, это не так по опреде-
           лению. Следовательно, нужно определить похожую, но другую, функцию,
           чтобы обрабатывать инициализацию:
           
             struct string { char* p;  int size; // размер вектора, на который
                 указывает p
           
                 string(int sz) { p = new char[size=sz]; }
                 ~string() { delete p; }
                 void operator=(string&); string(string&);
             };
           
             void string::string(string& a) {
                 p=new char[size=a.size]; strcpy(p,a.p);
             }
           
             Для типа  X инициализацию тем же типом X обрабатывает конструктор
           X(X&).  Нельзя не подчеркнуть еще раз, что присваивание и инициали-
           зация  -  разные  действия.  Это  особенно существенно при описании
           деструктора. Если класс X имеет конструктор X(X&), выполняющий нет-
           ривиальную работу вроде освобождения памяти, то скорее всего потре-
           буется полный комплект функций, чтобы полностью избежать побитового
           копирования объектов:
           
             class X {
                 // ...
                 X(something); //   конструктор:   создает  объект  X(&X);  //
                 конструктор:  копирует  в  инициализации  operator=(X&);   //
                 присваивание: чистит и копирует ~X(); // деструктор: чистит
             };
           
             Есть еще два случая,  когда объект копируется: как параметр функ-
           ции и как возвращаемое значение. Когда передается параметр, инициа-
           лизируется неинициализированная до этого  переменная  -  формальный
           параметр.  Семантика идентична семантике инициализации. То же самое
           происходит при возврате из функции, хотя это менее очевидно. В обо-
           их случаях будет применен X(X&), если он определен:
           
             string g(string arg) {
                 return arg;
             }
           
             main() {
                 string s = "asdf"; s = g(s);
             }
             Ясно, что после вызова g() значение s обязано быть "asdf".  Копи-
           рование  значения  s в параметр arg сложности не представляет:  для
           этого надо взывать string(string&). Для взятия копии этого значения
           из g() требуется еще один вызов string(string&); на этот раз иници-
           ализируемой является временная переменная, которая затем присваива-
           ется s.  Такие переменные, естественно, уничтожаются как положено с
           помощью string::~string() при первой возможности.
           
                6.7 Индексирование
           
             Чтобы задать смысл индексов  для  объектов  класса,  используется
           функция operator[]. Второй параметр (индекс) функции operator[] мо-
           жет быть любого типа. Это позволяет определять ассоциативные масси-
           вы  и т.п.  В качестве примера давайте перепишем пример из #2.3.10,
           где при написании небольшой программы для подсчета числа  вхождений
           слов  в  файле применялся ассоциативный массив.  Там использовалась
           функция. Здесь определяется надлежащий тип ассоциативного массива:
           
             struct pair { char* name; int val;
             };
           
             class assoc { pair* vec; int max; int free;
             public:
                 assoc(int); int& operator[](char*); void print_all();
             };
           
             В assoc хранится вектор пар pair длины max.  Индекс  первого  не-
           использованного элемента вектора находится в free. Конструктор выг-
           лядит так:
           
             assoc::assoc(int s) {
                 max = (s<16) ? s : 16; free = 0; vec = new pair[max];
             }
             При реализации применяется все тот же простой и неэффективный ме-
           тод поиска,  что использовался в #2.3.10.  Однако при  переполнении
           assoc увеличивается:
           
             #include 
           
             int assoc::operator[](char* p)
             /*
                 работа с множеством пар "pair":
                 поиск p,  возврат ссылки на целую часть его "pair" делает но-
                 вую "pair", если p не встречалось
             */
             { register pair* pp;
           
                 for (pp=&vec[free-1];         vec<=pp;        pp--)        if
                     (strcmp(p,pp->name)==0) return pp->val;
           
                 if (free==max) { // переполнение:  вектор увеличивается pair*
                     nvec  =  new  pair[max*2];  for  ( int i=0;  iname   =   new  char[strlen(p)+1];
                 strcpy(pp->name,p);  pp->val = 0;  // начальное  значение:  0
                 return pp->val;
             }
           
             Поскольку представление assoc скрыто,  нам нужен способ его печа-
           ти.  В следующем разделе будет показано,  как определить подходящий
           итератор, а здесь мы используем простую функцию печати:
           
             vouid assoc::print_all() {
                 for (int i = 0;  i>buf)  vec[buf]++;
                 vec.print_all();
             }
                6.8 Вызов Функции
           
             Вызов функции,  то есть запись выражение(список_выражений), можно
           проинтерпретировать как бинарную операцию,  и операцию вызова можно
           перегружать так же,  как и другие операции. Список параметров функ-
           ции  operator() вычисляется и проверяется в соответствие с обычными
           правилами передачи параметров.  Перегружающая  функция  может  ока-
           заться  полезной главным образом для определения типов с единствен-
           ной операцией и для типов, у которых одна операция настолько преоб-
           ладает,  что  другие  в  большинстве ситуаций можно не принимать во
           внимание.
           
             Для типа ассоциативного массива assoc мы не определили  итератор.
           Это можно сделать,  определив класс assoc_iterator, работа которого
           состоит в том,  чтобы в определенном порядке поставлять элементы из
           assoc.  Итератору нужен доступ к данным,  которые хранятся в assoc,
           поэтому он сделан другом:
           
             class assoc { friend class assoc_iterator;
                 pair* vec; int max; int free;
             public:
                 assoc(int); int& operator[](char*);
             };
           
           Итератор определяется как
           
             class assoc_iterator{ assoc* cs;  // текущий массив assoc int  i;
                 // текущий индекс
             public:
                 assoc_iterator(assoc& s)   {   cs  =  &s;  i  =  0;  }  pair*
                 operator()()
                     { return (ifree)? &cs->vec[i++] : 0; }
             };
           
             Надо инициализировать assoc_iterator для массива assoc, после че-
           го  он  будет  возвращать  указатель на новую pair из этого массива
           всякий раз,  когда его будут активизировать операцией (). По дости-
           жении конца массива он возвращает 0:
           
             main() // считает вхождения каждого слова во вводе {
                 const MAX  =  256;  //  больше  самого  большого  слова  char
                 buf[MAX];   assoc   vec(512);  while  (cin>>buf)  vec[buf]++;
                 assoc_iterator next(vec); pair* p; while ( p = next() )
                     cout << p->name << ": " << p->val << "\n";
             }
             Итераторный тип  вроде  этого  имеет  преимущество  перед набором
           функций,  которые выполняют ту же работу:  у него есть  собственные
           закрытые  данные  для хранения хода итерации.  К тому же обычно су-
           щественно, чтобы одновременно могли работать много итераторов этого
           типа.
           
             Конечно, такое  применение  объектов для представления итераторов
           никак особенно с перегрузкой  операций  не  связано.  Многие  любят
           использовать итераторы с такими операциями,  как first(),  next() и
           last() (первый, следующий и последний).
           
                6.9 Класс String
           
             Вот довольно реалистичный пример класса строк string.  В нем про-
           изводится  учет ссылок на строку с целью минимизировать копирование
           и в качестве констант  применяются  стандартные  символьные  строки
           С++.
           
             #include 
             #include 
           
             class string { struct srep {
                     char* s; // указатель на данные int n; // счетчик ссылок
             };
                 srep *p;
           
             public:
                 string(char *);  // string x = "abc" string();  // string  x;
                 string(string   &);   //   string  x  =  string  ...  string&
                 operator=(char *);  string& operator=(string  &);  ~string();
                 char& operator[](int i);
           
                 friend ostream&    operator<<(ostream&,    string&);   friend
                 istream& operator>>(istream&, string&);
           
                 friend int   operator==(string&   x,   char*    s)    {return
                     strcmp(x.p->s, s) == 0; }
           
                 friend int   operator==(string&   x,   string&   y)   {return
                     strcmp(x.p->s, y.p->s) == 0; }
           
                 friend int   operator!=(string&   x,   char*    s)    {return
                     strcmp(x.p->s, s) != 0; }
           
                 friend int   operator!=(string&   x,   string&   y)   {return
                     strcmp(x.p->s, y.p->s) != 0; }
           
             };
           Конструкторы и деструкторы просты (как обычно):
           
             string::string() {
                 p = new srep; p->s = 0; p->n = 1;
             }
           
             string::string(char* s) {
                 p = new srep;  p->s = new char[ strlen(s)+1  ];  strcpy(p->s,
                 s); p->n = 1;
             }
           
             string::string(string& x) {
                 x.p->n++; p = x.p;
             }
           
             string::~string() {
                 if (--p->n == 0) { delete p->s; delete p;
                 }
             }
           
             Как обычно,  операции  присваивания очень похожи на конструкторы.
           Они должны обрабатывать очистку своего первого (левого) операнда:
           
             string& string::operator=(char* s) {
                 if (p->n > 1) { // разъединить себя p->n--; p = new srep;
                 }
                 else if (p->n == 1) delete p->s;
           
                 p->s = new char[ strlen(s)+1 ];  strcpy(p->s,  s);  p->n = 1;
                 return *this;
             }
           
             Благоразумно обеспечить,  чтобы  присваивание объекта самому себе
           работало правильно:
           
             string& string::operator=(string& x) {
                 x.p->n++; if (--p->n == 0) {
                     delete p->s; delete p;
                 }
                 p = x.p; return *this;
             }
             Операция вывода задумана так, чтобы продемонстрировать применение
           учета ссылок.  Она повторяет каждую вводимую строку (с помощью опе-
           рации <<, которая определяется позднее):
           
             ostream& operator<<(ostream& s, string& x) {
                 return s << x.p->s << " [" << x.p->n << "]\n";
             }
           
             Операция ввода использует стандартную  функцию  ввода  символьной
           строки (#8.4.1).
           
             istream& operator>>(istream& s, string& x) {
                 char buf[256];  s >> buf;  x = buf;  cout << "echo: " << x <<
                 "\n"; return s;
             }
           
             Для доступа к отдельным символам предоставлена операция  индекси-
           рования. Осуществляется проверка индекса:
           
             void error(char* p) {
                 cerr << p << "\n"; exit(1);
             }
           
             char& string::operator[](int i) {
                 if (i<0  ||  strlen(p->s)s[i];
             }
           
             Головная программа просто немного опробует действия над строками.
           Она  читает  слова со ввода в строки,  а потом эти строки печатает.
           Она продолжает это делать до тех пор,  пока  не  распознает  строку
           done,  которая  завершает  сохранение  слов в строках,  или пока не
           встретит конец файла.  После этого она печатает строки  в  обратном
           порядке и завершается.
           
             main() {
                 string x[100]; int n;
           
                 cout << "отсюда начнем\n"; for (n = 0; cin>>x[n]; n++) {
                     string y;  if (n==100) error("слишком много строк"); cout
                     << (y = x[n]); if (y=="done") break;
                 }
                 cout << "отсюда мы пройдем обратно\n";  for (int i=n-1; 0<=i;
                 i--) cout << x[i];
             }
                6.10 Друзья и Члены
           
             Теперь, наконец,  можно  обсудить,  в каких случаях для доступа к
           закрытой части определяемого пользователем типа использовать члены,
           а  в  каких  -  друзей.  Некоторые  операции  должны  быть членами:
           конструкторы, деструкторы и виртуальные функции (см. следующую гла-
           ву), но обычно это зависит от выбора.
           
             Рассмотрим простой класс X:
           
             class X {
                 // ...
                 X(int); int m(); friend int f(X&);
             };
           
             Внешне не видно никаких причин делать f(X&) другом  дополнительно
           к  члену  X::m()  (или  наоборот),  чтобы  реализовать действия над
           классом X. Однако член X::m() можно вызывать только для "настоящего
           объекта",  в  то  время  как друг f() может вызываться для объекта,
           созданного с помощью неявного преобразования типа. Например:
           
             void g() {
                 1.m(); // ошибка f(1); // f(x(1));
             }
           
             Поэтому операция,  изменяющая состояние объекта, должна быть чле-
           ном,  а  не другом.  Для определяемых пользователем типов операции,
           требующие в случае фундаментальных типов операнд lvalue (=, *=, ++,
           *= и т.д.), наиболее естественно определяются как члены.
           
             И наоборот, если нужно иметь неявное преобразование для всех опе-
           рандов операции, то реализующая ее функция должна быть другом, а не
           членом. Это часто имеет место для функций, которые реализуют опера-
           ции,  не требующие при применении к фундаментальным типам lvalue  в
           качестве операндов (+, -, || и т.д.).
           
             Если никакие  преобразования типа не определены,  то оказывается,
           что нет никаких существенных оснований в пользу  члена,  если  есть
           друг,  который получает ссылочный параметр, и наоборот. В некоторых
           случаях программист может предпочитать один синтаксис вызова друго-
           му. Например, оказывается, что большинство предпочитает для обраще-
           ния матрицы m запись m.inv(). Конечно, если inv() действительно об-
           ращает матрицу m, а не просто возвращает новую матрицу, обратную m,
           ей следует быть членом.
           
             При прочих равных условиях выбирайте, чтобы функция была членом:
           никто не знает, вдруг когда-нибудь кто-то определит операцию преоб-
           разования.  Невозможно предсказать,  потребуют ли будущие изменения
           изменять  состояние  объекта.  Синтаксис  вызова функции члена ясно
           указывает пользователю,  что объект можно изменить; ссылочный пара-
           метр  является далеко не столь очевидным.  Кроме того,  выражения в
           члене могут быть заметно короче выражений в друге.  В функции друге
           надо использовать явный параметр, тогда как в члене можно использо-
           вать неявный this.  Если только не  применяется  перегрузка,  имена
           членов обычно короче имен друзей.
                6.11 Предостережение
           
             Как и большую часть возможностей в языках  программирования,  пе-
           регрузку  операций  можно использовать как правильно,  так и непра-
           вильно. В частности, можно так воспользоваться возможностью опреде-
           лять  новые  значения старых операций,  что они станут почти совсем
           непостижимы. Представьте, например, с какими сложностями столкнется
           человек,  читающий программу, в которой операция + была переопреде-
           лена для обозначения вычитания.
           
             Изложенный аппарат должен уберечь программиста/читателя от худших
           крайностей применения перегрузки,  потому что программист предохра-
           нен от изменения значения операций для основных типов данных  вроде
           int,  а также потому, что синтаксис выражений и приоритеты операций
           сохраняются.
           
             Может быть, разумно применять перегрузку операций главным образом
           так,  чтобы подражать общепринятому применению операций. В тех слу-
           чаях, когда нет общепринятой операции или имеющееся в С++ множество
           операций  не подходит для имитации общепринятого применения,  можно
           использовать запись вызова функции.
           
                6.12 Упражнения
           
             1. (*2) Определите итератор для класса string.  Определите опера-
                цию конкатенации + и операцию "добавить в конец" +=. Какие еще
                операции над string вы хотели бы  иметь  возможность  осущест-
                влять?
           
             2. (*1.5)  Задайте  с  помощью  перегрузки  () операцию выделения
                подстроки для класса строк.
           
             3. (*3) Постройте класс  string  так,  чтобы  операция  выделения
                подстроки могла использоваться в левой части присваивания. На-
                пишите сначала версию,  в которой строка  может  присваиваться
                подстроке  той же длины,  а потом версию,  где эти длины могут
                быть разными.
           
             4. (*2) Постройте класс string так, чтобы для присваивания, пере-
                дачи параметров и т.п. он имел семантику по значению, то есть,
                когда копируется строковое представление, а не просто управля-
                ющая структура данных класса sring.
           
             5. (*3)  Модифицируйте  класс string из предыдущего примера таким
                образом,  чтобы строка копировалась только когда это необходи-
                мо. То есть, храните совместно используемое представление двух
                строк, пока одна из этих строк не будет изменена. Не пытайтесь
                одновременно с этим иметь операцию выделения подстроки,  кото-
                рая может использоваться в левой части.
           
             6. (*4) Разработайте класс string с семантикой по значению, копи-
                рованием с задержкой и операцией подстроки, которая может сто-
                ять в левой части.
             7. (*2) Какие преобразования используются в каждом выражении сле-
                дующей программы:
           
                  struct X { int i; X(int); operator+(int);
                  };
           
                  struct Y { int i; Y(X); operator+(X); operator int();
                  };
           
                  X operator* (X,Y); int f(X);
           
                  X x = 1; Y y = x; int i = 2;
           
                  main() {
                     i + 10;  y + 10;  y + 10 * y; x + y + i; x * x + i; f(7);
                     f(y); y + y; 106 + y;
                  }
           
                Определите X и Y так,  чтобы они оба были целыми типами. Изме-
                ните программу так,  чтобы она работала  и  печатала  значения
                всех допустимых выражений.
           
             8. (*2)  Определите класс INT,  который ведет себя в точности как
                int. Подсказка: определите INT::operator int().
           
             9. (*1) Определите класс RINT,  который ведет себя в точности как
                int за исключением того, что единственные возможные операции -
                это + (унарный и бинарный),  - (унарный и бинарный),  *, /, %.
                Подсказка: не определяйте INT::operator int().
           
             10. (*3) Определите класс LINT,  ведущий себя как RINT, за исклю-
                чением того, что имеет точность не менее 64 бит.
           
             11. (*4) Определите класс,  который реализует арифметику с произ-
                вольной точностью.  Подсказка: вам надо управлять памятью ана-
                логично тому, как это делалось для класса string.
             12. (*2) Напишите программу,  доведенную до нечитаемого состояния
                с помощью макросов и перегрузки операций. Вот идея: определите
                для INT + так,  чтобы он означал -,  и наоборот, а потом с по-
                мощью макроопределения определите int как INT. Переопределение
                часто употребляемых функций,  использование параметров ссылоч-
                ного типа и несколько вводящих в заблуждение комментариев  по-
                могут устроить полную неразбериху.
           
             13. (*3)  Поменяйтесь со своим другом программами,  которые у вас
                получились в предыдущем упражнении. Не запуская ее попытайтесь
                понять,  что  делает программа вашего друга.  После выполнения
                этого упражнения вы будете знать, чего следует избегать.
           
             14. (*2) Перепишите примеры с comlpex (#6.3.1),  tiny (#6.3.2)  и
                string (#6.9) не используя friend функций.  Используйте только
                функции члены.  Протестируйте каждую из новых версий. Сравните
                их с версиями,  в которых используются функции друзья. Еще раз
                посмотрите Упражнение 5.3.
           
             15. (*2) Определите тип vec4 как вектор их четырех float. Опреде-
                лите operator[] для vec4.  Определите операции +,  -, *, /, =,
                +=, -=, *=, /= для сочетаний векторов и чисел с плавающей точ-
                кой.
           
             16. (*3) Определите класс mat4 как вектор из четырех vec4.  Опре-
                делите для mat4 operator[],  возвращающий vec4. Определите для
                этого типа обычные операции над матрицами. Определите функцию,
                выполняющие для mat4 исключение Гаусса.
           
             17. (*2) Определите класс vector,  аналогичный vec4, но с длиной,
                которая      задается      как      параметр      конструктора
                vector::vector(int).
           
             18. (*3) Определите класс matrix,  аналогичный mat4, но с размер-
                ностью,        задаваемой       параметрами       конструктора
                matrix::matrix(int,int).


Яндекс цитирования