Бьерн Страуструп - Язык программирования С++. Главы 5-7

ОГЛАВЛЕНИЕ

Главы 5 - 7 посвящены средствам построения новых типов, которые не имеют аналогов в С. В главе 5 вводится основное понятие - класс. В ней показано, как можно определять пользовательские типы (классы), инициализировать их, обращаться к ним, и, наконец, как уничтожать их. Глава 6 посвящена понятию производных классов, которое позволяет строить из простых классов более сложные. Оно дает также возможность эффективной и безопасной (в смысле типа) работы в тех ситуациях, когда типы объектов на стадии трансляции неизвестны.  В главе 7 объясняется, как можно определить унарные и бинарные операции для пользовательских типов, как задавать преобразования таких типов, и каким образом можно создавать, копировать и удалять объекты, представляющие пользовательские типы.


ГЛАВА 5. КЛАССЫ

        "Эти типы не абстрактные, они столь же реальны, как int и float"
              - Даг Макилрой

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

Следующие три главы завершают описание возможностей С++ для построения новых типов, и в них содержится больше интересных примеров.

5.1 Введение и краткий обзор

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

Тип есть вполне конкретное представление некоторого понятия. Например, в С++ тип float с операциями +, -, * и т.д. является хотя и ограниченным, но конкретным представлением математического понятия вещественного числа. Новый тип создается для того, чтобы стать специальным и конкретным представлением понятия, которое не находит прямого и естественного отражения среди встроенных типов. Например, в программе из области телефонной связи можно ввести тип trunk_module (линия-связи), в видеоигре - тип explosion (взрыв), а в программе, обрабатывающей текст, - тип list_of_paragraphs (список-параграфов). Обычно проще понимать и изменять программу, в которой типы хорошо представляют используемые в задаче понятия. Удачно подобранное множество пользовательских типов делает программу более ясной. Оно позволяет транслятору обнаруживать недопустимое использование объектов, которое в противном случае останется невыявленным до отладки программы.

Главное в определении нового типа - это отделить несущественные детали реализации (например, расположение данных в объекте нового типа) от тех его характеристик, которые существенны для правильного его использования (например, полный список функций, имеющих доступ к данным). Такое разделение обеспечивается тем, что вся работа со структурой данных и внутрение, служебные операции над нею доступны только через специальный интерфейс (через "одно горло").

Глава состоит из четырех частей:

   $$5.2 Классы и члены. Здесь вводится основное понятие пользовательского типа, называемого классом. Доступ к объектам класса может ограничиваться множеством функций, описания которых входят в описание класса. Эти функции называются функциями-членами и друзьями. Для создания объектов класса используются специальные функции-члены, называемые конструкторами. Можно описать специальную функцию-член для удаления объектов класса при его уничтожении. Такая функция называется деструктором.

   $$5.3 Интерфейсы и реализации. Здесь приводятся два примера разработки, реализации и использования классов.

   $$5.4 Дополнительные свойства классов. Здесь приводится много дополнительных подробностей о классах. Показано, как функции, не являющейся членом класса, предоставить доступ к его частной части. Такую функцию называют другом класса. Вводятся понятия статических членов класса и указателей на члены класса. Здесь же показано, как определить дискриминирующее объединение.

   $$5.5 Конструкторы и деструкторы. Объект может создаваться как автоматический, статический или как объект в свободной памяти. Кроме того, объект может быть членом некоторого агрегата (массива или другого класса), который тоже можно размещать одним из этих трех способов. Подробно объясняется использование конструкторов и деструкторов, описывается применение определяемых пользователем функций размещения в свободной памяти и функций освобождения памяти.


5.2 Классы и члены

Класс - это пользовательский тип. Этот раздел знакомит с основными средствами определения класса, создания его объектов, работы с такими объектами и, наконец, удаления этих объектов после использования.


 

5.2.1 Функции-члены

Посмотрим, как можно представить в языке понятие даты, используя для этого тип структуры и набор функций, работающих с переменными этого типа:
       struct date { int month, day, year; };
       date today;
       void set_date(date*, int, int, int);
       void next_date(date*);
       void print_date(const date*);
       // ...
Никакой явной связи между функциями и структурой date нет. Ее можно установить, если описать функции как члены структуры:
       struct date {
           int month, day, year;

           void set(int, int, int);
           void get(int*, int* int*);
           void next();
           void print();
       };
Описанные таким образом функции называются функциями-членами. Их можно вызывать только через переменные соответствующего типа, используя стандартную запись обращения к члену структуры:
         date today;
         date my_birthday;

         void f()
         {
           my_birthday.set(30,12,1950);
           today.set(18,1,1991);

           my_birthday.print();
           today.next();
          }
Поскольку разные структуры могут иметь функции-члены с одинаковыми именами, при определении функции-члена нужно указывать имя структуры:
         void date::next()
         {
           if (++day > 28 ) {
              // здесь сложный вариант
           }
         }
В теле функции-члена имена членов можно использовать без указания имени объекта. В таком случае имя относится к члену того объекта, для которого была вызвана функция.



5.2.2 Классы

Мы определили несколько функций для работы со структурой date, но из ее описания не следует, что это единственные функции, которые предоставляют доступ к объектам типа date. Можно установить такое ограничение, описав класс вместо структуры:
           class date {
              int month, day, year;
           public:
              void set(int, int, int);
              void get(int*, int*, int*);
              void next();
              void print()
           };
Служебное слово public (общий) разбивает описание класса на две части. Имена, описанные в первой частной (private) части класса, могут использоваться только в функциях-членах. Вторая - общая часть - представляет собой интерфейс с объектами класса. Поэтому структура - это такой класс, в котором по определению все члены являются общими. Функции-члены класса определяются и используются точно так же, как было показано в предыдущем разделе:
           void date::print()     // печать датыв принятом в США виде
           {
             cout << month << '/' << day << '/' << year ;
           }
Однако от функций не членов частные члены класса date уже ограждены:
           void backdate()
           {
             today.day--;    // ошибка
           }
Есть ряд преимуществ в том, что доступ к структуре данных ограничен явно указанным списком функций. Любая ошибка в дате (например, December, 36, 1985) могла быть внесена только функцией-членом, поэтому первая стадия отладки - локализация ошибки - происходит даже до первого пуска программы. Это только частный случай общего правила: любое изменение в поведении типа date может и должно вызываться изменениями в его членах. Другое преимущество в том, что потенциальному пользователю класса для работы с ним достаточно знать только определения функций-членов.

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


5.2.3 Ссылка на себя

В функции-члене можно непосредственно использовать имена членов того объекта, для которого она была вызвана:
        class X {
          int m;
        public:
          int readm() { return m; }
        };

        void f(X aa, X bb)
        {
          int a = aa.readm();
          int b = bb.readm();
          // ...
        }
При первом вызове readm() m обозначает aa.m, а при втором - bb.m.

У функции-члена есть дополнительный скрытый параметр, являющийся указателем на объект, для которого вызывалась функция. Можно явно использовать этот скрытый параметр под именем this. Считается, что в каждой функции-члене класса X указатель this описан неявно как

        X *const this;
и инициализируется, чтобы указывать на объект, для которого функция-член вызывалась. Этот указатель нельзя изменять, поскольку он постоянный (*const). Явно описать его тоже нельзя, т.к. this - это служебное слово. Можно дать эквивалентное описание класса X:
       class X {
          int m;
       public:
          int readm() { return this->m; }
       };
Для обращения к членам использовать this излишне. В основном this используется в функциях-членах, непосредственно работающих с указателями. Типичный пример - функция, которая вставляет элемент в список с двойной связью:
         class dlink {
            dlink* pre;  // указатель на предыдущий элемент
            dlink* suc;  // указатель на следующий элемент
         public:
            void append(dlink*);
            // ...
         };

         void dlink::append(dlink* p)
         {
           p->suc = suc;    // т.е. p->suc = this->suc
           p->pre = this;   // явное использование "this"
           suc->pre = p;    // т.е. this->suc->pre = p
           suc = p;         // т.е. this->suc = p
         }

         dlink* list_head;

         void f(dlink* a, dlink* b)
         {
           // ...
           list_head->append(a);
           list_head->append(b);
         }
Списки с такой общей структурой служат фундаментом списочных классов, описываемых в главе 8. Чтобы присоединить звено к списку, нужно изменить объекты, на которые настроены указатели this, pre и suc. Все они имеют тип dlink, поэтому функция-член dlink::append() имеет к ним доступ. Защищаемой единицей в С++ является класс, а не отдельный объект класса.

Можно описать функцию-член таким образом, что объект, для которого она вызывается, будет доступен ей только по чтению. Тот факт, что функция не будет изменять объект, для которого она вызывается (т.е. this*), обозначается служебным словом const в конце списка параметров:

            class X {
               int m;
            public:
               readme() const { return m; }
               writeme(int i) { m = i; }
            };
Функцию-член со спецификацией const можно вызывать для постоянных объектов, а функцию-член без такой спецификации - нельзя:
            void f(X& mutable, const X& constant)
            {
              mutable.readme();    // нормально
              mutable.writeme(7);  // нормально
              constant.readme();   // нормально
              constant.writeme(7); // ошибка
            }
В этом примере разумный транслятор смог бы обнаружить, что функция X::writeme() пытается изменить постоянный объект. Однако, это непростая задача для транслятора. Из-за раздельной трансляции он в общем случае не может гарантировать "постоянство"
объекта, если нет соответствующего описания со спецификацией
   const. Например, определения readme() и writeme() могли быть в другом файле:
            class X {
               int m;
            public:
               readme() const;
               writeme(int i);
            };
В таком случае описание readme() со спецификацией const существенно.

Тип указателя this в постоянной функции-члене класса X есть const X *const. Это значит, что без явного приведения с помощью this нельзя изменить значение объекта:

            class X {
               int m;
            public:
               // ...
               void implicit_cheat() const { m++; }  // ошибка
               void explicit_cheat() const { ((X*)this)->m++; }
                    // нормально
           };
Отбросить спецификацию const можно потому, что понятие  "постоянства" объекта имеет два значения. Первое, называемое  "физическим постоянством" состоит в том, что объект хранится в защищенной от записи памяти. Второе, называемое "логическим постоянством" заключается в том, что объект выступает как постоянный (неизменяемый) по отношению к пользователям. Операция над логически постоянным объектом может изменить часть данных объекта, если при этом не нарушается его постоянство с точки зрения пользователя. Операциями, ненарушающими логическое постоянство объекта, могут быть буферизация значений, ведение статистики, изменение переменных-счетчиков в постоянных функциях-членах.

Логического постоянства можно достигнуть приведением, удаляющим спецификацию const:

           class calculator1 {
              int cache_val;
              int cache_arg;
              // ...
           public:
              int compute(int i) const;
              // ...
           };

           int calculator1::compute(int i) const
           {
             if (i == cache_arg) return cache_val;
             // нелучший способ
             ((calculator1*)this)->cache_arg = i;
             ((calculator1*)this)->cache_val = val;
             return val;
           }
Этого же результата можно достичь, используя указатель на данные без const:
           struct cache {
               int val;
               int arg;
           };

           class calculator2 {
               cache* p;
               // ...
           public:
               int compute(int i) const;
               // ...
           };

           int calculator2::compute(int i) const
           {
             if (i == p->arg) return p->val;
             // нелучший способ
             p->arg = i;
             p->val = val;
             return val;
           }
Отметим, что const нужно указывать как в описании, так и в определении постоянной функции-члена. Физическое постоянство обеспечивается помещением объекта в защищенную по записи память, только если в классе нет конструктора ($$7.1.6).



5.2.4 Инициализация

Инициализация объектов класса с помощью таких функций как set_date() - неэлегантное и чреватое ошибками решение. Поскольку явно не было указано, что объект требует инициализации, программист может либо забыть это сделать, либо сделать дважды, что может привести к столь же катастрофическим последствиям. Лучше дать программисту возможность описать функцию, явно предназначенную для инициализации объектов. Поскольку такая функция конструирует значение данного типа, она называется конструктором. Эту функцию легко распознать - она имеет то же имя, что и ее класс:
            class date {
               // ...
               date(int, int, int);
            };
Если в классе есть конструктор, все объекты этого класса будут проинициализированы. Если конструктору требуются параметры, их надо указывать:
            date today = date(23,6,1983);
            date xmas(25,12,0);  // краткая форма
            date my_birthday;    // неправильно, нужен инициализатор
Часто бывает удобно указать несколько способов инициализации объекта. Для этого нужно описать несколько конструкторов:
            class date {
               int month, day, year;
            public:
               // ...
               date(int, int, int);  // день, месяц, год
               date(int, int);       // день, месяц и текущий год
               date(int);            // день и текущие год и месяц
               date();               // стандартное значение: текущая дата
               date(const char*);    // дата в строковом представлении
            };
Параметры конструкторов подчиняются тем же правилам о типах параметров, что и все остальные функции ($$4.6.6). Пока конструкторы достаточно различаются по типам своих параметров, транслятор способен правильно выбрать конструктор:
               date today(4);
               date july4("July 4, 1983");
               date guy("5 Nov");
               date now;       // инициализация стандартным значением
Размножение конструкторов в примере c date типично. При разработке класса всегда есть соблазн добавить еще одну возможность, - а вдруг она кому-нибудь пригодится. Чтобы определить действительно нужные возможности, надо поразмышлять, но зато в результате, как правило, получается более компактная и понятная программа. Сократить число сходных функций можно с помощью стандартного значения параметра. В примере с date для каждого параметра можно задать стандартное значение, что означает: "взять значение из текущей даты".
               class date {
                 int month, day, year;
               public:
                 // ...
                 date(int d =0, int m =0, y=0);
                 // ...
               };

               date::date(int d, int m, int y)
               {
                 day = d ? d : today.day;
                 month = m ? m : today.month;
                 year = y ? y : today.year;
                 // проверка правильности даты
                 // ...
               }
Когда используется стандартное значение параметра, оно должно отличаться от всех допустимых значений параметра. В случае месяца и дня очевидно, что при значении нуль - это так, но неочевидно, что нуль подходит для значения года. К счастью, в европейском календаре нет нулевого года, т.к. сразу после 1 г. до р.х. (year==-1) идет 1 г. р.х. (year==1). Однако для обычной программы это, возможно, слишком тонкий момент.

Объект класса без конструктора может инициализироваться присваиванием ему другого объекта этого же класса. Это незапрещено и в том случае, когда конструкторы описаны:

            date d = today;  // инициализация присваиванием
На самом деле, имеется стандартный конструктор копирования, определенный как поэлементное копирование объектов одного класса. Если такой конструктор для класса X не нужен, можно переопределить его как конструктор копирования X::X(const X&). Подробнее поговорим об этом в $$7.6.



5.2.5 Удаление

Пользовательские типы чаще имеют, чем не имеют, конструкторы, которые проводят надлежащую инициализацию. Для многих типов требуется и обратная операция - деструктор, гарантирующая правильное удаление объектов этого типа. Деструктор класса X обозначается ~X ("дополнение конструктора"). В частности, для многих классов используется свободная память (см. $$3.2.6), выделяемая конструктором и освобождаемая деструктором. Вот, например, традиционное определение типа стек, из которого для краткости полностью выброшена обработка ошибок:
            class char_stack {
               int size;
               char* top;
               char* s;
            public:
               char_stack(int sz) { top=s=new char[size=sz]; }
               ~char_stack()  { delete[] s; }  // деструктор
               void push(char c) { *top++ = c; }
               void pop()   { return *--top; }
           };
Когда объект типа char_stack выходит из текущей области видимости, вызывается деструктор:
           void f()
           {
             char_stack s1(100);
             char_stack s2(200);
             s1.push('a');
             s2.push(s1.pop());
             char ch = s2.pop();
             cout << ch << '\n';
           }
Когда начинает выполняться f(), вызывается конструктор char_stack, который размещает массив из 100 символов s1 и массив из 200 символов s2. При возврате из f() память, которая была занята обоими массивами, будет освобождена.



5.2.6 Подстановка

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

Преодолеть эту трудность помогают функции-подстановки (inline). Если в описании класса функция-член определена, а не только описана, то она считается подстановкой. Это значит, например, что при трансляции функций, использующих char_stack из предыдущего примера, не будет использоваться никаких операций вызова функций, кроме реализации операций вывода! Другими словами, при разработке такого класса не нужно принимать во внимание затраты на вызов функций. Любое, даже самое маленькое действие, можно смело определять как функцию без потери эффективности. Это замечание снимает наиболее часто приводимый довод в пользу общих членов данных.

Функцию-член можно описать со спецификацией inline и вне описания класса:

           class char_stack {
              int size;
              char* top;
              char* s;
           public:
              char pop();
              // ...
           };

           inline char char_stack::pop()
           {
             return *--top;
           }
Отметим, что недопустимо описывать разные определения функции-члена, являющейся подстановкой, в различных исходных файлах ($$R.7.1.2). Это нарушило бы понятие о классе как о цельном типе.



5.3 Интерфейсы и реализации

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

Очевидные примеры хороших классов дают контейнеры разных видов: таблицы, множества, списки, вектора, словари и т.д. Такой класс имеет операцию занесения в контейнер. Обычно имеется и операция проверки: был ли данный член занесен в контейнер? Могут быть операции упорядочивания всех членов и просмотра их в определенном порядке. Наконец, может быть операция удаления члена. Обычно контейнерные классы имеют конструкторы и деструкторы.


5.3.1 Альтернативные реализации

Пока описание общей части класса и функций-членов остается неизменным, можно, не влияя на пользователей класса, менять его реализацию. В подтверждение этого рассмотрим таблицу имен из программы калькулятора, приведенной в главе 3. Структура ее такова:
            struct name {
              char* string;
              name* next;
              double value;
            };
А вот вариант класса table (таблица имен):
            // файл table.h
            class table {
               name* tbl;
            public:
               table() { tbl = 0; }

               name* look(char*, int = 0);
               name* insert(char* s) { return look(s,1); }
            };
Эта таблица отличается от определенной в главе 3 тем, что это настоящий тип. Можно описать несколько таблиц, завести указатель на таблицу и т.д. Например:
            #include "table.h"

            table globals;
            table keywords;
            table* locals;

            main()
            {
              locals = new table;
              // ...
            }
Приведем реализацию функции table::look(), в которой используется линейный поиск в списке имен таблицы:
            #include <string.h>

            name* table::look(char* p, int ins)
            {
              for (name* n = tbl; n; n=n->next)
                  if (strcmp(p,n->string) == 0) return n;
              if (ins == 0) error("имя не найдено");

              name* nn = new name;
              nn->string = new char[strlen(p)+1];
              strcpy(nn->string,p);
              nn->value = 1;
              nn->next = tbl;
              tbl = nn;
              return nn;
            }
Теперь усовершенствуем класс table так, чтобы поиск имени шел по ключу (хэш-функции от имени), как это и было сделано в примере с калькулятором. Сделать это труднее, если соблюдать ограничение, требующее, чтобы не все программы, использующие приведенную версию класса table, надо было изменять:
             class table {
                name** tbl;
                int size;
             public:
                table(int sz = 15);
                ~table();

                name* look(char*, int = 0);
                name* insert(char* s) { return look(s,1); }
             };
Изменения в структуре данных и конструкторе произошли потому, что для хэширования таблица должна иметь определенный размер. Задание конструктора со стандартным значением параметра гарантирует, что старые программы, в которых не использовался размер таблицы, останутся верными. Стандартные значения параметров полезны в таких случаях, когда нужно изменить класс, не влияя на программы пользователей класса. Теперь конструктор и деструктор создают и уничтожают хэшированные таблицы:
             table::table(int sz)
             {
               if (sz < 0) error("размер таблицы отрицателен");
               tbl = new name*[size = sz];
               for ( int i = 0; i<sz; i++) tbl[i] = 0;
             }

             table::~table()
             {
               for (int i = 0; i<size; i++) {
                   name* nx;
                   for (name* n = tbl[i]; n; n=nx) {
                       nx = n->next;
                       delete n->string;
                       delete n;
                   }
               }
               delete tbl;
             }
Описав деструктор для класса name, можно получить более ясный и простой вариант table::~table(). Функция поиска практически совпадает с приведенной в примере калькулятора ($$3.13):
             name* table::look(const char* p, int ins)
             {
               int ii = 0;
               char* pp = p;
               while (*pp) ii = ii<<1 ^ *pp++;
               if (ii < 0) ii = -ii;
               ii %= size;

               for (name* n=tbl[ii]; n; n=n->next)
                   if (strcmp(p,n->string) == 0) return n;

                name* nn = new name;
                nn->string = new char[strlen(p)+1];
                strcpy(nn->string,p);
                nn->value = 1;
                nn->next = tbl[ii];
                tbl[ii] = nn;
                return nn;
              }
