    Глава 15 

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

    Научиться программировать на компьютере — все равно, что научиться ходить. 
Как только вы сумеете самостоятельно стоять на ногах, начнется настоящая 
работа — рассчитать, как добраться из пункта А в пункт В. 
    С теми знаниями о возможностях ANSI С и C++, которыми вы уже обладаете, вы 
вполне способны задумать и реализовать приложение изрядного размера. Но не 
заканчивайте на этом ваше обучение. В этой главе вы ознакомитесь с передовыми 
средствами C++, такими, как перегрузка операторов, потоковые классы, шаблоны и 
многое другое, что может оказаться неоценимым в ваших программистских 
скитаниях. Вы также узнаете о новейших средствах Borland C++ версии 4.0 — 
исключительных ситуациях, получении динамической информации о типе и новом 
строковом классе string ANSI C++. 

    Друзья 

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

    ЗАМЕЧАНИЕ 

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

    Дружественные классы 

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

    class AClass { 
    private: 
       double value;         // Закрытый член класса 
    public: 
       AClass() { value = 3.14159; } 
    }; 

    Класс AClass содержит один закрытый член value типа double. Конструктор 
класса присваивает этому члену значение 3.14159. За исключением этого 
действия, в классе не предусмотрены даже средства просмотра значения value 
объекта класса. Закрытый член находится в полной безопасности, как медвежонок 
под защитой мамаши-медведицы. 
    Далее, предположим, что вы объявили еще один класс, содержащий членом 
объект класса AClass: 

    class BClass { 
    private: 
         AClass anObject;	// член объекта класса AClass public: 
          void ShowValue(void) 
          { cout « anObject.value;   }	// ??? }; 

    Член anObject типа AClass закрыт в классе BClass. Функция-член ShowValue()
пытается отобразить value из anObject. Однако объявление не скомпилируется, 
поскольку член value закрыт в классе AClass и только функции-члены AClass 
имеют к нему доступ. 
    Замена в классе AClass спецификатора доступа private на protected для 
члена value не решает проблему. Защищенный член доступен в классе, в котором 
он объявлен, и в любых производных из него классах. Несмотря на то что класс 
BClass обладает объектом типа AClass, два этих класса не связаны между собой 
отношением родства, и члены BClass не имеют доступа к закрытым и защищенным 
членам AClass. Конечно, вы можете сделать член value открытым в классе AClass, 
но это сделает value доступным и во всех операторах. 
    Лучшее решение — сделать класс BClass другом класса AClass. Объекты BClass 
будут иметь доступ к value и всем другим закрытым и защищенным членам, а 
операторы вне двух классов будут лишены возможности доступа к запрещенным 
частям класса. Для этого следует указать ключевое слово friend внутри класса, 
к которому необходимо получить доступ в другом классе. В данном примере класс 
Вclacc получит доступ к закрытому члену value внутри AClass. Таким образом, для 
того, чтобы класс BClass получил доступ к этим закрытым данным в классе 
AClass, следует первый класс объявить дружественным второму. Вот новое 
объявление класса AClass: 

    class AClass { 
        friend class BClass;     // Класс BClass - друг класса AClass 
   private: 
       double value;                // Доступен членам классов AClass и BClass!!! 
   public: 
       AClass() { value = 3.14159; } ;	
   }

    Единственное отличие от предыдущего объявления — строка friend class 
BClass, дающая указание компилятору предоставить классу BClass доступ к 
защищенным и закрытым членам класса AClass. Другие операторы в других классах 
и в основной программе, как и раньше, не имеют доступа к объявленным закрытыми 
и защищенными членам класса AClass. Только BClass может частным образом 
проникнуть в закрытую "комнату" AClass. Таким образом, вы можете объявить 
любое число классов друзьями. Единственное ограничение — ключевое слово friend 
должно быть внутри объявления класса. Полезно запомнить еще некоторые факты.
    •   В классе должны быть перечислены все его друзья. 
    •   Класс, в котором содержатся закрытые и защищенные данные и в котором 
другой класс объявлен другом, дает этому другу специальный доступ к обычно 
скрытым членам объявленного класса. Класс никогда не может объявить сам себя 
другом другого класса. 


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

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

    Листинг 15.1.  FRIEND.CPP (использование дружественных классов) 

    1:	#include <iostream.n> 
    2: 
    3:	class Pal { 
    4:	   friend class Buddy;	//	Buddy - друг класса Pal 
    5:	private: 
    6:	   int x;	//	Доступен членам Pal и Buddy 
    7:	protected: 
    8:	   void doublex(void) { x *= x; }	//	Доступен в Pal и Buddy 
    9:	public: 
    10:	   Pal() { x = 100; }	//	Доступна всем пользователям 
    11:	   Pal(int n) { x = n; }	// 
    12:	}; 
    13: 
    14:	class Buddy { 
    15:	private: 
    16:	   Pal palObject;	//	Доступен только членам Buddy 
    17:	public: 
    18:	   void ShowValues(void);	//	Доступна всем пользователям 
    19:	}; 
    20: 
    21:	main() 
    22:	{ 
    23:		Buddy aSuddy; 
    24: 
    25:		aSuddy.ShowValues(); 
    26:		return 0; 
    27:	} 
    28: 
    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:	} 

    Модуль FRIEND начинается с объявления класса с именем Pal. В строке 4 
указывается, что второй класс Buddy  — друг класса Pal. Благодаря этой строке 
функции-члены класса Buddy имеют доступ к закрытым и защищенным членам 
объектов класса Pal. Конечно, класс Pal не имеет доступа к закрытым и 
защищенным членам Buddy. 
    В строке 16 в классе Buddy объявляется объект класса Pal. Запустив на 
выполнение код, вы можете убедиться что, хотя класс Buddy не состоит в родстве с 
Pal, функция-член ShowValues()имеет непосредственный доступ к закрытому члену х 
и к защищенной функции-члену doublex() (строки 29-37) класса Pal. 

    Взаимно дружественные классы 

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


    15. Передовые средства C++     369 137 

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

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

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

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

    Дружественные функции 
    
    Дружественные функции похожи (однако менее обременительны) на 
дружественные классы. Объявление функции другом класса открывает ей доступ к 
закрытым и защищенным членам объекта класса. Дружественные функции могут быть 
обычными функциями C++ или членами класса. 
    Обычно дружественная функция объявляется с параметрами-классами, с 
которыми она "водит" дружбу. Внутри дружественной функции операторы имеют 
доступ к обычно скрытым членам объекта класса, переданного аргументом функции. 
    В листинге 15.2 демонстрируется объявление и использование типичной 
дружественной двум классам функции. 
    В FRIENDFN объявляются два класса, One и Two. Неполное объявление класса в 
строке 3 позволяет ссылаться в One на Two. В строках 6 и 14 объявляется 
дружественная функция с именем Show(). Поскольку в каждом классе функция ShowO 
объявляется дружественной, операторам Show() разрешается доступ к закрытым и 
защищенным членам классов One и Two. (Классы, приведенные здесь, не имеют 
закрытых секций, но даже если бы они их имели, дружественная функция все равно 
имела бы доступ к членам этих секций.) 

    Листинг 15.2.   FRIENDFN.CPP (использование дружественных функций) 

    1: #include <iostream.h> 
    2: 
    3: class Two;	// Неполное объявление класса 
    4: 
    5: class One { 
    6:     friend void Show(0ne &c1, Two &c2); 
    7: private: 
    8:    char *s1;	// Доступен классу One и функции Show() 
    9: public: 
    10:  One() { s1 =	"Testing "; } 
    11: }; 
    12: 
    13: class Two { 
    14:     friend void	Show(0ne &c1, Two &c2); 
    15: private: 
    16:     char *s2;	// Доступен классу Two и функции Show() 
    17: public: 
    18:    Two() { s2 =	"one, two, three"; } 
    19: }; 
    20: 
    21: main() 
    22: { 
    23:  One c1; 
    24:  Two c2; 
    25: 
    26:  Show(c1, c2)	; 
    27:  return 0; 
    28: } 
    29: 
    30: void Show(0ne &c1, Two &c2) 
    31: { 
    32:    cout « c1.s1 « c2.s2 « '\n'; 
    33: } 


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

    Функция Show() объявлена с двумя ссылочными параметрами cl и с2 двух типов 
классов. Поскольку функция Show() дружественна обоим классам, операторы внутри 
Show() имеют доступ к защищенным и закрытым членам объектов классов-аргументов, 
переданных функции. 
    Например, в реализации функции Show() (строки 30-33) отображаются значения 
строк, адресованных указателями si и s2. Поскольку функция Show() дружественна 
обоим классам, она может ссылаться непосредственно на данные-члены s1 и s2, 
закрытые в своих классах. Другие "недружественные" функции не имеют такого 
доступа к скрытым членам этих классов. 

    Дружественные функции-члены 

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

    Листинг 15.3.  FRIEfJDMF.CPP (использование дружественных функций-членов) 

    1:	#include <iostream.h> 
    2: 
    3:	class One;  //	Неполное объявление класса 
    4: 
    5:	class Two { 
    6:	private: 
    7:	  char *s2; //	Доступен членам класса Two 
    8:	public: 
    9:	  Two() { s2 = "one, two, three"; } 
    10:	  void Show(0ne &c1); 
    11:	}; 
    12: 
    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:	}; 
    20: 
    21:	main() 
    22:	{ 
    23:		One c1; 
    24:		Two c2; 
    25: 
    26:		c2.Show(c1); 
    27:		return 0; 
    28:	} 
    29: 
    30:	void Two::Show(One &c1) 
    31:	{ 
    32:	  cout « c1.s1 « s2 « '\n'; 
    33:	} 

    Как и в модуле FRIENDFN (листинг 15.2), в новой программе FRIENDMF 
объявляется два класса, One и Two. В классе One объявляется аналогичная 
дружественная функция-член (строка 14) с помощью имени класса и оператора 
разрешения области видимости (Two::), чтобы указать компилятору 
местонахождение функции Show(). 
    Порядок объявления двух классов изменился по сравнению с приведенным в 
предыдущем листинге на обратный, поскольку класс, в котором содержится 
прототип функции-члена, должен объявляться до класса, указывающего функцию-член 
дружественной. Для класса One, объявляющего дружественной функцию Two::Show(), 
объявление класса Two уже должно быть доступно компилятору. 
    Еще одно отличие состоит в способе доступа к закрытым данным обоих классов 
в функции Show() (строки 30—33). Объявлен только один ссылочный параметр One &cl. 
Поскольку функция Show() — член класса Two, она имеет непосредственный 
доступ ко всем членам класса Two. Выражение c1.s1 в операторе вывода в поток 
в строке 32 не является недозволенным, так как функция Show() дружественна классу 
One, которому принадлежит закрытый член s1. Функция-член Show() может ссылаться 
на s2 непосредственно, поскольку этот член принадлежит классу Two, в котором 
объявлена Show(). 
    Поскольку функция Show() — член класса Two, в ней определен указатель this, 
ссылающийся на объект класса, для которого была вызвана функция-член. Поэтому в 
программе должен объявляться объект класса Two (строка 24), затем вызываться 
функция Show() для этого объекта (строка 26). 


    15. Передовые средства C++     371 

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

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

    TAnyClass c1,  с2,  сЗ; 

    Вы можете использовать эти объекты в выражениях вида:	

    сЗ = с1 + с2; 

    Для лучшего понимания сути перегрузки операторов полезно провести ревизию 
того, что вам известно об операторах вообще. Конечно, обычный оператор знак 
плюс (+) суммирует два значения. Знак минус (-) осуществляет операцию 
вычитания. Эти и другие символы называются бинарными операторами, поскольку им 
необходимы два аргумента. Другие, например оператор отрицания (!), — унарные, 
им требуется только один аргумент. Еще один пример — унарный минус (-). 
Выражение -count меняет знак значения count. 
    Перегрузка операторов дает возможность добавлять к встроенным типам 
данных, используемых операторами С++, новые типы. Перегрузка операторов 
объявляется так же, как и обычная дружественная функция или функция-член 
класса. В следующем разделе объясняется, как писать и использовать 
перегруженные унарные и бинарные функции-члены операторов. 

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

    Простой пример иллюстрирует перегрузку операторов для гипотетического 
класса с именем ZZ: 

    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);
       // ... прочие открытые, защищенные и закрытые члены 
    };
 
    В классе ZZ объявлены четыре перегруженные функции-оператора. (На практике 
в ZZ могут объявляться и другие открытые, защищенные и закрытые члены.) Имена 
функций — operator+(), operator-(), operator*() и operator/(). Обычно вы не 
имеете права использовать символы типа +, -, * или / в идентификаторах, однако 
для перегрузки операторов в C++ имя функции может состоять из слова operator и 
одного из символов, приведенных в табл. 15.1. 

    Таблица 15.1. Список операторов, которые могут быть перегружены 

    /	+	-	%	~	&	| 
    1	,	=	<	>	<-	>= 
    •H-	«	»	==	! =	&&	|| 
    /=	%=	"=	&=	!=	•»•=	-= 
    
    «=	»=	->	->*	[]	()	new	delete 

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

    friend ZZ operator-(ZZ a, ZZ b); 

    Функция объявляется дружественной классу, что дает функции operator+()
доступ к закрытым и защищенным членам этого класса. Функция возвращает объекты 
типа ZZ (но может возвращать и любой другой тип), и этот результат можно 
присвоить другому объекту ZZ. (Присваивание одного объекта другому создает 
несколько "скользких" моментов. Эта тема будет обсуждаться позднее в этой 
главе.) Более важно то, что имя функции.operator+() идентифицирует функцию как 
метод, с помощью которого выражения, использующие оператор сложения, могут 
оперировать объектами класса. Предполагается, что функция operator+()
складывает два объекта способом, подходящим для класса ZZ. 


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

    ЗАМЕЧАНИЕ 

    Компилятор не навязывает математических концепций перегруженным 
операторам. Для ясности вы могла бы определить операции, подобные сложению, в 
виде перегруженного оператора сложения; действия, подобные умножению  — в 
виде перегруженного оператора умножения и т.д. Если вы не стремитесь следовать 
традициям (например,  определяя знак плюс,  который умножает вместо сложения), 
результаты будут обескураживающими. 

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

    feeblewitz(a, b); 

    Здесь а и Ь — объекты типа ZZ. Заменив feeblewitz на operatof+, вы сможете 
вызывать функцию перегрузки оператора следующим образом: 

    operator+(a, d);	// Аналогично feeblewitz(a, b) 

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

    а + Ь;	// Аналогично operator+(a,  b),1 

    Выражения а + b и operator+(a, b) выполняют одну и ту же работу и в 
точности генерируют один и тот же код. Есть только одна причина использовать 
ту или иную форму — ясность. Перегрузка операторов не привносит в C++ ничего 
такого, чего бы вы уже не знали. Это всего лишь позволит вам использовать 
объекты в выражениях, вместо того чтобы передавать их функции. 
    Работающий пример класса, в котором используются перегруженные операторы, 
