    Передовые средства C++  367

    Друзья 
        Дружественные классы 
        Дружественные функции 
           Дружественные функции-члены 
           Перегрузка операторов 
               Перегрузка функций-операторов 
               Перегрузка операторных функций-членов 
               Перегрузка унарных операторов 
               Преобразование типов 
               Оператор индексирования массива 
              Оператор вызова функции 
              Оператор доступа к члену класса 
              Операторы инкремента и декремента 
              Перегрузка оператора new 
                 Перегрузка delete 
                Установка обработчика ошибок оператора new   
    Копирование объектов классов 
        Конструктор копии
        Вызов operator= из конструктора копии 
   Тонкости использования потоков ввода-вывода 
       Использование манипуляторов 
       Перегрузка оператора вывода в поток 
       Перегрузка оператора ввода из потока 
   Шаблоны   388
       Шаблонные функции 
       Шаблонные классы 390
   Разное 
       Указатели на функции-члены 
       Статические члены 
       Размещение объектов по заданным адресам	
       Инициализация членов объекта класса 
       Вложенные объявления в классе	
       Опция  fast this 
    Исключительные ситуации 
    Динамическая информация о типе 

    422

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

    14:	class Buddy { 
    15:	private: 
    16:	   Pal palObject;	//	Доступен только членам Buddy 
    17:	public: 
    18:	   void ShowValues(void);	//	Доступна всем пользователям 
    19:	}; 

    29:	void Buddy::Showvalues(void) 
    30:	{ 
    31:		Pal aPal(1234); вызов конструктора?
    32: 
    33:		cout « "\nBefore, palObject.x	==" « palObject.x; 
    34:		palObject.doublex( ) ; 
    35:		cout « "\nAfter, palObject.x	==" « palObject.x; 
    36:		cout « "\naPal.x == " « aPal.x « "\n'; 
    37:	} 


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

    class ВСlass;	// Дополнительное неполное объявление класса 

    class AClass { 
        friend class BClass;    // BClass имеет доступ к всем членам AClass 
        ... //  другие члены 
     }; 

    class BClass { 
       friend class AClass;    // AClass имеет доступ к всем членам BClass 
                                             // ... другие члены; 
     };


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