Очевидно, что функции-члены класса должны перетранслироваться всякий раз, когда в описание класса вносится какое-либо изменение. В идеале такое изменение никак не должно отражаться на пользователях класса. К сожалению, обычно бывает не так. Для размещения переменной, имеющей тип класса, транслятор должен знать размер объекта класса. Если размер объекта изменится, нужно перетранслировать файлы, в которых использовался класс. Можно написать системную программу (и она даже уже написана), которая будет определять минимальное множество файлов, подлежащих перетрансляции после изменения класса. Но такая программа еще не получила широкого распространения.

Возможен вопрос: почему С++ был спроектирован таким образом, что после изменения частной части класса требуется перетрансляция программ пользователя? Почему вообще частная часть класса присутствует в описании класса? Иными словами, почему описания частных членов присутствуют в заголовочных файлах, доступных пользователю, если все равно недоступны для него в программе? Ответ один - эффективность. Во многих системах программирования процесс трансляции и последовательность команд, производящая вызов функции, будет проще, если размер автоматических (т.е. размещаемых в стеке) объектов известен на стадии трансляции.

Можно не знать определения всего класса, если представлять каждый объект как указатель на "настоящий" объект. Это позволяет решить задачу, поскольку все указатели будут иметь одинаковый размер, а размещение настоящих объектов будет проводиться только в одном файле, в котором доступны частные части классов. Однако, такое решение приводит к дополнительному расходу памяти на каждый объект и дополнительному обращению к памяти при каждом использовании члена. Еще хуже, что каждый вызов функции с автоматическим объектом класса требует вызовов функций выделения и освобождения памяти. К тому же становится невозможной реализация подстановкой функций-членов, работающих с частными членами класса. Наконец, такое изменение сделает невозможным связывание программ на С++ и на С, поскольку транслятор С будет по другому обрабатывать структуры (struct). Поэтому такое решение было сочтено неприемлемым для С++.

С другой стороны, С++ предоставляет средство для создания абстрактных типов, в которых связь между интерфейсом пользователя и реализацией довольно слабая. В главе 6 вводятся производные классы и описываются абстрактные базовые классы, а в $$13.3 поясняется, как с помощью этих средств реализовать абстрактные типы. Цель этого - дать возможность определять пользовательские типы столь же эффективные и конкретные, как и стандартные, и дать основные средства определения более гибких вариантов типов, которые могут оказаться и не столь эффективными.


5.3.2 Законченный пример класса

Программирование без упрятывания данных (в расчете на структуры) требует меньшего предварительного обдумывания задачи, чем программирование с упрятыванием данных (в расчете на классы). Структуру можно определить не очень задумываясь о том, как ее будут использовать. Когда определяется класс, внимание концентрируется на том, чтобы обеспечить для нового типа полный набор операций. Это важное смещение акцента в проектировании программ. Обычно время, затраченное на разработку нового типа, многократно окупается в процессе отладки и развития программы.

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

           class intset {
             int cursize, maxsize;
             int  *x;
           public:
             intset(int m, int n);    // не более m целых из 1..n
             ~intset();

             int member(int t) const; // является ли t членом?
             void insert(int t);      // добавить к множеству t

             void start(int& i) const { i = 0; }
             void ok(int& i) const    { return i<cursize; }
             void next(int& i) const  { return x[i++]; }
          };
Для проверки этого класса вначале создадим, а затем распечатаем множество случайных целых чисел. Это простое множество целых можно использовать для проверки, есть ли повторения в их последовательности. Но для большинства задач нужен, конечно, более развитый тип множества. Как всегда возможны ошибки, поэтому нужна функция:
          #include <iostream.h>

          void error(const char *s)
          {
            cerr << "set: " << s << '\n';
            exit(1);
          }
Класс intset используется в функции main(), для которой должно быть задано два параметра: первый определяет число создаваемых случайных чисел, а второй - диапазон их значений:
          int main(int argc, char* argv[])
          {
            if (argc != 3) error("нужно задавать два параметра");
            int count = 0;
            int m = atoi(argv[1]);    // число элементов множества
            int n = atoi(argv[2]);    // из диапазона 1..n
            intset s(m,n);

            while (count<m) {
              int t = randint(n);
              if (s.member(t)==0) {
                  s.insert(t);
                  count++;
              }
            }

            print_in_order(&s);
          }
Значение счетчика параметров программы argc равно 3, хотя программа имеет только два параметра. Дело в том, что в argv[0] всегда передается дополнительный параметр, содержащий имя программы. Функция
          extern "C" int atoi(const char*)
является стандартной библиотечной функцией, преобразующей целое из строкового представления во внутреннюю двоичную форму. Как обычно, если вы не хотите иметь такое описание в своей программе, то вам надо включить в нее соответствующий заголовочный файл, содержащий описания стандартных библиотечных функций. Случайные числа генерируются с помощью стандартной функции rand:
            extern "C" int rand();  // будьте осторожны:
                                    // числа не совсем случайные
            int randint(int u)      // диапазон 1..u
            {
              int r = rand();
              if (r < 0) r = -r;
              return 1 + r%u;
            }
Подробности реализации класса мало интересны для пользователя, но в любом случае будут использоваться функции-члены. Конструктор размещает массив целых с размером, равным заданному максимальному размеру множества, а деструктор удаляет этот массив:
          intset::intset(int m, int n)  // не более m целых в 1..n
          {
            if (m<1 || n<m) error("недопустимый размер intset");
            cursize = 0;
            maxsize = m;
            x = new int[maxsize];
          }

          intset::~intset()
          {
            delete x;
          }
Целые добавляются таким образом, что они хранятся во множестве в возрастающем порядке:
          void intset::insert(int t)
          {
            if (++cursize > maxsize) error("слишком много элементов");
            int i = cursize-1;
            x[i] = t;

            while (i>0 && x[i-1]>x[i]) {
               int t = x[i];     // поменять местами x[i] и x[i-1]
               x[i] = x[i-1];
               x[i-1] = t;
               i--;
            }
          }
Чтобы найти элемент, используется простой двоичный поиск:
          int intset::member(int t) const  // двоичный поиск
          {
            int l = 0;
            int u = cursize-1;

            while (l <= u) {
              int m = (l+u)/2;
              if (t < x[m])
                 u = m-1;
              else if (t > x[m])
                 l = m+1;
              else
                 return 1;   // найден
            }
            return 0;        // не найден
          }
Наконец, нужно предоставить пользователю набор операций, с помощью которых он мог бы организовать итерацию по множеству в некотором порядке (ведь порядок, используемый в представлении intset, от него скрыт). Множество по своей сути не является внутренне упорядоченным, и нельзя позволить просто выбирать элементы массива (а вдруг завтра intset будет реализовано в виде связанного списка?).

Пользователь получает три функции: start() - для инициализации итерации, ok() - для проверки, есть ли следующий элемент, и next() - для получения следующего элемента:

           class intset {
              // ...
              void start(int& i) const   { i = 0; }
              int ok(int& i) const       { return i<cursize; }
              int next(int& i) const     { return x[i++]; }
           };
Чтобы обеспечить совместную работу этих трех операций, надо запоминать тот элемент, на котором остановилась итерация. Для этого пользователь должен задавать целый параметр. Поскольку наше представление множества упорядоченное, реализация этих операций тривиальна. Теперь можно определить функцию print_in_order:
           void print_in_order(intset* set)
           {
             int var;
             set->sart(var);
             while (set->ok(var)) cout << set->next(var) << '\n';
           }
Другой способ построения итератора по множеству приведен в $$7.8.



5.4 Еще о классах

В этом разделе описаны дополнительные свойства класса. Описан способ обеспечить доступ к частным членам в функциях, не являющихся членами ($$5.4.1). Описано, как разрешить коллизии имен членов ($$5.4.2) и как сделать описания классов вложенными ($$5.4.3), но при этом избежать нежелательной вложенности ($$5.4.4). Вводится понятие статических членов (static), которые используются для представления операций и данных, относящихся к самому классу, а не к отдельным его объектам ($$5.4.5). Раздел завершается примером, показывающим, как можно построить дискриминирующее (надежное) объединение ($$5.4.6).



5.4.1 Друзья

Пусть определены два класса: vector (вектор) и matrix (матрица). Каждый из них скрывает свое представление, но дает полный набор операций для работы с объектами его типа. Допустим, надо определить функцию, умножающую матрицу на вектор. Для простоты предположим, что вектор имеет четыре элемента с индексами от 0 до 3, а в матрице четыре вектора тоже с индексами от 0 до 3. Доступ к элементам вектора обеспечивается функцией elem(), и аналогичная функция есть для матрицы. Можно определить глобальную функцию multiply (умножить) следующим образом:
           vector multiply(const matrix& m, const vector& v);
           {
             vector r;
             for (int i = 0; i<3; i++) { // r[i] = m[i] * v;
                 r.elem(i) = 0;
                 for (int j = 0; j<3; j++)
                     r.elem(i) +=m.elem(i,j) * v.elem(j);
             }
             return r;
           }
Это вполне естественное решение, но оно может оказаться очень неэффективным. При каждом вызове multiply() функция elem() будет вызываться 4*(1+4*3) раз. Если в elem() проводится настоящий контроль границ массива, то на такой контроль будет потрачено значительно больше времени, чем на выполнение самой функции, и в результате она окажется непригодной для пользователей. С другой стороны, если elem() есть некий специальный вариант доступа без контроля, то тем самым мы засоряем интерфейс с вектором и матрицей особой функцией доступа, которая нужна только для обхода контроля.

Если можно было бы сделать multiply членом обоих классов vector и matrix, мы могли бы обойтись без контроля индекса при обращении к элементу матрицы, но в то же время не вводить специальной функции elem(). Однако, функция не может быть членом двух классов. Надо иметь в языке возможность предоставлять функции, не являющейся членом, право доступа к частным членам класса. Функция - не член класса, - имеющая доступ к его закрытой части, называется другом этого класса. Функция может стать другом класса, если в его описании она описана как friend (друг). Например:

            class matrix;

            class vector {
              float v[4];
              // ...
              friend vector multiply(const matrix&, const vector&);
            };

            class matrix {
              vector v[4];
              // ...
              friend vector multiply(const matrix&, const vector&);
            };
Функция-друг не имеет никаких особенностей, за исключением права доступа к закрытой части класса. В частности, в такой функции нельзя использовать указатель this, если только она действительно не является членом класса. Описание friend является настоящим описанием. Оно вводит имя функции в область видимости класса, в котором она была описана, и при этом происходят обычные проверки на наличие других описаний такого же имени в этой области видимости. Описание friend может находится как в общей, так и в частной частях класса, это не имеет значения.

Теперь можно написать функцию multiply, используя элементы вектора и матрицы непосредственно:

           vector multiply(const matrix& m, const vector& v)
           {
             vector r;
             for (int i = 0; i<3; i++) {  // r[i] = m[i] * v;
                 r.v[i] = 0;
                 for ( int j = 0; j<3; j++)
                     r.v[i] +=m.v[i][j] * v.v[j];
             }
             return r;
           }
Отметим, что подобно функции-члену дружественная функция явно описывается в описании класса, с которым дружит. Поэтому она является неотъемлемой частью интерфейса класса наравне с функцией-членом.

Функция-член одного класса может быть другом другого класса:

      class x {
         // ...
         void f();
      };

      class y {
         // ...
         friend void x::f();
      };
Вполне возможно, что все функции одного класса являются друзьями другого класса. Для этого есть краткая форма записи:
        class x {
          friend class y;
          // ...
        };
В результате такого описания все функции-члены y становятся друзьями класса x.



5.4.2 Уточнение имени члена

Иногда полезно делать явное различие между именами членов классов и прочими именами. Для этого используется операция :: (разрешения области видимости):
             class X {
                int m;
             public:
                int readm() const { return m; }
                void setm(int m)  { X::m = m; }
            };
В функции X::setm() параметр m скрывает член m, поэтому к члену можно обращаться, только используя уточненное имя X::m. Правый операнд операции :: должен быть именем класса.

Начинающееся с :: имя должно быть глобальным именем. Это особенно полезно при использовании таких распространенных имен как read, put,  open, которыми можно обозначать функции-члены, не теряя возможности обозначать ими же функции, не являющиеся членами. Например:

            class my_file {
               // ...
            public:
               int open(const char*, const char*);
            };

            int my_file::jpen(const char* name, const char* spec)
            {
              // ...
              if (::open(name,flag)) {  // используется open() из UNIX(2)
                 // ...
              }
              // ...
           }


5.4.3 Вложенные классы

Описание класса может быть вложенным. Например:
           class set {
              struct setmem {
                 int mem;
                 setmem* next;
                 setmem(int m, setmem* n) { mem=m; next=n; }
              };
              setmem* first;
           public:
              set() { first=0; }
              insert(int m) { first = new setmem(m,first); }
              // ...
           };
Доступность вложенного класса ограничивается областью видимости лексически объемлющего класса:
          setmem m1(1,0);  // ошибка: setmem не находится
                           // в глобальной области видимости
Если только описание вложенного класса не является совсем простым, то лучше описывать этот класс отдельно, поскольку вложенные описания могут стать очень запутанными:
         class setmem {
         friend class set;  // доступно только для членов set
           int mem;
           setmem* next;
           setmem(int m, setmem* n) { mem=m; next=n; }

           // много других полезных членов
        };

        class set {
           setmem* first;
        public:
           set() { first=0; }
           insert(int m) { first = new setmem(m,first); }
           // ...
        };
Полезное свойство вложенности - это сокращение числа глобальных имен, а недостаток его в том, что оно нарушает свободу использования вложенных типов (см. $$12.3).

Имя класса-члена (вложенного класса) можно использовать вне описания объемлющего его класса так же, как имя любого другого члена:

         class X {
            struct M1 { int m; };
         public:
            struct M2 { int m; };

            M1 f(M2);
        };

        void f()
        {   M1 a;      // ошибка: имя `M1' вне области видимости
            M2 b;      // ошибка: имя `M1' вне области видимости
            X::M1 c;   // ошибка: X::M1 частный член
            X::M2 d;   // нормально
        }
Отметим, что контроль доступа происходит и для имен вложенных классов.

В функции-члене область видимости класса начинается после уточнения X:: и простирается до конца описания функции. Например:

       M1 X::f(M2 a)    // ошибка: имя `M1' вне области видимости
          { /* ... */ }

      X::M1 X::f(M2 a)  // нормально
          { /* ... */ }

      X::M1 X::f(X::M2 a) // нормально, но третье уточнение X:: излишне
          { /* ... */ }


5.4.4 Статические члены

Класс - это тип, а не некоторое данное, и для каждого объекта класса создается своя копия членов, представляющих данные. Однако, наиболее удачная реализация некоторых типов требует, чтобы все объекты этого типа имели некоторые общие данные. Лучше, если эти данные можно описать как часть класса. Например, в операционных системах или при моделировании управления задачами часто нужен список задач:
        class task {
           // ...
           static task* chain;
           // ...
        };
Описав член chain как статический, мы получаем гарантию, что он будет создан в единственном числе, т.е. не будет создаваться для каждого объекта task. Но он находится в области видимости класса task, и может быть доступен вне этой области, если только описан в общей части. В этом случае имя члена должно уточняться именем класса:
         if (task::chain == 0)  // какие-то операторы
В функции-члене его можно обозначать просто chain. Использование статических членов класса может заметно сократить потребность в глобальных переменных.

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

         class task {
            // ...
            static task* task_chain;
            static void shedule(int);
            // ...
        };
Но описание статического члена - это только описание, и где-то в программе должно быть единственное определение для описываемого объекта или функции, например, такое:
           task* task::task_chain = 0;
           void task::shedule(int p) { /* ... */ }
Естественно, что и частные члены могут определяться подобным образом.

Отметим, что служебное слово static не нужно и даже нельзя использовать в определении статического члена класса. Если бы оно присутствовало, возникла бы неоднозначность: указывает ли оно на то, что член класса является статическим, или используется для описания глобального объекта или функции? Слово static одно из самых перегруженных служебных слов в С и С++. К статическому члену, представляющему данные, относятся оба основных его значения: "статически размещаемый" , т.е. противоположный объектам, размещаемым в стеке или свободной памяти, и "статический" в смысле с ограниченной областью видимости, т.е. противоположный объектам, подлежащим внешнему связыванию. К функциям-членам относится только второе значение static.


5.4.5 Указатели на члены

Можно брать адрес члена класса. Операция взятия адреса функции-члена часто оказывается полезной, поскольку цели и способы применения указателей на функции, о которых мы говорили в $$4.6.9, в равной степени относятся и к таким функциям. Указатель на член можно получить, применив операцию взятия адреса & к полностью уточненному имени члена класса, например, &class_name::member_name. Чтобы описать переменную типа "указатель на член класса X", надо использовать описатель вида X::*. Например:
           #include <iostream.h>

           struct cl
           {
              char* val;
              void print(int x) { cout << val << x << '\n'; }
              cl(char* v) { val = v; }
           };
Указатель на член можно описать и использовать так:
          typedef void (cl::*PMFI)(int);

          int main()
          {
            cl z1("z1 ");
            cl z2("z2 ");
            cl* p = &z2;
            PMFI pf = &cl::print;
            z1.print(1);
            (z1.*pf)(2);
            z2.print(3);
            (p->*pf)(4);
          }
Использование typedef для замены трудно воспринимаемого описателя в С достаточно типичный случай. Операции .* и ->* настраивают указатель на конкретный объект, выдавая в результате функцию, которую можно вызывать. Приоритет операции () выше, чем у операций .* и ->*, поэтому нужны скобки.

Во многих случаях виртуальные функции ($$6.2.5) успешно заменяют указатели на функции.


5.4.6 Структуры и объединения