поможет проиллюстрировать эти концепции. В листинге 15.4 содержится класс, 
способный запоминать целые значения в виде строк. С помощью перегруженных 
операторов программа может вычислять выражения, в которых складываются и 
вычитаются строки в виде эквивалентных им числовых значений. 

    Листинг 15.4.  STROPS.CPP (функции перегруженных операторов) 

    1:	#include <iostream.h> 
    2:	#include <stdlib.h> 
    3:	#include <string.h> 
    4: 
    5:	class TStrOp { 
    6:	private: 
    7:	   char value[12]; 
    8:	public: 
    9:	   TStrOp() { value[0] = 0; } 
    10:	   TStrOp(const char *s); 
    11:	   long GetValue(void) { return atol(value);  > 
    12:	   friend long operator+(TStrOp a,  TStrOp b); 
    13:	   friend long operator-(TStrOp a,  TStrOp b); 
    14:	}; 
    15: 
    16:	main() 
    17:	{
    18:		TStrOp a = "1234"; 
    19:		TStrOp b = "4321";
    20: 
    21:		cout « "\nValue of a == " « a.GetValue(); 
    22:		cout « "\nValue of b == " « b.GetValue(); 
    23:		cout « "\na + b + 6 == " « (a + b + 6); 
    24:		cout « "\na - b + 10 == " « (a + b + 10) 
    25:		« "\n" ; 
    26:		return 0; 
    27:	} 
    28: 
    29:	TStrOp::TStrOp(const char *s) 
    30:	{ 
    31:	  strncpy(value, s, 11): 
    32:	  value[11] = 0; 
    33:	}
    34: 
    35:	long operator+(TStrOp a,  TStrOp b) 
    36:	{ 
    37:	  return (atol(a,value) + atol(b.value)); 


    Гпава 15. Передовые средства C++     373 

    38: }
    39: 
    40:	long operator-(TStrOp a, TStrOp b) 
    41:	{ 
    42:	return (atol(a.value) - atol(b.value)); 
    43:	} 

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

    Value of a == 1234 
    Value of b == 4321 
    a + b + 6 == 5561 
    a - b + 10 == -3077 

    Перед тем как ознакомиться с классом и его функциями перегрузки операторов 
в программе, рассмотрим функцию main(). В строках 18-19 объявляются два 
объекта типа TStrOp — а и b. Объекты инициализируются символьными строками и 
могут быть реализованы также следующим образом: 

	TStrOp a("1234"); 
	TStrOp b("4321"); 

    Несмотря на то что со всей очевидностью эти объекты представляют собой 
строковые значения, выражения в строках 23 и 24 складывают и вычитают их 
непосредственным образом: 

    (а + b + 6) 
    (а - b + 10) 

    Обычно вы не можете использовать строки или объекты в подобных выражениях; 
перегрузка операторов расширяет знания C++ о том, объекты каких типов можно 
складывать и вычитать. 
    Две дружественные функции, объявленные в строках 12-13, перегружают 
операторы сложения и вычитания для пары объектов класса TStrOp. Объявления 
сходны с теми, что пояснялись ранее в гипотетическом классе ZZ. Конечно, в 
данном случае функция возвращает длинное целое. Обычно функции перегрузки 
операторов имеют тип класса (или ссылки на объект класса), объектами которого 
они оперируют, но это не обязательно. Функции перегрузки операторов, равно как 
и другие функции, могут возвращать любые типы данных. 
    Рассмотрим реализацию перегруженных операторов в строках 35-43. Поскольку 
функции объявлены дружественными и не членами класса, их реализация идентична 
реализации обычных функций в C++. Единственное отличие заключается в 
специальных именах функций — operator+ и operator-, что позволяет компилятору 
вычислять выражения, использующие операторы сложения и вычитания с объектами 
класса TStrOp. 
    Поскольку функции операторов — друзья, они имеют доступ к закрытым и 
защищенным членам объектов TStrOp. Этот факт позволяет функциям 
преобразовывать строки-значения в параметрах а и b в длинное целое. Это 
преобразование осуществляется путем обращения к стандартной функции atol 
(ASCII to long), прототип которой содержится в заголовочном файле STDLIB.H. 
Функции просто возвращают сумму или разницу преобразованных значений. 

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

    Перегруженные функции могут быть членами класса, как это демонстрируется в 
листинге 15.5. Эта программа похожа на STROPS.CPP (листинг 15.4), однако в ней 
иллюстрируется перегрузка операторов функциями-членами.

    Листинг 15.5.  STROPS2.CPP (перегрузка операторов функциями-членами) 

    1:	#include <iostream.h> 
    2:	#include <stdlib.h> 
    3: 
    4:	#include <string.h>	
    5:	class TStrOp { 
    6:	private: 
    7:	   char value[12]; 
    8:	public: 
    9:	   TStrOp ()  { value[0] = 0;   } 
    10:	   TStrOp(const char *s); 
    11:	    long GetValue(void) { return atol(value);   } 
    12:	    long operator+(TStrOp b); 
    13:	    long operator-(TStrOp b); 
    14:	}; 
    15: 
    16:	main() 
    17:	{ 
    18:	  TStrOp a = "1234"; 
    19:	  TStrOp b = "4321"; 


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

    21:	cout « "\nValue of a == " « a.GetValueO; 
    22:	cout « "\nValue of b == " « b.GetValueO; 
    23:	cout « "\na + b +   6 == " « (a + b + 6); 
    24:	cout « "\na - b + 10 == " « (a - b + 10) 
    25:	« '\n'; 
    26:	return 0; 
    27:	} 
    28: 
    29:	TStrOp::TStrOp(const char *s) 
    30:	{ 
    31:	  strncpy(value, s, 11); 
    32:	   value[11] = 0; 
    33:	} 
    34: 
    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:	} 

    Рассмотрим перегруженные операторные функции-члены, объявленные в строках 
12-13 и реализованные в строках 32-33. Поскольку эти функции — члены класса 
TStrOp, они уже имеют доступ к защищенным и закрытым членам класса, таким 
образом, не надо их указывать друзьями класса. Кроме того, эти функции-члены 
получают указатель this, ссылающийся на объект, для которого они были вызваны. 
Следовательно, функциям необходим всего лишь один параметр, а не два, как 
ранее. Для сложения двух строковых значений operator+() суммирует длинные 
целые значения, эквивалентные выражениям this->value и Ь.value. 
    Функциям-операторам и функциям-членам необходимо соответствующее число 
параметров. Вы не сможете объявить operator+() в строке 12 следующим образом: 

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

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

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

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

    long operator-(TStrOp a) 
    {
      return -atol(a.value); 
    }

     Поскольку унарный оператор "минус" — друг класса, оператор return имеет 
непосредственный доступ к закрытому члену параметра а, имеющему тип класса 
TStrOp. Теперь функция закончена, и компилятор может вычислить выражения, 
использующие унарный минус и объекты этого типа класса. Например, добавьте 
следующий оператор в функцию main() (лучше всего после строки 20): 

    cout "\п-а   == " « -а ; 


    15. Передовые средства C++     375 

    Скомпилируйте и запустите на выполнение модифицированную программу. Как вы 
видите, выражение заменено на операцию отрицания длинного целого представления 
строкового члена а. Вы научили C++ вьполнять операцию отрицания над длинными 
целыми значениями, представленными в строковой форме! 
    Как и в случае с перегруженными бинарными операторами, вы можете объявить 
перегруженные унарные операторы функциями-членами. Беря за основу первоначальную 
копию STROPS2.CPP, добавьте следующее объявление над строкой 14 (перед 
закрывающей скобкой класса TStrOp): 

    long operator-(void); 

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

    long TStrOp::operator-(void) { 

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

   } 


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

    cout « "\n-a    == " « -а; 

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

    Вы можете определить свои собственные правила преобразования типов данных 
для автоматического преобразования объектов в другие типы. Например, 
предположим, вы объявили и инициализировали объекты класса TStrOp из 
STROPS2.CPP (листинг 15.5) следующим образом: 

    TStrOp myValue = "9876"; 

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

    long x = myValue;         // ???	

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

    operator long() { return atol(value);  } 

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

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

    Вы можете перегрузить оператор индексирования массива [] для реализации 
доступа к данным-членам класса, подобного доступу к элементам массива, даже 
если эти данные имеют вид отдельных членов или списка. В простом примере 
(листинг 15.6) демонстрируется перегрузка оператора [] для класса, запоминающе 
четыре целочисленных значения в виде отдельных данных-членов. 
    Несмотря на простоту, класс PseudoArray содержит все необходимые 
компоненты для перегрузки оператора индексирования массива. В классе 
объявляются четыре закрытых целочисленных члена в строках 5-8. В строке 13 
объявляется функция-член operator[] с одним беззнаковым параметром. Параметр 
служит индексом в массиве и может иметь произвольный тип. 

    Листинг 15.6. SSOP.CPP (перегрузка оператора индексирования) 

    1:   #include <iostream.h> 
    2: 
    3:   class PseudoArray { 


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

    4:	private: 
    5:	  int value0; 
    6:	  int value1; 
    7:	  int value2; 
    8:	  int valueS; 
    9:	public: 
    10:	  PseudoArray(int v0, int v1, int v2, int v3) 
    11:	  { value0 = v0; value1 = v1; value2 = v2; values = v3; } 
    12:	  int Getlnt(unsigned i); 
    13:	  int operator[](unsigned i); 
    14:	}; 
    15: 
    16:	main()
    17:	{ 
    18:	  PseudoArray pa{10, 20, 30, 40); 
    19: 
    20:	  for (int i = 0; i <= 3; i++) 
    21:	   cout « "pa[" « i « "] == " « pa[i] « '\n'; 
    22:	  return 0; 
    23:	} 
    24: 
    25:	int PseudoArray::Getlnt(unsigned i) 
    26:	{ 
    27:	 switch (i) { 
    28:	  case 0: return value0; // Обратите внимание: break не нужен 
    29:	  case 1: return value1; // " 
    30:	  case 2: return value2; // 
    31:	  case 3: return value3; // " 
    32;	  default: return value0; 
    33:	  } 
    34:	} 
    35: 
    36:	int PseudoArray::operator[](unsigned i) 
    37:	{ 
    38:	  return Getlnt(i); 
    39:	} 

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

    PseudoArray pa(10, 20, 30, 40); 
    cout « "Value #2 == " « pa.Getlnt(1);	// Вызов функции-члена 

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

    cout « "Value #2 == " « ра[1];    // pa используется в качестве массива 

    В строке 21 приводится еще один пример. Выражения ра[1] и ра[2], 
оказывается, обеспечивают доступ к массиву несмотря на то, что указатель ра в 
действительности ссылается на объект класса. Перегрузка оператора индексации 
дает возможность использовать в массиве индексацию (удобный и ясный способ 
доступа к нескольким значениям) вместо вызова функции-члена для объекта класса.     
    Имейте в виду, что индексы перегруженных массивов могут быть произвольными 
типами данных. Например, вы можете заменить строку 13 на: 

    int operator[](char с); 

но затем использовать символьные индексы, подобные символам от 'А' до 'D', 
для индексации массива с начальным индексом 'А', в отличие от обычных массивов 
в С и C++, в которых используются целочисленные индексные  значения, начинающиеся 
с нуля. По сути, перегружая [] и используя символы в качестве индексов, вы устанавливаете 
соответствие между множеством символов и множеством значений, создавая тем самым 
так называемый ассоциативный массив. Вы можете реализовать функцию-член 
перегрузки оператора [] следуюobv образом: 

    int PseudoArray::operator[](char с) 
   {
    return Getlnt(c - 'A'); 
    }

    Затем вы можете использовать символьную переменную в качестве индекса 
массива. Например, после внесения предыдущих изменений замените строки 20-21 
на следующие: 

    for( char с = 'А';  с<='D'; с++)
       cout  « "ра[" « с « "] == " « ра[с] «'\п'; 


    Глава 15. Передовые средства C++     377 

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

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

    class TAnyClass { 
       int x; 
    public: 
        int operator()(void); 
       TAnyClass(int n) { x = n; } 
    }; 

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

    int TAnyClass::operator()(void) { 
       return x; 
    }	

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

    main() 
    { 
      TAnyClass object = 100; 
      int q = object(); // Выглядит как вызов функции! 
      cout « q; 
      return 0; 
   } 

    Второй оператор похож на вызов функции с именем object(), но на самом деле 
