    Глава 14 

    Объектно-ориентированное программирование 

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

    Простое наследование 

    Простое наследование описывает родство между двумя классами, один из 
которых наследует второй (рис. 14.1). Класс, находящийся на вершине иерархии, 
называется базовым классом. Прочие классы называются производными классами. 
    
    Также называемый "класс-предок"
    Базовый класс       1     Наследу    базовогог свойства класса 
    Также называемый "класс-потомок" 

    Рис. 14.1. Простое наследование 
    
    Из одного базового класса могут выводиться многие классы. Даже в случае, 
показанном на рис. 14.2, подобный вид взаимосвязи остается разновидностью 
простого наследования, поскольку каждый новый класс выводится из одного 
класса. Далее в этой главе вы ознакомитесь с множественным наследованием, в 
котором один класс может наследовать несколько базовых классов. 


    336 223 

    Предок 

    Базовый класс 

    Потомок 

    Производный класс   I      Производный класс   I     I Производный класс 

    Элементы одного уровня 

    Рис. 14.2. Несколько классов, производных от одного базового класса	.* 

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

    Другие производные классы Другие  производные классы	

Базовый класс   Базовый класс 
    класса А Базовый класс 
    i	k	А 1 
    Производный класс 
    класса В Базовый 'класс 
    1	ь 
    Производный класс		п) 
    класса С ^ Другие производные классы 
    А i		^\ 
    Производный класс		') 

    Рис. 14.3. Производный класс может быть базовым классом других производных классов 

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

    Объявление производных классов 

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

    class TBase { 
    private:
        int count; // Представляет закрытые данные класса 
    public:
       TBase() { count=0; }        
       void SetCount(int n) { count=n; }    
       int GetCount(void) { return count; } 
    } ;	

    Несмотря на то что члены TBase не имеют никакого практического применения, 
они очень похожи на те, тго используются в типичных классах C++, из которых 
могут быть выведены новые классы. Член count coдержит закрытые данные класса. 
Только закрытые функции-члены TBase могут непосредственно ссылаться на сount. 
Конструктор по умолчанию TBase() инициализирует count нулевым значением. Функция-член 

    Глава 14. Объектно-ориентированное программирование 337 224 

SetCount() присваивает count новое значение. Функция-член GetCount()
возвращает текущее значение count. Для упрощения кода конструктор TBase и две 
функции должны быть встраиваемыми, не требующими отдельной реализации. 
    Предположим, вам необходим новый класс, имеющий все свойства TBase, но, 
кроме того, способный увеличивать и уменьшать значение объекта на заданную 
величину. Вы можете переписать первоначальный класс, но это не всегда 
приемлемо в больших проектах, особенно если в работе над ними задействовано 
много программистов. Подобные изменения могут оказаться даже невозможными, 
если TBase — часть коммерческой библиотеки классов, исходные тексты которой 
недоступны. Вместо изменения TBase, можно вывести из него новый класс, 
дополнительно имеющий новые, необходимые вам свойства: 

    class TDerived:   public TBase { 
    public:	
      TDerived():  TBase() { }	
      void ChangeCount( int n )  { SetCount(GetCount() + n);   }. 
    }
 
    Производный класс назван TDerived. Сразу за именем класса следует 