По определению структура - это класс, все члены которого общие, т.е. описание
         struct s { ...
это просто краткая форма описания
         class s { public: ...
Поименованное объединение определяется как структура, все члены которой имеют один и тот же адрес ($$R.9.5). Если известно, что в каждый момент времени используется значение только одного члена структуры, то объявив ее объединением, можно сэкономить память. Например, можно использовать объединение для хранения лексем транслятора С:
          union tok_val {
             char* p;      // строка
             char v[8];    // идентификатор (не более 8 символов)
             long i;       // значения целых
             double d;     // значения чисел с плавающей точкой
          };
Проблема с объединениями в том, что транслятор в общем случае не знает, какой член используется в данный момент, и поэтому контроль типа невозможен. Например:
           void strange(int i)
           {
             tok_val x;
             if (i)
                x.p = "2";
             else
                x.d = 2;
             sqrt(x.d);     // ошибка, если i != 0
           }
Кроме того, определенное таким образом объединение нельзя инициализировать таким кажущимся вполне естественным способом:
           tok_val val1 = 12;   // ошибка: int присваивается  tok_val
           tok_val val2 = "12"; // ошибка: char* присваивается tok_val
Для правильной инициализации надо использовать конструкторы:
           union tok_val {
             char* p;    // строка
             char v[8];  // идентификатор (не более 8 символов)
             long i;     // значения целых
             double d;   // значения чисел с плавающей точкой

             tok_val(const char*);  // нужно выбирать между p и v
             tok_val(int ii)    { i = ii; }
             tok_val(double dd) { d = dd; }
           };
Эти описания позволяют разрешить с помощью типа членов неоднозначность при перегрузке имени функции (см. $$4.6.6 и $$7.3). Например:
           void f()
           {
             tok_val a = 10;    // a.i = 10
             tok_val b = 10.0;  // b.d = 10.0
           }
Если это невозможно (например, для типов char* и char[8] или int и char и т.д.), то определить, какой член инициализируется, можно, изучив инициализатор при выполнении программы, или введя дополнительный параметр. Например:
           tok_val::tok_val(const char* pp)
           {
             if (strlen(pp) <= 8)
                strncpy(v,pp,8);    // короткая строка
             else
                p = pp;            // длинная строка
           }
Но лучше подобной неоднозначности избегать.

Стандартная функция strncpy() подобно strcpy() копирует строки, но у нее есть дополнительный параметр, задающий максимальное число копируемых символов.

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

          class tok_val {
          public:
             enum Tag { I, D, S, N };

          private:
             union {
               const char* p;
               char v[8];
               long i;
               double d;
             };

             Tag tag;

             void check(Tag t) { if (tag != t) error(); }
           public:
             Tag get_tag() { return tag; }

             tok_val(const char* pp);
             tok_val(long ii)   { i = ii; tag = I; }
             tok_val(double dd) { d = dd; tag = D; }

             long& ival()        { check(I); return i; }
             double& fval()      { check(D); return d; }
             const char*& sval() { check(S); return p; }
             char* id()          { check(N); return v; }
          };

          tok_val::tok_val(const char* pp)
          {
            if (strlen(pp) <= 8)  { // короткая строка
               tag = N;
               strncpy(v,pp,8);
            }
            else {                  // длинная строка
               tag = S;
               p = pp;              // записывается только указатель
            }
          }
Использовать класс tok_val можно так:
          void f()
          {
            tok_val t1("короткая");       // присваивается v
            tok_val t2("длинная строка"); // присваивается p
            char s[8];
            strncpy(s,t1.id(),8);         // нормально
            strncpy(s,t2.id(),8);         // check() выдаст ошибку
          }
Описав тип Tag и функцию get_tag() в общей части, мы гарантируем, что тип tok_val можно использовать как тип параметра. Таким образом, появляется надежная в смысле типов альтернатива описанию параметров с эллипсисом. Вот, например, описание функции обработки ошибок, которая может иметь один, два, или три параметра с типами char*, int или double:
           extern tok_val no_arg;

           void error(
               const char* format,
               tok_val a1 = no_arg,
               tok_val a2 = no_arg,
               tok_val a3 = no_arg);


5.5 Конструкторы и деструкторы

Если у класса есть конструктор, он вызывается всякий раз при создании объекта этого класса. Если у класса есть деструктор, он вызывается всякий раз, когда уничтожается объект этого класса. Объект может создаваться как:
   [1] автоматический, который создается каждый раз, когда его описание встречается при выполнении программы, и уничтожается по выходе из блока, в котором он описан;
   [2] статический, который создается один раз при запуске программы и уничтожается при ее завершении;
   [3] объект в свободной памяти, который создается операцией new и уничтожается операцией delete;
   [4] объект-член, который создается в процессе создания другого класса или при создании массива, элементом которого он является. Кроме этого объект может создаваться, если в выражении явно используется его конструктор ($$7.3) или как временный объект ($$R.12.2). В обоих случаях такой объект не имеет имени. В следующих подразделах предполагается, что объекты относятся к классу с конструктором и деструктором. В качестве примера используется класс table из $$5.3.1.


5.5.1 Локальные переменные

Конструктор локальной переменной вызывается каждый раз, когда при выполнении программы встречается ее описание. Деструктор локальной переменной вызывается всякий раз по выходе из блока, где она была описана. Деструкторы для локальных переменных вызываются в порядке, обратном вызову конструкторов при их создании:
          void f(int i)
          {
            table aa;
            table bb;
            if (i>0) {
                table cc;
                // ...
            }
            // ...
          }
Здесь aa и bb создаются (именно в таком порядке) при каждом вызове f(), а уничтожаются они при возврате из f() в обратном порядке - bb, затем aa. Если в текущем вызове f() i больше нуля, то cc создается после bb и уничтожается прежде него.

Поскольку aa и bb - объекты класса table, присваивание aa=bb означает копирование по членам bb в aa (см. $$2.3.8). Такая интерпретация присваивания может привести к неожиданному (и обычно нежелательному) результату, если присваиваются объекты класса, в котором определен конструктор:

           void h()
           {
              table t1(100);
              table t2 = t1;   // неприятность
              table t3(200);

              t3 = t2;         // неприятность
           }
В этом примере конструктор table вызывается дважды: для t1 и t3. Он не вызывается для t2, поскольку этот объект инициализируется присваиванием. Тем не менее, деструктор для table вызывается три раза: для t1, t2 и t3! Далее, стандартная интерпретация присваивания - это копирование по членам, поэтому перед выходом из h() t1, t2 и t3 будут содержать указатель на массив имен, память для которого была выделена в свободной памяти при создании t1. Указатель на память, выделенную для массива имен при создании t3, будет потерян. Этих неприятностей можно избежать (см. $$1.4.2 и $$7.6).



5.5.2 Статическая память

Рассмотрим такой пример:
        table tbl(100);

        void f(int i)
        {
           static table tbl2(i);
        }

        int main()
        {
          f(200);
          // ...
        }
Здесь конструктор, определенный в $$5.3.1, будет вызываться дважды: один раз для tbl и один раз для tbl2. Деструктор table::~table() также будет вызван дважды: для уничтожения tbl и tbl2 по выходе из main(). Конструкторы глобальных статических объектов в файле вызываются в том же порядке, в каком встречаются в файле описания объектов, а деструкторы для них вызываются в обратном порядке. Конструктор локального статического объекта вызывается, когда при выполнении программы первый раз встречается определение объекта.

Традиционно выполнение main() рассматривалось как выполнение всей программы. На самом деле, это не так даже для С. Уже размещение статического объекта класса с конструктором и (или) деструктором позволяет программисту задать действия, которые будут выполняться до вызова main()и (или) по выходе из main().

Вызов конструкторов и деструкторов для статических объектов играет в С++ чрезвычайно важную роль. С их помощью можно обеспечить соответствующую инициализацию и удаление структур данных, используемых в библиотеках. Рассмотрим <iostream.h>. Откуда берутся cin, cout и cerr? Когда они инициализируются? Более существенный вопрос: поскольку для выходных потоков используются внутренние буфера символов, то происходит выталкивание этих буферов, но когда? Есть простой и очевидный ответ: все действия выполняются соответствующими конструкторами и деструкторами до запуска main() и по выходе из нее (см. $$10.5.1). Существуют альтернативы использованию конструкторов и деструкторов для инициализации и уничтожения библиотечных структур данных, но все они или очень специализированы, или неуклюжи, или и то и другое вместе.

Если программа завершается обращение к функции exit(), то вызываются деструкторы для всех построенных статических объектов. Однако, если программа завершается обращением к abort(), этого не происходит. Заметим, что exit() не завершает программу немедленно. Вызов exit() в деструкторе может привести к бесконечной рекурсии. Если нужна гарантия, что будут уничтожены как статические, так и автоматические объекты, можно воспользоваться особыми ситуациями ($$9).

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


5.5.3 Свободная память

Рассмотрим пример:
          main()
          {
             table* p = new table(100);
             table* q = new table(200);
             delete p;
             delete p;  // вероятно, вызовет ошибку при выполнении
           }
Конструктор table::table() будет вызываться дважды, как и деструктор table::~table(). Но это ничего не значит, т.к. в С++ не гарантируется, что деструктор будет вызываться только для объекта, созданного операцией new. В этом примере q не уничтожается вообще, зато p уничтожается дважды! В зависимости от типа p и q программист может считать или не считать это ошибкой. То, что объект не удаляется, обычно бывает не ошибкой, а просто потерей памяти. В то же время повторное удаление p - серьезная ошибка. Повторное применение delete к тому же самому указателю может привести к бесконечному циклу в подпрограмме, управляющей свободной памятью. Но в языке результат повторного удаления не определен, и он зависит от реализации.

Пользователь может определить свою реализацию операций new и delete (см. $$3.2.6 и $$6.7). Кроме того, можно установить взаимодействие конструктора или деструктора с операциями new и delete (см. $$5.5.6 и $$6.7.2). Размещение массивов в свободной памяти обсуждается в $$5.5.5.


5.5.4 Объекты класса как члены

Рассмотрим пример:
          class classdef {
             table members;
             int no_of_members;
             // ...
             classdef(int size);
             ~classdef();
          };
Цель этого определения, очевидно, в том, чтобы classdef содержал член, являющийся таблицей размером size, но есть сложность: надо обеспечить вызов конструктора table::table() с параметром size. Это можно сделать, например, так:
         classdef::classdef(int size)
            :members(size)
         {
            no_of_members = size;
            // ...
          }
Параметр для конструктора члена (т.е. для table::table()) указывается в определении (но не в описании) конструктора класса, содержащего член (т.е. в определении classdef::classdef()). Конструктор для члена будет вызываться до выполнения тела того конструктора, который задает для него список параметров.

Аналогично можно задать параметры для конструкторов других членов (если есть еще другие члены):

           class classdef {
             table members;
             table friends;
             int no_of_members;
             // ...
             classdef(int size);
             ~classdef();
           };
Списки параметров для членов отделяются друг от друга запятыми (а не двоеточиями), а список инициализаторов для членов можно задавать в произвольном порядке:
           classdef::classdef(int size)
               : friends(size), members(size), no_of_members(size)
           {
             // ...
           }
Конструкторы вызываются в том порядке, в котором они заданы в описании класса.

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

Если конструктору члена не требуется параметров, то и не нужно задавать никаких списков параметров. Так, поскольку конструктор table::table() был определен со стандартным значением параметра, равным 15, достаточно такого определения:

          classdef::classdef(int size)
              : members(size), no_of_members(size)
          {
             // ...
          }
Тогда размер таблицы friends будет равен 15.

Если уничтожается объект класса, который сам содержит объекты класса (например, classdef), то вначале выполняется тело деструктора объемлющего класса, а затем деструкторы членов в порядке, обратном их описанию.

Рассмотрим вместо вхождения объектов класса в качестве членов традиционное альтернативное ему решение: иметь в классе указатели на члены и инициализировать члены в конструкторе:

           class classdef {
              table* members;
              table* friends;
              int no_of_members;
              // ...
           };

          classdef::classdef(int size)
          {
             members = new table(size);
             friends = new table;  // используется стандартный
                                   // размер table
             no_of_members = size;
             // ...
           }
Поскольку таблицы создавались с помощью операции new, они должны уничтожаться операцией delete:
          classdef::~classdef()
          {
            // ...
            delete members;
            delete friends;
          }
Такие отдельно создаваемые объекты могут оказаться полезными, но учтите, что members и friends указывают на независимые от них объекты, каждый из которых надо явно размещать и удалять. Кроме того, указатель и объект в свободной памяти суммарно занимают больше места, чем объект-член.



5.5.5 Массивы объектов класса

Чтобы можно было описать массив объектов класса с конструктором, этот класс должен иметь стандартный конструктор, т.е. конструктор, вызываемый без параметров. Например, в соответствии с определением
          table tbl[10];
будет создан массив из 10 таблиц, каждая из которых инициализируется вызовом table::table(15), поскольку вызов table::table() будет происходить с фактическим параметром 15.

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

Когда уничтожается массив, деструктор должен вызываться для каждого элемента массива. Для массивов, которые размещаются не с помощью new, это делается неявно. Однако для размещенных в свободной памяти массивов неявно вызывать деструктор нельзя, поскольку транслятор не отличит указатель на отдельный объект массива от указателя на начало массива, например:

          void f()
          {
            table* t1 = new table;
            table* t2 = new table[10];
            delete t1;  // удаляется одна таблица
            delete t2;  // неприятность:
                        // на самом деле удаляется 10 таблиц
          }
В данном случае программист должен указать, что t2 - указатель на массив:
          void g(int sz)
          {
            table* t1 = new table;
            table* t2 = new table[sz];
            delete t1;
            delete[] t2;
          }
Функция размещения хранит число элементов для каждого размещаемого массива. Требование использовать для удаления массивов только операцию delete[] освобождает функцию размещения от обязанности хранить счетчики числа элементов для каждого массива. Исполнение такой обязанности в реализациях С++ вызывало бы существенные потери времени и памяти и нарушило совместимость с С.



5.5.6 Небольшие объекты

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

Вернемся к классу name, который использовался в примерах с table. Он мог бы определяться так:

          struct name {
             char* string;
             name* next;
             double value;

             name(char*, double, name*);
             ~name();

             void* operator new(size_t);
             void operator delete(void*, size_t);
           private:
             enum { NALL = 128 };
             static name* nfree;
           };
Функции name::operator new() и name::operator delete() будут использоваться (неявно) вместо глобальных функций operator new() и operator delete(). Программист может для конкретного типа написать более эффективные по времени и памяти функции размещения и удаления, чем универсальные функции operator new() и operator delete(). Можно, например, разместить заранее "куски" памяти, достаточной для объектов типа name, и связать их в список; тогда операции размещения и удаления сводятся к простым операциям со списком. Переменная nfree используется как начало списка неиспользованных кусков памяти:
            void* name::operator new(size_t)
            {
              register name* p = nfree;  // сначала выделить

              if (p)
                 nfree = p->next;
              else {                // выделить и связать в список
                 name* q = (name*) new char[NALL*sizeof(name) ];
                 for (p=nfree=&q[NALL-1]; q<p; p--) p->next = p-1;
                 (p+1)->next = 0;
              }

              return p;
            }
Распределитель памяти, вызываемый new, хранит вместе с объектом его размер, чтобы операция delete выполнялась правильно. Этого дополнительного расхода памяти можно легко избежать, если использовать распределитель, рассчитанный на конкретный тип. Так, на машине автора функция name::operator new() для хранения объекта name использует 16 байтов, тогда как стандартная глобальная функция operator new() использует 20 байтов.

Отметим, что в самой функции name::operator new() память нельзя выделять таким простым способом:

          name* q= new name[NALL];
Это вызовет бесконечную рекурсию, т.к. new будет вызывать name::name().

Освобождение памяти обычно тривиально:

          void name::operator delete(void* p, size_t)
          {
             ((name*)p)->next = nfree;
             nfree = (name*) p;
          }
Приведение параметра типа void* к типу name* необходимо, поскольку функция освобождения вызывается после уничтожения объекта, так что больше нет реального объекта типа name, а есть только кусок памяти размером sizeof(name). Параметры типа size_t в приведенных функциях name::operator new() и name::operator delete() не использовались. Как можно их использовать, будет показано в $$6.7. Отметим, что наши функции размещения и удаления используются только для объектов типа name, но не для массивов names.



5.6 Упражнения

 1. (*1) Измените программу калькулятора из главы 3 так, чтобы можно было воспользоваться классом table.
 2. (*1) Определите tnode ($$R.9) как класс с конструкторами и деструкторами и т.п., определите дерево из объектов типа
    tnode как класс с конструкторами и деструкторами и т.п.
 3. (*1) Определите класс intset ($$5.3.2) как множество строк.
 4. (*1) Определите класс intset как множество узлов типа tnode.

Структуру tnode придумайте сами.
 5. (*3) Определите класс для разбора, хранения, вычисления и печати простых арифметических выражений, состоящих из целых констант и операций +, -, * и /. Общий интерфейс класса должен выглядеть примерно так:

            class expr {
              // ...
            public:
              expr(char*);
              int eval();
              void print();
            };
Конструктор expr::expr() имеет параметр-строку, задающую выражение.

Функция expr::eval() возвращает значение выражения, а expr::print() выдает представление выражения в cout. Использовать эти функции можно так:

            expr("123/4+123*4-3");
            cout << "x = " << x.eval() << "\n";
            x.print();
Дайте два определения класса expr: пусть в первом для представления
используется связанный список узлов, а во втором - строка
символов. Поэкспериментируйте с разными форматами печати
выражения, а именно: с полностью расставленными скобками,
в постфиксной записи, в ассемблерном коде и т.д.
 6. (*1) Определите класс char_queue (очередь символов) так, чтобы его общий интерфейс не зависел от представления. Реализуйте класс как: (1) связанный список и (2) вектор. О параллельности не думайте.
 7. (*2) Определите класс histogram (гистограмма), в котором ведется подсчет чисел в определенных интервалах, задаваемых в виде параметров конструктору этого класса. Определите функцию выдачи гистограммы. Сделайте обработку значений, выходящих за интервал. Подсказка: обратитесь к <task.h>.
 8. (*2) Определите несколько классов, порождающих случайные числа с определенными распределениями. Каждый класс должен иметь конструктор, задающий параметры распределения и функцию draw, возвращающую "следующее" значение. Подсказка: обратитесь к
    <task.h> и классу intset.
 9. (*2) Перепишите примеры date ($$5.2.2 и $$5.2.4), char_stack
    ($$5.2.5) и intset ($$5.3.2), не используя никаких функций-членов
    (даже конструкторов и деструкторов).Используйте только class
и friend. Проверьте каждую из новых версий и сравните их с версиями, в которых используются функции-члены.
 10.(*3) Для некоторого языка составьте определения класса для таблицы имен и класса, представляющего запись в этой таблице. Исследуйте транслятор для этого языка, чтобы узнать, какой должна быть настоящая таблица имен.
 11.(*2) Измените класс expr из упражнения 5 так, чтобы в выражении можно было использовать переменные и операцию присваивания =.

Используйте класс для таблицы имен из упражнения 10.
 12.(*1) Пусть есть программа:

           #include <iostream.h>

           main()
           {
              cout << "Всем привет\n";
           }
Измените ее так, чтобы она выдавала:
  Инициализация
  Всем привет
  Удаление Саму функцию main() менять нельзя.



ГЛАВА 6ПРОИЗВОДНЫЕ КЛАССЫ

     Не плоди объекты без нужды.
                                 - В. Оккам

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

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

Основное назначение производных классов - упростить программисту задачу выражения общности классов.

6.1 Введение и краткий обзор

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

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

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

 $$6.4 производные классы показаны на законченном примере

$$6.5 вводится понятие множественного наследования как возможность иметь для класса более одного прямого базового класса, описываются способы разрешения коллизий имен, возникающих при множественном наследовании.

 $$6.6 обсуждается механизм контроля доступа.

 $$6.7 приводятся некоторые приемы управления свободной памятью для производных классов. В последующих главах также будут приводиться примеры, использующие эти возможности языка.


6.2 Производные классы

Обсудим, как написать программу учета служащих некоторой фирмы. В ней может использоваться, например, такая структура данных:
            struct employee {            // служащие
                 char*     name;         // имя
                 short     age;          // возраст
                 short     department;   // отдел
                 int       salary;       // оклад
                 employee* next;
                 // ...
             };
Поле next нужно для связывания в список записей о служащих одного отдела (employee). Теперь попробуем определить структуру данных для управляющего (manager):
            struct manager {
               employee emp;    // запись employee для управляющего
               employee* group; // подчиненный коллектив
               short   level;
               // ...
            };
Управляющий также является служащим, поэтому запись employee хранится в члене emp объекта manager. Для человека эта общность очевидна, но для транслятора член emp ничем не отличается от других членов класса. Указатель на структуру manager (manager*) не является указателем на employee (employee*), поэтому нельзя свободно использовать один вместо другого. В частности, без специальных действий нельзя объект manager включить в список объектов типа employee. Придется либо использовать явное приведение типа manager*, либо в список записей employee включить адрес члена emp. Оба решения некрасивы и могут быть достаточно запутанными. Правильное решение состоит в том, чтобы тип manager был типом employee с некоторой дополнительной информацией:
          struct manager : employee {
            employee* group;
            short level;
            // ...
          };
Класс manager является производным от employee, и, наоборот, employee является базовым классом для manager. Помимо члена group в классе manager есть члены класса employee (name, age и т.д.).

Графически отношение наследования обычно изображается в виде стрелки от производных классов к базовому:

              employee
                 ^
                 |
               manager
Обычно говорят, что производный класс наследует базовый класс, поэтому и отношение между ними называется наследованием. Иногда базовый класс называют суперклассом, а производный - подчиненным классом. Но эти термины могут вызывать недоумение, поскольку объект производного класса содержит объект своего базового класса. Вообще производный класс больше своего базового в том смысле, что в нем содержится больше данных и определено больше функций.

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

          void f()
          {
            manager m1, m2;
            employee e1, e2;
            employee* elist;
            elist = &m1;           // поместить m1 в elist
            m1.next = &e1;         // поместить e1 в elist
            e1.next = &m2;         // поместить m2 в elist
            m2.next = &e2;         // поместить m2 в elist
            e2.next = 0;           // конец списка
          }
Поскольку управляющий является и служащим, указатель manager* можно использовать как employee*. В то же время служащий не обязательно является управляющим, и поэтому employee* нельзя использовать как manager*.

В общем случае, если класс derived имеет общий базовый класс base, то указатель на derived можно без явных преобразований типа присваивать переменной, имеющей тип указателя на base. Обратное преобразование от указателя на base к указателю на derived может быть только явным:

         void g()
         {
           manager mm;
           employee* pe = &mm;  // нормально

           employee ee;
           manager* pm = &ee;  // ошибка:
                               // не всякий служащий является управляющим

           pm->level = 2;      // катастрофа: при размещении ee
                               // память для члена `level' не выделялась

          pm = (manager*) pe;  // нормально: на самом деле pe
                               // не настроено на объект mm типа manager

          pm->level = 2;       // отлично: pm указывает на объект mm
                               // типа manager, а в нем при размещении
                               // выделена память для члена `level'
       }
Иными словами, если работа с объектом производного класса идет через указатель, то его можно рассматривать как объект базового класса. Обратное неверно. Отметим, что в обычной реализации С++ не предполагается динамического контроля над тем, чтобы после преобразования типа, подобного тому, которое использовалось в присваивании pe в pm, получившийся в результате указатель действительно был настроен на объект требуемого типа (см. $$13.5).



6.2.1 Функции-члены

Простые структуры данных вроде employee и manager сами по себе не слишком интересны, а часто и не особенно полезны. Поэтому добавим к ним функции:
       class employee {
         char* name;
         // ...
       public:
         employee* next;  // находится в общей части, чтобы
                          // можно было работать со списком
         void print() const;
         // ...
      };

      class manager : public employee {
         // ...
      public:
         void print() const;
         // ...
      };
Надо ответить на некоторые вопросы. Каким образом функция-член производного класса manager может использовать члены базового класса employee? Какие члены базового класса employee могут использовать функции-члены производного класса manager? Какие члены базового класса employee может использовать функция, не являющаяся членом объекта типа manager? Какие ответы на эти вопросы должна давать реализация языка, чтобы они максимально соответствовали задаче программиста? Рассмотрим пример:
      void manager::print() const
      {
         cout << " имя "    << name << '\n';
      }
Член производного класса может использовать имя из общей части своего базового класса наравне со всеми другими членами, т.е. без указания имени объекта. Предполагается, что есть объект, на который настроен
 this, поэтому корректным обращением к name будет this->name. Однако, при трансляции функции manager::print() будет зафиксирована ошибка: члену производного класса не предоставлено право доступа к частным членам его базового класса, значит name недоступно в этой функции.

Возможно многим это покажется странным, но давайте рассмотрим альтернативное решение: функция-член производного класса имеет доступ к частным членам своего базового класса. Тогда само понятие частного (закрытого) члена теряет всякий смысл, поскольку для доступа к нему достаточно просто определить производный класс. Теперь уже будет недостаточно для выяснения, кто использует частные члены класса, просмотреть все функции-члены и друзей этого класса. Придется просмотреть все исходные файлы программы, найти производные классы, затем исследовать каждую функцию этих классов. Далее надо снова искать производные классы от уже найденных и т.д. Это, по крайней мере, утомительно, а скорее всего нереально. Нужно всюду, где это возможно, использовать вместо частных членов защищенные (см. $$6.6.1).

Как правило, самое надежное решение для производного класса - использовать только общие члены своего базового класса:

         void manager::print() const
         {
           employee::print();   // печать данных о служащих

           // печать данных об управляющих
         }
Отметим, что операция :: необходима, поскольку функция print() переопределена в классе manager. Такое повторное использование имен типично для С++. Неосторожный программист написал бы:
        void manager::print() const
        {
           print();   // печать данных о служащих

           // печать данных об управляющих
        }
В результате он получил бы рекурсивную последовательность вызовов manager::print().



6.2.2 Конструкторы и деструкторы

Для некоторых производных классов нужны конструкторы. Если конструктор есть в базовом классе, то именно он и должен вызываться с указанием параметров, если таковые у него есть:
        class employee {
          // ...
        public:
          // ...
          employee(char* n, int d);
       };

       class manager : public employee {
          // ...
       public:
          // ...
         manager(char* n, int i, int d);
       };
Параметры для конструктора базового класса задаются в определении конструктора производного класса. В этом смысле базовый класс выступает как класс, являющийся членом производного класса:
       manager::manager(char* n, int l, int d)
          : employee(n,d), level(l), group(0)
       {
       }
Конструктор базового класса employee::employee() может иметь такое определение:
       employee::employee(char* n, int d)
           : name(n), department(d)
       {
         next = list;
         list = this;
       }
Здесь list должен быть описан как статический член employee.

Объекты классов создаются снизу вверх: вначале базовые, затем члены и, наконец, сами производные классы. Уничтожаются они в обратном порядке: сначала сами производные классы, затем члены, а затем базовые. Члены и базовые создаются в порядке описания их в классе, а уничтожаются они в обратном порядке.


6.2.3 Иерархия классов

Производный класс сам в свою очередь может быть базовым классом:
        class employee { /* ... */ };
        class manager : public employee { /* ... */ };
        class director : public manager { /* ... */ };
Такое множество связанных между собой классов обычно называют иерархией классов. Обычно она представляется деревом, но бывают иерархии с более общей структурой в виде графа:
        class temporary { /* ... */ };
        class secretary : public employee { /* ... */ };

       class tsec
         : public temporary, public secretary { /* ... */ };

       class consultant
         : public temporary, public manager { /* ... */ };
Видим, что классы в С++ могут образовывать направленный ацикличный граф (подробнее об этом говорится в $$6.5.3). Этот граф для приведенных классов имеет вид:



6.2.4 Поля типа

Чтобы производные классы были не просто удобной формой краткого описания, в реализации языка должен быть решен вопрос: к какому из производных классов относится объект, на который смотрит указатель base*? Существует три основных способа ответа:

   [1] Обеспечить, чтобы указатель мог ссылаться на объекты только одного типа ($$6.4.2);
   [2] Поместить в базовый класс поле типа, которое смогут проверять функции;
   [3] использовать виртуальные функции ($$6.2.5).

Указатели на базовые классы обыкновенно используются при проектировании контейнерных классов (множество, вектор, список и т.д.). Тогда в случае [1] мы получим однородные списки, т.е. списки объектов одного типа. Способы [2] и [3] позволяют создавать разнородные списки, т.е. списки объектов нескольких различных типов (на самом деле, списки указателей на эти объекты). Способ [3] - это специальный надежный в смысле типа вариант способа [2]. Особенно интересные и мощные варианты дают комбинации способов [1] и [3]; они обсуждаются в главе 8.

Вначале обсудим простой способ с полем типа, т.е. способ [2]. Пример с классами manager/employee можно переопределить так:

          struct employee {
             enum empl_type { M, E };
             empl_type type;
             employee* next;
             char*     name;
             short     department;
             // ...
         };

         struct manager : employee {
            employee* group;
            short     level;
            // ...
        };
Имея эти определения, можно написать функцию, печатающую данные о произвольном служащем:
       void print_employee(const employee* e)
       {
          switch (e->type) {
          case E:
            cout << e->name << '\t' << e->department << '\n';
            // ...
            break;
         case M:
           cout << e->name << '\t' << e->department << '\n';
           // ...
           manager* p = (manager*) e;
           cout << "level" << p->level << '\n';
           // ...
           break;
         }
       }
Напечатать список служащих можно так:
       void f(const employee* elist)
       {
         for (; elist; elist=elist->next) print_employee(elist);
       }
Это вполне хорошее решение, особенно для небольших программ, написанных одним человеком, но оно имеет существенный недостаток: транслятор не может проверить, насколько правильно программист обращается с типами. В больших программах это приводит к ошибкам двух видов. Первый - когда программист забывает проверить поле типа. Второй - когда в переключателе указываются не все возможные значения поля типа. Этих ошибок достаточно легко избежать в процессе написания программы, но совсем нелегко избежать их при внесении изменений в нетривиальную программу, а особенно, если это большая программа, написанная кем-то другим. Еще труднее избежать таких ошибок потому, что функции типа print() часто пишутся так, чтобы можно было воспользоваться общностью классов:
             void print(const employee* e)
             {
               cout << e->name << '\t' << e->department << '\n';
               // ...
               if (e->type == M) {
                  manager* p = (manager*) e;
                  cout << "level" << p->level << '\n';
                  // ...
               }
             }
Операторы if, подобные приведенным в примере, сложно найти в большой функции, работающей со многими производными классами. Но даже когда они найдены, нелегко понять, что происходит на самом деле. Кроме того, при всяком добавлении нового вида служащих требуются изменения во всех важных функциях программы, т.е. функциях, проверяющих поле типа. В результате приходится править важные части программы, увеличивая тем самым время на отладку этих частей.

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


6.2.5 Виртуальные функции

С помощью виртуальных функций можно преодолеть трудности, возникающие при использовании поля типа. В базовом классе описываются функции, которые могут переопределяться в любом производном классе. Транслятор и загрузчик обеспечат правильное соответствие между объектами и применяемыми к ним функциями:
         class employee {
           char* name;
           short department;
           // ...
           employee* next;
           static employee* list;
         public:
           employee(char* n, int d);
           // ...
           static void print_list();
           virtual void print() const;

        };
Служебное слово virtual (виртуальная) показывает, что функция print() может иметь разные версии в разных производных классах, а выбор нужной версии при вызове print() - это задача транслятора. Тип функции указывается в базовом классе и не может быть переопределен в производном классе. Определение виртуальной функции должно даваться для того класса, в котором она была впервые описана (если только она не является чисто виртуальной функцией, см. $$6.3). Например:
          void employee::print() const
          {
             cout << name << '\t' << department << '\n';
             // ...
          }
Мы видим, что виртуальную функцию можно использовать, даже если нет производных классов от ее класса. В производном же классе не обязательно переопределять виртуальную функцию, если она там не нужна. При построении производного класса надо определять только те функции, которые в нем действительно нужны:
           class manager : public employee {
              employee* group;
              short     level;
              // ...
           public:
              manager(char* n, int d);
              // ...
              void print() const;
           };
Место функции print_employee() заняли функции-члены print(), и она стала не нужна. Список служащих строит конструктор employee ($$6.2.2). Напечатать его можно так:
           void employee::print_list()
           {
             for ( employee* p = list; p; p=p->next) p->print();
           }
Данные о каждом служащем будут печататься в соответствии с типом записи о нем. Поэтому программа
          int main()
          {
             employee e("J.Brown",1234);
             manager m("J.Smith",2,1234);
             employee::print_list();
          }
напечатает

          J.Smith 1234
                  level 2
          J.Brown 1234

Обратите внимание, что функция печати будет работать даже в том случае, если функция employee_list() была написана и оттранслирована еще до того, как был задуман конкретный производный класс manager! Очевидно, что для правильной работы виртуальной функции нужно в каждом объекте класса employee хранить некоторую служебную информацию о типе. Как правило, реализации в качестве такой информации используют просто указатель. Этот указатель хранится только для объектов класса с виртуальными функциями, но не для объектов всех классов, и даже для не для всех объектов производных классов. Дополнительная память отводится только для классов, в которых описаны виртуальные функции. Заметим, что при использовании поля типа, для него все равно нужна дополнительная память.

Если в вызове функции явно указана операция разрешения области видимости ::, например, в вызове manager::print(), то механизм вызова виртуальной функции не действует. Иначе подобный вызов привел бы к бесконечной рекурсии. Уточнение имени функции дает еще один положительный эффект: если виртуальная функция является подстановкой (в этом нет ничего необычного), то в вызове с операцией :: происходит подстановка тела функции. Это эффективный способ вызова, который можно применять в важных случаях, когда одна виртуальная функция обращается к другой с одним и тем же объектом. Пример такого случая - вызов функции manager::print(). Поскольку тип объекта явно задается в самом вызове manager::print(), нет нужды определять его в динамике для функции employee::print(), которая и будет вызываться.


6.3 Абстрактные классы

Многие классы сходны с классом employee тем, что в них можно дать разумное определение виртуальным функциям. Однако, есть и другие классы. Некоторые, например, класс shape, представляют абстрактное понятие (фигура), для которого нельзя создать объекты. Класс shape приобретает смысл только как базовый класс в некотором производном классе. Причиной является то, что невозможно дать осмысленное определение виртуальных функций класса shape:
            class shape {
               // ...
            public:
               virtual void rotate(int) { error("shape::rotate"); }
               virtual void draw()  { error("shape::draw"): }
               // нельзя ни вращать, ни рисовать абстрактную фигуру
               // ...
            };
Создание объекта типа shape (абстрактной фигуры) законная, хотя совершенно бессмысленная операция:
           shape s;  // бессмыслица: ``фигура вообще''
Она бессмысленна потому, что любая операция с объектом s приведет к ошибке.

Лучше виртуальные функции класса shape описать как чисто виртуальные. Сделать виртуальную функцию чисто виртуальной можно, добавив инициализатор = 0:

           class shape {
             // ...
           public:
             virtual void rotate(int) = 0; // чисто виртуальная функция
             virtual void draw() = 0;      // чисто виртуальная функция
           };
Класс, в котором есть виртуальные функции, называется абстрактным. Объекты такого класса создать нельзя:
           shape s;   // ошибка: переменная абстрактного класса shape
Абстрактный класс можно использовать только в качестве базового для другого класса:
           class circle : public shape {
             int radius;
           public:
             void rotate(int) { } // нормально:
                                  // переопределение shape::rotate
             void draw();         // нормально:
                                  // переопределение shape::draw

             circle(point p, int r);
           };
Если чисто виртуальная функция не определяется в производном классе, то она и остается таковой, а значит производный класс тоже является абстрактным. При таком подходе можно реализовывать классы поэтапно:
           class X {
           public:
             virtual void f() = 0;
             virtual void g() = 0;
           };

           X b;   // ошибка: описание объекта абстрактного класса X

           class Y : public X {
             void f();  // переопределение X::f
           };

           Y b;   // ошибка: описание объекта абстрактного класса Y

           class Z : public Y {
             void g();  // переопределение X::g
           };

           Z c;   // нормально
Абстрактные классы нужны для задания интерфейса без уточнения каких-либо конкретных деталей реализации. Например, в операционной системе детали реализации драйвера устройства можно скрыть таким абстрактным классом:
           class character_device {
           public:
              virtual int open() = 0;
              virtual int close(const char*) = 0;
              virtual int read(const char*, int) =0;
              virtual int write(const char*, int) = 0;
              virtual int ioctl(int ...) = 0;
              // ...
           };
Настоящие драйверы будут определяться как производные от класса character_device.

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


6.4 Пример законченной программы

Рассмотрим программу рисования геометрических фигур на экране. Она естественным образом распадается на три части:

   [1] монитор экрана: набор функций и структур данных низкого уровня для работы с экраном; оперирует только такими понятиями, как точки, линии;
   [2] библиотека фигур: множество определений фигур общего вида (например, прямоугольник, окружность) и стандартные функции для работы с ними;
   [3] прикладная программа: конкретные определения фигур, относящихся к задаче, и работающие с ними функции.

Как правило, эти три части программируются разными людьми в разных организациях и в разное время, причем они обычно создаются в перечисленном порядке. При этом естественно возникают затруднения, поскольку, например, у разработчика монитора нет точного представления о том, для каких задач в конечном счете он будет использоваться. Наш пример будет отражать этот факт. Чтобы пример имел допустимый размер, библиотека фигур весьма ограничена, а прикладная программа тривиальна. Используется совершенно примитивное представление экрана, чтобы даже читатель, на машине которого нет графических средств, сумел поработать с этой программой. Можно легко заменить монитор экрана на более развитую программу, не изменяя при этом библиотеку фигур или прикладную программу.


6.4.1 Монитор экрана

Вначале было желание написать монитор экрана на С, чтобы еще больше подчеркнуть разделение между уровнями реализации. Но это оказалось утомительным, и поэтому выбрано компромиссное решение: стиль программирования, принятый в С (нет функций-членов, виртуальных функций, пользовательских операций и т.д.), но используются конструкторы, параметры функций полностью описываются и проверяются и т.д. Этот монитор очень напоминает программу на С, которую модифицировали, чтобы воспользоваться возможностями С++, но полностью переделывать не стали.

Экран представлен как двумерный массив символов и управляется функциями put_point() и put_line(). В них для связи с экраном используется структура point:

         // файл screen.h

         const int XMAX=40;
         const int YMAX=24;

         struct point {
             int x, y;
             point() { }
             point(int a,int b) { x=; y=b; }
         };

         extern void put_point(int a, int b);
         inline void put_point(point p) { put_point(p.x,p.y); }

         extern void put_line(int, int, int, int);
         extern void put_line(point a, point b)
            { put_line(a.x,a.y,b.x,b.y); }

         extern void screen_init();
         extern void screen_destroy();
         extern void screen_refresh();
         extern void screen_clear();

         #include <iostream.h>
До вызова функций, выдающих изображение на экран (put_...), необходимо обратиться к функции инициализации экрана screen_init(). Изменения в структуре данных, описывающей экран, станут видимы на нем только после вызова функции обновления экрана screen_refresh(). Читатель может убедиться, что обновление экрана происходит просто с помощью копирования новых значений в массив, представляющий экран. Приведем функции и определения данных для управления экраном:
         #include "screen.h"
         #include <stream.h>

         enum color { black='*', white=' ' };

         char screen[XMAX] [YMAX];

         void screen_init()
         {
           for (int y=0; y<YMAX; y++)
               for (int x=0; x<XMAX; x++)
                   screen[x] [y] = white;
         }
Функция
         void screen_destroy() { }
приведена просто для полноты картины. В реальных системах обычно нужны подобные функции уничтожения объекта.

Точки записываются, только если они попадают на экран:

        inline int on_screen(int a, int b)  // проверка попадания
        {
           return 0<=a && a <XMAX && 0<=b && b<YMAX;
        }

        void put_point(int a, int b)
        {
           if (on_screen(a,b)) screen[a] [b] = black;
        }
Для рисования прямых линий используется функция put_line():
        void put_line(int x0, int y0, int x1, int y1)
        /*
  Нарисовать отрезок прямой (x0,y0) - (x1,y1).
  Уравнение прямой: b(x-x0) + a(y-y0) = 0.
  Минимизируется величина abs(eps),
  где eps = 2*(b(x-x0)) + a(y-y0).
  См. Newman, Sproull
           ``Principles of interactive Computer Graphics''
           McGraw-Hill, New York, 1979. pp. 33-34.
         */
         {
           register int dx = 1;
           int a = x1 - x0;
           if (a < 0) dx = -1, a = -a;

           register int dy = 1;
           int b = y1 - y0;
           if (b < 0) dy = -1, b = -b;

           int two_a = 2*a;
           int two_b = 2*b;
           int xcrit = -b + two_a;
           register int eps = 0;

           for (;;) {
             put_point(x0,y0);
             if (x0==x1 && y0==y1) break;
             if (eps <= xcrit) x0 +=dx, eps +=two_b;
             if (eps>=a || a<b) y0 +=dy, eps -=two_a;
           }
         }
Имеются функции для очистки и обновления экрана:
       void screen_clear() { screen_init(); }

       void screen_refresh()
       {
        for (int y=YMAX-1; 0<=y; y--) { // с верхней строки до нижней
           for (int x=0; x<XMAX; x++)   // от левого столбца до правого
               cout << screen[x] [y];
           cout << '\n';
        }
      }
Но нужно понимать, что все эти определения хранятся в некоторой библиотеке как результат работы транслятора, и изменить их нельзя.



6.4.2 Библиотека фигур

Начнем с определения общего понятия фигуры. Определение должно быть таким, чтобы им можно было воспользоваться (как базовым классом shape) в разных классах, представляющих все конкретные фигуры
 (окружности, квадраты и т.д.). Оно также должно позволять работать со всякой фигурой исключительно с помощью интерфейса, определяемого классом shape:
        struct shape {
           static shape* list;
           shape* next;

           shape() { next = list; list = this; }

           virtual point north() const = 0;
           virtual point south() const = 0;
           virtual point east() const = 0;
           virtual point west() const = 0;
           virtual point neast() const = 0;
           virtual point seast() const = 0;
           virtual point nwest() const = 0;
           virtual point swest() const = 0;

           virtual void draw() = 0;
           virtual void move(int, int) = 0;
         };
Фигуры помещаются на экран функцией draw(), а движутся по нему с помощью move(). Фигуры можно помещать относительно друг друга, используя понятие точек контакта. Для обозначения точек контакта используются названия сторон света в компасе: north - север, ... , neast - северо-восток, ... , swest - юго-запад. Класс каждой конкретной фигуры сам определяет смысл этих точек и определяет, как рисовать фигуру. Конструктор shape::shape() добавляет фигуру к списку фигур shape::list. Для построения этого списка используется член next, входящий в каждый объект shape. Поскольку нет смысла в объектах типа общей фигуры, класс shape определен как абстрактный класс. Для задания отрезка прямой нужно указать две точки или точку и целое. В последнем случае отрезок будет горизонтальным, а целое задает его длину. Знак целого показывает, где должна находиться заданная точка относительно конечной точки, т.е. слева или справа от нее:
          class line : public shape {
          /*
отрезок прямой ["w", "e" ]
            north() определяет точку - `` выше центра отрезка и
так далеко на север, как самая его северная точка''
          */
            point w, e;
         public:
            point north() const
              { return point((w.x+e.x)/2,e.y<w.y?w.y:e:y); }
            point south() const
              { return point((w.x+e.x)/2,e.y<w.y?e.y:w.y); }
            point east() const;
            point west() const;
            point neast() const;
            point seast() const;
            point nwest() const;
            point swest() const;

            void move(int a, int b)
               { w.x +=a; w.y +=b; e.x +=a; e.y +=b; }
            void draw() { put_line(w,e); }

            line(point a, point b) { w = a; e = b; }
            line(point a, int l) { w = point(a.x+l-1,a.y); e = a; }
         };
Аналогично определяется прямоугольник:
         class rectangle : public shape {
         /*   nw ------ n ----- ne
              |                  |
              |                  |
              w         c        e
              |                  |
              |                  |
              sw ------ s ----- se
         */
            point sw, ne;
         public:
            point north() const { return point((sw.x+ne.x)/2,ne.y); }
            point south() const { return point((sw.x+ne.x)/2,sw.y); }
            point east() const;
            point west() const;
            point neast() const { return ne; }
            point seast() const;
            point nwest() const;
            point swest() const { return sw; }

            void move(int a, int b)
            { sw.x+=a; sw.y+=b; ne.x+=a; ne.y+=b; }
            void draw();

            rectangle(point,point);
          };
Прямоугольник строится по двум точкам. Конструктор усложняется, так как необходимо выяснять относительное положение этих точек:
          rectangle::rectangle(point a, point b)
          {
            if (a.x <= b.x) {
               if (a.y <= b.y) {
                  sw = a;
                  ne = b;
               }
               else {
                  sw = point(a.x,b.y);
                  ne = point(b.x,a.y);
               }
             }
             else {
               if (a.y <= b.y) {
                  sw = point(b.x,a.y);
                  ne = point(a.x,b.y);
               }
               else {
                  sw = b;
                  ne = a;
               }
             }
           }
Чтобы нарисовать прямоугольник, надо нарисовать четыре отрезка:
           void rectangle::draw()
           {
             point nw(sw.x,ne.y);
             point se(ne.x,sw.y);
             put_line(nw,ne);
             put_line(ne,se);
             put_line(se,sw);
             put_line(sw,nw);
           }
В библиотеке фигур есть определения фигур и функции для работы с ними:
          void shape_refresh();      // нарисовать все фигуры
          void stack(shape* p, const shape* q); // поместить p над q
Функция обновления фигур нужна, чтобы работать с нашим примитивным представлением экрана; она просто заново рисует все фигуры. Отметим, что эта функция не имеет понятия, какие фигуры она рисует:
          void shape_refresh()
          {
            screen_clear();
            for (shape* p = shape::list; p; p=p->next) p->draw();
            screen_refresh();
          }
Наконец, есть одна действительно сервисная функция, которая рисует одну фигуру над другой. Для этого она определяет юг (south()) одной фигуры как раз над севером (north()) другой:
          void stack(shape* p, const shape* q) // поместить p над q
          {
            point n = q->north();
            point s = p->south();
            p->move(n.x-s.x,n.y-s.y+1);
          }
Представим теперь, что эта библиотека является собственностью некоторой фирмы, продающей программы, и, что она продает только заголовочный файл с определениями фигур и оттранслированные определения функций. Все равно вы сможете определить новые фигуры, воспользовавшись для этого купленными вами функциями.



6.4.3 Прикладная программа

Прикладная программа предельно проста. Определяется новая фигура myshape (если ее нарисовать, то она напоминает лицо), а затем приводится функция main(), в которой она рисуется со шляпой. Вначале дадим описание фигуры myshape:
           #include "shape.h"

           class myshape : public rectangle {
             line* l_eye;   // левый глаз
             line* r_eye;   // правый глаз
             line* mouth;   // рот
           public:
             myshape(point, point);
             void draw();
             void move(int, int);
           };
Глаза и рот являются отдельными независимыми объектами которые создает конструктор класса myshape:
           myshape::myshape(point a, point b) : rectangle(a,b)
           {
             int ll = neast().x-swest().x+1;
             int hh = neast().y-swest().y+1;
             l_eye = new line(
                 point(swest().x+2,swest().y+hh*3/4),2);
             r_eye = new line(
                 point(swest().x+ll-4,swest().y+hh*3/4),2);
             mouth = new line(
                 point(swest().x+2,swest().y+hh/4),ll-4);
           }
Объекты, представляющие глаза и рот, выдаются функцией shape_refresh() по отдельности. В принципе с ними можно работать независимо от объекта my_shape, к которому они принадлежат. Это один из способов задания черт лица для строящегося иерархически объекта myshape. Как это можно сделать иначе, видно из задания носа. Никакой тип "нос" не определяется, он просто дорисовывается в функции draw():
           void myshape::draw()
           {
             rectangle::draw();
             int a = (swest().x+neast().x)/2;
             int b = (swest().y+neast().y)/2;
             put_point(point(a,b));
           }
Движение фигуры myshape сводится к движению объекта базового класса rectangle и к движению вторичных объектов (l_eye, r_eye и mouth):
           void myshape::move(int a, int b)
           {
             rectangle::move(a,b);
             l_eye->move(a,b);
             r_eye->move(a,b);
             mouth->move(a,b);
           }
Наконец, определим несколько фигур и будем их двигать:
           int main()
           {
             screen_init();
             shape* p1 = new rectangle(point(0,0),point(10,10));
             shape* p2 = new line(point(0,15),17);
             shape* p3 = new myshape(point(15,10),point(27,18));
             shape_refresh();
             p3->move(-10,-10);
             stack(p2,p3);
             stack(p1,p2);
             shape_refresh();
             screen_destroy();
             return 0;
           }
Вновь обратим внимание на то, что функции, подобные shape_refresh() и stack(), работают с объектами, типы которых были определены заведомо после определения этих функций (и, вероятно, после их трансляции).

Вот получившееся лицо со шляпой:

           ***********
           *         *
           *         *
           *         *
           *         *
           *         *
           *         *
           *         *
           ***********
        *****************
           ***********
           *         *
           * **   ** *
           *         *
           *    *    *
           *         *
           * ******* *
           *         *
           ***********
Для упрощения примера копирование и удаление фигур не обсуждалось.



6.5 Множественное наследование

В $$1.5.3 и $$6.2.3 уже говорилось, что у класса может быть несколько прямых базовых классов. Это значит, что в описании класса после : может быть указано более одного класса. Рассмотрим задачу моделирования, в которой параллельные действия представлены стандартной библиотекой классов task, а сбор и выдачу информации обеспечивает библиотечный класс displayed. Тогда класс моделируемых объектов (назовем его
 satellite) можно определить так:
          class satellite : public task, public displayed {
             // ...
          };
Такое определение обычно называется множественным наследованием. Обратно, существование только одного прямого базового класса называется единственным наследованием.

Ко всем определенным в классе satellite операциям добавляется объединение операций классов task и displayed:

         void f(satellite& s)
         {
           s.draw();    // displayed::draw()
           s.delay(10); // task::delay()
           s.xmit();    // satellite::xmit()
         }
С другой стороны, объект типа satellite можно передавать функциям с параметром типа task или displayed:
         void highlight(displayed*);
         void suspend(task*);

         void g(satellite* p)
         {
           highlight(p);   // highlight((displayed*)p)
           suspend(p);     // suspend((task*)p);
         }
Очевидно, реализация этой возможности требует некоторого (простого) трюка от транслятора: нужно функциям с параметрами task и displayed передать разные части объекта типа satellite.

Для виртуальных функций, естественно, вызов и так выполнится правильно:

         class task {
            // ...
            virtual pending() = 0;
         };

         class displayed {
            // ...
            virtual void draw() = 0;
         };

         class satellite : public task, public displayed {
            // ...
            void pending();
            void draw();
         };
Здесь функции satellite::draw() и satellite::pending() для объекта типа satellite будут вызываться так же, как если бы он был объектом типа displayed или task, соответственно.

Отметим, что ориентация только на единственное наследование ограничивает возможности реализации классов displayed, task и satellite. В таком случае класс satellite мог бы быть task или displayed, но не то и другое вместе (если, конечно, task не является производным от displayed или наоборот). В любом случае теряется гибкость.


6.5.1 Множественное вхождение базового класса

Возможность иметь более одного базового класса влечет за собой возможность неоднократного вхождения класса как базового. Допустим, классы task и displayed являются производными класса link, тогда в satellite он будет входить дважды:
          class task : public link {
             // link используется для связывания всех
             // задач в список (список диспетчера)

             // ...
          };

          class displayed : public link {
             // link используется для связывания всех
             // изображаемых объектов (список изображений)

             // ...
          };
Но проблем не возникает. Два различных объекта link используются для различных списков, и эти списки не конфликтуют друг с другом. Конечно, без риска неоднозначности нельзя обращаться к членам класса
 link, но как это сделать корректно, показано в следующем разделе. Графически объект satellite можно представить так:

Но можно привести примеры, когда общий базовый класс не должен представляться двумя различными объектами (см. $$6.5.3).



6.5.2 Разрешение неоднозначности

Естественно, у двух базовых классов могут быть функции-члены с одинаковыми именами:
          class task {
             // ...
             virtual debug_info* get_debug();
          };

          class displayed {
             // ...
             virtual debug_info* get_debug();
          };
При использовании класса satellite подобная неоднозначность функций должна быть разрешена:
          void f(satellite* sp)
          {
           debug_info* dip = sp->get_debug(); //ошибка: неоднозначность
           dip = sp->task::get_debug();       // нормально
           dip = sp->displayed::get_debug();  // нормально
          }
Однако, явное разрешение неоднозначности хлопотно, поэтому для ее устранения лучше всего определить новую функцию в производном классе:
          class satellite : public task, public derived {
             // ...
             debug_info* get_debug()
             {
               debug_info* dip1 = task:get_debug();
               debug_info* dip2 = displayed::get_debug();
               return dip1->merge(dip2);
             }
          };
Тем самым локализуется информация из базовых для satellite классов. Поскольку satellite::get_debug() является переопределением функций get_debug() из обоих базовых классов, гарантируется, что именно она будет вызываться при всяком обращении к get_debug() для объекта типа satellite.

Транслятор выявляет коллизии имен, возникающие при определении одного и того же имени в более, чем одном базовом классе. Поэтому программисту не надо указывать какое именно имя используется, кроме случая, когда его использование действительно неоднозначно. Как правило использование базовых классов не приводит к коллизии имен. В большинстве случаев, даже если имена совпадают, коллизия не возникает, поскольку имена не используются непосредственно для объектов производного класса.

Аналогичная проблема, когда в двух классах есть функции с одним именем, но разным назначением, обсуждается в $$13.8 на примере функции draw() для классов Window и Cowboy.

Если неоднозначности не возникает, излишне указывать имя базового класса при явном обращении к его члену. В частности, если множественное наследование не используется, вполне достаточно использовать обозначение типа "где-то в базовом классе". Это позволяет программисту не запоминать имя прямого базового класса и спасает его от ошибок (впрочем, редких), возникающих при перестройке иерархии классов. Например, в функции из $$6.2.5

         void manager::print()
         {
           employee::print();
           // ...
         }
предполагается, что employee - прямой базовый класс для manager. Результат этой функции не изменится, если employee окажется косвенным базовым классом для manager, а в прямом базовом классе функции
 print() нет. Однако, кто-то мог бы следующим образом перестроить классы:
         class employee {
           // ...
           virtual void print();
         };

         class foreman : public employee {
           // ...
           void print();
         };

         class manager : public foreman {
           // ...
           void print();
         };
Теперь функция foreman::print() не будет вызываться, хотя почти наверняка предполагался вызов именно этой функции. С помощью небольшой хитрости можно преодолеть эту трудность:
         class foreman : public employee {
           typedef employee inherited;
           // ...
           void print();
         };

         class manager : public foreman {
           typedef foreman inherited;
           // ...
           void print();
         };

         void manager::print()
         {
           inherited::print();
           // ...
         }
Правила областей видимости, в частности те, которые относятся к вложенным типам, гарантируют, что возникшие несколько типов inherited не будут конфликтовать друг с другом. В общем-то дело вкуса, считать решение с типом inherited наглядным или нет.



6.5.3 Виртуальные базовые классы

В предыдущих разделах множественное наследование рассматривалось как существенный фактор, позволяющий за счет слияния классов безболезненно интегрировать независимо создававшиеся программы. Это самое основное применение множественного наследования, и, к счастью (но не случайно), это самый простой и надежный способ его применения.

Иногда применение множественного наследования предполагает достаточно тесную связь между классами, которые рассматриваются как "братские" базовые классы. Такие классы-братья обычно должны проектироваться совместно. В большинстве случаев для этого не требуется особый стиль программирования, существенно отличающийся от того, который мы только что рассматривали. Просто на производный класс возлагается некоторая дополнительная работа. Обычно она сводится к переопределению одной или нескольких виртуальных функций (см. $$13.2 и $$8.7). В некоторых случаях классы-братья должны иметь общую информацию. Поскольку С++ - язык со строгим контролем типов, общность информации возможна только при явном указании того, что является общим в этих классах. Способом такого указания может служить виртуальный базовый класс.

Виртуальный базовый класс можно использовать для представления "головного" класса, который может конкретизироваться разными способами:

         class window {
           // головная информация
           virtual void draw();
         };
Для простоты рассмотрим только один вид общей информации из класса window - функцию draw(). Можно определять разные более развитые классы, представляющие окна (window). В каждом определяется своя
 (более развитая) функция рисования (draw):
          class window_w_border : public virtual window {
             // класс "окно с рамкой"
             // определения, связанные с рамкой
             void draw();
          };

          class window_w_menu : public virtual window {
             // класс "окно с меню"
             // определения, связанные с меню
             void draw();
          };
Теперь хотелось бы определить окно с рамкой и меню:
          class window_w_border_and_menu
                : public virtual window,
                public window_w_border,
                public window_w_menu {
                // класс "окно с рамкой и меню"
                void draw();
          };
Каждый производный класс добавляет новые свойства окна. Чтобы воспользоваться комбинацией всех этих свойств, мы должны гарантировать, что один и тот же объект класса window используется для представления вхождений базового класса window в эти производные классы. Именно это обеспечивает описание window во всех производных классах как виртуального базового класса.

Можно следующим образом изобразить состав объекта класса window_w_border_and_menu:

Чтобы увидеть разницу между обычным и виртуальным наследованием, сравните этот рисунок с рисунком из $$6.5, показывающим состав объекта класса satellite. В графе наследования каждый базовый класс с данным именем, который был указан как виртуальный, будет представлен единственным объектом этого класса. Напротив, каждый базовый класс, который при описании наследования не был указан как виртуальный, будет представлен своим собственным объектом.

Теперь надо написать все эти функции draw(). Это не слишком трудно, но для неосторожного программиста здесь есть ловушка. Сначала пойдем самым простым путем, который как раз к ней и ведет:

            void window_w_border::draw()
            {
               window::draw();
               // рисуем рамку
            }

            void window_w_menu::draw()
            {
              window::draw();
              // рисуем меню
            }
Пока все хорошо. Все это очевидно, и мы следуем образцу определения таких функций при условии единственного наследования ($$6.2.1), который работал прекрасно. Однако, в производном классе следующего уровня появляется ловушка:
           void window_w_border_and_menu::draw() // ловушка!
           {
              window_w_border::draw();
              window_w_menu::draw();

              // теперь операции, относящиеся только
              // к окну с рамкой и меню
            }
На первый взгляд все вполне нормально. Как обычно, сначала выполняются все операции, необходимые для базовых классов, а затем те, которые относятся собственно к производным классам. Но в результате функция window::draw() будет вызываться дважды! Для большинства графических программ это не просто излишний вызов, а порча картинки на экране. Обычно вторая выдача на экран затирает первую.

Чтобы избежать ловушки, надо действовать не так поспешно. Мы отделим действия, выполняемые базовым классом, от действий, выполняемых из базового класса. Для этого в каждом классе введем функцию _draw(), которая выполняет нужные только для него действия, а функция draw() будет выполнять те же действия плюс действия, нужные для каждого базового класса. Для класса window
изменения сводятся к введению излишней функции:

          class window {
           // головная информация
           void _draw();
           void draw();
          };
Для производных классов эффект тот же:
           class window_w_border : public virtual window {
              // класс "окно с рамкой"
              // определения, связанные с рамкой
              void _draw();
              void draw();
           };

           void window_w_border::draw()
           {
              window::_draw();
              _draw();   // рисует рамку
           };
Только для производного класса следующего уровня проявляется отличие функции, которое и позволяет обойти ловушку с повторным вызовом window::draw(), поскольку теперь вызывается window::_draw()
и только один раз:
           class window_w_border_and_menu
               : public virtual window,
               public window_w_border,
               public window_w_menu {

               void _draw();
               void draw();
           };

           void window_w_border_and_menu::draw()
           {
             window::_draw();
             window_w_border::_draw();
             window_w_menu::_draw();

             _draw();  // теперь операции, относящиеся только
                       // к окну с рамкой и меню
           }
Не обязательно иметь обе функции window::draw() и window::_draw(), но наличие их позволяет избежать различных простых описок.

В этом примере класс window служит хранилищем общей для window_w_border и window_w_menu информации и определяет интерфейс для общения этих двух классов. Если используется единственное наследование, то общность информации в дереве классов достигается тем, что эта информация передвигается к корню дерева до тех пор, пока она не станет доступна всем заинтересованным в ней узловым классам. В результате легко возникает неприятный эффект: корень дерева или близкие к нему классы используются как пространство глобальных имен для всех классов дерева, а иерархия классов вырождается в множество несвязанных объектов.

Существенно, чтобы в каждом из классов-братьев переопределялись функции, определенные в общем виртуальном базовом классе. Таким образом каждый из братьев может получить свой вариант операций, отличный от других. Пусть в классе window есть общая функция ввода get_input():

           class window {
            // головная информация
            virtual void draw();
            virtual void get_input();
          };
В одном из производных классов можно использовать эту функцию, не задумываясь о том, где она определена:
          class window_w_banner : public virtual window {
             // класс "окно с заголовком"
             void draw();
             void update_banner_text();
          };

          void window_w_banner::update_banner_text()
          {
            // ...
            get_input();
            // изменить текст заголовка
          }
В другом производном классе функцию get_input() можно определять, не задумываясь о том, кто ее будет использовать:
          class window_w_menu : public virtual window {
             // класс "окно с меню"
             // определения, связанные с меню
             void draw();
             void get_input(); // переопределяет window::get_input()
          };
Все эти определения собираются вместе в производном классе следующего уровня:
          class window_w_banner_and_menu
             : public virtual window,
             public window_w_banner,
             public window_w_menu
          {
             void draw();
          };
Контроль неоднозначности позволяет убедиться, что в классах-братьях определены разные функции:
          class window_w_input : public virtual window {
             // ...
             void draw();
             void get_input();  // переопределяет window::get_input
          };

          class window_w_input_and_menu
             : public virtual window,
             public window_w_input,
             public window_w_menu
         {             // ошибка: оба класса window_w_input и
                       // window_w_menu переопределяют функцию
                       // window::get_input
            void draw();
         };
Транслятор обнаруживает подобную ошибку, а устранить неоднозначность можно обычным способом: ввести в классы window_w_input и
 window_w_menu функцию, переопределяющую "функцию-нарушителя", и каким-то образом устранить неоднозначность:
         class window_w_input_and_menu
           : public virtual window,
           public window_w_input,
           public window_w_menu
        {
           void draw();
           void get_input();
        };
В этом классе window_w_input_and_menu::get_input() будет переопределять все функции get_input(). Подробно механизм разрешения неоднозначности описан в $$R.10.1.1.



6.6 Контроль доступа

Член класса может быть частным (private), защищенным (protected) или общим (public): Частный член класса X могут использовать только функции-члены и друзья класса X.

Защищенный член класса X могут использовать только функции-члены и друзья класса X, а так же функции-члены и друзья всех производных от X классов (см. $$5.4.1).

Общий член можно использовать в любой функции. Эти правила соответствуют делению обращающихся к классу функций на три вида: функции, реализующие класс (его друзья и члены), функции, реализующие производный класс (друзья и члены производного класса) и все остальные функции.

Контроль доступа применяется единообразно ко всем именам. На контроль доступа не влияет, какую именно сущность обозначает имя. Это означает, что частными могут быть функции-члены, константы и т.д. наравне с частными членами, представляющими данные:

             class X {
             private:
               enum { A, B };
               void f(int);
               int a;
             };

             void X::f(int i)
             {
               if (i<A) f(i+B);
               a++;
             }

             void g(X& x)
             {
               int i = X::A;     // ошибка: X::A частный член
               x.f(2);           // ошибка: X::f частный член
               x.a++;            // ошибка: X::a частный член
             }


6.6.1 Защищенные члены

Дадим пример защищенных членов, вернувшись к классу window из предыдущего раздела. Здесь функции _draw() предназначались только для использования в производных классах, поскольку предоставляли неполный набор возможностей, а поэтому не были достаточны удобны и надежны для общего применения. Они были как бы строительным материалом для более развитых функций. С другой стороны, функции draw()
предназначались для общего применения. Это различие можно выразить, разбив интерфейсы классов window на две части - защищенный интерфейс и общий интерфейс:
            class window {
            public:
              virtual void draw();
              // ...
            protected:
              void _draw();
              // другие функции, служащие строительным материалом
            private:
              // представление класса
            };
Такое разбиение можно проводить и в производных классах, таких, как window_w_border или window_w_menu.

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

Вот менее практичный, но более подробный пример:

            class X {
            // по умолчанию частная часть класса
               int priv;
            protected:
               int prot;
            public:
               int publ;
               void m();
            };
Для члена X::m доступ к членам класса неограничен:
            void X::m()
            {
              priv = 1;   // нормально
              prot = 2;   // нормально
              publ = 3;   // нормально
            }
Член производного класса имеет доступ только к общим и защищенным членам:
            class Y : public X {
               void mderived();
            };

            Y::mderived()
            {
              priv = 1;    // ошибка: priv частный член
              prot = 2;    // нормально: prot защищенный член, а
                           // mderived() член производного класса Y
              publ = 3;    // нормально: publ общий член
            }
В глобальной функции доступны только общие члены:
            void f(Y* p)
            {
              p->priv = 1;  // ошибка: priv частный член
              p->prot = 2;  // ошибка: prot защищенный член, а f()
                            // не друг или член классов X и Y
              p->publ = 3;  // нормально: publ общий член
            }


6.6.2 Доступ к базовым классам

Подобно члену базовый класс можно описать как частный, защищенный или общий:
            class X {
            public:
              int a;
              // ...
            };

            class Y1 : public X {  };
            class Y2 : protected X { };
            class Y3 : private X { };
Поскольку X - общий базовый класс для Y1, в любой функции, если есть необходимость, можно (неявно) преобразовать Y1* в X*, и притом в ней будут доступны общие члены класса X:
            void f(Y1* py1, Y2* py2, Y3* py3)
            {
              X* px = py1;  // нормально: X - общий базовый класс Y1
              py1->a = 7;   // нормально
              px = py2;     // ошибка: X - защищенный базовый класс Y2
              py2->a = 7;   // ошибка
              px = py3;     // ошибка: X - частный базовый класс Y3
              py3->a = 7;   // ошибка
            }
Теперь пусть описаны
            class Y2 : protected X { };
            class Z2 : public Y2 { void f(); };
Поскольку X - защищенный базовый класс Y2, только друзья и члены Y2, а также друзья и члены любых производных от Y2 классов (в частности Z2) могут при необходимости преобразовывать (неявно) Y2* в X*. Кроме того они могут обращаться к общим и защищенным членам класса X:
            void Z2::f(Y1* py1, Y2* py2, Y3* py3)
            {
              X* px = py1; // нормально: X - общий базовый класс Y1
              py1->a = 7; // нормально
              px = py2;   // нормально: X - защищенный базовый класс Y2,
                          // а Z2 - производный класс Y2
              py2->a = 7; // нормально
              px = py3;   // ошибка: X - частный базовый класс Y3
              py3->a = 7; // ошибка
            }
Наконец, рассмотрим:
            class Y3 : private X { void f(); };
Поскольку X - частный базовый класс Y3, только друзья и члены Y3 могут при необходимости преобразовывать (неявно) Y3* в X*. Кроме того они могут обращаться к общим и защищенным членам класса X:
            void Y3::f(Y1* py1, Y2* py2, Y3* py3)
            {
              X* px = py1;  // нормально: X - общий базовый класс Y1
              py1->a = 7;   // нормально
              px = py2;     // ошибка: X - защищенный базовый класс Y2
              py2->a = 7;   // ошибка
              px = py3;     // нормально: X - частный базовый класс Y3,
                            // а Y3::f член Y3
              py3->a = 7;   // нормально
            }


6.7 Свободная память

Если определить функции operator new() и operator delete(), управление памятью для класса можно взять в свои руки. Это также можно, (а часто и более полезно), сделать для класса, служащего базовым для многих производных классов. Допустим, нам потребовались свои функции размещения и освобождения памяти для класса employee ($$6.2.5)
и всех его производных классов:
            class employee {
              // ...
            public:
              void* operator new(size_t);
              void operator delete(void*, size_t);
            };

            void* employee::operator new(size_t s)
            {
              // отвести память в `s' байтов
              // и возвратить указатель на нее
            }

            void employee::operator delete(void* p, size_t s)
            {
              // `p' должно указывать на память в `s' байтов,
              // отведенную функцией employee::operator new();
              // освободить эту память для повторного использования
            }
Назначение до сей поры загадочного параметра типа size_t становится очевидным. Это - размер освобождаемого объекта. При удалении простого служащего этот параметр получает значение sizeof(employee), а при удалении управляющего - sizeof(manager). Поэтому собственные функции классы для размещения могут не хранить размер каждого размещаемого объекта. Конечно, они могут хранить эти размеры (подобно функциям размещения общего назначения) и игнорировать параметр size_t в вызове operator delete(), но тогда вряд ли они будут лучше, чем функции размещения и освобождения общего назначения.

Как транслятор определяет нужный размер, который надо передать функции operator delete()? Пока тип, указанный в operator delete(), соответствует истинному типу объекта, все просто; но рассмотрим такой пример:

            class manager : public employee {
              int level;
              // ...
            };

            void f()
            {
              employee* p = new manager; // проблема
              delete p;
            }
В этом случае транслятор не сможет правильно определить размер. Как и в случае удаления массива, нужна помощь программиста. Он должен определить виртуальный деструктор в базовом классе employee:
            class employee {
              // ...
            public:
              // ...
              void* operator new(size_t);
              void operator delete(void*, size_t);
              virtual ~employee();
            };
Даже пустой деструктор решит нашу проблему:
           employee::~employee() { }
Теперь освобождение памяти будет происходить в деструкторе (а в нем размер известен), а любой производный от employee класс также будет вынужден определять свой деструктор (тем самым будет установлен нужный размер), если только пользователь сам не определит его. Теперь следующий пример пройдет правильно:
           void f()
           {
             employee* p = new manager; // теперь без проблем
             delete p;
           }
Размещение происходит с помощью (созданного транслятором) вызова
          employee::operator new(sizeof(manager))
а освобождение с помощью вызова
          employee::operator delete(p,sizeof(manager))
Иными словами, если нужно иметь корректные функции размещения и освобождения для производных классов, надо либо определить виртуальный деструктор в базовом классе, либо не использовать в функции освобождения параметр size_t. Конечно, можно было при проектировании языка предусмотреть средства, освобождающие пользователя от этой проблемы. Но тогда пользователь "освободился" бы и от определенных преимуществ более оптимальной, хотя и менее надежной системы.

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

          class X {
             // ...
          public:
             // ...
             virtual void f(); // в X есть виртуальная функция, поэтому
                               // определяем виртуальный деструктор
             virtual ~X();
          };


6.7.1 Виртуальные конструкторы

Узнав о виртуальных деструкторах, естественно спросить: "Могут ли конструкторы то же быть виртуальными?" Если ответить коротко - нет. Можно дать более длинный ответ: "Нет, но можно легко получить требуемый эффект".

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

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

Как правило "виртуальные конструкторы" являются стандартными конструкторами без параметров или конструкторами копирования, параметром которых служит тип результата:

          class expr {
             // ...
          public:
             expr();  // стандартный конструктор
             virtual expr* new_expr() { return new expr(); }
          };
Виртуальная функция new_expr() просто возвращает стандартно инициализированный объект типа expr, размещенный в свободной памяти. В производном классе можно переопределить функцию new_expr() так, чтобы она возвращала объект этого класса:
          class conditional : public expr {
             // ...
          public:
            conditional();  // стандартный конструктор
            expr* new_expr() { return new conditional(); }
          };
Это означает, что, имея объект класса expr, пользователь может создать объект в "точности такого же типа":
          void user(expr* p1, expr* p2)
          {
             expr* p3 = p1->new_expr();
             expr* p4 = p2->new_expr();
             // ...
           }
Переменным p3 и p4 присваиваются указатели неизвестного, но подходящего типа.

Тем же способом можно определить виртуальный конструктор копирования, называемый операцией размножения, но надо подойти более тщательно к специфике операции копирования:

           class expr {
              // ...
              expr* left;
              expr* right;
           public:
              // ...
                 // копировать `s' в `this'
              inline void copy(expr* s);
                 // создать копию объекта, на который смотрит this
              virtual expr* clone(int deep = 0);
           };
Параметр deep показывает различие между копированием собственно объекта (поверхностное копирование) и копированием всего поддерева, корнем которого служит объект (глубокое копирование). Стандартное значение 0 означает поверхностное копирование.

Функцию clone() можно использовать, например, так:

            void fct(expr* root)
            {
              expr* c1 = root->clone(1);  // глубокое копирование
              expr* c2 = root->clone();   // поверхностное копирование
              // ...
            }
Являясь виртуальной, функция clone() способна размножать объекты любого производного от expr класса.

Настоящее копирование можно определить так:

            void expr::copy(expression* s, int deep)
            {
              if (deep == 0) { // копируем только члены
                 *this = *s;
              }
              else { // пройдемся по указателям:
                 left = s->clone(1);
                 right = s->clone(1);
                 // ...
              }
            }
Функция expr::clone() будет вызываться только для объектов типа expr (но не для производных от expr классов), поэтому можно просто разместить в ней и возвратить из нее объект типа expr, являющийся собственной копией:
            expr* expr::clone(int deep)
            {
              expr* r = new expr();  // строим стандартное выражение
              r->copy(this,deep);    // копируем `*this' в `r'
              return r;
            }
Такую функцию clone() можно использовать для производных от expr классов, если в них не появляются члены-данные (а это как раз типичный случай):
            class arithmetic : public expr {
              // ...
              // новых членов-данных нет =>
              // можно использовать уже определенную функцию clone
            };
С другой стороны, если добавлены члены-данные, то нужно определять собственную функцию clone():
           class conditional : public expression {
              expr* cond;
           public:
              inline void copy(cond* s, int deep = 0);
              expr* clone(int deep = 0);
              // ...
           };
Функции copy() и clone() определяются подобно своим двойникам из expression:
           expr* conditional::clone(int deep)
           {
             conditional* r = new conditional();
             r->copy(this,deep);
             return r;
           }

           void conditional::copy(expr* s, int deep)
           {
             if (deep == 0) {
                *this = *s;
             }
             else {
                expr::copy(s,1);  // копируем часть expr
                cond = s->cond->clone(1);
             }
           }
Определение последней функции показывает отличие настоящего копирования в expr::copy() от полного размножения в expr::clone() (т.е. создания нового объекта и копирования в него). Простое копирование оказывается полезным для определения более сложных операций копирования и размножения. Различие между copy() и clone() эквивалентно различию между операцией присваивания и конструктором копирования ($$1.4.2) и эквивалентно различию между функциями _draw() и draw() ($$6.5.3). Отметим, что функция copy() не является виртуальной. Ей и не надо быть таковой, поскольку виртуальна вызывающая ее функция clone(). Очевидно, что простые операции копирования можно также определять как функции-подстановки.



6.7.2 Указание размещения

По умолчанию операция new создает указанный ей объект в свободной памяти. Как быть, если надо разместить объект в определенном месте? Этого можно добиться переопределением операции размещения. Рассмотрим простой класс:
        class X {
           // ...
        public:
          X(int);
          // ...
        };
Объект можно разместить в любом месте, если ввести в функцию размещения дополнительные параметры:
        // операция размещения в указанном месте:
        void* operator new(size_t, void* p) { return p; }
и задав эти параметры для операции new следующим образом:
        char buffer[sizeof(X)];

        void f(int i)
        {
          X* p = new(buffer) X(i); // разместить X в buffer
          // ...
        }
Функция operator new(), используемая операцией new, выбирается согласно правилам сопоставления параметров ($$R.13.2). Все функции operator new() должны иметь первым параметром size_t. Задаваемый этим параметром размер неявно передается операцией new.

Определенная нами функция operator new() с задаваемым размещением является самой простой из функций подобного рода. Можно привести другой пример функции размещения, выделяющей память из некоторой заданной области:

        class Arena {
           // ...
           virtual void* alloc(size_t) = 0;
           virtual void free(void*) = 0;
        };

        void operator new(size_t sz, Arena* a)
        {
          return a.alloc(sz);
        }
Теперь можно отводить память для объектов произвольных типов из различных областей (Arena):
        extern Arena* Persistent; // постоянная память
        extern Arena* Shared;     // разделяемая память

        void g(int i)
        {
          X* p = new(Persistent) X(i); // X в постоянной памяти
          X* q = new(Shared) X(i);     // X в разделяемой памяти
          // ...
        }
Если мы помещаем объект в область памяти, которая непосредственно не управляется стандартными функциями распределения свободной памяти, то надо позаботиться о правильном уничтожении объекта. Основным средством здесь является явный вызов деструктора:
        void h(X* p)
        {
          p->~X();             // вызов деструктора
          Persistent->free(p); // освобождение памяти
        }
Заметим, что явных вызовов деструкторов, как и глобальных функций размещения специального назначения, следует, по возможности, избегать. Бывают случаи, когда обойтись без них трудно, но новичок должен трижды подумать, прежде чем использовать явный вызов деструктора, и должен сначала посоветоваться с более опытным коллегой.



6.8 Упражнения

 1. (*1) Пусть есть класс
         class base {
         public:
            virtual void iam() { cout << "base\n"; }
         };
Определите два производных от base класса и в каждом определите функцию iam(), выдающую имя своего класса. Создайте объекты этих классов и вызовите iam() для них. Присвойте адреса объектов производных классов указателю типа base* и вызовите iam() с помощью этих указателей.
 2. (*2) Реализуйте примитивы управления экраном ($$6.4.1) разумным для вашей системы образом.
 3. (*2) Определите классы triangle (треугольник) и circle
    (окружность).
 4. (*2) Определите функцию, рисующую отрезок прямой, соединяющий две фигуры. Вначале надо найти самые ближайшие точки фигур, а затем соединить их.
 5. (*2) Измените пример с классом shape так, чтобы line было производным классом от rectangle, или наоборот.
 6. (*2) Пусть есть класс
         class char_vec {
            int sz;
            char element [1];
         public:
            static new_char_vec(int s);
            char& operator[] (int i) { return element[i]; }
            // ...
         };
Определите функцию new_char_vec() для отведения непрерывного
участка памяти для объектов char_vec так, чтобы элементы можно
было индексировать как массив element[]. В каком случае эта
функция вызовет серьезные трудности?
  7. (*1)Опишите структуры данных, которые нужны для
примера с классом shape из $$6.4, и объясните, как может
выполняться виртуальный вызов.
  8. (*1.5) Опишите структуры данных, которые нужны для примера
с классом satellite из $$6.5, и объясните, как может выполняться
виртуальный вызов.
  9. (*2) Опишите структуры данных, которые нужны для примера с
классом window из $$6.5.3, и объясните, как может выполняться
виртуальный вызов.
  10. (*2) Опишите класс графических объектов с набором возможных
операций, который будет общим базовым в библиотеке графических
объектов. Исследуйте какие-нибудь графические библиотеки,
чтобы понять, какие операции нужны. Определите класс объектов
базы данных с набором возможных операций, который будет
общим базовым классом объектов, хранящихся как последовательность
полей базы данных. Исследуйте какие-нибудь базы данных, чтобы
понять, какие операции нужны. Определите объект графической
базы данных, используя или не используя множественное
наследование. Обсудите относительные плюсы и минусы обоих
решений.
  11. (*2) Напишите вариант функции clone() из $$6.7.1, в котором
размножаемый объект может помещаться в область Arena
     ($$6.7.2), передаваемую как параметр. Реализуйте простой
класс Arena как производный от Arena.
  12. (*2) Пусть есть классы Circle (окружность), Square (квадрат) и
     Triangle (треугольник), производные от класса shape. Определите
функцию intersect() с двумя параметрами типа Shape*, которая
вызывает подходящую функцию, чтобы выяснить, пересекаются ли
заданные две фигуры. Для этого в указанных классах нужно
определить соответствующие виртуальные функции. Не тратьте
силы на функцию, которая действительно устанавливает, что
фигуры пересекаются, добейтесь только правильной
последовательности вызовов функций.
  13. (*5) Разработайте и реализуйте библиотеку для моделирования,
управляемого событиями. Подсказка: используйте <task.h>.

Там уже устаревшие функции и можно написать лучше. Должен
быть класс task (задача). Объект task должен уметь сохранять
свое состояние и восстанавливать его (для этого можно
определить функции task::save() и task::restore()) и тогда
он может действовать как сопрограмма. Специальные задачи
можно определять как объекты классов, производных от task.

Программу, которую выполняет задача, определите как
виртуальную функцию. Должна быть возможность передавать
параметры новой задаче как параметры ее конструктору или
конструкторам. Должен быть диспетчер, который реализует
понятие виртуального времени. Определите функцию
     task::delay(long), которая будет "съедать" виртуальное
время. Важный вопрос разработки: является ли
диспетчер частью класса task, или он должен быть независимым?
Задачи должны иметь возможность общения друг с другом.

Для этой цели разработайте класс queue (очередь). Придумайте
способ, чтобы задача могла ожидать входной поток из нескольких
очередей. Все динамические ошибки должны обрабатываться
единообразно. Как организовать отладку программ, написанных
с помощью такой библиотеки?


ГЛАВА 7. Перегрузка операций


  Если я выбираю слово, оно значит только то,
  что я решу, ни больше и ни меньше.
                       - Шалтай Болтай

Глава содержит описание механизма перегрузки операций в С++.

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

7.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.



7.2 Операторные функции

Можно описать функции, определяющие интерпретацию следующих операций:
      +    -    *    /    %    ^    &    |    ~    !
      =    <    >    +=   -=   *=   /=   %=   ^=   &=
      |=   <<   >>   >>=  <<=  ==   !=   <=   >=   &&
      ||   ++   --   ->*  ,    ->   []   ()   new  delete

Последние пять операций означают: косвенное обращение ($$7.9), индексацию ($$7.7), вызов функции ($$7.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 инициализаторы в этом примере являются эквивалентными.



7.2.1 Бинарные и унарные операции

Бинарную операцию можно определить как функцию-член с одним параметром, или как глобальную функцию с двумя параметрами. Значит, для любой бинарной операции @ выражение aa @ bb интерпретируется либо как aa.operator(bb), либо как operator@(aa,bb). Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров ($$R.13.2). Префиксная или постфиксная унарная операция может определяться как функция-член без параметров, или как глобальная функция с одними параметром. Для любой префиксной унарной операции @ выражение @aa интерпретируется либо как aa.operator@(), либо как operator@(aa). Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров ($$R.13.2). Для любой постфиксной унарной операции @ выражение @aa интерпретируется либо как aa.operator@(int), либо как operator@(aa,int). Подробно это объясняется в $$7.10. Если определены обе функции, то выбор интерпретации происходит по правилам сопоставления параметров
 ($$13.2). Операцию можно определить только в соответствии с синтаксическими правилами, имеющимися для нее в грамматике С++. В частности, нельзя определить % как унарную операцию, а + как тернарную. Проиллюстрируем сказанное примерами:
         class X {
           // члены (неявно используется указатель `this'):

           X* operator&();        // префиксная унарная операция &
                                  // (взятие адреса)
           X operator&(X);        // бинарная операция & (И поразрядное)
           X operator++(int);     // постфиксный инкремент
           X operator&(X,X);      // ошибка: & не может быть тернарной
           X operator/();         // ошибка: / не может быть унарной
         };

        // глобальные функции (обычно друзья)

         X operator-(X);          // префиксный унарный минус
         X operator-(X,X);        // бинарный минус
         X operator--(X&,int);    // постфиксный инкремент
         X operator-();           // ошибка: нет операнда
         X operator-(X,X,X);      // ошибка: тернарная операция
         X operator%(X);          // ошибка: унарная операция %
Операция [] описывается в $$7.7, операция () в $$7.8, операция -> в $$7.9, а операции ++ и -- в $$7.10.



7.2.2 Предопределенные свойства операций

Используется только несколько предположений о свойствах пользовательских операций. В частности, operator=, operator[], operator() и operator-> должны быть нестатическими функциями-членами. Этим обеспечивается то, что первый операнд этих операций является адресом.

Для некоторых встроенных операций их интерпретация определяется как комбинация других операций, выполняемых над теми же операндами. Так, если a типа int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения не сохраняются для пользовательских операций, если только пользователь специально не определил их с такой целью. Так, определение operator+=() для типа complex нельзя вывести из определений complex::operator+() и complex operator=().

По исторической случайности оказалось, что операции = (присваивание), &(взятие адреса) и , (операция запятая) обладают предопределенными свойствами для объектов классов. Но можно закрыть от произвольного пользователя эти свойства, если описать эти операции как частные:

         class X {
            // ...
         private:
            void operator=(const X&);
            void operator&();
            void operator,(const X&);
            // ...
         };

         void f(X a, X b)
         {
            a= b;   // ошибка: операция = частная
            &a;     // ошибка: операция & частная
            a,b     // ошибка: операция , частная
         }
С другой стороны, можно наоборот придать с помощью соответствующих определений этим операциям иное значение.



7.2.3 Операторные функции и пользовательские типы

Операторная функция должна быть либо членом, либо иметь по крайней мере один параметр, являющийся объектом класса (для функций, переопределяющих операции new и delete, это не обязательно). Это правило гарантирует, что пользователь не сумеет изменить интерпретацию выражений, не содержащих объектов пользовательского типа. В частности, нельзя определить операторную функцию, работающую только с указателями. Этим гарантируется, что в С++ возможны расширения, но не мутации (не считая операций =, &, и , для объектов класса).

Операторная функция, имеющая первым параметр основного типа, не может быть функцией-членом. Так, если мы прибавляем комплексную переменную aa к целому 2, то при подходящем описании функции-члена
 aa+2 можно интерпретировать как aa.operator+(2), но 2+aa так интерпретировать нельзя, поскольку не существует класса int, для которого + определяется как 2.operator+(aa). Даже если бы это было возможно, для интерпретации aa+2 и 2+aa пришлось иметь дело с двумя разными функциями-членами. Этот пример тривиально записывается с помощью функций, не являющихся членами.

Каждое выражение проверяется для выявления неоднозначностей. Если пользовательские операции задают возможную интерпретацию выражения, оно проверяется в соответствии с правилами $$R.13.2.


7.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, double);
          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);

          // ...
        };