выполняется оператор object.operator()();. 

    Оператор доступа к члену класса 

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

    class TAnyClass { 
       int x, у; 
     public: 
       TAnyClass(int xx, int yy) { x = xx; у = yy; } 
       TAnyClass* operator->();	
       int GetX(void) { return x; }	
       int GetY(void) { return y; } 

    Функция-член operator->() должна возвращать объект, ссылку или указатель 
на объект класса. В данном случае она возвращает указатель на класс TAnyClass. 
Оператор реализуется следующим образом: 

    TAnyClass* TAnyClass::operator->() { 
      cout « "\nAccessing member: ";	
      return this; 
    }
 
    В данном примере демонстрируется использование перегрузки оператора -> в 
качестве отладочного трюка. Перегруженный оператор выводит короткое сообщение 
перед тем, как вернуть указатель this, ссылающийся на объект, для которого 
была вызвана функция-член. В основной программе перегруженный оператор может 
применяться для объектов класса TAnyClass следующим образом: 

     main() 
    {	.                                            
       TAnyClass t(100, 200); 
        cout « t->GetX() « '\л': cout « t->GetY() « '\n , 
       return 0; 
    }	


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

    В двух операторах вывода в поток используется перегруженный оператор -> 
для доступа к функциям-членам GetX() и GetY() объекта t класса TAnyClass. На 
самом деле, эти операторы выполняются так, как приведенные ниже: 

    cout « (t.operator->())->GetX() « '\n'; cout « (t.operator->())->GetY() « '\n'; 

    Поэтому программа отобразит метки перед значениями, возвращаемыми 
функциями GetX() и GetY(): 

    Accessing member: 100 
    Accessing member: 200 

    Операторы инкремента и декремента 

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

    class TAnyClass { 
       int x; 
    public: 
       TAnyClass(int xx) { x = xx;   } 
       int operator++() { return ++x;  }	// Префиксный ++object 
       int operator++(int) { return x++;  )   	// Постфиксный object++ 
       int operator--() {  return --x;   }                // Префиксный --object 
       int operator--(int) { return х--;  }	// Постфиксный object--
       int GetX(void) {  return x;  } 
    }; 

    Функция-член operator++() определяет префиксный оператор инкремента для 
объекта типа TAnyClass; эта функция не имеет параметров. Функция-член 
operator++(int) определяет постфиксный оператор инкремента для объекта типа 
TAnyClass; C++ присваивает нуль единственному целочисленному параметру. 
    Демонстрационные функции реализуются встраиваемыми, однако они могут 
реализоваться и отдельно. Для объекта v типа TAnyClass выражение ++v вызовет 
перегруженный префиксный оператор ++. В действительности выполняется оператор 
x.operator++();. Выражение v++ вызовет перегруженный постфиксный оператор ++ и 
выполнится, как x.operator++(0);. Операторы декремента работают аналогичным образом. 
    Поэкспериментируйте с объявленным выше классом, добавив к нему программу 
на C++, где функция main() реализована следующим образом:	

    main()
   { 
      TAnyClass t(100); 
      cout « "t == " « t.GetX() « "; ++t == " « ++t « '\n'; 
      cout « "t == " « t.GetX() « "; t++ == " « t++ « '\n'; 
      cout « "t == " « t.GetxO « ";   --t == " « --t « '\n'; 
      cout « "t == " « t.GetxO « ";   t-- == " « t-- « '\n';
  } 


    Другие вопросы, касающиеся перегрузки операторов 

    Получив некоторое представление о перегрузке операторов, необходимо иметь 
в виду следующие факты. 
    •  C++ "не понимает" семантики перегруженного оператора. Вы должны 
обеспечить осмысленные функции перегрузки операторов. 
    •   C++ не может выводить сложные операторы из простых. В классе, в 
котором объявлены функции перегрузки операторов operator*() и operator=(), C++ 
не сможет вычислить выражение а *= b до тех пор, пока вы не перегрузите 
operator *=().
   •  Никогда не изменяйте синтаксис перегруженных операторов. Бинарные операторы 
должны оставаться бинарными, унарные — унарными. Например, нельзя создать 
унарное деление, поскольку такой встроенной возможности в C++ не существует. 
   •  Нельзя изобретать новые операторы. Вы можете перегружать только операторы, 
перечисленные в табл. 15.1. Нельзя перегружать символы препроцессора # и ##. 

    Перегрузка и управление памятью 

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


    Глава 15. Передовые средства C++     379 

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

    ПРЕДУПРЕЖДЕНИЕ 

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

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

    Вы можете перегружать new точно так же, как и операторы, подобные + и =. 
Перегрузка new в объявлении класса указывает компилятору, что отныне на вас 
лежит ответственность за поддержку запросов выделения памяти для объектов 
этого класса. 
    Для перегрузки new следует использовать прототип функции вида 

        void * operator new(size_t size);. 

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

    ЗАМЕЧАНИЕ 

    В строке 6 OVERNEW.CPP приводится команда #pragma warn -aus. Она  
предписывает компилятору выключить предупреждение о том, что в программе не 
используются переменные, объявленные исключительно в демонстрационных целях.

    Функция перегрузки оператора new, объявленная в строке 13 и реализованная 
в строках 38-48, проверяет наличие пространства в глобальном буфере. Если его 
нет, функция возвращает 0, что является основанием для возвращения оператором 
new значения null. (В программе не проверяется это условие, но, конечно, вы 
должны проверять его в реальных приложениях.) Если свободное пространство 
есть, глобальный индекс увеличивается на размер запрашиваемой памяти, который 
передается функции в параметре size. Затем функция возвращает адрес 
выделяемого участка памяти. 

   Листинг 15 7. OVERNEW.CPP (перегрузка new) 

    1:	«include <iostream.h> 
    2: 
    3:	// Следующая директива "pragma" выключает предупреждение компилятора о 
    4:	// том, что в программе объявляются и не используются демонстрационные переменные 
    5: 
    6:	#pragma warn -aus
    7:	
    8:	class BrandNew {            
    9:	private:                                  
    10:	   int x;                                   
    11:	public: 
    12:	   BrandNew () ;
    13:	    void * operator new(size_t size); 
    14:	};
    15:
    16:	char buf[ 512 ] ;
    17:	int index;
    18: 	
    19:	main()
    20:	{
    21:            cout « "\nCreating local instance";
    22:  	   BrandNew b1;	
    23: 
    24:  	   cout « "\nAllocating space via new"; 


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

    25:	BrandNew *Ь2 = new BrandNew; 
    26:	BrandNew *b3 = new BrandNew; 
    27:	BrandNew *54 = new BrandNew; 
    28:	BrandNew *Ь5 = new BrandNew; 
    29:	return 0; 
    30:	} 
    31: 
    32:	BrandNew::BrandNew() 
    33:	{ 
    34:	  cout « "\nlnside constructor"; 
    35:	  x = index; 
    36:	}
    37: 
    38:	void *BrandNew::operator new(size_t size) 
    39:	{ 
    40:	  cout « "\nlnside overloaded new. Size == " « size; 
    41:	  if (index >= 512 - sizeof(BrandNew)) 
    42:	     return 0; 
    43:	  else { 
    44:	    int k = index; 
    45:	    index += sizeof(BrandNew); 
    46:.	    return &buf[k]; 
    47:	  } 
    48:	}
 

    Перегрузка delete 

    Оператор delete — обратная сторона медали распределения памяти. Как и в 
случае с new, вы можете перегрузить оператор delete для отслеживания удаления 
объектов, адресованных указателями. Прототип функции перегрузки оператора 
delete должен иметь вид 

    void operator delete(void *p); 

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

    void operator delete(void *p,  size_t size); 

    В соответствии с вторым объявлением C++ будет дополнительно передавать 
функции число байтов в удаляемом объекте. 
    Для добавления функции перегрузки оператора delete в класс BrandNew в 
модуле OVERNEW (листинг 15.7) объявите функцию-член в открытой секции класса: 

    void BrandNew::operator delete(void *p) 
    {
       cout « "\nDeleting object at " « p; 
    } 

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

    delete Ь2; 
    delete ЬЗ;
    delete b4;
    deiete b5; 

    Запустив программу на выполнение, вы увидите сообщения, в точности, 
указывающие, когда вызывался перегруженный оператор delete. Этот пример 
приведен только для демонстрации, поскольку в полноценном приложении
 перегруженный оператор delete должен освобождать удаленные блоки памяти 
для последующего использовання их перегруженным new. (См. также раздел 
"Размещение объектов по заданным адресам" далее в этой главе.) 

    ЗАМЕЧАНИЕ

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


    Глава 15. Передовые средства C++     381 

    Установка обработчика ошибок оператора 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++. 

     ЗАМЕЧАНИЕ

     Сиотри также описание set_new_handler в книге 
Освоение Borland С++. Энциклопедия функций. 


    Копирование объектов классов 

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

    ЗАМЕЧАНИЕ 

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

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

    Почленная инициализация 

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


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

    class TAnyClass { 
    private:  
       int i; 
       double r; 
    public: 
     TAnyClass() 
          {i = 0;   r = 0;   } 
     TAnyClass(int 11, double rr) 
            { i = 11;   r = rr;   } 
     }; 

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

    TAnyClass v1; 
    TAnyClass v2(100, 3.14159); 

    Можно также использовать существующие объекты, подобные v2, для 
инициализации нового объекта v3: 

    TAnyClass v3 = v2; 

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

    TAnyClass v3; 
    v3.i = v2 i, // Исключительно для демонстрации 
    v3.r = v2.r; // 


    ЗАМЕЧАНИЕ

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

     Конструктор копии

    Вы можете снабдить ваше приложение конструктором копии для создания 
объектов, копирующихся из других объектов. Конструктор может выполнять любые 
действия, необходимые для создания безопасной копии, в частности, не 
дублировать указатели. (В случае необходимости Borland C++ создает 
конструкторы копии для классов, в которых они непосредственно не определяются.) 
    Конструктор копии следует объявлять как CLASS( const CLASS& ). Как и все 
конструкторы, конструктор кипии не может быть виртуальным и наследоваться 
производным классом. Вот новая версия класса TAnyClass с членами-указателями 
на данные и конструктором копии: 

    #include <string.h> 
    class TAnyClass { 
    private: 
        int i;        // Закрытый член класса 	
       double г;   // Закрытый член класса 
       char *s,     // Закрытый член-указатель
    public:
       TAnyClass() 
          { 1 = 0;   г = 0;  s = NULL,   } 
       TAnyClass(int ii, double rr, const char *ss) 
          { i = 11;   r = rr;  s = strdup(ss);   } 
       ~TAnyClass() { delete s;  }                                              
       const char *GetStr(void) { return s;  } 
       TAnyClass(TAnyClass &copy),  // Конструктор копии 

    Конструктор копии TAnyClass(TAnyClass &copy) имеет одинаковое имя с классом 
(TAnyClass), в котором он определен, в нем объявлена ссылка &сору на объект класса 
TAnyClass, данные из которого копируются в создаваемый объект. Реализуйте 
конструктор копии где-либо в другом месте программы (либо встраиваемым в 
объявлении класса): 

    Глава 15. Передовые средства C++     383 

    TAnyClass::TAnyClass(TAnyClass &copy) { 
    cout « "Inside TAnyClass1s copy constructor\n"; 
    i = copy.i; 
    r = copy. r; 
    if (copy.s) 
       s = strdup(copy.s); 
    else	
       s = NULL; 
    } 

    Можно обойтись и без оператора вывода в поток. Конечно, при отладке это 
отображение момента запуска конструктора приносит свою пользу. Следующие два 
оператора копируют члены i и г из сору в новый объект. Чтобы можно было 
обрабатывать строку, на которую ссылается указатель s, с помощью функции 
strdup( выделяется еще один участок памяти в куче для копии первоначальной 
строки. Или же, если в указателе объекта сору содержится нуль, указателю 
нового объекта присваивается NULL. Для скопированных объев класса TAnyClass 
более не существует опасности, что два или более указателей s будут случайно 
ссылаться на один и тот же участок памяти. Теперь в программе можно смело 
использовать следующие операторы: 

    TAnyClass v1; 
    TAnyClass v2(1.  2,   "A test string"); 
    TAnyClass v3 = v2;       // Вызывается конструктор копии 

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

    ЗАМЕЧАНИЕ 

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

    Почленное присваивание 

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

    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; // Закрытый член класса 
      double r; // Закрытый член класса 
      char *s; // Закрытый член-указатель! 
    public: 
       ......    // См. предыдущее объявление класса TAnyClass 
       void operator=(const TAnyClass &copy);    // Оператор присваивания 


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

    Объявление operator= перегружает оператор присваивания, задавая 
константный ссылочный параметр сору типа TAnyClass. Параметр ссылается на 
копируемый объект. Другими словами, если а и b — объекты класса TAnyClass, в 
присваивании вида а = b параметр сору ссылается на b. Функция перегрузки 
оператора присваивания реализуется следующим образом: 

    void TAnyClass::operator=(const TAnyClass &copy) 
    {
       cout « "\nlnside TAnyClass's operator* function; 
       if (this == &copy) 
         return; 
      delete s; 
      i = copy.i; 
      r = copy.r; 
      s = strdup(copy.s); 
   } 

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

Например, при попытке выполнить операторы 

    TAnyClass v1, v2; 
     v1 = v1; 

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

    Вызов operator= из конструктора копии 

    Вы можете легко устранить излишнее дублирование кода в конструкторе копии 
и перегруженном операторе присваивания. Внутри конструктора копии следует 
просто вызвать функцию перегрузки operator= присваиванием копируемого объекта 
объекту *this: 

    TAnyClass::TAnyClass(TAnyClass &copy) 
    { 
       s = NULL;	  // Инициализировать имеющиеся указатели нулевыми значениями 
       *this = copy;          // Вызвать перегруженный оператор присваивания 
    } 

    Весьма важно инициализировать все члены (обычно указатели), которые 
проверяет перегруженный оператор присваивания. Например, если указатель 
содержит всякий мусор вместо NULL, функция перегрузки опеpaтора присваивания 
попытается освободить память, которая не была выделена. Излишне говорить, что 
в лом случае вас ожидают неприятности. 

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

    Форматирование вывода 

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

    extern istream_withassign _Cdecl cin;   // Объект потока ввода 
    extern ostream_withassign _Cdecl cout; // Объект потока вывода 
    extern ostream_withassign _Cdecl cerr; // Объект потока вывода ошибок 
    ewtern ostream_withassign _Cdecl clog; // Объект буферизованного потока вывода ошибок 

    Кшассы istream_withassign и ostream_withassign в конечном счете выводятся 
из "прадедушки" классов потока ввода-вывода — ios. Вам не надо вникать во все 
детали реализации этих классов для того, чтобы использовать их в программах, однако, 
если вам интересно, в поставку Borland C++ входят полные и пространные файлы с 
исходными текстами реализации потоков ввода-вывода. 


    15. Передовые средства C++     385 
     Borland® C+ +4.5 

    Потоковые классы выполняют форматированный ввод-вывод со встроенной 
поддержкой обработки ошибок. Потоки ввода-вывода поддерживают базовые типы 
данных, подобные char, short, int, long, char* (напри мер,  символьная строка,  
завершающаяся нулем),  float, double,  long double и void* (отображается как 
значение-адрес). 
    Имейте в виду, что cin, cout, cerr и clog — объекты классов. В соответствующих 
им классах определены перегруженные операторы потоков ввода и вывода, что 
позволяет использовать операторы ввода-вывода в поток следующим образом: 

    cin » v;        // Прочитать v из стандартного ввода	
    cout « v;      // Записать v в стандартный вывод                  

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

    cout « "Balance == $" « balance < " (dollars)\n";	

    Для форматирования вывода вы можете установить один или несколько флагов в 
объекте потока ввода-вывода (см. объявление класса ios в заголовочном файле 
IOSTREAM.H). Следует использовать функции-члены flags(),  setf() и unsetfO для 
управления состоянием этих флагов. Например, для вывода беззнакового
значения в шестнадпатеричной форме следует установить флаг hex в cout 
путем следующего обращения к функции-члену setf() класса ios: 

    unsigned v = 12345;	
    cout « "Before:  " « v « '\n';	// Вывод по умолчанию (десятичный)     
    cout.setf(cout.hex);	// Модификация потока вывода              
    cout « "After:    " « v « '\n'	// Шестнадцатеричный вывод         

    Несмотря на то что это сработает, как показано дальше, существуют более 
простые способы форматирования потока вывода. 

    Использование манипуляторов 

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

    unsigned v = 12345; 
    cout « "In hexadecimal v == " « hex « v « '\n'; 
    cout « "In decimal v == " « dec « v « '\n'; 

    В последней строке используется манипулятор dec для возвращения cout в 
состояние форматированного вывода в десятичной форме. Вы также можете 
манипулировать потоками ввода. Следующие операторы читают шестнадпатеричное 
значение и выводят его же в десятичной форме: 

    cout « "Enter value in hex: "; 
    cin » hex » v;	
    cout « "Value in decimal == " « dec « v;	

    В табл. 15.2 перечислены манипуляторы ввода-вывода, определенные в 
заголовочных файлах IOMANIP.H и IOSTREAM.H. 
    Некоторые манипуляторы используются с параметрами. Например, манипулятор 
setbase(int n) форматирует значение в системе счисления с основанием п. (Если 
n — нуль, вывод по умолчанию становится десятичным, а ввод по умолчанию 
соответствует спецификациям ANSI С для символьного представления целочисленных 
значений.)	

    Таблица 15.2. Манипуляторы потоков ввода-вывода	

    Манипулятор	Действие 

    dec	Преобразовать в десятичную систему счисления 
    endl	Начать новую строку; вывести буфер потока 
    ends	Вставить нулевой завершающий символ в строку 
    flush	Выполнить довывод в поток 
    hex	Преобразовать в шестнадцатеричную систему счисления 
    lock(ios &ir)	Заблокировать дескриптор файла для ссылки ir на поток ввода-вывода 


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

    Окончание табл. 15.2 

    Манипулятор	Действие 

    oct	Преобразовать в восьмеричную систему счисления 
    resetiosflags( Gong f)	Очистить биты форматирования, заданные в / 
    setbase(int n)	Установить систему счисления с основанием n (0, 8, 10 или	16) 
    setiosflags( dong f)	Установить биты форматирования, заданные в f 
    setfil( int c)	Использовать символ с для заполнения при выравнивании 
    setprecision( Gnt n)	Установить точность вывода значений с плавающей точкой 
                                     равной п 
    setw(int n)	Установить ширину поля п 
    unlock( ios &ir)	Разблокировать дескриптор файла для ссылки ir потоков 
                                    ввода-выво
    ws	Извлечь пробельные символы 

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

    ostream& bell(ostream& os) { 
    return os « "\a";	// '\a' - управляющий код звонка 
    } 

    Функция bell() имеет тип &ostream, и в ней объявлен параметр os типа 
ostream&. Это позволит использовать функцию в каскадном операторе вывода в поток: 

    cout « bell « "Ding!"; 

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

    cout.width(8); 
    cout « v « '\n';       // Отобразить v в восьми позициях 
    cout « v « '\n';       // Отобразить v с выравниванием по умолчанию 

    Модификатор width недолговечен, он оказывает влияние только на следующее 
выводимое значение. Другие функции-члены используют функцию fill(charc), 
устанавливающую символ заполнения при выравнивании с помощью символьного 
параметра с, и precision(int n), устанавливающую точность вывода значений с 
плавающей точкой равной n. 

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

    Обычно потоки вывода могут поддерживать лишь базовые типы данных, подобные 
int, long, double, char* и т.д. С помощью перегрузки оператора вывода в поток «, вы можете 
заставить выражения, в которых используются эти операторы, выводить объекты 
ваших собственных классов. 
    В листинге 15.8 объявляется потоковый класс с именем TPoint и двумя 
целочисленными переменными х и у, запоминающими координаты, вероятно, на 
графическом дисплее. В программе перегружается оператор вывода в 
поток, так что можно использовать объекты класса TPoint в этих операторах, 
как показано в строках 20 и 23. 

    Листинг 15.8.  POINTOUT.CPP (классы и операторы вывода) 

    1:           #include <iostream.h> 
    2:
    3:	class TPoint { 
    A:	private: 
    5:	   int x,  y; 
    6:	public: 
    7:	  TPoint() { x = у = 0;   } 
    8:	  TPoint( int xx, int yy ) { x = xx; у = yy; }


    15. Передовые средства C++     387 

    9:	void PutX( int xx )	{ x = xx;   }                                       
    10:	void PutY(int yy)	{ y = yy; }                                                                                                   
    11:	int GetX(void)	{ return x;  }                                         
    12:	int GetY(void)	{return y;   }                                        
    13:	friend ostream& operator«(ostream& os, TPoint &p);                     
    14:	}	; 
    15:				
    16:	main()                                                                 
    17:	{ 
    18:	TPoint p;		
    19:				
    20:	 cout « p « '\n' ;
    21:	p.PutX(I00); 
    22:	p.PutY(200); 
    23:	cout « p « '\n';
    24:	return 0; 
    25:	} 
    26: 
    27:	ostream& operator«(ostream& os, TPoint &p)                                                                                                   | 
    28:	{ 
    29:	  os « "x == " « p.x « ",  у == " « p. у;                               
    30:	  return os; 
    31:	} 

    В строке 13 путем объявления дружественной функции-члена operator«()
перегружается оператор вывода в поток.Эта дружественная функция-член возвращает 
значения типа ostream&, и в ней объявлены два параметра — os типа ostream& и 
ссылка р на объект класса TPoint. 
    Поскольку функция operator«() — друг класса TPoint, оператор в строке 29 
имеет непосредственый доступ к данным-членам х и у объекта, на который 
ссылается ссылка р. Обратите внимание, что всю строку занимает один оператор 
вывода в поток, выводящий через ссылку os типа ostream значения двух 
данных-членов плюс две символьные строки. В конце концов, функция возвращает 
ссылку os. Запуск программы на выполнение приведет к следующим результатам:	

    х == 0,  у == 0 х == 100,  у == 200 

    Поскольку функция operator«() возвращает ссылку на ostream, можно выводить 
несколько объектов в одном операторе вывода в поток. Например, если p1, р2 и 
рЗ — объекты класса TPoint, вы можете отобразить их значения следующим образом: 

    cout « р1 « ";   " « р2 « ";   " « рЗ; 

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

    ((((cout « р1) « ";   ") « р2) « ";   ") « рЗ; 

    Разве вы не радуетесь тому, что вам не надо вводить все эти скобки? 
    
    Перегрузка оператора ввода из потока 

    Также возможна и перегрузка оператора ввода из потока », с ее помощью 
можно научить C++ читать из потока объекты класса. В листинге 15.9 к 
возможностям программы-примера POINTOUT.CPP из предыдущего раздела добавляются 
возможности чтения из потока. 

    Листинг 15.9.  POINTIN.CPP (классы и потоки ввода) 

    1:	#include <iostream.h> 
    2: 
    3:	class TPoint { 
    4:	private:	
    5:	   int x, y; 
    6:	public: 
    7:	  TPoint() { x = у = 0; } 
    8:	 TPoint(int xx, int yy)	{ х = хх; у = уу; } 
    9:	  void PutX(int xx) { x =	хх; }     
    10:	  void PutY(int yy) { у =	уу; }   
    11:	  int GetX(void) { return	x; }             
    12:	  int GetY(void) { return	у; }              
    13:	  friend ostream& operator«(ostream& os, TPoint &p); 
    14:	  friend istream& operator»(istream& is, TPoint &p); 
    15:	}; 
    16: 
    17:	main() 


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

    18:	{ 
    19:	TPoint p; 
    20: 
    21:	cout « p « '\n'; 
    22:	p.PutX(I00); 
    23:	p.PutY(200); 
    24:	cout « p « '\n'; 
    25:	cout « "\nEnter x and у values: "; 
    26:	cin » p; 
    27:	cout « "\n You entered: " « p; 
    28:	return 0; 
    29:	} 
    30: 
    31:	ost ream& operator«(ostream& os, TPoint &p) 
    32:	{ 
    33:	  os « "x == " « p.x « ",  у == " « p.у; 
    34:	  return os; 
    35:	} 
    36: 
    37:	istreamA operator»(istream& is, TPoint &p) 
    38:	{ 
    39:	  is » p.x » p.y; 
    40:	  return is; 
    41:	}
 
    В строке 14 для класса TPoint объявляется дружественная функция-член ввода 
из потока operator»(), аналогичная функции перегрузки оператора вывода в 
поток, приведенной в предыдущем разделе, только вместо ostream используется istream. 
    В реализации функции (строки 37-41) читаются значения для х и у с помощью 
ссылки is на istream. Функция возвращает ссылку is, таким образом можно 
вводить несколько значений в одном операторе ввода. 
    При запуске программы на выполнение запрашиваются два значения для х и у. 
Введите два целочисленных значения, разделенных пробелом. Оператор ввода из 
потока в строке 26 сохранит оба введенных вами значения в объекте класса, 
которые затем отобразятся в строке 27. 

    Шаблоны 

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

    Шаблонные функции 

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

    template<class T> void f(T param) 
    { 
       // Тело функции 
    } 

    Шаблонная функция начинается строкой template<class Т>, указывающей 
компилятору, что Т — определяемое пользователем имя функции. (Вы можете 
заменить Т на любое другое имя, если захотите.) Необходим, по крайней мере, 
один параметр Т для передачи функции данных для обработки. Вы можете также 
задать указатель (Т* param) или ссылку (Т& param). Функция может объявлять 
несколько параметров и возвращать значение типа Т: 

    template<class Т> Т f(int а, Т b) { 
           // Тело функции 
    } 

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

    double f(int a, double b); 

    Если это обычная функция, вы должны обеспечить ее реализацию. Однако, 
поскольку f() — шаблонная функция, компилятор реализует код функции, заменив Т в 
данном случае на double. 


    15. Передовые средства C++     389 

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

    Листинг 15.10.  MINMAX.H (шаблоны функций min и max) 

    1:	// minmax.h -- Шаблоны функций min и шах 
    2: 
    3:	#ifndef _MINMAX_H 
    4:	#define _MINMAX H 1 // Предотвращение повторных #indlude 
    5: 
    6:	template<class Т> Т max(T а, Т b) 
    7:	{ 
    8:	if (а > b)           
    9:	  return a;                                                               
    10:	else                                                                . 
    11:	  return b;                                                              
    12:	} 
    13:	
    14:	template<class T> Т min(T а, Т b) 
    15:	{                                                                  
    16:	  if (a < b)                                                 .           
    17:	    return a;          
    18;	else                 
    19:	  return b;      
    20:	}                                                                      
    21: 
    22:	#endif //  MINMAX H 

    Рассмотрим подробнее строку 6. В ней Т заявлен неопределенным типом, 
объект которого возвращает шаблонная функция mах(). Кроме того, функции mах()
необходимы два аргумента типа Т. Операторы в функции, приведенные в строках 
8-11 — схема для реальных операторов, которые будут сгенерированы позже, когда 
будет задан действительный тип Т. Шаблон функции min() объявляется аналогичным образом. 
    Вы можете использовать более одного неопределенного типа. Для объявления 
mах() с двумя различными неопределенными типами можно написать: 

     template<class T1,  class T2> T1 max(T1 a,  T2 b) 

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

    Листинг 15.11.  FTEMPLAT.CPP (использование шаблонов функций) 

    1:	#include <iostream.h> 
    2:	#include "minmax.h" 
    4:	int max(int a,  int b); 
    5:	double max(double a,  double b); 
    6:	char max(char a,   char b); 
    7:	
    8:	main()       
    9:	{        
    10:	   int i1 = 100,  12 = 200;                                           . 
    11:	  double d1 = 3.14159, d2 = 9.87654; 
    12:	  char c1 =  'A',  c2 = 'z'; 
    13: 
    14:	  cout « "max(i1,   i2) == " « max(i1,  i2) « '\n'; 
    15:	  cout « "max(d1,  d2) == " « raax(d1,  d2) « '\n'; 
    16:	  cout « "nax(c1, c2) ~ " « max(c1, c2) « '\n'\ 
    17:	  return 0;                                                             
    18:	} 

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


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

    Шаблонные классы 

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

    Листинг 15.12.  DB.H (шаблон класса TDatabase) 

    1:	// db.h -- Объявление шаблонного класса TDatabase 
    2: 
    3:	#ifndef __DB_H 
    4:	#define __DB H   1               // Предотвращение нескольких «include 
    5: 
    6:	template<class T> 
    7:	class TDatabase { 
    8:	private: 
    9:	   Т *rp;           // Указатель на записи 
    10:	   int num;          // Число записей 
    11:	public: 
    12:	   TDatabase(int n) 
    13:	       { rp = new T[num = n]; } 
    14:	   ~TDatabase()
    15:	    { delete[] rp; } 
    16:	  void DoNotning(void); 
    17:	  Т &GetRecord(int recnum); 
    18:	}; 
    19: 
    20:	template<class T> 
    21:	void TDatabase<T>::DoNothing(void) 
    22:	{ 
    23:	} 
    24: 
    25:	template<class T> 
    26:	Т &TDatabase<T>::GetRecord(int recnum) 
    27:	{ 
    28:	  Т *crp = rp; // Указатель на текущую запись = указатель на записи 
    29:	  if (0 <= recnum && recnum < num) 
    30:	     while (recnum--  > 0) 
    31:	         crp++; 
    32:	  return *crp; 
    33:	} 
    34: 
    35:	#endif   //     OB H 

    Объявление шаблона класса имеет следующий общий вид: 

    template<class T> class TDatabase { 
        // Закрытые, защищенные и открытые члены класса 
    }

    где Т — неопределенный тип, задаваемый пользователем шаблона. (Вы можете 
заменить Т на любой другой идентификатор, если захотите.) Т можно впоследствии 
заменить любым встроенным типом, другим классом, указателем и т.д. TDatabase — 
имя шаблонного класса. Для ясности заголовок шаблона лучше объявлять в 
отдельных строках: 

    template<class T> class TAnyClass { 
      ......
    } 

    Можно также указать несколько неопределенных типов: 

    template<class T1, class T2, class T3> class TAnotherClass { 
      ...       
    }	

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

    T *гр: 


    15. Передовые средства C++     391 

    На этой стадии настоящая природа Т еще не известна, поэтому в программе 
его можно использовать только в самых общих случаях. Тем не менее, конструктор 
TDatabase в строках 12-13 выделяет память для массива объектов Т, присваивая адрес 
массива указателю гр и заодно устанавливая член num равным требуемому числу 
записей. (Для простоты в классе игнорируются все связанные с выделением памяти 
ошибки, которые могут возникнуть.) В строке 15 этот массив удаляется с помощью 
оператора специального вида delete[] с целью обеспечения вызова деструктора для 
массива, содержащего объекты класса. 
    В строке 17 объявляется функция-член GetRecord(), возвращающая ссылку на 
объект типа Т, идентифицируемый номером записи recnum. Это еще один вид 
обобщенной операции, не требующей знания определенного типа Т. 
    Функции-члены шаблонного класса могут реализоваться встраиваемыми, как это 
показано на примере конструктора и деструктора класса TDatabase. Или же они могут 
реализоваться отдельно. Конечно, поскольку эти функции-члены шаблонного класса 
пока остаются только объявлениями, их вполне можно поместить в заголовочный 
файл, а не в отдельный модуль. Функция DoNothingO, которая, как подсказывает 
ее имя, не выполняет никаких действий, демонстрирует общий вид реализации 
функций-членов шаблонного класса: 

    template<class T> 
    void TDatabase<T>::DoNothing(void) 

    Заголовок функции-члена предварен фразой template<class Т>. Затем следует 
тип возвращаемого функцией значения (void), имя класса (TDatabase<T>) и 
оператор разрешения области видимости (::). Последним идет объявление самой 
функции (DoNothing(void)) и тело (пустое, в данном случае). Используйте этот 
пример как образец для написания ваших собственных функций-членов шаблонных классов. 
    В строках 25-33 демонстрируется более сложный пример функции-члена 
шаблонного класса вместе с операторами, реализующими действия функции. Функция 
GetRecord() возвращает ссылку на объект типа Т. Внутри функции указатель сгр типа Т 
присваивается указателю того же типа с именем гр. Иными словами, указатель 
сгр ссылается на первую запись, сохраненную в объекте класса TDatabase. В 
строке 29 проверяется, соответствует ли параметр recnum допустимому диапазону 
значений. Если да, цикл while в строках 30-31 декрементирует  параметр recnum 
до нуля и одновременно передвигает указатель сгр на одну запись в базе данных. 
    Обратите внимание на строку 31. Инкрементирование указателя р с помощью 
выражения р++ сдвигает указатель на число байтов объекта, на который 
ссылается р. В классе Database даже несмотря на то, что тип объекта, на 
который ссылается сгр, не известен, вполне приемлемо выражение сгр++. Позже, 
когда будет задан действительный тип для шаблона класса, компилятор сможет 
сгенерировать соответствующие инструкции для увеличения указателя сгр на 
величину sizeof(T). Без шаблонных классов подобные обобщенные операторы было 
бы не так-то легко написать. 
    В строке 32 возвращается разыменованное значение указателя сгр — другими 
словами, ссылка на любой объект, адресуемый сгр. Это завершает объявление 
шаблона класса, в котором не делается никаких предположений о том, какого типа 
данные в нем запоминаются. 
    Следующий шаг — использование шаблона для создания объекта класса базы 
данных, способного запомнить некоторое число записей. В листинге 15.13 
демонстрируется работа шаблона класса TDatabase. 

    Листинг 15.13. CTEMPLAT.CPP (использование шаблонов класса) 

    1:	#include <iostream.h> 
    2:	#include <string.h> 
    3:	#include "db.h"   
    4:	
    5:	class TRecord {    
    6:	private: 
    7:	   char name[41];                                                          
    6:	public:	
    9:	  TRecord()
    10:	    { nameCO] = 0;  }	
    11:	 TRecord(const char *s)	
    12:	    { Assign(s);  }             
    13:	 void Assign(const char *s)                     
    14:	    { strncpy(name,  s, 40);  }     
    15:	 char *GetName(void)	
    16:	    { return name;  }	
    17: 
    18:	
    19:	main()      
    20:	{                    
    21:	  int rn;                                      // Индекс числа записей	
    22:	 TDatabase<TRecord> db(3);       // База данных из трех TRecords	


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

    23:	TDatabase<TRecord*> dbp(3); // База данных из трех указателей 
    24:	TDatabase<TRecord> *pdb;   // Указатель на базу данных 
    25:	TDatabase<TRecord»> *ppdb;  // Указатель на базу данных указателей на TRecord , 
    26: 
    27:	cout « "\n\nDatabase of 3 TRecords\n"; 
    28:	db.GetRecord(O).Assign("George Washington"); 
    29:	db.GetRecord(1).Assign("John Adams"); 
    30:	db.GetRecord(2).Assign("Thomas Jefferson"); 
    31:	for (rn = 0; rn <= 2; rn++) 
    32:	  cout « db.GetRecord(rn).GetName() « '\n'; t;
    33: 
    34:	cout « "\n\nDatabase of 3 TRecord pointers\n"; 
    35:	dbp.GetRecord(0) = new TRecord("George Bush"); 
    36:	dbp.GetRecord(1) = new TRecord("Ronald Reagan"); 
    37:	dbp.GetRecord(2) = new TRecord ("Jimmy Carter"); 
    38:	for (rn = 0; rn <= 2; rn++) 
    39:	  cout « dbp.GetRecord(rn)->GetName() « '\n'; .,* 40: 
    41:	cout « "\n\nPointer to database of 3 TRecords\n"; 
    42:	pdb = new TDatabase<TRecord>(3); 
    43:	pdb->GetRecord(0).Assign("John Adams"); 
    44:	pdb->GetRecord(1).Assign("Thomas Jefferson"); 
    45:	pdb->GetRecord(2).Assign("Aaron Burr"); 
    46:	for (rn = 0; rn <= 2; rn++) 
    47:	   cout « pdb->GetRecord(rn).GetName() « '\n'; 48: 
    49:	cout « "\n\nPointer to database of 3 TRecord pointers\n"; 
    50:	ppdb = new TDatabase<TRecord *>(3); 
    51:	ppdb->GetRecord(0) = new TRecord("Dan Quayle"); 
    52:	ppdb->GetRecord(1) = new TRecord("George Bush"); 
    53:	ppdb->GetRecord(2) = new TRecord("Walter Mondale"); 
    54:	for (rn = 0; rn <= 2; rn++) 
    55:	   cout « ppdb->GetRecord(rn)->GetName() « '\n'; 
    56: 
    57:	return 0; 
    58:	} 

    После включения заголовочного файла DB.H (строка 3), содержащего 
объявление шаблонного класса TDatabase, в программе объявляется класс TRecord 
— один из объектов для запоминания в TDatabase. Класс TRecord очень прост, в 
нем объявлен в качестве данных-членов только символьный массив name. Однако 
TRecord на самом деле может быть произвольным классом, так как в классе 
TDatabase не делается никаких предположений о типе сохраняемых объектов. 
    В строках 22—25 демонстрируются четыре способа создания объектов класса с 
помощью шаблона класса. В строке 

    TDatabase<TRecord> db(3); 

определяется объект с именем db шаблонного класса типа TDatabase и 
задается TRecord в качестве класса, замещающего Т в шаблоне. Выражение в 
скобках (3) — инициализатор db, передаваемый конструктору класса TDatabase. 
    Вы можете также создать базу данных из других значений, для этого не 
обязательно использовать классы. Например, вы можете определить базу данных 
из 100 вещественных значений двойной точности следующим образом: 

    TDatabase<double> dbd(100); 

    Поскольку класс TDatabase написан для сохранения объектов произвольных 
типов, он называется контейнерным классом. Обычно, наилучшие контейнерные 
классы являются полностью обобщенными, хотя этот идеал часто трудно достичь. 
    В строках 23-25 демонстрируется создание других экземпляров шаблонного 
класса. В строке 23 объявляется объект dbp класса TDatabase, состоящий из трех 
указателей на TRecord, в строке 24 — указатель pdb на  объект класса TDatabase с 
незаданным числом объектов типа TRecord. Эти примеры могут покрыть большую часть 
объектов, которые вы можете захотеть создать на основе других шаблонов класса. 
   В программе объект класса TDatabase используется так же, как и любой другой не 
шаблонный объект. Например, в строке 28 

   db.GetRecord(0).Assign("George Washington"); 

вызывается функция GetRecord(0) для доступа к записи с индексом 0. Затем в строке 
вызывается функция-член Assign() этого объекта для задания строкового значения. 


    15. Передовые средства C++     393 

    В строках 34-55 демонстрируется использование других объектов класса 
TDatabase, Особенно любопытна строка 35: 

    dbp.Getfiecord(O) = new TRecord("George Bush"), 

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

    СОВЕТ

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

    Разное 

    Все компьютерные языки имеют ценные, но редко используемые средства. 
Скорее всего, вы не захотите пользоваться следующими приемами слишком часто, 
тем не менее одно или несколько этих средств C++ могут оказаться полезными для вас. 

    Указатели на функции-члены 

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

    double (TFirstClass::*myfnptr)(void); 

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

    void (TFirstClass::*myfnptr)(int, int); 

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

    Листинг 15.14. MFNPTR.CPP (указатели на функции-члены) 

    1:	#include <iostream.h> 
    2:	#include <iomanip.h> 
    3: 
    4:	class TFirstClass { 
    5;	private:         
    6:	   int count;
    7:	public:         
    8:	  TFirstClass() { count » 0; }
    9:	   int Access(void) ;           
    10:	}
    11:	
    12:	int (TFirstClass::*myfnptr)(void); 
    13: 
    14:	main() 
    15:	{ 


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

    16:	int i ; 
    17:	TFirstClass fc; 
    18: 
    19:	cout « "\nCall Access the normal way:\n"; 
    20:	for ( i = 0 ; i < 9 ; i++ ) 
    21:	   cout « setw(8) « dec « fc.Access() ; 
    22:	cout « "\n\nCall Access via the member function pointer\n"; 
    23:	myfnptr = &TFirstClass::Access; 
    24:	for ( i = 0 ; i < 9; i++ ) 
    25:	   cout « setw(8) « dec « (fc.*myfnptr)() ; 
    26:	cout « "\n\nMember function pointer and a dynamic instance\n"; 
    27:	TFirstClass *fp = new TFirstClass ; 
    28:	for (i = 0; i < 9; i++) 
    29:	   cout « setw(8) « dec « (fp->*myfnptr)(); 
    30:	return 0; 
    31:	} 
    32: 
    33:	int TFirstClass::Access(void) 
    34:	{ 
    35:	  return count++ ; 
    36:	} 

    В строке 12 myfnptr объявляется указателем на функцию-член класса 
TFirstClass, не имеющую аргументов и возвращающую целочисленное значение. 
Указатель может ссылаться на любые функции-члены класса, имеющие подобный вид. 
    В функции main() в строке 17 объявляется объект fc класса TFirstClass, в 
строке 21 вызывается функция-член Aceess() класса TFirstClass для объекта fc 
обычным способом. (Функция Access() только инкрементирует и возвращает 
закрытый член класса — должно же в программе хоть что-нибудь выполняться!) 
    В строке 25 демонстрируется вызов функции-члена Access() с помощью 
указателя myfnptr. Для инициализации указателя в строке 23 указателю myfnptr 
присваивается адрес функции-члена Access() класса TFirstClass. Вы можете также 
заменить объявление указателя (строка 12) и присваивание (строка 23) одним 
оператором, глобальным или локальным в функции main(): 

    int (TFirstClass::*myfnptr)(void) = &TFirstClass::Access; 

    При вызове по указателю функции-члена вы должны придерживаться следующих 
двух правил. 
    •  Обращайтесь к объекту класса. 
    •  Заключайте в круглые скобки вызов функции. 
    Например, если п — целочисленная переменная, и fс — объект класса 
TFirstClass, следующая строка присвоит n результат функции Access(): 

    n = (fc.*myfnptr)(); 

    Двухсимвольное выражение .* называется оператором ссылки на член и служит 
для разыменования указателя на функцию-член объекта класса. Круглые скобки в 
выражении (fс.*myfnptr) необходимы, поскольку оператор вызова функции () имеет 
более высокий приоритет, чем .*. Оператор, приведенный выше, в точности 
соответствует более распространенному 

    п = fс. Access(); 

    Можно также вызывать функции-члены с помощью указателей, когда объекты 
адресуются другими переменными-указателями. Например, вернемся к строкам 27-29 
в модуле MFNPTR (листинг 15.14). Сначала указателю fp присваивается адрес 
нового объекта класса TFirstClass. Затем в выражении 

    (fp->*myfnptr)(); 

в операторе вывода в поток в строке 29 вызывается функция-член Access() для 
объекта, адресованного указателем fp. Трехсимвольное выражение ->* — другой 
вид оператора ссылки на член. Опять же, необходимы дополнительные скобки, 
поскольку операция () имеет более высокий приоритет, чем ->*. Предыдущий 
оператор в точности соответствует следующему: 

    fp->Access(); 

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

    double TFirstClass::*dataPtr; 

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

    dataPtr = &TFirstClass::balance ;	


    Глава 15. Передовые средства C++     395 

    Или же можно объявить указатель и присвоить ему адрес члена balance 
следующим обрадом

   double TFirstClass::*dataPtr = &TFirstClass::balance;	

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

    fс.balance = 123456 ;
    cout « "\nBalance=" « fc.*dataPtr ;

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

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

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

   clacc TAnyClass {
   public:
      static void GlobalInit( void ) ;
      ...
   );

   Статические функции-члены не могут быть виртуальными. Реализовать статические 
функции-члены следует так же, как и другие, предварив имя функции именем класса и 
оператором разрешения области видимости: 

    void TAnyClass::GlobalInit(void) 
    {
      ... // Операторы, которые необходимо выполнить 
    }

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

    TAnyClass::GlobalInit();	// Вызвать статическую функцию Globallnit() 

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

    class TAnyClass {	
    private:	
       static char с;   // Статический член, хранящий данные 
    public: 
       char GetC(void) { return c; } 
    };	

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

    char TAnyClass::с = 'q'; 

    В действительности с — глобальная переменная, доступная только 
функциям-членам TAnyClass. Далее можно определить объект класса TAnyClass, 
использующий с. Например, следующие операторы определяют объект х и вызывают 
функцию-член GetC(), отображающую символ 'q':	

    TAnyClass х;	
    cout « х.GetC();         // Отображает статический член с           

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

    Иногда вам может понадобиться поместить объект в заданный участок памяти, 
например, в глобальный буфер в сегменте данных. Подобные объекты продолжают 
существовать и вне области их объявления, и иногда их называют "постоянными 
объектами".	
    В листинге 15.15 демонстрируется размещение объекта по заданному адресу. В 
программе также приводится пример непосредственного вызова деструктора — 
необычный прием, который может пригодиться для удаления объектов, созданных 
перегруженным оператором new, использующим нестандартные способы выделения памяти. 


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

    Листинг 15.15.  PERSIST.CPP (запоминание постоянных объектов в буфере) 

    1:	#include <iostream.h> 
    2:
    3:	class TPersist { 
    4:	private: 
    5:	   int x, y; 
    6:	public: 
    7:	   TPersist(int a,  int b) { x = а;  у = b;   }              // Конструктор 
    8:	   ~TPersistO { x = 0;  у = 0;  }                             // Деструктор 
    9:	   void *operator new(size_t,  void *p) {  return p;   } 
    10:	   friend ostream& operator« (ostream &os, TPersist &p); 
    11:	}; 
    12: 
    13:	char object[sizeof(TPersist)]; 
    14: 
    15:	main() 
    16:	{ 
    17:	  TPersist *p = new(object) TPersist(10,   20); 
    18:	  cout « *p « endl; 
    19:	  cout « "Address of object == " « tobject « endl; 
    20:	  cout « "Address of *p        == " « &(*p) « endl; 
    21:	  p->TPersist::~TPersist();                                        // Непосредственный вызов деструктора 
    22:	  //    p->TPersist();                                                        // Можно в ВС++ версии 4.0;   нельзя в 3.1 
    23:	  return 0; 
    24:	} 
    25: 
    26:	ostreamS operator« (ostreamS os, TPersist &p) 
    27:	{ 
    28:	  os « "x == " « p.x « ",  у == " « p.у; 
    29:	  return os; 
    30:	}
 
    В классе TPersist оператор new перегружается необычным способом. Вместо 
того чтобы выделить память для указателя, функция перегрузки оператора просто 
возвращает указатель р типа void. В программе в строке 17 оператор new 
используется для сохранения объекта класса TPersist в глобальном буфере, 
определенном в строке 13. Выражение new(object) вызывает перегруженную 
функцию-член оператора, передавая ей адреса символьного буфера object. 
Поскольку перегруженный оператор new просто возвращает переданный ему 
указатель р, этот несколько странно выглядящий оператор в действительности 
присваивает указателю р адрес объекта, а также вызывает конструктор класса 
TPersist для инициализации нового, выделенного в буфере объекта. 
    В программе отображаются адреса символьного буфера object и объекта в 
буфере. Эти адреса идентичны, что свидетельствует о размещении объекта в 
указанном месте. 
    Неплохим упражнением будет запуск PERSIST из отладчика Turbo Debugger. 
Проследите за содержимым буфера object во время пошагового выполнения кода с 
помощью клавиши <F8>. После выполнения строки 17 вы увидите, что в глобальном 
буфере появились значения 10 и 20; это доказывает, что перегруженный опера-: 
тор new не пользовался стандартными средствами выделения памяти в куче. 
    В строке 21 вызывается деструктор класса для объекта, адресуемого р. 
Поскольку объект содержится в глобальной переменной, может возникнуть 
необходимость в вызове деструктора именно таким способом для выполнения 
действий по очистке объекта. Для невиртуальных деструкторов следует 
использовать пряводящйся в листинге оператор следующего вида: 

    p->TPersist::~TPersist();     // Непосредственный вызов деструктора 

    C++ не распознает область действия объекта, сохраненного в буфере, и не 
может вызвать деструктор класса автоматически. Опять же, будет весьма 
поучительно выполнить этот оператор из Turbo Debugger. Если вы это сделаете, 
то увидите, как встраиваемые в код операторы деструктора обнуляют данные-члены 
объекта (строки 8 и 21). 
    Для виртуальных деструкторов, даже в случае с объектами производных 
классов, можно использовать следующую форму вызова деструктора: 

    p->TPersist();	// Непосредственный вызов деструктора 

    В Borland C++3.1 подобное упрощение синтаксиса запрещено.	


    15. Передовые средства C++     397 

    Инициализация членов объекта класса 

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

    class TAnyClass {	
    private:
       int x, у; 
    public: 
      TAnyClass() { x = 0; у = 0;  } 
       .....
    }

    В качестве альтернативы вы можете инициализировать члены так, как если бы 
они имели конструкторы: 

    class TAnyClass { 
    private: 
       int x, у; 
    public: 
       TAnyClass(): x(0), y(0) { } 
       .......
    };
 
    В этой версии конструктора TAnyClass членам х и у присваиваются нулевые 
значения аналогично тому, как это делалось с помощью присваиваний в предыдущем 
примере. Скомпилированный код один и тот же для обоих фрагментов, однако во 
втором случае инициализаторы данных-членов задаются до выполнения тела 
конструктора. (В данном случае тело конструктора пустое.) 
    Этот прием особенно полезен для инициализации объектов других классов. 
Например, предположим, что класс TAnyClass имеет член-объект класса TNewClass 
и конструктору класса TNewClass необходим один целочисленный параметр: 

    class TNewClass { 
       int x; 
    public: 
      TNewClass(int n) { x = n;  }	
      ....
    }

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

    class TAnyClass { 
    private: 
      int x; 
      TNewClass z;               // Объект-член класса
    public: 
       TAnyClass(int n): z(n) // Инициализация z
             { x = n; }                  // Инициализация x 
      .........
    }
     
    Член класса x, как и раньше, имеет тип int. Член z — объект класса TNewClass. 
Конструктор класса TAnyClass объявляется с целочисленным параметром 
n, использующимся для инициализации z перед выполнением тела конструктора. 
Внутри конструктора с помощью присваивания значение n запоминается в члене х. 
Если в классе имеется несколько объектов-членов, вы можете инициализировать 
их, перечислив и разделив запятыми, следующим образом: 

    class TAnyClass { 
    private: 
       int x;	
       TNewClass a, b, z;	// Три объекта-члена класса public: 
       TAnyClass(lnt n): a(n). b(n.), z(n) // Инициализация а, Ь и z 
           { x = n; } 	// Инициализация х 
      ......
    }; 
 

    Вложенные объявления в классе	

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


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

    Вложенный typedef следует использовать для экспорта типов данных из 
класса. Например, в классе TAnyClass typedef используется для объявления 
CLASS_COUNTER в качестве алиаса для int: 

    class TAnyClass { 
    public: 
       typedef int CLASS_COUNTER;      // Вложенное объявление typedef static 
       CLASS_COUN?ER ClassCount;  // Статическая переменная 
    public: 
       TAnyClass() { ClassCount++; }
       ~TAnyClass() { ClassCount--; } 
    };

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

    TAnyClass::CLASS_COUNTER TAnyClass::ClassCount; 

    Имя класса предшествует типу данных и имени переменной. Поскольку в классе 
TAnyClass существует typedef CLASS_COUNTER, в программе можно объявить 
переменную ClassCount, не зная действительного типа переменной. Без помощи со 
стороны программы класс может самостоятельно подсчитывать число созданных 
объектов его типа: 

    TAnyClass *cp1,  *ср2,  *срЗ;     // Объявление указателей на объекты класса. 
    ср1 = new TAnyClass;	// Создать объект класса 
    ср2 = new TAnyClass;	// 
    срЗ = new TAnyClass;	// 
    cout « "There are " « TAnyClass::ClassCount « " objects\n"; 

    Последний оператор отобразит 3. Опять же, имя класса предшествует ссылке 
на ClassCount. Также возможны сложные вложенные объявления. Внутри класса 
может объявляться структура: 

    class TAnyClass { 
    public: 
       struct ClassStruct { 
          int x; int y; 
       }; 
    };

    В программе может затем объявляться объект типа TAnyClass::ClassStruct: 

    TAnyClass::ClassStruct k; 

    k можно использовать точно так же, как и обычную структуру: 

       k.x = 1; 
       k.y = 2; 

    Дальнейшее развитие этой концепции приводит к вложенным объявлениям класса 
— один класс объявляется внутри другого: 

    class TAnyClass { 
    public: 
       class ClassClass { 
          int x; 
          int y; 
       public: 
          ClassClass() { x = 1; у = 2;  } ;
          nt GetX(void) { return x;   } 
          int GetY(void) { return y;   } 
       }; 
    }; 

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

    TAnyClass::ClassClass k; 
    cout « "k.x == " « k.GetX() « '\n'; 
    cout « "k.y == " « k.GetY() « '\n'; 

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

    Опция  fast this 

    В Borland C++ 3.1 был введен важный метод оптимизации, который теперь 
называется опцией fast this, с его помощью можно значительно улучшить 
быстродействие объектно-ориентированных программ. Как вам 


    Глава 15. Передовые средства C++  399

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

    TAnyClass anyvar; 

и вызывается функция-член класса TAnyClass 

    anyvar.AnyFunction();	

    функции AnyFunctionO передается указатель this, ссылающийся на объект 
класса anyvar. Образно выражаясь, this — способ адресации функциями-членами 
данных-членов объекта класса. 
    Для того чтобы избавиться от расходующих время и память передач указателя 
this в стеке, Borland C++ может передавать this в регистре, что экономит 
время, расходуемое на запоминание this в стеке и на ссылки внутри 
функции-члена через хранящийся в стеке указатель. Знание простой, но 
эффективной опции оптимизации fast this может домочь на порядок улучшить 
производительность, особенно в программах, в которых вызывается множество 
мелких функций-членов, — это типичная ситуация, характерная для большинства 
объектно-ориентированных приложений.
    Ближний указатель this передается в регистре si. Дальний указатель this 
передается в паре регистров ds:si, при этом компилятор автоматически сохраняет 
и восстанавливает значение ds в случае необходимости. Для включения опции fast 
this, находясь в DOS, скомпилируйте программу с опцией -ро. Например, можно 
ввести следующую команду: 

    Ьсс -ро myprog 

    Можно    также    установить    опцию    fast    this    из    интегрированной    среды 
Options\Project\Compiler\Code Generation и установки пункта FastThis. 
    В тексте программы, чтобы ввести в действие это соглашение, следует 
использовать следующую директиву #pragma: 

    #pragma option-po 

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

    #pragma option-po

    СОВЕТ

    Как   и  всякий  другой   вид  оптимизации,   применение  опции  fast   
this   не  дает   гарантии  получения положительных   результатов.   Одно   
из   очевидных   отрицательных   последствий   ее   использования   - 
уничтожение содержимого регистровой переменной si. В некоторых случаях можно 
обнаружить, что опция fast this ухудшает быстродействие. Для достижения 
наилучших результатов проверьте производительность вашего кода с включенной и 
выключенной опцией fast this. 

    Исключительные ситуации 

    Исключительные ситуации — новые "любимчики" ANSI C++, но до тех пор, пока 
вы не научитесь ими пользоваться, они могут показаться вам скорее 
дьявольскими, чем любимыми. Немногие программисты на C++ пользуются 
исключительными ситуациями в коммерческих программах — они слишком новы, и 
немногие компиляторы их поддерживают. Конечно, в некоторых библиотеках 
классов, таких как Borland's Object Windows (OWL), исключительные ситуации 
используются весьма интенсивно, так что важно понимать, как они работают. 

    ЗАМЕЧАНИЕ 

    Это введение в исключительные ситуации  —  переработанная глава 2 моей 
книги Mastering Windows Programming with Borland C++ 4. 

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


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

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

    Введение в исключительные ситуации 

    Теперь посмотрим, что на самом деле представляют собой исключительные 
ситуации в C++. Обнаружив состояние ошибки, функция может возбудить 
исключительную ситуацию, что вызовет такие последствия. 
    •   Функция объявляет, что возникло условие исключительной ситуации. Это 
может быть как ошибкой, так и любым другим обстоятельством, нуждающимся в 
специальной обработке. 
    •  Функция запрашивает решения проблемы обработчиком исключительной 
ситуации. Обработчик, если он существует, вызывается автоматически в ответ на 
посылку объекта исключительной ситуации. 
    В программе возбуждается исключительная ситуация с помощью выполнения 
оператора throw, обычно где-нибудь внутри функции: 

    throw 1; 

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

     throw "overflow"; 

    Это гораздо осмысленнее. Где-нибудь в другом месте программы обработчик 
строковых исключительных ситуаций может поймать и отобразить на дисплее 
посланный объект. В обработчике задается тип объекта (в этом примере — const 
char*) в выражении catch: 

    catch  (const char* message) {	.                          :            ,.   
         cout « "Error!  -- " « message « endl; 
        //   ... Другие действия в ответ на исключительную ситуацию.

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

    ЗАМЕЧАНИЕ 

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


    Глава 15. Передовые средства C++     401 

    Использование исключительных ситуаций 

    Что же представляет собой в точности исключительная ситуация? Ответ прост. 
Исключительные ситуации представляются объектами, которые очень похожи на 
аргументы функции. Это могут быть объекты любого типа, однако наиболее часто 
они представляют собой экземпляры класса. Например, вы можете объявить класс 
Overflow, он не нуждается в каком-либо содержимом, необходимо лишь имя: 

    class Overflow { }; 

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

    throw Overflow(); 

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

    catch (Overflow)  { 
        cout « "Overflow detected!" « endl; 
    }	

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

    catch (Overflow overObject) { 
       // ... 
     }

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

    class Overflow { 
       void Report()  { cout « "Error: overflow" « endl; } 
    };

    В классе Overflow объявляется функция-член ReportO, отображающая сообщение 
об ошибке (я использовал встраиваемую функцию исключительно для демонстрации, 
она может быть также и вызываемой функцией-членом). В операторе catch можно 
вызвать Report() объекта исключительной ситуации для отображения сообщения об ошибке: 

    catch (Overflow overObject)	{ 
       overObject.Report();	// Вызвать функцию Report брошенного объекта          
    }                    


    Возбуждение нескольких исключительных ситуаций 

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

    int AnyFunction() 
    { 
       if (conditionA)        // Если выполняется условие condition* 
         throw "Big trouble!";  // послать строковый объект 
      if (conditions)        // Если выполняется условие conditions 
        throw Overflow();     // послать объект класса Overflow 
     return 123;	// Если нет проблем, вернуть значение 
   } 

    Если условие conditionA справедливо (что бы оно ни значило), функция 
возбуждает строковую исключительную ситуацию с сообщением "Big trouble!". Если 
выполняется условие conditionB, функция посылает объект класса Overflow, 
создаваемый в данном случае с помощью обращения к конструктору класса по 
умолчанию. Если же функция не обнаружила ошибок, она возвращается обычным 
образом, передавая значение 123 тому, кто ее вызвал.	


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

    Вот три важных следствия из приведенного примера. 

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

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

    catch (Error e) { 
        // Обработка исключительной ситуации для класса Error 
    }
    catch (const char* message) { 
       /./ Обработка исключительной ситуации для строк 1 
     }


    Введение в блоки try 

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

    try { 
       int x = AnyFunction();	// Вызвать функцию 
    } 
    catch (Error e)  {              // Если функция возбудила исключительную ситуацию 
       e.Report();	// вызвать функцию Report объекта Error 
       exit(-1);                  //и прервать выполнение программы 

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

    try { 
    FunctionA();     // Испытать эту функцию во внешнем блоке 
       try {         // Вложенный блок try, пропускается в случае 
                      // исключительной ситуации
            FunctionB();  // Испытать эту функцию во внутреннем блоке 
       } 
       catch (Error e) {   // Перехватить исключительную ситуацию, возбуждаемую 
                                     // функцией FunctionB()
           e.Report();       // Вызвать функцию объекта исключительной ситуации 
                                    // Report {) 
       } 
    } 
    catch (Error e) {   // Перехватить исключительную ситуацию, возбуждаемую 
                                  // функцией FunctionA()
       e.Report();        // Вызвать функцию объекта исключительной ситуации 
                                 // Report() 
    }
 

    Глава 15. Передовые средства C++     403 

    Использование блоков try 

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

    try {	
        cout «"Неге we go!" « endl;	
        int х = AnyFunction();	
       cout «	"x == " « x « endl;	
    }	

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

    ЗАМЕЧАНИЕ 

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

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

    try { 
       cout « "Неге we go!" « endl, 
       int x = AnyFunction();	
       cout « "x == " « x « endl; 
     }	
     catch (char* message) { 
       cout « "Error1  -- " « message « endl, 
       exit(-1);   // Необязательно 
    catch (Overflow) { 
       cout « "Overflow!" « endl, 
       exit(-2);   // Необязательно 
    } 
   // В этом месте программа продолжает выполнение как обычно 

    В этом более пространном коде сначала испытывается вызов функции 
AnyFunction(). Если функция завершилась нормально, ее результат присваивается 
переменной х, которая отображается в следующем операторе записи в стандартный 
поток вывода. Если исключительных ситуаций не было, оба оператора catch 
пропускаются, поскольку нет объектов исключительных ситуаций объявленных 
типов, которые нужно было бы перехватывать. Конечно, если функция AnyFunction()
возбуждает исключительную ситуацию, выполнение блока try немедленно 
прерывается, посланные объекты перехватываются соответствующими операторами 
catch, a именно тем из них, в котором объявлен параметр типа, соответствующего 
типу данных посланного объекта. 
    В данном примере в операторах catch вызывается библиотечная функция exitO 
для аварийного завершения программы. Приведенная реакция не обязательна — вам 
предоставляется самому решать, что делать в ответ на возбуждение исключительной 
ситуации.	
    Вы можете вставить один, два, три или более операторов catch после блока 
try для обработки исключительных ситуаций различных типов. В этом примере 
два оператора catch обрабатывают исключительные ситуации для строк и класса 
Overflow. Любые другие типы исключительных ситуаций, не обрабатываемые 
операторами catch, передаются вверх по цепочке вызовов. Например, предположим, 
что функция AnyFunction также возбуждает исключительную ситуацию типа 
NewException. Если в функции g() был вызван упомянутый выше код, объект этой 
исключительной ситуации будет передаваться наверх в g(), так как в этом коде 
обрабатываются только исключительные ситуации типов char* и Overflow. 
    В конце концов, если ни один оператор catch подходящего типа не был найден 
в цепочке вызовов, ведущих к исключительной ситуации, выполнение программы 
прерывается вызовом специального обработчика исполняющей системы. Чтобы этого 
не произошло, следует запрограммировать все операторы catch для всех возможных 
типов посылаемых объектов 

   ЗАМЕЧАНИЕ

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


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

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

    Программирование с исключительными ситуациями 

    Пример программы EXCEPT. CPP (листинг 15.16) демонстрирует использование 
исключительных ситуаций ANSI C++. Вы можете скомпилировать и запустить 
программу из DOS или WINDOWS. Из приглашения DOS следует ввести bcc except, а 
затем имя программы для запуска получившегося в результате исполняемого файла 
EXCEPT.EXE. Или же из интегрированной среды скомпилируйте и запустите 
программу в качестве Easy WtH-приложения. 

    ЗАМЕЧАНИЕ 

    Для демонстрации в модуле ЕХСЕРТ.СРР имеется оператор, который не 
выполняет никаких действий, что служит причиной предупреждений компилятора 
"Unreachabte code at line 78" (Недостижимый код в строке 78). Я включил этот 
оператор throw для отладки. Если вы сделаете ошибку, модифицировав программу 
(конечно, она хранится на диске, и вы не должны этого делать), оператор throw 
в строке 78 создаст объект класса Error с помощью вызова конструктора по 
умолчанию. Изучите объявление класса. Созданный таким способом, объект 
Error отобраажит сообщение, свидетельствующее об ошибке в исходном коде. 


    Листинг 15.16.  EXCEPT.CPP (использование исключительных ситуаций ANSI C++) 

    1:	// Демонстрация исключительных ситуаций ANSI C++ 
    2: 
    3:	#include <iostream.h> 
    4:	#include <math.h> 
    5: 
    6:	// Прототипы функций 
    7:	void Instruct(); 
    8:	double Pow(double b, double e); 
    9:	double Power(double b, double e); 
   10: 
    11:	// Классы исключительных ситуаций 
    12:	class Error { 
    13:	  double b;   // Основание 
    14:	  double e;   // Показатель степени 
    15:	public: 
    16:	   Error() 
    17:	        { cout « "Error in source code!" « endl; } 
    18:	  Error(double bb, double ее) 
    19:	       : b(bb), e(ee) { }
    20:	  void Report(); 
    21:	}; . 
    22: 
    23:	int main() 
    24:	{ 
    25:	Instruct(); 
    26:	try { 
    27:	  double base,  exponent,   result; 
    28:	  cout « "base? "; 
    29:	  cin » base; 
    30:	  cout « "exponent? "; 
    31:	  cin » exponent; 
    32:	  result = Power(base, exponent); 
    33:	  cout « "result == " « result « endl; 
    34:	} 
    35:	catch (ErrorS, e) { 
    36:	  e.Report(); 
    37:	   return -1; 
    38:      } 
    39:      return 0; 
    40:   } 
    41: 


    Глава 15. Передовые средства C++     405 

    42:	void Instruct() 
    43:	{ 
    44:	  cout « "Power Demonstration\n\n";             
    45:	  cout « "This program displays the result of raising\n"; 
    46:	  cout « "a value (base) to a power (exponent). To\n"; 
    47:	  cout « "force an exception, enter a negative base\n"; 
    48:	  cout « "and a fractional exponent (e.g. -4 and 1.5)\n"; 
    49:	  cout « "Or, enter a zero base and an exponent less tnan\n"; 
    50:	  cout « "zero.\n\n"; 
    51:	} 
    52: 
    53:	// Подфункция возведения в степень 
    54:	double Pow(double b, double e) 
    55:	{ 
    56:	  return exp(e * log(b) ); 
    57:	}
    58: 
    59:	// Результирующий b возведенный в степень е 
    60:	double Power(double b, double e) 
    61:	{ 
    62:	if (b > 0.0) return Pow(b, e); 
    63:	if (b < 0.0) { 
    64:	   double ipart; 
    65:	  double fpart = modf(e, Sipart); 
    66:	   if (fpart == 0) { 
    67:	      if (fmod(ipart, 2) != 0) // Таким образом, ipart - нечетное число 
    68:	          return -Pow(-b, e); 
    69:	      else 
    70:	         return Pow(-b, e); 
    71:	   } else 
    72:	      throw Error(b, e); 
    73:	} else { 
    74:	   if (e == 0.0) return 1.0; 
    75:	   if (e < 1.0) throw Error(b, e); 
    76:	   return 0.0; 
    77:	} 
    78:	throw ErrprQ; // В этом месте будет предупреждение 
    79:     } 
    80: 
    81:	// Отображение значений, вызвавших исключительную ситуацию 
    82:	void 
    83:	Error::Report() 
    84:	{ 
    85:	  cout « "Domain error:" 
    86:	    « " base:" « b 
    87:	    « " exponent:" « e 
    88:	    « endl; 
    89:	} 

    В EXCEPT реализуется функция Power(), способная возвести любое 
действительное число в любую действительную степень. Конечно, два условия 
исключительной ситуации заставляют функцию возбуждать исключительную ситуацию. 

    •   Возведение отрицательного числа в дробную степень. 
    •   Возведение нуля в отрицательную степень. 

    Для обработки подобных ситуаций наилучшим образом подходят исключительные 
ситуации. Имеются также следующие альтернативы — прервать выполнение программы 
(плохо), возвратить специальное значение (не подходит) или установить 
глобальный флаг ошибки (плохой стиль программирования). 
    Вместо применения этих относительно неудачных решений, функция Power 
возбуждает исключительную ситуацию, представленную объектом класса Error. 
Например, при попытке возвести отрицательное число в дробную степень функция 
Power выполняет следующий оператор, создающий объект класса Error, 
инициализированный значениями b и е: 

    throw Error(b,  e); 

    Вернемся к функции main(). Функция Power вызывается внутри блока try, часть 
которого приведем еще раз: 
    try {       
        double ba.se, exponent, result    
         // ... запрашиваются основание и показатель степени 
        result = Power(base, exponent); 
        cout « "result == " « result « endl; 
    } 


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

    Если функция Power возбуждает исключительную ситуацию, присваивание 
переменной result и следующий оператор вывода пропускаются, а блок try 
немедленно завершается. Оператор catch следует сразу же за блоком try: 

    catch (Errors e) {	
       e.Report(); 
       return -1; } 
    return 0; 

    Если функция Power возбуждает исключительную ситуацию, объект е класса 
Error перехватывается по ссылке. В операторе catch вызывается функция-член 
Report класса Error для объекта исключительной ситуации е, отображающая 
значения, приведшие к ненормальному завершению функции Power. 

    ЗАМЕЧАНИЕ 

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

    Итак, программа осуществляет полный контроль над исключительными 
ситуациями. Пример заканчивает работу с кодом завершения -1, если были ошибки, 
или 0, если ошибок не было. Это обычная практика для программ, хотя 
операционные системы DOS и Windows игнорируют возвращаемые программами значения. 
    Бывают случаи, когда завершение программы нежелательно и возникает 
необходимость повторить операторы, приведшие к исключительной ситуации, — в 
данном случае, указав для new новые значения. Далее в этой главе я объясню, 
как использовать исключительные ситуации в этом случае, но сначала вы должны 
узнать об альтернативных формах объявления функций, возбуждающих 
исключительные ситуации. 

    Объявления исключительных ситуаций 

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

    void AnyFunction() throw(Error); 

    Следующее за именем функции и списком параметров (пустым в данном случае) 
выражение throw() указывает, что функция AnyFunctionO может возбуждать 
исключительные ситуации типа Error. Подобное объявление указывает компилятору, 
что функции AnyFunctionO не разрешается возбуждать исключительные ситуации 
других типов. Если вы обнаружили функцию, объявленную таким способом, вам не 
придется охотиться за операторами throw в теле функции для того, чтобы найти 
все возможные типы исключительных ситуаций. Объявление говорит вам о том, что 
функция AnyFunctionO возбуждает исключительную ситуацию типа класса Error. 
    Для задания функции, возбуждающей исключительные ситуации нескольких 
типов, следует перечислить типы данных объектов исключительных ситуаций, 
отделенные друг от друга запятыми в круглых скобках: 

    void AnyFunction() throw(Error, char*, OtherType); 


    Написание обработчиков исключительных ситуаций 

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


    Глава 15. Передовые средства C++     407 

    Power Demonstration 

    This program displays the result of raising a ualue (base) to a power 
(exponent). To force an exception, enter a negative base and a fractional 
exponent (e.g. -4 and 1.5) Or, enter a zero base and an exponent less than zero. 

    base? -4 
    exponent? 1.5 
    Domain error: base:-4 exponent: 1.5 
    Error detected: Try again! 
    base? 9 
    exponent? -9.93 
    Domain error: base:в exponent:-9.99 
    Error detected: Try againI 
    base? 2 
    exponent? 16 
    result ** 65536 
    Program is ending normally. 

    Рис. 15.1. Результат работы программы ЕХСЕРТ2 


    Листинг 15.17. ЕХСЕРТ2.СРР (обработка ошибок с помощью исключительных ситуаций) 

    1:	// Демонстрация обработки	ошибок с помощью исключительных ситуаций 
    2: 
    3:	#include <iostream.h> 
    4:	#include <math.h> 
    5: 
    6:	// Неполное объявление класса исключительных ситуаций, необходимое 
    7:	// для того, чтобы на него могла сослаться функция Power() 
    8:	Class Error; 
    9: 
    10:	// Прототипы функций (отметьте объявленную ситуацию в функции Power)   
    11:	void Instruct();	
    12:	void Run(); 
    13:	double Pow(double b, double e); 
    14:	double Power(double b, double e) throw(Error); 
    15:		
    16:	// Полное объявление класса объектов исключительной ситуации 
    17: 	class Error {
    18:	   double b;  // Base
    19:	   double e;  // Exponent	
    20:	public: 
    21:	   Error() 
    22:	        { cout « "Error in source code!" « endl; } 
    23:	   Error( double bb, double	ее) 
    24:	            : b(bb). e(ee) { } 
    25:	   void Report(); 
    26:	}; 
    27: 
    28:	int main () 
    29:	{ 
    30:	  Instruct(); 
    31:	  for (;;) { 
    32:	     try { 
    33:	        Run(); 
    34:	        cout « "Program is	ending normally. " « endl; 
    35:	        return 0; // Только если не было исключительных ситуаций 
    36:	    } 
    37:	    catch (...) { 
    38:	       cout « "Error detected: Try again!" « endl; 
    39:	    } 
    40:	  } 
    41:	} 
    42: 
    43:	void Instruct()
    44:	{ 


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

    45:	  cout « "Power Demonstration\n\n"; 
    46:	  cout « "This program displays the result of raising\n"; 
    47:	  cout « "a value (base) to a power (exponent). To\n"; 
    48:	  cout « "force an exception, enter a negative base\n"; 
    49:	  cout « "and a fractional exponent (e.g. -4 and 1. 5)\n"; 
    50:	  cout « "Or, enter a zero base and an exponent less than\n"; 
    51:	  cout « "zero.\n\n"; 
    52:	} 
    53: 
    54:	// Выполнить программ (повторно вызывается из main ()) 
    55:	void Run() 
    56:	{ 
    57:	  try { 
    58:	     double base, exponent, result; 
    59:	     cout « "base? "; 
    60:	     cin » base; 
    61:	     cout « "exponent? "; 
    62:	     cin » exponent; 
    63:	     result = Power(base, exponent); 
    64:	     cout « "result == " « result «	endl; 
    65:	  } 
    66:	  catch (Error& e) { 
    67:	    e.ReportO; // Отобразить сообщение об ошибке 
    68:	    throw e; // Передать исключительную ситуацию в место вызова 
    69:	  } 
   70:	} 
   71: 
   72:	// Подфункция для Power 
   73:	double Pow(double b, double e) 
   74:	{ 
   75:	  return exp(e * log(b)); 
   76:	} 
   77: 
   78:	// Возвести b в степень е 
   79:	double Power(double b, double e) throw(Error) 
   80:	{ 
   81:	if (b > 0.0) return Pow(b, e); 
   82:	if (b < 0.0) { 
   83:	   double ipart; 
   84:	   double fpart = nodf(e, Sipart); 
   85:	   if (fpart == 0) { 
   86:	      if (fmod( ipart, 2) != 0) // т.o. ipart - дополнение 
   87:	         return -Pow(-b, e); 
   88:	      else 
   89:	        return Pow(-b, e); 
   90:	    } else 
   91:	       throw Error(b, e); 
   92:	 } else { 
   93:	   if (e == 0.0) return 1.0; 
   94:	   if (e < 1.0) throw Error(b, e); 
   95:	   return 0.0; 
   96:	  } 
   97: 	} 
   98:
   99:	// Реализация функции-члена Report ()	класса Error() 
   100:	// Отобразить значения, вызвавшие исключительную ситуацию 
   101:	void 
   102:	Error::Report() 
   103:	{ 
   104:	  cout « "Domain error: " 
   105:	  « " base:" « b 
   106:	  « " exponent: " « e 
   107:	  « endl;	. 
   108:	} 

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


    Глава 15. Передовые средства C++     409 

    В больших программах прототипы функций и неполные объявления класса лучше 
всего размещать в 1 нескольких заголовочных файлах я модулях. Но, коиечно, не 
обязательно придерживаться этой конденяин » " небольших демонстрапионных 
модулях, подобных EXCEPlZCPP. 
    Для повторного запроса вводимых значений в функции mainO используется 
"бесконечный" цикл for следующего вида: 

    for (;;) { 
       // ..  повторять это "до бесконечности" 
    } 

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

    try { 
      Run(); 
      // Любые обычные операторы или return 
    } 
    catch (...)  { 
       // Операторы обработчика любых типов исключительных ситуаций 
    } 

    Если в функции Run() не было исключительных ситуаций, выполняются 
операторы, следующие за вызовом этой функции. Эти операторы могут быть какими 
угодно: например, оператор return для завершения программы или же библиотечная 
функция, подобна exit().	
    Когда используется оператор catch с многоточием в скобках, он может 
обрабатывать любые исключительные ситуации, нигде больше не обрабатываемые. 
Эти исключительные ситуации передаются назад из функции Run() (так как это 
единственная функция, вызванная из main()). Поэтому такой прием гарантирует, 
что никакие исключительные ситуации не прервут выполнение программы с помощью 
выполняемых по умолчанию подпрограмм исполняющей системы. 
    В листинге функция Run() очень похожа на вызывающую ее функцию main(). В 
ней с использованием блока try вызывается функция Pow() (в которой, в свою 
очередь, вызывается Power()) для перехвата любых возникших исключительных 
ситуаций. Конечно, оператор catch слегка отличается: 

    catch (Errors e) 
      e.Report();	// Отобразить сообщение об ошибке 
      throw e;	// Передать исключительную ситуацию обратно в main() 
    } 

    Если функция Power() возбуждает исключительную ситуацию, в операторе catch 
вызывается функция-член Report() для отображения сообщения об ошибке. После 
этого в операторе catch возбуждается еще одна исключительная ситуация. Это 
служит причиной немедленного завершения выполнения функции Run() и передачи 
объекта ошибки назад, в место вызова функции. Универсальный оператор catch (с 
многоточием в круглых скобках) в функции main() принимает это сообщение об 
ошибке, пропуская оператор return, завершающий программу, и заставляет цикл повторяться.	

    Необработанные исключительные ситуации 

    В предыдущем разделе демонстрировалось, как обработать все возможные 
исключительные ситуации, однако вы можете задуматься: "А что произойдет, если 
возникшая исключительная ситуация нигде не обработается?" Ответ прост и 
логичен. Необработанные исключительные ситуации передаются вверх по цепочке 
вызовов (имеется в виду цепочка вызовов одних функций другими) до тех пор, 
пока не встретится соответствующий им оператор catch или пока больше не 
останется нерассмотренных обработчиков исключительных ситуаций. Если 
произойдет последнее, для обработки исключительной ситуации вызывается одна из 
трех специальных функций, автоматически присоединяемых к каждой программе на 
C++, использующей исключительные ситуации. Эти функции имеют имена 
unexpected(), terminate() и abort(). Они вызываются в соответствии со следующими правилами. 


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

    Если в программе возникли исключительные ситуации, которые не 
обрабатываются оператором catch, то вызывается функция unexpected(). 
Исключительные ситуации, которые не обрабатываются ни одним оператором catch, 
называются непредвиденными исключительными ситуациями. По-умолчанию функция 
unexpected() вызывает функцию terminate(), назначение которой поясняется ниже. 
    Непредвиденные исключительные ситуации могут возбуждаться программой при 
обнаружении повреждения стека или деструктором класса (опасная практика, 
резервируемая только для наиболее критических проблем), вследствие чего 
вызывается функция terminate(). По умолчанию функция terminate() вызывает 
функцию abort(), назначение которой объясняется ниже. 
    Функция abort() находится ниже всех на тотемном столбе и немедленно 
прерывает выполнение программы. Она никогда не вызывается непосредственно. 
Если не обработаны все возможные исключительные ситуации и не приняты 
дополнительные меры для перепрограммирования функций unexpected() и, возможно, 
terminate(), необработанная исключительная ситуация аварийно прервет выполнение 
программы путем обращения к abort(). Если программа добралась до этой стадии, 
ничто уже не поможет вам предотвратить ее завершение. (Между нами говоря, если 
программа непредвиденно завершается, вам следует заняться обработкой ошибок!) 

    ЗАМЕЧАНИЕ 

    В исходном коде нсволняющея системы Borland C++ функция terminate не вызвает 
непосредственно функцию abort(), как это диктует ANSI C++, В Borland C++ 4.5 функция 
termiate() просто завершает программу. Поскольку вы не можете заменить функцию abort(), 
это небольшое несоответствие не имеет отрицательных последствий, во вообще полезно 
думать, что в стандартной ситуации функция terminate() вызывает abort().

    Для демонстрации того, что происходит, когда программа не обрабатывает все 
возможные исключительные ситуации, сохраните копию ЕХСЕРТ2.СРР под другим 
именем (я использовал Х.СРР). Перепишите в ней функцию main() следующим образом: 

    int main() { 
      Instruct(); 
      Run(); 
      return 0; 
   } 

    Powег Demonstration	
    rhis program displays the result of raising	
    в value (bass) to a power (exponent). To	
    Force an exception, enter a negative base	
    Domian error: Ьаse

     Program Aborted

    Рис. 15.2. Windows- и EasyWin-приложения выводят это сообщение, если они 
завершаются из-за необработанной исключительной ситуации 

    Любые необработанные исключительные ситуации из функции Run() заставляют 
программу вызывать функцию unexpected(), вызывающую функцию terminate(), 
которая, в свою очередь, вызывает функцию abort(), завершающую выполнение 
программы. Приложения для DOS, завершающиеся подобным образом, выводят 
сообщение "Abnormal program termination" (Ненормальное завершение программы). 
WINDOWS и EasyWin-приложения завершаются, отображая окно диалога, показанное 
на рис. 15.2. За исключением простейших примеров и тестовых программ, вы 
обязаны сделать все возможное для предотвращения такого непредвиденного завершения. 


    Глава 16. Передовые средства C++ 411 

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

    Замена функций unexpected() и terminate() 

    Для задания адреса вашей собственной функции-обработчика непредвиденных 
исключительных ситуаций следует использовать функцию set_unexpected(), 
объявленную в заголовочном файле EXCEPT. H. Функция должна иметь тип 
unexpected_function, не иметь аргументов и ничего не возвращать. 
    Ваш обработчик непредвиденных исключительных ситуаций может возбуждать 
исключительную ситуацию, в этом случае поиски оператора catch начинаются с 
того места, откуда первоначально был вызван обработчик. 
    Для задания адреса вашей собственной функции-обработчика завершения 
программы следует использовать функцию set_terminate(), также объявленную в 
заголовочном файле ЕХСЕРТ.Н. Функция должна иметь тип terminate_function, не 
иметь параметров и ничего не возвращать. 
    Обе функции — set_unexpected() и set_terminate() — возвращают адрес 
текущей функции-обработчика. Вы можете сохранить и затем восстановить 
существующие обработчики, запомнив их адреса в переменных, а затем передав их 
обратно функциям. Например, сначала объявляется прототип функции-обработчика: 

    void unexpectedHandler(); 

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

    set_unexpected(unexpectedHandler); 

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

    unexpected_function savedAdress;	// Объявить адресную переменную 
    savedAdress = set_unexpected(unexpectedHandler);	// Установить обработчик 
    // ... Новый обработчик перехватывает непредвиденные исключительные ситуации	-. 
    set_unexpected(savedAdress);    // Восстановить первоначальный обработчик 
    // ... Новый обработчик больше не используется 

    Пользовательская функция terminate() устанавливается аналогично, только 
вместо set_unexpected() вызывается функция set_terminate(), возвращающая адрес 
типа terminate_function. 
    В листинге 15.18 демонстрируется установка обработчиков непредвиденных 
исключительных ситуаций и завершения. В программе также демонстрируется 
приемлемый способ обработки непредвиденных исключительных ситуаций неизвестных 
типов, которые могут возбуждаться в плохо документированных библиотечных 
функциях. Вы можете скомпилировать и запустить следующую программу как DOS- 
или Easy Win-приложение. 

    Листинг 15.18.  UNEXPECT.CPP (установка функций-обработчиков unexpected() и terminate()) 

    1:	#include <iostrearo. h> 
    2:	#include <except.h> 
    3: 
    4:	#define MAXERR 10 
    5: 
    6:	// Класс, используемый, когда число ошибок превышает допустимое 
    7:	class HaxError 
    8:	{ 
    9:	);
    10: 
    11:	// Обычный класс исключительных ситуаций 
    12:	class Error {      
    13:	public:      
    14:	  Error();    //Конструктор 
    15:	  void Say();  // Функция, сообщающая о числе ошибок 
    16:	private: 
    17:	  static int count; // Доступен только в этом классе /      ' 
    18:	}; 
    19:	
    20:	// Прототипы функций, возбуждающих исключительные ситуацищ..; , 
    21:	void Run() throw(Error);                        
    22:	void trapper(); 


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

    23:	void zapper(); 
    24: 
    25:	// Глобальный статический счетчик числа объектов класса Error 
    26:	int Error::count; 
    27: 
    28:	void main()
    29:	{ 
    30:	  set_unexpected(trapper); 
    31:	  set_terminate(zapper); 
    32:	  for (;;) 
    33:	  { 
    34:	     try { 
    35:	       Run(); 
    36:	     } 
    37:	     catch (Error e)  { 
    38:	        e.Say(); 
    39:	     } 
    40:	  } 
    41:	} 
    42: 
    43:	// Функция, возбуждающая исключительную ситуацию 
    44:	void Run() throw(Error) 
    45:	{ 
    46:	   // throw ErrorO; 
    47:	   throw "An unknown exception object"; 
    48:	} 
    49: 
    50:	// Пользовательский обработчик непредвиденных ошибок 
    51:	void trapperQ 
    52:	{ 
    53:	  cout « endl « "Inside trapper function" « endl; 
    54:	  throw Error(); 
    55:	} 
    56: 
    57:	// Пользовательский обработчик завершения ошибки 
    58:	void zapper() 
    59:	{ 
    60:	  cout « endl « "Inside terminate function" « endl; 
    61:	  cout « endl « "Exiting program with error code -1" « endl; 
    62:	  exit(-1); 
    63:	} 
    64: 
    65:	// Конструктор класса Error 
    66:	Error: :Error() 
    67:	{ 
    68:	  count++; 
    69:	  if (count > MAXERR) 
    70:	     throw MaxError(); 
    71:	} 
    72: 
    73:	// Функция класса Error, выводящая сообщения 
    74:	void Error::Say() 
    75:	{ 
    76:	  cout « "Error: count = " « count « endl; 
    77:	}
 
    При запуске UNEXPECT выведутся десять сообщений об ошибках перед 
завершением. На рис. 15.3 демонстрируется результат работы программы. 
    В программе используются два класса исключительных ситуаций. Объект класса 
МахЕггог, не имеющего данных или функций, посылается, когда число ошибок 
превышает константу MAXERR, заданную в этом примере равной 10. Объекты класса 
Error посылаются так же, как и в предыдущих примерах. 
    Конструктор класса Error, располагающийся ближе к концу листинга, 
инкрементирует.статический член класса count. Поскольку член count — 
статический, существует только один экземпляр этого значения, и он существует 
до тех пор, пока программа не завершится. Закрыв член count в классе, мы 
обезопасили это важное значение от изменения в операторах программы вне 
класса. Только функции-члены класса Error имеют непосредственный доступ к 
счетчику ошибок программы. Функция-член Say() отображает значение члена count. 
    На пути к созданию объекта класса Error, если член count больше или равен 
константе MAXERR, конструктор возбуждает исключительную ситуацию типа 
МахЕггог. При возбуждении исключительной ситуации объект класса не создается, 
поэтому создание объекта класса Error прерывается (более подробно об этом вы 
узнаете позже). 


    Глава 15. Передовые средства C++     413 

    Inside trapper function 
    Error: count = 1

    Inside trapper function 
    Error: count = 2 

    Insade trapper function 
    Error: count = 3 

    inside trapper function 
    Error: count = 4	

    Inside trapper function 
    Error: count = 5

    Inside trapper function 
    Error: count = 6 

    Inside trapper function 
    Error: count = 7

    Inside trapper function 
    Error: count = 8 

    Inside trapper function 
    Error: count = 9 

    Inside trapper function 
    Error: count = 10 

    Inside trapper function 

    Inside terminate function 

    Exiting program with error code -1 


    Рис. 15.3. Результат работы программы UNEXPECT 


    В функции main() устанавливаются функции-обработчики, замещающие по
умолчанию   функции unexpected() и terminate(). В основной программе затем 
выполняется "бесконечный" цикл for, похожий на те, что вы уже видели раньше. 
Внутри цикла в блоке try вызывается функция Run(), и оператор catch перехватывает 
все объекты класса Error, которые посылает Run(). При возбуждении исключительной 
ситуации catch отображает текущее значение счетчика ошибок путем обращения к 
функции-члену Say() посланного объекта. 
    Функция Run() всегда возбуждает исключительную ситуацию, моделируя 
возникновение нескольких ошибок, которые может понадобиться обработать в более 
обширных программах. Пользовательский обработчик непредвиденных исключительных 
ситуаций trapperO выводит сообщение, свидетельствующее о вызове этой функции. 
Как показано на рис. 5.3, это происходит после.возбуждения 10 исключительных ситуаций. 
    Обработчик непредвиденных исключительных ситуаций может возбудить еще одну 
исключительную ситуацию для продолжения программы. В данном примере следующий 
оператор посылает новый экземпляр класса Error: 

    throw Error(); 

    Поскольку наш обработчик непредвиденных исключительных ситуаций не 
вызывается до тех пор, пока не возникнут 10 исключительных ситуаций, тем не 
менее создание объекта класса Error заставляет конструктор этого класса 
возбудить еще одну исключительную ситуацию типа MaxError (см. конструктор 
класса Error). Этот тип ошибки не поддерживается в операторе catch, поэтому 
вызывается обработчик завершения программы zapper. Функция завершения не может 
возбудить исключительную ситуацию. (Функции завершения и непредвиденных 
исключительных ситуаций также не могут выполнить оператор return).	


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

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

    void Run() throw(Error) 
    { 
      throw "An unknown exception object"; 
    }

    Когда вы скомпилируете и запустите программу, обработчик непредвиденных 
ситуаций trapper() будет вызываться для каждой исключительной ситуации, 
возбуждаемой функцией Run(). Это происходит потому, что не существует 
оператора catch для объектов исключительных ситуаций типа const char* или 
char*. Функция trapperO, тем не менее, транслирует нераспознанные объекты 
исключительных ситуаций, посылая объект известного типа, в данном случае, 
Error. Новая возбужденная исключительная ситуация продолжает выполнение 
программы в операторе catch внутри функции main(), который обрабатывает 
транслированную исключительную ситуацию. 
    В конце концов, в программе максимальное число ошибок будет превышено, и 
объект класса МахЕггог будет послан конструктором Error. В результате 
вызовется обработчик завершения программы путем обращения к библиотечной 
функции exit()
. 
    Исключительные ситуации и локальные объекты 

    С помощью специальной опции компилятора локальные объекты, оставшиеся в 
стеке, автоматически уничтожаются возбуждением исключительной ситуации, т.е. 
вызываются деструкторы этих объектов. Для разрешения автоматического 
уничтожения задайте опцию -xd для автономного компилятора. Или же из 
интегрированной среды выберите Options\Project..., откройте пункт C++ Options 
и выберите подпункт Exception handling\RTTI. (RTTI — Runtime Type Information, 
информация о типах времени исполнения). Убедитесь, что установлены пункты 
Enable exceptions и Enable destructor cleanup. 
    
    ЗАМЕЧАНИЕ

    Вы обязательно должны устанваливать автоматическую очистку деструкторами
для ObjectWindows-программ (OWL).

    В целом автоматическое уничтожение необходимо для функций, которые 

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

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

    int AnyFunction() 
    { 
      AnyClass object(123); 
       // ... 
       if (condition) throw Error(); 
       return object.Value(); 
    )

    Функция создает в стеке объект класса AnyClass. Если события развиваются 
нормально, при завершении функции вызовется деструктор объекта. Однако, если 
функция завершится исключительной ситуацией, деструктор объекта не вызовется, 
что в некоторых случаях может привести к серьезным проблемам, например, если в 
программе не освобождается выделенная для объекта память. 
    Использование ключа -xd автономного компилятора или аналогичных установок 
в интегрированной среде гарантирует, что в подобных случаях деструктор для 
object будет вызван по завершению функции в результате возбуждения 
исключительной ситуации. Обычно такая установка желательна, однако ее 
использование gриводит к накладным расходам, их можно избежать, если в вашей 
программе нет локальных объектов в функциях, в которых возбуждаются 
исключительные ситуации. (Не забудьте, что вы обязаны в любом случае 
установить опцию автоматического уничтожения для программ, использующих OWL.) 


    Глава 15. Передовые средства C++     415 

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

    int AnyFunction()
    { 
      AnyClass *p = new AnyClass(123);
      // ... 
      if (condition) {
        delete p;	//  Вы ОБЯЗАНЫ удалить в этом месте р 
        throw Error();      // Немедленно завершить функцию AnyFunction 
      } 
      delete p;	// Не выполняется, если условие condition истинно 
      return 123;	// Нормальное возвращение. Не выполняется, если 
                                    // возбуждалась исключительная ситуация
    } 

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

    int AnyFunction() 
    { 
      AnyClass *p = new AnyClass{123); 
      // ... 
      delete p; 
      if (condition) throw Error(); 
      return 123; 
    }

    Используйте флаг, подобный condition, чтобы указать наличие ошибок, 
удалить все динамически выделенные объекты, а затем возбудить исключительную ситуацию. 

    Исключительные ситуации и конструкторы 

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

    class AnyClass { 
    public: 
      AnyClass()
         { if (condition) throw Error(); } 
     ~AnyClass() { } 
   }; 

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

    class AnyClass { 
       OtherClass x; 
    public: 
       AnyClass(): x(123) 
          { if (condition) throw ErrorO;   } 
          ~AnyClass() { } 
    }; 

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

    class AnyClass { 
      OtherClass a, b, с; 
    public: 
      AnyClass: a(), b(). c() { } 
      ~AnyClassO { } 
    }; 


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

    Если конструктор объекта b возбуждает исключительную ситуацию, объект с не 
создается, как и объект класса AnyCIass. Деструктор объекта а вызывается как 
обычно (поскольку он был полностью создан до того, как b возбудил 
исключительную ситуацию), но деструкторы для объектов Ь, с и AnyCIass не вызываются. 

    Возбуждение исключительных ситуаций 

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

    Class Error { 
       void Raise()
	{ throw Error();  }  
    }; 

    Если е — объект класса Error, оператор 

    e.Raise();	// Возбудить исключительную ситуацию класса Error 

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

    throw е;	// Возбуждение исключительной ситуации с суцествующин объектом 


    Исключительные ситуации и управление распределением памяти 

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

    ЗАМЕЧАНИЕ 

    Если вы хотите чтобы new действоввал no-старому, возвращая нуль в случае возникновения 
ошибок, включите в программу заголовочный файл  NEW.H и добавьте в функцию main()
следующий оператор:

     set_new_handler(0) ;

    Раньше вы, вероятно, написали бы следующий код: 

    AnyCIass *p;		// Объявить указатель на AnyCIass 
    р = new AnyClass();	// Создать объект типа AnyCIass, адресуемый р 
    if (р == 0} Error();		// Вызвать процедуру, если не хватает памяти 

    Если после использования оператора new для выделения памяти объекту класса 
AnyCIass указатель р содержит нуль, это значит, что new не удалось выделить 
достаточно памяти для создания объекта. Сложность в том, что необходимо 
выполнять проверку возвращаемого значения new после каждого использования оператора. 
    Исключительные ситуации предлагают иной способ использования new. Теперь 
при нехватке памяти оператop new возбуждает исключительную ситуацию типа 
xalloc, определенного в заголовочном файле EХСЕРТ.Н следующим образом: 

    class xalloc : public xmsg { 
    public: 
        xalloc(const string &msg, size_t size), 
       size_t requested() const; 
       void raise() throw(xalloc); 
   private: 
      size_t siz; 
   }; 

    Для использования исключительных ситуаций вместе с new заключите 
операторы, выделяющие память, в try, за которым должен следовать оператор 
catch для объекта класса xalloc. Например, вы можете написать следующее: 

    try { 
      р = new AnyClass(); 
      // ... прочие операторы, выделяющие память 
    }
    catch (xalloc х) { 
      cout « "Memory error:  « x.why() « endl; 
      exit(-1);    
    }


    15. Передовые средства C++ Borland® C++4.5     417 

    Класс xailoc выводится из другого класса — xmsg, также объявленного в 
заголовочном файле EXCEPT.H.  Класс-предок содержит текстовое сообщение типа 
string (которое описывается далее в этой главе). Для доступа к этой строке, как 
показано в предыдущем фрагменте кода в операторе catch, следует использовать 
функцию-член why(). Для определения размера неудавшегося запроса выделения 
памяти в байтах следует использовать функцию x.requested(). 
    Вы также можете использовать один блок try в функции main() для обработки 
всех возникающих ошибок. Например, вашу функцию main() можно структурировать 
следующим образом:   

    void main()	
    {	.                            . 
       for (;;) {  
          try { 
            Run();	// Запустить программу 
         } 
          catch (xailoc x) { 
            cout « "Out of memory" « endl; 
            exit(-1);        // Завершить программу или выполнить другие действия 
         } 
     }
  }

    В этом примере идея заключается в вызове единственной функции Run(), 
запускающей программу. Любые исключительные ситуации будут обрабатываться в 
функции Run() (или в другой функции, которую вызывает Run()) либо в функции 
main(), где вы можете обработать их с помощью оператора catch. 
    В листинге 15.19 модифицированной версии программы MEMERR из главы 12, 
демонстрируется использование исключительных ситуаций вместе с оператором new 
для "изящной" обработки ошибок нехватки памяти. Вы можете скомпилировать и 
запустить эту программу как DOS- или Easy Win-приложение. 

    ПРЕДУПРЕЖДЕНИЕ 

    Поскольку в MEMERR выделяется вся доступная память, следует сохранить все 
документы и закрыть другие приложения до запуска программы на выполнение. Хоть 
это и маловероятно, однако MEMERR при выполнении может привести к краху DOS, 
Windows и администраторов памяти. (Я уже сталкивался с подобными 
неприятностям.) Это справедливо вдвойне, если вы модифицировали параметры 
программы. Будьте особенно осторожны при запуске этой программы. 


    Листинг 15.19. MEMERR.CPP (исключительные ситуации и оператор new) 

    1:	#include <iostream.h> 
    2:	#include <new.h> 
    3:	#include <except. h> 
    4:	#include <cstring.h> 
    5:	#include <stdlib.h>	
    6: 
    7:	struct q { 
    8:	  int ia[1024];	// 2048-байтовая структура 
    9:	}; 
    10: 
    11:	char *pool;	// Зарезервированный указатель пула памяти 
    12: 
    13: 	int main() 
    14:	{	
    15:	 struct q *qp;	// Указатель на структуру q 
    16: 
    17:	try {	
    18: 	   pool = new char[512]; 	// Зарезервировать немного памяти          
    19:	}
    20:	catch (xalloc) {	
    21:	  cout « "Not enough	RAM to run program" « endl;   
    22:	  exit(-1); 
    23:	} 
    24: 
    25:	for (;;) {	// Выполнять следующие операторы "до бесконечности" 
    26:	  try {	// Блок try для обработки исключительных ситуаций 
    27:	     qp = new q;	// Выделить новую структуру типа q 
    28:	      cout « qp « '\n	';// Отобразить адрес структуры 
    29:	  } 
    30:	 catch (xailoc x) {	// Обработать исключительную ситуацию из new  
    31:	   delete pool;	// Освободить память для следующей строки             
    32:	   cout « "Error: "	« x.why() « endl; // Вывести сообщение     
    33:	   exit(-1);	// Завершить программу (или другие действия) 


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

    34: 	    } 
    35: 	  } 
    36: 	  return(0); // В этом месте будет предупреждение "Unreachable code" 
    37:	} 
    
    В программе MEMERR показано, что необходимо быть особенно осмотрительными 
при обработке ошибок нехватки памяти. В программе в "бесконечном" цикле 
выделяются 2048-байтовые структуры типа q до тех пор, пока оператор new не 
возбудит исключительную ситуацию xalloc. Оператор catch, обрабатывающий эту 
ошибку, вызывает функцию why() (наследованную из класса xmsg) для отображения 
сообщения о возникшей проблеме. 
    Проблема заключается в том, что при истощении ресурсов свободной памяти 
нет никакой гарантии, что сработает оператор вывода в строке 32. 
Действительно, если закончилась свободная память, вы должны быть особенно 
осторожны с операциями ввода-вывода и любыми другими действиями, для 
выполнения которых может потребоваться некоторое количество памяти. В таких 
случаях вызов функции why() и простой вывод строки могут привести к краху 
системы и потере данных. 
    Решение этой проблемы — резервирование небольшого количества памяти в 
начале выполнения программы. В случае возникновения ошибки нехватки памяти 
следует освободить этот резерв, чтобы дать возможность операторам вывода 
получить немного свободной памяти для работы. После этого вы можете завершить 
выполнение программы или, если вы решили продолжить работу, заново 
зарезервировать участок памяти на случай возникновения в будущем проблем с 
выделением свободной памяти. 

    ПРЕДУПРЕЖДЕНИЕ 
    
    Если вы хотите испробоватъ все, о чем говорилось в предыдущем разделе, измените 
размер резервируемой памяти с 512 байт на 1 или 2, скомпилируйте программу заново и 
запустите на выполнение, Модифицированная программа может привести к краху системы с 
сообщением General Protect Fault (GPF) (общее нарушение защиты) и к потере данных. 
Этот эксперимент следует проводить крайне осторожно 


    Динамическая информация о типе 

    Еще одна новинка ANSI C++ — динамическая информация о типе — предназначена 
именно для того, что вы ожидали: для предоставления информации о типе объекта 
во время выполнения программы. Вы можете использовать ее для сравнения типов 
объектов и для доступа к строкам, в которых они описываются 
    ANSI С и ранние версии C++ не имеют средств, эквивалентных динамической 
информации о типе. Раньше для определения размера объекта в байтах вы могли 
использовать sizeof (конечно, у вас и сейчас остается такая возможность), 
однако не существовало никаких стандартных способов определить, принадлежат ли 
объекты одному типу данных или нет. 
    Оператор typeid возвращает объект класса Type_info, который описывает тип 
объекта. Вы можете использовать typeid как для простых объектов встроенных 
типов, подобных int и double, так и для классов, структур, массивов и других 
ваших собственных типов. Вы можете также использовать оператор typeid для 
сравнения типов данных на эквивалентность (равно — не равно) или по алфавиту. 
    Для использования typeid следует включить в программу заголовочный файл 
TYPEINFO.H. Оператор возвращает объекты класса Type_info, в этом классе 
имеются функции-члены, к которым вы можете обратиться. Одна из этих функций, 
name(), предназначается для доступа к имени объекта. Можно использовать ее 
следующим образом: 

    int count; 
    cout « typeid(count).name() « endl; 

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

    int i; 
    int j;
    if (typeid(i) == typeid(j)) 
       cout « "i and j are the same types'  « endl; 

    Или же можно использовать оператор != для проверки на неравенство типов данных. 

    if ( typeid( i ) != typeid( j ) ) 
       cout « "i and j are not the same types" « endl, 

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

    if ( typeid(i).before(typeid(d) ) ) 
       cout « "i's type name is lexically before d's type name"; 


    15. Передовые средства С++    419 

    Если по некоторым причинам typeid не может создать объект класса Type_info 
(например, при разыменовывании нулевого указателя), оператор возбуждает 
исключительную ситуацию типа Bad_typeid. Следовательно при использовании 
typeid необходимо запрограммировать оператор catch, обрабатывающий эту ошибку. 

    ЗАМЕЧАНИЕ 

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

    В листинге 15.20 демонстрируется использование оператора typeid. Вы можете 
скомпилировать и запустить эту программу как DOS- или Easy Win-приложение. 
(Не обращайте внимания на предупреждения комлялятора о том, что объявленные 
переменные не используются.) 

    Листинг 15.20.  TYPEINFO.CPP (демонстрация оператора typeid) 

    1:	#include <iostream.h> 
    2:	#include <string.h> 
    3:	#include <typeinfo. h> 
    4:		
    5:	class AnyClass { 
    6:	public: 
    7:	   AnyClass(int c) { count = c; } 
    8:	private: 
    9:	   int count; 
    10:	}; 
    11: 
    12:	#pragma warn -use 
    13: 
    14:	int main() 
    15:	{ 
    16:	   char c; 
    17:	   int i; 
    18:	   double d; 
    19:	   AnyClass a(1); 
    20:	   AnyClass *pa = new AnyClass(2); 
    21: 
    22:	   cout « "Name  " « typeid(c).name() « endl; 
    23:	   cout « "Name  " « typeid(i).nameO « endl; 
    24:	   cout « "Name   « typeid(d).name() « endl; 
    25:	   cout « "Наве  " « typeid(a).name() « endl; 
    26:	   cout « "Name  " « typeid (pa).name() « endl; 
    27:	   cout « "Name  " « typeid (* pa).name () « endl; 
    28:	   cout « "Name  " « typeid(typeid( i ) ).nane()« endl; 
    29: 
    30:	   cout « endl; // Начать с новой строки 
    31: 
    32:	// Следующие строки не выводят корректные имена типов, 
    33:	// это следствие ошибки в ВС++ 4. 
    34:	/* 
    35:	   if (typeid(c) != typeid(d)) 
    36:	      cout « typeid(c).name() « "	and " 
    37:	      « typeid(d).name() « "are not equivalent." « endl; 
    38:	*/ 
    39: 
    40:	// Очевидно, необходимо использовать name() только один раз в
    41:	// операторе вывода в поток. Следующие строки работают правильно.    
    42: 
    43:	if (typeid(c) != typeid(d)) {
    44:           cout « typeid(c).nante() « " and cout « typeid(d).name() « " are not equivalent." « endl; 
    45:        }
    46:
    47:
    48:       // Сравнить обычный и динамический объекты одного и того же типа 
    49:      if ( typeid( a ) == typeid( *pa ) ) 
    50:         cout « "a and »pa are objects of the same types" « endl; 
    51:
    52:     // Демонстрация обработки исклвчитепьной ситуации Bad_typeid 
    53     try { 
    54:       cout « endl; 
    55:       cout « "Using a dereferenced null pointer typeid" « endl; 
    56:       int *p; // Define a pointer 
    57:       p =0;  // Assign null to the pointer 
 

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

    58:	     cout « "Name : " « typeid(*p).name() «	endl ; 
    59:	     cout « "Borland C++ bug. This SHould not	appear. " « endl; 
    60:	     p = new int; // Never executes 
    61:	     delete p;   // Never executes
    62:	  } 
    63:	  catch (Bad_typeid x) { 
    64:	     cout « "Error: typeid exception thrown"	« endl; 
    65:	      // . . . exit program or take other action	here 
    66:	  } 
    67: 
    68:	  delete pa; 
    69:	  return 0; 
    70:	} 

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

    cout « "Name :  " « typeid(pa).name() « endl; ;    cout « "Name :  " « typeid(*pa).name() « endl; 

   Оператор typeid корректно распознает ра как указатель типа AnyClass*. 
Он также корректно идентифицирует разыvенованный указатель *ра как объект типа AnyClass. 
    В следующей строке отображается Type_info — имя класса объекта, 
возвращаемое оператором typeid: 

    cout « "Name :  " « typeid(typeid(i)).name() « endl; 

    Несомненная ошибка в Borland C++ не позволяет использовать функцию name() в 
операторах вывода в поток обычным способом. Заключенный в комментарии участок 
листинга использует следующий оператор: 

    if (typeid(c) != typeid(d)) 
        cout « typeid(c).name() « " and " « typeid(d).name() « " are not equivalent." « endl; 

    Эти строки должны отображать сообщение "char and double are not 
equivalent", но вместо этого выводят "char and char are not equivalent". По 
неизвестным причинам второе использование функции name() неверно идентифицирует 
вещественную переменную двойной точности d как имеющую символьный тип. 
Использование двух отдельных операторов вывода в поток, похоже, помогает 
справиться с этой проблемой: 

    if (typeid(c) != typeid(d)) { 
       cout « typeid(с),nameО « " and "; 
       cout « typeid(d).name() « " are not equivalent." « endl; 
    } 


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


    Резюме 

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

    Глава 15. Передовые средства C++ 421 

   •   Потоки ввода-вывода реализованы классами, производными от класса ios, 
объявления которых содержатся в заголовочном файле IOSTREAM.H. Реализованы 
четыре объекта потоков ввода-вывода — cin, cout, cerr и clog. В ios и 
производных от него классах определены операторы ввода («)   и вывода (») в 
потоке для всех встроенных типов данных C++. 
   •   Манипуляторы  потоков  ввода-вывода,  объявленные  в  заголовочных  файлах 
iomanip.h.  и  iostream.h, позволяют форматировать ввод и вывод в потоковых 
операторах ввода-вывода. Вы также можете написать свои собственные манипуляторы. 
   •   В классах могут перегружаться потоковые операторы ввода-вывода для 
обеспечения возможности записи к чтения объекта класса из потока. В программах 
затем можно будет использовать объекты этих классов = потоковых операторах ввода-вывода. 
    • Шаблоны предназначаются для создания функций и классов общего назначения, 
детали реализации которых обеспечиваются их пользователями. 
    •  Прочие средства C++, описанные в этой главе, включают в себя указатели на 
функции-члены, статические функции-члены, размещение объектов  по  заданным  
адресам,  непосредственный вызов  деструкторов, инициализацию членов объекта 
класса, вложенные объявления классов и применение опции fast this. 
   • Функция может возбуждать исключительную ситуацию, сигнализируя о 
возникновении исключительного условия. Обычно исключительные ситуации 
предназначены для сообщений об ошибках, но они могут иметь и другое 
применение. Для использования исключительных ситуаций на практике следует 
поместить функцию, возбуждающую исключительную ситуацию, в блок try за 
которым должны следовать один или несколько операторов catch. 
   •  Объекты   исключительных   ситуаций   могут   быть   произвольных   типов, 
например   целочисленными значениями, символьными строками или объектами классов. 
   •   Если в программе возникли необработанные исключительные ситуации, то по 
умолчанию вызывается функция unexpected(). Обычно функция unexpected() вызывает 
функцию terminate(), которая, в свою очередь, вызывает функцию abort() для 
аварийного завершения программы. Вы можете заменить функции unexpected()   и   
terminate() своими функциями-обработчиками   исключительных   ситуаций.   
Заменить функцию abort() нельзя. 
    •  С помощью нового оператора typeid можно получить динамическую информацию о 
типе любого объекта. Оператор возвращает объект класса Type_info, в котором 
описывается тип данных объекта. Вы также можете использовать оператор typeid для 
сравнения типов объектов. 

    Упражнения 

    15.1. Напишите код, моделирующий работу двигателя внутреннего сгорания. 
Придумайте два класса, один с именем TEngine (двигатель), другой с именем 
TFuel (горючее), в котором имеется закрытый вещественный член двойной точности 
level, показывающий сколько "горючего" залито в баки объекта класса. С помощью 
друзей объявите ваши классы так, чтобы класс TEngine имел непосредственный 
доступ к закрытому члену level класса TFuel. 
    15.2. Добавьте функции перегрузки операторов умножения (*) и деления (/) в 
класс TStrOp в листинге 15.5. 
    15.3. Добавьте функции перегрузки операторов ++ и -- в класс TStrOp в 
листинге 15.5. Реализуйте префиксные и постфиксные формы этих операторов. 
    15.4. Объявите и реализуйте оператор преобразования объекта класса TStrOp 
к типу double в листинге 15.5. 
    15.5. Дан класс с именем TFruit (фрукт) и объект orange (апельсин) типа 
TFruit, используйте orange для инициализации нового объекта с именем 
grapefruit (грейпфрут). 
    15.6. Напишите прототип для конструктора копирования гипотетического 
класса TFruit из упражнения 15.5. 
    15.7. Напишите перегруженный оператор присваивания для класса TFruit из 
упражнения 15.5.	
    15.8. Напишите программу, в которой используется шаблонная функция minO из 
листинга 15.10. 
    15.9. Создайте базу данных, из 100 случайных целочисленных значений с 
помощью шаблонного класса TDatabase из листинга 15.12. С помощью цикла for и 
оператора вывода в поток отобразите значения объектов в 8-символьных столбцах. 

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