двоеточие и одно из ключевых слов — public, protected или private. После этих 
элементов следует имя базового класса (TBase), от которого TDerived получгет 
наследство. На рис. 14.4 иллюстрируется структура класса TDerived и его конструктора. 

    Может быть открытым, закрытым или защищенным 
    Имя производного класса 
    Базовый класс; предок класса TDerived 
    class TDerived: public TBase { public: TDerivedO : TBaseQ { }_ 
    Конструктор по умолчанию для производного класса 
    Производные классы должны вызывать конструкторы своих базовых классов 
    В этом месте могут выполняться операторы. Этот конструктор не производит 
никаких действий, поэтому не содержит операторов 

    Рис. 14.4. Структура производного класса 

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

      class TDerived:   public TBase { 

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

    class TDerived:   private TBase { 

    В данном случае все открытые члены TBase становятся закрытыми членами в 
TDerived. Последующие наследования TDerived не смогут получить доступ к любым 
членам TBase. Позже вы ознакомитесь с третьим типом спецификатора класса — protected. 

    Использование конструкторов производного класса 

    Ознакомимся подробнее с конструктором класса TDerive — TDerive(). Обычно, в 
производном классе имеется конструктор, если он есть и в базовом классе. Кроме 
того, конструктор производного класса должен вызывать конструктор базового 
класса. (C++ может автоматически вставлять конструкторы и деструкторы по 
умолчанию, а также вызывать их, когда это необходимо для многих классов.) В 
TDerived в строке 

    TDerived():   TBase()  {   }	

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

    Tderived():  TBase()  { cout « "I am being initialized\n";   }  

    338     Часть III. Программирование на ANSI C++ 

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

    сlass TDerived:  public TBase {	4 
    public: 
      TDerived();	// Конструктор производного класса 
      void ChangeCount(int n)  { Set?ount(GetCount() + n);   } 
    }

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

   TDеrived::TDerived()  // Заголовок конструктора
      :  TBase()    // Вызов конструктора базового класса 
    {
       // В этом месте выполняются операторы конструктора 
    }

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

    Использование функций-членов производного класса 

    Производный класс наследует count из базового класса.  Конечно, поскольку 
count закрыт в TBase, в TDerived нельзя получить к нему непосредственный доступ. 
Только члены класса могут иметь доступ к закрытым членам этого же класса. 
Конечно, в TDerived можно объявить свои собственные закрытые члены. Напри-   
мер, рассмотрим класс, объявленный следующим образом: 

    class TDerived:  public TBase { 
    private: 
      int secondCount; 
   public: 
    TDerived();     // Конструктор производного класса	
    void ChangeCount(int n) { SetCount(GetCount() + n); }       

  TDerived наследует свойства TBase, включая закрытый член базового класса count. 
В новом классе добавился новый закрытый член secondCount. Для того чтобы его 
инициализировать, новый конструктор можно реалиэовать следующим образом: 

    TDerived::TDerived ()  // Заголовок конструктора
    :  TBase()     // Инициализация данных-членов TBase 
    {  
       secondCount = 0;     // Инициализация данных-членов TDerived 
    } 
     

    Вызов конструктора TBase() инициализирует объект класса TBase. Оператор 
присваивания члену  secondCount в конструкторе производного класса 
инициализирует объект класса TDerived. Конструктор базового класса должен 
вызываться первым, что обеспечит инициализацию всех наследуемых членов до 
того, как будут инициализированы члены производного класса. Рис. 14.5 
демонстрирует родственные взаимосвязи между классами TBase и Tderived. 
    В новом классе TDerived также объявлена открытая функция-член ChangeCount 
для увеличения и уменьшения значения члена count, наследованного из класса 
TBase. Конечно, поскольку count закрыт в классе TBase, в функции 
ChangeCount() нельзя получить непосредственный доступ к этому члену. Вы не 
можете написать функцию-член ChangeCount() следующим образом: 

    void ChangeCount(int n)  { count += n;   }	// ???                             

    Этот оператор вызовет сообщение об ошибке при компиляции, поскольку член 
count закрыт в базовом классе, и только члены TBase могут иметь непосредственный 
доступ к count. Для изменения члена count,  функция-член ChangeCountO может вызвать 
незакрытые наследуемые функции-члены, подобные SetCount()  и GetCount(): 

    void ChangeCount(int n) { SetCount(GetCount() + n);   } 


Глава 14. Объектно-ориентированное программирование 339 

    TDerived наследует данные-член count, но не может получить к нему 
непосредственный доступ 
    Конструкторы никогда не наследуются____ 
    class TBase { private: 
    int count; public: 
    TBase() { count = 0; } void SetCount (int n) 
    { count = n; } int GetCount(void) 
    { return count; } 
    class TDerived: public TBase { private: 
    int secondCount; public: 
    TDerivedf) 
    void ChangeCount(int n) 
    { SetCount(GetCount() + n); } 
    Функции-члены наследуются производным классом____ 

    Рис. 14.5. Взаимосвязь классов TBase и TDerived 

    Защищенные члены класса 

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

    Наследуются, но не доступны в производном классе 
    Базовый класс 
    Закрытые члены 
    Защищенные члены 
    Открытые члены^"^,/ 
    Базовый класс 
    ->•   Унаследованные члены 
    -*•   Унаследованные члены 
    Защищенные члены наследуются и доступны производному классу, но не во 
внешних операторах "*   Унаследованные члены 
    Открытые члены наследуются и доступны для всеобщего использования 

    Рис. 14.6. Закрытые, защищенные и открытые члены в базовом и производном классе 

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

    ЗАМЕЧАНИЕ 

    В главе15 объясняется, как друзья класса могут обойти эти правила. 

    340 Часть III. Программирование на ANSI C++ 

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

    сlass TAnyClass { 
    private:	* 
      int A;   // Доступен только членам TAnyClass 
      void fa(void);   //   "
   protected: 
     int В ;   // Доступен TAnyClass и членам производных классов 
     void fb(void) ;         //  "
     int С ;        // Доступен любым членам и всем,  кто его использует 
     vold fc(void);         //   "

    Член А закрыт в TAnyClass и доступен только другим членам этого класса, 
т.е. функции-члены fa(), fb() и fc()  имеют непосредственный доступ к А. Конечно, 
внешние операторы и даже функции-члены производных классов не могут 
ссылаться на А непосредственно. Вдобавок, функция-член fa() может вызываться 
только другими членами TAnyClass. Внешние операторы не могут вызывать 
функцию-член fa(), являющуюся исключительной "частной собственностью" 
TAnyClass.
   Член В и функция-член fb() защищены и доступны другим членам 
TAnyClass и любым членам производных классов. Конечно, вне класса 
защищенные члены имеют закрытый статус. Внешние операторы не могут иметь доступ 
к классу В или функции-члену fb(). 
    Член С и функция-член fc() открыты и свободны для прямого доступа всем 
членам TAnyClass, членам производных классов и операторам вне класса. 
Открытые члены класса всегда доступны для всех, кто использует этот класс. 
   Спеецификаторы доступа private, protected и   public могут также 
предшествовать имени базового класса в объявлении производного класса. 
Основанный на TAnyClass производный класс можно объявить следующим 
образом: 

    class TDerived: public TAnyClass { 
       // Новые данные-члены, функции-члены,  конструкторы etc. 
    } ;

    Спецификатор public можно заменить на private или protected. Если ни один 
из этих спецификаторов доступа не задан, члены наследуемого класса по умолчанию 
закрыты. Спецификаторы влияют на статус наследуемых членов. 
   • Члены открытого базового класса в производном классе сохраняют свои 
спецификации доступа public, protected и private. 
   • Открытые   члены   защищенного   базового   класса   становятся   
защищенными   в   производном   классе. Защищенные и закрытые члены сохраняют 
свои первоначальные спецификации доступа. Все  члены   закрытого  базового   
класса  становятся   закрытыми   в   производном   классе  независимо   от 
первоначальной спецификации доступа этих членов. 
    Третий случай, когда базовый класс объявлен закрытым для производного 
класса, наиболее существенно влияет на наследуемые члены. Для следующего базового класса 

    class TBase {
    protected: 
       int x;
    public:
       int у; 
       ......
    }

    В объявленном следующим образом производном классе 

    class TDerived:   private TBase {	// Обратите внимание на статус "private" 
    public:	
      void f(void); 
    };
 
    функция-член f() имеет доступ к членам х и у, наследованным из класса TBase. 
Однако, поскольку TBase объявлен закрытым базовым классом для класса TDerived, 
статус членов х и у изменился на private. В наследовании

    class TDescendant:  public TDerived { 
    public: 
       void g(void); 

    Главa 14. Объектно-ориентированное программирование 341 

функция-член g() не имеет доступа к членам х и у несмотря на то, что 
первоначально эти члены имели статус protected и public. 

    Квалификация доступа к отдельным членам 

    Спецификатор базового класса потенциально влияет на все наследуемые члены. 
Например, при объявлении 
    class А {         
    public:	
       int x;	' , 
       А() { cout « "Inside A's constructor\n"; x = 0; } 
       void Display(void) { cout « "x == " « x « '\n'; }
    } ;

класс В может наследовать А в качестве закрытого базового класса:	

   class В: private A {                                                   
   public:	
     B():  A()  { cout « "Inside B's constructor\n";  }   
    } ; 

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

    A objectA; 
    objectA. Display();        // Все в порядке,   Display() открыта в классе А 

    Строки же, приведенные ниже,  при компиляции вызовут сообщение об 
ошибке '"A::Display()'  is not accessible..." 'A:: Display ()' недоступна). 

    В objects; 
    objects.Display();       // ??? DisplayO закрыта в В 

    Если вы хотите сделать закрытыми в производном классе только некоторые из 
наследуемых членов, можно выборочно квалифицировать один или несколько из них. 
Предположим, вы хотите, чтобы открытый в А целочисленный член х стал закрытым 
в В, а открытая в А функция Display() осталась бы открытой в В так, чтобы 
внешние операторы могли ее вызывать. Для этого укажите базовый класс А 
закрытым в В, как это уже делалось ранее, а в открытой секции В квалифицируйте 
наследуемые члены, которые вы хотели бы оставить открытыми: 

    class В:   private А {	
    public:
      A::Display;               // Выборочное уточнение Display()
      В(): А() { cout « "Inside B's constructor";   }                      
    }  

    Выражение A::Display; в открытой секции класса В указывает компилятору, 
что этот член, объявленный в классе А, должен остаться открытым. Наследуемый 
целочисленный член х станет закрытым членом класса В, как и прежде, однако 
функция-член Display() останется открытой. Предыдущая попытка вызова Display()
для объекта класса В теперь скомпилируется и выполнится правильно: 

    В objectB;	
    objects. Display();  // Display() - открытый член В 


    Использование конструкторов и деструкторов 

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

    class TBase {	
    private: 
       char *basep; 
    public: 
       TBase(const char *s)  { basep = strdup(s);   } 
       ~TBase() { delete basep;   } 
       const char «GetStr(void)  {  return basep;   }
   } ; 
   
    
    342     Часть III. Программирование на ANSI C++ 

    Класс TBase имеет один защищенный член — указатель basep на строку 
символов. Конструктор класса вызывает библиотечную функцию strdupO для 
создания копии строкового аргумента в виде динамической переменной с 
присвоением адреса выделенной памяти указателю basep. Деструктор -TBase()
освобождает этот блок памяти, когда объект типа TBase выходит из 
области видимости. При создании объекта типа: 

    TBase  president("George Washington"); 

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

    cout « president.GetStr() ; 

    Когда president выходит из области видимости, деструктор класса удаляет 
строку, хранящуюся в куче. Рис. 14.7 иллюстрирует представление president в памяти. 


    Строка,	хранящаяся	в куче 
    Объект president класса TBase 
    char *basep 
    Другие члены класса 
    э*Ч 
    Указатель — член объекта ссылается на строку, хранящуюся в куче 
    Jnw 

    Рис. 14.7. Представление объекта типа TBase в памяти 


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

    class TDerived:  public TBase { 
    private: 
       char *uppercasep; 
    public: 
       TDerived(const char *s); "TDerivedO  { delete uppercase?;   } 
       const char GetUStr(void)  {  return uppercasep;   } ; 

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

    TDerived::TDerived(const char *s) 
        :  TBase(s)   // Вызов конструктора базового класса uppercasep 
    {
          strupr(strdup(s));         // Инициализация данных TDerived 
    }

    Конструктор TDerived вызывает конструктор TBase, копирующий строку, на 
которую ссылается s, в кучу и инициализирующий указатель basep адресом копии 
строки. По завершении этой операции конструктор производного класса создает 
еще одну копию строки, преобразуя ее символы в прописные буквы путем обращения 
к библиотечной функции strupr(), и присваивает uppercasep адрес этой копии строки. 
    Если вы создаете объект типа TDerived, значит вы создаете две копии 
строки: одну неизмененную и одну, символы которой преобразованы в прописные. 
Вы можете использовать объявление 

    TDerived president!"George Washington"); 

и затем выполнить следующие операторы: 

    cout « "Original string:   " « president.GetStr() « '\n'; 
    cout « "Uppercase string:  " « president.GetUStrO « '\n': 

    Первый оператор вывода отображает первоначальную неизмененную копию строки 
"George Washington". Второй — копию этой же строки, все буквы которой преобразованы 
в прописные, инициализированную конструктором производного класса: 

    Original string:  George Washington	
    Uppercase string:  GEORGE WASHINGTON


    Глава 14. Объектно-ориентированное программирование 343 

    Рис. 14.8 иллюстрирует представление объекта класса president в памяти. 

    Первоначальная строка, инициализированная конструктором базового класса 
    Объект president класса TBase 
    char *basep 
    char *uppercasep 
    Другие члены класса 
    "George Washington" 
    "GEORGE WASHINGTON" 
    
    Копия строки с преобразованием строчных букв в прописные, 
инициализированная конструктором производного класса

        Рис. 14.8. Представление объекта типа TDerived в памяти 


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

    ~TBaseO { delete basep;   } 

    Аналогично, деструктор TDerived освобождает память, на которую ссылается 
член uppercasep. Опять же, объявление деструктора встраиваемым позволяет 
упростить код: 

    ~TDerivedO { delete uppercasep;   }

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

    delete uppercasep;	

      Затем C++ вызывает деструктор базового класса, выполняющий оператор: 

    delete basep; 

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

    Использование замещающих функций-членов 

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

    class TBase {	
    private: 
       char *sptr; }
    public: 
      TBase(const char *s) { sptr = strdup(s); } 
      ~TBase() { delete sptr; } ;
      void Display(void) { cout « sptr « '\n'; }	
    } 


    344     Часть III. Программирование на ANSI C++ 



    Деструкторы выполняются i порядке от производного к базовому классу 

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

    Объекты производного класса 

    Объект базового     класса 

    Рис. 14.9. Подобно зданию, объекты возводятся вверх от основания и 
уничтожаются сверху вниз 


    Объявленный класс похож на те, что вы уже видели в этой главе. Конструктор 
TBase() вызывает strdup() для создания копии строкового аргумента в куче и 
присваивания адреса выделенного участка памяти закрытому члену sptr. 
Деструктор ~TBase()освобождает эту память, когда объект TBase выходит из 
области видимоcги. Функция-член Display() отображает адресуемую строку с 
помощью встраиваемого оператора вывода в поток. Вы можете объявить объект типа 
TBase как 

    TBase state("California") ; 

и затем вывести строку state оператором 

    state. Display() ;	

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

    cout « "State:   ";	
    state. Display(); 

    Но вам придется сделать аналогичные модификации во всей программе. Лучшее 
решение — вывести новый класс из TBase и заместить наследованную функцию-член 
Display() модифицированной версией. Вы можете написать производный класс 
следующим образом: 

    class TState:  public TBase {	
    public:	
      TState(const char *s): TBase(s) { } 
      void Display(void) ; // Замещающая функция
    } ;

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

    void TState::Display(void)	
   { 
       cout « "State:   "; // Новый оператор 
       TBase:: Display();    // Вызов замещенной функции-члена 
   } 


    Глава 14. Объектно-ориентированное программирование 345 

    Замещающая функция-член Display() выводит сначала "State: ", затем вызывает 
Display() базового клао для завершения строки. Поставив TBase:: перед DisplayO, 
вы указали компилятору, что следует вызвать функцию-член из TBase. 


    Объявление и использование объекта класса 


    TState state("Ohio"); 
    state.Display();

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

    class TPennsylvania:  public TState {	
    public:	
      TPennsylvania():  TState("Pennsylvanya") {}	
    };	

    Класс TPennsylvania выводится из TState. Новый конструктор, объявленный 
без параметров, передает символьную строку "Pennsylvania" конструктору базового 
класса. В следующих строках 

    TPennsylvania Pennsylvania;	
    Pennsylvania.Display() ;                                                                      

создается объект с именем Pennsylvania и выводится название штата. 
    Конструкторы также могут иметь дополнительные параметры. Наверное, вам 
понадобится класс, coxpаняющий кроме названия штата еще и плотность 
населения. Вы можете вывести новый класс из TState: 

    class TPopulation:  public TState { 
    private: 
       long population; 
    public: 
       TPopulation( long n,   const char *name ); 
       void Display(void); 
    }
 
    Конструктор TPopulation имеет длинный целый параметр плотности населения и 
символьный указатель на название штата. Вы можете реализовать конструктор 
следующим образом:	

    TPopulation::TPopulation(long n,  const char «name) 
    private:	
       long population;	
    public: 
       TPopulation (long n, const char *narae) ; 
       void Display(void); 
    }; 

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

    void TPopulation::Display(void) 
    { 
       TState: .Display() ; 
       cout « "Population == " « population « '\n'; 
    } 

    Где-нибудь в другом месте программы вы можете создать и использовать 
объект класса TPopulation для отображения названия штата и плотности населения: 

    TPopulation Nebraska(1570000,   "Nebraska"); 
    Nebraska. Display();	


    Использование наследования

    До сих пор в этой главе приводилось лишь несколько академических фактов, 
имеющих отношение к наследованию, производным классам, конструкторам и 
деструкторам. Более близкие к жизни примеры покажут, как заставить эти 
концепции работать в реальной программе. С помощью наследования вы можете 
выводить новые классы, приумножающие свойства уже существующих. Вместо того 
чтобы всякий раз писать код с чистого листа, вы сможете с помощью классов и 
наследования строить программы, использующие преимущества существующего кода, 
что значительно легче, чем с помощью обычных библиотек функций. 
    Borland C++ содержит расширенную библиотеку классов (см. главу 16). Один 
класс с именем TTimer разработан специально для событий, связанных со 
временем. В этой главе вы построите производный класс, ко


    346 Часть III. Программирование на ANSI C++

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

    ЗАМЕЧАНИЕ 

    Класс   TTimer   (с   двумя   Т)   в  Borland   C++   31   имел   имя   Timer 
  (с   одним   Т)    Определите   символ BI_OLDNAMES с помощью директивы #define 
для компиляции программ,  использующих старую форму имен  однако каждый 
символ имен функций-членов также следует преобразовать в прописной, так что 
лучше, откорректироватъ  программы  так,   чтобы  они  могли  использовать  
новые  классы.   Поскольку  TTimer   — низкоуровневый по своей природе (он 
обращается непосредственно к компьютерному оборудованию для обеспечения   
функций,   связанных  с  отсчетом   времени),   вы   можете   использовать  
этот  класс   только  в программах, написанных для DOS, но не для WINDOWS. 

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

    class TTimer
    { 
     public:
       TTimer();	// Конструктор        
       void Start();         // Начать отсчет времени 
       void Stop();	// Закончить отсчет времени 
       void Reset();	// Очистить таймер 
       int Status();       -  // 0==отсчет времени остановлен; 1==идет отсчет времени 
      double Time();         // Прошедшее время в миллисекундах 
      static double Resolution() ; // См. главу 15 
    private: 
      .......
  }
 
    ЗАМЕЧАНИЕ 

    Статические функции-члены описаны в главе 15. А пока не обращайте внимания 
на статическую; функцию-член resolution() в классе TTimer.	

    Предполагая, что вы установили Borland C++, используя имена каталогов по 
умолчанию, отметим, что объявление класса TTimer хранится в 
C:\BC45\INCLUDE\CLASSLIB. Модуль с исходными текстами T1MER.CPP располагается 
в C:\BC45\SOURCE\CLASSLIB. Ознакомьтесь с этими файлами для того„ ттобы узнать 
больше о том, как реализован класс TTimer. 
    Интересно, что в модуле класса TTimer имеется встроенная тестирующая 
программа, которую вы можете компилировать и запустить на выполнение. Для 
запуска программы на выполнение из DOS, перейдите в каталог, в котором располагается 
TIMER.CPP (на моем компьютере — C:\BC45\^OURCE\CLASSLIB), и введите следующую 
команду (аккуратно введите все пробелы, точно так, как показано ниже) :

    bcc -DTEST_TIMER -DBI_OLDNAMES -IC:\BC45\INCLUDE timer 

    Определение символа TEST_TIMER с помощью опции компилятора -D скомпилирует 
функцию main() в модуле TIMER.CPP, что позволит модулю обслужить собственную тестирующую 
программу.  Определение символа BI_OLDNAMES необходимо, поскольку тестирующая программа, 
похоже, была скопирована из версии 3.1 и, очевидно, не была перепроверена во время 
разработки Borland C++ 4.0. Вам необходимо задавать путь к включаемым файлам с 
помощью опции -I только в том случае, если это еще не сделано в вашей конфигурации 
по умолчанию. 
    После компиляции запустите на выполнение TIMER. Программа выводит отчет о 
точности таймера: 

    Resolution:   8.39e-07 
    О ms.,  actual time	=	0.024674	seconds. 
    100 ms.,  actual time	=	0.127362	seconds. 
    200 ms.,  actual time	=	0.222327	seconds. 
    300 ms.,  actual time	=	0.314453	seconds. 
    400 ms.,  actual time	=	0.393548	seconds. 
    500 ms.,  actual time	=	C.465171	seconds. 
    600 ms.,-actual time	=	0.630901	seconds. 
    700 ms.,  actual time	=	0.738385	seconds. 
    800 ms.,   actual time	=	0.775155	seconds. 
    900 ms.,   actual tin»	=	0.836996	seconds. 

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

    Глава 14. Объектно-ориентированное программирование 347 

    Создание класса измерения производительности 

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

    Листинг 14.1.  BENCH.H (объявление класса TBench) 

    1:   // bench.h -- Объявление класса TBench 
    2: 
    3:   #ifndef __BENCH_H 
    4:   #define __BENCH_H 1 // Предотвращение нескольких «include 
    5: 
    6: #include <classlib\timer.h> 
    7: 
    8: typedef void (* testfn)(void); 
    9:	
    10: class TBench: public TTimer { 
    11: public:	
    12:  TBench(): TTimer() { }                                 
    13:  void Benchmark(long numTests, testfn tf); 
    14:  void Report(void); 
    15: }; 
    16:	
    17: #endif // __BENCH_H

    Класс TBench выводится из TTimer. Конструктор TBench{} (строка 12) вызывает 
конструктор базового класса TTimer, но не выполняет никаких новых операторов 
для конструирования объекта TBench. Функци член Benchmark() (строка 13) имеет 
два параметра — numTests, равный числу тестов, которые необходимо выполнить, и 
tf, ссылающийся на тестируемую функцию, которая должна иметь тип void и не 
должна имет параметров (см. typedef в строке 8). Вторая функция-член Report(),
определенная в строке 14, отображает pезультат теста.	
    В листинге 14.2 приводится реализация производного класса TBench. 
Следующая программа демонстрирует использование этого модуля, поэтому не 
компилируйте его пока. 

    Листинг 14.2.  BENCH.CPP (реализация класса TBench) 

    1:	// bench. срр -- Реализация класса TBench 
    2: 
    3:	#include <stdio.h> 
    4:	#include "bench. h" 
    5: 
    6:	// Вызывает тестируемую функцию tf  numTests раз 
    7:	void TBench:: Benchmark (long numTests, testfn tf);
    8:	
    9:	printf( "Running %ld tests", numTests); 
    10:	Reset();   // Reset timer to zero 
    11:	Start() ;   // Start timing 
    12:	while (--numTests >= 0) 
    13:	   (* tf)(); // Call user test function 
    14:	Stop();    // Stop timing 
    15:	puts( "\nTests completed"); 
    16:	} 
    17:	
    18:	// Выводит результаты теста 
    19:	void TBench::Report(void)      
    20:	{
    21:	   double result = Time();
    22:	   if (result < Resolution())	
    23:	      putsC'Results too small for accuracy"); 
    24:	   printf( "Elapsed time == %6f sec. \n", result); 
    25:	}

    В строках 7-16 реализуется функция-член Benchmark(). После отображения 
числа выполняемых тестов, функция сбрасывает и запускает таймер с помощью 
вызова наследованных функций-членов Reset() и Start(). 
    В строках 12-13 вызывается необходимое число раз тестируемая функция, 
адрес которой хранится в указателе tf. После завершения последнего теста в 
строке 14 вызывается еще одна наследованная функция-член Stop(), прерывающая 
отсчет времени. В строке 15 выводится короткое сообщение о том, что тесты 
завершены.


    348 Часть III. Программирование на ANSI C++ 

    В строках 19-25 реализуется еще одна функция-член TBench — Report(). В 
строке 21 переменной result присваивается результат наследованной функции-члена 
Time(). В строках 22-23 выводится сообщение о том, что затраченное время слишком 
мало для того, чтобы его можно было верно оценить. (Если у вас возникло подобное 
сообщение, рекомендуется увеличить число тестов.) В строке 24 отображаются 
результаты измерений быстродействия в секундах. Для   того   чтобы   использовать   
класс  TBench,   следует   создать   объект   и   передать   функции-члену Benchmark()
число необходимых для выполнения тестов и адрес тестируемой функции. Затем следует 
вызвать функцию-член Report(). В листинге 14.3 демонстрируется эта последовательность 
действий, и он может послужить оболочкой для необходимых вам измерений производительности. 
После компиляции модулей BENCH.CPP и BENCH.CPP требуется скомпоновать их с 
библиотечным файлом TCLASDBS.LIB, содержащим скомпилированный код класса TTimer. 
Если вы пользуетесь автономным компилятором, добавьте следующие две строки в файл 
TURBOC.CFG, находящийся в текущем каталоге: 

    3B-IC:\c 45\include 
    -LC:\c 45\lib

    Затем из командной строки DOS введите следующую команду для компиляции 
модуля BENCH.CPP и тестовой программы TBENCH.CPP, использующей класс TBench. 

    с     teen h cen h bidss.lib 

    В дополнение к заданию имен файлов модулей TBENCH и BENCH предыдущая 
команда передаст компоновщику имя библиотечного файла ВС45 — BIDSS.LIB, в 
котором содержится скомпилированный код класса TTimer. Второй символ S в BIDSS 
означает малую (small) модель памяти, используемую в программах для DOS по 
умолчанию. (В главе 19 приводится более подробное объяснение использования 
моделей памяти в программировании для DOS и WINDOWS). Как уже отмечалось, 
класс TTimer, из которого выводится TBench, обращается к аппаратным средствам 
способом, не применимым для WINDOWS. Вы должны скомпилировать эту программу в 
качестве приложения для DOS.	

    ЗАМЕЧАНИЕ 

    Вы также можете скомпилировать TBENCH с помощью МАКЕ-файла TBENCH.МАК 
(текст которого здесь не приводится). Смените текущий каталог на тот, в 
котором хранится TBENCH GPP, и из командной строки DOS введите make 
-ftbench.mak Утилита Borland C++ MAKE EXE должна быть доступна для вызова по 
текущим установленным путям, и вам, возможно, придется скорректировать 
устройство и каталога в TBENCH.МАК. (См, главу 18 для того, чтобы больше 
узнать о МАКЕ ЕХЕ ) 

    Листинг 14.3. TBENCH.CPP (тестирование класса TBench) 

    1:   #include <iostream.h> 
    2:   #include <stdio.h> 
    3:   #include "cen h.h" 
    4: 
    5:    #define NUMTESTS 20000 
    6: 
    7:   void Testfn(void); 
    8: 
    9:   main() 
   10:    { 
   11:       TBench test; 
    12: 
    13:	cout « "Testing sprintf() fun tion\n" ;
    14:	test.Benchmark(NUMTESTS,   Testfn); 
    15:	test.Report();                   
    16:	return 0;                            
    17:   } 
    18:	
    19:	void Testfn(void)             
    20:	{                                                               Ъ 
    21:	 char cuffer[80]; 
    22:	 double d = 3.14159; 
    23: 
    24:	 sprintf(cuffer,   "%1f".  d); 
    25:     } 


    В строке 11 объявляется объект класса TBench с именем test. Поскольку C++ 
вызывает конструктор объекта класса (который вызовет конструктор базового 
класса Timer), вы можете быть уверены, что объект test соответствующим образом 
проинициализирован и, следовательно, готов к употреблению. После вывода 
короткого сообщения программа вызывает функцию-член Benchraark() (строка 14) и 
передает число необходимых для 


    Глава 14. Объектно-ориентированное программирование 349 

выполнения тестов, а также адрес тестируемой функции. В строке 15 
вызывается функция-член Report() объекта test для вывода результатов тестов. 
Тестируемая функция (строки 19-25) исключительно для демонстрации вызывает 
функцию sprintf(). Для проведения другого испытания измерения быстродействия 
замените ее держимое функции Testfn() какими-нибудь другими операторами. 
Выполнение программы приводит к вывод следующего отчета: 

    Testing sprintf() function 
    Running 20000 tests... 
    Tests completed 
    Elapsed time == 4.118690 sec. 

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

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

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

    ЗАМЕЧАНИЕ 

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

    Указатель на базовый класс может ссылаться на  объект базового класса или 
любого производного класса 
    Объект базового класса 
    Указатель на производный класс может ссылаться на объекты производного 
класса, но не на объекты базового класса 
    Указатель на базовый класс 
    Объект производного класса 
    Объект производного класса I 
    Указатель на производный класс 

    Рис. 14.10. Указатель на базовый клйос может ссылаться на объект 
производного класса

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

    A aObject;	//	Объявление	объекта	типа А                                    
    В bObject;	//	Объявление	объекта	типа В 
    С cObject:	//	Объявление	объекта	типа С 


    350 Часть III. Программирование на ANSI C++ 

    Указатель	на		
    базовый класс 
    Разрешено     Разрешено	 
     Разрешено 
    Базовый класс	1   Производный класс А	   Производный класс В	V 
    Данные-члены	^    Унаследованные	--       -"   1 
    !    базового класса i	данные-члены	|   данные-члены базового 
    Новые данные-члены   и производного классов 
    Новые данные-член
    Запрещено	Разрешено	
    Разрешено 
    Указатель на  производный класс А		

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

    По определению указатель типа А может ссылаться на любой из этих трех 
объектов, поскольку они связаны отношением наследования. Вы можете, к примеру, 
присвоить адрес cObject указателю на базовый класс р: 

    А *р;	// Объявить указатель на базовый класс р типа А* 
    р = &cObject; // Присвоить р адрес cObject 

    Несмотря на то что указатель р имеет тип А*, а не С*, он может ссылаться 
на объект типа С, поскольку этот класс выведен из А. Правило, возможно, будет 
понятнее вам, если вы будете думать о С, как об особом виде объекта А (так, 
золотая рыбка — особая разновидность рыб, но она все же остается рыбой). 
    Конечно, эта взаимосвязь объектов и указателей работает только в одном 
направлении. Объект типа С — особый вид объекта А, но объект типа А не является 
особым видом объектов В и С. 
    Если принцип все еще не ясен, представьте себе, что вы" — объект класса, 
выведенный из ваших родителей. Вы обладаете многими чертами ваших 
родителей, такими как цвет волос и кожи, наследованными от ваших отца и 
матери вместе с другими особенностями, наследованными от их родителей. Ваши 
родители и родители ваших родителей — часть вас, но вы не являетесь частью их. 
    Этот принцип становится особенно важным, когда в связанных отношениями 
родства классах определяются виртуальные функции.  Функции имеют точно такой же вид и 
программируются точно так же, как и обычные функции, только их объявления 
предваряются ключевым словом virtual. Например, класс А может объявить 
виртуальную функцию vf(): 

    class A { 
       public:	
          virtual void vf() ;
    }; 

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

    class В:   public A { 
     public: 
       virtual void vf(); // замещающая виртуальная функция    

    Когда в классе, подобном В, определяется виртуальная функция, имеющая 
одинаковое имя с виртуальной функцией класса-предка, такая функция называется 
замещающей. Виртуальная функция vf() в В замещает виртуальную функцию с тем же 
именем в А. 
    В классе С также можно определить замещающую виртуальную функцию: 

    class  С:    public   В   {
    public: 
       virtual void vf();
    }
    
    
    Глава 14. Объектно-ориентированное программирование 351 

    В данном случае употреблять ключевое слово virtual не обязательно, 
поскольку С находится в конце линии наследования. Конечно, если еще один класс 
D будет выведен из класса С и в D потребуется заместить функцию vf(), то в С эта 
функция должна быть определена с ключевым словом virtual. Будьте готовы сейчас 
последует самое важное. Вернемся к указателю р типа А*, который ссылается на 
объект cObject типа С*. Внимательно присмотритесь к оператору, вызывающему 
виртуальную функцию vf() для объекта, на который ссылается р: 

    p->vf();	 

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

    Виртуальные функции в реальной жизни 

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

    class Shape { 
    public:	
        ..........  // ... разнообразные члены 
        virtual void Draw() ; 
    }: 
 
    Вы еще не знаете, какие типы фигур вам потребуются, кроме того, вы 
захотите позже добавить новые фигуры. Конечно, вам известно, что необходимо 
где-то хранить коллекцию объектов Shape, вероятно, в массив указателей 
(предположим, он объявлен глобальным, и все указатели инициализированы нулевым значением): 

    Shape *picture[100];	// Массив указателей на объекты Shape 

    В соответствии с объявлением, массив picture может хранить адреса 100 
объектов Shape. Каждый элемент массива — указатель типа Shape*, который, как 
следует из предыдущего раздела и как вам уже известно, может ссылаться на 
объект типа Shape или любого производного от него класса. Поскольку в классе 
Shape определена виртуальная функция Draw(), вы можете запрограммировать цикл, 
вызывающий DrawO для указателей из массива picture: 

    int i = 0; 
    while (i < 100 && *picture[i] != 0)  { 
       // Вызвать виртуальную функцию Draw()             
       picture[i]->Draw(); 
       i++; 
    }

    Вы только что написали код, рисующий картинку до того, как у вас появилась 
хотя бы одна существующая фигура для прорисовки! Если и это не произвело на 
вас впечатления, имейте в виду, что вы можете скомпилировать этот код и 
сохранить его в библиотеке или .OBJ-файле. Позже вы (или другой программист) 
можете вывести новые классы из Shape, сохранить объекты этих классов в массиве 
picture, и цикл while будет вызывать виртуальные функции Draw() этих объектов 
для отрисовки соответствующих изображений. 
    Особо заметьте, что этот код не требует от вас точного задания типов 
данных объектов, на которые ссылаются указатели picture, требуется только 
чтобы эти объекты были производными от Shape. Объекты определяют во время 
выполнения, какую функцию Draw() следует вызвать. Предположим, вы хотите 
вывести классы Circle (круг) и Line (линия) из класса Shape: 
    
    class Circle:   public Shape { 
    public: 
       virtual void Draw(); 
    }; 
    
    class Line: public Shape { 
    public: 
       virtual void Draw(); 
    }; 

    Вам также необходимо запрограммировать замещающие функции Draw в классах 
Circle и Line для отрисовки соответствующих фигур. При выполнении программы 
рисующий цикл вызывает необходимые виртуальные функции, исходя из типа 
адресуемого объекта. Например, вы можете добавить в массив picture фигуры 
подобные следующим: 


    352 Часть III. Программирование на ANSI C++

    picture[]= new	Circle;
    picture[]= new	Circle; 
    picture[]= new	Line; 
    picture[]= new	Circle; 
    picture[]= new        Line; 

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

    Виртуальные и невиртуальные функции-члены 

    Рассмотрим еще один простой класс, в котором объявляются данные-члены, 
конструктор и функция-член, имея при этом в виду правила использования 
указателей на классы, изложенные в предыдущем разделе: 

    class TValue { 
    protected: 
       int value;	
       TValue(int n) { value = n;  }	
       int GetValue(void) { return value;  } 
   }; 

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

    Tvalue х(10);	// Инициализировать объект Tvalue x 
    cout « x.GetValue() « '\n';                 // Выведет 10 

    Затем выведите новый класс из TValue. Для демонстрации возможностей 
производный класс умножает значение базового класса на заданное значение. 
Давайте назовем этот класс TMult: 

    class TMult:   public TValue { 
    protected: 
       int multiplier;	
    public: 
       TMult(int n,  int m): TValue(n) { multiplier = m ; } 
       int GetValue(void) { return value * multiplier;   } 

    В TMult наследованная им функция-член GetValueO замещается новой функцией, 
возвращающей value * multiplier. Если вы объявите объект типа TMult и вызовете 
его функцию-член GetValue(), результатом будет, как вы могли ожидать, 
произведение двух значений, которыми был инициализирован объект. 

    TMult y(10,  2);	// Инициализировать объект TMult у                        
     cout « у.GetValueO « '\n';                 // Выведет 20                                                                     

    Посмотрим, .что произойдет, если вы используете указатель на объект типа 
TSingle или TDouble. Вы можете объявить и инициализировать указатель tvp типа 
TValue следующим образом: 

    TValue *tvp = new TValue(10); 

    Затем можете вызвать функцию-член GetValueO для объекта, на который 
ссылается указатель: 

    cout « tvp->GetValue() « '\n';	// Выведет 20 

    Конечно, вы также можете объявить указатель на производный класс TMult и 
вызвать замещающую функцию-член GetValue(): 

    TMult *tmp = new Tf -t(10,  2); 
    cout « tmp->GetValue() « '\n';	// Выведет 20 

    Применив правило C++, касающееся указателей и производных классов, вы 
также можете объявить указатель на базовый класс 

    TValue *basep;	// Указатель на объект TValue 

и затем присвоить адрес производного объекта этому указателю: 

    basep = new TMult(10, 2);         // !!! Адрес производного объекта 

    Указатель basep был объявлен для ссылок на объекты класса TValue, однако в 
данном операторе создается производный объект TMult, а его адрес присваивается 
basep. Как вы думаете, что произойдет при выполнении ледующего оператора: 

    cout « basep->GetValue() « '\г\';         // ??? Выведет 10 

    Этот оператор скомпилируется, однако не даст ожидаемого результата — 20. 
Как известно, в C++ указатель basep ссылается на объект TValue. 
Следовательно, C++ вызывает функцию-член GetValueO из базового 


    Глава 14. Объектно-ориентированное программирование 353 
    Освоение Borland® C++4.5 

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

    ЗАМЕЧАНИЕ 

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

    В листинге 14.4 объявляются классы TValue и TMult, как это было описано 
ранее, но с одним отличием. В новых классах GetValue() объявлена виртуальной 
функцией-членом. Вы можете скомпилировать и залустить программу как DOS 
или EasyWin-приложение. 

    Листинг 14.4.  VIRTUAL,CPP (виртуальные и невиртуальные функции-члены) 

   1.   #include <iostream.h> 
   2. 
   3:	class TValue { 
   4:		protected: 
   5:		   int value; 
   6:		public: 
   7:		   TValue(int n) { value = n; }                     
   8:		   virtual int GetValue(void) { return value; }
   9:		};                      
   10:
   11:	class TMult:   public TValue { 
   12:	protected:               
   13:	   int multiplier; 
   14:	public:
   15:	   TMult(int n,  int m): TValue(n) { multiplier = m;   } 
   16:	   virtual int *GetValue(void)  { return value * multiplier ; } 
   17: }; 
   18:
   19: main()
   20: { 
   21:   	TValue *basep;
   22:
   23:	basep = new TValue(10);   
   24:	cout « basep->GetValue() «  '\n';    // Выведет 10 
   25:	delete basep; 
   26:	 
   27:	basep = new TMult(10,  2);         .   // !!! 
   28:	cout « basep->GetValue() « '\n';    // !!! Выведет 20 
   29:	delete basep;
   30:	
   31:      return 0; 
   32: }         

    В строках 8 и 16 объявлению функции GetValue() предшествует 
ключевое слово virtual. Это служит указанием компилятору, что вызов 
функции-члена GetValue() компонуется во время выполнения. В технической
реализации адреса двух виртуальных функций-членов GetValue()
сохраняются во внутренней таблице. Когд операторы вызывает виртуальную 
функцию-член, C++ ищет адрес нужной функции в этой таблице. Если вы полагаете, 
что эти поиски отнимают время, то вы совершенно правы. Использование 
виртуальных функци может отрицательно сказаться на быстродействии вашей 
программы, хотя на практике серьезные замедления нежелательны. 
    Виртуальные функции-члены наследуются производными классами, как и обычные 
функции-члены. Конечно, однажды объявленная виртуальной, функция-член не 
может измениться. Другими словами, для добавления нового кода в функции 
GetValue() эта функция-член должна объявляться в производном классе TMult
(строки 11-17) точно так же, как это было сделано в TValue (строки 3-9). 
Конечно, вы можете удалитьключевое слово virtual в последнем из производных 
классов в линии наследования. 
    Когда вы запустите VIRTUAL на выполнение, вы увидите на экране значения 10 
и 20 — результаты вызова двух виртуальных функций-членов GetValue(). В строке 
21 объявляется указатель basep на класс TValue. В строке 23 вызывается new для 
создания объекта TValue, который инициализируется значением 10 (строка 24), 


    354 Часть III. Программирование на ANSI C++ 

    После, удаления этого объекта (строка 25) в строке 27 создается объект 
производного класса TMult. Coгласно: правилу использования указателей на классы в 
C++ указатель basep может ссылаться на объект производного  класса, даже если бы он 
был объявлен указателем на объект класса TValue. Поскольку basep ссылается на 
объект типа TMult, строка 28 отображает значение 20. 
    Внимательно сравните операторы вывода в строках 24 и 28. Как вы можете 
убедиться, исключая комментарии в конце строки, эти операторы абсолютно идентичны. 
Но как два одинаковых оператора могут дать разный результат? Ответ прост — 
объекты, на которые ссылается указатель, сами определяют для себя, какой из двух 
виртуальных функций-членов GetValue()следует вызвать. И что еще важнее, это 
решение принимается программой во время выполнения, а не компилятором или программистом. 

    Чисто виртуальные функции-члены 

    Чистая виртуальная функция-член — пустое место, ожидающее своего 
заполнения производным классом. 
     Объявляется чистая виртуальная функция точно так же, как и обычная, только 
ее объявление в классе завершается  = 0. Например, в классе 
   class AbstractClass    {  
   public: 
      virtual void f1(void);         // Обычная виртуальная функция-член 
      virtual void f2(void) = 0;  // Чистая виртуальная функция-член 
    } 

   f2()  - чистая виртуальная функция-член. Компилятору не потребуется реализация 
функции-члена f2(), в отличие от остальных функций-членов, объявленных в классе. 
   Если класс содержит хотя бы одну виртуальную функцию-член, он называется 
абстрактным классом. Подобно любой абстрактной идее, абстрактный класс описывает 
нереализованные концепции. Абстрактный класс —  лишь схема, на основании которой 
вы собираетесь создавать производные классы. В C++ вы не можете создавать  
объекты,   имеющие  типы   абстрактных  классов.   Например,   вы  не  сможете  
объявить объект  типа AbstractClass. Если вы предпримете попытку скомпилировать 
следующую строку: 

    AbstractClass anyObject;	// ??? 

    Компилятор выдаст сообщение об ошибке "Cannot create an instance of class 
'AbstractClass' (He могу создать класса 'AbstractClass'). Конечно, другие функции-члены 
класса AbstractClass могут вызывать чистую виртуальную функцию-член. Например, 
функция-член f1() может вызвать f2(): 

   void AbstractClass::f1(void) 
   {
      f2();	// Вызвать чистую виртуальную функцию-член 
      .....      //  Прочие операторы f1() 
    }

    Для того чтобы использовать абстрактный класс, следует вывести из него 
новый класс: 

    class MyClass:   public AbstractClass { 
    public:
      virtual void f2(void);	// Бывшая чистая виртуальная функция                
      .........
     }

     Производный класс MyClass наследует чистую виртуальную функцию-член,  но 
объявляет ее без = 0. ( Включение этого фрагмента сделает класс MyClass абстрактным.) 
Где-нибудь в другом месте следует реализовагь функцию-член f2(): 

    void MyClass::f2(void) 
    {

    _	// Операторы функции-члена 

    }

    Обращения к первоначальной чистой виртуальной функции-члену, как это 
реализовано, к примеру, в данном  случае с AnyClass::f1(), теперь переадресованы 
MyClass::f2(). С помощью простых объявлений чистых виртуалъных функций-членов 
в производном классе вы можете вставлять заглушки в код, которые могут быть другими 
функциями-членами, исключая в дальнейшем необходимость изменения и даже 
повторной компиляции этих членов. Чистые виртуальные функции подобны крючкам, на 
которые вы можете цеплять код  производных классов. 
    Теперь, когда все виртуальные функции-члены реализованы, компилятор сможет 
воспринять объекты типа <MnClass: 

    MyClass myObject; 

    Выполнение оператора	

    myObject.f1 ();	

    приведет к вызову наследованной f1(), которая вызовет f2() из MyClass.	: 


    14. Объектно-ориентированное программирование 355 

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

    Листинг 14.5.  SET.H (объявление класса TSet) 

    1:	// set.h -- Объявление класса TSet 
    2: 
    3:	#ifndef __SET_H 
    4:	#define _SET_H 1  // Предотвращение нескольких «include 
    5: 
    6:	class TElem ; 
    7:	typedef TElem *PTElem; 
    8:	typedef PTElem *PPTElem; 
    9: 
    10:	class TSet; 
    11:	typedef TSet *PTSet;   
    13:	class TSet {      
    14:	private:     
    15:		int max;    // Максимальное число элементов 
    16:		int index;   // Индекс массива множества 
    17:		PPTElem set; // Указатель на массив указателей на PTElem 
    18:	protected:                                    " 
    19:	  virtual int CompareElems(PTElem pi, PTElem p2) = 0; 
    20:	public: 
    21:	  TSet( int n ) 
    22:	      { max = n;  index = 0;  set = new PTElem[n];   } 
    23:	   virtual ~TSet() 
    24:	     { delete[] set;   } 
    25:	   void AddElem(PTElem p); 
    26:	   int HasElem(PTElem p);
    27:	}; 
    28: 
    29.    #endif       /I   __SET _H 
    
    Для того чтобы сделать класс TSet как можно более гибким, классу 
необходимо обеспечить его возможностью запоминать объекты классов незаданных 
типов. Как показано в строке 6, C++ позволяет делать неполные объявления 
классов, не имеющих тела: 

    class TElem;   // Неполное объявление класса	

    В соответствии с этим объявлением объекты класса TElem могут задаваться в 
параметрах функций, возвращаться функциями и т.д. Конечно, позже в программе 
должно присутствовать полное объявление класс TElem. Неполное описание класса 
дает возможность написания класса TSet без каких-либо привязок к конкретному 
типу объектов, которые будут запоминаться в классе. В строках объявляются два 
дополнительны символа, облегчающих чтение листинга. PTElem объявлен алиасом 
для TElem*. Другими словами, PTElem - указатель на объект типа TElem. 
Аналогично, PPTElem объявлен алиасом для указателя PTElem, т.е. указателем на 
указатель на объект типа TElem. В классе TSet алиас PPTElem используется для 
создания динамически изменяющего свой размер массива указателей PTElem. 
    В строках 10—11 похожие объявления используются для задания неполного 
описания класса TSet и алиас PTSet в качестве указателя на объект TSet. 
Единственная причина неполного объявления TSet в строке 10 - возможность 
компиляции последующего typedef-объявления. 
    Затем следует объявление класса TSet (строки 13-27). Три закрытых 
данных-члена скрывают детали внутренней реализации класса. В этой версии TSet 
множество хранится в массиве указателей PTElem, динамичесм изменяющем свой 
размер, указатель PPTelem используется для хранения адреса массива. (Член set 
— указател на массив указателей на объекты типа TElem.) Члены max и index 
используются для управления массивом. 

    СОВЕТ

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

    В строке 19 объявляется чистая виртуальная функция-член CompareElems как 
защищенный член класо TSet. Чистые виртуальные функции-члены также могут быть 
и открытыми, но чаще всего они защищены, по 


    356 Часть III. Программирование на ANSI С++

предполагается, что функция будет реализована в производном классе. 
Предполагается, что функция-член CompareElems() возвращает нуль, если два объекта 
класса TElem, на которые ссылаются указатели р1 и p2 идентичны. Очевидно, вы все еще 
не можете завершить функцию CompareElemsO, поскольку вы не имеете ни маллейшего 
представления о том, что содержит класс TElem. Конечно, с помощью 
виртуальных функций-членов вы можете, по крайней мере, завершить обобщенную 
планировку, необходимую для класса TSet. планировка объявлена и "частично 
реализована в строках 22-26. Встраиваемый конструктор класса (строки 21-22) 
инициализирует закрытые данные-члены класса и обращается к new для выделения 
массива указателей PTElem. Деструктор класса (строки 23-24) освобождает эту память, 
очищая все объекты TSet перед их выходом из области видимости. 
    Функции-члены AddElem() и HasElem() (строки 25-26) слишком длинны для 
того, чтобы сделать их встраиваемыми. Модуль реализации класса (листинг 14.6) 
дополняет эти функции. (Вы можете скомпилировать эту программу, но для 
того, чтобы использовать класс, необходима основная программа. Пример приво--  
дится в следующем разделе.) 

    14.6.  SET.CPP (реализация класса TSet) 

    1:  // set.cpp -- Реализация класса TSet 
    2:
    3:    #include <iostream.h> 
    4:    #include <stdlib.h> 
    5:    #include "set.h" 
    6:
    7:	// Добавляет элемент в множество. (В случае ошибки программа прерывается!) 
    8:	void TSet::AddElem( PTElem p) 
    9:	{ 
    10:	  if (set == NULL) { 
    11:	    cout « "\nERROR: Out of memory"; 
    12:	    exit ( 1 ) ;                             
    13:	  }      
    14:	  if (index >= max) {                     
    15:	    cout « "\nERROR: Set limit exceeded";          
    16:	    exit(1);                              
    17:	  } 
    18:	  set[index] = p; 
    19:	  ++index; 
    20:	} 
    21: 
    22:	// Возвращает истину, если элемент, адресуемый р, находится в множестве 
    23:	int TSet:: HasElem( PTElem p) 
    24:	{                 
    25:	  if (set == NULL)           
    26:	    return 0; // В пустом множестве не может быть элементов     
    27:	  for ( int i = 0; i < index; i++  ) 
    28:	    if (CompareElems(p, setfi]) ==0)       
    29:	        return 1; // Элемент в множестве                    
    30:	  return 0; // Элемента в множестве нет         
    31:	}                                      


    В SET.CPP реализуются две функции — AddElemO и HasElemO. Любопытно, что 
эти функции написаны до того, как будут разработаны элементы, которыми они 
оперируют. Все, что известно об элементах, это лишь тоо. что они имеют тип 
неопределенного класса с именем TElem. 
    Но даже в этом случае AddElemO (строки 8-20) добавляет в множество объекты 
класса TElem, адресуемые параметром р. В строках 10-17 осуществляется проверка 
состояния ошибки и аварийного завершения программы в случае нехватки памяти 
или переполнения множества. (В более сложных классах обеспечивается лучшая 
обработка ошибок, однако для данного примера вполне достаточно и такой 
минимальной поддержки.) В строках 18-19 объект TElem, на который ссылается 
указатель р, запоминается в массиве set. 
    Функция-член HasElemO (строки 23-31) возвращает истину, если переданный 
объект TElem находится в множестве. Функция HasElemO вызывает чистую 
виртуальную функцию-член класса CompareElemsO в строке 28. Несмотря на то 
что эта функция еще даже не существует, ее уже можно вызвать! В производном 
классе со временем будет реализован настоящий код, осуществляющий сравнение 
двух элементов. Благодаря виртуальным функциям-членам, оператор в строке 28 
может обратиться к еще не реализованному коду. 
    В программе TSET.CPP (листинг 14.7) поставляются недостающие части TSet и 
TElem для создания множества строк, содержащих названия месяцев, имеющих 
ровно 30 дней: апрель, июнь, сентябрь и ноябрь. Программа иллюстрирует, как 
виртуальные функции-члены и абстрактные классы могут обеспечить простоту 
разработки программ, которые будут завершены позднее. Для создания 
законченной программы следует скомпилировать и скомпоновать модули 
TSET.CPP и SET.CPP. Находясь в DOS, введите команду bcc tset set, за-


    Глава 14. Объектно-ориентированное программирование 357 

тем запустите на выполнение получившийся в результате исполняемый файл 
TSET.EXE, введя его имя. Или  же  из  интегрированной  среды  откройте файл
проекта  TSET.IDE  с   помощью   меню  Project  и   нажмите <Ctrl+F9> для 
компиляции и выполнения программы в виде EasyWin-приложения. 

    Листинг 14.7.  TSET.CPP (тестирование класса TSet) 

   1:	#include <iostream.h> 
   2:	#include <string.h>                                                     
   3:	#include "set.h"
   4:
   5:	class TElem {                                            
   6.	  private:            
   7:	     char *sp;    // Указатель на элемент-строку 
   8:	  public: 
   9:     TElem(const char *s) { sp = strdup(s);   }
  10:	    virtual -TElem() { delete sp;   }                                   
  11:	    virtual const char *GetString(void) {  return sp;   } ;                                                                 
  12: }
  13: 
  14:	class TMySet:   public TSet {           
  15:		protected:                                                            
  16:	   		virtual int CompareElems(PTElem p1,  PTElem p2);       
  17:		public:                                                                
  18:	   		TMySet(int n): TSet(n) { }                 
  19: };
  20:
  21:	void Test(const char »s,  PTSet setp); 
  22:	
  23: 	main()
  24:	{        
  25:	   TMySet thirties(12);    // Множество из 12 объектов TElem              
  26:	
  27:	   thirties.AddElem(new TElemC'Sep"));                           
  28:	   thirties.AddElem(new TElem("Apr"));                            
  29:	   thirties.AddElem(new TElem("Jun"));                                   
  30:	   thirties.AddElem(new TElem("Nov")} ;  
  31:	   Test( "Jan", &thirties);              
  32:	   Test("Feb", &thirties);            
  33:	   Test("Mar", &thirties);                                  
  34:	   Test("Apr", &thirties);                            
  35:    Test("May", &thirties);
  36:	   Test("Jun", &thirties);      
  37:	   Test("Jul", &thirties); 
  38:	   Test("Aug", &thirties); 
  39:	   Test("Sep", &thirties); 
  40:	   Test("0ct", &thirties);                               
  41:	   Test("Nov", &thirties);                             
  42:	   Test("Dec", &thirties);       
  43:	   return 0; 
  44:	} 
  45: 
  46:	// Сообщает,  принадлежит ли строка s множеству setp 
  47:	void Test(const char *s,   PTSet setp)	
  48:	{ 
  49:	  TElem testElem(s); 
  50: 
  51:	  if ( setp->HasElem( &testElem ) ) 
  52:	    cout « s « " is in the set\n"; 
  53:	  else 
  54:	    cout « s « " is not in the set\n"; 
  55: }
  56:		
  57:	// Возвращает нуль,  если два элемента идентичны,  иначе - не нуль 
  58:	int TMySet::ConpareElems( PTElem p1,  PTElem p2) 
  59:	{ 
  60:	   return ( stricmp(p1->GetString(), p2->GetString() ) ); 
  61:	} 

    Первый шаг к использованию класса TSet — написание класса TElem. Объекты 
класса TElem сохраняются в множестве. В строках 5-12 объявляется образец 
класса,TElem, способный запоминать строковые значения. Все функции-члены класса 
реализованы встраиваемыми. 
    

    358 Часть III. Программирование на ANSI С++

    В строках 14-19 из абстрактного класса TSet выводится новый класс TMySet. 
Конструктор нового класса не выполняет никаких дополнительных действий, он 
просто вызывает конструктор базового класса TSet(). Более существенно то, что 
в строке 16 объявляется виртуальная функция-член CompareElems(), объявленная 
чистой виртуальной функцией в классе TSet. 
    Пропустим все до конца листинга, где приводится реализация функции-члена 
CompareElems(). Эта функция вызывает библиотечную функцию stricmp() для 
сравнения двух строк. Доступ к этим строкам осуществляется путем обращения к 
функции-члену GetStringO класса TElem (см. также строку 11). При вызове в 
проке 51 HasElem() эта функция-член вызывает реализованную в строках 58-61 
CompareElems(). Вы можете запустить получившуюся программу в Turbo Debugger и 
проанализировать вызов этой функции для наблюдения за тем, как программа 
переадресует CompareElemsO. 
    Данный пример использования виртуальных функций-членов и абстрактных 
классов приводит к нескольким интересным наблюдениям. 
    •   Модуль SET можно скомпилировать заранее и сохранить в библиотеке 
классов. Пользователям не нужен файл SET.CPP с исходным текстом класса TSet. 
Основной программе необходим лишь заголовочный файл SET.H. 
    •  Другая программа может использовать класс TSet для хранения множества 
различных классов объекта TElem. He нужно повторно компилировать SET.CPP для 
каждого нового использования класса. Класс TSet обеспечивает лишь некоторые 
рудиментарные операции над множеством. Пользователю класса предоставляется 
реализация прочих деталей. 
    •   И наконец, при проектировании абстрактных классов, подобных TSet, 
программисты обычно помещают в класс множество виртуальных функций-членов на 
тот случай, если они когда-нибудь понадобятся. При создании абстрактных 
классов постарайтесь обойтись минимумом необходимых членов, вместо того чтобы 
пытаться охватить возможно большую область применения. Если ваши классы будут 
слишком сложны, другие программисты (и даже впоследствии вы сами) могут быть 
обескуражены перспективой вывода новых классов из ваших.	

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

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

    class TBase { 
    private: 
       char *sp1; 
    public: 
      TBase(const char *s) 
           { sp1 = strdup(s);   }	
	virtual ~TBase() 
        { delete sp1;   } 
    };

    Конструктор класса TBase выделяет пространство для строки путем обращения 
к функции strdup()и сохраняет адрес новой строки в указателе sp1. Виртуальный 
деструктор освобождает эту память, когда объект типа TBase выходит из области видимости. 
    Далее, выведем новый класс из TBase: 

    class TDerived:  public TBase { 
    private: 
       char *sp2; 
    public: 
       TDerived(const char *s1,  const char *s2):  TBase(s1) 
            { sp2 = strdup(s2);   } 
       virtual ~TDerived() 
          { delete sp2;   } 
    }; 

    Новый класс сохраняет вторую строку, на которую ссылается указатель sp2. 
Новый конструктор вызывает TBase(), передавая строку в базовый класс, а также 
выделяет дополнительно немного памяти для второй копии строки, удаляемой 
деструктором класса. 
    Когда объект TDerived выходит из области действия, важно, чтобы обе копии 
строки были удалены. Предположим, вы объявили указатель на класс TBase, но 
присвоили ему адрес объекта TDerived — это вполне допустимо, поскольку, как 
вам уже известно, указатель на базовый класс может ссылаться на объект этого 
класса или на объект любого производного класса. Программа на этой стадии 
выглядит следующим образом: 


    C++     Глава 14. Объектно-ориентированное программирование 359 

    TBase *pbase; 
    pbase = new TDerived("String 1",  "String 2"); 

    Рассмотрим что произойдет,  когда программа удалит объект,  на который 
ссылается указатель pbase: 

    delete pbase; // !!! 

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

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

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

    Базовый класс А      I     Базовый класс В      I        Базовый класс С      I 

                                            Новые данные-члены

    Рис. 14.12. Множественное наследование 

    Подключение базовых классов 

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

    class D:  public A,  public В,  public С {	
     ......                          
    }; 

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

    class D:  public A,   public В,  С {
     ........
    };

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

    class D:  public A,  public В { 
    public: 
         void f(void); 
         .......
     };

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

    
    360 247 Часть III. Программирование на ANSI C++

   void D::f( void )
   { 
       Display();      // ??? Двусмысленно; не скомпилируется 
       А::Display() ;    // Вызов функции-члена А 
       В::Display() ;    // Вызов функции-члена В 
   }
    
    Использование конструкторов нескольких базовых классов 

    В классе, выведенном из нескольких базовых, также может возникнуть 
необходимость в вызове конструкторов этих классов. Если классы А, В и С имеют 
конструкторы по умолчанию, в производном классе D их можно вызвать следующим образом:	

    сlass D:  public A,  public В,  public С { 
    public:
       D():  А(),   В(),   с()  {  }	// Встраиваемый конструктор 
       ........
     };

    Или же конструктор можно объявить как 

    class D:   public A,   public В,   public С { 
    public: 
        D() ;	// Конструктор,  реализованный снаружи 
        ......
    }; 

    и реализовать отдельно, обычно, в другом файле: 

    D::D() 
       :  А(),   В(),   С() 
    {	
        ... // Операторы в конструкторе D() 
    }
 
    Конечно, конструкторы могут иметь различные параметры, не использующиеся в 
данном примере. Конструкторы вызываются в том порядке, в котором объявлены их 
базовые классы. Было бы неразумно придумывать классы, зависящие от того, в 
каком порядке будут созданы объекты других классов. Как правило, классы 
должны действовать независимо друг от друга настолько, насколько-это вообще возможно. 

    ЗАМЕЧАНИЕ 

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

    Использование виртуальных базовых классов 

    Производные классы и их базовые классы образуют иерархию, которая может 
достичь невероятной сложности даже в относительно простых программах. Базовый 
класс может наследоваться одним и более классами, которые, в свою очередь, могут 
стать базовыми для еще большего числа классов. Следовательно, все эти классы 
связаны отношениями простого и множественного наследования и, подобно библейским 
персонажам, один из них порождается другим до тех пор, пока уже никто не сможет сказать, 
кто к кому относится. 
    Легко понять, почему возникают конфликты в сложной иерархии классов, 
особенно когда дело касается множественного наследования. Один из самых 
распространенных конфликтов возникает, когда производный класс наследует 
слишком много копий некоторых базовых классов, подобно победителю лотереи, 
который в один прекрасный день обнаруживает, что у него невероятное количество 
"троюродных сестер", о существовании которых он раньше даже не подозревал. 
    Для демонстрации того, как в производных классах возникают неприятности, 
связанные с наследованием, ознакомьтесь с классами программы, приведенной в 
листинге 14.8. В программе используется сложная взаимосвязь между тремя 
фиктивными компаниями и тремя вымышленными инвесторами: Бобом (Bob), Тедом 
Ted) и Алисой (Alice). Программа может показаться неудачной шуткой, однако 
заимствование тесных взаимосвязей из реальной жизни поможет объяснить 
типичные и зачастую доводящие до белого каления проблемы, возникающие с 
множественным наследованием. 


    Глава 14. Объектно-ориентированное программирование 361 

    Листинг 14.8. FRANCH.CPP (множественное наследование и вкладчики) 

 1: 	// franch.cpp 
 2:
 3:	#include <iostream.h>                    
 4:	#include <string.h>        
 5:
 6:	class Company {
 7:	private: 
 8:                char *name;                                                  
 9:	public:                                
10:	  Company(const char *s) {	
11:	    name = strdup(s); 
12:                cout << " In constructor for" ;
13:	    Display() ;
14: 	  }
15: 	  virtual ~Company() {
16: 	    cout << " In destractor for";
17:	    Display() ;
18:	    delete name;                                    
19:	  }
20:	  void Display(void) { cout « name « '\n';   } 
21:	}
22: 
23:	class Jennys:  public Company {
24:	public: 
25:	  Jennys():  Company("Jenny's") { }
26:	}
27: 
28:	class McDougles:  public Company {    
29:	public:                                      
30:	  McDougles():  Company( "McDougles")  { }   
31:	}
32:	
33:	class BurgerOueen:  public Company {   
34:	public:                               
35:	  BurgerQueen():  Company ("BurgerQueen") { } 
36:	} ;                                                                    
37:	
38:	class Bob                                 
39:	:  public Jennys,                         
40:	   public McDougles {                                   
41:	}
42: 
43:	class Ted 
44:	  : public McDougles, 
45:	    public BurgerOueen { 
46:	}
47: 
48:	class Alice                        
49:	   : public Jennys,	
50:	     public McDougles,                   
51:	     public BurgerQueen {
52:	 }
53:		
54:	main() 
55:	{ 
56:	   Bob *bobp; 
57:	   Ted  *tedp; 
58:	   Alice *alicep; 
59: 
60:	  cout « "\nlnitializing Bob's restaurant\n"; 
61:	  bobp = new Bob; 
62:	  cout « "Initializing Ted's restaurant\n"; 
63:	  tedp = new Ted; 
64:	  cout « "Initializing Alice's restaurant\n"; 
65:	  alicep = new Alice; 
66: 
67:	  cout « "\nDeleting Bob's restaurant\n";
68:	  delete bobp; 
69:	  cout « "Deleting Ted's restaurant\n"; 
70:	  delete tedp; 
71:	  cout « "Deleting Alice's restaurant\n";	
72:	  delete alicep;	
73:	
74:	  return 0; 
75:	} 
    
    
    362	Часть III. Программирование на ANSI C++

Company
   Jennys	McDouglas BurgerQueen	
Bob Ted Alice

РиС-14-13- Иерархия классов в программе FRANCH.CPP

    На рис. 14.13 иллюстрируется родословная классов FRANCH.CPP. В корне 
иерархии — класс Company, служащий базовым для трех производных 
классов — Jennys, McDouglas и BurgerQueen. Каждая из этих "компаний"-классов 
выводится из класса Comрапу, и, следовательно, каждый класс наследует член
name (строка 8), а также функцию-член Display() (строка 20).
    Три предприимчивых инвестора, Боб, Тед и Алиса, объявлены классами (строки 
38-52). Класс Bob строит свою кулинарную империю из двух классов Company — 
Jennys и McDougles. Класс Ted делает себе состояние на McDougles и 
BurgerQueen. Alice, самая честолюбивая душа из троицы, вкладывает средства 
сразу в три класса Company — Jennys, McDougles и BurgerQueen. Когда вы 
запустите программу, то увидите сообщения, рапортующие, какие конструкторы и 
деструкторы и когда были запущены, как они инициализировали и очищали три 
объекта классов, по одному на каждого вкладчика (строки 56-58). 

     СОВЕТ

    При отладке вашего собственного кода вы можете использовать операторы 
отображения, подобные тем, что использовались в FRANCH.CPP. Отображение 
сообщений из конструкторов, деструкторов или функций-членов — полезная обратная 
связь с сложной иерархической организацией классов. 

    Иерархия классов в FRANCH.CPP на первый взгляд проста. Однако настоящие 
проблемы с родством классов возникают, когда еще один класс выводится из группы 
компаний и инвесторов. Предположим, еще одна корпорация приобретает некоторые 
из компаний-родителей и избранных вкладчиков. Как и в реальном мире корпоративных 
финансов, сложные взаимосвязи между компаниями и людьми могут легко стать 
неуправляемыми. Рассмотрим класс Corporation, в котором делается попытка 
наследования компании McDougles вместе с акционерами Тедом и Алисой:

    class Corporation 
         public McDougles,	// ??? 
         public Ted, 
         public Alice {       
    public: 
         ..........
    };

    Вернемся назад, к рис. 14.13, и вы увидите, почему версия Corporation 
никогда не оторвется от земли. (Этот код даже не скомпилируется.) Ted и Alice уже 
частично выведены из McDougles. Попытка сделать то же самое в Corporation, 
плачевно заканчивается наследованием нескольких базовых классов McDougles. На 
этом компилятор выдаст вам сообщение о том, что "McDougles is inaccessible 
because [it is] also in Ted" (McDougles недоступен, поскольку он уже в Ted). 
    Когда вы получите подобное предупреждение в сложном множестве 
взаимосвязанных классов, попытайтесь идентифицировать те базовые классы, для 
которых необходим только один объект. В случае с Corporation имеет эиысл иметь 
только одну компанию McDougles. Даже несмотря на то, что Ted выводится из 
McDougles, это тот же McDougles, который пытается наследовать Corporation. В 
процессе приобретения активов Теда Corporation не определяется с двумя 
отдельными компаниями-родителями McDougles. Присутствует лишь один такой 
родитель, меющий непосредственное отношение к Corporation и косвенное, к ней 
же, через Ted. 
    В похожих (однако менее фривольных) ситуациях, когда в классах, 
наследующих несколько других, требуется только одна копия множественно 
наследованного базового класса, вы можете одним махом сократить 
число объектов этого базового класса до одного-единственного, объявив его 
виртуальным. В иерархии классов ствует только одна копия объекта виртуального 
базового класса даже в том случае, если класс этого объекта наследуется более 
одного раза. 
    В листинге 14.9 демонстрируется, как создать класс Corporation, 
использующий виртуальные базовые классы, и решить проблему размножения 
базового класса McDougles. 
    Скомпилируйте CONGLOM как Easy Win-приложение из интегрированной среды, 
так как в следующем разделе используются средства оболочки для исследования 
родства между классами в программе. Если вы компилируете программу из 
командной строки DOS, вы не сможете ими воспользоваться. 

    Листинг 14.9. CONGLOM.CPP (подключение базовых классов) 

    1:	// conglom.cpp 
    2: 
    3:	#include <iostream.h>                         
    4: 	#include <string.h>
    5:	                            
    6:	class Company {                                               
    

   Глава 14. Объектно-ориентированное программирование 363 

    7:   private: 
    8:       char *name; 
    9:   public:                                    
    10:      Company (const char *s) {              
    11:           name = strdup(s);                               
    12:          cout « " In constructor for ";
    13:          Display();	
    14:       }                         
    15:      virtual ~Company() {                      
    16:          cout « " In destructor for ";   
    17:          Display();                                           
    18:          delete name;        
    19:
    20:       void Display(void) { cout « name «  '\n';   } 
    21:    }; 
    22:                                                        
    23:   class Jennys:   public Company {  
    24:   public:                                                   
    25:       Jennys():  Company(" Jenny's") { }  // Конструуктор
    26:    };                                                               
    27: 
    28:   class McDougles:  public Company { 
    29:    public: 
    30:       McDougles(): Company ('McDougles") { } 
    31:   };	
    32: 
    33:   class BurgerQueen: public Company { 
    34:   public: 
    35:       BurgerQueenQ:  CompanyC'BurgerQueen") { }	
    36:   };                 
    37:
    38:   class Bob	
    39:       :  virtual public Jennys,	
    40:          virtual public McDougles {	
    41:   };	
    42: 
    43:   class Ted                                     
    44:       :  virtual public McDougles,	
    45:          virtual public BurgerQueen { 
    46:    }; 
    47:                                
    48 :   class Alice   
    49:       :  virtual public Jennys,      
    50:           virtual public McDougles, 
    51:          virtual public BurgerQueen {	1 
    52:   }; 
    53: 
    54:   class Corporation	
    55:       :  virtual public McDougles,     
    56:           public Ted,	
    57:          public Alice {	
    58:   private:	
    59:       char *name;	
    60:   public: 
    61:      Corporation():  McDougles(), Ted(),  Alice() 
    62:           { name = "Conglomerate Industries";   } 
    63:      void Display(void) { cout « name « '\n';   } 
    64:   }; 
    65:                                               
    66:   main()                
    67:   { 
    68:       cout « "\nForming a corporation\n";	
    69:       Corporation *cp; 
    70:       cp = new Corporation; 
    71:       cp->Display(); 
    72:
    74:    } 

    Класс Company не изменился, в то время как в Jennys, McDougles, 
BurgerQuen, Bob, Ted и Alice, конечно, потребовались изменения для 
предупреждения в будущем наследования нескольких копий их базового 
класса-родителя. Например, Bob теперь объявлен следующим образом: 

    class Bob 
    : virtual public Jennys,	
       virtual public McDougles { 
    

    364 Часть III. Программирование на ANSI C++ 

добавление ключевого слова virtual к перечисленным базовым классам 
указывает компилятору, что в последующих выводах классов, также наследующих Jennis или 
McDougies, должна существовать только одна копия этих двух базовых классов. 
Аналогично, в классах Ted и Alice (строки 43-52) базовые классы указаны виртуальными. 
    Теперь можно спроектировать класс Corporation (строки 54-64). Как и ранее, 
класс наследует базовые McDougies, Ted и Alice. Койечно, с объявлением 
McDougies виртуальным базовым классом в итоге будет существовать только одна копия 
класса Corporation, несмотря на то, что Ted и Alice также выведены из класса McDougies. 

    ЗАМЕЧАНИЕ 

    Ted и Alice также могли бы быть объявлены виртуальными базовыми классами в 
строках 56—57. Это было бы необходимо только в том случае, если бы в последующих 
выводах из класса Corporation также наследовались бы и эти  классы. 


    Использование Borland ObjectBrowser 

    К сожалению, нет ничего, кроме нескольких рисунков, что помогло бы 
разъяснить родственные взаимоотношения в множестве производных классов. Но 
если у вас установлена Microsoft Windows, вы можете использовать ObjectBrowser 
в интегрированной среде для отображения связанных классов и их членов. Вы 
сможете пользоваться им независимо от того, пишете ли вы программы для Windows 
или же для DOS. 
    ObjectBrowser прост в использовании, но доступен только в интегрированной среде. 
После компиляции CONGLOM в виде EasyWin-приложения (если у вас возникли проблемы, 
обратитесь к главе 2) следует выбрать View\Classes для просмотра графической схемы 
классов программы и их взаимосвязей (рис. 14.14). 
   Поскольку в модуле CONGLOM также используются и потоки ввода-вывода для 
отображения, на схеме показаны   ios, streambuf и другие классы из стандартных библиотек, 
поставляемых вместе с Borland C++. 
   Двойной щелчок мыши на любом классе (например, McDougies) приведет к открытию окна, 
демонстрирующего содержимое класса (рис. 14.15). Вы можете затем выбрать любую строку 
в окне для просмотра ее определения. Также попробуйте выбрать фильтры — F (функции), 
V (переменные), I (наследованные объявления ) и V (виртуальные функции) для изменения 
вида информации, отображаемой в окне. 

    Рис 14.14. Классы CONGLOM и их взаимосвязи 

    Рис. 14.15. Содержимое McDougies в окне ObjectBrowser 


    Резюме 

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

    Множественное наследование описывает родственные взаимосвязи классов, 
которые выводятся из нескольких базовых классов. 

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

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

    Производный класс наследует все имеющиеся в наличии данные- и функции-члены 
из базового класса за гключением конструкторов и деструкторов.

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


    Глава14. Объектно-ориентированное программирование 365 

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

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

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

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

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

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

    Деструкторы и функции-члены могут быть виртуальными, в отличие от 
конструкторов. C++ компонует виртуальные функции-члены и виртуальные 
деструкторы во время выполнения программы. Объявления чистых виртуальных 
функций-членов должны заканчиваться = 0. Любой класс, содержащий хотя бы одну 
чистую виртуальную функцию, называется абстрактным. Вы не можете создавать 
объекты абстрактного класса. Предполагается, что в производном классе, 
наследующем абстрактный, будут запрограммированы все чистые виртуальные 
функции-члены абстрактного класса. 

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

    Упражнения 

    14.1. Напишите ряд связанных между собой классов для программы, вводящей 
данные с экрана, наподобие базы данных, содержащей имена и адреса. Создайте 
обобщенные классы, запрашивающие и возвращающие значения различных типов 
данных в заданном участке экрана. (Цель данного упражнения — самостоятельное 
составление иерархии классов. В вашем ответе не обязательно читать и писать 
информацию в дисковые файлы.) 
    14.2. Выведите новый класс из класса TBench (листинги 14.1 и 14.2), 
выполняющий серию тестов с заданным числом повторений и сообщающий среднее 
затраченное время для всех тестов. 
    14.3. Разработайте и протестируйте класс, способный сохранять множество 
целых значений от 0 до 15, с использованием одного бита для одного значения. В 
классе должны быть функции-члены, добавляющие, удаляющие, извлекающие и 
отображающие значения в множестве. Используйте виртуальные функции-члены, 
конструкторы и деструкторы там, где это понадобится. В классе должно 
отображаться двоичное значение, подобное 0100100011001011, где 1 означает 
наличие элемента в множестве. 
    14.4. Разработайте класс, способный запомнит^ список объектов незаданного 
типа класса. 
    14.5. На основе выполненного упражнения 14.4 напишите программу, способную 
запоминать и отображать содержимое каталога диска в виде списка строковых объектов. 
    14.6. На основе классов, разработанных в ответе на упражнение 14.5, 
напишите, используя множественное наследование, новый класс, способный 
отображать содержимое каталога и запоминать строку, содержащую спецсимволы. 
Например, инициализация нового объекта-каталога строкой "*.СРР" приведет к 
отображению всех файлов с расширением .СРР, находящихся в'текущем каталоге, и 
к запоминанию самой этой строки для будущих ссылок. 
    

    366	Честь 111. Программирование на ANSI C++ 