Имея такое определение комплексного числа, можно писать:
       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;
       }
Все-таки утомительно, как мы это только что делали для operator*() описать для каждой комбинации complex и double свою функцию. Более того, разумные средства для комплексной арифметики должны предоставлять десятки таких функций (посмотрите, например, как описан тип complex в <complex.h>).



7.3.1 Конструкторы

Вместо того, чтобы описывать несколько функций, можно описать конструктор, который из параметра double создает complex:
         class complex {
            // ...
            complex(double r) { re=r; im=0; }
         };
Этим определяется как получить complex, если задан double. Это традиционный способ расширения вещественной прямой до комплексной плоскости.

 

Конструктор с единственным параметром не обязательно вызывать явно:

         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 operator+=(complex);
            complex operator*=(complex);

            // ...
         };
Все операции над комплексными переменными и целыми константами с учетом этого описания становятся законными. Целая константа будет интерпретироваться как комплексное число с мнимой частью, равной нулю. Так, a=b*2 означает
         a = operator*(b, complex( double(2), double(0) ) )
Новые версии операций таких, как + , имеет смысл определять только, если практика покажет, что повышение эффективности за счет отказа от преобразований типа стоит того. Например, если выяснится, что операция умножения комплексной переменной на вещественную константу является критичной, то к множеству операций можно добавить operator*=(double):
        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& operator+=(complex);
          complex& operator*=(complex);
          complex& operator*=(double);

          // ...
        };