Show - не член класса !

    21: main() 
    22: { 
    23:    One c1; 
    24:    Two c2; 
    25: 
    26:    Show(c1, c2)	; 
    27:    return 0; 
    28: } 

    30: void Show(0ne &c1, Two &c2) 
    31: { 
    32:    cout « c1.s1 « c2.s2 « '\n'; 
    33: } 


    3:	class One;  //	Неполное объявление класса 
    4: 
    5:	class Two { 
    6:	private: 
    7:	  char *s2; //	Доступен членам класса Two 

    13:	class One { 
    14:	   friend void Two::Show(One &c1); 
    15:	private: 
    16:	   char *s1; //	Доступен в классе One и в Two:: Show 
    17;	public: 
    18:	   One() { s1 =	"Testing "; } 
    19:	}; 
    Порядок объявления двух классов изменился по сравнению с приведенным в 
предыдущем листинге на обратный, поскольку класс, в котором содержится 
прототип функции-члена, должен объявляться до класса, указывающего функцию-член 
дружественной. Для класса One, объявляющего дружественной функцию Two::Show(), 
объявление класса Two уже должно быть доступно компилятору. 
    30:	void Two::Show(One &c1) 
    31:	{ 
    32:	  cout « c1.s1 « s2 « '\n'; 
    33:	} 
    Еще одно отличие состоит в способе доступа к закрытым данным обоих классов 
в функции Show() (строки 30—33). Объявлен только один ссылочный параметр One &cl. 
Поскольку функция Show() — член класса Two, она имеет непосредственный 
доступ ко всем членам класса Two.


    Перегрузка операторов 


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

Обычно вы не имеете права использовать символы типа +, -, * или / в идентификаторах, однако 
для перегрузки операторов в C++ имя функции может состоять из слова operator и одного из символов, 
приведенных в табл. 15.1. 

Операторы ., .*, :: и sizeof не могут перегружаться вообще. 
Кроме того, операторы =, (), [] и -> следует реализовать нестатическими 
функциями-членами. (Вы ознакомитесь со статическими функциями-членами далее в 
этой главе). 


    Перегрузка функций-операторов 


    class ZZ { 
    public: 
       friend ZZ operator+(ZZ a,  ZZ b); 
       friend ZZ operator-(ZZ a,   ZZ b);
       friend ZZ operator*(ZZ a,   ZZ b);
       friend ZZ operator/(ZZ a,   ZZ b);
       // ... прочие открытые, защищенные и закрытые члены 
    };


Присваивание одного объекта другому создает несколько "скользких" моментов. Эта тема будет 
обсуждаться позднее в этой главе.
 
Обычно функции перегрузки операторов имеют тип класса (или ссылки на объект класса), объектами 
которого они оперируют, но это не обязательно. 
    35:	long operator+(TStrOp a,  TStrOp b) 
    36:	{ 
    37:	  return (atol(a,value) + atol(b.value)); 
    38: 	}
    39: 
    40:	long operator-(TStrOp a, TStrOp b) 
    41:	{ 
    42: 	 return (atol(a.value) - atol(b.value)); 
    43:	} 
    Рассмотрим реализацию перегруженных операторов в строках 35-43. Поскольку 
функции объявлены дружественными и не членами класса, их реализация идентична 
реализации обычных функций в C++. 


    Перегрузка операторных функций-членов 


    Перегруженные функции могут быть членами класса, 
    35:	long TStrOp::operator+(TStrOp b) 
    36:	{ 
    37:	    return (atol(value) + atol(b.value)); 
    38:	} 
    39: 
    40:	long TStrOp::operator-(TStrOp b) 
    41:	{ 
    42:	   return (atol(value) - atol(b.value)); 
    43:	} 

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

    long operator+(TStrOp a,  TStrOp b);      // ??? 

    Эта строка не скомпилируется, поскольку operator+() — член класса TStrOp и 
как функция-член получает указатель this, ссылающийся на объект класса. С 
учетом указателя this и аргументов а и b всего функция имеет три параметра, а 
перегруженному бинарному оператору, подобному operator+(), необходимы только 
два. Для сложения двух объектов класса TStrOp функция operator+() суммирует 
this.value и b.value (строка 37). Ей не нужен третий параметр.

		Перегрузка унарных операторов 


Например, добавьте следующее объявление в STROPS2.CPP (листинг 12.5) прямо над 
private: в строке 6: friend long operator-(TStrOp a); 
    Даже несмотря на то что в строке 13 перегружен оператор вычитания, 
конфликтов не возникнет, поскольку в новом объявлении задан один параметр. 

    long operator-(void); 

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

    long TStrOp: :operator-(void) { 

    return -atol(value);    // аналогично -atol(this->value) 

   } 


		Преобразование типов 


    Подобное использование оператора ключевого слова специально обеспечивается 
в C++ для создания новых правил преобразования типов.
    operator long() { return atol(value);  } 

		Оператор индексирования массива 


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

    13:	  int operator[](unsigned i); 

    36:	int PseudoArray::operator[](unsigned i) 
    37:	{ 
    38:	  return Getlnt(i); 
    39:	} 

    Имейте в виду, что индексы перегруженных массивов могут быть произвольными 
типами данных. Например, вы можете заменить строку 13 на: 
    int operator[](char с); 

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

    Перегрузка вызова функции operator() делает объект класса похожим на 
функцию, которую можно вызвать. Перегруженная функция operator() может 
возвращать значения заданных типов или вовсе ничего не возвращать. Кроме того, 
в ней также могут объявляться параметры. Она не может быть статическим членом 
класса. 
      TAnyClass object = 100; 
      int q = object(); // Выглядит как вызов функции! 
    Второй оператор похож на вызов функции с именем object(), но на самом деле 
выполняется оператор object.operator()();. 

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

    В ранних версиях C++ было невозможно определить отдельные перегруженные 
операции для постфиксных и префиксных операторов ++ и --. Теперь это возможно, 
как показано в классе TAnyClass: 

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

   Чтобы использовать средства управления памятью C++ для выделения 
пространства в куче для объектов, в которых перегружен оператор new, 
поставьте перед оператором двойное двоеточие. Например, в строке 
BrandNew *х =  ::new BrandNew игнорируется перегруженный в объектах класса 
BrandNew оператор new. Аналогично, ::delete служит для обращения к оператору 
delete, используемому по умолчанию.

    Установка обработчика ошибок оператора new   
    Обычно, если new не может выполнить запрос выделения памяти, оператор 
возвращает null. Для того чтобы изменить действия, предпринимаемые по 
умолчанию в этом случае, в C++ следует присвоить адрес функцции-обработчика 
ошибок указателю _new_handler, определенному следующим образом:	
    typedef void (*vfp)(void);	
    vpf _new_handler;                                                                  
    Использование typedef не обязательно, но оно облегчает чтение объявления. 
Функция-обработчик ошибок ничего   не   возвращает   и   не   имеет   аргументов,   она   
устанавливается   с   помощью   вызова   функции  set_new_handler(), прототип которой 
содержится в файле NEW.Н:	
    vfp set_new_handler(vfp);      
    Обработчик ошибок реализуется так же, как и обычная функция C++. Например, 
вы можете аварийно завершить программу с сообщением об ошибке нехватки 
памяти: 
    void memerr(void)	
    {
      fputs("\n\nOut of memory\n", stderr);    
      exit(1);	
   }
    Для того чтобы функция memerrO вызывалась в случае возникновения ошибок, 
связанных с нехваткой памяти, передайте адрес memerr функции set_new_handler() 
следующим образом:	
    set_new_handler(memerr);                                                   
    Смотри также описание set_new_haadler() в книге Освоение Borland C++. 

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

    В C++ существует два механизма для безопасного копирования и присваивания 
объектов классов, имеющих члены-указатели: почленная инициализация и почленное 
присваивание 
    Конструктор копии TAnyClass(TAnyClass &copy) имеет одинаковое имя с классом 
(TAnyClass), в котором он определен, в нем объявлена ссылка &сору на объект класса 
TAnyClass, данные из которого копируются в создаваемый объект.
    TAnyClass v1; 
    TAnyClass v2(1.  2,   "A test string"); 
    TAnyClass v3 = v2;       // Вызывается конструктор копии 
...........................................................................................................
    TAnyClass v1; 
    TAnyClass v2(1, 2, "A test string"); 
    v1 = v2;    // ??? 
    После инициализации объекта v1 значениями по умолчанию и определения v2 с 
явными аргументами оператор присваивания копирует v2 в v1. Но берегитесь! 
Присваивание не вызывает ни один из конструкторов класса, даже конструктор 
копии.
    Конструктор копии не решит этой проблемы, поскольку задействованные 
объекты уже созданы. C++ вызывает конструкторы в случае инициализации новых 
объектов, но не в случае присваивания одного объекта другому. 

    К счастью, вы можете легко разрешить вашу проблему, перегрузив оператор 
присваивания =. Следует использовать объявление вида 
      void operator=(const CLASS&). 
Например, для перегрузки оператора присваивания в классе TAnyClass из 
предыдущего раздела модифицируйте его следующим образом: 
    class TAnyClass { 
    private: 
    int i;	II Закрытый член класса 
    double r; // Закрытый член класса 
    char *s; // Закрытый член-указатель! 
    public: 
       ......    // См. предыдущее объявление класса TAnyClass 
       void operator=(const TAnyClass &copy);    // Оператор присваивания 
    Объявление operator= перегружает оператор присваивания, задавая 
константный ссылочный параметр сору типа TAnyClass. Параметр ссылается на 
копируемый объект. Другими словами, если а и b — объекты класса TAnyClass, в 
присваивании вида а = b параметр сору ссылается на b. Функция перегрузки 
оператора присваивания реализуется следующим образом: 
    void TAnyClass::operator=(const TAnyClass Acopy) 
    {
       cout « "\nlnside TAnyClass's operator* function; 
       if (this == &copy) 
         return; 
      delete s; 
      i = copy, i; 
      r = copy, r; 
      s = strdup(copy. s); 
   } 
. (Удаление нулевого указателя не является ошибкой, следовательно, отсутствует 
необходимость в проверке указателя s на равенство нулю перед применением оператора 
delete.) 
    Как и ранее, оператор вывода — только лишь для отладки. В первом операторе 
if сравниваются скрытый указатель this и адрес ссылочного параметра сору. Это 
необходимо для предотвращения попытки присваивания объекта самому себе. 
Например, при попытке выполнить операторы 
    TAnyClass v1, v2; 
     v1 = v1; 
перегруженный оператор присваивания просто завершит свою работу без 
выполнения каких-либо действий. 
    Вы можете легко устранить излишнее дублирование кода в конструкторе копии 
и перегруженном операторе присваивания. Внутри конструктора копии следует 
просто вызвать функцию перегрузки operator= присваиванием копируемого объекта 
объекту *this: 

    Подключение заголовочного файла IOMANIP.H (в котором при необходимости 
включается заголовочный файл IOSTREAM.H) делает возможным использование 
нескольких манипуляторов, полезных для создания форматированного вывода и 
чтения форматированного ввода. Например, вы можете использовать манипулятор 
    cout « "Enter value in hex: "; 
    cin » hex » v;	
    cout « "Value in decimal == " « dec « v;	
    Модификатор width недолговечен, он оказывает влияние только на следующее 
выводимое значение.

    cout « р1 « ";   " « р2 « ";   " « рЗ; 
    На самом деле зги операторы выполняются путем обращения к нескольким 
функциям, реализующим перегрузку оператора вывода в поток. Предыдущая строка 
выполняется подобно написанной следующим образом: 
    ((((cout « р1) « ";   ") « р2) « ";   ") « рЗ; 
    Разве вы не радуетесь тому, что вам не надо вводить все эти скобки? 


    Вы можете ссылаться на функции-члены класса с помощью указателей, но не 
так, как вы ссылаетесь на обычные функции (см. главу 9). Функции-члены 
вызываются для объектов класса, и они получают скрытый указатель this. Поэтому 
адресация функций-членов с помощью указателей требует нового подхода. 
    double (TFirstClass::*myfnptr)(void); 
    23:	myfnptr = &TFirstClass::Access; 
    25:	cout « setw(8) « dec « (fc.*myfnptr)() ; 
    В дополнение к адресации функций-членов указателями, можно также ссылаться 
на другие открытые члены класса.
    double TFirstClass::*dataPtr; 
    Указатель dataPtr объявляется для адресации любых открытых данных-членов в 
классе TFirstClass, имеющих тип вещественных значений двойной точности. 
    dataPtr = &TFirstClass::balance ;	
или
   double TFirstClass::*dataPtr = &TFirstClass::balance;	

   Статические функции-члены не могут быть виртуальными.
    Статические функции-члены могут выполнять любые операторы, однако они не 
получают указатель this и не имеют доступа ни к каким данным или функциям-членам 
класса. Для вызова статической функции-члена в программе используется не объект, 
а имя класса: 
    TAnyClass::GlobalInit();	// Вызвать статическую функцию Globallnit() 
член с объявляется закрытым статическим членом класса. (Статические 
данные-члены также могут быть защищенными и открытыми.) Существует лишь одна 
копия TAnyClass::с независимо от того, сколько определяется объектов класса 
TAnyClass в программе. Статические данные-члены должны определяться и 
инициализироваться в программе, обычно с помощью глобальных объявлений, 
подобных следующему: 
    char TAnyClass::с = 'q'; 

    Размещение объектов по заданным адресам