Операции присваивания типа *= и += могут быть очень полезными для работы с пользовательскими типами, поскольку обычно запись с ними короче, чем с их обычными "двойниками" * и + , а кроме того они могут повысить скорость выполнения программы за счет исключения временных переменных:
         inline complex& complex::operator+=(complex a)
         {
            re += a.re;
            im += a.im;
            return *this;
         }
При использовании этой функции не требуется временной переменной для хранения результата, и она достаточно проста, чтобы транслятор мог "идеально" произвести подстановку тела. Такие простые операции как сложение комплексных тоже легко задать непосредственно:
         inline complex operator+(complex a, complex b)
         {
           return complex(a.re+b.re, a.im+b.im);
         }
Здесь в операторе return используется конструктор, что дает транслятору ценную подсказку на предмет оптимизации. Но для более сложных типов и операций, например таких, как умножение матриц, результат нельзя задать как одно выражение, тогда операции * и + проще реализовать с помощью *= и += , и они будут легче поддаваться оптимизации:
         matrix& matrix::operator*=(const matrix& a)
         {
            // ...
            return *this;
         }

         matrix operator*(const matrix& a, const matrix& b)
         {
           matrix prod = a;
           prod *= b;
           return prod;
         }
Отметим, что в определенной подобным образом операции не нужных никаких особых прав доступа к классу, к которому она применяется, т.е. эта операция не должна быть другом или членом этого класса.

Пользовательское преобразование типа применяется только в том случае, если оно единственное($$7.3.3).

Построенный в результате явного или неявного вызова конструктора, объект является автоматическим, и уничтожается при первой возможности,- как правило сразу после выполнения оператора, в котором он был создан.


7.3.2 Операции преобразования

Конструктор удобно использовать для преобразования типа, но возможны нежелательные последствия:

    [1] Неявные преобразования от пользовательского типа к основному невозможны (поскольку основные типы не являются классами).
    [2] Нельзя задать преобразование из нового типа в старый, не изменяя описания старого типа.
    [3] Нельзя определить конструктор с одним параметром, не определив тем самым и преобразование типа.

Последнее не является большой проблемой, а первые две можно преодолеть, если определить операторную функцию преобразования для исходного типа. Функция-член X::operator T(), где T - имя типа, определяет преобразование типа X в T. Например, можно определить тип tiny (крошечный), значения которого находятся в диапазоне 0..63, и этот тип может в арифметических операциях практически свободно смешиваться с целыми:

          class tiny {
            char v;
            void assign(int i)
            { if (i>63) { error("выход из диапазона"); v=i&~63;  }
              v=i;
            }
          public:
            tiny(int i) { assign(i) }
            tiny(const tiny& t) { v = t.v; }
            tiny& operator=(const tiny& t)
               { v = t.v; return *this; }
            tiny& operator=(int i) { assign(i); return *this; }
            operator int() { return v; }
          };
Попадание в диапазон проверяется как при инициализации объекта tiny, так и в присваивании ему 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;
Операция ввода cin>>x возвращает значение istream&. Оно неявно преобразуется в значение, показывающее состояние потока cin, которое затем проверяется в операторе while (см. $$10.3.2). Но все-таки определять неявное преобразование типа, при котором можно потерять преобразуемое значение, как правило, плохое решение.

Вообще, лучше экономно пользоваться операциями преобразования. Избыток таких операций может вызывать большое число неоднозначностей. Транслятор обнаруживает эти неоднозначности, но разрешить их может быть совсем непросто. Возможно вначале лучше для преобразований использовать поименованные функции, например, X::intof(), и только после того, как такую функцию как следуют опробуют, и явное преобразование типа будет сочтено неэлегантным решением, можно заменить операторной функцией преобразования X::operator int().


7.3.3 Неоднозначности

Присваивание или инициализация объекта класса X является законным, если присваиваемое значение имеет тип X, или если существует единственное преобразование его в значение типа X.

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

       class x { /* ... */ x(int); x(char*); };
       class y { /* ... */ y(int); };
       class z { /* ... */ z(x); };

       x f(x);
       y f(y);

       z g(z);

       void k1()
       {
         f(1);      // недопустимо, неоднозначность: f(x(1)) или f(y(1))
         f(x(1));
         f(y(1));
         g("asdf"); // недопустимо, g(z(x("asdf"))) не используется
       }
Пользовательские преобразования типа рассматриваются только в том случае, когда без них нельзя однозначно выбрать вызываемую функцию:
        class x { /* ... */ x(int); };

        void h(double);
        void h(x);

        void k2()
        {
          h(1);
        }
Вызов h(1) можно интерпретировать либо как h(double(1)), либо как h(x(1)), поэтому в силу требования однозначности его можно счесть незаконным. Но поскольку в первой интерпретации используется только стандартное преобразование, то по правилам, указанным в $$4.6.6 и $$R.13.2, выбирается оно.

Правила на преобразования типа не слишком просто сформулировать и реализовать, не обладают они и достаточной общностью. Рассмотрим требование единственности законного преобразования. Проще всего разрешить транслятору применять любое преобразование, которое он сумеет найти. Тогда для выяснения корректности выражения не нужно рассматривать все существующие преобразования. К сожалению, в таком случае поведение программы будет зависеть от того, какое именно преобразование найдено. В результате поведение программы будет зависеть от порядка описаний преобразований. Поскольку часто эти описания разбросаны по разным исходным файлам (созданным, возможно, разными программистами), то результат программы будет зависеть в каком порядке эти файлы сливаются в программу. С другой стороны, можно вообще запретить неявные преобразования, и это самое простое решение. Но результатом будет некачественный интерфейс, определяемый пользователем, или взрывной рост перегруженных функций и операций, что мы и видели на примере класса complex из предыдущего раздела.

При самом общем подходе учитываются все сведения о типах и рассматриваются все существующие преобразования. Например, с учетом приведенных описаний в присваивании aa=f(1) можно разобраться с вызовом f(1), поскольку тип aa задает единственное преобразование. Если aa имеет тип x, то единственным преобразованием будет f(x(1)), поскольку только оно дает нужный для левой части тип x. Если aa имеет тип y, будет использоваться f(y(1)). При самом общем подходе удается разобраться и с вызовом g("asdf"), поскольку g(z(x("asdf))) является его единственной интерпретацией. Трудность этого подхода в том, что требуется доскональный разбор всего выражения, чтобы установить интерпретацию каждой операции и вызова функции. В результате трансляция замедляется, вычисление выражения может произойти странным образом и появляются загадочные сообщения об ошибках, когда транслятор учитывает определенные в библиотеках преобразования и т.д. В результате транслятору приходится учитывать больше информации, чем известно самому программисту! Выбран подход, при котором проверка является строго восходящим процессом, когда в каждый момент рассматривается только одна операция с операндами, типы которых уже прошли проверку.

Требование строго восходящего разбора выражения предполагает, что тип возвращаемого значения не учитывается при разрешении перегрузки:

          class quad {
             // ...
          public:
             quad(double);
             // ...
          };

          quad operator+(quad,quad);

          void f(double a1, double a2)
          {
            quad r1 = a1+a2;       // сложение с двойной точностью
            quad r2 = quad(a1)+a2; // вынуждает использовать
                                   // операции с типами quad
          }
В проектировании языка делался расчет на строго восходящий разбор, поскольку он более понятный, а кроме того, не дело транслятора решать такие вопросы, какую точность для сложения желает программист.

Однако, надо отметить, что если определились типы обеих частей в присваивании и инициализации, то для их разрешения используется они оба:

          class real {
             // ...
          public:
             operator double();
             operator int();
             // ...
          };

          void g(real a)
          {
            double d = a;  // d = a.double();
            int i = a;     // i = a.int();

            d = a;         // d = a.double();
            i = a;         // i = a.int();
          }
В этом примере выражения все равно разбираются строго восходящим методом, когда в каждый момент рассматриваются только одна операция и типы ее операндов.



7.4 Литералы

Для классов нельзя определить литеральные значения, подобному тому как 1.2 и 12e3 являются литералами типа double. Однако, для интерпретации значений классов могут использоваться вместо функций-членов литералы основных типов. Общим средством для построения таких значений служат конструкторы с единственным параметром. Если конструктор достаточно простой и реализуется подстановкой, вполне разумно представлять его вызов как литерал. Например, с учетом описания класса complex в <complex.h> в выражении zz1*3+zz2*complex(1,2) произойдет два вызова функций, а не пять. Две операции * приведут к вызову функции, а операция + и вызовы конструктора для построения complex(3) и complex(1,2) будут реализованы подстановкой.



7.5 Большие объекты

При выполнении любой бинарной операции для типа complex реализующей эту операцию функции будут передаваться как параметры копии обоих операндов. Дополнительные расходы, вызванные копированием двух значений типа double, заметны, хотя по всей видимости допустимы. К сожалению представление не всех классов является столь удобно компактным. Чтобы избежать избыточного копирования, можно определять функции с параметрами типа ссылки:
           class matrix {
              double m[4][4];
           public:
              matrix();
              friend matrix operator+(const matrix&, const matrix&);
              friend matrix operator*(const matrix&, const matrix&);
           };
Ссылки позволяют без излишнего копирования использовать выражения с обычными арифметическими операциями и для больших объектов. Указатели для этой цели использовать нельзя, т.к. невозможно переопределить интерпретацию операции, если она применяется к указателю. Операцию плюс для матриц можно определить так:
           matrix operator+(const matrix& arg1, const& arg2)
           {
              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+(const matrix&, const matrix&);
              friend matrix& operator*(const matrix&, const matrix&);
           };
Это допустимо, но возникает проблема с выделением памяти. Поскольку ссылка на результат операции будет передаваться как ссылка на возвращаемое функцией значение, оно не может быть автоматической переменной этой функции. Поскольку операция может использоваться неоднократно в одном выражении, результат не может быть и локальной статической переменной. Как правило, результат будет записываться в отведенный в свободной памяти объект. Обычно бывает дешевле (по затратам на время выполнения и память данных и команд) копировать результирующее значение, чем размещать его в свободной памяти и затем в конечном счете освобождать выделенную память. К тому же этот способ проще запрограммировать.



7.6 Присваивание и инициализация

Рассмотрим простой строковый класс string:
             struct string {
                char* p;
                int size;  // размер вектора, на который указывает p

                string(int size) { p = new char[size=sz]; }
                ~string() { delete p; }
             };
Строка - это структура данных, содержащая указатель на вектор символов и размер этого вектора. Вектор создается конструктором и удаляется деструктором. Но как мы видели в $$5.5.1 здесь могут возникнуть проблемы:
             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 size) { p = new char[size=sz]; }
               ~string() { delete p; }
               string& operator=(const string&);
             };

             string& string::operator=(const string& a)
             {
               if (this !=&a) {   // опасно, когда s=s
                  delete p;
                  p = new char[size=a.size];
                  strcpy(p,a.p);
               }
               return *this;
             }
При таком определении string предыдущий пример пройдет как задумано. Но после небольшого изменения в f() проблема возникает снова, но в ином обличии:
             void f()
             {
               string s1(10);
               string s2 = s1;  // инициализация, а не присваивание
             }
Теперь только один объект типа string строится конструктором string::string(int), а уничтожаться будет две строки. Дело в том, что пользовательская операция присваивания не применяется к неинициализированному объекту. Достаточно взглянуть на функцию string::operator(), чтобы понять причину этого: указатель p будет тогда иметь неопределенное, по сути случайное значение. Как правило, в операции присваивания предполагается, что ее параметры проинициализированы. Для инициализации типа той, что приведена в этом примере это не так по определению. Следовательно, чтобы справиться с инициализацией нужна похожая, но своя функция:
             struct string {
               char* p;
               int size;   // размер вектора, на который указывает p

               string(int size) { p = new char[size=sz]; }
               ~string() { delete p; }
               string& operator=(const string&);
               string(const string&);
             };

             string::string(const string& a)
             {
                  p=new char[size=sz];
                  strcpy(p,a.p);
             }
Инициализация объекта типа X происходит с помощью конструктора X(const X&). Мы не перестаем повторять, что присваивание и инициализация являются разными операциями. Особенно это важно в тех случаях, когда определен деструктор. Если в классе X есть нетривиальный деструктор, например, производящий освобождение объекта в свободной памяти, вероятнее всего, в этом классе потребуется полный набор функций, чтобы избежать копирования объектов по членам:
             class X {
                // ...
                X(something);        // конструктор, создающий объект
                X(const X&);         // конструктор копирования
                operator=(const X&); // присваивание:
                                     // удаление и копирование
                ~X();                // деструктор, удаляющий объект
             };
Есть еще два случая, когда приходится копировать объект: передача параметра функции и возврат ею значения. При передаче параметра неинициализированная переменная, т.е. формальный параметр инициализируется. Семантика этой операции идентична другим видам инициализации. Тоже происходит и при возврате функцией значения, хотя этот случай не такой очевидный. В обоих случаях используется конструктор копирования:
            string g(string arg)
            {
              return arg;
            }

            main()
            {
              string s = "asdf";
              s = g(s);
            }
Очевидно, после вызова g() значение s должно быть "asdf". Не трудно записать в параметр s копию значения s, для этого надо вызвать конструктор копирования для string. Для получения еще одной копии значения s по выходе из g() нужен еще один вызов конструктора string(const string&). На этот раз инициализируется временная переменная, которая затем присваивается s. Для оптимизации одну, но не обе, из подобных операций копирования можно убрать. Естественно, временные переменные, используемые для таких целей, уничтожаются надлежащим образом деструктором string::~string() (см. $$R.12.2).

Если в классе X операция присваивания X::operator=(const X&) и конструктор копирования X::X(const X&) явно не заданы программистом, недостающие операции будут созданы транслятором. Эти созданные функции будут копировать по членам для всех членов класса X. Если члены принимают простые значения, как в случае комплексных чисел, это, то, что нужно, и созданные функции превратятся в простое и оптимальное поразрядное копирование. Если для самих членов определены пользовательские операции копирования, они и будут вызываться соответствующим образом:

           class Record {
             string name, address, profession;
             // ...
           };

           void f(Record& r1)
           {
             Record r2 = r1;
           }
Здесь для копирования каждого члена типа string из объекта r1 будет вызываться string::operator=(const string&). В нашем первом и неполноценном варианте строковый класс имеет член-указатель и деструктор. Поэтому стандартное копирование по членам для него почти наверняка неверно. Транслятор может предупреждать о таких ситуациях.



7.7 Индексация

Операторная функция operator[] задает для объектов классов интерпретацию индексации. Второй параметр этой функций (индекс) может иметь произвольный тип. Это позволяет, например, определять ассоциативные массивы. В качестве примера можно переписать определение из $$2.3.10, где ассоциативный массив использовался в небольшой программе, подсчитывающей число вхождений слов в файле. Там для этого использовалась функция. Мы определим настоящий тип ассоциативного массива:
           class assoc {
              struct pair {
                 char* name;
                 int val;
              };

              pair* vec;
              int max;
              int free;

              assoc(const assoc&);            // предотвращает копирование
              assoc& operator=(const assoc&); // предотвращает копирование
           public:
              assoc(int);
              int& operator[](const char*);
              void print_all();
            };
В объекте assoc хранится вектор из структур pair размером max. В переменной free хранится индекс первого свободного элемента вектора.

Чтобы предотвратить копирование объектов assoc, конструктор копирования и операция присваивания описаны как частные. Конструктор выглядит так:

            assoc::assoc(int s)
            {
              max = (s<16) ? 16 : s;
              free = 0;
              vec = new pair[max];
            }
В реализации используется все тот же неэффективный алгоритм поиска, что и в $$2.3.10. Но теперь, если вектор переполняется, объект
 assoc увеличивается:
            #include <string.h>

            int& assoc::operator[](const char* p)
            /*
  работает с множеством пар (структур pair):
  проводит поиск p, возвращает ссылку на
  целое значение из найденной пары,
  создает новую пару, если 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; i<max; i++) nvec[i] = vec[i];
                 delete vec;
                 vec = nvec;
                 max = 2*max;
              }

              pp = &vec[free++];
              pp->name = new char[strlen(p)+1];
              strcpy(pp->name,p);
              pp->val = 0;    // начальное значение = 0
              return pp->val;
            }
Поскольку представление объекта assoc скрыто от пользователя, нужно иметь возможность напечатать его каким-то образом. В следующем разделе будет показано как определить настоящий итератор для такого объекта. Здесь же мы ограничимся простой функцией печати:
            void assoc::print_all()
            {
              for (int i = 0; i<free; i++)
                  cout << vec[i].name << ": " << vec[i].val << '\n';
            }
Наконец, можно написать тривиальную программу:
            main() // подсчет числа вхождений во входной
                   // поток каждого слова
            {
              const MAX = 256;  // больше длины самого длинного слова
              char buf[MAX];
              assoc vec(512);
              while (cin>>buf) vec[buf]++;
              vec.print_all();
            }
Опытные программисты могут заметить, что второй комментарий можно легко опровергнуть. Решить возникающую здесь проблему предлагается в упражнении $$7.14 [20]. Дальнейшее развитие понятие ассоциативного массива получит в $$8.8.

Функция operator[]() должна быть членом класса. Отсюда следует, что эквивалентность x[y] == y[x] может не выполняться, если x объект класса. Обычные отношения эквивалентности, справедливые для операций со встроенными типами, могут не выполняться для пользовательских типов ($$7.2.2, см. также $$7.9).


7.8 Вызов функции

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

Мы не дали определения итератора для ассоциативного массива типа assoc. Для этой цели можно определить специальный класс assoc_iterator, задача которого выдавать элементы из assoc в некотором порядке. В итераторе необходимо иметь доступ к данным, хранимым в assoc, поэтому он должен быть описан как friend:

            class assoc {
            friend class assoc_iterator;
                pair* vec;
                int max;
                int free;
            public:
                assoc(int);
                int& operator[](const char*);
            };
Итератор можно определить так:
            class assoc_iterator {
               const assoc* cs;   // массив assoc
               int i;             // текущий индекс
            public:
               assoc_iterator(const assoc& s) { cs = &s; i = 0; }
               pair* operator()()
                 { return (i<cs->free)? &cs->vec[i++] : 0; }
            };
Массив assoc объекта assoc_iterator нужно инициализировать, и при каждом обращении к нему с помощью операторной функции () будет возвращаться указатель на новую пару (структура 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(vec) )
                  cout << p->name << ": " << p->val << '\n';
            }
Итератор подобного вида имеет преимущество перед набором функций, решающим ту же задачу: итератор может иметь собственные частные данные, в которых можно хранить информацию о ходе итерации. Обычно важно и то, что можно одновременно запустить сразу несколько итераторов одного типа.

Конечно, использование объектов для представления итераторов непосредственно никак не связано с перегрузкой операций. Одни предпочитают использовать тип итератора с такими операциями, как
first(), next() и last(), другим больше нравится перегрузка операции ++ , которая позволяет получить итератор, используемый как указатель (см. $$8.8). Кроме того, операторная функция operator() активно используется для выделения подстрок и индексации многомерных массивов.

Функция operator() должна быть функцией-членом.


7.9 Косвенное обращение

Операцию косвенного обращения к члену -> можно определить как унарную постфиксную операцию. Это значит, если есть класс
           class Ptr {
             // ...
             X* operator->();
           };
объекты класса Ptr могут использоваться для доступа к членам класса X также, как для этой цели используются указатели:
          void f(Ptr p)
          {
            p->m = 7;  // (p.operator->())->m = 7
          }
Превращение объекта p в указатель p.operator->() никак не зависит от члена m, на который он указывает. Именно по этой причине operator->() является унарной постфиксной операцией. Однако, мы не вводим новых синтаксических обозначений, так что имя члена по-прежнему должно идти после -> :
           void g(Ptr p)
           {
              X* q1 = p->;  // синтаксическая ошибка
              X* q2 = p.operator->(); // нормально
           }
Перегрузка операции -> прежде всего используется для создания "хитрых указателей", т.е. объектов, которые помимо использования как указатели позволяют проводить некоторые операции при каждом обращении к указуемому объекту с их помощью. Например, можно определить класс RecPtr для организации доступа к объектам класса Rec, хранимым на диске. Параметром конструктора RecPtr является имя, которое будет использоваться для поиска объекта на диске. При обращении к объекту с помощью функции RecPtr::operator->() он переписывается в основную память, а в конце работы деструктор RecPtr записывает измененный объект обратно на диск.
            class RecPtr {
               Rec* in_core_address;
               const char* identifier;
               // ...
            public:
               RecPtr(const char* p)
               : identifier(p) { in_core_address = 0; }
               ~RecPtr()
                  { write_to_disc(in_core_address,identifier); }
               Rec* operator->();
            };

            Rec* RecPtr::operator->()
            {
              if (in_core_address == 0)
                  in_core_address = read_from_disc(identifier);
              return in_core_address;
            }
Использовать это можно так:
            main(int argc, const char* argv)
            {
              for (int i = argc; i; i--) {
                  RecPtr p(argv[i]);
                  p->update();
              }
            }
На самом деле, тип RecPtr должен определяться как шаблон типа (см. $$8), а тип структуры Record будет его параметром. Кроме того, настоящая программа будет содержать обработку ошибок и взаимодействие с диском будет организовано не столь примитивно.

Для обычных указателей операция -> эквивалентна операциям, использующим * и []. Так, если описано

            Y* p;
то выполняется соотношение
            p->m == (*p).m == p[0].m
Как всегда, для определенных пользователем операций такие соотношения не гарантируются. Там, где все-таки такая эквивалентность требуется, ее можно обеспечить:
            class X {
              Y* p;
            public:
              Y* operator->() { return p; }
              Y& operator*() { return *p; }
              Y& operator[](int i) { return p[i]; }
            };
Если в вашем классе определено более одной подобной операции, разумно будет обеспечить эквивалентность, точно так же, как разумно предусмотреть для простой переменной x некоторого класса, в котором есть операции ++, += = и +, чтобы операции ++x и x+=1 были эквивалентны x=x+1.

Перегрузка -> как и перегрузка [] может играть важную роль для целого класса настоящих программ, а не является просто экспериментом ради любопытства. Дело в том, что в программировании понятие косвенности является ключевым, а перегрузка -> дает ясный, прямой и эффективный способ представления этого понятия в программе. Есть другая точка зрения на операцию ->, как на средство задать в С++ ограниченный, но полезный вариант понятия делегирования (см. $$12.2.8 и 13.9).


7.10 Инкремент и декремент

Если мы додумались до "хитрых указателей", то логично попробовать переопределить операции инкремента ++ и декремента -- , чтобы получить для классов те возможности, которые эти операции дают для встроенных типов. Такая задача особенно естественна и необходима, если ставится цель заменить тип обычных указателей на тип "хитрых указателей", для которого семантика остается прежней, но появляются некоторые действия динамического контроля. Пусть есть программа с распространенной ошибкой:
           void f1(T a)   // традиционное использование
           {
             T v[200];
             T* p = &v[10];
             p--;
             *p = a;   // Приехали: `p' настроен вне массива,
                       // и это не обнаружено
             ++p;
             *p = a;   // нормально
           }
Естественно желание заменить указатель p на объект класса CheckedPtrToT, по которому косвенное обращение возможно только при условии, что он действительно указывает на объект. Применять инкремент и декремент к такому указателю будет можно только в том случае, что указатель настроен на объект в границах массива и в результате этих операций получится объект в границах того же массива:
           class CheckedPtrToT {
              // ...
           };

           void f2(T a)  // вариант с контролем
           {
             T v[200];
             CheckedPtrToT p(&v[0],v,200);
             p--;
             *p = a;  // динамическая ошибка:
                      // `p' вышел за границы массива
             ++p;
             *p = a;  // нормально
           }
Инкремент и декремент являются единственными операциями в С++, которые можно использовать как постфиксные и префиксные операции. Следовательно, в определении класса CheckedPtrToT мы должны предусмотреть отдельные функции для префиксных и постфиксных операций инкремента и декремента:
           class CheckedPtrToT {
             T* p;
             T* array;
             int size;
           public:
                  // начальное значение `p'
                  // связываем с массивом `a' размера `s'
               CheckedPtrToT(T* p, T* a, int s);
                  // начальное значение `p'
                  // связываем с одиночным объектом
               CheckedPtrToT(T* p);

               T* operator++();     // префиксная
               T* operator++(int);  // постфиксная

               T* operator--();     // префиксная
               T* operator--(int);  // постфиксная

               T& operator*();      // префиксная
            };
Параметр типа int служит указанием, что функция будет вызываться для постфиксной операции. На самом деле этот параметр является искусственным и никогда не используется, а служит только для различия постфиксной и префиксной операции. Чтобы запомнить, какая версия функции operator++ используется как префиксная операция, достаточно помнить, что префиксной является версия без искусственного параметра, что верно и для всех других унарных арифметических и логических операций. Искусственный параметр используется только для "особых" постфиксных операций ++ и --.

С помощью класса CheckedPtrToT пример можно записать так:

             void f3(T a)  // вариант с контролем
             {
               T v[200];
               CheckedPtrToT p(&v[0],v,200);
               p.operator--(1);
               p.operator*() = a; // динамическая ошибка:
                                  // `p' вышел за границы массива
               p.operator++();
               p.operator*() = a; // нормально
             }
В упражнении $$7.14 [19] предлагается завершить определение класса CheckedPtrToT, а другим упражнением ($$9.10[2]) является преобразование его в шаблон типа, в котором для сообщений о динамических ошибках используются особые ситуации. Примеры использования операций ++ и -- для итераций можно найти в $$8.8.



7.11 Строковый класс

Теперь можно привести более осмысленный вариант класса string. В нем подсчитывается число ссылок на строку, чтобы минимизировать копирование, и используются как константы стандартные строки C++.
            #include <iostream.h>
            #include <string.h>

            class string {
               struct srep {
                 char* s;       // указатель на строку
                 int n;         // счетчик числа ссылок
                 srep() { n = 1; }
               };
               srep *p;

            public:
              string(const char *);   // string x = "abc"
              string();               // string x;
              string(const string &); // string x = string ...
              string& operator=(const char *);
              string& operator=(const string &);
              ~string();
              char& operator[](int i);

              friend ostream& operator<<(ostream&, const string&);
              friend istream& operator>>(istream&, string&);

              friend int operator==(const string &x, const char *s)
                { return strcmp(x.p->s,s) == 0; }

              friend int operator==(const string &x, const string &y)
                { return strcmp(x.p->s,y.p->s) == 0; }

              friend int operator!=(const string &x, const char *s)
                { return strcmp(x.p->s,s) != 0; }

              friend int operator!=(const string &x, const string &y)
                { return strcmp(x.p->s,y.p->s) != 0; }
           };
Конструкторы и деструкторы тривиальны:
           string::string()
           {
             p = new srep;
             p->s = 0;
           }

           string::string(const string& x)
           {
             x.p->n++;
             p = x.p;
           }

           string::string(const char* s)
           {
             p = new srep;
             p->s = new char[ strlen(s)+1 ];
             strcpy(p->s, s);
           }

           string::~string()
           {
             if (--p->n == 0) {
                delete[]  p->s;
                delete p;
             }
           }
Как и всегда операции присваивания похожи на конструкторы. В них нужно позаботиться об удалении первого операнда, задающего левую часть присваивания:
          string& string::operator=(const char* s)
          {
            if (p->n > 1) {  // отсоединяемся от старой строки
                p->n--;
                p = new srep;
            }
            else    // освобождаем строку со старым значением
                delete[] p->s;

            p->s = new char[ strlen(s)+1 ];
            strcpy(p->s, s);
            return *this;
          }

          string& string::operator=(const string& x)
          {
            x.p->n++;  // защита от случая ``st = st''
            if (--p->n == 0) {
               delete[] p->s;
               delete p
            }
            p = x.p;
            return *this;
          }
Операция вывода показывает как используется счетчик числа ссылок. Она сопровождает как эхо каждую введенную строку (ввод происходит с помощью операции << , приведенной ниже):
          ostream& operator<<(ostream& s, const string& x)
          {
             return s << x.p->s << " [" << x.p->n << "]\n";
          }
Операция ввода происходит с помощью стандартной функции ввода символьной строки ($$10.3.1):
          istream& operator>>(istream& s, string& x)
          {
             char buf[256];
             s >> buf;   // ненадежно: возможно переполнение buf
                         // правильное решение см. в $$10.3.1
             x = buf;
             cout << "echo: " << x << '\n';
             return s;
           }
Операция индексации нужна для доступа к отдельным символам. Индекс контролируется:
          void error(const char* p)
          {
            cerr << p << '\n';
            exit(1);
          }

         char& string::operator[](int i)
        {
         if (i<0 || strlen(p->s)<i) error("недопустимое значение индекса");
           return p->s[i];
        }
В основной программе просто даны несколько примеров применения строковых операций. Слова из входного потока читаются в строки, а затем строки печатаются. Это продолжается до тех пор, пока не будет обнаружена строка done, или закончатся строки для записи слов, или закончится входной поток. Затем печатаются все строки в обратном порядке и программа завершается.
         int main()
         {
           string x[100];
           int n;

           cout << " здесь начало \n";

           for ( n = 0; cin>>x[n]; n++) {
               if (n==100) {
                  error("слишком много слов");
                  return 99;
               }
               string y;
               cout << (y = x[n]);
               if (y == "done") break;

           }
           cout << "теперь мы идем по словам в обратном порядке \n";
           for (int i=n-1; 0<=i; i--) cout << x[i];
           return 0;
         }


7.12 Друзья и члены

В заключении можно обсудить, когда при обращении в закрытую часть пользовательского типа стоит использовать функции-члены, а когда функции-друзья. Некоторые функции, например конструкторы, деструкторы и виртуальные функции ($$R.12), обязаны быть членами, но для других есть возможность выбора. Поскольку, описывая функцию как член, мы не вводим нового глобального имени, при отсутствии других доводов следует использовать функции-члены.

Рассмотрим простой класс X:

          class X {
             // ...

            X(int);

            int m1();
            int m2() const;

            friend int f1(X&);
            friend int f2(const X&);
            friend int f3(X);
         };
Вначале укажем, что члены X::m1() и X::m2() можно вызывать только для объектов класса X. Преобразование X(int) не будет применяться к объекту, для которого вызваны X::m1() или X::m2():
          void g()
          {
             1.m1();  // ошибка: X(1).m1() не используется
             1.m2();  // ошибка: X(1).m2() не используется
          }
Глобальная функция f1() имеет то же свойство ($$4.6.3), поскольку ее параметр - ссылка без спецификации const. С функциями f2() и f3() ситуация иная:
          void h()
          {
            f1(1);  // ошибка: f1(X(1)) не используется
            f2(1);  // нормально: f2(X(1));
            f3(1);  // нормально: f3(X(1));
          }
Следовательно операция, изменяющая состояние объекта класса, должна быть членом или глобальной функцией с параметром-ссылкой без спецификации const. Операции над основными типами, которые требуют в качестве операндов адреса (=, *, ++ и т.д.), для пользовательских типов естественно определять как члены.

Обратно, если требуется неявное преобразование типа для всех операндов некоторой операции, то реализующая ее функция должна быть не членом, а глобальной функцией и иметь параметр типа ссылки со спецификацией const или нессылочный параметр. Так обычно обстоит дело с функциями, реализующими операции, которые для основных типов не требуют адресов в качестве операндов (+, -, || и т.д.).

Если операции преобразования типа не определены, то нет неопровержимых доводов в пользу функции-члена перед функцией-другом с параметром-ссылкой и наоборот. Бывает, что программисту просто одна форма записи вызова нравится больше, чем другая. Например, многим для обозначения функции обращения матрицы m больше нравится запись inv(m), чем m.inv(). Конечно, если функция inv() обращает саму матрицу m, а не возвращает новую, обратную m, матрицу, то inv() должна быть членом.

При всех прочих равных условиях лучше все-таки остановиться на функции-члене. Можно привести такие доводы. Нельзя гарантировать, что когда-нибудь не будет определена операция обращения. Нельзя во всех случаях гарантировать, что будущие изменения не повлекут за собой изменения в состоянии объекта. Запись вызова функции-члена ясно показывает программисту, что объект может быть изменен, тогда как запись с параметром-ссылкой далеко не столь очевидна. Далее, выражения допустимые в функции-члене могут быть существенно короче эквивалентных выражений в глобальной функции. Глобальная функция должна использовать явно заданные параметры, а в функции-члене можно неявно использовать указатель this. Наконец, поскольку имена членов не являются глобальными именами, они обычно оказываются короче, чем имен глобальных функций.


7.13 Предостережения

Как и всякое другое языковое средство, перегрузка операций может использоваться разумно и неразумно. В частности, возможностью придавать новый смысл обычным операциям можно воспользоваться так, что программа будет совершенно непостижимой. Представьте, каково будет читателю, если в своей программе вы переопределили операцию + так, чтобы она обозначала вычитание. Описанный здесь механизм перегрузки будет защищать программиста и пользователя от таких безрассудств. Поэтому программист не может изменить ни смысл операций над основными типами данных, такими, как int, ни синтаксис выражений и приоритеты операций для них.

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


 

7.14 Упражнения

 1. (*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию += , значащую "добавить в конец строки".

Какие еще операции вы хотели бы и смогли определить для этого класса?
 2. (*1.5) Определите для строкового класса операцию выделения подстроки с помощью перегрузки ().
 3. (*3) Определите класс string таким образом, чтобы операцию выделения подстроки можно было применять к левой части присваивания. Вначале напишите вариант, в котором строку можно присваивать подстроке той же длины, а затем вариант с различными длинами строк.
 4. (*2) Разработайте класс string таким образом, чтобы объекты его трактовались при передаче параметров и присваивании как значения, т.е. чтобы в классе string копировались сами представления строк, а не только управляющие структуры.
 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();
           };

           extern X operator*(X,Y);
           extern int f(X);

           X x = 1;
           Y y = x;
           int i = 2;

           int 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, за исключением того, что допустимыми будут только операции:
    + (унарный и бинарный), - (унарный и бинарный), *, / и %.

Подсказка: не надо определять RINT::operator int().
 10. (*3) Определите класс LINT, эквивалентный классу RINT, но в нем для представления целого должно использоваться не менее 64 разрядов.
 11. (*4) Определите класс, реализующий арифметику с произвольной точностью. Подсказка: Придется использовать память так, как это делается в классе string.
 12. (*2) Напишите программу, в которой благодаря макрокомандам и перегрузке будет невозможно разобраться. Совет: определите для типа INT + как -, и наоборот; с помощью макроопределения задайте
    int как INT. Кроме того, большую путаницу можно создать, переопределяя широко известные функции, и используя параметры типа ссылки и задавая вводящие в заблуждение комментарии.
 13. (*3) Обменяйтесь решениями упражнения [12] с вашим другом.

Попробуйте понять, что делает его программа, не запуская ее. Если вы сделаете это упражнение, вам станет ясно, чего надо избегать.
 14. (*2) Перепишите примеры с классами complex ($$7.3), tiny
    ($$7.3.2) и string ($$7.11), не используя дружественные функции.

Используйте только функции-члены. Проверьте новые версии этих классов. Сравните их с версиями, в которых используются дружественные функции. Обратитесь к упражнению 5.3.
 15. (*2) Определите тип vec4 как вектор из четырех чисел с плавающей точкой. Определите для него функцию operator[]. Для комбинаций векторов и чисел с плавающей точкой определите операции:
    +, -, *, /, =, +=, -=, *= и /=.
 16. (*3) Определите класс mat4 как вектор из четырех элементов типа
    vec4. Определите для него функцию operator[], возвращающую vec4.

Определите для этого типа обычные операции с матрицами. Определите в mat4 функцию, производящую преобразование Гаусса с матрицей.
 17. (*2) Определите класс vector, аналогичный классу vec4, но здесь размер вектора должен задаваться как параметр конструктора
    vector::vector(int).
 18. (*3) Определите класс matrix, аналогичный классу mat4, но здесь размерности матрицы должны задаваться как параметры конструктора
    matrix::matrix(int,int).
 19. (*3) Завершите определение класса CheckedPtrToT из $$7.10 и проверьте его. Чтобы определение этого класса было полным, необходимо определить, по крайней мере, такие операции: *, ->,
    =, ++ и --. Не выдавайте динамическую ошибку, пока действительно не произойдет обращение по указателю с неопределенным значением.
 20. (*1.5) Перепишите пример с программой подсчета слов из $$7.7 так, чтобы в ней не было заранее заданной максимальной длины слова.